实现秒杀功能
数据库设计
- 商品表
- 秒杀商品表
- 订单表
- 秒杀订单表
页面设计
商品列表页、商品详情页和订单详情页三个xml页面中都需在head双标签中引入静态文件。
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- md5.js -->
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
商品列表页
- 在domain中创建与新建的数据库表对应的映射类
- 创建GoodsController类,在其中添加list方法,用于返回goods_list.xml页面。
@RequestMapping("/to_list")
public String list(Model model, MiaoshaUser user) {
model.addAttribute("user", user);
//查询商品列表,包括商品和秒杀商品
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList); //放到Model中,供前端展示使用。
return "goods_list";
}
- 创建GoodsService类,在其中创建用于获取秒杀商品数据和商品数据的listGoodsVo方法。
@Service
public class GoodsService {
@Autowired
GoodsMapper goodsMapper;
public List<GoodsVo> listGoodsVo(){
return goodsMapper.listGoodsVo();
}
}
- 创建GoodsMapper接口,因为要查询两个商品表中部分信息,所以需要进行联表查询。
public interface GoodsMapper {
@Select("select g.*,mg.stock_count,mg.start_date,mg.end_date from miaosha_goods mg left join goods g on mg.goods_id = g.id")
public List<GoodsVo> listGoodsVo(); //goods表和miaosha_goods表之间做联合查询,接收的实体对象为自己封装的GoodsVo类。
- 为方便对查询信息进行封装,创建GoodsVo类。
@Data
public class GoodsVo extends Goods { //既包含普通商品,又包含秒杀商品的一个封装
private Double miaoshaPrice;
private Integer stockCount;
private Date startDate;
private Date endDate;
@Override
public String toString() {
return "GoodsVo{" +
"miaoshaPrice=" + miaoshaPrice +
", stockCount=" + stockCount +
", startDate=" + startDate +
", endDate=" + endDate +
'}';
}
}
- 向数据库中的商品表和秒杀商品表中添加几条数据用于测试,在static中创建img包,并将图片按照数据库中图片的存储路径进行保存。
- 在resources下的templates中创建goods_list.xml页面,通过使用for-each遍历后端返回的list,进行商品的显示。
<body>
<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>
</body>
商品详情页
- 向GoodsController类中,创建detail方法,用于返回goods_detail.xml商品详情页。
@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();
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";
}
注意:
在实际的应用中,id很少会设为自增长型,因为容易被人使用for循环遍历到所有的sql数据。一般会用snowflake,也就是分布式自增id算法。snowflake生成的ID整体上按照时间自增排序,并且因为由datacenter和workerId作区分所以整个分布式系统内不会产生ID碰撞。
- 在GoodsService中创建依据goodsId查找商品信息的方法getGoodsVoByGoodsId。
public GoodsVo getGoodsVoByGoodsId(long goodsId) {
return goodsMapper.getGoodsVoByGoodsId(goodsId);
}
- 在GoodsMapper接口中,创建getGoodsVoByGoodsId方法。
@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}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "goodsName", column = "goods_name"),
@Result(property = "goodsTitle", column = "goods_title"),
@Result(property = "goodsImg", column = "goods_img"),
@Result(property = "goodsDetail", column = "goods_detail"),
@Result(property = "goodsPrice", column = "goods_price"),
@Result(property = "goodsStock", column = "goods_stock"),
@Result(property = "miaoshaPrice", column = "miaosha_price"),
@Result(property = "stockCount", column = "stock_count"),
@Result(property = "startDate", column = "start_date"),
@Result(property = "endDate", column = "end_date")
})
public GoodsVo getGoodsVoByGoodsId(@Param("goodsId") long goodsId);
- 创建goods_detail.html页。coutDown方法用于倒计时,用reaminSeconds存储页面倒计时的时间。
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情</div>
<div class="panel-body">
<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}"/> <!-- 由于下面只有秒杀倒计时中才有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>
<!--秒杀功能的实现就是form表单的提交-->
<td>
<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
<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 = $("#countDown").text(); <!-- 这样写的话,在秒杀进行中和秒杀已结束时就没有值 -->
var remainSeconds = $("#remainSeconds").val(); <!-- 在隐藏域中取 -->
var timeout;
if (remainSeconds > 0){ //秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true); //按钮不能点
timeout = setTimeout(function () {
$("#countDown").text(remainSeconds - 1); //input标签用的是th:value 文案随着改
$("#remainSeconds").val(remainSeconds - 1); //span标签用的是th:text
countDown(); //不断回调countDown方法
}, 1000); //过1秒之后,setTimeout就会执行
}else if(remainSeconds == 0){ //秒杀进行中
$("#buyButton").attr("disabled", false);
if (timeout){
clearTimeout(timeout); //自带的清除函数吧
}
$("#miaoshaTip").html("秒杀进行中"); //等到remainSeconds减到0时,改文案
}else { //秒杀已经结束
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已结束");
}
}
</script>
注意:倒计时由客户端进行实现,不是由服务端进行实现的。因为若开启多个,数据量会很大。但是在客户端会造成时间不精确。
订单详情页
创建用于显示订单详情的order_detail.xml页面。根据后端存储在model中的名为goods的attribute,获取到相对应的值。订单的状态根据if进行判断赋值。
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td th:text="${goodsVo.goodsName}" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2">
<img th:src="@{${goodsVo.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 0}">待发货</span>
<span th:if="${orderInfo.status eq 0}">已发货</span>
<span th:if="${orderInfo.status eq 0}">已收货</span>
<span th:if="${orderInfo.status eq 0}">已退款</span>
<span th:if="${orderInfo.status eq 0}">已完成</span>
</td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
</td>
</tr>
<tr>
<td>收货人</td>
<td colspan="2">yanguobin 15842674359</td>
</tr>
<tr>
<td>收获地址</td>
<td colspan="2">中国 北京</td>
</tr>
</table>
</div>
</body>
秒杀功能
- 创建miaoshaController类,编写实现表单提交的doMiaosha方法。在其中根据前端中传入的的user,判断用户状态。若是为null就是此用户处于未登录的状态,直接返回登录页面。
model.addAttribute("user", user);
if (user == null){
return "login";
}
- 若处于登录状态,就根据前端传入的goodId进行商品库存的查询。若库存不足,便在model中添加对象,将错误信息进行保存,返回到miaosha_fail.html页面。
//判断秒杀库存
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goodsVo.getStockCount();
if (stock <= 0){
model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
return "miaosha_fail";
}
进入到result包中,在CodeMsg类中添加秒杀模块的错误提示。
public static final CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已经秒杀完毕");
public static final CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");
在resources下的templates中创建miaosha_fail.xml页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>秒杀失败</title>
</head>
<body>
秒杀失败:<p th:text="${errmsg}"></p>
</body>
</html>
- 为防止一名用户秒杀同一件商品多件,再添加方法进行判断是否秒杀到此商品。具体实现是使用了OderService对象进行查询。
//判断是否已经秒杀到
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (miaoshaOrder != null){
model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
return "miaosha_fail";
}
创建自动注入的OderService对象orderService,新建OrderService方法。
@Service
public class OrderService {
@Autowired
OrderInfoMapper orderInfoMapper;
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
return orderInfoMapper.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
}
}
创建oderMapper接口,在其中添加方法getMiaoshaOrderByUserIdGoodsId
public interface OrderInfoMapper {
@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId);
}
进入到result包中,在CodeMsg类中添加秒杀模块的错误提示。
public static final CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");
- 若此用户未秒杀到此商品,就进行减库存、下订单和写入秒杀订单的操作, 创建一个MiaoshaService类来进行对这些功能的实现。在Controller中通过创建miaoshaService对象,并调用对应的方法来进行这些操作。为方便秒杀后可以直接进入商品的详细信息,将返回的订单信息和商品信息写入到页面中,最终返回order_detail.xml订单的详情页。
//进行秒杀步骤:减库存 创建普通订单 创建秒杀订单 注意这是个事务操作
OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);
model.addAttribute("orderInfo", orderInfo);
model.addAttribute("goodsVo", goodsVo);
return "order_detail";
在Service下创建MiaoshaService类,减库存的本质就是对goods表中stockcount的数据进行更新。写订单的本质是向订单表和秒杀订单表中添加数据。
@Service
public class MiaoshaService {
@Autowired
GoodsService goodsService;
@Autowired
OrderService orderService;
@Transactional //进行秒杀步骤:减库存 创建普通订单 创建秒杀订单 注意这是个事务操作
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goodsVo) {
//减库存
goodsService.reduceStock(goodsVo);
//创建普通订单、创建秒杀订单 注意这是个事务操作
return orderService.createOrder(user, goodsVo);
}
}
注意:
1)@Transactional注解用于管理事务,当把@Transactional 注解放在类级别时,表示所有该类的公共方法都配置相同的事务属性信息。
2)原则上在每个模块的Service中应该使用此模块对应的Mapper接口,若是一定要使用其他模块的Mapper接口对象中的方法,可以向这个模块中先引入该模块的Service对象,再通过引入的Service调用希望使用的该模块的Mapper接口中的方法。
在GoodsService类中创建reduceStock方法。
public void reduceStock(GoodsVo goodsVo) {
MiaoshaGoods miaoshaGoods = new MiaoshaGoods();
miaoshaGoods.setGoodsId(goodsVo.getId());
goodsMapper.reduceStock(miaoshaGoods); //减库存其实就是根据商品id更新商品库存为原来库存减1,库存减1可以直接放到sql语句中实现
}
在GoodsMapper中写sql实现。
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId}")
public int reduceStock(MiaoshaGoods miaoshaGoods);
在OrderService中写reduceStock方法。
@Transactional
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goodsVo) {
//创建普通订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setUserId(user.getId());
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsCount(1);
orderInfo.setGoodsId(goodsVo.getId());
orderInfo.setGoodsName(goodsVo.getGoodsName());
orderInfo.setGoodsPrice(goodsVo.getMiaoshaPrice()); //这里是秒杀价格,而不是原价
orderInfo.setOrderChannel(1);
orderInfo.setStatus(0); //新建订单,未支付
long orderId = orderInfoMapper.insert(orderInfo); //返回值是通过@SelectKey获取的
//创建秒杀订单
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setUserId(user.getId());
miaoshaOrder.setGoodsId(goodsVo.getId());
miaoshaOrder.setOrderId(orderId);
orderInfoMapper.insertMiaoshaOrder(miaoshaOrder);
return orderInfo;
}
在OrderInfoMapper类中添加insert和insertMiaoshaOrder方法,分别用于向订单表中添加数据和向秒杀订单表中添加数据。
public interface OrderInfoMapper {
@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@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()") //使得该方法返回id
public long insert(OrderInfo orderInfo);
@Insert("insert into miaosha_order(user_id, goods_id, order_id) values(#{userId}, #{goodsId}, #{orderId})")
public int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);
}
注意(知识补充):
@SelectKey中的keyColumn表示数据库的列, keyProperty表示导入对象中与数据库中列对应的属性列,resultType表示返回值的类型,before表示是在参数之前获取还是之后获取,statement表示子查询语句。
遇到的问题
- 报错Error resolving template [goods_list], template might not exist or might not be accessible by any of the configured Template Resolvers,找不到goods_list页面
原因是:在运行后的target文件夹中的classes下找不到这个模版
解决方案:在pom中的build标签下添加
<resources>
<resource>
<directory>src/main/java</directory><!--java文件的路径-->
<includes>
<include>**/*.*</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory><!--资源文件的路径-->
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
- jdk查询完数据库后datetime和mysql中差8个小时
错误原因:时区错误
解决方案:spring.datasource.url中的serverTimezone=UTC,将其中的UTC改为Hongkong - miaosha_fail.xml页面中的${errmsg}会出现红色的波浪线
解决方案:为其添加注解<!--/*@thymesVar id="errmsg" type="java"*/-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>秒杀失败</title>
</head>
<body>
秒杀失败:<!--/*@thymesVar id="errmsg" type="java"*/-->
<p th:text="${errmsg}"></p>
</body>
</html>