# WaveRuleMatchService 流程文档

> 对应实现：`C:\work\gitlab\shopee\wes-loghub\wms-wave\src\main\groovy\com\ittx\wms\wave\service\hairo\WaveRuleMatchService.groovy`
>
> 本文档描述该服务的**当前代码流程**，按调用入口到出口的完整链路展开，包括主流程、分流流程、补缺口流程、回滚流程和详细的规则匹配机制。

---

## 目录

1. [服务概述](#1-服务概述)
2. [入口与总流程](#2-入口与总流程)
3. [前置校验与并发控制](#3-前置校验与并发控制)
4. [一波一单分流流程](#4-一波一单分流流程)
5. [推拣货任务当前口径](#5-推拣货任务当前口径)
6. [标准集波主流程](#6-标准集波主流程)
7. [格口分配策略](#7-格口分配策略)
8. [Shopee 波次绑定与 ESS 校验](#8-shopee-波次绑定与-ess-校验)
9. [内部波次创建与运行](#9-内部波次创建与运行)
10. [回滚机制](#10-回滚机制)
11. [首次集波内补缺口流程](#11-首次集波内补缺口流程)
12. [Flow Pick 换箱换波流程](#12-flow-pick-换箱换波流程)
13. [字段匹配规则（WAVE_RULE_FIELD_MATCHING）](#13-字段匹配规则wave_rule_field_matching)
14. [方法调用关系图](#14-方法调用关系图)

---

## 1. 服务概述

`WaveRuleMatchService` 是 Shopee 出库集波的核心服务，职责：

- 接收「工作站 + 出库类型」请求
- 查询启用的波次规则（`wave_rule`），按优先级排序
- 查找工作站下可用的分播墙格口（`wcs_sorting_wall_cell`）
- 从订单池（`shipment_header`）中匹配符合条件的出库单
- 完成 Shopee 波次号绑定、ESS 合波校验、内部波次创建与运行
- 首次集波成功返回 `ResponseMessageFactory.success()`，不返回「格口 + 命中规则 + 波次」明细

**三种分流入口：**

| 分支 | 条件 | 处理方式 |
|------|------|----------|
| 一波一单分流 | `pickType=1`、`XSCK+ticketType=1`、`ticketType=5`、`ticketType=6` | 一单一个内部波次；RT 不使用 Shopee 波次号 |
| 已绑波次格口补缺口 | 空闲格口已绑定 `shopeeWaveCode` 且 `currentWaveRule` 匹配当前规则 | 按 `low_threshold/maxShipments` 计算缺口后补单 |
| 空格口首次分配 | 其他普通批量候选单 | 按规则优先级逐规则匹配，先抢占格口，再绑定 Shopee 波次，再创建并运行内部波次 |

---

## 2. 入口与总流程

### 外部入口

```
POST wms/automation/waveRuleMatch/findBestMatchedRule
  → WaveRuleMatchController.findBestMatchedRule(map)
    → WaveRuleMatchService.findBestMatchedRuleByWorkStationAndOutboundType(warehouseCode, workStation, shipmentType)
```

### 调用链总图

```
findBestMatchedRuleByWorkStationAndOutboundType(String, String, String)
  │
  ├─ 参数校验（warehouseCode / workStation / shipmentType 非空）
  ├─ findEnabledWorkStation(warehouseCode, workStation)
  │   └─ 支持工作站编码 或 数字 ID 查询
  │
  └─ findBestMatchedRuleByWorkStationAndOutboundType(Long, String)  ← 统一入口
       │
       ├─ [1] 工作站校验：存在 / 启用 ENABLE / 仓库匹配
       ├─ [2] hasDifferentActiveShipmentType
       │       → 已占用 USED 格口关联 Shopee 波次反查当前工作站作业出库类型
       │       → 有其它出库类型时记录系统处理日志并返回 MSG_WRM_0014
       │
       ├─ [3] buildRuleMatchLockKey → Redis 锁（仓库+工作站+出库类型）
       │       失败 → MSG_WRM_0012 "工作站正在集波中"
       │
       ├─ [4] findEnabledRules(warehouseCode)
       │       → wave_rule WHERE warehouseCode=? AND status=ENABLE ORDER BY wavePriority DESC
       │       无规则 → MSG_WRM_0007
       │
       ├─ [5] assignSingleShipmentWaves(先尝试一波一单候选)
       │       → pickType=1 / ticketType=1,5,6
       │
       ├─ [6] fillShortageCells(已绑定 Shopee 波次格口补缺口)
       │       → 空闲 + shopeeWaveCode 有值 + currentWaveRule 匹配
       │
       ├─ [7] assignEmptyCells(空格口首次分配)
       │       → 普通批量候选单进入空格口分配
       │
       └─ [8] 成功返回 ResponseMessageFactory.success()
```

---

## 3. 前置校验与并发控制

### 3.1 工作站校验

```
findEnabledWorkStation(warehouseCode, workStation)
```

- 入参 `workStation` 兼容 **编码**（String）和 **数字 ID**（自动识别）
- 校验：`status=ENABLE`、`warehouseCode` 匹配
- 失败返回对应 `MSG_WRM_0004/0005/0006`

### 3.2 Redis 锁

```
buildRuleMatchLockKey → "wave_rule_match:{warehouseCode}:{workstationId}:{shipmentType}"
RedissonLockService.getAndTryLock(lockKey)
```

- 锁粒度：**仓库 + 工作站 + 出库类型**
- 获取失败时不阻塞等待，直接返回 `MSG_WRM_0012`（前端可提示用户稍后重试）
- 在 finally 块中 `WmsRedissionLockService.unlock(lock)`

### 3.3 同工作站出库类型隔离

```
hasDifferentActiveShipmentType(warehouseCode, workstationId, shipmentType)
  → findActiveWorkstationShipmentTypes(warehouseCode, workstationId)
```

- 查询当前工作站已占用的 `USED` 格口。
- 通过格口 `shopeeWaveCode` 关联 `shipment_header.shopeeWave`，获取当前工作站正在作业的 `shipmentType`。
- 若存在非本次请求的出库类型，入口直接拒绝本次集波。
- 拒绝时记录系统处理日志，并返回 `MSG_WRM_0014`。
- 该校验位于 Redis 锁之前，避免无效请求占用集波锁。

---

## 4. 一波一单分流流程

### 触发条件

`pickType=1`、`XSCK+ticketType=1`、`ticketType=5`、`ticketType=6`

### 方法

```
assignSingleShipmentWaves(warehouseCode, shipmentType, workstationId, rules)
```

### 流程图

```
for each WaveRule:
  findSingleShipmentWaveShipments(warehouseCode, shipmentType, rule)
    → buildAvailableShipmentSql + appendSingleWaveFilter + appendRuleFieldMatching
    → LIMIT 1（只取 1 单）
  if shipment found:
    createInternalWave(warehouseCode, rule)  → 创建内部波次
    addShipmentsToInternalWave([shipmentId], waveId)
      → ShipmentHeaderService.addMultipleToWave + WaveService.run
    return success

无任何规则命中 → MSG_WRM_0010
```

### 特点

- **RT 不使用 Shopee 波次号**（`shopeeWaveCode` 返回空字符串 `""`）
- **当前一波一单分流不走 ESS 校验**
- **不走格口分配**（不绑定格口）
- **一单一个内部波次**

---

## 5. 推拣货任务当前口径

### 触发条件

`pickType=2`（出库单由上游推入时设置，同时预写入 `shopeeWave`）当前作为待恢复的独立分流口径。

### 方法

当前代码中未保留 `assignPickingTaskSingleWaves` 和 `findPickingTaskShipments` 方法。

### 流程图

```
当前已落地:
  appendBatchWaveFilter 排除 pickType=2，避免推拣货任务进入普通批量集波。

待恢复:
  上游推单写入 pickType=2 + shopeeWave 后，
  WES 独立分流入口按一单一波一槽口处理。
```

### 特点

| 特性 | 说明 |
|------|------|
| 波次号来源 | 上游预写入 `shipment_header.shopeeWave`，WES 不走取号逻辑 |
| 当前状态 | 普通批量集波已过滤 `pickType=2` |
| 待实现 | 独立分流入口、一单一波一槽口、内部波次运行 |
| 补单 | 后续仍按“不走补单逻辑”处理 |

---

## 6. 标准集波主流程

### 6.1 主循环

```
for (WaveRule rule : rules) {
    List<WcsSortingWallCell> cells = findCells(workstationId)
    // 三层级分组
    List<List<WcsSortingWallCell>> cellGroups = [
        sameCurRuleCells,   // currentWaveRule == rule.code
        ruleCells,          // currentWaveRule 为空 && waveRule 匹配
        emptyCells          // 两者均为空
    ]
    for (group : cellGroups) {
        assignCellNumbers(warehouseCode, shipmentType, workstationId, rule, group)
        // 命中即返回，不继续尝试下一组格口
    }
}
```

### 6.2 assignCellNumbers 核心流程

```
assignCellNumbers(warehouseCode, shipmentType, workstationId, rule, cells):
  │
  ├─ 1. findSingleShipmentWaveShipments → 查一波一单候选
  │     → pickType=1 / XSCK+ticketType=1 / ticketType=5 / RT
  │     有 → singleShipmentWave=true, 取 1 单
  │     无 → 继续
  │
  ├─ 2. findAvailableShipments → 查批量集波候选
  │     → 排除 pickType=1,2 + 排除一波一单 ticketType
  │     → 规则字段匹配 + 订单结构匹配 + kickOutWave 过滤
  │     → LIMIT = maxShipments（默认 30）
  │
  ├─ 3. 仍无候选 → MSG_WRM_0010
  │
  ├─ 4. logPriorityRuleConsumption — 记录优先级消费日志
  │
  ├─ 5. createInternalWave(warehouseCode, rule) → 创建内部波次
  │     失败 → MSG_WRM_0009
  │
  └─ 6. for each cell in cells:
       │
       ├─ 取出 cell.shopeeWaveCode
       │
       ├─ [一波一单] singleShipmentWave=true && cell 已有波次号 → continue（不补入已用格口）
       │
       ├─ [已有波次] cell 已有波次号 → skipSingleRpln 检查
       │     → 单品单件 且 未低于 low_threshold → continue
       │
       ├─ [无波次号] → acquireUnusedShopeeWave → 取新号
       │     失败 → rollbackInternalWaveIfEmpty + MSG_WRM_0009
       │
       ├─ [补充新单] 已有波次号 + 非单品单件/低于阈值
       │     → findAvailableShipments(..., shopeeWaveCode) 过滤 kickOutWave
       │
       ├─ assignToCell:
       │    ├─ pickCellShipmentIds → 按格口缺口截断（单品单件=maxShipments, 其他=差额）
       │    ├─ ensureCellReadyForWave → 先抢占格口 USED
       │    ├─ bindShipmentShopeeWave → Shopee 波次绑定接口；成功整批写 shopeeWave，失败整批写 kickOutWave
       │    ├─ addShipmentsToInternalWave → 加入波次 + 运行
       │    └─ 成功后消费当前候选池
       │
       └─ 一个格口消费一批后 break（不再占用其他格口）
```

### 6.3 订单池 SQL 基础条件

```sql
SELECT id FROM shipment_header
WHERE warehouseCode = :warehouseCode
  AND shipmentType = :shipmentType
  AND leadingSts = 100          -- 订单池状态
  AND trailingSts = 100         -- 订单池状态
  AND processType = 'NORMAL'    -- 正常处理流程
  AND ifnull(waveId, 0) = 0     -- 未加入任何波次
  AND (lockCode IS NULL OR trim(lockCode) = '')  -- 未被锁定
  AND (cancelTime IS NULL OR cancelTime = '')    -- 未取消
  AND (holdTime IS NULL OR holdTime = '')        -- 未挂起
```

---

## 7. 格口分配策略

### 7.1 三层级匹配

| 层级 | 方法 | 条件 | 优先级 |
|------|------|------|--------|
| 1 | `matchCurRuleCells` | `currentWaveRule == rule.code` | 最高 |
| 2 | `matchRuleCells` | `currentWaveRule` 为空 且 `waveRule` 逗号分隔匹配 rule.code | 中 |
| 3 | `matchEmptyCells` | `currentWaveRule` 和 `waveRule` 均为空 | 兜底 |

### 7.2 格口抢占

```
occupySortingWallCell(warehouseCode, cell, rule, shopeeWaveCode)
```

```sql
UPDATE wcs_sorting_wall_cell
SET useStatus = 'USED',
    shopeeWaveCode = ?,
    currentWaveRule = ?,
    lastUpdatedBy = ?
WHERE id = ?
  AND warehouseCode = ?
  AND status = 'ENABLE'
  AND useStatus = 'IDLE'
```

- 使用乐观锁（`useStatus='IDLE'` 条件），同一格口只能被一个请求抢占成功
- 已有相同 Shopee 波次和规则的 USED 格口通过 `ensureCellReadyForWave` 复用（计划任务补单场景）

### 7.3 格口查询

```
findCells(workstationId)
  → 找出工作站下所有分播墙 -> 各墙下启用格口 -> 过滤 IDLE 状态格口
```

---

## 8. Shopee 波次绑定与 ESS 校验

### 8.1 波次号抢占

```
acquireUnusedShopeeWave(warehouseCode, waveType)
```

1. 查询 `shopee_wave` 表：`warehouseCode` + `waveType` + `status=100(UNUSED)`，按 id 升序取前 20
2. 逐条尝试乐观更新：`SET status=200(USING) WHERE id=? AND status=100`
3. 更新成功的第一条即抢占成功，标记 `processStamp='CLAIMED_UNUSED'`
4. 全部失败返回 null（号段耗尽）

### 8.2 Shopee 波次绑定

```
bindShipmentShopeeWave(warehouseCode, shipmentIds, shopeeWaveCode)
```

**流程：**

```
1. buildShopeeWaveBindLockKey
   → 同一 Shopee 波次号绑定串行，避免多个入口同时使用同一号段。

2. isShopeeWaveCodeAvailable
   → 获取锁后重新检查 shopee_wave 是否仍可用。

3. requestShopeeWaveBind
   → 预留真实上游绑定接口。
   → 当前口径只有整批成功或整批失败，不处理部分成功。

4. 失败单据 → markKickOutWave(shipmentIds, shopeeWaveCode)
   → UPDATE shipment_header SET kickOutWave = CONCAT(已有的, ',', 新波次号)
   → 逗号分隔，同波次号去重

5. 成功单据 → UPDATE shipment_header SET shopeeWave = ? WHERE id IN (...)
   → 同步更新 shopee_wave.status/pickType/groupKey

6. 返回 ResponseMessageFactory.success()
```

### 8.3 接口对接状态

`requestShopeeWaveBind` 当前预留真实接口调用位置：

```
接口成功 → 本批出库单全部成功绑定 shopeeWave
接口失败 → 本批出库单全部写入 kickOutWave
```

正式接口契约确认后，只需补齐 `requestShopeeWaveBind` 的真实调用；当前不再保留柔性拒单解析方法。

---

## 9. 内部波次创建与运行

### 9.1 createInternalWave

```
createInternalWave(warehouseCode, rule)
```

1. `resolveMasterCode(warehouseCode)` — 从系统参数读取波次主表编码（`WCS/EQUIPMENT_MANUFACTURER`）
2. `waveSvc.createInternalWave(session, warehouseCode, masterCode, ruleCode)` — 创建内部波次
3. 返回 waveId（Long）
4. 失败返回 null

### 9.2 addShipmentsToInternalWave

```
addShipmentsToInternalWave(shipmentIds, waveId)
```

1. `shipmentHeaderService.addMultipleToWave(session, shipmentIds, waveId)` — 加入波次
2. `waveSvc.run(session, waveId)` — 异步提交 `wms.wave` 队列消息
3. 任一步失败返回错误

### 9.3 波次运行结果

`WaveService#run` 只提交异步队列消息并返回提交结果，`WaveRuleMatchService` 不解析同步库存不足踢单明细。

库存不足踢单由 `wms-wave` 回池链路处理：回池时清空出库单头 `shopeeWave/waveRule`，不记录 `kickOutWave`；上游撤单接口仍需在回池链路接入。

---

## 10. 回滚机制

### 回滚场景与对应方法

| 场景 | 回滚方法 | 操作 |
|------|----------|------|
| Shopee 波次绑定失败 | `rollbackShipmentShopeeWave` | 回退 `shipment_header.shopeeWave = null`（仅当 `waveId=0` 时） |
| 内部波次空无单据 | `rollbackInternalWaveIfEmpty` | `UPDATE wave SET status=999`（标记异常，不删除数据） |
| 格口抢占失败 | `rollbackWaveShipments` + 逐单 `rollbackShipmentShopeeWave` | 从内部波次剔除 + 回退 Shopee 波次号 |
| ESS 拒绝 | `markKickOutWave` | 写入 `kickOutWave`，同波次后续补单过滤 |
| 库存不足踢单 | `ShipmentAllocationService.clearShopeeWaveBindingAfterKickOut` | 清空 `shopeeWave/waveRule`，不写 `kickOutWave` |
| 内部波次加单失败 | `removeShipmentsFromWave` | 调用 `ShipmentHeaderService.batchRemoveFromWave` |
| 新 Shopee 波次未使用 | `rollbackClaimedShopeeWaveIfUnused` | 恢复 `shopee_wave.status=100`（仅当无任何引用时） |
| 旧波次无未完成任务 | `markShopeeWaveCompletedIfNoOpenTask` | `shopee_wave.status=300(COMPLETED)` |

### 回滚原则

- **不删除数据**：波次标记 999 而非 DELETE，Shopee 波次号恢复 100 而非删除
- **不取消出库单**：`handleFlowPickChangeWaveFailure` 明确注释"不取消出库单"
- **先绑后解**：绑定顺序为 Shopee 波次号 → 格口 → 内部波次，回滚按反序执行

---

## 11. 首次集波内补缺口流程

### 11.1 入口

当前状态：`pollReplenishment/pollReplenishmentOnce` 已删除。补缺口能力在首次集波入口中通过 `fillShortageCells` 执行。

已具备的依赖方法：

- `fillShortageCells(...)`：入口内逐规则处理已绑定 Shopee 波次的可补格口。
- `findShortageCells(Long workStationId, WaveRule rule)`：SQL 查询空闲、已绑定 Shopee 波次、`currentWaveRule` 匹配当前规则的格口。
- `calcCellNeedQty(...)`：计算单品单件或非单品单件缺口。
- `assignToCell(...)`：先抢占格口，再请求 Shopee 波次绑定接口，再创建并运行内部波次。

入口不返回分配明细；成功、失败和跳过原因通过系统处理日志记录。

### 11.2 流程

```
findBestMatchedRuleByWorkStationAndOutboundType(...)
  │
  └─ fillShortageCells(warehouseCode, shipmentType, workstationId, rules)
       │
       └─ for each rule:
            findShortageCells(workstationId, rule)
              → wcs_sorting_wall_cell
              → useStatus = IDLE
              → shopeeWaveCode 非空
              → currentWaveRule = rule.code
            │
            └─ for each cell:
                 calcCellNeedQty
                 findAvailableShipments(..., shopeeWaveCode, groupKey)
                 assignToCell
```

### 11.3 fillShortageCells 逻辑

```
fillShortageCells(warehouseCode, shipmentType, workstationId, rules):
  │
  ├─ 参数校验（warehouseCode / shipmentType / workstationId / rules）
  │
  ├─ for each rule:
  │   ├─ findShortageCells → 查当前规则可补格口
  │   └─ for each cell:
  │       ├─ calcCellNeedQty → 计算缺口
  │       │   ├─ 单品单件: 未拣货数量 < low_threshold → need = maxShipments
  │       │   ├─ 非单品单件: need = maxShipments - 当前 Shopee 波次已绑单量
  │       │   └─ need <= 0 → 跳过
  │
  ├─ resolveShopeeWaveGroupKey → 同一 Shopee 波次只补相同 groupKey 单据
  │
  ├─ findAvailableShipments(..., shopeeWaveCode, groupKey)
  │   → 排除 kickOutWave 过滤的已拒单
  │
  └─ assignToCell
      ├─ ensureCellReadyForWave → 先抢占格口
      ├─ bindShipmentShopeeWave → Shopee 波次绑定接口
      ├─ createInternalWave
      └─ addShipmentsToInternalWave → WaveService#run
```

### 11.4 缺口计算

```
calcCellNeedQty(warehouseCode, rule, cell, shopeeWaveCode)
```

| 规则类型 | 触发条件 | 补单数量 |
|----------|----------|----------|
| 单品单件 | `< low_threshold` | `maxShipments`（一次性补满） |
| 非单品单件 | 始终尝试 | `maxShipments - 当前波次已绑单量` |
| 新格口/无波次号 | — | 返回 null（不截断） |

---

## 12. Flow Pick 换箱换波流程

### 12.1 触发

当前 Flow Pick 换箱换波接口为占位流程，需结合真实触发入口恢复；原计划任务补单入口已删除。

`isFlowPickCellFull`：单品单件 + `countShopeeShipments >= maxShipments`

### 12.2 流程

```
changeFlowPickWave(workstation, cell, rule):
  │
  ├─ 1. findFlowPickRemainingShipments
  │     → 当前工作站 + 旧波次 + 规则下 未完成拣货任务的订单
  │     无 → markShopeeWaveCompletedIfNoOpenTask + 返回
  │
  ├─ 2. requestFlowPickSealBox(封箱)
  │     → essFlowPickService.sealBox（动态调用）
  │       未接入 → 记录日志，返回 true（继续本地换波）
  │       失败 → 终止
  │
  ├─ 3. resolveShopeeWaveType(从剩余订单推断波次类型)
  │
  ├─ 4. acquireUnusedShopeeWave(取新号段)
  │     失败 → 返回错误
  │
  ├─ 5. requestFlowPickChangeBox(换箱)
  │     → essFlowPickService.changeBoxWave（动态调用）
  │       未接入 → 记录日志，返回 true（继续本地回写）
  │       失败 → rollbackClaimedShopeeWaveIfUnused + handleFlowPickChangeWaveFailure + 返回错误
  │
  ├─ 6. updateFlowPickShopeeWave(回写新波次号)
  │     → UPDATE shipment_header SET shopeeWave = 新号
  │     → UPDATE wcs_work_station_pick_task_header SET shopeeWave = 新号
  │     → UPDATE wcs_sorting_wall_cell SET shopeeWaveCode = 新号
  │
  └─ 7. markShopeeWaveCompletedIfNoOpenTask(旧波次标记完成)
```

### 12.3 当前状态

| 步骤 | 实现状态 | 说明 |
|------|----------|------|
| 剩余订单查询 | ✅ 已实现 | `findFlowPickRemainingShipments` |
| 封箱接口 | ⚠️ 动态调用占位 | `essFlowPickService.sealBox`（未接入时本地跳过） |
| 新波次取号 | ✅ 已实现 | `acquireUnusedShopeeWave` |
| 换箱接口 | ⚠️ 动态调用占位 | `essFlowPickService.changeBoxWave`（未接入时本地跳过） |
| 字段回写 | ✅ 已实现 | `updateFlowPickShopeeWave` |
| 旧波次完成 | ✅ 已实现 | `markShopeeWaveCompletedIfNoOpenTask` |
| 失败回滚 | ⚠️ 仅日志 | `handleFlowPickChangeWaveFailure` 仅记录日志，未实现 AGV 回库/订单池恢复 |

---

## 13. 字段匹配规则（WAVE_RULE_FIELD_MATCHING）

### 13.1 数据字典结构

通过数据字典表 `config_detail` + `config_value` 配置，`recordType = 'WAVE_RULE_FIELD_MATCHING'`。

| 字段 | 含义 | 示例 |
|------|------|------|
| `identifier` | 出库单字段 | `userDef1`、`shipmentCategory1`、`priority` |
| `value1` | 规则字段 | `channelId`、`shopId`、`urgentFlag` |
| `value2` | 操作符 | `IN` / `EQ` / `LE` |
| `warehouseCode` | 仓库级/全局 | 优先匹配当前仓，无配置时读取 `*` |

### 13.2 支持的操作符

| 操作符 | SQL 生成 | 说明 |
|--------|----------|------|
| `IN`（默认） | `shipmentField IN (:values)` | 规则值按逗号/中文逗号拆分多选 |
| `EQ` / `=` / `EQUAL` / `EQUALS` | `shipmentField = :value` | 等值匹配 |
| `LE` / `<=` / `LESS_OR_EQUAL` | `ifnull(shipmentField, 0) <= :value` | 数值小于等于 |

### 13.3 字段白名单

防止字典配置拼出非预期 SQL，白名单校验在 `toRuleFieldMapping` 中执行：

**出库单字段（30+）：**
`shipmentType`, `ticketType`, `pickType`, `sourcePlatform`, `sourceErp`, `erpOrderType`, `route`, `carrierCode`, `shipmentSubType`, `shipmentCategory1~8`, `shipToCountry/State/City/District/Town(Code)`, `hostCompanyCode`, `storeCode`, `shipObjType`, `requestedDeliveryType`, `priority`, `userDef1~8`

**规则字段（16）：**
`channelId`, `fulfillmentChainId`, `orderSize`, `skuSizeType`, `categoryLevel1Id`, `shopGroup`, `shopId`, `urgentFlag`, `outerPackaging`, `outerPackagingId`, `deliveryRegion`, `fragile`, `liquid`, `highValue`, `battery`, `danger`

### 13.4 执行流程

```
resolveRuleFieldMappings(warehouseCode)
  → 查 config_detail (优先 warehouseCode, 兜底 '*')
  → toRuleFieldMapping(detail)
    → 白名单校验
    → normalizeRuleOperator 规范化操作符
    → 返回 RuleFieldMapping(ruleField, shipmentField, operator)

appendRuleFieldMatching(sql, params, warehouseCode, rule)
  for each mapping:
    switch operator:
      IN  → appendIn(sql, params, shipmentField, ruleField + 'Values', splitCandidates(ruleValue))
      EQ  → appendEquals(sql, params, shipmentField, ruleField + 'Value', ruleValue)
      LE  → appendLessOrEqual(sql, params, shipmentField, ruleField + 'Value', toNonNegativeInteger(ruleValue))
```

---

## 14. 方法调用关系图

### 14.1 首次集波

```
findBestMatchedRuleByWorkStationAndOutboundType(String, String, String)    [入口-工作站编码]
  └─ findEnabledWorkStation
  └─ findBestMatchedRuleByWorkStationAndOutboundType(Long, String)         [统一入口]
       ├─ findEnabledRules
       ├─ assignSingleShipmentWaves                                        [一波一单分流]
       │   ├─ findSingleShipmentWaveShipments
       │   ├─ createInternalWave
       │   └─ addShipmentsToInternalWave
       ├─ fillShortageCells                                                [已绑定 Shopee 波次格口补缺口]
       │   ├─ findShortageCells
       │   ├─ calcCellNeedQty
       │   ├─ resolveShopeeWaveGroupKey
       │   ├─ findAvailableShipments(..., shopeeWaveCode, groupKey)
       │   └─ assignToCell
       └─ (标准集波) for each rule:                                       [标准集波]
            ├─ findCells → matchCurRuleCells / matchRuleCells / matchEmptyCells
            └─ assignCellNumbers
                 ├─ findSingleShipmentWaveShipments
                 ├─ findAvailableShipments
                 ├─ createInternalWave
                 ├─ acquireUnusedShopeeWave
                 └─ for each cell:
                      ├─ skipSingleRpln (单品单件)
                      ├─ assignToCell
                      │   ├─ pickCellShipmentIds
                      │   ├─ calcCellNeedQty
                      │   ├─ ensureCellReadyForWave → occupySortingWallCell
                      │   ├─ bindShipmentShopeeWave
                      │   │   ├─ requestShopeeWaveBind
                      │   │   ├─ markKickOutWave (整批失败)
                      │   │   └─ updateShopeeWaveBindingFields (整批成功)
                      │   ├─ addShipmentsToInternalWave → run
                      │   └─ 成功后消费当前候选池
                      └─ (回滚)
                           ├─ rollbackInternalWaveIfEmpty
                           ├─ rollbackWaveShipments → removeShipmentsFromWave
                           └─ rollbackShipmentShopeeWave
```

### 14.2 首次集波内补缺口

```
fillShortageCells
  └─ for each rule:
       ├─ findShortageCells
       └─ for each cell:
            ├─ calcCellNeedQty
            ├─ resolveShopeeWaveGroupKey
            ├─ findAvailableShipments(..., shopeeWaveCode, groupKey)
            └─ assignToCell
```

### 14.3 查询方法

```
订单查询:
  buildAvailableShipmentSql         ← 基础 SQL（异常过滤）
  buildAvailableShipmentParams      ← 基础参数
  appendBatchWaveFilter             ← 排除一波一单
  appendSingleWaveFilter            ← 一波一单条件
  appendRuleFieldMatching           ← WAVE_RULE_FIELD_MATCHING 字典匹配
    ├─ appendIn / appendEquals / appendLessOrEqual
    └─ resolveRuleFieldMappings → toRuleFieldMapping
  appendKickOutFilter               ← 排除已拒单
  appendShipmentPriorityOrder       ← 排序

格口查询:
  findCells(workstationId)
  findCells(workstationId, rule)
  matchCurRuleCells / matchRuleCells / matchEmptyCells
  findActiveWorkstationShipmentTypes(warehouseCode, workStationId)

规则查询:
  findEnabledRules(warehouseCode)

工作站查询:
  findEnabledWorkStation(warehouseCode, workStation)

波次查询:
  countShopeeShipments(warehouseCode, shopeeWaveCode)
  resolveShopeeWaveGroupKey(warehouseCode, shopeeWaveCode)
  resolveShopeeWaveType(warehouseCode, shipmentIds)

工具方法:
  toLongValue / toNonNegativeInteger
  ruleStringValue / ruleIntegerValue
  splitCandidates / matchesCellWaveRule
  buildRuleMatchLockKey
  isSingleRule
  resolveMasterCode / resolveShipmentLimit / resolveLowThreshold
  normalizeRuleOperator
```

---

## 附录：关键常量

| 常量 | 值 | 用途 |
|------|-----|------|
| `SHIPMENT_IN_POOL` | `100` | 出库单订单池状态 |
| `SHIPMENT_PROCESS_NORMAL` | `'NORMAL'` | 正常处理流程标识 |
| `DEFAULT_SHIPMENT_LIMIT` | `30` | 规则未配 `maxShipments` 的兜底上限 |
| `PICK_TYPE_PICKING_BY_ORDER` | `1` | 按单拣选标识 |
| `PICK_TYPE_PICKING_TASK` | `2` | 推拣货任务标识 |
| `TICKET_TYPE_RT` | `6` | RT ticketType，一波一单且不使用 Shopee 波次号 |
| `TICKET_TYPE_XSCK_TASK` | `1` | 销售出库任务 |
| `TICKET_TYPE_MTO_TASK` | `5` | MTO 任务 |
| `WAVE_RULE_FIELD_MATCHING` | `'WAVE_RULE_FIELD_MATCHING'` | 字段匹配字典类型 |

## 附录：TODO 清单（代码内标注）

| 位置 | TODO | 优先级 |
|------|------|--------|
| `findBestMatchedRuleByWorkStationAndOutboundType` | 用户权限与出库类型绑定 | P1 |
| `resolveMasterCode` 前注释 | 库存最优分配规则对齐 | P2 |
| `acquireUnusedShopeeWave` 前注释 | Shopee 波次号段申请接口 & 用完续号 | P1 |
| `requestFlowPickSealBox` Javadoc | Flow Pick 封箱接口正式对接 | P1 |
| `requestFlowPickChangeBox` Javadoc | Flow Pick 换箱接口正式对接 | P1 |
| `requestShopeeWaveBind` Javadoc | Shopee 波次绑定接口正式契约对接 | **P0** |
| `handleFlowPickChangeWaveFailure` Javadoc | 换波失败完整回滚（订单池恢复/取消任务/AGV 回库） | P1 |
