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:
@@ -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),
|
||||||
|
|||||||
@@ -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="预算值")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user