From 136fe3811f91993e69de5ed9af3a18c54a38409a Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Mon, 31 Aug 2026 07:44:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20auto-verify=20cron=E6=AE=8B=E7=95=99?= =?UTF-8?q?=E7=BC=BA=E9=99=B7=E4=BF=AE=E5=A4=8D+=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E6=8E=A5=E5=85=A5=20verify-cron-fix=20(auto?= =?UTF-8?q?=5Fverify=5Fcron=204=E9=A1=B9=E5=AF=B9=E9=BD=90verify.py:=20ent?= =?UTF-8?q?ity=5Fid=E8=BF=87=E6=BB=A4/period=E8=BF=87=E6=BB=A4/OKR?= =?UTF-8?q?=E5=B9=82=E7=AD=89/status=20completed;=20action=5Fplan=5Fweekly?= =?UTF-8?q?=20done->completed;=20action=5Fplans=E5=88=9B=E5=BB=BA=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90auto=5Fverify=5Frule)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/action_plans.py | 18 +++++++++- backend/scripts/action_plan_weekly.py | 2 +- backend/scripts/auto_verify_cron.py | 50 +++++++++++++++++++-------- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/backend/app/api/action_plans.py b/backend/app/api/action_plans.py index 22113df0..4728901d 100644 --- a/backend/app/api/action_plans.py +++ b/backend/app/api/action_plans.py @@ -9,7 +9,8 @@ from calendar import monthrange from app.database import get_db from app.deps import get_entity_id from app.auth_middleware import require_role, require_auth -from app.models import ActionPlan, KPIAlert, KPIDefinition, User, Objective, KR +from app.models import ActionPlan, KPIAlert, KPIDefinition, KPIValue, User, Objective, KR +from app.api.verify import build_auto_verify_rule logger = logging.getLogger("cma.action_plans") @@ -183,7 +184,22 @@ def create_plan( status="pending", progress=0, created_by=current_user.name or current_user.username, + auto_verify_rule=data.get("auto_verify_rule"), # 显式规则原样保存;None 时下面自动生成 ) + # 第三阶段接入(2026-08-31):创建 ActionPlan 自动生成验证规则 + # 请求体未传 auto_verify_rule 且关联 KPI 存在 → 复用 verify.build_auto_verify_rule 生成默认规则(不改变现有创建行为;auto_close 默认 false) + if not data.get("auto_verify_rule"): + baseline_value = data.get("baseline_value") + if baseline_value is None: + # 基线缺省时取 KPI 当前最新值(period <= 当前月),供 kpi_current_before 回填 + latest_kpi = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi_ent.id, + KPIValue.actual_value.isnot(None), + KPIValue.period <= datetime.now().strftime("%Y-%m"), + ).order_by(KPIValue.period.desc(), KPIValue.calculated_at.desc(), KPIValue.id.desc()).first() + if latest_kpi: + baseline_value = latest_kpi.actual_value + plan.auto_verify_rule = build_auto_verify_rule(kpi_ent, baseline_value=baseline_value) db.add(plan) db.commit() db.refresh(plan) diff --git a/backend/scripts/action_plan_weekly.py b/backend/scripts/action_plan_weekly.py index 08f88dbd..3de78190 100644 --- a/backend/scripts/action_plan_weekly.py +++ b/backend/scripts/action_plan_weekly.py @@ -27,7 +27,7 @@ def generate_weekly_report(): failed = sum(1 for p in plans if p.verify_status == "failed") escalated = sum(1 for p in plans if p.verify_status == "escalated") pending = sum(1 for p in plans if p.verify_status in ("pending", "retrying")) - done = sum(1 for p in plans if p.status == "done") + done = sum(1 for p in plans if p.status == "completed") # done 不在枚举(pending/in_progress/completed/cancelled),改 completed msg = ( f"📋 行动计划执行周报({datetime.now().strftime('%Y-%m-%d')})\n" diff --git a/backend/scripts/auto_verify_cron.py b/backend/scripts/auto_verify_cron.py index bee28a49..90eefe40 100644 --- a/backend/scripts/auto_verify_cron.py +++ b/backend/scripts/auto_verify_cron.py @@ -52,14 +52,25 @@ def run_auto_verify(): continue # 查最新KPI值 - kpi = db.query(KPIDefinition).filter( - KPIDefinition.kpi_code == kpi_code - ).order_by(KPIDefinition.id.desc()).first() + # 缺陷2残留修复(对齐 verify.py):从 plan 关联 KPI 向上取 entity_id,kpi_code 查询带 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 + 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.order_by(KPIDefinition.id.desc()).first() if not kpi: continue + # 缺陷3残留修复(对齐 verify.py):KPIValue 按 period <= 当前月过滤,跨月验证不取未来期间 + period_limit = now.strftime("%Y-%m") latest = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi.id - ).order_by(KPIValue.calculated_at.desc()).first() + KPIValue.kpi_id == kpi.id, + KPIValue.actual_value.isnot(None), + 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: continue @@ -74,11 +85,14 @@ def run_auto_verify(): 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 = "done" + plan.status = "completed" # 缺陷4残留修复:"done" 不在枚举(pending/in_progress/completed/cancelled),改 completed + plan.progress = 100 # 阶段3: OKR progress更新 - update_okr_progress(db, plan, +15) + 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: @@ -107,16 +121,24 @@ def run_auto_verify(): db.close() -def update_okr_progress(db, plan, delta): - """验证通过→更新所属OKR progress""" +def update_okr_progress(db, plan, delta, already_verified=False): + """验证通过→更新所属OKR progress + + 缺陷1残留修复(对齐 verify.py 2026-08-30):幂等防重复累加 + - already_verified=True(plan 已处于验证通过状态且 verified_at 非空)→ 跳过累加,保持原值 + """ okr_id = getattr(plan, "okr_id", None) or getattr(plan, "objective_id", None) if not okr_id: - return + return {"updated": False, "reason": "no_objective"} obj = db.query(Objective).filter(Objective.id == okr_id).first() - if obj: - current = obj.progress or 0 - obj.progress = min(current + delta, 100) - db.add(obj) + if not obj: + return {"updated": False, "reason": "objective_not_found"} + if already_verified: + return {"updated": False, "reason": "already_verified", "objective_id": obj.id, "progress": obj.progress or 0} + current = obj.progress or 0 + obj.progress = min(current + delta, 100) + db.add(obj) + return {"updated": True, "objective_id": obj.id, "progress": obj.progress} if __name__ == "__main__":