对于谷粒商城的基础篇笔记总结…
整体介绍
安装Linux虚拟机 1)安装vagrant
下载&安装 VirtualBox https://www.virtualbox.org/,要开启 CPU 虚拟化
下载&安装 Vagrant
2)安装Centos7
打开 window cmd 窗口,运行
执行完上面的命令后,会在用户的家目录下生成Vagrantfile文件,即可初始化一个 centos7 系统
运行 vagrant up
即可启动虚拟机。系统 root 用户的密码是 vagrant
下载镜像过程比较漫长,也可以采用先用下载工具下载到本地后,然后使用“ vagrant box add
”添加,再“vagrant up
”即可
1 2 3 4 5 # 将下载的镜像添加到virtualBox中 $ vagrant box add centos/7 E:\迅雷下载\CentOS-7-x86_64-Vagrant-1905_01.VirtualBox.box # 启动 $ vagrant up
vagrant ssh
开启SSH,并登陆到centos7
1 2 $ vagrant ssh [vagrant@localhost ~]$
默认虚拟机的 ip 地址不是固定 ip,开发不方便 修改 Vagrantfile
config.vm.network “private_network”, ip: “192.168.56.10”
这里的 ip 需要在物理机下使用 ipconfig 命令找到
改为这个指定的子网地址,重新使用 vagrant up
启动机器即可。然后再 vagrant ssh
连接机器
3)密码登录 默认只允许ssh登陆方式,为了后面操作方便,文件上传等,我们可以配置允许账户密码登录。
Vagrant ssh进去系统后
1 2 3 4 5 vi /etc/ssh/sshd_config 修改 PasswordAuthentication yes 重启服务 service sshd restart
然后就可以使用XShell进行连接了
4)修改 linux 的 yum 源 1)备份原 yum 源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
2)使用新 yum 源 curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.163.com/.help/CentOS7-Base-163.repo 3)生成缓存
yum makecache
安装docker Docker 安装文档:https://docs.docker.com/install/linux/docker-ce/centos/
卸载系统之前的 docker
1 2 3 4 5 6 7 8 sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine
安装必须的依赖
1 2 3 4 sudo yum install -y yum-utils sudo yum-config-manager \ --add-repo \ https:// download.docker.com/linux/ centos/docker-ce.repo
安装 docker,以及 docker-cli
1 sudo yum install docker-ce docker-ce-cli containerd.io
启动Docker.
1 sudo systemctl start docker
设置docker开机自启
1 sudo systemctl enable docker
测试 docker 常用命令,注意切换到 root 用户下
https://docs.docker.com/engine/reference/commandline/docker/
配置镜像加速
1 2 3 4 5 6 7 8 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://chqac97z.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker
1) docker安装MySQL
下载镜像文件
查看镜像
1 2 3 [vagrant@localhost home]$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE mysql 5.7 c20987f18b13 2 hours ago 448MB
创建实例并启动mysql容器
1 2 3 4 5 6 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
参数说明
-p 3306:3306:将容器的 3306 端口映射到主机的 3306 端口
-v /mydata/mysql/conf:/etc/mysql:将配置文件夹挂载到主机
-v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机
-v /mydata/mysql/data:/var/lib/mysql/:将配置文件夹挂载到主机
-e MYSQL_ROOT_PASSWORD=root:初始化 root 用户的密码
修改配置
1 2 3 4 5 6 7 8 9 10 11 12 13 [vagrant@localhost home]$ vi /mydata/mysql/conf/my.cnf [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
注意:解决 MySQL 连接慢的问题 在配置文件中加入如下,并重启 mysql
1 2 [mysqld] skip-name-resolve
解释:
skip-name-resolve:跳过域名解析
进入容器文件系统:
1 [vagrant@localhost home]$ docker exec -it mysql /bin/bash
设置root 远程访问
1 grant all privileges on *.* to 'root'@'%' identified by 'root' with grant option; flush privileges;
设置启动docker时,即运行mysql
1 2 [vagrant@localhost home]$ docker update mysql --restart=always mysql
2) docker中安装redis
下载镜像文件
1 [root@localhost home]# docker pull redis
创建实例并启动
1 2 3 [root@localhost home]# mkdir -p /mydata/redis/conf [root@localhost home]# touch /mydata/redis/conf/redis.conf [root@localhost home]# echo "appendonly yes" >> /mydata/redis/conf/redis.conf
1 2 3 [root@localhost home]# 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
redis 自描述文件:https://raw.githubusercontent.com/antirez/redis/4.0/redis.conf
使用redis 镜像执行redis-cli 命令连接
1 docker exec -it redis redis-cli
设置redis容器在docker启动的时候启动
1 2 [root@localhost home]# docker update redis --restart=always redis
开发环境统一 Maven 1 2 3 4 5 6 7 8 9 配置阿里云镜像 <mirrors > <mirror > <id > nexus-aliyun</id > <mirrorOf > central</mirrorOf > <name > Nexus aliyun</name > <url > http://maven.aliyun.com/nexus/content/groups/public</url > </mirror > </mirrors >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 配置 jdk 1.8 编译项目 <profiles > <profile > <id > jdk-1.8</id > <activation > <activeByDefault > true</activeByDefault > <jdk > 1.8</jdk > </activation > <properties > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > <maven.compiler.compilerVersion > 1.8</maven.compiler.compilerVersion > </properties > </profile > </profiles >
Idea&VsCode idea 安装 lombok、mybatisx 插件
Vscode 安装开发必备插件
Vetur —— 语法高亮、智能感知、Emmet 等 包含格式化功能, Alt+Shift+F (格式化全文),Ctrl+K Ctrl+F(格式化选中代码,两个 Ctrl需要同时按着) EsLint —— 语法纠错 Auto Close Tag —— 自动闭合 HTML/XML 标签 Auto Rename Tag —— 自动完成另一侧标签的同步修改 JavaScript(ES6) code snippets —— ES6 语法智能提示以及快速输入,除 js 外还支持.ts,.jsx,.tsx,.html,.vue,省去了配置其支持各种包含 js 代码文件的时间 HTML CSS Support —— 让 html 标签上写 class 智能提示当前项目所支持的样式 HTML Snippets —— html 快速自动补全 Open in browser —— 浏览器快速打开 Live Server —— 以内嵌服务器方式打开 Chinese (Simplified) Language Pack for Visual Studio Code —— 中文语言包
执行sql脚本 gulimall_oms.sql gulimall_pms.sql gulimall_sms.sql gulimall_ums.sql gulimall_wms.sql pms_catelog.sql sys_menus.sql
clone 人人开源 https://gitee.com/renrenio
克隆到本地:
1 2 git clone https://gitee.com/renrenio/renren-fast-vue.git git clone https://gitee.com/renrenio/renren-fast.git
将拷贝下来的“renren-fast”删除“.git”后,拷贝到“gulimall”工程根目录下,然后将它作为gulimall的一个module
创建“gulimall_admin
”的数据库,然后执行“renren-fast/db/mysql.sql
”中的SQl脚本
修改“application-dev.yml
”文件,默认为dev环境,修改连接mysql的url和用户名密码
1 2 3 4 5 6 7 8 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.10:3306/gulimall_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root
启动“gulimall_admin”,然后访问“http://localhost:8080/renren-fast/ ”
安装node.js,并且安装仓库
1 npm config set registry http ://registry .npm.taobao.org/
1 2 PS D:\tmp\renren-fast-vue> npm config set registry http://registry.npm.taobao.org/ PS D:\tmp\renren-fast-vue> npm install
1 PS D:\tmp\renren-fast-vue> npm run dev
常见问题1:“Module build failed: Error: Cannot find module ‘node-sass”
运行过程中,出现“Module build failed: Error: Cannot find module ‘node-sass’报错问题”,解决方法
用npm install -g cnpm –registry=https://registry.npm.taobao.org ,从淘宝镜像那下载,然后cnpm下载成功。
最后输入cnpm install node-sass –save。npm run dev终于能跑起来了!!! 版权声明:本文为CSDN博主「夕阳下美了剪影」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_38401285/article/details/86483278
常见问题2:cnpm - 解决 “ cnpm : 无法加载文件 C:\Users\93457\AppData\Roaming\npm\cnpm.ps1,因为在此系统上禁止运行脚本。有关详细信息 。。。 “
https://www.cnblogs.com/500m/p/11634969.html
所有问题的根源都在“node_modules”,npm install之前,应该将这个文件夹删除,然后再进行安装和运行。
再次运行npm run dev恢复正常:
clone renren-generator clone https://gitee.com/renrenio/renren-generator.git
然后将该项目放置到“gulimall”的跟路径下,然后添加该Module,并且提交到github上
修改配置 renren-generator/src/main/resources/generator.properties
1 2 3 4 5 6 7 8 9 10 11 12 mainPath =com.atguigu package =com.atguigu.gulimall moduleName =product author =zsxfa email =zsxfa@gmail.com tablePrefix =pms_
运行“renren-generator” 访问:<http://localhost:80/
点击“renren-fast”,能够看到它将“renren-fast”的所有表都列举了出来:
选择所有的表,然后点击“生成代码”,将下载的“renren.zip”,解压后取出main文件夹,放置到“gulimall-product”项目的main目录中。
下面的几个module,也采用同样的方式来操作。
整合mybatis-plus 1)、导入依赖
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.2.0</version > </dependency >
2)、配置
1、配置数据源;
1)、导入数据库的驱动。https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-versions.html
2)、在application.yml配置数据源相关信息
1 2 3 4 5 6 spring: datasource: username: root password: root url: jdbc:mysql://#:3306/gulimall_pms driver-class-name: com.mysql.cj.jdbc.Driver
2、配置MyBatis-Plus;
1)、使用@MapperScan
2)、告诉MyBatis-Plus,sql映射文件位置
1 2 3 4 5 6 mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml global-config: db-config: id-type: auto
微服务注册中心 要注意nacos集群所在的server,一定要关闭防火墙,否则容易出现各种问题。
搭建nacos集群,然后分别启动各个微服务,将它们注册到Nacos中。
1 2 3 4 5 6 application: name: gulimall-coupon cloud: nacos: discovery: server-addr: 192.168 .137 .14
查看注册情况:
http://127.0.0.1:8848/nacos/index.html#/serviceManagement
使用openfen 1)、引入open-feign
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
2)、编写一个接口,告诉SpringCLoud这个接口需要调用远程服务
修改“com.atguigu.gulimall.coupon.controller.CategoryBoundsController”,添加以下controller方法:
1 2 3 4 5 6 @RequestMapping("/member/list") public R memberCoupons () { CouponEntity couponEntity = new CouponEntity (); couponEntity.setCouponName("discount 20%" ); return R.ok().put("coupons" ,Arrays.asList(couponEntity)); }
新建“com.atguigu.gulimall.member.feign.CouponFeignService”接口
1 2 3 4 5 @FeignClient("gulimall_coupon") public interface CouponFeignService { @RequestMapping("/coupon/coupon/member/list") public R memberCoupons () ; }
修改“com.atguigu.gulimall.member.GulimallMemberApplication”类,添加上”@EnableFeignClients”:
1 2 3 4 5 6 7 8 9 @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign") public class GulimallMemberApplication { public static void main (String[] args) { SpringApplication.run(GulimallMemberApplication.class, args); } }
声明接口的每一个方法都是调用哪个远程服务的那个请求
3)、开启远程调用功能
io.niceseason.gulimall.member.controller.MemberController
1 2 3 4 5 6 7 8 @RequestMapping("/coupons") public R test () { MemberEntity memberEntity=new MemberEntity (); memberEntity.setNickname("zhangsan" ); R memberCoupons = couponFeignService.memberCoupons(); return memberCoupons.put("member" ,memberEntity).put("coupons" ,memberCoupons.get("coupons" )); }
(4)、访问http://localhost:8000/member/member/coupons
停止“gulimall-coupon”服务,能够看到注册中心显示该服务的健康值为0:
再次访问:http://localhost:8000/member/member/coupons
启动“gulimall-coupon”服务,再次访问,又恢复了正常。
配置中心 1)修改“gulimall-coupon”模块 添加pom依赖:
1 2 3 4 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency >
创建bootstrap.properties文件,该配置文件会优先于“application.yml”加载。
1 2 spring.application.name =gulimall-coupon spring.cloud.nacos.config.server-addr =127.0.0.1:8848
2)传统方式 为了详细说明config的使用方法,先来看原始的方式
创建“application.properties”配置文件,添加如下配置内容:
1 2 coupon.user.name ="zhangsan" coupon.user.age =30
修改“com\atguigu\gulimall\coupon\controller\CategoryBoundsController.java”文件,添加如下内容:
1 2 3 4 5 6 7 8 9 @Value("${coupon.user.name}") private String name;@Value("${coupon.user.age}") private Integer age;@RequestMapping("/test") public R getConfigInfo () { return R.ok().put("name" ,name).put("age" ,age); }
启动“gulimall-coupon”服务:
访问:http://localhost:7000/coupon/categorybounds/test
这样做存在的一个问题,如果频繁的修改application.properties,在需要频繁重新打包部署。下面我们将采用Nacos的配置中心来解决这个问题。
3)nacos config 1、在Nacos注册中心中,点击“配置列表”,添加配置规则:
DataID:gulimall-coupon
配置格式:properties
文件的命名规则为:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
${spring.application.name}:为微服务名
${spring.profiles.active}:指明是哪种环境下的配置,如dev、test或info
${spring.cloud.nacos.config.file-extension}:配置文件的扩展名,可以为properties、yml等
2、查看配置:
3、修改“com\atguigu\gulimall\coupon\controller\CategoryBoundsController”类,添加“@RefreshScope”注解
1 2 3 4 @RestController @RequestMapping("coupon/categorybounds") @RefreshScope public class CategoryBoundsController {
这样都会动态的从配置中心读取配置.
4、访问:http://localhost:7000/coupon/categorybounds/test
能够看到读取到了nacos 中的最新的配置信息,并且在指明了相同的配置信息时,配置中心中设置的值优先于本地配置。
4)Nacos支持三种配置加载方方案 Nacos支持“Namespace+group+data ID”的配置解决方案。
详情见:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-docs/src/main/asciidoc-zh/nacos-config.adoc
Namespace方案 通过命名空间实现环境区分
下面是配置实例:
1、创建命名空间:
“命名空间”—>“创建命名空间”:
创建三个命名空间,分别为dev,test和prop
2、回到配置列表中,能够看到所创建的三个命名空间
下面我们需要在dev命名空间下,创建“gulimall-coupon.properties”配置规则:
3、访问:http://localhost:7000/coupon/categorybounds/test
并没有使用我们在dev命名空间下所配置的规则,而是使用的是public命名空间下所配置的规则,这是怎么回事呢?
查看“gulimall-coupon”服务的启动日志:
1 2 3 4 2020 -04 -24 16 :37 :24 .158 WARN 32792 --- [ main] c.a .c .n .c .NacosPropertySourceBuilder : Ignore the empty nacos configuration and get it based on dataId[gulimall-coupon] & group[DEFAULT_GROUP]2020 -04 -24 16 :37 :24 .163 INFO 32792 --- [ main] c.a .nacos .client .config .utils .JVMUtil : isMultiInstance:false2020 -04 -24 16 :37 :24 .169 INFO 32792 --- [ main] b.c .PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-gulimall-coupon.properties ,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-gulimall-coupon,DEFAULT_GROUP'}]
**”gulimall-coupon.properties”**,默认就是public命名空间中的内容中所配置的规则。
4、指定命名空间
如果想要使得我们自定义的命名空间生效,需要在“bootstrap.properties”文件中,指定使用哪个命名空间:
1 spring.cloud.nacos.config.namespace =0114fcbe-b6f5-471a-85db-40b6f34e7597
这个命名空间ID来源于我们在第一步所创建的命名空间
5、重启“gulimall-coupon”,再次访问:http://localhost:7000/coupon/categorybounds/test
但是这种命名空间的粒度还是不够细化,对此我们可以为项目的每个微服务module创建一个命名空间。
6、为所有微服务创建命名空间
7、回到配置列表选项卡,克隆pulic的配置规则到coupon命名空间下
切换到coupon命名空间下,查看所克隆的规则
8、修改“gulimall-coupon”下的bootstrap.properties文件,添加如下配置信息
1 spring.cloud.nacos.config.namespace =0114fcbe-b6f5-471a-85db-40b6f34e7597
这里指明的是,读取时使用coupon命名空间下的配置。
9、重启“gulimall-coupon”,访问:http://localhost:7000/coupon/categorybounds/test
DataID方案 通过指定spring.profile.active和配置文件的DataID,来使不同环境下读取不同的配置,读取配置时,使用的是默认命名空间public,默认分组(default_group)下的DataID。
默认情况,Namespace=public,Group=DEFAULT GROUP,默认Cluster是DEFAULT
通过制定spring.profiles.active=dev
可以制定xxx-dev.properties
的配置文件
Group方案 通过Group实现环境区分
实例:通过使用不同的组,来读取不同的配置,还是以上面的gulimall-coupon微服务为例
1、新建“gulimall-coupon.properties”,将它置于“dev”组下
2、修改“bootstrap.properties”配置,添加如下的配置
1 spring.cloud.nacos.config.group =dev
3、重启“gulimall-coupon”,访问:http://localhost:7000/coupon/categorybounds/test
5)同时加载多个配置集 当微服务数量很庞大时,将所有配置都书写到一个配置文件中,显然不是太合适。对此我们可以将配置按照功能的不同,拆分为不同的配置文件。
如下面的配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 server: port: 7000 spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false username: root password: root application: name: gulimall-coupon cloud: nacos: discovery: server-addr: 127.0 .0 .1 :8848 mybatis-plus: global-config: db-config: id-type: auto mapper-locations: classpath:/mapper/**/*.xml
我们可以将,
数据源有关的配置写到一个配置文件中:
1 2 3 4 5 6 7 spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false username: root password: root
和框架有关的写到另外一个配置文件中:
1 2 3 4 5 mybatis-plus: global-config: db-config: id-type: auto mapper-locations: classpath:/mapper/**/*.xml
也可以将上面的这些配置交给nacos来进行管理。
实例:将“gulimall-coupon”的“application.yml”文件拆分为多个配置,并放置到nacos配置中心
1、创建“datasource.yml”,用于存储和数据源有关的配置
1 2 3 4 5 6 7 spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false username: root password: root
在coupon命名空间中,创建“datasource.yml”配置
2、将和mybatis相关的配置,放置到“mybatis.yml”中
1 2 3 4 5 mybatis-plus: global-config: db-config: id-type: auto mapper-locations: classpath:/mapper/**/*.xml
3、创建“other.yml”配置,保存其他的配置信息
1 2 3 4 5 6 7 8 9 10 server: port: 7000 spring: application: name: gulimall-coupon cloud: nacos: discovery: server-addr: 127.0 .0 .1 :8848
现在“mybatis.yml”、“datasource.yml”和“other.yml”共同构成了微服务的配置。
4、修改“gulimall-coupon”的“bootstrap.properties”文件,加载“mybatis.yml”、“datasource.yml”和“other.yml”配置
1 2 3 4 5 6 7 8 9 10 11 12 spring.cloud.nacos.config.extension-configs[0].data-id =mybatis.yml spring.cloud.nacos.config.extension-configs[0].group =dev spring.cloud.nacos.config.extension-configs[0].refresh =true spring.cloud.nacos.config.extension-configs[1].data-id =datasource.yml spring.cloud.nacos.config.extension-configs[1].group =dev spring.cloud.nacos.config.extension-configs[1].refresh =true spring.cloud.nacos.config.extension-configs[2].data-id =other.yml spring.cloud.nacos.config.extension-configs[2].group =dev spring.cloud.nacos.config.extension-configs[2].refresh =true
“spring.cloud.nacos.config.ext-config”已经被废弃,建议使用“spring.cloud.nacos.config.extension-configs”,根据自己的版本选择配置。
5、注释“application.yml”文件中的所有配置
6、重启“gulimall-coupon”服务,然后访问:http://localhost:7000/coupon/categorybounds/test
7、访问:http://localhost:7000/coupon/categorybounds/list ,查看是否能够正常的访问数据库
小结:
1)、微服务任何配置信息,任何配置文件都可以放在配置中心;
2)、只需要在bootstrap.properties中,说明加载配置中心的哪些配置文件即可;
3)、@Value, @ConfigurationProperties。都可以用来获取配置中心中所配置的信息;
4)、配置中心有的优先使用配置中心中的,没有则使用本地的配置。
网关 1、注册“gulimall-gateway”到Nacos 1)创建“gulimall-gateway” SpringCloud gateway
2)添加“gulimall-common”依赖和“spring-cloud-starter-gateway”依赖 1 2 3 4 5 6 7 8 9 <dependency > <groupId > io.niceseason.gulimall</groupId > <artifactId > gulimall-common</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency >
4)在Nacos中创建“gateway”命名空间,同时在该命名空间中创建“gulimall-gateway.yml”
5)创建“bootstrap.properties”文件,添加如下配置,指明配置中心地址和所属命名空间 1 2 3 spring.application.name =gulimall-gateway spring.cloud.nacos.config.server-addr =127.0.0.1:8848 spring.cloud.nacos.config.namespace =91170566-3108-4593-b497-3ffea6f4555c
6)创建“application.properties”文件,指定服务名和注册中心地址 1 2 3 spring.application.name =gulimall-gateway spring.cloud.nacos.discovery.server-addr =127.0.0.1:8848 server.port =88
7)启动“gulimall-gateway” 启动报错:
1 2 3 4 5 Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class
解决方法:在“com.atguigu.gulimall.gateway.GulimallGatewayApplication”中排除和数据源相关的配置
1 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
重新启动
访问:http://localhost:8848/nacos/# ,查看到该服务已经注册到了Nacos中
2、案例 现在想要实现针对于“http://localhost:88/hello?url=baidu”,转发到“https://www.baidu.com”,针对于“http://localhost:88/hello?url=qq”的请求,转发到“https://www.qq.com/”
1)创建“application.yml” 1 2 3 4 5 6 7 8 9 10 11 12 spring: cloud: gateway: routes: - id: baidu_route uri: https://www.baidu.com predicates: - Query=url, baidu - id: qq_route uri: https://www.qq.com/ predicates: - Query=url, qq
2)启动“gulimall-gateway” 3)测试 访问:http://localhost:88/hello?url=baidu
访问:http://localhost:88/hello?url=qq
Gateway官方文档
Vue 安装vue
1 2 # 最新稳定版 $ npm install vue
1、vue声明式渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let vm = new Vue ({ el : "#app" , data : { name : "张三" , num : 1 }, methods :{ cancle ( ){ this .num -- ; }, hello ( ){ return "1" } } });
2、双向绑定,模型变化,视图变化。反之亦然 双向绑定使用v-model
1 <input type ="text" v-model ="num" >
1 <h1> {{name}} ,非常帅,有{{num}}个人为他点赞{{hello ()}}</h1>
3、事件处理 v-xx:指令 1、创建vue实例,关联页面的模板,将自己的数据(data)渲染到关联的模板,响应式的 2、指令来简化对dom的一些操作。 3、声明方法来做更复杂的操作。methods里面可以封装方法。
v-on是按钮的单击事件:
1 <button v-on:click="num++">点赞</button>
在VUE中el,data和vue的作用:
el:用来绑定数据;
data:用来封装数据;
methods:用来封装方法,并且能够封装多个方法,如何上面封装了cancell和hello方法。
安装“Vue 3 Snippets”,用来做代码提示
为了方便的在浏览器上调试VUE程序,需要安装“vue-devtools ”,编译后安装到chrome中即可。
详细的使用方法见:Vue调试神器vue-devtools安装
“v-html”不会对于HTML标签进行转义,而是直接在浏览器上显示data所设置的内容;而“ v-text”会对html标签进行转义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <div id="app" > {{msg}} {{1 +1 }} {{hello ()}}<br/> <span v-html ="msg" > </span > <br /> <span v-text ="msg" > </span > </div> <script src ="../node_modules/vue/dist/vue.js" > </script > <script > new Vue ({ el :"#app" , data :{ msg :"<h1>Hello</h1>" , link :"http://www.baidu.com" }, methods :{ hello ( ){ return "World" } } }) </script >
:称为差值表达式,它必须要写在Html表达式,可以完成数学运算和方法调用
4、v-bind :单向绑定 给html标签的属性绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!-- 给html标签的属性绑定 --> <div id ="app" > <a v-bind:href ="link" > gogogo</a > <span v-bind:class ="{active:isActive,'text-danger':hasError}" :style ="{color: color1,fontSize: size}" > 你好</span > </div > <script src ="../node_modules/vue/dist/vue.js" > </script > <script > let vm = new Vue ({ el :"#app" , data :{ link : "http://www.baidu.com" , isActive :true , hasError :true , color1 :'red' , size :'36px' } }) </script >
上面所完成的任务就是给a标签绑定一个超链接。并且当“isActive”和“hasError”都是true的时候,将属性动态的绑定到,则绑定该“active”和 “text-danger”class。这样可以动态的调整属性的存在。
而且如果想要实现修改vm的”color1”和“size”, span元素的style也能够随之变化,则可以写作v-bind:style,也可以省略v-bind。
5、v-model双向绑定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <div id ="app" > 精通的语言: <input type ="checkbox" v-model ="language" value ="Java" > java<br /> <input type ="checkbox" v-model ="language" value ="PHP" > PHP<br /> <input type ="checkbox" v-model ="language" value ="Python" > Python<br /> 选中了 {{language.join(",")}} </div > <script src ="../node_modules/vue/dist/vue.js" > </script > <script > let vm = new Vue ({ el :"#app" , data :{ language : [] } }) </script > </body > </html >
上面完成的功能就是通过“v-model”为输入框绑定多个值,能够实现选中的值,在data的language也在不断的发生着变化,
如果在控制台上指定vm.language=[“Java”,”PHP”],则data值也会跟着变化。
通过“v-model”实现了页面发生了变化,则数据也发生变化,数据发生变化,则页面也发生变化,这样就实现了双向绑定。
6、v-on为按钮绑定事件 1 2 3 4 <!--事件中直接写js片段--> <button v-on:click ="num++" > 点赞</button > <!--事件指定一个回调函数,必须是Vue 实例中定义的函数--> <button @click ="cancle" > 取消</button >
上面是为两个按钮绑定了单击事件,其中一个对于num进行自增,另外一个自减。
v-on:click也可以写作@click
事件的冒泡:
1 2 3 4 5 6 7 8 <div style ="border: 1px solid red;padding: 20px;" v-on:click ="hello" > 大div <div style ="border: 1px solid blue;padding: 20px;" @click ="hello" > 小div <br /> <a href ="http://www.baidu.com" @click.prevent ="hello" > 去百度</a > </div > </div >
上面的这两个嵌套div中,如果点击了内层的div,则外层的div也会被触发;这种问题可以事件修饰符来完成:
1 2 3 4 5 6 7 8 9 <div style ="border: 1px solid red;padding: 20px;" v-on:click.once ="hello" > 大div <div style ="border: 1px solid blue;padding: 20px;" @click.stop ="hello" > 小div <br /> <a href ="http://www.baidu.com" @click.prevent.stop ="hello" > 去百度</a > </div > </div >
关于事件修饰符:
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑, 而不是去处理 DOM 事件细节。 为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。修饰符是由点开头的指令后缀来表示的。
.stop
:阻止事件冒泡到父元素 .prevent
:阻止默认事件发生 .capture
:使用事件捕获模式 .self
:只有元素自身触发事件才执行。(冒泡或捕获的都不执行) .once
:只执行一次
按键修饰符:
全部的按键别名:
.enter
.tab
.delete
(捕获“删除”和“退格”键) .esc
.space
.up
.down
.left
.right
7、v-for遍历循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <div id ="app" > <ul > <li v-for ="(user,index) in users" :key ="user.name" v-if ="user.gender == '女'" > 当前索引:{{index}} ==> {{user.name}} ==> {{user.gender}} ==>{{user.age}} <br > 对象信息: <span v-for ="(v,k,i) in user" > {{k}}=={{v}}=={{i}};</span > </li > </ul > <ul > <li v-for ="(num,index) in nums" :key ="index" > </li > </ul > </div > <script src ="../node_modules/vue/dist/vue.js" > </script > <script > let app = new Vue ({ el : "#app" , data : { users : [{ name : '柳岩' , gender : '女' , age : 21 }, { name : '张三' , gender : '男' , age : 18 }, { name : '范冰冰' , gender : '女' , age : 24 }, { name : '刘亦菲' , gender : '女' , age : 18 }, { name : '古力娜扎' , gender : '女' , age : 25 }], nums : [1 ,2 ,3 ,4 ,4 ] }, }) </script > </body > </html >
4、遍历的时候都加上:key来区分不同数据,提高vue渲染效率
过滤器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <div id ="app" > <ul > <li v-for ="user in userList" > {{user.id}} ==> {{user.name}} ==> {{user.gender == 1?"男":"女"}} ==> {{user.gender | genderFilter}} ==> {{user.gender | gFilter}} </li > </ul > </div > <script src ="../node_modules/vue/dist/vue.js" > </script > <script > Vue .filter ("gFilter" , function (val ) { if (val == 1 ) { return "男~~~" ; } else { return "女~~~" ; } }) let vm = new Vue ({ el : "#app" , data : { userList : [ { id : 1 , name : 'jacky' , gender : 1 }, { id : 2 , name : 'peter' , gender : 0 } ] }, filters : { genderFilter (val ) { if (val == 1 ) { return "男" ; } else { return "女" ; } } } }) </script > </body > </html >
组件化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <div id ="app" > <button v-on:click ="count++" > 我被点击了 {{count}} 次</button > <counter > </counter > <counter > </counter > <counter > </counter > <counter > </counter > <counter > </counter > <button-counter > </button-counter > </div > <script src ="../node_modules/vue/dist/vue.js" > </script > <script > Vue .component ("counter" , { template : `<button v-on:click="count++">我被点击了 {{count}} 次</button>` , data ( ) { return { count : 1 } } }); const buttonCounter = { template : `<button v-on:click="count++">我被点击了 {{count}} 次~~~</button>` , data ( ) { return { count : 1 } } }; new Vue ({ el : "#app" , data : { count : 1 }, components : { 'button-counter' : buttonCounter } }) </script > </body > </html >
组件其实也是一个 Vue 实例,因此它在定义时也会接收:data、methods、生命周期函数等
不同的是组件不会与页面的元素绑定,否则就无法复用了,因此没有 el 属性。
但是组件渲染需要 html 模板,所以增加了 template 属性,值就是 HTML 模板
全局组件定义完毕,任何 vue 实例都可以直接在 HTML 中通过组件名称来使用组件了
data 必须是一个函数,不再是一个对象。
生命周期钩子函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <div id ="app" > <span id ="num" > {{num}}</span > <button @click ="num++" > 赞!</button > <h2 > {{name}},有{{num}}个人点赞</h2 > </div > <script src ="../node_modules/vue/dist/vue.js" > </script > <script > let app = new Vue ({ el : "#app" , data : { name : "张三" , num : 100 }, methods : { show ( ) { return this .name ; }, add ( ) { this .num ++; } }, beforeCreate ( ) { console .log ("=========beforeCreate=============" ); console .log ("数据模型未加载:" + this .name , this .num ); console .log ("方法未加载:" + this .show ()); console .log ("html模板未加载:" + document .getElementById ("num" )); }, created : function ( ) { console .log ("=========created=============" ); console .log ("数据模型已加载:" + this .name , this .num ); console .log ("方法已加载:" + this .show ()); console .log ("html模板已加载:" + document .getElementById ("num" )); console .log ("html模板未渲染:" + document .getElementById ("num" ).innerText ); }, beforeMount ( ) { console .log ("=========beforeMount=============" ); console .log ("html模板未渲染:" + document .getElementById ("num" ).innerText ); }, mounted ( ) { console .log ("=========mounted=============" ); console .log ("html模板已渲染:" + document .getElementById ("num" ).innerText ); }, beforeUpdate ( ) { console .log ("=========beforeUpdate=============" ); console .log ("数据模型已更新:" + this .num ); console .log ("html模板未更新:" + document .getElementById ("num" ).innerText ); }, updated ( ) { console .log ("=========updated=============" ); console .log ("数据模型已更新:" + this .num ); console .log ("html模板已更新:" + document .getElementById ("num" ).innerText ); } }); </script > </body > </html >
新建代码片段 文件 ➡ 首选项 ➡ 用户代码片段 ➡ 点击新建全局代码片段 ➡ 取名 vue ➡ 确定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 { "Print to console" : { "prefix" : "vue" , "body" : [ "<!-- $1 -->" , "<template>" , "<div class='$2'>$5</div>" , "</template>" , "" , "<script>" , "//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)" , "//例如:import 《组件名称》 from '《组件路径》';" , "" , "export default {" , "//import引入的组件需要注入到对象中才能使用" , "components: {}," , "data() {" , "//这里存放数据" , "return {" , "" , "};" , "}," , "//监听属性 类似于data概念" , "computed: {}," , "//监控data中的数据变化" , "watch: {}," , "//方法集合" , "methods: {" , "" , "}," , "//生命周期 - 创建完成(可以访问当前this实例)" , "created() {" , "" , "}," , "//生命周期 - 挂载完成(可以访问DOM元素)" , "mounted() {" , "" , "}," , "beforeCreate() {}, //生命周期 - 创建之前" , "beforeMount() {}, //生命周期 - 挂载之前" , "beforeUpdate() {}, //生命周期 - 更新之前" , "updated() {}, //生命周期 - 更新之后" , "beforeDestroy() {}, //生命周期 - 销毁之前" , "destroyed() {}, //生命周期 - 销毁完成" , "activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发" , "}" , "</script>" , "<style scoped>" , "//@import url($3); 引入公共css类" , "$4" , "</style>" ] , "description" : "生成vue模板" } , "http-get请求" : { "prefix" : "httpget" , "body" : [ "this.\\$http({" , "url: this.\\$http.adornUrl('')," , "method: 'get'," , "params: this.\\$http.adornParams({})" , "}).then(({ data }) => {" , "})" ] , "description" : "httpGET请求" } , "http-post请求" : { "prefix" : "httppost" , "body" : [ "this.\\$http({" , "url: this.\\$http.adornUrl('')," , "method: 'post'," , "data: this.\\$http.adornData(data, false)" , "}).then(({ data }) => { });" ] , "description" : "httpPOST请求" } }
新建一个 .vue 文件输入 vue 测试 上面的配置中:"prefix": "vue"
、"prefix": "httpget"
、"prefix": "httppost"
就是你的快捷输入名称,可自行修改
element ui 官网: https://element.eleme.cn/#/zh-CN/component/installation
安装
在 main.js 中写入以下内容:
1 2 3 4 import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI);
后端开发 商品系统分类维护 递归树形结构获取数据 在注册中心中“product”命名空间中,创建“gulimall-product.yml”配置文件:
将“application.yml”内容拷贝到该配置文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 server: port: 10000 spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false username: root password: root application: name: gulimall-product cloud: nacos: discovery: server-addr: 127.0 .0 .1 :8848 mybatis-plus: global-config: db-config: id-type: auto mapper-locations: classpath:/mapper/**/*.xml
在本地创建“bootstrap.properties”文件,指明配置中心的位置和使用到的配置文件:
1 2 3 4 5 6 spring.application.name =gulimall-product spring.cloud.nacos.config.server-addr =127.0.0.1:8848 spring.cloud.nacos.config.namespace =77833c89-7c41-4325-8440-58c0d74e32c0 spring.cloud.nacos.config.extension-configs[0].data-id =gulimall-product.yml spring.cloud.nacos.config.extension-configs[0].group =DEFAULT_GROUP spring.cloud.nacos.config.extension-configs[0].refresh =true
然后启动gulimall-product,查看到该服务已经出现在了nacos的注册中心中了
修改“com.atguigu.gulimall.product.controller.CategoryController”类,添加如下代码:
1 2 3 4 5 6 7 8 9 @RequestMapping("/list/tree") public R list (@RequestParam Map<String, Object> params) { List<CategoryEntity> entities = categoryService.listWithTree(); return R.ok().put("page" , entities); }
测试:http://localhost:10000/product/category/list/tree
如何区别是哪种分类级别?
答:可以通过分类的parent_cid来进行判断,如果是一级分类,其值为0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @RequestMapping("/list/tree") public R list (@RequestParam Map<String, Object> params) { List<CategoryEntity> entities = categoryService.listWithTree(); return R.ok().put("page" , entities); } @Override public List<CategoryEntity> listWithTree () { List<CategoryEntity> entities = baseMapper.selectList(null ); List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0 ).map((menu)->{ menu.setChildren(getChildrens(menu,entities)); return menu; }).sorted((menu1,menu2)->{ return menu1.getSort() - menu2.getSort(); }).collect(Collectors.toList()); return level1Menus; } private List<CategoryEntity> getChildrens (CategoryEntity root, List<CategoryEntity> all) { List<CategoryEntity> children = all.stream().filter(CategoryEntity -> { return CategoryEntity.getParentCid() == root.getCatId(); }).map(categoryEntity -> { 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 children; }
下面是得到的部分JSON数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 [ { "catId" : 1 , "name" : "图书、音像、电子书刊" , "parentCid" : 0 , "catLevel" : 1 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ { "catId" : 22 , "name" : "电子书刊" , "parentCid" : 1 , "catLevel" : 2 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ { "catId" : 165 , "name" : "电子书" , "parentCid" : 22 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } , { "catId" : 166 , "name" : "网络原创" , "parentCid" : 22 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } , { "catId" : 167 , "name" : "数字杂志" , "parentCid" : 22 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } , { "catId" : 168 , "name" : "多媒体图书" , "parentCid" : 22 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } ] } , { "catId" : 23 , "name" : "音像" , "parentCid" : 1 , "catLevel" : 2 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ { "catId" : 169 , "name" : "音乐" , "parentCid" : 23 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } , { "catId" : 170 , "name" : "影视" , "parentCid" : 23 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } , { "catId" : 171 , "name" : "教育音像" , "parentCid" : 23 , "catLevel" : 3 , "showStatus" : 1 , "sort" : 0 , "icon" : null , "productUnit" : null , "productCount" : 0 , "childCategoryEntity" : [ ] } ] } , {
启动后端项目renren-fast
启动前端项目renren-fast-vue:
访问: http://localhost:8001/#/login
创建一级菜单:
创建完成后,在后台的管理系统中会创建一条记录:
然后创建子菜单:
创建renren-fast-vue\src\views\modules\product目录,子所以是这样来创建,是因为product/category,对应于product-category
在该目录下,新建“category.vue”文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <template > <el-tree :data ="menus" :props ="defaultProps" @node-click ="handleNodeClick" > </el-tree > </template > <script > export default { components : {}, computed : {}, watch : {}, data ( ) { return { menus : [], defaultProps : { children : "childrens" , label : "name" }, } }, methods : { handleNodeClick (data ) { console .log (data); }, getMenus ( ) { this .dataListLoading = true ; this .$http({ url : this .$http .adornUrl ("/product/category/list/tree" ), method : "get" }).then (({ data } ) => { console .log ("获取到数据" , data); this .menus =data; }); } }, created ( ) { this .getMenus (); }, mounted ( ) {}, beforeCreate ( ) {}, beforeMount ( ) {}, beforeUpdate ( ) {}, updated ( ) {}, beforeDestroy ( ) {}, destroyed ( ) {}, activated ( ) {} }; </script > <style scoped >
刷新页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”
这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree,
修正这个问题:
替换“static\config\index.js”文件中的“window.SITE_CONFIG[‘baseUrl’]”
替换前:
1 window .SITE_CONFIG ['baseUrl' ] = 'http://localhost:8080/renren-fast' ;
替换后:
1 window .SITE_CONFIG['baseUrl' ] = 'http://localhost:88/api' ;
http://localhost:88,这个地址是我们网关微服务的接口。
这里我们需要通过网关来完成路径的映射,因此将renren-fast注册到nacos注册中心中,并添加配置中心
1 2 3 4 5 6 7 8 9 10 11 application: name: renren-fast cloud: nacos: discovery: server-addr: 127.0 .0 .1 :8848 config: name: renren-fast server-addr: 127.0 .0 .1 .8848 namespace: ee409c3f-3206-4a3b-ba65-7376922a886d
配置网关路由,前台的所有请求都是经由“http://localhost:88/api”来转发的,在“gulimall-gateway”中添加路由规则:
1 2 3 4 - id: admin_route uri: lb://renren-fast predicates: - Path=/api/**
但是这样做也引入了另外的一个问题,再次访问:http://localhost:8001/#/login,发现验证码不再显示:
分析原因:
现在的验证码请求路径为,http://localhost:88/api/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
原始的验证码请求路径:http://localhost:8001/renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
在admin_route的路由规则下,在访问路径中包含了“api”,因此它会将它转发到renren-fast,网关在转发的时候,会使用网关的前缀信息,为了能够正常的取得验证码,我们需要对请求路径进行重写
关于请求路径重写:
6.16. The RewritePath
GatewayFilter
Factory
The RewritePath
GatewayFilter
factory takes a path regexp
parameter and a replacement
parameter. This uses Java regular expressions for a flexible way to rewrite the request path. The following listing configures a RewritePath
GatewayFilter
:
Example 41. application.yml
1 2 3 4 5 6 7 8 9 10 spring: cloud: gateway: routes: - id: rewritepath_route uri: https://example.org predicates: - Path=/foo/** filters: - RewritePath=/red(?<segment>/?.*), $\{segment}
For a request path of /red/blue
, this sets the path to /blue
before making the downstream request. Note that the $
should be replaced with $\
because of the YAML specification.
修改“admin_route”路由规则:
1 2 3 4 5 6 - id: admin_route uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
再次访问:http://localhost:8001/#/login,验证码能够正常的加载了。
但是很不幸新的问题又产生了,访问被拒绝了
问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制
跨域流程:
解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class GulimallCorsConfiguration { @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); } }
再次访问:http://localhost:8001/#/login
http://localhost:8001/renre已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
出现了多个请求,并且也存在多个跨源请求。
为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。
在显示分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree
但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。
解决方法就是定义一个product路由规则,进行路径重写:
1 2 3 4 5 6 - id: product_route uri: lb://gulimall-product predicates: - Path=/api/product/** filters: - RewritePath=/api/(?<segment>/?.*),/$\{segment}
在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。
删除/添加数据 添加delete和append标识,并且增加复选框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <el-tree :data="menus" show-checkbox //显示复选框 :props="defaultProps" :expand-on-click-node="false" //设置节点点击时不展开 node-key="catId" > <span class="custom-tree-node" slot-scope="{ node, data }"> <span>{{ node.label }}</span> <span> <el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">Append</el-button> <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)" >Delete</el-button> </span> </span> </el-tree>
修改“com.atguigu.gulimall.product.controller.CategoryController”类,添加如下代码:
1 2 3 4 5 6 7 @RequestMapping("/delete") public R delete (@RequestBody Long[] catIds) { categoryService.removeMenuByIds(Arrays.asList(catIds)); return R.ok(); }
com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
1 2 3 4 5 @Override public void removeMenuByIds (List<Long> asList) { categoryDao.deleteBatchIds(asList); }
然而多数时候,我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除;
可以设置show_status为0,标记它已经被删除。
mybatis-plus的逻辑删除:
配置全局的逻辑删除规则,在“src/main/resources/application.yml”文件中添加如下内容:
1 2 3 4 5 6 mybatis-plus: global-config: db-config: id-type: auto logic-delete-value: 1 logic-not-delete-value: 0
修改“com.atguigu.gulimall.product.entity.CategoryEntity”类,添加上@TableLogic,表明使用逻辑删除:
1 2 3 4 5 @TableLogic(value = "1",delval = "0") private Integer showStatus;
然后在POSTMan中测试一下是否能够满足需要。另外在“src/main/resources/application.yml”文件中,设置日志级别,打印出SQL语句:
1 2 3 logging: level: io.niceseason.gulimall.product: debug
打印的日志:
1 2 3 4 ==> Preparing: UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1 ==> Parameters: 1431 (Long) <== Updates: 1 get changedGroupKeys:[]
删除细节优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 remove (node, data ) { var ids = [data.catId ]; this .$confirm(`是否删除【${data.name} 】菜单?` , "提示" , { confirmButtonText : "确定" , cancelButtonText : "取消" , type : "warning" , }) .then (() => { this .$http({ url : this .$http .adornUrl ("/product/category/delete" ), method : "post" , data : this .$http .adornData (ids, false ), }).then (({ data } ) => { this .$message({ message : "菜单删除成功" , type : "success" , }); this .getMenus (); this .expandedKey = [node.parent .data .catId ]; }); }) .catch (() => {}); console .log ("remove" , node, data); },
添加数据
在模板上添加分类对话框
1 2 3 4 5 6 7 8 9 10 11 <el-dialog title ="添加分类" :visible.sync ="dialogFormVisible" > <el-form :model ="category" > <el-form-item label ="分类名称" > <el-input v-model ="category.name" autocomplete ="off" > </el-input > </el-form-item > </el-form > <div slot ="footer" class ="dialog-footer" > <el-button @click ="dialogFormVisible = false" > 取 消</el-button > <el-button type ="primary" @click ="addCategory" > 确 定</el-button > </div > </el-dialog >
在data属性中增加对话框显示属性dialogFormVisible
和提交数据category
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 data ( ) { return { menus : [], defaultProps : { children : "childrens" , label : "name" , }, expandedKey : [], dialogFormVisible : false , category : { name :"" , parentCid : 0 , catLevel : 0 , showStatus : 1 , sort : 0 , productUnit : "" , icon : "" , catId : null , }, }; },
分别添加添加
和确定
对应函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 append (data ) { console .log ("添加数据" , data); this .dialogFormVisible =true ; this .category .parentCid = data.catId ; this .category .catLevel = data.catLevel * 1 + 1 ; this .category .catId = null ; this .category .name = "" ; this .category .icon = "" ; this .category .productUnit = "" ; this .category .sort = 0 ; this .category .showStatus = 1 ; }, addCategory ( ){ this .$http({ url : this .$http .adornUrl ('/product/category/save' ), method : 'post' , data : this .$http .adornData (this .category , false ) }).then (({ data } ) => { this .$message({ message : "菜单保存成功" , type : "success" , }); this .dialogFormVisible = false ; this .getMenus (); this .expandedKey = [this .category .parentCid ]; }); }, },
修改数据 添加修改按钮
1 <el-button type ="text" size ="mini" @click ="() => edit(data)" > Edit</el-button >
使对话框回显数据并显示标题修改分类
,由于与 增加分类
公用统一对话框,所以需要添加属性title
并定制函数submitData()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <el-dialog :title ="title" :visible.sync ="dialogFormVisible" > <el-form :model ="category" > <el-form-item label ="分类名称" > <el-input v-model ="category.name" autocomplete ="off" > </el-input > </el-form-item > <el-form-item label ="图标" > <el-input v-model ="category.icon" autocomplete ="off" > </el-input > </el-form-item > <el-form-item label ="计量单位" > <el-input v-model ="category.productUnit" autocomplete ="off" > </el-input > </el-form-item > </el-form > <div slot ="footer" class ="dialog-footer" > <el-button @click ="dialogFormVisible = false" > 取 消</el-button > <el-button type ="primary" @click ="submitData" > 确 定</el-button > </div > </el-dialog >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 data ( ) { return { menus : [], defaultProps : { children : "childrens" , label : "name" , }, expandedKey : [], dialogFormVisible : false , category : { catId : null , name : "" , parentCid : 0 , catLevel : 0 , showStatus : 1 , sort : 0 , productUnit : "" , icon : "" , catId : null , }, title : "" , dialogType : "" }; }, edit (data ) { this .title = "修改分类" , this .dialogFormVisible = true , this .dialogType ="edit" , console .log ("修改数据" , data); this .$http({ url : this .$http .adornUrl (`/product/category/info/${data.catId} ` ), method : "get" , }).then (({ data } ) => { console .log ("回显数据" , data.category ); this .category = data.category ; }); }, editCategory ( ){ var {catId,name,icon,productUnit}=this .category ; this .$http({ url : this .$http .adornUrl ("/product/category/update" ), method : "post" , data : this .$http .adornData ({catId,name,icon,productUnit}, false ), }).then (({ data } ) => { this .$message({ message : "菜单修改成功" , type : "success" , }); this .dialogFormVisible = false ; this .getMenus (); this .expandedKey = [this .category .parentCid ]; }); }, submitData ( ){ this .dialogType =="add" ?this .addCategory ():this .editCategory (); }, },
菜单拖动 开启拖拽功能
在<el-tree>
添加属性draggable
开启拖拽功能
1 2 3 4 5 6 7 8 9 10 11 12 <el-tree :data ="menus" :props ="defaultProps" :expand-on-click-node ="false" show-checkbox node-key ="catId" :default-expanded-keys ="expandedKey" :draggable ="draggable" :allow-drop ="allowDrop" //绑定允许拖拽的函数 @node-drop ="handleDrop" ref ="menuTree" >
限制可拖拽范围
由于我们的菜单是三级分类,所以未防止超出三级的情况,有部分情况不允许被拖入:比如被拖拽的节点本身包含两级菜单,将其拖进第二层级的节点,那么最深层级就达到了四级,为防止这种情况的出现,我们需要编写在<el-tree>
中绑定allow-drop
属性并编写allowDrop()
函数
allowDrop()
的思路为将被拖拽节点的子节点通过递归遍历找出最深节点的level
,然后将被拖拽节点的相对深度与目标节点的相对深度相加,看是否超出最大深度3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 allowDrop (draggingNode, dropNode, type ){ console .log ("拖拽节点" ,draggingNode,dropNode,type); this .maxLevel =draggingNode.level ; this .countNodeLevel (draggingNode); let deep=(this .maxLevel -draggingNode.level )+1 ; console .log ("deep:" ,deep,"maxlevel:" ,this .maxLevel ,"dragging:" ,draggingNode.level ); if (type=="inner" ){ return deep+dropNode.level <=3 ; }else { return deep+dropNode.parent .level <=3 ; } }, countNodeLevel (node ){ if (node.childNodes !=null &&node.childNodes .length !=0 ){ for (let i=0 ;i<node.childNodes .length ;i++){ if (node.childNodes [i].level >this .maxLevel ){ this .maxLevel =node.childNodes [i].level ; } this .countNodeLevel (node.childNodes [i]); } } },
拖拽完成
拖拽完成后我们需要更新三个状态:
当前节点最新的父节点id,
当前拖拽节点的最新顺序
遍历姊妹节点的顺序即为新顺序
当前拖拽节点的最新层级
当前拖拽层级变化需要更新拖拽节点及其子节点
拖拽完成后需要更新变化的节点,根据被拖拽节点的防止位置的不同,变化的部分也有所不同
inner
父节点为dropNode
节点
姊妹节点为dropNode
的孩子节点
before/after
父节点为dropNode
的父节点
姊妹节点为dropNode
的父节点的孩子节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 handleDrop (draggingNode, dropNode, dropType, ev ) { let pCid=0 ; let siblings=null ; if (dropType=="inner" ){ pCid=dropNode.data .catId ; siblings=dropNode.childNodes ; }else { pCid=dropNode.parent .data .catId ==undefined ?0 :dropNode.parent .data .catId ; siblings=dropNode.parent .childNodes ; } this .pCid .push (pCid); for (let i=0 ;i<siblings.length ;i++){ if (siblings[i].data .catId ==draggingNode.data .catId ){ let catLevel=draggingNode.catLevel ; if (catLevel!=draggingNode.level ){ this .updateChildNodeLevel (siblings[i]); catLevel=draggingNode.level ; } this .updateNodes .push ({ catId :siblings[i].data .catId , catLevel, sort :i, parentCid :pCid, }); }else { this .updateNodes .push ({ catId :siblings[i].data .catId , sort :i, }); } } console .log (this .updateNodes ); }, updateChildNodeLevel (node ){ if (node.childNodes .length >0 ){ for (let i=0 ;i<node.childNodes .length ;i++){ this .updateNodes .push ({ catId :node.childNodes [i].data .catId , catLevel :node.childNodes [i].level , }); this .updateChildNodeLevel (node.childNodes [i]); } } },
设置菜单拖动开关
1 2 <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch> <el-button v-if="draggable" @click="batchSave">批量保存</el-button>
现在存在的一个问题是每次拖拽的时候,都会发送请求,更新数据库这样频繁的与数据库交互,现在想要实现一个拖拽过程中不更新数据库,拖拽完成后,通过批量保存
统一提交拖拽后的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 batchSave ( ){ this .$http({ url : this .$http .adornUrl ('/product/category/updateNodes' ), method : 'post' , data : this .$http .adornData (this .updateNodes , false ) }).then (({ data } ) => { this .$message({ message : "菜单顺序等修改成功" , type : "success" }); this .getMenus (); this .expandedKey = this .pCid ; this .updateNodes = []; this .maxLevel = 0 ; }); }
现在还存在一个问题,如果是将一个菜单连续的拖拽,最终还放到了原来的位置,但是updateNode中却出现了很多节点更新信息,这样显然也是一个问题。
批量删除 添加删除按钮
1 <el-button type="danger" plain size="small" @click="batchDelete">批量删除</el-button>
在<el-tree>
中添加 ref="tree"
属性以获得选中节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 batchDelete ( ){ let checkNodes = this .$refs .tree .getCheckedNodes (); let ids=[]; let names=[]; for (let i=0 ;i<checkNodes.length ;i++){ ids.push (checkNodes[i].catId ); names.push (checkNodes[i].name ); } this .$confirm(`是否删除【${names} 】菜单?` , "提示" , { confirmButtonText : "确定" , cancelButtonText : "取消" , type : "warning" , }).then (()=> { this .$http({ url : this .$http .adornUrl ('/product/category/delete' ), method : 'post' , data : this .$http .adornData (ids, false ) }).then (() => { this .$message({ message : "批量删除成功" , type : "success" }); this .getMenus (); }); } ).catch (); },
品牌菜单管理
将“”逆向工程得到的resources\src\views\modules\product文件拷贝到gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件
brand.vue brand-add-or-update.vue
但是显示的页面没有新增和删除功能,这是因为权限控制的原因,
1 2 <el-button v-if="isAuth('product:brand:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button> <el-button v-if="isAuth('product:brand:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
查看“isAuth”的定义位置:
它是在“index.js”中定义,现在将它设置为返回值为true,即可显示添加和删除功能。
再次刷新页面能够看到,按钮已经出现了:
添加“显示状态按钮” brand.vue
1 2 3 4 5 6 7 8 9 10 <template slot-scope="scope"> <el-switch v-model="scope.row.showStatus" active-color="#13ce66" inactive-color="#ff4949" @change="updateBrandStatus(scope.row)" :active-value = "1" :inactive-value = "0" ></el-switch> </template>
brand-add-or-update.vue
1 2 3 <el-form-item label="显示状态" prop="showStatus"> <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch> </el-form-item>
brand.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 updateBrandStatus (data ) { console .log ("最新状态" , data); let {brandId,showStatus} = data; this .$http({ url : this .$http .adornUrl ("/product/brand/update" ), method : "post" , data : this .$http .adornData ({brandId,showStatus}, false ) }).then (({ data } ) => { this .$message({ message : "状态更新成功" , type : "success" }); }); },
添加上传 和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
这里我们选择将图片放置到阿里云上,使用对象存储。
阿里云上使使用对象存储方式:
查看阿里云关于文件上传的帮助: https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ
1)添加依赖包 在Maven项目中加入依赖项(推荐方式)
在 Maven 工程中使用 OSS Java SDK,只需在 pom.xml 中加入相应依赖即可。以 3.8.0 版本为例,在 内加入如下内容:
1 2 3 4 5 <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.8.0</version > </dependency >
2)上传文件流 以下代码用于上传文件流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 String endpoint = "http://oss-cn-hangzhou.aliyuncs.com" ;String accessKeyId = "<yourAccessKeyId>" ;String accessKeySecret = "<yourAccessKeySecret>" ;OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret);InputStream inputStream = new FileInputStream ("<yourlocalFile>" );ossClient.putObject("<yourBucketName>" , "<yourObjectName>" , inputStream); ossClient.shutdown();
endpoint的取值:
accessKeyId和accessKeySecret需要创建一个RAM账号:
创建用户完毕后,会得到一个“AccessKey ID”和“AccessKeySecret”,然后复制这两个值到代码的“AccessKey ID”和“AccessKeySecret”。
另外还需要添加访问控制权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testUpload () throws FileNotFoundException { String endpoint = "oss-cn-shanghai.aliyuncs.com" ; String accessKeyId = "LTAI4G4W1RA4JXz2QhoDwHhi" ; String accessKeySecret = "R99lmDOJumF2x43ZBKT259Qpe70Oxw" ; OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); InputStream inputStream = new FileInputStream ("C:\\Users\\Administrator\\Pictures\\timg.jpg" ); ossClient.putObject("gulimall-images" , "time.jpg" , inputStream); ossClient.shutdown(); System.out.println("上传成功." ); }
更为简单的使用方式,是使用SpringCloud Alibaba
详细使用方法,见: https://help.aliyun.com/knowledge_detail/108650.html
(1)添加依赖
1 2 3 4 5 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alicloud-oss</artifactId > <version > 2.2.0.RELEASE</version > </dependency >
(2)创建“AccessKey ID”和“AccessKeySecret”
(3)配置key,secret和endpoint相关信息
1 2 3 4 access-key: xxx secret-key: xxx oss: endpoint: oss-cn-shanghai.aliyuncs.com
(4)注入OSSClient并进行文件上传下载等操作
但是这样来做还是比较麻烦,如果以后的上传任务都交给gulimall-product来完成,显然耦合度高。最好单独新建一个Module来完成文件上传任务。
其他方式 1)新建gulimall-third-party 2)添加依赖,将原来gulimall-common中的“spring-cloud-starter-alicloud-oss”依赖移动到该项目中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alicloud-oss</artifactId > <version > 2.2.0.RELEASE</version > </dependency > <dependency > <groupId > com.atguigu.gulimall</groupId > <artifactId > gulimall-common</artifactId > <version > 1.0-SNAPSHOT</version > <exclusions > <exclusion > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > </exclusion > </exclusions > </dependency >
另外也需要在“pom.xml”文件中,添加如下的依赖管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > ${spring-cloud.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-alibaba-dependencies</artifactId > <version > 2.2.1.RELEASE</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement >
3)在主启动类中开启服务的注册和发现
4)在nacos中注册 (1)创建命名空间“ gulimall-third-party ”
(2)在“ gulimall-third-party”命名空间中,创建“ gulimall-third-party.yml”文件
1 2 3 4 5 6 7 spring: cloud: alicloud: access-key: LTAI4G4W1RA4JXz2QhoDwHhi secret-key: R99lmDOJumF2x43ZBKT259Qpe70Oxw oss: endpoint: oss-cn-shanghai.aliyuncs.com
5)编写配置文件 application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 30000 spring: application: name: gulimall-third-party cloud: nacos: discovery: server-addr: 192.168 .137 .14 :8848 logging: level: io.niceseason.gulimall.product: debug
bootstrap.properties
1 2 3 4 5 6 spring.cloud.nacos.config.name =gulimall-third-party spring.cloud.nacos.config.server-addr =192.168.137.14:8848 spring.cloud.nacos.config.namespace =f995d8ee-c53a-4d29-8316-a1ef54775e00 spring.cloud.nacos.config.extension-configs[0].data-id =gulimall-third-party.yml spring.cloud.nacos.config.extension-configs[0].group =DEFAULT_GROUP spring.cloud.nacos.config.extension-configs[0].refresh =true
6) 编写测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package io.niceseason.gulimall.thirdparty;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClient;import com.aliyun.oss.OSSClientBuilder;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.InputStream;@SpringBootTest class GulimallThirdPartyApplicationTests { @Autowired OSSClient ossClient; @Test public void testUpload () throws FileNotFoundException { String endpoint = "oss-cn-shanghai.aliyuncs.com" ; String accessKeyId = "LTAI4G4W1RA4JXz2QhoDwHhi" ; String accessKeySecret = "R99lmDOJumF2x43ZBKT259Qpe70Oxw" ; OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); InputStream inputStream = new FileInputStream ("C:\\Users\\Administrator\\Pictures\\timg.jpg" ); ossClient.putObject("gulimall-images" , "time3.jpg" , inputStream); ossClient.shutdown(); System.out.println("上传成功." ); } }
https://help.aliyun.com/document_detail/31926.html?spm=a2c4g.11186623.6.1527.228d74b8V6IZuT
背景
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传 )时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。
原理介绍
服务端签名后直传的原理如下:
用户发送上传Policy请求到应用服务器。
应用服务器返回上传Policy和签名给用户。
用户直接上传数据到OSS。
编写“com.atguigu.gulimall.thirdparty.controller.OssController”类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 package com.atguigu.gulimall.thirdparty.controller;import com.aliyun.oss.OSS;import com.aliyun.oss.common.utils.BinaryUtil;import com.aliyun.oss.model.MatchMode;import com.aliyun.oss.model.PolicyConditions;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.text.SimpleDateFormat;import java.util.Date;import java.util.LinkedHashMap;import java.util.Map;@RestController public class OssController { @Autowired OSS ossClient; @Value ("${spring.cloud.alicloud.oss.endpoint}" ) String endpoint ; @Value("${spring.cloud.alicloud.oss.bucket}") String bucket ; @Value("${spring.cloud.alicloud.access-key}") String accessId ; @Value("${spring.cloud.alicloud.secret-key}") String accessKey ; @RequestMapping("/oss/policy") public Map<String, String> policy () { String host = "https://" + bucket + "." + endpoint; String format = new SimpleDateFormat ("yyyy-MM-dd" ).format(new Date ()); String dir = format; Map<String, String> respMap=null ; try { long expireTime = 30 ; long expireEndTime = System.currentTimeMillis() + expireTime * 1000 ; Date expiration = new Date (expireEndTime); 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 <String, String>(); 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 )); } catch (Exception e) { System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return respMap; } }
测试: http://localhost:30000/oss/policy
1 { "accessid" : "LTAI4G4W1RA4JXz2QhoDwHhi" , "policy" : "eyJleHBpcmF0aW9uIjoiMjAyMC0wNC0yOVQwMjo1ODowNy41NzhaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIwLTA0LTI5LyJdXX0=" , "signature" : "s42iRxtxGFmHyG40StM3d9vOfFk=" , "dir" : "2020-04-29/" , "host" : "https://gulimall-images.oss-cn-shanghai.aliyuncs.com" , "expire" : "1588129087" }
以后在上传文件时的访问路径为“ http://localhost:88/api/thirdparty/oss/policy”,
在“gulimall-gateway”中配置路由规则:
1 2 3 4 5 6 - id: third_party_route uri: lb://gulimall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?<segment>/?.*),/$\{segment}
测试是否能够正常跳转: http://localhost:88/api/thirdparty/oss/policy
上传组件 放置项目提供的upload文件夹到components目录下,一个是单文件上传,另外一个是多文件上传
1 2 3 4 5 6 7 8 9 10 11 PS D:\Project\gulimall\renren-fast-vue\src\components\upload> ls 目录: D:\Project\gulimall\renren-fast-vue\src\components\upload Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2020/4/29 星期三 12:02 3122 multiUpload.vue -a---- 2019/11/11 星期一 21:20 343 policy.js -a---- 2020/4/29 星期三 12:01 3053 singleUpload.vue
修改这两个文件的配置后
开始执行上传,但是在上传过程中,出现了如下的问题:
1 Access to XMLHttpRequest at 'http://gulimall-images.oss-cn-shanghai.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.
这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:
再次执行文件上传。
JSR303校验 步骤1:使用校验注解 在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
在非空处理方式上提供了@NotNull,@Blank和@NotEmpty
(1)@NotNull
The annotated element must not be null. Accepts any type. 注解元素禁止为null,能够接收任何类型
(2)@NotEmpty
the annotated element must not be null nor empty.
该注解修饰的字段不能为null或””
Supported types are:
支持以下几种类型
CharSequence (length of character sequence is evaluated)
字符序列(字符序列长度的计算)
Collection (collection size is evaluated) 集合长度的计算
Map (map size is evaluated) map长度的计算
Array (array length is evaluated) 数组长度的计算
(3)@NotBlank
The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence. 该注解不能为null,并且至少包含一个非空白字符。接收字符序列。
步骤2:在请求方法种,使用校验注解@Valid,开启校验, 1 2 3 4 5 6 @RequestMapping("/save") public R save (@Valid @RequestBody BrandEntity brand) { brandService.save(brand); return R.ok(); }
测试: http://localhost:88/api/product/brand/save
在postman种发送上面的请求
能够看到”defaultMessage”: “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 javax.validation.constraints.AssertFalse.message = 只能为false javax.validation.constraints.AssertTrue.message = 只能为true javax.validation.constraints.DecimalMax.message = 必须小于或等于{value} javax.validation.constraints.DecimalMin.message = 必须大于或等于{value} javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内) javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址 javax.validation.constraints.Future.message = 需要是一个将来的时间 javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间 javax.validation.constraints.Max.message = 最大不能超过{value} javax.validation.constraints.Min.message = 最小不能小于{value} javax.validation.constraints.Negative.message = 必须是负数 javax.validation.constraints.NegativeOrZero.message = 必须是负数或零 javax.validation.constraints.NotBlank.message = 不能为空 javax.validation.constraints.NotEmpty.message = 不能为空 javax.validation.constraints.NotNull.message = 不能为null javax.validation.constraints.Null.message = 必须为null javax.validation.constraints.Past.message = 需要是一个过去的时间 javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间 javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}" javax.validation.constraints.Positive.message = 必须是正数 javax.validation.constraints.PositiveOrZero.message = 必须是正数或零 javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间 org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的信用卡号码 org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一) org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码 org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址 org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间 org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间 org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配 org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配 org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配 org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配 org.hibernate.validator.constraints.NotBlank.message = 不能为空 org.hibernate.validator.constraints.NotEmpty.message = 不能为空 org.hibernate.validator.constraints.ParametersScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果 org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间 org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容 org.hibernate.validator.constraints.ScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果 org.hibernate.validator.constraints.URL.message = 需要是一个合法的URL org.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'} org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是
1 2 public @interface NotBlank { String message () default "{javax.validation.constraints.NotBlank.message}" ;
可以在添加注解的时候,修改message:
1 2 @NotBlank(message = "品牌名必须非空") private String name;
当再次发送请求时,得到错误提示信息
但是这种返回的错误结果并不符合我们的业务需要。
步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RequestMapping("/save") public R save (@Valid @RequestBody BrandEntity brand, BindingResult result) { if ( result.hasErrors()){ Map<String,String> map=new HashMap <>(); result.getFieldErrors().forEach((item)->{ String message = item.getDefaultMessage(); String field = item.getField(); map.put(field,message); }); return R.error(400 ,"提交的数据不合法" ).put("data" ,map); }else { } brandService.save(brand); return R.ok(); }
这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
步骤4:统一异常处理 可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
(1)抽取一个异常处理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package io.niceseason.gulimall.product.exception;import io.niceseason.common.utils.R;import lombok.extern.slf4j.Slf4j;import org.springframework.validation.BindingResult;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.HashMap;import java.util.Map;@Slf4j @RestControllerAdvice(basePackages = "common.atguigu.gulimall.product.controller") public class GulimallExceptionAdvice { @ExceptionHandler(value = Exception.class) public R handleValidException (MethodArgumentNotValidException exception) { Map<String,String> map=new HashMap <>(); BindingResult bindingResult = exception.getBindingResult(); bindingResult.getFieldErrors().forEach(fieldError -> { String message = fieldError.getDefaultMessage(); String field = fieldError.getField(); map.put(field,message); }); log.error("数据校验出现问题{},异常类型{}" ,exception.getMessage(),exception.getClass()); return R.error(400 ,"数据校验出现问题" ).put("data" ,map); } }
(2)测试: http://localhost:88/api/product/brand/save
(3)默认异常处理
1 2 3 4 5 @ExceptionHandler(value = Throwable.class) public R handleException (Throwable throwable) { log.error("未知异常{},异常类型{}" ,throwable.getMessage(),throwable.getClass()); return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg()); }
(4)错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则
为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package io.niceseason.common.exception;public enum BizCodeEnum { UNKNOW_EXEPTION(10000 ,"系统未知异常" ), VALID_EXCEPTION( 10001 ,"参数格式校验失败" ); private int code; private String msg; BizCodeEnum(int code, String msg) { this .code = code; this .msg = msg; } public int getCode () { return code; } public String getMsg () { return msg; } }
(5)测试: http://localhost:88/api/product/brand/save
分组校验功能(完成多场景的复杂校验) 1、给校验注解,标注上groups,指定什么情况下才需要进行校验 如:指定在更新和添加的时候,都需要进行校验
1 2 3 @NotEmpty @NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class}) private String name;
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
2、业务方法参数上使用@Validated注解,并在value中给出group接口 @Validated的value方法:
Specify one or more validation groups to apply to the validation step kicked off by this annotation. 指定一个或多个验证组以应用于此注释启动的验证步骤。
JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using them as type-safe group arguments, as implemented in SpringValidatorAdapter.
JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。
Other SmartValidator implementations may support class arguments in other ways as well.
其他SmartValidator 实现也可以以其他方式支持类参数。
3、默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。 自定义校验功能 1、编写一个自定义的校验注解 1 2 3 4 5 6 7 8 9 10 11 12 13 @Documented @Constraint(validatedBy = { ListValueConstraintValidator.class}) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) public @interface ListValue { String message () default "{com.atguigu.common.valid.ListValue.message}" ; Class<?>[] groups() default { }; Class<? extends Payload >[] payload() default { }; int [] value() default {}; }
2、编写一个自定义的校验器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ListValueConstraintValidator implements ConstraintValidator <ListValue,Integer> { private Set<Integer> set=new HashSet <>(); @Override public void initialize (ListValue constraintAnnotation) { int [] value = constraintAnnotation.value(); for (int i : value) { set.add(i); } } @Override public boolean isValid (Integer value, ConstraintValidatorContext context) { return set.contains(value); } }
3、关联自定义的校验器和自定义的校验注解 1 @Constraint(validatedBy = { ListValueConstraintValidator.class})
4、使用实例 1 2 3 4 5 @ListValue(value = {0,1},groups ={AddGroup.class}) private Integer showStatus;
商品SPU和SKU管理 重新执行“sys_menus.sql”
SPU = Standard Product Unit (标准化产品单元) ,SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。例如,iphone4就是一个SPU,N97也是一个SPU,这个与商家无关,与颜色、款式、套餐也无关。
**SKU=stock keeping unit(库存量单位)**,SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。在服装、鞋类商品中使用最多最普遍。 例如纺织品中一个SKU通常表示:规格、颜色、款式。
品牌管理和关联分类 现在想要实现点击菜单的左边,能够实现在右边展示数据
父子组件传递数据:
1)子组件给父组件传递数据,事件机制;
在category中绑定node-click事件,
1 <el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeClick" ></el-tree>
2)子组件给父组件发送一个事件,携带上数据;
1 2 3 4 nodeClick (data,Node,component ){ console .log ("子组件" ,data,Node ,component); this .$emit("tree-node-click" ,data,Node ,component); },
this.$emit(事件名,”携带的数据”);
3)父组件中的获取发送的事件
1 <category @tree-node-click="treeNodeClick"></category>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 treenodeclick (data, node, component ) { if (node.level == 3 ) { this .catId = data.catId ; this .getDataList (); } }, getDataList ( ) { this .dataListLoading = true ; this .$http({ url : this .$http .adornUrl (`/product/attrgroup/list/${this .catId} ` ), method : "get" , params : this .$http .adornParams ({ page : this .pageIndex , limit : this .pageSize , key : this .dataForm .key }) }).then (({ data } ) => { if (data && data.code === 0 ) { this .dataList = data.page .list ; this .totalPage = data.page .totalCount ; } else { this .dataList = []; this .totalPage = 0 ; } this .dataListLoading = false ; }); },
4)分组新增&级联选择器
由于三级分类的children
属性为[]
,因此显示效果如上,为了避免这种效果,我们可以为该字段添加注解 @JsonInclude(JsonInclude.Include.NON_EMPTY)
,表示当只有该字段不为空时才会返回该属性。
1 2 @JsonInclude(JsonInclude.Include.NON_EMPTY) private List<CategoryEntity> children;
4)分组修改与回显
由于修改时所属分类不能正常回显,因为缺少完整的三级路径,因此我们在AttrGroupEntity
中添加字段catelogPath
,并使用递归查找
1 2 @TableField(exist = false) private Long[] catelogPath;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RequestMapping("/info/{attrGroupId}") public R info (@PathVariable("attrGroupId") Long attrGroupId) { AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId); Long[] catelogPath=categoryService.findCatelogPathById(attrGroup.getCatelogId()); attrGroup.setCatelogPath(catelogPath); return R.ok().put("attrGroup" , attrGroup); } @Override public Long[] findCatelogPathById(Long categorygId) { List<Long> path = new LinkedList <>(); findPath(categorygId, path); Collections.reverse(path); Long[] objects = path.toArray(new Long [path.size()]); return objects; } private void findPath (Long categorygId, List<Long> path) { if (categorygId!=0 ){ path.add(categorygId); CategoryEntity byId = getById(categorygId); findPath(byId.getParentCid(),path); } }
5)分页插件激活
springboot采取以下方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @EnableTransactionManagement @Configuration @MapperScan("com.atguigu.gulimall.product.dao") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor () { PaginationInterceptor paginationInterceptor = new PaginationInterceptor (); paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize (true )); return paginationInterceptor; } }
参考MyBatis-Plus官网
关联分类
点击关联分类要查出所有已经关联的所有数据
根据品牌id查出关联所有信息
1 2 3 4 5 6 7 @RequestMapping("catelog/list") public R cateloglist (@RequestParam Long brandId) { QueryWrapper<CategoryBrandRelationEntity> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("brand_id" , brandId); List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(queryWrapper); return R.ok().put("data" , data); }
新增关联
保存对应分类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RequestMapping("/save") public R save (@RequestBody CategoryBrandRelationEntity categoryBrandRelation) { categoryBrandRelationService.saveDetail(categoryBrandRelation); return R.ok(); } @Override public void saveDetail (CategoryBrandRelationEntity categoryBrandRelation) { Long brandId = categoryBrandRelation.getBrandId(); Long catelogId = categoryBrandRelation.getCatelogId(); BrandEntity brandEntity = brandDao.selectById(brandId); CategoryEntity categoryEntity = categoryDao.selectById(catelogId); categoryBrandRelation.setBrandName(brandEntity.getName()); categoryBrandRelation.setCatelogName(categoryEntity.getName()); this .save(categoryBrandRelation); }
规格参数新增与VO 规格参数新增时,请求的URL:Request URL:
http://localhost:88/api/product/attr/base/list/0?t=1588731762158&page=1&limit=10&key=
当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范
比较规范的做法是,新建一个vo文件夹,将每种不同的对象,按照它的功能进行了划分。在java中,涉及到了这几种类型
1.PO(persistant object) 持久对象
PO 就是对应数据库中某个表中的一条记录,多个记录可以用 PO 的集合。 PO 中应该不包含任何对数据库的操作。
2.DO(Domain Object)领域对象
就是从现实世界中抽象出来的有形或无形的业务实体。
3.TO(Transfer Object) ,数据传输对象
不同的应用程序之间传输的对象
4.DTO(Data Transfer Object)数据传输对象
这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象。
5.VO(value object) 值对象 通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已。但应是抽象出的业务对象 , 可以和表对应 , 也可以不 , 这根据业务的需要 。用 new 关键字创建,由GC 回收的。
View object:视图对象; 接受页面传递来的数据,封装对象 将业务处理完成的对象,封装成页面要用的数据
6.BO(business object) 业务对象
从业务模型的角度看 , 见 UML 元件领域模型中的领域对象。封装业务逻辑的 java 对象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。business object: 业务对象 主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。 比如一个简历,有教育经历、工作经历、社会关系等等。 我们可以把教育经历对应一个 PO ,工作经历对应一个 PO ,社会关系对应一个 PO 。 建立一个对应简历的 BO 对象处理简历,每个 BO 包含这些 PO 。 这样处理业务逻辑时,我们就可以针对 BO 去处理。
7.POJO(plain ordinary java object) 简单无规则 java 对象
传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增加别的属性和方法。我的理解就是最基本的 java Bean ,只有属性字段及 setter 和 getter方法!。POJO 是 DO/DTO/BO/VO 的统称。
8.DAO(data access object) 数据访问对象
是一个 sun 的一个标准 j2ee 设计模式, 这个模式中有个接口就是 DAO ,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合 VO, 提供数据库的 CRUD 操作.
Request URL: http://localhost:88/api/product/attr/save,现在的情况是,它在保存的时候,只是保存了attr,并没有保存attrgroup,为了解决这个问题,我们新建了一个vo/AttrVo,在原AttrEntity基础上增加了attrGroupId字段,使得保存新增数据的时候,也保存了它们之间的关系。
1 2 3 4 @Data public class AttrVo extends AttrEntity { private Long attrGroupId; }
并且由于查询时显示了所属分类名和所属分组名,并且在修改时要回显其三级分类,所以我们要为返回时的属性定制vo
1 2 3 4 5 6 7 8 9 10 11 @Data public class AttrResponseVo extends AttrVo { private String catelogName; private String groupName; private Long[] catelogPath; }
通过” BeanUtils.copyProperties(attr,attrEntity);”能够实现在两个Bean之间拷贝数据,但是两个Bean的字段要相同
1 2 3 4 5 6 @Override public void saveAttr (AttrVo attr) { AttrEntity attrEntity = new AttrEntity (); BeanUtils.copyProperties(attr,attrEntity); this .save(attrEntity); }
问题:现在有两个查询,一个是查询部分,另外一个是查询全部,但是又必须这样来做吗?还是有必要的,但是可以在后台进行设计,两种查询是根据catId是否为零进行区分的。
规格 参数与销售属性的增删改查 查询
可以通过在添加路径变量{attrType}
同时用一个方法查询销售属性和规格参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @RequestMapping("/{attrType}/list/{catelogId}") public R infoCatelog (@RequestParam Map<String, Object> params, @PathVariable("catelogId") long catelogId, @PathVariable("attrType") String attrType) { PageUtils page = attrService.queryPage(params,catelogId,attrType); return R.ok().put("page" , page); } @Transactional @Override public PageUtils queryPage (Map<String, Object> params, long catelogId,String attrType) { QueryWrapper<AttrEntity> attrEntityQueryWrapper = new QueryWrapper <AttrEntity>().eq("attr_type" ,"base" .equalsIgnoreCase(attrType)?1 :0 ); if (catelogId != 0 ) { attrEntityQueryWrapper.eq("catelog_id" , catelogId); } String key = (String) params.get("key" ); if (!StringUtils.isEmpty(key)) { attrEntityQueryWrapper.and((wrapper) -> wrapper.eq("attr_id" , key).or().like("attr_name" , key)); } IPage<AttrEntity> page = this .page( new Query <AttrEntity>().getPage(params), attrEntityQueryWrapper ); List<AttrEntity> records = page.getRecords(); List<AttrRespVo> collect = records.stream().map((entity) -> { AttrRespVo respVo = new AttrRespVo (); BeanUtils.copyProperties(entity, respVo); CategoryEntity categoryEntity = categoryDao.selectOne(new QueryWrapper <CategoryEntity>().eq("cat_id" , entity.getCatelogId())); respVo.setCatelogName(categoryEntity.getName()); if ("base" .equalsIgnoreCase(attrType)) { AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper <AttrAttrgroupRelationEntity>().eq("attr_id" , entity.getAttrId())); if (attrAttrgroupRelationEntity != null && attrAttrgroupRelationEntity.getAttrGroupId() != null ) { AttrGroupEntity attrGroupEntity = attrGroupDao.selectOne(new QueryWrapper <AttrGroupEntity>().eq("attr_group_id" , attrAttrgroupRelationEntity.getAttrGroupId())); respVo.setGroupName(attrGroupEntity.getAttrGroupName()); } } return respVo; }).collect(Collectors.toList()); PageUtils pageUtils = new PageUtils (page); pageUtils.setList(collect); return pageUtils; }
保存
使用AttrVo
封装属性,如果AttrGroupId
非空,则为规则参数,需要更新属性-分组表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RequestMapping("/save") public R save (@RequestBody AttrVo attr) { attrService.saveAttr(attr); return R.ok(); } @Transactional @Override public void saveAttr (AttrVo attr) { AttrEntity attrEntity = new AttrEntity (); BeanUtils.copyProperties(attr,attrEntity); this .save(attrEntity); if (attr.getAttrGroupId() != null ) { AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity (); attrAttrgroupRelationEntity.setAttrGroupId(attr.getAttrGroupId()); attrAttrgroupRelationEntity.setAttrId(attrEntity.getAttrId()); attrAttrgroupRelationDao.insert(attrAttrgroupRelationEntity); } }
修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RequestMapping("/update") public R update (@RequestBody AttrVo attr) { attrService.updateAttr(attr); return R.ok(); } @Transactional @Override public void updateAttr (AttrVo attr) { AttrEntity entity = new AttrEntity (); BeanUtils.copyProperties(attr,entity); this .baseMapper.updateById(entity); if (attr.getAttrGroupId() != null ) { AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity (); attrAttrgroupRelationEntity.setAttrId(attr.getAttrId()); attrAttrgroupRelationEntity.setAttrGroupId(attr.getAttrGroupId()); Integer c = attrAttrgroupRelationDao.selectCount(new QueryWrapper <AttrAttrgroupRelationEntity>().eq("attr_id" , attrAttrgroupRelationEntity.getAttrId())); if (c>0 ){ attrAttrgroupRelationDao.update(attrAttrgroupRelationEntity, new UpdateWrapper <AttrAttrgroupRelationEntity>().eq("attr_id" , attr.getAttrId())); }else { attrAttrgroupRelationDao.insert(attrAttrgroupRelationEntity); } } }
修改回显时查询数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RequestMapping("/info/{attrId}") public R info (@PathVariable("attrId") Long attrId) { AttrRespVo respVo=attrService.getAttrInfo(attrId); return R.ok().put("attr" , respVo); } @Transactional @Override public AttrRespVo getAttrInfo (Long attrId) { AttrEntity attrEntity = this .baseMapper.selectById(attrId); AttrRespVo respVo = new AttrRespVo (); BeanUtils.copyProperties(attrEntity,respVo); AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper <AttrAttrgroupRelationEntity>().eq("attr_id" , attrEntity.getAttrId())); if (attrAttrgroupRelationEntity != null && attrAttrgroupRelationEntity.getAttrGroupId() != null ) { AttrGroupEntity attrGroupEntity = attrGroupDao.selectOne(new QueryWrapper <AttrGroupEntity>().eq("attr_group_id" , attrAttrgroupRelationEntity.getAttrGroupId())); respVo.setGroupName(attrGroupEntity.getAttrGroupName()); respVo.setAttrGroupId(attrGroupEntity.getAttrGroupId()); } CategoryEntity categoryEntity = categoryDao.selectOne(new QueryWrapper <CategoryEntity>().eq("cat_id" , attrEntity.getCatelogId())); respVo.setCatelogName(categoryEntity.getName()); Long[] catelogPathById = categoryService.findCatelogPathById(categoryEntity.getCatId()); respVo.setCatelogPath(catelogPathById); return respVo; }
查询分组关联属性和删除关联 获取属性分组的关联的所有属性
API:https://easydoc.xyz/doc/75716633/ZUqEdvA4/LnjzZHPj
发送请求:/product/attrgroup/{attrgroupId}/attr/relation
获取当前属性分组所关联的属性
如何查找:既然给出了attr_group_id,那么到中间表中查询出来所关联的attr_id,然后得到最终的所有属性即可。
可能出现null值的问题
查询分组未关联的属性 /product/attrgroup/{attrgroupId}/noattr/relation
API:https://easydoc.xyz/doc/75716633/ZUqEdvA4/d3EezLdO
获取属性分组里面还没有关联的本分类里面的其他基本属性,方便添加新的关联
Request URL: http://localhost:88/api/product/attrgroup/1/noattr/relation?t=1588780783441&page=1&limit=10&key=
属性分组,对应于“pms_attr_group”表,每个分组下,需要查看到关联了哪些属性信息,销售属性不需要和分组进行关联,但是规格参数要和属性分组进行关联。
规格参数:对应于pms_attr
表,attr_type=1,需要显示分组信息
销售属性:对应于pms_attr`表,attr_type=0,不需要显示分组信息
分组ID为9的分组:Request URL: http://localhost:88/api/product/attrgroup/9/noattr/relation?t=1588822258669&page=1&limit=10&key=
对应的数据库字段
attr_group_id attr_group_name sort descript icon catelog_id
9 主体 1 型号 平台 wu 454
10 显卡 1 显存容量 wu 454
11 输入设备 1 鼠标 键盘 wu 454
12 主板 1 显卡类型 芯片组 wu 454
13 规格 1 尺寸 wu 454
查询attrgroupId=9的属性分组:
1 AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
获取到分类信息:
1 Long catelogId = attrGroupEntity.getCatelogId()
目标:获取属性分组没有关联的其他属性
也就是获取attrgroupId=9的属性分组中,关联的分类catelog_id =454 (台式机),其他基本属性
在该属性分组中,现在已经关联的属性:
本分类下,存在哪些基本属性?
没有关联的其他属性
已经关联的属性,这些属性是如何关联上的?
答:在创建规格参数的时候,已经设置了需要关联哪些属性分组。
想要知道还没有关联哪些,先查看关联了哪些,如何排除掉这些就是未关联的
在中间表中显示了属性和属性分组之间的关联关系,在属性表中显示了所有的属性,
先查询中间表,得到所有已经关联的属性的id,然后再次查询属性表,排除掉已经建立关联的属性ID,将剩下的属性ID和属性建立起关联关系
添加属性和分组的关联关系 请求类型:Request URL: http://localhost:88/api/product/attrgroup/attr/relation
请求方式:POST
请求数据:[{“attrId”:10,”attrGroupId”:9}]
API:https://easydoc.xyz/doc/75716633/ZUqEdvA4/VhgnaedC
响应数据:
1 2 3 4 { "msg" : "success" , "code" : 0 }
本质就是在中间表pms_attr_attrgroup_relation中,添加一条记录的过程
发布商品 获取所有会员等级:/member/memberlevel/list
API:https://easydoc.xyz/doc/75716633/ZUqEdvA4/jCFganpf
在“gulimall-gateway”中修改“”文件,添加对于member的路由
1 2 3 4 5 6 - id: gulimall-member uri: lb://gulimall-member predicates: - Path=/api/member/** filters: - RewritePath=/api/(?<segment>/?.*),/$\{segment}
在“gulimall-member”中,创建“bootstrap.properties”文件,内容如下:
1 2 3 4 5 6 spring.cloud.nacos.config.name =gulimall-member spring.cloud.nacos.config.server-addr =xxx:8848 spring.cloud.nacos.config.namespace =xxx spring.cloud.nacos.config.extension-configs[0].data-id =gulimall-member.yml spring.cloud.nacos.config.extension-configs[0].group =DEFAULT_GROUP spring.cloud.nacos.config.extension-configs[0].refresh =true
获取分类关联的品牌 :/product/categorybrandrelation/brands/list
API:https://easydoc.xyz/doc/75716633/ZUqEdvA4/HgVjlzWV
遇到PubSub问题
分类变化后请求没有被监听无法发送查询品牌信息的请求
首先安装pubsub-js
订阅方组件,在src下的main.js中引用:
1 2 import PubSub from 'pubsub-js' Vue .prototype .PubSub = PubSub
获取分类下所有分组&关联属性
请求类型:/product/attrgroup/{catelogId}/withattr
请求方式:GET
请求URL:http://localhost:88/api/product/attrgroup/225/withattr?t=1588864569478
mysql默认的隔离级别为读已提交,为了能够在调试过程中,获取到数据库中的数据信息,可以调整隔离级别为读未提交:
1 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
但是它对于当前的事务窗口生效,如果想要设置全局的,需要加上global字段。
商品管理 当新建时:
1 2 3 4 5 6 7 t: 1588983621569 status: 0 key: brandId: 0 catelogId: 0 page: 1 limit: 10
当上架时:
1 2 3 4 5 6 7 t: 1588983754030 status: 1 key: brandId: 0 catelogId: 0 page: 1 limit: 10
当下架时:
1 2 3 4 5 6 7 t: 1588983789089 status: 2 key: brandId: 0 catelogId: 0 page: 1 limit: 10
在SPU中,写出的日期数据都不符合规则:
想要符合规则,可以设置写出数据的规则:
spring.jackson
1 2 jackson: date-format: yyyy-MM-dd HH:mm:ss
SKU检索:
Request URL: http://localhost:88/api/product/skuinfo/list?t=1588989437944&page=1&limit=10&key=&catelogId=0&brandId=0&min=0&max=0
请求体:
1 2 3 4 5 6 7 8 t: 1588989437944 page: 1 limit: 10 key: catelogId: 0 brandId: 0 min: 0 max: 0
API: https://easydoc.xyz/doc/75716633/ZUqEdvA4/ucirLq1D
仓库管理 库存信息表:wms_ware_info
【1】仓库列表功能:
【2】查询商品库存:
【3】查询采购需求:
【4】 合并采购需求:
合并整单选中parcharseID:Request URL: http://localhost:88/api/ware/purchase/merge
请求数据:
1 2 { purchaseId: 1 , items: [ 1 , 2 ] } items: [ 1 , 2 ]
合并整单未选择parcharseID :Request URL: http://localhost:88/api/ware/purchase/merge
涉及到两张表:wms_purchase_detail,wms_purchase
现在采购单中填写数据,然后关联用户,关联用户后,
总的含义,就是根据采购单中的信息,更新采购需求,在采购单中填写采购人员,采购单号,采购的时候,更新采购细节表中的采购人员ID和采购状态。
领取采购单
http://localhost:88/api/ware/purchase/received
(1)某个人领取了采购单后,先看采购单是否处于未分配状态,只有采购单是新建或以领取状态时,才更新采购单的状态
(2)
【1】仓库列表功能: https://easydoc.xyz/doc/75716633/ZUqEdvA4/mZgdqOWe
【2】查询商品库存: https://easydoc.xyz/doc/75716633/ZUqEdvA4/hwXrEXBZ
【3】查询采购需求: https://easydoc.xyz/doc/75716633/ZUqEdvA4/Ss4zsV7R
【4】 合并采购需求:https://easydoc.xyz/doc/75716633/ZUqEdvA4/cUlv9QvK
【5】查询未领取的采购单: https://easydoc.xyz/doc/75716633/ZUqEdvA4/hI12DNrH
【6】领取采购单: https://easydoc.xyz/doc/75716633/ZUqEdvA4/vXMBBgw1
完成采购,在完成采购过程中,需要涉及到设置SKU的name信息到仓库中,这是通过远程调用“gulimall-product”来实现根据sku_id查询得到sku_name的,如果这个过程发生了异常,事务不想要回滚,目前采用的方式是通过捕获异常的方式,防止事务回滚,是否还有其他的方式呢?这个问题留待以后解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Override public void addStock (Long skuId, Long wareId, Integer skuNum) { List<WareSkuEntity> wareSkuEntities = wareSkuDao.selectList(new QueryWrapper <WareSkuEntity>().eq("sku_id" , skuId).eq("ware_id" , wareId)); if (wareSkuEntities == null || wareSkuEntities.size() ==0 ){ WareSkuEntity wareSkuEntity = new WareSkuEntity (); wareSkuEntity.setSkuId(skuId); wareSkuEntity.setWareId(wareId); wareSkuEntity.setStock(skuNum); wareSkuEntity.setStockLocked(0 ); try { R info = productFeignService.info(skuId); if (info.getCode() == 0 ){ Map<String,Object> data=(Map<String,Object>)info.get("skuInfo" ); wareSkuEntity.setSkuName((String) data.get("skuName" )); } } catch (Exception e) { } wareSkuDao.insert(wareSkuEntity); }else { wareSkuDao.addStock(skuId,wareId,skuNum); } }
获取spu规格 在SPU管理页面,获取商品规格的时候,出现400异常,浏览器显示跳转不了
问题现象:
出现问题的代码:
1 2 3 4 5 6 7 attrUpdateShow (row ) { console .log (row); this .$router .push ({ path : "/product-attrupdate" , query : { spuId : row.id , catalogId : row.catalogId } }); },
暂时不知道如何解决问题。只能留待以后解决。
经过测试发现,问题和上面的代码没有关系,问题出现在“attrupdate.vue”上,该vue页面无法通过浏览器访问,当输入访问URL( http://localhost:8001/#/product-attrupdate )的时候,就会出现404,而其他的请求则不会出现这种情况,不知为何。
通过POSTMAN进行请求的时候,能够请求到数据。
经过分析发现,是因为在数据库中没有该页面的导航所导致的,为了修正这个问题,可以在“sys-menu”表中添加一行,内容位:
这样当再次访问的时候,在“平台属性”下,会出现“规格维护”菜单,
当再次点击“规格”的时候,显示出菜单
不过这种菜单并不符合我们的需要,我们需要让它以弹出框的形式出现。
修改商品规格 API: https://easydoc.xyz/doc/75716633/ZUqEdvA4/GhnJ0L85
URL:/product/attr/update/{spuId}
小结: 1. 在open fen中会将调用的数据转换为JSON,接收方接收后,将JSON转换为对象,此时调用方和被调用方的处理JSON的对象不一定都是同一个类,只要它们的字段类型吻合即可。 调用方:
1 2 3 4 5 6 7 8 9 @FeignClient(value = "gulimall-coupon") public interface CouponFenService { @PostMapping("/coupon/spubounds/save") R saveSpuBounds (@RequestBody SpuBoundTo spuBoundTo) ; @PostMapping("/coupon/skufullreduction/saveInfo") R saveSkuReduction (@RequestBody SkuReductionTo skuReductionTo) ; }
被调用方:
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping("/save") public R save (@RequestBody SpuBoundsEntity spuBounds) { spuBoundsService.save(spuBounds); return R.ok(); } @PostMapping("/saveInfo") public R saveInfo (@RequestBody SkuReductionTo skuReductionTo) { skuFullReductionService.saveSkuReduction(skuReductionTo); return R.ok(); }
调用方JSON化时的对象SpuBoundTo:
1 2 3 4 5 6 @Data public class SpuBoundTo { private Long spuId; private BigDecimal buyBounds; private BigDecimal growBounds; }
被调用方JSON数据对象化时的对象SpuBoundsEntity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Data @TableName("sms_spu_bounds") public class SpuBoundsEntity implements Serializable { private static final long serialVersionUID = 1L ; @TableId private Long id; private Long spuId; private BigDecimal growBounds; private BigDecimal buyBounds; private Integer work; }
2. 事务究竟要如何加上? 存在Batch操作的时候,才需要加上事务,单个操作无需添加事务控制。
SpringBoot中的事务
批量操作的时候,才需要事务
一个事务标注的方法上,方法内存在这些操作:
(1)批量更新一个表中字段
(2)更新多张表中的操作
实际上不论是哪种类型,方法中所有对于数据库的写操作,都会被整体当做一个事务,在这个事务过程中,如果某个操作出现了异常,则整体都不会被提交。这就是对于SpringBoot中的@Transactional的理解。
@EnableTransactionManagement和@Transactional的区别?
https://blog.csdn.net/abysscarry/article/details/80189232 https://blog.csdn.net/Driver_tu/article/details/99679145
https://www.cnblogs.com/leaveast/p/11765503.html
其他 1. 文档参考地址 http://www.jayh.club/#/02.PassJava%E6%9E%B6%E6%9E%84%E7%AF%87/01.%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E5%92%8C%E6%B7%BB%E5%8A%A0%E6%A8%A1%E5%9D%97
https://blog.csdn.net/ok_wolf/article/details/105400748
https://www.cnblogs.com/javalbb/p/12690862.html
https://blog.csdn.net/ok_wolf/article/details/105456170
https://easydoc.xyz/doc/75716633/ZUqEdvA4/jCFganpf
2. 开机启动docker 1 [vagrant@x`]$ sudo systemctl enable docker
在Docker中设置开机启动容器
1 [vagrant@x`]$ sudo update mysql --restart=always
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 查看防火墙状态 [root@hadoop-104 module]# systemctl status firewalld firewalld.service - firewalld - dynamic firewall daemon Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled) Active: active (running) since Wed 2020-04-22 21:26:23 EDT; 10min ago Docs: man:firewalld(1) Main PID: 5947 (firewalld) CGroup: /system.slice/firewalld.service └─5947 /usr/bin/python -Es /usr/sbin/firewalld --nofork --nopid Apr 22 21:26:20 hadoop-104 systemd[1]: Starting firewalld - dynamic firewall daemon... Apr 22 21:26:23 hadoop-104 systemd[1]: Started firewalld - dynamic firewall daemon. # 查看防火墙是否是开机启动 [root@hadoop-104 module]# systemctl list-unit-files|grep firewalld firewalld.service enabled # 关闭开机启动防火墙 [root@hadoop-104 module]# systemctl disable firewalld Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.service. Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service. # 停止防火墙 [root@hadoop-104 module]# systemctl stop firewalld # 再次查看防火墙 [root@hadoop-104 module]# systemctl list-unit-files|grep firewalld firewalld.service disabled [root@hadoop-104 module]#
3. 查看命令的安装位置 whereis mysql:查看mysql的安装位置
4. vscode中生成代码片段
新建一个全局的代码片段,名字为vue,然后回车:
将下面的代码片段粘贴到“vue-html.code-snippets”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 { "生成vue模板" : { "prefix" : "vue" , "body" : [ "<!-- $1 -->" , "<template>" , "<div class='$2'>$5</div>" , "</template>" , "" , "<script>" , "//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)" , "//例如:import 《组件名称》 from '《组件路径》';" , "" , "export default {" , "//import引入的组件需要注入到对象中才能使用" , "components: {}," , "data() {" , "//这里存放数据" , "return {" , "" , "};" , "}," , "//监听属性 类似于data概念" , "computed: {}," , "//监控data中的数据变化" , "watch: {}," , "//方法集合" , "methods: {" , "" , "}," , "//生命周期 - 创建完成(可以访问当前this实例)" , "created() {" , "" , "}," , "//生命周期 - 挂载完成(可以访问DOM元素)" , "mounted() {" , "" , "}," , "beforeCreate() {}, //生命周期 - 创建之前" , "beforeMount() {}, //生命周期 - 挂载之前" , "beforeUpdate() {}, //生命周期 - 更新之前" , "updated() {}, //生命周期 - 更新之后" , "beforeDestroy() {}, //生命周期 - 销毁之前" , "destroyed() {}, //生命周期 - 销毁完成" , "activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发" , "}" , "</script>" , "<style lang='scss' scoped>" , "//@import url($3); 引入公共css类" , "$4" , "</style>" ] , "description" : "生成VUE模板" } , "http-get请求" : { "prefix" : "httpget" , "body" : [ "this.\\$http({" , "url: this.\\$http.adornUrl('')," , "method: 'get'," , "params: this.\\$http.adornParams({})" , "}).then(({ data }) => {" , "})" ] , "description" : "httpGET请求" } , "http-post请求" : { "prefix" : "httppost" , "body" : [ "this.\\$http({" , "url: this.\\$http.adornUrl('')," , "method: 'post'," , "data: this.\\$http.adornData(data, false)" , "}).then(({ data }) => { });" ] , "description" : "httpPOST请求" } }
更多详细说明见: https://blog.csdn.net/z772330927/article/details/105730430/
5. vscode快捷键 ctrl+shift+f 全局搜索
alt+shift+f 格式化代码
6. 关闭eslint的语法检查
7. 安装mybatisx插件 在Marketplace中搜索“mybatisx”,安装后重启IDEA,使用时会自动在@Mapper标注的接口上,产生小图标,然后alt+enter,generate statement,就会自动的在xml文件中生成SQL。
8. mysql的批量删除 1 DELETE FROM `pms_attr_attrgroup_relation` WHERE (attr_id= ? AND attr_group_id ) OR (attr_id= ? AND attr_group_id )
9. String.join 1 2 3 java.lang.String @NotNull public static String join (@NotNull CharSequence delimiter, @NotNull Iterable<? extends CharSequence> elements)
Returns a new String composed of copies of the CharSequence elements joined together with a copy of the specified delimiter.
返回一个由CharSequence元素的副本和指定分隔符的副本组成的新字符串。
For example,
1 2 3 4 5 6 7 8 9 10 11 List<String> strings = new LinkedList <>(); strings.add("Java" );strings.add("is" ); strings.add("cool" ); String message = String.join(" " , strings);Set<String> strings = new LinkedHashSet <>(); strings.add("Java" ); strings.add("is" ); strings.add("very" ); strings.add("cool" ); String message = String.join("-" , strings);
Note that if an individual element is null, then “null” is added.
注意,如果单个元素为null,则添加“null”。
Params: delimiter – a sequence of characters that is used to separate each of the elements in the resulting String 用于分隔结果字符串中的每个元素的字符序列
elements – an Iterable that will have its elements joined together. 将其元素连接在一起的可迭代的。
Returns: a new String that is composed from the elements argument 由elements参数组成的新字符串
Throws: NullPointerException – If delimiter or elements is null
1 2 3 4 5 6 7 8 9 10 public static String join (CharSequence delimiter, Iterable<? extends CharSequence> elements) { Objects.requireNonNull(delimiter); Objects.requireNonNull(elements); StringJoiner joiner = new StringJoiner (delimiter); for (CharSequence cs: elements) { joiner.add(cs); } return joiner.toString(); }
能够看到实际上它就是通过创建StringJoiner,然后遍历elements,加入每个元素来完成的。
StringJoiner
1 2 java.util public final class StringJoiner extends Object
StringJoiner is used to construct a sequence of characters separated by a delimiter and optionally starting with a supplied prefix and ending with a supplied suffix. tringJoiner用于构造由分隔符分隔的字符序列,可以选择以提供的前缀开始,以提供的后缀结束。
Prior to adding something to the StringJoiner, its sj.toString() method will, by default, return prefix + suffix. However, if the setEmptyValue method is called, the emptyValue supplied will be returned instead. This can be used, for example, when creating a string using set notation to indicate an empty set, i.e. “{}”, where the prefix is “{“, the suffix is “}” and nothing has been added to the StringJoiner. 在向StringJoiner添加内容之前,它的sj.toString()方法在默认情况下会返回前缀+后缀。但是,如果调用setEmptyValue方法,则返回所提供的emptyValue。例如,当使用set符号创建一个字符串来表示一个空集时,可以使用这种方法。“{}”,其中前缀是“{”,后缀是“}”,没有向StringJoiner添加任何内容。
apiNote: The String “[George:Sally:Fred]” may be constructed as follows:
1 2 3 StringJoiner sj = new StringJoiner (":" , "[" , "]" );sj.add("George" ).add("Sally" ).add("Fred" ); String desiredString = sj.toString();
A StringJoiner may be employed to create formatted output from a java.util.stream.Stream using java.util.stream.Collectors.joining(CharSequence). For example: 使用StringJoiner从java.util.stream创建格式化输出流,使用java.util.stream.Collectors.joining (CharSequence进行)。例如:
1 2 3 4 List<Integer> numbers = Arrays.asList(1 , 2 , 3 , 4 ); String commaSeparatedNumbers = numbers.stream() .map(i -> i.toString()) .collect(Collectors.joining(", " ));
通过分析源码发现,在“”内部维护了一个StringBuilder,所有加入到它内部的元素都会先拼接上分割符,然后再拼接上加入的元素
1 2 3 4 public StringJoiner add (CharSequence newElement) { prepareBuilder().append(newElement); return this ; }
1 2 3 4 5 6 7 8 private StringBuilder prepareBuilder () { if (value != null ) { value.append(delimiter); } else { value = new StringBuilder ().append(prefix); } return value; }
10. 在Service中微服务比较多的时候,可以配置将一些微服务放置到compound中,组成一个小组
以后再运行时,直接选择这个compound即可很方便的运行或停止一组微服务:
另外可以单独为每个微服务,设置需要的运行时最大堆内存大小:
11. mysql的dateTime和timestamp的区别? MySQL中datetime和timestamp的区别及使用
TIMESTAMP和DATETIME的相同点:
1> 两者都可用来表示YYYY-MM-DD HH:MM:SS[.fraction]类型的日期。
TIMESTAMP和DATETIME的不同点:
1> 两者的存储方式不一样
对于TIMESTAMP,它把客户端插入的时间从当前时区转化为UTC(世界标准时间)进行存储。查询时,将其又转化为客户端当前时区进行返回。
而对于DATETIME,不做任何改变,基本上是原样输入和输出。
2> 两者所能存储的时间范围不一样
timestamp所能存储的时间范围为:’1970-01-01 00:00:01.000000’ 到 ‘2038-01-19 03:14:07.999999’。
datetime所能存储的时间范围为:’1000-01-01 00:00:00.000000’ 到 ‘9999-12-31 23:59:59.999999’。
总结:TIMESTAMP和DATETIME除了存储范围和存储方式不一样,没有太大区别。当然,对于跨时区的业务,TIMESTAMP更为合适。
https://www.cnblogs.com/Jashinck/p/10472398.html
12. SpringBoot中的事务 https://blog.csdn.net/Z__Sheng/article/details/89489053
13. IDEA RESTFUll clinet IntelliJ IDEA 使用 rest client