fix: OpenCode审查16条修复 — 战略地图隔离/安全/逻辑漏洞

安全:
- ⑦ update_map 字段白名单(禁entity_id篡改, status限值域)
- ⑧ objectives 全端点补entity校验(原括号错位导致隔离失效) + update白名单
- ⑨ strategic_map_versions 加entity_id列+回填, list/snapshot/rollback过滤
- ⑩ get_map_review KPI查询加entity(同编码跨企业混入)
- ⑪ _sync_map_objectives 强制继承地图entity

逻辑:
- ③ MapCanvas 自动保存 currentMap.value 解包(30s自动保存修复)
- ④ 乐观锁字段 version_num→version 对齐
- ⑤ 红黄绿灯: 越低越好型反向 + 负值修正 + 月目标优先
- ⑥ review 补 action_plan_summary 字段

验证: 跨企业写目标404✓ entity_id注入被拒✓ pytest 531 passed✓
This commit is contained in:
Hermes CI Fix
2026-08-26 23:25:44 +08:00
parent 95b73c9b90
commit 9b61c48051
5 changed files with 54 additions and 18 deletions
+29 -6
View File
@@ -115,8 +115,13 @@ def update_map(map_id: int, data: dict, db: Session = Depends(get_db), entity_id
raise HTTPException(404, "战略地图不存在")
old_status = m.status
# 安全白名单: 只允许编辑业务字段, 禁止 entity_id 等敏感字段被请求体篡改 (OpenCode审查#7)
# status 允许但仅限合法值域(draft/published), 防止任意发布
ALLOWED_MAP_FIELDS = {"title", "version", "dimensions", "canvas_data", "description", "status"}
for k, v in data.items():
if hasattr(m, k) and v is not None:
if k in ALLOWED_MAP_FIELDS and hasattr(m, k) and v is not None:
if k == "status" and v not in ("draft", "published"):
continue
setattr(m, k, v)
db.commit()
@@ -307,7 +312,7 @@ def _sync_map_objectives(m, db):
for i, obj in enumerate(dim.get("objectives", [])):
mo = MapObjective(
map_id=m.id,
entity_id=getattr(m, "entity_id", 1) or 1, # 账套隔离: 继承地图企业
entity_id=m.entity_id if m.entity_id is not None else 1, # 强制继承地图企业(OpenCode审查#11)
dimension_key=dim.get("key", ""),
name=obj.get("name", ""),
description=obj.get("description", ""),
@@ -378,9 +383,10 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
for code in obj.get("kpis", []):
all_kpi_codes.add(code)
# 查询KPI定义
# 查询KPI定义(账套隔离: 同编码跨企业会混入, OpenCode审查#10
kpi_defs = db.query(KPIDefinition).filter(
KPIDefinition.kpi_code.in_(all_kpi_codes) if all_kpi_codes else False
KPIDefinition.kpi_code.in_(all_kpi_codes) if all_kpi_codes else False,
KPIDefinition.entity_id == entity_id,
).all() if all_kpi_codes else []
kpi_map = {k.kpi_code: k for k in kpi_defs}
@@ -446,11 +452,21 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
continue
lv = latest_values.get(kpi_def.id, {})
actual = lv.get("actual_value")
target = kpi_def.target_value
# 多粒度目标优先: 月目标(多数KPI月度) → 兼容legacy target_value (OpenCode审查#5)
target = kpi_def.target_monthly or kpi_def.target_value
# 判断红黄绿灯(绿≥90% / 黄60-90% / 红<60%
# 方向修正: 越低越好型(费用率/成本率/损耗/逾期/投诉/流失) 与 负值目标 (OpenCode审查#5)
level = "gray"
if actual is not None and target:
ratio = actual / target
name = (kpi_def.kpi_name or "") + " " + (kpi_def.kpi_code or "")
lower_better = any(k in name for k in
("费用率", "成本率", "损耗", "逾期", "投诉", "流失", "负债率", "返修", "缺勤"))
if lower_better:
ratio = target / actual if actual else 0 # 实际越低越好: 目标/实际
else:
ratio = actual / target
if ratio < 0:
ratio = 0 # 负值目标(如亏损控制) 完成度不为负
if ratio >= 0.9:
level = "green"
elif ratio >= 0.6:
@@ -481,6 +497,13 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
"kpis": obj_kpis,
"level": worst_level,
"has_data": len(obj_kpis) > 0,
# 行动方案汇总(OpenCode审查#6): 按本目标KPI统计
"action_plan_summary": {
"total": len([p for p in action_plans_data if p["kpi_id"] in [k["kpi_id"] for k in obj_kpis]]),
"pending": len([p for p in action_plans_data
if p["kpi_id"] in [k["kpi_id"] for k in obj_kpis]
and p["status"] not in ("done", "completed")]),
} if obj_kpis else {"total": 0, "pending": 0},
}
objectives.append(obj_item)