fix: auto-verify验证引擎4项缺陷修复 verify-fix (OKR幂等防重累加/entity_id多租户回填/period期间过滤/status done->completed)
This commit is contained in:
@@ -532,7 +532,14 @@ def verify_action_plan(
|
||||
):
|
||||
"""
|
||||
验证ActionPlan的执行结果
|
||||
|
||||
|
||||
⚠️ 双 verify 入口关系(2026-08-30 评审收敛,暂不重构):
|
||||
- 本函数(/api/cma/bot-bridge/verify/{action_plan_id}): 轻量版 — 仅读取KPI并按condition
|
||||
校验,记录 verify_log,不写KPIValue回填、不联动OKR、不发企微通知。供Bot桥接通道调用。
|
||||
- verify.py(/api/cma/verify/{plan_id}): 完整链路 — 回填KPIValue + OKR progress联动
|
||||
+ 企微通知。业务侧手动/自动重验走那个入口。
|
||||
- 两者行为不一致,勿混用。
|
||||
|
||||
1. 读取ActionPlan的auto_verify_rule
|
||||
2. 读取关联KPI的当前值
|
||||
3. 按condition校验
|
||||
|
||||
+52
-11
@@ -8,6 +8,12 @@ POST /api/cma/verify/{plan_id} 手动验证行动计划执行结果
|
||||
→ 验证通过 → 所属OKR progress +15%
|
||||
→ 通知任总 (send_wecom_message)
|
||||
|
||||
⚠️ 双 verify 入口关系(2026-08-30 评审收敛,暂不重构):
|
||||
- 本文件: /api/cma/verify/{plan_id} — 完整链路(回填KPIValue + OKR联动 + 企微通知)
|
||||
- bot_bridge_v2.py: /api/cma/bot-bridge/verify/{action_plan_id} — 轻量版(仅记 verify_log,
|
||||
不回填KPIValue、不联动OKR、不通知),供财务Bot/研学Bot桥接通道调用
|
||||
- 两者行为不一致,勿混用:Bot 通道走 bot_bridge_v2,业务侧手动/自动重验走本文件。
|
||||
|
||||
规则格式(新):
|
||||
{
|
||||
"kpi_code": "C_REBATE_RATE",
|
||||
@@ -86,14 +92,22 @@ def evaluate_rule(rule: dict, actual, kpi_data: dict = None) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def update_okr_progress(db: Session, plan: ActionPlan) -> dict:
|
||||
"""验证通过 → 所属OKR progress +15%(每通过1个KR)"""
|
||||
def update_okr_progress(db: Session, plan: ActionPlan, already_verified: bool = False, force_recalc: bool = False) -> dict:
|
||||
"""验证通过 → 所属OKR progress +15%(每通过1个KR)
|
||||
|
||||
缺陷1修复(2026-08-30):幂等防重复累加
|
||||
- already_verified=True(调用前 plan 已 verify_status=='passed' 且 verified_at 非空)
|
||||
→ 跳过累加,返回当前值(保持原值)
|
||||
- force_recalc=True 时强制重新累加(业务确需重验场景由调用方显式开启;默认 False)
|
||||
"""
|
||||
if not plan.objective_id:
|
||||
return {"updated": False, "reason": "no_objective"}
|
||||
obj = db.query(Objective).filter(Objective.id == plan.objective_id).first()
|
||||
if not obj:
|
||||
return {"updated": False, "reason": "objective_not_found"}
|
||||
before = obj.progress or 0
|
||||
if already_verified and not force_recalc:
|
||||
return {"updated": False, "reason": "already_verified", "objective_id": obj.id, "before": before, "after": before}
|
||||
obj.progress = min(100, before + 15)
|
||||
db.flush()
|
||||
return {"updated": True, "objective_id": obj.id, "before": before, "after": obj.progress}
|
||||
@@ -148,12 +162,26 @@ def build_auto_verify_rule(kpi, baseline_value=None, verify_after_days: int = 7)
|
||||
|
||||
|
||||
def backfill_kpi_value(db: Session, plan: ActionPlan, actual, rule: dict, source: str = "verify"):
|
||||
"""回填KPI当前值: 写入kpi_current_before/after + 新KPIValue记录"""
|
||||
"""回填KPI当前值: 写入kpi_current_before/after + 新KPIValue记录
|
||||
|
||||
缺陷2修复(2026-08-30)多租户隔离:
|
||||
- KPIValue 创建时设置 entity_id(从 plan 关联 KPI 定义取,即 plan.kpi_id → kpi_definitions.entity_id)
|
||||
- kpi_code 查询 KPIDefinition 时带 entity_id 过滤(防跨租户误匹配 kpi_code)
|
||||
"""
|
||||
kpi = None
|
||||
kpi_code = rule.get("kpi_code") if rule else None
|
||||
# 优先按验证规则指定的KPI编码查询;无规则时才回退到plan.kpi_id
|
||||
# 确定 plan 所属 entity_id(从 plan.kpi_id → KPIDefinition.entity_id 向上取)
|
||||
entity_id = None
|
||||
if plan.kpi_id:
|
||||
pkpi = db.query(KPIDefinition).filter(KPIDefinition.id == plan.kpi_id).first()
|
||||
if pkpi:
|
||||
entity_id = pkpi.entity_id
|
||||
# 优先按验证规则指定的KPI编码查询(带 entity_id 过滤);无规则时才回退到plan.kpi_id
|
||||
if kpi_code:
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code).first()
|
||||
q = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code)
|
||||
if entity_id is not None:
|
||||
q = q.filter(KPIDefinition.entity_id == entity_id)
|
||||
kpi = q.first()
|
||||
elif plan.kpi_id:
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == plan.kpi_id).first()
|
||||
|
||||
@@ -166,6 +194,7 @@ def backfill_kpi_value(db: Session, plan: ActionPlan, actual, rule: dict, source
|
||||
if kpi:
|
||||
new_val = KPIValue(
|
||||
kpi_id=kpi.id,
|
||||
entity_id=kpi.entity_id, # 缺陷2修复:多租户回填 entity_id
|
||||
period=datetime.now().strftime("%Y-%m"),
|
||||
actual_value=actual,
|
||||
source_type="verify",
|
||||
@@ -215,19 +244,27 @@ def verify_action_plan(plan_id: int, payload: dict, db: Session = Depends(get_db
|
||||
db.commit()
|
||||
return {"plan_id": plan_id, "verify_status": plan.verify_status, "passed": passed}
|
||||
|
||||
# 2. 缺省actual → 取KPI最新值
|
||||
# 2. 缺省actual → 取KPI最新值(缺陷3修复:按 period <= 当前月过滤,跨月验证不取历史期间;支持调用方显式传 period 覆盖,默认当前月)
|
||||
if actual is None:
|
||||
kpi = None
|
||||
kpi_code = rule.get("kpi_code")
|
||||
if plan.kpi_id:
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == plan.kpi_id).first()
|
||||
elif kpi_code:
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code).first()
|
||||
# 缺陷2修复:kpi_code 查询带 entity_id 过滤(防跨租户误匹配)
|
||||
q = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code)
|
||||
if plan.kpi_id:
|
||||
pkpi = db.query(KPIDefinition).filter(KPIDefinition.id == plan.kpi_id).first()
|
||||
if pkpi and pkpi.entity_id is not None:
|
||||
q = q.filter(KPIDefinition.entity_id == pkpi.entity_id)
|
||||
kpi = q.first()
|
||||
if kpi:
|
||||
period_limit = payload.get("period") or datetime.now().strftime("%Y-%m")
|
||||
latest = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id,
|
||||
KPIValue.actual_value.isnot(None),
|
||||
).order_by(KPIValue.calculated_at.desc(), KPIValue.id.desc()).first()
|
||||
KPIValue.period <= period_limit, # 缺陷3修复:只取当前月及之前的期间
|
||||
).order_by(KPIValue.period.desc(), KPIValue.calculated_at.desc(), KPIValue.id.desc()).first()
|
||||
if latest:
|
||||
actual = latest.actual_value
|
||||
|
||||
@@ -237,6 +274,10 @@ def verify_action_plan(plan_id: int, payload: dict, db: Session = Depends(get_db
|
||||
# 4. 回写KPI当前值
|
||||
backfill = backfill_kpi_value(db, plan, actual, rule, source=source)
|
||||
|
||||
# 缺陷1修复:调用前先记录 plan 是否已处于"验证通过"状态(防止重复累加 OKR progress)
|
||||
already_verified = bool(plan.verify_status == "passed" and plan.verified_at is not None)
|
||||
force_recalc = bool(payload.get("force_recalc", False))
|
||||
|
||||
# 5. 更新状态
|
||||
plan.verify_status = "passed" if passed else "failed"
|
||||
plan.verify_result = "pass" if passed else "fail"
|
||||
@@ -250,14 +291,14 @@ def verify_action_plan(plan_id: int, payload: dict, db: Session = Depends(get_db
|
||||
"note": note,
|
||||
}]
|
||||
if passed:
|
||||
plan.status = "done"
|
||||
plan.status = "completed" # 缺陷4修复:"done" 不在枚举(pending/in_progress/completed/cancelled),改 completed
|
||||
plan.progress = 100
|
||||
plan.verified_at = datetime.now()
|
||||
|
||||
# 6. OKR进度联动(验证通过 → +15%)
|
||||
# 6. OKR进度联动(验证通过 → +15%;缺陷1修复:已通过过的 plan 不再重复累加,force_recalc 可强制重算)
|
||||
okr_update = None
|
||||
if passed:
|
||||
okr_update = update_okr_progress(db, plan)
|
||||
okr_update = update_okr_progress(db, plan, already_verified=already_verified, force_recalc=force_recalc)
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user