3 new tables + idempotent DDL migration (prod guard: refuses DB name cma without approval): tax_filing_log (P_TAX_TIMELY), hr_monthly_ledger (L_EMPLOYEE_TURNOVER), licence_ledger (P_LICENCE_OK). Entry API /api/cma/ext-ledger: create / CSV batch import / query by entity+period / derive trigger. on_time is server-derived only; HR rows rejected when end != begin + hires - separations. Deriver writes kpi_values only (source_type=derived, idempotent upsert); denominator 0 returns None and writes nothing; never creates or changes kpi_definitions. Tests: 22 new pytest cases (22 passed); scratch-DB e2e battery 28 assertions PASS with zero production write.
196 lines
8.7 KiB
Python
196 lines
8.7 KiB
Python
"""管理会计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, ext_input_ledgers
|
||
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, require_auth_401
|
||
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.include_router(ext_input_ledgers.router) # 外部输入台账(M2 准入3 派生,2026-09-15)
|
||
|
||
@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)。
|
||
# 依赖实现已上移到 app/auth_middleware.py 的 require_auth_401(用户端点统一入口,
|
||
# cma-api-auth-surface-audit-20260914):原局部定义等价迁移,其他端点行为不变。
|
||
|
||
|
||
@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, user=Depends(require_auth_401)):
|
||
"""清空缓存,指定module则只清该模块
|
||
|
||
2026-09-14 安全修复(cma-api-auth-surface-audit-20260914):本端点原为无鉴权公开
|
||
可写端点(无 token 亦可清空全站缓存 → 可被用于性能打击);补 require_auth_401,
|
||
未带 token → 401。
|
||
"""
|
||
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"}
|