尚硅谷秒杀代码解析
//detail.html 代码快1
<li>
<a href="javascript:" v-if="isBuy" @click="generateSeckillCode()" class="sui-btn btn-danger addshopcar">立即抢购</a>
<a href="javascript:" v-if="!isBuy" class="sui-btn btn-danger addshopcar" disabled="disabled">立即抢购</a>
</li>
看代码会发现点击“立即抢购”的时候先调用generateSeckillCode()这个方法,然后我们看generateSeckillCode()方法
//detail.html 代码快2
generateSeckillCode() {
//debugger
seckill.generateSeckillCode(this.skuId).then(response => {
var seckillCode = response.data.data
window.location.href = '/seckill-queue.html?skuId='+this.skuId+'&seckillCode='+seckillCode
})
},
从代码中看出要进入这个方法,需要拿取抢购码,然后重定向到的页面(这里可以看出前端有控制必须有抢购码才能进入秒杀方法)
//seckill.js 代码快3
// 获取秒杀参数
generateSeckillCode(skuId) {
return request({
url: this.api_name + '/generateSeckillCode/' + skuId,
method: 'get'
})
},
从js文件中我们可以看到获取抢购码的接口 请求方式是get,所以我们想写一个生成抢购码的接口,参数是skuId
/**
* 3.生成抢购码,防止用户直接跳转到商品详情页面进行秒杀
* http://api.gmall.com/seckill/generateSeckillCode/34
* @param skuId
* @param request
* @return
*/
@GetMapping("generateSeckillCode/{skuId}")
public RetVal generateSeckillCode(@PathVariable Long skuId, HttpServletRequest request){
//1.判断用户是否登录
String userId = AuthContextHolder.getUserId(request);
if(StringUtils.isNotEmpty(userId)) {
//2.从缓存中拿到秒杀商品的信息
SeckillProduct seckillProduct = seckillProductService.getSeckillProductBySkuId(skuId);
//Date currentTime = new Date();
Date currentTime = new Date();
//TODO 二、判断秒杀商品是否在秒杀时间之内
if (DateUtil.dateCompare(seckillProduct.getStartTime(), currentTime) &&
DateUtil.dateCompare(currentTime, seckillProduct.getEndTime())) {
//3.生成抢购码
//TODO 三、生成抢购码
String seckillCode = MD5.encrypt(userId);
return RetVal.ok(seckillCode);
}
}
return RetVal.fail().message("获取抢购码失败,请先登录");
}
代码解析:
1.请求方式与前端一致,get请求,参数是skuId,所以请求头是@GetMapping(“generateSeckillCode/{skuId}”)
2.方法名要和前端的请求路径一致所以使用写成generateSeckillCode,参数的skuid是必要的,因为生成抢购码需要userId,需要从请求中拿到,所以要用request,从request中获得skuId。
3.需要usreId才能生成抢购码,所以必须在用户登录的情况下才能获取到skuId,所以需要先判断用户是否登录
4.根据用户id生成抢购码,并进行md5加密,不加密的话不安全
5.返回抢购码给前端,然后就进入generateSeckillCode方法了。
假设已经登录,skuid=34,抢购码为c84258e9c39059a89ab77d846ddab909
进入generateSeckillCode方法之后会发现拿到,会跳转
window.location.href = ‘/seckill-queue.html?skuId=’+this.skuId+‘&seckillCode=’+seckillCode这个地址,然后调用下边这个方法,把skuid和抢购码给后端传过去,跳转到排队页面
//3.获取抢购码成功之后要去的页面
@GetMapping("/seckill-queue.html")
public String seckillQueue(Long skuId, String seckillCode, Model model) {
model.addAttribute("skuId", skuId);
model.addAttribute("seckillCode", seckillCode);
return "seckill/queue";
}
成功跳转到排队页面,继续看下面的前端代码
//2.html加载完成后执行
mounted() {
const timer = setInterval(() => {
if (this.code != 211) {
clearInterval(timer);
}
this.hasQualified()
}, 3000);
// 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
this.$once('hook:beforeDestroy', () => {
clearInterval(timer);
})
},
//1.html加载完成之前执行
created() {
this.prepareSeckill();
},
从前端代码可以看出页面加载之前会调用prepareSeckill()方法,加载之后调用hasQualified()方法
methods: {
prepareSeckill() {
seckill.prepareSeckill(this.skuId, this.seckillCode).then(response => {
console.log(JSON.stringify(response))
if (response.data.code == 200) {
this.isCheckOrder = true
} else {
this.show = 2
this.message = response.data.message
}
})
},
hasQualified() {
if (!this.isCheckOrder) return
seckill.hasQualified(this.skuId).then(response => {
//debugger
this.data = response.data.data
this.code = response.data.code
console.log(JSON.stringify(this.data))
//排队中
if (response.data.code == 211) {
this.show = 1
} else {
//秒杀成功
if (response.data.code == 215) {
this.show = 3
this.message = response.data.message
} else {
if (response.data.code == 218) {
this.show = 4
this.message = response.data.message
} else {
this.show = 2
this.message = response.data.message
}
}
}
})
}
}
然后我们看js文件中对接口的定义要求
//预下单
prepareSeckill(skuId, seckillCode) {
return request({
url: this.api_name + '/prepareSeckill/' + skuId + '?seckillCode=' + seckillCode,
method: 'post'
})
},
// 查询订单
hasQualified(skuId) {
return request({
url: this.api_name + '/hasQualified/' + skuId,
method: 'get'
})
},
然后根据这个我们在写出后端的接口
controller层
客户点击立即抢购,进入到排队页面,先进行prepareSeckill()预下单处理,这里需要对用户进行确认,看他的抢购码是否匹配,看秒杀商品的状态位是否正常,如果都正常的话,我们就可以进行抢购,然后把用户的id,抢购商品的id消息,发送给rabbitmq进行排队处理,
/**
* 4.秒杀预下单
* @param skuId
* @param seckillCode 抢购码
* @param request
* @return
*/
@PostMapping("prepareSeckill/{skuId}")
public RetVal prepareSeckill(@PathVariable Long skuId ,String seckillCode,HttpServletRequest request){
//1.抢购码是否匹配
String userId = AuthContextHolder.getUserId(request);
//TODO 四、判断用户抢购码是否匹配
if(!MD5.encrypt(userId).equals(seckillCode)){
//当用户预下单的时候用户不匹配,不能进行秒杀
return RetVal.build(null, RetValCodeEnum.SECKILL_ILLEGAL);
}
//2.该商品是否可以秒杀 状态为是否正常
//TODO 五、判断秒杀商品状态位是否为1
String state = (String) redisTemplate.opsForValue().get(RedisConst.SECKILL_STATE_PREFIX + skuId);
if(StringUtils.isEmpty(state)){
return RetVal.build(null, RetValCodeEnum.SECKILL_ILLEGAL);
}
//可以进行抢购
if (RedisConst.CAN_SECKILL.equals(state)){
//3.如果可以秒杀,吧秒杀用户id和商品skuid发送给rabbitmq 等会处理
UserSeckillSkuInfo userSeckillSkuInfo = new UserSeckillSkuInfo();
userSeckillSkuInfo.setUserId(userId);
userSeckillSkuInfo.setSkuId(skuId);
//TODO 六、把消息放到rabbitmq中进行排队处理 这里下一步之后记得看rabbitmq页面是否有消息在队列中
rabbitTemplate.convertAndSend(MqConst.PREPARE_SECKILL_EXCHANGE,MqConst.PREPARE_SECKILL_ROUTE_KEY,userSeckillSkuInfo);
}else{
return RetVal.build(null,RetValCodeEnum.SECKILL_FINISH);
}
return RetVal.ok();
}
消费者消费rabbitmq队列中的消息,当消费者拿到rabbitmq中的消息,调用秒杀商品服务,对下单信息进行处理
/**
* 预下单监听器
* @param userSeckillSkuInfo 用户预下单详情
* @param message rabbitmq生产者发布的消息 seckillController.prepareSeckill()方法中发布的消息
* @param channel 发布多条消息的信道
* @throws IOException
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MqConst.PREPARE_SECKILL_QUEUE,durable = "false"),
exchange = @Exchange(value = MqConst.PREPARE_SECKILL_EXCHANGE,durable = "false"),
key = {MqConst.PREPARE_SECKILL_ROUTE_KEY}))
public void prepareSeckill(UserSeckillSkuInfo userSeckillSkuInfo, Message message, Channel channel) throws IOException {
//TODO 七、这里对rabbitmq的消息进行消费
if (userSeckillSkuInfo!=null){
//开始处理预下单
seckillProductService.prepareSeckill(userSeckillSkuInfo);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
调用秒杀商品服务中的预下单处理方法,
先是判断商品状态位是否正常,
然后判断用户是否已经下过单,
还有就是校验库存是否正常,
如果要是都符合的话,就把客户预下单的信息存到redis中,(userId,商品数量,商品信息,订单码),然后把商品数量更新到redis和mysql中
/**
* 预下单消息处理
* @param userSeckillSkuInfo
*/
@Override
public void prepareSeckill(UserSeckillSkuInfo userSeckillSkuInfo) {
//TODO 八、rabbotmq对消息处理操作
String userId = userSeckillSkuInfo.getUserId();
Long skuId = userSeckillSkuInfo.getSkuId();
String state = (String)redisTemplate.opsForValue().get(RedisConst.SECKILL_STATE_PREFIX+skuId);
//判断商品是否已经卖完
if(RedisConst.CAN_NOT_SECKILL.equals(state)){
return;
}
//a.把用户抢购的商品放入缓存中,用于判断用户是否抢购过
Boolean flag = redisTemplate.opsForValue().setIfAbsent(RedisConst.PREPARE_SECKILL_USERID_SKUID + userId, skuId, RedisConst.PREPARE_SECKILL_LOCK_TIME, TimeUnit.SECONDS);
//b.从库存中减库存,也就是从list集合中弹出一个
//true下单成功,false至少是第二次下单,已经预下单过
//TODO 这走
if(!flag){
return;
}
String stockSkuId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();
//c.如果该库存没有了,通知其他结点商品不能秒杀
if(StringUtils.isEmpty(stockSkuId)){
redisTemplate.convertAndSend(RedisConst.PREPARE_PUB_SUB_SECKILL,skuId+":"+RedisConst.CAN_NOT_SECKILL);
return;
}
//d.把用户抢购的预下单商品信息存储到redis中
PrepareSeckillOrder prepareSeckillOrder = new PrepareSeckillOrder();
prepareSeckillOrder.setUserId(userId);
prepareSeckillOrder.setBuyNum(1);
SeckillProduct seckillProduct = getSeckillProductBySkuId(skuId);
prepareSeckillOrder.setSeckillProduct(seckillProduct);
//设置订单码
prepareSeckillOrder.setPrepareOrderCode(MD5.encrypt(userId+skuId));
redisTemplate.boundHashOps(RedisConst.PREPARE_SECKILL_USERID_ORDER).put(userId,prepareSeckillOrder);
//e.更新秒杀商品的库存信息到mysql+redis中
updateSeckillStock(skuId);
}
对redis和mysql秒杀商品的数量进行更新(如果商品数量为2的倍数时,对mysql的库存进行修改)
private void updateSeckillStock(Long skuId) {
//拿取商品剩余库存量库存
Long stockCount = redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).size();
//更新数据库设置更新频率
if(stockCount%2==0){
SeckillProduct seckillProduct = getSeckillProductBySkuId(skuId);
seckillProduct.setStockCount(stockCount.intValue());
baseMapper.updateById(seckillProduct);
//更新redis缓存,为了给用户看有没有机会秒到
redisTemplate.boundHashOps(RedisConst.SECKILL_PRODUCT).put(skuId,seckillProduct);
}
}
前端页面在执行完prepareSeckill()方法之后就继续执行了hasQualified()方法,去查看用户是否有抢购资格,
// 查询订单
hasQualified(skuId) {
return request({
url: this.api_name + '/hasQualified/' + skuId,
method: 'get'
})
},
/**
* 5.判断用户是否有抢购资格
* @param skuId 商品信息
* @param request 用来获取客户信息
* @return
*/
@GetMapping("hasQualified/{skuId}")
public RetVal hasQualified(@PathVariable Long skuId ,HttpServletRequest request){
//TODO 这里走完之后页面会显示 抢购成功
String userId = AuthContextHolder.getUserId(request);
return seckillProductService.hasQualified(skuId,userId);
}
service层实现类代码
根据用户id来查询redis缓存中是否有预下单信息,如果有的话就返回抢单成功的状态码
SECKILL_RUN(211, "正在排队中"),
SECKILL_FINISH(213, "已售罄"),
SECKILL_END(214, "秒杀已结束"),
PREPARE_SECKILL_SUCCESS(215, "抢单成功"),
/**
* 判断用户是否有抢购资格
* @param skuId
* @param userId
* @return
*/
@Override
public RetVal hasQualified(Long skuId, String userId) {
//判断用户是否有抢购资格
// 1.查询redis中是否
// 有用户id和skuid
Boolean isExist = redisTemplate.hasKey(RedisConst.PREPARE_SECKILL_USERID_SKUID +userId);
if(isExist){
/**
* 如果预下单有用户的预下单订单信息,说明用户预抢单成功
* prepareSeckillOrder为空,name就说明用户已经下单成功,此时走下边第二步
* prepareSeckillOrder不为空,那么用户就有抢购资格
*/
PrepareSeckillOrder prepareSeckillOrder=(PrepareSeckillOrder)redisTemplate.boundHashOps(RedisConst.PREPARE_SECKILL_USERID_ORDER).get(userId);
if(prepareSeckillOrder!=null){
return RetVal.build(prepareSeckillOrder, RetValCodeEnum.PREPARE_SECKILL_SUCCESS);
}
}
//第二步:如果用户已经下单(同一个用户购买同一件商品,才能到这里)
Integer orderId = (Integer)redisTemplate.boundHashOps(RedisConst.BOUGHT_SECKILL_USER_ORDER).get(userId);
if(orderId!=null){
return RetVal.build(null, RetValCodeEnum.SECKILL_ORDER_SUCCESS);
}
// 2.查询redis中是否有预下单信息
return RetVal.build(null,RetValCodeEnum.SECKILL_RUN);
// 3.当用户预下单成功就具有抢购资格
}
在前端页面我们可以看到,当用户抢购成功的时候,会有“去下单”可以点击
<div class="seckill_dev" v-if="show == 3">
恭喜取得抢购资格
<a href="/seckill-confirm.html" target="_blank">去下单</a>
</div>
从代码我们可以看到如果我们获得抢购资格之后,点击“去下单”,然后跳转到"/seckill-confirm.html"页面
//秒杀确认订单信息
@GetMapping("seckillConfirm")
public RetVal seckillConfirm(HttpServletRequest request) {
//TODO 这是支付页面详情
String userId = AuthContextHolder.getUserId(request);
//a.秒杀到的预售商品订单信息
PrepareSeckillOrder prepareSeckillOrder = (PrepareSeckillOrder)redisTemplate.boundHashOps(RedisConst.PREPARE_SECKILL_USERID_ORDER).get(userId);
if (prepareSeckillOrder==null){
return RetVal.fail().message("请求非法");
}
//b.获取用户的收货地址
List<UserAddress> userAddressList = userFeignClient.getAddressByUserId(userId);
//c.用户秒到的商品
SeckillProduct seckillProduct = prepareSeckillOrder.getSeckillProduct();
//d.秒杀商品的订单明细
OrderDetail orderDetail = new OrderDetail();
orderDetail.setSkuId(seckillProduct.getSkuId());
orderDetail.setSkuName(seckillProduct.getSkuName());
orderDetail.setImgUrl(seckillProduct.getSkuDefaultImg());
orderDetail.setSkuNum(prepareSeckillOrder.getBuyNum()+"");
orderDetail.setOrderPrice(seckillProduct.getCostPrice());
List<OrderDetail> orderDetailList = new ArrayList<>();
orderDetailList.add(orderDetail);
//e.返回数据给页面
Map<String, Object> retMap = new HashMap<>();
retMap.put("userAddressList",userAddressList);
retMap.put("orderDetailList",orderDetailList);
retMap.put("totalMoney",seckillProduct.getCostPrice());
return RetVal.ok(retMap);
}
//4.订单信息确认页面
@GetMapping("/seckill-confirm.html")
public String seckillConfirm(Model model) {
RetVal retVal = seckillFeignClient.seckillConfirm();
if(retVal.isOk()){
Map<String,Object> data = (Map<String,Object>)retVal.getData();
model.addAllAttributes(data);
return "seckill/confirm";
}else{
model.addAttribute("message",retVal.getMessage());
return "seckill/confirm";
}
未完待续…