第2章 声音管理.md 61 KB

谷粒随享

第2章 声音管理

学习目标:

  • 声音相关业务流程&数据模型
  • 新增声音(音视频文件上传到腾讯云点播服务)
  • 声音分页
  • 修改声音
  • 删除声音
  • 内容安全(文本,图片,音频)审核解决方案

1、新增声音

功能入口:运行小程序-->我的-->创作中心-->声音-->点击 + 添加声音

业务需求:当创作者新增专辑后,创作者将自己录制的音视频文件保存到声音。

  • 选择声音所属专辑
  • 设置声音封面图片
  • 选择本地录制好音频文件上传
  • 用户填写其他信息(声音标题、声音简介、是否公开)
  • 提交表单完成声音新增

1.1 获取用户专辑列表

需求:点击添加声音的时候会触发一个查询所有专辑列表,主要目的是为了让专辑与声音进行挂钩!

主要是根据userId,查询专辑Id 与专辑标题,然后按照专辑Id 进行降序排列。

声音-获取用户专辑列表

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

AlbumInfoApiController控制器

/**
 * TODO 该接口必须登录才能访问
 * 获取当前用户全部专辑列表
 * @return
 */
@Operation(summary = "获取当前用户全部专辑列表")
@GetMapping("/albumInfo/findUserAllAlbumList")
public Result<List<AlbumInfo>> getUserAllAlbumList(){
    //1.从ThreadLocal中获取当前登录用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.调用业务逻辑获取专辑列表
    List<AlbumInfo> list  = albumInfoService.getUserAllAlbumList(userId);
    return Result.ok(list);
}

AlbumInfoService接口

/**
 * 获取当前用户全部专辑列表
 * @param userId
 * @return
 */
List<AlbumInfo> getUserAllAlbumList(Long userId);

AlbumInfoServiceImpl实现类

/**
 * 获取当前用户全部专辑列表
 * @param userId
 * @return
 */
@Override
public List<AlbumInfo> getUserAllAlbumList(Long userId) {
    //1.构建查询条件QueryWrapper对象
    LambdaQueryWrapper<AlbumInfo> queryWrapper = new LambdaQueryWrapper<>();
    //1.1 查询条件
    queryWrapper.eq(AlbumInfo::getUserId, userId);
    //1.2 排序
    queryWrapper.orderByDesc(AlbumInfo::getId);
    //1.3 限制记录数
    queryWrapper.last("LIMIT 200");
    //1.4 指定查询列
    queryWrapper.select(AlbumInfo::getId, AlbumInfo::getAlbumTitle);
    //2.执行列表查询
    return albumInfoMapper.selectList(queryWrapper);
}

1.2 上传声音到点播服务

腾讯云点播服务面向音视频、图片等媒体,提供制作上传、存储、转码、媒体处理、媒体 AI、加速分发播放、版权保护等一体化高品质媒体服务。使用腾讯云服务可以为音视频文件的存储、传输、处理和分发提供可靠高效安全的解决方案。这可以节省你自己构建和维护存储基础设施的成本和精力,并为用户提供更好的音视频体验。

  1. 可靠性和可用性: 腾讯云提供高可靠性的存储服务,确保音视频文件的安全和持久保存。腾讯云具有跨多个地理区域和数据中心的数据冗余备份,以确保数据的高可用性和容灾能力。
  2. 扩展性和弹性: 腾讯云提供高度可扩展的存储解决方案,可以根据需要轻松扩展存储容量,适应不断增长的音视频数据需求。你可以根据实际情况动态调整存储空间,并随时新增或删除文件。
  3. 快速传输和低延迟: 腾讯云拥有全球范围的网络基础设施,可以提供快速的音视频文件上传和下载速度,同时降低传输延迟,使用户能够更快速地访问和共享文件。
  4. 安全性和权限控制: 腾讯云提供多层次的安全机制来保护用户的音视频文件。你可以使用腾讯云提供的身份验证和权限控制功能确保只有授权用户才能访问和管理文件。此外,腾讯云也提供数据加密和防止盗链等功能,以增强音视频文件的安全性。
  5. 多媒体处理和分发: 腾讯云提供了丰富的多媒体处理和分发服务,可以对上传的音视频文件进行转码、截图、剪辑等处理,并提供内容分发网络(CDN)加速服务,使用户能够高效地将音视频内容传送到全球各地的用户。

快速接入流程:云点播 快速入门-文档中心-腾讯云 (tencent.com)

  • 微信扫码登录

  • 关注公众号

  • 搜索云点播

  • 微信认证

  • 实名认证

  • 立即开通服务(点播平台、内容审核服务)

  • 右边:点击访问管理

准备工作:

  1. 应用管理导航菜单获取APPID

image-20231010095600292

  1. 访问管理新建用户,分配点播服务权限

image-20231010095827697

image-20231010100059988

image-20231010100134657

  1. 进入用户详情,获取秘钥

image-20231010100320526

  1. 在nacos配置中心service-album-dev.yaml中定义点播服务相关配置信息

    vod:
     appId: 1255727855 #需要修改为自己的
     secretId: AKIDTOFybJvQqCWnDdyDNRQJ54xkT8hBbxCK #需要修改为自己的
     secretKey: DKLH87saCmKZowS09IPRLE4pVCqQuKeu #需要修改为自己的
     region: ap-beijing #需要修改为自己的
     procedure: SimpleAesEncryptPreset #任务流
     #tempPath: /root/tingshu/tempPath
     tempPath: D:\code\workspace2023\tingshu\temp
     playKey: wrTwwu8U3DRSRDgC8l7q  #播放加密key
    
  2. 通过配置类读取(已提供)

    package com.atguigu.tingshu.album.config;
       
       
    import com.qcloud.vod.VodUploadClient;
    import com.tencentcloudapi.common.Credential;
    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 = "vod") //读取节点
    @Data
    public class VodConstantProperties {
       
       private Integer appId;
       private String secretId;
       private String secretKey;
       //https://cloud.tencent.com/document/api/266/31756#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
       private String region;
       private String procedure;
       private String tempPath;
       private String playKey;
       
       /**
        * 注册用于文件上传客户端对象
        *
        * @return
        */
       @Bean
       public VodUploadClient vodUploadClient() {
           return new VodUploadClient(secretId, secretKey);
       }
       
       /**
        * 调用点播平台认证对象
        * @return
        */
       @Bean
       public Credential cred() {
           return new Credential(secretId, secretKey);
       }
    }
    

需求:主播录制的音频文件需要上传到腾讯云-云点播云服务器,在声音表中存储音频文件腾讯云地址。

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

TrackInfoApiController控制器:

package com.atguigu.tingshu.album.api;

import com.atguigu.tingshu.album.service.TrackInfoService;
import com.atguigu.tingshu.album.service.VodService;
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;

import java.util.Map;

@Tag(name = "声音管理")
@RestController
@RequestMapping("api/album")
@SuppressWarnings({"all"})
public class TrackInfoApiController {

	@Autowired
	private TrackInfoService trackInfoService;

	@Autowired
	private VodService vodService;


	/**
	 * 音视频文件上传点播平台
	 * @param file
	 * @return {mediaFileId:"文件唯一标识",mediaUrl:"在线播放地址"}
	 */
	@Operation(summary = "音视频文件上传点播平台")
	@PostMapping("/trackInfo/uploadTrack")
	public Result<Map<String, String>> uploadTrack(@RequestParam("file")MultipartFile file) {
		Map<String, String> map = vodService.uploadTrack(file);
		return Result.ok(map);
	}

}

VodService接口

package com.atguigu.tingshu.album.service;

import org.springframework.web.multipart.MultipartFile;

import java.util.Map;

public interface VodService {

    /**
     * 音视频文件上传点播平台
     * @param file
     * @return
     */
    Map<String, String> uploadTrack(MultipartFile file);
}

VodServiceImpl实现类 云点播 Java SDK-开发指南-文档中心-腾讯云 (tencent.com) Java 语言实现声音上传功能API

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

import com.atguigu.tingshu.album.config.VodConstantProperties;
import com.atguigu.tingshu.album.service.VodService;
import com.atguigu.tingshu.common.util.UploadFileUtil;
import com.qcloud.vod.VodUploadClient;
import com.qcloud.vod.model.VodUploadRequest;
import com.qcloud.vod.model.VodUploadResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashMap;
import java.util.Map;


@Slf4j
@Service
public class VodServiceImpl implements VodService {

    @Autowired
    private VodConstantProperties vodConstantProperties;

    @Autowired
    private VodUploadClient vodUploadClient;


    /**
     * 音视频文件上传点播平台
     *
     * @param file
     * @return
     */
    @Override
    public Map<String, String> uploadTrack(MultipartFile file) {
        try {
            //1.将上传文件保存到本地得到文件路径 TODO:后续采用定时任务清理临时目录下使用完毕文件
            String filePath = UploadFileUtil.uploadTempPath(vodConstantProperties.getTempPath(), file);
            //2.构造上传请求对象:设置媒体本地上传路径
            VodUploadRequest request = new VodUploadRequest();
            request.setMediaFilePath(filePath);
            //3.调用上传方法,传入接入点地域及上传请求。
            VodUploadResponse response = vodUploadClient.upload(vodConstantProperties.getRegion(), request);
            //4.解析上传文件结果,得到文件标识以及访问路径
            if (response != null) {
                String fileId = response.getFileId();
                String mediaUrl = response.getMediaUrl();
                return Map.of("mediaFileId", fileId, "mediaUrl", mediaUrl);
            }
            return null;
        } catch (Exception e) {
            log.error("上传文件失败", e);
            throw new RuntimeException(e);
        }
    }
}

上传之后可以在:音视频管理 - 媒资管理 - 云点播 - 控制台 (tencent.com)-应用管理-点击主应用-媒资管理-音视频管理看是否有音频

1.3 保存声音

声音-保存声音

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

前端传递Json 字符串,此时我们可以使用封装好的TrackInfoVo实体类进行接收,方便处理数据

涉及到的表:

  • track_info 声音信息表

    • user_id、order_num、media_duration、media_size、media_type、source status 需要手动设置数据
    • user_id : 直接从工具类中获取(TODO暂时用固定用户ID
    • order_num : 声音在专辑中的排序值,从1开始依次递增,值越小排序越前,根据专辑Id上一条声音的排序值 并且按照声音Id 进行降序排列 并且获取第一条数.
    • media_duration media_size:根据流媒体Id腾讯点播平台获取到数据并赋值!
    • source status : 自己设置
  • track_stat 声音统计表

  • album_info 专辑表

TrackInfoApiController控制器

/**
 * TODO 该接口登录才可以访问
 * 保存声音
 * @param trackInfoVo
 * @return
 */
@Operation(summary = "保存声音")
@PostMapping("/trackInfo/saveTrackInfo")
public Result saveTrackInfo(@Validated @RequestBody TrackInfoVo trackInfoVo) {
	//1.获取用户ID
	Long userId = AuthContextHolder.getUserId();
	//2.调用业务层保存声音
	trackInfoService.saveTrackInfo(trackInfoVo, userId);
	return Result.ok();
}

TrackInfoService接口

package com.atguigu.tingshu.album.service;

import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.vo.album.TrackInfoVo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface TrackInfoService extends IService<TrackInfo> {
    /**
     * 保存声音
     * @param trackInfoVo
     * @param userId
     */
    void saveTrackInfo(TrackInfoVo trackInfoVo, Long userId);


    /**
     * 保存声音统计信息
     * @param trackId 声音ID
     * @param statType 统计类型
     * @param statNum 统计数值
     */
    void saveTrackStat(Long trackId, String statType, int statNum);
}

TrackInfoServiceImpl实现类

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

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import com.atguigu.tingshu.album.mapper.AlbumInfoMapper;
import com.atguigu.tingshu.album.mapper.TrackInfoMapper;
import com.atguigu.tingshu.album.mapper.TrackStatMapper;
import com.atguigu.tingshu.album.service.TrackInfoService;
import com.atguigu.tingshu.album.service.VodService;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.album.TrackStat;
import com.atguigu.tingshu.vo.album.TrackInfoVo;
import com.atguigu.tingshu.vo.album.TrackMediaInfoVo;
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.math.BigDecimal;

@Slf4j
@Service
@SuppressWarnings({"all"})
public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo> implements TrackInfoService {

    @Autowired
    private TrackInfoMapper trackInfoMapper;

    @Autowired
    private AlbumInfoMapper albumInfoMapper;

    @Autowired
    private VodService vodService;

    /**
     * 保存声音
     *
     * @param trackInfoVo
     * @param userId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveTrackInfo(TrackInfoVo trackInfoVo, Long userId) {
        //1.根据所属专辑ID查询专辑信息 得到封面图片,用于后续更新
        Long albumId = trackInfoVo.getAlbumId();
        AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
        if (albumInfo == null) {
            log.error("专辑:{}不存在", albumId);
            throw new GuiguException(404, "专辑不存在");
        }
        //2.新增声音记录
        //2.1 将声音VO转为PO
        TrackInfo trackInfo = BeanUtil.copyProperties(trackInfoVo, TrackInfo.class);
        //2.2 给属性赋值
        //2.2.1 设置用户ID
        trackInfo.setUserId(userId);
        //2.2.2 设置声音序号 要求从1开始递增
        trackInfo.setOrderNum(albumInfo.getIncludeTrackCount() + 1);
        //2.2.3 调用点播平台获取音频详情信息:时长、大小、类型
        TrackMediaInfoVo trackMediaInfoVo = vodService.getTrackMediaInfo(trackInfo.getMediaFileId());
        if (trackMediaInfoVo != null) {
            trackInfo.setMediaDuration(BigDecimal.valueOf(trackMediaInfoVo.getDuration()));
            trackInfo.setMediaSize(trackMediaInfoVo.getSize());
            trackInfo.setMediaType(trackMediaInfoVo.getType());
        }
        //2.2.4 来源:用户上传
        trackInfo.setSource(SystemConstant.TRACK_SOURCE_USER);
        //2.2.5 状态:待审核
        trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS);
        //2.2.6 封面图片 如果未提交使用所属专辑封面
        String coverUrl = trackInfo.getCoverUrl();
        if(StringUtils.isBlank(coverUrl)){
            trackInfo.setCoverUrl(albumInfo.getCoverUrl());
        }
        //2.3 新增声音记录
        trackInfoMapper.insert(trackInfo);
        Long trackId = trackInfo.getId();

        //3. 更新专辑信息:包含声音数量
        albumInfo.setIncludeTrackCount(albumInfo.getIncludeTrackCount() + 1);
        albumInfoMapper.updateById(albumInfo);

        //4.新增声音统计记录
        this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_PLAY, 0);
        this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_COLLECT, 0);
        this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_PRAISE, 0);
        this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_COMMENT, 0);

        //5.TODO 对点播平台音频文件进行审核(异步审核)
    }

    /**
     * 保存声音统计信息
     * @param trackId 声音ID
     * @param statType 统计类型
     * @param statNum 统计数值
     */
    @Override
    public void saveTrackStat(Long trackId, String statType, int statNum) {
        TrackStat trackStat = new TrackStat();
        trackStat.setTrackId(trackId);
        trackStat.setStatType(statType);
        trackStat.setStatNum(statNum);
        trackStatMapper.insert(trackStat);
    }
}

获取流媒体数据方法实现 参考地址API Explorer - 云 API - 控制台 (tencent.com)

VodService

/**
 * 获取音视频文件信息
 * @param mediaFileId
 * @return
 */
TrackMediaInfoVo getTrackMediaInfo(String mediaFileId);

VodServiceImpl

@Autowired
private Credential credential;

/**
 * 获取音视频文件信息
 *
 * @param mediaFileId 点播平台文件唯一标识
 * @return
 */
@Override
public TrackMediaInfoVo getTrackMediaInfo(String mediaFileId) {
    try {
        //1. 实例化要请求产品的client对象,clientProfile是可选的
        VodClient client = new VodClient(credential, vodConstantProperties.getRegion());
        //2.实例化一个请求对象,每个接口都会对应一个request对象
        DescribeMediaInfosRequest req = new DescribeMediaInfosRequest();
        String[] fileIds1 = {mediaFileId};
        req.setFileIds(fileIds1);

        //3.返回的resp是一个DescribeMediaInfosResponse的实例,与请求对象对应
        DescribeMediaInfosResponse resp = client.DescribeMediaInfos(req);
        //4.解析响应结果
        if (resp != null) {
            //4.1 获取音频详情结果集
            MediaInfo[] mediaInfoSet = resp.getMediaInfoSet();
            //4.2 获取音频详情对象
            MediaInfo mediaInfo = mediaInfoSet[0];
            //4.2.1 从详情基础信息获取类型
            TrackMediaInfoVo vo = new TrackMediaInfoVo();
            if (mediaInfo != null) {
                String type = mediaInfo.getBasicInfo().getType();
                vo.setType(type);
                //4.2.1 从详情元信息获取时长、大小
                MediaMetaData metaData = mediaInfo.getMetaData();
                vo.setDuration(metaData.getDuration());
                vo.setSize(metaData.getSize());
                return vo;
            }
        }
    } catch (TencentCloudSDKException e) {
        log.error("获取音视频文件:{}信息失败", mediaFileId, e);
        throw new RuntimeException(e);
    }
    return null;
}

2、查询声音列表

功能入口:运行小程序-->我的-->创作中心-->声音-->查询当前用户的声音列表,如下图所示:

声音-声音列表

查询分析:需要根据用户Id,状态或标题查询当前声音列表!这三个条件被封装到一个实体类中 TrackInfoQuery,返回结果对象封装到 TrackListVo 实体类中 修改下评论数属性:commentStatNum

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

TrackInfoApiController 控制器

/**
 * 条件分页查询当前用户声音列表(包含声音统计信息)
 *
 * @param page           页码
 * @param limit          页大小
 * @param trackInfoQuery 查询条件
 * @return
 */
@Operation(summary = "条件分页查询当前用户声音列表(包含声音统计信息)")
@PostMapping("/trackInfo/findUserTrackPage/{page}/{limit}")
public Result<Page<TrackListVo>> getUserTrackPage(
        @PathVariable int page,
        @PathVariable int limit,
        @RequestBody TrackInfoQuery trackInfoQuery
) {
    //1.获取当前用户ID
    Long userId = AuthContextHolder.getUserId();
    trackInfoQuery.setUserId(userId);
    //2.构建分页所需分页对象
    Page<TrackListVo> pageInfo = new Page<>(page, limit);
    //3.查询业务层(持久层)获取分页数据
    pageInfo = trackInfoService.getUserTrackPage(pageInfo, trackInfoQuery);
    return Result.ok(pageInfo);
}

TrackInfoService接口

/**
 * 条件分页查询当前用户声音列表(包含声音统计信息)
 *
 * @param pageInfo 分页对象
 * @param trackInfoQuery 查询条件(用户ID,关键字,审核状态)
 * @return
 */
Page<TrackListVo> getUserTrackPage(Page<TrackListVo> pageInfo, TrackInfoQuery trackInfoQuery);

TrackInfoServiceImpl实现类

/**
 * 条件分页查询当前用户声音列表(包含声音统计信息)
 *
 * @param pageInfo 分页对象
 * @param trackInfoQuery 查询条件(用户ID,关键字,审核状态)
 * @return
 */
@Override
public Page<TrackListVo> getUserTrackPage(Page<TrackListVo> pageInfo, TrackInfoQuery trackInfoQuery) {
    return trackInfoMapper.getUserTrackPage(pageInfo, trackInfoQuery);
}

TrackInfoMapper接口

package com.atguigu.tingshu.album.mapper;

import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.query.album.TrackInfoQuery;
import com.atguigu.tingshu.vo.album.TrackListVo;
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 TrackInfoMapper extends BaseMapper<TrackInfo> {


    /**
     * 条件分页查询当前用户声音列表(包含声音统计信息)
     * @param pageInfo
     * @param trackInfoQuery
     * @return
     */
    Page<TrackListVo> getUserTrackPage(Page<TrackListVo> pageInfo, @Param("vo") TrackInfoQuery trackInfoQuery);
}

SQL实现:

# 需求:条件分页查询当前用户声音列表(包含声音统计信息) 封装TrackListVo集合

# 第一步:采用左外连接关联查询声音表跟声音统计表 关联条件:统计表中声音ID跟声音表主键关联
select
    ti.id trackId,
    ti.album_id,
    ti.track_title,
    ti.cover_url,
    ti.media_duration,
    ti.status,
    stat_type,
    stat_num
from track_info ti left join  track_stat stat
on stat.track_id = ti.id;

# 第二步:根据声音ID进行分组 配合聚合函数max/sum + 判断if函数 完成行转列
# '统计类型:0701-播放量 0702-收藏量 0703-点赞量 0704-评论数',
select
    ti.id trackId,
    ti.album_id,
    ti.track_title,
    ti.cover_url,
    ti.media_duration,
    ti.status,
    max(if(stat_type='0701', stat_num, 0)) playStatNum,
    max(if(stat_type='0702', stat_num, 0)) collectStatNum,
    max(if(stat_type='0703', stat_num, 0)) praiseStatNum,
    max(if(stat_type='0704', stat_num, 0)) commentStatNum
from track_info ti left join  track_stat stat
                              on stat.track_id = ti.id
group by ti.id;

# 第三步:设置过滤,排序,分页
select
    ti.id trackId,
    ti.album_id,
    ti.track_title,
    ti.cover_url,
    ti.media_duration,
    ti.status,
    max(if(stat_type='0701', stat_num, 0)) playStatNum,
    max(if(stat_type='0702', stat_num, 0)) collectStatNum,
    max(if(stat_type='0703', stat_num, 0)) praiseStatNum,
    max(if(stat_type='0704', stat_num, 0)) commentStatNum
from track_info ti left join  track_stat stat
                              on stat.track_id = ti.id
where ti.user_id = ? and ti.is_deleted = 0 and ti.status = ?
group by ti.id
order by ti.id desc
limit 10;

# 第四步:查看执行计划
explain select
    ti.id trackId,
    ti.album_id,
    ti.track_title,
    ti.cover_url,
    ti.media_duration,
    ti.status,
    max(if(stat_type='0701', stat_num, 0)) playStatNum,
    max(if(stat_type='0702', stat_num, 0)) collectStatNum,
    max(if(stat_type='0703', stat_num, 0)) praiseStatNum,
    max(if(stat_type='0704', stat_num, 0)) commentStatNum
from track_info ti left join  track_stat stat
                              on stat.track_id = ti.id
where ti.user_id = ? and ti.is_deleted = 0 and ti.status = ?
group by ti.id
order by ti.id desc
limit 10;

TrackInfoMapper.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.TrackInfoMapper">
    <select id="findUserTrackPage" resultType="com.atguigu.tingshu.vo.album.TrackListVo">
        select
            ti.id as  track_id,
            ti.track_title,
            ti.cover_url,
            ti.media_duration,
            ti.status,
            max(if(stat.stat_type='0701', stat.stat_num, 0)) playStatNum,
            max(if(stat.stat_type='0702', stat.stat_num, 0)) collectStatNum,
            max(if(stat.stat_type='0703', stat.stat_num, 0)) praiseStatNum,
            max(if(stat.stat_type='0704', stat.stat_num, 0)) commentStatNum
        from track_info ti inner join track_stat stat on stat.track_id = ti.id and stat.is_deleted = 0
        <where>
            <if test="vo.userId != null">
                and ti.user_id = #{vo.userId}
            </if>
            <if test="vo.status != null and vo.status != ''">
                and  ti.status = #{vo.status}
            </if>
            <if test="vo.trackTitle != null and vo.trackTitle != ''">
                and  ti.track_title like concat('%',#{vo.trackTitle},'%')
            </if>
            and ti.is_deleted = 0
        </where>
        group by ti.id
        order by ti.id desc
    </select>
</mapper>

3、修改声音

声音-声音修改

3.1 回显声音详情

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

  1. 根据Id获取数据并回显
  2. 保存修改之后的数据

TrackInfoApiController控制器

/**
 * 根据声音ID查询声音信息
 *
 * @param id
 * @return
 */
@Operation(summary = "根据声音ID查询声音信息")
@GetMapping("/trackInfo/getTrackInfo/{id}")
public Result<TrackInfo> getTrackInfo(@PathVariable Long id) {
    TrackInfo trackInfo = trackInfoService.getById(id);
    return Result.ok(trackInfo);
}

3.2 修改声音

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

TrackInfoApiController控制器

传递声音Id ,封装好的TrackInfoVo 实体类。

/**
 * 修改声音信息
 * @param id
 * @param trackInfo
 * @return
 */
@Operation(summary = "修改声音信息")
@PutMapping("/trackInfo/updateTrackInfo/{id}")
public Result updateTrackInfo(@PathVariable Long id, @RequestBody TrackInfo trackInfo){
    trackInfoService.updateTrackInfo(trackInfo);
    return Result.ok();
}

TrackInfoService接口

/**
 * 修改声音信息
 * @param id
 * @param trackInfo
 * @return
 */
void updateTrackInfo(TrackInfo trackInfo);

TrackInfoServiceImpl实现类

/**
 * 修改声音信息
 *
 * @param id
 * @param trackInfo
 * @return
 */
@Override
public void updateTrackInfo(TrackInfo trackInfo) {
    //1.判断音频文件是否变更
    //1.1 根据声音ID查询声音记录得到“旧”的音频文件标识
    TrackInfo oldTrackInfo = trackInfoMapper.selectById(trackInfo.getId());
    //1.2 判断文件是否被更新
    if (!trackInfo.getMediaFileId().equals(oldTrackInfo.getMediaFileId())) {
        //1.3 如果文件被更新,再次获取新音频文件信息更新:时长,大小,类型
        TrackMediaInfoVo mediaInfoVo = vodService.getMediaInfo(trackInfo.getMediaFileId());
        if (mediaInfoVo != null) {
            trackInfo.setMediaType(mediaInfoVo.getType());
            trackInfo.setMediaDuration(BigDecimal.valueOf(mediaInfoVo.getDuration()));
            trackInfo.setMediaSize(mediaInfoVo.getSize());
            trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS);

            // 音频文件发生更新后,必须再次进行审核
            //4. 开启音视频任务审核;更新声音表:审核任务ID-后续采用定时任务检查审核结果
            //4.1 启动审核任务得到任务ID
            //String reviewTaskId = vodService.reviewMediaTask(trackInfo.getMediaFileId());
            //4.2 更新声音表:审核任务ID,状态(审核中)

            //4.2 更新声音表:审核任务ID,状态(审核中)
            //trackInfo.setReviewTaskId(reviewTaskId);
            //trackInfo.setStatus(SystemConstant.TRACK_STATUS_REVIEW_ING);
            //trackInfoMapper.updateById(trackInfo);
        }
        //1.4 从点播平台删除旧的音频文件
        vodService.deleteMedia(oldTrackInfo.getMediaFileId());
    }
    //2.更新声音信息
    trackInfoMapper.updateById(trackInfo);
}

VodService接口与实现类

/**
 * 删除音视频文件
 * @param mediaFileId
 */
void deleteMedia(String mediaFileId);

API Explorer - 云 API - 控制台 (tencent.com)

/**
 * 删除音视频文件
 *
 * @param mediaFileId
 */
@Override
public void deleteMedia(String mediaFileId) {
    try {
        //1.实例化一个认证对象
        Credential cred = new Credential(vodConstantProperties.getSecretId(), vodConstantProperties.getSecretKey());
        //2.实例化要请求产品的client对象,clientProfile是可选的
        VodClient client = new VodClient(cred, vodConstantProperties.getRegion());
        //3.实例化一个请求对象,每个接口都会对应一个request对象
        DeleteMediaRequest req = new DeleteMediaRequest();
        req.setFileId(mediaFileId);
        //4.返回的resp是一个DeleteMediaResponse的实例,与请求对象对应
        client.DeleteMedia(req);
    } catch (TencentCloudSDKException e) {
        log.error("[专辑服务]删除点播平台文件异常:{}", e);
    }
}

4、删除声音

声音-声音删除

涉及的表: track_info,album_info,track_stat,media

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

TrackInfoApiController控制器

/**
 * 删除声音记录
 * @param id
 * @return
 */
@Operation(summary = "删除声音记录")
@DeleteMapping("/trackInfo/removeTrackInfo/{id}")
public Result removeTrackInfo(@PathVariable Long id){
    trackInfoService.removeTrackInfo(id);
    return Result.ok();
}

TrackInfoService接口:

/**
 * 删除声音记录
 * @param id
 * @return
 */
void removeTrackInfo(Long id);

TrackInfoServiceImpl实现类

/**
 * 删除声音记录
 *
 * @param id
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void removeTrackInfo(Long id) {
    //1.根据ID查询欲被删除声音记录-得到专辑ID跟声音序号
    TrackInfo deleteTrackInfo = trackInfoMapper.selectById(id);
    Long albumId = deleteTrackInfo.getAlbumId();
    Integer orderNum = deleteTrackInfo.getOrderNum();

    //2.更新声音表序号-确保连续
    LambdaUpdateWrapper<TrackInfo> updateWrapper = new LambdaUpdateWrapper<>();
    //2.1 更新条件
    updateWrapper.eq(TrackInfo::getAlbumId, albumId);
    updateWrapper.gt(TrackInfo::getOrderNum, orderNum);
    //2.2 修改字段值
    updateWrapper.setSql("order_num = order_num - 1");
    trackInfoMapper.update(null, updateWrapper);

    //3.删除声音表记录
    trackInfoMapper.deleteById(id);

    //4.删除声音统计
    LambdaQueryWrapper<TrackStat> trackStatLambdaQueryWrapper = new LambdaQueryWrapper<>();
    trackStatLambdaQueryWrapper.eq(TrackStat::getTrackId, id);
    trackStatMapper.delete(trackStatLambdaQueryWrapper);

    //5.更新专辑包含声音数量
    AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
    albumInfo.setIncludeTrackCount(albumInfo.getIncludeTrackCount() - 1);
    albumInfoMapper.updateById(albumInfo);

    //6.删除点播平台音频文件
    vodService.deleteMedia(deleteTrackInfo.getMediaFileId());
}

5、内容审核方案

service-album模块pom.xml添加如下依赖

<dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java</artifactId>
    <version>3.1.1014</version>
</dependency>
<dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java-tms</artifactId>
    <version>3.1.1010</version>
</dependency>

文本内容安全接口:

图片内容安全接口:

音频内容安全接口:

5.1 文本&图片审核工具方法

  1. AuditService中新增方法用于审核文本

    package com.atguigu.tingshu.album.service;
       
    import org.springframework.web.multipart.MultipartFile;
       
    /**
    * @author: atguigu
    * @create: 2025-05-29 14:12
    */
    public interface AuditService {
       
       
       /**
        * 文本内容审核
        * @param content
        * @return
        */
       String auditText(String content);
       
       /**
        * 图片内容审核
        * @param file
        * @return
        */
       String auditImage(MultipartFile file);
       
    }
       
    
  2. AuditServiceImpl实现方法

    package com.atguigu.tingshu.album.service.impl;
       
    import cn.hutool.core.codec.Base64;
    import com.atguigu.tingshu.album.config.VodConstantProperties;
    import com.atguigu.tingshu.album.service.AuditService;
    import com.tencentcloudapi.common.AbstractModel;
    import com.tencentcloudapi.common.Credential;
    import com.tencentcloudapi.common.exception.TencentCloudSDKException;
    import com.tencentcloudapi.ims.v20201229.ImsClient;
    import com.tencentcloudapi.ims.v20201229.models.ImageModerationRequest;
    import com.tencentcloudapi.ims.v20201229.models.ImageModerationResponse;
    import com.tencentcloudapi.tms.v20201229.TmsClient;
    import com.tencentcloudapi.tms.v20201229.models.TextModerationRequest;
    import com.tencentcloudapi.tms.v20201229.models.TextModerationResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
       
    /**
    * @author: atguigu
    * @create: 2025-05-29 14:12
    */
    @Slf4j
    @Service
    public class AuditServiceImpl implements AuditService {
       
       @Autowired
       private Credential credential;
       
       @Autowired
       private VodConstantProperties vodConstantProperties;
       
       /**
        * 文本内容审核
        *
        * @param content
        * @return
        */
       @Override
       public String auditText(String content) {
           try {
               //1.实例化要请求产品的client对象,clientProfile是可选的
               TmsClient client = new TmsClient(credential, vodConstantProperties.getRegion());
               //2.实例化一个请求对象,每个接口都会对应一个request对象
               TextModerationRequest req = new TextModerationRequest();
               //对审核内容进行Base64编码
               req.setContent(Base64.encode(content));
               //3.返回的resp是一个TextModerationResponse的实例,与请求对象对应
               TextModerationResponse resp = client.TextModeration(req);
               //4.解析文本审核结果
               if (resp != null) {
                   String suggestion = resp.getSuggestion();
                   return suggestion.toLowerCase();
               }
           } catch (TencentCloudSDKException e) {
               log.error("文本内容:审核失败", content);
               System.out.println(e.toString());
           }
           return null;
       }
       
       /**
        * 图片内容审核
        *
        * @param file
        * @return
        */
       @Override
       public String auditImage(MultipartFile file) {
           try {
               //1.实例化要请求产品的client对象,clientProfile是可选的
               ImsClient client = new ImsClient(credential, vodConstantProperties.getRegion());
               //2.实例化一个请求对象,每个接口都会对应一个request对象
               ImageModerationRequest req = new ImageModerationRequest();
               //对图片进行Base64编码
               req.setFileContent(Base64.encode(file.getInputStream()));
               //3.返回的resp是一个ImageModerationResponse的实例,与请求对象对应
               ImageModerationResponse resp = client.ImageModeration(req);
               if (resp != null) {
                   String suggestion = resp.getSuggestion();
                   return suggestion.toLowerCase();
               }
           } catch (Exception e) {
               log.error("图片内容,{}审核失败", file.getOriginalFilename());
           }
           return null;
       }
       
       
    }
       
    
  3. 在保存专辑/声音,更新专辑/声音对文本内容进行内容审核

5.2 文本&图片审核

5.2.1 文本审核

AlbumInfoServiceImpl专辑新增&修改对文本审核

@Autowired
private AuditService auditService;

/**
 * 内容创作者或者平台运营人员-保存专辑
 *
 * @param albumInfoVo
 * @param userId
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAlbumInfo(AlbumInfoVo albumInfoVo, Long userId) {
    //1.保存专辑信息
    //1.1 将专辑VO转为PO对象
    AlbumInfo albumInfo = BeanUtil.copyProperties(albumInfoVo, AlbumInfo.class);

    //1.2 为专辑属性赋值:用户ID,试听集数,审核状态
    albumInfo.setUserId(userId);
    String payType = albumInfo.getPayType();
    if (ALBUM_PAY_TYPE_VIPFREE.equals(payType) || ALBUM_PAY_TYPE_REQUIRE.equals(payType)) {
        //只需要对VIP免费或付费资源设置试听集
        albumInfo.setTracksForFree(SystemConstant.TRACKSFORFREENUM);
    }
    albumInfo.setStatus(ALBUM_STATUS_NO_PASS);

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

    //2.保存专辑标签关系信息
    //2.1 获取VO中提交专辑标签关系集合
    List<AlbumAttributeValueVo> albumAttributeValueVoList = albumInfoVo.getAlbumAttributeValueVoList();
    if (CollUtil.isNotEmpty(albumAttributeValueVoList)) {
        //2.2 为专辑标签关联专辑ID,"批量"新增专辑标签关系
        for (AlbumAttributeValueVo albumAttributeValueVo : albumAttributeValueVoList) {
            AlbumAttributeValue albumAttributeValue = BeanUtil.copyProperties(albumAttributeValueVo, AlbumAttributeValue.class);
            albumAttributeValue.setAlbumId(albumId);
            albumAttributeValueMapper.insert(albumAttributeValue);
        }
    }

    //3.初始化专辑统计信息
    this.saveAlbumInfoStat(albumId, ALBUM_STAT_PLAY, 0);
    this.saveAlbumInfoStat(albumId, ALBUM_STAT_SUBSCRIBE, 0);
    this.saveAlbumInfoStat(albumId, ALBUM_STAT_BUY, 0);
    this.saveAlbumInfoStat(albumId, ALBUM_STAT_COMMENT, 0);

    //4.对专辑中文本内容进行审核  & 索引库ES中新增记录
    String text = albumInfo.getAlbumTitle() + albumInfo.getAlbumIntro();
    String suggestion = auditService.auditText(text);
    if ("pass".equals(suggestion)) {
        albumInfo.setStatus(ALBUM_STATUS_PASS);
        //TODO 发送MQ消息 通知 搜索服务 将专辑存入ES引库
    } else if ("review".equals(suggestion)) {
        albumInfo.setStatus(ALBUM_STATUS_REVIEW);
    } else if ("block".equals(suggestion)) {
        albumInfo.setStatus(ALBUM_STATUS_NO_PASS);
    }
    albumInfoMapper.updateById(albumInfo);
}


/**
 * 更新专辑信息
 *
 * @param id
 * @param albumInfoVo
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAlbumInfo(Long id, AlbumInfoVo albumInfoVo) {
    //1.更新专辑信息 状态:未审核
    AlbumInfo albumInfo = BeanUtil.copyProperties(albumInfoVo, AlbumInfo.class);
    albumInfo.setId(id);
    albumInfo.setStatus(ALBUM_STATUS_NO_PASS);
    albumInfoMapper.updateById(albumInfo);

    //2.更新专辑标签关系
    //2.1 根据专辑ID删除原有标签关系
    albumAttributeValueMapper.delete(
            new LambdaQueryWrapper<AlbumAttributeValue>()
                    .eq(AlbumAttributeValue::getAlbumId, id)
    );

    //2.2 从VO中获取提交专辑标签关系集合 再次批量保存
    List<AlbumAttributeValueVo> albumAttributeValueVoList = albumInfoVo.getAlbumAttributeValueVoList();
    if (CollUtil.isNotEmpty(albumAttributeValueVoList)) {
        //为专辑标签关联专辑ID,"批量"新增专辑标签关系
        for (AlbumAttributeValueVo albumAttributeValueVo : albumAttributeValueVoList) {
            AlbumAttributeValue albumAttributeValue = BeanUtil.copyProperties(albumAttributeValueVo, AlbumAttributeValue.class);
            albumAttributeValue.setAlbumId(id);
            albumAttributeValueMapper.insert(albumAttributeValue);
        }
    }
    //3.再次对内容进行审核
    String text = albumInfo.getAlbumTitle() + albumInfo.getAlbumIntro();
    String suggestion = auditService.auditText(text);
    if ("pass".equals(suggestion)) {
        albumInfo.setStatus(ALBUM_STATUS_PASS);
        //TODO 发送MQ消息 通知 搜索服务 将专辑存入ES引库
    } else if ("review".equals(suggestion)) {
        albumInfo.setStatus(ALBUM_STATUS_REVIEW);
        //TODO 发送MQ消息 通知 搜索服务 从ES引库删除
    } else if ("block".equals(suggestion)) {
        albumInfo.setStatus(ALBUM_STATUS_NO_PASS);
        //TODO 发送MQ消息 通知 搜索服务 从ES引库删除
    }
    albumInfoMapper.updateById(albumInfo);
}

TrackInfoServiceImpl声音保存修改新增文本审核逻辑

@Autowired
private AuditService auditService;

/**
 * 保存声音
 *
 * @param trackInfoVo
 * @param userId
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void saveTrackInfo(TrackInfoVo trackInfoVo, Long userId) {
    //1.根据所属专辑ID查询专辑信息 得到封面图片,用于后续更新
    Long albumId = trackInfoVo.getAlbumId();
    AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
    if (albumInfo == null) {
        log.error("专辑:{}不存在", albumId);
        throw new GuiguException(404, "专辑不存在");
    }
    //2.新增声音记录
    //2.1 将声音VO转为PO
    TrackInfo trackInfo = BeanUtil.copyProperties(trackInfoVo, TrackInfo.class);
    //2.2 给属性赋值
    //2.2.1 设置用户ID
    trackInfo.setUserId(userId);
    //2.2.2 设置声音序号 要求从1开始递增
    trackInfo.setOrderNum(albumInfo.getIncludeTrackCount() + 1);
    //2.2.3 调用点播平台获取音频详情信息:时长、大小、类型
    TrackMediaInfoVo trackMediaInfoVo = vodService.getTrackMediaInfo(trackInfo.getMediaFileId());
    if (trackMediaInfoVo != null) {
        trackInfo.setMediaDuration(BigDecimal.valueOf(trackMediaInfoVo.getDuration()));
        trackInfo.setMediaSize(trackMediaInfoVo.getSize());
        trackInfo.setMediaType(trackMediaInfoVo.getType());
    }
    //2.2.4 来源:用户上传
    trackInfo.setSource(SystemConstant.TRACK_SOURCE_USER);
    //2.2.5 状态:待审核
    trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS);
    //2.2.6 封面图片 如果未提交使用所属专辑封面
    String coverUrl = trackInfo.getCoverUrl();
    if (StringUtils.isBlank(coverUrl)) {
        trackInfo.setCoverUrl(albumInfo.getCoverUrl());
    }
    //2.3 新增声音记录
    trackInfoMapper.insert(trackInfo);
    Long trackId = trackInfo.getId();

    //3. 更新专辑信息:包含声音数量
    albumInfo.setIncludeTrackCount(albumInfo.getIncludeTrackCount() + 1);
    albumInfoMapper.updateById(albumInfo);

    //4.新增声音统计记录
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_PLAY, 0);
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_COLLECT, 0);
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_PRAISE, 0);
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_COMMENT, 0);

    //5.对点播平台音频文件进行审核(异步审核)
    String content = trackInfo.getTrackTitle() + trackInfo.getTrackIntro();
    String suggestion = auditService.auditText(content);
    if ("pass".equals(suggestion)) {
        trackInfo.setStatus(ALBUM_STATUS_PASS);
    } else if ("review".equals(suggestion)) {
        trackInfo.setStatus(ALBUM_STATUS_REVIEW);
    } else if ("block".equals(suggestion)) {
        trackInfo.setStatus(ALBUM_STATUS_NO_PASS);
    }
    trackInfoMapper.updateById(trackInfo);
}

/**
 * 更新声音信息
 *
 * @param id
 * @param trackInfoVo
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTrackInfo(TrackInfoVo trackInfoVo, Long id) {
    //1.判断音频是否修改过
    //1.1 根据声音ID查询声音记录,得到更新前音频ID
    TrackInfo trackInfo = trackInfoMapper.selectById(id);
    //1.2 对修改后的内容进行审核
    String content = trackInfoVo.getTrackTitle() + trackInfoVo.getTrackIntro();
    String suggestion = auditService.auditText(content);
    if ("pass".equals(suggestion)) {
        trackInfo.setStatus(ALBUM_STATUS_PASS);
    } else if ("review".equals(suggestion)) {
        trackInfo.setStatus(ALBUM_STATUS_REVIEW);
    } else if ("block".equals(suggestion)) {
        trackInfo.setStatus(ALBUM_STATUS_NO_PASS);
    }

    String oldMediaFileId = trackInfo.getMediaFileId();
    //1.2 跟参数中提交音频ID进行比较
    String newMediaFileId = trackInfoVo.getMediaFileId();
    if (!oldMediaFileId.equals(newMediaFileId)) {
        //2.如果修改过,获取新提交音频详情
        TrackMediaInfoVo trackMediaInfo = vodService.getTrackMediaInfo(newMediaFileId);
        if (trackMediaInfo != null) {
            trackInfo.setMediaUrl(trackInfoVo.getMediaUrl());
            trackInfo.setMediaFileId(newMediaFileId);
            trackInfo.setMediaDuration(BigDecimal.valueOf(trackMediaInfo.getDuration()));
            trackInfo.setMediaType(trackMediaInfo.getType());
            trackInfo.setMediaSize(trackMediaInfo.getSize());
        }
        //3.将原来旧文件从点播平台删除
        vodService.deleteTrackMedia(oldMediaFileId);
        //3.TODO 对修改后音频文件进行内容审核:发起审核任务
    }
    //4.更新声音记录,审核状态:未通过
    trackInfoMapper.updateById(trackInfo);
}

5.2.2 图片审核

  1. FileUploadServiceImpl中对上传图片进行审核

    @Autowired
    private AuditService auditService;
       
    /**
    * 图片上传到MInIO文件服务器
    *
    * @param file
    * @return
    */
    @Override
    public String fileUpload(MultipartFile file) {
       try {
           //1.校验文件是否为图片
           BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
           if (bufferedImage == null) {
               throw new GuiguException(500, "非法图片");
           }
           //2.限制图片文件大小 要求:900*900 像素,大小不超过2M
           int height = bufferedImage.getHeight();
           int width = bufferedImage.getWidth();
           if (height > 900 || width > 900) {
               throw new GuiguException(500, "图片限制900*900");
           }
       
           //对图片内容进行校验是否合法
           String suggestion = auditService.auditImage(file);
           if ("review".equals(suggestion) || "block".equals(suggestion)) {
               throw new GuiguException(500, "图片内容不合法");
           }
           //3.调用MInIOCLient对象文件上传
           //3.1 生成上传到MINIO文件唯一标识作为文件名称
           String folderName = "/" + DateUtil.today();
           String extName = FileUtil.extName(file.getOriginalFilename());
           String objectName = folderName + "/" + UUID.randomUUID().toString() + "." + extName;
       
           //3.2 调用putObject方法上传文件
           minioClient.putObject(
                   PutObjectArgs.builder().bucket(minioConstantProperties.getBucketName()).object(objectName).stream(
                                   file.getInputStream(), file.getSize(), -1)
                           .contentType("video/mp4")
                           .build());
           //4.返回文件访问路径 http://192.168.200.6:9000/tingshu/2023-05-05/f0d0f7c5-c0c5-4c0.png
           return minioConstantProperties.getEndpointUrl() + "/" + minioConstantProperties.getBucketName() + objectName;
       } catch (Exception e) {
           log.error("文件上传失败", e);
           throw new GuiguException(500, e.getMessage());
       }
       
    }
    

5.3 音频审核

5.3.1 发起审核任务

当声音保存后,需要对保存的声音进行内容审核,查询腾讯云点播平台文档得知服务端只能采用异步审核任务实现,后续发起的任务审核结果采用定时任务获取。

tips:因为发起审核后需要再声音表中记录声音对应的审核任务ID,故需要在track_info表中增加字段review_task_id在对应的实体类TrackInfo增加对应属性:

@Schema(description = "发起审核任务ID")
@TableField("review_task_id")
private String reviewTaskId;

5.3.1.1 TrackInfoServiceImpl

修改保存/修改声音方法:

/**
 * 更新声音信息
 *
 * @param id
 * @param trackInfoVo
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTrackInfo(TrackInfoVo trackInfoVo, Long id) {
    //1.判断音频是否修改过
    //1.1 根据声音ID查询声音记录,得到更新前音频ID
    TrackInfo trackInfo = trackInfoMapper.selectById(id);
    //1.2 对修改后的内容进行审核
    String content = trackInfoVo.getTrackTitle() + trackInfoVo.getTrackIntro();
    String suggestion = auditService.auditText(content);
    if ("pass".equals(suggestion)) {
        trackInfo.setStatus(TRACK_STATUS_PASS);
    } else if ("review".equals(suggestion)) {
        trackInfo.setStatus(TRACK_STATUS_REVIEWING);
    } else if ("block".equals(suggestion)) {
        trackInfo.setStatus(TRACK_STATUS_NO_PASS);
    }

    String oldMediaFileId = trackInfo.getMediaFileId();
    //1.2 跟参数中提交音频ID进行比较
    String newMediaFileId = trackInfoVo.getMediaFileId();
    if (!oldMediaFileId.equals(newMediaFileId)) {
        //2.如果修改过,获取新提交音频详情
        TrackMediaInfoVo trackMediaInfo = vodService.getTrackMediaInfo(newMediaFileId);
        if (trackMediaInfo != null) {
            trackInfo.setMediaUrl(trackInfoVo.getMediaUrl());
            trackInfo.setMediaFileId(newMediaFileId);
            trackInfo.setMediaDuration(BigDecimal.valueOf(trackMediaInfo.getDuration()));
            trackInfo.setMediaType(trackMediaInfo.getType());
            trackInfo.setMediaSize(trackMediaInfo.getSize());
        }
        //3.将原来旧文件从点播平台删除
        vodService.deleteTrackMedia(oldMediaFileId);
        //3.对修改后音频文件进行内容审核:发起审核任务
        String reviewTaskId = auditService.startReviewTask(trackInfo.getMediaFileId());
        trackInfo.setStatus(TRACK_STATUS_REVIEWING);
        trackInfo.setReviewTaskId(reviewTaskId);
    }
    //4.更新声音记录,审核状态:未通过
    trackInfoMapper.updateById(trackInfo);
}

/**
 * 保存声音
 *
 * @param trackInfoVo
 * @param userId
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void saveTrackInfo(TrackInfoVo trackInfoVo, Long userId) {
    //1.根据所属专辑ID查询专辑信息 得到封面图片,用于后续更新
    Long albumId = trackInfoVo.getAlbumId();
    AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
    if (albumInfo == null) {
        log.error("专辑:{}不存在", albumId);
        throw new GuiguException(404, "专辑不存在");
    }
    //2.新增声音记录
    //2.1 将声音VO转为PO
    TrackInfo trackInfo = BeanUtil.copyProperties(trackInfoVo, TrackInfo.class);
    //2.2 给属性赋值
    //2.2.1 设置用户ID
    trackInfo.setUserId(userId);
    //2.2.2 设置声音序号 要求从1开始递增
    trackInfo.setOrderNum(albumInfo.getIncludeTrackCount() + 1);
    //2.2.3 调用点播平台获取音频详情信息:时长、大小、类型
    TrackMediaInfoVo trackMediaInfoVo = vodService.getTrackMediaInfo(trackInfo.getMediaFileId());
    if (trackMediaInfoVo != null) {
        trackInfo.setMediaDuration(BigDecimal.valueOf(trackMediaInfoVo.getDuration()));
        trackInfo.setMediaSize(trackMediaInfoVo.getSize());
        trackInfo.setMediaType(trackMediaInfoVo.getType());
    }
    //2.2.4 来源:用户上传
    trackInfo.setSource(SystemConstant.TRACK_SOURCE_USER);
    //2.2.5 状态:待审核
    trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS);
    //2.2.6 封面图片 如果未提交使用所属专辑封面
    String coverUrl = trackInfo.getCoverUrl();
    if (StringUtils.isBlank(coverUrl)) {
        trackInfo.setCoverUrl(albumInfo.getCoverUrl());
    }
    //2.3 新增声音记录
    trackInfoMapper.insert(trackInfo);
    Long trackId = trackInfo.getId();

    //3. 更新专辑信息:包含声音数量
    albumInfo.setIncludeTrackCount(albumInfo.getIncludeTrackCount() + 1);
    albumInfoMapper.updateById(albumInfo);

    //4.新增声音统计记录
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_PLAY, 0);
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_COLLECT, 0);
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_PRAISE, 0);
    this.saveTrackStat(trackId, SystemConstant.TRACK_STAT_COMMENT, 0);

    //5.内容审核-文本审核
    String content = trackInfo.getTrackTitle() + trackInfo.getTrackIntro();
    String suggestion = auditService.auditText(content);
    if ("pass".equals(suggestion)) {
        trackInfo.setStatus(TRACK_STATUS_PASS);
    } else if ("review".equals(suggestion)) {
        trackInfo.setStatus(TRACK_STATUS_REVIEWING);
    } else if ("block".equals(suggestion)) {
        trackInfo.setStatus(TRACK_STATUS_NO_PASS);
    }
    //6.对点播平台音频文件进行审核(异步审核)
    String reviewTaskId = auditService.startReviewTask(trackInfo.getMediaFileId());
    trackInfo.setStatus(TRACK_STATUS_REVIEWING);
    trackInfo.setReviewTaskId(reviewTaskId);
    trackInfoMapper.updateById(trackInfo);
}

5.3.1.2 AuditService

/**
 * 启动审核任务,开始对音视频文件进行审核
 * @param mediaFileId
 * @return
 */
String startReviewTask(String mediaFileId);


/**
 * 根据审核任务ID查询审核结果
 * @param taskId
 * @return
 */
String getReviewTaskResult(String taskId);

5.3.1.3 AuditServiceImpl

/**
 * 启动审核任务
 *
 * @param mediaFileId
 * @return
 */
@Override
public String startReviewTask(String mediaFileId) {
    try {
        //1. 实例化要请求产品的client对象,clientProfile是可选的
        VodClient client = new VodClient(credential, vodConstantProperties.getRegion());
        //2.实例化一个请求对象,每个接口都会对应一个request对象
        ReviewAudioVideoRequest req = new ReviewAudioVideoRequest();
        req.setFileId(mediaFileId);
        //3.返回的resp是一个ReviewAudioVideoResponse的实例,与请求对象对应
        ReviewAudioVideoResponse resp = client.ReviewAudioVideo(req);
        if (resp != null) {
            return resp.getTaskId();
        }
    } catch (TencentCloudSDKException e) {
        log.error("启动审核任务失败", e);
    }
    return null;
}

/**
 * 根据审核任务ID查询审核建议
 *
 * @param taskId
 * @return
 */
@Override
public String getReviewTaskResult(String taskId) {
    try {
        //1.实例化要请求产品的client对象,clientProfile是可选的
        VodClient client = new VodClient(credential, vodConstantProperties.getRegion());
        //2.实例化一个请求对象,每个接口都会对应一个request对象
        DescribeTaskDetailRequest req = new DescribeTaskDetailRequest();
        req.setTaskId(taskId);
        //3.返回的resp是一个DescribeTaskDetailResponse的实例,与请求对象对应
        DescribeTaskDetailResponse resp = client.DescribeTaskDetail(req);
        if (resp != null) {
            //3.1 获取任务类型:音视频审核任务;
            if ("ReviewAudioVideo".equals(resp.getTaskType())) {
                //3.2 判断任务状态: 已完成
                if ("FINISH".equals(resp.getStatus())) {
                    //3.3 获取音视频审核结果
                    ReviewAudioVideoTask reviewAudioVideoTask = resp.getReviewAudioVideoTask();
                    //3.3.1 判断审核任务状态 FINISH
                    if ("FINISH".equals(reviewAudioVideoTask.getStatus())) {
                        //3.3.2 音视频审核任务输出 建议结果
                        ReviewAudioVideoTaskOutput output = reviewAudioVideoTask.getOutput();
                        String suggestion = output.getSuggestion();
                        return suggestion;
                    }
                }
            }
        }
    } catch (TencentCloudSDKException e) {
        System.out.println(e.toString());
    }
    return null;
}

5.3.2 定时任务获取审核结果

  1. 启动类上开启定时任务,加注解@EnableScheduling

    @SpringBootApplication//(scanBasePackages = {"cn.atguigu", "com.atguigu"})  //默认扫描到启动类所在包,扫描当前模块以及引入的jar
    @EnableDiscoveryClient
    @EnableFeignClients
    @EnableScheduling  //开启定时任务
    public class ServiceAlbumApplication {
       
       public static void main(String[] args) {
           SpringApplication.run(ServiceAlbumApplication.class, args);
       }
       
    }
    
  2. 新增定时任务类

    package com.atguigu.tingshu.album.task;
       
    import cn.hutool.core.collection.CollUtil;
    import com.atguigu.tingshu.album.mapper.TrackInfoMapper;
    import com.atguigu.tingshu.album.service.AuditService;
    import com.atguigu.tingshu.model.album.TrackInfo;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
       
    import java.util.List;
       
    import static com.atguigu.tingshu.common.constant.SystemConstant.*;
       
    /**
    * @author: atguigu
    * @create: 2025-05-29 15:14
    */
    @Slf4j
    @Component
    public class ReviceResultTask {
       
       @Autowired
       private TrackInfoMapper trackInfoMapper;
       
       
       @Autowired
       private AuditService auditService;
       
       
       /**
        * 定时获取处于审核中声音,音频内容审核结果
        */
       @Scheduled(cron = "0/5 * * * * ?")
       public void reviceResultJob() {
           log.info("开始获取审核结果");
           //1.根据条件:1.审核中状态  2.限制数量 3.查询声音ID跟审核任务ID
           List<TrackInfo> trackInfoList = trackInfoMapper.selectList(
                   new LambdaQueryWrapper<TrackInfo>()
                           .eq(TrackInfo::getStatus, TRACK_STATUS_REVIEWING)
                           .select(TrackInfo::getId, TrackInfo::getReviewTaskId)
                           .orderByAsc(TrackInfo::getId)
                           .last("limit 100")
           );
           //2.遍历声音列表
           if (CollUtil.isNotEmpty(trackInfoList)) {
               //2.1 根据声音表中审核任务ID查询审核结果
               for (TrackInfo trackInfo : trackInfoList) {
                   //2.2 根据审核建议更新审核建议
                   String suggestion = auditService.getReviewTaskResult(trackInfo.getReviewTaskId());
                   if(StringUtils.isNotBlank(suggestion)){
                       if ("pass".equals(suggestion)) {
                           trackInfo.setStatus(TRACK_STATUS_PASS);
                       } else if ("review".equals(suggestion)) {
                           trackInfo.setStatus(TRACK_STATUS_REVIEWING);
                       } else if ("block".equals(suggestion)) {
                           trackInfo.setStatus(TRACK_STATUS_NO_PASS);
                       }
                       trackInfoMapper.updateById(trackInfo);
                   }
               }
           }
       
       }
    }