From d33b5e1ced1833b4706eaae28ce8bf674495ff24 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Fri, 18 Sep 2026 15:39:12 +0800 Subject: [PATCH] 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). --- backend/app/api/dashboard.py | 33 +++++++++++++++++++++++++++--- frontend/src/views/MyDashboard.vue | 32 +++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index eddcc368..0ac975c6 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -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) diff --git a/frontend/src/views/MyDashboard.vue b/frontend/src/views/MyDashboard.vue index b206065f..c52556f7 100644 --- a/frontend/src/views/MyDashboard.vue +++ b/frontend/src/views/MyDashboard.vue @@ -56,6 +56,7 @@
📊 我的KPI + 数据截至 {{ dataAsOf }} 查看全部 →
@@ -70,10 +71,12 @@ {{ k.level === 'green' ? '达标' : k.level === 'yellow' ? '预警' : k.level === 'red' ? '危险' : '未评' }}
-
+
{{ k.actual_value == null ? '暂无数据' : periodActual(k) }}
-
{{ targetLabel(k) }} {{ k.target_value ?? '-' }}
+
+ 非当期{{ targetLabel(k) }} {{ k.target_value ?? '-' }} +
{ + const months = kpis.value + .map((k: any) => String(k.period || '')) + .filter((p: string) => MONTH_PERIOD_RE.test(p)) + .sort() + return months.length ? months[months.length - 1] : '' +}) +// 页头「数据截至 X 月」= 本页各卡中最新的月期间(无月期间时不出该文案,不编造时点) +const dataAsOf = computed(() => (latestMonth.value ? fmtPeriod(latestMonth.value) : '')) +// 非当期 = 该卡的值期间早于页面最新可得月(仍保留原期间标签,只加灰标提示,不隐藏) +function isStale(k: any): boolean { + const p = String(k.period || '') + return MONTH_PERIOD_RE.test(p) && !!latestMonth.value && p < latestMonth.value +} + function planIcon(p: any) { if (p.status === 'completed') return '✅' if (p.overdue) return '🔴' @@ -262,6 +282,8 @@ onMounted(async () => { } .section-action { font-size: 13px; color: #409eff; cursor: pointer; } .section-action:hover { text-decoration: underline; } +/* 数据截至 X 月(2026-09-18):说明本页各卡的最新可得期间,不改渲染结构 */ +.section-asof { font-size: 12px; color: #999; margin-right: 10px; } .loading-wrap { padding: 40px; } /* ── 空状态 ── */ @@ -310,6 +332,12 @@ onMounted(async () => { .kpi-value-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; } .kpi-actual { font-size: 24px; font-weight: 700; } .kpi-actual-empty { font-size: 15px; font-weight: 500; color: #999; } +/* 非当期灰标(2026-09-18):值期间落后于页面最新可得月时置灰,期间标签照旧显示 */ +.kpi-actual-stale { color: #999; } +.kpi-stale-tag { + font-size: 11px; color: #999; background: #f0f0f0; + padding: 0 6px; border-radius: 4px; margin-right: 6px; +} .kpi-target { font-size: 12px; color: #999; } /* ── 改善行动 ── */