RocketMQ异步下单+对接支付宝接口完成支付+支付宝通知处理

摘要——异步下单和支付的流程

       订单支付模块是一个系统中最核心也是最重要的模块,同时也是容易被高并发冲垮的模块,在用户量小的系统中,可以直接通过订单服务去调用支付服务,但是在用户群体大的系统中,我们需要引入MQ去做异步调用、流量消峰

整体流程

1.用户在商品页面点击购买按钮,前端发送请求到后台,获取一个本次订单的唯一token返回给前端【就是防重token,防止用户多次点击造成数据错乱】

2.前端拿到后即跳转到订单详情界面,此界面为了展示用户已选的商品和总金额,后台需要根据商品的id去查询商品的信息和每个商品的价格以及总金额返回给前端做回显,这里前端界面需要设置一个定时器提示用户支付最晚支付时间,后端通过rocketMQTemplate.syncSend向mq发送一个延迟消息,本身的订单服务和外部的支付服务作为两个消费者,如果超时检查订单仍为待支付,则修改订单服务和支付服务的订单状态为已取消,同时支付服务要关闭订单交易【Factory.Payment.Common().close(payOrder.getOrderNo)】,为防止意外,在支付宝回调通知中我们要去判断这个支付单的状态,如果支付单为已取消,则直接退款【Factory.Payment.Common().refund(payOrder.getOrderNo,dto.getTotal_amout())】,返回success给支付宝

3.用户点击下单,将携带token和商品信息以及价格数量等信息到后台,后台接收后就去预创建订单,因为引进了MQ,所以本地不做数据库的写操作,而是将这个操作交给MQ去执行,本地只需要将必须的订单属性封装好发送事务消息给MQ即可,同时也要将消息的主题message封装好一起发送过去,用于支付模块消费者获取到去做新增支付订单操作,事务监听器监听到后获取Object的数据,JSON转换为对象后调用业务方法进行保存订单数据,使用try-cache包裹,如果没有发生异常则是直接提交本地事务,MQ会携带message将事务消息发送到MQ队列中,然后支付服务消费者去监听MQ队列的某个topic完成支付订单的添加

4.只要消息发送成功,则会提示用户正在下单,前端定时器轮询去数据库中查询支付服务的支付订单,查询到后后端将单号返回给前端

5.前端获取到后携带单号以及一些必要的属性向后端支付接口发起调用,支付服务去调用支付宝的支付接口,返回支付宝给的form表单给前端,剩下的交给支付宝去操作了

6.支付完成后通过回调地址return_url作为支付完成或取消后的跳转页面,如果没有指定回调地址,则会跳转至支付宝的默认回调地址

7.支付宝同时会根据notify_url对支付结果进行通知,最大努力通知8次,在没有上线的时候,支付宝是外网访问我们的内网,要用到natapp去穿透内网,让支付宝能访问到我们的接口进行通知,支付宝通知时会返回一大堆参数,总共有23个,主要有签名、支付金额、消息主体、订单号、支付宝自己的订单号、交易状态、签名方式等,我们通过dto去接收这些参数,然后去做验签,验签成功判断支付状态是否成功,金额是否正确、订单号是否匹配等校验,校验不通过返回fail让支付宝重新发送通知,校验成功则做业务处理:修改支付订单状态、添加支付流水、为保证支付服务和课程服务以及订单服务的一致性,使用mq发送事务消息让mq去操作本地事务

8.成功后发送message到mq队列,以广播的形式让课程服务和订单服务去消费,修改订单状态和添加course_user_learn记录,如果购买过,增加总时长,没有则新增记录,在mq成功执行本地事务并发送事务消息后为支付宝返回success,让它别再通知我们了,整个下单支付流程就结束了

一:RcoketMQ事务消息异步下单

       通过RocketMQ的事务消息,我们去保证本地事务与发送消息的原子性,用户点击下单,后端通过订单服务预创建订单,然后将订单数据封装进dto发送事务消息,需要携带的参数有:txProducerGroup【消息的分组】、destination【消息的topic和tag,用:隔开】、message【发送给消费者的消息内容】、arg【扩展参数,将封装好的dto放到这儿传给MQ让MQ去执行本地事务的操作】,然后事务监听器通过注解@RocketMQTransactionListener的txProducerGroup属性去监听某一消息组,监听器类实现RocketMQLocalTransactionListener接口,重写两个方法,分别为执行本地事务操作的方法和事务回调的方法,在执行本地事务的方法中,用try-catch包裹代码,通过强转参数Obejct,调用业务层方法将这个参数携带过去完成添加,返回RocketMQLocalTransactionState.COMMIT,cache代表业务方法执行失败,则返回回滚ROLLBACK,在回调方法中,主要通过传过来的message与订单中一致的订单号去数据库中查询,如果查到这个数据则代表本地事务执行成功,提交,反之回滚,此方法一般不会被调用,除非MQ很久没有响应,本地事务执行成功后MQ会携带message发送消息到MQ队列中,然后支付服务创建一个消费者,通过@RocketMQMessageListener去创建一个消费组、指定监听的topic和tag以及消费模式,一般使用集群,一个只允许被消费一次,消费者去实现RocketMQListener,指定泛型MessageExt,然后在重写的方法里面通过携带过来的message去保存支付订单,需要注意的是,这里要去保证消息的幂等性,所以要先去判断数据库中是否已经创建了此订单,然后再去调用业务方法保存订单,这样就完成了MQ的异步下单

1.1:主业务代码,订单服务发送事务消息到MQ:

package cn.ybl.service.impl;

import cn.ybl.domain.CourseOrder;
import cn.ybl.domain.CourseOrderItem;
import cn.ybl.dto.CourseOrder2PayOrder;
import cn.ybl.dto.CourseOrderDto;
import cn.ybl.enums.GlobalErrorCode;
import cn.ybl.feign.CourseFeign;
import cn.ybl.mapper.CourseOrderMapper;
import cn.ybl.result.JSONResult;
import cn.ybl.service.ICourseOrderItemService;
import cn.ybl.service.ICourseOrderService;
import cn.ybl.util.AssertUtil;
import cn.ybl.util.CodeGenerateUtils;
import cn.ybl.vo.CourseOrderVo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yang
 * @since 2022-09-16
 */
@Service
public class CourseOrderServiceImpl extends ServiceImpl<CourseOrderMapper, CourseOrder> implements ICourseOrderService {

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Autowired
    private CourseFeign courseFeign;

    @Autowired
    private ICourseOrderItemService courseOrderItemService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 前端下单,保存订单信息
     */
    @Override
    public JSONResult placeOrder(CourseOrderDto courseOrderDto) {
        Long loginId = 3l;  //登录用户id暂时写死
        String courseIds = StringUtils.join(courseOrderDto.getCourseIds(),",");
        //1.判断防重token是否正确
        String key = loginId + ":" + courseIds + ":" +"token";
        Object tokenRedis = redisTemplate.opsForValue().get(key);
        AssertUtil.isNotNull(tokenRedis,GlobalErrorCode.TOKEN_EMPTY_ERROR);
        AssertUtil.isEquals(courseOrderDto.getToken(),tokenRedis.toString(),GlobalErrorCode.TOKEN_ERROR);
        //调用feign接口,查询当前下单的课程的信息和价格等信息
        JSONResult orderInfo = courseFeign.getOrderInfo(courseIds);
        AssertUtil.isNotNull(orderInfo.getData(),GlobalErrorCode.COURSE_SERVER_ERROR);
        //将返回结果转为原来的对象,这样做是为了传给保存订单和保存详情两个方法,避免查询两次
        CourseOrderVo courseOrderVo = JSON.parseObject(JSONObject.toJSONString(orderInfo.getData()), CourseOrderVo.class);
        //2.校验通过,保存数据到订单主表
        CourseOrder courseOrder = saveOrder(courseOrderDto,courseOrderVo);
        //3.保存数据到订单明细表【保存后要将结果设置到主表的title字段】
        CourseOrder courseOrderAndItem = saveDeatil(courseOrder, courseOrderVo);
        //使用MQ发送事务消息,完成MQ的异步下单(MQ需要保证本地事务执行和发送消息的原子性,所以本地事务的执行应交给MQ来完成)
        Message<CourseOrder2PayOrder> message = MessageBuilder.withPayload(new CourseOrder2PayOrder(
                courseOrder.getTotalAmount(),
                courseOrder.getPayType(),
                courseOrder.getOrderNo(),
                courseOrder.getUserId(),
                courseOrder.getTitle()
        )).build();
        //发送事务消息
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
                "txCourseOrder",  //消息的分组
                "order-topic:order-tag",  //消息的topic和tag
                message,  //消息主体,用于生成支付服务的支付单的必备参数,同时可以通过订单号检查本地事务回调,MQ需要将这个消息发送给pay服务
                courseOrderAndItem  //扩展参数,用于让MQ执行本地事务,将订单详情用list封装进主订单,再将主订单通过arg发送给消息监听器去执行添加订单操作
        );
        //获取本地事务执行状态
        LocalTransactionState localTransactionState = result.getLocalTransactionState();
        //获取消息发送状态
        SendStatus sendStatus = result.getSendStatus();
        Boolean isTrue = localTransactionState == LocalTransactionState.COMMIT_MESSAGE || sendStatus!= SendStatus.SEND_OK;
        AssertUtil.isTrue(isTrue,GlobalErrorCode.CREATE_ORDER_ERROR);
        //消息发送成功则删除redis中的token
        redisTemplate.delete(key);
        //将订单号传给前端
        return JSONResult.success(courseOrder.getOrderNo());
    }

    /**
     * RocketMQ异步下单,执行本地事务,最终的添加订单和详情是由MQ调此方法实现
     * @param order
     */
    @Override
    @Transactional
    public void saveCourseOrderAndItem(CourseOrder order) {
        List<CourseOrderItem> courseOrderItems = order.getCourseOrderItems();  //订单详情
        //保存主订单
        insert(order);
        //循环保存订单详情
        courseOrderItems.forEach(e->{
            e.setOrderId(order.getId());
            courseOrderItemService.insert(e);
        });
    }

    //封装订单明细表的数据
    private CourseOrder saveDeatil(CourseOrder courseOrder,CourseOrderVo courseOrderVo) {
        StringBuffer title = new StringBuffer();
        title.append("购买课程:【");
        //循环,传了多个课程就要保存多条记录
        List<cn.ybl.vo.CourseOrder> courseInfos = courseOrderVo.getCourseInfos();
        for (cn.ybl.vo.CourseOrder order:courseInfos){
            CourseOrderItem courseOrderItem = new CourseOrderItem();
            courseOrderItem.setOrderNo(courseOrder.getOrderNo());  //订单id
            courseOrderItem.setAmount(courseOrder.getTotalAmount());  //价格
            courseOrderItem.setCount(courseOrder.getTotalCount());  //数量
            courseOrderItem.setCreateTime(new Date());
            courseOrderItem.setCourseId(order.getCourse().getId());
            courseOrderItem.setCoursePic(order.getCourse().getPic());
            courseOrderItem.setCourseName(order.getCourse().getName());
            courseOrderItem.setVersion(0);
            //将订单明细数据封装进订单主表的list,因为一个主表可以对应多个订单明细
            courseOrder.getCourseOrderItems().add(courseOrderItem);
            title.append(order.getCourse().getName());
        }
        title.append("\"】,支付【"+courseOrder.getTotalAmount()+"】元");
        courseOrder.setTitle(title.toString());
        return courseOrder;
    }

    //封装订单主表的数据
    private CourseOrder saveOrder(CourseOrderDto courseOrderDto, CourseOrderVo courseOrderVo) {
        //下单课程id【1,2,3......】
        List<Long> courseIds = courseOrderDto.getCourseIds();
        String courseIdsArr = StringUtils.join(courseIds, ",");
        //支付方式
        Integer payType = courseOrderDto.getPayType();
        //普通订单
        Integer type = courseOrderDto.getType();
        CourseOrder courseOrder = new CourseOrder();
        courseOrder.setCreateTime(new Date()); // 订单创建时间
        courseOrder.setOrderNo(CodeGenerateUtils.generateOrderSn(3));  //根据用户id生成订单编号
        courseOrder.setTotalAmount(courseOrderVo.getTotalAmount()); //支付总金额
        courseOrder.setTotalCount(courseIdsArr.length());  //下单数量
        courseOrder.setStatusOrder(0);  //订单状态,下单成功待支付
        courseOrder.setUserId(3l);  //用户id
        courseOrder.getTitle();  //标题【需要先保存订单详情后将订单详情所有数据拼接起来】
        courseOrder.setVersion(0);
        courseOrder.setPayType(payType);
        return courseOrder;
    }
}

1.2:MQ事务监听器【相当于生产者】

package cn.ybl.mq;

import cn.ybl.domain.CourseOrder;
import cn.ybl.dto.CourseOrder2PayOrder;
import cn.ybl.enums.GlobalErrorCode;
import cn.ybl.service.ICourseOrderService;
import cn.ybl.util.AssertUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;

import java.nio.charset.StandardCharsets;

/**
 * @Author Mr.Yang
 * @Date 2022/9/18 14:15
 * @description
 */
@RocketMQTransactionListener(txProducerGroup = "txCourseOrder")
public class CourseOrderListener implements RocketMQLocalTransactionListener {

    @Autowired
    private ICourseOrderService orderService;

    /**
     * 执行本地事务
     * @param message
     * @param o
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            AssertUtil.isNotNull(o, GlobalErrorCode.PARAM_IS_NULL);
            CourseOrder order = (CourseOrder) o;
            //将此对象传给courseOrder业务去保存主订单和详情
            orderService.saveCourseOrderAndItem(order);
            //本地事务执行成功,提交消息到MQ,让支付模块消费者去监听消息,获取到message保存支付订单
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            e.printStackTrace();
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 事务回调,检查本地事务是否成功
     * @param message
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        AssertUtil.isNotNull(message,GlobalErrorCode.PARAM_IS_NULL);
        String str = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);
        CourseOrder2PayOrder courseOrder2PayOrder = JSONObject.parseObject(str, CourseOrder2PayOrder.class);
        EntityWrapper<CourseOrder> wrapper = new EntityWrapper<>();
        wrapper.eq("order_no",courseOrder2PayOrder.getOrderNo());
        CourseOrder order = orderService.selectOne(wrapper);
        if(order==null){  //本地事务执行失败,回滚
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        //本地事务执行成功,提交
        return RocketMQLocalTransactionState.COMMIT;
    }
}

1.3:支付服务的消费者

package cn.ybl.mq;

import cn.ybl.domain.PayOrder;
import cn.ybl.dto.CourseOrder2PayOrder;
import cn.ybl.result.JSONResult;
import cn.ybl.service.IPayOrderService;
import cn.ybl.service.IPayService;
import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * @Author Mr.Yang
 * @Date 2022/9/18 20:09
 * @description 消息消费者,通过订单服务发送的消息携带的message参数保存支付订单
 */
@Component
@RocketMQMessageListener(
        consumerGroup = "service-pay-consumer",
        topic = "order-topic",
        selectorExpression = "order-tag",
        messageModel = MessageModel.CLUSTERING)
public class PayOrderConsumer implements RocketMQListener<MessageExt> {

    @Autowired
    private IPayOrderService payOrderService;

    @Autowired
    private IPayService payService;

    /**
     * MQ是自动签收机制,只要方法不抛出异常,MQ就认为消费成功了,如果抛出异常,MQ会再次投递消息
     * 需要保证消息的幂等性,即一个消息只能被消费一次
     * @param messageExt
     */
    @Override
    public void onMessage(MessageExt messageExt) {
        if(messageExt.getBody()==null){
            return;
        }
        String message = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        //转为我们封装的dto
        CourseOrder2PayOrder courseOrder2PayOrder = JSON.parseObject(message,CourseOrder2PayOrder.class);
        //保存支付订单操作,需要先保证消息的幂等性
        PayOrder payOrder = payService.selectOrderNo(courseOrder2PayOrder.getOrderNo());
        if(payOrder!=null){  //如果订单已经存在,直接return,以后不再推送此消息
            return;
        }
        payOrderService.saveOrder(courseOrder2PayOrder);
    }
}

二:上面MQ的异步下单,完成了支付模块的订单创建,前端此时获取到支付订单的订单号,需要发送请求去调用支付宝的接口

2.1引入依赖

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-easysdk</artifactId>
    <version>2.2.2</version>
</dependency>

2.2控制层

/**
 * 支付申请
 */
@PostMapping("/apply")
public JSONResult apply(@RequestBody PayApplyDto payApplyDto){
    return JSONResult.success(payService.apply(payApplyDto));
}

2.3业务层【直接拷贝支付宝开放平台sdk中Java版的easy版即可】

package cn.ybl.service.impl;

import cn.ybl.domain.AlipayInfo;
import cn.ybl.domain.PayFlow;
import cn.ybl.domain.PayOrder;
import cn.ybl.dto.AlipayNotifyDto;
import cn.ybl.dto.Pay2Order2CourseDto;
import cn.ybl.dto.PayApplyDto;
import cn.ybl.enums.GlobalErrorCode;
import cn.ybl.service.IAlipayInfoService;
import cn.ybl.service.IPayOrderService;
import cn.ybl.service.IPayService;
import cn.ybl.util.AssertUtil;
import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.kernel.Config;
import com.alipay.easysdk.kernel.util.ResponseChecker;
import com.alipay.easysdk.payment.common.models.AlipayTradeCloseResponse;
import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.Date;

/**
 * @Author Mr.Yang
 * @Date 2022/9/18 20:28
 * @description
 */
@Service
public class PayServiceImpl implements IPayService {

    @Autowired
    private IPayOrderService payOrderService;

    @Autowired
    private IAlipayInfoService alipayInfoService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 查询是否已经将MQ中的消息消费【保存支付订单,前端要根据支付订单查询是否已经创建】
     * @param orderNo
     * @return
     */
    @Override
    public PayOrder selectOrderNo(String orderNo) {
        EntityWrapper<PayOrder> wrapper = new EntityWrapper<>();
        wrapper.eq("order_no",orderNo);
        PayOrder payOrder = payOrderService.selectOne(wrapper);
        return payOrder;
    }

    /**
     * 支付申请
     */
    @Override
    public String apply(PayApplyDto payApplyDto) {
        //根据订单编号查询预创订单
        Wrapper<PayOrder> wrapper = new EntityWrapper();
        wrapper.eq("order_no", payApplyDto.getOrderNo());
        PayOrder payOrder = payOrderService.selectOne(wrapper);
        //支付有很多种,1为支付宝,2为微信,3为银联
        if(payApplyDto.getPayType() == 1){
            return alipay(payApplyDto,payOrder);
        }else{
            //..........
        }
        return null;
    }

    /**
     * 用户支付后,支付宝根据notify_url发起异步回调通知,告诉我们用户支付状态,我们再去做相关业务
     * 处理支付订单和处理服务订单以及修改课程服务的购买记录需要同时成功,为了应对高并发,使用MQ事务消息
     * 1.验证签名【是不是支付宝给我们发的】
     * 2.新增支付流水
     * 3.修改支付订单状态、修改时间
     * 4.调用订单服务修改订单状态
     * 5.添加/修改课程服务的表course_user_learn
     * @param dto
     * @return
     */
    @Override
    public String notify(AlipayNotifyDto dto) {
        //根据回调返回的单号查询订单
        PayOrder payOrder = selectOrderNo(dto.getOut_trade_no());
        //1.对支付结果作相关判断
        confirmPayResult(dto,payOrder);
        try {
            //如果订单已经取消,则退款
            if(payOrder.getPayStatus()==2){
                // @TODO
                Factory.Payment.Common().refund(payOrder.getOrderNo(),dto.getTotal_amount());
                return "success";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //2.修改支付订单支付状态,修改时间
        PayOrder updatePayOrder = updatePayOrder(payOrder);
        //3.新增支付流水
        PayOrder payOrderAndPayFlow = insertPayFlow(dto, updatePayOrder);
        //封装发送的消息主体
        Message<Pay2Order2CourseDto> message = MessageBuilder.withPayload(new Pay2Order2CourseDto(
                payOrder.getOrderNo(),
                payOrder.getExtParams()
        )).build();
        //发送事务消息到MQ,需要保证支付服务和订单服务以及课程服务的数据库一致性
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
                "tx-pay-service",
                "topic-pay:tag-pay",
                message,
                payOrderAndPayFlow
        );
        //检查消息是否发送成功
        SendStatus sendStatus = result.getSendStatus();
        //获取本地事务执行状态
        LocalTransactionState localTransactionState = result.getLocalTransactionState();
        Boolean b = localTransactionState == LocalTransactionState.COMMIT_MESSAGE || sendStatus==SendStatus.SEND_OK;
        AssertUtil.isTrue(b,GlobalErrorCode.PARAM_IS_NULL);  //让支付宝重新通知我们
        //执行成功发送success给支付宝
        return "success";
    }

    /**
     * 订单支付超时,取消订单并关闭交易,来自mq的延迟消息
     * @param orderNo
     */
    @Override
    public void cancelPayOrder(String orderNo) {
        try {
            Wrapper<PayOrder> wrapper = new EntityWrapper<>();
            wrapper.eq("order",orderNo);
            PayOrder payOrder = payOrderService.selectOne(wrapper);
            if(payOrder==null){
                return;
            }
            //修改订单状态,不管支付没支付都取消,回调判断退款即可
            payOrder.setPayStatus(2);
            payOrderService.updateById(payOrder);
            //告诉支付宝关闭交易
            Factory.Payment.Common().close(payOrder.getOrderNo());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
     * 新增支付流水,不做新增操作,返回即可,等下发给MQ去执行
     * @param dto
     */
    private PayOrder insertPayFlow(AlipayNotifyDto dto,PayOrder payOrder) {
        PayFlow payFlow = new PayFlow();
        payFlow.setNotifyTime(new Date());
        payFlow.setSubject(dto.getSubject());
        payFlow.setOutTradeNo(dto.getOut_trade_no());
        payFlow.setTotalAmount(new BigDecimal(dto.getTotal_amount()));
        payFlow.setTradeStatus("TRADE_SUCCESS");
        payFlow.setPaySuccess(true);
        payOrder.setPayFlow(payFlow);
        return payOrder;
    }

    /*
     * 修改支付订单的修改时间、支付状态,不做修改操作,返回即可,等下发给MQ去执行
     * @param payOrder
     */
    private PayOrder updatePayOrder(PayOrder payOrder) {
        //如果订单已经为已支付,则报错回滚
        payOrder.setPayStatus(1);
        payOrder.setUpdateTime(new Date());
        return payOrder;
    }

    /*
     * 对支付结果进行相关判断
     * @param dto
     */
    private void confirmPayResult(AlipayNotifyDto dto,PayOrder payOrder) {
        AssertUtil.isNotNull(payOrder,GlobalErrorCode.PARAM_IS_NULL);  //随便抛一个错,控制层返回fail
        //支付金额是否正确
        AssertUtil.isEquals(payOrder.getAmount(),new BigDecimal(dto.getTotal_amount()),GlobalErrorCode.PARAM_IS_NULL);
        //订单是否正确
        AssertUtil.isEquals(payOrder.getOrderNo(),dto.getOut_trade_no(),GlobalErrorCode.PARAM_IS_NULL);
        //是否成功支付
        AssertUtil.isTrue(dto.isTradeSuccess(),GlobalErrorCode.PARAM_IS_NULL);
    }

    /**
     * 支付宝支付
     * @param payApplyDto
     * @param payOrder
     * @return String
     */
    private String alipay(PayApplyDto payApplyDto,PayOrder payOrder) {
        //到数据库查询支付宝支付相关配置
        AlipayInfo alipayInfo = alipayInfoService.selectList(null).get(0);
        // 1. 设置参数(全局只需设置一次)
        Factory.setOptions(getOptions(alipayInfo));
        try {
            // 2. 发起API调用(以创建当面付收款二维码为例)
            AlipayTradePagePayResponse response = Factory.Payment.Page().pay(
                            payOrder.getSubject(),  //标题
                            payOrder.getOrderNo(),  //订单号
                            payOrder.getAmount().toString(),  //支付金额
                            StringUtils.hasLength(payApplyDto.getCallUrl()) ? payApplyDto.getCallUrl() : alipayInfo.getReturnUrl()  //支付成功回调地址
                    );
            // 3. 处理响应或异常
            if (ResponseChecker.success(response)) {
                return response.getBody();
            } else {
                return null;
            }
        } catch (Exception e) {
            System.err.println("调用遭遇异常,原因:" + e.getMessage());
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /*
     * 设置收款账户相关配置【支付宝开放平台拷贝的】
     * @return Config
     */
    private static Config getOptions(AlipayInfo alipayInfo) {

        Config config = new Config();
        config.protocol = alipayInfo.getProtocol();
        config.gatewayHost = alipayInfo.getGatewayHost();
        config.signType = alipayInfo.getSignType();
        config.appId = alipayInfo.getAppId();
        // 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
        config.merchantPrivateKey = alipayInfo.getMerchantPrivateKey();
        config.alipayPublicKey = alipayInfo.getAlipayPublicKey();
        //可设置异步通知接收服务地址(可选)
        config.notifyUrl = alipayInfo.getNotifyUrl();
        return config;
    }
}

3.支付宝异步回调通知后台支付结果

3.1封装支付宝返回的参数

package cn.ybl.dto;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;

/**
 * @Author Mr.Yang
 * @Date 2022/9/19 19:04
 * @description 支付宝异步回调通知给我们返回的参数
 */
@Data
public class AlipayNotifyDto {

    public static final String WAIT_BUYER_PAY = "WAIT_BUYER_PAY";
    public static final String TRADE_CLOSED = "TRADE_CLOSED";
    public static final String TRADE_SUCCESS = "TRADE_SUCCESS";
    public static final String TRADE_FINISHED = "TRADE_FINISHED";

    private String charset;
    private String gmt_create;
    private String gmt_payment;
    private String notify_time;
    //我们数据库中的subject字段
    private String subject;
    //签名
    private String sign;
    private String buyer_id;
    //支付金额【发票】
    private String invoice_amount;
    private String version;
    private String notify_id;
    private String fund_bill_list;
    private String notify_type;
    //我们自己的支付单号【订单号】
    private String out_trade_no;
    //支付金额
    private String total_amount;
    /*交易状态:
      WAIT_BUYER_PAY(交易创建,等待买家付款)、
      TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、
      TRADE_SUCCESS(交易支付成功)、
      TRADE_FINISHED(交易结束,不可退款)
    */
    private String trade_status;
    //支付宝内部自己的交易单号
    private String trade_no;
    private String auth_app_id;
    private String receipt_amount;
    private String point_amount;
    private String app_id;
    private String buyer_pay_amount;
    //签名方式
    private String sign_type;
    private String seller_id;
    private String code;
    private String msg;
    private String passback_params;

    @JSONField(serialize = false)
    public boolean isTradeSuccess(){
        return this.trade_status.equals(TRADE_SUCCESS) || this.trade_status.equals(TRADE_FINISHED);
    }

    @JSONField(serialize = false)
    public boolean isTradeWit(){
        return this.trade_status.equals(WAIT_BUYER_PAY);
    }

    @JSONField(serialize = false)
    public boolean isTradeFail(){
        //未付款交易超时关闭,或支付完成后全额退款。
        return this.trade_status.equals(TRADE_CLOSED);
    }
}

3.2编写接收支付宝通知的接口

/**
     * 支付宝支付异步回调通知接口,数据库中notify_url字段就是支付宝给我们发起回调的地址,在测试环境中需要内网穿透支付宝才能访问到我们的内网
     * 支付宝会通知我们支付结果,然后我们拿到这个结果去做用户支付后的业务处理
     * 如果业务执行完毕,返回success给支付宝,,如果失败则返回fail,支付宝会再次通知我们,最大努力通知8次
     */
    @RequestMapping("/alipay/notify")
    public String notify(AlipayNotifyDto notifyDto){
        try {
            //验签
            Map map = JSON.parseObject(JSON.toJSONString(notifyDto), Map.class);
            Boolean result = Factory.Payment.Common().verifyNotify(map);
            if(!result){
                return "fail";  //验签失败,让支付宝重新发
            }
            return payService.notify(notifyDto);  //执行业务方法,执行成功返回success
        } catch (Exception e) {
            e.printStackTrace();
            return "fail";  //业务方法执行异常,让支付宝再次通知支付结果给我们
        }
    }

3.3业务方法,对支付结果进行验证,修改支付订单状态,添加支付流水,发送mq事务消息

/**
     * 用户支付后,支付宝根据notify_url发起异步回调通知,告诉我们用户支付状态,我们再去做相关业务
     * 处理支付订单和处理服务订单以及修改课程服务的购买记录需要同时成功,为了应对高并发,使用MQ事务消息
     * 1.验证签名【是不是支付宝给我们发的】
     * 2.新增支付流水
     * 3.修改支付订单状态、修改时间
     * 4.调用订单服务修改订单状态
     * 5.添加/修改课程服务的表course_user_learn
     * @param dto
     * @return
     */
    @Override
    public String notify(AlipayNotifyDto dto) {
        //根据回调返回的单号查询订单
        PayOrder payOrder = selectOrderNo(dto.getOut_trade_no());
        //1.对支付结果作相关判断
        confirmPayResult(dto,payOrder);
        //2.修改支付订单支付状态,修改时间
        PayOrder updatePayOrder = updatePayOrder(payOrder);
        //3.新增支付流水
        PayOrder payOrderAndPayFlow = insertPayFlow(dto, updatePayOrder);
        //封装发送的消息主体
        Message<Pay2Order2CourseDto> message = MessageBuilder.withPayload(new Pay2Order2CourseDto(
                payOrder.getOrderNo(),
                payOrder.getExtParams()
        )).build();
        //发送事务消息到MQ,需要保证支付服务和订单服务以及课程服务的数据库一致性
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
                "tx-pay-service",
                "topic-pay:tag-pay",
                message,
                payOrderAndPayFlow
        );
        //检查消息是否发送成功
        SendStatus sendStatus = result.getSendStatus();
        //获取本地事务执行状态
        LocalTransactionState localTransactionState = result.getLocalTransactionState();
        Boolean b = localTransactionState == LocalTransactionState.COMMIT_MESSAGE || sendStatus==SendStatus.SEND_OK;
        AssertUtil.isTrue(b,GlobalErrorCode.PARAM_IS_NULL);
        //执行成功发送success给支付宝
        return "success";
    }

    /*
     * 新增支付流水,不做新增操作,返回即可,等下发给MQ去执行
     * @param dto
     */
    private PayOrder insertPayFlow(AlipayNotifyDto dto,PayOrder payOrder) {
        PayFlow payFlow = new PayFlow();
        payFlow.setNotifyTime(new Date());
        payFlow.setSubject(dto.getSubject());
        payFlow.setOutTradeNo(dto.getOut_trade_no());
        payFlow.setTotalAmount(new BigDecimal(dto.getTotal_amount()));
        payFlow.setTradeStatus("TRADE_SUCCESS");
        payFlow.setPaySuccess(true);
        payOrder.setPayFlow(payFlow);
        return payOrder;
    }

    /*
     * 修改支付订单的修改时间、支付状态,不做修改操作,返回即可,等下发给MQ去执行
     * @param payOrder
     */
    private PayOrder updatePayOrder(PayOrder payOrder) {
        //如果订单已经为已支付,则报错回滚
        payOrder.setPayStatus(1);
        payOrder.setUpdateTime(new Date());
        return payOrder;
    }

    /*
     * 对支付结果进行相关判断
     * @param dto
     */
    private void confirmPayResult(AlipayNotifyDto dto,PayOrder payOrder) {
        AssertUtil.isNotNull(payOrder,GlobalErrorCode.PARAM_IS_NULL);  //随便抛一个错,控制层返回fail
        //支付金额是否正确
        AssertUtil.isEquals(payOrder.getAmount(),new BigDecimal(dto.getTotal_amount()),GlobalErrorCode.PARAM_IS_NULL);
        //订单是否正确
        AssertUtil.isEquals(payOrder.getOrderNo(),dto.getOut_trade_no(),GlobalErrorCode.PARAM_IS_NULL);
        //是否成功支付
        AssertUtil.isTrue(dto.isTradeSuccess(),GlobalErrorCode.PARAM_IS_NULL);
    }

3.4编写mq事务监听器,操作本地事务,成功则携带message发送消息,否则回滚本地事务

package cn.ybl.mq;

import cn.ybl.domain.PayFlow;
import cn.ybl.domain.PayOrder;
import cn.ybl.dto.Pay2Order2CourseDto;
import cn.ybl.enums.GlobalErrorCode;
import cn.ybl.service.IPayFlowService;
import cn.ybl.service.IPayOrderService;
import cn.ybl.util.AssertUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;

import java.nio.charset.StandardCharsets;

/**
 * @Author Mr.Yang
 * @Date 2022/9/19 20:46
 * @description 监听支付回调后发送的消息,完成业务操作——添加支付流水和修改支付订单,并发送消息给MQ队列
 */
@RocketMQTransactionListener(txProducerGroup = "tx-pay-service")
public class PayOrderListener implements RocketMQLocalTransactionListener {

    @Autowired
    private IPayOrderService payOrderService;

    @Autowired
    private IPayFlowService payFlowService;

    /**
     * 执行本地业务
     * @param message
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            AssertUtil.isNotNull(o, GlobalErrorCode.PARAM_IS_NULL); //防御性编程,如果报错我就让本地事务执行失败
            PayOrder payOrder = (PayOrder) o ;
            payOrderService.updatePayOrder(payOrder);
            payFlowService.savePayFlow(payOrder.getPayFlow());
            return RocketMQLocalTransactionState.COMMIT;  //本地事务执行成功,发送消息message到MQ
        } catch (Exception e) {
            e.printStackTrace();
            return  RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 本地事务检查回调函数,手动去查询本地事务是否执行成功
     * @param message
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        AssertUtil.isNotNull(message,GlobalErrorCode.PARAM_IS_NULL);
        //通过获取message通过orderNo去查询支付订单是否修改成功和支付流水是否保存成功
        String string = new String((byte[])message.getPayload(),StandardCharsets.UTF_8);
        Pay2Order2CourseDto pay2Order2CourseDto = JSON.parseObject(string, Pay2Order2CourseDto.class);
        EntityWrapper<PayOrder> wrapper = new EntityWrapper<>();
        wrapper.eq("order_no",pay2Order2CourseDto.getOrderNo());
        if(payOrderService.selectOne(wrapper).getPayStatus()!=1){
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        EntityWrapper<PayFlow> entityWrapper = new EntityWrapper<>();
        entityWrapper.eq("out_trade_no",pay2Order2CourseDto.getOrderNo());
        PayFlow payFlow = payFlowService.selectOne(entityWrapper);
        if(payFlow==null){
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        return RocketMQLocalTransactionState.COMMIT;
    }
}

发送消息后订单服务order和课程服务course自己去定义一个消费者消费消息即可完成整套下单支付流程

MQ的异步下单和对接支付宝结束,重点理解实现思想

posted @ 2022-09-18 23:38  yyybl  阅读(1515)  评论(1)    收藏  举报