试算模型
试算模型是根据商品和用户的属性,计算出最终的优惠价格的模型。大致工作流程如下:
- 获取商品信息:从数据库中获取商品的基本信息,如原价、来源和渠道等。
- 获取用户信息:从数据库中获取用户的基本信息,如是否潜在用户、往期购买记录等。
- 计算优惠:根据商品和用户的信息和预设的优惠规则,计算出最终的优惠价格。
- 返回结果:将计算结果返回给前端,供用户查看。
多分支规则树
多分支规则树是一种用于表示复杂决策逻辑的树形结构。每个节点代表一个决策点,有apply
和get
方法,分别用于执行策略和获取下一个策略处理器,这样的节点组成的数据结构就是规则树,不同的分支可以代表不同的决策路径,通过这种方式,可以清晰地表达出不同条件下的处理逻辑,便于后续的维护和扩展。
在拼团交易平台中,多分支规则树可以用于实现复杂的优惠策略。例如,根据用户的购买历史、当前商品的属性等信息,动态调整优惠力度和适用范围。
下图为项目中多分支规则树的结构示意图:

基础实现
两个最基层的接口StrategyMapper
和StrategyHandler
分别负责策略的映射和执行。
其中 <T, D, R>
分别代表请求参数(requestParams)、动态上下文(dynamicContext)和响应结果(response)
public interface StrategyMapper<T, D, R> {
/**
* 获取下一个策略处理器
* @param requestParameter 请求参数
* @param dynamicContext 动态上下文
* @return 策略处理器
*/
StrategyHandler<T, D, R> get(T requestParameter, D dynamicContext);
}
public interface StrategyHandler<T, D, R> {
/**
* 执行策略
* @param requestParameter 请求参数
* @param dynamicContext 动态上下文
* @return 响应结果 R
*/
R apply(T requestParameter, D dynamicContext) throws Exception;
}
让AbstractStrategyRouter
实现StrategyMapper
和StrategyHandler
接口,并作为多分支规则树的基础实现,提供了一个通用的路由方法router
,用于根据请求参数和动态上下文获取对应的策略处理器,并执行相应的逻辑。
AbstractMultiThreadStrategyRouter
相比AbstractStrategyRouter
的区别是支持多线程并发处理。
因为在处理业务逻辑时,大部分时间花在了数据加载上,而不是业务流程受理,使用多线程异步加载数据可以显著提高处理效率。
public abstract class AbstractStrategyRouter<T, D, R> implements StrategyMapper<T, D, R>, StrategyHandler<T, D, R> {
@Getter
@Setter
protected StrategyHandler<T, D, R> defaultStrategyHandler = StrategyHandler.DEFAULT;
public R router(T requestParameter, D dynamicContext) throws Exception {
StrategyHandler<T, D, R> strategyHandler = get(requestParameter, dynamicContext);
if(null != strategyHandler) return strategyHandler.apply(requestParameter, dynamicContext);
return defaultStrategyHandler.apply(requestParameter, dynamicContext);
}
}
public abstract class AbstractMultiThreadStrategyRouter<T, D, R> implements StrategyMapper<T, D, R>, StrategyHandler<T, D, R> {
@Getter
@Setter
protected StrategyHandler<T, D, R> defaultStrategyHandler = StrategyHandler.DEFAULT;
public R router(T requestParameter, D dynamicContext) throws Exception {
// 获取策略处理器
StrategyHandler<T, D, R> strategyHandler = get(requestParameter, dynamicContext);
// 如果策略处理器不为空,则执行策略
if(null != strategyHandler) return strategyHandler.apply(requestParameter, dynamicContext);
// 否则执行默认策略处理器
return defaultStrategyHandler.apply(requestParameter, dynamicContext);
}
@Override
public R apply(T requestParameter, D dynamicContext) throws Exception {
// 异步加载数据
multiThread(requestParameter, dynamicContext);
// 业务流程受理
return doApply(requestParameter, dynamicContext);
}
// 继承者需要实现以下方法
/**
* 异步加载数据
*/
protected abstract void multiThread(T requestParameter, D dynamicContext) throws ExecutionException, InterruptedException, TimeoutException;
/**
* 业务流程受理
*/
protected abstract R doApply(T requestParameter, D dynamicContext) throws Exception;
}
以上为多分支规则树的基本结构和接口定义。
业务实现
接着需要对上文定义的基类进行初步实现,包括输入输出和业务逻辑节点。
模型输入输出实体
开头说到试算模型需要根据商品和用户的属性来计算最终的优惠价格
项目中使用了MarketProductEntity
来作为输入(也就是上文提到的T
)
public class MarketProductEntity {
/** 用户ID */
private String userId;
/** 商品ID */
private String goodsId;
/** 渠道 */
private String source;
/** 来源 */
private String channel;
}
其次使用类TrialBalanceEntity
作为模型的输出(也就是上文提到的R
)
public class TrialBalanceEntity {
/** 商品ID */
private String goodsId;
/** 商品名称 */
private String goodsName;
/** 原始价格 */
private BigDecimal originalPrice;
/** 折扣价格 */
private BigDecimal deductionPrice;
/** 拼团目标数量 */
private Integer targetCount;
/** 拼团开始时间 */
private Date startTime;
/** 拼团结束时间 */
private Date endTime;
/** 是否可见拼团 */
private Boolean isVisible;
/** 是否可参与进团 */
private Boolean isEnable;
}
业务逻辑基本节点及其工厂
使用AbstractGroupBuyMarketSupport
类作为实现规则树的业务基类
所有的业务逻辑类都会继承AbstractGroupBuyMarketSupport
public abstract class AbstractGroupBuyMarketSupport<MarketProductEntity, DynamicContext, TrialBalanceEntity> extends AbstractMultiThreadStrategyRouter<cn.bugstack.domain.activity.model.entity.MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, cn.bugstack.domain.activity.model.entity.TrialBalanceEntity> {
// 资源请求超时时间
protected long timeout = 500;
// 获取营销活动数据的句柄
@Resource
protected IActivityRepository repository;
@Override
protected void multiThread(cn.bugstack.domain.activity.model.entity.MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws ExecutionException, InterruptedException, TimeoutException {
// 缺省的方法 考虑到不是所有的业务实现都需要异步加载数据
}
}
那么现在试算模型在业务实现中实际上就是一个以AbstractGroupBuyMarketSupport
为基类的节点,根据输入MarketProductEntity
计算得到TrialBalanceEntity
。
下文的DefaultActivityStrategyFactory
类就是一个工厂类,用于创建试算模型的根节点。
内部定义了DynamicContext
类,用于存储在多线程中需要共享的数据。
@Service
public class DefaultActivityStrategyFactory {
// 具体逻辑的根节点 模型的入口
private final RootNode rootNode;
// 构造函数 自动注入根节点
public DefaultActivityStrategyFactory(RootNode rootNode) {
this.rootNode = rootNode;
}
public StrategyHandler<MarketProductEntity, DynamicContext, TrialBalanceEntity> strategyHandler() {
return rootNode;
}
/**
* 动态上下文类 用于存储在多线程中需要共享的数据
*/
public static class DynamicContext {
// 拼团活动营销配置值对象
private GroupBuyActivityDiscountVO groupBuyActivityDiscountVO;
// 商品信息
private SkuVO skuVO;
// 折扣价格
private BigDecimal deductionPrice;
}
}
具体业务逻辑
图中有说明 实际业务逻辑是从RootNode
开始,进入SwitchNode
判断活动资格,最终通过MarketNode
或不通过MarketNode
完成优惠的计算,最终在EndNode
返回TrialBalanceEntity
。
RootNode
负责参数的校验
@Slf4j
@Service
public class RootNode extends AbstractGroupBuyMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private SwitchNode switchNode;
// 处理
@Override
protected TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("拼团商品查询试算服务-RootNode userId:{} requestParameter:{}", requestParameter.getUserId(), JSON.toJSONString(requestParameter));
// 参数判断
if (StringUtils.isBlank(requestParameter.getUserId()) || StringUtils.isBlank(requestParameter.getGoodsId()) ||
StringUtils.isBlank(requestParameter.getSource()) || StringUtils.isBlank(requestParameter.getChannel())) {
throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo());
}
// 会调用get 并执行get返回的handler的apply方法 实际上是递归进入switchNode的doApply方法
return router(requestParameter, dynamicContext);
}
// 获取下一个节点
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return switchNode;
}
}
SwitchNode
判断用户是否有资格享受优惠
@Slf4j
@Service
public class SwitchNode extends AbstractGroupBuyMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private MarketNode marketNode;
// 处理
@Override
public TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 可以做一些预处理 根据userId和goodsId查询用户是否有资格享受优惠
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return marketNode;
}
}
MarketNode
根据用户的资格和商品信息计算优惠价格
MarketNode
属于营销的核心业务逻辑,下文代码比较复杂,只需知道执行的逻辑是 multiThread
->doApply
的顺序。
@Slf4j
@Service
public class MarketNode extends AbstractGroupBuyMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private ThreadPoolExecutor threadPoolExecutor;
/**
* <a href="https://bugstack.cn/md/road-map/spring-dependency-injection.html">Spring 注入详细说明</a>
* 会根据Service("xx")注入相应的折扣计算服务 策略模式
* key为xx 折扣策略
* value为IDiscountCalculateService实现类 具体的策略逻辑
*/
@Resource
private Map<String, IDiscountCalculateService> discountCalculateServiceMap;
@Resource
private EndNode endNode;
@Resource
private ErrorNode errorNode;
// 异步加载数据 包括活动的配置、商品信息等
@Override
protected void multiThread(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws ExecutionException, InterruptedException, TimeoutException {
// 异步查询活动配置
QueryGroupBuyActivityDiscountVOThreadTask queryGroupBuyActivityDiscountVOThreadTask = new QueryGroupBuyActivityDiscountVOThreadTask(requestParameter.getSource(), requestParameter.getChannel(), requestParameter.getGoodsId(), repository);
FutureTask<GroupBuyActivityDiscountVO> groupBuyActivityDiscountVOFutureTask = new FutureTask<>(queryGroupBuyActivityDiscountVOThreadTask);
threadPoolExecutor.execute(groupBuyActivityDiscountVOFutureTask);
// 异步查询商品信息 - 在实际生产中,商品有同步库或者调用接口查询。这里暂时使用DB方式查询。
QuerySkuVOFromDBThreadTask querySkuVOFromDBThreadTask = new QuerySkuVOFromDBThreadTask(requestParameter.getGoodsId(), repository);
FutureTask<SkuVO> skuVOFutureTask = new FutureTask<>(querySkuVOFromDBThreadTask);
threadPoolExecutor.execute(skuVOFutureTask);
// 写入上下文 - 对于一些复杂场景,获取数据的操作,有时候会在下N个节点获取,这样前置查询数据,可以提高接口响应效率
dynamicContext.setGroupBuyActivityDiscountVO(groupBuyActivityDiscountVOFutureTask.get(timeout, TimeUnit.MINUTES));
dynamicContext.setSkuVO(skuVOFutureTask.get(timeout, TimeUnit.MINUTES));
log.info("拼团商品查询试算服务-MarketNode userId:{} 异步线程加载数据「GroupBuyActivityDiscountVO、SkuVO」完成", requestParameter.getUserId());
}
@Override
public TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("拼团商品查询试算服务-MarketNode userId:{} requestParameter:{}", requestParameter.getUserId(), JSON.toJSONString(requestParameter));
// 获取上下文数据
GroupBuyActivityDiscountVO groupBuyActivityDiscountVO = dynamicContext.getGroupBuyActivityDiscountVO();
if (null == groupBuyActivityDiscountVO) {
return router(requestParameter, dynamicContext);
}
GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount = groupBuyActivityDiscountVO.getGroupBuyDiscount();
SkuVO skuVO = dynamicContext.getSkuVO();
if (null == groupBuyDiscount || null == skuVO) {
return router(requestParameter, dynamicContext);
}
// 优惠试算
IDiscountCalculateService discountCalculateService = discountCalculateServiceMap.get(groupBuyDiscount.getMarketPlan());
if (null == discountCalculateService) {
log.info("不存在{}类型的折扣计算服务,支持类型为:{}", groupBuyDiscount.getMarketPlan(), JSON.toJSONString(discountCalculateServiceMap.keySet()));
throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0001.getInfo());
}
// 折扣价格
BigDecimal deductionPrice = discountCalculateService.calculate(requestParameter.getUserId(), skuVO.getOriginalPrice(), groupBuyDiscount);
dynamicContext.setDeductionPrice(deductionPrice);
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 不存在配置的拼团活动,走异常节点
if (null == dynamicContext.getGroupBuyActivityDiscountVO() || null == dynamicContext.getSkuVO() || null == dynamicContext.getDeductionPrice()) {
return errorNode;
}
return endNode;
}
}
EndNode
负责封装TrialBalanceEntity
并返回
@Slf4j
@Service
public class EndNode extends AbstractGroupBuyMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Override
public TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("拼团商品查询试算服务-EndNode userId:{} requestParameter:{}", requestParameter.getUserId(), JSON.toJSONString(requestParameter));
GroupBuyActivityDiscountVO groupBuyActivityDiscountVO = dynamicContext.getGroupBuyActivityDiscountVO();
SkuVO skuVO = dynamicContext.getSkuVO();
// 折扣价格
BigDecimal deductionPrice = dynamicContext.getDeductionPrice();
// 将折扣价格和其他信息封装成TrialBalanceEntity
return TrialBalanceEntity.builder()
.goodsId(skuVO.getGoodsId())
.goodsName(skuVO.getGoodsName())
.originalPrice(skuVO.getOriginalPrice())
.deductionPrice(deductionPrice)
.targetCount(groupBuyActivityDiscountVO.getTarget())
.startTime(groupBuyActivityDiscountVO.getStartTime())
.endTime(groupBuyActivityDiscountVO.getEndTime())
.isVisible(false)
.isEnable(false)
.build();
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return defaultStrategyHandler;
}
}