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:
Hermes CI Fix
2026-07-18 23:17:55 +08:00
parent 70e5b83a9f
commit 59f4ab2c76
5 changed files with 120 additions and 8 deletions
+66 -5
View File
@@ -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")
+29
View File
@@ -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
View File
@@ -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")