当前位置: 首页 > news >正文

学习秒杀系统-实现秒杀功能(商品列表,商品详情,基本秒杀功能实现,订单详情)

文章目录

  • 前言
  • 数据库设计
  • 秒杀商品列表页
  • 秒杀商品详情
  • 实现简单秒杀
  • 订单详情

前言

由于慕课课程中是先实现最基本的功能然后对其压测,压测那个地方出问题,然后在对其优化。所以本文记录的也是实现的是简单的秒杀功能没有涉及到高并发的优化。

数据库设计

1 商品表 包含所有商品的所有信息
2 订单表 包含所有订单的所有信息
3 秒杀商品表 包含秒杀商品的相关信息(id,商品id,商品库存,秒杀价格,开始日期,结束日期)
4 秒杀订单表 包含秒杀订单的相关信息(id,订单id,用户id,商品id)
为什么要这样设计?可不可以保留1和2 ,3和4在1和2中添加一个字段表示?
实际上这样可行,但是不推荐,因为秒杀有很多种类型。今天秒杀,明天促销,后天八折 岂不是每次搞一个活动都要去重新设计表和字段。而且还要修改后端相关代码。这样不利于维护和扩展。 其次我们单独新建一个表,是因为秒杀是多个用户同一时间下订单所以并发量非常大我们需要单独一个表在redis支撑后期。
详细字段可以github上找一下。

秒杀商品列表页

首页秒杀商品列表页其实就是将所有的秒杀商品信息查询出来并返回。

首先在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")public List<GoodsVo> listGoodsVo();

这里需要解释一下可能的疑问点
1.左连接是什么意思?
左连接是保存左表的全部内容,然后按定义的连接条件与右表连接,若右表没有连接规定的数据则对应的字段为null。
2.mg.goods_id = g.id 是连接条件还是查询条件?
是连接条件,也就是按什么条件将两表连接在一起。总不能乱拼,肯定按一定的条件拼接。若要再写查询条件后边只需跟where,这里where作用的是连接后的表。
3.为什么选择左连接?
其实左连接和右连接没有区别,最重要的一点是如果你使用左连接则会保存左表的全部数据,所以左 表一般是右表的子集(或者比右表小),如果左表是更大的,右表是小的,则连接后会发现最后的数据表很多字段为null。在这里秒杀商品是商品的子集

查询出来后我们要新建一个VO对象接受,因为查询出来的字段不仅包含商品字段还会包含秒杀商品的相关字段
所以我们需要在VO目录下新建一个对象

然后在GoodsService下定义

	@AutowiredGoodsDao goodsDao;public List<GoodsVo> listGoodsVo(){return goodsDao.listGoodsVo();}

最后在GoodsController类定义以下方法

    @RequestMapping("/to_list")public String list(Model model,MiaoshaUser user) {// 向goods_list页面添加user对象,至于user是怎么拦截的可以看登录功能model.addAttribute("user", user);//查询商品列表List<GoodsVo> goodsList = goodsService.listGoodsVo();model.addAttribute("goodsList", goodsList);return "goods_list";}

实际上我们写代码是从controller层开始写,这里只是方便展示从dao层开始写。

package com.imooc.miaosha.vo;import java.util.Date;import com.imooc.miaosha.domain.Goods;public class GoodsVo extends Goods{private Double miaoshaPrice;private Integer stockCount;private Date startDate;private Date endDate;public Integer getStockCount() {return stockCount;}public void setStockCount(Integer stockCount) {this.stockCount = stockCount;}public Date getStartDate() {return startDate;}public void setStartDate(Date startDate) {this.startDate = startDate;}public Date getEndDate() {return endDate;}public void setEndDate(Date endDate) {this.endDate = endDate;}public Double getMiaoshaPrice() {return miaoshaPrice;}public void setMiaoshaPrice(Double miaoshaPrice) {this.miaoshaPrice = miaoshaPrice;}
}

疑问点?我们new一个GoodsVo对象可以访问父类的private字段吗?
不能,只能通过get方法访问,即使子类继承了父类。

随后实现good_list前端页面

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

前端的可以不用详细了解,但是你必须得看懂前端的代码,这个代码就是循环将表格展示出来,循环的内容是我们后端添加的GoodsList对象。我们使用goods接受的对象,后边的是状态可以先不用管。 这里goodsName ,goodsImg貌似是父类的私有字段,为什么可以goods可以访问?
实际上Thymeleaf 访问的是 Java Bean 的 getter 方法,而不是直接访问字段。
即使字段是 private 的,只要它有对应的 public getXxx() 方法(getter),Thymeleaf 就可以访问到这个值。

秒杀商品详情

前面商品列表前端中最后一行我们有一个超链接,是去点击商品详情的。我们需要将商品id通过路径传入。我们的需求是,点进去详情后展示的有基本商品信息和秒杀倒计时以及秒杀按钮。
第一步我们商品详情页面还是需要查询出来具体的商品所以我们首先需要在GoodsController包下定义以下方法

方法中,首先根据商品id查询具体商品信息 并添加到页面中,其次,由于详情页需要展示秒杀的状态所以我们要判断此时秒杀的状态。具体来说根据当前时间和秒杀时间判断

 @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();// 秒杀状态 0是未开始 1是进行中 2是已结束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";}

1.查询商品信息
在GoodsService下定义以下方法

	public GoodsVo getGoodsVoByGoodsId(long goodsId) {return goodsDao.getGoodsVoByGoodsId(goodsId);}

在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 where g.id = #{goodsId}")public GoodsVo getGoodsVoByGoodsId(@Param("goodsId")long goodsId);

其查询代码的含义是 按着查询条件查询拼接完成的代码,参数我们需要传递到sql语句中所以需要用@param将参数传递过去

2.秒杀基本条件判断
这里代码中有注释且逻辑较为简单。我们只需记住后端传给前端了秒杀商品的状态和此刻的剩余时间。

3.前端逻辑处理

<div class="panel panel-default"><div class="panel-heading">秒杀商品详情</div><div class="panel-body">//判断user是否为空<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">//点击后会向地址/miaosha/do_miaosha post goods_id<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>

实现简单秒杀

最后我们可以实现秒杀功能主要包含三部分
1减库存 2下订单 3写入秒杀订单.这三步必须在一个事务内部实现,因为如果有一个失败了就只能全部失败。

    @RequestMapping("/do_miaosha")public String list(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) {model.addAttribute("user", user);if(user == null) {return "login";}//判断库存,这里查询goods有两个目的一个是判断库存另一个是写入商品相关信息。这里虽然是goods但是联表查询的goodsVoGoodsVo 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.REPEATE_MIAOSHA.getMsg());return "miaosha_fail";}//减库存 下订单 写入秒杀订单OrderInfo orderInfo = miaoshaService.miaosha(user, goods);model.addAttribute("orderInfo", orderInfo);model.addAttribute("goods", goods);return "order_detail";}

判断库存这一步就是根据商品id查询商品,随后判断库存是否小于0。不过多赘述

第二部判断用户是否秒杀过此商品了?因为每个秒杀商品用户只能秒杀一次,所以需要判断。我们判断订单表中用户是否秒杀此商品,因此我们的条件要有两个一个是用户id一个是商品id。缺一不可

@Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}")public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId);

我们只需要在dao层加入相关代码。

随后就是核心代码我们首先创建miaosha对象

package com.imooc.miaosha.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.domain.OrderInfo;
import com.imooc.miaosha.vo.GoodsVo;@Service
public class MiaoshaService {@AutowiredGoodsService goodsService;@AutowiredOrderService orderService;@Transactionalpublic OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {//减库存 下订单 写入秒杀订单goodsService.reduceStock(goods);//order_info maiosha_orderreturn orderService.createOrder(user, goods);}}

首先这是一个事务必须定义@Transactional
1.减库存
在我们对应秒杀商品数据库下的库存–,首先我们需要知道是那个商品。

	public void reduceStock(GoodsVo goods) {MiaoshaGoods g = new MiaoshaGoods();g.setGoodsId(goods.getId());goodsDao.reduceStock(g);}

这里为啥要new一个秒杀商品呢?因为我们传过来的参数是GoodsVo 在数据库并没有表对应此类型,所以需要new秒杀商品对象,随后传入商品的id,dao利用id对其更新库存。其实我个人感觉对于此更新方法直接传一个goodsId然后用@Param绑定不就好了为什么非要传一个对象?
dao层的实现

@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId}")public int reduceStock(MiaoshaGoods g);

疑问点:为什么更新操作的返回值是int?这是因为更新操作会返回更新成功的行数

生成订单分为两步1.写orderinfo 2.写秒杀order 具体业务逻辑

package com.imooc.miaosha.service;import java.util.Date;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import com.imooc.miaosha.dao.OrderDao;
import com.imooc.miaosha.domain.MiaoshaOrder;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.domain.OrderInfo;
import com.imooc.miaosha.vo.GoodsVo;@Service
public class OrderService {@AutowiredOrderDao orderDao;public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);}@Transactionalpublic 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(0);orderInfo.setUserId(user.getId());//生成订单,这里要获取订单id然后写入秒杀订单里面long orderId = orderDao.insert(orderInfo);//秒杀订单信息吸入MiaoshaOrder miaoshaOrder = new MiaoshaOrder();miaoshaOrder.setGoodsId(goods.getId());miaoshaOrder.setOrderId(orderId);miaoshaOrder.setUserId(user.getId());//生成秒杀订单orderDao.insertMiaoshaOrder(miaoshaOrder);return orderInfo;}}

随后在dao层写入代码

@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()")public long insert(OrderInfo orderInfo);

疑问点:这里为什么也要加@Transactional ?外层不是加了吗?
其实单说秒杀外层函数加@Transactional其实够了,在里边加注解是为了防止别的方法调用此函数时形成不一致的情况。

疑问点:插入订单时如何返回订单id的?
用Mybatis中SelectKey注解,具体解释如下
@SelectKey(
keyColumn = “id”, // 数据库中自增主键的列名
keyProperty = “id”, // Java 对象中对应的属性名
resultType = long.class, // 主键的 Java 类型
before = false, // 表示在 insert 语句执行“之后”再执行 select last_insert_id()
statement = “select last_insert_id()” // 执行的 SQL 语句,用于获取最近插入记录的自增主键
)
在执行 @Insert 插入操作后,自动执行一条 SQL(这里是 select last_insert_id()),把插入成功后的自增主键值写入你传入对象(orderInfo)的某个属性中(这里是 id)。

随后在将相关信息写入秒杀订单表

	@Insert("insert into miaosha_order (user_id, goods_id, order_id)values(#{userId}, #{goodsId}, #{orderId})")public int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);

订单详情

最后我们返回的是订单页
我们只需要将后端的订单信息和商品信息传入到前端,前端按一定的形式展示即可。

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

今天这一节主要实现的是简单的秒杀,后续还会进行优化。

http://www.lryc.cn/news/589544.html

相关文章:

  • Sharding-JDBC 分布式事务实战指南:XA/Seata 方案解析(三)
  • 2HDMI/1DP转EDP/LVDS,支持4K,144HZ和240HZ.
  • LSA链路状态通告
  • 学习软件测试的第十六天
  • 项目进度跨地域团队协作困难,如何统一进度安排
  • 原来时间序列挖掘这么简单
  • 力扣73:矩阵置零
  • NW917NW921美光固态闪存NW946NW952
  • 游戏行业中的恶梦:不断升级的DDoS攻击
  • 【HarmonyOS】ArkUI-X 跨平台框架入门详解(一)
  • 3.正则化——新闻分类
  • 【stm32】新建工程
  • STM32裸机开发(中断,轮询,状态机)与freeRTOS
  • MyBatis与Spring整合优化实战指南:从配置到性能调优
  • Conda 核心命令快速查阅表
  • 系统编程是什么
  • 22-C#的委托简单使用-2
  • ai问答推荐企业排名优化?:五大企业核心竞争力全景对比
  • 从0开始学习R语言--Day47--Nomogram
  • 【51单片机先流水2秒后数码显示2秒后显示END】2022-9-5
  • 判断QMetaObject::invokeMethod()里的函数是否调用成功
  • 密码协议的基本概念
  • 【Linux手册】重定向是如何实现的?Linux下为什么一切皆文件?
  • 【env环境】rtthread5.1.0使用fal组件
  • npm install failed如何办?
  • 差分信号接口选型指南:深入解析LVDS、SubLVDS、SLVDS与SLVDS-EC**
  • 回顾一下Docker的基本操作
  • 项目:从零开始制作一个微信小程序(第六天)
  • 专业文档搜索工具,快速定位文本内容
  • Spring AI Alibaba 1.0 vs Spring AI 深度对比