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:
@@ -62,11 +62,13 @@ def list_bot_kpis(
|
||||
source: str = Query("finance-bot", description="Bot标识"),
|
||||
period: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取某Bot的所有KPI(含评分)"""
|
||||
"""获取某Bot的所有KPI(含评分)(账套隔离 2026-09-21:仅本账套,对齐 update_bot_kpi_value)"""
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.bot_source == source,
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).order_by(KPIDefinition.kpi_code).all()
|
||||
|
||||
if not kpis:
|
||||
|
||||
@@ -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不存在")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition, BudgetPlan, KPIValue, OperationLog
|
||||
import json
|
||||
@@ -75,12 +76,16 @@ def _calc_budget(kpi: KPIDefinition) -> dict:
|
||||
def get_kpi_budget_candidates(
|
||||
year: int = None,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取可用于生成预算的KPI列表,按类型分类"""
|
||||
"""获取本账套可用于生成预算的KPI列表,按类型分类(账套隔离 2026-09-21)"""
|
||||
if not year:
|
||||
year = datetime.now().year
|
||||
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).all()
|
||||
|
||||
# 获取每个KPI的最新实际值
|
||||
latest_values = {}
|
||||
|
||||
+25
-12
@@ -198,9 +198,12 @@ def api_create_plan(request: Request, data: dict, db: Session = Depends(get_db))
|
||||
|
||||
|
||||
@router.put("/plans/{plan_id}")
|
||||
def api_update_plan(plan_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""更新收付款计划"""
|
||||
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
|
||||
def api_update_plan(plan_id: int, data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""更新收付款计划(账套隔离 2026-09-21:跨账套 id → 404)"""
|
||||
plan = db.query(CashPlan).filter(
|
||||
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
|
||||
).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "计划不存在")
|
||||
if "plan_type" in data:
|
||||
@@ -248,9 +251,12 @@ def api_update_plan(plan_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.delete("/plans/{plan_id}")
|
||||
def api_delete_plan(plan_id: int, db: Session = Depends(get_db)):
|
||||
"""删除收付款计划"""
|
||||
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
|
||||
def api_delete_plan(plan_id: int, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""删除收付款计划(账套隔离 2026-09-21:跨账套 id → 404)"""
|
||||
plan = db.query(CashPlan).filter(
|
||||
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
|
||||
).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "计划不存在")
|
||||
db.delete(plan)
|
||||
@@ -259,9 +265,12 @@ def api_delete_plan(plan_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/plans/{plan_id}/complete")
|
||||
def api_complete_plan(plan_id: int, db: Session = Depends(get_db)):
|
||||
"""标记计划为已完成(收款到账/付款完成)"""
|
||||
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
|
||||
def api_complete_plan(plan_id: int, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""标记计划为已完成(收款到账/付款完成)(账套隔离 2026-09-21:跨账套 id → 404)"""
|
||||
plan = db.query(CashPlan).filter(
|
||||
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
|
||||
).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "计划不存在")
|
||||
plan.status = "completed"
|
||||
@@ -616,9 +625,13 @@ def api_receivables(
|
||||
|
||||
|
||||
@router.post("/receivables/{plan_id}/payment")
|
||||
def api_register_payment(plan_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""回款登记 — 更新计划状态 + 自动消除到期未收款预警 + 联动催收行动/F_AR_DAYS"""
|
||||
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
|
||||
def api_register_payment(plan_id: int, data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""回款登记 — 更新计划状态 + 自动消除到期未收款预警 + 联动催收行动/F_AR_DAYS
|
||||
(账套隔离 2026-09-21:跨账套 id → 404)"""
|
||||
plan = db.query(CashPlan).filter(
|
||||
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
|
||||
).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "应收计划不存在")
|
||||
if plan.plan_type != "receive":
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy import func, desc
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition, KPIValue, KPIAlert, User
|
||||
import logging
|
||||
@@ -45,6 +46,7 @@ def list_customer_kpis(
|
||||
end_date: str = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取客户维度KPI列表(含最新值、预警、趋势)"""
|
||||
start, end = parse_period(period, start_date, end_date)
|
||||
@@ -54,6 +56,7 @@ def list_customer_kpis(
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.dimension == "customer",
|
||||
KPIDefinition.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
).order_by(KPIDefinition.kpi_code).all()
|
||||
|
||||
result = []
|
||||
|
||||
@@ -360,6 +360,7 @@ def get_my_kpis(
|
||||
current_user: User = Depends(require_auth),
|
||||
period: str = Query("month"),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取当前用户负责的KPI
|
||||
- business角色:只看自己负责的KPI
|
||||
@@ -370,7 +371,10 @@ def get_my_kpis(
|
||||
name = current_user.name
|
||||
period_str = datetime.now().strftime("%Y-%m")
|
||||
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
).all()
|
||||
result = []
|
||||
|
||||
for k in kpis:
|
||||
|
||||
@@ -10,7 +10,7 @@ import json
|
||||
import logging
|
||||
|
||||
from app.database import get_db
|
||||
from app.deps import assert_entity_scope
|
||||
from app.deps import assert_entity_scope, get_entity_id
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition, KPIValue, KpiDataQualityLog, OperationLog
|
||||
from app.api.kpis import kpi_to_dict
|
||||
@@ -36,9 +36,12 @@ def _log_to_dict(log):
|
||||
# ============================================================
|
||||
|
||||
@router.get("/check")
|
||||
def run_quality_check(db: Session = Depends(get_db)):
|
||||
"""扫描全部KPI,生成数据质量报告"""
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
def run_quality_check(db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""扫描本账套KPI,生成数据质量报告(账套隔离 2026-09-21)"""
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).all()
|
||||
issues = []
|
||||
current_period = datetime.now().strftime("%Y-%m")
|
||||
|
||||
@@ -228,9 +231,12 @@ def delete_quality_log(log_id: int, db: Session = Depends(get_db), user=WRITE_RO
|
||||
# ============================================================
|
||||
|
||||
@router.get("/stats")
|
||||
def quality_stats(db: Session = Depends(get_db)):
|
||||
"""数据质量统计"""
|
||||
total_kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").count()
|
||||
def quality_stats(db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""数据质量统计(账套隔离 2026-09-21:KPI 口径按会话账套;质量日志表无 entity_id 列,见回报)"""
|
||||
total_kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).count()
|
||||
total_logs = db.query(KpiDataQualityLog).count()
|
||||
open_logs = db.query(KpiDataQualityLog).filter(KpiDataQualityLog.status == "open").count()
|
||||
|
||||
@@ -256,7 +262,10 @@ def quality_stats(db: Session = Depends(get_db)):
|
||||
|
||||
# ── 数据审计看板统计 ──
|
||||
# KPI完整度评分
|
||||
all_kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
all_kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
).all()
|
||||
total = len(all_kpis)
|
||||
complete_kpis = 0
|
||||
missing_metadata_count = 0
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import SystemConfig, OperationLog
|
||||
from app.models.driver_budget import DriverFactorTemplate, DriverFactorBudget
|
||||
@@ -254,6 +255,7 @@ def driver_calculate(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""
|
||||
驱动因子计算预算。
|
||||
@@ -313,6 +315,7 @@ def driver_calculate(
|
||||
|
||||
# 记录计算结果
|
||||
budget_record = DriverFactorBudget(
|
||||
entity_id=entity_id, # 账套隔离 2026-09-21:写侧先写归属(否则读侧过滤误杀本账套行)
|
||||
name=name,
|
||||
industry=industry,
|
||||
template_id=template_id,
|
||||
@@ -457,9 +460,12 @@ def driver_sensitivity(
|
||||
def list_driver_history(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""查看最近的驱动因子预算计算记录"""
|
||||
records = db.query(DriverFactorBudget).order_by(
|
||||
"""查看本账套最近的驱动因子预算计算记录(账套隔离 2026-09-21)"""
|
||||
records = db.query(DriverFactorBudget).filter(
|
||||
DriverFactorBudget.entity_id == entity_id,
|
||||
).order_by(
|
||||
DriverFactorBudget.created_at.desc()
|
||||
).limit(limit).all()
|
||||
|
||||
|
||||
@@ -343,6 +343,7 @@ def submit_reimbursement(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""提交报销单 → 自动校验规则(超限自动打回)"""
|
||||
applicant = data.get("applicant") or (current_user.name if hasattr(current_user, "name") else current_user.username)
|
||||
@@ -366,6 +367,7 @@ def submit_reimbursement(
|
||||
expense_date = None
|
||||
|
||||
reimb = ExpenseReimbursement(
|
||||
entity_id=entity_id, # 账套隔离 2026-09-21:写侧先写归属(否则读侧过滤误杀本账套行)
|
||||
reimb_no="BX" + datetime.now().strftime("%Y%m%d%H%M%S") + f"{random.randint(100, 999)}",
|
||||
applicant=applicant,
|
||||
department=data.get("department", ""),
|
||||
@@ -537,6 +539,7 @@ def resubmit_reimbursement(
|
||||
def expense_stats(
|
||||
period: str = Query(None, description="期间 YYYY-MM,默认当前月"),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""费用审核看板统计: 状态计数 / 费用类型统计 / 超限预警 / 本月总额"""
|
||||
@@ -544,7 +547,9 @@ def expense_stats(
|
||||
period = period or now.strftime("%Y-%m")
|
||||
|
||||
counts = {"pending": 0, "approved": 0, "rejected": 0, "returned": 0, "total": 0}
|
||||
for st, cnt in db.query(ExpenseReimbursement.status, func.count(ExpenseReimbursement.id)).group_by(
|
||||
for st, cnt in db.query(ExpenseReimbursement.status, func.count(ExpenseReimbursement.id)).filter(
|
||||
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
).group_by(
|
||||
ExpenseReimbursement.status
|
||||
).all():
|
||||
if st in counts:
|
||||
@@ -560,6 +565,7 @@ def expense_stats(
|
||||
)
|
||||
.filter(
|
||||
ExpenseReimbursement.status.in_(["pending", "approved"]),
|
||||
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
func.date_format(ExpenseReimbursement.expense_date, "%Y-%m") == period,
|
||||
)
|
||||
.group_by(ExpenseReimbursement.expense_type)
|
||||
@@ -583,6 +589,7 @@ def expense_stats(
|
||||
.filter(
|
||||
ExpenseReimbursement.status == "returned",
|
||||
ExpenseReimbursement.check_result == "fail",
|
||||
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
)
|
||||
.order_by(ExpenseReimbursement.id.desc())
|
||||
.limit(50)
|
||||
@@ -604,7 +611,9 @@ def expense_stats(
|
||||
for r in over_limit_rows
|
||||
]
|
||||
|
||||
recent_rows = db.query(ExpenseReimbursement).order_by(ExpenseReimbursement.id.desc()).limit(10).all()
|
||||
recent_rows = db.query(ExpenseReimbursement).filter(
|
||||
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
|
||||
).order_by(ExpenseReimbursement.id.desc()).limit(10).all()
|
||||
recent = [_reimb_to_dict(r) for r in recent_rows]
|
||||
|
||||
# 本月预算使用率(招待费15万/月 管理费91万/月)
|
||||
|
||||
@@ -15,6 +15,7 @@ from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition
|
||||
|
||||
@@ -166,9 +167,13 @@ def validate_kpi(
|
||||
def governance_audit(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""全量校验所有活跃KPI,输出按4条规则分组的不合规清单。"""
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
"""校验本账套活跃KPI,输出按4条规则分组的不合规清单(账套隔离 2026-09-21)。"""
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).all()
|
||||
|
||||
non_compliant = []
|
||||
by_rule: dict = {1: [], 2: [], 3: [], 4: []}
|
||||
|
||||
+17
-6
@@ -437,9 +437,13 @@ def get_kpi_causality_chain(
|
||||
kpi_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user = Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""KPI因果链追踪 — 返回单个KPI的上下游因果链"""
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
"""KPI因果链追踪 — 返回单个KPI的上下游因果链(账套隔离 2026-09-21:跨账套 id → 404)"""
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.id == kpi_id,
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).first()
|
||||
if not kpi:
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
|
||||
@@ -884,14 +888,21 @@ def kpi_to_dict(k):
|
||||
|
||||
|
||||
@router.put("/{kpi_id}/associate-map")
|
||||
def associate_kpi_map(kpi_id: int, data: dict, db: Session = Depends(get_db), user=WRITE_ROLES):
|
||||
"""关联KPI到战略地图"""
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
def associate_kpi_map(kpi_id: int, data: dict, db: Session = Depends(get_db), user=WRITE_ROLES,
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""关联KPI到战略地图(账套隔离 2026-09-21:KPI 与地图均须属本账套,跨账套 → 404)"""
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.id == kpi_id,
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).first()
|
||||
if not kpi:
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
map_id = data.get("map_id")
|
||||
if map_id is not None:
|
||||
m = db.query(StrategicMap).filter(StrategicMap.id == map_id).first()
|
||||
m = db.query(StrategicMap).filter(
|
||||
StrategicMap.id == map_id,
|
||||
StrategicMap.entity_id == entity_id,
|
||||
).first()
|
||||
if not m:
|
||||
raise HTTPException(404, "战略地图不存在")
|
||||
kpi.map_id = map_id
|
||||
|
||||
@@ -52,8 +52,10 @@ def update_channel(ch_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
ch = db.query(NotificationChannel).filter(NotificationChannel.id == ch_id).first()
|
||||
if not ch:
|
||||
raise HTTPException(404, "渠道不存在")
|
||||
# 字段白名单(2026-09-21 mass-assignment 收口):仅业务字段,其余键忽略
|
||||
_updatable = ("name", "config", "enabled")
|
||||
for k, v in data.items():
|
||||
if hasattr(ch, k) and k not in ("id", "created_at"):
|
||||
if k in _updatable:
|
||||
setattr(ch, k, v)
|
||||
db.commit()
|
||||
db.refresh(ch)
|
||||
|
||||
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_role
|
||||
from app.models import OKRTemplate, StrategicMap
|
||||
from app.utils.kr_name import sanitize_kr_name
|
||||
@@ -253,7 +254,8 @@ def increment_use_count(template_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/{template_id}/apply")
|
||||
def apply_okr_template(template_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
def apply_okr_template(template_id: int, data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""应用 OKR 模板 — 创建战略地图并填入 O+KR"""
|
||||
t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first()
|
||||
if not t:
|
||||
@@ -293,6 +295,7 @@ def apply_okr_template(template_id: int, data: dict, db: Session = Depends(get_d
|
||||
})
|
||||
|
||||
m = StrategicMap(
|
||||
entity_id=entity_id, # 账套隔离 2026-09-21:原漏写→落模型默认 1(对齐 maps.py)
|
||||
title=map_title,
|
||||
version=data.get("version", "v1.0"),
|
||||
status="draft",
|
||||
|
||||
Reference in New Issue
Block a user