Files
huachuang/决策管理模块-策略链执行器架构设计.md
2026-07-18 17:38:34 +08:00

394 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# 决策管理模块 — 策略链执行器架构设计
## 一、旧代码问题分析
### 1.1 当前 `outBoundSectDiv` / `inBoundSectDiv` 的代码
```java
// StructattrServiceImpl.java (旧项目)
public List<StrategyStructMaterialVO> outBoundSectDiv(StrategyStructParam param) {
StSectStrategy one = iStSectStrategyService.getOne(...);
List<String> strategy = one.getStrategy();
Map<String, Decisioner> decisionerMap = SpringContextHolder.getBeansOfType(Decisioner.class);
List list = null;
for (String decisionerType : strategy) {
Decisioner decisioner = decisionerMap.get(decisionerType);
list = decisioner.handler(list, param);
if (CollectionUtils.isEmpty(list)){
throw new BadRequestException("...");
}
}
return list;
}
```
### 1.2 逐个问题分析
#### 问题一:`List list = null` 作为初始输入
`outBoundSectDiv``null` 作为初始列表传给第一个策略处理器。
`FIFORuleHandler` 为例,它**完全忽略输入参数 `list`**,在方法内部自行查询库存:
```java
// FIFORuleHandler.handler() — 注意 list 参数从未被使用
public List<StrategyStructMaterialVO> handler(List<StrategyStructMaterialVO> list, StrategyStructParam param) {
// 自行查询库存,不依赖入参 list
List<StructattrVechielDto> vechielDtos = iStructattrService.collectVechicle(...);
// ... 处理并返回
}
```
这导致了**两种不一致的初始数据加载模式**
| 方法 | 初始数据来源 | 首个处理器行为 |
|------|------------|-------------|
| `inBoundSectDiv` | Service 层查询所有可用货位 → 传入链 | 处理器接收实际数据,进行筛选/排序 |
| `outBoundSectDiv` | 传 `null` | 首个处理器FIFO自行查询库存 |
**根因**:策略链没有统一的数据加载阶段,初始数据的获取逻辑散落在 Service 层和各个处理器之间。
#### 问题二:`SpringContextHolder.getBeansOfType(Decisioner.class)` 全量扫描
```java
Map<String, Decisioner> decisionerMap = SpringContextHolder.getBeansOfType(Decisioner.class);
```
**每次调用**都从 Spring 容器中扫描**所有** `Decisioner` 类型的 Bean旧项目中有 10+ 个),然后从中取一个用。这相当于每次都把整个工具箱倒出来只为了拿一把螺丝刀。
**正确做法**:通过 Bean 名称精确查找。
```java
// 应该这样做 — 按名称精确获取
DecisionManage<?, ?> decisioner = (DecisionManage<?, ?>) applicationContext.getBean("fifo");
```
#### 问题三:原生类型 `List` 丢失泛型信息
```java
List list = null; // 原生类型
list = decisioner.handler(list, param); // 隐式不安全转换
return list; // 返回值也是原生类型
```
`Decisioner<T, P>` 是泛型类但使用时完全退化为原生类型。编译器无法检查类型安全FIFO 返回 `List<StrategyStructMaterialVO>` 但赋值给 `List`,再返回为 `List<StrategyStructMaterialVO>` —— 全靠程序员记忆保证不出错。
#### 问题四:策略链编排逻辑重复
`inBoundSectDiv``outBoundSectDiv` 中策略链的加载、遍历、执行、空检查逻辑**完全重复**,只有初始数据加载方式不同:
```java
// 两段代码的重复部分
StSectStrategy one = iStSectStrategyService.getOne(...); // 查策略链配置
List<String> strategy = one.getStrategy(); // 解析策略列表
Map<String, Decisioner> decisionerMap = ...; // 获取所有Bean
for (String decisionerType : strategy) { // 遍历执行
list = decisioner.handler(list, param);
if (CollectionUtils.isEmpty(list)) { throw ...; }
}
```
#### 问题五:空检查时机有逻辑漏洞
```java
list = decisioner.handler(list, param);
if (CollectionUtils.isEmpty(list)){
throw new BadRequestException("当前策略" + ... + "无可用货位,分配前数量:" + list.size());
}
```
异常消息中的 `分配前数量` 实际是**分配后**的数量handler 已经执行完了),且 "分配前" 语义模糊。另外,如果某个处理器确实应该返回空列表(如过滤后无符合条件的结果),这里直接抛异常阻止了整个链,没有给后续处理器降级处理的机会。
#### 问题六:无可观测性
- 无执行耗时记录(仅 `limitStorage``alleyAve` 内部有手动计时)
- 无输入/输出数量变化追踪
- 异常时只有简单的消息,无堆栈上下文
---
## 二、新架构设计
### 2.1 核心思想
引入 **`StrategyChainExecutor`**(策略链执行器)作为独立的编排层,将策略链的加载、查找、执行、监控统一封装,与业务 Service 解耦。
### 2.2 架构分层
```
┌─────────────────────────────────────────────────────────┐
│ Controller 层 │
│ WarehouseStrategyController / 调度任务入口 │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ Service 层 │
│ XxxServiceImpl │
│ 职责:加载初始候选数据(可用货位 / 库存记录) │
│ 调用 StrategyChainExecutor 执行策略链 │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ StrategyChainExecutor新增   
│ 职责:① 查询库区策略链配置 │
│ ② 按 Bean 名称精确获取策略处理器 │
│ ③ 按序执行策略链 │
│ ④ 统一日志、耗时、异常处理 │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ DecisionManage<T, P>(策略处理器) │
│ 职责:单一维度的货位筛选/排序逻辑 │
└─────────────────────────────────────────────────────────┘
```
### 2.3 类职责矩阵
| 类 | 职责 | 不该做的事 |
|----|------|-----------|
| **StrategyChainExecutor** | 编排策略链:查配置 → 找Bean → 依次执行 → 日志监控 | 不加载初始数据,不关心业务类型 |
| **DecisionManage<T,P>** | 实现单一策略逻辑(筛选/排序) | 不查策略链配置,不关心其他策略 |
| **XxxServiceImpl** | 加载初始候选数据,调用执行器 | 不直接操作策略Bean不遍历策略链 |
| **WarehouseStrategyDO** | 库区→策略链的映射关系 | — |
| **WarehouseStrategyConfigDO** | 单个策略的参数配置 | — |
---
## 三、核心类设计
### 3.1 StrategyChainExecutor
```java
package cn.code.nl.module.wms.manage;
import cn.code.nl.framework.common.exception.ServiceException;
import cn.code.nl.module.wms.dal.dataobject.warehousestrategy.WarehouseStrategyDO;
import cn.code.nl.module.wms.dal.mysql.warehousestrategy.WarehouseStrategyMapper;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 策略链执行器,负责按库区配置的策略链依次调用策略处理器
*
* @Author: liyongde
* @Date: 2026/7/18
*/
@Slf4j
@Component
public class StrategyChainExecutor {
@Resource
private WarehouseStrategyMapper warehouseStrategyMapper;
@Resource
private ApplicationContext applicationContext;
/**
* 执行策略链
* <p>
* 根据库区编码和策略类型加载策略链配置,从 Spring 容器中按名称查找对应的策略处理器,
* 按序执行,每个处理器的输出作为下一个处理器的输入。
*
* @param sectionCode 库区编码
* @param strategyType 策略类型1=入库2=出库)
* @param candidates 初始候选列表(入库时为可用货位列表,出库时传空列表由首个策略自行查询)
* @param param 决策参数(物料信息、出入库类型等)
* @param <T> 货位/库存数据类型
* @return 策略链执行后的结果列表
*/
@SuppressWarnings("unchecked")
public <T> List<T> execute(String sectionCode, String strategyType,
List<T> candidates, JSONObject param) {
// 1. 查询库区策略链配置
WarehouseStrategyDO strategy = warehouseStrategyMapper.selectOne(
WarehouseStrategyDO::getSectionCode, sectionCode,
WarehouseStrategyDO::getStrategyType, strategyType);
if (strategy == null) {
throw new ServiceException(500, "当前库区 " + sectionCode + " 未配置策略链");
}
// 2. 解析策略编码列表
List<String> strategyCodes = JSON.parseArray(strategy.getStrategy(), String.class);
if (CollUtil.isEmpty(strategyCodes)) {
throw new ServiceException(500, "当前库区 " + sectionCode + " 策略链为空");
}
// 3. 按序执行策略链
List<T> result = candidates;
for (String strategyCode : strategyCodes) {
DecisionManage<T, JSONObject> handler;
try {
handler = (DecisionManage<T, JSONObject>) applicationContext.getBean(strategyCode);
} catch (Exception e) {
log.error("策略 [{}] 未找到对应的处理器 Bean", strategyCode);
throw new ServiceException(500, "策略 " + strategyCode + " 未注册");
}
String strategyName = handler.strategyConfig.getStrategyName();
int inputSize = result != null ? result.size() : 0;
log.info("执行策略 [{}]{},输入候选数量:{}", strategyCode, strategyName, inputSize);
long startTime = System.currentTimeMillis();
result = handler.handler(result, param);
long cost = System.currentTimeMillis() - startTime;
int outputSize = result != null ? result.size() : 0;
log.info("策略 [{}] 执行完成,耗时:{}ms输出数量{}(减少:{}",
strategyCode, cost, outputSize, inputSize - outputSize);
if (CollUtil.isEmpty(result)) {
throw new ServiceException(500,
"策略 " + strategyName + " 执行后无可用货位");
}
}
return result;
}
}
```
### 3.2 调用方的使用方式
入库和出库的调用代码变得简洁统一:
```java
// 入库分配
public List<Structattr> inBoundSectDiv(StrategyStructParam param) {
// 1. 加载初始候选数据(可用货位)
List<Structattr> candidates = structattrMapper.selectList(
new LambdaQueryWrapperX<Structattr>()
.eq(Structattr::getIsUsed, true)
.eq(Structattr::getLockType, "未锁定")
.eq(Structattr::getSectCode, param.getSectCode()));
if (CollUtil.isEmpty(candidates)) {
throw new ServiceException(500, "库区 " + param.getSectCode() + " 无可用货位");
}
// 2. 执行策略链
JSONObject params = (JSONObject) JSON.toJSON(param);
return strategyChainExecutor.execute(
param.getSectCode(), "1", candidates, params);
}
// 出库分配
public List<StrategyStructMaterialVO> outBoundSectDiv(StrategyStructParam param) {
// 出库的首个策略FIFO自行查询库存传入空列表即可
JSONObject params = (JSONObject) JSON.toJSON(param);
return strategyChainExecutor.execute(
param.getSectCode(), "2", null, params);
}
```
---
## 四、新旧代码对比
| 维度 | 旧代码 | 新架构 |
|------|--------|--------|
| **编排职责** | 散落在 `StructattrServiceImpl` 中,与 CRUD 逻辑混在一起 | 独立的 `StrategyChainExecutor` 组件 |
| **Bean 查找** | `getBeansOfType()` 全量扫描 | `applicationContext.getBean(name)` 精确查找 |
| **类型安全** | 原生 `List`,无泛型约束 | `@SuppressWarnings("unchecked")` 显式标注,调用方强类型 |
| **日志** | 无统一日志,部分处理器内部有 | 每个策略执行前后记录:名称、耗时、输入/输出数量 |
| **空值处理** | 错误消息写"分配前"实际是"分配后" | 明确区分输入/输出数量 |
| **代码复用** | `inBoundSectDiv` / `outBoundSectDiv` 中策略遍历逻辑重复 | 统一由 `execute()` 处理 |
| **异常处理** | `BadRequestException`,无处理器查找失败的兜底 | `ServiceException`,区分"未配置策略链"和"处理器未注册" |
| **扩展性** | 新增分配类型需复制整段遍历代码 | 只需调用 `execute()` + 准备初始数据 |
---
## 五、数据流示意
### 入库流程
```
调用方:
① 查询库区所有可用货位(未锁定、未占用)
② 得到 List<Structattr>(如 200 个)
StrategyChainExecutor.execute(sectionCode, "1", candidates, param):
③ 查 wms_warehouse_strategy → strategy = ["limitStorage","nearby","alleyAve"]
④ 依次执行:
ctx.getBean("limitStorage") → handler(200个, param) → 80个过滤尺寸不匹配
ctx.getBean("nearby") → handler(80个, param) → 10个按距离取最近
ctx.getBean("alleyAve") → handler(10个, param) → 10个巷道均衡排序
⑤ 返回最终 10 个货位
```
### 出库流程
```
调用方:
① 不预加载数据,传 null
StrategyChainExecutor.execute(sectionCode, "2", null, param):
③ 查 wms_warehouse_strategy → strategy = ["fifo"]
④ 依次执行:
ctx.getBean("fifo") → handler(null, param) → FIFO内部查库存 → 5个分配结果
⑤ 返回 5 个库存分配记录
```
---
## 六、扩展指南
### 新增一个策略
**Step 1**:创建策略处理器
```java
@Service("myStrategy") // Bean 名称 = 策略编码
@Slf4j
public class MyStrategyHandler extends DecisionManage<Structattr, JSONObject> {
@Override
public List<Structattr> handler(List<Structattr> list, JSONObject param) {
// 从配置读取参数
String configParam = this.strategyConfig.getParam();
// 实现筛选/排序逻辑
return filteredList;
}
}
```
**Step 2**:在数据库注册策略配置
```sql
INSERT INTO wms_warehouse_strategy_config
(strategy_code, strategy_name, strategy_type, class_type, param, is_used)
VALUES ('myStrategy', '我的策略', '1', 'diy', '{"threshold": 10}', 1);
```
**Step 3**:在库区策略链中加入新策略
`wms_warehouse_strategy.strategy` JSON 数组中追加 `"myStrategy"` 即可,无需修改任何 Java 代码。
### 新增一个分配场景
只需在 Service 层加载对应初始数据,调用 `strategyChainExecutor.execute()`
```java
// 例如:盘点分配
public List<Structattr> inventoryDiv(String sectionCode) {
List<Structattr> candidates = loadInventorySlots(sectionCode);
return strategyChainExecutor.execute(sectionCode, "3", candidates, new JSONObject());
}
```
---
## 七、实施建议
1. **先实现 `StrategyChainExecutor`**,它是整个架构的枢纽,不依赖任何具体策略
2. **迁移现有策略处理器**:旧项目的 `FIFORuleHandler``NearbyRuleHandler` 等改为继承新的 `DecisionManage<T, P>``handler` 方法签名基本不变
3. **调整 Service 层调用**:将 `StructattrServiceImpl` 中的策略遍历代码替换为对 `StrategyChainExecutor.execute()` 的调用
4. **数据库映射**:旧表 `st_sect_strategy` → 新表 `wms_warehouse_strategy`,旧表 `st_strategy_config` → 新表 `wms_warehouse_strategy_config`,字段映射见之前的字段分析文档