fix(h2-forecast): 对外 KPIValue 残余读路径统一实测区口径(7 处)
任总 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 / 不部署 / 不重启 / 不写库。
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] = {
|
||||
|
||||
@@ -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:
|
||||
# 无历史数据,按行业标准推荐
|
||||
|
||||
Reference in New Issue
Block a user