【项目系列】- 谷粒商城基础篇(二刷)
作者:互联网
文章目录
1. 环境搭建
1.1 创建项目微服务
从子模块中粘贴一个pom文件到父项目中,然后进行修改:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--修改坐标-->
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall</name>
<description>聚合服务</description>
<packaging>pom</packaging>
<!--聚合子模块-->
<modules>
<module>gulimall-order</module>
<module>gulimall-ware</module>
<module>gulimall-coupon</module>
<module>gulimall-product</module>
<module>gulimall-member</module>
</modules>
</project>
1.2 前端脚手架的配置
① 在一个文件夹比如gulimall_qianduan_code
文件夹下克隆下面两个项目:
git clone https://gitee.com/renrenio/renren-fast.git
git clone https://gitee.com/renrenio/renren-fast-vue.git
②将renren-fast文件夹中的.git
文件去掉后,将整个文件夹放入后台项目中:
③ 因为在gulimall项目下添加了一个子模块,因此在gulimall的pom文件中将这个子模块添加进去:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall</name>
<description>聚合服务</description>
<packaging>pom</packaging>
<modules>
<module>gulimall-order</module>
<module>gulimall-ware</module>
<module>gulimall-coupon</module>
<module>gulimall-product</module>
<module>gulimall-member</module>
<!--添加新的子模块-->
<module>renren-fast</module>
</modules>
</project>
④ 在renren-fast模块的dev-applicaiton.yml文件中配置数据库:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.38.22:3306/gulimall_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
⑤ 在官网下载nodejs并在cmd窗口配置npm镜像:
node -v
npm config set registry http://registry.npm.taobao.org/
⑥ 使用vscode打开项目renren-fast-vue,在终端安装npm:
npm install
第一次安装可能会失败,按照下面的操作重新安装即可:
# 先清除缓存
npm rebuild node-sass
npm uninstall node-sass
然后在VS Code中删除node_modules
# 执行
npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
# 如果上面没有报错,就继续执行
npm install
# 运行项目
npm run dev
1.3 代码生成器生成gulimall-product代码
① 将代码生成器项目克隆下来,然后删除.git文件,并放到项目工程下:
git clone https://gitee.com/renrenio/renren-generator.git
② 修改application.yml:
server:
port: 8082
# mysql
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
# 配置要生成的微服务代码的数据库
url: jdbc:mysql://192.168.38.22:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
③ 修改generator.properties:
mainPath=com.atguigu
package=com.atguigu.gulimall
# 将模块名称修改为product
moduleName=product
author=hengheng
email=hengheng@gmail.com
# 将表头修改为pms_
tablePrefix=pms_
④ 启动renren-generator项目并访问:localhost:8082
⑤ 将生成代码中的main文件夹替换掉gulimall-product项目的main文件夹
⑥ 创建gulimall-common服务,来配置所有微服务公共的依赖和类:使用new moudle—>maven创建
1.4 代码生成器生成gulimall-coupon代码
① 修改application.yml文件:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
# 配置要生成的微服务代码的数据库
url: jdbc:mysql://192.168.38.22:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
② 修改generator.approperties文件:
mainPath=com.atguigu
package=com.atguigu.gulimall
# 将模块名称修改为coupon
moduleName=coupon
author=hengheng
email=hengheng@gmail.com
# 将表头修改为sms_
tablePrefix=sms_
③ 启动renren-genrator项目并访问生成代码:
④ 将生成代码文件夹下的main文件夹替换gulimall-coupon文件夹下的main文件夹
⑤ 在gulimall-coupon服务的pom文件中加入gulimall-common服务依赖:
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
重复以上步骤继续使用代码生成器生成gulimall-order、gulimall-member、gulimall-ware服务的代码。
1.5 测试微服务的基本CRUD功能
① 在gulimall-common添加mysql依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
② 添加mybatis相关配置:
server:
port: 7000
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.38.22:3306/gulimall_sms?useUnicode=true&characterEncoding= utf-8
driver-class-name: com.mysql.jdbc.Driver
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
③ 测试向数据库中添加一条数据:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = GulimallProductApplicaiton.class)
public class GulimallProductApplicationTests {
@Autowired
BrandService brandService;
@Test
public void contextLoads() {
BrandEntity brandEntity = new BrandEntity();
brandEntity.setName("测试数据");
brandService.save(brandEntity);
System.out.println("保存成功");
}
}
④ 因为更改了项目的版本(从2.5.4改成了2.1.8.RELEASE),导致测试出现Failed to load ApplicationContext ,只需要在pom文件中添加:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
⑤插入数据库中的数据中文乱码,改了idea的file Encoding仍然乱码,因此修改application.yml文件:
spring:
datasource:
url: jdbc:mysql://192.168.38.22:3306/gulimall_pms?useUnicode=true&characterEncoding= utf-8
2. SpringCloud Alibaba分布式组件
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
在gulimall-common的项目中引入坐标,配置我们使用的SpringCloud Alibaba的版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.1 nacos注册中心
下面以coupon服务为例,将gulimall-coupon服务注册进nacos。
① 首先需要配置gulimall-coupon服务的端口号和服务名称:
server:
port: 7000
spring:
application:
name: gulimall-coupon
② 到http://github.com/alibaba/nacos/releases下载nacos压缩包并解压,运行nacos
③ 在gulimall-common模块中引入 Nacos Discovery Starter,这样每个服务中都会引入相应的坐标
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
④ 在gulimall-coupon服务的配置文件中配置 Nacos Server 地址
spring:
application:
name: gulimall-coupon
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
⑤ 在gulimall-coupon服务中使用 @EnableDiscoveryClient 注解开启服务注册与发现功能
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallCouponApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallCouponApplication.class, args);
}
}
⑥ 启动项目,访问http://localhost:8848/nacos/
⑦ 将gulimall-member也放进nacos注册中心
2.2 OpenFeign远程调用
需求:gulimall-member会员服务远程调用gulimall-coupon会员服务
① 在gulimall-member中导入远程调用依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
② 在gulimall-member中编写远程调用的接口
/**
* 远程调用gulimall-coupon服务的接口
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
}
③ 在gulimall-member的主配置类开启远程调用功能,服务启动时会自动扫描带@FeignClient注解的方法
//开启远程调用功能,服务启动时会自动扫描带有@FeignClient注解的类
@EnableFeignClients("com.atguigu.gulimall.member.feign")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallMemberApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallMemberApplication.class, args);
}
}
④ 在gulimall-coupon中编写被远程调用的方法
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Autowired
private CouponService couponService;
@RequestMapping("/member/list")
public R membercoupons(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("满100减10");
return R.ok().put("coupons",Arrays.asList(couponEntity));
}
}
⑤ 在gulimall-member的远程调用接口中指明要调用的远程方法
/**
* 远程调用gulimall-coupon服务的接口
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
* 调用gulimall-coupon服务中映射路径为/coupon/coupon/member/list该方法
*/
@RequestMapping("/coupon/coupon/member/list")
public R membercoupons();
}
⑥ 在gulimall-member中测试远程调用gulimall-coupon服务
@RestController
@RequestMapping("member/member")
public class MemberController {
@Autowired
private MemberService memberService;
@Autowired
CouponFeignService couponFeignService;
@RequestMapping("/coupons")
public R test(){
MemberEntity memberEntity = new MemberEntity();
memberEntity.setNickname("张三");
//调用couponFeignService接口中的方法
R membercoupons = couponFeignService.membercoupons();
return R.ok()
.put("member",memberEntity)
.put("coupons",membercoupons.get("coupons"));
}
}
⑦ 启动两个项目,访问http://localhost:8000/member/member/coupons
2.3 Nacos配置中心
https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md
下面以coupon服务为例:
① 首先,在gulimall-common服务中引入 Nacos Config Starter
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
② 在gulimall-coupon的bootstrap.properties中配置服务名称和nacos配置中心地址,bootstrap.properties会优先于其他配置文件加载
spring.application.name=gulimall-coupon
# 指定配置中心服务器地址,nacos服务器就是一个配置中心
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
③ 编写一个配置文件application.properties
coupon.username=hengheng
coupon.age=20
④ 测试使用@Value直接从配置文件中取值
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Autowired
private CouponService couponService;
//使用@Value注解从配置文件中取值
@Value("${coupon.username}")
private String username;
@Value("${coupon.age}")
private String age;
@RequestMapping("/test")
public R test(){
return R.ok().put("name",username).put("age",age);
}
}
存在的问题:假如项目上线后,我们需要修改配置文件中的值,就需要在代码中修改后重新打包上线,假如项目配置在10个服务器上,那么这10个服务器都要这样做,而若我们将配置文件的配置放在配置中心,就只需要修改配置中心的配置项即可。
⑤ 将application.properties中的配置放到nacos配置中心,其中dataID默认为gullimall-coupon.properteis
⑥ 在CouponController类上添加@RefreshScope注解,该注解可以动态的从Nacos Config 中获取相应的配置
⑦ 将nacos配置文件中的age改为25
⑧ 重新访问:http://localhost:7000/coupon/coupon/test
2.4 Nacos配置中心-命名空间和配置分组
在本项目中为每一个微服务都创建一个命名空间,这样启动服务时就会只加载该命名空间的配置文件
① 为gulimall-coupon创一个一个命名空间coupon
② 在gulimall-coupon的bootstrap.properties文件中配置该命名空间namespace
spring.cloud.nacos.config.namespace=8c2dbbf4-986a-4485-a1b2-06bb3fa2472f
③ 这样以后这个服务启动的时候就会加载coupon命名空间下的配置。(默认加载的 DEFAULT_GROUP下的)
在本项目中,设置Data ID同为gulimall-coupon.properties,group不同,分别为prod\dev\test\,这样可以根据不同的环境加载不同的配置文件
④ 在gulimall-coupon的bootstrap.properties文件中配置该分组group
spring.cloud.nacos.config.group=dev
⑤ 这样以后便只会加载group=dev的配置文件
2.5 Nacos配置中心-加载多配置
需求:将application.yml配置文件中的内容都放在配置中心的配置文件中,代替application.yml文件
① 将与数据源有关的配置放到coupon命名空间的配置中
② 将于mybatis相关的配置放到coupon命名空间的配置中
③ 将其他相关配置放到coupon命名空间的配置中
④ 在gulimall-coupon的bootstrap.properties中配置要加载的配置文件
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=8c2dbbf4-986a-4485-a1b2-06bb3fa2472f
spring.cloud.nacos.config.group=dev
spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true
spring.cloud.nacos.config.ext-config[2].data-id=other.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true
2.6 Gateway网关
https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/
① 创建gulimall-gateway项目,同时添加pom文件
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!--因为本服务不需要使用MyBatis,如果导入依赖但没有配置会报错,这里将该依赖排除-->
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
② 将服务名称配置进nacos,在bootstrap.properteis中编写nacos配置中心的相关配置
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=80127ed3-2158-4409-a62b-1bd2046c9f14
③ 在application.yml中编写nacos注册中心和其他相关配置
server:
port: 88
spring:
application:
name: gulimall-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: test_route
uri: https://www.baidu.com/
predicates:
- Query=url,baidu # 访问url=baidu,就会路由到百度网页
- id: qq_route
uri: https://www.qq.com/
predicates:
- Query=url,qq # 访问url=qq,就会路由到qq网页
④ 启动项目并访问http://localhost:88/?url=baidu,会进入到www.baidu.com
//开启服务注册与发现功能
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallGatewayApplication.class, args);
}
}
3. 商品服务 - 三级分类
3.1 查询-递归树形结构数据获取
需求分析:查询所有分类数据,并以树形结构组装起来
① 在CategoryEntity类中添加属性 chrilden
package com.atguigu.gulimall.product.entity;
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
/**
* 分类数据的子分类数据
* 该字段在数据库中不存在
*/
@TableField(exist = false)
private List<CategoryEntity> chrilden;
}
② Controller层:
package com.atguigu.gulimall.product.controller;
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@ApiOperation("查询所有分类以及子分类数据,并以json树形结构返回")
@GetMapping("/list/tree")
public R listWithTree(){
List<CategoryEntity> categoryEntityList = categoryService.listWithTree();
return R.ok().put("data", categoryEntityList);
}
}
③ Service层:
package com.atguigu.gulimall.product.service.impl;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public List<CategoryEntity> listWithTree() {
//查询所有分类数据
List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
/**
* 1、从所有分类数据中过滤出一级分类,即parentId==0
* 2、->左边为方法参数,如果只有一个参数,小括号可以省略,->右边为方法体,如果只有一行代码,大括号可以省略
* 3、找到当前分类的子分类
* 4、对菜单按照sort字段的自然排序方式进行排序,两两比较大小,负数交换位置,正数不交换
*/
List<CategoryEntity> levelMenus = categoryEntityList.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.map(menu -> {
menu.setChrilden(getChrildens(menu, categoryEntityList));
return menu;
}).sorted((menu1, menu2)->{
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return levelMenus;
}
private List<CategoryEntity> getChrildens(CategoryEntity root, List<CategoryEntity> categoryEntityList) {
/**
* 1、找到当前分类下的子分类
* 2、找到当前分类的子分类
* 3、对分类进行排序
*/
List<CategoryEntity> chridlens = categoryEntityList.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(menu -> {
menu.setChrilden(getChrildens(menu,categoryEntityList));
return menu;
}).sorted((menu1,menu2)->{
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return chridlens;
}
}
④ 测试:http://localhost:10001/product/category/list/tree
3.2 前端-配置网关路由和路径重写
① 在前端项目renren-fast-vue项目中renren-fast-vue\src\views\modules文件夹下创建文件夹product,在product文件夹下面创建文件category.vue
② 利用element-ui中的树形控件展示三级分类
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
export default {
components: {},
props: {},
data () {
return {
data: [],
defaultProps: {
children: 'children',
label: 'label'
}
}
},
methods: {
handleNodeClick (data) {
console.log(data)
},
//编写一个方法,发送请求获取三级分类的数据
getMenus () {
this.$http({
//请求路径
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get',
}).then(data=>{
console.log("成功获取到数据:",data)
})
},
},
created () {
//调用方法
this.getMenus();
},
}
</script>
<style scoped>
</style>
③ 刷新人人快速开发平台,按f12,发现tree请求的结果为404,
且tree的请求路径为http://localhost:8080/renren-fast/product/category/list/tree,
后端tree的真正请求路径为:http://localhost:10001/product/category/list/tree,
修改 static\config\index.js文件中的api接口请求地址,改为给网关发送请求,通过网关跳到后端的gulimall-product、gulimall-order、gulimall-coupon等服务
④ 重新刷新人人快速开发平台,发现验证码不见了
此时访问验证码发送的请求为:http://localhost:88/api/captcha.jpg?uuid=?
而原来验证码的请求为:http://localhost:8080/renren-fast/captcha.jpg?uuid=?
⑤ 前台的所有请求都是经由“http://localhost:88/api”来转发的,配置网关路由,在gulimall-gateway服务的applicaiton.yml文件中添加路由规则,将请求跳转到renren-fast服务中
# 将请求(1)通过网关路径重写为请求(2)
(1)http://localhost:88/api/captcha.jpg?uuid=?
(2)http://localhost:8080/renren-fast/captcha.jpg?uuid=?
spring:
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast # 负载均衡到renren-fast服务
predicates:
- Path=/api/** # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务
filters:
# 意思是将路径 /api/xxx 重写为 /renren-fast/xxx
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
# http://localhost:88/api/captcha.jpg?uuid=?
# http://localhost:8080/renren-fast/captcha.jpg?uuid=?
⑥ 访问登录请求,发现出现403 Forbidden错误:
3.3 网关统一配置跨域
① 跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。其中,同源策略是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;
② 跨域的解决方法:
- Access-Control-Allow-Origin:支持哪些来源的请求跨域
- Access-Control-Allow-Methods:支持哪些方法跨域
- Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含cookie
- Access-Control-Expose-Headers:跨域请求暴露的字段
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
- Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效 。
③ 在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
3.4 网关将请求转给gulimall-product
① 重启renren-fast项目,登录成功后,进入分类维护菜单,并没有获取到分类数据,打开f12,报错404,即请求的http://localhost:88/api/product/category/list/tree不存在
② 因为前端访问三级分类数据的请求路径为http://localhost:88/api/product/category/list/tree,但是后端的请求路径为http://localhost:10001/product/category/list/tree,因此需要通过网关路由到gulimall-product服务,让网关将与gulimall-product的请求都路由到gulimall-product
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/** # 只要请求为/api/product/**,就路由到gulimall-product服务
filters:
# 意思是将路径 /api/xxx 重写为/xxx
- RewritePath=/api/(?<segment>/?.*),/$\{segment}
http://localhost:88/api/product/category/list/tree路由到http://localhost:10001/product/category/list/tree
③ 将gulimall-product服务注册进注册中心,否则gulimall-gateway将无法发现这个服务
application.yml:
server:
port: 10001
spring:
application:
name: gulimall-product
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
bootstrap.properties:
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=80fa1f00-1d72-49e0-bd03-fdcc12cda9e8
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
④ 重新访问:http://localhost:88/api/product/category/list/tree
⑤ 将查询到的分类数据展示出来
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data () {
return {
//调用getMenus()方法后返回的数据
menus: [],
//要展示的数据,其中右侧为getMenus()方法后返回数据类的属性
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
handleNodeClick (data) {
console.log(data)
},
getMenus () {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get',
}).then(({ data }) => {
console.log("成功获取到数据:", data.data),
this.menus = data.data
})
},
},
created () {
this.getMenus()
},
}
3.5 删除-逻辑删除分类树的节点
① 前端配置三级分类树: 一级和二级节点后面添加Append,只要没有子节点该节点后面就添加Delete,点击Delete后前端发送请求到后端实现删除该节点的功能
② 配置mybatis-plus的逻辑删除功能
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
③ 在实体类字段上加上@TableLogic
注解,如果上面的配置和数据库字段表示的相反,可自定义逻辑删除值
package com.atguigu.gulimall.product.entity;
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
/**
* 是否显示[0-不显示,1显示]
* 因为application.yml与我们数据库中showStatus的逻辑是反的,
* 因此可以自定义逻辑删除和逻辑未删除,value代表逻辑未删除,delvalue代表逻辑删除
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
}
④ Controller层
package com.atguigu.gulimall.product.controller;
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 逻辑删除
* @RequestBody:说明前端传来的是post请求,批量删除节点
*/
@ApiOperation("逻辑删除分类树中的节点")
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
}
⑤ Service层
package com.atguigu.gulimall.product.service.impl;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public void removeMenuByIds(List<Long> asList) {
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
}
⑥ 测试:http://localhost:api/product/category/delete
⑦ 前端点击Delete发送删除节点的请求
//删除节点
remove(node, data) {
var ids = [data.catId];
//删除之前,给一个提示信息,确定是否删除该节点
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
//删除成功后,给一个提示信息
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
}).catch(() => {});
console.log("remove", node, data);
}
3.6 新增 - 分类树节点
① vue加入对话框el-dialog组件
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level<=2" type="text" size="mini" @click="() => append(data)">Append</el-button>
<el-button
v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)"
>Delete</el-button>
</span>
</span>
</el-tree>
<el-dialog
:title="提示"
:visible.sync="dialogVisible"
width="30%"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
② 点击确定,发送请求到后端新增分类节点
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data () {
return {
//新增节点时初始化的提交数据的默认值
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
},
dialogVisible: false,
//调用getMenus()方法后返回的数据
menus: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
//获取菜单数据,并展示三级分类
getMenus () {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get',
}).then(({ data }) => {
console.log("成功获取到数据:", data.data),
this.menus = data.data
})
},
//新增节点(data点击append的节点的数据)
append (data) {
console.log(data)
this.dialogVisible = true;
//新增节点的parentCid是当前点击的节点的cid
this.category.parentCid = data.catId;
//新增节点的层级是当前点击的节点的层级+1
this.category.catLevel = data.catLevel*1+1;
},
//前端发送请求添加三级分类数据
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
created () {
this.getMenus()
},
}
3.7 修改 - 分类树节点
① 添加edit图标,点击edit后调用edit(data)方法
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button type="text" size="mini" @click="edit(data)">edit</el-button>
</span>
</el-tree>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
② edit(data)方法中发送请求/product/category/info/${data.catId}获取要回显的数据,addCategory ()保存修改后的数据
<script>
export default {
components: {},
props: {},
data () {
return {
//新增节点时初始化的提交数据的默认值
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
//调用getMenus()方法后返回的数据
menus: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
// 新增节点的数据
append (data) {
console.log("append", data)
this.dialogType = "add"
this.title = "添加分类"
this.dialogVisible = true
this.category.parentCid = data.catId
this.category.catLevel = data.catLevel * 1 + 1
this.category.catId = null
this.category.name = ""
this.category.icon = ""
this.category.productUnit = ""
this.category.sort = 0
this.category.showStatus = 1
},
// 要修改节点的数据
edit (data) {
console.log("要修改的数据", data)
this.dialogType = "edit"
this.title = "修改分类"
this.dialogVisible = true
//发送请求获取当前节点最新的数据
this.$http({
//修改节点data的catId
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
//请求成功
console.log("要回显的数据", data)
this.category.name = data.data.name
this.category.catId = data.data.catId
this.category.icon = data.data.icon
this.category.productUnit = data.data.productUnit
this.category.parentCid = data.data.parentCid
this.category.catLevel = data.data.catLevel
this.category.sort = data.data.sort
this.category.showStatus = data.data.showStatus
})
},
//如果为add调用新增节点的方法,如果为edit调用修改节点的方法
submitData () {
if (this.dialogType == "add") {
this.addCategory()
}
if (this.dialogType == "edit") {
this.editCategory()
}
},
//添加三级分类
addCategory () {
console.log("提交的三级分类数据", this.category)
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
})
//关闭对话框
this.dialogVisible = false
//刷新出新的菜单
this.getMenus()
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
//修改三级分类数据
editCategory () {
//要发送给后端的数据
var { catId, name, icon, productUnit } = this.category
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
})
//关闭对话框
this.dialogVisible = false
//刷新出新的菜单
this.getMenus()
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
created () {
this.getMenus()
},
}
</script>
<script>
export default {
components: {},
props: {},
data () {
return {
//新增节点时初始化的提交数据的默认值
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
//调用getMenus()方法后返回的数据
menus: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
// 新增节点的数据
append (data) {
console.log("append", data)
this.dialogType = "add"
this.title = "添加分类"
this.dialogVisible = true
this.category.parentCid = data.catId
this.category.catLevel = data.catLevel * 1 + 1
this.category.catId = null
this.category.name = ""
this.category.icon = ""
this.category.productUnit = ""
this.category.sort = 0
this.category.showStatus = 1
},
// 要修改节点的数据
edit (data) {
console.log("要修改的数据", data)
this.dialogType = "edit"
this.title = "修改分类"
this.dialogVisible = true
//发送请求获取当前节点最新的数据
this.$http({
//修改节点data的catId
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
//请求成功
console.log("要回显的数据", data)
this.category.name = data.data.name
this.category.catId = data.data.catId
this.category.icon = data.data.icon
this.category.productUnit = data.data.productUnit
this.category.parentCid = data.data.parentCid
this.category.catLevel = data.data.catLevel
this.category.sort = data.data.sort
this.category.showStatus = data.data.showStatus
})
},
//如果为add调用新增节点的方法,如果为edit调用修改节点的方法
submitData () {
if (this.dialogType == "add") {
this.addCategory()
}
if (this.dialogType == "edit") {
this.editCategory()
}
},
//添加三级分类
addCategory () {
console.log("提交的三级分类数据", this.category)
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
})
//关闭对话框
this.dialogVisible = false
//刷新出新的菜单
this.getMenus()
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
//修改三级分类数据
editCategory () {
//要发送给后端的数据
var { catId, name, icon, productUnit } = this.category
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
})
//关闭对话框
this.dialogVisible = false
//刷新出新的菜单
this.getMenus()
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
created () {
this.getMenus()
},
}
</script>
③ 后端的获取分类节点信息和方法:
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 信息
*/
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateById(category);
return R.ok();
}
}
3.8 批量保存- 批量修改节点顺序
① 前端,发送批量修改的请求
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
updateNodes: [],
}
};
},
methods: {
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
message: "菜单顺序等修改成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = this.pCid;
this.updateNodes = [];
this.maxLevel = 0;
});
},
};
</script>
<style scoped>
</style>
② 后端接收前端传来的json数据
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] categoryEntities){
categoryService.updateBatchById(Arrays.asList(categoryEntities));
return R.ok();
}
}
4. 商品服务 - 商品管理
如果想要启动整个项目,至少启动gulimall-product、gulimall-gateway、renren-fast项目,启动nacos服务。
4.1 状态开关功能
需求描述: 添加显示状态的开关按钮,开关的打开和关闭都会更会更新数据库show_status字段的值
① 添加显示状态的开关按钮,点击开关时,触发@change="updateBrandStatus(scope.row)
<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>
② 发送请求到后端更改数据库中show_status的值
updateBrandStatus (data) {
console.log("最新信息", data)
let { brandId, showStatus } = data
//发送请求修改状态
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
data: this.$http.adornData({ brandId, showStatus }, false)
}).then(({ data }) => {
this.$message({
type: "success",
message: "状态更新成功"
})
})
},
③ 后端方法
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
}
4.2 Spring整合阿里云对象存储OSS实现文件上传
需求分析:点击新增后显示新增品牌,我们希望将品牌logo进行文件上传,和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
① 进入https://www.aliyun.com/,选择产品–>存储–>对象存储OSS–>立即开通–>支付宝登录–>bucket列表
② 点击文件上传,上传几张图片:
③ 点击图片详情,复制url即可在浏览器访问图片:
④ 上面方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云对象存储。
⑤ SpringCloudAlibaba整合阿里云对象存储OSS,首先在gulimall-common服务的pom文件中导入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
⑥ 在gulimall-product服务的application.yml文件中配置access-key、secret-key、endpoint,他们的值从阿里云的对象存储OSS中获取(点击用户头像–>AccessKey管理–>开始使用子用户Accesskey,对象存储->gulimall-hengheng->概览得到endpoint)
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI5tFCEYUMBn4eWQKoC68V
secret-key: hhUDcncbXfFDg0fuMKW6S6CoBJ8wZ0
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
⑦ 测试上传文件:
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallProductApplicationTests {
@Autowired
private OSSClient ossClient;
@Test
public void test() throws FileNotFoundException {
// 上传文件流。
InputStream inputStream
= new FileInputStream("C:\\Users\\18751\\Desktop\\2.jpg");
ossClient.putObject("gulimall-hengheng", "2.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功");
}
}
4.3 SpringCloud整合阿里云对象存储OSS
① 创建gulimall-third-party服务专门用来集成第三方服务,修改pom文件,将OSS这个第三方SDK不再放入gulimall-common中而是放入gulimall-third-party服务中:
<dependencies>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
② 将gulimall-third-party服务的配置文件放进nacos配置中心,服务一启动就会加载命名空间下的配置文件:
在nacos中为gulimall-third-party创建一个命名空间third-party,并在bootstrap.properties文件中配置该命名空间namespace和加载的配置文件oss.yml
oss.yml配置详情:
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=gulimall-third-party
spring.cloud.nacos.config.namespace=cc589d72-014d-4db3-8b5a-b7b7238c25bb
spring.cloud.nacos.config.ext-config[0].data-id=oss.yml
spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.ext-config[0].refresh=true
③ 在application.yml中将gulimall-third-party服务注册进nacos注册中心:
spring:
application:
name: gulimall-third-party
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI5tFCEYUMBn4eWQKoC68V
secret-key: hhUDcncbXfFDg0fuMKW6S6CoBJ8wZ0
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket: gulimall-hengheng
server:
port: 30000
④ 服务的主启动类上加上注解@EnableDiscoveryClient,开启服务的注册与发现:
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallThirdPartyApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallThirdPartyApplication.class, args);
}
}
⑤ 采用JavaScript客户端直接签名时,AccessKey ID和AcessKey Secret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案
@RestController
public class OssController {
@Autowired
private OSS ossClient;
//从配置文件中获取值
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@RequestMapping("/oss/policy")
public R policy() {
String host = "https://" + bucket + "." + endpoint;
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format + "/";
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
System.out.println(e.getMessage());
}
return R.ok().put("data",respMap);
}
}
⑥ 访问http://localhost:30000/oss/policy得到签名:
⑦ 在gulimall-gateway中配置网关路由
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
⑧ 前端:将项目提供的upload文件下放在renren-fast-vue\src\components目录下,更改两个文件的文件上传地址,改为阿里云提供的 Bucket 域名:http://gulimall-hengheng.oss-cn-hangzhou.aliyuncs.com
4.4 前端表单校验
① 新增一个商品上传商品logo图片后,显示的并不是图片而是图片链接:
② 在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: 80px" />
</template>
</el-table-column>
③ 前端表单校验:
在brand-add-or-update.vue中添加表单校验:
<el-form-item label="排序" prop="sort">
<!--将sort字段绑定一个数字-->
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
dataRule: {
name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
logo: [{ required: true, message: "品牌logo地址不能为空", trigger: "blur" }],
descript: [{ required: true, message: "介绍不能为空", trigger: "blur" }],
showStatus: [
{
required: true,
message: "显示状态[0-不显示;1-显示]不能为空",
trigger: "blur",
},
],
firstLetter: [
{
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("首字母必须填写"))
} else if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error("首字母必须a-z或者A-Z之间"))
} else {
callback()
}
},
trigger: "blur"
}
],
sort: [
{
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("排序字段必须填写"))
} else if (!Number.isInteger(value) || value < 0) {
callback(new Error("排序必须是一个大于等于0的整数"))
} else {
callback()
}
},
trigger: "blur"
}
},
4.5 JSR303后端参数校验
注意:当对这些注解的使用规则不确定时,查看源码即可
① 给Bean添加校验注解,并添加自己的message提示:
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private Long brandId;
//不能提交一个空串
@NotBlank(message = "品牌名必须提交")
private String name;
//该注解不能为null,并且至少包含一个非空字符。
@NotBlank
@URL(message = "logo必须是一个合法的url地址")
private String logo;
private String descript;
private Integer showStatus;
//必须要提交这个参数
@NotEmpty
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母")
private String firstLetter;
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
② 实体类上标记校验注解之后,一定要开启校验功能@Valid,BingResult用来接收校验出错的结果:
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 保存
* 在实体类加上注解后,要使用@Valid
* @param brand 品牌
* @param result 接收参数校验的异常结果
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
Map<String,String> map = new HashMap<>();
if(result.hasErrors()){
result.getFieldErrors().forEach((item)->{
//属性
String field = item.getField();
//校验注解的message
String message = item.getDefaultMessage();
map.put(field,message);
});
return R.ok().put("data",map);
}else{
brandService.save(brand);
}
return R.ok();
}
}
③ 使用postman测试:http://localhost:88/api/product/brand/save
4.6 统一异常处理
对于参数校验发生的异常,我们使用了 BindingResult result这个变量来接收,但是这样做太复杂,因为参数校验的实体类很多,我们需要在每个Controller层的相应方法中加上参数校验并接收异常响应结果,因此只需要做统一异常处理即可,即将Controller层中所有的异常都抛出去,然后统一处理Controller层的异常,假如出现参数校验异常,就会直接抛出。
① 不再对异常结果进行处理,出现异常时直接抛出,最后统一异常处理:
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 保存
* 在实体类加上注解后,要使用@Valid
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result*/){
brandService.save(brand);
return R.ok();
}
}
② 异常处理类:
// 这个注解相当于 @RequestBody和 @ControllerAdvice
@RestControllerAdvice
@Slf4j
public class GulimallExceptionControllerAdvice {
/**
* 如果出现MethodArgumentNotValidException就会捕获
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
//出现异常,打印日志
log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError -> {
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
}));
return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMessage()).put("data",errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
//出现异常,打印日志
log.error("出现的错误:{}",throwable.getMessage());
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(),BizCodeEnum.UNKNOW_EXCEPTION.getMessage());
}
}
③ 定义给返回给前端的异常状态码和异常信息的枚举类:
public enum BizCodeEnum {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String message;
private BizCodeEnum(int code,String message){
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
4.7 JSR303后端参数分组校验
新增品牌和修改品牌的某些字段的注解校验规则不一样时,可以分组校验,校验注解只有在指定的分组下才生效,而且如果开启了分组校验注解功能,那些没有指定分组的校验注解就会不生效。
① 定义两个分组,一个用于标识新增商品,一个用于标识修改商品:
package com.atguigu.common.valid;
public interface AddGroup {
}
package com.atguigu.common.valid;
public interface UpdateGroup {
}
② 标注了UpdataGroup.class的注解会在修改时起作用,标注了AddGroup.class的注解会在新增时起作用:
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
@NotBlank(message = "品牌名必须提交",groups = {UpdateGroup.class,AddGroup.class})
private String name;
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups = {AddGroup.class,UpdateGroup.class})
private String logo;
private String descript;
private Integer showStatus;
@NotEmpty(groups = {AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
@NotNull(groups = {AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
③ 开启分组校验注解功能,@Validated指明校验字段所属的分组类:
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
@RequestMapping("/save")
public R save(@Validated(value = AddGroup.class) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
@RequestMapping("/update")
public R update(@Validated(value = UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
}
4.8 JSR303自定义参数校验
自定义参数校验,直接仿照其他校验注解的源码来写,比如@NotNull注解
① 校验需求:showStatus的参数值只能为0和1
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
@ListValue(vals={0,1},message = "必须提供指定的值")
private Integer showStatus;
}
② 仿照@NotNull注解自定义一个注解:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
//加入自定义校验器
@Constraint(validatedBy = {ListValueConstraintValidator.class})
public @interface ListValue {
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
③ 自定义一个校验器:
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
Set<Integer> set = new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
//注解的属性
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 判断是否校验成功
* @param value 要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
④ 因为定义了分组校验,因此需要指明注解生效的分组:
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
@ListValue(vals={0,1},message = "必须提供指定的值",groups = {AddGroup.class})
private Integer showStatus;
}
5. 商品服务 - 属性分组
5.1 获取分类属性分组
每个品牌下有多个分类(比如华为这个品牌下有手机,电视,笔记本等),每个分类下有很多属性分组(比如手机这个分类下有主体,基本信息,主芯片等)
前端属性分组目前的效果:
需求:根据第三级分类的categoryId查询该分类下的属性分组
① Controller层:
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrGroupService attrGroupService;
/**
* 根据categoryId查询三级分类下的属性分组
*/
@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}
}
② Service层:
@Service("attrGroupService")
public class AttrGroupServiceImpl extends ServiceImpl<AttrGroupDao, AttrGroupEntity> implements AttrGroupService {
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
//select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
String key = (String) params.get("key");
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
if(!StringUtils.isEmpty(key)){
wrapper.and((obj)->{
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
if( catelogId == 0){
IPage<AttrGroupEntity> page
= this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
return new PageUtils(page);
}else {
wrapper.eq("catelog_id",catelogId);
IPage<AttrGroupEntity> page
= this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
}
}
③ 测试:点击手机会显示手机下的属性分组,当catalogId=1时又会进一步查询
5.2 新增分类属性分组
修改前端代码后,发现没有四级分类,但是仍然会显示第四级分类的可选框:
① 修改CategoryEntity类使得只有Children有数据时,才会返回给前端:
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
/**
* 分类数据的子分类数据,该字段在数据库中不存在
*/
@TableField(exist = false)
//只有这个字段不null才返回给前端
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private List<CategoryEntity> children;
}
5.3 修改分类属性分组
① 通过前端代码可以看出,前端并不会将三级分类的完整路径传给后端,只是将最后一级的catalogId传给后端
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,
//向后端提交最后一级的categotyId
catelogId: this.catelogPath[this.catelogPath.length - 1]
})
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.visible = false
this.$emit("refreshDataList")
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
② 在修改分类属性分组时,我们希望回显三级分类信息,在AttrGroupEntity中增加一个catalogPath属性:
@Data
@TableName("pms_attr_group")
public class AttrGroupEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分组id
*/
@TableId
private Long attrGroupId;
/**
* 组名
*/
private String attrGroupName;
/**
* 排序
*/
private Integer sort;
/**
* 描述
*/
private String descript;
/**
* 组图标
*/
private String icon;
/**
* 所属分类id
*/
private Long catelogId;
/**
* 所属分类路径,该字段在数据库中不存在
*/
@TableField(exist = false)
private Long[] catelogPath;
}
③ Controller层:
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrGroupService attrGroupService;
@Autowired
private CategoryService categoryService;
/**
* 信息
*/
@RequestMapping("/info/{attrGroupId}")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
//根据catalogId查询出该分类的完整路径
Long[] catalogPath = categoryService.findCatalogPath(attrGroup.getCatelogId());
attrGroup.setCatelogPath(catalogPath);
return R.ok().put("attrGroup", attrGroup);
}
}
④ Service层:
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
/**
* 根据最后一级的catalogId,获取三级分类的完整路径:[2,25,225]
*/
@Override
public Long[] findCatalogPath(Long catalogId) {
List<Long> list = new ArrayList<>();
List<Long> parentPath = findParentPath(catalogId,list);
Collections.reverse(parentPath);
return parentPath.toArray(new Long[parentPath.size()]);
}
/**
* 递归获取三级分类完整路径:[2,25,225]
*/
private List<Long> findParentPath(Long catalogId, List<Long> paths) {
paths.add(catalogId);
CategoryEntity categoryEntity = getById(catalogId);
if(categoryEntity.getParentCid()!=0){
findParentPath(categoryEntity.getParentCid(),paths);
}
return paths;
}
}
6. 商品服务 - 品牌管理
6.1 模糊查询品牌列表
@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<BrandEntity> wrapper = new QueryWrapper<>();
//如果参数中包含key
String key = (String)params.get("key");
if(StringUtils.isNotEmpty(key)){
wrapper.eq("brand_id",key).or().like("name",key);
}
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),wrapper
);
return new PageUtils(page);
}
}
前端代码不再编写,将E:\studyresource\gulimall_resources\分布式基础篇资料源码\代码\前端\modules下的代码拷贝到前端文件夹下的modules中。
6.2 获取品牌关联的分类
一个品牌(华为)会关联多个分类(手机,电视),一个分类(手机)会关联多个品牌(华为,小米),这样就属于多对多的关系,在数据库中一般就会有中间表:
@RestController
@RequestMapping("product/categorybrandrelation")
public class CategoryBrandRelationController {
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
/**
* 列表
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = categoryBrandRelationService.queryPage(params);
return R.ok().put("page", page);
}
@RequestMapping("/catalog/list")
public R list(@RequestParam("brandId") Long brandId){
List<CategoryBrandRelationEntity> categoryEntities = categoryBrandRelationService.list(
new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
return R.ok().put("data", categoryEntities);
}
}
6.3 新增品牌关联的分类
① Controller层:
@RestController
@RequestMapping("product/categorybrandrelation")
public class CategoryBrandRelationController {
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}
}
② Service层:
@Service("categoryBrandRelationService")
public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {
@Autowired
private CategoryService categoryService;
@Autowired
private BrandService brandService;
@Override
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
CategoryEntity categoryEntity = categoryService.getById(catelogId);
BrandEntity brandEntity = brandService.getById(brandId);
categoryBrandRelation.setCatelogName(categoryEntity.getName());
categoryBrandRelation.setBrandName(brandEntity.getName());
this.save(categoryBrandRelation);
}
}
③ 测试:
6.4 品牌和分类级联更新
需求:当品牌名和分类名修改的时候,关联分类中的品牌名和分类名也要跟着修改。
1、更新品牌管理中的品牌名称时,同时更新品牌关联分类中的品牌名称
① Controller层:
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated(value = UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);
return R.ok();
}
}
② Service层:
@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
@Transactional
@Override
public void updateDetail(BrandEntity brand) {
// 更新品牌表中的数据
this.updateById(brand);
if(StringUtils.isNotEmpty(brand.getName())){
//如果更新了品牌名称,级联更新品牌和分类关联表中的brandName
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
}
}
}
@Service("categoryBrandRelationService")
public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {
@Autowired
private CategoryService categoryService;
@Autowired
private BrandService brandService;
@Override
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
categoryBrandRelationEntity.setBrandId(brandId);
categoryBrandRelationEntity.setBrandName(name);
//更新的条件
this.update(categoryBrandRelationEntity,
new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}
}
2、更新三级分类的分类名称时,同时更新品牌关联分类中的分类名称
① Controller层:
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateDetail(category);
return R.ok();
}
}
② Service层:
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
@Transactional
@Override
public void updateDetail(CategoryEntity category) {
//更新分类表中的分类名称
this.updateById(category);
//更新品牌分类表中的分类名称
if(StringUtils.isNotEmpty(category.getName())){
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
}
}
@Service("categoryBrandRelationService")
public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {
@Autowired
private CategoryService categoryService;
@Autowired
private BrandService brandService;
@Override
public void updateCategory(Long catId, String name) {
CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
categoryBrandRelationEntity.setCatelogId(catId);
categoryBrandRelationEntity.setCatelogName(name);
this.update(categoryBrandRelationEntity,
new UpdateWrapper<CategoryBrandRelationEntity>().eq("catelog_id",catId));
}
}
7. 商品服务 - 平台属性
7.1 新增规格参数
需求分析:
当新增规格参数时,页面提交的数据如图
当新增成功后发现只有pms_attr表中有数据:
而对应的属性和属性分组的关联表pms_attr_attrgroup_relation中没有数据:
因此需求为:当新增规格参数时,我们不仅要保存基本属性到pms_attr表中,还要保存属性对应的属性分组到pms_attr_attrgroup_relation表中。
① 因为AttrEntity类中没有attrGroupId这个属性,因此新增一个AttrVo不仅包含AttrEntity的基本属性还包括前端传过来的attrGroupId
@Data
public class AttrVo {
/**
* 属性id
*/
@TableId
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 属性图标
*/
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;
/**
* 增加的属性,用于接收前端传过来的attrGroupId,保存属性分组与属性之间的关联关系
*/
private Long attrGroupId;
}
② Controller层:
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody AttrEntity attr){
attrService.saveAttr(attr);
return R.ok();
}
}
③ Service层:
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrAttrgroupRelationService attrAttrgroupRelationService;
@Transactional
@Override
public void saveAttr(AttrVo attrVo) {
//保存基本信息到pms_attr中
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attrVo,attrEntity);
this.save(attrEntity);
//保存属性和属性分组到关联表pms_attr_attrgroup_relation中
AttrAttrgroupRelationEntity attrgroupRelationEntity = new AttrAttrgroupRelationEntity();
attrgroupRelationEntity.setAttrId(attrEntity.getAttrId());
attrgroupRelationEntity.setAttrGroupId(attrVo.getAttrGroupId());
attrAttrgroupRelationService.save(attrgroupRelationEntity);
}
}
7.2 获取分类下的规格参数
需求分析:接口API地址https://easydoc.net/s/78237135/ZUqEdvA4/Ld1Vfkcd
在响应的结果中除了包括基本信息还需要包括所属分类名称和所属分组名称:
① 响应的数据vo:
//除了包括AttrVo的属性外,还包括的属性
@Data
public class AttrRespVo extends AttrVo {
/**
* 分类名称
*/
private String catelogName;
/**
* 属性分组名称,一定要和接口api中响应数据的名称保持一致
*/
private String groupName;
}
② Controller层:
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
@GetMapping("/base/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String,Object> params,
@PathVariable("catelogId") Long catelogId){
//分页查询返回的结果都是PageUtils
PageUtils page = attrService.queryBaseAttrPage(params,catelogId);
return R.ok().put("page",page);
}
}
② Service层:
注意,尽量不要使用Service层的getById()方法,后面出现了一些bug,原因这个方法查询不到数据,最后改用了
attrAttrgroupRelationService.getOne(
new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrEntity.getAttrId()));
但是最好也不要这样用,而是用Dao层的接口调用。
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrAttrgroupRelationService attrAttrgroupRelationService;
@Autowired
CategoryService categoryService;
@Autowired
AttrGroupService attrGroupService;
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId,String attrType) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>();
// 1、查询某一个分类下的规格参数,如果catelogId==0时查询所有规格参数
if(catelogId!=0){
queryWrapper.eq("catelog_id",catelogId);
}
// 2、模糊查询
String key = (String) params.get("key");
if(StringUtils.isNotEmpty(key)){
queryWrapper.and((item->{
item.eq("attr_id",key).or().like("attr_name",key);
}));
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params),queryWrapper);
PageUtils pageUtils = new PageUtils(page);
// 3、响应的数据需要包括分类名称和分组名称
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVoList = records.stream().map(attrEntity -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//分类名称
CategoryEntity categoryEntity
= categoryService.getById(attrEntity.getCatelogId());
if(categoryEntity!=null){
attrRespVo.setCatelogName(categoryEntity.getName());
}
//分组名称
//如果是基本属性(规格参数),才设置分组信息
AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationService.getOne(
new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrEntity.getAttrId()));
if(relationEntity!=null && relationEntity.getAttrGroupId()!=null){
AttrGroupEntity attrGroupEntity
= attrGroupService.getById(relationEntity.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(respVoList);
return pageUtils;
}
}
7.3 修改规格参数
需求分析:
修改规格参数时,查询规格参数详情并回显三级分类完整路径,修改完成后保存规格参数数据
1、修改规格参数时,查询规格参数详情并回显三级分类完整路径
① 在响应类AttrRespVo中加入三级分类路径属性:
@Data
public class AttrRespVo extends AttrVo {
/**
* 分类名称
*/
private String catelogName;
/**
* 属性分组名称
*/
private String groupName;
/**
* 所属分类的完整路径
*/
private Long[] catelogPath;
}
② Controller层:
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
/**
* 信息
*/
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId){
AttrRespVo attrRespVo = attrService.getAttrInfo(attrId);
return R.ok().put("attr", attrRespVo);
}
}
② Service层:
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrAttrgroupRelationService attrAttrgroupRelationService;
@Autowired
CategoryService categoryService;
@Autowired
AttrGroupService attrGroupService;
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrEntity attrEntity = getById(attrId);
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity,attrRespVo);
//设置分组信息
AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationService
.getOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrEntity.getAttrId()));
if(relationEntity!=null){
AttrGroupEntity attrGroupEntity = attrGroupService.getOne(
new QueryWrapper<AttrGroupEntity>().eq("attr_group_id",relationEntity.getAttrGroupId()));
if(attrGroupEntity!=null){
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
//设置分类信息
CategoryEntity categoryEntity = categoryService.getById(attrRespVo.getCatelogId());
if(categoryEntity!=null){
attrRespVo.setCatelogName(categoryEntity.getName());
}
attrRespVo.setCatelogPath(categoryService.findCatelogPath(attrRespVo.getCatelogId()));
return attrRespVo;
}
}
2、修改完成后保存规格参数数据,不仅保存规格参数,还要保存关联的属性分组pms_attr_attrgroup_relation
① Controller层:
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody AttrVo attrVo){
attrService.updateAttr(attrVo);
return R.ok();
}
}
② Service层:
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrAttrgroupRelationService attrAttrgroupRelationService;
@Autowired
CategoryService categoryService;
@Autowired
AttrGroupService attrGroupService;
@Transactional
@Override
public void updateAttr(AttrVo attrVo) {
// 更新规格参数
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attrVo,attrEntity);
this.updateById(attrEntity);
// 更新关联关系
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attrVo.getAttrGroupId());
relationEntity.setAttrId(attrVo.getAttrId());
//根据attr_id查询关联属性分组
int count = attrAttrgroupRelationService.count(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrVo.getAttrId()));
if(count>0){
// 更新规格参数关联的属性分组信息
attrAttrgroupRelationService.update(relationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrVo.getAttrId()));
}else{
// 新增规格参数关联的属性分组信息
attrAttrgroupRelationService.save(relationEntity);
}
}
}
7.4 获取分类下的销售属性
属性包括两种:基本属性和销售属性,基本属性就是规格参数
销售属性和规格参数是同一张表,通过attr_type来区分,attr_type=1代表规格参数,attr_type=0代表销售属性
① Controller层:
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
// /base/list/{catelogId}
// /sale/list/{catelogId}
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String,Object> params,
@PathVariable("catelogId") Long catelogId,
@PathVariable("attrType") String attrType){
//分页查询返回的结果都是PageUtils
PageUtils page = attrService.queryBaseAttrPage(params,catelogId,attrType);
return R.ok().put("page",page);
}
}
② Service层:所有需要设置规格参数关联分组的地方都要判断是规格参数还是销售属性的请求,因为销售属性没有关联分组,如果不判断就会导致新增的销售属性也会有一个关联分组,但是值为null
7.5 获取属性分组关联的属性
需求接口文档:https://easydoc.net/s/78237135/ZUqEdvA4/LnjzZHPj,获取某一个属性分组下关联的所有属性
① Controller层:
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrGroupService attrGroupService;
@Autowired
private CategoryService categoryService;
@Autowired
private AttrService attrService;
@GetMapping("/{attrgroupId}/attr/relation")
public R attrRelation(@PathVariable("attrgroupId") String attrgroupId){
List<AttrEntity> data = attrService.getRelationAttr(attrgroupId);
return R.ok().put("data",data);
}
}
② Service层:
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrAttrgroupRelationService attrAttrgroupRelationService;
@Autowired
CategoryService categoryService;
@Autowired
AttrGroupService attrGroupService;
@Override
public List<AttrEntity> getRelationAttr(String attrgroupId) {
List<AttrAttrgroupRelationEntity> relationEntities = attrAttrgroupRelationService.list(
new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
//获取到所有的attrId
List<Long> attrIds = relationEntities.stream().map(relationEntity -> {
Long attrId = relationEntity.getAttrId();
return attrId;
}).collect(Collectors.toList());
if(attrIds == null || attrIds.size() == 0){
return null;
}
Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
return (List<AttrEntity>) attrEntities;
}
}
7.6 删除属性分组关联的属性
需求分析:实现批量删除功能
① Controller层:
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrGroupService attrGroupService;
@Autowired
private CategoryService categoryService;
@Autowired
private AttrService attrService;
@PostMapping("/attr/relation/delete")
public R deleteRelation(@RequestBody AttrGroupRelationVo[] relationVos){
attrService.deleteRelation(relationVos);
return R.ok();
}
}
② Service层:
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrAttrgroupRelationService attrAttrgroupRelationService;
@Override
public void deleteRelation(AttrGroupRelationVo[] relationVos) {
//批量删除属性分组和属性的关联关系
List<AttrAttrgroupRelationEntity> relationEntityList = Arrays.asList(relationVos).stream().map(relationVo -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(relationVo, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
attrAttrgroupRelationService.deleteBatchRelation(relationEntityList);
}
}
7.7 查询分组未关联的属性
① Controller层:
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrService attrService;
@GetMapping("/{attrgroupId}/noattr/relation")
public R attrNoRelation(@PathVariable("attrgroupId") Long attrgroupId,
@RequestParam Map<String, Object> params){
PageUtils page = attrService.getNoRelationAttr(params,attrgroupId);
return R.ok().put("page",page);
}
}
② Service层:
@Service("attrService")
public class AttrServiceImpl extends ServiceImpl<AttrDao, AttrEntity> implements AttrService {
@Autowired
AttrGroupDao attrGroupDao;
@Autowired
AttrAttrgroupRelationDao attrAttrgroupRelationDao;
@Autowired
CategoryDao categoryDao;
@Autowired
AttrDao attrDao;
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
// 当前分组只能关联当前分类下所有分组都没有关联的属性
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
Long catelogId = attrGroupEntity.getCatelogId();
// 当前分类下的所有分组
List<AttrGroupEntity> attrGroupEntities = attrGroupDao.selectList(
new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<Long> attrGroupIdList = attrGroupEntities.stream().map(attrGroupEntity1 -> {
return attrGroupEntity1.getAttrGroupId();
}).collect(Collectors.toList());
// 这些分组关联的属性
List<AttrAttrgroupRelationEntity> groupId = attrAttrgroupRelationDao.selectList(
new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", attrGroupIdList));
List<Long> attrIds = groupId.stream().map(item -> {
return item.getAttrId();
}).collect(Collectors.toList());
// 从当前分类的所有属性中移除这些属性
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>()
.eq("catelog_id", catelogId)
.eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
if(attrIds!=null && attrIds.size()>0){
queryWrapper.notIn("attr_id",attrIds);
}
//模糊查询
String key = (String)params.get("key");
if(StringUtils.isNotEmpty(key)){
queryWrapper.and(item->{
item.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
目前需要打开的四个项目:启动nacos,启动mysql,启动前端,如果不想做前端,以后可以直接使用完整项目
7.8 新增属性分组和属性的关联关系
① Controller层:
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrAttrgroupRelationService attrAttrgroupRelationService;
@PostMapping("/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVo> attrGroupRelationVos ){
attrAttrgroupRelationService.saveRelation(attrGroupRelationVos);
return R.ok();
}
}
② Service层:
@Service("attrAttrgroupRelationService")
public class AttrAttrgroupRelationServiceImpl extends ServiceImpl<AttrAttrgroupRelationDao, AttrAttrgroupRelationEntity> implements AttrAttrgroupRelationService {
@Override
public void saveRelation(List<AttrGroupRelationVo> attrGroupRelationVos) {
//把集合中所有的vo类装换为实体类
List<AttrAttrgroupRelationEntity> collect = attrGroupRelationVos.stream().map(attrGroupRelationVo -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(attrGroupRelationVo, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
saveBatch(collect);
}
}
8.商品服务 - 新增商品
8.1 获取分类关联的品牌
品牌和分类是多对多的关系,一个分类下有多个品牌,一个品牌可以关联多个分类
① Controller层:
@RestController
@RequestMapping("product/categorybrandrelation")
public class CategoryBrandRelationController {
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
//product/categorybrandrelation/brands/list
@GetMapping("/brands/list")
public R relationBrandsList(@RequestParam(value = "catId",required = true) String catId){
List<BrandEntity> brandEntities = categoryBrandRelationService.getBrandsByCatId(catId);
List<BrandVo> collect = brandEntities.stream().map(item -> {
BrandVo brandVo = new BrandVo();
brandVo.setBrandId(item.getBrandId());
brandVo.setBrandName(item.getName());
return brandVo;
}).collect(Collectors.toList());
return R.ok().put("data",collect);
}
}
② Service层:
@Service("categoryBrandRelationService")
public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {
@Autowired
private CategoryService categoryService;
@Autowired
private BrandService brandService;
@Autowired
private BrandDao brandDao;
@Override
public List<BrandEntity> getBrandsByCatId(String catId) {
List<CategoryBrandRelationEntity> relationEntityList
= this.baseMapper.selectList(
new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId));
List<Long> brandIds
= relationEntityList.stream().map(categoryBrandRelationEntity -> {
return categoryBrandRelationEntity.getBrandId();
}).collect(Collectors.toList());
List<BrandEntity> brandEntities = brandDao.selectBatchIds(brandIds);
return brandEntities;
}
}
8.2 获取分类下的属性分组及关联的属性
① Controller层
@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
@Autowired
private AttrGroupService attrGroupService;
@GetMapping("/{catelogId}/withattr")
public R getAttrGroupWithAttrs(@PathVariable("catelogId") String catelogId){
List<AttrGroupWithAttrsVo> attrGroupWithAttrsVos
= attrGroupService.getAttrGroupWithAttrsByCategoryId(catelogId);
return R.ok().put("data",attrGroupWithAttrsVos);
}
}
② Service层
@Service("attrGroupService")
public class AttrGroupServiceImpl extends ServiceImpl<AttrGroupDao, AttrGroupEntity> implements AttrGroupService {
@Autowired
private AttrAttrgroupRelationDao relationDao;
@Autowired
private AttrService attrService;
@Override
public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrsByCategoryId(String catelogId) {
//查询当前分类下的属性分组
List<AttrGroupEntity> attrGroupEntities = this.baseMapper.selectList(
new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<AttrGroupWithAttrsVo> collect
= attrGroupEntities.stream().map(attrGroupEntity -> {
AttrGroupWithAttrsVo attrsVo = new AttrGroupWithAttrsVo();
BeanUtils.copyProperties(attrGroupEntity, attrsVo);
// 获取分组下的所有属性
List<AttrEntity> relationAttr
= attrService.getRelationAttr(attrGroupEntity.getAttrGroupId());
attrsVo.setAttrs(relationAttr);
return attrsVo;
}).collect(Collectors.toList());
return collect;
}
}
8.3 新增商品
① 请求vo
@Data
public class SpuSaveVo {
/**
* pms_spu_info
*/
private String spuName;
private String spuDescription;
private Long catalogId;
private Long brandId;
private BigDecimal weight;
private int publishStatus;
/**
* pms_spu_info_desc
*/
private List<String> decript;
/**
* pms_spu_images
*/
private List<String> images;
/**
* sms_spu_bounds
*/
private Bounds bounds;
/**
* pms_product_attr_value
*/
private List<BaseAttrs> baseAttrs;
private List<Skus> skus;
}
② Controller层
@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
@Autowired
private SpuInfoService spuInfoService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody SpuSaveVo spuSaveVo){
spuInfoService.saveSpuInfo(spuSaveVo);
return R.ok();
}
}
③ Service层
@Service("spuInfoService")
public class SpuInfoServiceImpl extends ServiceImpl<SpuInfoDao, SpuInfoEntity> implements SpuInfoService {
@Autowired
SpuInfoDescService spuInfoDescService;
@Autowired
SpuImagesService spuImagesService;
@Autowired
ProductAttrValueService attrValueService;
@Autowired
SkuInfoDao skuInfoDao;
@Autowired
SkuImagesService skuImagesService;
@Autowired
SkuSaleAttrValueService skuSaleAttrValueService;
@Autowired
CouponFeignService couponFeignService;
@Autowired
SkuInfoService skuInfoService;
@Transactional(rollbackFor = Exception.class)
@Override
public void saveSpuInfo(SpuSaveVo spuSaveVo) {
// 保存pms_spu_info,spu的基本信息
SpuInfoEntity spuInfoEntity = new SpuInfoEntity();
BeanUtils.copyProperties(spuSaveVo,spuInfoEntity);
spuInfoEntity.setCreateTime(new Date());
spuInfoEntity.setUpdateTime(new Date());
this.baseMapper.insert(spuInfoEntity);
// 保存pms_spu_info_desc,spu的图片描述信息
SpuInfoDescEntity spuInfoDescEntity = new SpuInfoDescEntity();
spuInfoDescEntity.setSpuId(spuInfoEntity.getId());
spuInfoDescEntity.setDecript(String.join(",",spuSaveVo.getDecript()));
spuInfoDescService.saveInfoDesc(spuInfoDescEntity);
// 保存pms_spu_images,保存spu的图片集,一个spu对应多张图片
List<String> images = spuSaveVo.getImages();
spuImagesService.saveSpuImages(images,spuInfoEntity.getId());
// 保存pms_product_attr_value,商品的规格参数
List<BaseAttrs> baseAttrs = spuSaveVo.getBaseAttrs();
attrValueService.saveBaseAttrs(baseAttrs,spuInfoEntity.getId());
// 保存sms_spu_bounds,远程调用gulimall-coupon服务,保存积分信息
Bounds bounds = spuSaveVo.getBounds();
SpuBoundTo spuBoundTo = new SpuBoundTo();
BeanUtils.copyProperties(bounds,spuBoundTo);
spuBoundTo.setSpuId(spuInfoEntity.getId());
R r = couponFeignService.saveSpuBounds(spuBoundTo);
if(r.getCode() != 0){
log.error("远程保存spu积分信息失败");
}
// 保存sku信息
List<Skus> skus = spuSaveVo.getSkus();
if(skus!=null && skus.size()>0){
skus.forEach(item->{
// 保存sku的基本信息;pms_sku_info
String defaultImg = "";
for (Images image : item.getImages()) {
if(image.getDefaultImg() == 1){
defaultImg = image.getImgUrl();
}
}
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtils.copyProperties(item,skuInfoEntity);
skuInfoEntity.setBrandId(spuInfoEntity.getBrandId());
skuInfoEntity.setCatalogId(spuInfoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSpuId(spuInfoEntity.getId());
skuInfoEntity.setSkuDefaultImg(defaultImg);
skuInfoService.saveSkuInfo(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
// 保存sku的图片信息;pms_sku_image
List<SkuImagesEntity> imagesEntities
= item.getImages().stream().map(img -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(img.getImgUrl());
skuImagesEntity.setDefaultImg(img.getDefaultImg());
return skuImagesEntity;
}).filter(entity->{
//返回true就是需要,false就是剔除
return !StringUtils.isEmpty(entity.getImgUrl());
}).collect(Collectors.toList());
skuImagesService.saveBatch(imagesEntities);
//TODO 没有图片路径的无需保存
List<Attr> attr = item.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities
= attr.stream().map(a -> {
SkuSaleAttrValueEntity attrValueEntity
= new SkuSaleAttrValueEntity();
BeanUtils.copyProperties(a, attrValueEntity);
attrValueEntity.setSkuId(skuId);
return attrValueEntity;
}).collect(Collectors.toList());
// 保存sku的销售属性信息:pms_sku_sale_attr_value
skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
// 保存sku的优惠、满减等信息,调用远程gulimall-coupon服务
// gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
SkuReductionTo skuReductionTo = new SkuReductionTo();
BeanUtils.copyProperties(item,skuReductionTo);
skuReductionTo.setSkuId(skuId);
if(skuReductionTo.getFullCount() >0
|| skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
if(r1.getCode() != 0){
log.error("远程保存sku优惠信息失败");
}
}
});
}
}
}
④ CouponFeignService
/**
* 1、调用的服务名称
* 2、调用的服务中的方法的映射路径
* 3、SpuBrandsTo用于将gulimall-product服务中的数据传给gulimall-coupon服务
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
@PostMapping("/coupon/skufullreduction/saveinfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
⑤ SpuBoundsController
@RestController
@RequestMapping("coupon/spubounds")
public class SpuBoundsController {
@Autowired
private SpuBoundsService spuBoundsService;
/**
* 保存
*/
@PostMapping("/save")
public R save(@RequestBody SpuBoundsEntity spuBounds){
spuBoundsService.save(spuBounds);
return R.ok();
}
}
⑥ SkuFullReductionController
@RestController
@RequestMapping("coupon/skufullreduction")
public class SkuFullReductionController {
@Autowired
private SkuFullReductionService skuFullReductionService;
@PostMapping("/saveinfo")
public R saveInfo(@RequestBody SkuReductionTo reductionTo){
skuFullReductionService.saveSkuReduction(reductionTo);
return R.ok();
}
}
⑦ SkuFullReductionServiceImpl
@Service("skuFullReductionService")
public class SkuFullReductionServiceImpl extends ServiceImpl<SkuFullReductionDao, SkuFullReductionEntity> implements SkuFullReductionService {
@Autowired
MemberPriceService memberPriceService;
@Autowired
SkuFullReductionService skuFullReductionService;
@Autowired
SkuLadderService skuLadderService;
@Override
public void saveSkuReduction(SkuReductionTo skuReductionTo) {
//保存sms_sku_ladder/sms_sku_full_reduction/sms_member_price
//保存sms_sku_ladder
SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
BeanUtils.copyProperties(skuReductionTo,skuLadderEntity);
skuLadderEntity.setAddOther(skuReductionTo.getCountStatus());
skuLadderService.save(skuLadderEntity);
if(skuReductionTo.getFullCount() > 0){
skuLadderService.save(skuLadderEntity);
}
//保存sms_sku_full_reduction
SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
BeanUtils.copyProperties(skuReductionTo,skuFullReductionEntity);
if(skuFullReductionEntity.getFullPrice().compareTo(new BigDecimal("0"))==1){
this.save(skuFullReductionEntity);
}
this.save(skuFullReductionEntity);
//保存sms_member_price
List<MemberPrice> memberPriceList = skuReductionTo.getMemberPrice();
List<MemberPriceEntity> collect = memberPriceList.stream().map(item -> {
MemberPriceEntity memberPriceEntity = new MemberPriceEntity();
memberPriceEntity.setMemberLevelName(item.getName());
memberPriceEntity.setMemberPrice(item.getPrice());
memberPriceEntity.setSkuId(skuReductionTo.getSkuId());
memberPriceEntity.setMemberLevelId(item.getId());
memberPriceEntity.setAddOther(1);
return memberPriceEntity;
}).filter(item-> {
return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1;
}).collect(Collectors.toList());
memberPriceService.saveBatch(collect);
}
}
⑧ 在主配置类上加上@EnableFeignClients
// feign接口所在的包
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
8.4 SPU检索
①Controller层
@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
@Autowired
private SpuInfoService spuInfoService;
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = spuInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
}
② Service层
@Service("spuInfoService")
public class SpuInfoServiceImpl extends ServiceImpl<SpuInfoDao, SpuInfoEntity> implements SpuInfoService {
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SpuInfoEntity> queryWrapper = new QueryWrapper<>();
// (id=1 or spu_name like xxx)
String key = (String)params.get("key");
if(StringUtils.isNotEmpty(key)){
queryWrapper.and((w)->{
w.eq("id",key).or().like("spu_name",key);
});
}
// status=1 and (id=1 or spu_name like xxx)
String status = (String)params.get("status");
if(StringUtils.isNotEmpty(status)){
queryWrapper.eq("publish_status",status);
}
String catelogId = (String)params.get("catelogId");
if(StringUtils.isNotEmpty(catelogId) && "0".equalsIgnoreCase(catelogId)){
queryWrapper.eq("catalog_id",catelogId);
}
String brandId = (String)params.get("brandId");
if(StringUtils.isNotEmpty(brandId) && "0".equalsIgnoreCase(brandId)){
queryWrapper.eq("brand_id",brandId);
}
IPage<SpuInfoEntity> page = this.page(
new Query<SpuInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
③ 由于数据库中的时间格式不是yyyy-MM-dd HH:mm:ss的形式,因此可以在application.yml中添加:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
8.5 SKU检索
① Controller层
@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
@Autowired
private SkuInfoService skuInfoService;
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = skuInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
}
② Service层
@Service("skuInfoService")
public class SkuInfoServiceImpl extends ServiceImpl<SkuInfoDao, SkuInfoEntity> implements SkuInfoService {
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
String key = (String)params.get("key");
if(StringUtils.isNotEmpty(key)){
queryWrapper.and((wrapper)->{
wrapper.eq("sku_id",key).or().like("sku_name",key);
});
}
String catalogId = (String)params.get("catalog_id");
if(StringUtils.isNotEmpty(catalogId) && !"0".equalsIgnoreCase(catalogId)){
queryWrapper.eq("catalog_id",catalogId);
}
String brandId = (String)params.get("brand_id");
if(StringUtils.isNotEmpty(brandId) && !"0".equalsIgnoreCase(brandId)){
queryWrapper.eq("brand_id",brandId);
}
String min = (String)params.get("min");
if(StringUtils.isNotEmpty(min)){
queryWrapper.ge("price",min);
}
String max = (String)params.get("max");
if(StringUtils.isNotEmpty(max)){
//当max的值大于0才拼接:compareTo返回1时表示前者大于后者
try {
//如果传进来的不是数字会出现异常,比如abc
if(new BigDecimal(max).compareTo(new BigDecimal("0"))==1){
queryWrapper.le("price",max);
}
}catch (Exception e){
}
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
9.商品服务 - 仓库管理
9.1 整合gulimall-ware服务
① 将gulimall-ware服务加入注册中心
spring:
application:
name: gulimall-ware
cloud:
nacos:
discovery:
server-addr: 127.0.0.1
② 设置日志打印级别:debug,控制台会打印SQL信息
logging:
level:
com.atguigu.gulimall: debug
③ 开启服务注册与发现功能
@EnableTransactionManagement
@MapperScan("com.atguigu.gulimall.product.dao")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
④ 在gulimall-gateway中配置网关
- id: ware_route
uri: lb://gulimall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
⑤ 将gulimall-ware加入配置中心,bootstrap.properties
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=gulimall-ware
spring.cloud.nacos.config.namespace=33deb04b-3977-4c53-9545-e1f1dc2f272e
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rlpwI3u-1632237077788)(imgs/image-20210921152245127.png)]
⑥ 仓库维护模糊检索
@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
@Autowired
private WareInfoService wareInfoService;
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = wareInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
}
@Service("wareInfoService")
public class WareInfoServiceImpl extends ServiceImpl<WareInfoDao, WareInfoEntity> implements WareInfoService {
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<WareInfoEntity> queryWrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if(StringUtils.isNotEmpty(key)){
queryWrapper.and(item->{
item.eq("id",key)
.or().like("name",key)
.or().eq("areacode",key)
.or().like("address",key);
});
}
IPage<WareInfoEntity> page = this.page(
new Query<WareInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
9.3 检索库存
① Controller层
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = wareSkuService.queryPage(params);
return R.ok().put("page", page);
}
}
② Service层
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<WareSkuEntity> queryWrapper = new QueryWrapper<>();
String skuId = (String) params.get("skuId");
if(StringUtils.isNotEmpty(skuId)){
queryWrapper.eq("sku_id",skuId);
}
String wareId = (String) params.get("wareId");
if(StringUtils.isNotEmpty(wareId)){
queryWrapper.eq("ware_id",wareId);
}
IPage<WareSkuEntity> page = this.page(
new Query<WareSkuEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
9.4 查询采购需求
① Controller层
@RestController
@RequestMapping("ware/purchasedetail")
public class PurchaseDetailController {
@Autowired
private PurchaseDetailService purchaseDetailService;
@RequestMapping("/unreceive/list")
public R unreceivelist(@RequestParam Map<String, Object> params){
PageUtils page = purchaseService.queryPageUnreceive(params);
return R.ok().put("page", page);
}
}
② Service层
@Service("purchaseDetailService")
public class PurchaseDetailServiceImpl extends ServiceImpl<PurchaseDetailDao, PurchaseDetailEntity> implements PurchaseDetailService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<PurchaseDetailEntity> queryWrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if(StringUtils.isNotEmpty(key)){
queryWrapper.and(item->{
item.eq("purchase_id",key).or().eq("sku_id",key);
});
}
String status = (String) params.get("status");
if(StringUtils.isNotEmpty(status)){
queryWrapper.eq("status",status);
}
String wareId = (String) params.get("wareId");
if(StringUtils.isNotEmpty(wareId)){
queryWrapper.eq("ware_id",wareId);
}
IPage<PurchaseDetailEntity> page = this.page(
new Query<PurchaseDetailEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
9.5 查询未领取的采购单
① Controller层
@RestController
@RequestMapping("ware/purchase")
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = purchaseService.queryPageUnreceive(params);
return R.ok().put("page", page);
}
}
② Service层
@Service("purchaseService")
public class PurchaseServiceImpl extends ServiceImpl<PurchaseDao, PurchaseEntity> implements PurchaseService {
@Override
public PageUtils queryPageUnreceive(Map<String, Object> params) {
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
// status为0和1代表为还没领取的采购单
new QueryWrapper<PurchaseEntity>().eq("status","0").or().eq("status","1")
);
return new PageUtils(page);
}
}
9.6 合并采购需求到采购单
需求:如果没有选择任何【采购单】,将自动创建新单进行合并,如果有采购单就会将采购需求合并到采购单
所谓合并采购单就是更新采购需求项。
① 请求vo
@Data
public class MergeVo {
//前端请求参数
private Long purchaseId;
private List<Long> items;
}
② Controller层
@RestController
@RequestMapping("ware/purchase")
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@PostMapping("/merge")
public R merge(@RequestBody MergeVo mergeVo){
purchaseService.merge(mergeVo);
return R.ok();
}
}
③ Service层
@Service("purchaseService")
public class PurchaseServiceImpl extends ServiceImpl<PurchaseDao, PurchaseEntity> implements PurchaseService {
@Autowired
PurchaseDetailService purchaseDetailService;
@Override
public void merge(MergeVo mergeVo) {
Long purchaseId = mergeVo.getPurchaseId();
//说明没有采购单,则新建一个采购单
if(purchaseId==null){
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setCreateTime(new Date());
purchaseEntity.setUpdateTime(new Date());
purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());
this.baseMapper.insert(purchaseEntity);
purchaseId = purchaseEntity.getId();
}
//更新要合并的采购需求,将他们的状态改为已分配
List<Long> items = mergeVo.getItems();
Long finalPurchaseId = purchaseId;
List<PurchaseDetailEntity> collect = items.stream().map(id -> {
PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
detailEntity.setId(id);
detailEntity.setPurchaseId(finalPurchaseId);
detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());
return detailEntity;
}).collect(Collectors.toList());
purchaseDetailService.updateBatchById(collect);
}
}
合并采并单之前:
合并采购单之后:
9.7 领取采购单
需求分析:领取采购单就是更新采购单的状态,只有采购单状态为新建和已分配状态才能领取,领取胡需要将采购单中的采购需求撞他变为正在采购状态
① Controller层
@RestController
@RequestMapping("ware/purchase")
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@PostMapping("/received")
public R receive(@RequestBody List<Long> ids){
purchaseService.receive(ids);
return R.ok();
}
}
② Service层
@Service("purchaseService")
public class PurchaseServiceImpl extends ServiceImpl<PurchaseDao, PurchaseEntity> implements PurchaseService {
@Autowired
PurchaseDetailService purchaseDetailService;
@Override
public void receive(List<Long> ids) {
List<PurchaseEntity> purchaseEntityList = ids.stream().map(id -> {
return this.getById(id);
}).filter(purchaseEntity -> {
// 过滤出采购单的状态是创建和已分配的
if(purchaseEntity.getStatus()
.equals(WareConstant.PurchaseStatusEnum.CREATED.getCode())||
purchaseEntity.getStatus()
.equals(WareConstant.PurchaseStatusEnum.ASSIGNED.getCode())){
return true;
}
return false;
}).map(purchaseEntity -> {
// 采购单的状态设置为已领取
purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());
purchaseEntity.setUpdateTime(new Date());
return purchaseEntity;
}).collect(Collectors.toList());
// 批量更新采购单状态
this.updateBatchById(purchaseEntityList);
// 更改采购需求的状态,除了stream可以取出每一个元素,foreach也可以,双层list时先用foreach
purchaseEntityList.forEach(purchaseEntity -> {
List<PurchaseDetailEntity> purchaseDetailEntityList
= purchaseDetailService.getByPurchaseId(purchaseEntity.getId());
List<PurchaseDetailEntity> collect
= purchaseDetailEntityList.stream().map(purchaseDetailEntity -> {
purchaseDetailEntity.setStatus(
WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());
return purchaseDetailEntity;
}).collect(Collectors.toList());
purchaseDetailService.updateBatchById(collect);
});
}
}
9.8 完成采购
① 请求vo
@Data
public class PurchaseDoneVo {
private Long id;
private List<PurchaseItemDoneVo> items;
}
@Data
public class PurchaseItemDoneVo {
private Long itemId;
private Integer status;
private String reason;
}
② Controller层
@RestController
@RequestMapping("ware/purchase")
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@PostMapping("/done")
public R finish(@RequestBody PurchaseDoneVo purchaseDoneVo){
purchaseService.done(purchaseDoneVo);
return R.ok();
}
}
③ Service层
@Service("purchaseService")
public class PurchaseServiceImpl extends ServiceImpl<PurchaseDao, PurchaseEntity> implements PurchaseService {
@Autowired
PurchaseDetailService purchaseDetailService;
@Autowired
WareSkuService wareSkuService;
@Transactional(rollbackFor = Exception.class)
@Override
public void done(PurchaseDoneVo purchaseDoneVo) {
// 修改采购项的状态,只有采购项都采购成功了,采购单才能采购成功
List<PurchaseItemDoneVo> items = purchaseDoneVo.getItems();
Boolean flag = true;
List<PurchaseDetailEntity> purchaseDetailEntityList = new ArrayList<>();
for(PurchaseItemDoneVo purchaseItemDoneVo:items){
PurchaseDetailEntity detailEntity = purchaseDetailService.getById(purchaseItemDoneVo.getItemId());
if(purchaseItemDoneVo.getStatus()
.equals(WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode())){
// 如果前端传来的采购项状态为有异常
detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum
.HASERROR.getCode());
flag = false;
}else{
// 如果前端传来的采购项状态为成功
detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum
.FINISH.getCode());
// 采购成功后,添加进库存
wareSkuService.addStock(detailEntity.getSkuId(),detailEntity.getSkuNum()
,detailEntity.getWareId());
}
purchaseDetailEntityList.add(detailEntity);
}
purchaseDetailService.updateBatchById(purchaseDetailEntityList);
// 改变采购单单的状态
PurchaseEntity purchaseEntity
= this.baseMapper.selectById(purchaseDoneVo.getId());
if(flag){
purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.FINISH.getCode());
}else{
purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.HASERROR.getCode());
}
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}
}
④ 添加到库存addStock()方法
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
@Autowired
private ProductFeignService productFeignService;
@Override
public void addStock(Long skuId, Integer skuNum, Long wareId) {
// 判断库存中有没有这个数据
Integer count = this.baseMapper.selectCount(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
if(count<=0){
// 新增库存
WareSkuEntity wareSkuEntity = new WareSkuEntity();
wareSkuEntity.setStock(skuNum);
wareSkuEntity.setSkuId(skuId);
wareSkuEntity.setWareId(wareId);
wareSkuEntity.setStockLocked(0);
// 远程查询gulimall-product服务
try {
R info = productFeignService.info(skuId);
Map<String, Object> data = (Map<String, Object>) info.get("skuInfo");
if (info.getCode() == 0) {
wareSkuEntity.setSkuName((String) data.get("skuName"));
}
}catch (Exception e){
}
this.baseMapper.insert(wareSkuEntity);
}else {
//更新库存数量
this.baseMapper.addStock(skuId,skuNum,wareId);
}
}
}
⑤ 远程查询gulimall-product服务
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
public R info(@PathVariable("skuId") Long skuId);
}
9.9 获取SPU规格
① Controller层
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
@Autowired
private ProductAttrValueService productAttrValueService;
@GetMapping("/base/listforspu/{spuId}")
public R listForSpu(@PathVariable("spuId") Long spuId){
List<ProductAttrValueEntity> data
= productAttrValueService.listForSpuBySpuId(spuId);
return R.ok().put("data",data);
}
}
② Service层
@Service("productAttrValueService")
public class ProductAttrValueServiceImpl extends ServiceImpl<ProductAttrValueDao, ProductAttrValueEntity> implements ProductAttrValueService {
@Override
public List<ProductAttrValueEntity> listForSpuBySpuId(Long spuId) {
return this.baseMapper
.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
}
}
前端访问显示:
解决方法,在数据库中gulimall-admin中的sys_menu表中添加一行数据:
9.10 修改商品规格
① 请求vo
@Data
public class ProductAttrValueVo {
private Long attrId;
private String attrName;
private String attrValue;
private Integer quickShow;
}
② Controller层
@RestController
@RequestMapping("product/attr")
public class AttrController {
@Autowired
private AttrService attrService;
@Autowired
private ProductAttrValueService productAttrValueService;
@PostMapping("/update/{spuId}")
public R updateSpu(@PathVariable("spuId") Long spuId,
@RequestBody List<ProductAttrValueVo> attrValueVos){
productAttrValueService.updateSpuAttr(spuId,attrValueVos);
return R.ok();
}
}
③ Service层
@Service("productAttrValueService")
public class ProductAttrValueServiceImpl extends ServiceImpl<ProductAttrValueDao, ProductAttrValueEntity> implements ProductAttrValueService {
@Autowired
private AttrService attrService;
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueVo> attrValueVos) {
this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));
List<ProductAttrValueEntity> collect = attrValueVos.stream().map(productAttrValueVo -> {
ProductAttrValueEntity attrValueEntity = new ProductAttrValueEntity();
attrValueEntity.setSpuId(spuId);
BeanUtils.copyProperties(productAttrValueVo, attrValueEntity);
return attrValueEntity;
}).collect(Collectors.toList());
this.saveBatch(collect);
}
}
标签:private,class,二刷,谷粒,gulimall,new,data,public,商城 来源: https://blog.csdn.net/qq_42764468/article/details/119962189