feat(warning-p0a): 资金缺口阈值外置 + 复算对拍 + 讲人话(数字守恒) + 数据就绪核验
ci/woodpecker/push/main Pipeline failed

P0-A(任务 fullstack-20261017-warning-capability-build-p0):
- cash_thresholds.py:阈值迁 per-entity(复用 alert_rules.rule_type=cash_gap_threshold,
  默认 20/10 万不动;无配置/非法/异常一律回落默认,fail-safe 不改预警行为)
- cash_forecast_engine.py:阈值改从配置读取,响应新增 threshold_source 明写来源
- verify_cash_gap_replay.py:同输入双跑逐字段 diff=0 + 手算 3/3 + 端点逐字段对拍(--live)
- cash_gap_narrative.py:复用 ai_brief.call_deepseek,只写原因/建议;
  断言生成文本数字集合 ⊆ 规则层数字集合,违例即不带文案上线
- audit_warning_data_readiness.py:G0 只读核验(cma_reader SELECT only),
  行数/期间/枚举 + 数据质量缺口(期初全 0、负期末、单批次、无员工维度表)

未 push(push 即部署,须项目侧书面放行);未改生产库/未动 cron。
This commit is contained in:
wecom-fullstack
2026-10-17 23:33:51 +08:00
parent a52faa90a6
commit 885cfddbff
6 changed files with 1335 additions and 13 deletions
+24
View File
@@ -21,6 +21,11 @@ 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
# G1(2026-10-17):资金缺口阈值的单一事实源(默认 20/10 万 + 参数校验)
from app.utils.cash_thresholds import (
CASH_GAP_RULE_TYPE,
validate_params as validate_cash_gap_params,
)
logger = logging.getLogger("alert_rules")
@@ -32,6 +37,7 @@ VALID_RULE_TYPES = (
"trend_down", # 环比下降
"forecast_deviation", # 预测偏差(升级2b 2026-08-25)
"customer_concentration", # 客户集中度(P2 2026-09-10)
"cash_gap_threshold", # 资金缺口阈值配置(G1 2026-10-17,配置行,不进预警生成链)
)
# ============================================================
@@ -130,6 +136,12 @@ def create_alert_rule(data: dict, db: Session = Depends(get_db), user=Depends(re
raise HTTPException(404, "KPI不存在")
if rule_type not in VALID_RULE_TYPES: # 升级2b: 预测偏差 / P2-20260910: 客户集中度
raise HTTPException(400, f"不支持的规则类型: {rule_type}")
# G1(2026-10-17):资金缺口阈值为引擎级参数,入库前校验数值(非法配置一律拒收,
# 防「配置写坏 → 预警静默回落默认值」的假生效)。
if rule_type == CASH_GAP_RULE_TYPE:
err = validate_cash_gap_params(data.get("params"))
if err:
raise HTTPException(400, err)
rule = AlertRule(
entity_id=entity_id,
@@ -162,6 +174,12 @@ def update_alert_rule(rule_id: int, data: dict, db: Session = Depends(get_db), e
for field in ("rule_type", "trigger_on", "enabled", "params"):
if field in data:
setattr(rule, field, data[field])
# G1(2026-10-17):改完仍是资金阈值配置行 → 参数必须合法(拒收非法值)
if rule.rule_type == CASH_GAP_RULE_TYPE:
err = validate_cash_gap_params(rule.params)
if err:
db.rollback()
raise HTTPException(400, err)
db.commit()
db.refresh(rule)
return {"data": {c.name: getattr(rule, c.name) for c in AlertRule.__table__.columns}}
@@ -189,6 +207,8 @@ def batch_create_rules(data: dict, db: Session = Depends(get_db), entity_id: int
rule_type = rule_data.get("rule_type", "static")
if rule_type not in VALID_RULE_TYPES:
continue
if rule_type == CASH_GAP_RULE_TYPE and validate_cash_gap_params(rule_data.get("params")):
continue
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
if not kpi or kpi.entity_id != entity_id:
continue
@@ -283,6 +303,10 @@ def run_all_alert_checks(db: Session = Depends(get_db), entity_id: int = Depends
for rule in rules:
try:
# G1(2026-10-17):cash_gap_threshold 是资金引擎的参数配置行,不是 KPI 预警规则,
# 不进 KPIValue 比价链(否则未来若绑定到有值的 KPI 会误生成预警)。
if rule.rule_type == CASH_GAP_RULE_TYPE:
continue
kpi = kpi_cache.get(rule.kpi_id)
if kpi is None:
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == rule.kpi_id).first()