fix: 自动创建KPI时补全所有NOT NULL字段(formula/data_source/data_owner/unit/entity_id)
This commit is contained in:
@@ -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), "基于推测"),
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+2
-1
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user