上传源代码版本

This commit is contained in:
Im-Jenisson
2026-06-01 16:30:29 +08:00
commit b2a9b7d3c2
462 changed files with 104365 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
# 面单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. 测试验证功能正确性和性能提升效果