微信支付-v3版本

1 篇文章 0 订阅

最近整理一下关于微信支付的内容

文档说明

代码

文件目录示意图
  • 目录示意图
代码段—小程序(其他的类似)
  • AesUtil --密钥加解密
 
  /**
   * @program:  
   * @author: zzg
   * @create: 2021-07-27 10:30
   */
  import cn.hutool.core.codec.Base64;
  
  import javax.crypto.Cipher;
  import javax.crypto.NoSuchPaddingException;
  import javax.crypto.spec.GCMParameterSpec;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.GeneralSecurityException;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.NoSuchAlgorithmException;
  
  /**
   * 〈一句话功能简述〉<br>
   * 〈〉
   *
   * @author Gym
   * @create 2020/12/18
   * @since 1.0.0
   */
  public class AesUtil {
      static final int KEY_LENGTH_BYTE = 32;
      static final int TAG_LENGTH_BIT = 128;
      private final byte[] aesKey;
  
      /**
       * @param key APIv3 密钥
       */
      public AesUtil(byte[] key) {
          if (key.length != KEY_LENGTH_BYTE) {
              throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
          }
          this.aesKey = key;
      }
  
      /**
       * 证书和回调报文解密
       *
       * @param associatedData associated_data
       * @param nonce          nonce
       * @param cipherText     ciphertext
       * @return {String} 平台证书明文
       * @throws GeneralSecurityException 异常
       */
      public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException {
          try {
              Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
              SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
              GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
              cipher.init(Cipher.DECRYPT_MODE, key, spec);
              cipher.updateAAD(associatedData);
              return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8);
          } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
              throw new IllegalStateException(e);
          } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
              throw new IllegalArgumentException(e);
          }
      }
  }
  • Pay–支付工具类

  import cn.hutool.core.util.StrUtil;
  import cn.hutool.http.ContentType;
  import cn.hutool.json.JSONUtil;
  import com.alibaba.fastjson.JSONObject;
  import okhttp3.HttpUrl;
  import org.apache.http.HttpEntity;
  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.impl.client.HttpClients;
  import org.apache.http.message.BasicHeader;
  import org.apache.http.protocol.HTTP;
  import org.apache.http.util.EntityUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.security.KeyFactory;
  import java.security.NoSuchAlgorithmException;
  import java.security.PrivateKey;
  import java.security.Signature;
  import java.security.spec.InvalidKeySpecException;
  import java.security.spec.PKCS8EncodedKeySpec;
  import java.util.*;
  
  /**
   * @program: 
   * @author: zzg
   * @create: 2021-07-27 10:31
   */
  public class Pay {
      //调试日志
      private final static Logger logger = LoggerFactory.getLogger(Pay.class);
  
  
      //请求网关
      private static final String url_prex = "https://api.mch.weixin.qq.com/";
      //编码
      private static final String charset = "UTF-8";
      /**
       * 微信支付下单
       *
       * @param url                请求地址(只需传入域名之后的路由地址)
       * @param jsonStr            请求体 json字符串 此参数与微信官方文档一致
       * @param mercId             商户ID
       * @param serial_no          证书序列号
       * @param privateKeyFilePath 私钥的路径
       * @return 订单支付的参数
       * @throws Exception
       */
      public static JSONObject  V3PayPost(String url, String jsonStr, String mercId, String serial_no, String privateKeyFilePath) throws Exception {
          String body = "";
          //创建httpclient对象
          CloseableHttpClient client = HttpClients.createDefault();
          //创建post方式请求对象
          HttpPost httpPost = new HttpPost(url_prex + url);
          //装填参数
          StringEntity s = new StringEntity(jsonStr, charset);
          s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
                  "application/json"));
          //设置参数到请求对象中
          httpPost.setEntity(s);
          String post = getToken("POST", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, jsonStr);
          //设置header信息
          //指定报文头【Content-type】、【User-Agent】
          httpPost.setHeader("Content-type", "application/json");
          httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
          httpPost.setHeader("Accept", "application/json");
          httpPost.setHeader("Authorization",
                  "WECHATPAY2-SHA256-RSA2048 " + post);
          //执行请求操作,并拿到结果(同步阻塞)
          CloseableHttpResponse response = client.execute(httpPost);
          //获取结果实体
          HttpEntity entity = response.getEntity();
          if (entity != null) {
              //按指定编码转换结果实体为String类型
              body = EntityUtils.toString(entity, charset);
          }
          EntityUtils.consume(entity);
          //释放链接
          response.close();
          logger.info("请求地址:{},请求参数:{}。返回:{}",url,jsonStr,body);
          JSONObject jsonObject = JSONObject.parseObject(body);
          return jsonObject;
  
      }
  
  
  
      /**
       * 微信支付下单
       *
       * @param url                请求地址(只需传入域名之后的路由地址)
       * @param mercId             商户ID
       * @param serial_no          证书序列号
       * @param privateKeyFilePath 私钥的路径
       * @return 订单支付的参数
       * @throws Exception
       */
      public static JSONObject  V3PayGet(String url,String mercId, String serial_no, String privateKeyFilePath) throws Exception {
          String body = "";
          //创建httpclient对象
          CloseableHttpClient client = HttpClients.createDefault();
          //创建post方式请求对象
          HttpGet httpGet = new HttpGet(url_prex + url);
  
  
          String post = getToken("GET", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, "");
          //设置header信息
          //指定报文头【Content-type】、【User-Agent】
          httpGet.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
          httpGet.setHeader("Accept", "application/json");
          httpGet.setHeader("Authorization",
                  "WECHATPAY2-SHA256-RSA2048 " + post);
          //执行请求操作,并拿到结果(同步阻塞)
          CloseableHttpResponse response = client.execute(httpGet);
          //获取结果实体
          HttpEntity entity = response.getEntity();
          if (entity != null) {
              //按指定编码转换结果实体为String类型
              body = EntityUtils.toString(entity, charset);
          }
          EntityUtils.consume(entity);
          //释放链接
          response.close();
          logger.info("请求地址:{}。返回:{}",url,body);
          JSONObject jsonObject = JSONObject.parseObject(body);
          return jsonObject;
      }
  
  
  
      /**
       * 生成组装请求头
       *
       * @param method             请求方式
       * @param url                请求地址
       * @param mercId             商户ID
       * @param serial_no          证书序列号
       * @param privateKeyFilePath 私钥路径
       * @param body               请求体
       * @return 组装请求的数据
       * @throws Exception
       */
      static String getToken(String method, HttpUrl url, String mercId, String serial_no, String privateKeyFilePath, String body) throws Exception {
          String nonceStr = UUID.randomUUID().toString().replace("-", "");
          long timestamp = System.currentTimeMillis() / 1000;
          String message = buildMessage(method, url, timestamp, nonceStr, body);
          String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath);
          return "mchid=\"" + mercId + "\","
                  + "nonce_str=\"" + nonceStr + "\","
                  + "timestamp=\"" + timestamp + "\","
                  + "serial_no=\"" + serial_no + "\","
                  + "signature=\"" + signature + "\"";
      }
  
  
      /**
       * 微信调起支付参数
       * 返回参数如有不理解 请访问微信官方文档
       * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
       *
       * @param prepayId           微信下单返回的prepay_id
       * @param appId              应用ID(appid)
       * @param privateKeyFilePath 私钥的地址
       * @return 当前调起支付所需的参数
       * @throws Exception
       */
      public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception {
          String time = System.currentTimeMillis() / 1000 + "";
          String nonceStr = UUID.randomUUID().toString().replace("-", "");
          String packageStr = "prepay_id=" + prepayId;
          ArrayList<String> list = new ArrayList<>();
          list.add(appId);
          list.add(time);
          list.add(nonceStr);
          list.add(packageStr);
          //加载签名
          String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath);
          JSONObject jsonObject = new JSONObject();
          jsonObject.put("appid", appId);
          jsonObject.put("timeStamp", time);
          jsonObject.put("nonceStr", nonceStr);
          jsonObject.put("packages", packageStr);
          jsonObject.put("signType", "RSA");
          jsonObject.put("paySign", packageSign);
          return jsonObject;
      }
  
  
      /**
       * 处理微信异步回调
       *
       * @param request
       * @param response
       * @param privateKey 32的秘钥
       */
      public static JSONObject notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception {
          Map<String, String> map = new HashMap<>(12);
          String result = readData(request);
          // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
          String plainText = verifyNotify(result, privateKey);
          System.out.println("plainText:"+plainText);
          if (StrUtil.isNotEmpty(plainText)) {
              response.setStatus(200);
              map.put("code", "SUCCESS");
              map.put("message", "SUCCESS");
          } else {
              response.setStatus(500);
              map.put("code", "ERROR");
              map.put("message", "签名错误");
          }
          response.setHeader("Content-type", ContentType.JSON.toString());
          response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
          response.flushBuffer();
          JSONObject jsonObject = JSONObject.parseObject(plainText);
  
          return jsonObject;
      }
  
      /**
       * 生成签名
       *
       * @param message            请求体
       * @param privateKeyFilePath 私钥的路径
       * @return 生成base64位签名信息
       * @throws Exception
       */
      static String sign(byte[] message, String privateKeyFilePath) throws Exception {
          Signature sign = Signature.getInstance("SHA256withRSA");
          sign.initSign(getPrivateKey(privateKeyFilePath));
          sign.update(message);
          return Base64.getEncoder().encodeToString(sign.sign());
      }
  
      /**
       * 组装签名加载
       *
       * @param method    请求方式
       * @param url       请求地址
       * @param timestamp 请求时间
       * @param nonceStr  请求随机字符串
       * @param body      请求体
       * @return 组装的字符串
       */
      static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
          String canonicalUrl = url.encodedPath();
          if (url.encodedQuery() != null) {
              canonicalUrl += "?" + url.encodedQuery();
          }
          return method + "\n"
                  + canonicalUrl + "\n"
                  + timestamp + "\n"
                  + nonceStr + "\n"
                  + body + "\n";
      }
  
      /**
       * 获取私钥。
       *
       * @param filename 私钥文件路径  (required)
       * @return 私钥对象
       */
      static PrivateKey getPrivateKey(String filename) throws IOException {
          String content = new String(Files.readAllBytes(Paths.get(filename)), "UTF-8");
  //        String content = new String(Files.readAllBytes(new ClassPathResource(filename).getFile().toPath()), "UTF-8");
          try {
              String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                      .replace("-----END PRIVATE KEY-----", "")
                      .replaceAll("\\s+", "");
              KeyFactory kf = KeyFactory.getInstance("RSA");
              return kf.generatePrivate(
                      new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
          } catch (NoSuchAlgorithmException e) {
              throw new RuntimeException("当前Java环境不支持RSA", e);
          } catch (InvalidKeySpecException e) {
              throw new RuntimeException("无效的密钥格式");
          }
      }
  
      /**
       * 构造签名串
       *
       * @param signMessage 待签名的参数
       * @return 构造后带待签名串
       */
      static String buildSignMessage(ArrayList<String> signMessage) {
          if (signMessage == null || signMessage.size() <= 0) {
              return null;
          }
          StringBuilder sbf = new StringBuilder();
          for (String str : signMessage) {
              sbf.append(str).append("\n");
          }
          return sbf.toString();
      }
  
      /**
       * v3 支付异步通知验证签名
       *
       * @param body 异步通知密文
       * @param key  api 密钥
       * @return 异步通知明文
       * @throws Exception 异常信息
       */
      static String verifyNotify(String body, String key) throws Exception {
          // 获取平台证书序列号
          cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body);
          cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource");
          String cipherText = resource.getStr("ciphertext");
          String nonceStr = resource.getStr("nonce");
          String associatedData = resource.getStr("associated_data");
          AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
          // 密文解密
          return aesUtil.decryptToString(
                  associatedData.getBytes(StandardCharsets.UTF_8),
                  nonceStr.getBytes(StandardCharsets.UTF_8),
                  cipherText
          );
      }
  
  
      /**
       * 处理返回对象
       *
       * @param request
       * @return
       */
      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();
                  }
              }
          }
      }
  }

  • MyWxPayConfig—配置

  import lombok.Data;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.stereotype.Component;
  
  /**
   * @program:  
   * @author: zzg
   * @create: 2021-07-26 20:25
   */
  @Component
  @Data
  public class MyWxPayConfig  {
  
      /**
       * 设置微信公众号或者小程序等的appid
       */
      @Value("${wx.appid}")
      private String appId;
  
      /**
       * 微信支付园区商家号
       */
  
      @Value("${wx.mchId}")
      private String mchId;
  
      /**
       * 微信支付园区商家密钥
       */
      @Value("${wx.mchKey}")
      private String mchKey;
  
  
      /**
       * apiclient_cert.p12文件的绝对路径
       */
      @Value("${wx.keyPath}")
      private String keyPath;
  
      /**
       * apiclient_key.pem文件的绝对路径
       */
      @Value("${wx.privateKey}")
      private String privateKey;
  
      @Value("${wx.notifyUrl}")
      private String notifyUrl;
  
  
  
      //商户API证书序列号serial_no
      @Value("${wx.serialNo}")
      private String serialNo;
  
  
  }

使用
package com.example.demo;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.pay.wx.v3.MyWxPayConfig;
import com.example.demo.pay.wx.v3.Pay;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * 微信支付test
 *
 * @program: demo
 * @author: zzg
 * @create: 2022-02-17 15:15
 */

@SpringBootTest
public class WxpayTest {

    @Autowired
    private MyWxPayConfig myWxPayConfig;

    /**
     *
     *  1.获取预支付订单
     *  2,生成签名支付信息
     *  3.手机端调起支付
     *
     * @author zzg
     * @date 2022/2/16
     * @throws
     */
    @Test
    public void wxPayV3(){
        String orderNo = String.valueOf(System.currentTimeMillis());

        try {
            //获取预支付订单
            String prepayId = wxPayv3("测试", orderNo, 1, "oCGNQ5M9wji19ytIvddaRt2ing8NKws");
            //生成签名支付信息
            JSONObject object = Pay.WxTuneUp(prepayId,myWxPayConfig.getAppId(), myWxPayConfig.getPrivateKey());
            object.put("orderNo",orderNo);
            System.out.println(object);
        } catch (Exception e) {
            e.printStackTrace();
        }



    }

    /**
     *
     * @param description 购买描述
     * @param outTradeNo 商户订单
     * @param total  金额(分)
     * @param openId  微信用户openId
     * @author zzg
     * @date 2022/2/16
     * @return java.lang.String
     * @throws
     */
    public String wxPayv3(String description,String outTradeNo,Integer total,String openId) throws Exception {

        //支付的请求参数信息(此参数与微信支付文档一致,文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml)
        Map<String, Object> data = new HashMap();
        data.put("description",description);
        data.put("out_trade_no", outTradeNo);
        data.put("notify_url", myWxPayConfig.getNotifyUrl());

        HashMap<String, Object> map = new HashMap<>();
        map.put("total",total);
        map.put("currency","CNY");

        data.put("amount", map);

        HashMap<String, Object> map1 = new HashMap<>();
        map1.put("openid",openId);
        data.put("payer",map1);
        data.put("appid",myWxPayConfig.getAppId());
        data.put("mchid",myWxPayConfig.getMchId());



        String wxPayRequestJsonStr = JSONUtil.toJsonStr(data);
        //第一步获取prepay_id
        com.alibaba.fastjson.JSONObject prepayId = Pay.V3PayPost("v3/pay/transactions/jsapi", wxPayRequestJsonStr, myWxPayConfig.getMchId(), myWxPayConfig.getSerialNo(), myWxPayConfig.getPrivateKey());
        if(StringUtils.isNotBlank(prepayId.getString("prepay_id"))){
            return prepayId.getString("prepay_id");
        }else{

            System.out.println("支付异常");
        }
        return "";
    }




    //支付回调
    /**
     * @PostMapping(value = "/wxnoty")
     *     public void wxnoty(HttpServletRequest request, HttpServletResponse response) throws Exception {
     *         wxnoty(request,response);
     *     }
     */

     //回调
    public void wxnoty(HttpServletRequest request, HttpServletResponse response) {

        JSONObject notify=new  JSONObject();
        Integer dealstatus=0;
        try {
            notify = Pay.notify(request, response, myWxPayConfig.getMchKey());

        }catch (Exception e){

        }finally {
          //做其他事情

        }

    }


    //主动查询付款情况,根据商户的订单号查询
    public void searchOrder() {
        //自己的订单号
        String orderNo="";
        try {
            JSONObject jsonObject = Pay.V3PayGet("v3/pay/transactions/out-trade-no/" + orderNo + "?mchid=" + myWxPayConfig.getMchId()  , myWxPayConfig.getMchId(), myWxPayConfig.getSerialNo(), myWxPayConfig.getPrivateKey());
            //处理其他事件,如:订单支付状态修改
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

    //退款
    /**
     *  一年内的订单退款,超过请使用 资金应用-付款  功能
     * @author zzg
     * @date 2022/2/17
     * @throws
     */
    @Test
    public void refund(){

        Map<String, Object> data = new HashMap();

        //微信支付交易号
        data.put("transaction_id","");
        //自己定义的退款单号
        data.put("out_refund_no", "");

        data.put("reason","押金退还");

        //退款回调地址
        data.put("notify_url","");

        HashMap<String, Object> map = new HashMap<>();
        //退款金额,单位:分
        map.put("refund",100);
        //本次退款交易号的全部支付金额,单位:分
        map.put("total",100);
        map.put("currency","CNY");

        data.put("amount", map);
        String wxPayRequestJsonStr = JSONUtil.toJsonStr(data);

        JSONObject object = null;
        try {
            object = Pay.V3PayPost("v3/refund/domestic/refunds", wxPayRequestJsonStr, myWxPayConfig.getMchId(), myWxPayConfig.getSerialNo(), myWxPayConfig.getPrivateKey());

            System.out.println(object);
        } catch (Exception e) {
            e.printStackTrace();
        }


    }


}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值