尚硅谷—谷粒商城

6 篇文章 0 订阅
5 篇文章 0 订阅

sql:https://download.csdn.net/download/tian__c/19475671

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

环境搭建

1、安装Linux虚拟机

  • 下载&安装VirtualBox:https://www.virtualbox.org/, 要开启CPU虚拟化

  • 下载vagrant

  • https://app.vagrantup.com/boxes/search Vagrant 官方镜像仓库

  • https://www.vagrantup.com/downloads.html Vagrant 下载

  • 安装后重启系统。cmd中输入vagrant有版本代表成功了。

  • vagrant init centos/7,即可初始化一个centos7系统。(注意这个命令在哪个目录下执行的,他的Vagrantfile就生成在哪里)

  • vagrant up启动虚拟机环境。

  • 启动完成后ctrl+c退出

  • 前面的页面中有ssh账号信息。vagrant ssh 就会连上虚拟机。可以使用exit退出

下次使用也可以直接vagrant up直接启动,但要确保当前目录在Vagrantfile下,不过我们也可以配置环境变量。启动后再次vagrant ssh连上即可

也可以用vagrantbox直接右键启动
在这里插入图片描述
默认虚拟机的ip地址不是固定ip,开发不方便

  • 修改Vagrantfile
  • 使用ipconfig查看ip,并修改
    config.vm.network "private_network",ip:"169.254.249.10""
  • vagrant reload重启
  • nagrant ssh 连接
  • ip addr 查看ip
  • 互相能ping通

在这里插入图片描述

2、安装docker

#卸载系统之前的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

sudo systemctl start docker
# 设置开机自启动
sudo systemctl enable docker

docker -v
sudo docker images

# 配置镜像加速

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

docker安装mysql

sudo docker pull mysql:5.7

# --name指定容器名字 -v目录挂载 -p指定端口映射  -e设置mysql参数 -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

参数说明
-p 3306:3306: 将容器的3306端口映射到主机的3306端口
-v /mydata/mysg/conf:/etc/mysgl:将配置文件夹挂载到主机
-v /mydata/mysg/log:/var/log/mysg!:将日志文件夹挂载到主机
-v /mydata/mysgl/data:/var/ib/mysgl/:将配置文件夹挂载到主机
-e MYSQL ROOT_ PASSWORD=root:初始化root用户的密码

su root 密码为vagrant,这样就可以不写sudo了

[root@localhost vagrant]# docker ps 查看正在运行的服务

在这里插入图片描述
Navicat连接mysql:

  • 地址:169.254.249.10
  • 账号:root
  • 密码:root

进入mysql容器内部查看:

docker exec -it mysql bin/bash
exit;

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

重启:docker restart mysql

docker安装redis

  • 如果直接挂载的话docker会以为挂载的是一个目录,所以我们先创建一个文件然后再挂载,在虚拟机中。
# 在虚拟机中
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

  • 默认是不持久化的。在配置文件中输入appendonly yes,就可以aof持久化了。修改完docker restart redis,docker -it redis redis-cli
vi /mydata/redis/conf/redis.conf
# 插入下面内容
appendonly yes
保存

重启
docker restart redis

  • 设置redis容器在docker启动的时候启动
docker update redis --restart=always

用Redis Desktop Manager连接redis
在这里插入图片描述

3、开发环境统一

  • jdk1.8以上
  • maven:在settings中配置阿里云镜像,配置jdk1.8。
     <!-- 阿里云仓库 -->
<mirrors>
	 <mirror>
			<id>nexus-aliyun</id>
			<mirrorOf>*</mirrorOf>
			<name>Nexus aliyun</name>
			<url>http://maven.aliyun.com/nexus/content/groups/public</url>
	</mirror>
</mirrors>
<profiles>
	<profile>
		<id>jdk-1.8</id>
		<activation>
			<activeByDefault>true</activeByDefault>
			<jdk>1.8</jdk>
		</activation>

		<properties>
			<maven.compiler.source>1.8</maven.compiler.source>
			<maven.compiler.target>1.8</maven.compiler.target>
			<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
		</properties>
	</profile>
</profiles>

IDEA安装插件lombok,mybatisX。IDEA设置里配置好maven

安装vscode

下载vsCode用于前端管理系统。在vsCode里安装插件。

  • Auto Close Tag
  • Auto Rename Tag
  • Chinese
  • ESlint
  • HTML CSS Support
  • HTML Snippets
  • JavaScript ES6
  • Live Server
  • open in brower
  • Vetur
  • Vue 2 Snippets

安装配置git

下载git客户端,右键桌面Git GUI/bash Here。去bash

# 配置用户名
git config --global user.name "username"  //(名字,随意写)

# 配置邮箱
git config --global user.email "55333@qq.com" // 注册账号时使用的邮箱

# 配置ssh免密登录
ssh-keygen -t rsa -C "55333@qq.com"
三次回车后生成了密钥:公钥私钥
cat ~/.ssh/id_rsa.pub

也可以查看密钥
浏览器登录码云后,个人头像上点设置--ssh公钥---随便填个标题---复制
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCg0FWB5ait+D5iTyZ8Mviq1BO3bvWImDyonsz5lZ/Vj9xkUW+92tAWzpdBanFEZcDVfNPW1woOAp24oXSr+9MFY1E501QIsV1cvI1gt9QWPjMhlfTgs3sJ3ngOATEIJJMPWdZr6d+KJvp2sQN5gQJxv2EEYqSjP190bECO4fybKBZdimiN+++thint/QHIPUdw7MTlfiUv8vYgvoGgTkZc8EU+dFql7YraPanOteE68O1LNCJgUJZHj3kbzA/pcu0SLW0B7GIqNXEvqmSUzkAofbPERtI2yk9HXm8+VymEcS+vHlcYE6e1Y9mSkqrUX29dem6esvcMPsla8RFUs9md2Ps60VIBE1tVvTp6naLc4Zz7TmaVjvBDiByHCrnX+MiVHa2b8w9gOH/uwgH7jypwczwUn+LRLQCAwAVBGGU7L1CuDJlZIY0FsPS6znrWbQSkG6YVkeXyRiKLfLUMGTio+3bxhrtPZWajOzcQA8dV1QnWpfxwufAgHi+nl1TD5d8= 2512555150@qq.com

# 测试
ssh -T git@gitee.com
测试成功,就可以无密给码云推送仓库了

创建gitee仓库

  • 在码云新建仓库,仓库名gulimall,选择语言java,在.gitignore选中maven(就会忽略掉maven一些个人无需上传的配置文件),许可证选Apache-2.0,开发模型选生成/开发模型,开发时在dev分支,发布时在master分支,创建。
  • 复制链接 https://gitee.com/chensibo/gulimall.git
    在这里插入图片描述
    把链接粘贴,clone 下gitee上的项目

4、创建项目微服务

建module:gulimall-product

在这里插入图片描述

依次创建出以下服务

  • 商品服务product
  • 存储服务ware
  • 订单服务order
  • 优惠券服务coupon
  • 用户服务member

共同点:

  • 导入web和openFeign
  • group:com.wlq.gulimall
  • Artifact:gulimall-XXX
  • 每一个服务,包名com.wlq.gulimall.XXX{product/order/ware/coupon/member}
  • 模块名:gulimall-XXX

修改聚合服务的pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.wlq.gulimall</groupId>
    <artifactId>gulimall</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall</name>
    <description>聚合服务</description>
    <packaging>pom</packaging>

    <modules>
        <module>gulimall-coupon</module>
        <module>gulimall-member</module>
        <module>gulimall-order</module>
        <module>gulimall-product</module>
        <module>gulimall-ware</module>
    </modules>



</project>

项目保存gitee

  • 在maven窗口刷新,并点击+号,找到刚才的pom.xml添加进来,发现多了个root。这样比如运行root的clean命令,其他项目也一起clean了。

  • 修改总项目的.gitignore,把小项目里的垃圾文件在提交的时候忽略掉

**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore

  • 在version control/local Changes,点击刷新看Unversioned Files,可以看到变化。

  • 全选最后剩下21个文件,选择右键、Add to VCS。

  • 在IDEA中安装插件:gitee,重启IDEA。

在Dfault changelist右键点击commit,去掉右面的勾选Perform code analysis、CHECK TODO,然后点击COMMIT,有个下拉列表,点击commit and push才会提交到云端。此时就可以在浏览器中看到了。
在这里插入图片描述
在这里插入图片描述

  • commit只是保存更新到本地
  • push才是提交到gitee
    在这里插入图片描述

5、数据库建立

设置容器自动启动

sudo docker update redis --restart=always
sudo docker update mysql--restart=always

在这里插入图片描述
在这里插入图片描述
创建SQL:连接Navicat

创建数据库:

  • gulimall_oms
  • gulimall_pms
  • gulimall_sms
  • gulimall_ums
  • gulimall_wms

在这里插入图片描述

6、人人开源

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

git.bash执行:

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

将下载的文件复制到gulimall项目

修改pom

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

1、创建后台管理的数据库,在db文件里,复制mysql的

在这里插入图片描述

2、修改配置文件(后端)

spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://169.254.249.10:3306/gulimall_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
            username: root
            password: root

访问:http://localhost:8080/renren-fast/
在这里插入图片描述

3、renren-fast-vue添加进vscode(前端)

将renren-fast-vue拖进vscode打开

4、安装node.js

  • node -v检查
  • 执行:npm config set registry http://registry.npm.taobao.org/
  • 然后去VScode的项目终端中输入 npm install,是要去拉取依赖
  • npm run dev
  • http://localhost:8001/#/login

注意要启动vagrant ssh,启动renren-fast
在这里插入图片描述
登入:

  • admin
  • admin

5、导入renren-generator

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

<modules>
        <module>gulimall-coupon</module>
        <module>gulimall-member</module>
        <module>gulimall-order</module>
        <module>gulimall-product</module>
        <module>gulimall-ware</module>
        <module>renren-fast</module>
        <module>renren-generator</module>
    </modules>
server:
  port: 80

# mysql
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #MySQL配置
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://169.254.249.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
mainPath=com.wlq
#\u5305\u540D
package=com.wlq.gulimall
moduleName=product
#\u4F5C\u8005
author=wlq
#Email
email=sunlightcs@gmail.com
#表前缀
tablePrefix=pms_

运行renren-generator主启动类

访问:http://localhost/

选中所有生成代码
在这里插入图片描述
解压后将main文件取代项目中的product中的main
在这里插入图片描述

6、逆向工程创建:建gulimall-common

<dependencies>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.12</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>


    </dependencies>

改product的pom(每个服务都添加)

		<dependency>
            <groupId>com.wlq.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
  • 根据product里的报错在renren-fast里找到相应的类进行导入直到不报错

common添加mysql数据源
根据自己mysql导入相应的版本

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>

product项目下新建yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://169.254.249.10:3306/gulimall_pms?
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto #配置主键自增

启动类添加注解

@MapperScan("com.wlq.gulimall.product.dao")
@SpringBootApplication
public class GulimallProductApplication {

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

}

test方法修改:

@SpringBootTest
class GulimallProductApplicationTests {

    @Autowired
    BrandService brandService;

    @Test
    void contextLoads() {

        BrandEntity brandEntity = new BrandEntity();

        brandEntity.setName("华为");
        brandService.save(brandEntity);
        System.out.println("保存成功....");
    }
    //查询
     //   List<BrandEntity> list = brandService.list(new QueryWrapper<BrandEntity>().eq("brand_id", 1L));
     //   list.forEach((item)->{
      //      System.out.println(item);
     //   });

}

在这里插入图片描述
在这里插入图片描述

7、为其它服务生成生成代码(按照第5步)

为每个服务添加server-port,以coupon为例,依次累加1000

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://169.254.249.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto #配置主键自增

server:
  port: 7000

并运行主启动类测试一下

springcloud Alibaba

1、简介

个人笔记:https://editor.csdn.net/md/?articleId=117130694

2、nacos注册中心

Common引入alibaba依赖:

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

nacos依赖

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

7000添加进nacos服务中心

YML

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://169.254.249.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver

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

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto #配置主键自增

server:
  port: 7000
  • 主启动加上:@EnableDiscoveryClient

  • 启动nacos再启动7000

同样的方法将其他几个也注册进nacos注册中心中心

3、nacos配置中心

<!--配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

1、新建bootstrap.properties

spring.application.name=gulimall-coupon

spring.cloud.nacos.config.server-addr=127.0.0.1:8848

2、修改controller

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

3、添加:@RefreshScope动态刷新
4、访问:http://localhost:7000/coupon/coupon/test

3、sentinel

4、seata

5、OSS

1、简介:

对象存储服务(Object Storage Service, Oss) 是一一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

springcloud

1、openfeign

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

写一个优惠券会员服务

1、修改coupon的controller:

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

2、在member中创建feign,在里面新建CouponFeignService接口

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

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

3、主启动类添加@EnableFeignClients(basePackages = "com.wlq.gulimall.member.feign")
4、修改member的controller

@Autowired
    CouponFeignService couponFeignService;

    @RequestMapping("/coupons")
    public R test(){
        MemberEntity memberEntity = new MemberEntity();
        memberEntity.setNickname("张三");

        R membercoupons = couponFeignService.membercoupons();

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

5、启动7000,8000

6、访问:http://localhost:8000/member/member/coupons
在这里插入图片描述

2、gateway

1、创建gulimall-gateway

2、主启动&POM

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
public class GulimallGatewayApplication {

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


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

3、创建getway命名空间
新增配置:
在这里插入图片描述

4、YML

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

application.properties

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

bootstrap.properties

spring.application.name=gulimall-gateway

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=91198d24-4eee-48d3-9a91-f7d98f6614e0

5、启动
http://localhost:88/hello?url=baidu

3、sleuth+zipkin服务链路追踪



商品服务-三级分类

  • 这里需要启动gateway,nacos,product,renren-fast

一、sql语句

gulimall_pms表pms_category的SQL语句:第一行有:pms_catelog.sql

二、查出所有分类及子分类

1、业务类

修改CategoryController

/**
     * 查出所有分类以及子分类,以树结构组装起来
     */
    public R list(){
        List<CategoryEntity> entities = categoryService.listWithTree();

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

修改CategoryService

public interface CategoryService extends IService<CategoryEntity> {

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

    List<CategoryEntity> listWithTree();
}

修改对应的Impl

@Override
    public List<CategoryEntity> listWithTree() {
        //1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2、组成父子的树形结构
        return entities;
    }

2、测试

在这里插入图片描述

三、组装成父子结构

1、业务类

修改CategoryEntity

@TableField(exist = false)
	private List<CategoryEntity> children;

Impl

@Override
    public List<CategoryEntity> listWithTree() {
        //1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2、组成父子的树形结构

        //2.1找到所有的一级分类
        List<CategoryEntity> level1Menus = entities.stream().filter(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 -> {
            //找到子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity,all));
            return categoryEntity;
        }).sorted((menu1,menu2)->{
            //菜单的排序
            return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());

        return children;
    }

2、测试

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

使用F12查看3级目录完成

四、与后台管理系统联调

编写前端项目

1、创建src\views\modules\product
2、新建category.vue

输入vue创建Vue模板:https://www.cnblogs.com/songjilong/p/12635448.html

3、使用element创建树级结构

https://element.eleme.cn/#/zh-CN/component/tree

<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>

<script>
  export default {
    data() {
      return {
        data: [{
          label: '一级 1',
          children: [{
            label: '二级 1-1',
            children: [{
              label: '三级 1-1-1'
            }]
          }]
        }, {
          label: '一级 2',
          children: [{
            label: '二级 2-1',
            children: [{
              label: '三级 2-1-1'
            }]
          }, {
            label: '二级 2-2',
            children: [{
              label: '三级 2-2-1'
            }]
          }]
        }, {
          label: '一级 3',
          children: [{
            label: '二级 3-1',
            children: [{
              label: '三级 3-1-1'
            }]
          }, {
            label: '二级 3-2',
            children: [{
              label: '三级 3-2-1'
            }]
          }]
        }],
        defaultProps: {
          children: 'children',
          label: 'label'
        }
      };
    },
    methods: {
      handleNodeClick(data) {
        console.log(data);
      }
    }
  };
</script>
  • alt shift f代码整理
4、查看:

在这里插入图片描述

5、编写method(参照sys里的role.vue)
1、复制role.vue的请求模板
this.$http({
          url: this.$http.adornUrl('/sys/role/list'),
          method: 'get',
          params: this.$http.adornParams({
            'page': this.pageIndex,
            'limit': this.pageSize,
            'roleName': this.dataForm.roleName
          })
        }).then(({data}) => {
          if (data && data.code === 0) {
            this.dataList = data.page.list
            this.totalPage = data.page.totalCount
          } else {
            this.dataList = []
            this.totalPage = 0
          }
          this.dataListLoading = false
        })

修改:

 //方法集合
  methods: {
    handleNodeClick(data) {
      console.log(data);
    },
    getMenus(){
        this.$http({
          url: this.$http.adornUrl('/product/category/list/tree'),
          method: 'get'
        }).then(({data})=>{
            console.log("成功获取的菜单数据...",data.data)
            this.menus = data.data;
        })
    }
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
      this.getMenus();
  },
2、修改config下的index.js,交给网关分配路径
  // api接口请求地址
  window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
3、将renren-fast添加进nacos服务注册中心

给renren-fast添加common依赖

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

YML

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

添加注解:@EnableDiscoveryClient

4、修改网关

YML

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: https://www.baidu.com
          predicates:
            - Query=url,baidu

        - id: qq_route
          uri: https://www.qq.com
          predicates:
            - Query=url,qq

        - id: admin_route
          uri: lb"//renren-fast
          predicates:
            - Path=/api/**
  ## 前端项目,带前缀,/api
5、测试;http://localhost:8001/#/login

在这里插入图片描述
修改:注意:精确的path需要写在上面

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: https://www.baidu.com
          predicates:
            - Query=url,baidu

        - id: qq_route
          uri: https://www.qq.com
          predicates:
            - Query=url,qq

        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
            # 路径重写,高精确的path写在上面

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



  ## 前端项目,带前缀,/api
  ## http://Localhost:88/api/captcha.ipg   http://renren-fast:8080/renren-fast/captcha.jpg
  ## http://Localhost:88/api/product/category/list/tree  http://localhost:10000/product/category/list/tree
  • 访问成功,点击登入发现报错跨域异常

在这里插入图片描述

6、解决跨域问题

1、在gateway下新建config,CorsConfiguration类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;


@Configuration
public class GulimallCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter(){

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();

        //1、配置跨域
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);


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

    }

}

2、将fast里的默认跨域注解掉 io.renren.config.CorsConfig

3、再重新启动

  • 成功登入
    在这里插入图片描述
7、树形展示三级分类(查询)

1、修改product,添加bootstrap.properties

spring.application.name=gulimall-product

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=86a8b9b7-a3f3-448b-a0e8-a812f1419032

添加命名空间
在这里插入图片描述
2、YML

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://169.254.249.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.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 #配置主键自增

server:
  port: 10000

3、主启动添加:@EnableDiscoveryClient

4、访问:http://Localhost:88/api/product/category/list/tree
在这里插入图片描述

5、修改vue

<template>
  <el-tree
    :data="menus"
    :props="defaultProps"
    @node-click="handleNodeClick"
  ></el-tree>
</template>
export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},

  data() {
    return {
      menus: [],
        
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },

6、访问http://localhost:8001/#/product-category
在这里插入图片描述

8、树形展示三级分类(删除)
<template>
  <el-tree
    :data="menus"
    :props="defaultProps"
    :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>
//方法集合
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取的菜单数据...", data.data);
        this.menus = data.data;
      });
    },
    append(data) {
      console.log("append", data);
    },

    remove(node, data) {
      console.log("remove", node, data);
    },
  },

测试
在这里插入图片描述

9、逻辑删除
  • postman发送请求测试
    在这里插入图片描述
  • 发现1432数据被删除了,mysql里面直接没有了

删除方法改进

1、修改CategoryController的删除方法

/**
     * 删除
     * @RequestBody:获取请求体,必须发送POST请求
     * SpringMVC自动将请求体的数据(json),转为对应的对象
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] catIds){
//		categoryService.removeByIds(Arrays.asList(catIds));

        //1、检查当前删除的菜单,是否被别的地方引用

        categoryService.removeMenuByIds(Arrays.asList(catIds));

        return R.ok();
    }

2、修改CategoryService接口

void removeMenuByIds(List<Long> asList);

3、修改Impl

@Override
    public void removeMenuByIds(List<Long> asList) {

        //TODO 1、检查当前删除的菜单,是否被其他地方引用

        //逻辑删除
        baseMapper.deleteBatchIds(asList);
    }

4、修改yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://169.254.249.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.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 #配置主键自增
      logic-delete-value: 1
      logic-not-delete-value: 0

server:
  port: 10000

logging:
  level: 
    com.wlq.gulimall: debug #打印日志

5、给been加上逻辑删除注解

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

6、postman测试

在这里插入图片描述

  • 发现show_status变为了0
    在这里插入图片描述

  • yml添加logging日志后发现:删除sql变成了update

logging:
  level:
    com.wlq.gulimall.product: debug
10、删除效果细化

1、修改method

remove(node, data) {  
      var ids = [data.catId];
      this.$http({
        url: this.$http.adornUrl("/product/category/delete"),
        method: "post",
        data: this.$http.adornData(ids, false),
      }).then(({ data }) => {
        console.log("删除成功");
        this.getMenus();  //菜单刷新
      });
      console.log("remove", node, data);
    },

2、访问并删除一个
在这里插入图片描述

  • 根据console里的 ID提示在数据库查看
    在这里插入图片描述

  • 发现165的status变为了0
    在这里插入图片描述

3、添加弹窗提示

remove(node, data) {
      var ids = [data.catId];
      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        this.$http({
          url: this.$http.adornUrl("/product/category/delete"),
          method: "post",
          data: this.$http.adornData(ids, false),
        }).then(({ data }) => {
          console.log("删除成功");
          this.getMenus(); //菜单刷新
        });
      }).catch(()=>{

      });

      console.log("remove", node, data);
    },

在这里插入图片描述
4、添加消息提示

remove(node, data) {
      var ids = [data.catId];
      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
        
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            this.$message({
              message: "菜单删除成功",
              type: "success",
            });
            this.getMenus(); //菜单刷新
          });
        })
        .catch(() => {
          this.$message({
          message: '您取消了删除',
          type: 'warning'
        });
        });

      console.log("remove", node, data);
    },
  • default-expanded-keys 默认展开的节点的 key 的数组
<template>
  <el-tree
    :data="menus"
    :props="defaultProps"
    :expand-on-click-node="false"
    show-checkbox
    node-key="catId"
    :default-expanded-keys="expandedKey"
  >
data() {
    return {
      menus: [],
      expandedKey:[],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
this.getMenus(); //菜单刷新
this.expandedKey = [node.parent.data.catId] //设置要默认展开的菜单
  • UPDATE pms_category SET show_status=1将数据还原
11、新增
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
      <el-form :model="category">
        <el-form-item label="分类名称">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="addCategory">确 定</el-button>
      </span>
    </el-dialog>
data() {
    return {
      category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
      menus: [],
      expandedKey: [],
      dialogVisible: false,
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
append(data) {
      console.log("append", data);
      this.dialogVisible = true;
      this.category.parentCid = data.catId;
      this.category.catLevel = data.catLevel * 1 + 1;
    },

method

//添加三级分类
    addCategory() {
      console.log("添加的三级分类数据", this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单保存成功",
          type: "success",
        });
        //关闭对话框
        this.dialogVisible = false;
        this.getMenus(); //菜单刷新
        this.expandedKey = [this.category.parentCid]; //设置要默认展开的菜单
      });
    },

测试:
在这里插入图片描述
在这里插入图片描述
mysql也写入了:
在这里插入图片描述

12、基本修改
<el-button
            v-if="node.level <= 2"
            type="text"
            size="mini"
            @click="edit(data)"
          >
            edit
          </el-button>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
      <el-form :model="category">
        <el-form-item label="分类名称">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="图标">
          <el-input v-model="category.icon" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="计量单位">
          <el-input
            v-model="category.productUnit"
            autocomplete="off"
          ></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitData">确 定</el-button>
      </span>
    </el-dialog>
  • method
//方法集合
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取的菜单数据...", data.data);
        this.menus = data.data;
      });
    },
    append(data) {
      console.log("append", data);
      this.dialogType = "add";
      this.title = "添加分类";
      this.dialogVisible = true;
      this.category.parentCid = data.catId;
      this.category.catLevel = data.catLevel * 1 + 1;
      this.category.catId = null;
      this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.sort = 0;
      this.category.showStatus = 1;
    },

    //修改
    edit(data) {
      console.log("要修改的数据", data);
      this.dialogType = "edit";
      this.title = "修改分类";
      this.dialogVisible = true;

      //发送请求获取当前节点最新的数据
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        //请求成功
        console.log("要回显的数据", data);
        this.category.name = data.data.name;
        this.category.catId = data.data.catId;
        this.category.icon = data.data.icon;
        this.category.productUnit = data.data.productUnit;
        this.category.parentCid = data.data.parentCid;
      });
    },

    //提交数据(双重绑定)
    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },

    //修改三级分类数据
    editCategory() {
      //解构
      var { catId, name, icon, productUnit } = this.category;
      // var data = {catId:catId,name:name,icon:icon,productUnit:productUnit};

      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData({ catId, name, icon, productUnit }, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单修改成功",
          type: "success",
        });
        this.dialogVisible = false; //关闭对话框
        this.getMenus(); //菜单刷新
        this.expandedKey = [this.category.parentCid]; //设置要默认展开的菜单
      });
    },

    //添加三级分类
    addCategory() {
      console.log("添加的三级分类数据", this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单保存成功",
          type: "success",
        });
        //关闭对话框
        this.dialogVisible = false;
        this.getMenus(); //菜单刷新
        this.expandedKey = [this.category.parentCid]; //设置要默认展开的菜单
      });
    },

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

      console.log("remove", node, data);
    },
  },
data() {
    return {
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        productUnit: "",
        icon: "",
      },
      title: "",
      dialogType: "", //edit,add
      menus: [],
      expandedKey: [],
      dialogVisible: false,
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
  • 获取最新的值

修改controller

/**
     * 信息
     */
    @RequestMapping("/info/{catId}")
    public R info(@PathVariable("catId") Long catId){
		CategoryEntity category = categoryService.getById(catId);

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

测试:

  • 修改:
    在这里插入图片描述
    在这里插入图片描述

  • 添加:
    在这里插入图片描述
    在这里插入图片描述

  • 删除
    在这里插入图片描述
    在这里插入图片描述

13、修改-拖拽效果

el-tree添加draggable 和 :allow-drop=“allowDrop”

<el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      draggable
      :allow-drop="allowDrop"
    >

data添加初始值

data() {
    return {

      maxLevel: 0,

      },
    };
  },

写allowDrop方法

allowDrop(draggingNode, dropNode, type) {
      //1、被拖动的当前节点以及所在的父节点总层数不能大于3

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

      //  this.maxLevel
      if (type == "inner") {
        return (deep + dropNode.level) <= 3;
      } else {
        return (deep + dropNode.parent.level) <= 3;
      }
    },
    countNodeLevel(node) {
      // 找到所有的子节点,求出最大深度
      if (node.children != null && node.children.length > 0) {
        for (let i = 0; i < node.children.length; i++) {
          if (node.children[i].catLevel > this.maxLevel) {
            this.maxLevel = node.children[i].catLevel;
          }
          this.countNodeLevel(node.children[i]);
        }
      }
    },
14、修改-拖拽数据收集

在这里插入图片描述

<el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      draggable
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
    >

method

	handleDrop(draggingNode, dropNode, dropType, ev) {
      console.log("handleDrop: ", draggingNode, dropNode, dropType);
      //1、当前节点最新的父节点id
      let pCid = 0;
      let siblings = null;
      if (dropType == "before" || dropType == "after") {
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;
        siblings = dropNode.parent.childNodes;
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
      }

      //2、当前拖拽节点的最新顺序
      for (let i = 0; i < siblings.length; i++) {
        if (siblings[i].data.catId == draggingNode.data.catId) {
          //如果遍历的是当前正在拖拽的节点
          let catLevel = draggingNode.level;
          if (siblings[i].level != draggingNode.level) {
            //当前节点的层级发生变化
            catLevel = siblings[i].level;
            //修改它子节点的层级
            this.updateChildNodeLevel(siblings[i]);
          }
          this.updateNodes.push({
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
          });
        } else {
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }

      //3、当前拖拽节点的最新层级
      console.log("updateNodes", this.updateNodes);
    },
    updateChildNodeLevel(node) {
      if (node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          var cNode = node.childNodes[i].data;
          this.updateNodes.push({
            catId: cNode.catId,
            catLevel: node.childNodes[i].level,
          });
          this.updateChildNodeLevel(node.childNodes[i]);
        }
      }
    },

data

data() {
    return {
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        productUnit: "",
        icon: "",
      },
      updateNodes: [],
      maxLevel: 0,
      title: "",
      dialogType: "", //edit,add
      menus: [],
      expandedKey: [],
      dialogVisible: false,
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
15、修改-拖拽功能完成(收集的数据发送给后端)
/**
     * 批量修改
     */
    @RequestMapping("/update/sort")
    public R updateSort(@RequestBody CategoryEntity[] category){
        categoryService.updateBatchById(Arrays.asList(category));

        return R.ok();
    }
  • postman 测试

  • [{"catId":1,"sort":10},{"catId":225,"catLevel":2}]
    在这里插入图片描述

  • 查看225数据

SELECT * FROM pms_category WHERE cat_id = 225
  • 发现level变为了2
    在这里插入图片描述

  • 1号的sort变为了10
    在这里插入图片描述

  • 修改method

	//3、当前拖拽节点的最新层级
      console.log("updateNodes", this.updateNodes);
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.updateNodes, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单顺序等修改成功",
          type: "success",
        });
        //刷新菜单
        this.getMenus();
        this.expandedKey = [pCid]; //设置要默认展开的菜单
        this.updateNodes = [];
        this.maxLevel = 0;
      });
16、添加按钮控制是否需要拖拽
  • 添加按钮
<div>
    <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽">
    </el-switch>
    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      :draggable="draggable"
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
    >
  • data添加
draggable: false,
17、批量保存,免得拖一下跟数据库交互一下
  • 添加一个按钮
<el-button @click="batchSave">批量保存</el-button>
  • data添加
pCid: [],
  • 在handleDrop方法后面添加this.pCid.push(pCid);
handleDrop(draggingNode, dropNode, dropType, ev) {
      console.log("handleDrop: ", draggingNode, dropNode, dropType);
      //1、当前节点最新的父节点id
      let pCid = 0;
      let siblings = null;
      if (dropType == "before" || dropType == "after") {
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;
        siblings = dropNode.parent.childNodes;
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
      }
      this.pCid.push(pCid);
  • 将前面数据发送给后端的代码剪切进batchSave方法里
batchSave() {
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.updateNodes, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单顺序等修改成功",
          type: "success",
        });
        //刷新菜单
        this.getMenus();
        this.expandedKey = this.pCid; //设置要默认展开的菜单
        this.updateNodes = [];
        this.maxLevel = 0;
        // this.pCid = 0;
      });
    },
  • 修改batchSave()方法
		//刷新菜单
        this.getMenus();
        this.expandedKey = this.pCid; //设置要默认展开的菜单
        this.updateNodes = [];
        this.maxLevel = 0;
        // this.pCid = 0;
  • 修改allowDrop (如果是拿数据库的level,进行批量保存之前数据并没有存进数据库,从而可能导致拖拽的节点判断错误)
this.countNodeLevel(draggingNode);
// 当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
countNodeLevel(node) {
      // 找到所有的子节点,求出最大深度
      if (node.childNodes != null && node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          if (node.childNodes[i].level > this.maxLevel) {
            this.maxLevel = node.childNodes[i].level;
          }
          this.countNodeLevel(node.childNodes[i]);
        }
      }
    },
18、删除-批量删除
  • 添加按钮
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      :draggable="draggable"
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
      ref="menuTree"
    >
  • getCheckedNodes
    在这里插入图片描述
  • method
//批量删除
    batchDelete() {
      let catIds = [];
      let checkedNodes = this.$refs.menuTree.getCheckedNodes();
      console.log("被选中的节点", checkedNodes);
      for (let i = 0; i < checkedNodes.length; i++) {
        catIds.push(checkedNodes[i].catId);
      }
      this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(catIds, false),
          }).then(({ data }) => {
            this.$message({
              message: "菜单批量删除成功",
              type: "success",
            });
            this.getMenus();
          });
        })
        .catch(() => {});
    },

品牌管理-使用逆向工程的前后端代码

1、新增品牌管理

在这里插入图片描述

  • 复制(逆向工程生成的)

在这里插入图片描述
在这里插入图片描述

  • 重启 npm run dev

在这里插入图片描述

  • 修改utils下的index.js的是否拥有权限
/**
 * 是否有权限
 * @param {*} key
 */
export function isAuth (key) {
  // return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
  return true
}
  • 测试新增,修改,删除
    在这里插入图片描述

2、效果优化和快速显示开关

  • 由于前段总报语法错误,其实没有错误
  • 将build\webpack.base.conf.js里的createLintingRule注释掉
const createLintingRule = () => ({
  // test: /\.(js|vue)$/,
  // loader: 'eslint-loader',
  // enforce: 'pre',
  // include: [resolve('src'), resolve('test')],
  // options: {
  //   formatter: require('eslint-friendly-formatter'),
  //   emitWarning: !config.dev.showEslintErrorsInOverlay
  // }
})
  • 重启后报错消失

  • 修改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-add-or-update.vue

<el-form-item label="显示状态" prop="showStatus">
        <el-switch
            v-model="dataForm.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
          >
          </el-switch>
      </el-form-item>
  • 监听开关状态

在这里插入图片描述

  • brand.vue修改switch,添加监听事件
<el-switch
            v-model="scope.row.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            @change="updateBrandStatus(scope.row)"
          >
          </el-switch>
  • 编写method,实现updateBrandStatus
//开关监听
    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: brandId, showStatus: showStatus ? 1 : 0 },
          false
        ),
      }).then(({ data }) => {
        this.$message({
          message: "状态更新成功",
          type: "success",
        });
      });
    },
  • 由于switch只识别true和false,修改相对应的值为1和0
<el-switch
            v-model="scope.row.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            :active-value="1"
            :inactive-value="0"
            @change="updateBrandStatus(scope.row)"
          >
  • 测试
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3、文件上传

1、云储存开通与使用

开通对象存储OSS:https://common-buy.aliyun.com/?spm=5176.7933691.J_5253785160.2.61b84c59Fi9NqM&commodityCode=oss

1、创建bucket

在这里插入图片描述

  • 上传的文件可通过URL直接访问
    在这里插入图片描述

4、阿里云对象储存方式

在这里插入图片描述

安装SDK

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

简单上传

文档:https://help.aliyun.com/document_detail/84781.html?spm=a2c4g.11186623.6.936.a01826fd211WcS

  • 查看endpointID
    在这里插入图片描述

  • 添加子账户
    在这里插入图片描述
    选择使用子用户,创建用户
    在这里插入图片描述

  • 添加权限
    在这里插入图片描述

  • 复制
    在这里插入图片描述

  • 写test(product)

@Test
    public void testUpload() throws FileNotFoundException {
        // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
        String endpoint = "oss-cn-shenzhen.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "LTAI5tJjtcA2vHdjbu97jWDP";
        String accessKeySecret = "gKo9taMkPfYpx5H22TanJjq7BqbNNd";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Desktop\\picture\\dlam.png");
        // 填写Bucket名称和Object完整路径。Object完整路径中不能包含Bucket名称。
        ossClient.putObject("gulimall-wlq", "dlam.png", inputStream);

        // 关闭OSSClient。
        ossClient.shutdown();

        System.out.println("上传完成...");
    }
  • 运行
    在这里插入图片描述

每次都需要一对配置很麻烦

导入OSS-Starter

  • 将SDK注释
<!--<dependency>-->
            <!--<groupId>com.aliyun.oss</groupId>-->
            <!--<artifactId>aliyun-sdk-oss</artifactId>-->
            <!--<version>3.10.2</version>-->
        <!--</dependency>-->
  • 导入OSS-Starter
<!-- oss-starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
        </dependency>
  • 修改product的YML
spring:
  cloud:
    alicloud:
      access-key: LTAI5tJjtcA2vHdjbu97jWDP
      secret-key: gKo9taMkPfYpx5H22TanJjq7BqbNNd
      oss:
        endpoint: oss-cn-shenzhen.aliyuncs.com
  • test
@Autowired
    OSSClient ossClient;

    @Test
    public void testUpload2() throws FileNotFoundException {

        InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Desktop\\picture\\qqfc4.jpeg");

        ossClient.putObject("gulimall-wlq", "qqfc4.jpeg", inputStream);

        ossClient.shutdown();

        System.out.println("上传完成...");
    }
  • 这里如果报空指针异常 在class上添加 @RunWith(SpringRunner.class)
  • 测试
    在这里插入图片描述
  • 总结
  1. 引入oss-starter
  2. 配置key,endpoint相关信 息即可
  3. 使用ossclient进行相关操作

新建module(gulimall-third-party)

  • 选择web和openfeign
    在这里插入图片描述
  • xml

将common里的oss-starter引用删掉,并放在这里gulimall-third-party

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

		<dependency>
            <groupId>com.wlq.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>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  • 创建命名空间
    在这里插入图片描述

  • 将product里yml写的oss配置删除,并在配置中心创建oss.yml
    在这里插入图片描述

  • bootstrap.properties

spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=b78b2e2a-7249-47fa-9968-104cf4646678
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
  • YML
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alicloud:
     access-key: LTAI5tJjtcA2vHdjbu97jWDP
     secret-key: gKo9taMkPfYpx5H22TanJjq7BqbNNd
     oss:
        endpoint: oss-cn-shenzhen.aliyuncs.com
        bucket: gulimall-wlq

  application:
    name: gulimall-third-party

server:
  port: 30000
  • 主启动添加:@EnableDiscoveryClient,并启动

  • 测试test

	@Autowired
    OSSClient ossClient;

    @Test
    public void testUpload2() throws FileNotFoundException {

        InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Desktop\\picture\\qqfc5.jpeg");

        ossClient.putObject("gulimall-wlq", "qqfc5.jpeg", inputStream);

        ossClient.shutdown();

        System.out.println("上传完成...");
    }
  • 查看
    在这里插入图片描述

服务端签名后直传

官方文档:https://help.aliyun.com/document_detail/31926.html?spm=a2c4g.11186623.6.1733.4d8914a0ClFUYU

应用服务器核心代码解析

应用服务器源码包含了签名直传服务和上传回调服务两个功能。

  • 签名直传服务
  • 签名直传服务响应客户端发送给应用服务器的GET消息,代码片段如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。
        String accessKey = "<yourAccessKeySecret>"; // 请填写您的AccessKeySecret。
        String endpoint = "oss-cn-hangzhou.aliyuncs.com"; // 请填写您的 endpoint。
        String bucket = "bucket-name"; // 请填写您的 bucketname 。
        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
        // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
        String callbackUrl = "http://88.88.88.88:8888";
        String dir = "user-dir-prefix/"; // 用户上传文件时指定的前缀。

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

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

            Map<String, String> respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));

            JSONObject jasonCallback = new JSONObject();
            jasonCallback.put("callbackUrl", callbackUrl);
            jasonCallback.put("callbackBody",
                    "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
            jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
            String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
            respMap.put("callback", base64CallbackBody);

            JSONObject ja1 = JSONObject.fromObject(respMap);
            // System.out.println(ja1.toString());
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET, POST");
            response(request, response, ja1.toString());

        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        } finally { 
            ossClient.shutdown();
        }
    }
OSS获取服务端签名
  • 新建controller:OssController(gulimall-third-party)
@RestController
public class OssController {

    @Autowired
    OSSClient 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(){

        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.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);
            // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

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

            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));


        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        } finally {
            ossClient.shutdown();
        }
        return respMap;
    }
}
  • 启动

访问:http://localhost:30000/oss/policy
在这里插入图片描述

  • 修改网关添加映射

YML

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

http://localhost:88/api/thirdparty/oss/policy
在这里插入图片描述

OSS前后联调测试上传
  • 将upload复制进components
    在这里插入图片描述

  • 复制阿里云的bucket域名
    在这里插入图片描述

  • 修改upload里的vue

<el-upload
      action="http://gulimall-wlq.oss-cn-shenzhen.aliyuncs.com"
    >
  • 修改product\brand-add-or-update.vue
<script>
import SingleUpload from "@/components/upload/singleUpload"
</srcipt>
  • 修改singleupload.vue
beforeUpload(file) {
        let _self = this;
        return new Promise((resolve, reject) => {
          policy().then(response => {
            console.log("响应的数据",response);
            _self.dataObj.policy = response.data.policy;
            _self.dataObj.signature = response.data.signature;
            _self.dataObj.ossaccessKeyId = response.data.accessid;
            _self.dataObj.key = response.data.dir +getUUID()+'_${filename}';
            _self.dataObj.dir = response.data.dir;
            _self.dataObj.host = response.data.host;
            console.log("响应的数据222",_self.dataObj);
            resolve(true)
          }).catch(err => {
            reject(false)
          })
        })
      },
  • 添加components:{SingleUpload},
    在这里插入图片描述
<el-form-item label="品牌logo地址" prop="logo">
        <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> -->
        <single-upload v-model="dataForm.logo"></single-upload>
      </el-form-item>
  • 修改OssController
@RequestMapping("oss/policy")
    public R policy(){
	    ....
        return R.ok().put("data",respMap);
    }
表单效验(品牌新增功能调试)
  • 修改brand-add-or-update.vue
<el-form-item label="显示状态" prop="showStatus">
        <el-switch
            v-model="dataForm.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            :active-value="1"
            :inactive-value="0"
          >
          </el-switch>
      </el-form-item>
  • 测试新增,新增一个小米
    在这里插入图片描述
  • 添加自定义列和图片显示
<el-table-column
        prop="logo"
        header-align="center"
        align="center"
        label="品牌logo地址"
      >
        <template slot-scope="scope">
          <el-image
            style="width: 100px; height: 80px"
            :src="scope.row.logo"
            fit="contain"
          ></el-image>
        </template>
      </el-table-column>
  • 发现报错不能找到image组件,去element官网修改src\element-ui\index.js
/**
 * UI组件, 统一使用饿了么桌面端组件库(https://github.com/ElemeFE/element)
 *
 * 使用:
 *  1. 项目中需要的组件进行释放(解开注释)
 *
 * 注意:
 *  1. 打包只会包含释放(解开注释)的组件, 减少打包文件大小
 */
import Vue from 'vue'
import {
  Pagination,
  Dialog,
  Autocomplete,
  Dropdown,
  DropdownMenu,
  DropdownItem,
  Menu,
  Submenu,
  MenuItem,
  MenuItemGroup,
  Input,
  InputNumber,
  Radio,
  RadioGroup,
  RadioButton,
  Checkbox,
  CheckboxButton,
  CheckboxGroup,
  Switch,
  Select,
  Option,
  OptionGroup,
  Button,
  ButtonGroup,
  Table,
  TableColumn,
  DatePicker,
  TimeSelect,
  TimePicker,
  Popover,
  Tooltip,
  Breadcrumb,
  BreadcrumbItem,
  Form,
  FormItem,
  Tabs,
  TabPane,
  Tag,
  Tree,
  Alert,
  Slider,
  Icon,
  Row,
  Col,
  Upload,
  Progress,
  Spinner,
  Badge,
  Card,
  Rate,
  Steps,
  Step,
  Carousel,
  CarouselItem,
  Collapse,
  CollapseItem,
  Cascader,
  ColorPicker,
  Transfer,
  Container,
  Header,
  Aside,
  Main,
  Footer,
  Timeline,
  TimelineItem,
  Link,
  Divider,
  Image,
  Calendar,
  Loading,
  MessageBox,
  Message,
  Notification
} from 'element-ui';

Vue.use(Pagination);
Vue.use(Dialog);
Vue.use(Autocomplete);
Vue.use(Dropdown);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Menu);
Vue.use(Submenu);
Vue.use(MenuItem);
Vue.use(MenuItemGroup);
Vue.use(Input);
Vue.use(InputNumber);
Vue.use(Radio);
Vue.use(RadioGroup);
Vue.use(RadioButton);
Vue.use(Checkbox);
Vue.use(CheckboxButton);
Vue.use(CheckboxGroup);
Vue.use(Switch);
Vue.use(Select);
Vue.use(Option);
Vue.use(OptionGroup);
Vue.use(Button);
Vue.use(ButtonGroup);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(DatePicker);
Vue.use(TimeSelect);
Vue.use(TimePicker);
Vue.use(Popover);
Vue.use(Tooltip);
Vue.use(Breadcrumb);
Vue.use(BreadcrumbItem);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Tabs);
Vue.use(TabPane);
Vue.use(Tag);
Vue.use(Tree);
Vue.use(Alert);
Vue.use(Slider);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Upload);
Vue.use(Progress);
Vue.use(Spinner);
Vue.use(Badge);
Vue.use(Card);
Vue.use(Rate);
Vue.use(Steps);
Vue.use(Step);
Vue.use(Carousel);
Vue.use(CarouselItem);
Vue.use(Collapse);
Vue.use(CollapseItem);
Vue.use(Cascader);
Vue.use(ColorPicker);
Vue.use(Transfer);
Vue.use(Container);
Vue.use(Header);
Vue.use(Aside);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Timeline);
Vue.use(TimelineItem);
Vue.use(Link);
Vue.use(Divider);
Vue.use(Image);
Vue.use(Calendar);


Vue.use(Loading.directive)

Vue.prototype.$loading = Loading.service
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$prompt = MessageBox.prompt
Vue.prototype.$notify = Notification
Vue.prototype.$message = Message

Vue.prototype.$ELEMENT = { size: 'medium' }

  • 再次测试,发现图片显示空白
    在这里插入图片描述
  • 修改image
<template slot-scope="scope">
          <!-- <el-image
            style="width: 100px; height: 80px"
            :src="scope.row.logo"
            fit="contain"
          ></el-image> -->
          <img :src="scope.row.logo" style="width: 100px; height: 80px">
        </template>
  • 成功显示
    在这里插入图片描述
  • 修改排序,添加number
<el-form-item label="排序" prop="sort">
        <el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
      </el-form-item>
  • 修改dataRule(brand-add-or-update.vue)
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",
          },
        ],

data

dataForm: {
        brandId: 0,
        name: "",
        logo: "",
        descript: "",
        showStatus: 1,
        firstLetter: "",
        sort: 0,
      },
  • 测试
    在这里插入图片描述
后端也要进行效验(JSR303)
1、品牌添加校验
  • 给需要效验的数据的been添加注解,比如这里是品牌的效验,在brandentity上添加注解
  • 注意:这里要使用注解需要引入validation-api
  • entity:@NotBlank 不能为空
/**
	 * 品牌名
	 */
	@NotBlank
	private String name;
  • controller,@Valid:告诉springmvc这个字段需要效验
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand){
        brandService.save(brand);

        return R.ok();
    }
  • 使用postman跳过前端效验进行测试
    在这里插入图片描述

  • 直接badrequest
    在这里插入图片描述

  • 修改message提示内容

/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名必须提交")
	private String name;
2、获取校验结果

给校验的bean后紧跟一个BindingResult, 就可以获取到校验的结果

  • controller
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
        if (result.hasErrors()){
            Map<String,String> map = new HashMap<>();
            //1、获取效验的错误
            result.getFieldErrors().forEach((item)->{
                //FieldError 获取到错误提示
                String message = item.getDefaultMessage();
                //获取错误属性的名字
                String field = item.getField();
                map.put(field,message);
            });

            return R.error(400,"提交的数据不合法").put("data",map);

        }else{
            brandService.save(brand);
        }


        return R.ok();
    }
  • postman 测试
    在这里插入图片描述
3、同理修改其它几个
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@TableId
	private Long brandId;

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

	/**
	 * 品牌logo地址
	 */
	@NotEmpty
	@URL(message = "logo必须是一个合法的URL地址")
	private String logo;

	/**
	 * 介绍
	 */
	private String descript;

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	private Integer showStatus;

	/**
	 * 检索首字母
	 * @Pattern:自定义
	 */
	@NotEmpty
	@Pattern(regexp = "/^[a-zA-z]$/",message = "检索首字母必须是一个字母")
	private String firstLetter;

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


}
  • postman测试
    在这里插入图片描述
    问题:这样每个业务里的新增修改都要加入这些效验功能,代码重复
4、统一异常处理 ControllerAdvice
  • 将上面写的BindingResult注释掉
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand /*,BindingResult result*/){
//        if (result.hasErrors()){
//            Map<String,String> map = new HashMap<>();
//            //1、获取效验的错误
//            result.getFieldErrors().forEach((item)->{
//                //FieldError 获取到错误提示
//                String message = item.getDefaultMessage();
//                //获取错误属性的名字
//                String field = item.getField();
//                map.put(field,message);
//            });
//
//            return R.error(400,"提交的数据不合法").put("data",map);
//
//        }else{
//            brandService.save(brand);
//        }

        brandService.save(brand);

        return R.ok();
    }
  • 新建exception.GulimallExceptionControllerAdvice.java
/**
 * 集中处理所有异常
 */
//@ControllerAdvice(basePackages = "com.wlq.gulimall.product.controller")
//@ResponseBody
@Slf4j
@RestControllerAdvice(basePackages = "com.wlq.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value = Exception.class)
    public R handleVaildException(Exception e){
        log.error("数据效验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
        return R.error();
    }
}
  • postman测试
    在这里插入图片描述
  • 查看控制台
ERROR 4780 --- [io-10000-exec-1] .g.p.e.GulimallExceptionControllerAdvice : 数据效验出现问题
arguments []; default message [sort]]; default message [不能为null]] ,
异常类型:class.org.springframework.web.bind.MethodArgumentNotValidException
  • 根据控制台提示的异常类型修改controller:class.org.springframework.web.bind.MethodArgumentNotValidException
/**
 * 集中处理所有异常
 */
//@ControllerAdvice(basePackages = "com.wlq.gulimall.product.controller")
//@ResponseBody
@Slf4j
@RestControllerAdvice(basePackages = "com.wlq.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(400,"数据校验出现问题").put("data",errorMap);
    }
}
  • postman测试
    在这里插入图片描述
  • 还能写更多异常处理,建议在项目里的异常都抛出器,都用ControllerAdvice来感知处理
/**
 * 集中处理所有异常
 */
//@ControllerAdvice(basePackages = "com.wlq.gulimall.product.controller")
//@ResponseBody
@Slf4j
@RestControllerAdvice(basePackages = "com.wlq.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(400,"数据校验出现问题").put("data",errorMap);
    }

    //其它异常,如果异常能匹配的上面的精确异常就走上面的,否则就走这个最大的异常处理
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        return R.error();
    }
}
5、状态码推荐这样做
  • 错误码和错误信息定义类
    • 1.错误码定义规则为5位数字
    • 2.前两位表示业务场景,最后三位表示错误码。例如: 10001。10:通用001:系 统未知异常
    • 3.维护错误码后需要维护错误描述,将他们定义为枚举形式
  • 错误码列表:
    • 10 :通用
      • 001 :参数格式校验
    • 11 :商品
    • 12 :订单
    • 13 :购物车
    • 14 :物流

common创建exception,再创建一个枚举BizCodeEnume

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;
    }
}
  • 修改GulimallExceptionControllerAdvice的错误码和错误码信息
//数据校验异常处理
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e){
        ...
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

    //其它异常,如果异常能匹配的上面的精确异常就走上面的,否则就走这个最大的异常处理
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
  • postman测试
    在这里插入图片描述
6、分组校验(多场景复制校验)

common新建valid,再新建接口AddGroup,UpdateGroup(空接口)

新增
  • 修改BrandEntity
// @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
//给校验注解标注什么情况需要进行校验

/**
	 * 品牌id
	 */
	@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;

	/**
	 * 品牌logo地址
	 */
	@NotEmpty(groups = {AddGroup.class})
	@URL(message = "logo必须是一个合法的URL地址",groups = {AddGroup.class,UpdateGroup.class})
	private String logo;
  • 修改controller:@Validated({AddGroup.class})指定一个或者多个分组,就会安装这个分组进行校验
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
        brandService.save(brand);

        return R.ok();
    }
  • postman测试
    在这里插入图片描述
    在这里插入图片描述
修改
  • 修改controller
/**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
		brandService.updateById(brand);

        return R.ok();
    }
  • entity
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@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;

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

	/**
	 * 介绍
	 */
	private String descript;

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	private Integer showStatus;

	/**
	 * 检索首字母
	 * @Pattern:自定义
	 */
	@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;

}
  • 注意:默认没有指定分组的校验注解@NotBlank,在分组校验情况下不生效,只会在@Validated生效;
7、自定义效验

1)、编写一个自定义的校验注解
2)、编写一个自定义的校验器
3)、关联自定义的校验器和自定义的校脸注解

  • entity
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@ListValue(vals={0,1},groups = {AddGroup.class})
	private Integer showStatus;
<dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
            <scope>compile</scope>
        </dependency>
  • 创建com.wlq.common.valid.ListValue(自定义的校验注解)
@Documented
@Constraint(
        validatedBy = {ListValueConstraintValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {

    String message() default "{com.wlq.common.valid.ListValue.message}";

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

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

    int[] vals() default {};

}
  • 创建ValidationMessages.properties
com.wlq.common.valid.ListValue.message=必须提交指定的值
  • 创建ListValueConstraintValidator(自定义的校验器)
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();

    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {

        int [] vals = constraintAnnotation.vals();
        for (int val : vals){
            set.add(val);
        }

    }

    /**
     * 判断是否校验成功
     * @param value
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {

        return set.contains(value);
    }
}
  • 测试
    在这里插入图片描述
  • 测试新增
    在这里插入图片描述
8、完善
  • 修改显示状态出现效验异常
    在这里插入图片描述
  • 错误分析
    @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})

品牌名的非空判断里面带有UpdateGroup.class,所有在修改状态显示的时候由于其也携带UpdateGroup.class,所有也会一并校验品牌名是否为空。解决:我们单独写一个update来处理

  • 修改BrandController,添加一个修改显示状态的方法,不再使用同一个update
	/**
     * 修改状态显示
     */
    @RequestMapping("/update/status")
    public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
        brandService.updateById(brand);

        return R.ok();
    }
  • common的valid新增UpdateStatusGroup接口
    在这里插入图片描述

  • 修改entity

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
	@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
	private Integer showStatus;
  • 修改brand.vue
updateBrandStatus(data) {
      console.log("最新信息:", data);
      //解构
      let { brandId, showStatus } = data;
      //发送请求,修改状态
      this.$http({
        url: this.$http.adornUrl("/product/brand/update/status"),
        method: "post",
        data: this.$http.adornData(
          { brandId: brandId, showStatus: showStatus ? 1 : 0 },
          false
        ),
      }).then(({ data }) => {
        this.$message({
          message: "状态更新成功",
          type: "success",
        });
      });
    },
  • 报错消失
    在这里插入图片描述

SPU&SKU

SPU: Standard Product Unit (标准化产品单元)

是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了个产品的特性。

SKU: Stock Keeping Unit (库存量单位)

即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU这是对于大型连锁超市DC (配送中心)物流管理的一一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。

平台属性:基本属性、规格参数、销售属性

每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性:

  • 属性是以三级分类组织起来的
  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的分组
  • 属性的分组也是以三级分类组织起来的
  • 属性名确定的,但是值是每一个商品不同来决定的

在这里插入图片描述

在这里插入图片描述

  • 在gulimall_admin下运行sql文件:sys_menus.sql
    在这里插入图片描述

1、基本属性(属性分组)

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

前端组件抽取
  • 新建views\modules\common\category.vue,用于公共使用
    在这里插入图片描述

  • 修改common下的category.vue

<template>
  <el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree">
  </el-tree>
</template>
data() {
    //这里存放数据
    return {
      menus: [],
      expandedKey: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
methods: {
      //获取菜单数据
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取的菜单数据...", data.data);
        this.menus = data.data;
      });
    },
  },
created() {
      this.getMenus();
  },
  • 新建attrgroup.vue
    在这里插入图片描述
  • 使用分栏间隔布局
    在这里插入图片描述
  • 修改product下的attrgroup.vue
<template>
  <el-row :gutter="20">
    <el-col :span="6">菜单</el-col>
    <el-col :span="18">表格</el-col>
  </el-row>
</template>
  • 导入common下的category.vue
import Category from '../common/category'

export default {
  //import引入的组件需要注入到对象中才能使用
  //组件名:组件对象,相同可以写一个
  components: {Category:Category},
<template>
  <el-row :gutter="20">
    <el-col :span="6">
        <category></category>
    </el-col>
    <el-col :span="18">
        表格
    </el-col>
  </el-row>
</template>
  • 查看
    在这里插入图片描述

  • 复制逆向工程生成的代码
    在这里插入图片描述

  • 复制

  • 复制template下的div放到表格里面
    在这里插入图片描述

  • 复制script全部

  • 复制attrgroup-add-or-update.vue到product

  • 查看效果
    在这里插入图片描述

父子组件交互

父子组件传递数据

1、子组件给父组件传递数据,使用事件机制,子组件给父组件发送一个事件,携带上数据。

//向父组件发送事件
        this.$emit("事件名",携带的数据...);
  • 使用node-click事件
    在这里插入图片描述
  • 修改common下的category.vue
<template>
  <el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeclick">
  </el-tree>
</template>
  • 添加nodeclick方法
//回调
    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>

method

	//感知树节点被点击
      treenodeclick(data,node,component){
          console.log("attrgroup感知到categrop的节点被点击",data,node,component);
          console.log("刚才被点击的是:"+data.name,"id为:"+data.catId);
      },
  • 测试
    在这里插入图片描述
获取分类属性分组
  • 修改AttrGroupController
/**
     * 列表
     */
    @RequestMapping("/list/{catelogId}")
    public R list(@RequestParam Map<String, Object> params,
                  @PathVariable Long catelogId){
//        PageUtils page = attrGroupService.queryPage(params);

        PageUtils page = attrGroupService.queryPage(params,catelogId);

        return R.ok().put("page", page);
    }
  • service添加方法
public interface AttrGroupService extends IService<AttrGroupEntity> {

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

    PageUtils queryPage(Map<String, Object> params, Long catelogId);
}
  • 添加方法的实现
@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 psm_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
            QueryWrapper<AttrGroupEntity> wrapper =new QueryWrapper<AttrGroupEntity>().eq("catelog_id",catelogId);
            if (!StringUtils.isEmpty(key)){
                wrapper.and((obj)->{
                    obj.eq("attr_group_id",key).or().like("attr_group_name",key);
                });
            }

            IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),wrapper);
            return new PageUtils(page);
        }
    }
  • postman测试
    在这里插入图片描述
  • 查看debug日志打印的SQL语句
==>  Preparing: SELECT attr_group_id,icon,catelog_id,sort,descript,attr_group_name FROM pms_attr_group WHERE (catelog_id = ? AND ( (attr_group_id = ? OR attr_group_name LIKE ?) )) 
2021-06-10 16:16:47.379 DEBUG 12808 --- [io-10000-exec-1] c.w.g.p.dao.AttrGroupDao.selectPage      : ==> Parameters: 1(Long), aa(String), %aa%(String)
2021-06-10 16:16:47.398 DEBUG 12808 --- [io-10000-exec-1] c.w.g.p.dao.AttrGroupDao.selectPage      : <==      Total: 0

后台测试没问题了

  • 调整前端

date

catId: 0,
  • 修改URL
url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
//感知树节点被点击
    treenodeclick(data, node, component) {
      console.log("attrgroup感知到categrop的节点被点击", data, node, component);
      console.log("刚才被点击的是:" + data.name, "id为:" + data.catId);
      if(node.level == 3){
        this.catId = data.catId;
        this.getDataList();//重新查询
      }

    },
  • 测试
    在这里插入图片描述
  • 修改查到的231数据
    在这里插入图片描述
  • 再次点击231移动电源
    在这里插入图片描述
  • 测试查询根据id和组名
    在这里插入图片描述
    在这里插入图片描述
分组新增&级联选择器
  • 级联选择器

在这里插入图片描述

  • 修改attrgroup-add-or-update.vue
<el-form-item label="所属分类id" prop="catelogId">
        <!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input> @change="handleChange"-->
        <el-cascader
          v-model="dataForm.catelogIds"
          :options="categorys"
          :props="props"	//指定选项的值为选项对象的某个属性值
        ></el-cascader>
      </el-form-item>
  • date
visible: false,
      props: {
        value:"catId",
        label:"name",
        children:"children"
      },
      categorys: [],
      dataForm: {
        attrGroupId: 0,
        attrGroupName: "",
        sort: "",
        descript: "",
        icon: "",
        catelogIds: [],
        catelogId: 0,
      },
  • method
methods: {
	//获取菜单
    getCategorys() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取的菜单数据...", data.data);
        this.menus = data.data;
      });
    },
dataFormSubmit() {
			  ....
              catelogId: this.dataForm.catelogIds[this.dataForm.catelogIds.length-1],
              ....
    },
  • created
created(){
    this.getCategorys();
  }
  • 查看效果
    在这里插入图片描述
  • 修改categoryEntity,空数组就不显示
	//这个字段不为空的时候返回才带数据
	@JsonInclude(JsonInclude.Include.NON_EMPTY)
	@TableField(exist = false)
	private List<CategoryEntity> children;
  • 测试,没有出现空数组
    在这里插入图片描述
    成功
    在这里插入图片描述
  • 测试新增
    在这里插入图片描述
    在这里插入图片描述
  • 分组修改,没有回现
    在这里插入图片描述
  • 修改init
init(id) {
    		.....
          }).then(({ data }) => {
            if (data && data.code === 0) {
				....
              //查出catelogId完整路径
              this.dataForm.catelogPath = data.attrGroup.catelogPath;
            }

    },

在这里插入图片描述

  • 修改AttrGroupEntity
@TableField(exist = false)//在数据库中不存在
	private Long[] catelogPath;
  • 修改controller
	@Autowired
    private CategoryService categoryService;
    
	/**
     * 信息
     */
    @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);
    }
  • service
public interface CategoryService extends IService<CategoryEntity> {

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

    List<CategoryEntity> listWithTree();

    void removeMenuByIds(List<Long> asList);

    /**
     * 找到catelogid的完整路径
     * [父/子/孙]
     * @param catelogId
     * @return
     */
    Long[] findCatelogPath(Long catelogId);
}
  • 实现
//[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;
    }
  • test
@Autowired
    CategoryService categoryService;

    public void testFindPath(){
        Long[] catelogPath = categoryService.findCatelogPath(225L);
        log.info("完整路径:{}", Arrays.asList(catelogPath));
    }

在这里插入图片描述

  • 查看前端
    在这里插入图片描述

  • 完善
    问题:新增没有回现
    在这里插入图片描述

  • closed
    在这里插入图片描述

<el-dialog
    :title="!dataForm.attrGroupId ? '新增' : '修改'"
    :close-on-click-modal="false"
    :visible.sync="visible"
    @closed="dialogClose"
  >
methods: {
    dialogClose(){
      this.dataForm.catelogPath = [];
    },
  • 查看
    在这里插入图片描述
  • 完善,可搜索的级联选择
<el-cascader
          v-model="dataForm.catelogPath"
          :options="categorys"
          :props="props"
          filterable
          placeholder="试试搜索:手机"
        ></el-cascader>
  • 查看
    在这里插入图片描述

2、品牌管理优化

品牌分类关联与级联更新

1、分页插件&模糊查询
  • product下新建config.MyBatisConfig

官网:https://mp.baomidou.com/guide/page.html

@Configuration
@EnableTransactionManagement    //开启事务
@MapperScan("com.wlq.gulimall.product.dao")
public class MyBatisConfig {
    //引入分页插件
    // 旧版
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
         paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
         paginationInterceptor.setLimit(1500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
}
  • 模糊查询

BrandServiceImpl

@Slf4j
@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        //获取key
        String key = (String) params.get("key");

//        System.out.println("key:"+key);


        //检索条件
        QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
        if (!StringUtils.isEmpty(key)){
            //非空就要进行模糊检索
            queryWrapper.eq("brand_id",key).or().like("name",key);
        }
        IPage<BrandEntity> page = this.page(
                new Query<BrandEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

}
  • 测试
    在这里插入图片描述
  • 复制
    在这里插入图片描述
    覆盖src\views\modules
2、关联分类
  • 修改CategoryBrandRelationController
/**
     * 获取当前品牌关联的所有分类列表
     */
    @GetMapping("/catelog/list")
    public R cateloglist(@RequestParam("brandId") Long brandId){
        List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(
                new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));

        return R.ok().put("data", data);
    }
  • 修改save方法
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
		categoryBrandRelationService.saveDetail(categoryBrandRelation);

        return R.ok();
    }

CategoryBrandRelationService

public interface CategoryBrandRelationService extends IService<CategoryBrandRelationEntity> {

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

    void saveDetail(CategoryBrandRelationEntity categoryBrandRelation);
}

CategoryBrandRelationServiceImpl

	@Autowired
    BrandDao brandDao;

    @Autowired
    CategoryDao categoryDao;

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

        //1、查询详细名字
        BrandEntity brandEntity = brandDao.selectById(brandId); //根据品牌id查到品牌详情
        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);

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

        this.save(categoryBrandRelation);
    }
  • 测试
    在这里插入图片描述
    在这里插入图片描述
3、级联更新(数据一致性)

在这里插入图片描述
在这里插入图片描述

如果别处修改了品牌命名或目录名字,每个表应该也会同步修改

1、brand修改(使用UpdateWrapper)
  • 修改brandcontroller的update方法
/**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
		brandService.updateDatail(brand);

        return R.ok();
    }
  • brandServiceImpl
	@Autowired
    CategoryBrandRelationService categoryBrandRelationService;
    
	@Override
    public void updateDatail(BrandEntity brand) {
        //保证冗余字段数据一致
        this.updateById(brand);
        if (!StringUtils.isEmpty(brand.getName())){
            //同步更新其他关联表中的数据
            categoryBrandRelationService.updateById(brand.getBrandId(),brand.getName());

            //TODO 更新其它关联
        }
    }
  • 修改CategoryBrandRelationServiceImpl
@Override
    public void updateById(Long brandId, String name) {
        CategoryBrandRelationEntity relationEntity = new CategoryBrandRelationEntity;
        relationEntity.setBrandId(brandId);
        relationEntity.setBrandName(name);
        this.update(relationEntity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
    }
  • 测试
    在这里插入图片描述
    在这里插入图片描述
2、Category修改(使用sql语句)
  • CategoryController
/**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@RequestBody CategoryEntity category){
		categoryService.updateCascade(category);

        return R.ok();
    }
  • CategoryService添加实现
void updateCascade(CategoryEntity category);
  • CategoryServiceImpl
	@Autowired
    CategoryBrandRelationService categoryBrandRelationService;
    
	/**
     * 级联更新所有关联的数据
     * @param category
     */
    @Override
    @Transactional
    public void updateCascade(CategoryEntity category) {
        //更新自己
        this.updateById(category);
        //更新关联表
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }
  • CategoryBrandRelationService
void updateCategory(Long catId, String name);
  • CategoryBrandRelationServiceImpl
@Override
    public void updateCategory(Long catId, String name) {
        this.baseMapper.updateCategory(catId,name);
    }
  • CategoryBrandRelationDao
@Mapper
public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {

    //如果以后mybatis声明的map接口有两个参数,一定要使用@Param为每一个参数起一个自己的名字,否则你想要获取参数的值就很麻烦
    void updateCategory(@Param("catId") Long catId, @Param("name") String name);
}
  • mybatis-plus使用alt+enter自动生成sql
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.wlq.gulimall.product.dao.CategoryBrandRelationDao">

	<!-- 可根据自己的需求,是否要使用 -->
    <resultMap type="com.wlq.gulimall.product.entity.CategoryBrandRelationEntity" id="categoryBrandRelationMap">
        <result property="id" column="id"/>
        <result property="brandId" column="brand_id"/>
        <result property="catelogId" column="catelog_id"/>
        <result property="brandName" column="brand_name"/>
        <result property="catelogName" column="catelog_name"/>
    </resultMap>
    <update id="updateCategory">
        UPDATE 'pms_category_brand_relation' SET catelog_name=#{catId} WHERE catelog_id=#{name}
    </update>


</mapper>
  • 测试
    在这里插入图片描述
    在这里插入图片描述
  • 在BrandServiceImpl的updateDatail和CategoryServiceImpl的updateCascade方法前面加上@Transactional事务注解

3、规格参数

规格参数新增与VO

1、新增
1、给查询全部添加模糊查询
  • 修改AttrGroupController
@Override
    public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
        String key = (String) params.get("key");
        //select * from psm_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
        QueryWrapper<AttrGroupEntity> wrapper =new QueryWrapper<AttrGroupEntity>();
        if (!StringUtils.isEmpty(key)){
            wrapper.and((obj)->{
                obj.eq("attr_group_id",key).or().like("attr_group_name",key);
            });
        }
        if (catelogId == 0){
            IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
                    wrapper);
            return new PageUtils(page);
        }else {
            wrapper.eq("catelog_id",catelogId);

            IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),wrapper);
            return new PageUtils(page);
        }
    }
2、规格参数
  • 测试新增
    在这里插入图片描述
  • 查看数据库

pms_attr_attrgroup_relation表没有对应,需要添加private Long attrGroupId;规范使用Vo

2、Vo
1、object划分

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

http://localhost:88/api/product/attr/base/list/0t=1588731762158&page=1&limit=10&key=
当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段

如在一些Entity中,为了让mybatis-plus与知道某个字段不与数据库匹配,
    那么就加个
    @TableField(exist=false)
    private Long attrGroupId;

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

PO、DO、TO、DTO

1.PO持久对象

PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据的操作。

2、DO(Domain 0bject)领域对象

就是从现实世界中推象出来的有形或无形的业务实体。

3.TO(Transfer 0bject),数据传输对象传输的对象

不同的应用程序之间传输的对象。微服务

4.DTO(Data Transfer Obiect)数据传输对象

这个概念来源于J2EE的设汁模式,原来的目的是为了EJB的分布式应用握供粗粒度的数据实体,以减少分布式调用的次数,从而握分布式调用的性能和降低网络负载,但在这里,泛指用于示层与服务层之间的数据传输对象。

5.VO(value object)值对象

通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要。用new关韃字创建,由GC回收的

View object:视图对象

接受页面传递来的对象,封装对象

将业务处理完成的对象,封装成页面要用的数据

6.BO(business object)业务对象

从业务模型的度看.见IJML元#领嵫模型的领嵫对象。封装业务逻辑的java对象,通过用DAO方法,结合PO,VO进行业务操作。businessobject:业务对象主要作用是把业务逻辑封装为一个对苤。这个对象可以包括一个或多个其它的对彖。比如一个简历,有教育经历、工怍经历、社会关系等等。我们可以把教育经历对应一个PO工作经历

7、POJO简单无规则java对象

8、DAO

2、新建VO对象

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

通过BeanUtils.copyProperties(attr,attrEntity);能够实现在两个Bean之间属性对拷

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

    private Long attrGroupId;
}

  • 修改AttrController
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody AttrVo attr){
		attrService.saveAttr(attr);

        return R.ok();
    }
  • AttrService
void saveAttr(AttrVo attr);
  • impl
	@Autowired
    AttrAttrgroupRelationDao relationDao;

	@Transactional
    @Override
    public void saveAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        BeanUtils.copyProperties(attr,attrEntity);
        //1、保存基本数据
        this.save(attrEntity);
        //2、保存关联关系
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        relationEntity.setAttrGroupId(attr.getAttrGroupId());
        relationEntity.setAttrId(attrEntity.getAttrId());
        relationDao.insert(relationEntity);
    }
  • 测试
    在这里插入图片描述
  • 查看数据库

在这里插入图片描述
在这里插入图片描述

3、分页查询
  • 修改AttrController
// /product/attr/base/list/{catelogId}
    @GetMapping("/base/list/{catelogId}")
    public R baseAttrList(@RequestParam Map<String,Object> params,
                          @PathVariable("catelogId") Long catelogId){
        PageUtils page = attrService.queryBaseAttrPage(params,catelogId);
        return R.ok().put("page",page);
    }
  • AttrService
PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId);
  • impl
@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  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
        );

        return new PageUtils(page);
    }
  • 测试
    在这里插入图片描述

发现这里还有显示所属分类和所属分组,抽取Vo

  • 新建AttrRespVo
@Data
public class AttrRespVo extends AttrVo {
    /**
     *      "catelogName":"手机/数码/手机" //所属分类名字
     *      "groupName":"主体",//所属分组名字
     */
    private String catelogName;
    private String groupName;
}
  • 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  attr_name
            queryWrapper.and((wrapper)->{
                wrapper.eq("attr_id",key).or().like("attr_name",key);
            });
        }

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

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

            //1、设置分类和分组的名字
            AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
            if (attrId != null) {
                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);
    }
  • 测试
    在这里插入图片描述
  • 测试修改发现并没有写入数据库
4、规格修改
  • 修改AttrRespVo
@Data
public class AttrRespVo extends AttrVo {
    /**
     * 所属分类,所属分组
     *      "catelogName":"手机/数码/手机" //所属分类名字
     *      "groupName":"主体",//所属分组名字
     *
     *      "catelogPath": [2, 34, 225] //分类完整路径
     */
    private String catelogName;
    private String groupName;

    private Long[] catelogPath;
}
  • 修改AttrController
/**
     * 信息
     */
    @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);
    }
  • service添加实现getAttrInfo
  • 修改AttrServiceImpl
@Override
    public AttrRespVo getAttrInfo(Long attrId) {

        //数据要封装到VO里面
        AttrRespVo respVo = new AttrRespVo();

        //先查到attr的详细信息
        AttrEntity attrEntity = this.getById(attrId);

        //再设置到分组信息
        //1、先将查询到的信息attrEntity拷贝到respVo里面
        BeanUtils.copyProperties(attrEntity,respVo);
        //2、再给respVo放 所属分组id,所属分类完整路径
            //通过分组关联信息获取分组id    relationDao:分组关联表
        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());
            }
        }

        //再设置类信息
        //注入CategoryService,通过id查询到完整路径
        Long catelogId = attrEntity.getCatelogId();
        Long[] catelogPath = categoryService.findCatelogPath(catelogId);

        respVo.setCatelogPath(catelogPath);

        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
        if (categoryEntity!=null){
            respVo.setGroupName(categoryEntity.getName());
        }
        
        return respVo;
    }
  • 测试
    在这里插入图片描述

  • 修改更新方法

	/**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@RequestBody AttrVo attr){
		attrService.updateAttr(attr);

        return R.ok();
    }
  • service添加updateAttr实现
/**
 * 商品属性
 *
 * @author wlq
 * @email sunlightcs@gmail.com
 * @date 2021-06-02 17:00:13
 */
public interface AttrService extends IService<AttrEntity> {

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

    void saveAttr(AttrVo attr);

    PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId);

    AttrRespVo getAttrInfo(Long attrId);

    void updateAttr(AttrVo attr);
}
  • 添加service实现impl
@Transactional
    @Override
    public void updateAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        //  将页面带来的attr信息copy给attrEntity
        BeanUtils.copyProperties(attr,attrEntity);
        this.updateById(attrEntity);

        //1、修改分组关联
        //  attrgroupRelationEntity:需要更新的值
        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);
        }

    }
  • 测试

修改之前
在这里插入图片描述
在这里插入图片描述
修改之后

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、销售属性维护

1、查询

在这里插入图片描述

  • 修改AttrController
/*
    //获取分类规格参数  base
    //获取分类销售属性  sale
     */
    // /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);
    }
  • service添加上type
PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type);
  • impl
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?1:0);

        //  现在有两个检索条件:catelogId分类id ;   属性的类型:type
        if (catelogId != 0){
            queryWrapper.eq("catelog_id",catelogId);
        }
}
  • 测试
    在这里插入图片描述

  • 新增属性
    在这里插入图片描述

  • 发现没有显示,控制台报错空指针

  • 修改,需要判断,销售属性没有分组,规格参数才进行设置分类分组名字

//1、设置分类和分组的名字
            //  判断,销售属性没有分组,规格参数才进行设置分类分组名字
            if ("base".equalsIgnoreCase(type)){
                AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
                if (attrId != null) {
                    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
                    attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }
  • 再次测试,数据出现

  • 还有一个问题

数据库中间表pms_ attr_attrgroup_ relation

  • 3号销售属性在中间表中的分组分类id也存了空的关联关系
    在这里插入图片描述
    在这里插入图片描述
  • 原因是共用了一个save方法

2、修改保存判断

	public void saveAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        BeanUtils.copyProperties(attr,attrEntity);
        //1、保存基本数据
        this.save(attrEntity);
        //2、保存关联关系
        //  1 表示为 基本属性  , 0为销售属性
        if (attr.getAttrType() == 1){
            AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
            relationEntity.setAttrGroupId(attr.getAttrGroupId());
            relationEntity.setAttrId(attrEntity.getAttrId());
            relationDao.insert(relationEntity);
        }
    }
  • 由于需要经常进行属性类型的判读,我们抽取出来,写到common里面
  • 新建:com.wlq.common.constant.ProductConstant
public class ProductConstant {

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

        private int code;
        private String msg;

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

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }
    }

}
  • 修改saveAttr
public void saveAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        BeanUtils.copyProperties(attr,attrEntity);
        //1、保存基本数据
        this.save(attrEntity);
        //2、保存关联关系
        //  1 表示为 基本属性  , 0为销售属性
        if (attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
            AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
            relationEntity.setAttrGroupId(attr.getAttrGroupId());
            relationEntity.setAttrId(attrEntity.getAttrId());
            relationDao.insert(relationEntity);
        }
    }
  • 修改保存方法queryBaseAttrPage
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
  • 修改getAttrInfo,判断基本属性才进行回显
//  判断基本属性才进行回显
        if (attrEntity.getAttrType()== ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
            //2、再给respVo放 所属分组id,所属分类完整路径
            //通过分组关联信息获取分组id    relationDao:分组关联表
            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());
                }
            }
        }
  • 同理修改updateAttr
public void updateAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        //  将页面带来的attr信息copy给attrEntity
        BeanUtils.copyProperties(attr,attrEntity);
        this.updateById(attrEntity);

        if (attrEntity.getAttrType()==ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
            //1、修改分组关联
            //  attrgroupRelationEntity:需要更新的值
            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);
            }
        }

    }
  • 重新添加一个销售属性
    在这里插入图片描述

  • 查看在这里插入图片描述
    关联关系表就不会插入销售属性了

在这里插入图片描述

5、查询分组关联属性&删除、新增关联

  • 获取属性分组的关联的所有属性

编写 /product/attrgroup/{attrgroupId}/attr/relation

  • AttrGroupController
@Autowired
    AttrService attrService;

    //  /product/attrgroup/{attrgroupId}/attr/relation  //获取属性分组的关联的所有属性
    @GetMapping("/{attrgroupId}/attr/relation")
    public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){
        List<AttrEntity> entities = attrService.getRelationAttr(attrgroupId);
        return R.ok().put("data",entities);
    }
  • 添加service
List<AttrEntity> getRelationAttr(Long attrgroupId);
  • impl
/**
     * 根据分组id查找关联的所有基本属性
     *      //  根据中间表的attr_group_id 找到 attr_id  再去到attr表中进行查询
     * @param attrgroupId
     * @return
     */
    @Override
    public List<AttrEntity> getRelationAttr(Long attrgroupId) {

        List<AttrAttrgroupRelationEntity> entities = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));

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

        if (attrIds == null || attrIds.size() == 0){
            return null;
        }

        Collection<AttrEntity> attrEntities = this.listByIds(attrIds);

        return (List<AttrEntity>) attrEntities;

    }
  • 测试
    在这里插入图片描述

1、删除属性与分组的关联关系

由于只需两个请求参数:[{“attrId”:1,“attrGroupId”:2}]

  • 编写Vo:AttrGroupRelationVo
@Data
public class AttrGroupRelationVo {

    //{"attrId":1,"attrGroupId":2}

    private Long attrId;
    private Long attrGroupId;
}
  • AttrGroupController
/**
     * 删除属性与分组的关联关系
     */
    ///product/attrgroup/attr/relation/delete
    @GetMapping("/attr/relation/delete")
    public R deleteRelation(AttrGroupRelationVo[] vos){
        attrService.deleteRelation(vos);
        return R.ok();
    }
  • service
  • impl
/**
     * 删除属性与分组的关联关系
     * @param vos
     */
    @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);
    }
  • 添加deleteBatchRelation 批量删除方法实现
@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {

    void deleteBatchRelation(@Param("entities") List<AttrAttrgroupRelationEntity> entities);
}
  • 修改AttrAttrgroupRelationDao.xml
<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>
  • 测试
    在这里插入图片描述
    在这里插入图片描述

  • 在点击新建关联,应该会查出本属性没有关联的其它属性

2、获取属性分组没有关联的其他属性

  • AttrGroupController
/**
     *  获取属性分组没有关联的其他属性
     */
    ///product/attrgroup/{attrgroupId}/noattr/relation
    @GetMapping("/{attrgroupId}/noattr/relation")
    public R attrNoRelation(@PathVariable("attrgroupId") Long attrgroupId,
                            @RequestParam Map<String, Object> params){
        PageUtils page = attrService.getNoRelationAttr(params,attrgroupId);
        return R.ok().put("page",page);
    }
PageUtils getNoRelationAttr(Map<String, Object> params, Long attrGroupId);
  • impl
 /**
     * 获取当前分组没有关联的所有属性
     * @param params
     * @param attrgroupId
     * @return
     */
    @Override
    public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {

        //1、当前分组只能关联自己所属的分类里面的所有属性
        AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
        Long catelogId = attrGroupEntity.getCatelogId();

        //2、当前分组只能关联别的分组没有引用的属性
        //2.1)、当前分类下的其它分组
        List<AttrGroupEntity> group = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId).ne("attr_group_id",attrgroupId));
        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).notIn("attr_id", attrIds);

        if (attrIds!=null && attrIds.size()>0){
            wrapper.notIn("attr_id",attrIds);
        }

        //由于返回是一个分页,调用分页方法调用条件查询,页面会传来分页参数,并且有模糊查询,wrapper
        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;
    }
  • 测试新建关联
    在这里插入图片描述
    在这里插入图片描述
  • 这里有一个问题

如果已经关联了一个基本属性,再新建还是会显示

  • 排除自己已有的基本属性

修改AttrServiceImpl

//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());
  • 查看
    在这里插入图片描述
    在这里插入图片描述

3、新增关联


@Autowired
    AttrAttrgroupRelationService relationService;
/**
     * 新增关联
     * @param vos
     * @return
     */
    ///product/attrgroup/attr/relation
    @PostMapping("/attr/relation")
    public R addRelation(@RequestBody List<AttrGroupRelationVo> vos){

        relationService.saveBatch(vos);
        return R.ok();
    }
  • impl
@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);
    }
  • 发现控制台报空指针
//1、设置分类和分组的名字
            if("base".equalsIgnoreCase(type)){
                AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
                if (attrId != null && attrId.getAttrGroupId()!=null) {
                    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
                    attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }

            }
public void saveAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
//        attrEntity.setAttrName(attr.getAttrName());
        BeanUtils.copyProperties(attr,attrEntity);
        //1、保存基本数据
        this.save(attrEntity);
        //2、保存关联关系
        if(attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId()!=null){
            AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
            relationEntity.setAttrGroupId(attr.getAttrGroupId());
            relationEntity.setAttrId(attrEntity.getAttrId());
            relationDao.insert(relationEntity);
        }

    }
  • 测试

在这里插入图片描述

在这里插入图片描述

  • 添加记录
    在这里插入图片描述

7、商品维护

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
gulimall_pms 商品 drop table if exists pms_attr; drop table if exists pms_attr_attrgroup_relation; drop table if exists pms_attr_group; drop table if exists pms_brand; drop table if exists pms_category; drop table if exists pms_category_brand_relation; drop table if exists pms_comment_replay; drop table if exists pms_product_attr_value; drop table if exists pms_sku_images; drop table if exists pms_sku_info; drop table if exists pms_sku_sale_attr_value; drop table if exists pms_spu_comment; drop table if exists pms_spu_images; drop table if exists pms_spu_info; drop table if exists pms_spu_info_desc; /*==============================================================*/ /* Table: pms_attr */ /*==============================================================*/ create table pms_attr ( attr_id bigint not null auto_increment comment '属性id', attr_name char(30) comment '属性名', search_type tinyint comment '是否需要检索[0-不需要,1-需要]', icon varchar(255) comment '属性图标', value_select char(255) comment '可选值列表[用逗号分隔]', attr_type tinyint comment '属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]', enable bigint comment '启用状态[0 - 禁用,1 - 启用]', catelog_id bigint comment '所属分类', show_desc tinyint comment '快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整', primary key (attr_id) ); alter table pms_attr comment '商品属性'; /*==============================================================*/ /* Table: pms_attr_attrgroup_relation */ /*==============================================================*/ create table pms_attr_attrgroup_relation ( id bigint not null auto_increment comment 'id', attr_id bigint comment '属性id', attr_group_id bigint comment '属性分组id', attr_sort int comment '属性组内排序', primary key (id) ); alter table pms_attr_attrgroup_relation comment '属性&
尚硅谷谷粒商城是一个基于Java技术栈的电商平台,为用户提供购物、下单、支付等功能。谷粒商城的开发过程中,尚硅谷团队使用了一些工具和技术来支持项目管理和开发。 在谷粒商城项目中,Word文档被用来记录项目的需求分析、设计文档、测试报告等重要文档。Word作为一款强大且易于操作的文字处理工具,提供了丰富的文本编辑和格式调整功能,方便团队成员协作。团队成员可以使用Word进行文档的撰写、修改、评论等操作,确保项目进展和沟通的顺利进行。 在需求分析阶段,团队成员可以使用Word文档书写详细的用户需求和功能描述。这有助于项目管理人员和开发人员全面理解用户需求,并为后续的设计和开发工作提供指导。 在设计文档编写阶段,Word文档可以用来记录系统的架构设计、数据库设计、用户界面设计等关键信息。通过文档的编写和维护,可以保持项目的整体结构性和可维护性,方便各个团队成员在开发过程中理解和遵守设计规范。 在测试报告阶段,Word文档可以用来记录测试用例、测试结果和问题反馈。通过编写详细的测试报告,可以帮助开发人员理解和修复软件中的漏洞和问题,最终提高产品的质量。 总而言之,尚硅谷谷粒商城的Word文档在项目管理和开发过程中起到了承上启下的作用。通过充分利用Word的功能,团队成员可以高效地协作与沟通,确保项目的顺利进行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值