【Java秒杀方案】10.功能开发-【商品详情展示优化】页面缓存,前后端分离(网页静态化),对象缓存

商品详情-展现

http://localhost:8080/goodsDetail.htm?goodsId=2
在这里插入图片描述
redis商品对象缓存
在这里插入图片描述

1.功能要点

1.1 商品表和秒杀商品表分开设计的好处:t_goods,t_seckill_goods ,t_order,t_seckill_order

  1. 满足业务需要,可能有些商品在参加秒杀活动的同时,还再正价售卖
  2. 商品秒杀的价格和正常价格不一致,可能每次秒杀的价格也不一样,通过t_seckill_goods ,可以区分开
  3. 秒杀订单表t_seckill_goods ,同时设置goods_id 和user_id唯一索引,可以在数据库层面防止超卖
  4. 实际过程中,可以再把秒杀活动id添加到上面唯一索引,因为每个商品可能参与多次秒杀活动

1.2 关于商品秒杀商品状态和倒计时

可以在把商品开始秒杀的时间,放在redis里面,
这样直接从redis返回就行,不需要每次刷新商品的时候,都去数据库访问

2. 优化点

  1. 没有优化的做法:首先是通过thymeleaf模板引擎,后端渲染直接返回
  2. 优化的步骤1:页面缓存:在后端,把goodsDetail.htm?goodsId=2 这个url页面直接缓存,
    通过thymeleaf手动渲染,把页面放在redis里面
  3. 优化的步骤2:前后端分类,把goodsDetail.htm放在resources/static目录下,
    用户访问这个页面,通过ajax http://localhost:8080/goods/toDetail/2 活动商品对象,
    这个商品对象是缓存在redis里面的,这样可以减少网络传输的数据量,
    页面访问商品详情时,页面有浏览器的缓存,只需要单独获取商品的对象信息

3. 代码实现

goodsDetail.htm

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>商品详情</title>
  <script type="text/javascript" src="/js/jquery.min.js" ></script>
  <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>

  <table>

    <tr>
      <td>商品名称</td>
      <td>
        <input type="text" id="goodsName" name="goodsName"  readonly/>
      </td>
    </tr>
    <tr>
      <td>商品图片</td>
      <td>
        <img th:src="" id ="goodsImg" width="200px" height="200px" />
      </td>
    </tr>
    <tr>
      <td>商品价格</td>
      <td >
        <input type="text"  id="goodsPrice" name="goodsPrice"   readonly/>
      </td>
    </tr>
    <tr>
      <td>商品库存</td>
      <td >
        <input type="text" id="stockCount" readonly/>
      </td>
    </tr>
    <tr>
      <td>开始时间</td>
      <td>
        <input type="text"  id="startDate" name="startDate"   readonly/>
      </td>
    </tr>
    <tr>
      <td>结束时间</td>
      <td>
        <input type="text"  id="endDate" name="endDate"   readonly/>
      </td>
    </tr>
    <tr>
      <td>秒杀状态-倒计时</td>
      <td>
       <span id="seckillStatus" >

       </span>
      </td>
    </tr>
    <tr>
      <td>倒计时<input type="hidden" id="remainSeconds" /></td>
      <td>
        <span>秒杀倒计时: <span id="remainSecondsShow" >60</span></span>
      </td>
    </tr>

    <tr>
      <td>验证码</td>
      <td>
        <img src=""id="captchashow" alt="验证码">
      </td>
    </tr>

    <tr>
      <td>验证码确认</td>
      <td>
        <input type="text"  id="captcha" name="captcha"  />
      </td>
    </tr>

     <tr>
      <td>确认购买</td>
      <td>
        <input type="hidden"  id="goodsId" name="goodsId" />
        <button id="buyBtn" type="button" onclick="getSeckillPath();" disabled  >购买</button>

      </td>
    </tr>
  </table>


</body>
<script>
  console.log("goodsDetail");

  var goodsId= g_getQueryString("goodsId");
  //goodsDetail.htm?goodsId=1
  $(function (){
    init();
    getCaptcha();
  })

  //判断字符是否为空
  function isEmpty(obj){
    return (typeof obj === 'undefined' || obj === null || obj === "");
  }

  //判断字符是否非空
  function isNotEmpty(str){
    if(str != null && str.trim().length > 0){
      return true;
    }
    return false;
  }

  function getSeckillPath(){
    console.log("getSeckillPath...");
    var captchaval=$("#captcha").val();
    if(isEmpty(captchaval)){
      alert("请输入验证码");
      return;
    }

    $.ajax({
      url:"/seckill/path",
      type:"get",
      data:{
        goodsId:goodsId,
        captcha:$("#captcha").val()
      },
      success:function (data){
        if(data.code==200){
          console.log(data);
          var path =data.obj;
          if(isNotEmpty(path)){
            //alert("请求秒杀地址成功");
            console.log("请求秒杀地址成功");
            doSeckill(path)
          }else{
            alert("path路径为空");
          }
        }else{
          alert(data.message);
        }
      },
      error:function (data){
        alert("请求秒杀地址出现问题");
      }
    })
  }

  var result_timer;

  function doSeckill(path){
    console.log("doSeckill...");

    $.ajax({
      url:"/seckill/"+path+"/doSeckill",
      type:"post",
      data:{
        goodsId:goodsId,
        path:path
      },
      success:function (data){
        if(data.code==200){
          console.log(data);
          if(data.obj==0){
            alert("排队中...请稍后");
            //result_timer=setInterval(getResult(),2000)
            setTimeout(function (){
              getResult();
            },100)

          }
        }else{
          console.log(data.message);
          //alert(data.message);
        }
      },
      error:function (data){
        alert("秒杀出现问题");
      }
    })

  }

  function getResult(){
    console.log("getResult...");
    $.ajax({
      url:"/seckill/getResult",
      type:"post",
      data:{goodsId,goodsId},
      success:function (data){
        if(data.code==200){
          var result=data.obj;
          if(result==0){
            alert("排队中...请稍后");
            setTimeout(function (){
              getResult();
            },100)

          }else if(result==-1){
            alert("秒杀失败");
          }else{
            alert("秒杀成功,跳转订单页面");
            window.location.href="/orderDetail.htm?orderId="+result;
          }

        }else{

          alert(data.message);
        }
      },
      error:function (data){
        alert("商品秒杀结果出现问题,清继续等待");
      }
    })

  }



  function init(){
    console.log("init...");
    $.ajax({
      url:"/goods/toDetail/"+goodsId,
      type:"post",
      data:{},
      success:function (data){
        if(data.code==200){
          //alert("登录成功");
          //console.log(data.obj);
          render(data.obj);
          //window.location.href="/goods/toList"
        }else{
          alert(data.message);
        }
      },
      error:function (data){
        alert("商品详情出现问题");
      }
    })
  }

  $("#captchashow").click(function (){
    getCaptcha();
  })
  function getCaptcha(){
    console.log("getCaptcha...");
    $("#captchashow").attr("src","/seckill/captcha?goodsId="+goodsId+"&time="+(new Date()).getTime());
  }


  var timer;
  var remainSeconds;
  var seckillStatus;

  function render(obj) {
    remainSeconds = obj.remainSeconds;
    seckillStatus = obj.seckillStatus;
    $("#remainSeconds").val(remainSeconds);
    $("#remainSecondsShow").html(remainSeconds);
    var seckillStatusDesc=["秒杀未开始","秒杀正在进行","秒杀已结束","状态未知"]
    $("#seckillStatus").html(seckillStatusDesc[seckillStatus]);

    var goodsVo=obj.goodsVo;
    $("#goodsId").val(goodsVo.goodsId);
    $("#stockCount").val(goodsVo.stockCount);
    $("#goodsImg").attr("src",goodsVo.goodsImg);
    $("#goodsName").val(goodsVo.goodsName);
    $("#goodsPrice").val(goodsVo.goodsPrice);
    $("#startDate").val( new Date(goodsVo.startDate).format('yyyy-MM-dd HH:mm:ss'));
    $("#endDate").val( new Date(goodsVo.endDate).format('yyyy-MM-dd HH:mm:ss'));

    if (seckillStatus == 0) {

      $("#buyBtn").attr("disabled", true);
      timer = setInterval("countDown()", 1000);
    }

    if (seckillStatus == 1) {
      $("#buyBtn").attr("disabled", false);
    }


  }

  function countDown(){
    if(remainSeconds>0){
      remainSeconds--;
      $("#remainSecondsShow").html(remainSeconds);
    }else  if(remainSeconds==0){
      clearInterval(timer);
      $("#seckillStatus").html("秒杀正在进行");
      $("#buyBtn").attr("disabled",false);

    }
  }

  //dates.format(goods.startDate,'yyyy-MM-dd HH:mm:ss')


</script>
</html>

GoodsDetailVo.java

package com.example.miaosha.vo;

import com.example.miaosha.pojo.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoodsDetailVo {
    private User user;
    private GoodsVo goodsVo;
    private int seckillStatus;
    private int remainSeconds;
}

GoodsController.java 中关于商品详情 toDetail

package com.example.miaosha.controller;


import com.example.miaosha.mapper.GoodsMapper;
import com.example.miaosha.pojo.Goods;
import com.example.miaosha.pojo.Order;
import com.example.miaosha.pojo.User;
import com.example.miaosha.rabbitmq.MQSender;
import com.example.miaosha.service.IGoodsService;
import com.example.miaosha.vo.GoodsDetailVo;
import com.example.miaosha.vo.GoodsVo;
import com.example.miaosha.vo.RespBean;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author cch
 * @since 2021-11-17
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {
    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ThymeleafViewResolver thymeleafViewResolver;



    private String templateGoodsList="goodsList";
    private String templateGoods="goods:";
    private String templateGoodsDetail="goodsDetail:";


    //网页静态化  同时用对象缓存
    @RequestMapping( "/toDetail/{goodsId}")
    @ResponseBody
    public RespBean toDetail(Model model, User user,@PathVariable Long goodsId, HttpServletRequest request, HttpServletResponse response){
        //if(null==user){ return "login"; }
        //model.addAttribute("user", user);

        //Redis 中获取页面,如果不为空,直接返回html
        ValueOperations valueOperations = redisTemplate.opsForValue();
        GoodsDetailVo goodsDetailVoRedis = (GoodsDetailVo) valueOperations.get(templateGoodsDetail + goodsId);
        if(goodsDetailVoRedis !=null){
            return RespBean.success(goodsDetailVoRedis);
        }

        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date now=new Date();
        //0未开始 1正在进行 2 已结束
        int seckillStatus=0;
        int remainSeconds=0;
        if(now.before(startDate)){
            seckillStatus=0;
            remainSeconds=(int)((startDate.getTime()-now.getTime())/1000);
        }else if(now.after(endDate)){
            seckillStatus=2;
            remainSeconds=-1;
        }else{
            seckillStatus=1;
            remainSeconds=0;
        }

        GoodsDetailVo goodsDetailVo = new GoodsDetailVo();
        goodsDetailVo.setUser(user);
        goodsDetailVo.setGoodsVo(goodsVo);
        goodsDetailVo.setSeckillStatus(seckillStatus);
        goodsDetailVo.setRemainSeconds(remainSeconds);

        //如果为空,手动渲染,并且存入redis
        valueOperations.set(templateGoodsDetail+goodsId,goodsDetailVo,60, TimeUnit.SECONDS);

        return RespBean.success(goodsDetailVo);
    }

    //网页静态化  没有对象缓存
    @RequestMapping( "/toDetail3/{goodsId}")
    @ResponseBody
    public RespBean toDetail3(Model model, User user,@PathVariable Long goodsId, HttpServletRequest request, HttpServletResponse response){
        //if(null==user){ return "login"; }
        //model.addAttribute("user", user);

        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date now=new Date();
        //0未开始 1正在进行 2 已结束
        int seckillStatus=0;
        int remainSeconds=0;
        if(now.before(startDate)){
            seckillStatus=0;
            remainSeconds=(int)((startDate.getTime()-now.getTime())/1000);
        }else if(now.after(endDate)){
            seckillStatus=2;
            remainSeconds=-1;
        }else{
            seckillStatus=1;
            remainSeconds=0;
        }

        GoodsDetailVo goodsDetailVo = new GoodsDetailVo();
        goodsDetailVo.setUser(user);
        goodsDetailVo.setGoodsVo(goodsVo);
        goodsDetailVo.setSeckillStatus(seckillStatus);
        goodsDetailVo.setRemainSeconds(remainSeconds);

        return RespBean.success(goodsDetailVo);
    }


    //整个页面缓存
    @RequestMapping(value = "/toDetail2/{goodsId}",produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toDetail2(Model model, User user,@PathVariable Long goodsId, HttpServletRequest request, HttpServletResponse response){
        //if(null==user){ return "login"; }
        //model.addAttribute("user", user);

        //Redis 中获取页面,如果不为空,直接返回html
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String html = (String) valueOperations.get(templateGoods+goodsId);
        if(!StringUtils.isEmpty(html)){
            return html;
        }

        //Goods goods = goodsService.getById(id);
        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date now=new Date();
        //0未开始 1正在进行 2 已结束
        int seckillStatus=0;
        int remainSeconds=0;
        if(now.before(startDate)){
            seckillStatus=0;
            remainSeconds=(int)((startDate.getTime()-now.getTime())/1000);
        }else if(now.after(endDate)){
            seckillStatus=2;
            remainSeconds=-1;
        }else{
            seckillStatus=1;
            remainSeconds=0;
        }
        model.addAttribute("seckillStatus",seckillStatus);
        model.addAttribute("remainSeconds",remainSeconds);
        model.addAttribute("goods", goodsVo);

        //如果为空,手动渲染,并且存入redis
        WebContext webContext = new WebContext(request, response, request.getServletContext(),request.getLocale(), model.asMap());

        html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail",
                webContext);
        if(!StringUtils.isEmpty(html)){
            valueOperations.set(templateGoods+goodsId,html,60, TimeUnit.SECONDS);
        }

        return html;
    }
    //原始通过themeleay渲染 返回,没有页面缓存
    @RequestMapping("/toDetail0/{goodsId}")
    public String toDetail0(Model model, User user,@PathVariable Long goodsId){
        //if(null==user){ return "login"; }
        model.addAttribute("user", user);

        //Goods goods = goodsService.getById(id);
        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date now=new Date();
        //0未开始 1正在进行 2 已结束
        int seckillStatus=0;
        int remainSeconds=0;
        if(now.before(startDate)){
            seckillStatus=0;
            remainSeconds=(int)((startDate.getTime()-now.getTime())/1000);
        }else if(now.after(endDate)){
            seckillStatus=2;
            remainSeconds=-1;
        }else{
            seckillStatus=1;
            remainSeconds=0;
        }
        model.addAttribute("seckillStatus",seckillStatus);
        model.addAttribute("remainSeconds",remainSeconds);
        model.addAttribute("goods", goodsVo);

        return "goodsDetail";
    }



}

IGoodsService GoodsServiceImpl GoodsMapper 参照商品列表

GoodsMapper.xml

<select id="findGoodsVoById" resultType="com.example.miaosha.vo.GoodsVo">
        select
            g.id,
            g.goods_name,
            g.goods_title,
            g.goods_detail,
            g.goods_img,
            g.goods_price,
            g.goods_stock,
            sg.seckill_price,
            sg.stock_count,
            sg.start_date,
            sg.end_date
        from
            t_goods g ,t_seckill_goods sg
        where g.id = sg.goods_id and g.id=#{goodsId}
    </select>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值