# 决策管理模块 — 策略链执行器架构设计 ## 一、旧代码问题分析 ### 1.1 当前 `outBoundSectDiv` / `inBoundSectDiv` 的代码 ```java // StructattrServiceImpl.java (旧项目) public List outBoundSectDiv(StrategyStructParam param) { StSectStrategy one = iStSectStrategyService.getOne(...); List strategy = one.getStrategy(); Map 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 handler(List list, StrategyStructParam param) { // 自行查询库存,不依赖入参 list List vechielDtos = iStructattrService.collectVechicle(...); // ... 处理并返回 } ``` 这导致了**两种不一致的初始数据加载模式**: | 方法 | 初始数据来源 | 首个处理器行为 | |------|------------|-------------| | `inBoundSectDiv` | Service 层查询所有可用货位 → 传入链 | 处理器接收实际数据,进行筛选/排序 | | `outBoundSectDiv` | 传 `null` | 首个处理器(FIFO)自行查询库存 | **根因**:策略链没有统一的数据加载阶段,初始数据的获取逻辑散落在 Service 层和各个处理器之间。 #### 问题二:`SpringContextHolder.getBeansOfType(Decisioner.class)` 全量扫描 ```java Map 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` 是泛型类,但使用时完全退化为原生类型。编译器无法检查类型安全,FIFO 返回 `List` 但赋值给 `List`,再返回为 `List` —— 全靠程序员记忆保证不出错。 #### 问题四:策略链编排逻辑重复 `inBoundSectDiv` 和 `outBoundSectDiv` 中策略链的加载、遍历、执行、空检查逻辑**完全重复**,只有初始数据加载方式不同: ```java // 两段代码的重复部分 StSectStrategy one = iStSectStrategyService.getOne(...); // 查策略链配置 List strategy = one.getStrategy(); // 解析策略列表 Map 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(策略处理器) │ │ 职责:单一维度的货位筛选/排序逻辑 │ └─────────────────────────────────────────────────────────┘ ``` ### 2.3 类职责矩阵 | 类 | 职责 | 不该做的事 | |----|------|-----------| | **StrategyChainExecutor** | 编排策略链:查配置 → 找Bean → 依次执行 → 日志监控 | 不加载初始数据,不关心业务类型 | | **DecisionManage** | 实现单一策略逻辑(筛选/排序) | 不查策略链配置,不关心其他策略 | | **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; /** * 执行策略链 *

* 根据库区编码和策略类型加载策略链配置,从 Spring 容器中按名称查找对应的策略处理器, * 按序执行,每个处理器的输出作为下一个处理器的输入。 * * @param sectionCode 库区编码 * @param strategyType 策略类型(1=入库,2=出库) * @param candidates 初始候选列表(入库时为可用货位列表,出库时传空列表由首个策略自行查询) * @param param 决策参数(物料信息、出入库类型等) * @param 货位/库存数据类型 * @return 策略链执行后的结果列表 */ @SuppressWarnings("unchecked") public List execute(String sectionCode, String strategyType, List candidates, JSONObject param) { // 1. 查询库区策略链配置 WarehouseStrategyDO strategy = warehouseStrategyMapper.selectOne( WarehouseStrategyDO::getSectionCode, sectionCode, WarehouseStrategyDO::getStrategyType, strategyType); if (strategy == null) { throw new ServiceException(500, "当前库区 " + sectionCode + " 未配置策略链"); } // 2. 解析策略编码列表 List strategyCodes = JSON.parseArray(strategy.getStrategy(), String.class); if (CollUtil.isEmpty(strategyCodes)) { throw new ServiceException(500, "当前库区 " + sectionCode + " 策略链为空"); } // 3. 按序执行策略链 List result = candidates; for (String strategyCode : strategyCodes) { DecisionManage handler; try { handler = (DecisionManage) 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 inBoundSectDiv(StrategyStructParam param) { // 1. 加载初始候选数据(可用货位) List candidates = structattrMapper.selectList( new LambdaQueryWrapperX() .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 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(如 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 { @Override public List handler(List 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 inventoryDiv(String sectionCode) { List candidates = loadInventorySlots(sectionCode); return strategyChainExecutor.execute(sectionCode, "3", candidates, new JSONObject()); } ``` --- ## 七、实施建议 1. **先实现 `StrategyChainExecutor`**,它是整个架构的枢纽,不依赖任何具体策略 2. **迁移现有策略处理器**:旧项目的 `FIFORuleHandler`、`NearbyRuleHandler` 等改为继承新的 `DecisionManage`,`handler` 方法签名基本不变 3. **调整 Service 层调用**:将 `StructattrServiceImpl` 中的策略遍历代码替换为对 `StrategyChainExecutor.execute()` 的调用 4. **数据库映射**:旧表 `st_sect_strategy` → 新表 `wms_warehouse_strategy`,旧表 `st_strategy_config` → 新表 `wms_warehouse_strategy_config`,字段映射见之前的字段分析文档