支付系统设计实现4统一配置中心

49次阅读

共计 9850 个字符,预计需要花费 25 分钟才能阅读完成。

前因后果

最近公司正在做一个统一的商城平台,也就是允许不同的 App 进入商城进行商品的创建,销售,结算,对账。由于我们拥有 5 个不同客户端包括了 App 与微信小程序,很显然不可能去同时维护 5 个不同的商城。于是我们提出了统一商城的概念,不同的客户端入驻商城由商城分配 platformId(客户端平台) 与 serviceId(客户端服务)。一个 platform 可以对应多个 service,例如 App 可以提供实体商品销售与网络课程的服务。
这样一来的话有如下好处:

  • 商城只需要开发一次,易于维护。
  • 对账清晰,全部从商城开始对账,通过订单的 platformId 与 serviceId 进行区分。
  • 商家可以选择平台入驻并提供服务。
  • 后期商城可以转向微服务。

同时也带了一些复杂的问题:

  • 不同的客户端支付方式不同。
  • 不同平台的商品需求不同。
  • 平台,服务,商家,商品,订单,资金的关系如何维护。

由于我负责支付模块的开发,这里我们只针对支付模块进行讨论。在开发之前我们先来整理一下支付模块需要开发哪些功能?

  1. 根据不同的平台与服务提供不同的支付方式。
  2. 需要给支付订单增加前缀来进行区分 (方便对账)。
  3. 集成所有需要提供的支付方式(微信 App,支付宝 App,微信小程序,微信 Wap,支付宝 Wap)。

接下来我们再整理一下模块中最复杂的难点:

  • 如何抽象支付的流程保证可扩展性?

难点分析

抽象是最复杂也是最考验程序员业务分析能力的地方,我们应该如何切分支付业务将变与不变的地方切分开来。由于微信与支付宝的 api 有所不同,并且各种支付方式的 api 也会不同。我进行了如下切分:

首先是不变的地方:

  1. 各个支付方式的 api 是不会变的。
  2. 支付的流程是不会变的(可以参考本系列第一篇文章)。
  3. 退款的流程是不会变的。

变的地方:

  1. 各个支付的 api 是不同的,所需参数,所调用的接口都不同。
  2. 前端所需要唤醒支付键盘的参数是不同的。
  3. 认证用户的方式不同。

根据以上分析,我们绘制如下的类图:

  • 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 层:

正文完
 0