From 47ef861488d7322106315bbd85bc5368e3f3170a Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Wed, 29 Jul 2026 18:07:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BAKPI?= =?UTF-8?q?=E6=97=B6=E8=A1=A5=E5=85=A8=E6=89=80=E6=9C=89NOT=20NULL?= =?UTF-8?q?=E5=AD=97=E6=AE=B5(formula/data=5Fsource/data=5Fowner/unit/enti?= =?UTF-8?q?ty=5Fid)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/analysis_results.py | 184 ++++++++++ backend/app/api/data.py | 6 + backend/app/main.py | 3 +- backend/app/models/__init__.py | 20 ++ frontend/src/api/index.ts | 7 + frontend/src/views/AnalysisConfidence.vue | 407 ++++++++++++++++++++++ 6 files changed, 626 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/analysis_results.py create mode 100644 frontend/src/views/AnalysisConfidence.vue diff --git a/backend/app/api/analysis_results.py b/backend/app/api/analysis_results.py new file mode 100644 index 00000000..36ec6df6 --- /dev/null +++ b/backend/app/api/analysis_results.py @@ -0,0 +1,184 @@ +"""置信度评分系统 — 财务Bot分析结论管理""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from sqlalchemy import func +from app.database import get_db +from app.auth_middleware import require_auth +from app.models import AnalysisResult, KPIValue, KPIDefinition +from typing import Optional, List +from datetime import datetime + +router = APIRouter(prefix="/api/cma/analysis", tags=["置信度评分"], + dependencies=[Depends(require_auth)], +) + + +def _calc_confidence(has_actual: bool, has_target: bool, has_trend: bool, has_review: bool) -> int: + """基于数据完整度自动计算置信度""" + if has_review: + return 95 + if has_actual and has_target and has_trend: + return 85 + if has_actual and has_target: + return 70 + if has_actual: + return 50 + return 30 # 无实际值,基于推测 + + +@router.get("/result") +async def get_analysis_results( + period: Optional[str] = Query(None, description="期间 YYYY-MM"), + kpi_code: Optional[str] = Query(None, description="KPI编码"), + db: Session = Depends(get_db), +): + """查询分析结论(按期间和/或KPI编码过滤)""" + query = db.query(AnalysisResult).order_by(AnalysisResult.created_at.desc()) + + if period: + query = query.filter(AnalysisResult.period == period) + if kpi_code: + query = query.filter(AnalysisResult.kpi_code == kpi_code) + + results = query.all() + + return { + "total": len(results), + "period": period, + "results": [ + { + "id": r.id, + "period": r.period, + "结论": r.conclusion, + "置信度": f"{r.confidence}%", + "数据来源": r.data_source, + "计算逻辑": r.calculation_logic, + "可比基准": r.comparable_benchmark, + "局限": r.limitations, + "kpi_code": r.kpi_code, + "kpi_name": r.kpi_name, + "has_actual": bool(r.has_actual), + "has_target": bool(r.has_target), + "has_trend": bool(r.has_trend), + "has_review": bool(r.has_review), + "created_at": r.created_at.strftime("%Y-%m-%d %H:%M:%S") if r.created_at else None, + } + for r in results + ], + } + + +@router.post("/result") +async def create_analysis_result( + period: str = Query(..., description="期间 YYYY-MM"), + conclusion: str = Query(..., description="分析结论"), + data_source: Optional[str] = Query(None, description="数据来源"), + calculation_logic: Optional[str] = Query(None, description="计算逻辑"), + comparable_benchmark: Optional[str] = Query(None, description="可比基准"), + limitations: Optional[str] = Query(None, description="局限说明"), + kpi_code: Optional[str] = Query(None, description="关联KPI编码"), + kpi_name: Optional[str] = Query(None, description="关联KPI名称"), + has_actual: bool = Query(False, description="有实际值"), + has_target: bool = Query(False, description="有目标值"), + has_trend: bool = Query(False, description="有历史趋势"), + has_review: bool = Query(False, description="有人工复核"), + db: Session = Depends(get_db), +): + """提交分析结果(自动计算置信度)""" + confidence = _calc_confidence(has_actual, has_target, has_trend, has_review) + + result = AnalysisResult( + period=period, + conclusion=conclusion, + confidence=confidence, + data_source=data_source, + calculation_logic=calculation_logic, + comparable_benchmark=comparable_benchmark, + limitations=limitations, + kpi_code=kpi_code, + kpi_name=kpi_name, + has_actual=1 if has_actual else 0, + has_target=1 if has_target else 0, + has_trend=1 if has_trend else 0, + has_review=1 if has_review else 0, + ) + db.add(result) + db.commit() + db.refresh(result) + + return { + "id": result.id, + "period": result.period, + "结论": result.conclusion, + "置信度": f"{result.confidence}%", + "数据来源": result.data_source, + "计算逻辑": result.calculation_logic, + "可比基准": result.comparable_benchmark, + "局限": result.limitations, + "confidence_score": result.confidence, + "message": "分析结论已保存", + } + + +@router.delete("/result/{result_id}") +async def delete_analysis_result( + result_id: int, + db: Session = Depends(get_db), +): + """删除分析结论""" + result = db.query(AnalysisResult).filter(AnalysisResult.id == result_id).first() + if not result: + raise HTTPException(404, "分析结论不存在") + db.delete(result) + db.commit() + return {"message": "已删除"} + + +@router.post("/auto-calculate") +async def auto_calculate_confidence( + period: str = Query(..., description="期间 YYYY-MM"), + kpi_code: str = Query(..., description="KPI编码"), + db: Session = Depends(get_db), +): + """根据KPI数据完整性自动生成置信度评分""" + # 查找KPI定义 + kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code).first() + if not kpi: + raise HTTPException(404, f"KPI编码 {kpi_code} 不存在") + + # 查找该期间的实际值 + value = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi.id, + KPIValue.period == period, + ).first() + + # 查找历史数据(趋势) + trend_values = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi.id, + ).order_by(KPIValue.period.desc()).limit(6).all() + + has_actual = value is not None and value.actual_value is not None + has_target = kpi.target_value is not None + has_trend = len(trend_values) >= 2 + has_review = False + + confidence = _calc_confidence(has_actual, has_target, has_trend, has_review) + + return { + "kpi_code": kpi_code, + "kpi_name": kpi.kpi_name, + "period": period, + "has_actual": has_actual, + "has_target": has_target, + "has_trend": has_trend, + "has_review": has_review, + "confidence": confidence, + "confidence_label": f"{confidence}%", + "数据完备度": { + "10%": "无数据", + "50%": "有实际值", + "70%": "有实际值+目标值", + "85%": "有实际值+目标值+历史趋势", + "95%": "有全部数据+人工复核", + }.get(str(confidence), "基于推测"), + } diff --git a/backend/app/api/data.py b/backend/app/api/data.py index 78c3f2d1..c51581ce 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -244,11 +244,17 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends if not kpi_code: new_code = f"{stype_prefix}{len(kpis) + created_kpis + 1:03d}" new_kpi = KPIDefinition( + entity_id=1, kpi_code=new_code, kpi_name=clean_name, dimension="finance", category="financial_report", + formula="-", data_source_type="excel", + data_source="Excel导入", + data_owner="财务部", + frequency="monthly", + unit="元", status="active", ) db.add(new_kpi) diff --git a/backend/app/main.py b/backend/app/main.py index 575ed95a..fccc5fe6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from dotenv import load_dotenv from app.database import init_db -from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law +from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law, analysis_results from app.utils.cache import clear_all as clear_cache, delete as delete_cache from scripts.erp_sync import run_sync as run_erp_sync from app.auth_middleware import require_auth @@ -72,6 +72,7 @@ app.include_router(subjects.router) app.include_router(driver_budget.router) app.include_router(bot_kpis.router) app.include_router(bot_iron_law.router) +app.include_router(analysis_results.router) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a5973ec6..4189aee2 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -435,6 +435,26 @@ class BudgetDeviationAlert(Base): created_at = Column(DateTime, server_default=func.now()) +class AnalysisResult(Base): + """财务Bot分析结论 — 带置信度评分""" + __tablename__ = "analysis_results" + id = Column(Integer, primary_key=True, index=True) + period = Column(String(20), nullable=False, comment="期间 YYYY-MM") + conclusion = Column(String(1000), nullable=False, comment="分析结论") + confidence = Column(Integer, nullable=False, comment="置信度 0-100") + data_source = Column(String(500), nullable=True, comment="数据来源") + calculation_logic = Column(String(1000), nullable=True, comment="计算逻辑") + comparable_benchmark = Column(String(500), nullable=True, comment="可比基准") + limitations = Column(String(1000), nullable=True, comment="局限说明") + has_actual = Column(Integer, default=0, comment="有实际值") + has_target = Column(Integer, default=0, comment="有目标值") + has_trend = Column(Integer, default=0, comment="有历史趋势") + has_review = Column(Integer, default=0, comment="有人工复核") + kpi_code = Column(String(50), nullable=True, comment="关联KPI编码") + kpi_name = Column(String(200), nullable=True, comment="关联KPI名称") + created_at = Column(DateTime, server_default=func.now()) + + class ForecastAccuracy(Base): """预测准确率 — 上期预测 vs 本期实际""" __tablename__ = "forecast_accuracy" diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 50dccef9..3411905e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -293,4 +293,11 @@ export const botKpiApi = { updateValue: (id: number, data: any) => api.post(`/bot-kpis/${id}/value`, data), } +export const analysisApi = { + getResults: (params?: any) => api.get('/analysis/result', { params }), + createResult: (params: any) => api.post('/analysis/result', null, { params }), + deleteResult: (id: number) => api.delete(`/analysis/result/${id}`), + autoCalculate: (params?: any) => api.post('/analysis/auto-calculate', null, { params }), +} + export default api diff --git a/frontend/src/views/AnalysisConfidence.vue b/frontend/src/views/AnalysisConfidence.vue new file mode 100644 index 00000000..f67b656f --- /dev/null +++ b/frontend/src/views/AnalysisConfidence.vue @@ -0,0 +1,407 @@ + + + + +