feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限

This commit is contained in:
Hermes CI Fix
2026-07-12 17:46:08 +08:00
parent cdf00efd69
commit ee25d5fa1d
8871 changed files with 1778433 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
"""知识摘要 API — 管理会计OS持久记忆
提供:
- 查询最近摘要列表
- 查询单个摘要详情
- 手动触发各层级摘要生成
- 查询未摘要的事件
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, desc
from datetime import datetime, timedelta
from typing import Optional
from app.database import get_db
from app.auth_middleware import require_role
from app.models import KnowledgeEvent, KnowledgeSummary
from app.services.knowledge_service import (
generate_summary_sync,
generate_daily_sync,
generate_weekly_sync,
generate_monthly_sync,
get_last_summary,
extract_events,
)
import logging
logger = logging.getLogger("cma.knowledge_api")
router = APIRouter(prefix="/api/cma/knowledge", tags=["知识摘要"],
dependencies=[Depends(require_role("ceo", "finance", "business", "it"))],
)
def summary_to_dict(s: KnowledgeSummary) -> dict:
return {
"id": s.id,
"level": s.level,
"period_key": s.period_key,
"title": s.title,
"content": s.content,
"kpi_changes": s.kpi_changes,
"decision_points": s.decision_points,
"key_metrics": s.key_metrics,
"prev_summary_id": s.prev_summary_id,
"model": s.model,
"is_stale": s.is_stale,
"created_at": s.created_at.isoformat() if s.created_at else None,
}
# ── 查询 ──
@router.get("/summaries")
def list_summaries(
level: Optional[str] = None,
limit: int = 20,
offset: int = 0,
db: Session = Depends(get_db),
):
"""获取摘要列表,按层级筛选,按时间倒序"""
query = db.query(KnowledgeSummary)
if level:
query = query.filter(KnowledgeSummary.level == level)
query = query.order_by(desc(KnowledgeSummary.id)).offset(offset).limit(limit)
total = db.query(func.count(KnowledgeSummary.id)).select_from(KnowledgeSummary)
if level:
total = total.filter(KnowledgeSummary.level == level)
total = total.scalar()
return {
"total": total,
"items": [summary_to_dict(s) for s in query.all()],
}
@router.get("/summaries/latest")
def latest_summary(
level: str = Query("daily", description="层级: daily/weekly/monthly/cumulative"),
db: Session = Depends(get_db),
):
"""获取指定层级的最新摘要"""
s = get_last_summary(db, level)
if not s:
return {"detail": f"没有{level}层级的摘要"}, 404
return summary_to_dict(s)
@router.get("/summaries/{summary_id}")
def get_summary(summary_id: int, db: Session = Depends(get_db)):
"""获取单条摘要详情"""
s = db.query(KnowledgeSummary).filter(KnowledgeSummary.id == summary_id).first()
if not s:
raise HTTPException(status_code=404, detail="摘要不存在")
return summary_to_dict(s)
# ── 事件查询 ──
@router.get("/events")
def list_events(
since: Optional[str] = None,
until: Optional[str] = None,
limit: int = 50,
db: Session = Depends(get_db),
):
"""查询未摘要的原始事件
如果不传时间,默认返回最近7天的操作记录和预警。
"""
try:
dt_since = datetime.fromisoformat(since) if since else datetime.utcnow() - timedelta(days=7)
dt_until = datetime.fromisoformat(until) if until else datetime.utcnow()
except ValueError:
raise HTTPException(status_code=400, detail="时间格式错误,请使用 ISO 格式如 2026-06-01T00:00:00")
events = extract_events(db, dt_since, dt_until)
return {"since": dt_since.isoformat(), "until": dt_until.isoformat(), "total": len(events), "events": events[:limit]}
# ── 手动触发 ──
@router.post("/generate/daily")
def trigger_daily_summary(db: Session = Depends(get_db)):
"""手动触发每日摘要生成"""
try:
result = generate_daily_sync(db)
return {"message": "每日摘要已生成", "summary": result}
except Exception as e:
logger.exception("每日摘要生成失败")
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
@router.post("/generate/weekly")
def trigger_weekly_summary(db: Session = Depends(get_db)):
"""手动触发周度摘要生成"""
try:
result = generate_weekly_sync(db)
return {"message": "周度摘要已生成", "summary": result}
except Exception as e:
logger.exception("周度摘要生成失败")
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
@router.post("/generate/monthly")
def trigger_monthly_summary(db: Session = Depends(get_db)):
"""手动触发月度摘要生成"""
try:
result = generate_monthly_sync(db)
return {"message": "月度摘要已生成", "summary": result}
except Exception as e:
logger.exception("月度摘要生成失败")
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")