Files
cma-management/backend/app/main.py
T
Hermes CI Fix 59e65a68b9 fix(alerts): cash_plan 到期未收款预警引擎修复 — entity归属/KPI实体感知/余额快照批次/去重刷新/端点鉴权 (dama-cashplan-alert-engine-fix-20260913)
F1 两处 KPIAlert 显式写 entity_id(列默认 1 → 博海预警错挂酣客);顺带修 deviation_engine/
   alert_generator/deviation_push/bot_bridge/bot_bridge_v2 同表创建点
F2 KPI 选择实体感知:候选码 ∪ 实体前缀后缀码(_AR_BALANCE/_CASH_BALANCE/_AP_BALANCE)
   + 兜底 ORDER BY id(此前命中 uk_entity_kpi 必中 BH_ADM_EXPENSE 437 管理费用)
F3 去重命中改为刷新 alert_message/等级/suggestion.days_late/归属(此前 continue → 文本冻结首版)
F4 余额快照批次(bohai_*_20260630 / receivables_migrate)开关:默认 skip 不逐户,另备
   aggregate(按批次聚合1条) / per_customer(旧行为)
F5 POST /api/cma/admin/alerts/check 补鉴权(无 token 401)+ 可选 entity_id 只跑指定账套
2026-09-13 00:37:10 +08:00

200 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""管理会计OS — 主入口"""
import logging
from typing import Optional
from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from dotenv import load_dotenv
from app.database import init_db, get_db
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, lead, tenant, 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, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json
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
from app.api.audit_log import bot_audit_middleware
load_dotenv()
# 日志配置
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("cma")
app = FastAPI(title="管理会计OS API", version="1.0.0", docs_url="/docs")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Bot API 操作审计(L1-L4分级标注 + JSON行审计日志,不阻塞业务)
app.middleware("http")(bot_audit_middleware)
app.include_router(auth.router)
app.include_router(kpis.router)
app.include_router(kpi_governance.router)
app.include_router(templates.router)
app.include_router(maps.router)
app.include_router(dashboard.router)
app.include_router(data.router)
app.include_router(alerts.router)
app.include_router(ai_analysis.router)
app.include_router(ai_suggestions.router)
app.include_router(bot_suggestions.router)
app.include_router(alert_rules.router)
app.include_router(users.router)
app.include_router(thresholds.router)
app.include_router(notifications.router)
app.include_router(permissions.router)
app.include_router(action_plans.router)
app.include_router(alignment.router)
app.include_router(org.router)
app.include_router(objectives.router)
app.include_router(versions.router)
app.include_router(budget.router)
app.include_router(value_sources.router)
app.include_router(zero_based.router)
app.include_router(derivation_rules.router)
app.include_router(cash_classify.router)
app.include_router(cashflow_structure.router)
app.include_router(cashflow_structure.compat_router)
app.include_router(cost.router)
app.include_router(predict.router)
app.include_router(growth_quality.router)
app.include_router(products.router)
app.include_router(reports.router)
app.include_router(security.router)
app.include_router(knowledge.router)
app.include_router(bot_bridge.router)
app.include_router(bot_bridge_v2.router)
app.include_router(lead.router)
app.include_router(tenant.router)
app.include_router(customer_dashboard.router)
app.include_router(deviation_push.router)
app.include_router(budget_generate.router)
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.include_router(bsc_layers.router)
app.include_router(okr.router)
app.include_router(okr_templates.router)
app.include_router(subjects.router)
app.include_router(driver_budget.router)
app.include_router(bot_kpis.router)
app.include_router(ontology.router)
app.include_router(bot_iron_law.router)
app.include_router(analysis_results.router)
app.include_router(expenses.router)
app.include_router(cash.router)
app.include_router(cash_vouchers_json.router)
app.include_router(data_classification.router)
app.include_router(tax_compliance.router)
app.include_router(verify.router)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"未捕获异常: {exc}", exc_info=True)
return JSONResponse(status_code=500, content={"detail": "服务器内部错误"})
@app.on_event("startup")
def startup():
init_db()
logger.info("管理会计OS后端启动完成")
# ERP 直连同步通道停用(2026-08-31 起 erp-gateway DB 凭据删除;真实财务数据走 zxt 智享通注入)
_ERP_CHANNEL_DISABLED_REASON = ("ERP直连同步通道已于2026-08-31停用(erp-gateway DB 凭据已删除,zxbtest 账号停用)。"
"财务原始数据真实通道为 zxt 智享通注入(source_type='zxt-sync'),本端点不再可用。")
# ── admin 端点鉴权(2026-09-13)──────────────────────────────
# 未带 token → 显式 401(缺少认证),校验逻辑仍完全委托 require_auth(无效 token → 401)。
# 说明:FastAPI 的 HTTPBearer(auto_error=True) 对"缺 header"返回 403;本端点验收要求 401,
# 故用 auto_error=False 的实例自行判空,不改动全局 require_auth(避免其他端点行为漂移)。
_optional_bearer = HTTPBearer(auto_error=False)
def require_auth_401(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_optional_bearer),
db=Depends(get_db),
):
if credentials is None or not credentials.credentials:
raise HTTPException(status_code=401, detail="未认证:缺少 Bearer token")
return require_auth(credentials=credentials, db=db)
@app.post("/api/cma/admin/erp-sync")
def admin_erp_sync(user=Depends(require_auth), kpi_codes: str = None):
"""[已停用] 手动触发ERP数据同步 — ERP 直连通道 2026-08-31 停用,返回 410"""
return JSONResponse(status_code=410, content={"detail": _ERP_CHANNEL_DISABLED_REASON})
@app.get("/api/cma/admin/erp-sync/dry-run")
def admin_erp_sync_dry_run(user=Depends(require_auth), kpi_codes: str = None):
"""[已停用] 试运行 — ERP 直连通道 2026-08-31 停用,返回 410"""
return JSONResponse(status_code=410, content={"detail": _ERP_CHANNEL_DISABLED_REASON})
@app.post("/api/cma/admin/alerts/check")
def admin_check_alerts(entity_id: Optional[int] = None, user=Depends(require_auth_401)):
"""手动触发预警检查(含资金管理预警:缺口前3天 + 到期未收款)
- 2026-09-13 安全修复:补鉴权(与同文件 /admin/erp-sync 等 admin 端点一致);
未带 token 显式返回 401(require_auth_401,语义修正:401=未认证 / 403=无权)
- 新增可选 entity_id:只跑指定实体(不传=全部 active 实体,兼容原行为)
"""
from app.database import get_session_local
from scripts.alert_generator import generate_and_push
from app.utils.cash_forecast_engine import check_cash_alerts
from app.models import Entity
db = get_session_local()()
try:
result = generate_and_push(db)
# 资金管理预警 — 指定实体或全部激活企业
cash_result = None
try:
if entity_id is not None:
target = db.query(Entity).filter(
Entity.id == entity_id, Entity.status == "active").first()
if not target:
return JSONResponse(status_code=404,
content={"detail": f"企业不存在或未激活: entity_id={entity_id}"})
entities = [target]
else:
entities = db.query(Entity).filter(Entity.status == "active").all()
cash_list = []
for e in entities:
cash_list.append(check_cash_alerts(db, entity_id=e.id))
cash_result = {"entities": len(entities), "details": cash_list}
except Exception as e:
logger.warning(f"资金预警检查失败: {e}")
return {"message": "预警检查完成", "result": result, "cash": cash_result}
except Exception as e:
return JSONResponse(status_code=500, content={"detail": f"检查失败: {str(e)}"})
finally:
db.close()
@app.post("/api/cma/admin/cache/clear")
def admin_clear_cache(module: str = None):
"""清空缓存,指定module则只清该模块"""
if module:
delete_cache(module)
return {"message": f"缓存已清空: {module}"}
else:
clear_cache()
return {"message": "全部缓存已清空"}
@app.get("/health")
def health():
return {"status": "ok", "version": "1.0.0"}