From 6c1ea85d719d2feb4dcb277075916eae8feeedfb Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Thu, 1 Oct 2026 21:12:02 +0800 Subject: [PATCH] fix(alignment): /tree map_id filter via map_objectives.kpis union (v2) - 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 --- backend/app/api/alignment.py | 40 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/backend/app/api/alignment.py b/backend/app/api/alignment.py index 2e68bf7e..925efd12 100644 --- a/backend/app/api/alignment.py +++ b/backend/app/api/alignment.py @@ -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.) 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} # 构建父子关系