谷粒商城篇章8 ---- P236-P247 ---- 购物车【分布式高级篇五】

news2024/11/12 13:21:53

目录

1 环境搭建

1.1 新建购物车服务模块gulimall-cart

1.2 购物车服务相关配置

1.2.1 pom.xml 

1.2.2 yml配置

1.2.2.1 application.yml配置

1.2.2.2 bootstrap.yml配置

1.2.3 主类

1.3 SwitchHosts增加配置

1.4 网关配置

1.5 整合SpringSession

1.5.1 session数据存储在redis

1.5.2 开启SpringSession

1.6 购物车页面搭建

2 购物车需求

3 数据模型分析

3.1 数据存储方式

3.2 数据存储结构

4 购物车流程分析

5 购物车功能实现

5.1 VO/TO编写

5.1.1 购物项CartItemVo

5.1.2 购物车CartVo

5.1.3 用户信息UserInfoTo

5.1.4 商品信息SkuInfoVo

5.2 ThreadLocal用户身份鉴别

5.3 页面环境搭建

5.4 添加购物车

5.4.1 自定义拦截器

5.4.2 自定义线程池

5.4.2.1 线程池属性配置类

5.4.2.2 yml中配置线程池

5.4.2.3 自定义线程池

5.4.3 远程调用接口

5.4.3.1 远程调用的openFeign接口

5.4.3.2 商品服务新增获取商品销售属性组合接口

5.4.3.2.1 controller层

5.4.3.2.2 service层

5.4.3.2.3 dao层

5.4.4 添加购物车功能实现

5.4.4.1 controller层

5.4.4.2 service层

5.4.4.3 页面修改

5.4.4.3.1 商品详情页修改

5.4.4.3.2 加入购物车成功页修改

5.4.5 获取&合并购物车

5.4.5.1 controller层

5.4.5.2 service层

5.4.5.3 页面调整cartList.html

5.4.6 选中购物项

5.4.6.1 页面修改

5.4.6.2 controller层 

5.4.6.3 service层

5.4.7 改变购物项数量

5.4.7.1 页面修改

5.4.7.2 controller层

5.4.7.3 service层

5.4.8 删除购物项

5.4.8.1 页面修改

5.4.8.2 controller层

5.4.8.3 service层


1 环境搭建

1.1 新建购物车服务模块gulimall-cart

这里SpingBoot版本2.7.8、java版本1.8,与其他模块保持一致。

1.2 购物车服务相关配置

1.2.1 pom.xml 

<?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>

1.2.2 yml配置

1.2.2.1 application.yml配置

端口、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
1.2.2.2 bootstrap.yml配置

nacos配置中心、服务名。

spring:
  cloud:
    nacos:
      config:
        server-addr: xxx.xxx.xxx.10:8848
        namespace: b4932580-8b13-4c64-90f6-7b868942b603
  application:
    name: gulimall-cart

1.2.3 主类

开启服务发现和远程调用。

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {

	public static void main(String[] args) {
		SpringApplication.run(GulimallCartApplication.class, args);
	}

}

1.3 SwitchHosts增加配置

增加购物车服务ip和域名映射:xxx.xxx.xxx.10  cart.gulimall.com

1.4 网关配置

- id: gulimall_cart_route
  uri: lb://gulimall-cart
  predicates:
    # 由以下的主机域名访问转发到购物车服务
    - Host=cart.gulimall.com

1.5 整合SpringSession

注意: SpringSession的相关依赖和配置类GulimallSessionConfig.java都在公共模块(gulimall-common)已经配置,这里购物车模块直接引入公共模块即可。

公共模块(gulimall-common)可以参考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 认证服务【分布式高级篇四】-CSDN博客

1.5.1 session数据存储在redis

1.5.2 开启SpringSession

gulimall-cart/src/main/java/com/wen/gulimall/cart/GulimallCartApplication.java

购物车主类上添加以下注解: 

@EnableRedisHttpSession // 整合redis作为session存储

1.6 购物车页面搭建

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。

2 购物车需求

(1)用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】

  •  放入数据库
  • mongodb
  • 放入 redis(采用)

        登录以后, 会将临时购物车的数据全部合并过来, 并清空临时购物车;
(2)用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】

  • 放入 localstorage(客户端存储, 后台不存)
  • cookie
  • WebSQL
  • 放入 redis(采用)

        浏览器即使关闭, 下次进入, 临时购物车数据都在。

相关功能:
- 用户可以使用购物车一起结算下单
- 给购物车添加商品
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量。
- 用户可以在购物车中删除商品。
- 选中不选中商品
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化

注意:当前JD登录后才可以添加商品。

3 数据模型分析

3.1 数据存储方式

        由于购物车是一个读多写多的场景,存放在数据库并不合适,但是购物车的数据需要持久化,这里我们选择读写速度快的redis,不用担心redis宕机,redis安装时配置持久化到本地磁盘。

3.2 数据存储结构

        购物车中,每一个购物项信息,都是一个对象,购物车中不止一条数据,最终会是对象数组,但是使用 list 存储并不合适,因为 list 存储查找某个购物项时需要进行遍历,耗费大量时间,所以这里使用 hash 存储。

  • 首先不同用户应该有独立的购物车, 因此购物车应该以用户的作为 key 来存储,Value 是用户的所有购物车信息。 这样看来基本的 k-v 结构就可以了。
  • 但是, 我们对购物车中的商品进行增、 删、 改操作, 基本都需要根据商品 id 进行判断,为了方便后期处理, 我们的购物车也应该是 k-v 结构, key 是商品 id, value 才是这个商品的购物车信息。
  • 综上所述, 我们的购物车结构是一个双层 Map: Map<String,Map<String,String>>

        (1)第一层Map,Key是用户id;

        (2)第二层Map,Key是购物车中的商品id,值是购物项数据。

4 购物车流程分析

参照之前的京东:

user-key 是随机生成的 id, 不管有没有登录都会有这个 cookie 信息。

(1)两个功能: 新增商品到购物车、 查询购物车。

(2)新增商品: 判断是否登录

  • 是: 则添加商品到后台 Redis 中, 把 user 的唯一标识符作为 key。
  • 否: 则添加商品到后台 redis 中, 使用随机生成的 user-key 作为 key。

(3)查询购物车列表: 判断是否登录

  • 否: 直接根据 user-key 查询 redis 中数据并展示
  • 是: 已登录, 则需要先根据 user-key 查询 redis 是否有数据。

        - 有: 需要提交到后台添加到 redis, 合并数据, 而后查询。

        - 否: 直接去后台查询 redis, 而后返回。

5 购物车功能实现

5.1 VO/TO编写

5.1.1 购物项CartItemVo

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;
    }
}

5.1.2 购物车CartVo

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;
    }
}

5.1.3 用户信息UserInfoTo

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; // 是否是临时用户
}

5.1.4 商品信息SkuInfoVo

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;
}

5.2 ThreadLocal用户身份鉴别

ThreadLocal同一个线程共享数据。

5.3 页面环境搭建

        确保商城首页、搜索服务、商品详情、购物车服务页面可以连贯起来。此处省略修改。具体代码见Gitee - 基于 Git 的代码托管和研发协作平台。

5.4 添加购物车

5.4.1 自定义拦截器

自定义购物车拦截器:

(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);
        }
    }
}

5.4.2 自定义线程池

        自定义线程池用于添加购物车时多个远程调用比较消耗时间,使用CompletableFuture异步编排多线程来节省时间。

5.4.2.1 线程池属性配置类

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;
}
5.4.2.2 yml中配置线程池

gulimall-cart/src/main/resources/application.yml

gulimall:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10
5.4.2.3 自定义线程池
/**
 * @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());

    }
}

5.4.3 远程调用接口

5.4.3.1 远程调用的openFeign接口

        添加购物车需要远程调用商品服务(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);
}
5.4.3.2 商品服务新增获取商品销售属性组合接口

注意:商品服务(gulimall-product)新增根据skuId获取商品销售属性组合接口;根据skuId获取商品详情信息接口已存在,直接使用。

5.4.3.2.1 controller层

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);
    }
    
    ...
}
5.4.3.2.2 service层

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);
    }

}
5.4.3.2.3 dao层

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>

5.4.4 添加购物车功能实现

5.4.4.1 controller层

        添加购物车功能不能用转发,使用转发地址栏不变,刷新浏览器会改变商品数量,参照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";
    }
}
5.4.4.2 service层

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;
    }
}
5.4.4.3 页面修改
5.4.4.3.1 商品详情页修改

商品详情页点击加入购物车需要带上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;
});
5.4.4.3.2 加入购物车成功页修改

gulimall-cart/src/main/resources/templates/success.html

5.4.5 获取&合并购物车

5.4.5.1 controller层

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";
    }

    ...
}
5.4.5.2 service层

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;
    }
}
5.4.5.3 页面调整cartList.html

gulimall-cart/src/main/resources/templates/cartList.html

5.4.6 选中购物项

5.4.6.1 页面修改

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);
});

5.4.6.2 controller层 

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";
    }

    ...
}
5.4.6.3 service层

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);
    }
    
    ...
}

5.4.7 改变购物项数量

5.4.7.1 页面修改

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;
    });
5.4.7.2 controller层

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";
    }
    
    ...
}
5.4.7.3 service层

 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));
    }

    ...
}

5.4.8 删除购物项

5.4.8.1 页面修改

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;
}
5.4.8.2 controller层

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";
    }
    
    ...
}
5.4.8.3 service层

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());
    }
    
    ...
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1387955.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RPA与通知机器人的完美结合

写在前面 在现代快节奏的工作环境中&#xff0c;我们经常会面临多个任务同时进行的情况&#xff0c;你还在为时间不够用、忙碌而惆怅吗&#xff1f;你还在为时刻盯着电脑流程而烦恼吗&#xff1f;你还在为及时收不到自己的自动化任务进度而焦躁吗&#xff1f;别担心&#xff0…

Elasticsearch:聊天机器人教程(二)

这是继上一篇文章 “Elasticsearch&#xff1a;聊天机器人教程&#xff08;一&#xff09;”的续篇。本教程的这一部分讨论聊天机器人实现中最有趣的方面&#xff0c;以帮助你理解它并对其进行自定义。 数据摄入 在此应用程序中&#xff0c;所有示例文档的摄取都是通过 flask …

教你用五步让千年的兵马俑跳上现代的科目三?

以下是一张我上月去西安拍的兵马俑照片&#xff1a; 使用通义千问&#xff0c;5步就能它舞动起来&#xff0c;跳上现在流行的“科目三”舞蹈。 千年兵马俑跳上科目三 全民舞王 第1步 打开通义千问App&#xff0c;我使用的是华为手机&#xff0c;苹果版的没试&#xff1b; 在…

编译 FastDFS 时报错 fatal error: sf/sf_global.h: No such file or directory 解决办法

编译 FastDFS 时&#xff0c;报错如下 gcc -Wall -D_FILE_OFFSET_BITS64 -D_GNU_SOURCE -g -O1 -DDEBUG_FLAG -c -o ../common/fdfs_global.o ../common/fdfs_global.c -I../common -I/usr/local/include In file included from ../common/fdfs_global.c:21:0: ../common/fdf…

【控制篇 / 分流】(7.4) ❀ 01. 对指定IP网段访问进行分流 ❀ FortiGate 防火墙

【简介】公司有两条宽带&#xff0c;一条ADSL拨号用来上网&#xff0c;一条移动SDWAN&#xff0c;已经连通总部内网服务器&#xff0c;领导要求&#xff0c;只有访问公司服务器IP时走移动SDWAN&#xff0c;其它访问都走ADSL拨号&#xff0c;如果你是管理员&#xff0c;你知道有…

pod 控制器

pod 控制器&#xff1a; pv pvc 动态pv pod控制器&#xff1a;工作负载&#xff0c;workload&#xff0c;用于管理pod的中间层&#xff0c;确保pod资源符号预期的状态。 预期状态&#xff1a; 1&#xff0c;副本数 2&#xff0c;容器的重启策略 3&#xff0c;镜像拉取策略…

【软件测试】前端性能测试工具原理

不同于后端性能测试知识的琐碎、独立&#xff0c;这篇文章为你介绍前端性能测试工具WebPagetest&#xff0c;以一个具体网站为例&#xff0c;和你分析WebPagetest的用法&#xff0c;以及前端性能相关的主要概念与指标。 WebPagetest功能简介 WebPagetest&#xff0c;是前端性…

STM32--7针0.96寸OLED屏幕显示(4线SPI)

本文介绍基于STM32F103C8T60.96寸OLED&#xff08;7针&#xff09;的显示&#xff08;完整程序代码见文末链接&#xff09; 一、简介 OLED&#xff0c;即有机发光二极管&#xff08; Organic Light Emitting Diode&#xff09;。 OLED 由于同时具备自发光&#xff0c;不需背光…

【RL】(task1)绪论、马尔科夫过程、动态规划、DQN(更新中)

note 文章目录 note一、马尔科夫过程二、动态规划DQN算法时间安排Reference 一、马尔科夫过程 递归结构形式的贝尔曼方程计算给定状态下的预期回报&#xff0c;这样的方式使得用逐步迭代的方法就能逼近真实的状态/行动值。 有了Bellman equation就可以计算价值函数了马尔科夫过…

day2·算法-快乐数-有效三角形个数

今天又来更新啦&#xff0c;准备蓝桥杯的小伙伴可以和我一起来刷题&#xff0c;建议大家先看题&#xff0c;整理出思路&#xff0c;再看如何用简单的写法将思路构建出来&#xff0c;然后优化细节&#xff0c;找到解决某些例外出现的方法&#xff0c;从而成功解答这道题。 快乐…

ucloud轻量云(wordpress)配置ssl

ucloud 轻量云(wordpress)配置ssl 1、上传ssl证书到/usr/local/software/apache/conf&#xff0c;这里的文件名和内容与ucloud控制台下载下来的文件名和内容保持一致 2、修改httpd.conf文件 vim /usr/local/software/apache/conf/httpd.conf 找到下面两行&#xff0c;去掉注…

SqlAlchemy使用教程(四) MetaData 与 SQL Express Language 的使用

四、Database MetaData 与 SQL Express Language 的使用 MetaData对象用于描述表结构&#xff0c;SQL Express Language是DBAPI SQL的统一封装器。MetaData 与SQL Express 语句可以在Core层使用&#xff0c;ORM层基于MetaData, SQL Express基础上做了进一步抽象。本章将介绍在…

前端框架前置学习Node.js(2)npm使用,Node.js总结

npm - 软件包管理器 定义 npm是Node.js标准的软件包管理器 npm仓库中包含大量软件包,使其成为世界上最大的单一语言代码仓,并且可以确定几乎可用于一切的软件包 最初是为了下载和管理Node.js包依赖的方式,但其现在已成为前端JavaScript中使用的工具 使用: 1.初始化清单文…

VC++读取ini文件示例2

之前学习过ini文件读写&#xff1b;继续熟悉&#xff1b; CString str1;UINT m1 0;UINT m2 0;TCHAR p1[32];m1 GetPrivateProfileString(_T("mymoney1"), _T("moneyname1"), _T("空"), p1, sizeof(p1), _T("E:\\VCPrj\\VC2015\\cattest\…

免费的域名要不要?

前言 eu.org的免费域名相比于其他免费域名注册服务&#xff0c;eu.org的域名后缀更加独特。同时&#xff0c;eu.org的域名注册也比较简单&#xff0c;只需要填写一些基本信息&#xff0c;就可以获得自己的免费域名。 博客地址 免费的域名要不要&#xff1f;-雪饼前言 eu.org…

FPGA之LUT

由于FPGA需要被反复烧写,它实现组合逻辑的基本结构不可能像ASIC那样通过固定的与非门来完成,而只能采用一种易于反复配置的结构。查找表可以很好地满足这一要求,目前主流FPGA都采用了基于SRAM工艺的查找表结构。LUT本质上就是一个RAM.它把数据事先写入RAM后,每当输入一个信号就…

【QML COOK】- 009-组件(Components)

组件对于QML来说就如同C的类一样。可以用同一个组件创建多个对象。 组件有两种定义方式&#xff1a; 用独立的.qml文件定义组件在.qml文件中用Component对象定义组件 1. 创建项目&#xff0c;新建文件IndependentComponent.qml import QtQuickRectangle {id : rootText {id…

2.3 数据链路层03

2.3 数据链路层03 2.3.7 以太网交换机 1、以太网交换机的基本功能 以太网交换机是基于以太网传输数据的交换机&#xff0c;以太网交换机通常都有多个接口&#xff0c;每个接口都可以直接与一台主机或另一个以太网交换机相连&#xff0c;一般都工作在全双工方式。 以太网交换…

go中如何进行单元测试案例

一. 基础介绍 1. 创建测试文件 测试文件通常与要测试的代码文件位于同一个包中。测试文件的名称应该以 _test.go 结尾。例如&#xff0c;如果你要测试的文件是 math.go&#xff0c;那么测试文件可以命名为 math_test.go。 2. 编写测试函数 测试函数必须导入 testing 包。每…

一文读懂「Large Language Model,LLM」大语言模型

中国大语言模型产业价值链 资料 艾瑞咨询&#xff1a;https://www.iresearch.com.cn/Detail/report?id4166&isfree0&type