四张表:商品表、秒杀商品表、订单表、秒杀订单表
1.1)为什么要扩展出一个秒杀商品表来?
直接在商品表中添加一个字段,是不是秒杀商品不就行了?乍一听,好像是可以,但是你想过没有,我们今天搞一次秒杀,明天又搞一次大促销,后天搞一次9块9包邮,随着时间增加,我们的活动可能会越来越多,假如说每一次都要用一个字段来标识,时间长了以后,这个表会越来越难以维护,并且这样的商品表特别不稳定,经常修改,修改之后,代码都要改,这个极其麻烦。
所以我们单独拿出一张表,通过一个商品ID来关联到我们的商品表,也就是说,我们的商品表是相对来说比较稳定的,主要是包含商品的基本信息,名称啊,图片啊,价格啊......
这个秒杀价格,我们把它加到秒杀商品表,秒杀商品还包含什么?开始时间啊,结束时间啊,数量啊……,同理,还有秒杀订单表
1.2)订单表中为什么要冗余商品名称?
就是方便生成订单列表时,不需要再去关联商品表了,直接把商品名称拉过来显示。
1.3)注意:实际项目中的价格存储,都是以整数形式存储,不使用小数存储,数据库中全是“分”
1.4)注意:数据库中的id,实际项目中很少设置成自增的,为什么啊?因为那样很容易让别人给遍历了,从1循环到最大值,把库中信息全部遍历,那么id正常来说如何设计呢?那应该用什么?分布式ID生成算法SnowFlake,分布式id生成算法的有很多种,Twitter的SnowFlake就是其中经典的一种。这个以后再细说。
2.1)pojo层:创建对应的实体类:Goods、SecKillGoods、OrderInfo、SecKillOrder
2.2)dao层:创建GoodsDao,因为我们不仅想查出商品列表,还想直接把秒杀信息也查出来,但是商品表和秒杀商品表是在两个表里,所以再建立一个GoodsVo类,继承商品类,将秒杀商品的信息带过来,这样就把两个表合到一起了。
@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")
public List listGoodsVo();
}
2.3)service层:GoodsService
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
public List listGoodsVo() {
return goodsDao.listGoodsVo();
}
}
2.4)controller层:GoodsController
@GetMapping("/to_list")
public String list(Model model, SecKillUser user) {
model.addAttribute("user", user);
//查询商品列表
List goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
return "goods_list";
}
2.5)测试:
完成!
3.1)pojo层:对应的实体类已创建
3.2)dao层: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 where g.id = #{goodsId}")
public GoodsVo getGoodsVoByGoodsId(@Param("goodsId")long goodsId);
3.3)service层:GoodsService
public GoodsVo getGoodsVoByGoodsId(long goodsId) {
return goodsDao.getGoodsVoByGoodsId(goodsId);
}
3.4)controller层:GoodsController
@GetMapping("/to_detail/{goodsId}")
public String detail(Model model,SecKillUser user,
@PathVariable("goodsId")long goodsId) {
model.addAttribute("user", user);
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int seckillStatus = 0;
int remainSeconds = 0;
if(now < startAt ) {//秒杀还没开始,倒计时
seckillStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
}else if(now > endAt){//秒杀已经结束
seckillStatus = 2;
remainSeconds = -1;
}else {//秒杀进行中
seckillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus", seckillStatus);
model.addAttribute("remainSeconds", remainSeconds);
return "goods_detail";
}
3.5)测试:
完成!可查看到商品具体信息了。
3.6)注意
秒杀按钮什么时候可以点击?
秒杀倒计时、秒杀结束,都不可以点;只有秒杀进行中可以点击。如果秒杀未开始,页面上要显示一个倒计时。
这个倒计时肯定是客户端来做,不应该取请求服务端,为什么呀?可想而知,我们的客户端非常多,大家都去请求服务端的话,访问量非常大,但是在客户端做会不会不精确啊,那肯定会不精确,没法跟服务器的时间保持完全一致,秒杀有时也是看运气。
上面,我们把详情页已经做好,现在我们实现秒杀功能。
做秒杀,其实很简单,就是做一个表单的提交:
提交时就传递了一个参数,商品id:name="goodsId",提交的路径:action="/miaosha/do_miaosha"
4.1)dao层:
GoodsDao
@Update("update miaosha_goods set stock_count = stock_count -1 where goods_id = #{goodsId}")
public int reduceStock(SecKillGoods g);
OrderDao
@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
public SecKillOrder getSecKillOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId);
@Insert("insert into order_info(user_id, goods_id, goods_name, goods_count, goods_price, order_channel, status, create_date)values("
+ "#{userId}, #{goodsId}, #{goodsName}, #{goodsCount}, #{goodsPrice}, #{orderChannel},#{status},#{createDate} )")
@SelectKey(keyColumn="id", keyProperty="id", resultType=long.class, before=false, statement="select last_insert_id()")
public long insert(OrderInfo orderInfo);
@Insert("insert into miaosha_order (user_id, goods_id, order_id)"
+ "values(#{userId}, #{goodsId}, #{orderId})")
public void insertSecKillOrder(SecKillOrder seckillOrder);
4.2)service层:
GoodsService
public void reduceStock(GoodsVo goods) {
SecKillGoods g = new SecKillGoods();
g.setGoodsId(goods.getId());
goodsDao.reduceStock(g);
}
OrderService
public SecKillOrder getSecKillOrderByUserIdGoodsId(Long userId, Long goodsId) {
return orderDao.getSecKillOrderByUserIdGoodsId(userId, goodsId);
}
@Transactional
public OrderInfo createOrder(SecKillUser user, GoodsVo goods) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setUserId(user.getId());
orderInfo.setGoodsId(goods.getId());
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsName(goods.getGoodsName());
orderInfo.setGoodsCount(1);
orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
//1pc, 2android, 3ios
orderInfo.setOrderChannel(1);
//订单状态,0新建未支付,1已支付,2已发货,3已收货,4,已退款,5已完成
orderInfo.setStatus(0);
long orderId = orderDao.insert(orderInfo);
SecKillOrder secKillOrder = new SecKillOrder();
secKillOrder.setUserId(user.getId());
secKillOrder.setOrderId(orderId);
secKillOrder.setGoodsId(goods.getId());
orderDao.insertSecKillOrder(secKillOrder);
return orderInfo;
}
SecKillService
@Service
public class SecKillService {
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
@Transactional
public OrderInfo miaosha(SecKillUser user, GoodsVo goods) {
//减库存、下订单、写入秒杀订单
goodsService.reduceStock(goods);
//创建订单信息,写入秒杀订单
return orderService.createOrder(user, goods);
}
}
通常情况下我们是在Service中使用自己的Dao,如果要使用非自己的Dao,此时并不是直接引用,而是引用的对用Dao的Service,这点要注意。为什么这么做呢???主要是为了更加清晰,也为了方便管理,Service用到自己的Dao则直接引用,用到别人的Dao,则引用对应的Serivice。
4.3)controller层:
@PostMapping("/do_miaosha")
public String list(Model model, SecKillUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return "login";
}
//判断库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
model.addAttribute("errmsg", CodeMsg.SECKILL_OVER.getMsg());
return "seckill_fail";
}
//判断是否已经秒杀到了
SecKillOrder order = orderService.getSecKillOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
model.addAttribute("errmsg", CodeMsg.REPEATE_SECKILL.getMsg());
return "seckill_fail";
}
//减库存 下订单 写入秒杀订单
OrderInfo orderInfo = secKillService.miaosha(user, goods);
model.addAttribute("orderInfo", orderInfo);
model.addAttribute("goods", goods);
return "order_detail";
}
4.4)CodeMsg 中添加
public static CodeMsg SECKILL_OVER = new CodeMsg(500500, "商品已经秒杀完毕");
public static CodeMsg REPEATE_SECKILL = new CodeMsg(500501, "不能重复秒杀");
4.5)测试:先修改一下时间,使其可以秒杀
秒杀成功后直接跳转到订单详情页面:
我们来看一下数据库:
秒杀商品表对应商品数量-1:
订单表新增一条记录:
秒杀订单表也新增一条记录:
同一个商品,再次点击秒杀时:
到此为止,秒杀的基本功能已经完成!