- 实现 ScreenshotService,通过 HtmlToImage 异步任务 API 截图 - 实现 PdfGenerationService,将截图合并为 PDF 并保存到本地 - 在 ReportQueueConsumer 中集成 PDF 生成流程 - 添加 HtmlToImageSettings、ReportSettings 配置模型 - AssessmentRecord 新增 ReportUrl 字段 - 添加 DebugController 用于手动触发 PDF 生成测试 - 添加 PdfSharpCore NuGet 包依赖 - 更新 .gitignore 忽略生成的 PDF 文件
11 KiB
11 KiB
Implementation Plan: PDF 报告生成
Overview
在现有报告生成流水线基础上,新增 PDF 生成环节。当 ReportGenerationService 完成结论数据生成(Status=4)后,通过外部 HtmlToImage 异步任务 API 对报告页面截图,将所有图片按 SortOrder 合并为 PDF 文件(PdfSharpCore),保存到本地磁盘并将访问 URL 写入 assessment_records.ReportUrl。
Tasks
-
1. 配置模型与数据库变更
- 1.1 创建 HtmlToImageSettings 配置模型
- 在
MiAssessment.Core/Models/HtmlToImageSettings.cs创建配置类 - 包含 BaseUrl、ApiKey、TimeoutSeconds、PollingIntervalMs(默认 1000)、MaxPollingSeconds(默认 120)属性
- 所有属性添加 XML 注释
- Requirements: 1.1, 1.2
- 在
- 1.2 创建 ReportSettings 配置模型
- 在
MiAssessment.Core/Models/ReportSettings.cs创建配置类 - 包含 BaseUrl、OutputPath(默认
wwwroot/reports)、MaxConcurrency(默认 5)属性 - Requirements: 6.1, 8.1, 10.1
- 在
- 1.3 在 AppSettings 中添加 CdnPrefix 属性
- 在
MiAssessment.Model/Models/Auth/AppSettings.cs中添加CdnPrefix字符串属性(默认空字符串) - appsettings.json 中已有
"CdnPrefix": "",确保模型能正确绑定 - Requirements: 6.3
- 在
- 1.4 在 AssessmentRecord 实体添加 ReportUrl 字段
- 在
MiAssessment.Model/Entities/AssessmentRecord.cs添加ReportUrl属性(nvarchar(500),可空) - 添加
[MaxLength(500)]特性和 XML 注释 - Requirements: 9.1
- 在
- 1.5 执行数据库迁移脚本
- 创建 SQL 脚本
ALTER TABLE assessment_records ADD ReportUrl NVARCHAR(500) NULL; - 放置在
temp_sql/目录下 - Requirements: 9.1
- 创建 SQL 脚本
- 1.1 创建 HtmlToImageSettings 配置模型
-
2. Checkpoint - 确认配置模型和数据库变更
- Ensure all tests pass, ask the user if questions arise.
-
3. 截图服务实现
- 3.1 创建 IScreenshotService 接口
- 在
MiAssessment.Core/Interfaces/IScreenshotService.cs定义接口 - 包含
Task<byte[]> CaptureAsync(string url)方法 - 添加 XML 注释
- Requirements: 1.3
- 在
- 3.2 实现 ScreenshotService
- 在
MiAssessment.Core/Services/ScreenshotService.cs实现 IScreenshotService - 通过
IHttpClientFactory.CreateClient("HtmlToImage")获取 HttpClient - 注入
HtmlToImageSettings和ILogger<ScreenshotService> - 实现异步任务三步流程:
- 步骤 1:
POST /api/tasks/image提交任务(source.type=url, format=png, width=1309, height=926, fullPage=false, waitUntil=networkidle0, saveLocal=true),解析响应获取 taskId - 步骤 2:
GET /api/tasks/{taskId}/status轮询状态,间隔 PollingIntervalMs,直到 completed/failed/stalled 或超过 MaxPollingSeconds - 步骤 3:
GET /api/tasks/{taskId}/download下载 PNG 字节数组
- 步骤 1:
- 任务提交失败时调用
EnsureSuccessStatusCode()抛出 HttpRequestException - 任务状态为 failed/stalled 时抛出 InvalidOperationException
- 轮询超时时抛出 TimeoutException
- 下载结果为空时抛出 InvalidOperationException
- 所有失败场景记录 Error 日志(含 URL、taskId、HTTP 状态码)
- Requirements: 1.1, 1.3, 1.4, 1.5, 1.6, 1.7
- 在
- * 3.3 编写 ScreenshotService 单元测试
- 在测试项目中创建
ScreenshotServiceTests.cs - 使用 MockHttpMessageHandler 模拟 HTTP 响应
- 验证提交任务时的请求参数正确(source.type=url、format=png、width=1309、height=926、fullPage=false、waitUntil=networkidle0)
- 验证设置了 X-API-Key 请求头
- 验证轮询状态直到 completed 后下载结果
- 验证任务 failed 时抛出 InvalidOperationException
- 验证轮询超时时抛出 TimeoutException
- Requirements: 1.3, 1.5, 1.6, 1.7
- 在测试项目中创建
- 3.1 创建 IScreenshotService 接口
-
4. PDF 生成服务实现
- 4.1 创建 IPdfGenerationService 接口
- 在
MiAssessment.Core/Interfaces/IPdfGenerationService.cs定义接口 - 包含
Task GeneratePdfAsync(long recordId)方法 - 添加 XML 注释
- Requirements: 7.2
- 在
- 4.2 实现 PdfGenerationService 核心逻辑
- 在
MiAssessment.Core/Services/PdfGenerationService.cs实现 IPdfGenerationService - 注入
MiAssessmentDbContext、IScreenshotService、ReportSettings、AppSettings、ILogger<PdfGenerationService> - 实现
GeneratePdfAsync(long recordId)方法:- 验证
ReportSettings.BaseUrl非空,否则抛出 InvalidOperationException - 查询
report_page_configs中 Status=1 的记录,按 SortOrder 升序 - 无启用配置时抛出 InvalidOperationException
- 使用 SemaphoreSlim(MaxConcurrency) 控制并发
- PageType=1:从
wwwroot/images/static-pages/读取图片,文件不存在时记录 Warning 跳过 - PageType=2:拼接完整 URL 调用
IScreenshotService.CaptureAsync,失败时记录 Error 跳过 - 过滤失败页面,按 SortOrder 排序
- 所有页面均失败时抛出 InvalidOperationException
- 部分失败时记录跳过的页面数量和名称
- 验证
- Requirements: 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 4.1, 4.3, 4.4, 5.4, 5.5, 8.1, 8.2, 10.1, 10.2, 10.3
- 在
- 4.3 实现 BuildPageUrl 静态方法
- 在 PdfGenerationService 中实现
internal static string BuildPageUrl(string baseUrl, string routeUrl, long recordId) - 处理 baseUrl 末尾斜杠、routeUrl 前导斜杠
- 已有查询参数时使用
&追加 recordId - 已包含 recordId 参数时不重复追加
- 确保不出现双斜杠(协议部分除外)
- Requirements: 4.1, 4.2, 8.3
- 在 PdfGenerationService 中实现
- 4.4 实现 PDF 合并与文件保存逻辑
- 使用 PdfSharpCore 创建 PDF 文档
- 每页尺寸设置为 981.75pt × 694.5pt(1309×926px 按 72/96 换算)
- 横版布局,页面边距为 0,图片铺满整页
- 文件名格式:
report_{recordId}_{yyyyMMddHHmmss}.pdf - 自动创建输出目录(
Directory.CreateDirectory) - 保存后更新
assessment_records.ReportUrl为{CdnPrefix}/reports/{fileName} - CdnPrefix 为空时 URL 以
/reports/开头 - 记录 Info 日志(含 recordId、文件路径)
- Requirements: 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 6.4, 6.5, 7.3, 9.2
- * 4.5 编写 BuildPageUrl 属性测试
- Property 1: URL 拼接正确性
- 使用 FsCheck 生成随机 baseUrl、routeUrl、recordId
- 验证结果不含双斜杠(协议部分除外)、包含 recordId 参数且仅出现一次、已有 recordId 时不重复追加
- Validates: Requirements 4.1, 4.2, 8.3
- * 4.6 编写 PDF 页面顺序属性测试
- Property 2: PDF 页面顺序与 SortOrder 一致
- 使用 FsCheck 生成随机 SortOrder 列表和图片字节数组
- 验证 PDF 页面顺序严格按 SortOrder 升序排列
- Validates: Requirements 5.1, 10.3
- * 4.7 编写部分失败容错性属性测试
- Property 3: 部分页面失败时的容错性
- 使用 FsCheck 生成混合成功/失败的页面配置
- 验证 PDF 恰好包含所有成功页面,不包含失败页面
- Validates: Requirements 4.4, 5.5
- * 4.8 编写文件名格式属性测试
- Property 4: 文件名格式正确性
- 使用 FsCheck 生成随机 recordId 和 DateTime
- 验证文件名匹配
^report_\d+_\d{14}\.pdf$ - Validates: Requirements 6.2
- * 4.9 编写 ReportUrl 格式属性测试
- Property 5: ReportUrl 格式正确性
- 使用 FsCheck 生成随机 CdnPrefix 和文件名
- 验证 ReportUrl 格式为
{CdnPrefix}/reports/{fileName},CdnPrefix 为空时以/reports/开头 - Validates: Requirements 6.3
- * 4.10 编写 PdfGenerationService 单元测试
- 验证 BaseUrl 为空时抛出异常
- 验证无启用配置时抛出异常
- 验证所有页面失败时抛出异常
- 验证部分页面失败时仍生成 PDF
- 验证 PageType=1 从正确路径读取文件
- 验证 PageType=2 调用 ScreenshotService
- 验证 PDF 生成后更新 ReportUrl
- Requirements: 2.2, 3.2, 4.4, 5.4, 5.5, 8.2
- 4.1 创建 IPdfGenerationService 接口
-
5. Checkpoint - 确认核心服务实现
- Ensure all tests pass, ask the user if questions arise.
-
6. 服务注册与配置集成
- 6.1 在 appsettings.json 添加配置节
- 添加
HtmlToImageSettings配置节(BaseUrl、ApiKey、TimeoutSeconds、PollingIntervalMs、MaxPollingSeconds) - 添加
ReportSettings配置节(BaseUrl、OutputPath、MaxConcurrency) - Requirements: 1.2, 6.1, 8.1, 10.1
- 添加
- 6.2 在 Program.cs 注册服务
- 绑定
HtmlToImageSettings配置并注册为 Singleton - 绑定
ReportSettings配置并注册为 Singleton - 注册命名 HttpClient
"HtmlToImage",配置 BaseAddress、Timeout、X-API-Key 默认请求头 - IScreenshotService 和 IPdfGenerationService 通过 Autofac ServiceModule 自动扫描注册(确认 ServiceModule 能扫描到新服务)
- Requirements: 1.1, 1.2
- 绑定
- 6.3 确保 MiAssessment.Core 项目引用 PdfSharpCore NuGet 包
- 在
MiAssessment.Core.csproj中添加<PackageReference Include="PdfSharpCore" /> - Requirements: 5.3
- 在
- 6.1 在 appsettings.json 添加配置节
-
7. ReportQueueConsumer 集成 PDF 生成
- 7.1 修改 ReportQueueConsumer.ProcessMessageAsync
- 在
ReportGenerationService.GenerateReportAsync成功后,通过 scope 解析IPdfGenerationService并调用GeneratePdfAsync - PDF 生成调用包裹在 try-catch 中,失败时仅记录 Error 日志,不改变 Status=4,不触发重试
- PDF 生成成功时记录 Info 日志
- Requirements: 7.1, 7.4
- 在
- * 7.2 编写 PDF 生成失败不影响记录状态的属性测试
- Property 6: PDF 生成失败不影响测评记录状态
- 验证 Status=4 的记录在 PDF 生成异常时 Status 保持为 4
- Validates: Requirements 7.4
- * 7.3 编写页面配置过滤与排序属性测试
- Property 7: 页面配置过滤与排序
- 使用 FsCheck 生成混合 Status 值的 report_page_configs 记录
- 验证查询结果仅包含 Status=1 的记录且按 SortOrder 升序
- Validates: Requirements 2.1
- 7.1 修改 ReportQueueConsumer.ProcessMessageAsync
-
8. 静态文件中间件配置
- 8.1 确保 wwwroot/reports/ 目录可通过 HTTP 访问
- 确认 Program.cs 中
app.UseStaticFiles()已配置(已存在) - 确保
wwwroot/reports/目录下的 PDF 文件可直接通过 URL 下载 - 如需要,在
wwwroot/reports/下创建.gitkeep文件确保目录存在 - Requirements: 11.1, 11.2
- 确认 Program.cs 中
- 8.1 确保 wwwroot/reports/ 目录可通过 HTTP 访问
-
9. Final checkpoint - 确认所有功能集成完成
- Ensure all tests pass, ask the user if questions arise.
Notes
- Tasks marked with
*are optional and can be skipped for faster MVP - Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- Unit tests validate specific examples and edge cases
- IScreenshotService 和 IPdfGenerationService 通过 Autofac ServiceModule 自动扫描注册,无需手动注册
- PDF 生成失败不影响结论数据完整性(Status 保持为 4)