秒杀功能开发及管理后台

一、数据库设计

数据库要设计以下几个表:商品表、订单表、秒杀商品表、秒杀订单表。之所以多设计一个秒杀商品表,是为了更好的维护和扩展。

1.商品表

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);

2.秒杀商品表

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');

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;

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;

创建完这四张表之后,分别为其创建domain对象。idea有可以直接生成的工具,也可以自己写,反正只要字段对应就OK。

二、商品列表页

1.商品+秒杀商品

创建一个GoodsVo,将商品和秒杀信息统一起来:

@Data
public class GoodsVo extends Goods {
	private Double miaoshaPrice;
	private Integer stockCount;
	private Date startDate;
	private Date endDate;
}

2.Dao与Service

新建一个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。

3.写页面

<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>

三、意图使用云Mysql

意识到自己要换电脑,还要重新建数据库表,那岂不是很麻烦。本来想用Flyway,可觉得还是不如直接部署到云上来得方便。当然,Flyway可以以后再引入。

Mysql安装在服务器上参照这篇博客。

然后再Navicat中直接将表复制过去即可,非常方便。

四、商品详情页

在商品列表页中点击“详情”会跳转到地址'/goods/to_detail/'+${goods.id}

1.Controller

在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";
    }

2.Service

写对应的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);

3.页面

<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>

4.页面的倒计时功能

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,就显示秒杀已结束。

不过似乎没法由进行中 --> 结束

五、秒杀功能实现

1.MiaoshaController

    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中逻辑十分清晰,就是判断是否有库存以及是否一人多次下单。秒杀成功后跳转到成功页面即可。

2.Service & Dao

实现一下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);

3.@SelectKey简介

1.@SelectKey简介

@SelectKey注解的作用域是方法,效果与标签等同。

@SelectKey注解用在已经被 @Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider 注解了的方法上。若在未被上述四个注解的方法上作 @SelectKey 注解则视为无效。

2.@SelectKey的使用注意事项

@SelectKey注解,既听命他人,也指挥别人,主要表现在两个方面:

(1)自身无效的情况。需要前置注解:@Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider,否则无效。
(2)他人无效的情况。如果指定了 @SelectKey 注解,那么 MyBatis 就会忽略掉由 @Options 注解所设置的生成主键。

3.@SelectKey的属性

@SelectKey的属性有下面几个:

statement属性:填入将会被执行的 SQL 字符串数组。
keyProperty属性:填入将会被更新的参数对象的属性的值。
before属性:填入 true 或 false 以指明 SQL 语句应被在插入语句的之前还是之后执行。
resultType属性:填入 keyProperty 的 Java 类型。
statementType属性:填入Statement、 PreparedStatement 和 CallableStatement 中的 STATEMENT、 PREPARED 或 CALLABLE 中任一值填入 。默认值是 PREPARED。

4.@SelectKey的应用场景

如果向数据库中插入一条数据,同时又希望返回该条记录的主键,该怎么处理了?有两种情况:

(1)数据库主键不是自增列,需要预先生成
(2)是自增列,插入之后才能获知

这两种情况都可以通过SelectKey解决,第一个种就是before,第二张是after。

4.写页面

订单详情页面:

<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>

你可能感兴趣的:(秒杀系统)