63031dd183
- 后端:reports_export 路由(summary 聚合 + JSON 导出) - 测试:3 个导出测试,总计 67 passed - docker-compose.dev.yml:开发覆盖(源码挂载 + hot reload + debugpy) - 前端 14 路由构建成功
212 lines
6.9 KiB
Python
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"',
|
|
},
|
|
)
|