购物车
环境搭建
创建购物车项目
第一步、创建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.com
127.0.0.1 search.gulimall.com
127.0.0.1 item.gulimall.com
127.0.0.1 auth.gulimall.com
127.0.0.1 cart.gulimall.com
# Gulimall Host End
第三步、导入依赖
<dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>
因为目前不用数据库,故还需要排除掉mybatis相关依赖
@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_routeuri: lb://gulimall-cartpredicates:- Host=cart.gulimall.com
动静资源处理
- 将资料中购物车文件夹下的所有的静态资源复制到服务器的:
mydata/nginx/html/static/cart
目录下 - 将资料中购物车文件夹下的 两个页面复制到 gulimall-cart服务的
templates
目录下 - 替换掉网页中的所有资源申请路径
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
前端页面环境搭建
需求:实现页面的跳转
- 当我们在商品详情页
item.html
点击加入购物车之后,跳转到加入成功页success.html
- 在成功页
success.html
点击 购物车 进入购物车列表页cartList.html
- 在成功页
success.html
点击 查看商品详情 跳转到该商品的详情页- 在 首页
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.com/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.com/3.html">查看商品详情</a><a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"id="GotoShoppingCart"><b></b>去购物车结算</a>
</div>
Gulimall-cart 服务中 cartList.html 页面
<div class="one_top_left"><a href="http://gulimall.com" 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.com">首页</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";}
}
数据模型分析
需求描述
购物车需求
-
用户可以在 登录状态 下将商品添加到购物车 [ 用户在线购物车 ]
- 放入数据库(购物车的读和写都多,不适合)
- mongodb(购物车数据类型是文档型的,可以用MongoDB但效率也不高)
- 放入 Redis (采用Redis的持久化机制。采用)
登录之后,会将离线(临时)购物车的数据全部合并过来,并清空离线购物车;
-
用户可在 未登录状态 下将商品添加到购物车 [ 用户离线临时购物车 ]
- 放入 localstorage、cookie、WebSQL(客户端(浏览器)存储,后台服务器不存。但因为大数据动态推荐商品功能,这些存储在浏览器的方式我们的电商项目不采用这种方式)
- 放入 Redis(即使用户将浏览器关闭,但用户下次进入我们的商城,临时购物车数据仍然都在。采用)
浏览器即使关闭,下次进入,临时购物车数据都在
-
购物功能
- 用户可以使用购物车一起结算下单(选中要结算的购物车中的商品)
- 给购物车添加商品
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量
- 用户可以在购物车中删除商品
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化
购物车数据结构
数据存储:
- 购物车是一个读多写多的场景,因此放入数据库并不合适,因此可以选用redis(它有读写锁)。但购物车又需要进行购物车中商品数据的持久化,因此这里我们可以使用Redis的持久化机制存储购物车数据。
既然我们选择了redis存储我们购物车中的商品数据,那么应该以什么数据结构存储这些商品数据呢?
Redis中每个用户的购物车都是由各个购物项组成的,购物车中每一个购物项都是一个独立的sku对象,所以购物车的数据类型应该是一个集合。但是将购物车中的购物项存为list类型的话,修改起来太麻烦要从头到尾遍历(因为不知道购物车中商品信息在redis中的存储位置)。因此可以使用hash来存储购物车中的购物项:
- Map<String k1,Map<String k2,CartltemInfo>>(Redis的数据是以键值对的形式存于其中的)
- K1:用户标识
- Map<String k2,CartltemInfo>(以Hash数据类型存储购物车中的购物项)
- K2 :商品Id
- CartltemInfo :购物项详情
VO编写
Cart
需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
- 计算商品的总数量
- 计算商品类型数量
- 计算商品的总价
/*** 整个购物车,对计算商品数量、商品类型数量和商品总价的get方法进行了特殊处理,在调用get方法计算数量或总价*/
public class Cart {List<CartItem> items;private Integer countNum;// 商品数量private Integer countType;//商品类型数量private BigDecimal totalAmount;//商品总价private BigDecimal reduce = new BigDecimal("0.00");//减免价格public Integer getCountNum() {int count = 0;if (this.items != null && this.items.size() > 0) {for (CartItem item : this.items) {count += item.getCount();}}return count;}public Integer getCountType() {return this.items != null && this.items.size() > 0 ? this.items.size() : 0;}public BigDecimal getTotalAmount() {BigDecimal amount = new BigDecimal("0");//计算价格总和if (items.size() > 0 && items != null) {for (CartItem item : this.items) {BigDecimal totalPrice = item.getTotalPrice();amount = amount.add(totalPrice);}}//减去优惠价格amount = amount.subtract(getReduce());return amount;}
}
CartItem
- 计算小计价格
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;/*** 计算当前购物项总价* @return*/public BigDecimal getTotalPrice() {return this.price.multiply(new BigDecimal("" + this.count));}
}
ThreadLocal用户身份鉴别
环境配置
因为我们将购物车数据存储在了Redis中。因此我们先需要导入Spring整合Redis的依赖以及Redis的配置。项目上线之后,应该有一个专门的Redis负责存储购物车的数据不应该使用缓存的Redis。我们还用到了session来记录用户登录状态(通过判断Session中是否有用户的数据),因此还需要导入SpringSession的依赖
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=192.168.10.10
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 {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer() {return new GenericJackson2JsonRedisSerializer();}
}
问题描述
参考京东和淘宝之前都有离线购物车功能,就是在用户未登录的时候也可以将商品加入购物车,并且,用户进行登录之后,临时购物车的商品并不会丢失,而是会加到用户的购物车里。
问题是:如何保留购物车中的商品?
我们可以将购物车中的商品保存到Redis
中,Redis
具有持久化策略,服务器宕机了也可以恢复数据。
但是,如何怎么去记住是那些用户将商品添加到了购物车?也就是Redis
中的key
应该是什么?
如果我们登录的话,这个key
可以利用用户的账号生成,但是临时用户如何保存呢?
学习jd的解决方案可以这样来做:
用户第一次访问购物车的时候为用户颁发一个Cookie(无论是否登录都颁发),cookie的key就叫user-key
,value在后台随机生成。同时要设置cookie的过期时间为1个月,还有cookie的作用域。如果手动清除user-key
,那么临时购物车的购物项也被清除,所以user-key
是用来标识和存储临时购物车数据的。
同时,在我们进入购物车页面的时候,要判断用户是否登录,同时要判断是否已经向用户颁发了cookie。
如果我们在每一个controller层的方法都进行判断的话,会使代码变得冗余。
所以,我们使用SpringMVC
的拦截器机制。综上分析,购物车数据展示需求如下:
-
用户登录之后点击购物车:
- 访问Session中存储的之前登录的用户信息,并进行用户身份信息(userId)的封装,userId只有登录过的UserInfoTo才有,没登陆过的这个属性值为null,这个属性就是购物车取数据时是取用户购物车还是临时购物车的一个判断。
-
用户未登录的时候点击购物车:
- Cookie中有 user-key,则表示有临时用户,访问临时用户的购物车信息,把user-key进行用户身份信息的封装
- Cookie中没有 user-key,则表示没有临时用户
- 创建一个临时用户即生成一个name为user-key的cookie临时标识,过期时间为一个月,以后每次浏览器进行访问购物车的时候都会携带user-key。user-key 是用来标识和存储临时购物车数据的,处理完后会返回这个user-key
总之没登陆的用户userId为空,登录的userId有值,但是登不登陆其都有userKey这个属性的值。
以上封装好的信息会放进ThreadLocal交给controller进行处理。
第一步、编写用户身份信息的封装的TO
package com.atguigu.cart.vo;@ToString
@Data
public class UserInfoTo {private Long userId;private String userKey; private boolean tempUser = false; // 判断是否有临时用户
}
编写拦截器
想要使用SpringMVC
的拦截器机制,需要先编写一个类,实现HandlerInterceptor
接口,根据业务需求重写接口中的方法。
拦截器作用为在调用购物车的接口(实际上拦截器拦截所有请求,不只是购物车的请求)前,先通过 Session 信息判断是否登录,并分别进行用户身份信息的封装,并把user-key
放在 Cookie 中。
重写了两个方法:
- 在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
- 在执行目标方法之后,若没有临时用户则分配临时用户,让浏览器保存
同时,还要指定拦截器作用在那些请求上。
注意细节:整合SpringSession之后,Session获取数据都是从Redis中获取的
/*** 在执行目标方法之前,判断用户登录状态,并封装传递给controller目标请求*/
public class CartInterceptor implements HandlerInterceptor {public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<UserInfoTo>();/*** 业务执行前拦截*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();//从Session中获取数据MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);UserInfoTo userInfoTo = new UserInfoTo();//1 用户已经登录,设置userIdif (member != null) {userInfoTo.setUserId(member.getId());}Cookie[] cookies = request.getCookies();if (cookies != null && cookies.length > 0) {for (Cookie cookie : cookies) {String name = cookie.getName();//2 如果cookie中已经有user-Key,则直接设置if (CartConstant.TEMP_USER_COOKIE_NAME.equals(name)) {userInfoTo.setUserKey(cookie.getValue());userInfoTo.setHasUserKey(true);break;}}}//3 执行到这里 如果cookie为空即没有user-key(没有临时用户),则我们通过uuid生成user-key分配一个if (StringUtils.isEmpty(userInfoTo.getUserKey())) {String uuid = UUID.randomUUID().toString();userInfoTo.setUserKey(uuid);}//4 将用户身份认证信息即封装好的UserInfo放入threadlocal进行传递threadLocal.set(userInfoTo);return true;}/*** 业务执行之后的拦截器,目的是让让浏览器保存Cookie*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();//如果cookie中没有临时用户信息user-key,我们为其生成if (!userInfoTo.isHasUserKey()) {Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());//设置作用域cookie.setPath("gulimall.com");cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);response.addCookie(cookie);}}
}
这里哪怕之前登陆过,我们也会让其带上userkey,便于之后的整合用户购物车和临时购物车。
第三步、添加拦截器的WebConfig配置类
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 配置CartInterceptor拦截器拦截所有请求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-keyUserInfoTo 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;
}
ThreadLocal
ThreadLocal是一个池,也就是一个Map,存放的是一个线程下的共享数据,即同一个线程共享ThreadLocal中的数据
- 核心原理是:Map<Thread,Object> threadLocal
- 在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。线程之间互不干扰
已知:
- 一次请求进来: 拦截器 ==>> Controller ==>> Service ==>> dao 用的都是同一个线程
因为拦截器在拦截请求之后会向controller层传递一个UserInfoVo
对象,所以为了获取到这个对象,使用ThreadLocal
所以在拦截器处设置一个静态的ThreadLocal变量:
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<UserInfoTo>();
放上数据后就可以传递给后面。
添加商品到购物车
在gulimall-product模块,修改添加购物车按钮
前端页面修改
修改item页面
点击 加入购物车 按钮时,发送请求:
http://cart.gulimall.com/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页面
后端接口编写
业务逻辑:
- 保存在Redis中的key
- 如果用户已经登录,则存储在Redis中的key是用户的Id
- 如果用户没有登录,则存在在Redis中的key是临时用户对应的
user-key
- 购物车保存
- 若当前商品已经存在购物车,只需增添数量
- 否则需要查询商品购物项所需信息,并添加新商品至购物车
主体代码编写
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 {@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、远程查询当前要添加的商品的信息 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 是 用户的IdcartKey = CART_PREFIX+userInfoTo.getUserId();} else {// 用户没有登录,则存在在Redis中的key 是 临时用户对应的 `user-key`cartKey = CART_PREFIX+userInfoTo.getUserKey();}// 绑定hashBoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);return operations;}
}
这里因为我们购物项存储在redis中使用的数据类型是hash,所以key是购物车id(哪个用户的),value是购物项(hash):其hashkey是商品id,hashvalue是商品详情。所以如果我们用redisTemplate.operationHash那么每次都要写清key和hashkey,太麻烦了,所以使用redisTemplate.boundHashOps并且把获取操作的购物车封装为一个方法,每次都是操作当前登录用户或者临时用户的购物车。
异步编排
假设 远程查询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 {@Beanpublic 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
远程查询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);
}
Gulimall-product 服务中
- Controller层编写查询sku的组合信息
@RestController
@RequestMapping("product/skusaleattrvalue")
public class SkuSaleAttrValueController {@Autowiredprivate SkuSaleAttrValueService skuSaleAttrValueService;@GetMapping("/stringlist/{skuId}")public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){return skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);}//....
}
- Service层实现类 SkuSaleAttrValueServiceImpl 中编写方法
@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {SkuSaleAttrValueDao dao = this.baseMapper;return dao.getSkuSaleAttrValuesAsStringList(skuId);
}
- 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>
- 在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);
}
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";
}
这样重定向之后浏览器url地址就变了,改为我们重定向后的url地址,那么刷新这个地址只是重复查询这个购物车数据而不是添加数据了,这里注意两点, 我们使用model和RedirectAttributes.addAttribute如果是get请求的话,添加的数据会自动拼接到返回地址的后面作为参数传递过去,还有一点就是重定向使用/默认是当前服务的ip和端口号,如果重定向页面涉及到了跨服务,那么应该写清所要跳转的服务的域名。
RedirectAttributes的addFlashAttribut()方法:将对象存储在Session中且只能使用一次,再次刷新就没有了
RedirectAttributes的addAttribut()方法:将对象拼接在url中(get请求)
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"><imgstyle="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 {@AutowiredCartService 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);
});
修改购物项数量
前端页面修改
修改购物车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;
});
后端接口编写
- 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";
}
- 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));
}
删除购物项
前端页面修改
后端接口编写
- CartController
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId) {cartService.deleteItem(skuId);return "redirect:http://cart.gulimall.cn/cart.html";
}
- CartServiceImpl.java
/*** 删除购物项* @param skuId*/
@Override
public void deleteItem(Long skuId) {BoundHashOperations<String, Object, Object> cartOps = getCartOps();cartOps.delete(skuId.toString());
}
感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。