• 微信支付APIV3统一回调接口封装(H5、JSAPI、H5、App、小程序)


      😊 @ 作者: 一恍过去
      🎊 @ 社区: Java技术栈交流
      🎉 @ 主题: 微信支付统一回调接口封装(H5、JSAPI、H5、App、小程序)
      ⏱️ @ 创作时间: 2022年07月12日

      前言

      对微信支付的H5、JSAPI、H5、App、小程序支付方式进行统一,此封装接口适用于普通商户模式支付,如果要进行服务商模式支付可以结合服务商官方API进行参数修改(未验证可行性)。

      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

      WechatPayConfig:

      
      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;
      
      /**
       * @Author:
       * @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
      • 155

      6、回调校验器

      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.*;
      
      /**
       * @Author: 
       * @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
      • 149

      7、回调Body内容处理

      HttpUtils:

      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

      6、支付/退款回调通知

      NotifyController:

      
      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;
      
      
      /**
       * @Author: 
       * @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("支付回调");
      
              // 处理通知参数
              Map<String,Object> bodyMap = getNotifyBody(request);
              if(bodyMap==null){
                  return falseMsg(response);
              }
      
              log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
              if(lock.tryLock()) {
                  try {
                      // 解密resource中的通知数据
                      String resource = bodyMap.get("resource").toString();
                      Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),1);
                      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("退款回调");
      
              // 处理通知参数
              Map<String,Object> bodyMap = getNotifyBody(request);
              if(bodyMap==null){
                  return falseMsg(response);
              }
      
              log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
              if(lock.tryLock()) {
                  try {
                      // 解密resource中的通知数据
                      String resource = bodyMap.get("resource").toString();
                      Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),2);
                      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
    • 相关阅读:
      JavaScript基础 JavaScript第一天 1. JavaScript介绍
      burp suite安装sqlmap插件
      给博客园商业化的一份公开信
      QTableWidget 表格部件
      思科设备EIGRP配置命令
      【从零开始的Java开发】1-6-1 集合排序:对整型和字符串、Comparator接口、Comparable接口
      Nova中的api
      springMVC下载文件
      .NET版Word处理控件Aspose.Words功能演示:从C#.NET中的模板生成Word文档
      ubuntu18.04安装pyaudio
    • 原文地址:https://blog.csdn.net/zhuocailing3390/article/details/125707433