分布式电商项目-基础篇
center-sept Lv2

docker环境搭建

todo

docker 通用命令集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#查看容器进程
docker ps -a
docker ps

#启动容器
docker start $容器id

#停止容器
docker stop $容器id

#停止所有的容器
docker stop $(docker ps -a -q)

#删除容器
docker rm -f $容器id

#删除所有的容器(只删除单个时把后面的变量改为image id即可)
docker rm $(docker ps -a -q)

#删除指定id的镜像
docker rmi <image id>

#想要删除untagged images,也就是那些id为的image的话可以用

docker rmi $(docker images | grep "^" | awk "{print $3}")

#设置创建好的容器 开机自动启动
docker update $容器id --restart=always

#讲自己配置好的容器重新生成自己的镜像 上传到Docker Hub 上
docker commit $容器id centersept0915/test:redistest
#登录docker hub 账号
docker login
#你的用户名
Username: centersept0915
#你的密码
Password:
#讲生成好的镜像上传到hub 上
docker push centersept0915/test:redistest
#拉取镜像
docker pull centersept0915/test:redistest

docker 安装 mysql

1
docker run -p 3306:3306 --name mysql  -v /mydata/mysql/log:/var/log/mysql  -v /mydata/mysql/data:/var/lib/mysql  -v /mydata/mysql/conf:/etc/mysql  -e MYSQL_ROOT_PASSWORD=root  -d mysql:5.7

命令解释:todo

使用mysql客户端工具连接服务器

账户名:root 密码: root

docker 安装以及操作 redis

1
2
3
4
5
6
#创建redis 容器
docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf -d redis redis-server /etc/redis/redis.conf

#运行redis 客户端
docker exec -it redis redis-cli

可以下载redis的客户端(redis desktop manageer)

[redis desktop manageer]: https://redisdesktop.com/ “redis 桌面管理器”

进行界面操作

Maven

重新编辑 maven 的 setting.xml ,添加一下配置到文件中

1
2
3
4
5
6
7
8
9
10
#配置阿里云镜像
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirror0f>central</mirror0f>mirror0f>
<name>Nexus aliyun</name>
<utl>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
配置jdk1.8编译项目
<profiles>
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdl>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1 8</maven.compiler.target>
<maven.compiler.compilerVersion> 1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>

IDEA & VSCode

idea 配置 (后端开发工具)

1.安装lombok插件

快速生成类的构造函数,get/set 方法等

2.安装mybatisx插件

快速定位mapper 文件

VSCode 配置 (前端开发工具)

安装插件

1.Auto Close Tag

自动添加HTML/XML关闭标签

2.Auto Rename Tag

自动重命名成对的HTML/XML标签

3.Chinese (Simplified) Language Pack for Visual Studio Code插件

汉化插件

4.ESLint

检查ES 语法

5.HTML CSS Support

6.HTML Snippets

7.JavaScript (ES6) code snippets

ES 语法提示

8.Live Server

启动一个开发本地服务器与动态页面动态重新加载功能

9.open in browser

这允许您在默认浏览器或应用程序中打开当前文件。

10.Vetur

Vue 语法支持 ,Vue 工具

配置git-ssh

1.在电脑上安装git

todo

2.配置git的基本用户信息

1
2
3
4
5
#全局配置git的用户名
git config --global user.name "your name"
#全局配置git的邮箱
git config --global user.email "your email"

3.配置ssh免登录

在控制台生成个人 ssh 私钥和公钥

1
2
#生成私钥和公钥 连续三次回车确定
ssh-keygen -t rsa -C "your email"

执行完成之后,目录下会生成 id_rsa (私钥)和 id_rsa.pub(公钥)两个文件。

然后登录gitee(码云) 或者 github ,在设置中找到SSH KEY配置,将 id_rsa.pub 里的内容复制粘贴到里面去。

最后 使用

1
2
3
4
#码云测试是否添加成功
ssh-T git@gitee.com
#github测试是否添加成功
ssh-T git@github.com

idea 搭建 后端服务项目环境

导入码云上的项目

复制仓库url,在idea新建选择 new > Project from Version Control .. > git 粘贴在URL 中,选择相应的工作目录。

如果没有配置gitee账号,需要自行登录。

在项目上新建相应的微服务项目

  1. 商品服务(product)
  2. 仓储服务(ware)
  3. 订单服务(order)
  4. 优惠卷服务(coupon)
  5. 用户服务(member)

创建微服务模块

1.右键主项目 > New > Module.. > Spring initializr 输入相关信息

2.每个服务统一包名和模块名称

com.组织名称.项目名称.模块名称….

3.选择微服务必要的maven 依赖

Spring Web

Spring Web Service

OpenFeign 用于微服务之间的互相调用

4.创建完成微服务模块,在父目录下 新建一个 pom 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<modelVersion>4.0.0</modelVersion>
<groupId>com.centersept.supermall</groupId>
<artifactId>super-mall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>super-mall</name>
<description>baba-project</description>
<packaging>pom</packaging>
<modules>
<module>super-mall-coupon</module>
<module>super-mall-member</module>
<module>super-mall-order</module>
<module>super-mall-product</module>
<module>super-mall-ware</module>
</modules>

把编辑好的父Pom 文件,添加到 Idea maven 插件 管理中,方便打包部署。

编辑.gitignore

编辑这个文件可以 忽略提交这些文件,在这个文件中追加如下内容:

1
2
3
4
5
6
**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore

最后提交代码到码云,安装码云插件(gitee)。

数据库 搭建配置

1.安装 Power Designer 建模软件

[]: http://forspeed.rbread01.cn/down/powerdesigner1029.zip

注:表不做外键关联,因为数据量太大,外键会浪费数据库的性能。

2.根据业务进行数据建模,然后生成相应的 sql 语句

3.点击菜单栏 Database > generate Database > Preview , 查看生成的 sql 语句

4.每个服务模块建立一个数据库,然后分别执行相应的业务sql,创建表结构。

搭建后台管理系统

人人开源项目

注:在 git 搜索人人开源,使用别人的构建好的脚手架,进行快速开发

renren-fast 和 renren-fast-vue 能快速构建,后台管理系统。

renren-generator 快速生成代码层

引入后台项目中

1.打开git bash ,执行,克隆项目到文件夹中

1
2
3
git clone https://gitee.com/renrenio/renren-fast.git 

git clone https://gitee.com/renrenio/renren-fast-vue.git

2.删除克隆的项目中的 .git 文件夹,然后 renren-fast 复制整个文件夹到你的项目中,然后更改 父Pom 加上 该模块,更改后的配置为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<modelVersion>4.0.0</modelVersion>
<groupId>com.centersept.supermall</groupId>
<artifactId>super-mall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>super-mall</name>
<description>baba-project</description>
<packaging>pom</packaging>
<modules>
<module>super-mall-coupon</module>
<module>super-mall-member</module>
<module>super-mall-order</module>
<module>super-mall-product</module>
<module>renren-fast</module>
<module></module>
</modules>

3.创建相应的数据库和表,执行项目中db文件夹下的sql文件(根据数据库选择相应的语句),执行完成之后,可以更改项目配置文件夹中关于db数据源的配置,然后启动项目。

构建前端项目

1.安装 Node.js

todo

2.配置好node,打开前端项目下的终端,执行

1
2
3
4
# 安装包文件依赖
npm install
# 启动前端项目
npm run dev

2.1 安装中出现了错误

1.node-gyp 这个包需要安装 python 作为依赖 所以我们需要安装python,在管理员powershell 中执行以下语句:

Error: Can’t find Python executable “python”, you can set the PYTHON env variable.

方法一:

1
2
3
4
#安装windows-build-tools
npm install --global --production windows-build-tools
#安装node-gyp
npm install --global node-gyp

方法二:

使用管理员运行VSCode 再执行 npm install 命令

注:node-sass 安装报错

node-sass是一个项目依赖,在一个项目中在使用sass语法的时候,必须通过sass-loader来解析sass,从而使sass语法变成浏览器能够识别的CSS语法,而node-sass模块就是对sass-loader的支持模块,所以不安装node-sass,sass-loader就不能正常工作

要考虑到 node-sass 和 node.js 的版本兼容性

参考:https://www.npmjs.com/package/node-sass

3.成功之后,浏览器打开 localhost:8001,账号:admin 密码:admin 登录.(没验证码,一定是没有启动后端服务)

逆向工程搭建&使用

1.克隆项目下来

1
git clone https://gitee.com/renrenio/renren-generator.git

2.删除.git 配置 ,然后把文件复制到后台工程目录下,修改pom 文件,引入该项目。

3.修改逆向工程的application.yml 配置文件的数据源配置,修改为自己的

4.修改generator.properties 生成代码的基本配置,主要修改以下配置

1
2
3
4
5
6
7
8
9
10
11
mainPath=com.centersept.supermall
#包名
package=com.centersept.supermall
#需要生成的模块
moduleName=product
#作者
author=center_sept
#Email
email=253079439@qq.com
#表前缀(类名不会包含表前缀)
tablePrefix=pms_

5.直接启动逆向工程,使用浏览器直接输入localhost 访问自动生成代码系统。

5.1 进入菜单 renren-fast , 选择全部的表,点击生成代码来生成代码。

5.2 解压生成的文件,把解压的main文件夹,复制到你生成的模块下。

6.导入代码之后,存在报错,还需要生成一个公共的模块,把公共代码导入进去,其他服务依赖这个公共模块。

6.1 每个模块加入一下依赖

1
2
3
4
5
<dependency>
<groupId>com.centersept.supermall</groupId>
<artifactId>super-mall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

7.从renren-fast 模块,把utils 和 xss 里的类copy 到 common 模块

7.1 修改pom 的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!--    mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<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>org.apache.tomcat.embed</groupId>-->
<!-- <artifactId>tomcat-embed-core</artifactId>-->
<!-- <version>9.0.36</version>-->
<!-- </dependency>-->

8.修改逆向工程中的controller模板 Controller.java.vm, 注释掉shiro的注解

9.最后再次生成代码,复制粘贴到项目中。

配置&测试模块的CRUD

配置

1.配置数据源

1.1 common pom的依赖添加,导入数据库的驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
 <!--        mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<!-- 一般tomcat 里自带的jar包 所以设置为provided 如果存在不需要引入-->
<scope>provided</scope>
</dependency>

1.2 生成application.yml 配置文件,将数据源配置进去

1
2
3
4
5
6
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.72.130:3306/supermall_pms
driver-class-name: com.mysql.jdbc.Driver

2.配置mybatis-plus

2.1 配置mybatisPlus ,在启动类加入注解,扫描mapper 接口,生成代理类,注入到spring 容器中,让spring 进行管理

1
@MapperScan("com.centersept.supermall.product.dao")

2.2 在配置文件追加一下内容

1
2
3
4
5
6
7
8
mybatis-plus:
#mapper 的路径 ,classpath* 全路径扫描,即java的类路径下;classpath 即 resource 路径下
mapper-locations: classpath:/mapper/**/*.xml
#配置主键使用规则
global-config:
db-config:
#主键自增
id-type: auto

测试是否能crud

在 test 文件夹下的测试类,添加以下内容测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Autowired
BrandService brandService;

@Test
void contextLoads() {
BrandEntity brandEntity = new BrandEntity();
brandEntity.setDescript("测试");
brandEntity.setName("测试");
boolean b = brandService.save(brandEntity);
System.out.println(b);
brandEntity.setBrandId(1L);
brandEntity.setDescript("修改测试");
b = brandService.updateById(brandEntity);
System.out.println(b);
List<BrandEntity> queryListDto = brandService.list(new QueryWrapper<BrandEntity>().eq("brand_id",1L));
System.out.println(queryListDto.toString());
}

逆向工程生成全部模块的业务代码

1.每个模块的pom 都加上common 模块的依赖

2.配置每个模块的数据库Url 为对应的库

3.每个模块的端口号都修改,从8000开始,+1000 递增,方便每个模块生成多个服务,直接在后面加1。

即 订单模块定义8000 开始,再加一个订单服务则对用端口为8001。仓储模块则使用9000。

分布式组件

Spring-cloud-netflix

todo

Spring-cloud-alibaba 分布式框架解决方案

SpringCloud Alibaba-Nacos: 注册中心(服务发现/注册 )
SpringCloud Alibaba-Nacos配置中心(动态配置管理)
SpringCloud- Ribbon:负载均衡
SpringCloud- Feign:声明式HTTP客户端(调用远程服务)
SpringCloud Alibaba-Sentinel:服务容错(限流、降级、熔断)
SpringCloud- Gateway: API 网关(webflux 编程模式)
SpringCloud- Sleuth:调用链监控
SpringCloud Alibaba-Seata:原Fescar, 即分布式事务解决方案

引入分布式框架Spring-cloud-alibaba

在公共模块common 的pom 加入 一下配置:

注:框架的版本和spring-boot的版本要同步,具体参考官方文档。

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Nacos 注册中心

1.首先,修改 pom.xml 文件,引入在公共common模块引入 Nacos Discovery Starter。

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.Nacos server 配置

注:window下操作

2.1 https://github.com/alibaba/nacos/releases 下载 Nacos server , 我下载的是这个版本

2.2 下载完成后,解压,startup.cmd 启动

2.2.1 出现启动不了,窗口一闪的情况。

解决:你的jdk环境变量没有配置好,必须把jdk环境置顶,也就是上移,让系统优先选择。

最后,在微服务业务模块中加入以下配置

1
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

3.使用 @EnableDiscoveryClient 注解开启服务注册与发现功能,启动项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@RestController
class EchoController {
@GetMapping(value = "/echo/{string}")
public String echo(@PathVariable String string) {
return string;
}
}
}

4.使用浏览器登录 http://localhost:8848/nacos/ ,账号密码 nacos ,但是并没有我们注册的服务。

那是因为微服务模块配置文件没有配置apllication.name 这个属性,加入之后就可以发现了。

4.1 修改配置文件之后出现

1
org.springframework.boot.context.properties.ConfigurationPropertiesBean

因为 springboot版本和springcloud版本不兼容,换一个版本试试。换成Greenwich.SR3 版本的。

Nacos服务中心 部署到 docker

1.获取镜像

todo

OpenFeigh 测试远程调用

原理

todo

使用

1.引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.项目接入feigh

2.1在优惠券服务取一个controller类文件,定义一个数据返回List

1
2
3
4
5
6
@RequestMapping("/member/list")
public R membercoupons(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("测试优惠券");
return R.ok().put("coupons",Arrays.asList(couponEntity));
}

2.2 在会员模块下创建feigh包,这个文件夹用于声明feigh接口。

1
2
3
4
5
6
@FeignClient("supermall-coupon")
public interface CouponFeighService {

@RequestMapping("/coupon/coupon/member/list")
public R membercoupons();
}

2.3 在会员服务的启动类中加入注解,开启远程调用

1
@EnableFeignClients(basePackages = "com.centersept.supermall.member.feigh")

也可以在可以在接口上加入@Component 注解,交给spring 注入。

2.4 在会员模块下的MemberController类中加入一下内容,远程调用优惠券服务

1
2
3
4
5
6
7
8
9
10
11
@Autowired
CouponFeighService couponFeignService;

@RequestMapping("/coupons")
public R test(){
MemberEntity memberEntity = new MemberEntity();
memberEntity.setNickname("大帅逼");

R membercoupons = couponFeignService.membercoupons();
return R.ok().put("member",memberEntity).put("coupons",membercoupons.get("coupons"));
}

2.5 浏览器输入http://localhost:9000/member/member/coupons,测试是否连通。

Nacos 配置中心

简单配置实例

1.在common 模块引入nacos 配置中心的依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2.在应用的/src/main/resources/bootstrap.properties 配置文件中配置Nacos Config 元数据,现在 coupon 模块创建

3.在controller层 创建一个测试的用户代码,如下

1
2
3
4
5
6
7
8
9
10
@Value("${user.testname}")
private String testUserName;

@Value("${user.age}")
private Integer testAge;

@RequestMapping("/test")
public R test(){
return R.ok().put("testUserName",testUserName).put("testAge",testAge);
}

注:user.name spring 会自动去读取计算机的名字

4.在 application.properties 中配置相应的属性,如下

1
2
user.testname=超人
user.age=25

5.为了不让我们重新打包重启,则在配置中心加上配置。启动的如下日志已经提醒,从 nacos 配置中心获取配置信息的

1
2020-07-16 21:00:46.045  INFO 14188 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='NACOS', propertySources=[NacosPropertySource {name='supermall-coupon.properties'}]}

我们需要在 Nacos 配置中心配置相应的配置文件

6.打开Nacos 管理界面,点击配置列表,然后新建一个 DataId 为 supermall-coupon.properties 的命名,再然后把配置的内容写到配置内容栏里面,发布。

7.这个时候还没有结束,还需要在配置属性的类里面,加上@RefreshScope 注解,才能达到热更新的效果。

8.因为配置中心是bootstrap.properties 中配置加载的,所以相同的配置项,优先加载Nacos 配置中心的配置。

命名空间与配置分组

1.命名空间

配置隔离

默认:public

1.开发,测试,生产

配置命名空间

1.在命名空间中,分别添加开发,测试,生产 的命名空间。

2.打开配置列表,发现页面上已经有我们添加的命名空间。

3.然后我们在新的命名空间中添加我们的配置项。我们可以直接在public 的命名空间中,选择已经配置好的配置,然后克隆到其他命名空间就可以了。

4.如果我们要在不同环境切换配置命名空间,可以在bootstrap.properties 中 增加命名空间配置项

1
spring.cloud.nacos.config.namespace=98af8d57-dd3a-4b68-b1a1-945ee78f1239

5.也可以每个微服务创建一个命名空间。


2.配置集

所有的配置的集合

3.配置集ID

类似配置文件名

4.配置分组

默认所有的配置集都属于:DEFAULT_GROUP

需要在boostrap.properties 加入以下配置

1
2
#配置分组的命名
spring.cloud.nacos.config.group=test

一般来说一个微服务使用命名空间来隔离,然后使用分组来隔离环境的配置。

加载多配置源

1.新建一个配置 ,把数据源的配置当道这个配置中来,Data ID 为 datasource.yml ,Group 为 dev, 配置格式为YAML 配置内容为以下内容:

1
2
3
4
5
6
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.72.130:3306/supermall_sms
driver-class-name: com.mysql.jdbc.Driver

2.mybatis 的配置,也可以创建一个出来进行加载,配置如下:

1
2
3
4
5
6
7
8
mybatis-plus:
#mapper 的路径 ,classpath* 全路径扫描,即java的类路径下;classpath 即 resource 路径下
mapper-locations: classpath:/mapper/**/*.xml
#配置主键使用规则
global-config:
db-config:
#主键自增
id-type: auto

3.新建一个其他的配置,统一进行管理

1
2
3
4
5
6
7
8
9
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: supermall-coupon
server:
port: 8000

4.把这些创建好的配置,配置到项目中,在bootstrap.properties 中加入以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# datasource
spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
#是否动态刷新
spring.cloud.nacos.config.ext-config[0].refresh=true

# mybatis
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
#是否动态刷新
spring.cloud.nacos.config.ext-config[1].refresh=true

#other
spring.cloud.nacos.config.ext-config[2].data-id=other.yml
spring.cloud.nacos.config.ext-config[2].group=dev
#是否动态刷新
spring.cloud.nacos.config.ext-config[2].refresh=true

SpringCloud-Gateway 网关

网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制等。而springcloud gateway
作为SpringCloud官方推出的第二代网关框架,取代了Zuul 网关(网飞)。

主要作用 :动态路由,鉴权

rps:32213 每秒请求数

组件:Route,Predicate,Filter

注:官方文档中有很多配置实例,可以直接上去查看。

1.创建gatewey 模块,加入网关模块,以及引入common 模块

2.加入nacos 依赖,然后配置nacos的配置,新建bootstrap.properties ,加入以下配置:

1
2
3
spring.application.name=<你的应用名称>
spring.cloud.nacos.discovery.server-addr=<服务所在ip>:8848
spring.cloud.nacos.config.server-addr=<服务所在ip>:8848

3.排除数据库配置,在启动类上加入

1
2
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

4.测试路由断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
spring:
cloud:
gateway:
routes:
# - id: test_route
# uri: https://www.baidu.com
# predicates:
# - Query=url,baidu
#
# - id: qq_route
# uri: https://www.qq.com
# predicates:
# - Query=url,qq

- id: product_route
uri: lb://supermall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}

- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

前端基础

技术栈简介

ES6 语法

JavaScript 是它的实现

前端基础

1.let 声明变量

var 声明的变量往往会越域
let 声明的变量有严格局部作用域

1
2
3
4
var a - 1;
let b - 2;
console.log(a); 1
console.log(b);1ReferenceError:b is not defined

var 会存在变量提升
let 不存在变量提升

1
2
3
4
console.log(×); /l undefined
var x =10;
console.log(y); //ReferenceError: y is not defined
let y = 20;

2.const 声明常量(只读变量)

声明之后不允许改变

一旦声明必须初始化,否则报错

3.解构表达式

数组解构

1
2
3
4
5
6
let arr = [1,12,3]
// let a = arr[0]
// let b = arr[1]
// let c = arr[2]
let [a,b,c] = arr
console.log(a,b,c)

对象解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const person = {
name: "jack",
age: 21,
language: ["java", "js", "css"]
}
const name = person.name
const age = person.age
const language = person.language

const {name,age,language} = person

console.log(name,age,language)
// 别名解构
const {name:userName,age:superAge,language} = person

4.字符串扩展

字符串API

1
2
3
4
5
let str = "hello.wue"";
console.log(str.startswith(hello"))//trui
console.log(str.endswith(".vue")//true
console.log(str.includes("e"));//true
console.log(str.includes("hello")//true

字符串模板

1
2
3
4
5
6
7
8
// 反引号 `
//1、多行字符串
let ss = `
<div>
<span>hello world<span>
</div>
`
console.log(ss)

字符串插入变量

1
2
3
4
5
6
//2、字符串插入变量和表达式。变量名写在${}中,${}中可以放入JavaScript表达式。
let info = 我是${abc},今年${age+10}了,我想说${fun()};
console.logo(info)
function fun(){
return "hello"
}

5.函数优化

5.1 函数参数默认值

1
2
3
4
5
6
7
8
9
10
11
12
// 在ES6以前,我们无法给一个函数的设置默认值,只能采用变通写去:
function add(a,b){
/判斯b是否为空,为空就拾认值1
b = b || 1 ;
return a + b;
}
// 传一个参数
console.log(add(10));
// 现在可以这么写:直接给参数写上默认值,没传就会自动使用默认值
function add(a,b=1){
return a + b;
}

5.2 不定参数

1
2
3
4
5
6
function fun(..values){
console.log(values.length)
}
fun(12) //2
fun(1,2,3,4) //4

5.3 箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原来声明一个函数
var func = function (obj) {
console.log(obj)
}
// 现在
var funcNew = obj => console.log(obj)
// 原来
var sum = function(a,b){
return a+b;
}
// 现在
var sum2 = (a,b) => a+b;

function hello (person) {
console.log("hello,"+person.name)
}
// 箭头函数,加解构表达式
var hello2 = ({name}) => console.log("hello,"+person.name)
hello2(person)

6.对象优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const person = {
name: "jack",
age: 21,
language: ["java", "js", "css"]
}

console.log(Object.keys(person));/["name","age""language"l
console.log(Object.values(person));77["jack"21Array(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)

// 声明对象简写
const age = 23
const name = "张三"
const person1 = {age:age,name:name}
// 属性名和变量值一样 直接一下简写
const person2 = {age,name}

// 对象函数属性简写
let person3 = {
name:"jack",
eat: function (food) {
console.log(this.name + "在吃" + food)
},
// 箭头函数不能用this, 用 对象.属性
eat: foot => console.log(person3.name + "在吃" + food),
eat3 (foot) {
console.log(this.name + "在吃" + food)
}
}

// 对象扩展运算符
// 拷贝对象(深拷贝)
let p1 = {name:"tom",age:12}
let someone = {...person1}
console.log(someone) // {name:"tom",age:12}

// 合并对象
let age1 = {age:12}
let name1 = {name:"tom"}
let p2 = {name1:"am"}
p2 = {...age1,...name1}

7.map 和 reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//数组中新增了map和reduce 方法

//map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回

let arr = ['1','20','-3','222']

arr = arr.map((item)=>{item*2})

// reduce() 为数组中的每个元素一次执行回调函数,不包括数组中被删除或从未被赋值的元素

// arr.reduce(callback,[initialValue])

1.previousValue(上一次调用回调返回的值,或者是提供的初始值(initialvalue)

2.currentvalue(数组中当前被处理的元素)
3.index(当前元素在数组中的素引)
4.array (调用reduce的数组)

arr.reduce((a,b)=>{
console.log("上次值"+a)
console.log("当前值"+b)
return a+b
})

8.Promise (异步编排)

问题:ajax 一步一步的递归调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 1.Promise 可以封装异步操作
let p = new Promise((resolve,reject)=>{
// 1.异步操作
&.ajax({
url:"mock/user.json",
success:function(data){
resolve(data)
},
error:function(err){
reject(err)
}
})
if(true){
resolve(data)
}else{
reject(error)
}
})
p.then((obj)=>{
return new Promise((resolve,reject)=>{
// 1.异步操作
&.ajax({
url:`mock/user_corse_${obj.id}.json`,
success:function(data){
resolve(data)
},
error:function(err){
reject(err)
}
})
})
}).catch((err){

}).then((data)=>{
// 1.异步操作
&.ajax({
url:`mock/cors_score_${data.id}.json`,
success:function(data){
console.log(data)
},
error:function(err){
console.log(err)
}
})
})

function get(url, data){
return new Proeise((resolve,reject)->{
$.ajax({
url: url,
data: data,
success: function (data){
resolve(data);
},
error: function (err){
reject(err)
})
});
}
get("mock/user.json")
.then((data)=>{
return get(`mock/user_corse_${data.id}.json`)
}).then((data)=>{
return get(`mock/cors_score_${data.id}.json`)
}).then((data)=>{
console.log(data)
}).catch((err)=>{
console.log(err)
})

9.模块化

9.1 什么是模块化

模块化就是把代码进行拆分,方便重复利用。类似java中的导包,要使用一个包,必须先导包。而s中没有包的概念,换来的是模块。
模块功能主要由两个命令构成.’export”和”import’.
export:命令用于规定模块的对外接口。
import:命令用于导入其他模块提供的功能。

9.2 export 导出

给导入另外起名,使用以下格式

1
2
3
4
5
export default {
sum (a,b) {
return a+b;
}
}

9.3 import 导入

vue

MVVM

todo

官网看引入教程

nodejs


开始搭建前端模块

Vue模块化开发

搭建Vue脚手架

1
2
3
4
5
6
7
8
9
10
11
12
13
# 全局安装webpack
npm install webpack -g

# 全局安装Vue的脚手架
npm install -g @vue/cli-init

# 新建一个项目工作文件,然后vue脚手架使用webpack模块初始化一个(自己的项目名称)
vue init webpack super-mall-vue

# 启动新建好的vue项目
npm run dev
# 打包项目
npm run build

创建一个组件

1.在src/components 下创建vue 文件,内容是vue的基本三大属性

1
2
3
4
5
6
7
8
9
10
11
12
<template>

</template>

<script>
export default {

}
</script>
<style lang="sass" scoped>

</style>

2.在src/router 下,编辑index.js,添加一下代码

1
2
3
4
5
6
7
8
9
10
11
import hello from '@/components/{你的文件名}'

export default new Router({
routes: [
{
path:'/{你的文件名}',
name:'{你的组件命名}',
component:hello
}
]
})

引用ElementUI

安装依赖
1
npm i element-ui
引入依赖

在 main.js 中写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
el: '#app',
render: h => h(App)
});

使用elementUI 提供的布局以及配置动态路由菜单

1.替换App.vue 中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<template>
<el-container style="height: 500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']" router='true'>
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>导航一</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>导航二</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="2-1">选项1</el-menu-item>
<el-menu-item index="2-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="2-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="2-4">
<template slot="title">选项4</template>
<el-menu-item index="2-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>导航三</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="3-1">选项1</el-menu-item>
<el-menu-item index="3-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="3-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="3-4">
<template slot="title">选项4</template>
<el-menu-item index="3-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
</el-menu>
</el-aside>

<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>

<el-main>
<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>
</el-main>
</el-container>
</el-container>
</template>

<script>
export default {
data() {
const item = {
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
};
return {
tableData: Array(20).fill(item)
}
}
};
</script>

<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}

.el-aside {
color: #333;
}
</style>

2.删除<el-main>中的内容,替换成的标签,然后在中加入属性router=”true”,最后标签中加入route=’/{Vue Router 路径对象}’,是为了点击菜单达到替换内容的目的

VSCode,加入vue代码片段

1.点击文件-首选项-用户代码-点击新建代码片段-取名vue.json,然后替换一下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"Print to console": {
"prefix": "vue",
"body": [
"<template>",
" <div>",
" </div>",
"</template>",
"",
"<script>",
" export default {",
" data() {",
" return {",
"",
" };",
" },",
" created() {",
"",
" },",
" mounted() {",
"",
" },",
" methods: {",
"",
" },",
"}",
"</script>",
"",
"<style lang='scss' scoped>",
"$4",
"</style>"
],
"description": "Log output to console"
}

商品服务

三级分类

查询树形展示三级分类数据

1.在PMS库中的pms_category表,导入菜单分类数据

2.然后在product模块中,对应的service文件中编写三级分类业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public List<CategoryEntity> menu() {
List<CategoryEntity> allList = baseMapper.selectList(null);
// 找到所有的一级分类
List<CategoryEntity> level1Nenus = allList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0
).map((item) -> {
item.setChildren(getChildren(item, allList));
return item;
}).sorted((item1, item2) -> {
return (item1.getSort() == null ? 0 : item1.getSort()) - (item2.getSort() == null ? 0 : item2.getSort());
}).collect(Collectors.toList());
return level1Nenus;
}

private List<CategoryEntity> getChildren(CategoryEntity currentItem, List<CategoryEntity> allList) {
List<CategoryEntity> collect = allList.stream().filter((item) -> {
return item.getParentCid().equals(currentItem.getCatId());
}).map((item) -> {
item.setChildren(getChildren(item, allList));
return item;
}).sorted((item1, item2) -> {
return (item1.getSort() == null ? 0 : item1.getSort()) - (item2.getSort() == null ? 0 : item2.getSort());
}).collect(Collectors.toList());
return collect;
}

配置网关路由与路径重写和网关统一配置跨域

1.启动前端代码,再启动后端服务,进入系统的系统管理->菜单管理,添加商品系统的一级目录,然后再添加这个一级目录的分类维护的子目录,path为product/category。

2.然后根据这个path的属性,在前端src/views/modules 下创建一个product文件夹,然后在这个文件夹下创建category.vue 文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>

<script>
export default {
data() {
return {
data: [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1'
}]
}]
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1'
}]
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1'
}]
}]
}, {
label: '一级 3',
children: [{
label: '二级 3-1',
children: [{
label: '三级 3-1-1'
}]
}, {
label: '二级 3-2',
children: [{
label: '三级 3-2-1'
}]
}]
}],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenu(){
this.$http({
url: this.$http.adornUrl('/product/category/menu'),
method: 'get'
}).then(({data}) => {
console.log('菜单'+data)
})
}
},
activated () {
this.getMenu()
},
}
</script>

<style>

</style>

3.配置好请求,打开页面,发现请求404,我们要配置项目父目录下static->config的index.js 文件中的api请求接口路径,把所有服务请求都提交到网关服务去,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 开发环境
*/
;(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;
})();

4.配置好之后,还需要配置网关模块的配置文件,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://supermall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

5.再把后端管理项目加入到nacos 服务发现中,配置nacos服务发现,重启网关服务。

6.但是我们发现,请求给了403,看控制台报错是没有配置跨域策略,所以我们必须在网关配置跨域策略,加入以下文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class SupermallCorsConfiguration {

@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

CorsConfiguration corsConfiguration = new CorsConfiguration();

//1、配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
// 允许携带cookie
corsConfiguration.setAllowCredentials(true);

source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}

7.重新请求,发现还是403,看了错误显示跨域配置了多个值,导致的报错,经过查找发现,后台管理服务中也增加了跨域策略,所以我们需要删除它,在io.renren.config目录下的CorsConfig 。

树型展示三级分类数据

1.修改category.vue文件,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>

<script>
export default {
data() {
return {
menus: [],
defaultProps: {
children: 'children',
label: 'name'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenu(){
this.$http({
url: this.$http.adornUrl('/product/category/menu'),
method: 'get'
}).then(({data}) => {
this.menus = data.data
console.log('菜单=========',data.data)
})
}
},
activated () {
this.getMenu()
},
}
</script>

<style>

</style>

页面效果

修改过的页面内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<template>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
default-expand-all
show-checkbox
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
</template>

<script>
export default {
data() {
return {
menus: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
append(data) {
console.log("append", data);
},

remove(node, data) {
console.log("remove", node, data);
},
getMenu() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenu();
}
};
</script>

逻辑删除

以下为官方操作文档路径

https://baomidou.com/guide/logic-delete.html#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9A

1.使用mybatis-plus插件集成的便利性,修改application.yml,加上如下配置

1
2
3
4
5
6
7
8
9
10
mybatis-plus:
#mapper 的路径 ,classpath* 全路径扫描,即java的类路径下;classpath 即 resource 路径下
mapper-locations: classpath:/mapper/**/*.xml
#配置主键使用规则
global-config:
db-config:
#主键自增
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0

2.修改DO实体类,把逻辑删除的字段加上@TableLogic 注解

3.然后使用postman 调用请求接口,为了看到逻辑删除执行的语句,我们需要修改日志框架的日志级别为debug级别,配置文件加入以下配置

1
2
3
logging:
level:
com.centersept.supermall: debug

删除效果细化

category.vue 修改之后的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<template>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
</template>

<script>
export default {
data() {
return {
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
append(data) {
console.log("append", data);
},

remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenus();
}
};
</script>

新增效果完成

category.vue 修改之后的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.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>
</div>
</template>

<script>
export default {
data() {
return {
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
append(data) {
console.log("append", data);
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenus();
}
};
</script>

基本修改效果完成

category.vue 修改之后的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="edit(data)"
>edit</el-button
>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
data() {
return {
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
edit(data) {
console.log("要修改的数据", 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.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
//修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenus();
}
};
</script>

拖拽效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
:draggable="draggable"
:allow-drop="allowDrop"
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="edit(data)"
>edit</el-button
>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
data() {
return {
draggable: true,
maxLevel: 0,
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3

//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);

// this.maxLevel
if (type == "inner") {
// console.log(
// `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
// );
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]);
}
}
},
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
edit(data) {
console.log("要修改的数据", 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.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
//修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenus();
}
};
</script>

拖拽数据收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="edit(data)"
>edit</el-button
>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input
v-model="category.productUnit"
autocomplete="off"
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
data() {
return {
updateNodes: [],
draggable: true,
maxLevel: 0,
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
// 菜单拖拽 start =======================================================
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
this.pCid.push(pCid);
//2、当前拖拽节点的最新顺序,
for (let i = 0; i < siblings.length; i++) {
if (siblings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = siblings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3

//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);

// this.maxLevel
if (type == "inner") {
// console.log(
// `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
// );
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]);
}
}
},
// 菜单拖拽 end =======================================================
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
edit(data) {
console.log("要修改的数据", 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.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
//修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenus();
}
};
</script>

拖拽功能完成

1.后端代码增加接口 CategoryController

1
2
3
4
5
6
7
8
/**
* 批量更新菜单排序
*/
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] category) {
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}

2.单行修改频繁调用不够友好,需要使用按钮进行批量修改。单行提交代码省略。

最终菜单页面完成代码效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
></el-switch>
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
ref="menuTree"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="edit(data)"
>edit</el-button
>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span></el-tree
>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input
v-model="category.productUnit"
autocomplete="off"
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
data() {
return {
pCid: [],
updateNodes: [],
draggable: false,
maxLevel: 0,
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
// 菜单批量处理 start =======================================================
batchDelete() {
let catIds = [];
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
console.log("被选中的元素", checkedNodes);
for (let i = 0; i < checkedNodes.length; i++) {
catIds.push(checkedNodes[i].catId);
}
this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
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(() => {});
},
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 = [];
this.maxLevel = 0;
// this.pCid = 0;
});
},
// 菜单批量处理 end =======================================================
// 菜单拖拽 start =======================================================
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
this.pCid.push(pCid);
//2、当前拖拽节点的最新顺序,
for (let i = 0; i < siblings.length; i++) {
if (siblings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = siblings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3

//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);

// this.maxLevel
if (type == "inner") {
// console.log(
// `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
// );
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]);
}
}
},
// 菜单拖拽 end =======================================================
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
edit(data) {
console.log("要修改的数据", 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.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
//修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
},
// 查询菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/menu"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
console.log("菜单=========", data.data);
});
}
},
activated() {
this.getMenus();
}
};
</script>

品牌管理

使用逆向工程的前后端代码

1.在菜单管理中在商品服务下生成一个品牌管理的菜单,菜单路径为product/brand

2.使用逆向生成代码的两个文件 brand.vue 和 brand-add-or-update.vue 文件放到product 文件夹下。

3.由于做了权限设置,所以进入/utils/index.js 文件夹下修改 isAuth 方法的返回值一直为true。

效果优化与快速显示开关

关闭ESLint 语法检查

打开build文件夹下的webpack.base.conf.js,然后注释调createLintingRule中的方法体。

1.优化brand.vue 之后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
<template>
<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:brand:save')"
type="primary"
@click="addOrUpdateHandle()"
>新增</el-button
>
<el-button
v-if="isAuth('product:brand:delete')"
type="danger"
@click="deleteHandle()"
:disabled="dataListSelections.length <= 0"
>批量删除</el-button
>
</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="brandId"
header-align="center"
align="center"
label="品牌id"
>
</el-table-column>
<el-table-column
prop="name"
header-align="center"
align="center"
label="品牌名"
>
</el-table-column>
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="介绍"
>
</el-table-column>
<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>
<el-table-column
prop="firstLetter"
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
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="addOrUpdateHandle(scope.row.brandId)"
>修改</el-button
>
<el-button
type="text"
size="small"
@click="deleteHandle(scope.row.brandId)"
>删除</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>
</template>

<script>
import AddOrUpdate from "./brand-add-or-update";
export default {
data() {
return {
dataForm: {
key: ""
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
};
},
components: {
AddOrUpdate
},
activated() {
this.getDataList();
},
methods: {
// 修改显示状态
updateBrandStatus(data) {
console.log("最新信息", data);
let { brandId, showStatus } = data;
//发送请求修改状态
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
data: this.$http.adornData({ brandId, showStatus }, false)
}).then(({ data }) => {
this.$message({
type: "success",
message: "状态更新成功"
});
});
},
// 获取数据列表
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/brand/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.brandId;
});
this.$confirm(
`确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}
).then(() => {
this.$http({
url: this.$http.adornUrl("/product/brand/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>

brand-add-or-update.vue 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="140px">
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<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>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>

<script>
export default {
data () {
return {
visible: false,
dataForm: {
brandId: 0,
name: '',
logo: '',
descript: '',
showStatus: '',
firstLetter: '',
sort: ''
},
dataRule: {
name: [
{ required: true, message: '品牌名不能为空', trigger: 'blur' }
],
logo: [
{ required: true, message: '品牌logo地址不能为空', trigger: 'blur' }
],
descript: [
{ required: true, message: '介绍不能为空', trigger: 'blur' }
],
showStatus: [
{ required: true, message: '显示状态[0-不显示;1-显示]不能为空', trigger: 'blur' }
],
firstLetter: [
{ required: true, message: '检索首字母不能为空', trigger: 'blur' }
],
sort: [
{ required: true, message: '排序不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init (id) {
this.dataForm.brandId = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(`/product/brand/info/${this.dataForm.brandId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name
this.dataForm.logo = data.brand.logo
this.dataForm.descript = data.brand.descript
this.dataForm.showStatus = data.brand.showStatus
this.dataForm.firstLetter = data.brand.firstLetter
this.dataForm.sort = data.brand.sort
}
})
}
})
},
// 表单提交
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/product/brand/${!this.dataForm.brandId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'brandId': this.dataForm.brandId || undefined,
'name': this.dataForm.name,
'logo': this.dataForm.logo,
'descript': this.dataForm.descript,
'showStatus': this.dataForm.showStatus,
'firstLetter': this.dataForm.firstLetter,
'sort': this.dataForm.sort
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

云存储开通与使用

开通阿里云OSS

OSS 整合测试

普通Maven项目引入

1.在阿里云平台开通RAM服务,然后在RAM 服务下创建子账号,然后保存生成的AccessKey ID 和 AccessKey Secret 。

2.查看OSS官方文档,引入下面的maven依赖,根据文档示例写一个简单的demo 测试。

1
2
3
4
5
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
SpringCloud Alibaba OSS 引入

1.打开github 上的 demo示例,https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample.

2.加入以下以下maven依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
</dependency>

3.然后在配置文件文件中加入以下配置

1
2
3
4
// application.properties
alibaba.cloud.access-key=your-ak
alibaba.cloud.secret-key=your-sk
alibaba.cloud.oss.endpoint=***

4.然后在业务在业务项目中使用以下代码进行OSS客户端注入访问

1
2
3
4
5
6
7
8
9
10
@Service
public class YourService {
@Autowired
private OSSClient ossClient;

public void saveFile() {
// download file to local
ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File("pathOfYourLocalFile"));
}
}

5.弄一个测试demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.aliyun.oss.OSSClient;
import com.centersept.supermall.product.service.BrandService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SuperMallProductApplicationTests {

@Autowired
OSSClient ossClient;

@Test
public void test() throws FileNotFoundException {
InputStream inputStream = new FileInputStream("C:\\Users\\qkz-admin\\Pictures" +
"\\ba4957ffgy1g89ginl2cwj20dw0dw75d.jpg");
ossClient.putObject("center-test", "ba4957ffgy1g89ginl2cwj20dw0dw75d.jpg", inputStream);
ossClient.shutdown();
System.out.println("上传完成");
}
}

OSS 获取服务端签名

1.建立一个新的模块,该模块是引入第三方服务的模块,命名super-mall-TTS

2.maven的pom加入以下内容

注:依赖的springboot的版本号一定要一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.centersept.supermall</groupId>
<artifactId>super-mall-tts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>super-mall-tts</name>
<description>第三方服务模块</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>com.centersept.supermall</groupId>
<artifactId>super-mall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入阿里云封装好的cloud oss-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

3.创建bootstrap.properties 文件加入nacos config 的配置。

4.创建一个生成签名的contorller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.centersept.supermall.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author Center_Sept
* @date 2020/11/19 15:51
*/
@RestController
public class OssController {

@Autowired
private OSS ossClient;
/**
* 端点
*/
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
/**
* bucket
*/
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
/**
* access-key
*/
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;

/**
* 功能描述:获得上传policy和签名
*/
@RequestMapping("/oss/policy")
public R policy() {
String host = "https://" + bucket + "." + endpoint; // host的格式为 https:// + bucketname + endpoint
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
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;
// 30 s 超时
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>();
// access-key
respMap.put("accessid", accessId);
// 策略
respMap.put("policy", encodedPolicy);
// 策略签名
respMap.put("signature", postSignature);
// 用户上传文件时指定的前缀
respMap.put("dir", dir);
// 主机
respMap.put("host", host);
// 过期时间
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
return R.ok().put("data", respMap);
}

}

OSS 前后联调测试上传

1.引入以下三个文件,放在src/components/upload 文件夹下。

多文件上传组件 multiUpload.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<div>
<el-upload
action="http://center-test.oss-cn-beijing.aliyuncs.com"
:data="dataObj"
list-type="picture-card"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview"
:limit="maxCount"
:on-exceed="handleExceed"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt />
</el-dialog>
</div>
</template>
<script>
import { policy } from "./policy";
import { getUUID } from '@/utils'
export default {
name: "multiUpload",
props: {
//图片属性数组
value: Array,
//最大上传图片数量
maxCount: {
type: Number,
default: 30
}
},
data() {
return {
dataObj: {
policy: "",
signature: "",
key: "",
ossaccessKeyId: "",
dir: "",
host: "",
uuid: ""
},
dialogVisible: false,
dialogImageUrl: null
};
},
computed: {
fileList() {
let fileList = [];
for (let i = 0; i < this.value.length; i++) {
fileList.push({ url: this.value[i] });
}

return fileList;
}
},
mounted() {},
methods: {
emitInput(fileList) {
let value = [];
for (let i = 0; i < fileList.length; i++) {
value.push(fileList[i].url);
}
this.$emit("input", value);
},
handleRemove(file, fileList) {
this.emitInput(fileList);
},
handlePreview(file) {
this.dialogVisible = true;
this.dialogImageUrl = file.url;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy()
.then(response => {
console.log("这是什么${filename}");
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessid;
_self.dataObj.key = response.data.dir + "/"+getUUID()+"_${filename}";
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
resolve(true);
})
.catch(err => {
console.log("出错了...",err)
reject(false);
});
});
},
handleUploadSuccess(res, file) {
this.fileList.push({
name: file.name,
// url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名
url: this.dataObj.host + "/" + this.dataObj.key.replace("${filename}",file.name)
});
this.emitInput(this.fileList);
},
handleExceed(files, fileList) {
this.$message({
message: "最多只能上传" + this.maxCount + "张图片",
type: "warning",
duration: 1000
});
}
}
};
</script>
<style>
</style>

请求OSS签名封装脚本 policy.js

1
2
3
4
5
6
7
8
9
10
11
12
import http from '@/utils/httpRequest.js'
export function policy() {
return new Promise((resolve,reject)=>{
http({
url: http.adornUrl("/tts/oss/policy"),
method: "get",
params: http.adornParams({})
}).then(({ data }) => {
resolve(data);
})
});
}

单文件上传组件 singleUpload.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<template> 
<div>
<el-upload
action="http://center-test.oss-cn-beijing.aliyuncs.com"
:data="dataObj"
list-type="picture"
:multiple="false" :show-file-list="showFileList"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="fileList[0].url" alt="">
</el-dialog>
</div>
</template>
<script>
import {policy} from './policy'
import { getUUID } from '@/utils'

export default {
name: 'singleUpload',
props: {
value: String
},
computed: {
imageUrl() {
return this.value;
},
imageName() {
if (this.value != null && this.value !== '') {
return this.value.substr(this.value.lastIndexOf("/") + 1);
} else {
return null;
}
},
fileList() {
return [{
name: this.imageName,
url: this.imageUrl
}]
},
showFileList: {
get: function () {
return this.value !== null && this.value !== ''&& this.value!==undefined;
},
set: function (newValue) {
}
}
},
data() {
return {
dataObj: {
policy: '',
signature: '',
key: '',
ossaccessKeyId: '',
dir: '',
host: '',
// callback:'',
},
dialogVisible: false
};
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
handleRemove(file, fileList) {
this.emitInput('');
},
handlePreview(file) {
this.dialogVisible = true;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy().then(response => {
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessid;
_self.dataObj.key = response.data.dir + '/'+getUUID()+'_${filename}';
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
resolve(true)
}).catch(err => {
reject(false)
})
})
},
handleUploadSuccess(res, file) {
console.log("上传成功...")
this.showFileList = true;
this.fileList.pop();
this.fileList.push({name: file.name, url: this.dataObj.host + '/' + this.dataObj.key.replace("${filename}",file.name) });
this.emitInput(this.fileList[0].url);
}
}
}
</script>
<style>

</style>

2.修改brand-add-or-update.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="140px">
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<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>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>

<script>
import SingleUpload from "@/components/upload/singleUpload";
export default {
components: { SingleUpload },
data () {
return {
visible: false,
dataForm: {
brandId: 0,
name: '',
logo: '',
descript: '',
showStatus: '',
firstLetter: '',
sort: ''
},
dataRule: {
name: [
{ required: true, message: '品牌名不能为空', trigger: 'blur' }
],
logo: [
{ required: true, message: '品牌logo地址不能为空', trigger: 'blur' }
],
descript: [
{ required: true, message: '介绍不能为空', trigger: 'blur' }
],
showStatus: [
{ required: true, message: '显示状态[0-不显示;1-显示]不能为空', trigger: 'blur' }
],
firstLetter: [
{ required: true, message: '检索首字母不能为空', trigger: 'blur' }
],
sort: [
{ required: true, message: '排序不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init (id) {
this.dataForm.brandId = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(`/product/brand/info/${this.dataForm.brandId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name
this.dataForm.logo = data.brand.logo
this.dataForm.descript = data.brand.descript
this.dataForm.showStatus = data.brand.showStatus
this.dataForm.firstLetter = data.brand.firstLetter
this.dataForm.sort = data.brand.sort
}
})
}
})
},
// 表单提交
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/product/brand/${!this.dataForm.brandId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'brandId': this.dataForm.brandId || undefined,
'name': this.dataForm.name,
'logo': this.dataForm.logo,
'descript': this.dataForm.descript,
'showStatus': this.dataForm.showStatus,
'firstLetter': this.dataForm.firstLetter,
'sort': this.dataForm.sort
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>

3.上传操作之后,点击保存,保存上传的路径

表单校验&自定义校验器

1.修改显示状态,返回后端的值,修改如下

1
2
3
4
5
6
7
8
9
<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>

2.把表格显示的URL地址,转变为显示图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
<template slot-scope="scope">
<!-- <el-image v-if="scope.row.logo != null && scope.row.logo != ''"
style="width: 100px; height: 80px"
:src="scope.row.logo"
fit="fill"></el-image> -->
<img v-if="scope.row.logo != null && scope.row.logo != ''" :src="scope.row.logo" style="width: 100px; height: 80px" />
</template>
</el-table-column>

3.加入表单验证,修改brand-add-or-upadate.vue,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible"
>
<el-form
:model="dataForm"
:rules="dataRule"
ref="dataForm"
@keyup.enter.native="dataFormSubmit()"
label-width="140px"
>
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<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>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input
v-model="dataForm.firstLetter"
placeholder="检索首字母"
></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>

<script>
import SingleUpload from "@/components/upload/singleUpload";
export default {
components: { SingleUpload },
data() {
return {
visible: false,
dataForm: {
brandId: 0,
name: "",
logo: "",
descript: "",
showStatus: 1,
firstLetter: "",
sort: 0
},
dataRule: {
name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
logo: [
{ required: true, message: "品牌logo地址不能为空", trigger: "blur" }
],
descript: [
{ required: true, message: "介绍不能为空", trigger: "blur" }
],
showStatus: [
{
required: true,
message: "显示状态[0-不显示;1-显示]不能为空",
trigger: "blur"
}
],
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"
}]
}
};
},
methods: {
init(id) {
this.dataForm.brandId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(
`/product/brand/info/${this.dataForm.brandId}`
),
method: "get",
params: this.$http.adornParams()
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name;
this.dataForm.logo = data.brand.logo;
this.dataForm.descript = data.brand.descript;
this.dataForm.showStatus = data.brand.showStatus;
this.dataForm.firstLetter = data.brand.firstLetter;
this.dataForm.sort = data.brand.sort;
}
});
}
});
},
// 表单提交
dataFormSubmit() {
this.$refs["dataForm"].validate(valid => {
if (valid) {
this.$http({
url: this.$http.adornUrl(
`/product/brand/${!this.dataForm.brandId ? "save" : "update"}`
),
method: "post",
data: this.$http.adornData({
brandId: this.dataForm.brandId || undefined,
name: this.dataForm.name,
logo: this.dataForm.logo,
descript: this.dataForm.descript,
showStatus: this.dataForm.showStatus,
firstLetter: this.dataForm.firstLetter,
sort: this.dataForm.sort
})
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.visible = false;
this.$emit("refreshDataList");
}
});
} else {
this.$message.error(data.msg);
}
});
}
});
}
}
};
</script>

JSR303表单验证

1.给Bean实体类添加校验注解(在javax.validation.constraints 中),并定义自己的message提示,举个栗子,在BrandEntity.java 中修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id")
@Null(message = "新增不能指定id")
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@Min(value = 0, message = "排序必须大于等于0")
private Integer sort;

}

2.在对应的controller的保存接口上加上@Valid注解开启校验,但是返回的是默认的响应,不是我们想要的。

1
2
3
4
5
6
7
8
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}

3.自定义校验体,需要在入参中加入BindingResult类型的参数,就可以获取到校验的结果,自定义返回给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
//1、获取校验的错误结果
result.getFieldErrors().forEach((item) -> {
//FieldError 获取到错误提示
String message = item.getDefaultMessage();
//获取错误的属性的名字
String field = item.getField();
map.put(field, message);
});
return R.error(400, "提交的数据不合法").put("data", map);
} else {
brandService.save(brand);
}
return R.ok();
}

4.也可以使用BindingResultUtils 提供的方法,对入参进行校验。

统一异常处理

1.为了不增加代码的侵入性,我们可以自定义一个异常类,基于AOP做统一处理。

2.在对应的模块下,创建一个exception包,声明一个统一的异常类型,然后加上@RestControllerAdvice(basePackages = “com.atguigu.gulimall.product.controller”)类注解,方法上加入@ExceptionHandler(value = MethodArgumentNotValidException.class)对应处理的异常注解,然后在公共模块定义统一的异常代码枚举类。

统一异常类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class SupermallExceptionControllerAdvice {


@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();

Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError) -> {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(), BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",
errorMap);
}

@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {

log.error("错误:", throwable);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}

}

异常代码枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");

private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

3.最后把controller侵入业务的代码删除调。

JSR303分组校验

1.定义校验分组接口,接口不需要实现类,仅仅是使用来标识分组。分别定义addGroup.java 和 upGroup.java分组,放到common模块下,放到valid的包下。

2.在bean实体类的字段上的校验注解中加入group属性,使用刚刚定义好的接口类型根据需求填充到group中。如,新增接口应该使用addGroup.class类标识,更新接口使用upGroup.class 标识。举了栗子:

1
2
3
4
5
6
7
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;

3.在对应的controller类上,加上@Validated 注解,开启校验功能,如下:

1
2
3
4
5
6
7
8
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}

注:默认没有指定分组的校验注解@NutBlank,在分组校验情况@Validated({AddGroup.class})下不生效。

JSR303自定义校验注解

1.在common模块引入javax.validation.validation-api maven依赖:

1
2
3
4
5
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

2.编写一个自定义的校验注解,在common模块中,valid包下新建一个注解类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.centersept.supermall.common.valid.ListValue.message}";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };

int[] vals() default { };
}

3.新建一个ListValueConstraintValidator校验器,该校验器需要实现ConstraintValidator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
//判断是否校验成功
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}

4.根据javax.validation 依赖规范,在resource 文件夹下新建ValidationMessages.properties文件,才能使String message() default “{com.centersept.supermall.common.valid.ListValue.message}”;生效。

1
com.centersept.supermall.common.valid.ListValue.message=显示状态参数错误

5.修改BrandEntity实体类的showStatus属性注解值

1
2
3
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;

6.为了只校验在修改状态的时候才判断该校验值,所以我们要重新声明一个请求方法

1
2
3
4
5
6
7
8
/**
* 修改状态
*/
@RequestMapping("/update/status")
public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}

7.最后再修改前端页面的请求地址。

SPU&SKU&规格参数&销售属性

SPU = Standard Product Unit (标准产品单位)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
通俗点讲,属性值、特性相同的商品就可以称为一个SPU。

例如:
iphone4就是一个SPU,与商家,与颜色、款式、套餐都无关。

SKU=stock keeping unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。

例如:
纺织品中一个SKU通常表示:规格、颜色、款式。

属性分组

前端组件抽取&父子组件交互

1.执行提供的sys_menus.sql文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
DROP TABLE IF EXISTS `sys_menu`;

CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` varchar(50) DEFAULT NULL COMMENT '菜单名称',
`url` varchar(200) DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(500) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
`type` int(11) DEFAULT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`icon` varchar(50) DEFAULT NULL COMMENT '菜单图标',
`order_num` int(11) DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=76 DEFAULT CHARSET=utf8mb4 COMMENT='菜单管理';
INSERT INTO `sys_menu` ( `menu_id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num` )
VALUES
( 1, 0, '系统管理', NULL, NULL, 0, 'system', 0 ),
( 2, 1, '管理员列表', 'sys/user', NULL, 1, 'admin', 1 ),
( 3, 1, '角色管理', 'sys/role', NULL, 1, 'role', 2 ),
( 4, 1, '菜单管理', 'sys/menu', NULL, 1, 'menu', 3 ),
( 5, 1, 'SQL监控', 'http://localhost:8080/renren-fast/druid/sql.html', NULL, 1, 'sql', 4 ),
( 6, 1, '定时任务', 'job/schedule', NULL, 1, 'job', 5 ),
( 7, 6, '查看', NULL, 'sys:schedule:list,sys:schedule:info', 2, NULL, 0 ),
( 8, 6, '新增', NULL, 'sys:schedule:save', 2, NULL, 0 ),
( 9, 6, '修改', NULL, 'sys:schedule:update', 2, NULL, 0 ),
( 10, 6, '删除', NULL, 'sys:schedule:delete', 2, NULL, 0 ),
( 11, 6, '暂停', NULL, 'sys:schedule:pause', 2, NULL, 0 ),
( 12, 6, '恢复', NULL, 'sys:schedule:resume', 2, NULL, 0 ),
( 13, 6, '立即执行', NULL, 'sys:schedule:run', 2, NULL, 0 ),
( 14, 6, '日志列表', NULL, 'sys:schedule:log', 2, NULL, 0 ),
( 15, 2, '查看', NULL, 'sys:user:list,sys:user:info', 2, NULL, 0 ),
( 16, 2, '新增', NULL, 'sys:user:save,sys:role:select', 2, NULL, 0 ),
( 17, 2, '修改', NULL, 'sys:user:update,sys:role:select', 2, NULL, 0 ),
( 18, 2, '删除', NULL, 'sys:user:delete', 2, NULL, 0 ),
( 19, 3, '查看', NULL, 'sys:role:list,sys:role:info', 2, NULL, 0 ),
( 20, 3, '新增', NULL, 'sys:role:save,sys:menu:list', 2, NULL, 0 ),
( 21, 3, '修改', NULL, 'sys:role:update,sys:menu:list', 2, NULL, 0 ),
( 22, 3, '删除', NULL, 'sys:role:delete', 2, NULL, 0 ),
( 23, 4, '查看', NULL, 'sys:menu:list,sys:menu:info', 2, NULL, 0 ),
( 24, 4, '新增', NULL, 'sys:menu:save,sys:menu:select', 2, NULL, 0 ),
( 25, 4, '修改', NULL, 'sys:menu:update,sys:menu:select', 2, NULL, 0 ),
( 26, 4, '删除', NULL, 'sys:menu:delete', 2, NULL, 0 ),
( 27, 1, '参数管理', 'sys/config', 'sys:config:list,sys:config:info,sys:config:save,sys:config:update,sys:config:delete', 1, 'config', 6 ),
( 29, 1, '系统日志', 'sys/log', 'sys:log:list', 1, 'log', 7 ),
( 30, 1, '文件上传', 'oss/oss', 'sys:oss:all', 1, 'oss', 6 ),
( 31, 0, '商品系统', '', '', 0, 'editor', 0 ),
( 32, 31, '分类维护', 'product/category', '', 1, 'menu', 0 ),
( 34, 31, '品牌管理', 'product/brand', '', 1, 'editor', 0 ),
( 37, 31, '平台属性', '', '', 0, 'system', 0 ),
( 38, 37, '属性分组', 'product/attrgroup', '', 1, 'tubiao', 0 ),
( 39, 37, '规格参数', 'product/baseattr', '', 1, 'log', 0 ),
( 40, 37, '销售属性', 'product/saleattr', '', 1, 'zonghe', 0 ),
( 41, 31, '商品维护', 'product/spu', '', 0, 'zonghe', 0 ),
( 42, 0, '优惠营销', '', '', 0, 'mudedi', 0 ),
( 43, 0, '库存系统', '', '', 0, 'shouye', 0 ),
( 44, 0, '订单系统', '', '', 0, 'config', 0 ),
( 45, 0, '用户系统', '', '', 0, 'admin', 0 ),
( 46, 0, '内容管理', '', '', 0, 'sousuo', 0 ),
( 47, 42, '优惠券管理', 'coupon/coupon', '', 1, 'zhedie', 0 ),
( 48, 42, '发放记录', 'coupon/history', '', 1, 'sql', 0 ),
( 49, 42, '专题活动', 'coupon/subject', '', 1, 'tixing', 0 ),
( 50, 42, '秒杀活动', 'coupon/seckill', '', 1, 'daohang', 0 ),
( 51, 42, '积分维护', 'coupon/bounds', '', 1, 'geren', 0 ),
( 52, 42, '满减折扣', 'coupon/full', '', 1, 'shoucang', 0 ),
( 53, 43, '仓库维护', 'ware/wareinfo', '', 1, 'shouye', 0 ),
( 54, 43, '库存工作单', 'ware/task', '', 1, 'log', 0 ),
( 55, 43, '商品库存', 'ware/sku', '', 1, 'jiesuo', 0 ),
( 56, 44, '订单查询', 'order/order', '', 1, 'zhedie', 0 ),
( 57, 44, '退货单处理', 'order/return', '', 1, 'shanchu', 0 ),
( 58, 44, '等级规则', 'order/settings', '', 1, 'system', 0 ),
( 59, 44, '支付流水查询', 'order/payment', '', 1, 'job', 0 ),
( 60, 44, '退款流水查询', 'order/refund', '', 1, 'mudedi', 0 ),
( 61, 45, '会员列表', 'member/member', '', 1, 'geren', 0 ),
( 62, 45, '会员等级', 'member/level', '', 1, 'tubiao', 0 ),
( 63, 45, '积分变化', 'member/growth', '', 1, 'bianji', 0 ),
( 64, 45, '统计信息', 'member/statistics', '', 1, 'sql', 0 ),
( 65, 46, '首页推荐', 'content/index', '', 1, 'shouye', 0 ),
( 66, 46, '分类热门', 'content/category', '', 1, 'zhedie', 0 ),
( 67, 46, '评论管理', 'content/comments', '', 1, 'pinglun', 0 ),
( 68, 41, 'spu管理', 'product/spu', '', 1, 'config', 0 ),
( 69, 41, '发布商品', 'product/spuadd', '', 1, 'bianji', 0 ),
( 70, 43, '采购单维护', '', '', 0, 'tubiao', 0 ),
( 71, 70, '采购需求', 'ware/purchaseitem', '', 1, 'editor', 0 ),
( 72, 70, '采购单', 'ware/purchase', '', 1, 'menu', 0 ),
( 73, 41, '商品管理', 'product/manager', '', 1, 'zonghe', 0 ),
( 74, 42, '会员价格', 'coupon/memberprice', '', 1, 'admin', 0 ),
( 75, 42, '每日秒杀', 'coupon/seckillsession', '', 1, 'job', 0 );

2.添加树型菜单公共组件,再modules下创建common文件夹,然后创建一个category.vue文件,内容如下:

注:使用this.&emit(‘{事件名称}’,[参数。。。])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<div>
<el-input placeholder="输入关键字进行过滤" v-model="filterText"></el-input>
<el-tree
:data="menus"
:props="defaultProps"
node-key="catId"
ref="menuTree"
@node-click="nodeclick"
:filter-node-method="filterNode"
:highlight-current = "true"
></el-tree>
</div>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';

export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
filterText: "",
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {
filterText(val) {
this.$refs.menuTree.filter(val);
}
},
//方法集合
methods: {
//树节点过滤
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
this.menus = data.data;
});
},
nodeclick(data, node, component) {
console.log("子组件category的节点被点击", data, node, component);
//向父组件发送事件;
this.$emit("tree-node-click", data, node, component);
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>

</style>

3.如何在父页面引用子组件,然后响应子组件的冒泡事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<category @tree-node-click="treenodeclick"></category>
</template>
<script>
import Category from "../common/category";

export default {
components: { Category },
methods: {
//感知树节点被点击
treenodeclick(data, node, component) {
if (node.level == 3) {
this.catId = data.catId;
}
}
}
}
</script>

获取分类属性分组

1.增加接口

AttrGroupController

1
2
3
4
5
6
7
8
9
/**
* 列表
*/
@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}

AttrGroupServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
String key = (String) params.get("key");
//select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
if(!StringUtils.isEmpty(key)){
wrapper.and((obj)->{
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}

if( catelogId == 0){
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wrapper);
return new PageUtils(page);
}else {
wrapper.eq("catelog_id",catelogId);
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wrapper);
return new PageUtils(page);
}
}

3.修改前端文件

attrgroup.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<template>
<el-row :gutter="20">
<el-col :span="6">
<category @tree-node-click="treenodeclick"></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 type="success" @click="getAllDataList()">查询全部</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="relationHandle(scope.row.attrGroupId)">关联</el-button>
<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>

<!-- 修改关联关系 -->
<!-- <relation-update v-if="relationVisible" ref="relationUpdate" @refreshData="getDataList"></relation-update> -->
</div>
</el-col>
</el-row>
</template>

<script>
/**
* 父子组件传递数据
* 1)、子组件给父组件传递数据,事件机制;
* 子组件给父组件发送一个事件,携带上数据。
* // this.$emit("事件名",携带的数据...)
*/
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
import Category from "../common/category";
import AddOrUpdate from "./attrgroup-add-or-update";
export default {
//import引入的组件需要注入到对象中才能使用
components: { Category
, AddOrUpdate
// , RelationUpdate
},
props: {},
data() {
return {
catId: 0,
dataForm: {
key: ""
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
relationVisible: false
};
},
activated() {
this.getDataList();
},
methods: {
//处理分组与属性的关联
// relationHandle(groupId) {
// this.relationVisible = true;
// this.$nextTick(() => {
// this.$refs.relationUpdate.init(groupId);
// });
// },
//感知树节点被点击
treenodeclick(data, node, component) {
if (node.level == 3) {
this.catId = data.catId;
this.getDataList(); //重新查询
}
},
getAllDataList(){
this.catId = 0;
this.getDataList();
},
// 获取数据列表
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
method: "get",
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key
})
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataList = data.page.list;
this.totalPage = data.page.totalCount;
} else {
this.dataList = [];
this.totalPage = 0;
}
this.dataListLoading = false;
});
},
// 每页数
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>

分组新增&级联选择器

1.使用标签,引入级联选择器

2.为了返回的字段中不存在空字段值的参数返回到响应参数中,在字段变量上注解@JsonInclude(JsonInclude.Include.NON_EMPTY)

分组修改&级联选择器回显

1.this.$nextTick() 的用法是指:等到组件渲染完毕才调用next方法中的方法。

2.渲染级联回显

2.1 添加回显字段

1
2
@TableField(exist = false)
private Long[] catelogPath;

2.2 增加方法

AttrGroupController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 @Autowired
private CategoryService categoryService;
/**
* 信息
*/
@RequestMapping("/info/{attrGroupId}")
public R infoNew(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);

Long catelogId = attrGroup.getCatelogId();
Long[] path = categoryService.findCatelogPath(catelogId);

attrGroup.setCatelogPath(path);

return R.ok().put("attrGroup", attrGroup);
}

CategoryServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Long[] findCatelogPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
Collections.reverse(parentPath);
return parentPath.toArray(new Long[parentPath.size()]);
}

private List<Long> findParentPath(Long catelogId,List<Long> paths){
//1、收集当前节点id
paths.add(catelogId);
CategoryEntity byId = this.getById(catelogId);
if(byId.getParentCid()!=0){
findParentPath(byId.getParentCid(),paths);
}
return paths;
}

3.使用自带属性filterable,增加搜索功能。

4.重置弹出框数据,标签自带数据closed ,声明关闭回调方法,然后置空动态绑定参数。

品牌分类关联与级联更新

1.引入MyBatisPlus插件实现分页

1.1 在product模块下,创建config包,包下建立插件配置类,使用分页插件,注入到Spring 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableTransactionManagement //开启事务
@MapperScan("<你到数据控制层包路径>")
public class MyBatisConfig {

//引入分页插件
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
paginationInterceptor.setOverflow(true);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}

1.2 修改品牌查询方法,能够通过关键词进行多行检索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PageUtils queryPage(Map<String, Object> params) {
//1、获取key
String key = (String) params.get("key");
QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
if(!StringUtils.isEmpty(key)){
queryWrapper.eq("brand_id",key).or().like("name",key);
}
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),
queryWrapper

);
return new PageUtils(page);
}

2.品牌分类以及关联

2.1编写品牌分类信息查询接口,CategoryBrandRelationController 增加以下方法:

1
2
3
4
5
6
7
8
9
10
/**
* 获取当前品牌关联的所有分类列表
*/
@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);
}

2.2 编写平台分类信息保存接口

CategoryBrandRelationServiceImpl

1
2
3
4
5
6
7
8
9
10
11
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
//1、查询详细名字
BrandEntity brandEntity = brandDao.selectById(brandId);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
categoryBrandRelation.setBrandName(brandEntity.getName());
categoryBrandRelation.setCatelogName(categoryEntity.getName());
this.save(categoryBrandRelation);

}

CategoryBrandRelationController

1
2
3
4
5
6
7
8
/**
* 保存关联信息
*/
@RequestMapping("/save")
public R saveDetail(@RequestBody CategoryBrandRelationEntity categoryBrandRelation) {
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}

3.级联更新,如果更新品牌的名称,我们就需要同时更新关联的品牌数据

BrandServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
@Override
public void updateDetail(BrandEntity brand) {
//保证冗余字段的数据一致
this.updateById(brand);
if(!StringUtils.isEmpty(brand.getName())){
//同步更新其他关联表中的数据
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());

//TODO 更新其他关联
}
}

BrandController.java

1
2
3
4
5
6
7
8
9
10
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);

return R.ok();
}

CategoryBrandRelationServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
@Override
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity relationEntity = new CategoryBrandRelationEntity();
relationEntity.setBrandId(brandId);
relationEntity.setBrandName(name);
this.update(relationEntity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}
@Override
public void updateCategory(Long catId, String name) {
this.baseMapper.updateCategory(catId,name);
}

CategoryBrandRelationDao.xml

1
2
3
<update id="updateCategory">
UPDATE `pms_category_brand_relation` SET catelog_name=#{name} WHERE catelog_id=#{catId}
</update>

CategoryBrandRelationDao.java

1
void updateCategory(@Param("catId") Long catId, @Param("name") String name);

CategoryServiceImpl.java

1
2
3
4
5
6
7
8
9
10
/**
* 级联更新所有关联的数据
* @param category
*/
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

CategoryController.java

1
2
3
4
5
6
7
8
9
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateCascade(category);
return R.ok();
}

平台属性

规格参数新增与VO

1.VO的概念

略(搜索 object的划分)

2.规格参数新增接口

2.1 新增规格参数VO类

2.2 业务逻辑

AttrServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Transactional
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
// attrEntity.setAttrName(attr.getAttrName());
BeanUtils.copyProperties(attr,attrEntity);
//1、保存基本数据
this.save(attrEntity);
//2、保存关联关系
if(attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId()!=null){
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
relationDao.insert(relationEntity);
}
}

AttrController.java

1
2
3
4
5
6
7
8
9
10
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:attr:save")
public R save(@RequestBody AttrVo attr){
attrService.saveAttr(attr);

return R.ok();
}

规格参数列表

1.新增规格列表查询接口

修改AttrController

1
2
3
4
5
6
@GetMapping("/base/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId) {
PageUtils page = attrService.queryBaseAttrPage(params, catelogId);
return R.ok().put("page", page);
}

修改AttrServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>();

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);
//1、设置分类和分组的名字
AttrAttrgroupRelationEntity attrId =
relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",
attrEntity.getAttrId()));
if (attrId != null && attrId.getAttrGroupId() != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());

pageUtils.setList(respVos);
return pageUtils;
}

因为要关联查询数据,分类信息和分组信息,新增响应类AttrRespVo

1
2
3
4
5
6
7
8
9
10
11
@Data
public class AttrRespVo extends AttrVo {
/**
* "catelogName": "手机/数码/手机", //所属分类名字
* "groupName": "主体", //所属分组名字
*/
private String catelogName;
private String groupName;

private Long[] catelogPath;
}

规格修改

1.查询属性详情

1.1 修改 /product/attr/info/{attrId} 接口

AttrController.java

1
2
3
4
5
6
7
8
9
10
/**
* 信息
*/
@RequestMapping("/info/{attrId}")
//@RequiresPermissions("product:attr:info")
public R info(@PathVariable("attrId") Long attrId){
//AttrEntity attr = attrService.getById(attrId);
AttrRespVo respVo = attrService.getAttrInfo(attrId);
return R.ok().put("attr", respVo);
}

AttrServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo respVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity, respVo);
//1、设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation =
relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if (attrgroupRelation != null) {
respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if (attrGroupEntity != null) {
respVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
//2、设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
respVo.setCatelogPath(catelogPath);

CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if(categoryEntity!=null){
respVo.setCatelogName(categoryEntity.getName());
}
return respVo;
}

2.更新属性分组

AttrServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void updateAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr, attrEntity);
this.updateById(attrEntity);
//1、修改分组关联
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attr.getAttrId());
Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
if (count > 0) {
relationDao.update(relationEntity, new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
} else {
relationDao.insert(relationEntity);
}
}

AttrController.java

1
2
3
4
5
6
7
8
9
10
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:attr:update")
public R update(@RequestBody AttrVo attr){
attrService.updateAttr(attr);

return R.ok();
}

销售属性维护

1.修改之前的所有方法,如果是基本属性需要关联修改,如果是销售属性就不需要,所以我们要定义一个常量作为判断。

AttrServiceImpl.java 修改部分方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@Transactional
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
// attrEntity.setAttrName(attr.getAttrName());
BeanUtils.copyProperties(attr,attrEntity);
//1、保存基本数据
this.save(attrEntity);
//2、保存关联关系
if(attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId()!=null){
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
relationDao.insert(relationEntity);
}


}

@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());

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);

//1、设置分类和分组的名字
if("base".equalsIgnoreCase(type)){
AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if (attrId != null && attrId.getAttrGroupId()!=null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}

}


CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());

pageUtils.setList(respVos);
return pageUtils;
}

@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo respVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,respVo);



if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//1、设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if(attrgroupRelation!=null){
respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if(attrGroupEntity!=null){
respVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}


//2、设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
respVo.setCatelogPath(catelogPath);

CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if(categoryEntity!=null){
respVo.setCatelogName(categoryEntity.getName());
}


return respVo;
}

@Transactional
@Override
public void updateAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);
this.updateById(attrEntity);

if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//1、修改分组关联
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();

relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attr.getAttrId());

Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
if(count>0){

relationDao.update(relationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attr.getAttrId()));

}else{
relationDao.insert(relationEntity);
}
}


}

AttrController.java 修改

1
2
3
4
5
6
7
8
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId,
@PathVariable("attrType")String type){

PageUtils page = attrService.queryBaseAttrPage(params,catelogId,type);
return R.ok().put("page", page);
}

在common 模块添加常量类 ProductConstant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}
}
}

查询分组关联属性&删除关联

1.查询分组关联属性

AttrServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 根据分组id查找关联的所有基本属性
* @param attrgroupId
* @return
*/
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
List<AttrAttrgroupRelationEntity> entities = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));

List<Long> attrIds = entities.stream().map((attr) -> {
return attr.getAttrId();
}).collect(Collectors.toList());

if(attrIds == null || attrIds.size() == 0){
return null;
}
Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
return (List<AttrEntity>) attrEntities;
}

AttrGroupController 添加以下方法

1
2
3
4
5
@GetMapping("/{attrgroupId}/attr/relation")
public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){
List<AttrEntity> entities = attrService.getRelationAttr(attrgroupId);
return R.ok().put("data",entities);
}

2.删除关联

增加 AttrGroupRelationVo

1
2
3
4
5
6
7
@Data
public class AttrGroupRelationVo {

//"attrId":1,"attrGroupId":2
private Long attrId;
private Long attrGroupId;
}

AttrServiceImpl

1
2
3
4
5
6
7
8
9
10
public void deleteRelation(AttrGroupRelationVo[] vos) {
//relationDao.delete(new QueryWrapper<>().eq("attr_id",1L).eq("attr_group_id",1L));
//
List<AttrAttrgroupRelationEntity> entities = Arrays.asList(vos).stream().map((item) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
relationDao.deleteBatchRelation(entities);
}

AttrGroupController 增加

1
2
3
4
5
@PostMapping("/attr/relation/delete")
public R deleteRelation(@RequestBody AttrGroupRelationVo[] vos){
attrService.deleteRelation(vos);
return R.ok();
}

AttrAttrgroupRelationDao

1
void deleteBatchRelation(@Param(value = "entities") List<AttrAttrgroupRelationEntity> entities);

AttrAttrgroupRelationDao.xml

1
2
3
4
5
6
<delete id="deleteBatchRelation">
DELETE FROM `pms_attr_attrgroup_relation` WHERE
<foreach separator=" OR " item="item" collection="entities">(attr_id=#{item.attrId} AND
attr_group_id=#{item.attrGroupId})
</foreach>
</delete>

查询分组未关联属性

AttrGroupController

1
2
3
4
5
6
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 获取当前分组没有关联的所有属性
* @param params
* @param attrgroupId
* @return
*/
@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> group = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<Long> collect = group.stream().map(item -> {
return item.getAttrGroupId();
}).collect(Collectors.toList());

//2.2)、这些分组关联的属性
List<AttrAttrgroupRelationEntity> groupId = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", collect));
List<Long> attrIds = groupId.stream().map(item -> {
return item.getAttrId();
}).collect(Collectors.toList());

//2.3)、从当前分类的所有属性中移除这些属性;
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);
PageUtils pageUtils = new PageUtils(page);
return pageUtils;
}

新增分组与属性关联

AttrAttrgroupRelationServiceImpl.java

1
2
3
4
5
6
7
8
public void saveBatch(List<AttrGroupRelationVo> vos) {
List<AttrAttrgroupRelationEntity> collect = vos.stream().map(item -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
this.saveBatch(collect);
}

AttrGroupController.java

1
2
3
4
5
@PostMapping("/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVo> vos){
relationService.saveBatch(vos);
return R.ok();
}

新增商品

调试会员等级相关接口

1.在网关模块添加Member模块路由配置

routes.yml

1
2
3
4
5
6
- id: member_route
uri: lb://supermall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}

2.在会员页面调试新增接口,添加会员信息

获取分类关联的品牌

CategoryBrandRelationController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* /product/categorybrandrelation/brands/list
*
* 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

1
2
3
4
5
6
7
8
9
10
public List<BrandEntity> getBrandsByCatId(Long catId) {

List<CategoryBrandRelationEntity> catelogId = relationDao.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-js

1.引入前端组件pubsub-js

现在项目根目录使用命令,下载pubsub-js

1
npm install pubsub-js --save

在spuuadd.vue ,category-cascader.vue,brand-select.vue 引入pubsub-js

1
2
// 导入
import PubSub from "pubsub-js"

修改部分使用this.PubSub 的地方,直接使用 PubSub。

获取分类下所有分组以及属性

AttrGroupRelationVo 添加实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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

1
2
3
4
5
6
7
8
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据分类id查出所有的分组以及这些组里面的属性
* @param catelogId
* @return
*/
@Override
public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrsByCatelogId(Long catelogId) {
//com.atguigu.gulimall.product.vo
//1、查询分组信息
List<AttrGroupEntity> attrGroupEntities = this.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
//2、查询所有属性
List<AttrGroupWithAttrsVo> collect = attrGroupEntities.stream().map(group -> {
AttrGroupWithAttrsVo attrsVo = new AttrGroupWithAttrsVo();
BeanUtils.copyProperties(group,attrsVo);
List<AttrEntity> attrs = attrService.getRelationAttr(attrsVo.getAttrGroupId());
attrsVo.setAttrs(attrs);
return attrsVo;
}).collect(Collectors.toList());
return collect;
}

修改spuadd.vue的showBaseAttrs()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
showBaseAttrs() {
if (!this.dataResp.steped[0]) {
this.$http({
url: this.$http.adornUrl(
`/product/attrgroup/${this.spu.catalogId}/withattr`
),
method: "get",
params: this.$http.adornParams({})
}).then(({ data }) => {
//先对表单的baseAttrs进行初始化
data.data.forEach(item => {
let attrArray = [];
if (item.attrs) {
item.attrs.forEach(attr => {
attrArray.push({
attrId: attr.attrId,
attrValues: "",
showDesc: attr.showDesc
});
});
}
this.dataResp.baseAttrs.push(attrArray);
});
this.dataResp.steped[0] = 0;
this.dataResp.attrGroups = data.data;
});
}
}

商品新增VO抽取

1
2
3
4
5
6
@Data
public class Attr {
private Long attrId;
private String attrName;
private String attrValue;
}
1
2
3
4
5
6
@Data
public class BaseAttrs {
private Long attrId;
private String attrValues;
private int showDesc;
}
1
2
3
4
5
6
7
@Data
public class Bounds {

private BigDecimal buyBounds;
private BigDecimal growBounds;

}
1
2
3
4
5
6
@Data
public class Images {

private String imgUrl;
private int defaultImg;
}
1
2
3
4
5
6
7
8
@Data
public class MemberPrice {

private Long id;
private String name;
private BigDecimal price;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
public class Skus {

private List<Attr> attr;
private String skuName;
private BigDecimal price;
private String skuTitle;
private String skuSubtitle;
private List<Images> images;
private List<String> descar;
private int fullCount;
private BigDecimal discount;
private int countStatus;
private BigDecimal fullPrice;
private BigDecimal reducePrice;
private int priceStatus;
private List<MemberPrice> memberPrice;


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class SpuSaveVo {

private String spuName;
private String spuDescription;
private Long catalogId;
private Long brandId;
private BigDecimal weight;
private int publishStatus;
private List<String> decript;
private List<String> images;
private Bounds bounds;
private List<BaseAttrs> baseAttrs;
private List<Skus> skus;

}

商品新增业务流程分析

SpuInfoServiceImpl 下增加预备方法

1
2
3
4
5
6
7
8
9
10
11
12
public void saveSpuInfo(SpuSaveVo vo) {
//1、保存spu基本信息 pms_spu_info
//2、保存Spu的描述图片 pms_spu_info_desc
//3、保存spu的图片集 pms_spu_images
//4、保存spu的规格参数;pms_product_attr_value
//5、保存spu的积分信息;supermall_sms->sms_spu_bounds
//5、保存当前spu对应的所有sku信息;
//5.1)、sku的基本信息;pms_sku_info
//5.2)、sku的图片信息;pms_sku_image
//5.3)、sku的销售属性信息:pms_sku_sale_attr_value
// //5.4)、sku的优惠、满减等信息;supermall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
}

SpuInfoController

1
2
3
4
5
6
7
8
9
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:spuinfo:save")
public R save(@RequestBody SpuSaveVo vo){
spuInfoService.saveSpuInfo(vo);
return R.ok();
}

保存SPU基本信息

新增修改 SpuInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Autowired
SpuInfoDescService spuInfoDescService;

@Autowired
SpuImagesService imagesService;

@Autowired
AttrService attrService;

@Autowired
ProductAttrValueService attrValueService;

@Transactional(rollbackFor = Exception.class)
@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.saveBaseSpuInfo(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.saveSpuInfoDesc(descEntity);
//3、保存spu的图片集 pms_spu_images
List<String> images = vo.getImages();
imagesService.saveImages(infoEntity.getId(),images);
//4、保存spu的规格参数;pms_product_attr_value
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
valueEntity.setAttrId(attr.getAttrId());
AttrEntity id = attrService.getById(attr.getAttrId());
valueEntity.setAttrName(id.getAttrName());
valueEntity.setAttrValue(attr.getAttrValues());
valueEntity.setQuickShow(attr.getShowDesc());
valueEntity.setSpuId(infoEntity.getId());

return valueEntity;
}).collect(Collectors.toList());
attrValueService.saveProductAttr(collect);
//5、保存spu的积分信息;supermall_sms->sms_spu_bounds
//5、保存当前spu对应的所有sku信息;
//5.1)、sku的基本信息;pms_sku_info
//5.2)、sku的图片信息;pms_sku_image
//5.3)、sku的销售属性信息:pms_sku_sale_attr_value
// //5.4)、sku的优惠、满减等信息;supermall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
}

@Override
public void saveBaseSpuInfo(SpuInfoEntity infoEntity) {
this.baseMapper.insert(infoEntity);
}

SpuInfoDescServiceImpl

1
2
3
4
@Override
public void saveSpuInfoDesc(SpuInfoDescEntity descEntity) {
this.baseMapper.insert(descEntity);
}

SpuImagesServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void saveImages(Long id, List<String> images) {
if (images == null || images.size() == 0) {
return;
} else {
List<SpuImagesEntity> collect = images.stream().map(img -> {
SpuImagesEntity spuImagesEntity = new SpuImagesEntity();
spuImagesEntity.setSpuId(id);
spuImagesEntity.setImgUrl(img);

return spuImagesEntity;
}).collect(Collectors.toList());

this.saveBatch(collect);
}
}

ProductAttrValueServiceImpl

1
2
3
4
@Override
public void saveProductAttr(List<ProductAttrValueEntity> collect) {
this.saveBatch(collect);
}

保存SKU基本信息

修改SpuInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@Autowired
SkuInfoService skuInfoService;

@Autowired
SkuImagesService skuImagesService;

@Autowired
SkuSaleAttrValueService skuSaleAttrValueService;

@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.saveBaseSpuInfo(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.saveSpuInfoDesc(descEntity);
//3、保存spu的图片集 pms_spu_images
List<String> images = vo.getImages();
imagesService.saveImages(infoEntity.getId(), images);
//4、保存spu的规格参数;pms_product_attr_value
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
valueEntity.setAttrId(attr.getAttrId());
AttrEntity id = attrService.getById(attr.getAttrId());
valueEntity.setAttrName(id.getAttrName());
valueEntity.setAttrValue(attr.getAttrValues());
valueEntity.setQuickShow(attr.getShowDesc());
valueEntity.setSpuId(infoEntity.getId());

return valueEntity;
}).collect(Collectors.toList());
attrValueService.saveProductAttr(collect);
//5、保存spu的积分信息;supermall_sms->sms_spu_bounds
// todo
//5、保存当前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();
}
}
// private String skuName;
// private BigDecimal price;
// private String skuTitle;
// private String skuSubtitle;
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtils.copyProperties(item, skuInfoEntity);
skuInfoEntity.setBrandId(infoEntity.getBrandId());
skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSpuId(infoEntity.getId());
skuInfoEntity.setSkuDefaultImg(defaultImg);
//5.1)、sku的基本信息;pms_sku_info
skuInfoService.saveSkuInfo(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
List<SkuImagesEntity> imagesEntities = 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.isEmpty(entity.getImgUrl());
}).collect(Collectors.toList());
//5.2)、sku的图片信息;pms_sku_image
skuImagesService.saveBatch(imagesEntities);
//没有图片路径的无需保存
List<Attr> attr = item.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> {
SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity();
BeanUtils.copyProperties(a, attrValueEntity);
attrValueEntity.setSkuId(skuId);

return attrValueEntity;
}).collect(Collectors.toList());
//5.3)、sku的销售属性信息:pms_sku_sale_attr_value
skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
});
}
//5.4)、sku的优惠、满减等信息;supermall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
//todo
}

SkuInfoServiceImpl

1
2
3
4
@Override
public void saveSkuInfo(SkuInfoEntity skuInfoEntity) {
this.baseMapper.insert(skuInfoEntity);
}

调用远程服务保存优惠等信息

1.product模块的处理

1.1 在product模块下创建feign包,新建一个CouponFeignService远程调用接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FeignClient("supermall-coupon")
public interface CouponFeignService {


/**
* 1、CouponFeignService.saveSpuBounds(spuBoundTo);
* 1)、@RequestBody将这个对象转为json。
* 2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。
* 将上一步转的json放在请求体位置,发送请求;
* 3)、对方服务收到请求。请求体里有json数据。
* (@RequestBody SpuBoundsEntity spuBounds);将请求体的json转为SpuBoundsEntity;
* 只要json数据模型是兼容的。双方服务无需使用同一个to
* @param spuBoundTo
* @return
*/
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);


@PostMapping("/coupon/skufullreduction/saveinfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}

1.2 在common模块新建一个to包,在该包下新建涉及的TO类

1
2
3
4
5
6
7
@Data
public class SpuBoundTo {

private Long spuId;
private BigDecimal buyBounds;
private BigDecimal growBounds;
}
1
2
3
4
5
6
7
8
9
10
11
12
@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;
}
1
2
3
4
5
6
7
@Data
public class MemberPrice {
private Long id;
private String name;
private BigDecimal price;

}

1.3 修改SpuInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
@Autowired
CouponFeignService couponFeignService;

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.saveBaseSpuInfo(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.saveSpuInfoDesc(descEntity);
//3、保存spu的图片集 pms_spu_images
List<String> images = vo.getImages();
imagesService.saveImages(infoEntity.getId(), images);
//4、保存spu的规格参数;pms_product_attr_value
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
valueEntity.setAttrId(attr.getAttrId());
AttrEntity id = attrService.getById(attr.getAttrId());
valueEntity.setAttrName(id.getAttrName());
valueEntity.setAttrValue(attr.getAttrValues());
valueEntity.setQuickShow(attr.getShowDesc());
valueEntity.setSpuId(infoEntity.getId());

return valueEntity;
}).collect(Collectors.toList());
attrValueService.saveProductAttr(collect);
//5、保存spu的积分信息;supermall_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积分信息失败");
}
//5、保存当前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();
}
}
// private String skuName;
// private BigDecimal price;
// private String skuTitle;
// private String skuSubtitle;
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtils.copyProperties(item, skuInfoEntity);
skuInfoEntity.setBrandId(infoEntity.getBrandId());
skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSpuId(infoEntity.getId());
skuInfoEntity.setSkuDefaultImg(defaultImg);
//5.1)、sku的基本信息;pms_sku_info
skuInfoService.saveSkuInfo(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
List<SkuImagesEntity> imagesEntities = 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.isEmpty(entity.getImgUrl());
}).collect(Collectors.toList());
//5.2)、sku的图片信息;pms_sku_image
skuImagesService.saveBatch(imagesEntities);
//没有图片路径的无需保存
List<Attr> attr = item.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> {
SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity();
BeanUtils.copyProperties(a, attrValueEntity);
attrValueEntity.setSkuId(skuId);

return attrValueEntity;
}).collect(Collectors.toList());
//5.3)、sku的销售属性信息:pms_sku_sale_attr_value
skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
SkuReductionTo skuReductionTo = new SkuReductionTo();
BeanUtils.copyProperties(item,skuReductionTo);
skuReductionTo.setSkuId(skuId);
if(skuReductionTo.getFullCount() >0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
if(r1.getCode() != 0){
log.error("远程保存sku优惠信息失败");
}
}
});
}
}

1.4 在启动类加入@EnableFeignClients(basePackages = “com.centersept.supermall.product.feign”)注解,开启远程调用功能。

2.处理coupon模块

2.1 在nacos上配置Coupon模块的配置文件,Coupon模块引入nacos的配置

2.2 修改SpuBoundsController 方法

1
2
3
4
5
6
7
8
9
/**
* 保存
*/
@PostMapping("/save")
public R save(@RequestBody SpuBoundsEntity spuBounds){
spuBoundsService.save(spuBounds);

return R.ok();
}

2.3 新增 SkuFullReductionController 和 SkuFullReductionServiceImpl 的方法

1
2
3
4
5
6
@PostMapping("/saveinfo")
public R saveInfo(@RequestBody SkuReductionTo reductionTo){

skuFullReductionService.saveSkuReduction(reductionTo);
return R.ok();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Autowired
SkuLadderService skuLadderService;

@Autowired
MemberPriceService memberPriceService;

@Override
public void saveSkuReduction(SkuReductionTo reductionTo) {
//1、// //5.4)、sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
//sms_sku_ladder
SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
skuLadderEntity.setSkuId(reductionTo.getSkuId());
skuLadderEntity.setFullCount(reductionTo.getFullCount());
skuLadderEntity.setDiscount(reductionTo.getDiscount());
skuLadderEntity.setAddOther(reductionTo.getCountStatus());
if(reductionTo.getFullCount() > 0){
skuLadderService.save(skuLadderEntity);
}

//2、sms_sku_full_reduction
SkuFullReductionEntity reductionEntity = new SkuFullReductionEntity();
BeanUtils.copyProperties(reductionTo,reductionEntity);
if(reductionEntity.getFullPrice().compareTo(new BigDecimal("0"))==1){
this.save(reductionEntity);
}

//3、sms_member_price
List<MemberPrice> memberPrice = reductionTo.getMemberPrice();

List<MemberPriceEntity> collect = memberPrice.stream().map(item -> {
MemberPriceEntity priceEntity = new MemberPriceEntity();
priceEntity.setSkuId(reductionTo.getSkuId());
priceEntity.setMemberLevelId(item.getId());
priceEntity.setMemberLevelName(item.getName());
priceEntity.setMemberPrice(item.getPrice());
priceEntity.setAddOther(1);
return priceEntity;
}).filter(item->{
return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1;
}).collect(Collectors.toList());

memberPriceService.saveBatch(collect);
}

商品保存debug完成

  1. 使用Idea一键启动全部项目

1.1 打开 Edit Configuration… ,点击加号,选择Compound

1.2 在右边的界面点击加号添加需要运行的Application类

  1. 由于库表的id非自增的,修改 SpuInfoDescEntity 实体类 id 注解为 @TableId(type = IdType.INPUT)

商品保存其他问题

1.空图片的数据不需要保存到库中,需要做过滤

2.还有满减和折扣没有设置的,也需要过滤保存

3.还有会员价格。

商品管理

SPU检索

1.SPU管理界面查询接口

SpuInfoController

1
2
3
4
5
6
7
8
9
10
/**
* 列表
*/
@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);
}

SpuInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public PageUtils queryPageByCondition(Map<String, Object> params) {

QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();

String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
wrapper.and((w) -> {
w.eq("id", key).or().like("spu_name", key);
});
}
// status=1 and (id=1 or spu_name like xxx)
String status = (String) params.get("status");
if (!StringUtils.isEmpty(status)) {
wrapper.eq("publish_status", status);
}

String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(brandId) && !"0".equalsIgnoreCase(brandId)) {
wrapper.eq("brand_id", brandId);
}

String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)) {
wrapper.eq("catalog_id", catelogId);
}

/**
* status: 2
* key:
* brandId: 9
* catelogId: 225
*/

IPage<SpuInfoEntity> page = this.page(
new Query<SpuInfoEntity>().getPage(params),
wrapper
);

return new PageUtils(page);
}

2.时间格式化,在application.yml 加入以下配置:

1
2
3
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss

SKU检索

1.SKU管理界面查询

SkuInfoController

1
2
3
4
5
6
7
8
9
10
/**
* 列表
*/
@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);
}

SkuInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
/**
* key:
* catelogId: 0
* brandId: 0
* min: 0
* max: 0
*/
String key = (String) params.get("key");
if(!StringUtils.isEmpty(key)){
queryWrapper.and((wrapper)->{
wrapper.eq("sku_id",key).or().like("sku_name",key);
});
}

String catelogId = (String) params.get("catelogId");
if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){

queryWrapper.eq("catalog_id",catelogId);
}

String brandId = (String) params.get("brandId");
if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(catelogId)){
queryWrapper.eq("brand_id",brandId);
}

String min = (String) params.get("min");
if(!StringUtils.isEmpty(min)){
queryWrapper.ge("price",min);
}

String max = (String) params.get("max");

if(!StringUtils.isEmpty(max) ){
try{
BigDecimal bigDecimal = new BigDecimal(max);

if(bigDecimal.compareTo(new BigDecimal("0"))==1){
queryWrapper.le("price",max);
}
}catch (Exception e){

}

}


IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
queryWrapper
);

return new PageUtils(page);
}

SPU规格维护

1.获取SPU规格接口编写

1.1 编写/product/attr/base/listforspu/{spuId}

AttrController

1
2
3
4
5
6
7
8
@Autowired
ProductAttrValueService productAttrValueService;

@GetMapping("/base/listforspu/{spuId}")
public R baseAttrlistforspu(@PathVariable("spuId") Long spuId){
List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrlistforspu(spuId);
return R.ok().put("data",entities);
}

ProductAttrValueServiceImpl

1
2
3
4
public List<ProductAttrValueEntity> baseAttrlistforspu(Long spuId) {
List<ProductAttrValueEntity> entities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
return entities;
}

2.修改商品规格

2.1 编写 /product/attr/update/{spuId} 接口

AttrController

1
2
3
4
5
6
@PostMapping("/update/{spuId}")
public R updateSpuAttr(@PathVariable("spuId") Long spuId,
@RequestBody List<ProductAttrValueEntity> entities) {
productAttrValueService.updateSpuAttr(spuId, entities);
return R.ok();
}

ProductAttrValueServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
//1、删除这个spuId之前对应的所有属性
this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));

List<ProductAttrValueEntity> collect = entities.stream().map(item -> {
item.setSpuId(spuId);
return item;
}).collect(Collectors.toList());
this.saveBatch(collect);
}

仓储服务

仓库管理

整合ware服务&获取仓库列表

1.仓库管理页面查询

WareInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public PageUtils queryPage(Map<String, Object> params) {

QueryWrapper<WareInfoEntity> wareInfoEntityQueryWrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
wareInfoEntityQueryWrapper.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),
wareInfoEntityQueryWrapper
);

return new PageUtils(page);
}

查询库存&创建采购需求

1.商品库存管理页面查询接口

修改 WareSkuServiceImpl 查询接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public PageUtils queryPage(Map<String, Object> params) {
/**
* skuId: 1
* wareId: 2
*/
QueryWrapper<WareSkuEntity> queryWrapper = new QueryWrapper<>();
String skuId = (String) params.get("skuId");
if(!StringUtils.isEmpty(skuId)){
queryWrapper.eq("sku_id",skuId);
}

String wareId = (String) params.get("wareId");
if(!StringUtils.isEmpty(wareId)){
queryWrapper.eq("ware_id",wareId);
}


IPage<WareSkuEntity> page = this.page(
new Query<WareSkuEntity>().getPage(params),
queryWrapper
);

return new PageUtils(page);
}

2.采购需求管理页面查询接口

修改 PurchaseDetailServiceImpl 查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public PageUtils queryPage(Map<String, Object> params) {
/**
* status: 0,//状态
* wareId: 1,//仓库id
*/

QueryWrapper<PurchaseDetailEntity> queryWrapper = new QueryWrapper<PurchaseDetailEntity>();

String key = (String) params.get("key");
if(!StringUtils.isEmpty(key)){
//purchase_id sku_id
queryWrapper.and(w->{
w.eq("purchase_id",key).or().eq("sku_id",key);
});
}

String status = (String) params.get("status");
if(!StringUtils.isEmpty(status)){
//purchase_id sku_id
queryWrapper.eq("status",status);
}

String wareId = (String) params.get("wareId");
if(!StringUtils.isEmpty(wareId)){
//purchase_id sku_id
queryWrapper.eq("ware_id",wareId);
}

IPage<PurchaseDetailEntity> page = this.page(
new Query<PurchaseDetailEntity>().getPage(params),
queryWrapper
);

return new PageUtils(page);
}

合并采购需求

1.查询未领取得采购单

PurchaseController

1
2
3
4
5
6
7
@RequestMapping("/unreceive/list")
//@RequiresPermissions("ware:purchase:list")
public R unreceivelist(@RequestParam Map<String, Object> params){
PageUtils page = purchaseService.queryPageUnreceivePurchase(params);

return R.ok().put("page", page);
}

PurchaseServiceImpl

1
2
3
4
5
6
7
8
9
@Override
public PageUtils queryPageUnreceivePurchase(Map<String, Object> params) {
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
new QueryWrapper<PurchaseEntity>().eq("status",0).or().eq("status",1)
);

return new PageUtils(page);
}

2.合并采购单

2.1 Common 模块创建Ware模块枚举类WareConstant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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;
}
}
}

2.2 创建前端接收类MergeVo

1
2
3
4
5
6
@Data
public class MergeVo {

private Long purchaseId; //整单id
private List<Long> items;//[1,2,3,4] //合并项集合
}

2.3 PurchaseServiceImpl 添加合并接口逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Autowired
PurchaseDetailService detailService;

@Transactional(rollbackFor = RuntimeException.class)
@Override
public void mergePurchase(MergeVo mergeVo) {
Long purchaseId = mergeVo.getPurchaseId();
if (purchaseId == null) {
//1、新建一个
PurchaseEntity purchaseEntity = new PurchaseEntity();

purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());
purchaseEntity.setCreateTime(new Date());
purchaseEntity.setUpdateTime(new Date());
this.save(purchaseEntity);
purchaseId = purchaseEntity.getId();
}

List<Long> items = mergeVo.getItems();
// 确认采购单状态是0,1才可以合并
List<Long> itemsNew = new ArrayList<>(items.size());
items.forEach(i -> {
PurchaseDetailEntity byId = detailService.getById(i);
if (byId.getStatus() == WareConstant.PurchaseDetailStatusEnum.CREATED.getCode() || byId.getStatus() == WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode()) {
itemsNew.add(i);
}
});
Long finalPurchaseId = purchaseId;
if (itemsNew.size()==0) {
return;
}
items = itemsNew;
List<PurchaseDetailEntity> collect = items.stream().map(i -> {
PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();

detailEntity.setId(i);
detailEntity.setPurchaseId(finalPurchaseId);
detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());
return detailEntity;
}).collect(Collectors.toList());

detailService.updateBatchById(collect);

PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(purchaseId);
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}

2.4 PurchaseController 增加接口

1
2
3
4
5
@PostMapping("/merge")
public R merge(@RequestBody MergeVo mergeVo){
purchaseService.mergePurchase(mergeVo);
return R.ok();
}

领取采购单

/ware/purchase/received 接口 PurchaseController

1
2
3
4
5
6
7
8
9
/**
* 领取采购单
* @return
*/
@PostMapping("/received")
public R received(@RequestBody List<Long> ids){
purchaseService.received(ids);
return R.ok();
}

编写领取逻辑 PurchaseServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
*
* @param ids 采购单id
*/
@Override
public void received(List<Long> ids) {
//1、确认当前采购单是新建或者已分配状态
List<PurchaseEntity> collect = ids.stream().map(id -> {
PurchaseEntity byId = this.getById(id);
return byId;
}).filter(item -> {
if (item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||
item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {
return true;
}
return false;
}).map(item->{
item.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());
item.setUpdateTime(new Date());
return item;
}).collect(Collectors.toList());

//2、改变采购单的状态
this.updateBatchById(collect);

//3、改变采购项的状态
collect.forEach((item)->{
List<PurchaseDetailEntity> entities = detailService.listDetailByPurchaseId(item.getId());
List<PurchaseDetailEntity> detailEntities = entities.stream().map(entity -> {
PurchaseDetailEntity entity1 = new PurchaseDetailEntity();
entity1.setId(entity.getId());
entity1.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());
return entity1;
}).collect(Collectors.toList());
detailService.updateBatchById(detailEntities);
});
}

PurchaseDetailService 根据puchaseId 查找数据接口

1
2
3
4
public List<PurchaseDetailEntity> listDetailByPurchaseId(Long id) {
List<PurchaseDetailEntity> purchaseId = this.list(new QueryWrapper<PurchaseDetailEntity>().eq("purchase_id", id));
return purchaseId;
}

完成采购

1.新建两个接收前端信息的VO类

PurchaseDoneVo

1
2
3
4
5
6
7
8
@Data
public class PurchaseDoneVo {

@NotNull
private Long id;//采购单id

private List<PurchaseItemDoneVo> items;
}

PurchaseItemDoneVo

1
2
3
4
5
6
7
@Data
public class PurchaseItemDoneVo {
//{itemId:1,status:4,reason:""}
private Long itemId;
private Integer status;
private String reason;
}
  1. 添加/ware/purchase/done 完成采购单接口

    PurchaseController

1
2
3
4
5
@PostMapping("/done")
public R finish(@RequestBody PurchaseDoneVo doneVo){
purchaseService.done(doneVo);
return R.ok();
}
  1. 业务逻辑

PurchaseServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void done(PurchaseDoneVo doneVo) {
Long id = doneVo.getId();

//2、改变采购项的状态
Boolean flag = true;
List<PurchaseItemDoneVo> items = doneVo.getItems();

List<PurchaseDetailEntity> updates = new ArrayList<>();
for (PurchaseItemDoneVo item : items) {
PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
if (item.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()) {
flag = false;
detailEntity.setStatus(item.getStatus());
} else {
detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.FINISH.getCode());
////3、将成功采购的进行入库
PurchaseDetailEntity entity = detailService.getById(item.getItemId());
wareSkuService.addStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum());

}
detailEntity.setId(item.getItemId());
updates.add(detailEntity);
}

detailService.updateBatchById(updates);

//1、改变采购单状态
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(id);
purchaseEntity.setStatus(flag ? WareConstant.PurchaseStatusEnum.FINISH.getCode() :
WareConstant.PurchaseStatusEnum.HASERROR.getCode());
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}

WareSkuServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void addStock(Long skuId, Long wareId, Integer skuNum) {
//1、判断如果还没有这个库存记录新增
List<WareSkuEntity> entities = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
if(entities == null || entities.size() == 0){
WareSkuEntity skuEntity = new WareSkuEntity();
skuEntity.setSkuId(skuId);
skuEntity.setStock(skuNum);
skuEntity.setWareId(wareId);
skuEntity.setStockLocked(0);
wareSkuDao.insert(skuEntity);
}else{
wareSkuDao.addStock(skuId,wareId,skuNum);
}
}

WareSkuDao 添加方法

1
void addStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("skuNum") Integer skuNum);

WareSkuDao.xml

1
2
3
<update id="addStock">
UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>

4.由于需要查询skuName ,需要调用Product模块的查询方法来获取

4.1 在feign包下新建 ProductFeignService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@FeignClient("supermall-product")
public interface ProductFeignService {

/**
* /product/skuinfo/info/{skuId}
*
*
* 1)、让所有请求过网关;
* 1、@FeignClient("supermall-gateway"):给gulimall-gateway所在的机器发请求
* 2、/api/product/skuinfo/info/{skuId}
* 2)、直接让后台指定服务处理
* 1、@FeignClient("supermall-gateway")
* 2、/product/skuinfo/info/{skuId}
*
* @return
*/
@RequestMapping("/product/skuinfo/info/{skuId}")
R info(@PathVariable("skuId") Long skuId);
}

4.2 在启动类加入feign 开启注解

4.3 修改 WareSkuServiceImpl 逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    @Autowired
ProductFeignService productFeignService;

@Override
public void addStock(Long skuId, Long wareId, Integer skuNum) {
//1、判断如果还没有这个库存记录新增
List<WareSkuEntity> entities =
wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
if (entities == null || entities.size() == 0) {
WareSkuEntity skuEntity = new WareSkuEntity();
skuEntity.setSkuId(skuId);
skuEntity.setStock(skuNum);
skuEntity.setWareId(wareId);
skuEntity.setStockLocked(0);
// //TODO 远程查询sku的名字,如果失败,整个事务无需回滚
// //1、自己catch异常
// //TODO 还可以用什么办法让异常出现以后不回滚?高级
try {
R info = productFeignService.info(skuId);
Map<String,Object> data = (Map<String, Object>) info.get("skuInfo");

if(info.getCode() == 0){
skuEntity.setSkuName((String) data.get("skuName"));
}
}catch (Exception e){

}
wareSkuDao.insert(skuEntity);
} else {
wareSkuDao.addStock(skuId, wareId, skuNum);
}
}

分布式基础篇-总结

  1. 分布式基础概念

微服务,注册中心,配置中心,远程调用,Feign的使用,网关

  1. 基础开发

SpringBoot 2.0 ,SpringCloud,Mybatis-Plus,Vue组件化,阿里云对象存储

  1. 环境

Vagrant / VM,Linux,Docker,MySQL,Redis,逆向工程&人人开源项目

  1. 开发规范

    数据校验JSR303,全局异常处理,全局统一返回,全局跨域处理

    枚举状态,业务状态码,VO与TO与PO的划分,逻辑删除

    Lombok:@Data,@Slf4j

 评论