共计 9850 个字符,预计需要花费 25 分钟才能阅读完成。
前因后果
最近公司正在做一个统一的商城平台,也就是允许不同的 App 进入商城进行商品的创建,销售,结算,对账。由于我们拥有 5 个不同客户端包括了 App 与微信小程序,很显然不可能去同时维护 5 个不同的商城。于是我们提出了统一商城的概念,不同的客户端入驻商城由商城分配 platformId(客户端平台) 与 serviceId(客户端服务)。一个 platform 可以对应多个 service,例如 App 可以提供实体商品销售与网络课程的服务。
这样一来的话有如下好处:
- 商城只需要开发一次,易于维护。
- 对账清晰,全部从商城开始对账,通过订单的 platformId 与 serviceId 进行区分。
- 商家可以选择平台入驻并提供服务。
- 后期商城可以转向微服务。
同时也带了一些复杂的问题:
- 不同的客户端支付方式不同。
- 不同平台的商品需求不同。
- 平台,服务,商家,商品,订单,资金的关系如何维护。
由于我负责支付模块的开发,这里我们只针对支付模块进行讨论。在开发之前我们先来整理一下支付模块需要开发哪些功能?
- 根据不同的平台与服务提供不同的支付方式。
- 需要给支付订单增加前缀来进行区分 (方便对账)。
- 集成所有需要提供的支付方式(微信 App,支付宝 App,微信小程序,微信 Wap,支付宝 Wap)。
接下来我们再整理一下模块中最复杂的难点:
- 如何抽象支付的流程保证可扩展性?
难点分析
抽象是最复杂也是最考验程序员业务分析能力的地方,我们应该如何切分支付业务将变与不变的地方切分开来。由于微信与支付宝的 api 有所不同,并且各种支付方式的 api 也会不同。我进行了如下切分:
首先是不变的地方:
- 各个支付方式的 api 是不会变的。
- 支付的流程是不会变的(可以参考本系列第一篇文章)。
- 退款的流程是不会变的。
变的地方:
- 各个支付的 api 是不同的,所需参数,所调用的接口都不同。
- 前端所需要唤醒支付键盘的参数是不同的。
- 认证用户的方式不同。
根据以上分析,我们绘制如下的类图:
- PaymentApplicationService:作为业务服务暴露给其他模块。
- PaymentUtil:抽象支付工具类在 PaymentApplicationService 中使用。
- WeCahtPayUtil:具体工具类的实现,内部具体功能实现需要依赖 PaymentRequestBuilder。但是可以在这一层进行对 response 的替换与修改。
- MyPaymentClient:封装微信与支付宝的 sdk,以此来屏蔽细节上的差异,与适配器模式思想类似。
- PaymentRequestBuilder:根据不同的支付方式与支付业务构造请求对象。内部会包含针对 api 不同的代码判断。
- PaymentConfigParser:针对不同的认证方式提供不同的构造解析,因为认证方式的不同会是得初始化参数不同,而这些特殊的配置信息是按照 json 格式存储在数据库中的,在获取时也需要相应的解析器。
- PaymentUtilCache:支付工具类获取一次即可缓存,以节约性能。
- PaymentConfig:实际存储支付配置信息的数据库对象。
代码实例
现在来看看代码中如何使用支付:
外部模块调用支付:
@PostMapping("/pay")
@Retry(on = ObjectOptimisticLockingFailureException.class)
public BaseResponse pay(@RequestBody @Valid PaymentPayVO pay) throws Exception {CustomerValueObject customer = RequestMetadataHolder.getCustomerValueObject();
PaymentTypeEnum paymentType = EnumUtils.getEnum(PaymentTypeEnum.class,pay.getPaymentType()).orElse(null);
PaymentChannelEnum paymentChannel = getPayChannel(paymentType);
if(paymentType == null){throw new InvalidOperationException("支付方式异常");
}
List<OrderDO> orderDOList = orderRepository.findAllById(pay.getOrderIds());
//todo 思考如何处理额外的配置参数
Map<String,String> hockParams = new HashMap<>(2); if(PaymentTypeEnum.WECHAT_PAY_JSAPI.getValue().equals(pay.getPaymentType())){if(StringUtils.isBlank(pay.getOpenId())){throw new InvalidOperationException("openId 不能为空");
}
hockParams.put("openid",pay.getOpenId());
}else if(PaymentTypeEnum.WECHAT_PAY_H5.getValue().equals(pay.getPaymentType())){JSONObject scene = new JSONObject();
JSONObject info = new JSONObject();
info.put("type","Wap");info.put("wap_url","http://mall.servicesplus.cn");info.put("wap_name","韩希商城");
scene.put("h5_info",info);
hockParams.put("scene",scene.toJSONString());
}
PaymentResultDTO payResult = paymentApplicationService.createPayment(orders,
paymentAmount, paymentChannel, paymentType,
customer.getBuyerId(),customer.getServiceId(),customer.getPlatformId(),hockParams);
return BaseResponse.success(payResult,"支付 sdk 数据获取成功");
}
支付内部获取支付工具类:
private String lunchPay(PaymentDO payment,List<PaymentOrderDO> paymentOrders,PaymentConfigDO paymentConfig,Map<String,String> hockParams) throws Exception {
// 计算签名字符串
IPaymentUtil payUtil = payUtilRepository.get(payment);
String signString = payUtil.getSignStr(paymentConfig.getOrderNamePrefix() + ":" + payment.getId(), payment.getOutTradeNo(), payment.getPaymentAmount(), hockParams);
payment.lunchPay(signString);
for (PaymentOrderDO po : paymentOrders) {po.lunchPay(signString);
}
PaymentFlowDO paymentFlow = new PaymentFlowDO(payment.getId(),payment.getOutTradeNo(), PaymentFlowEnum.RECEIVE_SIGNATURE,signString);
paymentJpaRepository.save(payment);
paymentOrderJpaRepository.saveAll(paymentOrders);
paymentFlowJpaRepository.save(paymentFlow);
return signString;
}
payUtilRepository 生成具体支付工具类:
@Component
public class PayUtilRepository {
private final PaymentConfigJpaRepository paymentConfigJpaRepository;
@Autowired
public PayUtilRepository(PaymentConfigJpaRepository paymentConfigJpaRepository) {this.paymentConfigJpaRepository = paymentConfigJpaRepository;}
public IPaymentUtil get(PaymentDO payment) throws Exception {String[] paymentKey = payment.getPaymentKey().split("_");
String serviceId = paymentKey[1];
String platformId = paymentKey[2];
Optional<PaymentConfigDO> paymentConfigOptional = paymentConfigJpaRepository.findByServiceIdAndPlatformIdAndPaymentType(serviceId,platformId,payment.getPaymentType());
if(!paymentConfigOptional.isPresent()){throw new InvalidOperationException("该服务没有支付配置");
}
PaymentConfigDO paymentConfig = paymentConfigOptional.get();
BasePaymentConfigParser parser = PaymentParserFactory.getParser(paymentConfig);
return parser.get(paymentConfig);
}
}
支付配置工厂解析配置生成具体工具类:
public class PaymentParserFactory {public static BasePaymentConfigParser getParser(PaymentConfigDO payConfig) throws InvalidOperationException {Optional<PaymentAuthEnum> payAuthOptional = EnumUtils.getEnum(PaymentAuthEnum.class,payConfig.getPaymentAuth());
if(!payAuthOptional.isPresent()){throw new InvalidOperationException("支付认证方式不存在");
}else {
BasePaymentConfigParser payParser;
PaymentAuthEnum payAuth = payAuthOptional.get();
switch (payAuth){
case WECHAT_CERT:
payParser = new WeChatPayWithCertParser();
break;
case ALI_PAY_CERT:
payParser = new AliPayWithCertParser();
break;
case ALI_PAY_PUBLIC_KEY:
payParser = new AliPayWithPublicKeyParser();
break;
default:
throw new InvalidOperationException("支付认证方式不存在");
}
return payParser;
}
}
}
public abstract class BasePaymentConfigParser {
/**
* 缓存工具类避免多次创建
*/
private static ConcurrentHashMap<String, IPaymentUtil> payUtilCache = new ConcurrentHashMap<>(8);
/**
* 子类检查特定配置信息
* @param configJson 配置 json
* @throws InvalidOperationException 配置异常
*/
abstract void checkConfig(JSONObject configJson) throws InvalidOperationException;
/**
* 子类解析配置生成目标工具类
* @param payConfig 配置类
* @return 支付工具
* @throws Exception 解析配置异常
*/
abstract IPaymentUtil parseConfig(PaymentConfigDO payConfig) throws Exception;
public IPaymentUtil get(PaymentConfigDO payConfig) throws Exception {String cacheKey = payConfig.getPlatformId() + payConfig.getServiceId() + payConfig.getAppId();
if(payUtilCache.containsKey(cacheKey)){return payUtilCache.get(cacheKey);
}else {checkConfig(JSONObject.parseObject(payConfig.getConfigJson()));
IPaymentUtil paymentUtil = parseConfig(payConfig);
payUtilCache.putIfAbsent(cacheKey,paymentUtil);
return paymentUtil;
}
}
}
支付宝普通公钥模式配置解析器:
@NoArgsConstructor
public class AliPayWithPublicKeyParser extends BasePaymentConfigParser{
@Override
void checkConfig(JSONObject configJson) throws InvalidOperationException {String appPrivateKey = configJson.getString("appPrivateKey");
String appPublicKey = configJson.getString("appPublicKey");
if(StringUtils.isEmpty(appPrivateKey) || StringUtils.isEmpty(appPublicKey)){throw new InvalidOperationException("支付宝普通公钥模式配置异常");
}
}
@Override
IPaymentUtil parseConfig(PaymentConfigDO payConfig) {JSONObject configJson =JSONObject.parseObject(payConfig.getConfigJson());
String appPrivateKey = configJson.getString("appPrivateKey");
String appPublicKey = configJson.getString("appPublicKey");
PaymentTypeEnum payType = EnumUtils.getEnum(PaymentTypeEnum.class,payConfig.getPaymentType()).orElse(null);
return new AliPayUtil(payType,payConfig.getAppId(),appPrivateKey,payConfig.getPayNotifyUrl(),payConfig.getGateWayUrl(),
payConfig.getCharSet(),payConfig.getFormat(),payConfig.getPaymentSignType(),appPublicKey);
}
}
在具体工具类中获取请求对象:
@Override
public String getSignStr(String orderName,String outTradeNo, BigDecimal paymentAmount, Map<String,String> hockParams) {
try {AliPayHxClient payClient = getClient();
AlipayResponse alipayResponse = payClient.
buildPayRequest(orderName, outTradeNo, paymentAmount, hockParams).
execute();
return alipayResponse.getBody();}catch (Exception e) {log.error("获取支付宝支付签名字符串异常", e);
return null;
}
}
private AliPayHxClient getClient() throws Exception{AliPayHxClient client = new AliPayHxClient(this.appId, this.privateKey, this.payNotifyUrl, this.gateway, this.charSet, this.format, this.signType,this.redirectUrl);
if (PaymentAuthEnum.ALI_PAY_PUBLIC_KEY.equals(this.authModel)) {
return client.
buildPublicKeyAuth(this.publicKey).
buildPayType(this.typeModel).
build();} else if (PaymentAuthEnum.ALI_PAY_CERT.equals(this.authModel)) {
return client.
buildCertAuth(this.appCertPath, publicCertPath, rootCertPath).
buildPayType(this.typeModel).
build();}else {throw new InvalidOperationException("认证方式异常");
}
}
MyPaymentClient 中通过 paymentRequestBuilder 获取请求对象:
/**
* 支付请求
*/
AliPayHxClient buildPayRequest(String orderName, String outTradeNo, BigDecimal paymentAmount, Map<String, String> hockParams) throws InvalidOperationException {hockParams.put("redirectUrl",redirectUrl);
this.alipayRequest = AliPayRequestBuilder.buildPayRequest(this.typeModel,orderName,outTradeNo,paymentAmount,this.payNotifyUrl,hockParams);
this.business = PaymentBusinessEnum.PAY;
return this;
}
/**
* 退款请求
*/
AliPayHxClient buildRefundRequest(String outTradeNo, String outRefundNo, BigDecimal totalAmount, BigDecimal refundAmount, RefundTypeEnum refundType) throws InvalidOperationException {this.alipayRequest = AliPayRequestBuilder.buildRefundRequest(outTradeNo,outRefundNo,totalAmount,refundAmount,refundType);
this.business = PaymentBusinessEnum.REFUND;
return this;
}
PaymentRequestBuilder 中构建真实请求对象:
/**
* 支付请求
*/
static AlipayRequest buildPayRequest(PaymentTypeEnum payType, String orderName, String outTradeNo, BigDecimal paymentAmount, String payNotifyUrl, Map<String, String> hockParams) throws InvalidOperationException {
AlipayRequest request;
if(PaymentTypeEnum.ALI_PAY_APP.equals(payType)){AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo(outTradeNo);
model.setSubject(orderName);
model.setBody(orderName);
model.setTimeoutExpress("30m");
model.setTotalAmount(Arith.round(paymentAmount.doubleValue(), 2) + "");
model.setPassbackParams("");
model.setProductCode(APP_PAY_PRODUCT_CODE);
request = new AlipayTradeAppPayRequest();
request.setBizModel(model);
}else if((PaymentTypeEnum.ALI_PAY_H5.equals(payType))){String redirectUrl = hockParams.get("redirectUrl");
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(outTradeNo);
model.setSubject(orderName);
model.setBody(orderName);
model.setTotalAmount(Arith.round(paymentAmount.doubleValue(), 2) + "");
model.setTimeoutExpress("30m");
model.setPassbackParams("");
model.setProductCode(H5_PAY_PRODUCT_CODE);
model.setQuitUrl(redirectUrl);
request = new AlipayTradeWapPayRequest();
request.setReturnUrl(redirectUrl);
request.setBizModel(model);
}else {throw new InvalidOperationException("暂不提供该支付方式");
}
request.setNotifyUrl(payNotifyUrl);
return request;
}
代码结构
DO 层:
Domain 层:
正文完