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")
|
||||
|
||||
Reference in New Issue
Block a user