Files
LabelChange-server/.trae/documents/sql_implementation_details.md
2026-06-01 16:30:29 +08:00

318 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# SQL 改进实现细节文档
## 核心逻辑理解
### 用户需求核心梳理
#### 当前指标体系
- **当天应该换单数** = 历史未完成换单数 + 当日新增换单数
- **实际换单数** = 当天换单完成的包裹数
- **当天换单完成率** = 实际换单数 / 当天应该换单数
#### 新增/修改的考核时间规则
当前系统对每个包裹有一个"考核时间",用来判断包裹是否在规定时间内完成了换单。新需求改变了考核时间的计算方式:
**基于标签率的分组考核**
```
IF 客户标签率 >= 80% THEN
IF 到仓时间.hour < 16 THEN
考核时间 = 次日16:00
ELSE
考核时间 = 次日23:59
END IF
ELSE
考核时间 = 该包裹实际完成换单的时间
END IF
```
这意味着:
- 对于标签率高的客户(>=80%),给予固定的考核时间窗口
- 对于标签率低的客户(<80%)只要换单完成了就算达标
#### 新的24小时换单率计算
**公式**(完成时间 <= 考核时间的包裹数) / 当天应该换单数
**含义**
- 分子通过24小时内完成考核的包裹数
- 分母当天应该完成的所有包裹数包括历史未完成+当日新增
#### 新增指标
1. **16点前到仓包裹数**当日 HOUR(到仓时间) < 16 的包裹
2. **16点后到仓包裹数**当日 HOUR(到仓时间) >= 16 的包裹
---
## SQL实现方案详解
### 关键计算步骤
#### 步骤A计算客户级别标签率新增CTE
用于在考核时间计算中判断是否应用固定时间窗口:
```sql
CustomerLabelRates AS (
SELECT
l.CustomerId,
COUNT(DISTINCT l.Id) AS total_requests,
COUNT(DISTINCT CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN l.Id END) AS labeled_requests,
ROUND(
COUNT(DISTINCT CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN l.Id END) /
COUNT(DISTINCT l.Id) * 100,
2
) AS label_rate_percent
FROM label_replace_requests l
GROUP BY l.CustomerId
)
```
#### 步骤A.5计算系统整体标签率在最终SELECT中
用于输出到前端展示系统全局指标。
**重要**标签率的分母应该是与交接单关联的所有订单数因为当前的统计都是基于与arrival_handover_forms关联的订单
```sql
-- 在最终SELECT的子查询中计算
CONCAT(ROUND(
(SELECT COUNT(DISTINCT l.Id) FROM label_replace_requests l
INNER JOIN arrival_handover_forms a
ON l.BillOfLadingNumber = a.HandoverNumber OR l.MasterPackageNumber = a.HandoverNumber
WHERE l.Label IS NOT NULL AND l.Label != '')
/
(SELECT COUNT(DISTINCT l.Id) FROM label_replace_requests l
INNER JOIN arrival_handover_forms a
ON l.BillOfLadingNumber = a.HandoverNumber OR l.MasterPackageNumber = a.HandoverNumber)
* 100,
2
), '%') AS 系统标签率
```
**说明**:这样计算的标签率与后续的换单统计保持逻辑一致,都是基于与交接单有关联的订单。
#### 步骤B重新计算考核时间修改ArrivalRequests CTE
需要合并客户标签率信息,并根据新规则计算考核时间:
```sql
-- 关键伪代码逻辑
考核时间 = CASE
WHEN clr.label_rate_percent >= 80 THEN
CASE
WHEN HOUR(a.ReceiptTime) < 16 THEN
DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY) + TIME '16:00:00'
ELSE
DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY) + TIME '23:59:59'
END
ELSE
-- 对于标签率低的客户,需要获取实际完成时间
-- 这个需要在后续步骤中通过JOIN获得
oss.首次成功时间
END
```
#### 步骤C统计16点分段到仓的包裹数修改DailyBase CTE
```sql
DailyBase AS (
SELECT
dd.日期,
-- 当日新增换单数
COUNT(DISTINCT CASE
WHEN ar.到货日期 = dd.日期 AND ar.LabelRetrievedAt IS NOT NULL
THEN ar.RequestId
END) AS 当日新增换单数,
-- 新增16点前到仓
COUNT(DISTINCT CASE
WHEN ar.到货日期 = dd.日期 AND HOUR(ar.到货时间) < 16
THEN ar.RequestId
END) AS 16点前到仓包裹数,
-- 新增16点后到仓
COUNT(DISTINCT CASE
WHEN ar.到货日期 = dd.日期 AND HOUR(ar.到货时间) >= 16
THEN ar.RequestId
END) AS 16点后到仓包裹数,
-- 当天标签推送数
COUNT(DISTINCT CASE
WHEN DATE(CONVERT_TZ(ar.LabelRetrievedAt, '+00:00', '-05:00')) = dd.日期
THEN ar.RequestId
END) AS 当日标签推送数
FROM DistinctDates dd
CROSS JOIN ArrivalRequests ar
GROUP BY dd.日期
)
```
#### 步骤D重新计算24小时完成数新/修改CTE
```sql
Daily24HCompletedOrders AS (
SELECT
ar.到货日期 AS 日期,
COUNT(DISTINCT ar.NeutralWaybillNumber) AS 24H内完成数
FROM ArrivalRequests ar
INNER JOIN OverallScanStatus oss ON ar.NeutralWaybillNumber = oss.NeutralWaybillNumber
WHERE
oss.曾成功 = 1
AND oss.首次成功时间 IS NOT NULL
AND ar.考核时间 IS NOT NULL
-- 关键条件:完成时间 <= 考核时间
AND oss.首次成功时间 <= ar.考核时间
GROUP BY ar.到货日期
)
```
#### 步骤E计算"当天应该换单数"(在最终输出中)
```sql
当天应该换单数 = 累计要换的总单数 + 当日新增换单数
-- 或在CASE中根据是否是历史日期判断
```
#### 步骤F修改最终SELECT中的24小时率计算
```sql
-- 原逻辑:
CASE
WHEN 当日完成数 = 0 THEN '0.00%'
ELSE CONCAT(ROUND(24H内完成数 / 当日完成数 * 100, 2), '%')
END AS 24H换单率
-- 新逻辑:
CASE
WHEN 当天应该换单数 = 0 THEN '0.00%'
ELSE CONCAT(ROUND(24H内完成数 / 当天应该换单数 * 100, 2), '%')
END AS 24H换单率
```
---
## 实现复杂点分析
### 1. 考核时间的二阶段计算问题
**问题**:对于标签率低的客户,考核时间需要是"该包裹实际完成换单的时间"但这个时间在关联ArrivalRequests时还不可知。
**解决方案**
- 在ArrivalRequests中先计算一个"参考考核时间"(对标签率>=80%的客户)
- 对于标签率<80%的客户在后续JOIN OverallScanStatus时使用首次成功时间作为考核时间
- 在最终统计时通过CASE WHEN判断
```sql
ArrivalRequests AS (
SELECT
...,
l.CustomerId,
-- 先计算标签率
clr.label_rate_percent,
-- 基础到仓时间
a.到货时间,
-- 根据标签率计算参考考核时间
CASE
WHEN clr.label_rate_percent >= 80 THEN
CASE
WHEN HOUR(a.ReceiptTime) < 16 THEN
CONCAT(DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY), ' 16:00:00')
ELSE
CONCAT(DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY), ' 23:59:59')
END
ELSE
NULL -- 低标签率客户,考核时间取决于完成时间
END AS 基础考核时间
FROM ...
LEFT JOIN CustomerLabelRates clr ON l.CustomerId = clr.CustomerId
)
```
### 2. 时间比较精度问题
**问题**`首次成功时间 <= 考核时间`的比较需要考虑
- UTC和UTC-5的转换
- 时间戳的精度秒级
**解决方案**
```sql
-- 确保都转换为UTC-5时区
WHEN CONVERT_TZ(oss.首次成功时间, '+00:00', '-05:00') <= ar.考核时间 THEN 1
```
### 3. 统计维度的叠加问题
**问题**多个CTE都需要按日期统计需要确保JOIN逻辑不会导致数据重复计数
**解决方案**
- 在每个COUNT中使用DISTINCT确保去重
- 使用CASE WHEN限制统计范围
- 在最终聚合时使用GROUP BY日期
---
## DTO修改方案
### DailyLabelStatsChineseDto 新增字段
```csharp
/// <summary>
/// 系统整体标签率(%
/// </summary>
[SugarColumn(ColumnName = "系统标签率")]
public string LabelRate { get; set; }
/// <summary>
/// 16点前到仓的包裹数
/// </summary>
[SugarColumn(ColumnName = "16点前到仓包裹数")]
public int BeforeNoonArrivedCount { get; set; }
/// <summary>
/// 16点后到仓的包裹数
/// </summary>
[SugarColumn(ColumnName = "16点后到仓包裹数")]
public int AfternoonArrivedCount { get; set; }
/// <summary>
/// 当天应该换单数(历史未完成+当日新增)
/// </summary>
[SugarColumn(ColumnName = "当天应该换单数")]
public int ShouldReplaceCount { get; set; }
```
### C#映射代码
```csharp
var dto = new DailyLabelStatsChineseDto
{
// ... 现有字段 ...
LabelRate = reader["系统标签率"] as string ?? "0.00%",
BeforeNoonArrivedCount = reader["16点前到仓包裹数"] != DBNull.Value
? Convert.ToInt32(reader["16点前到仓包裹数"])
: 0,
AfternoonArrivedCount = reader["16点后到仓包裹数"] != DBNull.Value
? Convert.ToInt32(reader["16点后到仓包裹数"])
: 0,
ShouldReplaceCount = reader["当天应该换单数"] != DBNull.Value
? Convert.ToInt32(reader["当天应该换单数"])
: 0,
};
```
---
## 测试验证清单
- [ ] SQL语法校验无错误
- [ ] 数据准确性验证16点分段统计
- [ ] 时区转换确认所有时间操作都基于UTC-5
- [ ] 标签率计算确认>=80%和<80%的分组逻辑
- [ ] 考核时间逻辑抽样验证几个包裹的考核时间是否正确
- [ ] 24小时率对比原逻辑确保新分母计算正确
- [ ] 当天应该换单数验证 = 累计未完成 + 当日新增
- [ ] Excel导出确认新字段能正确导出