上传源代码版本

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,41 @@
# 尊祐客户分拣信息回传 - 验证检查清单
## 功能验证
- [ ] 检查 SendWebhookToZunYou 方法是否正确实现
- [ ] 检查方法参数是否正确,包括 waybillNumber, scanTime, printTime, scanResult, finalMileTrackingNumber
- [ ] 检查回传数据格式是否符合尊祐系统要求
- [ ] 检查请求头是否包含正确的 token
- [ ] 检查是否在 DownloadLabelByWaybillNumber 方法的 finally 块中添加了尊祐客户的判断
- [ ] 检查尊祐客户(ZY_SH)的回传是否能够正确触发
- [ ] 检查回传是否采用异步方式,不阻塞主流程
## 数据验证
- [ ] 检查回传数据中的 WaybillNumber 是否正确
- [ ] 检查回传数据中的 TrackingNumber 是否正确获取自 finalMileTrackingNumber
- [ ] 检查回传数据中的 Replaced 字段是否根据 scanResult 正确判断
- [ ] 检查回传数据中的 ReplacedAt 是否使用正确的 UTC 时间格式
## 错误处理
- [ ] 检查网络错误处理是否正确
- [ ] 检查尊祐系统返回错误时的处理是否正确
- [ ] 检查错误日志是否完整记录
- [ ] 检查回传失败是否不影响主流程
## 日志记录
- [ ] 检查回传请求的详细信息是否记录
- [ ] 检查回传成功时的响应信息是否记录
- [ ] 检查回传失败时的错误信息是否记录
- [ ] 检查日志格式是否与现有回传方法一致
## 代码质量
- [ ] 检查代码风格是否与现有代码一致
- [ ] 检查方法命名是否规范
- [ ] 检查注释是否完整
- [ ] 检查是否存在代码重复
## 测试验证
- [ ] 测试尊祐客户的标签下载请求是否能够触发回传
- [ ] 测试回传数据格式是否正确
- [ ] 测试请求头的 token 是否正确设置
- [ ] 测试错误处理机制是否正常工作
- [ ] 测试日志记录是否完整

View File

@@ -0,0 +1,87 @@
# 尊祐客户分拣信息回传 - 产品需求文档
## Overview
- **Summary**: 为尊祐客户添加分拣信息回传功能,当系统处理标签替换请求时,向尊祐系统发送回传通知,包含面单号、跟踪号、替换状态和替换时间等信息。
- **Purpose**: 实现与尊祐系统的对接,确保尊祐能够及时获取标签替换的状态信息,便于其内部物流管理和跟踪。
- **Target Users**: 尊祐客户及其系统集成人员。
## Goals
- 实现向尊祐系统的分拣信息回传功能
- 确保回传数据的准确性和及时性
- 与现有回传机制保持一致的代码风格和错误处理方式
- 提供必要的日志记录以便于问题排查
## Non-Goals (Out of Scope)
- 不修改现有的其他客户回传逻辑
- 不改变系统的核心业务流程
- 不涉及尊祐系统内部的业务逻辑修改
## Background & Context
- 系统已经实现了向派通国际(PT_GZ)和讯通系统(XT_JX)的回传功能
- 回传逻辑位于LabelController.cs的DownloadLabelByWaybillNumber方法中
- 回传采用异步方式,不阻塞主流程
- 尊祐系统提供了RESTful API接口用于接收回传数据
## Functional Requirements
- **FR-1**: 当客户代码为ZY_SH时向尊祐系统发送回传通知
- **FR-2**: 回传数据应包含面单号、跟踪号、替换状态和替换时间
- **FR-3**: 回传请求应包含指定的token认证信息
- **FR-4**: 回传应采用异步方式,不阻塞主流程
- **FR-5**: 回传失败时应记录错误日志,但不影响主流程
## Non-Functional Requirements
- **NFR-1**: 回传请求应设置合理的超时时间,避免长时间阻塞
- **NFR-2**: 应提供详细的日志记录,便于问题排查
- **NFR-3**: 代码应遵循现有代码风格和架构模式
## Constraints
- **Technical**: 使用现有的HttpClientFactory创建HttpClient实例
- **Business**: 必须使用尊祐提供的API地址和token
- **Dependencies**: 依赖现有的LabelReplaceEntity数据模型需要从中获取跟踪号信息
## Assumptions
- 尊祐系统的API接口能够正常接收和处理回传数据
- LabelReplaceEntity中包含了所需的跟踪号信息
- 系统能够正确获取客户代码ZY_SH的客户信息
## Acceptance Criteria
### AC-1: 尊祐客户回传触发
- **Given**: 系统处理尊祐客户(ZY_SH)的标签替换请求
- **When**: 标签下载完成后
- **Then**: 系统应向尊祐系统发送回传通知
- **Verification**: `programmatic`
- **Notes**: 回传应在finally块中异步执行
### AC-2: 回传数据格式正确
- **Given**: 系统向尊祐系统发送回传通知
- **When**: 构造回传数据
- **Then**: 回传数据应符合尊祐系统要求的格式包含WaybillNumber、TrackingNumber、Replaced和ReplacedAt字段
- **Verification**: `programmatic`
- **Notes**: Replaced字段应根据扫描结果判断ReplacedAt应使用UTC时间
### AC-3: 回传请求头正确
- **Given**: 系统向尊祐系统发送回传通知
- **When**: 构造HTTP请求
- **Then**: 请求头应包含指定的token认证信息
- **Verification**: `programmatic`
- **Notes**: token值为1c96499e-3c58-4e20-bc5d-b52ce9f9e36d
### AC-4: 回传失败处理
- **Given**: 向尊祐系统发送回传通知失败
- **When**: 网络错误或尊祐系统返回错误
- **Then**: 系统应记录错误日志,但不影响主流程
- **Verification**: `human-judgment`
- **Notes**: 应使用与现有回传逻辑相同的错误处理方式
### AC-5: 回传成功记录
- **Given**: 向尊祐系统发送回传通知成功
- **When**: 尊祐系统返回成功状态
- **Then**: 系统应记录成功日志
- **Verification**: `human-judgment`
- **Notes**: 应记录响应状态码和响应内容
## Open Questions
- [ ] 尊祐系统对回传数据的具体验证规则是什么?
- [ ] 尊祐系统的API接口是否需要额外的认证或参数
- [ ] 当TrackingNumber为空时回传应如何处理

View File

@@ -0,0 +1,49 @@
# 尊祐客户分拣信息回传 - 实现计划
## [ ] 任务 1: 实现 SendWebhookToZunYou 方法
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在 LabelController.cs 中添加新的私有方法 SendWebhookToZunYou
- 方法参数包括 waybillNumber, scanTime, printTime, scanResult, finalMileTrackingNumber
- 实现向尊祐系统发送回传通知的逻辑
- 构造符合尊祐系统要求的 JSON 数据
- 设置请求头包含指定的 token
- 处理响应和错误情况
- **Acceptance Criteria Addressed**: AC-2, AC-3, AC-4, AC-5
- **Test Requirements**:
- `programmatic` TR-1.1: 方法能够正确构造回传数据格式
- `programmatic` TR-1.2: 方法能够正确设置请求头的 token
- `programmatic` TR-1.3: 方法能够正确处理网络错误和响应错误
- `human-judgment` TR-1.4: 方法的代码风格与现有回传方法一致
- **Notes**: 参考现有的 SendWebhookToPatuen 和 SendWebhookToXunTong 方法的实现方式
## [ ] 任务 2: 在回传逻辑中添加尊祐客户判断
- **Priority**: P0
- **Depends On**: 任务 1
- **Description**:
- 在 DownloadLabelByWaybillNumber 方法的 finally 块中,添加对尊祐客户(ZY_SH)的判断
- 当客户代码为 ZY_SH 时,调用 SendWebhookToZunYou 方法
- 确保使用异步方式调用,不阻塞主流程
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- `programmatic` TR-2.1: 当客户代码为 ZY_SH 时,能够触发回传
- `programmatic` TR-2.2: 回传调用不阻塞主流程
- `human-judgment` TR-2.3: 代码逻辑与现有回传判断逻辑一致
- **Notes**: 参考现有的 PT_GZ 和 XT_JX 客户的回传判断逻辑
## [ ] 任务 3: 测试回传功能
- **Priority**: P1
- **Depends On**: 任务 1, 任务 2
- **Description**:
- 模拟尊祐客户的标签下载请求
- 验证回传请求是否正确发送
- 检查日志记录是否完整
- 验证错误处理是否正常
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3, AC-4, AC-5
- **Test Requirements**:
- `programmatic` TR-3.1: 系统能够正确触发尊祐客户的回传
- `programmatic` TR-3.2: 回传数据格式符合要求
- `human-judgment` TR-3.3: 日志记录完整且清晰
- `human-judgment` TR-3.4: 错误处理机制正常工作
- **Notes**: 可以使用现有的测试方法 TestXunTongWebhook 作为参考,创建类似的测试方法

View File

@@ -0,0 +1,12 @@
# 到货信息统计 MySQL 语句分析 - 验证清单
- [x] 验证业务逻辑理解是否正确
- [x] 验证 MySQL 语句是否正确实现提单号优先的分组逻辑
- [x] 验证 MySQL 语句是否正确计算所有统计数据
- [x] 验证 MySQL 语句是否支持过滤条件
- [x] 验证 MySQL 语句的性能是否满足要求
- [x] 验证索引建议是否合理
- [x] 验证文档是否完整且可读
- [x] 验证 MySQL 语句是否可以直接在数据库中执行
- [x] 验证统计结果是否与预期一致
- [x] 验证所有测试要求是否满足

View File

@@ -0,0 +1,160 @@
# 到货信息统计 MySQL 语句
## 1. 基本统计语句
```sql
SELECT
COALESCE(l.BillOfLadingNumber, l.MasterPackageNumber, 'Unknown') AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
(SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplaceCompletedCount,
COUNT(*) - (SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplacePendingCount,
(SELECT MIN(h.ReceiptTime)
FROM arrival_handover_forms h
WHERE h.HandoverNumber = COALESCE(l.BillOfLadingNumber, l.MasterPackageNumber)) AS ArrivalTime,
MAX(l.BillOfLadingNumber) AS BillOfLadingNumber,
MAX(l.MasterPackageNumber) AS MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= '2026-12-31' OR '2026-12-31' IS NULL)
GROUP BY
COALESCE(l.BillOfLadingNumber, l.MasterPackageNumber, 'Unknown')
ORDER BY
l.CreatedAt DESC;
```
## 2. 优化版本(使用子查询优化扫描记录统计)
```sql
WITH scan_summary AS (
SELECT
NeutralWaybillNumber,
COUNT(*) AS ReturnedLabelCount
FROM
label_scan_history
WHERE
Result = 0
GROUP BY
NeutralWaybillNumber
),
arrival_summary AS (
SELECT
HandoverNumber,
MIN(ReceiptTime) AS MinReceiptTime
FROM
arrival_handover_forms
GROUP BY
HandoverNumber
)
SELECT
COALESCE(l.BillOfLadingNumber, l.MasterPackageNumber, 'Unknown') AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplaceCompletedCount,
COUNT(*) - COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplacePendingCount,
COALESCE(a.MinReceiptTime, b.MinReceiptTime) AS ArrivalTime,
MAX(l.BillOfLadingNumber) AS BillOfLadingNumber,
MAX(l.MasterPackageNumber) AS MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
LEFT JOIN
scan_summary s ON l.NeutralWaybillNumber = s.NeutralWaybillNumber
LEFT JOIN
arrival_summary a ON l.BillOfLadingNumber = a.HandoverNumber
LEFT JOIN
arrival_summary b ON l.MasterPackageNumber = b.HandoverNumber
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= '2026-12-31' OR '2026-12-31' IS NULL)
GROUP BY
COALESCE(l.BillOfLadingNumber, l.MasterPackageNumber, 'Unknown')
ORDER BY
l.CreatedAt DESC;
```
## 3. 实现说明
### 3.1 分组逻辑
使用 `COALESCE` 函数实现提单号优先的分组逻辑:
-`BillOfLadingNumber` 不为 NULL 时,使用 `BillOfLadingNumber` 作为分组依据
-`BillOfLadingNumber` 为 NULL 时,使用 `MasterPackageNumber` 作为分组依据
- 当两者都为 NULL 时,使用 'Unknown' 作为分组依据
### 3.2 统计计算
- **到货订单数量**:使用 `COUNT(*)` 统计每个分组的记录数
- **无标签数据数量**:使用 `SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END)` 统计
- **已有标签订单数**:使用 `SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END)` 统计
- **已有标签率**:计算已有标签订单数占总订单数的百分比
- **换单完成数量**:统计有扫描记录且结果为 0 的记录数
- **未换单完成数量**:总订单数减去换单完成数量
- **到货时间**:从到货交接单表中获取最早的收货时间
### 3.3 过滤条件
- **客户ID过滤**可根据需要修改客户ID值
- **日期范围过滤**:可根据需要修改日期范围
## 4. 性能优化建议
1. **索引优化**
-`label_replace_requests` 表上添加以下索引:
- `(CustomerId, CreatedAt)`
- `(BillOfLadingNumber)`
- `(MasterPackageNumber)`
-`label_scan_history` 表上添加索引:
- `(NeutralWaybillNumber, Result)`
-`arrival_handover_forms` 表上添加索引:
- `(HandoverNumber, ReceiptTime)`
2. **查询优化**
- 使用 CTE (Common Table Expressions) 减少重复子查询
- 避免在 GROUP BY 子句中使用复杂表达式
- 合理使用 JOIN 替代子查询
3. **数据量控制**
- 考虑添加分页功能,避免一次性返回大量数据
- 对于历史数据,可以考虑归档策略
## 5. 使用说明
1. **参数调整**
- 客户ID修改 `l.CustomerId = 1` 中的 1 为实际客户ID
- 日期范围:修改 `'2026-01-01'``'2026-12-31'` 为实际日期范围
2. **结果解释**
- `Key` 列:表示分组依据,可能是提单号、主包号或 'Unknown'
- 其他列:表示各统计指标
3. **注意事项**
- `Key` 是 MySQL 关键字,使用反引号包围
- 所有字符串参数都使用单引号包围
- 日期参数使用 'YYYY-MM-DD' 格式
- 可以根据实际需要修改示例值

View File

@@ -0,0 +1,71 @@
# 到货信息统计 MySQL 语句分析
## Overview
- **Summary**: 分析如何编写 MySQL 语句,在有提单号的情况下以提单号统计到货信息,无提单号的情况下以主包号统计到货信息
- **Purpose**: 提供一个统一的 SQL 语句,根据数据情况自动选择合适的分组依据
- **Target Users**: 开发人员、数据库管理员
## Goals
- 分析到货信息统计的业务逻辑
- 提供统一的 MySQL 语句实现
- 确保统计结果的准确性
- 优化查询性能
## Non-Goals (Out of Scope)
- 不修改现有代码
- 不涉及前端实现细节
- 不处理权限和安全问题
## Background & Context
在物流系统中,到货信息统计是一个常见需求。通常情况下,我们会按提单号进行统计,但在某些情况下(如没有提单号时),需要按主包号进行统计。当前实现可能需要编写多个 SQL 语句来处理不同情况,我们需要一个统一的解决方案。
## Functional Requirements
- **FR-1**: 当记录有提单号时,按提单号分组统计
- **FR-2**: 当记录没有提单号时,按主包号分组统计
- **FR-3**: 计算到货订单数量、无标签数据数量、已有标签率等统计数据
- **FR-4**: 支持按客户ID、日期范围等条件过滤
## Non-Functional Requirements
- **NFR-1**: 性能优化,避免全表扫描
- **NFR-2**: 代码可读性和可维护性
- **NFR-3**: 数据准确性
## Constraints
- **Technical**: 使用MySQL数据库
- **Business**: 无特殊业务约束
- **Dependencies**: 依赖 `label_replace_requests`
## Assumptions
- 数据库表结构已正确创建
- 所有必要的索引已添加
- 数据量在合理范围内
## Acceptance Criteria
### AC-1: 基本统计功能
- **Given**: 存在带有提单号的记录
- **When**: 执行统计查询
- **Then**: 按提单号分组统计
- **Verification**: `programmatic`
### AC-2: 无提单号情况
- **Given**: 存在没有提单号但有主包号的记录
- **When**: 执行统计查询
- **Then**: 按主包号分组统计
- **Verification**: `programmatic`
### AC-3: 性能优化
- **Given**: 数据量较大如10万条记录
- **When**: 执行查询
- **Then**: 查询响应时间在可接受范围内(<5秒
- **Verification**: `programmatic`
### AC-4: 数据准确性
- **Given**: 存在测试数据
- **When**: 执行查询并与手动计算结果比较
- **Then**: 统计数据与手动计算一致
- **Verification**: `human-judgment`
## Open Questions
- [ ] 如何处理既没有提单号也没有主包号的记录
- [ ] 是否需要考虑性能优化的索引策略

View File

@@ -0,0 +1,53 @@
# 到货信息统计 MySQL 语句分析 - 实现计划
## [x] Task 1: 分析业务逻辑
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 分析到货信息统计的业务逻辑
- 确定分组依据的优先级(提单号优先,无提单号时使用主包号)
- 识别需要统计的字段和计算逻辑
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-4
- **Test Requirements**:
- `human-judgment` TR-1.1: 理解业务逻辑
- `human-judgment` TR-1.2: 确定分组策略
- **Notes**: 重点关注如何处理既没有提单号也没有主包号的记录
## [x] Task 2: 编写 MySQL 语句
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 编写统一的 MySQL 语句,实现按提单号或主包号分组统计
- 包含所有必要的统计计算
- 支持过滤条件
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-4
- **Test Requirements**:
- `programmatic` TR-2.1: 验证 SQL 语句的正确性
- `human-judgment` TR-2.2: 确保语句可读性
- **Notes**: 使用 COALESCE 函数来实现优先级逻辑
## [x] Task 3: 性能优化分析
- **Priority**: P1
- **Depends On**: Task 2
- **Description**:
- 分析 SQL 语句的性能
- 识别可能的优化点
- 提供索引建议
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `human-judgment` TR-3.1: 分析查询执行计划
- `human-judgment` TR-3.2: 评估索引使用情况
- **Notes**: 考虑使用 EXPLAIN 分析查询执行计划
## [x] Task 4: 编写完整的文档
- **Priority**: P1
- **Depends On**: Task 2, Task 3
- **Description**:
- 编写详细的 SQL 语句文档
- 包含参数说明和使用示例
- 提供性能优化建议
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3, AC-4
- **Test Requirements**:
- `human-judgment` TR-4.1: 文档完整性
- `human-judgment` TR-4.2: 文档可读性
- **Notes**: 确保文档包含所有必要的信息

View File

@@ -0,0 +1,24 @@
# 批量查询接口 - 验证清单
- [ ] 批量查询label_replace_requests接口
- [ ] 接口返回正确的记录列表,包含所有字段
- [ ] 分页参数正常工作
- [ ] 排序参数正常工作
- [ ] 筛选参数正常工作
- [ ] 接口响应时间不超过5秒
- [ ] 支持至少1000条记录的批量查询
- [ ] 批量查询label_scan_history接口
- [ ] 接口返回正确的记录列表,包含所有字段
- [ ] 分页参数正常工作
- [ ] 排序参数正常工作
- [ ] 筛选参数正常工作
- [ ] 接口响应时间不超过5秒
- [ ] 支持至少1000条记录的批量查询
- [ ] HTML页面
- [ ] 页面布局清晰,交互流畅
- [ ] 页面能正确调用批量查询接口
- [ ] 页面能正确展示查询结果
- [ ] 页面支持分页、排序和筛选操作
- [ ] 页面响应迅速,交互流畅

View File

@@ -0,0 +1,140 @@
# 批量查询换单状态接口规格说明
## 1. 需求概述
新增一个可以查询有没有换单成功以及换单成功时间的接口。支持输入多个中性面单或者是尾程单号。头部中要加入customerCode和apiKey的校验。然后要通过customerCode在labelreplacerequests中找到对应的订单并且换单是否成功要通过有没有成功返回标签的扫描记录来判断这里做连表查询。
## 2. 接口设计
### 2.1 接口路径
`GET /api/Label/label-replace/status`
### 2.2 请求参数
#### 2.2.1 头部参数
| 参数名 | 类型 | 必选 | 描述 |
|-------|------|------|------|
| customerCode | string | 是 | 客户代码 |
| apiKey | string | 是 | API密钥 |
#### 2.2.2 查询参数
| 参数名 | 类型 | 必选 | 描述 |
|-------|------|------|------|
| waybills | string | 否 | 中性面单单号,多个单号用逗号分隔 |
| trackings | string | 否 | 尾程跟踪单号,多个单号用逗号分隔 |
### 2.3 响应结构
```json
{
"status": "ok",
"timestamp": "2026-03-10T10:00:00Z",
"count": 2,
"data": [
{
"waybillNumber": "WB1234567890",
"trackingNumber": "TN9876543210",
"replaced": true,
"replacedAt": "2026-03-10T09:30:00Z"
},
{
"waybillNumber": "WB0987654321",
"trackingNumber": "TN1234567890",
"replaced": false,
"replacedAt": null
}
]
}
```
### 2.4 错误响应
```json
{
"status": "error",
"message": "错误信息",
"errorDetails": "详细错误信息"
}
```
## 3. 业务逻辑
1. **头部校验**验证customerCode和apiKey是否有效
2. **参数验证**确保至少提供了waybills或trackings参数
3. **数据查询**
- 根据customerCode找到对应的客户ID
- 根据提供的单号查询label_replace_requests表
- 联合查询label_scan_history表判断是否有成功的扫描记录Result=0
4. **结果处理**
- 对于每个单号,判断是否换单成功
- 记录换单成功的时间(最新的成功扫描记录时间)
- 构建响应数据
## 4. 技术实现
### 4.1 控制器实现
`LabelController`中添加新的方法,处理批量查询请求。
### 4.2 服务层实现
`ILabelReplaceService`接口中添加新的方法,实现批量查询逻辑。
### 4.3 数据访问层实现
`ILabelReplaceRepository`接口中添加新的方法,实现连表查询逻辑。
## 5. 验证规则
1. **头部校验**
- customerCode和apiKey不能为空
- 验证apiKey是否与customerCode匹配且有效
2. **参数验证**
- 至少提供waybills或trackings参数
- 单号格式验证
3. **数据验证**
- 确保查询的订单属于指定的客户
- 确保扫描记录与订单关联
## 6. 性能考虑
- 支持批量查询减少API调用次数
- 使用索引优化查询性能
- 合理处理大数据量的情况
## 7. 安全考虑
- 严格的API密钥验证
- 防止SQL注入攻击
- 限制查询结果数量
## 8. 测试用例
### 8.1 成功场景
- 批量查询多个中性面单号
- 批量查询多个尾程跟踪单号
- 混合查询中性面单和尾程跟踪单号
### 8.2 失败场景
- 缺少头部参数
- API密钥无效
- 提供的单号不存在
- 提供的单号不属于指定客户
## 9. 实现步骤
1.`ILabelReplaceService`中添加新方法
2.`LabelReplaceService`中实现该方法
3.`ILabelReplaceRepository`中添加新方法
4.`LabelReplaceRepository`中实现该方法
5.`LabelController`中添加新的API端点
6. 实现头部校验逻辑
7. 编写测试用例
8. 部署和验证

View File

@@ -0,0 +1,60 @@
# 批量查询接口 - 实现计划
## [x] 任务 1: 添加批量查询label_replace_requests的接口
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在LabelController中添加批量查询接口
- 支持分页、排序和筛选参数
- 返回所有字段的记录列表
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- `programmatic` TR-1.1: 接口返回正确的记录列表,包含所有字段
- `programmatic` TR-1.2: 分页参数正常工作
- `programmatic` TR-1.3: 排序参数正常工作
- `programmatic` TR-1.4: 筛选参数正常工作
- **Notes**: 使用现有的ILabelReplaceService和ILabelReplaceRepository服务
## [x] 任务 2: 添加批量查询label_scan_history的接口
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在LabelController中添加批量查询接口
- 支持分页、排序和筛选参数
- 返回所有字段的记录列表
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- `programmatic` TR-2.1: 接口返回正确的记录列表,包含所有字段
- `programmatic` TR-2.2: 分页参数正常工作
- `programmatic` TR-2.3: 排序参数正常工作
- `programmatic` TR-2.4: 筛选参数正常工作
- **Notes**: 使用现有的ILabelScanService和ILabelScanRepository服务
## [x] 任务 3: 生成HTML页面
- **Priority**: P1
- **Depends On**: 任务 1, 任务 2
- **Description**:
- 创建HTML页面包含查询表单和结果展示
- 添加JavaScript代码调用批量查询接口
- 支持分页、排序和筛选操作
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `human-judgment` TR-3.1: 页面布局清晰,交互流畅
- `programmatic` TR-3.2: 页面能正确调用批量查询接口
- `programmatic` TR-3.3: 页面能正确展示查询结果
- `human-judgment` TR-3.4: 页面支持分页、排序和筛选操作
- **Notes**: 使用Bootstrap和jQuery简化开发
## [x] 任务 4: 测试和验证
- **Priority**: P1
- **Depends On**: 任务 1, 任务 2, 任务 3
- **Description**:
- 测试批量查询接口的功能
- 测试HTML页面的功能
- 验证接口响应时间
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3
- **Test Requirements**:
- `programmatic` TR-4.1: 接口响应时间不超过5秒
- `programmatic` TR-4.2: 支持至少1000条记录的批量查询
- `human-judgment` TR-4.3: HTML页面响应迅速交互流畅
- **Notes**: 使用Postman测试API接口

View File

@@ -0,0 +1,21 @@
# 批量查询页面优化 - 验证清单
## 功能验证
- [x] 表格单元格宽度是否根据数据长度自动调整
- [x] 标签替换请求表格中是否不再显示参考号列
- [x] 标签替换请求表格中是否不再显示更新时间列
- [x] 其他列的显示顺序和内容是否不受影响
- [x] 所有查询标签页的功能是否正常
- [x] 数据导出功能是否正常
- [x] 分页功能是否正常
## 视觉验证
- [x] 表格在不同屏幕尺寸下是否显示正常
- [x] 页面在不同浏览器中是否显示一致
- [x] 表格数据是否完整显示,没有被截断
- [x] 页面布局是否美观,没有错位或重叠
## 性能验证
- [x] 页面加载速度是否正常
- [x] 表格渲染速度是否正常
- [x] 数据查询响应时间是否正常

View File

@@ -0,0 +1,66 @@
# 批量查询页面优化 - 产品需求文档
## Overview
- **Summary**: 优化批量查询页面的数据显示,使表格单元格与数据长度相适应,并在标签替换请求中取消参考号和更新时间的显示。
- **Purpose**: 提高页面的可读性和用户体验,减少不必要的信息显示,使数据展示更加清晰。
- **Target Users**: 系统管理员和操作人员。
## Goals
- 优化所有数据查询表格的显示,使单元格宽度与数据长度相适应。
- 在标签替换请求查询中取消参考号和更新时间的显示。
- 保持其他功能的完整性和可用性。
## Non-Goals (Out of Scope)
- 不修改后端API接口。
- 不改变其他页面的功能和布局。
- 不影响数据导出功能。
## Background & Context
当前批量查询页面的表格显示存在以下问题:
1. 表格单元格宽度固定,导致数据过长时显示不完整或换行,影响可读性。
2. 标签替换请求查询中显示了参考号和更新时间,这些信息可能对用户来说不是必要的,增加了表格的复杂度。
## Functional Requirements
- **FR-1**: 优化表格显示,使单元格宽度根据数据长度自动调整。
- **FR-2**: 在标签替换请求查询中移除参考号和更新时间的显示。
- **FR-3**: 保持其他查询功能和数据展示的完整性。
## Non-Functional Requirements
- **NFR-1**: 页面加载速度和响应时间不受影响。
- **NFR-2**: 表格显示效果在不同浏览器中保持一致。
- **NFR-3**: 代码修改应保持简洁不引入新的bug。
## Constraints
- **Technical**: 基于现有的HTML、CSS和JavaScript代码进行修改。
- **Dependencies**: 依赖Bootstrap CSS框架和jQuery库。
## Assumptions
- 后端API返回的数据结构保持不变。
- 用户使用现代浏览器访问页面。
## Acceptance Criteria
### AC-1: 表格单元格宽度自适应
- **Given**: 用户打开批量查询页面并执行查询操作。
- **When**: 表格显示查询结果时。
- **Then**: 表格单元格宽度应根据数据长度自动调整,确保数据完整显示。
- **Verification**: `human-judgment`
- **Notes**: 验证所有查询标签页的表格显示效果。
### AC-2: 标签替换请求中取消参考号和更新时间显示
- **Given**: 用户打开标签替换请求查询标签页。
- **When**: 执行查询操作后。
- **Then**: 表格中不应显示参考号和更新时间列。
- **Verification**: `human-judgment`
- **Notes**: 确保其他列的显示顺序和内容不受影响。
### AC-3: 其他功能保持正常
- **Given**: 用户使用页面的其他功能。
- **When**: 执行查询、导出等操作时。
- **Then**: 所有功能应正常工作,不受优化影响。
- **Verification**: `human-judgment`
- **Notes**: 验证数据导出、分页等功能是否正常。
## Open Questions
- [ ] 表格宽度自适应是否需要考虑不同屏幕尺寸的响应式布局?
- [ ] 取消参考号和更新时间的显示是否会影响数据导出功能?

View File

@@ -0,0 +1,42 @@
# 批量查询页面优化 - 实现计划
## [x] Task 1: 优化表格显示,使单元格宽度自适应
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 添加CSS样式使表格单元格宽度根据内容自动调整
- 确保表格在不同屏幕尺寸下都能正常显示
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- `human-judgment` TR-1.1: 表格单元格宽度应根据数据长度自动调整
- `human-judgment` TR-1.2: 表格在不同屏幕尺寸下显示正常
- **Notes**: 使用Bootstrap的表格类和适当的CSS样式实现自适应宽度
## [x] Task 2: 移除标签替换请求中的参考号和更新时间列
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 修改HTML表格结构移除参考号和更新时间列
- 更新JavaScript渲染函数移除对应的数据处理
- 确保导出功能不受影响
- **Acceptance Criteria Addressed**: AC-2, AC-3
- **Test Requirements**:
- `human-judgment` TR-2.1: 标签替换请求表格中不再显示参考号和更新时间列
- `human-judgment` TR-2.2: 其他列的显示顺序和内容不受影响
- `human-judgment` TR-2.3: 数据导出功能正常工作
- **Notes**: 需要修改HTML表格头部和JavaScript渲染函数
## [x] Task 3: 验证所有查询功能正常
- **Priority**: P2
- **Depends On**: Task 1, Task 2
- **Description**:
- 测试所有查询标签页的功能
- 验证数据导出、分页等功能是否正常
- 确保页面在不同浏览器中显示一致
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `human-judgment` TR-3.1: 所有查询标签页的功能正常
- `human-judgment` TR-3.2: 数据导出功能正常
- `human-judgment` TR-3.3: 分页功能正常
- `human-judgment` TR-3.4: 页面在不同浏览器中显示一致
- **Notes**: 测试时需要执行各种查询操作,验证功能完整性

View File

@@ -0,0 +1,12 @@
# 数据看板查询逻辑分析 - 验证清单
- [x] 验证当前查询逻辑的理解是否正确
- [x] 验证 MySQL 语句是否完整覆盖所有过滤条件
- [x] 验证 MySQL 语句是否正确计算所有统计数据
- [x] 验证 MySQL 语句的性能是否优于当前实现
- [x] 验证索引建议是否合理
- [x] 验证文档是否完整且可读
- [x] 验证 MySQL 语句是否可以直接在数据库中执行
- [x] 验证统计结果是否与当前实现一致
- [x] 验证性能优化建议是否可行
- [x] 验证所有测试要求是否满足

View File

@@ -0,0 +1,262 @@
# 数据看板查询 MySQL 语句
## 1. 提单号预报查询
### 1.1 基本查询语句
```sql
SELECT
l.BillOfLadingNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
(SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplaceCompletedCount,
COUNT(*) - (SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplacePendingCount,
(SELECT MIN(h.ReceiptTime)
FROM arrival_handover_forms h
WHERE h.HandoverNumber = l.BillOfLadingNumber
OR h.HandoverNumber = l.MasterPackageNumber) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = :billOfLadingNumber OR :billOfLadingNumber IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = :masterPackageNumber OR :masterPackageNumber IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= :endDate OR :endDate IS NULL)
GROUP BY
l.BillOfLadingNumber
ORDER BY
l.CreatedAt DESC;
```
### 1.2 优化版本(使用子查询优化扫描记录统计)
```sql
WITH scan_summary AS (
SELECT
NeutralWaybillNumber,
COUNT(*) AS ReturnedLabelCount
FROM
label_scan_history
WHERE
Result = 0
GROUP BY
NeutralWaybillNumber
),
arrival_summary AS (
SELECT
HandoverNumber,
MIN(ReceiptTime) AS MinReceiptTime
FROM
arrival_handover_forms
GROUP BY
HandoverNumber
)
SELECT
l.BillOfLadingNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplaceCompletedCount,
COUNT(*) - COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplacePendingCount,
COALESCE(a.MinReceiptTime, b.MinReceiptTime) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
LEFT JOIN
scan_summary s ON l.NeutralWaybillNumber = s.NeutralWaybillNumber
LEFT JOIN
arrival_summary a ON l.BillOfLadingNumber = a.HandoverNumber
LEFT JOIN
arrival_summary b ON l.MasterPackageNumber = b.HandoverNumber
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = :billOfLadingNumber OR :billOfLadingNumber IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = :masterPackageNumber OR :masterPackageNumber IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= :endDate OR :endDate IS NULL)
GROUP BY
l.BillOfLadingNumber
ORDER BY
l.CreatedAt DESC;
```
## 2. 大箱号预报查询
### 2.1 基本查询语句
```sql
SELECT
l.MasterPackageNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
(SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplaceCompletedCount,
COUNT(*) - (SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplacePendingCount,
(SELECT MIN(h.ReceiptTime)
FROM arrival_handover_forms h
WHERE h.HandoverNumber = l.BillOfLadingNumber
OR h.HandoverNumber = l.MasterPackageNumber) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = :billOfLadingNumber OR :billOfLadingNumber IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = :masterPackageNumber OR :masterPackageNumber IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= :endDate OR :endDate IS NULL)
GROUP BY
l.MasterPackageNumber
ORDER BY
l.CreatedAt DESC;
```
### 2.2 优化版本(使用子查询优化扫描记录统计)
```sql
WITH scan_summary AS (
SELECT
NeutralWaybillNumber,
COUNT(*) AS ReturnedLabelCount
FROM
label_scan_history
WHERE
Result = 0
GROUP BY
NeutralWaybillNumber
),
arrival_summary AS (
SELECT
HandoverNumber,
MIN(ReceiptTime) AS MinReceiptTime
FROM
arrival_handover_forms
GROUP BY
HandoverNumber
)
SELECT
l.MasterPackageNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplaceCompletedCount,
COUNT(*) - COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplacePendingCount,
COALESCE(a.MinReceiptTime, b.MinReceiptTime) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
LEFT JOIN
scan_summary s ON l.NeutralWaybillNumber = s.NeutralWaybillNumber
LEFT JOIN
arrival_summary a ON l.BillOfLadingNumber = a.HandoverNumber
LEFT JOIN
arrival_summary b ON l.MasterPackageNumber = b.HandoverNumber
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = :billOfLadingNumber OR :billOfLadingNumber IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = :masterPackageNumber OR :masterPackageNumber IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= :endDate OR :endDate IS NULL)
GROUP BY
l.MasterPackageNumber
ORDER BY
l.CreatedAt DESC;
```
## 3. 参数说明
| 参数 | 类型 | 描述 |
|------|------|------|
| customerId | INT | 客户ID |
| billOfLadingNumber | VARCHAR(100) | 提单号 |
| masterPackageNumber | VARCHAR(100) | 大箱号 |
| startDate | DATETIME | 开始日期 |
| endDate | DATETIME | 结束日期 |
## 4. 性能优化建议
1. **索引优化**
-`label_replace_requests` 表上添加以下索引:
- `(CustomerId, CreatedAt)`
- `(BillOfLadingNumber, CreatedAt)`
- `(MasterPackageNumber, CreatedAt)`
-`label_scan_history` 表上添加索引:
- `(NeutralWaybillNumber, Result)`
-`arrival_handover_forms` 表上添加索引:
- `(HandoverNumber, ReceiptTime)`
2. **查询优化**
- 使用 CTE (Common Table Expressions) 减少重复子查询
- 避免在 GROUP BY 子句中使用函数
- 合理使用 LEFT JOIN 替代子查询
3. **数据量控制**
- 考虑添加分页功能,避免一次性返回大量数据
- 对于历史数据,可以考虑归档策略
4. **执行计划分析**
- 使用 `EXPLAIN` 分析查询执行计划
- 确保索引被正确使用
- 优化 JOIN 顺序
## 5. 注意事项
- 以上语句假设数据库表结构与当前代码一致
- 实际使用时需要替换参数占位符(如 `1`)为实际值
- 对于大量数据,建议使用优化版本的查询语句
- 可以根据实际情况调整索引策略

View File

@@ -0,0 +1,239 @@
# 数据看板查询 MySQL 语句(修复版)
## 1. 提单号预报查询
### 1.1 基本查询语句
```sql
SELECT
l.BillOfLadingNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
(SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplaceCompletedCount,
COUNT(*) - (SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplacePendingCount,
(SELECT MIN(h.ReceiptTime)
FROM arrival_handover_forms h
WHERE h.HandoverNumber = l.BillOfLadingNumber
OR h.HandoverNumber = l.MasterPackageNumber) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = 'BOL123456' OR 'BOL123456' IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = 'MP123456' OR 'MP123456' IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= '2026-12-31' OR '2026-12-31' IS NULL)
GROUP BY
l.BillOfLadingNumber
ORDER BY
l.CreatedAt DESC;
```
### 1.2 优化版本(使用子查询优化扫描记录统计)
```sql
WITH scan_summary AS (
SELECT
NeutralWaybillNumber,
COUNT(*) AS ReturnedLabelCount
FROM
label_scan_history
WHERE
Result = 0
GROUP BY
NeutralWaybillNumber
),
arrival_summary AS (
SELECT
HandoverNumber,
MIN(ReceiptTime) AS MinReceiptTime
FROM
arrival_handover_forms
GROUP BY
HandoverNumber
)
SELECT
l.BillOfLadingNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplaceCompletedCount,
COUNT(*) - COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplacePendingCount,
COALESCE(a.MinReceiptTime, b.MinReceiptTime) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
LEFT JOIN
scan_summary s ON l.NeutralWaybillNumber = s.NeutralWaybillNumber
LEFT JOIN
arrival_summary a ON l.BillOfLadingNumber = a.HandoverNumber
LEFT JOIN
arrival_summary b ON l.MasterPackageNumber = b.HandoverNumber
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = 'BOL123456' OR 'BOL123456' IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = 'MP123456' OR 'MP123456' IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= '2026-12-31' OR '2026-12-31' IS NULL)
GROUP BY
l.BillOfLadingNumber
ORDER BY
l.CreatedAt DESC;
```
## 2. 大箱号预报查询
### 2.1 基本查询语句
```sql
SELECT
l.MasterPackageNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
(SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplaceCompletedCount,
COUNT(*) - (SELECT COUNT(*)
FROM label_scan_history s
WHERE s.NeutralWaybillNumber = l.NeutralWaybillNumber
AND s.Result = 0) AS ReplacePendingCount,
(SELECT MIN(h.ReceiptTime)
FROM arrival_handover_forms h
WHERE h.HandoverNumber = l.BillOfLadingNumber
OR h.HandoverNumber = l.MasterPackageNumber) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = 'BOL123456' OR 'BOL123456' IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = 'MP123456' OR 'MP123456' IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= '2026-12-31' OR '2026-12-31' IS NULL)
GROUP BY
l.MasterPackageNumber
ORDER BY
l.CreatedAt DESC;
```
### 2.2 优化版本(使用子查询优化扫描记录统计)
```sql
WITH scan_summary AS (
SELECT
NeutralWaybillNumber,
COUNT(*) AS ReturnedLabelCount
FROM
label_scan_history
WHERE
Result = 0
GROUP BY
NeutralWaybillNumber
),
arrival_summary AS (
SELECT
HandoverNumber,
MIN(ReceiptTime) AS MinReceiptTime
FROM
arrival_handover_forms
GROUP BY
HandoverNumber
)
SELECT
l.MasterPackageNumber AS `Key`,
c.CustomerCode AS CustomerCode,
COUNT(*) AS ArrivalOrderCount,
SUM(CASE WHEN l.Label IS NULL OR l.Label = '' THEN 1 ELSE 0 END) AS NoLabelDataCount,
SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) AS LabeledOrderCount,
CONCAT(ROUND((SUM(CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2), '%') AS LabelRate,
COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplaceCompletedCount,
COUNT(*) - COALESCE(SUM(s.ReturnedLabelCount), 0) AS ReplacePendingCount,
COALESCE(a.MinReceiptTime, b.MinReceiptTime) AS ArrivalTime,
l.BillOfLadingNumber,
l.MasterPackageNumber
FROM
label_replace_requests l
LEFT JOIN
customers c ON l.CustomerId = c.Id
LEFT JOIN
scan_summary s ON l.NeutralWaybillNumber = s.NeutralWaybillNumber
LEFT JOIN
arrival_summary a ON l.BillOfLadingNumber = a.HandoverNumber
LEFT JOIN
arrival_summary b ON l.MasterPackageNumber = b.HandoverNumber
WHERE
1=1
-- 客户ID过滤
AND (l.CustomerId = 1 OR 1 IS NULL)
-- 提单号过滤
AND (l.BillOfLadingNumber = 'BOL123456' OR 'BOL123456' IS NULL)
-- 大箱号过滤
AND (l.MasterPackageNumber = 'MP123456' OR 'MP123456' IS NULL)
-- 日期范围过滤
AND (l.CreatedAt >= '2026-01-01' OR '2026-01-01' IS NULL)
AND (l.CreatedAt <= '2026-12-31' OR '2026-12-31' IS NULL)
GROUP BY
l.MasterPackageNumber
ORDER BY
l.CreatedAt DESC;
```
## 3. 使用说明
1. **参数说明**
- 客户ID1示例值根据实际情况修改
- 提单号:'BOL123456'(示例值,根据实际情况修改)
- 大箱号:'MP123456'(示例值,根据实际情况修改)
- 开始日期:'2026-01-01'(示例值,根据实际情况修改)
- 结束日期:'2026-12-31'(示例值,根据实际情况修改)
2. **注意事项**
- `Key` 是 MySQL 关键字,使用反引号包围
- 所有字符串参数都使用单引号包围
- 日期参数使用 'YYYY-MM-DD' 格式
- 可以根据实际需要修改示例值
3. **性能优化**
- 对于大量数据,建议使用优化版本的查询语句
- 确保相关表有合适的索引
- 可以使用 `EXPLAIN` 分析查询执行计划

View File

@@ -0,0 +1,67 @@
# 数据看板查询逻辑分析 - MySQL语句分析
## Overview
- **Summary**: 分析数据看板查询的MySQL语句实现包括标签替换请求的获取、过滤、分组和统计逻辑
- **Purpose**: 帮助排查数据看板查询逻辑的问题提供MySQL语句的等价实现
- **Target Users**: 开发人员、数据库管理员
## Goals
- 分析数据看板查询的完整流程
- 提供MySQL等价语句
- 识别潜在的性能问题
- 提供优化建议
## Non-Goals (Out of Scope)
- 不修改现有代码
- 不涉及前端实现细节
- 不处理权限和安全问题
## Background & Context
数据看板通过 `DashboardController.GetDashboardData` 接口获取数据,该接口调用 `LabelReplaceService.GetDashboardDataAsync` 方法。当前实现使用内存过滤和分组,可能在数据量较大时存在性能问题。
## Functional Requirements
- **FR-1**: 支持按提单号或大箱号查询
- **FR-2**: 支持按客户ID过滤
- **FR-3**: 支持按日期范围过滤
- **FR-4**: 计算到货订单数量、无标签数据数量、已有标签率、换单完成数量、未换单完成数量
- **FR-5**: 获取到货时间
## Non-Functional Requirements
- **NFR-1**: 性能优化,避免全表扫描
- **NFR-2**: 代码可读性和可维护性
- **NFR-3**: 数据准确性
## Constraints
- **Technical**: 使用MySQL数据库SqlSugar ORM框架
- **Business**: 无特殊业务约束
- **Dependencies**: 依赖 `label_replace_requests``label_scan_history``arrival_handover_forms`
## Assumptions
- 数据库表结构已正确创建
- 所有必要的索引已添加
- 数据量在合理范围内
## Acceptance Criteria
### AC-1: 基本查询功能
- **Given**: 提供查询参数(类型、提单号/大箱号、日期范围、客户ID
- **When**: 调用数据看板查询接口
- **Then**: 返回正确的统计数据
- **Verification**: `programmatic`
### AC-2: 性能优化
- **Given**: 数据量较大如10万条记录
- **When**: 执行查询
- **Then**: 查询响应时间在可接受范围内(<5秒
- **Verification**: `programmatic`
### AC-3: 数据准确性
- **Given**: 存在测试数据
- **When**: 执行查询并与手动计算结果比较
- **Then**: 统计数据与手动计算一致
- **Verification**: `human-judgment`
## Open Questions
- [ ] 到货时间的获取逻辑是否完整
- [ ] 扫描记录的查询是否需要进一步优化
- [ ] 是否需要添加更多索引以提高性能

View File

@@ -0,0 +1,53 @@
# 数据看板查询逻辑分析 - 实现计划
## [x] Task 1: 分析当前查询逻辑
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 分析 `LabelReplaceService.GetDashboardDataAsync` 方法的实现
- 了解当前的过滤、分组和统计逻辑
- 识别潜在的性能问题
- **Acceptance Criteria Addressed**: AC-1, AC-3
- **Test Requirements**:
- `human-judgment` TR-1.1: 理解当前代码逻辑
- `human-judgment` TR-1.2: 识别性能瓶颈
- **Notes**: 重点关注内存过滤和分组的性能影响
## [x] Task 2: 生成 MySQL 等价语句
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 为提单号预报生成 MySQL 等价语句
- 为大箱号预报生成 MySQL 等价语句
- 包含所有过滤条件和统计计算
- **Acceptance Criteria Addressed**: AC-1, AC-3
- **Test Requirements**:
- `programmatic` TR-2.1: 验证 MySQL 语句的正确性
- `human-judgment` TR-2.2: 确保语句可读性
- **Notes**: 使用 MySQL 聚合函数和分组操作
## [x] Task 3: 性能优化分析
- **Priority**: P1
- **Depends On**: Task 2
- **Description**:
- 分析 MySQL 语句的性能
- 识别可能的优化点
- 提供索引建议
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- `human-judgment` TR-3.1: 分析查询执行计划
- `human-judgment` TR-3.2: 评估索引使用情况
- **Notes**: 考虑使用 EXPLAIN 分析查询执行计划
## [x] Task 4: 编写完整的 MySQL 语句文档
- **Priority**: P1
- **Depends On**: Task 2, Task 3
- **Description**:
- 编写详细的 MySQL 语句文档
- 包含参数说明和使用示例
- 提供性能优化建议
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3
- **Test Requirements**:
- `human-judgment` TR-4.1: 文档完整性
- `human-judgment` TR-4.2: 文档可读性
- **Notes**: 确保文档包含所有必要的信息

View File

@@ -0,0 +1,12 @@
# 数据看板查询逻辑重构 - 验证清单
- [ ] 验证当前查询逻辑的分析是否正确
- [ ] 验证新查询逻辑的设计是否合理
- [ ] 验证后端API修改是否完成
- [ ] 验证提单号预报功能是否正常
- [ ] 验证大箱号预报功能是否正常
- [ ] 验证统计数据的准确性
- [ ] 验证查询性能是否满足要求
- [ ] 验证API接口是否与前端兼容
- [ ] 验证文档是否完整且可读
- [ ] 验证所有测试要求是否满足

View File

@@ -0,0 +1,82 @@
# 数据看板查询逻辑重构 - 修改说明文档
## 1. 变更概述
本次修改重构了数据看板的查询逻辑,从到货交接单的数据开始,以到货交接单的交接单号去匹配提单号和主包号,从而统计到货情况。
## 2. 变更内容
### 2.1 后端API修改
修改了 `LabelReplaceService.cs` 中的 `GetDashboardDataAsync` 方法,具体变更如下:
1. **查询逻辑起点变更**:从 `label_replace_requests` 表变更为 `arrival_handover_forms`
2. **数据匹配方式**:以到货交接单的 `HandoverNumber` 去匹配 `label_replace_requests` 表中的 `BillOfLadingNumber``MasterPackageNumber`
3. **保持兼容性**保持现有的API接口不变确保前端代码无需修改
### 2.2 实现细节
#### 2.2.1 提单号预报查询逻辑
1. 获取所有符合条件的到货交接单
2. 对于每个到货交接单,以其交接单号作为提单号,匹配 `label_replace_requests` 表中的 `BillOfLadingNumber`
3.`BillOfLadingNumber` 分组统计
4. 计算各种统计指标
#### 2.2.2 大箱号预报查询逻辑
1. 获取所有符合条件的到货交接单
2. 对于每个到货交接单,以其交接单号作为大箱号,匹配 `label_replace_requests` 表中的 `MasterPackageNumber`
3.`MasterPackageNumber` 分组统计
4. 计算各种统计指标
## 3. 性能优化
### 3.1 索引优化建议
-`arrival_handover_forms` 表的 `HandoverNumber` 字段添加索引
-`label_replace_requests` 表的 `BillOfLadingNumber``MasterPackageNumber` 字段添加索引
-`label_replace_requests` 表的 `CustomerId` 字段添加索引
### 3.2 查询优化
- 使用批量查询减少数据库访问次数
- 避免在循环中进行数据库查询
- 合理使用内存缓存
## 4. 使用说明
### 4.1 前端使用
前端代码无需修改继续使用现有的API接口
- 提单号预报:`/api/dashboard/query?type=billOfLading&billOfLadingNumber=xxx&startDate=xxx&endDate=xxx&customerId=xxx`
- 大箱号预报:`/api/dashboard/query?type=masterPackage&masterPackageNumber=xxx&startDate=xxx&endDate=xxx&customerId=xxx`
### 4.2 后端配置
1. 确保 `IArrivalHandoverFormService` 接口已正确实现
2. 确保数据库表结构已正确创建
3. 考虑添加必要的索引以提高查询性能
## 5. 测试建议
### 5.1 功能测试
1. 测试提单号预报功能,验证数据是否正确统计
2. 测试大箱号预报功能,验证数据是否正确统计
3. 测试各种过滤条件,确保过滤功能正常工作
### 5.2 性能测试
1. 测试大数据量下的查询性能
2. 测试响应时间,确保在可接受范围内(<5秒
### 5.3 数据准确性测试
1. 使用测试数据验证统计结果的准确性
2. 与手动计算结果进行比较
## 6. 注意事项
1. **数据一致性**确保到货交接单与标签替换请求数据的一致性
2. **性能监控**监控查询性能必要时进行进一步优化
3. **错误处理**确保错误处理机制完善避免因数据异常导致查询失败
4. **日志记录**确保关键操作有充分的日志记录便于排查问题
## 7. 总结
本次修改重构了数据看板的查询逻辑从到货交接单的数据开始以交接单号匹配提单号和主包号从而更加准确地统计到货情况修改保持了与前端的兼容性同时提供了更好的性能和数据准确性

View File

@@ -0,0 +1,285 @@
# 数据看板新查询逻辑设计
## 1. 新查询逻辑概述
### 1.1 基本思路
- 从到货交接单arrival_handover_forms开始查询
- 以到货交接单的交接单号HandoverNumber去匹配标签替换请求label_replace_requests表中的提单号BillOfLadingNumber或主包号MasterPackageNumber
- 保持现有的两个模块:提单号预报和大箱号预报
### 1.2 实现步骤
1. 获取所有符合条件的到货交接单
2. 对于每个到货交接单,根据其交接单号匹配标签替换请求
3. 按提单号或大箱号分组统计
4. 计算各种统计指标
## 2. 具体实现逻辑
### 2.1 提单号预报查询逻辑
1. 获取所有符合条件的到货交接单
2. 对于每个到货交接单,以其交接单号作为提单号,匹配 label_replace_requests 表中的 BillOfLadingNumber
3. 按 BillOfLadingNumber 分组统计
4. 计算统计指标
### 2.2 大箱号预报查询逻辑
1. 获取所有符合条件的到货交接单
2. 对于每个到货交接单,以其交接单号作为大箱号,匹配 label_replace_requests 表中的 MasterPackageNumber
3. 按 MasterPackageNumber 分组统计
4. 计算统计指标
## 3. 代码实现设计
### 3.1 修改 LabelReplaceService.cs 中的 GetDashboardDataAsync 方法
```csharp
public async Task<List<DashboardDataDto>> GetDashboardDataAsync(string type, string billOfLadingNumber, string masterPackageNumber, string startDate, string endDate, int? customerId)
{
try
{
_logger.LogInformation("Retrieving dashboard data. Type: {type}, BillOfLading: {billOfLading}, MasterPackage: {masterPackage}",
type, billOfLadingNumber, masterPackageNumber);
// 1. 获取所有符合条件的到货交接单
var allArrivalForms = await _arrivalHandoverFormService.GetAllAsync();
// 2. 过滤到货交接单
var filteredArrivalForms = allArrivalForms.Where(form =>
{
// 按日期过滤
if (!string.IsNullOrEmpty(startDate))
{
var start = DateTime.Parse(startDate);
if (form.ReceiptTime < start)
return false;
}
if (!string.IsNullOrEmpty(endDate))
{
var end = DateTime.Parse(endDate);
if (form.ReceiptTime > end)
return false;
}
return true;
}).ToList();
// 3. 获取所有标签替换请求
var allRequests = await _labelReplaceRepository.GetAllAsync();
// 4. 按类型匹配数据
var matchedRequests = new List<LabelReplaceEntity>();
foreach (var form in filteredArrivalForms)
{
if (type == "billOfLading")
{
// 提单号预报:以交接单号匹配提单号
var requests = allRequests.Where(req =>
req.BillOfLadingNumber == form.HandoverNumber &&
(!customerId.HasValue || req.CustomerId == customerId.Value) &&
(string.IsNullOrEmpty(billOfLadingNumber) || req.BillOfLadingNumber == billOfLadingNumber)
);
matchedRequests.AddRange(requests);
}
else
{
// 大箱号预报:以交接单号匹配主包号
var requests = allRequests.Where(req =>
req.MasterPackageNumber == form.HandoverNumber &&
(!customerId.HasValue || req.CustomerId == customerId.Value) &&
(string.IsNullOrEmpty(masterPackageNumber) || req.MasterPackageNumber == masterPackageNumber)
);
matchedRequests.AddRange(requests);
}
}
// 5. 按提单号或大箱号分组
var groupedData = new Dictionary<string, List<LabelReplaceEntity>>();
foreach (var req in matchedRequests)
{
string key;
if (type == "billOfLading")
{
key = req.BillOfLadingNumber ?? "Unknown";
}
else
{
key = req.MasterPackageNumber ?? "Unknown";
}
if (!groupedData.ContainsKey(key))
{
groupedData[key] = new List<LabelReplaceEntity>();
}
groupedData[key].Add(req);
}
// 6. 处理每个分组的数据
var dashboardData = new List<DashboardDataDto>();
foreach (var group in groupedData)
{
var requests = group.Value;
if (requests.Count == 0)
continue;
var firstRequest = requests.First();
// 获取客户简称
string customerCode = "Unknown";
if (firstRequest.CustomerId.HasValue)
{
var customer = await _customerRepository.GetByIdAsync(firstRequest.CustomerId.Value);
if (customer != null)
{
customerCode = customer.CustomerCode;
}
}
// 计算到货订单数量
int arrivalOrderCount = requests.Count;
// 计算无标签数据数量
int noLabelDataCount = requests.Count(req => string.IsNullOrEmpty(req.Label));
// 计算已有标签订单数
int labeledOrderCount = requests.Count(req => !string.IsNullOrEmpty(req.Label));
// 计算已有标签率
string labelRate = "0%";
if (arrivalOrderCount > 0)
{
double rate = (double)labeledOrderCount / arrivalOrderCount * 100;
labelRate = $"{rate:F2}%";
}
// 计算换单完成数量(根据扫描记录)
int replaceCompletedCount = 0;
try
{
// 收集所有需要查询的中性面单单号
var waybillNumbers = requests.Select(req => req.NeutralWaybillNumber).Where(num => !string.IsNullOrEmpty(num)).Distinct().ToList();
if (waybillNumbers.Count > 0)
{
// 一次性批量查询所有扫描记录
var allScans = await _labelScanService.GetScanRecordsByNeutralWaybillNumbersAsync(waybillNumbers);
// 在内存中处理结果
var returnedLabelScans = allScans.Where(scan => scan.Result == ScanResult.ReturnedLabel).Select(scan => scan.NeutralWaybillNumber).ToHashSet();
// 统计换单完成数量
replaceCompletedCount = requests.Count(req => !string.IsNullOrEmpty(req.NeutralWaybillNumber) && returnedLabelScans.Contains(req.NeutralWaybillNumber));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating replace completed count");
// 如果批量查询失败,回退到单条查询
foreach (var req in requests)
{
try
{
var scans = await _labelScanService.GetScanRecordsByNeutralWaybillNumberAsync(req.NeutralWaybillNumber);
if (scans.Any(scan => scan.Result == ScanResult.ReturnedLabel))
{
replaceCompletedCount++;
}
}
catch (Exception innerEx)
{
_logger.LogError(innerEx, "Error querying scan records for waybill: {WaybillNumber}", req.NeutralWaybillNumber);
}
}
}
// 计算未换单完成数量
int replacePendingCount = arrivalOrderCount - replaceCompletedCount;
// 获取到货时间(从到货交接单表中获取)
DateTime? arrivalTime = null;
try
{
// 尝试从到货交接单表中获取到货时间
string handoverNumber = type == "billOfLading" ? firstRequest.BillOfLadingNumber : firstRequest.MasterPackageNumber;
if (!string.IsNullOrEmpty(handoverNumber))
{
var arrivalForms = await _arrivalHandoverFormService.GetArrivalHandoverFormsByHandoverNumberAsync(handoverNumber);
if (arrivalForms != null && arrivalForms.Count > 0)
{
arrivalTime = arrivalForms.Min(form => form.ReceiptTime);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving arrival time from handover forms");
// 如果出错,不显示到货时间
arrivalTime = null;
}
// 创建数据看板DTO
var dto = new DashboardDataDto
{
Key = group.Key,
CustomerCode = customerCode,
ArrivalOrderCount = arrivalOrderCount,
ReplaceCompletedCount = replaceCompletedCount,
ReplacePendingCount = replacePendingCount,
NoLabelDataCount = noLabelDataCount,
LabeledOrderCount = labeledOrderCount,
LabelRate = labelRate,
ArrivalTime = arrivalTime,
BillOfLadingNumber = firstRequest.BillOfLadingNumber,
MasterPackageNumber = firstRequest.MasterPackageNumber
};
dashboardData.Add(dto);
}
_logger.LogInformation("Retrieved {count} dashboard data items", dashboardData.Count);
return dashboardData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving dashboard data");
return new List<DashboardDataDto>();
}
}
```
## 4. 性能优化考虑
### 4.1 索引优化
-`arrival_handover_forms` 表的 `HandoverNumber` 字段添加索引
-`label_replace_requests` 表的 `BillOfLadingNumber``MasterPackageNumber` 字段添加索引
-`label_replace_requests` 表的 `CustomerId` 字段添加索引
### 4.2 查询优化
- 使用批量查询减少数据库访问次数
- 避免在循环中进行数据库查询
- 合理使用内存缓存
### 4.3 数据量控制
- 考虑添加分页功能
- 对于历史数据,可以考虑归档策略
## 5. 测试计划
### 5.1 功能测试
- 测试提单号预报功能
- 测试大箱号预报功能
- 测试各种过滤条件
### 5.2 性能测试
- 测试大数据量下的查询性能
- 测试响应时间
### 5.3 数据准确性测试
- 使用测试数据验证统计结果的准确性
- 与手动计算结果进行比较
## 6. 注意事项
- 处理一个到货交接单对应多个提单号或主包号的情况
- 处理标签替换请求中没有提单号或主包号的情况
- 确保与前端API接口兼容
- 保持代码的可读性和可维护性

View File

@@ -0,0 +1,72 @@
# 数据看板查询逻辑重构 - 产品需求文档
## Overview
- **Summary**: 重构 batch_query.html 中的数据看板查询逻辑,从到货交接单的数据开始,以到货交接单的交接单号去匹配提单号和主包号,从而统计到货情况
- **Purpose**: 优化数据看板的查询逻辑,使其更加准确地反映到货情况
- **Target Users**: 物流操作人员、数据分析师
## Goals
- 从到货交接单的数据开始,以交接单号匹配提单号和主包号
- 保持现有的两个模块:提单号预报和大箱号预报
- 确保统计数据的准确性
- 优化查询性能
## Non-Goals (Out of Scope)
- 不修改现有的UI结构
- 不修改其他模块的功能
- 不涉及数据库结构变更
## Background & Context
当前数据看板的查询逻辑是从标签替换请求表开始,直接按提单号或大箱号分组统计。这种方式可能会导致统计结果不准确,因为它没有考虑到到货交接单的实际情况。
## Functional Requirements
- **FR-1**: 保持现有的两个模块结构(提单号预报和大箱号预报)
- **FR-2**: 修改查询逻辑,从到货交接单的数据开始
- **FR-3**: 以到货交接单的交接单号去匹配提单号和主包号
- **FR-4**: 统计到货订单数量、无标签数据数量、已有标签率等指标
- **FR-5**: 支持按客户ID、日期范围等条件过滤
## Non-Functional Requirements
- **NFR-1**: 性能优化,避免全表扫描
- **NFR-2**: 代码可读性和可维护性
- **NFR-3**: 数据准确性
## Constraints
- **Technical**: 使用现有的API架构
- **Business**: 无特殊业务约束
- **Dependencies**: 依赖 `arrival_handover_forms` 表和 `label_replace_requests`
## Assumptions
- 数据库表结构已正确创建
- 所有必要的索引已添加
- 数据量在合理范围内
## Acceptance Criteria
### AC-1: 基本查询功能
- **Given**: 存在到货交接单记录
- **When**: 执行提单号预报查询
- **Then**: 从到货交接单开始,以交接单号匹配提单号统计
- **Verification**: `programmatic`
### AC-2: 大箱号查询功能
- **Given**: 存在到货交接单记录
- **When**: 执行大箱号预报查询
- **Then**: 从到货交接单开始,以交接单号匹配主包号统计
- **Verification**: `programmatic`
### AC-3: 数据准确性
- **Given**: 存在测试数据
- **When**: 执行查询并与手动计算结果比较
- **Then**: 统计数据与手动计算一致
- **Verification**: `human-judgment`
### AC-4: 性能优化
- **Given**: 数据量较大如10万条记录
- **When**: 执行查询
- **Then**: 查询响应时间在可接受范围内(<5秒
- **Verification**: `programmatic`
## Open Questions
- [ ] 如何处理一个到货交接单对应多个提单号或主包号的情况
- [ ] 是否需要修改后端API还是只需要修改前端逻辑

View File

@@ -0,0 +1,67 @@
# 数据看板查询逻辑重构 - 实现计划
## [x] Task 1: 分析当前查询逻辑
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 分析当前数据看板的查询逻辑
- 了解现有的API接口
- 确定需要修改的部分
- **Acceptance Criteria Addressed**: AC-1, AC-2
- **Test Requirements**:
- `human-judgment` TR-1.1: 理解当前查询逻辑
- `human-judgment` TR-1.2: 识别需要修改的部分
- **Notes**: 重点关注查询逻辑的起点和数据匹配方式
## [x] Task 2: 设计新的查询逻辑
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 设计从到货交接单开始的查询逻辑
- 确定如何以交接单号匹配提单号和主包号
- 设计统计计算方法
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3
- **Test Requirements**:
- `human-judgment` TR-2.1: 验证查询逻辑设计
- `human-judgment` TR-2.2: 确保统计方法正确
- **Notes**: 考虑如何处理一个交接单对应多个提单号或主包号的情况
## [x] Task 3: 实现后端API修改
- **Priority**: P0
- **Depends On**: Task 2
- **Description**:
- 修改后端API实现从到货交接单开始的查询逻辑
- 确保API接口与前端兼容
- 优化查询性能
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-4
- **Test Requirements**:
- `programmatic` TR-3.1: 验证API功能
- `programmatic` TR-3.2: 测试性能
- **Notes**: 考虑使用JOIN和索引优化查询
## [x] Task 4: 测试修改后的功能
- **Priority**: P1
- **Depends On**: Task 3
- **Description**:
- 测试提单号预报功能
- 测试大箱号预报功能
- 验证统计数据的准确性
- 测试性能
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3, AC-4
- **Test Requirements**:
- `programmatic` TR-4.1: 功能测试
- `human-judgment` TR-4.2: 数据准确性验证
- **Notes**: 使用测试数据验证功能
## [x] Task 5: 编写文档
- **Priority**: P1
- **Depends On**: Task 4
- **Description**:
- 编写修改说明文档
- 记录查询逻辑的变更
- 提供使用说明
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3, AC-4
- **Test Requirements**:
- `human-judgment` TR-5.1: 文档完整性
- `human-judgment` TR-5.2: 文档可读性
- **Notes**: 确保文档包含所有必要的信息

View File

@@ -0,0 +1,746 @@
# 到货交接单和出货交接单模块规格说明
## 1. 需求概述
在系统中增加两个新模块:到货交接单和出货交接单,用于记录和管理货物的交接过程。
### 1.1 到货交接单
- **交接单号**:唯一标识
- **头程物流商送达时间**:物流商送达时间
- **收货时间**:实际收货时间
- **POD**:图片链接,多张用逗号隔开
- **备注**:交接备注信息
- **创建人**:创建该交接单的用户
- **创建时间**:交接单创建时间
- **修改时间**:交接单最后修改时间
### 1.2 出货交接单
- **交接单号**:唯一标识,格式如 BOL-GOFO-202
- **大包数**:大包数量
- **小包数**:小包数量
- **渠道**:物流渠道
- **交货时间**:实际交货时间
- **POD**:图片链接,多张用逗号隔开
- **备注**:交接备注信息
- **创建人**:创建该交接单的用户
- **创建时间**:交接单创建时间
- **修改时间**:交接单最后修改时间
## 2. 技术方案
### 2.1 数据库设计
#### 2.1.1 到货交接单表 (`arrival_handover_forms`)
| 字段名 | 数据类型 | 约束 | 描述 |
|--------|----------|------|------|
| `Id` | `INT` | `PRIMARY KEY, AUTO_INCREMENT` | 主键ID |
| `HandoverNumber` | `VARCHAR(100)` | `NOT NULL, UNIQUE` | 交接单号 |
| `LogisticsProviderArrivalTime` | `DATETIME` | `NULL` | 头程物流商送达时间 |
| `ReceiptTime` | `DATETIME` | `NULL` | 收货时间 |
| `POD` | `TEXT` | `NULL` | 图片链接,多张用逗号隔开 |
| `Remarks` | `TEXT` | `NULL` | 备注 |
| `Creator` | `VARCHAR(50)` | `NOT NULL` | 创建人 |
| `CreatedAt` | `DATETIME` | `NOT NULL` | 创建时间 |
| `UpdatedAt` | `DATETIME` | `NOT NULL` | 修改时间 |
#### 2.1.2 出货交接单表 (`shipping_handover_forms`)
| 字段名 | 数据类型 | 约束 | 描述 |
|--------|----------|------|------|
| `Id` | `INT` | `PRIMARY KEY, AUTO_INCREMENT` | 主键ID |
| `HandoverNumber` | `VARCHAR(100)` | `NOT NULL, UNIQUE` | 交接单号 |
| `BigBagCount` | `INT` | `NOT NULL` | 大包数 |
| `SmallBagCount` | `INT` | `NOT NULL` | 小包数 |
| `Channel` | `VARCHAR(100)` | `NOT NULL` | 渠道 |
| `DeliveryTime` | `DATETIME` | `NULL` | 交货时间 |
| `POD` | `TEXT` | `NULL` | 图片链接,多张用逗号隔开 |
| `Remarks` | `TEXT` | `NULL` | 备注 |
| `Creator` | `VARCHAR(50)` | `NOT NULL` | 创建人 |
| `CreatedAt` | `DATETIME` | `NOT NULL` | 创建时间 |
| `UpdatedAt` | `DATETIME` | `NOT NULL` | 修改时间 |
### 2.2 模型设计
#### 2.2.1 到货交接单模型 (`ArrivalHandoverFormEntity`)
```csharp
using System;
using SqlSugar;
namespace MDL.Models
{
[SugarTable("arrival_handover_forms")]
public class ArrivalHandoverFormEntity
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
[SugarColumn(Length = 100, IsNullable = false, IsUnique = true)]
public string HandoverNumber { get; set; }
[SugarColumn(IsNullable = true)]
public DateTime? LogisticsProviderArrivalTime { get; set; }
[SugarColumn(IsNullable = true)]
public DateTime? ReceiptTime { get; set; }
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
public string POD { get; set; }
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
public string Remarks { get; set; }
[SugarColumn(Length = 50, IsNullable = false)]
public string Creator { get; set; }
[SugarColumn(IsNullable = false)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[SugarColumn(IsNullable = false)]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}
```
#### 2.2.2 出货交接单模型 (`ShippingHandoverFormEntity`)
```csharp
using System;
using SqlSugar;
namespace MDL.Models
{
[SugarTable("shipping_handover_forms")]
public class ShippingHandoverFormEntity
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
[SugarColumn(Length = 100, IsNullable = false, IsUnique = true)]
public string HandoverNumber { get; set; }
[SugarColumn(IsNullable = false)]
public int BigBagCount { get; set; }
[SugarColumn(IsNullable = false)]
public int SmallBagCount { get; set; }
[SugarColumn(Length = 100, IsNullable = false)]
public string Channel { get; set; }
[SugarColumn(IsNullable = true)]
public DateTime? DeliveryTime { get; set; }
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
public string POD { get; set; }
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
public string Remarks { get; set; }
[SugarColumn(Length = 50, IsNullable = false)]
public string Creator { get; set; }
[SugarColumn(IsNullable = false)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[SugarColumn(IsNullable = false)]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}
```
### 2.3 仓库设计
#### 2.3.1 到货交接单仓库接口 (`IArrivalHandoverFormRepository`)
```csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using MDL.Models;
namespace DAL.Interfaces
{
public interface IArrivalHandoverFormRepository
{
Task<int> InsertAsync(ArrivalHandoverFormEntity form);
Task<ArrivalHandoverFormEntity> GetByIdAsync(int id);
Task<ArrivalHandoverFormEntity> GetByHandoverNumberAsync(string handoverNumber);
Task<List<ArrivalHandoverFormEntity>> GetAllAsync();
Task<int> UpdateAsync(ArrivalHandoverFormEntity form);
Task<int> DeleteAsync(int id);
Task<bool> ExistsByHandoverNumberAsync(string handoverNumber);
Task<(List<ArrivalHandoverFormEntity> Forms, int TotalCount)> GetArrivalHandoverFormsBatchAsync(
int page,
int pageSize,
string sortBy,
string sortOrder,
string handoverNumber,
string creator);
}
}
```
#### 2.3.2 出货交接单仓库接口 (`IShippingHandoverFormRepository`)
```csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using MDL.Models;
namespace DAL.Interfaces
{
public interface IShippingHandoverFormRepository
{
Task<int> InsertAsync(ShippingHandoverFormEntity form);
Task<ShippingHandoverFormEntity> GetByIdAsync(int id);
Task<ShippingHandoverFormEntity> GetByHandoverNumberAsync(string handoverNumber);
Task<List<ShippingHandoverFormEntity>> GetAllAsync();
Task<int> UpdateAsync(ShippingHandoverFormEntity form);
Task<int> DeleteAsync(int id);
Task<bool> ExistsByHandoverNumberAsync(string handoverNumber);
Task<(List<ShippingHandoverFormEntity> Forms, int TotalCount)> GetShippingHandoverFormsBatchAsync(
int page,
int pageSize,
string sortBy,
string sortOrder,
string handoverNumber,
string channel,
string creator);
}
}
```
### 2.4 服务设计
#### 2.4.1 到货交接单服务接口 (`IArrivalHandoverFormService`)
```csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using MDL.Models;
namespace BLL.Interfaces
{
public interface IArrivalHandoverFormService
{
Task<int> CreateArrivalHandoverFormAsync(ArrivalHandoverFormEntity form);
Task<ArrivalHandoverFormEntity> GetArrivalHandoverFormByIdAsync(int id);
Task<ArrivalHandoverFormEntity> GetArrivalHandoverFormByNumberAsync(string handoverNumber);
Task<List<ArrivalHandoverFormEntity>> GetAllArrivalHandoverFormsAsync();
Task<int> UpdateArrivalHandoverFormAsync(ArrivalHandoverFormEntity form);
Task<int> DeleteArrivalHandoverFormAsync(int id);
Task<string> GenerateArrivalHandoverNumberAsync();
Task<(List<ArrivalHandoverFormEntity> Forms, int TotalCount)> GetArrivalHandoverFormsBatchAsync(
int page,
int pageSize,
string sortBy,
string sortOrder,
string handoverNumber,
string creator);
}
}
```
#### 2.4.2 出货交接单服务接口 (`IShippingHandoverFormService`)
```csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using MDL.Models;
namespace BLL.Interfaces
{
public interface IShippingHandoverFormService
{
Task<int> CreateShippingHandoverFormAsync(ShippingHandoverFormEntity form);
Task<ShippingHandoverFormEntity> GetShippingHandoverFormByIdAsync(int id);
Task<ShippingHandoverFormEntity> GetShippingHandoverFormByNumberAsync(string handoverNumber);
Task<List<ShippingHandoverFormEntity>> GetAllShippingHandoverFormsAsync();
Task<int> UpdateShippingHandoverFormAsync(ShippingHandoverFormEntity form);
Task<int> DeleteShippingHandoverFormAsync(int id);
Task<string> GenerateShippingHandoverNumberAsync();
Task<(List<ShippingHandoverFormEntity> Forms, int TotalCount)> GetShippingHandoverFormsBatchAsync(
int page,
int pageSize,
string sortBy,
string sortOrder,
string handoverNumber,
string channel,
string creator);
}
}
```
### 2.5 控制器设计
#### 2.5.1 到货交接单控制器 (`ArrivalHandoverFormController`)
```csharp
using BLL.Interfaces;
using MDL.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace CONTROLLER.Controllers
{
[Route("api/arrival-handover")]
[ApiController]
public class ArrivalHandoverFormController : ControllerBase
{
private readonly IArrivalHandoverFormService _arrivalHandoverFormService;
public ArrivalHandoverFormController(IArrivalHandoverFormService arrivalHandoverFormService)
{
_arrivalHandoverFormService = arrivalHandoverFormService;
}
[HttpPost("create")]
public async Task<IActionResult> CreateArrivalHandoverForm([FromBody] ArrivalHandoverFormEntity form)
{
try
{
if (string.IsNullOrEmpty(form.HandoverNumber))
{
form.HandoverNumber = await _arrivalHandoverFormService.GenerateArrivalHandoverNumberAsync();
}
form.CreatedAt = DateTime.UtcNow;
form.UpdatedAt = DateTime.UtcNow;
var result = await _arrivalHandoverFormService.CreateArrivalHandoverFormAsync(form);
return Ok(new
{
code = 0,
message = "success",
data = result
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("get/{id}")]
public async Task<IActionResult> GetArrivalHandoverFormById(int id)
{
try
{
var form = await _arrivalHandoverFormService.GetArrivalHandoverFormByIdAsync(id);
return Ok(new
{
code = 0,
message = "success",
data = form
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("get-by-number/{handoverNumber}")]
public async Task<IActionResult> GetArrivalHandoverFormByNumber(string handoverNumber)
{
try
{
var form = await _arrivalHandoverFormService.GetArrivalHandoverFormByNumberAsync(handoverNumber);
return Ok(new
{
code = 0,
message = "success",
data = form
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("list")]
public async Task<IActionResult> GetArrivalHandoverForms(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string sortBy = "CreatedAt",
[FromQuery] string sortOrder = "desc",
[FromQuery] string handoverNumber = "",
[FromQuery] string creator = "")
{
try
{
var (forms, totalCount) = await _arrivalHandoverFormService.GetArrivalHandoverFormsBatchAsync(
page, pageSize, sortBy, sortOrder, handoverNumber, creator);
return Ok(new
{
code = 0,
message = "success",
data = new
{
forms,
totalCount
}
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpPut("update")]
public async Task<IActionResult> UpdateArrivalHandoverForm([FromBody] ArrivalHandoverFormEntity form)
{
try
{
form.UpdatedAt = DateTime.UtcNow;
var result = await _arrivalHandoverFormService.UpdateArrivalHandoverFormAsync(form);
return Ok(new
{
code = 0,
message = "success",
data = result
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpDelete("delete/{id}")]
public async Task<IActionResult> DeleteArrivalHandoverForm(int id)
{
try
{
var result = await _arrivalHandoverFormService.DeleteArrivalHandoverFormAsync(id);
return Ok(new
{
code = 0,
message = "success",
data = result
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("generate-number")]
public async Task<IActionResult> GenerateArrivalHandoverNumber()
{
try
{
var number = await _arrivalHandoverFormService.GenerateArrivalHandoverNumberAsync();
return Ok(new
{
code = 0,
message = "success",
data = number
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
}
}
```
#### 2.5.2 出货交接单控制器 (`ShippingHandoverFormController`)
```csharp
using BLL.Interfaces;
using MDL.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace CONTROLLER.Controllers
{
[Route("api/shipping-handover")]
[ApiController]
public class ShippingHandoverFormController : ControllerBase
{
private readonly IShippingHandoverFormService _shippingHandoverFormService;
public ShippingHandoverFormController(IShippingHandoverFormService shippingHandoverFormService)
{
_shippingHandoverFormService = shippingHandoverFormService;
}
[HttpPost("create")]
public async Task<IActionResult> CreateShippingHandoverForm([FromBody] ShippingHandoverFormEntity form)
{
try
{
if (string.IsNullOrEmpty(form.HandoverNumber))
{
form.HandoverNumber = await _shippingHandoverFormService.GenerateShippingHandoverNumberAsync();
}
form.CreatedAt = DateTime.UtcNow;
form.UpdatedAt = DateTime.UtcNow;
var result = await _shippingHandoverFormService.CreateShippingHandoverFormAsync(form);
return Ok(new
{
code = 0,
message = "success",
data = result
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("get/{id}")]
public async Task<IActionResult> GetShippingHandoverFormById(int id)
{
try
{
var form = await _shippingHandoverFormService.GetShippingHandoverFormByIdAsync(id);
return Ok(new
{
code = 0,
message = "success",
data = form
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("get-by-number/{handoverNumber}")]
public async Task<IActionResult> GetShippingHandoverFormByNumber(string handoverNumber)
{
try
{
var form = await _shippingHandoverFormService.GetShippingHandoverFormByNumberAsync(handoverNumber);
return Ok(new
{
code = 0,
message = "success",
data = form
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("list")]
public async Task<IActionResult> GetShippingHandoverForms(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string sortBy = "CreatedAt",
[FromQuery] string sortOrder = "desc",
[FromQuery] string handoverNumber = "",
[FromQuery] string channel = "",
[FromQuery] string creator = "")
{
try
{
var (forms, totalCount) = await _shippingHandoverFormService.GetShippingHandoverFormsBatchAsync(
page, pageSize, sortBy, sortOrder, handoverNumber, channel, creator);
return Ok(new
{
code = 0,
message = "success",
data = new
{
forms,
totalCount
}
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpPut("update")]
public async Task<IActionResult> UpdateShippingHandoverForm([FromBody] ShippingHandoverFormEntity form)
{
try
{
form.UpdatedAt = DateTime.UtcNow;
var result = await _shippingHandoverFormService.UpdateShippingHandoverFormAsync(form);
return Ok(new
{
code = 0,
message = "success",
data = result
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpDelete("delete/{id}")]
public async Task<IActionResult> DeleteShippingHandoverForm(int id)
{
try
{
var result = await _shippingHandoverFormService.DeleteShippingHandoverFormAsync(id);
return Ok(new
{
code = 0,
message = "success",
data = result
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
[HttpGet("generate-number")]
public async Task<IActionResult> GenerateShippingHandoverNumber()
{
try
{
var number = await _shippingHandoverFormService.GenerateShippingHandoverNumberAsync();
return Ok(new
{
code = 0,
message = "success",
data = number
});
}
catch (Exception ex)
{
return Ok(new
{
code = 9999,
message = ex.Message
});
}
}
}
}
```
## 3. 数据库SQL脚本
### 3.1 到货交接单表创建脚本
```sql
CREATE TABLE IF NOT EXISTS `arrival_handover_forms` (
`Id` INT NOT NULL AUTO_INCREMENT,
`HandoverNumber` VARCHAR(100) NOT NULL,
`LogisticsProviderArrivalTime` DATETIME NULL,
`ReceiptTime` DATETIME NULL,
`POD` TEXT NULL,
`Remarks` TEXT NULL,
`Creator` VARCHAR(50) NOT NULL,
`CreatedAt` DATETIME NOT NULL,
`UpdatedAt` DATETIME NOT NULL,
PRIMARY KEY (`Id`),
UNIQUE INDEX `UQ_HandoverNumber` (`HandoverNumber`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### 3.2 出货交接单表创建脚本
```sql
CREATE TABLE IF NOT EXISTS `shipping_handover_forms` (
`Id` INT NOT NULL AUTO_INCREMENT,
`HandoverNumber` VARCHAR(100) NOT NULL,
`BigBagCount` INT NOT NULL,
`SmallBagCount` INT NOT NULL,
`Channel` VARCHAR(100) NOT NULL,
`DeliveryTime` DATETIME NULL,
`POD` TEXT NULL,
`Remarks` TEXT NULL,
`Creator` VARCHAR(50) NOT NULL,
`CreatedAt` DATETIME NOT NULL,
`UpdatedAt` DATETIME NOT NULL,
PRIMARY KEY (`Id`),
UNIQUE INDEX `UQ_HandoverNumber` (`HandoverNumber`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
## 4. 实现计划
1. **创建模型类**在MDL项目中创建ArrivalHandoverFormEntity和ShippingHandoverFormEntity
2. **创建仓库接口**在DAL项目中创建IArrivalHandoverFormRepository和IShippingHandoverFormRepository
3. **实现仓库类**在DAL项目中实现ArrivalHandoverFormRepository和ShippingHandoverFormRepository
4. **创建服务接口**在BLL项目中创建IArrivalHandoverFormService和IShippingHandoverFormService
5. **实现服务类**在BLL项目中实现ArrivalHandoverFormService和ShippingHandoverFormService
6. **创建控制器**在CONTROLLER项目中创建ArrivalHandoverFormController和ShippingHandoverFormController
7. **更新依赖注入**在Program.cs中注册新的服务和仓库
8. **测试API**验证所有CRUD操作是否正常工作
## 5. 验收标准
1. 数据库表结构正确创建
2. 所有API接口正常工作
3. 交接单号生成规则正确
4. POD字段支持多个图片链接用逗号隔开
5. 分页查询功能正常
6. 所有CRUD操作返回正确的响应格式

View File

@@ -0,0 +1,38 @@
# 标签扫描记录功能增强 - 验证清单
## 功能验证
* [x] 验证 LabelController.cs 的 GetLabelScanBatch 方法已正确添加 startCreatedAt 和 endCreatedAt 参数
* [x] 验证 ILabelScanService 接口的 GetLabelScanRecordsByPageAsync 方法已正确更新参数签名
* [x] 验证 LabelScanService.cs 的实现已正确传递时间参数到仓储层
* [x] 验证 ILabelScanRepository 接口的 GetByPageAsync 方法已正确更新参数签名
* [x] 验证 LabelScanRepository.cs 中已正确实现时间筛选逻辑
* [x] 验证 batch\_query.html 中前端 JavaScript 代码已正确发送时间参数
* [x] 实际测试时间筛选功能,输入开始和结束时间,验证查询结果正确
## 界面文字验证
* [x] 验证标签页按钮已从"标签扫描记录"更新为"换单扫描记录"
* [x] 验证卡片标题已从"标签扫描记录查询"更新为"换单扫描记录查询"
* [x] 检查其他可能的相关文字是否也需要更新
## 页面美观度和用户体验验证
* [x] 验证页面配色和样式得到了优化
* [x] 验证加载过程有更好的视觉反馈
* [x] 验证表格的视觉效果更加美观
* [x] 验证整体布局和间距更加舒适
* [x] 在不同浏览器和屏幕尺寸下测试页面显示效果

View File

@@ -0,0 +1,69 @@
# 标签扫描记录功能增强 - 产品需求文档
## Overview
- **Summary**: 修复批量查询界面标签扫描记录的时间筛选功能,将"标签扫描记录"重命名为"换单扫描记录",并优化页面用户体验
- **Purpose**: 解决标签扫描记录查询时创建时间范围筛选无效的问题,改善界面文字描述,提升用户操作的舒适性和美观度
- **Target Users**: 使用批量查询界面的运营人员、管理人员和客服人员
## Goals
1. 修复标签扫描记录查询中的创建时间开始和结束筛选无效问题
2. 将界面中的"标签扫描记录"相关文字更新为"换单扫描记录"
3. 优化页面的视觉设计和用户体验,使数据加载更舒适
## Non-Goals (Out of Scope)
- 修改核心业务逻辑
- 添加新的数据筛选条件(除时间筛选外)
- 修改标签生成和替换的核心功能
## Background & Context
- 目前标签替换请求的时间筛选功能已修复
- 标签扫描记录查询功能类似,但时间筛选存在同样问题
- 用户反馈"标签扫描记录"的名称不够直观,希望更准确地反映业务含义
- 页面整体视觉可以进一步优化,提升用户体验
## Functional Requirements
- **FR-1**: 修复标签扫描记录查询接口的时间筛选功能,支持按创建时间开始和结束范围筛选
- **FR-2**: 将界面中所有"标签扫描记录"相关文字更新为"换单扫描记录"
- **FR-3**: 优化页面布局、配色和加载效果,提升用户体验
## Non-Functional Requirements
- **NFR-1**: 查询性能保持不变或有所提升
- **NFR-2**: 页面在主流浏览器Chrome、Firefox、Safari、Edge中正常显示
- **NFR-3**: 响应式设计,支持不同屏幕尺寸
## Constraints
- **Technical**: 使用现有的技术栈ASP.NET Core, SQLSugar, Bootstrap, Vanilla JS
- **Business**: 修改不能影响现有功能的稳定性
- **Dependencies**: 依赖现有的标签扫描记录相关数据结构和服务
## Assumptions
1. 用户已具备使用现有批量查询界面的基本操作知识
2. 后端数据结构和存储方式保持不变
3. 现有代码库的架构和设计模式保持一致
## Acceptance Criteria
### AC-1: 时间筛选修复
- **Given**: 用户在换单扫描记录查询界面输入了创建时间开始和/或结束时间
- **When**: 用户点击查询按钮
- **Then**: 查询结果只包含创建时间在指定范围内的记录
- **Verification**: programmatic
- **Notes**: 验证从前端到后端到数据库的完整流程
### AC-2: 文字更新
- **Given**: 用户访问批量查询界面
- **When**: 用户切换到换单扫描记录标签
- **Then**: 所有相关的界面元素(标签页标题、卡片标题、表单元素、提示文字等)都显示为"换单扫描记录"
- **Verification**: human-judgment
- **Notes**: 检查所有显示的文字内容
### AC-3: 页面美观度提升
- **Given**: 用户访问批量查询界面
- **When**: 用户使用任意查询功能
- **Then**: 页面视觉效果更加美观,数据加载过程更舒适
- **Verification**: human-judgment
- **Notes**: 评估整体视觉设计和用户体验的改进
## Open Questions
-

View File

@@ -0,0 +1,73 @@
# 标签扫描记录功能增强 - 实施计划
## [ ] Task 1: 修复 LabelController.cs 中的 GetLabelScanBatch 接口
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在 GetLabelScanBatch 方法中添加 startCreatedAt 和 endCreatedAt 时间参数
- 调用服务时传递这些时间参数
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- programmatic: 验证接口新增参数正确接收和传递
- **Notes**: 参考已修复的 GetLabelReplaceBatch 接口实现
## [ ] Task 2: 更新 ILabelScanService 接口定义
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 修改 GetLabelScanRecordsByPageAsync 方法签名,添加 startCreatedAt 和 endCreatedAt 参数
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- programmatic: 验证接口定义正确更新
## [ ] Task 3: 更新 LabelScanService.cs 实现
- **Priority**: P0
- **Depends On**: Task 2
- **Description**:
- 更新 GetLabelScanRecordsByPageAsync 方法实现,新增时间参数并传递给仓储层
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- programmatic: 验证服务正确传递参数
## [ ] Task 4: 更新 ILabelScanRepository 接口定义
- **Priority**: P0
- **Depends On**: Task 3
- **Description**:
- 修改 GetByPageAsync 方法签名,添加 startCreatedAt 和 endCreatedAt 参数
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- programmatic: 验证仓储接口定义正确更新
## [ ] Task 5: 实现 LabelScanRepository.cs 中的时间筛选逻辑
- **Priority**: P0
- **Depends On**: Task 4
- **Description**:
- 在 GetByPageAsync 方法中添加时间参数解析和筛选逻辑
- 参考 LabelReplaceRepository.cs 的实现方式
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- programmatic: 验证时间筛选逻辑正确工作
## [ ] Task 6: 更新 batch_query.html 中的界面文字
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 将标签页按钮从"标签扫描记录"修改为"换单扫描记录"
- 将卡片标题从"标签扫描记录查询"修改为"换单扫描记录查询"
- 更新其他相关显示文字
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- human-judgment: 检查所有界面文字正确更新
## [ ] Task 7: 优化页面美观度和用户体验
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 优化页面的配色和样式
- 添加加载动画和更好的用户反馈
- 改进表格的视觉效果
- 优化整体布局和间距
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- human-judgment: 评估页面美观度和用户体验的改进

View File

@@ -0,0 +1,12 @@
# 订单导入功能模块 - 文件夹上传功能 - 验证清单
- [ ] 面单文件上传和FTP上传检查标签页已从页面中移除
- [ ] 相关的JavaScript函数和事件处理已被移除
- [ ] 订单导入标签页中的文件选择控件支持选择文件夹
- [ ] 系统能够正确处理文件夹选择事件
- [ ] 系统能够上传文件夹中的所有PDF文件
- [ ] 上传过程显示进度反馈
- [ ] 上传失败时显示错误信息
- [ ] 只上传PDF文件忽略其他类型的文件
- [ ] 保持现有界面的简洁性
- [ ] 不影响其他功能模块

View File

@@ -0,0 +1,64 @@
# 订单导入功能模块 - 文件夹上传功能 - 产品需求文档
## 概述
- **摘要**修改订单导入功能去掉面单文件上传和FTP上传检查模块将面单文件选择改为选择文件夹并上传文件夹中的所有PDF文件。
- **目的**:简化用户操作流程,减少用户输入错误,确保面单文件能够正确上传。
- **目标用户**:使用订单导入功能的操作人员,需要上传面单文件的用户。
## 目标
- 去掉面单文件上传和FTP上传检查模块
- 修改订单导入功能中的面单文件选择为选择文件夹
- 实现上传文件夹中的所有PDF文件
## 非目标(超出范围)
- 不修改现有的订单导入核心逻辑
- 不改变现有的FTP服务器配置
- 不添加新的用户界面元素,保持界面简洁
## 背景与上下文
- 现有系统已经实现了订单导入功能,可以通过 Excel 文件导入订单数据
- 现有系统已经实现了面单文件上传功能,但需要用户逐个选择文件
- 用户希望能够更方便地批量上传面单文件,通过选择文件夹的方式
## 功能需求
- **FR-1**去掉面单文件上传和FTP上传检查模块
- **FR-2**:修改订单导入功能中的面单文件选择为选择文件夹
- **FR-3**实现上传文件夹中的所有PDF文件
## 非功能需求
- **NFR-1**:保持现有界面的简洁性,不增加额外的用户输入字段
- **NFR-2**:上传过程应该有明确的进度反馈
- **NFR-3**:错误处理应该清晰明确,方便用户理解和解决问题
## 约束
- **技术**:基于现有的 ASP.NET Core Web API 和文件上传服务
- **依赖**:依赖现有的文件上传服务
## 假设
- 文件夹中只包含需要上传的PDF文件
- 用户能够正确选择包含面单文件的文件夹
## 验收标准
### AC-1去掉面单文件上传和FTP上传检查模块
- **给定**:用户打开批量查询页面
- **当**:用户查看页面标签页
- **然后**页面中不再显示面单文件上传和FTP上传检查标签页
- **验证**`human-judgment`
### AC-2订单导入功能支持选择文件夹
- **给定**:用户进入订单导入标签页
- **当**:用户点击面单文件选择按钮
- **然后**系统弹出文件夹选择对话框允许用户选择包含PDF文件的文件夹
- **验证**`human-judgment`
### AC-3上传文件夹中的所有PDF文件
- **给定**用户选择了包含PDF文件的文件夹
- **当**:用户点击"导入订单"按钮
- **然后**系统自动上传文件夹中的所有PDF文件
- **验证**`programmatic`
- **备注**:上传过程应该有进度反馈
## 开放问题
- [ ] 如何处理文件夹中包含非PDF文件的情况
- [ ] 上传失败时的错误处理机制是什么?

View File

@@ -0,0 +1,39 @@
# 订单导入功能模块 - 文件夹上传功能 - 实现计划
## [/] 任务 1去掉面单文件上传和FTP上传检查模块
- **优先级**P0
- **依赖**:无
- **描述**
- 从批量查询页面中移除面单文件上传和FTP上传检查标签页
- 移除相关的JavaScript函数和事件处理
- **验收标准**AC-1
- **测试要求**
- `human-judgment` TR-1.1: 页面中不再显示面单文件上传和FTP上传检查标签页
- `human-judgment` TR-1.2: 相关的JavaScript函数和事件处理已被移除
- **备注**:确保不影响其他功能模块
## [ ] 任务 2修改订单导入功能中的面单文件选择为选择文件夹
- **优先级**P0
- **依赖**:任务 1
- **描述**
- 修改订单导入标签页中的文件选择控件,支持选择文件夹
- 更新相关的JavaScript代码处理文件夹选择事件
- **验收标准**AC-2
- **测试要求**
- `human-judgment` TR-2.1: 订单导入标签页中的文件选择控件支持选择文件夹
- `programmatic` TR-2.2: 系统能够正确处理文件夹选择事件
- **备注**使用HTML5的directory属性实现文件夹选择功能
## [ ] 任务 3实现上传文件夹中的所有PDF文件
- **优先级**P0
- **依赖**:任务 2
- **描述**
- 修改订单导入逻辑在导入订单后自动上传所选文件夹中的所有PDF文件
- 实现上传进度的显示
- 处理上传过程中的错误
- **验收标准**AC-3
- **测试要求**
- `programmatic` TR-3.1: 系统能够上传文件夹中的所有PDF文件
- `human-judgment` TR-3.2: 上传过程显示进度反馈
- `programmatic` TR-3.3: 上传失败时显示错误信息
- **备注**只上传PDF文件忽略其他类型的文件

View File

@@ -0,0 +1,18 @@
# 订单导入功能模块 - FTP 集成功能 - 验证清单
- [ ] 前端订单导入表单是否添加了面单文件选择功能
- [ ] 面单文件选择控件是否允许选择多个文件
- [ ] 界面是否保持简洁,没有添加额外的目录输入字段
- [ ] 订单导入后是否自动上传面单文件
- [ ] 上传过程是否显示进度反馈
- [ ] 是否使用预设的上传目录路径,无需用户输入
- [ ] 文件上传完成后是否自动检查 FTP 上传状态
- [ ] 导入结果中是否显示上传和检查的状态
- [ ] 后端 `OrderImport` 端点是否支持接收面单文件
- [ ] 后端是否实现了文件上传到 FTP 服务器的逻辑
- [ ] 后端是否在文件上传完成后检查 FTP 上传状态
- [ ] 后端响应中是否包含上传和检查的状态信息
- [ ] 完整流程测试是否通过
- [ ] 用户体验是否流畅,反馈是否清晰
- [ ] 错误处理是否清晰明确
- [ ] 不同文件类型和大小的测试是否通过

View File

@@ -0,0 +1,78 @@
# 订单导入功能模块 - FTP 集成功能 - 产品需求文档
## 概述
- **摘要**:在订单导入功能模块中整合面单文件上传功能和 FTP 上传检查功能,无需提供上传目录选择参数,系统默认使用预设的上传目录路径,实现文件自动上传至指定位置,并完成 FTP 上传状态的自动检查与反馈。
- **目的**:简化用户操作流程,减少用户输入错误,确保面单文件能够正确上传到指定的 FTP 位置,并及时反馈上传状态。
- **目标用户**:使用订单导入功能的操作人员,需要上传面单文件到 FTP 服务器的用户。
## 目标
- 整合面单文件上传功能到订单导入模块
- 整合 FTP 上传检查功能到订单导入模块
- 系统默认使用预设的上传目录路径,无需用户输入
- 实现文件自动上传至指定位置
- 完成 FTP 上传状态的自动检查与反馈
## 非目标(超出范围)
- 不修改现有的订单导入核心逻辑
- 不改变现有的 FTP 服务器配置
- 不添加新的用户界面元素,保持界面简洁
## 背景与上下文
- 现有系统已经实现了订单导入功能,可以通过 Excel 文件导入订单数据
- 现有系统已经实现了面单文件上传功能,可以上传文件到 FTP 服务器
- 现有系统已经实现了 FTP 上传检查功能,可以检查文件是否存在于 FTP 服务器
- 用户当前需要在不同的标签页之间切换来完成订单导入、文件上传和上传检查,操作繁琐
## 功能需求
- **FR-1**:在订单导入功能模块中整合面单文件上传功能
- **FR-2**:在订单导入功能模块中整合 FTP 上传检查功能
- **FR-3**:系统默认使用预设的上传目录路径,无需用户输入
- **FR-4**:实现文件自动上传至指定位置
- **FR-5**:完成 FTP 上传状态的自动检查与反馈
## 非功能需求
- **NFR-1**:保持现有界面的简洁性,不增加额外的用户输入字段
- **NFR-2**:上传和检查过程应该有明确的进度反馈
- **NFR-3**:错误处理应该清晰明确,方便用户理解和解决问题
## 约束
- **技术**:基于现有的 ASP.NET Core Web API 和 FTP 上传服务
- **依赖**:依赖现有的 `FtpUploadService` 和相关的 API 端点
## 假设
- 预设的上传目录路径是固定的,不需要用户配置
- 面单文件的命名规则与订单数据中的中性面单单号或尾程单号一致
## 验收标准
### AC-1订单导入时自动上传面单文件
- **给定**:用户选择了包含订单数据的 Excel 文件和对应的面单文件
- **当**:用户点击 "导入订单" 按钮
- **然后**:系统自动将面单文件上传到预设的 FTP 目录
- **验证**`programmatic`
- **备注**:上传过程应该有进度反馈
### AC-2订单导入时自动检查 FTP 上传状态
- **给定**:面单文件已经上传到 FTP 服务器
- **当**:文件上传完成后
- **然后**:系统自动检查文件是否成功上传到 FTP 服务器
- **验证**`programmatic`
- **备注**:检查结果应该在导入结果中显示
### AC-3使用预设的上传目录路径
- **给定**:用户进行订单导入操作
- **当**:系统执行文件上传操作
- **然后**:系统使用预设的上传目录路径,无需用户输入
- **验证**`programmatic`
- **备注**:预设路径应该在代码中配置
### AC-4上传和检查状态的反馈
- **给定**:系统执行文件上传和检查操作
- **当**:操作完成后
- **然后**:系统在导入结果中显示上传和检查的状态
- **验证**`human-judgment`
- **备注**:状态信息应该清晰易读
## 开放问题
- [ ] 预设的上传目录路径具体是什么?
- [ ] 面单文件的命名规则是否与现有系统一致?

View File

@@ -0,0 +1,78 @@
# 订单导入功能模块 - FTP 集成功能 - 实现计划
## [x] 任务 1修改前端订单导入表单添加面单文件选择功能
- **优先级**P0
- **依赖**:无
- **描述**
- 在订单导入标签页中添加面单文件选择控件
- 允许用户选择多个面单文件
- 保持界面简洁,不添加额外的目录输入字段
- **验收标准**AC-1, AC-3
- **测试要求**
- `programmatic` TR-1.1: 表单能够正确选择面单文件
- `human-judgment` TR-1.2: 界面简洁,操作流程顺畅
- **备注**:使用现有的文件选择控件样式,保持与现有界面的一致性
## [x] 任务 2修改前端订单导入逻辑整合 FTP 上传功能
- **优先级**P0
- **依赖**:任务 1
- **描述**
- 修改 `importOrders` 函数,在导入订单后自动上传面单文件
- 使用预设的上传目录路径,无需用户输入
- 实现上传进度的显示
- **验收标准**AC-1, AC-3, AC-4
- **测试要求**
- `programmatic` TR-2.1: 订单导入后自动上传面单文件
- `programmatic` TR-2.2: 上传过程显示进度
- `human-judgment` TR-2.3: 上传状态反馈清晰
- **备注**:使用现有的 FTP 上传 API 端点,保持与现有上传功能的一致性
## [x] 任务 3修改前端订单导入逻辑整合 FTP 上传检查功能
- **优先级**P0
- **依赖**:任务 2
- **描述**
- 修改 `importOrders` 函数,在文件上传完成后自动检查 FTP 上传状态
- 在导入结果中显示上传和检查的状态
- **验收标准**AC-2, AC-4
- **测试要求**
- `programmatic` TR-3.1: 文件上传完成后自动检查 FTP 状态
- `human-judgment` TR-3.2: 检查结果在导入结果中清晰显示
- **备注**:使用现有的 FTP 检查 API 端点,保持与现有检查功能的一致性
## [x] 任务 4修改后端订单导入端点支持面单文件上传
- **优先级**P0
- **依赖**:无
- **描述**
- 修改 `OrderImport` 端点,支持接收面单文件
- 实现文件上传到 FTP 服务器的逻辑
- 使用预设的上传目录路径
- **验收标准**AC-1, AC-3
- **测试要求**
- `programmatic` TR-4.1: 端点能够接收并处理面单文件
- `programmatic` TR-4.2: 文件正确上传到预设的 FTP 目录
- **备注**:复用现有的 `FtpUploadService` 来处理文件上传
## [x] 任务 5修改后端订单导入端点支持 FTP 上传检查
- **优先级**P0
- **依赖**:任务 4
- **描述**
- 修改 `OrderImport` 端点,在文件上传完成后检查 FTP 上传状态
- 在响应中包含上传和检查的状态信息
- **验收标准**AC-2, AC-4
- **测试要求**
- `programmatic` TR-5.1: 端点能够检查文件在 FTP 服务器上的存在性
- `programmatic` TR-5.2: 响应中包含完整的上传和检查状态
- **备注**:复用现有的 `FtpUploadService.CheckFilesExistsAsync` 方法
## [x] 任务 6测试整合后的订单导入功能
- **优先级**P1
- **依赖**:任务 3, 任务 5
- **描述**
- 测试订单导入、面单文件上传和 FTP 上传检查的完整流程
- 验证所有功能正常工作
- 验证错误处理和边界情况
- **验收标准**AC-1, AC-2, AC-3, AC-4
- **测试要求**
- `programmatic` TR-6.1: 完整流程测试通过
- `human-judgment` TR-6.2: 用户体验流畅,反馈清晰
- **备注**:测试不同的文件类型和大小,确保系统能够正确处理

View File

@@ -0,0 +1,188 @@
# PDF上传功能技术规范
## 1. 需求分析
### 1.1 功能需求
- 允许用户上传PDF文件到服务器
- 服务器通过FTP协议将文件存储到指定位置
- 提供上传状态反馈和错误处理
- 支持文件验证和安全性检查
### 1.2 技术要求
- 安全性FTP账号密码不暴露在前端
- 可靠性:支持大文件上传和断点续传
- 可扩展性:易于集成到现有系统
- 兼容性:支持主流浏览器
## 2. 技术方案
### 2.1 实现方式选择
| 方案 | 优点 | 缺点 | 推荐度 |
|------|------|------|--------|
| 前端直接FTP上传 | 操作直观,无需修改后端 | 安全性差,浏览器兼容性问题 | ❌ |
| 后端API上传 | 安全性高,支持服务器端处理 | 需要修改后端代码 | ✅ |
**推荐方案**后端API上传
- 理由安全性更高FTP账号密码保存在服务器端支持服务器端验证和处理
- 架构:前端 → 后端API → FTP服务器
### 2.2 技术栈
- 前端HTML5, JavaScript, jQuery, Bootstrap
- 后端ASP.NET Core, C#
- FTP客户端FluentFTP推荐或System.Net.FtpWebRequest
## 3. 架构设计
### 3.1 后端架构
```mermaid
flowchart TD
A[前端上传请求] --> B[UploadController.PdfUpload]
B --> C[文件验证]
C --> D[FTP上传服务]
D --> E[FTP服务器]
D --> F[返回上传结果]
F --> A
```
### 3.2 前端架构
```mermaid
flowchart TD
A[用户选择PDF文件] --> B[文件验证]
B --> C[AJAX上传请求]
C --> D[UploadController.PdfUpload]
D --> E[显示上传进度]
E --> F[显示上传结果]
```
## 4. 详细设计
### 4.1 后端API设计
#### 4.1.1 接口定义
| API路径 | 方法 | 功能 | 参数 | 返回值 |
|---------|------|------|------|--------|
| /api/upload/pdf | POST | 上传PDF文件 | files: List<IFormFile><br>type: string<br>folder: string | {code: int, message: string, data: List<string>} |
#### 4.1.2 实现细节
- 扩展现有UploadController添加PdfUpload方法
- 集成FTP客户端库实现FTP上传功能
- 添加文件验证逻辑(大小、类型、安全性)
- 实现错误处理和日志记录
### 4.2 前端界面设计
#### 4.2.1 界面元素
- 文件选择器:支持多文件选择
- 上传按钮:触发上传操作
- 进度显示:实时显示上传进度
- 结果反馈:显示上传成功/失败信息
- 历史记录:显示已上传的文件列表
#### 4.2.2 实现细节
- 在batch_query.html中添加PDF上传标签页
- 使用FormData和XMLHttpRequest实现文件上传
- 添加文件验证逻辑(大小、类型)
- 实现上传进度监听
## 5. 安全性考虑
### 5.1 后端安全
- FTP账号密码存储在配置文件中不硬编码
- 实现文件类型验证只允许PDF文件
- 实现文件大小限制
- 添加文件内容安全检查
- 记录上传日志,便于审计
### 5.2 前端安全
- 实现客户端文件类型验证
- 实现客户端文件大小限制
- 防止XSS攻击
- 保护用户隐私
## 6. 性能优化
### 6.1 后端优化
- 使用异步上传,避免阻塞主线程
- 实现断点续传功能
- 优化FTP连接管理
- 使用缓存机制减少重复上传
### 6.2 前端优化
- 实现分块上传,支持大文件
- 添加上传进度显示
- 优化UI响应速度
- 实现上传队列管理
## 7. 测试计划
### 7.1 功能测试
- 单文件上传测试
- 多文件上传测试
- 大文件上传测试
- 错误处理测试
- 边界情况测试
### 7.2 性能测试
- 上传速度测试
- 并发上传测试
- 系统负载测试
### 7.3 安全性测试
- 文件类型验证测试
- 文件大小限制测试
- 安全性检查测试
## 8. 实施计划
### 8.1 后端开发
1. 添加FTP客户端库依赖
2. 扩展UploadController添加PdfUpload方法
3. 实现FTP上传服务
4. 添加文件验证逻辑
5. 编写单元测试
### 8.2 前端开发
1. 在batch_query.html中添加PDF上传标签页
2. 实现文件选择和上传功能
3. 添加进度显示和结果反馈
4. 实现错误处理
### 8.3 测试和部署
1. 功能测试
2. 性能测试
3. 安全性测试
4. 部署到测试环境
5. 部署到生产环境
## 9. 预期效果
### 9.1 功能效果
- 用户可以通过前端界面上传PDF文件
- 系统将文件通过FTP上传到指定服务器
- 用户可以看到上传进度和结果
- 系统提供错误处理和反馈
### 9.2 技术效果
- 安全性高FTP账号密码不暴露
- 可靠性强,支持大文件上传
- 扩展性好,易于集成到现有系统
- 兼容性好,支持主流浏览器
## 10. 风险评估
### 10.1 潜在风险
- FTP服务器连接失败
- 大文件上传超时
- 网络不稳定导致上传失败
- 安全性漏洞
### 10.2 风险缓解
- 实现重试机制
- 设置合理的超时时间
- 实现断点续传
- 加强安全性检查
- 完善错误处理和日志记录

View File

@@ -0,0 +1,71 @@
# 收货查询接口设计文档
## 1. 接口概述
本接口用于查询收货信息,通过输入提单号或大箱号,返回对应的包裹数、已有标签率和到货时间。
## 2. 接口参数
### 2.1 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| billOfLadingNumber | string | 否 | 提单号 |
| masterPackageNumber | string | 否 | 大箱号 |
| callback | string | 否 | JSONP回调函数名 |
**注意**billOfLadingNumber和masterPackageNumber至少需要提供一个。
### 2.2 响应参数
| 参数名 | 类型 | 描述 |
|-------|------|------|
| code | int | 响应码0表示成功9999表示失败 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.packageCount | int | 包裹数 |
| data.labelRate | double | 已有标签率范围0-1 |
| data.arrivalTime | string | 到货时间格式yyyy-MM-dd HH:mm:ss |
## 3. 业务逻辑
1. 根据输入的提单号或大箱号查询label_replace_requests表获取符合条件的记录
2. 计算包裹数:符合条件的记录总数
3. 计算已有标签率Label字段有值的记录数除以总记录数
4. 查询arrival_handover_forms表获取到货时间
5. 组装响应数据并返回
## 4. 接口路径
GET /api/arrival-handover/receipt-query
## 5. 示例请求
```
GET /api/arrival-handover/receipt-query?billOfLadingNumber=BL123456
```
## 6. 示例响应
### 成功响应
```json
{
"code": 0,
"message": "success",
"data": {
"packageCount": 10,
"labelRate": 0.8,
"arrivalTime": "2026-03-25 10:30:00"
}
}
```
### 失败响应
```json
{
"code": 9999,
"message": "参数错误:请提供提单号或大箱号"
}
```

View File

@@ -0,0 +1,12 @@
# 回传接口请求详情记录 - 验证清单
- [x] 检查SendWebhookToXunTong方法是否记录了请求的JSON数据
- [x] 检查日志中是否包含完整的请求JSON数据
- [x] 检查SendWebhookToXunTong方法是否记录了请求发起的IP地址
- [x] 检查日志中是否包含准确的IP地址信息
- [x] 检查记录操作是否不影响接口的正常功能
- [x] 检查接口响应时间是否增加不超过10ms
- [x] 检查日志格式是否清晰易读
- [x] 检查记录的信息是否不包含敏感数据
- [x] 运行测试接口,验证所有功能是否正常
- [x] 检查代码编译是否通过,无语法错误

View File

@@ -0,0 +1,68 @@
# 回传接口请求详情记录 - 产品需求文档
## Overview
- **Summary**: 在讯通回传接口中添加请求JSON和请求发起IP地址的记录功能以便更好地追踪和调试接口调用。
- **Purpose**: 提高系统的可观测性和可调试性,便于排查接口调用问题。
- **Target Users**: 系统运维人员和开发人员。
## Goals
- 记录回传接口的请求JSON数据
- 记录请求发起的IP地址
- 确保记录的信息完整且准确
- 不影响接口的正常功能和性能
## Non-Goals (Out of Scope)
- 不修改接口的核心业务逻辑
- 不改变接口的请求和响应格式
- 不增加额外的外部依赖
## Background & Context
- 现有的讯通回传接口已经实现了基本的功能,但缺乏对请求详情的记录
- 在生产环境中,当接口调用出现问题时,需要更详细的信息来排查问题
- 记录请求JSON和IP地址有助于追踪接口调用来源和内容
## Functional Requirements
- **FR-1**: 在SendWebhookToXunTong方法中记录请求的JSON数据
- **FR-2**: 在SendWebhookToXunTong方法中记录请求发起的IP地址
- **FR-3**: 确保记录的信息包含在日志中,便于查询和分析
## Non-Functional Requirements
- **NFR-1**: 记录操作不影响接口的响应时间增加的延迟不超过10ms
- **NFR-2**: 记录的信息清晰易读,便于分析和调试
- **NFR-3**: 确保记录的信息不包含敏感数据
## Constraints
- **Technical**: 使用现有的日志系统,不引入新的日志框架
- **Business**: 不增加系统的存储成本和计算成本
- **Dependencies**: 依赖现有的HttpClientFactory和日志系统
## Assumptions
- 系统已经配置了合适的日志级别和存储策略
- 接口调用的JSON数据大小在合理范围内不会导致日志过大
## Acceptance Criteria
### AC-1: 记录请求JSON
- **Given**: 系统调用讯通回传接口
- **When**: 接口发送请求前
- **Then**: 系统记录请求的JSON数据到日志中
- **Verification**: `programmatic`
- **Notes**: 日志中应包含完整的请求JSON数据
### AC-2: 记录请求IP地址
- **Given**: 系统调用讯通回传接口
- **When**: 接口发送请求前
- **Then**: 系统记录请求发起的IP地址到日志中
- **Verification**: `programmatic`
- **Notes**: 日志中应包含准确的IP地址信息
### AC-3: 不影响接口功能
- **Given**: 系统调用讯通回传接口
- **When**: 接口执行过程中
- **Then**: 记录操作不影响接口的正常功能和响应时间
- **Verification**: `programmatic`
- **Notes**: 接口应能正常完成请求响应时间增加不超过10ms
## Open Questions
- [ ] 系统是否需要记录响应数据?
- [ ] 日志级别应设置为INFO还是DEBUG

View File

@@ -0,0 +1,38 @@
# 回传接口请求详情记录 - 实现计划
## [x] Task 1: 修改SendWebhookToXunTong方法添加请求JSON记录
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在SendWebhookToXunTong方法中在发送请求前记录请求的JSON数据
- 确保日志中包含完整的请求JSON数据
- 使用合适的日志级别INFO
- **Acceptance Criteria Addressed**: AC-1, AC-3
- **Test Requirements**:
- `programmatic` TR-1.1: 调用接口后检查日志中是否包含请求JSON数据
- `human-judgement` TR-1.2: 检查日志格式是否清晰易读
- **Notes**: 确保JSON数据不包含敏感信息
## [x] Task 2: 修改SendWebhookToXunTong方法添加请求IP地址记录
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在SendWebhookToXunTong方法中记录请求发起的IP地址
- 确保IP地址信息准确记录到日志中
- **Acceptance Criteria Addressed**: AC-2, AC-3
- **Test Requirements**:
- `programmatic` TR-2.1: 调用接口后检查日志中是否包含IP地址信息
- `human-judgement` TR-2.2: 检查IP地址格式是否正确
- **Notes**: 考虑使用HttpContext获取IP地址
## [x] Task 3: 验证实现的正确性
- **Priority**: P1
- **Depends On**: Task 1, Task 2
- **Description**:
- 运行测试接口验证请求JSON和IP地址是否正确记录
- 检查接口响应时间是否符合要求
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3
- **Test Requirements**:
- `programmatic` TR-3.1: 调用TestXunTongWebhook接口检查日志输出
- `programmatic` TR-3.2: 测量接口响应时间确保增加不超过10ms
- **Notes**: 可以使用Postman或curl工具测试

View File

@@ -0,0 +1,10 @@
# 出货交接单添加功能修复 - 验证清单
- [ ] 检查到货交接单的实现方式,确认它如何处理 POD 和 Remarks 字段
- [ ] 修改出货交接单的 `processShippingHandoverForm` 函数,确保 POD 和 Remarks 字段在为空时传递 null 或 undefined
- [ ] 测试无 POD 无备注的情况,确保能正常提交
- [ ] 测试有 POD 无备注的情况,确保能正常提交
- [ ] 测试无 POD 有备注的情况,确保能正常提交
- [ ] 测试有 POD 有备注的情况,确保能正常提交
- [ ] 验证修复后功能与到货交接单保持一致
- [ ] 确保修复不影响其他功能模块的正常运行

View File

@@ -0,0 +1,65 @@
# 出货交接单添加功能修复 - 产品需求文档
## 概述
- **Summary**: 修复出货交接单添加时出现的验证错误问题,确保 POD 和 Remarks 字段在没有上传文件或填写备注时也能正常提交。
- **Purpose**: 解决用户在添加出货交接单时遇到的 400 错误,提升用户体验。
- **Target Users**: 使用 LabelReplaceServer 系统添加出货交接单的操作人员。
## Goals
- 修复出货交接单添加时的验证错误问题
- 确保 POD 和 Remarks 字段在为空时能正常提交
- 保持与后端 API 的兼容性
## Non-Goals (Out of Scope)
- 不修改后端 API 验证逻辑
- 不改变其他功能模块的行为
- 不添加新的功能特性
## Background & Context
- 当前在添加出货交接单时,如果没有上传 POD 图片或填写备注,会收到 400 错误,提示 "The POD field is required." 和 "The Remarks field is required."
- 从前端代码分析当没有上传文件时POD 字段被设置为空字符串Remarks 字段也可能为空字符串
- 后端 API 似乎要求这些字段不为空
## Functional Requirements
- **FR-1**: 当用户没有上传 POD 图片时,系统应该能正常提交出货交接单
- **FR-2**: 当用户没有填写备注时,系统应该能正常提交出货交接单
- **FR-3**: 修复后的功能应该与现有的到货交接单添加功能保持一致
## Non-Functional Requirements
- **NFR-1**: 修复不应该影响其他功能模块的正常运行
- **NFR-2**: 修复应该保持代码的可读性和可维护性
- **NFR-3**: 修复后的功能应该与后端 API 完全兼容
## Constraints
- **Technical**: 前端使用 jQuery 和 Bootstrap后端使用 .NET Core
- **Dependencies**: 依赖后端 API 的验证逻辑
## Assumptions
- 后端 API 实际上接受 POD 和 Remarks 字段为 null 或 undefined但不接受空字符串
- 到货交接单的添加功能已经正确处理了类似情况
## Acceptance Criteria
### AC-1: 无 POD 图片时能正常添加
- **Given**: 用户未上传 POD 图片
- **When**: 用户点击 "添加" 按钮提交出货交接单
- **Then**: 系统成功添加出货交接单,不返回 400 错误
- **Verification**: `programmatic`
- **Notes**: POD 字段应该传递 null 或 undefined而不是空字符串
### AC-2: 无备注时能正常添加
- **Given**: 用户未填写备注
- **When**: 用户点击 "添加" 按钮提交出货交接单
- **Then**: 系统成功添加出货交接单,不返回 400 错误
- **Verification**: `programmatic`
- **Notes**: Remarks 字段应该传递 null 或 undefined而不是空字符串
### AC-3: 有 POD 图片和备注时能正常添加
- **Given**: 用户上传了 POD 图片并填写了备注
- **When**: 用户点击 "添加" 按钮提交出货交接单
- **Then**: 系统成功添加出货交接单,与修复前行为一致
- **Verification**: `programmatic`
## Open Questions
- [ ] 后端 API 对 POD 和 Remarks 字段的具体验证规则是什么?
- [ ] 到货交接单是如何处理这些字段的?

View File

@@ -0,0 +1,40 @@
# 出货交接单添加功能修复 - 实现计划
## [ ] 任务 1: 分析到货交接单的实现方式
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 查看到货交接单添加功能的实现代码,了解它如何处理 POD 和 Remarks 字段
- 对比出货交接单的实现,找出差异
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `human-judgement` TR-1.1: 确认到货交接单的实现方式
- `human-judgement` TR-1.2: 识别出货交接单与到货交接单实现的差异
- **Notes**: 重点关注 `processArrivalHandoverForms` 函数的实现
## [ ] 任务 2: 修改出货交接单添加逻辑
- **Priority**: P0
- **Depends On**: 任务 1
- **Description**:
- 修改 `processShippingHandoverForm` 函数,确保 POD 和 Remarks 字段在为空时传递 null 或 undefined
- 保持与到货交接单实现的一致性
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3
- **Test Requirements**:
- `programmatic` TR-2.1: 验证无 POD 图片时能正常提交
- `programmatic` TR-2.2: 验证无备注时能正常提交
- `programmatic` TR-2.3: 验证有 POD 图片和备注时能正常提交
- **Notes**: 修改 `batch_query.html` 文件中的 `processShippingHandoverForm` 函数
## [ ] 任务 3: 测试修复结果
- **Priority**: P1
- **Depends On**: 任务 2
- **Description**:
- 测试出货交接单添加功能,验证修复是否成功
- 测试各种场景:无 POD 无备注、有 POD 无备注、无 POD 有备注、有 POD 有备注
- **Acceptance Criteria Addressed**: AC-1, AC-2, AC-3
- **Test Requirements**:
- `programmatic` TR-3.1: 测试无 POD 无备注的情况
- `programmatic` TR-3.2: 测试有 POD 无备注的情况
- `programmatic` TR-3.3: 测试无 POD 有备注的情况
- `programmatic` TR-3.4: 测试有 POD 有备注的情况
- **Notes**: 使用浏览器测试功能,确保所有场景都能正常提交

View File

@@ -0,0 +1,40 @@
# 系统架构全面分析 - 验证检查清单
- [ ] 检查点 1: 数据库索引是否已正确创建
- 验证bag_tags表是否有TagNumber、ChannelName、Status索引
- 验证bag_tag_waybills表是否有TagNumber、FinalMileTrackingNumber索引和组合索引
- [ ] 检查点 2: 数据库查询性能优化
- 验证AssociateWaybill方法执行时间是否小于1秒
- 验证数据库查询响应时间是否小于500ms
- 验证批量查询袋牌信息的响应时间是否小于3秒
- [ ] 检查点 3: 线程管理优化
- 验证Task.Run的使用是否合理
- 验证线程池使用率是否保持在70%以下
- 验证并发处理100个请求时系统是否保持稳定
- [ ] 检查点 4: 内存管理优化
- 验证系统运行24小时后内存使用是否保持稳定
- 验证处理1000个关联操作后内存增长是否小于10%
- 验证缓存策略是否合理,缓存项是否有适当的过期时间
- [ ] 检查点 5: 错误处理和系统稳定性
- 验证所有异常是否都被正确捕获和处理
- 验证系统连续运行24小时是否无崩溃
- 验证处理异常情况时系统是否保持稳定
- [ ] 检查点 6: 系统监控和预警机制
- 验证系统是否能监控关键性能指标
- 验证日志是否包含足够的信息用于问题诊断
- 验证是否有预警机制及时发现潜在问题
- [ ] 检查点 7: 系统整体性能
- 验证袋牌与运单关联操作响应时间是否小于5秒
- 验证系统是否能稳定处理正常业务负载
- 验证系统启动时间是否合理
- [ ] 检查点 8: 代码质量
- 验证代码是否符合最佳实践
- 验证是否有适当的注释和文档
- 验证是否有代码冗余或性能瓶颈

View File

@@ -0,0 +1,75 @@
# 系统架构全面分析 - 应用程序死机问题诊断报告
## 概述
- **Summary**: 对LabelReplaceServer系统进行全面架构分析诊断应用程序死机的根本原因并提供相应的解决方案。
- **Purpose**: 识别系统中的性能瓶颈、资源管理问题和潜在的崩溃风险,确保系统稳定运行。
- **Target Users**: 系统开发人员、运维人员和项目管理人员。
## Goals
- 识别导致应用程序死机的根本原因
- 分析系统架构中的性能瓶颈
- 提供具体的解决方案和优化建议
- 确保系统稳定运行,避免类似问题再次发生
## Non-Goals (Out of Scope)
- 重构整个系统架构
- 实现新功能或业务逻辑
- 解决与死机无关的其他问题
## Background & Context
- 系统是一个基于ASP.NET Core的标签替换服务主要处理袋牌管理和运单关联等功能
- 从日志分析来看,系统在处理`AssociateWaybill`请求时响应时间过长最长达到166秒
- 应用程序多次重启,可能是由于崩溃导致
## Functional Requirements
- **FR-1**: 系统应能处理袋牌与运单的关联操作响应时间不应超过5秒
- **FR-2**: 系统应能支持批量查询袋牌信息响应时间不应超过3秒
- **FR-3**: 系统应能稳定运行,避免因资源耗尽或性能问题导致崩溃
## Non-Functional Requirements
- **NFR-1**: 系统应具有良好的内存管理,避免内存泄漏
- **NFR-2**: 系统应具有合理的线程管理,避免线程池耗尽
- **NFR-3**: 系统应具有高效的数据库操作,避免长时间阻塞
- **NFR-4**: 系统应具有健壮的错误处理机制,避免未处理异常导致崩溃
## Constraints
- **Technical**: 基于ASP.NET Core、MySQL数据库、SqlSugar ORM框架
- **Business**: 系统需要处理大量的袋牌和运单数据
- **Dependencies**: 依赖外部服务如Amazon S3等
## Assumptions
- 数据库连接配置正确
- 网络环境稳定
- 服务器硬件资源充足
## Acceptance Criteria
### AC-1: 数据库查询性能优化
- **Given**: 系统处理袋牌与运单关联操作
- **When**: 执行数据库查询时
- **Then**: 查询响应时间应小于1秒
- **Verification**: `programmatic`
### AC-2: 线程管理优化
- **Given**: 系统处理并发请求
- **When**: 执行异步操作时
- **Then**: 线程池使用率应保持在合理范围内,避免耗尽
- **Verification**: `programmatic`
### AC-3: 内存管理优化
- **Given**: 系统运行过程中
- **When**: 处理大量数据时
- **Then**: 内存使用应保持稳定,避免持续增长
- **Verification**: `programmatic`
### AC-4: 系统稳定性
- **Given**: 系统连续运行24小时
- **When**: 处理正常业务负载时
- **Then**: 系统应保持稳定,无崩溃现象
- **Verification**: `programmatic`
## Open Questions
- [ ] 数据库索引是否已正确创建和使用?
- [ ] 系统是否存在内存泄漏问题?
- [ ] 线程池配置是否合理?
- [ ] 缓存策略是否最优?

View File

@@ -0,0 +1,66 @@
# 系统架构全面分析 - 实现计划
## [ ] Task 1: 数据库性能优化
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 检查并确保数据库索引已正确创建
- 优化BagTagRepository中的数据库查询
- 分析并优化AssociateWaybill方法中的数据库操作
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- `programmatic` TR-1.1: AssociateWaybill方法执行时间应小于1秒
- `programmatic` TR-1.2: 数据库查询响应时间应小于500ms
- **Notes**: 重点关注bag_tag_waybills表的查询性能确保索引被正确使用
## [ ] Task 2: 线程管理优化
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 优化Task.Run的使用避免过度创建线程
- 实现合理的线程池配置
- 优化异步操作的处理方式
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- `programmatic` TR-2.1: 线程池使用率应保持在70%以下
- `programmatic` TR-2.2: 并发处理100个请求时系统应保持稳定
- **Notes**: 注意Task.Run的使用场景避免在高并发情况下创建过多线程
## [ ] Task 3: 内存管理优化
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 检查并修复可能的内存泄漏问题
- 优化缓存策略,避免内存过度使用
- 实现内存使用监控
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `programmatic` TR-3.1: 系统运行24小时后内存使用应保持稳定
- `programmatic` TR-3.2: 处理1000个关联操作后内存增长应小于10%
- **Notes**: 重点关注缓存的使用,确保缓存项有合理的过期时间
## [ ] Task 4: 错误处理和系统稳定性优化
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 实现更健壮的错误处理机制
- 添加系统健康检查
- 优化日志记录,便于问题诊断
- **Acceptance Criteria Addressed**: AC-4
- **Test Requirements**:
- `programmatic` TR-4.1: 系统连续运行24小时无崩溃
- `programmatic` TR-4.2: 处理异常情况时系统应保持稳定
- **Notes**: 确保所有异常都被正确捕获和处理,避免未处理异常导致系统崩溃
## [ ] Task 5: 系统监控和预警机制
- **Priority**: P2
- **Depends On**: Task 1, Task 2, Task 3, Task 4
- **Description**:
- 实现系统性能监控
- 添加预警机制,及时发现潜在问题
- 优化系统日志,便于问题分析
- **Acceptance Criteria Addressed**: AC-4
- **Test Requirements**:
- `programmatic` TR-5.1: 系统应能监控关键性能指标
- `human-judgment` TR-5.2: 日志应包含足够的信息用于问题诊断
- **Notes**: 重点监控数据库查询性能、线程池使用情况和内存使用情况

View File

@@ -0,0 +1,78 @@
# 时间戳转换 - Product Requirement Document
## Overview
- **Summary**: 将到货及出库模块接口文档中提到的所有时间字段从DateTime类型统一转换为long类型的毫秒时间戳UTC确保API接口文档与实际代码实现一致。
- **Purpose**: 解决数据库中可null DateTime类型与API接口long类型时间戳之间的转换问题提供一致的API接口体验。
- **Target Users**: 后端开发人员、API调用方
## Goals
- 将出库交接单相关接口中的所有时间参数从DateTime?改为long?
- 将返回实体中的所有时间字段转换为long?类型的时间戳
- 保持文档与代码实现的一致性
- 确保时间戳使用UTC时区
## Non-Goals (Out of Scope)
- 不修改数据库表结构
- 不修改实体类的数据库映射属性
- 不影响其他模块的接口
## Background & Context
根据接口文档要求所有时间字段应使用long类型的毫秒时间戳UTC。目前代码中仍有部分接口使用DateTime类型需要统一转换。
## Functional Requirements
- **FR-1**: 修改出库交接单控制器中的所有时间参数为long?类型
- **FR-2**: 创建时间戳转换辅助方法处理DateTime与long之间的双向转换
- **FR-3**: 修改返回实体时的时间字段转换逻辑,确保所有返回的时间都是时间戳格式
- **FR-4**: 修改到货交接单控制器中的时间参数(如果有)
- **FR-5**: 修改袋牌相关接口中的时间字段(如果有)
## Non-Functional Requirements
- **NFR-1**: 转换逻辑必须处理null值情况
- **NFR-2**: 时间戳必须使用UTC时区
- **NFR-3**: 保持API向后兼容性尽可能
## Constraints
- **Technical**: .NET 6+, C#, ASP.NET Core
- **Business**: 需要尽快完成避免影响API调用方
- **Dependencies**: 依赖现有代码结构
## Assumptions
- 所有时间戳都使用毫秒级精度
- 所有时间戳都基于UTC时区
- SqlSugar ORM能正确处理DateTime?类型的数据库映射
## Acceptance Criteria
### AC-1: 出库交接单创建接口时间参数转换
- **Given**: 用户调用创建出库交接单接口
- **When**: 传入deliveryTime参数
- **Then**: 参数类型为long?系统正确转换为DateTime?存储到数据库
- **Verification**: `programmatic`
### AC-2: 出库交接单确认接口时间参数转换
- **Given**: 用户调用确认出库交接单接口
- **When**: 传入deliveryTime参数
- **Then**: 参数类型为long?系统正确转换为DateTime?存储到数据库
- **Verification**: `programmatic`
### AC-3: 出库交接单列表查询接口时间参数转换
- **Given**: 用户调用查询出库交接单列表接口
- **When**: 传入startDeliveryTime和endDeliveryTime参数
- **Then**: 参数类型为long?系统正确转换为DateTime?进行查询
- **Verification**: `programmatic`
### AC-4: 返回实体时间字段转换
- **Given**: 系统返回包含时间字段的实体数据
- **When**: 序列化响应数据
- **Then**: 所有DateTime类型字段都转换为long?类型的时间戳
- **Verification**: `programmatic`
### AC-5: 空值处理正确
- **Given**: 时间字段为null
- **When**: 进行转换
- **Then**: 转换结果也为null不抛出异常
- **Verification**: `programmatic`
## Open Questions
- [ ] 是否需要修改其他模块的接口?
- [ ] 是否需要添加单元测试?

View File

@@ -0,0 +1,98 @@
# 时间戳转换 - The Implementation Plan (Decomposed and Prioritized Task List)
## [ ] Task 1: 创建时间戳转换辅助工具类
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 创建一个静态辅助类提供DateTime与long时间戳之间的双向转换方法
- 支持null值处理
- 确保使用UTC时区
- **Acceptance Criteria Addressed**: AC-5
- **Test Requirements**:
- `programmatic` TR-1.1: 验证DateTime转换为long时间戳正确
- `programmatic` TR-1.2: 验证long时间戳转换为DateTime正确
- `programmatic` TR-1.3: 验证null值处理正确
- **Notes**: 参考ArrivalHandoverFormService.cs中的现有实现
## [ ] Task 2: 修改出库交接单控制器 - 创建接口
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 修改CreateShippingHandoverForm方法将DeliveryTime参数从DateTime?改为long?
- 使用辅助类转换参数后再赋值给实体
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- `programmatic` TR-2.1: 验证创建接口接受long?类型参数
- `programmatic` TR-2.2: 验证时间戳正确转换并存储
## [ ] Task 3: 修改出库交接单控制器 - 确认接口
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 修改ConfirmShippingHandoverForm方法将deliveryTime参数从DateTime?改为long?
- 使用辅助类转换参数
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- `programmatic` TR-3.1: 验证确认接口接受long?类型参数
## [ ] Task 4: 修改出库交接单控制器 - 列表查询接口
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 修改GetShippingHandoverForms方法将startDeliveryTime和endDeliveryTime参数从DateTime?改为long?
- 使用辅助类转换参数
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `programmatic` TR-4.1: 验证列表查询接口接受long?类型参数
## [ ] Task 5: 修改出库交接单控制器 - 更新接口
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 修改UpdateShippingHandoverForm方法将DeliveryTime参数从DateTime?改为long?
- 使用辅助类转换参数
- **Acceptance Criteria Addressed**: AC-1
- **Test Requirements**:
- `programmatic` TR-5.1: 验证更新接口接受long?类型参数
## [ ] Task 6: 修改返回实体的时间字段转换逻辑
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 修改所有返回实体数据的接口,不直接返回实体对象
- 创建匿名对象或DTO将DateTime字段转换为long?时间戳
- 涉及的实体ShippingHandoverFormEntity, BagTagEntity, ArrivalHandoverFormEntity
- **Acceptance Criteria Addressed**: AC-4
- **Test Requirements**:
- `programmatic` TR-6.1: 验证返回的时间字段是long?类型
- `programmatic` TR-6.2: 验证时间戳值正确
## [ ] Task 7: 检查并修改到货交接单控制器
- **Priority**: P1
- **Depends On**: Task 1
- **Description**:
- 检查ArrivalHandoverFormController中的所有时间参数
- 确保所有时间参数和返回值都使用long?类型时间戳
- **Acceptance Criteria Addressed**: AC-1, AC-4
- **Test Requirements**:
- `programmatic` TR-7.1: 验证到货交接单接口时间参数正确
## [ ] Task 8: 检查并修改袋牌相关接口
- **Priority**: P1
- **Depends On**: Task 1
- **Description**:
- 检查袋牌相关控制器中的时间字段
- 确保返回的BagTagEntity中的时间字段转换为long?时间戳
- **Acceptance Criteria Addressed**: AC-4
- **Test Requirements**:
- `programmatic` TR-8.1: 验证袋牌接口时间字段正确
## [ ] Task 9: 验证文档与代码一致性
- **Priority**: P1
- **Depends On**: Task 2-8
- **Description**:
- 对比接口文档与实际代码实现
- 确保所有接口都符合文档要求
- **Acceptance Criteria Addressed**: 所有AC
- **Test Requirements**:
- `human-judgement` TR-9.1: 人工检查文档与代码一致性

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 |