当前位置:   article > 正文

SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款)_微信支付apiv3

微信支付apiv3
最近的一个微信小程序项目里有用到微信支付,网上找的资料都是特别乱,看起来特别懵,结合了好多文章的内容,终于做了出来,可能我的这个博文看起来也是特别乱,但是是可以直接C走简单改一改就可以用的。(支付成功回调,和退款回调因为昨天刚在阿里申请的域名还不让备案,目前回调还不确定有什么问题,但是支付和退款经过反复确认是没有问题的了)等域名备案成功后,回调如果有什么问题在更新改一下。

这是整体的一个微信支付+退款的项目结构。
如果大家需要的话,我可以等确认回调没有问题以后可以把我这个项目中完整的jsapi支付及退款做一个demo。或者按照图片的顺序把代码贴进来。

 

1.微信支付-准备工作 

1.获取商户号
微信商户平台 申请成为商户 => 提交资料 => 签署协议 => 获取商户号
2.获取AppID
微信公众平台 注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3.申请商户证书
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
4.获取微信的证书
获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥

2.开干!!!!

1.引入pom.xml

  1. <!-- 微信支付 -->
  2. <dependency>
  3. <groupId>com.github.wechatpay-apiv3</groupId>
  4. <artifactId>wechatpay-apache-httpclient</artifactId>
  5. <version>0.4.2</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>com.google.code.gson</groupId>
  9. <artifactId>gson</artifactId>
  10. <version>2.8.9</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.apache.commons</groupId>
  14. <artifactId>commons-lang3</artifactId>
  15. <version>3.12.0</version>
  16. </dependency>
  17. <dependency>
  18. <groupId>com.alibaba</groupId>
  19. <artifactId>fastjson</artifactId>
  20. <version>1.2.78</version>
  21. </dependency>

2.配置application.yml

  1. # 微信相关配置
  2. vx:
  3. # appid
  4. appId: appid
  5. # 小程序密钥
  6. secret: 小程序密钥
  7. # 商户号
  8. mchId: 商户号
  9. # 证书序列号
  10. mchSerialNo: 证书序列号
  11. # api密钥
  12. apiKey: api密钥
  13. # 证书地址
  14. keyPath: D:/wxPayPem/apiclient_key.pem
  15. certPath: D:/wxPayPem/apiclient_cert.pem
  16. certP12Path: D:/wxPayPem/apiclient_cert.p12

说明:这个证书地址是在微信商户平台内生成下载的API证书文件
具体教程:什么是商户API证书?如何获取商户API证书?

3.都配置完了之后创建对应的配置实体。

  1. import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
  2. import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
  3. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
  4. import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
  5. import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
  6. import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
  7. import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
  8. import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
  9. import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
  10. import lombok.Data;
  11. import org.apache.http.impl.client.CloseableHttpClient;
  12. import org.springframework.beans.factory.annotation.Value;
  13. import org.springframework.context.annotation.Bean;
  14. import org.springframework.context.annotation.Configuration;
  15. import java.io.FileInputStream;
  16. import java.io.FileNotFoundException;
  17. import java.io.IOException;
  18. import java.nio.charset.StandardCharsets;
  19. import java.security.GeneralSecurityException;
  20. import java.security.PrivateKey;
  21. /**
  22. * <p>
  23. * 配置信息实体
  24. * </p>
  25. *
  26. * @author Lch
  27. * @dateTime 2024/2/26 15:31
  28. */
  29. @Configuration
  30. @Data
  31. public class WxPayConfig {
  32. /**
  33. * appid
  34. */
  35. @Value("${vx.appId}")
  36. private String appId;
  37. /**
  38. * 小程序密钥
  39. */
  40. @Value("${vx.secret}")
  41. private String secret;
  42. /**
  43. * 商户号
  44. */
  45. @Value("${vx.mchId}")
  46. private String mchId;
  47. /**
  48. * 证书序列号
  49. */
  50. @Value("${vx.mchSerialNo}")
  51. private String mchSerialNo;
  52. /**
  53. * api密钥
  54. */
  55. @Value("${vx.apiKey}")
  56. private String apiKey;
  57. /**
  58. * 证书地址
  59. */
  60. @Value("${vx.keyPath}")
  61. private String keyPath;
  62. /**
  63. * 获取商户的私钥文件
  64. *
  65. * @param filename 证书地址
  66. * @return 私钥文件
  67. */
  68. public PrivateKey getPrivateKey(String filename) {
  69. try {
  70. return PemUtil.loadPrivateKey(new FileInputStream(filename));
  71. } catch (FileNotFoundException e) {
  72. throw new RuntimeException("私钥文件不存在");
  73. }
  74. }
  75. /**
  76. * 获取签名验证器
  77. */
  78. @Bean
  79. public Verifier getVerifier() {
  80. // 获取商户私钥
  81. final PrivateKey privateKey = getPrivateKey(keyPath);
  82. // 私钥签名对象
  83. PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
  84. // 身份认证对象
  85. WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
  86. // 获取证书管理器实例
  87. CertificatesManager certificatesManager = CertificatesManager.getInstance();
  88. try {
  89. // 向证书管理器增加需要自动更新平台证书的商户信息
  90. certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiKey.getBytes(StandardCharsets.UTF_8));
  91. } catch (IOException | GeneralSecurityException | HttpCodeException e) {
  92. e.printStackTrace();
  93. }
  94. try {
  95. return certificatesManager.getVerifier(mchId);
  96. } catch (NotFoundException e) {
  97. e.printStackTrace();
  98. throw new RuntimeException("获取签名验证器失败");
  99. }
  100. }
  101. /**
  102. * 获取微信支付的远程请求对象
  103. * @return Http请求对象
  104. */
  105. @Bean
  106. public CloseableHttpClient getWxPayClient() {
  107. // 获取签名验证器
  108. Verifier verifier = getVerifier();
  109. // 获取商户私钥
  110. final PrivateKey privateKey = getPrivateKey(keyPath);
  111. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey)
  112. .withValidator(new WechatPay2Validator(verifier));
  113. return builder.build();
  114. }
  115. }

4.创建一个枚举类 type用到了什么就写什么,或者不创建枚举类都可以。

  1. import lombok.AllArgsConstructor;
  2. import lombok.Getter;
  3. /**
  4. * <p>
  5. * 请求地址枚举类
  6. * </p>
  7. *
  8. * @author Lch
  9. * @dateTime 2024/2/26 15:41
  10. */
  11. @AllArgsConstructor
  12. @Getter
  13. public enum WxApiConstants {
  14. /**
  15. * jsapi下单
  16. */
  17. JSAPI_PAY("/v3/pay/transactions/jsapi"),
  18. /**
  19. * 申请退款
  20. */
  21. DOMESTIC_REFUNDS("/v3/refund/domestic/refunds");
  22. /**
  23. * 类型
  24. */
  25. private final String type;
  26. public String getType() {
  27. return type;
  28. }
  29. }

5.微信支付求数据的对象

  1. import lombok.Data;
  2. import lombok.experimental.Accessors;
  3. import javax.validation.constraints.NotBlank;
  4. /**
  5. * <p>
  6. * 预支付参数
  7. * </p>
  8. *
  9. * @author Lch
  10. * @dateTime 2024/2/26 13:30
  11. */
  12. @Data
  13. @Accessors(chain = true)
  14. public class WxPayReqParam {
  15. /**
  16. * 总金额
  17. */
  18. @NotBlank(message = "总金额不能为空!")
  19. private String totalPrice;
  20. /**
  21. * 商品名称
  22. */
  23. @NotBlank(message = "商品名称不能为空!")
  24. private String goodsName;
  25. /**
  26. * openId
  27. */
  28. @NotBlank(message = "openId不能为空!")
  29. private String openId;
  30. /**
  31. * 订单号
  32. */
  33. @NotBlank(message = "商品订单号不能为空!")
  34. private String orderNumber;
  35. }

6.将请求参数封装成Map集合+创建微信支付订单的三种方式(Native,Jsapi,App),
我使用的是Jsapi,别的支付方式可以在刚才的枚举类中“WxApiConstants”,添加别的下单路径,或者就是写死了也可以,如果你也是Jsapi支付,应该就不需要改动什么。
开发指引-JSAPI支付 | 微信支付商户平台文档中心

  1. import com.cx.sasmc.vxpay.reqparam.WxPayReqParam;
  2. import com.cx.sasmc.vxpay.vxapienum.WxApiConstants;
  3. import com.google.gson.Gson;
  4. import org.apache.commons.lang3.StringUtils;
  5. import org.apache.http.client.methods.CloseableHttpResponse;
  6. import org.apache.http.client.methods.HttpPost;
  7. import org.apache.http.entity.StringEntity;
  8. import org.apache.http.impl.client.CloseableHttpClient;
  9. import org.apache.http.util.EntityUtils;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import java.io.IOException;
  13. import java.util.HashMap;
  14. import java.util.Map;
  15. /**
  16. * <p>
  17. * 将请求参数封装成Map集合
  18. * </p>
  19. *
  20. * @author Lch
  21. * @dateTime 2024/2/26 15:54
  22. */
  23. public class WxPayCommon {
  24. private final static Logger logger = LoggerFactory.getLogger(WxPayCommon.class);
  25. /**
  26. * 封装基础通用请求数据
  27. * @param wxPayConfig 微信的配置文件
  28. * @param basePayData 微信支付基础请求数据
  29. * @return 封装后的map对象
  30. */
  31. public static Map<String, Object> getBasePayParams(WxPayConfig wxPayConfig, WxPayReqParam basePayData) {
  32. Map<String, Object> paramsMap = new HashMap<>();
  33. paramsMap.put("appid", wxPayConfig.getAppId());
  34. paramsMap.put("mchid", wxPayConfig.getMchId());
  35. // 如果商品名称过长则截取
  36. String title = basePayData.getGoodsName().length() > 62 ? basePayData.getGoodsName().substring(0, 62) : basePayData.getGoodsName();
  37. paramsMap.put("description",title);
  38. paramsMap.put("out_trade_no", basePayData.getOrderNumber());
  39. paramsMap.put("notify_url", "https://你自己的回调域名.cn/vxPay/payNotify");
  40. Map<String, Integer> amountMap = new HashMap<>();
  41. amountMap.put("total", Integer.valueOf(basePayData.getTotalPrice()));
  42. paramsMap.put("amount", amountMap);
  43. return paramsMap;
  44. }
  45. /**
  46. * 获取请求对象(Post请求)
  47. * @param paramsMap 请求参数
  48. * @return Post请求对象
  49. */
  50. public static HttpPost getHttpPost(String type, Map<String, Object> paramsMap) {
  51. // 1.设置请求地址
  52. HttpPost httpPost = new HttpPost(type);
  53. // 2.设置请求数据
  54. Gson gson = new Gson();
  55. String jsonParams = gson.toJson(paramsMap);
  56. // 3.设置请求信息
  57. StringEntity entity = new StringEntity(jsonParams, "utf-8");
  58. entity.setContentType("application/json");
  59. httpPost.setEntity(entity);
  60. httpPost.setHeader("Accept", "application/json");
  61. return httpPost;
  62. }
  63. /**
  64. * 解析响应数据
  65. * @param response 发送请求成功后,返回的数据
  66. * @return 微信返回的参数
  67. */
  68. public static HashMap<String, String> resolverResponse(CloseableHttpResponse response) {
  69. try {
  70. // 1.获取请求码
  71. int statusCode = response.getStatusLine().getStatusCode();
  72. // 2.获取返回值 String 格式
  73. final String bodyAsString = EntityUtils.toString(response.getEntity());
  74. Gson gson = new Gson();
  75. if (statusCode == 200) {
  76. // 3.如果请求成功则解析成Map对象返回
  77. HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
  78. return resultMap;
  79. } else {
  80. if (StringUtils.isNoneBlank(bodyAsString)) {
  81. logger.error("微信支付请求失败,提示信息:{}", bodyAsString);
  82. // 4.请求码显示失败,则尝试获取提示信息
  83. HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
  84. throw new RuntimeException(resultMap.get("message"));
  85. }
  86. logger.error("微信支付请求失败,未查询到原因,提示信息:{}", response);
  87. // 其他异常,微信也没有返回数据,这就需要具体排查了
  88. throw new IOException("request failed");
  89. }
  90. } catch (Exception e) {
  91. e.printStackTrace();
  92. throw new RuntimeException(e.getMessage());
  93. } finally {
  94. try {
  95. response.close();
  96. } catch (IOException e) {
  97. e.printStackTrace();
  98. }
  99. }
  100. }
  101. /**
  102. * 创建微信支付订单-jsapi方式
  103. * @param wxPayConfig 微信配置信息
  104. * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格
  105. * @param openId 通过微信小程序或者公众号获取到用户的openId
  106. * @param wxPayClient 微信请求客户端()
  107. * @return 微信支付二维码地址
  108. */
  109. public static String wxJsApiPay(WxPayConfig wxPayConfig, WxPayReqParam basePayData, String openId, CloseableHttpClient wxPayClient) {
  110. // 1.获取请求参数的Map格式
  111. Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData);
  112. // 1.1 添加支付者信息
  113. Map<String,String> payerMap = new HashMap<>();
  114. payerMap.put("openid",openId);
  115. paramsMap.put("payer",payerMap);
  116. // 2.获取请求对象
  117. HttpPost httpPost = getHttpPost("https://api.mch.weixin.qq.com"+WxApiConstants.JSAPI_PAY.getType(),paramsMap);
  118. // 3.完成签名并执行请求
  119. CloseableHttpResponse response = null;
  120. try {
  121. response = wxPayClient.execute(httpPost);
  122. } catch (IOException e) {
  123. e.printStackTrace();
  124. throw new RuntimeException("微信支付请求失败");
  125. }
  126. // 4.解析response对象
  127. HashMap<String, String> resultMap = resolverResponse(response);
  128. if (resultMap != null) {
  129. // native请求返回的是二维码链接,前端将链接转换成二维码即可
  130. return resultMap.get("prepay_id");
  131. }
  132. return null;
  133. }
  134. }

7.创建实体存储前端微信支付所需参数(WxChatPayDto)
 小程序内需要多个参数才可以唤起微信支付,就是输入密码支付的那个支付页面。

  1. import lombok.Data;
  2. /**
  3. * <p>
  4. * 前端微信支付所需参数
  5. * </p>
  6. *
  7. * @author Lch
  8. * @dateTime 2024/2/26 16:26
  9. */
  10. @Data
  11. public class WxChatPayDto {
  12. /**
  13. * 需要支付的小程序id
  14. */
  15. private String appid;
  16. /**
  17. * 时间戳(当前的时间)
  18. */
  19. private String timeStamp;
  20. /**
  21. * 随机字符串,不长于32位。
  22. */
  23. private String nonceStr;
  24. /**
  25. * 小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
  26. */
  27. private String prepayId;
  28. /**
  29. * 签名类型,默认为RSA,仅支持RSA。
  30. */
  31. private String signType;
  32. /**
  33. * 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
  34. */
  35. private String paySign;
  36. }

8. 微信支付

  1. /**
  2. * 微信用户调用微信支付
  3. */
  4. @Override
  5. public WxChatPayDto wxPay(WxPayReqParam param) {
  6. String prepayId = WxPayCommon.wxJsApiPay(wxPayConfig, param, param.getOpenId(), wxPayClient);
  7. WxChatPayDto wxChatPayDto = new WxChatPayDto();
  8. wxChatPayDto.setAppid(wxPayConfig.getAppId());
  9. wxChatPayDto.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000));
  10. wxChatPayDto.setNonceStr(UUID.randomUUID().toString().replaceAll("-", ""));
  11. wxChatPayDto.setPrepayId("prepay_id=" + prepayId);
  12. wxChatPayDto.setSignType("RSA");
  13. wxChatPayDto.setPaySign(getSign(wxChatPayDto.getNonceStr(),wxChatPayDto.getAppid(),wxChatPayDto.getPrepayId(),Long.parseLong(wxChatPayDto.getTimeStamp())));
  14. return wxChatPayDto;
  15. }

返回给小程序的wxChatPayDto就可以唤起支付页面了。

9.成功回调

  1. import com.alibaba.fastjson.JSONObject;
  2. import com.cx.sasmc.vxpay.config.WxPayConfig;
  3. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
  4. import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
  5. import org.springframework.stereotype.Component;
  6. import javax.annotation.Resource;
  7. import javax.servlet.ServletInputStream;
  8. import javax.servlet.http.HttpServletRequest;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.BufferedReader;
  11. import java.io.IOException;
  12. import java.io.InputStreamReader;
  13. import java.io.UnsupportedEncodingException;
  14. import java.security.GeneralSecurityException;
  15. import java.util.HashMap;
  16. import java.util.Map;
  17. import java.util.stream.Collectors;
  18. import java.util.stream.Stream;
  19. /**
  20. * <p>
  21. * 微信支付回调工具类
  22. * </p>
  23. *
  24. * @author Lch
  25. * @dateTime 2024/2/27 8:50
  26. */
  27. @Component
  28. public class WxPayCallbackUtil {
  29. @Resource
  30. private Verifier verifier;
  31. @Resource
  32. private WxPayConfig wxPayConfig;
  33. /**
  34. * 获取回调数据
  35. * @param request
  36. * @param response
  37. * @return
  38. */
  39. public Map<String, String> wxChatPayCallback(HttpServletRequest request, HttpServletResponse response) {
  40. //获取报文
  41. String body = getRequestBody(request);
  42. //随机串
  43. String nonceStr = request.getHeader("Wechatpay-Nonce");
  44. //微信传递过来的签名
  45. String signature = request.getHeader("Wechatpay-Signature");
  46. //证书序列号(微信平台)
  47. String serialNo = request.getHeader("Wechatpay-Serial");
  48. //时间戳
  49. String timestamp = request.getHeader("Wechatpay-Timestamp");
  50. //构造签名串 应答时间戳\n,应答随机串\n,应答报文主体\n
  51. String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n"));
  52. Map<String, String> map = new HashMap<>(2);
  53. try {
  54. //验证签名是否通过
  55. boolean result = verifiedSign(serialNo, signStr, signature);
  56. if(result){
  57. //解密数据
  58. String plainBody = decryptBody(body);
  59. return convertWechatPayMsgToMap(plainBody);
  60. }
  61. } catch (Exception e) {
  62. e.printStackTrace();
  63. }
  64. return map;
  65. }
  66. /**
  67. * 转换body为map
  68. * @param plainBody
  69. * @return
  70. */
  71. public Map<String,String> convertWechatPayMsgToMap(String plainBody){
  72. Map<String,String> paramsMap = new HashMap<>(2);
  73. JSONObject jsonObject = JSONObject.parseObject(plainBody);
  74. //商户订单号
  75. paramsMap.put("out_trade_no",jsonObject.getString("out_trade_no"));
  76. //交易状态
  77. paramsMap.put("trade_state",jsonObject.getString("trade_state"));
  78. //附加数据
  79. paramsMap.put("attach",jsonObject.getString("attach"));
  80. if (jsonObject.getJSONObject("attach") != null && !jsonObject.getJSONObject("attach").equals("")){
  81. paramsMap.put("account_no",jsonObject.getJSONObject("attach").getString("accountNo"));
  82. }
  83. return paramsMap;
  84. }
  85. /**
  86. * 解密body的密文
  87. *
  88. * "resource": {
  89. * "original_type": "transaction",
  90. * "algorithm": "AEAD_AES_256_GCM",
  91. * "ciphertext": "",
  92. * "associated_data": "",
  93. * "nonce": ""
  94. * }
  95. *
  96. * @param body body
  97. * @return
  98. */
  99. public String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException {
  100. AesUtil aesUtil = new AesUtil(wxPayConfig.getApiKey().getBytes("utf-8"));
  101. JSONObject object = JSONObject.parseObject(body);
  102. JSONObject resource = object.getJSONObject("resource");
  103. String ciphertext = resource.getString("ciphertext");
  104. String associatedData = resource.getString("associated_data");
  105. String nonce = resource.getString("nonce");
  106. return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext);
  107. }
  108. /**
  109. * 验证签名
  110. *
  111. * @param serialNo 微信平台-证书序列号
  112. * @param signStr 自己组装的签名串
  113. * @param signature 微信返回的签名
  114. * @return
  115. * @throws UnsupportedEncodingException
  116. */
  117. public boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException {
  118. return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature);
  119. }
  120. /**
  121. * 读取请求数据流
  122. *
  123. * @param request
  124. * @return
  125. */
  126. public String getRequestBody(HttpServletRequest request) {
  127. StringBuffer sb = new StringBuffer();
  128. try (ServletInputStream inputStream = request.getInputStream();
  129. BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
  130. ) {
  131. String line;
  132. while ((line = reader.readLine()) != null) {
  133. sb.append(line);
  134. }
  135. } catch (IOException e) {
  136. e.printStackTrace();
  137. }
  138. return sb.toString();
  139. }
  140. }

成功回调方法

  1. /**
  2. * 微信支付成功回调
  3. * @param request request
  4. * @param response response
  5. * @return map
  6. */
  7. @Override
  8. public Map<String, String> wxOrderCallBack(HttpServletRequest request, HttpServletResponse response) {
  9. Map<String, String> map = new HashMap<>(2);
  10. try {
  11. Map<String, String> stringMap = wxChatPayCallback.wxChatPayCallback(request, response);
  12. //支付成功
  13. if (stringMap.get("trade_state").equals("SUCCESS")){
  14. // 获取咱们自己生成的订单号
  15. String out_trade_no = stringMap.get("out_trade_no");
  16. if (!stringRedisTemplate.hasKey("ORDER_NO:"+out_trade_no)){
  17. QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
  18. queryWrapper.eq("orderNumber",OrderStatusConstants.UNPAID);
  19. Order order = orderMapper.selectOne(queryWrapper);
  20. if (ObjectUtil.isNotEmpty(order)){
  21. //编写支付成功后逻辑 修改订单为已付款。
  22. Order upOrder = new Order();
  23. upOrder.setId(order.getId());
  24. upOrder.setOrderStatus(OrderStatusConstants.PAID);
  25. orderMapper.updateById(upOrder);
  26. // (通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
  27. // 数据有效期设置25小时
  28. stringRedisTemplate.opsForValue().set("ORDER_NO:"+out_trade_no,out_trade_no,25L, TimeUnit.HOURS);
  29. }
  30. }
  31. }
  32. //响应微信
  33. map.put("code", "SUCCESS");
  34. map.put("message", "成功");
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. }
  38. return map;
  39. }

至此支付就完成了。

10.退款 说明(项目中用到了Sa-token,@SaIgnore注解标识这个方法无需鉴权,可以匿名访问)

  1. /**
  2. * 申请退款
  3. */
  4. @SaIgnore
  5. @PostMapping("/vxPayRefund")
  6. public Result payRefund(@RequestBody @Validated WxPayRefundReqParam param){
  7. String s = vxPayService.payRefund(param);
  8. if (ObjectUtil.isNotEmpty(s)){
  9. return Result.ok(s);
  10. } else {
  11. return Result.fail("退款失败");
  12. }
  13. }
  1. /**
  2. * 申请退款
  3. * @param param param
  4. * @return string
  5. */
  6. @Override
  7. public String payRefund(WxPayRefundReqParam param) {
  8. Map<String, String> map = WxPayRefundUtil.refundPay(param, wxPayClient);
  9. Order upOrder = new Order();
  10. upOrder.setRefundNumber(map.get("refundNumber"));
  11. upOrder.setOrderStatus(OrderStatusConstants.REFUND_PROCESSING);
  12. UpdateWrapper<Order> updateWrapper = new UpdateWrapper<>();
  13. updateWrapper.eq("orderNumber",param.getTransactionId());
  14. orderMapper.update(upOrder,updateWrapper);
  15. return map.get("refund_id");
  16. }
  1. /**
  2. * 发起微信退款申请
  3. * @param param 微信支付申请退款请求参数
  4. * @return 微信支付二维码地址
  5. */
  6. public static Map<String,String> refundPay(WxPayRefundReqParam param,CloseableHttpClient wxPayClient) {
  7. Map<String,String> returnMap = new HashMap<>();
  8. // 1.获取请求参数的Map格式
  9. Map<String, Object> paramsMap = getRefundParams(param);
  10. // 2.获取请求对象
  11. HttpPost httpPost = WxPayCommon.getHttpPost("https://api.mch.weixin.qq.com"+ WxApiConstants.DOMESTIC_REFUNDS.getType(), paramsMap);
  12. // 3.完成签名并执行请求
  13. CloseableHttpResponse response = null;
  14. try {
  15. response = wxPayClient.execute(httpPost);
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. throw new RuntimeException("微信支付请求失败");
  19. }
  20. // 4.解析response对象
  21. HashMap<String, String> resultMap = WxPayCommon.resolverResponse(response);
  22. if (resultMap != null) {
  23. // 返回微信支付退款单号
  24. returnMap.put("refund_id",resultMap.get("refund_id"));
  25. returnMap.put("refundNumber",paramsMap.get("out_refund_no").toString());
  26. return returnMap;
  27. }
  28. return null;
  29. }

退款请求参数:

  1. package com.cx.sasmc.vxpay.vxpayutil;
  2. import com.cx.sasmc.utils.OrderNumberGenerate;
  3. import com.cx.sasmc.vxpay.config.WxPayCommon;
  4. import com.cx.sasmc.vxpay.reqparam.WxPayRefundReqParam;
  5. import com.cx.sasmc.vxpay.vxapienum.WxApiConstants;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.apache.http.client.methods.CloseableHttpResponse;
  8. import org.apache.http.client.methods.HttpPost;
  9. import org.apache.http.impl.client.CloseableHttpClient;
  10. import java.io.IOException;
  11. import java.util.HashMap;
  12. import java.util.Map;
  13. /**
  14. * <p>
  15. * 微信支付退款
  16. * </p>
  17. *
  18. * @author Lch
  19. * @dateTime 2024/2/27 9:49
  20. */
  21. public class WxPayRefundUtil {
  22. /**
  23. * 封装微信支付申请退款请求参数
  24. * @param param 微信支付申请退款请求参数
  25. * @return 封装后的map微信支付申请退款请求参数对象
  26. */
  27. private static Map<String, Object> getRefundParams(WxPayRefundReqParam param) {
  28. String out_refund_no = OrderNumberGenerate.orderNo("100001");
  29. Map<String, Object> paramsMap = new HashMap<>();
  30. if (StringUtils.isNoneBlank(param.getTransactionId())) {
  31. paramsMap.put("out_trade_no", param.getTransactionId());
  32. }
  33. paramsMap.put("out_refund_no", out_refund_no);
  34. paramsMap.put("notify_url", "https://你自己的回调域名.cn/vxPay/refundWechatCallback");
  35. Map<String, Object> amountMap = new HashMap<>();
  36. amountMap.put("refund", Long.valueOf(param.getRefundMoney()));
  37. amountMap.put("total", Long.valueOf(param.getTotalMoney()));
  38. amountMap.put("currency", "CNY");
  39. paramsMap.put("amount", amountMap);
  40. return paramsMap;
  41. }
  42. /**
  43. * 发起微信退款申请
  44. * @param param 微信支付申请退款请求参数
  45. * @return 微信支付二维码地址
  46. */
  47. public static Map<String,String> refundPay(WxPayRefundReqParam param,CloseableHttpClient wxPayClient) {
  48. Map<String,String> returnMap = new HashMap<>();
  49. // 1.获取请求参数的Map格式
  50. Map<String, Object> paramsMap = getRefundParams(param);
  51. // 2.获取请求对象
  52. HttpPost httpPost = WxPayCommon.getHttpPost("https://api.mch.weixin.qq.com"+ WxApiConstants.DOMESTIC_REFUNDS.getType(), paramsMap);
  53. // 3.完成签名并执行请求
  54. CloseableHttpResponse response = null;
  55. try {
  56. response = wxPayClient.execute(httpPost);
  57. } catch (IOException e) {
  58. e.printStackTrace();
  59. throw new RuntimeException("微信支付请求失败");
  60. }
  61. // 4.解析response对象
  62. HashMap<String, String> resultMap = WxPayCommon.resolverResponse(response);
  63. if (resultMap != null) {
  64. // 返回微信支付退款单号
  65. returnMap.put("refund_id",resultMap.get("refund_id"));
  66. returnMap.put("refundNumber",paramsMap.get("out_refund_no").toString());
  67. return returnMap;
  68. }
  69. return null;
  70. }
  71. }

退款回调的请求参数

  1. import cn.hutool.core.date.DateUtil;
  2. import lombok.Data;
  3. import java.math.BigDecimal;
  4. import java.util.Date;
  5. /**
  6. * <p>
  7. * 微信退款回调参数
  8. * </p>
  9. *
  10. * @author Lch
  11. * @dateTime 2024/2/27 13:40
  12. */
  13. @Data
  14. public class WxChatCallbackRefundReqParam {
  15. /**
  16. * 商户订单号
  17. */
  18. private String orderId;
  19. /**
  20. * 商户退款单号,out_refund_no
  21. */
  22. private String refundId;
  23. /**
  24. * 微信支付系统生成的订单号
  25. */
  26. private String transactionId;
  27. /**
  28. * 微信支付系统生成的退款订单号
  29. */
  30. private String transactionRefundId;
  31. /**
  32. * 退款渠道 1.ORIGINAL:原路退款 2.BALANCE:退回到余额
  33. * 3.OTHER_BALANCE:原账户异常退到其他余额账户
  34. * 4.OTHER_BANKCARD:原银行卡异常退到其他银行卡
  35. */
  36. private String channel;
  37. /**
  38. * 退款成功时间 当前退款成功时才有此返回值
  39. */
  40. private Date successTime;
  41. /**
  42. * 退款状态 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。
  43. * 1.SUCCESS:退款成功 2.CLOSED:退款关闭 3.PROCESSING:退款处理中 4.ABNORMAL:退款异常
  44. */
  45. private String status;
  46. /**
  47. * 退款金额
  48. */
  49. private BigDecimal refundMoney;
  50. public Date getSuccessTime() {
  51. return successTime;
  52. }
  53. public void setSuccessTime(String successTime) {
  54. // Hutool工具包的方法,自动识别一些常用格式的日期字符串
  55. this.successTime = DateUtil.parse(successTime);
  56. }
  57. }

 退款业务处理接口

  1. import com.cx.sasmc.vxpay.reqparam.WxChatCallbackRefundReqParam;
  2. /**
  3. * <p>
  4. * 退款回调
  5. * </p>
  6. *
  7. * @author Lch
  8. * @dateTime 2024/2/27 13:54
  9. */
  10. public interface WechatRefundCallback {
  11. /**
  12. * 退款成功处理情况
  13. */
  14. void success(WxChatCallbackRefundReqParam refundData);
  15. /**
  16. * 退款失败处理情况
  17. */
  18. void fail(WxChatCallbackRefundReqParam refundData);
  19. }

 微信退款回调方法

  1. import javax.servlet.http.HttpServletRequest;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. /**
  5. * <p>
  6. * 将通知参数转化为字符串
  7. * </p>
  8. *
  9. * @author Lch
  10. * @dateTime 2024/2/27 13:43
  11. */
  12. public class HttpUtils {
  13. /**
  14. * 将通知参数转化为字符串
  15. * @param request
  16. * @return
  17. */
  18. public static String readData(HttpServletRequest request) {
  19. BufferedReader br = null;
  20. try {
  21. StringBuilder result = new StringBuilder();
  22. br = request.getReader();
  23. for (String line; (line = br.readLine()) != null; ) {
  24. if (result.length() > 0) {
  25. result.append("\n");
  26. }
  27. result.append(line);
  28. }
  29. return result.toString();
  30. } catch (IOException e) {
  31. throw new RuntimeException(e);
  32. } finally {
  33. if (br != null) {
  34. try {
  35. br.close();
  36. } catch (IOException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }
  41. }
  42. }
  1. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import javax.servlet.http.HttpServletRequest;
  5. import java.io.IOException;
  6. import java.nio.charset.StandardCharsets;
  7. import java.time.DateTimeException;
  8. import java.time.Duration;
  9. import java.time.Instant;
  10. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
  11. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;
  12. /**
  13. * <p>
  14. * 微信支付 退款回调请求验证
  15. * </p>
  16. *
  17. * @author Lch
  18. * @dateTime 2024/2/27 13:47
  19. */
  20. public class WechatPayValidatorForRequest {
  21. private final Logger logger = LoggerFactory.getLogger(WechatPayValidatorForRequest.class);
  22. /**
  23. * 应答超时时间,单位为分钟
  24. */
  25. protected static final long RESPONSE_EXPIRED_MINUTES = 5;
  26. protected final Verifier verifier;
  27. protected final String body;
  28. protected final String requestId;
  29. public WechatPayValidatorForRequest(Verifier verifier, String body, String requestId) {
  30. this.verifier = verifier;
  31. this.body = body;
  32. this.requestId = requestId;
  33. }
  34. protected static IllegalArgumentException parameterError(String message, Object... args) {
  35. message = String.format(message, args);
  36. return new IllegalArgumentException("parameter error: " + message);
  37. }
  38. protected static IllegalArgumentException verifyFail(String message, Object... args) {
  39. message = String.format(message, args);
  40. return new IllegalArgumentException("signature verify fail: " + message);
  41. }
  42. public final boolean validate(HttpServletRequest request) throws IOException {
  43. try {
  44. validateParameters(request);
  45. String message = buildMessage(request);
  46. String serial = request.getHeader(WECHAT_PAY_SERIAL);
  47. String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
  48. if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
  49. throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
  50. serial, message, signature, request.getHeader(REQUEST_ID));
  51. }
  52. } catch (IllegalArgumentException e) {
  53. logger.warn(e.getMessage());
  54. return false;
  55. }
  56. return true;
  57. }
  58. protected final void validateParameters(HttpServletRequest request) {
  59. // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
  60. String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
  61. String header = null;
  62. for (String headerName : headers) {
  63. header = request.getHeader(headerName);
  64. if (header == null) {
  65. throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
  66. }
  67. }
  68. String timestampStr = header;
  69. try {
  70. Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
  71. // 拒绝过期应答
  72. if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
  73. throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
  74. }
  75. } catch (DateTimeException | NumberFormatException e) {
  76. throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
  77. }
  78. }
  79. protected final String buildMessage(HttpServletRequest request) throws IOException {
  80. String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
  81. String nonce = request.getHeader(WECHAT_PAY_NONCE);
  82. return timestamp + "\n"
  83. + nonce + "\n"
  84. + body + "\n";
  85. }
  86. }
  1. import com.cx.sasmc.vxpay.config.WxPayConfig;
  2. import com.cx.sasmc.vxpay.reqparam.WxChatCallbackRefundReqParam;
  3. import com.cx.sasmc.vxpay.service.WechatRefundCallback;
  4. import com.google.gson.Gson;
  5. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
  6. import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
  7. import org.apache.commons.lang3.StringUtils;
  8. import javax.servlet.http.HttpServletRequest;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.math.BigDecimal;
  11. import java.nio.charset.StandardCharsets;
  12. import java.security.GeneralSecurityException;
  13. import java.util.HashMap;
  14. import java.util.Map;
  15. /**
  16. * <p>
  17. * 微信支付申请退款回调
  18. * </p>
  19. *
  20. * @author Lch
  21. * @dateTime 2024/2/27 13:51
  22. */
  23. public class WxPayRefundCallbackUtil {
  24. /**
  25. * 微信支付申请退款回调方法
  26. *
  27. * @param verifier 证书
  28. * @param wxPayConfig 微信配置
  29. * @param refundCallback 回调方法,用于处理业务逻辑,包含退款成功处理于退款失败处理
  30. * @return json格式的string数据,直接返回给微信
  31. */
  32. public static String wxPayRefundCallback(HttpServletRequest request, HttpServletResponse response, Verifier verifier, WxPayConfig wxPayConfig, WechatRefundCallback refundCallback) {
  33. Gson gson = new Gson();
  34. // 1.处理通知参数
  35. final String body = HttpUtils.readData(request);
  36. HashMap<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
  37. // 2.签名验证
  38. WechatPayValidatorForRequest wechatForRequest = new WechatPayValidatorForRequest(verifier, body, (String) bodyMap.get("id"));
  39. try {
  40. if (!wechatForRequest.validate(request)) {
  41. // 通知验签失败
  42. response.setStatus(500);
  43. final HashMap<String, Object> map = new HashMap<>();
  44. map.put("code", "ERROR");
  45. map.put("message", "通知验签失败");
  46. return gson.toJson(map);
  47. }
  48. } catch (Exception e) {
  49. e.printStackTrace();
  50. }
  51. // 3.获取明文数据
  52. String plainText = decryptFromResource(bodyMap, wxPayConfig);
  53. HashMap<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
  54. // log.info("退款plainTextMap:{}", plainTextMap);
  55. // 4.封装微信返回的数据
  56. WxChatCallbackRefundReqParam refundData = getRefundCallbackData(plainTextMap);
  57. if ("SUCCESS".equals(refundData.getStatus())) {
  58. // 执行业务逻辑
  59. refundCallback.success(refundData);
  60. } else {
  61. // 特殊情况退款失败业务处理,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款
  62. refundCallback.fail(refundData);
  63. }
  64. // 5.成功应答
  65. response.setStatus(200);
  66. final HashMap<String, Object> resultMap = new HashMap<>();
  67. resultMap.put("code", "SUCCESS");
  68. resultMap.put("message", "成功");
  69. return gson.toJson(resultMap);
  70. }
  71. private static WxChatCallbackRefundReqParam getRefundCallbackData(HashMap<String, Object> plainTextMap) {
  72. Gson gson = new Gson();
  73. WxChatCallbackRefundReqParam refundData = new WxChatCallbackRefundReqParam();
  74. String successTime = String.valueOf(plainTextMap.get("success_time"));
  75. if (StringUtils.isNoneBlank(successTime)) {
  76. refundData.setSuccessTime(successTime);
  77. }
  78. refundData.setOrderId(String.valueOf(plainTextMap.get("out_trade_no")));
  79. refundData.setRefundId(String.valueOf(plainTextMap.get("out_refund_no")));
  80. refundData.setTransactionId(String.valueOf(plainTextMap.get("transaction_id")));
  81. refundData.setTransactionRefundId(String.valueOf(plainTextMap.get("refund_id")));
  82. refundData.setChannel(String.valueOf(plainTextMap.get("channel")));
  83. final String status = String.valueOf(plainTextMap.get("refund_status"));
  84. refundData.setStatus(status);
  85. String amount = String.valueOf(plainTextMap.get("amount"));
  86. HashMap<String, Object> amountMap = gson.fromJson(amount, HashMap.class);
  87. String refundMoney = String.valueOf(amountMap.get("refund"));
  88. refundData.setRefundMoney(new BigDecimal(refundMoney).movePointLeft(2));
  89. // log.info("refundData:{}", refundData);
  90. return refundData;
  91. }
  92. /**
  93. * 对称解密
  94. */
  95. private static String decryptFromResource(HashMap<String, Object> bodyMap, WxPayConfig wxPayConfig) {
  96. // 通知数据
  97. Map<String, String> resourceMap = (Map) bodyMap.get("resource");
  98. // 数据密文
  99. String ciphertext = resourceMap.get("ciphertext");
  100. // 随机串
  101. String nonce = resourceMap.get("nonce");
  102. // 附加数据
  103. String associateData = resourceMap.get("associated_data");
  104. AesUtil aesUtil = new AesUtil(wxPayConfig.getApiKey().getBytes(StandardCharsets.UTF_8));
  105. try {
  106. return aesUtil.decryptToString(associateData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
  107. } catch (GeneralSecurityException e) {
  108. e.printStackTrace();
  109. throw new RuntimeException("解密失败");
  110. }
  111. }
  112. }
  1. /**
  2. * 退款回调
  3. * @param request r
  4. * @param response rp
  5. * @return s
  6. */
  7. @Override
  8. public String refundWechatCallback(HttpServletRequest request, HttpServletResponse response) {
  9. return WxPayRefundCallbackUtil.wxPayRefundCallback(request, response, verifier, wxPayConfiga, new WechatRefundCallback() {
  10. /**
  11. * 退款成功
  12. */
  13. @Override
  14. public void success(WxChatCallbackRefundReqParam refundData) {
  15. Order upOrder = new Order();
  16. upOrder.setId(Long.valueOf(refundData.getOrderId()));
  17. upOrder.setOrderStatus(OrderStatusConstants.CANCELED);
  18. orderMapper.updateById(upOrder);
  19. }
  20. /**
  21. * 退款失败
  22. */
  23. @Override
  24. public void fail(WxChatCallbackRefundReqParam refundData) {
  25. Order order = orderMapper.selectById(Long.valueOf(refundData.getOrderId()));
  26. if (ObjectUtil.isNotEmpty(order.getOperatorName()) && ObjectUtil.isNotEmpty(order.getOperatorId())){
  27. Order upOrder = new Order();
  28. upOrder.setId(Long.valueOf(refundData.getOrderId()));
  29. upOrder.setOrderStatus(OrderStatusConstants.OPERATOR_ACCEPTED);
  30. orderMapper.updateById(upOrder);
  31. } else {
  32. Order upOrder = new Order();
  33. upOrder.setId(Long.valueOf(refundData.getOrderId()));
  34. upOrder.setOrderStatus(OrderStatusConstants.PAID);
  35. orderMapper.updateById(upOrder);
  36. }
  37. }
  38. });
  39. }

也是真乱啊 如果大家需要demo就留个言或者私信我一下我就弄一个,或者在在博文里按照图片的顺序把所有代码全部贴进来。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/608341
推荐阅读
相关标签
  

闽ICP备14008679号