diff --git a/backend/app/api/bot_bridge.py b/backend/app/api/bot_bridge.py index 2455a641..95622085 100644 --- a/backend/app/api/bot_bridge.py +++ b/backend/app/api/bot_bridge.py @@ -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") diff --git a/backend/app/api/entities.py b/backend/app/api/entities.py new file mode 100644 index 00000000..0575682e --- /dev/null +++ b/backend/app/api/entities.py @@ -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 + ] + } diff --git a/backend/app/api/kpis.py b/backend/app/api/kpis.py index 88639818..24608357 100644 --- a/backend/app/api/kpis.py +++ b/backend/app/api/kpis.py @@ -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") diff --git a/backend/app/main.py b/backend/app/main.py index 1d526682..e81e06e1 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, 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): diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index bd5361a5..218e20d9 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -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名称")