亿级高并发电商项目-- 实战篇 --万达商城项目 十三(编写购物车、优化修改商品、下架商品方法、购物车模块监听修改商品、删除商品消息)

时间:2021-04-16 01:05:19


亿级高并发电商项目-- 实战篇 --万达商城项目 十三(编写购物车、优化修改商品、下架商品方法、购物车模块监听修改商品、删除商品消息)

 

亿级高并发电商项目-- 实战篇 --万达商城项目 十三(编写购物车、优化修改商品、下架商品方法、购物车模块监听修改商品、删除商品消息)

 

????作者简介:大家好,我是小童,Java开发工程师,博客博主,Java领域新星创作者
????系列专栏:前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶
????如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步????
????如果感觉博主的文章还不错的话,请????三连支持????一下博主哦
????博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人 

 专栏:高并发项目 

编写购物车服务接口

用户购买商品的流程为: 搜索商品 > 查看商品详情 > 添加到购物车 > 生成商品订单 > 支 付 ,搜索商品和查看商品详情功能已经完成,接下来我们编写购物 车服务。购物车数据属于临时数据,为了节约数据库开销,我们会 将其存放到redis中。

在通用模块编写购物车服务接口:

// 购物车服务
public interface CartService {
    // 新增商品到购物车
    void addCart(Long userId, CartGoods cartGoods);
    // 修改购物车商品数量
    void handleCart(Long userId, Long goodId, Integer num);
    // 删除购物车商品
    void deleteCartOption(Long userId, Long goodId);
    // 获取用户购物车
    List<CartGoods> findCartList(Long userId);
    // 更新redis中的商品数据,在管理员更新商品后执行
    void refreshCartGoods(CartGoods cartGoods);
    // 删除redis中的商品数据,在管理员下架商品后执行
    void deleteCartGoods(CartGoods cartGoods);
}

创建购物车服务模块

1、创建名为 shopping_cart_service 的SpringBoot工程,添加相关依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.itbaizhan</groupId>
        <artifactId>shopping_common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.8</version>
    </dependency>
  <!-- 操作zookeeper -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.2.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

 2、设置该工程的父工程为 shopping 。

<parent>
    <groupId>com.ittxc</groupId>
    <artifactId>shopping</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>

3、给 shopping 工程设置子模块

<!-- 子模块 -->
<modules>
    <!-- 购物车服务 -->
    <module>shopping_cart_service</module>
</modules>

4、编写配置文件 application.yml

# 端口号
server:
 port: 9009
# 日志格式
logging:
 pattern:
   console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
spring:
  # redis
 redis:
   host: 192.168.0.159
   port: 6379
   timeout: 30000
   jedis:
     pool:
       max-active: 8
       max-wait: -1
       max-idle: 8
       min-idle: 0
dubbo:
 application:
   name: shopping_cart_service # 项目名
 registry:
   address: zookeeper://192.168.0.159 #注册中心地址
   port: 2181       # 注册中心的端口
   timeout: 10000 # 注册到zk上超时时间,ms
 protocol:
   name: dubbo # dubbo使用的协议
   port: -1 # dubbo自动分配端口
 scan:
   base-packages: com.itbaizhan.shopping_cart_service.service # 包扫描

5、启动类忽略数据源自动配置

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class ShoppingCartServiceApplication {
    public static void main(String[] args)
{
      SpringApplication.run(ShoppingCartServiceApplication.class, args);
   }
}

创建购物车Api模块

1、创建名为 shopping_cart_customer_api 的SpringBoot工程,添加相关依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.8</version>
    </dependency>
    <!-- 操作zookeeper -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.2.0</version>
        </dependency>
    <dependency>
        <groupId>com.itbaizhan</groupId>
        <artifactId>shopping_common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-startertest</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

 2、设置该工程的父工程为 shopping 。

<parent>
    <groupId>com.ittxc</groupId>
    <artifactId>shopping</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>

3、给 shopping 工程设置子模块

<!-- 子模块 -->
<modules>
    <!-- 购物车api -->
  <module>shopping_cart_customer_api</module>
</modules>

4、编写配置文件 application.yml

# 端口号
server:
 port: 8005
# 日志格式
logging:
 pattern:
   console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
dubbo:
 application:
   name: shopping_cart_customer_api # 项目名
 registry:
   address: zookeeper://192.168.0.159 #注册中心地址
   port: 2181       # 注册中心的端口
   timeout: 10000 # 注册到zk上超时时间,ms
 protocol:
   name: dubbo # dubbo使用的协议
   port: -1 # dubbo自动分配端口

5、启动类忽略数据源自动配置

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class ShoppingUserCustomerApiApplication {
    public static void main(String[] args)
{
      SpringApplication.run(ShoppingUserCustomerApiApplication.class, args);
   }
}

编写查询用户购物车功能

我们将用户的购物车信息保存到redis当中,所有用户的购物车作为 一个hash类型的数据保存,hash的键是用户id,hash的值是购物车商品列表。

亿级高并发电商项目-- 实战篇 --万达商城项目 十三(编写购物车、优化修改商品、下架商品方法、购物车模块监听修改商品、删除商品消息)

 在购物车服务模块创建购物车服务接口的实现类,重写查询用户购物车功能

@DubboService
public class CartServiceImpl implements CartService {
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public List<CartGoods> findCartList(Long userId) {
        Object cartList = redisTemplate.boundHashOps("cartList").get(userId);
        if (cartList == null) {
            return new ArrayList<CartGoods>();
       } else {
            return (List<CartGoods>)cartList;
       }
   }
}

 编写添加商品到购物车方法

@Override
public void addCart(Long userId, CartGoods cartGoods) {
    // 1.根据用户id获取用户购物车列表
    List<CartGoods> cartList = findCartList(userId);
    // 2.查询购物车是否有该商品,如果有商品,添加商品数量
    for (CartGoods cartGoods1 : cartList) {
        if(cartGoods.getGoodId().equals(cartGoods1.getGoodId())){
            int newNum = cartGoods1.getNum() + cartGoods.getNum();
            cartGoods1.setNum(newNum);
            redisTemplate.boundHashOps("cartList").put(userId,cartList);
            return;
       }
   }
    // 3.如果购物车没有该商品,将商品添加到购物车列表
    cartList.add(cartGoods);
    redisTemplate.boundHashOps("cartList").put(userId,cartList);
}

编写修改购物车商品数量方法

@Override
public void handleCart(Long userId, Long goodId, Integer num) {
    // 获取用户购物车列表
    List<CartGoods> cartList = findCartList(userId);
    // 遍历列表找到对应商品
    for (CartGoods cartGoods : cartList) {
        if(goodId.equals(cartGoods.getGoodId())) {
            // 改变商品数量
            cartGoods.setNum(num);
            break;
       }
   }
    // 将新的购物车列表保存到redis中
   redisTemplate.boundHashOps("cartList").put(userId, cartList);
}

编写删除购物车商品方法

@Override
public void deleteCartOption(Long userId,Long goodId) {
    // 获取用户购物车列表
    List<CartGoods> cartList = findCartList(userId);
    // 将商品移出列表
    for (CartGoods cartGoods : cartList) {
        if (goodId.equals(cartGoods.getGoodId())) {
            cartList.remove(cartGoods);
            break;
       }
   }
    // 将新的购物车列表保存到redis中
    redisTemplate.boundHashOps("cartList").put(userId, cartList);
}

编写购物车控制器

在进行购物车操作之前,要先获取用户的Id,而用户的令牌中只保存了用户名,所以我们要修改JWT工具类,让令牌中也保存用户的 Id:

1、修改JWT工具类

public class JWTUtil {
    //token过期时间,一天
    private static final Long EXPIRE_DATE = 1000*60*60*24L;
    // 秘钥
    private static final String SECRET = "xiaotong";
    // 签发者
    private static final String ISSUER = "XIAOTONG";
    /**
     * 签名生成
     * @param shoppingUser
     * @return
     */
    public static String sign(ShoppingUser shoppingUser){
        String token = JWT.create()
        .withIssuer(ISSUER) // 签发者
               .withIssuedAt(new Date())// 签发时间
               .withExpiresAt(new Date(new Date().getTime() + EXPIRE_DATE))// 过期时间
               .withSubject(shoppingUser.getUsername())// 保存用户名
               .withClaim("userId",shoppingUser.getId())// 保存用户id
               .sign(Algorithm.HMAC256(SECRET)); // 秘钥
                return token;
   }
    /**
     * 签名解析
     * @param token 签名字符串
     * @return 解析得出的用户名
     */
    public static String verify(String token){
        try {
            String username = JWT
                   .require(Algorithm.HMAC256(SECRET))
                   .withIssuer(ISSUER)
                   .build()
                   .verify(token)
                   .getSubject();
            return username;
       } catch (Exception e){
            throw new BusException(CodeEnum.VERIFY_TOKEN_ERROR);
       }
   }
    /**
     * 签名解析,获取用户id
     * @param token 签名字符串
     * @return 用户id
     */
    public static Long getId(String token)
       {
        try {
            Long userId = JWT
                   .require(Algorithm.HMAC256(SECRET))
                   .withIssuer(ISSUER)
                   .build()
                   .verify(token)
                   .getClaim("userId")
                   .asLong();
            return userId;
       } catch (Exception e){
            throw new BusException(CodeEnum.VERIFY_TOKEN_ERROR);
       }
   }
}

2、在购物车Api模块编写购物车控制器

/**
* 购物车
*/
@RestController
@RequestMapping("/user/cart")
public class CartController {
    @DubboReference
    private CartService cartService;
    /**
     * 查询用户购物车
     * @param token 用户令牌
     * @return 用户购物车列表
     */
    @GetMapping("/findCartList")
    public BaseResult<List<CartGoods>> findCartList(@RequestHeader String token){
        Long userId = JWTUtil.getId(token); // 获取用户id
        List<CartGoods> cartList = cartService.findCartList(userId);
        return BaseResult.ok(cartList);
   }
    /**
     * 新增商品到购物车
     * @param cartGoods 购物车商品
     * @param token 用户令牌
     * @return 操作结果
     */
    @PostMapping("/addCart")
    public BaseResult addCart(@RequestBody CartGoods cartGoods,@RequestHeader String token){
        Long userId = JWTUtil.getId(token); // 获取用户id
        cartService.addCart(userId,cartGoods);
        return BaseResult.ok();
   }
    /**
     * 修改购物车商品数量
     * @param token 用户令牌
     * @param goodId 商品id
     * @param num 修改后的数量
     * @return 操作结果
     */
    @GetMapping("/handleCart")
    public BaseResult addCart(@RequestHeader String token,Long goodId,Integer num){
        Long userId = JWTUtil.getId(token); // 获取用户id
        cartService.handleCart(userId,goodId,num);
        return BaseResult.ok();
   }
    /**
     * 删除购物车商品
     * @param token 用户令牌
     * @param goodId 商品id
     * @return 操作结果
     */
    @DeleteMapping("/deleteCart")
    public BaseResult addCart(@RequestHeader String token,Long goodId){
        Long userId = JWTUtil.getId(token); // 获取用户id
        cartService.deleteCartOption(userId,goodId);
        return BaseResult.ok();
   }
}

编写修改所有用户购物车商品方法

当管理员修改了商品的价格等数据后,需要将数据同步到用户购物车中,所以我们要编写给所有用户的购物车中修改某件商品的方法。

@Override
public void refreshCartGoods(CartGoods
cartGoods) {
    // 获取所有用户购物车商品
    BoundHashOperations cartList = redisTemplate.boundHashOps("cartList");
    Map<Long,List<CartGoods>> allCartGoods = cartList.entries();
    Set<Map.Entry<Long, List<CartGoods>>> entries = allCartGoods.entrySet();
    // 遍历所有用户的购物车
    for (Map.Entry<Long, List<CartGoods>> entry : entries) {
        List<CartGoods> goodsList = entry.getValue();
        // 遍历一个用户购物车的所有商品
        for (CartGoods goods : goodsList) {
            // 如果该商品是被更新的商品,修改商品数据
            if(cartGoods.getGoodId().equals(goods.getGoodId())){
              goods.setGoodsName(cartGoods.getGoodsName());
              goods.setHeaderPic(cartGoods.getHeaderPic());
              goods.setPrice(cartGoods.getPrice());
           }
       }
   }
    // 将改变后所有用户购物车重新放入redis
    redisTemplate.delete("cartList");
    redisTemplate.boundHashOps("cartList").putAll(allCartGoods);
}

编写删除所有用户购物车商品方法

当管理员下架某件商品后,所有用户的购物车中该商品也应该被删除,所以我们要编写给所有用户的购物车中删除某件商品的方法。

@Override
public void deleteCartGoods(CartGoods cartGoods) {
    BoundHashOperations cartList = redisTemplate.boundHashOps("cartList");
    // 所有用户的购物车
    Map<String,List<CartGoods>> allCartGoods = cartList.entries();
    Set<Map.Entry<String, List<CartGoods>>> entries = allCartGoods.entrySet();
    // 遍历所有用户的购物车
    for (Map.Entry<String, List<CartGoods>> entry : entries) {
        List<CartGoods> goodsList = entry.getValue();
        // 遍历一个用户购物车的所有商品
        for (CartGoods goods : goodsList) {
            // 如果该商品是被删除的商品
            if (cartGoods.getGoodId().equals(goods.getGoodId())){
                goodsList.remove(goods);
                break;
           }
       }
   }
    // 将改变后的map重新放入redis
    redisTemplate.delete("cartList");
    redisTemplate.boundHashOps("cartList").putAll(allCartGoods);
}

 优化修改商品、下架商品方法

接下来我们修改商品服务模块代码,在修改商品后发送消息同步修 改购物车数据;在下架商品后发送消息同步删除购物车数据:

1、在MQ配置文件创建队列,绑定队列到交换机:

@Configuration
public class RabbitConfig {
    // 交换机
    private final String GOODS_EXCHANGE = "goods_exchange";
    // 同步商品数据队列
    private final String SYNC_GOODS_QUEUE = "sync_goods_queue";
    // 删除商品数据队列
    private final String DEL_GOODS_QUEUE = "del_goods_queue";
    // 向购物车同步商品队列
    private final String SYNC_CART_QUEUE = "sync_cart_queue";
    // 向购物车删除商品队列
    private final String DEL_CART_QUEUE = "del_cart_queue";
    // 创建交换机
    @Bean(GOODS_EXCHANGE)
    public Exchange getExchange() {
        return ExchangeBuilder
               .topicExchange(GOODS_EXCHANGE) // 交换机类型
               .durable(true) // 是否持久化
               .build();
   }
    // 创建队列
    @Bean(SYNC_GOODS_QUEUE)
    public Queue getQueue1() {
        return new Queue(SYNC_GOODS_QUEUE); // 队列名
   }
    @Bean(DEL_GOODS_QUEUE)
    public Queue getQueue2() {
        return new Queue(DEL_GOODS_QUEUE);
       // 队列名
   }
    @Bean(SYNC_CART_QUEUE)
    public Queue getQueue3() {
        return new Queue(SYNC_CART_QUEUE);
     // 队列名
   }
 @Bean(DEL_CART_QUEUE)
    public Queue getQueue4() {
        return new Queue(DEL_CART_QUEUE);
// 队列名
   }
    // 交换机绑定队列
    @Bean
    public Binding bindQueue1(@Qualifier(GOODS_EXCHANGE) Exchange exchange,
                                  @Qualifier(SYNC_GOODS_QUEUE) Queue queue)
   {
        return BindingBuilder
               .bind(queue)
               .to(exchange)
               .with("#.sync_goods.#")
               .noargs();
   }
    @Bean
    public Binding bindQueue2(@Qualifier(GOODS_EXCHANGE) Exchange exchange,
                                  @Qualifier(DEL_GOODS_QUEUE) Queue queue) {
        return BindingBuilder
               .bind(queue)
               .to(exchange)
               .with("#.del_goods.#")
               .noargs();
   }
    @Bean
    public Binding
bindQueue3(@Qualifier(GOODS_EXCHANGE)
Exchange exchange,
                            
@Qualifier(SYNC_CART_QUEUE) Queue queue)
{
        return BindingBuilder
               .bind(queue)
               .to(exchange)
               .with("#.sync_cart.#")
               .noargs();
   }
    @Bean
    public Binding bindQueue4(@Qualifier(GOODS_EXCHANGE) Exchange exchange,
                            @Qualifier(DEL_CART_QUEUE) Queue queue) {
               return BindingBuilder
               .bind(queue)
               .to(exchange)
               .with("#.del_cart.#")
               .noargs();
   }
}
@DubboService
public class GoodsServiceImpl implements
GoodsService {
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private GoodsImageMapper goodsImageMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Override
    public void update(Goods goods) {
        // 删除旧图片数据
        Long goodsId = goods.getId(); //商品id
        QueryWrapper<GoodsImage> queryWrapper = new QueryWrapper();
        queryWrapper.eq("goodsId",goodsId);
        goodsImageMapper.delete(queryWrapper);
        // 删除旧规格项数据
        goodsMapper.deleteGoodsSpecificationOption(goodsId);
        // 插入商品数据
        goodsMapper.updateById(goods);
        // 插入图片数据
        List<GoodsImage> images = goods.getImages(); // 商品图片
        for (GoodsImage image : images) {
            image.setGoodsId(goodsId); // 给图片设置商品id
            goodsImageMapper.insert(image); // 插入图片
       }
        // 插入商品_规格项数据
        List<Specification> specifications = goods.getSpecifications(); // 获取规格
        List<SpecificationOption> options = new ArrayList(); // 规格项集合
        // 遍历规格,获取规格中的所有规格项
        for (Specification specification : specifications) {
            options.addAll(specification.getSpecificationOptions());
       }
        // 遍历规格项,插入商品_规格项数据
        for (SpecificationOption option : options) {
           goodsMapper.addGoodsSpecificationOption(goodsId,option.getId());
       }
      rabbitTemplate.convertAndSend("goods_exchange","sync_goods",findById(goodsId));
        // 将商品修改数据同步到用户购物车
          CartGoods cartGoods = new CartGoods();
         cartGoods.setGoodId(goods.getId());
         cartGoods.setGoodsName(goods.getGoodsName());
         cartGoods.setHeaderPic(goods.getHeaderPic());
         cartGoods.setPrice(goods.getPrice());
         rabbitTemplate.convertAndSend("goods_exchange","sync_cart",cartGoods);
   }
    @Override
    public void putAway(Long id, Boolean isMarketable) {
      goodsMapper.putAway(id,isMarketable);
        if (isMarketable){
            // 上架时同步到ES
          rabbitTemplate.convertAndSend("goods_exchange","sync_goods",findById(id));
       }else{
            // 下架删除ES数据
            rabbitTemplate.convertAndSend("goods_exchange","del_goods",id);
            // 商品下架删除用户购物车
            CartGoods cartGoods = new CartGoods();
            cartGoods.setGoodId(id);
            rabbitTemplate.convertAndSend("goods_exchange","del_cart",cartGoods);
       }
   }
}

购物车模块监听修改商品、删除商品消息

1、在购物车服务模块添加RabbitMQ依赖

<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、添加RabbitMQ配置

spring:
  # redis
 redis:
   host: 192.168.0.159
   port: 6379
   timeout: 30000
   jedis:
     pool:
       max-active: 8
       max-wait: -1
       max-idle: 8
       min-idle: 0
  # rabbitmq
 rabbitmq:
   host: 192.168.0.159
   port: 5672
   username: guest
   password: guest
   virtual-host: /

3、修改购物车服务接口实现类,监听队列 

@DubboService
@Service
public class CartServiceImpl implements CartService {
    @Autowired
    private RedisTemplate redisTemplate;
    // 监听修改购物车商品队列
    @RabbitListener(queues = "sync_cart_queue")
    public void listenSyncQueue(CartGoods cartGoods){
        refreshCartGoods(cartGoods);
   }
    // 监听删除购物车商品队列
    @RabbitListener(queues = "del_cart_queue")
    public void listenDelQueue(CartGoods cartGoods){
        deleteCartGoods(cartGoods);
   }
}

4、测试管理员修改商品、下架商品,看用户购物车是否发生改变。