上传源代码版本
This commit is contained in:
627
.trae/specs/usps-auto-bag/tasks.md
Normal file
627
.trae/specs/usps-auto-bag/tasks.md
Normal 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 |
|
||||
|
||||
Reference in New Issue
Block a user