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:
@@ -156,10 +156,16 @@ def generate_rule_suggestions(db: Session, entity_id: int,
|
||||
"kpi_id": b.kpi_id, "period": period, "budget_value": round(actual, 2),
|
||||
})
|
||||
|
||||
# 4. pending预警 → 建行动方案
|
||||
alerts = db.query(KPIAlert).filter(KPIAlert.status == "pending").all()
|
||||
# 4. pending预警 → 建行动方案(多租户隔离 2026-09-07: 只取本账套预警)
|
||||
alerts = db.query(KPIAlert).filter(
|
||||
KPIAlert.entity_id == entity_id,
|
||||
KPIAlert.status == "pending",
|
||||
).all()
|
||||
for a in alerts:
|
||||
k = db.query(KPIDefinition).filter(KPIDefinition.id == a.kpi_id).first()
|
||||
k = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.id == a.kpi_id,
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).first()
|
||||
kpi_name = k.kpi_name if k else f"KPI#{a.kpi_id}"
|
||||
title = f"处理预警:{kpi_name} {a.alert_message[:30]}"
|
||||
content = f"存在待处理预警({a.alert_level}级):{a.alert_message}。建议建立行动方案跟进。"
|
||||
@@ -291,8 +297,11 @@ async def dashboard_analysis(role: str = Query("ceo"), db: Session = Depends(get
|
||||
"unit": k.unit,
|
||||
})
|
||||
|
||||
# 获取预警
|
||||
alerts = db.query(KPIAlert).filter(KPIAlert.status == "pending").count()
|
||||
# 获取预警(多租户隔离 2026-09-07: 只统计本账套)
|
||||
alerts = db.query(KPIAlert).filter(
|
||||
KPIAlert.entity_id == entity_id,
|
||||
KPIAlert.status == "pending",
|
||||
).count()
|
||||
|
||||
# 构建分析prompt
|
||||
kpi_text = "\n".join([f"- {k['name']}({k['code']}): 目标={k['target']}, 实际={k['actual']}({k['period']}), 维度={k['dimension']}" for k in kpi_summary if k['actual'] is not None])
|
||||
@@ -339,8 +348,12 @@ async def kpi_analysis(kpi_id: int, db: Session = Depends(get_db),
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.id == kpi_id,
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).first()
|
||||
if not kpi:
|
||||
# 跨企业 kpi_id 同样 404(多租户隔离 2026-09-07)
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
|
||||
values = db.query(KPIValue).filter(KPIValue.kpi_id == kpi_id).order_by(KPIValue.period.asc()).all()
|
||||
@@ -422,9 +435,11 @@ async def _stream_analysis(prompt: str):
|
||||
|
||||
|
||||
@router.get("/dashboard-analysis-stream")
|
||||
async def dashboard_analysis_stream(role: str = Query("ceo"), db: Session = Depends(get_db)):
|
||||
async def dashboard_analysis_stream(role: str = Query("ceo"), db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""AI分析驾驶舱数据 — SSE流式输出"""
|
||||
cache_key = f"dashboard_analysis:{role}"
|
||||
# 多租户隔离 2026-09-07: cache key 补 entity_id(原跨企业串缓存)
|
||||
cache_key = f"dashboard_analysis:{role}:{entity_id}"
|
||||
cached = cache_get("ai", cache_key)
|
||||
if cached:
|
||||
# 缓存存在,直接以流的形式一次性返回
|
||||
@@ -434,7 +449,10 @@ async def dashboard_analysis_stream(role: str = Query("ceo"), db: Session = Depe
|
||||
yield "data: {\"text\": \"[DONE]\"}\n\n"
|
||||
return StreamingResponse(cached_stream(), media_type="text/event-stream")
|
||||
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
KPIDefinition.status == "active",
|
||||
).all()
|
||||
kpi_summary = []
|
||||
for k in kpis:
|
||||
latest = db.query(KPIValue).filter(KPIValue.kpi_id == k.id).order_by(KPIValue.period.desc()).first()
|
||||
@@ -443,7 +461,10 @@ async def dashboard_analysis_stream(role: str = Query("ceo"), db: Session = Depe
|
||||
"target": k.target_value, "actual": latest.actual_value if latest else None,
|
||||
"period": latest.period if latest else None, "unit": k.unit,
|
||||
})
|
||||
alerts = db.query(KPIAlert).filter(KPIAlert.status == "pending").count()
|
||||
alerts = db.query(KPIAlert).filter(
|
||||
KPIAlert.entity_id == entity_id,
|
||||
KPIAlert.status == "pending",
|
||||
).count()
|
||||
kpi_text = "\n".join([f"- {k['name']}({k['code']}): 目标={k['target']}, 实际={k['actual']}({k['period']}), 维度={k['dimension']}" for k in kpi_summary if k['actual'] is not None])
|
||||
role_label = {"ceo": "CEO(总经理)", "finance": "财务部", "business": "业务部"}.get(role, "管理层")
|
||||
prompt = f"""我是一家公司的管理层,以下是当前管理会计系统的KPI数据和系统状态,请给出专业的分析和管理建议:
|
||||
@@ -468,6 +489,7 @@ async def ask_question(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""自然语言查询 — CEO问企业经营问题"""
|
||||
body = await request.json()
|
||||
@@ -476,8 +498,11 @@ async def ask_question(
|
||||
if not question:
|
||||
raise HTTPException(400, "请输入问题")
|
||||
|
||||
# 收集系统数据作为上下文
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
# 收集系统数据作为上下文(多租户隔离 2026-09-07: 只取本账套)
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
KPIDefinition.status == "active",
|
||||
).all()
|
||||
kpi_context = []
|
||||
for k in kpis:
|
||||
latest = db.query(KPIValue).filter(KPIValue.kpi_id == k.id).order_by(KPIValue.period.desc()).first()
|
||||
@@ -487,13 +512,21 @@ async def ask_question(
|
||||
f"{' ⚠️' + alert.alert_level if alert else ''}"
|
||||
)
|
||||
|
||||
# 获取改善计划
|
||||
plans = db.query(ActionPlan).order_by(ActionPlan.created_at.desc()).limit(10).all()
|
||||
# 获取改善计划(多租户隔离 2026-09-07: 只取本账套)
|
||||
plans = db.query(ActionPlan).filter(
|
||||
ActionPlan.entity_id == entity_id,
|
||||
).order_by(ActionPlan.created_at.desc()).limit(10).all()
|
||||
plan_context = [f"- {p.title}({p.assignee}, {p.status}, {p.progress}%)" for p in plans]
|
||||
|
||||
# 获取预警
|
||||
red_alerts = db.query(KPIAlert).filter(KPIAlert.status == "pending", KPIAlert.alert_level == "red").count()
|
||||
yellow_alerts = db.query(KPIAlert).filter(KPIAlert.status == "pending", KPIAlert.alert_level == "yellow").count()
|
||||
red_alerts = db.query(KPIAlert).filter(
|
||||
KPIAlert.entity_id == entity_id,
|
||||
KPIAlert.status == "pending", KPIAlert.alert_level == "red",
|
||||
).count()
|
||||
yellow_alerts = db.query(KPIAlert).filter(
|
||||
KPIAlert.entity_id == entity_id,
|
||||
KPIAlert.status == "pending", KPIAlert.alert_level == "yellow",
|
||||
).count()
|
||||
|
||||
system_context = f"""你是管理会计OS的AI助手,基于以下企业数据回答管理层问题。
|
||||
|
||||
@@ -525,9 +558,13 @@ async def ask_question(
|
||||
async def review_plans(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""AI复盘改善行动计划执行效果"""
|
||||
plans = db.query(ActionPlan).order_by(ActionPlan.created_at.asc()).all()
|
||||
# 多租户隔离 2026-09-07: ActionPlan 按 entity_id 过滤(表 2026-08-31 已加列回填)
|
||||
plans = db.query(ActionPlan).filter(
|
||||
ActionPlan.entity_id == entity_id,
|
||||
).order_by(ActionPlan.created_at.asc()).all()
|
||||
|
||||
if not plans:
|
||||
return {"analysis": "暂无改善行动计划,无法复盘"}
|
||||
|
||||
Reference in New Issue
Block a user