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