第1章 专辑管理.md 68 KB

谷粒随享

第1章 环境搭建、专辑管理

学习目标:

  • 了解听书项目业务背景
  • 搭建项目环境
    • 虚拟机环境(Docker软件)
    • 数据库环境
    • Maven项目环境(基础代码)
  • 专辑管理
    • MinIO分布式文件存储服务

随堂流程图:https://kdocs.cn/join/gvfzfav?f=101

项目地址:https://gitee.com/lvhonlgong/tingshu-1123.git

课件更新命令:

git pull origin master

如果修改过课件导致本地跟远端不一致,强制更新(远端覆盖本地)

git fetch --all
git reset --hard origin/master
git pull

1、谷粒随享

1.1 项目背景

随着智能手机和高速互联网的普及,人们开始寻求更便捷的方式来获取信息和娱乐。有声书的出现使得人们可以在旅途中、跑步时、做家务时等各种场景下,以更加灵活的方式享受阅读。

在过去,有声书主要是由专业的演员朗读,制作成录音带或CD。但随着数字化媒体的发展,听书软件应运而生,为用户提供了更多选择,包括自助出版的有声书和多样化的内容。

意义:

  1. 便捷性:听书软件使得阅读不再局限于纸质书籍,用户可以通过手机等设备在任何时间、任何地点收听有声书,节省了携带实体书的麻烦。
  2. 多样化内容:听书软件提供了广泛的有声书选择,涵盖了各种类型的图书、小说、杂志、教育内容等。这样的多样性使得用户能够根据个人兴趣和需求选择内容。
  3. 阅读体验:通过专业的朗读演员和音效制作,听书软件可以提供更加生动、有趣的阅读体验,有助于吸引更多读者,尤其是那些不太喜欢阅读纸质书籍的人。
  4. 辅助功能:听书软件通常还具备一些辅助功能,如调整朗读速度、书签功能、字幕显示等,有助于提高可访问性,使得视力受损或其他障碍的用户也能轻松阅读。
  5. 支持作家和内容创作者:听书软件为作家和内容创作者提供了另一种传播作品的渠道,有助于扩大影响力和读者群。
  6. 学习工具:听书软件也可以用作学习工具,提供学术教材、外语学习材料等,帮助用户在学习过程中更好地理解和吸收知识。

总的来说,听书软件的开发推动了阅读体验的数字化和个性化,为用户提供了更加便捷、多样化的阅读方式,也促进了作家和内容创作者的创作和传播。

平台包含三角色:

内容创作者: 将书籍录制成有声书(音频)制作成专辑,在听书平台上发布专辑,可以达到盈利目的。

商家运营者:运营听书平台上所有的专辑,对专辑进行审核、发布、订单管理、支付管理。消费者消费(购买VIP会员,购买专辑、购买声音)可以达到盈利目的。

用户:收听平台上优质资源(知识付费),购买会员,购买专辑,购买声音

1.2 项目技术栈

  • SpringBoot:简化Spring应用的初始搭建以及开发过程
  • SpringCloud:基于Spring Boot实现的云原生应用开发工具,SpringCloud使用的技术:(Spring Cloud Gateway、Spring Cloud Task和Spring Cloud Feign、Spring Cloud Nacos、Sentinel等)
  • MyBatis-Plus:持久层框架(简化持久层开发)基于Mybatis
  • Redis:内存做缓存
  • Redisson:基于redis的Java驻内存数据网格 - 框架;操作redis的框架
  • MongoDB: 分布式文档型数据库
  • Kafka:消息中间件;大型分布式项目是标配;分布式事务最终一致性
  • ElasticSearch+Kibana+Logstash/Filebeat 全文检索服务器+可视化数据监控:检索
  • ThreadPoolExecutor+CompletableFuture:线程池来实现异步操作,提高效率
  • Xxl-Job: 分布式定时任务调用中心
  • Swagger/Knife4J/YAPI:Api接口文档工具
  • MinIO(私有化对象存储集群):分布式文件存储 类似于OSS(公有)
  • 在线支付平台:微信支付
  • MySQL:关系型数据库
  • Hutool:Java工具类库
  • Lombok: 实体类的中get/set 生成的jar包
  • Natapp:内网穿透工具
  • Docker:容器化技术; 生产环境(运维人员);快速搭建环境
  • Git:代码管理工具;git使用,拉代码、提交、推送、合并、冲突解决
  • Canal:阿里开源增量订阅组件,数据增量同步
  • Seata:阿里开源分布式事务解决方案

前端技术栈

  • UniApp
  • Vue3全家桶
  • TypeScript
  • GraceUI
  • UniUI
  • uniapp-axios-adapter

1.3 项目架构图

1.4 环境搭建

  1. 参考听书软件环境安装.md

  2. 导入听书初始化项目资料中的tingshu-parent项目导入idea开发工具中即可!

1.4.1 虚拟机环境

第一步:

img

第二步:改NAT模式的子网IP:192.168.200.0

img

第三步:应用确定

第四步:启动虚拟机

img

第五步:登录虚拟机

IP:192.168.200.6
登录用户:root
登录密码:root

1.4.2 虚拟机容器列表

目前在虚拟机中安装以下容器服务都是开机自启动!

URL 账号密码
Portainer http://192.168.200.6:19000 admin/admin1234567
MySQL 192.168.200.6:3306 root/root
Redis 192.168.200.6:6379
Elasticsearch http://192.168.200.6:9200 elastic/111111
Kibana http://192.168.200.6:5601 elastic/111111
Logstash 收集日志的后台进程,无需访问
Zookeeper 192.168.200.6:2181
Kafka 192.168.200.6:9092
Kafdrop http://192.168.200.6:9093
Zipkin http://192.168.200.6:9411
Nacos http://192.168.200.6:8848/nacos nacos/nacos
MinIO http://192.168.200.6:9001 admin/admin123456
YAPI http://192.168.200.6:3000 admin@admin.com/ymfe.org
MongoDB 192.168.200.6:27017

Tips:如果发现某些容器启动失败(说明该容器依赖其他容器,确保被依赖容器先正常启动),重启虚拟机

Nacos容器依赖MySQL容器-先启动MySQL容器

Kibana容器依赖ElasticSearch容器:先启动ES容器

Kafka容器依赖zookeeper容器:先启动zookeeper

Kafdrop容器依赖Kafka容器:先启动Kafka

YAPI容器依赖MongoDB容器:先启动MongoDB

如果发现Docker服务重启/启动报错。将Docker服务重启,所有容器开机自启

systemctl restart docker

1.4.3 小程序工程

  1. 找到配套资料中mp-weixin-微信小程序.zip解压

image-20240401103203674

  1. 配套资料\02-软件\找到安装微信开发者工具

    image-20231013210033403

  2. 每个同学注册申请微信小程序测试号(微信登录会使用到)测试账号申请入口https://mp.weixin.qq.com/wxamp/sandbox?doc=1

    appId:测试号应用ID
    appSecret:测试号对应秘钥
    
  3. 在微信开发者工具中导入,导入选择信任项目,注意:这里填写自己申请测试号应用ID

image-20231013205325436

  1. 小程序默认访问的后端网关地址为本地8500端口

image-20231013205735725

1.4.4 导入初始化工程

  1. 听书/配套资料/初始后台代码/tingshu-parent.zip压缩包解压
  2. 在Idea中导入

image-20231013202902427

1.4.5 MP入门

Mybatis 增强工具包 - 只做增强不做改变,简化CRUD操作,

  • 无侵入:Mybatis-Plus 在 Mybatis 的基础上进行扩展,只做增强不做改变,引入 Mybatis-Plus 不会对您现有的 Mybatis 构架产生任何影响,而且 MP 支持所有 Mybatis 原生的特性
  • 依赖少:仅仅依赖 Mybatis 以及 Mybatis-Spring
  • 损耗小:启动即会自动注入基本CURD,性能基本无损耗,直接面向对象操作
  • 通用CRUD操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 多种主键策略:支持多达4种主键策略(内含分布式唯一ID生成器),可自由配置,完美解决主键问题
  • 支持ActiveRecord:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可实现基本 CRUD 操作
  • 支持代码生成:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用(P.S. 比 Mybatis 官方的 Generator 更加强大!)
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置分页插件:基于Mybatis物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于写基本List查询
  • 内置性能分析插件:可输出Sql语句以及其执行时间,建议开发测试时启用该功能,能有效解决慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,预防误操作
  1. 创建数据库:user_demo

  2. 初始化用户表

    CREATE TABLE `user` (
     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '姓名',
     `age` int DEFAULT NULL COMMENT '年龄',
     `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
     PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
       
    INSERT INTO `user` (id, name, age, email) VALUES
    (1, 'Jone', 18, 'test1@baomidou.com'),
    (2, 'Jack', 20, 'test2@baomidou.com'),
    (3, 'Tom', 28, 'test3@baomidou.com'),
    (4, 'Sandy', 21, 'test4@baomidou.com'),
    (5, 'Billie', 24, 'test5@baomidou.com');
    
  3. 创建demo工程mp_demo

    image-20231111150907972

  4. pom.xml

    <?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">
       <modelVersion>4.0.0</modelVersion>
       
       <groupId>com.atguigu</groupId>
       <artifactId>mp_demo</artifactId>
       <version>1.0-SNAPSHOT</version>
       
       <parent>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-parent</artifactId>
           <version>3.0.5</version>
           <!-- 版本对应: https://start.spring.io/actuator/info -->
       </parent>
       
       <properties>
           <maven.compiler.source>17</maven.compiler.source>
           <maven.compiler.target>17</maven.compiler.target>
           <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
           <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
           <mysql.version>8.0.30</mysql.version>
       </properties>
       <dependencies>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-web</artifactId>
           </dependency>
           <dependency>
               <groupId>com.baomidou</groupId>
               <artifactId>mybatis-plus-boot-starter</artifactId>
               <version>${mybatis-plus.version}</version>
           </dependency>
           <!--mysql-->
           <dependency>
               <groupId>mysql</groupId>
               <artifactId>mysql-connector-java</artifactId>
               <version>${mysql.version}</version>
           </dependency>
           <dependency>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
           </dependency>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-test</artifactId>
               <scope>test</scope>
           </dependency>
       </dependencies>
    </project>
       
    
  5. applicaton.yaml

    server:
     port: 8801
    spring:
     datasource:
       type: com.zaxxer.hikari.HikariDataSource
       driver-class-name: com.mysql.cj.jdbc.Driver
       url: jdbc:mysql://192.168.200.6:3306/user_demo?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
       username: root
       password: root
       hikari:
         connection-test-query: SELECT 1
         connection-timeout: 60000
         idle-timeout: 500000
         max-lifetime: 540000
         maximum-pool-size: 10
         minimum-idle: 5
         pool-name: GuliHikariPool
    mybatis-plus:
     configuration:
       log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
     mapper-locations: classpath:mapper/*Mapper.xml
    
  6. 启动类

    package com.atguigu;
       
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
       
    /**
    * @author: atguigu
    * @create: 2024-02-18 14:22
    */
    @SpringBootApplication
       
    public class MPDemoApp {
       public static void main(String[] args) {
           SpringApplication.run(MPDemoApp.class, args);
       }
    }
       
    
  7. MyBatisPlus配置

    package com.atguigu.demo.config;
       
    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
       
    @Configuration
    @MapperScan("com.atguigu.demo.mapper")  //扫描持久层所在包全路径
    public class MybatisPlusConfig {
       
       /**
        * 添加分页插件
        */
       @Bean
       public MybatisPlusInterceptor mybatisPlusInterceptor() {
           MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
           interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
           //interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
           return interceptor;
       }
    }
    
  8. 创建实体类

    package com.atguigu.demo.model;
       
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
       
    @Data
    @TableName("user")
    public class User {
       //主键自增
       @TableId(type = IdType.AUTO)
       private Long id;
       
       //普通列
       @TableField("user_name")
       private String name;
       
       
       private Integer age;
       private String email;
    }
    
  9. 新增持久层Mapper继承BaseMapper

    package com.atguigu.mp.mapper;
       
    import com.atguigu.mp.model.User;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
       
    public interface UserMapper extends BaseMapper<User> {
    }
       
    
  10. 新增业务层接口,继承 IService<实体类class>

    package com.atguigu.mp.service;
        
    import com.atguigu.mp.model.User;
    import com.baomidou.mybatisplus.extension.service.IService;
        
    public interface UserService extends IService<User> {
        
        //继承MP父接口未实现方法
    }
    
  11. Service实现类

    package com.atguigu.mp.service.impl;
        
    import com.atguigu.mp.mapper.UserMapper;
    import com.atguigu.mp.model.User;
    import com.atguigu.mp.service.UserService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
        
    /**
     * @author: atguigu
     * @create: 2023-11-11 14:25
     */
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    }
    
  12. 调用持久层或者业务层CURD方法。业务层提供方法都是Mapper持久层方法

    package com.atguigu.mp.service.impl;
        
    import com.atguigu.mp.mapper.UserMapper;
    import com.atguigu.mp.model.User;
    import com.atguigu.mp.service.UserService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
        
    /**
     * @author: atguigu
     * @create: 2023-12-06 13:58
     */
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
        
    }
    
    1. 测试类

      package com.atguigu;
          
      import com.atguigu.demo.mapper.UserMapper;
      import com.atguigu.demo.model.User;
      import com.atguigu.demo.service.UserService;
      import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
          
      import java.time.Year;
      import java.util.List;
          
      import static org.junit.jupiter.api.Assertions.*;
          
      @SpringBootTest
      class MPDemoAppTest {
          
      @Autowired
      private UserMapper userMapper;
          
      /**
       * 测试MP提供持久层CURD方法
       */
      @Test
      public void testMapper(){
          //System.out.println("xxxx");
          //1.新增
          //User user = new User();
          //user.setName("张三");
          //user.setAge(15);
          //user.setEmail("zhangsan@qq.com");
          //userMapper.insert(user);
          //System.out.println(user);
          //2.主键查询
          //User user1 = userMapper.selectById(6);
          //System.out.println(user1);
          
          //3.查询所有
          //List<User> userList = userMapper.selectList(null);
          //System.out.println(userList);
          
          //4.修改
          //User user = new User();
          //user.setId(6L);
          //user.setName("zhangsan");
          //userMapper.updateById(user);
          
          //5.删除
          //userMapper.deleteById(6L);
          
          //6.条件查询 根据用户姓名+年龄等值查询列表
          //6.1 创建Wrapper条件对象  弊端:有字段写错风险
          //QueryWrapper<User> queryWrapper = new QueryWrapper<>();
          //6.2 封装查询条件 from user where user_name = 'Sandy'
          //queryWrapper.eq("user_name", "Sandy");
          //queryWrapper.eq("age", 21);
          //支持链式编程
          //queryWrapper.eq("user_name", "Sandy").eq("age", 21);
          
          //7. 条件构建 采用Lambda形式
          //LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
          //// 通过方法引用获取类中数属性上注解中字段名称
          //queryWrapper.eq(User::getName, "Sandy")
          //        .between(User::getAge, 21, 25);
          //List<User> userList = userMapper.selectList(queryWrapper);
          //System.out.println(userList);
          
          
          //8.分页查询 MP自动拦截查询SQL生成分页sql limit ?(起始位置),?(页大小) 前提配置分页插件
          Page<User> page = new Page<>(1, 2);
          Page<User> userPage = userMapper.selectPage(page, null);
          System.out.println("总页:  " + userPage.getPages());
          System.out.println("总记录数 = " + userPage.getTotal());
          System.out.println("当前页记录 = " + userPage.getRecords());
          
      }
          
          
      @Autowired
      private UserService userService;
          
          
      /**
       * 测试业务层CURD方法
       */
      @Test
      public void testService(){
          //1.查询单个
          //User user = userService.getById(5L);
          //System.out.println(user);
          
          //2.新增
          //User user = new User();
          //user.setName("李四");
          //user.setAge(20);
          //user.setEmail("lisi@qq.com");
          //userService.save(user);
          
          //3.查询所有
          //List<User> list = userService.list();
          //System.out.println(list);
          
          //4.修改
          //User user = new User();
          //user.setId(7L);
          //user.setName("lisi");
          //userService.updateById(user);
          
          //5.删除
          //userService.removeById(7L);
          
          //7.条件分页查询 模糊查询姓名,范围查询年龄
          //7.1 构建分页对象
          Page<User> page = new Page<>(1, 2);
          //7.2 构建条件对象
          LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
          queryWrapper.like(User::getName, "a")
                  .ge(User::getAge, 10);
          
          //7.3 执行分页查询
          Page<User> userPage = userService.page(page, queryWrapper);
          System.out.println(userPage);
          
      }
      }
      

2、专辑管理添加

功能入口:运行app项目-->我的-->创作中心-->专辑-->点击 + 添加专辑。提供给内容创作者/平台运营人员。使用(该功能必须登录才能访问),专辑经过审核(文字+封面)机制,审核通过内容创作者用户录制声音(音频文件-声音管理)新增专辑,将声音关联到专辑下。其他的普通用户(网民)可以在APP/小程序中进行获取内容资源(有声书等资源)

主要功能如下:

  1. 先获取到专辑分类
  2. 查询专辑标签
  3. 文件上传
  4. 保存专辑

2.1 查看分类数据

需求:在保存专辑需要为新增专辑关联分类,三级分类数据需要采用列表展示

image-20231001113440333

创建视图:

#需求:查询所有分类,所有1级分类及1及分类下 2级分类 及2级下3级分类

#方式一:遍历查询1,2,3级表 效率低
#1.1 查询所有一级分类  共计15条记录  执行1条SQL
select *
from base_category1
where is_deleted = 0;

#1.2 遍历所有1级分类查询2级分类列表  执行15条SQL
select *
from base_category2
where category1_id = 1
  and is_deleted = 0;

#1.3 遍历所有2级分类查询3级分类列表 执行92条SQL
select *
from base_category3
where category2_id = 101
  and is_deleted = 0;


#方式二:使用MySQL视图对象:View  定义:视图本身不存放数据,数据都来源于原始表
#创建语法 create [or replace] view 视图名称 as SQL(简单或复杂)
#作用:1.屏蔽掉敏感字段(建立视图设置只读权限) 2.封装复杂数据

create or replace view v_c1 as
select id, name
from base_category1;

select *
from v_c1;
update v_c1
set name = '音乐1'
where id = 1;


#方式二:采用视图View封装所有1,2,3分类
/**
  从需求出发:
  1.确定查询涉及到的表
  2.确定关联方式及关联字段(内连接,外连接(左,右))
  3.确定是否需要进行条件过滤(where条件)
  4.是否需要进行分组 (group by)
  5.是否需要进行分组过滤 (having)
  6.是否需要进行排序(order by)
  7.是否需要进行分页(limit)
  8.查看SQL的执行计划(explain+SQL) type 至少达到"range"级别(不能index(全索引扫描),all(全表扫描))
*/
#2.1 查询1级分类下包含2级分类
select *
from base_category1 bc1
         left join base_category2 bc2
                   on bc2.category1_id = bc1.id;

#2.2 再关联3级分类表得出所有三级分类
select *
from base_category1 bc1
         left join base_category2 bc2
                   on bc2.category1_id = bc1.id
         left join base_category3 bc3 on bc3.category2_id = bc2.id

#2.3 列精简
select
    bc3.id,
    bc1.id category1_id,
    bc1.name category1_name,
    bc2.id category2_id,
    bc2.name category2_name,
    bc3.id category3_id,
    bc3.name category3_name,
    bc3.is_deleted,
    bc3.create_time,
    bc3.update_time
from base_category1 bc1
         left join base_category2 bc2
                   on bc2.category1_id = bc1.id
         left join base_category3 bc3 on bc3.category2_id = bc2.id
where bc1.is_deleted = 0;

#2.4 查询执行计划 区分度较高列:is_deleted
explain select
    bc3.id,
    bc1.id category1_id,
    bc1.name category1_name,
    bc2.id category2_id,
    bc2.name category2_name,
    bc3.id category3_id,
    bc3.name category3_name,
    bc3.is_deleted,
    bc3.create_time,
    bc3.update_time
from base_category1 bc1
         left join base_category2 bc2
                   on bc2.category1_id = bc1.id
         left join base_category3 bc3 on bc3.category2_id = bc2.id
where bc1.is_deleted = 0;


#2.6将以上SQL作为分类视图base_category_view数据来源
create or replace view base_category_view as
select
    bc3.id,
    bc1.id category1_id,
    bc1.name category1_name,
    bc2.id category2_id,
    bc2.name category2_name,
    bc3.id category3_id,
    bc3.name category3_name,
    bc3.is_deleted,
    bc3.create_time,
    bc3.update_time
    from base_category1 bc1
    left join base_category2 bc2
    on bc2.category1_id = bc1.id
    left join base_category3 bc3 on bc3.category2_id = bc2.id
    where bc1.is_deleted = 0;

涉及到的视图对象: base_category_view ,在这张视图中存储了所有的分类数据。展示分类数据的格式如下:

[
 {
    "categoryName":"音乐",   #一级分类名称
    "categoryId":1,                       #一级分类ID
    "categoryChild":[                     #当前一级分类包含的二级分类集合
        {
            "categoryName":"音乐音效",     #二级分类名称
            "categoryId":101,               #二级分类ID
            "categoryChild":[             #当前二级分类包含的三级分类集合
               {
                  "categoryName": "催眠音乐",
                  "categoryId": 1001
                },
                {
                  "categoryName": "放松音乐",
                  "categoryId": 1002
                },
                {
                  "categoryName": "提神音乐",
                  "categoryId": 1003
                }
            ]
        }
    ]
},
{
    "categoryName":"有声书",
    "categoryId":2,
    "categoryChild":[
        {
            "categoryName":"男频小说",
            "categoryId":104,
            "categoryChild":[
                {
                    "categoryName":"军事小说",
                    "categoryId":1009
                }
            ]
        }
    ]
 }
]

YAP接口地址:http://192.168.200.6:3000/project/11/interface/api/11

2.1.1 控制器

service-album模块中BaseCategoryApiController控制器编写

package com.atguigu.tingshu.album.api;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.album.service.BaseCategoryService;
import com.atguigu.tingshu.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;


@Tag(name = "分类管理")
@RestController
@RequestMapping(value = "/api/album")
@SuppressWarnings({"all"})
public class BaseCategoryApiController {

    @Autowired
    private BaseCategoryService baseCategoryService;


    /**
     * 查询所有1,2,3级分类列表(将子分类封装到categoryChild)
     *
     * @return [{"categoryChild":[{"categoryName":"音乐音效","categoryId":101}],"categoryName":"音乐","categoryId":1}]
     */
    @Operation(summary = "查询所有1,2,3级分类列表(将子分类封装到categoryChild属性)")
    @GetMapping("/category/getBaseCategoryList")
    public Result<List<JSONObject>> getBaseCategoryList() {
        List<JSONObject> list = baseCategoryService.getBaseCategoryList();
        return Result.ok(list);
    }


}
	

2.2.2 业务层

接口与实现类

package com.atguigu.tingshu.album.service;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.model.album.BaseCategory1;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

public interface BaseCategoryService extends IService<BaseCategory1> {


    /**
     * 查询所有1,2,3级分类列表(将子分类封装到categoryChild)
     */
    List<JSONObject> getBaseCategoryList();
}

package com.atguigu.tingshu.album.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.album.mapper.BaseCategory1Mapper;
import com.atguigu.tingshu.album.mapper.BaseCategory2Mapper;
import com.atguigu.tingshu.album.mapper.BaseCategory3Mapper;
import com.atguigu.tingshu.album.mapper.BaseCategoryViewMapper;
import com.atguigu.tingshu.album.service.BaseCategoryService;
import com.atguigu.tingshu.model.album.BaseCategory1;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@SuppressWarnings({"all"})
public class BaseCategoryServiceImpl extends ServiceImpl<BaseCategory1Mapper, BaseCategory1> implements BaseCategoryService {

    @Autowired
    private BaseCategory1Mapper baseCategory1Mapper;

    @Autowired
    private BaseCategory2Mapper baseCategory2Mapper;

    @Autowired
    private BaseCategory3Mapper baseCategory3Mapper;


    @Autowired
    private BaseCategoryViewMapper baseCategoryViewMapper;


    /**
     * 查询所有1,2,3级分类列表(将子分类封装到categoryChild)
     */
    @Override
    public List<JSONObject> getBaseCategoryList() {
        //1.查询分类视图(封装1,2,3级分类所有数据)中所有数据 共计401条记录
        List<BaseCategoryView> allCategoryList = baseCategoryViewMapper.selectList(null);
        List<JSONObject> jsonObject1List = new ArrayList<>();
        //2.处理1级分类
        //2.1 对所有分类视图记录按1级分类ID分组得到Map Key:1级分类ID  Value:1级分类集合
        Map<Long, List<BaseCategoryView>> map1 = allCategoryList
                .stream()
                .collect(
                        Collectors.groupingBy(BaseCategoryView::getCategory1Id)
                );
        //2.2 遍历map1封装所有1级分类JSON对象
        for (Map.Entry<Long, List<BaseCategoryView>> entry1 : map1.entrySet()) {
            //2.2.1 获取1级分类ID
            Long category1Id = entry1.getKey();
            //2.2.2 获取1级分类名称
            String category1Name = entry1.getValue().get(0).getCategory1Name();
            //2.2.3 构建1级分类JSON对象
            JSONObject jsonObject1 = new JSONObject();
            jsonObject1.put("categoryId", category1Id);
            jsonObject1.put("categoryName", category1Name);
            //2.2.4 存入结果jsonObject1List中
            jsonObject1List.add(jsonObject1);
            //3.TODO 处理2级分类
            List<JSONObject> jsonObject2List = new ArrayList<>();
            //3.1 对map1中集合进行根据2级分类ID进行分组 得到Map2 Key:二级分类ID Value:“二级”分类集合
            Map<Long, List<BaseCategoryView>> map2 =
                    entry1.getValue().stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));
            //3.2 遍历map2 封装2级分类对象
            for (Map.Entry<Long, List<BaseCategoryView>> entry2 : map2.entrySet()) {
                //3.2.1 获取2级分类ID
                Long category2Id = entry2.getKey();
                //3.2.2 获取2级分类名称
                String category2Name = entry2.getValue().get(0).getCategory2Name();
                //3.2.3 构建2级分类JSON对象
                JSONObject jsonObject2 = new JSONObject();
                jsonObject2.put("categoryId", category2Id);
                jsonObject2.put("categoryName", category2Name);
                jsonObject2List.add(jsonObject2);
                //4.TODO 处理3级分类
                //4.1 创建三级分类JSON对象集合
                List<JSONObject> jsonObject3List = new ArrayList<>();
                //4.2 遍历"二级"分类集合 得到所有三级分类
                for (BaseCategoryView baseCategoryView : entry2.getValue()) {
                    //4.3 封装三级分类JSON对象
                    //4.3.1 获取3级分类ID
                    Long category3Id = baseCategoryView.getCategory3Id();
                    //4.3.2 获取3级分类名称
                    String category3Name = baseCategoryView.getCategory3Name();
                    //4.3.3 构建3级分类JSON对象
                    JSONObject jsonObject3 = new JSONObject();
                    jsonObject3.put("categoryId", category3Id);
                    jsonObject3.put("categoryName", category3Name);
                    //4.3.4 构建3级分类放入三级分类JSON对象
                    jsonObject3List.add(jsonObject3);
                }
                //4.4 将三级分类集合加入到二级分类对象中"categoryChild"属性中
                jsonObject2.put("categoryChild", jsonObject3List);
            }
            //3.3 将2级分类JSON集合加入到1级分类对象中"categoryChild"
            jsonObject1.put("categoryChild", jsonObject2List);

        }

        return jsonObject1List;
    }
}

2.2 专辑标签列表

YAPI接口地址: http://192.168.200.6:3000/project/11/interface/api/15

2.2.1 控制器

BaseCategoryApiController控制器中添加代码

/**
 * 根据1级分类ID查询关联所有标签列表(标签值)
 * @param category1Id
 * @return
 */
@Operation(summary = "根据1级分类ID查询关联所有标签列表(标签值)")
@GetMapping("/category/findAttribute/{category1Id}")
public Result<List<BaseAttribute>> getAttributesByCategory1Id(@PathVariable Long category1Id){
    List<BaseAttribute> list = baseCategoryService.getAttributesByCategory1Id(category1Id);
    return Result.ok(list);
}

2.2.2 业务层

BaseCategoryService

/**
 * 根据1级分类ID查询关联所有标签列表(标签值)
 * @param category1Id
 * @return
 */
List<BaseAttribute> getAttributesByCategory1Id(Long category1Id);

BaseCategoryServiceImpl

@Autowired
private BaseAttributeMapper baseAttributeMapper;

/**
 * 根据1级分类ID查询关联所有标签列表(标签值)
 * @param category1Id
 * @return
 */
@Override
public List<BaseAttribute> getAttributesByCategory1Id(Long category1Id) {
    return baseAttributeMapper.getAttributesByCategory1Id(category1Id);
}

2.2.3 持久层

BaseAttributeMapper中添加方法

package com.atguigu.tingshu.album.mapper;

import com.atguigu.tingshu.model.album.BaseAttribute;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface BaseAttributeMapper extends BaseMapper<BaseAttribute> {


    /**
     * 根据1级分类ID查询关联所有标签列表(标签值)
     * @param category1Id
     * @return
     */
    List<BaseAttribute> getAttributesByCategory1Id(@Param("category1Id") Long category1Id);
}

动态SQL:

#需求:根据1级分类ID查询该分类关联的标签列表(包含标签值)
#关联查询:驱动表(在查询条件列建立索引) 被驱动表(关联字段上建立索引)
select
    ba.id,
    ba.attribute_name,
    bav.id base_attribute_value_id,
    bav.value_name,
    bav.attribute_id
from base_attribute ba inner join base_attribute_value bav
on bav.attribute_id = ba.id
where ba.category1_id = ?
and ba.is_deleted = 0;

#查询执行计划
explain select
    ba.id,
    ba.attribute_name,
    bav.id base_attribute_value_id,
    bav.value_name,
    bav.attribute_id
from base_attribute ba inner join base_attribute_value bav
                                  on bav.attribute_id = ba.id
where ba.category1_id = ?
  and ba.is_deleted = 0;

resources目录下创建mapper目录并添加配置文件BaseAttributeMapper.xml

<?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.atguigu.tingshu.album.mapper.BaseAttributeMapper">

    <!--自定义结果集:封装一对多-->
    <resultMap id="baseAttributeMap" type="com.atguigu.tingshu.model.album.BaseAttribute" autoMapping="true">
        <!--封装一方属性-->
        <id column="id" property="id"></id>
        <!--封装多方属性attributeValueList-->
        <collection property="attributeValueList" ofType="com.atguigu.tingshu.model.album.BaseAttributeValue" autoMapping="true">
            <id column="base_attribute_value_id" property="id"></id>
        </collection>
    </resultMap>

    <!--根据1级分类ID查询关联所有标签列表-->
    <select id="getAttributesByCategory1Id" resultMap="baseAttributeMap">
        select ba.id,
               ba.attribute_name,
               bav.id base_attribute_value_id,
               bav.value_name,
               bav.attribute_id
        from base_attribute ba
                 inner join base_attribute_value bav
                            on bav.attribute_id = ba.id
        where ba.category1_id = #{category1Id}
          and ba.is_deleted = 0
    </select>
</mapper>

2.3 分布式文件存储

2.3.1 MinIO 简介

​ MinIO 是一个基于Apache License v3.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。

https://docs.min.io/ 英文

特点

  • 高性能:作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率

  • 可扩容:不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心

  • 云原生:容器化、基于K8S的编排、多租户支持

  • Amazon S3兼容:Minio使用Amazon S3 v2 / v4 API。可以使用Minio SDK,Minio Client,AWS SDK和AWS CLI访问Minio服务器。

  • 可对接后端存储: 除了Minio自己的文件系统,还支持DAS、 JBODs、NAS、Google云存储和Azure Blob存储。

  • SDK支持: 基于Minio轻量的特点,它得到类似Java、Python或Go等语言 的sdk支持

  • Lambda计算: Minio服务器通过其兼容AWS SNS / SQS的事件通知服务触发Lambda功能。支持的目标是消息队列,如Kafka,NATS,AMQP,MQTT,Webhooks以及Elasticsearch,Redis,Postgres和MySQL等数据库。

  • 有操作页面

  • 功能简单: 这一设计原则让MinIO不容易出错、更快启动

  • 支持纠删码:MinIO使用纠删码、Checksum来防止硬件错误和静默数据污染。在最高冗余度配置下,即使丢失1/2的磁盘也能恢复数据!

存储机制

​ Minio使用纠删码erasure code和校验和checksum。 即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。纠删码是一种恢复丢失和损坏数据的数学算法

2.3.2 Docker安装(已完成)

docker pull minio/minio

docker run \ -p 9000:9000 \ -p 9001:9001 \ --name minio \ -d --restart=always \ -e "MINIO_ROOT_USER=admin" \ -e "MINIO_ROOT_PASSWORD=admin123456" \ -v /home/data:/data \ -v /home/config:/root/.minio \ minio/minio server /data --console-address ":9001"

浏览器访问:http://192.168.200.6:9001/minio/login,如图:

登录账户说明:安装时指定了登录账号:admin/admin123456

注意:文件上传时,需要调整一下linux 服务器的时间与windows 时间一致!

> 第一步:安装ntp服务
> yum -y install ntp
> 第二步:开启开机启动服务
> systemctl enable ntpd
> 第三步:启动服务  Tips:联网正常前提下如果定时同步失败,先停止服务,再启动
> systemctl stop ntpd
> systemctl start ntpd
> 第四步:更改时区
> timedatectl set-timezone Asia/Shanghai
> 第五步:启用ntp同步
> timedatectl set-ntp yes
> 第六步:同步时间
> ntpq -p
> ```

### 2.3.3 专辑图片上传

MinIO-JavaAPI:https://min.io/docs/minio/linux/developers/java/API.html

Tomcat默认限制上传文件大小:1MB

通过修改配置更改:

yaml spring: servlet:

multipart:
  max-file-size: 10MB     #单个文件最大限制
  max-request-size: 20MB  #多个文件最大限制

**业务需求**:在新增专辑前需要为专辑设置专辑封面,选中本机图片文件后将文件上传到MInIO,且返回上传后文件在线地址,方便用户进行预览。效果如下:

 ![image-20231001112153675](assets/image-20231001112153675.png)

> YAPI文档地址:http://192.168.200.6:3000/project/11/interface/api/13

java package com.atguigu.tingshu.album.api;

import com.atguigu.tingshu.album.service.FileUploadService; import com.atguigu.tingshu.common.result.Result; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile;

@Tag(name = "上传管理接口") @RestController @RequestMapping("api/album") public class FileUploadApiController {

@Autowired
private FileUploadService fileUploadService;


/**
 * 上传文件(专辑声音封面、用户头像等)
 *
 * @param file
 * @return 上传文件成功后在线地址用于前端预览图片
 */
@Operation(summary = "上传文件(专辑声音封面、用户头像等)")
@PostMapping("/fileUpload")
public Result<String> fileUpload(@RequestParam("file") MultipartFile file) {
    String fileUrl = fileUploadService.fileUpload(file);
    return Result.ok(fileUrl);
}

}


**配置MinIO客户端对象**

java package com.atguigu.tingshu.album.config;

import io.minio.MinioClient; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;

@Configuration @ConfigurationProperties(prefix = "minio") //读取节点 @Data public class MinioConstantProperties {

private String endpointUrl;
private String accessKey;
private String secreKey;
private String bucketName;

/**
 * 操作MInIO客户端对象
 * @return
 */
@Bean
public MinioClient minioClient() {
    return
            MinioClient.builder()
                    .endpoint(endpointUrl)
                    .credentials(accessKey, secreKey)
                    .build();
}

}


**FileUploadService**

java package com.atguigu.tingshu.album.service;

import org.springframework.web.multipart.MultipartFile;

public interface FileUploadService {

/**
 * 上传文件(专辑声音封面、用户头像等)
 *
 * @param file
 * @return 上传文件成功后在线地址用于前端预览图片
 */
String fileUpload(MultipartFile file);

}


**FileUploadServiceImpl**

java package com.atguigu.tingshu.album.service.impl;

import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.util.IdUtil; import com.atguigu.tingshu.album.config.MinioConstantProperties; import com.atguigu.tingshu.album.service.FileUploadService; import com.atguigu.tingshu.common.execption.GuiguException; import com.atguigu.tingshu.common.result.ResultCodeEnum; import io.minio.MinioClient; import io.minio.PutObjectArgs; import io.minio.errors.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException;

/**

  • @author: atguigu
  • @create: 2024-05-07 10:16 */ @Slf4j @Service public class FileUploadServiceImpl implements FileUploadService {

    @Autowired private MinioClient minioClient;

    @Autowired private MinioConstantProperties props;

    /**

    • 上传文件(专辑声音封面、用户头像等) *
    • @param file
    • @return 上传文件成功后在线地址用于前端预览图片 */ @Override public String fileUpload(MultipartFile file) { try {

      //1.验证文件是否为图片
      BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
      if (bufferedImage == null) {
          throw new GuiguException(400, "图片文件有误!");
      }
      //2.验证图片文件大小(长宽限制900)
      int width = bufferedImage.getWidth();
      int height = bufferedImage.getHeight();
      if (width > 1500 || height > 1500) {
          throw new GuiguException(400, "图片尺寸过大!");
      }
      
      //3.将图片文件上传到MInIO中tingshu存储空间
      //3.1 生成随机唯一文件名称避免 格式:/日期/文件名称.后缀
      String folderName = DateUtil.today();
      String objectName = "/" + folderName + "/" + IdUtil.getSnowflakeNextId() + "." + FileNameUtil.extName(file.getOriginalFilename());
      //3.2 调用客户端对象上传文件方法
      minioClient.putObject(
              PutObjectArgs.builder().bucket(props.getBucketName()).object(objectName).stream(
                              file.getInputStream(), file.getSize(), -1)
                      .contentType(file.getContentType())
                      .build());
      //4.拼接文件在线地址
      return props.getEndpointUrl() + "/" + props.getBucketName() + objectName;
      

      } catch (Exception e) {

      log.error("[专辑服务]文件上传异常:{}", e);
      throw new RuntimeException("[专辑服务]文件上传异常:" + e);
      

      } } }

      
      ## 2.4 保存专辑
      
      设为私密:表示不发布的意思,后续可以通过这个按钮选项实现专辑的上架-下架操作
      
      涉及的表:
      
      - **album_info** 专辑表  
      - 初始化userId 默认值1 为了后续能查到数据
      - 并设置初始化状态为审核通过
      - 如果是付费专辑则设置前五集为免费试看
      
      - **album_attribute_value** 专辑属性值表
      - 设置专辑Id
      
      - **album_stat** 专辑统计表
      - 初始化统计数目为0
      
      ### 2.4.1 控制层
      
      > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/17
      
      **Tips**:新增的专辑需要将专辑关联到主播用户,但由于还未完成登录功能,故在获取用户工具类`AuthContextHolder`中**getUserId方法中**将获取用户ID的返回值写为固定。
      
      ![image-20231002093707199](assets/image-20231002093707199.png)
      
      在`service-album`模块中**AlbumInfoApiController**
      
      

      java

package com.atguigu.tingshu.album.api;

import com.atguigu.tingshu.album.service.AlbumInfoService; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.common.util.AuthContextHolder; import com.atguigu.tingshu.model.album.AlbumInfo; import com.atguigu.tingshu.vo.album.AlbumInfoVo; import com.tencentcloudapi.cat.v20180409.models.AlarmInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;

@Tag(name = "专辑管理") @RestController @RequestMapping("api/album") @SuppressWarnings({"all"}) public class AlbumInfoApiController {

@Autowired private AlbumInfoService albumInfoService;

/**
 * TODO 该接口要求用户必须登录才能访问
 * 当前用户保存专辑
 *
 * @param albumInfoVo
 * @return
 */
@Operation(summary = "当前用户保存专辑")
@PostMapping("/albumInfo/saveAlbumInfo")
public Result saveAlbumInfo(@RequestBody @Validated AlbumInfoVo albumInfoVo) {
    //1.从ThreadPoolLocal中获取获取当前用户ID TODO 暂时未做登录及授权:获取到固定1
    Long userId = AuthContextHolder.getUserId();
    //2.调用业务层保存专辑
    albumInfoService.saveAlbumInfo(userId, albumInfoVo);
    return Result.ok();
}

}


### 2.4.2 业务层

**AlbumInfoService接口**

java package com.atguigu.tingshu.album.service;

import com.atguigu.tingshu.model.album.AlbumInfo; import com.atguigu.tingshu.vo.album.AlbumInfoVo; import com.baomidou.mybatisplus.extension.service.IService;

public interface AlbumInfoService extends IService {

/**
 * 当前用户保存专辑
 * @param userId 用户ID
 * @param albumInfoVo 专辑信息VO对象
 */
void saveAlbumInfo(Long userId, AlbumInfoVo albumInfoVo);

/**
 * 保存专辑统计类型
 * @param albumInfoId 专辑ID
 * @param statType 统计类型
 * @param num 数量
 */
void saveAlbumStat(Long albumInfoId, String statType, int num);

}


**AlbumInfoServiceImpl实现类**

java package com.atguigu.tingshu.album.service.impl;

import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollectionUtil; import com.atguigu.tingshu.album.mapper.AlbumAttributeValueMapper; import com.atguigu.tingshu.album.mapper.AlbumInfoMapper; import com.atguigu.tingshu.album.mapper.AlbumStatMapper; import com.atguigu.tingshu.album.service.AlbumInfoService; import com.atguigu.tingshu.common.constant.SystemConstant; import com.atguigu.tingshu.model.album.AlbumAttributeValue; import com.atguigu.tingshu.model.album.AlbumInfo; import com.atguigu.tingshu.model.album.AlbumStat; import com.atguigu.tingshu.model.search.AlbumInfoIndex; import com.atguigu.tingshu.vo.album.AlbumAttributeValueVo; import com.atguigu.tingshu.vo.album.AlbumInfoVo; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j @Service @SuppressWarnings({"all"}) public class AlbumInfoServiceImpl extends ServiceImpl implements AlbumInfoService {

@Autowired
private AlbumInfoMapper albumInfoMapper;

@Autowired
private AlbumAttributeValueMapper albumAttributeValueMapper;

@Autowired
private AlbumStatMapper albumStatMapper;

/**
 * 当前用户保存专辑
 *
 * @param userId      用户ID
 * @param albumInfoVo 专辑信息VO对象
 */
@Override
@Transactional(rollbackFor = Exception.class)  //默认事务回滚异常:RuntimeException
public void saveAlbumInfo(Long userId, AlbumInfoVo albumInfoVo) {
    //1.保存专辑信息
    //1.1 将前端提交的专辑信息VO对象拷贝为PO对象-同名同类型属性直接拷贝即可
    AlbumInfo albumInfo = BeanUtil.copyProperties(albumInfoVo, AlbumInfo.class);

    //1.2 手动未其他属性赋值
    albumInfo.setUserId(userId);
    albumInfo.setTracksForFree(5);
    albumInfo.setStatus(SystemConstant.ALBUM_STATUS_NO_PASS);

    //1.3 执行保存专辑,得到专辑ID
    albumInfoMapper.insert(albumInfo);
    Long albumInfoId = albumInfo.getId();

    //2.保存专辑标签
    List<AlbumAttributeValueVo> albumAttributeValueVoList = albumInfoVo.getAlbumAttributeValueVoList();
    if (CollectionUtil.isNotEmpty(albumAttributeValueVoList)) {
        for (AlbumAttributeValueVo albumAttributeValueVo : albumAttributeValueVoList) {
            //2.1 将专辑统计VO转为PO对象
            AlbumAttributeValue albumAttributeValue = BeanUtil.copyProperties(albumAttributeValueVo, AlbumAttributeValue.class);
            //2.2 关联专辑ID
            albumAttributeValue.setAlbumId(albumInfoId);
            //2.3 保存专辑标签关系
            albumAttributeValueMapper.insert(albumAttributeValue);
        }
    }

    //3.初始化专辑统计信息
    this.saveAlbumStat(albumInfoId, SystemConstant.ALBUM_STAT_PLAY, 0);
    this.saveAlbumStat(albumInfoId, SystemConstant.ALBUM_STAT_SUBSCRIBE, 0);
    this.saveAlbumStat(albumInfoId, SystemConstant.ALBUM_STAT_BUY, 0);
    this.saveAlbumStat(albumInfoId, SystemConstant.ALBUM_STAT_COMMENT, 0);
}

/**
 * 保存专家统计类型
 *
 * @param albumInfoId 专辑ID
 * @param statType    统计类型
 * @param num         数量
 */
@Override
public void saveAlbumStat(Long albumInfoId, String statType, int num) {
    AlbumStat albumStat = new AlbumStat();
    albumStat.setAlbumId(albumInfoId);
    albumStat.setStatType(statType);
    albumStat.setStatNum(num);
    albumStatMapper.insert(albumStat);
}

}


# 3、查看专辑列表

​   需求:创作者中心-->查询**当前登录用户**发布的专辑列表,每个专辑包含:专辑ID,专辑封面图片、专辑名称、专辑包含声音个数、创建时间、**播放量、购买量、订阅数、评论数量**等 ;根据专辑审核状态及任意关键字进行模糊查询。

  ![](assets/image-20231114083625117.png)

> YAPI接口文档:http://192.168.200.6:3000/project/11/interface/api/19

## 3.1 控制层

**AlbumInfoApiController控制器**

查询数据的时候,我们将页面渲染的数据封装到一个实体类中AlbumListVo,只需要返回这个类的集合即可!

java /**

  • 条件分页查询当前用户发布专辑列表 *
  • @param page
  • @param limit
  • @param albumInfoQuery 查询条件对象
  • @return 专辑列表分页对象 */ @Operation(summary = "条件分页查询当前用户发布专辑列表") @PostMapping("/albumInfo/findUserAlbumPage/{page}/{limit}") public Result> getUserAlbumPage(

    @PathVariable int page,
    @PathVariable int limit,
    @RequestBody AlbumInfoQuery albumInfoQuery
    

    ) { //1.从ThreadPoolLocal中获取获取当前用户ID TODO 暂时未做登录及授权:获取到固定1 Long userId = AuthContextHolder.getUserId(); if (userId != null) {

    albumInfoQuery.setUserId(userId);
    

    } //2.创建分页对象-封装页码跟页大小 Page pageInfo = new Page<>(page, limit); //3.调用业务层(持久层)查询数据库得到分页对象其他属性:总记录数、总页数、当前页数据等信息 pageInfo = albumInfoService.getUserAlbumPage(albumInfoQuery, pageInfo); return Result.ok(pageInfo); }

    
    ## 3.2 业务层
    
    **AlbumInfoService接口**
    
    

    java

    /**

    • 条件分页查询当前用户发布专辑列表
    • @param albumInfoQuery 查询条件对象
    • @param pageInfo MP分页对象
    • @return */ Page getUserAlbumPage(AlbumInfoQuery albumInfoQuery, Page pageInfo);

      
      **AlbumInfoServiceImpl实现类**
      
      

      java

      /**

      • 条件分页查询当前用户发布专辑列表 *
      • @param albumInfoQuery 查询条件对象
      • @param pageInfo MP分页对象
      • @return */ @Override public Page getUserAlbumPage(AlbumInfoQuery albumInfoQuery, Page pageInfo) { return albumInfoMapper.getUserAlbumPage(pageInfo, albumInfoQuery); }

        
        ## 3.3 持久层
        
        SQL演练:
        
        

        sql

        #需求:分页条件查询专辑列表查询专辑对应四项统计信息 #1.涉及到查询表专辑表,专辑统计表 关联条件:统计表中专辑ID跟专辑表主键关联 select * from album_info ai left join album_stat stat on stat.album_id = ai.id;

        #2.专辑信息+四项统计信息封装到一个Java对象中AlbumListVo select

        ai.id albumId,
        ai.album_title,
        ai.cover_url,
        ai.include_track_count,
        ai.is_finished,
        ai.status,
        stat.stat_type,
        stat.stat_num
        

        from album_info ai left join album_stat stat

                                 on stat.album_id = ai.id
        

        where ai.user_id = ? and ai.is_deleted = 0;

        #3.如何将每个专辑四条统计信息从行转为列,根据专辑ID进行分组, MySQL5.7以后变为严格模式 sql_mode=only_full_group_by:select出现分组字段及聚合函数 select

        ai.id albumId,
        ai.album_title,
        ai.cover_url,
        ai.include_track_count,
        ai.is_finished,
        ai.status,
        stat.stat_type,
        stat.stat_num
        

        from album_info ai left join album_stat stat

                                 on stat.album_id = ai.id
        

        where ai.user_id = ? and ai.is_deleted = 0 group by ai.id;

        #没有意义 select

        ai.id albumId
        

        from album_info ai left join album_stat stat

                                 on stat.album_id = ai.id
        

        where ai.user_id = ? and ai.is_deleted = 0 group by ai.id

        #4.借助于函数解决问题:从行转为列 if(condition, value_if_true, value_if_false) select if(1=2, 'a', 'b')

        select

        ai.id albumId,
        ai.album_title,
        ai.cover_url,
        ai.include_track_count,
        ai.is_finished,
        ai.status,
        max(if(stat.stat_type='0401', stat.stat_num, 0)) playStatNum,
        max(if(stat.stat_type='0402', stat.stat_num, 0)) subscribeStatNum,
        max(if(stat.stat_type='0403', stat.stat_num, 0)) buyStatNum,
        max(if(stat.stat_type='0404', stat.stat_num, 0)) commentStatNum
        

        from album_info ai left join album_stat stat

                                 on stat.album_id = ai.id
        

        where ai.user_id = ? and ai.status = '0301' and ai.is_deleted = 0 group by ai.id order by ai.id desc limit 10;

        #5.查看执行计划 explain select

                ai.id albumId,
                ai.album_title,
                ai.cover_url,
                ai.include_track_count,
                ai.is_finished,
                ai.status,
                max(if(stat.stat_type='0401', stat.stat_num, 0)) playStatNum,
                max(if(stat.stat_type='0402', stat.stat_num, 0)) subscribeStatNum,
                max(if(stat.stat_type='0403', stat.stat_num, 0)) buyStatNum,
                max(if(stat.stat_type='0404', stat.stat_num, 0)) commentStatNum
            from album_info ai left join album_stat stat
                                         on stat.album_id = ai.id
            where ai.user_id = ? and ai.status = '0301' and ai.is_deleted = 0
            group by ai.id
            order by ai.id desc
            limit 10;
        
        
        **AlbumInfoMapper **
        
        

        java package com.atguigu.tingshu.album.mapper;

        import com.atguigu.tingshu.model.album.AlbumInfo; import com.atguigu.tingshu.query.album.AlbumInfoQuery; import com.atguigu.tingshu.vo.album.AlbumListVo; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param;

        @Mapper public interface AlbumInfoMapper extends BaseMapper {

        /**
         * 条件分页查询当前用户发布专辑列表
         *
         * @param pageInfo       MP分页对象,MP自动生成分页SQL
         * @param albumInfoQuery 查询条件
         * @return
         */
        Page<AlbumListVo> getUserAlbumPage(Page<AlbumListVo> pageInfo,@Param("vo") AlbumInfoQuery albumInfoQuery);
        

        }

        
        **AlbumInfoMapper.xml 实现**
        
        

        xml <?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" >

        <!--条件分页查询当前用户发布专辑列表-->
        <select id="getUserAlbumPage" resultType="com.atguigu.tingshu.vo.album.AlbumListVo">
            select
                ai.id albumId,
                ai.album_title,
                ai.cover_url,
                ai.include_track_count,
                ai.is_finished,
                ai.status,
                max(if(stat.stat_type='0401', stat.stat_num, 0)) playStatNum,
                max(if(stat.stat_type='0402', stat.stat_num, 0)) subscribeStatNum,
                max(if(stat.stat_type='0403', stat.stat_num, 0)) buyStatNum,
                max(if(stat.stat_type='0404', stat.stat_num, 0)) commentStatNum
            from album_info ai left join album_stat stat
                                         on stat.album_id = ai.id
            <where>
                <if test="vo.userId != null">
                    ai.user_id = #{vo.userId}
                </if>
                <if test="vo.status != null and vo.status != ''">
                    and ai.status = #{vo.status}
                </if>
                <if test="vo.albumTitle != null and vo.albumTitle != ''">
                    and ai.album_title like concat('%', #{vo.albumTitle}, '%')
                </if>
            </where>
            and ai.is_deleted = 0
            group by ai.id
            order by ai.id desc
        </select>
        

        
        # 4、删除专辑
        
        在本项目中所有删除都采用逻辑删除,利用MybatisPlus提供逻辑删除。在表中提供逻辑删除字段:is_deleted
        
        在Java实体类中映射逻辑删除字段,属性使用@TableLogic,调用MP提供持久层或者业务层删除方法时候,自动实现逻辑删除(修改操作)
        
        > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/21
        
        ## 4.1 控制层
        
        **AlbumInfoApiController控制器**
        
        

        java /**

        • 根据专辑ID删除专辑 *
        • @param id
        • @return */ @Operation(summary = "根据专辑ID删除专辑") @DeleteMapping("/albumInfo/removeAlbumInfo/{id}") public Result removeAlbumInfo(@PathVariable Long id) { albumInfoService.removeAlbumInfo(id); return Result.ok(); }

          
          ## 4.2 业务层
          
          **AlbumInfoService接口**
          
          

          java

        /**

        • 根据专辑ID删除专辑
        • @param id */ void removeAlbumInfo(Long id);

          
          **AlbumInfoServiceImpl实现类**
          
          

          java

        @Autowired private TrackInfoMapper trackInfoMapper;

        /**

        • 根据专辑ID删除专辑 *
        • @param id / @Override @Transactional(rollbackFor = Exception.class) public void removeAlbumInfo(Long id) { //1.根据专辑ID查询专辑下是否关联声音 select count() from track_info where album_id = ? limit 1 LambdaQueryWrapper trackInfoLambdaQueryWrapper = new LambdaQueryWrapper<>(); trackInfoLambdaQueryWrapper.eq(TrackInfo::getAlbumId, id); trackInfoLambdaQueryWrapper.last("limit 1"); Long count = trackInfoMapper.selectCount(trackInfoLambdaQueryWrapper); if (count > 0) {

          throw new GuiguException(500, "专辑下存在声音!");
          

          } //2.根据主键专辑ID删除专辑信息 albumInfoMapper.deleteById(id);

          //3.根据专辑ID删除标签列表 LambdaQueryWrapper albumAttributeValueLambdaQueryWrapper = new LambdaQueryWrapper<>(); albumAttributeValueLambdaQueryWrapper.eq(AlbumAttributeValue::getAlbumId, id); albumAttributeValueMapper.delete(albumAttributeValueLambdaQueryWrapper);

          //4.根据专辑ID删除统计列表 LambdaQueryWrapper albumStatLambdaQueryWrapper = new LambdaQueryWrapper<>(); albumStatLambdaQueryWrapper.eq(AlbumStat::getAlbumId, id); albumStatMapper.delete(albumStatLambdaQueryWrapper); }

          
          # 5、专辑修改
          
          ## 5.1 回显数据
          
          1. 需要根据专辑id获取到对应的回显数据,需要回显专辑与属性数据,不需要回显统计数据!
          2. 根据修改内容保存最新数据
          
          > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/23
          
          **AlbumInfoApiController控制器**
          
          

          java

          /**

          • 根据专辑ID查询专辑信息(包含标签列表)
          • @param id
          • @return */ @Operation(summary = "根据专辑ID查询专辑信息(包含标签列表)") @GetMapping("/albumInfo/getAlbumInfo/{id}") public Result getAlbumInfo(@PathVariable Long id){ AlbumInfo albumInfo = albumInfoService.getAlbumInfo(id); return Result.ok(albumInfo); }

            
            **AlbumInfoService接口**
            
            

            java

          • /**

            • 根据专辑ID查询专辑信息(包含标签列表)
            • @param id
            • @return */ AlbumInfo getAlbumInfo(Long id);

              
              **AlbumInfoServiceImpl实现类**
              
              

              java

            /**

            • 根据专辑ID查询专辑信息(包含标签列表) *
            • @param id
            • @return */ @Override public AlbumInfo getAlbumInfo(Long id) { //1.根据主键专辑ID查询专辑信息 //AlbumInfo albumInfo = this.getById(id); AlbumInfo albumInfo = albumInfoMapper.selectById(id); if (albumInfo != null) {

              //2.根据专辑ID查询专辑标签列表
              LambdaQueryWrapper<AlbumAttributeValue> albumAttributeValueLambdaQueryWrapper = new LambdaQueryWrapper<>();
              albumAttributeValueLambdaQueryWrapper.eq(AlbumAttributeValue::getAlbumId, id);
              List<AlbumAttributeValue> albumAttributeValueList = albumAttributeValueMapper.selectList(albumAttributeValueLambdaQueryWrapper);
              albumInfo.setAlbumAttributeValueVoList(albumAttributeValueList);
              

              } return albumInfo; }

              
              
              
              ## 5.2 保存修改后数据
              
              涉及的表:
              
              - album_info    根据主键进行更新
              - album_attribute_value  先删除所有数据,再新增数据
              
              > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/25
              
              **AlbumInfoApiController控制器**
              
              

              java

            /**

            • 修改专辑信息
            • @param id
            • @param albumInfo
            • @return */ @Operation(summary = "修改专辑信息") @PutMapping("/albumInfo/updateAlbumInfo/{id}") public Result updateAlbumInfo(@PathVariable Long id, @RequestBody AlbumInfo albumInfo){ albumInfoService.updateAlbumInfo(albumInfo); return Result.ok(); }

              
              **AlbumInfoService接口**
              
              

              java

            /**

            • 修改专辑信息
            • @param albumInfo */ void updateAlbumInfo(AlbumInfo albumInfo);

              
              **AlbumInfoServiceImpl实现类**
              
              

              java

            /**

            • 修改专辑信息 *
            • @param albumInfo */ @Override @Transactional(rollbackFor = Exception.class) public void updateAlbumInfo(AlbumInfo albumInfo) { //1.修改专辑表基本信息 albumInfoMapper.updateById(albumInfo);

              //2.修改专辑标签关系记录 List albumAttributeValueVoList = albumInfo.getAlbumAttributeValueVoList(); //2.1 删除该专辑下旧关联的标签记录 LambdaQueryWrapper albumAttributeValueLambdaQueryWrapper = new LambdaQueryWrapper<>(); albumAttributeValueLambdaQueryWrapper.eq(AlbumAttributeValue::getAlbumId, albumInfo.getId()); albumAttributeValueMapper.delete(albumAttributeValueLambdaQueryWrapper); //2.2 新增专辑标签记录 if (CollectionUtil.isNotEmpty(albumAttributeValueVoList)) {

              for (AlbumAttributeValue albumAttributeValue : albumAttributeValueVoList) {
                  //2.2 关联专辑ID
                  albumAttributeValue.setAlbumId(albumInfo.getId());
                  //2.3 保存专辑标签关系
                  albumAttributeValueMapper.insert(albumAttributeValue);
              }
              

              } } ```