Java - 微信支付

首先貼出官方文檔,關於介紹,場景,參數說明,可以直接看文檔:https://pay.weixin.qq.com/wiki/doc/api/index.html

一. APP支付

官方文檔:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1

   推薦maven依賴:

<!--微信支付sdk -->
<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>3.0.9</version>
</dependency>

1. 【統一下單】

商戶系統先調用該接口在微信支付服務後臺生成預支付交易單,返回正確的預支付交易會話標識後再在APP裏面調起支付。

   微信統一工具類

package com.maiji.cloud.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.*;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import com.google.common.collect.Maps;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;

public class WXUtil {

    public static final String APP_ID = "";
    
    public static final String APPLET_APP_ID = "";
    
    public static final String MCH_ID = ""; // 微信商戶號
   
    public static final String KEY = "";  // 微信支付密鑰
    
    public static final String NOTIFY_URL = ""; // 微信支付成功回調url
  
    public static final String REFUND_NOTIFY_URL = "";   // 微信退款成功回調

    public static final String SIGN_PACKAGE = "Sign=WXPay";

    private static Cipher cipher = null;  //解碼器


    public static String createSign(Map parameters) throws Exception {
        List<String> keys = new ArrayList<String>(parameters.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
        String key = keys.get(i);
        Object value = parameters.get(key);
//        value = URLEncoder.encode(value, "UTF-8");
        if(StringUtils.isNotBlank(parameters.get(key)+"") && value != null) {
        	if (i == keys.size() - 1) {//拼接時,不包括最後一個&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }	
        }
        }
        prestr += "&key="+KEY;
        return prestr;
    }
    
    public static Map<String, Object> objectToMap(Object obj) throws Exception { 
    	if(obj == null){ 
    	return null; 
    	} 
    	Map<String, Object> map = new HashMap<String, Object>(); 
    	Field[] declaredFields = obj.getClass().getDeclaredFields(); 
    	for (Field field : declaredFields) { 
    	field.setAccessible(true); 
    	map.put(field.getName(), field.get(obj)); 
    	} 
    	return map; 
    	}

    public static Map applayRefund(String userId, String payId, String orderNo, Double amount, Double refundMoney) throws Exception {
        Map map = new HashMap();
        map.put("appid", APP_ID);
        map.put("mch_id", MCH_ID);
        map.put("nonce_str", UUID_MD5.getUUID());
        map.put("transaction_id", payId);
        map.put("out_refund_no", orderNo);
        int total_fee = (int) (amount * 100);
        map.put("total_fee", total_fee+"");
        int refund_fee = (int) (refundMoney * 100);
        map.put("refund_fee", refund_fee+"");
        map.put("notify_url", REFUND_NOTIFY_URL);
                             
        String stringSign = WXUtil.createSign(map);
        String sign = WXPayUtil.MD5(stringSign).toUpperCase();
        map.put("sign", sign);

        String data = WXPayUtil.mapToXml(map);

        File file = new File("apiclient_cert.p12");
        InputStream in = new FileInputStream(file);

        char password[] = WXUtil.MCH_ID.toCharArray();
        java.io.InputStream certStream = in;
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(certStream, password);
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, password);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
                new String[] { "TLSv1" }, null, new DefaultHostnameVerifier());

        BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager();
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();
        String url = (new StringBuilder()).append("https://").append("api.mch.weixin.qq.com")
                .append("/secapi/pay/refund").toString();
        HttpPost httpPost = new HttpPost(url);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(10000).build();
        httpPost.setConfig(requestConfig);
        StringEntity postEntity = new StringEntity(data, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.addHeader("User-Agent", (new StringBuilder()).append(WXPayConstants.USER_AGENT).append(" ")
                .append(map.get("mch_id")).toString());
        httpPost.setEntity(postEntity);
        CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
        org.apache.http.HttpEntity httpEntity = httpResponse.getEntity();
        String reponseString = EntityUtils.toString(httpEntity, "UTF-8");
        return WXPayUtil.xmlToMap(reponseString);
    }
    
    /**
     * 提現
     * @param openid 微信openid
     * @param realName 真實姓名
     * @param money 提現金額
     * @return
     * @throws Exception
     */
    public static Map wxWithdraw(String openid,String realName, Double money) throws Exception{
    	  Map map = new HashMap();
          map.put("mch_appid", APP_ID);
          map.put("mchid", MCH_ID);
          map.put("nonce_str", UUID_MD5.getUUID());
          map.put("partner_trade_no", UUID_MD5.getUUID());
          map.put("openid", openid);
          map.put("check_name", "FORCE_CHECK"); //強制實名,如需不校真實姓名,則改爲NO_CHECK
          map.put("re_user_name", realName);
          int total_fee = (int) (money * 100);
          map.put("amount", total_fee+"");
          map.put("desc", "賬戶提現");
          map.put("spbill_create_ip", "xxx.xxx.xxx.xxx");

          String stringSign = WXUtil.createSign(map);
          String sign = WXPayUtil.MD5(stringSign).toUpperCase();
          map.put("sign", sign);

          String data = WXPayUtil.mapToXml(map);

          File file = new File("apiclient_cert.p12");

          InputStream in = new FileInputStream(file);

          char password[] = WXUtil.MCH_ID.toCharArray();
          java.io.InputStream certStream = in;
          KeyStore ks = KeyStore.getInstance("PKCS12");
          ks.load(certStream, password);
          KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
          kmf.init(ks, password);
          SSLContext sslContext = SSLContext.getInstance("TLS");
          sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
          SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
                  new String[] { "TLSv1" }, null, new DefaultHostnameVerifier());

          BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager();
          CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();
          String url = (new StringBuilder()).append("https://").append("api.mch.weixin.qq.com")
                  .append("/mmpaymkttransfers/promotion/transfers").toString();
          HttpPost httpPost = new HttpPost(url);
          RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(10000).build();
          httpPost.setConfig(requestConfig);
          StringEntity postEntity = new StringEntity(data, "UTF-8");
          httpPost.addHeader("Content-Type", "text/xml");
          httpPost.addHeader("User-Agent", (new StringBuilder()).append(WXPayConstants.USER_AGENT).append(" ")
                  .append(map.get("mch_id")).toString());
          httpPost.setEntity(postEntity);
          CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
          org.apache.http.HttpEntity httpEntity = httpResponse.getEntity();
          String reponseString = EntityUtils.toString(httpEntity, "UTF-8");
          return WXPayUtil.xmlToMap(reponseString);
    	
    	
    }

    /**
  * 微信返回參數解密
  * @param reqInfo
  * @return
  * @throws Exception
  */
    public static Map parseReqInfo(String reqInfo) throws Exception {
    	init();
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] base64ByteArr = decoder.decode(reqInfo);
 
        String result = new String(cipher.doFinal(base64ByteArr));
        return WXPayUtil.xmlToMap(result);
    }
 
    public static void init() {
        String key = getMD5(KEY);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
        Security.addProvider(new BouncyCastleProvider());
        try {
            cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
    }
 
    public static String getMD5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            String result = MD5(str, md);
            return result;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return "";
        }
    }
 
    public static String MD5(String strSrc, MessageDigest md) {
        byte[] bt = strSrc.getBytes();
        md.update(bt);
        String strDes = bytes2Hex(md.digest());
        return strDes;
    }
 
    public static String bytes2Hex(byte[] bts) {
        StringBuffer des = new StringBuffer();
        String tmp = null;
        for (int i = 0; i < bts.length; i++) {
            tmp = (Integer.toHexString(bts[i] & 0xFF));
            if (tmp.length() == 1) {
                des.append("0");
            }
            des.append(tmp);
        }
        return des.toString();
    }

    public static String httpClient(String data) throws Exception{
        BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager();
        HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(connManager).build();
        String url = (new StringBuilder()).append("https://").append("api.mch.weixin.qq.com").append("/pay/unifiedorder").toString();
        HttpPost httpPost = new HttpPost(url);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(10000).build();
        httpPost.setConfig(requestConfig);
        StringEntity postEntity = new StringEntity(data, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.addHeader("User-Agent", (new StringBuilder()).append(WXPayConstants.USER_AGENT).append(" ").append(WXUtil.MCH_ID).toString());
        httpPost.setEntity(postEntity);
        HttpResponse httpResponse = httpClient.execute(httpPost);
        org.apache.http.HttpEntity httpEntity = httpResponse.getEntity();
        String reponseString = EntityUtils.toString(httpEntity, "UTF-8");
        return reponseString;
    }

}

   統一下單 - 微信預支付

@NoArgsConstructor
@Data
@Accessors(chain = true)
public class WeixinPayReqDto {

    @ApiModelProperty(value="請求參數")
    private WeixinPayReqData data;

    @NoArgsConstructor
    @Data
    @Accessors(chain=true)
    public class WeixinPayReqData {
        @ApiModelProperty(value="總金額")
        private double totalMoney;
	       
        @ApiModelProperty(value="訂單號")
        private String out_trade_no;

        @ApiModelProperty(value="openid")
        private String openid;
    }
}
@PostMapping("weixinPay")
public BaseDataResDto<Map<String, Object>> weixinPay(@RequestBody WeixinPayReqDto param) throws Exception {
    return capitalMainLogService.weixinPay(param, maijiToken);
}
@Override
public BaseDataResDto<Map<String, Object>> weixinPay(WeixinPayReqDto param) throws Exception {
    // 根據訂單號查看訂單是否存在
    ShopingOrder shopingOrder = new ShopingOrder();
    shopingOrder.setOrderNo(param.getData().getOut_trade_no());
    ShopingOrder newShopingOrder = shopingOrderMapper.selectOne(shopingOrder);
    if (newShopingOrder == null) return new BaseDataResDto<>(Status.PARAMETERERROR, "訂單編號不存在");
    if (newShopingOrder.getStatus() != 0) {
        return new BaseDataResDto<>(Status.PARAMETERERROR, "該訂單已支付");
    }
    // 校驗支付金額和應付金額是否一致
    if (param.getData().getTotalMoney() != newShopingOrder.getAmount()) {
        return new BaseDataResDto<>(Status.PARAMETERERROR, "支付金額錯誤");
    }

    String out_trade_no = shopingOrder.getOrderNo() + "_" + (int) (Math.random() * 1000000);
    Map map = new HashMap();
    map.put("appid", WXUtil.APP_ID);
    map.put("mch_id", WXUtil.MCH_ID);
    map.put("nonce_str", UUID_MD5.getUUID());
    map.put("sign_type", "MD5");
    map.put("body", "商品支付");
    map.put("out_trade_no", out_trade_no);
    map.put("fee_type", "CNY");
    map.put("total_fee", (int) (param.getData().getTotalMoney() * 100) + "");
    map.put("spbill_create_ip", "192.168.100.8");
    map.put("notify_url", WXUtil.NOTIFY_URL);
    map.put("trade_type", "APP");
    map.put("sign", WXPayUtil.MD5(WXUtil.createSign(map)).toUpperCase());
    // 請求報文 (map轉爲xml)
    String data = WXPayUtil.mapToXml(map);

    Map mapResponse = WXPayUtil.xmlToMap(WXUtil.httpClient(data));
    BaseDataResDto paramMap = new BaseDataResDto(Status.SUCCESS);

    Map<String, String> mapWeixinPay = new HashMap<String, String>();
    if (mapResponse.get("return_code") != null && mapResponse.get("result_code") != null) {
        mapWeixinPay.put("appid", WXUtil.APP_ID);
        mapWeixinPay.put("partnerid", WXUtil.MCH_ID);
        if (mapResponse.get("prepay_id") == null) return new BaseDataResDto<>(Status.PARAMETERERROR);
        long time = System.currentTimeMillis() / 1000;
        mapWeixinPay.put("noncestr", Long.toString(time));
        mapWeixinPay.put("timestamp", Long.toString(time));
        mapWeixinPay.put("prepayid", (String) mapResponse.get("prepay_id"));
        mapWeixinPay.put("package", "Sign=WXPay");
        String signString = WXUtil.createSign(mapWeixinPay);
        String signs = WXPayUtil.MD5(signString).toUpperCase();
        mapWeixinPay.put("sign", signs);
        paramMap.setData(mapWeixinPay);
        newShopingOrder.setPrepayId((String) mapResponse.get("prepay_id"));
        newShopingOrder.setNonceStr(map.get("nonce_str") + "");
    } else {
        paramMap.setStatus(Status.PARAMETERERROR);
    }
    // 保存預支付訂單號
    shopingOrderMapper.updateById(newShopingOrder.setOutTradeNo(out_trade_no));
    return paramMap;
}

    

成功返回給安卓/IOS,由他們調起微信收銀臺發起付款即可。

2. 【微信支付成功回調】 (支付結果通知)

支付完成後,微信會把相關支付結果及用戶信息通過數據流的形式發送給商戶,商戶需要接收處理,並按文檔規範返回應答。

該鏈接是通過【統一下單API】中提交的參數notify_url設置,如果鏈接無法訪問,商戶將無法接收到微信通知。

通知url必須爲直接可訪問的url,不能攜帶參數。

注意:

① 同樣的通知可能會多次發送給商戶系統。商戶系統必須能夠正確處理重複的通知。

② 後臺通知交互時,如果微信收到商戶的應答不符合規範或超時,微信會判定本次通知失敗,重新發送通知,直到成功爲止(在通知一直不成功的情況下,微信總共會發起多次通知,通知頻率爲15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 總計 24h4m),但微信不保證通知最終一定能成功。

③ 在訂單狀態不明或者沒有收到微信支付結果通知的情況下,建議商戶主動調用微信支付【查詢訂單API】確認訂單狀態。

@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
@NoArgsConstructor
@Data
@Accessors(chain=true)
public class PayResDto {
    private String return_msg;
    private String return_code;
    private String appid;
    private String mch_id;
    private String device_info;
    private String nonce_str;
    private String sign;
    private String result_code;
    private String err_code;
    private String err_code_des;
    private String openid;
    private String is_subscribe;
    private String trade_type;
    private String bank_type;
    private Integer total_fee;
    private String fee_type;
    private Integer cash_fee;
    private String cash_fee_type;
    private Integer coupon_fee;
    private Integer coupon_count;
    private String coupon_id_$n;
    private Integer coupon_fee_$n;
    private String transaction_id;
    private String out_trade_no;
    private String attach;
    private String time_end;
}
import com.github.wxpay.sdk.WXPayUtil;

@PostMapping("weixinPayCallBack")
public String weixinPayCallBack(@RequestBody PayResDto param) throws Exception {
    Map map = capitalMainLogService.weixinPayCallBack(param);
    return WXPayUtil.mapToXml(map);
}
@Override
public Map weixinPayCallBack(PayResDto param) {
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("return_code", "SUCCESS");
    if (param == null) {
        map.put("return_msg", "參數爲空");
         return map;
    }

    // 根據訂單號查看訂單信息
    ShopingOrder shopingOrder = shopingOrderService.selectOne(new EntityWrapper<ShopingOrder>().eq("order_no", param.getOut_trade_no().substring(0, 20)));

    // 已經付款
    if (Arrays.asList(1, 2, 3, 5, 6).contains(shopingOrder.getStatus())) {
        map.put("return_msg", "OK");
        return map;
    }
             
    shopingOrder.setStatus(1).setPayId(param.getTransaction_id()).setPayDate(newDate()).setOutTradeNo(param.getOut_trade_no()).setPayType(2);
    // 修改支付狀態爲成功
    shopingOrderMapper.updateById(shopingOrder);

    // 分銷
    distributionService.asyncDistributionFund(shopingOrder);
    map.put("return_msg", "OK");
    return map;
}

3. 【查詢訂單】

接口提供所有微信支付訂單的查詢,商戶可以通過該接口主動查詢訂單狀態,完成下一步的業務邏輯。

需要調用查詢接口的情況:

◆ 當商戶後臺、網絡、服務器等出現異常,商戶系統最終未接收到支付通知;

◆ 調用支付接口後,返回系統錯誤或未知交易狀態情況;

◆ 調用被掃支付API,返回USERPAYING的狀態;

◆ 調用關單或撤銷接口API之前,需確認支付狀態;
 

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    Charset thirdRequest = Charset.forName("UTF-8");

    // 處理請求中文亂碼問題
    List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
    for (HttpMessageConverter<?> messageConverter : messageConverters) {
        if (messageConverter instanceof StringHttpMessageConverter) {
            ((StringHttpMessageConverter) messageConverter).setDefaultCharset(thirdRequest);
        }
        if (messageConverter instanceof MappingJackson2HttpMessageConverter) {
            ((MappingJackson2HttpMessageConverter) messageConverter).setDefaultCharset(thirdRequest);
        }
        if (messageConverter instanceof AllEncompassingFormHttpMessageConverter) {
            ((AllEncompassingFormHttpMessageConverter) messageConverter).setCharset(thirdRequest);
        }
    }

    return restTemplate;
}
@Autowired
private RestTemplate restTemplate;

/** 微信查詢訂單 */
@PostMapping("weixinOrderQuery")
public Map<String, String> weixinOrderQuery(@RequestParam String out_trade_no)  throws Exception {
    Map map = new HashMap();
    map.put("appid", WXUtil.APP_ID);
    map.put("mch_id", WXUtil.MCH_ID);
    map.put("nonce_str", UUID_MD5.getUUID());
    map.put("out_trade_no", out_trade_no);
    String sign = WXPayUtil.MD5(WXUtil.createSign(map)).toUpperCase();
    map.put("sign", sign);

    String data = WXPayUtil.mapToXml(map);

    String response = restTemplate.postForObject("https://api.mch.weixin.qq.com/pay/orderquery", data, String.class);
    return WXPayUtil.xmlToMap(response);
}

   

4. 【退款】

當交易發生之後一段時間內,由於買家或者賣家的原因需要退款時,賣家可以通過退款接口將支付款退還給買家,微信支付將在收到退款請求並且驗證成功之後,按照退款規則將支付款按原路退到買家帳號上。

注意:

◆ 交易時間超過一年的訂單無法提交退款;

◆ 微信支付退款支持單筆交易分多次退款,多次退款需要提交原支付訂單的商戶訂單號和設置不同的退款單號。申請退款總金額不能超過訂單金額。 一筆退款失敗後重新提交,請不要更換退款單號,請使用原商戶退款單號。

◆ 請求頻率限制:150qps,即每秒鐘正常的申請退款請求次數不超過150次

   錯誤或無效請求頻率限制:6qps,即每秒鐘異常或錯誤的退款申請請求不超過6次

◆ 每個支付訂單的部分退款次數不能超過50次

◆ 如果同一個用戶有多筆退款,建議分不同批次進行退款,避免併發退款導致退款失敗

@Override
public BaseResDto executeRefund(String orderRefundId) throws Exception{

    ShoppingOrderRefundEntity shoppingOrderRefund = shopingOrderRefundService.selectById(orderRefundId).setRefundMiddleTime(new Date()).setStatus(3);//退款中
    String orderId = shoppingOrderRefund.getOrderId();
    ShopingOrder shopingOrder = shopingOrderMapper.selectById(orderId)         .setRefundStatus(3);//退款中
    Double refundMoney = shoppingOrderRefund.getRefundMoney();
    Double amount = shopingOrder.getAmount();
    if (refundManey > amount) 
        return BaseResDto.baseResDto(Status.ERROR, "退款金額錯誤!");
    Map wxMap = WXUtil.applayRefund(shopingOrder.getUserId(), shopingOrder.getPayId(), shopingOrder.getOutTradeNo(), amount, refundMoney);
    if (!wxMap.get("result_code").equals("SUCCESS"))
        return BaseResDto.baseResDto(Status.ERROR, "申請微信退款失敗!");

    // 下面接入自己的業務邏輯...

    return new BaseResDto(Status.SUCCESS);       
}

5. 【退款回調通知】

當商戶申請的退款有結果後(退款狀態爲:退款成功、退款關閉、退款異常),微信會把相關結果發送給商戶,商戶需要接收處理,並返回應答。
對後臺通知交互時,如果微信收到商戶的應答不是成功或超時,微信認爲通知失敗,微信會通過一定的策略定期重新發起通知,儘可能提高通知的成功率,但微信不保證通知最終能成功(通知頻率爲15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 總計 24h4m)。
注意:同樣的通知可能會多次發送給商戶系統。商戶系統必須能夠正確處理重複的通知。
推薦的做法是,當收到通知進行處理時,首先檢查對應業務數據的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務數據進行狀態檢查和處理之前,要採用數據鎖進行併發控制,以避免函數重入造成的數據混亂。
特別說明:退款結果對重要的數據進行了加密,商戶需要用商戶祕鑰進行解密後才能獲得結果通知的內容。

@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class WXRefundReqDto { //嚴格按照微信官方文檔封裝的實體類
	
    private String return_msg;
    private String return_code;
    private String appid;
    private String mch_id;
    private String nonce_str;
    private String req_info;
    private String transaction_id; //微信訂單號
    private String out_trade_no; //商戶訂單號
    private String refund_id; //微信退款單號
    private String out_refund_no; //商戶退款單號
    private String total_fee; //訂單金額
    private String settlement_total_fee; //應結訂單金額
    private String refund_fee; //申請退款金額
    private String settlement_refund_fee; //退款金額
    private String refund_status; //退款狀態
    private String success_time; //退款成功時間
    private String refund_recv_accout; //退款入賬賬戶
    private String refund_account; //退款資金來源
    private String refund_request_source; //退款發起來源

}
@PostMapping("refundCallBack")
public String refundCallBack(@RequestBody WXRefundReqDto param) throws Exception {
    Map map = capitalMainLogService.refundCallBack(param);
    return WXPayUtil.mapToXml(map);
}
@Override
public Map refundCallBack(WXRefundReqDto param) throws Exception {
       
    Map map = new HashMap();
    if (param == null) {
        map.put("return_code", "FAIL");
        map.put("return_msg", "參數爲空");
        return map;
    }
    if (!"SUCCESS".equals(param.getReturn_code())) {
        map.put("return_code", "FAIL");
        map.put("return_msg", "退款失敗");
        return map;
    }

    Map signMap = objectToMap(param);
    signMap.remove("return_code");
    signMap.remove("return_msg");
    signMap.remove("appid");
    signMap.remove("mch_id");
    signMap.remove("nonce_str");
    signMap.remove("req_info");
    String refundSignString = WXUtil.createSign(signMap);
    String sign = WXPayUtil.MD5(refundSignString).toUpperCase();
    // 驗證簽名
    if (!sign.equals(param.getReq_info())) {
        map.put("return_code", "FAIL");
        map.put("return_msg", "簽名錯誤");
    }

    // 根據訂單號查看訂單信息
    String order_no = (String) WXUtil.parseReqInfo(param.getReq_info()).get("out_trade_no");
    ShopingOrder shopingOrder = shopingOrderMapper.selectOne(new ShopingOrder().setOutTradeNo(order_no));

    // 已經退款
    if (shopingOrder.getStatus() == 4) {
        map.put("return_msg", "OK");
        map.put("return_code", "SUCCESS");
        return map;
    }

    // 修改訂單表退款狀態     
    shopingOrder.setRefundStatus(4); //退款成功
    if (!shopingOrderService.updateById(shopingOrder)) {
        map.put("return_code", "FAIL");
        return map;
    }

    // 其他業務邏輯自行編寫...

    map.put("return_msg", "OK");
    map.put("return_code", "SUCCESS");
    return map;
}

6. 【查詢退款】

提交退款申請後,通過調用該接口查詢退款狀態。退款有一定延時,用零錢支付的退款20分鐘內到賬,銀行卡支付的退款3個工作日後重新查詢退款狀態。

@Autowired
private RestTemplate restTemplate;

/** 查詢微信退款 */
@PostMapping("wxRefundQuery")
public Map<String, String> wxRefundQuery(@RequestParam String out_trade_no) throws Exception{
    Map map = new HashMap();
    map.put("appid", WXUtil.APP_ID);
    map.put("mch_id", WXUtil.MCH_ID);
	map.put("nonce_str", UUID_MD5.getUUID());
    map.put("out_trade_no", out_trade_no);
	String sign = WXPayUtil.MD5(WXUtil.createSign(map)).toUpperCase();
	map.put("sign", sign);

    String data = WXPayUtil.mapToXml(map);

    String response = restTemplate.postForObject("https://api.mch.weixin.qq.com/pay/refundquery", data, String.class);
    return WXPayUtil.xmlToMap(response);
}

7. 【企業付款到零錢】(提現)

使用條件和開通步驟參考官方文檔:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1

企業付款爲企業提供付款至用戶零錢的能力,支持通過API接口付款,或通過微信支付商戶平臺(pay.weixin.qq.com)網頁操作付款。

【使用條件】(切記,博主就被坑過)

◆ 商戶號(或同主體其他非服務商商戶號)已入駐90日

◆ 截止今日回推30天,商戶號(或同主體其他非服務商商戶號)連續不間斷保持有交易

◆ 登錄微信支付商戶平臺-產品中心,開通企業付款。

◆ spbill_create_ip 必須在商戶平臺配置好IP白名單

@PostMapping("withdraw")
public BaseResDto withdraw(@RequestParam Bigdecimal money,@RequestHeader("token") String token) {
       
    // 驗證token,並根據token獲取用戶實體UserEntity,此處省略...

    Map map = WXUtil.wxWithdraw(userEntity.getWxOpenId(), userEntity.getRealName(), money); //如果check_name爲NO_CHECK,則傳暱稱也可以

    if (!("SUCCESS".equals(map.get("return_code")) && "SUCCESS".equals(map.get("result_code"))))
        return new BaseResDto(Status.PARAMETERERROR, "提現失敗");

    // 此處編寫業務邏輯,並記錄提現日誌

    return new BaseResDto(Status.SUCCESS);
}

二. 掃碼支付

Native支付可分爲兩種模式,商戶根據支付場景選擇相應模式。

一般我們電商網站都會選用模式二:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5

@RestController
public class WxNativePay {

    @PostMapping("/wxNativePay")
    public BaseDataResDto<JSONObject> wxNativePay()  {
        try{
            Map map = new HashMap();
            map.put("appid", WXUtil.APP_ID);
            map.put("mch_id", WXUtil.MCH_ID);
            map.put("nonce_str", UUID_MD5.getUUID());
            map.put("sign_type", "MD5");
            map.put("body", "商品支付");
            map.put("out_trade_no", "1234560001");
            map.put("fee_type", "CNY");
            map.put("total_fee", 10 + "");
            map.put("spbill_create_ip", "127.0.0.1");
            map.put("notify_url", WXUtil.NOTIFY_URL);
            map.put("trade_type", "NATIVE");
            map.put("sign", WXPayUtil.MD5(WXUtil.createSign(map)).toUpperCase());
            // 請求報文 (map轉爲xml)
            String data = WXPayUtil.mapToXml(map);
            Map<String, String> mapResponse = WXPayUtil.xmlToMap(WXUtil.httpClient(data));

            if (Objects.equals(mapResponse.get("result_code"),"SUCCESS")) {
                String content = mapResponse.get("code_url");
                JSONObject json = new JSONObject();
                json.put("codeUrl", content);
                return new BaseDataResDto(Status.SUCCESS).setData(json);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return new BaseDataResDto(Status.ERROR);
    }
}

      

直接將code_url返給前端,前端調用第三方庫轉化爲二維碼,供用戶掃碼付款;注意:code_url有效期爲2小時,過期後掃碼不能再發起支付。

【小結】:

原理很簡單,簽名驗籤是關鍵,無非就是API;微信小程序支付,H5,Web掃碼支付,APP支付,區別不大,且支付結果通知(異步回調),查詢訂單,退款,查詢退款等的API是一致的;下單API僅僅也只是 trade_type 不同而已 ~

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