1、环境搭建
1)、创建gulimall-cart模块
2)、修改C:\Windows\System32\drivers\etc\hosts里的域名
#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
192.168.119.127 cart.gulimall.com
3)、在/mydata/nginx/html/static/目录创建cart文件夹,将所有的静态资源全部都传到虚拟机/mydata/nginx/html/static/cart目录下
将两个静态页面加入gulimall-cart服务里
4)、改静态资源的访问路径,只举一个例子
5)、添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.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>Hoxton.SR8</spring-cloud.version></properties><dependencies><dependency><groupId>com.auguigu.gulimall</groupId><artifactId>gulimall-commom</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></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><repositories><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url></repository></repositories></project>
6)、添加配置
server.port=40000
spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
7)、为主启动类添加注解
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {public static void main(String[] args) {SpringApplication.run(GulimallCartApplication.class, args);}}
8)、修改网关,给购物车配置路由
- id: gulimall_cart_routeuri: lb://gulimall-cartpredicates:- Host=cart.gulimall.com
测试
先把cartList.html改成index.html启动项目 http://cart.gulimall.com/
前端页面修改跳转到首页的链接
<ul class="header-left"><li><a href="http://gulimall.com">首页</a></li></ul>
<div class="one_top_left"><a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a><a href="#" class="one_left_link">购物车</a></div>
2、数据模型分析
2.1需求描述(多读多写状态Redis)
没登陆是临时用户,点击购物车生成一个user-key的cookie数据,然后登录后将临时数据和登录下已有的购物车合并。关闭浏览器后,未登录的临时购物车清空。
用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
1.放入数据库
2.mongodb
3.放入 redis(采用)
登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车;
用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
放入 localstorage(客户端存储,后台不存)
cookie
WebSQL
放入 redis(采用)
浏览器即使关闭,下次进入,临时购物车数据都在(和上面两个不同的状态)
基本功能:
用户可以使用购物车一起结算下单
给购物车添加商品
用户可以查询自己的购物车
用户可以在购物车中修改购买商品的数量。
用户可以在购物车中删除商品。
选中不选中商品
在购物车中展示商品优惠信息
2.2、数据存储
购物车是一个读多写多的场景,因此放入数据库并不合适,但购物车又是需要持久化,因此这里我们选用redis存储购物车数据。
2.3、数据结构
购物项
因此每一个购物项信息,都是一个对象,基本字段包括:
{skuId: 2131241, check: true, title: "Apple iphone.....", defaultImage: "...", price: 4999, count: 1, totalPrice: 4999, skuSaleVO: {...}
}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[{...},{...},{...}
]
Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map<String, List>
首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,Value 是用户的所有购物车信息。这样看来基本的k-v
结构就可以了。
但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是k-v
结构,key 是商品 id,value 才是这个商品的购物车信息。
综上所述,我们的购物车结构是一个双层 Map :
Map<String k1,Map<String k2,CartItemInfo>>
第一层 Map,Key 是用户 id(区分不同的购物车)
第二层 Map,Key 是购物车中商品 id,值是购物项数据
一个购物车是由各个购物项组成的,但是我们用 List进行存储并不合适,因为使用 List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用 hash进行存储。
2.4、流程
参照京东
user-key 是随机生成的 id,不管有没有登录都会有这cookie 信息。
两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。
查询购物车列表:判断是否登录
否:直接根据 user-key 查询 redis 中数据并展示
是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
有:需要提交到后台添加到 redis,合并数据,而后查询。
否:直接去后台查询 redis,而后返回。
3、VO编写
1.因此每一个购物项信息,都是一个对象,基本字段包括:
添加“com.atguigu.gulimall.cart.vo.CartItem”类,代码如下:
需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
package com.atguigu.gulimall.cart.vo;import java.math.BigDecimal;
import java.util.List;/*** @Description: 购物项* @Date: 2024/5/19 19:04* @Version 1.0*/
public class CartItem {/*** 商品id*/private Long skuId;/*** 是否选中*/private Boolean check = true;/*** 标题*/private String title;/*** 图片*/private String image;/*** 商品套餐属性,多种属性,因此用List*/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;}
}
添加“com.atguigu.gulimall.cart.vo.Cart”类,代码如下:
package com.atguigu.gulimall.cart.vo;import java.math.BigDecimal;
import java.util.List;/*** @Description: 整体购物车 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算* @Date: 2024/5/19 19:07* @Version 1.0*/
public class Cart {/*** 购物车子项信息*/List<CartItem> items;/*** 商品数量*/private Integer countNum;/*** 商品类型数量*/private Integer countType;/*** 商品总价*/private BigDecimal totalAmount;/*** 减免价格*/private BigDecimal reduce = new BigDecimal("0.00");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) {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 subtract = amount.subtract(getReduce());return subtract;}public BigDecimal getReduce() {return reduce;}public void setReduce(BigDecimal reduce) {this.reduce = reduce;}
}
2、导入redis和SpringSession的依赖
整合spring-session
原因:购物车需要区分登录,未登录
<!--整合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>
3、配置redis和SpringSession存储类型
#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379
spring.session.store-type=redis
4、添加SpringSession配置类
添加“com.atguigu.gulimall.cart.config.GulimallSessionConfig”类,代码如下:
@EnableRedisHttpSession //自动开启RedisHttpSession
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;
}@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){return new GenericJackson2JsonRedisSerializer();
}
}
4、ThreadLocal用户身份鉴别
拦截器里使用共享变量,建立一个公共的ThreadLocal
1)、ThreadLocal同一个线程共享数据
#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379
spring.session.store-type=redis
2)、核心原理:
Map<Thread,Object> threadLocal
3)、用户身份鉴别方式
参考京东,在点击购物车时,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,
如果手动清除user-key,那么临时购物车的购物项也被清除,
所以user-key是用来标识和存储临时购物车数据的。
4)、使用ThreadLocal进行用户身份鉴别信息传递
在调用购物车的接口前,
先通过session信息判断是否登录,并分别进行用户身份信息的封装,并把user-key放在cookie中
这个功能使用拦截器进行完成
添加“com.atguigu.gulimall.cart.vo.UserInfoTo”类,代码如下:
@ToString
@Data
public class UserInfoTo {
private Long userId;private String userKey; //一定封装private boolean tempUser = false; //判断是否有临时用户
}
添加\common\constant\CartConstant.java
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;}
判断用户是否登录,使用拦截器CartInterceptor
添加“com.atguigu.gulimall.cart.interceptor.CartInterceptor”类,代码如下:
package com.atguigu.gulimall.cart.interceptor;import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.constant.CartConstant;
import com.atguigu.common.vo.MemberResponseVO;
import com.atguigu.gulimall.cart.vo.U serInfoTo;
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;/*** @Description: 在执行目标方法之前,判断用户的登录状态。并封装传递给目标请求* @Date: 2024/5/19 19:39* @Version 1.0*/
public class CartInterceptor implements HandlerInterceptor {// ThreadLocal同一个线程共享数据public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();/*** 在目标方法执行之前拦截** @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {UserInfoTo userInfoTo = new UserInfoTo();HttpSession session = request.getSession();MemberResponseVO member = (MemberResponseVO) session.getAttribute(AuthServerConstant.LOGIN_USER);// 1、用户登录,封装用户idif (member != null) {userInfoTo.setUserId(member.getId());}// 2、如果有临时用户,封装临时用户Cookie[] cookies = request.getCookies();if (cookies != null && cookies.length > 0) {for (Cookie cookie : cookies) {String name = cookie.getName();if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {userInfoTo.setUserKey(cookie.getValue());userInfoTo.setTempUser(true);}}}// 3、如果没有临时用户,一定保存一个临时用户if (StringUtils.isEmpty(userInfoTo.getUserKey())) {String uuid = UUID.randomUUID().toString();userInfoTo.setUserKey(uuid);}// 目标方法执行之前,将用户信息保存到ThreadLocalthreadLocal.set(userInfoTo);return true;}/*** 业务执行之后 分配临时用户,让浏览器保存** @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {UserInfoTo userInfoTo = threadLocal.get();// 如果没有临时用户,第一次访问购物车就添加临时用户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);}}
}
添加拦截器的配置,不能只把拦截器加入容器中,不然拦截器不生效的
添加“com.atguigu.gulimall.cart.config.GulimallWebConfig”类,代码如下:
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {/*** 添加拦截器的配置,不能只把拦截器加入容器中,不然拦截器不生效的* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");}
}
修改index.html名字为cartList.html
添加“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:
@Controller
public class CartController {
/*** 浏览器有一个cookie;user-key:标识用户身份,一个月后过期* 如果第一次使用jd购物车功能,都会给一个临时的用户身份* 浏览器保存,每次访问都会带上有这个cookies** 登录 session有* 没登录,按照cookie里面带来的user-key来做* 第一次,如果没有临时用户,帮忙创建一个临时用户*/
@GetMapping("/cart.html")
public String cartListPage(){//快速得到用户信息,id,user-keyUserInfoTo userInfoTo = CartInterceptor.threadLocal.get();System.out.println(userInfoTo);return "cartList";
}
}
5、页面调整
1、点击商品详情里面的“加入购物车”按钮,可以跳转到购物车页面
修改gulimall-product模块下的item.html页面代码:
<div class="box-btns-two"><a href="http://cart.gulimall.com/addToCart">加入购物车</a></div>
修改"com.atguigu.gulimall.cart.controller.CartController"类,代码如下:
/*** 添加商品到购物车*/
@GetMapping("/addToCart")
public String addToCart() {return "sucess";
}
2、点击首页的“我的购物车”按钮,可以跳转到购物车列表
修改gulimall-product模块的index.html页面代码:
<div class="header_gw"><img src="/static/index/img/img_15.png" /><span><a href="http://cart.gulimall.com/addToCart">我的购物车</a></span><span>0</span></div>
3、实现购物车列表的首页链接跳转
修改gulimall-cart模块的cartList.html页面代码:
<ul class="header-left"><li><a href="http://gulimall.com">首页</a></li></ul><div class="one_top_left"><a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a><a href="#" class="one_left_link">购物车</a></div>
4、实现加入购物车成功页的首页链接跳转
修改gulimall-cart模块的success.html页面代码:
<ul class="hd_wrap_left"><li class="hd_home"><i class="glyphicon glyphicon-home"></i><a href="http://gulimall.com">谷粒商城首页</a></li></ul><div class="nav_top"><div class="nav_top_one"><a href="http://gulimall.com"><img src="/static/cart/img/logo1.jpg" style="height: 60px;width:180px;"/></a></div><div class="nav_top_two"><input type="text"/><button>搜索</button></div></div>
5、实现加入购物车成功页的“查看商品详情”和“去购物车结算”链接跳转
修改gulimall-cart模块的success.html页面代码:
<div class="bg_shop"><a class="btn-tobback" href="'http://item.gulimall.com/10.html'">查看商品详情</a><a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"id="GotoShoppingCart"><b></b>去购物车结算</a></div>
6、添加商品到购物车
在gulimall-product模块,修改“加入购物车”按钮
思路:
点击加入购物车要判断是哪个商品(skuid)和数量num
item.html
<div class="box-btns clear"><div class="box-btns-one"><input type="text" name="" id="productNum" value="1"/><div class="box-btns-one1"><div><button id="jia">+</button></div><div><button id="jian">-</button></div></div></div><div class="box-btns-two"><a id="addToCart" th:attr="skuId=${item.info.getSkuI}" >加入购物车</a></div><div class="box-btns-three"><img src="/static/item/img/e4ed3606843f664591ff1f68f7fda12d.png" alt=""/> 分享</div></div>
$("#addToCart").click(function () {var num = $("#numInput").val();var skuId = $(this).attr("skuId");location.href = "http://cart.gulimall.com/addToCart?skuId=" + skuId + "&num=" + num;return false;
});
修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:
注意:
cartService.addToCart(skuId,num);需要返回购物项信息。
/*** 添加商品到购物车*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,@RequestParam("num") Integer num,Model model) {CartItem cartItem = cartService.addToCart(skuId,num);model.addAttribute("item", cartItem);return "success";
}
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
public interface CartService {CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException;
}
修改gulimall-cart模块的加入购物车成功页success.html的代码,实现动态显示
<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="/javascript:;" target="_blank"><img style="height: 60px;width:60px;"th:src="${item.image}"src="/img/shop1.jpg"></a></div><div class="p-info"><div class="p-name"><a th:href="'http://item.gulimall1.com/'+${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.gulimall1.com/' + ${item.skuId} + '.html'">查看商品详情</a><a class="btn-addtocart" href="http://cart.gulimall1.com/cart.html"id="GotoShoppingCart"><b></b>去购物车结算</a></div></div></div><div th:if="${item == null}" class="mc success-cont"><h2>购物车中心无商品</h2><a href="http://gulimall1.com">去购物</a></div></div>
业务逻辑(思路)重要
1.若当前商品已经存在购物车,只需增添数量
2.否则需要查询商品购物(product项目中通过skuid查询)项所需信息,并添加新商品至购物车
3.还需要查询当前商品的销售组合属性(通过pms_sku_sale_attr_value表) 通过contact的sql查询
达到 {
颜色:红色,
内存:256G
}的效果
4.异步任务为避免cartItem信息为空,所以要执行完(使用allof.get()进行阻塞)再toJSONString以及put
5.判断购物车是否有此商品。有仅需数量加,没有需要添加到购物车。
修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:
@Slf4j
@Service
public class CartServiceImpl implements CartService {private final String CART_PREFIX = "gulimall:cart";@AutowiredStringRedisTemplate stringRedisTemplate;
@Autowired
ProductFeignService productFeignService;@Autowired
ThreadPoolExecutor executor;@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {// 获取我们要操作的购物车,临时购物车、用户购物车BoundHashOperations<String, Object, Object> cartOps = getCartOps();String res = (String) cartOps.get(skuId.toString());// 1、添加新商品到购物车(购物车无此商品)if (StringUtils.isEmpty(res)) {CartItem cartItem = new CartItem();/*** 异步查询*/CompletableFuture<Void> getSkuInfo = CompletableFuture.runAsync(() -> {// 1.1、远程查询要添加的商品信息R skuInfo = productFeignService.getSkuInfo(skuId);SkuInfoVo data = skuInfo.getData("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);CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {// 1.2、远程查询sku的组合信息List<String> values = productFeignService.getSkuSaleAttrValues(skuId);cartItem.setSkuAttr(values);}, executor);// 异步任务为避免cartItem信息为空,所以要执行完再toJSONString以及putCompletableFuture.allOf(getSkuInfo, getSkuSaleAttrValues).get();String jsonString = JSON.toJSONString(cartItem);cartOps.put(skuId.toString(), jsonString);return cartItem;} else {// 2、购物车有此商品,将数据取出修改数量即可CartItem cartItem = JSON.parseObject(res, CartItem.class);cartItem.setCount(cartItem.getCount() + num);// 更新redis信息cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));return cartItem;}
}/*** 获取我们要操作的购物车,临时购物车、用户购物车** @return*/
private BoundHashOperations<String, Object, Object> getCartOps() {// 得到用户信息 账号用户 、临时用户UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();// 1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户 然后决定用临时购物车还是用户购物车// 放入缓存的keyString cartKey = "";if (userInfoTo.getUserId() != null) {// gulimall:cart:1cartKey = CART_PREFIX + userInfoTo.getUserId();} else {// gulimall:cart:vq3gxcadazcdv-acz1c2-s3wefasdascartKey = CART_PREFIX + userInfoTo.getUserKey();}BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);return operations;
}
}
0、配置线程池
application.properties添加线程池配置
gulimall.thread.core= 20
gulimall.thread.max-size= 200
gulimall.thread.keep-alive-time= 10
添加“com.atguigu.gulimall.cart.config.ThreadPoolConfigProperties”类,代码如下:
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {private Integer core;private Integer maxSize;private Integer keepAliveTime;
}
添加“com.atguigu.gulimall.cart.config.MyThreadConfig”类,代码如下:
//如果ThreadPoolConfigProperties.class类没有加上@Component注解,那么我们在需要的配置类里开启属性配置的类加到容器中
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){return new ThreadPoolExecutor(pool.getCore(),pool.getMaxSize(),pool.getKeepAliveTime(),TimeUnit.SECONDS,new LinkedBlockingQueue<>(100000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}
}
1、远程查询要添加的商品信息
添加“com.atguigu.gulimall.cart.vo.SkuInfoVo”类,代码如下
@Data
public class SkuInfoVo {private Long skuId;/*** spuId*/private Long spuId;/*** sku名称*/private String skuName;/*** sku介绍描述*/private String skuDesc;/*** 所属分类id*/private Long catalogId;/*** 品牌id*/private Long brandId;/*** 默认图片*/private String skuDefaultImg;/*** 标题*/private String skuTitle;/*** 副标题*/private String skuSubtitle;/*** 价格*/private BigDecimal price;/*** 销量*/private Long saleCount;
}
添加“com.atguigu.gulimall.cart.feign.ProductFeignService”类,代码如下:
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);@GetMapping("product/skusaleattrvalue/stringlist/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}
2、远程查询sku的组合信息
修改“com.atguigu.gulimall.product.app.SkuSaleAttrValueController”类,代码如下:
@GetMapping("stringlist/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){return skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
}
修改“com.atguigu.gulimall.product.service.SkuSaleAttrValueService”,代码如下:
List getSkuSaleAttrValuesAsStringList(Long skuId);
修改“com.atguigu.gulimall.product.service.impl.SkuSaleAttrValueServiceImpl”类,代码如下:
@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {return this.baseMapper.getSkuSaleAttrValuesAsStringList(skuId);
}
修改“com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao”类,代码如下:
List getSkuSaleAttrValuesAsStringList(Long skuId);
<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">select CONCAT(attr_name,":",attr_value) from pms_sku_sale_attr_valuewhere sku_id = #{skuId}</select>
我们重启服务,准备测试,点击搜索商品页面发现,报错
报错内容如下
Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: "session.loginUser.nickname" (template: "list" - line 69, col 44)at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:393)at org.attoparser.MarkupParser.parse(MarkupParser.java:257)at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:230)... 52 more
通过查询资料了解到Thymeleaf并没有提供th:else语法,所以我们修改相关的代码
修改gulimall-search模块的list.html页面的代码
修改gulimall-search模块的list.html页面的代码
bug:不断刷新页面会一直增加数量
处理问题:不断刷新页面会一直增加数量
解决:重定向到另一个页面,只读不写(RedirectAttributes携带参数)
attributes.addFlashAttribute();将数据放在session里面可以在页面取出,但只能取一次
attributes.addAttribute("skuId",skuId); 将数据放在url后面
所以我们修改逻辑在controller的addToCart方法里添加商品,商品添加完跳转到成功页面我们改为改成重定向另一个方法,专门查询数据跳转到成功页面
修改”com.atguigu.gulimall.cart.controller.CartController“类,代码如下:
/*** 添加商品到购物车* <p>* 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 attributes) throws ExecutionException, InterruptedException {cartService.addToCart(skuId, num);attributes.addAttribute("skuId", skuId);return "redirect:http://cart.gulimall.com/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";
}
修改”com.atguigu.gulimall.cart.service.CartService“类,代码如下:
/*** 获取购物车中某个购物项** @param skuId* @return*/
CartItem getCartItem(Long skuId);
修改”com.atguigu.gulimall.cart.service.impl.CartServiceImpl“类,代码如下:
@Override
public CartItem getCartItem(Long skuId) {BoundHashOperations<String, Object, Object> cartOps = getCartOps();String s = (String) cartOps.get(skuId.toString());CartItem cartItem = JSON.parseObject(s, CartItem.class);return cartItem;
}
7、获取&合并购物车
思路:
1.若用户未登录,则直接使用user-key获取购物车数据
2.否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下
@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {// 快速得到用户信息,id,user-key// UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();Cart cart = cartService.getCart();model.addAttribute("cart", cart);return "cartList";}
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
/*** 获取整个购物车* * @return*/
Cart getCart() throws ExecutionException, InterruptedException;
修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:
@Override
public Cart getCart() throws ExecutionException, InterruptedException {Cart cart = new Cart();// 1、登录UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();if (userInfoTo.getUserId() != null) {String cartKey = CART_PREFIX + userInfoTo.getUserId();// 1.1、如果临时购物车的数据还没有合并【合并购物车】String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();// 临时购物车List<CartItem> tempsCartItems = getCartItems(tempCartKey);if (tempsCartItems != null) {// 临时购物车有数据,需要合并for (CartItem item : tempsCartItems) {addToCart(item.getSkuId(), item.getCount());}// 清除临时购物车的数据clearCart(tempCartKey);}// 1.2、获取登录后的购物车数据【包含合并过来的临时购物车的数据,和登录后的购物车数据】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;
}/*** 获取购物项* * @param cartKey* @return*/
private List<CartItem> getCartItems(String cartKey){BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);List<Object> values = operations.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;
}
清空购物车数据
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
/*** 清空购物车数据* * @param cartKey*/
void clearCart(String cartKey);
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
@Override
public void clearCart(String cartKey) {stringRedisTemplate.delete(cartKey);
}
cartList.html
<div class="one_search"><div class="one_sousuo"><div class="one_search_top"><div class="one_top_left"><a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a><a href="#" class="one_left_link">购物车</a></div><div class="one_top_right"><input type="text" class="one_right_txt" placeholder="" onfocus="this.placeholder=''" onblur="this.placeholder='' "><input type="button" value="搜索" class="one_right_btn"></div></div><div class="one_search_load" th:if="${session.loginUser == null}"><img src="/static/cart/img/shop_07.jpg" class="one_load_wraing"><span>您还没有登录!登录后购物车的商品将保存到您账号中</span><a href="#"><input type="button" onclick="login()" value="立即登录" class="one_load_btn"></a></div></div></div><div class="One_BdyShop"><div class="OneBdy_box"><div class="One_tabTop"><div class="One_Topleft"><span>全部商品 </span></div></div><div class="One_ShopTop"><ul><li><input type="checkbox" class="allCheck">全选</li><li>商品</li><li>单价</li><li>数量</li><li>小计</li><li>操作</li></ul></div><div class="One_ShopCon"><h1 th:if="${cart.items == null}">购物车还没有商品,<a href="http://gulimall.com">去购物</a></h1><ul th:if="${cart.items != null}"><li th:each="item:${cart.items}"><div></div><div><ol><li><input type="checkbox" class="check" th:checked="${item.check}"></li><li><dt><img th:src="${item.image}" alt=""></dt><dd style="width: 300px"><p><span th:text="${item.title}">TCL 55A950C 55英寸32核</span><br><span th:each="attr:${item.skuAttr}" th:text="${attr}">尺码: 55时 超薄曲面 人工智能</span></p></dd></li><li><p class="dj" th:text="'¥'+${#numbers.formatDecimal(item.price,3,2)}">4599.00</p></li><li><p><span>-</span><span th:text="${item.count}">5</span><span>+</span></p></li><li style="font-weight:bold"><p class="zj">¥[[${#numbers.formatDecimal(item.totalPrice,3,2)}]]</p></li><li><p>删除</p></li></ol></div></li></ul></div><div class="One_ShopFootBuy fix1"><div><ul><li><input type="checkbox" class="allCheck"><span>全选</span></li><li>删除选中的商品</li><li>移到我的关注</li><li>清除下柜商品</li></ul></div><div><font style="color:#e64346;font-weight:bold;" class="sumNum"> </font> <ul><li><img src="img/buyNumleft.png" alt=""></li><li><img src="img/buyNumright.png" alt=""></li></ul></div><div><ol><li>总价:<span style="color:#e64346;font-weight:bold;font-size:16px;" class="fnt">¥[[${#numbers.formatDecimal(cart.totalAmount,3,2)}]]</span></li><li>优惠:<span style="color:#e64346;font-weight:bold;font-size:16px;" class="fnt">[[${#numbers.formatDecimal(cart.reduce,1,2)}]]</span></li></ol></div><div><button onclick="toTrade()" type="button">去结算</button></div></div></div></div><div class="One_isDel"><p><span>删除</span><span><img src="img/错误.png" alt=""></span></p><div><dl><dt><img src="img/感叹三角形 (2).png" alt=""></dt><dd><li>删除商品?</li><li>您可以选择移到关注,或删除商品。</li></dd></dl></div><div><button type="button">删除</button></div></div><div class="One_moveGzIfNull"><p><span>删除</span><span><img src="img/错误.png" alt=""></span></p><dl><dt><img src="img/感叹三角形 (2).png" alt=""></dt><dd>请至少选中一件商品!</dd></dl></div>
function login() {window.location.href = "http://auth.gulimall.com/login.html";}
8、选中购物车项
修改cartList.html
<li><input type="checkbox" th:attr="skuId=${item.skuId}" class="itemCheck" th:checked="${item.check}"></li>
$(".itemCheck").click(function(){var skuId = $(this).attr("skuId");var check = $(this).prop("checked");location.href = "http://cart.gulimall.com/checkItem?skuId=" + skuId + "&check=" + (check? 1 : 0);})
修改“com.atguigu.gulimall.cart.controller.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";}
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
/*** 勾选购物项* @param skuId* @param check*/
void checkItem(Long skuId, Integer check);
修改“com.atguigu.gulimall.cart.service.impl.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 jsonString = JSON.toJSONString(cartItem);cartOps.put(skuId.toString(), jsonString);
}/*** 获取我们要操作的购物车,临时购物车、用户购物车** @return*/
private BoundHashOperations<String, Object, Object> getCartOps() {// 得到用户信息 账号用户 、临时用户UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();// 1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户 然后决定用临时购物车还是用户购物车// 放入缓存的keyString cartKey = "";if (userInfoTo.getUserId() != null) {cartKey = CART_PREFIX + userInfoTo.getUserId();} else {cartKey = CART_PREFIX + userInfoTo.getUserKey();}BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);return operations;
}
9、修改购物项数量
修改CartList.html
<li><p th:attr="skuId=${item.skuId}"><span class="countOpsBtn">-</span><span class="countOpsNum" th:text="${item.count}">5</span><span class="countOpsBtn">+</span></p></li>
js文件
$(".countOpsBtn").click(function () {const skuId = $(this).parent().attr("skuId");const num = $(this).parent().find(".countOpsNum").text();location.href = "http://cart.gulimall1.com/changeItemCount?skuId=" + skuId + "&num=" + num;});
修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:
@GetMapping("/changeItemCount")
public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num){cartService.changeItemCount(skuId,num);return "redirect:http://cart.gulimall.com/cart.html"}
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
/*** 修改购物项数量* * @param skuId* @param num*/
void changeItemCount(Long skuId, Integer num);
修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:
@Override
public void changeItemCount(Long skuId, Integer num) {CartItem cartItem = getCartItem(skuId);cartItem.setCount(num);BoundHashOperations<String, Object, Object> cartOps = getCartOps();cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
10、删除购物车项
<li><p class="deleteItemBtn" th:attr="skuId=${item.skuId}">删除</p></li>
修改cartList.html
<div class="One_isDel"><p><span>删除</span><span><img src="img/错误.png" alt=""></span></p><div><dl><dt><img src="img/感叹三角形 (2).png" alt=""></dt><dd><li>删除商品?</li><li>您可以选择移到关注,或删除商品。</li></dd></dl></div><div><button type="button" onclick="deleteItem()">删除</button></div></div>
/*** 删除购物项*/var deleteId = 0;$(".deleteItemBtn").click(function(){deleteId = $(this).attr("skuId");})function deleteItem() {location.href = "http://cart.gulimall.com/deleteItem?skuId=" + deleteId;}
修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){cartService.deleteItem(skuId);return "redirect:http://cart.gulimall.com/cart.html";
}
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
/*** 删除购物项* @param skuId*/
void deleteItem(Long skuId);
修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:
@Override
public void deleteItem(Long skuId) {BoundHashOperations<String, Object, Object> cartOps = getCartOps();cartOps.delete(skuId.toString());
}