商品详情-展现
http://localhost:8080/goodsDetail.htm?goodsId=2
redis商品对象缓存
1.功能要点
1.1 商品表和秒杀商品表分开设计的好处:t_goods,t_seckill_goods ,t_order,t_seckill_order
- 满足业务需要,可能有些商品在参加秒杀活动的同时,还再正价售卖
- 商品秒杀的价格和正常价格不一致,可能每次秒杀的价格也不一样,通过t_seckill_goods ,可以区分开
- 秒杀订单表t_seckill_goods ,同时设置goods_id 和user_id唯一索引,可以在数据库层面防止超卖
- 实际过程中,可以再把秒杀活动id添加到上面唯一索引,因为每个商品可能参与多次秒杀活动
1.2 关于商品秒杀商品状态和倒计时
可以在把商品开始秒杀的时间,放在redis里面,
这样直接从redis返回就行,不需要每次刷新商品的时候,都去数据库访问
2. 优化点
- 没有优化的做法:首先是通过thymeleaf模板引擎,后端渲染直接返回
- 优化的步骤1:页面缓存:在后端,把goodsDetail.htm?goodsId=2 这个url页面直接缓存,
通过thymeleaf手动渲染,把页面放在redis里面 - 优化的步骤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>