文章目录
1. 项目概述
1.1 项目架构图
1、谷粒商场微服务架构图
2、微服务划分图
1.2 分布式基础概念
- 微服务
拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。
- 集群&分布式&节点
集群是个物理形态,分布式是个工作方式。 只要是一堆机器,就可以叫集群,他们是不是一起协作着干活,这个谁也不知道。
例如:京东是一个分布式系统,众多业务运行在不同的机器,所有业务构成一个大型的业 务集群。每一个小的业务,比如用户系统,访问压力大的时候一台服务器是不够的。我们就 应该将用户系统部署到多个服务器,也就是每一个业务系统也可以做集群化;
节点:集群中的一个服务器
- 远程调用
在分布式系统中,各个服务可能处于不同主机,但是服务之间不可避免的需要互相调用,我 们称为远程调用。 SpringCloud 中使用HTTP+JSON 的方式完成远程调用
- 负载均衡
分布式系统中,A 服务需要调用 B 服务,B 服务在多台机器中都存在,A 调用任意一个 服务器均可完成功能。 为了使每一个服务器都不要太忙或者太闲,我们可以负载均衡的调用每一个服务器,提升网站的健壮性。
- 常见的负载均衡算法:
轮询:为第一个请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择,直 到最后一个,然后循环。
最小连接:优先选择连接数最少,也就是压力最小的后端服务器,在会话较长的情况下 可以考虑采取这种方式。
散列:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程 度上保证特定用户能连接到相同的服务器。如果你的应用需要处理状态而要求用户能连接到 和之前相同的服务器,可以考虑采取这种方式。
- 服务注册/发现&注册中心
A 服务调用 B 服务,A 服务并不知道 B 服务当前在哪几台服务器有,哪些正常的,哪些服务 已经下线。解决这个问题可以引入注册中心;
如果某些服务下线,我们其他人可以实时的感知到其他服务的状态,从而避免调用不可用的服务。
- 配置中心
每一个服务最终都有大量的配置,并且每个服务都可能部署在多台机器上。我们经常需要变 更配置,我们可以让每个服务在配置中心获取自己的配置。
- 服务熔断&服务降级
在微服务架构中,微服务之间通过网络进行通信,存在相互依赖,当其中一个服务不可用时, 有可能会造成雪崩效应。要防止这样的情况,必须要有容错机制来保护服务。
1)服务熔断
a. 设置服务的超时,当被调用的服务经常失败到达某个阈值,我们可以开 启断路保护机制,后来的请求不再去调用这个服务。本地直接返回默认 的数据
2)服务降级
a. 在运维期间,当系统处于高峰期,系统资源紧张,我们可以让非核心业 务降级运行。降级:某些服务不处理,或者简单处理【抛异常、返回 NULL、 调用 Mock 数据、调用 Fallback 处理逻辑】。
- API网关
在微服务架构中,API Gateway 作为整体架构的重要组件,它抽象了微服务中都需要的公共 功能,同时提供了客户端负载均衡,服务自动熔断,灰度发布,统一认证,限流流控,日 志统计等丰富的功能,帮助我们解决很多 API 管理难题。
2. linux环境搭建
2.1 搭建linux虚拟机
1、步骤
- 下载vitualbox
https://www.virtualbox.org/wiki/Downloads
- 下载vagrant
https://www.vagrantup.com/
- 普通安装linux虚拟机太麻烦,可以利用vagrant可以帮助我们快速地创建一个虚拟机。主要装了vitualbox,vagrant可以帮助我们快速创建出一个虚拟机。他有一个镜像仓库。
- 在主机的cmd操作:
# 1、初始化一个centos7系统
vagrant init centos/7
# 2、启动虚拟机环境
vagrant up
# 3、连上虚拟机
vagrant ssh
- 登录成功!
2、虚拟机环境配置
- 修改vagrantfile的内容
-
打开cmd,输入
ipconfig
查询ip地址 -
不难看出,主机的ip地址为192.168.56.1
- 因此,在vagrantfile中也要设置网络为同一个安全组,及192.168.56.xx(本处为101,图示的端口号不要看)
- 查看虚拟机和主机的ip,并且尝试相互之间ping通
ip addr
命令查看虚拟机ip信息
- ipconfig命令查看主机ip信息
- 虚拟机ping主机
- 主机ping虚拟机
- 其他:本地wlan地址

2.2 虚拟机安装docker
https://docs.docker.com/engine/install/centos/
1、安装
# 1、卸载系统之前的docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
# 2、下载yum-utils
sudo yum install -y yum-utils
# 3、配置镜像
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
# 4、下载镜像
sudo yum install docker-ce docker-ce-cli containerd.io
# 5、启动docker
sudo systemctl start docker
# 6、查看版本和镜像信息
docker -v
sudo docker images
# 7、设置开机自启动
sudo systemctl enable docker
2、阿里云镜像加速
https://cr.console.aliyun.com/cn-qingdao/instances/mirrors
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://hqj3cnew.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
2.3 安装mysql-docker
1、步骤
- 用docker安装上mysql,去docker仓库里搜索mysql
sudo docker pull mysql:5.7
# --name指定容器名字 -v目录挂载 -p指定端口映射 -e设置mysql参数 -d后台运行
sudo 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=123456 \
-d mysql:5.7
- 测试虚拟机是否可以使用数据库
su root
密码为vagrant,这样就可以不写sudo了
- 对数据库进行挂载设置
# 1、展示所有容器
sudo docker ps
# 2、进入已启动的容器
sudo docker exec -it mysql bin/bash
# 3、退出进入的容器
exit;
# 4、因为有目录映射,所以我们可以直接在镜像外执行
vi /mydata/mysql/conf/my.conf
[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
# 5、重新启动mysql
docker restart mysql
# 6、启动docker后自动启动mysql容器
sudo docker update mysql --restart=always
2.4 安装redis-docker
1、步骤
# 先su root获得权限
su root
密码:vagrant
# 1、在虚拟机中创建一个文件
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
# 2、下载redis
docker pull redis
# 3、挂载文件到虚拟机中
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
# 4、直接进去redis客户端。
docker exec -it redis redis-cli
# 1、修改配置文件
vi redis.conf
# 插入下面内容
appendonly yes
# 2、重启redis
docker restart redis
# 3、设置redis容器在docker启动的时候启动
docker update redis --restart=always
2、下载redis-desktop-manager
- 连接虚拟机ip
- 可视化存储信息
3. 配置开发环境
3.1 maven和vsCode设置
1、maven配置
- 在settings中配置阿里云镜像(上网查)
<!--1、配置maven仓库地址-->
<localRepository>D:\Java_Tools\maven\apache-maven-3.6.1\maven-repo</localRepository>
<!--2、配置阿里云仓库镜像-->
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
- IDEA安装插件lombok,mybatisX
- 设置里配置好maven
2、vsCode设置
下载vsCode用于前端管理系统。在vsCode里安装插件。
- Auto Close Tag
- Auto Rename Tag
- Chinese
- ESlint
- HTML CSS Support
- HTML Snippets
- JavaScript ES6
- Live Server
- open in brower
- Vetur
3.2 git相关配置
1、安装git
下载git客户端,右键桌面Git GUI/bash Here。去bash,
# 配置用户名
git config --global user.name "username" //(名字,随意写)
# 配置邮箱
git config --global user.email "xxx@qq.com" // 注册账号时使用的邮箱
# 配置ssh免密登录
ssh-keygen -t rsa -C "xxx@qq.com"
#三次回车后生成了密钥:公钥私钥
cat ~/.ssh/id_rsa.pub
# 浏览器登录码云后,个人头像上点设置--ssh公钥---随便填个标题---复制(即可!!!)
# 测试
ssh -T git@gitee.com
# 测试成功,就可以无密给码云推送仓库了
2、配置阿里云镜像环境
- 新建一个gitee仓库
- idea中将gitee项目拉取下来
- 创建项目框架
- 在项目中配置子项目Spring Initializer
- 导入基本包(基本配置)
依次创建出以下服务模块
- 商品服务product
- 存储服务ware
- 订单服务order
- 优惠券服务coupon
- 用户服务member
- 创建总项目的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall</name>
<description>聚合服务</description>
<packaging>pom</packaging>
<modules>
<module>gulimall-coupon</module>
<module>gulimall-member</module>
<module>gulimall-order</module>
<module>gulimall-product</module>
<module>gulimall-ware</module>
</modules>
</project>
- 修改总项目的
.gitignore
(git提交时忽略垃圾文件)
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore
- 将项目提交到gitee
- 在version control/local Changes中Add to VCS提交项目
-
在IDEA中安装插件:gitee,重启IDEA
-
看上述图,点击Commit Files,去掉右面的勾选Perform code analysis、CHECK TODO,然后点击COMMIT,有个下拉列表,点击commit and push才会提交到云端。(commit只是保存更新到本地/push才是提交到gitee)
3.3 数据库
1、步骤
- 下载sql文件
https://gitee.com/HanFerm/gulimall/tree/master/sql文件
- 连接数据库
- 导入数据库文件
- 新建查询,生成下面几个数据库
gulimall-oms
gulimall-pms
gulimall-sms
gulimall-ums
gulimall-wms
- 问题处理
gulimall_admin里面的schedule_job表要清空,不然启动renrenfast模块会报获取定时任务CronTrigger出现异常。
如果出现了该问题的话:
- 先看数据库名和IDEA中的是否匹配
- 否则,加上类似下面的语句
CREATE DATABASE /*!32312 IF NOT EXISTS*/`guli_pms` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
-- 根据需要改数据库名即可,表名无需改动
USE `gulimall_pms`;
-- 也集合P71的内容进行了修改
4. 人人项目
在码云上搜索人人开源,我们使用renren-fast(后端)、renren-fast-vue(前端)项目。
https://gitee.com/renrenio
- 使用git下载
git clone https://gitee.com/renrenio/renren-fast.git
git clone https://gitee.com/renrenio/renren-fast-vue.git
4.1 设置renren-fast
1、步骤
- 移动项目到IDEA
- 我们把renren-fast移动到我们的项目文件夹(删掉.git文件)
- 在IDEA项目里的pom.xml添加一个renrnen-fast
<modules>
<module>gulimall-coupon</module>
<module>gulimall-member</module>
<module>gulimall-order</module>
<module>gulimall-product</module>
<module>gulimall-ware</module>
<module>renren-fast</module>
</modules>
- 创建数据库
- 然后打开
renren-fast/db/mysql.sql
,在navicat中打开运行
- 修改项目里renren-fast中的application-dev.yml
-
先启动虚拟机
-
url设置为虚拟机的ip
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.56.101:3306/gulimall_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
- 运行RenrenApplication(端口8080)
- 浏览器输入http://localhost:8080/renren-fast/得到{“msg”:“invalid token”,“code”:401}就代表无误
4.2 设置renren-fast-vue
1、步骤
- 安装node:http://nodejs.cn/download/
- 主机cmd查看版本并安装淘宝镜像
node -v
npm config set registry http://registry.npm.taobao.org/
- 在VScode的项目终端中输入
npm install
npm run dev
- 登录账号密码:admin/admin
4.3 设置逆向工程
1、逆向工程搭建步骤
- 下载文件
git clone https://gitee.com/renrenio/renren-generator.git
下载到桌面后,同样把里面的.git文件删除,然后移动到我们IDEA项目目录中。
- 配置好pom.xml
<modules>
<module>gulimall-coupon</module>
<module>gulimall-member</module>
<module>gulimall-order</module>
<module>gulimall-product</module>
<module>gulimall-ware</module>
<module>renren-fast</module>
<module>renren-generator</module>
</modules>
- 修改application.yml
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.56.101:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
- 修改generator.properties
mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
moduleName=product
#作者
author=cocochimp
#Email
email=cocochimp@gmail.com
#表前缀(类名不会包含表前缀)
tablePrefix=pms_
- 运行RenrenApplication(端口80)
- 启动不成功,修改application中设置port为801
-
在网页上下方点击每页显示50个(pms库中的表),以让全部都显示,然后点击全部,点击生成代码。下载了压缩包
-
解压压缩包,把main放到gulimall-product的同级目录下
-
删除gulimall-product的src包
4.4 设置gulimail-common
1、步骤
- 创建gulimail-common子模块
在项目上右击:new modules— maven—然后在name上输入gulimall-common。
在pom.xml中也自动添加了<module>gulimall-common</module>
- 在common项目的pom.xml中添加
<description>每一个微服务公共的依赖,bean,工具类等</description>
<!-- mybatisPLUS-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--简化实体类,用@Data代替getset方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!-- httpcomponent包。发送http请求 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.12</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>io.renren</groupId>
<artifactId>renren-fast</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>
- 设置gulimall-product的pom.xml
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 将renren-fast中的common配置一部分复制到gulimall-common
- renren-fast目录下的文件
- 复制到gulimall-common目录
- 将爆红的文件都进行导包处理!
- 注释renren-generator中的一些信息
- Controller.java.vm
- 下面类似的都注释!
- 注释头文件!
- 重新运行RenrenApplication(端口80)
- 重复上述过程!
- 将把controller放到gulimall-product的同级目录下!
2、整合MyBatis-Plus
- 导入依赖
<!-- 数据库驱动
https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<!--tomcat里一般都带-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
- 配置数据源
-
导入数据库的驱动
-
gulimall-product项目新建application.yml文件,配置数据源相关信息
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.56.101:3306/gulimall_pms?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
# MapperScan
# sql映射文件位置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
- 配置MyBatis-Plus
- GulimallProductApplication启动类使用@MapperScan
- 告诉MyBatis-Plus,sql映射文件位置
@MapperScan("com.atguigu.gulimall.product.dao")
- 补充:修改gulimall-common的文件
3、测试
- 在gulimall-product的测试类中进行测试
- 测试添加
@SpringBootTest
class gulimallProductApplicationTests {
@Autowired
BrandService brandService;
@Test
public void contextLoads() {
BrandEntity brandEntity = new BrandEntity();
brandEntity.setName("华为");
brandService.save(brandEntity);
System.out.println("保存成功");
}
}
- 测试修改
@SpringBootTest
class gulimallProductApplicationTests {
@Autowired
BrandService brandService;
@Test
public void contextLoads() {
BrandEntity brandEntity = new BrandEntity();
brandEntity.setBrandId(1L);
brandEntity.setDescript("修改");
brandService.updateById(brandEntity);
}
}
- 测试查询
@Autowired
BrandService brandService;
@Test
public void contextLoads() {
List<BrandEntity> list = brandService.list(new QueryWrapper<BrandEntity>().eq("brand_id", 1L));
list.forEach((item)->{
System.out.println(item);
});
}
4.5 其他项目的逆向工程
4.5.1 coupon
1、优惠券服务:步骤
- 重新打开generator逆向工程,修改generator.properties
mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
moduleName=coupon
#作者
author=cocochimp
#Email
email=cocochimp@gmail.com
#表前缀(类名不会包含表前缀)
tablePrefix=sms_
- 修改yml数据库信息
url: jdbc:mysql://192.168.56.101:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
-
启动renren-generator(80端口)项目,重复下载过程(main)
-
依赖于common,修改pom.xml
<dependency>
<groupId>com.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 删除resources下的src包
- 添加application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.56.101:3306/gulimall_sms?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
# MapperScan
# sql映射文件位置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
server:
port: 7000
- 运行GulimallCouponApplication.java
http://localhost:7000/coupon/coupon/list
- 测试成功!
4.5.2 member
1、优惠券服务:步骤
- 重新打开generator逆向工程,修改generator.properties
mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
moduleName=member
#作者
author=cocochimp
#Email
email=cocochimp@gmail.com
#表前缀(类名不会包含表前缀)
tablePrefix=ums_
- 修改yml数据库信息
url: jdbc:mysql://192.168.56.101:3306/gulimall_ums?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
-
启动renren-generator(80端口)项目,重复下载过程(main)
-
依赖于common,修改pom.xml
<dependency>
<groupId>com.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 删除resources下的src包
- 添加application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.56.101:3306/gulimall_ums?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
# MapperScan
# sql映射文件位置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
server:
port: 8000
- 运行GulimallMemberApplication.java
http://localhost:8000/member/growthchangehistory/list
- 测试成功!
4.5.3 order
1、优惠券服务:步骤
- 重新打开generator逆向工程,修改generator.properties
#主目录
mainPath=com
#包名
package=com.gulimall
#模块名
moduleName=order
#作者
author=cocochimp
#Email
email=2427886409@qq.com
#表前缀(类名不会包含表前缀)
tablePrefix=oms_
- 修改yml数据库信息
url: jdbc:mysql://192.168.56.101:3306/gulimall_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
-
启动renren-generator(80端口)项目,重复下载过程(main)
-
依赖于common,修改pom.xml
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 删除resources下的src包
- 添加application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.56.101:3306/gulimall_oms?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
# MapperScan
# sql映射文件位置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
server:
port: 9000
- 运行GulimallOrderApplication.java
http://localhost:9000/order/order/list
- 测试成功!
4.5.4 ware
1、步骤
- 重新打开generator逆向工程,修改generator.properties
mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
moduleName=ware
#作者
author=cocochimp
#Email
email=cocochimp@gmail.com
#表前缀(类名不会包含表前缀)
tablePrefix=wms_
- 修改yml数据库信息
url: jdbc:mysql://192.168.56.101:3306/gulimall_wms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
-
启动renren-generator(80端口)项目,重复下载过程(main)
-
依赖于common,修改pom.xml
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 删除resources下的src包
- 添加application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.56.101:3306/gulimall_wms?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
# MapperScan
# sql映射文件位置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
server:
port: 11000
- 运行GulimallWareApplication.java
http://localhost:11000/ware/wareinfo/list
- 测试成功!
2、问题解决
- 找不到或无法加载主类的:
关了工程,把工程里的.idea删了,再打开就好了!
- longblob错误:
改成byte[]
5. 分布式组件
1、本项目开发服务概要图
2、什么是微服务架构:
SpringCloud 是微服务一站式服务解决方案,微服务全家桶。它是微服务开发的主流技术栈。它采用了名称,而非数字版本号。
SpringCloud 和 springCloud Alibaba 目前是最主流的微服务框架组合。
3、结合上面两者的特点,得出本项目的最终技术搭配方案:
技术栈 | 作用 |
---|---|
SpringCloud Alibaba - Nacos | 注册中心(服务发现/注册) |
SpringCloud Alibaba - Nacos | 配置中心(动态配置管理) |
Springcloud - Ribbon | 负载均衡 |
Springcloud - Feign | 声明式HTTP客户端(调用远程服务) |
SpringCloud Alibaba - Sentinel | 服务容错(限流、降级、熔断) |
Springcloud - Gateway | API网关(webflux编程模式) |
Springcloud - Sleuth | 调用链监控 |
SpringCloud Alibaba - Seata | 源Fescar,即分布式事务解决方案 |
5.1 nacos作服务注册中心
1、项目配置环境
- 注册中心:nacos
- 配置中心:nacos
- 网关:gateway
- 远程调用:netflix把feign闭源了,spring cloud开了个openFeign
- 在common的pom.xml中加入
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 在common的pom.xml中加入(声明nacos)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在某个微服务项目(coupon)的yml配置文件加入
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-coupon
- 在该微服务项目的启动类中:使用
@EnableDiscoveryClient
注解开启服务注册与发现功能
@EnableDiscoveryClient
- 将所有的微服务项目都配置好nacos!
2、下载Nacos
- 解压后打开bin下的startup.cmd
- 启动nacos可视化界面:http://127.0.0.1:8848/nacos
账号密码都是:nacos
- 启动gulimall-coupon和gulimall-member, 查看服务注册中心:
5.2 openfegin远程调用
1、声明式远程调用
feign是一个声明式的HTTP客户端,他的目的就是让远程调用更加简单。给远程服务发的是HTTP请求。
会员服务想要远程调用优惠券服务,只需要给会员服务里引入openfeign依赖,他就有了远程调用其他服务的能力。
- 导入依赖(已完成)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 编写一个接口,告诉SpringCloud这个接口需要调用远程服务
- 在gulimall-coupon中的CouponController中添加测试方法
@RequestMapping("/member/list")
public R membercoupons(){ //全系统的所有返回都返回R
// 模拟去数据库查用户对于的优惠券
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("满100-10");//优惠券的名字
return R.ok().put("coupons",Arrays.asList(couponEntity));
}
- 在
com.atguigu.gulimall.member.feign
中新建接口CouponFeignService
@FeignClient("gulimall-coupon")//告诉spring cloud这个接口是一个远程客户端,要调用coupon服务(nacos中找到)
public interface CouponFeignService {
// 远程服务的url
@RequestMapping("/coupon/coupon/member/list")//注意写全优惠券类上还有映射
R membercoupons();//得到一个R对象
}
- 在member的主启动类上加注解
@EnableDiscoveryClient,
告诉member是一个远程调用客户端,member要调用东西的
@EnableFeignClients(basePackages="com.atguigu.gulimall.member.feign")//扫描接口方法注解
- 在member的MemberController写一个测试
@Autowired
private CouponFeignService couponFeignService; //注入刚才的CouponFeignService接口
@RequestMapping("/coupons")
public R coupons(){
MemberEntity memberEntity = new MemberEntity();
memberEntity.setNickname("会员昵称张三");
R membercoupons = couponFeignService.membercoupons();
return Objects.requireNonNull(R.ok().put("member", memberEntity)).put("coupons", membercoupons.get("coupons"));
}
- 启动gulimall-member和gulimall-coupon项目
访问http://localhost:8000/member/member/coupons
测试成功!!!
5.3 nacos作配置中心
1、步骤
- common中添加依赖 nacos 配置中心
<!--配置中心做配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 在coupon项目中创建/src/main/resources/bootstrap.yml,
- 优先级别application.properties高
# 改名字,对应nacos里的配置文件名
spring:
application:
name: gulimall-coupon
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml # 指定配置文件为yaml格式
- 浏览器去nacos里的配置列表,点击+号,data ID:
gulimall-coupon.yaml
,配置:
- 在controller包的CouponController类中编写测试代码
@Value("${coupon.user.name}")
private String name;
@Value("${coupon.user.age}")
private int age;
@RequestMapping("/test")
public R nacos(){
return R.ok().put("name", name).put("age", age);
}
- 导入头文件注释,用于支持动态刷新
@RefreshScope
2、测试
- 启动coupon微服务器,访问http://localhost:7000/coupon/coupon/test
- 更改nacos配置中心的内容,并发布
- 刷新微服务器的页面
数据变化,测试成功!!!将其他微服务都配置上nacos作配置中心!
3、nacos概念(以coupon服务为例)
- 命名空间:配置隔离
默认:public(保留空间):新增的配置默认在public空间
- 开发,测试,生产:利用命名空间来做环境隔离
注意:在bootstrap.properties配置上,需要哪个命名空间下的配置:namespace:6ca5d2cf-87fd-481a-8df6-8520456a498d
- 每个微服务之间相互隔离配置,每个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置
# 改名字,对应nacos里的配置文件名
spring:
application:
name: gulimall-coupon
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml # 指定配置文件为yaml格式
namespace: 6ca5d2cf-87fd-481a-8df6-8520456a498d
group: prod
- 配置集:所有的配置的集合
- 配置集ID:类似文件名
- Data ID:gulimall-coupon.yml
- 配置分组:
- 默认所有的配置集都属于:DEFAULT_GROUP
- 每个微服务都创建自己的命名空间,使用配置分组区分环境:dev/test/prod
5.4 网关gateway-88
1、三大核心概念
- Route(路由): 发一个请求给网关,网关要将请求路由到指定的服务。路由有id,目的地uri,断言的集合,匹配了断言就能到达指定位置,
- Predicate(断言):就是java里的断言函数,匹配请求里的任何信息,包括请求头等。根据请求头路由哪个服务
- Filter(过滤器):过滤器请求和响应都可以被修改。客户端发请求给服务端。中间有网关。先交给映射器,如果能处理就交给handler处理,然后交给一系列filer,然后给指定的服务,再返回回来给客户端。

客户端发请求给服务端。中间有网关。先交给映射器,如果能处理就交给handler处理,然后交给一系列filer,然后给指定的服务,再返回回来给客户端。
2、实现步骤
- 新建gulimall-gateway作为网关
- 创建项目
- 导入依赖(不依赖到common服务)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 在nacos上新建gateway命名空间,在命名空间中新建配置
gulimall-gateway.yml
- 设置配置文件
- 配置application.properties
# 应用名称
spring.application.name=gulimall-gateway
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=88
- 配置bootstrap.yml
spring:
application:
name: gulimall-gateway
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
namespace: 225a3c36-6e5a-4898-91e6-e599988bcb61
- 配置application.yml(配置路由)
spring:
cloud:
gateway:
routes:
- id: baidu_route # 每一个路由的名字,唯一即可
uri: https://www.baidu.com # 匹配后提供服务的路由地址
predicates: # 断言规则
- Query=url,baidu #如果url参数等于baidu 符合断言,转到uri
- id: qq_route # 每一个路由的名字,唯一即可
uri: https://www.qq.com # 匹配后提供服务的路由地址
predicates: # 断言规则
- Query=url,qq #如果url参数等于baidu 符合断言,转到uri
- 主启动类
@EnableDiscoveryClient
@SpringBootApplication(exclude={DruidDataSourceAutoConfigure.class,DataSourceAutoConfiguration.class}) //不用数据源,过滤掉数据源配置
3、测试
启动网关,访问http://localhost:88?url=baidu测试,成功!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xS3gTWtD-1663203102812)(C:\Users\koko\AppData\Roaming\Typora\typora-user-images\image-20220727211221195.png)]
6. 前端基础
6.1 ES6基础
1. let & const
快捷键:!+ Enter
生成模板
- let声明后不能作用于{}外,var可以
- let只能声明一次,var可以声明多次
- var会变量提升(使用在定义之前),let必须先定义再使用
- const一旦初始化后,不能改变
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// var 声明的变量往往会越域
// let 声明的变量有严格局部作用域
{
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
// var 可以声明多次
// let 只能声明一次
var m = 1
var m = 2
let n = 3
// let n = 4
console.log(m) // 2
console.log(n) // Identifier 'n' has already been declared
// var 会变量提升
// let 不存在变量提升
console.log(x); // undefined
var x = 10;
console.log(y); //ReferenceError: y is not defined
let y = 20;
// let
// 1. const声明之后不允许改变
// 2. 一但声明必须初始化,否则会报错
const a = 1;
a = 3; //Uncaught TypeError: Assignment to constant variable.
</script>
</body>
</html>
2. 解构表达式
- 数组解构
let arr = [1,2,3];
let [a,b,c] = arr
- 对象解构
const{name:abc, age, language} = person
其中name:abc
代表把name改名为abc - 字符串函数
str.startsWith();str.endsWith();str.includes();str.includes()
- 字符串模板,``支持一个字符串定义为多行
- 占位符功能 ${}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//数组解构
let arr = [1,2,3];
// // let a = arr[0];
// // let b = arr[1];
// // let c = arr[2];
let [a,b,c] = arr;
console.log(a,b,c)
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
// const name = person.name;
// const age = person.age;
// const language = person.language;
//对象解构 // 把name属性变为abc,声明了abc、age、language三个变量
const { name: abc, age, language } = person;
console.log(abc, age, language)
//4、字符串扩展
let str = "hello.vue";
console.log(str.startsWith("hello"));//true
console.log(str.endsWith(".vue"));//true
console.log(str.includes("e"));//true
console.log(str.includes("hello"));//true
//字符串模板 ``可以定义多行字符串
let ss = `<div>
<span>hello world<span>
</div>`;
console.log(ss);
function fun() {
return "这是一个函数"
}
// 2、字符串插入变量和表达式。变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。
let info = `我是${abc},今年${age + 10}了, 我想说: ${fun()}`;
console.log(info);
</script>
</body>
</html>
3. 函数优化
- 支持函数形参默认值
function add(a, b = 1){}
- 支持不定参数
function fun(...values){}
- 支持箭头函数
var print = obj => console.log(obj);
- 支持箭头函数+解构函数
var hello2 = ({name}) => console.log("hello," +name); hello2(person);
<!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>
<script>
//在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通写法:
function add(a, b) {
// 判断b是否为空,为空就给默认值1
b = b || 1;
return a + b;
}
// 传一个参数
console.log(add(10));
//现在可以这么写:直接给参数写上默认值,没传就会自动使用默认值
function add2(a, b = 1) {
return a + b;
}
console.log(add2(20));
//2)、不定参数
function fun(...values) {
console.log(values.length)
}
fun(1, 2) //2
fun(1, 2, 3, 4) //4
//3)、箭头函数。lambda
//以前声明一个方法
// var print = function (obj) {
// console.log(obj);
// }
var print = obj => console.log(obj);
print("hello");
var sum = function (a, b) {
c = a + b;
return a + c;
}
var sum2 = (a, b) => a + b;
console.log(sum2(11, 12));
var sum3 = (a, b) => {
c = a + b;
return a + c;
}
console.log(sum3(10, 20))
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
function hello(person) {
console.log("hello," + person.name)
}
//箭头函数+解构
var hello2 = ({name}) => console.log("hello," +name);
hello2(person);
</script>
</body>
</html>
4. 对象优化
- 可以获取map的键值对
Object.keys()
、Object.values
、Object.entries
Object.assgn(target,source1,source2)
合并source1,source2到target- 支持对象名声明简写:如果属性名和属性值的变量名相同可以省略
let someone = {...person}
取出person对象所有的属性拷贝到当前对象
<!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>
<script>
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
console.log(Object.keys(person));//["name", "age", "language"]
console.log(Object.values(person));//["jack", 21, Array(3)]
console.log(Object.entries(person));//[Array(2), Array(2), Array(2)]
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
// 合并
//{a:1,b:2,c:3}
Object.assign(target, source1, source2);
console.log(target);//["name", "age", "language"]
//2)、声明对象简写
const age = 23
const name = "张三"
const person1 = { age: age, name: name }
// 等价于
const person2 = { age, name }//声明对象简写
console.log(person2);
//3)、对象的函数属性简写
let person3 = {
name: "jack",
// 以前:
eat: function (food) {
console.log(this.name + "在吃" + food);
},
//箭头函数this不能使用,要使用的话需要使用:对象.属性
eat2: food => console.log(person3.name + "在吃" + food),
eat3(food) {
console.log(this.name + "在吃" + food);
}
}
person3.eat("香蕉");
person3.eat2("苹果")
person3.eat3("橘子");
//4)、对象拓展运算符
// 1、拷贝对象(深拷贝)
let p1 = { name: "Amy", age: 15 }
let someone = { ...p1 }
console.log(someone) //{name: "Amy", age: 15}
// 2、合并对象
let age1 = { age: 15 }
let name1 = { name: "Amy" }
let p2 = { name: "zhangsan" }
p2 = { ...age1, ...name1 }
console.log(p2)
</script>
</body>
</html>
5. map和reduce
arr.map()
接收一个函数,将arr中的所有元素用接收到的函数处理后放入新的数组arr.reduce()
为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
<!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>
<!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>
<script>
//数组中新增了map和reduce方法。
//map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
let arr = ['1', '20', '-5', '3'];
// arr = arr.map((item)=>{
// return item*2
// });
arr = arr.map(item => item * 2);
console.log(arr);
//reduce() 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
//[2, 40, -10, 6]
//arr.reduce(callback,[initialValue])
/**
1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue (数组中当前被处理的元素)
3、index (当前元素在数组中的索引)
4、array (调用 reduce 的数组)*/
let result = arr.reduce((a, b) => {
console.log("上一次处理后:" + a);
console.log("当前正在处理:" + b);
return a + b;
}, 100);
console.log(result)
</script>
</body>
</html>
<script>
//数组中新增了map和reduce方法。
//map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
let arr = ['1', '20', '-5', '3'];
// arr = arr.map((item)=>{
// return item*2
// });
arr = arr.map(item => item * 2);
console.log(arr);
//reduce() 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
//[2, 40, -10, 6]
//arr.reduce(callback,[initialValue])
/**
1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue (数组中当前被处理的元素)
3、index (当前元素在数组中的索引)
4、array (调用 reduce 的数组)*/
let result = arr.reduce((a, b) => {
console.log("上一次处理后:" + a);
console.log("当前正在处理:" + b);
return a + b;
}, 100);
console.log(result)
</script>
</body>
</html>
6. promise
- 优化异步操作。封装ajax
- 把Ajax封装到Promise中,赋值给let p
- 在Ajax中成功使用resolve(data),失败使用reject(err)
- p.then().catch()
<!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>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<script>
//1、查出当前用户信息
//2、按照当前用户的id查出他的课程
//3、按照当前课程id查出分数
// $.ajax({
// url: "mock/user.json",
// success(data) {
// console.log("查询用户:", data);
// $.ajax({
// url: `mock/user_corse_${data.id}.json`,
// success(data) {
// console.log("查询到课程:", data);
// $.ajax({
// url: `mock/corse_score_${data.id}.json`,
// success(data) {
// console.log("查询到分数:", data);
// },
// error(error) {
// console.log("出现异常了:" + error);
// }
// });
// },
// error(error) {
// console.log("出现异常了:" + error);
// }
// });
// },
// error(error) {
// console.log("出现异常了:" + error);
// }
// });
//1、Promise可以封装异步操作
// let p = new Promise((resolve, reject) => {
// //1、异步操作
// $.ajax({
// url: "mock/user.json",
// success: function (data) {
// console.log("查询用户成功:", data)
// resolve(data);
// },
// error: function (err) {
// reject(err);
// }
// });
// });
// p.then((obj) => {
// return new Promise((resolve, reject) => {
// $.ajax({
// url: `mock/user_corse_${obj.id}.json`,
// success: function (data) {
// console.log("查询用户课程成功:", data)
// resolve(data);
// },
// error: function (err) {
// reject(err)
// }
// });
// })
// }).then((data) => {
// console.log("上一步的结果", data)
// $.ajax({
// url: `mock/corse_score_${data.id}.json`,
// success: function (data) {
// console.log("查询课程得分成功:", data)
// },
// error: function (err) {
// }
// });
// })
function get(url, data) {
return new Promise((resolve, reject) => {
$.ajax({
url: url,
data: data,
success: function (data) {
resolve(data);
},
error: function (err) {
reject(err)
}
})
});
}
get("mock/user.json")
.then((data) => {
console.log("用户查询成功~~~:", data)
return get(`mock/user_corse_${data.id}.json`);
})
.then((data) => {
console.log("课程查询成功~~~:", data)
return get(`mock/corse_score_${data.id}.json`);
})
.then((data)=>{
console.log("课程成绩查询成功~~~:", data)
})
.catch((err)=>{
console.log("出现异常",err)
});
</script>
</body>
</html>
7. 模块化
export
用于规定模块的对外接口,export
不仅可以导出对象,一切JS变量都可以导出。比如:基本类型变量、函数、数组、对象import
用于导入其他模块提供的功能
// user.js
var name = "jack"
var age = 21
function add(a,b){
return a + b;
}
// 导出变量和函数
export {name,age,add}
---------------------------------------------------------------
// hello.js
// 导出后可以重命名
export default {
sum(a, b) {
return a + b;
}
}
--------------------------------------------------------------
// main.js
import abc from "./hello.js"
import {name,add} from "./user.js"
abc.sum(1,2);
console.log(name);
add(1,3);
6.2 vue基础
1、MVVM思想
M:model 包括数据和一些基本操作
V:view 视图,页面渲染结果
VM:View-model,模型与视图间的双向操作(无需开发人员干涉)
视图和数据通过VM绑定起来,model里有变化会自动地通过Directives填写到视view中,视图表单中添加了内容也会自动地通过DOM Listeners保存到模型中。
官方文档:https://cn.vuejs.org/v2/guide/
1. vue安装
给当前项目安装vue(控制台)
npm init -y
npm install vue
引入vue
<script src="./node_modules/vue/dist/vue.js"></script>
2. v-model, v-on
- new VUE
- v-model 双向绑定
- v-on 绑定事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="num">
<!-- v-model实现双向绑定。此处代表输入框和vue里的data绑定 -->
<button v-on:click="num++">点赞</button>
<!-- v-on:click绑定事件,实现自增。 -->
<button v-on:click="cancel">取消</button>
<!-- 回调自定义的方法。 此时字符串里代表的函数 -->
<h1> {{name}} ,非常帅,有{{num}}个人为他点赞{{hello()}}</h1>
<!-- 先从vue中拿到值填充到dom,input再改变num值,vue实例更新,然后此处也更新 -->
</div>
<!-- 导入依赖 -->
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
//1、vue声明式渲染
let vm = new Vue({ //生成vue对象
el: "#app",//绑定元素 div id="app" // 可以指定恰标签,但是不可以指定body标签
data: { //封装数据
name: "张三", // 也可以使用{} //表单中可以取出
num: 1
},
methods:{ //封装方法
cancel(){
this.num -- ;
},
hello(){
return "1"
}
}
});
// 还可以在html控制台vm.name
//2、双向绑定,模型变化,视图变化。反之亦然。
//3、事件处理
//v-xx:指令
//1、创建vue实例,关联页面的模板,将自己的数据(data)渲染到关联的模板,响应式的
//2、指令来简化对dom的一些操作。
//3、声明方法来做更复杂的操作。methods里面可以封装方法。
</script>
</body>
</html>
3. v-text、v-html、v-ref
<!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">
{{msg}} {{1+1}} {{hello()}} 前面的内容如果网速慢的话会先显示括号,然后才替换成数据。
v-html 和v-text能解决这个问题
<br/>
用v-html取内容
<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"
}
}
})
</>
</body>
</html>
4. 单向绑定v-bind:
-
花括号只能写在标签体内(
<div 标签内> 标签体 </div>
),不能用在标签内。插值表达式只能用在标签体里,如果我们这么用
<a href="{{}}">
是不起作用的,所以要用v-bind -
跳转页面
<a v-bind:href="link">跳转</a>
-
用
v-bind:
,简写为:
。表示把model绑定到view。可以设置src、title、class等
<!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">
</head>
<body>
<!-- 给html标签的属性绑定 -->
<div id="app">
<a v-bind:href="link">跳转</a>
<!-- class,style {class名:vue值}-->
<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>
</body>
</html>
5. 双向绑定v-model
<!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">
精通的语言:如果是多选框,那么会把每个value值赋值给vue数据
<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>
6. v-on事件
-
事件监听可以使用 v-on 指令
-
v-on:事件类型="方法"
,可以简写成@事件类型="方法"
-
Vue.js 为 v-on 提供了事件修饰符来处理 DOM 事件细节,如:event.preventDefault() 或 event.stopPropagation()。
-
Vue.js 通过由点 . 表示的指令后缀来调用修饰符。
- .stop - 阻止冒泡
- .prevent - 阻止默认事件
- .capture - 阻止捕获
- .self - 只监听触发该元素的事件
- .once - 只触发一次
- .left - 左键事件
- .right - 右键事件
- .middle - 中间滚轮事
<!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">
<!--事件中直接写js片段-->
<button v-on:click="num++">点赞</button>
<!--事件指定一个回调函数,必须是Vue实例中定义的函数-->
<button @click="cancel">取消</button>
<!-- -->
<h1>有{{num}}个赞</h1>
<!-- 事件修饰符 -->
<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>
<!-- 按键修饰符: -->
<input type="text" v-model="num" v-on:keyup.up="num+=2" @keyup.down="num-=2" @click.ctrl="num=10"><br />
提示:
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el:"#app",
data:{
num: 1
},
methods:{
cancel(){
this.num--;
},
hello(){
alert("点击了")
}
}
})
</script>
</body>
</html>
7. v-for遍历
- 可以遍历 数组[] 字典{} 。对于字典
<li v-for="(value, key, index) in object">
- 遍历的时候都加上:key来区分不同数据,提高vue渲染效率
<!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>
<!-- 4、遍历的时候都加上:key来区分不同数据,提高vue渲染效率 -->
<li v-for="(user,index) in users" :key="user.name" v-if="user.gender == '女'">
<!-- 1、显示user信息:v-for="item in items" -->
当前索引:{{index}} ==> {{user.name}} ==>
{{user.gender}} ==>{{user.age}} <br>
<!-- 2、获取数组下标:v-for="(item,index) in items" -->
<!-- 3、遍历对象:
v-for="value in object"
v-for="(value,key) in object"
v-for="(value,key,index) in object"
-->
对象信息:
<span v-for="(v,k,i) in user">{{k}}=={{v}}=={{i}};</span>
<!-- 4、遍历的时候都加上:key来区分不同数据,提高vue渲染效率 -->
</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>
8. v-if和v-show
-
在vue实例的data指定一个bool变量,然后v-show赋值即可。show里的字符串也可以比较
-
if是根据表达式的真假,切换元素的显示和隐藏(操作dom元素)
-
区别:show的标签F12一直都在,if的标签会移除,
-
if操作dom树对性能消耗大
<!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>
<!--
v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。
v-show,当得到结果为true时,所在的元素才会被显示。
-->
<div id="app">
<button v-on:click="show = !show">点我呀</button>
<!-- 1、使用v-if显示 -->
<h1 v-if="show">if=看到我....</h1>
<!-- 2、使用v-show显示 -->
<h1 v-show="show">show=看到我</h1>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
let app = new Vue({
el: "#app",
data: {
show: true
}
})
</script>
</body>
</html>
9. v-else和v-else-if
<!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="random=Math.random()">点我呀</button>
<span>{{random}}</span>
<h1 v-if="random>=0.75">
看到我啦? >= 0.75
</h1>
<h1 v-else-if="random>=0.5">
看到我啦? >= 0.5
</h1>
<h1 v-else-if="random>=0.2">
看到我啦? >= 0.2
</h1>
<h1 v-else>
看到我啦? < 0.2
</h1>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
let app = new Vue({
el: "#app",
data: { random: 1 }
})
</script>
</body>
</html>
10. 计算属性和监听器
计算属性computed:属性不是具体值,而是通过一个函数计算出来的,随时变化
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>计算后反转字符串: {{ reversedMessage }}</p>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'Runoob!'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
</script>
监听watch:监听属性 watch,我们可以通过 watch 来响应数据的变化。
以下实例通过使用 watch 实现计数器:
<!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>西游记; 价格:{{xyjPrice}},数量:<input type="number" v-model="xyjNum"> </li>
<li>水浒传; 价格:{{shzPrice}},数量:<input type="number" v-model="shzNum"> </li>
<li>总价:{{totalPrice}}</li>
{{msg}}
</ul>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
//watch可以让我们监控一个值的变化。从而做出相应的反应。
new Vue({
el: "#app",
data: {
xyjPrice: 99.98,
shzPrice: 98.00,
xyjNum: 1,
shzNum: 1,
msg: ""
},
computed: {
totalPrice(){
return this.xyjPrice*this.xyjNum + this.shzPrice*this.shzNum
}
},
watch: {
xyjNum: function(newVal,oldVal){
if(newVal>=3){
this.msg = "库存超出限制";
this.xyjNum = 3
}else{
this.msg = "";
}
}
},
})
</script>
</body>
</html>
11. 过滤器filter
过滤器filter:定义filter组件后,管道符后面跟具体过滤器{{user.gender | gFilter}}
<!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>
<!-- 过滤器常用来处理文本格式化的操作。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 -->
<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: { // 局部过滤器,只可以在当前vue实例中使用
genderFilter(val) {
if (val == 1) {
return "男";
} else {
return "女";
}
}
}
})
</script>
</body>
</html>
12. 组件化
-
在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。例如可能会有相同的头部导航。
-
但是如果每个页面都自开发,这无疑增加了我们开发的成本。所以我们会把页面的不同分拆分成立的组件,然后在不同页面就可以共享这些组件,避免重复开发。
-
在vue里,所有的vue实例都是组件
-
组件其实也是一个vue实例,因此它在定义时也会接收:data、methods、生命周期函数等
-
不同的是组件不会与页面的元素绑定(所以不写el),否则就无法复用了,因此没有el属性。
-
但是组件渲染需要html模板,所以增加了template属性,值就是HTML模板
-
data必须是一个函数,不再是一个对象。
-
全局组件定义完毕,任何vue实例都可以直接在HTML中通过组件名称来使用组件了
<!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">
</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>
//1、全局声明注册一个组件 // counter标签,代表button
// 把页面中<counter>标签替换为指定的template,而template中的数据用data填充
Vue.component("counter", {
template: `<button v-on:click="count++">我被点击了 {{count}} 次</button>`,
data() {// 如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例:
return {
count: 1 // 数据
}
}
});
//2、局部声明一个组件
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>
13. 生命周期和钩子函数
每个vue实例在被创建时都要经过一系列的初始化过程:创建实例,装载模板、渲染模板等等。vue为生命周期中的每个状态都设置了钩子函数(监听函)。每当vue实列处于不同的生命周期时,对应的函数就会被触发调用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a6NPkoxa-1663203102812)(https://cn.vuejs.org/images/lifecycle.png)]
<!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>
6.3 vue-dome
1. vue初始化
1、全局安装webpack
npm install webpack -g
2、全局安装vue脚手架
npm install -g @vue/cli-init
3、初始化vue项目
- 在工程文件夹下cmd,输入以下命令初始化vue项目appname为想要起的工程名
vue init webpack appname
- 如果一直卡在downloading template,配置淘宝镜像
npm config set chromedriver_cdnurl https://npm.taobao.org/mirrors/chromedriver
- 初始化成功,运行项目
cd vue-dome
npm run dev
- 启动成功
2. vue项目目录结构
目录/文件 | 说明 |
---|---|
build | 项目构建(webpack)相关代码 |
config | 配置目录,包括端口号等。我们初学可以使用默认的。 |
node_modules | npm 加载的项目依赖模块 |
src | 这里是我们要开发的目录,基本上要做的事情都在这个目录里。里面包含了几个目录及文件:assets : 放置一些图片,如logo等。components : 目录里面放了一个组件文件,可以不用。App.vue : 项目入口文件,我们也可以直接将组件写这里,而不使用 components 目录。main.js : 项目的核心文件。 |
static | 静态资源目录,如图片、字体等。 |
test | 初始测试目录,可删除 |
.xxxx文件 | 这些是一些配置文件,包括语法配置,git配置等 |
index.html | 首页入口文件。 |
package.json | 项目配置文件。 |
README.md | 项目的说明文档,markdown 格式 |
3. 修改vue项目
1、分析项目的关系结构
- index.html
- 其中只有一个
div
- 其中只有一个
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
- main.js
- new Vue绑定div
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router, //采用router路由
components: { App },//绑定App组件
template: '<App/>'
})
- App.vue
- 首先显示一张图片,图片路径为
"./assets/logo.png
- 其中的
<router-view/>
是根据url要决定访问的vue,在main.js中提及了使用的是./router
规则
- 首先显示一张图片,图片路径为
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
...
</style>
- router/index.js
- routes表示路由规则
- 当访问
/
时, 显示组件Helloword
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
2、实现点击Hello,显示自己创建的Hello组件
- 创建hello.vue组件
<template>
<div>
<h1>你好,hello,{{name}}</h1>
</div>
</template>
<script>
export default {
data(){
return {
name: "张三"
}
}
}
</script>
<style>
</style>
- 编写路由,修改/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Hello from '@/components/hello' //导入自定义的组件
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
//新增路由
{
path: '/hello',
name: "Hello",
component: Hello
}
]
})
- 修改App.vue
<template>
<div id="app">
<img src="./assets/logo.png">
<router-link to="/hello">去hello</router-link> <!--新增去hello-->
<router-link to="/">去首页</router-link><!--新增去首页-->
<router-view/>
</div>
</template>
- 运行测试效果
4. 快速生成组件模板
1、步骤
-
文件->首选项->用户代码 新建全局代码片段
-
把下面代码粘贴进去
{
"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>",
"$4",
"</style>"
],
"description": "生成vue模板"
}
}
- 在创建组件时直接输入
vue
点击回车就可生成模板
6.4 ElementUI
官方文档:https://element.eleme.cn/#/zh-CN/component/installation
- 安装
npm install element-ui
- 在main.js下引入
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
然后就可以使用elementui之中的组件。
- 快速搭建后台管理系统的页面
elementui手册中找到Container 布局容器
,找到代码直接复制到App.vue
组件
启动测试:
- 实现当点击用户列表,显示用户。点击hello组件,显示hello。
- 把
<el-main>
中的数据列表换成路由视图<router-view></router-view>
- 新建MyTable组件,用来显示用户数据
<!-- -->
<template>
<div class="">
<el-table :data="tableData">
<el-table-column prop="date" label="日期" width="140"> </el-table-column>
<el-table-column prop="name" label="姓名" width="120"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
const item = {
date: "2016-05-02",
name: "王小虎",
address: "上海市普陀区金沙江路 1518 弄",
};
return {
tableData: Array(20).fill(item),
};
},
};
</script>
<style scoped>
</style>
- 添加路由规则
import MyTable from '@/components/MyTable'
{
path: '/mytable',
name: "mytable",
components: MyTable
}
- 修改App.vue
启动测试!!!
7. 三级分类
7.1 查询-递归树形结构获取
实现查询出所有分类和子分类,并且把它们以父子的结构组装起
1、步骤
- 先把数据导入表
pms_category
- 修改gulimall-product微服务内容
CategoryController
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
CategoryEntity
:新增属性
@TableField(exist = false) //表示数据库表中不存在
private List<CategoryEntity> children;
categoryService
:接口新增方法listWithTree()
,
List<CategoryEntity> listWithTree();
- 编写实现类
categoryServiceImpl
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、组装成父子的树形结构
//2.1 找到所有的一级分类
List<CategoryEntity> level1Menus = entities.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.map((menu)->{
// 设置一级分类的子分类
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
//排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
})
.collect(Collectors.toList());
return level1Menus;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){
List<CategoryEntity> children = all.stream()
.filter(CategoryEntity -> CategoryEntity.getParentCid().equals(root.getCatId()))
.map(categoryEntity -> {
//递归查找
categoryEntity.setChildren(getChildren(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;
}
- 启动测试:http://localhost:9999/product/category/list/tree
- 由于10000端口被占用,改为9999端口
7.2 三级分类的前端
1、步骤
-
启动renren-fast(IDEA)和renren-fast-vue(VsCode)
-
登录进去
- 新增一级菜单
商品系统
- 新增菜单
分类维护
,在商品系统
下,路由为product/category
- 新增之后的菜单在都保存在数据库之中:查看
gulimall_admin
中的sys_menu
表
- 前端路由规则
在左侧点击【商品系统-分类维护】,希望在此展示3级分类。可以看到
- url是
http://localhost:8001/#/product-category
- 填写的菜单路由是
product/category
- 对应的视图是
src/view/modules/product/category.vue
所以如果我们要自定义product/category
视图的话,就要
-
创建
src/view/modules/product/category.vue
-
创建vue模板,然后去elementui看如何使用多级目录
<!-- -->
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
data: [],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取后台数据
getMenus(){
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(data=>{
console.log("成功了获取到菜单数据....", data)
})
}
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
//创建完成时,就调用getMenus函数
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {
},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped>
</style>
- 启动测试:http://localhost:8001/#/sys-menu
发现,他是给8080端口发的请求,而我们的商品服务在10000端口。我们以后还会同时发向更多的端口,所以需要配置网关,前端只向网关发送请求,然后由网关自己路由到相应端口。
- 让前端向网关发送端口
修改static/config/index.js
/**
* 开发环境
*/
;(function () {
window.SITE_CONFIG = {};
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
// cdn地址 = 域名 + 版本号
window.SITE_CONFIG['domain'] = './'; // 域名
window.SITE_CONFIG['version'] = ''; // 版本号(年月日时分)
window.SITE_CONFIG['cdnUrl'] = window.SITE_CONFIG.domain + window.SITE_CONFIG.version;
})();
刷新,发现验证码出不来。
分析原因:前端给网关发验证码请求,但是验证码请求在renren-fast服务里,所以要想使验证码好使,需要把renren-fast服务注册到服务中心,并且由网关进行路由
- renren-fast注册到服务注册中心
- 引入nacos依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
- 修改配置文件
application.yml
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
-
开启服务注册功能
@EnableDiscoveryClient
-
注册成功
- 配置网关,新增路由
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/** # 把所有api开头的请求都转发给renren-fast
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# 默认规则, 请求过来:http://localhost:88/api/captcha.jpg 转发--> http://renren-fast:8080/api/captcha.jpg
# 但是真正的路径是http://renren-fast:8080/renren-fast/captcha.jpg
# 所以使用路径重写把/api/* 改变成 /renren-fast/*
2、遇到的问题
- 404问题:
显示验证码的路径一直显示404,跟着网上和雷神老师的方法都不行,最后决定放弃,将前端路由改成http://localhost:8080/renren-fast
- 503问题:
- 首先启动项目后观察renren-fast和gulimall-gateway的nacos是否注册成功:http://localhost:8848/nacos
- 发现是gulimall-gateway的maven中的spring-cloud-starter-gateway和spring-boot-starter-test冲突了
- 由于我没有通过前后端接口映射,所以不存在跨域问题!
7.3 解决跨域问题
- 什么是跨域问题?
指的是浏览器
不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略
:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域问题
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js | 同一域名下 | 允许 |
http://www.a.com/lab/a.js http://www.a.com/script/b.js | 同一域名下不同文件夹 | 允许 |
http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名,不同端口 | 不允许 |
http://www.a.com/a.js https://www.a.com/b.js | 同一域名,不同协议 | 不允许 |
http://www.a.com/a.js http://70.32.92.74/b.js | 域名和域名对应ip | 不允许 |
http://www.a.com/a.js http://script.a.com/b.js | 主域相同,子域不同 | 不允许 |
http://www.a.com/a.js http://a.com/b.js | 同一域名,不同二级域名(同上) | 不允许(cookie这种情况下也不允许访问 |
http://www.cnblogs.com/a.js http://www.a.com/b.js | 不同域名 | 不允许 |
- 跨域的流程
跨域请求的实现是通过预检请求实现的,先发送一个OPSTIONS探路,收到响应允许跨域后再发送真实请求
- 解决办法1:使用nginx部署为同一域
- 解决方法2: 配置当次请求允许跨域
-
Access-Control-Allow-Origin : 支持哪些来源的请求跨域
-
Access-Control-Allow-Method : 支持那些方法跨域
-
Access-Control-Allow-Credentials :跨域请求默认不包含cookie,设置为true可以包含cookie
-
Access-Control-Expose-Headers : 跨域请求暴露的字段
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
-
Access-Control-Max-Age :表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效。
- 用方法2解决跨域问题: 配置filter,每个请求来了以后,返回给浏览器之前都添加上那些字段
- 在gulimall-gateway中新建配置类GulimallCorsConfiguration.java
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration= new CorsConfiguration();
//1、配置跨域
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
- 注释掉renren-fast/src/main/java/io.renren/config/CorsConfig之中的跨域
- 成功解决跨域问题,登录到后台管理界面:http://localhost:8001/#/home
7.4 前端展示查询结果
- 查询不到数据
- 在网关中配置新的路由
# 精确的路由要放在上面
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- 新建一个命名空间http://localhost:8848/nacos
- 新建一个bootstrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=02dcfc8a-cf6a-4fe4-aeab-8c2056c23f59
- 发现返回的是一个对象,对象.data.data才是我们要的数据。 修改
category.vue
<!-- -->
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
menus: [],
defaultProps: {
children: 'children', //子节点
label: 'name' //name属性作为标签的值,展示出来
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus(){
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data})=>{
console.log("成功了获取到菜单数据....", data.data)
this.menus = data.data;
})
}
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {
},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped>
</style>
- 启动测试,成功!
7.5 三级分类删除
1、菜单删除的页面展示
node与data
在element-ui的tree中,有2个非常重要的属性
node代表当前结点(是否展开等信息,element-ui自带属性),
data是结点数据,是自己的数据。
data从哪里来:前面ajax发送请求,拿到data,赋值给menus属性,而menus属性绑定到标签的data属性。而node是ui的默认规则
实现:
- 在每一个菜单后面添加
append, delete
- 点击按钮时,不进行菜单的打开合并
:expand-on-click-node="false"
- 当没有子菜单时,才可以显示delete按钮。当为一级、二级菜单时,才显示append按钮
- 添加多选框
show-checkbox
- 设置
node-key=""
标识每一个节点的不同
修改category.vue
<!-- -->
<template>
<el-tree
:data="menus"
show-checkbox
:props="defaultProps"
@node-click="handleNodeClick"
:expand-on-click-node="false"
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
v-if="node.level <= 2"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
type="text"
v-if="node.childNodes.length == 0"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
menus: [],
defaultProps: {
children: "children", //子节点
label: "name", //name属性作为标签的值,展示出来
},
};
},
methods: {
handleNodeClick(data) {},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功了获取到菜单数据....", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
},
remove(node, data) {
console.log("remove", node, data);
},
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
2、编写删除的后台代码
- 查看controller,发现参数
@RequestBody
注解,请求体里面的数据,只有post请求才有请求体。
- 修改CategoryController类
/**
* 删除
* @RequestBody 只有post请求才有请求体
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
- 逻辑删除步骤
- 在application.yml中配置统一的全局规则(可以省略)
# 配置逻辑删除
mybatis-plus.global-config.db-config.logic-delete-value=0
mybatis-plus.global-config.db-config.logic-not-delete-value=1
- 配置逻辑删除组件(mybatis-plus3.1.1开始不需要这一步)
- 实体类字段上加上
@TableLogic(value = "1", delval = "0")
注解,三级分类把showStatus字段作为逻辑删除字段
-
在service接口中声明方法
removeMenuByIds
,Impl里面实现。 留下一个TODO检查当前删除的菜单,是否被别的地方引用
。 -
修改
CategoryServiceImpl
类
// CategoryServiceImpl
@Override
public void removeMenuByIds(List<Long> catIds) {
//TODO 1、检查当前删除的菜单,是否被别的地方引用
baseMapper.deleteBatchIds(catIds);
}
- 重启项目,进行测试,idea->tools->Http Client->Test RESTful web service
### Send POST request with json body
POST http://localhost:88/api/product/category/delete
Content-Type: application/json
[1431]
测试成功!
- 可以打印日志,在配置文件中修改日志级别
logging:
level:
com.xmh.guliamll.product: debug
3、前端向后端发请求
- 新增两个代码块,发送get请求和post请求。
文件->首选项->用户片段
"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请求"
}
实现:
- 编写前端remove方法,实现向后端发送请求
- 点击
delete
弹出提示框,是否删除这个节点: elementui中MessageBox 弹框
中的确认消息
添加到删除之前 - 删除成功后有消息提示: elementui中
Message 消息提示
- 原来展开状态的菜单栏,在删除之后也应该展开:
el-tree
组件的default-expanded-keys
属性,默认展开。 每次删除之后,把删除菜单的父菜单的id值
赋给默认展开值即可。
- 增加category.vue内容
//在el-tree中设置默认展开属性,绑定给expandedKey
:default-expanded-keys="expandedKey"
//data中添加属性
expandedKey: [],
//完整的remove方法
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(() => {});
},
测试
- 点击三级分类的Delete按钮
- 删除成功!!!
7.6 三级分类新增
- elementui中
Dialog 对话框
- 一个会话的属性为:
visible.sync=“dialogVisible”
- 导出的data中"dialogVisible = false"
- 点击确认或者取消后的逻辑都是@click=“dialogVisible = false” 关闭会话
-
点击append,弹出对话框,输入分类名称
-
点击确定,添加到数据库: 新建方法
addCategory
发送post请求到后端; 因为要把数据添加到数据库,所以在前端数据中按照数据库的格式声明一个category
。点击append时,计算category
属性,点击确定时发送post请求。
1、实现步骤
- category.vue头文件
<!--对话框组件-->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="categroy">
<el-form-item label="分类名称">
<el-input v-model="categroy.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</span>
</el-dialog>
- category.vue数据与方法体
//1data中新增数据
//按照数据库格式声明的数据
categroy: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
//判断是否显示对话框
dialogVisible: false,
//修改append方法,新增addCategory方法
//点击append后,计算category属性,显示对话框
append(data) {
console.log("append", data);
this.dialogVisible = true;
this.categroy.parentCid = data.catId;
this.categroy.catLevel = data.catLevel * 1 + 1;
},
//点击确定后,发送post请求
//成功后显示添加成功,展开刚才的菜单
addCategory() {
console.log("提交的数据", this.categroy);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.categroy, false),
}).then(({ data }) => {
this.$message({
message: "添加成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.categroy.parentCid];
this.dialogVisible = false;
});
},
2、测试
- 点击二级分类的Append
- 添加成功!!!
7.7 基本修改功能
实现修改名称,图标,计量单位。
-
新增Edit按钮:复制之前的append
-
查看controller,发现updata方法是由id进行更新的,所以
data
中的category
中新增catId
-
增加、修改的时候也修改图标和计量单位,所以data的
category
新增inco,productUnit
-
新建
edit
方法,用来绑定Edit按钮。新建editCategory
方法,用来绑定对话框的确定按钮。 -
复用对话框:
- data数据中新增
dialogType
,用来标记此时对话框是由edit打开的,还是由append打开的。 - 新建方法
submitData
,与对话框的确定按钮进行绑定,在方法中判断,如果dialogType==add
调用addCategory(),如果dialogType==edit
调用editCategory() - data数据中新增
title
,绑定对话框的title,用来做提示信息。判断dialogType
的值,来选择提示信息。
- data数据中新增
-
防止多个人同时操作,对话框中的回显的信息应该是由数据库中读出来的:点击Edit按钮,发送httpget请求。(看好返回的数据)
-
编辑
editCategory
方法:- controller之中的更新是动态更新,根据id,发回去什么值修改什么值,所以把要修改的数据发回后端就好。
- 成功之后发送提示消息,展开刚才的菜单。
-
编辑之后,再点击添加,发现会回显刚才编辑的信息。所以在
append
方法中重置回显的信息。
代码+注释:
category.vue
<!--在el-tree的span标签下添加一个新的el-button-->
<!--编辑按钮-->
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<!--可复用的对话框-->
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%">
<el-form :model="categroy">
<el-form-item label="分类名称">
<el-input v-model="categroy.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="categroy.inco" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input
v-model="categroy.productUnit"
autocomplete="off"
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
category.vue
//data, 新增了title、dialogType。 categroy中新增了inco、productUnit、catId
data() {
return {
title: "",
dialogType: "",
categroy: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
inco: "",
productUnit: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children", //子节点
label: "name", //name属性作为标签的值,展示出来
},
};
//方法
//绑定对话框的确定按钮,根据dialogType判断调用哪个函数
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//绑定Edit按钮,设置dialogType、title,从后台读取数据,展示到对话框内
edit(data) {
this.dialogType = "edit";
this.title = "修改菜单";
this.dialogVisible = true;
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
console.log(data);
this.categroy.catId = data.data.catId;
this.categroy.name = data.data.name;
this.categroy.inco = data.data.inco;
this.categroy.productUnit = data.data.productUnit;
});
},
//绑定对话框的确定按钮,向后台发送更新请求,传过去想要修改的字段
editCategory() {
var { catId, name, inco, productUnit } = this.categroy;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, inco, productUnit }, false),
}).then(({ data }) => {
this.$message({
message: "修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.categroy.parentCid];
this.dialogVisible = false;
});
},
//点击append按钮,清空编辑之后的回显数据
append(data) {
this.dialogType = "add";
this.title = "添加菜单";
console.log("append", data);
this.dialogVisible = true;
this.categroy.parentCid = data.catId;
this.categroy.catLevel = data.catLevel * 1 + 1;
this.categroy.name = "",
this.categroy.inco = "",
this.categroy.productUnit = ""
},
测试:
- 修改成功!!!
7.8 拖拽修改功能
- 拖拽功能的前端实现:lementui树型控件->可拖拽节点
- 在
<el-tree>
中加入属性draggable
表示节点可拖拽。 - 在
<el-tree>
中加入属性:allow-drop="allowDrop"
,拖拽时判定目标节点能否被放置。 allowDrop
有三个参数draggingNode
表示拖拽的节点,dropNode
表示拖拽到哪个节点,type
表示拖拽的类型’prev’、‘inner’ 和 ‘next’,表示拖拽到目标节点之前、里面、之后。allowDrop
函数实现判断,拖拽后必须保持数型的三层结构。- 节点的深度 = 最深深度 - 当前深度 + 1
- 当拖拽节点拖拽到目标节点的内部,要满足: 拖拽节点的深度 + 目标节点的深度 <= 3
- 当拖拽节点拖拽的目标节点的两侧,要满足: 拖拽节点的深度 + 目标节点的父节点的深度 <= 3
代码 + 注释:
category.vue
<!--el-tree中添加属性-->
draggable
:allow-drop="allowDrop"
// data中新增属性,用来记录当前节点的最大深度
maxLevel: 1,
//新增方法
allowDrop(draggingNode, dropNode, type) {
console.log("allowDrag:", draggingNode, dropNode, type);
//节点的最大深度
this.countNodeLevel(draggingNode.data);
console.log("level:", this.maxLevel);
//当前节点的深度
let deep = (this.maxLevel - draggingNode.data.catLevel) + 1;
console.log(deep)
if (type == "inner"){
return (deep + dropNode.level) <= 3;
}else{
return (deep + dropNode.parent.level) <= 3;
}
},
//计算当前节点的最大深度
countNodeLevel(node) {
//找到所有的子节点,求出最大深度
if (node.children != null && node.children.length > 0){
for (let i = 0; i < node.children.length; i++){
if (node.children[i].catLevel > this.maxLevel){
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
- 拖拽基本修改成功!!!
- 拖拽功能的数据收集
- 在
<el-tree>
中加入属性@node-drop="handleDrop"
,表示拖拽事件结束后触发事件handleDrop
,handleDrop
共四个参数,draggingNode
:被拖拽节点对应的 Node;dropNode:
结束拖拽时最后进入的节点;dropType:
被拖拽节点的放置位置(before、after、inner);ev:
event - 拖拽可能影响的节点的数据:parentCid、catLevel、sort
- data中新增
updateNodes
,把所有要修改的节点都传进来。 - 要修改的数据:拖拽节点的parentCid、catLevel、sort
- 要修改的数据:新的兄弟节点的sort (把新的节点收集起来,然后重新排序)
- 要修改的数据:子节点的catLevel
- data中新增
代码 + 注释:
category.vue
//el-tree中新增属性,绑定handleDrop,表示拖拽完触发
@node-drop="handleDrop"
//data 中新增数据,用来记录需要更新的节点(拖拽的节点(parentCid、catLevel、sort),拖拽后的兄弟节点(sort),拖拽节点的子节点(catLevel))
updateNodes: [],
//新增方法
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新父节点的id
let pCid = 0;
//拖拽后的兄弟节点,分两种情况,一种是拖拽到两侧,一种是拖拽到内部
let sibings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined? 0 : dropNode.parent.data.catId;
sibings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
sibings = dropNode.childNodes;
}
//2、当前拖拽节点的最新顺序
//遍历所有的兄弟节点,如果是拖拽节点,传入(catId,sort,parentCid,catLevel),如果是兄弟节点传入(catId,sort)
for (let i = 0; i < sibings.length; i++) {
if (sibings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (sibings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = sibings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(sibings[i]);
}
this.updateNodes.push({
catId: sibings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i });
}
}
//每次拖拽后把数据清空,否则要修改的节点将会越拖越多
(this.updateNodes = []), (this.maxLevel = 1);
},
// 修改拖拽节点的子节点的层级
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
//遍历子节点,传入(catId,catLevel)
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
//处理子节点的子节点
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
- 拖拽功能实现
- 在后端编写批量修改的方法
update/sort
- 前端发送post请求,把要修改的数据发送过来
- 提示信息,展开拖拽节点的父节点
//批量修改
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] category){
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
测试批量修改:
POST http://localhost:88/api/product/category/update/sort
Content-Type: application/json
[
{
"catId": 1,
"sort" : 10
},
{
"catId": 226,
"catLevel": 2
}
]
前端发送请求:
//3、当前拖拽节点的最新层级
console.log("updateNodes",this.updateNodes);
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [pCid];
});
- 批量拖拽功能
- 添加开关,控制拖拽功能是否开启
- 每次拖拽都要和数据库交互,不合理。批量拖拽过后,一次性保存。
<!--添加拖拽开关和批量保存按钮-->
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
>
</el-switch>
<el-button v-if="draggable" size="small" round @click="batchSave"
>批量保存</el-button
>
//data中新增数据
pCid:[], //批量保存过后要展开的菜单id
draggable: false, //绑定拖拽开关是否打开
//修改了一些方法,修复bug,修改过的方法都贴在下面了
//点击批量保存按钮,发送请求
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = this.pCid;
});
this.updateNodes = [];
},
//
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新父节点的id
let pCid = 0;
let sibings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
sibings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
sibings = dropNode.childNodes;
}
//2、当前拖拽节点的最新顺序
for (let i = 0; i < sibings.length; i++) {
if (sibings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (sibings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = sibings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(sibings[i]);
}
this.updateNodes.push({
catId: sibings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i });
}
}
this.pCid.push(pCid);
console.log(this.pCid)
//3、当前拖拽节点的最新层级
//console.log("updateNodes", this.updateNodes)
//拖拽之后重新置1
this.maxLevel = 1;
},
// 修改拖拽判断逻辑
allowDrop(draggingNode, dropNode, type) {
console.log("allowDrag:", draggingNode, dropNode, type);
this.maxLevel = draggingNode.level;
//节点的最大深度
this.countNodeLevel(draggingNode);
console.log("maxLevel:", this.maxLevel);
//当前节点的深度
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("level",deep);
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]);
}
}
},
7.9 批量删除功能
- 新增删除按钮
<el-button type="danger" size="small" @click="batchDelete" round>批量删除</el-button>
<!--eltree中新增属性,用作组件的唯一标示-->
ref="menuTree"
- 批量删除方法
batchDelete(){
let catIds = [];
let catNames = [];
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
for (let i = 0; i < checkedNodes.length; i++){
catIds.push(checkedNodes[i].catId);
catNames.push(checkedNodes[i].name);
}
this.$confirm(`是否批量删除【${catNames}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
this.$http({
url:this.$http.adornUrl('/product/category/delete'),
method:'post',
data: this.$http.adornData(catIds, false)
}).then(({data})=>{
this.$message({
message: "菜单批量删除成功",
type: "success",
});
this.getMenus();
})
}).catch(()=>{
});
},
8. 品牌管理
idea删除文件恢复
这里要用到之前生成的renren-fast前端代码。。但是我给删除了。还好idea可以找回删除文件。
1、右键点击resources->Local History->Show History
2、找到删除前端的记录
3、右键->Revert。 找回成功!
8.1 使用逆向工程前后端代码
1、菜单管理->新增菜单
2、把生成的前端代码复制到前端工程下
前端代码路径\product\main\resources\src\views\modules\product
3、没有新增删除按钮: 修改权限,Ctrl+Shift+F查找isAuth
,全部返回为true
4、查看效果
8.2 效果优化-快速显示开关
1、关闭ES6的语言检验
1、在列表中添加自定义列:中间加<template></template>
标签。可以通过 Scoped slot
可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据
2、修改开关状态,发送修改请求
3、数据库中showStatus是01,开关默认值是true/false。 所以在开关中设置:active-value="1" 、:inactive-value="0"
属性,与数据库同步
代码+注释:
<!--brand.vue中显示状态那一列-->
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态"
>
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
@change="updateBrandStatus(scope.row)"
>
</el-switch>
</template>
</el-table-column>
<!--brand-add-or-update.vue中显示状态那一列-->
<el-form-item label="显示状态" prop="showStatus">
<el-switch
v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</el-form-item>
//brand.vue中新增方法,用来修改状态
updateBrandStatus(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",
});
});
},
8.3 文件上传功能
和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
这里我们选择将图片放置到阿里云上,使用对象存储。
帮助文档:
-
https://github.com/alibaba/aliyun-spring-boot/blob/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample/README-zh.md
-
https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ
上传策略:服务端签名后直传
- 开通阿里云OSS对象存储服务,创建新的Bucket
- 获取
Endpoint
、AccessKey ID
、AccessKey Secret
- 创建子账户
- 点击创建用户
-
新建成功后得到
AccessKey ID
、AccessKey Secret
-
对子账户分配权限,管理OSS对象存储服务
- Endpoint(gulimall-xmh -> 概览 -> Endpoint(地域节点))
- 建立第三方工程(直接实现服务端签名后直传,之前测试的就不在笔记上记录了)
- 新建module,
gulimall-third-party
- 引入依赖
<dependencies>
<dependency>
<groupId>com.xmh.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- nacos新建命名空间
third-party
- 命名空间中新建配置文件
oss.yaml
spring:
cloud:
alicloud:
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket: cocochimp-markdown-img
access-key: LTAI5tGSAG1RKv7tNqFSjoAv
secret-key: YuEHVZkjeDmBZTiCD3hegSR2shOdRU
- 项目新建
bootstrap.properties
spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=50094eed-e13e-47aa-9755-a8e4f426670f
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
- 项目新建
application.yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-third-party
server:
port: 30000
- 新建主启动类
com/xmh/gulimall/thirdparty/GulimallThirdPartyApplication.java
@SpringBootApplication
@EnableDiscoveryClient
public class GulimallThirdPartyApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallThirdPartyApplication.class, args);
}
}
- 测试上传文件功能
@SpringBootTest
class GulimallThirdPartyApplicationTest {
@Autowired
OSSClient ossClient;
@Test
public void testUpload() throws FileNotFoundException {
//上传文件流。
InputStream inputStream = new FileInputStream("E:\\SystemDefault\\桌面\\1.jpg");
ossClient.putObject("gulimall-xmh", "hahaha1.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功.");
}
}
- 新建controller
@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; // host的格式为 bucketname.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) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}
访问http://localhost:30000/oss/policy测试:
- 配置网关
- 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测试:
- 前端联调,实现文件上传功能
- 文件上传组件在/renren-fast-vue/src/components中
- 修改组件中
el-upload
中的action
属性,替换成自己的Bucket域名
- 把单个文件上传组件应用到
brand-add-or-update.vue
//在<script>标签中导入组件
import singleUpload from "@/components/upload/singleUpload"
//在export default中声明要用到的组件
components:{
singleUpload
},
<!--用新的组件替换原来的输入框-->
<el-form-item label="品牌logo地址" prop="logo">
<singleUpload v-model="dataForm.logo"></singleUpload>
</el-form-item>
- 解决跨域问题
- 创建新规则
对象存储oss—>Bucket列表—>具体的Bucket信息—>权限管理—>跨域设置
- 下滑找到跨域配置选项
- 创建新得跨域规则
- 配置成功后,点击图片上传,进行测试。
测试成功!
8.4 效果优化-显示图片
- 新增品牌,发现在品牌logo下面显示的是地址。应该显示图片。
- 在品牌logo下添加图片标签
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo"
>
<template slot-scope="scope">
<img :src="scope.row.logo" style="width: 100px; height: 80px">
</template>
</el-table-column>
8.5 前端表单校验
- 首字母只能为a-z或者A-Z的一个字母
- 排序必须是大于等于0的一个整数
el-form中rules
表示校验规则
brand-add-or-update.vue
//排序加上.number表示要接受一个数字
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
//首字母校验规则
firstLetter: [
{
validator: (rule, value, callback) => {
if (value == '') {
callback(new Error("首字母必须填写"));
} else if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error("首字母必须在a-z或者A-Z之间"));
} else {
callback();
}
},
trigger: "blur",
},
],
//排序字段校验规则
sort: [
{
validator: (rule, value, callback) => {
if (value == '') {
callback(new Error("排序字段必须填写"));
} else if (!Number.isInteger(value) || value < 0) {
callback(new Error("排序字段必须是一个大于等于0的整数"));
} else {
callback();
}
},
trigger: "blur",
},
],
8.6 JSR303数字校验
1、基本校验实现
- springboot2.3.0以上需要手动引入依赖,引入到common中
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
- 给bean添加校验注解,并定义自己的message提示
BrandEntity.java
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0, message = "排序必须大于等于0")
private Integer sort;
}
- 在需要校验的方法上添加
@Valid
注解,并返回提示信息 - 给校验的bean后紧跟着一个BindingResult,就可以获取到校验的结果
BrandController.java
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if (result.hasErrors()){
Map<String, String> map = new HashMap<>();
//1、获取校验的结果
result.getFieldErrors().forEach((item)->{
//获取到错误提示
String message = item.getDefaultMessage();
//获取到错误属性的名字
String field = item.getField();
map.put(field, message);
});
return R.error().put("data", map);
}else{
brandService.save(brand);
}
return R.ok();
}
测试:
POST http://localhost:88/api/product/brand/save
Content-Type: application/json
{"name": "aa", "logo": "avc"}
测试结果:
2、统一异常处理
- 针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码。
在common中新建BizCodeEnume
用来存储状态码
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
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;
}
}
- 在product里面新建类
GulimallExceptionControllerAdvice
,用来集中处理所有异常
@RestControllerAdvice(basePackages = "com.xmh.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
// 处理数据校验异常
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}
//处理全局异常
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
return R.error(BizCodeEnume.UNKNOW_EXEPTION.getCode(), BizCodeEnume.UNKNOW_EXEPTION.getMsg());
}
}
- 测试集结果
3、分组校验功能
-
在common中新建valid包,里面新建两个空接口
AddGroup
,UpdateGroup
用来分组 -
给校验注解,标注上groups,指定什么情况下才需要进行校验
如:指定在更新和添加的时候,都需要进行校验
@NotEmpty @NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class}) private String name;
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
-
业务方法参数上使用@Validated注解,并在value中给出group接口,标记当前校验是哪个组
@RequestMapping("/save") public R save(@Valided({AddGroup.class}) @RequestBody BrandEntity brand){ ... }
-
默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效
4、自定义校验
- 编写一个自定义校验注解
ListValue
- 新建配置文件
ValidationMessages.properties
保存注解信息 - 编写一个自定义校验器
ListValueConstraintValidator
- 关联自定义的校验器和自定义的校验注解
(可以指定多个不同的校验器,适配不同类型的校验)
校验注解:
@Documented
@Constraint(validatedBy = {})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{com.xmh.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
配置文件:
com.xmh.common.valid.ListValue.message=必须提交指定的值
校验器:
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set=new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
int[] value = constraintAnnotation.vals();
for (int i : value) {
set.add(i);
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
关联校验器和校验注解:在校验注解的@Constraint
注解上关联校验器
@Constraint(validatedBy = {ListValueConstraintValidator.class})
校验注解添加到showStatus上,进行测试
@ListValue(vals = {0, 1},groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
- brand.vue
url: this.$http.adornUrl("/product/brand/update/status"),
9. 属性分组
1、SPU与SKU
SPU:“商品介绍”与“规格与包装”(规格属性)
SKU:详细的商品数值(销售属性)
2、运行
sys_menus.sql
创建全部菜单
以后前端代码不自己编写了,复制/代码/前端/modules/文件夹里面的内容复制到vs中
- 接口文档地址:https://easydoc.xyz/s/78237135
9.1 前端组件抽取
想要实现点击菜单的左边,能够实现在右边展示数据并且修改。
1、前端组件
-
在modules下新建
common/categroy.vue
,公共组件。抽出去左侧菜单<!-- --> <template> <el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree"> </el-tree> </template> <script> //这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等) //例如:import 《组件名称》 from '《组件路径》'; export default { //import引入的组件需要注入到对象中才能使用 components: {}, data() { //这里存放数据 return { menus: [], expandedKey: [], defaultProps: { children: "children", //子节点 label: "name", //name属性作为标签的值,展示出来 }, }; }, //监听属性 类似于data概念 computed: {}, //监控data中的数据变化 watch: {}, //方法集合 methods: { getMenus() { this.$http({ url: this.$http.adornUrl("/product/category/list/tree"), method: "get", }).then(({ data }) => { console.log("成功了获取到菜单数据....", data.data); this.menus = data.data; }); }, }, //生命周期 - 创建完成(可以访问当前this实例) created() { this.getMenus(); }, //生命周期 - 挂载完成(可以访问DOM元素) mounted() {}, beforeCreate() {}, //生命周期 - 创建之前 beforeMount() {}, //生命周期 - 挂载之前 beforeUpdate() {}, //生命周期 - 更新之前 updated() {}, //生命周期 - 更新之后 beforeDestroy() {}, //生命周期 - 销毁之前 destroyed() {}, //生命周期 - 销毁完成 activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发 }; </script> <style scoped> </style>
-
把前端是生成的
attrgroup-add-or-update.vue
复制到modules/product下 -
在modules/product/下创建
attrgroup.vue
- 布局:左侧6用来显示菜单,右侧18用来显示表格
- 引入公共组件
Category, AddOrUpdate
- 剩下的复制生成的
attrgroup.vue
<!-- --> <template> <el-row :gutter="20"> <el-col :span="6"><category></category></el-col> <el-col :span="18" ><div class="mod-config"> <el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()" > <el-form-item> <el-input v-model="dataForm.key" placeholder="参数名" clearable ></el-input> </el-form-item> <el-form-item> <el-button @click="getDataList()">查询</el-button> <el-button v-if="isAuth('product:attrgroup:save')" type="primary" @click="addOrUpdateHandle()" >新增</el-button > <el-button v-if="isAuth('product:attrgroup:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0" >批量删除</el-button > </el-form-item> </el-form> <el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%" > <el-table-column type="selection" header-align="center" align="center" width="50" > </el-table-column> <el-table-column prop="attrGroupId" header-align="center" align="center" label="分组id" > </el-table-column> <el-table-column prop="attrGroupName" header-align="center" align="center" label="组名" > </el-table-column> <el-table-column prop="sort" header-align="center" align="center" label="排序" > </el-table-column> <el-table-column prop="descript" header-align="center" align="center" label="描述" > </el-table-column> <el-table-column prop="icon" header-align="center" align="center" label="组图标" > </el-table-column> <el-table-column prop="catelogId" header-align="center" align="center" label="所属分类id" > </el-table-column> <el-table-column fixed="right" header-align="center" align="center" width="150" label="操作" > <template slot-scope="scope"> <el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)" >修改</el-button > <el-button type="text" size="small" @click="deleteHandle(scope.row.attrGroupId)" >删除</el-button > </template> </el-table-column> </el-table> <el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage" layout="total, sizes, prev, pager, next, jumper" > </el-pagination> <!-- 弹窗, 新增 / 修改 --> <add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList" ></add-or-update></div ></el-col> </el-row> </template> <script> //这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等) //例如:import 《组件名称》 from '《组件路径》'; import Category from "../common/category.vue"; import AddOrUpdate from "./attrgroup-add-or-update.vue"; export default { //import引入的组件需要注入到对象中才能使用 components: { Category, AddOrUpdate}, data() { return { dataForm: { key: "", }, dataList: [], pageIndex: 1, pageSize: 10, totalPage: 0, dataListLoading: false, dataListSelections: [], addOrUpdateVisible: false, }; }, activated() { this.getDataList(); }, methods: { // 获取数据列表 getDataList() { this.dataListLoading = true; this.$http({ url: this.$http.adornUrl("/product/attrgroup/list"), 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; }); }, // 每页数 sizeChangeHandle(val) { this.pageSize = val; this.pageIndex = 1; this.getDataList(); }, // 当前页 currentChangeHandle(val) { this.pageIndex = val; this.getDataList(); }, // 多选 selectionChangeHandle(val) { this.dataListSelections = val; }, // 新增 / 修改 addOrUpdateHandle(id) { this.addOrUpdateVisible = true; this.$nextTick(() => { this.$refs.addOrUpdate.init(id); }); }, // 删除 deleteHandle(id) { var ids = id ? [id] : this.dataListSelections.map((item) => { return item.attrGroupId; }); this.$confirm( `确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`, "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", } ).then(() => { this.$http({ url: this.$http.adornUrl("/product/attrgroup/delete"), method: "post", data: this.$http.adornData(ids, false), }).then(({ data }) => { if (data && data.code === 0) { this.$message({ message: "操作成功", type: "success", duration: 1500, onClose: () => { this.getDataList(); }, }); } else { this.$message.error(data.msg); } }); }); }, }, }; </script> <style scoped> </style>
9.2 父子组件传递数据
要实现功能:点击左侧,右侧表格对应内容显示。
父子组件传递数据:category.vue
点击时,引用它的attgroup.vue
能感知到, 然后通知到add-or-update
1、子组件发送事件
-
在
category.vue
中的树形控件绑定点击事件@node-click="nodeclick"
-
node-click方法中有三个参数(data, node, component),data表示当前数据,node为elementui封装的数据
-
点击之后向父组件发送事件:
this.$emit("tree-node-click",...)
…为参数
//组件绑定事件
<el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeclick">
//methods中新增方法
nodeclick(data, node, component){
console.log("子组件categroy的节点被点击:", data,node,component);
this.$emit("tree-node-click", data,node,component); //向父组件发送tree-node-click事件
}
2、父组件接收事件
//引用的组件,可能会发散tree-node-click事件,当接收到时,触发父组件的treenodeclick方法
<category @tree-node-click="treenodeclick"></category>
//methods中新增treenodeclick方法,验证父组件是否接收到
treenodeclick(data, node, component){
console.log("attrgroup感知到category的节点被点击:",data, node, component);
console.log("刚才被点击的菜单名字", data.name);
},
3、启动测试
9.3 获取分类属性分组
1、接口url:
/product/attrgroup/list/{catelogId}
2、修改controller
@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params, @PathVariable Long catelogId){
//PageUtils page = attrGroupService.queryPage(params);
PageUtils page = attrGroupService.queryPage(params, catelogId);
return R.ok().put("page", page);
}
3、service新增接口,实现类新增方法
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
if (catelogId == 0){ //如果传过来的id是0,则查询所有属性
//this.page两个参数,第一个参数是查询页码信息,其中Query.getPage方法传入一个map,会自动封装成IPage
//第二个参数是查询条件,空的wapper就是查询全部
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
new QueryWrapper<AttrGroupEntity>());
return new PageUtils(page);
}else{
String key = (String) params.get("key");
QueryWrapper<AttrGroupEntity> wapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId);
if (!StringUtils.isEmpty(key)){
wapper.and((obj)->{
obj.like("attr_group_name", key).or().eq("attr_group_id", key);
});
}
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wapper);
return new PageUtils(page);
}
}
4、测试
GET http://localhost:88/api/product/attrgroup/list/1?page=1&key=aa
Content-Type: application/json
###
测试结果:
5、修改前端代码
-
修改getDataList()中的请求路径
-
data中新增
catId
-
methods中修改点击方法
treenodeclick(data, node, component) {
//必须是三级分类,才显示属性
if (data.catLevel == 3){
this.catId = data.catId;
this.getDataList();
}
},
6、数据库中新增数据,进行测试
7、进行测试:
9.4 属性分组新增功能
新增时,父id改换成选择框
1、新增选择框,添加菜单数据
- 发现可以选择但是并不显示名称,原因是显示的属性是
label
,通过props
属性进行绑定
<!--v-model 绑定要提交的数据,options绑定选择菜单, props绑定选择框的参数-->
<el-form-item label="所属分类id" prop="catelogId">
<el-cascader v-model="dataForm.catelogIds" :options="categroys" :props="props"></el-cascader>
</el-form-item>
//data中新增属性,props用来绑定选择框的参数,categorys用来保存菜单
props:{
value:"catId",
label:"name",
children:"children"
},
categroys:[],
//方法中新增getCategorys(),用来获取选择菜单的值
getCategorys() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功了获取到菜单数据....", data.data);
this.categroys = data.data;
});
},
//组件创建的时候就要获取菜单的值
created(){
this.getCategorys();
}
2、发现返回的数据,三级菜单下面也有children(为空)解决方法:设置后端,当children为空时,不返回children字段
在children字段上添加注解:当值为空时,不返回当前字段
@JsonInclude(JsonInclude.Include.NON_EMPTY)
3、选择之后报错,原因是:el-cascader绑定的dataForm.catelogId
是一个数组,其中包含选择框的父节点id和自己的id。而我们要提交的只是他自己的id。
//修改data中的dataFrom
dataForm: {
attrGroupId: 0,
attrGroupName: "",
sort: "",
descript: "",
icon: "",
catelogIds: [], //保存父节点和子节点的id
catelogId: 0 //保存要提交的子节点的id
},
//修改表单提交方法,要传送的数据,只传最后一个子id
catelogId: this.dataForm.catelogIds[this.dataForm.catelogIds.length - 1],
测试:
显示效果:
9.5 修改回显分类功能
1、前端新增完整路径
init(id) {
this.dataForm.attrGroupId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.attrGroupId) {
this.$http({
url: this.$http.adornUrl(
`/product/attrgroup/info/${this.dataForm.attrGroupId}`
),
method: "get",
params: this.$http.adornParams(),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
this.dataForm.sort = data.attrGroup.sort;
this.dataForm.descript = data.attrGroup.descript;
this.dataForm.icon = data.attrGroup.icon;
this.dataForm.catelogId = data.attrGroup.catelogId;
//查出catelogId的完整路径
this.dataForm.catelogPath = data.attrGroup.catelogPath;
}
});
}
});
},
2、后端AttrGroupEntity
新增完整路径属性
@TableField(exist = false)
private Long[] catelogPath;
3、修改controller
@Autowired
private CategoryService categoryService;
@RequestMapping("/info/{attrGroupId}")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
Long catelogId = attrGroup.getCatelogId();
//根据id查询完整路径
Long[] path = categoryService.findCatelogPath(catelogId);
attrGroup.setCatelogPath(path);
return R.ok().put("attrGroup", attrGroup);
}
4、修改categoryService,新增接口,实现方法
//categoryService接口
/*
*找到catelogId的完整路径
*/
Long[] findCatelogPath(Long catelogId);
//categoryServiceImpl实现方法
//查找完整路径方法
@Override
public Long[] findCatelogPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
return parentPath.toArray(new Long[parentPath.size()]);
}
//递归查找父节点id
public List<Long> findParentPath(Long catelogId,List<Long> paths){
//1、收集当前节点id
CategoryEntity byId = this.getById(catelogId);
if (byId.getParentCid() != 0){
findParentPath(byId.getParentCid(), paths);
}
paths.add(catelogId);
return paths;
}
5、当对话框关闭时,情况数据,防止不合理回显
<el-dialog
:title="!dataForm.attrGroupId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible"
@closed="dialogClose" //关闭时,绑定方法dialogClose
>
//新增方法
dialogClose(){
this.dataForm.catelogPath = [];
},
6、选择框加上搜索功能:filterable
, 显示提示信息placeholder="试试搜索:手机"
<el-cascader
placeholder="试试搜索:手机"
filterable
v-model="dataForm.catelogPath"
:options="categroys"
:props="props"
></el-cascader>
测试:
9.6 实现分页-引入插件
发现自动生成的分页条不好使,原因是没有引入mybatis-plus的分页插件。新建配置类,引入如下配置
product—>config—>MybatisPlusConfig配置类
@Configuration
@EnableTransactionManagement
@MapperScan("com.xmh.gulimall.product.dao")
public class MybatisPlusConfig {
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
}
10. 关联分类
1、添加数据,把写好的所有前端代码复制到指定目录下
文件资料在工程目录下docs文件夹内
2、小米品牌下面可能包括手机、电器等分类,同样手机分类可能包括小米、华为等多个品牌。所以品牌与分类是多对多的关系。表pms_category_brand_relation
保存品牌与分类的多对多关系
3、查看文档,获取品牌关联的分类: /product/categorybrandrelation/catelog/list
根据传过来的brandId,查找所有的分类信息
/**
* 获取当前品牌关联的所有分类列表
*/
@GetMapping("/catelog/list")
public R catelogList(@RequestParam("brandId") Long brandId){
List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(
new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId)
);
return R.ok().put("data", data);
}
4、新增品牌与分类关联关系:product/categorybrandrelation/save
保存的时候,前端传过来brandid和categoryid,存储的时候还要存brandName和categoryName,所以在保存之前进行查找
controller
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:categorybrandrelation:save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}
service
@Autowired
CategoryDao categoryDao;
@Autowired
BrandDao brandDao;
@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);
}
5、要对品牌(分类)名字进行修改时,品牌分类关系表之中的名字也要进行修改
- 品牌名字修改同时修改表数据
BrandController
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(value = {UpdateGroup.class})@RequestBody BrandEntity brand){
brandService.updateByIdDetail(brand);
return R.ok();
}
BrandServiceImpl
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Transactional
@Override
public void updateByIdDetail(BrandEntity brand) {
//保证冗余字段的数据一致
this.updateById(brand);
if (!StringUtils.isEmpty(brand.getName())){
//如果修改了名字,则品牌分类关系表之中的名字也要修改
categoryBrandRelationService.updateBrand(brand.getBrandId(), brand.getName());
//TODO 更新其他关联
}
}
CategoryBrandRelationServiceImpl
@Override
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
categoryBrandRelationEntity.setBrandName(name);
categoryBrandRelationEntity.setBrandId(brandId);
this.update(categoryBrandRelationEntity, new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
}
- 分类名字修改同时修改表数据
CategoryController
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateCascade(category);
return R.ok();
}
CategroyServiceImpl
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
CategoryBrandRelationServiceImpl
@Override
public void updateCategory(Long catId, String name) {
this.baseMapper.updateCategroy(catId, name);
}
CategoryBrandRelationDao
void updateCategroy(@Param("catId") Long catId,@Param("name") String name);
CateBrandRelationDao.xml
<update id="updateCategroy">
UPDATE `pms_category_brand_relation` SET catelog_name=#{name} WHERE catelog_id=#{catId}
</update>
测试:
- 可以新增关联分类名
- 修改品牌名后关联分类随之改变
- 分类维护中修改分类名后关联分类随之修改
11. 规格参数
11.1 规格参数新增
-
url地址:
/product/attr/save
-
规格参数新增时,请求的URL:Request
-
当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范。比较规范的做法是,新建一个vo文件夹,将每种不同的对象,按照它的功能进行了划分。
-
查看前端返回的数据,发现比数据库中的attr多了attrGroupId字段, 所以新建AttrVo
在数据库gulimall_pms中新添一个字段value_type
{
"attrGroupId": 0, //属性分组id
"attrName": "string",//属性名
"attrType": 0, //属性类型
"catelogId": 0, //分类id
"enable": 0, //是否可用
"icon": "string", //图标
"searchType": 0, //是否检索
"showDesc": 0, //快速展示
"valueSelect": "string", //可选值列表
"valueType": 0 //可选值模式
}
- 查看后端的save方法,只保存的attr,并没有保存attrGroup的信息。所以稍微修改一下。
1)创建Vo
@Data
public class AttrVo extends AttrEntity {
private Long attrGroupId;
}
2)AttrController
@RequestMapping("/save")
public R save(@RequestBody AttrVo attr){
attrService.saveAttr(attr);
return R.ok();
}
- AttrServiceImpl
@Autowired
private AttrAttrgroupRelationDao attrAttrgroupRelationDao;
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
//保存attrEntity
//利用attr的属性给attrEntity的属性赋值,前提是他们俩的属性名一直
BeanUtils.copyProperties(attr, attrEntity);
this.save(attrEntity);
//保存AttrGroupId信息
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
attrAttrgroupRelationDao.insert(relationEntity);
}
测试:
- 数据库字段中成功添加新规格参数信息:
11.2 获取分类规格参数
- url地址:
/product/attr/base/list/{catelogId}
1、新建AttrRespVo
@Data
public class AttrRespVo extends AttrVo {
private String catelogName;
private String groupName;
}
2、AttrController
@RequestMapping("/base/list/{catelogId}")
public R baseList(@RequestParam Map<String, Object> params, @PathVariable("catelogId") Long catelogId){
PageUtils page = attrService.queryBaseAttrPage(params, catelogId);
return R.ok().put("page", page);
}
3、AttrServiceImpl
@Autowired
private CategoryDao categoryDao;
@Autowired
private AttrGroupDao attrGroupDao;
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>();
//模糊查询
if (catelogId != 0){
queryWrapper.eq("catelog_id", catelogId);
}
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
//attr_id attr_name
queryWrapper.and((wrapper) -> {
wrapper.eq("attr_id", key).or().like("attr_name", key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
//封装分页数据
PageUtils pageUtils = new PageUtils(page);
//查出新增的属性
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
CategoryEntity categoryEntity = categoryDao.selectOne(new QueryWrapper<CategoryEntity>().eq("cat_id", attrEntity.getCatelogId()));
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
AttrAttrgroupRelationEntity attrgroupRelationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if (attrgroupRelationEntity != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectOne(new QueryWrapper<AttrGroupEntity>().eq("attr_group_id", attrgroupRelationEntity.getAttrGroupId()));
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
return attrRespVo;
}).collect(Collectors.toList());
// 把新的数据传送过去
pageUtils.setList(respVos);
return pageUtils;
}
测试:
11.3 查询属性详情
- url地址:
/product/attr/info/{attrId}
1、修改AttrRespVo
@Data
public class AttrRespVo extends AttrVo {
private String catelogName;
private String groupName;
private Long[] catelogPath;
}
2、AttrController
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId){
AttrRespVo attrRespVo = attrService.getAttrInfo(attrId);
return R.ok().put("attr", attrRespVo);
}
3、AttrServiceImpl
@Autowired
private CategoryService categoryService;
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo attrRespVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity, attrRespVo);
AttrAttrgroupRelationEntity attrAttrgroupRelation = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if (attrAttrgroupRelation != null){
attrRespVo.setAttrGroupId(attrAttrgroupRelation.getAttrGroupId());
}
Long[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId());
attrRespVo.setCatelogPath(catelogPath);
return attrRespVo;
}
11.4 修改属性
- url地址:
/product/attr/update
1、AttrController
@RequestMapping("/update")
public R update(@RequestBody AttrVo attr){
attrService.updateAttr(attr);
return R.ok();
}
2、AttrServiceImpl
//保存时,要修改两张表
@Transactional
@Override
public void updateAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr, attrEntity);
this.updateById(attrEntity);
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attr.getAttrId());
//判断是新增还是删除
Integer count = attrAttrgroupRelationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
if (count > 0){
attrAttrgroupRelationDao.update(relationEntity, new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
}else{
attrAttrgroupRelationDao.insert(relationEntity);
}
}
测试:
修改后回显:
12. 平台属性
12.1 销售属性
- url地址:
/product/attr/sale/list/{catelogId}
1、可以通过在添加路径变量{attrType}
同时用一个方法查询销售属性和规格参数
注意:销售属性,没有分组信息,所以复用方法的时候,要判断是销售属性还是规格参数
AttrController
@RequestMapping("/{attrType}/list/{catelogId}")
public R baseList(@RequestParam Map<String, Object> params, @PathVariable("catelogId") Long catelogId,
@PathVariable("attrType") String attrType){
PageUtils page = attrService.queryBaseAttrPage(params, catelogId, attrType);
return R.ok().put("page", page);
}
AttrServiceImpl
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String attrType) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type", "base".equalsIgnoreCase(attrType)?1:0);
if (catelogId != 0){
queryWrapper.eq("catelog_id", catelogId);
}
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
//attr_id attr_name
queryWrapper.and((wrapper) -> {
wrapper.eq("attr_id", key).or().like("attr_name", key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
PageUtils pageUtils = new PageUtils(page);
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
if ("base".equalsIgnoreCase(attrType)){
AttrAttrgroupRelationEntity attrgroupRelationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if (attrgroupRelationEntity != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectOne(new QueryWrapper<AttrGroupEntity>().eq("attr_group_id", attrgroupRelationEntity.getAttrGroupId()));
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
CategoryEntity categoryEntity = categoryDao.selectOne(new QueryWrapper<CategoryEntity>().eq("cat_id", attrEntity.getCatelogId()));
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());
// 把新的数据传送过去
pageUtils.setList(respVos);
return pageUtils;
}
2、当新增/修改规格参数时,会在attrAttrGroupRelation表之中新增数据,但是销售属性没有分组信息。所以在新增/修改时,进行判断
在Common中新建类ProductConstant,用来商品服务中的保存常量
public class ProductConstant {
public enum AttrEnum{
ATTR_TYPE_BASE(1,"基本属性"), ATTR_TYPE_SALE(0,"销售属性");
private int code;
private String msg;
AttrEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
}
}
在saveAttr,getAttrInfo,updateAttr
中,设计分组信息之前做判断
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode())//如果是规格参数,再操作分组信息
测试:(新添加一个销售属性)
12.2 获取属性分组的关联的所有属性
- url地址:
/product/attrgroup/{attrgroupId}/attr/relation
attrgroupController
@GetMapping("/{attrgroupId}/attr/relation")
public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){
List<AttrEntity> data = attrService.getRelationAttr(attrgroupId);
return R.ok().put("data", data);
}
attrServiceImpl
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
List<AttrAttrgroupRelationEntity> relationEntities = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
List<Long> attrIds = relationEntities.stream().map((entity) -> {
return entity.getAttrId();
}).collect(Collectors.toList());
List<AttrEntity> attrEntities = this.baseMapper.selectBatchIds(attrIds);
return attrEntities;
}
测试:
12.3 删除属性与分组的关联关系
- url地址:
/product/attrgroup/attr/relation/delete
1、新建AttrGroupRelationVo
@Data
public class AttrGroupRelationVo {
private Long attrId;
private Long attrGroupId;
}
2、AttrGroupController
@PostMapping("/attr/relation/delete")
public R attrRelationDelete(@RequestBody AttrGroupRelationVo[] vos){
attrService.deleteRelation(vos);
return R.ok();
}
3、AttrServiceImpl
@Override
public void deleteRelation(AttrGroupRelationVo[] vos) {
List<AttrGroupRelationVo> relationVos = Arrays.asList(vos);
List<AttrAttrgroupRelationEntity> entities = relationVos.stream().map((relationVo) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(relationVo, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
//根据attrId,attrGroupId批量删除关联关系
attrAttrgroupRelationDao.deleteBatchRelation(entities);
}
4、AttrAttrgroupRelationDao.xml
<delete id="deleteBatchRelation">
DELETE FROM `pms_attr_attrgroup_relation` WHERE
<foreach collection="entities" item="item" separator=" OR ">
(attr_id=#{item.attrId} AND attr_group_id=#{item.attrGroupId})
</foreach>
</delete>
12.4 获取属性分组没有关联的其他属性
- url地址:
/product/attrgroup/{attrgroupId}/noattr/relation
AttrGroupController
@GetMapping("/{attrgroupId}/noattr/relation")
public R attrNoRelation(@PathVariable("attrgroupId") Long attrgroupId, @RequestParam Map<String, Object> params){
PageUtils page = attrService.getNoRelationAttr(params, attrgroupId);
return R.ok().put("page", page);
}
AttrServiceImpl
-
当前分组只能关联自己所属分类里面的所有属性
-
当前分组只能关联别的分组没有引用的属性
- 当前分类下的其他分组
- 这些分组关联的属性
- 从当前分类的所有属性中移除这些属性
/*
*获取当前分组没有关联的所有属性
*@param:[params, attrgroupId]
*@return:com.xmh.common.utils.PageUtils
*@date: 2021/8/9 20:35
*/
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
//1、当前分组只能关联自己所属分类里面的所有属性
//先查询出当前分组所属的分类
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
Long catelogId = attrGroupEntity.getCatelogId();
//2、当前分组只能关联别的分组没有引用的属性
//2.1当前分类下的所有分组
List<AttrGroupEntity> attrGroupEntities = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<Long> attrGroupIds = attrGroupEntities.stream().map(attrGroupEntity1 -> {
return attrGroupEntity1.getAttrGroupId();
}).collect(Collectors.toList());
//2.2这些分组关联的属性
List<AttrAttrgroupRelationEntity> relationEntities = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", attrGroupIds));
List<Long> attrIds = relationEntities.stream().map((relationEntity) -> {
return relationEntity.getAttrId();
}).collect(Collectors.toList());
// 从当前分类的所有属性中移除这些属性
QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>().eq("catelog_id", catelogId).eq("attr_type", ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
if (attrIds != null && attrIds.size() > 0){
wrapper.notIn("attr_id", attrIds);
}
//模糊查询
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
wrapper.and((w)->{
w.eq("attr_id", key).or().like("attr_name", key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
12.5 添加属性与分组关联关系
- url地址:
/product/attrgroup/attr/relation
AttrGroupController
@PostMapping("/attr/relation")
public R attrRelation(@RequestBody List<AttrGroupRelationVo> vos){
relationService.saveBatch(vos);
return R.ok();
}
AttrAttrgroupRelationServiceImpl
@Override
public void saveBatch(List<AttrGroupRelationVo> vos) {
List<AttrAttrgroupRelationEntity> entities = vos.stream().map((vo) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(vo, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
this.saveBatch(entities);
}
13. 新增商品
13.1 调试会员等级相关接口
- url地址:
/member/memberlevel/list
获取所有会员等级(这个方法已经自动生成了,启动会员服务即可)
- 把gulimall-member添加到服务注册中心,然后启动gulimall-member服务
- 配置网关路由
- id: member_route
uri: lb://gulimall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
点击 用户系统-会员等级,进行测试
添加一些数据
13.2 获取分类关联的品牌
-
url地址:
/product/categorybrandrelation/brands/list
-
新增商品时,点击商品的分类,要获取与该分类关联的所有品牌
新建BrandVo
@Data
public class BrandVo {
private Long brandId;
private String brandName;
}
CategoryBrandRelationController
/**
* 获取当前分类关联的所有品牌
* 1、 Controller: 处理请求,接受和校验数据
* 2、Service接受controller传来的数据,进行业务处理
* 3、Controller接受Service处理完的数据,封装成页面指定的vo
*/
@GetMapping("/brands/list")
public R relationBrandsList(@RequestParam(value = "catId", required = true) Long catId){
List<BrandEntity> vos = categoryBrandRelationService.getBrandsByCatId(catId);
List<BrandVo> collect = vos.stream().map(item -> {
BrandVo brandVo = new BrandVo();
brandVo.setBrandId(item.getBrandId());
brandVo.setBrandName(item.getName());
return brandVo;
}).collect(Collectors.toList());
return R.ok().put("data", collect);
}
CategoryBrandRelationServiceImpl
@Autowired
BrandService brandService;
@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
List<CategoryBrandRelationEntity> catelogId = this.baseMapper.selectList(new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId));
List<BrandEntity> collect = catelogId.stream().map(item -> {
Long brandId = item.getBrandId();
BrandEntity byId = brandService.getById(brandId);
return byId;
}).collect(Collectors.toList());
return collect;
}
测试
解决问题:
关于pubsub、publish报错,无法发送查询品牌信息的请求:
- npm install --save pubsub-js
- 在src下的main.js中引用:
① import PubSub from ‘pubsub-js’
② Vue.prototype.PubSub = PubSub
13.3 获取分类下所有分组以及属性
- url地址:
/product/attrgroup/{catelogId}/withattr
新建AttrGroupWithAttrsVo
@Data
public class AttrGroupWithAttrsVo {
/**
* 分组id
*/
private Long attrGroupId;
/**
* 组名
*/
private String attrGroupName;
/**
* 排序
*/
private Integer sort;
/**
* 描述
*/
private String descript;
/**
* 组图标
*/
private String icon;
/**
* 所属分类id
*/
private Long catelogId;
private List<AttrEntity> attrs;
}
AttrGroupController
@GetMapping("/{catelogId}/withattr")
public R getAttrGroupWithAttrs(@PathVariable("catelogId") Long catelogId){
//1、查出当前分类下的所有属性分组
//2、查出每个属性分组的所有属性
List<AttrGroupWithAttrsVo> vos = attrGroupService.getAttrGroupWithAttrsByCatelogId(catelogId);
return R.ok().put("data", vos);
}
AttrGroupServiceImpl
/*
*根据分类id查出所有的分组以及这些分组里面的属性
*@param:[catelogId]
*@return:java.util.List<com.xmh.gulimall.product.vo.AttrGroupWithAttrsVo>
*@date: 2021/8/16 21:13
*/
@Override
public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrsByCatelogId(Long catelogId) {
List<AttrGroupEntity> attrGroupEntities = this.baseMapper.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<AttrGroupWithAttrsVo> collect = attrGroupEntities.stream().map(item -> {
AttrGroupWithAttrsVo attrsVo = new AttrGroupWithAttrsVo();
BeanUtils.copyProperties(item, attrsVo);
List<AttrEntity> relationAttr = attrService.getRelationAttr(item.getAttrGroupId());
attrsVo.setAttrs(relationAttr);
return attrsVo;
}).collect(Collectors.toList());
return collect;
}
测试
13.4 新增商品
- url地址:
/product/spuinfo/save
1、按照视频添加测试数据,复制要提交的json
2、生成SpuSaveVo
-
json格式化工具:
https://www.bejson.com/
,json生成java类:https://www.bejson.com/json2javapojo/new/
-
利用json生成
SpuSaveVo
,生成代码,复制到vo包下 -
微调vo,把所有id字段改成Long类型,把所有double类型改成BigDecimal类型
-
真实项目要加上数据校验
3、需要保存的东西
-
保存spu基本信息
pms_spu_info
-
保存spu的描述图片
pms_spu_info_desc
-
保存spu的图片集
pms_spu_images
-
保存spu的规格参数
pms_product_attr_value
-
保存spu的积分信息
gulimall_sms
->sms_spu_bounds
-
保存spu对应的所有sku信息
- sku的基本信息
pms_sku_info
- sku的图片信息
pms_sku_images
- sku的销售属性信息
pms_sku_sale_attr_value
- sku的优惠、满减等信息
gulimall_sms
->sms_sku_ladder
/sms_sku_full_reduction
/sms_member_price
- sku的基本信息
4、具体实现
SpuInfoController
@RequestMapping("/info/{id}")
//@RequiresPermissions("product:spuinfo:info")
public R info(@PathVariable("id") Long id){
SpuInfoEntity spuInfo = spuInfoService.getById(id);
return R.ok().put("spuInfo", spuInfo);
}
SpuInfoServiceImpl
@Autowired
SkuInfoService skuInfoService;
@Autowired
SkuImagesService skuImagesService;
@Autowired
SkuSaleAttrValueService skuSaleAttrValueService;
@Autowired
CouponFeignService couponFeignService;
@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
//1、保存spu基本信息`pms_spu_info`
SpuInfoEntity infoEntity = new SpuInfoEntity();
BeanUtils.copyProperties(vo, infoEntity);
infoEntity.setCreateTime(new Date());
infoEntity.setUpdateTime(new Date());
this.save(infoEntity);
//2、保存spu的描述图片`pms_spu_info_desc`
List<String> decript = vo.getDecript();
SpuInfoDescEntity descEntity = new SpuInfoDescEntity();
descEntity.setSpuId(infoEntity.getId());
descEntity.setDecript(String.join(",", decript));
spuInfoDescService.save(descEntity);
//3、保存spu的图片集`pms_spu_images`
List<String> images = vo.getImages();
if (images != null && images.size() != 0) {
List<SpuImagesEntity> collect = images.stream().map(image -> {
SpuImagesEntity imagesEntity = new SpuImagesEntity();
imagesEntity.setSpuId(infoEntity.getId());
imagesEntity.setImgUrl(image);
return imagesEntity;
}).collect(Collectors.toList());
imagesService.saveBatch(collect);
}
//4、保存spu的规格参数`pms_product_attr_value`
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> productAttrValueEntityList = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
valueEntity.setAttrId(attr.getAttrId());
AttrEntity byId = attrService.getById(attr.getAttrId());
valueEntity.setAttrName(byId.getAttrName());
valueEntity.setAttrValue(attr.getAttrValues());
valueEntity.setQuickShow(attr.getShowDesc());
valueEntity.setSpuId(infoEntity.getId());
return valueEntity;
}).collect(Collectors.toList());
productAttrValueService.saveBatch(productAttrValueEntityList);
//5、保存spu的积分信息`gulimall_sms`->`sms_spu_bounds`
Bounds bounds = vo.getBounds();
SpuBoundTo spuBoundTo = new SpuBoundTo();
BeanUtils.copyProperties(bounds, spuBoundTo);
spuBoundTo.setSpuId(infoEntity.getId());
R r = couponFeignService.saveSpuBounds(spuBoundTo);
if (r.getCode() != 0){
log.error("远程保存spu积分信息失败");
}
//6、保存spu对应的所有sku信息
List<Skus> skus = vo.getSkus();
if (skus != null && skus.size() > 0) {
skus.forEach(item -> {
String defaultImg = "";
//查找出默认图片
for (Images image : item.getImages()) {
if (image.getDefaultImg() == 1) {
defaultImg = image.getImgUrl();
}
}
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtils.copyProperties(item, skuInfoEntity);
skuInfoEntity.setSpuId(infoEntity.getId());
skuInfoEntity.setBrandId(infoEntity.getBrandId());
skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSkuDefaultImg(defaultImg);
//6.1、sku的基本信息`pms_sku_info`
skuInfoService.save(skuInfoEntity);
//6.2、sku的图片信息`pms_sku_images`
Long skuId = skuInfoEntity.getSkuId();
List<SkuImagesEntity> skuImagesEntities = item.getImages().stream().map(img -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(img.getImgUrl());
skuImagesEntity.setDefaultImg(img.getDefaultImg());
return skuImagesEntity;
}).collect(Collectors.toList());
skuImagesService.saveBatch(skuImagesEntities);
//6.3、sku的销售属性信息`pms_sku_sale_attr_value`
List<Attr> attr = item.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> {
SkuSaleAttrValueEntity skuSaleAttrValueEntity = new SkuSaleAttrValueEntity();
skuSaleAttrValueEntity.setSkuId(skuId);
BeanUtils.copyProperties(a, skuSaleAttrValueEntity);
return skuSaleAttrValueEntity;
}).collect(Collectors.toList());
skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
//6.4、sku的优惠、满减等信息`gulimall_sms`->`sms_sku_ladder`/`sms_sku_full_reduction`/`sms_member_price`
SkuReductionTo skuReductionTo = new SkuReductionTo();
BeanUtils.copyProperties(item, skuReductionTo);
skuReductionTo.setSkuId(infoEntity.getId());
R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
if (r1.getCode() != 0){
log.error("远程保存优惠信息失败");
}
});
}
}
在common
中新建SkuReductionTo
用来做远程调用
@Data
public class SkuReductionTo {
private Long skuId;
private int fullCount;
private BigDecimal discount;
private int countStatus;
private BigDecimal fullPrice;
private BigDecimal reducePrice;
private int priceStatus;
private List<MemberPrice> memberPrice;
}
在common
中新建MemberPrice
@Data
public class MemberPrice {
private Long id;
private String name;
private BigDecimal price;
}
在product
包中新建fegin.CouponFeignService
用来远程调用Coupon服务
一共调用了两个服务"coupon/spubounds/save"
和"coupon/skufullreduction/saveInfo"
第一个服务自动生成,直接调用即可
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
*1、CouponFeginService.saveSpuBounds(spuBoudnTo);
* 1)、@RequestBody 将这个对象转化为json
* 2)、找到gulimall-coupon服务,给coupon/spubounds/save发送请求
* 将上一步转的json放在请求体位置,发送数据
* 3)、对方服务接受请求,请求体里面有json数据
* public R save(@RequestBody SpuBoundsEntity spuBounds);
* 将请求体的json转化为SpuBoundsEntity;
* 只要json数据模型是兼容的。双方无需使用同一个to
*@param:[spuBoundTo]
*@return:com.xmh.common.utils.R
*@date: 2021/8/18 1:28
*/
@PostMapping("coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
@PostMapping("coupon/skufullreduction/saveInfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
第二个服务在SkuFullReductionController
中新建方法
@PostMapping("/saveInfo")
public R saveInfo(@RequestBody SkuReductionTo skuReductionTo){
skuFullReductionService.saveSkuReduction(skuReductionTo);
return R.ok();
}
在SkuFullReductionServiceImpl
中实现
public void saveSkuReduction(SkuReductionTo skuReductionTo) {
//1、保存满减打折,会员价 sms_sku_ladder
SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
skuLadderEntity.setSkuId(skuReductionTo.getSkuId());
skuLadderEntity.setAddOther(skuReductionTo.getCountStatus());
skuLadderEntity.setFullCount(skuReductionTo.getFullCount());
skuLadderEntity.setDiscount(skuReductionTo.getDiscount());
skuLadderService.save(skuLadderEntity);
//2、保存满减信息 sms_sku_full_reduction
SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
skuFullReductionEntity.setSkuId(skuReductionTo.getSkuId());
skuFullReductionEntity.setFullPrice(skuReductionTo.getFullPrice());
skuFullReductionEntity.setReducePrice(skuReductionTo.getReducePrice());
skuFullReductionEntity.setAddOther(skuReductionTo.getPriceStatus());
this.save(skuFullReductionEntity);
//3、保存会员价格 sms_member_price
List<MemberPrice> memberPrice = skuReductionTo.getMemberPrice();
List<MemberPriceEntity> collect = memberPrice.stream().map(item -> {
MemberPriceEntity memberPriceEntity = new MemberPriceEntity();
memberPriceEntity.setSkuId(skuReductionTo.getSkuId());
memberPriceEntity.setMemberLevelId(item.getId());
memberPriceEntity.setMemberPrice(item.getPrice());
memberPriceEntity.setMemberLevelName(item.getName());
memberPriceEntity.setAddOther(1);
return memberPriceEntity;
}).collect(Collectors.toList());
memberPriceService.saveBatch(collect);
}
最后在R
之中添加getCode方法,方便判断远程调用是否成功
public Integer getCode(){
return Integer.parseInt((String) this.get("code"));
}
13.5 内存调优
1、新建Compound
2、把服务添加到新建的compound里
3、设置每个项目最大占用内存为100M
13.6 商品保存debug
1、由于函数是个事务,而数据库默认读出已经提交了的数据,所以用如下命令设置隔离级别,以方便查看数据库变化
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
2、出现问题SpuInfoDescEntity
,mybatis默认主键为自增的,而SpuInfoDescEntity
中的主键为自己输入的,所以修改主键注释
3、抛出异常,修改R
中的getCode方法
public Integer getCode(){
return (Integer) this.get("code");
}
4、出现问题,保存sku图片时,有些图片是没有路径的,没有路径的图片,无需保存。
在收集图片的时候进行过滤
List<SkuImagesEntity> skuImagesEntities = item.getImages().stream().map(img -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(img.getImgUrl());
skuImagesEntity.setDefaultImg(img.getDefaultImg());
return skuImagesEntity;
}).filter(entity -> {
//返回true是需要,返回false是过滤掉
return !StringUtils.isNullOrEmpty(entity.getImgUrl());
}).collect(Collectors.toList());
skuImagesService.saveBatch(skuImagesEntities);
5、保存折扣信息的时候,满0元打0折这种都是无意义的,要过滤掉
解决方法:在保存之前做判断,过滤掉小于等于0的无意义信息(不贴代码了),要注意的是判断BigDecimal进行判断时,要用compareTo函数。
例:
if (skuReductionTo.getFullCount() > 0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
if (r1.getCode() != 0){
log.error("远程保存优惠信息失败");
}
}
14. 商品管理
14.1 SPU检索
- url地址:
/product/spuinfo/list
1、SpuInfoController.java
@RequestMapping("/list")
//@RequiresPermissions("product:spuinfo:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = spuInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
2、SpuInfoServiceImpl
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isNullOrEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)){
wrapper.eq("catalog_id", catelogId);
}
String brandId = (String) params.get("brandId");
if (!StringUtils.isNullOrEmpty(brandId) && !"0".equalsIgnoreCase(brandId)){
wrapper.eq("brand_id", brandId);
}
String status = (String) params.get("status");
if (!StringUtils.isNullOrEmpty(status)){
wrapper.eq("publish_status", status);
}
String key = (String) params.get("key");
if (!StringUtils.isNullOrEmpty(key)){
wrapper.and((w) -> {
w.eq("id", key).or().like("spu_name", key);
});
}
IPage<SpuInfoEntity> page = this.page(
new Query<SpuInfoEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
3、测试成功
4、发现时间格式不对,对时间进行格式化
在product配置文件中新增配置:
表示对json中的所有时间数据进行格式化,重启测试,测试成功:
14.2 SKU检索
- url地址:
/product/skuinfo/list
商品系統
1、SkuInfoController
@RequestMapping("/list")
//@RequiresPermissions("product:skuinfo:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = skuInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
2、SkuInfoServiceImpl
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isNullOrEmpty(key)){
wrapper.and((w) -> {
w.eq("sku_id", key).or().like("sku_name", key);
});
}
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isNullOrEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)){
wrapper.eq("catalog_id", catelogId);
}
String brandId = (String) params.get("brandId");
if (!StringUtils.isNullOrEmpty(brandId) && !"0".equalsIgnoreCase(brandId)){
wrapper.eq("brand_id", brandId);
}
String min = (String) params.get("min");
if (!StringUtils.isNullOrEmpty(min) && !"0".equalsIgnoreCase(min)){
wrapper.ge("price", min);
}
String max = (String) params.get("max");
if (!StringUtils.isNullOrEmpty(max) && !"0".equalsIgnoreCase(max)){
wrapper.le("price", max);
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
QueryWrapper<SkuInfoEntity> wrapper = new QueryWrapper<>();
wrapper.eq("spu_id", spuId);
List<SkuInfoEntity> skuInfoEntities = baseMapper.selectList(wrapper);
return skuInfoEntities;
}
测试:
15. 仓库管理
15.1 数据表的说明
用到gulimall_wms数据库中的的两张表,第一张是wms_ware_info
,表示有几个仓库
第二张表是wms_ware_sku
,表每个仓库有几个sku商品
15.2 整合仓库服务
1、要整合仓库服务,首先把仓库服务注册到nacos中
2、配置网关
- id: ware_route
uri: lb://gulimall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
3、配置后测试仓库维护
4、实现仓库模糊查询功能
点击查询,查看url
http://localhost:88/api/ware/wareinfo/list?t=1633696575331&page=1&limit=10&key=
WareInfoController.java
@RequestMapping("/list")
//@RequiresPermissions("ware:wareinfo:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = wareInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
WareInfoServiceImpl.java
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<WareInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isNullOrEmpty(key)){
wrapper.eq("id", key).or().like("name", key).or().like("address", key).or().like("areacode", key);
}
IPage<WareInfoEntity> page = this.page(
new Query<WareInfoEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
设置日志输出级别,方便查看sql语句
logging:
level:
com.xmh: debug
测试成功
15.3 查询库存的模糊查询
- url地址:
/ware/waresku/list
库存系统
1、实现库存模糊查询功能,WareSkuServiceImpl.java
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>();
String wareId = (String) params.get("wareId");
if (!StringUtils.isNullOrEmpty(wareId)){
wrapper.eq("ware_id", wareId);
}
String skuId = (String) params.get("skuId");
if (!StringUtils.isNullOrEmpty(skuId)){
wrapper.eq("sku_id", skuId);
}
IPage<WareSkuEntity> page = this.page(
new Query<WareSkuEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
15.4 采购需求的模糊查询
- url地址:
/ware/purchasedetail/list
库存系统
1、PurchaseDetailServiceImpl.java
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<PurchaseDetailEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isNullOrEmpty(key)){
wrapper.and(w -> {
w.eq("sku_id", key).or().eq("purchase_id", key);
});
}
String status = (String) params.get("status");
if (!StringUtils.isNullOrEmpty(status)){
wrapper.eq("status", status);
}
String wareId = (String) params.get("wareId");
if (!StringUtils.isNullOrEmpty(wareId)){
wrapper.eq("ware_id", wareId);
}
IPage<PurchaseDetailEntity> page = this.page(
new Query<PurchaseDetailEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
15.5 合并采购流程
1、采购逻辑,新建采购需求后还要可以提供合并采购单,比如一个仓库的东西可以合并到一起,让采购人员一趟采购完
新建采购需求后还要可以提供合并采购单,比如一个仓库的东西可以合并到一起,让采购人员一趟采购完
新建采购单,可以在采购单后面分配给员工,员工可以在系统管理->管理员列表中新建
15.6 查询未领取的采购单
- url地址:
/ware/purchase/unreceive/list
库存系统
查询未领取的采购单
1、PurchaseController.java
@RequestMapping("/unreceive/list")
//@RequiresPermissions("ware:purchase:list")
public R unreceiveList(@RequestParam Map<String, Object> params){
PageUtils page = purchaseService.queryPageUnreceive(params);
return R.ok().put("page", page);
}
2、新建常量枚举类constant.WareConstant
public class WareConstant {
/** 采购单状态枚举 */
public enum PurchaseStatusEnum{
CREATED(0,"新建"),ASSIGNED(1,"已分配"),
RECEIVE(2,"已领取"),FINISH(3,"已完成"),
HASERROR(4,"有异常");
private int code;
private String msg;
PurchaseStatusEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode(){
return code;
}
public String getMsg(){
return msg;
}
}
/** 采购需求枚举 */
public enum PurchaseDetailStatusEnum{
CREATED(0,"新建"),ASSIGNED(1,"已分配"),
BUYING(2,"正在采购"),FINISH(3,"已完成"),
HASERROR(4,"采购失败");
private int code;
private String msg;
PurchaseDetailStatusEnum(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}
3、queryPageUnreceive.java
@Override
public PageUtils queryPageUnreceive(Map<String, Object> params) {
QueryWrapper<PurchaseEntity> wrapper = new QueryWrapper<>();
wrapper.eq("status", WareConstant.PurchaseStatusEnum.CREATED.getCode()).or().eq("status", WareConstant.PurchaseStatusEnum.ASSIGNED.getCode());
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
测试成功
15.7 合并采购需求
- url地址:
/ware/purchase/merge
库存系统
选择要合并的采购需求,然后合并到整单
如果不选择整单id,则自动创建新的采购单
1、新建MergerVo.java
@Data
public class MergerVo {
private Long purchaseId; //整单id
private List<Long> items; //合并项集合
}
2、分配,就是修改【采购需求】里对应的【采购单id、采购需求状态】,即purchase_detail表
并且不能重复分配采购需求给不同的采购单
,如果还没去采购,或者采购失败,就可以修改
PurchaseController.java
@PostMapping("/merge")
//@RequiresPermissions("ware:purchase:list")
public R merge(@RequestBody MergeVo mergeVo){
purchaseService.mergePurchase(mergeVo);
return R.ok();
}
PurchaseServiceImpl.java
@Autowired
private PurchaseDetailService detailService;
@Transactional
@Override
public void mergePurchase(MergeVo mergeVo) {
Long purchaseId = mergeVo.getPurchaseId();
// 如果采购id为null 说明没选采购单
if (purchaseId == null){
//新建采购单
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());
this.save(purchaseEntity);
purchaseId = purchaseEntity.getId();
}
//合并采购需求
List<Long> items = mergeVo.getItems();
Long finalPurchaseId = purchaseId;
List<PurchaseDetailEntity> list = detailService.getBaseMapper().selectBatchIds(items).stream().filter(entity -> {
//如果还没去采购,或者采购失败,就可以修改
return entity.getStatus() < WareConstant.PurchaseDetailStatusEnum.BUYING.getCode()
|| entity.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode();
}).map(entity -> {
//修改状态,以及采购单id
entity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());
entity.setPurchaseId(finalPurchaseId);
return entity;
}).collect(Collectors.toList());
detailService.updateBatchById(list);
}
对采购单的创建时间、更新时间进行自动填充
在PurchaseEntity
中添加注解
/**
* 创建日期
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新日期
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
新建MyMetaObjectHandler
对注解进行处理
@Slf4j
@Component // 一定不要忘记把处理器加到IOC容器中!
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill.....");
// setFieldValByName(String fieldName, Object fieldVal, MetaObject
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
// 更新时的填充策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill.....");
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
在配置文件中对时间json进行格式化
jackson:
date-format: yyyy-MM-dd HH:mm:ss
测试成功!
15.8 领取采购单
采购单分配给了采购人员,采购人员在手机端领取采购单,此时的采购单应该为新建
或已分配
状态,在采购人员领取后采购单
的状态变为已领取
,采购需求
的状态变为正在采购
- url地址:
/ware/purchase/received
库存系统
1、PurchaseController.java
/**
* 领取采购单/ware/purchase/received
*/
@PostMapping("/received")
//@RequiresPermissions("ware:purchase:list")
public R received(@RequestBody List<Long> ids){
purchaseService.received(ids);
return R.ok();
}
2、PurchaseServiceImpl.java
@Transactional
@Override
public void received(List<Long> ids) {
// 没有采购需求直接返回,否则会破坏采购单
if (ids == null || ids.size() == 0) {
return;
}
List<PurchaseEntity> list = this.getBaseMapper().selectBatchIds(ids).stream().filter(entity -> {
//确保采购单的状态是新建或者已分配
return entity.getStatus() <= WareConstant.PurchaseStatusEnum.ASSIGNED.getCode();
}).map(entity -> {
//修改采购单的状态为已领取
entity.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());
return entity;
}).collect(Collectors.toList());
this.updateBatchById(list);
//修改该采购单下的所有采购需求的状态为正在采购
UpdateWrapper<PurchaseDetailEntity> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("purchase_id", ids);
PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());
detailService.update(purchaseDetailEntity, updateWrapper);
}
3、用idea自带的HTTP Client发送post请求,模拟采购人员领取采购单,进行测试
POST http://localhost:88/api//ware/purchase/received
Content-Type: application/json
[3]
4、测试成功!
15.9 完成采购
完成采购的步骤:
- 判断所有采购需求的状态,采购需求全部完成时,采购单状态才为完成
- 采购项完成的时候,增加库存(调用远程获取skuName)
- 加上分页插件
- url地址:
/ware/purchase/done
库存系统
1、新建PurchaseItemDoneVo
,PurchaseDoneVo
@Data
public class PurchaseItemDoneVo {
private Long itemId;
private Integer status;
private String reason;
}
@Data
public class PurchaseDoneVo {
private Long id;
private List<PurchaseItemDoneVo> items;
}
2、PurchaseController.java
/**
* 完成采购
*/
@PostMapping("/done")
//@RequiresPermissions("ware:purchase:list")
public R received(@RequestBody PurchaseDoneVo vo){
purchaseService.done(vo);
return R.ok();
}
3、PurchaseServiceImpl.java
@Autowired
private WareSkuService wareSkuService;
@Autowired
private ProductFeignService productFeignService;
@Override
public void done(PurchaseDoneVo vo) {
//1、根据前端发过来的信息,更新采购需求的状态
List<PurchaseItemDoneVo> items = vo.getItems();
List<PurchaseDetailEntity> updateList = new ArrayList<>();
boolean flag = true;
for (PurchaseItemDoneVo item : items){
Long detailId = item.getItemId();
PurchaseDetailEntity detailEntity = detailService.getById(detailId);
detailEntity.setStatus(item.getStatus());
//采购需求失败
if (item.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()){
flag = false;
}else {
//3、根据采购需求的状态,更新库存
// sku_id, sku_num, ware_id
// sku_id, ware_id, stock sku_name(调用远程服务获取), stock_locked(先获取已经有的库存,再加上新购买的数量)
String skuName = "";
try {
R info = productFeignService.info(detailEntity.getSkuId());
if(info.getCode() == 0){
Map<String,Object> data=(Map<String,Object>)info.get("skuInfo");
skuName = (String) data.get("skuName");
}
} catch (Exception e) {
}
//更新库存
wareSkuService.addStock(detailEntity.getSkuId(), detailEntity.getWareId(), skuName, detailEntity.getSkuNum());
}
updateList.add(detailEntity);
}
//保存采购需求
detailService.updateBatchById(updateList);
//2、根据采购需求的状态,更新采购单的状态
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(vo.getId());
purchaseEntity.setStatus(flag ? WareConstant.PurchaseStatusEnum.FINISH.getCode() : WareConstant.PurchaseStatusEnum.HASERROR.getCode());
this.updateById(purchaseEntity);
}
4、新建feign.ProductFeignService
接口,用来远程获取skuName
ProductFeignService.java
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R info(@PathVariable("skuId") Long skuId);
}
5、主启动类加上注解@EnableFeignClients
6、WareSkuServiceImpl.java
实现入库操作
@Override
public void addStock(Long skuId, Long wareId, String skuName, Integer skuNum) {
WareSkuEntity wareSkuEntity = this.baseMapper.selectOne(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
if (wareSkuEntity == null){
//新增
wareSkuEntity = new WareSkuEntity();
wareSkuEntity.setStock(skuNum);
}else {
wareSkuEntity.setStock(wareSkuEntity.getStock() + skuNum);
}
wareSkuEntity.setSkuName(skuName);
wareSkuEntity.setWareId(wareId);
wareSkuEntity.setSkuId(skuId);
this.saveOrUpdate(wareSkuEntity);
}
7、添加分页插件,复制product服务中的即可
测试
POST http://localhost:88/api/ware/purchase/done
Content-Type: application/json
{
"id": 7,
"items": [
{"itemId":6,"status":3,"reason":"完成"},
{"itemId":7,"status":3,"reason":"完成"}
]
}
测试成功!
15.10 获取spu规格
- url地址:
/product/attr/base/listforspu/{spuId}
商品系统
1、AttrController.java
@Autowired
private ProductAttrValueService productAttrValueService;
@GetMapping("/base/listforspu/{spuId}")
public R baseListforspu(@PathVariable("spuId") Long spuId){
List<ProductAttrValueEntity> entityList = productAttrValueService.baseAttrlistForSpu(spuId);
return R.ok().put("data", entityList);
}
2、ProductAttrValueServiceImpl.java
@Override
public List<ProductAttrValueEntity> baseAttrlistForSpu(Long spuId) {
List<ProductAttrValueEntity> entities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
return entities;
}
测试,点击规格
修改规格参数后成功回显!
如果此时出现400页面,向gulimall_admin中的sys_menu表中添加一条数据即可
INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);
15.11 修改商品规格
- url地址:
/product/attr/update/{spuId}
商品系统
1、AttrController.java
@PostMapping("/update/{spuId}")
public R updateSpuAttr(@PathVariable("spuId") Long spuId, @RequestBody List<ProductAttrValueEntity> entities){
productAttrValueService.updateSpuAttr(spuId, entities);
return R.ok();
}
2、ProductAttrValueServiceImpl.java
因为修改的时候,有新增有修改有删除。 所以就先把spuId对应的所有属性都删了,再新增
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
//1、删除这个spuId对应的所有属性
this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
//2、新增回去
for (ProductAttrValueEntity entity : entities){
entity.setSpuId(spuId);
}
this.saveBatch(entities);
}
16.基础篇总结
如果本文章对你有帮助,请留下一个小小的赞,谢谢!!!