微信支付 轉賬 退款

直接上乾貨

package com.yzf.mall.services.support.wxpay.service.impl;

import static com.alipay.api.internal.util.file.FileUtils.openInputStream;

import com.google.common.collect.Maps;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Security;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.Base64Utils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class WxPay {

  /**
   * 微信退款解密信息需要藉助第三方加解密工具bouncycastle,引入第三方包 https://mvnrepository.com/artifact/bouncycastle
   */
  private static BouncyCastleProvider provider;

  private static InputStream certStream;

  // 將第三方包加載到內存
  static {
    provider = new BouncyCastleProvider();
    Security.addProvider(provider);

    try {
      certStream = openInputStream(new File("微信商戶號p12證書路徑"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }


  // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓微信支付配置化參數↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
  private String appId = "";
  private String appSecret = "";
  /**
   * 商戶api
   */
  private String apiSecret = "商戶號的密鑰";
  private String mchId = "商戶號ID";
  /**
   * 商品簡單描述
   */
  private String body = "支付描述";
  /**
   * 交易類型
   */
  private String signType = "MD5";
  private String feeType = "CNY";
  private String sceneInfo = "{\"h5_info\":{\"type\":\"online pay\",\"wap_url\":\"www.songjingzhou.com\",\"wap_name\":\"songjingzhou\"}}";
  private String payNotifyUrl = "https://wxnotify.songjingzhou.com/api/notify/wxPayNotify";
  private String refundNotifyUrl = "https://wxnotify.songjingzhou.com/api/notify/wxRefundNotify";
  private String spbillCreateIp;
  private String transferUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑微信支付配置化參數↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

  private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

  public static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
  public static final String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
  private static final int CONNECT_TIME_OUT = 8000;
  private static final int READ_TIME_OUT = 10000;


  public Map<String, String> getWxPayResponse(PayInfo payInfo) throws Exception {
    HashMap<String, String> param = Maps.newHashMap();
    param.put("body", body); // 描述
    param.put("openid", payInfo.getOpenId());
    param.put("out_trade_no", payInfo.getOid()); // 系統內部訂單號,最好加前綴予以區分,否則公衆號和小程序的訂單無法區分
    param.put("total_fee", String.valueOf(payInfo.getTotalFee()));
    param.put("spbill_create_ip", spbillCreateIp);
    param.put("trade_type", "JSAPI");
    param.put("scene_info", sceneInfo);
    param.put("notify_url", payNotifyUrl);

    try {
      Map<String, String> requestMap = fillRequestData(param);

      String respXml = requestWithoutCert(UNIFIED_ORDER_URL, requestMap, CONNECT_TIME_OUT,
        READ_TIME_OUT);
      Map<String, String> res = this.processResponseXml(respXml);
      // 上一步得到的結果集需要再次簽名後給前端使用

      return _reSign4Front(res.get("nonce_str"), res.get("prepay_id"));
    } catch (Exception e) {
      throw e;
    }
  }

  private Map<String, String> _reSign4Front(String nonceStr, String prePayId)
    throws Exception {
    Map<String, String> param = Maps.newHashMap();
    param.put("appId", appId); // 商戶賬號appid
    String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
    param.put("timeStamp", timeStamp);
    param.put("nonceStr", nonceStr);
    param.put("package", "prepay_id=" + prePayId);
    param.put("signType", "MD5");
    String paySign = generateSignature(param, apiSecret, "MD5");

    Map<String, String> result = Maps.newHashMap();
    result.put("appId", appId);
    result.put("timeStamp", timeStamp);
    result.put("nonceStr", nonceStr);
    result.put("prepayId", "prepay_id=" + prePayId); // 注意這不是手誤
    result.put("signType", "MD5");
    result.put("paySign", paySign);

    return result;
  }

  /**
   * 微信支付異步通知,可以進行白名單限制
   */
  public String wxPayNotify(String xmlStr) throws Exception {
    try {
      Map<String, String> map = processResponseXml(xmlStr);

      // TODO 業務處理
    } catch (Exception e) {
      throw e;
    }

    return _setWxReturnXML();
  }

  private String _setWxReturnXML() {
    return "<xml><return_code><![CDATA[" + "SUCCESS"
      + "]]></return_code><return_msg><![CDATA[" + "OK"
      + "]]></return_msg></xml>";
  }

  /**
   * 微信退款
   */
  public void wxPayRefund(WxRefundRequest request) throws Exception {
    try {
      Map<String, String> param = Maps.newHashMap();
      param.put("transaction_id", request.getTransactionId());
      param.put("out_refund_no", request.getPayRefundId());
      param.put("total_fee", String.valueOf(request.getTotalFee()));
      param.put("refund_fee", String.valueOf(request.getRefundFee()));
      param.put("refund_fee_type", feeType);
      param.put("refund_desc", request.getRefundDesc());
      param.put("notify_url", payNotifyUrl);

      Map<String, String> requestMap = fillRequestData(param);
      String xml = requestWithCert(REFUND_URL, requestMap, CONNECT_TIME_OUT, READ_TIME_OUT);
      Map<String, String> responseMap = processResponseXml(xml);

      // TODO 業務處理
    } catch (Exception e) {
      throw e;
    }
  }


  public String wxRefundNotify(String xmlStr) throws Exception {
    Map<String, String> map;
    try {
      map = xmlToMap(xmlStr);
    } catch (Exception e) {
      throw e;
    }
    String reqInfo = map.get("req_info");

    Map<String, String> notify = _decodeRefundNotify(reqInfo);

    if ("SUCCESS".equals(notify.get("refund_status"))) {
      // 業務主鍵
      String id = notify.get("out_refund_no");
      // TODO 業務邏輯
    }

    return _setWxReturnXML();
  }

  /**
   * 退款解密在windows上沒問題,但是在linux環境中會出現 JCE cannot authenticate the provider BC
   *
   * JDK 8 注意 linux需要 在jdk1.8.0_171/jre/lib/ext 下添加 bcprov-jdk15on-1.62.jar 同時修改
   * jdk1.8.0_171/jre/lib/security文件添加 security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider
   *
   * JDK 11 沒有jre目錄,不能按照上面的方式處理,而亞馬遜版的jdk11 則不需要做任何修改可以直接使用
   */
  private Map<String, String> _decodeRefundNotify(String reqInfo) throws Exception {
    String result;
    try {
      String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
      String keyMd5Code = DigestUtils.md5Hex(apiSecret).toLowerCase();
      SecretKeySpec key = new SecretKeySpec(keyMd5Code.getBytes(), "AES");
      Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
      cipher.init(Cipher.DECRYPT_MODE, key);
      byte[] decryptContent = cipher.doFinal(Base64Utils.decodeFromString(reqInfo));
      result = new String(decryptContent, StandardCharsets.UTF_8);

      Map<String, String> map = xmlToMap(result);
      return map;
    } catch (Throwable t) {
      throw t;
    }
  }

  /**
   * 微信轉賬
   */
  public void wxTransfer(String partnerTradeId, String openId, String amount, String node)
    throws Exception {

    Map<String, String> param = Maps.newHashMap();
    param.put("mch_appid", appId);
    param.put("mchid", mchId);
    param.put("partner_trade_no", partnerTradeId);
    param.put("openid", openId);
    param.put("check_name", "NO_CHECK");
    param.put("amount", amount);
    param.put("desc", node);

    Map<String, String> requestMap = _sign4Transfer(param);
    String response = requestWithCert(transferUrl, requestMap, 8000, 10000);

    try {
      Map<String, String> map = xmlToMap(response);

      // TODO 業務
    } catch (Throwable th) {
    }
  }

  /**
   * 微信轉賬簽名
   */
  private Map<String, String> _sign4Transfer(Map<String, String> reqData) throws Exception {
    reqData.put("nonce_str", generateNonceStr());

    reqData.put("sign", generateSignature(reqData, apiSecret, signType));
    return reqData;
  }

  /**
   * 生成簽名. 注意,若含有sign_type字段,必須和signType參數保持一致。
   *
   * @param data 待簽名數據
   * @param key API密鑰
   * @param signType 簽名方式
   * @return 簽名
   */
  public static String generateSignature(Map<String, String> data, String key,
    String signType) throws Exception {
    Set<String> keySet = data.keySet();
    String[] keyArray = keySet.toArray(new String[keySet.size()]);
    Arrays.sort(keyArray);
    StringBuilder sb = new StringBuilder();
    for (String k : keyArray) {
      if (k.equals("sign")) {
        continue;
      }
      if (data.get(k).trim().length() > 0) // 參數值爲空,則不參與簽名
      {
        sb.append(k).append("=").append(data.get(k).trim()).append("&");
      }
    }
    sb.append("key=").append(key);
    if ("MD5".equals(signType)) {
      return MD5(sb.toString()).toUpperCase();
    } else if ("HMACSHA256".equals(signType)) {
      return HMACSHA256(sb.toString(), key);
    } else {
      throw new Exception(String.format("Invalid sign_type: %s", signType));
    }
  }

  /**
   * 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br> 該函數適用於商戶適用於統一下單等接口,不適用於紅包、代金券接口
   */
  public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
    reqData.put("appid", appId);
    reqData.put("mch_id", mchId);
    reqData.put("nonce_str", generateNonceStr());
    if ("MD5".equals(this.signType)) {
      reqData.put("sign_type", "MD5");
    } else if ("HMACSHA256".equals(this.signType)) {
      reqData.put("sign_type", "HMACSHA256");
    }
    reqData.put("sign", generateSignature(reqData, apiSecret, signType));
    return reqData;
  }

  /**
   * 獲取隨機字符串 Nonce Str
   *
   * @return String 隨機字符串
   */
  public static String generateNonceStr() {
    return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
  }

  /**
   * XML格式字符串轉換爲Map
   *
   * @param strXML XML字符串
   * @return XML數據轉換後的Map
   */
  public static Map<String, String> xmlToMap(String strXML) throws Exception {
    Map<String, String> data = new HashMap<String, String>();
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
    InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
    org.w3c.dom.Document doc = documentBuilder.parse(stream);
    doc.getDocumentElement().normalize();
    NodeList nodeList = doc.getDocumentElement().getChildNodes();
    for (int idx = 0; idx < nodeList.getLength(); ++idx) {
      Node node = nodeList.item(idx);
      if (node.getNodeType() == Node.ELEMENT_NODE) {
        org.w3c.dom.Element element = (org.w3c.dom.Element) node;
        data.put(element.getNodeName(), element.getTextContent());
      }
    }
    try {
      stream.close();
    } catch (Exception ex) {

    }
    return data;
  }

  /**
   * 不需要證書的請求
   *
   * @param url String
   * @param reqData 向wxpay post的請求數據
   * @param connectTimeoutMs 超時時間,單位是毫秒
   * @param readTimeoutMs 超時時間,單位是毫秒
   * @return API返回數據
   */
  public String requestWithoutCert(String url, Map<String, String> reqData,
    int connectTimeoutMs, int readTimeoutMs) throws Exception {
    String UTF8 = "UTF-8";
    String reqBody = mapToXml(reqData);
    URL httpUrl = new URL(url);
    HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
    httpURLConnection.setDoOutput(true);
    httpURLConnection.setRequestMethod("POST");
    httpURLConnection.setConnectTimeout(connectTimeoutMs);
    httpURLConnection.setReadTimeout(readTimeoutMs);
    httpURLConnection.connect();
    OutputStream outputStream = httpURLConnection.getOutputStream();
    outputStream.write(reqBody.getBytes(UTF8));

    //獲取內容
    InputStream inputStream = httpURLConnection.getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF8));
    final StringBuffer stringBuffer = new StringBuffer();
    String line = null;
    while ((line = bufferedReader.readLine()) != null) {
      stringBuffer.append(line);
    }
    String resp = stringBuffer.toString();
    if (stringBuffer != null) {
      try {
        bufferedReader.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    if (inputStream != null) {
      try {
        inputStream.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    if (outputStream != null) {
      try {
        outputStream.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    return resp;
  }

  /**
   * 需要證書的請求
   *
   * @param url String
   * @param reqData 向wxpay post的請求數據  Map
   * @param connectTimeoutMs 超時時間,單位是毫秒
   * @param readTimeoutMs 超時時間,單位是毫秒
   * @return API返回數據
   */
  public String requestWithCert(String url, Map<String, String> reqData,
    int connectTimeoutMs, int readTimeoutMs) throws Exception {
    String UTF8 = "UTF-8";
    String reqBody = mapToXml(reqData);
    URL httpUrl = new URL(url);
    char[] password = mchId.toCharArray();
    KeyStore ks = KeyStore.getInstance("PKCS12");
    ks.load(certStream, password);

    // 實例化密鑰庫 & 初始化密鑰工廠
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(ks, password);

    // 創建SSLContext
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
    HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

    HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();

    httpURLConnection.setDoOutput(true);
    httpURLConnection.setRequestMethod("POST");
    httpURLConnection.setConnectTimeout(connectTimeoutMs);
    httpURLConnection.setReadTimeout(readTimeoutMs);
    httpURLConnection.connect();
    OutputStream outputStream = httpURLConnection.getOutputStream();
    outputStream.write(reqBody.getBytes(UTF8));

    //獲取內容
    InputStream inputStream = httpURLConnection.getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF8));
    final StringBuffer stringBuffer = new StringBuffer();
    String line = null;
    while ((line = bufferedReader.readLine()) != null) {
      stringBuffer.append(line);
    }
    String resp = stringBuffer.toString();
    if (stringBuffer != null) {
      try {
        bufferedReader.close();
      } catch (IOException e) {
        // e.printStackTrace();
      }
    }
    if (inputStream != null) {
      try {
        inputStream.close();
      } catch (IOException e) {
        // e.printStackTrace();
      }
    }
    if (outputStream != null) {
      try {
        outputStream.close();
      } catch (IOException e) {
        // e.printStackTrace();
      }
    }
    if (certStream != null) {
      try {
        certStream.close();
      } catch (IOException e) {
        // e.printStackTrace();
      }
    }

    return resp;
  }

  /**
   * 處理 HTTPS API返回數據,轉換成Map對象。return_code爲SUCCESS時,驗證簽名。
   *
   * @param xmlStr API返回的XML格式數據
   * @return Map類型數據
   */
  public Map<String, String> processResponseXml(String xmlStr) throws Exception {
    String RETURN_CODE = "return_code";
    String return_code;
    Map<String, String> respData = xmlToMap(xmlStr);
    if (respData.containsKey(RETURN_CODE)) {
      return_code = respData.get(RETURN_CODE);
    } else {
      throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
    }

    if (return_code.equals("FAIL")) {
      return respData;
    } else if (return_code.equals("SUCCESS")) {
      if (this.isSignatureValid(respData, apiSecret, "MD5")) {
        return respData;
      } else {
        throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
      }
    } else {
      throw new Exception(
        String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
    }
  }

  /**
   * 判斷簽名是否正確,必須包含sign字段,否則返回false。
   *
   * @param data Map類型數據
   * @param key API密鑰
   * @param signType 簽名方式
   * @return 簽名是否正確
   */
  public static boolean isSignatureValid(final Map<String, String> data, String key,
    String signType)
    throws Exception {

    String sign = data.get("sign");
    return generateSignature(data, key, signType).equals(sign);
  }

  /**
   * 生成 MD5
   *
   * @param data 待處理數據
   * @return MD5結果
   */
  public static String MD5(String data) throws Exception {
    java.security.MessageDigest md = MessageDigest.getInstance("MD5");
    byte[] array = md.digest(data.getBytes("UTF-8"));
    StringBuilder sb = new StringBuilder();
    for (byte item : array) {
      sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
    }
    return sb.toString().toUpperCase();
  }

  /**
   * 生成 HMACSHA256
   *
   * @param data 待處理數據
   * @param key 密鑰
   * @return 加密結果
   */
  public static String HMACSHA256(String data, String key) throws Exception {
    Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
    SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
    sha256_HMAC.init(secret_key);
    byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
    StringBuilder sb = new StringBuilder();
    for (byte item : array) {
      sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
    }
    return sb.toString().toUpperCase();
  }

  /**
   * 將Map轉換爲XML格式的字符串
   *
   * @param data Map類型數據
   * @return XML格式的字符串
   */
  public static String mapToXml(Map<String, String> data) throws Exception {
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
    org.w3c.dom.Document document = documentBuilder.newDocument();
    org.w3c.dom.Element root = document.createElement("xml");
    document.appendChild(root);
    for (String key : data.keySet()) {
      String value = data.get(key);
      if (value == null) {
        value = "";
      }
      value = value.trim();
      org.w3c.dom.Element filed = document.createElement(key);
      filed.appendChild(document.createTextNode(value));
      root.appendChild(filed);
    }
    TransformerFactory tf = TransformerFactory.newInstance();
    Transformer transformer = tf.newTransformer();
    DOMSource source = new DOMSource(document);
    transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    StringWriter writer = new StringWriter();
    StreamResult result = new StreamResult(writer);
    transformer.transform(source, result);
    String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
    try {
      writer.close();
    } catch (Exception ex) {
    }
    return output;
  }
}

class PayInfo {

  private String openId;
  /**
   * 系統內部訂單號
   */
  private String oid;
  /**
   * 總費用
   */
  private int totalFee;

  public String getOpenId() {
    return openId;
  }

  public void setOpenId(String openId) {
    this.openId = openId;
  }

  public String getOid() {
    return oid;
  }

  public void setOid(String oid) {
    this.oid = oid;
  }

  public int getTotalFee() {
    return totalFee;
  }

  public void setTotalFee(int totalFee) {
    this.totalFee = totalFee;
  }
}

class WxRefundRequest {

  /**
   * 微信訂單號
   */
  private String transactionId;
  /**
   * 系統內部退款單號
   */
  private String payRefundId;
  private Integer totalFee;
  private Integer refundFee;
  private String refundDesc;

  public String getTransactionId() {
    return transactionId;
  }

  public void setTransactionId(String transactionId) {
    this.transactionId = transactionId;
  }

  public String getPayRefundId() {
    return payRefundId;
  }

  public void setPayRefundId(String payRefundId) {
    this.payRefundId = payRefundId;
  }

  public Integer getTotalFee() {
    return totalFee;
  }

  public void setTotalFee(Integer totalFee) {
    this.totalFee = totalFee;
  }

  public Integer getRefundFee() {
    return refundFee;
  }

  public void setRefundFee(Integer refundFee) {
    this.refundFee = refundFee;
  }

  public String getRefundDesc() {
    return refundDesc;
  }

  public void setRefundDesc(String refundDesc) {
    this.refundDesc = refundDesc;
  }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章