feat: 路线图R1决策建议一键落地+R2机会推送+R5预算闭环
R1(P0): AI建议一键应用到KPI/预算/行动方案
- 新表 ai_suggestions + AISuggestion 模型(init_db自动建)
- /api/cma/ai/suggestions CRUD + /{id}/apply(复用kpis/budget/action_plans) + dismiss
- 应用写 OperationLog(action=ai_suggestion_apply, detail含suggestion_id/before/after)
- 规则驱动建议生成 generate_rule_suggestions(低执行率/高执行率/预算超支/pending预警)
- 幂等: 同entity+type+target_id+title+unapplied不重复建; applied后拒绝重复应用
- 前端: Dashboard AI面板建议卡(应用到/忽略) + 建议中心页 /ai-suggestions
R2(P1): 数据找人扩大-机会类推送
- scripts/opportunity_detector.py: KPI向好(执行率>110%)/预算余量(<70%且actual>0)/预测上行
- scripts/daily_push.py: 异常+机会 每日9:15推企微(8800/send, --dry-run调试)
- crontab: 15 9 * * * (alert_generator 9:00之后)
R5(P0): 预算闭环加固
- auto-decompose批量幂等: 只取年度行(period=YYYY-00)+同KPI多版本取一行
- scripts/closed_loop_check.py: 预算执行率异常→检查现金流/行动同步→缺失提示+报告
- scripts/verify_decompose_idempotent.py: 幂等验证脚本
测试: test_ai_suggestions(10例)+test_roadmap_r2r5(14例); 修test_budget幂等契约适配年度行
全量: 673 passed
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""验证年度预算分解幂等 — R5 (2026-08-30)
|
||||
|
||||
调用 /api/cma/budget/auto-decompose 3 次,对比月度预算值是否不变。
|
||||
用法: cd /root/cma-management/backend && ./venv/bin/python3 scripts/verify_decompose_idempotent.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
BASE = os.getenv("CMA_BASE", "http://127.0.0.1:8010")
|
||||
|
||||
|
||||
def post(path, body, token=None):
|
||||
req = urllib.request.Request(
|
||||
BASE + path,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}" if token else ""},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def main():
|
||||
# 登录(账套模式必须 entity_id)
|
||||
login = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1})
|
||||
token = login.get("token") or login.get("access_token")
|
||||
if not token:
|
||||
print("❌ 登录失败:", login)
|
||||
sys.exit(1)
|
||||
print("✅ 登录成功")
|
||||
|
||||
runs = []
|
||||
for i in range(3):
|
||||
r = post("/api/cma/budget/auto-decompose", {"year": 2026, "method": "equal", "version": "v1.0"}, token)
|
||||
print(f"第{i+1}次: {r.get('message', '')} created={r.get('created', 0)}")
|
||||
# 提取 (kpi_id -> monthly tuple)
|
||||
snap = {}
|
||||
for res in r.get("results", []):
|
||||
snap[res["kpi_id"]] = tuple(res.get("monthly") or [])
|
||||
runs.append(snap)
|
||||
|
||||
# 对比三次结果
|
||||
same = runs[0] == runs[1] == runs[2]
|
||||
print(f"\n三次结果一致: {'✅ 是(幂等)' if same else '❌ 否(不幂等)'}")
|
||||
if not same:
|
||||
for i in range(1, 3):
|
||||
for kid in runs[0]:
|
||||
if runs[0].get(kid) != runs[i].get(kid):
|
||||
print(f" KPI {kid} 第1次={runs[0].get(kid)} 第{i+1}次={runs[i].get(kid)}")
|
||||
sys.exit(0 if same else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user