6.5 KiB
6.5 KiB
面单PDF字节流缓存方案规划
一、需求与现有逻辑评估
现有逻辑分析
- 下载接口:
LabelController.DownloadLabelByWaybillNumber实时根据中性面单号获取标签数据,若为URL则实时下载,校验PDF页数后返回字节流,存在网络开销和超时风险。 - 下单接口:
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(状态+重试次数,用于定时任务扫描)
三、定时任务实现方案
任务执行逻辑
-
扫描条件:每次扫描订单表中满足以下条件的记录:
-
Label字段不为空 -
未在
LabelPdfCache表中存在,或LabelPdfCache中状态为失败且重试次数<3
-
-
处理流程:
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)
原有逻辑保持不变,新增缓存查询逻辑:
// 新增:先查询缓存
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访问逻辑
五、重试机制设计
重试规则
-
默认最大重试次数:3次
-
重试触发条件:
-
网络请求超时
-
HTTP请求错误(5xx、429等可重试错误)
-
临时IO异常
-
-
不重试条件:
-
PDF页数超过1页(校验不通过,无需重试)
-
标签数据格式错误(base64解析失败)
-
4xx错误(404、403等客户端错误)
-
退避策略
-
第1次失败:间隔10分钟重试
-
第2次失败:间隔30分钟重试
-
第3次失败:标记为最终失败,不再重试
六、异常处理与降级策略
- 缓存服务异常:缓存服务不可用时,直接降级走原有实时下载逻辑,不影响主流程
- 定时任务异常:定时任务执行失败不影响正常接口调用,仅预缓存功能暂时失效
- 缓存失效策略:
- 触发时机:当订单的
Label字段更新时(如换单服务更新标签URL、重新生成标签等场景),同步调用_labelPdfCacheService.InvalidateCacheAsync(waybillNumber)将对应缓存标记为失效状态(Status=3) - 失效后处理:
- 被标记为失效的缓存不会在下载接口中被返回
- 定时任务扫描时会优先处理状态为已失效的记录,重置重试次数为0,重新下载解析新的Label内容
- 失效缓存的字节流会保留24小时后自动清理,方便问题排查
- 兜底校验:下载接口查询缓存时,会额外校验订单表的
Label更新时间与缓存更新时间,若缓存更新时间早于Label更新时间,自动忽略缓存走实时下载逻辑,同时异步触发缓存更新,避免标记失效遗漏导致返回旧数据
- 触发时机:当订单的
- 确保系统稳定: 定时任务的启动和执行不能影响整个程序.
七、实施步骤
- 新增
LabelPdfCache实体类和数据库迁移脚本 - 实现
LabelPdfCacheService缓存操作服务 - 实现定时任务服务(基于Hangfire或现有定时任务框架)
- 改造
DownloadLabelByWaybillNumber接口增加缓存逻辑 - 适配换单服务的缓存查询逻辑
- 测试验证功能正确性和性能提升效果