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:
@@ -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不存在")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user