品优购项目笔记 day19

目标

  • 能够说出秒杀的实现思路
  • 实现秒杀频道首页功能
  • 实现秒杀商品详情页功能
  • 实现秒杀下单功能
  • 实现秒杀支付功能

1. 秒杀业务分析

1. 需求分析

所谓秒杀,就是网络卖家发布一些超低价格的商品,所有买家在统一时间网上抢购的一种销售方式。通俗说就是网络商家为了促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。

通常两种限制:库存限制、时间限制。

需求:

  1. 商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
  2. 运营商审核秒杀申请
  3. 秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详情页
  4. 商品详情页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内无法秒杀
  5. 秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单
  6. 当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,回复库存

2. 数据库表分析

Tb_seckill_goods 秒杀商品表

在这里插入图片描述

Tb_seckill_order 秒杀订单表

在这里插入图片描述

3. 秒杀实现思路

秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力,读取商品详情信息时用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为0时或活动期结束,同步到数据库。产生的秒杀预订单也不会立刻写到数据库,而是先写到缓存,当用户付款成功再写到数据库。

4. 工程搭建与准备

1. 工程模块搭建

  1. 创建秒杀服务接口模块pinyougou-seckill-interface ,依赖pinyougou-pojo
  2. 创建秒杀服务模块pinyougou-seckill-service(war),pom.xml引入依赖参见其它服务工程,依赖 pinyougou-seckill-service , Tomcat7插件运行端口为9009。添加web.xml、 spring 配置文件参见其它服务工程, dubbox的端口为20889
  3. 创建秒杀频道web模块 pinyougou-seckill-web(war) pom.xml引入依赖参见cart_web工程(需添加单点登录和权限控制),依赖 pinyougou-seckill-interface ,Tomcat7插件运行端口为9109 添加web.xml、 spring 配置文件参见cart_web工程。将秒杀相关的页面及资源拷贝到此模块。添加angularJS.

2. 代码生成

生成代码,拷入工程

5. 秒杀商品后台管理(TODO)

运营商系统web模块pinyougou-manager-web依赖 pinyougou-seckill-interface

商家系统web模块pinyougou-shop-web依赖pinyougou-seckill-interface

扫描二维码关注公众号,回复: 6395432 查看本文章

1. 商家后台

  1. 秒杀商品列表
  2. 秒杀商品申请
  3. 秒杀订单查询

2. 运营商后台

  1. 待审核秒杀商品列表
  2. 秒杀商品审核
  3. 秒杀订单查询

2. 品优购-秒杀频道首页

1. 需求分析

秒杀频道首页,显示正在秒杀的商品(已经开始,未结束的商品)

2. 后端代码

1. 服务接口层

  1. 修改seckill-interface的SeckillGoodsService.java
/**
	 * 返回正在参与秒杀的商品
	 * @return
	 */
public List<TbSeckillGoods> findList();
  1. 服务实现层

修改seckill-service的SeckillGoodsServiceImpl.java

@Override
public List<TbSeckillGoods> findList() {
  TbSeckillGoodsExample example = new TbSeckillGoodsExample();
  Criteria criteria = example.createCriteria();
  criteria.andStatusEqualTo("1");//审核通过的商品
  criteria.andStockCountGreaterThan(0);//库存数大于0
  criteria.andStartTimeLessThanOrEqualTo(new Date());//开始日期小于等于当前日期
  criteria.andEndTimeGreaterThanOrEqualTo(new Date());//截止日期大于等于当前日期
  List<TbSeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example);
  return seckillGoodsList;
}

3. 控制层

修改seckill-web的SeckillGoodsController.java

@RequestMapping("/findList")
public List<TbSeckillGoods> findList(){
  return seckillGoodsService.findList();
}

3. 前端代码

1. 服务层

在seckill-web创建seckillGoodsService.js

app.service('seckillGoodsService',function ($http) {
  // 读取列表数据到表单
  this.findList=function () {
    return $http.get('seckillGoods/findList.do');
  }
});

2. 控制层

在seckill-web创建seckillGoodsController.js

// 控制层
app.controller('seckillGoodsController',function () {
  seckillGoodsService.findList().success(
    function (response) {
      $scope.list = response;
    }
  );
});

3. 页面

修改seckill-index.html,引入js

<script type="text/javascript" src="plugins/angularjs/angular.min.js">  </script>
<script type="text/javascript" src="js/base.js">  </script>
<script type="text/javascript" src="js/service/seckillGoodsService.js">  </script>
<script src="js/controller/seckillGoodsController.js">  </script>

指令

<body ng-app="pinyougou" ng-controller="seckillGoodsController" ng-init="findList()">

循环列表的实现

<li class="seckill-item" ng-repeat="pojo in list">
  <div class="pic" onclick="location.href='seckill-item.html'">
    <img src="{{pojo.smallPic}}" width="200px" height="200px" alt=''  >
  </div>
  <div class="intro"><span>{{pojo.title}}</span></div>
  <div class='price'><b class='sec-price'>¥{{pojo.costPrice}}</b><b class='ever-price'>¥{{pojo.price}}</b></div>
  <div class='num'>
    <div>已售{{ ((pojo.num-pojo.stockCount)/pojo.num*100).toFixed(0) }}%</div>
    <div class='progress'>
      <div class='sui-progress progress-danger'><span style='width: {{ ((pojo.num-pojo.stockCount)/pojo.num*100).toFixed(0) }}%;' class='bar'></span></div>
    </div>
    <div>剩余<b class='owned'>{{pojo.stockCount}}</b></div>
  </div>
  <a class='sui-btn btn-block btn-buy' href='seckill-item.html' target='_blank'>立即抢购</a>
</li>

测试:seckill-service和seckill-web都启动,通过测试

4. 缓存处理

修改seckill-service的SeckillGoodsServiceImpl.java

@Override
public List<TbSeckillGoods> findList() {
  List<TbSeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();
  if(seckillGoodsList==null || seckillGoodsList.size()==0){
    TbSeckillGoodsExample example = new TbSeckillGoodsExample();
    Criteria criteria = example.createCriteria();
    criteria.andStatusEqualTo("1");//审核通过的商品
    criteria.andStockCountGreaterThan(0);//库存数大于0
    criteria.andStartTimeLessThanOrEqualTo(new Date());//开始日期小于等于当前日期
    criteria.andEndTimeGreaterThanOrEqualTo(new Date());//截止日期大于等于当前日期
    seckillGoodsList = seckillGoodsMapper.selectByExample(example);
    // 将列表数据装入缓存
    for (TbSeckillGoods seckillGoods : seckillGoodsList) {
      redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getId(),seckillGoods);
    }
    System.out.println("从数据库中读取数据装入缓存");
  }else {
    System.out.println("从缓存中读取数据");
  }
  return seckillGoodsList;
}

3. 品优购-秒杀详情页

1. 需求分析

商品详情页显示秒杀商品信息

2. 显示详情页信息

1. 后端代码

修改seckill-interface的SeckillGoodsService

// 从缓存中查询
public TbSeckillGoods findOneFromRedis(Long id);

修改seckill-service的SeckillGoodsServiceImpl.java

@Override
public TbSeckillGoods findOneFromRedis(Long id) {
  return (TbSeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(id);
}

修改seckill-web的SeckillGoodsController

@RequestMapping("/findOneFromRedis")
public TbSeckillGoods findOneFromRedis(Long id){
  return seckillGoodsService.findOneFromRedis(id);
}

2. 前端代码

seckill-web的seckillGoodsService.js

this.findOne=function(id){
  return $http.get('seckillGoods/findOneFromRedis.do?id='+id);		
}

seckill-web的seckillGoodsController.js,引入$location服务

// 查询商品
$scope.findOne = function () {
  //接收参数
  var id=$location.search()['id'];
  seckillGoodsService.findOne(id).success(
    function (response) {
      $scope.entity=response;
    }
  );
}

修改seckill-item.html,引入js

<script type="text/javascript" src="plugins/angularjs/angular.min.js">  </script>
<script type="text/javascript" src="js/base.js">  </script>
<script type="text/javascript" src="js/service/seckillGoodsService.js">  </script>
<script src="js/controller/seckillGoodsController.js">  </script> 

指令

<body ng-app="pinyougou" ng-controller="seckillGoodsController" ng-init="findOne()">

用表达式显示标题

<h4>{{entity.title}}</h4>

图片

<span class="jqzoom"><img jqimg="{{entity.smallPic}}" src="{{entity.smallPic}}" width="400px" height="400px" /></span>

价格

<div class="fl price"><i>¥</i>
	<em>{{entity.costPrice}}</em>
	<span>原价:{{entity.price}}</span>
</div>

介绍

<div class="intro-detail">{{entity.introduction}}</div>

剩余库存

剩余库存:{{entity.stockCount}}

3. 秒杀倒计时效果

1. $interval服务简介

在angularjs中$interval服务用来处理间歇性处理一些事情

格式为:

 $interval(执行的函数,间隔的毫秒数,运行次数);

运行次数可以省略,不省略会无限循环执行

取消用cancel

案例

$scope.second=10;
time=$interval(function () {
  $scope.second=$scope.second-1;
  if($scope.second<=0){
    $interval.cancel(time);
  }
},1000);

页面效果表达式显示$scope.second

2. 秒杀倒计时

修改seckillGoodsController.js,实现

// 转化秒为 天小时分钟秒格式 xxx天 10:22:33
convertTimeString = function (allSecond) {
  var days = Math.floor(allSecond/(60*60*24));
  var hours = Math.floor((allSecond-days*60*60*24)/(60*60));//小时
  var minutes = Math.floor((allSecond-days*60*60*24-hours*60*60)/60);//分钟数
  var seconds = allSecond-days*60*60*24-hours*60*60-minutes*60;//秒数
  var timeString="";
  if(days>0){
    timeString+=days+"天";
  }
  return timeString+hours+":"+minutes+":"+seconds;
}
// 查询商品
$scope.findOne = function () {
  //接收参数
  var id=$location.search()['id'];
  seckillGoodsService.findOne(id).success(
    function (response) {
      $scope.entity=response;

      // 倒计时开始
      // 获取从结束时间到当前日期的秒数
      allSecond = Math.floor((new Date($scope.entity.endTime).getTime()-new Date().getTime())/1000);
      time=$interval(function () {
        allSecond=allSecond-1;
        $scope.timeString = convertTimeString(allSecond);
        if(allSecond<=0){
          $interval.cancel(time);
        }
      },1000);
    }
  );
}

页面修改,显示time

<span class="overtime"> 距离结束:{{timeString}}</span>

4. 品优购-秒杀下单

1. 需求分析

商品详情页点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀

2. 后端代码

1. 服务接口层

修改seckill-interface的SeckillOrderService.java

// 秒杀下单
public void submitOrder(Long seckillId,String userId);

2. 服务实现层

spring配置文件配置IdWorker

seckill-service的SeckillOrderServiceImpl.java实现方法

@Override
public void submitOrder(Long seckillId, String userId) {
  // 查询缓存中的商品
  TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(seckillId);
  if(seckillGoods==null){
    throw new RuntimeException("商品不存在");
  }
  if(seckillGoods.getStockCount()<=0){
    throw new RuntimeException("商品已经被抢光");
  }

  // 减少库存
  seckillGoods.setStockCount(seckillGoods.getStockCount()-1);//减库存
  redisTemplate.boundHashOps("seckillGoods").put(seckillId,seckillGoods);//存入缓存
  if(seckillGoods.getStockCount()==0){
    seckillGoodsMapper.updateByPrimaryKey(seckillGoods);//更新数据库
    redisTemplate.boundHashOps("seckillGoods").delete(seckillId);
    System.out.println("商品同步到数据库。。。");
  }

  // 3. 存储秒杀订单(不向数据库中存储,只向缓存存储)、
  TbSeckillOrder seckillOrder = new TbSeckillOrder();
  seckillOrder.setId(idWorker.nextId());
  seckillOrder.setSeckillId(seckillId);
  seckillOrder.setMoney(seckillGoods.getPrice());
  seckillOrder.setSellerId(seckillGoods.getSellerId());
  seckillOrder.setCreateTime(new Date());
  seckillOrder.setStatus("0");//状态

  redisTemplate.boundHashOps("seckillOrder").put(userId,seckillOrder);
  System.out.println("保存订单成功(redis)");
}

3. 控制层

修改seckill-web的SeckillOrderController.java

@RequestMapping("/submitOrder")
public Result submitOrder(Long seckillId){
  // 提取当前用户
  String username = SecurityContextHolder.getContext().getAuthentication().getName();
  if("anonymousUser".equals(username)){
    return new Result(false,"当前用户未登录");
  }

  try {
    seckillOrderService.submitOrder(seckillId,username);
    return new Result(true,"提交订单成功");
  } catch(RuntimeException e){
    e.printStackTrace();
    return new Result(false,e.getMessage());
  }
  catch (Exception e) {
    e.printStackTrace();
    return new Result(false,"提交订单失败");
  }
}

3. 前端代码

1. 前端服务层

seckill-web的seckillGoodsService.js

// 提交订单
this.submitOrder=function (seckillId) {
  return $http.get('seckillOrder/submitOrder.do?seckillId='+seckillId);
}

2. 前端控制层

seckill-web的seckillGoodsController.js

// 提交订单
$scope.submitOrder=function () {
  seckillGoodsService.submitOrder($scope.entity.id).success(
    function (response) {
      if(response.success){
        alert("抢购成功,请在5分钟内完成支付");
        location.href = "pay.html";//跳转支付页面
      }else{
        alert(response.message);
      }
    }
  );
};

3. 页面

修改seckill-item.html

<a href="submitOrder()" target="_blank" class="sui-btn  btn-danger addshopcar">立即抢购</a>

5. 品优购-秒杀支付

1. 需求分析

用户成功下单后,跳转到支付页面,支付页显示微信支付二维码,用户完成支付后,保存订单到数据库

2. 生成支付二维码

1. 后端代码

  1. seckill-web工程引入pay-interface依赖
  2. 修改seckill-intercface的SeckillOrderService.java
// 缓存中获取订单
public TbSeckillOrder searchOrderFromRedisByUserId(String userId);
  1. 修改seckill-service的SeckillOrderServiceImpl.java
@Override
public TbSeckillOrder searchOrderFromRedisByUserId(String userId) {
  return (TbSeckillOrder) redisTemplate.boundHashOps("seckillOrder").get(userId);
}
  1. 在seckill-web新建PayController.java
@RequestMapping("/createNative")
public Map createNative(){
  // 1. 获取当前登录用户名
  String username = SecurityContextHolder.getContext().getAuthentication().getName();
  // 2. 提取秒杀订单(缓存)
  TbSeckillOrder seckillOrder = seckillOrderService.searchOrderFromRedisByUserId(username);
  // 3. 调用微信支付接口
  if(seckillOrder!=null){
    return weixinPayService.createNative(seckillOrder.getId()+"",(long)(seckillOrder.getMoney().doubleValue()*100)+"");
  }else{
    return new HashMap();
  }

}

2. 前端代码

将cart-web工程的payService.js payController.js pay.html qrious.min.js拷贝到seckill_web工程 payController.js暂时注释对查询的调用

3. 支付成功保存订单

1. 后端代码

  1. 修改seckill-interface的SeckillOrderService.java,定义方法
// 保存订单到数据库
public void saveOrderFromRedisToDb(String userId,Long orderId,String transactionId);
  1. 在seckill-service的SeckillOrderServiceImpl.java实现该方法
@Override
public void saveOrderFromRedisToDb(String userId, Long orderId, String transactionId) {
  // 1. 从缓存中提取订单数据
  TbSeckillOrder seckillOrder = searchOrderFromRedisByUserId(userId);
  if(seckillOrder==null){
    throw new RuntimeException("不存在订单");
  }
  if(seckillOrder.getId().longValue()!=orderId.longValue()){
    throw new RuntimeException("订单号不符");
  }
  // 2. 修改订单实体的属性
  seckillOrder.setPayTime(new Date());// 支付日期
  seckillOrder.setStatus("1");//已支付
  seckillOrder.setTransactionId(transactionId);

  // 3. 将订单存入数据库
  seckillOrderMapper.insert(seckillOrder);
  // 4. 清除缓存
  redisTemplate.boundHashOps("seckillOrder").delete(userId);
}
  1. 修改seckill-web的PayController.java,增加查询的方法
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
  // 1. 获取当前登录用户名
  String username = SecurityContextHolder.getContext().getAuthentication().getName();
  Result result = null;
  int x = 0;
  while(true){
    Map map = weixinPayService.queryPayStatus(out_trade_no);
    if(map==null){
      result = new Result(false,"支付发生错误");
      break;
    }
    if("SUCCESS".equals(map.get("trade_state"))){
      result = new Result(true,"支付成功");
      // 修改订单状态
      seckillOrderService.saveOrderFromRedisToDb(username,Long.valueOf(out_trade_no), (String) map.get("transaction_id"));
      break;
    }
    ...

2. 前端代码

调用查询的方法,参见cart-web工程

queryPayStatus();//调用查询

4. 订单超时处理

当用户下单后5分钟尚未付款应该释放订单,增加库存

1. 删除缓存中的订单

  1. 修改seckill-interface的SeckillOrderService.java
// 删除缓存中的订单
public void deleteOrderFromRedis(String userId,Long orderId);
  1. 修改seckill-service的SeckillOrderServiceImpl.java
@Override
public void deleteOrderFromRedis(String userId, Long orderId) {
  // 1. 查询出缓存中的订单
  TbSeckillOrder seckillOrder = searchOrderFromRedisByUserId(userId);
  if(seckillOrder!=null){
    // 删除缓存中的订单
    redisTemplate.boundHashOps("seckillOrder").delete(userId);

    // 2. 库存回退
    TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(seckillOrder.getSeckillId());
    if(seckillGoods!=null){
      seckillGoods.setStockCount(seckillGoods.getStockCount()+1);
      redisTemplate.boundHashOps("seckillGoods").put(seckillOrder.getSeckillId(),seckillGoods);
    }else{
      seckillGoods = new TbSeckillGoods();
      seckillGoods.setId(seckillOrder.getSeckillId());
      seckillGoods.setStockCount(1);// 数量为1
      // TODO set其他属性设置
      redisTemplate.boundHashOps("seckillGoods").put(seckillOrder.getSeckillId(),seckillGoods);
    }
    System.out.println("订单取消:"+orderId);
  }
}

2. 关闭微信订单

  1. 修改pay-interface的WeixinPayService接口
// 关闭订单
public Map closePay(String out_trade_no);
  1. 修改pay-service的WeixinPayServiceImpl
@Override
public Map closePay(String out_trade_no) {
  // 1. 封装参数
  Map param = new HashMap();
  param.put("appid",appid);//公众账号ID
  param.put("mch_id",partner);//商户号
  param.put("out_trade_no",out_trade_no);//商户订单号
  param.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符串
  try {
    String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
    // 2. 发送请求

    HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/closeorder");
    client.setHttps(true);
    client.setXmlParam(xmlParam);
    client.post();

    // 3. 获取结果
    String xmlResult = client.getContent();
    Map<String, String> mapResult = WXPayUtil.xmlToMap(xmlResult);
    System.out.println("调用查询API返回结果:"+mapResult);
    return mapResult;
  } catch (Exception e) {
    e.printStackTrace();
    return null;
  }
}

3. 超时调用服务

修改seckill-web的PayController.java

if(x>=100){
  result = new Result(false,"二维码超时");

  // 关闭支付
  Map<String,String> payResult = weixinPayService.closePay(out_trade_no);
  if(payResult!=null && "FAIL".equals(payResult.get("return_code"))){
    if("ORDERPAID".equals(payResult.get("err_code"))){
      result = new Result(true,"支付成功");
      // 修改订单状态
      seckillOrderService.saveOrderFromRedisToDb(username,Long.valueOf(out_trade_no), (String) map.get("transaction_id"));
      break;
    }
  }
  // 删除订单
  if(result.isSuccess()==false){
    seckillOrderService.deleteOrderFromRedis(username,Long.valueOf(out_trade_no));
  }
  break;
}

3. 前端代码

seckill-web的payController.js

// 查询支付状态
queryPayStatus = function () {
  payService.queryPayStatus($scope.out_trade_no).success(
    function (response) {
      if(response.success){
        location.href = "paysuccess.html#?money="+$scope.money;
      }else{
        if(response.message==="二维码超时"){
          location.href="payTimeOut.html";
        }else{
          location.href = "payfail.html";
        }
      }
    }
  );
}

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/90723794