fix(cma): entity isolation batch4 (cash plan writes, budget ownership, read leaks, okr template)

P0-A cash.py: scope api_update_plan / api_delete_plan / api_complete_plan /
api_register_payment to session entity (cross-entity id -> 404, aligning with
the file's existing get_entity_id endpoints).
P0-B budget.py: write entity_id on BudgetPlan creation + require the KPI to
belong to the session entity; scope /comparison, /comparison/kpi/{id} and
/deviation-report reads to the session entity.
P1 notifications.py: explicit field whitelist (name/config/enabled) on
update_channel to close mass-assignment.
P1 read leaks scoped to session entity: bot_kpis list, data_quality check/stats
(KPI calibre only; quality-log table has no entity_id column), kpis causality
chain + associate-map (KPI and map ownership), dashboard my-kpis,
customer_dashboard list, kpi_governance audit, budget_generate candidates,
driver_budget history (+ entity_id on its write path), expenses stats
(+ entity_id on its write path).
P2 okr_templates.py: write entity_id on the StrategicMap created by apply.

tests: tests/test_cross_entity_isolation_batch4_20260921.py (30 cases;
RED before fix = 22 failed, GREEN after = 30 passed; full suite 1302 passed /
1 xfailed / 0 failed).
This commit is contained in:
Hermes CI Fix
2026-09-21 09:37:38 +08:00
parent 8c5853f64b
commit a0762b367f
14 changed files with 749 additions and 44 deletions
+24 -6
View File
@@ -77,8 +77,9 @@ def create_budget_plan(
data: dict,
db: Session = Depends(get_db),
current_user=Depends(require_auth),
entity_id: int = Depends(get_entity_id),
):
"""创建或更新单条预算计划"""
"""创建或更新单条预算计划(账套隔离 2026-09-21:KPI 归属校验 + 写会话账套)"""
kpi_id = data.get("kpi_id")
period = data.get("period")
budget_value = data.get("budget_value")
@@ -86,8 +87,11 @@ def create_budget_plan(
if not all([kpi_id, period, budget_value is not None]):
raise HTTPException(400, "缺少必要参数: kpi_id, period, budget_value")
# 检查KPI是否存在
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
# 检查KPI是否存在(账套隔离 2026-09-21:仅本账套 KPI,防把预算挂到对端 KPI)
kpi = db.query(KPIDefinition).filter(
KPIDefinition.id == kpi_id,
KPIDefinition.entity_id == entity_id,
).first()
if not kpi:
raise HTTPException(404, "KPI不存在")
@@ -112,6 +116,7 @@ def create_budget_plan(
return {"message": "预算已更新", "id": existing.id}
else:
plan = BudgetPlan(
entity_id=entity_id, # 账套隔离 2026-09-21:原漏写→落模型默认 1
kpi_id=kpi_id,
map_id=map_id,
period=period,
@@ -394,6 +399,7 @@ def get_deviation_report(
dimension: Optional[str] = Query(None),
alert_level: Optional[str] = Query(None),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""获取差异分析报告(汇总多个KPI的实际vs预算差异)"""
if period is None:
@@ -404,7 +410,10 @@ def get_deviation_report(
else:
period = datetime.now().strftime("%Y-%m")
query = db.query(KPIDefinition).filter(KPIDefinition.status == "active")
query = db.query(KPIDefinition).filter(
KPIDefinition.status == "active",
KPIDefinition.entity_id == entity_id, # 账套隔离 2026-09-21
)
if kpi_id:
query = query.filter(KPIDefinition.id == kpi_id)
if dimension:
@@ -640,6 +649,7 @@ def get_budget_comparison(
kpi_id: Optional[int] = Query(None),
year: Optional[int] = Query(None),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""
获取实际值vs预测值对比数据
@@ -685,7 +695,11 @@ def get_budget_comparison(
)
if kpi_id:
query = query.filter(BudgetPlan.kpi_id == kpi_id)
query = query.filter(BudgetPlan.period.in_(periods), BudgetPlan.status == "active")
query = query.filter(
BudgetPlan.period.in_(periods),
BudgetPlan.status == "active",
BudgetPlan.entity_id == entity_id, # 账套隔离 2026-09-21
)
budget_plans = query.all()
# 按KPI+期间索引
@@ -761,6 +775,7 @@ def get_kpi_comparison(
kpi_id: int,
year: Optional[int] = Query(None),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""
获取单个KPI的实际vs预测对比数据(用于图表展示)
@@ -771,7 +786,10 @@ def get_kpi_comparison(
now = datetime.now()
y = year or now.year
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
kpi = db.query(KPIDefinition).filter(
KPIDefinition.id == kpi_id,
KPIDefinition.entity_id == entity_id, # 账套隔离 2026-09-21
).first()
if not kpi:
raise HTTPException(404, "KPI不存在")