1. 品牌的新增
1.1 url 异步请求
-  点击品牌管理下的新增品牌,填写品牌信息后提交  
2.打开浏览器控制台


由此可以得知:
- 请求方式:POST
- 请求路径:/item/brand
- 请求参数:{name: “测试品牌”, image: “”, cids: “76,327”, letter: “C”}
- 返回参数:无
1.2 实现品牌新增
1.2.1 Controller
分析四个参数:
- 请求方式:POST
- 请求路径:/item/brand
- 请求参数:brand 对象,外加商品分类的 id 数组 cids
- 返回参数:无
/**
 * 新增品牌
 * @param cids
 * @param brand 
 * @return
 */
@PostMapping
public ResponseEntity<Void> saveBrand(@RequestParam("cids") List<Long> cids, Brand brand) {
    brandService.saveBrand(cids,brand);
    // 响应 201
    return ResponseEntity.status(HttpStatus.CREATED).build();
}
1.2.2 Service
我们不仅要新增品牌,还要新增品牌和商品分类的中间表。由于是新增操作,所以还需要加上事务管理。
/**
 * 新增品牌
 * @param cids
 * @param brand
 * @return
 */
@Transactional
public void saveBrand(List<Long> cids, Brand brand) {
    // 先新增 Brand
    brandMapper.insertSelective(brand);
    // 再新增中间表
    for (Long cid : cids) {
        brandMapper.insertCategoryAndBrand(cid, brand.getId());
    }
}
1.2.3 Mapper
通用 Mapper 只能处理单表,因此我们要手写新增商品分类和品牌的中间表数据的方法
/**
 * 新增商品分类和品牌的中间表数据
 * @param cid 商品分类 id
 * @param bid 品牌 id
 */
@Insert("INSERT INTO tb_category_brand(category_id, brand_id) VALUES(#{cid}, #{bid})")
void insertCategoryAndBrand(@Param("cid") Long cid, @Param("bid") Long bid);
1.2.4 测试


响应状态码 400,请求参数不合法,那我们再看看请求体的内容
{name: "测试品牌", image: "", cids: "76,327", letter: "C"}
发现请求体的数据格式是 JSON 格式。
但我们 Controller 中接受参数是以 String 类型接受的,那么现在解决办法有两个:
- 将 Controller 中接受参数改为以 JSON 类型接受
- 将请求体内容改为以 String 类型发送
这两种方法都是可行的。第一种,如果将 Controller 中接受参数改为以 JSON 类型接受,我们就需要创建一个 Java 实体类,好用来反序列化,但这样为了一个方法去创建一个实体类,显然成本太大。
所以我们就采用第二种方法了。
1.2.5 解决问题
找到前端发送请求的代码

修改参数转换为以 String 类型发送

再次测试,新增品牌成功


打开控制台,可以看到请求体已经是以字符串形式发送了

2. 实现图片上传
刚才在实现品牌的新增中,我们并没有上传图片,接下来完成图片上传。
2.1 搭建工程
文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此我们创建一个独立的微服务,专门处理各种上传。
2.1.1 创建 leyou-upload
右键 leyou 项目 --> New Module --> Maven --> Next

填写项目信息 --> Next

填写保存的位置 --> Finish

2.1.2 添加依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.leyou.upload</groupId>
    <artifactId>leyou-upload</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>
2.1.3 配置文件
server:
  port: 8082
spring:
  application:
    name: upload-service
  servlet:
    multipart:
      max-file-size: 5MB
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
      lease-renewal-interval-in-seconds: 5
      lease-expiration-duration-in-seconds: 10
注意:我们添加了限制文件大小的配置
2.1.4 引导类
@SpringBootApplication
@EnableDiscoveryClient
public class LeyouUploadApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeyouUploadApplication.class, args);
    }
}
2.2 实现图片上传
2.2.1 Controller
参考自定义组件中的文件上传组件可以知道以下内容:
- 请求方式:上传肯定是 POST
- 请求路径:/upload/image
- 请求参数:参数名是 file,SpringMVC 会封装为一个接口:MultipartFile
- 返回结果:上传成功后得到的文件的 url 路径,返回类型 String
@RestController
@RequestMapping("/upload")
public class UploadController {
    @Autowired
    private UploadService uploadService;
    /**
     * 图片上传
     * @param file
     * @return
     */
    @PostMapping("/image")
    public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile file) {
        String url = uploadService.uploadImage(file);
        if(StringUtils.isBlank(url)) {
            return ResponseEntity.badRequest().build();
        }
        return ResponseEntity.status(HttpStatus.CREATED).body(url);
    }
}
2.2.2 Service
在上传文件过程中,我们需要对文件进行检验:
- 检验文件类型
- 检验文件内容
@Service
public class UploadService {
    // 图片类型
    private static final List<String> IMAGE_CONTENT_TYPES = Arrays.asList("image/png", "image/jpeg");
    // 日志
    private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class);
    /**
     * 图片上传
     *
     * @param file
     * @return
     */
    public String uploadImage(MultipartFile file) {
        // 获取文件名
        String filename = file.getOriginalFilename();
        // 获取文件类型
        String contentType = file.getContentType();
        // 文件类型不合法,直接返回 null
        if (!IMAGE_CONTENT_TYPES.contains(contentType)) {
            LOGGER.info("文件类型不合法:{}", filename);
            return null;
        }
        try {
            BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
            // 如果文件内容不合法,直接返回 null
            if (bufferedImage == null) {
                LOGGER.info("文件内容不合法:{}", filename);
                return null;
            }
            // 保存文件到服务器
            file.transferTo(new File("C:\\Users\\admin\\Desktop\\img\\" + filename));
            // 返回 url
            return "http://image.leyou.com/" + filename;
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.info("服务器内部错误:{}", filename);
        }
        return null;
    }
}
注意:这里图片地址使用了另外的 url,原因如下:
- 图片不能保存在服务器内部,这样会对服务器产生额外的加载负担
- 一般静态资源都应该使用独立域名,这样访问静态资源时不会携带一些不必要的 cookie,减小请求的数据量
2.2.3 测试
-  打开 Postman 接口测试工具 
-  选择 Post 请求方式,输入请求地址 

填写 Headers

填写 Body,选择文件

发送请求,得到响应的 url

去目录查看一下

2.2.4 通过浏览器访问图片
现在我们返回了图片的 url,但这个 url 浏览器还不能访问到,也就无法做到图片的回显。
通过 nginx 代理图片路径
打开 nginx 配置文件,添加如下配置
server {
    listen       80;
    server_name  image.leyou.com;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    location / {
        root C:\\Users\\admin\\Desktop\\img;
    }
}
重启 nginx

配置 host 文件

通过浏览器访问图片
2.3 绕过网关
图片上传是文件的传输,如果也经过 Zuul 网关的代理,文件就会经过多次网路传输,造成不必要的网络负担。在高并发时,可能导致网络阻塞,Zuul 网关不可用。这样我们的整个系统就瘫痪了。所以,我们上传文件的请求就不经过网关来处理了。
2.3.1 nginx 反向代理

这里可以看到请求路径为:api/upload/image,那么肯定会经过 Zuul 网关。如果要绕过 Zuul 网关,可以使用 nginx 反向代理到图片上传的服务地址,步骤如下:
修改 nginx 配置文件,将以 /api/upload 开头的请求拦截下来,转交到图片上传的服务地址
server {
    listen       80;
    server_name  api.leyou.com;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    location /api/upload {
        proxy_pass http://127.0.0.1:8082;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
    }
    location / {
        proxy_pass http://127.0.0.1:10010;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
    }
}
这样请求路径变为了:http://127.0.0.1:8002/api/upload/image,但还是多了一个 /api。
nginx 提供了 rewrite 指令,用于对地址进行重写,格式如下:
rewrite "用来匹配路径的正则" 重写后的路径 [指令];
修改 nginx 配置文件,重写地址
server {
    listen       80;
    server_name  api.leyou.com;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    location /api/upload {
        proxy_pass http://127.0.0.1:8082;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
        rewrite "^/api/(.*)$" /$1 break; 
    }
    location / {
        proxy_pass http://127.0.0.1:10010;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
    }
}
- 首先,我们映射路径是 /api/upload,而下面一个映射路径是 / ,根据最长路径匹配原则,/api/upload 优先级更高。也就是说,凡是以 /api/upload 开头的路径,都会被第一个配置处理
- proxy_pass:反向代理,这次我们代理到 8082 端口,也就是 upload-service 服务
- rewrite "^/api/(.*)$" /$1 break,路径重写:
"^/api/(.*)$":匹配路径的正则表达式,用了分组语法,把/api/以后的所有部分当做1组
 /$1:重写的目标路径,这里用$1引用前面正则表达式匹配到的分组(组编号从1开始),即/api/后面的所有。这样新的路径就是除去/api/以外的所有,就达到了去除/api前缀的目的
 break:指令,常用的有2个,分别是:last、break
 last:重写路径结束后,将得到的路径重新进行一次路径匹配
 break:重写路径结束后,不再重新匹配路径。
  
3.重启 nginx 服务器

2.3.2 跨域问题
原来在网关的服务中添加了跨域过滤器,解决了跨域问题。现在不经过网关了,那么同样需要在图片上传的服务中添加了跨域过滤器。

@Configuration
public class LeyouCorsConfigration {
    @Bean
    public CorsFilter corsFilter() {
        // 初始化 cors 配置对象
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://manage.leyou.com"); //允许的域
        config.setAllowCredentials(true); //是否发送 Cookie 信息
        config.addAllowedMethod("*"); //允许的请求方式
        config.addAllowedHeader("*"); //允许的头信息
        //初始化 cors 配置源对象
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config); //添加映射路径,拦截一切请求
        return new CorsFilter(configSource); //返回 CorsFilter
    }
}
2.3.3 测试
-  重启 leyou-upload 服务 
-  再次上传图片,成功回显 

点击提交,成功新增品牌

2.3.4 文件上传的缺陷
上传本身没有任何问题,问题出在保存文件的方式,我们是保存在服务器机器,就会有下面的问题:
- 单机器存储,存储能力有限
- 无法进行水平扩展,因为多台机器的文件无法共享,会出现访问不到的情况
- 数据没有备份,有单点故障风险
- 并发能力差
这个时候,最好使用分布式文件存储来代替本地文件存储。
3. FastDFS
3.1 什么是分布式文件系统
分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连。
通俗来讲:
- 传统文件系统管理的文件就存储在本机。
- 分布式文件系统管理的文件存储在很多机器,这些机器通过网络连接,要被统一管理。无论是上传或者访问文件,都需要通过管理中心来访问
3.2 什么是 FastDFS
FastDFS 是由淘宝的余庆先生所开发的一个轻量级、高性能的开源分布式文件系统。用纯 C 语言开发,功能丰富:
- 文件存储
- 文件同步
- 文件访问(上传、下载)
- 存取负载均衡
- 在线扩容
适合有大容量存储需求的应用或系统。同类的分布式文件系统有谷歌的 GFS、HDFS(Hadoop)、TFS(淘宝)等。
3.3 FastDFS 的架构
3.3.1 架构图

FastDFS 两个主要的角色:Tracker Server 和 Storage Server 。
- Tracker Server:跟踪服务器,主要负责调度 storage 节点与 client 通信,在访问上起负载均衡的作用,和记录 storage 节点的运行状态,是连接 client 和 storage 节点的枢纽。
- Storage Server:存储服务器,保存文件和文件的 meta data(元数据),每个 storage server 会启动一个单独的线程主动向 Tracker cluster 中每个 tracker server 报告其状态信息,包括磁盘使用情况,文件同步情况及文件上传下载次数统计等信息
- Group:文件组,多台 Storage Server 的集群。上传一个文件到同组内的一台机器上后,FastDFS 会将该文件即时同步到同组内的其它所有机器上,起到备份的作用。不同组的服务器,保存的数据不同,而且相互独立,不进行通信。
- Tracker Cluster:跟踪服务器的集群,有一组 Tracker Server(跟踪服务器)组成。
- Storage Cluster :存储集群,有多个 Group 组成。
 
3.3.2 上传和下载流程

- Client 通过 Tracker server 查找可用的 Storage server。
- Tracker server 向 Client 返回一台可用的 Storage server 的 IP 地址和端口号。
- Client 直接通过 Tracker server 返回的 IP 地址和端口与其中一台 Storage server 建立连接并进行文件上传。
- 上传完成,Storage server 返回 Client 一个文件 ID,文件上传结束。

- Client 通过 Tracker server 查找要下载文件所在的的 Storage server。
- Tracker server 向 Client 返回包含指定文件的某个 Storage server 的 IP 地址和端口号。
- Client 直接通过 Tracker server 返回的 IP 地址和端口与其中一台 Storage server 建立连接并指定要下载文件。
- 下载文件成功。
3.4 安装 FastDFS
参考下载的 FastDFS 资料

注意:配置 FastDFS 服务器 ip 是你自己虚拟机的 ip
3.5 使用 FastDFS 客户端
3.5.1 添加依赖
在父工程中,我们已经锁定了依赖的版本:
<fastDFS.client.version>1.26.1-RELEASE</fastDFS.client.version>
所以直接在 leyou-upload 工程中直接引入依赖即可:
<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
</dependency>
3.5.2 引入配置类

@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
    
}
3.5.3 配置文件
在 leyou-upload 工程中添加 FastDFS 配置
fdfs:
  so-timeout: 1501 # 超时时间
  connect-timeout: 601 # 连接超时时间
  thumb-image: # 缩略图
    width: 60
    height: 60
  tracker-list: # tracker地址:你的虚拟机服务器地址+端口(默认是22122)
    - 192.168.222.132:22122
3.5.4 配置 host 文件
将来通过 image.leyou.com 这个域名访问 fastDFS 服务器上的图片资源。所以需要配置 hosts 文件,将 image.leyou.com 代理到虚拟机地址。
192.168.222.132 image.leyou.com
3.5.5 测试
注意:测试的时候虚拟机上 FastDFS 和 Nginx 需要是启动的,并且防火请要是关闭的。
编写测试类

@SpringBootTest
@RunWith(SpringRunner.class)
public class FastDFSTest {
    @Autowired
    private FastFileStorageClient storageClient;
    @Autowired
    private ThumbImageConfig thumbImageConfig;
    @Test
    public void testUpload() throws FileNotFoundException {
        // 上传的文件
        File file = new File("C:\\Users\\admin\\Desktop\\1.jpg");
        // 上传并保存图片,参数:1-上传的文件流 2-文件的大小 3-文件的后缀 4-可以不写
        StorePath storePath = this.storageClient.uploadFile(
                new FileInputStream(file), file.length(), "jpg", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
    }
    @Test
    public void testUploadAndCreateThumb() throws FileNotFoundException {
        File file = new File("C:\\Users\\admin\\Desktop\\1.jpg");
        // 上传并且生成缩略图
        StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
                new FileInputStream(file), file.length(), "jpg", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
        // 获取缩略图路径
        String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
        System.out.println(path);
    }
}
testUpload 运行结果
group1/M00/00/00/wKjehF5bzsuAab4AAADSZYQEtvY842.jpg
M00/00/00/wKjehF5bzsuAab4AAADSZYQEtvY842.jpg
testUploadAndCreateThumb 运行结果
group1/M00/00/00/wKjehF5b0u-AAX-AAADSZYQEtvY315.jpg
M00/00/00/wKjehF5b0u-AAX-AAADSZYQEtvY315.jpg
M00/00/00/wKjehF5b0u-AAX-AAADSZYQEtvY315_60x60.jpg
访问第一组第一个路径(正常上传的图片)
访问第二组第三个路径(上传的缩略图)

3.5.6 使用 FastDFS 改造项目
修改 leyou-upload 项目中的 UploadService,将文件上传到本地改为文件上传到 FastDFS
@Service
public class UploadService {
    // 图片类型
    private static final List<String> IMAGE_CONTENT_TYPES = Arrays.asList("image/png", "image/jpeg");
    // 日志
    private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class);
    @Autowired
    private FastFileStorageClient storageClient;
    /**
     * 图片上传
     *
     * @param file
     * @return
     */
    public String uploadImage(MultipartFile file) {
        // 获取文件名
        String filename = file.getOriginalFilename();
        // 获取文件类型
        String contentType = file.getContentType();
        // 文件类型不合法,直接返回 null
        if (!IMAGE_CONTENT_TYPES.contains(contentType)) {
            LOGGER.info("文件类型不合法:{}", filename);
            return null;
        }
        try {
            BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
            // 如果文件内容不合法,直接返回 null
            if (bufferedImage == null) {
                LOGGER.info("文件内容不合法:{}", filename);
                return null;
            }
            // 获取文件名后缀
            String type = StringUtils.substringAfterLast(filename, ".");
            // 保存文件到 FastDFS 服务器
            StorePath storePath = this.storageClient.uploadFile(
                    file.getInputStream(), file.getSize(), type, null);
            // 返回 url
            return "http://image.leyou.com/" + storePath.getFullPath();
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.info("服务器内部错误:{}", filename);
        }
        return null;
    }
}
3.5.7 再次测试
新增品牌完成后,可以看到原来上传到本地的图片已经无法显示了,而上传到 FastDFS 上的图片可以显示

4. 品牌的修改
4.1 品牌的回显
要修改品牌信息,首先要让品牌信息回显。
4.1.1 前端部分
我们找到前端中的修改品牌方法

可以看到这里有一个根据品牌信息查询商品分类的请求,可以得到以下信息:
- 请求方式:GET
- 请求参数:品牌 id,这里用的是 Rest 风格的占位符
- 请求地址:/item/category/bid
- 返回参数:商品分类的集合
4.1.2 实现品牌的回显
4.1.2.1 Controller
在 CategoryController 中编写方法
/**
 * 根据品牌 Id 查询品牌分类
 * @param bid
 * @return
 */
@GetMapping("/bid/{bid}")
public ResponseEntity<List<Category>> queryCategoryByBrandId(@PathVariable("bid") Long bid) {
    if (bid == null || bid.longValue() < 0) {
        return ResponseEntity.badRequest().build(); // 响应 400
    }
    List<Category> categories = categoryService.queryCategoryByBrandId(bid);
    if(CollectionUtils.isEmpty(categories)) {
        return ResponseEntity.notFound().build(); // 响应 404
    }
    return ResponseEntity.ok(categories);
}
4.1.2.2 Service
在 CategoryService 中编写方法
/**
 * 根据品牌 Id 查询品牌分类
 * @param bid
 * @return
 */
public List<Category> queryCategoryByBrandId(Long bid) {
    List<Category> categories = categoryMapper.queryCategoryByBrandId(bid);
    return categories;
}
4.1.2.3 Mapper
在 CategoryMapper 中编写方法
/**
 * 根据品牌 Id 查询品牌分类
 * @param bid
 * @return
 */
@Select("SELECT * FROM tb_category WHERE id IN (SELECT category_id FROM tb_category_brand WHERE brand_id = #{bid})")
List<Category> queryCategoryByBrandId(Long bid);
4.1.2.4 测试
-  点击修改按钮 

品牌信息回显成功

4.2 品牌的修改
4.2.1 前端部分
找到前端提交表单的方法

可以看到当 isEdit 值为 true 时,发送 put 请求。

而在修改品牌方法中已经把 isEdit 值赋值为 true 了
于是可以得出四个参数:
- 请求方式:PUT
- 请求路径:/item/brand
- 请求参数:brand 对象,外加商品分类的 id 数组 cids
- 返回参数:无
4.2.2 实现品牌的修改
4.2.2.1 Controller
在 BrandController 编写方法
/**
 * 更新品牌
 * @param cids
 * @param brand
 * @return
 */
@PutMapping
public ResponseEntity<Void> updateBrand(@RequestParam("cids") List<Long> cids, Brand brand) {
    brandService.updateBrand(cids,brand);
    // 响应 204
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
4.2.2.2 Service
在 BrandService 编写方法
/**
 * 更新品牌
 * @param cids
 * @param brand
 */
@Transactional
public void updateBrand(List<Long> cids, Brand brand) {
    // 先更新 Brand
    brandMapper.updateByPrimaryKey(brand);
    // 通过品牌 id 删除中间表
    brandMapper.deleteCategoryAndBrandByBid(brand.getId());
    // 再新增中间表
    for (Long cid : cids) {
        brandMapper.insertCategoryAndBrand(cid, brand.getId());
    }
}
4.2.2.3 Mapper
在 BrandMapper 编写方法
/**
 * 通过品牌 id 删除中间表
 * @param bid
 */
@Delete("DELETE FROM tb_category_brand WHERE brand_id = #{bid}")
void deleteCategoryAndBrandByBid(Long bid);
4.2.2.4 测试
-  修改品牌名称 


点击提交,修改成功

5. 品牌的删除
5.1 前端部分
前端代码里没有删除函数,我就自己定义了一个,删除成功后重新加载数据

可以看到这里有一个删除品牌的请求,可以得到以下信息:
- 请求方式:DELETE
- 请求参数:品牌 id,这里用的是 Rest 风格的占位符
- 请求地址:/item/brand/bid
- 返回参数:无
5.2 实现品牌的删除
5.2.1 Controller
在 BrandController 中编写
/**
 * 删除品牌
 * @param bid
 * @return
 */
@DeleteMapping("/bid/{bid}")
public ResponseEntity<Void> deleteBrand(@PathVariable("bid") Long bid) {
    brandService.deleteBrand(bid);
    // 响应 204
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
5.2.2 Service
在 BrandService 中编写
/**
 * 删除品牌
 * @param bid
 */
@Transactional
public void deleteBrand(Long bid) {
    // 通过品牌 id 删除中间表
    brandMapper.deleteCategoryAndBrandByBid(bid);
    // 删除品牌
    brandMapper.deleteByPrimaryKey(bid);
}
5.2.3 测试
点击删除按钮,删除成功





















