318 lines
9.2 KiB
Markdown
318 lines
9.2 KiB
Markdown
# 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导出:确认新字段能正确导出
|
||
|