SpringCloud - 商城基础篇

文章目录

SpringCloud商城- 基础篇

1. 环境搭建

1.1 centos7安装docker

安装官网:https://docs.docker.com/engine/install/centos/

# 卸载旧版本的docker
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
                  
sudo yum install -y yum-utils
sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo   
    
sudo yum install docker-ce docker-ce-cli containerd.io
# 启动docker
sudo systemctl start docker
# 设置开机自启动
sudo systemctl enable docker
# 查看docker安装版本
docker -v
# 查看docker下的容器
sudo docker images

# 配置docker阿里云镜像加速:https://cr.console.aliyun.com/cn-qingdao/instances/mirrors
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://chqac97z.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

1.2 docker安装MySQL5.7

# 下载MySQL5.7容器
sudo docker pull mysql:5.7

# 检查下载的容器
sudo docker images

# --name指定容器名字 -v目录挂载 -p指定端口映射  -e设置mysql参数(密码为root) -d后台运行
sudo docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7

# 查看运行中的容器
docker ps

# 配置MySQL
vi /mydata/mysql/conf/my.conf 

# 插入下面的内容
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve

# 重启MySQL
docker restart mysql

1.3 docker安装Redis

# 在虚拟机中
mkdir -p /mydata/redis/conf

touch /mydata/redis/conf/redis.conf

docker pull redis

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

# 进入redis客户端。
docker exec -it redis redis-cli

# 进入Redis的配置文件,使得数据持久化
vim /mydata/redis/conf/redis.conf

# 插入下面内容
appendonly yes

# 重启redis
docker restart redis

如果出现问题,可以删除容器和镜像重新安装:

# 停止启动的容器
docker stop CONTAINER ID
# 删除容器
docker rm CONTAINER ID
# 删除镜像
docker rmi IMAGE ID  

1.4 环境安装配置

① 配置maven

② 给idea装两个插件:lombok,MybatisX

③ 安装vscode,并安装插件

④ 配置git:

$ git config --global user.name "hengheng"
$ git config --global user.email "18751887307@163.co"
$ ssh-keygen -t rsa -C "18751887307@163.com"
# 配置码云上的ssh密钥
$ cat ~/.ssh/id_rsa.pub
# 测试是否配置成功
$ ssh -T git@gitee.com

⑤ 项目结构创建:

商品服务product、仓储服务ware、订单服务order、优惠券服务coupon、会员服务member

在这里插入图片描述

1.5 数据库初始化

sudo docker ps
sudo docker ps -a
# 这两个命令的差别就是后者会显示  【已创建但没有启动的容器】

# 我们接下来设置我们要用的容器每次都是自动启动
sudo docker update redis --restart=always
sudo docker update mysql --restart=always
# 如果不配置上面的内容的话,我们也可以选择手动启动
sudo docker start mysql
sudo docker start redis
# 如果要进入已启动的容器
sudo docker exec -it mysql /bin/bash

创建数据库及数据库表:

gulimall-oms、gulimall-pms、gulimall-sms、gulimall-ums、gulimall-wms

在这里插入图片描述

2. 快速开发

2.1 人人开源

① 在码云上搜索人人开源,我们使用renren-fast,renren-fast-vue项目,启动renren-fast访问localhost:8080

https://gitee.com/renrenio,将这两个项目克隆下来,然后删除.git文件后放入后端项目gulimall中

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

② 配置前端工程:

下载nodejs并配置npm镜像:

node -v
npm config set registry http://registry.npm.taobao.org/

在VSCode的终端运行:

npm install

如果运行报错:npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。右击vscode,选择一管理员身份运行,即可解决问题。

安装失败,按照下面方式重新安装,安装成功:

# 先清除缓存
npm rebuild node-sass
npm uninstall node-sass

然后在VS Code中删除node_modules

# 执行
npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

# 如果上面没有报错,就继续执行
npm install 

# 运行项目
npm run dev 

访问http://localhost:8001/#/login,输入admin和admin即可登录后台管理系统,实现前后端联调。

2.2 逆向工程生成5个微服务crud代码

将代码生成器项目克隆下来,然后删除.git文件,并放到项目工程下

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

在application.yml中配置MySQL,先生成gulimall_pms的项目代码:

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

④ 测试类:加入@RunWith(SpringRunner.class)注解,否则@Autowired注解无法完成属性注入

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = GulimallProductApplication.class)
public class GulimallProductApplicationTests {
    @Autowired
    private BrandService brandService;
}

⑤ 插入数据库中的数据中文乱码,只需要在product项目的application.yml中配置编码即可

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.38.22:3306/gulimall_pms?useUnicode=true&characterEncoding= utf-8
    driver-class-name: com.mysql.jdbc.Driver

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

⑥ 使用逆向工程生成所有微服务项目代码,修改renren-generator项目的配置文件:

修改generator.properties:

mainPath=com.atguigu
package=com.atguigu.gulimall
# 将模块名称修改为coupon
moduleName=coupon
author=hengheng
email=hengheng@gmail.com
# 将表头修改为sms_
tablePrefix=sms_

修改application.yml:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    # 将数据库修改为gulimall_sms
    url: jdbc:mysql://192.168.38.22:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

3. SpringCloud Alibaba

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

在gulimall-common的项目中引入坐标:

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

3.1 Nacos 注册中心

下面以coupon服务为例,将gulimall-coupon服务注册进nacos:

① 在gulimall-common模块中引入 Nacos Discovery Starter,这样每个服务中都会引入相应的坐标

 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 </dependency>

② 在gulimall-coupon服务的配置文件中配置 Nacos Server 地址:

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

③ 在gulimall-coupon服务中使用 @EnableDiscoveryClient 注解开启服务注册与发现功能:

@SpringBootApplication
@EnableDiscoveryClient
public class GulimallCouponApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallCouponApplication.class, args);
    }
}

④ 启动项目,访问http://localhost:8848/nacos/

在这里插入图片描述

同样,将gulimall-member服务也注册进注册中心:

在这里插入图片描述

3.2 Openfeign 远程调用

需求:gulimall-member会员服务远程调用gulimall-coupon会员服务

① 在member会员服务中引入openfeign坐标 :

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

② 在优惠卷gulimall-coupon中编写一个请求,让gulimall-member会员服务通过openfeign远程调用这个接口:

@RequestMapping("coupon/coupon")
public class CouponController {
    @Autowired
    private CouponService couponService;

    @RequestMapping("/member/list")
    public R membercoupons(){
        CouponEntity couponEntity = new CouponEntity();
        couponEntity.setCouponName("满100减10");
        return R.ok().put("coupons",Arrays.asList(couponEntity));
    }
}

③ 在gulimall-member服务中编写一个接口,用来调用远程服务业务:

//指定要调用的注册中心的服务的服务名
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
    //这个就是要调用的gulimall-coupon中的接口方法
    @RequestMapping("/coupon/coupon/member/list")
    public R membercoupons();
}

④ 主启动类开启远程服务调用功能:

//开启远程调用功能,服务启动时会自动扫描带有@FeignClient注解的方法
//每个方法中指定了被调用的远程服务的方法
@EnableFeignClients("com.atguigu.gulimall.member.feign")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallMemberApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallMemberApplication.class, args);
    }
}

⑤ gulimall-member中编写一个请求方法实现远程调用:

@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    CouponFeignService couponFeignService;
    
    @RequestMapping("/coupons")
    public R test(){
        MemberEntity memberEntity = new MemberEntity();
        memberEntity.setNickname("张三");
        //调用couponFeignService接口中的方法
        R membercoupons = couponFeignService.membercoupons();
        return R.ok().put("member",memberEntity).put("coupons",membercoupons.get("coupons"));
    }
}

⑥ 报错,巨坑:因为我创建服务的时候没有勾选openfeign,导致引入依赖后相关注解不识别,后来导入openfeign坐标时指明版本号,但是启动项目后访问报错500,后面通过这样解决了:

在pom文件中添加:

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

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

⑦ 测试,访问http://localhost:8000/member/member/coupons

在这里插入图片描述

3.3 Nacos配置中心

https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md

下面以coupon服务为例:

① 首先,在gulimall-common服务中引入 Nacos Config Starter:

 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
 </dependency>

② 在gulimall-coupon服务模块的bootstrap.properties中配置数据:

# 指代服务名称
spring.application.name=gulimall-coupon
# 指定配置中心服务器地址,nacos服务器就是一个配置中心
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

③ 创建一个配置列表,Data ID默认为:spring.application.name.properties(spring.application.name.yml)

在这里插入图片描述

④ 从Nacos Config 中获取相应的配置,这里我们使用 @Value 注解来将对应的配置注入到Controller 的 userName 和 age 字段,并添加 @RefreshScope 打开动态刷新功能。

@RefreshScope
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
    @Autowired
    private CouponService couponService;

    @Value("${coupon.user.name}")
    private String name;
    @Value("${coupon.user.age}")
    private Integer age;

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

⑤ 测试,访问http://localhost:7000/coupon/coupon/test

3.4 Nacos配置中心-命名空间与配置分组

① Data ID相同,命名空间不同,启动服务访问http://localhost:7000/coupon/coupon/test,默认加载public命名空间的配置:

在这里插入图片描述

如果想要加载dev命名空间的配置,只需要在bootstrap.properties中添加配置:

spring.cloud.nacos.config.namespace=b2add807-4c38-43f0-be83-d5ee4fcaea57

② 也可以为每一个微服务创建一个命名空间,这样启动服务时就会只加载该命名空间的配置文件

在这里插入图片描述

将该命名空间配置在bootstrap.properties文件中:

spring.cloud.nacos.config.namespace=8c2dbbf4-986a-4485-a1b2-06bb3fa2472f

这样以后这个服务启动的时候就会加载coupon命名空间下的配置。

③ Data ID相同,group不同:

在这里插入图片描述

在bootstrap.properties中配置group:

spring.cloud.nacos.config.group=dev

3.5 Nacos配置中心-加载多配置集

需求:将application.yml配置文件中的内容都放在配置中心的配置文件中,进热代替application.yml文件

① 将与数据源有关的配置放到coupon命名空间的配置中:

在这里插入图片描述

② 将于mybatis相关的配置放到coupon命名空间的配置中:
在这里插入图片描述

③ 将其他相关配置放到coupon命名空间的配置中:

在这里插入图片描述

④ 在bootstrap.properties中配置要加载的配置文件:

spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=8c2dbbf4-986a-4485-a1b2-06bb3fa2472f
spring.cloud.nacos.config.group=test

spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true

spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true

spring.cloud.nacos.config.ext-config[2].data-id=other.yml
spring.cloud.nacos.config.ext-config[2].group=dev
# 动态刷新,默认为false
spring.cloud.nacos.config.ext-config[2].refresh=true

3.6 GataWay网关

发送请求需要知道商品服务的地址,如果商品服务器有123服务器,1号掉线后,还得改,所以需要网关动态地管理,他能从注册中心中实时地感知某个服务上线还是下线。请求也要加上询问权限,看用户有没有权限访问这个请求,也需要网关。

https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/

① 创建gulimall-gateway项目,同时添加pom文件:

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

② 编写注册中心的配置文件application.properties:

spring.application.name=gulimall-gateway
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=88

③ 编写配置中心的配置文件bootstrap.properties:

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=fc4f6402-947a-4363-a088-a84576024445

在这里插入图片描述

④ 主配置类中引入注册发现注解@EnableDiscoveryClient,用于发现其他服务:

@EnableDiscoveryClient
//因为引入了mybatis坐标,而又不需要数据库,所以需要排除,否则启动报错
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallGatewayApplication.class, args);
    }
}

⑤ 编写application.yml,来完成需求:

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: https://www.baidu.com/
          predicates:
            - Query=url,baidu  # 访问url=baidu,就会路由到百度网页
        - id: qq_route
          uri: https://www.qq.com/
          predicates:
            - Query=url,qq     # 访问url=qq,就会路由到qq网页

访问:http://localhost:88/hello?url=qq

4. 商品服务 - 三级分类

4.1 Nacos配置中心和注册中心

① 在注册中心中“product”命名空间中,创建“gulimall-product.yml”配置文件:

② 将“application.yml”内容拷贝到该配置文件中:

server:
  port: 10000
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.38.22:3306/gulimall_pms?useUnicode=true&characterEncoding= utf-8
    driver-class-name: com.mysql.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-product

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

③ 在本地创建“bootstrap.properties”文件,指明配置中心的位置和使用到的配置文件:

spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=80fa1f00-1d72-49e0-bd03-fdcc12cda9e8
spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-product.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true

④ 启动gulimall-product,查看到该服务已经出现在了nacos的注册中心中了

4.2 递归树形结构获取数据 (业务)

①在CategoryController 中编写请求入口方法:

/**
  * 查询出所有分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
    List<CategoryEntity> entities = categoryService.listWithTree();
    return R.ok().put("data", entities);
}

② 在CategoryServiceImpl中编写方法递归查找所有分类,以树形结构组装起来:一级分类ParentCid=0,一级分类的下的二级分类满足categoryEntity.getParentCid() == root.getCatId()

@Override
public List<CategoryEntity> listWithTree() {
    //1、查出所有分类
    List<CategoryEntity> entities = baseMapper.selectList(null);
    //2、组装成父子的树形结构
    //2.1)、找到所有的一级分类
    List<CategoryEntity> level1Menus = entities.stream().filter(
        //一级分类的parentId=0,根据这个条件构建出一级分类的数据
        categoryEntity -> categoryEntity.getParentCid() == 0
    ).map((menu)->{
        menu.setChildren(getChildrens(menu,entities));
        return menu;
    }).sorted((menu1,menu2)->{
        return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
    }).collect(Collectors.toList());
    return level1Menus;
}

//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
    List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
        return categoryEntity.getParentCid() == root.getCatId();
    }).map(categoryEntity -> {
        //1、找到子菜单
        categoryEntity.setChildren(getChildrens(categoryEntity,all));
        return categoryEntity;
    }).sorted((menu1,menu2)->{
        //2、菜单的排序
        return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
    }).collect(Collectors.toList());
    return children;
}

③ 访问:http://localhost:10000/product/category/list/tree

4.3 配置网关路由与路径重写

1、 前端-分析

① 后端启动renren-fast项目,右键以管理员身份运行vscode,打开前端renren-fast-vue项目,在终端运行前端项目:npm run dev

② 进入人人后台管理系统,创建一级菜单:

在这里插入图片描述

③ 给商品系统添加一个子菜单,点击新增,选择菜单:

④ 创建renren-fast-vue\src\views\modules\product目录,子所以是这样来创建,是因为product/category,对应于product-category,在该目录下,新建“category.vue”文件 ,编写一个方法:

methods: {
    //写一个方法
    getMenus() {
        //发送请求,最终请求的是 http://localhost:88/api/product/category/list/tree
        //http://localhost:88/api是index.js中配置的baseurl 
        this.$http({
            url: this.$http.adornUrl("/product/category/list/tree"),
            method: "get",
        }).then(data => {
            console.log("成功获取到菜单数据...", data);
            // this.menus = data.data;
        })
    },
}

⑤ 刷新人人项目页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”,这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree
在这里插入图片描述

需要修改 static\config\index.js文件中的api接口请求地址,改成给网关发请求,让网关路由到指定的地址:

//修改前为:
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
//修改后为:
// api接口请求地址,改成给网关发请求,让网关路由到指定的地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

2、后端-配置网关路由

http://localhost:88,这个地址是我们网关微服务的接口,我们需要通过网关来完成路径的映射,因此将renren-fast注册到nacos注册中心中,并添加配置中心:

① 在renren-fast项目下的pom文件中导入gulimall-common坐标依赖:

<dependency>
   <groupId>com.atguigu.gulimall</groupId>
   <artifactId>gulimall-common</artifactId>
   <version>0.0.1-SNAPSHOT</version>
</dependency>

② 在renren-fast项目下的application.yml中配置服务名称和注册中心地址,将这个服务注册进nacos:

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

③ 在renren-fast项目下的bootstrap.properties文件中配置服务名称和配置中心nacos的地址:

spring.application.name=renren-fast
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=0ac4b668-86be-4fd1-b4b0-66bcc7007873

③ 在renren-fast项目的主启动类上添加@EnableDiscoveryClient注解,开启服务注册与发现:

@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class RenrenApplication {
	public static void main(String[] args) {
		SpringApplication.run(RenrenApplication.class, args);
	}
}

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

- id: admin_route
  uri: lb://renren-fast  # 负载均衡到renren-fast服务
  predicates:
    - Path=/api/**       # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务

⑤ 启动renren-fast项目,访问http://localhost:8001/#/login,发现验证码报错:

分析原因:

现在验证码请求路径为:http://localhost:88/api/captcha.jpg?uuid=?

原始验证码请求路径为:http://localhost:8080/renren-fast/captcha.jpg?uuid=?

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

3、后端-路径重写

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#rewritelocationresponseheader-gatewayfilter-factory

# 官网示例
spring:
  cloud:
    gateway:
      routes:
      - id: rewritepath_route
        uri: https://example.org
        predicates:
        - Path=/foo/**
        filters:
        # 意思是将/red/xxx 重写为 /xxx
        - RewritePath=/red(?<segment>/?.*), $\{segment}

因此修改gulimall-gateway服务下的application.yml文件中“admin_route”路由规则:

- id: admin_route
  uri: lb://renren-fast  # 负载均衡到renren-fast服务
  predicates:
    - Path=/api/**       # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务
  filters:
    # 意思是将路径 /api/xxx 重写为 /renren-fast/xxx
    - RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
    # http://localhost:88/api/captcha.jpg?uuid=?
    # http://localhost:8080/renren-fast/captcha.jpg?uuid=?

再次访问:http://localhost:8001/#/login,验证码能够正常的加载了。

但是填写验证码准备登陆时,出现了403状态码, 原因:CORS 头缺少 Access-Control-Allow-Origin,这是一种跨域问题,访问的域名和端口和原来的请求不同,请求就会被限制,因此需要解决跨域问题。
在这里插入图片描述

4.4 网关统一配置跨域

① 跨域:

在这里插入图片描述

② 跨域流程:

在这里插入图片描述

③ 解决跨域方法1:使用ngnix部署为同一域

④ 解决跨域方法2:配置当次请求允许跨域,添加请求头:

在这里插入图片描述

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

@Configuration
public class GulimallCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);
        
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}

⑤ 重启项目,填写验证码访问http://localhost:8001/#/login,报错:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)

在这里插入图片描述

因为renren-fast项目中也配置了跨域,因此出现了多个跨域,需要把renren-fast中配置的去掉,即去掉该项目下的io.renren.config.CorsConfig类。

⑥ 重启renren-fast项目,访问http://localhost:8001/#/login登录成功,进入分类维护菜单,打开f12,发现出现404错误,即请求的http://localhost:88/api/product/category/list/tree不存在

在这里插入图片描述

因为我们之前定义的网关映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree,但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。

需要在网关gulimall-gateway的application.yml文件中,定义一个gulimall-product服务的路径重写:

- id: product_route
  uri: lb://gulimall-product
  predicates:
    - Path=/api/product/** # 只要请求为/api/product/**,就会路由到gulimall-product服务下
  filters:
    # 意思是将路径 /api/xxx 重写为/xxx
    - RewritePath=/api/(?<segment>/?.*),/$\{segment}

访问:http://localhost:88/api/product/category/list/tree,出现{"msg":"invalid token","code":401}

因为/api/product/**路由规则会拦截/api/** 的路由规则,因此在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,调整路由规则顺序:

- id: product_route
  uri: lb://gulimall-product
  predicates:
    - Path=/api/product/** # 只要请求为/api/product/**,就会路由到gulimall-product服务下
  filters:
    # 意思是将路径 /api/xxx 重写为/xxx
    - RewritePath=/api/(?<segment>/?.*),/$\{segment}

- id: admin_route
  uri: lb://renren-fast  # 负载均衡到renren-fast服务
  predicates:
    - Path=/api/**       # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务
  filters:
    # 意思是将路径 /api/xxx 重写为 /renren-fast/xxx
    - RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}

再次访问http://localhost:88/api/product/category/list/tree,即可得到所有分类数据:

4.5 逻辑删除

1、分析:

① 在前端页面category.vue文件中给三级分类添加Append和Delete按钮:

<template>
  <!-- 参考ElementUI中的Tree树形控件 -->
  <!-- 里面的冒号代表v-bind -->
  <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick" :expand-on-click-node="false" show-checkbox node-key="catId">
    <span class="custom-tree-node" slot-scope="{ node, data }">
      <span>{{ node.label }}</span>
      <span>
        <!-- 三级节点不可以追加节点,只有一级和二级节点可以 -->
        <el-button  v-if="node.level<=2" type="text" size="mini" @click="() => append(data)">Append</el-button>
        <!-- 只有当前节点没有子节点才可以删除该节点-->
        <el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">Delete</el-button>
      </span>
    </span>
  </el-tree>
</template>

在这里插入图片描述

② 测试代码生成工具生成的com.bigdata.gulimall.product.controller.CategoryController类中delete()方法:

/**
   * 删除
   * @RequestBody:获取请求体,必须发送post请求
   * SpringMvc自动将请求体的数据(json),转换为对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
    //传入的是数组
    categoryService.removeByIds(Arrays.asList(catIds));
    return R.ok();
}

使用postman,模拟前端发送请求localhost:88/api/product/category/delete,删除catid=1432的那行数据:

在这里插入图片描述

③ 业务上删除菜单的时候,不能直接删除,需要检查当前删除的菜单是否被别的地方引用,修改delete()方法:

@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  1、检查当前删除的菜单,是否被别的地方引用
    //逻辑删除
    baseMapper.deleteBatchIds(asList);
}

④ 我们并不希望将数据库中的数据删除,而是标记它被删除了,这就是逻辑删除,可以设置show_status为0,标记它已经被删除:

在这里插入图片描述

2、后端-逻辑删除:

参考Mybatis-plus官方文档:https://baomidou.com/guide/logic-delete.html

① 在gulimall-product服务的application.yml文件中配置逻辑删除的规则:

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

② 实体类字段上加上@TableLogic注解:

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

③ 另外在gulimall-product服务的application.yml文件中,设置日志级别,方便打印出SQL语句:

logging:
  level:
    com.atguigu.gulimall: debug

④ 在postman模拟前端发送请求localhost:88/api/product/category/delete,删除catid=1431的数据,检查数据库发现show_status=0;

在这里插入图片描述

⑤ 前端-删除菜单:

点击Delete删除菜单:

  • 参考ElementUI中的Tree树形控件,在templeate标签中添加<el-tree>

  • 在script标签中添加remove()方法,同时参考ElementUI中的 Message 消息提示组件和 MessageBox 弹框组件

⑥ 前端-新增菜单:

点击Append新增菜单:

  • 参考ElementUI中的Dialog 对话框组件,在templeate标签中添加<el-dialog>

  • 在script标签中添加append (data) 方法和addCategory ()方法

4.6 前端-修改

1、前端-基本修改效果:

点击edit修改菜单:

① 修改<el-dialog>,添加一个按钮edit

② 修改<el-dialog>,添加绑定的数据

③ 在script标签中添加edit(data)方法,submitData () 方法,editCategory ()方法,并对append (data) 方法和addCategory ()方法进行修改

④ 后端修改package com.atguigu.gulimall.product.controller.CategoryController()类中的方法:

@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId){
    CategoryEntity category = categoryService.getById(catId);
    return R.ok().put("data", category);
}

2、 前端-拖拽效果:

通过拖拽节点改变节点顺序以及父子关系:

① 参考ElementUI中的Tree 树形控件,找到可拖拽节点,给<el-tree>添加属性draggable

② 在script标签中编写 allowDrop ()方法和 countNodeLevel ()方法

3、前端-拖拽数据收集并保存:

拖动菜单时需要修改顺序和级别,收集拖拽后节点发生变化的节点数据:

① 参考ElementUI中的Tree 树形控件,找到可拖拽节点,给<el-tree>添加@node-drop=“handleDrop”

② 在script标签中编写 handleDrop ()方法和updateChildNodeLevel ()方法,将收集的节点数据放进 updateNodes: []

③ 在script标签中编写batchSave ()方法将 updateNodes: []中的数据发送给服务器

④ 后端编写方法接收数据并插入数据库

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

4、前端-批量删除:

① 在template中新增批量删除按钮

② 在script标签中编写batchDelete ()方法

5. 商品服务 - 品牌管理

5.1 逆向生成的前端代码

1、完成逆向工程前端页面展示

① 在renren-fast-vue项目的菜单管理里中新增一个菜单:

在这里插入图片描述

② 将之前逆向工程生成的gulimall\gulimall-product\src\main\resources\src\views\modules\product项目下的brand-add-or-update.vuebrand.vue文件拷贝到前端项目renren-fast-vue/src/views/modules/product中,然后在终端重新执行npm run dev重新运行项目,打开菜单栏的品牌管理,发现没有新增和删除功能:

在这里插入图片描述

③ 这是因为权限控制的原因,将src\utils\index.js中的isAuth()方法永远返回true即可

/**
 * 是否有权限
 */
export function isAuth (key) {
  return true;
}

测试新增和删除一个品牌,均正确

在这里插入图片描述

2、显示状态按钮

① 找到ElementUI的Table表格组件中的自定义列模板和ElementUI的Switch 开关組件,在brand.vue中修改显示状态,将显示状态设置为自定义的开关:

在这里插入图片描述

<el-table-column prop="showStatus" header-align="center" align="center" label="显示状态">
    <template slot-scope="scope">
        <el-switch v-model="scope.row.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
    </template>
</el-table-column>

② 点击新增时也能够将显示状态设置为开关:

可以看到在brand.vue文件中引入了一个组件brand-add-or-update.vue,注意addOrUpdateHandle (id) 方法:

<script>
//从外部导入的功能    
import AddOrUpdate from './brand-add-or-update'
export default {
  data () {
    return {
      addOrUpdateVisible: false
    }
  },
  components: {
    AddOrUpdate
  },
  methods: {
    // 新增 / 修改
    addOrUpdateHandle (id) {
      this.addOrUpdateVisible = true
      this.$nextTick(() => {
        this.$refs.addOrUpdate.init(id)
      })
    },
  }
}
</script>

点击新增就会触发addOrUpdateHandle()方法:

<el-button v-if="isAuth('product:brand:save')" type="primary"
           @click="addOrUpdateHandle()">新增</el-button>

addOrUpdateHandle()方法中会将this.addOrUpdateVisible = true,会显示add-or-update组件:

<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" 	                                            @refreshDataList="getDataList"></add-or-update>

所以我们要到brand-add-or-update.vue中修改显示状态将它设置为开关:

<el-form-item label="显示状态" prop="showStatus">
    <template slot-scope="scope">
        <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
    </template>
</el-form-item>

在这里插入图片描述

③ 点击开关修改显示状态,并改变数据库中show_status的值(数据库中显示为1,不显示为0),而ElementUI的开关组件默认是显示为true,不显示为false,因此需要给开关组件的active-value和inactive-value属性绑定值,让其显示为1,不显示为0,同时还需要编写一个@change="updateBrandStatus(scope.row)"方法,将数据库中的show_status的值做相应更改:

<el-switch
           v-model="scope.row.showStatus"
           active-color="#13ce66"
           inactive-color="#ff4949"
           :active-value="1"
           :inactive-value="0"
           @change="updateBrandStatus(scope.row)"></el-switch>
updateBrandStatus (data) {
    console.log("最新信息", data)
    let { brandId, showStatus } = data
    //发送请求修改数据库中显示状态
    this.$http({
        url: this.$http.adornUrl("/product/brand/update"),
        method: "post",
        data: this.$http.adornData({ brandId, showStatus }, false)
    }).then(({ data }) => {
        this.$message({
            type: "success",
            message: "状态更新成功"
        })
    })
},

5.2 文件上传云存储品牌logo

5.2.1 云存储开通与使用

需求:点击新增后显示新增品牌,我们希望将品牌logo进行文件上传,和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。

① 进入https://www.aliyun.com/,选择产品,存储,对象存储OSS,立即开通

在这里插入图片描述

② 使用支付宝登录后完成实名认证,然后再开通对象存储OSS,管理控制台创建Bucket:

在这里插入图片描述

③ 找到创建的bucket,点击文件上传,上传一张图片,然后点击详情,复制url即可在浏览器访问呢图片:

在这里插入图片描述

④ 这种方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云对象存储, 文件上传方式:

在这里插入图片描述

5.2.2 OSS整合测试

参考:https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.920.15b85cb1mYuz5t

方式1:原生方式整合OSS:

① 在Maven工程中使用OSS Java SDK,只需在pom.xml中加入相应依赖即可,在gulimall-product服务中:

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.10.2</version>
</dependency>

② 在gulimall-product服务中编写一个测试类,上传文件流:

endpoint的取值和accessKeyId和accessKeySecret的取值需要从阿里云的对象存储OSS中获取

@Test
public void test() throws FileNotFoundException {
    // Endpoint以杭州为例,https://oss.console.aliyun.com/bucket/oss-cn-hangzhou/gulimall-hengheng/overview
    String endpoint = "oss-cn-hangzhou.aliyuncs.com";
    // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,https://ram.console.aliyun.com/users/gulimall/
    String accessKeyId = "LTAI4G8B8As46B7xkMxaXRi8";
    String accessKeySecret = "HYgldN3cSkvXnycsprCyGCYUPj76yr";
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    // 上传文件流。
    InputStream inputStream = new FileInputStream("C:\\Users\\18751\\Desktop\\谷粒商城-微服务架构图.jpg");
    ossClient.putObject("gulimall-hengheng", "谷粒商城-微服务架构图.jpg", inputStream);
    // 关闭OSSClient。
    ossClient.shutdown();
    System.out.println("上传成功");
}

③ 测试成功:

在这里插入图片描述

方式2:SpringCloud Alibaba-OSS

https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample

① 在gulimall-common服务的pom文件中导入依赖(注意不是官网的配置,因为官网配置不识别):

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>

② 在gulimall-product服务的application.yml文件中配置access-key和secret-key

spring:
  cloud:
    alicloud:
      access-key: LTAI4G8B8As46B7xkMxaXRi8
      secret-key: HYgldN3cSkvXnycsprCyGCYUPj76yr
      oss:
        endpoint: oss-cn-hangzhou.aliyuncs.com

③ 测试:

@Test
public void test() throws FileNotFoundException {
    // 上传文件流。
    InputStream inputStream = new FileInputStream("C:\\Users\\18751\\Desktop\\2.jpg");
    ossClient.putObject("gulimall-hengheng", "2.jpg", inputStream);
    // 关闭OSSClient。
    ossClient.shutdown();
    System.out.println("上传成功");
}

在这里插入图片描述

5.2.3 创建gulimall-third-party服务集成OSS服务

① 创建gulimall-third-party服务专门用来集成第三方服务,修改pom文件,将OSS这个第三方SDK不再放入gulimall-common中而是放入gulimall-third-party服务中:

<dependencies>
    <dependency>
        <groupId>com.atguigu.gulimall</groupId>
        <artifactId>gulimall-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <exclusions>
            <exclusion>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

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

② 编写bootstrap.properties文件,配置nacos的配置中心地址,同时加载oss.yml中配置:

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=gulimall-third-party
spring.cloud.nacos.config.namespace=cc589d72-014d-4db3-8b5a-b7b7238c25bb

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

创建一个命名空间third-party:

在这里插入图片描述

在该命名空间下新增一个配置,当gulimall-third-party服务启动的时候就会加载该配置文件:

在这里插入图片描述

③ 编写application.yml文件,将gulimall-third-party服务放入注册中心 :

spring:
  application:
    name: gulimall-third-party
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alicloud:
      access-key: LTAI4G8B8As46B7xkMxaXRi8
      secret-key: HYgldN3cSkvXnycsprCyGCYUPj76yr
      oss:
        endpoint: oss-cn-hangzhou.aliyuncs.com
        bucket: gulimall-hengheng
server:
  port: 30000

④ 在gulimall-third-party服务的主启动类上加上注解@EnableDiscoveryClient,开启服务的注册与发现:

@EnableDiscoveryClient
@SpringBootApplication
public class GulimallThirdPartyApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallThirdPartyApplication.class, args);
    }
}

启动报错,修改pom文件:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <!--版本更改为2.1.18-->
    <version>2.1.18.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>1.8</java.version>
    <!--版本更改为Greenwich.SR3-->
    <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>

⑤ 测试文件上传:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = GulimallThirdPartyApplication.class)
public class GulimallThirdPartyApplicationTests {
    @Autowired
    private OSSClient ossClient;

    @Test
    public void test() throws FileNotFoundException {
        // 上传文件流。
        InputStream inputStream = new FileInputStream("C:\\Users\\18751\\Desktop\\3.jpg");
        ossClient.putObject("gulimall-hengheng", "3.jpg", inputStream);
        // 关闭OSSClient。
        ossClient.shutdown();
        System.out.println("上传成功");
    }
}

在这里插入图片描述

5.2.4 OSS获取服务端签名

官网:https://help.aliyun.com/document_detail/31926.html?spm=a2c4g.11186623.6.1718.24962061SagJUn

采用JavaScript客户端直接签名时,AccessKey ID和AcessKey Secret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。

在这里插入图片描述

① 在gulimall-third-party服务中编写com.bigdata.gulimall.thirdparty.controller.OssController类:

@RestController
public class OssController {
    @Autowired
    private OSS ossClient;
    //从配置文件中获取值
    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;
    @Value("${spring.cloud.alicloud.oss.bucket}")
    private String bucket;
    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;

    @RequestMapping("/oss/policy")
    public Map<String,String> policy() {
        // host的格式为 bucketname.endpoint
        String host = "https://" + bucket + "." + endpoint; 
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        // 用户上传文件时指定的前缀。
        String dir = format + "/"; 
        Map<String, String> respMap = null;
        
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return respMap;
    }
}

② 访问http://localhost:30000/oss/policy,可以得到签名数据:

{"accessid":"LTAI4G8B8As46B7xkMxaXRi8","policy":"eyJleHBpcmF0aW9uIjoiMjAyMC0xMi0wOFQwNzoyMDo1My4wNTVaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIwLTEyLTA4LyJdXX0=","signature":"vYHsKqyQ3rs4swMV+RYDN3+lEBs=","dir":"2020-12-08/","host":"https://gulimall-hello.oss-cn-hangzhou.aliyuncs.com","expire":"1607412053"}

③ 在gulimall-gateway中配置网关路由, 上传文件路径为:http://localhost:88/api/thirdparty/oss/policy

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

5.2.5 OSS前后端联调测试上传

① 将项目提供的upload文件下放在renren-fast-vue\src\components目录下,然后等待两个文件的文件上传地址,改为阿里云提供的 Bucket 域名:http://gulimall-hengheng.oss-cn-hangzhou.aliyuncs.com

② 在brand-add-or-update.vue文件中使用singleUpload.vue这个单文件上传组件,如何使用?

首先,需要在brand-add-or-update.vue文件中导入外部组件singleUpload.vueL:

//导入外部组件
import SingleUpload from "@/components/upload/singleUpload";

其次,在components中指明这个vue组件中需要用到哪些组件,指明后就可以在vue中使用了:

export default {
    components: {SingleUpload},
}

最后,在vue组件中使用这个组件:

<el-form-item label="品牌logo地址" prop="logo">
    <!-- 属性名要和components指明的组件名称相同,SingleUpload或single-upload -->
    <single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>

③ 修改后端com.bigdata.gulimall.thirdparty.controller.OssController类的返回值:

@RestController
public class OssController {
    @Autowired
    private OSS ossClient;
    //从配置文件中获取值
    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;
    @Value("${spring.cloud.alicloud.oss.bucket}")
    private String bucket;
    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;

    @RequestMapping("/oss/policy")
    public R policy() {
        String host = "https://" + bucket + "." + endpoint;  
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        String dir = format + "/";  
        Map<String, String> respMap = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
        } catch (Exception e) {
        System.out.println(e.getMessage());
    }
//        return respMap;
        return R.ok().put("data",respMap);
    }
}

④ 在页面点击新增,然后文件上传品牌logo,但是出现了如下的问题:

在这里插入图片描述

这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:概览–》跨域访问–》创建规则

在这里插入图片描述

⑤ 解决完成后,重新进行文件上传,即可:

5.3 表单校验

5.3.1 前端表单校验

① 显示图片:

点击新增,新增一个品牌:

在这里插入图片描述

但是显示的是一个图片链接,不是图片:

在这里插入图片描述

在brand.vue中动态绑定图片地址:

<el-table-column prop="logo" header-align="center" align="center" label="品牌logo地址">
    <template slot-scope="scope">
        <img :src="scope.row.logo" style="width: 100px; height: 80px" />
    </template>
</el-table-column>

在这里插入图片描述

② 前端表单校验:

参考:https://element.eleme.cn/#/zh-CN/component/form,ElementUI的form表单组件:

在brand-add-or-update.vue中添加:

<el-form-item label="排序" prop="sort">
    <!--将sort字段绑定一个数字-->
    <el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
dataRule: {
        name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
        logo: [{ required: true, message: "品牌logo地址不能为空", trigger: "blur" }],
        descript: [{ required: true, message: "介绍不能为空", trigger: "blur" }],
        showStatus: [
          {
            required: true,
            message: "显示状态[0-不显示;1-显示]不能为空",
            trigger: "blur",
          },
        ],
        firstLetter: [
          {
            validator: (rule, value, callback) => {
              if (value == "") {
                callback(new Error("首字母必须填写"))
              } else if (!/^[a-zA-Z]$/.test(value)) {
                callback(new Error("首字母必须a-z或者A-Z之间"))
              } else {
                callback()
              }
            },
            trigger: "blur"
          }
        ],
        sort: [
          {
            validator: (rule, value, callback) => {
              if (value == "") {
                callback(new Error("排序字段必须填写"))
              } else if (!Number.isInteger(value) || value < 0) {
                callback(new Error("排序必须是一个大于等于0的整数"))
              } else {
                callback()
              }
            },
            trigger: "blur"
          }
 },

在这里插入图片描述

5.3.2 JSR303后端数据校验

参考:javax.validation.constraints包

① 给Bean添加校验注解,并添加自己的message提示:

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	@NotNull(message = "修改必须指定品牌id")
	@Null(message = "新增不能指定id")
	@TableId
	private Long brandId;

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

    //该注解不能为null,并且至少包含一个非空字符。 
	@NotBlank
	@URL(message = "logo必须是一个合法的url地址")
	private String logo;

	private String descript;
	private Integer showStatus;

	@NotEmpty
	@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母")
	private String firstLetter;

	@NotNull
	@Min(value = 0,message = "排序必须大于等于0")
	private Integer sort;
}

② 开启校验功能@Valid,接收校验出错的结果BingResult:

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

③ 使用postman测试:http://localhost:88/api/product/brand/save

在这里插入图片描述

5.3.3 统一异常处理

上一节中对于参数校验发生的异常,我们使用了 BindingResult result这个变量来接收,但是这样做太复杂,因为参数校验的实体类很多,我们需要在每个Controller层的相应方法中加上参数校验并接收异常响应结果,因此只需要做统一异常处理即可,即将Controller层中所有的异常都抛出去,然后统一处理Controller层的异常。

① 在com.atguigu.gulimall.product.exception包下新建一个统一异常处理类:

@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
    //如果能够精确匹配到该异常就会执行这个方法,否则执行下面的方法
    @ExceptionHandler(value= MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e){
        log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
        BindingResult bindingResult = e.getBindingResult();

        Map<String,String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        //不再自己指定响应状态码和状态,而是封装一个枚举类
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        log.error("错误:",throwable);
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
}

②在package com.atguigu.common.exception包下封装一个枚举类,定义各种响应状态码和响应消息:

public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败");

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

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

5.3.4 JSR303分组数据校验

新增品牌和修改品牌的某些字段的注解校验规则不一样时,可以分组校验,校验注解只有在指定的分组下才生效,而且如果开启了分组校验注解功能,那些没有指定分组的校验注解就会不生效。

① 在com.atguigu.common.valid包下定义两个接口,不用写具体实现:

public interface UpdateGroup {
}
public interface AddGroup {
}

② 开启分组校验注解功能,在方法上使用@Validated({AddGroup.class})注解指明校验字段所属的分组类:

/**
   * 保存/新增
   */
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
    brandService.save(brand);
    return R.ok();
}

/**
   * 修改
   */
@RequestMapping("/update")
public R update(@Validated(UpdateGroup.class)@RequestBody BrandEntity brand){
    brandService.updateById(brand);
    return R.ok();
}

③ 给校验注解标注什么情况需要进行校验,默认没有指定分组的校验注解,在开启了分组校验的情况下不生效。

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
   private static final long serialVersionUID = 1L;

   @NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
   @Null(message = "新增不能指定id",groups = {AddGroup.class})
   @TableId
   private Long brandId;

   @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class, UpdateGroup.class})
   private String name;

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

   private String descript;
   private Integer showStatus;

   @NotEmpty(groups={AddGroup.class})
   @Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
   private String firstLetter;

   @NotNull(groups={AddGroup.class})
   @Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
   private Integer sort;
}

5.3.5 JSR303自定义数据校验

在gulimall-common服务中,com.atguigu.common.valid包下:

① 自定义一个校验注解:

@Documented
//关联自定义的校验器
@Constraint(validatedBy = { ListValueConstraintValidator.class })
//注解可以放在哪里
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    //校验注解发生异常的时候,提示信息该配置文件中获取
    String message() default "{com.atguigu.common.valid.ListValue.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    int[] vals() default { };
}

ValidationMessages.properties文件:

com.atguigu.common.valid.ListValue.message=must be special num

② 编写一个自定义校验器ConstraintValidator:

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();
    
    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    //判断是否校验成功
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        //判断set集合中是否包含这个值,如果不包含就报错
        return set.contains(value);
    }
}

③ 给showStatus字段添加自定义校验注解并指明分组:

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

④ @Validated(UpdateStatusGroup.class)开启注解校验功能:

@RequestMapping("/update/status")
public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
    brandService.updateById(brand);
    return R.ok();
}

⑤ 测试:

访问新增方法:http://localhost:88/api/product/brand/save

在这里插入图片描述

访问修改方法:http://localhost:88/api/product/brand/update/status

在这里插入图片描述

⑥ 测试前后端联调,前端点击新增和修改一个品牌:
在这里插入图片描述

6. 商品服务-属性分组

6.1 前端组件抽取与父子组件

1、 前端组件抽取:

接口文档地址:https://easydoc.xyz/s/78237135/ZUqEdvA4/

将项目提供的sys_menus.sql文件,放在gulimall_admin数据库的sys_menu表中执行,然后刷新页面

① 因为三级分类这个组件,我们在其他地方也有用到,因此将其抽取到一个公共组件中,新建src\views\modules\common\category.vue

② 在src\views\modules\product\attrgroup.vue中导入公共组件,并使用:

import Category from "../common/category";
export default {
  components: {
    Category,
  },
}

③ 在vue实例中使用:

<el-col :span="6"><category></category></el-col>

2、父子组件:

① 在category.vue组件的<el-tree>标签中绑定一个事件 @node-click:

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

nodeclick(data, node, component)方法,在该方法内向父组件发送事件,从而可以在父组件中使用:

nodeclick(data, node, component) {
    console.log("子组件category的节点被点击", data, node, component);
    //向父组件发送事件;
    this.$emit("tree-node-click", data, node, component);
}

③ 在attrgroup.vue中使用这个组件:

 <el-col :span="6"><category @tree-node-click="treenodeclick"></category></el-col>
//感知树节点被点击
treenodeclick(data, node, component) {
    console.log("attrgroup感知到category的节点被攻击:",data,node,component);
    console.log("刚才被点解的菜单id:",data.catId);
},

6.2 获取分类属性分组

需要用到 mybatis plus强大的条件构造器queryWrapper、updateWrapper:

在这里插入图片描述

查看前端API接口文档开发:https://easydoc.xyz/s/78237135/ZUqEdvA4/OXTgKobR

① 根据前端API文档可知,前端会发送一个get请求:/product/attrgroup/list/{catelogId},其中请求参数为:

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

② 在com.atguigu.gulimall.product.controller.AttrGroupController类中定义一个方法,接收前端请求参数,请求路径为/product/attrgroup/list/{catelogId},请求方式为get请求:

@RestController
@RequestMapping("product/attrgroup")
public class AttrGroupController {
    @Autowired
    private AttrGroupService attrGroupService;

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

③ 在com.atguigu.gulimall.product.service.impl.AttrGroupServiceImpl 类中编写业务逻辑:

@Service("attrGroupService")
public class AttrGroupServiceImpl extends ServiceImpl<AttrGroupDao, AttrGroupEntity> implements AttrGroupService {

    @Override
    public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
        if( catelogId == 0){
            IPage<AttrGroupEntity> page = this.page(
                    //分页条件
                    new Query<AttrGroupEntity>().getPage(params),
                    //查询条件:无
                    new QueryWrapper<AttrGroupEntity>()
            );
            return new PageUtils(page);
        }else {
            String key = (String) params.get("key");
            //select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
            //QueryWrapper:构造查询条件
            QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
            //查询条件1:数据库字段catelog_id的值与前端传入的参数catelogId相同,即catelog_id=catelogId
            wrapper.eq("catelog_id",catelogId);
            //查询条件2:字段 attr_group_id==key 或  字段 attr_group_name like %key%
            if(!StringUtils.isEmpty(key)){
                wrapper.and((obj)->{
                    obj.eq("attr_group_id",key).or().like("attr_group_name",key);
                });
            }
            //根据分页条件params和查询条件wrapper进行查询
            IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
            return new PageUtils(page);
        }
    }
}

④ 使用postman测试:http://localhost:88/api/product/attrgroup/list/1?page=1&key=aa

在这里插入图片描述

⑤ 前后端联调:

//感知树节点被点击
treenodeclick(data, node, component) {
    //如果是三级节点,就显示属性分组
    if (node.level == 3) {
        this.catId = data.catId;
        this.getDataList(); //重新查询
    }
},

在这里插入图片描述

6.3 分组新增与修改

1、 分组新增

需求:点击新增属性分组的时候,填写所属分类时能够选择所属三级分类

在这里插入图片描述

① 在attrgroup-add-or-update.vue文件中,加入elementui的级联选择器:

<el-form-item label="所属分类id" prop="catelogId">
    <el-cascader v-model="dataForm.catelogIds" :options="categorys" :props="props">
    </el-cascader>
</el-form-item>

② 向后台请求所有分类数据:

getCategorys () {
    this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get"
    }).then(({ data }) => {
        this.categorys = data.data
    })
},

③ 在vue实例一创建的时候,就执行上面的方法:

created () {
    this.getCategorys()
}

④ 数据data:

data () {
    return {
        props: {
            value: "catId",
            label: "name",
            children: "children",
        },
        categorys: [],
        visible: false,
        //提交给数据库的数据
        dataForm: {
            attrGroupId: 0,
            attrGroupName: '',
            sort: '',
            descript: '',
            icon: '',
            catelogIds: [],
            catelogId: 0,
        },
    },

2、 分组修改

需求:点击修改,回显分类

在这里插入图片描述

① attrgroup-add-or-update.vue文件,在init()方法中,将catelogId的完整路径回显:

init (id) {
    this.dataForm.attrGroupId = id || 0
    this.visible = true
    this.$nextTick(() => {
        this.$refs['dataForm'].resetFields()
        if (this.dataForm.attrGroupId) {
            this.$http({
                url: this.$http.adornUrl(`/product/attrgroup/info/${this.dataForm.attrGroupId}`),
                method: 'get',
                params: this.$http.adornParams()
            //从数据库中查询出的数据data,将catelogId的完整路径回显出来
            }).then(({ data }) => {
                if (data && data.code === 0) {
                    this.dataForm.attrGroupName = data.attrGroup.attrGroupName
                    this.dataForm.sort = data.attrGroup.sort
                    this.dataForm.descript = data.attrGroup.descript
                    this.dataForm.icon = data.attrGroup.icon
                    this.dataForm.catelogId = data.attrGroup.catelogId
                    //查出catelogId的完整路径
                    this.dataForm.catelogPath = data.attrGroup.catelogPath
                }
            })
        }
    })
},

② 后端将catelogId的完整路径查询出来并响应给前端data:

首先,在AttrGroupEntity类中添加一个字段,然后将attrgroup响应给前端:

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

在AttrGroupController类中编写一个方法,接受前端请求参数,调用Service逻辑,相应数据:

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

③ 在CategoryServiceImpl类中递归收集三级菜单id:

//[2,25,225]
@Override
public Long[] findCatelogPath(Long catelogId) {
    List<Long> paths = new ArrayList<>();
    List<Long> parentPath = findParentPath(catelogId, paths);

    Collections.reverse(parentPath);
    return parentPath.toArray(new Long[parentPath.size()]);
}

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

④ 测试:点击修改,所属分类就会回显三级分类数据

在这里插入图片描述

6.4 品牌分类关联

6.4.1 数据准备

① 将分类维护的原始数据更新,将项目提供的pms_catelog.sql在pms_category表中执行

② 在品牌管理中,统计的分页数据是错的,因此引入mybatis-plus的分页插件:

在这里插入图片描述

在com.atguigu.gulimall.product.config包下,配置分页插件即可:

@Configuration
@EnableTransactionManagement //开启事务
@MapperScan("com.atguigu.gulimall.product.dao")
public class MyBatisConfig {
    //引入分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
         paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInterceptor.setLimit(1000);
        return paginationInterceptor;
    }
}

③ 开启品牌的模糊查询功能:

在这里插入图片描述

在BrandServiceImpl类中修改queryPage()方法,增加模糊查询条件

@Override
public PageUtils queryPage(Map<String, Object> params) {
    QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
    String key = (String)params.get("key");
    if(!StringUtils.isEmpty(key)){
        //brand_id=key 或者 name like %key%
        queryWrapper.eq("brand_id",key).or().like("name",key);
    }
    IPage<BrandEntity> page = this.page(
        new Query<BrandEntity>().getPage(params),queryWrapper);
    return new PageUtils(page);
}

在这里插入图片描述

④ 给品牌管理增加一些有用数据,并将原来数据删除,同时将项目提供的前端代码中modules文件夹下的common和product文件夹替换掉renren-fast-vue\src\views\modules下的common和product文件夹。

在这里插入图片描述

⑤ 关联分类功能:一个品牌(华为)会关联分类(手机,电视),一个分类(手机)会关联多个品牌(华为,小米),这样就属于多对多的关系,在数据库中一般就会有中间表:

在这里插入图片描述

6.4.2 获取品牌关联的分类

参考前端接口API地址:https://easydoc.xyz/s/78237135/ZUqEdvA4/SxysgcEF

① 请求地址为:/product/categorybrandrelation/catelog/list,请求方式为get,请求参数为brandId

② 获取当前品牌关联的所有分类列表:

 /**
  * 获取当前品牌关联的所有分类列表
  */
@GetMapping("/catelog/list")
 public R list(@RequestParam("brandId")Long brandId){
    List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(
            new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
    return R.ok().put("page", data);
 }

6.4.3 新增品牌与分类关联关系

参考前端接口API地址:https://easydoc.xyz/s/78237135/ZUqEdvA4/7jWJki5e

① 请求地址为:product/categorybrandrelation/save,请求方式post,请求参数:{“brandId”:1,“catelogId”:2}

② 新增品牌与分类关联关系:

pms_category_brand_relation表:

在这里插入图片描述

前端传来的参数包括brand_id和catelog_id,但是不包括brand_name和catelog_name,当新增关联时,数据库就不会保存brand_name和catelog_name,因此需要将他们查询出来,再保存数据到数据库。

在CategoryBrandRelationController类中:

@RequestMapping("/save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
    categoryBrandRelationService.saveDetail(categoryBrandRelation);
    return R.ok();
}

在CategoryBrandRelationServiceImpl类中:

@Override
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
    Long brandId = categoryBrandRelation.getBrandId();
    Long catelogId = categoryBrandRelation.getCatelogId();

    //根据brandId和catelogId查询出详情
    BrandEntity brandEntity = brandDao.selectById(brandId);
    CategoryEntity categoryEntity = categoryDao.selectById(catelogId);

    categoryBrandRelation.setBrandName(brandEntity.getName());
    categoryBrandRelation.setCatelogName(categoryEntity.getName());

    this.save(categoryBrandRelation);
}

③ 测试:

在这里插入图片描述

数据库中数据:

在这里插入图片描述

6.4.5 品牌级联更新

需求:当品牌管理中的品牌名和分类名修改的时候,关联分类中的品牌名和分类名也要跟着修改。

① 在BrandController类中修改update()方法:

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

② 在BrandServiceImpl类中 :

@Override
public void updateDetail(BrandEntity brand) {
    //保障冗余字段的数据一致
    //更新品牌管理
    this.updateById(brand);
    if(!StringUtils.isEmpty(brand.getName())){
        //同步更新其他关联数据
        categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
    }
}

③ 在CategoryBrandRelationServiceImpl类中:

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

6.4.6 分类级联更新

① 在CategoryController类中修改update()方法:

@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
    categoryService.updateCascade(category);
    return R.ok();
}

② 在CategoryServiceImpl类中:

@Override
public void updateCascade(CategoryEntity category) {
    this.updateById(category);
    //更新关联分类:
    categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

③ 在CategoryBrandRelationServiceImpl类中:

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

7. 商品服务 - 平台属性

7.1 规则参数

7.1.1 需求分析

① 进入平台属性—》规格参数—》新增

在这里插入图片描述

数据库保存的数据,就是新增的属性数据:

在这里插入图片描述

② 进入平台属性—》属性分组—》关联:

在这里插入图片描述

可以看到属性分组和属性进行了相关联:

在这里插入图片描述

③ 表属性分组与属性的关联表 :

在这里插入图片描述

问题来了:当我们新增了属性的时候,属性并没有和属性分组关联起来,导致关联表数据为空,因此需要重写新增属性的方法。

同时页面提交的数据还多了一个attrGroupId字段,可以使用VO对象来封装,来接受页面提交的数据。

7.1.2 新增规格参数

① 在com.atguigu.gulimall.product.vo包下新建一个AttrVo对象,用于接收页面提交的请求数据,这个AttrVo对象就是比AttrEntity实体类多了一个字段attrGroupId,并且该字段不在数据库中:

@Data
public class AttrVo {
    /**
     * 属性id
     */
    private Long attrId;
    /**
     * 属性名
     */
    private String attrName;
    /**
     * 是否需要检索[0-不需要,1-需要]
     */
    private Integer searchType;
    /**
     * 值类型[0-为单个值,1-可以选择多个值]
     */
    private Integer valueType;
    /**
     * 属性图标
     */
    private String icon;
    /**
     * 可选值列表[用逗号分隔]
     */
    private String valueSelect;
    /**
     * 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
     */
    private Integer attrType;
    /**
     * 启用状态[0 - 禁用,1 - 启用]
     */
    private Long enable;
    /**
     * 所属分类
     */
    private Long catelogId;
    /**
     * 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
     */
    private Integer showDesc;

    //多余的参数,不在数据库中
    private Long attrGroupId;
}

② 在AttrController中修改新增属性的方法:

@RequestMapping("/save")
public R save(@RequestBody AttrVo attr){
    attrService.saveAttr(attr);
    return R.ok();
}

③ 在attr表中保存基本属性信息,然后在关联表中保存关联信息,在AttrServiceImpl类中:

@Autowired
private AttrAttrgroupRelationDao relationDao;

@Transactional
@Override
public void saveAttr(AttrVo attr) {
    AttrEntity attrEntity = new AttrEntity();
    BeanUtils.copyProperties(attr,attrEntity);
    //保存基本数据
    this.save(attrEntity);
    //保存关联数据
    AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
    relationEntity.setAttrGroupId(attr.getAttrGroupId());
    relationEntity.setAttrId(attrEntity.getAttrId());
    relationDao.insert(relationEntity);
}

④ 测试:新增属性

在这里插入图片描述

在这里插入图片描述

7.1.3 查询分类规格参数列表

前端API:https://easydoc.xyz/s/78237135/ZUqEdvA4/Ld1Vfkcd

① 请求路径:/product/attr/base/list/{catelogId},分页查询,模糊查询

② 在AttrController中:

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

③ 在AttrServiceImpl类中:

@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
    QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>();
    if(catelogId!=0){
        queryWrapper.eq("catelog_id",catelogId);
    }
    String key = (String) params.get("key");
    if(!StringUtils.isEmpty(key)){
        //attr_id=key 或者 attr_name like %key%
        queryWrapper.and((wrapper)->{
            wrapper.eq("attr_id",key).or().like("attr_name",key);
        });
    }
    IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params),queryWrapper);
    return new PageUtils(page);
}

重启项目,查看规格参数,可以看到在响应数据时还需要响应所属分类和所属分组,可以参考API前端文档。
在这里插入图片描述

④ 添加响应对象AttrRespVo,比AttrEntity多了所属分类和所属分组:

@Data
public class AttrRespVo extends AttrVo {
    /**
     *   "catelogName": "手机/数码/手机", //所属分类名字
     *   "groupName": "主体", //所属分组名字
     */
    private String catelogName;
    private String groupName;
}

⑤ 在AttrServiceImpl类中修改queryBaseAttrPage()方法:

@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
    QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>();

    if(catelogId != 0){
        queryWrapper.eq("catelog_id",catelogId);
    }

    String key = (String) params.get("key");
    if(!StringUtils.isEmpty(key)){
        //attr_id  attr_name
        queryWrapper.and((wrapper)->{
            wrapper.eq("attr_id",key).or().like("attr_name",key);
        });
    }

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

    PageUtils pageUtils = new PageUtils(page);
    List<AttrEntity> records = page.getRecords();
    List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
        AttrRespVo attrRespVo = new AttrRespVo();
        BeanUtils.copyProperties(attrEntity, attrRespVo);

        //查询一条记录
        AttrAttrgroupRelationEntity attrId = relationDao.selectOne(
                new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
        //根据attrId查询出AttrGroupEntity,然后查询出AttrGroupName
        if (attrId != null) {
            AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
            attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
        }
        CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
        if (categoryEntity != null) {
            attrRespVo.setCatelogName(categoryEntity.getName());
        }
        return attrRespVo;
    }).collect(Collectors.toList());
    pageUtils.setList(respVos);
    return pageUtils;
}

在这里插入图片描述

7.1.4 查询属性详情

需求:在修改规格参数时,希望回显所属分类(分类的完整路径)和所属分组

在这里插入图片描述

① 前端接口API:https://easydoc.xyz/s/78237135/ZUqEdvA4/7C3tMIuF

在这里插入图片描述

① AttrRespVo中编写需要响应的字段:

@Data
public class AttrRespVo extends AttrVo {
    private String catelogName;
    private String groupName;
    private Long[] catelogPath;
}

② 在AttrController中修改info()方法,加上响应字段:

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

③ 在AttrServiceImpl类中:

@Override
public AttrRespVo getAttrInfo(Long attrId) {
    AttrRespVo respVo = new AttrRespVo();
    AttrEntity attrEntity = this.getById(attrId);
    //将attrEntity中的基本属性拷贝到respVo
    BeanUtils.copyProperties(attrEntity,respVo);
    
    //1、设置分组信息
    //查询一条记录,条件为:attr_id=attrId
    AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
    if(attrgroupRelation!=null){
        respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
        AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
        if(attrGroupEntity!=null){
            respVo.setGroupName(attrGroupEntity.getAttrGroupName());
        }
    }

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

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

测试:点击修改,可以显示所属分类和所属分组,但是当修改所属分组后,保存不成功,因为我们只做了新增的功能save(),并没有完成修改保存的功能,即修改update()方法:

在这里插入图片描述

7.1.5 修改规格参数

① 在AttrController中:

@RequestMapping("/update")
public R update(@RequestBody AttrVo attr){
    attrService.updateAttr(attr);
    return R.ok();
}

② 在AttrServiceImpl类中:

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

    //1、修改分组关联
    AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();

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

    Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
    if(count>0){
        relationDao.update(relationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attr.getAttrId()));
    }else{
        relationDao.insert(relationEntity);
    }
}

7.2 销售属性

因为后期代码量太大,并且多为增删改查,因此笔记不再做的很细了。

① 前端API接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/FTx6LRbR

在这里插入图片描述

② 数据库分析:

对于pms_attr这张表,既存放了规格参数(基本属性),又存放了销售属性,当attr_type=0时代表销售属性,当attr_type=1时代表规格参数:

在这里插入图片描述

可以将规格参数和销售属性的查询复用一个方法,将attrType作为参数判断条件:

//product/attr/sale/list/0?
///product/attr/base/list/{catelogId}
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
                      @PathVariable("catelogId") Long catelogId,
                      @PathVariable("attrType")String type){

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

③ 当新增一个销售属性时,我们发现属性关联表的attr_group_id为null,因此需要在saveAttr(AttrVo attr)方法中在保存基本属性后,保存关联数据时,需要判断attr.getAttrType()==1,只有成立才保存attr_group_id

在这里插入图片描述

7.3 属性分组与属性的关联关系

① 获取指定分组关联的所有属性:获取属性分组的关联的所有属性

在这里插入图片描述

接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/LnjzZHPj

在AttrController中,根据attrgroupId找到组内关联的所有属性:

@RequestMapping("/{attrgroupId}/attr/relation")
public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){
    List<AttrEntity> entities = attrService.getRelationAttr(attrgroupId);
    return R.ok().put("data",entities);
}
/**
 * 根据attrgroupId查找所有属性
 * @param attrgroupId
 * @return
 */
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
    //根据attrgroupId查找所有关联的属性
    List<AttrAttrgroupRelationEntity> entities
            = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>()
            .eq("attr_group_id", attrgroupId));

    // 获取所有的属性的attrIds
    List<Long> attrIds = entities.stream().map((attr) -> {
        return attr.getAttrId();
    }).collect(Collectors.toList());
	
    //根据attrIds获取所有的属性
    Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
    return (List<AttrEntity>) attrEntities;
}

在这里插入图片描述

② 移除属性与分组的关联关系:

接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/qn7A2Fht

属性并不删除,只是移除属性和分组之间的关联关系而已,在AttrGroupController中:

//请求参数:[{"attrId":1,"attrGroupId":2}]
@PostMapping("/attr/relation/delete")
public R deleteRelation( @RequestBody AttrGroupRelationVo[] vos){
    attrService.deleteRelation(vos);
    return R.ok();
}

在AttrServiceImpl中:

@Override
public void deleteRelation(AttrGroupRelationVo[] vos) {
    List<AttrAttrgroupRelationEntity> entities = Arrays.asList(vos).stream().map((item) -> {
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        BeanUtils.copyProperties(item, relationEntity);
        return relationEntity;
    }).collect(Collectors.toList());
    //批量移除属性分组和属性的关联关系
    relationDao.deleteBatchRelation(entities);
}

下面要做的就是自己手动的写sql语句,实现deleteBatchRelation(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>

③ 查询属性分组未关联的所有属性:

需求分析:点击关联后,新建关联,获取属性分组里面还没有关联的本分类里面的其他基本属性,方便添加新的关联

前端接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/d3EezLdO

//获取当前分组没有关联的所有属性
@RequestMapping("/{attrgroupId}/noattr/relation")
public R attrNoRelation(@PathVariable("attrgroupId") Long attrgroupId,
                        @RequestParam Map<String,Object> params){
    PageUtils page =  attrService.getNoRelationAttr(params,attrgroupId);
    return R.ok().put("page",page);
}
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
    //1、当前分组只能关联自己所属的分类里面的所有属性
    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
    Long catelogId = attrGroupEntity.getCatelogId();
    //2、当前分组只能关联别的分组没有引用的属性
    //2.1)、当前分类下的其他分组
    List<AttrGroupEntity> group = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
    List<Long> collect = group.stream().map(item -> {
        return item.getAttrGroupId();
    }).collect(Collectors.toList());

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

    //2.3)、从当前分类的所有属性中移除这些属性;
    QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>().eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
    if(attrIds!=null && attrIds.size()>0){
        wrapper.notIn("attr_id", attrIds);
    }
    String key = (String) params.get("key");
    if(!StringUtils.isEmpty(key)){
        wrapper.and((w)->{
            w.eq("attr_id",key).or().like("attr_name",key);
        });
    }
    IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);

    PageUtils pageUtils = new PageUtils(page);

    return pageUtils;
}

在这里插入图片描述

④ 添加属性与分组关联关系 :

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/VhgnaedC

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

8. 商品服务 - 新增商品

8.1 调试会员等级相关接口

① 首先启动gulimall-member服务,启动报错:NacosException: endpoint is blank,原因是我们在gulimall-common中引入了nacos服务配置中心,但是却没有使用配置中心,因此我们修改pom文件,将nacos配置中心坐标排除掉:

<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <exclusions>
        <exclusion>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </exclusion>
    </exclusions>
</dependency>

② 在guimall-gateway的applicaiton.yml文件中配置路由规则:

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

③ 将项目提供的前端代码拷贝到src\views\modules文件夹下面,启动项目,找到用户系统,会员等级

在这里插入图片描述

8.2 发布商品

1、 获取分类关联的品牌

在这里插入图片描述

前端:https://easydoc.xyz/s/78237135/ZUqEdvA4/HgVjlzWV

在CategoryBrandRelationController中,获取分类关联的所有品牌:

@GetMapping("/brands/list")
public R relationBrandsList(@RequestParam(value = "catId",required = true) Long catId){
   List<BrandEntity> entities =  categoryBrandRelationService.getBrandsByCatId(catId);
    List<BrandVo> collect = entities.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);
}

对于 pms_category_brand_relation表:我们可以根据catelog_id查询出brand_id ,根据brand_id查询出品牌

在这里插入图片描述

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

测试:分类下的品牌仍然不显示
在这里插入图片描述

postman测试后端接口没问题:

在这里插入图片描述

原因是前端代码的问题:

//安装pubsub-js
npm install --save pubsub-js
//在main.js中引入
import PubSub from 'pubsub-js'
//在main.js中挂载全局
Vue.prototype.PubSub = PubSub

在这里插入图片描述

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

前端:https://easydoc.xyz/s/78237135/ZUqEdvA4/6JM6txHf

获取分类下所有分组,以及该分组下关联的所有属性

@GetMapping("/{catelogId}/withattr")
public R getAttrGroupWithAttrs(@PathVariable("catelogId") Long catelogId){
    //1、查询当前分类下的所有属性分组
    //2、查询当前属性分组下所有的属性
    // AttrGroupWithAttrsVo是响应数据的vo,封装了要响应的属性分组和对应的属性
   List<AttrGroupWithAttrsVo> vos
           =  attrGroupService.getAttrGroupWithAttrsByCatelogId(catelogId);
   return R.ok().put("data",vos);
}

在属性分组表中关联了一个catelog_id字段,可以通过这个字段查询出该分类下所有的分组:

在这里插入图片描述

@Override
public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrsByCatelogId(Long catelogId) {
    //1、查询该分类下所有的分组信息
    List<AttrGroupEntity> attrGroupEntities
            = this.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
    //2、查询出所有属性
    List<AttrGroupWithAttrsVo> collect = attrGroupEntities.stream().map((item) -> {
        AttrGroupWithAttrsVo attrsVo = new AttrGroupWithAttrsVo();
        BeanUtils.copyProperties(item, attrsVo);
        List<AttrEntity> attrs = attrService.getRelationAttr(attrsVo.getAttrGroupId());
        attrsVo.setAttrs(attrs);
        return attrsVo;
    }).collect(Collectors.toList());
    return collect;
}

获取到了手机这个分类下的所有分组,以及每个分组下的基本属性信息:
在这里插入图片描述

8.3 新增商品

接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/5ULdV3dd

@PostMapping("/save")
public R save(@RequestBody SpuSaveVo vo){
    spuInfoService.saveSpuInfo(vo);
    return R.ok();
}
@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.savePurInfoDesc(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 valueEntity = new ProductAttrValueEntity();
        valueEntity.setSpuId(spuInfoEntity.getId());
        valueEntity.setAttrId(attr.getAttrId());
        AttrEntity attrEntity = attrService.getById(attr.getAttrId());
        valueEntity.setAttrName(attrEntity.getAttrName());
        valueEntity.setAttrValue(attr.getAttrValues());
        valueEntity.setQuickShow(attr.getShowDesc());
        return valueEntity;
    }).collect(Collectors.toList());
    attrValueService.saveProductAttr(collect);

    //5、保存spu的积分信息;gulimall_sms->sms_spu_bounds

    //5、保存当前spu对应的所有的sku信息
    List<Skus> skus = vo.getSkus();
    if(skus!=null && skus.size()>0){

        skus.forEach(item->{
            //获取默认图片
            String defaultImg = "";
            for(Images image : item.getImages()){
                if(image.getDefaultImg()==1){
                    defaultImg = image.getImgUrl();
                }
            }
            //5.1 保存sku的基本信息:pms_sku_info
            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);
            //只有保存了skuInfoEntity,才能获取自增主键,进而保存skuImagesEntity
            skuInfoService.saveSkuInfo(skuInfoEntity);

            //5.2 保存sku的图片信息:pms_sku_images
            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;
            }).collect(Collectors.toList());
            skuImagesService.saveBatch(imagesEntities);

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

            //5.4 sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
        });
    }
}

以上已经完成业务实现都属于商品服务gulimall-product,而对于剩下的未完成的业务是需要远程调用实现的,因为gulimall-product服务已经无法处理,需要交给gulimall-coupon服务来处理,因此使用openFeign来实现远程调用,让gulimall-product服务来调用gulimall-coupon服务。

1、 保存spu的积分信息

① 首先保证gulimall-product和gulimall-coupon服务都在注册中心中,并且在主启动类上都开启了服务注册与发现功能。

② 远程调用openfeign,首先需要在gulimall-product服务中引入openfeign坐标依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

③ 主启动类上开启远程服务调用功能:

@EnableFeignClients(basePackages ="com.atguigu.gulimall.product.feign" )
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallProductApplication.class,args);
    }
}

④ 在gulimall-common服务中定义一个TO对象,作为数据传输对象SpuBoundTo,用来将gulimall-product接收的数据传给gulimall-coupon服务:

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

⑤ 要调用的远程服务为:

package com.atguigu.gulimall.coupon.controller;

@RestController
@RequestMapping("coupon/spubounds")
public class SpuBoundsController {
    @Autowired
    private SpuBoundsService spuBoundsService;
    
    @PostMapping("/save")
    public R save(@RequestBody SpuBoundsEntity spuBounds){
		spuBoundsService.save(spuBounds);
        return R.ok();
    }
}

⑥ 在gulimall-product中定义一个调用远程服务的接口,调用远程服务:

//指明要调用的远程服务名称
@FeignClient(value = "gulimall-coupon")
public interface CouponFeignService {
    /**
     * 1、couponFeignService.saveSpuBounds(spuBoundTo)找到这个远程服务
     * 2、将saveSpuBounds(@RequestBody SpuBoundTo spuBounds)请求体转换为json
     * 3、找到gulimall-coupon服务,发送/coupon/spubounds/save请求
     * 4、远程服务save(@RequestBody SpuBoundsEntity spuBounds)收到请求
     *    将请求体中的json转换为SpuBoundsEntity
     */
    @PostMapping("/coupon/spubounds/save")
    public R saveSpuBounds(@RequestBody SpuBoundTo spuBounds);
}

⑦ 在业务类中调用openFeign接口:

@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
    //5、保存spu的积分信息;gulimall_sms->sms_spu_bounds
    Bounds bounds = vo.getBounds();
    SpuBoundTo spuBoundTo = new SpuBoundTo();
    BeanUtils.copyProperties(bounds,spuBoundTo);
    spuBoundTo.setSpuId(spuInfoEntity.getId());
    couponFeignService.saveSpuBounds(spuBoundTo);
}

2、 保存spu的优惠满减信息

gulimall_sms-> sms_sku_ladder sms_sku_full_reduction sms_member_price

在这里插入图片描述

① 在com.atguigu.common.to包中定义TO对象,接受请求提交的数据,并在两个服务间传送数据:

@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;
}

② 被调用的远程服务接口方法:

package com.atguigu.gulimall.coupon.controller;

@RestController
@RequestMapping("coupon/skufullreduction")
public class SkuFullReductionController {
    @Autowired
    private SkuFullReductionService skuFullReductionService;

    //被调用的远程服务的接口方法:
    @PostMapping("/saveinfo")
    public R saveInfo(@RequestBody SkuReductionTo reductionTo){
        skuFullReductionService.saveSkuReduction(reductionTo);
        return R.ok();
    }
}
package com.atguigu.gulimall.coupon.service.impl;

@Service("skuFullReductionService")
public class SkuFullReductionServiceImpl extends ServiceImpl<SkuFullReductionDao, SkuFullReductionEntity> implements SkuFullReductionService {

    @Autowired
    private SkuLadderService skuLadderService;

    @Autowired
    private MemberPriceService memberPriceService;

    @Override
    public void saveSkuReduction(SkuReductionTo reductionTo) {
        //1、保存  sms_sku_ladder
        SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
        skuLadderEntity.setSkuId(reductionTo.getSkuId());
        skuLadderEntity.setDiscount(reductionTo.getDiscount());
        skuLadderEntity.setFullCount(reductionTo.getFullCount());
        skuLadderEntity.setAddOther(reductionTo.getCountStatus());
        skuLadderService.save(skuLadderEntity);

        //2、保存 sms_full_reduction
        SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
        BeanUtils.copyProperties(reductionTo,skuFullReductionEntity);
        this.save(skuFullReductionEntity);

        //3、sms_member_price
        List<MemberPrice> memberPriceList = new ArrayList<>();
        List<MemberPriceEntity> collect = memberPriceList.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;
        }).collect(Collectors.toList());
        memberPriceService.saveBatch(collect);
    }
}

③ 定义调用远程服务的接口方法:

package com.atguigu.gulimall.product.feign;

//指明要调用的远程服务名称
@FeignClient(value = "gulimall-coupon")
public interface CouponFeignService {
    @PostMapping("/coupon/spubounds/save")
    public R saveSpuBounds(@RequestBody SpuBoundTo spuBounds);

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

③ 在业务类中调用openFeign接口:

@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
    //5、保存当前spu对应的sku信息
    List<Skus> skus = vo.getSkus();
    if(skus!=null && skus.size()>0){
        skus.forEach(item->{
            //5.4 sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
            SkuReductionTo skuReductionTo = new SkuReductionTo();
            BeanUtils.copyProperties(item,skuReductionTo);
            skuReductionTo.setSkuId(skuId);
            R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
            if(r1.getCode()!=0){
                log.error("远程保存spu优惠信息失败");
            }
        });
    }
}

9. 商品服务 - 商品管理

9.1 SPU检索

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/9LISLvy7

在这里插入图片描述

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

根据检索条件进行检索:

@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 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);
    }
    String status = (String) params.get("status");
    if(!StringUtils.isEmpty(status)){
        wrapper.eq("publish_status",status);
    }
    IPage<SpuInfoEntity> page = this.page(
        new Query<SpuInfoEntity>().getPage(params),wrapper);
    return new PageUtils(page);
}

9.2 SKU检索

接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/ucirLq1D

在这里插入图片描述

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

根据检索条件进行检索:

@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
    QueryWrapper<SkuInfoEntity> wrapper = new QueryWrapper<>();
    String key = (String) params.get("key");
    if(!StringUtils.isEmpty(key)){
        wrapper.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)){
        wrapper.eq("catalog_id",catelogId);
    }
    String brandId = (String) params.get("brandId");
    if(!StringUtils.isEmpty(brandId) && "0".equalsIgnoreCase(brandId)){
        wrapper.eq("brand_id",brandId);
    }
    String min = (String) params.get("min");
    if(!StringUtils.isEmpty(min)){
        wrapper.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){
                wrapper.le("price",max);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    IPage<SkuInfoEntity> page = this.page(
            new Query<SkuInfoEntity>().getPage(params),wrapper);

    return new PageUtils(page);
}

在这里插入图片描述

10. 仓储服务 - 仓库管理

仓储服务对应于gulimall-ware服务,首先需要将该服务注册进nacos注册中心

① 配置nacos的注册中心地址:

server:
  port: 11000
spring:
  application:
    name: gulimall-ware
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.38.22:3306/gulimall_wms?useUnicode=true&characterEncoding= utf-8
    driver-class-name: com.mysql.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

② 开启服务注册与发现:

@EnableTransactionManagement
@EnableDiscoveryClient
@MapperScan("com.atguigu.gulimall.ware.dao")
@SpringBootApplication
public class GulimallWareApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallWareApplication.class, args);
    }
}

③ 启动服务报错:com.alibaba.nacos.api.exception.NacosException: endpoint is blank

原因是我们引入了nacos配置中心的坐标,需要排除掉:

<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <exclusions>
        <exclusion>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </exclusion>
    </exclusions>
</dependency>

在这里插入图片描述

④ 在网关中配置库存系统的路由规则:

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

10.1 获取仓库列表

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/mZgdqOWe
在这里插入图片描述

需要根据条件进行模糊检索:

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

在这里插入图片描述

10.2 查询库存

在这里插入图片描述

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/hwXrEXBZ

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

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

    return new PageUtils(page);
}

10.3 查询采购需求

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/Ss4zsV7R

在这里插入图片描述

@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
    PageUtils page = purchaseDetailService.queryPage(params);
    return R.ok().put("page", page);
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
    //   key: '华为',//检索关键字
    //   status: 0,//状态
    //   wareId: 1,//仓库id
    QueryWrapper<PurchaseDetailEntity> queryWrapper = new QueryWrapper<>();
    String key = (String) params.get("key");
    if(!StringUtils.isEmpty(key)){
        queryWrapper.and((wrapper)->{
            wrapper.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);
}

在这里插入图片描述

10.4 合并采购需求

① 查询未领取的采购单:https://easydoc.xyz/s/78237135/ZUqEdvA4/hI12DNrH

在这里插入图片描述

@RequestMapping("/unreceive/list")
public R unreceivelist(@RequestParam Map<String, Object> params){
    PageUtils page = purchaseService.queryPageUnreceivedPurchase(params);
    return R.ok().put("page", page);
}
@Override
public PageUtils queryPageUnreceivedPurchase(Map<String, Object> params) {
    IPage<PurchaseEntity> page = this.page(
            new Query<PurchaseEntity>().getPage(params),
            new QueryWrapper<PurchaseEntity>()
        			//新建和已分配状态的采购需求
                    .eq("status",0).or().eq("status",1)
    );
    return new PageUtils(page);
}

在这里插入图片描述

② 合并采购需求 :https://easydoc.xyz/s/78237135/ZUqEdvA4/cUlv9QvK

将下面的两个采购需求合并为一个采购单:

在这里插入图片描述

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

合并采购单时,我们需要获取采购单id,并且需要改变采购单的状态,由新建状态改为已分配状态:

在这里插入图片描述

@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);
        //得到采购单id
        purchaseId = purchaseEntity.getId();
    }else{
        List<Long> items = mergeVo.getItems();
        Long finalPurchaseId = purchaseId;
        List<PurchaseDetailEntity> collect = items.stream().map((item) -> {
            //新建采购需求实体类
            PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
            purchaseDetailEntity.setId(item);
            purchaseDetailEntity.setPurchaseId(finalPurchaseId);
            purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());
            return purchaseDetailEntity;
        }).collect(Collectors.toList());
        purchaseDetailService.updateBatchById(collect);

        //在合并完采购单后,希望新的采购单时间显示是正确的
        PurchaseEntity purchaseEntity = new PurchaseEntity();
        purchaseEntity.setCreateTime(new Date());
        purchaseEntity.setUpdateTime(new Date());
        purchaseEntity.setId(purchaseId);
        this.updateById(purchaseEntity);
    }
}

在这里插入图片描述

10.5 领取采购单

当领取采购单之后,采购需求的采购状态应该变为正在采购:
在这里插入图片描述

采购员领取采购单之后,采购单状态应该变为已领取,并且采购单不能再分配其他人采购:

在这里插入图片描述

领取采购单前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/vXMBBgw1

@PostMapping("/received")
public R received(@RequestBody List<Long> ids){
    purchaseService.received();
    return R.ok();
}
@Override
public void received(List<Long> ids) {
    //1、确认当前采购单是新建或者已分配状态
    List<PurchaseEntity> collect = ids.stream().map(id -> {
        PurchaseEntity byId = this.getById(id);
        return byId;
    }).filter(item -> {
        if (item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||
            item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {
            return true;
        }
        return false;
    }).map(item->{
        item.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());
        item.setUpdateTime(new Date());
        return item;
    }).collect(Collectors.toList());

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

    //3、改变采购项(采购需求)的状态
    collect.forEach((item)->{
        List<PurchaseDetailEntity> entities = purchaseDetailService.listDetailByPurchaseId(item.getId());
        List<PurchaseDetailEntity> detailEntities = entities.stream().map(entity -> {
            PurchaseDetailEntity entity1 = new PurchaseDetailEntity();
            entity1.setId(entity.getId());
            //将采购状态改为正在采购
            entity1.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());
            return entity1;
        }).collect(Collectors.toList());
        purchaseDetailService.updateBatchById(detailEntities);
    });
}
@Override
public List<PurchaseDetailEntity> listDetailByPurchaseId(Long id) {
    List<PurchaseDetailEntity> purchaseId
            = this.list(new QueryWrapper<PurchaseDetailEntity>().eq("purchase_id", id));
    return purchaseId;
}

10.6 完成采购

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/cTQHGXbK

@PostMapping("/done")
public R finish(@RequestBody PurchaseDoneVo purchaseDoneVo){
    purchaseService.done(purchaseDoneVo);
    return R.ok();
}
@Transactional
@Override
public void done(PurchaseDoneVo purchaseDoneVo) {
    //1、改变采购单每一个采购项的状态
    Boolean flag = true;
    List<PurchaseItemDoneVo> items = purchaseDoneVo.getItems();
    List<PurchaseDetailEntity> list = new ArrayList<>();
    for(PurchaseItemDoneVo item:items){
        PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
        if(item.getStatus()==WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()){
            flag=false;
            purchaseDetailEntity.setStatus(item.getStatus());
        }else{
            purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.FINISH.getCode());
            //将成功采购的进行入库
            PurchaseDetailEntity entity = purchaseDetailService.getById(item.getItemId());
            wareSkuService.addStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum());
        }
        purchaseDetailEntity.setId(item.getItemId());
        list.add(purchaseDetailEntity);
    }
    //批量更新采购项的状态
    purchaseDetailService.updateBatchById(list);

    //2、改变采购单状态
    PurchaseEntity purchaseEntity = new PurchaseEntity();
    purchaseEntity.setId(purchaseDoneVo.getId());
    if(flag==true){
        purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.FINISH.getCode());
    }else{
        purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.HASERROR.getCode());
    }
    purchaseEntity.setUpdateTime(new Date());
    this.updateById(purchaseEntity);
}

假设1号采购单状态为采购失败,2号采购单状态为已完成,使用postman测试:

在这里插入图片描述

在这里插入图片描述

查看商品库存:

在这里插入图片描述

我们可以将库存中商品的名字查询出来保存,需要用到远程调用openfeign,即远程调用gulimall-product服务。

① gulimall-ware服务开启远程调用功能:

@EnableFeignClients 
@EnableTransactionManagement
@EnableDiscoveryClient
@MapperScan("com.atguigu.gulimall.ware.dao")
@SpringBootApplication
public class GulimallWareApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallWareApplication.class, args);
    }
}

② 编写远程调用openfeign接口:

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

③ 商品服务的远程调用接口:

@Override
public void addStock(Long skuId, Long wareId, Integer skuNum) {
    //判断是否存在库存记录
    List<WareSkuEntity> wareSkuEntities
            = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>()
            .eq("sku_id", skuId).eq("ware_id", wareId));
    //如果不存在就新增库存
    if(wareSkuEntities==null || wareSkuEntities.size()==0){
        WareSkuEntity wareSkuEntity = new WareSkuEntity();
        wareSkuEntity.setSkuId(skuId);
        wareSkuEntity.setStock(skuNum);
        wareSkuEntity.setWareId(wareId);
        wareSkuEntity.setStockLocked(0);
        // 远程调用商品服务:加上try--catch后就不会回归事务
        try {
            R info = productFeignService.info(skuId);
            Map<String,Object> skuInfo =(Map<String,Object>) info.get("skuInfo");
            if(info.getCode()==0){
                wareSkuEntity.setSkuName((String)skuInfo.get("skuName"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        wareSkuDao.insert(wareSkuEntity);
    }else{
        wareSkuDao.addStock(skuId,wareId,skuNum);
    }
}

11. 商品服务 - Spu管理

11.1 获取spu规格

在这里插入图片描述

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/GhhJhkg7

@GetMapping("/base/listforspu/{spuId}")
public R baseAttrlistforspu(@PathVariable("spuId") Long spuId){
    List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrlistforspu(spuId);
    return R.ok().put("data",entities);
}
@Override
public List<ProductAttrValueEntity> baseAttrlistforspu(Long spuId) {
    List<ProductAttrValueEntity> entities
            = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>()
            .eq("spu_id", spuId));
    return entities;
}

前端页面报错400:

在这里插入图片描述

使用postman测试后端接口,发现没有错误:

在这里插入图片描述

解决方法,在数据库中gulimall-admin中的sys_menu表中添加一行数据:

在这里插入图片描述

重启前端项目和后端项目后,再次点击spu管理中的规格回显:

在这里插入图片描述

11.2 修改商品规格

前端:https://easydoc.xyz/s/78237135/ZUqEdvA4/GhnJ0L85

@PostMapping("/update/{spuId}")
public R updateSpuAttr(@PathVariable("spuId") Long spuId,
                       @RequestBody List<ProductAttrValueEntity> entities){
    productAttrValueService.updateSpuAttr(spuId,entities);
    return R.ok();
}
@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
    //1、删除这个spuId之前对应的所有属性
    this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));
    
    List<ProductAttrValueEntity> collect = entities.stream().map((item) -> {
        item.setSpuId(spuId);
        return item;
    }).collect(Collectors.toList());
    this.saveBatch(collect);
}

在这里插入图片描述

恭喜您,完结撒花😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄😄

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页