# 24H换单率超过100% - 最终根本原因与修复计划

**日期**：2026-05-16  
**核心问题**：24H换单率远超100%（4745%等），根本原因是分母计算错误

---

## 业务逻辑最终澄清（用户确认）

### 包裹的属性关系

```
考核时间
   ↑
   决定者：作业时的标签率（交接单维度）
   ↓
标签率（两种）
   1. 作业时标签率（固定）
      = 最早扫描时间之前的有标签包裹 / 该交接单所有包裹
      用途：决定考核时间、决定是否纳入24H换单率
   
   2. 统计的标签率（最终）
      = 当前有标签的包裹 / 该交接单所有包裹
      用途：业务报表展示
```

### 标签率与考核时间的关系

```
对于交接单中的每个包裹：
- 作业时标签率≥80% → 有考核时间 → 需要在规定时间内完成
- 作业时标签率<80% → 无考核时间 → 完成即达标

关键：考核时间是交接单维度确定的，然后应用到该交接单的所有包裹
```

### 分母的精确定义（用户最终确认）

```
高标签率应该换单数 = 当日到货 且 作业时标签率≥80% 的交接单中的有标签包裹数

具体计算方式：
1. 找出当日到货的所有交接单
2. 对每个交接单：
   a. 计算作业时标签率 = 最早扫描之前有标签的数 / 总数
   b. 如果作业时标签率≥80%：
      → 计算这个交接单有多少个有标签的包裹
      → 这些有标签包裹加入应该换单数
   c. 如果作业时标签率<80%：
      → 跳过这个交接单（不加入应该换单数）
3. 求和得到总的高标签率应该换单数
```

---

## 当前代码的根本问题

### 问题1：InterchangeUnitLabelRatesAtFirstScan的计算

**当前代码**（第761-791行）：
```sql
SELECT 
    l.BillOfLadingNumber,
    l.MasterPackageNumber,
    COUNT(DISTINCT l.Id) AS total_requests,
    COUNT(DISTINCT CASE 
        WHEN l.Label IS NOT NULL AND l.Label != '' 
        AND l.LabelRetrievedAt < (...)
        THEN l.Id 
    END) AS labeled_at_first_scan,
    ...
FROM label_replace_requests l
GROUP BY l.BillOfLadingNumber, l.MasterPackageNumber
```

**问题**：
- COUNT(DISTINCT l.Id) 统计的是"所有包裹"的数量
- 但根据用户规则，应该统计的是"有标签的包裹"的数量

**正确做法**：
- 分子应该是：最早扫描时间之前有标签的包裹数
- 分母应该是：该交接单所有有标签的包裹数（不是所有包裹）

### 问题2：DailyHighLabelRateShould的计算

**当前代码**（第1100-1117行）：
```sql
DailyHighLabelRateShould AS (
    SELECT 
        DATE(CONVERT_TZ(ar.到货时间, '+00:00', '-05:00')) AS 日期,
        COUNT(DISTINCT CONCAT(ar.BillOfLadingNumber, '|', ar.MasterPackageNumber)) 
            AS 高标签率应该换单数
    FROM ArrivalRequests ar
    INNER JOIN InterchangeUnitLabelRatesAtFirstScan...
    WHERE label_rate_at_first_scan >= 80
```

**问题**：
- 这里统计的是交接单级别的计数
- 但应该统计的是"有标签的包裹"数量
- 如果一个交接单有10个包裹，其中8个有标签，标签率80%
- 应该换单数应该是8，不是1

**导致的后果**：
- 分母被严重低估（每个交接单只算1而不是该交接单的有标签包裹数）
- 分子/分母比率就会巨大

---

## 修复方案

### 第1步：修正InterchangeUnitLabelRatesAtFirstScan

**改进思路**：
- 保留交接单维度的标签率计算（用于决定考核时间）
- 但同时记录"有标签包裹数"用于分母计算

```sql
InterchangeUnitLabelRatesAtFirstScan AS (
    SELECT 
        l.BillOfLadingNumber,
        l.MasterPackageNumber,
        -- 统计有标签的包裹数（用于后续分母计算）
        COUNT(DISTINCT CASE 
            WHEN l.Label IS NOT NULL AND l.Label != '' 
            THEN l.Id 
        END) AS total_labeled_at_any_time,
        -- 作业时有标签的包裹数
        COUNT(DISTINCT CASE 
            WHEN l.Label IS NOT NULL AND l.Label != '' 
            AND l.LabelRetrievedAt < (
                SELECT MIN(lsh.CreatedAt)
                FROM label_scan_history lsh
                WHERE lsh.NeutralWaybillNumber = l.NeutralWaybillNumber
            )
            THEN l.Id 
        END) AS labeled_at_first_scan,
        -- 作业时标签率 = 作业时有标签数 / 所有有标签数
        ROUND(
            COUNT(DISTINCT CASE 
                WHEN l.Label IS NOT NULL AND l.Label != '' 
                AND l.LabelRetrievedAt < (
                    SELECT MIN(lsh.CreatedAt)
                    FROM label_scan_history lsh
                    WHERE lsh.NeutralWaybillNumber = l.NeutralWaybillNumber
                )
                THEN l.Id 
            END) * 100.0 / 
            COUNT(DISTINCT CASE 
                WHEN l.Label IS NOT NULL AND l.Label != '' 
                THEN l.Id 
            END),
            2
        ) AS label_rate_at_first_scan
    FROM label_replace_requests l
    GROUP BY l.BillOfLadingNumber, l.MasterPackageNumber
)
```

### 第2步：修正DailyHighLabelRateShould

**改进思路**：
- 统计的是"有标签的包裹数"，而不是交接单数

```sql
DailyHighLabelRateShould AS (
    SELECT 
        DATE(CONVERT_TZ(ar.到货时间, '+00:00', '-05:00')) AS 日期,
        -- 改为统计：有标签的包裹数
        SUM(CASE 
            WHEN iulr_first.label_rate_at_first_scan >= 80 
            THEN iulr_first.total_labeled_at_any_time
            ELSE 0
        END) AS 高标签率应该换单数
    FROM ArrivalRequests ar
    INNER JOIN InterchangeUnitLabelRatesAtFirstScan iulr_first
        ON ar.BillOfLadingNumber = iulr_first.BillOfLadingNumber
        AND ar.MasterPackageNumber = iulr_first.MasterPackageNumber
    WHERE ar.到货日期 IS NOT NULL
    GROUP BY DATE(CONVERT_TZ(ar.到货时间, '+00:00', '-05:00'))
)
```

### 第3步：确保分子与分母维度一致

**分子的逻辑**（DailyBeforeNoonPassed + DailyAfternoonPassed）：
- 已经是按"订单"（NeutralWaybillNumber）计数
- 这是正确的

**但需要确保**：
- 只统计有标签的订单
- 只统计作业时标签率≥80%的订单
- 只统计在考核时间内完成的订单

---

## 实施步骤

### 步骤1：修改InterchangeUnitLabelRatesAtFirstScan CTE
- 位置：第761-791行
- 任务：添加total_labeled_at_any_time字段，调整label_rate_at_first_scan计算逻辑
- 验证：确保新增字段计算正确

### 步骤2：修改DailyHighLabelRateShould CTE
- 位置：第1100-1117行
- 任务：改为SUM统计有标签包裹数而不是COUNT交接单
- 验证：结果应该更大（因为统计包裹而不是交接单）

### 步骤3：编译验证
- 运行：`dotnet build src/DAL/DAL.csproj`
- 预期：编译成功

### 步骤4：测试和验证

**测试内容**（不要求≤100%，因为这是可能的正常现象）：

测试1：验证分母计算逻辑
```sql
SELECT 
    日期,
    高标签率应该换单数,
    (SELECT COUNT(*) FROM ...) as 应该的包裹数
FROM DailyHighLabelRateShould
WHERE 日期 = '2026-05-15'
```

测试2：验证分子分母是否合理对应
```sql
SELECT 
    日期,
    (SELECT SUM(...) FROM DailyBeforeNoonPassed WHERE 日期='2026-05-15') as 分子_16点前,
    (SELECT SUM(...) FROM DailyAfternoonPassed WHERE 日期='2026-05-15') as 分子_16点后,
    (SELECT 高标签率应该换单数 FROM DailyHighLabelRateShould WHERE 日期='2026-05-15') as 分母
```

测试3：验证数据是否能手工解释
- 选择某一天的数据
- 查看具体的订单数和包裹数
- 确认分子分母的计算逻辑是否符合业务规则
- **不要求分子≤分母或24H换单率≤100%，因为这是可能的正常现象**

---

## 预期结果

修复后：
- ✅ 分母正确反映"有标签包裹数"而不是"交接单数"
- ✅ 数据逻辑一致、可解释
- ✅ 24H换单率数据合理（虽然可能>100%，但不会是4745%这样极端的值）
- ✅ 分子分母的对应关系清晰

