- 新增 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
This commit is contained in:
@@ -18,6 +18,9 @@ from app.utils.oplog import log_op
|
||||
# 裁定 R1(2026-09-19 财务口径):demo 账套不参与预警生成/遍历(单一事实源)
|
||||
# 裁定 R5(2026-09-19 财务口径):期间非规整月 YYYY-MM → 该 kpi_alerts 行不入库(生成侧守卫)
|
||||
from app.utils.alert_entity_scope import guard_alert_period, is_excluded_entity
|
||||
# 「实测区 / 预估区」分区口径(财务裁定 2026-10-02):**预估行禁作预警依据**——
|
||||
# 本文件的取数读路径一律加 actual_only_clause(),动态阈值白名单去掉 estimated。
|
||||
from app.utils.kpi_value_scope import actual_only_clause
|
||||
|
||||
logger = logging.getLogger("alert_rules")
|
||||
|
||||
@@ -317,11 +320,12 @@ def run_all_alert_checks(db: Session = Depends(get_db), entity_id: int = Depends
|
||||
alerts_generated += 1
|
||||
continue
|
||||
|
||||
# 获取最新值
|
||||
# 获取最新值(仅实测区:预估行禁作预警依据,财务裁定 2026-10-02)
|
||||
latest_value = value_cache.get(rule.kpi_id)
|
||||
if latest_value is None:
|
||||
latest_value = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == rule.kpi_id,
|
||||
actual_only_clause(),
|
||||
).order_by(KPIValue.period.desc()).first()
|
||||
if latest_value:
|
||||
value_cache[rule.kpi_id] = latest_value
|
||||
@@ -410,7 +414,8 @@ def calculate_dynamic_thresholds(db: Session = Depends(get_db), entity_id: int =
|
||||
values = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id,
|
||||
KPIValue.actual_value.isnot(None),
|
||||
KPIValue.data_status.in_(["verified", "estimated"]),
|
||||
# 白名单去掉 estimated(财务裁定 2026-10-02):预估行不参与动态阈值计算
|
||||
KPIValue.data_status.in_(["verified"]),
|
||||
KPIValue.period < current_period,
|
||||
).order_by(KPIValue.period.desc()).limit(3).all()
|
||||
|
||||
@@ -521,10 +526,11 @@ def _check_trend(kpi_id: int, value: float, direction: str, params: dict, db: Se
|
||||
"""趋势检查 — 环比变化"""
|
||||
threshold_pct = params.get("threshold_pct", 10)
|
||||
|
||||
# 获取上月值
|
||||
# 获取上月值(仅实测区:伪期间预估行会因字符串序顶掉上月实测值)
|
||||
prev_value = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi_id,
|
||||
KPIValue.actual_value.isnot(None),
|
||||
actual_only_clause(),
|
||||
).order_by(KPIValue.period.desc()).offset(1).limit(1).first()
|
||||
|
||||
if not prev_value or not prev_value.actual_value or prev_value.actual_value == 0:
|
||||
@@ -753,9 +759,11 @@ def run_forecast_deviation_check(
|
||||
if not log or log.forecast_value is None:
|
||||
continue
|
||||
# 该预测期的实际值(同period匹配;兼容 2026-H1 等半年度)
|
||||
# 仅实测区:预估行不得与预测值自比(否则偏差恒 0,预警失效)
|
||||
actual = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == rule.kpi_id,
|
||||
KPIValue.period == log.period,
|
||||
actual_only_clause(),
|
||||
).order_by(KPIValue.id.desc()).first()
|
||||
if not actual or not actual.actual_value:
|
||||
continue
|
||||
@@ -878,9 +886,11 @@ def _check_customer_concentration(kpi_id: int, params: dict, db: Session, entity
|
||||
detail = f"{head}收入占比={share}%(合计{total:.0f}元,{rng[0]}~{rng[1]}:{items})"
|
||||
|
||||
if share is None and source in ("auto", "kpi"):
|
||||
# 仅实测区:预估行禁作预警依据(财务裁定 2026-10-02)
|
||||
latest = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi_id,
|
||||
KPIValue.actual_value.isnot(None),
|
||||
actual_only_clause(),
|
||||
).order_by(KPIValue.period.desc(), KPIValue.id.desc()).first()
|
||||
if latest:
|
||||
share = round(float(latest.actual_value), 2)
|
||||
|
||||
Reference in New Issue
Block a user