装闭 RenoPit 源码解析(13):生成AI装修闭坑PDF报告

1785882998954

网页报告适合在线浏览,装修沟通中还需要可以保存和转发的 PDF。装闭 RenoPit 在开源仓库 fthux/RenoPit 中使用 ReportLab,把与 React 报告页相同的数据生成 A4 中文文档。

一、前端如何发起 PDF 下载

AnalysisPage 点击下载按钮后请求 /api/projects/{id}/report/pdf。响应不是 JSON,而是 application/pdf 二进制数据。前端把它转换成 Blob,再创建临时 URL 和 <a> 元素触发下载:

const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `renopit-report-${projectId.slice(0, 8)}.pdf`
a.click()
URL.revokeObjectURL(url)

下载状态由 downloading 控制,接口失败时读取后端 detail 并显示 Toast。

二、PDF 接口为什么再次调用内部 API

download_report_pdf() 没有在函数中重新实现结果转换,而是通过 httpx.AsyncClient 请求本机 FastAPI:

  1. /api/projects/{id}/result 获取与 React 相同的聚合结果;
  2. /api/projects/{id}/images?include_storage_path=true 获取图片及磁盘路径。

容器内使用 http://localhost:8000,因为请求发生在 backend 容器内部。这样 PDF 与网页共享 _build_result_data()、文档序列化和交叉核查注入逻辑,不会维护第二套结果映射。

三、ReportLab 如何创建文档

generate_pdf() 首先调用 _ensure_font_registered()。如果配置的中文 TTF 字体存在,就通过 TTFont 注册;否则样式回退到 Helvetica。Docker 环境通过 FONT_PATH 指定 Noto Sans CJK 等中文字体。

生成过程使用内存缓冲区:

buf = io.BytesIO()
doc = BaseDocTemplate(
    buf,
    pagesize=A4,
    leftMargin=25 * mm,
    rightMargin=25 * mm,
    topMargin=25 * mm,
    bottomMargin=25 * mm,
)

文档注册 CoverContent 两套 PageTemplate。封面模板负责首页样式,内容模板负责后续页眉、页脚和页码。doc.build(story) 将 Flowable 列表排版到 PDF,最后从 BytesIO 取出字节。

四、story 如何组织报告章节

_build_content() 按固定顺序向 story 添加 ParagraphTableSpacerImagePageBreak

flowchart TD A[封面与基本信息] --> B[综合评分] B --> C[严重程度统计] C --> D[装修问题详情] D --> E[合同与报价单风险] E --> F[增项预测] F --> G[跨文档核查] G --> H[设计图纸]

没有文档、增项或图片时,对应章节会被跳过。文档分析按创建时间倒序排列,增项预测只渲染找到的第一组结果。

五、评分和风险卡片如何绘制

评分卡使用 Table 创建数字、等级和进度条。分数低于 60 使用高风险色,60~79 使用中风险色,80 以上使用强调色;等级文字与网页报告保持一致。

每个装修问题由 _build_pitfall_card() 生成表格卡片。标题行包含风险圆点和严重程度,后续行按数据情况追加分类、位置、问题分析、陷阱说明和建议。所有来自 LLM 的文本先经过 _sanitize_html(),避免特殊字符破坏 ReportLab 的 Paragraph 标记。

合同风险、增项预测和跨文档差异各自有独立构建函数,最终都返回 Flowable 或更新后的 story

六、设计图片如何嵌入

_build_image_section() 根据 storage_path 检查图片存在,再使用 Pillow 读取尺寸。图片会在 160mm × 120mm 范围内等比缩放,然后作为 ReportLab Image 加入文档。

函数还会查找分析问题中的 bbox 和位置描述。如果问题能够与当前图片名对应,就缩放坐标并在图片下方列出相关风险;没有定位信息时显示“该图片未检测到具体问题位置”。

七、Report 记录和 HTTP 响应

PDF 生成成功后,接口查询最近一次已完成分析。如果项目还没有 Report 记录,就创建一条包含项目 ID、分析 ID 和文件路径的记录。记录保存失败只写警告,不阻止本次字节响应返回。

最终响应设置 Content-Disposition,文件名使用项目 UUID 前八位。前端下载结束后释放临时 Object URL。

八、PDF 生成链路小结

RenoPit 的 PDF 链路是“复用结果 API、收集图片路径、构造 Flowable、内存生成字节、记录 Report、浏览器 Blob 下载”。网页与 PDF 消费同一份聚合数据,因此问题数量、评分和文档章节保持一致。

完整的 ReportLab 样式和章节构建代码位于 fthux/RenoPit。最后一篇将分析不启动后端也能运行的 Demo 模式,以及健康检查、Nginx 和 Docker Compose 如何组成项目运行环境。

posted on 2026-08-19 23:46  fthux  阅读(0)  评论(0)    收藏  举报