接入微信支付- Java接入微信支付代码

1,150 阅读7分钟

项目中需要接入支付功能,肯定是绕不开微信支付的。微信生态有着广泛的用户基础,接入微信支付可以使用户更便捷、高效地完成支付。

最近在梳理自己所负责的项目的逻辑,正好整理到了支付中心这块的代码,便有了写这一篇文章的想法。一是为了便于自己之后的查看,再就是也希望能帮助到一些新接触微信支付的同行。

由于作者的能力有限,文章中难免有些不足甚至错误的地方。也欢迎大家指正讨论,大家共同进步。

准备工作

这块内容主要介绍下需要注册的微信相关账号,了解这些的可以直接跳过。

微信商户平台 pay.weixin.qq.com

注册完成并完成微信认证,这些流程及所需的材料按照流程里所需的提供即可,这里不仅行展开。

进入产品中心申请需要开通的产品,在后面的代码中会对接JSAPI、H5、APP、NATIVE四种支付方式,大家按照自己实际的业务需求开通申请即可。

image.png

  • JSAPI支付:JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,该支付可以用于微信内置浏览器打开的页面中。默认会开通。
  • APP支付:App支付是指商户通过在移动端应用App中集成开放SDK调起微信支付模块来完成支付。需要在开放平台创建移动应用。
  • H5支付:H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。
  • NATIVE支付:Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。

在产品中心的AppID账号管理中,进行绑定需要接入支付功能的微信应用。

在产品中心的开发配置中,添加需要发起JSAPI支付的授权目录,否则前端无法调起微信支付。

image.png

在账户中心找到API安全,配置API密钥及证书。需要保存好证书文件和API密钥,后面对接API时会用到。

image.png

微信公众平台 mp.weixin.qq.com

注册类型请选择服务号,并完成微信认证。在微信支付页面关联商户平台。

image.png

微信开放平台 open.weixin.qq.com

接入APP支付需要注册该平台账号,否则可以跳过。在其中创建移动应用。

image.png

数据库表结构

完成上面的准备工作后,我们便可进行开发了。

考虑到后续增加商户和微信应用的便利性,我这里是将商户和支付渠道使用数据库存储。

数据库表贴出来的并不全面,这里只是展示了商户表、支付渠道表、订单表和支付日志表的一些信息。至于产品数据、订单产品明细及其他业务数据这里不进行展开,根据各自的业务自行设计即可。

商户表和支付渠道表中的信息是在上面在上面创建的微信应用中便可获取到。

商户表

image.png

支付渠道表

一条支付渠道对应一个微信应用和交易类型。交易类型即微信提供的那几种支付方式。

image.png

订单表

image.png

支付日志表

image.png

数据库表根据各自的业务情况进行设计即可,我这里只是整了一个简单的示例。

示例代码

在写代码之前,我们先简单的了解下对接微信支付的大致流程。了解了是怎么的流程后我们更清晰的明白我们作为服务端需要开发哪些功能。

image.png 在自己系统中的下单代码这里就不粘贴了,示例代码中包括上图中的2、3和6三步的代码。

微信支付API文档:pay.weixin.qq.com/doc/v3/merc…

这里使用了开源的工具,这里封装了和微信交互的API,可以节省很多工作,引入其maven依赖:

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>wx-java-pay-spring-boot-starter</artifactId>
    <version>${wx-java.version}</version>
</dependency>

客户端发起支付

请求参数

{
  "orderNo": "", // 下单接口获取到的系统生成订单号
  "paymentChannelId": 1, // 支付渠道ID
  "ip": "", // ip地址 从request中获取
  "openId": "" // JSAPI支付需要使用静默授权获取支付用户对应的openId
}

响应数据

{
  "paymentChannelType": "100", // 支付渠道类型
  "orderAmount": "0.01", // 金额
  "payParams": {}, // 其他支付的参数
  "codeUrl": "", // native支付生成二维码中的内容
  "mwebUrl": "" // H5支付拉起支付的url
}

支付的代码很适合使用策略模式,这里我们也使用策略模式来实现对应的逻辑。

策略接口PayStrategy代码如下:

public interface PayStrategy {
    /**
     * 获取支付渠道类型
     *
     * @return 支付渠道类型
     */
    PaymentChannelTypeEnum getPaymentChannelType();

    /**
     * 发起支付
     *
     * @param paymentChannel 支付渠道
     * @param payDTO         发起支付对象
     * @return 发起支付结果
     */
    PayResultDTO pay(PaymentChannel paymentChannel, PayDTO payDTO);
}

支付策略抽象实现类PayAbstractStrategy代码如下:

@Slf4j
public abstract class PayAbstractStrategy implements PayStrategy {
    @Resource
    protected IPaymentChannelService paymentChannelService;
​
    @Resource
    protected IOrderService orderService;
​
    @Resource
    private YhzbRedisClient yhzbRedisClient;
​
    @Override
    public PayResultDTO pay(PaymentChannel paymentChannel, PayDTO payDTO) {
        String lockKey = RedisConstant.PAY_LOCK_PREFIX + payDTO.getOrderNo();
​
        boolean lockResult = yhzbRedisClient.setNx(lockKey, "", RedisConstant.DEFAULT_LOCK_DURATION);
        if (!lockResult) {
            throw new BaseException(BaseResultCode.OPERATE_TOO_OFTEN);
        }
​
        try {
            Order order = orderService.getByOrderNo(payDTO.getOrderNo());
            if (order == null) {
                throw new BaseException(BusinessResultCode.ORDER_NOT_EXISTS);
            }
            if (!Objects.equals(order.getStatus(), OrderStatusEnum.WAIT_PAY.getCode())) {
                throw new BaseException(BusinessResultCode.ORDER_STATUS_FAIL, "订单非待支付,不可进行该操作...");
            }
            return this.pay(paymentChannel, order, payDTO);
        } finally {
            yhzbRedisClient.del(lockKey);
        }
    }
​
    protected abstract PayResultDTO pay(PaymentChannel paymentChannel, Order order, PayDTO payDTO);
}

微信支付抽象类WxPayAbstractStrategy代码如下:

@Slf4j
public abstract class WxPayAbstractStrategy extends PayAbstractStrategy {
    @Resource
    private IOrderItemService orderItemService;
​
    @Value("${pay.notify.domain:}")
    private String payNotifyDomain;
​
    @Override
    protected PayResultDTO pay(PaymentChannel paymentChannel, Order order, PayDTO payDTO) {
        WxPayService wxPayService = paymentChannelService.getWxPayService(paymentChannel);
​
        WxPayUnifiedOrderV3Request unifiedOrderV3Request = new WxPayUnifiedOrderV3Request();
        unifiedOrderV3Request.setOutTradeNo(order.getOrderNo());
​
        if (Objects.equals(this.getPaymentChannelType(), PaymentChannelTypeEnum.WX_JSAPI)) {
            if (StringUtils.isBlank(payDTO.getOpenId())) {
                throw new BaseException(BaseResultCode.PARAM_VALID_ERROR, "支付用户OPENID不能为空");
            }
​
            WxPayUnifiedOrderV3Request.Payer payer = new WxPayUnifiedOrderV3Request.Payer();
            payer.setOpenid(payDTO.getOpenId());
​
            unifiedOrderV3Request.setPayer(payer);
        }
        int totalFee = order.getPayAmount().multiply(new BigDecimal("100")).setScale(0, RoundingMode.DOWN).intValue();
​
        WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount();
        amount.setTotal(totalFee);
        unifiedOrderV3Request.setAmount(amount);
​
        Date expireDate = DateUtil.offsetSecond(order.getCreatedTime(), 60 * 30);
        String expireTimeStr = DateUtil.format(expireDate, "yyyy-MM-dd") + "T" + DateUtil.format(expireDate, "HH:mm:ss") + "+08:00";
        unifiedOrderV3Request.setTimeExpire(expireTimeStr);
​
        WxPayUnifiedOrderV3Request.SceneInfo sceneInfo = new WxPayUnifiedOrderV3Request.SceneInfo();
        sceneInfo.setPayerClientIp(payDTO.getIp());
        unifiedOrderV3Request.setSceneInfo(sceneInfo);
​
        unifiedOrderV3Request.setNotifyUrl(payNotifyDomain + "/api/pay/wx" + paymentChannel.getId() + "/notify");
​
        try {
            WxPayUnifiedOrderV3Result unifiedOrderV3Result = wxPayService.unifiedOrderV3(this.getTradeType(), unifiedOrderV3Request);
​
            orderService.pay(order, paymentChannel);
​
            // TODO: 添加支付日志
​
​
            return this.buildPayResultDTO(wxPayService, paymentChannel, order, unifiedOrderV3Result);
        } catch (WxPayException e) {
            log.error("订单发起支付失败,支付请求对象:【{}】", JSON.toJSONString(payDTO), e);
​
            throw new BaseException(BusinessResultCode.ORDER_PAY_FAIL);
        }
    }
​
    /**
     * 构建前端调起微信支付参数
     *
     * @param payService              支付服务
     * @param paymentChannel          支付渠道
     * @param wxPayUnifiedOrderResult 微信下单结果
     * @return 参数
     */
    protected Map<String, String> buildPayParams(WxPayService payService, PaymentChannel paymentChannel, WxPayUnifiedOrderV3Result wxPayUnifiedOrderResult) {
        return new HashMap<>();
    }
​
    /**
     * 获取交易类型
     *
     * @return 交易类型
     */
    protected abstract TradeTypeEnum getTradeType();
​
    /**
     * 构建支付结果
     *
     * @param payService              支付服务
     * @param paymentChannel          支付渠道
     * @param order                   订单
     * @param wxPayUnifiedOrderResult 微信下单结果
     * @return 支付结果
     */
    private PayResultDTO buildPayResultDTO(WxPayService payService, PaymentChannel paymentChannel, Order order, WxPayUnifiedOrderV3Result wxPayUnifiedOrderResult) {
​
        PayResultDTO resultDTO = new PayResultDTO();
        resultDTO.setPaymentChannelType(this.getPaymentChannelType().getCode());
​
        DecimalFormat df = new DecimalFormat("0.00");
        String orderAmount = df.format(order.getPayAmount()).equals(".00") ? "0.00" : df.format(order.getPayAmount());
        resultDTO.setOrderAmount(orderAmount);
​
        resultDTO.setPayParams(this.buildPayParams(payService, paymentChannel, wxPayUnifiedOrderResult));
​
        resultDTO.setMwebUrl(wxPayUnifiedOrderResult.getH5Url());
​
        if (StringUtils.isNotBlank(wxPayUnifiedOrderResult.getCodeUrl())) {
            resultDTO.setCodeUrl(wxPayUnifiedOrderResult.getCodeUrl());
        }
​
        return resultDTO;
    }
}

微信JSAPI支付实现类

@Slf4j
@Service
public class WxJSAPIPayStrategy extends WxPayAbstractStrategy {
​
    @Override
    public PaymentChannelTypeEnum getPaymentChannelType() {
        return PaymentChannelTypeEnum.WX_JSAPI;
    }
​
    @Override
    protected Map<String, String> buildPayParams(WxPayService payService, PaymentChannel paymentChannel, WxPayUnifiedOrderV3Result wxPayUnifiedOrderResult) {
        WxPayConfig config = payService.getConfig();
        String appId = config.getAppId();
        String mchId = config.getMchId();
​
        WxPayUnifiedOrderV3Result.JsapiResult jsapiResult = wxPayUnifiedOrderResult.getPayInfo(this.getTradeType(), appId, mchId, config.getPrivateKey());
​
        Map<String, String> params = new HashMap<>();
​
        params.put("appId", jsapiResult.getAppId());
        params.put("timeStamp", jsapiResult.getTimeStamp());
        params.put("nonceStr", jsapiResult.getNonceStr());
        params.put("package", jsapiResult.getPackageValue());
        params.put("signType", jsapiResult.getSignType());
​
        params.put("paySign", jsapiResult.getPaySign());
​
        return params;
    }
​
    @Override
    protected TradeTypeEnum getTradeType() {
        return TradeTypeEnum.JSAPI;
    }
}

微信APP支付实现类

@Slf4j
@Service
public class WxAppPayStrategy extends WxPayAbstractStrategy {
​
    @Override
    public PaymentChannelTypeEnum getPaymentChannelType() {
        return PaymentChannelTypeEnum.WX_APP;
    }
​
    @Override
    protected Map<String, String> buildPayParams(WxPayService payService, PaymentChannel paymentChannel, WxPayUnifiedOrderV3Result wxPayUnifiedOrderResult) {
        WxPayConfig config = payService.getConfig();
        String appId = config.getAppId();
        String mchId = config.getMchId();
​
        Map<String, String> params = new HashMap<>();
        WxPayUnifiedOrderV3Result.AppResult appResult = wxPayUnifiedOrderResult.getPayInfo(this.getTradeType(), appId, mchId, config.getPrivateKey());
​
        params.put("appId", appResult.getAppid());
        params.put("timeStamp", appResult.getTimestamp());
        params.put("nonceStr", appResult.getNoncestr());
        params.put("prepayId", appResult.getPrepayId());
        params.put("paySign", appResult.getSign());
​
        params.put("package", appResult.getPackageValue());
​
        params.put("partnerId", appResult.getPartnerId());
​
        return params;
    }
​
    @Override
    protected TradeTypeEnum getTradeType() {
        return TradeTypeEnum.APP;
    }
}

微信NATIVE支付实现类

@Slf4j
@Service
public class WxNativePayStrategy extends WxPayAbstractStrategy {
​
    @Override
    public PaymentChannelTypeEnum getPaymentChannelType() {
        return PaymentChannelTypeEnum.WX_NATIVE;
    }
​
    @Override
    protected TradeTypeEnum getTradeType() {
        return TradeTypeEnum.NATIVE;
    }
}

微信H5支付实现类

@Slf4j
@Service
public class WxH5PayStrategy extends WxPayAbstractStrategy {
​
    @Override
    public PaymentChannelTypeEnum getPaymentChannelType() {
        return PaymentChannelTypeEnum.WX_H5;
    }
​
    @Override
    protected TradeTypeEnum getTradeType() {
        return TradeTypeEnum.H5;
    }
}

支付渠道PaymentChannelServiceImpl相关代码

@Service
public class PaymentChannelServiceImpl extends ServiceImpl<PaymentChannelMapper, PaymentChannel> implements IPaymentChannelService {
    private static final ConcurrentHashMap<Integer, WxPayService> WX_PAY_SERVICE_MAP = new ConcurrentHashMap<>();

    @Resource
    private IMachineService machineService;

    @Resource
    private YhzbRedisClient yhzbRedisClient;

    @Override
    public WxPayService getWxPayService(PaymentChannel paymentChannel) {
        WxPayService wxPayService = WX_PAY_SERVICE_MAP.get(paymentChannel.getId());
        if (wxPayService == null) {
            Machine machine = machineService.queryById(paymentChannel.getMachineId());
            if (machine == null) {
                throw new BaseException(BaseResultCode.PARAM_VALID_ERROR, "支付渠道无效...");
            }
            WxPayConfig payConfig = new WxPayConfig();
            payConfig.setAppId(paymentChannel.getAppId());
            payConfig.setMchId(machine.getMchId());
            payConfig.setMchKey(machine.getKey());
            payConfig.setApiV3Key(machine.getKey());
            payConfig.setKeyPath(machine.getKeyPath());
            payConfig.setPrivateKeyPath(machine.getPrivateKeyPath());
            payConfig.setPrivateCertPath(machine.getPrivateCertPath());

            payConfig.setUseSandboxEnv(false);

            wxPayService = new WxPayServiceImpl();
            wxPayService.setConfig(payConfig);

            WX_PAY_SERVICE_MAP.put(paymentChannel.getId(), wxPayService);
        }
        return wxPayService;
    }

    @Override
    public PaymentChannel queryById(Integer id) {
        String cacheKey = RedisConstant.PAYMENT_CHANNEL_CACHE_PREFIX + id;

        PaymentChannel paymentChannel = (PaymentChannel) yhzbRedisClient.get(cacheKey);

        if (paymentChannel == null) {
            QueryWrapper<PaymentChannel> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq(PaymentChannel.IS_DELETE, YesNoEnum.NO.getCode())
                    .eq(PaymentChannel.ID, id);

            paymentChannel = this.getOne(queryWrapper);

            if (paymentChannel != null) {
                yhzbRedisClient.set(cacheKey, paymentChannel, RedisConstant.DEFAULT_DURATION);
            }
        }
        return paymentChannel;
    }
}

订单OrderServiceImpl相关代码

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
    @Resource
    private YhzbRedisClient yhzbRedisClient;

    @Override
    public Order getByOrderNo(String orderNo) {
        String cacheKey = RedisConstant.ORDER_CACHE_PREFIX + orderNo;

        Order order = (Order) yhzbRedisClient.get(cacheKey);

        if (order == null) {
            QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq(Order.IS_DELETE, YesNoEnum.NO.getCode())
                    .eq(Order.ORDER_NO, orderNo);

            order = this.getOne(queryWrapper);

            if (order != null) {
                yhzbRedisClient.set(cacheKey, order, RedisConstant.DEFAULT_DURATION);
            }
        }
        return order;
    }

    @Override
    public void pay(Order order, PaymentChannel paymentChannel) {
        Order editEntity = new Order();
        editEntity.setId(order.getId());
        editEntity.setStatus(OrderStatusEnum.PAYING.getCode());
        editEntity.setPaymentChannelId(paymentChannel.getId());
        editEntity.setPaymentChannelType(paymentChannel.getType());

        this.updateById(editEntity);

        String cacheKey = RedisConstant.ORDER_CACHE_PREFIX + order.getOrderNo();
        yhzbRedisClient.del(cacheKey);
    }

    @Override
    public void paySuccess(String orderNo, String partnerOrderNo, String openid, PaymentChannel paymentChannel) {
        Order order = this.getByOrderNo(orderNo);
        if (order == null || Objects.equals(order.getStatus(), OrderStatusEnum.PAYED.getCode())) {
            return;
        }

        Order editEntity = new Order();
        editEntity.setId(order.getId());
        editEntity.setPartnerOrderNo(partnerOrderNo);
        editEntity.setStatus(OrderStatusEnum.PAYED.getCode());
        editEntity.setPayTime(new Date());
        editEntity.setPaymentChannelId(paymentChannel.getId());
        editEntity.setPaymentChannelType(paymentChannel.getType());

        this.updateById(editEntity);

        String cacheKey = RedisConstant.ORDER_CACHE_PREFIX + order.getOrderNo();
        yhzbRedisClient.del(cacheKey);

        // TODO: 发送订单变更MQ消息
    }
}

到这里对接发起支付的代码就编写完成了。

微信支付结果通知

支付完成之后我们处理自身业务的逻辑是接收微信的回调通知。也就是我们需要给微信提供一个接口。这个回调地址是在调用微信下单接口时传递过去的。微信的回调会在一定的时间里重试,回调频率为:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m

回调接口定义

image.png

回调逻辑处理代码

image.png

回调的代码示例也大致就这样,在示例代码中为了简单我这里使用了同步的方式处理。建议大家接入RocketMQ或其他消息中间件使用异步的方式,接收到微信的回调时间,发送消息后直接返回成功数据。

好了,今天的文章到此结束了,希望本文能对你有那么些许的帮助。欢迎大家点赞加关注。

有问题或者更好的想法也欢迎大家留言讨论,大家一起学习进步!

如果本文对您有些许的帮助,请大方的关注作者吧。

欢迎大家关注我的公众号为我加油打气:【Bug搬运小能手】。文章会优先在公众号发布。