# USPS 自动集包开发任务分解 ## 任务概览 | 阶段 | 任务数 | 预计工时 | | ------ | ------ | ------- | | 模型定义 | 2 | 2h | | 数据访问层 | 2 | 3h | | 业务逻辑层 | 3 | 5h | | 接口层 | 2 | 3h | | 测试与优化 | 2 | 3h | | **总计** | **11** | **16h** | *** ## 阶段一:模型定义 ### 任务 1.1:创建任务状态模型 **文件**:`src/MDL/Models/AutoPackTaskStatus.cs` **内容**: ```csharp namespace MDL.Models { /// /// 自动集包任务状态 /// 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 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 { /// /// 启动自动集包请求 /// public class StartAutoPackRequest { public string TagNumber { get; set; } public string Creator { get; set; } = "system"; } /// /// 启动自动集包响应 /// 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; } } /// /// 自动集包进度响应 /// 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 FailedItems { get; set; } } /// /// 自动集包结果响应 /// 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 FailedItems { get; set; } } } ``` **验收标准**: - [ ] 包含 Start、Progress、Result 三种响应 DTO - [ ] 字段与 spec.md 定义一致 - [ ] 支持前端 WinForm 调用 *** ## 阶段二:数据访问层 ### 任务 2.1:扩展 Repository 接口 **文件**:`src/DAL/Interfaces/IBagTagRepository.cs` **新增方法**: ```csharp /// /// 查询符合条件的 USPS 包裹 /// Task> GetEligibleUspsWaybillsAsync(DateTime cutoffTime); /// /// 获取符合条件的包裹数量 /// Task 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 /// /// 启动自动集包任务 /// Task StartAutoPackAsync(string tagNumber, string creator); /// /// 查询自动集包进度 /// Task GetAutoPackProgressAsync(string taskId); /// /// 取消自动集包任务 /// Task CancelAutoPackAsync(string taskId); /// /// 获取自动集包结果 /// Task 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 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(), 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 waybills, string creator) { var random = new Random(); var cacheKey = $"autopack:{taskId}"; foreach (var waybill in waybills) { // 1. 获取任务状态 var taskStatus = await _cacheService.GetAsync(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(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 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 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 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 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 |