1. 数据库设计
1.1. 商品表
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)
)engine=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
insert into goods values(1, 'iphoneX', 'Apple iPhone X(A1865) 64GB 银色 移动联通电信4G手机','/img/iphonex.png','Apple iPhone X(A1865) 64GB 银色 移动联通电信4G手机',8765.00,10000),
(2, '华为Meta9','华为 Mate 9 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待','/img/meta10.png','华为Mate9 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待', 3212.00, -1);
1.2. 秒杀商品表
create table miaosha_goods (
id bigint(20) not null auto_increment comment '秒杀的商品表',
goods_id bigint default null comment '商品id',
miaosha_price decimal(10, 2) default '0.00' comment '秒杀价',
stock_count int(11) default null comment '库存数量',
start_date datetime default null comment '秒杀开始时间',
end_date datetime default null comment '秒杀结束时间',
primary key (id)
)engine=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
insert into miaosha_goods values(1,1,0.01,4,'2021-10-08 13:11:00', '2021-10-9 14:00:18'),
(2,2,0.01,9,'2021-10-08 13:11:00', '2021-10-9 14:00:18');
1.3. 订单表
create table order_info(
id bigint(20) not null auto_increment,
user_id bigint(20) default null comment '用户id',
goods_id bigint(20) default null comment '商品ID',
delivery_addr_id bigint(20) default null comment '收货地址ID',
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新建未支付,1已支付,2已发货,3已收货,4已退款,5已完成',
create_date datetime default null comment '订单的创建时间',
pay_date datetime default null comment '支付时间',
primary key(id)
) engine=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;
1.4. 秒杀订单表
create table miaosha_order(
id bigint(20) not null auto_increment,
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)
)engine=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
2. 商品列表页
goods_list.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>商品列表</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- 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>
</head>
<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>
</html>
GoodsController.java
@RequestMapping("/to_list")
public String toList(Model model, MiaoshaUser user) {
//查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
return "goods_list";
}
3. 商品详情页
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>商品详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- 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>
</head>
<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}" />
<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">
<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>
GoodsMapper.java
public interface GoodsMapper extends BaseMapper<Goods> {
@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> listGoodsVo();
@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}")
GoodsVo getGoodsVoByGoodsId(@Param("goodsId") long goodsId);
}
GoodsService.java
@Service
public class GoodsService {
@Autowired
private GoodsMapper goodsMapper;
public List<GoodsVo> listGoodsVo() {
return goodsMapper.listGoodsVo();
}
public GoodsVo getGoodsVoByGoodsId(long goodsId) {
return goodsMapper.getGoodsVoByGoodsId(goodsId);
GoodsController.java
@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("remianSeconds", remainSeconds);
return "goods_detail";
}
4. 订单详情页
order_detail.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>订单详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- 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>
</head>
<body>
<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>
</body>
</html>
MiaoshaController.java
@RequestMapping("/miaosha")
@Controller
public class MiaoshaController {
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
@Autowired
private MiaoshaService miaoshaService;
@RequestMapping("/do_miaosha")
public String doMiaosha(Model model, MiaoshaUser 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.MIAO_SHA_OVER.getMsg());
return "miaosha_fail";
}
// 判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
model.addAttribute("errmsg", CodeMsg.REPEAT_MIAOSHA.getMsg());
return "miaosha_fail";
}
// 减库存 下订单 写入秒杀订单
OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
model.addAttribute("orderInfo",orderInfo);
model.addAttribute("goods", goods);
return "order_detail";
}
}
MiaoshaService.java
@Service
public class MiaoshaService {
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
// 减库存 下订单 写入秒杀订单
goodsService.reduceStock(goods);
// order_info miaosha_order
return orderService.createOrder(user, goods);
}
}
Orderservice.java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
return orderMapper.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
}
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(1);
orderInfo.setStatus(0);
orderInfo.setUserId(user.getId());
long orderId = orderMapper.insert(orderInfo);
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderId);
miaoshaOrder.setUserId(user.getId());
long id = orderMapper.insertMiaoshaOrder(miaoshaOrder);
return orderInfo;
}
}
OrderMapper.java
public interface OrderMapper{
@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long id, @Param("goodsId")long goodsId);
/**
* SelectKey在MyBatis中是为了解决Insert数据时不支持主键自动生成的问题,他可以很随意的设置生成主键的方式。
* 1. statement是要运行的SQL语句, 他的返回值通过resultType来指定
* 2. before标识查询语句statement运行的时机
* 3. keyProperty表示查询结果赋值给代码中的那个对象,keyColumn表示将查询结果赋值给数据库表中哪一列
* 4. keyProperty和keyColumn都不是必需的,有没有都可以
* 5. before=true, 插入之前进行查询,可以将查询结果赋给keyProperty和keyColumn,赋给keyColumn相当于更改数据库
* 6. before=false 先插入,再查询,这时只能将结果赋给keyProperty
* 7. 赋值给keyProperty用来"读"数据库,赋值给keyColumn用来写数据库
* 8. selectKey的两大作用: 1、生成主键;2、获取刚刚插入数据的主键。
* 9. 使用selectKey, 并且使用MySQL的last_insert_id()函数时,before必须为false,也就是说必须先插入然后执行last_insert_id()才能获取插入数据的ID
*/
@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()")
long insert(OrderInfo orderInfo);
@Insert("insert into miaosha_order(user_id, goods_id, order_id) values(#{userId},#{goodsId},#{orderId})")
int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);
}
GoodsService.java
@Service
public class GoodsService {
@Autowired
private GoodsMapper goodsMapper;
public List<GoodsVo> listGoodsVo() {
return goodsMapper.listGoodsVo();
}
public GoodsVo getGoodsVoByGoodsId(long goodsId) {
return goodsMapper.getGoodsVoByGoodsId(goodsId);
}
public void reduceStock(GoodsVo goods) {
goodsMapper.reduceStock(goods.getId());
}
}
GoodsMapper.java
public interface GoodsMapper extends BaseMapper<Goods> {
@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> listGoodsVo();
@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}")
GoodsVo getGoodsVoByGoodsId(@Param("goodsId") long goodsId);
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{id}")
int reduceStock(long id);
}