项目源码下载地址:
https://github.com/wangqianlong513/springboot-redis-rabbitmq-seckill
上文讲到了登录成功,然后开始往后台/goods/to_list跳转。
1、/goods/to_list方法如下。注意此处使用了thymeleaf模板引擎,通过引擎生成了html文件。@RequestMapping(value="/to_list", produces="text/html")中的produdes=“text/html”表示此方法将返回html格式的文件。在list方法中,先通过goodsSevice方法查询到商品列表数据。然后把列表数据存入到上下文SpringWebContxt中,最后再把上下文作为参数,传入到thymeleaf的视图解析器的模板引擎中,从而产生html文件,最终把html文件返回。
@RequestMapping(value="/to_list", produces="text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user) {
model.addAttribute("user", user);
//取缓存
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
SpringWebContext ctx = new SpringWebContext(request,response,
request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
//手动渲染
String html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if(!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
2、good_list.html文件内容如下。主要是商品的列表展示,通过table标签分行展示,对后台查询的数据goodsList通过th:each进行遍历展示。然后有一个详情超链接,此超链接往商品详情页goods_detail.htm跳转,注意:是直接由一个html向另外一个html文件跳转,传入参数是商品id:goods.id。
<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_detail.htm?goodsId='+${goods.id}">详情</a></td>
</tr>
</table>
3、goods_detail.htm文件主要内容如下。现在问题来了,因为这个页面是从上一个goods_list页面跳转过来的,而且跳转的时候仅仅传递了goods_id,那么这个detail页面中的数据是如何获取到的呢?这里是通过页面中的js函数进行查询的。我们知道,在JQuery中,有$(document).ready(function(){})或者$(function(){})函数,作用是在网页加载完毕后需要执行的function方法,与普通的js方法不同,此方法使用了符号$。此处也是使用了类似的功能。PS:此处使用的是页面静态化技术。一般有两种方案来应对高并发,一种是此处的静态化技术,还有一种就是缓存技术。
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td colspan="3" id="goodsName"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="3"><img id="goodsImg" width="200" height="200" /></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td id="startTime"></td>
<td >
<input type="hidden" id="remainSeconds" />
<span id="miaoshaTip"></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" id="goodsId" />
</form>-->
<div class="row">
<div class="form-inline">
<img id="verifyCodeImg" width="80" height="32" style="display:none" onclick="refreshVerifyCode()"/>
<input id="verifyCode" class="form-control" style="display:none"/>
<button class="btn btn-primary" type="button" id="buyButton"onclick="getMiaoshaPath()">立即秒杀</button>
</div>
</div>
<input type="hidden" name="goodsId" id="goodsId" />
</td>
</tr>
<tr>
<td>商品原价</td>
<td colspan="3" id="goodsPrice"></td>
</tr>
<tr>
<td>秒杀价</td>
<td colspan="3" id="miaoshaPrice"></td>
</tr>
<tr>
<td>库存数量</td>
<td colspan="3" id="stockCount"></td>
</tr>
</table>
4、detail详情页中有下面js方法,作用是在页面加载完成后执行这个js方法,这个方法中调用了getDetail()函数。
$(function(){
getDetail();
});
5、getDetail()函数如下。此函数接收从goods_list中传递过来的商品id(g_getQueryString就是自定义的从URL地址中获取指定名称对应的参数值的方法):goods_id,然后通过ajax调用后台的方法/goods/deatail方法,同时要传入参数goodsId。
function getDetail(){
var goodsId = g_getQueryString("goodsId");
$.ajax({
url:"/goods/detail/"+goodsId,
type:"GET",
success:function(data){
if(data.code == 0){
render(data.data);
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
6、后台的/goods/detail方法如下。此方法作用是根据商品id查询商品的详信息并返回。
@RequestMapping(value="/detail/{goodsId}")
@ResponseBody
public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,
@PathVariable("goodsId")long goodsId) {
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
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;
}
GoodsDetailVo vo = new GoodsDetailVo();
vo.setGoods(goods);
vo.setUser(user);
vo.setRemainSeconds(remainSeconds);
vo.setMiaoshaStatus(miaoshaStatus);
return Result.success(vo);
}
7、上述6中的方法返回成功后,会返回到上述5中的getDetail方法中的success模块。此模块中会调用render方法来渲染页面。render方法如下。此方法会把上述6中返回的数据“填充”到detail页面中。至此,可以看到商品详情的基本数据。同时此render方法中也调用了countDown方法。
function render(detail){
var miaoshaStatus = detail.miaoshaStatus;
var remainSeconds = detail.remainSeconds;
var goods = detail.goods;
var user = detail.user;
if(user){
$("#userTip").hide();
}
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src", goods.goodsImg);
$("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"));
$("#remainSeconds").val(remainSeconds);
$("#goodsId").val(goods.id);
$("#goodsPrice").text(goods.goodsPrice);
$("#miaoshaPrice").text(goods.miaoshaPrice);
$("#stockCount").text(goods.stockCount);
countDown();
}
8、countDown方法如下。此方法主要是对秒杀时间进行控制处理,比如秒杀进行中、秒杀已经结束和倒计时功能。其中倒计时中,使用了setTimeout方法,设置参数为1000,表示每隔1000ms(1s)中执行一个更新剩余时间的操作。有个需要特别注意的地方,对于秒杀进行中的商品,也就是允许秒杀的商品,在点击“秒杀”按钮之前,需要先填入验证码,此处属于秒杀系统的“流量销峰”的优化操作。这个验证码,调用了后台的/miaosha/verifyCode方法,传入了goodsId参数。
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var timeout;
if(remainSeconds > 0){//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒");
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("秒杀进行中");
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());
$("#verifyCodeImg").show();
$("#verifyCode").show();
}else{//秒杀已经结束
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
$("#verifyCodeImg").hide();
$("#verifyCode").hide();
}
}
9、后台的/miaosha/verifyCode方法如下。方法中调用了miaoshaService中的createVerifyCode方法。此方法返回一个BufferedImage类型的对象,在java中,Image是一个抽象类,BufferedImage是其实现类,是一个带缓冲区图像类,主要作用是将一幅图片加载到内存中(BufferedImage生成的图片在内存里有一个图像缓冲区,利用这个缓冲区我们可以很方便地操作这个图片),提供获得绘图对象、图像缩放、选择图像平滑度等功能,通常用来做图片大小变换、图片变灰、设置透明不透明等。然后通过ImageIO(ImageIO框架提供了读取与写入图片数据的基本方法,使用它可以直接获取到图片文件的内容数据)的write方法把内存中的图像数据已JPEG的格式输出。
@RequestMapping(value="/verifyCode", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
try {
BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.flush();
out.close();
return null;
}catch(Exception e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
10、miaoshaService中的createVerifyCode的方法如下。此方法就是具体的生成验证码的过程。此处的验证码是数字的四则运算,当随机生成了操作符(+、-、*、/)和操作数的时候,调用了calc方法来计算运算结果。同时把计算结果存入缓存(为什么存入缓存?因为待会用户输入验证码后就可以直接从redis中取出正确的计算结果进行匹配啦)。最后返回BufferedImage对象。
public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) {
if(user == null || goodsId <=0) {
return null;
}
int width = 80;
int height = 32;
//create the image
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// set the background color
g.setColor(new Color(0xDCDCDC));
g.fillRect(0, 0, width, height);
// draw the border
g.setColor(Color.black);
g.drawRect(0, 0, width - 1, height - 1);
// create a random instance to generate the codes
Random rdm = new Random();
// make some confusion
for (int i = 0; i < 50; i++) {
int x = rdm.nextInt(width);
int y = rdm.nextInt(height);
g.drawOval(x, y, 0, 0);
}
// generate a random code
String verifyCode = generateVerifyCode(rdm);
g.setColor(new Color(0, 100, 0));
g.setFont(new Font("Candara", Font.BOLD, 24));
g.drawString(verifyCode, 8, 24);
g.dispose();
//把验证码存到redis中
int rnd = calc(verifyCode);
redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, rnd);
//输出图片
return image;
}
11、calc方法如下。此方法是通过JavaScript引擎来计算验证码结果。
private static int calc(String exp) {
try {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
return (Integer)engine.eval(exp);
}catch(Exception e) {
e.printStackTrace();
return 0;
}
}
12、在detail页面的验证码标签单击,会刷新验证码。js方法如下。和上述生成二维码的过程是一样的。
<img id="verifyCodeImg" width="80" height="32" style="display:none" onclick="refreshVerifyCode()"/>
function refreshVerifyCode(){
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()+"×tamp="+new Date().getTime());
}