feat: v2.0 — maintenance module, AI analysis, station power fix

- Add full 检修维护中心 (6.4): 3-type work orders (消缺/巡检/抄表),
  asset management, warehouse, work plans, billing settlement
- Add AI智能分析 tab with LLM-powered diagnostics (StepFun + ZhipuAI)
- Add AI模型配置 settings page (provider, temperature, prompts)
- Fix station power accuracy: use API station total (station_power)
  instead of inverter-level computation — eliminates timing gaps
- Add 7 new DB models, 4 new API routers, 5 new frontend pages
- Migrations: 009 (maintenance expansion) + 010 (AI analysis)
- Version bump: 1.6.1 → 2.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-12 21:16:03 +08:00
parent 7947a230c4
commit f0f13faf00
30 changed files with 3325 additions and 52 deletions

View File

@@ -105,58 +105,107 @@ async def get_overview(db: AsyncSession = Depends(get_db), user: User = Depends(
@router.get("/realtime")
async def get_realtime_data(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""实时功率数据 - 获取最近的采集数据,按站去重防止重复计数"""
"""实时功率数据 - 优先使用API电站汇总数据(station_power),回退到按站去重计算"""
now = datetime.now(timezone.utc)
window_start = now - timedelta(minutes=20)
# Get latest power per station (dedup by device name prefix)
# Sungrow collectors report station-level power, so multiple devices
# sharing the same station (AP1xx = Phase 1, AP2xx = Phase 2) report
# identical values. GROUP BY station prefix and take MAX to avoid
# double-counting.
from sqlalchemy import text as sa_text
pv_ids = await _get_pv_device_ids(db)
hp_ids = await _get_hp_device_ids(db)
# PV power: dedup by station prefix
# ── PV power ──
# Strategy: Use station_power (direct from API station summary) if available.
# This matches iSolarCloud's own total exactly, avoiding any timing/grouping
# discrepancies from computing it ourselves.
# Fallback: group by device name prefix and take MAX (legacy method).
pv_power = 0
if pv_ids:
pv_q = await db.execute(
# Try station_power first (API station total, stored by collector)
station_q = await db.execute(
select(
func.substring(Device.name, 1, 3).label("station"),
EnergyData.device_id,
func.max(EnergyData.value).label("power"),
).select_from(EnergyData).join(
Device, EnergyData.device_id == Device.id
).where(
and_(
EnergyData.timestamp >= window_start,
EnergyData.data_type == "power",
EnergyData.data_type == "station_power",
EnergyData.device_id.in_(pv_ids),
)
).group_by(sa_text("1"))
).group_by(EnergyData.device_id)
)
pv_power = sum(row[1] or 0 for row in pv_q.all())
else:
pv_power = 0
station_rows = station_q.all()
# Heat pump power: dedup by station prefix
if station_rows:
# station_power is per-station total. Multiple devices sharing
# the same ps_id will have identical values, so we dedup by value
# to avoid double-counting. In practice, only ONE device per ps_id
# stores station_power (collector-level dedup), but we guard here
# too by taking distinct values.
seen_powers = set()
for row in station_rows:
val = round(row[1] or 0, 1)
if val > 0:
seen_powers.add(val)
pv_power = sum(seen_powers)
else:
# Fallback: legacy method — group by device name prefix, MAX per group
from sqlalchemy import text as sa_text
pv_q = await db.execute(
select(
func.substring(Device.name, 1, 3).label("station"),
func.max(EnergyData.value).label("power"),
).select_from(EnergyData).join(
Device, EnergyData.device_id == Device.id
).where(
and_(
EnergyData.timestamp >= window_start,
EnergyData.data_type == "power",
EnergyData.device_id.in_(pv_ids),
)
).group_by(sa_text("1"))
)
pv_power = sum(row[1] or 0 for row in pv_q.all())
# ── Heat pump power (same logic) ──
heatpump_power = 0
if hp_ids:
hp_q = await db.execute(
station_q = await db.execute(
select(
func.substring(Device.name, 1, 3).label("station"),
EnergyData.device_id,
func.max(EnergyData.value).label("power"),
).select_from(EnergyData).join(
Device, EnergyData.device_id == Device.id
).where(
and_(
EnergyData.timestamp >= window_start,
EnergyData.data_type == "power",
EnergyData.data_type == "station_power",
EnergyData.device_id.in_(hp_ids),
)
).group_by(sa_text("1"))
).group_by(EnergyData.device_id)
)
heatpump_power = sum(row[1] or 0 for row in hp_q.all())
else:
heatpump_power = 0
station_rows = station_q.all()
if station_rows:
seen_powers = set()
for row in station_rows:
val = round(row[1] or 0, 1)
if val > 0:
seen_powers.add(val)
heatpump_power = sum(seen_powers)
else:
from sqlalchemy import text as sa_text
hp_q = await db.execute(
select(
func.substring(Device.name, 1, 3).label("station"),
func.max(EnergyData.value).label("power"),
).select_from(EnergyData).join(
Device, EnergyData.device_id == Device.id
).where(
and_(
EnergyData.timestamp >= window_start,
EnergyData.data_type == "power",
EnergyData.device_id.in_(hp_ids),
)
).group_by(sa_text("1"))
)
heatpump_power = sum(row[1] or 0 for row in hp_q.all())
return {
"timestamp": str(now),