1. Day15 秒杀前端

所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。

秒杀商品通常有两种限制:库存限制、时间限制。

需求:

  1. 秒杀频道首页列出秒杀商品
  2. 点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
  3. 秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
  4. 当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。

2. 秒杀商品存入缓存

15. Day15 秒杀前端 - 图1

秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。

2.1. 秒杀服务搭建

  1. 在changgou_service_api项目下 新建changgou_service_seckill_api服务 添加公共依赖
  1. <dependencies>
  2. <dependency>
  3. <groupId>com.changgou</groupId>
  4. <artifactId>changgou_common</artifactId>
  5. <version>1.0-SNAPSHOT</version>
  6. </dependency>
  7. </dependencies>
  1. 在changgou_service_seckill_api服务下 添加com.changgou.seckill.feigncom.changgou.seckill.pojo两个包

  2. 将资源文件夹下的两个pojo类放入com.changgou.seckill.pojo包中

  3. 在changgou_parent项目下 新建服务changgou_service_seckill 添加依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>com.changgou</groupId>
  4. <artifactId>changgou_common_db</artifactId>
  5. <version>1.0-SNAPSHOT</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.cloud</groupId>
  9. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.changgou</groupId>
  13. <artifactId>changgou_service_order_api</artifactId>
  14. <version>1.0-SNAPSHOT</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>com.changgou</groupId>
  18. <artifactId>changgou_service_seckill_api</artifactId>
  19. <version>1.0-SNAPSHOT</version>
  20. </dependency>
  21. <dependency>
  22. <groupId>com.changgou</groupId>
  23. <artifactId>changgou_service_goods_api</artifactId>
  24. <version>1.0-SNAPSHOT</version>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.amqp</groupId>
  28. <artifactId>spring-rabbit</artifactId>
  29. </dependency>
  30. <!--oauth依赖-->
  31. <dependency>
  32. <groupId>org.springframework.cloud</groupId>
  33. <artifactId>spring-cloud-starter-oauth2</artifactId>
  34. </dependency>
  35. </dependencies>
  1. 创建包com.changgou.seckill 然后创建启动类 SecKillApplication
  1. @SpringBootApplication
  2. @EnableEurekaClient
  3. @MapperScan(basePackages = {"com.changgou.seckill.dao"})
  4. public class SecKillApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(SecKillApplication.class, args);
  7. }
  8. //idwork
  9. @Bean
  10. public IdWorker idWorker() {
  11. return new IdWorker(1, 1);
  12. }
  13. //设置redisTemplate序列化
  14. @Bean
  15. public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  16. // 1.创建 redisTemplate 模版
  17. RedisTemplate<Object, Object> template = new RedisTemplate<>();
  18. // 2.关联 redisConnectionFactory
  19. template.setConnectionFactory(redisConnectionFactory);
  20. // 3.创建 序列化类
  21. GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
  22. // 6.序列化类,对象映射设置
  23. // 7.设置 value 的转化格式和 key 的转化格式
  24. template.setValueSerializer(genericToStringSerializer);
  25. template.setKeySerializer(new StringRedisSerializer());
  26. template.afterPropertiesSet();
  27. return template;
  28. }
  29. }
  1. 设置application
  1. server:
  2. port: 9016
  3. spring:
  4. jackson:
  5. time-zone: GMT+8
  6. application:
  7. name: seckill
  8. datasource:
  9. driver-class-name: com.mysql.jdbc.Driver
  10. url: jdbc:mysql://192.168.130.128:3306/changgou_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8
  11. username: root
  12. password: root
  13. main:
  14. allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  15. redis:
  16. host: 192.168.130.128
  17. rabbitmq:
  18. host: 192.168.130.128
  19. eureka:
  20. client:
  21. service-url:
  22. defaultZone: http://127.0.0.1:6868/eureka
  23. instance:
  24. prefer-ip-address: true
  25. feign:
  26. hystrix:
  27. enabled: true
  28. client:
  29. config:
  30. default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
  31. connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
  32. readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
  33. #hystrix 配置
  34. hystrix:
  35. command:
  36. default:
  37. execution:
  38. timeout:
  39. #如果enabled设置为false,则请求超时交给ribbon控制
  40. enabled: true
  41. isolation:
  42. strategy: SEMAPHORE
  43. thread:
  44. # 熔断器超时时间,默认:1000/毫秒
  45. timeoutInMilliseconds: 20000
  1. 将author的公钥复制一份到resource目录下

  2. 创建 com.changgou.config 包 然后新建ResourceServerConfig配置类

  1. @Configuration
  2. @EnableResourceServer
  3. //开启方法上的PreAuthorize注解
  4. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  5. public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  6. //公钥
  7. private static final String PUBLIC_KEY = "public.key";
  8. /***
  9. * 定义JwtTokenStore
  10. * @param jwtAccessTokenConverter
  11. * @return
  12. */
  13. @Bean
  14. public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
  15. return new JwtTokenStore(jwtAccessTokenConverter);
  16. }
  17. /***
  18. * 定义JJwtAccessTokenConverter
  19. * @return
  20. */
  21. @Bean
  22. public JwtAccessTokenConverter jwtAccessTokenConverter() {
  23. JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  24. converter.setVerifierKey(getPubKey());
  25. return converter;
  26. }
  27. /**
  28. * 获取非对称加密公钥 Key
  29. *
  30. * @return 公钥 Key
  31. */
  32. private String getPubKey() {
  33. Resource resource = new ClassPathResource(PUBLIC_KEY);
  34. try {
  35. InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
  36. BufferedReader br = new BufferedReader(inputStreamReader);
  37. return br.lines().collect(Collectors.joining("\n"));
  38. } catch (IOException ioe) {
  39. return null;
  40. }
  41. }
  42. /***
  43. * Http安全配置,对每个到达系统的http请求链接进行校验
  44. * @param http
  45. * @throws Exception
  46. */
  47. @Override
  48. public void configure(HttpSecurity http) throws Exception {
  49. //所有请求必须认证通过
  50. http.authorizeRequests()
  51. .anyRequest().
  52. authenticated(); //其他地址需要认证授权
  53. }
  54. }
  1. 更改网关路径过滤类 添加秒杀工程过滤信息

进到到changgou_gateway_web项目下的 com.changgou.web.gateway.filter.URLFilter 过滤类中

添加 路径为/api/seckill 之前我们添加过 所有无需添加

15. Day15 秒杀前端 - 图2

  1. 更改网关的application配置文件 添加秒杀服务的路由转发
  1. #秒杀微服务
  2. - id: changgou_seckill_route
  3. uri: lb://seckill
  4. predicates:
  5. - Path=/api/seckill/**
  6. filters:
  7. - StripPrefix=1

15. Day15 秒杀前端 - 图3

2.2. 时间操作

15. Day15 秒杀前端 - 图4

15. Day15 秒杀前端 - 图5

根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。

缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。

  1. 将资源文件夹下的DateUtil工具类 放到changgou_common的util包下
  1. public class DateUtil {
  2. /***
  3. * 从yyyy-MM-dd HH:mm格式转成yyyyMMddHH格式
  4. * @param dateStr
  5. * @return
  6. */
  7. public static String formatStr(String dateStr){
  8. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
  9. try {
  10. Date date = simpleDateFormat.parse(dateStr);
  11. simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");
  12. return simpleDateFormat.format(date);
  13. } catch (ParseException e) {
  14. e.printStackTrace();
  15. }
  16. return null;
  17. }
  18. /***
  19. * 获取指定日期的凌晨
  20. * @return
  21. */
  22. public static Date toDayStartHour(Date date){
  23. Calendar calendar = Calendar.getInstance();
  24. calendar.setTime(date);
  25. calendar.set(Calendar.HOUR_OF_DAY, 0);
  26. calendar.set(Calendar.MINUTE, 0);
  27. calendar.set(Calendar.SECOND, 0);
  28. calendar.set(Calendar.MILLISECOND, 0);
  29. Date start = calendar.getTime();
  30. return start;
  31. }
  32. /***
  33. * 时间增加N分钟
  34. * @param date
  35. * @param minutes
  36. * @return
  37. */
  38. public static Date addDateMinutes(Date date,int minutes){
  39. Calendar calendar = Calendar.getInstance();
  40. calendar.setTime(date);
  41. calendar.add(Calendar.MINUTE, minutes);// 24小时制
  42. date = calendar.getTime();
  43. return date;
  44. }
  45. /***
  46. * 时间递增N小时
  47. * @param hour
  48. * @return
  49. */
  50. public static Date addDateHour(Date date,int hour){
  51. Calendar calendar = Calendar.getInstance();
  52. calendar.setTime(date);
  53. calendar.add(Calendar.HOUR, hour);// 24小时制
  54. date = calendar.getTime();
  55. return date;
  56. }
  57. /***
  58. * 获取时间菜单
  59. * @return
  60. */
  61. public static List<Date> getDateMenus(){
  62. //定义一个List<Date>集合,存储所有时间段
  63. List<Date> dates = new ArrayList<Date>();
  64. //循环12次
  65. Date date = toDayStartHour(new Date()); //凌晨
  66. for (int i = 0; i <12 ; i++) {
  67. //每次递增2小时,将每次递增的时间存入到List<Date>集合中
  68. dates.add(addDateHour(date,i*2));
  69. }
  70. //判断当前时间属于哪个时间范围
  71. Date now = new Date();
  72. for (Date cdate : dates) {
  73. //开始时间<=当前时间<开始时间+2小时
  74. if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){
  75. now = cdate;
  76. break;
  77. }
  78. }
  79. //当前需要显示的时间菜单
  80. List<Date> dateMenus = new ArrayList<Date>();
  81. for (int i = 0; i <5 ; i++) {
  82. dateMenus.add(addDateHour(now,i*2));
  83. }
  84. return dateMenus;
  85. }
  86. /***
  87. * 时间转成yyyyMMddHH
  88. * @param date
  89. * @return
  90. */
  91. public static String date2Str(Date date){
  92. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");
  93. return simpleDateFormat.format(date);
  94. }
  95. }

2.2.1. 当前业务整体流程分析

  1. 1.查询所有符合条件的秒杀商品
  2. 1) 获取时间段集合并循环遍历出每一个时间段
  3. 2) 获取每一个时间段名称,用于后续rediskey的设置
  4. 3) 状态必须为审核通过 status=1
  5. 4) 商品库存个数>0
  6. 5) 秒杀商品开始时间>=当前时间段
  7. 6) 秒杀商品结束<当前时间段+2小时
  8. 7) 排除之前已经加载到Redis缓存中的商品数据
  9. 8) 执行查询获取对应的结果集
  10. 2.将秒杀商品存入缓存

2.3. 代码实现

2.3.1. 更改启动类,添加开启定时任务注解

  1. @EnableScheduling

2.3.2. 定义定时任务类

秒杀工程新建task包,并新建任务类SeckillGoodsPushTask

业务逻辑:

1)获取秒杀时间段菜单信息

2)遍历每一个时间段,添加该时间段下秒杀商品

2.1)将当前时间段转换为String,作为redis中的key

2.2)查询商品信息(状态为1,库存大于0,秒杀商品开始时间大于当前时间段,秒杀商品结束时间小于当前时间段,当前商品的id不在redis中)

3)添加redis

  1. @Component
  2. public class SeckillGoodsPushTask {
  3. @Autowired
  4. private SeckillGoodsMapper seckillGoodsMapper;
  5. // redis key的前缀
  6. public static final String SECKILL_GOODS_KEY = "seckill_goods";
  7. @Autowired
  8. private RedisTemplate redisTemplate;
  9. @Scheduled(cron = "0/30 * * * * ?")
  10. public void loadSecKillGoodsToRedis() {
  11. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  12. //获取当前时间到 每2小时的5个时间集合
  13. List<Date> dateMenus = DateUtil.getDateMenus();
  14. for (Date dateMenu : dateMenus) {
  15. //获取每个时间段名称 用于redis中作为key
  16. String redisExtName = DateUtil.date2Str(dateMenu);
  17. Example example = new Example(SeckillGoods.class);
  18. Example.Criteria criteria = example.createCriteria();
  19. // 状态要为1 表示已审核
  20. criteria.andEqualTo("status", "1");
  21. // 商品库存个数 要大于0
  22. criteria.andGreaterThan("stockCount", 0);
  23. // 秒杀商品开始时间 >= 当前时间段
  24. criteria.andGreaterThanOrEqualTo("startTime", sdf.format(dateMenu));
  25. // 并且秒杀商品结束时间 < 当前时间段+2
  26. criteria.andLessThan("endTime", sdf.format(DateUtil.addDateHour(dateMenu, 2)));
  27. // 获取当前时间段在redis的所有商品的key值 key为redis前缀+时间段名
  28. Set keys = redisTemplate.boundHashOps(SECKILL_GOODS_KEY + redisExtName).keys();
  29. if (keys != null && keys.size() > 0) {
  30. // 排除之前已经加载到Redis缓存中的商品 即在redis已经存在
  31. criteria.andNotIn("id", keys);
  32. }
  33. // 符合以上条件的结果集合
  34. List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example);
  35. // 添加到redis缓存中
  36. for (SeckillGoods seckillGoods : seckillGoodsList) {
  37. redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName, seckillGoods.getGoodsId(), seckillGoods);
  38. }
  39. }
  40. }
  41. }

3. 秒杀页面

15. Day15 秒杀前端 - 图6

秒杀商品首页会显示处于秒杀中以及未开始秒杀的商品。

3.1. 需求分析

秒杀首页需要显示不同时间段的秒杀商品信息,然后当用户选择不同的时间段,查询该时间段下的秒杀商品,实现过程分为两大过程:

  1. 1) 加载时间菜单
  2. 2)加载时间菜单下秒杀商品信息

3.1.1. 加载时间菜单分析

每2个小时就会切换一次抢购活动,所以商品发布的时候,我们将时间定格在2小时内抢购,每次发布商品的时候,商品抢购开始时间和结束时间是这2小时的边界。

每2小时会有一批商品参与抢购,所以我们可以将24小时切分为12个菜单,每个菜单都是个2小时的时间段,当前选中的时间菜单需要根据当前时间判断,判断当前时间属于哪个秒杀时间段,然后将该时间段作为选中的第1个时间菜单。

3.1.2. 加载对应秒杀商品分析

进入首页时,到后台查询时间菜单信息,然后将第1个菜单的时间段作为key,在Redis中查询秒杀商品集合,并显示到页面,页面每次点击切换不同时间段菜单的时候,都将时间段传入到后台,后台根据时间段获取对应的秒杀商品集合。

3.2. 秒杀渲染服务 - 渲染秒杀首页

  1. 创建工程changgou_web_seckill,用于秒杀页面渲染 添加依赖
  1. <dependencies>
  2. <dependency>
  3. <groupId>com.changgou</groupId>
  4. <artifactId>changgou_service_seckill_api</artifactId>
  5. <version>1.0-SNAPSHOT</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  10. </dependency>
  11. </dependencies>
  1. 创建com.changgou.seckill.web包 并新建启动类 SecKillWebApplication
  1. @SpringBootApplication
  2. @EnableEurekaClient
  3. @EnableFeignClients(basePackages = {"com.changgou.seckill.feign"})
  4. public class SecKillWebApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(SecKillWebApplication.class, args);
  7. }
  8. /**
  9. * Feign拦截器
  10. */
  11. @Bean
  12. public FeignInterceptor feignInterceptor() {
  13. return new FeignInterceptor();
  14. }
  15. /**
  16. * 设置 redisTemplate 的序列化设置
  17. * @param redisConnectionFactory
  18. * @return
  19. */
  20. @Bean
  21. public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  22. // 1.创建 redisTemplate 模版
  23. RedisTemplate<Object, Object> template = new RedisTemplate<>();
  24. // 2.关联 redisConnectionFactory
  25. template.setConnectionFactory(redisConnectionFactory);
  26. // 3.创建 序列化类
  27. GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
  28. // 6.序列化类,对象映射设置
  29. // 7.设置 value 的转化格式和 key 的转化格式
  30. template.setValueSerializer(genericToStringSerializer);
  31. template.setKeySerializer(new StringRedisSerializer());
  32. template.afterPropertiesSet();
  33. return template;
  34. }
  35. }
  1. application
  1. server:
  2. port: 9104
  3. eureka:
  4. client:
  5. service-url:
  6. defaultZone: http://127.0.0.1:6868/eureka
  7. instance:
  8. prefer-ip-address: true
  9. feign:
  10. hystrix:
  11. enabled: true
  12. spring:
  13. jackson:
  14. time-zone: GMT+8
  15. thymeleaf:
  16. cache: false
  17. application:
  18. name: seckill-web
  19. main:
  20. allow-bean-definition-overriding: true
  21. redis:
  22. host: 192.168.130.128
  23. #hystrix 配置
  24. hystrix:
  25. command:
  26. default:
  27. execution:
  28. timeout:
  29. #如果enabled设置为false,则请求超时交给ribbon控制
  30. enabled: true
  31. isolation:
  32. strategy: SEMAPHORE
  33. thread:
  34. timeoutInMilliseconds: 60000
  35. #请求处理的超时时间
  36. ribbon:
  37. ReadTimeout: 4000
  38. #请求连接的超时时间
  39. ConnectTimeout: 3000
  1. 添加静态化资源 在resource创建 templates文件夹 并将资源文件夹的静态资源复制进去

15. Day15 秒杀前端 - 图7

  1. 将当前微服务添加到网关中
  1. #秒杀渲染微服务
  2. - id: changgou_seckill_web_route
  3. uri: lb://seckill-web
  4. predicates:
  5. - Path=/api/wseckillgoods/**
  6. filters:
  7. - StripPrefix=1

3.3. 时间菜单实现

时间菜单显示,先运算出每2小时一个抢购,就需要实现12个菜单,可以先计算出每个时间的临界值,然后根据当前时间判断需要显示12个时间段菜单中的哪个菜单,再在该时间菜单的基础之上往后挪4个菜单,一直显示5个时间菜单

3.3.1. 时间菜单获取

在changgou_web_seckill项目下创建 controller层包

再创建 控制层类 SecKillGoodsController

  1. @Controller
  2. @RequestMapping("/wseckillgoods")
  3. public class SecKillGoodsController {
  4. //跳转到秒杀首页
  5. @RequestMapping("/toIndex")
  6. public String toIndex() {
  7. return "seckill-index";
  8. }
  9. //获取秒杀时间集合信息
  10. @RequestMapping("/timeMenus")
  11. @ResponseBody
  12. public List<String> dataMenus() {
  13. //获取当前时间段修改集合
  14. List<Date> dateMenus = DateUtil.getDateMenus();
  15. List<String> result = new ArrayList<>();
  16. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  17. for (Date dateMenu : dateMenus) {
  18. result.add(sdf.format(dateMenu));
  19. }
  20. return result;
  21. }
  22. }

3.3.2. 页面加载时间菜单

修改seckill-index.html 第113行的代码

  1. <!--秒杀时间-->
  2. <div class="sectime">
  3. <div class="item-time active"
  4. v-for="(item,index) in dateMenus">
  5. <div class="time-clock">{{item}}</div>
  6. <div class="time-state-on">
  7. <span class="on-text" v-if="index==0">快抢中</span>
  8. <span class="on-over" v-if="index==0">距离结束:01:02:03</span>
  9. <span class="on-text" v-if="index>0">即将开始</span>
  10. <span class="on-over" v-if="index>0">距离开始:03:02:01</span>
  11. </div>
  12. </div>
  13. </div>

修改当前页面的vue代码 编写请求

  1. var app = new Vue({
  2. el: '#app',
  3. data() {
  4. return {
  5. goodslist: [],
  6. dateMenus: [],
  7. ctime: 0, //当前时间菜单选中的下标,
  8. alltimes: []
  9. }
  10. },
  11. methods: {
  12. loadMenus: () => {
  13. axios.get("/api/wseckillgoods/timeMenus").then((response) => {
  14. app.dateMenus = response.data
  15. })
  16. }
  17. },
  18. create: () => {
  19. this.loadMenus()
  20. }
  21. })