第1章 专辑管理.md 41 KB

谷粒随享

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

学习目标:

1、谷粒随享

1.1 项目背景

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

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

意义:

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

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

1.2 项目技术栈

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

前端技术栈

  • 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
登录密码:123456

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
Kibana http://192.168.200.6:5601
Logstash 收集日志的后台进程,无需访问
Zookeeper 192.168.200.6:2181
Kafka http://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

1.4.3 小程序工程

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

    image-20231013205031296

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

    image-20231013210033403

  3. 在微信开发者工具中导入,导入选择信任项目

image-20231013205325436

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

image-20231013205735725

1.4.4 导入初始化工程

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

image-20231013202902427

2、专辑管理添加

功能入口:运行app项目-->我的-->创作中心-->专辑-->点击 + 添加专辑

主要功能如下:

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

2.1 查看分类数据

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

image-20231001113440333

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

[
 {
    "categoryName":"音乐",   #一级分类名称
    "categoryId":101,                       #一级分类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.List;


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

	@Autowired
	private BaseCategoryService baseCategoryService;

	/**
	 * 查询所有分类
	 *
	 * @return
	 */
	@Operation(summary = "查询所有分类")
	@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级分类)
     *
     * @return
     */
    List<JSONObject> getBaseCategoryList();
}
package com.atguigu.tingshu.album.service.impl;

import cn.hutool.core.collection.CollectionUtil;
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;

    /**
     * 查询所有的分类数据
     *
     * @return
     */
    @Override
    public List<JSONObject> getBaseCategoryList() {
        List<JSONObject> listResult = new ArrayList<>();
        try {
            //1.查询数据库中"分类视图-封装所有分类数据"得到所有分类集合
            //1.1 调用分类视图业务层查询到所有910条分类数据
            List<BaseCategoryView> allCategoryList = baseCategoryViewMapper.selectList(null);
            //hutool工具包下
            if (CollectionUtil.isNotEmpty(allCategoryList)) {
                //1.2 根据分类对象中一级分类ID进行分组,得到所有一级分类  分组后Map的Key:一级分类ID  分组后Map的Value:当前一级分类集合  Map长度就是一级分类数量
                Map<Long, List<BaseCategoryView>> category1ListMap =
                        allCategoryList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory1Id));

                //1.3 遍历一级分类Map 每遍历一次就处理一个一级分类
                for (Map.Entry<Long, List<BaseCategoryView>> entry : category1ListMap.entrySet()) {
                    //2.处理集合获取所有一级分类
                    //2.1 新创建1级分类对象 分组1级分类对应 categoryId,categoryName,categoryChild
                    JSONObject category1 = new JSONObject();
                    //2.2 获取1级分类ID-Map的Key,为1级分类对象分类ID赋值
                    Long category1Id = entry.getKey();
                    category1.put("categoryId", category1Id);

                    //2.3 获取1级分类集合-Map中Value,为1级分类对象分类名称赋值
                    String category1Name = entry.getValue().get(0).getCategory1Name();
                    category1.put("categoryName", category1Name);

                    //3.在一级分类集合中处理二级分类-将二级分类集合放入一级分类对象中categoryChild属性中
                    //3.1 将以及分类集合按照二级分类ID进行分组 得到包含二级分类信息Map集合
                    Map<Long, List<BaseCategoryView>> category2ListMap = entry.getValue().stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));
                    //3.2 遍历包含二级分类信息Map集合
                    List<JSONObject> category2List = new ArrayList<>();
                    for (Map.Entry<Long, List<BaseCategoryView>> entry2 : category2ListMap.entrySet()) {
                        //3.2.1 构建二级分类对象
                        JSONObject category2 = new JSONObject();
                        //3.2.2 封装二级分类ID
                        Long category2Id = entry2.getKey();
                        category2.put("categoryId", category2Id);
                        //3.2.3 封装二级分类名称
                        String category2Name = entry2.getValue().get(0).getCategory2Name();
                        category2.put("categoryName", category2Name);
                        //4.处理三级分类-将三级分类集合放入二级分类对象中categoryChild属性中
                        //4.1 遍历二级分类集合 获取分类对象中三级分类ID跟名称
                        List<JSONObject> category3List = new ArrayList<>();
                        for (BaseCategoryView baseCategoryView : entry2.getValue()) {
                            JSONObject category3 = new JSONObject();
                            category3.put("categoryId", baseCategoryView.getCategory3Id());
                            category3.put("categoryName", baseCategoryView.getCategory3Name());
                            category3List.add(category3);
                        }
                        category2.put("categoryChild", category3List);

                        //3.3 将二级分类装入二级分类集合中
                        category2List.add(category2);

                    }
                    //3.4 将二级分类集合放入一级分类对象中
                    category1.put("categoryChild", category2List);
                    listResult.add(category1);
                }
            }
            //5.返回经过处理所有一级分类集合
            return listResult;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

2.2 专辑标签列表

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

2.2.1 控制器

BaseCategoryApiController控制器中添加代码

/**
 * 根据一级分类Id 查询分类属性列表
 * @param category1Id
 * @return
 */
@Operation(summary = "根据一级分类Id 查询分类属性列表")
@GetMapping("/category/findAttribute/{category1Id}")
public Result<List<BaseAttribute>> findAttribute(@PathVariable Long category1Id) {
	// 调用服务层方法
	List<BaseAttribute> list = baseCategoryService.getAttributeByCategory1Id(category1Id);
	return Result.ok(list);
}

2.2.2 业务层

BaseCategoryService

/**
* 根据一级分类Id获取分类属性
* @param category1Id
* @return
*/
List<BaseAttribute> getAttributeByCategory1Id(Long category1Id);

BaseCategoryServiceImpl

@Autowired
private BaseAttributeMapper baseAttributeMapper;


/**
 * 根据一级分类Id获取分类属性
 * @param category1Id
 * @return
 */
@Override
public List<BaseAttribute> getAttributeByCategory1Id(Long category1Id) {
    return baseAttributeMapper.getAttributeByCategory1Id(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> {


    /**
     * 根据一级分类Id获取分类属性
     *
     * @param category1Id
     * @return
     */
    List<BaseAttribute> getAttributeByCategory1Id(@Param("category1Id") Long category1Id);
}

在resource 目录下创建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:表示主键 property:表示实体类的属性名 column:表示通过sql 执行以后查询出来的字段名-->
        <id column="id" property="id"></id>
        <!--result : 表示映射普通字段-->
        <!--<result property="" column=""></result>-->
        <!--mybatis 如何配置一对多-->
        <!--ofType : 返回的数据类型-->
        <collection property="attributeValueList" ofType="com.atguigu.tingshu.model.album.AlbumAttributeValue"
                    autoMapping="true">
            <id column="attr_value_id" property="id"></id>
        </collection>
    </resultMap>


    <!--根据分类ID查询专辑标签列表-->
    <select id="getAttributeByCategory1Id" resultMap="baseAttributeMap">
        select ba.id,
               ba.attribute_name,
               ba.category1_id,
               bav.id attr_value_id,
               bav.value_name
        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
        order by ba.id
    </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://IP:9001/minio/login,如图:

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

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

第一步:安装ntp服务 yum -y install ntp 第二步:开启开机启动服务 systemctl enable ntpd 第三步:启动服务 systemctl start ntpd 第四步:更改时区 timedatectl set-timezone Asia/Shanghai 第五步:启用ntp同步 timedatectl set-ntp yes 第六步:同步时间 ntpq -p

2.3.3 专辑图片上传

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

image-20231001112153675

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

package com.atguigu.tingshu.album.api;

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.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(MultipartFile file) {
        String fileUrl = fileUploadService.uploadImage(file);
        return Result.ok(fileUrl);
    }

}

配置MinIO客户端对象

package com.atguigu.tingshu.album.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: atguigu
 * @create: 2023-04-18 15:19
 */
@Configuration
public class MinIOConfig {

    
    @Autowired
    private MinioConstantProperties prop;


    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(prop.getEndpointUrl())
                .credentials(prop.getAccessKey(), prop.getSecreKey())
                .build();
    }

}

FileUploadService

package com.atguigu.tingshu.album.service;

import org.springframework.web.multipart.MultipartFile;

public interface FileUploadService {

    /**
     * 将文件上传到MinIO
     * @param file
     * @return
     */
    String uploadImage(MultipartFile file);
}

FileUploadServiceImpl

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 io.minio.MinioClient;
import io.minio.PutObjectArgs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

/**
 * @author: atguigu
 * @create: 2023-10-09 22:09
 */
@Service
public class FileUploadServiceImpl implements FileUploadService {

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioConstantProperties minioConstantProperties;


    @Override
    public String uploadImage(MultipartFile file) {
        try {
            //1.验证文件是否为图片(校验后缀MIME,文件内容 校验文件头信息)
            BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
            if (bufferedImage == null) {
                throw new RuntimeException("图片格式非法!");
            }
            //2.构建上传后文件名称 形式: /yyyy-MM-dd/文件名称.png
            String folderName = DateUtil.today();
            String fileName = IdUtil.randomUUID();
            String extName = FileNameUtil.extName(file.getOriginalFilename());
            String objectName = "/" + folderName + "/" + fileName + "." + extName;
            //3.调用MinIoClient对象完车文件上传
            minioClient.putObject(
                    PutObjectArgs.builder().bucket(minioConstantProperties.getBucketName()).object(objectName).stream(
                                    file.getInputStream(), file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build());
            //4.拼接上传后文件访问地址
            return minioConstantProperties.getEndpointUrl() + "/" + minioConstantProperties.getBucketName() + objectName;
        } catch (Exception 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:新增的专辑需要将专辑关联到主播用户,但由于还未完成登录功能,故在获取用户工具类AuthContextHoldergetUserId方法中将获取用户ID的返回值写为固定。

image-20231002093707199

service-album模块中AlbumInfoApiController

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.vo.album.AlbumInfoVo;
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.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;


    /**
     * 新增专辑方法
     *
     * @param albumInfoVo
     * @return
     */
    @Operation(summary = "新增专辑")
    @PostMapping("/albumInfo/saveAlbumInfo")
    public Result save(@RequestBody @Validated AlbumInfoVo albumInfoVo) {
        //TODO 用户ID暂时固定为1L 后续做完认证后鉴权后改为动态获取即可
        //调用服务层保存方法;
        albumInfoService.saveAlbumInfo(albumInfoVo, AuthContextHolder.getUserId());
        return Result.ok();
    }
}

2.4.2 业务层

AlbumInfoService接口

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<AlbumInfo> {
    /**
     * 保存专辑
     * @param albumInfoVo
     * @param userId
     */
    void saveAlbumInfo(AlbumInfoVo albumInfoVo, Long userId);
}

AlbumInfoServiceImpl实现类

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.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 java.util.List;

@Slf4j
@Service
@SuppressWarnings({"all"})
public class AlbumInfoServiceImpl extends ServiceImpl<AlbumInfoMapper, AlbumInfo> implements AlbumInfoService {

    @Autowired
    private AlbumInfoMapper albumInfoMapper;

    @Autowired
    private AlbumAttributeValueMapper albumAttributeValueMapper;

    @Autowired
    private AlbumStatMapper albumStatMapper;

    /**
     * 保存专辑方法
     *
     * @param albumInfoVo
     * @param userId      -- AuthContextHolder中暂写固定值
     * @return
     */
    @Override
    public void saveAlbumInfo(AlbumInfoVo albumInfoVo, Long userId) {
        //1.拷贝专辑vo信息到专辑PO对象
        AlbumInfo albumInfo = BeanUtil.copyProperties(albumInfoVo, AlbumInfo.class);
        //	设置用户Id
        albumInfo.setUserId(userId);
        //	设置专辑审核状态为:通过
        albumInfo.setStatus(SystemConstant.ALBUM_STATUS_PASS);
        //  付费&VIP付费专辑 默认前前5集免费试看
        if (!SystemConstant.ALBUM_PAY_TYPE_FREE.equals(albumInfo.getPayType())) {
            albumInfo.setTracksForFree(5);
        }
        // 保存专辑
        this.save(albumInfo);

        //2.保存专辑标签值
        List<AlbumAttributeValueVo> albumAttributeValueVoList = albumInfoVo.getAlbumAttributeValueVoList();
        if (CollectionUtil.isNotEmpty(albumAttributeValueVoList)) {
            for (AlbumAttributeValueVo albumAttributeValueVo : albumAttributeValueVoList) {
                AlbumAttributeValue albumAttributeValue = BeanUtil.copyProperties(albumInfoVo, AlbumAttributeValue.class);
                albumAttributeValue.setAlbumId(albumInfo.getId());
                //保存专辑标签值
                albumAttributeValueMapper.insert(albumAttributeValue);
            }
        }


        //3.初始化当前专辑相关统计数据
        this.saveAlbumStat(albumInfo.getId(), SystemConstant.ALBUM_STAT_PLAY);
        this.saveAlbumStat(albumInfo.getId(), SystemConstant.ALBUM_STAT_SUBSCRIBE);
        this.saveAlbumStat(albumInfo.getId(), SystemConstant.ALBUM_STAT_BUY);
        this.saveAlbumStat(albumInfo.getId(), SystemConstant.ALBUM_STAT_COMMENT);
    }

    /**
     * 初始化保存专辑统计数据
     *
     * @param albumId
     * @param statType
     */
    @Override
    public void saveAlbumStat(Long albumId, String statType) {
        AlbumStat albumStat = new AlbumStat();
        albumStat.setAlbumId(albumId);
        albumStat.setStatNum(0);
        albumStat.setStatType(statType);
        albumStatMapper.insert(albumStat);

    }
}

3、查看专辑列表

image-20231009230416248

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

3.1 控制层

AlbumInfoApiController控制器

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

/**
 * 根据条件查询专辑列表
 *
 * @param page
 * @param limit
 * @param albumInfoQuery
 * @return
 */
@Operation(summary = "获取当前用户专辑分页列表")
@PostMapping("/albumInfo/findUserAlbumPage/{page}/{limit}")
public Result getUserAlbumPage(
        @PathVariable Long page,
        @PathVariable Long limit,
        @RequestBody AlbumInfoQuery albumInfoQuery
) {
    //	获取数据:
    albumInfoQuery.setUserId(AuthContextHolder.getUserId());
    Page<AlbumListVo> albumInfoPage = new Page<>(page, limit);
    //	调用服务层方法
    albumInfoPage = this.albumInfoService.getUserAlbumPage(albumInfoPage, albumInfoQuery);
    //	返回数据集
    return Result.ok(albumInfoPage);
}

3.2 业务层

AlbumInfoService接口

/**
     * 查询专辑列表
     * @param albumInfoPage
     * @param albumInfoQuery
     * @return
     */
IPage<AlbumListVo> getUserAlbumPage(Page<AlbumListVo> albumInfoPage, AlbumInfoQuery albumInfoQuery);

AlbumInfoServiceImpl实现类

/**
 * 专辑列表
 * @param albumInfoPage
 * @param albumInfoQuery
 * @return
 */
@Override
public IPage<AlbumListVo> getUserAlbumPage(Page<AlbumListVo> albumInfoPage, AlbumInfoQuery albumInfoQuery) {
   // 调用mapper 层方法
   return albumInfoMapper.getUserAlbumPage(albumInfoPage,albumInfoQuery);
}

3.3 持久层

**AlbumInfoMapper **

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.core.metadata.IPage;
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<AlbumInfo> {
    /**
     * 根据条件查询专辑列表
     * @param albumInfoPage
     * @param albumInfoQuery
     * @return
     */
    Page<AlbumListVo> getUserAlbumPage(Page<AlbumListVo> albumInfoPage, @Param("vo") AlbumInfoQuery albumInfoQuery);
}

AlbumInfoMapper.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.AlbumInfoMapper">


    <select id="getUserAlbumPage" resultType="com.atguigu.tingshu.vo.album.AlbumListVo">
        select
        info.album_id,
        info.album_title,
        info.cover_url,
        info.include_track_count,
        info.status,
        info.create_time,
        MAX(IF(info.stat_type = '0401', info.stat_num, 0)) as play_stat_num,
        MAX(IF(info.stat_type = '0402', info.stat_num, 0)) as subscribe_stat_num,
        MAX(IF(info.stat_type = '0403', info.stat_num, 0)) as buy_stat_num,
        MAX(IF(info.stat_type = '0404', info.stat_num, 0)) as comment_stat_num
        from (
        select
        album.id album_id,
        album.album_title,
        album.cover_url,
        album.include_track_count,
        album.status,
        album.create_time,
        stat.stat_type,
        stat.stat_num
        from album_info album
        left join album_stat stat on stat.album_id = album.id
        <where>
            <if test="vo.userId != null">
                and album.user_id = #{vo.userId}
            </if>
            <if test="vo.albumTitle != null and vo.albumTitle != ''">
                and album.album_title like CONCAT('%',#{vo.albumTitle},'%')
            </if>
            <if test="vo.status != null and vo.status != ''">
                and album.status = #{vo.status}
            </if>
            and album.is_deleted = 0
        </where>
        ) info
        group by info.album_id
        order by info.create_time desc
    </select>
</mapper>

4、删除专辑

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

4.1 控制层

AlbumInfoApiController控制器

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

4.2 业务层

AlbumInfoService接口

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

AlbumInfoServiceImpl实现类

@Override
@Transactional(rollbackFor = Exception.class)
public void removeAlbumInfoById(Long id) {
    //	删除专辑表的数据 album_info
    this.removeById(id);
    //	删除专辑属性信息
    albumAttributeValueMapper.delete(new LambdaQueryWrapper<AlbumAttributeValue>().eq(AlbumAttributeValue::getAlbumId, id));
    //	删除专辑对应的统计数据
    albumStatMapper.delete(new LambdaQueryWrapper<AlbumStat>().eq(AlbumStat::getAlbumId, id));
}

5、专辑修改

5.1 回显数据

  1. 需要根据专辑id获取到对应的回显数据,需要回显专辑与属性数据,不需要回显统计数据!
  2. 根据修改内容保存最新数据

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

BaseCategoryApiController控制器

/**
	 * 根据id 获取到专辑信息
	 * @param id
	 * @return
	 */
@Operation(summary = "获取专辑信息")
@GetMapping("/albumInfo/getAlbumInfo/{id}")
public Result<AlbumInfo> getAlbumInfoById(@PathVariable Long id) {
  //	调用服务层方法
  AlbumInfo albumInfo = albumInfoService.getAlbumInfoById(id);
  return Result.ok(albumInfo);
}

AlbumInfoService接口

/**
* 根据专辑Id 获取到专辑信息
* @param id
* @return
*/
AlbumInfo getAlbumInfoById(Long id);

AlbumInfoServiceImpl实现类

@Override
public AlbumInfo getAlbumInfoById(Long id) {
  //	根据id 查询专辑数据
  AlbumInfo albumInfo = this.getById(id);
  //	回显时,需要回显专辑数据信息数据
  if (albumInfo!=null){
    List<AlbumAttributeValue> albumAttributeValueList = albumAttributeValueMapper.selectList(new LambdaQueryWrapper<AlbumAttributeValue>().eq(AlbumAttributeValue::getAlbumId, id));
    albumInfo.setAlbumAttributeValueVoList(albumAttributeValueList);
  }
  return albumInfo;
}

5.2 保存修改之后的数据

涉及的表:

  • album_info 根据主键进行更新

  • album_attribute_value 先删除所有数据,再新增数据

AlbumInfoApiController控制器

/**
	 * 修改专辑信息
	 * @param id
	 * @param albumInfoVo
	 * @return
	 */
@Operation(summary = "修改专辑")
@PutMapping("/albumInfo/updateAlbumInfo/{id}")
public Result updateById(@PathVariable Long id,@RequestBody @Validated AlbumInfoVo albumInfoVo){
  //调用服务层方法
  albumInfoService.updateAlbumInfo(id,albumInfoVo);
  return Result.ok();
}

AlbumInfoService接口

/**
* 修改专辑数据
* @param id
* @param albumInfoVo
*/
void updateAlbumInfo(Long id, AlbumInfoVo albumInfoVo);

AlbumInfoServiceImpl实现类

@Override
@Transactional(rollbackFor = Exception.class)
public void updateAlbumInfo(Long id, AlbumInfoVo albumInfoVo) {
    //1.拷贝专辑入参到专辑PO对象
    AlbumInfo albumInfo = BeanUtil.copyProperties(albumInfoVo, AlbumInfo.class);
    albumInfo.setId(id);
    //2.修改专辑
    this.updateById(albumInfo);
    //3.处理专辑标签
    //3.1 先删除标签
    LambdaQueryWrapper<AlbumAttributeValue> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(AlbumAttributeValue::getAlbumId, id);
    albumAttributeValueMapper.delete(queryWrapper);
    //3.2 在新增专辑标签
    List<AlbumAttributeValueVo> albumAttributeValueVoList = albumInfoVo.getAlbumAttributeValueVoList();
    if (CollectionUtil.isNotEmpty(albumAttributeValueVoList)) {
        for (AlbumAttributeValueVo albumAttributeValueVo : albumAttributeValueVoList) {
            //	创建专辑标签对象
            AlbumAttributeValue albumAttributeValue = BeanUtil.copyProperties(albumAttributeValueVo, AlbumAttributeValue.class);
            //	赋值专辑属性Id
            albumAttributeValue.setAlbumId(id);
            //  保存专辑标签
            albumAttributeValueMapper.insert(albumAttributeValue);
        }
    }
}