什么是计费?
计费是结算系统的核心业务,主要承担各业务系统推送过来的计费需求,是收单流程的重要一环。
业务系统推送业务单据到结算系统以后,结算系统按照计费规则计算各项费用并进行输出,这个过程称为计费。
目前系统主要支持的费用项有货款、物流费、平台佣金、仓储费、推广费、售后费、VAT税、清关费等,本文以货款、物流费、平台佣金为例阐述计费优化的思路和具体实践。
为什么要优化?
总结来说,业务上计费科目越来越多,难以快速支持;技术上代码臃肿,可读性差,稳定性不高,难以维护。
计费作为结算系统的基础服务,在系统建立初期就已存在,跟大多数系统一样,早期业务简单,没有必要过度设计,随着系统规模见长,各种需求、逻辑掺杂进来,代码基本都写在一个大类中,很难完全理解业务。并且有些不在计费范围内的业务代码,例如清分和通知上下游,也耦合在计费模块中,业务边界混杂。
计费功能核心流程现状
计费功能核心代码现状
/**
* 订单计费服务
*/
public class OrderBillingService {
private Locker locker;
/**
* 计费
*
* @param order 订单
* @param event 订单事件
*/
public void billing(OrderDTO order, OrderEvent event) {
// 1.预校验
if (!"ORDER_SIGN".equals(event.getEventType())) {
return;
}
// 2.并发检查
if (!locker.tryLock(order.getOrderNumber())) {
return;
}
// 3.幂等检查
BillingOrder billingOrder = getByOrderNumber(order.getOrderNumber());
if (billingOrder != null) {
return;
}
// 4.模型转换
BillingDTO billingDTO = translate(order);
MerchantDTO seller = getSeller(order.getSellerId());
// 5.加载计费规则
Contract contract = loadBillingContract(billingDTO, seller);
Promotion promotion = loadBillingPromotion(billingDTO, seller);
// 6.开始结算计费
BigDecimal orderAmount = billingDTO.getOrderAmount(); // 订单金额
BigDecimal billingAmount = BigDecimal.ZERO;
BigDecimal logisticsFeeTotal = billingDTO.getLogisticsFee(); // 订单物流费
List<BillingSkuDTO> orderSkuList = billingDTO.getSkuList(); //计费明细
for (BillingSkuDTO billingSkuDTO : orderSkuList) {
BigDecimal skuBillingAmount = billingSkuDTO.getAmount(); // 明细计费金额
// 6.1.计算物流费
BigDecimal logisticsFee = billingSkuDTO.getAmount()
.divide(orderAmount, 6, BigDecimal.ROUND_HALF_UP)
.multiply(logisticsFeeTotal); // 示例按金额占比分摊物流费,实际分摊比这要复杂
FeeDTO logisticsFeeDTO = new FeeDTO();
logisticsFeeDTO.setFeeType("logistics");
logisticsFeeDTO.setFee(logisticsFee);
logisticsFeeDTO.setAmount(logisticsFee);
billingSkuDTO.addFee(logisticsFeeDTO);
skuBillingAmount = skuBillingAmount.subtract(logisticsFee);
// 6.2.计算佣金
BigDecimal commissionFeeRate = contract.getCommissionFeeRate();
BigDecimal commissionFee = billingSkuDTO.getAmount().multiply(commissionFeeRate);
FeeDTO commissionFeeDTO = new FeeDTO();
commissionFeeDTO.setFeeType("commission");
commissionFeeDTO.setFee(commissionFee);
commissionFeeDTO.setAmount(commissionFee);
billingSkuDTO.addFee(commissionFeeDTO);
skuBillingAmount = skuBillingAmount.subtract(commissionFee);
// 6.3.计算货款
billingSkuDTO.setBillingAmount(skuBillingAmount);
}
// 7.开始优惠计费
for (BillingSkuDTO billingSkuDTO : orderSkuList) {
BigDecimal skuBillingAmount = billingSkuDTO.getBillingAmount(); // 明细计费金额
List<FeeDTO> feeDTOList = billingSkuDTO.getFeeList();
for (FeeDTO feeDTO : feeDTOList) {
// 该费用可优惠
if (promotion.isPromotion(feeDTO, billingDTO)) {
// 获取优惠金额
BigDecimal promotionFee = promotion.getPromotionFee(feeDTO, billingDTO);
feeDTO.setPromotion(promotionFee);
feeDTO.setAmount(feeDTO.getAmount().subtract(promotionFee));
skuBillingAmount = skuBillingAmount.add(promotionFee);
}
}
// 重新计算货款
billingSkuDTO.setBillingAmount(skuBillingAmount);
billingAmount = billingAmount.add(skuBillingAmount);
}
billingDTO.setBillingAmount(billingAmount);
// 8.持久化
billingOrder = save(billingDTO);
// 9.清分
clearing(billingOrder);
// 10.计费完成
publish(billingOrder);
}
以上代码做了很大的简化和提炼,但是仍不难发现其中存在的问题。入口的 billing 方法过于复杂,既有校验代码,又有核心计费逻辑,还有计费后的持久化,以及计费职责范围以外的清分、回调等。虽然多数逻辑做了抽离,但是整体逻辑仍然太重,职责很多,相当臃肿,导致计费核心很难扩展。
此次重构的目标就是解决以上问题,让核心逻辑高度内聚,可插拔易扩展,流程可编排、节点可复用,同时逻辑跟其他模块松解耦、不粘连。
重构思路和目标
计费流程节点清晰,各个节点业务逻辑比较容易划分边界,所以重构思路上以大类拆分、节点复用为主,比如预校验、模型转换、持久化和清分这几个步骤比较通用,其他各业务计费步骤在逻辑上比较内聚,这两种情况都可以抽象出节点,通过责任链进行编排、重构。
针对上述提到面临的问题,提出以下目标。
易扩展:可扩展性强,流程节点可插拔、可调整,不用大幅改动即可快速支持新需求。 好运维:代码结构清晰,容易理解,出现问题好定位。
使用责任链重构
责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
计费类图
定义处理者接口,拆分实现具体的处理者。其中费用计算作为计费整体流程中的一环,自身逻辑内聚,职责单一且又有固定的流程,单独拆分出一个子链出来。
重构后的代码
// 流程配置
public class BillingHandlerChainConfig {
public BillingHandlerChain getOrderBillingHandlerChain() {
BillingHandlerChain orderChain = new OrderBillingHandlerChain();
List<BillingHandler> handlers = new ArrayList<>();
// 1.校验
handlers.add(new PreCheckBillingHandler());
// 2.模型转换
handlers.add(new TranslateBillingHandler());
// 3.计费规则
handlers.add(new RuleBillingHandler());
// 4.计费
CalculateBillingHandler calculateBillingHandler = new CalculateBillingHandler();
handlers.add(calculateBillingHandler);
List<BillingHandler> calculateHandlers = new ArrayList<>();
calculateBillingHandler.setHandlers(calculateHandlers);
// 4.1.物流费计算
calculateHandlers.add(new LogisticsBillingHandler());
// 4.2.佣金计算
calculateHandlers.add(new CommissionBillingHandler());
// 4.3.优惠计算
calculateHandlers.add(new PromotionBillingHandler());
// 4.4.货款计算
calculateHandlers.add(new GoodsBillingHandler());
// 5.持久化
handlers.add(new SaveBillingHandler());
// 6.发布事件
handlers.add(new PublishBillingHandler());
orderChain.setHandlers(handlers);
return orderChain;
}
}
// 计费服务入口
public class OrderBillingService {
private BillingHandlerChain orderBillingHandlerChain;
private void billing(OrderDTO order, OrderEvent event) {
BillingContext context = initContext(order, event);
orderBillingHandlerChain.handle(context);
}
}
// 订单计费主链
public class OrderBillingHandlerChain implements BillingHandlerChain {
private List<BillingHandler> handlers;
@Override
public void setHandlers(List<BillingHandler> handlers) {
this.handlers = handlers;
}
@Override
public boolean handle(BillingContext context) {
if (handlers == null) {
throw new IllegalStateException("订单计费没有可用流程");
}
for (BillingHandler handler : handlers) {
if (!handler.handle(context)) {
return false;
}
}
return true;
}
}
// 计费处理器/计费子链
public class CalculateBillingHandler implements BillingHandler, BillingHandlerChain {
private List<BillingHandler> handlers;
@Override
public void setHandlers(List<BillingHandler> handlers) {
this.handlers = handlers;
}
@Override
public boolean handle(BillingContext context) {
if (handlers == null) {
throw new IllegalStateException("计费子链没有可用流程");
}
for (BillingHandler handler : handlers) {
if (!handler.handle(context)) {
return false;
}
}
return true;
}
}
// 物流费处理器
public class LogisticsBillingHandler implements BillingHandler {
@Override
public boolean handle(BillingContext context) {
BillingDTO billingDTO = context.getBillingOrder();
BigDecimal orderAmount = billingDTO.getOrderAmount(); // 订单金额
BigDecimal logisticsFeeTotal = billingDTO.getLogisticsFee(); // 订单物流费
List<BillingSkuDTO> orderSkuList = billingDTO.getSkuList(); //计费明细
for (BillingSkuDTO billingSkuDTO : orderSkuList) {
BigDecimal skuBillingAmount = billingSkuDTO.getBillingAmount(); // 明细计费金额
BigDecimal logisticsFee = billingSkuDTO.getAmount()
.divide(orderAmount, 6, BigDecimal.ROUND_HALF_UP)
.multiply(logisticsFeeTotal); // 示例按金额占比分摊物流费,实际分摊比这要复杂
FeeDTO logisticsFeeDTO = new FeeDTO();
logisticsFeeDTO.setFeeType("logistics");
logisticsFeeDTO.setFee(logisticsFee);
logisticsFeeDTO.setAmount(logisticsFee);
billingSkuDTO.addFee(logisticsFeeDTO);
skuBillingAmount = skuBillingAmount.subtract(logisticsFee);
billingSkuDTO.setBillingAmount(skuBillingAmount);
}
return true;
}
}
// 优惠处理器
public class PromotionBillingHandler implements BillingHandler {
@Override
public boolean handle(BillingContext context) {
BillingDTO billingDTO = context.getBillingOrder();
List<BillingSkuDTO> orderSkuList = billingDTO.getSkuList(); //计费明细
Promotion promotion = context.getPromotion();
// 开始优惠计费
for (BillingSkuDTO billingSkuDTO : orderSkuList) {
BigDecimal skuBillingAmount = billingSkuDTO.getBillingAmount(); // 明细计费金额
List<FeeDTO> feeDTOList = billingSkuDTO.getFeeList();
for (FeeDTO feeDTO : feeDTOList) {
// 该费用可优惠
if (promotion.isPromotion(feeDTO, billingDTO)) {
// 获取优惠金额
BigDecimal promotionFee = promotion.getPromotionFee(feeDTO, billingDTO);
feeDTO.setPromotion(promotionFee);
feeDTO.setAmount(feeDTO.getAmount().subtract(promotionFee));
skuBillingAmount = skuBillingAmount.add(promotionFee);
}
}
billingSkuDTO.setBillingAmount(skuBillingAmount);
}
return true;
}
}
原来逻辑中的清分、计费完成通知上游业务两个环节,本质上不在计费业务的范围内,只是通过计费完成来触发的其他业务,在此次改造中完全独立出去,改由计费的最后一环「发布计费完成事件」来驱动,保持计费业务高内聚。
一些思考
腐化产生的原因
代码腐化是不可避免的,只能尽量减轻。
腐化产生的原因很多,可能是缺乏良好的设计,一开始就没做好规划,后面业务代码堆砌,就更加混乱,难以维护了。本文所针对的计费模块,就是这个问题。
也可能是缺乏良好的实践,虽然有好的设计和编码标准,但是规约没有执行到位,也是出现腐化的一个重要原因。 当然,这两种原因也都有一些客观因素存在,比如初期业务简单,没必要大搞设计,或者为了需求快速上线,做了一些妥协,成了技术债务。再比如人员更替,技术水平和认知各不相同,后来者也可能不会遵照之前的约定。
如何减轻腐化
腐化带来开发效率的降低和系统风险的提升,在实践中可以从下面几点来减轻腐化带来的影响。
制定规约
建立包括编码标准,代码审查和单元测试相关的规约,定期总结执行情况,养成防腐的习惯。
定期重构
针对已经腐化的代码,定期重构,清理无效代码,去除冗余,改善代码结构,增强可读性,提高可扩展性,降低维护成本。
评论( 0 )