很久以前写的,很多地方写的应该不大行,随便看看,有时间重写一下。(笔记里面的大部分图片被我删了,OSS 访问要钱…)
有一些地方和老师写的不一样(主要是类、数据库表的命名有些不同)
Docker,虚拟化容器技术,基于镜像。pull
的服务为 Images,run
的服务为 Containers。
Linux 安装 Docker
# Uninstall Old Versions
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
# Install Required Packages
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
# Install Docker
sudo yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Start / Restart / Stop / Status
sudo systemctl start docker
sudo systemctl restart docker
sudo systemctl stop docker
sudo systemctl status docker
# Set Docker Daemon
sudo systemctl enable docker
# Images Accelerator
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://xzstr9ov.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
# Uninstall Docker Engine & Delete All Images
sudo yum remove docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd
Docker 安装 MySQL
# 拉取 MySQL 镜像
docker pull mysql
# 启动 MySQL 服务,并运行容器
docker run -p 3306:3306 --name mysql \
-v /devTools/mysql/log:/var/log/mysql \
-v /devTools/mysql/data:/var/lib/mysql \
-v /devTools/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql
说明:
-p 3306:3306 # MySQL 容器的 3306 端口映射到 Linux 的 3306 端口
-v /devTools/mysql/log:/var/log/mysql \ # MySQL 容器中的配置文件夹挂载到 Linux 中的 /devTools/mysql/log
-v /devTools/mysql/data:/var/lib/mysql \ # MySQL 容器中的将日志文件夹挂载到 Linux 中的 /devTools/mysql/data
-v /devTools/mysql/conf:/etc/mysql \ # MySQL 容器中的将配置文件夹挂载到 Linux 中的 /devTools/mysql/conf
-e MYSQL_ROOT_PASSWORD=root \ # 初始化 root 用户的密码
-d mysql: # Daemon
# 直接在 Linux 即可访问 /devTools/mysql 中的文件
[root@VM-8-5-centos ~]# ls /devTools/mysql/
conf data log
MySQL 配置:
vi /devTools/mysql/conf/my.cnf
# 添加以下信息到 /devTools/mysql/conf/my.cnf 中
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve
# 配置后需要重启 MySQL
docker restart mysql
Docker 安装 Redis
# 拉取 Redis 镜像
docker pull redis
# 启动 Redis 服务,并运行容器
docker run -p 6379:6379 --name redis \
-v /devTools/redis/data:/data \
-v /devTools/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
# Redis 配置
vi /devTools/redis/conf/redis.conf
# AOF 持久化
appendonly yes
# 配置后需要重启
docker restart redis
Maven 配置(
settings.xml
)
<mirrors>
<mirror>
<id>nexus-aliyunid>
<mirrorOf>centralmirrorOf>
<name>Nexus aliyunname>
<url>http://maven.aliyun.com/nexus/content/groups/publicurl>
mirror>
mirrors>
<profiles>
<profile>
<id>jdk-1.8id>
<activation>
<activeByDefault>trueactiveByDefault>
<jdk>1.8jdk>
activation>
<properties>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<maven.compiler.compilerVersion>1.8maven.compiler.compilerVersion>
properties>
profile>
profiles>
Git
# 配置用户名
git config --global user.name iTsawaysu
# 配置邮箱
git config --global user.email [email protected]
# 配置 SSH 免密登录
# 进入 Git Bash,输入以下命令,连续三次回车。
ssh-keygen -t rsa -C "[email protected]"
cat ~/.ssh/id_rsa.pub
# 将 id_rsa.pub 中的 SSH 公钥添加到 Gitee or GitHub 中
# 测试
ssh -T [email protected]
ssh -T [email protected]
创建仓库 Gitee or GitHub 仓库。
创建 Spring Boot 微服务
商品服务 mall-product
仓储服务 mall-ware
订单服务 mall-order
优惠服务 mall-coupon
用户服务 mall-member
# 共同点
依赖:spring-web、openfeign
包名:com.sun.mall.xxx
Spring Boot 版本:2.1.8.RELEASE
Spring Cloud 版本:Greenwich.SR3
mall
父项目:
<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.0modelVersion>
<groupId>com.sun.mallgroupId>
<artifactId>mallartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>mallname>
<description>聚合服务description>
<packaging>pompackaging>
<modules>
<module>mall-couponmodule>
<module>mall-membermodule>
<module>mall-ordermodule>
<module>mall-productmodule>
<module>mall-waremodule>
modules>
project>
创建数据库
Database Name:
mall_oms
mall_pms
mall_sms
mall_ums
mall_wms
mall_admin
Character Set:
utf8mb4
Collation:
utf8mb4_general_ci
The Last One Thing: Execute SQL File.
导入 人人开源 & 逆向工程 & 公共模块
renren-fast
导入到 mall
项目中,在 mall-admin
数据库中运行 SQL 文件,修改配置,启动。renren-fast-vue
:npm install & npm run dev
即可访问后台管理系统。renren-generator
,修改配置(数据库连接信息)和 generator.properties
中的信息;再将 /resources/template/Controller.java.vm
中的 @RequiresPermissions
注解先注释掉。main
目录导入到 对应的微服务中。mall-common
模块,引入公共依赖、公共类,该模块作为公共模块。各个微服务依次整合 MyBatis Plus
在 mall-cmmon
中导入依赖;
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.2.0version>
dependency>
配置数据源;
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mall_pms?useSSL=false
username: root
password: root
在主启动类上添加 @MapperScan("com.sun.mall.product.dao")
注解扫描并注册 Mapper 接口;
设置 Mapper 映射文件位置 & 全局设置 主键ID 为自增类型。
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
在 mall-cmmon
模块中导入 spring-cloud-alibaba
指定版本。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.REALEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
- 引入
spring-cloud-starter-alibaba-discovery
依赖;- 在配置文件中 配置 Nacos Server 的地址:
spring.cloud.nacos.discovery.server-addr
;- 使用
@EnableDiscoveryClient
注解开启 服务的注册和发现功能;- 下载 Nacos 并且解压;启动 Naocs,再启动微服务;在
localhost:8848/nacos
中即可查看服务注册情况。
各个微服务都需要注册到 注册中心,因此可以将 Nacos 的依赖放到 mall-common
中。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
配置文件:
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
主启动类上添加 @EnableDiscoveryClient
注解,开启服务注册与发现功能;
启动 Nacos;启动 Nacos 后再启动微服务,将微服务注册到 Nacos 注册中心。
实现步骤:
- 引入
spring-cloud-starter-openfeign
依赖;- 开启远程调用功能:
@EnableFeignClients(basePackages = "com.sun.mall.member.feign")
;- 声明远程接口(FeignService)并标注
@FeignClient("服务名字")
注解;- 在 FeignController 中调用 FeignService 中声明的方法。
实现过程:
- 服务启动,自动扫描
@EnableFeignClients
注解所指定的包;- 通过
@FeignClient("服务名")
注解,在 注册中心 中找到对应的服务;- 最后,调用请求路径所对应的 Controller 方法。
在 mall-coupon
中准备一个 Controller 方法;
@GetMapping("/test")
public R test(){
return R.ok().put("data", "openFeign OK~~~");
}
引入 OpenFeign 的依赖;
在 mall-member
的主启动类上标注 @EnableFeignClients
注解,开启远程调用功能;
@EnableFeignClients(basePackages = "com.sun.mall.member.feign")
在 mall-member
中声明远程接口 CouponFeignService;
@Component
@FeignClient("mall-coupon")
public interface CouponFeignService {
/**
* 1. 服务启动,会自动扫描 @EnableFeignClients 注解指定的包;
* 2. 通过 @FeignClient("服务名") 注解,在 注册中心 中找到对应的服务;
* 3. 最后再调用 该请求路径所对应的 Controller 方法。
*/
@GetMapping("/coupon/coupon/test")
public R test();
}
在 mall-member
中声明 CouponFeignController;
@RestController
@RequestMapping("/member/coupon")
public class CouponFeignController {
@Resource
private CouponFeignService couponFeignService;
@GetMapping("/test")
public R test() {
return couponFeignService.test();
}
}
最后,启动 mall-coupon & mall-member
,访问 localhost:8000/member/coupon/test
{
"msg": "success",
"code": 0,
"data": "openFeign OK~~~"
}
- 引入
spring-cloud-starter-alibaba-nacos-config
依赖;- 在
bootstrap.yml
中配置 Nacos Config;- 启动 Nacos,新建一个配置 —— Data ID 为
mall-coupon-dev.yml
;
- Data ID 的完整格式:
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
。
bootstrap.yml
spring:
application:
name: mall-coupon
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
profiles:
active: dev
CouponController
:测试从外部获取配置信息是否成功。
注意: 在 Controller 上标注 @RefreshScope
注解,动态获取并刷新配置。
@RefreshScope
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public R configInfo(){
return R.ok().put("configInfo", configInfo);
}
}
访问 localhost:7000/coupon/coupon/configInfo
即可获取到 Nacos 配置中心的配置信息。
Namespace & Group
Namespace 命名空间 —— 隔离配置
public
为保留空间,默认新增的配置都在 public
空间;dev
、test
、prod
命名空间,默认访问的是 public
,可以在 bootstrap.yml
中配置,访问指定的命名空间。spring:
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
namespace: 52f9c44d-bb3f-4ff3-9ec8-9c7e3d8e11bf
Group 配置分组
DEV_GROUP
、TEST_GROUP
;bootstrap.yml
中 spring.cloud.nacos.config.group
配置项的值,即可指定的获取 同名不同组 的配置文件。spring:
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
namespace: 52f9c44d-bb3f-4ff3-9ec8-9c7e3d8e11bf
group: DEV_GROUP
最终配置
以
mall-ware
为例,其他微服务同理。
Nacos 中创建 命名空间 dev
;
Nacos 中创建 mall-ware-dev.yml
,属于 dev
空间;
server:
port: 11000
spring:
application:
name: mall-ware
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mall_wms?useSSL=false
username: root
password: root
Nacos 中创建 mybatis.yml
,属于 public
空间;
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
bootstrap.yml
spring:
application:
name: mall-ware
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
namespace: 9214d082-ba56-4d03-b137-9c1a2a4b0e59
ext-config:
- data-id: mybatis.yml
refresh: true
profiles:
active: dev
网关:微服务最边缘的服务,直接暴露给用户,作为用户和微服务间的桥梁。
ip:port
,可以实现负载均衡、Token 拦截、权限验证、限流等操作。Nginx 和 Gateway 的区别:
Gateway 工作原理:
路由(Route):构建网关的基本模块,由 ID、目标 URI、一系列断言 和 过滤器 组成;
断言(Predicate):可以匹配 HTTP 请求中的任何信息,比如 请求头、请求参数等;
过滤(Filter):可以在请求被路由前或后,对请求进行修改。
网关测试
创建 mall-gateway
模块,引入 spring-cloud-starter-gateway
和 mall-common
(去除公共模块中的 MP) 依赖;
<dependency>
<groupId>com.sun.mallgroupId>
<artifactId>mall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
bootstrap.yml
spring:
application:
name: mall-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
在 Nacos 中创建 mall-gateway.yml
server:
port: 88
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
- id: baidu_route
uri: https://www.baidu.com
predicates:
- Query=where, bd
- id: qq_route
uri: https://www.bilibili.com/
predicates:
- Query=where, bz
访问 localhost:88?where=bd
跳转到 百度,访问 localhost:88?where=bz
跳转到 B站。
浏览器出于安全考虑,使用 XMLHttpRequest
对象发起 HTTP 请求时必须遵守 同源策略,否则就是跨域的 HTTP 请求,默认情况下是禁止的。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
@Configuration
public class MallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
最后:记得将 renren-fast
中所配置的 CorsConfig 注释掉。
mall_pms
中pms_category
表的设计。(运行pms_catelog.sql
对该表进行数据填充)
字段 | 类型 | 长度 | 注释 |
---|---|---|---|
cat_id |
bigint |
20 | 分类 ID |
name |
char |
50 | 分类名称 |
parent_cid |
bigint |
20 | 父分类 ID |
cat_level |
int |
11 | 层级 |
show_status |
tinyint |
4 | 是否显示 [0 不显示,1 显示] |
sort |
int |
11 | 排序 |
icon |
char |
255 | 图标地址 |
product_unit |
char |
50 | 计量单位 |
product_count |
int |
11 | 商品数量 |
查询所有分类及其子分类,以树形结构展示。
为 CategoryEntity 增加一个属性:
/**
* 子分类(在数据库表中不存在的字段)
*/
@TableField(exist = false)
private List<CategoryEntity> children;
注意:
Long
类型比较需要先使用longValue()
将包装类转换为基本类型long
。
/**
* 查询所有分类及其子分类,以树形结构展示
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> categoryEntityList = categoryService.displayWithTreeStructure();
return R.ok().put("data", categoryEntityList);
}
/**
* 查询所有分类及其子分类,以树形结构展示
*/
@Override
public List<CategoryEntity> displayWithTreeStructure() {
// 1. 查出所有分类
List<CategoryEntity> categoryEntityList = query().list();
// 2. 组装为父子的树形结构
List<CategoryEntity> categoriesWithTreeStructure = categoryEntityList.stream()
// 2.1 找到所有的一级分类(父分类ID 为 0)
.filter(categoryEntity -> categoryEntity.getParentCid().longValue() == 0)
.map(levelOneCategory -> {
// 2.2 为所有的一级分类设置 children
levelOneCategory.setChildren(setChildrenCategory(levelOneCategory, categoryEntityList));
return levelOneCategory;
})
// 2.3 排序
.sorted((o1, o2) -> {
return (o1.getSort() == null ? 0 : o1.getSort()) - (o2.getSort() == null ? 0 : o2.getSort());
})
.collect(Collectors.toList());
return categoriesWithTreeStructure;
}
/**
* 递归查找所有子分类
* @param rootCategory 当前分类
* @param allCategories 所有分类
* @return 子分类
*/
private List<CategoryEntity> setChildrenCategory(CategoryEntity rootCategory, List<CategoryEntity> allCategories) {
List<CategoryEntity> subcategoryList = allCategories.stream()
// 找到子菜单
.filter(categoryEntity -> categoryEntity.getParentCid().longValue() == rootCategory.getCatId().longValue())
.map(childrenCategory -> {
childrenCategory.setChildren(setChildrenCategory(childrenCategory, allCategories));
return childrenCategory;
})
.sorted((o1, o2) -> {
return (o1.getSort() == null ? 0 : o1.getSort()) - (o2.getSort() == null ? 0 : o2.getSort());
})
.collect(Collectors.toList());
return subcategoryList;
}
在前端页面的 系统管理 - 菜单管理 中,新增 商品系统 目录,在该目录下新增 分类维护 菜单,菜单路由为
product/category
。(新增的菜单对应增加到了mall-admin
中的sys_menu
表中)
点击菜单管理,URL 为 http://localhost:8001/#/sys-menu
;点击分类维护,URL 为 http://localhost:8001/#/product-category
。
可以发现 product/category
中的 /
被替换为 -
了;通过查看前端代码能够找到 sys-role
具体的视图为 src/views/modules/sys/role.vue
;因此创建 product-category
视图的位置应该为 src/views/modules/product/category.vue
。
访问分类维护菜单无法获取到数据,因为发送的请求是 http://localhost:8080/renren-fast/product/category/list/tree
,提供服务的接口是 10000
端口的 mall-product
微服务。
renren-fast-vue/static/config/index.js
中定义了 API 接口的基准路径地址,为 localhost:8080/renren-fast
;将其修改为给 Gateway 发送请求,由 Gateway 路由到指定的微服务。(注意,这里可为前端项目统一添加一个请求路径的前缀 api
)
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
但是这样会导致所有的请求都打到 Gateway,例如:由 renren-fast
负责处理的验证码也会被发送到 Gateway。
网关配置:将
renren-fast
注册到 Nacos 注册中心,网关负责将所有访问/api/**
的请求转发给renren-fast
处理。
引入 spring-cloud-starter-alibaba-nacos-discovery
并定义 Spring Cloud 版本;
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
...
dependencies>
在主启动类上标注 @EnbaleDiscoveryClient
注解;
在配置文件中将 renren-fast
注册到 Nacos 注册中心;
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server_addr: localhost:8848
网关配置。
server:
port: 88
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
http://localhost:88/api/captcha.jpg
请求获取验证码,发现该请求的路径为网关中断言的路径 /api/**
,于是会将请求转发给 renren-fast
处理,请求地址变为 http://localhost:8080/renren-fast:8080/api/captch.jpg
;http://localhost:8080/renren-fast/captch.jpg
;因此,需要 路径重写。server:
port: 88
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
# http://localhost:8080/api/renren-fast/captch.jpg ===> http://localhost:8080/renren-fast/captch.jpg
localhost:88/api
,所有请求都转发给了 renren-fast
(负责处理 /api/**
请求);localhost:88/api/product/category/list/tree
树形展示分类数据需要由 mall-product
处理,于是需要为其配置相应的网关路由规则;/api/product/**
开头的所有请求交给 mall-product
处理,其他微服务同理。server:
port: 88
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
# 路径更为精确的路由放到上方(高优先级)
- id: product_route
uri: lb://mall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
总结
只做了五件事!
category.vue
;逻辑删除
实体类字段上添加 @Logic
注解并且指定逻辑删除规则(逻辑删除规则也可在 application.yml
中进行全局配置)。
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1", delval = "0")
private Integer showStatus;
后端代码
/**
* @ReqeustBody 获取请求体中的内容,只有 POST 请求与请求体
* Spring MVC 自动将请求体中的数据(JSON)转换为对应的对象 Long[]
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds) {
categoryService.removeCategoriesByIds(Arrays.asList(catIds));
return R.ok();
}
@Override
public void removeCategoriesByIds(List<Long> catIds) {
// TODO 判断当前删除菜单是否被其他地方引用
this.removeByIds(catIds);
}
前端代码
util/httpRequest.js
中封装了一些拦截器:http.adornUrl
处理请求路径;http.adornParams
处理 GET 请求参数;http.adornData
处理 POST 请求参数。
category.vue
<el-tree empty-text="暂无数据..." :highlight-current="true" :data="categories" :props="defaultProps"
accordion :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)">Appendel-button>
<el-button v-if="node.childNodes.length === 0" type="text" size="mini" @click="() => remove(node, data)">Deleteel-button>
span>
span>
el-tree>
data() {
return {
...
expandedKey: []
...
}
}
remove(node, data) {
let ids = [data.catId];
// Confirm 是否删除
this.$confirm(`此操作将永久删除【${data.name}】分类, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(() => {
this.$message({
type: 'success',
dangerouslyUseHTMLString: true, // 使用 HTML 片段
message: '删除成功!',
center: true, // 文字居中
showClose: true // 可关闭
})
this.load();
// 设置默认展开的分类
this.expandedKey = [node.parent.data.catId]
})
}).catch(() => {
this.$message({
type: 'info',
dangerouslyUseHTMLString: true,
message: '已取消删除!',
center: true,
showClose: true
})
});
}
后端代码
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody CategoryEntity category) {
categoryService.save(category);
return R.ok();
}
前端代码
<el-dialog title="添加分类" :visible.sync="dialogFormVisible" :center="true" width="30%">
<el-form :model="category">
<el-form-item label="分类名称" prop="name">
<el-input v-model="category.name" autocomplete="off" @keyup.enter.native="addCategory">el-input>
el-form-item>
el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消el-button>
<el-button type="primary" @click="addCategory">确 定el-button>
div>
el-dialog>
data() {
return {
dialogFormVisible: false,
category: {name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0},
}
}
// 弹出对话框
append(data) {
// 当前要添加的分类的 父分类ID
this.category.parentCid = data.catId;
// 当前要添加的分类的 level
this.category.catLevel = data.catLevel * 1 + 1;
this.dialogFormVisible = true;
},
// 添加分类
addCategory() {
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(() => {
this.$message({
type: 'success',
dangerouslyUseHTMLString: true,
message: '添加成功!',
center: true,
showClose: true
})
this.dialogFormVisible = false;
this.load();
// 设置默认展开的分类
this.expandedKey = [this.category.parentCid]
}).catch(() => {
this.$message({
type: 'info',
dangerouslyUseHTMLString: true,
message: '添加失败!',
center: true,
showClose: true
})
this.dialogFormVisible = false;
})
}
后端代码
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId) {
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("category", category);
}
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category) {
categoryService.updateById(category);
return R.ok();
}
前端代码
{{ node.label }}
append(data)"> Append
Edit
remove(node, data)"> Delete
<el-tree ... :default-expanded-keys="expandedKey" draggable :allow-drop="allowDrop">el-tree>
maxLevel: 1
/**
* 拖拽时判断目标节点能否被放置
* @param draggingNode 被拖拽的节点
* @param dropNode 结束拖拽时最后进入的节点
* @param type 被拖拽节点的放置位置三种情况(prev、inner、next)
*/
allowDrop(draggingNode, dropNode, type) {
this.maxLevel = draggingNode.level;
this.countCurrentNodeDeep(draggingNode);
let maxDeep = Math.abs(this.maxLevel - draggingNode.level) + 1;
if (type == "inner") {
return maxDeep + dropNode.level <= 3;
} else {
return maxDeep + dropNode.parent.level <= 3;
}
},
// 求出当前节点的最大深度
countCurrentNodeDeep(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.countCurrentNodeDeep(node.childNodes[i]);
}
}
}
被拖拽节点的
parentId
改变了;目标节点所处的层级顺序改变了;被拖拽节点的层级改变了。
注意:inner
针对目标节点,before
和 after
针对目标节点的父节点。
let parentId = 0;
if (dropType == "before" || dropType == "after") {
parentId = dropNode.data.parentId;
siblings = dropNode.parent.childNodes;
} else {
parentId = dropNode.data.catId;
siblings = dropNode.childNodes;
}
排序:为 被拖拽节点 的 父节点 的 孩子 设置 sort
属性(遍历索引值)。
for (let i = 0; i < siblings.length; i++) {
this.updateNodes.push({catId: siblings[i].data.catId, sort: i, parentCid: parentId});
}
层级更新。
拖拽到目标节点的前或后,则 被拖拽节点的层级不变;
拖拽到目标节点里面,则 被拖拽节点的层级 为 目标节点的层级 + 1(即为 siblings[i].level
)。
// 遍历所有的兄弟节点:如果是拖拽节点,传入 catId、sort、parentCid、catLevel;如果是兄弟节点传入 catId、sort。
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: parentId, catLevel: catLevel});
}else{
this.updateNodes.push({catId: siblings[i].data.catId, sort: i});
}
}
Ultimate Version
updateNodes: []
/**
* 拖拽成功完成时触发的事件
* @param draggingNode 被拖拽的节点
* @param dropNode 结束拖拽时最后进入的节点
* @param dropType 被拖拽节点的放置位置(before、after、inner)
* @param ev event
*/
handleDrop(draggingNode, dropNode, dropType, ev) {
// 拖拽后的 ParentId 和 兄弟节点
let parentId = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
parentId = dropNode.parent.data.catId == undefined ? 0: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
parentId = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// 遍历所有的兄弟节点:如果是拖拽节点,传入 catId、sort、parentCid、catLevel;如果是兄弟节点传入 catId、sort。
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: parentId, catLevel: catLevel});
}else{
this.updateNodes.push({catId: siblings[i].data.catId, sort: i});
}
}
this.maxLevel = 1;
},
// 修改拖拽节点的子节点的层级
updateChildNodeLevel(node){
if (node.childNodes.length > 0){
for (let i = 0; i < node.childNodes.length; i++){
// 遍历子节点,传入 catId、catLevel
let cNode = node.childNodes[i].data;
this.updateNodes.push({catId: cNode.catId, catLevel: node.childNodes[i].level});
// 处理子节点的子节点
this.updateChildNodeLevel(node.childNodes[i]);
}
}
}
/**
* 批量修改
*/
@RequestMapping("/update/sort")
public R updateBatch(@RequestBody CategoryEntity[] categoryEntities) {
categoryService.updateBatchById(Arrays.asList(categoryEntities));
return R.ok();
}
<div style="line-height: 35px; margin-bottom: 20px">
<el-switch v-model="isDraggable"
style="margin-right: 10px"
active-text="开启拖拽"
inactive-text="关闭拖拽"
active-color="#13ce66"
inactive-color="#ff4949">
el-switch>
<el-button v-if="isDraggable"
type="primary"
size="small"
round
@click="batchSave">
批量保存
el-button>
div>
<el-tree :draggable="isDraggable" ...>el-tree>
parentId: [],
isDraggable: false
// 批量修改
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false)
}).then(() => {
this.successMessage("分类顺序修改成功");
this.load();
this.expandedKey = this.parentId;
}).catch(() => {
this.errorMessage("分类顺序修改失败");
});
// 每次拖拽后把数据清空,否则要修改的节点将会越拖越多
this.updateNodes = [];
},
/**
* 拖拽成功完成时触发的事件
* @param draggingNode 被拖拽的节点
* @param dropNode 结束拖拽时最后进入的节点
* @param dropType 被拖拽节点的放置位置(before、after、inner)
* @param ev event
*/
handleDrop(draggingNode, dropNode, dropType, ev) {
// 拖拽后的 ParentId 和 兄弟节点
let parentId = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
parentId = dropNode.parent.data.catId == undefined ? 0: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
parentId = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// 遍历所有的兄弟节点:如果是拖拽节点,传入 catId、sort、parentCid、catLevel;如果是兄弟节点传入 catId、sort。
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: parentId, catLevel: catLevel});
}else{
this.updateNodes.push({catId: siblings[i].data.catId, sort: i});
}
}
this.parentId.push(parentId);
this.maxLevel = 1;
},
// 修改拖拽节点的子节点的层级
updateChildNodeLevel(node){
if (node.childNodes.length > 0){
for (let i = 0; i < node.childNodes.length; i++){
// 遍历子节点,传入 catId、catLevel
let cNode = node.childNodes[i].data;
this.updateNodes.push({catId: cNode.catId, catLevel: node.childNodes[i].level});
// 处理子节点的子节点
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
/**
* 拖拽时判断目标节点能否被放置
* @param draggingNode 被拖拽的节点
* @param dropNode 结束拖拽时最后进入的节点
* @param type 被拖拽节点的放置位置三种情况(prev、inner、next)
*/
allowDrop(draggingNode, dropNode, type) {
this.maxLevel = draggingNode.level;
this.countCurrentNodeDeep(draggingNode);
let maxDeep = Math.abs(this.maxLevel - draggingNode.level) + 1;
if (type == "inner") {
return maxDeep + dropNode.level <= 3;
} else {
return maxDeep + dropNode.parent.level <= 3;
}
},
// 求出当前节点的最大深度
countCurrentNodeDeep(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.countCurrentNodeDeep(node.childNodes[i]);
}
}
}
<el-button type="danger"
size="small"
round
@click="batchDelete">
批量删除
el-button>
<el-tree ... ref="categoryTree">el-tree>
// 批量删除
batchDelete() {
// getCheckedNodes(leafOnly, includeHalfChecked):若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组。
let checkedNodes = this.$refs.categoryTree.getCheckedNodes();
let batchDeleteCatIds = [];
for (let i = 0; i < checkedNodes.length; i++) {
batchDeleteCatIds.push(checkedNodes[i].catId);
}
console.log(checkedNodes);
console.log(checkedNodes[0].parentCid);
console.log(batchDeleteCatIds);
this.$confirm("此操作将永久删除, 是否继续?", '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(batchDeleteCatIds, false)
}).then(() => {
this.successMessage("删除成功");
this.load();
// 设置默认展开的分类
this.expandedKey = [checkedNodes[0].parentCid];
}).catch(() => {
this.errorMessage("删除失败");
})
}).catch(() => {
this.errorMessage("已取消删除");
});
}
cateogry.vue
批量保存
批量删除
{{ node.label }}
append(data)"> Append
Edit
remove(node, data)"> Delete
mall_pms
中pms_brand
表的设计。
字段 | 类型 | 长度 | 注释 |
---|---|---|---|
brand_id |
bigint |
20 | 品牌 ID |
name |
char |
50 | 品牌名 |
logo |
varchar |
2000 | 品牌 logo 地址 |
descript |
longtext |
介绍 | |
show_status |
tinyint |
4 | 显示状态[0-不显示;1-显示] |
first_letter |
char |
1 | 检索首字母 |
sort |
int |
11 | 排序 |
导入逆向工程生成的前端代码
在前端页面的 系统管理 - 菜单管理 中,新增 品牌管理 菜单,该菜单在 商品管理 目录,菜单路由为 product/brand
;
将 mall/mall-product/src/main/resources/src/views/modules/product
中的 brand.vue
和 brand-add-or-update.vue
复制到 mall/renren-fast-vue/src/views/modules/product
处;
修改 renren-fast-vue/src/utils/index.js
中的 isAuth()
方法。
export function isAuth (key) {
// return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
return true;
}
Mybatis Plus 分页插件
@Configuration
@MapperScan("com.sun.mall.product.dao")
public class MyBatisPlusConfiguration {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setOverflow(true);
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}
优化
brand-add-or-update.vue
:修改新增时的 dialog,修改显示状态为按钮。
<el-dialog ...>
<el-form :model="dataForm" ... label-position="left" label-width="120px">
...
<el-form-item label="显示状态" prop="showStatus">
<el-switch v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949">
el-switch>
el-form-item>
...
el-form>
...
el-dialog>
brand.vue
:修改显示状态为按钮、前后联调。
<el-table
:data="dataList" ... style="width: 100%;">
...
<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>
updateBrandStatus(data) {
let {brandId, showStatus} = data;
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
data: this.$http.adornData({brandId, showStatus}, false)
}).then(() => {
this.successMessage("修改成功");
}).catch(() => {
this.errorMessage("修改失败");
})
}
OSS Object Storage Service 对象存储服务
上传方式
普通上传
用户将文件上传到应用服务器,上传请求提交到网关,通过网关路由给 mall-product
处理;
在 mall-product
中通过 Java 代码将文件上传到 OSS。
上传慢:用户数据需先上传到应用服务器,之后再上传到OSS,网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。
扩展性差:如果后续用户数量逐渐增加,则应用服务器会成为瓶颈。
服务端签名后直传
aliyun-sdk-oss
导入 aliyun-sdk-oss
依赖;
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.5.0version>
dependency>
编写测试代码。
@Test
public void uploadTest() throws FileNotFoundException {
String endpoint = "xxx";
String accessKeyId = "xxx";
String accessKeySecret = "xxx";
String bucketName = "xxx";
String objectName = "test/mountain.jpg";
String filePath = "/Users/sun/Pictures/3.jpg";
// 创建 OSSClient 实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流
InputStream inputStream = new FileInputStream(filePath);
// 创建 PutObject 请求
ossClient.putObject(bucketName, objectName, inputStream);
}
/Users/sun/Pictures/3.jpg
被上传到了 OSS 中的 test
目录下,名称为 mountain.jpg
。
spring-cloud-starter-alicloud-oss
导入 spring-cloud-starter-alicloud-oss
依赖;
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alicloud-ossartifactId>
dependency>
在配置文件中配置 OSS 服务对应的 accessKey
、secretKey
和 endpoint
;
spring:
cloud:
alicloud:
access-key: ?
secret-key: ?
oss:
endpoint: ?
注入 OSSClient 并进行文件上传下载等操作。
@Autowired
private OSSClient ossClient;
@Test
public void uploadTest() {
String bucketName = "xxx";
String objectName = "test/sky.jpg";
String filePath = "/Users/sun/Pictures/12.jpg";
ossClient.putObject(bucketName, objectName, new File(filePath));
}
/Users/sun/Pictures/12.jpg
被上传到了 OSS 中的 test
目录下,名称为 sky.jpg
。
创建
mall-third-party
模块,整合一系列第三方服务。
导入 mall-common(排除 MP)
、spring-cloud-starter-alicloud-oss
、web
、openfeign
等相关依赖。
主启动类上标注 @EnableDiscoveryClient
注解。
创建并配置 bootstrap.yml
。
spring:
application:
name: mall-third-party
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
namespace: e16311c7-1c51-4902-8866-0245ddcd4dd1
在 Nacos 中创建并配置 mall-third-party.yml
。
server:
port: 30000
spring:
cloud:
alicloud:
access-key:
secret-key:
oss:
bucket: itsawaysu
endpoint: oss-cn-shanghai.aliyuncs.com
在 Nacos 中的 mall-gateway.yml
对该模块进行网关配置。
server:
port: 88
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
- id: third_party_route
uri: lb://mall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty(?>.*),/$\{segment}
用户请求应用服务器获取上传 Policy
accessKeyId
、policy
、callback
等参数);@RestController
@RequestMapping("/oss")
public class OSSController {
@Autowired
private OSS ossClient;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
/**
* 获取上传 Policy
*/
@RequestMapping("/policy")
protected R policy() {
// 文件上传后的访问地址:https://mall-study-sun.oss-cn-shanghai.aliyuncs.com/test/mountain.jpg(https://bucket.endpoint/objectName)
String host = "https://" + bucket + "." + endpoint; // host = bucket.endpoint
String dir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM-dd")) + "/";
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConditions = new PolicyConditions();
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
System.out.println(e.getMessage());
}
return R.ok().put("data", respMap);
}
}
访问 http://localhost:88/api/thirdparty/oss/policy
成功获取到上传 Policy 以及 防伪签名。
{
"accessid": "LTAI5t5hCVLcicph8qh1A33p",
"policy": "eyJleHBpcmF0aW9uIjoiMjAyMi0xMS0wM1QwNzoyMzo1Ni4wODNaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIyLzExLzAzLyJdXX0=",
"signature": "U8uEOeOOnwbvEizYRVV1jcUb+2g=",
"dir": "2022/11/03/",
"host": "https://mall-study-sun.oss-cn-shanghai.aliyuncs.com",
"expire": "1667460236"
}
配置 CORS
客户端进行表单直传到 OSS 时,会从浏览器向 OSS 发送带有 Origin
的请求消息。OSS 对带有 Origin
头的请求消息会进行跨域规则(CORS)的验证。因此需要为 Bucket 设置跨域规则以支持 Post 方法。
brand.vue
:显示上传的图片。
<el-table-column prop="logo" header-align="center" align="center" label="品牌Logo地址">
<template slot-scope="scope">
<img :src="scope.row.logo" style="width: 100px; height: 100px">
template>
el-table-column>
brand-add-or-update.vue
:导入 SingleUpload 组件;将显示状态按钮的值从 布尔 转换为 数字(0、1)。
...
...
SingleUpload.vue
点击上传
只能上传 JPG/PNG 文件,且不超过 10MB
brand-add-or-update.vue
<el-form :model="dataForm"
:rules="dataRule" ...>
<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.number="dataForm.sort" placeholder="排序">el-input>
el-form-item>
el-form>
export default {
name: "brand-add-or-update",
components: {singleUpload},
data() {
return {
...
},
dataRule: {
name: [
{required: true, message: '品牌名不能为空', trigger: 'blur'}
],
logo: [
{required: true, message: '品牌Logo地址不能为空', trigger: 'blur'}
],
descript: [
{required: true, message: '介绍不能为空', trigger: 'blur'}
],
showStatus: [
{required: true, message: '显示状态不能为空', 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'}
]
}
}
},
...
}
JSR303 数据校验
给实体类标注校验注解 —— javax.validation.constraints
包下的注解,并自定义错误提示消息。
@NotNull
:不能为 null
;@NotEmpty
:该注解标注的 String、Collection、Map、数组
不能为 null
、长度不能为 0;@NotBlank
:适用于 String
,不能为 null
、不能为空(纯空格的 String
)。@NotBlank(message = "name 不能为空~")
private String name;
@URL(message = "logo 必须是一个合法的 URL 地址~")
@NotBlank(message = "logo 不能为 null~")
private String logo;
@NotBlank(message = "firstLetter 不能为 null~")
@Pattern(regexp = "^[a-zA-Z]$", message = "firstLetter 必须是一个字母~")
private String firstLetter;
@NotNull(message = "sort 不能为 null~")
@Min(value = 0, message = "sort 的值必须大于 0~")
private Integer sort;
开启校验功能:发送请求提交数据时,告诉 Spring MVC 数据需要校验 —— 在需要校验的 Bean
前标注 @Valid
注解。
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
效果:校验错误后有一个默认的响应。
在需要校验的 Bean 后紧跟一个 BindingResult
,能够获取到校验结果。
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> map = new HashMap<>();
// 获取校验的错误结果
bindingResult.getFieldErrors().forEach(item -> {
// 错误的属性名称
String field = item.getField();
// 错误提示
String defaultMessage = item.getDefaultMessage();
map.put(field, defaultMessage);
});
return R.error(400, "所提交的数据不合法!").put("data", map);
} else {
brandService.save(brand);
}
return R.ok();
}
之后的代码中有很多业务需要使用校验功能,意味着在 Controller 中的代码是重复的,每次都需要重复书写,很麻烦;可以做一个统一的处理,统一异常处理。
统一异常处理
Controller 中只需要关注业务代码:
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
创建异常处理类,对 MethodArgumentNotValidException
数据校验异常进行统一处理。
@RestControllerAdvice(basePackages = "com.sun.mall.product.controller")
@Slf4j
public class MallExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handleDataVerificationException(MethodArgumentNotValidException e) {
log.error("异常类型:{};异常信息:{}", e.getClass(), e.getMessage());
Map<String, String> map = new HashMap<>();
BindingResult bindingResult = e.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> map.put(fieldError.getField(), fieldError.getDefaultMessage()));
return R.error(400, "数据校验失败").put("data", map);
}
}
在
mall-common
创建 BizCodeEnum 错误提示枚举。
public enum BizCodeEnum {
UNKNOWN_EXCEPTION(50000, "系统异常"),
VALID_EXCEPTION(50001, "参数格式校验失败");
private Integer code;
private String msg;
BizCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
@RestControllerAdvice(basePackages = "com.sun.mall.product.controller")
@Slf4j
public class MallExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handleDataVerificationException(MethodArgumentNotValidException e) {
log.error("异常类型:{};异常信息:{}", e.getClass(), e.getMessage());
Map<String, String> map = new HashMap<>();
BindingResult bindingResult = e.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> map.put(fieldError.getField(), fieldError.getDefaultMessage()));
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data", map);
}
@ExceptionHandler(Throwable.class)
public R handleGlobalException(Throwable throwable) {
log.error("异常类型:{};异常信息:{}", throwable.getClass(), throwable.getMessage());
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
}
}
新增接口作为标识;
package com.sun.mall.common.validation;
/**
* @author sun
* 新增标识
*/
public interface AddGroup {
}
/**
* @author sun
* 修改标识
*/
public interface UpdateGroup {
}
对校验注解的 groups
属性进行编辑(什么情况下需要进行校验);
@Null(message = "新增时不能指定 ID", groups = {AddGroup.class})
@NotNull(message = "修改时必须指定 ID", groups = {UpdateGroup.class})
@TableId
private Long brandId;
@NotBlank(message = "name 不能为空~", groups = {AddGroup.class, UpdateGroup.class})
private String name;
@URL(message = "logo 必须是一个合法的 URL 地址~", groups = {UpdateGroup.class})
@NotBlank(message = "logo 不能为 null~", groups = {AddGroup.class})
private String logo;
@NotBlank(message = "firstLetter 不能为 null~")
@Pattern(regexp = "^[a-zA-Z]$", message = "firstLetter 必须是一个字母~")
private String firstLetter;
@NotNull(message = "sort 不能为 null~")
@Min(value = 0, message = "sort 的值必须大于 0~")
private Integer sort;
使用 @Validated
注解,并标注什么情况下对 Bean 进行校验。
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand) {
brandService.updateById(brand);
return R.ok();
}
测试
/save
:校验 brandId
、name
、logo
属性;/update
:校验 brandId
、name
、logo
属性。
注意:若请求接口标注了 @Validated({XxxGroup})
,默认未指定的分组的校验注解不生效;例如:firstLetter
和 sort
属性。
- 编写一个自定义的校验注解;
- 编写一个自定义的校验器;
- 关联自定义的校验器和自定义的校验注解。
validation
依赖;<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>2.0.1.Finalversion>
dependency>
mall-common/src/main/resources
目录下创建 ValidationMessages.properties
,提供错误提示;com.sun.mall.common.validation.OptionalValue.message="必须提交指定值"
@Documented
// 关联此注解和校验器
@Constraint(validatedBy = {OptionalValueConstraintValidator.class})
// 指定此注解可以标记在什么位置
// 此处可以指定多个不同的校验器,若需要校验 Double 类型的数据,再定义一个校验器 implements ConstraintValidator 即可。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
// 指定此注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
public @interface OptionalValue {
// 首先需要满足 JSR303 的规范,即要有 message、groups、Payload 三个属性。
// 出错时的错误提示从哪获取
String message() default "{com.sun.mall.common.validation.OptionalValue.message}";
// 支持分组校验功能
Class<?>[] groups() default {};
// 负载信息
Class<? extends Payload>[] payload() default {};
// 注解的配置项
int[] values() default {};
}
public class OptionalValueConstraintValidator implements ConstraintValidator<OptionalValue, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation 注解中提交的值
*/
@Override
public void initialize(OptionalValue constraintAnnotation) {
int[] values = constraintAnnotation.values();
for (int value : values) {
set.add(value);
}
}
/**
* 判断是否校验成功
* @param integer 需要校验的值
*/
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
// 注解中指定的值 是否包含 提交的值
return set.contains(integer);
}
}
/**
* 显示状态[0-不显示;1-显示]
* @OptionalValue 可选值为 ?
*/
@OptionalValue(values = {0, 1}, groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
BrandEntity
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@Null(message = "新增时不能指定 ID", groups = {AddGroup.class})
@NotNull(message = "修改时必须指定 ID", groups = {UpdateGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "name 不能为空~", groups = {AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo 必须是一个合法的 URL 地址~", groups = {AddGroup.class, UpdateGroup.class})
@NotBlank(message = "logo 不能为 null~", groups = {AddGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
* @OptionalValue 可选值为 ?
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@OptionalValue(values = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotBlank(message = "firstLetter 不能为 null~", groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$", message = "firstLetter 必须是一个字母~", groups = {AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "sort 不能为 null~", groups = {AddGroup.class})
@Min(value = 0, message = "sort 的值必须大于 0~", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;
}
SPU Standard Product Unit 标准化产品单元
一组可复用、易检索的标准化信息的集合,该集合 描述了一个产品的特性。
例如:iPhone 11 就是一个 SPU。
SKU Stock Keeping Unit 库存量单位
库存进出计量的基本单元,每种产品对应有唯一的 SKU 号。
例如:iPhone 11 白色 128G,可以确定 商品价格 和 库存 的集合,称为 SPU。
规格参数
产品会包含一个 规格参数,规格参数由 属性组 和 属性 组成。
子组件向父组件传递数据
父组件给子组件绑定自定义事件。
<category @tree-node-click="treeNodeClick">category>
子组件触发自定义事件并且传递数据;自定义方法被触发后,父组件中的回调函数被调用。
this.$emit("tree-node-click");
解绑。
this.$off('tree-node-click');
抽取公共组件:
src/views/modules/common/category.vue
{{ node.label }}
执行
sys_menus.sql
代码生成菜单;在src/views/modules/product
目录下 创建attrgroup.vue
;左边显示 三级分类,右边显示 属性分组。
查询
新增
批量删除
修改
删除
BackEnd
/**
* 根据 分类ID 查询 属性分组
*/
@RequestMapping("/list/{catelogId}")
public R listByCatelogId(@RequestParam Map<String, Object> params, @PathVariable("catelogId") Long catelogId){
PageUtils page = attrGroupService.queryAttrGroupsByCategoryId(params, catelogId);
return R.ok().put("page", page);
}
@Override
public PageUtils queryAttrGroupsByCategoryId(Map<String, Object> params, Long catelogId) {
LambdaQueryWrapper<AttrGroupEntity> wrapper = new LambdaQueryWrapper<>();
if (catelogId != 0) {
wrapper.eq(AttrGroupEntity::getCatelogId, catelogId);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
// select * from psm_attr_group where catelog_id = catelogId and (attr_group_id = key or attr_group_name like %key%)
wrapper.and(one -> {
one.eq(AttrGroupEntity::getAttrGroupId, key).or().like(AttrGroupEntity::getAttrGroupName, key);
});
}
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
FrontEnd
data() {
return {
catId: 0,
...
}
}
treeNodeClick(data, node, component) {
// console.log("父组件:", data, node, component);
if (node.level == 3) {
this.catId = data.catId;
this.getDataList();
}
},
// 获取数据列表
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl(`/product/attrGroup/list/${this.catId}`),
method: 'get',
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key
})
}).then(({data}) => {
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
})
}
点击新增的时候,所属分类 应该是一个级联选择器,而不是手动输入一个分类ID。(注意:只能选择 三级分类!)
级联选择器
options
属性指定选项数组后(通过后端获取),即可渲染出一个级联选择器。props
属性
value
:指定选项的值为选项对象的某个属性值;label
:指定选项标签为选项对象的某个属性值;children
:指定选项的字选项为选项对象的某个属性值。
data() {
return {
props: {
// 指定选项的值为对象的某个属性值
value: "catId",
// 指定选项标签为对象的某个属性值
label: "name",
// 指定选项的子选项为对象的某个属性值
children: "children"
},
categories: [],
...
}
},
created() {
this.getCategories();
},
methods: {
getCategories() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree")
}).then(({data}) => {
this.categories = data.data;
}).catch(error => {
console.log(error);
})
},
...
}
三级分类后面还有一个空选项
- 出现原因:三级分类的
children
为[]
,级联选择器会对该空数组进行渲染。- 解决方案:如果
children
属性为空,后端不会将该children
值发送到前端。
通过 @JsonInclude
注解实现,value
常用值:
JsonInclude.Include.ALWAYS
:默认策略,始终包含该属性;JsonInclude.Include.NON_NULL
:不为 null
的时候包含该属性;JsonInclude.Include.NON_EMPTY
:不为 null
、空字符串、空容器 等情况时包含该属性。@TableField(exist = false)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<CategoryEntity> children;
此时报错:
Invalid prop: type check failed for prop "value". Excepted Array, got String.
说明 catelogId
存储的是一个数组;选择一个三级分类,存储的内容包括 一级、二级和三级分类的 ID。因此,只需要选中该数组中的最后一个值(三级分类 ID)即可。
catelogPath
(数组)与级联选择器绑定;catelogPath
数组最后一个元素。<el-form-item label="所属分类ID" prop="catelogId">
<el-cascader v-model="dataForm.catelogPath" :options="categories" :props="props">el-cascader>
el-form-item>
data() {
catelogId: 0,
catelogPath: []
}
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/${!this.dataForm.attrGroupId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
attrGroupId: this.dataForm.attrGroupId || undefined,
attrGroupName: this.dataForm.attrGroupName,
sort: this.dataForm.sort,
descript: this.dataForm.descript,
icon: this.dataForm.icon,
// 提交 catelogPath 数组中的最后一个值
catelogId: this.dataForm.catelogPath[this.dataForm.catelogPath.length - 1]
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
// 触发父组件的 refreshDataList 事件后,父组件监听到后调用 getDataList() 函数刷新页面。
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
修改分组时,级联选择器无法成功回显。
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)">修改el-button>
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true;
// 当前组件完全渲染完毕(下一次 DOM 更新结束后)后,调用其指定的回调函数
this.$nextTick(() => {
//
// 调用 AddOrUpdate 组件的 init(id) 方法
this.$refs.addOrUpdate.init(id);
})
}
init(id) {
this.dataForm.attrGroupId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs['dataForm'].resetFields();
// 如果 groupId 不为空,则通过 groupId 查询该属性分组的信息,并且填入表格进行回显。
if (this.dataForm.attrGroupId) {
this.$http({
url: this.$http.adornUrl(`/product/attrGroup/info/${this.dataForm.attrGroupId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
this.dataForm.sort = data.attrGroup.sort;
this.dataForm.descript = data.attrGroup.descript;
this.dataForm.icon = data.attrGroup.icon;
this.dataForm.catelogId = data.attrGroup.catelogId;
// 查询出当前三级分类的完整路径,即 [一级分类ID, 二级分类ID, 三级分类ID]
this.dataForm.catelogPath = data.attrGroup.catelogPath;
}
})
}
})
}
后端代码修改
/**
* AttrGroupEntity
*
* 三级分类完整路径 [一级分类ID, 二级分类ID, 三级分类ID]
*/
private Long[] catelogPath;
/**
* AttrGroupController
*
* 根据 属性分组ID 获取属性分组(根据 分类ID 获取其完整路径)
*/
@RequestMapping("/info/{attrGroupId}")
public R info(@PathVariable("attrGroupId") Long attrGroupId) {
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
Long catelogId = attrGroup.getCatelogId();
Long[] catelogPath = categoryService.getCompletePathByCategoryId(catelogId);
attrGroup.setCatelogPath(catelogPath);
return R.ok().put("attrGroup", attrGroup);
}
@Override
public Long[] getCompletePathByCategoryId(Long catId) {
List<Long> completePath = new ArrayList<>();
completePath = getParentCategoryId(catId, completePath);
Collections.reverse(completePath);
return completePath.toArray(new Long[completePath.size()]);
}
private List<Long> getParentCategoryId(Long catId, List<Long> completePath) {
CategoryEntity category = getById(catId);
completePath.add(category.getCatId());
if (category.getParentCid().longValue() != 0) {
getParentCategoryId(category.getParentCid(), completePath);
}
return completePath;
}
开启搜索选项 & 清空新增弹窗
<el-cascader v-model="dataForm.catelogPath"
:options="categories"
:props="props"
placeholder="试试搜索:手机"
filterable>
el-cascader>
<el-dialog ... @closed="closeDialog">
closeDialog() {
this.dataForm.catelogPath = [];
}
配置 MyBatis Plus 分页插件
@Configuration
@MapperScan("com.sun.mall.product.dao")
public class MyBatisPlusConfiguration {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setOverflow(true);
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}
模糊查询
@Override
public PageUtils queryPage(Map<String, Object> params) {
String key = (String) params.get("key");
LambdaQueryWrapper<BrandEntity> wrapper= new LambdaQueryWrapper<>();
if (StrUtil.isNotBlank(key)) {
wrapper.like(BrandEntity::getBrandId, key).or().like(BrandEntity::getName, key);
}
IPage<BrandEntity> page = this.page(new Query<BrandEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
品牌和分类是多对多的关系:例如小米品牌下有多种类型的产品,而手机、平板分类下又有多种品牌的产品。pms_category_brand_relation
表中保存品牌和分类的多对多关系。
中间表增加冗余字段,brand_name
和 catelog_name
字段,例如根据 brand_id
查询其对应的分类,就能直接获取到 分类名称,而不需要再去分类表中查询,从而提高查询效率。
名称 | 类型 | 说明 |
---|---|---|
id | bigint | 主键ID |
brand_id | bigint | 品牌ID |
catelog_id | bigint | 分类ID |
brand_name | varchar | 品牌名称 |
catalog_name | varchar | 分类名称 |
根据
brandId
查询所有的分类信息。
@GetMapping("/category/list")
public R list(@RequestParam("brandId") Long brandId){
List<CategoryBrandRelationEntity> categoryBrandRelationEntityList = categoryBrandRelationService.getCategoriesByBrandId(brandId);
return R.ok().put("data", categoryBrandRelationEntityList);
}
@Override
public List<CategoryBrandRelationEntity> getCategoriesByBrandId(Long brandId) {
return lambdaQuery().eq(CategoryBrandRelationEntity::getBrandId, brandId).list();
}
新增关联关系
@RequestMapping("/save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation) {
categoryBrandRelationService.addBrandCategoryRelation(categoryBrandRelation);
return R.ok();
}
@Override
public void addBrandCategoryRelation(CategoryBrandRelationEntity categoryBrandRelation) {
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
BrandEntity brand = brandService.getById(brandId);
CategoryEntity category = categoryService.getById(catelogId);
categoryBrandRelation.setBrandName(brand.getName());
categoryBrandRelation.setCatelogName(category.getName());
this.save(categoryBrandRelation);
}
更新品牌或分类的同时需要一同更新关系表的冗余字段。
/**
* 更新品牌表,并且更新品牌分类关联表。
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand) {
brandService.updateBrandAndRelation(brand);
return R.ok();
}
@Transactional
@Override
public void updateBrandAndRelation(BrandEntity brand) {
this.updateById(brand);
CategoryBrandRelationEntity relation = new CategoryBrandRelationEntity();
relation.setBrandId(brand.getBrandId());
relation.setBrandName(brand.getName());
if (StrUtil.isNotBlank(brand.getName())) {
categoryBrandRelationService.update(
relation,
new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brand.getBrandId())
);
}
}
/**
* 更新分类表,并且更新品牌分类关联表。
*/
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category) {
categoryService.updateCategoryAndRelation(category);
return R.ok();
}
@Transactional
@Override
public void updateCategoryAndRelation(CategoryEntity category) {
this.updateById(category);
CategoryBrandRelationEntity relation = new CategoryBrandRelationEntity();
relation.setCatelogId(category.getCatId());
relation.setCatelogName(category.getName());
if (StrUtil.isNotBlank(category.getName())) {
categoryBrandRelationService.update(
relation,
new UpdateWrapper<CategoryBrandRelationEntity>().eq("catelog_id", category.getCatId())
);
}
}
字段 | 类型 | 长度 | 注释 |
---|---|---|---|
attr_id |
bigint |
属性ID | |
attr_name |
char |
30 | 属性名 |
attr_type |
tinyint |
属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性] | |
value_type |
tinyint |
值类型[0-单个值,1-多个值] | |
search_type |
tinyint |
是否需要检索[0-不需要,1-需要] | |
value_select |
char |
255 | 可选值列表[逗号隔开] |
icon |
varchar |
255 | 属性图标 |
catelog_id |
bigint |
所属分类 | |
enable |
bigint |
启用状态[0-禁用,1-启用] | |
show_desc |
tinyint |
快速展示[是否展示在介绍上:0-否,1-是] |
注意:6.1、6.2、6.3 为基本属性(规格参数),6.4、6.5、6.6 为销售属性。
/product/attr/save
;提交该请求时会多出一个数据库中不存在的 groupId
属性,规范的做法是创建一个 VO。@Data
public class AttrVo {
/**
* 属性id
*/
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 值类型[0-为单个值,1-可以选择多个值]
*/
private Integer valueType;
/**
* 属性图标
*/
private String icon;
/**
* 可选值列表[用逗号分隔]
*/
private String valueSelect;
/**
* 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
*/
private Integer attrType;
/**
* 启用状态[0-禁用,1-启用]
*/
private Long enable;
/**
* 所属分类
*/
private Long catelogId;
/**
* 快速展示【是否展示在介绍上;0-否,1-是】,在 SKU 中仍然可以调整。
*/
private Integer showDesc;
/**
* 属性分组 ID
*/
private Long attrGroupId;
}
/**
* 新增属性的同时信息到 属性&属性分组关联表 中
*/
@RequestMapping("/save")
public R save(@RequestBody AttrVo attrVo) {
attrService.saveAttr(attrVo);
return R.ok();
}
@Transactional
@Override
public void saveAttr(AttrVo attrVo) {
AttrEntity attrEntity = new AttrEntity();
BeanUtil.copyProperties(attrVo, attrEntity);
this.save(attrEntity);
AttrAttrgroupRelationEntity relation = new AttrAttrgroupRelationEntity();
relation.setAttrId(attrEntity.getAttrId());
relation.setAttrGroupId(attrVo.getAttrGroupId());
relation.setAttrSort(0);
attrAttrgroupRelationService.save(relation);
}
@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);
}
获取到的 AttrEntity 中没有 所属分类 和 所属分组 属性。
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
LambdaQueryWrapper<AttrEntity> wrapper = new LambdaQueryWrapper<>();
if (catelogId != 0) {
wrapper.eq(AttrEntity::getCatelogId, catelogId);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(one -> {
one.eq(AttrEntity::getAttrId, key).or().like(AttrEntity::getAttrName, key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
创建 AttrResponseVo
,包含 catelogName
和 groupName
属性。
@Data
public class AttrResponseVo extends AttrVo {
/**
* 所属分类
*/
private String catelogName;
/**
* 所属分组
*/
private String groupName;
}
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
LambdaQueryWrapper<AttrEntity> wrapper = new LambdaQueryWrapper<AttrEntity>();
if (catelogId != 0) {
wrapper.eq(AttrEntity::getCatelogId, catelogId);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(one -> {
one.eq(AttrEntity::getAttrId, key).or().like(AttrEntity::getAttrName, key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
PageUtils pageUtils = new PageUtils(page);
List<AttrResponseVo> attrResponseVoList = page.getRecords()
.stream()
.map(attrEntity -> {
AttrResponseVo attrResponseVo = new AttrResponseVo();
BeanUtil.copyProperties(attrEntity, attrResponseVo);
AttrAttrgroupRelationEntity relation = attrAttrgroupRelationService
.lambdaQuery()
.eq(AttrAttrgroupRelationEntity::getAttrId, attrResponseVo.getAttrId())
.one();
if (ObjectUtil.isNotNull(relation)) {
AttrGroupEntity attrGroup = attrGroupService.getById(relation.getAttrGroupId());
attrResponseVo.setGroupName(attrGroup.getAttrGroupName());
}
CategoryEntity category = categoryService.getById(attrResponseVo.getCatelogId());
if (ObjectUtil.isNotNull(category)) {
attrResponseVo.setCatelogName(category.getName());
}
return attrResponseVo;
})
.collect(Collectors.toList());
pageUtils.setList(attrResponseVoList);
return pageUtils;
}
修改属性之前需要先通过
product/attrGroup/info/{attrId}
获取到其详细信息,例如:分组名称、分类的完整路径等。
@Data
public class AttrResponseVo extends AttrVo {
/**
* 所属分类
*/
private String catelogName;
/**
* 所属分组
*/
private String groupName;
/**
* 属性的完整路径
*/
private Long[] catelogPath;
}
/**
* 通过 属性ID 获取属性的详细信息
*/
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId) {
AttrResponseVo attr = attrService.getAttrDetailByAttrId(attrId);
return R.ok().put("attr", attr);
}
@Override
public AttrResponseVo getAttrDetailByAttrId(Long attrId) {
AttrEntity attrEntity = getById(attrId);
AttrResponseVo attrResponseVo = new AttrResponseVo();
BeanUtil.copyProperties(attrEntity, attrResponseVo);
// 设置分组信息
AttrAttrgroupRelationEntity relation = attrAttrgroupRelationService
.lambdaQuery()
.eq(AttrAttrgroupRelationEntity::getAttrId, attrId)
.one();
if (ObjectUtil.isNotNull(relation)) {
attrResponseVo.setAttrGroupId(relation.getAttrGroupId());
AttrGroupEntity attrGroup = attrGroupService.getById(relation.getAttrGroupId());
if (ObjectUtil.isNotNull(attrGroup)) {
attrResponseVo.setGroupName(attrGroup.getAttrGroupName());
}
}
// 设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] path = categoryService.getCompletePathByCategoryId(catelogId);
attrResponseVo.setCatelogPath(path);
CategoryEntity categoryEntity = categoryService.getById(catelogId);
if (ObjectUtil.isNotNull(categoryEntity)) {
attrResponseVo.setCatelogName(categoryEntity.getName());
}
return attrResponseVo;
}
修改
attr
的同时还需要修改attr_attrgroup_relation
表。
@RequestMapping("/update")
public R update(@RequestBody AttrVo attrVo) {
attrService.updateAttr(attrVo);
return R.ok();
}
@Transactional
@Override
public void updateAttr(AttrVo attrVo) {
AttrEntity attrEntity = new AttrEntity();
BeanUtil.copyProperties(attrVo, attrEntity);
this.updateById(attrEntity);
AttrAttrgroupRelationEntity relation = new AttrAttrgroupRelationEntity();
relation.setAttrId(attrVo.getAttrId());
relation.setAttrGroupId(attrVo.getAttrGroupId());
Integer count = attrAttrgroupRelationService.lambdaQuery().eq(AttrAttrgroupRelationEntity::getAttrId, attrVo.getAttrId()).count();
if (count > 0) {
attrAttrgroupRelationService.update(
relation,
new LambdaUpdateWrapper<AttrAttrgroupRelationEntity>().eq(AttrAttrgroupRelationEntity::getAttrId, attrVo.getAttrId())
);
} else {
attrAttrgroupRelationService.save(relation);
}
}
在
mall-common
中创建ProductConstants
。
public class ProductConstants {
public enum AttrEnum {
ATTR_TYPE_BASE(1, "基本属性"),
ATTR_TYPE_SALE(0, "销售属性");
private Integer code;
private String msg;
AttrEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}
修改 6.2 处代码(判断是 基础属性 or 销售属性)。
/**
* 获取分类的 基本属性 or 销售属性
*/
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
@PathVariable("attrType") String attrType,
@PathVariable("catelogId") Long catelogId) {
PageUtils page = attrService.queryBaseAttrOrSaleAttrPage(params, catelogId);
return R.ok().put("page", page);
}
@Override
public PageUtils queryBaseAttrOrSaleAttrPage(Map<String, Object> params, String attrType, Long catelogId) {
// 请求路径中的 {attrType} 为 base,则查询条件为 attrType = 1;否则,查询条件为 attrType = 0。
LambdaQueryWrapper<AttrEntity> wrapper = new LambdaQueryWrapper<AttrEntity>()
.eq(AttrEntity::getAttrType, "base".equals(attrType)
? ProductConstants.AttrEnum.ATTR_TYPE_BASE.getCode()
: ProductConstants.AttrEnum.ATTR_TYPE_SALE.getCode());
if (catelogId != 0) {
wrapper.eq(AttrEntity::getCatelogId, catelogId);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(one -> {
one.eq(AttrEntity::getAttrId, key).or().like(AttrEntity::getAttrName, key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
PageUtils pageUtils = new PageUtils(page);
List<AttrResponseVo> attrResponseVoList = page.getRecords()
.stream()
.map(attrEntity -> {
AttrResponseVo attrResponseVo = new AttrResponseVo();
BeanUtil.copyProperties(attrEntity, attrResponseVo);
if ("base".equalsIgnoreCase(attrType)) {
AttrAttrgroupRelationEntity relation = attrAttrgroupRelationService
.lambdaQuery()
.eq(AttrAttrgroupRelationEntity::getAttrId, attrResponseVo.getAttrId())
.one();
if (ObjectUtil.isNotNull(relation) && relation.getAttrGroupId() != null) {
AttrGroupEntity attrGroup = attrGroupService.getById(relation.getAttrGroupId());
attrResponseVo.setGroupName(attrGroup.getAttrGroupName());
}
}
CategoryEntity category = categoryService.getById(attrResponseVo.getCatelogId());
if (ObjectUtil.isNotNull(category)) {
attrResponseVo.setCatelogName(category.getName());
}
return attrResponseVo;
})
.collect(Collectors.toList());
pageUtils.setList(attrResponseVoList);
return pageUtils;
}
修改 6.1 处代码(判断是否为 基础属性,再同步新增到 属性-属性分组表)。
@Transactional
@Override
public void saveAttr(AttrVo attrVo) {
AttrEntity attrEntity = new AttrEntity();
BeanUtil.copyProperties(attrVo, attrEntity);
this.save(attrEntity);
if (attrVo.getAttrType().equals(ProductConstants.AttrEnum.ATTR_TYPE_BASE.getCode()) && attrVo.getAttrGroupId() != null) {
AttrAttrgroupRelationEntity relation = new AttrAttrgroupRelationEntity();
relation.setAttrId(attrEntity.getAttrId());
relation.setAttrGroupId(attrVo.getAttrGroupId());
relation.setAttrSort(0);
attrAttrgroupRelationService.save(relation);
}
}
修改 6.3 处代码(判断是否为 基础属性,再设置分组信息;再同步修改 or 新增到 属性-属性分组表)。
@Override
public AttrResponseVo getAttrDetailByAttrId(Long attrId) {
AttrEntity attrEntity = getById(attrId);
AttrResponseVo attrResponseVo = new AttrResponseVo();
BeanUtil.copyProperties(attrEntity, attrResponseVo);
// 设置分组信息
if (attrEntity.getAttrType().equals(ProductConstants.AttrEnum.ATTR_TYPE_BASE.getCode())) {
AttrAttrgroupRelationEntity relation = attrAttrgroupRelationService
.lambdaQuery()
.eq(AttrAttrgroupRelationEntity::getAttrId, attrId)
.one();
if (ObjectUtil.isNotNull(relation)) {
attrResponseVo.setAttrGroupId(relation.getAttrGroupId());
AttrGroupEntity attrGroup = attrGroupService.getById(relation.getAttrGroupId());
if (ObjectUtil.isNotNull(attrGroup)) {
attrResponseVo.setGroupName(attrGroup.getAttrGroupName());
}
}
}
// 设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] path = categoryService.getCompletePathByCategoryId(catelogId);
attrResponseVo.setCatelogPath(path);
CategoryEntity categoryEntity = categoryService.getById(catelogId);
if (ObjectUtil.isNotNull(categoryEntity)) {
attrResponseVo.setCatelogName(categoryEntity.getName());
}
return attrResponseVo;
}
@Transactional
@Override
public void updateAttr(AttrVo attrVo) {
AttrEntity attrEntity = new AttrEntity();
BeanUtil.copyProperties(attrVo, attrEntity);
this.updateById(attrEntity);
if (attrEntity.getAttrType().equals(ProductConstants.AttrEnum.ATTR_TYPE_BASE.getCode())) {
AttrAttrgroupRelationEntity relation = new AttrAttrgroupRelationEntity();
relation.setAttrId(attrVo.getAttrId());
relation.setAttrGroupId(attrVo.getAttrGroupId());
Integer count = attrAttrgroupRelationService.lambdaQuery().eq(AttrAttrgroupRelationEntity::getAttrId, attrVo.getAttrId()).count();
if (count > 0) {
attrAttrgroupRelationService.update(
relation,
new LambdaUpdateWrapper<AttrAttrgroupRelationEntity>().eq(AttrAttrgroupRelationEntity::getAttrId, attrVo.getAttrId())
);
} else {
attrAttrgroupRelationService.save(relation);
}
}
}
根据 groupId 获取该分组所关联的属性。
/**
* 根据 groupId 查询 当前分组关联的所有属性
*/
@GetMapping("/{groupId}/attr/relation")
public R getAssociatedAttrByGroupId(@PathVariable("groupId") Long groupId) {
List<AttrEntity> attrEntityList = attrService.getAssociatedAttrByGroupId(groupId);
return R.ok().put("data", attrEntityList);
}
@Override
public List<AttrEntity> getAssociatedAttrByGroupId(Long groupId) {
return attrAttrgroupRelationService
// 根据 groupId 获取到 List
.lambdaQuery()
.eq(AttrAttrgroupRelationEntity::getAttrGroupId, groupId)
.list()
.stream()
// 遍历 List 获取到 List
.map(relation -> this.getById(relation.getAttrId()))
.collect(Collectors.toList());
}
删除 属性分组与其关联属性 的 关联关系。
@Data
public class AttrGroupRelationVo {
private Long attrId;
private Long attrGroupId;
}
/**
* 删除 属性分组 和 属性 间的关联
*/
@PostMapping("/attr/relation/delete")
public R deleteTheRelations(@RequestBody List<AttrGroupRelationVo> attrGroupRelationVoList) {
attrService.deleteTheRelations(attrGroupRelationVoList);
return R.ok();
}
@Override
public void deleteTheRelations(List<AttrGroupRelationVo> attrGroupRelationVoList) {
List<AttrAttrgroupRelationEntity> relationList = attrGroupRelationVoList
.stream()
.map(item -> {
AttrAttrgroupRelationEntity relation = new AttrAttrgroupRelationEntity();
BeanUtil.copyProperties(item, relation);
return relation;
})
.collect(Collectors.toList());
// DELETE FROM psm_attr_attrgroup_relation WHERE (attr_id = ? AND attr_group_id = ?) OR (attr_id = ? AND attr_group_id = ?) OR ...
AttrAttrgroupRelationDao.deleteBatchByRelations(relationList);
}
@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {
void deleteBatchByRelations(@Param("relationList") List<AttrAttrgroupRelationEntity> relationList);
}
<delete id="deleteBatchByRelations">
DELETE FROM mall_pms.pms_attr_attrgroup_relation
<where>
<foreach collection="relationList" item="item" separator=" OR ">
(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId})
foreach>
where>
delete>
获取属性分组中,还没有关联的、本分类中的其他属性,方便添加新的关联。
- 当前分组只能关联其所属的分类中的属性;
- 当前分组只能关联其他分组未引用的属性。
/**
* 根据 groupId 查询 当前分组未关联的所有属性
*/
@GetMapping("/{groupId}/noAttr/relation")
public R getNotAssociatedAttrByGroupId(@PathVariable("groupId") Long groupId,
@RequestParam Map<String, Object> params) {
PageUtils page = attrService.getNotAssociatedAttrByGroupId(groupId, params);
return R.ok().put("page", page);
}
@Override
public PageUtils getNotAssociatedAttrByGroupId(Long groupId, Map<String, Object> params) {
// 当前分组只能关联 自己所属分类中的属性;当前分组只能关联 其所属分组中其他分组未引用的属性。
// 找到其他分组关联的属性,并且将其排除。(当前分组所关联的属性也需要排除)
AttrGroupEntity attrGroup = attrGroupService.getById(groupId);
Long catelogId = attrGroup.getCatelogId();
// 找到当前 分类ID 对应的所有 attrGroup
List<AttrGroupEntity> attrGroupEntityList = attrGroupService
.lambdaQuery()
.eq(AttrGroupEntity::getCatelogId, catelogId)
.list();
List<Long> attrGroupIdList = attrGroupEntityList
.stream()
.map(item -> item.getAttrGroupId())
.collect(Collectors.toList());
// 获取该分类下所有分组所关联的属性。
List<AttrAttrgroupRelationEntity> relationList = attrAttrgroupRelationService
.lambdaQuery()
.in(AttrAttrgroupRelationEntity::getAttrGroupId, attrGroupIdList)
.list();
List<Long> attrIdList = relationList
.stream()
.map(item -> item.getAttrId())
.collect(Collectors.toList());
// 排除
LambdaQueryWrapper<AttrEntity> wrapper = new LambdaQueryWrapper<AttrEntity>()
.eq(AttrEntity::getCatelogId, catelogId)
.eq(AttrEntity::getAttrType, ProductConstants.AttrEnum.ATTR_TYPE_BASE.getCode());
if (!attrIdList.isEmpty() && ObjectUtil.isNotNull(attrIdList)) {
wrapper.notIn(AttrEntity::getAttrId, attrIdList);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(w -> {
w.eq(AttrEntity::getAttrId, key).or().like(AttrEntity::getAttrName, key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
/**
* 添加属性和分组的关联关系
*/
@PostMapping("/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVo> attrGroupRelationVoList) {
attrAttrgroupRelationService.saveBatchAssociatedRelation(attrAttrgroupRelationService);
return R.ok();
}
@Override
public void saveBatchAssociatedRelation(List<AttrGroupRelationVo> attrGroupRelationVoList) {
List<AttrAttrgroupRelationEntity> relationList = attrGroupRelationVoList
.stream()
.map(item -> {
AttrAttrgroupRelationEntity relation = new AttrAttrgroupRelationEntity();
BeanUtil.copyProperties(item, relation);
return relation;
})
.collect(Collectors.toList());
this.saveBatch(relationList);
}
用户系统 - 会员等级:/member/memberLevel/list
:获取所有会员等级(该方法已经生成,启动 mall-member
服务即可)
配置网关路由:
server:
port: 9999
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
- id: product_route
uri: lb://mall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: third_party_route
uri: lb://mall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty(?>.*),/$\{segment}
- id: member_route
uri: lb://mall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
/**
* 根据 catId 获取该分类关联的品牌
*/
@GetMapping("/brands/list")
public R brandRelationList(@RequestParam(value = "catId", required = true) Long catId) {
List<BrandEntity> brandEntityList = categoryBrandRelationService.getBrandsByCatId(catId);
List<BrandVo> brandVoList = brandEntityList
.stream()
.map(brandEntity -> {
BrandVo brandVo = new BrandVo();
brandVo.setBrandId(brandEntity.getBrandId());
brandVo.setBrandName(brandEntity.getName());
return brandVo;
}).collect(Collectors.toList());
return R.ok().put("data", brandVoList);
}
@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
List<CategoryBrandRelationEntity> relationList = this.lambdaQuery()
.eq(CategoryBrandRelationEntity::getCatelogId, catId)
.list();
return relationList
.stream()
.map(item -> brandService.getById(item.getBrandId()))
.collect(Collectors.toList());
}
@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;
}
@Override
public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrsByCategoryId(Long catelogId) {
List<AttrGroupEntity> attrGroupList = this.lambdaQuery().eq(AttrGroupEntity::getCatelogId, catelogId).list();
return attrGroupList
.stream()
.map(attrGroup -> {
AttrGroupWithAttrsVo attrGroupWithAttrsVo = new AttrGroupWithAttrsVo();
BeanUtil.copyProperties(attrGroup, attrGroupWithAttrsVo);
// 根据 groupId 查询当前分组关联的所有属性
attrGroupWithAttrsVo.setAttrs(attrService.getAssociatedAttrByGroupId(attrGroup.getAttrGroupId()));
return attrGroupWithAttrsVo;
})
.collect(Collectors.toList());
}
抽取 VO
SpuSaveVo
@Data
public class SpuSaveVo {
private String spuName;
private String spuDescription;
private Long catalogId;
private Long brandId;
private BigDecimal weight;
private Integer publishStatus;
/**
* 描述信息
*/
private List<String> decript;
/**
* 图片集
*/
private List<String> images;
/**
* 规格参数
*/
private List<BaseAttrs> baseAttrs;
/**
* 积分信息
*/
private Bounds bounds;
/**
* Sku 信息
*/
private List<Skus> skus;
}
*** BaseAttrs***
/**
* 规格参数
*/
@Data
public class BaseAttrs {
private Long attrId;
private String attrValues;
private Integer showDesc;
}
Skus
@Data
public class Skus {
private String skuName;
private BigDecimal price;
private String skuTitle;
private String skuSubtitle;
private Integer fullCount;
private BigDecimal discount;
private Integer countStatus;
private BigDecimal fullPrice;
private BigDecimal reducePrice;
private Integer priceStatus;
private List<String> descar;
/**
* 图片信息
*/
private List<Images> images;
/**
* 销售属性
*/
private List<Attr> attr;
/**
* 会员价格
*/
private List<MemberPrice> memberPrice;
}
Attr
/**
* 销售属性
*/
@Data
public class Attr {
private Long attrId;
private String attrName;
private String attrValue;
}
Images
/**
* 图片信息
*/
@Data
public class Images {
private String imgUrl;
private Integer defaultImg;
}
Bounds
/**
*
*/
@Data
public class Bounds {
private BigDecimal buyBounds;
private BigDecimal growBounds;
}
MemberPrice(mall-common
)
/**
* 会员价格
*/
@Data
public class MemberPrice {
private Long id;
private String name;
private BigDecimal price;
}
需要保存的信息
pms_spu_info
;pms_spu_info_desc
;pms_spu_images
;pms_product_attr_value
;mall_sms.sms_spu_bounds
;pms_sku_info
;pms_sku_images
;pms_sku_sale_attr_value
;mall_sms.sms_sku_ladder / sms_sku_full_reduction / sms_member_price
。
StrUtil.join(CharSequence conjunction, Iterable
:以 conjunction 为分隔符将多个对象转换为字符串。iterable)
@Test
public void strJoinTest() {
List<String> list = Arrays.asList("你好", "好好学习", "天天向上");
System.out.println(list);
String join = StrUtil.join(" - ", list);
System.out.println(join);
// [你好, 好好学习, 天天向上]
// 你好 - 好好学习 - 天天向上
}
远程调用
在 mall-common
中创建 SpuBoundsDTO
和 SkuReductionDTO
;
@Data
public class SpuBoundsDTO {
private Long spuId;
private BigDecimal buyBounds;
private BigDecimal growBounds;
}
@Data
public class SkuReductionDTO {
private Long skuId;
private Integer fullCount;
private BigDecimal discount;
private Integer countStatus;
private BigDecimal fullPrice;
private BigDecimal reducePrice;
private Integer priceStatus;
private List<MemberPrice> memberPrice;
}
在 mall-product
主启动类上标注 @EnableFeignClients(basePackages = "com.sun.mall.product.feign")
注解;
创建 feign/CouponFeignService
,调用 /coupon/spuBounds/save
和 /coupon/spuBounds/saveInfo
两个接口;
@Component
@FeignClient("mall-coupon")
public interface CouponFeignService {
/**
* 1. 服务启动,自动扫描主启动类上 @EnableFeignClients 注解所指向的包;即使不指定 basePackages,该注解也会扫描到标注 @FeignClient 注解的接口。
* 2. 在该包中找到标注 @FeignClient 的接口,该注解会在注册中心中找到对应的服务。
* 3. 当在 Controller 中调用该接口的方法时,该服务会进行处理。
*
* save(@RequestBody SpuBoundsDTO spuBoundsDTO)
* @RequestBody 将这个对象转化为 JSON,找到 mall-coupon 服务;发送 /coupon/spuBounds/save 请求;将上一步转换的 JSON 放在请求体发送数据。
*
* save(@RequestBody SpuBoundsEntity spuBounds)
* mall-coupon 服务接收到请求,请求体中的 JSON 数据会被 @RequestBody 转换为 SpuBoundsEntity;只要 JSON 数据是兼容的,不需要使用同一个 POJO。
*/
@PostMapping("/coupon/spuBounds/save")
R save(@RequestBody SpuBoundsDTO spuBoundsDTO);
@PostMapping("/coupon/skuFullReduction/saveSkuReduction")
R saveSkuReduction(@RequestBody SkuReductionDTO skuReductionDTO);
}
mall-coupon - SpuBoundsController & SkuFullReductionController
@RestController
@RequestMapping("/coupon/spuBounds")
public class SpuBoundsController {
@Autowired
private SpuBoundsService spuBoundsService;
// 第一个服务已经生成,直接调用即可。
@RequestMapping("/save")
public R saveSpuBounds(@RequestBody SpuBoundsEntity spuBoundsEntity) {
spuBoundsService.save(spuBoundsEntity);
return R.ok();
}
}
@RestController
@RequestMapping("/coupon/skuFullReduction")
public class SkuFullReductionController {
@Autowired
private SkuFullReductionService skuFullReductionService;
@PostMapping("/coupon/skuFullReduction/saveSkuReduction")
public R saveSkuReduction(@RequestBody SkuReductionDTO skuReductionDTO) {
skuFullReductionService.saveSkuReduction(skuReductionDTO);
return R.ok();
}
}
// TODO 待完善
@Transactional
@Override
public void saveSkuReduction(SkuReductionDTO skuReductionDTO) {
// 满减打折、会员价 `sms_sku_ladder`
SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
skuLadderEntity.setSkuId(skuReductionDTO.getSkuId());
skuLadderEntity.setFullCount(skuReductionDTO.getFullCount());
skuLadderEntity.setDiscount(skuReductionDTO.getDiscount());
skuLadderEntity.setAddOther(skuReductionDTO.getCountStatus());
if (skuLadderEntity.getFullCount() > 0) {
skuLadderService.save(skuLadderEntity);
}
// 满减信息 `sms_sku_full_reduction`
SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
BeanUtil.copyProperties(skuReductionDTO, skuFullReductionEntity);
if (skuFullReductionEntity.getFullPrice().compareTo(BigDecimal.ZERO) == 1) {
this.save(skuFullReductionEntity);
}
// 会员价格 `sms_member_price`
List<MemberPrice> memberPriceList = skuReductionDTO.getMemberPrice();
List<MemberPriceEntity> memberPriceEntityList = memberPriceList
.stream()
.filter(memberPrice -> memberPrice.getPrice().compareTo(BigDecimal.ZERO) == 1)
.map(memberPrice -> {
MemberPriceEntity memberPriceEntity = new MemberPriceEntity();
memberPriceEntity.setSkuId(skuReductionDTO.getSkuId());
memberPriceEntity.setMemberLevelId(memberPrice.getId());
memberPriceEntity.setMemberPrice(memberPrice.getPrice());
memberPriceEntity.setMemberLevelName(memberPrice.getName());
memberPriceEntity.setAddOther(1);
return memberPriceEntity;
}).collect(Collectors.toList());
memberPriceService.saveBatch(memberPriceEntityList);
}
为统一返回类 R
添加一个方法。
public Integer getCode() {
return (Integer) this.get("code");
}
saveSpuInfo
@RequestMapping("/save")
public R save(@RequestBody SpuSaveVo spuSaveVo) {
spuInfoService.saveSpuInfo(spuSaveVo);
return R.ok();
}
@Resource
private SpuInfoDescService spuInfoDescService;
@Resource
private SpuImagesService spuImagesService;
@Resource
private ProductAttrValueService productAttrValueService;
@Resource
private AttrService attrService;
@Resource
private SkuInfoService skuInfoService;
@Resource
private SkuImagesService skuImagesService;
@Resource
private SkuSaleAttrValueService skuSaleAttrValueService;
@Resource
private CouponFeignService couponFeignService;
// TODO 待完善
@Transactional
@Override
public void saveSpuInfo(SpuSaveVo spuSaveVo) {
// 1. 保存 Spu 的基本信息 - `pms_spu_info`;
SpuInfoEntity spuInfoEntity = new SpuInfoEntity();
BeanUtil.copyProperties(spuSaveVo, spuInfoEntity);
spuInfoEntity.setCreateTime(new Date());
spuInfoEntity.setUpdateTime(new Date());
this.save(spuInfoEntity);
// 2. 保存 Spu 的描述信息 - `pms_spu_info_desc`;
List<String> descList = spuSaveVo.getDecript();
SpuInfoDescEntity spuInfoDescEntity = new SpuInfoDescEntity();
spuInfoDescEntity.setSpuId(spuInfoEntity.getId());
spuInfoDescEntity.setDecript(StrUtil.join(", ", descList));
spuInfoDescService.save(spuInfoDescEntity);
// 3. 保存 Spu 的图片集 - `pms_spu_images`;
List<String> images = spuSaveVo.getImages();
if (CollectionUtil.isEmpty(images) || ObjectUtil.isNull(images)) {
List<SpuImagesEntity> spuImagesList = images
.stream()
.map(image -> {
SpuImagesEntity spuImagesEntity = new SpuImagesEntity();
spuImagesEntity.setSpuId(spuInfoEntity.getId());
spuImagesEntity.setImgUrl(image);
return spuImagesEntity;
})
.collect(Collectors.toList());
spuImagesService.saveBatch(spuImagesList);
}
// 4. 保存 Spu 的规格参数 - `pms_product_attr_value`;
List<BaseAttrs> baseAttrsList = spuSaveVo.getBaseAttrs();
List<ProductAttrValueEntity> productAttrValueList = baseAttrsList.stream()
.map(baseAttr -> {
ProductAttrValueEntity productAttrValue = new ProductAttrValueEntity();
productAttrValue.setAttrId(baseAttr.getAttrId());
productAttrValue.setAttrName(attrService.getById(baseAttr.getAttrId()).getAttrName());
productAttrValue.setAttrValue(baseAttr.getAttrValues());
productAttrValue.setQuickShow(baseAttr.getShowDesc());
productAttrValue.setSpuId(spuInfoEntity.getId());
return productAttrValue;
}).collect(Collectors.toList());
productAttrValueService.saveBatch(productAttrValueList);
// 5. 保存 Spu 的积分信息 - `mall_sms.sms_spu_bounds`;
Bounds bounds = spuSaveVo.getBounds();
SpuBoundsDTO spuBoundsDTO = new SpuBoundsDTO();
BeanUtil.copyProperties(bounds, spuBoundsDTO);
spuBoundsDTO.setSpuId(spuInfoEntity.getId());
R rpcResult = couponFeignService.save(spuBoundsDTO);
if (rpcResult.getCode() != 0) {
log.error("远程保存 Spu 的积分信息失败");
}
// 6. 保存 Spu 对应的 Sku 信息。
List<Skus> skusList = spuSaveVo.getSkus();
if (!skusList.isEmpty() && ObjectUtil.isNotNull(skusList)) {
skusList.forEach(sku -> {
// 找到默认图片(SkuDefaultImg 为 1)
String defaultImage = "";
for (Images image : sku.getImages()) {
if (image.getDefaultImg() == 1) {
defaultImage = image.getImgUrl();
}
}
// 6.1 Sku 的基本信息 - `pms_sku_info`;
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtil.copyProperties(sku, skuInfoEntity);
skuInfoEntity.setSpuId(spuInfoEntity.getId());
skuInfoEntity.setBrandId(spuInfoEntity.getBrandId());
skuInfoEntity.setCatalogId(spuInfoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSkuDefaultImg(defaultImage);
skuInfoService.save(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
// 6.2 Sku 的图片信息 - `pms_sku_images`;
List<SkuImagesEntity> skuImagesList = sku.getImages()
.stream()
.filter(image -> StrUtil.isNotBlank(image.getImgUrl()))
.map(image -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(image.getImgUrl());
skuImagesEntity.setDefaultImg(image.getDefaultImg());
return skuImagesEntity;
})
.collect(Collectors.toList());
skuImagesService.saveBatch(skuImagesList);
// 6.3 Sku 的销售属性信息 - `pms_sku_sale_attr_value`;
List<Attr> attrList = sku.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueList = attrList
.stream()
.map(attr -> {
SkuSaleAttrValueEntity skuSaleAttrValueEntity = new SkuSaleAttrValueEntity();
BeanUtil.copyProperties(attr, skuSaleAttrValueEntity);
skuSaleAttrValueEntity.setSkuId(skuId);
return skuSaleAttrValueEntity;
})
.collect(Collectors.toList());
skuSaleAttrValueService.saveBatch(skuSaleAttrValueList);
// 6.4 Sku 的优惠、满减、会员信息 - `mall_sms.sms_sku_ladder / sms_sku_full_reduction / sms_member_price`。
SkuReductionDTO skuReductionDTO = new SkuReductionDTO();
BeanUtil.copyProperties(sku, skuReductionDTO);
skuReductionDTO.setSkuId(skuId);
// fullCount - 满多少件;fullPrice - 满多少钱。
if (skuReductionDTO.getFullCount() > 0 || skuReductionDTO.getFullPrice().compareTo(BigDecimal.ZERO) == 1) {
R anotherRpcResult = couponFeignService.saveSkuReduction(skuReductionDTO);
if (rpcResult.getCode() != 0) {
log.error("远程保存 Sku 优惠信息失败");
}
}
});
}
}
/**
* SPU 检索
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = spuInfoService.querySpuInfoPageByParams(params);
return R.ok().put("page", page);
}
@Override
public PageUtils querySpuInfoPageByParams(Map<String, Object> params) {
LambdaQueryWrapper<SpuInfoEntity> wrapper = new LambdaQueryWrapper<>();
String catelogId = (String) params.get("catelogId");
if (StrUtil.isNotBlank(catelogId) && !"0".equalsIgnoreCase(catelogId)) {
wrapper.eq(SpuInfoEntity::getCatalogId, catelogId);
}
String brandId = (String) params.get("brandId");
if (StrUtil.isNotBlank(brandId) && !"0".equalsIgnoreCase(brandId)) {
wrapper.eq(SpuInfoEntity::getBrandId, brandId);
}
String status = (String) params.get("status");
if (StrUtil.isNotBlank(status)) {
wrapper.eq(SpuInfoEntity::getPublishStatus, status);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(w -> {
w.eq(SpuInfoEntity::getId, key)
.or()
.like(SpuInfoEntity::getSpuName, key);
});
}
IPage page = this.page(new Query().getPage(params), wrapper);
return new PageUtils(page);
}
格式化时间格式:1. 在属性上使用
@JsonFormat
注解配置; 2. 在配置文件中对所有时间数据进行格式化。
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date createTime;
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date updateTime;
jackson:
date-format: yyyy-MM-dd HH:mm:ss
/**
* 检索 SKU
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = skuInfoService.querySkuInfoPageByParams(params);
return R.ok().put("page", page);
}
@Override
public PageUtils querySkuInfoPageByParams(Map<String, Object> params) {
LambdaQueryWrapper<SkuInfoEntity> wrapper = new LambdaQueryWrapper<>();
String catelogId = (String) params.get("catelogId");
if (StrUtil.isNotBlank(catelogId) && !"0".equalsIgnoreCase(catelogId)) {
wrapper.eq(SkuInfoEntity::getCatalogId, catelogId);
}
String brandId = (String) params.get("brandId");
if (StrUtil.isNotBlank(brandId) && !"0".equalsIgnoreCase(brandId)) {
wrapper.eq(SkuInfoEntity::getBrandId, brandId);
}
String min = (String) params.get("min");
if (StrUtil.isNotBlank(min)) {
wrapper.ge(SkuInfoEntity::getPrice, min);
}
String max = (String) params.get("max");
if (StrUtil.isNotBlank(max) && new BigDecimal(max).compareTo(BigDecimal.ZERO) == 1) {
wrapper.le(SkuInfoEntity::getPrice, max);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(w -> {
w.eq(SkuInfoEntity::getSkuId, key)
.or()
.like(SkuInfoEntity::getSkuName, key);
});
}
IPage page = this.page(new Query().getPage(params), wrapper);
return new PageUtils(page);
}
/**
* 获取 SPU 规格
*/
@GetMapping("/base/listForSpu/${spuId}")
public R baseListForSpu(@PathVariable("spuId") Long spuId) {
List<ProductAttrValueEntity> productAttrValueEntityList = productAttrValueService.getBaseListForSpu(spuId);
return R.ok().put("data", productAttrValueEntityList);
}
@Override
public List<ProductAttrValueEntity> getBaseListForSpu(Long spuId) {
return lambdaQuery().eq(ProductAttrValueEntity::getSpuId, spuId).list();
}
在 SPU 管理页面点击商品后的 规格,若出现 400 错误页面,需要在
/src/router/index.js
中添加以下代码。
{
path: '/product-attrupdate',
component: _import('modules/product/attrupdate'),
name: 'attr-update',
meta: {title: '规格维护', isTab: true}
}
/**
* 修改商品规格
*/
@PostMapping("/update/{spuId}")
public R updateSpuAttr(Long spuId, List<ProductAttrValueEntity> productAttrValueList) {
productAttrValueService.updateSpuAttr(spuId, productAttrValueList);
return R.ok();
}
@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> productAttrValueList) {
// 更新规格参数时,有的数据会新增、有的数据会修改、有的数据会被删除;可以直接删除所有的属性,然后直接新增即可。
this.remove(lambdaQuery().eq(ProductAttrValueEntity::getSpuId, spuId));
for (ProductAttrValueEntity productAttrValueEntity : productAttrValueList) {
productAttrValueEntity.setSpuId(spuId);
}
this.saveBatch(productAttrValueList);
}
wms_ware_info
:每个仓库的信息;wms_ware_sku
:每个仓库中 SKU 商品的信息。
/**
* 根据 params 条件分页查询展示 仓库信息
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = wareInfoService.queryWareInfoPageByParams(params);
return R.ok().put("page", page);
}
@Override
public PageUtils queryWareInfoPageByParams(Map<String, Object> params) {
LambdaQueryWrapper<WareInfoEntity> wrapper = new LambdaQueryWrapper<>();
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.eq(WareInfoEntity::getId, key)
.or().like(WareInfoEntity::getName, key)
.or().like(WareInfoEntity::getAddress, key)
.or().like(WareInfoEntity::getAreacode, key);
}
IPage<WareInfoEntity> page = this.page(new Query<WareInfoEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
/**
* 根据 params 条件分页查询展示 商品库存信息
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = wareSkuService.queryWareSkuPageByParams(params);
return R.ok().put("page", page);
}
@Override
public PageUtils queryWareSkuPageByParams(Map<String, Object> params) {
LambdaQueryWrapper<WareSkuEntity> wrapper = new LambdaQueryWrapper<>();
String wareId = (String) params.get("wareId");
if (StrUtil.isNotBlank(wareId) && !"0".equalsIgnoreCase(wareId)) {
wrapper.eq(WareSkuEntity::getWareId, wareId);
}
String skuId = (String) params.get("skuId");
if (StrUtil.isNotBlank(skuId) && !"0".equalsIgnoreCase(skuId)) {
wrapper.eq(WareSkuEntity::getSkuId, skuId);
}
IPage<WareSkuEntity> page = this.page(new Query<WareSkuEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
/**
* 根据 params 条件分页查询展示 采购需求
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = purchaseDetailService.queryPurchaseDetailPageByParams(params);
return R.ok().put("page", page);
}
@Override
public PageUtils queryPurchaseDetailPageByParams(Map<String, Object> params) {
LambdaQueryWrapper<PurchaseDetailEntity> wrapper = new LambdaQueryWrapper<>();
String status = (String) params.get("status");
if (StrUtil.isNotBlank(status)) {
wrapper.eq(PurchaseDetailEntity::getStatus, status);
}
String wareId = (String) params.get("wareId");
if (StrUtil.isNotBlank(wareId) && !"0".equalsIgnoreCase(wareId)) {
wrapper.eq(PurchaseDetailEntity::getWareId, wareId);
}
String key = (String) params.get("key");
if (StrUtil.isNotBlank(key)) {
wrapper.and(w -> {
w.eq(PurchaseDetailEntity::getSkuId, key)
.or().eq(PurchaseDetailEntity::getPurchaseId, key);
});
}
IPage<PurchaseDetailEntity> page = this.page(new Query<PurchaseDetailEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
采购简要流程
创建
WareConstants
仓储服务相关的枚举类。
public class WareConstants {
/**
* 采购单状态枚举
*/
public enum PurchaseStatusEnum {
CREATED(0, "新建"),
ASSIGNED(1, "已分配"),
RECEIVED(2, "已领取"),
FINISHED(3, "已完成"),
HAS_ERROR(4, "有异常");
private Integer code;
private String msg;
PurchaseStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
/**
* 采购需求枚举
*/
public enum PurchaseDetailStatusEnum {
CREATED(0, "新建"),
ASSIGNED(1, "已分配"),
BUYING(2, "正在采购"),
FINISHED(3, "已完成"),
HAS_ERROR(4, "采购失败");
private Integer code;
private String msg;
PurchaseDetailStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}
点击合并采购单时,需要查询出 新建、已分配 状态的采购单。
/**
* 根据 params 条件查询展示 新建、已分配 状态的采购单
*/
@GetMapping("/unreceived/list")
public R getUnreceivedPurchase(@RequestParam Map<String, Object> params) {
PageUtils page = purchaseService.queryUnreceivedPurchasePageByParams(params);
return R.ok().put("page", page);
}
@Override
public PageUtils queryUnreceivedPurchasePageByParams(Map<String, Object> params) {
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
new LambdaQueryWrapper<PurchaseEntity>()
// 0-新建;1-已分配
.eq(PurchaseEntity::getStatus, WareConstants.PurchaseStatusEnum.CREATED.getCode())
.or()
.eq(PurchaseEntity::getStatus, WareConstants.PurchaseStatusEnum.ASSIGNED.getCode())
);
return new PageUtils(page);
}
合并采购需求到采购单
- 创建一个用户,然后将采购单分配给该用户;
- 此时点击合并采购需求时,即可查询到 分配到该用户的那个采购单。
@Data
public class MergeVo {
/**
* 整单 ID
*/
private Long purchaseId;
/**
* 合并项(采购需求)的 ID 集合
*/
private List<Long> items;
}
/**
* 合并采购需求到采购单(如果没有采购单则新建一个采购单再合并到里面)
*/
@PostMapping("/merge")
public R mergePurchaseDetails(@RequestBody MergeVo mergeVo) {
purchaseService.mergePurchaseDetailsToPurchaseOrder(mergeVo);
return R.ok();
}
@Transactional
@Override
public void mergePurchaseDetailsToPurchaseOrder(MergeVo mergeVo) {
Long purchaseId = mergeVo.getPurchaseId();
if (purchaseId == null) {
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setCreateTime(new Date());
purchaseEntity.setUpdateTime(new Date());
purchaseEntity.setStatus(WareConstants.PurchaseStatusEnum.CREATED.getCode());
this.save(purchaseEntity);
purchaseId = purchaseEntity.getId();
}
if (this.getById(purchaseId).getStatus().equals(WareConstants.PurchaseStatusEnum.CREATED.getCode()) || this.getById(purchaseId).getStatus().equals(WareConstants.PurchaseStatusEnum.ASSIGNED.getCode())) {
Long finalPurchaseId = purchaseId;
List<Long> items = mergeVo.getItems();
List<PurchaseDetailEntity> purchaseDetailEntityList = items.stream()
.map(item -> {
PurchaseDetailEntity purchaseDetail = new PurchaseDetailEntity();
// 采购单 ID
purchaseDetail.setPurchaseId(finalPurchaseId);
// 采购商品 ID
purchaseDetail.setId(item);
purchaseDetail.setStatus(WareConstants.PurchaseDetailStatusEnum.ASSIGNED.getCode());
return purchaseDetail;
})
.collect(Collectors.toList());
purchaseDetailService.updateBatchById(purchaseDetailEntityList);
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(purchaseId);
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}
}
/**
* 领取采购单
*/
@PostMapping("/received")
public R receivedThePurchaseOrder(@RequestBody List<Long> purchaseOrderIdList) {
purchaseService.receivedThePurchaseOrder(purchaseOrderIdList);
return R.ok();
}
@Transactional
@Override
public void receivedThePurchaseOrder(List<Long> purchaseOrderIdList) {
// 1. 确认当前采购单状态为 新建 or 已分配
List<PurchaseEntity> purchaseEntityList = purchaseOrderIdList
.stream()
.map(this::getById)
.filter(purchaseEntity -> purchaseEntity.getStatus().equals(WareConstants.PurchaseStatusEnum.CREATED.getCode()) || purchaseEntity.getStatus().equals(WareConstants.PurchaseStatusEnum.ASSIGNED.getCode()))
.collect(Collectors.toList());
if (!purchaseEntityList.isEmpty() && ObjectUtil.isNotNull(purchaseEntityList)) {
// 2. 改变采购单的状态
purchaseEntityList.forEach(purchaseEntity -> {
purchaseEntity.setStatus(WareConstants.PurchaseStatusEnum.RECEIVED.getCode());
purchaseEntity.setUpdateTime(new Date());
});
this.updateBatchById(purchaseEntityList);
// 3. 改变采购项的状态
purchaseEntityList.forEach(purchaseEntity -> {
List<PurchaseDetailEntity> purchaseDetailList = getPurchaseDetailsByPurchaseId(purchaseEntity.getId());
purchaseDetailList = purchaseDetailList
.stream()
.map(purchaseDetail -> {
purchaseDetail.setStatus(WareConstants.PurchaseDetailStatusEnum.BUYING.getCode());
return purchaseDetail;
})
.collect(Collectors.toList());
purchaseDetailService.updateBatchById(purchaseDetailList);
});
} else {
throw new RuntimeException(WareConstants.PurchaseDetailStatusEnum.HAS_ERROR.getMsg());
}
}
private List<PurchaseDetailEntity> getPurchaseDetailsByPurchaseId(Long id) {
return purchaseDetailService.lambdaQuery().eq(PurchaseDetailEntity::getPurchaseId, id).list();
}
@Data
public class PurchaseItemVo {
private Long itemId;
private Integer status;
private String reason;
}
@Data
public class PurchaseDoneVo {
/**
* 采购单 ID
*/
@NotNull
private Long id;
/**
* 采购项
*/
private List<PurchaseItemVo> items;
}
/**
* 完成采购
*/
@PostMapping("/done")
public R done(@RequestBody PurchaseDoneVo purchaseDoneVo) {
purchaseService.completeThePurchase(purchaseDoneVo);
return R.ok();
}
@Transactional
@Override
public void completeThePurchase(PurchaseDoneVo purchaseDoneVo) {
// 1. 改变采购项状态
List<PurchaseItemVo> items = purchaseDoneVo.getItems();
List<PurchaseDetailEntity> purchaseDetailList = new ArrayList<>();
Boolean flag = true;
for (PurchaseItemVo item : items) {
PurchaseDetailEntity purchaseDetail = new PurchaseDetailEntity();
if (item.getStatus().equals(WareConstants.PurchaseDetailStatusEnum.HAS_ERROR.getCode())) {
flag = false;
purchaseDetail.setStatus(item.getStatus());
} else {
purchaseDetail.setStatus(WareConstants.PurchaseDetailStatusEnum.FINISHED.getCode());
// 将采购成功的入库
PurchaseDetailEntity detail = purchaseDetailService.getById(item.getItemId());
wareSkuService.addStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum());
}
purchaseDetail.setId(item.getItemId());
purchaseDetailList.add(purchaseDetail);
}
purchaseDetailService.updateBatchById(purchaseDetailList);
// 2. 改变采购单状态(其状态的依据是所有采购项的状态)
Long purchaseId = purchaseDoneVo.getId();
PurchaseEntity purchase = new PurchaseEntity();
purchase.setId(purchaseId);
purchase.setStatus(flag ? WareConstants.PurchaseStatusEnum.FINISHED.getCode() : WareConstants.PurchaseStatusEnum.HAS_ERROR.getCode());
purchase.setUpdateTime(new Date());
this.updateById(purchase);
}
addStock(Long skuId, Long wareId, Integer skuNum)
@Override
public void addStock(Long skuId, Long wareId, Integer skuNum) {
List<WareSkuEntity> wareSkuList = lambdaQuery().eq(WareSkuEntity::getSkuId, skuId).eq(WareSkuEntity::getWareId, wareId).list();
// 如果没有这个库存记录,则新增;有则更新库存
if (wareSkuList.isEmpty()) {
WareSkuEntity wareSku = new WareSkuEntity();
wareSku.setWareId(wareId);
wareSku.setSkuId(skuId);
wareSku.setStock(skuNum);
wareSku.setStockLocked(0);
// 远程查询获取 SkuName
try {
R info = productFeignService.info(skuId);
Map<String, Object> skuInfo = (Map<String, Object>) info.get("skuInfo");
if (info.getCode() == 0) {
wareSku.setSkuName((String) skuInfo.get("skuName"));
}
} catch (Exception e) {
log.error("error: ", e);
}
this.save(wareSku);
} else {
this.getBaseMapper().updateStock(skuId, wareId, skuNum);
}
}
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {
void updateStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("skuNum") Integer skuNum);
}
<update id="updateStock">
UPDATE `mall_wms`.wms_ware_sku SET stock = stock + #{skuNum}
<where>
sku_id = #{skuId} AND ware_id = #{wareId}
where>
update>
远程调用: 1. 标注
@EnableFeignClients
注解; 2. 编写XxxFeignService
。
@Component
@FeignClient("mall-product")
public interface ProductFeignService {
@GetMapping("/product/skuInfo/info/{skuId}")
R info(@PathVariable("skuId") Long skuId);
}
server:
port: 9999
spring:
application:
name: mall-gateway
cloud:
gateway:
routes:
- id: product_route
uri: lb://mall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: third_party_route
uri: lb://mall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty(?>.*),/$\{segment}
- id: coupon_route
uri: lb://mall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: member_route
uri: lb://mall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: ware_route
uri: lb://mall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: order_route
uri: lb://mall-order
predicates:
- Path=/api/order/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
Mac:Node 版本为:v14.15.0
。
npm install rimraf -g
rimraf node_modules
npm cache clean --force
npm install
npm run dev
Windows:
npm install [email protected]
npm install
npm run dev
'parent.relativePath' of POM io.renren:renren-fast:3.0.0 (/Users/sun/IdeaProjects/mall/renren-fast/pom.xml) points at com.sun.mall:mall instead of org.springframework.boot:spring-boot-starter-parent, please verify your project structure
子模块的 parent 不是父模块,而是继承了 Spring Boot。
只需要在
标签中加上
即可。
renren-fast
的 Spring Boot 版本为 2.6.6,需要修改为 2.1.8.RELEASE;
依赖引入:不能直接将 mall-common
引入,需要单独引入;
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
dependencies>
CorsConfig 中的 allowedOriginPatterns
报错,需要将其修改为 allowOrigins
。
若未报错,但是未注册成功:检查主启动类是否标注 @EnableDiscoveryClient
注解、配置文件中的服务名称 和 Nacos 地址。
# 如果安装时 node-sass 也保存的话
npm uninstall sass-loader
npm uninstall node-sass
npm install [email protected]
npm install [email protected]
# 安装
npm install [email protected] --save
在 main.js
中引用:
import Pubsub from 'pubsub-js'
Vue.prototype.PubSub = Pubsub;