微信支付的服务商模式V3支付(可直接使用)

 直连商户模式和服务商模式区别:

        直连商户:例如张三开了一个小程序,然后别人在这个小程序买东西,结账的时候,钱是直接打到张三的账号上的。

        服务商模式:例如张三开了一个小程序,然后这个小程序中有一个开分店的功能,然后别人在分店购买东西,在结账的时候,钱是直接打到分店的负责人的账号上的。
        本文章说的是服务商模式(直连模式看这篇文章:微信直连商户V3支付(可直接使用)_流连勿忘返的博客-CSDN博客

微信支付逻辑(重点):

微信支付的服务商模式V3支付(可直接使用)_第1张图片


        前端点击支付按钮,在调起微信自带支付页面之前,要往后端发一个请求,后端先是负责调用微信的 "统一下单" 接口,在调用这个接口的时候,会把本地订单号也一起发过去,然后会得到一个 prepay_id ,然后再针对 prepay_id 和一些参数做一个算法,得到相对应的签名值,然后返回给前端,然后前端就可以根据这些返回值调用支付,就可以支付了

        如果支付成功,那就ok了,因为有把本地的订单号一起传过去给微信那边,所以就相当于这个本地的订单号跟微信那边的订单绑定了,所以只要支付了腾讯那边的订单,那就相当于完成了本地订单。



1.申请证书,设置V3秘钥(这一步是服务商账号操作的)

微信支付的服务商模式V3支付(可直接使用)_第2张图片

 2.设置APPid账号管理(这一步是服务商操作的)

微信支付的服务商模式V3支付(可直接使用)_第3张图片

服务商账号关联要支付的小程序

 3.新增子商户(这个子商户实际上就是分店的主负责人)

微信支付的服务商模式V3支付(可直接使用)_第4张图片

可以手动添加,也可以使用接口来添加

4.配置子商户

微信支付的服务商模式V3支付(可直接使用)_第5张图片

 点击这个,然后跳转到这里:

微信支付的服务商模式V3支付(可直接使用)_第6张图片

继续点击:配置 

微信支付的服务商模式V3支付(可直接使用)_第7张图片

 把要支付的小程序或者公众号id配置进去

5.maven地址

        
            com.github.wechatpay-apiv3
            wechatpay-apache-httpclient
            0.4.7
        

6.公共参数接口

package com.example.demo.zhifu;

/**
 * @Description:
 * @Author sk
 * @Date: 2023/7/5 14:31
 */

/**
 * 服务商模式
 */
public interface ServiceProvider {
    String NOTIFY_URL = ""; //回调地址

    String sp_mchid = ""; //服务商的商户号

    String sub_mchid = "xxx"; // 子商户号,在这里写固定的,用于测试

    String MCH_SERIAL_NO = ""; // 服务商的商户证书序列号
    String API_3KEY = "";   // 服务商的V3的密钥
    String sp_appid = "";     // ApId

    String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAgPXTRI0OFMEk" +
            "yf4+OSHs0K7wpDKfChB4xchJHJ39WwSS+A/fsyIEzC547D0NbUeiRby4ybAIfroa" +
            "zQCXRjRr0x6typGVY2ul9khWhSeC/CZHd0JrfcOCDHa3uJR01MElrGBIwgGSINrk" +
            "luW+jYveIVtc+uI1DSZrUOFxj8dg7//dvlhWluClwUbQiv9OG131Bi1j/fivUhI2" +
            "hiPy8zWADiCqTv5xzH3RBIbRJgNO/eIxUvfzGgyPECQ9C6XN4uxKxVWHOcg/vAD7" +
            "vQJHFO5sZ4/Z5pisHlUNr3aclTWVQg9n+ReOb8ztlmoqU4bvkh+3QveqsScDCqWl" +
            "A0CZ0Nr/AgMBAAECggEBALoMKQltaFIiluSiYBjtCK+ipGCooM/6Xx8KL98RTFQv" +
            "YkVUf6r4qrkuSP/PedX/NstLUPDa5EnhiKYcWSTa0hEfsrfOXlOeCc0VMKaF/EDo" +
            "x2oshcHzgz+uIhK/zqL3eFCbv1ayQehj3oosmJBIptQhMvay9mrFccsoGSqzBcPV" +
            "nwg04jlqZK";

}

其中的 商户证书序列号对应的证书秘钥 在下载好的证书的这个地方:


微信支付的服务商模式V3支付(可直接使用)_第8张图片

 

其中红色框起来的就是商户证书序列号对应的证书秘钥:


微信支付的服务商模式V3支付(可直接使用)_第9张图片

7.下单工具类

import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;

/**
 * 服务商的下单工具类
 * @author [email protected]
 */
public class PayMerchantUtil {
    private CloseableHttpClient httpClient;
    private CertificatesManager certificatesManager;
    private Verifier verifier;

    /**
     * App下单  具体下单场景查看官方文档
     *
     * @param total
     * @param description
     * @return
     * @throws Exception
     */
    public String requestwxChatPay(String orderSn, int total, String description,String openid) throws Exception {

        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(ServiceProvider.privateKey.getBytes("utf-8")));
        // 获取证书管理器实例
        certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(ServiceProvider.sp_mchid, new WechatPay2Credentials(ServiceProvider.sp_mchid,
                        new PrivateKeySigner(ServiceProvider.MCH_SERIAL_NO, merchantPrivateKey)),
                ServiceProvider.API_3KEY.getBytes(StandardCharsets.UTF_8));
        // 从证书管理器中获取verifier
        verifier = certificatesManager.getVerifier(ServiceProvider.sp_mchid);
        httpClient = WechatPayHttpClientBuilder.create()
                .withMerchant(ServiceProvider.sp_mchid, ServiceProvider.MCH_SERIAL_NO, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(certificatesManager.getVerifier(ServiceProvider.sp_mchid)))
                .build();

        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi");
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        //组合请求参数JSON格式
        ObjectNode rootNode = objectMapper.createObjectNode();
        rootNode.put("sp_appid", ServiceProvider.sp_appid)
                .put("sp_mchid", ServiceProvider.sp_mchid)
                .put("sub_mchid", ServiceProvider.sub_mchid)
                // 回调地址
                .put("notify_url", ServiceProvider.NOTIFY_URL + "returnNotify")
                .put("description", description)
                .put("out_trade_no", orderSn);
        rootNode.putObject("amount")
                // total:金额,以分为单位,假如是10块钱,那就要写 1000
                .put("total", total)
                .put("currency", "CNY");
        rootNode.putObject("payer")
                .put("sp_openid", openid);// openid
        try {
            objectMapper.writeValue(bos, rootNode);
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            //获取预支付ID
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String bodyAsString = EntityUtils.toString(response.getEntity());
            //微信成功响应
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                //时间戳
                String timestamp = System.currentTimeMillis() / 1000 + "";
                //随机字符串
                String nonce = RandomUtil.randomString(32);
                StringBuilder builder = new StringBuilder();

                // Appid
                builder.append(ServiceProvider.sp_appid).append("\n");
                // 时间戳
                builder.append(timestamp).append("\n");
                // 随机字符串
                builder.append(nonce).append("\n");
                JsonNode jsonNode = objectMapper.readTree(bodyAsString);
                // 预支付会话ID
                builder.append("prepay_id=").append(jsonNode.get("prepay_id").textValue()).append("\n");
                //获取签名
                String sign = this.sign(builder.toString().getBytes("utf-8"), merchantPrivateKey);

                JSONObject jsonMap = new JSONObject();
                jsonMap.put("noncestr", nonce);
                jsonMap.put("timestamp", timestamp);
                jsonMap.put("prepayid", jsonNode.get("prepay_id").textValue());
                jsonMap.put("sign", sign);
                jsonMap.put("appid", ServiceProvider.sp_appid);
                jsonMap.put("partnerid", ServiceProvider.sp_mchid);

                return jsonMap.toJSONString();//响应签名数据,前端拿着响应数据调起微信SDK
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 计算签名
     *
     * @param message
     * @param yourPrivateKey
     * @return
     */
    private String sign(byte[] message, PrivateKey yourPrivateKey) {
        try {
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(yourPrivateKey);
            sign.update(message);
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
            e.printStackTrace();
        }
        return "";
    }

8.回调签名工具类

package com.example.demo.zhifu;

/**
 * @Description:
 * @Author sk
 * @Date: 2023/7/5 14:31
 */

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * 回调签名配置
 * @author [email protected]
 */
public class AesUtil {
    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;

    public AesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }
    public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

9.下单的controller

package com.example.demo.zhifu;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description:
 * @Author sk
 * @Date: 2023/7/5 19:10
 */
@RestController
    @RequestMapping(value = "/pay")
public class payController {


    /**
     * 预支付下单
     * @param orderSn 订单号
     * @param total 分
     * @param description 描述
     * @return
     */
    @GetMapping(value = "/getPay")
    public String getPay(String orderSn,int total , String description)
    {
        PayMerchantUtil payMerchantUtil = new PayMerchantUtil();
        try {
            return payMerchantUtil.requestwxChatPay(orderSn, total, description, "oYgFI91D00GpCwccdnKDR4KNxI4k");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    // 支付回调
    @PostMapping(value = "/returnNotify")
    public Map returnNotify(@RequestBody JSONObject jsonObject)
    {
        // v3 私钥
        String key = "xxxxx";
        String json = jsonObject.toString();
        String associated_data = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.associated_data");
        String ciphertext = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.ciphertext");
        String nonce = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.nonce");
        try {
            String decryptData = new AesUtil(key.getBytes(StandardCharsets.UTF_8)).decryptToString(associated_data.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
            System.out.println("decryptData = " + decryptData);
            //TODO 业务校验

        } catch (Exception e) {
            e.printStackTrace();
        }

        HashMap stringStringHashMap = new HashMap<>();
        stringStringHashMap.put("code","200");
        stringStringHashMap.put("message","返回成功");
        // 返回这个说明应答成功
        return stringStringHashMap;
    }

}

10.官方文档:

产品能力概览 | 微信支付服务商平台文档中心

11.注意事项 

        1.有位兄弟看了博客之后,在使用的过程中,出现了签名不正确的问题,原因如下:

        业务不同,当前的服务商模式的业务是:所有的商户用的都是同一个小程序,所以在预支付下单的接口中,并没有传递 sub_appid 这个参数。
        而那位兄弟的业务是:服务商与特约商户,两个主体不一致,服务商有自己的服务号appid ,小程序是另外一个主体公司,然后开发的小程序,所以要传递 sub_appid 参数

        2.经常出现{"code":"PARAM_ERROR","detail":{"location":"body","value":0},"message":"输入源“/body/reason”映射到值字段“退款原因”字符串规则校验失败,字符数 0,小于最小值 1"},是因为我退款原因没写上,写上之后就没有这个问题了

        3.支付回调跟退款的回调,必须是外网可以访问的,这推荐使用内网穿透


        4.前端代码:

uni.requestPayment({
					provider: 'wxpay',
					timeStamp: this.timeStamp,
					nonceStr: this.nonceStr,
					package: this.package,
					signType: 'RSA',
					paySign: this.paySign,
					success: function(res) {
						console.log('支付成功:' + JSON.stringify(res));
						uni.switchTab({
							url:'/pages/index/index'
						})
					},
					fail: function(err) {
						uni.showToast({
							title:"支付失败,请重新支付",
							icon:'none'
						})
						console.log('支付失败:' + JSON.stringify(err));
					}
				});

出现这个报错:
微信支付的服务商模式V3支付(可直接使用)_第10张图片

解决方案:
微信小程序demo 调用支付jsapi缺少参数 total_fee,支付签名验证失败 究极解决方案-CSDN博客

你可能感兴趣的:(微信)