【Spring Boot组件集成实战】集成微信支付服务

更多精彩内容,请访问 Spring Boot组件集成实战专栏

推荐项目:一套基于Spring Boot+Layui的内容管理系统/快速开发脚手架(含完整的开发文档、演示网址等)

0. 项目演示

正式开始之前,请看本文项目的演示视频。

演示地址1:https://www.bilibili.com/video/BV1cS4y1f7PP/

演示地址2:https://live.csdn.net/v/184413

1. 开发环境准备

在接入微信支付之前,需要先在微信支付商户平台入驻,成为商家,才能继续后续的开发。

微信支付商户平台网址:https://pay.weixin.qq.com

不过,个人用户是无法成为商家的,只有以下几种用户类型才可以成为商家

image-20220115145322097

成为商家后,需要完成证书申请、秘钥配置、产品申请等操作,具体如下。

1.1 申请证书与秘钥配置

进入微信支付商户平台,在账号中心→API安全页面,完成API证书的申请,和API秘钥的配置,如下图所示。

image-20220115145749066

API证书申请过程稍微复杂,但是官方有详细的申请较长,点击右侧的查看指引按钮,根据教程一步一步来操作即可。

两种API秘钥则是自定义的32个字符的字符串,任意填写并记住即可。

1.2 申请产品

进入微信支付商户平台,在产品中心→我的产品页面,可以查看当前商户已开通未开通产品,根据项目需求,自行申请开通即可。

image-20220115150243817

1.3 开发配置信息填写

进入微信支付商户平台,在产品中心→开发配置页面,记下本商户的商户号,并填写已申请支付产品的各项授权目录回调链接等信息。

image-20220115150520867

1.4 APPID账号管理

进入微信支付商户平台,在产品中心→AppID账号管理页面,关联诸如服务号、订阅号、小程序、企业微信、移动应用、网站应用等的APPID,如下图所示。

image-20220115150707060

至此,微信支付商户的基本信息配置完毕,总结下来,以下5项信息是必须的:

  • 商户号
  • AppID
  • API证书(3个文件)
  • APIv2秘钥
  • APIv3秘钥
  • 回调链接

2. 项目创建与依赖导入

2.1 创建Spring Boot项目

创建Spring Boot项目的教程太多太多了…比如:https://cxhit.blog.csdn.net/article/details/113782979,所以这里就不再赘述。

项目结构如下图所示。

image-20220115151044879

2.2 导入依赖

pom.xml文件中,引入微信支付的第三方SDK依赖:

<!-- 微信支付的核心依赖 -->
<!-- https://github.com/Wechat-Group/WxJava -->
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-pay</artifactId>
    <version>4.2.2.B</version>
</dependency>

其中最新版本可前往Maven官方仓库查看。

3. 实现支付服务类

3.1 创建配置实体类

com.cxhit.pay.wechat.entity包下,新建名为WeChatPayEntity实体类,并写入如下内容。

package com.cxhit.pay.wechat.entity;

import java.io.Serializable;

/**
 * 微信支付配置信息
 *
 * @author 拾年之璐
 * @since 2022/1/13 17:20
 */
public class WeChatPayEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 必填:微信支付商户号
     */
    private String mchId;

    /**
     * 必填:商户绑定的微信公众号、小程序、开放平台等的appid
     */
    private String appId;

    /**
     * 必填:APIv2密钥(调用v2版本的API时,需用APIv2密钥生成签名)
     */
    private String mchKey;

    /**
     * 必填:APIv3密钥(调用APIv3的下载平台证书接口、处理回调通知中报文时,要通过该密钥来解密信息)
     */
    private String apiV3Key;

    /**
     * 必填:apiclient_cert.p12证书文件的绝对路径,或者以classpath:开头的类路径。
     */
    private String keyPath;

    /**
     * 必填:apiclient_key.pem证书文件的绝对路径,或者以classpath:开头的类路径。
     */
    private String privateKeyPath;

    /**
     * 必填:apiclient_cert.pem证书文件的绝对路径,或者以classpath:开头的类路径。
     */
    private String privateCertPath;

    /**
     * 必填:微信支付异步回调通知地址。通知url必须以https开头(SSL协议),外网可访问,不能携带参数。
     */
    private String notifyUrl;
}

注意:为了减少代码篇幅,此代码引入了lombok。如果没有使用lombok,请自行生成Get和Set方法。

3.2 实现支付服务类

com.cxhit.pay.wechat.service包下,新建名为WeChatPayService的服务类,并写入如下代码。

以下代码实现了基本的配置和支付、退款、查询功能,有详细的注释。

另注意:以下代码中使用了Hutool组件的Id生成工具(用于生成订单号),请在pom文件中自行添加hutool的依赖。

package com.cxhit.pay.wechat.service;

import com.cxhit.pay.wechat.entity.WeChatPayEntity;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;


/**
 * 微信支付服务类(只实现V3接口)
 *
 * @author 拾年之璐
 * @since 2022/1/13 17:32
 */
@Service
public class WeChatPayService {

    // 是否启用沙箱环境。微信支付的沙箱环境贼垃圾...好多年不维护...千万不要用。。。
    private static final Boolean SAND_BOX_ENV = false;
    // 支付结果回调地址
    private static final String NOTIFY_URL = "https://open.zxdmy.com/api/wx/pay/notify";


    /**
     * 获取微信支付相关接口服务(后续的几个服务方法,实现了基本的实例)
     * (此接口也可以直接在controller中使用)
     *
     * @return 微信支付服务接口
     */
    public WxPayService getWxPayService() {
        // TODO 此处可以从数据库读取微信支付的相关秘钥、证书等配置信息。但我们这里就直接写入静态数据进行演示
        WeChatPayEntity weChatPayEntity = new WeChatPayEntity();
        // 1. 填充基本信息(商户号与APPID)
        weChatPayEntity.setMchId("15333333333");
            weChatPayEntity.setAppId("wx123456789101112");
        // 2. 填充秘钥信息
        weChatPayEntity.setMchKey("abcdefghabcdefghabcdefghabcdefgh");
        weChatPayEntity.setApiV3Key("abcdefghabcdefghabcdefghabcdefgh");
        // 3. 填充证书路径信息
        weChatPayEntity.setKeyPath("E:\\微信支付\\Cert\\apiclient_cert.p12");
        weChatPayEntity.setPrivateKeyPath("E:\\微信支付\\Cert\\apiclient_key.pem");
        weChatPayEntity.setPrivateCertPath("E:\\微信支付\\Cert\\apiclient_cert.pem");
        // 4. 填充回调URL
        weChatPayEntity.setNotifyUrl(NOTIFY_URL);

        // 以下代码无需修改
        // 生成配置
        WxPayConfig payConfig = new WxPayConfig();
        // 填充基本配置信息
        payConfig.setAppId(StringUtils.trimToNull(weChatPayEntity.getAppId()));
        payConfig.setMchId(StringUtils.trimToNull(weChatPayEntity.getMchId()));
        payConfig.setMchKey(StringUtils.trimToNull(weChatPayEntity.getMchKey()));
        payConfig.setApiV3Key(StringUtils.trimToNull(weChatPayEntity.getApiV3Key()));
        payConfig.setKeyPath(StringUtils.trimToNull(weChatPayEntity.getKeyPath()));
        payConfig.setPrivateCertPath(StringUtils.trimToNull(weChatPayEntity.getPrivateCertPath()));
        payConfig.setPrivateKeyPath(StringUtils.trimToNull(weChatPayEntity.getPrivateKeyPath()));
        payConfig.setNotifyUrl(StringUtils.trimToNull(weChatPayEntity.getNotifyUrl()));
        // 创建配置服务
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(payConfig);
        // 可以指定是否使用沙箱环境
        payConfig.setUseSandboxEnv(SAND_BOX_ENV);
        if (SAND_BOX_ENV) {
            try {
                payConfig.setMchKey(wxPayService.getSandboxSignKey());
                wxPayService.setConfig(payConfig);
            } catch (WxPayException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        // 返回结果
        return wxPayService;
    }

    /**
     * 下单接口(只设置了必填信息)(V3版本)
     *
     * @param tradeType   必填:交易类型:jsapi(含小程序)、app、h5、native
     * @param description 必填:商品描述(商品标题)
     * @param outTradeNo  必填:商家订单号
     * @param total       必填:商品金额(单位:分)
     * @param openId      特殊必填:支付用户的OpenId,JSAPI支付时必填。
     * @return 支付返回结果:{0:Y|N,1:支付结果} <br>
     * 关于支付结果: <br>
     * APP支付、JSAPI支付为[预支付交易会话标识] <br>
     * Native支付为[二维码链接] <br>
     * H5支付为[支付跳转链接]
     */
    public String[] pay(String tradeType, String description, String outTradeNo, Integer total, String openId) {
        // 构建统一下单请求参数对象
        WxPayUnifiedOrderV3Request wxPayUnifiedOrderV3Request = new WxPayUnifiedOrderV3Request();
        // 对象中写入数据
        wxPayUnifiedOrderV3Request
                // 【1】必填信息
                // 商品描述:必填
                .setDescription(description)
                // 商户订单号:必填,同一个商户号下唯一
                .setOutTradeNo(outTradeNo)
                // 通知地址:必填,公网域名必须为https,外网可访问。可不填,通过配置信息读取(但这个组件没写...)
                .setNotifyUrl(NOTIFY_URL)
                // 订单金额:单位(分)
                .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(total))
                // 【2】选填信息
                // 附加信息
                .setAttach("附加信息")
                // 订单优惠标记
                // ...
                .setGoodsTag("ABCD");

        try {
            // 根据请求类型,返回指定类型,其中包含:【3】条件必填信息
            switch (tradeType.toLowerCase()) {
                // Native支付
                case "native":
                    return new String[]{
                            "Y", this.getWxPayService().unifiedOrderV3(TradeTypeEnum.NATIVE, wxPayUnifiedOrderV3Request).getCodeUrl()
                    };
                // JSAPI支付
                case "jsapi":
                    // 用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid
                    wxPayUnifiedOrderV3Request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId));
                    return new String[]{
                            "Y", this.getWxPayService().unifiedOrderV3(TradeTypeEnum.JSAPI, wxPayUnifiedOrderV3Request).getPrepayId()
                    };
                // H5支付
                case "h5":
                    wxPayUnifiedOrderV3Request.setSceneInfo(
                            new WxPayUnifiedOrderV3Request.SceneInfo()
                                    // 用户终端IP
                                    .setPayerClientIp("12.34.56.78")
                                    .setH5Info(
                                            new WxPayUnifiedOrderV3Request.H5Info()
                                                    // 场景类型
                                                    .setType("wechat")
                                    )
                    );
                    return new String[]{
                            "Y", this.getWxPayService().unifiedOrderV3(TradeTypeEnum.H5, wxPayUnifiedOrderV3Request).getH5Url()
                    };
                // APP支付
                case "app":
                    return new String[]{
                            "Y", this.getWxPayService().unifiedOrderV3(TradeTypeEnum.APP, wxPayUnifiedOrderV3Request).getPrepayId()
                    };
                default:
                    // throw new RuntimeException("输入的[" + tradeType + "]不合法,只能为native、jsapi、h5、app其一,请核实!");
                    return new String[]{
                            "N", "输入的[" + tradeType + "]不合法,只能为native、jsapi、h5、app其一,请核实!"
                    };
            }
        } catch (WxPayException e) {
            // throw new RuntimeException(e.getMessage());
            return new String[]{
                    "N", e.getMessage()
            };
        }
    }

    /**
     * 订单查询接口(新版V3)
     *
     * @param transactionId 微信订单号
     * @param outTradeNo    商户系统内部的订单号,当没提供微信订单号(transactionId)时需要传
     * @return 订单成功(SUCCESS):{0:Y,1:商户单号,2:微信单号,3:订单金额(分),4:交易时间,5:交易状态,6:交易描述}
     * 订单异常:{0:N,1:订单状态,2:订单描述}
     * 查询错误:{0:E,1:错误代码,2:错误描述}
     */
    public String[] query(String transactionId, String outTradeNo) {
        // 商家单号和微信单号不能同时为空
        if (null == transactionId && null == outTradeNo) {
            return new String[]{
                    "E",
                    "ERROR",
                    "微信单号和商户单号不能同时为空,请检查!"
            };
        }
        try {
            // 执行查询并返回查询结果
            WxPayOrderQueryV3Result wxPayOrderQueryV3Result = this.getWxPayService().queryOrderV3(transactionId, outTradeNo);
            // 如果交易成功,或者在退款中
            if ("SUCCESS".equals(wxPayOrderQueryV3Result.getTradeState()) || "REFUND".equals(wxPayOrderQueryV3Result.getTradeState())) {
                return new String[]{
                        "Y",
                        wxPayOrderQueryV3Result.getOutTradeNo(),
                        wxPayOrderQueryV3Result.getTransactionId(),
                        String.valueOf(wxPayOrderQueryV3Result.getAmount().getTotal()),
                        wxPayOrderQueryV3Result.getSuccessTime(),
                        wxPayOrderQueryV3Result.getTradeState(),
                        wxPayOrderQueryV3Result.getTradeStateDesc()
                };
            } else {
                return new String[]{
                        "N",
                        wxPayOrderQueryV3Result.getTradeState(),
                        wxPayOrderQueryV3Result.getTradeStateDesc()
                };
            }
        } catch (WxPayException e) {
            // throw new RuntimeException(e.getMessage());
            return new String[]{
                    "E",
                    e.getErrCode(),
                    e.getErrCodeDes()
            };
        }
    }


    /**
     * 退款接口(新版V3)
     *
     * @param outTradeNo  商户订单号
     * @param outRefundNo 商户退款单号
     * @param total       订单总金额(单位:分)
     * @param refund      退款金额(单位:分)
     * @return 退款成功或退款处理中:{0:Y,1:商户单号,2:微信单号,3:退款单号,4:订单金额(分),5:退款金额(分),6:退款时间}<br>
     * 订单异常:{0:N,1:订单状态,2:订单描述}
     * 退款错误:{0:E,1:错误代码,2:错误描述}
     */
    public String[] refund(String outTradeNo, String outRefundNo, Integer total, Integer refund) {
        // 几个参数不能为空
        if (null == outTradeNo || null == outRefundNo || null == total || null == refund) {
            return new String[]{
                    "E",
                    "ERROR",
                    "商户单号、退款单号、订单金额、退款金额均不能为空,请检查!"
            };
        }
        // 构造请求参数
        WxPayRefundV3Request wxPayRefundV3Request = new WxPayRefundV3Request();
        wxPayRefundV3Request
                .setOutTradeNo(outTradeNo)
                .setOutRefundNo(outRefundNo)
                .setAmount(new WxPayRefundV3Request.Amount()
                        .setTotal(total)
                        .setRefund(refund)
                        .setCurrency("CNY")
                );
        try {
            // 执行请求并返回信息
            WxPayRefundV3Result wxPayRefundV3Result = this.getWxPayService().refundV3(wxPayRefundV3Request);
            // 退款处理中 || 退款成功
            if ("PROCESSING".equals(wxPayRefundV3Result.getStatus()) || "SUCCESS".equals(wxPayRefundV3Result.getStatus())) {
                return new String[]{
                        "Y",
                        wxPayRefundV3Result.getOutTradeNo(),
                        wxPayRefundV3Result.getTransactionId(),
                        wxPayRefundV3Result.getOutRefundNo(),
                        String.valueOf(wxPayRefundV3Result.getAmount().getTotal()),
                        String.valueOf(wxPayRefundV3Result.getAmount().getRefund()),
                        wxPayRefundV3Result.getCreateTime()
                };
            } else {
                return new String[]{
                        "N",
                        wxPayRefundV3Result.getStatus(),
                        "退款失败"
                };
            }

        } catch (WxPayException e) {
            // throw new RuntimeException(e.getMessage());
            return new String[]{
                    "E",
                    e.getErrCode(),
                    e.getErrCodeDes()
            };
        }
    }
}

3.3 服务类的补充说明

一般来说,我们将配置信息放在yaml文件中。这种操作是没有问题的。

但本文中的支付服务类的实现方案,可以实现将微信支付的配置信息,经过加密后,存储在数据库中。

当需要发起支付的时候,从数据库中读取信息后,经过解密,再写入到微信的支付配置类中。

对于本演示项目,其配置信息就直接写在代码里了,如下图所示。
image-20220115151928651

另外:

为了方便,本支付服务类的返回结果,直接通过数组形式返回。实际项目中,建议通过bean实体的形式返回。

或者直接在控制类中调用getWxPayService()方法,在控制类中实现支付、查询、退款等功能。

整个服务类放在控制类中也没有问题。

4. 控制类调用与前端展示

4.1 实现控制类

com.cxhit.pay.wechat.controller包中,新建WeChatPayController控制类,并实现如下代码。

代码有详细注释,不过多解释。

package com.cxhit.pay.wechat.controller;

import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.IdUtil;
import com.cxhit.pay.wechat.service.WeChatPayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 微信支付控制类
 *
 * @author 拾年之璐
 * @since 2022/1/13 18:04
 */
@Controller
@RequestMapping("")
public class WeChatPayController {

    @Autowired
    private WeChatPayService weChatPayService;

    /**
     * 首页
     *
     * @return 首页
     */
    @GetMapping("")
    public String index() {
        return "index";
    }

    /**
     * 微信支付接口
     *
     * @param title 商品名称
     * @param price 商品价格
     * @return 返回结果
     */
    @PostMapping(value = "/pay")
    @ResponseBody
    public Dict pay(String title, String price) {
        // 生成商家单号
        String outTradeNo = IdUtil.simpleUUID();
        // 支付宝价格转换成浮点数后,乘100,再取整,得以分为单位的价格
        Integer total = (int) (Float.parseFloat(price) * 100);
        // 发起支付请求
        String[] result = weChatPayService.pay("native", title, outTradeNo, total, null);
        // 返回结果:下单成功
        if ("Y".equals(result[0])) {
            return Dict.create().set("code", 200).set("qrcode", result[1]).set("outTradeNo", outTradeNo);
        }
        // 下单失败
        else {
            return Dict.create().set("code", 500).set("msg", result[1]);
        }
    }

    /**
     * 退款接口
     *
     * @param outTradeNo 商家单号
     * @param amount     退款金额(不能大于总金额)
     * @return 退款结果
     */
    @PostMapping(value = "/refund")
    @ResponseBody
    public Dict refund(String outTradeNo, String amount) {
        // 生成商家退款单号
        String outRefundNo = IdUtil.simpleUUID();
        // 查询订单金额
        String[] query = weChatPayService.query(null, outTradeNo);
        // 查询成功
        if (query[0].equals("Y")) {
            int total = Integer.parseInt(query[3]);
            // 支付宝价格转换成浮点数后,乘100,再取整,得以分为单位的价格
            int refund = (int) (Float.parseFloat(amount) * 100);
            if (refund > total) {
                return Dict.create().set("code", 500).set("msg", "退款错误:退款金额不能大于支付金额!");
            }
            // 发起退款
            String[] result = weChatPayService.refund(outTradeNo, outRefundNo, total, refund);
            // 退款成功
            if (result[0].equals("Y")) {
                return Dict.create().set("code", 200).set("msg", "退款进行中,稍后到账!" +
                        " <br>商户单号:" + result[1] +
                        " <br>退款单号:" + result[3] +
                        " <br>订单金额:" + result[4] + "分" +
                        " <br>退款金额:" + result[5] + "分" +
                        " <br>退款时间:" + result[6]
                );
            }
            // 退款失败
            else if (result[0].equals("N")) {
                return Dict.create().set("code", 500).set("msg", "退款失败:" + result[1] + result[2]);
            }
            // 退款发生错误
            else {
                return Dict.create().set("code", 500).set("msg", "退款错误:" + result[1] + result[2]);
            }
        }
        // 查询失败
        else {
            return Dict.create().set("code", 500).set("msg", "退款错误:" + query[1] + query[2]);
        }
    }

    /**
     * 查询接口
     *
     * @param outTradeNo 商家订单号
     * @return 结果
     */
    @PostMapping(value = "/query")
    @ResponseBody
    public Dict query(String outTradeNo) {
        // 查询订单
        String[] query = weChatPayService.query(null, outTradeNo);
        // 查询成功
        if (query[0].equals("Y")) {
            return Dict.create().set("code", 200).set("msg", "查询成功!" +
                    " <br>商户单号:" + query[1] +
                    " <br>微信单号:" + query[2] +
                    " <br>订单金额:" + query[3] + "分" +
                    " <br>交易时间:" + query[4] +
                    " <br>交易状态:" + query[5] +
                    " <br>交易描述:" + query[6]
            );
        }
        // 查询失败
        else if (query[0].equals("N")) {
            return Dict.create().set("code", 500).set("msg", "查询结果:" + query[1] + query[2]);
        }
        // 查询发送异常
        else {
            return Dict.create().set("code", 500).set("msg", "查询失败:" + query[1] + query[2]);
        }
    }
}

4.2 实现前端展示

前端代码较长,请看项目源码。

最终实现的前端展示页面如下图所示。

image-20220115152513177

5. 本文源码下载

本文源码下载

至此,Spring Boot集成微信支付项目结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾年之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值