由于一次需求变更,由于的业务流程不支持这种新的场景,这也意味着需要设计支持新场景的业务逻辑处理。但是又要考虑后期的扩展性,同时尽可能不影响原来的业务,需要分析利弊寻找一种技术设计方案来解决当前需要解决的问题。通过分析,勾勒出涉及的变动点,这些变动点如果在原有代码新增if else就会造成程序的可维护性很差,而且不利于后期场景复用,因此寻找一种策略+分发的处理机制,通过上下文封装场景,然后通过调度器来分发处理器完成处理,同时把稳定的能力下沉到能力层,跟业务场景有关的通过路由分发完成最终业务逻辑处理。
一、流程梳理
通过梳理上图,整个业务流程可以精简如上。通过分析上图可以得出如下结论:
- 该业务流程需要对接外部资产支付系统,意味着资金交易的生命周期维护(即上述红色流程)。
- 紫色的块意味着业务逻辑需要变更,原有的策略假设为A,新增一种策略为B,也有意味着相关变动点需要考虑场景选择A或者B,以便处理。
- 涉及金额处理的,比如付款金额或者退款金额,需要根据场景来选择具体如何计算,场景不同,计算方式也不同,假设两种不同计算金额方式为A、B两种策略。
- 在交付环节,业务的特殊之处就是在下单的时候,产品类型分为基础班(交付1份)、高级版(交付3份),所以交付完成的处理触发机制不同。与此同时,如果交付数量未达标,还涉及退款处理。
- 两种场景的不同之处,如果商品总额可以除尽意味着可以按照商品数量,交由资产系统完成付款和退款的处理。但是不能除尽意味着需要业务方自己计算付款和退款金额。
- 业务订单有自己的状态机(未交付、已交付),交易订单也有自己的状态机(冻结、支付中、支付成功、结算完成)。这两种领域之间有联系,业务订单先于交易订单创建,业务订单触发交付的时候,资金订单才发起付款处理。
- 业务订单由于不同的产品类型,意味着交付是分阶段付款。这也意味着商品总额如果除以商品数量不尽的情况,需要特殊处理。比如一个商品总额10¥,购买3个商品,通过与产品沟通,交付第一个付款3¥,交付第二个付款3¥,最后一个交付付款4¥,然后结算完成。可以这么处理。
二、设计梳理
不同业务线的产品类型划分,也意味着购买不同产品类型的产品触发的交付条件不同,进而触发付款的条件也不同,付款金额和退款金额也有所区别。
假设一订单购买的是高级版的商品,此商品总额10¥,需要交付三份,在指定时间未达成交付,意味着未交付的那几份,需要退款。这里与产品沟通,可以采取四舍五入的金额处理方式。Math.floor(10/3) = 3,最后一份则 buyAmount-paidAmount即 10-6。
未全交付场景下的自动结算流程,先退款未交付的,再付款已交付的。 原有的商品定价,对于资产系统而言
三、技术方案
3.1、下单接口扩展
3.3.1、Request类新增deviceType
/**
* 1-iOS、2-android、3-其他
*/
@ThriftField(5)
private Integer deviceType;
3.3.2、申请单新增字段产品策略(productStrategy)
该字段是后续付款和退款时,具体选择商品数量(quantity)和商品价格(price)两种策略模型。
新增一个产品策略枚举:
/**
* 产品策略类型
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/7 8:21 PM
*/
@Getter
public enum ProductStrategyEnum {
/**
* 标准产品策略
*/
UNKNOWN(0, "UnknownProductStrategy", "Unknown"),
/**
* 标准产品商品数量确认付款策略
*/
STANDARD_QUANTITY(1, "StandardQuantityProductStrategy", "quantity"),
/**
* 标准产品商品价格确认付款策略
*/
STANDARD_PRICE(2, "StandardPriceProductStrategy", "price"),
;
/**
* 策略索引
*/
private final int value;
/**
* 策略名称
*/
private final String name;
/**
* 策略简称
*/
private final String shortName;
ProductStrategyEnum(int value, String name, String shortName) {
this.value = value;
this.name = name;
this.shortName = shortName;
}
public static ProductStrategyEnum valueOfShortName(String shortName) {
for (ProductStrategyEnum each : values()) {
if (Objects.equals(each.getShortName(), shortName)) {
return each;
}
}
return UNKNOWN;
}
}
下单BO类和POJO类新增属性:
/**
* 产品策略
* {@link ProductStrategyEnum#getShortName()}
*/
private String productStrategy;
3.3.3、下单产品策略的处理逻辑
定义一个类:ProxyLiveComponent
,新增如下方法:getProductStrategy
。
/**
* 获取产品策略
*
* @param requestBO 请求
* @return 返回
*/
public ProductStrategyEnum getProductStrategy(JobApplyLiveRequestBO requestBO) {
if (SideBusinessSubtypeEnum.isSocial(requestBO.getSideBusinessSubtype()) && Objects.equals(requestBO.getDeviceType(), 1)) {
int deliveryCount = getSocialProxyDeliveryCount(requestBO.getProductType());
// 基础版、高级版=职位数*投递量 ;至尊版=职位数
int quantity = requestBO.getJobNumbers().size() * deliveryCount;
GetOrderPriceRequestBO getOrderPriceRequest = assetPaymentConverter.convertToGetOrderPriceRequestBO(requestBO, quantity);
//下单时需要向资产系统询价,如果账号资金充足则可以选择商品数量;不足只能选择商品价格。
PaymentTypeEnum paymentType;
try {
paymentType = assetPaymentBusiness.autoPrice(getOrderPriceRequest);
} catch (Exception e) {
log.error("[getProductStrategy]下单获取产品策略异常,req={}", JsonUtils.toJson(requestBO), e);
throw new BusinessException("获取产品策略异常", e);
}
if (PaymentTypeEnum.RMB == paymentType) {
return ProductStrategyEnum.STANDARD_PRICE;
}
}
return ProductStrategyEnum.STANDARD_QUANTITY;
}
然后再business层,新增如下代码
ProductStrategyEnum productStrategy = proxyLiveComponent.getProductStrategy(requestBO);
jobApplyLiveBO.setProductStrategy(productStrategy.getShortName());
//入库
3.2、询价模块的设计
3.2.1、Context设计
上下文类的作用在于封装整个业务处理所依赖的相关参数,后续扩展只需要在此类增加成员属性即可。
/**
* 上下文接口
*
* @author : 石冬冬-Sieg Heil
* @since 2023/2/17 7:14 AM
*/
public interface Context {
}
具体场景Context实现Context
接口。
/**
* 产品策略上下文对象
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/7 8:55 PM
*/
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Data
public class ProductEnquiryContext implements Context {
/**
* 产品策略
*/
private ProductStrategyEnum productStrategy;
/**
* 上下文依赖的请求参数
*/
private Request request;
/**
* 上下文处理结果
*/
private Response response;
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Data
public static class Request {
/**
* 请求参数
*/
private QualifyProductRequestBO qualifyProductRequest;
/**
* 产品
*/
private LiveProductBO liveProduct;
/**
* 申请单
*/
private JobApplyLiveBO jobApplyLive;
}
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Data
public static class Response {
/**
* 数量
*/
private Integer quantity;
/**
* 询价属性
*/
private Map<String, String> priceAttributes;
}
}
3.2.2、ProductStrategy类设计
/**
* 处理接口
* 适用于多场景、多策略的处理器
*
* @author : 石冬冬-Sieg Heil
* @since 2023/2/17 6:46 AM
*/
public interface Handle<C extends Context> {
/**
* 获取处理器名称
*
* @return 处理器名称
*/
String getName();
/**
* 执行处理
*
* @param context 上下文对象
*/
void execute(C context);
}
3.2.2.1、AbstractProductStrategy类
/**
* 抽象产品策略
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/8 2:28 PM
*/
@Slf4j
public abstract class AbstractProductStrategy implements Handle<ProductEnquiryContext> {
final static String LOG_PREFIX = "[ProductStrategy]";
/**
* 产品策略
*/
protected ProductStrategyEnum productStrategy;
@Autowired
protected ProxyLiveComponent proxyLiveComponent;
public AbstractProductStrategy(ProductStrategyEnum productStrategy) {
this.productStrategy = productStrategy;
}
/**
* 执行策略
*
* @param context 上下文对象
*/
@Override
public void execute(ProductEnquiryContext context) {
ProductEnquiryContext.Request request = context.getRequest();
JobApplyLiveBO jobApplyLive = request.getJobApplyLive();
ProductEnquiryContext.Response response = ProductEnquiryContext
.Response.builder().quantity(calculateQuantity(context))
.priceAttributes(getPriceAttributes(context))
.build();
log.info("{}|{}|applyId={},response={}", LOG_PREFIX, getName(), jobApplyLive.getId(), JsonUtils.toJson(response));
context.setResponse(response);
}
/**
* 计算商品数量
*
* @param context 上下文对象
* @return 返回值
*/
protected abstract Integer calculateQuantity(ProductEnquiryContext context);
/**
* 获取询价属性
*
* @param context 上下文对象
* @return 返回值
*/
protected abstract Map<String, String> getPriceAttributes(ProductEnquiryContext context);
public ProductStrategyEnum getProductStrategy() {
return productStrategy;
}
}
3.2.2.2、ProxyLiveStandardPriceStrategy
商品数量询价策略类
Service
@Slf4j
public class ProxyLiveStandardPriceStrategy extends AbstractProductStrategy {
final int THRESHOLD = 5;
public ProxyLiveStandardPriceStrategy() {
super(ProductStrategyEnum.STANDARD_PRICE);
}
@Override
public String getName() {
return ProxyLiveStandardPriceStrategy.class.getSimpleName();
}
@Override
protected Integer calculateQuantity(ProductEnquiryContext context) {
int quantity = 1;
ProductEnquiryContext.Request request = context.getRequest();
JobApplyLiveBO jobApplyLive = request.getJobApplyLive();
List<String> jobNumbers = JSONObject.parseArray(jobApplyLive.getJobInfo(), String.class);
if (jobNumbers.size() > THRESHOLD) {
throw new BusinessException("询价失败!");
}
return quantity;
}
@Override
protected Map<String, String> getPriceAttributes(ProductEnquiryContext context) {
ProductEnquiryContext.Request request = context.getRequest();
JobApplyLiveBO jobApplyLive = request.getJobApplyLive();
List<String> jobNumbers = JSONObject.parseArray(jobApplyLive.getJobInfo(), String.class);
Map<String, String> attributes = Maps.newHashMap();
ProductTypeEnum productType = ProductTypeEnum.valueOf(jobApplyLive.getProductType());
ProductSKUEnum productSKU = ProductSKUEnum.valueOf(jobNumbers.size(), productType);
Optional.ofNullable(productSKU).orElseThrow(() -> {
log.error("{}未查询到产品规格|jobApplyLive={}", LOG_PREFIX, JsonUtils.toJson(jobApplyLive));
return new NotExistException("未查询到产品规格");
});
attributes.put(MetaToBusinessEnum.PROXY_LIVE.getKey(), String.valueOf(productSKU.getSpecificationIndex()));
//此字段用于资产支持小额支付
if (jobNumbers.size() <= THRESHOLD) {
attributes.put("hideRmb", "true");
}
return attributes;
}
}
3.2.2.3、ProxyLiveStandardQuantityStrategy
商品价格询价策略类
@Service
public class ProxyLiveStandardQuantityStrategy extends AbstractProductStrategy {
public ProxyLiveStandardQuantityStrategy() {
super(ProductStrategyEnum.STANDARD_QUANTITY);
}
@Override
public String getName() {
return ProxyLiveStandardQuantityStrategy.class.getSimpleName();
}
@Override
protected Integer calculateQuantity(ProductEnquiryContext context) {
ProductEnquiryContext.Request request = context.getRequest();
JobApplyLiveBO jobApplyLive = request.getJobApplyLive();
List<String> jobNumbers = JSONObject.parseArray(jobApplyLive.getJobInfo(), String.class);
int buyCount = proxyLiveComponent.getSocialProxyDeliveryCount(jobApplyLive.getProductType());
int quantity = jobNumbers.size();
if (SideBusinessSubtypeEnum.isSocial(jobApplyLive.getSideBusinessSubtype())) {
quantity = jobNumbers.size() * buyCount;
}
return quantity;
}
@Override
protected Map<String, String> getPriceAttributes(ProductEnquiryContext context) {
Map<String, String> attributes = Maps.newHashMap();
LiveProductBO liveProduct = context.getRequest().getLiveProduct();
attributes.put(MetaToBusinessEnum.PROXY_LIVE.getKey(), String.valueOf(liveProduct.getProductType()));
return attributes;
}
}
3.2.2.4、HandleDispatcher
该类作用通过场景分发选择何种策略处理,对于业务方不需要具体关注内部策略的实现,只需要通过该类完成处理。
@Slf4j
public abstract class AbstractHandleDispatcher<H extends Handle, C extends Context> {
/**
* 获取分发器名称
*
* @return 分发器名称
*/
protected abstract String getName();
/**
* 获取处理器集合
*
* @return 处理器集合
*/
protected abstract List<H> getHandlers();
/**
* 查找处理器
*
* @param context 处理器上下文
* @return 要分发的处理器
*/
protected abstract Optional<H> find(C context);
/**
* 执行分发
*
* @param context 处理器上下文
*/
public void execute(C context) {
Optional<H> optional = find(context);
if (optional.isPresent()) {
optional.get().execute(context);
} else {
log.error("no find handler", JsonUtils.toJson(context));
}
}
}
ProductEnquiryDispatcher 具体询价调度器。
@Component
@Slf4j
public class ProductEnquiryDispatcher extends AbstractHandleDispatcher<AbstractProductStrategy, ProductEnquiryContext> {
@Autowired
private List<AbstractProductStrategy> strategies;
@Override
protected String getName() {
return ProductEnquiryDispatcher.class.getSimpleName();
}
@Override
protected List<AbstractProductStrategy> getHandlers() {
return strategies;
}
@Override
protected Optional<AbstractProductStrategy> find(ProductEnquiryContext context) {
return getHandlers().stream().filter(each -> each.getProductStrategy() == context.getProductStrategy()).findAny();
}
}
3.3、不同场景下的价格计算策略设计
业务逻辑处理时,只需要委托给JobApplyLiveCalculateMediator
类来完成计算,外部不需要关注具体的计算器,内部计算器业务逻辑扩展修改,不需要业务方修改,做到职责隔离。
3.3.1、UML类图
3.3.1.1、Calculator UML类图
每一种场景都是一个计算器,如果对场景进行计算调整,只需要找到对应的计算器修改逻辑即可。倘若后续增加一种场景,只需要继承基类即可。
3.3.1.2、Context UML类图
每一种计算器对应一个Context上下文类,该类封装了计算所需的相关依赖参数。
3.3.2、Calculator
3.3.2.1、AbstractCalculator
该类是一个泛型类,这样便于具体场景扩展,因为计算结果有的需要返回Integer
,有的返回BigDecimal
,有的是具体一个BO类
,所以方便扩展,灵活性强,符合代码松耦合的设计目标。
/**
* 抽象计算器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/17 9:59 AM
*/
@Slf4j
public abstract class AbstractCalculator<R, C extends Context> {
/**
* 获取计算器名称
*
* @return 返回值
*/
protected abstract String getName();
/**
* 执行计算
*
* @param ctx
* @return 返回计算结果
*/
protected abstract R doExecute(C ctx);
/**
* 执行计算
*
* @param context
* @return
*/
protected R execute(C context) {
R result = doExecute(context);
log.info("[{}]|{}|{}", getName(), JsonUtils.toJson(result), JsonUtils.toJson(context));
return result;
}
}
3.3.2.2、DeductAmountCalculator
@Service
@Slf4j
public class DeductAmountCalculator extends AbstractProxyLiveCalculator {
@Override
protected String getName() {
return DeductAmountCalculator.class.getSimpleName();
}
@Override
protected Integer doExecute(AbstractCalculateContext ctx) {
DeductAmountCalculateContext context = (DeductAmountCalculateContext) ctx;
JobApplyLiveBO jobApplyLive = context.getJobApplyLive();
JobPoolBO jobPool = context.getJobPool();
List<String> proxyList = getProxyJobList(jobApplyLive);
int proxyCount = proxyList.size();
int finishedCount = jobPool.getDeliveryMatchCount();
UserOrderInfoBO userOrderInfoBO = findOrder(jobApplyLive.getId());
int deductAmount = 0;
int buyAmount = Optional.ofNullable(userOrderInfoBO.getBuyAmount()).orElse(BigDecimal.ZERO).intValue();
int payAmount = Optional.ofNullable(userOrderInfoBO.getPayAmount()).orElse(BigDecimal.ZERO).intValue();
if (finishedCount < proxyCount - 1) {
deductAmount = Math.round((float) buyAmount / proxyCount);
}
if (finishedCount == proxyCount - 1) {
deductAmount = buyAmount - payAmount;
}
JSONObject extend = new JSONObject();
extend.put("applyId", jobApplyLive.getId());
extend.put("proxyCount", proxyCount);
extend.put("finishedCount", finishedCount);
extend.put("deductAmount", deductAmount);
ctx.setExtend(extend);
return deductAmount;
}
}
3.3.3、Context
3.3.3.1、AbstractCalculateContext
该抽象类封装了相关场景所依赖的共同参数,相关子类只需要继承此基类,然后扩展自己的成员属性即可。
/**
* 抽象计算上下文
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/17 9:52 AM
*/
@Data
@SuperBuilder
public class AbstractCalculateContext implements Context {
/**
* 申请单
*/
private JobApplyLiveBO jobApplyLive;
/**
* 职位池
*/
private JobPoolBO jobPool;
/**
* 扩展项
*/
private JSONObject extend;
}
3.3.3.2、PartialDeductAmountCalculateContext
/**
* 计算部分划扣金额上下文
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/17 9:52 AM
*/
@Data
@SuperBuilder
public class PartialDeductAmountCalculateContext extends AbstractCalculateContext {
/**
* 当前要部分交付完成退款金额
*/
private int presentRefundAmount;
}
3.3.4、CalculateMediator
相关计算器统一通过Spring @Autowired
注入到该类,作为其成员属性。该类相当于一个委托类,并提供一系列成员方法,提供业务场景的金额计算调用,业务场景不需要关注具体选择何种计算器,只需要交由其处理。做到计算器与业务场景的隔离,方便后期扩展维护。
@Component
@Slf4j
public class JobApplyLiveCalculateMediator {
@Autowired
private DeductAmountCalculator deductAmountCalculator;
@Autowired
private RefundAmountCalculator refundAmountCalculator;
@Autowired
private PartialDeductAmountCalculator partialDeductAmountCalculator;
@Autowired
private DeliveryDeductAmountCalculator deliveryDeductAmountCalculator;
//示例代码方法1
public int calculateDeductAmount(JobApplyLiveBO jobApplyLive, JobPoolBO jobPool) {
DeductAmountCalculateContext context = DeductAmountCalculateContext.builder().jobApplyLive(jobApplyLive).jobPool(jobPool).build();
return deductAmountCalculator.execute(context);
}
//省略方法2、3、4、5
}
3.4、付款与退款的设计
3.4.1、Context类的设计
/**
* 交易处理上下文对象
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:24 AM
*/
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Data
public class TransactionHandleContext implements Context {
/**
* 请求参数,需要外部调用实例化
*/
private Request request;
/**
* 计算结果,内部处理使用
*/
private CalculateResult calculateResult;
/**
* 付款请求参数
*/
private PaymentRequestBO paymentRequest;
/**
* 订单信息
*/
private UserOrderInfoBO userOrderInfo;
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Data
public static class Request {
/**
* 产品策略
*/
protected ProductStrategyEnum productStrategy;
/**
* 业务类型
*/
private BusinessTypeEnum businessType;
/**
* 交易类型
*/
private TransactionTypeEnum transactionType;
}
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@Data
public static class CalculateResult {
/**
* 确认
*/
private BigDecimal confirmed;
/**
* 累计付款
*/
private BigDecimal paid;
/**
* 累计退款
*/
private BigDecimal refunded;
/**
* 是否满足结算完成
*/
private boolean matchSettle;
}
}
3.4.2、TransactionHandler类的设计
/**
* 抽象交易处理处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:26 AM
*/
@Slf4j
public abstract class AbstractTransactionHandler implements Handle<TransactionHandleContext> {
protected final static String LOG_PREFIX = "[TransactionHandler]";
/**
* 产品策略
*/
protected ProductStrategyEnum productStrategy;
/**
* 交易类型
*/
private TransactionTypeEnum transactionType;
public AbstractTransactionHandler(ProductStrategyEnum productStrategy, TransactionTypeEnum transactionType) {
this.productStrategy = productStrategy;
this.transactionType = transactionType;
}
@Autowired
protected UserOrderInfoDAO userOrderInfoDAO;
@Autowired
protected PaymentBusiness paymentBusiness;
@Autowired
protected UserOrderInfoDetailDAO userOrderInfoDetailDAO;
@Autowired
protected UserOrderInfoDetailConverter userOrderInfoDetailConverter;
@Autowired
private RedisClient redisClient;
/**
* 执行订单检查
*
* @param context 上下文对象
* @return 返回校验结果
*/
protected abstract CheckedResult doCheckOrder(TransactionHandleContext context);
/**
* 执行计算
*
* @param context 上下文对象
*/
protected abstract void doCalculate(TransactionHandleContext context);
/**
* 执行处理
*
* @param context 上下文对象
*/
protected abstract void doExecute(TransactionHandleContext context);
/**
* 获取加锁key
*
* @param context 上下文对象
* @return 返回值
*/
protected String getLockKey(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
return "confirmPayment:lock:" + paymentRequestBO.getBusinessId() + "-" + paymentRequestBO.getBusinessType();
}
/**
* 执行数据装载
*
* @param context 上下文对象
*/
protected void doPrepare(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
UserOrderInfoBO userOrderInfoBO = getUserOrderInfo(paymentRequestBO.getBusinessId(), paymentRequestBO.getBusinessType());
context.setUserOrderInfo(userOrderInfoBO);
}
/**
* 执行处理
*
* @param context 上下文对象
*/
@Override
public void execute(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
String key = getLockKey(context);
redisClient.lock(() -> {
try {
doExecute(context);
} catch (Exception e) {
log.error("{}|{}|param={}", LOG_PREFIX, getName(), JsonUtils.toJson(paymentRequestBO), e);
}
}, key);
}
private UserOrderInfoBO getUserOrderInfo(long businessId, int businessType) {
UserOrderInfoBO userOrderInfoBO = new UserOrderInfoBO();
userOrderInfoBO.setBusinessId(businessId);
userOrderInfoBO.setBusinessType(businessType);
List<UserOrderInfoBO> list = userOrderInfoDAO.list(userOrderInfoBO);
if (CollectionUtils.isEmpty(list)) {
throw new BusinessException("支付订单不存在!");
}
return list.get(0);
}
protected void addOrderDetail(PaymentRequestBO paymentRequestBO, Long orderId) {
UserOrderInfoDetailBO bo = userOrderInfoDetailConverter.convertToUserOrderInfoDetailBO(paymentRequestBO, orderId);
userOrderInfoDetailDAO.insert(bo);
}
public ProductStrategyEnum getProductStrategy() {
return productStrategy;
}
public TransactionTypeEnum getTransactionType() {
return transactionType;
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public static class CheckedResult {
/**
* 校验成功
*/
private boolean success;
/**
* 校验消息
*/
private String message;
}
}
3.4.3、PaymentTransactionHandler类的设计
/**
* 抽象付款处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:26 AM
*/
@Slf4j
public abstract class AbstractPaymentTransactionHandler extends AbstractTransactionHandler {
public AbstractPaymentTransactionHandler(ProductStrategyEnum productStrategy) {
super(productStrategy, TransactionTypeEnum.PAYMENT);
}
@Override
public void doExecute(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
doPrepare(context);
UserOrderInfoBO userOrderInfoBO = context.getUserOrderInfo();
String trans = userOrderInfoBO.getPaymentNo();
Long bizId = userOrderInfoBO.getBusinessId();
if (userOrderInfoBO.getState() == PayStatusEnum.SUCCESS.getValue()) {
return;
}
if (userOrderInfoBO.getState() != PayStatusEnum.FREEZE.getValue()) {
throw new BusinessException("订单不是冻结状态,无法确认支付!");
}
CheckedResult checked = doCheckOrder(context);
if (!checked.isSuccess()) {
throw new BusinessException(checked.getMessage());
}
doCalculate(context);
ConfirmPaymentBO confirmPaymentBO = doBuildConfirmPayment(context);
log.info("{}|{}|trans={}|bizId={}|Calculate={}|confirmPayment-pay:{}", LOG_PREFIX, getName(),trans, bizId, JsonUtils.toJson(context.getCalculateResult()), JsonUtils.toJson(confirmPaymentBO));
boolean result = paymentBusiness.confirmPayment(confirmPaymentBO);
if (!result) {
throw new BusinessException("订单确认支付失败!");
}
UserOrderInfoBO updateUserOrderInfoBO = doBuildUserOrderInfo(context);
userOrderInfoDAO.update(updateUserOrderInfoBO);
addOrderDetail(paymentRequestBO, userOrderInfoBO.getId());
}
protected ConfirmPaymentBO doBuildConfirmPayment(TransactionHandleContext context) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
ConfirmPaymentBO confirmPaymentBO = new ConfirmPaymentBO();
confirmPaymentBO.setUserOrderInfoBO(context.getUserOrderInfo());
confirmPaymentBO.setConfirmType(ConfirmTypeEnum.valueOf(userOrderInfo.getConfirmType()));
confirmPaymentBO.setConfirmed(calculateResult.getConfirmed().longValue());
return confirmPaymentBO;
}
protected UserOrderInfoBO doBuildUserOrderInfo(TransactionHandleContext context) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
UserOrderInfoBO updateUserOrderInfo = new UserOrderInfoBO();
updateUserOrderInfo.setId(context.getUserOrderInfo().getId());
if (calculateResult.isMatchSettle()) {
updateUserOrderInfo.setState(PayStatusEnum.FINISH.getValue());
}
invokeUserOrderInfoSet(context, updateUserOrderInfo);
return updateUserOrderInfo;
}
/**
* 设置需要更新订单的相关属性
*
* @param context 上下文信息
* @param orderInfo 订单信息
*/
protected abstract void invokeUserOrderInfoSet(TransactionHandleContext context, UserOrderInfoBO orderInfo);
}
3.4.3.1、StandardPricePaymentTransactionHandler类的设计
/**
* (按商品价格)付款交易处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:31 AM
*/
@Service
@Slf4j
public class StandardPricePaymentTransactionHandler extends AbstractPaymentTransactionHandler {
public StandardPricePaymentTransactionHandler() {
super(ProductStrategyEnum.STANDARD_PRICE);
}
@Override
public String getName() {
return StandardPricePaymentTransactionHandler.class.getSimpleName();
}
@Override
protected CheckedResult doCheckOrder(TransactionHandleContext context) {
PaymentRequestBO paymentRequest = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
BigDecimal buyAmount = Optional.ofNullable(userOrderInfo.getBuyAmount()).orElse(BigDecimal.ZERO);
BigDecimal payAmount = Optional.ofNullable(userOrderInfo.getPayAmount()).orElse(BigDecimal.ZERO);
BigDecimal cancelAmount = Optional.ofNullable(userOrderInfo.getCancelAmount()).orElse(BigDecimal.ZERO);
BigDecimal operateAmount = BigDecimal.valueOf(Optional.ofNullable(paymentRequest.getOperateCount()).orElse(0));
BigDecimal rest = buyAmount.subtract(payAmount).subtract(cancelAmount);
if (rest.compareTo(operateAmount) == -1) {
return CheckedResult.builder().success(Boolean.FALSE).message("支付金额大于剩余金额,无法确认支付").build();
}
return CheckedResult.builder().success(Boolean.TRUE).build();
}
@Override
protected void doCalculate(TransactionHandleContext context) {
PaymentRequestBO paymentRequest = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
BigDecimal buyAmount = Optional.ofNullable(userOrderInfo.getBuyAmount()).orElse(BigDecimal.ZERO);
BigDecimal payAmount = Optional.ofNullable(userOrderInfo.getPayAmount()).orElse(BigDecimal.ZERO);
BigDecimal cancelAmount = Optional.ofNullable(userOrderInfo.getCancelAmount()).orElse(BigDecimal.ZERO);
BigDecimal operateAmount = BigDecimal.valueOf(Optional.ofNullable(paymentRequest.getOperateCount()).orElse(0));
BigDecimal total = payAmount.add(operateAmount).add(cancelAmount);
boolean matchSettle = total.compareTo(buyAmount) == 0;
TransactionHandleContext.CalculateResult calculateResult = TransactionHandleContext.CalculateResult.builder()
.confirmed(payAmount.add(operateAmount))
.paid(payAmount.add(operateAmount))
.matchSettle(matchSettle)
.build();
context.setCalculateResult(calculateResult);
}
@Override
protected void invokeUserOrderInfoSet(TransactionHandleContext context, UserOrderInfoBO orderInfo) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
orderInfo.setPayAmount(calculateResult.getPaid());
}
}
3.4.3.2、StandardPricePaymentTransactionHandler类的设计
/**
* (按商品价格)付款交易处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:31 AM
*/
@Service
@Slf4j
public class StandardPricePaymentTransactionHandler extends AbstractPaymentTransactionHandler {
public StandardPricePaymentTransactionHandler() {
super(ProductStrategyEnum.STANDARD_PRICE);
}
@Override
public String getName() {
return StandardPricePaymentTransactionHandler.class.getSimpleName();
}
@Override
protected CheckedResult doCheckOrder(TransactionHandleContext context) {
PaymentRequestBO paymentRequest = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
BigDecimal buyAmount = Optional.ofNullable(userOrderInfo.getBuyAmount()).orElse(BigDecimal.ZERO);
BigDecimal payAmount = Optional.ofNullable(userOrderInfo.getPayAmount()).orElse(BigDecimal.ZERO);
BigDecimal cancelAmount = Optional.ofNullable(userOrderInfo.getCancelAmount()).orElse(BigDecimal.ZERO);
BigDecimal operateAmount = BigDecimal.valueOf(Optional.ofNullable(paymentRequest.getOperateCount()).orElse(0));
BigDecimal rest = buyAmount.subtract(payAmount).subtract(cancelAmount);
if (rest.compareTo(operateAmount) == -1) {
return CheckedResult.builder().success(Boolean.FALSE).message("支付金额大于剩余金额,无法确认支付").build();
}
return CheckedResult.builder().success(Boolean.TRUE).build();
}
@Override
protected void doCalculate(TransactionHandleContext context) {
PaymentRequestBO paymentRequest = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
BigDecimal buyAmount = Optional.ofNullable(userOrderInfo.getBuyAmount()).orElse(BigDecimal.ZERO);
BigDecimal payAmount = Optional.ofNullable(userOrderInfo.getPayAmount()).orElse(BigDecimal.ZERO);
BigDecimal cancelAmount = Optional.ofNullable(userOrderInfo.getCancelAmount()).orElse(BigDecimal.ZERO);
BigDecimal operateAmount = BigDecimal.valueOf(Optional.ofNullable(paymentRequest.getOperateCount()).orElse(0));
BigDecimal total = payAmount.add(operateAmount).add(cancelAmount);
boolean matchSettle = total.compareTo(buyAmount) == 0;
TransactionHandleContext.CalculateResult calculateResult = TransactionHandleContext.CalculateResult.builder()
.confirmed(payAmount.add(operateAmount))
.paid(payAmount.add(operateAmount))
.matchSettle(matchSettle)
.build();
context.setCalculateResult(calculateResult);
}
@Override
protected void invokeUserOrderInfoSet(TransactionHandleContext context, UserOrderInfoBO orderInfo) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
orderInfo.setPayAmount(calculateResult.getPaid());
}
}
3.4.4、TransactionHandler类的设计
/**
* 抽象退款处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:26 AM
*/
@Slf4j
public abstract class AbstractRefundTransactionHandler extends AbstractTransactionHandler {
public AbstractRefundTransactionHandler(ProductStrategyEnum productStrategy) {
super(productStrategy, TransactionTypeEnum.REFUND);
}
@Override
public void doExecute(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
doPrepare(context);
UserOrderInfoBO userOrderInfoBO = context.getUserOrderInfo();
String trans = userOrderInfoBO.getPaymentNo();
Long bizId = userOrderInfoBO.getBusinessId();
if (userOrderInfoBO.getState() != PayStatusEnum.FREEZE.getValue()) {
throw new BusinessException("订单不是冻结状态,无法取消支付!");
}
CheckedResult checked = doCheckOrder(context);
if (!checked.isSuccess()) {
throw new BusinessException(checked.getMessage());
}
doCalculate(context);
ConfirmPaymentBO confirmPaymentBO = doBuildConfirmPayment(context);
log.info("{}|{}|trans={}|bizId={}|Calculate={}|confirmPayment-cancel:{}", LOG_PREFIX, getName(), trans, bizId, JsonUtils.toJson(context.getCalculateResult()), JsonUtils.toJson(confirmPaymentBO));
boolean result = paymentBusiness.confirmPayment(confirmPaymentBO);
if (!result) {
throw new BusinessException("订单取消支付失败!");
}
UserOrderInfoBO updateUserOrderInfoBO = doBuildUserOrderInfo(context);
userOrderInfoDAO.update(updateUserOrderInfoBO);
addOrderDetail(paymentRequestBO, userOrderInfoBO.getId());
}
protected ConfirmPaymentBO doBuildConfirmPayment(TransactionHandleContext context) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
ConfirmPaymentBO confirmPaymentBO = new ConfirmPaymentBO();
confirmPaymentBO.setUserOrderInfoBO(context.getUserOrderInfo());
confirmPaymentBO.setConfirmType(ConfirmTypeEnum.valueOf(userOrderInfo.getConfirmType()));
confirmPaymentBO.setRefunded(calculateResult.getConfirmed().longValue());
return confirmPaymentBO;
}
protected UserOrderInfoBO doBuildUserOrderInfo(TransactionHandleContext context) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
UserOrderInfoBO updateUserOrderInfo = new UserOrderInfoBO();
updateUserOrderInfo.setId(context.getUserOrderInfo().getId());
if (calculateResult.isMatchSettle()) {
updateUserOrderInfo.setState(PayStatusEnum.FINISH.getValue());
}
invokeUserOrderInfoSet(context, updateUserOrderInfo);
return updateUserOrderInfo;
}
/**
* 设置需要更新订单的相关属性
*
* @param context 上下文信息
* @param orderInfo 订单信息
*/
protected abstract void invokeUserOrderInfoSet(TransactionHandleContext context, UserOrderInfoBO orderInfo);
}
3.4.4.1、StandardPriceRefundTransactionHandler类的设计
/**
* (按商品价格)退款交易处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:31 AM
*/
@Service
@Slf4j
public class StandardPriceRefundTransactionHandler extends AbstractRefundTransactionHandler {
public StandardPriceRefundTransactionHandler() {
super(ProductStrategyEnum.STANDARD_PRICE);
}
@Override
public String getName() {
return StandardPriceRefundTransactionHandler.class.getSimpleName();
}
@Override
protected CheckedResult doCheckOrder(TransactionHandleContext context) {
PaymentRequestBO paymentRequest = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
BigDecimal buyAmount = Optional.ofNullable(userOrderInfo.getBuyAmount()).orElse(BigDecimal.ZERO);
BigDecimal payAmount = Optional.ofNullable(userOrderInfo.getPayAmount()).orElse(BigDecimal.ZERO);
BigDecimal cancelAmount = Optional.ofNullable(userOrderInfo.getCancelAmount()).orElse(BigDecimal.ZERO);
BigDecimal operateAmount = BigDecimal.valueOf(Optional.ofNullable(paymentRequest.getOperateCount()).orElse(0));
BigDecimal rest = buyAmount.subtract(payAmount).subtract(cancelAmount);
if (rest.compareTo(operateAmount) == -1) {
return CheckedResult.builder().success(Boolean.FALSE).message("支付金额大于剩余金额,无法确认支付").build();
}
return CheckedResult.builder().success(Boolean.TRUE).build();
}
@Override
protected void doCalculate(TransactionHandleContext context) {
PaymentRequestBO paymentRequest = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
BigDecimal buyAmount = Optional.ofNullable(userOrderInfo.getBuyAmount()).orElse(BigDecimal.ZERO);
BigDecimal payAmount = Optional.ofNullable(userOrderInfo.getPayAmount()).orElse(BigDecimal.ZERO);
BigDecimal cancelAmount = Optional.ofNullable(userOrderInfo.getCancelAmount()).orElse(BigDecimal.ZERO);
BigDecimal operateAmount = BigDecimal.valueOf(Optional.ofNullable(paymentRequest.getOperateCount()).orElse(0));
BigDecimal total = payAmount.add(operateAmount).add(cancelAmount);
boolean matchSettle = total.compareTo(buyAmount) == 0;
TransactionHandleContext.CalculateResult calculateResult = TransactionHandleContext.CalculateResult.builder()
.confirmed(cancelAmount.add(operateAmount))
.refunded(cancelAmount.add(operateAmount))
.matchSettle(matchSettle)
.build();
context.setCalculateResult(calculateResult);
}
@Override
protected void invokeUserOrderInfoSet(TransactionHandleContext context, UserOrderInfoBO orderInfo) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
orderInfo.setCancelAmount(calculateResult.getRefunded());
}
}
3.4.4.2、StandardQuantityRefundTransactionHandler类的设计
/**
* (按商品数量)退款交易处理器
*
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 10:31 AM
*/
@Service
@Slf4j
public class StandardQuantityRefundTransactionHandler extends AbstractRefundTransactionHandler {
public StandardQuantityRefundTransactionHandler() {
super(ProductStrategyEnum.STANDARD_QUANTITY);
}
@Override
public String getName() {
return StandardQuantityRefundTransactionHandler.class.getSimpleName();
}
@Override
protected CheckedResult doCheckOrder(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
int buyCount = Optional.ofNullable(userOrderInfo.getBuyCount()).orElse(0);
int payCount = Optional.ofNullable(userOrderInfo.getPayCount()).orElse(0);
int cancelCount = Optional.ofNullable(userOrderInfo.getCancelCount()).orElse(0);
int operateCount = Optional.ofNullable(paymentRequestBO.getOperateCount()).orElse(0);
if ((buyCount - payCount - cancelCount) < operateCount) {
return CheckedResult.builder().success(Boolean.FALSE).message("支付数大于剩余数量,无法确认支付").build();
}
return CheckedResult.builder().success(Boolean.TRUE).build();
}
@Override
protected void doCalculate(TransactionHandleContext context) {
PaymentRequestBO paymentRequestBO = context.getPaymentRequest();
UserOrderInfoBO userOrderInfo = context.getUserOrderInfo();
int buyCount = Optional.ofNullable(userOrderInfo.getBuyCount()).orElse(0);
int payCount = Optional.ofNullable(userOrderInfo.getPayCount()).orElse(0);
int cancelCount = Optional.ofNullable(userOrderInfo.getCancelCount()).orElse(0);
int operateCount = Optional.ofNullable(paymentRequestBO.getOperateCount()).orElse(0);
int total = payCount + operateCount + cancelCount;
boolean matchSettle = total == buyCount;
TransactionHandleContext.CalculateResult calculateResult = TransactionHandleContext.CalculateResult.builder()
.confirmed(BigDecimal.valueOf(cancelCount + operateCount))
.refunded(BigDecimal.valueOf(cancelCount + operateCount))
.matchSettle(matchSettle)
.build();
context.setCalculateResult(calculateResult);
}
@Override
protected void invokeUserOrderInfoSet(TransactionHandleContext context, UserOrderInfoBO orderInfo) {
TransactionHandleContext.CalculateResult calculateResult = context.getCalculateResult();
orderInfo.setCancelCount(calculateResult.getRefunded().intValue());
}
}
四、优化设计
1、计算模型优化
第一版的设计,有这么一个JobApplyLiveCalculator
类,里边有若干个成员方法,但是缺点就是后续如果增加场景,就会增加再增加一个成员方法,同时,如果方法增加参数,意味着相关调用方都需要增加参数,意味着耦合性强,因此为了达到松耦合,符合单一原则,抽象了如下领域模型。
Context
:上下文类封装各个场景所需要的相关参数,同时抽象出一个AbstractContext
类,各个场景继承此类。Calculator
:抽象出AbstractCalculator
,各个场景有对应的计算器完成计算。Mediator
:委托类,只能处理委托给相关场景的Calculator
完成处理,返回把计算的结果再返回给调用方。
2、Dispatcher优化
该类具体就是完成业务场景处理器的路由与分发,场景不需要关注选择何种处理器,只需要委托该类处理,做到职责隔离,方便后期扩展。
2.1、第一版Dispatcher设计
/**
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 2:18 PM
*/
@Component
@Slf4j
public class TransactionHandleDispatcher {
@Autowired
private List<AbstractTransactionHandler> handlers;
/**
* 执行分发策略
*
* @param context 上下文对象
*/
public void execute(TransactionHandleContext context) {
handlers.stream().forEach(each -> {
Optional<AbstractTransactionHandler> optional = handlers.stream().filter(each -> {
TransactionHandleContext.Request request = context.getRequest();
boolean find = request.getProductStrategy() == each.getProductStrategy()
&& request.getTransactionType() == each.getTransactionType();
if (find) {
each.execute(context);
}
});
return request.getProductStrategy() == each.getProductStrategy() && request.getTransactionType() == each.getTransactionType();
}).findAny();
if (optional.isPresent()) {
optional.get().execute(context);
} else {
log.error("[TransactionHandle]未找到对应的处理器", JsonUtils.toJson(context));
}
}
}
由于各个模块都有对应的Dispatcher,但是该类仔细观察,还可以继续抽象。通过分析,发现这个类有以下几个特征:
- 通过
Spring
@Autowired
自动装配相关处理类的子类,子类具有有哪些,这个类不需要关注。 - 对外提供的
execute
方法,返回类型void
,入参是一个Context
类。 execute
方法内部,需要根据Context
类的实例化参数来进行选择一个Handler来处理。
通过分析如上特征,我认为可以对Dispatcher
再进行抽象,所有的上下文类提取一个Context
接口,所有的处理器类实现一个Handler
接口。
2.2、第二版Dispatcher设计
目标:基于面向接口编程的设计。
2.2.1、Context接口
该接口没有成员方法,它代表一种上下文能力特征,只要具备此特征就需要实现该接口。
/**
* 上下文接口
*
* @author : 石冬冬-Sieg Heil
* @since 2023/2/17 7:14 AM
*/
public interface Context {
}
2.2.2、Handle接口
该接口两个成员方法,同时该类规约了上下文类需要实现Context接口。
/**
* 处理接口
* 适用于多场景、多策略的处理器
*
* @author : 石冬冬-Sieg Heil
* @since 2023/2/17 6:46 AM
*/
public interface Handle<C extends Context> {
/**
* 获取处理器名称
*
* @return 处理器名称
*/
String getName();
/**
* 执行处理
*
* @param context 上下文对象
*/
void execute(C context);
}
2.2.3、AbstractDispatcher抽象类
只要实现Handle接口的类,同时具备Spring Bean容器管理的相关处理器Handler都可以通@Autowired完成应用成员启动时自动装配。对于子类而言,主要实现find方法,完成从getHander()方法中获取处理器集合,选择一个场景匹配的处理器完成处理。
/**
* 抽象处理分发器
*
* @author : 石冬冬-Sieg Heil
* @since 2023/2/17 6:42 AM
*/
@Slf4j
public abstract class AbstractHandleDispatcher<H extends Handle, C extends Context> {
/**
* 获取分发器名称
*
* @return 分发器名称
*/
protected abstract String getName();
/**
* 获取处理器集合
*
* @return 处理器集合
*/
protected abstract List<H> getHandlers();
/**
* 查找处理器
*
* @param context 处理器上下文
* @return 要分发的处理器
*/
protected abstract Optional<H> find(C context);
/**
* 执行分发
*
* @param context 处理器上下文
*/
public void execute(C context) {
Optional<H> optional = find(context);
if (optional.isPresent()) {
optional.get().execute(context);
} else {
log.error("no find handler", JsonUtils.toJson(context));
}
}
}
具体一个Dispatcher
类,只需要继承此类,实现抽象方法即可。
/**
* @author 石冬冬(Chris Suk)
* @since 2023/2/13 2:18 PM
*/
@Component
@Slf4j
public class TransactionHandleDispatcher extends AbstractHandleDispatcher<AbstractTransactionHandler, TransactionHandleContext> {
@Autowired
private List<AbstractTransactionHandler> handlers;
@Override
protected String getName() {
return TransactionHandleDispatcher.class.getSimpleName();
}
@Override
protected List<AbstractTransactionHandler> getHandlers() {
return handlers;
}
@Override
protected Optional<AbstractTransactionHandler> find(TransactionHandleContext context) {
return getHandlers().stream().filter(each -> {
TransactionHandleContext.Request request = context.getRequest();
return request.getProductStrategy() == each.getProductStrategy() && request.getTransactionType() == each.getTransactionType();
}).findAny();
}
}
五、总结
谈言之,这个需求总体涉及变动点多,为了考虑尽可能代码变动影响面小,所以经过慎重选择,设计出一种利弊均衡的技术方案设计,这种设计同时达成了一个目标,业务场景与底层能力解耦,能力通过抽象下沉,委托给Dispatcher
或者Mediator
完成具体处理器Handler
的分发和处理。
恰如一个需求来了,有个项目经理来完成跟产品经理对接,产品经理不需要关注后端、前端是哪位成员,他只需要跟项目经理对接即可,做到隔离。如果后端有成员变更,对于产品经理做到隔离,产品经理无需关注后端有成员变更,他只需要关注目标的达成,通过接口隔离
即可实现。
同时,这个需求也让我从零熟悉了整个业务流程,它不仅是一种挑战,更是一种成长。