为什么我们需要一个统一的算费流程。
我在第二家公司的时候,从从事的是医疗领域的电商,当时我们的优惠相关的计算嵌入在订单的代码里面。
public abstract class OrderSubmitHandler {
/**
* 1. 初始化订单信息
* init: storeVO,orderMainDO,orderSubDOList,freeFlag
*/
public abstract OrderSubmitDTO initOrder(OrderSubmitRequest request);
/**
* 2. 组装优惠券优惠,同时初始化实物订单的order_sub
*/
public abstract void handleCoupon(OrderSubmitDTO result);
/**
* 3. DTC + VIP
*/
public abstract void handleDTCAndVIP(OrderSubmitDTO result);
/**
* 4. 计算orderMain金额
*/
public abstract void computeMainOrderPay(OrderSubmitDTO result);
/**
* 5. 插入订单主表、子订单、快照信息、支付单信息
*/
public abstract void insertDB(OrderSubmitDTO result);
/**
* 6. 生成预约信息
*/
public abstract void insertReserve(OrderSubmitDTO result);
/**
* 7. 生成返回值
*/
public abstract OrderAddResponse getResponse(OrderSubmitDTO result);
/**
* 提交流程
* */
@Transactional
public OrderAddResponse submitOrder(OrderSubmitRequest request){
log.info("request:{}",JSON.toJSONString(request));
//1. 初始化
OrderSubmitDTO result = this.initOrder(request);
log.info( "initOrder:{}", JSON.toJSONString(result) );
//2. 组装优惠券优惠
this.handleCoupon(result);
log.info( "handleCoupon:{}", JSON.toJSONString(result) );
//先计算dtc,vip只优惠首次支持金额的88折,即7%的88折扣
//3. DTC + vip的优惠
this.handleDTCAndVIP(result);
log.info( "handleDTCAndVIP:{}", JSON.toJSONString(result) );
//4. 计算order_main金额
this.computeMainOrderPay(result);
//this.handleVipCoupon(result);
//5. 插入订单主表、子订单、快照信息、支付单信息
this.insertDB(result);
//6. 如果免费单,生成预约信息
this.insertReserve(result);
OrderAddResponse response = this.getResponse(result);
return response ;
}
}
每当优惠扩展的时候我们不得不增加流程或者增加优惠计算的代码,由于优惠代码又是平铺式的编写,导致后期复杂的代码难以维护,可读写性也大大降低。
于是我希望设计这样一段代码,每一个扣减逻辑是一个单独的算子,可以分开实现,费用的计算可以由多个算子组成,算子可以排序实现任意流程的编排。算子需要足够的抽象,不止支持优惠卷,还是支持其他抵扣的虚拟货币。我希望可以保存每种计算方式的金额,这样就可以在订单里展示优惠详情,每一项抵扣了多少金额。
设计
首先我们需要设计一个算子接口
public interface FeeCalculate<O> {
/**
* 根据费用项计算每个费用项明细
*
* @param list
* @return
*/
Map<FeeItemType, List<PayItem>> payItemList(List<FeeItem<O>> list);
/**
* 每个支付方式waitPay
*
* @param list
* @return
*/
Map<FeeItemType, BigDecimal> calculateWaitPay(List<FeeItem<O>> list);
/**
* 获取计算器的唯一编码
*
* @return
*/
Unique getUnique();
}
其中FeeItem代表一个单独的费用项,必须邮费,打包费之类的。
public interface FeeItem<O> {
/**
* 原始金额
*
* @return
*/
BigDecimal getFeeItemOriginMoney();
/**
* 费用类型
*
* @return
*/
FeeItemType getFeeItemType();
/**
* 获取订单原始信息
*
* @return
*/
O getOrderInfo();
}
不用的费用需要不同的计算方式,所以我们需要不同的type来表示
/**
* 为了区分不同费用不同计算方式
*/
public enum FeeItemType implements BaseEnum<FeeItemType> {
SERVICE_FEE(1, "服务费"),
ELECTRIC_FEE(2, "电费"),
OVER_WEIGHT_FEE(3, "超重费"),
OVER_TIME_FEE(4, "超时费");
FeeItemType(Integer code, String name) {
this.code = code;
this.name = name;
}
private Integer code;
private String name;
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getName() {
return this.name;
}
public static Optional<FeeItemType> of(Integer code) {
return Optional.ofNullable(BaseEnum.parseByCode(FeeItemType.class, code));
}
}
不同的费用由不同的方式支付或者抵扣
public interface PayItem {
BigDecimal getMoney();
PayGroup getPayGroup();
PayType getPayType();
}
PayGroup代表不同的支付方式
public enum PayGroup implements BaseEnum<PayGroup> {
THIRD_PAY(1, "三方支付"),
PLATFORM_PAY(2, "平台支付"),
VIRTUAL_PROPERTY(3, "虚拟资产"),
BANK(4, "银行卡支付"),
COUPON(4, "优惠劵");
PayGroup(Integer code, String name) {
this.code = code;
this.name = name;
}
private Integer code;
private String name;
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getName() {
return this.name;
}
public static Optional<PayGroup> of(Integer code) {
return Optional.ofNullable(BaseEnum.parseByCode(PayGroup.class, code));
}
}
PayType代表具体的支付类型,比如三方支付下的微信支付。
public enum PayType implements BaseEnum<PayType> {
WECHAT(1, "微信支付"),
ALIPAY(2,"支付宝"),
COIN(3,"虚拟币"),
ACTIVITY(4,"活动")
;
PayType(Integer code, String name) {
this.code = code;
this.name = name;
}
private Integer code;
private String name;
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getName() {
return this.name;
}
public static Optional<PayType> of(Integer code) {
return Optional.ofNullable(BaseEnum.parseByCode(PayType.class, code));
}
}
在上面的需求中,我希望算子只需要实现自己的业务逻辑,不需要关注算子的编排和具体的执行逻辑,当前算子需要上一个算子的计算结果,所以我需要使用装饰器模式来增强算子的能力,并使用责任链链接多个算子,规划算子执行的流程。
public abstract class AbstractCalculator<O> implements FeeCalculate<O> {
private final FeeCalculate<O> feeCalculate;
private final Unique unique;
protected AbstractCalculator(FeeCalculate<O> feeCalculate, Unique unique) {
this.feeCalculate = feeCalculate;
this.unique = unique;
}
@Override
public Unique getUnique() {
return unique;
}
/**
* 当前抵扣
*/
protected abstract Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left, O o);
/**
* 当前抵扣的明细
*/
protected abstract Map<FeeItemType, List<PayItem>> payItemList();
@Override
public Map<FeeItemType, List<PayItem>> payItemList(List<FeeItem<O>> list) {
//初始化算子的支付项
Map<FeeItemType, List<PayItem>> map;
if (Objects.nonNull(feeCalculate) && Objects.nonNull(feeCalculate.payItemList(list))) {
map = feeCalculate.payItemList(list);
} else {
map = Maps.newHashMap();
}
//获取已经存在的支付项
Map<FeeItemType, List<PayItem>> currentList = payItemList();
//合并算子付费项
if (Objects.nonNull(currentList) && !currentList.isEmpty()) {
currentList.forEach((key, value) -> {
List<PayItem> tempList = map.getOrDefault(key, Lists.newArrayList());
tempList.addAll(value);
map.put(key, tempList);
});
}
return map;
}
@Override
public Map<FeeItemType, BigDecimal> calculateWaitPay(List<FeeItem<O>> list) {
//如果没有上层包装,那么直接返回订单的实际金额减去当前抵扣的金额
if (Iterables.isEmpty(list)) {
//计费项为空
throw new RuntimeException(FeeEnum.FEE_ITEM_EMPTY.getName());
}
Map<FeeItemType, BigDecimal> leftMap = Maps.newHashMap();
if (Objects.isNull(feeCalculate)) {
for (FeeItem<O> item : list) {
leftMap.put(item.getFeeItemType(), item.getFeeItemOriginMoney());
}
Map<FeeItemType, BigDecimal> currentDeduct = currentPayItem(leftMap,
list.get(0).getOrderInfo());
//合并费用
currentDeduct.forEach(
(key, value) -> leftMap.put(key, NumberUtil.sub(leftMap.get(key), value))
);
return leftMap;
} else {
//存在下一个算子,流程未完
Map<FeeItemType, BigDecimal> left = feeCalculate.calculateWaitPay(list);
//如果有任何一个
Optional<BigDecimal> greaterThanZero = left.values().stream()
.toList().stream()
//过滤出所有不为零的费用
.filter(s -> NumberUtil.isGreater(s, BigDecimal.ZERO))
.findFirst();
//算子无抵扣项直接返回
if (greaterThanZero.isEmpty()) {
return left;
}
Map<FeeItemType, BigDecimal> current = currentPayItem(left, list.get(0).getOrderInfo());
Map<FeeItemType, BigDecimal> temp = Maps.newHashMap();
for (FeeItem<O> item : list) {
//如果当前有抵扣
if (Objects.nonNull(current.get(item.getFeeItemType()))) {
//超过剩余支付金额,抛出异常
if (NumberUtil.isGreater(current.get(item.getFeeItemType()),
left.get(item.getFeeItemType()))) {
throw new RuntimeException(FeeEnum.AMOUNT_GREATER_ERROR.getName());
}
//正常抵扣
temp.put(item.getFeeItemType(),
NumberUtil.sub(left.get(item.getFeeItemType()), current.get(item.getFeeItemType())));
} else {
//如果当前没有抵扣,直接返回剩余金额
temp.put(item.getFeeItemType(), left.get(item.getFeeItemType()));
}
}
return temp;
}
}
}
测试用例
1.当前订单是一个简单的服务费支付订单,用户参加了一个活动,使用了一个优惠券,希望计算出需要支付的实际金额.
初始化抵扣算子
/**
* 服务费抵扣活动
*/
public class ActivityCalculator extends AbstractCalculator<OrderInfo> {
public ActivityCalculator(FeeCalculate<OrderInfo> feeCalculate) {
super(feeCalculate, CalculateType.ACTIVITY);
}
/**
* 算子抵扣金额
*/
@Override
protected Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left,
OrderInfo o) {
Map<FeeItemType, BigDecimal> map = Maps.newHashMap();
map.put(FeeItemType.SERVICE_FEE, new BigDecimal("4"));
System.out.println("活动抵扣了4元费用");
return map;
}
@Override
protected Map<FeeItemType, List<PayItem>> payItemList() {
Map<FeeItemType, List<PayItem>> map = Maps.newHashMap();
List<PayItem> payItems = Lists.newArrayList();
ActivityPayItem ap = new ActivityPayItem(new BigDecimal(4));
ap.setActivityName("节日活动");
payItems.add(ap);
map.put(FeeItemType.SERVICE_FEE, payItems);
return map;
}
}
/**
* 付费用优惠券
*/
public class CouponCalculator extends AbstractCalculator<OrderInfo> {
public CouponCalculator(FeeCalculate<OrderInfo> feeCalculate) {
super(feeCalculate, CalculateType.COUPON);
}
@Override
protected Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left,
OrderInfo o) {
Map<FeeItemType, BigDecimal> map = Maps.newHashMap();
map.put(FeeItemType.SERVICE_FEE, new BigDecimal("5"));
System.out.println("劵抵扣了5元费用");
return map;
}
@Override
protected Map<FeeItemType, List<PayItem>> payItemList() {
Map<FeeItemType, List<PayItem>> map = Maps.newHashMap();
List<PayItem> payItems = Lists.newArrayList();
CouponPayItem cp = new CouponPayItem(new BigDecimal(5));
cp.setCouponCode("C1234528");
payItems.add(cp);
map.put(FeeItemType.SERVICE_FEE, payItems);
return map;
}
}
初始化抵扣项
public class ActivityPayItem extends AbstractPayItem {
public ActivityPayItem(BigDecimal money) {
super(money, PayType.ACTIVITY, PayGroup.COUPON);
}
private String activityName;
}
public class CouponPayItem extends AbstractPayItem {
public CouponPayItem(BigDecimal money) {
super(money, PayType.COIN, PayGroup.COUPON);
}
private String couponCode;
private String source;
}
一个普通的例子
public class CalculateTest {
public static void main(String[] args) {
//初始化一个简单的订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTradeFlowNo("T0323423432");
orderInfo.setOrderType("普通订单");
orderInfo.setPayAmount(new BigDecimal(0));
orderInfo.setServiceFee(new BigDecimal(20));
//初始化服务费用
List<FeeItem<OrderInfo>> list = Lists.newArrayList();
list.add(new ServiceFeeItem(orderInfo, FeeItemType.SERVICE_FEE, orderInfo.getServiceFee()));
//编排算子
FeeCalculate<OrderInfo> feeCalculate = new ActivityCalculator(new CouponCalculator(null));
//获取待支付金额
Map<FeeItemType, BigDecimal> leftPay = feeCalculate.calculateWaitPay(list);
leftPay.forEach((k, v) -> {
System.out.println("待支付项:" + k.getName() + v.toPlainString() + "元");
});
//抵扣项展示
Map<FeeItemType, List<PayItem>> payItemList = feeCalculate.payItemList(list);
payItemList.forEach((k, v) -> {
StringBuffer sb = new StringBuffer();
v.forEach(p -> {
sb.append("支付类型:").append(p.getPayType().getName());
sb.append("支付金额:").append(p.getMoney()).append("元");
sb.append(" ! ").append("\n");
});
System.out.println("已经抵扣:\n" + k.getName()+"\n" + sb);
});
}
}
一个复杂的例子
当我们希望把一些计算的规则配置暴露给运营人员,当运营配置好规则然后查询出来生成算子。
规则相关
public interface FeeRule {
/**
* 获取配置的数值
* @return
*/
BigDecimal getConfigValue();
/**
* 获取规则类型
* @return
*/
FeeRuleType getRuleType();
/**
* 规则的顺序
* @return
*/
Integer getOrder();
}
一个简单的工厂类用来生成算子
public class CalculatorFactory {
public static FeeCalculate<OrderInfo> getFeeCalculateByRuleType(FeeCalculate<OrderInfo> calculate, FeeRule rule) {
if (Objects.equals(FeeRuleType.FREE_TIME, rule.getRuleType())) {
FreeTimeRule time = (FreeTimeRule) rule;//这里可以强制转化
return new FreeTimeCalculator(calculate, CalculatorType.FREE_TIME, time.getConfigValue().intValue());
} else if (Objects.equals(FeeRuleType.FREE_TIMES, rule.getRuleType())) {
FreeTimesRule timesRule = (FreeTimesRule) rule;
return new FreeTimesCalculator(calculate, CalculatorType.FREE_TIMES, timesRule.getConfigValue().intValue());
} else if (Objects.equals(FeeRuleType.PLUS_RULE, rule.getRuleType())) {
//不需要可以不转
return new PlusRuleCalculator(calculate, CalculatorType.PLUS_DISCOUNT, rule.getConfigValue());
} else if (Objects.equals(FeeRuleType.MAX_LIMIT, rule.getRuleType())) {
return new MaxLimitCalculator(calculate, CalculatorType.MAX_LIMIT, rule.getConfigValue());
}
return null;
}
}
编写流程
public class FeeCalculateTest {
public void testFee() {
//初始化规则
List<FeeRule> ruleList = Lists.newArrayList();
FreeTimesRule freeTimesRule = new FreeTimesRule(new BigDecimal(0), FeeRuleType.FREE_TIMES, 3);
FreeTimeRule freeTimeRule = new FreeTimeRule(new BigDecimal(1), FeeRuleType.FREE_TIME, 1);
PlusRule plusRule = new PlusRule(new BigDecimal("0.95"), FeeRuleType.PLUS_RULE, 4);
MaxLimitRule maxLimitRule = new MaxLimitRule(new BigDecimal("1.4"), FeeRuleType.MAX_LIMIT, 5);
ruleList.add(freeTimesRule);
ruleList.add(freeTimeRule);
ruleList.add(plusRule);
ruleList.add(maxLimitRule);
//排序规则
List<FeeRule> sortRules = ruleList.stream().sorted(Comparator.comparingInt(FeeRule::getOrder))
.toList();
//初始化支付项
List<FeeItem<OrderInfo>> payItemList = Lists.newArrayList();
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCarNo("dddd");
orderInfo.setParkTimes(3);
orderInfo.setUserId(4L);
orderInfo.setTotalMoney(new BigDecimal("30"));
ParkingFeeItem parkingFeeItem = new ParkingFeeItem(orderInfo);
payItemList.add(parkingFeeItem);
//核心流程
FeeCalculate<OrderInfo> calculate = null;
for (FeeRule feeRule : sortRules) {
//根据规则类型获取对应的计算器类型,生成FeeCalculate
calculate = CalculatorFactory.getFeeCalculateByRuleType(calculate, feeRule);
}
//计算费用
assert calculate != null;
Map<FeeItemType, BigDecimal> waitPay = calculate.calculateWaitPay(payItemList);
BigDecimal waitPayMoney = waitPay.get(FeeItemType.SERVICE_FEE);
System.out.println("待支付金额" + waitPayMoney);
Map<FeeItemType, List<PayItem>> map = calculate.payItemList(payItemList);
MapUtils.debugPrint(System.out, "console", map);
List<PayItem> payList = map.get(FeeItemType.SERVICE_FEE);
payList.forEach(payItem -> {
System.out.println(payItem.getMoney());
System.out.println(payItem.getPayType());
System.out.println(payItem.getPayGroup());
});
}
public static void main(String[] args) {
FeeCalculateTest test = new FeeCalculateTest();
test.testFee();
}
}
写在最后
完整代码
MyTest/src/main/java/com/fee at master · wty4427300/MyTest (github.com)使用的设计模式
装饰器
工厂
责任链