feat: 预算版本管理API — 修复前端'加载版本失败'(budgetApi.versions未定义+后端无端点)

- 后端: GET /budget/versions(版本列表) POST /versions/submit /versions/approve /versions/diff
- 前端: budgetApi.versions/versionSubmit/versionApprove/versionDiff
- 数据: budget_plans已有v2.0 active 24条
- 验证: 版本列表200(v2.0 24KPI 预算730万) 版本对比200
This commit is contained in:
Hermes CI Fix
2026-08-25 17:11:03 +08:00
parent 1c536cc5e1
commit 835ac10e95
2 changed files with 162 additions and 0 deletions
+157
View File
@@ -954,3 +954,160 @@ def budget_method_comparison(data: dict):
"recommended": recommended,
"recommended_name": "零基预算",
}
# ============ 预算版本管理 API2026-08-25 补充,修复前端"加载版本失败" ============
@router.get("/versions")
def list_budget_versions(
year: Optional[int] = Query(None, description="预算年份"),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""预算版本列表 — 按版本聚合统计"""
query = db.query(BudgetPlan).filter(BudgetPlan.entity_id == entity_id)
if year:
query = query.filter(BudgetPlan.budget_year == year)
rows = query.all()
# 按version+status聚合
version_map = {}
for r in rows:
key = (r.version or "v1.0", r.status or "active")
if key not in version_map:
version_map[key] = {
"version": r.version or "v1.0",
"status": r.status or "active",
"kpi_count": 0,
"total_budget": 0.0,
"periods": set(),
"updated_at": r.updated_at,
}
v = version_map[key]
v["kpi_count"] += 1
v["total_budget"] += r.budget_value or 0
v["periods"].add(r.period)
if r.updated_at and (v["updated_at"] is None or r.updated_at > v["updated_at"]):
v["updated_at"] = r.updated_at
result = []
for key, v in version_map.items():
result.append({
"version": v["version"],
"status": v["status"],
"kpi_count": v["kpi_count"],
"total_budget": round(v["total_budget"], 2),
"period_count": len(v["periods"]),
"year": year,
"updated_at": v["updated_at"].strftime("%Y-%m-%d %H:%M") if v["updated_at"] else "",
})
result.sort(key=lambda x: x["version"], reverse=True)
return result
@router.post("/versions/submit")
def submit_budget_version(
data: dict,
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""提交版本审批:active → submitted"""
version = data.get("version", "v1.0")
rows = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id,
BudgetPlan.version == version,
).all()
if not rows:
raise HTTPException(404, f"版本 {version} 不存在")
for r in rows:
r.status = "submitted"
db.commit()
return {"success": True, "version": version, "status": "submitted", "count": len(rows)}
@router.post("/versions/approve")
def approve_budget_version(
data: dict,
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""审批版本:submitted → approved/rejected"""
version = data.get("version", "v1.0")
action = data.get("action", "approved")
rows = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id,
BudgetPlan.version == version,
).all()
if not rows:
raise HTTPException(404, f"版本 {version} 不存在")
new_status = "approved" if action == "approved" else "rejected"
for r in rows:
r.status = new_status
db.commit()
return {"success": True, "version": version, "status": new_status, "count": len(rows)}
@router.post("/versions/diff")
def diff_budget_versions(
data: dict,
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""版本差异对比:v1.0 vs v2.0 逐KPI差异"""
version_a = data.get("version_a", "")
version_b = data.get("version_b", "")
year = data.get("year")
if not version_a or not version_b:
raise HTTPException(400, "需要 version_a 和 version_b")
def load_version(ver: str):
query = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id,
BudgetPlan.version == ver,
)
if year:
query = query.filter(BudgetPlan.budget_year == year)
return {r.kpi_id: r for r in query.all()}
va = load_version(version_a)
vb = load_version(version_b)
kpi_ids = set(va.keys()) | set(vb.keys())
kpis = db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()
kpi_map = {k.id: k for k in kpis}
diffs = []
total_a = total_b = 0.0
changed_count = 0
for kid in sorted(kpi_ids):
ra = va.get(kid)
rb = vb.get(kid)
val_a = ra.budget_value if ra else 0
val_b = rb.budget_value if rb else 0
total_a += val_a
total_b += val_b
k = kpi_map.get(kid)
diff = val_b - val_a
if abs(diff) > 0.001:
changed_count += 1
diffs.append({
"kpi_code": k.kpi_code if k else "",
"kpi_name": k.kpi_name if k else f"KPI-{kid}",
"version_a": round(val_a, 2),
"version_b": round(val_b, 2),
"diff": round(diff, 2),
})
diffs.sort(key=lambda x: -abs(x["diff"]))
return {
"summary": {
"version_a": version_a,
"version_b": version_b,
"kpi_total": len(kpi_ids),
"changed_count": changed_count,
"total_a": round(total_a, 2),
"total_b": round(total_b, 2),
"total_diff": round(total_b - total_a, 2),
},
"diffs": diffs,
}
+5
View File
@@ -178,6 +178,11 @@ export const budgetApi = {
autoDecompose: (data: any) => api.post('/budget/auto-decompose', data),
deviationReport: (params?: any) => api.get('/budget/deviation-report', { params }),
methodComparison: (data: any) => api.post('/budget/method-comparison', data),
// 版本管理
versions: (params?: any) => api.get('/budget/versions', { params }),
versionSubmit: (data: any) => api.post('/budget/versions/submit', data),
versionApprove: (data: any) => api.post('/budget/versions/approve', data),
versionDiff: (data: any) => api.post('/budget/versions/diff', data),
// 持续规划
getConfig: () => api.get('/budget/config'),
setConfig: (data: any) => api.post('/budget/config', data),