由于业务设计到了支付,本来想去网上找个写好的用,但是都太麻烦了,有的封装的太重太复杂。导致学习成本太高。我太懒了,本来只需要看微信的就行了,用别人的还要去学他们的使用方法,也不懂他们内部代码逻辑。干脆自己按照官方的方式来吧。

不BB,上干货。

  1. 直接百度搜索:微信官方文档(点击直达)
    在这里插入图片描述
  2. 官方文档里面列出了所有微信生态的文档,我们只需要关注微信支付模块即可
    在这里插入图片描述
  3. 有关微信支付的相关知识本文不再赘述,本文只负责最简单的技术实现
  4. 本文是商户角色模式,关于合作商角色模式和这个差不多,区别在于多了子商户概念及相关参数。商户模式搞懂了,那合作商模式就无压力,无非就是换个包下的对象调接口,再加点参数。

一、开发准备

“官方开发准备”说明文档

按照官方文档收集好以下必要参数:

1 在 小程序|公众号 内:

  • 申请AppID
  • 生成AppSecret

2 在商户平台

  • 申请mchid
  • 配置API key
  • 下载并配置商户证书

3 小程序|公众号 和商户进行AppID和mchid的绑定

至此,你有了以下这些必须参数及私钥文件:

  • appid(公众号|小程序)
  • AppSecret(密钥)
  • merchantId(商户号)
  • merchantSerialNumber(商户证书序列号)
  • apiV3key(apiV3密钥)
  • apiclient_key.pem(商户API私钥文件)

(ps:这几个参数和私钥文件还不知道在哪里配置在哪里找,就看看官方说明,或者网上搜搜,相关资料太多,这里就不再赘述了)

二、在项目内编写配置文件和工具类

  1. pom.xml加入依赖
        <!-- hutool 工具包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.32</version>
        </dependency>

        <!-- 微信支付 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.12</version>
        </dependency>

依托于hutool工具类,不想用这个的也可以用其他的,或者自己编写。
微信支付sdk,官方推荐使用这个,我就用这个了。当前时间是2024年10月,最新版本是0.2.14。为啥我没用最新的,因为我懒,懒得把0.2.12改成0.2.14。后续可能官方还会推出更高的新版本。你想用哪个您老随意哈。

  1. 编写配置文件:properties.properties
    放到resources目录下
    在这里插入图片描述
    内容如下,参数更换成你自己的:
###     微信公众号 配置     ###
# 公众号【 XXXXXX( 公众号名字 )  】
wechat.office.account.appid=xxx
wechat.office.account.secret=xxxxxx

###     微信小程序 配置     ###
# 小程序【 XXXXXXX(小程序名字) 】
wechat.mini.program.appid = xxx
wechat.mini.program.secret = xxxxxxx

###     微信支付 配置     ###
#appid(公众号|小程序)
wechat.pay.appid = xxx
#商户号
wechat.pay.merchantId = xxxx
#商户证书序列号
wechat.pay.merchantSerialNumber = xxxxxxxxxxxxxxxxxxxx
#商户APIV3密钥
wechat.pay.apiV3key = xxxxxxxxxxxxxxxxxx
#异步通知url(注意拦截器是否拦截)
wechat.pay.notify.url = https://xxxxxxxx
  1. 编写配置文件读取工具:PropertiesUtil.java
import cn.hutool.setting.dialect.Props;
import cn.hutool.setting.dialect.PropsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 读取 resources/properties.properties 全局配置文件
 */
public class PropertiesUtil {

    private static final Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);

    //properties.properties 文件 
    private static final String configFileName = "properties";

    private PropertiesUtil() { }

    //读取properties.properties文件
    private static final Props props = PropsUtil.get(configFileName);

    public static String getString(String key) {
        return props.getStr(key, "");
    }

    public static Integer getInteger(String key) {
        return props.getInt(key, 0);
    }

    public static Boolean getBool(String key) {
        return props.getBool(key, false);
    }

    public static Object get(Object key) {
        return props.get(key);
    }

}
  1. 在resources目录下放入私钥文件apiclient_key.pem
    在这里插入图片描述

三、封装相关服务

  1. 创建微信支付服务并读取配置文件:WechatPayService.java
/**
 * 微信支付服务
 */
public class WechatPayService {

    // appid  使用自己刚刚写的PropertiesUtil工具读取,下同
    private static final String appid = PropertiesUtil.getString("wechat.pay.appid");
    // 商户号
    private static final String merchantId = PropertiesUtil.getString("wechat.pay.merchantId");
    // 商户证书序列号
    private static final String merchantSerialNumber = PropertiesUtil.getString("wechat.pay.merchantSerialNumber");
    // 商户APIV3密钥
    private static final String apiV3key = PropertiesUtil.getString("wechat.pay.apiV3key");
    // 回调地址
    private static final String notifyUrl = PropertiesUtil.getString("wechat.pay.notify.url");
    // 商户API私钥文件   hutool工具读取resources目录下的私钥文件apiclient_key.pem
    private static final String privateKey = ResourceUtil.readUtf8Str("apiclient_key.pem");
}
  1. 添加自动验签对象
    private static final RSAAutoCertificateConfig config =
                new RSAAutoCertificateConfig.Builder()
                        .merchantId(merchantId)
                        .privateKey(privateKey)
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3key)
                        .build();
  1. 封装 微信支付下单服务,用来获取微信的预支付订单信息 给前端使用
    /**
     * 微信支付下单 创建订单
     *
     * @param fen         订单总金额,单位为分。
     * @param description 商品描述
     * @param outTradeNo  商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,长度[6-32]
     * @param openId      用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid
     * @return 下单结果
     */
    public static PrepayWithRequestPaymentResponse createOrder(Integer fen, String description, String outTradeNo, String openId) {
        //这里使用JSAPI支付模块,其他模块大同小异,使用其他支付模块,就用其他支付模块包内的对象
        //使用Jsapi服务扩展对象:com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension 
        JsapiServiceExtension serviceExtension = new JsapiServiceExtension.Builder().config(config).build();
        //构建对象com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest,并设置必填参数
        //其他参数请移步[官方接口文档:https://pay.weixin.qq.com/doc/v3/merchant/4012525057]
        PrepayRequest request = new PrepayRequest();
        Amount amount = new Amount();
        amount.setTotal(fen);
        request.setAmount(amount);
        request.setAppid(appid);
        request.setMchid(merchantId);
        request.setDescription(description);
        request.setNotifyUrl(notifyUrl);
        request.setOutTradeNo(outTradeNo);
        Payer payer = new Payer();
        payer.setOpenid(openId);
        request.setPayer(payer);
        //发送请求
        return serviceExtension.prepayWithRequestPayment(request);
    }
  1. 封装 微信回调服务

    /**
     * 下单支付回调
     * @param request HttpServletRequest 
     * @return 通知结果
     */
    public static Transaction payCallback(HttpServletRequest request) {
        String signature = request.getHeader("Wechatpay-Signature");
        String nonce = request.getHeader("Wechatpay-Nonce");
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        String serial = request.getHeader("Wechatpay-Serial");
        String signatureType = request.getHeader("Wechatpay-Signature-Type");
        String requestBody = getRequestBody(request);//1.采用输入流方式接收请求体

        // 构建com.wechat.pay.java.core.notification.RequestParam
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(serial)
                .nonce(nonce)
                .signType(signatureType)
                .signature(signature)
                .timestamp(timestamp)
                .body(requestBody)
                .build();
        //构建通知解析器com.wechat.pay.java.core.notification.NotificationParser
        NotificationParser parser =
        	 new NotificationParser(config);
        //验签、解密并转换成 com.wechat.pay.java.service.payments.model.Transaction
        return parser.parse(requestParam, Transaction.class);//获取通知结果
    }

    /** 获取请求体 */
    private static String getRequestBody(HttpServletRequest request) {
        StringBuilder requestBody = new StringBuilder();
        try {
        	//hutool工具读取输入流内容
            requestBody.append(IoUtil.readUtf8(request.getInputStream()));
        } catch (IOException e) {
            logger.error("读取请求体发生了异常.........");
            e.printStackTrace();
        }
        return requestBody.toString();
    }

  1. 封装 订单查询服务
    /**
     * 商户订单号查询订单
     * @param outTradeNo 商户订单号
     * @return com.wechat.pay.java.service.payments.model.Transaction
     */
    public static Transaction queryOrderByOutTradeNo(String outTradeNo) {
        JsapiServiceExtension serviceExtension = new JsapiServiceExtension.Builder().config(config).build();

        QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
        request.setMchid(merchantId);
        request.setOutTradeNo(outTradeNo);

        return serviceExtension.queryOrderByOutTradeNo(request);
    }
  1. 封装 退款
    /**
     * 微信退款申请
     *
     * @param outTradeNo  原支付交易对应的商户订单号:商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,长度[6-32]
     * @param outRefundNo 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
     * @param money       退款金额,单位为分,只能为整数,不能超过原订单支付金额。
     * @param total       原支付交易的订单总金额,单位为分,只能为整数。
     * @return com.wechat.pay.java.service.refund.model.Refund
     */
    public static Refund refund(String outTradeNo, String outRefundNo, Long money, Long total) {

        RefundService service = new RefundService.Builder().config(config).build();
        CreateRequest request = new CreateRequest();
        // request.setXxx(val)设置所需参数,具体参数可见Request定义
        request.setOutTradeNo(outTradeNo);
        request.setOutRefundNo(outRefundNo);

        AmountReq amount = new AmountReq();
        amount.setTotal(total);
        amount.setRefund(money);
        amount.setCurrency("CNY");
        request.setAmount(amount);

        return service.create(request);
    }

至此,完毕
针对其他接口自己按照这个进行封装吧
在这里插入图片描述

四、创建接口 使用相关服务

业务说明:
小程序用户要想支付成功,支付接口需要小程序用户的openid,此参数需要授权登录才可拿到。拿到后就可以调用支付下单接口获取预付订单信息返回给前端,前端用户用预付订单信息调起支付输入密码键盘,用户正确输入密码支付成功后,微信支付会请求你的服务器设定的回调地址接口。此接口会带有预付订单相关信息,根据这些信息再更新自己系统内的数据。至此,整个微信支付流程走完。

  1. 封装微信小程序服务:WechatMiniService.java 用户授权获取open_id
    官方文档
/**
 * <h1>微信小程序服务</h1>
 */
public class WechatMiniService {
    private final static Logger logger = LoggerFactory.getLogger(WechatMiniService.class);

    //小程序 appid
    private static final String appId = PropertiesUtil.getString("wechat.mini.program.appid");
    //小程序 secret
    private static final String secret = PropertiesUtil.getString("wechat.mini.program.secret");


    /**
     * <h1> 小程序授权登录 </h1>
     * <h2>功能描述</h2>
     * 登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。
     *
     * @param code 授权码
     */
    public static MiniCode2Session jscode2session(String code) {
        return jscode2session(appId, secret, code);
    }

    /**
     * <h1> 小程序授权登录 </h1>
     * <h2>功能描述</h2>
     * 登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。
     *
     * @param appId     appId
     * @param appSecret appSecret
     * @param code      授权码
     */
    public static MiniCode2Session jscode2session(String appId, String appSecret, String code) {
        String url = "https://api.weixin.qq.com/sns/jscode2session";

        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("appid", appId);
        paramMap.put("secret", appSecret);
        paramMap.put("js_code", code);
        paramMap.put("grant_type", "authorization_code");

        try {
            //hutool的http请求工具
            String result = HttpUtil.get(url, paramMap, 10000);
            //hutool的JOSN工具
            JSONObject json = JSONUtil.parseObj(result);
            return json.toBean(MiniCode2Session.class);
        } catch (Exception e) {
            String failMsg = "小程序授权登录使用授权码code获取token时,发送get请求异常";
            logger.error(failMsg);
            logger.error(e.getMessage());
            return new MiniCode2Session("1000010001", failMsg);
        }
    }

	//其他相关接口封装省略:解析手机号、生成小程序码、等等的其他功能
}

接口返回对象封装


import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 微信小程序授权登录结果对象
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MiniCode2Session {

    private String session_key;
    private String unionid;
    private String openid;

    private String errcode;
    private String errmsg;


    public MiniCode2Session(String errcode, String errmsg) {
        this.errcode = errcode;
        this.errmsg = errmsg;
    }


    /**
     * <h1> 如果存在 access_token , 则表示请求成功 </h1>
     *
     * @return boolean
     */
    public boolean isSuccess() {
        return StrUtil.isNotBlank(this.session_key);
    }

    /**
     * <h1> 获取错误消息 </h1>
     * errcode + " : " + errmsg;
     */
    public String getErrorMessage() {
        return this.errcode + " : " + this.errmsg;
    }

}

按照此模式你还可以封装一个公众号相关服务。微信官方文档里面有那么多模块,每一个你都可以这样封装。

  1. Controller添加小程序授权登录接口
    @PostMapping("/login")
    public R<?> login(@RequestBody JSONObject json) {
		//此处伪代码,自己完善保存用户信息,以及token生成相关逻辑,接口权限拦截问题
		//这些不在支付范围内
		
		//使用前端传来的code换取openid
        MiniCode2Session code2Session = WechatMiniService.jscode2session(json.getStr("code"));
        if (code2Session.isSuccess()) {
            return R.ok(code2Session);
        }
        return R.fail(code2Session.getErrorMessage());
    }
  1. 前端拿到open_id,调用下单接口

    @PostMapping("/create/order")
    @Transactional
    public R<?> createOrder(@RequestBody Orders order) {
        //单价*数量=总价(单位元)
        BigDecimal totalAmount = NumberUtil.mul(order.getProductPrice(), order.getProductCount());
        //总价元 * 100 = 总价分
        Integer fen = NumberUtil.mul(totalAmount,new BigDecimal("100")).intValue();//金额单位转成分
        //hutool工具雪花算法生成同一商户下不重复的支付单号
        String outTradeNo = IdUtil.getSnowflakeNextIdStr();
        //生成支付记录
        PrepayWithRequestPaymentResponse payment = WechatPayPartnerService.createOrder(tenant.getSubAppid(), tenant.getSubMchid(), fen, order.getProductName(), outTradeNo, user.getCodes());

        //省略业务逻辑

        return R.ok(payment);
    }
  1. 前端拿到下单接口返回的信息,调起输入密码键盘
    前端小程序支付成功后,微信支付会进行回调请求,请求地址就是在下单接口中配置的回调地址参数

  2. Controller添加 不被拦截的回调接口

    //支付通知
    @PostMapping("/callback")
    public JSONObject callback(HttpServletRequest request, HttpServletResponse response) {
        JSONObject result = JSONUtil.createObj();
        result.set("code", "SUCCESS");
        try {
            logger.info("收到支付回调");
            Transaction transaction = WechatPayService.payCallback(request);
            String outTradeNo = transaction.getOutTradeNo();//Transaction内还有很多参数,直接get某个参数获取
            logger.info("商户内部支付单号:{}", outTradeNo);

            //省略业务相关逻辑

        } catch (Exception e) {
            result.set("code", "FAIL");
        }
        return result;
    }

至此,整个支付流程完毕 撒花鼓掌吧

五、对接过程问题分析及排错

  1. 公众号|小程序 必须认证,开通微信支付
  2. 商户号必须和appid主体进行绑定
  3. 全程必须https
  4. 看清官方接口文档内请求头Content-Type类型:
    :application/x-www-form-urlencoded
    :application/json
    :multipart/form-data
  5. 看清官方接口文档内参数位置放在哪个位置:
    :Path
    :Header
    :Body
  6. 是否开启白名单拦截
  7. 是否配置相关域名
  8. 是否配置相关js接口权限

每一个问题请自行查询解决方案,网上方案太多了,这里不再赘述

六、服务商模式

服务商模式下注意的点就是
要使用服务商包下的例如:

//服务商下的:com.wechat.pay.java.service.partnerpayments.jsapi.JsapiServiceExtension
//一般商户下的:com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension
JsapiServiceExtension serviceExtension = new JsapiServiceExtension.Builder().config(config).build();

这就是序言内第4点说的:无非就是换个包下的对象调接口,再加点参数。
所有对象都换成用服务商包下的对象即可

七、吐槽一下

在看完官方的文档后,感觉网上的文章不能说有问题,但是太复杂了,动不动就签名验签的。太烦了。看我这流程代码里面,有个鸡毛的签名验签代码,每个逻辑就只有几行代码。都是按照官方推荐的来的。

当然了,我这代码也有不足之处。没有啥分层分模块分架构。优点就是全部静态方法,使用时只需一行代码搞定(^ _ ^)。简单的不要不要的。主要是理解官方整个支付流程机实现方法。

有不足之处望各位指出来!

八、关于微信支付公钥问题

2025年08月,近期在使用上面的这套逻辑搞微信支付,发现微信支付平台改版了,强制要求使用微信支付公钥模式;直接取消了平台证书模式;没办法,只能按照官方的来吧。。。。

其实也简单,就几个步骤,改动代码量也不多。
1、升级sdk到当前最新版0.2.17 (具体哪个版本开始支持平台公钥模式没有细查,反正当前最新版本肯定支持,直接用当前的最新版本0.2.17即可)
2、properties配置文件增加一个配置项:

#微信支付公钥
wechat.pay.publicKeyId = PUB_KEY_ID_xxxxx

3、添加平台公钥文件到项目资源目录下:pub_key.pem
在这里插入图片描述

4、更换自动验证对象
由:

    private static final RSAAutoCertificateConfig config =
                new RSAAutoCertificateConfig.Builder()
                        .merchantId(merchantId)
                        .privateKey(privateKey)
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3key)
                        .build();

更换为:

    // publicKeyId
    private static final String publicKeyId = PropertiesUtil.getString("wechat.pay.publicKeyId");
    // 微信支付平台公钥文件
    private static final String pubKey = ResourceUtil.readUtf8Str("pub_key.pem");

    //自动验签对象
    private static final RSAPublicKeyConfig config =
            new RSAPublicKeyConfig.Builder()
                    .merchantId(merchantId)
                    .publicKeyId(publicKeyId)
                    .publicKey(pubKey)
                    .privateKey(privateKey)
                    .merchantSerialNumber(merchantSerialNumber)
                    .apiV3Key(apiV3key)
                    .build();

至此可以正常处理了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐