fix(security): ai_analysis多租户隔离缺口 + dashboard预警计数 + bot_kpis归属 + cost账套过滤 + kpis create白名单 (OpenCode审查20260907)

- ai_analysis: generate_rule_suggestions pending预警按entity过滤; kpi_analysis归属校验404;
  dashboard-analysis-stream/ask/review-plans 补 get_entity_id + KPI/ActionPlan/KPIAlert 全按账套过滤;
  stream cache key 含 entity_id (原跨企业串缓存)
- dashboard: summary alert_count 补 KPIAlert.entity_id 过滤 (kpi_total 已有)
- bot_kpis: update_bot_kpi_value 归属校验 404 (原任意登录可改他企 bot KPI)
- cost: ActualCost/StandardCost 确认多企业共用 → cost_overview/variance/breakdown/dashboard
  补 entity 过滤(engine 函数加 entity_id 透传); dashboard total_cost 既有 KeyError 修复
- kpis: create_kpi 字段白名单 (仿 UPDATE_KPI_WHITELIST+kpi_code), 拦截 status/important_flag 注入
- tests: 新增 test_multitenant_ai_20260907.py 11条越权用例 (跨entity 404/空返回断言)
- 全量 pytest: 745 passed / 10 failed(与基线 HEAD 一致, 存量问题非本次引入) / 1 xfailed
This commit is contained in:
Hermes CI Fix
2026-09-07 10:22:24 +08:00
parent d71c7fef67
commit ac645a56ee
7 changed files with 410 additions and 53 deletions
+30 -15
View File
@@ -56,18 +56,23 @@ def calc_efficiency_variance(standard_hours: float, actual_hours: float,
# 产品级差异分析
# ============================================================
def calc_product_variance(product_code: str, period: str) -> dict:
"""计算指定产品在指定期间的成本差异"""
def calc_product_variance(product_code: str, period: str, entity_id: Optional[int] = None) -> dict:
"""计算指定产品在指定期间的成本差异(entity_id=None 时不过滤,API 调用必传——多租户隔离 2026-09-07)"""
db = get_session_local()()
try:
standards = db.query(StandardCost).filter(
q_std = db.query(StandardCost).filter(
StandardCost.product_code == product_code,
StandardCost.status == "active",
).all()
actuals = db.query(ActualCost).filter(
)
q_act = db.query(ActualCost).filter(
ActualCost.product_code == product_code,
ActualCost.period == period,
).all()
)
if entity_id is not None:
q_std = q_std.filter(StandardCost.entity_id == entity_id)
q_act = q_act.filter(ActualCost.entity_id == entity_id)
standards = q_std.all()
actuals = q_act.all()
if not standards or not actuals:
return {"product_code": product_code, "error": "标准成本或实际成本数据不足", "items": [], "summary": {}}
@@ -191,12 +196,16 @@ def allocate_cost(activity_id: int, period: str, product_code: str,
# 成本总览数据
# ============================================================
def get_cost_overview(period: str) -> dict:
"""获取成本总览数据(总成本、结构占比、趋势)"""
def get_cost_overview(period: str, entity_id: Optional[int] = None) -> dict:
"""获取成本总览数据(总成本、结构占比、趋势)
entity_id=None 时不过滤(兼容存量调用);API 调用必须传 entity_id —— 多租户隔离 2026-09-07"""
db = get_session_local()()
try:
# 从实际成本表汇总
actual_costs = db.query(ActualCost).filter(ActualCost.period == period).all()
# 从实际成本表汇总(按账套过滤)
q_costs = db.query(ActualCost).filter(ActualCost.period == period)
if entity_id is not None:
q_costs = q_costs.filter(ActualCost.entity_id == entity_id)
actual_costs = q_costs.all()
total_cost = sum(a.actual_cost for a in actual_costs)
# 按成本类型分组
@@ -238,7 +247,10 @@ def get_cost_overview(period: str) -> dict:
trend = []
for p in reversed(months_texts):
costs = db.query(ActualCost).filter(ActualCost.period == p).all()
q_trend = db.query(ActualCost).filter(ActualCost.period == p)
if entity_id is not None:
q_trend = q_trend.filter(ActualCost.entity_id == entity_id)
costs = q_trend.all()
total = round(sum(c.actual_cost for c in costs), 2)
trend.append({"period": p, "total_cost": total})
except Exception:
@@ -256,14 +268,17 @@ def get_cost_overview(period: str) -> dict:
db.close()
def get_cost_breakdown(product_code: str, period: str) -> dict:
"""获取成本构成(料/工/费占比)"""
def get_cost_breakdown(product_code: str, period: str, entity_id: Optional[int] = None) -> dict:
"""获取成本构成(料/工/费占比)(entity_id=None 不过滤——多租户隔离 2026-09-07,API 必传)"""
db = get_session_local()()
try:
actuals = db.query(ActualCost).filter(
q_actuals = db.query(ActualCost).filter(
ActualCost.product_code == product_code,
ActualCost.period == period,
).all()
)
if entity_id is not None:
q_actuals = q_actuals.filter(ActualCost.entity_id == entity_id)
actuals = q_actuals.all()
material = sum(a.actual_cost for a in actuals if a.cost_type == "material")
labor = sum(a.actual_cost for a in actuals if a.cost_type == "labor")