fix(cma): my-dashboard KPI 取数同期内主值优先(重复行不再被选中)

同一期间并存两行(主值 ledger/verified 与自动归集重复行 auto_collect/error)时,
原判据按 calculated_at 倒序会取到写入更晚的重复行;今日两行数值一致故不可见,
漂移后卡片会静默显示重复行值。故在月期分支的排序中加「主值优先」:
verified > ledger > 其余,置于 period 倒序之后(跨期口径不变)。

范围: backend/app/api/dashboard.py 的 latest_kpi_value() 单函数(含 case 导入)。
无 DB 写入、不动采集器/派生函数/cron/口径定义/前端。

单号: project-20260918-kpi-row-priority
This commit is contained in:
fullstackBot
2026-09-21 14:56:43 +08:00
committed by Hermes CI Fix
parent b4f94a12b3
commit 27ea7e7f53
+23 -3
View File
@@ -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: