一、环境搭建
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=30000
spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.195.128
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.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
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//session作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public 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
@Data
public class UserInfoTo {
private Long userId; // 登录后使用userId
private 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表示拒绝该方法;
*/
@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) {
String name = cookie.getName();
// 判断获取到的cookie是否存在临时用户标识user-key
if (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);
}
// 目标方法执行之前,将用户信息放入ThreadLocal
threadLocal.set(userInfoTo);
return true;
}
// 业务执行之后,将临时用户user-key保存至浏览器的cookie中
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
// 如果没有任何信息,创建临时用户,保存cookie
if (!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;
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer{
@Override
public 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
@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、远程查询当前要添加的商品信息
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逆转为CartItem
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount() + num);
// 再将CartItem换回json存入redis
cartOps.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操作都针对此key
BoundHashOperations<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
// 获取购物车
@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();
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;
}
// 清空临时购物车
@Override
public 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
// 勾选购物项
@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);
}
// 获取我们要操作的购物车,临时购物车、用户购物车
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操作都针对此key
BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(cartKry);
return operation;
}
// 查询购物车的某个购物项
@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;
}
七、修改购物项数据
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
// 修改购物项数量
@Override
public 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
// 删除购物项
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}
<br />[<br />](https://blog.csdn.net/wts563540/article/details/109713677)