数据库要设计以下几个表:商品表、订单表、秒杀商品表、秒杀订单表。之所以多设计一个秒杀商品表,是为了更好的维护和扩展。
CREATE TABLE `NewTable` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID' ,
`goods_name` varchar(16) NULL DEFAULT NULL COMMENT '商品名称' ,
`goods_title` varchar(64) NULL DEFAULT NULL COMMENT '商品标题' ,
`goods_img` varchar(64) NULL DEFAULT NULL COMMENT '商品图片' ,
`goods_detail` longtext NULL COMMENT '商品详情介绍' ,
`goods_price` decimal(10,2) NULL DEFAULT 0.00 COMMENT '商品单价' ,
`goods_stock` int(11) NULL DEFAULT 0 COMMENT '商品库存,-1表示没有限制' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=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','华为 Mate 9 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待',3212.00,-1);
CREATE TABLE `miaosha_goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀的商品表',
`goods_id` bigint(20) 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,10,'2017-11-05 15:18:00','2017-11-13 14:00:18'),
(2,2,0.01,10,'2017-11-12 14:00:14','2017-11-13 14:00:24');
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;
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;
创建完这四张表之后,分别为其创建domain对象。idea有可以直接生成的工具,也可以自己写,反正只要字段对应就OK。
创建一个GoodsVo,将商品和秒杀信息统一起来:
@Data
public class GoodsVo extends Goods {
private Double miaoshaPrice;
private Integer stockCount;
private Date startDate;
private Date endDate;
}
新建一个GoodsDao,其会从两个表中查询出GoodsVo,所以这里用到了左连接来查询。(其实用注解来写sql语句还是挺不方便的,不知道老师什么时候会换成XML)
@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> listGoodsVo();
}
然后在Service里也写出对应的方法。(省略)在Controller中注入Service,并且将查询到的内容写入Model。
<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>
意识到自己要换电脑,还要重新建数据库表,那岂不是很麻烦。本来想用Flyway,可觉得还是不如直接部署到云上来得方便。当然,Flyway可以以后再引入。
Mysql安装在服务器上参照这篇博客。
然后再Navicat中直接将表复制过去即可,非常方便。
在商品列表页中点击“详情”会跳转到地址'/goods/to_detail/'+${goods.id}
在Controller中设置一个对应的方法:
将得到的id在数据库中查询到对应商品,写入Model。并且计算现在的状态(还未开始、已开始、已结束),然后也写入Model。
@RequestMapping("/to_detail/{goodsId}")
public String detail(Model model, MiaoshaUser user,
@PathVariable("goodsId")long goodsId){
model.addAttribute("user",user);
GoodsVo goods = goodsService.getByGoodsId(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";
}
写对应的Service方法。其实和之前查询列表是类似的,不同的是这里限制了ID,在Dao中写对应sql语句即可。
@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=#{id}")
GoodsVo getByGoodsId(@Param("id") long goodsId);
<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>
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("秒杀已经结束");
}
}
如果秒杀还没开始,就将“购买”按钮禁用,然后设置一个定时器,每隔一秒钟时间减一秒。如果reaminSeconds归零,说明倒计时完毕,去掉定时器,激活购买按钮。如果秒杀结束,也就是remiainSecond<0,就显示秒杀已结束。
不过似乎没法由进行中 --> 结束
public String list(Model model, MiaoshaUser user, @RequestParam("goodsId")long goodsId){
model.addAttribute("user",user);
if(user==null){
return "login";
}
//判断商品是否有库存
GoodsVo goodsVo = goodsService.getByGoodsId(goodsId);
int stock = goodsVo.getGoodsStock();
if(stock<=0){
model.addAttribute("errmsg", CodeMsg.MIAOSHA_OVER.getMsg());
return "miaosha_fail";
}
//判断是否已经买过此商品(防止一人买多个)
MiaoshaOrder order = orderService.getOrderById(user.getId(),goodsId);
if(order!=null){
model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
return "miaosha_fail";
}
//开始秒杀:减库存、下订单、写入秒杀订单(事务)
OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);
model.addAttribute("orderInfo", orderInfo);
model.addAttribute("goods",goodsVo);
return "order_detail";
}
Controller中逻辑十分清晰,就是判断是否有库存以及是否一人多次下单。秒杀成功后跳转到成功页面即可。
实现一下Controller中定义的方法。
OrderService目前只是简单地调用了Dao的方法,并没有进行处理。
//根据userId与goodsId查询订单
@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")
MiaoshaOrder getByUserAndGoodsId(@Param("userId")Long userId,@Param("goodsId") long goodsId);
MiaoshaService中的miaosha()方法需要定义为一个事务。
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goodsVo) {
//减库存
goodsService.reduceStock(goodsVo);
//下订单
return orderService.createOrder(user,goodsVo);
}
其又涉及到其他两个Service,在Service中对数据进行处理并且调用Dao。
GoodsService省略,GoodsDao中真正执行-1操作。
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId}")
void reduceStock(long goodsId);
在OrderService中,创建一个OrderInfo设置它的值然后插入数据表。
@Transactional
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goodsVo) {
OrderInfo orderInfo = new OrderInfo();
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); //未支付为0
orderInfo.setUserId(user.getId());
long orderId = orderDao.insertOrderInfo(orderInfo); //插入订单表
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goodsVo.getId());
miaoshaOrder.setOrderId(orderId);
miaoshaOrder.setUserId(user.getId());
orderDao.insertMiaoshaOrder(miaoshaOrder); //插入秒杀订单表
return orderInfo;
}
在OrderDao中实现方法:
在此注意一个以前从未使用的注解:@SelectKey
@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 insertOrderInfo(OrderInfo orderInfo);
@Insert("insert into miaosha_order (user_id, goods_id, order_id)values(#{userId}, #{goodsId}, #{orderId})")
int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);
@SelectKey注解的作用域是方法,效果与
@SelectKey注解用在已经被 @Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider 注解了的方法上。若在未被上述四个注解的方法上作 @SelectKey 注解则视为无效。
@SelectKey注解,既听命他人,也指挥别人,主要表现在两个方面:
(1)自身无效的情况。需要前置注解:@Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider,否则无效。
(2)他人无效的情况。如果指定了 @SelectKey 注解,那么 MyBatis 就会忽略掉由 @Options 注解所设置的生成主键。
@SelectKey的属性有下面几个:
statement属性:填入将会被执行的 SQL 字符串数组。
keyProperty属性:填入将会被更新的参数对象的属性的值。
before属性:填入 true 或 false 以指明 SQL 语句应被在插入语句的之前还是之后执行。
resultType属性:填入 keyProperty 的 Java 类型。
statementType属性:填入Statement、 PreparedStatement 和 CallableStatement 中的 STATEMENT、 PREPARED 或 CALLABLE 中任一值填入 。默认值是 PREPARED。
如果向数据库中插入一条数据,同时又希望返回该条记录的主键,该怎么处理了?有两种情况:
(1)数据库主键不是自增列,需要预先生成
(2)是自增列,插入之后才能获知
这两种情况都可以通过SelectKey解决,第一个种就是before,第二张是after。
订单详情页面:
<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 18812341234td>
tr>
<tr>
<td>收货地址td>
<td colspan="2">北京市昌平区回龙观龙博一区td>
tr>
table>
div>