目录
这里SpingBoot版本2.7.8、java版本1.8,与其他模块保持一致。
<?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.7.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wen.gulimall</groupId>
<artifactId>gulimall-cart</artifactId>
<version>1.0</version>
<name>gulimall-cart</name>
<description>购物车服务</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.wen.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0</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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
端口、nacos服务中心、服务名、redis。
server:
port: 30000
spring:
cloud:
nacos:
discovery:
server-addr: xxx.xxx.xxx.10:8848
application:
name: gulimall-cart
redis:
host: xxx.xxx.xxx.10
nacos配置中心、服务名。
spring:
cloud:
nacos:
config:
server-addr: xxx.xxx.xxx.10:8848
namespace: b4932580-8b13-4c64-90f6-7b868942b603
application:
name: gulimall-cart
开启服务发现和远程调用。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallCartApplication.class, args);
}
}
增加购物车服务ip和域名映射:xxx.xxx.xxx.10? cart.gulimall.com
- id: gulimall_cart_route
uri: lb://gulimall-cart
predicates:
# 由以下的主机域名访问转发到购物车服务
- Host=cart.gulimall.com
注意: SpringSession的相关依赖和配置类GulimallSessionConfig.java都在公共模块(gulimall-common)已经配置,这里购物车模块直接引入公共模块即可。
公共模块(gulimall-common)可以参考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 认证服务【分布式高级篇四】-CSDN博客
gulimall-cart/src/main/java/com/wen/gulimall/cart/GulimallCartApplication.java
购物车主类上添加以下注解:?
@EnableRedisHttpSession // 整合redis作为session存储
1. 将购物车的静态资源放到nginx相关目录/root/docker/nginx/html/static/cart下;
2. 将购物车相关页面cartList.html和success.html复制到gulimall-cart/src/main/resources/templates下;
3. 将cartList.html和index.html页面的src和href以/static/cart/开头;
4. 将index.html页面的thymeleaf相关th:先删除;
5. 修改index.html页面的谷粒商城和谷粒商城首页的超链接的href属性值修改为http://gulimall.com。
(1)用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
????????登录以后, 会将临时购物车的数据全部合并过来, 并清空临时购物车;
(2)用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
????????浏览器即使关闭, 下次进入, 临时购物车数据都在。
相关功能:
- 用户可以使用购物车一起结算下单
- 给购物车添加商品
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量。
- 用户可以在购物车中删除商品。
- 选中不选中商品
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化
注意:当前JD登录后才可以添加商品。
????????由于购物车是一个读多写多的场景,存放在数据库并不合适,但是购物车的数据需要持久化,这里我们选择读写速度快的redis,不用担心redis宕机,redis安装时配置持久化到本地磁盘。
? ? ? ? 购物车中,每一个购物项信息,都是一个对象,购物车中不止一条数据,最终会是对象数组,但是使用?list?存储并不合适,因为 list 存储查找某个购物项时需要进行遍历,耗费大量时间,所以这里使用 hash 存储。
? ? ? ? (1)第一层Map,Key是用户id;
? ? ? ? (2)第二层Map,Key是购物车中的商品id,值是购物项数据。
参照之前的京东:
user-key 是随机生成的 id, 不管有没有登录都会有这个 cookie 信息。
(1)两个功能: 新增商品到购物车、 查询购物车。
(2)新增商品: 判断是否登录
(3)查询购物车列表: 判断是否登录
????????- 有: 需要提交到后台添加到 redis, 合并数据, 而后查询。
????????- 否: 直接去后台查询 redis, 而后返回。
gulimall-cart/src/main/java/com/wen/gulimall/cart/vo/CartItemVo.java
public class CartItemVo {
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;
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;
}
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/vo/CartVo.java
/**
* @description 购物车
* 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
*/
public class CartVo {
private 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(CollUtil.isNotEmpty(items)){
for (CartItemVo item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
return CollUtil.isNotEmpty(items)?items.size():0;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
// 1. 计算购物项总价
if(CollUtil.isNotEmpty(items)){
for (CartItemVo item : items) {
amount = amount.add(item.getTotalPrice());
}
}
// 2. 减去优惠总价
BigDecimal subtract = amount.subtract(getReduce());
return subtract;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/vo/UserInfoTo.java
封装ThreadLocal中传输的用户信息。
@Data
public class UserInfoTo {
private Long userId;
private String userKey; // 关联购物车
private boolean tempUser =false; // 是否是临时用户
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/vo/SkuInfoVo.java?
@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;
}
ThreadLocal同一个线程共享数据。
????????确保商城首页、搜索服务、商品详情、购物车服务页面可以连贯起来。此处省略修改。具体代码见Gitee - 基于 Git 的代码托管和研发协作平台。
自定义购物车拦截器:
(1)所有请求执行之前:拦截所有请求给每个线程封装UserInfoTo对象;
(2)所有请求执行之后:业务执行之后:分配临时用户,让浏览器保存。
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
/**
* @author W
* @createDate
* @description 自定义拦截器需要添加到webmvc中,否则不起作用
*/
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
/**
* 业务执行之前
* 拦截所有请求给每个线程封装UserInfoTo对象
* 1、从session中获取MemberResponseVo != null(登录状态),为UserInfoTo设置userId
* 2、从request中获取cookie,找到user-key的value,为UserInfoTo设置userKey和tempUser
* 目标方法执行之前:在ThreadLocal中存入用户信息【同一个线程共享数据】
* 从session中获取数据【使用session需要cookie中的GULISESSION 值】
* @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();
MemberRespVo memberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(memberRespVo!=null){
// 用户登录
userInfoTo.setUserId(memberRespVo.getId());
}
Cookie[] cookies = request.getCookies();
if(ArrayUtil.isNotEmpty(cookies)){
for (Cookie cookie : cookies) {
String name = cookie.getName();
if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}
// 如果没有临时用户,分配一个临时用户
if(StrUtil.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
threadLocal.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 {
// 设置Cookie :user-key标识用户身份,一个月过期
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);
}
}
}
????????自定义线程池用于添加购物车时多个远程调用比较消耗时间,使用CompletableFuture异步编排多线程来节省时间。
gulimall-cart/src/main/java/com/wen/gulimall/cart/config/ThreadPoolConfigProperties.java
@Data
@Component
@ConfigurationProperties(prefix = "gulimall.thread")
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
gulimall-cart/src/main/resources/application.yml
gulimall:
thread:
core-size: 20
max-size: 200
keep-alive-time: 10
/**
* @description 线程池配置
* 注意:线程池属性类ThreadPoolConfigProperties已经使用了@Component放入容器中,不需要在使用@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
*/
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties poolProperties){
return new ThreadPoolExecutor(poolProperties.getCoreSize(),
poolProperties.getMaxSize(),
poolProperties.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
????????添加购物车需要远程调用商品服务(gulimall-product)根据skuId获取商品详情信息和商品的销售属性组合。
gulimall-cart/src/main/java/com/wen/gulimall/cart/feign/ProductFeignService.java
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R info(@PathVariable("skuId") Long skuId);
@GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
List<String> getSkuSaleAttrValues(@PathVariable Long skuId);
}
注意:商品服务(gulimall-product)新增根据skuId获取商品销售属性组合接口;根据skuId获取商品详情信息接口已存在,直接使用。
gulimall-product/src/main/java/com/wen/gulimall/product/app/SkuSaleAttrValueController.java
@RestController
@RequestMapping("product/skusaleattrvalue")
public class SkuSaleAttrValueController {
@Autowired
private SkuSaleAttrValueService skuSaleAttrValueService;
@GetMapping("/stringlist/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable Long skuId){
return skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
}
...
}
gulimall-product/src/main/java/com/wen/gulimall/product/service/SkuSaleAttrValueService.java
public interface SkuSaleAttrValueService extends IService<SkuSaleAttrValueEntity> {
...
List<String> getSkuSaleAttrValuesAsStringList(Long skuId);
}
gulimall-product/src/main/java/com/wen/gulimall/product/service/impl/SkuSaleAttrValueServiceImpl.java
@Service("skuSaleAttrValueService")
public class SkuSaleAttrValueServiceImpl extends ServiceImpl<SkuSaleAttrValueDao, SkuSaleAttrValueEntity> implements SkuSaleAttrValueService {
...
@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
return this.baseMapper.getSkuSaleAttrValuesAsStringList(skuId);
}
}
gulimall-product/src/main/java/com/wen/gulimall/product/dao/SkuSaleAttrValueDao.java
@Mapper
public interface SkuSaleAttrValueDao extends BaseMapper<SkuSaleAttrValueEntity> {
...
List<String> getSkuSaleAttrValuesAsStringList(Long skuId);
}
?gulimall-product/src/main/resources/mapper/product/SkuSaleAttrValueDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wen.gulimall.product.dao.SkuSaleAttrValueDao">
...
<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
SELECT CONCAT(attr_name,':',attr_value) FROM pms_sku_sale_attr_value WHERE sku_id = #{skuId}
</select>
</mapper>
? ? ? ? 添加购物车功能不能用转发,使用转发地址栏不变,刷新浏览器会改变商品数量,参照jd,添加购物车成功应重定向到success.html页面。
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
@Controller
public class CartController {
@Resource
private CartService cartService;
...
/**
* 加入购物车
*
* RedirectAttributes attributes
* attributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
* attributes.addAttribute():将数据放在请求域中(转发),或将数据拼接到url的后面(重定向)
* @return
*/
@GetMapping("/addToCart")
public String addToCart(Long skuId, Integer num, RedirectAttributes attributes){
cartService.addToCart(skuId,num);
// 转发放到请求域中,重定向拼接到地址的后面
attributes.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
/**
* 跳转到成功页
* @param skuId
* @return
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(Long skuId, Model model){
// 重定向到成功页面,再次查询购物车数据即可
CartItemVo item = cartService.getCartItem(skuId);
model.addAttribute("item",item);
return "success";
}
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java
public interface CartService {
/**
* 将商品添加到购物车
* @param skuId
* @param num
* @return
*/
CartItemVo addToCart(Long skuId, Integer num);
/**
* 获取购物车中某个购物项
* @param skuId
* @return
*/
CartItemVo getCartItem(Long skuId);
}
用户key存储到redis的前缀,在购物车常量类进行定义。
gulimall-common/src/main/java/com/wen/common/constant/CartConstant.java
public class CartConstant {
...
public static final String CART_PREFIX = "gulimall:cart:";
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java?
@Service
public class CartServiceImpl implements CartService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ProductFeignService productFeignService;
@Resource
private ThreadPoolExecutor executor;
/**
* 1. 多个远程调用比较慢就需要使用多线程,一个服务一个线程池
* @param skuId
* @param num
* @return
*/
@Override
public CartItemVo addToCart(Long skuId, Integer num) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
Object o = cartOps.get(skuId.toString());
if(o==null){
// 新增商品
CartItemVo cartItemVo = new CartItemVo();
// 1. 远程查询要添加的商品信息skuInfo
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
R info = productFeignService.info(skuId);
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItemVo.setSkuId(skuId);
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setCheck(true);
cartItemVo.setCount(num);
},executor);
// 2. 远程查询sku的销售属性组合信息
CompletableFuture<Void> getSkuSaleAttrValuesTask = CompletableFuture.runAsync(() -> {
List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
cartItemVo.setSkuAttr(skuSaleAttrValues);
},executor);
try {
CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValuesTask).get();// 等待所有异步任务完成
String s = JSON.toJSONString(cartItemVo);
// 将购物项信息放在redis
cartOps.put(skuId.toString(),s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return cartItemVo;
}else{
// 购物车由此商品,修改数量
String res = o.toString();
CartItemVo cartItemVo = JSON.parseObject(res, CartItemVo.class);
cartItemVo.setCount(cartItemVo.getCount()+num);
// 更新redis中的数据
cartOps.put(skuId.toString(),JSON.toJSONString(cartItemVo));
return cartItemVo;
}
}
@Override
public CartItemVo getCartItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String str = (String) cartOps.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(str, CartItemVo.class);
return cartItemVo;
}
/**
* 获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey = "";
if(userInfoTo.getUserId()!=null){
// 用户登录
cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
}else {
// 临时用户
cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);
return operations;
}
}
商品详情页点击加入购物车需要带上skuId和商品数量。
1. 商品详情页路径gulimall-product/src/main/resources/templates/item.html
2. 修改的地方
(1)给商品数量输入框添加id="numInput",方便获取商品的数量,如下
<div class="box-btns-one">
<input type="text" name="" id="numInput" value="1" />
<div class="box-btns-one1">
<div>
<button id="jia">
+
</button>
</div>
<div>
<button id="jian">
-
</button>
</div>
</div>
</div>
(2)当点击购物车时带上skuId和商品数量num
1)给加入购物车按钮添加id="addToCartA",添加skuId属性,如下:
<div class="box-btns-two">
<a href="#" id="addToCartA" th:attr="skuId=${item.info.skuId}">
加入购物车
</a>
</div>
2)单击按钮后的操作,如下:
$("#addToCartA").click(function (){
var num = $("#numInput").val();
var skuId = $(this).attr("skuId");
location.href = "http://cart.gulimall.com/addToCart?skuId="+skuId+"&num="+num;
});
gulimall-cart/src/main/resources/templates/success.html
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
@Controller
public class CartController {
@Resource
private CartService cartService;
/**
* 浏览器有一个Cookie:user-key;用来标识用户身份,一个月后过期;
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
* 浏览器以后保存,每次访问都会带上这个Cookie。
*
* 登录:session里有loginUser
* 未登录:按照cookie里面的user-key来做。
* 第一次:如果没有临时用户,创建一个临时用户。
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(Model model){
CartVo cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
...
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java
public interface CartService {
/**
* 将商品添加到购物车
* @param skuId
* @param num
* @return
*/
CartItemVo addToCart(Long skuId, Integer num);
/**
* 获取购物车中某个购物项
* @param skuId
* @return
*/
CartItemVo getCartItem(Long skuId);
/**
* 获取整个购物车
* @return
*/
CartVo getCart();
/**
* 清空购物车数据
* @param cartKey
*/
void clearCart(String cartKey);
}
?gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java
@Service
public class CartServiceImpl implements CartService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ProductFeignService productFeignService;
@Resource
private ThreadPoolExecutor executor;
/**
* 1. 多个远程调用比较慢就需要使用多线程,一个服务一个线程池
* @param skuId
* @param num
* @return
*/
@Override
public CartItemVo addToCart(Long skuId, Integer num) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
Object o = cartOps.get(skuId.toString());
if(o==null){
// 新增商品
CartItemVo cartItemVo = new CartItemVo();
// 1. 远程查询要添加的商品信息skuInfo
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
R info = productFeignService.info(skuId);
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItemVo.setSkuId(skuId);
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setCheck(true);
cartItemVo.setCount(num);
},executor);
// 2. 远程查询sku的销售属性组合信息
CompletableFuture<Void> getSkuSaleAttrValuesTask = CompletableFuture.runAsync(() -> {
List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
cartItemVo.setSkuAttr(skuSaleAttrValues);
},executor);
try {
CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValuesTask).get();// 等待所有异步任务完成
String s = JSON.toJSONString(cartItemVo);
// 将购物项信息放在redis
cartOps.put(skuId.toString(),s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return cartItemVo;
}else{
// 购物车由此商品,修改数量
String res = o.toString();
CartItemVo cartItemVo = JSON.parseObject(res, CartItemVo.class);
cartItemVo.setCount(cartItemVo.getCount()+num);
// 更新redis中的数据
cartOps.put(skuId.toString(),JSON.toJSONString(cartItemVo));
return cartItemVo;
}
}
...
@Override
public CartVo getCart() {
CartVo cartVo = new CartVo();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if(userInfoTo.getUserId()!=null){
// 1. 登录
String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
// 2. 如果临时购物车的数据还没有合并【合并购物车】
String tempCartKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
List<CartItemVo> tempCartItems = getCartItems(tempCartKey);
if(tempCartItems!=null){
// 临时购物车有数据,需要合并
for (CartItemVo itemVo : tempCartItems) {
addToCart(itemVo.getSkuId(),itemVo.getCount());
}
// 清空临时购物车的数据
clearCart(tempCartKey);
}
// 3. 获取登录后的购物车的数据【包含合并过来的临时数据,和登录后的购物车的数据】
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
}else {
// 2. 临时登录
String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
//获取临时购物车的购物项
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
}
return cartVo;
}
/**
* 清空购物车
* @param cartKey
*/
@Override
public void clearCart(String cartKey) {
stringRedisTemplate.delete(cartKey);
}
...
/**
* 获取购物项
* @param cartKey
* @return
*/
private List<CartItemVo> getCartItems(String cartKey){
BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if(values!=null && values.size()>0){
List<CartItemVo> collect = values.stream().map(obj -> {
String str = (String) obj;
return JSON.parseObject(str, CartItemVo.class);
}).collect(Collectors.toList());
return collect;
}
return null;
}
}
gulimall-cart/src/main/resources/templates/cartList.html
gulimall-cart/src/main/resources/templates/cartList.html
1. 将选中未选中的input框的class属性改为itemChecked;
2. 给选中未选中的input框添加skuId属性,可以判断是哪个商品的选中未选中;
3. 给选中未选中的input框添加单击事件,获取skuId和checked的属性值,请求后端改变状态,如下:
$(".itemChecked").click(function () {
const skuId = $(this).attr("skuId");
const checked = $(this).prop("checked");
location.href = "http://cart.gulimall.com/checkItem?skuId=" + skuId + "&check=" + (checked ? 1 : 0);
});
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
@Controller
public class CartController {
@Resource
private CartService cartService;
@GetMapping("/checkItem")
public String checkItem(Long skuId,Integer check){
cartService.checkItem(skuId,check);
return "redirect:http://cart.gulimall.com/cart.html";
}
...
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java
public interface CartService {
...
/**
* 勾选购物项
* @param skuId
* @param check
*/
void checkItem(Long skuId, Integer check);
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java
@Service
public class CartServiceImpl implements CartService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ProductFeignService productFeignService;
@Resource
private ThreadPoolExecutor executor;
...
@Override
public void checkItem(Long skuId, Integer check) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
CartItemVo cartItem = getCartItem(skuId);
cartItem.setCheck(check == 1);
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
}
...
}
gulimall-cart/src/main/resources/templates/cartList.html
点击添加数量或减少数量触发单击事件,如下:
$(".countOpsBtn").click(function () {
const skuId = $(this).parent().attr("skuId");
const num = $(this).parent().find(".countOpsNum").text();
location.href = "http://cart.gulimall.com/countItem?skuId=" + skuId + "&num=" + num;
});
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
@Controller
public class CartController {
@Resource
private CartService cartService;
@GetMapping("/countItem")
public String countItem(Long skuId,Integer num){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
...
}
?gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java
public interface CartService {
...
/**
* 改变购物项的数量
* @param skuId
* @param num
*/
void changeItemCount(Long skuId, Integer num);
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java
@Service
public class CartServiceImpl implements CartService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ProductFeignService productFeignService;
@Resource
private ThreadPoolExecutor executor;
...
@Override
public void changeItemCount(Long skuId, Integer num) {
CartItemVo cartItem = getCartItem(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
...
}
gulimall-cart/src/main/resources/templates/cartList.html
点击删除按钮时将skuId放到全局,等点击确定删除按钮时根据skuId区分要删除的商品,如下:
let deleteId = 0;
$(".deleteItemBtn").click(function () {
deleteId = $(this).attr("skuId");
});
//删除购物车选项
function deleteItem() {
location.href = "http://cart.gulimall.com/deleteItem?skuId=" + deleteId;
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
@Controller
public class CartController {
@Resource
private CartService cartService;
@GetMapping("/deleteItem")
public String deleteItem(Long skuId){
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}
...
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java
public interface CartService {
...
/**
* 删除购物项
* @param skuId
*/
void deleteItem(Long skuId);
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java?
@Service
public class CartServiceImpl implements CartService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ProductFeignService productFeignService;
@Resource
private ThreadPoolExecutor executor;
...
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}
...
}