做秒杀,先把功能做出来再进行优化,因此本节主要实现下单的一个基本流程。

1. 表设计

商品表:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称',
`goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题',
`goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品图片',
`goods_detail` LONGTEXT COMMENT '商品的详情介绍',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
`goods_stock` INT(11) DEFAULT '0' COMMENT '商品库存,-1表示没有限制',
PRIMARY KEY (`id`)
);

秒杀商品表:

1
2
3
4
5
6
7
8
9
CREATE TABLE `miaosha_goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
`goods_id` BIGINT(16) DEFAULT NULL COMMENT '商品id',
`miaosha_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '秒杀价',
`stock_count` INT(11) DEFAULT '0' COMMENT '库存数量',
`start_date` datetime DEFAULT NULL COMMENT '秒杀开始时间',
`end_date` datetime DEFAULT NULL COMMENT '秒杀结束时间',
PRIMARY KEY (`id`)
);

订单信息表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE `order_info`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'order ID',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户id',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品id',
`delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收货地址',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称',
`goods_count` INT(11) DEFAULT '0' COMMENT '商品数量',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
`order_channel` TINYINT(4) DEFAULT '0' COMMENT '1pc,2android,3ios',
`status` TINYINT(4) DEFAULT '0' COMMENT '0新建未支付,2已支付,3已发货4,已收货,5已完成',
`create_date` datetime DEFAULT NULL COMMENT '订单创建时间',
`pay_date` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (`id`)
);

秒杀订单表:

1
2
3
4
5
6
7
CREATE TABLE `miaosha_order`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀 order ID',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户id',
`order_id` BIGINT(20) DEFAULT NULL COMMENT '订单id',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品id',
PRIMARY KEY (`id`)
);

2. 商品列表页展示

1
2
3
4
5
6
@Mapper
public interface GoodsDao {

@Select("select g.*,mg.stock_count,mg.start_date,mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id = g.id")
List<GoodsVo> getGoodsVoList();
}

注意要创建一个vo对象来承载goods和miaosha_goods两个对象。

另外注意,之前的yml中对于mybatis的配置,忘记了配置驼峰写法:

1
2
configuration:
map-underscore-to-camel-case: true

jsp就不贴在这了。

3. 商品详情页面

接收前端传来的goods_id,因为要处理显示秒杀活动还剩多少秒,所以进行了相应的判断,以及秒杀的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RequestMapping("/to_detail/{goodsId}")
public String toList(@PathVariable("goodsId") long goodsId,Model model, MiaoshaUser user){
if(user == null)
return "login";
model.addAttribute("user",user);

GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods",goodsVo);
long startAt = goodsVo.getStartDate().getTime();
long endAt = goodsVo.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;//秒杀活动的状态,0-秒杀前;1-正在秒杀;2-秒杀结束
int remainSeconds = 0;//秒杀活动还剩多少秒
if(now < startAt){
miaoshaStatus = Constants.MiaoshaStatus.BEFORE_START;
remainSeconds = (int)(startAt-now)/1000;
}else if (now > endAt){
miaoshaStatus = Constants.MiaoshaStatus.AFTER_MIAOSHA;
remainSeconds = -1;
}else {
miaoshaStatus = Constants.MiaoshaStatus.ON_MIAOSHA;
remainSeconds = 0;
}

model.addAttribute("miaoshaStatus",miaoshaStatus);
model.addAttribute("remainSeconds",remainSeconds);
return "goods_detail";
}

对于前端,只需要拿到这个remainSeconds就可以了,可以对应显示秒杀还剩多久、秒杀是否结束等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var timeout;
if(remainSeconds > 0){//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
timeout = setTimeout(function(){
$("#countDown").text(remainSeconds - 1);
$("#remainSeconds").val(remainSeconds - 1);
countDown();
},1000);
}else if(remainSeconds == 0){//秒杀进行中
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
$("#miaoshaTip").html("秒杀进行中");
}else{//秒杀已经结束
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
}
}

4. 秒杀功能实现

判断库存
   |
根据userId和goodsId判断是否已经抢过了
   |
减库存,下订单,并且写入秒杀订单(同一事务中完成)

判断库存:

1
2
3
4
5
6
//判断库存
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
if(goodsVo.getStockCount() <= 0){
model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
return "miaosha_fail";
}

判断是否已经抢过了:

1
2
3
4
5
6
//判断是否已经秒杀到了
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder != null){
model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
return "miaosha_fail";
}

减库存,下订单,并且写入秒杀订单(同一事务中完成):

1
2
//减库存、下订单、写入秒杀订单,需要在一个事务中执行
OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);

在MiaoshaService中写:

1
2
3
4
5
6
7
8
9
10
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存、下订单、写入秒杀订单
boolean success =goodsService.reduceStock(goods);
if(success){
return orderService.createOrder(user,goods);
}else{
return null;
}
}

对于减库存:

这里只需要减miaosha_goods表里的库存即可。因为秒杀的数据是先从goods表里得到的,所以goods表里的库存此段已经减掉了。

1
2
@Update("update miaosha_goods set stock_count = stock_count-1 where goods_id=#{goodsId}")
int reduceStock(MiaoshaGoods g);
1
2
3
4
5
6
public boolean reduceStock(GoodsVo goods) {
MiaoshaGoods g = new MiaoshaGoods();
g.setGoodsId(goods.getId());
int ret = goodsDao.reduceStock(g);
return ret > 0;
}

对于创建订单:先是普通的order_info表插入,还有一个是miaosha_order表插入,那么就要在一个事务中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Transactional
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsCount(1);
orderInfo.setGoodsId(goods.getId());
orderInfo.setGoodsName(goods.getGoodsName());
orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
orderInfo.setOrderChannel(1);
orderInfo.setStatus(Constants.OrderStatus.NOT_PAID.getStatus());//新建未支付
orderInfo.setUserId(user.getId());

orderDao.insert(orderInfo);

MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderInfo.getId());
miaoshaOrder.setUserId(user.getId());

orderDao.insertMiaoshaOrder(miaoshaOrder);

return orderInfo;
}

对于,orderDao.insert(orderInfo)的具体实现:

1
2
3
4
5
@Insert("insert into order_info(user_id,goods_id,goods_name,goods_price,goods_count,order_channel,status,create_date) " +
"values(#{userId},#{goodsId},#{goodsName},#{goodsPrice},#{goodsCount},#{orderChannel},#{status},#{" +
"createDate})")
@SelectKey(keyColumn = "id",keyProperty = "id",resultType = long.class,before = false,statement = "select last_insert_id()")
long insert(OrderInfo orderInfo);

对于orderDao.insertMiaoshaOrder(miaoshaOrder)的具体实现:

1
2
@Insert("insert into miaosha_order(user_id,goods_id,order_id)values(#{userId},#{goodsId},#{orderId})")
int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);

下单成功的话,就跳到订单详情页面:

1
return "order_detail";

否则跳到错误提示页面:

1
return "miaosha_fail";