fix(alignment): /tree map_id filter via map_objectives.kpis union (v2)
ci/woodpecker/push/main Pipeline failed

- add missing 'or_' import (NameError on map_id calls)

- union of map_objectives.kpis codes and KPIDefinition.map_id, intersect with entity active KPIs

- cross-entity / unknown map_id -> empty tree (no leak)

- no map_id -> unchanged behaviour
This commit is contained in:
Hermes CI Fix
2026-10-01 21:12:02 +08:00
parent 22ec40052d
commit 6c1ea85d71
+31 -9
View File
@@ -3,7 +3,7 @@
管理员可初始化选择,后续按模式运作"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from sqlalchemy import func, or_
from typing import Optional, List
from datetime import datetime
import json
@@ -11,7 +11,7 @@ import json
from app.database import get_db
from app.auth_middleware import require_auth, require_role, require_auth_401
from app.deps import get_entity_id
from app.models import KPIDefinition, OperationLog, RolePermission
from app.models import KPIDefinition, MapObjective, OperationLog, RolePermission, StrategicMap
router = APIRouter(prefix="/api/cma/alignment", tags=["KPI目标对齐"],
# 用户端点:router 级要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914);
@@ -146,11 +146,15 @@ def get_alignment_tree(
跨实体显式传参(?entity_id=<他人账套>)由 get_entity_id 返回 403。
地图筛选(2026-10-01 project-20261001-align-tree-map-filter-fullstack):
`map_id` 参与**KPI 选择集**过滤——给定 map_id 时只保留
`KPIDefinition.map_id == map_id` 的本账套 active KPI;不传时维持改前行为(全量)。
跨账套 map_id:KPI 选择集本身已按 entity_id 隔离(且关联写入端点
`PUT /kpis/{id}/associate-map` 亦强制 KPI↔地图同账套)⇒ 返回空树,不泄漏他账套 KPI。
过滤只作用于 KPI 选择集,三种模式的构建函数(vertical/horizontal/bsc_chain)不变。
`map_id` 参与**KPI 选择集**过滤,且只作用于 KPI 选择集——三种模式的构建函数不变。
过滤取「本账套该地图已关联的 KPI」,关联源按**权威程度**合并两条通道:
① `map_objectives.kpis`(**战略地图编辑器**写入的 KPI 编码数组,真源
`maps.py::_sync_map_objectives`;现网唯一有内容的通道);
② `KPIDefinition.map_id`(KPI 详情页「关联地图」写入;现网 329 行全为 NULL)。
两者取并集后与「本账套 `status='active'`」求交。跨账套/不存在的地图 id:先核该地图
归属本账套,不属本账套即返回**空树**(不泄漏他账套 KPI)。
不传 `map_id`:逐字段维持改前行为(本账套全部 active KPI)。
"""
# 获取当前账套的对齐模式(B3(a) 拆账套 2026-09-22:只读 alignment_config.<entity_id>)
config_perm = _get_alignment_perm(db, entity_id)
@@ -163,9 +167,27 @@ def get_alignment_tree(
.filter(KPIDefinition.entity_id == entity_id,
KPIDefinition.status == "active"))
# 地图筛选:只缩小 KPI 选择集,不改动任何构建逻辑
empty_by_map = False
if map_id is not None:
kpi_query = kpi_query.filter(KPIDefinition.map_id == map_id)
kpis = kpi_query.order_by(KPIDefinition.kpi_code).all()
owned = (db.query(StrategicMap.id)
.filter(StrategicMap.id == map_id, StrategicMap.entity_id == entity_id)
.first())
if not owned:
empty_by_map = True # 跨账套 / 不存在 ⇒ 空树(不泄漏他账套 KPI)
else:
codes = set()
for (arr,) in (db.query(MapObjective.kpis)
.filter(MapObjective.map_id == map_id).all()):
if isinstance(arr, list):
codes.update(str(c) for c in arr if c)
if codes:
kpi_query = kpi_query.filter(or_(
KPIDefinition.map_id == map_id,
KPIDefinition.kpi_code.in_(sorted(codes)),
))
else:
kpi_query = kpi_query.filter(KPIDefinition.map_id == map_id)
kpis = [] if empty_by_map else kpi_query.order_by(KPIDefinition.kpi_code).all()
kpi_map = {k.id: k for k in kpis}
# 构建父子关系