From ac4bbc689135d78502464a577ed9f8ee10a66d37 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Fri, 2 Oct 2026 21:01:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(h2-forecast):=20=E5=AF=B9=E5=A4=96=20KPIVal?= =?UTF-8?q?ue=20=E6=AE=8B=E4=BD=99=E8=AF=BB=E8=B7=AF=E5=BE=84=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=AE=9E=E6=B5=8B=E5=8C=BA=E5=8F=A3=E5=BE=84=EF=BC=88?= =?UTF-8?q?7=20=E5=A4=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 任总 2026-10-02 11:35 直令「对外 /api/cma/** 读 KPIValue 默认只取实测区; 预估禁进趋势序列 / 禁作预警与预算依据」的同族残余读路径收口。 7 处逐处改(均引 app/utils/kpi_value_scope.actual_only_clause,不新写字面量口径): - kpis.py:604 GET /kpis/{id} 详情历史值(预估禁进趋势序列) - ai_analysis.py:363 GET /ai/kpi-analysis/{id} 趋势序列/LLM 上下文 - thresholds.py:132 GET /thresholds/suggest/{id} 阈值推荐依据 - budget_generate.py:94 GET /budget/kpi-budget-candidates 预算候选最新值 - budget.py:1380 POST /budget/apply-method 派生基数(预算依据) - maps.py:657 GET /maps/{id}/review 地图节点最新值 - data_quality.py:363 GET /data-quality/stats 缺失判定(仅预估行不算有实际值) 证据:同脚本双树 A/B 11/11(改前 3/11 FAIL)+ pytest 3 failed/1529 passed 与基线逐条同 ID。 不 push / 不部署 / 不重启 / 不写库。 --- backend/app/api/ai_analysis.py | 7 ++++++- backend/app/api/budget.py | 3 +++ backend/app/api/budget_generate.py | 5 ++++- backend/app/api/data_quality.py | 3 +++ backend/app/api/kpis.py | 3 +++ backend/app/api/maps.py | 5 ++++- backend/app/api/thresholds.py | 7 ++++++- 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/backend/app/api/ai_analysis.py b/backend/app/api/ai_analysis.py index d80d369b..13ee1491 100644 --- a/backend/app/api/ai_analysis.py +++ b/backend/app/api/ai_analysis.py @@ -9,6 +9,7 @@ from app.auth_middleware import require_auth, require_role from app.models import KPIDefinition, KPIValue, KPIAlert, StrategicMap, User, ActionPlan, BudgetPlan, AISuggestion from app.utils.cache import get as cache_get, set as cache_set from app.utils.alert_entity_scope import month_period_clause +from app.utils.kpi_value_scope import actual_only_clause import json, hashlib, httpx, os, urllib.request, urllib.parse from datetime import datetime, date router = APIRouter(prefix="/api/cma/ai", tags=["AI分析"], @@ -360,7 +361,11 @@ async def kpi_analysis(kpi_id: int, db: Session = Depends(get_db), # 跨企业 kpi_id 同样 404(多租户隔离 2026-09-07) raise HTTPException(404, "KPI不存在") - values = db.query(KPIValue).filter(KPIValue.kpi_id == kpi_id).order_by(KPIValue.period.asc()).all() + values = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi_id, + # 实测区口径:预估行禁入 AI 趋势序列/LLM 上下文(kpi_value_scope.actual_only_clause) + actual_only_clause(), + ).order_by(KPIValue.period.asc()).all() trend_data = [] for v in values: diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py index 97bfe2c7..05e51ec4 100644 --- a/backend/app/api/budget.py +++ b/backend/app/api/budget.py @@ -11,6 +11,7 @@ from app.database import get_db from app.deps import get_entity_id from app.auth_middleware import require_auth, require_role from app.models import BudgetPlan, KPIDefinition, OperationLog, KPIValue +from app.utils.kpi_value_scope import actual_only_clause router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"], dependencies=[Depends(require_role("ceo", "finance", "it"))], @@ -1380,6 +1381,8 @@ def apply_budget_method( base_actual = db.query(KPIValue).filter( KPIValue.kpi_id == rule.base_kpi_id, KPIValue.actual_value.isnot(None), + # 实测区口径:预估行禁作预算基数(kpi_value_scope.actual_only_clause) + actual_only_clause(), ).order_by(KPIValue.calculated_at.desc()).first() if base_actual: base_val = base_actual.actual_value diff --git a/backend/app/api/budget_generate.py b/backend/app/api/budget_generate.py index 5882dfb1..523dcd35 100644 --- a/backend/app/api/budget_generate.py +++ b/backend/app/api/budget_generate.py @@ -9,6 +9,7 @@ from app.deps import get_entity_id from app.auth_middleware import require_auth, require_role from app.models import KPIDefinition, BudgetPlan, KPIValue, OperationLog from app.utils.oplog import log_op, SCOPE_BATCH +from app.utils.kpi_value_scope import actual_only_clause import json import logging from datetime import datetime @@ -92,7 +93,9 @@ def get_kpi_budget_candidates( latest_values = {} for kpi in kpis: v = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi.id + KPIValue.kpi_id == kpi.id, + # 实测区口径:预估行禁作预算生成依据(kpi_value_scope.actual_only_clause) + actual_only_clause(), ).order_by(KPIValue.calculated_at.desc()).first() if v: latest_values[kpi.id] = v.actual_value diff --git a/backend/app/api/data_quality.py b/backend/app/api/data_quality.py index 1caa80b9..12b03849 100644 --- a/backend/app/api/data_quality.py +++ b/backend/app/api/data_quality.py @@ -19,6 +19,7 @@ from app import dq_rule_config as dqcfg from pydantic import BaseModel, Field from app.api.kpis import kpi_to_dict from app.utils.alert_entity_scope import month_period_clause +from app.utils.kpi_value_scope import actual_only_clause logger = logging.getLogger("data-quality") @@ -363,6 +364,8 @@ def quality_stats(db: Session = Depends(get_db), entity_id: int = Depends(get_en val = db.query(KPIValue).filter( KPIValue.kpi_id == kpi.id, KPIValue.actual_value.isnot(None), + # 实测区口径:预估行不算「有实际值」(kpi_value_scope.actual_only_clause) + actual_only_clause(), ).first() if not val: missing_data_count += 1 diff --git a/backend/app/api/kpis.py b/backend/app/api/kpis.py index f4a77084..e602a22b 100644 --- a/backend/app/api/kpis.py +++ b/backend/app/api/kpis.py @@ -12,6 +12,7 @@ from app.auth_middleware import require_auth, require_role, filter_kpis_by_role, from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog, Entity, KPICausality, KPIHierarchy, User from app.utils.oplog import log_op from app.utils.alert_entity_scope import month_period_clause +from app.utils.kpi_value_scope import actual_only_clause from app.api.kpi_governance import validate_kpi_payload, kpi_issues_message from app.utils.threshold_parser import ( calc_level as _tp_calc_level, @@ -604,6 +605,8 @@ def get_kpi(kpi_id: int, db: Session = Depends(get_db), entity_id: int = Depends vals = db.query(KPIValue).filter( KPIValue.kpi_id == kpi_id, KPIValue.actual_value.isnot(None), + # 实测区口径:预估行禁入趋势序列(kpi_value_scope.actual_only_clause) + actual_only_clause(), ).order_by(KPIValue.period.asc()).all() is_reverse = kpi.kpi_code in REVERSE_INDICATORS target = kpi.target_value diff --git a/backend/app/api/maps.py b/backend/app/api/maps.py index a3e0c62d..c15f735c 100644 --- a/backend/app/api/maps.py +++ b/backend/app/api/maps.py @@ -8,6 +8,7 @@ from app.deps import get_entity_id from app.auth_middleware import require_auth, require_role from app.models import StrategicMap, OperationLog, MapObjective, Objective, KR, KPIDefinition, ReviewRecord, User from app.utils.kr_name import sanitize_kr_name +from app.utils.kpi_value_scope import actual_only_clause import json import logging @@ -655,7 +656,9 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen # 取每个KPI的最新一条 for kid in kpi_ids: v = db.query(KPIValue).filter( - KPIValue.kpi_id == kid + KPIValue.kpi_id == kid, + # 实测区口径:预估行禁作地图节点实测值(kpi_value_scope.actual_only_clause) + actual_only_clause(), ).order_by(KPIValue.calculated_at.desc()).first() if v: latest_values[kid] = { diff --git a/backend/app/api/thresholds.py b/backend/app/api/thresholds.py index 4123dd4b..85a0994b 100644 --- a/backend/app/api/thresholds.py +++ b/backend/app/api/thresholds.py @@ -11,6 +11,7 @@ from app.database import get_db from app.auth_middleware import require_auth, require_role from app.models import KPIDefinition, KPIValue, OperationLog from app.utils.threshold_parser import THRESHOLD_HELP, is_valid_threshold +from app.utils.kpi_value_scope import actual_only_clause from typing import Any, Optional import json @@ -129,7 +130,11 @@ def suggest_threshold(kpi_id: int, db: Session = Depends(get_db)): if not kpi: return {"error": "KPI不存在"} - values = db.query(KPIValue).filter(KPIValue.kpi_id == kpi_id).order_by(KPIValue.period.asc()).all() + values = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi_id, + # 实测区口径:预估行禁作阈值推荐依据(kpi_value_scope.actual_only_clause) + actual_only_clause(), + ).order_by(KPIValue.period.asc()).all() if not values: # 无历史数据,按行业标准推荐