網站接入微信支付後如何實現退款和取消預約?

2023-06-21 12:02:44

需求

取消預約分兩種情況:

  • 未支付取消訂單,直接通知醫院取消預約狀態並更新相關資料,然後修改平臺訂單狀態
  • 已支付取消訂單,退款給使用者並在資料庫中記錄退款記錄,通知醫院取消預約狀態並更新相關資料,然後修改平臺訂單狀態

第01章-未支付取消預約

1、後端介面

1.1、Controller

FrontOrderInfoController中新增介面方法

@ApiOperation("取消預約")
@ApiImplicitParam(name = "outTradeNo",value = "訂單id", required = true)
@GetMapping("/auth/cancelOrder/{outTradeNo}")
public Result cancelOrder(@PathVariable("outTradeNo") String outTradeNo, HttpServletRequest request, HttpServletResponse response) {

    authContextHolder.checkAuth(request, response);
    orderInfoService.cancelOrder(outTradeNo);
    return Result.ok().message("預約已取消");
}

1.2、Service

在OrderStatusEnum增加兩個狀態:

CANCLE_UNREFUND(-2,"取消預約,退款中"),
CANCLE_REFUND(-3,"取消預約,已退款"),

介面:OrderInfoService

/**
     * 根據訂單號取消訂單
     * @param outTradeNo
     */
void cancelOrder(String outTradeNo);

實現:OrderInfoServiceImpl

@Override
public void cancelOrder(String outTradeNo) {

    //獲取訂單
    OrderInfo orderInfo = this.selectByOutTradeNo(outTradeNo);
     //當前時間大於退號時間,不能取消預約
        DateTime quitTime = new DateTime(orderInfo.getQuitTime());
        if (quitTime.isBeforeNow()) {
            throw new GuiguException(ResultCodeEnum.CANCEL_ORDER_NO);
        }

    
    //呼叫醫院端介面,同步資料
    Map<String, Object> params = new HashMap<>();
    params.put("hoscode", orderInfo.getHoscode());
    params.put("hosOrderId", orderInfo.getHosOrderId());
    params.put("hosScheduleId", orderInfo.getHosScheduleId());
    params.put("timestamp", HttpRequestHelper.getTimestamp());
    params.put("sign", HttpRequestHelper.getSign(params, "8af52af00baf6aec434109fc17164aae"));
    JSONObject jsonResult = HttpRequestHelper.sendRequest(params, "http://localhost:9998/order/updateCancelStatus");

    if(jsonResult.getInteger("code") != 200) {
        throw new GuiguException(ResultCodeEnum.CANCEL_ORDER_FAIL);
    }

    //是否支付
    if (orderInfo.getOrderStatus().intValue() == OrderStatusEnum.PAID.getStatus().intValue()) {

       
        //已支付,則退款
        log.info("退款");
        //wxPayService.refund(outTradeNo);
        
        //更改訂單狀態
        this.updateStatus(outTradeNo, OrderStatusEnum.CANCLE_UNREFUND.getStatus());
    }else{
        //更改訂單狀態
        this.updateStatus(outTradeNo, OrderStatusEnum.CANCLE.getStatus());
    }

    //TODO 根據醫院返回資料,更新排班數量
    //TODO 給就診人傳送簡訊

}

2、前端整合

2.1、api

orderInfo.js中新增方法

//取消預約
cancelOrder(outTradeNo) {
    return request({
        url: `/front/order/orderInfo/auth/cancelOrder/${outTradeNo}`,
        method: 'get'
    })
},

2.2、頁面

order/show.vue中新增方法

//取消預約方法
cancelOrder() {
    this.$confirm('確定取消預約嗎?', '提示', {
        confirmButtonText: '確定',
        cancelButtonText: '取消',
        type: 'warning',
    }).then(() => {
        // 點選確定,遠端呼叫
        orderInfoApi.cancelOrder(this.orderInfo.outTradeNo).then((response) => {
            this.$message.success('取消成功')
            this.init()
        })
    })
},

第02章-已支付取消預約

1、申請退款

1.1、參考檔案

參考檔案:申請退款API

注意:此步驟只是申請退款,具體退款是否成功,要通過退款查詢介面獲取,或通過退款回撥通知獲取。

退款SDK:wechatpay-java/service/src/example/java/com/wechat/pay/java/service/refund at main · wechatpay-apiv3/wechatpay-java · GitHub

1.2、呼叫退款業務

OrderInfoServiceImpl

@Resource
private WxPayService wxPayService;
//已支付,則退款
log.info("退款");
wxPayService.refund(outTradeNo);

1.3、退款申請

介面:WxPayService

/**
     * 退款
     * @param outTradeNo
     */
void refund(String outTradeNo);

實現:WxPayServiceImpl

@Resource
private RefundInfoService refundInfoService;

@Override
public void refund(String outTradeNo) {

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

    // 呼叫介面
    try {

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

        CreateRequest request = new CreateRequest();
        // 呼叫request.setXxx(val)設定所需引數,具體引數可見Request定義
        request.setOutTradeNo(outTradeNo);
        request.setOutRefundNo("TK_" + outTradeNo);
        AmountReq amount = new AmountReq();
        //amount.setTotal(orderInfo.getAmount().multiply(new BigDecimal(100)).intValue());
        amount.setTotal(1L);//1分錢
        amount.setRefund(1L);
        amount.setCurrency("CNY");
        request.setAmount(amount);
        // 呼叫介面
        Refund response = service.create(request);

        Status status = response.getStatus();

        //            SUCCESS:退款成功(退款申請成功)
        //            CLOSED:退款關閉
        //            PROCESSING:退款處理中
        //            ABNORMAL:退款異常
        if(Status.CLOSED.equals(status)){

            throw new GuiguException(ResultCodeEnum.FAIL.getCode(), "退款已關閉,無法退款");

        }else if(Status.ABNORMAL.equals(status)){

            throw new GuiguException(ResultCodeEnum.FAIL.getCode(), "退款異常");

        } else{
			//SUCCESS:退款成功(退款申請成功) || PROCESSING:退款處理中
            //記錄支退款紀錄檔
            refundInfoService.saveRefundInfo(orderInfo, response);
        }

    } 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);
    }
}

1.4、記錄退款記錄

介面:RefundInfoService

/**
     * 儲存退款記錄
     * @param orderInfo
     * @param response
     */
void saveRefundInfo(OrderInfo orderInfo, Refund response);

實現:RefundInfoServiceImpl

@Override
public void saveRefundInfo(OrderInfo orderInfo, Refund response) {

    // 儲存退款記錄
    RefundInfo refundInfo = new RefundInfo();
    refundInfo.setOutTradeNo(orderInfo.getOutTradeNo());
    refundInfo.setOrderId(orderInfo.getId());
    refundInfo.setPaymentType(PaymentTypeEnum.WEIXIN.getStatus());
    refundInfo.setTradeNo(response.getOutRefundNo());
    refundInfo.setTotalAmount(new BigDecimal(response.getAmount().getRefund()));
    refundInfo.setSubject(orderInfo.getTitle());
    refundInfo.setRefundStatus(RefundStatusEnum.UNREFUND.getStatus());//退款中
    baseMapper.insert(refundInfo);
}

2、退款回撥

前面我們已經申請了退款,具體退款是否成功,要通過退款查詢介面獲取,或通過退款回撥通知獲取。這裡我們學習退款通知如何實現。

2.1、內網穿透

資料:資料>微信支付>小米球ngrok.rar

參考小米球使用教學開通內網穿透服務,獲取內網穿透服務地址

2.2、設定回撥地址

將組態檔中的開發引數wxpay.notify-refund-url主機地址部分修改為自己的內網穿透地址,例如:

#退款通知回撥地址:申請退款是提交這個引數
wxpay.notify-refund-url=http://agxnyzl04y90.ngrok.xiaomiqiu123.top/api/order/wxpay/refunds/notify

2.3、設定請求引數

呼叫退款申請API時新增引數notify-refund-url引數。

WxPayServiceImpl類中的refund方法中新增如下引數:

request.setNotifyUrl(wxPayConfig.getNotifyRefundUrl());

2.4、更新退款狀態

介面:RefundInfoService

/**
 * 更新退款狀態
 * @param refundNotification
 * @param refund
 */
void updateRefundInfoStatus(RefundNotification refundNotification, RefundStatusEnum refund);

實現:RefundInfoServiceImpl

@Override
public void updateRefundInfoStatus(RefundNotification refundNotification, RefundStatusEnum refundStatus) {
    LambdaQueryWrapper<RefundInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(RefundInfo::getOutTradeNo, outTradeNo);
    RefundInfo refundInfo = new RefundInfo();
    refundInfo.setRefundStatus(refundStatus.getStatus());
    refundInfo.setCallbackContent(refundNotification.toString());
    refundInfo.setCallbackTime(new Date());
    baseMapper.update(refundInfo, queryWrapper);
}

2.5、引入依賴

<!--回撥驗籤程式碼需要-->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.14.9</version>
</dependency>

2.6、引入工具類

將資料目錄中的請求引數工具放入service-order微服務的utils包中

資料:資料>微信支付>RequestUtils.java

證書和回撥報文解密-介面規則

簽名驗證-介面規則

程式碼參考:GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library

2.7、開發回撥介面

建立controller.api包,建立ApiWXPayController類:

解析回撥引數、驗籤、請求內容解密、獲取退款結果、記錄退款紀錄檔

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

/**
 * 接收微信傳送給伺服器的遠端回撥
 */
@Api(tags = "微信支付介面")
@Controller
@RequestMapping("/api/order/wxpay")
@Slf4j
public class ApiWXPayController {

    @Resource
    private RefundInfoService refundInfoService;

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private RSAAutoCertificateConfig rsaAutoCertificateConfig;

    /**
     * 退款結果通知
     * 退款狀態改變後,微信會把相關退款結果傳送給商戶。
     */
    @PostMapping("/refunds/notify")
    public String callback(HttpServletRequest request, HttpServletResponse response){

        log.info("退款通知執行");

        Map<String, String> map = new HashMap<>();//應答物件

        try {

             /*使用回撥通知請求的資料,構建 RequestParam。
            HTTP 頭 Wechatpay-Signature
            HTTP 頭 Wechatpay-Nonce
            HTTP 頭 Wechatpay-Timestamp
            HTTP 頭 Wechatpay-Serial
            HTTP 頭 Wechatpay-Signature-Type
            HTTP 請求體 body。切記使用原始報文,不要用 JSON 物件序列化後的字串,避免驗籤的 body 和原文不一致。*/
            // 構造 RequestParam
            String signature = request.getHeader("Wechatpay-Signature");
            String nonce = request.getHeader("Wechatpay-Nonce");
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            String wechatPayCertificateSerialNumber = request.getHeader("Wechatpay-Serial");

            //請求體
            String requestBody = RequestUtils.readData(request);

            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(wechatPayCertificateSerialNumber)
                    .nonce(nonce)
                    .signature(signature)
                    .timestamp(timestamp)
                    .body(requestBody)
                    .build();

            // 初始化 NotificationParser
            NotificationParser parser = new NotificationParser(rsaAutoCertificateConfig);

            // 驗籤、解密並轉換成 Transaction
            RefundNotification refundNotification = parser.parse(requestParam, RefundNotification.class);

            String orderTradeNo = refundNotification.getOutTradeNo();
            Status refundStatus = refundNotification.getRefundStatus();

            if("SUCCESS".equals(refundStatus.toString())){
                log.info("更新退款記錄:已退款");
                //退款狀態
                refundInfoService.updateRefundInfoStatus(refundNotification, RefundStatusEnum.REFUND);
                //訂單狀態
                orderInfoService.updateStatus(orderTradeNo, OrderStatusEnum.CANCLE_REFUND.getStatus());
            }

            //成功應答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            return JSONObject.toJSONString(map);

        } catch (Exception e) {

            log.error(ExceptionUtils.getStackTrace(e));

            //失敗應答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失敗");
            return JSONObject.toJSONString(map);
        }
    }
}