feat(report): 报告 PDF 导出管线(Phase A,WeasyPrint 服务端渲染)

- POST /reports/{id}/export:request_key 幂等 + hd_export 日配额三段式(reserve/commit/refund)+ tier2 熔断场景闸(BudgetGuardDep),仅 succeeded 报告可导
- GET /reports/{id}/export/download:归属校验 + 路径收敛(rendered_asset_key 必须落在 report_export_dir 内),FileResponse 携带 UTF-8 文件名
- celery 任务 meida.report.export(cpu 队列):渲染→落盘 var/exports→回写 rendered_asset_key;业务性拒绝(报告状态变化)结案退款不重试,基础设施异常重试一次后兜底结案退款
- packages/report/pdf:Jinja2 模板 + WeasyPrint 渲染器(url_fetcher 全拒外部资源防 SSRF;维度中文名按 DIMENSION_BY_KEY 回填;色板维度渲染色卡)
- 单测 10 例(资格/幂等/并发同键/下载校验/渲染);scripts/seed_pdf_e2e.sql 供本地 E2E 造数
This commit is contained in:
xsl
2026-08-30 13:17:23 +08:00
parent cb8a1e3c0f
commit cd0ddb131b
17 changed files with 1507 additions and 5 deletions
+16
View File
@@ -191,6 +191,21 @@ def storage_service_dep(settings: SettingsDep) -> StorageService:
return StorageService(settings)
def budget_guard_dep(settings: SettingsDep) -> DailyBudgetGuard:
"""全局日预算的场景闸(架构 §13.3):hd_export 等可降级场景在请求入口先过它。
每次请求新建实例是刻意的:Guard 本身无状态(水位读 Redis),
与 build_llm_gateway 里给网关用的那只互不影响。
"""
return DailyBudgetGuard(
RedisCostCounter(get_redis()),
daily_budget_cents=Decimal(settings.llm_daily_budget_cents),
enabled=settings.llm_daily_budget_enabled,
tz_offset_hours=settings.llm_budget_timezone_offset_hours,
tiers=list(settings.llm_budget_tiers),
)
def agent_service_dep(
session: SessionDep,
gateway: GatewayDep,
@@ -204,4 +219,5 @@ BillingServiceDep = Annotated[BillingService, Depends(billing_service_dep)]
TaskServiceDep = Annotated[TaskService, Depends(task_service_dep)]
AnalyticsServiceDep = Annotated[AnalyticsService, Depends(analytics_service_dep)]
StorageServiceDep = Annotated[StorageService, Depends(storage_service_dep)]
BudgetGuardDep = Annotated[DailyBudgetGuard, Depends(budget_guard_dep)]
AgentServiceDep = Annotated[AgentService, Depends(agent_service_dep)]