直连商户模式和服务商模式区别:
直连商户:例如张三开了一个小程序,然后别人在这个小程序买东西,结账的时候,钱是直接打到张三的账号上的。
服务商模式:例如张三开了一个小程序,然后这个小程序中有一个开分店的功能,然后别人在分店购买东西,在结账的时候,钱是直接打到分店的负责人的账号上的。
本文章说的是服务商模式(直连模式看这篇文章:微信直连商户V3支付(可直接使用)_流连勿忘返的博客-CSDN博客)
微信支付逻辑(重点):
前端点击支付按钮,在调起微信自带支付页面之前,要往后端发一个请求,后端先是负责调用微信的 "统一下单" 接口,在调用这个接口的时候,会把本地订单号也一起发过去,然后会得到一个 prepay_id ,然后再针对 prepay_id 和一些参数做一个算法,得到相对应的签名值,然后返回给前端,然后前端就可以根据这些返回值调用支付,就可以支付了如果支付成功,那就ok了,因为有把本地的订单号一起传过去给微信那边,所以就相当于这个本地的订单号跟微信那边的订单绑定了,所以只要支付了腾讯那边的订单,那就相当于完成了本地订单。
1.申请证书,设置V3秘钥(这一步是服务商账号操作的)
2.设置APPid账号管理(这一步是服务商操作的)
服务商账号关联要支付的小程序
3.新增子商户(这个子商户实际上就是分店的主负责人)
可以手动添加,也可以使用接口来添加
4.配置子商户
点击这个,然后跳转到这里:
继续点击:配置
把要支付的小程序或者公众号id配置进去
5.maven地址
<dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-apache-httpclient</artifactId><version>0.4.7</version></dependency>
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 = ""; // ApIdString 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";}
其中的 商户证书序列号对应的证书秘钥 在下载好的证书的这个地方:
其中红色框起来的就是商户证书序列号对应的证书秘钥:
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 liaozan8888@163.com*/ 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));// 从证书管理器中获取verifierverifier = 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);// openidtry {objectMapper.writeValue(bos, rootNode);httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));//获取预支付IDCloseableHttpResponse 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();// Appidbuilder.append(ServiceProvider.sp_appid).append("\n");// 时间戳builder.append(timestamp).append("\n");// 随机字符串builder.append(nonce).append("\n");JsonNode jsonNode = objectMapper.readTree(bodyAsString);// 预支付会话IDbuilder.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 liaozan8888@163.com*/ 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<String, String> stringStringHashMap = new HashMap<>();stringStringHashMap.put("code","200");stringStringHashMap.put("message","返回成功");// 返回这个说明应答成功return stringStringHashMap;}}
10.官方文档:
产品能力概览 | 微信支付服务商平台文档中心