当前位置:首页 » 《随便一记》 » 正文

畅购商城项目(面试版)一_Redhur-的博客

28 人参与  2021年11月11日 11:23  分类 : 《随便一记》  评论

点击全文阅读


文章目录

  • 一、关于三级目录
  • 二、使用nginx
  • 三、JMeter压测+JvisualVM监测+性能优化
  • 四、分布式缓存
      • 1、Redis
      • 2、缓存击穿、穿透、雪崩
      • 3、 加锁解决缓存击穿(本地锁)
      • 4、加锁解决缓存击穿(分布式锁)
      • 5、Redisson基本介绍
      • 6、将Redisson集成到项目里
      • 7、如何保证缓存和数据库中的数据一致?
  • 五、SpringCache
      • 1.和缓存有关的注解:
      • 2.@Cacheable注解的使用,同时它存在的问题:
      • 3.解决@Cacheable注解存在的问题
      • 4.解决@Cacheable注解存在的问题
      • 5.`@CacheEvict`、`@Cacheput`、`@chaching`注解的演示
      • 6.在配置文件中,还可以指定一些缓存的自定义配置
      • 7.SpringCache的不足
  • 阶段总结:
  • 六、ES检索查询
      • 1.前台功能
      • 2.前台传回来的检索条件
      • 3.后台返回给前台的数据
      • 4.总体逻辑:
  • 七、面包屑功能
  • 八、 商品详情页
  • 九、注册页面—验证码功能
  • 八、注册页面—注册功能
      • 给密码加密的方式
  • P219 登陆页面—用户名密码登录
  • 九、登陆页面—完成微博登录
  • 十、SpringSession—session不共享、不跨域问题
      • 1.session不能跨域问题
      • 2.分布式下session共享问题
      • 3.session共享问题的解决方案
      • 4.总说:
      • 5.修改微博登陆的代码
      • 6.修改账号密码登录的代码
  • 十一、单点登录
          • 1.为什么要单点登录?
          • 2.单点登录的原理?
  • 十二、购物车(面试版)
          • 1.添加商品到购物车
          • 2.购物车种数据的存储方式:
          • 3.展示购物车:
          • 4.更加细节的东西
  • 十四、购物车(精细版)
      • 1、关于拦截器
      • 2、添加商品到购物车
          • 7.测试:
      • 3、获取购物车
      • 4、选中购物车项
      • 5、修改购物项数量
      • 6、删除购物项

一、关于三级目录

查询数据中封装了三级目录的那张表,然后把里面的数据一次性全查出来封装到实体类List0<CategoryEntity>里面;
List0<CategoryEntity>实体类中查找parent_cid为0的就是1级分类;
查找List0<CategoryEntity>,里面parent_id为一级分类id的就是二级分类;
查找List0<CategoryEntity>,里面找parent_id为二级分类id的就是三级分类;

总之全程只查询了"pms_category"表,没有涉及到其他表,而且只查询了一遍数据库。

二、使用nginx

浏览器访问gulimall.com,本地根据在windows的hosts文件配置里找到gulimall.com映射的是虚拟机ip“192.168.56.106”,于是转发到虚拟机,虚拟机会交给nginx,nginx有一处配置专门监听gulimall.com,监听到以后根据配置转发到88端口的gulimall-gateway,网关根据断言转发到gulimall-product

三、JMeter压测+JvisualVM监测+性能优化

1.中间件的影响
前台请求先经过nginx,然后nginx交给gulimall-Gateway,然后才能到达具体的服务,中间两个中间件nginx、Gateway会不会影响性能?

2.做了哪些压测?
先压测localhost:10000,因为它没有使用中间件直接访问到首页,所以响应也挺快的;
然后压测gulimall.com,因为它有了nginx,有了Gateway,查看它的响应时间;

3.关于数据的三次查询
访问首页gulimall.com,首页会访问数据库查询数据库的三级目录,模板引擎需要将查询到的数据转交给thymeleaf然后渲染到页面,你的业务代码都会导致响应会慢很多。比如你查询三级分类时查询数据库要尽量一次拿到pms_category的所有数据,而不是一级分类查一下数据库、二级分类查一下数据库、三级分类再查一下数据库。

3.优化的方向:

1.优化JVM:测试时JvisualVM发现伊甸园区内存只有32M,超小,所以垃圾回收次数非常多,所以如果伊甸园区调整的大一些就gc的时间就减少很多,那么吞吐量也就上去了。而且老年代也很小,导致几乎就要爆满了。给gulimall-product设置一Xmx1024m -xms1024m 一Xmn512m(内存最大占用1024M,初值也时1024M,相当于内存大小固定好了就是1024M,Xmn就是伊甸园区,给伊甸园区调大到512M)

2.业务代码也很影响性能。①查询数据库次数问题。③模板的缓存问题,你开发时经常在yml中有个配置就是thymeleaf.cache: false关闭thymeleaf缓存便于调试,到了实际上线后一定要开启缓存。④优化日志级别,以前是debug,现在改为error,也就是只打印错误日志。

3.使用Redis缓存,将三级目录的数据存放到缓存里面,这就不用来一个查一个。

4.动静分离:以前我们是动态请求、静态请求都是先找nginx,然后找Gateway,然后找具体的微服务。所以我们可以把静态资源上传到nginx上面,这样静态请求只需要找到nginx就看拿到对应的资源了。

5.优化数据库:优化数据库,在查询三级目录时经常查询parent_cid,由于parent_cid不是主键没有索引导致查询起来其实很慢,你如果查询id那种主键、有索引的就会很快很快,所以给parent_cid加上索引(索引类型就是普通索引,不用选成主键索引),那么查询速度就会快很多

串起来:一个请求先经过nginx,然后经过业务代码,代码底层有JVM,另外你还优化了两个数据库(RedisMySQL

四、分布式缓存

1、Redis

1.哪些数据适合放入锾存?

  • 即时性、数据一致性要求不高的
  • 访间量大且更新频率不高的数据(读多,写少)

2.本地缓存与分布式缓存对比
本地缓存存在的问题:
(1)缓存不共享:每个服务都有一个缓存,但是这个缓存并不共享。
(2)缓存一致性问题:在一台设备上的缓存更新后,其他设备上的缓存可能还未更新,这样当从其他设备上获取数据的时候,得到的可能就是未给更新的数据。

分布式缓存:
一个服务的不同副本可以共享同一个缓存空间,可以是redis,必要时还可以使用redis集群。

2、缓存击穿、穿透、雪崩

前面我们将查询三级分类数据的查询进行了优化,将查询结果放入到Redis中,当再次获取到相同数据的时候,直接从缓存中读取,没有则到数据库中查询,并将查询结果放入到Redis缓存中

1.缓存穿透:

  • 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都是缓存是不命中然后查询数据库然后数据库也没有。
  • 解决办法就是:从数据库查询到null以后写入缓存,以后凡是请求这个资源的让缓存直接返回null,别再查询数据了。(当然缓存中存放的这个null肯定得有过期时间)

2.缓存雪崩:

  • 缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
  • 解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

3.缓存击穿

  • 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在100万请求同时进来前一秒正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿(还没来得及写入缓存数据库就被查了100来万遍)。
  • 解决:加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

简单来说:缓存穿透是指查询一个永不存在的数据(巧计:真空下光线穿透);缓存雪崩是值大面积key同时失效问题;缓存击穿是指高频key失效问题;

3、 加锁解决缓存击穿(本地锁)

本地锁在分布式情况下存在的问题

把gulimall-product复制四份,然后让JMeter大并发去访问gulimall.com,我们发现在分布式下的四个服务分别存在着四个缓存未命中的情况,也就意味着会有四次查询数据库的操作,显然我们的synchronize锁未能实现限制其他服务实例进入临界区,也就印证了在分布式情况下,本地锁只能针对于当前的服务生效。

4、加锁解决缓存击穿(分布式锁)

在Redisson出现之前我们使用的就是去Redis中占坑的方式去获得分布式锁,我们占坑的方法lock=setIfAbsent(key,value)就是如果这个key不存在的话就设置key-value,而且返回true;如果存在了就设置不了key-value,返回false

加了分布式锁解决缓存击穿,但是分布式锁存在很多需要考虑的因素:

  • 万一某个线程拿到分布式锁后执行业务逻辑时抛出异常,此时会被该线程独占这把锁——解决办法:给锁要设置过期时间
  • 加锁和给锁设置过期时间这两行代码存在时间差,万一在这个时间差内出现断电什么的也会被某个线程拿到没有被设置过期时间的锁——解决办法:加锁和设置过程时间弄成原子性操做,要么同时成功要么同时失败。
  • 删锁时发现你的锁已经过期了,你的锁已经被别的线程拿到了,别的线程就会进来,你此时如果删锁那么删掉的是别人的锁——解决办法:给锁设置uuid,每个人都不一样,你就删不了别人的锁了。
  • 上一步说到删锁时先比对uuid再删锁,期间存在时间差,万一在你比对完发现这把锁就是你的锁,正要删锁时你的锁过期了,被别的线程拿到了,你删掉的就是别人的锁。——解决办法:比对uuid和删锁必须是原子操作。
  • 一句话:加锁保证原子性,解锁保证原子性。
  • 可以看到:手写一个分布式锁很麻烦,所以我们用Redisson,Redisson就是分布式锁,而且不用再考虑上面的那些功能

5、Redisson基本介绍

1.SpringBoot整合Redisson
写一个配置类,配置类中指明Redis的地址,返回Redisson

@Configuration
 
public class MyRedisConfig {
 
    @Bean(destroyMethod="shutdown")
 
        public RedissonClient redisson() throws IOException {
 
        Config config = new Config();
 
        config.useSingleServer().setAddress("redis://192.168.137.14:6379");
 
        RedissonClient redisson = Redisson.create(config);
 
        return redisson;
    }
}

然后直接使用

RLock lock = redisson.getLock("my-lock");
lock.lock();
lock.unlock();

2.Redisson的特性
设想一种情况,一个请求线程在执行业务方法的时候,突然发生了中断,此时没有来得及执行释放锁操作,那么同时等待的另外一个线程是否会发生死锁。

在A服务在获取锁后,突然中断它的运行;等待的B服务会很快就拿到锁,不会因为A没有释放锁而被卡死。通这是因为在Redisson中会为每个锁加上“leaseTime”,默认是30秒,如果A服务宕机,到了时间就会自动释放锁。如果A服务没有宕机,而且30秒不够用,Redisson会自动给它续期。当然,人家默认的自动解锁时间是30秒,如果你改为10秒,那么10秒后立刻释放锁,不会给锁续期,但是这种自定义解锁场景也很常用,你可以自定义300秒,如果一个业务300秒都没有执行完肯定就有问题,而且我们还可以拿它评估一下业务的最大执行用时。


小结:redisson的lock具有如下特点

  • (1)阻塞式等待。默认的锁的时间是30s。
  • (2)锁定的制动续期,如果业务超长,运行期间会自动给锁续上新的30s,无需担心业务时间长,锁自动被删除的问题。
  • (3)加锁的业务只要能够运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。
  • (4)可以自定义解锁时间,时间到了不会续期,但它可以评估一下业务的最大执行用时

3.Redisson的读写锁
写+读:要等写完才能读
写+写:等前一个写完后一个才能写
读+读:相当于无锁,大家都能读
读+写:有读锁,写必须等待
读写锁适合经常读、很少写的情况,因为读的时候相当于无锁。

4.Redisson的闭锁
走完五个人就锁门,这就是闭锁

5.Redisson的信号量
车库停车,3个停车位,获取到信号量才能进去停车。

以上演示的Redisson的读写锁、闭锁、信号量都是分布式下也适用的情况。

6、将Redisson集成到项目里

public Map<String, List<Catalog2Vo>> getCatalogJson() {

        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //缓存中没有,查询数据库
            System.out.println("缓存不命中。。。。将要查询数据库。。。。");
            Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
            return catalogJsonFromDb;
        }
        
        System.out.println("缓存命中。。。。直接返回。。。。");
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {});//转为我们指定的对象
        return result;

    }
    public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedissonLock() {

		//注意锁的粒度问题
        RReadWriteLock lock = redissonClient.getReadWriteLock("catalogJson-lock");

        lock.lock();

        Map<String, List<Catalogs2Vo>> dataFromDb = null;
        try {
            dataFromDb = getCatalogJsonFromDB();//从数据库中查询三级目录,查询之前我们还要再去缓存中确定一次,如果没有才需要继续查询
        } finally {
            lock.unlock();
        }
        return dataFromDb;
    }

3.使用Redsson时应该注意锁的粒度问题
     给锁起名字要注意,不能都起一样的名字,一样的名字代表同一把锁,获取三级分类数据、获取品牌、获取属性锁到同一把锁里面,那就导致粒度很粗。假如访问三级分类是高并发的请求,访问品牌是低并发的,他俩如果同一把锁那么高并发的锁住导致低并发的也访问不到。

7、如何保证缓存和数据库中的数据一致?

  • 如何保证缓存和数据库中的数据一致?
         ①双写模式(改了数据库顺带着改了缓存)
         ②失效模式(改了数据库顺带删了缓存)
         ③双写模式/失效模式+读写锁
         ④使用Canal(MySQL中一有什么变化就会同步到缓存中来)

  • 各自的弊端:
    ①双写模式:A要把a改为1,然后B要把a改为2,最后数据库a应该是2。但是A把a改为1本来顺带改一下缓存结果卡顿了,导致B把a改为2顺带先改了缓存,然后卡顿的A改了缓存导致缓存中a是1但数据库中的a是2。高并发下缓存不一致出现了,这就又又又得加锁解决。
    ②失效模式:(A和B是写操作,改了数据库就要删缓存;C是读操作,如果缓存中读不到就得去数据库读然后写到缓存中)现在有这么一个场景:A要把a改为1,然后B要把a改为2,然后C要读取a,本来C读到的a应该是2。但是A要把a改为1然顺带删了缓存;然后B要把a改为2结果B卡顿住了;C进来读取缓存发现缓存没有数据就读数据库读到了a=1,因为缓存中没有数据所以C要把读到的a写到缓存上,但是C写缓存之前也卡顿了一下;结果现在B变流畅了,它把数据库a改为2,顺带要删缓存,结果发现缓存中还没有数据所以就不删了;现在C开始了,它把读到的a=1写到缓存中。又要加锁。
    ③双写模式/失效模式+读写锁:这个没什么问题,但是代码太复杂了吧。
    ④使用Canal:Canal是第三方的,使用起来非常方便,而且也没什么问题,但是又加了一个中间件,还得自定义一些功能,我们这个小项目就不用了。


我们系统的一致性解决方案:
实时性、一致性要求高的那就去数据库中查;
实时性、一致性要求不高的那就放到缓存中,如果害怕出现脏数据,那就给缓存加上过期时间,然后使用双写模式/失效模式+读写锁,代码很复杂,所以SpringCache应用而生

五、SpringCache

1.和缓存有关的注解:

@Cacheable:触发将数据保存到缓存的操作
@CacheEvict:触发将数据从缓存中删除的操作
@CachePut:在不影响方法执行的情况下更新缓存。
@Caching:组合以上多个操作
@CacheConfig: 在类级别上共享一些公共的与缓存相关的设置。

2.@Cacheable注解的使用,同时它存在的问题:

  • 默认的过期时间是无限时间
  • 默认的数据保存方式不是json的
  • 默认的key不好,我们要自定义存到缓存里面的key
//这是以前编写的前台访问/index时获取一级目录的方法,我们只需要在上面添加@Cacheable注解就表示如果缓存中有就不用执行下面的方法,缓存中没有就执行下面的方法查出数据并且放入缓存
@Cacheable({"catagory"})
@Override
public List<CategoryEntity> getLevel1Categorys() {
    System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

这里是引用

3.解决@Cacheable注解存在的问题

@Cacheable注解里面有些默认配置不合理,我们要自定义

  • 自定义过期时间
  • 自定义存到缓存里面的key
#在yml中指定过期时间
spring.cache.redis.time-to-live=3600000
//因为spel动态取值,所有需要额外加''表示字符串
@Cacheable(value = {"catagory"},key = "'Level1Categorys'")
@Override
public List<CategoryEntity> getLevel1Categorys() {
    System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

在这里插入图片描述

4.解决@Cacheable注解存在的问题

@Cacheable注解里面有些默认配置不合理,我们要自定义

  • 自定义数据保存方式是json,写个配置类就行了

5.@CacheEvict@Cacheput@chaching注解的演示

说明:
getLevel1Categorys()是从数据库中读取一级分类数据,getCatalogJson()是从数据库中读取三级分类数据,;
updateCascade()是更新数据库中的三级分类数据。一旦数据库中三级分类数据被更新,那么那么一级目录的数据和三级目录的数据都变了,所以需要清除getLevel1Categorys()和getCatalogJson()里面的缓存数据。
1.清除缓存的方法一

@Caching(evict = {
       @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
       @CacheEvict(value = "category",key = "'getCatalogJson'")
})
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
    ...................
}
@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
     ...................
}
@Cacheable(value = {"catagory"},key = "#root.method.name")
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
    ...................
}

1.清除缓存的方法二

@CacheEvict(value = "category",allEntries = true)       //删除某个分区下的所有数据
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
    //更新数据库同时修改缓存中的数据,
}

6.在配置文件中,还可以指定一些缓存的自定义配置

spring.cache.type=redis
 
#设置超时时间,默认是毫秒
 
spring.cache.redis.time-to-live=3600000
 
#设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀
 
spring.cache.redis.key-prefix=CACHE_
 
spring.cache.redis.use-key-prefix=true
 
#是否缓存空值,防止缓存穿透
 
spring.cache.redis.cache-null-values=true

7.SpringCache的不足

p155 缓存击穿、穿透、雪崩问题能用SpringCache解决掉吗?

(总说)先明确什么是读模式什么是写模式:

  • getLevel1Categorys()是从数据库中读取一级分类数据,getCatalogJson()是从数据库中读取三级分类数据,它们都是读模式,读模式就是从数据中读取数据,然后使用@Cacheable将数据放入缓存。
  • updateCascade()是更新数据库中的三级分类数据,他就是写模式,写模式就是更新数据库数据,数据库数据一旦被更新就需要使用@CacheEvict()注解清除缓存中的旧数据。

1)、读模式

  • 缓存穿透:并发查询一个null数据就会产生缓存穿透。SpringCache可以解决。解决方案:缓存空数据,可通过yml中的spring.cache.redis.cache-null-values=true配置来实现
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:需要加锁,使用@Cacheable(sync = true)来解决击穿问题。
  • 缓存雪崩:大量的key同时过期。解决:加随机时间。
  • 也就是说在读模式中SpringCache能够解决掉所有问题。

2)、写模式:(缓存与数据库一致)

  • 读写加锁。
  • 引入Canal,感知到MySQL的更新去更新Redis
  • 读多写多,直接去数据库查询就行

3)、总结:

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):

写模式(只要缓存的数据有过期时间就足够了,过期了让它自己更新就可以了)

特殊数据:你还想加缓存,还想保证数据库和缓存的一致性,那就需要结合Redisson来使用

阶段总结:

p136到p138是搭建了首页;
p139和p140是让我们借助nginx来通过域名访问这个首页(假如nginx肯定会使访问路线更加曲折,从而影响性能);
p141-p147是通过Jmeter、JvisualVM来分析加入Gateway、nginx这些中间件带来的性能损失,
p148-p150是进行性能优化—动静分离、JVM内存优化、代码优化
p151--p154是进行性能优化—使用Redis
p155—p158是要解决缓存击穿就需要加锁,而加本地锁不行,只能加分布式锁,但是加分布式锁又要考虑一堆分布式并发问题,于是就有了Redisson;
p159—p166是给你介绍了Redisson分布式锁的用法,但是用上Redisson后还要考虑缓存和数据库一致性问题,于是SpringCache应用而生。
p167—p172有了SpringCache,常规数据的缓存你可以不用Redisson,因为SpringCache已经考虑到缓存雪崩、击穿、穿透问题了,它里面可以加锁,可以设置过期时间等等。

从P173开始,完整的笔记全部参考这位博主写的笔记:谷粒商城-个人笔记(高级篇二)


分割线

六、ES检索查询

1.前台功能

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.前台传回来的检索条件

下面的内容必须背诵,你至少得知道你的ES检索是可以检索哪些东西啊,比如可以根据销量检索,可以根据价格检索等等

在这里插入图片描述

完整查询参数 keyword=小米&catalog3Id=1&brandId=1&hasStock=0/1&skuPrice=400_1900&at trs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏&sort=saleCount_desc/asc

@Data
public class SearchParam {

    private String keyword;//页面传递过来的全文匹配关键字
 
     //sort=saleCount_asc/desc销量
     //sort=skuPrice_asc/desc价格
     //sort=hotScore_asc/desc热度分
    private String sort;//排序条件
 
    //hasStock=0/1
    private Integer hasStock;//是否只显示有货
 
 	//skuPrice=1_500
    private String skuPrice;//价格区间查询
 
 	//brandId=2&brandId=3
    private List<Long> brandId;//按照品牌进行查询,可以多选
  
  	//catelog3Id=1
    private Long catalog3Id;//三级分类id
    
    //attr=1_3G:4G:5G;attrs=2_骁龙
    private List<String> attrs;//按照属性进行筛选
 
    private Integer pageNum = 1;//页码
}

3.后台返回给前台的数据

@Data
public class SearchResult {
 	
 	//查询到的商品信息
    private List<SkuEsModel> products;
 
 	//分页信息
    private Integer pageNum;//当前页码
    private Long total;//总记录数
    private Integer totalPages;//总页码
 
 	//所有涉及到的品牌
    private List<BrandVo> brands;
 
 	//所有涉及到的分类
    private List<CatalogVo> catalogs;
 
 	//所有涉及到的属性
    private List<AttrVo> attrs;
}

4.总体逻辑:

前台把查询的条件封装到SearchParam里面,后台根据SearchParam查询ElastiSearch,后台写的Java代码事实上就是动态的DSL语句,用DSL语句查询ElasticSearch,把查询到的结果从DSL语句中提取出来,封装到SearchResult里面返回给前台。

    @GetMapping("/list.html")
    public String listPage(SearchParam searchParam, Model model) {
        SearchResult result = mallSearchService.search(searchParam);
        System.out.println("===================="+result);
        model.addAttribute("result", result);
        return "list";
    }

七、面包屑功能

在这里插入图片描述
在这里插入图片描述

做法很简单,我们之前前台给后台传回去的SearchParam不变,但是后台返回给前台的SearchResult里面再添加一个新的字段List<NavVo> navs,在NavVo里面有navName,navValue,link这三个字段;

假如你点击了一个属性是“高清屏”,那么前台传给后台就有attrs=4_高清屏,对于NavVo里的navValue其实就是"高清屏";对于NavVo里的navName其实就是根据attrId调用gulimall-product查询属性表得到attr_name,attrId不就是4嘛;对于NavVo里的link其实就是没点面包屑之前的url,点了面包屑不就是在原先url基础上多拼装了一个attrs=4_高清屏嘛,所以你从前端拿到现在的url(也就是点了面包屑以后的url)然后切割一下就行了。



需要注意的三个点:
①因为远程调用gulimall-product所以可以在被调用的gulimall-product的那个方法上添加缓存@cacheable(value = "attr",key = " 'attrInfo'+#root.args[0]")
②如何从前端拿到现在的url?

在这里插入图片描述

③通过以上方法拿到的前端的url是被URL编码的结果&attrs=%257B%2522request%255Fid%2522%253A%25,不是你想要的url,所以你需要先解码。


分割线

八、 商品详情页

前台传回来的只有skuid,然后查询对应的表得到对应的封装信息

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

九、注册页面—验证码功能

总说:

完成用户在注册页面的发送验证码的操做:前台发送/sms/sendcode的请求给后台的gulimall-auth-server,然后gulimall-auth-server会先验证一下验证码是否在60秒前发送过(接口防刷),如果没有就使用OpenFeign远程调用gulimall-thrid-party的sendCode方法完成第三方服务的发送验证码功能。

关于接口防刷

gulimall-auth-server如何校验验证码是否在60秒前发送过?当前台带着手机号发送/sms/sendcode的请求给后台,后台先到redis中根据key为(“sms:code:”+phone)尝试获取这段redis信息,如果获取不到,后台会在redis中存储(key为"sms:code:"+phone,value为"验证码_当前时间",过期时间是10分钟)的一段信息,然后远程调用发送验证码方法;假如60秒内前台带着该手机号再次发送/sms/sendcode的请求给后台,后台先到redis中根据key为(“sms:code:”+phone)尝试获取这段redis信息,如果能够获取到这段信息就判断时间差是否小于60s,如果是就不进行发送验证码操做。

八、注册页面—注册功能

用户会填好验证码和个人的注册信息封装到UserRegistVo后发送给gulimall-auth-server
然后gulimall-auth-server首先进行JSR303校验,若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面;
若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话远程调用会员服务注册;
会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面。


编写UserRegistVo类,代码如下:

@Data
public class UserRegistVo  {
    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6, max = 18, message = "用户名必须是6-18位字符")
    private String userName;
 
    @NotEmpty(message = "密码必须填写")
    @Length(min = 6, max = 18, message = "密码必须是6-18位字符")
    private String password;
 
    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
    private String phone;
 
    @NotEmpty(message = "验证码必须填写")
    private String code;
}

编写LoginController类,下面的注释一定一定要好好看!!!

    /**
     * 下面的代码可以说相当重要,regist()方法一共有三个参数,UserRegistVo是封装前台传过来的数据,BindingResult封装JSR303校验错误信息
     * RedirectAttributes是重定向携带数据。转发的时候session共享数据,重定向的时候如何共享数据呢?
     * 使用RedirectAttributes,它利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉。
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result,
                         RedirectAttributes redirectAttributes){
        if (result.hasErrors()){

            //如果校验不通过,则封装校验结果,将错误信息封装到redirectAttributes中
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            redirectAttributes.addFlashAttribute("errors",errors);
            
            //使用return "reg"; 转发会出现重复提交的问题,不要以转发的方式
            //使用 return "forward:/reg.html"; 会出现问题:Request method 'POST' not supported的问题(原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的))
            //使用重定向  解决重复提交的问题。但面临着数据不能携带的问题,就用RedirectAttributes。
            return "redirect:http://auth.gulimall.com/reg.html";
        }
 
        //1、校验验证码
        String code = vo.getCode();
        String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(s)) {
            if (code.equals(s.split("_")[0])) {
                //验证码通过,删除缓存中的验证码;令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                //真正注册调用远程服务注册
                R r = memberFeignService.regist(vo);
                if (r.getCode() == 0) {
                    //成功
                    return "redirect:http://auth.gulimall.com/login.html";
                } else {
                    Map<String, String> errors = new HashMap<>();
                    errors.put("msg", r.getData(new TypeReference<String>() {
                    }));
                    redirectAttributes.addFlashAttribute("errors", errors);
                }
            } else {
                Map<String, String> errors = new HashMap<>();
                errors.put("code", "验证码错误");
                redirectAttributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        } else {
            Map<String, String> errors = new HashMap<>();
            errors.put("code", "验证码错误");
            redirectAttributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/reg.html";//校验出错重定向到注册页
        }
 
        //注册成功回到登录页
        return "redirect:http://auth.gulimall.com/login.html";
    }

远程调用会员服务,会员服务干了什么?会查询ums_member表的phone的数量是否大于0,如果大于0说明手机号已经存在,返回错误信息;查询ums_member表的username的数量是否大于0,如果大于0说明用户名已经存在,返回错误信息;如果ums_member表中手机号数量问0、用户名数据为0,那就把(用户名、手机号、密码)一并存入ums_member表,其中密码加密适用了BCrypt加密方式

在这里插入图片描述

给密码加密的方式

给用户密码加密的三种方式对比:MD5加密、盐值加密、BCrypt加密

可逆加密:知道了加密算法后通过密文可以推算出原来的明文
不可逆加密:即使知道了加密算法通过密文也不可以推算出原来的明文

①MD5加密:知道了密文可以推算出原来的明文,网上随处可找MD5破解

        String s = DigestUtils.md5Hex("123456");
        System.out.println(s);//e10adc3949ba59abbe56e057f20f883e

②MD5加盐(盐值加密)
可以给随机盐也可以给指定盐值,反正就是对“密码+盐值”进行MD5加密,你只能把盐值保存起来然后下一次对“密码+盐值”进行再加密然后比对密文是否一致来判断用户名密码正确与否

		String s = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq");  //$1$qqqqqqqq就是你指定的盐值
        System.out.println(s); //$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1

③BCrypt加密
Spring家的BCrypt加密,即使明文一样,每次加密的密文都不一样,但是你可以匹配明文和密文,人家就会告诉你这两个匹配与否

        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123456");//$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
        boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");
        System.out.println(matches);//true

P219 登陆页面—用户名密码登录

总说:

前台发送 /login到后台的gulimall-auth-server模块中,然后gulimall-auth-server会使用OpenFeign远程调用gulimall-member的login()方法,在该方法中根据用户名查询ums_member表拿到MemberEntity然后进行用户名密码比对完成登录,登录成功重定向到首页,登陆失败重定向到登录页

九、登陆页面—完成微博登录

1.微博登陆的流程

这里是引用

2.有两个地址很重要

(1)是“ 在登录页引导用户至授权页”的地址:
这一步是前台完成的,前台html中的url要写成

Get
https://api.weibo.com/oauth2/authorize?client_id=1917008757&response_type=code&redirect_uri=http://gulimall.com/oauth2.0/weibo/success

client_id:是你创建网站应用时的app key,
redirect_uri是用户使用微博登录后重定向到哪里去。

我们指定redirect_uri=http://gulimall.com/oauth2.0/weibo/success也就是说用户用户使用微博登录后,相当于发送 /oauth2.0/weibo/success到后台的gulimall-auth-server模块中,那么gulimall-auth-server会使用code换取token,这就涉及到换取token的url:

(2)是换取token的url
这一步是后台完成的,后台发送这样的url才能获取到token

POST
https://api.weibo.com/oauth2/access_token?client_id=1917008757&client_secret=94d9cc62c60d5f9f3d0c62389593024f&grant_type=authorization_code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success&code=CODE

client_id: 创建网站应用时的app key;
client_secret: 创建网站应用时的app secret
redirect_uri: 认证完成后的跳转链接(需要和平台高级设置一致);
code:换取令牌的认证码

后台发送这么个请求就可以根据用户授权返回的code换取token(换回来的不仅仅是token,还有uid用户id、expires_in令牌的过期时间等等,这些被封装到SocialUser中),拿到SocialUser中的token就可以向微博官方发送别的请求换取用户信息

3.微博登陆的具体流程

这里是引用

4.编码总说:

①前台带着code发送 /oauth2.0/weibo/success请求到后台的gulimall-auth-server模块中,然后gulimall-auth-server会先使用code换取SocialUser,然后拿着SocialUser到OpenFeign远程调用gulimall-memberoauth2Login()方法,在该方法中会先用SocialUser的uid查询数据库来判断用户是否是第一次用微博登录,如果是第一次的话我们就得给该用户注册(拿着token到微博里面查询该用户的基本信息,然后insert到咱们的数据库里面);如果该用户之前已经用微博登陆过,那就到数据库中更新一下token。
如果一切顺利,gulimall-member就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到RedirectAttributes然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html

②前台用户用微博登录后我们会拿到用户的code,后台用code到微博里面换取token这样才能用token访问到用户基本信息;用户每登陆一次访问微博的token就会变一次,所以当用户下次用微博登陆时我们需要到数据库更新一下token

十、SpringSession—session不共享、不跨域问题

在这里插入图片描述

之前我们学过解决跨域问题。


现在是解决的是不同域名下没办法共享session问题。共享session干什么?用户一登陆就被存在session里面了,共享session就可以实现一次登录处处生效。


我们在auth.gulimall.com登陆成功后把用户信息存到session里面,但是登陆成功会跳转到gulimall.com不是同一个微服务,我们每个微服务都有自己的域名,它们域名不一样,就没办法共享session。

1.session不能跨域问题

在这里插入图片描述

2.分布式下session共享问题

多台服务器都有会员服务,你在A服务器上把用户信息保存到内存上了,下次如果落在B服务器上,即使浏览器带着cookie来了,由于B服务器内存肯定没有存储用户信息,这也是问题。

这里是引用

3.session共享问题的解决方案

  1. session复制
    用户登录后,A服务器得到session后,把session也复制到别的机器上,显然这种处理很不好

  2. 客户端存储
    把session存储到浏览器上,肯定相当不安全

  3. hash一致性
    根据用户,到指定的机器上登录。但是远程调用还是不好解决

  4. redis统一存储
    最终的选择方案,把session放到redis中,这样每个微服务都可以获取到session

4.总说:

浏览器会在auth.gulimall.com里面登录成功,auth.gulimall.com会将登陆成功的用户的从数据库查到的用户相关信息存到session里面,而且存session时不是存到自己的内存里面而是存到redis里面,然后auth.gulimall.com给浏览器发cookie,而且发的cookie的作用域不能仅仅是auth.gulimall.com而是要放大服务到.gulimall.com,此时浏览器访问其它任何服务都会带上这个cookie。

如果你把redis里面的session清空,那就是把登陆过的用户信息清空,虽然前台的浏览器访问后台时携带了cookie信息,但是到redis里面查不到用户信息,所以你就得重新登陆。而且我们设置了redis里面的session默认30分钟过期,也就是30分钟后redis里面的用户信息就没有了

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.修改微博登陆的代码

①修改sprinsession的存储类型是redis(这很重要·,以后存到session中就是存到redis中)

spring:
  session:
    store-type: redis

②增加一个配置类,由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化,并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com

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

③修改gulimall-auth-server的Controller

以前的逻辑是:

这是微博登陆的代码,前台带着code发送 /oauth2.0/weibo/success请求到后台的gulimall-auth-server模块中,然后gulimall-auth-server会先使用code换取SocialUser,然后拿着SocialUser到OpenFeign远程调用gulimall-memberoauth2Login()方法,在该方法中如果是第一次的话我们就得给该用户注册;如果该用户之前已经用微博登陆过,那就到数据库中更新一下token。
如果一切顺利,gulimall-member就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到RedirectAttributes然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html

现在的逻辑就是:

gulimall-member就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到SpringSession中然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html

6.修改账号密码登录的代码

修改.auth.gulimall.com的LoginController,目的是账号密码登陆的用户也要存到session里面(我们原来做的工作只是把微博登陆的用户存到sesson里面)

十一、单点登录

1.为什么要单点登录?

在但系统服务中,springsession把auth.gulimall.com作用域放大到gulimall.com,放大作用域就能共享session,但要是多系统情况下,域名完全不一样,没办法通过放大作用域的方式来共享session,这就需要用登录解决。
共享session干什么?用户一登陆就被存在session里面了,共享session就可以实现一次登录处处生效
你在新浪微博(https://weibo.com/)里面注册登录了,同时就要保证在新浪体育(https://sports.com/)、新浪新闻(https://news.com/)里面全都可以拿到session数据

2.单点登录的原理?

两个域名不一样的服务端client1和client2,还有一个负责登录的ssoserver,还有一个浏览器,它们四个之间的故事

请添加图片描述

先说明一下这个路径的含义:http://ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees的含义就是让你访问http://ssoserver.com:8080/login.html登陆页面,而 redirect_url=http:I/client1.com:8081/employees的含义是当你完成登陆后会重定向到http:I/client1.com:8081/employees的位置

第1-11步的解析:只有登陆了才能查看员工信息。一开始浏览器访问client1.com的员工信息http:I/client1.com:8081/employees,client1会根据这个url有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees,ssoserver.com会判断是否登陆过,没有登陆过就展示这个登陆页面,用户会输入账号密码进行登录,提交登陆请求http:/ssoserver.com:8080/doLogin?usermame,password,redirect_url给ssoserver.com,那么ssoserver.com会保存用户状态到redis,同时ssoserver.com会命令重定向到http: /lclient1.com:8081/employees?token=dadadadsdeuieu(浏览器访问路径),同时ssoserver.com会命令浏览器保存sso_token=dadadadsdeuieu这样式的cookie。浏览器这次就可以访问员工信息了,他的访问路径是刚刚提到的http://lclient1.com:8081/employees?token=dadadadsdeuieu比一开始访问员工信息的http:I/client1.com:8081/employees多了token=dadadadsdeuieu,这就回到第2步了,client1会根据有没有token参数判断是否登录,这次client1会觉得它登陆过了就可以访问员工信息了。

第12-19步解析:这次浏览器要访问客户端2的boss信息http:I/client2.com:8081/boss,client2会根据有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client2.com:8081/boss,ssoserver.com会判断是否登陆过,由于浏览器有sso_token=dadadadsdeuieu这样式的cookie,而且从redis能查到,说明它之前在client1或者client2登陆过,ssoserver.com会命令重定向到http:/lclient2.com:8082/boss?token=dadadadsdeuieu,所以浏览器就会访问http://lclient2.com:8082/boss?token=dadadadsdeuieu,这就回到了第2步,client2会根据有没有token参数判断是否登录,登陆过就响应页面。

所以说,以后浏览器无论访问client1还是client2,由于浏览器中保存了cookie,所以ssoserver.com就会判定它登陆过,所以以后都不用登陆。

演示

代码用的网友的,截屏用到老师的,网友喜欢自己起名字,把token改为redisKey什么的,不要计较细节上的不同

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

十二、购物车(面试版)

1.添加商品到购物车

1)如果用户没有登录:
情况1:第一次来,那就在浏览器种创建一个cookie(user-key),设置cookie的作用域、过期时间
情况2:之前来过,从浏览器种获取到cookie(user-key)
不论是情况1还是情况2,现在都有了user-key了,然后将用户购物车信息存到redis中

2)如果用户已经完成登录:
按照userId来存到redis中

2.购物车种数据的存储方式:

这里是引用
本节内容就是说明了用户购物车里的信息应该使用哪个数据库存储(MySQL还是Redis?),以及使用了Redis后是用List存储这些信息呢还是使用Hash存储这些信息?以及购物车VO、购物项VO的编写
在这里插入图片描述

3.展示购物车:

1)如果用户没有登录:
从cookie中获取user-key,使用user-key从Redis获取购物车数据

2)如果用户已经完成登录:
使用userId从Redis获取购物车数据,并尝试从浏览器中拿到user-key、查询Redis中对应临时购物车数据与用户购物车数据合并,并删除临时购物车

4.更加细节的东西

面试官要是没有问道太细节的东西,你也就不用解释user-key是什么,userId是什么,只会显得更加乱。他要是问道更加细节的东西,那就用下面的知识:

登录拦截器:
(登陆拦截器设计也是一个重点,后面得讲)

在购物车的所有Controller执行之前,我们先执行一个拦截器。在拦截器里需要区分用户的三种状态:1.用户已登录 2.用户未登录,而且还是第一次来 3.用户未登录,而且前两天已经来过了
“用户未登录,而且还是第一次来”这种情况的用户就需要在浏览器中保存一段cookie(user-key),并且设置cookie的过期时间什么的

如果用户没有登录,UserInfoTo中的userId是空的,但userKey不是空的
如果用户已经登录,UserInfoTo中的userId不是空的,但userKey是空的
如果UserInfoTo中的userId不是空的,UserInfoTo中的userKey也不是空的,那就说明用户现在登录了,但是他之前还没有登录的时候也访问过京东购物车;那么就需要合并购物车了

拦截器执行完后,UserInfoTo中的userId不是空的表示账号用户,反之为临时用户 ,然后决定用临时购物车还是用户购物车。将用户购物车信息存到redis中,redis中肯定需要键值对,账号用户的购物车的redis中的key是gulimall:cart:1(1是userId);临时用户的redis中的key是gulimall:cart:uuid其中uuid就是user-key。

添加到购物车:

添加新商品到购物车,第一步先看redis里面能不能查到skuid,查不到说明购物车里面之前没有添加过此商品,那就需要远程查询此商品的一系列信息;能查到说明购物车有此商品,将数据取出修改数量即可。

展示购物车:

若用户未登录,则直接使用user-key获取购物车数据;否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车

十四、购物车(精细版)

1、关于拦截器

在购物车的所有Controller执行之前,我们先执行一个拦截器。在拦截器里判断用户是否登录,以及用户是第几次登录

一个用户进来我们执行的 “ 拦截器—Controller—Service—Dao ” 这一套流程让同一个线程执行,这就使用了ThreadLocal技术,ThreadLocal是同一个线程共享数据,这个线程里面的数据会共享,使用过程就是:

ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();//创建一个threadLocal
...
userInfoTo.setTempUser(true);//标注该用户是临时用户
userInfoTo.setuserkey(uuid);//设置user-key
threadLocal.set(userInfoTo);//把要共享的数据userInfoTo设置进threadlocal里面
....
UserInfoTo userInfoTo = threadLocal.get();//后期就可以获取到这个共享的数据


UserInfoTo如下:

@ToString
@Data
public class UserInfoTo {
 
    private Long userId;
 
    private String userKey; 
 
    private boolean tempUser = false;  //这个相当重要,我们会根据tempUser是true还是false来决定有没有执行postHandle()方法
}

登陆拦截器如下:

/**
 * @Description: 在执行目标方法之前,判断用户的登录状态。并封装传递给目标请求
 */
public class CartInterceptor implements HandlerInterceptor {

    //ThreadLocal同一个线程共享数据
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
    
    /**
     * 在目标方法执行之前拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        MemberResponseVO member = (MemberResponseVO) 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) {
                String name = cookie.getName();
                if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);
                }
            }
        }
 
        //用户没有登陆,而且没有临时用户信息,一定保存一个临时用户
        if (StringUtils.isEmpty(userInfoTo.getUserKey())){
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        
        //userInfoTo存到threadLocal中
        threadLocal.set(userInfoTo);
        return true;
    }
 
    /**
     * 业务执行之后 分配临时用户,让浏览器保存
     */
    @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);
        }
 
    }
}

请你假设一下以下三种情况在拦截器中会发生什么:
如果用户没有登录,而且浏览器中没有用户的临时信息:UserInfoTo中的userId是空的,但userKey不是空的
如果用户没有登录,但是浏览器中有用户的临时信息:UserInfoTo中的userId是空的,但userKey不是空的
如果用户已经登录,UserInfoTo中的userId不是空的,但userKey是空的

登录Controller如下:

@Controller
public class CartController {
 
    /**
     * 登录  session有
     * 没登录,按照cookie里面带来的user-key来做
     * 第一次,如果没有临时用户,帮忙创建一个临时用户
     */
    @GetMapping("/cart.html")
    public String cartListPage(){
 
        //快速得到用户信息,id,user-key
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoTo);
        return "cartList";
    }
}

2、添加商品到购物车

getCartOps()方法里面逻辑:

因为是拦截器先执行的,所以先得到拦截器ThreadLocal的返回结果UserInfoTo userInfoTo = threadLocal.get(),如果userInfoTo.getUserId()不为空表示账号用户,反之为临时用户 ,然后决定用临时购物车还是用户购物车。将用户购物车信息存到redis中,redis中肯定需要键值对,账号用户的购物车的redis中的key是gulimall:cart:1(1是用户id,表示1号用户的购物车);临时用户的redis中的key是gulimall:cart:uuid其中uuid就是我们拦截器里存下的user-key。 redisTemplate.boundHashOps(cartKey)是说以后所有对redis的增删改查都是针对redia中key为cartKey的增删改查。

addToCart()方法里面的逻辑:

添加新商品到购物车,第一步先看redis里面能不能查到skuid,查不到说明购物车里面之前没有添加过此商品,那就需要远程查询此商品的一系列信息;能查到说明购物车有此商品,将数据取出修改数量即可。

7.测试:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、获取购物车

在这里插入图片描述

若用户未登录,则直接使用user-key获取购物车数据;否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、选中购物车项

在这里插入图片描述

5、修改购物项数量

在这里插入图片描述

6、删除购物项

在这里插入图片描述


点击全文阅读


本文链接:http://zhangshiyu.com/post/30768.html

缓存  数据  购物车  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1