谷粒商城-分布式高级篇[商城业务-购物车]

  • Post author:
  • Post category:其他



  1. 谷粒商城-分布式基础篇【环境准备】

  2. 谷粒商城-分布式基础【业务编写】

  3. 谷粒商城-分布式高级篇【业务编写】持续更新

  4. 谷粒商城-分布式高级篇-ElasticSearch

  5. 谷粒商城-分布式高级篇-分布式锁与缓存

  6. 项目托管于gitee



一、环境搭建



1、项目创建


第一步、创建gulimall-cart服务,并进行降版本处理

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>gulimall-cart</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-cart</name>
<description>购物车</description>
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>

在这里插入图片描述

在这里插入图片描述

第二步、修改域名

vim /etc/hosts

# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
# Gulimall Host End

第三步、导入依赖

<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

因为目前不用数据库,故排除掉

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {

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

}

第四步、添加配置

server.port=40000

spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

第五步、为启动类添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {

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

}

第六步、修改网关,给购物车配置路由

- id: gulimall_cart_route
  uri: lb://gulimall-cart
  predicates:
  	- Host=cart.gulimall.cn



2、动静资源处理


  1. 将资料中购物车文件夹下的所有的静态资源复制到服务器的:

    mydata/nginx/html/static/cart

    目录下

  2. 将资料中购物车文件夹下的 两个页面复制到 gulimall-cart服务的

    templates

    目录下

  3. 替换掉网页中的所有资源申请路径

hgw@HGWdeAir cart % ll 
total 0
drwxrwxr-x@  5 hgw  staff   160B  5 29  2019 bootstrap
drwxrwxr-x@  8 hgw  staff   256B  3 22  2020 css
drwxrwxr-x@ 16 hgw  staff   512B  3 22  2020 image
drwxrwxr-x@ 74 hgw  staff   2.3K  3 22  2020 img
drwxrwxr-x@  6 hgw  staff   192B  3 22  2020 js



3、页面环境搭建


需求:实现页面的跳转

  1. 当我们在商品详情页

    item.html

    点击加入购物车之后,跳转到加入成功页

    success.html
  2. 在成功页

    success.html

    点击 购物车 进入购物车列表页

    cartList.html
  3. 在成功页

    success.html

    点击 查看商品详情 跳转到该商品的详情页
  4. 在 首页

    index.html

    中点击我的购物车也跳转到 购物车列表页

    cartList.html

gulimall-product 服务中的 Item.html

<div class="box-btns-two">
   <a href="http://cart.gulimall.cn/addToCart">
      加入购物车
   </a>
</div>

//......

<div class="nav_top_three"><a href="http://cart.gulimall.cn/cart.html">我的购物车</a><span class="glyphicon glyphicon-shopping-cart"></span>
  <div class="nav_top_three_1">
    <img src="/static/item/img/44.png"/>购物车还没有商品,赶紧选购吧!
  </div>
</div>

Gulimall-cart 服务中 success.html 页面

<div class="bg_shop">
  <a class="btn-tobback" href="http://item.gulimall.cn/3.html">查看商品详情</a>
  <a class="btn-addtocart" href="http://cart.gulimall.cn/cart.html"
     id="GotoShoppingCart"><b></b>去购物车结算</a>
</div>

Gulimall-cart 服务中 success.html 页面

<div class="one_top_left">
  <a href="http://gulimall.cn" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a>
  <a href="/static/cart#" class="one_left_link">购物车</a>
</div>

//.....

<li>
    <a href="http://gulimall.cn">首页</a>
</li>

Gulimall-cart 服务中的 CartController类中添加映射

@Controller
public class CartController {

    @GetMapping("/cart.html")
    public String cartListPage(){

        return "cartList";
    }

    /**
     * 添加商品到购物车
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart() {
        return "success";
    }
}



二、数据模型分析



2.1、购物车需求




2.1.1、需求描述:

需求描述

  • 用户可以在


    登录状态


    下将商品添加到购物车

    [

    用户在线购物车

    ]

    1. 放入数据库(购物车的读和写都多,不适合)
    2. 放入 Redis (采用Redis的持久化机制,

      采用

    登录之后,会将

    离线购物车

    的数据全部合并过来,并清空

    离线购物车

  • 用户可在


    未登录状态


    下将商品添加到购物车

    [

    用户离线临时购物车

    ]

    1. 放入 localstorage、cookie、WebSQL(因为大数据推荐,这些存储在浏览器的方式不采用)
    2. 放入 Redis(采用)

    浏览器即使关闭,下次进入,

    临时购物车

    数据都在


  • 购物功能

    • 用户可以使用购物车一起结算下单
    • 给购物车

      添加商品
    • 用户可以

      查询自己的购物车
    • 用户可以在购物车中

      修改购买商品的数量
    • 用户可以在购物车中

      删除商品
    • 在购物车中展示商品优惠信息
    • 提示购物车商品价格变化


  • 数据存储:

    • 购物车是一个读多写多的场景,因此放入数据库并不合适,但购物车又需要持久化,因此这里我们选用Redis的持久化机制存储购物车数据



2.1.2、购物车数据结构


在这里插入图片描述

Redis中 每个用户的购物车 都是由各个购物项组成,根据分析这里使用 Hash进行存储比较合适:


  • Map

    <String k1,

    Map

    <String k2,CartltemInfo>>

    • K1:用户标识
    • Map<String k2,CartltemInfo>

      • K2 :商品Id
      • CartltemInfo :购物项详情



2.2、VO编写


在这里插入图片描述

  • Cart

    需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算

    • 计算商品的总数量
    • 计算商品类型数量
    • 计算商品的总价
package com.atguigu.cart.vo;
/**
 * Data time:2022/4/10 10:41
 * StudentID:2019112118
 * Author:hgw
 * Description: 整个购物车
 *  需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
 */
public class Cart {

    /**
     *
     */
    List<CartItem> items;
    /**
     * 商品的总数量
     */
    private Integer countNum;
    /**
     * 商品类型数量
     */
    private Integer countType;
    /**
     * 商品总价
     */
    private BigDecimal totalAmount;
    /**
     * 减免价格
     */
    private BigDecimal reduce = new BigDecimal("0");

    public List<CartItem> getItems() {
        return items;
    }

    public void setItems(List<CartItem> items) {
        this.items = items;
    }

    public Integer getCountNum() {
        int count = 0;
        if (items!=null && items.size()>0) {
            for (CartItem item : items) {
                countNum += item.getCount();
            }
        }
        return count;
    }


    public Integer getCountType() {
        int count = 0;
        if (items!=null && items.size()>0) {
            for (CartItem item : items) {
                countNum += 1;
            }
        }
        return count;
    }


    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        // 1、计算购物项总价
        if (items!=null && items.size()>0) {
            for (CartItem item : items) {
                BigDecimal totalPrice = item.getTotalPrice();
                amount = amount.add(totalPrice);
            }
        }
        // 2、减去优惠总价
        BigDecimal subtract = amount.subtract(getReduce());
        return subtract;
    }


    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}
  • CartItem

    • 计算小计价格
package com.atguigu.cart.vo;
/**
 * Data time:2022/4/10 10:41
 * StudentID:2019112118
 * Author:hgw
 * Description: 购物项内容
 */
public class CartItem {
    /**
     * 商品Id
     */
    private Long skuId;
    /**
     * 商品是否被选中(默认被选中)
     */
    private Boolean check = true;
    /**
     * 商品标题
     */
    private String title;
    /**
     * 商品图片
     */
    private String image;
    /**
     * 商品套餐信息
     */
    private List<String> skuAttr;
    /**
     * 商品价格
     */
    private BigDecimal price;
    /**
     * 数量
     */
    private Integer count;
    /**
     * 小计价格
     */
    private BigDecimal totalPrice;

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public Boolean getCheck() {
        return check;
    }

    public void setCheck(Boolean check) {
        this.check = check;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public List<String> getSkuAttr() {
        return skuAttr;
    }

    public void setSkuAttr(List<String> skuAttr) {
        this.skuAttr = skuAttr;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    /**
     * 动态计算当前的总价
     * @return
     */
    public BigDecimal getTotalPrice() {
        return this.price.multiply(new BigDecimal("" + this.count));
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }
}



2.3、整合Redis


1、导入redis和SpringSession的依赖

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、编写配置

# 配置redis
spring.redis.host=124.222.223.222
spring.redis.port=6379

3、添加SpringSession配置类(自定义Session配置类)

将 gulimall-auth-server 服务中

/com/atguigu/gulimall/auth/config

路径下的GulimallSessionConfig.java配置类复制到 gulimall-cart服务的config包下:

package com.atguigu.cart.config;

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.cn");
        cookieSerializer.setCookieName("GULISESSION");

        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}



三、ThreadLocal 用户身份鉴别

需求:参照京东

  • 用户登录,访问Session中的用户信息
  • 用户未登录

    • Cookie中有 user-key,则表示有临时用户
    • Cookie中没有 user-key,则表示没有临时用户

      • 创建一个封装 并返回 user-key


ThreadLocal:同一个线程共享数据

  • 核心原理是:Map<Thread,Object> threadLocal
  • 在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。

    线程之间互不干扰

已知:

  • 一次请求进来: 拦截器 ==>> Controller ==>> Service ==>> dao 用的都是同一个线程

在这里插入图片描述



(1)用户身份鉴别方式

  • 当用户登录之后点击购物车,则进行用户登录
  • 用户未登录的时候点击购物车,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,以后每次浏览器进行访问购物车的时候都会携带user-key。user-key 是用来标识和存储临时购物车数据的



2)使用ThreadLocal 进行用户身份鉴别信息传递

  • 在调用购物车的接口前,先通过session信息判断是否登录,并分别进行用户身份信息的封装

    • session有用户信息则进行用户登录 userInfoTo.setUserId(member.getId());
    • session中没有用户信息

      • cookie中携带 user-key,则表示有临时用户,把user-key进行用户身份信息的封装: userInfoTo.setUserKey(cookie.getValue());

        userInfoTo.setTempUser(true); 并标识携带user-key
      • cookie中未携带 user-key,则表示没有临时用户,进行分配
  • 将信息封装好放进ThreadLocal
  • 在调用购物车的接口后,若cookie中未携带 user-key,则分配临时用户,让浏览器保存


第一步

、编写 用户身份信息的封装的TO

package com.atguigu.cart.vo;

@ToString
@Data
public class UserInfoTo {
    private Long userId;
    private String userKey; 

    private boolean tempUser = false;   // 判断是否有临时用户
}

**第二步、**编写拦截器

  • 在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
  • 在执行目标方法之后,若没有临时用户则分配临时用户,让浏览器保存
package com.atguigu.cart.interceptor;
/**
 * Data time:2022/4/10 11:29
 * StudentID:2019112118
 * Author:hgw
 * Description: 拦截器
 * 在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
 *  登录用户
 *  临时用户
 */
public class CartInterceptor implements HandlerInterceptor {

    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     *  true:   放行
     *  false:  不放行
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (member != null) {
            // 用户登录
            userInfoTo.setUserId(member.getId());
        }
        Cookie[] cookies = request.getCookies();
        if (cookies!=null && cookies.length>0){
            for (Cookie cookie : cookies) {
                //user-key
                String name = cookie.getName();
                if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);   // 标识携带user-key
                }
            }
        }

        // 如果没有临时用户,则进行分配
        if (StringUtils.isEmpty(userInfoTo.getUserKey())){
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }

        // 目标方法执行之前,将信息封装好放进ThreadLocal
        threadLocal.set(userInfoTo);
        return true;
    }

    /**
     * 目标方法执行之后
     *  分配临时用户,让浏览器保存
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();

        if (!userInfoTo.isTempUser()) {
            // 持续的延长临时用户的过期时间
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            cookie.setDomain("gulimall.cn");
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }

    }
}


第三步

、添加拦截器的配置

添加拦截器的配置,不能只把拦截器加入容器中,不然拦截器不生效的

package com.atguigu.cart.config;

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

**第四步、**编写Controller处理请求

package com.atguigu.cart.controller;

@Controller
public class CartController {

    /**
     * 去往用户购物车页面
     *  浏览器有一个cookie:user-key 用来标识用户身份,一个月后过期
     *  如果第一次使用京东的购物车功能,都会给一个临时用户身份;浏览器以后保存,每次访问都会带上这个cookie;
     * 登录:Session有
     * 没登录:按照cookie里面的user-key来做。
     *  第一次:如果没有临时用户,帮忙创建一个临时用户。
     * @return
     */
    @GetMapping("/cart.html")
    public String cartListPage(){

        // 1、快速得到用户信息,登录:id,没登录:user-key
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();

        return "cartList";
    }
}

添加常量:

package com.atguigu.common.constant;

public class CartConstant {
    public static final String TEMP_USER_COOKIE_NAME = "user-key";
    public static final int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
}



四、添加商品到购物车

在gulimall-product模块,修改添加购物车按钮



4.1、前端页面修改

第一步、修改item页面

点击 加入购物车 按钮时,发送请求:


http://cart.gulimall.cn/addToCart?skuId=?&num=?

  • skuId:当前商品的skuId
  • num: 当前商品加入购物车的数量

在这里插入图片描述

在这里插入图片描述

$("#addToCartA").click(function () {
   var num = $("#numInput").val();
   var skuId = $(this).attr("skuId");
   location.href = "http://cart.gulimall.cn/addToCart?skuId="+skuId+"&num="+num;
});

第二步、修改 success页面

在这里插入图片描述



4.2、后端接口编写


业务逻辑:

  1. 保存在Redis中的key

    • 如果用户已经登录,则存储在Redis中的key,则是用户的Id
    • 如果用户没有登录,则存在在Redis中的key,是临时用户对应的

      user-key
  2. 购物车保存

    • 若当前商品已经存在购物车,只需增添数量
    • 否则需要查询商品购物项所需信息,并添加新商品至购物车



4.2.1、主体代码编写


1、Controller层接口 CartController类 编写添加商品到购物车方法

/**
 * 添加商品到购物车
 * @param skuId 商品的skuid
 * @param num   添加的商品数量
 * @return
 */
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
                        @RequestParam("num") Integer num,
                        Model model) throws ExecutionException, InterruptedException {

    CartItem cartItem = cartService.addToCart(skuId,num);
    model.addAttribute("item",cartItem);
    return "success";
}

2、Service层实现类 CartController 编写方法

@Slf4j
@Service
public class CartServiceImpl implements CartService {

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    ThreadPoolExecutor executor;

    // 用户标识前缀
    private final String CART_PREFIX = "gulimall:cart:";


    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        String res = (String) cartOps.get(skuId.toString());
        if (StringUtils.isEmpty(res)){
            // 购物车无此商品,添加新商品到购物车 (封装到购物项)
            CartItem cartItem = new CartItem();
            // 1、远程查询当前要添加的商品的信息 SKU信息并封装
            CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                cartItem.setSkuId(skuId);
                cartItem.setCheck(true);
                cartItem.setTitle(data.getSkuTitle());
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setPrice(data.getPrice());
                cartItem.setCount(num);
            },executor);

            // 2、远程查询sku的组合信息
            CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
                List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(values);
            }, executor);

            // 3、等远程查询都完成之后在向Redis中放数据
            CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
            String s = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(), s);

            return cartItem;
        } else {
            // 购物车有此商品,增添数量
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount() + num);
            cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));
            return cartItem;
        }
    }

    /**
     * 获取到要操作的购物车
     * @return
     */
    private  BoundHashOperations<String, Object, Object> getCartOps() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        // 1、判断用户有没有登录
        String cartKey = "";
        if (userInfoTo.getUserId() != null){
            // 用户已登录,则存储在Redis中的key 是 用户的Id
            cartKey = CART_PREFIX+userInfoTo.getUserId();
        } else {
            // 用户没有登录,则存在在Redis中的key 是 临时用户对应的 `user-key`
            cartKey = CART_PREFIX+userInfoTo.getUserKey();
        }
        // 绑定hash
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }
}


4.2.1.1、异步编排

假设 远程查询sku的组合信息 查询需要1秒,远程查询sku的组合信息有需要1.5秒,那总耗时就需要2.5秒。

若使用异步编排的话,只需要1.5秒。

1、 将gulimall-product中

com/atguigu/gulimall/product/config

路径下的 MyThreadConfig、ThreadPoolConfigProperties类复制到 gulimall-cart 服务下的 config 路径下:

package com.atguigu.cart.config;

@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(pool.getCoreSize(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}
package com.atguigu.cart.config;

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

2、配置 线程池

# 配置线程池
gulimall.thread.core-size: 20
gulimall.thread.max-size: 200
gulimall.thread.keep-alive-time: 10



4.2.2、远程查询sku的组合信息


在gulimall-cart 服务中编写远程调用feign接口

package com.atguigu.cart.feign;

@FeignClient("gulimall-product")
public interface ProductFeignService {
    @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
    List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}



4.2.3、远程查询sku的组合信息


Gulimall-product 服务中

  1. Controller层编写查询sku的组合信息
@RestController
@RequestMapping("product/skusaleattrvalue")
public class SkuSaleAttrValueController {
    @Autowired
    private SkuSaleAttrValueService skuSaleAttrValueService;

    @GetMapping("/stringlist/{skuId}")
    public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){
        return  skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
    }
  
  //....
}
  1. Service层实现类 SkuSaleAttrValueServiceImpl 中编写方法
@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
    SkuSaleAttrValueDao dao = this.baseMapper;
    return dao.getSkuSaleAttrValuesAsStringList(skuId);
}
  1. Dao层xml的SQL语句 SkuSaleAttrValueDao.xml
<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
    SELECT CONCAT(attr_name,":",attr_value) FROM pms_sku_sale_attr_value WHERE sku_id=#{skuId};
</select>
  1. 在gulimall-cart 服务中编写远程调用feign接口
package com.atguigu.cart.feign;

@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);

    @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
    List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}

在这里插入图片描述



4.3、RedirectAttribute


在这里插入图片描述

上述编写的代码,只要我们刷新success页面会一直增加数量,客户说 体验感不好

这里修改逻辑:

  • 在controller的addToCart方法里添加商品
  • 商品添加完跳转到成功页面我们改为改成重定向另一个方法,专门查询数据跳转到成功页面

1、Controller层 CartController 类中编写业务

/**
 * 添加商品到购物车
 * @param skuId 商品的skuid
 * @param num   添加的商品数量
 * @return
 * RedirectAttributes
 *  ra.addFlashAttribute(, ) :将数据放在session里面可以在页面里取出,但是只能取一次
 *  ra.addAttribute(,); 将数据放在url后面
 */
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
                        @RequestParam("num") Integer num,
                        RedirectAttributes ra) throws ExecutionException, InterruptedException {
    cartService.addToCart(skuId,num);
    ra.addAttribute("skuId", skuId);
    return "redirect:http://cart.gulimall.cn/addToCartSuccess.html";
}

/**
 * 跳转到成功页
 * @param skuId
 * @param model
 * @return
 */
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model) {
    // 重定向到成功页面,再次查询购物车数据
    CartItem cartItem = cartService.getCartItem(skuId);
    model.addAttribute("item",cartItem);
    return "success";
}

2、Service层 CartServiceImpl 实现类编写 获取购物车某个购物项方法

@Override
public CartItem getCartItem(Long skuId) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    String str = (String) cartOps.get(skuId.toString());
    CartItem cartItem = JSON.parseObject(str, CartItem.class);
    return cartItem;
}

3、success页面修改

<div class="success-wrap">
    <div class="w" id="result">
        <div class="m succeed-box">
            <div th:if="${item!=null}" class="mc success-cont">
                <div class="success-lcol">
                    <div class="success-top"><b class="succ-icon"></b>
                        <h3 class="ftx-02">商品已成功加入购物车</h3></div>
                    <div class="p-item">
                        <div class="p-img">
                            <a href="/static/cart/javascript:;" target="_blank"><img
                                    style="height: 60px;width:60px;" th:src="${item.image}"></a>
                        </div>
                        <div class="p-info">
                            <div class="p-name">
                                <a th:href="'http://item.gulimall.cn/'+${item.skuId}+'.html'"
                                   th:text="${item.title}">TCL 55A950C 55英寸32核人工智能 HDR曲面超薄4K电视金属机身(枪色)</a>
                            </div>
                            <div class="p-extra"><span class="txt" th:text="'数量:'+${item.count}">  数量:1</span></div>
                        </div>
                        <div class="clr"></div>
                    </div>
                </div>
                <div class="success-btns success-btns-new">
                    <div class="success-ad">
                        <a href="/#none"></a>
                    </div>
                    <div class="clr"></div>
                    <div class="bg_shop">
                        <a class="btn-tobback" th:href="'http://item.gulimall.cn/'+${item.skuId}+'.html'">查看商品详情</a>
                        <a class="btn-addtocart" href="http://cart.gulimall.cn/cart.html"
                           id="GotoShoppingCart"><b></b>去购物车结算</a>
                    </div>
                </div>
            </div>
            <div th:if="${item==null}" class="mc success-cont">
                <h2>购物车中无商品</h2>
                <a href="http://gulimall.cn">去购物</a>
            </div>
        </div>
    </div>
</div>



五、获取购物车

  • 若用户



    登录,则使用user-key获取Redis中购物车数据

  • 若用户

    登录

    ,则使用userId获取Redis中购物车数据,并将


    • user-key 对应的临时购物车数据


    • 用户购物车数据

    合并 并删除临时购物车。


第一步、Controller层 CartController 类编写方法

@Controller
public class CartController {

    @Autowired
    CartService cartService;

    @GetMapping("/cart.html")
    public String cartListPage(Model model) throws ExecutionException, InterruptedException {
        Cart cart = cartService.getCart();
        model.addAttribute("cart",cart);
        return "cartList";
    }

第二步、编写Service层 方法

package com.atguigu.cart.service;

public interface CartService {
		//....

    /**
     * 获取购物车某个购物项
     * @param skuId
     * @return
     */
    CartItem getCartItem(Long skuId);

    /**
     * 获取整个购物车
     * @return
     */
    Cart getCart() throws ExecutionException, InterruptedException;

    /**
     * 清空购物车数据
     * @param cartKey
     */
    void clearCart(String cartKey);
}

实现类

CartServiceImpl

方法:

@Override
public CartItem getCartItem(Long skuId) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    String str = (String) cartOps.get(skuId.toString());
    CartItem cartItem = JSON.parseObject(str, CartItem.class);
    return cartItem;
}

@Override
public Cart getCart() throws ExecutionException, InterruptedException {

    Cart cart = new Cart();
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    if (userInfoTo.getUserId()!=null){
        // 1、登录状态
        String cartKey = CART_PREFIX + userInfoTo.getUserId();
        // 2、如果临时购物车的数据还没有合并,则合并购物车
        String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
        List<CartItem> tempCartItems = getCartItems(tempCartKey);
        if (tempCartItems!=null) {
            // 临时购物车有数据,需要合并
            for (CartItem item : tempCartItems) {
                addToCart(item.getSkuId(),item.getCount());
            }
            // 清除临时购物车的数据
            clearCart(tempCartKey);
        }
        // 3、删除临时购物车
        // 4、获取登录后的购物车数据
        List<CartItem> cartItems = getCartItems(cartKey);
        cart.setItems(cartItems);

    } else {
        // 2、没登录状态
        String cartKey = CART_PREFIX + userInfoTo.getUserKey();
        // 获取临时购物车的所有项
        List<CartItem> cartItems = getCartItems(cartKey);
        cart.setItems(cartItems);
    }
    return cart;
}

@Override
public void clearCart(String cartKey) {
    // 直接删除该键
    redisTemplate.delete(cartKey);
}

第三步、修改购物车前端页面 cartList.html

在这里插入图片描述

测试结果:

在这里插入图片描述



六、选中购物项[是否选中]

**第一步、**Controller层方法编写


gulimall-cart

服务

com/atguigu/cart/controller/

路径下 CartController.java类中添加映射方法

@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,
                        @RequestParam("check") Integer check) {
    cartService.checkItem(skuId,check);
    return "redirect:http://cart.gulimall.cn/cart.html";
}

**第二步、**Service层实现类方法中编写是否选中购物项方法

/**
 * 勾选购物项
 * @param skuId
 * @param check
 */
void checkItem(Long skuId, Integer check);


gulimall-cart

服务中

com/atguigu/cart/service/impl/

路径下 CartServiceImpl.java 实现类:

@Override
public void checkItem(Long skuId, Integer check) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    CartItem cartItem = getCartItem(skuId);
    cartItem.setCheck(check==1?true:false);
    String s = JSON.toJSONString(cartItem);
    cartOps.put(skuId.toString(),s);
}

第三步、页面修改

在这里插入图片描述

$(".itemCheck").click(function () {
    var skuId = $(this).attr("skuId");
    var check = $(this).prop("checked");
    location.href = "http://cart.gulimall.cn/checkItem?skuId="+skuId+"&check="+(check?1:0);
});

在这里插入图片描述



七、修改购物项数量



7.1、前端 cartList.html 页面修改

前端 cartList.html 页面修改

<li>
    <p style="width:80px" th:attr="skuId=${item.skuId}">
        <span class="countOpsBtn">-</span>
        <span class="countOpsNum" th:text="${item.count}">5</span>
        <span class="countOpsBtn">+</span>
    </p>
</li>
$(".countOpsBtn").click(function () {
    var skuId = $(this).parent().attr("skuId");
    var num = $(this).parent().find(".countOpsNum").text();
    location.href = "http://cart.gulimall.cn/countItem?skuId="+skuId+"&num="+num; 
});



7.2、后端 接口编写

后端 接口编写

  1. Controller 层 接口编写

修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:

@GetMapping("/countItem")
public String countItem(@RequestParam("skuId") Long skuId,
                        @RequestParam("num") Integer num) {
    cartService.countItem(skuId,num);
    return "redirect:http://cart.gulimall.cn/cart.html";
}
  1. Service 层编写
/**
 * 修改购物项数量
 * @param skuId
 * @param num
 */
void countItem(Long skuId, Integer num);

修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

@Override
public void countItem(Long skuId, Integer num) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    CartItem cartItem = getCartItem(skuId);
    cartItem.setCount(num);
    cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}



八、删除购物项



8.1、前端修改

在这里插入图片描述

在这里插入图片描述



8.2、后端接口

  1. CartController
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId) {
    cartService.deleteItem(skuId);
    return "redirect:http://cart.gulimall.cn/cart.html";
}
  1. CartServiceImpl.java
/**
 * 删除购物项
 * @param skuId
 */
@Override
public void deleteItem(Long skuId) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    cartOps.delete(skuId.toString());
}



版权声明:本文为m0_49183244原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。