From fd814462a2a631b5ff3a98c37bed178af1be23e3 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Thu, 27 Aug 2026 17:33:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A2=84=E7=AE=97=E6=8C=89=E6=88=98?= =?UTF-8?q?=E7=95=A5=E5=9C=B0=E5=9B=BE=E9=9A=94=E7=A6=BB(map=5Fid)=20?= =?UTF-8?q?=E2=80=94=20=E5=88=87=E6=8D=A2=E5=9C=B0=E5=9B=BE=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=8C=BA=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 预算数据无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, 部署 --- backend/app/api/budget.py | 12 ++++++++++-- backend/app/models/budget_plan.py | 1 + frontend/src/views/BudgetManagement.vue | 8 ++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py index 87ead453..215c0ce2 100644 --- a/backend/app/api/budget.py +++ b/backend/app/api/budget.py @@ -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), diff --git a/backend/app/models/budget_plan.py b/backend/app/models/budget_plan.py index 142682cf..7b560321 100644 --- a/backend/app/models/budget_plan.py +++ b/backend/app/models/budget_plan.py @@ -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="预算值") diff --git a/frontend/src/views/BudgetManagement.vue b/frontend/src/views/BudgetManagement.vue index 8b450554..70769363 100644 --- a/frontend/src/views/BudgetManagement.vue +++ b/frontend/src/views/BudgetManagement.vue @@ -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([]); 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(); (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 }