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

17 KiB
Raw Permalink Blame History

决策管理模块 — 策略链执行器架构设计

一、旧代码问题分析

1.1 当前 outBoundSectDiv / inBoundSectDiv 的代码

// 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 作为初始输入

outBoundSectDivnull 作为初始列表传给第一个策略处理器。

FIFORuleHandler 为例,它完全忽略输入参数 list,在方法内部自行查询库存:

// 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) 全量扫描

Map<String, Decisioner> decisionerMap = SpringContextHolder.getBeansOfType(Decisioner.class);

每次调用都从 Spring 容器中扫描所有 Decisioner 类型的 Bean旧项目中有 10+ 个),然后从中取一个用。这相当于每次都把整个工具箱倒出来只为了拿一把螺丝刀。

正确做法:通过 Bean 名称精确查找。

// 应该这样做 — 按名称精确获取
DecisionManage<?, ?> decisioner = (DecisionManage<?, ?>) applicationContext.getBean("fifo");

问题三:原生类型 List 丢失泛型信息

List list = null;                          // 原生类型
list = decisioner.handler(list, param);    // 隐式不安全转换
return list;                               // 返回值也是原生类型

Decisioner<T, P> 是泛型类但使用时完全退化为原生类型。编译器无法检查类型安全FIFO 返回 List<StrategyStructMaterialVO> 但赋值给 List,再返回为 List<StrategyStructMaterialVO> —— 全靠程序员记忆保证不出错。

问题四:策略链编排逻辑重复

inBoundSectDivoutBoundSectDiv 中策略链的加载、遍历、执行、空检查逻辑完全重复,只有初始数据加载方式不同:

// 两段代码的重复部分
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 ...; }
}

问题五:空检查时机有逻辑漏洞

list = decisioner.handler(list, param);
if (CollectionUtils.isEmpty(list)){
    throw new BadRequestException("当前策略" + ... + "无可用货位,分配前数量:" + list.size());
}

异常消息中的 分配前数量 实际是分配后的数量handler 已经执行完了),且 "分配前" 语义模糊。另外,如果某个处理器确实应该返回空列表(如过滤后无符合条件的结果),这里直接抛异常阻止了整个链,没有给后续处理器降级处理的机会。

问题六:无可观测性

  • 无执行耗时记录(仅 limitStoragealleyAve 内部有手动计时)
  • 无输入/输出数量变化追踪
  • 异常时只有简单的消息,无堆栈上下文

二、新架构设计

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

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 调用方的使用方式

入库和出库的调用代码变得简洁统一:

// 入库分配
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:创建策略处理器

@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:在数据库注册策略配置

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()

// 例如:盘点分配
public List<Structattr> inventoryDiv(String sectionCode) {
    List<Structattr> candidates = loadInventorySlots(sectionCode);
    return strategyChainExecutor.execute(sectionCode, "3", candidates, new JSONObject());
}

七、实施建议

  1. 先实现 StrategyChainExecutor,它是整个架构的枢纽,不依赖任何具体策略
  2. 迁移现有策略处理器:旧项目的 FIFORuleHandlerNearbyRuleHandler 等改为继承新的 DecisionManage<T, P>handler 方法签名基本不变
  3. 调整 Service 层调用:将 StructattrServiceImpl 中的策略遍历代码替换为对 StrategyChainExecutor.execute() 的调用
  4. 数据库映射:旧表 st_sect_strategy → 新表 wms_warehouse_strategy,旧表 st_strategy_config → 新表 wms_warehouse_strategy_config,字段映射见之前的字段分析文档