上传源代码版本

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,343 @@
# USPS 自动集包开发检查清单
## 开发前准备
- [ ] 已阅读 USPS自动集包需求文档.md
- [ ] 已阅读 spec.md 接口规范
- [ ] 已阅读 tasks.md 任务分解
- [ ] 已确认数据库表结构label_replace_requests, bag_tag_waybills
- [ ] 已确认 USPS 运单号识别规则
---
## 阶段一:模型定义
### 任务 1.1:创建任务状态模型
**文件**`src/MDL/Models/AutoPackTaskStatus.cs`
- [ ] 创建 `AutoPackTaskStatus`
- [ ] TaskId (string)
- [ ] TagNumber (string)
- [ ] Status (string)
- [ ] TotalCount (int)
- [ ] ProcessedCount (int)
- [ ] SuccessCount (int)
- [ ] FailedCount (int)
- [ ] CurrentWaybill (string)
- [ ] StartTime (DateTime)
- [ ] EndTime (DateTime?)
- [ ] FailedItems (List<AutoPackFailedItem>)
- [ ] CancellationTokenSource (CancellationTokenSource)
- [ ] 创建 `AutoPackFailedItem`
- [ ] WaybillNumber (string)
- [ ] ErrorCode (int)
- [ ] ErrorMessage (string)
- [ ] 添加必要的 using 语句
- [ ] 编译通过
### 任务 1.2:创建请求/响应 DTO
**文件**`src/MDL/Models/AutoPackRequest.cs`
- [ ] 创建 `StartAutoPackRequest`
- [ ] TagNumber (string)
- [ ] Creator (string, 默认 "system")
- [ ] 创建 `StartAutoPackResponse`
- [ ] TaskId (string)
- [ ] TagNumber (string)
- [ ] Status (string)
- [ ] TotalCount (int)
- [ ] Message (string)
- [ ] 创建 `AutoPackProgressResponse`
- [ ] TaskId (string)
- [ ] TagNumber (string)
- [ ] Status (string)
- [ ] TotalCount (int)
- [ ] ProcessedCount (int)
- [ ] SuccessCount (int)
- [ ] FailedCount (int)
- [ ] CurrentWaybill (string)
- [ ] Progress (int)
- [ ] Message (string)
- [ ] StartTime (DateTime)
- [ ] EstimatedEndTime (DateTime?)
- [ ] FailedItems (List<AutoPackFailedItem>)
- [ ] 创建 `AutoPackResultResponse`
- [ ] TaskId (string)
- [ ] TagNumber (string)
- [ ] Status (string)
- [ ] TotalCount (int)
- [ ] SuccessCount (int)
- [ ] FailedCount (int)
- [ ] StartTime (DateTime)
- [ ] EndTime (DateTime?)
- [ ] Duration (int)
- [ ] FailedItems (List<AutoPackFailedItem>)
- [ ] 编译通过
---
## 阶段二:数据访问层
### 任务 2.1:扩展 Repository 接口
**文件**`src/DAL/Interfaces/IBagTagRepository.cs`
- [ ] 添加 `GetEligibleUspsWaybillsAsync(DateTime cutoffTime)` 方法声明
- [ ] 添加 `GetEligibleUspsWaybillCountAsync(DateTime cutoffTime)` 方法声明
- [ ] 添加 XML 注释
- [ ] 编译通过
### 任务 2.2:实现 Repository 方法
**文件**`src/DAL/Repositories/BagTagRepository.cs`
- [ ] 实现 `GetEligibleUspsWaybillsAsync` 方法
- [ ] 使用 SqlSugar 执行 SQL 查询
- [ ] 查询条件ReplaceStatus = 'Y'
- [ ] 查询条件CreatedAt >= cutoffTime
- [ ] 查询条件FinalMileTrackingNumber IS NOT NULL AND != ''
- [ ] 查询条件:未关联到 bag_tag_waybillsLEFT JOIN + IS NULL
- [ ] 查询条件USPS 格式REGEXP '^(92|93|94|95)'
- [ ] 查询条件USPS 格式REGEXP '^420[0-9]{5}(92|93|94|95)'
- [ ] 查询条件USPS 格式REGEXP '^420[0-9]{9}(92|93|94|95)'
- [ ] ORDER BY CreatedAt ASC
- [ ] 返回 List<string>
- [ ] 实现 `GetEligibleUspsWaybillCountAsync` 方法
- [ ] 使用 COUNT(*) 查询
- [ ] 相同的 WHERE 条件
- [ ] 返回 int
- [ ] 添加异常处理
- [ ] 编译通过
---
## 阶段三:业务逻辑层
### 任务 3.1:扩展 Service 接口
**文件**`src/BLL/Interfaces/IBagTagService.cs`
- [ ] 添加 `StartAutoPackAsync(string tagNumber, string creator)` 方法声明
- [ ] 添加 `GetAutoPackProgressAsync(string taskId)` 方法声明
- [ ] 添加 `CancelAutoPackAsync(string taskId)` 方法声明
- [ ] 添加 `GetAutoPackResultAsync(string taskId)` 方法声明
- [ ] 添加 XML 注释
- [ ] 编译通过
### 任务 3.2:实现任务启动逻辑
**文件**`src/BLL/Services/BagTagService.cs`
- [ ] 注入 ICacheService如未注入
- [ ] 实现 `StartAutoPackAsync` 方法
- [ ] 验证袋牌存在GetByTagNumberAsync
- [ ] 验证袋牌状态为 "Opened"
- [ ] 计算 cutoffTimeDateTime.UtcNow.AddHours(-84)
- [ ] 调用 GetEligibleUspsWaybillsAsync 获取包裹列表
- [ ] 检查包裹数量 > 0
- [ ] 生成唯一 TaskId格式task_{timestamp}_{guid}
- [ ] 创建 AutoPackTaskStatus 对象
- [ ] 存入缓存key: "autopack:{taskId}", 过期时间 60 分钟)
- [ ] 启动后台任务Task.Run
- [ ] 返回 StartAutoPackResponse
- [ ] 添加异常处理
- [ ] 编译通过
### 任务 3.3:实现自动集包执行逻辑
**文件**`src/BLL/Services/BagTagService.cs`
- [ ] 创建 `ExecuteAutoPackAsync` 私有方法
- [ ] 参数taskId, tagNumber, waybills, creator
- [ ] 创建 Random 对象
- [ ] 遍历 waybills
- [ ] 从缓存获取任务状态
- [ ] 检查 CancellationTokenSource.IsCancellationRequested
- [ ] 更新 CurrentWaybill
- [ ] 调用 AssociateWaybillAsync 进行关联
- [ ] 更新 SuccessCount 或 FailedCount
- [ ] 记录失败信息到 FailedItems
- [ ] 更新 ProcessedCount
- [ ] 保存任务状态到缓存
- [ ] 随机延迟 2-4 秒(最后一个不延迟)
- [ ] 任务完成处理
- [ ] 设置 Statuscompleted/cancelled
- [ ] 设置 EndTime
- [ ] 清空 CurrentWaybill
- [ ] 保存最终状态到缓存
- [ ] 实现 `GetAutoPackProgressAsync` 方法
- [ ] 从缓存获取任务状态
- [ ] 计算 Progress 百分比
- [ ] 计算 EstimatedEndTime
- [ ] 映射到 AutoPackProgressResponse
- [ ] 返回响应
- [ ] 实现 `CancelAutoPackAsync` 方法
- [ ] 从缓存获取任务状态
- [ ] 检查任务是否存在且状态为 processing
- [ ] 调用 CancellationTokenSource.Cancel()
- [ ] 更新状态为 cancelled
- [ ] 保存到缓存
- [ ] 返回 bool
- [ ] 实现 `GetAutoPackResultAsync` 方法
- [ ] 从缓存获取任务状态
- [ ] 映射到 AutoPackResultResponse
- [ ] 计算 Duration
- [ ] 返回响应
- [ ] 添加异常处理
- [ ] 编译通过
---
## 阶段四:接口层
### 任务 4.1:实现控制器方法
**文件**`src/CONTROLLER/Controllers/BagTagController.cs`
- [ ] 添加 `StartAutoPack` 方法
- [ ] HTTP POST 路由:"auto-pack/start"
- [ ] 参数:[FromBody] StartAutoPackRequest
- [ ] 调用 _bagTagService.StartAutoPackAsync
- [ ] 返回统一响应格式 { code, message, data }
- [ ] 异常处理
- [ ] 添加 `GetAutoPackProgress` 方法
- [ ] HTTP GET 路由:"auto-pack/progress/{taskId}"
- [ ] 参数string taskId
- [ ] 调用 _bagTagService.GetAutoPackProgressAsync
- [ ] 处理任务不存在的情况
- [ ] 返回统一响应格式
- [ ] 异常处理
- [ ] 添加 `CancelAutoPack` 方法
- [ ] HTTP POST 路由:"auto-pack/cancel/{taskId}"
- [ ] 参数string taskId
- [ ] 调用 _bagTagService.CancelAutoPackAsync
- [ ] 处理取消失败的情况
- [ ] 返回统一响应格式
- [ ] 异常处理
- [ ] 添加 `GetAutoPackResult` 方法
- [ ] HTTP GET 路由:"auto-pack/result/{taskId}"
- [ ] 参数string taskId
- [ ] 调用 _bagTagService.GetAutoPackResultAsync
- [ ] 处理任务不存在的情况
- [ ] 返回统一响应格式
- [ ] 异常处理
- [ ] 编译通过
### 任务 4.2:注册依赖注入
**文件**`src/CONTROLLER/Program.cs`
- [ ] 检查 ICacheService 是否已注册
- [ ] 检查 IBagTagService 是否已注册
- [ ] 确认无需额外注册
---
## 阶段五:测试与优化
### 任务 5.1:编写单元测试
- [ ] 测试启动任务 - 袋牌不存在
- [ ] 测试启动任务 - 袋牌未打开
- [ ] 测试启动任务 - 无符合条件的包裹
- [ ] 测试启动任务 - 成功启动
- [ ] 测试查询进度 - 任务不存在
- [ ] 测试查询进度 - 正常查询
- [ ] 测试取消任务 - 任务不存在
- [ ] 测试取消任务 - 任务已完成
- [ ] 测试取消任务 - 正常取消
- [ ] 测试自动集包执行 - 全部成功
- [ ] 测试自动集包执行 - 部分失败
- [ ] 测试自动集包执行 - 取消操作
- [ ] 所有测试通过
### 任务 5.2:性能优化与联调
- [ ] SQL 查询性能优化
- [ ] 检查 label_replace_requests 表索引CreatedAt, ReplaceStatus, FinalMileTrackingNumber
- [ ] 检查 bag_tag_waybills 表索引FinalMileTrackingNumber
- [ ] 缓存配置优化
- [ ] 确认缓存过期时间合理60分钟
- [ ] 并发控制
- [ ] 确认同时只能有一个自动集包任务在运行(可选)
- [ ] 异常处理完善
- [ ] 数据库连接异常
- [ ] 缓存访问异常
- [ ] 关联操作异常
- [ ] WinForm 联调
- [ ] 前端能正常调用启动接口
- [ ] 前端能正常轮询进度
- [ ] 进度条实时更新
- [ ] 取消功能正常
- [ ] 大数据量测试100+ 包裹)
---
## 代码审查清单
### 代码规范
- [ ] 命名规范符合项目标准
- [ ] 方法添加 XML 注释
- [ ] 复杂逻辑添加行内注释
- [ ] 无死代码
- [ ] 无 Console.WriteLine使用 ILogger
### 异常处理
- [ ] 所有异步方法有 try-catch
- [ ] 异常信息不暴露敏感信息
- [ ] 异常正确记录日志
### 性能
- [ ] 数据库查询使用参数化 SQL
- [ ] 避免 N+1 查询问题
- [ ] 缓存使用合理
### 安全
- [ ] 输入参数验证
- [ ] 防止 SQL 注入
- [ ] 权限检查(如需要)
---
## 部署检查清单
- [ ] 代码编译通过
- [ ] 单元测试全部通过
- [ ] 数据库迁移脚本(如需要)
- [ ] 配置文件更新(如需要)
- [ ] API 文档更新
- [ ] 部署到测试环境
- [ ] 测试环境验证通过
- [ ] 部署到生产环境
- [ ] 生产环境验证通过
---
## 文档检查清单
- [ ] spec.md 已更新(如有变更)
- [ ] tasks.md 已更新(如有变更)
- [ ] 接口文档已更新
- [ ] 前端联调文档已提供
---
## 验收标准
### 功能验收
- [ ] 创建 USPS 袋牌后能自动触发集包(或手动触发接口)
- [ ] 正确筛选符合条件的 USPS 包裹84小时内、未集包、已换单
- [ ] 逐个关联包裹,间隔 2-4 秒
- [ ] 实时反馈进度(总数、已处理数、成功数、失败数)
- [ ] 支持取消操作
- [ ] 记录操作日志
### 性能验收
- [ ] 100 个包裹处理时间 < 10 分钟
- [ ] 前端轮询响应时间 < 100ms
- [ ] 内存占用稳定无内存泄漏
### 兼容性验收
- [ ] WinForm 前端正常调用
- [ ] 接口响应格式符合规范
- [ ] 错误码定义清晰

View File

@@ -0,0 +1,439 @@
# USPS 自动集包接口规格说明
## 1. 需求概述
根据 USPS自动集包需求文档实现 USPS 尾程包裹的自动集包功能。当用户在系统中创建 USPS 袋牌后,系统自动筛选符合条件的包裹并逐个关联到该袋牌。
**核心特点**
- 前端使用 WinForm需要实时反馈关联进度
- 采用异步任务 + 进度查询的设计模式
- 每次关联间隔 2-4 秒随机延迟,模拟人工操作
---
## 2. 接口设计
### 2.1 启动自动集包任务
**接口路径**`POST /api/bagtag/auto-pack/start`
**请求参数**
| 参数名 | 类型 | 必选 | 描述 |
|--------|------|------|------|
| tagNumber | string | 是 | USPS 袋牌号 |
| creator | string | 否 | 操作人,默认为 "system" |
**请求示例**
```json
{
"tagNumber": "USPS202604031200010001",
"creator": "admin"
}
```
**响应结构**
```json
{
"code": 0,
"message": "success",
"data": {
"taskId": "task_20260403120001_abc123",
"tagNumber": "USPS202604031200010001",
"status": "processing",
"totalCount": 50,
"message": "自动集包任务已启动"
}
}
```
**错误响应**
```json
{
"code": 1001,
"message": "Bag tag not found or not in opened status",
"data": null
}
```
---
### 2.2 查询自动集包进度
**接口路径**`GET /api/bagtag/auto-pack/progress/{taskId}`
**路径参数**
| 参数名 | 类型 | 必选 | 描述 |
|--------|------|------|------|
| taskId | string | 是 | 任务ID |
**响应结构**
```json
{
"code": 0,
"message": "success",
"data": {
"taskId": "task_20260403120001_abc123",
"tagNumber": "USPS202604031200010001",
"status": "processing",
"totalCount": 50,
"processedCount": 25,
"successCount": 24,
"failedCount": 1,
"currentWaybill": "9201234567890123456789",
"progress": 50,
"message": "正在处理第 25/50 个包裹",
"startTime": "2026-04-03T12:00:01Z",
"estimatedEndTime": "2026-04-03T12:03:30Z",
"failedItems": [
{
"waybillNumber": "9201234567890123456788",
"errorCode": 10035,
"errorMessage": "Waybill is already associated with another bag tag"
}
]
}
}
```
**状态说明**
| 状态值 | 说明 |
|--------|------|
| pending | 等待处理 |
| processing | 处理中 |
| completed | 已完成 |
| failed | 失败/异常终止 |
| cancelled | 已取消 |
---
### 2.3 取消自动集包任务
**接口路径**`POST /api/bagtag/auto-pack/cancel/{taskId}`
**路径参数**
| 参数名 | 类型 | 必选 | 描述 |
|--------|------|------|------|
| taskId | string | 是 | 任务ID |
**响应结构**
```json
{
"code": 0,
"message": "Task cancelled successfully",
"data": {
"taskId": "task_20260403120001_abc123",
"status": "cancelled",
"processedCount": 25,
"successCount": 24,
"failedCount": 1
}
}
```
---
### 2.4 获取任务结果
**接口路径**`GET /api/bagtag/auto-pack/result/{taskId}`
**路径参数**
| 参数名 | 类型 | 必选 | 描述 |
|--------|------|------|------|
| taskId | string | 是 | 任务ID |
**响应结构**(任务完成后):
```json
{
"code": 0,
"message": "success",
"data": {
"taskId": "task_20260403120001_abc123",
"tagNumber": "USPS202604031200010001",
"status": "completed",
"totalCount": 50,
"successCount": 48,
"failedCount": 2,
"startTime": "2026-04-03T12:00:01Z",
"endTime": "2026-04-03T12:03:45Z",
"duration": 224,
"failedItems": [
{
"waybillNumber": "9201234567890123456788",
"errorCode": 10035,
"errorMessage": "Waybill is already associated with another bag tag"
},
{
"waybillNumber": "9201234567890123456787",
"errorCode": 10033,
"errorMessage": "Channel does not match"
}
]
}
}
```
---
## 3. 业务逻辑
### 3.1 筛选条件
系统仅自动关联同时满足以下条件的包裹:
1. **尾程渠道为 USPS**
- 通过运单号识别渠道92/93/94/95 开头,或 420+邮编+92/93/94/95
2. **当前尚未被集包**
- 未关联到任何袋牌bag_tag_waybills 表中不存在)
3. **已完成换单**
- label_replace_requests 表中存在对应记录
- 换单状态为 "Y"(正常换单)
4. **换单时间在前 3 天内84小时**
- 以创建袋牌时间为基准
- CreatedAt >= 当前时间 - 84小时
### 3.2 处理流程
```
┌─────────────────────────────────────────────────────────────┐
│ 1. 接收启动请求 (POST /api/bagtag/auto-pack/start) │
│ - 验证袋牌存在且状态为 Opened │
│ - 生成唯一任务ID │
│ - 查询符合条件的包裹总数 │
│ - 返回任务ID给前端 │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. 异步执行自动集包任务 │
│ - 创建后台任务 │
│ - 逐个关联包裹 │
│ - 每单间隔 2-4 秒随机延迟 │
│ - 实时更新任务进度状态 │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. 前端轮询进度 (GET /api/bagtag/auto-pack/progress/{id}) │
│ - 建议轮询间隔1-2 秒 │
│ - 显示进度条、当前处理单号、成功/失败数量 │
│ - 可实时取消任务 │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. 任务完成 │
│ - 返回最终结果 │
│ - 记录操作日志 │
└─────────────────────────────────────────────────────────────┘
```
### 3.3 关联处理逻辑
对于每个符合条件的包裹:
1. 调用 `AssociateWaybillAsync` 方法进行关联
2. 记录关联结果(成功/失败)
3. 更新任务进度状态
4. 生成 2-4 秒随机延迟
5. 处理下一个包裹
### 3.4 任务状态管理
任务状态存储在内存缓存中ICacheService包含
```csharp
public class AutoPackTaskStatus
{
public string TaskId { get; set; }
public string TagNumber { get; set; }
public string Status { get; set; } // pending/processing/completed/failed/cancelled
public int TotalCount { get; set; }
public int ProcessedCount { get; set; }
public int SuccessCount { get; set; }
public int FailedCount { get; set; }
public string CurrentWaybill { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public List<FailedItem> FailedItems { get; set; }
public CancellationTokenSource CancellationTokenSource { get; set; }
}
```
---
## 4. 技术实现
### 4.1 新增模型
**AutoPackTaskStatus**(任务状态模型)
- 位置:`MDL/Models/AutoPackTaskStatus.cs`
**AutoPackRequest**(启动请求模型)
- 位置:`MDL/Models/BagTagRequest.cs`(扩展)
### 4.2 接口定义
**IBagTagService** 扩展:
```csharp
// 启动自动集包任务
Task<AutoPackTaskResult> StartAutoPackAsync(string tagNumber, string creator);
// 查询任务进度
Task<AutoPackTaskStatus> GetAutoPackProgressAsync(string taskId);
// 取消任务
Task<bool> CancelAutoPackAsync(string taskId);
// 获取任务结果
Task<AutoPackTaskStatus> GetAutoPackResultAsync(string taskId);
```
**IBagTagRepository** 扩展:
```csharp
// 查询符合条件的 USPS 包裹
Task<List<string>> GetEligibleUspsWaybillsAsync(string tagNumber, DateTime cutoffTime);
// 获取符合条件的包裹数量
Task<int> GetEligibleUspsWaybillCountAsync(string tagNumber, DateTime cutoffTime);
```
### 4.3 控制器实现
`BagTagController` 中添加:
- `POST /api/bagtag/auto-pack/start`
- `GET /api/bagtag/auto-pack/progress/{taskId}`
- `POST /api/bagtag/auto-pack/cancel/{taskId}`
- `GET /api/bagtag/auto-pack/result/{taskId}`
---
## 5. 数据库查询
### 5.1 查询符合条件的包裹
```sql
SELECT DISTINCT
l.FinalMileTrackingNumber
FROM label_replace_requests l
LEFT JOIN bag_tag_waybills b ON l.FinalMileTrackingNumber = b.FinalMileTrackingNumber
WHERE
l.ReplaceStatus = 'Y'
AND l.CreatedAt >= @CutoffTime
AND l.FinalMileTrackingNumber IS NOT NULL
AND l.FinalMileTrackingNumber != ''
AND b.Id IS NULL -- 未关联到任何袋牌
AND (
-- USPS 运单号格式92/93/94/95 开头
l.FinalMileTrackingNumber REGEXP '^(92|93|94|95)'
-- 或 420+邮编+92/93/94/95 开头
OR l.FinalMileTrackingNumber REGEXP '^420[0-9]{5}(92|93|94|95)'
OR l.FinalMileTrackingNumber REGEXP '^420[0-9]{9}(92|93|94|95)'
)
ORDER BY l.CreatedAt ASC
```
---
## 6. 错误码定义
| 错误码 | 说明 |
|--------|------|
| 0 | 成功 |
| 1001 | 袋牌不存在或状态不正确 |
| 1002 | 任务不存在 |
| 1003 | 任务已取消 |
| 1004 | 任务已完成 |
| 1005 | 无符合条件的包裹 |
| 10031 | 袋牌不存在 |
| 10032 | 袋牌未打开 |
| 10033 | 渠道不匹配 |
| 10035 | 运单已关联到其他袋牌 |
| 9999 | 系统错误 |
---
## 7. 前端集成建议
### 7.1 WinForm 调用流程
```csharp
// 1. 启动自动集包
var response = await httpClient.PostAsJsonAsync("/api/bagtag/auto-pack/start",
new { tagNumber = "USPS202604031200010001", creator = "admin" });
var result = await response.Content.ReadFromJsonAsync<ApiResponse<AutoPackResult>>();
var taskId = result.Data.TaskId;
// 2. 轮询进度
timer = new Timer(async _ => {
var progressResponse = await httpClient.GetAsync($"/api/bagtag/auto-pack/progress/{taskId}");
var progress = await progressResponse.Content.ReadFromJsonAsync<ApiResponse<AutoPackProgress>>();
// 更新UI进度条、当前单号、成功/失败数
UpdateUI(progress.Data);
if (progress.Data.Status == "completed" || progress.Data.Status == "failed")
{
timer.Stop();
ShowResult(progress.Data);
}
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
// 3. 取消任务(用户点击取消按钮)
await httpClient.PostAsync($"/api/bagtag/auto-pack/cancel/{taskId}", null);
```
### 7.2 UI 展示建议
- **进度条**:显示 `processedCount / totalCount` 百分比
- **当前处理**:显示 `currentWaybill` 单号
- **统计信息**:成功数、失败数、剩余数
- **预计完成时间**:根据当前速度计算
- **失败列表** expandable 面板显示失败明细
---
## 8. 性能考虑
1. **异步处理**:使用 `Task.Run` 或后台服务执行集包任务
2. **缓存进度**:使用 `ICacheService` 存储任务状态,避免数据库压力
3. **批量查询**:一次性查询所有符合条件的包裹,避免多次数据库访问
4. **延迟控制**2-4 秒随机延迟,避免对系统造成过大压力
---
## 9. 安全考虑
1. **袋牌状态验证**:确保袋牌处于 Opened 状态才能自动集包
2. **渠道匹配**:自动识别运单号渠道,确保与袋牌渠道一致
3. **重复关联检查**:避免将已关联的包裹再次关联
4. **任务超时**:设置任务最大执行时间(如 30 分钟),超时自动终止
---
## 10. 日志记录
每个包裹关联操作记录订单日志:
```csharp
await _orderLogService.RecordOrderLogAsync(
neutralWaybillNumber: string.Empty,
finalMileTrackingNumber: waybillNumber,
operationType: OrderLogOperationType.PACK,
operationResult: success ? OrderLogOperationResult.SUCCESS : OrderLogOperationResult.FAILED,
operationDescription: $"自动集包到袋牌 {tagNumber}: {(success ? "成功" : errorMessage)}",
@operator: "system_auto"
);
```

View File

@@ -0,0 +1,627 @@
# USPS 自动集包开发任务分解
## 任务概览
| 阶段 | 任务数 | 预计工时 |
| ------ | ------ | ------- |
| 模型定义 | 2 | 2h |
| 数据访问层 | 2 | 3h |
| 业务逻辑层 | 3 | 5h |
| 接口层 | 2 | 3h |
| 测试与优化 | 2 | 3h |
| **总计** | **11** | **16h** |
***
## 阶段一:模型定义
### 任务 1.1:创建任务状态模型
**文件**`src/MDL/Models/AutoPackTaskStatus.cs`
**内容**
```csharp
namespace MDL.Models
{
/// <summary>
/// 自动集包任务状态
/// </summary>
public class AutoPackTaskStatus
{
public string TaskId { get; set; }
public string TagNumber { get; set; }
public string Status { get; set; } // pending/processing/completed/failed/cancelled
public int TotalCount { get; set; }
public int ProcessedCount { get; set; }
public int SuccessCount { get; set; }
public int FailedCount { get; set; }
public string CurrentWaybill { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public List<AutoPackFailedItem> FailedItems { get; set; } = new();
public CancellationTokenSource CancellationTokenSource { get; set; }
}
public class AutoPackFailedItem
{
public string WaybillNumber { get; set; }
public int ErrorCode { get; set; }
public string ErrorMessage { get; set; }
}
}
```
**验收标准**
- [ ] 模型包含所有必要字段
- [ ] 字段命名符合项目规范
- [ ] 支持 JSON 序列化
***
### 任务 1.2:创建请求/响应 DTO
**文件**`src/MDL/Models/AutoPackRequest.cs`
**内容**
```csharp
namespace MDL.Models
{
/// <summary>
/// 启动自动集包请求
/// </summary>
public class StartAutoPackRequest
{
public string TagNumber { get; set; }
public string Creator { get; set; } = "system";
}
/// <summary>
/// 启动自动集包响应
/// </summary>
public class StartAutoPackResponse
{
public string TaskId { get; set; }
public string TagNumber { get; set; }
public string Status { get; set; }
public int TotalCount { get; set; }
public string Message { get; set; }
}
/// <summary>
/// 自动集包进度响应
/// </summary>
public class AutoPackProgressResponse
{
public string TaskId { get; set; }
public string TagNumber { get; set; }
public string Status { get; set; }
public int TotalCount { get; set; }
public int ProcessedCount { get; set; }
public int SuccessCount { get; set; }
public int FailedCount { get; set; }
public string CurrentWaybill { get; set; }
public int Progress { get; set; }
public string Message { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EstimatedEndTime { get; set; }
public List<AutoPackFailedItem> FailedItems { get; set; }
}
/// <summary>
/// 自动集包结果响应
/// </summary>
public class AutoPackResultResponse
{
public string TaskId { get; set; }
public string TagNumber { get; set; }
public string Status { get; set; }
public int TotalCount { get; set; }
public int SuccessCount { get; set; }
public int FailedCount { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public int Duration { get; set; }
public List<AutoPackFailedItem> FailedItems { get; set; }
}
}
```
**验收标准**
- [ ] 包含 Start、Progress、Result 三种响应 DTO
- [ ] 字段与 spec.md 定义一致
- [ ] 支持前端 WinForm 调用
***
## 阶段二:数据访问层
### 任务 2.1:扩展 Repository 接口
**文件**`src/DAL/Interfaces/IBagTagRepository.cs`
**新增方法**
```csharp
/// <summary>
/// 查询符合条件的 USPS 包裹
/// </summary>
Task<List<string>> GetEligibleUspsWaybillsAsync(DateTime cutoffTime);
/// <summary>
/// 获取符合条件的包裹数量
/// </summary>
Task<int> GetEligibleUspsWaybillCountAsync(DateTime cutoffTime);
```
**验收标准**
- [ ] 接口定义符合项目规范
- [ ] 参数设计合理cutoffTime = 当前时间 - 84小时
***
### 任务 2.2:实现 Repository 方法
**文件**`src/DAL/Repositories/BagTagRepository.cs`
**实现要点**
1. 使用 SqlSugar 执行 SQL 查询
2. 查询条件:
- ReplaceStatus = 'Y'
- CreatedAt >= cutoffTime
- FinalMileTrackingNumber 不为空
- 未关联到 bag\_tag\_waybills
- USPS 运单号格式92/93/94/95 或 420+邮编+92/93/94/95
- 有一条换单成功的扫描记录
**SQL 参考**
```sql
SELECT DISTINCT
l.FinalMileTrackingNumber
FROM label_replace_requests l
LEFT JOIN bag_tag_waybills b ON l.FinalMileTrackingNumber = b.FinalMileTrackingNumber
WHERE
l.ReplaceStatus = 'Y'
AND l.CreatedAt >= @CutoffTime
AND l.FinalMileTrackingNumber IS NOT NULL
AND l.FinalMileTrackingNumber != ''
AND b.Id IS NULL
AND (
l.FinalMileTrackingNumber REGEXP '^(92|93|94|95)'
OR l.FinalMileTrackingNumber REGEXP '^420[0-9]{5}(92|93|94|95)'
OR l.FinalMileTrackingNumber REGEXP '^420[0-9]{9}(92|93|94|95)'
)
ORDER BY l.CreatedAt ASC
```
**验收标准**
- [ ] SQL 查询性能优化(使用索引)
- [ ] 正确处理 USPS 运单号格式
- [ ] 返回结果按 CreatedAt 升序排列
***
## 阶段三:业务逻辑层
### 任务 3.1:扩展 Service 接口
**文件**`src/BLL/Interfaces/IBagTagService.cs`
**新增方法**
```csharp
/// <summary>
/// 启动自动集包任务
/// </summary>
Task<StartAutoPackResponse> StartAutoPackAsync(string tagNumber, string creator);
/// <summary>
/// 查询自动集包进度
/// </summary>
Task<AutoPackProgressResponse> GetAutoPackProgressAsync(string taskId);
/// <summary>
/// 取消自动集包任务
/// </summary>
Task<bool> CancelAutoPackAsync(string taskId);
/// <summary>
/// 获取自动集包结果
/// </summary>
Task<AutoPackResultResponse> GetAutoPackResultAsync(string taskId);
```
**验收标准**
- [ ] 接口定义完整
- [ ] 返回类型与 DTO 对应
***
### 任务 3.2:实现任务启动逻辑
**文件**`src/BLL/Services/BagTagService.cs`
**实现步骤**
1. 验证袋牌存在且状态为 Opened
2. 生成唯一任务ID格式`task_{timestamp}_{guid}`
3. 查询符合条件的包裹列表
4. 如果无符合条件的包裹,返回错误
5. 创建任务状态对象,存入缓存
6. 启动后台任务执行自动集包
7. 返回任务信息给前端
**关键代码**
```csharp
public async Task<StartAutoPackResponse> StartAutoPackAsync(string tagNumber, string creator)
{
// 1. 验证袋牌
var tag = await _bagTagRepository.GetByTagNumberAsync(tagNumber);
if (tag == null || tag.Status != "Opened")
{
throw new Exception("Bag tag not found or not in opened status");
}
// 2. 查询符合条件的包裹
var cutoffTime = DateTime.UtcNow.AddHours(-84);
var waybills = await _bagTagRepository.GetEligibleUspsWaybillsAsync(cutoffTime);
if (waybills.Count == 0)
{
throw new Exception("No eligible waybills found");
}
// 3. 创建任务
var taskId = $"task_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid().ToString("N").Substring(0, 8)}";
var taskStatus = new AutoPackTaskStatus
{
TaskId = taskId,
TagNumber = tagNumber,
Status = "processing",
TotalCount = waybills.Count,
ProcessedCount = 0,
SuccessCount = 0,
FailedCount = 0,
StartTime = DateTime.UtcNow,
FailedItems = new List<AutoPackFailedItem>(),
CancellationTokenSource = new CancellationTokenSource()
};
// 4. 保存任务状态到缓存
await _cacheService.SetAsync($"autopack:{taskId}", taskStatus, 60); // 缓存60分钟
// 5. 启动后台任务
_ = Task.Run(async () => await ExecuteAutoPackAsync(taskId, tagNumber, waybills, creator));
// 6. 返回响应
return new StartAutoPackResponse
{
TaskId = taskId,
TagNumber = tagNumber,
Status = "processing",
TotalCount = waybills.Count,
Message = "自动集包任务已启动"
};
}
```
**验收标准**
- [ ] 袋牌验证逻辑正确
- [ ] 任务ID生成唯一
- [ ] 任务状态正确存入缓存
- [ ] 后台任务正确启动
***
### 任务 3.3:实现自动集包执行逻辑
**文件**`src/BLL/Services/BagTagService.cs`
**实现步骤**
1. 从缓存获取任务状态
2. 遍历包裹列表逐个处理
3. 每次处理前检查取消令牌
4. 调用 AssociateWaybillAsync 进行关联
5. 更新任务状态(成功/失败计数、当前单号)
6. 记录订单日志
7. 生成 2-4 秒随机延迟
8. 处理完成后更新任务状态为 completed 或 failed
**关键代码**
```csharp
private async Task ExecuteAutoPackAsync(string taskId, string tagNumber, List<string> waybills, string creator)
{
var random = new Random();
var cacheKey = $"autopack:{taskId}";
foreach (var waybill in waybills)
{
// 1. 获取任务状态
var taskStatus = await _cacheService.GetAsync<AutoPackTaskStatus>(cacheKey);
if (taskStatus == null || taskStatus.CancellationTokenSource?.IsCancellationRequested == true)
{
break;
}
// 2. 更新当前处理单号
taskStatus.CurrentWaybill = waybill;
await _cacheService.SetAsync(cacheKey, taskStatus, 60);
try
{
// 3. 执行关联
var (success, errorCode, errorMessage) = await AssociateWaybillAsync(tagNumber, waybill, "system_auto");
if (success)
{
taskStatus.SuccessCount++;
}
else
{
taskStatus.FailedCount++;
taskStatus.FailedItems.Add(new AutoPackFailedItem
{
WaybillNumber = waybill,
ErrorCode = errorCode,
ErrorMessage = errorMessage
});
}
}
catch (Exception ex)
{
taskStatus.FailedCount++;
taskStatus.FailedItems.Add(new AutoPackFailedItem
{
WaybillNumber = waybill,
ErrorCode = 9999,
ErrorMessage = ex.Message
});
}
// 4. 更新进度
taskStatus.ProcessedCount++;
await _cacheService.SetAsync(cacheKey, taskStatus, 60);
// 5. 随机延迟 2-4 秒
if (taskStatus.ProcessedCount < waybills.Count)
{
var delay = random.Next(2000, 4001);
await Task.Delay(delay);
}
}
// 6. 任务完成
var finalStatus = await _cacheService.GetAsync<AutoPackTaskStatus>(cacheKey);
if (finalStatus != null)
{
finalStatus.Status = finalStatus.CancellationTokenSource?.IsCancellationRequested == true ? "cancelled" : "completed";
finalStatus.EndTime = DateTime.UtcNow;
finalStatus.CurrentWaybill = null;
await _cacheService.SetAsync(cacheKey, finalStatus, 60);
}
}
```
**验收标准**
- [ ] 支持取消操作
- [ ] 2-4 秒随机延迟正确实现
- [ ] 进度实时更新到缓存
- [ ] 失败记录完整保存
- [ ] 订单日志正确记录
***
## 阶段四:接口层
### 任务 4.1:实现控制器方法
**文件**`src/CONTROLLER/Controllers/BagTagController.cs`
**新增接口**
1. **启动自动集包**
```csharp
[HttpPost("auto-pack/start")]
public async Task<IActionResult> StartAutoPack([FromBody] StartAutoPackRequest request)
{
try
{
var result = await _bagTagService.StartAutoPackAsync(request.TagNumber, request.Creator);
return Ok(new { code = 0, message = "success", data = result });
}
catch (Exception ex)
{
return Ok(new { code = 1001, message = ex.Message });
}
}
```
1. **查询进度**
```csharp
[HttpGet("auto-pack/progress/{taskId}")]
public async Task<IActionResult> GetAutoPackProgress(string taskId)
{
try
{
var result = await _bagTagService.GetAutoPackProgressAsync(taskId);
if (result == null)
{
return Ok(new { code = 1002, message = "Task not found" });
}
return Ok(new { code = 0, message = "success", data = result });
}
catch (Exception ex)
{
return Ok(new { code = 9999, message = ex.Message });
}
}
```
1. **取消任务**
```csharp
[HttpPost("auto-pack/cancel/{taskId}")]
public async Task<IActionResult> CancelAutoPack(string taskId)
{
try
{
var success = await _bagTagService.CancelAutoPackAsync(taskId);
if (!success)
{
return Ok(new { code = 1002, message = "Task not found or already completed" });
}
return Ok(new { code = 0, message = "Task cancelled successfully" });
}
catch (Exception ex)
{
return Ok(new { code = 9999, message = ex.Message });
}
}
```
1. **获取结果**
```csharp
[HttpGet("auto-pack/result/{taskId}")]
public async Task<IActionResult> GetAutoPackResult(string taskId)
{
try
{
var result = await _bagTagService.GetAutoPackResultAsync(taskId);
if (result == null)
{
return Ok(new { code = 1002, message = "Task not found" });
}
return Ok(new { code = 0, message = "success", data = result });
}
catch (Exception ex)
{
return Ok(new { code = 9999, message = ex.Message });
}
}
```
**验收标准**
- [ ] 所有接口响应格式统一
- [ ] 错误码与 spec.md 一致
- [ ] 支持 JSONP参考现有代码
***
### 任务 4.2:注册依赖注入
**文件**`src/CONTROLLER/Program.cs`(如有需要)
检查是否需要额外注册服务,通常现有 DI 配置已足够。
**验收标准**
- [ ] 服务能正常解析
- [ ] 无循环依赖
***
## 阶段五:测试与优化
### 任务 5.1:编写单元测试
**文件**`test/...`(根据项目测试规范)
**测试用例**
1. 启动任务 - 袋牌不存在
2. 启动任务 - 袋牌未打开
3. 启动任务 - 无符合条件的包裹
4. 启动任务 - 成功启动
5. 查询进度 - 任务不存在
6. 查询进度 - 正常查询
7. 取消任务 - 任务不存在
8. 取消任务 - 任务已完成
9. 取消任务 - 正常取消
10. 自动集包执行 - 全部成功
11. 自动集包执行 - 部分失败
12. 自动集包执行 - 取消操作
**验收标准**
- [ ] 核心逻辑覆盖率达到 80%+
- [ ] 所有测试用例通过
***
### 任务 5.2:性能优化与联调
**优化项**
1. SQL 查询性能优化(添加索引)
2. 缓存过期时间调整
3. 并发任务数限制
4. 异常处理完善
**联调检查**
1. WinForm 前端能正常调用接口
2. 进度实时更新
3. 取消功能正常
4. 大数据量1000+ 包裹)测试
**验收标准**
- [ ] 100 个包裹处理时间 < 10 分钟
- [ ] 内存占用稳定
- [ ] 前端无卡顿
***
## 依赖关系图
```
任务 1.1 (模型) ──┐
├──→ 任务 2.1 (Repository接口) ──→ 任务 2.2 (Repository实现)
任务 1.2 (DTO) ───┘ │
任务 3.1 (Service接口) ──────────────────────────────────────┤
│ │
↓ ↓
任务 3.2 (启动逻辑) ─────────────→ 任务 3.3 (执行逻辑)
│ │
└────────────────┬─────────────────┘
任务 4.1 (控制器)
任务 4.2 (DI注册)
┌───────────┴───────────┐
↓ ↓
任务 5.1 (单元测试) 任务 5.2 (优化联调)
```
***
## 风险与应对
| 风险 | 影响 | 应对措施 |
| ------------ | -- | -------------------------- |
| 包裹数量过大导致任务超时 | | 添加任务超时机制支持断点续传 |
| 缓存失效导致进度丢失 | | 使用持久化存储如数据库保存任务状态 |
| 并发任务过多 | | 限制同时运行的自动集包任务数量 |
| 前端轮询频率过高 | | 建议轮询间隔 1-2 或改用 WebSocket |