一、环境搭建

1.docker中安装并启动mysql

拉取mysql

docker pull mysql:5.7

启动mysql

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

在/mydata/mysql/conf修改mysql配置

vi /mydata/mysql/conf/my.cnf
[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci' init_connect='SET NAMES utf8' character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve

2.docker 安装 redis

1、下载镜像文件

docker pull redis

2、创建实例并启动

mkdir -p /mydata/redis/conf 
touch /mydata/redis/conf/redis.conf
docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \ -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \ -d redis redis-server /etc/redis/redis.conf

3.maven 3.6.3

注意在idea创建项目时,使用 IDEA 内置的 Spring Initializr 创建 Spring Boot 新项目时,没有 Java 8 的选项了,只剩下了 >= 17 的版本,原来这是因为 Spring Boot 官方不再支持 Spring Boot 的 2.x 版本了,之后全力维护 3.x;而 Spring Boot 3.x 对 JDK 版本的最低要求是 17!

我们可以使用阿里云提供的镜像(https://start.aliyun.com/),

在 IDEA 里更改 Server URL 即可:https://start.aliyun.com/

4.git配置

1.下载

https://git-sc

2.配置git,右键选择git bash

# 配置git用户名
git config --global user.name "username"
# 配置邮箱
git config --global user.email "username@qq.com"

3、配置 ssh 免密登录

1.我使用Git同时绑定了Github和Gitee

ssh-keygen -t rsa -C "此处填你的github绑定邮箱" 
ssh-keygen -t rsa -C "此处填你的gitee绑定邮箱" -f "id_rsa_gitee"

使用命令后

image-20240322211731103

2.将id_rsa.pub和id_rsa_gitee.pub中的ssh公钥复制绑定然后编写config文件(没有可自己创建配置如下信息)

# gitee
Host gitee.com
HostName gitee.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_gitee

# github
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa
  1. ssh -T git@gitee.com 和 ssh -T git@github.com 测试是否成功连接

image-20240322212131277

5.接口文档

https://easydoc.net/s/78237135/ZUqEdvA4/HqQGp9TI

二、快速开发(逆向工程)

1.使用人人开源搭建后台管理系统

1.从gitee上克隆下来

git clone https://gitee.com/renrenio/renren-fast.git
git clone https://gitee.com/renrenio/renren-fast-vue.git
代码生成器
git clone https://gitee.com/renrenio/renren-generator.git

2.将项目导入到自己的项目中并交给聚合服务管理(讲原来的.git删掉)

<module>gulimall-coupon</module>
<module>gulimall-member</module>
<module>gulimall-order</module>
<module>gulimall-product</module>
<module>gulimall-ware</module>
<module>renren-fast</module>
<module>renren-generator</module>

3.复制db文件夹中sql语句创建gulimall_admin数据库执行

4.修改配置文件中数据库连接信息启动renren-fast因为renren-fast-vue前后端分离,所以要将renren-fast-vue启动才能访问8080端口

5.打开renren-fast-vue

执行

npm install
然后
npm run dev

下载项目所依赖的组件并运行

6.修改renren-generator的配置信息想要生成哪个数据库增删改查代码就写哪个

# mysql
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
#修改表即可
url: jdbc:mysql://192.168.188.132:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123

修改generator.properties

mainPath=com.atguigu

package=com.atguigu.gulimall
//修改
moduleName=product

author=yinqiangqiang
#Email
email=1872173046@qq.com
//修改表前缀
tablePrefix=pms_

三、分布式组件(Spring Cloud Alibaba和Spring Cloud)

1.前置选择

1.1选择方案:

springcloudAlibaba中文文档官网

https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/README-zh.md

springcloud官网

http://spring.p2hp.com/projects/spring-cloud.html

*SpringCloud Alibaba - Nacos***:注册中心(服务发现/注册)

*SpringCloud Alibaba - Nacos***:配置中心(动态配置管理)

*SpringCloud - Ribbon***:负载均衡

*SpringCloud - Feign***:声明式HTTP 客户端(调用远程服务)

*SpringCloud Alibaba - Sentinel***:服务容错(限流、降级、熔断)

*SpringCloud - Gateway***:API网关(webflux编程模式)

*SpringCloud - Sleuth***:调用链监控

*SpringCloud Alibaba - Seata***:原Fescar,即分布式事务解决方案

1.2.版本选择

  • 2022.x 分支对应的是 Spring Cloud 2022 与 Spring Boot 3.0.x,最低支持 JDK 17。
  • 2021.x 分支对应的是 Spring Cloud 2021 与 Spring Boot 2.6.x,最低支持 JDK 1.8。
  • 2020.0 分支对应的是 Spring Cloud 2020 与 Spring Boot 2.4.x,最低支持 JDK 1.8。
  • 2.2.x 分支对应的是 Spring Cloud Hoxton 与 Spring Boot 2.2.x,最低支持 JDK 1.8。
  • greenwich 分支对应的是 Spring Cloud Greenwich 与 Spring Boot 2.1.x,最低支持 JDK 1.8。
  • finchley 分支对应的是 Spring Cloud Finchley 与 Spring Boot 2.0.x,最低支持 JDK 1.8。
  • 1.x 分支对应的是 Spring Cloud Edgware 与 Spring Boot 1.x,最低支持 JDK 1.7。

2.spring Cloud Alibaba使用

如果需要使用已发布的版本,在 dependencyManagement 中添加如下配置。

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

然后在 dependencies 中添加自己所需使用的依赖即可使用。如果你想选择老版本,可以参考版本说明

3.SpringCloud Alibaba-Nacos[作为注册中心]

  1. 首先,修改 pom.xml 文件,引入 spring-cloud-starter-alibaba-nacos-config ;

    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. 在应用的 /src/main/resources/application.yaml 配置文件中配置 Nacos 地址并引入服务配置;

spring:
cloud:
nacos:
server-addr: localhost:8848 #端口
application:
name: gulimall-coupon

3.启动nacos时用命令行启动的话输入以单机模式启动

startup.cmd -m standalone

点击的话可能以集群方式启动就会一直starting

4.加注解@EnableDiscoveryClient,我的nacos2.2.0好像不用加也可以注册成功

4.feign声明式远程调用使用

Feign 使用:

1、导入openfeign包,可以在使用idea构建模块一开始就选择

2、开启@EnableFeignClients (“使用远程调用的包”)功能,只要服务启动自动扫描该包下有@FeignClient注解的接口

3、编写接口,进行远程调用

//优惠券服务
@RequestMapping("/member/list")
public R memberCoupon(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("满100减10");
return R.ok().put("coupons", Arrays.asList(couponEntity));
}
//会员服务调用优惠券服务,使用openFeign
@Autowired
private CouponFeignService couponFeignService;

public R test(){
MemberEntity memberEntity = new MemberEntity();
memberEntity.setNickname("张三");
R memberCoupon = couponFeignService.memberCoupon();
return R.ok().put("member",memberEntity).put("coupons",memberCoupon.get("coupons"));
}

4.SpringCloud Feign在Hoxton.M2 RELEASED版本之后抛弃了Ribbon,使用了spring-cloud-loadbalancer。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>

5.SpringCloud Alibaba-Nacos配置中心

1.common模块pom文件引入 Nacos Config

<dependency>

<groupId>com.alibaba.cloud</groupId>

<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>

</dependency>

gulimall-coupon模块加入(2020.X.X版本官方重构了bootstrap引导配置的加载方式,需要添加以下依赖)(我的gulimall-common加了报错,所以加到了每个微服务里)

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

2.在resource文件夹下创建bootstrap.properties会优先于application.yml加载

spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

3.测试:application.properties加入

coupon.user.name=zhangsan
coupon.user.password=123

4.编写controller

@Value("${coupon.user.name}")
private String name;
@Value("${coupon.user.password}")
private int password;

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

5.注解@RefreshScope(加在Controller层不要放在启动类中,刷新数据不出)或者使用@ConfigurationProperties注解代替@Value注解。

6.nacos中配置列表点加号新建配置文件,文件名为gulimall-coupon.properties看控制台日志确定(也可指定)

7.然后重启服务就可以热部署了(优先使用配置中心中有的配置)

6.spring cloud Gateway(API网关)

理解: (满足某种断言,路由到某个地方)

1.功能

权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。

路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。

限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

2.网关是一个单独的服务,也可以注册到nacos中

3.使用步骤

3.1注解@EnableDiscoveryClient(看版本是否要加)

3.2引入gulimall-common和cloud-bootstrap依赖并且排除掉mybatisplus相关依赖或者排除掉与数据库有关的自动配置(**@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})**),不然会报错

Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

3.application.yml案例配置

server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
filters: #过滤器在路由到服务之间进行一系列的操作,可以加请求头等等

四、商品服务

1.三级分类-递归树形结构数据

1.1controller层CategoryController类

@RequestMapping("/list/tree")
//@RequiresPermissions("product:category:list")
public R list(){
List<CategoryEntity> listWithTree = categoryService.listWithTree();

return R.ok().put("data", listWithTree);
}

1.2在Entity层CategoryEntity类中加入属性因为该属性不在数据库中,所以exist=false

@TableField(exist = false)
//表示子分类
private List<CategoryEntity> children;

1.3service层CategoryServiceImpl类(用到了递归和stream流)

public List<CategoryEntity> listWithTree() {
//1.查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2.组装成父子树形结构
//2.1找到所有的一级分类
List<CategoryEntity> level1Menus = entities.stream()
.filter(categoryEntity ->
categoryEntity.getParentCid() == 0)//过滤出parentcid为0
.map(menu -> {
menu.setChildren(getChildrens(menu, entities));//为改分类赋值子分类
return menu;
})
.sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) -
(menu2.getSort() == null ? 0 : menu2.getSort()))
.collect(Collectors.toList());
return level1Menus;
}

private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
List<CategoryEntity> children = all.stream()
.filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
})
.map(menu -> {
menu.setChildren(getChildrens(menu, all));
return menu;
})
.sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) -
(menu2.getSort() == null ? 0 : menu2.getSort()))
.collect(Collectors.toList());
return children;
}

1.4启动服务,可知已经分类完成

image-20240329203917436

1.5启动renren-fast服务和renren-fast-vue服务(npm run dev)登录后台管理系统后

创建一级菜单:

image-20200425164019287

创建完成后,在后台的管理系统中会创建一条记录:

image-20200425164201813

然后创建子菜单:

image-20240329204652329

这个后台管理系统规则为点击分类维护菜单后,product/category路径会被替换为product-category如下:

http://localhost:8001/#/product-category

1.6配置renren-fast-vue文件

创建renren-fast-vue\src\views\modules\product目录,之所以这样创建,是因为product/category,对应于product-category

在该目录下,新建“category.vue”文件:

刷新页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”,这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree,

修正这个问题:

替换“static\config\index.js”文件中的“window.SITE_CONFIG[‘baseUrl’]”

替换前:

window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';

替换后:带上前缀/api在网关中容易区分路由到哪个微服务

window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

http://localhost:88,这个地址是我们网关微服务的接口。

1.7将renren-fast注册到nacos,通过网关路由到该服务

修改依赖

<spring-boot.version>2.6.13</spring-boot.version>

引入依赖

<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

在application.yml中加入配置

application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 192.168.137.14:8848

config:
name: renren-fast
server-addr: 192.168.137:8848
namespace: ee409c3f-3206-4a3b-ba65-7376922a886d

配置网关路由,前台的所有请求都是经由“http://localhost:88/api”来转发的,在“gulimall-gateway”中添加路由规则:

- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**

但是当我们访问:http://localhost:8001/#/login,发现验证码不显示:

分析原因:

  1. 现在的验证码请求路径为,http://localhost:88/api/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
  2. 原始的验证码请求路径:http://localhost:8001/renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6

在admin_route的路由规则下,在访问路径中包含了“api”,因此它会将它转发到renren-fast,网关在转发的时候,会使用网关的前缀信息,为了能够正常的取得验证码,我们需要对请求路径进行重写

1.8路径重写

修改路由规则

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

报错503

image-20240329215453873

在网关服务pom文件中加入

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>

原因:

springcloud2020弃用了Ribbon,因此Alibaba在2021版本nacos中删除了Ribbon的jar包
需要引入springcloud loadbalancer包。

请求状态为200,可是验证码不出现

renren-fast控制台报错

When allowCredentials is true, allowedOrigins cannot contain the special value “*” since that cannot be set on the “Access-Control-Allow-Origin” response header. To allow credentials to a set of origins, list them explicitly or consider using “allowedOriginPatterns” instead.

解决:renren-fast corsConfig类将.allowedOrigins(““)替换为.allowedOriginPatterns(““)

image-20240330204832290

image-20240330205000104

1.9访问被拒403和CORS错误(跨域)

image-20240330210352760

控制台报错:

Access to XMLHttpRequest at ‘http://localhost:88/api/sys/login‘ from origin ‘http://localhost:8001‘ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。 就是说8001给远程的88端口发送请求默认是不允许的,利用同源策略限制。

问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制

解决:

因为这个登录请求是非简单请求,所以会在发送请求之前发送预检请求OPTIONS,询问服务器是否允许跨域,那么我们只需配置当次请求允许跨域即可。网关统一配置跨域

跨域流程:

image-20200425193136641

image-20200425193523849

image-20200425193614185

解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。

@Configuration
public class GulimallCorsConfiguration {

@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//配置跨域
corsConfiguration.addAllowedHeader("*");//允许任意请求头跨域
corsConfiguration.addAllowedMethod("*");//允许任意请求方式跨域
corsConfiguration.addAllowedOriginPattern("*");//允许任意请求来源跨域
corsConfiguration.setAllowCredentials(true);//是否允许携带cookie跨域

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

再次访问:http://localhost:8001/#/login

![image-20240330213939607](C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20240330213939607.png

image-20200425195437299

http://localhost:8001/renre已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6

出现了多个请求,并且也存在多个跨源请求。

原因:renren-fast项目也配置了跨域

image-20240330214016679

为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问,成功登录。

1.10显示分类信息

在显示分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在

![image-20200425213240724](D:/黑马代码资料/谷粒商城/谷粒商城/Guli Mall/文档/三阶段大纲md/images/image-20200425213240724.pngimage-20240330220912276

这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree

但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。

解决方法就是定义一个product路由规则,进行路径重写:

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

在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。


2.三级分类-菜单删除、新增和修改

2.1测试删除

添加delete和append标识,并且增加复选框

<el-tree
:data="menus"
show-checkbox //显示复选框
:props="defaultProps"
:expand-on-click-node="false" //设置节点点击时不展开
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">Append</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>Delete</el-button>
</span>
</span>
</el-tree>

测试删除数据,打开postman输入“ http://localhost:88/api/product/category/delete ”,请求方式设置为POST,为了比对效果,可以在删除之前查询数据库的pms_category表:

由于delete请求接收的是一个数组,所以这里使用JSON方式,传入了一个数组:

image-20240331181120496

再次查询数据库能够看到cat_id为1432的数据已经被删除了。

修改“com.bigdata.gulimall.product.controller.CategoryController”类,添加如下代码:

 @RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//删除之前需要判断待删除的菜单那是否被别的地方所引用。
// categoryService.removeByIds(Arrays.asList(catIds));

categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}

com.bigdata.gulimall.product.service.impl.CategoryServiceImpl

@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 检查当前的菜单是否被别的地方所引用
categoryDao.deleteBatchIds(asList);
}

2.2逻辑删除(我的版本是3.2.0)

然而多数时候,我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除;

可以设置show_status为0,标记它已经被删除。

mybatis-plus的逻辑删除:

image-20240331182211380

配置全局的逻辑删除规则,在“src/main/resources/application.yml”文件中添加如下内容:

mybatis-plus:
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0

修改“com.bigdata.gulimall.product.entity.CategoryEntity”类,添加上@TableLogic,表明使用逻辑删除:

/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;

然后在POSTMan中测试一下是否能够满足需要。另外在“src/main/resources/application.yml”文件中,设置日志级别,打印出SQL语句:

logging:
level:
com.bigdata.gulimall.product: debug

打印的日志:

==>  Preparing: UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1 
==> Parameters: 1431(Long)
<== Updates: 1
get changedGroupKeys:[]

image-20240331182616586

前端代码片段

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

2.3新增和修改

页面

<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:default-expanded-keys="expandedKey"
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 type="text" size="mini" @click="() => edit(data)">
edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>

data

data() {
return {
title: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
icon: "",
productUnit: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name",
},
};

方法

getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.tree);
this.menus = data.tree;
});
},
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 = null;
this.category.icon = "";
this.category.productUnit = "";
this.category.showStatus = 1;
this.category.sort = 0;
},

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);
},
//添加三级分类
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];
},
edit(data) {
console.log("要修改的数据", data);
this.dialogType = "edit";
this.title = "修改分类";
this.dialogVisible = true;

this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
//请求成功
console.log("要回显的数据", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.showStatus = data.data.showStatus;
this.category.sort = data.data.sort;
});
},
//修改三级分类数据
editCategory() {
//解构
var { catId, name, icon, productUnit } = this.category;
console.log("productUnit",this.category.productUnit);
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];
});
},
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},

3.三级分类-修改-菜单拖拽

同一个菜单内拖动 正常
拖动到父菜单的前面或后面 正常
拖动到父菜单同级的另外一个菜单中 正常

关注的焦点在于,拖动到目标节点中,使得目标节点的catlevel+deep小于3即可。拖动到目标节点前后的条件是,使得

拖动菜单时需要修改顺序和级别

需要考虑两种类型节点的catLevel

一种关系是:如果是同一个节点下的子节点的前后移动,则不需要修改其catLevel

如果是拖动到另外一个节点内或父节点中,则要考虑修改其catLevel

如果拖动到与父节点平级的节点关系中,则要将该拖动的节点的catLevel,设置为兄弟节点的Level,

先考虑parentCid还是先考虑catLevel?

两种关系在耦合

另外还有一种是前后拖动的情况

哪个范围最大?

肯定是拖动类型关系最大,

如果是前后拖动,则拖动后需要看待拖动节点的层级和设置待拖动节点的parentId,

​ 如果待拖动节点和目标节点的层级相同,则认为是同级拖动,只需要修改节点的先后顺序即可;

​ 否则认为是跨级拖动,则需要修改层级和重新设置parentID

​ 如果

​ 以拖动类型来分,并不合适,比较合适的是跨级拖动和同级拖动

如何判断是跨级拖动还是同级拖动,根据拖动的层级来看,如果是同一级的拖动,只需要修改先后顺序即可,但是这样也会存在一个问题,就是当拖动到另外一个分组下的同级目录中,显然也需要修改parentID,究竟什么样的模型最好呢?

另外也可以判断在跨级移动时,跨级后的parentID是否相同,如果不相同,则认为是在不同目录下的跨级移动需要修改parentID。

顺序、catLevel和parentID

同级移动:

(1)首先判断待移动节点和目标节点的catLevel是否相同,

(2)相同则认为是同级移动,

​ 如果此时移动后目标节点的parentID和待移动节点的相同,但是移动类型是前后移动,只需要调整顺序即可,此时移动类型是inner,则需要修改catLevel和parentId和顺序

​ 如果此时移动后目标节点的parentID和待移动节点的不相同,但是移动类型是前后移动,则需要调整顺序和parentId,此时移动类型是inner,则需要修改catLevel和parentId和顺序

通过这两步的操作能看到一些共性,如果抽取移动类型作为大的分类,则在这种分类下,

如果是前后移动,则分为下面几种情况:

​ 同级别下的前后移动:界定标准为catLevel相同,但是又可以分为parentID相同和parentID不同,parent相同时,只需要修改顺序即可;parentID不同时,需要修改parentID和顺序

​ 不同级别下的前后移动:界定标准为catLevel不同,此时无论如何都要修改parentID,顺序和catLevel

如果是inner类型移动,则分为一下的几种情况。

​ 此时不论是同级inner,还是跨级innner,都需要修改parentID,顺序和catLevel

哪种情况需要更新子节点呢?

那就要看要拖拽的节点是否含有子节点,如果有子节点,则需要更新子节点的catLevel,不需要更新它之间的顺序和parentId,只需要更新catLevel即可。这种更新子节点的Level应该归类,目前的目标是只要有子节点就更新它的catLevel,

(2)如果待移动节点和目标节点的catLevel不同,则认为是跨级移动。如果是移动到父节点中,则需要设置catLevel,parentID和顺序。此时需要分两种情况来考虑,如果是移动到父节点中,则需要设置catLevel,parentID和顺序,如果是移动到兄弟节点中,则需要设置

包含移动到父节点同级目录,兄弟节点中。

@RequestMapping("/update/sort")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity[] category){
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}

设置菜单拖动开关

<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>

但是现在存在的一个问题是每次拖拽的时候,都会发送请求,更新数据库这样频繁的与数据库交互,现在想要实现一个拖拽过程中不更新数据库,拖拽完成后,统一提交拖拽后的数据。

现在还存在一个问题,如果是将一个菜单连续的拖拽,最终还放到了原来的位置,但是updateNode中却出现了很多节点更新信息,这样显然也是一个问题。

批量删除

<el-button type="danger" plain size="small" @click="batchDelete">批量删除</el-button>
//批量删除
batchDelete() {
let checkNodes = this.$refs.menuTree.getCheckedNodes();

// console.log("被选中的节点:",checkNodes);

let catIds = [];
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId);
}

this.$confirm(`确定要删除?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false)
}).then(({ data }) => {
this.$message({
message: "菜单批量删除成功",
type: "success"
});

//重新刷新页面
this.getMeus();
});


})
.catch(() => {
//取消删除
});
},

4.品牌管理菜单

4.1后台管理系统添加,品牌管理菜单

image-20240401183921302

(2)将“”逆向工程得到的resources\src\views\modules\product文件拷贝到gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件

brand.vue 和brand-add-or-update.vue

但是显示的页面没有新增和删除功能,这是因为权限控制的原因,

image-20200428170325515

<el-button v-if="isAuth('product:brand:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:brand:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>

查看“isAuth”的定义位置:

image-20200428170437592

它是在“index.js”中定义,现在将它设置为返回值为true,即可显示添加和删除功能。

再次刷新页面能够看到,按钮已经出现了:

image-20200428170644511

4.2添加“显示状态按钮”

brand.vue

<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="updateBrandStatus(scope.row)"
:active-value = "1"//绑定数字0,1
:inactive-value = "0"
></el-switch>
</template>

brand-add-or-update.vue

<el-form-item label="显示状态" prop="showStatus">
<el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
//更新开关的状态
updateBrandStatus(data) {
console.log("最新状态", data);
let {brandId,showStatus} = data;
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
data: this.$http.adornData({brandId,showStatus}, false)
}).then(({ data }) => {

this.$message({
message: "状态更新成功",
type: "success"
});

});
},

4.3添加文件上传(分布式)采用阿里云对象存储

image-20240401211353297

image-20240401212320723

上传模型:防止密码泄露,缓解应用服务器压力

image-20200428184029655

查看阿里云关于文件上传的帮助: https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ

采用spring cloud alibaba的start依赖更为简单 详情:

https://github.com/alibaba/aliyun-spring-boot/blob/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample/README-zh.md

(1).gulimall-third-party服务引入依赖

<!--        阿里云对象存储-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-spring-boot-dependencies</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2).在gulimall-third-party服务oss.yml配置文件中配置 OSS 服务对应的 accessKey、secretKey 和 endpoint。

image-20240401233044265

3).引入服务注册和配置中心

pom.xml

<!--        阿里云对象存储-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<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>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<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>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-spring-boot-dependencies</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

bootstrap.properties:

spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=a192f48e-ee37-40f5-bdd5-07f60caedf8c

spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true

application.yml:

spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-third-party

server:
port: 30000

4)controller层代码

@RestController
public class OssController {

@Autowired
OSS ossClient;
@Value("${alibaba.cloud.oss.endpoint}")
private String endpoint;
@Value("${alibaba.cloud.oss.bucket}")
private String bucket;
@Value("${alibaba.cloud.access-key}")
private String accessId;

@RequestMapping("/oss/policy")
public R policy(){
// 填写Host名称,格式为https://bucketname.endpoint。
//https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2.jpg
String host = "https://"+bucket+"."+endpoint;
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
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);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
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));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return R.ok().put("data",respMap);
}
}

5)gateway网关配置路由

- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}

6)启动成功测试

image-20240402210916615

拿到对象存储服务端签名数据了

7)前端发请求到后端拿到签名数据后直接post上传文件给阿里云(存在cors问题)

上传组件:放置项目提供的upload文件夹到components目录下,一个是单文件上传,另外一个是多文件上传

PS D:\Project\gulimall\renren-fast-vue\src\components\upload> ls


目录: D:\Project\gulimall\renren-fast-vue\src\components\upload


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2020/4/29 星期三 12:0 3122 multiUpload.vue
2
-a---- 2019/11/11 星期一 21: 343 policy.js
20
-a---- 2020/4/29 星期三 12:0 3053 singleUpload.vue
1


PS D:\Project\gulimall\renren-fast-vue\src\components\upload>

修改这两个文件的配置后

开始执行上传,但是在上传过程中,出现了如下的问题:

image-20200429124629150

Access to XMLHttpRequest at 'http://gulimall-images.oss-cn-shanghai.aliyuncs.com/' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问 数据安全->跨域设置:

image-20240402214245512![image-

再次执行文件上传。

8)前端表单数据校验

使用的是form表单自定义校验

image-20240402225131728

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.4JSR303校验(@Valid注解)

​ 后端也要进行数据校验,因为我如果使用postman发请求就可以跳过前端校验,所以在后端最好也要进行校验。

步骤1:使用校验注解

在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。

在非空处理方式上提供了@NotNull,@Blank和@

(1)@NotNull:注解元素禁止为null,能够接收任何类型

(2)@NotEmpty:该注解修饰的字段不能为null或””

支持以下几种类型

CharSequence (length of character sequence is evaluated)

字符序列(字符序列长度的计算)

Collection (collection size is evaluated)
集合长度的计算

Map (map size is evaluated)
map长度的计算

Array (array length is evaluated)
数组长度的计算

(3)@NotBlank

The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.
该注解不能为null,并且至少包含一个非空白字符。接收字符序列。

步骤2:在请求方法种,使用校验注解@Valid,开启校验,

  @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);

return R.ok();
}

测试: http://localhost:88/api/product/brand/save

在postman种发送上面的请求

{
"timestamp": "2020-04-29T09:20:46.383+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}

能够看到”defaultMessage”: “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:

javax.validation.constraints.AssertFalse.message     = 只能为false
javax.validation.constraints.AssertTrue.message = 只能为true
javax.validation.constraints.DecimalMax.message = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message = 必须大于或等于{value}
javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message = 最大不能超过{value}
javax.validation.constraints.Min.message = 最小不能小于{value}
javax.validation.constraints.Negative.message = 必须是负数
javax.validation.constraints.NegativeOrZero.message = 必须是负数或零
javax.validation.constraints.NotBlank.message = 不能为空
javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.NotNull.message = 不能为null
javax.validation.constraints.Null.message = 必须为null
javax.validation.constraints.Past.message = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message = 必须是正数
javax.validation.constraints.PositiveOrZero.message = 必须是正数或零
javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间

org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message = 不能为空
org.hibernate.validator.constraints.NotEmpty.message = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间
org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message = 需要是一个合法的URL

org.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是

public @interface NotBlank {

String message() default "{javax.validation.constraints.NotBlank.message}";

可以在添加注解的时候,修改message:

@NotBlank(message = "品牌名必须非空")
private String name;

当再次发送请求时,得到的错误提示信息:

{
"timestamp": "2020-04-29T09:36:04.125+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "品牌名必须非空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}

但是这种返回的错误结果并不符合我们的业务需要。

步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。

public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
Map<String ,String> map = new HashMap<>();
if (result.hasErrors()){
result.getFieldErrors().forEach(item->{
//得到错误提示
String message = item.getDefaultMessage();
//得到错误的字段名
String field = item.getField();
map.put(field,message);
});
return R.error(400,"提交的数据不合法").put("data",map);
}else {
brandService.save(brand);
}
return R.ok();
}

image-20240402233045151

这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。

步骤4:统一异常处理

可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。也就是原save方法不变发生异常由异常处理类感知并捕获。

(1)抽取一个异常处理类

package com.bigdata.gulimall.product.exception;

import com.bigdata.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
* 集中处理所有异常
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
BindingResult result = e.getBindingResult();
Map<String ,String> errMap = new HashMap<>();
result.getFieldErrors().forEach(item->{
errMap.put(item.getField(),item.getDefaultMessage());
});
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",errMap);
}
}

(2)测试: http://localhost:88/api/product/brand/save

image-20200429183334783

(3)默认异常处理

@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg());
}

(4)错误状态码

上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

image-20200429183748249

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

package com.bigdata.common.exception;

/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeEnum {

UNKNOW_EXEPTION(10000,"系统未知异常"),

VALID_EXCEPTION( 10001,"参数格式校验失败");

private int code;
private String msg;

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

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

(5)测试: http://localhost:88/api/product/brand/save

image-20200429191830967

4.4. 分组校验功能(完成多场景的复杂校验@Validated注解)

1、在gulimall-common服务创建校验空接口

image-20240403001535814

2、给校验注解,标注上groups,指定什么情况下才需要进行校验

如:指定在更新和添加的时候,都需要进行校验

@NotEmpty(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups = {AddGroup.class,UpdateGroup.class})
private String logo;

在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

2、业务方法参数上使用@Validated注解

image-20240403001746868

@Validated的value方法:
指定一个或多个验证组以应用于此注释启动的验证步骤。

JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。

其他SmartValidator 实现也可以以其他方式支持类参数。

3、默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

4.5. 自定义校验

1、编写一个自定义的校验注解

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class})/*关联注解器-可以指定多个*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.bigdata.common.valid.ListValue.message}";

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

Class<? extends Payload>[] payload() default { };
// 可以指定数据只能是value数组指定的值
int[] value() default {};
}

2、编写一个自定义的校验器

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set=new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
int[] value = constraintAnnotation.value();
for (int i : value) {
set.add(i);
}

}

@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}

3、编写ValidationMessages.properties

com.atguigu.common.valid.ListValue.message=必须提交指定的值

4、关联自定义的校验器和自定义的校验注解

@Constraint(validatedBy = { ListValueConstraintValidator.class})

5、使用实例

/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(value = {0,1},groups ={AddGroup.class})
private Integer showStatus;

6、postman测试发现乱码(必须重启product服务,改了好多次发现必须重启后服务才能生效)

{
"msg": "参数格式校验失败",
"code": 10001,
"data": {
"logo": "不能为空",
"showStatus": "必须提交指定的值",
"sort": "不能为null",
"firstLetter": "不能为空"
}
}
setting -> file encoding

image-20240403225903690

然后添加gulimall-common的resource文件

image-20240403225958128

4.6品牌和分类的关联与级联更新

1.品牌和分类的关联

​ 比如小米品牌和分类的关系是多对多(小米不仅可以有手机还有平板,然后手机不仅有小米还有华为等等),那么就需要维护一张关系表pms_category_brand_relation。

/**
* 查询品牌关联的所有分类
* @param brandId
* @return
*/
@GetMapping("/catelog/list")
//@RequiresPermissions("product:categorybrandrelation:list")
public R categoylist(@RequestParam("brandId") Long brandId){//前端传id
List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
return R.ok().put("data", data);
}

@RequestMapping("/save")
//@RequiresPermissions("product:categorybrandrelation:save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);

return R.ok();
}
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
BrandEntity brandEntity = brandDao.selectById(brandId);
categoryBrandRelation.setBrandName(brandEntity.getName());
categoryBrandRelation.setCatelogName(categoryEntity.getName());
this.save(categoryBrandRelation);//将name也保存
}

2.级联更新

1.修改BrandController的Update

 @RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);

return R.ok();
}

2.新增brandserviceImpl方法

public void updateDetail(BrandEntity brand) {
//保证冗余字段的数据一致
this.updateById(brand);
if (!StringUtils.isEmpty(brand.getName())){
//同步更新其他表
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());

//TODO 更新其他关联
}
}

3.修改CategoryController的Update

 @RequestMapping("/update")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateCascade(category);

return R.ok();
}

4.新增CategoryServiceImpl的updateCascade方法

@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
if (!StringUtils.isEmpty(category.getName())){
//级联更新
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
}

5.新增CategoryBrandRelationServiceImpl修改品牌和修改分类方法

@Override
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
categoryBrandRelationEntity.setBrandId(brandId);
categoryBrandRelationEntity.setBrandName(name);
System.out.println("categoryBrandRelationEntity"+categoryBrandRelationEntity);
this.update(categoryBrandRelationEntity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}

@Override
public void updateCategory(Long catId, String name) {
this.baseMapper.updateCategory(catId,name);
}

6.效果:修改品牌名和分类名级联修改

image-20240405220805328

5.平台属性—属性分组

5.1. 点击子组件,父组件触发事件

现在想要实现点击菜单的左边,能够实现在右边展示数据

image-20200430215649355

父子组件传递数据:

1)子组件给父组件传递数据,事件机制;

在category中绑定node-click事件,

<el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeClick"	></el-tree>

2)子组件给父组件发送一个事件,携带上数据;

nodeClick(data,Node,component){
console.log("子组件",data,Node,component);
this.$emit("tree-node-click",data,Node,component);
},

this.$emit(事件名,”携带的数据”);

3)父组件中的获取发送的事件

<category @tree-node-click="treeNodeClick"></category>
//获取发送的事件数据
treeNodeClick(data,Node,component){
console.log("attgroup感知到的category的节点被点击",data,Node,component);
console.log("刚才被点击的菜单ID",data.catId);
},

image-20240404192909422

5.2分组查询

1.AttrGroupController修改新增为携带catelogId

@RequestMapping("/list/{catelogId}")
//@RequiresPermissions("product:attrgroup:list")
public R list(@RequestParam Map<String, Object> params, @PathVariable("catelogId") Long catelogId){
//PageUtils page = attrGroupService.queryPage(params);
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}

2.service层实现类添加方法

@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
if (catelogId == 0){ //如果分类等于0,查询出所有的数据,前端可以设置
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
new QueryWrapper<AttrGroupEntity>()
);
return new PageUtils(page);
}else {
//select * from pms_attr_group where catelog_id = ? and ()
String key = (String) params.get("key");
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id",catelogId);
if (!StringUtils.isBlank(key)){
wrapper.and((obj)->{ //可以按两个搜索条件查询
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wrapper);
return new PageUtils(page);
}
}

3.测试

image-20240404205537174

5.3分组新增和级联选择器

1.新增时如果想要出现级联效果attrgroup-add-or-update.vue

 <el-form-item label="所属分类id" prop="catelogId">
<!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input> -->
<el-cascader
v-model="dataForm.catelogId"
:options="categorys"
@change="handleChange"
:props="props"
></el-cascader>
</el-form-item>
return {
props:{
value:"catId",
label:"name",
children:"children"
},
categorys: []//存放分类获取到的数据
}
getCategorys(){
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.tree);
this.categorys = data.tree;
});
},

2.但是分类多出了一列,可知后端返回的children有空列。

image-20240404210041007

image-20240404210451638

3.修改后端代码加入注解@JsonInclude

/**
* 子分类
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)//只有当字段不为空的时候才响应给前端
@TableField(exist = false)
private List<CategoryEntity> children;

image-20240404210847327

可知children空数组没有响应给前端了

5.4分组修改和级联选择器回显

点击修改时所属分类不回显

原因:

image-20240404212041875

可知后端返回数据只有catelogId:225,所以不会回显。

解决:

在attrgroup-add-or-update.vue的 init(id)方法回显数据时查出catelgId完整路径

1.AttrGroupEntity添加catelogPath属性

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

2.AttrGroupController.java修改返回数据将全路径返回

 @RequestMapping("/info/{attrGroupId}")
//@RequiresPermissions("product:attrgroup:info")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
Long catelogId = attrGroup.getCatelogId();
Long[] path = categoryService.findCategoryPath(catelogId);
attrGroup.setCatelogPath(path);
return R.ok().put("attrGroup", attrGroup);
}

3.实现逻辑CategoryServiceImpl.java

@Override
public Long[] findCategoryPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
//翻转[2,25,225]
Collections.reverse(parentPath);
return (Long[]) parentPath.toArray(new Long[parentPath.size()]);
}
//逆序【225,25,2】
private List<Long> findParentPath(Long catelogId, List<Long> paths) {
paths.add(catelogId);
CategoryEntity category = this.getById(catelogId);
if (category.getParentCid() != 0){
findParentPath(category.getParentCid(),paths);
}
return paths;
}

4.前端init方法

this.dataForm.catelogPath = data.attrGroup.catelogPath;

6.平台属性—基本属性/规格参数和销售属性

规格参数新增时,请求的URL:Request URL:

http://localhost:88/api/product/attr/base/list/0?t=1588731762158&page=1&limit=10&key=

当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范

1588732021702

比较规范的做法是,新建一个vo文件夹,将每种不同的对象,按照它的功能进行了划分。在java中,涉及到了这几种类型

1588732152646

6.1新增属性

常量

public class ProductConstant {

public enum AttrEnum{
ATTR_TYPE_BASE(1,"基本属性"),ATTR_TYPE_SALE(0,"销售属性");

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

public int getCode() {
return code;
}

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

Request URL: http://localhost:88/api/product/attr/save,现在的情况是,它在保存的时候,只是保存了attr,并没有保存attrgroup,为了解决这个问题,我们新建了一个vo/AttrVo,在原AttrEntity基础上增加了attrGroupId字段,使得保存新增数据的时候,也保存了它们之间的关系。

通过” BeanUtils.copyProperties(attr,attrEntity);”能够实现在两个Bean之间拷贝数据,但是两个Bean的字段要相同

@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);
this.save(attrEntity);
}

AttrServiceImpl类

@Transactional
@Override
public void saveAttr(AttrVo attrVo) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attrVo,attrEntity);
//保存基本数据
this.save(attrEntity);
//再保存分组到关系表
if (attrVo.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attrVo.getAttrGroupId() != null){
//判断attrType是否等于1,如果不是1就不添加到关联表
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrId(attrEntity.getAttrId());
relationEntity.setAttrGroupId(attrVo.getAttrGroupId());
attrAttrgroupRelationService.save(relationEntity);
}
}

问题:现在有两个查询,一个是查询部分,另外一个是查询全部,但是又必须这样来做吗?还是有必要的,但是可以在后台进行设计,两种查询是根据catId是否为零进行区分的。

6.2查询属性(基本属性和销售属性)、分组和分类名字

查询前端响应会多出两个字段

@Data
public class AttrRespVo extends AttrVo{
/**
* 分类名字
*/
private String catelogName;

/**
* 分组名字
*/
private String groupName;
}

AttrController类

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

//修改后----------->
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String ,Object> params,
@PathVariable("catelogId") Long catelogId,
@PathVariable("attrType") String attrType){
PageUtils page = attrService.queryBasePage(params,catelogId,attrType);
return R.ok().put("page",page);
}

AttrServiceImpl类

public PageUtils queryBasePage(Map<String, Object> params, Long catelogId) {
//QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();
//如果base等于attrType就为1否则0,1就是基本属性,0是销售属性
QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(attrType)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
if (catelogId != 0){
wrapper.eq("catelog_id",catelogId);

}
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
wrapper.and((queryWrapper)->{
queryWrapper.eq("attr_id",key)
.or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
PageUtils pageUtils = new PageUtils(page);
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVoList = records.stream().map((attrEntity -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//设置分类和分组名字
//根据分类id查询分类名字
CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
String categoryName = categoryEntity.getName();
System.out.println("---->"+categoryEntity.getName());
attrRespVo.setCatelogName(categoryName);
}
//根据关系表先查组id
if ("base".equalsIgnoreCase(attrType)){
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if (relationEntity != null && relationEntity.getAttrGroupId() != null) {
//根据group_id查询组名字
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
return attrRespVo;
})).collect(Collectors.toList());
pageUtils.setList(respVoList);
return pageUtils;
}

image-20240406193003758

6.3修改

6.3.1.回显路径完整

在回显时要回显分类的完整路径如:【手机/手机通讯/手机】

GET请求:/product/attr/info/{attrId}

1.AttrRespVo类新增,完整路径属性

@Data
public class AttrRespVo extends AttrVo{
/**
* 分类名字
*/
private String catelogName;

/**
* 分组名字
*/
private String groupName;

/**
* 分类完整路径
*/
private Long[] catelogPath;
}

2.修改controller代码

@RequestMapping("/info/{attrId}")
//@RequiresPermissions("product:attr:info")
public R info(@PathVariable("attrId") Long attrId){
//AttrEntity attr = attrService.getById(attrId);
AttrRespVo attrRespVo = attrService.getAttrInfo(attrId);
return R.ok().put("attr", attrRespVo);
}

3.修改业务层代码

@Transactional
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo attrRespVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,attrRespVo);
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//设置分组信息
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if (relationEntity != null){
attrRespVo.setAttrGroupId(relationEntity.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
if (attrGroupEntity != null){
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
//设分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] categoryPath = categoryService.findCategoryPath(catelogId);
attrRespVo.setCatelogPath(categoryPath);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if (categoryEntity != null){
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}
6.3.2修改确认
@RequestMapping("/update")
//@RequiresPermissions("product:attr:update")
public R update(@RequestBody AttrVo attrVo){
//attrService.updateById(attr);
attrService.updateAttr(attrVo);
return R.ok();
}
@Transactional
@Override
public void updateAttr(AttrVo attrVo) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attrVo,attrEntity);
this.updateById(attrEntity);
if (attrVo.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//修改分组关联
AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity();
attrAttrgroupRelationEntity.setAttrGroupId(attrVo.getAttrGroupId());
attrAttrgroupRelationEntity.setAttrId(attrVo.getAttrId());
//如果attr_id不为空,修改为空新增
Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrVo.getAttrId()));
if (count > 0){
relationDao.update(attrAttrgroupRelationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrVo.getAttrId()));
}else {
relationDao.insert(attrAttrgroupRelationEntity);
}
}

}

7、平台属性—-查询分组关联属性和删除关联

image-20240406213822530

7.1查询分组关联属性

API:https://easydoc.net/s/78237135/ZUqEdvA4/LnjzZHPj

发送请求:/product/attrgroup/{attrgroupId}/attr/relation

如何查找:既然给出了attr_group_id,那么到中间表中查询出来所关联的attr_id,然后得到最终的所有属性即可。

可能出现null值的问题

AttrGroupController

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

AttrServiceImpl.java

/**
* 根据分组id获取规格参数
* @param attrgroupId
* @return
*/
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
List<AttrAttrgroupRelationEntity> relationEntities = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
List<Long> attrIds = relationEntities.stream().map(attrRelation -> {
return attrRelation.getAttrId();
}).collect(Collectors.toList());
if (attrIds == null || attrIds.size() == 0){
return null;
}
Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
return (List<AttrEntity>) attrEntities;
}

查询成功

image-20240406215824094

7.2删除关联

API:https://easydoc.net/s/78237135/ZUqEdvA4/qn7A2Fht

POST:/product/attrgroup/attr/relation/delete

@PostMapping("/attr/relation/delete")
public R deleteRelation(AttrGroupRelationVo[] vos){
attrService.deleteRelation(vos);
return R.ok();
}

service

/**
* 删除关联关系
* @param vos
*/
@Override
public void deleteRelation(AttrGroupRelationVo[] vos) {
List<AttrAttrgroupRelationEntity> entities = Arrays.stream(vos).map(item -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
relationDao.deleteBatchRelation(entities);
}

dao和mapper.xml

@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {

void deleteBatchRelation(@Param("entities") List<AttrAttrgroupRelationEntity> entities);
}
<delete id="deleteBatchRelation">
DELETE from pms_attr_attrgroup_relation WHERE
<foreach collection="entities" item="item" separator="OR">
(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId})
</foreach>
</delete>

7.3. 查询分组未关联的属性

/product/attrgroup/{attrgroupId}/noattr/relation

API:https://easydoc.xyz/doc/75716633/ZUqEdvA4/d3EezLdO

获取属性分组里面还没有关联的本分类里面的其他基本属性,方便添加新的关联

Request URL: http://localhost:88/api/product/attrgroup/1/noattr/relation?t=1588780783441&page=1&limit=10&key=

1588780868214

属性分组,对应于“pms_attr_group”表,每个分组下,需要查看到关联了哪些属性信息,销售属性不需要和分组进行关联,但是规格参数要和属性分组进行关联。

规格参数:对应于pms_attr表,attr_type=1,需要显示分组信息

销售属性:对应于pms_attr`表,attr_type=0,不需要显示分组信息

分组ID为9的分组:Request URL: http://localhost:88/api/product/attrgroup/9/noattr/relation?t=1588822258669&page=1&limit=10&key=

对应的数据库字段

attr_group_id attr_group_name sort descript icon catelog_id


        9  主体                    1  型号 平台                   wu               454
       10  显卡                    1  显存容量                    wu               454
       11  输入设备                  1  鼠标 键盘                   wu               454
       12  主板                    1  显卡类型 芯片组                wu               454
       13  规格                    1  尺寸                      wu               454

查询attrgroupId=9的属性分组:

AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);

获取到分类信息:

Long catelogId = attrGroupEntity.getCatelogId();

目标:获取属性分组没有关联的其他属性

也就是获取attrgroupId=9的属性分组中,关联的分类catelog_id =454 (台式机),其他基本属性

在该属性分组中,现在已经关联的属性:

1588822997675

本分类下,存在哪些基本属性?

没有关联的其他属性

已经关联的属性,这些属性是如何关联上的?

答:在创建规格参数的时候,已经设置了需要关联哪些属性分组。

想要知道还没有关联哪些,先查看关联了哪些,如何排除掉这些就是未关联的

在中间表中显示了属性和属性分组之间的关联关系,在属性表中显示了所有的属性,

先查询中间表,得到所有已经关联的属性的id,然后再次查询属性表,排除掉已经建立关联的属性ID,将剩下的属性ID和属性建立起关联关系

public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
//当前分组只能关联所属分类的id
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
Long catelogId = attrGroupEntity.getCatelogId();
//当前分组只能关联别的分组没有引用的属性
//当前分类下的分组
List<AttrGroupEntity> groups = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<Long> list = groups.stream().map(item -> {
return item.getAttrGroupId();
}).collect(Collectors.toList());
//这些分组关联的属性
List<AttrAttrgroupRelationEntity> groupId = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", list));
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.isEmpty(key)){
queryWrapper.and((w)->{

w.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);
return pageUtils;
}

7.4新增分组与属性关联

POST: /product/attrgroup/attr/relation

@PostMapping("/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVo> vos){
relationService.saveBatch(vos);
return R.ok();
}
public void saveBatch(List<AttrGroupRelationVo> vos) {
List<AttrAttrgroupRelationEntity> collect = vos.stream().map(item -> {
AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, attrAttrgroupRelationEntity);
return attrAttrgroupRelationEntity;
}).collect(Collectors.toList());
this.saveBatch(collect);
}

8. 发布商品

要走完以下所有流程才可以发布商品

image-20240407220254182

8.1.获取所有会员等级:

/member/memberlevel/list

API:https://easydoc.net/doc/75716633/ZUqEdvA4/jCFganpf

在“gulimall-gateway”中修改“”文件,添加对于member的路由

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

在“gulimall-member”中,创建“bootstrap.properties”文件,内容如下:

spring.cloud.nacos.config.name=gulimall-member
spring.cloud.nacos.config.server-addr=192.168.137.14:8848
spring.cloud.nacos.config.namespace=795521fa-77ef-411e-a8d8-0889fdfe6964
spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-member.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true

8.2获取分类关联的品牌:

/product/categorybrandrelation/brands/list

API:https://easydoc.net/doc/75716633/ZUqEdvA4/HgVjlzWV

​ 当我填写信息时没有显示分类对应的品牌,如下图:

image-20240407231109057

CategoryBrandRelationController类
/**
* 查询分类关联的所有品牌
* @param catId
* @return
*/
@GetMapping("/brands/list")
public R RelationBrandsList(@RequestParam("catId") Long catId){
List<BrandEntity> vos = categoryBrandRelationService.getBrandsByCatId(catId);
List<BrandVo> collect = vos.stream().map(item -> {
BrandVo brandVo = new BrandVo();
brandVo.setBrandId(item.getBrandId());
brandVo.setBrandName(item.getName());
return brandVo;
}).collect(Collectors.toList());
return R.ok().put("data",collect);
}
 @Autowired
@Lazy
BrandService brandService;

@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
List<CategoryBrandRelationEntity> catelogId = this.baseMapper.selectList(new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId));
List<BrandEntity> collect = catelogId.stream().map(item -> {
Long brandId = item.getBrandId();
BrandEntity brandEntity = brandService.getById(brandId);
return brandEntity;
}).collect(Collectors.toList());
return collect;
}

前端没有发送获取品牌的请求:

image-20240407234038268

network没有请求

解决:

  1. 首先安装pubsub-js
`npm install --save pubsub-js`
  1. 订阅方组件
`import PubSub from 'pubsub-js'`

该this.PubSub为PubSub。

main.js文件

image-20240407234727008

成功获取到了

image-20240407235311950

获取分类下所有分组&关联属性

请求类型:/product/attrgroup/{catelogId}/withattr

请求方式:GET

请求URL:http://localhost:88/api/product/attrgroup/225/withattr?t=1588864569478

mysql默认的隔离级别为读已提交,为了能够在调试过程中,获取到数据库中的数据信息,可以调整隔离级别为读未提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

但是它对于当前的事务窗口生效,如果想要设置全局的,需要加上global字段。

8.3新增商品

POST /product/spuinfo/save

参数:

{"spuName":"华为 HUAWEI Mate30 Pro","spuDescription":"华为 HUAWEI Mate30 Pro","catalogId":225,"brandId":2,"weight":0.198,"publishStatus":0,"decript":["https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//1c670d5a-5eed-4eb7-8573-ec4e475c8ed4_73366cc235d68202.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//77d839cc-5e61-45b6-86ff-ff857d869f16_528211b97272d88a.jpg"],"images":["https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//0232ea2e-d964-4ebb-864d-d0bab3f0da0d_1f15cdbcf9e1273c.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//c519d573-1daf-4e53-84ba-0271aba53040_2b1837c6c50add30.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//5cd361cd-d389-49f7-8250-8264d6a89a5a_8bf441260bffa42f.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//d487cd03-1d01-4056-89b0-9aaef8bf7179_28f296629cca865e.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//783d5381-2711-4fd7-8c6b-598eefb452bd_b5c6b23d01dcdf81.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//478aaf6e-65f8-4235-8ab3-7208a4f85853_919c850652e98031.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//53f4caf8-7cff-4696-89ea-225f4f1c6a0d_d511faab82abb34b.jpg","https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//3bd219db-002b-46bb-8cb8-c6d81c9877f1_a83bf5250e14caf2.jpg"],"bounds":{"buyBounds":500,"growBounds":500},"baseAttrs":[{"attrId":1,"attrValues":"2000","showDesc":0},{"attrId":6,"attrValues":"A2217","showDesc":0},{"attrId":3,"attrValues":"73.1mm","showDesc":0},{"attrId":7,"attrValues":"以官网信息为准;陶瓷","showDesc":0},{"attrId":8,"attrValues":"161.5mm","showDesc":0}],"skus":[{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"亮黑色"},{"attrId":5,"attrName":"选择版本","attrValue":"8+256g"}],"skuName":"华为 HUAWEI Mate30 Pro 亮黑色 8+256g","price":"6299","skuTitle":"华为 HUAWEI Mate30 Pro 亮黑色 8+256g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//5cd361cd-d389-49f7-8250-8264d6a89a5a_8bf441260bffa42f.jpg","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//d487cd03-1d01-4056-89b0-9aaef8bf7179_28f296629cca865e.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0}],"descar":["亮黑色","8+256g"],"fullCount":3,"discount":0.98,"countStatus":0,"fullPrice":10000,"reducePrice":50,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":6159}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"亮黑色"},{"attrId":5,"attrName":"选择版本","attrValue":"8+128g"}],"skuName":"华为 HUAWEI Mate30 Pro 亮黑色 8+128g","price":"5799","skuTitle":"华为 HUAWEI Mate30 Pro 亮黑色 8+128g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//5cd361cd-d389-49f7-8250-8264d6a89a5a_8bf441260bffa42f.jpg","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//d487cd03-1d01-4056-89b0-9aaef8bf7179_28f296629cca865e.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0}],"descar":["亮黑色","8+128g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"星河银"},{"attrId":5,"attrName":"选择版本","attrValue":"8+256g"}],"skuName":"华为 HUAWEI Mate30 Pro 星河银 8+256g","price":"6299","skuTitle":"华为 HUAWEI Mate30 Pro 星河银 8+256g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//0232ea2e-d964-4ebb-864d-d0bab3f0da0d_1f15cdbcf9e1273c.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//478aaf6e-65f8-4235-8ab3-7208a4f85853_919c850652e98031.jpg","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//53f4caf8-7cff-4696-89ea-225f4f1c6a0d_d511faab82abb34b.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0}],"descar":["星河银","8+256g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"星河银"},{"attrId":5,"attrName":"选择版本","attrValue":"8+128g"}],"skuName":"华为 HUAWEI Mate30 Pro 星河银 8+128g","price":"5799","skuTitle":"华为 HUAWEI Mate30 Pro 星河银 8+128g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//0232ea2e-d964-4ebb-864d-d0bab3f0da0d_1f15cdbcf9e1273c.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//478aaf6e-65f8-4235-8ab3-7208a4f85853_919c850652e98031.jpg","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//53f4caf8-7cff-4696-89ea-225f4f1c6a0d_d511faab82abb34b.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0}],"descar":["星河银","8+128g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"翡冷绿"},{"attrId":5,"attrName":"选择版本","attrValue":"8+256g"}],"skuName":"华为 HUAWEI Mate30 Pro 翡冷绿 8+256g","price":"6299","skuTitle":"华为 HUAWEI Mate30 Pro 翡冷绿 8+256g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//783d5381-2711-4fd7-8c6b-598eefb452bd_b5c6b23d01dcdf81.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//3bd219db-002b-46bb-8cb8-c6d81c9877f1_a83bf5250e14caf2.jpg","defaultImg":0}],"descar":["翡冷绿","8+256g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"翡冷绿"},{"attrId":5,"attrName":"选择版本","attrValue":"8+128g"}],"skuName":"华为 HUAWEI Mate30 Pro 翡冷绿 8+128g","price":"5799","skuTitle":"华为 HUAWEI Mate30 Pro 翡冷绿 8+128g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//783d5381-2711-4fd7-8c6b-598eefb452bd_b5c6b23d01dcdf81.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//3bd219db-002b-46bb-8cb8-c6d81c9877f1_a83bf5250e14caf2.jpg","defaultImg":0}],"descar":["翡冷绿","8+128g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"罗兰紫"},{"attrId":5,"attrName":"选择版本","attrValue":"8+256g"}],"skuName":"华为 HUAWEI Mate30 Pro 罗兰紫 8+256g","price":"6299","skuTitle":"华为 HUAWEI Mate30 Pro 罗兰紫 8+256g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//783d5381-2711-4fd7-8c6b-598eefb452bd_b5c6b23d01dcdf81.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//3bd219db-002b-46bb-8cb8-c6d81c9877f1_a83bf5250e14caf2.jpg","defaultImg":0}],"descar":["罗兰紫","8+256g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]},{"attr":[{"attrId":4,"attrName":"选择颜色","attrValue":"罗兰紫"},{"attrId":5,"attrName":"选择版本","attrValue":"8+128g"}],"skuName":"华为 HUAWEI Mate30 Pro 罗兰紫 8+128g","price":"5799","skuTitle":"华为 HUAWEI Mate30 Pro 罗兰紫 8+128g麒麟990 4G/5G全网通","skuSubtitle":"爆款热销 优选 速发","images":[{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//01da444d-4fc8-4745-8e6d-1bb736388c49_0d40c24b264aa511.jpg","defaultImg":1},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//783d5381-2711-4fd7-8c6b-598eefb452bd_b5c6b23d01dcdf81.jpg","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"","defaultImg":0},{"imgUrl":"https://gulimall20031201.oss-cn-chengdu.aliyuncs.com/2024-04-11//3bd219db-002b-46bb-8cb8-c6d81c9877f1_a83bf5250e14caf2.jpg","defaultImg":0}],"descar":["罗兰紫","8+128g"],"fullCount":0,"discount":0,"countStatus":0,"fullPrice":0,"reducePrice":0,"priceStatus":0,"memberPrice":[{"id":2,"name":"铜牌会员","price":0}]}]}

修改“com.atguigu.gulimall.product.controller.SpuInfoController”类:

@RequestMapping("/save")
//@RequiresPermissions("product:spuinfo:save")
public R save(@RequestBody SpuSaveVo vo){
spuInfoService.saveSpuInfo(vo);
return R.ok();
}

修改“com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl”类:

@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
//1.保存spu基本信息:pms_spu_info
SpuInfoEntity spuInfoEntity = new SpuInfoEntity();
BeanUtils.copyProperties(vo,spuInfoEntity);
spuInfoEntity.setCreateTime(new Date());
spuInfoEntity.setUpdateTime(new Date());
this.saveBaseSpuInfo(spuInfoEntity);

//2.保存spu的描述图片:pms_spu_info_desc
List<String> decript = vo.getDecript();
SpuInfoDescEntity descEntity = new SpuInfoDescEntity();
descEntity.setSpuId(spuInfoEntity.getId());
descEntity.setDecript(String.join(",",decript));
spuInfoDescService.saveSpuInfoDesc(descEntity);

//3.保存spu的图片集:pms_spu_images
List<String> images = vo.getImages();
spuImagesService.saveImages(spuInfoEntity.getId(),images);

//4.保存spu的规格参数:pms_product_attr_value
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity productAttrValueEntity = new ProductAttrValueEntity();
productAttrValueEntity.setAttrId(attr.getAttrId());
AttrEntity id = attrService.getById(attr.getAttrId());
productAttrValueEntity.setAttrName(id.getAttrName());
productAttrValueEntity.setAttrValue(attr.getAttrValues());
productAttrValueEntity.setQuickShow(attr.getShowDesc());
productAttrValueEntity.setSpuId(spuInfoEntity.getId());
return productAttrValueEntity;
}).collect(Collectors.toList());
productAttrValueService.saveProductAttr(collect);

//5.保存spu积分信息:gulimall_sms -> sms_spu_bounds
Bounds bounds = vo.getBounds();
SpuBoundsTo spuBoundsTo = new SpuBoundsTo();
BeanUtils.copyProperties(bounds,spuBoundsTo);
spuBoundsTo.setSpuId(spuInfoEntity.getId());
R r = couponFeignService.saveSpuBounds(spuBoundsTo);
if (r.getCode() != 0){
log.error("远程保存spu信息失败");
}

//5.保存当前spu对应的所有sku信息
List<Skus> skus = vo.getSkus();
if (skus != null && skus.size() > 0){
skus.forEach(item->{
String defaultImg = "";
for (Images img : item.getImages()) {
if (img.getDefaultImg() == 1){
defaultImg = img.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);

//5.1) sku的基本信息:pms_sku_info
skuInfoService.saveSkuInfo(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
List<SkuImagesEntity> imagesEntities = item.getImages().stream().map(img -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(img.getImgUrl());
skuImagesEntity.setDefaultImg(img.getDefaultImg());
return skuImagesEntity;
}).filter(entity->{
return !StringUtils.isEmpty(entity.getImgUrl());//图片路径不为空才返回
}).collect(Collectors.toList());

//5.2) sku的图片信息:pms_sku_images
skuImagesService.saveBatch(imagesEntities);

//5.3) sku的销售属性信息:pms_sku_sale_attr_value
List<Attr> attr = item.getAttr();
List<SkuSaleAttrValueEntity> saleAttrValueEntities = attr.stream().map(a -> {
SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity();
BeanUtils.copyProperties(a, attrValueEntity);
attrValueEntity.setSkuId(skuId);
return attrValueEntity;
}).collect(Collectors.toList());
skuSaleAttrValueService.saveBatch(saleAttrValueEntities);

//5.4) sku优惠、满减等信息gulimall_sms
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优惠信息失败");
}
}
});
}

}

保存spu基本信息 pms_spu_info

修改“com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl”类:

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

保存spu的描述图片 pms_spu_info_desc

修改“com.atguigu.gulimall.product.service.impl.SpuInfoDescServiceImpl”类:

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

保存spu的图片集 pms_spu_images

修改“com.atguigu.gulimall.product.service.impl.SpuImagesServiceImpl”类:

@Override
public void saveImages(Long id, List<String> images) {
if (images == null || images.size() == 0){

}else {
List<SpuImagesEntity> collect = images.stream().map(img -> {
SpuImagesEntity spuImagesEntity = new SpuImagesEntity();
spuImagesEntity.setSpuId(id);
spuImagesEntity.setImgUrl(img);
return spuImagesEntity;
}).collect(Collectors.toList());
this.saveBatch(collect);
}
}

保存spu的规格参数 pms_product_attr_value

修改“com.atguigu.gulimall.product.service.impl.ProductAttrValueServiceImpl”类:

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

保存spu的积分信息 gulimall_sms –> sms_spu_bounds

这里需要远程调用第三方服务
1、创建openFeign配置接口

2、在启动类GulimallProductApplication中加上@EnableFeignClients(basePackages = “com.atguigu.gulimall.product.feign”)

3、在gulimall-common添加服务与服务之间调用的to类“com.atguigu.common.to.SpuBoundTo”类:

每个服务间传递的数据可以封装到这个to内

@Data
public class SpuBoundTo {
private Long SpuId;
private BigDecimal buyBounds;
private BigDecimal growBounds;
}

修改“com.atguigu.gulimall.product.feign.CouponFeignService”类:

@FeignClient("gulimall-coupon")
public interface CouponFeignService {

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

@PostMapping("/coupon/skufullreduction/saveInfo")
R saveSkuReduction(@RequestBody SkuReductionTokuReductionTo);
}

com.atguigu.gulimall.coupon.controller.SpuBoundsController接口

/**
* 保存
*/
@PostMapping("/save")
public R save(@RequestBody SpuBoundsEntity spuBounds){
spuBoundsService.save(spuBounds);

return R.ok();
}

保存当前spu对应的所有sku信息
1)、sku的基本信息 pms_sku_info

修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

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

2)、sku的优惠、满减等信息 gulimall_sms -> sms_sku_ladder/sms_sku_full_reduction/sms_member_price

在gulimall-common添加服务与服务之间调用的to类“com.atguigu.common.to.MemberPrice”,“com.atguigu.common.to.SkuReductionTo”类,代码如下:

@Data
public class SkuReductionTo {

private Long skuId;
private int fullCount;
private BigDecimal discount;
private int countStatus;
private BigDecimal fullPrice;
private BigDecimal reducePrice;
private int priceStatus;
private List<MemberPrice> memberPrice;
}
@Data
public class MemberPrice {

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

}

然后我把product服务中的memberPrice删除了,不然会报空指针异常。

修改“com.atguigu.gulimall.coupon.service.impl.SkuFullReductionServiceImpl”类:

@Override
public void saveSkuReduction(SkuReductionTo reductionTo) {
System.out.println("---->"+reductionTo);
// sku优惠、满减等信息gulimall_sms
//sku_ladder
SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
skuLadderEntity.setSkuId(reductionTo.getSkuId());
skuLadderEntity.setFullCount(reductionTo.getFullCount());
skuLadderEntity.setDiscount(reductionTo.getDiscount());
skuLadderEntity.setAddOther(reductionTo.getCountStatus());
if(reductionTo.getFullCount() > 0){
skuLadderService.save(skuLadderEntity);
}

//满减
SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
BeanUtils.copyProperties(reductionTo,skuFullReductionEntity);
if (reductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
this.save(skuFullReductionEntity);
}

//会员价格
List<MemberPrice> memberPrice = reductionTo.getMemberPrice();
List<MemberPriceEntity> collect = memberPrice.stream().map(item -> {
MemberPriceEntity memberPriceEntity = new MemberPriceEntity();
memberPriceEntity.setSkuId(reductionTo.getSkuId());
memberPriceEntity.setMemberLevelId(item.getId());
memberPriceEntity.setMemberLevelName(item.getName());
memberPriceEntity.setMemberPrice(item.getPrice());
memberPriceEntity.setAddOther(1);
return memberPriceEntity;
}).filter(item->{
return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1;
}).collect(Collectors.toList());
memberPriceService.saveBatch(collect);
}

添加成功

image-20240411135142902

9、spu&sku检索

9.1spu检索

GET      /product/spuinfo/list

请求参数

{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为',//检索关键字
catelogId: 6,//三级分类id
brandId: 1,//品牌id
status: 0,//商品状态
}

分页数据

响应数据

{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{

"brandId": 0, //品牌id
"brandName": "品牌名字",
"catalogId": 0, //分类id
"catalogName": "分类名字",
"createTime": "2019-11-13T16:07:32.877Z", //创建时间
"id": 0, //商品id
"publishStatus": 0, //发布状态
"spuDescription": "string", //商品描述
"spuName": "string", //商品名字
"updateTime": "2019-11-13T16:07:32.877Z", //更新时间
"weight": 0 //重量

}]
}
}

修改“com.atguigu.gulimall.product.controller.SpuInfoController”类:

/**
* 列表
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = spuInfoService.queryPageByCondition(params);

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

修改“com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl”类:

@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
wrapper.and(w->{
w.eq("id",key).or().like("spu_name",key);
});
}
String status = (String) params.get("status");
System.out.println("====>"+status);
if (!StringUtils.isEmpty(status)){
wrapper.eq("publish_status",status);
}
String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(brandId) && !"0".equalsIgnoreCase(brandId)){
wrapper.eq("brand_id",brandId);
}
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)){
wrapper.eq("catalog_id",catelogId);
}
IPage<SpuInfoEntity> page = this.page(
new Query<SpuInfoEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}

image-20240411141404798

前端显示时间格式化一下,product服务中的application.yml配置指定格式化

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

image-20240411141648816

9.2sku检索

GET   /product/skuinfo/list

请求参数

{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为',//检索关键字
catelogId: 0,
brandId: 0,
min: 0,
max: 0
}

分页数据

响应数据

{
"msg": "success",
"code": 0,
"page": {
"totalCount": 26,
"pageSize": 10,
"totalPage": 3,
"currPage": 1,
"list": [{
"skuId": 1,
"spuId": 11,
"skuName": "华为 HUAWEI Mate 30 Pro 星河银 8GB+256GB",
"skuDesc": null,
"catalogId": 225,
"brandId": 9,
"skuDefaultImg": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-26/60e65a44-f943-4ed5-87c8-8cf90f403018_d511faab82abb34b.jpg",
"skuTitle": "华为 HUAWEI Mate 30 Pro 星河银 8GB+256GB麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄4G全网通手机",
"skuSubtitle": "【现货抢购!享白条12期免息!】麒麟990,OLED环幕屏双4000万徕卡电影四摄;Mate30系列享12期免息》",
"price": 6299.0000,
"saleCount": 0
}]
}
}

修改“com.atguigu.gulimall.product.controller.SkuInfoController”类,代码如下:

 @RequestMapping("/list")
    public R list(@RequestParam Map<String, Object> params){
        PageUtils page = skuInfoService.queryPageByCondition(params);
 
        return R.ok().put("page", page);
    }

修改“com.atguigu.gulimall.product.service.SkuInfoService”类,代码如下:

PageUtils queryPageByCondition(Map<String, Object> params);

修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

@Override
    public PageUtils queryPageByCondition(Map<String, Object> params) {
        QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
        
        String key = (String) params.get("key");
        if (!StringUtils.isEmpty(key)){
            queryWrapper.and((w)->{
                w.eq("sku_id",key).or().like("sku_name",key);
            });
        }
 
        String catelogId = (String) params.get("catelogId");
        if (!StringUtils.isEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)){
            queryWrapper.eq("catalog_id",catelogId);
        }
 
        String brandId = (String) params.get("brandId");
        if (!StringUtils.isEmpty(brandId) && !"0".equalsIgnoreCase(brandId)){
            queryWrapper.eq("brand_id",brandId);
        }
 
        String min = (String) params.get("min");
        if (!StringUtils.isEmpty(min)){
            queryWrapper.ge("price",min);
        }
 
        String max = (String) params.get("max");
        if (!StringUtils.isEmpty(max)){
            try{
                BigDecimal bigDecimal = new BigDecimal(max);
                if (bigDecimal.compareTo(new BigDecimal("0"))==1){
                    queryWrapper.le("price",max);
                }
            }catch (Exception e){
 
            }
        }
 
        IPage<SkuInfoEntity> page = this.page(
                new Query<SkuInfoEntity>().getPage(params),
                queryWrapper
        );
        return new PageUtils(page);
    }

五、仓储服务

1.仓库管理

1.1启动仓库服务&获取仓库列表

API : GET /ware/wareinfo/list

请求参数

{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为'//检索关键字
}

响应数据:

{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"id": 2,
"name": "aa",
"address": "bbb",
"areacode": "124"
}]
}
}

1.pom.xml

<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>

application.yml

spring:
datasource:
username: root
password: root
url: jdbc:mysql://8.138.99.194:3306/gulimall_wms?useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: localhost:8848
application:
name: gulimall-ware

mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
server:
port: 11000

logging:
level:
com.atguigu: debug

3.@EnableTransactionManagement启动类上加

4.gateway加

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

5.com.atguigu.gulimall.ware.controller.WareInfoController获取仓库列表

@RequestMapping("/list")
//@RequiresPermissions("ware:wareinfo:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = wareInfoService.queryPage(params);

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

6.com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl加入模糊查询

@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<WareInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
wrapper.and(w->{
w.eq("id",key).or().like("name",key).or().like("address",key)
.or().eq("areacode",key);
});
}
IPage<WareInfoEntity> page = this.page(
new Query<WareInfoEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}

1.2查询库存&查询采购需求

查询商品库存API:GET /ware/waresku/list

请求参数:

{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
wareId: 123,//仓库id
skuId: 123//商品id
}

响应参数:

{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"id": 1,
"skuId": 1,
"wareId": 1,
"stock": 1,
"skuName": "dd",
"stockLocked": 1
}]
}
}

1.com.atguigu.gulimall.ware.controller.WareSkuController

@RequestMapping("/list")
//@RequiresPermissions("ware:waresku:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = wareSkuService.queryPage(params);

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

2.com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl

@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>();
String skuId = (String) params.get("skuId");
if (!StringUtils.isEmpty(skuId)){
wrapper.eq("sku_id",skuId);
}
String wareId = (String) params.get("wareId");
if (!StringUtils.isEmpty(wareId)){
wrapper.eq("ware_id",wareId);
}
IPage<WareSkuEntity> page = this.page(
new Query<WareSkuEntity>().getPage(params),
wrapper
);

return new PageUtils(page);
}

查询采购需求

API: GET /ware/purchasedetail/list

请求参数

{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为',//检索关键字
status: 0,//状态
wareId: 1,//仓库id
}

响应数据

{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"id": 2,
"purchaseId": 1,
"skuId": 1,
"skuNum": 2,
"skuPrice": 22.0000,
"wareId": 1,
"status": 1
}]
}
}

1.com.atguigu.gulimall.ware.controller.PurchaseDetailController

@RequestMapping("/list")
//@RequiresPermissions("ware:purchasedetail:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = purchaseDetailService.queryPage(params);

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

2.com.atguigu.gulimall.ware.service.impl.PurchaseDetailServiceImpl

@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<PurchaseDetailEntity> queryWrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
queryWrapper.and(w->{
w.eq("purchase_id",key).or().eq("sku_id",key);
});
}
String status = (String) params.get("status");
if (!StringUtils.isEmpty(status)){
queryWrapper.eq("status",status);
}
String wareId = (String) params.get("wareId");
if (!StringUtils.isEmpty(wareId)){
queryWrapper.eq("ware_id",wareId);
}
IPage<PurchaseDetailEntity> page = this.page(
new Query<PurchaseDetailEntity>().getPage(params),
queryWrapper
);

return new PageUtils(page);
}

1.3合并采购需求

1.点击合并整单先发送查询未领取采购单需求

image-20240413175324767

GET /ware/purchase/unreceive/list

响应数据

{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"id": 1,
"assigneeId": 1,
"assigneeName": "aa",
"phone": "123",
"priority": 1,
"status": 1,
"wareId": 1,
"amount": 22.0000,
"createTime": "2019-12-12",
"updateTime": "2019-12-12"
}]
}
}
@RequestMapping("/unreceive/list")
//@RequiresPermissions("ware:purchase:list")
public R unreceiveList(@RequestParam Map<String, Object> params){
PageUtils page = purchaseService.queryPageUnreceivePurchase(params);
return R.ok().put("page", page);
}
@Override
public PageUtils queryPageUnreceivePurchase(Map<String, Object> params) {
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
new QueryWrapper<PurchaseEntity>().eq("status",0).or().eq("status",1)//0-新建 1 - 刚分配未领取
);

return new PageUtils(page);
}

可以查出采购单了

合并采购需求:

POST /ware/purchase/merge

请求参数

{
purchaseId: 1, //整单id
items:[1,2,3,4] //合并项集合
}

响应数据

{
"msg": "success",
"code": 0
}

1.定义MergeVo

@Data
public class MergeVo {
private Long purchaseId;
private List<Long> items;
}

2.编写采购单状态和采购需求状态枚举

public class WareConstant {
public enum PurchaseStatusEnum{
CREATED(0,"新建"),
ASSIGNED(1,"已分配"),
RECEIVE(2,"已领取"),
FINISH(3,"已完成"),
HASEERR(4,"有异常");
private int code;
private String msg;
PurchaseStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
public enum PurchaseDetailStatusEnum{
CREATED(0,"新建"),
ASSIGNED(1,"已分配"),
BUYING(2,"正在采购"),
FINISH(3,"已完成"),
HASEERR(4,"采购失败");
private int code;
private String msg;
PurchaseDetailStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}

3.controller层com.atguigu.gulimall.ware.controller.PurchaseController

@RequestMapping("merge")
//@RequiresPermissions("ware:purchase:list")
public R merge(@RequestBody MergeVo mergeVo){
purchaseService.mergePurchase(mergeVo);
return R.ok();
}

4.service层com.atguigu.gulimall.ware.service.impl.PurchaseServiceImpl

@Transactional
@Override
public void mergePurchase(MergeVo mergeVo) {
Long purchaseId = mergeVo.getPurchaseId();
if (purchaseId == null){
//新建一个采购单
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());
purchaseEntity.setCreateTime(new Date());
purchaseEntity.setUpdateTime(new Date());
this.save(purchaseEntity);
purchaseId = purchaseEntity.getId();
}
//合并
List<Long> items = mergeVo.getItems();
Long finalPurchaseId = purchaseId;
List<PurchaseDetailEntity> collect = items.stream().map(i -> {
PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
detailEntity.setPurchaseId(finalPurchaseId);
detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());
detailEntity.setId(i);
return detailEntity;
}).collect(Collectors.toList());
purchaseDetailService.updateBatchById(collect);
//更新采购单时间
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(purchaseId);
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}

5.测试

两个采购需求

image-20240413214210398

image-20240413214344034

image-20240413214451313

如果不选择采购单id就会新建采购单

image-20240413214521312

#格式化日期
jackson:
date-format: yyyy-MM-dd HH:mm:ss

1.4采购人员领取采购单

POST

/ware/purchase/received

请求参数

[1,2,3,4]//采购单id

分页数据

响应数据

{
"msg": "success",
"code": 0
}