feat: 预算按战略地图隔离(map_id) — 切换地图数据区分

问题: 预算数据无map_id, 两地图共用KPI时预算无法区分(切地图数据均显示)
修复:
- budget_plans 加 map_id 字段+索引
- list 支持 map_id 过滤(含NULL历史预算兼容迁移)
- create 收 map_id + 去重键含map_id(同KPI不同地图可各自预算)
- 前端: loadBudget/loadStrategyBudget 传 map_id, 保存带 map_id
- 端到端验证: 地图51 F_REVENUE 300(map_id=51) 地图51显示/地图32不显示 ✓
pytest 35+11 passed, 部署
This commit is contained in:
Hermes CI Fix
2026-08-27 17:33:57 +08:00
parent 3c64dc767e
commit fd814462a2
3 changed files with 15 additions and 6 deletions
+10 -2
View File
@@ -3,7 +3,7 @@
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from sqlalchemy import func, or_
from typing import Optional
from datetime import datetime
@@ -20,6 +20,7 @@ router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"],
@router.get("/plans")
def list_budget_plans(
kpi_id: Optional[int] = Query(None),
map_id: Optional[int] = Query(None, description="按战略地图过滤预算"),
period: Optional[str] = Query(None),
year: Optional[int] = Query(None),
version: Optional[str] = Query(None),
@@ -32,6 +33,9 @@ def list_budget_plans(
KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id
).filter(KPIDefinition.entity_id == entity_id)
if map_id:
# 按地图隔离: 显示该地图的预算 + 未绑定地图的历史预算(NULL, 兼容迁移)
query = query.filter(or_(BudgetPlan.map_id == map_id, BudgetPlan.map_id.is_(None)))
if kpi_id:
query = query.filter(BudgetPlan.kpi_id == kpi_id)
if period:
@@ -51,6 +55,7 @@ def list_budget_plans(
result.append({
"id": p.id,
"kpi_id": p.kpi_id,
"map_id": p.map_id,
"kpi_code": kpi.kpi_code if kpi else "",
"kpi_name": kpi.kpi_name if kpi else "",
"dimension": kpi.dimension if kpi else "",
@@ -88,12 +93,14 @@ def create_budget_plan(
year, month = period.split("-")
version = data.get("version", "v1.0")
map_id = data.get("map_id") # 预算归属地图(2026-08-27: 按地图隔离)
# 检查是否已有记录(去重)
# 检查是否已有记录(去重, 含map_id
existing = db.query(BudgetPlan).filter(
BudgetPlan.kpi_id == kpi_id,
BudgetPlan.period == period,
BudgetPlan.version == version,
BudgetPlan.map_id == map_id,
BudgetPlan.status == "active",
).first()
@@ -106,6 +113,7 @@ def create_budget_plan(
else:
plan = BudgetPlan(
kpi_id=kpi_id,
map_id=map_id,
period=period,
budget_value=budget_value,
budget_year=int(year),
+1
View File
@@ -9,6 +9,7 @@ class BudgetPlan(Base):
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
map_id = Column(Integer, nullable=True, comment="归属战略地图ID (预算按地图隔离 2026-08-27)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
period = Column(String(20), nullable=False, comment="预算期间 2026-05")
budget_value = Column(Float, nullable=False, comment="预算值")
+4 -4
View File
@@ -545,7 +545,7 @@ async function loadBudget() {
if (mapCodes.size === 0) { budgetList.value = []; total.value = 0; return }
noMap.value = false; loading.value = true
try {
const params: any = { page: page.value, page_size: pageSize.value, entity_id: getEntityId() }; if (filterYear.value) params.year = filterYear.value; if (filterMonth.value > 0) params.budget_month = filterMonth.value; if (searchKpi.value) params.keyword = searchKpi.value
const params: any = { page: page.value, page_size: pageSize.value, entity_id: getEntityId(), map_id: selectedMapId.value }; if (filterYear.value) params.year = filterYear.value; if (filterMonth.value > 0) params.budget_month = filterMonth.value; if (searchKpi.value) params.keyword = searchKpi.value
const r: any = await budgetApi.list(params); const d = r.data || r || []; const allPlans = (Array.isArray(d) ? d : (d.items || [])) as any[]
// 只保留选中地图关联KPI的预算记录
const plans = allPlans.filter((p: any) => mapCodes.has(p.kpi_code))
@@ -625,7 +625,7 @@ const strategyTree = ref<any[]>([]); const strategyFilterYear = ref(currentYear)
async function loadStrategyBudget() {
if (!selectedMap.value) return
try { const p: any = { budget_year: strategyFilterYear.value, entity_id: getEntityId() }; if (strategyFilterMonth.value > 0) p.budget_month = strategyFilterMonth.value; const r: any = await budgetApi.list(p); const d = r.data || r || []; const plans = (Array.isArray(d) ? d : (d.items || [])) as any[]; strategyAllBudgetPlans.value = plans
try { const p: any = { budget_year: strategyFilterYear.value, entity_id: getEntityId(), map_id: selectedMapId.value }; if (strategyFilterMonth.value > 0) p.budget_month = strategyFilterMonth.value; const r: any = await budgetApi.list(p); const d = r.data || r || []; const plans = (Array.isArray(d) ? d : (d.items || [])) as any[]; strategyAllBudgetPlans.value = plans
let dims = selectedMap.value.dimensions; if (typeof dims === 'string') { try { dims = JSON.parse(dims) } catch { dims = [] } }; if (!Array.isArray(dims) || dims.length === 0) { strategyTree.value = []; return }
const kr: any = await kpiApi.list({ page_size: 100, entity_id: getEntityId() }); const kd = kr.data || kr || []; const km = new Map<string, any>(); (Array.isArray(kd) ? kd : (kd.items || [])).forEach((k: any) => km.set(k.kpi_code, k))
const tree: any[] = []
@@ -640,12 +640,12 @@ function strategySelectDim(dimKey: string) { const dim = strategyTree.value.find
function strategyBatchSetSame() { strategySelectedKpis.value.forEach((k: any) => { k._editValue = strategyBatchValue.value; k._changed = (k._editValue !== (k.budget_value ?? 0)) }) }
async function strategyBatchSave() {
const ts = strategySelectedKpis.value.filter((k: any) => k._changed); if (ts.length === 0) { ElMessage.warning('没有需要保存的变更'); return }; strategyBatchSaving.value = true; let ok = 0; let no = 0
for (const r of ts) { try { if (r.plan_id) { await budgetApi.update(r.plan_id, { budget_value: r._editValue }) } else { const rr: any = await budgetApi.create({ kpi_id: r.kpi_id, period: r.period, budget_value: r._editValue, budget_year: strategyFilterYear.value, budget_month: strategyFilterMonth.value || 1 }); r.plan_id = rr.id }; r.budget_value = r._editValue; r._changed = false; ok++ } catch { no++ } }
for (const r of ts) { try { if (r.plan_id) { await budgetApi.update(r.plan_id, { budget_value: r._editValue }) } else { const rr: any = await budgetApi.create({ kpi_id: r.kpi_id, period: r.period, budget_value: r._editValue, budget_year: strategyFilterYear.value, budget_month: strategyFilterMonth.value || 1, map_id: selectedMapId.value }); r.plan_id = rr.id }; r.budget_value = r._editValue; r._changed = false; ok++ } catch { no++ } }
ElMessage.success(`批量保存完成:${ok} 成功,${no} 失败`); strategyBatchSaving.value = false; loadStrategyBudget()
}
async function saveStrategyKpi(row: any) {
row._saving = true
try { if (row.plan_id) { await budgetApi.update(row.plan_id, { budget_value: row._editValue }) } else { const rr: any = await budgetApi.create({ kpi_id: row.kpi_id, period: row.period, budget_value: row._editValue, budget_year: strategyFilterYear.value, budget_month: strategyFilterMonth.value || 1 }); row.plan_id = rr.id }; row.budget_value = row._editValue; row._changed = false; ElMessage.success('已保存') } catch (e) { ElMessage.error('保存失败') }
try { if (row.plan_id) { await budgetApi.update(row.plan_id, { budget_value: row._editValue }) } else { const rr: any = await budgetApi.create({ kpi_id: row.kpi_id, period: row.period, budget_value: row._editValue, budget_year: strategyFilterYear.value, budget_month: strategyFilterMonth.value || 1, map_id: selectedMapId.value }); row.plan_id = rr.id }; row.budget_value = row._editValue; row._changed = false; ElMessage.success('已保存') } catch (e) { ElMessage.error('保存失败') }
row._saving = false
}