1.环境搭建
创建mail-cart
模块
导入依赖
<dependencies>
<dependency>
<groupId>com.peigen.mail</groupId>
<artifactId>mail-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
</dependencies>
编写com.pei.mailcart.MailCartApplication
配置文件
server.port=30001
spring.application.name=mail-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.121.128
#配置线程池
mymail.thread.coreSize=20
mymail.thread.maxSize=200
mymail.thread.keepAliveTime=10
为启动类加上注解
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
导入前端
省略
域名转发
配置host,使得访问cart.mymail.com的时候会转发给服务器处理
服务器会将请求转发给本机的mail-gateway
模块,该模块再将请求转发给mail-cart
模块
配置路由转发:
https://gitee.com/peigenn/typora-pic/raw/master/202306082013694.png
2.分析购物车数据结构
使用redis存储购物车数据
对于一个商城系统来说,购物车是读多写多的地方,所以我们用redis来存储数据
在上面我们已经配置过redis,不在赘述。
购物车数据结构
- 每个用户都应该有自己的购物车
- 每个购物车中有很多商品
- 每个商品有自己的属性,比如颜色,价格等
所以我们用redis中的hash类型存储购物车数据:
Map<String k1,Map<String k2,Cartltemlnfo>>
k1:标识每一个用户的购物车
k2:购物项的商品id
Cartltemlnfo:商品的具体属性
编写om.pei.mailcart.vo.CartVo
购物车类
package com.pei.mailcart.vo;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
/**
* @Description: 整个购物车存放的商品信息 需要计算的属性需要重写get方法,保证每次获取属性都会进行计算
* @Created: with IntelliJ IDEA.
* @author: PEIGEN
* @createTime: 2023-06-7 16:42
**/
public class CartVo {
/**
* 购物车子项信息
*/
List<CartItemVo> items;
/**
* 商品数量
*/
private Integer countNum;
/**
* 商品类型数量
*/
private Integer countType;
/**
* 商品总价
*/
private BigDecimal totalAmount;
/**
* 减免价格
*/
private BigDecimal reduce = new BigDecimal("0.00");;
public List<CartItemVo> getItems() {
return items;
}
public void setItems(List<CartItemVo> items) {
this.items = items;
}
public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += 1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
// 计算购物项总价
if (!CollectionUtils.isEmpty(items)) {
for (CartItemVo cartItem : items) {
if (cartItem.getCheck()) {
amount = amount.add(cartItem.getTotalPrice());
}
}
}
// 计算优惠后的价格
return amount.subtract(getReduce());
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
编写com.pei.mailcart.vo.CartItemVo
商品类
package com.pei.mailcart.vo;
import java.math.BigDecimal;
import java.util.List;
/**
* @Description: 购物项内容
* @Created: with IntelliJ IDEA.
* @author: PEIGEN
* @createTime: 2023-06-7 16:43
**/
public class CartItemVo {
private Long skuId;
private Boolean check = true;
private String title;
private String image;
/**
* 商品套餐属性
*/
private List<String> skuAttrValues;
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> getSkuAttrValues() {
return skuAttrValues;
}
public void setSkuAttrValues(List<String> skuAttrValues) {
this.skuAttrValues = skuAttrValues;
}
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;
}
}
3.临时购物车搭建
在用户为登录的情况下,我们自动给用户分配一个临时的购物车id,这样用户就可以将商品加入到该临时购物车中。
为用户生产临时的购物车id:user-key,并将其放在cookie中
创建用户信息类:存储用户信息
@ToString
@Data
public class UserInfoTo {
private Long userId;
private String userKey; //一定封装
private boolean tempUser = false; //判断是否有临时用户
}
为加入购物车请求配置拦截器:判断是否登录,未登录则为其分配临时购物车
com.pei.mailcart.interceptor.CartInterceptor
package com.pei.mailcart.interceptor;
import com.pei.common.vo.MemberResponseVo;
import com.pei.mailcart.to.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;
import static com.pei.common.constant.AuthServerConstant.LOGIN_USER;
import static com.pei.common.constant.CartConstant.TEMP_USER_COOKIE_NAME;
import static com.pei.common.constant.CartConstant.TEMP_USER_COOKIE_TIMEOUT;
/**
* @Description: 在执行目标方法之前,判断用户的登录状态.并封装传递给controller目标请求
* @Created: with IntelliJ IDEA.
* @author: PEIGEN
* @createTime: 2023-06-7 17:31
**/
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> toThreadLocal = new ThreadLocal<>();
/***
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
//获得当前登录用户的信息
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(LOGIN_USER);
if (memberResponseVo != null) {
//用户登录了
userInfoTo.setUserId(memberResponseVo.getId());
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
//user-key
String name = cookie.getName();
if (name.equals(TEMP_USER_COOKIE_NAME)) {
userInfoTo.setUserKey(cookie.getValue());
//标记为已是临时用户
userInfoTo.setTempUser(true);
}
}
}
//如果没有临时用户一定分配一个临时用户
if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//目标方法执行之前
toThreadLocal.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 = toThreadLocal.get();
//如果没有临时用户一定保存一个临时用户
if (!userInfoTo.getTempUser()) {
//创建一个cookie
Cookie cookie = new Cookie(TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
//扩大作用域
cookie.setDomain("mymail.com");
//设置过期时间
cookie.setMaxAge(TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
为了使拦截器生效,需要注册拦截器
com.pei.mailcart.interceptor.GulimallWebConfig
package com.pei.mailcart.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Description:
* @Created: with IntelliJ IDEA.
* @author: PEIGEN
* @createTime: 2023-06-7 17:57
**/
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor())//注册拦截器
.addPathPatterns("/**");
}
}
4.增删改查购物车
先看几个重要的方法,后续的增删改查需要用到
com.pei.mailcart.service.impl.CartServiceImpl#getCartItems
:获取购物车里面的数据
/**
* 获取购物车里面的数据
* @param cartKey
* @return
*/
private List<CartItemVo> getCartItems(String cartKey) {
//获取购物车里面的所有商品
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
String str = (String) obj;
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
com.pei.mailcart.service.impl.CartServiceImpl#getCart
:合并用户购物车和临时购物车
/**
* 合并用户购物车和临时购物车
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public CartVo getCart() throws ExecutionException, InterruptedException {
CartVo cartVo = new CartVo();
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
if (userInfoTo.getUserId() != null) {
//1、登录
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//临时购物车的键
String temptCartKey = CART_PREFIX + userInfoTo.getUserKey();
//2、如果临时购物车的数据还未进行合并
List<CartItemVo> tempCartItems = getCartItems(temptCartKey);
if (tempCartItems != null) {
//临时购物车有数据需要进行合并操作
for (CartItemVo item : tempCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
//清除临时购物车的数据
clearCartInfo(temptCartKey);
}
//3、获取登录后的购物车数据【包含合并过来的临时购物车的数据和登录后购物车的数据】
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
} else {
//没登录
String cartKey = CART_PREFIX + userInfoTo.getUserKey();
//获取临时购物车里面的所有购物项
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
}
return cartVo;
}
跳转购物车请求
后端代码:
com.pei.mailcart.controller.cartController#cartListPage
/**
* 去购物车页面的请求
* 浏览器有一个cookie:user-key 标识用户的身份,一个月过期
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份:
* 浏览器以后保存,每次访问都会带上这个cookie;
*
* 登录:session有
* 没登录:按照cookie里面带来user-key来做
* 第一次,如果没有临时用户,自动创建一个临时用户
*
* @return
*/
@GetMapping(value = "/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
//快速得到用户信息:id,user-key
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
CartVo cartVo = cartService.getCart();
model.addAttribute("cart",cartVo);
return "cartList";
}
com.pei.mailcart.service.impl.CartServiceImpl#getCart
/**
* 获取用户登录或者未登录购物车里所有的数据
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public CartVo getCart() throws ExecutionException, InterruptedException {
CartVo cartVo = new CartVo();
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
if (userInfoTo.getUserId() != null) {
//1、登录
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//临时购物车的键
String temptCartKey = CART_PREFIX + userInfoTo.getUserKey();
//2、如果临时购物车的数据还未进行合并
List<CartItemVo> tempCartItems = getCartItems(temptCartKey);
if (tempCartItems != null) {
//临时购物车有数据需要进行合并操作
for (CartItemVo item : tempCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
//清除临时购物车的数据
clearCartInfo(temptCartKey);
}
//3、获取登录后的购物车数据【包含合并过来的临时购物车的数据和登录后购物车的数据】
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
} else {
//没登录
String cartKey = CART_PREFIX + userInfoTo.getUserKey();
//获取临时购物车里面的所有购物项
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
}
return cartVo;
}
添加商品到购物车
后端代码:
com.pei.mailcart.controller.cartController#addCartItem
/**
* 添加商品到购物车
* attributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
* attributes.addAttribute():将数据放在url后面
* @return
*/
@GetMapping(value = "/addCartItem")
public String addCartItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes attributes) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId,num);
attributes.addAttribute("skuId",skuId);
return "redirect:http://cart.mymail.com/addToCartSuccessPage.html";
}
com.pei.mailcart.service.impl.CartServiceImpl#addToCart
/**
* 将商品添加到购物车
* @param skuId
* @param num
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//判断Redis是否有该商品的信息
String productRedisValue = (String) cartOps.get(skuId.toString());
//如果没有就添加数据
if (StringUtils.isEmpty(productRedisValue)) {
//2、添加新的商品到购物车(redis)
CartItemVo cartItemVo = new CartItemVo();
//开启第一个异步任务
CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
//1、远程查询当前要添加商品的信息
R productSkuInfo = productFeignService.getInfo(skuId);
SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
//数据赋值操作
cartItemVo.setSkuId(skuInfo.getSkuId());
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setCount(num);
}, executor);
//开启第二个异步任务
CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
//2、远程查询skuAttrValues组合信息
List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
cartItemVo.setSkuAttrValues(skuSaleAttrValues);
}, executor);
//等待所有的异步任务全部完成
CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(), cartItemJson);
return cartItemVo;
} else {
//购物车有此商品,修改数量即可
CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
cartItemVo.setCount(cartItemVo.getCount() + num);
//修改redis的数据
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(),cartItemJson);
return cartItemVo;
}
}
跳转到添加购物车成功页面
不管是购物车中商品的增加、删除、选中、数量加减,都需要将页面跳转到success页面,因为这些操作都是一次性的,如果还在原来的页面,我们点击一次刷新就执行了一次操作。
后端代码:
com.pei.mailcart.controller.cartController#addToCartSuccessPage
/**
* 跳转到添加购物车成功页面
* @param skuId
* @param model
* @return
*/
@GetMapping(value = "/addToCartSuccessPage.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,
Model model) {
//重定向到成功页面。再次查询购物车数据即可
CartItemVo cartItemVo = cartService.getCartItem(skuId);
model.addAttribute("cartItem",cartItemVo);
return "success";
}
com.pei.mailcart.service.impl.CartServiceImpl#getCartItem
@Override
public CartItemVo getCartItem(Long skuId) {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String redisValue = (String) cartOps.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);
return cartItemVo;
}
商品是否选中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ThOmJE9R-1686487850704)(C:/Users/PEIGEN/AppData/Roaming/Typora/typora-user-images/image-20230608205118033.png)]
后端代码:
com.pei.mailcart.controller.cartController#checkItem
/**
* 商品是否选中
* @param skuId
* @param checked
* @return
*/
@GetMapping(value = "/checkItem")
public String checkItem(@RequestParam(value = "skuId") Long skuId,
@RequestParam(value = "checked") Integer checked) {
cartService.checkItem(skuId,checked);
return "redirect:http://cart.mymail.com/cart.html";
}
com.pei.mailcart.service.impl.CartServiceImpl#checkItem
@Override
public void checkItem(Long skuId, Integer checked) {
//查询购物车里面的商品
CartItemVo cartItem = getCartItem(skuId);
//修改商品状态
cartItem.setCheck(checked == 1?true:false);
//序列化存入redis中
String redisValue = JSON.toJSONString(cartItem);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.put(skuId.toString(),redisValue);
}
改变商品数量
后端代码:
com.pei.mailcart.controller.cartController#countItem
/**
* 改变商品数量
* @param skuId
* @param num
* @return
*/
@GetMapping(value = "/countItem")
public String countItem(@RequestParam(value = "skuId") Long skuId,
@RequestParam(value = "num") Integer num) {
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.mymail.com/cart.html";
}
com.pei.mailcart.service.impl.CartServiceImpl#changeItemCount
/**
* 修改购物项数量
* @param skuId
* @param num
*/
@Override
public void changeItemCount(Long skuId, Integer num) {
//查询购物车里面的商品
CartItemVo cartItem = getCartItem(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//序列化存入redis中
String redisValue = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),redisValue);
}
删除商品信息
后端代码:
com.pei.mailcart.controller.cartController#deleteItem
/**
* 删除商品信息
* @param skuId
* @return
*/
@GetMapping(value = "/deleteItem")
public String deleteItem(@RequestParam("skuId") Integer skuId) {
cartService.deleteIdCartInfo(skuId);
return "redirect:http://cart.mymail.com/cart.html";
}
com.pei.mailcart.service.impl.CartServiceImpl#deleteIdCartInfo
/**
* 删除购物项
* @param skuId
*/
@Override
public void deleteIdCartInfo(Integer skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}
总结
该篇我们创建了mail-cart模块,实现了购物车中商品的增删改查。