Web实战课的学习笔记4

这篇博客详细记录了实现Web应用中的秒杀功能的全过程,从数据库设计(商品表、秒杀商品表、订单表、秒杀订单表)到页面设计(商品列表页、商品详情页、订单详情页)。在商品详情页中提到了分布式自增ID算法Snowflake。在秒杀功能实现中,涉及到用户登录状态检查、商品库存验证、防重复秒杀机制以及事务管理。还讨论了遇到的问题,如模板页面找不到、时区错误和页面显示异常等,并提供了相应的解决办法。
摘要由CSDN通过智能技术生成

实现秒杀功能

数据库设计

在这里插入图片描述

  1. 商品表
    在这里插入图片描述
  2. 秒杀商品表
    在这里插入图片描述
  3. 订单表
    在这里插入图片描述
  4. 秒杀订单表
    在这里插入图片描述

页面设计

商品列表页、商品详情页和订单详情页三个xml页面中都需在head双标签中引入静态文件。

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

商品列表页

  1. 在domain中创建与新建的数据库表对应的映射类
    在这里插入图片描述
  2. 创建GoodsController类,在其中添加list方法,用于返回goods_list.xml页面。
 @RequestMapping("/to_list")
    public String list(Model model, MiaoshaUser user) {
        model.addAttribute("user", user);
        //查询商品列表,包括商品和秒杀商品
        List<GoodsVo> goodsList = goodsService.listGoodsVo();
        model.addAttribute("goodsList", goodsList);     //放到Model中,供前端展示使用。
        return "goods_list";
    }
  1. 创建GoodsService类,在其中创建用于获取秒杀商品数据和商品数据的listGoodsVo方法。
@Service
public class GoodsService {

    @Autowired
    GoodsMapper goodsMapper;

    public List<GoodsVo> listGoodsVo(){
        return goodsMapper.listGoodsVo();
    }
}
  1. 创建GoodsMapper接口,因为要查询两个商品表中部分信息,所以需要进行联表查询。
public interface GoodsMapper {

    @Select("select g.*,mg.stock_count,mg.start_date,mg.end_date from miaosha_goods mg left join goods g on mg.goods_id = g.id")
    public List<GoodsVo> listGoodsVo();   //goods表和miaosha_goods表之间做联合查询,接收的实体对象为自己封装的GoodsVo类。

  1. 为方便对查询信息进行封装,创建GoodsVo类。
@Data
public class GoodsVo extends Goods {    //既包含普通商品,又包含秒杀商品的一个封装
    private Double miaoshaPrice;
    private Integer stockCount;
    private Date startDate;
    private Date endDate;

    @Override
    public String toString() {
        return "GoodsVo{" +
                "miaoshaPrice=" + miaoshaPrice +
                ", stockCount=" + stockCount +
                ", startDate=" + startDate +
                ", endDate=" + endDate +
                '}';
    }
}
  1. 向数据库中的商品表和秒杀商品表中添加几条数据用于测试,在static中创建img包,并将图片按照数据库中图片的存储路径进行保存。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 在resources下的templates中创建goods_list.xml页面,通过使用for-each遍历后端返回的list,进行商品的显示。
<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>

商品详情页

  1. 向GoodsController类中,创建detail方法,用于返回goods_detail.xml商品详情页。
 @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("remainSeconds", remainSeconds);
        return "goods_detail";
    }

注意:
在实际的应用中,id很少会设为自增长型,因为容易被人使用for循环遍历到所有的sql数据。一般会用snowflake,也就是分布式自增id算法。snowflake生成的ID整体上按照时间自增排序,并且因为由datacenter和workerId作区分所以整个分布式系统内不会产生ID碰撞。

  1. 在GoodsService中创建依据goodsId查找商品信息的方法getGoodsVoByGoodsId。
 public GoodsVo getGoodsVoByGoodsId(long goodsId) {
        return goodsMapper.getGoodsVoByGoodsId(goodsId);
    }
  1. 在GoodsMapper接口中,创建getGoodsVoByGoodsId方法。
  @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}")
    @Results({
            @Result(property = "id", column = "id"),
            @Result(property = "goodsName", column = "goods_name"),
            @Result(property = "goodsTitle", column = "goods_title"),
            @Result(property = "goodsImg", column = "goods_img"),
            @Result(property = "goodsDetail", column = "goods_detail"),
            @Result(property = "goodsPrice", column = "goods_price"),
            @Result(property = "goodsStock", column = "goods_stock"),
            @Result(property = "miaoshaPrice", column = "miaosha_price"),
            @Result(property = "stockCount", column = "stock_count"),
            @Result(property = "startDate", column = "start_date"),
            @Result(property = "endDate", column = "end_date")
    })
    public GoodsVo getGoodsVoByGoodsId(@Param("goodsId") long goodsId);
  1. 创建goods_detail.html页。coutDown方法用于倒计时,用reaminSeconds存储页面倒计时的时间。
<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}"/>    <!-- 由于下面只有秒杀倒计时中才有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>
                <!--秒杀功能的实现就是form表单的提交-->
                <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 = $("#countDown").text();     <!-- 这样写的话,在秒杀进行中和秒杀已结束时就没有值 -->
        var remainSeconds = $("#remainSeconds").val();  <!-- 在隐藏域中取 -->
        var timeout;
        if (remainSeconds > 0){ //秒杀还没开始,倒计时
            $("#buyButton").attr("disabled", true); //按钮不能点
            timeout = setTimeout(function () {
                $("#countDown").text(remainSeconds - 1);    //input标签用的是th:value       文案随着改
                $("#remainSeconds").val(remainSeconds - 1); //span标签用的是th:text
                countDown();    //不断回调countDown方法
            }, 1000);   //过1秒之后,setTimeout就会执行
        }else if(remainSeconds == 0){   //秒杀进行中
            $("#buyButton").attr("disabled", false);
            if (timeout){
                clearTimeout(timeout);  //自带的清除函数吧
            }
            $("#miaoshaTip").html("秒杀进行中"); //等到remainSeconds减到0时,改文案
        }else { //秒杀已经结束
            $("#buyButton").attr("disabled", true);
            $("#miaoshaTip").html("秒杀已结束");
        }
    }
</script>

注意:倒计时由客户端进行实现,不是由服务端进行实现的。因为若开启多个,数据量会很大。但是在客户端会造成时间不精确。

订单详情页

创建用于显示订单详情的order_detail.xml页面。根据后端存储在model中的名为goods的attribute,获取到相对应的值。订单的状态根据if进行判断赋值。

<body>
    <div class="panel panel-default">
        <div class="panel-heading">秒杀订单详情</div>
        <table class="table" id="goodslist">
            <tr>
                <td>商品名称</td>
                <td th:text="${goodsVo.goodsName}" colspan="3"></td>
            </tr>
            <tr>
                <td>商品图片</td>
                <td colspan="2">
                    <img th:src="@{${goodsVo.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 0}">待发货</span>
                    <span th:if="${orderInfo.status eq 0}">已发货</span>
                    <span th:if="${orderInfo.status eq 0}">已收货</span>
                    <span th:if="${orderInfo.status eq 0}">已退款</span>
                    <span th:if="${orderInfo.status eq 0}">已完成</span>
                </td>
                <td>
                    <button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
                </td>
            </tr>
            <tr>
                <td>收货人</td>
                <td colspan="2">yanguobin 15842674359</td>
            </tr>
            <tr>
                <td>收获地址</td>
                <td colspan="2">中国 北京</td>
            </tr>
        </table>
    </div>
</body>

秒杀功能

  1. 创建miaoshaController类,编写实现表单提交的doMiaosha方法。在其中根据前端中传入的的user,判断用户状态。若是为null就是此用户处于未登录的状态,直接返回登录页面。
 model.addAttribute("user", user);
        if (user == null){
            return "login";
        }
  1. 若处于登录状态,就根据前端传入的goodId进行商品库存的查询。若库存不足,便在model中添加对象,将错误信息进行保存,返回到miaosha_fail.html页面。
//判断秒杀库存
        GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
        int stock = goodsVo.getStockCount();
        if (stock <= 0){
            model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
            return "miaosha_fail";
        }

进入到result包中,在CodeMsg类中添加秒杀模块的错误提示。

public static final CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已经秒杀完毕");
public static final CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");

在resources下的templates中创建miaosha_fail.xml页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
    <title>秒杀失败</title>
</head>
<body>
    秒杀失败:<p th:text="${errmsg}"></p>
</body>
</html>
  1. 为防止一名用户秒杀同一件商品多件,再添加方法进行判断是否秒杀到此商品。具体实现是使用了OderService对象进行查询。
        //判断是否已经秒杀到
        MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (miaoshaOrder != null){
            model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
            return "miaosha_fail";
        }

创建自动注入的OderService对象orderService,新建OrderService方法。

@Service
public class OrderService {

    @Autowired
    OrderInfoMapper orderInfoMapper;

    public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
        return orderInfoMapper.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
    }
}

创建oderMapper接口,在其中添加方法getMiaoshaOrderByUserIdGoodsId

public interface OrderInfoMapper {

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

进入到result包中,在CodeMsg类中添加秒杀模块的错误提示。

public static final CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");
  1. 若此用户未秒杀到此商品,就进行减库存、下订单和写入秒杀订单的操作, 创建一个MiaoshaService类来进行对这些功能的实现。在Controller中通过创建miaoshaService对象,并调用对应的方法来进行这些操作。为方便秒杀后可以直接进入商品的详细信息,将返回的订单信息和商品信息写入到页面中,最终返回order_detail.xml订单的详情页。
 //进行秒杀步骤:减库存 创建普通订单 创建秒杀订单    注意这是个事务操作
        OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);
        model.addAttribute("orderInfo", orderInfo);
        model.addAttribute("goodsVo", goodsVo);
        return "order_detail";

在Service下创建MiaoshaService类,减库存的本质就是对goods表中stockcount的数据进行更新。写订单的本质是向订单表和秒杀订单表中添加数据。

@Service
public class MiaoshaService {

    @Autowired
    GoodsService goodsService;

    @Autowired
    OrderService orderService;

    @Transactional      //进行秒杀步骤:减库存 创建普通订单 创建秒杀订单    注意这是个事务操作
    public OrderInfo miaosha(MiaoshaUser user, GoodsVo goodsVo) {
        //减库存
        goodsService.reduceStock(goodsVo);
        //创建普通订单、创建秒杀订单  注意这是个事务操作
        return orderService.createOrder(user, goodsVo);
    }
}

注意:
1)@Transactional注解用于管理事务,当把@Transactional 注解放在类级别时,表示所有该类的公共方法都配置相同的事务属性信息。
2)原则上在每个模块的Service中应该使用此模块对应的Mapper接口,若是一定要使用其他模块的Mapper接口对象中的方法,可以向这个模块中先引入该模块的Service对象,再通过引入的Service调用希望使用的该模块的Mapper接口中的方法。

在GoodsService类中创建reduceStock方法。

public void reduceStock(GoodsVo goodsVo) {
        MiaoshaGoods miaoshaGoods = new MiaoshaGoods();
        miaoshaGoods.setGoodsId(goodsVo.getId());
        goodsMapper.reduceStock(miaoshaGoods);     //减库存其实就是根据商品id更新商品库存为原来库存减1,库存减1可以直接放到sql语句中实现
    }

在GoodsMapper中写sql实现。

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

在OrderService中写reduceStock方法。

 @Transactional
    public OrderInfo createOrder(MiaoshaUser user, GoodsVo goodsVo) {
        //创建普通订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setUserId(user.getId());
        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);     //新建订单,未支付
        long orderId = orderInfoMapper.insert(orderInfo);  //返回值是通过@SelectKey获取的
        //创建秒杀订单
        MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
        miaoshaOrder.setUserId(user.getId());
        miaoshaOrder.setGoodsId(goodsVo.getId());
        miaoshaOrder.setOrderId(orderId);
        orderInfoMapper.insertMiaoshaOrder(miaoshaOrder);
        return orderInfo;
    }

在OrderInfoMapper类中添加insert和insertMiaoshaOrder方法,分别用于向订单表中添加数据和向秒杀订单表中添加数据。

public interface OrderInfoMapper {

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

    @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()")    //使得该方法返回id
    public long insert(OrderInfo orderInfo);

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

注意(知识补充):
@SelectKey中的keyColumn表示数据库的列, keyProperty表示导入对象中与数据库中列对应的属性列,resultType表示返回值的类型,before表示是在参数之前获取还是之后获取,statement表示子查询语句。

遇到的问题

  1. 报错Error resolving template [goods_list], template might not exist or might not be accessible by any of the configured Template Resolvers,找不到goods_list页面
    原因是:在运行后的target文件夹中的classes下找不到这个模版
    解决方案:在pom中的build标签下添加
<resources>
            <resource>
                <directory>src/main/java</directory><!--java文件的路径-->
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory><!--资源文件的路径-->
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
  1. jdk查询完数据库后datetime和mysql中差8个小时
    错误原因:时区错误
    解决方案:spring.datasource.url中的serverTimezone=UTC,将其中的UTC改为Hongkong
  2. miaosha_fail.xml页面中的${errmsg}会出现红色的波浪线
    解决方案:为其添加注解 <!--/*@thymesVar id="errmsg" type="java"*/-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
    <title>秒杀失败</title>
</head>
<body>
    秒杀失败:<!--/*@thymesVar id="errmsg" type="java"*/-->
    <p th:text="${errmsg}"></p>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值