fix(cma): cashflow statements importer + auto_verify/alert_generator observability

- add backend/scripts/import_cashflow_statements.py: load zxt cashflow stat (CNY) into cashflow_statements in 10k CNY, dry-run default, --apply gated

- auto_verify_cron: per-reason skip counters + legacy-condition rule now resolves kpi_code via plan.kpi_id (was unreachable) + --dry-run

- alert_generator: coverage self-check warns when current-month KPI values are absent (0 alerts != no trigger)

task: finance-20260916-cma-health-gaps-3items (items 1/2/3)
This commit is contained in:
Hermes CI Fix
2026-09-16 23:46:48 +08:00
parent 27d3e1cd73
commit 9b3a131070
3 changed files with 275 additions and 21 deletions
+65 -19
View File
@@ -27,7 +27,19 @@ def evaluate_condition(condition, actual, target):
return func(actual, target)
def run_auto_verify():
def run_auto_verify(dry_run=False):
"""扫描到期pending计划,按 auto_verify_rule 自动验证执行效果。
2026-09-16 修复(任务 finance-20260916-cma-health-gaps-3items · 项2):
① 统计口径透明化——原汇总行「共处理N个到期计划」把「扫描到但全部 continue 跳过」的
计划也算作"处理",导致连续 36 天「验证通过0个」无法区分空转与失效。现输出
逐原因计数(无规则 / 无KPI代码 / KPI定义未匹配 / 无KPI值 / 实际评估)。
② 旧表达式规则可达——原 `if not kpi_code: continue` 位于 evaluate_condition 的
旧表达式兼容分支之前,使 rule={"condition":"value < 75"}(无 kpi_code,仅挂
plan.kpi_id)永远无法验证,该兼容分支成死代码。现先用 plan.kpi_id 反查
kpi_code 再判定(只用于本轮取数,不回写 DB)。
③ 新增 --dry-run:只读复算、不写库(供巡检/复核留痕用)。
"""
db = next(get_db())
try:
now = datetime.now()
@@ -41,14 +53,26 @@ def run_auto_verify():
verified_count = 0
escalated_count = 0
messages = []
# 跳过原因计数(2026-09-16 新增,用于区分「确无对象」与「判据失效」)
skip = {"no_rule": 0, "no_kpi_code": 0, "kpi_def_missing": 0, "no_kpi_value": 0}
evaluated = 0
for plan in plans:
rule = plan.auto_verify_rule
if not rule or not isinstance(rule, dict):
skip["no_rule"] += 1
continue
kpi_code = rule.get("kpi_code")
if not kpi_code and plan.kpi_id:
# 旧表达式规则(只有 condition,无 kpi_code)→ 用计划关联 KPI 反查
link_kpi = db.query(KPIDefinition).filter(KPIDefinition.id == plan.kpi_id).first()
if link_kpi and link_kpi.kpi_code:
kpi_code = link_kpi.kpi_code
rule = dict(rule)
rule["kpi_code"] = kpi_code
if not kpi_code:
skip["no_kpi_code"] += 1
continue
# 查最新KPI值
@@ -63,6 +87,7 @@ def run_auto_verify():
q = q.filter(KPIDefinition.entity_id == entity_id)
kpi = q.order_by(KPIDefinition.id.desc()).first()
if not kpi:
skip["kpi_def_missing"] += 1
continue
# 缺陷3残留修复(对齐 verify.py):KPIValue 按 period <= 当前月过滤,跨月验证不取未来期间
period_limit = now.strftime("%Y-%m")
@@ -72,6 +97,7 @@ def run_auto_verify():
KPIValue.period <= period_limit,
).order_by(KPIValue.period.desc(), KPIValue.calculated_at.desc(), KPIValue.id.desc()).first()
if not latest or latest.actual_value is None:
skip["no_kpi_value"] += 1
continue
actual = latest.actual_value
@@ -80,43 +106,58 @@ def run_auto_verify():
retry_max = rule.get("retry_max", 3)
passed = evaluate_condition(condition, actual, target)
evaluated += 1
plan.kpi_current_before = rule.get("baseline_value")
plan.kpi_current_after = actual
if not dry_run:
plan.kpi_current_before = rule.get("baseline_value")
plan.kpi_current_after = actual
if passed:
# 缺陷1残留修复(对齐 verify.py):plan 已处于"验证通过"状态(passed + verified_at 非空)则跳过 OKR 累加,防重复累加
already_verified = bool(plan.verify_status == "passed" and plan.verified_at is not None)
plan.verify_status = "passed"
plan.verified_at = now
plan.status = "completed" # 缺陷4残留修复:"done" 不在枚举(pending/in_progress/completed/cancelled),改 completed
plan.progress = 100
# 阶段3: OKR progress更新
update_okr_progress(db, plan, +15, already_verified=already_verified)
if not dry_run:
plan.verify_status = "passed"
plan.verified_at = now
plan.status = "completed" # 缺陷4残留修复:"done" 不在枚举(pending/in_progress/completed/cancelled),改 completed
plan.progress = 100
# 阶段3: OKR progress更新
update_okr_progress(db, plan, +15, already_verified=already_verified)
messages.append(f"✅ 行动计划#{plan.id}验证通过: {plan.title} ({kpi_code}: {rule.get('baseline_value')}→{actual})")
verified_count += 1
else:
plan.verify_attempts = (plan.verify_attempts or 0) + 1
if plan.verify_attempts >= retry_max:
plan.verify_status = "escalated"
attempts = (plan.verify_attempts or 0) + (0 if dry_run else 1)
if not dry_run:
plan.verify_attempts = attempts
if attempts >= retry_max:
if not dry_run:
plan.verify_status = "escalated"
messages.append(f"🚨 行动计划#{plan.id}连续{retry_max}次验证失败,升级人工: {plan.title}")
escalated_count += 1
else:
plan.verify_status = "retrying"
messages.append(f"⚠️ 行动计划#{plan.id}验证失败(第{plan.verify_attempts}次),待重试: {plan.title}")
if not dry_run:
plan.verify_status = "retrying"
messages.append(f"⚠️ 行动计划#{plan.id}验证失败(第{attempts}次),待重试: {plan.title}")
db.commit()
if not dry_run:
db.commit()
# 汇总通知
if messages:
if messages and not dry_run:
summary = "\n".join(messages)
try:
send_wecom_message(f"📋 行动计划自动验证日报\n{summary}", title="Auto-Verify")
except Exception:
pass
print(f"[{now.strftime('%Y-%m-%d %H:%M')}] 验证通过{verified_count}个,升级{escalated_count}个,共处理{len(plans)}个到期计划")
return {"verified": verified_count, "escalated": escalated_count, "total": len(plans)}
# 汇总行:保留「验证通过N个」前缀(巡检脚本 cma-daily-healthcheck.py 按该字面量取数),
# 追加逐原因明细 —— 使「0 通过」可区分为「无验证对象」还是「判据失效」
print(f"[{now.strftime('%Y-%m-%d %H:%M')}] 验证通过{verified_count}个,升级{escalated_count}个,"
f"共处理{len(plans)}个到期计划 | 实际评估{evaluated}个;跳过明细: "
f"无规则{skip['no_rule']}个/无KPI代码{skip['no_kpi_code']}个/"
f"KPI定义未匹配{skip['kpi_def_missing']}个/无KPI值{skip['no_kpi_value']}个"
+ ("(dry-run 未写库)" if dry_run else ""))
return {"verified": verified_count, "escalated": escalated_count, "total": len(plans),
"evaluated": evaluated, "skip": skip, "dry_run": dry_run}
finally:
db.close()
@@ -142,4 +183,9 @@ def update_okr_progress(db, plan, delta, already_verified=False):
if __name__ == "__main__":
run_auto_verify()
import sys as _sys
_dry = "--dry-run" in _sys.argv
res = run_auto_verify(dry_run=_dry)
if _dry:
import json as _json
print(_json.dumps(res, ensure_ascii=False))