# 24小时换单率修复计划 - 基于作业时标签率的正确实现

**发起时间**：2026-05-16  
**基于**：用户对业务逻辑的最终澄清

---

## 问题分析

### 之前的错误理解
之前我们按照"到货日期"vs"完成日期"的维度不一致来诊断问题，但**这不是根本问题**。

**根本问题是**：我们没有正确理解"标签率在什么时刻"被用于判断包裹的考核方式

---

## 用户的核心逻辑（正确的业务定义）

### 关键定义
```
最早扫描时间 = 最早的扫描记录的时间（任何Result值）
完成时间 = Result=0的扫描记录的时间（首次成功时间）
```

### 1. 作业策略决策阶段
```
当时没有高于80%的交接单 → 对低于80%的包裹作业（无考核）
当时有高于80%的交接单 → 对高于80%的包裹作业（有考核）
```

**关键**：这个决策是在**最早扫描时刻**做出的，此时的标签率是**固定的**

### 2. 历史数据统计阶段
```
对于历史数据：
- 用"最早扫描时间"作为作业开始的分割点
- 统计最早扫描时间之前推送的标签数
- 这样可以确认当时作业时的标签率是多少
- 用这个"冻结"的标签率来判断是否纳入24H换单率统计
```

### 3. 24H换单率定义（正确）
```
分子 = 纳入考核(作业时标签率≥80%)并且完成考核的包裹数
分母 = 作业时标签率≥80%的交接单中的全部有标签包裹数

关键：都是基于"作业时的标签率"（最早扫描时刻之前的标签数）
而不是"现在的标签率"（数据统计时刻的标签率）

完成时间 = Result=0的扫描记录时间
考核通过 = 完成时间 <= 考核时间
```

---

## 当前代码的根本缺陷

### 缺陷1：冻结标签率的定义错误
**当前实现**（第734-763行 InterchangeUnitLabelRates）：
```sql
冻结标签率 = 有标签包裹数 / 总包裹数
```

**问题**：这是现在看到的标签率，不是作业时的标签率

**正确做法**：
```sql
冻结标签率 = 作业时刻有标签的包裹数 / 总包裹数
即：最早扫描时间之前推送的标签数 / 总包裹数
```

### 缺陷2：没有区分两种标签率

**应该有两个标签率**：
1. **作业时标签率**（在最早扫描时刻）
   - 用于判断这个交接单是否纳入24H换单率统计
   - 用于确定是否需要考核时间
   - 来源：最早扫描时间之前的标签数 / 总数

2. **数据统计时标签率**（现在看到的）
   - 用于最终业务报表展示
   - 当前代码的InterchangeUnitLabelRates已经计算了
   - 但容易混淆

### 缺陷3：没有正确使用最早扫描时间

**当前代码**：最早扫描时间没有被正确利用
- 没有按最早扫描时间切分标签

**应该做的**：
- 统计最早扫描时间之前的有标签包裹
- 与总包裹数比较，得到作业时标签率
- 用作业时标签率判断是否纳入24H考核

---

## 修复方案

### 步骤1：创建"作业时标签率"CTE

**创建新的CTE** `InterchangeUnitLabelRatesAtFirstScan`：

首先需要获取每个包裹的最早扫描时间：
```sql
-- 获取最早扫描时间（任何Result值的最早扫描记录）
-- 获取完成时间（Result=0的最早扫描记录时间）
-- 这两个时间定义了作业时的标签率统计范围
```

然后创建CTE：
```sql
InterchangeUnitLabelRatesAtFirstScan AS (
    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 < (
                     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 l.Id), 
            2
        ) AS label_rate_at_first_scan
    FROM label_replace_requests l
    GROUP BY l.BillOfLadingNumber, l.MasterPackageNumber
)
```

### 步骤2：修改考核时间的判断逻辑

**修改ArrivalFormsEnhanced中的考核时间**（第774行左右）：

考核时间的判断应该基于"作业时的标签率"（最早扫描时刻之前的标签数），而不是现在的标签率：

```sql
考核时间逻辑修改：
-- 使用 label_rate_at_first_scan（作业时标签率）而不是 label_rate_percent（现在的标签率）
CASE 
    WHEN COALESCE(iulr_at_first.label_rate_at_first_scan, 0) >= 80 THEN
        -- 作业时标签率>=80%：根据到仓时间确定考核时间
        CASE
            WHEN HOUR(a.到货时间) < 16 THEN 
                -- 16点前到仓：考核时间 = 当日16点 ~ 次日16点
                CONCAT(DATE_ADD(DATE(a.到货时间), INTERVAL 1 DAY), ' 16:00:00')
            ELSE 
                -- 16点后到仓：考核时间 = 当日16点 ~ 次日23:59:59
                CONCAT(DATE_ADD(DATE(a.到货时间), INTERVAL 1 DAY), ' 23:59:59')
        END
    ELSE
        -- 作业时标签率<80%：考核时间为NULL，完成即达标
        NULL
END
```

**关键点**：
- 这个考核时间决定了是否需要在规定时间内完成
- 如果作业时标签率≥80%，包裹需要在规定时间内完成（完成时间 <= 考核时间）
- 如果作业时标签率<80%，包裹完成即达标（无考核时间要求）

### 步骤3：修改24H换单率的分子和分母

**分子（DailyHighLabelRateAssessed）**：

统计在规定时间内完成的高标签率包裹数

```
完成时间 = Result=0的扫描记录时间（首次成功时间）
考核通过 = 完成时间 <= 考核时间 且 作业时标签率≥80%
分子 = 所有考核通过的包裹总数
```

关键点：
- 使用Result=0的扫描时间作为完成时间
- 与作业时的考核时间（基于作业时标签率）比较
- 仅统计作业时标签率≥80%的包裹

**分母（DailyHighLabelRateShould）**：

统计应该纳入24H考核的高标签率包裹数

修改为统计"作业时标签率≥80%"的包裹，而不是"现在标签率≥80%"

```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 (
        SELECT DISTINCT 
            BillOfLadingNumber,
            MasterPackageNumber
        FROM InterchangeUnitLabelRatesAtFirstScan  -- 改这里：用作业时标签率
        WHERE label_rate_at_first_scan >= 80      -- 改这里：用作业时标签率判断
    ) high_label_units ON ar.BillOfLadingNumber = high_label_units.BillOfLadingNumber
                       AND ar.MasterPackageNumber = high_label_units.MasterPackageNumber
    GROUP BY DATE(CONVERT_TZ(ar.到货时间, '+00:00', '-05:00'))
)
```

**关键点**：
- 分母统计的是有标签的包裹（至少推送过标签）
- 基于作业时的标签率（最早扫描之前的标签数）
- 所有这些包裹都应该在规定时间内完成

### 步骤4：调整分子分母的日期维度

**问题**：当前分子按"完成日期"，分母按"到货日期"

**两个选项**：

#### 选项A：都按到货日期（推荐）
- 分子改为按到货日期分组（改DailyBeforeNoonPassed和DailyAfternoonPassed）
- 含义：某天到货的高标签率订单，24小时内完成的比例

#### 选项B：都按完成日期
- 分母改为按完成日期分组
- 含义：某天完成的高标签率订单中，有多少满足考核要求

**根据用户的业务描述，推荐选项A**

---

## 实施步骤列表

### 第一阶段：准备和分析
- [ ] 1.1 确认最早扫描时间的获取方式是否正确
- [ ] 1.2 分析当前label_replace_requests和label_scan_history的数据关系
- [ ] 1.3 验证是否有足够数据来计算"作业时标签率"

### 第二阶段：代码修改
- [ ] 2.1 创建InterchangeUnitLabelRatesAtFirstScan CTE
- [ ] 2.2 修改ArrivalFormsEnhanced中的考核时间逻辑
- [ ] 2.3 修改DailyHighLabelRateShould（分母）
- [ ] 2.4 修改DailyBeforeNoonPassed和DailyAfternoonPassed（分子按到货日期）
- [ ] 2.5 修改DailyHighLabelRateAssessed的GROUP BY逻辑
- [ ] 2.6 确保外层SELECT的GROUP BY日期仍然生效

### 第三阶段：验证和测试
- [ ] 3.1 编译C#代码，确保无语法错误
- [ ] 3.2 在测试环境运行查询，验证24H换单率≤100%
- [ ] 3.3 手工验证某个日期的数据，确保分子≤分母
- [ ] 3.4 验证作业时标签率和当前标签率的差异

### 第四阶段：部署和文档
- [ ] 4.1 生成修复说明文档
- [ ] 4.2 更新现有的设计文档
- [ ] 4.3 部署到生产环境

---

## 关键设计点

### 1. 最早扫描时间的用法
```
最早扫描时间T的含义：
- 在T之前：系统还没有开始作业
- 在T时刻：确定了作业时的标签率
- 在T之后：标签可能继续增加，但标签率"冻结"

公式：
作业时标签率 = (T时刻之前推送的标签数) / (总包裹数)
```

### 2. 两种标签率的区分
```
作业时标签率（冻结）
↓
决定是否纳入24H考核
↓
如果≥80%：设定考核时间，需要在规定时间完成
如果<80%：无考核时间，完成即达标

数据统计时标签率（最终）
↓
用于业务报表展示
↓
可能与作业时不同，因为标签可能继续增加
```

### 3. 分子分母的对齐
```
建议方案（按到货日期维度）：

某一天的24H换单率 = 
  (该天到货、作业时标签率≥80%、已完成的包裹数) 
  / 
  (该天到货、作业时标签率≥80%的全部包裹数)

这样符合"24小时内完成率"的业务含义
```

---

## 预期结果

修复后应该得到：
```
24H换单率 ∈ [0%, 100%]
分子 ≤ 分母
数据可以手工验证
```

---

## 风险和注意事项

1. **最早扫描时间可能为NULL**
   - 对于没有开始作业的订单，最早扫描时间为NULL
   - 这时候按照最终标签率判断？还是认为标签率为0？
   - 需要与用户确认处理方式

2. **性能影响**
   - 新增的CTE会增加查询复杂度
   - 需要验证查询性能是否可以接受

3. **数据一致性**
   - 修改后历史数据可能会重新计算
   - 需要确认是否需要数据迁移或清理

---

## 文档引用

- 当前代码：LabelReplaceRepository.cs
- 之前的诊断：24h_rate_numerator_denominator_diagnosis.md
- 业务设计文档：应该继续更新

