Files
cma-management/backend/app/utils/kpi_value_scope.py
T
Hermes CI Fix 5d7b3cc728
ci/woodpecker/push/main Pipeline failed
feat(h2-forecast): 实测区/预估区口径分离 —— 对外读路径默认只取实测区
- 新增 app/utils/kpi_value_scope.py(口径单一事实源:actual_only_clause / forecast_only_clause / is_forecast_row)
- dashboard.py: /kpis /my-kpis /finance-analysis /predict /my-dashboard / latest_kpi_value 默认剔预估,include_forecast=true 单列返回
- alert_rules.py: 动态阈值白名单去 estimated + 4 处读路径加实测区判据
- 前端 ForecastTag.vue 新增;Dashboard.vue / MyDashboard.vue 预估单列 + 标签(不与实测相加)

Task: finance-20261002-h2-forecast-separation-deploy-fullstack
2026-10-02 23:33:15 +08:00

68 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""KPI 取值分区口径 —— 「实测区 / 预估区」单一事实源(财务裁定 2026-10-02)
依据:`wecom-finance/pending-tasks/finance-20261002-h2-forecast-actual-separation-fullstack.md`
(任总 2026-10-02 11:35 CST DM 直令「把 H2 预测行与实际分离」)
财务口径裁定(逐字):
| 区 | 判据 | 展示 | 禁止 |
|---|---|---|---|
| 实测区 | `data_status` ∈ {verified, pending, calculated} 且 `source_type` ≠ forecast | 正常展示 | 不得含 estimated |
| 预估区 | `data_status = estimated` 或 `source_type = forecast` | 单列区 / 带「预估」标签 | 禁与实测相加、禁进趋势序列、禁作预警依据 |
实现口径说明(**边界决策,供复核**):
- 本模块按**预估区的补集**实现实测区(`data_status='estimated'` 或 `source_type='forecast'`
一律排除),而不是按 `{verified, pending, calculated}` 正向白名单实现。
理由两条:①裁定的「禁止」列只点名 `estimated`;②本单验收要求
「回归:实测区各端点返回值与改动前逐期等值(**仅少掉 2 行预估**)」,
正向白名单会连带排除 `invalid`(6) / `error`(6) 行,超出该回归口径。
若产品口径要求把 invalid/error 也逐出取数层,属**另一次口径变更**,须财务重新裁定。
- `NULL` 值按实测处理(`coalesce(col, '')`):历史行 `data_status` 可能为空,
空值不等于「预估」(fail-open 到既有行为,不静默吞行)。
约定:所有对外 `/api/cma/**` 的 KPIValue 读路径**默认只取实测区**;
仅当调用方显式传 `include_forecast=true` 时,预估行才**以独立字段**
(`forecast_value` / `is_forecast` / `forecast_rows`)返回,**不与实测相加**。
禁止在调用点各写一份 `data_status == 'estimated'` 字面量(避免多处漂移)。
"""
from sqlalchemy import and_, func, or_
from app.models import KPIValue
#: 预估区 —— 数据状态判据
FORECAST_DATA_STATUS = "estimated"
#: 预估区 —— 来源类型判据
FORECAST_SOURCE_TYPES = ("forecast",)
def _non_null_neq(column, value):
"""`coalesce(column, '') != value` —— NULL 视为「非该值」(不静默吞历史空值行)"""
return func.coalesce(column, "") != value
def actual_only_clause():
"""实测区 SQL 判据 = 预估区补集(对外读路径默认过滤条件)"""
return and_(
_non_null_neq(KPIValue.data_status, FORECAST_DATA_STATUS),
*[_non_null_neq(KPIValue.source_type, t) for t in FORECAST_SOURCE_TYPES],
)
def forecast_only_clause():
"""预估区 SQL 判据(`include_forecast=true` 时单列取数用)"""
return or_(
KPIValue.data_status == FORECAST_DATA_STATUS,
KPIValue.source_type.in_(FORECAST_SOURCE_TYPES),
)
def is_forecast_row(row) -> bool:
"""Python 侧同判据(单一事实源,避免调用点重写字面量)"""
if row is None:
return False
data_status = getattr(row, "data_status", None) or ""
source_type = getattr(row, "source_type", None) or ""
return (data_status == FORECAST_DATA_STATUS
or source_type in FORECAST_SOURCE_TYPES)