Files
selfrelease 63031dd183 feat: T1.7 投后报告导出 + T1.8 docker-compose.dev.yml + T0.4 补全
- 后端:reports_export 路由(summary 聚合 + JSON 导出)
- 测试:3 个导出测试,总计 67 passed
- docker-compose.dev.yml:开发覆盖(源码挂载 + hot reload + debugpy)
- 前端 14 路由构建成功
2026-07-18 22:21:51 +08:00

212 lines
6.9 KiB
Python

"""投后报告生成 + PDF 导出路由。"""
import json
import logging
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import get_current_user
from app.models.company import Company
from app.models.health_score import HealthScore
from app.models.report import MonthlyReport
from app.models.risk import RiskEvent
from app.models.user import User
from app.schemas.common import ApiResponse, success
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/reports-export", tags=["reports-export"])
@router.get("/{company_id}/summary")
async def get_company_summary(
company_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取企业投后报告汇总数据(用于前端展示或导出)。"""
# 验证企业
company_result = await db.execute(
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
)
company = company_result.scalar_one_or_none()
if not company:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
# 获取最新月报
reports_result = await db.execute(
select(MonthlyReport)
.where(MonthlyReport.company_id == company_id)
.order_by(MonthlyReport.period_year.desc(), MonthlyReport.period_month.desc())
.limit(12)
)
reports = reports_result.scalars().all()
# 获取最新健康度评分
score_result = await db.execute(
select(HealthScore)
.where(HealthScore.company_id == company_id)
.order_by(HealthScore.calculated_at.desc())
.limit(12)
)
scores = score_result.scalars().all()
# 获取风险事件
risk_result = await db.execute(
select(RiskEvent)
.where(RiskEvent.company_id == company_id)
.order_by(RiskEvent.identified_at.desc())
.limit(20)
)
risks = risk_result.scalars().all()
summary = {
"company": {
"name": company.name,
"industry": company.industry,
"stage": company.stage,
"description": company.description,
"website": company.website,
},
"latest_score": {
"total_score": scores[0].total_score if scores else None,
"financial_score": scores[0].financial_score if scores else None,
"operational_score": scores[0].operational_score if scores else None,
"ai_commercial_score": scores[0].ai_commercial_score if scores else None,
"ai_cost_score": scores[0].ai_cost_score if scores else None,
"trend": scores[0].trend if scores else None,
} if scores else None,
"score_history": [
{
"total_score": s.total_score,
"calculated_at": s.calculated_at.isoformat(),
"trend": s.trend,
}
for s in scores
],
"recent_reports": [
{
"period": f"{r.period_year}-{r.period_month:02d}",
"status": r.status,
"ai_summary": r.ai_summary,
}
for r in reports
],
"risks": [
{
"title": r.title,
"severity": r.severity,
"status": r.status,
"type": r.type,
"description": r.description,
}
for r in risks
],
"generated_at": datetime.now(timezone.utc).isoformat(),
}
return success(data=summary)
@router.get("/{company_id}/pdf")
async def export_pdf(
company_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""导出企业投后报告为 JSON 格式(前端可转 PDF)。
当前返回 JSON 格式的完整报告数据,前端可使用浏览器打印功能生成 PDF。
后续可集成 weasyprint 等库生成服务端 PDF。
"""
# 复用 summary 逻辑
company_result = await db.execute(
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
)
company = company_result.scalar_one_or_none()
if not company:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
# 获取数据(与 summary 相同逻辑)
reports_result = await db.execute(
select(MonthlyReport)
.where(MonthlyReport.company_id == company_id)
.order_by(MonthlyReport.period_year.desc(), MonthlyReport.period_month.desc())
.limit(12)
)
reports = reports_result.scalars().all()
score_result = await db.execute(
select(HealthScore)
.where(HealthScore.company_id == company_id)
.order_by(HealthScore.calculated_at.desc())
.limit(12)
)
scores = score_result.scalars().all()
risk_result = await db.execute(
select(RiskEvent)
.where(RiskEvent.company_id == company_id)
.order_by(RiskEvent.identified_at.desc())
.limit(20)
)
risks = risk_result.scalars().all()
report_data = {
"title": f"投后管理报告 — {company.name}",
"generated_at": datetime.now(timezone.utc).isoformat(),
"company": {
"name": company.name,
"industry": company.industry,
"stage": company.stage,
"description": company.description,
},
"health_scores": [
{
"total": s.total_score,
"financial": s.financial_score,
"operational": s.operational_score,
"ai_commercial": s.ai_commercial_score,
"ai_cost": s.ai_cost_score,
"trend": s.trend,
"date": s.calculated_at.isoformat(),
}
for s in scores
],
"monthly_reports": [
{
"period": f"{r.period_year}{r.period_month}",
"status": r.status,
"summary": r.ai_summary,
}
for r in reports
],
"risk_events": [
{
"title": r.title,
"severity": r.severity,
"status": r.status,
"description": r.description,
"suggested_action": r.suggested_action,
}
for r in risks
],
}
# 返回可下载的 JSON 文件
json_str = json.dumps(report_data, ensure_ascii=False, indent=2)
from urllib.parse import quote
safe_name = quote(company.name)
return StreamingResponse(
iter([json_str.encode("utf-8")]),
media_type="application/json",
headers={
"Content-Disposition": f'attachment; filename="report_{safe_name}_{datetime.now().strftime("%Y%m%d")}.json"',
},
)