feat: KPI通用化数据层改造
- 新增entities表(企业实体)+ 种子数据(酣客/博海) - kpi_definitions新增entity_id字段(DEFAULT 1,向后兼容) - 后端API list_kpis支持可选的entity_id过滤参数 - 新增 GET /api/cma/entities API返回企业列表 - 43个现有KPI自动获得entity_id=1(酣客) - 前端重新构建并部署
This commit is contained in:
@@ -3,7 +3,7 @@ CMA BOT API桥接层 — 供财务BOT/店研学BOT调用
|
||||
无需用户登录,使用 BOT API Key 认证
|
||||
"""
|
||||
import os, json, logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Header
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Header, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc
|
||||
from datetime import datetime
|
||||
@@ -30,10 +30,9 @@ def _load_bot_keys():
|
||||
raw = os.getenv("CMA_BOT_API_KEYS", "")
|
||||
if not raw:
|
||||
_BOT_API_KEYS = {
|
||||
"cma-bot-finance-2026": {"role": "finance", "name": "财务分析Bot", "dimension": "finance"},
|
||||
"cma-bot-customer-2026": {"role": "business", "name": "客户分析Bot", "dimension": "customer"},
|
||||
"cma-bot-process-2026": {"role": "it", "name": "流程分析Bot", "dimension": "process"},
|
||||
"cma-bot-learning-2026": {"role": "ceo", "name": "战略分析Bot", "dimension": "learning"},
|
||||
"cma-bot-finance-2026": {"role": "finance", "name": "财务BOT"},
|
||||
"cma-bot-shop-2026": {"role": "business", "name": "店研学BOT"},
|
||||
"cma-bot-admin-2026": {"role": "ceo", "name": "管理BOT"},
|
||||
}
|
||||
else:
|
||||
try:
|
||||
@@ -463,6 +462,68 @@ def bot_query(
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
def bot_import_excel(
|
||||
file: UploadFile = File(...),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Bot上传Excel导入KPI数据到CMA"""
|
||||
import pandas as pd, io, hashlib
|
||||
from app.models import KPIValue
|
||||
try:
|
||||
content = file.file.read()
|
||||
df = pd.read_excel(io.BytesIO(content))
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"Excel解析失败: {e}")
|
||||
finally:
|
||||
file.file.close()
|
||||
|
||||
# 智能识别列名
|
||||
col_map = {"kpi_code": ["kpi_code", "KPI编码", "指标编码", "code"],
|
||||
"period": ["period", "期间", "月份", "month", "日期"],
|
||||
"actual_value": ["actual_value", "实际值", "值", "金额", "value", "amount"]}
|
||||
|
||||
mapped = {}
|
||||
for field, aliases in col_map.items():
|
||||
for col in df.columns:
|
||||
if str(col).strip() in aliases or str(col).strip().lower() in aliases:
|
||||
mapped[field] = str(col).strip()
|
||||
break
|
||||
|
||||
if "actual_value" not in mapped:
|
||||
raise HTTPException(400, f"无法识别数值列,支持的列名: {col_map['actual_value']}")
|
||||
if "kpi_code" not in mapped:
|
||||
raise HTTPException(400, f"无法识别KPI编码列,支持的列名: {col_map['kpi_code']}")
|
||||
|
||||
kpi_col = mapped["kpi_code"]
|
||||
val_col = mapped["actual_value"]
|
||||
period_col = mapped.get("period")
|
||||
|
||||
count = 0
|
||||
errors = []
|
||||
for idx, row in df.iterrows():
|
||||
try:
|
||||
kpi_code = str(row[kpi_col]).strip()
|
||||
val = float(row[val_col])
|
||||
period = str(row[period_col]).strip() if period_col else datetime.now().strftime("%Y-%m")
|
||||
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code).first()
|
||||
if not kpi:
|
||||
errors.append(f"第{idx+2}行: KPI编码 '{kpi_code}' 不存在,跳过")
|
||||
continue
|
||||
|
||||
kv = KPIValue(kpi_id=kpi.id, period=period, actual_value=val,
|
||||
batch_id=hashlib.md5(f"{datetime.now()}".encode()).hexdigest()[:12])
|
||||
db.add(kv)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"第{idx+2}行: {e}")
|
||||
|
||||
db.commit()
|
||||
return {"ok": True, "imported": count, "errors": len(errors), "detail": errors[:5]}
|
||||
|
||||
|
||||
# ── 自然语言查询 ──
|
||||
|
||||
@router.get("/nlp")
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""企业实体 API"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth
|
||||
from app.models import Entity
|
||||
|
||||
router = APIRouter(prefix="/api/cma/entities", tags=["企业实体"],
|
||||
dependencies=[Depends(require_auth)],
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_entities(db: Session = Depends(get_db)):
|
||||
"""获取企业列表"""
|
||||
entities = db.query(Entity).filter(Entity.status == "active").order_by(Entity.id).all()
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": e.id,
|
||||
"name": e.name,
|
||||
"short_name": e.short_name,
|
||||
"industry": e.industry,
|
||||
}
|
||||
for e in entities
|
||||
]
|
||||
}
|
||||
+10
-2
@@ -8,7 +8,7 @@ import json
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth, require_role, filter_kpis_by_role, kpi_visible_dims
|
||||
from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog
|
||||
from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog, Entity
|
||||
|
||||
router = APIRouter(prefix="/api/cma/kpis", tags=["KPI字典"],
|
||||
dependencies=[Depends(require_role("ceo", "finance", "business", "it"))],
|
||||
@@ -26,6 +26,7 @@ def list_kpis(
|
||||
keyword: Optional[str] = None,
|
||||
epic: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
entity_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user = Depends(require_auth),
|
||||
):
|
||||
@@ -42,9 +43,16 @@ def list_kpis(
|
||||
if category:
|
||||
cats_list = [c.strip() for c in category.split(',')] if ',' in category else [category]
|
||||
query = query.filter(KPIDefinition.category.in_(cats_list))
|
||||
if entity_id is not None:
|
||||
query = query.filter(KPIDefinition.entity_id == entity_id)
|
||||
total = query.count()
|
||||
kpis = query.order_by(KPIDefinition.kpi_code).offset((page-1)*page_size).limit(page_size).all()
|
||||
return {"total": total, "page": page, "page_size": page_size, "data": [kpi_to_dict(k) for k in kpis]}
|
||||
result = {"total": total, "page": page, "page_size": page_size, "data": [kpi_to_dict(k) for k in kpis]}
|
||||
if entity_id is not None:
|
||||
ent = db.query(Entity).filter(Entity.id == entity_id).first()
|
||||
if ent:
|
||||
result["entity"] = {"id": ent.id, "name": ent.name, "short_name": ent.short_name}
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
|
||||
+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, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports
|
||||
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, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities
|
||||
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
|
||||
@@ -62,6 +62,7 @@ app.include_router(knowledge_articles.router)
|
||||
app.include_router(kpi_causality.router)
|
||||
app.include_router(data_quality.router)
|
||||
app.include_router(bi_reports.router)
|
||||
app.include_router(entities.router)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
|
||||
@@ -7,6 +7,18 @@ from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllo
|
||||
from app.models.knowledge import KnowledgeEvent, KnowledgeSummary
|
||||
|
||||
|
||||
class Entity(Base):
|
||||
"""企业实体"""
|
||||
__tablename__ = "entities"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False, comment="企业全称")
|
||||
short_name = Column(String(50), comment="企业简称")
|
||||
industry = Column(String(50), comment="行业")
|
||||
status = Column(String(20), default="active", comment="active/inactive/demo")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户"""
|
||||
__tablename__ = "users"
|
||||
@@ -37,6 +49,7 @@ class KPIDefinition(Base):
|
||||
"""KPI字典"""
|
||||
__tablename__ = "kpi_definitions"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, default=1, comment="企业ID")
|
||||
map_id = Column(Integer, ForeignKey("strategic_maps.id"), nullable=True, comment="关联战略地图")
|
||||
kpi_code = Column(String(50), unique=True, nullable=False, comment="KPI编码")
|
||||
kpi_name = Column(String(200), nullable=False, comment="KPI名称")
|
||||
|
||||
Reference in New Issue
Block a user