谷粒商城之高级篇(2)

本文详细介绍了谷粒商城的购物车服务与订单服务的实现,包括购物车的环境搭建、数据模型分析、Vo编写、用户身份鉴别、页面环境搭建及添加、获取、合并购物车的逻辑。同时,文章讨论了订单服务的环境搭建、整合SpringSession、订单的基本概念、登录拦截、订单确认页的模型抽取、Feign远程调用中的问题及解决方案。文章中涉及到了分布式事务的幂等性处理,以及订单确认页的渲染、库存查询、运费模拟、信息显示等细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.6 购物车服务

2.6.1 环境搭建

①域名配置

1670484012821

②创建 微服务

1670483850563

暂时需要的插件

1670483921031

  • 此外,导入 公共包的依赖
        <dependency>
            <groupId>com.atguigu.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
  • application.properties配置
server.port=40000

spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

  • 主启动类加上nacos服务发现注册注解,以及后面需要用到的远程服务注解,并且因为导入了common包,需要暂时排除数据库自动配置
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

③动静分离

  • 静态资源放到 Nginx中

1670484089189

  • 动态页面放到 templates下。

1670486214726

  • 页面前缀替换:

1670484259663

1670484292158

1670484319702

  • 网关配置
        - id: gulimall_cart_route
          uri: lb://gulimall-cart
          predicates:
            - Host=cart.gulimall.com

⑦测试,为了方便,将success页面重命名为index页面

出现问题,替换:将 th:替换成空字符串

1670486459504

1670486537234

前端页面跳转

点击 图标等能返回首页。

1670486575527

1670486595060

1670486616360

2.6.2 数据模型分析

1、购物车需求
1)、需求描述:

  • 用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
    • 放入数据库
    • mongodb
    • 放入redis(采用)
      登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车;
  • 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
    • 放入localstorage(客户端存储,后台不存)
    • cookie
    • WebSQL
    • 放入redis(采用)
      浏览器即使关闭,下次进入,临时购物车数据都在
  • 用户可以使用购物车一起结算下单
  • 给购物车添加商品
  • 用户可以查询自己的购物车
  • 用户可以在购物车中修改购买商品的数量
  • 用户可以在购物车中删除商品
  • 选中不选中商品
  • 在购物车中展示商品优惠信息
  • 提示购物车商品价格变化

2)、数据结构

1670492965283

因此每一个购物项信息,都是一个对象,基本字段包括:

{
   
    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,Map<String,String>>

  • 第一层Map,Key 是用户id
  • 第二层Map,Key 是购物车中商品id,值是购物项数据

将购物车中的购物项存为list类型的话,修改起来太麻烦要从头到尾遍历。可以使用hash来存储购物车中的购物项

1670493136121

2.6.3 vo编写

购物项的Vo编写

/**购物项内容
 */
public class CartItem {
   

    private Long skuId;

    private Boolean check = true;//是否被选中

    private String title;//标题

    private String image;//图片

    private List<String> skuAttr;//销售属性组合描述

    private BigDecimal price;//商品单价

    private Integer count;//商品数量

    private BigDecimal totalPrice;//总价,总价需要计算

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

编写购车Vo

/**整个购物车
 * 需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
 */
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) {
   
                BigDecimal totalPrice = item.getTotalPrice();
                amount = amount.add(totalPrice);
            }
        }

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

2.6.4 ThreadLocal用户身份鉴别

1.将购物车数据存储至Redis中,因此,需要导入Spring整合Redis的依赖以及Redis的配置。项目上线之后,应该有一个专门的Redis负责存储购物车的数据不应该使用缓存的Redis

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置redis地址

spring.redis.host=192.168.56.10

2.编写服务层

@Slf4j
@Service
public class CartServiceImpl implements CartService {
   

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
}

3.判断用户是否登录则通过判断Session中是否有用户的数据,因此,导入SpringSession的依赖

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

配置Session

/**自定义SpringSession完成子域session共享
 * @author wystart
 * @create 2022-12-06 21:48
 */
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {
   


    //子域共享问题解决
    @Bean
    public CookieSerializer cookieSerializer() {
   


        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

        cookieSerializer.setDomainName("gulimall.com");// 扩大session作用域,也就是cookie的有效域
        cookieSerializer.setCookieName("GULISESSION");


        return cookieSerializer;


    }


    // 默认使用jdk进行序列化机制,这里我们使用json序列化方式来序列化对象数据到redis中
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
   
        return new GenericJackson2JsonRedisSerializer();
    }

}

4. cookie中的user-key说明

第一次访问京东,会给你的cookie中设置user-key标识你的身份,有效期为一个月,浏览器会保存你的user-key,以后访问都会带上

  *浏览器有一个cookie;user-key;标识用户身份,一个月后个过期;
     * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
     * 浏览器以后保存,每次访问都会带上这个cookie;
     *
     *
     * 登录:session有
     * 没登录:按照cookie里面带来user-key来做。
     * 第一次:如果没有临时用户,帮忙创建一个临时用户。
     *

5.编写To与常量

购物车服务下

@ToString
@Data
public class UserInfoTo {
   


    private Long userId;

    private String userKey;//一定要封装

    private boolean tempUser = false;//标识位


}

公共服务下:新建 CartConstant

public class CartConstant {
   


    public static final String TEMP_USER_COOKIE_NAME = "user-key";

    //过期时间为1一个月
    public static final int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;//临时用户的过期时间


}

6.编写拦截器

拦截器逻辑:业务执行之前,判断是否登录,若登录则封装用户信息,将标识位设置为true,postHandler就不再设置作用域和有效时间,否则为其创建一个user-key

注意细节:整合SpringSession之后,Session获取数据都是从Redis中获取的

使用ThreadLocal,解决线程共享数据问题,方便同一线程共享UserInfoTo

1670509051220

1670509028337

编写拦截器实现类

/**在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
 * @author wystart
 * @create 2022-12-08 20:58
 */
public class CartInterceptor implements HandlerInterceptor {
   

    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
   

        UserInfoTo userInfoTo = new UserInfoTo();

        HttpSession session = request.getSession();
        MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (member != null){
   
            //用户登录
            userInfoTo.setUserId(member.getId());
        }

        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0){
   
            for (Cookie cookie : cookies) {
   
                //user-key
                String name = cookie.getName();
                if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
   
                    userInfoTo.setUserKey(cookie.getValue());
                }
            }
        }

        //如果没有临时用户一定分配一个临时用户
        if (StringUtils.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 {
   

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

    }
}

配置拦截器,否则拦截器不生效

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
   


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   


        //配置CartInterceptor拦截器拦截所有请求
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

@Controller
public class CartController {
   


    /**
     *浏览器有一个cookie;user-key;标识用户身份,一个月后个过期;
     * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
     * 浏览器以后保存,每次访问都会带上这个cookie;
     *
     *
     * 登录:session有
     * 没登录:按照cookie里面带来user-key来做。
     * 第一次:如果没有临时用户,帮忙创建一个临时用户。
     *
     * @return
     */
    @GetMapping("/cart.html")
    public String cartListPage(){
   

        //1、快速得到用户信息,id,user-key
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoTo);

        return "cartList";

    }

}

Debug测试UserInfoTo中是否有数据

1670509402698

user-key也有

1670509457763

2.6.5 页面环境搭建

  • 首页点击购物车去购物车页面

1670511281802

index.html

1670511304093

  • 检索页面点击 我的购物车去购物车页面

1670511420137

list.html

1670511448888

  • 商品详情页修改

1670511502767

item.html

1670511522997

1670511557904

CartController

  /**
     * 添加商品到购物车
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(){
   

        return "success";
    }

  • 加入商品成功后,跳转到购物车列表页面

success.html

这里“查看商品详情暂时写死了”!!

1670511671840

1670511683481

  • 购物车详情页面

cartList.html

1670511805204

1670511831933

1670511872855

2.6.6 添加购物车

编写添加商品进入购物车的请求方法,需要知道商品的SkuId和数量

1670512328186

为加入购物车绑定单击事件,url改为#避免跳转并且设置id为超链接自定义属性,用于存储skuId

1670513076392

 <a href="#" id="addToCartA" th:attr="skuId=${item.info.skuId}">
                                    加入购物车
                                </a>

为文本框设置id

1670513111712

编写单击事件 ,$(this)指当前实例,return false : 禁止默认行为

        $("#addToCartA").click(function (){
   
            var num = $("#numInput").val();//获取数量
            var skuId = $(this).attr("skuId");//获取商品的skuId
            location.href = "http://cart.gulimall.com/addToCart?skuId="+skuId+"&num="+num;//拼接路径
            return false;//取消默认行为
        })

修改加入购物车的成功页面的显示

①默认图片的显示、商品详情页跳转以及标题显示、商品数量显示

1670575289110

购物车前缀

1670553041321

boundHashOps()方法:所有的增删改查操作只针对这个key

将其抽取成方法

选中->右击->Refactor->Extract Method

 /**
     * 获取到我们要操作的购物车
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
   
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        String cartKey = "";
        if (userInfoTo.getUserId() != null){
   
            //gulimall:cart:1  ----登录用户
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        }else{
   
            //gulimall:cart:xxxxx ----临时用户
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }

        //redisTemplate.boundHashOps(cartKey):以后关于这个cartKey就都绑定到redis中操作了
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }

远程调用product查询sku详情

1670553144088

记得主启动类要加上@EnableFeignClients:开启远程调用注解

远程调用product服务查询销售属性

①编写product服务中的查询销售属性AsString的接口

SkuSaleAttrValueController

    @GetMapping("/stringlist/{skuId}")
    public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){
   

        return skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);

    }

SkuSaleAttrValueServiceImpl

 @Override
    public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
   

        SkuSaleAttrValueDao dao = this.baseMapper;

        return dao.getSkuSaleAttrValuesAsStringList(skuId);


    }

数据库查询SQL语句

select concat(attr_name,":",attr_value)
from `pms_sku_sale_attr_value`
where sku_id = 21

1670516761878

SkuSaleAttrValueDao.xml

 <select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
        select concat(attr_name,":",attr_value)
        from `pms_sku_sale_attr_value`
        where sku_id = #{skuId}
    </select>

购物车服务远程调用接口

@FeignClient("gulimall-product")
public interface ProductFeignService {
   



    @RequestMapping("/product/skuinfo/info/{skuId}")
    //@RequiresPermissions("product:skuinfo:info")
     R getSkuInfo(@PathVariable("skuId") Long skuId);



    @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
    List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}

配置线程池提高查询效率:因为多次调用远程服务

MyThreadConfig(直接复制之前商品服务的)

@Configuration
public class MyThreadConfig {
   


    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
   

        return new ThreadPoolExecutor(pool.getCoreSize(),
                pool.getMaxSize(),pool.getKeepAliveTime(),
                TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }


}

ThreadPoolConfigProperties

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
   

    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;

}

application.properties配置

gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10

使用异步编排

①编写vo,属性从SkuInfoEntity中copy

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

②异步编排

    @Autowired
    ThreadPoolExecutor executor;

	@Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
   
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        CartItem cartItem = new CartItem();
        //异步编排
        CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
   
            //1、远程调用商品服务查询商品详情
            R skuInfo = productFeignService.getSkuInfo(skuId);
            SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
   
            });
            //2、商品添加 到购物车
            cartItem.setCheck(true);
            cartItem.setCount(num);
            cartItem.setImage(data.getSkuDefaultImg());
            cartItem.setTitle(data.getSkuTitle());
            cartItem.setSkuId(skuId);
            cartItem.setPrice(data.getPrice());
        }, executor);


        //3、远程查询sku的组合信息
        //同时调用多个远程服务,为了不影响最终的查询速度,我们可以使用多线程的方式,使用自定义的线程池提高效率
        CompletableFuture<Void> getSkuoSaleAttrValues = CompletableFuture.runAsync(() -> {
   
            List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
            cartItem.setSkuAttr(values);
        }, executor);

        CompletableFuture.allOf(getSkuInfoTask,getSkuoSaleAttrValues).get();
        String s = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),s);
        return cartItem;

    }

测试:

出现问题:

1670553817566

不能使用th:else,改为 th:if

item.html

1670553905398

list.html

1670553937661

未登录状态

1670518045017

已登录状态

1670518111464

redis中存储:cart:3代表已登录状态;cart:xxxxx代表临时用户,没有登录状态。

1670518159029

完善购物车添加细节①:之前是都认为购物车中没有要添加的此商品存在,现在要判断购物车中是否有我们要添加的此商品。也即是:上面的操作是针对添加新商品进购物车,若购物车里已存在此商品则是一个数量的叠加

@Override
    public CartItem addToCart(Long skuId
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值