diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index b894f9c0..60da5620 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -1,7 +1,7 @@ """驾驶舱 API v2 — 支持时间区间""" from fastapi import APIRouter, Depends, Query, Request, HTTPException from sqlalchemy.orm import Session -from sqlalchemy import func, or_ +from sqlalchemy import case, func, or_ from datetime import datetime, timedelta from typing import Optional from app.database import get_db @@ -580,13 +580,33 @@ def predict_kpis(db: Session = Depends(get_db), entity_id: int = Depends(get_ent # 季度汇总行/半年度预估值(forecast)当成"最新"。 _MONTH_PERIOD_REGEX = r"^[0-9]{4}-(0[1-9]|1[0-2])$" +# 主值优先(2026-09-18 同期间重复行判据收口,项目单 project-20260918-kpi-row-priority) +# 同一期间可能并存两行:①主值(source_type='ledger' / data_status='verified',结账/复核口径) +# ②自动归集重复行(source_type='auto_collect',备注自述「与主值为同一事实重复行」)。 +# 原判据仅看写入时刻:重复行由复采批量写入、calculated_at 更晚 ⇒ 会被选中(实测 entity1 +# 2026-08 的 F_REVENUE/F_NET_PROFIT:2136/2127 auto_collect 写于 09-14,晚于主值 1822/2102)。 +# 今日两行数值一致(Δ=0)故用户不可见;一旦重复行与主值漂移,卡片会静默显示重复行的值。 +# 故在**同一期间内**加「主值优先」:verified(已复核)> ledger(账面主值)> 其余。 +# 注意:跨期间仍以 period 倒序为第一判据(不改变 d33b5e1 的期间口径——当期只有未复核行时, +# 不会回退到上一期已复核行)。 +_MAIN_VALUE_PRIORITY = case( + (KPIValue.data_status == "verified", 0), + (KPIValue.source_type == "ledger", 1), + else_=2, +) + def latest_kpi_value(db: Session, kpi_id: int) -> Optional[KPIValue]: - """该 KPI 的最新可用值:月期间优先,无月期间行时回退 calculated_at 倒序。""" + """该 KPI 的最新可用值:月期间优先(同期内主值优先),无月期间行时回退 calculated_at 倒序。""" monthly = ( db.query(KPIValue) .filter(KPIValue.kpi_id == kpi_id, KPIValue.period.op("REGEXP")(_MONTH_PERIOD_REGEX)) - .order_by(KPIValue.period.desc(), KPIValue.calculated_at.desc(), KPIValue.id.desc()) + .order_by( + KPIValue.period.desc(), + _MAIN_VALUE_PRIORITY, + KPIValue.calculated_at.desc(), + KPIValue.id.desc(), + ) .first() ) if monthly is not None: