一、商品秒杀涉及的知识点
- 鉴权策略封装
- 掊口访问频次限制
- 小程序设计
- 页面防抖
- 接口调用
- 订单创建事务使用
- 超卖防御
二、订单库存系统方案(3种)
- 下单减库存 优点是库存和订单的强一致性,商品不会卖超,但是可能导致恶意下单,影响正常流程
- 支付减库存 优点是避免恶意下单,支付和库存强一致性,但是可能出现订单无法支付,商品库存不足等问题。
- 预扣库存 预扣库存是指用户请求之后预扣库存,生成订单,在时效内支付,否则订单失效,库存还原
- 代码:itshujia.com 或者wx搜:《IT书架》 回复:秒杀
三、小程序秒杀页面
- 商品秒杀详情页面页面
- 该商品秒杀时间未到,则上方进行提醒秒杀商品倒计时,并且立即抢购禁用(按钮置灰)
- 商品秒杀时间开始,则把立即秒杀按钮禁用状态改为可以点击
- 点击立即秒杀按钮要考虑页面防抖,不能重复在同一秒中重复发起N次网络请求
四、小程序部分代码展示
- 小程序wxml代码
<view>商品秒杀页面</view>
<l-countdown time-type="second" time="{{expire_time}}" bind:linend="changeBtn" />
<l-card type="primary" full="{{true}}" image="{{goods.goods.goods_image}}" title="{{goods.goods.goods_name}}">
<view>
价格:{{goods.goods.goods_price}}
</view>
<view>
<!-- <button disabled="true" bindtap="buyGoods" >抢购</button> -->
<l-button disabled="{{ btn_disable }}" bind:lintap="buyGoods" type="error" data-goods_id="{{ goods.goods.id }}">立即秒杀</l-button>
</view>
</l-card>
- 小程序js代码
// pages/goods_detail/goods_detail.js
import {
debounce
} from "../../utils/util"
Page({
/**
* 页面的初始数据
*/
data: {
goods:{},
expire_time:0,
btn_disable:false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (e) {
//商品id
let goods_id = e.goods_id;
this.getGoodsDetail(goods_id)
},
//获取商品详情
getGoodsDetail(goods_id){
var token = wx.getStorageSync('token')
wx.request({
url: 'http://www.zfw.com/api/v1/goods_detail?goods_id='+goods_id,
header: {
'Authorization': `Bearer ${token}`
},
success: res => {
//当前的时间戳
let now_time = Math.round(new Date().getTime() / 1000).toString();
let expire_time = res.data.data.start_time-now_time;
this.setData({
goods:res.data.data,
expire_time
})
if(expire_time > 0){
this.setData({
btn_disable:true
})
}
console.log(this.data.goods)
}
})
},
//立即抢购 debounce此方法是引入的util工具类的页面防抖函数
buyGoods:debounce(function (e) {
let goods_id = e[0].currentTarget.dataset.goods_id
var token = wx.getStorageSync('token')
wx.request({
url: 'http://www.zfw.com/api/v1/snap_up',
header: {
'Authorization': `Bearer ${token}`
},
method:"POST",
data:{
goods_id
},
success: res => {
let code = res.statusCode.toString()
if (!code.startsWith('2')){
wx.showToast({
title: '异常!',
icon:1
})
}
if(res.data.errorCode == 0){
wx.redirectTo({
url: '/pages/order/order',
})
}else{
wx.showToast({
title: res.data.msg,
})
}
console.log(res.data)
}
})
}),
changeBtn(){
this.setData({
btn_disable:false
})
}
})
- 小程序json代码,引入的自定义组件
{
"usingComponents": {
"l-card":"/miniprogram_npm/lin-ui/card",
"l-button":"/miniprogram_npm/lin-ui/button",
"l-countdown":"/miniprogram_npm/lin-ui/countdown"
}
}
五、后台业务逻辑
要考虑订单超卖,这次代码使用的redis队列实现的
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Goods;
use Illuminate\Http\Request;
use App\Models\ActivityGoods;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Lcobucci\JWT\Exception;
class GoodsController extends Controller
{
//商品秒杀列表
public function activityList()
{
$result = ActivityGoods::with(['goods'])
->get();
return response()->json(['errorCode' => 0, 'data' => $result, 'msg' => '查询成功']);
}
//商品秒杀列表
public function goodsDetail(Request $request)
{
$goods_id = $request->get('goods_id');
$result = ActivityGoods::with(['goods'])
->where('goods_id',$goods_id)
->first();
return response()->json(['errorCode' => 0, 'data' => $result, 'msg' => '查询成功']);
}
//同步库存
public function syncStock()
{
//查出所有参与秒杀活动列表
$result = ActivityGoods::with(['goods'])
->get()->toArray();
//进行把参与秒杀的商品写入到数据库
foreach ($result as $val){
//生成对应商品库存队列
$goods = "activity_goods_".$val['goods_id'];
for ($i=0; $i < $val['sku_nums']; $i++) {
Redis::lpush($goods, 1);
}
}
}
//校验库存
public function checkStock(Request $request)
{
//获取token
$token = explode(' ',$request->header('authorization'))[1];
//进行查看
$userInfo = Cache::get($token);
//抢购用户id
$userID = $userInfo->id;
//商品id
$goodsID = $request->input("goods_id");
//对应商品库存队列
$goods = "activity_goods_".$goodsID;
//对应商品抢购成功用户集合 {1,3,4}
$robSuccessUser = "success_user".$goodsID;
//进行判断当前用户是否在抢成功的队列里面
$result = Redis::sismember($robSuccessUser,$userID);
//如果你在这里面,就抢完了
if ($result) {
//如果抢购成功 返回状态码,进行下单
return response()->json(['errorCode' => 20000, 'data' => '', 'msg' => '已经抢购过了']);
}
//减库存,把队列里面的数据从左边 头
$count = Redis::lpop($goods);
if (!$count) {
//如果抢购成功 返回状态码,进行下单
return response()->json(['errorCode' => 20001, 'data' => '', 'msg' => '已经抢光了哦']);
}
//把当前这个秒杀的uid存储到中奖的队列里set
$success = Redis::sadd($robSuccessUser, $userID);
if(!$success){
//已经在成功队列里了,加回库存,防止的是同个用户并发请求
Redis::lpush($goods, 1);
//如果抢购成功 返回状态码,进行下单
return response()->json(['errorCode' => 20002, 'data' => '', 'msg' => '已经抢购过了']);
}
//如果抢购成功 返回状态码,进行下单
return response()->json(['errorCode' => 0, 'data' => '', 'msg' => '秒杀成功']);
}
//创建订单
public function createOrder(Request $request)
{
//获取token
$token = explode(' ',$request->header('authorization'))[1];
//进行查看
$userInfo = Cache::get($token);
//抢购用户id
$userID = $userInfo->id;
//商品id
$goodsID = $request->input("goods_id");
//对应商品抢购成功用户集合 {1,3,4}
$robSuccessUser = "success_user".$goodsID;
//进行判断当前用户是否在抢成功的队列里面
$result = Redis::sismember($robSuccessUser,$userID);
//如果你在这里面,就抢完了
if (!$result) {
//如果抢购成功 返回状态码,进行下单
return response()->json(['errorCode' => 20003, 'data' => '', 'msg' => '手慢了!']);
}
DB::beginTransaction();
try{
//减库存
//生成订单
DB::commit();
//下单成功,跳转支付页面
return response()->json(['errorCode' => 0, 'data' => '', 'msg' => '下单成功!']);
}catch (\Exception $e){
DB::rollBack();
}
}
}