# 面单PDF字节流缓存方案规划 ## 一、需求与现有逻辑评估 ### 现有逻辑分析 1. **下载接口**:`LabelController.DownloadLabelByWaybillNumber` 实时根据中性面单号获取标签数据,若为URL则实时下载,校验PDF页数后返回字节流,存在网络开销和超时风险。 2. **下单接口**:`TagController.LabelReplace` 生成标签替换记录,存储到订单表(`LabelReplaceEntity`对应表),`Label`字段存储URL或base64编码内容。 ### 核心需求目标 * 减少URL实时下载的网络开销 * 提前校验面单问题(页数超限、无数据等) * 实现下载重试机制,避免超时 * 不影响原有换单流程,无缓存时降级使用原URL访问 ## 二、数据库表设计 ### 表名:`LabelPdfCache` | 字段名 | 类型 | 说明 | | -------------------- | ------------------------- | ------------------------------ | | Id | bigint | 主键,自增 | | NeutralWaybillNumber | varchar(50) | 中性面单号,唯一索引 | | PdfBytes | longblob / varbinary(max) | PDF二进制字节流 | | PageCount | int | PDF实际页数 | | FileSize | int | 文件大小(字节) | | Status | tinyint | 缓存状态:0=待处理,1=处理成功,2=处理失败,3=已失效 | | RetryCount | int | 已重试次数,默认0 | | LastRetryTime | datetime | 最后重试时间 | | ErrorMessage | varchar(500) | 处理失败错误信息 | | CreatedTime | datetime | 创建时间 | | UpdatedTime | datetime | 更新时间 | ### 索引设计 * 唯一索引:`IX_NeutralWaybillNumber`(中性面单号,用于快速查询) * 普通索引:`IX_Status_RetryCount`(状态+重试次数,用于定时任务扫描) ## 三、定时任务实现方案 ### 任务执行逻辑 1. **扫描条件**:每次扫描订单表中满足以下条件的记录: * `Label`字段不为空 * 未在`LabelPdfCache`表中存在,或`LabelPdfCache`中状态为失败且重试次数<3 2. **处理流程**: ```mermaid graph LR A[扫描待处理订单] --> B{是否有缓存记录} B -->|无| C[新建缓存记录状态=待处理] B -->|有失败记录| D[更新重试次数+1,状态=待处理] C --> E[下载/解析Label内容] D --> E E --> F{解析成功?} F -->|是| G[校验PDF页数≤1,大小正常] F -->|否| H[更新状态=失败,记录错误信息] G -->|校验通过| I[保存字节流,状态=成功] G -->|校验失败| J[更新状态=失败,记录校验错误] ``` ### 任务配置 * 执行频率:建议每5分钟执行一次,可配置 * 单次处理数量:每次最多处理100条,避免占用过多资源 * 重试间隔:失败后至少间隔10分钟再重试 ## 四、接口逻辑改造 ### 1. 下载接口优化(LabelController) 原有逻辑保持不变,新增缓存查询逻辑: ```csharp // 新增:先查询缓存 var cache = await _labelPdfCacheService.GetValidCacheAsync(waybillNumber); if (cache != null && cache.Status == 1) { _logger.LogInformation("Hit PDF cache for waybill: {number}", waybillNumber); scanResult = ScanResult.ReturnedLabel; scanDescription = "命中缓存返回标签"; return File(cache.PdfBytes, "application/pdf", $"label_{waybillNumber}.pdf"); } // 未命中缓存,走原有逻辑 // ... 原有下载/解析逻辑 ... // 新增:解析成功后异步写入缓存 _ = Task.Run(async () => { try { await _labelPdfCacheService.SaveCacheAsync(waybillNumber, labelBytes, pageCount, labelBytes.Length); } catch (Exception ex) { _logger.LogWarning(ex, "Save cache failed for waybill: {number}", waybillNumber); } }); ``` ### 2. 换单接口兼容(TagController) 无需改造原有逻辑,当调用`_labelReplaceService.ProcessLabelReplaceAsync`时,若需要返回字节流: * 先查询缓存,存在则直接使用 * 不存在则走原有URL访问逻辑 ## 五、重试机制设计 ### 重试规则 1. 默认最大重试次数:3次 2. 重试触发条件: * 网络请求超时 * HTTP请求错误(5xx、429等可重试错误) * 临时IO异常 3. 不重试条件: * PDF页数超过1页(校验不通过,无需重试) * 标签数据格式错误(base64解析失败) * 4xx错误(404、403等客户端错误) ### 退避策略 * 第1次失败:间隔10分钟重试 * 第2次失败:间隔30分钟重试 * 第3次失败:标记为最终失败,不再重试 ## 六、异常处理与降级策略 1. **缓存服务异常**:缓存服务不可用时,直接降级走原有实时下载逻辑,不影响主流程 2. **定时任务异常**:定时任务执行失败不影响正常接口调用,仅预缓存功能暂时失效 3. **缓存失效策略**: - **触发时机**:当订单的`Label`字段更新时(如换单服务更新标签URL、重新生成标签等场景),同步调用`_labelPdfCacheService.InvalidateCacheAsync(waybillNumber)`将对应缓存标记为失效状态(Status=3) - **失效后处理**: 1. 被标记为失效的缓存不会在下载接口中被返回 2. 定时任务扫描时会优先处理状态为已失效的记录,重置重试次数为0,重新下载解析新的Label内容 3. 失效缓存的字节流会保留24小时后自动清理,方便问题排查 - **兜底校验**:下载接口查询缓存时,会额外校验订单表的`Label`更新时间与缓存更新时间,若缓存更新时间早于Label更新时间,自动忽略缓存走实时下载逻辑,同时异步触发缓存更新,避免标记失效遗漏导致返回旧数据 4. 确保系统稳定: 定时任务的启动和执行不能影响整个程序. ## 七、实施步骤 1. 新增`LabelPdfCache`实体类和数据库迁移脚本 2. 实现`LabelPdfCacheService`缓存操作服务 3. 实现定时任务服务(基于Hangfire或现有定时任务框架) 4. 改造`DownloadLabelByWaybillNumber`接口增加缓存逻辑 5. 适配换单服务的缓存查询逻辑 6. 测试验证功能正确性和性能提升效果