1. Day15 秒杀前端
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有两种限制:库存限制、时间限制。
需求:
- 秒杀频道首页列出秒杀商品
- 点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
- 秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
- 当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
2. 秒杀商品存入缓存

秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。
2.1. 秒杀服务搭建
- 在changgou_service_api项目下 新建
changgou_service_seckill_api服务 添加公共依赖
<dependencies><dependency><groupId>com.changgou</groupId><artifactId>changgou_common</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
在changgou_service_seckill_api服务下 添加
com.changgou.seckill.feign和com.changgou.seckill.pojo两个包将资源文件夹下的两个pojo类放入com.changgou.seckill.pojo包中
在changgou_parent项目下 新建服务
changgou_service_seckill添加依赖
<dependencies><dependency><groupId>com.changgou</groupId><artifactId>changgou_common_db</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>com.changgou</groupId><artifactId>changgou_service_order_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.changgou</groupId><artifactId>changgou_service_seckill_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.changgou</groupId><artifactId>changgou_service_goods_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency><!--oauth依赖--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency></dependencies>
- 创建包
com.changgou.seckill然后创建启动类SecKillApplication
@SpringBootApplication@EnableEurekaClient@MapperScan(basePackages = {"com.changgou.seckill.dao"})public class SecKillApplication {public static void main(String[] args) {SpringApplication.run(SecKillApplication.class, args);}//idwork@Beanpublic IdWorker idWorker() {return new IdWorker(1, 1);}//设置redisTemplate序列化@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 1.创建 redisTemplate 模版RedisTemplate<Object, Object> template = new RedisTemplate<>();// 2.关联 redisConnectionFactorytemplate.setConnectionFactory(redisConnectionFactory);// 3.创建 序列化类GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);// 6.序列化类,对象映射设置// 7.设置 value 的转化格式和 key 的转化格式template.setValueSerializer(genericToStringSerializer);template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;}}
- 设置application
server:port: 9016spring:jackson:time-zone: GMT+8application:name: seckilldatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://192.168.130.128:3306/changgou_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8username: rootpassword: rootmain:allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册redis:host: 192.168.130.128rabbitmq:host: 192.168.130.128eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: trueclient:config:default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒#hystrix 配置hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHOREthread:# 熔断器超时时间,默认:1000/毫秒timeoutInMilliseconds: 20000
将author的公钥复制一份到resource目录下
创建
com.changgou.config包 然后新建ResourceServerConfig配置类
@Configuration@EnableResourceServer//开启方法上的PreAuthorize注解@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//公钥private static final String PUBLIC_KEY = "public.key";/**** 定义JwtTokenStore* @param jwtAccessTokenConverter* @return*/@Beanpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {return new JwtTokenStore(jwtAccessTokenConverter);}/**** 定义JJwtAccessTokenConverter* @return*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setVerifierKey(getPubKey());return converter;}/*** 获取非对称加密公钥 Key** @return 公钥 Key*/private String getPubKey() {Resource resource = new ClassPathResource(PUBLIC_KEY);try {InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());BufferedReader br = new BufferedReader(inputStreamReader);return br.lines().collect(Collectors.joining("\n"));} catch (IOException ioe) {return null;}}/**** Http安全配置,对每个到达系统的http请求链接进行校验* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {//所有请求必须认证通过http.authorizeRequests().anyRequest().authenticated(); //其他地址需要认证授权}}
- 更改网关路径过滤类 添加秒杀工程过滤信息
进到到changgou_gateway_web项目下的 com.changgou.web.gateway.filter.URLFilter 过滤类中
添加 路径为/api/seckill 之前我们添加过 所有无需添加

- 更改网关的application配置文件 添加秒杀服务的路由转发
#秒杀微服务- id: changgou_seckill_routeuri: lb://seckillpredicates:- Path=/api/seckill/**filters:- StripPrefix=1

2.2. 时间操作


根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。
缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。
- 将资源文件夹下的DateUtil工具类 放到changgou_common的util包下
public class DateUtil {/**** 从yyyy-MM-dd HH:mm格式转成yyyyMMddHH格式* @param dateStr* @return*/public static String formatStr(String dateStr){SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");try {Date date = simpleDateFormat.parse(dateStr);simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");return simpleDateFormat.format(date);} catch (ParseException e) {e.printStackTrace();}return null;}/**** 获取指定日期的凌晨* @return*/public static Date toDayStartHour(Date date){Calendar calendar = Calendar.getInstance();calendar.setTime(date);calendar.set(Calendar.HOUR_OF_DAY, 0);calendar.set(Calendar.MINUTE, 0);calendar.set(Calendar.SECOND, 0);calendar.set(Calendar.MILLISECOND, 0);Date start = calendar.getTime();return start;}/**** 时间增加N分钟* @param date* @param minutes* @return*/public static Date addDateMinutes(Date date,int minutes){Calendar calendar = Calendar.getInstance();calendar.setTime(date);calendar.add(Calendar.MINUTE, minutes);// 24小时制date = calendar.getTime();return date;}/**** 时间递增N小时* @param hour* @return*/public static Date addDateHour(Date date,int hour){Calendar calendar = Calendar.getInstance();calendar.setTime(date);calendar.add(Calendar.HOUR, hour);// 24小时制date = calendar.getTime();return date;}/**** 获取时间菜单* @return*/public static List<Date> getDateMenus(){//定义一个List<Date>集合,存储所有时间段List<Date> dates = new ArrayList<Date>();//循环12次Date date = toDayStartHour(new Date()); //凌晨for (int i = 0; i <12 ; i++) {//每次递增2小时,将每次递增的时间存入到List<Date>集合中dates.add(addDateHour(date,i*2));}//判断当前时间属于哪个时间范围Date now = new Date();for (Date cdate : dates) {//开始时间<=当前时间<开始时间+2小时if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){now = cdate;break;}}//当前需要显示的时间菜单List<Date> dateMenus = new ArrayList<Date>();for (int i = 0; i <5 ; i++) {dateMenus.add(addDateHour(now,i*2));}return dateMenus;}/**** 时间转成yyyyMMddHH* @param date* @return*/public static String date2Str(Date date){SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");return simpleDateFormat.format(date);}}
2.2.1. 当前业务整体流程分析
1.查询所有符合条件的秒杀商品1) 获取时间段集合并循环遍历出每一个时间段2) 获取每一个时间段名称,用于后续redis中key的设置3) 状态必须为审核通过 status=14) 商品库存个数>05) 秒杀商品开始时间>=当前时间段6) 秒杀商品结束<当前时间段+2小时7) 排除之前已经加载到Redis缓存中的商品数据8) 执行查询获取对应的结果集2.将秒杀商品存入缓存
2.3. 代码实现
2.3.1. 更改启动类,添加开启定时任务注解
@EnableScheduling
2.3.2. 定义定时任务类
秒杀工程新建task包,并新建任务类SeckillGoodsPushTask
业务逻辑:
1)获取秒杀时间段菜单信息
2)遍历每一个时间段,添加该时间段下秒杀商品
2.1)将当前时间段转换为String,作为redis中的key
2.2)查询商品信息(状态为1,库存大于0,秒杀商品开始时间大于当前时间段,秒杀商品结束时间小于当前时间段,当前商品的id不在redis中)
3)添加redis
@Componentpublic class SeckillGoodsPushTask {@Autowiredprivate SeckillGoodsMapper seckillGoodsMapper;// redis key的前缀public static final String SECKILL_GOODS_KEY = "seckill_goods";@Autowiredprivate RedisTemplate redisTemplate;@Scheduled(cron = "0/30 * * * * ?")public void loadSecKillGoodsToRedis() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//获取当前时间到 每2小时的5个时间集合List<Date> dateMenus = DateUtil.getDateMenus();for (Date dateMenu : dateMenus) {//获取每个时间段名称 用于redis中作为keyString redisExtName = DateUtil.date2Str(dateMenu);Example example = new Example(SeckillGoods.class);Example.Criteria criteria = example.createCriteria();// 状态要为1 表示已审核criteria.andEqualTo("status", "1");// 商品库存个数 要大于0criteria.andGreaterThan("stockCount", 0);// 秒杀商品开始时间 >= 当前时间段criteria.andGreaterThanOrEqualTo("startTime", sdf.format(dateMenu));// 并且秒杀商品结束时间 < 当前时间段+2criteria.andLessThan("endTime", sdf.format(DateUtil.addDateHour(dateMenu, 2)));// 获取当前时间段在redis的所有商品的key值 key为redis前缀+时间段名Set keys = redisTemplate.boundHashOps(SECKILL_GOODS_KEY + redisExtName).keys();if (keys != null && keys.size() > 0) {// 排除之前已经加载到Redis缓存中的商品 即在redis已经存在criteria.andNotIn("id", keys);}// 符合以上条件的结果集合List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example);// 添加到redis缓存中for (SeckillGoods seckillGoods : seckillGoodsList) {redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName, seckillGoods.getGoodsId(), seckillGoods);}}}}
3. 秒杀页面

秒杀商品首页会显示处于秒杀中以及未开始秒杀的商品。
3.1. 需求分析
秒杀首页需要显示不同时间段的秒杀商品信息,然后当用户选择不同的时间段,查询该时间段下的秒杀商品,实现过程分为两大过程:
1) 加载时间菜单2)加载时间菜单下秒杀商品信息
3.1.1. 加载时间菜单分析
每2个小时就会切换一次抢购活动,所以商品发布的时候,我们将时间定格在2小时内抢购,每次发布商品的时候,商品抢购开始时间和结束时间是这2小时的边界。
每2小时会有一批商品参与抢购,所以我们可以将24小时切分为12个菜单,每个菜单都是个2小时的时间段,当前选中的时间菜单需要根据当前时间判断,判断当前时间属于哪个秒杀时间段,然后将该时间段作为选中的第1个时间菜单。
3.1.2. 加载对应秒杀商品分析
进入首页时,到后台查询时间菜单信息,然后将第1个菜单的时间段作为key,在Redis中查询秒杀商品集合,并显示到页面,页面每次点击切换不同时间段菜单的时候,都将时间段传入到后台,后台根据时间段获取对应的秒杀商品集合。
3.2. 秒杀渲染服务 - 渲染秒杀首页
- 创建工程
changgou_web_seckill,用于秒杀页面渲染 添加依赖
<dependencies><dependency><groupId>com.changgou</groupId><artifactId>changgou_service_seckill_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies>
- 创建
com.changgou.seckill.web包 并新建启动类SecKillWebApplication
@SpringBootApplication@EnableEurekaClient@EnableFeignClients(basePackages = {"com.changgou.seckill.feign"})public class SecKillWebApplication {public static void main(String[] args) {SpringApplication.run(SecKillWebApplication.class, args);}/*** Feign拦截器*/@Beanpublic FeignInterceptor feignInterceptor() {return new FeignInterceptor();}/*** 设置 redisTemplate 的序列化设置* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 1.创建 redisTemplate 模版RedisTemplate<Object, Object> template = new RedisTemplate<>();// 2.关联 redisConnectionFactorytemplate.setConnectionFactory(redisConnectionFactory);// 3.创建 序列化类GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);// 6.序列化类,对象映射设置// 7.设置 value 的转化格式和 key 的转化格式template.setValueSerializer(genericToStringSerializer);template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;}}
- application
server:port: 9104eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: truespring:jackson:time-zone: GMT+8thymeleaf:cache: falseapplication:name: seckill-webmain:allow-bean-definition-overriding: trueredis:host: 192.168.130.128#hystrix 配置hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHOREthread:timeoutInMilliseconds: 60000#请求处理的超时时间ribbon:ReadTimeout: 4000#请求连接的超时时间ConnectTimeout: 3000
- 添加静态化资源 在resource创建
templates文件夹 并将资源文件夹的静态资源复制进去

- 将当前微服务添加到网关中
#秒杀渲染微服务- id: changgou_seckill_web_routeuri: lb://seckill-webpredicates:- Path=/api/wseckillgoods/**filters:- StripPrefix=1
3.3. 时间菜单实现
时间菜单显示,先运算出每2小时一个抢购,就需要实现12个菜单,可以先计算出每个时间的临界值,然后根据当前时间判断需要显示12个时间段菜单中的哪个菜单,再在该时间菜单的基础之上往后挪4个菜单,一直显示5个时间菜单。
3.3.1. 时间菜单获取
在changgou_web_seckill项目下创建 controller层包
再创建 控制层类 SecKillGoodsController
@Controller@RequestMapping("/wseckillgoods")public class SecKillGoodsController {//跳转到秒杀首页@RequestMapping("/toIndex")public String toIndex() {return "seckill-index";}//获取秒杀时间集合信息@RequestMapping("/timeMenus")@ResponseBodypublic List<String> dataMenus() {//获取当前时间段修改集合List<Date> dateMenus = DateUtil.getDateMenus();List<String> result = new ArrayList<>();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");for (Date dateMenu : dateMenus) {result.add(sdf.format(dateMenu));}return result;}}
3.3.2. 页面加载时间菜单
修改seckill-index.html 第113行的代码
<!--秒杀时间--><div class="sectime"><div class="item-time active"v-for="(item,index) in dateMenus"><div class="time-clock">{{item}}</div><div class="time-state-on"><span class="on-text" v-if="index==0">快抢中</span><span class="on-over" v-if="index==0">距离结束:01:02:03</span><span class="on-text" v-if="index>0">即将开始</span><span class="on-over" v-if="index>0">距离开始:03:02:01</span></div></div></div>
修改当前页面的vue代码 编写请求
var app = new Vue({el: '#app',data() {return {goodslist: [],dateMenus: [],ctime: 0, //当前时间菜单选中的下标,alltimes: []}},methods: {loadMenus: () => {axios.get("/api/wseckillgoods/timeMenus").then((response) => {app.dateMenus = response.data})}},create: () => {this.loadMenus()}})
