1

网站接入微信支付后如何实现退款和取消预约? - 自律即自由-

 1 year ago
source link: https://www.cnblogs.com/deyo/p/17495586.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

取消预约分两种情况:

  • 未支付取消订单,直接通知医院取消预约状态并更新相关数据,然后修改平台订单状态
  • 已支付取消订单,退款给用户并在数据库中记录退款记录,通知医院取消预约状态并更新相关数据,然后修改平台订单状态

第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

注意:此步骤只是申请退款,具体退款是否成功,要通过退款查询接口获取,或通过退款回调通知获取。

image-20230318152550929

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

image-20230301042206981

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

证书和回调报文解密-接口规则

image-20230320135113609

签名验证-接口规则

image-20230320120403904

代码参考:GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library

image-20230320134319244

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK