網站怎麼接入微信掃碼支付?

2023-06-21 06:00:43

第01章-準備工作

1、微信支付產品介紹

參考資料:產品中心 - 微信支付商戶平臺 (qq.com)

付款碼支付、JSAPI支付、小程式支付、Native支付、APP支付、刷臉支付

1.1、付款碼支付

使用者展示微信錢包內的「付款碼」給商家,商家掃描後直接完成支付,適用於線下面對面收銀的場景。

1.2、JSAPI支付

  • 線下場所:商戶展示一個支付二維條碼,使用者使用微信掃描二維條碼後,輸入需要支付的金額,完成支付。
  • 公眾號場景:使用者在微信內進入商家公眾號,開啟某個頁面,選擇某個產品,完成支付。
  • PC網站場景:在網站中展示二維條碼,使用者使用微信掃描二維條碼,輸入需要支付的金額,完成支付。

特點:使用者在使用者端輸入支付金額

1.3、小程式支付

在微信小程式平臺內實現支付的功能。

1.4、Native支付

Native支付是指商戶展示支付二維條碼,使用者再用微信「掃一掃」完成支付的模式。這種方式適用於PC網站。

特點:商家預先指定支付金額

1.5、APP支付

商戶通過在行動端獨立的APP應用程式中整合微信支付模組,完成支付。

1.6、刷臉支付

使用者在刷臉裝置前通過攝像頭刷臉、識別身份後進行的一種支付方式。

2、介面版本

微信支付企業主流的API版本有v2和v3,課程中我們使用微信支付APIv3。

V2和V3的比較

相比較而言,APIv2比APIv3安全性更高,但是APIv2中有一些功能在APIv3中尚未完整實現,具體參考如下API字典頁面:API字典概覽 | 微信支付商戶平臺檔案中心 (qq.com)

3、接入指引

3.1、獲取開發引數

如果需要獨立申請和開通微信支付功能,可以參考如下官方檔案。開通微信支付後,才能獲取相關的開發引數以及商戶公鑰和商戶私鑰檔案。

參考資料:微信支付接入指引 - 微信支付商戶平臺 (qq.com)

3.2、設定開發引數

service-order服務的resources目錄中建立wxpay.properties

這個檔案定義了在「接入指引」的步驟中我們提前準備的微信支付相關的引數,例如商戶號、APPID、API祕鑰等等

# 微信支付相關引數
wxpay:
  mch-id: 1558950191 #商戶號
  mch-serial-no: 34345964330B66427E0D3D28826C4993C77E631F # 商戶API證書序列號
  private-key-path: D:/project/yygh/cert/apiclient_key.pem # 商戶私鑰檔案
  api-v3-key: UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B # APIv3金鑰
  appid: wx74862e0dfcf69954 # APPID
  notify-url: https://7d92-115-171-63-135.ngrok.io/api/order/wxpay/payment/notify # 接收支付結果通知地址
  notify-refund-url: http://agxnyzl04y90.ngrok.xiaomiqiu123.top/api/order/wxpay/refunds/notify # 接收退款結果通知地址

3.3、複製商戶私鑰

將商戶私鑰檔案複製到組態檔指定的目錄下:

資料:資料>微信支付>商戶證書>apiclient_key.pem

private-key-path: D:/project/yygh/cert/apiclient_key.pem # 商戶私鑰檔案

3.4、證書金鑰使用說明(瞭解)

參考檔案:APIv3證書與金鑰使用說明

一個完整的請求和響應的流程:

  • 商戶使用商戶私鑰對請求進行簽名,傳送給微信支付平臺,平臺使用商戶公鑰進行簽名驗證。
  • 微信支付平臺使用平臺私鑰對響應進行簽名,商戶使用微信支付平臺公鑰對響應進行驗籤。

第02章-訂單支付

1、微信支付平臺證書的獲取

1.1、引入SDK

參考檔案:SDK&工具

我們可以使用官方提供的 SDK wechatpay-java

在service-order微服務中新增依賴:

<!--微信支付APIv3-->
<dependency>
  <groupId>com.github.wechatpay-apiv3</groupId>
  <artifactId>wechatpay-java</artifactId>
  <version>0.2.6</version>
</dependency>

1.2、讀取支付引數

在config 包中 建立 WxPayConfig.java

package com.atguigu.syt.order.config;

@Configuration
@PropertySource("classpath:wxpay.properties") //讀取組態檔
@ConfigurationProperties(prefix="wxpay") //讀取wxpay節點
@Data
public class WxPayConfig {

    // 商戶號
    private String mchId;

    // 商戶API證書序列號
    private String mchSerialNo;

    // 商戶私鑰檔案
    private String privateKeyPath;

    // APIv3金鑰
    private String apiV3Key;

    // APPID
    private String appid;
    
    // 接收支付結果通知地址
    private String notifyUrl;
    
    // 接收退款結果通知地址
    private String notifyRefundUrl;
   
}

1.3、自動更新微信支付平臺證書

在 API 請求過程中,使用者端需使用微信支付平臺證書,驗證伺服器應答的真實性和完整性。我們使用自動更新平臺證書的設定類 RSAAutoCertificateConfig。每個商戶號只能建立一個 RSAAutoCertificateConfig

WxPayConfig中新增如下方法:

/**
     * 獲取微信支付設定物件
     * @return
     */
@Bean
public RSAAutoCertificateConfig getConfig(){

    return new RSAAutoCertificateConfig.Builder()
        .merchantId(mchId)
        .privateKeyFromPath(privateKeyPath)
        .merchantSerialNumber(mchSerialNo)
        .apiV3Key(apiV3Key)
        .build();
}

RSAAutoCertificateConfig 通過 RSAAutoCertificateProvider 自動下載微信支付平臺證書。 同時,RSAAutoCertificateProvider 會啟動一個後臺執行緒,定時更新證書(目前設計為60分鐘),以實現證書過期時的新老證書平滑切換。

常見錯誤:引入商戶私鑰後如果專案無法啟動,則需要升級JDK版本,並重新設定idea編譯和執行環境到最新版本的JDK。建議升級到1.8.0_300以上

2、生成支付二維條碼

2.1、Native支付流程

參考檔案:業務流程時序圖

2.2、Controller

在service-order中建立FrontWXPayController

package com.atguigu.syt.order.controller.front;

@Api(tags = "微信支付介面")
@RestController
@RequestMapping("/front/order/wxpay")
public class FrontWXPayController {

    @Resource
    private WxPayService wxPayService;

    @Resource
    private AuthContextHolder authContextHolder;

    @ApiOperation("獲取支付二維條碼url")
    @ApiImplicitParam(name = "outTradeNo",value = "訂單號", required = true)
    @GetMapping("/auth/nativePay/{outTradeNo}")
    public Result<String> nativePay(@PathVariable String outTradeNo, HttpServletRequest request, HttpServletResponse response) {

        //校驗使用者登入狀態
        authContextHolder.checkAuth(request, response);

        String codeUrl = wxPayService.createNative(outTradeNo);
        return Result.ok(codeUrl);
    }
}

2.3、Service

SDK參考程式碼:wechatpay-java/NativePayServiceExample.java at main · wechatpay-apiv3/wechatpay-java · GitHub

具體程式碼範例如下:

Native下單API引數參考:Native下單API

介面:OrderInfoService

/**
 * 根據訂單號獲取訂單
 * @param outTradeNo
 * @return
 */
OrderInfo selectByOutTradeNo(String outTradeNo);

實現:OrderInfoServiceImpl

@Override
public OrderInfo selectByOutTradeNo(String outTradeNo) {
    LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(OrderInfo::getOutTradeNo, outTradeNo);
    return baseMapper.selectOne(queryWrapper);
}

介面:WxPayService

package com.atguigu.syt.order.service;

public interface WxPayService {

    /**
     * 獲取支付二維條碼utl
     * @param outTradeNo
     * @return
     */
    String createNative(String outTradeNo);
}

實現:WxPayServiceImpl

package com.atguigu.syt.order.service.impl;

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private RSAAutoCertificateConfig rsaAutoCertificateConfig;

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private OrderInfoService orderInfoService;

    @Override
    public String createNative(String outTradeNo) {

        // 初始化服務
        NativePayService service = new NativePayService.Builder().config(rsaAutoCertificateConfig).build();

        // 呼叫介面
        try {

            //獲取訂單
            OrderInfo orderInfo = orderInfoService.selectByOutTradeNo(outTradeNo);

            PrepayRequest request = new PrepayRequest();
            // 呼叫request.setXxx(val)設定所需引數,具體引數可見Request定義
            request.setAppid(wxPayConfig.getAppid());
            request.setMchid(wxPayConfig.getMchId());

            request.setDescription(orderInfo.getTitle());
            request.setOutTradeNo(outTradeNo);
            request.setNotifyUrl(wxPayConfig.getNotifyUrl());

            Amount amount = new Amount();
            //amount.setTotal(orderInfo.getAmount().multiply(new BigDecimal(100)).intValue());
            amount.setTotal(1);//1分錢
            request.setAmount(amount);
            // 呼叫介面
            PrepayResponse prepayResponse = service.prepay(request);

            return prepayResponse.getCodeUrl();

        } catch (HttpException e) { // 傳送HTTP請求失敗
            // 呼叫e.getHttpRequest()獲取請求列印紀錄檔或上報監控,更多方法見HttpException定義
            log.error(e.getHttpRequest().toString());
            throw new GuiguException(ResultCodeEnum.FAIL);
        } catch (ServiceException e) { // 服務返回狀態小於200或大於等於300,例如500
            // 呼叫e.getResponseBody()獲取返回體列印紀錄檔或上報監控,更多方法見ServiceException定義
            log.error(e.getResponseBody());
            throw new GuiguException(ResultCodeEnum.FAIL);
        } catch (MalformedMessageException e) { // 服務返回成功,返回體型別不合法,或者解析返回體失敗
            // 呼叫e.getMessage()獲取資訊列印紀錄檔或上報監控,更多方法見MalformedMessageException定義
            log.error(e.getMessage());
            throw new GuiguException(ResultCodeEnum.FAIL);
        }
    }
}

可見,使用 SDK 並不需要計算請求籤名和驗證應答簽名。

3、前端整合

3.1、api

建立api/wxpay.js

import request from '~/utils/request'
export default {
  //獲取支付二維條碼url
  nativePay(outTradeNo) {
    return request({
      url: `/front/order/wxpay/auth/nativePay/${outTradeNo}`,
      method: 'get'
    })
  },
}

3.2、修改show.vue

引入api

import wxpayApi from '~/api/wxpay'

新增data資料

codeUrl: null, //微信支付二維條碼
isPayShow: false, //不顯示登入二維條碼元件
payText: '支付'

修改按鈕

將
<div class="v-button" @click="pay()">支付</div>
修改為
<div class="v-button" @click="pay()">{{payText}}</div>

新增方法

//支付
pay() {
    //防止重複提交
    if(this.isPayShow) return
    this.isPayShow = true
    this.payText = '支付中.....'

    //顯示二維條碼
    wxpayApi.nativePay(this.orderInfo.outTradeNo).then((response) => {
        this.codeUrl = response.data
        this.dialogPayVisible = true
    })
},

//關閉對話方塊
closeDialog(){
    //恢復支付按鈕
    this.isPayShow = false
    this.payText = '支付'
}

用二維條碼元件替換img圖片

<img src="二維條碼連結">

第03章-查詢支付結果

1、支付查單

參考檔案:微信支付查單介面

商戶可以主動呼叫微信支付查單介面,同步訂單狀態。

呼叫查詢訂單介面,如果支付成功則更新訂單狀態新增交易記錄,如果支付尚未成功則輪詢查單

1.1、更新訂單狀態

OrderInfoService介面

/**
     * 根據訂單號更新訂單狀態
     * @param outTradeNo
     * @param status
     */
void updateStatus(String outTradeNo, Integer status);

OrderInfoServiceImpl實現

@Override
public void updateStatus(String outTradeNo, Integer status) {

    LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(OrderInfo::getOutTradeNo, outTradeNo);

    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setOrderStatus(status);
    baseMapper.update(orderInfo, queryWrapper);
}

1.2、新增交易記錄

PaymentInfoService介面

/**
     * 儲存交易記錄
     * @param orderInfo
     * @param transaction
     */
void savePaymentInfo(OrderInfo orderInfo, Transaction transaction);

實現:PaymentInfoServiceImpl

@Override
public void savePaymentInfo(OrderInfo orderInfo, Transaction transaction) {

    PaymentInfo paymentInfo = new PaymentInfo();
    paymentInfo.setOrderId(orderInfo.getId());
    paymentInfo.setPaymentType(PaymentTypeEnum.WEIXIN.getStatus());
    paymentInfo.setOutTradeNo(orderInfo.getOutTradeNo());//資料庫欄位的長度改成32
    paymentInfo.setSubject(orderInfo.getTitle());
    paymentInfo.setTotalAmount(orderInfo.getAmount());
    paymentInfo.setPaymentStatus(PaymentStatusEnum.PAID.getStatus());
    paymentInfo.setTradeNo(transaction.getTransactionId());
    paymentInfo.setCallbackTime(new Date());
    paymentInfo.setCallbackContent(transaction.toString());
    baseMapper.insert(paymentInfo);
}

1.3、查單Controller

FrontWXPayController

@ApiOperation("查詢支付狀態")
@ApiImplicitParam(name = "outTradeNo",value = "訂單id", required = true)
@GetMapping("/queryPayStatus/{outTradeNo}")
public Result queryPayStatus(@PathVariable String outTradeNo) {
    //呼叫查詢介面
    boolean success = wxPayService.queryPayStatus(outTradeNo);
    if (success) {
        return Result.ok().message("支付成功");
    }
    return Result.ok().message("支付中").code(250);
}

1.4、查單Service

介面:WxPayService

/**
     * 查詢訂單支付狀態
     * @param outTradeNo
     * @return
     */
boolean queryPayStatus(String outTradeNo);

實現:WxPayServiceImpl

@Resource
private PaymentInfoService paymentInfoService;

@Override
public boolean queryPayStatus(String outTradeNo) {

    // 初始化服務
    NativePayService service = new NativePayService.Builder().config(rsaAutoCertificateConfig).build();

    // 呼叫介面
    try {

        QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
        // 呼叫request.setXxx(val)設定所需引數,具體引數可見Request定義
        request.setOutTradeNo(outTradeNo);
        request.setMchid(wxPayConfig.getMchId());

        // 呼叫介面
        Transaction transaction = service.queryOrderByOutTradeNo(request);
        Transaction.TradeStateEnum tradeState = transaction.getTradeState();

        //支付成功
        if(tradeState.equals(Transaction.TradeStateEnum.SUCCESS)){

            OrderInfo orderInfo = orderInfoService.selectByOutTradeNo(outTradeNo);

            //更新訂單狀態
            orderInfoService.updateStatus(outTradeNo, OrderStatusEnum.PAID.getStatus());
            //記錄支付紀錄檔
            paymentInfoService.savePaymentInfo(orderInfo, transaction);

            //通知醫院修改訂單狀態
            //組裝引數
            HashMap<String, Object> paramsMap = new HashMap<>();
            paramsMap.put("hoscode", orderInfo.getHoscode());
            paramsMap.put("hosOrderId", orderInfo.getHosOrderId());
            paramsMap.put("timestamp", HttpRequestHelper.getTimestamp());

            paramsMap.put("sign", HttpRequestHelper.getSign(paramsMap, "8af52af00baf6aec434109fc17164aae"));
            //傳送請求
            JSONObject jsonResult = HttpRequestHelper.sendRequest(paramsMap, "http://localhost:9998/order/updatePayStatus");
            //解析響應
            if(jsonResult.getInteger("code") != 200) {
                log.error("查單失敗,"
                          + "code:" + jsonResult.getInteger("code")
                          + ",message:" + jsonResult.getString("message")
                         );
                throw new GuiguException(ResultCodeEnum.FAIL.getCode(), jsonResult.getString("message"));
            }

            //返回支付結果
            return true;//支付成功
        }

        return false;//支付中

    } catch (HttpException e) { // 傳送HTTP請求失敗
        // 呼叫e.getHttpRequest()獲取請求列印紀錄檔或上報監控,更多方法見HttpException定義
        log.error(e.getHttpRequest().toString());
        throw new GuiguException(ResultCodeEnum.FAIL);
    } catch (ServiceException e) { // 服務返回狀態小於200或大於等於300,例如500
        // 呼叫e.getResponseBody()獲取返回體列印紀錄檔或上報監控,更多方法見ServiceException定義
        log.error(e.getResponseBody());
        throw new GuiguException(ResultCodeEnum.FAIL);
    } catch (MalformedMessageException e) { // 服務返回成功,返回體型別不合法,或者解析返回體失敗
        // 呼叫e.getMessage()獲取資訊列印紀錄檔或上報監控,更多方法見MalformedMessageException定義
        log.error(e.getMessage());
        throw new GuiguException(ResultCodeEnum.FAIL);
    }
}

2、前端整合

2.1、修改request.js

utils/request.js的響應攔截器中增加對250狀態的判斷

}else if (response.data.code !== 200) {
    
修改為

}else if (response.data.code !== 200 && response.data.code !== 250) {

2.2、api

在api/wxpay.js中新增的方法

//查詢訂單
queryPayStatus(outTradeNo) {
    return request({
        url: `/front/order/wxpay/queryPayStatus/${outTradeNo}`,
        method: 'get'
    })
},

3.3、show.vue輪詢查單

js中修改pay方法:每隔3秒查單

新增queryPayStatus方法

完善closeDialog方法:停止定時器

//發起支付
pay(){
  if(this.isPayShow) return
  this.isPayShow = true
  this.payText = '支付中.....'
    
  wxpayApi.nativePay(this.orderInfo.outTradeNo).then((response) => {
    this.codeUrl = response.data
    this.dialogPayVisible = true
      
    //每隔3秒查單
    this.timer = setInterval(()=>{
      console.log('輪詢查單:' + new Date())
      this.queryPayStatus()
    }, 3000)
  })
},
    
//查詢訂單狀態
queryPayStatus(){
  wxpayApi.queryPayStatus(this.orderInfo.outTradeNo).then(response => {
    if(response.code == 250){
        return
    }
    //清空定時器
    clearInterval(this.timer)
    window.location.reload()
  })
},

//關閉對話方塊
closeDialog(){
    //停止定時器
    clearInterval(this.timer)
    //恢復支付按鈕
    this.isPayShow = false
    this.payText = '支付'
}

3、查單應答超時

3.1、測試應答超時

//應答超時
//設定響應超時,支付成功後沒有及時響應,可能繼續輪詢查單,此時資料庫會記錄多餘的支付紀錄檔
try {
    TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}


//返回支付結果
return true;//支付成功

3.2、處理重複通知

支付成功後,判斷訂單狀態:

OrderInfo orderInfo = orderInfoService.selectByOutTradeNo(outTradeNo);
//處理支付成功後重複查單
//保證介面呼叫的冪等性:無論介面被呼叫多少次,產生的結果是一致的
Integer orderStatus = orderInfo.getOrderStatus();
if (OrderStatusEnum.UNPAID.getStatus().intValue() != orderStatus.intValue()) {
    return true;//支付成功、關單、。。。
}

//更新訂單狀態
//記錄支付紀錄檔
......