一、环境搭建
1、创建gulimall-cart模块


2、修改域名
修改C:\Windows\System32\drivers\etc\hosts里的域名
3、复制静态资源
在/mydata/nginx/html/static/目录创建cart文件夹,将所有的静态资源全部都传到虚拟机/mydata/nginx/html/static/cart目录下
将两个静态页面加入gulimall-cart服务里
4、添加配置
server.port=30000spring.application.name=gulimall-cartspring.cloud.nacos.discovery.server-addr=127.0.0.1:8848spring.redis.host=192.168.195.128gulimall.thread.core-size=20gulimall.thread.max-size=200gulimall.thread.keep-alive-time=10
5、为主启动类添加注解
package com.atguigu.gulimall.cart;@EnableFeignClients@EnableDiscoveryClient@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)public class GulimallCartApplication {public static void main(String[] args) {SpringApplication.run(GulimallCartApplication.class, args);}}
6、修改网关,给购物车配置路由
7、测试
先把cartList.html改成index.html启动项目
http://cart.gulimall.com/
二、数据模型分析
1、需求描述
1.1 用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
- 放入数据库
购物车是一个读多写多的场景,因此放入数据库并不合适
- mongodb
- 放入 redis(采用)
登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车
Redis的好处:
- **数据结构好组织**- **Redis具有极高的读写并发性能**- **Redis是内存数据库,一旦宕机就丢失数据,可以在安装Redis的时候指定好持久化策略(损失部分性能,保证数据不丢失)**- <br />
1.2 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
- 放入 localstorage(客户端存储,后台不存)
- cookie(客户端存储)
- WebSQL (客户端存储)
- 放入 redis(采用)
浏览器即使关闭,下次进入,临时购物车数据都在
1.3 其他功能
- 用户可以使用购物车一起结算下单
- 给购物车添加商品
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量。
- 用户可以在购物车中删除商品。
- 选中不选中商品
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化
2、数据结构
一个购物车是由各个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要逐个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用hash进行存储
因此每一个购物项信息,都是一个对象,基本字段包括:
{skuId: 2131241,check: true, // 是否被选中title: "Apple iphone.....", // 商品标题defaultImage: "...", // 商品默认图片price: 4999,count: 1,totalPrice: 4999,skuSaleVO: {...} // 商品销售属性组合}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[{...},{...},{...}]
Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map
- 首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,Value是用户的所有购物车信息。这样看来基本的k-v结构就可以了。
- 但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是k-v结构,key 是商品 id,value 才是这个商品的购物车信息。
综上所述,我们的购物车结构是一个双层 Map:Map
- 第一层 Map,Key 是用户 id
- 第二层 Map,Key 是购物车中商品 id,值是购物项数据
3、VO编写
3.1 购物项VO
package com.atguigu.gulimall.cart.vo;/*** 购物车中的每个购物项内容*///@Data 不用@Data,避免把get、set方法内容覆盖了。totalPrice需要计算得到public class CartItem {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; // 商品总价/*** 计算当前购物项的总价* new BigDecimal("" + this.count)把count变成字符串* @return*/public BigDecimal getTotalPrice() {return this.price.multiply(new BigDecimal("" + this.count));}public void setTotalPrice(BigDecimal totalPrice) {this.totalPrice = totalPrice;}***其他get、set***}
3.2 整个购物车VO
package com.atguigu.gulimall.cart.vo;/*** 整个购物车* 需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算*/public class Cart {private List<CartItem> Items; // 所有的购物项private Integer countNum; // 商品数量private Integer countType; // 商品类型private BigDecimal totalAmount; // 商品总价private BigDecimal reduce = new BigDecimal(0); // 商品减免价格// 获取商品总数量public Integer getCountNum() {int count = 0;if (Items != null && Items.size() > 0) {for (CartItem item : Items) {count += item.getCount();}}return count;}// 获取商品总类型public Integer getCountType() {int count = 0;if (Items != null && Items.size() > 0) {for (CartItem item : Items) {count += 1;}}return count;}// 获取商品总价public BigDecimal getTotalAmount() {BigDecimal amount = new BigDecimal(0);// 1.计算购物车所有商品总价if (Items != null && Items.size() > 0) {for (CartItem item : Items) {if(item.getCheck()){BigDecimal totalPrice = item.getTotalPrice();amount = amount.add(totalPrice);}}}// 2.减去优惠价格BigDecimal amountFinal = amount.subtract(getReduce());return amountFinal;}***其他get、set方法***}
4、导入redis和SpringSession依赖
<!--整合SpringSession完成session共享问题--><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>
5、添加SpringSession配置类
package com.atguigu.gulimall.cart.config;@EnableRedisHttpSession // 自动开启RedisHttpSession@Configurationpublic class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();//session作用域cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer(){//序列化机制return new GenericJackson2JsonRedisSerializer();}}
三、ThreadLocal用户身份鉴别
1、功能分析


user-key 是随机生成的 id,不管有没有登录都会有这个 cookie 信息,有效期为一个月
两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
- 是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
- 否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。
查询购物车列表:判断是否登录
- 否:直接根据 user-key 查询 redis 中数据并展示
- 是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
- 有:需要提交到后台添加到 redis,合并数据,而后查询。
- 否:直接去后台查询 redis,而后返回。
(1) 用户身份鉴别方式
参考京东,在点击购物车时,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,如果手动清除user-key,那么临时购物车的购物项也被清除,所以user-key是用来标识和存储临时购物车数据
(2) 使用ThreadLocal进行用户身份鉴别信息传递
在调用购物车的接口前,先通过session信息判断是否登录,并分别进行用户身份信息的封装,并把user-key放在cookie中
2、登录拦截器
2.1 UserInfoTo
package com.atguigu.gulimall.cart.vo;// 传输对象to@ToString@Datapublic class UserInfoTo {private Long userId; // 登录后使用userIdprivate String userKey; // 未登录使用用户临时键private boolean tempUser = false; // 判断是否存在临时用户信息}
2.2 CartConstant常量
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;}
2.3 实现拦截器
package com.atguigu.gulimall.cart.interceptor;/*** 在执行目标方法前,先判断用户登录状态。并封装用户信息传递给Controller*/public class CartInterceptor implements HandlerInterceptor {// 同一线程共享数据public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();/*** 目标方法执行之前拦截,判断用户是否已登录* @return return true表示放行该方法;return false表示拒绝该方法;*/@Overridepublic 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) {String name = cookie.getName();// 判断获取到的cookie是否存在临时用户标识user-keyif (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {userInfoTo.setUserKey(cookie.getValue());userInfoTo.setTempUser(true);}}}// 如果没有临时用户,分配临时用户if (StringUtils.isEmpty(userInfoTo.getUserKey())) {String uuid = UUID.randomUUID().toString();userInfoTo.setUserKey(uuid);}// 目标方法执行之前,将用户信息放入ThreadLocalthreadLocal.set(userInfoTo);return true;}// 业务执行之后,将临时用户user-key保存至浏览器的cookie中@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {UserInfoTo userInfoTo = threadLocal.get();// 如果没有任何信息,创建临时用户,保存cookieif (!userInfoTo.isTempUser()) {Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());cookie.setDomain("gulimall.com");// 单位秒cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);response.addCookie(cookie);}}}
2.4 添加拦截器的配置
不能只把拦截器加入容器中,不然拦截器不生效的
package com.atguigu.gulimall.cart.config;@Configurationpublic class GulimallWebConfig implements WebMvcConfigurer{@Overridepublic void addInterceptors(InterceptorRegistry registry) {// addInterceptor:添加拦截器// addPathPatterns:被拦截的请求,/**:拦截所有请求registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");}}
四、添加商品到购物车
业务逻辑:
若当前商品已经存在购物车,只需增添数量
否则需要查询商品购物项所需信息,并添加新商品至购物车
1、CartController
/*** 添加商品到购物车* RedirectAttributes attributes* attributes.addFlashAttribute():将数据放在session里面可以在页面取出,但只能取一次* attributes.addAttribute("skuId",skuId):将数据放在url后面* @return*/@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);// 重定向到成功页面,防止刷新页面重复提交商品,同时将商品ID传递过去return "redirect:http://cart.gulimall.com/addToCartSuccess.html";}



@GetMapping("/addToCartSuccess.html")public String addToCartSuccessPage(@RequestParam("skuId")Long skuId, Model model){// 重定向到成功页面,再次查询购物车数据CartItem items = cartService.getCartItem(skuId);model.addAttribute("items",items);return "success";}
2、CartServiceImpl
@AutowiredStringRedisTemplate redisTemplate;@AutowiredProductFeignService productFeignService;@AutowiredThreadPoolExecutor executor;private final String CART_PREFIX = "gulimall:cart:";@Overridepublic 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、远程查询当前要添加的商品信息CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {R skuInfo = productFeignService.getSkuInfo(skuId);SkuInfoVo data = skuInfo.getData2("skuInfo", new TypeReference<SkuInfoVo>() {});cartItem.setCheck(true);cartItem.setCount(num);cartItem.setImage(data.getSkuDefaultImg());cartItem.setTitle(data.getSkuTitle());cartItem.setSkuId(skuId);cartItem.setPrice(data.getPrice());}, executor);// 2、远程查询sku的组合信息CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {List<String> values = productFeignService.getSkuSaleAttrValues(skuId);cartItem.setSkuAttr(values);}, executor);// 把cartItem对象转换为json格式CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrValues).get();String s = JSON.toJSONString(cartItem);cartOps.put(skuId.toString(), s);return cartItem;} else {// 购物车有此商品,修改数量// 将json逆转为CartItemCartItem cartItem = JSON.parseObject(res, CartItem.class);cartItem.setCount(cartItem.getCount() + num);// 再将CartItem换回json存入rediscartOps.put(skuId.toString(), JSON.toJSONString(cartItem));return cartItem;}}
/*** 获取我们要操作的购物车,临时购物车、用户购物车*/private BoundHashOperations<String, Object, Object> getCartOps() {UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();// 判断真实用户还是临时用户String cartKry = "";if (userInfoTo.getUserId() != null) {cartKry = CART_PREFIX + userInfoTo.getUserId();} else {cartKry = CART_PREFIX + userInfoTo.getUserKey();}// 绑定key,以后所有redis操作都针对此keyBoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(cartKry);return operation;}
五、获取购物车
若用户未登录,则直接使用user-key获取购物车数据
否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
1、CartController
/*** 浏览器有一个cookies,user-key:标识用户身份,过期时间一个月。* 浏览器以后添加商品,每次访问都会带上这个cookie* 第一次登录需要创建一个临时用户** 登录:有session* 未登录:按照cookie里面带来的user-key来识别用户身份* 第一次:如果没有临时用户身份,帮忙创建一个临时用户身份* @param* @return*/@GetMapping("/cart.html")public String cartListPage(Model model) throws ExecutionException, InterruptedException {// 快速得到用户信息UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();Cart cart = cartService.getCart();model.addAttribute("cart",cart);return "cartList";}
2、CartServiceImpl
// 获取购物车@Overridepublic 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();String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();// 1.2 如果临时购物车的数据还没有合并?// 先获取临时购物车,然后合并List<CartItem> tempCartItems = getCartItems(tempCartKey);if (tempCartItems != null) {for (CartItem item : tempCartItems) {addToCart(item.getSkuId(), item.getCount());}}// 最后清空临时购物车clearCart(tempCartKey);// 1.3 最终获取登录后的购物车数据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;}// 清空临时购物车@Overridepublic void clearCart(String cartKey) {redisTemplate.delete(cartKey);}/*** 获取购物车中的购物项*/private List<CartItem> getCartItems(String cartKey) {BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(cartKey);List<Object> values = hashOps.values();if (values != null && values.size() > 0) {List<CartItem> collect = values.stream().map((obj) -> {String str = (String) obj;CartItem cartItem = JSON.parseObject(str, CartItem.class);return cartItem;}).collect(Collectors.toList());return collect;}return null;}
六、选中购物项
1、 CartController
// 勾选购物项@GetMapping("/checkItem")public String checkItem(@RequestParam("skuId") Long skuId,@RequestParam("check") Integer check){cartService.checkItem(skuId,check);return "redirect:http://cart.gulimall.com/cart.html";}
2、 CartServiceImpl
// 勾选购物项@Overridepublic 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);}// 获取我们要操作的购物车,临时购物车、用户购物车private BoundHashOperations<String, Object, Object> getCartOps() {UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();// 判断真实用户还是临时用户String cartKry = "";if (userInfoTo.getUserId() != null) {cartKry = CART_PREFIX + userInfoTo.getUserId();} else {cartKry = CART_PREFIX + userInfoTo.getUserKey();}// 绑定key,以后所有redis操作都针对此keyBoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(cartKry);return operation;}// 查询购物车的某个购物项@Overridepublic 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;}
七、修改购物项数据
1、 CartController
// 修改购物项数量@GetMapping("/countItem")public String countItem(@RequestParam("skuId") Long skuId,@RequestParam("num")Integer num){cartService.countItem(skuId,num);return "redirect:http://cart.gulimall.com/cart.html";}
2、 CartServiceImpl
// 修改购物项数量@Overridepublic void countItem(Long skuId, Integer num) {BoundHashOperations<String, Object, Object> cartOps = getCartOps();CartItem cartItem = getCartItem(skuId);cartItem.setCount(num);String s = JSON.toJSONString(cartItem);cartOps.put(skuId.toString(), s);}
八、删除购物项
1、 CartController
// 删除购物项@GetMapping("/deleteItem")public String deleteItem(@RequestParam("skuId") Long skuId){cartService.deleteItem(skuId);return "redirect:http://cart.gulimall.com/cart.html";}
2、 CartServiceImpl
// 删除购物项@Overridepublic void deleteItem(Long skuId) {BoundHashOperations<String, Object, Object> cartOps = getCartOps();cartOps.delete(skuId.toString());}
<br />[<br />](https://blog.csdn.net/wts563540/article/details/109713677)
