文章目录
前言
由于慕课课程中是先实现最基本的功能然后对其压测,压测那个地方出问题,然后在对其优化。所以本文记录的也是实现的是简单的秒杀功能没有涉及到高并发的优化。
数据库设计
1 商品表 包含所有商品的所有信息
2 订单表 包含所有订单的所有信息
3 秒杀商品表 包含秒杀商品的相关信息(id,商品id,商品库存,秒杀价格,开始日期,结束日期)
4 秒杀订单表 包含秒杀订单的相关信息(id,订单id,用户id,商品id)
为什么要这样设计?可不可以保留1和2 ,3和4在1和2中添加一个字段表示?
实际上这样可行,但是不推荐,因为秒杀有很多种类型。今天秒杀,明天促销,后天八折 岂不是每次搞一个活动都要去重新设计表和字段。而且还要修改后端相关代码。这样不利于维护和扩展。 其次我们单独新建一个表,是因为秒杀是多个用户同一时间下订单所以并发量非常大我们需要单独一个表在redis支撑后期。
详细字段可以github上找一下。
秒杀商品列表页
首页秒杀商品列表页其实就是将所有的秒杀商品信息查询出来并返回。
首先在GoodsDao下 查询满足条件的内容
bash
@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<GoodsVo> listGoodsVo();
这里需要解释一下可能的疑问点
1.左连接是什么意思?
左连接是保存左表的全部内容,然后按定义的连接条件与右表连接,若右表没有连接规定的数据则对应的字段为null。
2.mg.goods_id = g.id 是连接条件还是查询条件?
是连接条件,也就是按什么条件将两表连接在一起。总不能乱拼,肯定按一定的条件拼接。若要再写查询条件后边只需跟where,这里where作用的是连接后的表。
3.为什么选择左连接?
其实左连接和右连接没有区别,最重要的一点是如果你使用左连接则会保存左表的全部数据,所以左 表一般是右表的子集(或者比右表小),如果左表是更大的,右表是小的,则连接后会发现最后的数据表很多字段为null。在这里秒杀商品是商品的子集
查询出来后我们要新建一个VO对象接受,因为查询出来的字段不仅包含商品字段还会包含秒杀商品的相关字段
所以我们需要在VO目录下新建一个对象
然后在GoodsService下定义
bash
@Autowired
GoodsDao goodsDao;
public List<GoodsVo> listGoodsVo(){
return goodsDao.listGoodsVo();
}
最后在GoodsController类定义以下方法
bash
@RequestMapping("/to_list")
public String list(Model model,MiaoshaUser user) {
// 向goods_list页面添加user对象,至于user是怎么拦截的可以看登录功能
model.addAttribute("user", user);
//查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
return "goods_list";
}
实际上我们写代码是从controller层开始写,这里只是方便展示从dao层开始写。
bash
package com.imooc.miaosha.vo;
import java.util.Date;
import com.imooc.miaosha.domain.Goods;
public class GoodsVo extends Goods{
private Double miaoshaPrice;
private Integer stockCount;
private Date startDate;
private Date endDate;
public Integer getStockCount() {
return stockCount;
}
public void setStockCount(Integer stockCount) {
this.stockCount = stockCount;
}
public Date getStartDate() {
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
public Date getEndDate() {
return endDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
public Double getMiaoshaPrice() {
return miaoshaPrice;
}
public void setMiaoshaPrice(Double miaoshaPrice) {
this.miaoshaPrice = miaoshaPrice;
}
}
疑问点?我们new一个GoodsVo对象可以访问父类的private字段吗?
不能,只能通过get方法访问,即使子类继承了父类。
随后实现good_list前端页面
bash
<div class="panel panel-default">
<div class="panel-heading">秒杀商品列表</div>
<table class="table" id="goodslist">
<tr><td>商品名称</td><td>商品图片</td><td>商品原价</td><td>秒杀价</td><td>库存数量</td><td>详情</td></tr>
<tr th:each="goods,goodsStat : ${goodsList}">
<td th:text="${goods.goodsName}"></td>
<td ><img th:src="@{${goods.goodsImg}}" width="100" height="100" /></td>
<td th:text="${goods.goodsPrice}"></td>
<td th:text="${goods.miaoshaPrice}"></td>
<td th:text="${goods.stockCount}"></td>
<td><a th:href="'/goods/to_detail/'+${goods.id}">详情</a></td>
</tr>
</table>
</div>
前端的可以不用详细了解,但是你必须得看懂前端的代码,这个代码就是循环将表格展示出来,循环的内容是我们后端添加的GoodsList对象。我们使用goods接受的对象,后边的是状态可以先不用管。 这里goodsName ,goodsImg貌似是父类的私有字段,为什么可以goods可以访问?
实际上Thymeleaf 访问的是 Java Bean 的 getter 方法,而不是直接访问字段。
即使字段是 private 的,只要它有对应的 public getXxx() 方法(getter),Thymeleaf 就可以访问到这个值。
秒杀商品详情
前面商品列表前端中最后一行我们有一个超链接,是去点击商品详情的。我们需要将商品id通过路径传入。我们的需求是,点进去详情后展示的有基本商品信息和秒杀倒计时以及秒杀按钮。
第一步我们商品详情页面还是需要查询出来具体的商品所以我们首先需要在GoodsController包下定义以下方法
方法中,首先根据商品id查询具体商品信息 并添加到页面中,其次,由于详情页需要展示秒杀的状态所以我们要判断此时秒杀的状态。具体来说根据当前时间和秒杀时间判断
bash
@RequestMapping("/to_detail/{goodsId}")
public String detail(Model model,MiaoshaUser 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();
// 秒杀状态 0是未开始 1是进行中 2是已结束
int miaoshaStatus = 0;
int remainSeconds = 0;
//基本条件判断
if(now < startAt ) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
}else if(now > endAt){//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
return "goods_detail";
}
1.查询商品信息
在GoodsService下定义以下方法
bash
public GoodsVo getGoodsVoByGoodsId(long goodsId) {
return goodsDao.getGoodsVoByGoodsId(goodsId);
}
在GoodsDao下
bash
@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);
其查询代码的含义是 按着查询条件查询拼接完成的代码,参数我们需要传递到sql语句中所以需要用@param将参数传递过去
2.秒杀基本条件判断
这里代码中有注释且逻辑较为简单。我们只需记住后端传给前端了秒杀商品的状态和此刻的剩余时间。
3.前端逻辑处理
bash
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情</div>
<div class="panel-body">
//判断user是否为空
<span th:if="${user eq null}"> 您还没有登录,请登陆后再操作<br/></span>
<span>没有收货地址的提示。。。</span>
</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td colspan="3" th:text="${goods.goodsName}"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="3"><img th:src="@{${goods.goodsImg}}" width="200" height="200" /></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td th:text="${#dates.format(goods.startDate, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td id="miaoshaTip">
<input type="hidden" id="remainSeconds" th:value="${remainSeconds}" />
<span th:if="${miaoshaStatus eq 0}">秒杀倒计时:<span id="countDown" th:text="${remainSeconds}"></span>秒</span>
<span th:if="${miaoshaStatus eq 1}">秒杀进行中</span>
<span th:if="${miaoshaStatus eq 2}">秒杀已结束</span>
</td>
<td>
<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
//点击后会向地址/miaosha/do_miaosha post goods_id
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
<input type="hidden" name="goodsId" th:value="${goods.id}" />
</form>
</td>
</tr>
<tr>
<td>商品原价</td>
<td colspan="3" th:text="${goods.goodsPrice}"></td>
</tr>
<tr>
<td>秒杀价</td>
<td colspan="3" th:text="${goods.miaoshaPrice}"></td>
</tr>
<tr>
<td>库存数量</td>
<td colspan="3" th:text="${goods.stockCount}"></td>
</tr>
</table>
</div>
</body>
<script>
$(function(){
countDown();
});
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("秒杀已经结束");
}
}
</script>
</html>
实现简单秒杀
最后我们可以实现秒杀功能主要包含三部分
1减库存 2下订单 3写入秒杀订单.这三步必须在一个事务内部实现,因为如果有一个失败了就只能全部失败。
bash
@RequestMapping("/do_miaosha")
public String list(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return "login";
}
//判断库存,这里查询goods有两个目的一个是判断库存另一个是写入商品相关信息。这里虽然是goods但是联表查询的goodsVo
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
return "miaosha_fail";
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
return "miaosha_fail";
}
//减库存 下订单 写入秒杀订单
OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
model.addAttribute("orderInfo", orderInfo);
model.addAttribute("goods", goods);
return "order_detail";
}
判断库存这一步就是根据商品id查询商品,随后判断库存是否小于0。不过多赘述
第二部判断用户是否秒杀过此商品了?因为每个秒杀商品用户只能秒杀一次,所以需要判断。我们判断订单表中用户是否秒杀此商品,因此我们的条件要有两个一个是用户id一个是商品id。缺一不可
bash
@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId);
我们只需要在dao层加入相关代码。
随后就是核心代码我们首先创建miaosha对象
bash
package com.imooc.miaosha.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.domain.OrderInfo;
import com.imooc.miaosha.vo.GoodsVo;
@Service
public class MiaoshaService {
@Autowired
GoodsService goodsService;
@Autowired
OrderService orderService;
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存 下订单 写入秒杀订单
goodsService.reduceStock(goods);
//order_info maiosha_order
return orderService.createOrder(user, goods);
}
}
首先这是一个事务必须定义@Transactional
1.减库存
在我们对应秒杀商品数据库下的库存--,首先我们需要知道是那个商品。
bash
public void reduceStock(GoodsVo goods) {
MiaoshaGoods g = new MiaoshaGoods();
g.setGoodsId(goods.getId());
goodsDao.reduceStock(g);
}
这里为啥要new一个秒杀商品呢?因为我们传过来的参数是GoodsVo 在数据库并没有表对应此类型,所以需要new秒杀商品对象,随后传入商品的id,dao利用id对其更新库存。其实我个人感觉对于此更新方法直接传一个goodsId然后用@Param绑定不就好了为什么非要传一个对象?
dao层的实现
bash
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId}")
public int reduceStock(MiaoshaGoods g);
疑问点:为什么更新操作的返回值是int?这是因为更新操作会返回更新成功的行数
生成订单分为两步1.写orderinfo 2.写秒杀order 具体业务逻辑
bash
package com.imooc.miaosha.service;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imooc.miaosha.dao.OrderDao;
import com.imooc.miaosha.domain.MiaoshaOrder;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.domain.OrderInfo;
import com.imooc.miaosha.vo.GoodsVo;
@Service
public class OrderService {
@Autowired
OrderDao orderDao;
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
}
@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(0);
orderInfo.setUserId(user.getId());
//生成订单,这里要获取订单id然后写入秒杀订单里面
long orderId = orderDao.insert(orderInfo);
//秒杀订单信息吸入
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderId);
miaoshaOrder.setUserId(user.getId());
//生成秒杀订单
orderDao.insertMiaoshaOrder(miaoshaOrder);
return orderInfo;
}
}
随后在dao层写入代码
bash
@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);
疑问点:这里为什么也要加@Transactional ?外层不是加了吗?
其实单说秒杀外层函数加@Transactional其实够了,在里边加注解是为了防止别的方法调用此函数时形成不一致的情况。
疑问点:插入订单时如何返回订单id的?
用Mybatis中SelectKey注解,具体解释如下
@SelectKey(
keyColumn = "id", // 数据库中自增主键的列名
keyProperty = "id", // Java 对象中对应的属性名
resultType = long.class, // 主键的 Java 类型
before = false, // 表示在 insert 语句执行"之后"再执行 select last_insert_id()
statement = "select last_insert_id()" // 执行的 SQL 语句,用于获取最近插入记录的自增主键
)
在执行 @Insert 插入操作后,自动执行一条 SQL(这里是 select last_insert_id()),把插入成功后的自增主键值写入你传入对象(orderInfo)的某个属性中(这里是 id)。
随后在将相关信息写入秒杀订单表
bash
@Insert("insert into miaosha_order (user_id, goods_id, order_id)values(#{userId}, #{goodsId}, #{orderId})")
public int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);
订单详情
最后我们返回的是订单页
我们只需要将后端的订单信息和商品信息传入到前端,前端按一定的形式展示即可。
bash
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td th:text="${goods.goodsName}" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2"><img th:src="@{${goods.goodsImg}}" width="200" height="200" /></td>
</tr>
<tr>
<td>订单价格</td>
<td colspan="2" th:text="${orderInfo.goodsPrice}"></td>
</tr>
<tr>
<td>下单时间</td>
<td th:text="${#dates.format(orderInfo.createDate, 'yyyy-MM-dd HH:mm:ss')}" colspan="2"></td>
</tr>
<tr>
<td>订单状态</td>
<td >
<span th:if="${orderInfo.status eq 0}">未支付</span>
<span th:if="${orderInfo.status eq 1}">待发货</span>
<span th:if="${orderInfo.status eq 2}">已发货</span>
<span th:if="${orderInfo.status eq 3}">已收货</span>
<span th:if="${orderInfo.status eq 4}">已退款</span>
<span th:if="${orderInfo.status eq 5}">已完成</span>
</td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
</td>
</tr>
<tr>
<td>收货人</td>
<td colspan="2">XXX 18812341234</td>
</tr>
<tr>
<td>收货地址</td>
<td colspan="2">北京市昌平区回龙观龙博一区</td>
</tr>
</table>
</div>
今天这一节主要实现的是简单的秒杀,后续还会进行优化。