440 lines
13 KiB
Markdown
440 lines
13 KiB
Markdown
# 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"
|
||
);
|
||
```
|