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