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__":