1. 程式人生 > >Spring Boot入門教程(四十):微信支付整合-刷卡支付

Spring Boot入門教程(四十):微信支付整合-刷卡支付

一:準備工作

使用微信支付需要先開通服務號,然後還要開通微信支付,最後還要配置一些開發引數,過程比較多。

二:支付方式

  • 刷卡支付(MICROPAY) :刷卡支付是使用者展示微信錢包內的“刷卡條碼/二維碼”給商戶系統掃描後直接完成支付的模式。主要應用線下面對面收銀的場景。相當於支付寶的條碼支付

  • 掃碼支付:掃碼支付是商戶系統按微信支付協議生成支付二維碼,使用者再用微信“掃一掃”完成支付的模式。該模式適用於PC網站支付、實體店單品或訂單支付、媒體廣告支付等場景。相當於支付寶的電腦網站支付

  • H5支付:H5支付是指商戶在微信客戶端外的移動端網頁展示商品或服務,使用者在前述頁面確認使用微信支付時,商戶發起本服務呼起微信客戶端進行支付。主要用於觸屏版的手機瀏覽器請求微信支付的場景。可以方便的從外部瀏覽器喚起微信支付。相當於支付寶的手機網站支付

  • 公眾號支付(JSAPI):商戶已有H5商城網站,使用者通過訊息或掃描二維碼在微信內開啟網頁時,可以呼叫微信支付完成下單購買的流程。

  • App支付:APP支付又稱移動端支付,是商戶通過在移動端應用APP中整合開放SDK調起微信支付模組完成支付的模式。

  • 小程式支付:在小程式中使用

H5支付: 是應用再微信客戶端外,是一種WAP支付。
公眾號支付(JSAPI): 是應用在微信內的一種支付。
兩種方式的應用場景不一樣。

這裡寫圖片描述

三:刷卡支付場景介紹

這裡寫圖片描述

1. 場景介紹

  • 步驟1:使用者選擇刷卡支付付款並開啟微信,進入“我”->“錢包”->“收付款”條碼介面;

  • 步驟2:收銀員在商戶系統操作生成支付訂單,使用者確認支付金額;

  • 步驟3:商戶收銀員用掃碼裝置掃描使用者的條碼/二維碼,商戶收銀系統提交支付;

  • 步驟4:微信支付後臺系統收到支付請求,根據驗證密碼規則判斷是否驗證使用者的支付密碼,不需要驗證密碼的交易直接發起扣款,需要驗證密碼的交易會彈出密碼輸入框。支付成功後微信端會彈出成功頁面,支付失敗會彈出錯誤提示。

2. 驗證密碼規則:

  • 支付金額>1000元的交易需要驗證使用者支付密碼
  • 使用者賬號每天最多有5筆交易可以免密,超過後需要驗證密碼
  • 微信支付後臺判斷使用者支付行為有異常情況,符合免密規則的交易也會要求驗證密碼

3. 免密支付流程:

這裡寫圖片描述

(1)收銀員在商戶收銀臺生成支付訂單,向用戶展示支付金額;

(2)使用者開啟微信客戶端,點選“我的錢包”,選擇“刷卡”,進入條碼介面;

(3)收銀員使用掃碼裝置讀取使用者手機螢幕上的條碼;

(4)掃碼裝置將讀取的資訊上傳給門店收銀臺;

(5)門店收銀臺得到支付資訊後,向商戶收銀後臺發起支付請求。

(6)商戶後臺對門店收銀臺的支付請求進行處理,生成簽名後呼叫【提交刷卡支付API】向微信支付系統發起支付請求。

(7)微信支付系統得到商戶側的支付請求之後會對請求進行驗證,驗證通過之後會對請求資料進行處理,最後將處理後的支付結果返回給商戶收銀後臺。如果支付成功,微信支付系統會將支付結果返回給商戶,同時把支付結果通知給使用者(以簡訊、微信訊息的形式通知)。

(8)商戶收銀後臺對得到的支付結果進行簽名驗證和處理,再將支付結果返回給門店收銀臺。

(9)收銀員看到門店收銀臺的支付結果後給使用者發貨。

4. 驗密支付流程

在商戶呼叫【提交刷卡支付API】發起支付請求之後,微信支付後臺提示使用者輸入密碼確認支付,介面同步返回USERPAYING狀態,商戶系統再輪詢呼叫查詢訂單介面來確認當前使用者是否已經支付成功。

以下時序圖說明驗密支付流程:
這裡寫圖片描述

由於在商戶收銀後臺向微信支付系統發起支付請求之前的流程是完全一樣的,所以這裡只介紹商戶發起支付請求之後的邏輯。

(1)商戶門店生成訂單後,收銀臺向後臺系統發起支付請求。

(2)後臺呼叫微信支付【提交刷卡支付API】生成支付交易。

(3)微信支付系統對商戶請求進行驗證,驗證通過後判斷當前使用者需要輸入密碼。

(4)微信支付系統返回USERPAYING狀態,商戶後臺系統將應答結果返回給商戶門店收銀臺。

(5)微信支付系統通知使用者微信客戶端輸入密碼。

(6)使用者得到輸入密碼提示後,確認支付並輸入密碼。

(7)完成密碼輸入,提交微信支付。

(8)微信客戶端在使用者完成支付後提示微信支付後臺系統返回的支付結果,而且微信支付系統會通過簡訊、微信訊息給使用者傳送支付結果提醒。

(9)商戶收銀臺得到USERPAYING狀態後,經過商戶後臺系統呼叫【查詢訂單API】查詢實際支付結果。

(10)如果支付結果仍為USERPAYING,則每隔5秒迴圈呼叫【查詢訂單API】判斷實際支付結果,如果使用者取消支付或累計30秒使用者都未支付,商戶收銀臺退出查詢流程後繼續呼叫【撤銷訂單API】撤銷支付交易。

5. 異常處理

使用者遇到支付異常,請按如下說明處理

(1)使用者微信端彈出系統錯誤提示框,使用者可在交易列表檢視交易情況,如果未找到訂單,需要商戶重新發起支付交易;如果訂單顯示成功支付,商戶收銀系統再次呼叫【查詢訂單API】查詢實際支付結果;

(2)使用者微信端彈出支付失敗提示,例如:餘額不足,信用卡失效。需要重新發起支付;

(3)當交易超時或支付交易失敗,商戶收銀系統必須呼叫【撤銷訂單API】,撤銷此交易。

(4)由於銀行系統異常、使用者餘額不足、不支援使用者卡種等原因使當前支付交易失敗,商戶收銀系統應該把錯誤提示明確展示給收銀員。

(5)根據返回的錯誤碼,判斷是否需要撤銷交易,具體詳見API返回錯誤碼列表

public Map<String, String> microPayWithPos(Map<String, String> reqData) throws Exception {
    return this.microPayWithPos(reqData, this.config.getHttpConnectTimeoutMs());
}

/**
 * 提交刷卡支付,針對軟POS,儘可能做成功
 * 內建重試機制,最多60s
 * @param reqData
 * @param connectTimeoutMs
 * @return
 * @throws Exception
 */
public Map<String, String> microPayWithPos(Map<String, String> reqData, int connectTimeoutMs) throws Exception {
    int remainingTimeMs = 60*1000;
    long startTimestampMs = 0;
    Map<String, String> lastResult = null;
    Exception lastException = null;

    while (true) {
        startTimestampMs = WXPayUtil.getCurrentTimestampMs();
        int readTimeoutMs = remainingTimeMs - connectTimeoutMs;
        if (readTimeoutMs > 1000) {
            try {
                lastResult = this.microPay(reqData, connectTimeoutMs, readTimeoutMs);
                String returnCode = lastResult.get("return_code");
                if (returnCode.equals("SUCCESS")) {
                    String resultCode = lastResult.get("result_code");
                    String errCode = lastResult.get("err_code");
                    if (resultCode.equals("SUCCESS")) {
                        break;
                    }
                    else {
                        // 看錯誤碼,若支付結果未知,則重試提交刷卡支付
                        if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) {
                            remainingTimeMs = remainingTimeMs - (int)(WXPayUtil.getCurrentTimestampMs() - startTimestampMs);
                            if (remainingTimeMs <= 100) {
                                break;
                            }
                            else {
                                WXPayUtil.getLogger().info("microPayWithPos: try micropay again");
                                if (remainingTimeMs > 5*1000) {
                                    Thread.sleep(5*1000);
                                }
                                else {
                                    Thread.sleep(1*1000);
                                }
                                continue;
                            }
                        }
                        else {
                            break;
                        }
                    }
                }
                else {
                    break;
                }
            }
            catch (Exception ex) {
                lastResult = null;
                lastException = ex;
            }
        }
        else {
            break;
        }
    }

    if (lastResult == null) {
        throw lastException;
    }
    else {
        return lastResult;
    }
}

看Demo中的方法,整個方法也沒有查詢訂單,也沒有撤銷訂單的操作,而是不停的重複呼叫microPay,這和上面文件的(9)、(10)條邏輯不一樣

注意:微信的刷卡支付並沒有支付通知介面,只有退款通知介面;支付寶的條碼支付是有支付通知介面的,兩家是不一樣的。

四:開發文件

在整合之前,一定要熟悉業務,熟悉文件

刷卡文件,開發前請詳細認證的看完該文件。

刷卡文件中有demo,SDK與DEMO下載,需要下載下來,熟悉一下專案結構,以及READEME.md

五:整合步驟

1. 引入SDK

  • wxpay-sdk 專案中的src就是要引入的sdk,可以直接將src的所有7個原始檔拖入到自己專案中
  • wxpay-sdk 的READEME.md中說可以通過maven來引入sdk

現在有兩種方式,選擇其中一種,究竟選哪一種?

wxpay-sdk的src下面有7個java檔案,通過maven引入可以看到的sdk中就4個java檔案,兩種方式檔案個數不一致,多出來的3個檔案是demo中用到的檔案,並不是原始sdk中的檔案,通過檢視sdk中的原始碼可以看到,這4個檔案其實就是使用httpclient來呼叫微信支付的支付介面,其中很多重要的邏輯並沒有按照官方文件中說的那樣缺少重要的邏輯實現,這個需要自己去完善一些邏輯
這裡寫圖片描述

開發中發現WXPay這個類和maven中的類並不完全一樣,多了兩個方法microPayWithPos,其中這兩個方法是demo作者自己基於內部方法microPay的一個封裝,完善了部分邏輯, 但是完善的邏輯和官網文件的描述不一致,有重大邏輯問題
這裡寫圖片描述

檢視demo中的test發現竟然有一個類叫test,命名不清晰,語義太籠統,而且還是小寫字母開頭,不符合Java的基本命名規範,關於java中的測試的命名規則一般類名以Test作為字尾,而該demo以Test作為字首,一般測試類方法的命名以test作為字首,但是demo中的測試並沒有什麼test,一般測試類都要使用測試框架如Junit等,但是demo中的測試並沒有使用測試框架,而是使用main方法來執行的。
這裡寫圖片描述

對wxpay-sdk的評價:wxpay-sdk只是使用httpclient來呼叫微信支付的介面,只管呼叫微信支付介面,然後解析一下響應,並不處理支付中的業務邏輯,儘管一些和支付密切相關的重要邏輯也不會處理,甚至解密的工具方法都沒有現成的,需要開發者自己去處理。看demo可以知道,寫的不是一般的爛,沒想到微信那麼大的廠竟然sdk寫的這麼懶,demo寫的這麼low,真是丟人,和支付寶的sdk比一個地下一個天上。

罵完了,還是要整合的,這裡我選擇不相信demo的原始檔,使用README.md中的maven來引入sdk,一個是demo寫的太爛,多出來的檔案也不是不要不行的,多出來的方法有重大邏輯缺陷,萬一哪天微信哪天良心發現完善sdk了,更新了maven版本,自己只需要修改一下maven的版本號就行了。

<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

2. application.yml

配置微信支付引數,其中appID、mchID、key、certPath是必須的,notifyUrl是可選的,sandboxKey和useSandbox是自己配置的,便於正式環境和沙箱環境的切換,關於引數的值去微信的開發配置中檢視

  • useSandbox:true表示使用沙箱環境,如果為false為正式環境
  • sandboxKey: 沙箱環境API祕鑰,需要通過下面的WXPayClient#getSignKey方法獲取
pay:
    wxpay:
        appID: xxx
        mchID: xxx
        key: xxx
        sandboxKey: xxx
        certPath: /var/local/cert/apiclient_cert.p12
        notifyUrl: http://65ta5j.natappfree.cc/wxpay/refund/notify
        useSandbox: false

3. MyWXPayConfig

配置微信引數,其實就是一種Properties類,需要實現WXPayConfig中的方法
注意:demo中的WXPayConfig是一種抽象類abstract class,可以看到maven與demo的不統一

/**
 * 微信支付的引數配置
 *
 * @author mengday zhang
 */
@Data
@Slf4j
@ConfigurationProperties(prefix = "pay.wxpay")
public class MyWXPayConfig implements WXPayConfig {

    /** 公眾賬號ID */
    private String appID;

    /** 商戶號 */
    private String mchID;

    /** API 金鑰 */
    private String key;

    /** API 沙箱環境金鑰 */
    private String sandboxKey;

    /** API證書絕對路徑 */
    private String certPath;

    /** 退款非同步通知地址 */
    private String notifyUrl;

    private Boolean useSandbox;

    /** HTTP(S) 連線超時時間,單位毫秒 */
    private int httpConnectTimeoutMs = 8000;

    /** HTTP(S) 讀資料超時時間,單位毫秒 */
    private int httpReadTimeoutMs = 10000;


    /**
     * 獲取商戶證書內容
     *
     * @return 商戶證書內容
     */
    @Override
    public InputStream getCertStream()  {
        File certFile = new File(certPath);
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(certFile);
        } catch (FileNotFoundException e) {
            log.error("cert file not found, path={}, exception is:{}", certPath, e);
        }
        return inputStream;
    }

    @Override
    public String getKey(){
        if (useSandbox) {
            return sandboxKey;
        }

        return key;
    }
}

這裡寫圖片描述

4. WXPayClient

WXPayClient 是對WXPay的一個封裝,增加了microPayWithPOS方法,內部呼叫WXPay#microPay,但是sdk中的microPay並沒有處理當微信支付時微信提示使用者輸入密碼,這時sdk直接返回的錯誤,這裡處理的邏輯就是在指定時間內去輪詢支付結果,然後將輪詢的結果返回出去,而不是就直接返回錯誤了,demo專案也有個microPayWithPOS實現,但是它的邏輯是當用戶輸入密碼的情況會輪詢的去下單去呼叫WXPay#microPay,這種做法是不符合微信的官方文件的。另外增加了獲取沙箱環境API祕鑰和解密退換通知的方法。

吐槽一下:感覺像這種解密方法WXPayUtil中竟然沒有,這是必須的不可少的啊,怎麼會沒有呢,這也太爛了吧,這還稱為sdk啊

/**
 * WXPayClient
 * <p>
 * 對WXPay的簡單封裝,處理支付密切相關的邏輯.
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2018/6/16
 */
@Slf4j
public class WXPayClient extends WXPay {

    /** 金鑰演算法 */
    private static final String ALGORITHM = "AES";
    /** 加解密演算法/工作模式/填充方式 */
    private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding";
    /** 使用者支付中,需要輸入密碼 */
    private static final String ERR_CODE_USERPAYING = "USERPAYING";
    private static final String ERR_CODE_AUTHCODEEXPIRE = "AUTHCODEEXPIRE";
    /** 交易狀態: 未支付 */
    private static final String TRADE_STATE_NOTPAY = "NOTPAY";

    /** 使用者輸入密碼,嘗試30秒內去查詢支付結果 */
    private static Integer remainingTimeMs = 10000;

    private WXPayConfig config;

    public WXPayClient(WXPayConfig config, WXPayConstants.SignType signType, boolean useSandbox) {
        super(config, signType, useSandbox);
        this.config = config;
    }

    /**
     *
     * 刷卡支付
     *
     * 對WXPay#microPay(Map)增加了當支付結果為USERPAYING時去輪詢查詢支付結果的邏輯處理
     *
     * 注意:該方法沒有處理return_code=FAIL的情況,暫時不考慮網路問題,這種情況直接返回錯誤
     *
     * @param reqData
     * @return
     * @throws Exception
     */
    public Map<String, String> microPayWithPOS(Map<String, String> reqData) throws Exception {
        // 開始時間(毫秒)
        long startTimestampMs = System.currentTimeMillis();

        Map<String, String> responseMapForPay = super.microPay(reqData);
        log.info(responseMapForPay.toString());

        // // 先判斷 協議欄位返回(return_code),再判斷 業務返回,最後判斷 交易狀態(trade_state)
        // 通訊標識,非交易標識
        String returnCode = responseMapForPay.get("return_code");
        if (WXPayConstants.SUCCESS.equals(returnCode)) {
            String errCode = responseMapForPay.get("err_code");
            // 餘額不足,信用卡失效
            if (ERR_CODE_USERPAYING.equals(errCode) || "SYSTEMERROR".equals(errCode) || "BANKERROR".equals(errCode)) {
                Map<String, String> orderQueryMap = null;
                Map<String, String> requestData = new HashMap<>();
                requestData.put("out_trade_no", reqData.get("out_trade_no"));

                // 使用者支付中,需要輸入密碼或系統錯誤則去重新查詢訂單API err_code, result_code, err_code_des
                // 每次迴圈時的當前系統時間 - 開始時記錄的時間 > 設定的30秒時間就退出
                while (System.currentTimeMillis() - startTimestampMs < remainingTimeMs) {
                    // 商戶收銀臺得到USERPAYING狀態後,經過商戶後臺系統呼叫【查詢訂單API】查詢實際支付結果。
                    orderQueryMap = super.orderQuery(requestData);
                    String returnCodeForQuery = orderQueryMap.get("return_code");
                    if (WXPayConstants.SUCCESS.equals(returnCodeForQuery)) {
                        // 通訊成功
                        String tradeState = orderQueryMap.get("trade_state");
                        if (WXPayConstants.SUCCESS.equals(tradeState)) {
                            // 如果成功了直接將查詢結果返回
                            return orderQueryMap;
                        }
                        // 如果支付結果仍為USERPAYING,則每隔5秒迴圈呼叫【查詢訂單API】判斷實際支付結果
                        Thread.sleep(1000);
                    }
                }

                // 如果使用者取消支付或累計30秒使用者都未支付,商戶收銀臺退出查詢流程後繼續呼叫【撤銷訂單API】撤銷支付交易。
                String tradeState = orderQueryMap.get("trade_state");
                if (TRADE_STATE_NOTPAY.equals(tradeState) || ERR_CODE_USERPAYING.equals(tradeState) || ERR_CODE_AUTHCODEEXPIRE.equals(tradeState)) {
                    Map<String, String> reverseMap = this.reverse(requestData);
                    String returnCodeForReverse = reverseMap.get("return_code");
                    String resultCode = reverseMap.get("result_code");
                    if (WXPayConstants.SUCCESS.equals(returnCodeForReverse) && WXPayConstants.SUCCESS.equals(resultCode)) {
                        // 如果撤銷成功,需要告訴客戶端已經撤銷訂單了
                        responseMapForPay.put("err_code_des", "使用者取消支付或尚未支付,後臺已經撤銷該訂單,請重新支付!");
                    }
                }
            }
        }

        return responseMapForPay;
    }


    /**
     * 解密退款通知
     *
     * <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_16&index=11>退款結果通知文件</a>
     * @param reqInfo
     * @return
     * @throws Exception
     */
    public Map<String, String> decodeRefundNotify(String reqInfo) throws Exception {
        //(1)對加密串A做base64解碼,得到加密串B
        byte[] bytes = new BASE64Decoder().decodeBuffer(reqInfo);

        //(2)對商戶key做md5,得到32位小寫key* ( key設定路徑:微信商戶平臺(pay.weixin.qq.com)-->賬戶設定-->API安全-->金鑰設定 )
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
        SecretKeySpec key = new SecretKeySpec(WXPayUtil.MD5(config.getKey()).toLowerCase().getBytes(), ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key);

        // (3)用key*對加密串B做AES-256-ECB解密(PKCS7Padding)
        // java.security.InvalidKeyException: Illegal key size or default parameters
        // https://www.cnblogs.com/yaks/p/5608358.html
        String responseXml = new String(cipher.doFinal(bytes),"UTF-8");
        Map<String, String> responseMap = WXPayUtil.xmlToMap(responseXml);
        return responseMap;
    }


    /**
     * 獲取沙箱環境驗籤祕鑰API
     * <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=23_1">獲取驗籤祕鑰API文件</a>
     * @return
     * @throws Exception
     */
    public Map<String, String> getSignKey() throws Exception {
        Map<String, String> reqData = new HashMap<>();
        reqData.put("mch_id", config.getMchID());
        reqData.put("nonce_str", WXPayUtil.generateNonceStr());
        String sign = WXPayUtil.generateSignature(reqData, config.getKey(), WXPayConstants.SignType.MD5);
        reqData.put("sign", sign);
        String responseXml = this.requestWithoutCert("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", reqData,
                config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs());

        Map<String, String> responseMap = WXPayUtil.xmlToMap(responseXml);

        return responseMap;
    }
}

5. WXPayConfiguration

/**
 * 微信支付配置
 *
 * @author mengday zhang
 */
@Configuration
@EnableConfigurationProperties(MyWXPayConfig.class)
public class WXPayConfiguration {

    @Autowired
    private MyWXPayConfig wxPayConfig;

    /**
     * useSandbox true為沙盒環境
     * @return
     */
    @Bean
    public WXPay wxPay() {
        return new WXPay(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox() );
    }

    @Bean
    public WXPayClient wxPayClient() {
        return new WXPayClient(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox());
    }
}

6. WXPayMicroPayController

/**
 * 微信支付-刷卡支付.
 * <p>
 * detailed description
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2018/6/18
 */
@Slf4j
@RestController
@RequestMapping("/wxpay/microPay")
public class WXPayMicroPayController {

    @Autowired
    private WXPayClient wxPayClient;

    /**
     * 刷卡支付(類似支付寶的條碼支付)
     *
     * 和支付寶的好像不一樣,支付寶有支付通知,但是微信好像沒,微信有退款通知
     *
     * 微信支付後臺系統收到支付請求,根據驗證密碼規則判斷是否驗證使用者的支付密碼,不需要驗證密碼的交易直接發起扣款,
     * 需要驗證密碼的交易會彈出密碼輸入框。支付成功後微信端會彈出成功頁面,支付失敗會彈出錯誤提示
     * 注意該介面有可能返回錯誤碼為USERPAYING使用者支付中
     *
     * 驗證密碼規則
     * ◆ 支付金額>1000元的交易需要驗證使用者支付密碼
     * ◆ 使用者賬號每天最多有5筆交易可以免密,超過後需要驗證密碼
     * ◆ 微信支付後臺判斷使用者支付行為有異常情況,符合免密規則的交易也會要求驗證密碼
     *
     * 使用者刷卡條形碼規則:18位純數字,以10、11、12、13、14、15開頭
     */
    @PostMapping("")
    public Object microPay(String authCode) throws Exception {
        Map<String, String> reqData = new HashMap<>();
        // 商戶訂單號
        reqData.put("out_trade_no", String.valueOf(System.nanoTime()));
        // 訂單總金額,單位為分,只能為整數
        reqData.put("total_fee", "2");
        // 授權碼
        reqData.put("auth_code", authCode);
        // 商品描述
        reqData.put("body", "測試");
        Map<String, String> resultMap = wxPayClient.microPayWithPOS(reqData);
        log.info(resultMap.toString());

        return resultMap;
    }
}

7. WXPayController


/**
 * 微信支付 - 通用API.
 *
 * <p>
 * 類似支付寶中的條碼支付.
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2018/6/15
 */
@Slf4j
@RestController
@RequestMapping("/wxpay")
public class WXPayController {

    @Autowired
    private WXPay wxPay;

    @Autowired
    private WXPayClient wxPayClient;

    @Autowired
    private MyWXPayConfig wxPayConfig;



    /**
     * 訂單查詢
     * @param orderNo
     * @return
     * @throws Exception
     */
    @GetMapping("/orderQuery")
    public Object orderQuery(String orderNo) throws Exception {
        Map<String, String> data = new HashMap<>();
        data.put("out_trade_no", orderNo);
        Map<String, String> result = wxPay.orderQuery(data);

        return result;
    }

    /**
     * 退款
     * 注意:呼叫申請退款、撤銷訂單介面需要商戶證書
     * 注意:沙箱環境響應結果可能會是"沙箱支付金額不正確,請確認驗收case",但是正式環境不會報這個錯誤
     * 微信支付的最小金額是0.1元,所以在測試支付時金額必須大於0.1元,否則會提示微信支付配置錯誤,可以將microPay的total_fee大於1再退款
     */
    @PostMapping("/refund")
    public Object refund(String orderNo) throws Exception {
        Map<String, String> reqData = new HashMap<>();
        // 商戶訂單號
        reqData.put("out_trade_no", orderNo);
        // 授權碼
        reqData.put("out_refund_no", orderNo);
        // 訂單總金額,單位為分,只能為整數
        reqData.put("total_fee", "2");
        //退款金額
        reqData.put("refund_fee", "2");
        // 退款非同步通知地址
        reqData.put("notify_url", wxPayConfig.getNotifyUrl());
        reqData.put("refund_fee_type", "CNY");
        reqData.put("op_user_id", wxPayConfig.getMchID());

        Map<String, String> resultMap = wxPay.refund(reqData);
        log.info(resultMap.toString());

        return resultMap;
    }


    /**
     * 退款結果通知
     *
     * 特別說明:退款結果對重要的資料進行了加密,商戶需要用商戶祕鑰進行解密後才能獲得結果通知的內容
     * @param request
     * @throws Exception
     */
    @RequestMapping("/refund/notify")
    public String refundNotify(HttpServletRequest request) throws Exception {

//        Map<String, String> notifyMap = new HashMap<>();
//        notifyMap.put("nonce_str", "9b4e428ae262d5dca96178027e849fa9");
//        notifyMap.put("req_info", "VKGj8c81RwQ45LcyWEVBE9/HsNfADGbgmbIAQZ2ydcbIFhIIcJFKFQwGfcSGgFGtQlWvg6KDNsRjmCjN+PvipJ3roynJ7cME0LOFG50VGtk4EYHqdjFzUVANI7GpT+i6Ok+ZWivH0MwoGK2fsz3WG+bYs2XJBwav/K89tKjFhZGitCKKBeGqcP99fa/gAL0swNXXNQHmL806Zi+QcACzL3E89BtP9FlXM2Gi+wPQafvPr+/iE+LrPdMlNUa5LiZnenZXUF24kMdhaTafczcKL4sZjRXQHEfEyc/pIZPbIjcNIETvHsskyzKuHVr/SAFkxaM6RR1Kl9pyWZGUjkH5SOeqsT8uL7YQmTlDXrnXmno3AvZdnepTGL5w69yIOmQNPeMqsd01ES9WX36GZYOigfi2+BJ9RRXjIffmpB/MFF+zryyvLTaJE2obCwFSHrmOD8YbaJqrZXOUvWZQrn7wIQgaCypo8V57cD3w5d2RSgIHNrdnEDYlbRcLNYgKuL+T9+1HPhU/frowZgwPN9IB53OahZV3p1Yvos23kvhqPCLn3BYgUihRbey6QhEtL2QyifiQ9e8WVLzWpRZ+DOa8VrhYvSuTfjRdjoNanqHFvXGP6uEsEa+DETqnexpB7xOS9m/CdmlNCwbdUplPEVzNQQdzYT4kybi00Y8A+EdairxfVyK9A7MAYAMtAO9yxV2ht0bn3SofFyZe/YSzdJgxdtcxBf1CVYN6x+yHcSueCSgq4cM/2VCwh4J1+pUVmNpEm0OVcdKbV5USkaxJR0h7Yd+n5FTz5Q2S/qvyDo202cUzLFPI5UqQm5X+FOrWDAkmmr5yVcDQIm3dAdb31jkz0X2TPYt5g7ciQ1h9heyVxJ67FexKvEM4pKCCubtWx6nyxcOUghHMrh8DSoBtewtNjbnwGVIbLsSb6X9MIYAkWIDbqNVP1f63GiZU+cJlhBmvcb8aeQUdTTj7EX5pOTIVSVv5D6SkKmpGU4FGvV+WjufuGX4ZRZo+01p6xl0sfZVmucG1UtxhX6bMCJb06yDwxpv7tGwkwS4TCK4SQp40Xe0=");
//        notifyMap.put("appid", "xxx");
//        notifyMap.put("mch_id", "xxx");
//        notifyMap.put("return_code", "SUCCESS");

        // 注意:同樣的通知可能會多次傳送給商戶系統。商戶系統必須能夠正確處理重複的通知。
        // 推薦的做法是,當收到通知進行處理時,首先檢查對應業務資料的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。
        // 在對業務資料進行狀態檢查和處理之前,要採用資料鎖進行併發控制,以避免函式重入造成的資料混亂。
        // TODO 處理業務
        Map<String, String> requstInfoMap = wxPayClient.decodeRefundNotify(request);

        // 商戶處理退款通知引數後同步返回給微信引數
        Map<String, String> responseMap = new HashMap<>();
        responseMap.put("return_code", "SUCCESS");
        responseMap.put("return_msg", "OK");
        String responseXml = WXPayUtil.mapToXml(responseMap);
        return responseXml;
    }

    /**
     * 退款查詢
     * @param orderNo
     * @return
     * @throws Exception
     */
    @GetMapping("/refundQuery")
    public Object refundQuery(String orderNo) throws Exception {
        Map<String, String> reqData = new HashMap<>();
        reqData.put("out_trade_no", orderNo);
        Map<String, String> result = wxPay.refundQuery(reqData);

        return result;
    }


    /**
     * 下載對賬單
     * 注意:
     *      微信在次日9點啟動生成前一天的對賬單,建議商戶10點後再獲取;
     *      對賬單介面只能下載三個月以內的賬單。
     * @throws Exception
     */
    @PostMapping("/downloadBill")
    public Object downloadBill(String billDate) throws Exception {
        Map<String, String> reqData = new HashMap<>();
        reqData.put("bill_date", billDate);
        reqData.put("bill_type", "ALL");
        Map<String, String> resultMap = wxPay.downloadBill(reqData);

        return resultMap;
    }




    /**
     * 獲取沙箱環境API祕鑰,
     *
     * 這裡只是為了獲取,可以放在main方法下執行,這裡作為api來執行的,實際情況下不應該暴露出來
     * @return
     * @throws Exception
     */
    @GetMapping("/sandbox/getSignKey")
    public Object getSignKey() throws Exception {
        Map<String, String> signKey = wxPayClient.getSignKey();
        log.info(signKey.toString());

        return signKey;
    }
}

注意:如果需要退款非同步通知的話,通知地址需要外網能訪問到,可以通過natapp 外網對映本地 來做到

我的微信公眾號