秒杀流程

摘要:

       我们的秒杀是基于纯redis的秒杀,库存和商品都是放到redis中,然后库存使用redisson和信号量来保证原子性,用户发起秒杀请求,直接走redis进行秒杀商品,如果符合资格,就预扣减库存,并生成预创订单写入redis,然后将单号返回,然后获取一个防重token并查询商品详情,进入商品详情页,用户带着订单号点击确认下单,后端进行参数校验,然后将redis中的预创订单生成真实订单写入数据库courseOrder,并通过RocketMQ发起事务消息,通知支付服务预创支付订单,前端同时携带订单号向支付订单接口发起轮询查询订单是否存在,如果存在则返回支付订单号,用户带着支付订单号去请求支付宝接口完成支付,高并发的流量主要集中在秒杀请求,而对于进入商品页详情以及支付等接口的调用很少,因为用户没有资格秒杀商品就被拦截在外面了,根本进不到下一步

秒杀架构:

秒杀详细流程:

  • 后台在秒杀活动管理界面添加秒杀活动,设置好秒杀时间周期

  • 后台添加秒杀课程到秒杀活动中

  • 通过定时任务把秒杀课程发布到Redis中

  • 秒杀走Redis秒杀,秒杀成功,把预创订单放到Redis中,订单号返回给用户

  • 用户拿着订单号,进入确认订单页面,提交订单

  • 用户拿着订单号去下单,订单服务去Redis获取预创订单数据,保存订单到数据库,同时保存支付单,然后删除预创订单

  • 下单成功,用户拿着订单号去支付,支付服务根据订单号查询支付单,发起支付申请

  • 支付结果处理和普通购买流程一致。

引入Redisson分布式锁依赖:

<!--redisson分布式锁-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
<!--整合Redis , 底层可以用jedis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!--排除redis默认的java开发客户端依赖,因为高并发情况下会有内存溢出问题,我们使用jedis来操作Java-->
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--引入redis的java客户端包,jedis依赖-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

一:秒杀流程

1.后台发布秒杀商品,将商品添加进redis中,并通过redisson和信号量设置库存【注意这里需要一对多的关系,也就是一个活动对应多个商品,我们需要使用redis的hash结构】

1.1注册redisson

package cn.ybl.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 注册redisson
 */
@Configuration
public class RedissonConfig {

    //创建客户端
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("root");
        return Redisson.create(config);
    }
}

1.2redis的序列化配置

package cn.ybl.config;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;

/**
 * RedisJson序列化
 */
//缓存的配置
@Configuration
public class RedisConfig {

    @Resource
    private RedisConnectionFactory factory;


    //使用JSON进行序列化
    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(factory);
        //JSON格式序列化
        GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();
         //key的序列化
        redisTemplate.setKeySerializer(serializer);
        //value的序列化
        redisTemplate.setValueSerializer(serializer);
        //hash结构key的序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //hash结构value的序列化
        redisTemplate.setHashValueSerializer(serializer);
        return redisTemplate;
    }

} 

1.3发布活动,将活动对应的库存存入redis中,使用redis的hash结构,库存使用redisson以及信号量

/**
  * 发布活动,需要判断是否重复发布,需要判断此活动中是否有商品,判断是否在有效时间之内
  */
@Override
public void publish(Long activityId) {
    AssertUtil.isNotNull(activityId, GlobalErrorCode.PARAM_IS_NULL);
    //获取活动信息
    KillActivity killActivity = selectById(activityId);
    //秒杀活动不存在
    AssertUtil.isNotNull(killActivity, GlobalErrorCode.KILL_ACTIVITY_EMPTY);
    //秒杀活动重复发布
    AssertUtil.isEqualsTrim(killActivity.getPublishStatus(),1,GlobalErrorCode.KILL_ACTIVITY_REPEAT);
    //查询活动下的商品
    Wrapper<KillCourse> wrapper = new EntityWrapper<>();
    wrapper.eq("activity_id",killActivity.getId());
    List<KillCourse> killCourses = killCourseService.selectList(wrapper);
    //活动下没有商品
    AssertUtil.isNotNull(killCourses,GlobalErrorCode.KILL_ACTIVITY_PRODUCT_EMPTY);
    Date date = new Date();
    //活动过期
    boolean time = date.before(killActivity.getBeginTime());
    AssertUtil.isTrue(time,GlobalErrorCode.KILL_ACTIVITY_EXPIRED);
    //发布活动,使用hash结构将活动与对应的课程缓存进redis,并设置库存的信号量
    killCourses.forEach(e->{
        //设置信号量
        RSemaphore semaphore = redissonClient.getSemaphore(e.getId().toString());
        semaphore.trySetPermits(e.getKillCount());
        //hash结构存入redis————Long,Map<Long,Value>
        String Akey = "activity"+":"+activityId;
        redisTemplate.opsForHash().put(Akey,e.getId().toString(),e);
    });
    killActivity.setPublishStatus(1);//修改状态为已发布
    killActivity.setPublishTime(new Date());  //修改发布时间
    updateById(killActivity);
}

2.前端查询所有的秒杀课程信息进行展示

/**
  * 从redis获取秒杀商品
  */
@Override
public List<KillCourse> online() {
    //获取redis中所有key以activity开头的值
    Set<Object> keys = redisTemplate.keys("activity:*");
    //秒杀活动不存在
    AssertUtil.isNotNull(keys,GlobalErrorCode.KILL_ACTIVITY_DOES_NOT_EXIST);
    List<KillCourse> killCourses = new ArrayList<>();
    keys.forEach(k->{
        //从redis取出所有大key为k的值
        List values = redisTemplate.opsForHash().values(k);
        killCourses.addAll(values);
    });
    return killCourses;
}

 3.用户点击商品,进入商品详情页,后端通过秒杀id和课程id组合条件从redis查询出这个商品的信息

/**
  * 从redis获取获取一个秒杀商品的信息
  */
@Override
public KillCourse one(Long killId, Long activityId) {
    //先对参数进行判空
    AssertUtil.isNotNull(killId,GlobalErrorCode.PARAM_IS_NULL);
    AssertUtil.isNotNull(activityId,GlobalErrorCode.PARAM_IS_NULL);
    //从redis中获取商品信息
    String Akey = "activity"+":"+activityId;
    KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get(Akey, killId.toString());
    return killCourse;
}

4.用户点击立即秒杀,后台需要判断此用户是否多次秒杀,redisson是否扣减信号量成功等问题,扣减成功后创建一个预创订单写入redis,然后将订单号返回给用户

/**
  * 用户商品详情页点击立即秒杀
  */
@Override
public String kill(Long activityId, Long killId) {
    //1.判空
    AssertUtil.isNotNull(activityId,GlobalErrorCode.PARAM_IS_NULL);
    AssertUtil.isNotNull(killId,GlobalErrorCode.PARAM_IS_NULL);
    //2.查询当前用户是否秒杀过,一个用户只能秒杀一个
    Long loginId = 3l;
    String UKey = "loginId"+":"+killId;
    //请勿重复秒杀,只能秒杀一件商品
    AssertUtil.isNull(redisTemplate.opsForValue().get(UKey),GlobalErrorCode.KILL_COURSE_REPEAT);
    //3.查询是否存在此秒杀活动对应的课程
    String Akey = "activity"+":"+activityId;
    KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get(Akey, killId.toString());
    AssertUtil.isNotNull(killCourse,GlobalErrorCode.KILL_ACTIVITY_DOES_NOT_EXIST);
    //4.执行预扣减信号量【库存】
    //获取信号量
    RSemaphore semaphore = redissonClient.getSemaphore(killId.toString());
    //尝试扣减信号量
    boolean b = semaphore.tryAcquire(1);
    //扣减失败,提示商品售罄
    AssertUtil.isTrue(b,GlobalErrorCode.KILL_COURSE_SOLD_OUT);
    //5.秒杀成功,预创订单,订单号作为key值存入redis,将订单号返回
    //创建订单号
    String orderNo = CodeGenerateUtils.generateOrderSn(loginId);
    KillOrderRedisDto killOrderRedisDto = new KillOrderRedisDto(
        orderNo,
        1,
        loginId,
        killId,
        killCourse.getCourseId(),
        killCourse.getActivityId()
    );
    redisTemplate.opsForValue().set(orderNo,killOrderRedisDto);
    //将用户秒杀记录存入redis,防止一个用户多次秒杀
    redisTemplate.opsForValue().set(UKey,killCourse);
    return orderNo;
}

5.用户拿到订单号后进入商品详情页面进行下单,在商品详情页面的钩子函数中需要向后台请求一个防重token,并查询商品详情进行回显

5.1防重token

/**
  * 获取防重token
  * @param courseIds
  * @return JSONResult
  */
@Override
public JSONResult createToken(String courseIds) {
    AssertUtil.isNotNull(courseIds, GlobalErrorCode.PARAM_IS_NULL);
    //生成token
    String token = StrUtils.getComplexRandomString(6);
    //用户id暂时写死
    Long loginId = 3l;
    //存入redis
    String key = loginId+":"+courseIds+":"+"token";  //  loginId:courseId  为key

    redisTemplate.opsForValue().set(key,token);
    return JSONResult.success(token);
}

5.2返回订单详情

/**
  * 获取秒杀订单详情
  */
@Override
public JSONResult getKillOrderInfo(Long courseId, String orderNo) {
    //获取预创订单
    KillOrderRedisDto killOrderRedisDto = (KillOrderRedisDto) redisTemplate.opsForValue().get(orderNo);
    AssertUtil.isNotNull(killOrderRedisDto,GlobalErrorCode.KILL_COURSE_ORDER_EMPTY);
    //获取redis中的秒杀课程详情
    KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get("activity" + ":" + killOrderRedisDto.getActivityId(), killOrderRedisDto.getKillId().toString());
    AssertUtil.isNotNull(killCourse,GlobalErrorCode.KILL_COURSE_ORDER_EMPTY);
    CourseOrder order = new CourseOrder();
    //封装课程信息
    Course course = new Course();
    course.setName(killCourse.getCourseName());
    course.setId(killCourse.getCourseId());
    course.setTeacherNames(killCourse.getTeacherNames());
    course.setPic(killCourse.getCoursePic());
    order.setCourse(course);
    //封装课程营销信息
    CourseMarket courseMarket = new CourseMarket();
    courseMarket.setPrice(killCourse.getKillPrice());
    order.setCourseMarket(courseMarket);
    //创建返回给前端数据的VO
    CourseOrderVo courseOrderVo = new CourseOrderVo();
    courseOrderVo.getCourseInfos().add(order);
    courseOrderVo.setTotalAmount(killCourse.getKillPrice());
    return JSONResult.success(courseOrderVo);
}

6.用户点击提交订单,携带订单号和token向订单服务发起请求,订单服务首先进行防重token的校验,然后通过订单号向redis中取出预创订单,然后创建主订单和订单明细,MQ携带支付订单所需要的信息发起事务消息,课程订单的本地事务交给MQ来执行,执行成功后发送事务消息并返回订单号,同时发送延迟消息,防止支付超时,超时自动取消课程订单和支付订单

/**
  * 前端下单,保存秒杀订单详情
  * 需要做的事:
  *      1.验证防重token是否正确
  *      2.将redis的预创订单同步到CourseOrder订单
  *      3.发送事务消息,创建支付订单,需要最后将支付订单返回给前端带着去下单
  *      4.发送延迟消息,订单超时自动关闭订单
  */
@Override
public JSONResult killPlaceOrder(KillOrderDto killOrderDto) {
    Long loginId = 3l;  //登录用户id暂时写死
    //从redis中获取预创订单
    KillOrderRedisDto killOrderRedisDto = (KillOrderRedisDto) redisTemplate.opsForValue().get(killOrderDto.getOrderNo());
    //1.预创订单异常
    AssertUtil.isNotNull(killOrderRedisDto,GlobalErrorCode.KILL_COURSE_ORDER_EMPTY);
    //2.获取防重token
    String tokenKey = loginId +":"+killOrderRedisDto.getCourseId()+":"+"token";
    Object tokenRedis = redisTemplate.opsForValue().get(tokenKey);
    //防重token异常
    AssertUtil.isNotNull(tokenRedis,GlobalErrorCode.TOKEN_EMPTY_ERROR);
    //3.创建主订单
    List<Long> list = new ArrayList<>();
    list.add(killOrderRedisDto.getCourseId());
    //从redis中获取到秒杀课程详情
    KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get("activity" + ":" + killOrderRedisDto.getActivityId(), killOrderRedisDto.getKillId().toString());
    //判空,秒杀商品不存在
    AssertUtil.isNotNull(killCourse,GlobalErrorCode.KILL_ACTIVITY_DOES_NOT_EXIST);
    //主订单属性设置完成
    CourseOrder order = saveKillOrder(killOrderDto.getPayType(), killOrderRedisDto.getOrderNo(), killCourse.getKillPrice());
    //4.保存订单明细【包含了主订单信息和订单详情信息】
    CourseOrder courseOrderAndItem = saveKillDeatil(order, killOrderRedisDto.getCourseId(), killCourse.getCoursePic(), killCourse.getCourseName());
    //5.发送事务消息,让mq去执行本地事务,然后发送消息到mq队列,让支付服务拿到message去创建支付单
    Map<String, Long> map = new HashMap<>();
    map.put("loginId",loginId);
    map.put("courseIds",killCourse.getCourseId());
    Message<CourseOrder2PayOrder> message = MessageBuilder.withPayload(new CourseOrder2PayOrder(
        order.getTotalAmount(),
        order.getPayType(),
        order.getOrderNo(),
        order.getUserId(),
        order.getTitle(),
        JSON.toJSONString(map)
    )).build();
    TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
        "tx-killOrder-service",
        "topic-killorder:tag-killorder",
        message,
        courseOrderAndItem);
    //获取本地事务执行状态和消息发送状态
    LocalTransactionState localTransactionState = result.getLocalTransactionState();
    SendStatus sendStatus = result.getSendStatus();
    boolean b = localTransactionState == LocalTransactionState.COMMIT_MESSAGE || sendStatus == SendStatus.SEND_OK;
    AssertUtil.isTrue(b,GlobalErrorCode.CREATE_ORDER_ERROR);
    //消息发送成功,删除redis中的预创订单
    redisTemplate.delete(order);
    //发送一个延迟消息,如果超时未支付,则取消订单
    rocketMQTemplate.syncSend(
        "topic-killOrderTimeOut:tag-killOrderTimeOut",
        MessageBuilder.withPayload(killOrderRedisDto.getOrderNo()).build(),   //延迟消息内容,订单号
        3000,   //超过3s未发送成功就不发了
        7     //延迟消息的等级,必须在3分钟之内支付,否则支付超时
    );
    return JSONResult.success(killOrderRedisDto.getOrderNo());
}

6.2MQ事务监听器

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.JSON;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
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/24 11:46
 * @description 秒杀订单事务消息监听器
 */
@RocketMQTransactionListener(txProducerGroup = "tx-killOrder-service")
public class KillOrderListener implements RocketMQLocalTransactionListener {

    @Autowired
    private ICourseOrderService orderService;

    /**
     * 执行本地事务
     * @param message
     * @param o
     * @return
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            AssertUtil.isNotNull(o, GlobalErrorCode.PARAM_IS_NULL);
            CourseOrder order = (CourseOrder) o;
            //将此对象传给courseOrder业务去保存主订单和详情
            orderService.saveCourseOrderAndItem(order);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            e.printStackTrace();
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 回调
     * @param message
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        AssertUtil.isNotNull(message,GlobalErrorCode.PARAM_IS_NULL);
        String s = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);
        CourseOrder2PayOrder courseOrder2PayOrder = JSON.parseObject(s, CourseOrder2PayOrder.class);
        Wrapper<CourseOrder> wrapper = new EntityWrapper<>();
        wrapper.eq("order_no",courseOrder2PayOrder.getOrderNo());
        CourseOrder order = orderService.selectOne(wrapper);
        if(order!=null){
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}

6.3支付超时课程订单消费者

package cn.ybl.mq;

import cn.ybl.domain.CourseOrder;
import cn.ybl.service.ICourseOrderService;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
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/24 11:38
 * @description 秒杀支付超时消费者,需要将订单状态改为取消,同时支付订单也要改为取消,广播消费
 */
@Component
@RocketMQMessageListener(
        consumerGroup = "killOrderTimeOut-consumer",
        topic = "topic-killOrderTimeOut",
        selectorExpression = "tag-killOrderTimeOut",
        messageModel = MessageModel.BROADCASTING)
public class KillOrderTimeOutConsumer implements RocketMQListener<MessageExt> {

    @Autowired
    private ICourseOrderService courseOrderService;

    @Override
    public void onMessage(MessageExt messageExt) {
        if(messageExt.getBody()==null){
            return;
        }
        String orderNo = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        Wrapper<CourseOrder> wrapper = new EntityWrapper<>();
        wrapper.eq("order_no",orderNo);
        CourseOrder order = courseOrderService.selectOne(wrapper);
        if(order==null){
            return;
        }
        if(order.getStatusOrder()==0){
            order.setStatusOrder(4);
            courseOrderService.updateById(order);
        }
    }
}

6.4支付超时秒杀服务消费者【需要增加扣除的信号量,本步骤省略...........】

7.支付服务获取到消息后创建支付单,前端通过这个全局唯一订单号向支付服务发起轮询查询接口请求,查询支付单是否存在,接收到前端查询支付单的请求后如果查到了就将支付单号返回给前端

package cn.ybl.mq;

import cn.ybl.domain.PayOrder;
import cn.ybl.dto.CourseOrder2PayOrder;
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/24 12:33
 * @description 秒杀订单创建支付订单
 */
@Component
@RocketMQMessageListener(
    consumerGroup = "killOrder-consumer",
    topic = "topic-killorder",
    selectorExpression = "tag-killorder",
    messageModel = MessageModel.BROADCASTING
)
public class KillOrderConsumer implements RocketMQListener<MessageExt> {

    @Autowired
    private IPayService payService;

    @Autowired
    private IPayOrderService payOrderService;

    @Override
    public void onMessage(MessageExt messageExt) {
        if(messageExt.getBody()==null){
            return;
        }
        String string = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        //转为我们封装的对象
        CourseOrder2PayOrder courseOrder2PayOrder = JSON.parseObject(string, CourseOrder2PayOrder.class);
        //保存支付单
        PayOrder payOrder = payService.selectOrderNo(courseOrder2PayOrder.getOrderNo());
        if(payOrder!=null){
            return;   //保证幂等性
        }
        payOrderService.saveOrder(courseOrder2PayOrder);
    }
}

7.2支付超时支付服务消费者

package cn.ybl.mq;

import cn.ybl.service.IPayService;
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/24 13:17
 * @description 支付超时,取消支付订单
 */
@Component
@RocketMQMessageListener(
        consumerGroup = "killOrderTimeOut-consumer",
        topic = "topic-killOrderTimeOut",
        selectorExpression = "tag-killOrderTimeOut",
        messageModel = MessageModel.BROADCASTING
)
public class KillOrderTimeOutConsumer implements RocketMQListener<MessageExt> {

    @Autowired
    private IPayService payService;

    @Override
    public void onMessage(MessageExt messageExt) {
        if(messageExt.getBody()==null){
            return;  //让mq别发了
        }
        //获取订单号
        String orderNo = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        //取消支付订单,并且要关闭支付交易
        payService.cancelPayOrder(orderNo);
    }
}

7.3查询支付单接口

/**
  * 查询是否已经将MQ中的消息消费【保存支付订单,前端要根据支付订单查询是否已经创建,返回订单号
  * @param orderNo
  * @return
  */
@GetMapping("/checkPayOrder/{orderNo}")
public JSONResult checkPayOrder(@PathVariable("orderNo") String orderNo){
    AssertUtil.isNotNull(orderNo, GlobalErrorCode.PARAM_IS_NULL);
    PayOrder payOrder = payService.selectOrderNo(orderNo);
    return payOrder !=null ? JSONResult.success(payOrder.getOrderNo()):JSONResult.error();
}

8.前端拿到支付单后继续请求支付接口,支付接口对接支付宝返回给前端

/**
  * 支付申请
  */
@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;
}
/**
  * 支付宝支付
  * @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;
}

9.用户支付成功后后端通过支付宝的回调通知确认是否支付成功,支付成功做后续业务,如果订单已取消但是支付成功了则做退款操作

/**
  * 用户支付后,支付宝根据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";
}
posted @ 2022-09-24 15:50  yyybl  阅读(832)  评论(0)    收藏  举报