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:
+36
-20
@@ -170,6 +170,8 @@ def create_actual_cost(data: dict, db: Session = Depends(get_db),
|
||||
|
||||
# ============================================================
|
||||
# ABC 作业成本
|
||||
# 注:ABC 作业中心/分配为单实体演示数据(ab_activities/ab_allocations 全为 entity 1,
|
||||
# 历史演示用途),OpenCode 多租户审查 2026-09-07 判定暂不强制按账套隔离;若启用多企业 ABC 需补 entity 过滤。
|
||||
# ============================================================
|
||||
|
||||
@router.get("/abc/activities")
|
||||
@@ -229,34 +231,41 @@ def list_allocations(period: Optional[str] = Query(None),
|
||||
# ============================================================
|
||||
|
||||
@router.get("/overview")
|
||||
def cost_overview(period: Optional[str] = Query(None)):
|
||||
"""成本总览(总成本、结构占比、趋势)"""
|
||||
def cost_overview(period: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""成本总览(总成本、结构占比、趋势)— 多租户隔离 2026-09-07"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
return get_cost_overview(period)
|
||||
return get_cost_overview(period, entity_id=entity_id)
|
||||
|
||||
|
||||
@router.get("/variance")
|
||||
def cost_variance(product_code: str = Query(...),
|
||||
period: Optional[str] = Query(None)):
|
||||
"""量差价差分析"""
|
||||
period: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""量差价差分析 — 多租户隔离 2026-09-07"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
return calc_product_variance(product_code, period)
|
||||
return calc_product_variance(product_code, period, entity_id=entity_id)
|
||||
|
||||
|
||||
@router.get("/breakdown")
|
||||
def cost_breakdown(product_code: str = Query(...),
|
||||
period: Optional[str] = Query(None)):
|
||||
"""成本构成(料/工/费占比)"""
|
||||
period: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""成本构成(料/工/费占比)— 多租户隔离 2026-09-07"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
return get_cost_breakdown(product_code, period)
|
||||
return get_cost_breakdown(product_code, period, entity_id=entity_id)
|
||||
|
||||
|
||||
@router.get("/comparison")
|
||||
def cost_method_comparison(entity: str = Query("hanke")):
|
||||
"""三种成本法对比分析:传统/变动/作业成本法 (CMA P1)"""
|
||||
"""三种成本法对比分析:传统/变动/作业成本法 (CMA P1)
|
||||
注:纯静态教学样例数据(无 DB 访问),不涉多租户泄漏——OpenCode 审查 2026-09-07 豁免标注"""
|
||||
if entity == "hanke":
|
||||
gross_revenue = 713 # 万
|
||||
channel_rebate_rate = 0.828
|
||||
@@ -317,35 +326,42 @@ def cost_method_comparison(entity: str = Query("hanke")):
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
def cost_dashboard(period: Optional[str] = Query(None)):
|
||||
"""成本分析首页—汇总数据"""
|
||||
def cost_dashboard(period: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""成本分析首页—汇总数据(多租户隔离 2026-09-07: ActualCost 按账套过滤)"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
overview = get_cost_overview(period)
|
||||
overview = get_cost_overview(period, entity_id=entity_id)
|
||||
|
||||
# 获取所有产品列表
|
||||
db = get_db().__next__()
|
||||
# 获取所有产品列表(本账套)
|
||||
db_session = db if db is not None else get_db().__next__()
|
||||
try:
|
||||
products = db.query(ActualCost.product_code, ActualCost.product_name).filter(
|
||||
ActualCost.period == period
|
||||
products = db_session.query(ActualCost.product_code, ActualCost.product_name).filter(
|
||||
ActualCost.entity_id == entity_id,
|
||||
ActualCost.period == period,
|
||||
).distinct().all()
|
||||
product_list = [{"code": p[0], "name": p[1]} for p in products]
|
||||
|
||||
# 各产品成本
|
||||
product_costs = []
|
||||
for code, name in products:
|
||||
costs = db.query(ActualCost).filter(
|
||||
costs = db_session.query(ActualCost).filter(
|
||||
ActualCost.entity_id == entity_id,
|
||||
ActualCost.product_code == code,
|
||||
ActualCost.period == period,
|
||||
).all()
|
||||
total = round(sum(c.actual_cost for c in costs), 2)
|
||||
product_costs.append({"product_code": code, "product_name": name, "total_cost": total})
|
||||
finally:
|
||||
db.close()
|
||||
if db is None:
|
||||
db_session.close()
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"overview": overview,
|
||||
"products": product_list,
|
||||
"total_cost": round(sum(p["total_cost"] for p in product_list) + overview.get("erp_cost", 0), 2) if product_list else 0,
|
||||
# 修复既有bug(2026-09-07测试实证): 原对 product_list 求 p["total_cost"] 必 KeyError(产品列表无total_cost键)
|
||||
# → 改为 product_costs(含各产品总额),有产品数据时端点不再 500
|
||||
"total_cost": round(sum(p["total_cost"] for p in product_costs) + overview.get("erp_cost", 0), 2) if product_costs else 0,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user