Bootstrap

【谷粒商城】框架扩充篇(3/4)

学到高级篇已经击败90%的人了,加油

一、Elastic Search

ES笔记:https://blog.csdn.net/hancoder/article/details/113922398

二、公用工具

商品发布只是可以上架了,上架后才可被检索

Feign

远程调用源码

// ReflectiveFeign
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (!"equals".equals(method.getName())) {
        if ("hashCode".equals(method.getName())) {
            return this.hashCode();
        } else {
            return "toString".equals(method.getName()) ? this.toString() : ((MethodHandler)this.dispatch.get(method)).invoke(args);
        }
    } else { // 处理equals方法
        try {
            Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
            return this.equals(otherHandler);
        } catch (IllegalArgumentException var5) {
            return false;
        }
    }
}
// SynchronousMethodHandler.JAVA;

public Object invoke(Object[] argv) throws Throwable {
    // 传过来的数据,构造 RequestTemplate,里面body有数据
    RequestTemplate template = this.buildTemplateFromArgs.create(argv);
    Options options = this.findOptions(argv);
    // 重试器,要注意重复调用、接口幂等性。可以写重试器自己的实现
    Retryer retryer = this.retryer.clone();

    while(true) {
        try {
            // 执行后得到响应,解码得到bean
            return this.executeAndDecode(template, options);
        } catch (RetryableException var9) {
            RetryableException e = var9;

            try {
                retryer.continueOrPropagate(e);
            } catch (RetryableException var8) {
                Throwable cause = var8.getCause();
                if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
                    throw cause;
                }
                throw var8;
            }
        }
    }
}

body里是数据,feign将bean转为了 json

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    // 构造出请求
    Request request = this.targetRequest(template);
    if (this.logLevel != Level.NONE) {
        // 打印日志
        this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
    }

    long start = System.nanoTime();

    Response response;
    try {
        // 执行。client是LoadBalancerFeignClient。跳转到远程
        response = this.client.execute(request, options);
        response = response.toBuilder().request(request).requestTemplate(template).build();
    } catch (IOException var16) {
        if (this.logLevel != Level.NONE) {
            this.logger.logIOException(this.metadata.configKey(), this.logLevel, var16, this.elapsedTime(start));
        }

        throw FeignException.errorExecuting(request, var16);
    }
。。。
公共返回类R

因为是个hashmap,所以setData不成功

public class R<T> extends HashMap<String,Object>{
    //     把setData重写成PUT
    public R setData(Object data){
        put("data", data);
        return this;
    }

    public <T> T getData(TypeReference<T> typeReference){
        // get("data") 默认是map类型 所以再由map转成string再转json
        Object data = get("data");//得到list,list每个值是map类型
        // list<Map>转json
        String s = JSON.toJSONString(data);
        // json转list<T>
        return JSON.parseObject(s, typeReference);
    }
}

在其他处是new TypeReference<List<T>>

data的值对应的是List,而list的每个值是map

三、商城系统首页

P136

页面与静态资源处理

不使用前后端分离开发了,管理后台用vue

页面在课件位置: 【尚硅谷公众号-回复谷粒商城-高级篇-资料源码.zip\代码\html】

静态资源处理

nginx发给网关集群,网关再路由到微服务

静态资源放到nginx中,后面的很多服务都需要放到nginx中

html\首页资源\index放到gulimall-product下的static文件夹

把index.html放到templates中

pom依赖

导入thymeleaf依赖、热部署依赖devtools使页面实时生效

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

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

关闭thymeleaf缓存,方便开发实时看到更新

  thymeleaf:
    cache: false
    suffix: .html
    prefix: classpath:/templates/

web开发放到web包下,原来的controller是前后分离对接手机等访问的,所以可以改成app,对接app应用

渲染一级分类菜单

刚导入index.html时,里面的分类菜单都是写死的,我们要访问数据库拿到放到model中,然后在页面foreach填入

thymeleaf笔记:https://blog.csdn.net/hancoder/article/details/113945941

@GetMapping({"/", "index.html"})
public String getIndex(Model model) {
    //获取所有的一级分类
    List<CategoryEntity> catagories = categoryService.getLevel1Catagories();
    model.addAttribute("catagories", catagories);
    return "index";
}

页面遍历菜单数据

<li th:each="catagory:${catagories}" >
    <a href="#" class="header_main_left_a" ctg-data="3" th:attr="ctg-data=${catagory.catId}"><b th:text="${catagory.name}"></b></a>
</li>
渲染三级分类菜单
@ResponseBody
@RequestMapping("index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatlogJson() {

    Map<String, List<Catelog2Vo>> map = categoryService.getCatelogJson();
    return map;
}


@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
    List<CategoryEntity> entityList = baseMapper.selectList(null);
    // 查询所有一级分类
    List<CategoryEntity> level1 = getCategoryEntities(entityList, 0L);
    Map<String, List<Catelog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        // 拿到每一个一级分类 然后查询他们的二级分类
        List<CategoryEntity> entities = getCategoryEntities(entityList, v.getCatId());
        List<Catelog2Vo> catelog2Vos = null;
        if (entities != null) {
            catelog2Vos = entities.stream().map(l2 -> {
                Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null);
                // 找当前二级分类的三级分类
                List<CategoryEntity> level3 = getCategoryEntities(entityList, l2.getCatId());
                // 三级分类有数据的情况下
                if (level3 != null) {
                    List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList());
                    catelog2Vo.setCatalog3List(catalog3Vos);
                }
                return catelog2Vo;
            }).collect(Collectors.toList());
        }
        return catelog2Vos;
    }));
    return parent_cid;
}

四、Nginx

本来想把nginx另写一篇,csdn不给审核,说翻墙。。。我服了

在hosts中设置192.168.56.10 gulimall.com

利用nginx转到网关(记得关防火墙)

1、Nginx+网关+openFeign的逻辑

要实现的逻辑:本机浏览器请求gulimall.com,通过配置hosts文件之后,那么当你在浏览器中输入gulimall.com的时候,相当于域名解析DNS服务解析得到ip 192.168.56.10,也就是并不是访问java服务,而是先去找nginx。什么意思呢?是说如果某一天项目上线了,gulimall.com应该是nginx的ip,用户访问的都是nginx

请求到了nginx之后,

  • 如果是静态资源/static/*直接在nginx服务器中找到静态资源直接返回。
  • 如果不是静态资源/(他配置在/static/*的后面所以才优先级低),nginx把他upstream转交给另外一个ip 192.168.56.1:88这个ip端口是网关gateway
    • (在upstream的过程中要注意配置proxy_set_header Host $host;

到达网关之后,通过url信息断言判断应该转发给nacos中的哪个微服务(在给nacos之前也可以重写url),这样就得到了响应

而对于openFeign,因为在服务中注册了nacos的ip,所以他并不经过nginx

2、Nginx配置文件

nginx.conf:

  • 全局块:配置影响nginx全局的指令。如:用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成worker process故障等
  • events块:配置影响 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 work process下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 word process 可以同时支持的最大连接数等。
  • http块:
    • http全局块:配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。错误页面等
    • server块:这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的。每个 http 块可以包括多个 server 块,而每个 server 块就相当于一个虚拟主机。
      • location1:配置请求的路由,以及各种页面的处理情况
      • location2

3、Nginx+网关配置

  1. 修改主机hosts,映射gulimall.com到192.168.56.10。关闭防火墙

  2. 修改nginx/conf/nginx.conf,将upstream映射到我们的网关服务

        upstream gulimall{
            # 88是网关
            server 192.168.56.1:88;
        }
    
  3. 修改nginx/conf/conf.d/gulimall.conf,接收到gulimall.com的访问后,如果是/,转交给指定的upstream,由于nginx的转发会丢失host头,造成网关不知道原host,所以我们添加头信息

      location / {
            proxy_pass http://gulimall;
            proxy_set_header Host $host;
        }
    
  4. 配置gateway为服务器,将域名为**.gulimall.com转发至商品服务。配置的时候注意 网关优先匹配的原则,所以要把这个配置放到后面

        - id: gulimall_host_route
              uri: lb://gulimall-product
              predicates:
                - Host=**.gulimall.com
    

不一定非要按我的来

conf.d/gulimall.conf

监听来自gulimall:80的请求,

  • 对于以/static开头的请求,就是找 /usr/share/nginx/html这个相对路径。
    • 为什么找那个?因为我们映射了docker外面的/mydata/data/nginx/html某一列到这个目录,所以在docker中就是去这找静态资源
  • 其他的请求,转发到http://gulimall 这个upstream ,并且由于nginx的转发会丢失host头(host头是HTTP1.1开始新增的请求头),造成网关不知道原host,所以我们添加头信息
nginx.conf

在这里最重要的是这个再转给网关的配置

这个地方因为可能把后面视频的内容也挪过来了所以写的比较乱,也懒得改了,总之就是分为/static拦截和/拦截,将/拦截转发到upstream gulimall即可,转发时代上请求头

Nginx的原理其实就是 NIO-select/read+线程池 ,很多中间件/框架的原理都是这个

nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;  # 包含了哪些配置文件
}

conf.d/gulimall.conf

server {
    listen       80;
    server_name gulimall.com  *.gulimall.com;

    location /static {
        root   /usr/share/nginx/html;
    }

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;
    location / {
        proxy_pass http://gulimall;
        proxy_set_header Host $host;  # 
    }

	upstream gulimall{
        # 88是网关
        server 192.168.56.1:88;
    }
    include /etc/nginx/conf.d/*.conf;  # 包含了哪些配置文件
}

测试:http://gulimall.com/api/product/attrgroup/list/1

http://localhost:88/api/product/attrgroup/list/1

请求结果相同

此时请求接口和请求页面都是gulimall.com

五、压力测试

JVM参数、工具、调优笔记:https://blog.csdn.net/hancoder/article/details/108312012

Jmeter

下载:https://jmeter.apache.org/download_jmeter.cgi

创建测试计划,添加线程组

线程数==用户

ramp-up 多长时间内发送完

添加-取样器-HTTP请求

添加-监听器-查看结果树

添加-监听器-汇总报告

Jmeter Address Already in use错误解决

报错原因:

1、windows系统为了保护本机,限制了其他机器到本机的连接数.
2、TCP/IP 可释放已关闭连接并重用其资源前,必须经过的时间。关闭和释放之间的此时间间隔通称 TIME_WAIT 状态或两倍最大段生命周期(2MSL)状态。此时间期间,重新打开到客户机和服务器的连接的成本少于建立新连接。减少此条目的值允许 TCP/IP 更快地释放已关闭的连接,为新连接提供更多资源。如果运行的应用程序需要快速释放和创建新连接,而且由于 TIME_WAIT 中存在很多连接,导致低吞吐量,则调整此参数。

修改操作系统注册表
1、打开注册表:运行-regedit
2、直接输入找到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters
3、右击Parameters新建 DWORD32值,name:TcpTimedWaitDelay,value:30(十进制) ——> 设置为30秒回收(默认240)
4、新建 DWORD值,name:MaxUserPort,value:65534(十进制) ——> 设置最大连接数65534
注意:修改时先选择十进制,再填写数字。
5、重启系统

Jconsole、JvisualVM

JVM写到别处:https://blog.csdn.net/hancoder/article/details/105210258

看这个视频的真的有没学过JVM的吗。。。

同样贴上之前的JVM学习笔记:https://blog.csdn.net/hancoder/article/details/108312012

运行状态:

  • 运行:正在运行
  • 休眠:sleep
  • 等待:wait
  • 驻留:线程池里面的空闲线程
  • 监视:阻塞的线程,正在等待锁

要监控GC,安装插件:工具-插件。可用插件-检查最新版本 报错的时候百度“插件中心”,改个JVM对应的插件中心url.xml.z

安装visual GC

优化
  • SQL耗时越小越好,一般情况下微秒级别
  • 命中率越高越好,一般情况下不能低于95%
  • 锁等待次数越低越好,等待时间越短越好
  • 中间件越多,性能损失雨大,大多都损失在网络交互了

视频教程中的测试结果

压测内容压测线程数吞吐量/s90%响应时间99%响应时间
Nginx(浪费CPU)502120101204
Gateway(浪费CPU)509200921
简单服务(返回字符串)509850848
首页一级菜单渲染50350260491
首页菜单渲染(开缓存)50465119306
首页菜单渲染(开缓存、优化数据库、关日志)50465127304
三级分类数据获取5041327513756
三级分类(优化业务)501540925891
首页全量数据获取502.72401426556
首页全量数据获取(动静分类)504.91491316421
Nginx+GateWay50
Gateway+简单服务5030002867
全链路(Nginx+GateWay+简单服务)5065084537

product微服务的 -Xmx1024m -Xms1024m -Xmn512m

Nginx动静分离

由于动态资源和静态资源目前都处于服务端,所以为了减轻服务器压力,我们将js、css、img等静态资源放置在Nginx端,以减轻服务器压力

  1. 静态文件上传到 mydata/nginx/html/static/index/css,这种格式

  2. 修改index.html的静态资源路径,加上static前缀src="/static/index/img/img_09.png"

  3. 修改/mydata/nginx/conf/conf.d/gulimall.conf

    如果遇到有/static为前缀的请求,转发至html文件夹

        location /static {
            root   /usr/share/nginx/html;
        }
    
    
        location / {
        	proxy_pass http://gulimall;
    		proxy_set_header Host $host;
        }
    
优化三级分类

优化前

对二级菜单的每次遍历都需要查询数据库,浪费大量资源

优化后

仅查询一次数据库,剩下的数据通过遍历得到并封装

//优化业务逻辑,仅查询一次数据库
List<CategoryEntity> categoryEntities = this.list();
//查出所有一级分类
List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
    //遍历查找出二级分类
    List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
    List<Catalog2Vo> catalog2Vos=null;
    if (level2Categories!=null){
        //封装二级分类到vo并且查出其中的三级分类
        catalog2Vos = level2Categories.stream().map(cat -> {
            //遍历查出三级分类并封装
            List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
            List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
            if (level3Catagories != null) {
                catalog3Vos = level3Catagories.stream()
                    .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
                    .collect(Collectors.toList());
            }
            Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
            return catalog2Vo;
        }).collect(Collectors.toList());
    }
    return catalog2Vos;
}));
return listMap;

六、redisson分布式锁与缓存

笔记写到了别处:https://blog.csdn.net/hancoder/article/details/114004280

七、检索

建立微服务和检索相关代码写到了https://blog.csdn.net/hancoder/article/details/113922398 末尾

八、异步编排

线程基础百度吧

异步编排参考网上链接即可:https://blog.csdn.net/weixin_45762031/article/details/103519459

CompletableFuture介绍

Future是Java 5添加的类,用来描述一个异步计算的结果。你可以使用isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。

虽然Future以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的CPU资源,而且也不能及时地得到计算结果,为什么不能用观察者设计模式当计算结果完成及时通知监听者呢?

很多语言,比如Node.js,采用回调的方式实现异步编程。Java的一些框架,比如Netty,自己扩展了Java的 Future接口,提供了addListener等多个扩展方法;Google guava也提供了通用的扩展Future;Scala也提供了简单易用且功能强大的Future/Promise异步编程模式。

作为正统的Java类库,是不是应该做点什么,加强一下自身库的功能呢?

在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。

创建异步对象

CompletableFuture 提供了四个静态方法来创建一个异步操作。

static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。

  • runAsync方法不支持返回值。
  • supplyAsync可以支持返回值。

计算完成时回调方法:当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:

public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor);

public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn);

whenComplete可以处理正常和异常的计算结果,exceptionally处理异常情况。BiConsumer<? super T,? super Throwable>可以定义处理业务

whenComplete 和 whenCompleteAsync 的区别:

  • whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
  • whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。

方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

public class CompletableFutureDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture future = CompletableFuture.supplyAsync(new Supplier<Object>() {
            @Override
            public Object get() {
                System.out.println(Thread.currentThread().getName() + "\t completableFuture");
                int i = 10 / 0;
                return 1024;
            }
        }).whenComplete(new BiConsumer<Object, Throwable>() {
            @Override
            public void accept(Object o, Throwable throwable) {
                System.out.println("-------o=" + o.toString());
                System.out.println("-------throwable=" + throwable);
            }
        }).exceptionally(new Function<Throwable, Object>() {
            @Override
            public Object apply(Throwable throwable) {
                System.out.println("throwable=" + throwable);
                return 6666;
            }
        });
        System.out.println(future.get());
    }
}

handle 方法

handle 是执行任务完成时对结果的处理。
handle 是在任务完成后再执行,还可以处理异常的任务。

public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
线程串行化方法

thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。

thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。

thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作

带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);

public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);

九、商品详情

添加hosts内容 192.168.56.10 item.gulimall.com

修改网关 使item路由到product

复制详情页的html到product,静态文件放到nginx

(1) 商品详情VO

观察我们要建立怎样的VO

hLoNrT.md.jpg

hLotMV.md.jpg

@Data
public class SkuItemVo {

    /*** 1 sku基本信息的获取:如标题*/
    SkuInfoEntity info;

    boolean hasStock = true;

    /*** 2 sku的图片信息*/
    List<SkuImagesEntity> images;

    /*** 3 获取spu的销售属性组合。每个attrName对应一个value-list*/
    List<ItemSaleAttrVo> saleAttr;

    /*** 4 获取spu的介绍*/
    SpuInfoDescEntity desc;

    /*** 5 获取spu的规格参数信息,每个分组的包含list*/
    List<SpuItemAttrGroup> groupAttrs;

    /*** 6 秒杀信息*/
    SeckillInfoVo seckillInfoVo;
}

@ToString
@Data
public class ItemSaleAttrVo{
	private Long attrId;
	private String attrName;

	/** AttrValueWithSkuIdVo两个属性 attrValue、skuIds */
	private List<AttrValueWithSkuIdVo> attrValues;
}

@ToString
@Data
public class SpuItemAttrGroup{
	private String groupName;
    
	/** 两个属性attrName、attrValue */
	private List<SpuBaseAttrVo> attrs;
}

(2) sql构建

我们观察商品页面与VO,可以大致分为5个部分需要封装。1 2 4比较简单,单表就查出来了。我们分析3、5

我们在url中首先有sku_id,在从sku_info表查标题的时候,顺便查到了spu_id、catelog_id,这样我们就可以操作剩下表了。

分组规格参数

在5查询规格参数中

  • pms_product_attr_value 根据spu_id获得spu相关属性
  • pms_attr_attrgroup_relation根据catelog_id获得属性的分组
<!-- 封装自定义结果集 -->
<resultMap id="SpuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroup">
    <result column="attr_group_name" property="groupName" javaType="string"></result>
    <collection property="attrs" ofType="com.atguigu.gulimall.product.vo.SpuBaseAttrVo">
        <result column="attr_name" property="attrName" javaType="string"></result>
        <result column="attr_value" property="attrValue" javaType="string"></result>
    </collection>
</resultMap>

<select id="getAttrGroupWithAttrsBySpuId" resultMap="SpuItemAttrGroupVo">

    SELECT pav.`spu_id`, ag.`attr_group_name`, ag.`attr_group_id`, aar.`attr_id`, attr.`attr_name`,pav.`attr_value`
    FROM `pms_attr_group` ag
    LEFT JOIN `pms_attr_attrgroup_relation` aar ON aar.`attr_group_id` = ag.`attr_group_id`
    LEFT JOIN `pms_attr` attr ON attr.`attr_id` = aar.`attr_id`
    LEFT JOIN `pms_product_attr_value` pav ON pav.`attr_id` = attr.`attr_id`
    WHERE ag.catelog_id = #{catalogId} AND pav.`spu_id` = #{spuId}
</select>
@Override
public List<SpuItemAttrGroup> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {

    // 1.出当前Spu对应的所有属性的分组信息 以及当前分组下所有属性对应的值
    // 1.1 查询所有分组
    AttrGroupDao baseMapper = this.getBaseMapper();

    return baseMapper.getAttrGroupWithAttrsBySpuId(spuId, catalogId);
}
sku售卖属性

在3查询售卖参数中,

为什么是spu的销售属性,而不是sku的销售属性:url是skuID,但是销售属性要显示所有spu的sku[],为了提前看有无货、快速获得其他的sku_id。

pms_sku_info查出该spuId对应的skuId

根据spu获取销售属性对应的所有值。首先知道spu是没有销售属性的,而是spu对应sku[]的销售属性

根据各种选项决定一个sku是如何做到的?我们可以利用一下ES的倒排索引。比较难想到,先正序看一下吧

  • pms_sku_info根据spu得到所有sku_id[]
  • pms_sku_sale_attr_value根据sku得到销售属性
  • 查询出来之后需要根据属性attr_id分组,分组要查询的列得在group by之后出现过,或者查询的列是用分组函数聚合出的。
    • GROUP_CONCAT就把没分组的列都聚合到一起。比如分组后name为zs的对应id有1、2、3,那么GROUP_CONCAT(id)该列就是123
    • 而聚合后如果有重复值,比如id有1,2,2,那么就可以用DISTINCT聚合成1,2
    • 最后GROUP_CONCAT(DISTINCT info.sku_id) sku_ids

查询得到的结果特别像ES中的倒排索引

    <resultMap id="SkuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.ItemSaleAttrVo">
        <result column="attr_id" property="attrId"></result>
        <result column="attr_name" property="attrName"></result>
        <collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo">
            <result column="attr_value" property="attrValue"></result>
            <result column="sku_ids" property="skuIds"></result>
        </collection>
    </resultMap>

    <select id="getSaleAttrsBySpuId" resultMap="SkuItemSaleAttrVo">
        SELECT ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`,
            GROUP_CONCAT(DISTINCT info.`sku_id`) sku_ids
        FROM `pms_sku_info` info LEFT JOIN `pms_sku_sale_attr_value` ssav
        ON ssav.`sku_id` = info.`sku_id`
        WHERE info.`spu_id` = #{spuId}
        GROUP BY ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`
    </select>

(3) controller-service

@Controller
public class ItemController {

	@Autowired
	private SkuInfoService skuInfoService;

	@RequestMapping("/{skuId}.html")
	public String skuItem(@PathVariable("skuId") Long skuId, Model model) 
        throws ExecutionException, InterruptedException {

		SkuItemVo vo = skuInfoService.item(skuId);

		model.addAttribute("item", vo);
		return "item";
	}
}
@Override //SkuInfoServiceImpl  @TableName("pms_sku_info")
public SkuItemVo item(Long skuId) {
    SkuItemVo skuItemVo = new SkuItemVo();
    //1、sku基本信息的获取  pms_sku_info
    SkuInfoEntity skuInfoEntity = this.getById(skuId);
    skuItemVo.setInfo(skuInfoEntity);
    Long spuId = skuInfoEntity.getSpuId();
    Long catalogId = skuInfoEntity.getCatalogId();


    //2、sku的图片信息    pms_sku_images
    List<SkuImagesEntity> skuImagesEntities = skuimagesService.list(new QueryWrapper<SkuimagesEntity>().eq("sku_id", skuId));
    skuItemVo.setimages(skuimagesEntities);

    //3、获取spu的销售属性组合-> 依赖1 获取spuId
    List<SkuItemSaleAttrVo> saleAttrVos=skuSaleAttrValueService.listSaleAttrs(spuId);
    skuItemVo.setSaleAttr(saleAttrVos);

    //4、获取spu的介绍-> 依赖1 获取spuId
    SpuInfoDescEntity byId = spuInfoDescService.getById(spuId);
    skuItemVo.setDesc(byId);

    //5、获取spu的规格参数信息-> 依赖1 获取spuId catalogId
    List<SpuItemAttrGroupVo> spuItemAttrGroupVos=productAttrValueService.getProductGroupAttrsBySpuId(spuId, catalogId);
    skuItemVo.setGroupAttrs(spuItemAttrGroupVos);
    //TODO 6、秒杀商品的优惠信息

    return skuItemVo;
}

(4) 优化:异步编排

因为商品详情是查多个sql,所以可以利用线程池进行异步操作,但是因为有的步骤需要用到第一步的spu_d结果等想你想,所以需要使用异步编排。

调用thenAcceptAsync()可以接受上一步的结果且没有返回值。

最后调用get()方法使主线程阻塞到其他线程完成任务。

@Override // SkuInfoServiceImpl
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
    SkuItemVo skuItemVo = new SkuItemVo();

    CompletableFuture<SkuInfoEntity> infoFutrue = CompletableFuture.supplyAsync(() -> {
        //1 sku基本信息
        SkuInfoEntity info = getById(skuId);
        skuItemVo.setInfo(info);
        return info;
    }, executor);
    // 无需获取返回值
    CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
        //2 sku图片信息
        List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);
    }, executor);
    // 在1之后
    CompletableFuture<Void> saleAttrFuture =infoFutrue.thenAcceptAsync(res -> {
        //3 获取spu销售属性组合 list
        List<ItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBuSpuId(res.getSpuId());
        skuItemVo.setSaleAttr(saleAttrVos);
    },executor);
    // 在1之后
    CompletableFuture<Void> descFuture = infoFutrue.thenAcceptAsync(res -> {
        //4 获取spu介绍
        SpuInfoDescEntity spuInfo = spuInfoDescService.getById(res.getSpuId());
        skuItemVo.setDesc(spuInfo);
    },executor);
    // 在1之后
    CompletableFuture<Void> baseAttrFuture = infoFutrue.thenAcceptAsync(res -> {
        //5 获取spu规格参数信息
        List<SpuItemAttrGroup> attrGroups = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
        skuItemVo.setGroupAttrs(attrGroups);
    }, executor);

    // 6.查询当前sku是否参与秒杀优惠
    CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
        R skuSeckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
        if (skuSeckillInfo.getCode() == 0) {
            SeckillInfoVo seckillInfoVo = skuSeckillInfo.getData(new TypeReference<SeckillInfoVo>() {});
            skuItemVo.setSeckillInfoVo(seckillInfoVo);
        }
    }, executor);

    // 等待所有任务都完成再返回
    CompletableFuture.allOf(imageFuture,saleAttrFuture,descFuture,baseAttrFuture,secKillFuture).get();
    return skuItemVo;
}

线程池参数:

  • 20-50核心线程,200最大线程,1W长度等待队列

(5) 页面sku切换

页面上12 45渲染都比较简单,我们需要看看4是如何渲染的。

之前拿到的sku_ids是用,分隔的,

通过控制class中是否包换checked属性来控制显示样式,因此要根据skuId判断

  • 选择的标签多个checked class,下面有个containers函数是判断当前的元素是否是当前sku的元素
<div class="box-attr clear" th:each="attr : ${item.saleAttr}">
    <dl>
        <dt>选择[[${attr.attrName}]]</dt>
        <dd th:each="vals : ${attr.attrValues}">
            <a class="sku_attr_value" th:attr="skus=${vals.skuIds},
                                               class=${#lists.contains(
                                                 		 #strings.listSplit(vals.skuIds,','),
                                                          item.info.skuId.toString()
                                                       )?
                                               		  'sku_attr_value checked':
                                              		  'sku_attr_value'}">
                <!--<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" />-->
                [[${vals.attrValue}]]
            </a>
        </dd>
    </dl>
</div>

显示处理完了,下面编写选中某个售卖属性后如何变化页面元素

实际上我觉得应该发送ajax请求更改页面元素。这里选用的是根据选中的售卖属性组合判断出sku_id

怎么找交集:

$(".sku_attr_value").click(function () {
        var skus = new Array();
        // 1.获取所有加了checked的属性
        // 1.1 点击的元素加上自定义属性
        $(this).addClass("clicked");
        var curr = $(this).attr("skus").split(",");
        // 当前被点击的所有sku组合的数组放进去
        skus.push(curr)
        // 去掉同一行所有的checked
        $(this).parent().parent().find(".sku_attr_value").removeClass("checked");

        // 注意这个a[class='sku_attr_value checked']
        $("a[class='sku_attr_value checked']").each(function () {
        	// 把选择的元素的[sku_id]都放到skus中
            skus.push($(this).attr("skus").split(","));
        });
        // 2.取出他们的交集 得到skuId 调用filter方法的一定是jQuery元素
        var filterEle = skus[0];
        for (var i = 1; i < skus.length; i++) {
        	// $(a).filter(b)就是求a b的交集
            filterEle = $(filterEle).filter(skus[i]);
        }
        console.log(filterEle[0])
        // 3.跳转
        location.href = "http://item.gulimall.com/" + filterEle[0] + ".html";
    });

十、认证服务

认证服务的笔记写到了另外一篇:https://blog.csdn.net/hancoder/article/details/114242184

笔记不易

离线笔记均为markdown格式,图片也是云图,10多篇笔记20W字,压缩包仅500k,推荐使用typora阅读。也可以自己导入有道云笔记等软件中

阿里云图床现在每周得几十元充值,都要自己往里搭了,麻烦不要散播与转发

打赏后请主动发支付信息到邮箱 [email protected] ,上班期间很容易忽略收账信息,邮箱回邮基本秒回

禁止转载发布,禁止散播,若发现大量散播,将对本系统文章图床进行重置处理。

技术人就该干点技术人该干的事

如果帮到了你,留下赞吧,谢谢支持

;