# gulimall **Repository Path**: programerjava/gulimall ## Basic Information - **Project Name**: gulimall - **Description**: 商城系统 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 4 - **Created**: 2020-07-05 - **Last Updated**: 2023-04-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 1、关于vagrant安装过程的坑(转) 执行 vagrant up 命令报错 如下 ``` PS F:\centos7> vagrant up Bringing machine 'default' up with 'virtualbox' provider... ==> default: Importing base box 'centos/7'... There was an error while executing `VBoxManage`, a CLI used by Vagrant for controlling VirtualBox. The command and stderr is shown below. Command: ["import", "\\\\?\\D:\\HashiCorp\\Vagrant\\boxes\\centos-VAGRANTSLASH-7\\0\\virtualbox\\box.ovf", "--vsys", "0", "--vmname", "centos-7-1-1.x86_64_1583492895965_65141", "--vsys", "0", "--unit", "10", "--disk", "C:/Users/\u65E0\u5173\u98CE\u6708/VirtualBox VMs/centos-7-1-1.x86_64_1583492895965_65141/centos-7-1-1.x86_64.vmdk"] Stderr: 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Interpreting \\?\D:\HashiCorp\Vagrant\boxes\centos-VAGRANTSLASH-7\0\virtualbox\box.ovf... OK. 0%... Progress state: VBOX_E_INVALID_OBJECT_STATE VBoxManage.exe: error: Appliance import failed VBoxManage.exe: error: Storage for the medium 'C:\Users\鏃犲叧椋庢湀\VirtualBox VMs\centos-7-1-1.x86_64_1583492895965_65141\centos-7-1-1.x86_64.vmdk' is not created VBoxManage.exe: error: Details: code VBOX_E_INVALID_OBJECT_STATE (0x80bb0007), component ApplianceWrap, interface IAppliance VBoxManage.exe: error: Context: "enum RTEXITCODE __cdecl handleImportAppliance(struct HandlerArg *)" at line 957 of file VBoxManageAppliance.cpp ``` 错误是 box的存储硬盘空间不够 修改box的默认存储地址 解决 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306193632667.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3gzMjkzNTc4NDI=,size_16,color_FFFFFF,t_70) 还有就是在执行 vagrant up命令的时候报 字符编码有问题不能解析 解决方案如下: 1、找到 Vagrant 安装路径 博主是在 C:\HashiCorp\Vagrant\embedded\gems\2.2.6\gems\vagrant-2.2.6\lib\vagrant\util\io.rb 找到io.rb文件 32行替换成: data << io.readpartial(READ_CHUNK_SIZE).encode(“UTF-8”, invalid: :replace,undef: :replace, replace: ‘?’) 2、Vagrant安装路径不能带有中文。 **注**:vagrant详解:https://blog.csdn.net/cuixhao110/article/details/105519946/ ​ vagrant**常用**命令: ``` 1.在空文件夹初始化虚拟机 vagrant init [box-name] 2.在初始化完的文件夹内启动虚拟机 vagrant up 3.ssh登录启动的虚拟机 vagrant ssh 4.挂起启动的虚拟机 vagrant suspend 5.重启虚拟机 vagrant reload 6.关闭虚拟机 vagrant halt 7.查找虚拟机的运行状态 vagrant status 8.销毁当前虚拟机 vagrant destroy ``` - 执行vagrant up:No usable default provider could be found for your system. Vagrant relies on .......报错的解决方法 原文链接:https://blog.csdn.net/M82_A1/java/article/details/97250162 - ### Vagrant使用国内镜像安装插件和box镜像 转:https://blog.csdn.net/dafei1288/article/details/105828516/ ### 2、[vagrant]-virtualbox 网络适配器没有 && VirtualBox Host-Only Ethernet Adapter 问题(转) vagrant ,virtualbox安装完成后 使用vagrant命令 ``` vagrant up 1 ``` 出现以下报错 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190305155632208.png)在 **控制面板\网络和 Internet\网络连接** 中也没有 virtualbox 的虚拟网卡 # 解决办法 1. “win+r”输入“devmgmt.msc” 2. 点击网络适配器 3. 添加硬件向导 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190305160042424.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM5NTgyNTc=,size_16,color_FFFFFF,t_70) \4. 选择从列表安装,下一步 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190305160227473.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM5NTgyNTc=,size_16,color_FFFFFF,t_70) 5.选择网络适配器 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190305160310441.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM5NTgyNTc=,size_16,color_FFFFFF,t_70) \5. 选择驱动文件,点击确定 驱动文件路径:VirtualBox安装目录下drivers\ network\ netadp6目录下VBoxNetAdp6.inf ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190305160415441.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM5NTgyNTc=,size_16,color_FFFFFF,t_70) - 安装完成后 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190305160749327.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM5NTgyNTc=,size_16,color_FFFFFF,t_70) - ### VirtualBox 硬盘容量扩充 - ### 使用df -h查看硬盘容量 - ![1594951996494](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594951996494.png) - 打开VirtualBox的“管理”-->虚拟介质管理 - ![img](https://www.pianshen.com/images/821/8cf7540dfd06e3b0136fd27e786e18f5.png) - 增加容量![1594952099611](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594952099611.png) - ### 3、docker安装 - 安装所需依赖 ``` yum install -y yum-utils device-mapper-persistent-data lvm2 ``` - 设置docker下载镜像(阿里云) ``` yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo ``` - 更新yum软件包索引 我们在更新或配置yum源之后,通常都会使用yum makecache生成缓存,这个命令是将软件包信息提前在本地缓存一份,用来提高搜索安装软件的速度。 ``` yum makecache fast ``` - 安装docker ce ``` yum install -y docker-ce ``` - 启动docker ``` systemctl start docker ``` - 配置docker镜像加速器 ``` sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://9nmxujqa.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker ``` ### 4、安装mysql - 下载镜像 ``` docker pull mysql:5.7 ``` - 创建mysql容器 ``` docker run -p 3306:3306 --name mysql \ -v /mydata/mysql/log:/var/log/mysql \ -v /mydata/mysql/data:/var/lib/mysql \ -v /mydata/mysql/conf:/etc/mysql \ -e MYSQL_ROOT_PASSWORD=root \ -d mysql:5.7 ``` - 修改配置文件 ``` [client] default-character-set=utf8 [mysql] default-character-set=utf8 [mysqld] init_connect='SET collation_connection = utf8_unicode_ci' init_connect='SET NAMES utf8' character-set-server=utf8 collation-server=utf8_unicode_ci skip-character-set-client-handshake skip-name-resolve ``` - 查看是否修改成功 ``` show variables like "%character%"; ``` - 设置docker启动时,就运行mysql ``` docker update mysql --restart=always ``` - 设置docker开机自启动 ``` systemctl enable docker ``` - docker启动时没有空间了 - 日志 ``` 使用 docker logs mysql 查看mysql运行日志 ``` ![1594950876847](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594950876847.png) - 原因:空间不足,删除多余的镜像或增加容量 ### 5、下载redis - 下载镜像 ``` docker pull redis ``` - 创建redis配置文件 ``` mkdir -p /mydata/redis/conf ``` - 启动redis ``` docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \ -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \ -d redis redis-server /etc/redis/redis.conf ``` - 修改配置文件 ``` echo "appendonly yes" >> /mydata/redis/conf/redis.conf #启用AOF持久化机制 ``` - 连接redis客服端 ``` docker exec -it redis redis-cli ``` - 设置redis容器在docker启动时便启动 ``` docker update redis --restart=always ``` ### 6、git下载安装 - 进入git bash ``` # 配置用户名 git config --global user.name "username" (姓名) # 配置邮箱 git config --global user.email "username.@email.com" ``` - 生成ssh密钥过程 - 查看是否有ssh密码:cd ~/.ssh 如果没有密钥则不会有此文件夹,有则备份删除 - 执行指令创建 mkdir ~/.ssh - 生成RSA密钥对 ssh-keygen -t rsa -C "邮箱地址" - 生成密钥对 查看公钥内容 cat ~/.ssh//Users/qingxing/.ssh/id_rsa': Hi qingxing! You've successfully authenticated, but GITEE.COM does not provide shell access..pub - 查看密钥 将公钥内容全部赋值并粘贴(注:公钥内容以ssh-rsa开头) - 粘贴地址 : https://gitee.com/profile/sshkeys - 将地址添加到gitee 添加完公钥后进行测试公钥(测试SSH连接) - ssh -T git@gitte.com 当总段提示 welcome to Gitee.com,yourname! 表示连接成功 至此以后只要拷贝ssh链接地址,然后利用git指令即可进行相关操作! ### 7、创建项目微服务 - 商品服务(product)、仓储服务(ware)、订单服务(order)、优惠券服务(coupon)、用户服务(member) - 共同: - 都要导入web、openfeign依赖 - 每一个服务,包名:com.qx.gulimall.xxx(product/order/ware/coupon/member) - 模块名:guli-xxx ### 8、导入人人快速开发平台vue(renren-fast-vue) - 安装(在该vue项目下终端使用命令) ``` npm install ``` - 运行 ``` npm run dev ``` - 运行时错误情况 - node-sass安装导入失败 - ``` Module build failed: Error: Missing binding D:\projects\gulimall-front\renren-fast-vue\node_modules\node-sass\vendor\win32-x64-64\binding.node Node Sass could not find a binding for your current environment: Windows 64-bit with Node.js 10.x Found bindings for the following environments: - Windows 64-bit with Node.js 10.x This usually happens because your environment has changed since running `npm install`. Run `npm rebuild node-sass --force` to build the binding for your current environment. ``` - 处理:重新构建 ``` npm rebuild node-sass ``` - 情况2:python未下载 - ``` gyp verb check python checking for Python executable "python2" in the PATH gyp verb `which` failed Error: not found: python2 gyp verb `which` failed at getNotFoundError (D:\projects\gulimall-front\renren-fast-vue\node_modules\which\which.js:13:12) ``` - 下载安装python ### 9、导入renren-generator - 修改application.yml中的数据库配置 - 修改generator.properties中的生成文件属性 - 导入到SQL文件数据库 - 错误日志:sqlyog不显示数据库名 - 解决:连接数据时,不用写数据/库这一列 ![1594023551547](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594023551547.png) ### 10、使用renren-generator生成基础类 - 修改配置文件 ``` 1、改变连接数据库 2、改变主类(mainPath)、包名(package)、模块名(module)、表前缀(tablePrefix) ``` - 在其网页端生成代码 - 将代码导入到微服务模块中 - 创建gulimall-common模块,导入相关类并添加依赖 - 使其他微服务模块导入common模块,作为该项目的基础公共类 ``` com.qx.gulimall gulimall-common 0.0.1-SNAPSHOT compile ``` ### 11、整合Mybatis-Plus、初始化项目 - 导入mysql连接依赖(mysql-connector),mybatis-plus依赖 - 编写数据源配置(url、driver-class-name、password、username) - 配置mybatis-plus(包扫描规则、日志) ``` mybatis-plus: # 默认扫描mapper包 # classpath:只扫描当前resources类路径下的文件, # classpaths:扫描整个类路径下文件,包括java、resources mapper-locations: classpath:**/mapper/**/*.xml # Mybatis-Plus日志 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` - 在启动类上添加mapper类扫描 ``` @MapperScan("com.qx.gulimall.*.dao") ``` ### 12、使用技术方案 - SpringCloud Alibaba - Nacos:注册中心(服务发现/注册) - SpringCloud Alibaba - Nacos:配置中心(服务配置管理) - SpringCloud Ribbon:负载均衡 - SpringCloud Feign:声明式HTTP客服端(调用远程服务) - SpringCloud Alibaba - Sentinel:服务容错(限流、降级、熔断) - SpringCloud - Gateway:Api网关(webflux编程模式) - SpringCloud - Sleuth:调用链监控 - SpringCloud Alibaba - Seata:原Fescar,即分布式事务解决方案 ### 13、导入nacos注册中心和配置中心 - 在common模块中添加依赖 ``` com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` - 在application.yml中配置参数 ``` # 服务发现 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 # 微服务名(必须要写,dataID生成需要用到) spring.application.name=×××.××× ``` - 在启动类添加服务发现(springcloud2.0后可不写) ``` @EnableDiscoveryClient ``` - 出现错误 日志 ``` c.a.cloud.nacos.NacosConfigProperties : create config service error!properties=NacosConfigProperties{serverAddr='null', encode='null', group='DEFAULT_GROUP', prefix='null', fileExtension='properties', timeout=3000, endpoint='null', namespace='null', accessKey='null', secretKey='null', contextPath='null', clusterName='null', name='null', sharedDataids='null', refreshableDataids='null', extConfig=null},e=, com.alibaba.nacos.api.exception.NacosException: java.lang.reflect.InvocationTargetException ``` 原因: 1、将nacos服务发现参数写错了 ``` # 错误写法 spring.cloud.nacos.config.server-addr=127.0.0.1:8848 # 应该写成这样 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 ``` 2、导错依赖:将nacos配置中心导入而未配置bootstrap.yml文件 ``` com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` bootstrapProperties-gulimall-coupon-dev.properties - 导入配置中心 ``` com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` - 创建bootstrap.yml配置文件 ``` spring: application: name: gulimall-coupon cloud: nacos: config: server-addr: localhost:8848 ``` ### 14、Nacos基本原理 - #### Nacos Config 数据结构 ``` Nacos Config 主要通过 dataId 和 group 来唯一确定一条配置,我们假定你已经了解此背景。如果不了解,请参考 Nacos 文档。 Nacos Client 从 Nacos Server 端获取数据时,调用的是此接口:ConfigService.getConfig(String dataId, String group, long timeoutMs)。 ``` - #### Spring Cloud 应用获取数据 ##### dataID 在 Nacos Config Starter 中,dataId 的拼接格式如下 ``` ${prefix} - ${spring.profiles.active} . ${file-extension} ``` - `prefix` 默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix`来配置。 - `spring.profiles.active` 即为当前环境对应的 profile,可不写。 **注意,当 activeprofile 为空时,对应的连接符 `-` 也将不存在,dataId 的拼接格式变成 `${prefix}`.`${file-extension}`** - `file-extension` 为配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension`来配置。 目前只支持 `properties` 类型。 ##### group - `group` 默认为 `DEFAULT_GROUP`,可以通过 `spring.cloud.nacos.config.group` 配置。 #### 自动注入 Nacos Config Starter 实现了 `org.springframework.cloud.bootstrap.config.PropertySourceLocator`接口,并将优先级设置成了最高(最先加载)。 在 Spring Cloud 应用启动阶段,会主动从 Nacos Server 端获取对应的数据,并将获取到的数据转换成 PropertySource 且注入到 Environment 的 PropertySources 属性中,所以使用 @Value 注解也能直接获取 Nacos Server 端配置的内容。 #### 动态刷新 Nacos Config Starter 默认为所有获取数据成功的 Nacos 的配置项添加了监听功能,在监听到服务端配置发生变化时会实时触发 `org.springframework.cloud.context.refresh.ContextRefresher` 的 refresh 方法 。 如果需要对 Bean 进行动态刷新,请参照 Spring 和 Spring Cloud 规范。推荐给类添加 `@RefreshScope` 或 `@ConfigurationProperties `注解。 #### 配置空间 ##### 命名空间:配置隔离 - 默认:public(保留空间);默认新增的所有配置都在public空间 ​ 可做环境隔离,在bootstrap.yml配置文件中指明具体命名空间( 命名空间ID)配置集:所有配置文件的集合 - 配置集ID:类似文件名 DataID:类似文件名(gulimall-ware.yaml) - 配置分组 默认所有配置集都属于:DEFAULT_GROUP ``` cloud: nacos: config: # 配置文件格式,如果是properties文件则不用配置此项 file-extension: yaml # 命名空间 namespace: 2239f495-6d59-440d-ad73-e661374610af # 组名 group: prod # 新增额外配置集 ext-config: # 配置集ID - data-id: aliyun.yaml # 组名 group: aliyun # 是否动态刷新 refresh: true # 文件刷新时刷新 ``` ### 15、SpringCloud Gateway - 添加依赖(common和gateway) ``` org.springframework.cloud spring-cloud-starter-gateway ``` - 配置application.properties ``` spring.application.name=gulimall-gateway spring.cloud.nacos.discovery.server-addr=localhost:8848 ``` - 配置bootstrap.yml ``` spring: application: name: gulimall-gateway cloud: nacos: # nacos配置属性 config: # 名称空间 namespace: 4df967df-2df4-4560-b0a3-d19cb02abd3a # nacos运行地址 server-addr: localhost:8848 # 如果nacos中配置文件是yaml格式的,则file-extension必须写明是yaml文件, # 默认properties,否则会导致服务启动时找不到配置文件,注册出错 file-extension: yaml ``` - 在nacos网页新建gulimall-gateway.yaml(为yaml格式时,**必须写上file-extension**) ``` server.port=88 spring.cloud.nacos.discovery.server-addr=localhost:8848 spring.cloud.nacos.discovery.namespace=4df967df-2df4-4560-b0a3-d19cb02abd3a spring: cloud: # 网关 gateway: # 路由 routes: # 路由ID - id: baidu-test # 目的URL uri: http://www.baidu.com # 断言 predicates: - Query=uri,baidu - id: qq-test uri: http://www.qq.com predicates: - Query=param,qq ``` - 启动类排除数据源自动配置 ``` @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) ``` - Spring Cloud Gateway 核心概念 - 路由。路由是网关最基础的部分,路由信息有一个ID、一个目的地URL、一组断言和一组Filter组成。如果断言为真,则说明请求的URL和配置匹配 - 断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自http request中的任何信息,比如请求头和参数等。 - 过滤器。一个标准的Spring webFilter。Spring Cloud Gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行处理。 ### 16、Vue - 安装 ``` # 最新稳定版 npm install vue ``` - vue声明式渲染 ``` let vm = new Vue({ el: "#app",//绑定元素 data: { //封装数据 name: "张三", num: 1 }, methods:{ //封装方法 cancle(){ this.num -- ; }, hello(){ return "1" } } }); ``` - v-model 双向绑定,模型变化,视图变化,反之亦然 ```

{{name}} ,非常帅,有{{num}}个人为他点赞{{hello()}}

``` - 时间处理 v-xx:指令 - 1、创建vue实例,关联页面的模板,将自己的数据(data)渲染到关联的模板,响应式的 - 2、指令来简化对dom的一些操作 - 3、声明方法来做更复杂的操作 v-on:指令:按钮的点击事件,可以简写为 "`@`" v-html:不会对HTML标签转义,直接在浏览器上显示data所设置的内容 v-text:会对HTML标签转义 - ```html
{{msg}} {{1+1}} {{hello()}}

``` {{msg}}:差值表达式,可以完成数据渲染、数学运算和方法调用 el:用来绑定数据 data:用来封装数据 methods:用来封装方法 v-bind:单向绑定,模型变化,视图变化 事件冒泡: - ```
大div
小div
去百度
``` - 上面嵌套div中,如果点击了内层的div,则外层的div也会被触发;这种问题可以用事件修饰符来完成 ```
大div
小div
去百度
``` v-for遍历循环 ​ 格式 - ``` v-for="(item,index) in items" :key="index" ``` filter过滤器 - Vue.filter(name,callback(param)) - name:管道名 - callback(param):回调函数 - |:管道符,表示使用过滤操作 ```html
``` vue组件 - ```html
``` 注意: - 组件也是一个Vue实例,因此它将在定义时也会接受:data、methods、生命周期函数等 - 不同的是组件不会与页面的元素绑定,否则就无法复用了,因此没有el属性 - 但是组件渲染需要html模板,所以增加了template属性,值就是HTML模板 - 全局组件定义完毕,任何vue实例都可以直接在HTML中通过组件名称来使用组件了 - data必须是一个函数,不再是一个对象 声明周期与钩子函数 - 每个Vue实例在创建时都要经过一系列的初始化过程:创建实例,转载模板,渲染模板等等。 Vue为声明周期中的每个状态都设置了钩子函数(监听函数)。每当Vue实例处于不同的生命周期时,对应的函数就会被触发调用 - 生命周期:创建==》销毁 ![Vue 实例生命周期](https://cn.vuejs.org/images/lifecycle.png) ### 17、ElementUI - 安装 ``` npm install element-ui -S ``` - 在main.js中写入以下内容 ``` import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) ``` ### 18、在页面显示商品树形结构 - 前置工作:在Gateway网关中添加路由 - ```yaml - id: product_route uri: lb://gulimall-product predicates: - Path=/api/product/** filters: - RewritePath=/api/(?/?.*),/$\{segment} # 该节点必须在下面,不然网关会走先下面的断言,并转发到renren-fast - id: renren-fast uri: lb://renren-fast predicates: - Path=/api/** ``` - 路由规则:精确的路由规则放置到模糊的路由规则的前面,否则,精确的路由规则将不会被匹配到,类似于异常体系中的try catch子句中异常的处理 - 编写后端代码 service ```java @Override public List listTree() { // 查找所有数据 List entityList = baseMapper.selectList(null); // 组装数据 List returnList = entityList // 转换为流的形式 .stream() // 过滤掉父节点不是0的数据 .filter(categoryEntity -> { return categoryEntity.getParentCid() == 0; }) // 组装数据 .map(categoryEntity -> { // 查找子节点 List childrens = getChildrens(categoryEntity, entityList); // 设置子节点 categoryEntity.setChildren(childrens); return categoryEntity; }) // 排序 .sorted((menu1,menu2) -> { return (menu1.getSort() == null ? 0 : menu1.getSort()) - ( menu2.getSort() == null ? 0 : menu2.getSort()); }) .collect(Collectors.toList()) ; return returnList; } // 递归查找所有菜单的子菜单 private List getChildrens(CategoryEntity root,List all){ // 生成children List entities = all .stream() .filter(categoryEntity -> { // 遍历的该节点ID与根节点ID是否相同 return categoryEntity.getParentCid().equals(root.getCatId()); }) .map(categoryEntity -> { // 递归,找到当前菜单子菜单 // root.setChildren(getChildrens(categoryEntity, all)); 这是查找父节点子菜单,不能查到当前节点子菜单 categoryEntity.setChildren(getChildrens(categoryEntity, all)); return categoryEntity; }) .sorted((menu1, menu2) -> { return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()); }) .collect(Collectors.toList()); return entities; } ``` - 跨域: 指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。 - 同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域; ​ 即端口号前面地址都要相同 - 跨域流程 非简单请求(put、delete)等等,需要先发送预检请求 ![1594220314102](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594220314102.png) - 在网关微服务中添加配置类 ```java @Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter(){ UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.setAllowCredentials(true); source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } } ``` - 前端 - 在static->index.js中改变api接口请求地址 ``` window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api' ``` - 使用ElementUI的树形节点 ```vue {{ node.label }} @click="() => append(data)"> Append @click="() => remove(node, data)"> Delete ``` ```vue ``` - 逻辑删除 - 在yaml文件中配置全局删除规则 ```yaml mybatis-plus: global-config: db-config: # logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) ``` - 在实体类中配置当前删除字段规则 ```java /** * 是否显示[0-不显示,1显示] */ @TableLogic(value = "1",delval = "0") private Integer showStatus; ``` - 设置日志级别 ```yaml logging: level: com.bigdata.gulimall.product: debug ``` - 错误日志: **情况1**:使用gateway网关的`lb://renren-fast`找不到具体微服务(503) 网站报错信息: ``` Whitelabel Error Page This application has no configured error view, so you are seeing this as a fallback. [c8234ee8-9] There was an unexpected error (type=Service Unavailable, status=503). ``` 控制台日志: ``` c.netflix.loadbalancer.BaseLoadBalancer : Client: bootstrapProperties-gulimall-product instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=bootstrapProperties-gulimall-product,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client bootstrapProperties-gulimall-product initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=bootstrapProperties-gulimall-product,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@f657dc0 ``` 解决:使用地址代替 ``` // 解决:使用http://localhost:8080 routes: - id: admin-route uri: http://localhost:8080 ``` 原因:不知道为什么将gateway网关的配置文件放在nacos的自定义命名空间,就找不到其他方服务,只有放在 public 公共空间才行 - 网上找了许多都不是具体原因 eg:#注意routes下面的id前面的 `-` 要与routes对齐,不然调用服务时会出现503错误 - 07/09:找到原因了: - 在配置网关路由时,必须先配置nacos的服务发现地址:discovery:server-addr,否则网关回报找不到服务异常(不是主要原因) - 必须将gateway的配置文件放在nacos的public空间才能找到其他微服务,放在给gateway网关自己生成的命名空间就找不到其他服务。 - **情况2:**Idea聚合项目的module变灰、多root问题 原因:未在父工程中导入该模块 解决: ``` gulimall-gateway ``` ### 19、前端商品品牌 - ### 替换显示状态 - 前端 ```vue ``` - 后端 1、将gulimall-third-party微服务注册到nacos中 - 在 “gulimall-gateway” 中配置路由规则: ```yaml - id: third_party_route uri: lb://gulimall-gateway predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?/?.*),/$\{segment} ``` - 创建命名空间 “ gulimall-third-party ” 并编写 gulimall-third-party .yml文件 ```yaml server: port: 9050 spring: application: name: gulimall-third-party cloud: alicloud: access-key: LTAI4G6sLYD3UTuX1NkAbbWx secret-key: NrrUFkrQAAUmnmQvjsClwXZD8HpAG7 oss: endpoint: oss-cn-beijing.aliyuncs.com bucketName: gulimall-qx # 日志输出 logging: level: com.qx.gulimall.product: debug ``` - 编写bootstrap.properties文件 ```properties spring.application.name=gulimall-third-party spring.cloud.nacos.config.server-addr=localhost:8848 spring.cloud.nacos.config.namespace=8cad2ad2-ea07-4fa1-9563-ec32751c108a spring.cloud.nacos.discovery.namespace=8cad2ad2-ea07-4fa1-9563-ec32751c108a spring.cloud.nacos.discovery.server-addr=localhost:8848 # 如果nacos中配置文件为yaml格式,则必须写明此项 spring.cloud.nacos.config.file-extension=yaml ``` - 创建OssController编写代码 ```java @RequestMapping("/thirdparty/oss") @RestController public class OssController { @Autowired private OSS ossClient; @Value("${spring.cloud.alicloud.access-key}") private String accessKey; @Value("${spring.cloud.alicloud.secret-key}") private String secretKey; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpointKey; @Value("${spring.cloud.alicloud.oss.bucketName}") private String bucketName; @RequestMapping("policy") public R getPoliceoGet() { String accessId = accessKey; // 请填写您的AccessKeyId。 String accessKey = secretKey; // 请填写您的AccessKeySecret。 String endpoint = endpointKey; // 请填写您的 endpoint。 String bucket = bucketName; // 请填写您的 bucketname 。 String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。 // String callbackUrl = "http://88.88.88.88:8888"; // 定义文件夹名称 String dateTime = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); String dir = dateTime + "/"; // 用户上传文件时指定的前缀。 // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey); Map respMap = null; try { long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。 PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); respMap = new LinkedHashMap(); respMap.put("accessid", accessId); respMap.put("policy", encodedPolicy); respMap.put("signature", postSignature); respMap.put("dir", dir); respMap.put("host", host); respMap.put("expire", String.valueOf(expireEndTime / 1000)); // respMap.put("expire", formatISO8601Date(expiration)); } catch (Exception e) { // Assert.fail(e.getMessage()); System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return R.ok().put("data",respMap); } } ``` - 在前段执行上传时,有跨域访问限制,需要在oss控制台配置跨域规则 ``` Access to XMLHttpRequest at 'http://gulimall-images.oss-cn-beijing.aliyuncs.com/' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. ``` ![https://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/assets/img/zh-CN/2719914951/p12308.png](https://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/assets/img/zh-CN/2719914951/p12308.png) - 服务端签名直传 #### 背景 采用JavaScript客户端直接签名(参见[JavaScript客户端签名直传](https://help.aliyun.com/document_detail/31925.html#concept-frd-4gy-5db))时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。 #### 原理介绍 [![img](http://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/assets/img/6875011751/p1472.png)](http://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/assets/img/6875011751/p1472.png) 服务端签名后直传的原理如下: 1. 用户发送上传Policy请求到应用服务器。 2. 应用服务器返回上传Policy和签名给用户。 3. 用户直接上传数据到OSS。 - 在brand.vue中替换logo为图片 ```vue ``` - 使用elemen提供的自定义规则 ```js firstLetter: [ // 使用自定义规则验证表单有效性 {validator: (rule, value, callback) => { if (value === '') { callback(new Error('请输入首字母')) // 正则表达式错误写法:/^[a-Z]$/ } else if (!/^[A-z]$/.test(value)) { // 使用正则表达式定义规则 callback(new Error('检索首字母必须为a-Z的单个字母')) } else { callback() } }, trigger: 'blur'} ], sort: [ // { required: true, message: '排序不能为空', trigger: 'blur' } {validator: (rule, value, callback) => { if (value === '') { callback(new Error('排序不能为空')) } else if (!/^\d{1,}$/.test(value)) { callback(new Error('排序字段只能为数字')) } else { callback() } }, trigger: 'blur'} ] ``` - 使用``标签后,报异常 日志: ``` [Vue warn]: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option. ``` 原因:使用的ElementUI组件未引入``标签 解决:手动引入 - 在`src/element-ui/index.js`中配置使用`` ```js import { …… // 引入组件 Image } from 'element-ui' …… // 使用组件 Vue.use(Image) ``` - 使用正则表达式报错 - 错误日志 ``` Invalid regular expression: /^[a-Z]$/: Range out of order in character class (70:25) You may need an appropriate loader to handle this file type. ``` - 原因:正则表达式书写错误/^[a-Z]$/,没有改正则表达式,正确写法:/^[A-z]$/ ### 20、JSR303效验 - 使用效验注解 - @NotNull : 注解元素禁止为null,能够接收任何类型 - @NotEmpty : 该注解修饰的字段不能为null或"" ,支持Collection、Map、Array - @NotBlank : 该注解不能为null,并且至少包含一个非空白字符。接收字符序列。 - 在请求方法上开启效验注解@Valid - ``` @RequestMapping("/save") public R save( @Valid @RequestBody BrandEntity brand) ``` - 给效验的Bean后,紧跟一个BindResult,就可以获取效验的结果,拿到效验结果,自定义封装 - ```java @RequestMapping("/save") public R save( @Valid @RequestBody BrandEntity brand,BindingResult bindingResult){ System.out.println(bindingResult); // 判断结果是否有错 if(bindingResult.hasErrors()){ Map resultMap = new HashMap<>(); // 遍历所有字段错误并封装到Map中 bindingResult.getFieldErrors().forEach((item) -> { String message = item.getDefaultMessage(); String field = item.getField(); resultMap.put(field,message); }); return R.error(400,"提交数据不合法!").put("data",resultMap); } else { brandService.save(brand); return R.ok(); } } ``` - 统一异常处理 - 使用SpringMVC提供的@ControllerAdvice,通过“basepackages”处理具体路径下的异常 - 代码 ```java @Slf4j // RestControllerAdvice = ControllerAdvice + ResponseBody @RestControllerAdvice(basePackages = "com.qx.gulimall.product.controller") public class ExceptionAdvice { // 标记该方法为异常处理方法 value:具体异常 @ExceptionHandler(value = MethodArgumentNotValidException.class) public R handleValidException(MethodArgumentNotValidException e){ // 记录日志 log.error("数据不合法:" + e); // 获取结果集 BindingResult bindingResult = e.getBindingResult(); // 創建map封装结果 Map map = new HashMap<>(); bindingResult.getFieldErrors().forEach((item) -> { // 将错误放入map map.put(item.getField(),item.getDefaultMessage()); }); // 使用自定义封装的结果集返回自定义内容 return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map); } @ExceptionHandler(Exception.class) public R globalException(Exception e){ log.error(e.getMessage()); return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg()); } } ``` - 分组校验功能(完成多场景下的复杂校验) - 给校验加注解,标注groups,指定具体情况下执行校验 ``` @TableId @Null(message = "品牌id自增,无需携带",groups = {AddGroup.class}) @NotNull(message = "品牌id不能为空",groups = {UpdateGroup.class}) private Long brandId; @NotBlank(message = "品牌名必须非空",groups = {AddGroup.class}) private String name; …… ``` - 给字段添加注解后,必须使用@Validated注解,并且指定groups @Validated的value方法: 指定一个或多个验证组以应用于此注释启动的验证步骤。 ```java /** * 保存 * @Valid:如果想使用效验规则,则必须添加该注解 * 如果字段使用Groups指定了,那么必须使用@Validated注解,并且指定group接口类 */ @RequestMapping("/save") public R save( @RequestBody /*@Valid*/ @Validated({AddGroup.class}) BrandEntity brand){ brandService.save(brand); return R.ok(); // } } ``` - 自定义校验规则 - 编写一个自定义的校验注解 ```java @Documented // 指定自定义的校验注解 @Constraint(validatedBy = { ListValueConstraintValidator.class }) // 该注解可以作用的地方 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) // 校验时机 @Retention(RUNTIME) public @interface ListValue { // 自定义消息接收路径 String message() default "{com.qx.common.valid.message}"; // 分组 Class[] groups() default { }; // Class[] payload() default { }; // 自定义数组值 int[] value() default { }; } ``` - 编写一个自定义的校验器 ```java public class ListValueConstraintValidator implements ConstraintValidator { // 初始化一个set Set set = new HashSet<>(); // 校验初始化 @Override public void initialize(ListValue constraintAnnotation) { int[] value = constraintAnnotation.value(); for (int i : value) { // 添加到set中 set.add(i); } } /**value:判断要校验的内容*/ @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return set.contains(value); } } ``` - 关联自定义的校验器和自定义的校验注解 ```java // 指定自定义的校验注解,可以指定多个校验器 @Constraint(validatedBy = { ListValueConstraintValidator.class }) ``` - 使用 ```java /** * 显示状态[0-不显示;1-显示] 必须指定value = {0,1} */ @ListValue(value = {0,1},groups = {AddGroup.class, UpdateStatusGroup.class,UpdateGroup.class}) private Integer showStatus; ``` - @Valid注解不生效,参数不能正确效验(转) - spirngboot升级到2.3之后,hibernate-validator消失 原版本会有, 2.3中已经删除了 ``` org.hibernate.validator hibernate-validator 6.0.17.Final compile ``` - 解决:手动引入依赖 ``` org.springframework.boot spring-boot-starter-validation ``` - 启动renren-fast失败 - 日志 ``` javax.management.InstanceNotFoundException: org.springframework.boot:type=Admin,name=SpringApplication at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getMBean(DefaultMBeanServerInterceptor.java:1095) ``` - 原因:未加载配置文件 - 解决:去掉下面这两个**√** ![1594610545587](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594610545587.png) - 校验注解写错,报错 - 错误日志 ``` avax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.lang.Integer'. Check configuration for 'showStatus' ``` - 错误位置 ``` @NotBlank private Integer showStatus; ``` - 原因:@NotBlank只能处理字符类型数据,showStatus为int类型数据 - 解决:使用@NotEmpty ``` @NotNull private Integer showStatus; ``` ### 21、商品SPU和SKU管理(转) - 认识SPU和SKU - #### SPU = Standard Product Unit (标准化产品单元) - SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准信息的集合,该集合描述了一个产品的特性。 - #### SKU = Stock Keeping Unit (库存量单元) - SKU即库存进出计量的单位。SKU是物理上不可分割的最小存货单元。也就是锁一款商品,可以根据SKU来确定具体的货物存量。 ``` 如一件M码(四个尺码:S码、M码、L码、X码)的粉色(三种颜色:粉色、黄色、黑色)Zara女士风衣,其中M码、粉色就是一组SKU的组合。 SKU在生成时, 会根据属性生成相应的笛卡尔积,根据一组SKU可以确定商品的库存情况,那么上面的Zara女士风衣一共有4 * 3 = 12个SKU组合。 M码+粉色这两个属性组合被称为一组SKU、因为SKU是物理上不可分割的最小存货单元,单凭尺寸或者颜色是没有办法确认这款商品的库存情况。 同理商家进货补货也是通过SKU来完成的,试问淘宝店家跟供货商说我要100件红色女士风衣?供应商知道该怎么给他备货吗? 显然是不知道的。因为还欠缺了另外的一个销售属性【尺码】。 ``` - SKU 属性(会影响到库存和价格的属性, 又叫销售属性) - #### SKU和商品之间的关系 1)SKU(或称商品SKU)指的是商品子实体。 2)商品SPU和商品SKU是包含关系,一个商品SPU包含若干个商品SKU子实体,商品SKU从属于商品SPU。 3)SKU不是编码,每个SKU包含一个唯一编码,即SKU Code,用于管理。 4)商品本身也有一个编码,即Product Code,但不作为直接库存管理使用。 - 父子组件传递数据 - 子组件给父组件传递数据,事件机制:子组件给父组件发送一个事件,携带上数据 this.$emit("定义事件名",参数……) - 公共组件 - 商品——category - 前端 ```vue ``` - 属性分组 - 前端——attgroup ```vue ``` - 前端——attr-add-or-update ```vue …… …… ``` - 前端——category-cascader ```vue ``` - 后端 ```java /** * 商品子菜单(数据表中不存在) * @JsonInclude(value = JsonInclude.Include.NON_EMPTY):设置数据包含,表示当数据为“NON_EMPTY“不为空才设置 */ @TableField(exist = false) @JsonInclude(value = JsonInclude.Include.NON_EMPTY) private List children; ``` ```java /** * 查找当前分类id的三级id * @param catelogId * @return */ @Override public Long[] getCatlogPath(Long catelogId) { List longList = new ArrayList<>(); // List longListResult = recursionSearchPidByCatId(catelogId, longList); recursionSearchPidByCatId(catelogId, longList); longList.add(catelogId); /*Long [] ids = new Long[3]; ids[0] = catelogId; for (int i = 1; i < longListResult.size(); i++) { // 从i-1 == 0 开始 ids[i] = longListResult.get(i-1); } // 以上for方法可以使用toArray转为Long数组 // longList.toArray(new Long[longList.size()]) // 如果在递归方法插入之前,必须使用倒序,因为前段数据需要这样排列 Collections.reverse(longList); */ // java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Long; at com.qx.gulimall.product.service.impl.CategoryServiceImpl.getCatlogPath(CategoryServiceImpl.java:130) ~[classes/:na] // 直接强转,List会直接转为Object类型,不能转为Long类型 return longList.toArray(new Long[longList.size()]); } /** * 递归查找 * @param catelogId * @return */ private List recursionSearchPidByCatId(Long catelogId , List idList){ // 设置查询条件 QueryWrapper wrapper = new QueryWrapper().select("parent_cid").eq("cat_id", catelogId); // 查询 List objects = baseMapper.selectObjs(wrapper); // 类型转换 long id = Long.parseLong(objects.get(0).toString()); // 当父id不为0时,表示还未遍历到根节点 if(id != 0){ // 递归 recursionSearchPidByCatId(id,idList); // 先递归了再将id存入集合,避免再次将集合倒序排列 idList.add(id); } /*else{ // 遍历到根节点时,将当前id添加进集合,如果将添加方法置为递归方法之前可以执行下面操作,否则会造成一次重复添加 idList.add(catelogId); }*/ // 这样会每个造成重复添加 // idList.add(catelogId); return idList; } ``` - - 错误情况1 - 前台页面只出现了一个分类 - 页面 ![1594705561228](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1594705561228.png) - 原因 未将排序字段逆序,前端只能识别第一个字段 需要字段 [6 14 255] 得到字段 [255 6 14] - 解决方法1:将其逆序排列 ``` Collections.reverse(longList); ``` - 解决方法2:递归后在添加 ```java recursionSearchPidByCatId(id,idList); // 先递归了再将id存入集合,避免再次将集合倒序排列 idList.add(id); ``` - 补充一:Collections ```java public static void main(String[] args) { List list = new LinkedList(); for ( int i = 0 ; i < 9 ; i ++ ) { list.add( " a " + i); } Collections.sort(list); // 顺序排列 System.out.println(list); Collections.shuffle(list); // 混乱的意思 System.out.println(list); Collections.reverse(list); // 倒序排列 System.out.println(list); System.out.println(Collections.binarySearch(list, " a5 " )); // 折半查找 } ``` - vue中的`.sync`(子组件控制父组件中元素) - api: `update:myPropName` - eg:在一个包含 `title` prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图: ```vue this.$emit('update:title', newTitle) ``` - ```vue
``` - pubsub、publish报错,无法查询品牌信息的请求 - 1、npm install --save pubsub-js - 2、在src下的main.js中引用 - import PubSub from 'pubsub-js' - Vue.prototype.PubSub = PubSub - 安装失败 - 日志 ``` ERROR Failed to compile with 2 errors 22:36:08 This dependency was not found: * pubsub-js in ./src/main.js, ./node_modules/babel-loader/lib!./node_modules/vue-loader/lib/selector.js?type=script&index=0&bustCache!./src/views/modules/common/brand-select.vue ``` - 解决,在package.json中找到pubsub-js ```json "dependencies": { …… // 这里应该为对应版本信息,而不是文件夹或文件 // "pubsub-js": "node_modules/pubsub-js", "pubsub-js": "^1.6.0", …… }, ``` ### 22、规格参数新增与VO - 说明: - 规格参数新增时,请求的URL: http://localhost:88/api/product/attr/base/list/0?t=1588731762158&page=1&limit=10&key= - 当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范 ``` @TableField(exist = false) private Long[] catelogPath; ``` - 比较规范的是,新建一个vo文件夹,将美中不同的对象,按照它的功能进行了划分。 ![1588732152646](https://gitee.com/cosmoswong/markdownblog/raw/master/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/images/1588732152646.png) - 在``中读取当前行数据 - ```vue ``` - 后端 - ```java @Transactional(rollbackFor = {Exception.class}) @Override public void saveAttrVo(AttrVo attr) { // 复制属性 AttrEntity attrEntity = new AttrEntity(); BeanUtils.copyProperties(attr,attrEntity); this.save(attrEntity); // 保存冗余字段 Long attrGroupId = attr.getAttrGroupId(); Long attrId = attrEntity.getAttrId(); AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity(); relationEntity.setAttrGroupId(attrGroupId); relationEntity.setAttrId(attrId); attrAttrgroupRelationService.save(relationEntity); } ``` ### 23、属性 - 前端 - category ```vue ``` - attr-add-or-update ```vue ``` - category-cascader ```vue ``` - 后端 - ```java @Service("attrService") public class AttrServiceImpl extends ServiceImpl implements AttrService { @Autowired private CategoryService categoryService; @Autowired private AttrGroupService attrGroupService; @Autowired private AttrAttrgroupRelationService attrAttrgroupRelationService; @Transactional(rollbackFor = {Exception.class}) @Override public void saveAttrVo(AttrVo attr) { // 复制属性 AttrEntity attrEntity = new AttrEntity(); BeanUtils.copyProperties(attr,attrEntity); this.save(attrEntity); // 保存冗余字段 Long attrGroupId = attr.getAttrGroupId(); Long attrId = attrEntity.getAttrId(); AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity(); relationEntity.setAttrGroupId(attrGroupId); relationEntity.setAttrId(attrId); attrAttrgroupRelationService.save(relationEntity); } @Override public AttrResVo getInfoById(Long attrId) { // 查询基本参数 AttrEntity attrEntity = this.getById(attrId); AttrResVo attrVo = new AttrResVo(); // 复制属性 if(attrEntity != null){ BeanUtils.copyProperties(attrEntity,attrVo); // 通过catelogId查询catelog Long catelogId = attrEntity.getCatelogId(); CategoryEntity categoryEntity = categoryService.getById(catelogId); String categoryName = null; if(categoryEntity != null){ categoryName = categoryEntity.getName(); attrVo.setAttrGroupName(categoryName); } /*// 通过关联信息查询分组 AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationService.getOne(new QueryWrapper().eq("attr_id", attrId)); String attrGroupName = null; if(relationEntity != null){ Long attrGroupId = relationEntity.getAttrGroupId(); AttrGroupEntity groupEntity = attrGroupService.getById(attrGroupId); attrGroupName = groupEntity.getAttrGroupName(); attrVo.setAttrGroupId(attrEntity.getAttrId()); attrVo.setAttrGroupName(attrGroupName); }*/ // 查出关联中间表--》attrGroupId ArrayList attrGroupNameList = getAttrGroupNameList(attrEntity); attrVo.setAttrGroupNameList(attrGroupNameList); // 设置catlogPath路径 Long[] catlogPath = categoryService.getCatlogPath(catelogId); attrVo.setCatelogPath(catlogPath); } return attrVo; } @Override public PageUtils queryTypePage(Map params, Long catelogId, String type) { // 创建包装类对象 attr_type:0-销售属性,1-基本属性 QueryWrapper wrapper = new QueryWrapper(). eq("attr_type", type.equalsIgnoreCase("base")? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() :ProductConstant.AttrEnum.ATTR_TYPE_SALE.getMsg()); // 判断catelogId是否为0 if(catelogId != 0){ wrapper.eq("catelog_id",catelogId); } // 封装参数 if(params != null && params.size() > 0){ // 检索关键字 Object key = params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.and(queryWrapper -> { // 拼接条件 queryWrapper.eq("attr_id",key).or().like("attr_name",key); }); } } IPage page = this.page( new Query().getPage(params), wrapper ); if(page.getSize() != 0){ System.out.println(page); PageUtils pageUtils = new PageUtils(page); List records = // 使用流式转换重新封装数据 page.getRecords().stream() .map( (attrEntity) -> { // 复制属性 AttrResVo attrResVo = new AttrResVo(); BeanUtils.copyProperties(attrEntity, attrResVo); // 查询catelogName和groupName String catelogName = null; Long catelogIdChild = attrEntity.getCatelogId(); if (catelogIdChild != 0) { CategoryEntity categoryEntity = categoryService.getById(catelogIdChild); catelogName = categoryEntity.getName(); } // 查出关联中间表--》attrGroupId List relationList = attrAttrgroupRelationService.list( new QueryWrapper() .eq("attr_id", attrEntity.getAttrId())); // 定义多重分组 ArrayList attrGroupNameList = getAttrGroupNameList(attrEntity); attrResVo.setCatelogName(catelogName); // 将多重分组添加到集合中 attrResVo.setAttrGroupNameList(attrGroupNameList); /*// 查出关联中间表--》attrGroupId AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationService.getOne( new QueryWrapper() .eq("attr_id", attrEntity.getAttrId())); if(relationEntity != null){ Long attrGroupId = relationEntity.getAttrGroupId(); // 通过attrGroupId查询attrgroup AttrGroupEntity groupEntity = attrGroupService.getById(attrGroupId); String attrGroupName = groupEntity.getAttrGroupName(); // 设置catelogName和groupName attrResVo.setAttrGroupName(attrGroupName); } attrResVo.setCatelogName(catelogName);*/ return attrResVo; }) .collect(Collectors.toList()); // 重新设置records数据 pageUtils.setList(records); return pageUtils; } else{ return null; } } /** * 更新属性 * @param attrResVo */ @Override public void updateAttr(AttrResVo attrResVo) { } @Override public PageUtils querySalePage(Map params, Long catelogId) { // 创建包装类对象// attr_type:0-销售属性,1-基本属性 QueryWrapper wrapper = new QueryWrapper().eq("attr_type",0); // 判断catelogId是否为0 if(catelogId != 0){ wrapper.eq("catelog_id",catelogId); } // 封装参数 if(params != null && params.size() > 0){ // 检索关键字 Object key = params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.and(queryWrapper -> { queryWrapper.eq("attr_type",0).eq("attr_id",key).or().like("attr_name",key); }); } } IPage page = this.page( new Query().getPage(params), wrapper ); if(page.getSize() != 0){ System.out.println(page); PageUtils pageUtils = new PageUtils(page); List records = // 使用流式转换重新封装数据 page.getRecords().stream().map((attrEntity) -> { // 复制属性 AttrResVo attrResVo = new AttrResVo(); BeanUtils.copyProperties(attrEntity,attrResVo); // 查询catelogName和groupName String catelogName = null; Long catelogIdChild = attrEntity.getCatelogId(); if(catelogIdChild != 0){ CategoryEntity categoryEntity = categoryService.getById(catelogIdChild); catelogName = categoryEntity.getName(); } // 查出关联中间表--》attrGroupId AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationService.getOne( new QueryWrapper() .eq("attr_id", attrEntity.getAttrId())); if(relationEntity != null){ Long attrGroupId = relationEntity.getAttrGroupId(); // 通过attrGroupId查询attrgroup AttrGroupEntity groupEntity = attrGroupService.getById(attrGroupId); String attrGroupName = groupEntity.getAttrGroupName(); // 设置catelogName和groupName attrResVo.setAttrGroupName(attrGroupName); } attrResVo.setCatelogName(catelogName); return attrResVo; }) .collect(Collectors.toList()); // 重新设置records数据 pageUtils.setList(records); return pageUtils; } else{ return null; } } @Override public PageUtils queryBasePage(Map params, Long catelogId) { // 创建包装类对象 attr_type:0-销售属性,1-基本属性 QueryWrapper wrapper = new QueryWrapper().eq("attr_type",1); // 判断catelogId是否为0 if(catelogId != 0){ wrapper.eq("catelog_id",catelogId); } // 封装参数 if(params != null && params.size() > 0){ // 检索关键字 Object key = params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.and(queryWrapper -> { // 拼接条件 queryWrapper.eq("attr_id",key).or().like("attr_name",key); }); } } IPage page = this.page( new Query().getPage(params), wrapper ); if(page.getSize() != 0){ System.out.println(page); PageUtils pageUtils = new PageUtils(page); List records = // 使用流式转换重新封装数据 page.getRecords().stream() .map( (attrEntity) -> { // 复制属性 AttrResVo attrResVo = new AttrResVo(); BeanUtils.copyProperties(attrEntity, attrResVo); // 查询catelogName和groupName String catelogName = null; Long catelogIdChild = attrEntity.getCatelogId(); if (catelogIdChild != 0) { CategoryEntity categoryEntity = categoryService.getById(catelogIdChild); catelogName = categoryEntity.getName(); } // 查出关联中间表--》attrGroupId List relationList = attrAttrgroupRelationService.list( new QueryWrapper() .eq("attr_id", attrEntity.getAttrId())); // 定义多重分组 ArrayList attrGroupNameList = getAttrGroupNameList(attrEntity); attrResVo.setCatelogName(catelogName); // 将多重分组添加到集合中 attrResVo.setAttrGroupNameList(attrGroupNameList); /*// 查出关联中间表--》attrGroupId AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationService.getOne( new QueryWrapper() .eq("attr_id", attrEntity.getAttrId())); if(relationEntity != null){ Long attrGroupId = relationEntity.getAttrGroupId(); // 通过attrGroupId查询attrgroup AttrGroupEntity groupEntity = attrGroupService.getById(attrGroupId); String attrGroupName = groupEntity.getAttrGroupName(); // 设置catelogName和groupName attrResVo.setAttrGroupName(attrGroupName); } attrResVo.setCatelogName(catelogName);*/ return attrResVo; }) .collect(Collectors.toList()); // 重新设置records数据 pageUtils.setList(records); return pageUtils; } else{ return null; } } private ArrayList getAttrGroupNameList(AttrEntity attrEntity){ List relationList = attrAttrgroupRelationService.list( new QueryWrapper() .eq("attr_id", attrEntity.getAttrId())); // 定义多重分组 ArrayList attrGroupNameList = new ArrayList<>(); if(!CollectionUtils.isEmpty(relationList)){ for (AttrAttrgroupRelationEntity relationEntity : relationList) { if (relationEntity != null) { Long attrGroupId = relationEntity.getAttrGroupId(); // 通过attrGroupId查询attrgroup AttrGroupEntity groupEntity = attrGroupService.getById(attrGroupId); String attrGroupName = groupEntity.getAttrGroupName(); // 设置groupName attrGroupNameList.add(attrGroupName); } } } return attrGroupNameList; } } ``` ### 24、属性分组 - 前端 - attgroup ```vue ``` - attr-group-relation ```vue ``` - 后端 ```java @Service("attrGroupService") public class AttrGroupServiceImpl extends ServiceImpl implements AttrGroupService { @Autowired private AttrAttrgroupRelationService attrAttrgroupRelationService; @Autowired private AttrService attrService; @Autowired private AttrGroupService attrGroupService; @Override public PageUtils queryPage(Map params) { IPage page = this.page( new Query().getPage(params), new QueryWrapper() ); return new PageUtils(page); } @Override public PageUtils queryPage(Map params, Long catlogId) { // 定义包装类对象 QueryWrapper wrapper = new QueryWrapper<>(); // 当id为0时,查询所有 if(catlogId != 0){ wrapper.eq("catelog_id",catlogId); } // 初始化page对象 IPage page = null; // 自定义query对象,辅助查询 Query entityQuery = new Query<>(); if(!CollectionUtils.isEmpty(params)){ Object key = params.get("key"); // 判断key的有效性 if(key != null && !StringUtils.isEmpty(key)){ wrapper.and((obj) -> { obj.like("attr_group_name",key) .or() .like("attr_group_id",key); }); } } page = this.page(entityQuery.getPage(params),wrapper); return new PageUtils(page); } @Override public List getDetails(Long attrgroupId) { // 设置查询条件 QueryWrapper queryWrapper = new QueryWrapper().eq("attr_group_id", attrgroupId); // 查出关联关系 List relationList = attrAttrgroupRelationService.list(queryWrapper); // 创建集合接收id List attrIdList = null; if(!CollectionUtils.isEmpty(relationList)){ attrIdList = new ArrayList<>(); for (AttrAttrgroupRelationEntity relationEntity : relationList) { Long attrId = relationEntity.getAttrId(); attrIdList.add(attrId); } // 通过attrIdList查询返回 return attrService.listByIds(attrIdList); } return null; } @Override public PageUtils getNoattrRelation(Map params, Long attrgroupId) { // 获取属性分组里面还没有关联的本分类里面的其他基本属性,方便添加新的关联 QueryWrapper queryWrapper = new QueryWrapper<>(); // 不为0时,设置id if(attrgroupId != 0){ // queryWrapper.ne("attr_group_id", attrgroupId); queryWrapper.eq("attr_group_id", attrgroupId); } // 通过在分组表查询当前分组下的分类ID AttrGroupEntity groupEntity = attrGroupService.getById(attrgroupId); Long catelogId = groupEntity.getCatelogId(); // 申明属性表查询包装类封装条件 QueryWrapper wrapper = new QueryWrapper<>(); // 关联属性必须为当前分类下的数据 wrapper.eq("catelog_id",catelogId); if(params != null && params.size() > 0){ // 获取id为attr_group_id的关系表数据 List relationList = attrAttrgroupRelationService.list(queryWrapper); // 遍历关系表获取属性attrId,并将其放入wrapper中 relationList.forEach(relation -> { // 不包含关系表中attrId的数据 wrapper.ne("attr_id",relation.getAttrId()); }); // 拼接条件key Object key = params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.like("attr_name",key); } } // 查询attrId不为attrIdList中的数据 Query attrEntityQuery = new Query<>(); IPage page = attrService.page(attrEntityQuery.getPage(params), wrapper); PageUtils pageUtils = new PageUtils(page); /*IPage page = null; if(params != null && params.size() > 0){ // 使用自定义query对象 Query query = new Query<>(); page = attrAttrgroupRelationService.page(query.getPage(params), queryWrapper); }*/ // 创建pageUtils封装对象 // PageUtils pageUtils = new PageUtils(page); //Variable used in lambda expression should be final or effectively final // 为什么 Lambda 表达式(匿名类) 不能访问非 final 的局部变量呢? // 因为实例变量存在堆中,而局部变量是在栈上分配,Lambda 表达(匿名类) 会在另一个线程中执行。 // 如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了, // 而 final 类型的局部变量在 Lambda 表达式(匿名类) 中其实是局部变量的一个拷贝。 /*if(page != null){ List> records = page.getRecords().stream().map(attrAttrgroupRelationEntity -> { // 查询 Long attrId = attrAttrgroupRelationEntity.getAttrId(); // 查询attr_id不为已经使用的attrID--》ne QueryWrapper wrapper = new QueryWrapper().ne("attr_id",attrId); // key不能设置为key = "";这样使用like条件会查出全部,也不能将key设置在集合外面,不在同一个线程,当访问key变量时,key可能已经销毁了 String key = null; if(!StringUtils.isEmpty(params.get("key"))){ key = (String) params.get("key"); wrapper.or().like("attr_name", key); } // 直接将查询出来的当前集合返回,由于查询的是不等于该attrId的,故可能不止一个结果 List attrEntityList = attrService.list(wrapper); if(attrEntityList != null){ return attrEntityList; } else { return null; } }).collect(Collectors.toList()); // 创建集合组装数据 ArrayList attrReturnList = new ArrayList<>(); // 取出所有数据 for (List record : records) { attrReturnList.addAll(record); } // 设置结果集 pageUtils.setList(attrReturnList); }*/ return pageUtils; } } ``` - 错误情况一(转): - 日志 lambda表达式提示变量错误:Variable used in lambda expression should be final or effectively final... - 解决: ![img](https://oscimg.oschina.net/oscnet/1ec8636e107375d0eec8c5bef31fbd8a043.jpg) - 原因 ``` 分析:根据语法,变量必须为final.但是a已经被修改了. 为什么 Lambda 表达式(匿名类) 不能访问非 final 的局部变量呢? 因为实例变量存在堆中,而局部变量是在栈上分配,Lambda 表达(匿名类) 会在另一个线程中执行。如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了,而 final 类型的局部变量在 Lambda 表达式(匿名类) 中其实是局部变量的一个拷贝。 ``` ### 25、发布商品 - 在gateway网关中添加gulimall-member的路由 ```yaml - id: gulimall-member uri: lb://gulimall-member predicates: - Path=/api/member/** filters: - RewritePath=/api/(?/?.*),/$\{segment} ``` - 获取分类下所有分组&关联属性 ```java @Override public List getGroupWithAttr(Long catelogId) { // 0、创建AttGroupWithAttrVo接收并组装数据 List attGroupWithAttrVoList = new ArrayList<>(); // 1、通过catelogId获取当前分类ID的分组,进而获得分组ID QueryWrapper wrapper = new QueryWrapper<>(); List groupList = this.list(wrapper.eq("catelog_id",catelogId)); // 封装数据 List groupWithAttrVoList = groupList.stream().map(group -> { // 创建wrapper封装分组id QueryWrapper relationQueryWrapper = new QueryWrapper<>(); Long attrGroupId = group.getAttrGroupId(); // 定义AttGroupWithAttrVo AttGroupWithAttrVo attGroupWithAttrVo = new AttGroupWithAttrVo(); /*relationQueryWrapper.eq("attr_group_id", attrGroupId); // 通过分组ID获得属性和分组关联表,进而获得属性ID List relationEntityList = attrAttrgroupRelationService.list(relationQueryWrapper); if(relationEntityList != null){ // ①:遍历获取属性id List attrIdList = relationEntityList.stream().map(relationEntity -> { Long attrId = relationEntity.getAttrId(); return attrId; }).collect(Collectors.toList()); // attrService. // 3、通过属性 // id查询属性 List attrEntityList = attrService.listByIds(attrIdList);*/ // 使用写过的方法替换 List attrEntityList = getRelationAttr(attrGroupId); // 遍历组装group // 复制属性分组 BeanUtils.copyProperties(group, attGroupWithAttrVo); // 设置属性 attGroupWithAttrVo.setAttrs(attrEntityList); return attGroupWithAttrVo; }).collect(Collectors.toList()); return groupWithAttrVoList; } ``` - 获取分类关联的品牌 ```java @Override public List listByCatId(Long catId) { QueryWrapper wrapper = new QueryWrapper(); // 是否需要组装id if(catId != 0){ wrapper.eq("catelog_id", catId); } List brandRelationList = baseMapper.selectList(wrapper); // 去重 // 设置存储brandId的集合 // List branIdList = new ArrayList<>(); HashSet branIdList = new HashSet<>(); List collect = brandRelationList.stream().filter(brandRelation -> { // 查看branIdList是否有当前brandId Long brandId = brandRelation.getBrandId(); boolean b = branIdList.contains(brandId); if(!b){ // 将当前id添加到集合中 branIdList.add(brandId); return true; }else { return false; } })/*.map(brandRelation -> { // 将除了brandId和brandname外其他字段去重 brandRelation.setId(null); brandRelation.setCatelogId(null); brandRelation.setCatelogName(null); return brandRelation; })*/.collect(Collectors.toList()); return collect; } ``` ### 26、商品管理 - 修改日期格式 ```yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss ``` - SKU检索 ```java @Override public PageUtils queryPageByCondition(Map params) { // page=1&limit=10&key=&catelogId=0&brandId=0&min=0&max=0 QueryWrapper wrapper = new QueryWrapper<>(); // 组装条件 Object key = params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.and(w -> { w.like("sku_name",key); }); } Object catelogId = params.get("catelogId"); if(!StringUtils.isEmpty(catelogId)){ // id为0时,不设条件 if(Integer.parseInt(catelogId.toString()) != 0){ wrapper.eq("catalog_id",catelogId); } } Object brandId = params.get("brandId"); if(!StringUtils.isEmpty(brandId)){ // id为0时,不设条件 if(Integer.parseInt(brandId.toString()) != 0){ wrapper.eq("brand_id",brandId); } } Object min = params.get("min"); if(!StringUtils.isEmpty(min)){ wrapper.ge("price",min); } Object max = params.get("max"); if(!StringUtils.isEmpty(max) && max.equals(0)){ wrapper.le("price",max); } IPage page = this.page(new Query().getPage(params), wrapper); return new PageUtils(page); } ``` ### 27、仓储管理 - 合并整单未选择purchaseId ```java @Transactional(rollbackFor = Exception.class) @Override public void mergePurchase(MergeVo mergeVo) { // 采购单ID,为null表示未分配 Long purchaseId = mergeVo.getPurchaseId(); if(purchaseId == null || StringUtils.isEmpty(mergeVo.getPurchaseId())){ PurchaseEntity purchase = new PurchaseEntity(); // 设置时间 purchase.setCreateTime(new Date()); purchase.setUpdateTime(new Date()); purchase.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode()); // 保存 this.save(purchase); // 获取保存ID purchaseId = purchase.getId(); } List items = mergeVo.getItems(); // 采购单ID必须有效 Long finalPurchaseId = purchaseId; List purchaseDetailList = items.stream() // 采购单必须为新建,或者已分配才行 .filter(entity -> { // 如果前台未传id则直接放行 if(mergeVo.getPurchaseId() == null || StringUtils.isEmpty(mergeVo.getPurchaseId())){ return true; } else{ // 调用查询 PurchaseEntity sku_id = this.getById(entity); if(sku_id.getStatus().equals(WareConstant.PurchaseStatusEnum.CREATED.getCode()) || sku_id.getStatus().equals(WareConstant.PurchaseStatusEnum.ASSIGN.getCode())){ return true; } return false; } }) .map(entity -> { // 设置采购单详情 PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity(); purchaseDetailEntity.setId(entity); purchaseDetailEntity.setPurchaseId(finalPurchaseId); purchaseDetailEntity.setStatus(WareConstant.PurchaseStatusEnum.ASSIGN.getCode()); return purchaseDetailEntity; }).collect(Collectors.toList()); // 执行保存 purchaseDetailService.updateBatchById(purchaseDetailList); // 采购项必须已分配或新建才行 PurchaseEntity byId = this.getById(purchaseId); System.out.println(byId.getStatus()); System.out.println(byId.getStatus().equals(WareConstant.PurchaseStatusEnum.CREATED.getCode())); // if(byId.getStatus().equals(byId.getStatus().equals(WareConstant.PurchaseStatusEnum.CREATED.getCode()) || 多加了一个equals if(byId.getStatus().equals(WareConstant.PurchaseStatusEnum.CREATED.getCode()) || byId.getStatus().equals(WareConstant.PurchaseStatusEnum.ASSIGN.getCode())){ // 设置采购时间 PurchaseEntity purchaseEntity = new PurchaseEntity(); purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.ASSIGN.getCode()); purchaseEntity.setId(purchaseId); this.updateById(purchaseEntity); } } ``` - 领取采购单 ```java /** * 完成采购需求 * @param ids */ @Transactional(rollbackFor = Exception.class) @Override public void receivedPurchase(List ids) { // 判断当前采购单ID是否未被采购 List purchaseEntityList = ids.stream().map(id -> { // 执行采购,并返回状态 boolean flag = purchaseDetailService.updateStatusByPurchaseId(id); PurchaseEntity purchaseEntity = new PurchaseEntity(); // 为true表示当前采购项可以被采购 if(flag){ purchaseEntity.setId(id); purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.RECEIVED.getCode()); } return purchaseEntity; }).collect(Collectors.toList()); this.updateBatchById(purchaseEntityList); } /** * 设为采购状态 * @param purchaseId */ @Override public boolean updateStatusByPurchaseId(Long purchaseId) { List purchaseDetailList = this .list(new QueryWrapper().eq("purchase_id", purchaseId)); List purchaseDetailEntityList = purchaseDetailList.stream().filter(purchaseDetailEntity -> { // 当已经分配时,才执行采购 if (purchaseDetailEntity.getStatus().equals(WareConstant.PurchaseDetailStatusEnum.ASSIGN.getCode())) { return true; } return false; }).map(purchaseDetailEntity -> { purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode()); return purchaseDetailEntity; }).collect(Collectors.toList()); // 执行更新 boolean b = this.updateBatchById(purchaseDetailEntityList); return b; } ``` - 添加库存 ```java @Override public void addStock(WareSkuEntity wareSkuEntity) { Long skuId = wareSkuEntity.getSkuId(); Long wareId = wareSkuEntity.getWareId(); Integer stock = wareSkuEntity.getStock(); // 查询判断当前仓库是否还有该商品库存 List wareSkuEntityList = this.list( new QueryWrapper() .eq("sku_id", skuId) .eq("ware_id", wareId)); // 不存在则新增,存在则只改库存 if(wareSkuEntityList != null && wareSkuEntityList.size() > 0){ baseMapper.updateStock(skuId,wareId,stock); } else{ baseMapper.insert(wareSkuEntity); } } ``` mybatis.xml文件 ```xml update wms_ware_sku set stock = stock + #{stock} where sku_id = #{skuId} and ware_id = #{wareId} ``` ### 28、 获取spu规格 - 代码 ```java @Override public List listforspuBySpuId(Long spuId) { // 通过spuId获取所有attrId List productAttrValueList = productAttrValueService.list( new QueryWrapper() .eq("spu_id", spuId)); return productAttrValueList; } ``` - 在SPU管理页面,获取商品规格的时候,出现404异常,浏览器显示跳转不了 - 原因:要去的页面未在数据库中写入 ```vue attrUpdateShow(row) { console.log(row); this.$router.push({ path: "/product-attrupdate", query: { spuId: row.id, catalogId: row.catalogId } }); } ``` - 解决,在数据库中加上该条路径语句 ```sql INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0) ``` ### 29、修改商品规格 ```java @Override public void updateBatchBySpuId(Long spuId, List productAttrValueEntityList) { // 封装条件 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("spu_id",spuId); productAttrValueEntityList.forEach(entity -> { // 为每一个ProductAttrValueEntity设置属性 ProductAttrValueEntity productAttrValueEntity = new ProductAttrValueEntity(); BeanUtils.copyProperties(entity, productAttrValueEntity); productAttrValueEntity.setSpuId(spuId); this.baseMapper.updateBySpuId(productAttrValueEntity); }); // this.baseMapper.updateBatchBySpuId(attrValueEntityList); // mybatis不能在一个sql语句中执行多个更新操作 } ``` # HTTP访问控制(CORS) https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS 后台管理系统接口 https://easydoc.xyz/doc/75716633/ZUqEdvA4/Ld1Vfkcd ### 使用R传输数据 - 代码 ```java public class R extends HashMap implements Serializable { private static final long serialVersionUID = 1L; // 数据传输时,可以直接利用该泛型,避免每次都强转 private T data; public T getData() { return data; } // 设置数据,返回R可以直接链式编程 public R setData(T data) { this.data = data; return this; } ``` - 效果:不行 - 原因 ``` R对象继承了HashMap,在分布式传输时,不能直接取出数据,必须借用Map容器装载数据,使用Map的get()方法取出数据 ``` - 解决 将数据放入Map中 ```java public R setData(Object o){ this.put("data",o); return this; } public T getData(TypeReference typeReference){ // 从map中获取数据 Object data = this.get("data"); // 将数据转为JSON字符串 String s = JSON.toJSONString(data); // 将字符串转为所需要的类型 T t = JSON.parseObject(s, typeReference); // 将数据返回 return t; } ``` ### ThymeLeaf模板引擎 - 使用 - 在yaml中关闭thymeLeaf缓存 - 静态资源都放在static文件加下就可以按照路径直接访问 - 页面放在templates下,直接访问 - 页面修改不重启服务器实时更新 - 引入devtools - 修改完页面同时按下Ctrl + Shift + F9自动编译页面 - 错误情况一: - 日志 - 原因:使用thymeleaf渲染的页面,第一次跳转时,必须要写上名称空间,不然可能报找不到模板异常 - 解决 ```html ``` - 错误情况二: - 日志 ![1595768338202](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595768338202.png) ![1595768374702](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595768374702.png) - 原因: SpringBoot2.0之后css静态资源需要加上/static/,即使按住Ctrl加鼠标左键能跳转到页面也不行,必须加上。 js文件的资源则不用加上/static/,加上反而报错。 ### 使用nginx代理访问gulimall谷粒商城 - 一:直接代理到商品服务 - 浏览器访问地址会经过DNS解析,然后去根据地址请求资源 - 使用SwitchHosts改变本地host映射,使虚拟机地址(…56.10)能够直接通过gulimall.com访问 ``` # gulimall 192.168.56.10 gulimall.com ``` - 使用nginx进行反向代理,将所有来自原gulimall.com的请求,都转到商品服务 - 查看nginx.conf,发现只要在conf.d文件目录下的所有配置文件都会作为nginx的配置文件 ``` include /etc/nginx/conf.d/*.conf; ``` - 复制一份默认nginx的默认配置default.conf ``` cp default.conf gulimall.conf ``` - 修改 gulimall.conf ``` # 监听端口 listen 80; # 服务名称:必须是全路径,需要带上.com,不然不能映射到路径 server_name gulimall.com; #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; # /:不带参数地址 location / { # proxy_pass :/ 路径映射到http://192.168.1.6:9010地址 proxy_pass http://192.168.1.6:9010; } ``` - 二:代理到网关,再通过网关代理到商品微服务 - 修改nginx的配置文件nginx.conf。 添加网关微服务的地址 ``` vi nginx.conf # 必须在http代码块中修改该操作 http { …… # 配置nginx的负载均衡 upstream gulimall{ # server必须要写上,后面会根据server中配置的地址去负载 server 192.168.1.6:88; } …… } ``` - 修改gulimall.conf ``` server { # 监听端口号 listen 80; server_name gulimall.com; #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; # 地址跳转配置 location / { # 必须设置此请求头信息,不然nginx将地址代理给网关时,会丢失掉host信息 # $host:表示动态取出host proxy_set_header HOST $host; # nginx中的负载均衡,负载到配置的gulimall的地址 proxy_pass http://gulimall; } …… } ``` - 在gateway网关中配置application.yaml ``` …… ## 必须将此项路由配置到最后面,不然会覆盖前面product中的配置 - id: gulimall_host_route uri: http://localhost:9010 predicates: - Host=**.gulimall.com,gulimall.com ``` ### 压力测试 - 解释 ![1595839615009](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595839615009.png) - 性能指标 ![1595839660741](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595839660741.png) - JMeter Address Already in use - ![1595840834183](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595840834183.png) ### 性能监控 - JVM内存模型![1595842137002](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595842137002.png) ![1595842538266](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595842538266.png) ![1595842556137](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595842556137.png) - 堆 - 定义:所有对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为“GC堆”;也是我们优化最多考虑的地方。 ![1595842759834](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595842759834.png) - 垃圾回收 ![1595842786424](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595842786424.png) ![1595843013120](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595843013120.png) ### 缓存 - 哪些数据适合使用缓存 - 即时性、数据一致性要求不高的 - 访问量大且更新频率不高的数据(读多、写少) - 缓存使用流程 ![1595856213290](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1595856213290.png) ### 整合redis - 导入依赖 ``` org.springframework.boot spring-boot-starter-data-redis ``` - 在application.yaml中设置连接地址 ``` spring: redis: ## redis主机地址 host: 192.168.56.10 ``` - redis缓存穿透,缓存击穿,缓存血崩原因+解决方案(转) - 缓存处理流程 前台请求,后台先从缓存中获取数据,取到直接返回结果,未取到则从数据库中查询,r若数据为空,直接返回空结果,并返回给前台,若数据不为空,将数据设置到缓存中,并返回数据给前台。 ![img](https://img-blog.csdn.net/20180919143214712?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tvbmd0aWFvNQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - 缓存穿透:key对应的数据在数据源并不存在,每次针对key的请求从缓存中获取不到,请求都回到数据源,从而可能压垮数据源。比如用一个不同用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。 - 解决方案一:布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 - 解决方案二:如果一个查询返回的数据为空(不管数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但他的过期时间会很短,最长不超过五分钟。 - 缓存击穿:key对应的数据存在。但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期,一般都会从后端DB加载数据并设置到缓存,这个时候大并发请求可能瞬间把后端数据库压垮。 - 解决方案一:设置热点数据永不过期 - 解决方案二:加互斥锁,就是在缓存失效的时候(判断拿出来的值为空),不是立即去loaddb,而是先使用缓存工具的某些带成功操作返回值的操作(比如redis的SETNX)去set一个mutex key,当操作返回成功时,再load db的操作并设置缓存;否则,就重试整个get缓存的方法。 - 缓存雪崩:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿是指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查询数据库。 图解: ![redis2.md](https://mmbiz.qpic.cn/mmbiz_jpg/W5Wzice6Iz7gSqQ4UoogMtkSMWGWgynS8oQtvBSortCptOZwgeUyJ6a69Aib1GEVl6vLKwY7TyVWOJHBfNPyWSpg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - 解决方案一:设置缓存数据不同的过期时间 - 解决方案二:缓存数据库分布式部署时,将热尔点数据均匀分布在不同缓存数据库中 ### 分布式锁 - 业务逻辑 ![1596007629734](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596007629734.png) - 原理 我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,知道释放锁。“占坑”可以去redis,可以去数据库,可以去任何能访问到数据的地方。 等待去占坑可以采用**自旋**的方式。 - 分布式锁演进 - 阶段一 - 原理图: ![1596008205006](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596008205006.png) - 核心代码 ![1596008260476](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596008260476.png) - 出现的问题 setnx占好了位置,但业务代码异常或者程序在执行过程中宕机。未执行删除与锁逻辑,这就造成了**死锁**。 - 解决:在设置锁的时候设置好过期时间,即使没有删除,也会自动删除。 - 阶段二 - 核心代码 ![1596008580546](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596008580546.png) - 问题 在设置过期时间时,出现网络异常或者断电,导致后面所有线程都不能够获取到锁,成了**死锁** - 解决 设置过期时间和占位必须是原子的。redis支持使用set nx ex命令,既保证了占位的唯一性,又保证了在一定时间会自动过期。 - 阶段三: - 核心代码 ![1596008905724](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596008905724.png) - 问题一: 删除所直接删除 如果由于业务时间过长,锁自己过期了,我们直接删除,很有可能把别人持有的锁删除了。 - 解决一: 在设置锁的同时,设置好值,值可以采用uuid的形式,尽量避免重复,在删除锁的时候先比较值是否相等,相等才执行删除。 - 问题二: 如果正好判断是当前的值,正要删除的时候,锁已经过期,而另一个线程已经为锁设置了新的值,那么执行删除的则会是别人的锁。 - 解决二: 删除锁必须保证原子性。使用redis+lua脚本完成。 ``` Stringscript="ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end"; 保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期 ``` ### Redisson(转) - 简介 Redisson是一个在Redis的基础上实现的Java驻内存网络(In-Memory Data Grid)。塌不惊提供了一系列的分布式对象,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中的放在处理业务逻辑上。 - 项目整合 - 依赖 ``` org.redisson redisson 3.10.5 ``` - 配置类 ```java /** * 所有对Redisson的使用都是通过RedissonClient对象 * @return */ @Bean(destroyMethod = "shutdown") public RedissonClient getRedissonClient(){ // 创建配置 Config config = new Config(); // 设置单节点模式, SingleServerConfig singleServer = config.useSingleServer(); // 设置主机地址 Redis url should start with redis:// or rediss:// (for SSL connection) SSL:表示安全的连接 // 必须要带上redis://和端口号port 否则会报IllegalArgumentException(redis://)异常和StringIndexOutOfBoundsException异常(-port) singleServer.setAddress("redis://192.168.56.10:6379"); // 创建redisson RedissonClient redissonClient = Redisson.create(config); return redissonClient; } ``` - 配置文件 ```yaml spring: redis: host: 192.168.56.10 ``` - 分布式锁 - 可重入锁 ``` RLock lock = redisson.getLock("anyLock"); //阻塞式等待。默认加的锁都是30s时间。不会续期 lock.lock(); ``` - 注: 锁的自动续期,如果业务超长,运行期间自动给锁续上30秒,每次10秒,不用担心业务时间长,锁自动过期被删除掉。 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。 - 给锁限定时间 ``` // 加锁以后10秒钟自动解锁,无需调用unlock方法手动解锁 //自动解锁时间一定要大于业务的执行时间。 lock.lock(10, TimeUnit.SECONDS); ``` - 加重试锁 ``` // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } } ``` - 注: - 如果我们传递了锁的超时时间,就执行lua脚本,进行占锁,默认超时就是我们指定的时间。 - 如果我们未指定锁的超时时间,就使用30 * 1000 【LockWatchdogTimeout看门狗的默认时间】 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200704140222798.png) lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒) - 默认值30000 - 监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。如果该看门口未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况 - 可以异步执行 ```java RLock lock = redisson.getLock("anyLock"); lock.lockAsync(); lock.lockAsync(10, TimeUnit.SECONDS); Future res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS); 12341234 ``` RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误 - 读写锁(ReadWriteLock) - 基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。 - ``` 加锁:保证能读到最新数据,加锁期间,读锁是一个排它锁(互斥锁,独享锁),读锁是一个共享锁 写锁没释放就必须等待 读+读:相当于无锁,并发读 读+写:等待写锁释放 写+读:等待写锁释放 写+写:阻塞方式 只要有写,都必须等待 ``` - 公平锁 它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。 ```java // 第一种 RLock fairLock = redisson.getFairLock("anyLock"); // 最常见的使用方法 fairLock.lock(); // 第二种 // 10秒钟以后自动解锁 // 无需调用unlock方法手动解锁 fairLock.lock(10, TimeUnit.SECONDS); // 第三种 // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS); ... fairLock.unlock(); ``` 异步执行的相关方法 ```java RLock fairLock = redisson.getFairLock("anyLock"); fairLock.lockAsync(); fairLock.lockAsync(10, TimeUnit.SECONDS); Future res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS); ``` - 闭锁(CountDownLatch) 基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。 ```java RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); latch.trySetCount(1); latch.await(); 123123 // 在其他线程或其他JVM里 RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); latch.countDown(); 123123 ``` - 信号量(Semaphore) ```java /** * 可以利用信号灯来执行限流,只有在规定流量范围内,才能进入程序 * semaphore:信号灯 * @return */ @GetMapping("semphore/acquire") @ResponseBody public String trySignal() throws InterruptedException { // 获取操作对象 RSemaphore rSemaphore = redissonClient.getSemaphore("signal1"); // 设置该“signal”剩余位置 rSemaphore.trySetPermits(10); // 请求获取位置 rSemaphore.acquire(); Thread.sleep(3000); return rSemaphore.getName() + "< -- >signal acquire"; } @GetMapping("semphore/release") @ResponseBody public String releaseSignal() throws InterruptedException { RSemaphore rSemaphore = redissonClient.getSemaphore("signal1"); rSemaphore.release(5); // 释放一个位置 rSemaphore.release(); Thread.sleep(3000); return rSemaphore.getName() + "< -- >signal 释放成功"; } ``` ### 缓存数据一致性 - 双写模式 - 图解 ![1596111991831](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596111991831.png) - 原因 由于卡顿等原因,导致缓存2先执行完写缓存,而当缓存一执行写缓存时,会覆盖缓存2;这样总会有一方未能写入自己修改的数据。 - 问题:脏数据 这些数据由于未能和数据库中保持一致,所以会出现暂时的脏数据问题,必须重新修改或者缓存数据失效才会重新从数据库中查询。 读到的最新数据有延迟,但符合最终一致性。 - 失效模式 - 图解 ![1596112422446](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596112422446.png) - 原因:写入db-1关系数据库后,执行db-2 NoSQL删缓存操作完成后,恰巧一个线程又执行设置缓存操作,故这个线程设置缓存时不能设置最新的缓存。这种模式也满足最终一致性。 - 谷粒商城系统采用这种一致性解决方案 - 要想避免失效模式出现的情况,需要以下两点 - 缓存所有数据都有过期时间,数据国旗下一次查询触发主动更新 - 读写数据的时候,加上分布式的读写锁。 ### SpringCache(半转) - 概述 Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。 Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。 其特点总结如下: - 通过少量的配置 annotation 注释即可使得既有代码支持缓存 - 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存 - 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition - 支持 AspectJ,并通过其实现任何方法的缓存支持 - 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性 - 基本注解 - `@Cacheable`: Triggers cache population.:触发cache缓存 - `@CacheEvict`: Triggers cache eviction.:触发cache删除 - `@CachePut`: Updates the cache without interfering with the method execution.:更新缓存不用在方法是否执行 - `@Caching`: Regroups multiple cache operations to be applied on a method.:组合多个对缓存的操作 - `@CacheConfig`: Shares some common cache-related settings at class-level.:分享一些在类上的相同的缓存设置 - 使用SpringCache - 导入依赖(项目中有springframe则默认导入了springcache相关的依赖) ``` org.springframework.boot spring-boot-starter-cache ``` - 在application.yml中配置springcache的存储对象 ``` spring: # 指定cache缓存使用redis cache: type: redis ``` - 添加注解,开启缓存 ``` @EnableCaching ``` - **注**:cache缓存的对象必须实现序列化 ``` public static class Catelog3Vo implements Serializable{ private static final long serialVersionUID = 1L; …… ``` - 常用注解详解 - `@Cacheable`:针对方法配置,能根据方法的请求参数对其结果进行缓存 主要参数 - value:缓存名称,在spring配置文件中定义,必须指定至少一个 ​ eg: @Cacheable(value=”mycache” **/** value={”cache1”,”cache2”} - key:缓存的key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。eg: @Cacheable(value=”testcache”,key=”#userName”) - condition:缓存的条件, 可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 eg:@Cacheable(value=”testcache”,condition=”#userName.length()>2”) - `@CachePut`:能够能局方法的请求参数对其结果进行缓存,和@Cacheable不同,它每次都会触发真实方法的调用 主要参数 - 同上 - `@CacheEvict`:根据一定条件对缓存进行清空(修改数据后对缓存删除) 主要参数 - allEntries:是否清空所有缓存的内容,缺省为false,如果true,则hi清空所有的缓存 eg: @CachEvict(value=”testcache”,allEntries=true) - beforeInvocation:是否在方法执行前就清空,默认false,如果true,则在方法执行前清空缓存;默认情况下,如果方法抛出异常,则不会清空缓存 - ` @Caching `:可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。 其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200709195345427.png) - 缓存中默认行为 - 如果缓存中有,会直接从缓存中加载数据 - key默认自动生成,缓存的名字:SimpleKey[] (自主生成的key值) - 默认ttl时间 **-1** :表示永不过期 - Spring-Cache基本原理 - 和spring的事务管理类似,spring cache的关键原理就是spring AOP,通过AOP,其实现了在方法调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。 - 直接调用 - 图解 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200707170111547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RfQV9JX0hfQV9P,size_16,color_FFFFFF,t_70) - 由图示,当客服端“Calling code”时,调用一个普通类Plain Object的foo()方法的时候,是直接作用在pojo类自身对象上的,客服端拥有的时被调用者的直接引用。 - 动态代理 - Spring Cache 利用了Spring AOP的动态代理技术,即当客服端尝试调用pojo的foo()方法的时候,给它的不是pojo自身的引用,而是一个动态代理生成的代理类 - 图解 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020070717012590.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RfQV9JX0hfQV9P,size_16,color_FFFFFF,t_70) 如图所示,使用代理后,实际客服端拥有的是一个代理的引用,在调用foo()方法的时候,会首先调用proxy的foo()方法,这个时候proxy可以整体控制实际的pojo.foo()方法的入参和返回值,比如缓存结果,比如直接略过执行实际的foo()方法等,都是可以轻松做到的。 - Spring-Cache总结 - 总结 ![1596246666894](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1596246666894.png) - 不足之处 - 读模式 - 缓存穿透:查询数据不存在。解决:缓存空数据(`cache-null-values: true`) - 缓存击穿:同时查询同一个过期的 数据。解决:加锁,使用注解中配置的参数`sync = true` - 缓存雪崩:大量缓存同时过期。解决:加锁,使用注解中配置的参数`sync = true`,加随机时间—加上过期时间`time-to-live: 3600000` - 写模式(缓存与数据库一致) - 写入加锁 - 引入canal,修改或增加数据时,canal会自动同步数据库数据 - 读多写多,直接查询数据库就行 - 注:常规数据(读多写多,即时性,一致性要求不高的数据,完全可以使用spring-cache) ​ 特殊数据:特殊设计 ### 获取分类排序后的数据 - 要求 - 尽可能减少数据库的查询操作 - 尽可能重复获取相同数据次数 - 代码 ```java public List getCategoryByParentCid(List categoryList,Long catId){ // 从category中筛选出数据 List collect = categoryList.stream() .filter(category -> category.getParentCid().equals(catId)) .collect(Collectors.toList()); return collect; } @Override public Map> getCatelogWithOrder(){ // 获取ops操作对象 ValueOperations ops = stringRedisTemplate.opsForValue(); // 获取数据 String cateGoryListJson = ops.get("cateGoryList_redis"); // 初始化map Map> cateGoryList = null; // 判断redis中是否存有数据 if(StringUtils.isEmpty(cateGoryListJson)){ // 从数据库中获取数据 cateGoryList = getCatelogWithOrderFromDB(); // 将数据转为json cateGoryListJson = JSON.toJSONString(cateGoryList); // 将数据保存到redis中 ops.set("cateGoryList_redis", cateGoryListJson, 1, TimeUnit.DAYS); // 返回数据 return cateGoryList; } // 将数据转为map cateGoryList = JSON.parseObject(cateGoryListJson,new TypeReference>>(){}); return cateGoryList; } public Map> getCatelogWithOrderFromDB() { // 获取一级分类,只需要获取一级分类及其后代们的分类 List level1CatList = getLevel1Catlog(); // 获取所有分类,将下面代码中每遍历一次查询一次数据库变为只查询一次 List categoryList = this.list(); // 在collect中组装,不在map()方法中组装是因为在map()收集结束后不能获取根元素的catId, // 而在收集时组装可以获取到stream流中所有遍历的每个元素,包括catId // 通过一级分类组装二级分类、三级分类 Map> collect = level1CatList.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> { // 新建二级分类 // Catelog2Vo catelog2Vo = new Catelog2Vo(); // 查找二级分类,将在数据库中查找分类变为在所有分类中查找,降低了查询数据库的频率 // QueryWrapper wrapper = new QueryWrapper().eq("parent_cid", l1.getCatId()); List catLevel2List = getCategoryByParentCid(categoryList, l1.getCatId()); // 设置数据 // catelog2Vo.setCatalog1Id(l1.getCatId().toString()); List catelog2Vos = null; if(catLevel2List != null && catLevel2List.size() > 0){ // 查找三级分类 catelog2Vos = catLevel2List.stream().map(l2 -> { /*QueryWrapper wrapper1 = new QueryWrapper().eq("parent_cid", l2.getCatId()); List catLevel3List = this.list(wrapper1);*/ // 新建二级分类,不能将二级分类新建在获取List的外面,这样在遍历内部循环时,每个二级Catelog2Vo地址都是同一个地址(外面二级Catelog2Vo的地址) Catelog2Vo catelog2Vo = new Catelog2Vo(); catelog2Vo.setCatalog1Id(l1.getCatId().toString()); List catLevel3List = getCategoryByParentCid(categoryList, l2.getCatId()); // 设置数据 catelog2Vo.setId(l2.getCatId().toString()); catelog2Vo.setName(l2.getName()); List catLevel3s = null; // 避免子类数据空空遍历设置错误数据 if(catLevel3List != null && catLevel3List.size() > 0){ // 设置三级分类数据 catLevel3s = catLevel3List.stream().map(l3 -> { Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(); catelog3Vo.setCatalog2Id(l2.getCatId().toString()); catelog3Vo.setId(l3.getCatId().toString()); catelog3Vo.setName(l3.getName()); return catelog3Vo; }).collect(Collectors.toList()); } catelog2Vo.setCatalog3List(catLevel3s); return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); System.out.println(collect); return collect; } ``` - 总结 - 使用redis作为临时存储 - 遍历数据时,不能将当前遍历的对象放入map()方法外部,这样会造成每次遍历的数据都会变为最后一次遍历的数据 ### CompletableFuture异步编排 ### 检索 - ``` URLEncoder.encode(filename,"utf-8") ``` -Xmn:新生代 ### session设置超时时长 ### 消息队列-RabbitMQ 作用:应用解耦、流量削谷、异步处理 点对点、发布订阅 ![1598505586080](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1598505586080.png) 使用: 类:RabbitAutoConfiguration -》 配置连接工厂:RabbitConnectionFactoryCreator -》spring封装的rabbit模板操作配置RabbitTemplateConfiguration(RabbitTemplateConfigurer、RabbitTemplate、AmqpAdmin)-》消息模板配置:MessagingTemplateConfiguration(RabbitMessagingTemplate) RabbitConnectionFactoryCreator -> RabbitProperties默认配置类 -》 配置好了基本配置信息 启动类配置:@EnableRabbit(开启rabbitmq入口) @rabbitListener(用在类、方法上):开启监听rabbitmq事件(在开启前必须先开启@enableRabbitmq) @rabbitListener和@RabbitHandler(方法上)可以配合使用 ### 消息确认机制-可靠抵达 - 保证消息不丢失,可以使用事务消息(性能下降250倍),项目中使用消息确认机制 - publisher-confirmCallback 确认模式 - publisher - returnCallback 未投递到queue退回模式 - consumer ack机制 ![1598838335338](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1598838335338.png) ![1598839516284](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1598839516284.png) 定制RabbitTermplate - 服务端收到消息回调 - spring:rabbitmq:publisher-confirms: true(Deprecated configuration property : 不赞成配置的属性) - 设置confirmReturnCallback - 消息正确抵达队列进行回调 - spring:rabbitmq:publisher-returns: true - spring:rabbitmq:template:mandatory: true ![1598844647886](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1598844647886.png) ### 本地事务: - 由于本类方法在互相调用时,使用的代理对象进行调用,默认的事务会失效(不使用aop),必须使用spring的aop-aspect的动态代理 - 开启@EnableAspectJAutoProxy(exposeProxy = true)注解 - ``` 开启spring的切面切入动态代理(cglib动态代理) exposeProxy=true表示开启代理,可以使用AopContext直接调用当前类方法,避免本类事务互相调用失效 ``` ### CAP定理 ![1599300829540](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599300829540.png) ![](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599302557086.png) ### Date格式序列化错误(https://blog.csdn.net/a718515028/article/details/85245957) - 错误日志 ``` com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2020-07-20 09:58:47": not a valid representation (error: Failed to parse Date value '2020-07-20 09:58:47': Cannot parse date "2020-07-20 09:58:47": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)) at [Source: (PushbackInputStream); line: 1, column: 120] (through reference chain: com.qx.gulimall.order.entity.to.SpuInfoTo["createTime"]) at ``` - 原因:序列化使用的是Jackson,Jackson默认序列化是 ``` ("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "EEE, dd MMM yyyy HH:mm:ss zzz", "yyyy-MM-dd")) ``` 使用FastJson进行序列化形式 ### 消息队列流程 ![1599384348857](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599384348857.png) ### seata - client启动报错: - 错误日志 ``` [imeoutChecker_2] i.s.c.r.netty.NettyClientChannelManager : no available service 'null' found, please make sure registry config correct ``` - 原因: 在 `org.springframework.cloud:spring-cloud-starter-alibaba-seata`的`org.springframework.cloud.alibaba.seata.GlobalTransactionAutoConfiguration`类中,默认会使用 `${spring.application.name}-fescar-service-group`作为服务名注册到Seata Server上,如果和`file.conf `中的配置不一致,会提示`no available server to connect`错误。 也可以通过配置` spring.cloud.alibaba.seata.tx-service-group `修改后缀,但是必须和file.conf中的配置保持一致 - 错误日志 ``` Caused by: io.seata.common.exception.ShouldNeverHappenException: Auto proxy of DataSource can't be enabled as you've created a DataSourceProxy bean.Please consider removing DataSourceProxy bean or disabling auto proxy of DataSource. ``` - 原因 ``` 这个问题我使用了 druid连接池就好了 我原来使用的是hikari 也是出现这个错误 然后将seata版本尝试调整到1.1.0 希望对你有帮助 ``` - 解决:导入seata-all依赖 ``` com.alibaba.cloud spring-cloud-alibaba-seata 2.2.0.RELEASE io.seata seata-all io.seata seata-all 1.1.0 ``` - 错误日志 ``` Caused by: java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.String at io.seata.config.FileConfiguration$$EnhancerByCGLIB$$862af1eb.getConfig() ~[seata-all-1.2.0.jar:1.2.0] at io.seata.config.FileConfiguration.addConfigListener(FileConfiguration.java:185) ~[seata-all-1.2.0.jar:1.2.0] ... 42 common frames omitted ``` - 解决:注释掉以下内容 ``` io.seata seata-all 1.2.0 ``` ### 所有错误解决: 1. 不用写任何配置,spring默认已经配置好了,配置了就报错(DataSourceProxy、GlobalTransactionScanner、xid跨服务传递) 2. 导入依赖时,只导入spring的任意一个依赖即可(spring-cloud-alibaba-seata),多导少导都报错 ``` 添加seata依赖(建议单选) 依赖seata-all 依赖seata-spring-boot-starter,支持yml、properties配置(.conf可删除),内部已依赖seata-all 依赖spring-cloud-alibaba-seata,内部集成了seata,并实现了xid传递 ``` ``` 本项目导入依赖 com.alibaba.cloud spring-cloud-alibaba-seata 2.2.0.RELEASE ``` 本系统使用分布式事务方案 ![1599448638029](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599448638029.png) ### 死信路由 ![1599459305809](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599459305809.png) ![1599460313004](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599460313004.png) ![1599460413114](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599460413114.png) ![1599460535234](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599460535234.png) ![1599460716927](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599460716927.png) ![1599460784000](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599460784000.png) ![1599466995523](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599466995523.png) 库存解锁与锁定: ``` 每锁定一条库存后,给数据库中WareOrderTaskDetail保存数据,当锁定失败后全部回滚,锁定成功就讲其放入延时队列,延迟时间到,检查订单状态,确认是否需要解锁 ``` MQ异常 - 日志 ``` Caused by: org.springframework.amqp.AmqpException: No method found for class [B at org.springframework.amqp.rabbit.listener.adapter.DelegatingInvocableHandler.getHandlerForPayload(DelegatingInvocableHandler.java:151) ~[spring-rabbit-2.2.9.RELEASE.jar:2.2.9.RELEASE] at org.springframework.amqp.rabbit.listener.adapter.DelegatingInvocableHandler.getMethodFor(DelegatingInvocableHandler.java:270) ~[spring-rabbit-2.2.9.RELEASE.jar:2.2.9.RELEASE] ``` - 原因 - 在订单微服务中配置了MQ的MessageConverter消息转换器为Jackson2JsonMessageConverter,而在仓库微服务中未配置,导致实体类没有转换过来。 - RabbitMQ转换消息时,使用了两条通道,如果将监听`@RabbitListener(queues = "order.release.order.queue")`标注在类上会先转化为对象的字节数组,再调用payload.getClass()方法得到`class[B`错误对象 - 大神解答https://jira.spring.io/browse/AMQP-573 ``` There are two conversions in the @RabbitListener pipeline. The first converts from a Spring AMQP Message to a spring-messaging Message. There is currently no way to change the first converter from SimpleMessageConverter which handles String, Serializable and passes everything else as byte[]. The second converter converts the message payload to the method parameter type (if necessary). With method-level @RabbitListeners there is a tight binding between the handler and the method. With class-level @RabbitListener s, the message payload from the first conversion is used to select which method to invoke. Only then, is the argument conversion attempted. This mechanism works fine with Java Serializable objects since the payload has already been converted before the method is selected. However, with JSON, the first conversion returns a byte[] and hence we find no matching @RabbitHandler. We need a mechanism such that the first converter is settable so that the payload is converted early enough in the pipeline to select the appropriate handler method. A ContentTypeDelegatingMessageConverter is probably most appropriate. And, as stated in AMQP-574, we need to clearly document the conversion needs for a @RabbitListener, especially when using JSON or a custom conversion. ``` - 博客:https://www.jianshu.com/p/08dacf5176d4 https://www.jianshu.com/p/f94b2e8be0a9 ![1599812535761](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599812535761.png) ### 当Controller方法值返回void时Spring拦截器执行链重复调用多次 - 解决:https://www.jianshu.com/p/fcbaedb3ec50 ### springmvc接收数据时,不能将Date数据转换过来 - yml配置文件中配置 ``` spring.mvc.date-format=yyyy-MM-dd HH:mm:ss ``` 秒杀 ![1599881352609](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1599881352609.png) 定时任务 - @EnableScheduling 开启定时任务 - @Scheduled 开启一个定时任务 异步任务 - @EnableAsync 开启异步任务功能 - @Async给希望异步执行的方法标注 使用异步 + 定时任务完成任务不阻塞 - Spring中6位组成,不允许第七位的年 - 在周几的位置,1-7代表周一到周日 - 定时任务不应该阻塞。默认是阻塞的 - 可以让业务运行以异步的方式,自己提交到线程池 ``` CompletableFuture.runAsync(() -> { // 业务逻辑 },executor); ``` - 支持定时任务线程池;设置TaskSchedulingProperties; ![1600054686948](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1600054686948.png) ![1600054862433](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1600054862433.png) 秒杀流程 ![1600080669894](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1600080669894.png) ![1600083083414](C:\Users\qingxing\AppData\Roaming\Typora\typora-user-images\1600083083414.png)