為什么我們需要一個(gè)統(tǒng)一的算費(fèi)流程醒第。
我在第二家公司的時(shí)候琐鲁,從從事的是醫(yī)療領(lǐng)域的電商韩玩,當(dāng)時(shí)我們的優(yōu)惠相關(guān)的計(jì)算嵌入在訂單的代碼里面剥槐。
public abstract class OrderSubmitHandler {
/**
* 1. 初始化訂單信息
* init: storeVO,orderMainDO朦乏,orderSubDOList,freeFlag
*/
public abstract OrderSubmitDTO initOrder(OrderSubmitRequest request);
/**
* 2. 組裝優(yōu)惠券優(yōu)惠,同時(shí)初始化實(shí)物訂單的order_sub
*/
public abstract void handleCoupon(OrderSubmitDTO result);
/**
* 3. DTC + VIP
*/
public abstract void handleDTCAndVIP(OrderSubmitDTO result);
/**
* 4. 計(jì)算orderMain金額
*/
public abstract void computeMainOrderPay(OrderSubmitDTO result);
/**
* 5. 插入訂單主表球及、子訂單、快照信息呻疹、支付單信息
*/
public abstract void insertDB(OrderSubmitDTO result);
/**
* 6. 生成預(yù)約信息
*/
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. 組裝優(yōu)惠券優(yōu)惠
this.handleCoupon(result);
log.info( "handleCoupon:{}", JSON.toJSONString(result) );
//先計(jì)算dtc吃引,vip只優(yōu)惠首次支持金額的88折,即7%的88折扣
//3. DTC + vip的優(yōu)惠
this.handleDTCAndVIP(result);
log.info( "handleDTCAndVIP:{}", JSON.toJSONString(result) );
//4. 計(jì)算order_main金額
this.computeMainOrderPay(result);
//this.handleVipCoupon(result);
//5. 插入訂單主表、子訂單际歼、快照信息、支付單信息
this.insertDB(result);
//6. 如果免費(fèi)單姑蓝,生成預(yù)約信息
this.insertReserve(result);
OrderAddResponse response = this.getResponse(result);
return response ;
}
}
每當(dāng)優(yōu)惠擴(kuò)展的時(shí)候我們不得不增加流程或者增加優(yōu)惠計(jì)算的代碼鹅心,由于優(yōu)惠代碼又是平鋪式的編寫(xiě),導(dǎo)致后期復(fù)雜的代碼難以維護(hù)纺荧,可讀寫(xiě)性也大大降低旭愧。
于是我希望設(shè)計(jì)這樣一段代碼,每一個(gè)扣減邏輯是一個(gè)單獨(dú)的算子宙暇,可以分開(kāi)實(shí)現(xiàn)输枯,費(fèi)用的計(jì)算可以由多個(gè)算子組成,算子可以排序?qū)崿F(xiàn)任意流程的編排占贫。算子需要足夠的抽象桃熄,不止支持優(yōu)惠卷,還是支持其他抵扣的虛擬貨幣型奥。我希望可以保存每種計(jì)算方式的金額瞳收,這樣就可以在訂單里展示優(yōu)惠詳情,每一項(xiàng)抵扣了多少金額厢汹。
設(shè)計(jì)
首先我們需要設(shè)計(jì)一個(gè)算子接口
public interface FeeCalculate<O> {
/**
* 根據(jù)費(fèi)用項(xiàng)計(jì)算每個(gè)費(fèi)用項(xiàng)明細(xì)
*
* @param list
* @return
*/
Map<FeeItemType, List<PayItem>> payItemList(List<FeeItem<O>> list);
/**
* 每個(gè)支付方式waitPay
*
* @param list
* @return
*/
Map<FeeItemType, BigDecimal> calculateWaitPay(List<FeeItem<O>> list);
/**
* 獲取計(jì)算器的唯一編碼
*
* @return
*/
Unique getUnique();
}
其中FeeItem代表一個(gè)單獨(dú)的費(fèi)用項(xiàng)螟深,必須郵費(fèi),打包費(fèi)之類(lèi)的烫葬。
public interface FeeItem<O> {
/**
* 原始金額
*
* @return
*/
BigDecimal getFeeItemOriginMoney();
/**
* 費(fèi)用類(lèi)型
*
* @return
*/
FeeItemType getFeeItemType();
/**
* 獲取訂單原始信息
*
* @return
*/
O getOrderInfo();
}
不用的費(fèi)用需要不同的計(jì)算方式,所以我們需要不同的type來(lái)表示
/**
* 為了區(qū)分不同費(fèi)用不同計(jì)算方式
*/
public enum FeeItemType implements BaseEnum<FeeItemType> {
SERVICE_FEE(1, "服務(wù)費(fèi)"),
ELECTRIC_FEE(2, "電費(fèi)"),
OVER_WEIGHT_FEE(3, "超重費(fèi)"),
OVER_TIME_FEE(4, "超時(shí)費(fèi)");
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));
}
}
不同的費(fèi)用由不同的方式支付或者抵扣
public interface PayItem {
BigDecimal getMoney();
PayGroup getPayGroup();
PayType getPayType();
}
PayGroup代表不同的支付方式
public enum PayGroup implements BaseEnum<PayGroup> {
THIRD_PAY(1, "三方支付"),
PLATFORM_PAY(2, "平臺(tái)支付"),
VIRTUAL_PROPERTY(3, "虛擬資產(chǎn)"),
BANK(4, "銀行卡支付"),
COUPON(4, "優(yōu)惠劵");
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代表具體的支付類(lèi)型界弧,比如三方支付下的微信支付。
public enum PayType implements BaseEnum<PayType> {
WECHAT(1, "微信支付"),
ALIPAY(2,"支付寶"),
COIN(3,"虛擬幣"),
ACTIVITY(4,"活動(dòng)")
;
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));
}
}
在上面的需求中搭综,我希望算子只需要實(shí)現(xiàn)自己的業(yè)務(wù)邏輯垢箕,不需要關(guān)注算子的編排和具體的執(zhí)行邏輯,當(dāng)前算子需要上一個(gè)算子的計(jì)算結(jié)果兑巾,所以我需要使用裝飾器模式來(lái)增強(qiáng)算子的能力舰讹,并使用責(zé)任鏈鏈接多個(gè)算子,規(guī)劃算子執(zhí)行的流程闪朱。
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;
}
/**
* 當(dāng)前抵扣
*/
protected abstract Map<FeeItemType, BigDecimal> currentPayItem(Map<FeeItemType, BigDecimal> left, O o);
/**
* 當(dāng)前抵扣的明細(xì)
*/
protected abstract Map<FeeItemType, List<PayItem>> payItemList();
@Override
public Map<FeeItemType, List<PayItem>> payItemList(List<FeeItem<O>> list) {
//初始化算子的支付項(xiàng)
Map<FeeItemType, List<PayItem>> map;
if (Objects.nonNull(feeCalculate) && Objects.nonNull(feeCalculate.payItemList(list))) {
map = feeCalculate.payItemList(list);
} else {
map = Maps.newHashMap();
}
//獲取已經(jīng)存在的支付項(xiàng)
Map<FeeItemType, List<PayItem>> currentList = payItemList();
//合并算子付費(fèi)項(xiàng)
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) {
//如果沒(méi)有上層包裝月匣,那么直接返回訂單的實(shí)際金額減去當(dāng)前抵扣的金額
if (Iterables.isEmpty(list)) {
//計(jì)費(fèi)項(xiàng)為空
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());
//合并費(fèi)用
currentDeduct.forEach(
(key, value) -> leftMap.put(key, NumberUtil.sub(leftMap.get(key), value))
);
return leftMap;
} else {
//存在下一個(gè)算子,流程未完
Map<FeeItemType, BigDecimal> left = feeCalculate.calculateWaitPay(list);
//如果有任何一個(gè)
Optional<BigDecimal> greaterThanZero = left.values().stream()
.toList().stream()
//過(guò)濾出所有不為零的費(fèi)用
.filter(s -> NumberUtil.isGreater(s, BigDecimal.ZERO))
.findFirst();
//算子無(wú)抵扣項(xiàng)直接返回
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) {
//如果當(dāng)前有抵扣
if (Objects.nonNull(current.get(item.getFeeItemType()))) {
//超過(guò)剩余支付金額奋姿,拋出異常
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 {
//如果當(dāng)前沒(méi)有抵扣锄开,直接返回剩余金額
temp.put(item.getFeeItemType(), left.get(item.getFeeItemType()));
}
}
return temp;
}
}
}
測(cè)試用例
1.當(dāng)前訂單是一個(gè)簡(jiǎn)單的服務(wù)費(fèi)支付訂單,用戶(hù)參加了一個(gè)活動(dòng)称诗,使用了一個(gè)優(yōu)惠券萍悴,希望計(jì)算出需要支付的實(shí)際金額.
初始化抵扣算子
/**
* 服務(wù)費(fèi)抵扣活動(dòng)
*/
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("活動(dòng)抵扣了4元費(fèi)用");
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("節(jié)日活動(dòng)");
payItems.add(ap);
map.put(FeeItemType.SERVICE_FEE, payItems);
return map;
}
}
/**
* 付費(fèi)用優(yōu)惠券
*/
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元費(fèi)用");
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;
}
}
初始化抵扣項(xiàng)
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;
}
一個(gè)普通的例子
public class CalculateTest {
public static void main(String[] args) {
//初始化一個(gè)簡(jiǎn)單的訂單
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTradeFlowNo("T0323423432");
orderInfo.setOrderType("普通訂單");
orderInfo.setPayAmount(new BigDecimal(0));
orderInfo.setServiceFee(new BigDecimal(20));
//初始化服務(wù)費(fèi)用
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("待支付項(xiàng):" + k.getName() + v.toPlainString() + "元");
});
//抵扣項(xiàng)展示
Map<FeeItemType, List<PayItem>> payItemList = feeCalculate.payItemList(list);
payItemList.forEach((k, v) -> {
StringBuffer sb = new StringBuffer();
v.forEach(p -> {
sb.append("支付類(lèi)型:").append(p.getPayType().getName());
sb.append("支付金額:").append(p.getMoney()).append("元");
sb.append(" ! ").append("\n");
});
System.out.println("已經(jīng)抵扣:\n" + k.getName()+"\n" + sb);
});
}
}
一個(gè)復(fù)雜的例子
當(dāng)我們希望把一些計(jì)算的規(guī)則配置暴露給運(yùn)營(yíng)人員,當(dāng)運(yùn)營(yíng)配置好規(guī)則然后查詢(xún)出來(lái)生成算子。
規(guī)則相關(guān)
public interface FeeRule {
/**
* 獲取配置的數(shù)值
* @return
*/
BigDecimal getConfigValue();
/**
* 獲取規(guī)則類(lèi)型
* @return
*/
FeeRuleType getRuleType();
/**
* 規(guī)則的順序
* @return
*/
Integer getOrder();
}
一個(gè)簡(jiǎn)單的工廠類(lèi)用來(lái)生成算子
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;//這里可以強(qiáng)制轉(zhuǎn)化
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())) {
//不需要可以不轉(zhuǎn)
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;
}
}
編寫(xiě)流程
public class FeeCalculateTest {
public void testFee() {
//初始化規(guī)則
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);
//排序規(guī)則
List<FeeRule> sortRules = ruleList.stream().sorted(Comparator.comparingInt(FeeRule::getOrder))
.toList();
//初始化支付項(xiàng)
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) {
//根據(jù)規(guī)則類(lèi)型獲取對(duì)應(yīng)的計(jì)算器類(lèi)型癣诱,生成FeeCalculate
calculate = CalculatorFactory.getFeeCalculateByRuleType(calculate, feeRule);
}
//計(jì)算費(fèi)用
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();
}
}
寫(xiě)在最后
完整代碼
MyTest/src/main/java/com/fee at master · wty4427300/MyTest (github.com)使用的設(shè)計(jì)模式
裝飾器
工廠
責(zé)任鏈