谷粒商城-高级篇-秒杀业务

1、后台添加秒杀商品


1、配置网关

- id: coupon_route
      uri: lb://gulimall-coupon
      predicates:
        - Path=/api/coupon/**
      filters:
        - RewritePath=/api/(?<segment>.*),/$\{segment}

2、每日秒杀关联商品功能实现

点击关联商品后,应该查询当前场次的所有商品

点击关联商品的时候,会弹出一个页面,并且F12可以看到会调用一个url请求:

http://localhost:88/api/coupon/seckillskurelation/list?t=1716706075726&page=1&limit=10&key=&promotionSessionId=1

根据此url去完善该接口

修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”代码如下:

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
    QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
    // 场次id不是null
    String promotionSessionId = (String) params.get("promotionSessionId");
    if (!StringUtils.isEmpty(promotionSessionId)) {
        queryWrapper.eq("promotion_session_id", promotionSessionId);
    }
    IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params), queryWrapper);

    return new PageUtils(page);
}
}

2、搭建秒杀服务环境


1、导入pom.xml依赖

4.0.0

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-seckill</name>
    <description>秒杀</description>
 
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>
 
    <dependencies>
        <!--以后使用redis.client作为所有分布式锁,分布式对象等功能框架-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
 
        <dependency>
            <groupId>com.auguigu.gulimall</groupId>
            <artifactId>gulimall-commom</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
 
</project>

2、添加配置

spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.119.127

3、主启动类添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
    SpringApplication.run(GulimallSeckillApplication.class, args);
}
}


3、定时任务

3.1、cron 表达式

语法:秒 分 时 日 月 周 年(Spring 不支持)

http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

特殊字符:


3.2、SpringBoot 整合定时任务


springboot整合定时任务流程:

定时任务:

1、@EnableScheduling 开启定时任务

2、@Scheduled 开启一个定时任务

3、自动配置类 TaskSchedulingAutoConfiguration

异步任务 :

1、@EnableAsync 开启异步任务功能

2、@Async 给希望异步执行的方法上标注

3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties

添加“com.atguigu.gulimall.seckill.scheduled.HelloSchedule”类,代码如下:

@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
 * 1、Spring中6位组成,不允许7位d的年
 * 2、周的位置,1-7代表周一到周日
 * 3、定时任务不应该阻塞。默认是阻塞的
 *      1)、可以让业务运行以异步的方式,自己提交到线程池
 *      2)、支持定时任务线程池;设置TaskSchedulingProperties;
 *              spring.task.scheduling.pool.size=5
 *      3)、让定时任务异步执行,自己提交到线程池
 *          异步任务
 *
 *      解决:使用异步任务来完成定时任务不阻塞的功能@Async
 */						定时任务+异步任务,实现异步任务不阻塞
@Async
@Scheduled(cron = "*/5 * * * * ?")
public void hello() throws InterruptedException {
    log.info("hello......");
    Thread.sleep(3000);
}
}

配置定时任务参数

spring.task.execution.pool.core-size=20
spring.task.execution.pool.max-size=50

4、秒杀商品上架

4.1、秒杀商品上架思路

项目独立部署,独立秒杀模块gulimall-seckill
使用定时任务每天三点上架最新秒杀商品,削减高峰期压力

4.2、秒杀商品上架流程

4.3、存储模型设计


1、查询秒杀活动场次和sku信息的存储模型

添加“com.atguigu.gulimall.seckill.vo.SeckillSessionWithSkus”类,代码如下:

package com.atguigu.gulimall.seckill.vo;
 
import lombok.Data;
 
import java.util.Date;
import java.util.List;
 
@Data
public class SeckillSessionWithSkus {
    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;
 
    private List<SeckillSkuVo> relationEntities;
 
}

2、查询秒杀活动商品关联的存储模型

添加“com.atguigu.gulimall.seckill.vo.SeckillSkuVo”类,代码如下:

package com.atguigu.gulimall.seckill.vo;
 
import lombok.Data;
 
import java.math.BigDecimal;
 
@Data
public class SeckillSkuVo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
 
}

3、查询商品信息的存储模型

添加“com.atguigu.gulimall.seckill.vo.SkuInfoVo”类,代码如下:

package com.atguigu.gulimall.seckill.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SkuInfoVo {
private Long skuId;
/**
 * spuId
 */
private Long spuId;
/**
 * sku名称
 */
private String skuName;
/**
 * sku介绍描述
 */
private String skuDesc;
/**
 * 所属分类id
 */
private Long catalogId;
/**
 * 品牌id
 */
private Long brandId;
/**
 * 默认图片
 */
private String skuDefaultImg;
/**
 * 标题
 */
private String skuTitle;
/**
 * 副标题
 */
private String skuSubtitle;
/**
 * 价格
 */
private BigDecimal price;
/**
 * 销量
 */
private Long saleCount;
}

4、缓存获得秒杀活动场次和sku信息的存储模型

添加"com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo"类,代码如下:

package com.atguigu.gulimall.seckill.to;
 
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;
 
import java.math.BigDecimal;
 
@Data
public class SeckillSkuRedisTo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    //以上都为SeckillSkuRelationEntity的属性
 
    //skuInfo
    private SkuInfoVo skuInfo;
 
    //当前商品秒杀的开始时间
    private Long startTime;
 
    //当前商品秒杀的结束时间
    private Long endTime;
 
    //当前商品秒杀的随机码
    private String randomCode;
}

4.4、定时上架


配置定时任务

添加“com.atguigu.gulimall.seckill.config.ScheduledConfig”类,代码如下:

@EnableAsync // 开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling // 开启对定时任务的支持
@Configuration
public class ScheduledConfig {
}

每天凌晨三点远程调用coupon服务上架最近三天的秒杀商品

添加“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下

@Slf4j
@Component
public class SeckillSkuScheduled {
@Autowired
private SeckillService seckillService;

/**
 * TODO 幂等性处理
 * 上架最近三天的秒杀商品
 */
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
    // 重复上架无需处理
    log.info("上架秒杀的信息......");
    seckillService.uploadSeckillSkuLatest3Days();
}
}

添加“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下

public interface SeckillService {
    void uploadSeckillSkuLatest3Days();
}

添加“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下

@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private CouponFeignService couponFeignService;

@Autowired
private ProductFeignService productFeignService;

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired
private RedissonClient redissonClient;

private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:"; //秒杀活动信息

private final String SKUKILL_CACHE_PREFIX = "seckill:skus:"; // 秒杀商品信息

private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码

@Override
public void uploadSeckillSkuLatest3Days() {
    // 1、扫描最近三天需要参与秒杀的活动的场次和sku信息
    R session = couponFeignService.getLasts3DaySession();
    if (session.getCode() == 0){
        // 上架商品
        List<SeckillSessionWithSkus> data = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
        });
        // 2、缓存到redis

        // 2.1、缓存活动信息
        saveSessionInfos(data);

        // 2.2、缓存获得关联商品信息
        saveSessionSkuInfos(data);
    }
}
}

4.4.1、获取最近三天的秒杀信息

思路:
1.获取最近三天的秒杀场次信息,

先获取今天的日期,通过LocalDate()

获取今天的最大日期和最小日期LocalTime.Min和Max

拼接获取到要查询的时间。

2.再通过秒杀场次id查询对应的商品信息

遍历场次获取商品信息(通过关联表@TableField(exist=false))

添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:

@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/lasts3DaySession")
R getLasts3DaySession();
}

gulimall-coupon

修改“com.atguigu.gulimall.coupon.controller.SeckillSessionController”类,代码如下:

/**
 * 获取最近3天的秒杀商品
 */
@GetMapping("/lasts3DaySession")
public R getLasts3DaySession(){
    List<SeckillSessionEntity> session = seckillSessionService.getLasts3DaySession();
    return R.ok().setData(session);
}

添加“com.atguigu.gulimall.coupon.service.SeckillSessionService”类,代码如下:

/**
 * 获取最近3天的秒杀商品
 *
 * @return
 */
List<SeckillSessionEntity> getLasts3DaySession();

添加“com.atguigu.gulimall.coupon.service.impl.SeckillSessionServiceImpl”类,代码如下:

在查询近三天的秒杀场次时时,同时把关联的消息<List>存储进去。

包含的字段:

商品id

秒杀价格

秒杀数量

秒杀限制数量

秒杀排序

@Override
public List<SeckillSessionEntity> getLasts3DaySession() {
    // 计算最近三天
    List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
    if (!CollectionUtils.isEmpty(list)) {
        return list.stream().map(session -> {
            Long id = session.getId();
            List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
            session.setRelationEntities(relationEntities);
            return session;
        }).collect(Collectors.toList());
    }
    return null;
}

/**
 * 起始时间
 *
 * @return
 */
private String startTime() {
    LocalDate now = LocalDate.now();
    LocalTime time = LocalTime.MIN;
    LocalDateTime start = LocalDateTime.of(now, time);
    String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}

/**
 * 结束时间
 *
 * @return
 */
private String endTime() {
    LocalDate now = LocalDate.now();
    LocalDate localDate = now.plusDays(2);
    LocalTime time = LocalTime.MIN;
    LocalDateTime end = LocalDateTime.of(localDate, time);
    String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}

4.4.2、在redis中缓存秒杀活动信息

以key,value的方式存储在redis

private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

key:(key为 seckill:sessions:+时间范围)

SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;

value:(值为 场次id_商品id<List>)

item.getPromotionSessionId().toString() + "_" + item.getSkuId().toString()

    /**
     * 缓存秒杀活动信息
     *
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if (!hasKey) {
                List<String> collect = session.getRelationEntities()
                        .stream()
                        .map(item -> item.getPromotionSessionId().toString() + "_" + item.getSkuId().toString())
                        .collect(Collectors.toList());
                System.out.println("saveSessionInfos------------------------" + collect);
                // 缓存活动信息(list操作)
                stringRedisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }

4.4.3、在redis中缓存获得关联秒杀活动的商品信息

实现思路:

1.

   /**
     * 缓存获得关联秒杀的商品信息
     *
     * @param List<SeckillSessionWithSkus> 
     */
    private void saveSessionSkuInfos(List<SeckillSessionWithSkus>  sessions) {         // 准备hash操作         
    BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);         
    sessions.stream().
    forEach(session -> {             session.getRelationEntities().stream().forEach(seckillSkuVo -> {                 if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {                     // 缓存商品                     SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
    
                // 1、sku的基本信息
                R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if (0 == r.getCode()) {
                    SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    redisTo.setSkuInfo(skuInfo);
                }

                // 2、sku的秒杀信息
                BeanUtils.copyProperties(seckillSkuVo, redisTo);

                // 3、设置当前商品的秒杀时间信息
                redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());

                // 4、随机码
                String token = UUID.randomUUID().toString().replace("_", "");
                redisTo.setRandomCode(token);
                
                // 如果当前这个场次的商品的库存信息已经上架就不需要上架
                // 5、使用库存作为分布式信号量 ==》限流
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                // 5.1、商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                String jsonString = JSON.toJSONString(redisTo);
                log.info("saveSessionSkuInfos------------------------" + jsonString);
                ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
            }
        });
    });
}
    /**
     * 缓存秒杀活动所关联的商品信息
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {

        sessions.stream().forEach(session -> {
            //准备hash操作,绑定hash
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //生成随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                if (!operations.hasKey(redisKey)) {

                    //缓存我们商品信息
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    Long skuId = seckillSkuVo.getSkuId();
                    //1、先查询sku的基本信息,调用远程服务
                    R info = productFeignService.getSkuInfo(skuId);
                    if (info.getCode() == 0) {
                        SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
                        redisTo.setSkuInfo(skuInfo);
                    }

                    //2、sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo,redisTo);

                    //3、设置当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());

                    //4、设置商品的随机码(防止恶意攻击)
                    redisTo.setRandomCode(token);

                    //序列化json格式存入Redis中
                    String seckillValue = JSON.toJSONString(redisTo);
                    operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

                    //如果当前这个场次的商品库存信息已经上架就不需要上架
                    //5、使用库存作为分布式Redisson信号量(限流)
                    // 使用库存作为分布式信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });
        });
    }

添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:

@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}

4.5、幂等性保证

定时任务-分布式下的问题

由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法
分布式锁:锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态


修改“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下:

@Slf4j
@Component
public class SeckillSkuScheduled {
@Autowired
private SeckillService seckillService;

@Autowired
private RedissonClient redissonClient;

private final String upload_lock = "seckill:upload:lock";

/**
 * 上架最近3天的秒杀商品
 * 幂等性处理
 */
@Scheduled(cron = "*/3 0 0 * * ?")
public void uploadSeckillSkuLatest3Days() {
    // 重复上架无需处理
    log.info("上架秒杀的信息......");
    // 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态
    RLock lock = redissonClient.getLock(upload_lock);
    lock.lock(10, TimeUnit.SECONDS);
    try {
        seckillService.uploadSeckillSkuLatest3Days();
    } finally {
        lock.unlock();
    }

}
}

5、获取当前秒杀商品

5.1、获取到当前可以参加秒杀的商品信息

添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下

@Controller
public class SeckillController {
@Autowired
private SeckillService seckillService;

/**
 * 获取到当前可以参加秒杀的商品信息
 *
 * @return
 */
@ResponseBody
@GetMapping(value = "/getCurrentSeckillSkus")
public R getCurrentSeckillSkus() {
    List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
    return R.ok().setData(vos);
}
}

添加“com.atguigu.gulimall.seckill.service.SeckillService”类:代码如下:

 List getCurrentSeckillSkus(); 

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

获取当前秒杀场次的商品

逻辑:

1. 查询到所有的场次信息,场次的key的格式为==>前缀:开始时间_结束时间

   @Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    // 获取秒杀活动场次的的所有key
    Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
    long currentTime = System.currentTimeMillis();
    for (String key : keys) {
        String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
        String[] split = replace.split("_");
        long startTime = Long.parseLong(split[0]);
        long endTime = Long.parseLong(split[1]);
        // 当前秒杀活动处于有效期内
        // 幂等性判断
        if (currentTime > startTime && currentTime < endTime) {
            // 获取这个秒杀场次的所有商品信息
            // 遍历每个场次,拿到每个场次的所有值,范围在-100, 100
            // 值的格式为 场次id_商品id
            List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
            
            /*
              private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:"; //秒杀活动信息
              private final String SKUKILL_CACHE_PREFIX = "seckill:skus:"; // 秒杀商品信息
              private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
            */
            // 获取SKUKILL_CACHE_PREFIX 秒杀活动的所有信息
            BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            assert range != null;
            // Has 存储SKUKILL_CACHE_PREFIX中 通过场次id_商品id获取商品的具体信息
            List<String> strings = hashOps.multiGet(range);
            if (!CollectionUtils.isEmpty(strings)) {
            // 将获取到的商品信息转换成SeckillSkuRedisTo类型
                return strings.stream().map(item -> JSON.parseObject(item, SeckillSkuRedisTo.class))
                        .collect(Collectors.toList());
            }
            break;
        }
    }
    return null;
}

5.2、首页获取并拼装数据

1、配置网关

    - id: gulimall_seckill_route
      uri: lb://gulimall-seckill
      predicates:
        - Host=seckill.gulimall.com

2、配置域名

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
192.168.119.127 cart.gulimall.com
192.168.119.127 order.gulimall.com
192.168.119.127 member.gulimall.com
192.168.119.127 seckill.gulimall.com


3、修改gulimall-product模块的index.html页面,代码如下:

    <div class="swiper-container swiper_section_second_list_left">
        <div class="swiper-wrapper">
            <div class="swiper-slide">
                <!-- 动态拼装秒杀商品信息 -->
                <ul id="seckillSkuContent"></ul>

            </div>

4、首页效果展示

6、商品详情页获取当前商品的秒杀信息

修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

@Override
public SkuItemVo item(Long skuId) {
    SkuItemVo skuItemVo = new SkuItemVo();
    // 使用异步编排
    CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
        // 1、sku基本信息获取    pms_sku_info
        SkuInfoEntity info = getById(skuId);
        skuItemVo.setInfo(info);
        return info;
    }, executor);

    CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
        // 2、sku的图片信息      pms_sku_images
        List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);
    }, executor);

    CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
        // 3、获取spu的销售属性组合
        List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
        skuItemVo.setSaleAttr(saleAttrVos);
    }, executor);

    CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
        // 4、获取spu的介绍 pms_spu_info_desc
        SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
        skuItemVo.setDesc(desc);
    }, executor);

    CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
        // 5、获取spu的规格参数信息
        List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
        skuItemVo.setGroupAttrs(attrGroupVos);
    }, executor);

    CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
        // 6、查询当前sku是否参与秒杀优惠
        R r = seckillFeignService.getSkuSecKillInfo(skuId);
        if (0 == r.getCode()) {
            SeckillInfoVo skillInfo = r.getData(new TypeReference<SeckillInfoVo>() {
            });
            skuItemVo.setSeckillInfo(skillInfo);
        }
    }, executor);
    // 等待所有任务执行完成
    try {
        CompletableFuture.allOf(saleAttrFuture, imageFuture, descFuture, baseAttrFuture, seckillFuture).get();
    } catch (InterruptedException e) {
        log.error("1等待所有任务执行完成异常{}", e);
    } catch (ExecutionException e) {
        log.error("2等待所有任务执行完成异常{}", e);
    }
    return skuItemVo;
}

添加“com.atguigu.gulimall.product.vo.SeckillInfoVo”类,代码如下:

@Data
public class SeckillInfoVo {
private Long id;
/**
 * 活动id
 */
private Long promotionId;
/**
 * 活动场次id
 */
private Long promotionSessionId;
/**
 * 商品id
 */
private Long skuId;
/**
 * 秒杀价格
 */
private BigDecimal seckillPrice;
/**
 * 秒杀总量
 */
private Integer seckillCount;
/**
 * 每人限购数量
 */
private Integer seckillLimit;
/**
 * 排序
 */
private Integer seckillSort;

/**
 * 当前商品秒杀的开始时间
 */
private Long startTime;

/**
 * 当前商品秒杀的结束时间
 */
private Long endTime;

/**
 * 当前商品秒杀的随机码
 */
private String randomCode;
}

修改“com.atguigu.gulimall.product.vo.SkuItemVo”类,代码如下:

远程调用gulimall-seckill

添加“com.atguigu.gulimall.product.feign.SeckillFeignService”类,代码如下:

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}


修改“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

/**
 * 获取秒杀商品的详情信息
 */
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSecKillInfo(@PathVariable("skuId") Long skuId){
    SeckillSkuRedisTo to = seckillService.getSkuSecKillInfo(skuId);
    return R.ok().setData(to);
}

修改“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下:

SeckillSkuRedisTo getSkuSecKillInfo(Long skuId);

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

查获取指定skuid的商品的秒杀信息

1.商品的存储格式为 场次id_商品id

2.通过正则表达式找到匹配的 String regx = "\\d_" + skuId;

3.处于秒杀时间段的商品设置随机码

@Override
public SeckillSkuRedisTo getSkuSecKillInfo(Long skuId) {
    // 1、获取所有需要参与秒杀的商品的key
    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    Set<String> keys = hashOps.keys();
    if (null != keys){
        // 1_10(正则表达式)
        String regx = "\\d_" + skuId;
        for (String key : keys) {
            // 匹配场次商品id
            if (Pattern.matches(regx, key)){
                String json = hashOps.get(key);
                SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);

                // 随机码
                long current = new Date().getTime();
                // 不在秒杀时间范围内的随机码置空,不返回
                if (current < skuRedisTo.getStartTime() || current > skuRedisTo.getEndTime()){
                    skuRedisTo.setRandomCode(null);
                }
                return skuRedisTo;
            }

        }
    }
    return null;
}

修改gulimall-product模块的item.html页面,代码如下:

            <div class="box-summary clear">
                <ul>
                    <li>京东价</li>

                    <li>
                        <span>¥</span>

                        <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>

                    </li>

                    <li style="color: red" th:if="${item.seckillInfo != null}">
                                <span th:if="${#dates.createNow().getTime() <= item.seckillInfo.startTime}">
                                    商品将会在[[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
                                </span>


                        <span th:if="${item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
                                    秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
                                </span>


                    </li>

                    <li>
                        <a href="/static/item/">
                            预约说明
                        </a>

                    </li>

                </ul>

            </div>

详情页效果展示:

7、登录检查

1.设置session域,

2.设置拦截器

1、pom引入SpringSession依赖和redis

    <!--整合SpringSession完成session共享问题-->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>


    <!--引入redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

2、在配置文件中添加SpringSession的保存方式

#SpringSession保存方式
spring.session.store-type=redis


3、SpringSession的配置

添加“com.atguigu.gulimall.seckill.config.GulimallSessionConfig”类,代码如下:

@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    cookieSerializer.setDomainName("gulimall.com");
    cookieSerializer.setCookieName("GULISESSION");
    return cookieSerializer;
}

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
    return new GenericJackson2JsonRedisSerializer();
}
}

4、主启动类开启RedisHttpSession这个功能

@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
    SpringApplication.run(GulimallSeckillApplication.class, args);
}
}

添加用户登录拦截器

添加“com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor”类,代码如下:

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String requestURI = request.getRequestURI();
    AntPathMatcher matcher = new AntPathMatcher();
    boolean match = matcher.match("/kill", requestURI);
    // 如果是秒杀,需要判断是否登录,其他路径直接放行不需要判断
    if (match) {
        MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute != null){
            loginUser.set(attribute);
            return true;
        }else {
            //没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
    return true;
}
}

把拦截器配置到spring中,否则拦截器不生效。
添加addInterceptors表示当前项目的所有请求都要讲过这个拦截请求

添加“com.atguigu.gulimall.seckill.config.SeckillWebConfig”类,代码如下:

@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}


修改item.html

                        <div class="box-btns-two" th:if="${item.seckillInfo != null && (item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime)}">
                            <a href="#" id="seckillA" th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfo.promotionSessionId},code=${item.seckillInfo.randomCode}">
                                立即抢购
                            </a>

                        </div>

                        <div class="box-btns-two" th:if="${item.seckillInfo == null || (item.seckillInfo.startTime > #dates.createNow().getTime() || #dates.createNow().getTime() > item.seckillInfo.endTime)}">
                            <a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">
                                加入购物车
                            </a>

                        </div>

前端要考虑秒杀系统设计的限流思想
在进行立即抢购之前,前端先进行判断是否登录

    /**
     * 立即抢购
     */
    $("#seckillA").click(function () {
        var isLogin = [[${session.loginUser != null}]];//true
        if (isLogin) {
            var killId = $(this).attr("sessionId") + "_" + $(this).attr("skuId");
            var key = $(this).attr("code");
            var num = $("#numInput").val();
            location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + key + "&num=" + num;
        } else {
            alert("秒杀请先登录!");
        }
 
        return false;
    });

8、秒杀系统设计

8.1、秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。
限流方式:
1.前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
2.nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
3.网关限流,限流的过滤器
4.代码中使用分布式信号量
5.abbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

8.2、秒杀架构


秒杀架构思路

项目独立部署,独立秒杀模块gulimall-seckill
使用定时任务每天三点上架最新秒杀商品,削减高峰期压力
秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口
库存预热,先从数据库中扣除一部分库存以redisson信号量的形式存储在redis中
队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单
秒杀架构图

8.3、秒杀( 高并发) 系统关注的问题

8.4、秒杀流程


秒杀流程图一

秒杀流程图二

我们使用秒杀流程图二来实现功能

8.5、代码实现

8.5.1、秒杀接口


点击立即抢购时,会发送请求

秒杀请求会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单

添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

* @param killId 秒杀商品id

* @param key 随机码

* @param num 数量

@GetMapping("/kill")
public R seckill(@RequestParam("killId") String killId,
                 @RequestParam("key") String key,
                 @RequestParam("num") Integer num){
    // 1、判断是否登录(登录拦截器已经自动处理)

    String orderSn = seckillService.kill(killId, key, num);
    return R.ok().setData(orderSn);
}

修改“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下:

/**
 * 秒杀
 *
 * @param killId 秒杀商品id
 * @param key 随机码
 * @param num 数量
 * @return
 */
String kill(String killId, String key, Integer num);

使用队列削峰 做流量削峰

引入rabbitMQ依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

在配置文件中添加rabbitMQ的配置

#RabbitMQ的地址
spring.rabbitmq.host=192.168.119.127
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
配置RabbitMQ(消息确认机制暂未配置)

添加“com.atguigu.gulimall.seckill.config.MyRabbitConfig”类,代码如下:

@Configuration
public class MyRabbitConfig {
    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
 
}

主启动类不用启动//@EnableRabbit 不用监听RabbitMQ, 因为我们只用来发送消息,不接收消息

重要

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

// TODO 上架秒杀商品的时候,每一个数据都有一个过期时间。
// TODO 秒杀后续的流程,简化了 收货地址等信息。
// String killId 商品id, String key 随机码, Integer num 购买数量
@Override
public String kill(String killId, String key, Integer num) {
    long s1 = System.currentTimeMillis();
    // 0、从拦截器获取用户信息
    MemberResponseVO repsVo = LoginUserInterceptor.loginUser.get();
    // 1、获取当前商品的详细信息
    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    // killId格式为 场次id_商品id
    String json = hashOps.get(killId);
    if (!StringUtils.isEmpty(json)){
        // 拿到商品的秒杀信息
        SeckillSkuRedisTo redis = JSON.parseObject(json, SeckillSkuRedisTo.class);
        // 2、校验合法性
        Long startTime = redis.getStartTime();
        Long endTime = redis.getEndTime();
        long current = new Date().getTime();
        long ttl = endTime - startTime; // 场次存活时间
        // 2.1、校验时间的合法性
        if (current >= startTime && current <= endTime){
            // 2.2、校验随机码和商品id
            String randomCode = redis.getRandomCode();
            String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
            // 判断随机码 和 商品id是否匹配
            if (randomCode.equals(key) && skuId.equals(killId)){
                // 2.3、验证购物的数量是否合理,小于秒杀限制量
                if (num <= redis.getSeckillLimit()){
                    // 2.4、验证这个人是否购买过。
                    // 幂等性处理。如果只要秒杀成功,就去占位  
                    // userId_sessionId_skillId
                    // SETNX
                    String redisKey = repsVo.getId() + "_" + skuId;
                    // 2.4.1、自动过期--
                    // 通过在redis中使用 用户id_skuId 来占位看是否买过
                    // 存储的是购买数量
                    Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (ifAbsent){
                        // 2.5、占位成功,说明该用户未秒杀过该商品,则继续尝试获取库存信号量
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        // 尝试消耗 num个信号量
                        boolean b = semaphore.tryAcquire(num);
                        if (b){
                            // 秒杀成功
                            // 2.6、快速下单发送MQ消息 10ms
                            String orderSn = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(orderSn);
                            orderTo.setMemberId(repsVo.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                            orderTo.setSkuId(redis.getSkuId());
                            orderTo.setSeckillPrice(redis.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2-s1));
                            return orderSn;
                        }
                        return null;

                    }else {
                        // 2.4.2、说明已经买过
                        return null;
                    }

                }

            }else {
                return null;
            }
        }else {

            return null;
        }

    }
    return null;
}

新建“com.atguigu.common.to.mq.SeckillOrderTo”类,代码如下:

@Data
public class SeckillOrderTo {
/**
 * 订单号
 */
private String orderSn;

/**
 * 活动场次id
 */
private Long promotionSessionId;

/**
 * 商品id
 */
private Long skuId;

/**
 * 秒杀价格
 */
private BigDecimal seckillPrice;

/**
 * 购买数量
 */
private Integer num;

/**
 * 会员id
 */
private Long memberId;
}

8.5.2、创建订单


gulimall-order

1、创建秒杀队列,并绑定队列到订单交换机

修改“com.atguigu.gulimall.order.config.MyMQConfig”类,代码如下:

/**
 * 订单秒杀队列
 */
@Bean
public Queue orderSeckillOrderQueue() {
    return new Queue("order.seckill.order.queue", true, false, false);
}

/**
 * 绑定订单秒杀队列
 */
@Bean
public Binding orderSeckillOrderQueueBinding() {
    return new Binding("order.seckill.order.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.seckill.order",
            null);
}

2、监听消息队列

添加“com.atguigu.gulimall.order.listener.OrderSeckillListener”类,代码如下:

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
private OrderService orderService;

@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
    try {
        log.info("准备创建秒杀单的详细信息。。。");
        orderService.createSeckillOrder(seckillOrder);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        // 修改失败 拒绝消息 使消息重新入队
        channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
    }
}
}

3、创建秒杀订单

添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

/**
 * 创建秒杀订单
 * 
 * @param seckillOrder
 */
void createSeckillOrder(SeckillOrderTo seckillOrder);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
    // TODO 1、保存订单信息
    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setOrderSn(seckillOrder.getOrderSn());
    orderEntity.setMemberId(seckillOrder.getMemberId());
    orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
    orderEntity.setPayAmount(multiply);
    this.save(orderEntity);
    // TODO 2、保存订单项信息
    OrderItemEntity entity = new OrderItemEntity();
    entity.setOrderSn(seckillOrder.getOrderSn());
    entity.setRealAmount(multiply);
    //TODO 3、获取当前sku的详细信息进行设置
    entity.setSkuQuantity(seckillOrder.getNum());

    orderItemService.save(entity);
}

8.5.3、秒杀页面完成


把gulimall-cart服务的成功页面放到gulimall-seckill服务里

修改里面的静态资源路径,我们借用购物车的资源,替换如下:

引入thymeleaf依赖

     <!--模板引擎 thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

在配置里关闭thymeleaf缓存

#关闭缓存

spring.thymeleaf.cache=false



修改“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

@GetMapping("/kill")
public String seckill(@RequestParam("killId") String killId,
                 @RequestParam("key") String key,
                 @RequestParam("num") Integer num,
                 Model model){
    // 1、判断是否登录(登录拦截器已经自动处理)

    String orderSn = seckillService.kill(killId, key, num);
    model.addAttribute("orderSn", orderSn);
    return "success";
}
<div class="main">
 
    <div class="success-wrap">
        <div class="w" id="result">
            <div class="m succeed-box">
                <div th:if="${orderSn != null}" class="mc success-cont">
                    <h1>恭喜,秒杀成功,订单号[[${orderSn}]]</h1>
                    <h2>正在准备订单数据,10s以后自动跳转支付 <a style="color: red" th:href="${'http://order.gulimall.com/payOrder?orderSn='+orderSn}">去支付</a></h2>
 
                </div>
                <div th:if="${orderSn == null}">
                    <h1>手气不好,秒杀失败,下次再来</h1>
                </div>
            </div>
        </div>
    </div>
 
</div>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值