fix(cma): my-dashboard KPI period caliber (monthly period priority) + as-of/stale marks

my_dashboard picked the row with max calculated_at, so batch backfills made stale periods win (revenue/net-profit showed 2026-07, rebate rate 2026-04). Now prefer the latest monthly period, fall back to calculated_at. Frontend adds data-as-of label and stale tag; not deployed yet (push gated by another pending commit).
This commit is contained in:
Hermes CI Fix
2026-09-18 15:39:12 +08:00
parent df2be301b3
commit d33b5e1ced
2 changed files with 60 additions and 5 deletions
+30 -3
View File
@@ -565,6 +565,35 @@ def predict_kpis(db: Session = Depends(get_db), entity_id: int = Depends(get_ent
# ── 个人工作台 ──────────────────────────────
# KPIValue 取数口径(2026-09-18 工作台「我的KPI」期间缺口修正)
# 原实现(my_dashboard 内联)按 calculated_at 倒序取「最近写入」的一条:批量补录/复采会把
# **旧期间**的行写得更晚 → 卡片显示过期期间。实测 entity1:F_REVENUE/F_NET_PROFIT 的
# 2026-07 行写入时刻(2026-09-14 17:01:22, id=2128/2130)晚于 2026-08 行(17:00:30, id=2125/2127)
# → 取到 7 月;C_REBATE_RATE 1–4 月补录行 calculated_at 并列(2026-08-24 17:17:38) → 取到 4 月。
# 新口径:优先取「月期间 YYYY-MM」中最新的一期(月期是采集/录入的常规粒度);若该 KPI 只有
# H1/H2/FY 等汇总期行,则回退到原按 calculated_at 的取法(不改变无月期 KPI 的行为)。
# 注意:**不能**只按 period 字典序排 —— '2026-H1'/'2026-H2' 字典序大于 '2026-08',会把
# 季度汇总行/半年度预估值(forecast)当成"最新"。
_MONTH_PERIOD_REGEX = r"^[0-9]{4}-(0[1-9]|1[0-2])$"
def latest_kpi_value(db: Session, kpi_id: int) -> Optional[KPIValue]:
"""该 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())
.first()
)
if monthly is not None:
return monthly
return (
db.query(KPIValue)
.filter(KPIValue.kpi_id == kpi_id)
.order_by(KPIValue.calculated_at.desc(), KPIValue.id.desc())
.first()
)
@router.get("/my-dashboard")
def my_dashboard(
@@ -613,9 +642,7 @@ def my_dashboard(
kpi_list = []
for k in all_kpis:
latest_v = db.query(KPIValue).filter(
KPIValue.kpi_id == k.id
).order_by(KPIValue.calculated_at.desc()).first()
latest_v = latest_kpi_value(db, k.id)
actual = latest_v.actual_value if latest_v else None
target = kpi_target_by_frequency(k)