• 微信服务商模式支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)


      😊 @ 作者: 一恍过去
      🎊 @ 社区: Java技术栈交流
      🎉 @ 主题: 微信服务商模式支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)
      ⏱️ @ 创作时间: 2022年08月24日

      在这里插入图片描述

      微信服务商模式支付是指第三方机构作为微信支付的服务商,为商户提供支付服务。在这种模式下,商户可以通过与服务商合作,接入微信支付的功能,从而实现在线支付功能。

      • 商户与服务商签约:商户需要选择合适的服务商,并与其签订协议。协议中会包含商户的结算方式、费率等相关条款。
      • 商户开通微信支付:商户在与服务商签约后,需要提交相关的资料给服务商完成开通微信支付的申请。
      • 资质审核:微信支付会对商户提交的资料进行审核,包括商户的营业执照、经营许可证等。审核通过后,商户将获得微信支付的商户号。
      • 技术对接:商户需要按照微信支付提供的开发文档,与服务商进行技术对接。这包括接口的集成、支付页面的设计等。
      • 支付交易处理:商户接入微信支付后,用户在商户的网站或应用上选择微信支付,支付请求将通过服务商的系统发送到微信支付平台。微信支付平台会进行交易的处理,并将支付结果返回给服务商。
      • 结算与资金管理:商户的交易款项将通过服务商进行结算。服务商会按照约定的结算周期将款项划拨到商户的账户。

      参考API: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml

      1、引入POM

              
              <dependency>
                  <groupId>com.github.wechatpay-apiv3groupId>
                  <artifactId>wechatpay-apache-httpclientartifactId>
                  <version>0.4.7version>
              dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      2、配置Yaml

      wxpay:
        #应用编号
        appId: xxxx
        #商户号
        mchId: xxx
        # APIv2密钥
        apiKey: xxxx
        # APIv3密钥
        apiV3Key: xxx
        # 微信支付V3-url前缀
        baseUrl: https://api.mch.weixin.qq.com/v3
        # 支付通知回调, pjm6m9.natappfree.cc 为内网穿透地址
        notifyUrl: http://pjm6m9.natappfree.cc/pay/payNotify
        # 退款通知回调, pjm6m9.natappfree.cc 为内网穿透地址
        refundNotifyUrl: http://pjm6m9.natappfree.cc/pay/refundNotify
        # 密钥路径,resources根目录下
        keyPemPath: apiclient_key.pem
        #商户证书序列号
        serialNo: xxxxx
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

      3、配置密钥文件

      在商户/服务商平台的”账户中心" => “API安全” 进行API证书、密钥的设置,API证书主要用于获取“商户证书序列号”以及“p12”、“key.pem”、”cert.pem“证书文件,j将获取的apiclient_key.pem文件放在项目的resources目录下。
      在这里插入图片描述

      4、配置PayConfig

      
      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.exception.HttpCodeException;
      import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
      import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
      import lombok.Data;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.http.impl.client.CloseableHttpClient;
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.context.annotation.Bean;
      import org.springframework.stereotype.Component;
      
      import java.io.IOException;
      import java.io.InputStream;
      import java.nio.charset.StandardCharsets;
      import java.security.GeneralSecurityException;
      import java.security.PrivateKey;
      
      /**
       * @Description:
       **/
      @Component
      @Data
      @Slf4j
      @ConfigurationProperties(prefix = "wxpay")
      public class WechatPayConfig {
          /**
           * 应用编号
           */
          private String appId;
          /**
           * 商户号
           */
          private String mchId;
          /**
           * 服务商商户号
           */
          private String slMchId;
          /**
           * APIv2密钥
           */
          private String apiKey;
          /**
           * APIv3密钥
           */
          private String apiV3Key;
          /**
           * 支付通知回调地址
           */
          private String notifyUrl;
          /**
           * 退款回调地址
           */
          private String refundNotifyUrl;
      
          /**
           * API 证书中的 key.pem
           */
          private String keyPemPath;
      
          /**
           * 商户序列号
           */
          private String serialNo;
      
          /**
           * 微信支付V3-url前缀
           */
          private String baseUrl;
      
          /**
           * 获取商户的私钥文件
           * @param keyPemPath
           * @return
           */
          public PrivateKey getPrivateKey(String keyPemPath){
      
                  InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
                  if(inputStream==null){
                      throw new RuntimeException("私钥文件不存在");
                  }
                  return PemUtil.loadPrivateKey(inputStream);
          }
      
          /**
           * 获取证书管理器实例
           * @return
           */
          @Bean
          public  Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {
      
              log.info("获取证书管理器实例");
      
              //获取商户私钥
              PrivateKey privateKey = getPrivateKey(keyPemPath);
      
              //私钥签名对象
              PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey);
      
              //身份认证对象
              WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
      
              // 使用定时更新的签名验证器,不需要传入证书
              CertificatesManager certificatesManager = CertificatesManager.getInstance();
              certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));
      
              return certificatesManager.getVerifier(mchId);
          }
      
      
          /**
           * 获取支付http请求对象
           * @param verifier
           * @return
           */
          @Bean(name = "wxPayClient")
          public CloseableHttpClient getWxPayClient(Verifier verifier)  {
      
              //获取商户私钥
              PrivateKey privateKey = getPrivateKey(keyPemPath);
      
              WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                      .withMerchant(mchId, serialNo, privateKey)
                      .withValidator(new WechatPay2Validator(verifier));
      
              // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
              return builder.build();
          }
      
          /**
           * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
           */
          @Bean(name = "wxPayNoSignClient")
          public CloseableHttpClient getWxPayNoSignClient(){
      
              //获取商户私钥
              PrivateKey privateKey = getPrivateKey(keyPemPath);
      
              //用于构造HttpClient
              WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                      //设置商户信息
                      .withMerchant(mchId, serialNo, privateKey)
                      //无需进行签名验证、通过withValidator((response) -> true)实现
                      .withValidator((response) -> true);
      
              // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
              return builder.build();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154

      5、定义统一枚举

      WechatPayUrlEnum:

      
      @AllArgsConstructor
      @Getter
      public enum WechatPayUrlEnum {
      
      
      	/**
      	 * native
      	 */
      	NATIVE("native"),
      	/**
      	 * app
      	 */
      	APP("app"),
      	/**
      	 * h5
      	 */
      	H5("h5"),
      	/**
      	 *  jsapi
      	 */
      	JSAPI("jsapi"),
      
      	/**
      	 *  小程序jsapi
      	 */
      	SUB_JSAPI("sub_jsapi"),
      
      	/**
      	 * 下单
      	 */
      	PAY_TRANSACTIONS("/pay/partner/transactions/"),
      
      	/**
      	 * 查询订单
      	 */
      	ORDER_QUERY_BY_NO("/pay/partner/transactions/out-trade-no/"),
      
      	/**
      	 * 关闭订单
      	 */
      	CLOSE_ORDER_BY_NO("/pay/partner/transactions/out-trade-no/%s/close"),
      
      	/**
      	 * 申请退款
      	 */
      	DOMESTIC_REFUNDS("/refund/domestic/refunds"),
      
      	/**
      	 * 查询单笔退款
      	 */
      	DOMESTIC_REFUNDS_QUERY("/refund/domestic/refunds/"),
      
      	/**
      	 * 申请交易账单
      	 */
      	TRADE_BILLS("/bill/tradebill"),
      
      	/**
      	 * 申请资金账单
      	 */
      	FUND_FLOW_BILLS("/bill/fundflowbill"),
      
      	/**
      	 * 申请单个子商户资金账单
      	 */
      	SUB_MERCHANT_FUND_FLOW_BILLS("/bill/sub-merchant-fundflowbill");
      
      	/**
      	 * 类型
      	 */
      	private final String type;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73

      6、封装统一请求处理

      WechatPayRequest:

      
      import lombok.extern.slf4j.Slf4j;
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpStatus;
      import org.apache.http.client.methods.CloseableHttpResponse;
      import org.apache.http.client.methods.HttpGet;
      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 org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      import java.io.IOException;
      
      /**
       * @Description:
       **/
      @Component
      @Slf4j
      public class WechatPayRequest {
          @Resource
          private CloseableHttpClient wxPayClient;
          public  String wechatHttpGet(String url) {
              try {
                  // 拼接请求参数
                  HttpGet httpGet = new HttpGet(url);
                  httpGet.setHeader("Accept", "application/json");
      
                  //完成签名并执行请求
                  CloseableHttpResponse response = wxPayClient.execute(httpGet);
      
                  return getResponseBody(response);
              }catch (Exception e){
                  throw new RuntimeException(e.getMessage());
              }
          }
      
          public  String wechatHttpPost(String url,String paramsStr) {
              try {
                  HttpPost httpPost = new HttpPost(url);
                  StringEntity entity = new StringEntity(paramsStr, "utf-8");
                  entity.setContentType("application/json");
                  httpPost.setEntity(entity);
                  httpPost.setHeader("Accept", "application/json");
      
                  CloseableHttpResponse response = wxPayClient.execute(httpPost);
                  return getResponseBody(response);
              }catch (Exception e){
                  throw new RuntimeException(e.getMessage());
              }
          }
      
         private String getResponseBody(CloseableHttpResponse response) throws IOException {
      
                  //响应体
                 HttpEntity entity = response.getEntity();
                 String body = entity==null?"":EntityUtils.toString(entity);
                  //响应状态码
                  int statusCode = response.getStatusLine().getStatusCode();
      
                  //处理成功,204是,关闭订单时微信返回的正常状态码
                  if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
                      log.info("成功, 返回结果 = " + body);
                  } else {
                      String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
                      log.error(msg);
                      throw new RuntimeException(msg);
                  }
                  return body;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72

      7、回调校验器

      WechatPayValidator:

      
      import com.alibaba.fastjson.JSONObject;
      import com.alibaba.fastjson.TypeReference;
      import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
      import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.http.HttpEntity;
      import org.apache.http.client.methods.CloseableHttpResponse;
      import org.apache.http.util.EntityUtils;
      
      import javax.servlet.http.HttpServletRequest;
      import java.io.IOException;
      import java.nio.charset.StandardCharsets;
      import java.time.DateTimeException;
      import java.time.Duration;
      import java.time.Instant;
      import java.util.Map;
      
      import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
      
      /**
       * @Description:
       **/
      @Slf4j
      public class WechatPayValidator {
          /**
           * 应答超时时间,单位为分钟
           */
          private static final long RESPONSE_EXPIRED_MINUTES = 5;
          private final Verifier verifier;
          private final String requestId;
          private final String body;
      
      
          public WechatPayValidator(Verifier verifier, String requestId, String body) {
              this.verifier = verifier;
              this.requestId = requestId;
              this.body = body;
          }
      
          protected static IllegalArgumentException parameterError(String message, Object... args) {
              message = String.format(message, args);
              return new IllegalArgumentException("parameter error: " + message);
          }
      
          protected static IllegalArgumentException verifyFail(String message, Object... args) {
              message = String.format(message, args);
              return new IllegalArgumentException("signature verify fail: " + message);
          }
      
          public final boolean validate(HttpServletRequest request)  {
              try {
                  //处理请求参数
                  validateParameters(request);
      
                  //构造验签名串
                  String message = buildMessage(request);
      
                  String serial = request.getHeader(WECHAT_PAY_SERIAL);
                  String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
      
                  //验签
                  if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                      throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                              serial, message, signature, requestId);
                  }
              } catch (IllegalArgumentException e) {
                  log.warn(e.getMessage());
                  return false;
              }
      
              return true;
          }
      
          private  void validateParameters(HttpServletRequest request) {
      
              // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
              String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
      
              String header = null;
              for (String headerName : headers) {
                  header = request.getHeader(headerName);
                  if (header == null) {
                      throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
                  }
              }
      
              //判断请求是否过期
              String timestampStr = header;
              try {
                  Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
                  // 拒绝过期请求
                  if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                      throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
                  }
              } catch (DateTimeException | NumberFormatException e) {
                  throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
              }
          }
      
          private  String buildMessage(HttpServletRequest request)  {
              String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
              String nonce = request.getHeader(WECHAT_PAY_NONCE);
              return timestamp + "\n"
                      + nonce + "\n"
                      + body + "\n";
          }
      
          private  String getResponseBody(CloseableHttpResponse response) throws IOException {
              HttpEntity entity = response.getEntity();
              return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
          }
      
          /**
           * 对称解密,异步通知的加密数据
           * @param resource 加密数据
           * @param apiV3Key apiV3密钥
           * @param type 1-支付,2-退款
           * @return
           */
          public static  Map<String, Object> decryptFromResource(String resource,String apiV3Key,Integer type) {
      
              String msg = type==1?"支付成功":"退款成功";
              log.info(msg+",回调通知,密文解密");
              try {
              //通知数据
              Map<String, String> resourceMap = JSONObject.parseObject(resource, new TypeReference<Map<String, Object>>() {
              });
              //数据密文
              String ciphertext = resourceMap.get("ciphertext");
              //随机串
              String nonce = resourceMap.get("nonce");
              //附加数据
              String associatedData = resourceMap.get("associated_data");
      
              log.info("密文: {}", ciphertext);
              AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
              String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                      nonce.getBytes(StandardCharsets.UTF_8),
                      ciphertext);
      
              log.info(msg+",回调通知,解密结果 : {}", resourceStr);
              return JSONObject.parseObject(resourceStr, new TypeReference<Map<String, Object>>(){});
          }catch (Exception e){
              throw new RuntimeException("回调参数,解密失败!");
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148

      8、回调Body内容处理工具

      HttpUtils:

      
      import javax.servlet.http.HttpServletRequest;
      import java.io.BufferedReader;
      import java.io.IOException;
      
      /**
       * @Description:
       **/
      public class HttpUtils {
      
          /**
           * 将通知参数转化为字符串
           * @param request
           * @return
           */
          public static String readData(HttpServletRequest request) {
              BufferedReader br = null;
              try {
                  StringBuilder result = new StringBuilder();
                  br = request.getReader();
                  for (String line; (line = br.readLine()) != null; ) {
                      if (result.length() > 0) {
                          result.append("\n");
                      }
                      result.append(line);
                  }
                  return result.toString();
              } catch (IOException e) {
                  throw new RuntimeException(e);
              } finally {
                  if (br != null) {
                      try {
                          br.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40

      在这里插入图片描述

      9、封装统一代码

      9.1、统一下单处理接口

      PayController:

      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.JSONObject;
      import com.alibaba.fastjson.TypeReference;
      import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
      import com.lhz.demo.pay.WechatPayConfig;
      import com.lhz.demo.model.enums.WechatPayUrlEnum;
      import com.lhz.demo.pay.WechatPayRequest;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.http.client.methods.CloseableHttpResponse;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.impl.client.CloseableHttpClient;
      import org.apache.http.util.EntityUtils;
      import org.springframework.web.bind.annotation.*;
      
      import javax.annotation.Resource;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.io.*;
      import java.nio.charset.StandardCharsets;
      import java.util.*;
      
      
      /**
       * @Description:
       **/
      @Api(tags = "支付接口(API3)")
      @RestController
      @RequestMapping("/test")
      @Slf4j
      public class PayController {
      
          @Resource
          private WechatPayConfig wechatPayConfig;
      
          @Resource
          private WechatPayRequest wechatPayRequest;
      
      	/**
           * 无需应答签名
           */
          @Resource
          private CloseableHttpClient wxPayNoSignClient;
      
      	// 公众号APPID
          private static final String WX_JSAPI_SUB_APPID = "xxxxxxxx";
      	// 小程序APPID
          private static final String WX_JSAPI_MINI_SUB_APPID = "xxxxxxxx";
          // 模拟特约商户子商户号,实际情况:应该写在表中,与商家表进行映射,根据订单动态获取
          private static  final String subMchId ="xxx";
          
      	/**
           * type:h5、jsapi、app、native、sub_jsapi
           * H5支付时,需要在商户平台--"产品中心"--"开发配置"自行配置支付提交的域名
           * @param type
           * @return
           */
          @ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口")
          @ApiOperationSupport(order = 10)
          @GetMapping("/transactions")
          public Map<String,Object> transactions(String type) {
      
              log.info("统一下单API,支付方式:{}",type);
      
              // 统一参数封装
              Map<String, Object> params = new HashMap<>(8);
              params.put("sp_appid", wechatPayConfig.getAppId());
              params.put("sp_mchid", wechatPayConfig.getMchId());
              // 子商户号
              params.put("sub_mchid",subMchId);
      
              params.put("description", "测试商品");
              int outTradeNo = new Random().nextInt(999999999);
              params.put("out_trade_no", outTradeNo + "");
              // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
              params.put("attach", "name=123");
              params.put("notify_url", wechatPayConfig.getNotifyUrl());
      
      
      
              // 金额单位为分
              Map<String, Object> amountMap = new HashMap<>(4);
              amountMap.put("total", 1);
              amountMap.put("currency", "CNY");
              params.put("amount", amountMap);
      
              // 场景信息
              Map<String, Object> sceneInfoMap = new HashMap<>(4);
              // 客户端IP
              sceneInfoMap.put("payer_client_ip", "127.0.0.1");
              // 商户端设备号(门店号或收银设备ID)
              sceneInfoMap.put("device_id", "127.0.0.1");
      
              // 除H5与JSAPI有特殊参数外,其他的支付方式都一样
              if (type.equals(WechatPayUrlEnum.H5.getType())) {
      
                  Map<String, Object> h5InfoMap = new HashMap<>(4);
                  // 场景类型:iOS, Android, Wap
                  h5InfoMap.put("type", "IOS");
                  sceneInfoMap.put("h5_info", h5InfoMap);
              } else if (type.equals(WechatPayUrlEnum.JSAPI.getType()) || type.equals(WechatPayUrlEnum.SUB_JSAPI.getType())) {
                  // TODO 小程序或者公众号支付时,需要传入子应用的appId,并且在服务商平台的特约商户管理中进行授权
                  // 判断是否小程序还是服务号支付
                  String subAppId = WX_JSAPI_SUB_APPID;
                  if (type.equals(WechatPayUrlEnum.SUB_JSAPI.getType())) {
                      // 发起JSAPI的小程序
                      subAppId = WX_JSAPI_MINI_SUB_APPID;
                  }
      
                  // 子商户申请的应用ID,若sub_openid有传的情况下,sub_appid必填
                   params.put("sub_appid", subAppId);
                  Map<String, Object> payerMap = new HashMap<>(4);
                  // openId需要对应小程序或者公众号授权的openId
                  payerMap.put("sub_openid", "xxxx");
                  params.put("payer", payerMap);
      
              }
      
              params.put("scene_info", sceneInfoMap);
      
              String paramsStr = JSON.toJSONString(params);
              log.info("请求参数 ===> {}" + paramsStr);
      
              // 重写type值,因为小程序会多一个下划线(sub_type)
              String[] split = type.split("_");
              String  newType = split[split.length - 1];
      
              String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.PAY_TRANSACTIONS.getType().concat(newType)), paramsStr);
              Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>(){});
      
              Map<String, Object> signMap = paySignMsg(resMap, type);
              resMap.put("type",type);
              resMap.put("signMap",signMap);
              return resMap;
          }
      
      
          private  Map<String, Object>  paySignMsg(Map<String, Object> map,String type){
              // 设置签名信息,Native与H5不需要
              if(type.equals(WechatPayUrlEnum.H5.getType()) || type.equals(WechatPayUrlEnum.NATIVE.getType()) ){
                  return null;
              }
      
              long timeMillis = System.currentTimeMillis();
              String appId = wechatPayConfig.getAppId();
              String timeStamp = timeMillis/1000+"";
              String nonceStr = timeMillis+"";
              String prepayId = map.get("prepay_id").toString();
              String packageStr = "prepay_id="+prepayId;
      
              // 公共参数
              Map<String, Object> resMap = new HashMap<>();
              resMap.put("nonceStr",nonceStr);
              resMap.put("timeStamp",timeStamp);
      
              // JSAPI、SUB_JSAPI(小程序)
              if(type.equals(WechatPayUrlEnum.JSAPI.getType()) || type.equals(WechatPayUrlEnum.SUB_JSAPI.getType()) ) {
                  resMap.put("appId",appId);
                  resMap.put("package", packageStr);
                  // 使用字段appId、timeStamp、nonceStr、package进行签名
                  String paySign = createSign(resMap);
                  resMap.put("paySign", paySign);
                  resMap.put("signType", "HMAC-SHA256");
              }
              // APP
              if(type.equals(WechatPayUrlEnum.APP.getType())) {
                  resMap.put("appid",appId);
                  resMap.put("prepayid", prepayId);
                  // 使用字段appId、timeStamp、nonceStr、prepayId进行签名
                  String sign = createSign(resMap);
                  resMap.put("package", "Sign=WXPay");
                  resMap.put("partnerid", wechatPayConfig.getMchId());
                  resMap.put("sign", sign);
                  resMap.put("signType", "HMAC-SHA256");
              }
              return resMap;
          }
      
          /**
           * 获取加密数据
           */
          private  String createSign(Map<String, Object> params){
              try {
                  Map<String, Object> treeMap = new TreeMap<>(params);
                  List<String> signList = new ArrayList<>(5);
                  for (Map.Entry<String, Object> entry : treeMap.entrySet())
                  {
                      signList.add(entry.getKey() + "=" + entry.getValue());
                  }
                  String signStr = String.join("&", signList);
      
                  signStr = signStr+"&key="+wechatPayConfig.getApiV3Key();
                  System.out.println(signStr);
      
                  Mac sha = Mac.getInstance("HmacSHA256");
                  SecretKeySpec secretKey = new SecretKeySpec(wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                  sha.init(secretKey);
                  byte[] array = sha.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
                  StringBuilder sb = new StringBuilder();
                  for (byte item : array) {
                      sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
                  }
                  signStr = sb.toString().toUpperCase();
                  System.out.println(signStr);
      
                  return signStr;
              }catch (Exception e){
                  throw new RuntimeException("加密失败!");
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182
      • 183
      • 184
      • 185
      • 186
      • 187
      • 188
      • 189
      • 190
      • 191
      • 192
      • 193
      • 194
      • 195
      • 196
      • 197
      • 198
      • 199
      • 200
      • 201
      • 202
      • 203
      • 204
      • 205
      • 206
      • 207
      • 208
      • 209
      • 210
      • 211
      • 212

      9.2 、其他处理(退款、查询、取消订单等)接口

      包含接口:根据订单号查询订单关闭(取消)订单申请退款查询单笔退款信息申请交易账单申请资金账单申请单个子商户资金账单下载账单

      PayController:

      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.JSONObject;
      import com.alibaba.fastjson.TypeReference;
      import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
      import com.lhz.demo.pay.WechatPayConfig;
      import com.lhz.demo.model.enums.WechatPayUrlEnum;
      import com.lhz.demo.pay.WechatPayRequest;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.http.client.methods.CloseableHttpResponse;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.impl.client.CloseableHttpClient;
      import org.apache.http.util.EntityUtils;
      import org.springframework.web.bind.annotation.*;
      
      import javax.annotation.Resource;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.io.*;
      import java.nio.charset.StandardCharsets;
      import java.util.*;
      
      
      /**
       * @Description:
       **/
      @Api(tags = "支付接口(API3)")
      @RestController
      @RequestMapping("/test")
      @Slf4j
      public class PayController {
      
          @Resource
          private WechatPayConfig wechatPayConfig;
      
          @Resource
          private WechatPayRequest wechatPayRequest;
      
      	/**
           * 无需应答签名
           */
          @Resource
          private CloseableHttpClient wxPayNoSignClient;
          
          // 模拟特约商户子商户号,实际情况:应该写在表中,与商家表进行映射,根据订单动态获取
          private static  final String subMchId ="xxx";
      
      	 @ApiOperation(value = "根据订单号查询订单-统一接口", notes = "根据订单号查询订单-统一接口")
          @ApiOperationSupport(order = 15)
          @GetMapping("/transactions/{orderNo}")
          public  Map<String, Object> transactionsByOrderNo(@PathVariable("orderNo") String orderNo) {
      
              // TODO 如果是扫码支付时,该接口就很有必要,应该前端通过轮询的方式请求该接口查询订单是否支付成功
      
              log.info("根据订单号查询订单,订单号: {}", orderNo);
      
              String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.ORDER_QUERY_BY_NO.getType().concat(orderNo))
                      .concat("?sp_mchid=").concat(wechatPayConfig.getMchId())
                      .concat("&sub_mchid=").concat(subMchId);
      
              String res = wechatPayRequest.wechatHttpGet(url);
              log.info("查询订单结果:{}",res);
      
              Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
      
              String outTradeNo = resMap.get("out_trade_no").toString();
              String appId = resMap.get("sp_appid").toString();
              String mchId = resMap.get("sp_mchid").toString();
              String subMchId = resMap.get("sub_mchid").toString();
              // 支付后才返回参数
              Object payer = resMap.get("payer");
              Object attach = resMap.get("attach");
              /**
               *         交易状态,枚举值:
               *         SUCCESS:支付成功
               *         REFUND:转入退款
               *         NOTPAY:未支付
               *         CLOSED:已关闭
               *         REVOKED:已撤销(仅付款码支付会返回)
               *         USERPAYING:用户支付中(仅付款码支付会返回)
               *         PAYERROR:支付失败(仅付款码支付会返回)
               */
              String tradeState = resMap.get("trade_state").toString();
      
              log.info("outTradeNo:"+outTradeNo);
              log.info("appId:"+appId);
              log.info("mchId:"+mchId);
              log.info("tradeState:"+tradeState);
              log.info("payer:"+payer);
              log.info("payer:"+attach);
              log.info("subMchId:"+subMchId);
      
              return resMap;
          }
      
          /**
           * 关闭(取消)订单
           * @param orderNo
           * @return
           */
          @ApiOperation(value = "关闭(取消)订单-统一接口", notes = "关闭(取消)订单-统一接口")
          @ApiOperationSupport(order = 20)
          @PostMapping("/closeOrder/{orderNo}")
          public void closeOrder(@PathVariable("orderNo") String orderNo) {
      
              // TODO 用于在客户下单后,不进行支付,取消订单的场景
              log.info("根据订单号取消订单,订单号: {}", orderNo);
      
              String url = String.format(WechatPayUrlEnum.CLOSE_ORDER_BY_NO.getType(), orderNo);
              url = wechatPayConfig.getBaseUrl().concat(url);
      
              // 设置参数
              Map<String, String> params = new HashMap<>(2);
              params.put("sp_mchid", wechatPayConfig.getMchId());
              params.put("sub_mchid", subMchId);
      
              String paramsStr = JSON.toJSONString(params);
              log.info("请求参数 ===> {}" + paramsStr);
      
              String res = wechatPayRequest.wechatHttpPost(url,paramsStr);
          }
      
          /**
           * 申请退款
           * @param orderNo
           */
          @ApiOperation(value = "申请退款-统一接口", notes = "申请退款-统一接口")
          @ApiOperationSupport(order = 25)
          @PostMapping("/refundOrder/{orderNo}")
          public void refundOrder(@PathVariable("orderNo") String orderNo) {
      
              log.info("根据订单号申请退款,订单号: {}", orderNo);
      
              String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.DOMESTIC_REFUNDS.getType());
      
              // 设置参数
              Map<String, Object> params = new HashMap<>(2);
              // 订单编号
              params.put("out_trade_no", orderNo);
              params.put("sub_mchid", subMchId);
              // 退款单编号 - 自定义
              int outRefundNo = new Random().nextInt(999999999);
              log.info("退款申请号:{}",outRefundNo);
              params.put("out_refund_no",outRefundNo+"");
              // 退款原因
              params.put("reason","申请退款");
              // 退款通知回调地址
              params.put("notify_url", wechatPayConfig.getRefundNotifyUrl());
      
              Map<String, Object>  amountMap =new HashMap<>();
              //退款金额,单位:分
              amountMap.put("refund", 1);
              //原订单金额,单位:分
              amountMap.put("total", 1);
              //退款币种
              amountMap.put("currency", "CNY");
              params.put("amount", amountMap);
      
              String paramsStr = JSON.toJSONString(params);
              log.info("请求参数 ===> {}" + paramsStr);
      
      
              String res = wechatPayRequest.wechatHttpPost(url,paramsStr);
      
              log.info("退款结果:{}",res);
          }
      
          /**
           * 查询单笔退款信息
           * @param refundNo
           * @return
           */
          @ApiOperation(value = "查询单笔退款信息-统一接口", notes = "查询单笔退款信息-统一接口")
          @ApiOperationSupport(order = 30)
          @GetMapping("/queryRefundOrder/{refundNo}")
          public  Map<String, Object> queryRefundOrder(@PathVariable("refundNo") String refundNo) {
      
              log.info("根据订单号查询退款订单,订单号: {}", refundNo);
      
              String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.DOMESTIC_REFUNDS_QUERY.getType().concat(refundNo))
                      .concat("?sub_mchid=").concat(subMchId);
      
              String res = wechatPayRequest.wechatHttpGet(url);
              log.info("查询退款订单结果:{}",res);
      
              Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
      
              String successTime = resMap.get("success_time").toString();
              String refundId = resMap.get("refund_id").toString();
              /**
               * 款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。
               * 枚举值:
               * SUCCESS:退款成功
               * CLOSED:退款关闭
               * PROCESSING:退款处理中
               * ABNORMAL:退款异常
               */
              String status = resMap.get("status").toString();
      
              /**
               * 枚举值:
               * ORIGINAL:原路退款
               * BALANCE:退回到余额
               * OTHER_BALANCE:原账户异常退到其他余额账户
               * OTHER_BANKCARD:原银行卡异常退到其他银行卡
               */
              String channel = resMap.get("channel").toString();
              String userReceivedAccount = resMap.get("user_received_account").toString();
      
              log.info("successTime:"+successTime);
              log.info("channel:"+channel);
              log.info("refundId:"+refundId);
              log.info("status:"+status);
              log.info("userReceivedAccount:"+userReceivedAccount);
      
              // TODO 在查询单笔退款信息时,可以再去查询一次订单的状态,保证该订单已经退款完毕了
      
              return resMap;
          }
      
          /**
           * 申请交易账单
           * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请 ,如果传入日期未为当天则会出错
           * @param billType 分为:ALL、SUCCESS、REFUND
           * ALL:返回当日所有订单信息(不含充值退款订单)
           * SUCCESS:返回当日成功支付的订单(不含充值退款订单)
           * REFUND:返回当日退款订单(不含充值退款订单)
           * @return
           */
          @ApiOperation(value = "申请交易账单-统一接口", notes = "申请交易账单-统一接口")
          @ApiOperationSupport(order = 35)
          @GetMapping("/tradeBill")
          public  String tradeBill(@RequestParam("billDate") String billDate, @RequestParam("billType")  String billType) {
      
              log.info("申请交易账单,billDate:{},billType:{}", billDate,billType);
      
              String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.TRADE_BILLS.getType())
                      .concat("?bill_date=").concat(billDate).concat("&bill_type=").concat(billType);
              // 填则默认返回服务商下的交易或退款数据,下载某个子商户下的交易或退款数据,则该字段必填
              url = url.concat("&sub_mchid=").concat(subMchId);
      
              String res = wechatPayRequest.wechatHttpGet(url);
              log.info("查询退款订单结果:{}",res);
      
              Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
      
              String downloadUrl = resMap.get("download_url").toString();
      
              return downloadUrl;
          }
      
          /**
           *
           * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
           * @param accountType 分为:BASIC、OPERATION、FEES
           * BASIC:基本账户
           * OPERATION:运营账户
           * FEES:手续费账户
           * @return
           */
          @ApiOperation(value = "申请资金账单-统一接口", notes = "申请资金账单-统一接口")
          @ApiOperationSupport(order = 40)
          @GetMapping("/fundFlowBill")
          public String fundFlowBill(@RequestParam("billDate") String billDate, @RequestParam("accountType")  String accountType) {
      
              log.info("申请交易账单,billDate:{},accountType:{}", billDate,accountType);
      
              String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.FUND_FLOW_BILLS.getType())
                      .concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType);
      
              String res = wechatPayRequest.wechatHttpGet(url);
              log.info("查询退款订单结果:{}",res);
      
              Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
      
              String downloadUrl = resMap.get("download_url").toString();
      
              return downloadUrl;
          }
      
          /**
           *
           * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
           * @param accountType 分为:BASIC、OPERATION、FEES
           * BASIC:基本账户
           * OPERATION:运营账户
           * FEES:手续费账户
           * @return
           */
          @ApiOperation(value = "申请单个子商户资金账单-统一接口", notes = "申请单个子商户资金账单-统一接口")
          @ApiOperationSupport(order = 40)
          @GetMapping("/subFundFlowBill")
          public String subMerchantFundFlowBill(@RequestParam("billDate") String billDate, @RequestParam("accountType")  String accountType) {
      
              log.info("申请单个子商户资金账单,billDate:{},accountType:{}", billDate,accountType);
      
              String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.FUND_FLOW_BILLS.getType())
                      .concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType)
                      .concat("&sub_mchid=").concat(billDate).concat("&algorithm=").concat("AEAD_AES_256_GCM");
      
              String res = wechatPayRequest.wechatHttpGet(url);
              log.info("查询退款订单结果:{}",res);
      
              Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
      
              String downloadBillCount = resMap.get("download_bill_count").toString();
              String downloadBillList = resMap.get("download_bill_list").toString();
              List<Map<String, Object>> billListMap = JSONObject.parseObject(downloadBillList, new TypeReference<List<Map<String, Object>>>(){});
              String downloadUrl = billListMap.get(0).get("download_url").toString();
      
              log.info("downloadBillCount="+downloadBillCount);
              log.info("downloadUrl="+downloadUrl);
      
              return downloadUrl;
          }
      
      
          @ApiOperation(value = "下载账单-统一接口", notes = "下载账单-统一接口")
          @ApiOperationSupport(order = 45)
          @GetMapping("/downloadBill")
          public void downloadBill(String downloadUrl) {
      
              log.info("下载账单,下载地址:{}",downloadUrl);
      
              HttpGet httpGet = new HttpGet(downloadUrl);
              httpGet.addHeader("Accept", "application/json");
      
              CloseableHttpResponse response =null;
              try {
                  //使用wxPayClient发送请求得到响应
                  response =  wxPayNoSignClient.execute(httpGet);
      
                  String body = EntityUtils.toString(response.getEntity());
      
                  int statusCode = response.getStatusLine().getStatusCode();
                  if (statusCode == 200 || statusCode == 204) {
                      log.info("下载账单,返回结果 = " + body);
                  } else {
                      throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + body);
                  }
                  // TODO 将body内容转为excel存入本地或者输出到浏览器,演示存入本地
                  writeStringToFile(body);
      
              }catch (Exception e){
                  throw new RuntimeException(e.getMessage());
              }
              finally {
                  if(response!=null) {
                      try {
                          response.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      
          private void writeStringToFile(String body) {
              FileWriter fw = null;
              try {
                  String filePath = "C:\\Users\\lhz12\\Desktop\\wxPay.txt";
                   fw = new FileWriter(filePath, true);
                  BufferedWriter  bw = new BufferedWriter(fw);
                   bw.write(body);
              } catch (Exception e) {
                  e.printStackTrace();
              }finally {
                  try {
                      if(fw!=null) {
                          fw.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182
      • 183
      • 184
      • 185
      • 186
      • 187
      • 188
      • 189
      • 190
      • 191
      • 192
      • 193
      • 194
      • 195
      • 196
      • 197
      • 198
      • 199
      • 200
      • 201
      • 202
      • 203
      • 204
      • 205
      • 206
      • 207
      • 208
      • 209
      • 210
      • 211
      • 212
      • 213
      • 214
      • 215
      • 216
      • 217
      • 218
      • 219
      • 220
      • 221
      • 222
      • 223
      • 224
      • 225
      • 226
      • 227
      • 228
      • 229
      • 230
      • 231
      • 232
      • 233
      • 234
      • 235
      • 236
      • 237
      • 238
      • 239
      • 240
      • 241
      • 242
      • 243
      • 244
      • 245
      • 246
      • 247
      • 248
      • 249
      • 250
      • 251
      • 252
      • 253
      • 254
      • 255
      • 256
      • 257
      • 258
      • 259
      • 260
      • 261
      • 262
      • 263
      • 264
      • 265
      • 266
      • 267
      • 268
      • 269
      • 270
      • 271
      • 272
      • 273
      • 274
      • 275
      • 276
      • 277
      • 278
      • 279
      • 280
      • 281
      • 282
      • 283
      • 284
      • 285
      • 286
      • 287
      • 288
      • 289
      • 290
      • 291
      • 292
      • 293
      • 294
      • 295
      • 296
      • 297
      • 298
      • 299
      • 300
      • 301
      • 302
      • 303
      • 304
      • 305
      • 306
      • 307
      • 308
      • 309
      • 310
      • 311
      • 312
      • 313
      • 314
      • 315
      • 316
      • 317
      • 318
      • 319
      • 320
      • 321
      • 322
      • 323
      • 324
      • 325
      • 326
      • 327
      • 328
      • 329
      • 330
      • 331
      • 332
      • 333
      • 334
      • 335
      • 336
      • 337
      • 338
      • 339
      • 340
      • 341
      • 342
      • 343
      • 344
      • 345
      • 346
      • 347
      • 348
      • 349
      • 350
      • 351
      • 352
      • 353
      • 354
      • 355
      • 356
      • 357
      • 358
      • 359
      • 360
      • 361
      • 362
      • 363
      • 364
      • 365
      • 366
      • 367
      • 368
      • 369
      • 370
      • 371
      • 372
      • 373
      • 374
      • 375
      • 376
      • 377
      • 378

      10、支付/退款回调通知

      NotifyController:

      
      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.JSONObject;
      import com.alibaba.fastjson.TypeReference;
      import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
      import com.lhz.demo.pay.WechatPayConfig;
      import com.lhz.demo.pay.WechatPayValidator;
      import com.lhz.demo.utils.HttpUtils;
      import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.web.bind.annotation.PostMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.util.HashMap;
      import java.util.Map;
      import java.util.concurrent.locks.ReentrantLock;
      
      
      /**
       * @Description:
       **/
      @Api(tags = "回调接口(API3)")
      @RestController
      @Slf4j
      public class NotifyController {
      
          @Resource
          private WechatPayConfig wechatPayConfig;
      
          @Resource
          private Verifier verifier;
      
          private final ReentrantLock lock = new ReentrantLock();
      
          @ApiOperation(value = "支付回调", notes = "支付回调")
          @ApiOperationSupport(order = 5)
          @PostMapping("/payNotify")
          public Map<String, String> payNotify(HttpServletRequest request, HttpServletResponse response) {
              log.info("支付回调");
              if(lock.tryLock()) {
                  // 处理通知参数
                  Map<String,Object> bodyMap = getNotifyBody(request);
                  if(bodyMap==null){
                      return falseMsg(response);
                  }
      
                  log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
                  try {
                      // 解密resource中的通知数据
                      String resource = bodyMap.get("resource").toString();
                      Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),1);
                      log.info("通知参数:{}", JSON.toJSONString(resourceMap));
                      String orderNo = resourceMap.get("out_trade_no").toString();
                      String transactionId = resourceMap.get("transaction_id").toString();
      
                      // TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
                      log.warn("=========== 根据订单号,做幂等处理 ===========");
                  } finally {
                      //要主动释放锁
                      lock.unlock();
                  }
              }
      
              //成功应答
              return trueMsg(response);
          }
      
      
          @ApiOperation(value = "退款回调", notes = "退款回调")
          @ApiOperationSupport(order = 5)
          @PostMapping("/refundNotify")
          public Map<String, String> refundNotify(HttpServletRequest request, HttpServletResponse response) {
              log.info("退款回调");
              if(lock.tryLock()) {
                  // 处理通知参数
                  Map<String,Object> bodyMap = getNotifyBody(request);
                  if(bodyMap==null){
                      return falseMsg(response);
                  }
      
                  log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
      
                  try {
                      // 解密resource中的通知数据
                      String resource = bodyMap.get("resource").toString();
                      Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),2);
                      log.info("通知参数:{}", JSON.toJSONString(resourceMap));
                      String orderNo = resourceMap.get("out_trade_no").toString();
                      String transactionId = resourceMap.get("transaction_id").toString();
      
                      // TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
      
                      log.warn("=========== 根据订单号,做幂等处理 ===========");
                  } finally {
                      //要主动释放锁
                      lock.unlock();
                  }
              }
      
              //成功应答
              return trueMsg(response);
          }
      
          private Map<String,Object> getNotifyBody(HttpServletRequest request){
              //处理通知参数
              String body = HttpUtils.readData(request);
              log.info("退款回调参数:{}",body);
      
              // 转换为Map
              Map<String, Object> bodyMap = JSONObject.parseObject(body, new TypeReference<Map<String, Object>>(){});
              // 微信的通知ID(通知的唯一ID)
              String notifyId = bodyMap.get("id").toString();
      
              // 验证签名信息
              WechatPayValidator wechatPayValidator
                      = new WechatPayValidator(verifier, notifyId, body);
              if(!wechatPayValidator.validate(request)){
      
                  log.error("通知验签失败");
                  return null;
              }
              log.info("通知验签成功");
              return bodyMap;
          }
      
          private Map<String, String> falseMsg(HttpServletResponse response){
              Map<String, String> resMap = new HashMap<>(8);
              //失败应答
              response.setStatus(500);
              resMap.put("code", "ERROR");
              resMap.put("message", "通知验签失败");
              return resMap;
          }
      
          private Map<String, String> trueMsg(HttpServletResponse response){
              Map<String, String> resMap = new HashMap<>(8);
              //成功应答
              response.setStatus(200);
              resMap.put("code", "SUCCESS");
              resMap.put("message", "成功");
              return resMap;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148

      在这里插入图片描述

    • 相关阅读:
      STC51单片机学习笔记2——汇编流水灯
      扫雷小游戏(简单详细)(内附完整代码)
      python及第三方库交叉编译
      玩转堆排序以及Topk问题——【数据结构】
      Leetcode算法题
      boundary IoU 的计算方式
      如何看待越来越多人报名参加软考?
      0915练习
      微服务与SpringCloud
      Java多线程(一)
    • 原文地址:https://blog.csdn.net/zhuocailing3390/article/details/126450642