Files
LabelChange-server/.trae/specs/usps-auto-bag/tasks.md
2026-06-01 16:30:29 +08:00

628 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |