Files
huachuang/决策管理模块-策略链执行器架构设计.md

394 lines
17 KiB
Markdown
Raw Permalink Normal View History

2026-07-18 17:38:34 +08:00
# 决策管理模块 — 策略链执行器架构设计
## 一、旧代码问题分析
### 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`,字段映射见之前的字段分析文档