欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

秒杀功能开发及管理后台

程序员文章站 2022-04-22 08:05:29
...

一、数据库设计

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

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

[email protected]简介

[email protected]简介

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

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

[email protected]的使用注意事项

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

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

[email protected]的属性

@SelectKey的属性有下面几个:

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

[email protected]的应用场景

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

(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  18812341234</td>  
     </tr>
     <tr>
     		<td>收货地址</td>  
        	<td colspan="2">北京市昌平区回龙观龙博一区</td>  
     </tr>
  </table>
</div>
相关标签: 秒杀系统