秒杀商品展示及商品秒杀

目录

一、登录方式调整

二、秒杀商品展示&秒杀商品详情&商品秒杀功能

1.生成秒杀订单

2.绑定秒杀商品 

3.查看看秒商品 

4.订单秒杀

① 移除seata相关

② 生成秒杀订单

③ 前端页面秒杀测试 


一、登录方式调整

第1步:从zmall-common的pom.xml中移除spring-session-data-redis依赖

(注意:由于我的spring session没有了,用户以及商品和订单可能会报错注释掉就好了) 

注意:

1)本章节中不采用spring-session方式,改用redis直接存储用户登录信息,主要是为了方便之后的jmeter压测;

2)这里只注释调用spring-session的依赖,保留redis的依赖;  

springboot 集成redis 只要yml中添加redis相关配置,那么Spring容器就会初始化一个redisConnection对象

第2步:在zmall-common公共模块中定义ReidsConfig配置类

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> restTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
        //String类型Key序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //String类型Value序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //Hash类型Key序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //Hash类型Value序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

这里一定要注意,最后在将RedisConnectionFactory设置到RedisTemplate中,不要在最前做该步操作,不然会导致String和Hash类型的序列化无效,将采用默认的JdkSerializationRedisSerializer进行序列化,从而导致保存的key前缀出现乱码问题。细心!!!细心!!!细心!!!o(╥﹏╥)o

参考地址:RedisTemplate写入Redis数据出现无意义乱码前缀\xac\xed\x00\x05_hunger_wang的博客-CSDN博客_\xac

第3步:在zmall-common公共模块中配置redis相关服务,建一个包叫service

IRedisService

public interface IRedisService {

    /**
     * 将登陆用户对象保存到Redis中,并以token来命名
     * @param token
     * @param user
     */
    void setUserToRedis(String token, User user);

    /**
     * 根据token令牌从Redis中获取User对象
     * @param token
     * @return
     */
    User getUserByToken(String token);
}

RedisServcieImple  

@Service
public class RedisServiceImpl implements IRedisService {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    public void setUserToRedis(String token, User user) {
        String key="user:"+token;
        redisTemplate.boundValueOps(key).set(user,7200,TimeUnit.SECONDS);
    }

    @Override
    public User getUserByToken(String token) {
        return (User) redisTemplate.opsForValue().get("user:"+token);
    }
}

 用户登录成功后,将用户对象保存到Redis中,并设置超时时间7200秒。

第4步:在zmall-common公共模块中配置,配置类里自定义参数解析UserArgumentResolver、WebConfig

UserArgumentResolver

/**
 * 自定义用户参数类
 */
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private IRedisService redisService;

    /**
     * 只有supportsParameter方法执行返回true,才能执行下面的resolveArgument方法
     * @param methodParameter
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> type = methodParameter.getParameterType();
        return type== User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter,
                                  ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest,
                                  WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest req= (HttpServletRequest) nativeWebRequest.getNativeRequest();
        //从cookie获取token令牌
        String token = CookieUtils.getCookieValue(req, "token");
        //判断cookie中的token令牌是否为空
        if(StringUtils.isEmpty(token))
            throw new BusinessException(JsonResponseStatus.TOKEN_ERROR);
        //根据token令牌获取redis中存储的user对象,方便jmeter测试
        User user = redisService.getUserByToken(token);
        if(null==user)
            throw new BusinessException(JsonResponseStatus.TOKEN_ERROR);
        return user;
    }
}

WebConfig

@Component
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //添加静态资源访问映射
        //registry.addResourceHandler("/static/**")
        //        .addResourceLocations("classpath:/static/");
    }
}

第5步:用户登录业务调整,将spring-session方式更改为redis方式存储登录用户信息。

UserServiceimpl


@Autowired
    private RedisServiceImpl redisService;
...

//5.通过UUID生成token令牌并保存到cookie中
String token= UUID.randomUUID().toString().replace("-","");
//将随机生成的Token令牌保存到Cookie中,并设置1800秒超时时间
CookieUtils.setCookie(req,resp,"token",token,7200);
//6.将token令牌与spring session进行绑定并存入redis中
//HttpSession session = req.getSession();
//session.setAttribute(token,us);
//将token令牌与user绑定后存储到redis中,方便jmeter测试
redisService.setUserToRedis(token,us);

这里采用Redis方式直接存储登录用户信息,只为后续使用Jmeter压测时提供便利。正常运行使用项目还是可以使用spring-session方式。

 第6步:修改商品服务zmall-product模块中的index方法,将之前从HttpSession中获取登录用户信息改换成User对象参数方式

 @RequestMapping("/index.html")
    public String index(Model model, User user){
        System.out.println(user);
//        按照商品的销量降序排序获取销量排名Top5的商品
        List<Product> products = productService.list(new QueryWrapper<Product>()
                .orderByDesc("hot")
                .last("limit 5"));
        model.addAttribute("hots",products);
        return "index";
    }

在调用index方法之前,先由自定义的参数解析器进行参数解析并返回解析结果User,所以在这里可直接在方法参数中获取的User对象。  

在启动项目之前要先启动我们的nacos和nginx在启动我们的项目 

第7步:重启zmall-user和zmall-product模块,http://zmall.com/user-serv/login.html完成用户登录后,直接在浏览器地址栏输入:http://zmall.com/product-serv/index.html,查看zmall-product模块中的控制台是否已经获取到登录用户对象信息。  

二、秒杀商品展示&秒杀商品详情&商品秒杀功能

1.生成秒杀订单

我们用的表zmall-kill,由于我们之前的表没有数据,所以我们要手动添加数据如下图所示:

/*
 Navicat Premium Data Transfer

 Source Server         : 本地连接
 Source Server Type    : MySQL
 Source Server Version : 80013
 Source Host           : localhost:3306
 Source Schema         : zmall

 Target Server Type    : MySQL
 Target Server Version : 80013
 File Encoding         : 65001

 Date: 10/02/2023 10:09:28
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for zmall_kill
-- ----------------------------
DROP TABLE IF EXISTS `zmall_kill`;
CREATE TABLE `zmall_kill`  (
  `id` int(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `item_id` int(11) NULL DEFAULT NULL COMMENT '商品id',
  `total` int(11) NULL DEFAULT NULL COMMENT '可被秒杀的总数',
  `start_time` datetime(0) NULL DEFAULT NULL COMMENT '秒杀开始时间',
  `end_time` datetime(0) NULL DEFAULT NULL COMMENT '秒杀结束时间',
  `is_active` tinyint(11) NULL DEFAULT 1 COMMENT '是否有效(1=是;0=否)',
  `create_time` timestamp(0) NULL DEFAULT NULL COMMENT '创建的时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '待秒杀商品表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of zmall_kill
-- ----------------------------
INSERT INTO `zmall_kill` VALUES (1, 733, 10, '2023-02-09 00:04:56', '2024-02-10 00:05:01', 1, '2023-02-10 00:05:13');
INSERT INTO `zmall_kill` VALUES (2, 734, 9, '2023-02-09 00:04:56', '2024-02-10 00:05:01', 1, '2023-02-10 00:05:59');
INSERT INTO `zmall_kill` VALUES (3, 735, 9, '2023-02-09 00:04:56', '2024-02-10 00:05:01', 1, '2023-02-10 00:06:16');
INSERT INTO `zmall_kill` VALUES (4, 736, 10, '2023-02-09 00:04:56', '2024-02-10 00:05:01', 1, '2023-02-10 00:06:34');
INSERT INTO `zmall_kill` VALUES (5, 737, 10, '2023-02-09 00:04:56', '2024-02-10 00:05:01', 0, '2023-02-10 00:06:53');
INSERT INTO `zmall_kill` VALUES (6, 738, 10, '2023-02-09 00:04:56', '2024-02-10 00:05:01', 1, '2023-02-10 00:07:10');

SET FOREIGN_KEY_CHECKS = 1;

2.绑定秒杀商品 

添加sellDetail.html页面到zmall-product模块中;实现首页秒杀商品展示,必须保证秒杀商品状态为已激活、且秒杀商品的活动时间为有效时间范围之内。

  • 将资料目录中的《易买网网页素材.rar》,将其中SellDetails.html添加到zmall-product项目的templates下,最好请将SellDetails.html页面首字母改成小写

  • 将页面中的头部申明<!DOCTYPE html ....>修改成<!DOCTYPE html>(支持H5风格)

  • 在页面中通过<#include>指令引入common目录中的head.html

index.html 找到我们显示秒杀的地方修改为如下代码。

<#if kills??>
     <#list kills as g>
         <div class="sell_${g_index?if_exists+1}">
             <div class="sb_img"><a href="${ctx}/sellDetail.html?pid=${g.item_id}"><img src="${g.fileName}" width="242" height="356" /></a></div>
             <div class="s_price">¥<span>${g.price}</span></div>
             <div class="s_name">
             <h2><a href="${ctx}/sellDetail.html?pid=${g.item_id}">${g.name}</a></h2>
             倒计时:<span>1200</span> 时 <span>30</span> 分 <span>28</span> 秒
             </div>
         </div>
     </#list>
 </#if>

 把我们生成的mapper复制到公共模块中,如下图所示:可以把生成中的mapper删除掉了。 

ProductMapper.xml

<select id="queryKillProdNews" resultType="java.util.Map">
        select
        k.id,k.item_id,p.name,p.price,p.fileName
        from
        zmall_kill k,zmall_product p
        where k.item_id=p.id and
        k.is_active=1 and
        (now() between start_time and end_time)
        order by k.create_time desc
        limit 4
    </select>
    <select id="queryKillProdById" resultType="java.util.Map">
        select
        k.id,k.item_id,k.total,p.name,p.price,p.fileName
        from
        zmall_kill k,zmall_product p
        where k.item_id=p.id and k.is_active=1 and item_id=#{value}
    </select>

mapper层

@Repository
public interface ProductMapper extends BaseMapper<Product> {
//    @MapKey("queryKillProdNews")
    List<Map<String,Object>> queryKillProdNews();

    Map<String,Object> queryKillProdById(Integer pid);
}

service层

public interface IProductService extends IService<Product> {
    void updateStock(Integer pid,Integer num);

    /**
     * 首页显示秒杀商品查询
     * @return
     */
    List<Map<String,Object>> queryKillProdNews();

    /**
     * 根据商品ID查询秒杀商品信息
     * @param pid 秒杀商品ID
     * @return
     */
    Map<String,Object> queryKillProdById(Integer pid);
}

ProductServiceImpl实现层

 @Autowired
    private ProductMapper productMapper;

@Override
    public List<Map<String, Object>> queryKillProdNews() {
        return productMapper.queryKillProdNews();
    }

    @Override
    public Map<String, Object> queryKillProdById(Integer pid) {
        return productMapper.queryKillProdById(pid);
    }

controller层

方法一:
 @RequestMapping("/index.html")
    public String index(Model model, User user){
        System.out.println(user);
//        按照商品的销量降序排序获取销量排名Top5的商品
        List<Product> products = productService.list(new QueryWrapper<Product>()
                .orderByDesc("hot")
                .last("limit 5"));
        model.addAttribute("hots",products);

        //获取显示秒杀商品
        List<Map<String, Object>> maps = productService.queryKillProdNews();
        model.addAttribute("kills",maps);
        return "index";
    }

方法二:
@RequestMapping("/index.html")
    public ModelAndView toIndex(User user){
        System.out.println("user:"+ JSON.toJSONString(user));
        ModelAndView mv=new ModelAndView();
        //获取热卖商品列表
        List<Product> hot = productService.list(new QueryWrapper<Product>()
                .orderByDesc("hot")
                .last("limit 4"));

        //获取显示秒杀商品
        List<Map<String, Object>> maps = productService.queryKillProdNews();

        mv.addObject("kills",maps);
        mv.addObject("hots",hot);
        mv.setViewName("index");


        return mv;
    }

我们已经绑定秒杀商品成功了,全是从数据库里查询出来的。

没修改之前 

修改之后 

3.查看看秒商品 

点击限时秒杀中的秒杀商品,根据秒杀商品ID查询秒杀商品详情信息并跳转到sellDetail.html页面展示秒杀商品信息。

ProductController

@RequestMapping("/sellDetail.html")
    public String sellDetail(@RequestParam Integer pid, Model model){
        //根据商品ID查询秒杀商品信息
        Map<String, Object> prod = productService.queryKillProdById(pid);
        model.addAttribute("prod",prod);
        return "sellDetails";
    }

sellDetail.html

<table border="0" style="width:100%; margin-bottom:50px;" cellspacing="0" cellpadding="0">
                  <tr valign="top">
                    <td width="315">
                    	<div class="lim_name">${(prod.name)!}</div>
                        <div class="lim_price">
                        	<span class="ch_txt">¥${(prod.price)!}</span>
                       		<a href="javascript:void(0);" class="ch_a" pid="${(prod.item_id)!}" price="${(prod.price)!}">抢购</a>
                        </div>
                        <div class="lim_c">
                        	<table border="0" style="width:100%; color:#888888;" cellspacing="0" cellpadding="0">
                              <tr>
                                <td width="35%">市场价 </td>
                                <td width="65%">折扣</td>
                              </tr>
                              <tr style="font-family:'Microsoft YaHei';">
                                <td style="text-decoration:line-through;">¥${(prod.price)!}</td>
                                <td>8.0</td>
                              </tr>
                            </table>
                        </div>
                        <div class="lim_c">
                        	<div class="des_choice">
                                <span class="fl">型号:</span>
                                <ul>
                                    <li class="checked">30ml<div class="ch_img"></div></li>
                                    <li>50ml<div class="ch_img"></div></li>
                                    <li>100ml<div class="ch_img"></div></li>
                                </ul>
                            </div>
                            <div class="des_choice">
                                <span class="fl">颜色:</span>
                                <ul>
                                    <li>红色<div class="ch_img"></div></li>
                                    <li class="checked">白色<div class="ch_img"></div></li>
                                    <li>黑色<div class="ch_img"></div></li>
                                </ul>
                            </div>
                        </div>
                        <div class="lim_c">
                        	<span class="fl">数量:</span><input type="text" value="${(prod.total)!}" class="lim_ipt" />
                        </div>
                        <div class="lim_clock">
                        	距离团购结束还有<br />
                            <span>1200 时 30 分 28 秒</span>
                        </div>
                    </td>
                    <td width="525" align="center" style="border-left:1px solid #eaeaea;"><img src="${(prod.fileName)!}" width="460" height="460" /></td>
                  </tr>
                </table>

4.订单秒杀

① 移除seata相关

第1步:先注释掉zmall-order和zmall-product模块中的seata依赖

第2步:分别删掉zmall-order和zmall-product模块中resources目录下的bootstrap.xml和register.conf文件

seata分布式事务,进行jmeter压测秒杀订单接口效率太低(1000个并发请求,吞吐量为4.5/s)o(╥﹏╥)o  

② 生成秒杀订单

将SnowFlake雪花ID生成工具类导入到zmall-common模块中utils,然后再生成秒杀订单时使用雪花ID来充当秒杀订单编号;在zmall-order模块中完成秒杀订单生成工作。

IOrderService

public interface IOrderService extends IService<Order> {
    Order createOrder(Integer pid,Integer num);

    /**
     * 生成秒杀订单
     * @param user 登陆用户对象
     * @param pid  秒杀商品ID
     * @param price 秒杀商品价格
     * @return
     */
    JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price);
}

 OrderServiceImpl

@Autowired
private KillServiceImpl killService;
@Autowired
private OrderDetailServiceImpl orderDetailService;

@Transactional
    @Override
    public JsonResponseBody<?> createKillOrder(User user, Integer pid, Float price) {
        //1.根据秒杀商品编号获取秒杀商品库存是否为空
        Kill kill = killService.getOne(new QueryWrapper<Kill>().eq("item_id",pid));
        if(kill.getTotal()<1)
            throw new BusinessException(JsonResponseStatus.STOCK_EMPTY);
        //2.秒杀商品库存减一
        killService.update(new UpdateWrapper<Kill>()
                .eq("item_id",pid)
                .setSql("total=total-1"));
        //3.生成秒杀订单及订单项
        SnowFlake snowFlake=new SnowFlake(2,3);
        Long orderId=snowFlake.nextId();
        int orderIdInt = new Long(orderId).intValue();
        //创建订单
        Order order=new Order();
        order.setUserId(user.getId());
        order.setLoginName(user.getLoginName());
        order.setCost(price);
        order.setSerialNumber(orderIdInt+"");
        this.save(order);
        //创建订单项
        OrderDetail orderDetail=new OrderDetail();
        orderDetail.setOrderId(orderIdInt);
        orderDetail.setProductId(pid);
        orderDetail.setQuantity(1);
        orderDetail.setCost(price);
        orderDetailService.save(orderDetail);
        return new JsonResponseBody();
    }

OrderController

@RequestMapping("/createKillOrder/{pid}/{price}")
    @ResponseBody
    public JsonResponseBody<?> createKillOrder(User user,
                                               @PathVariable("pid") Integer pid,
                                               @PathVariable("price") Float price){
        return orderService.createKillOrder(user,pid,price);
    }

把我们的启动类上的数据源删除

package com.zking.zmall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@MapperScan({"com.zking.zmall.mapper"})
@SpringBootApplication
public class ZmallOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZmallOrderApplication.class, args);
    }

}

③ 前端页面秒杀测试 

在sellDetail.html页面中添加订单秒杀JS方法

<script>
    $(function(){
        $('.ch_a').click(function(){
            let pid=$(this).attr('pid');
            let price=$(this).attr('price');
            console.log(pid);
            console.log(price);
            $.post('http://zmall.com/order-serv/createKillOrder',{pid:pid,price:price},function(rs){
                console.log(rs);
                if(rs.code===200)
                    alert('秒杀成功');
                else
                    alert(rs.msg);
            },'json');
        });
    });
</script>

 启动类

这里虽然已经能正常展示秒杀效果,但是还是存在很多问题,比如:重复抢购问题等等问题。  

注意:当我们是以下代码的时候

$.post('http://user.zmall.com/userLogin',{
                loginName:loginName,
                password:password
            },function(rs){
                console.log(rs);
                if(rs.code===200){
                    location.href='http://product.zmall.com/index.html';
                }else{
                    alert(rs.msg);
                }
            },'json');

post方式不能垮二级域名发送亲戚,location.href可以跨二级域名发送请求;

$(function(){
       $('.ch_a').click(function(){
            let pid=$(this).attr("pid");
            let price=$(this).attr("price");
            console.log("pid=%s,price=%s",pid,price);
           $.post('http://zmall.com/order-serv/createKillOrder/'+pid+'/'+price,{},function(rs){
               console.log(rs);
               if(rs.code===200)
                   alert('秒杀成功');
               else
                   alert(rs.msg);
           },'json');
       });
    });

$.post('http://zmall.com/order-serv/createKillOrder/'+pid+'/'+price,{},function(rs){});能够正常访问;

$.post('http://order.zmall.com/createKillOrder/'+pid+'/'+price,{},function(rs){});则会出现跨域问题;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值