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
+30 -8
View File
@@ -3,7 +3,7 @@
管理员可初始化选择,后续按模式运作""" 管理员可初始化选择,后续按模式运作"""
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import func from sqlalchemy import func, or_
from typing import Optional, List from typing import Optional, List
from datetime import datetime from datetime import datetime
import json import json
@@ -11,7 +11,7 @@ import json
from app.database import get_db from app.database import get_db
from app.auth_middleware import require_auth, require_role, require_auth_401 from app.auth_middleware import require_auth, require_role, require_auth_401
from app.deps import get_entity_id 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 = APIRouter(prefix="/api/cma/alignment", tags=["KPI目标对齐"],
# 用户端点:router 级要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914); # 用户端点:router 级要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914);
@@ -146,11 +146,15 @@ def get_alignment_tree(
跨实体显式传参(?entity_id=<他人账套>)由 get_entity_id 返回 403。 跨实体显式传参(?entity_id=<他人账套>)由 get_entity_id 返回 403。
地图筛选(2026-10-01 project-20261001-align-tree-map-filter-fullstack): 地图筛选(2026-10-01 project-20261001-align-tree-map-filter-fullstack):
`map_id` 参与**KPI 选择集**过滤——给定 map_id 时只保留 `map_id` 参与**KPI 选择集**过滤,且只作用于 KPI 选择集——三种模式的构建函数不变。
`KPIDefinition.map_id == map_id` 的本账套 active KPI;不传时维持改前行为(全量)。
跨账套 map_id:KPI 选择集本身已按 entity_id 隔离(且关联写入端点 过滤取「本账套该地图已关联的 KPI」,关联源按**权威程度**合并两条通道:
`PUT /kpis/{id}/associate-map` 亦强制 KPI↔地图同账套)⇒ 返回空树,不泄漏他账套 KPI。 ① `map_objectives.kpis`(**战略地图编辑器**写入的 KPI 编码数组,真源
过滤只作用于 KPI 选择集,三种模式的构建函数(vertical/horizontal/bsc_chain)不变。 `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>) # 获取当前账套的对齐模式(B3(a) 拆账套 2026-09-22:只读 alignment_config.<entity_id>)
config_perm = _get_alignment_perm(db, entity_id) config_perm = _get_alignment_perm(db, entity_id)
@@ -163,9 +167,27 @@ def get_alignment_tree(
.filter(KPIDefinition.entity_id == entity_id, .filter(KPIDefinition.entity_id == entity_id,
KPIDefinition.status == "active")) KPIDefinition.status == "active"))
# 地图筛选:只缩小 KPI 选择集,不改动任何构建逻辑 # 地图筛选:只缩小 KPI 选择集,不改动任何构建逻辑
empty_by_map = False
if map_id is not None: if map_id is not None:
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) kpi_query = kpi_query.filter(KPIDefinition.map_id == map_id)
kpis = kpi_query.order_by(KPIDefinition.kpi_code).all() kpis = [] if empty_by_map else kpi_query.order_by(KPIDefinition.kpi_code).all()
kpi_map = {k.id: k for k in kpis} kpi_map = {k.id: k for k in kpis}
# 构建父子关系 # 构建父子关系