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

