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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user