统一支付设计

380 阅读4分钟

支付结果通知

    /**
     * 支付结果-微信通知
     * 一共有两层保险,第一个是分布式锁,第二个是数据库乐观锁
     * @param id 交易编号
     * @param request http 请求对象
     */
    @Inner(value = false)
    @ApiOperation("支付结果-微信通知")
    @PostMapping("/transaction/{id}/result/wx") @Transactional
    public void wxPayResult(@PathVariable("id") long id, HttpServletRequest request) {
        Trade trade = tradeService.getById(id);

        // 加入分布式锁
        String lockKey = "pay_".concat(trade.getOrderNo());
        redisLocker.lockUp(lockKey, 60);

        String body = getRequestBody(request);

        TenantPaySetup paySetting = tenantPaySetupCall.getByTenantId(trade.getTenantId());

        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(request.getHeader("Wechatpay-Serial"))
                .nonce(request.getHeader("Wechatpay-Nonce"))
                .signature(request.getHeader("Wechatpay-Signature"))
                .timestamp(request.getHeader("Wechatpay-Timestamp"))
                .body(body)
                .build();
        NotificationConfig config = wxConfigManager.getMerchantConfig(trade.getTenantId());

        NotificationParser parser = new NotificationParser(config);
        Transaction transaction = null;
        try {
            transaction = parser.parse(requestParam, Transaction.class);
        } catch (Exception e) {
            log.error("支付结果验证失败,拒绝处理。");
            throw new RuntimeException(e);
        }

        // 微信支付成功
        if (transaction.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
            try {
                if (trade.getStatus().equals(2)) {

                    log.info("数据已被其他线程更新,执行跳过");

                } else if (trade.getStatus().equals(3)) {

                    // 没有成功收到微信的成功通知,被系统超时关闭了
                    log.info("订单关闭,走退款");
                    wxRefundService.create(trade, "交易已关闭,资金原路退回");
                }

                /* 如果同笔订单已支付成功,后面的交易记录执行退款 */
                long successTradeCount = tradeService.count(
                        Wrappers.<Trade>lambdaQuery()
                                .eq(Trade::getOrderNo, trade.getOrderNo())
                                .eq(Trade::getStatus, 2)
                );

                if (successTradeCount > 0) {
                    log.info("同笔订单已支付成功,交易执行退款,orderNo:{}", trade.getOrderNo());
                    wxRefundService.create(trade, "交易已关闭,资金原路退回");
                    return;
                }

                // 支付成功,商品订单和交易记录更新为支付成功
                String tradeNo =  transaction.getOutTradeNo();
                int version = trade.getVersion();
                trade.setStatus(2);
                trade.setVersion(version + 1);
                boolean updateResult = tradeService.update(trade,
                        Wrappers.<Trade>lambdaUpdate()
                                .eq(Trade::getId, trade.getId())
                                .eq(Trade::getVersion, version)
                );
                if (updateResult) {
                    log.info("支付成功,交易记录已更新");
                    PaySuccessMessage successMessage = new PaySuccessMessage();
                    successMessage.setChannel(trade.getChannel());
                    successMessage.setMethod(trade.getMethod());
                    successMessage.setOrderNo(trade.getOrderNo());
                    successMessage.setTradeNo(trade.getId());
                    amqpTemplate.convertAndSend(trade.getQueue(), trade.getRoutingKey(), JSON.toJSONString(successMessage));
                    log.info("向内部系统广播成功状态");

                    // 交易记录已被更新,继续重试
                } else {

                    boolean retry = true;
                    while (retry) {
                        log.info("交易记录已被更新,继续重试");
                        Trade lockTrade = tradeService.getById(tradeNo);
                        // 继续更新
                        if (lockTrade.getStatus().equals(1)) {
                            version = lockTrade.getVersion();
                            lockTrade.setStatus(2);
                            lockTrade.setVersion(version + 1);
                            updateResult = tradeService.update(lockTrade,
                                    Wrappers.<Trade>lambdaUpdate()
                                            .eq(Trade::getId, trade.getId())
                                            .eq(Trade::getVersion, version)
                            );
                            if (updateResult) {
                                log.info("支付成功,交易记录已更新");

                                PaySuccessMessage successMessage = new PaySuccessMessage();
                                successMessage.setChannel(trade.getChannel());
                                successMessage.setMethod(trade.getMethod());
                                successMessage.setOrderNo(trade.getOrderNo());
                                successMessage.setTradeNo(trade.getId());
                                amqpTemplate.convertAndSend(trade.getQueue(), trade.getRoutingKey(), JSON.toJSONString(successMessage));
                                log.info("向内部系统广播成功状态");
                                break;
                            }
                            // 交易记录被其他线程更新成功了
                        } else if (lockTrade.getStatus().equals(2)) {
                            log.info("交易记录已被其他线程更新为支付成功,跳过处理");
                            break;

                            // 订单被关闭
                        } else if (lockTrade.getStatus().equals(3)) {
                            log.info("订单关闭,走退款");
                            wxRefundService.create(trade, "交易已关闭,资金原路退回");
                            break;
                        } else {
                            log.info("数据已被其他线程更新,执行跳过");
                            break;
                        }
                    }
                }
            } catch (AmqpException e) {
                 /* 如果处理失败,将消息保存到队列 */
                amqpTemplate.convertAndSend(PayQueueEnum.QUEUE_PAY_FAILURE, PayQueueEnum.KEY_PAY_FAILURE, JSON.toJSONString(transaction));
                throw new RuntimeException(e);
            }
        } else {
            // 微信支付失败
            // 不用在意微信通道的状态,只要没有成功,就跳过,系统定时器会关闭超时交易订单
            log.info("微信支付失败,description:{}", JSON.toJSONString(transaction));
        }

        // 释放资源
        redisLocker.unlock(lockKey);
    }

退款结果通知

/**
     * 退款结果-微信通知
     * 一共有两层保险,第一个是分布式锁,第二个是数据库乐观锁
     * @param id 交易编号
     * @param request http 请求对象
     */
    @Inner(value = false)
    @ApiOperation("退款结果-微信通知")
    @PostMapping("/transaction/{id}/refund/result/wx") @Transactional
    public void WxRefundResult(@PathVariable("id") long id, HttpServletRequest request) {
        Trade trade = tradeService.getById(id);

        // 加入分布式锁
        String lockKey = "pay_".concat(trade.getOrderNo());
        redisLocker.lockUp(lockKey, 60);

        String body = getRequestBody(request);

        TenantPaySetup paySetting = tenantPaySetupCall.getByTenantId(trade.getTenantId());

        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(request.getHeader("Wechatpay-Serial"))
                .nonce(request.getHeader("Wechatpay-Nonce"))
                .signature(request.getHeader("Wechatpay-Signature"))
                .timestamp(request.getHeader("Wechatpay-Timestamp"))
                .body(body)
                .build();
        NotificationConfig config = wxConfigManager.getMerchantConfig(trade.getTenantId());

        NotificationParser parser = new NotificationParser(config);
        RefundNotification refundNotification = null;
        try {
            refundNotification = parser.parse(requestParam, RefundNotification.class);
        } catch (Exception e) {
            log.error("支付结果验证失败,拒绝处理。");
            throw new RuntimeException(e);
        }

        // 微信支付成功
        if (refundNotification.getRefundStatus().equals(Status.SUCCESS)) {
            try {
                if (trade.getStatus().equals(4)) {
                    log.info("数据已被更新,执行跳过");

                }

                // 支付成功,商品订单和交易记录更新为支付成功
                String tradeNo =  refundNotification.getOutTradeNo();
                int version = trade.getVersion();
                trade.setStatus(4);
                trade.setVersion(version + 1);
                boolean updateResult = tradeService.update(trade,
                        Wrappers.<Trade>lambdaUpdate()
                                .eq(Trade::getId, trade.getId())
                                .eq(Trade::getVersion, version)
                );

                if (updateResult) {
                    log.info("退款成功,交易记录已更新");

                    // 被其他线程更新了
                } else  {
                    boolean retry = true;
                    while (retry) {
                        log.info("交易记录已被更新,继续重试");
                        Trade lockTrade = tradeService.getById(tradeNo);
                        // 继续更新
                        if (!lockTrade.getStatus().equals(4)) {
                            version = lockTrade.getVersion();
                            lockTrade.setStatus(2);
                            lockTrade.setVersion(version + 1);
                            updateResult = tradeService.update(lockTrade,
                                    Wrappers.<Trade>lambdaUpdate()
                                            .eq(Trade::getId, trade.getId())
                                            .eq(Trade::getVersion, version)
                            );
                            if (updateResult) {
                                log.info("退款成功,交易记录已更新");
                                break;
                            }

                            // 交易记录被其他线程更新成功了
                        } else {
                            log.info("数据已被更新,执行跳过");
                            break;
                        }
                    }
                }
            } catch (AmqpException e) {
                /* 如果处理失败,将消息保存到队列 */
                amqpTemplate.convertAndSend(PayQueueEnum.QUEUE_PAY_FAILURE, PayQueueEnum.KEY_PAY_FAILURE, JSON.toJSONString(refundNotification));
                throw new RuntimeException(e);
            }
        } else {
            // 微信支付失败
            // 不用在意微信通道的状态,只要没有成功,就跳过,系统定时器会关闭超时交易订单
            log.info("微信退款失败,description:{}", JSON.toJSONString(refundNotification));
        }

        // 释放资源
        redisLocker.unlock(lockKey);
    }

JS 发起交易

    /**
     * 下单-js
     * @param form 支付表单
     * @return 预支付id
     */
    @Inner
    @ApiOperation("下单-jsapi")
    @PostMapping("/transaction/wx/js") @Transactional
    public String createTradeByJs(@RequestBody PayForm form) {

        Trade trade = new Trade();
        trade.setAmount(new BigDecimal(form.getAmount()));
        trade.setTenantId(form.getTenantId());
        trade.setChannel(form.getChannel());
        trade.setMethod(form.getMethod());
        trade.setOrderNo(form.getOrderNo());
        trade.setVersion(1);
        trade.setStatus(1);
        trade.setUserId(form.getUserId());
        trade.setQueue(form.getQueue());
        trade.setRoutingKey(form.getRoutingKey());

        tradeService.save(trade);

        PrepayResponse response = wxJsPayService.prepay(trade);
        return response.getPrepayId();

    }

PC 二维码支付

    /**
     * 下单-native
     * @param form 支付表单
     * @return 二维码内容 url
     */
    @Inner
    @ApiOperation("下单-native")
    @PostMapping("/transaction/wx/native") @Transactional
    public String createTradeByNative(@RequestBody PayForm form) {

        Trade trade = new Trade();
        trade.setAmount(new BigDecimal(form.getAmount()));
        trade.setTenantId(form.getTenantId());
        trade.setChannel(form.getChannel());
        trade.setMethod(form.getMethod());
        trade.setOrderNo(form.getOrderNo());
        trade.setVersion(1);
        trade.setStatus(1);
        trade.setUserId(form.getUserId());
        trade.setQueue(form.getQueue());
        trade.setRoutingKey(form.getRoutingKey());

        tradeService.save(trade);

        String codeUrl = wxNativePayService.prepay(trade).getCodeUrl();
        return codeUrl;

    }

微信接入文档

签名验证 pay.weixin.qq.com/wiki/doc/ap…

sdk github github.com/wechatpay-a…

下单接口 pay.weixin.qq.com/wiki/doc/ap…

开发注意的问题

商户证书配置只能存在一个,因为创建 RSAAutoCertificateConfig 会开启一个定时器,多个config会冲突。 github.com/wechatpay-a…