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 fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import func from sqlalchemy import func, or_
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime
@@ -20,6 +20,7 @@ router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"],
@router.get("/plans") @router.get("/plans")
def list_budget_plans( def list_budget_plans(
kpi_id: Optional[int] = Query(None), kpi_id: Optional[int] = Query(None),
map_id: Optional[int] = Query(None, description="按战略地图过滤预算"),
period: Optional[str] = Query(None), period: Optional[str] = Query(None),
year: Optional[int] = Query(None), year: Optional[int] = Query(None),
version: Optional[str] = Query(None), version: Optional[str] = Query(None),
@@ -32,6 +33,9 @@ def list_budget_plans(
KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id
).filter(KPIDefinition.entity_id == entity_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: if kpi_id:
query = query.filter(BudgetPlan.kpi_id == kpi_id) query = query.filter(BudgetPlan.kpi_id == kpi_id)
if period: if period:
@@ -51,6 +55,7 @@ def list_budget_plans(
result.append({ result.append({
"id": p.id, "id": p.id,
"kpi_id": p.kpi_id, "kpi_id": p.kpi_id,
"map_id": p.map_id,
"kpi_code": kpi.kpi_code if kpi else "", "kpi_code": kpi.kpi_code if kpi else "",
"kpi_name": kpi.kpi_name if kpi else "", "kpi_name": kpi.kpi_name if kpi else "",
"dimension": kpi.dimension if kpi else "", "dimension": kpi.dimension if kpi else "",
@@ -88,12 +93,14 @@ def create_budget_plan(
year, month = period.split("-") year, month = period.split("-")
version = data.get("version", "v1.0") version = data.get("version", "v1.0")
map_id = data.get("map_id") # 预算归属地图(2026-08-27: 按地图隔离)
# 检查是否已有记录(去重) # 检查是否已有记录(去重, 含map_id
existing = db.query(BudgetPlan).filter( existing = db.query(BudgetPlan).filter(
BudgetPlan.kpi_id == kpi_id, BudgetPlan.kpi_id == kpi_id,
BudgetPlan.period == period, BudgetPlan.period == period,
BudgetPlan.version == version, BudgetPlan.version == version,
BudgetPlan.map_id == map_id,
BudgetPlan.status == "active", BudgetPlan.status == "active",
).first() ).first()
@@ -106,6 +113,7 @@ def create_budget_plan(
else: else:
plan = BudgetPlan( plan = BudgetPlan(
kpi_id=kpi_id, kpi_id=kpi_id,
map_id=map_id,
period=period, period=period,
budget_value=budget_value, budget_value=budget_value,
budget_year=int(year), budget_year=int(year),
+1
View File
@@ -9,6 +9,7 @@ class BudgetPlan(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)") 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") kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
period = Column(String(20), nullable=False, comment="预算期间 2026-05") period = Column(String(20), nullable=False, comment="预算期间 2026-05")
budget_value = Column(Float, nullable=False, comment="预算值") 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 } if (mapCodes.size === 0) { budgetList.value = []; total.value = 0; return }
noMap.value = false; loading.value = true noMap.value = false; loading.value = true
try { 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[] const r: any = await budgetApi.list(params); const d = r.data || r || []; const allPlans = (Array.isArray(d) ? d : (d.items || [])) as any[]
// 只保留选中地图关联KPI的预算记录 // 只保留选中地图关联KPI的预算记录
const plans = allPlans.filter((p: any) => mapCodes.has(p.kpi_code)) 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() { async function loadStrategyBudget() {
if (!selectedMap.value) return 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 } 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 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[] = [] 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)) }) } function strategyBatchSetSame() { strategySelectedKpis.value.forEach((k: any) => { k._editValue = strategyBatchValue.value; k._changed = (k._editValue !== (k.budget_value ?? 0)) }) }
async function strategyBatchSave() { 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 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() ElMessage.success(`批量保存完成:${ok} 成功,${no} 失败`); strategyBatchSaving.value = false; loadStrategyBudget()
} }
async function saveStrategyKpi(row: any) { async function saveStrategyKpi(row: any) {
row._saving = true 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 row._saving = false
} }