**谷粒随享** ## 第2章 声音管理 **学习目标:** - 声音相关业务流程&数据模型 - 新增声音(音视频文件上传到腾讯云点播服务) - 声音分页 - 修改声音 - 删除声音 - 内容安全(文本,图片,音频)审核解决方案 # 1、新增声音 **功能入口**:运行小程序-->我的-->创作中心-->声音-->点击 **+** 添加声音 业务需求:当创作者新增专辑后,创作者将自己录制的音视频文件保存到声音。 - 选择声音所属专辑 - 设置声音封面图片 - 选择本地录制好音频文件上传 - 用户填写其他信息(声音标题、声音简介、是否公开) - 提交表单完成声音新增 ## 1.1 获取用户专辑列表 需求:点击添加声音的时候会触发一个查询所有专辑列表,主要目的是为了让专辑与声音进行挂钩! 主要是根据userId,查询专辑Id 与专辑标题,然后按照专辑Id 进行降序排列。 ![声音-获取用户专辑列表](assets/声音-获取用户专辑列表.gif) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/27 **AlbumInfoApiController控制器** ```java /** * TODO 该接口必须登录才能访问 * 获取当前用户全部专辑列表 * @return */ @Operation(summary = "获取当前用户全部专辑列表") @GetMapping("/albumInfo/findUserAllAlbumList") public Result> getUserAllAlbumList(){ //1.从ThreadLocal中获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用业务逻辑获取专辑列表 List list = albumInfoService.getUserAllAlbumList(userId); return Result.ok(list); } ``` **AlbumInfoService接口** ```java /** * 获取当前用户全部专辑列表 * @param userId * @return */ List getUserAllAlbumList(Long userId); ``` **AlbumInfoServiceImpl**实现类 ```java /** * 获取当前用户全部专辑列表 * @param userId * @return */ @Override public List getUserAllAlbumList(Long userId) { //1.构建查询条件QueryWrapper对象 LambdaQueryWrapper 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)](https://cloud.tencent.com/document/product/266/8757) - 微信扫码登录 - 关注公众号 - 搜索云点播 - 微信认证 - 实名认证 - 立即开通服务(点播平台、内容审核服务) - 右边:点击访问管理 准备工作: 1. 应用管理导航菜单获取APPID ![image-20231010095600292](assets/image-20231010095600292.png) 2. 访问管理新建用户,分配点播服务权限 ![image-20231010095827697](assets/image-20231010095827697.png) ![image-20231010100059988](assets/image-20231010100059988.png) ![image-20231010100134657](assets/image-20231010100134657.png) 3. 进入用户详情,获取秘钥 ![image-20231010100320526](assets/image-20231010100320526.png) 4. 在nacos配置中心`service-album-dev.yaml`中定义点播服务相关配置信息 ```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 ``` 5. 通过配置类读取**(已提供)** ```java package com.atguigu.tingshu.album.config; import com.qcloud.vod.VodUploadClient; 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); } } ``` **需求**:主播录制的音频文件需要上传到腾讯云-云点播云服务器,在`声音表`中存储音频文件腾讯云地址。 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/29 **TrackInfoApiController控制器:** ```java 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> uploadTrack(@RequestParam("file") MultipartFile file) { Map map = vodService.uploadTrack(file); return Result.ok(map); } } ``` **VodService接口** ```java package com.atguigu.tingshu.album.service; import org.springframework.web.multipart.MultipartFile; import java.util.Map; public interface VodService { /** * 音视频文件上传点播平台 * @param file 文件 * @return {mediaFileId:"文件唯一标识",mediaUrl:"在线播放地址"} */ Map uploadTrack(MultipartFile file); } ``` **VodServiceImpl实现类** [云点播 Java SDK-开发指南-文档中心-腾讯云 (tencent.com)](https://cloud.tencent.com/document/product/266/10276) Java 语言实现声音上传功能API ```java 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 {mediaFileId:"文件唯一标识",mediaUrl:"在线播放地址"} */ @Override public Map uploadTrack(MultipartFile file) { try { //1. 将上传文件保存到临时目录下 String uploadTempPath = UploadFileUtil.uploadTempPath(vodConstantProperties.getTempPath(), file); //2.构造上传请求对象 设置媒体本地上传路径 VodUploadRequest request = new VodUploadRequest(); request.setMediaFilePath(uploadTempPath); //3.调用上传,完成文件上传 VodUploadResponse response = uploadClient.upload(vodConstantProperties.getRegion(), request); //4.解析上传响应对象封装响应结果 if (response != null) { String fileId = response.getFileId(); String mediaUrl = response.getMediaUrl(); Map map = new HashMap<>(); map.put("mediaFileId", fileId); map.put("mediaUrl", mediaUrl); return map; } return null; } catch (Exception e) { throw new GuiguException(500, "上传文件到云点播服务异常:" + e); } } } ``` 上传之后可以在:[音视频管理 - 媒资管理 - 云点播 - 控制台 (tencent.com)-应用管理-点击主应用-媒资管理-音视频管理](https://console.cloud.tencent.com/vod/media)看是否有音频 ## 1.3 保存声音 ![声音-保存声音](assets/声音-保存声音.gif) > 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控制器** ```java /** * TODO 该接口必须登录才能访问 * 保存声音 * * @param trackInfo * @return */ @Operation(summary = "保存声音") @PostMapping("/trackInfo/saveTrackInfo") public Result saveTrackInfo(@RequestBody TrackInfo trackInfo) { //1.获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用业务层保存声音-发起审核任务 trackInfoService.saveTrackInfo(userId, trackInfo); return Result.ok(); } ``` **TrackInfoService接口** ```java 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 { /** * 保存声音 * @param userId 用户ID * @param trackInfo 专辑信息 */ void saveTrackInfo(Long userId, TrackInfo trackInfo); /** * 保存声音统计信息 * @param trackId 声音ID * @param statType 统计类型 * @param statNum 统计数值 */ void saveTrackStat(Long trackId, String statType, int statNum); } ``` **TrackInfoServiceImpl实现类** ```java 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 implements TrackInfoService { @Autowired private TrackInfoMapper trackInfoMapper; @Autowired private AlbumInfoMapper albumInfoMapper; @Autowired private VodService vodService; /** * 保存声音 * * @param userId 用户ID * @param trackInfo 专辑信息 */ @Override @Transactional(rollbackFor = Exception.class) public void saveTrackInfo(Long userId, TrackInfo trackInfo) { //1.根据专辑ID查询专辑信息-更新专辑信息;复用专辑封面图片 AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId()); //2.新增声音 //2.1 设置声音额外基本信息:用户ID,声音排序序号,状态,来源 trackInfo.setUserId(userId); trackInfo.setOrderNum(albumInfo.getIncludeTrackCount() + 1); trackInfo.setSource(SystemConstant.TRACK_SOURCE_USER); trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS); if(StringUtils.isBlank(trackInfo.getCoverUrl())){ trackInfo.setCoverUrl(albumInfo.getCoverUrl()); } //2.2 远程调用腾讯点播平台获取音频详情信息 TrackMediaInfoVo mediaInfoVo = vodService.getMediaInfo(trackInfo.getMediaFileId()); if (mediaInfoVo != null) { trackInfo.setMediaDuration(BigDecimal.valueOf(mediaInfoVo.getDuration())); trackInfo.setMediaSize(mediaInfoVo.getSize()); trackInfo.setMediaType(mediaInfoVo.getType()); } //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 调用点播平台发起音频文件审核任务(异步审核) } } ``` 初始化统计数据 ```java @Autowired private TrackStatMapper trackStatMapper; /** * 保存声音统计信息 * @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)](https://console.cloud.tencent.com/api/explorer?Product=vod&Version=2018-07-17&Action=DescribeMediaInfos) **VodService** ```java /** * 获取点播平台音频详情信息 * @param mediaFileId 文件唯一标识 * @return */ TrackMediaInfoVo getMediaInfo(String mediaFileId); ``` **VodServiceImpl** ```java /** * 获取点播平台音频详情信息 * * @param mediaFileId 文件唯一标识 * @return */ @Override public TrackMediaInfoVo getMediaInfo(String mediaFileId) { try { //1.实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 Credential cred = new Credential(vodConstantProperties.getSecretId(), vodConstantProperties.getSecretKey()); //2.实例化要请求产品的client对象,clientProfile是可选的 VodClient client = new VodClient(cred, vodConstantProperties.getRegion()); //3.实例化一个请求对象,每个接口都会对应一个request对象 DescribeMediaInfosRequest req = new DescribeMediaInfosRequest(); String[] fileIds1 = {mediaFileId}; req.setFileIds(fileIds1); //4.发起请求,返回的resp是一个DescribeMediaInfosResponse的实例,与请求对象对应 DescribeMediaInfosResponse resp = client.DescribeMediaInfos(req); if (resp != null) { //5.解析结果获取响应结果中文件详情列表集合 MediaInfo[] mediaInfoSet = resp.getMediaInfoSet(); //5.1 获取第一个详情对象 if (mediaInfoSet != null && mediaInfoSet.length > 0) { MediaInfo mediaInfo = mediaInfoSet[0]; if (mediaInfo != null) { //5.2 获取基本信息 MediaBasicInfo basicInfo = mediaInfo.getBasicInfo(); //5.3 获取元信息 MediaMetaData metaData = mediaInfo.getMetaData(); //5.4 封装出参对象 TrackMediaInfoVo trackMediaInfoVo = new TrackMediaInfoVo(); trackMediaInfoVo.setType(basicInfo.getType()); trackMediaInfoVo.setDuration(metaData.getDuration()); trackMediaInfoVo.setSize(metaData.getSize()); return trackMediaInfoVo; } } } } catch (Exception e) { throw new GuiguException(500, "点播平台获取音频详情异常:" + e); } return null; } ``` # 2、查询声音列表 **功能入口**:运行小程序-->我的-->创作中心-->声音-->查询当前用户的声音列表,如下图所示: ![声音-声音列表](assets/声音-声音列表.gif) 查询分析:需要根据用户Id,状态或标题查询当前声音列表!这三个条件被封装到一个实体类中 **TrackInfoQuery**,返回结果对象封装到 **TrackListVo** 实体类中 修改下评论数属性:**commentStatNum** > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/33 **TrackInfoApiController 控制器** ```java /** * 条件分页查询当前用户声音列表(包含声音统计信息) * * @param page 页码 * @param limit 页大小 * @param trackInfoQuery 查询条件 * @return */ @Operation(summary = "条件分页查询当前用户声音列表(包含声音统计信息)") @PostMapping("/trackInfo/findUserTrackPage/{page}/{limit}") public Result> getUserTrackPage( @PathVariable int page, @PathVariable int limit, @RequestBody TrackInfoQuery trackInfoQuery ) { //1.获取当前用户ID Long userId = AuthContextHolder.getUserId(); trackInfoQuery.setUserId(userId); //2.构建分页所需分页对象 Page pageInfo = new Page<>(page, limit); //3.查询业务层(持久层)获取分页数据 pageInfo = trackInfoService.getUserTrackPage(pageInfo, trackInfoQuery); return Result.ok(pageInfo); } ``` **TrackInfoService接口** ```java /** * 条件分页查询当前用户声音列表(包含声音统计信息) * * @param pageInfo 分页对象 * @param trackInfoQuery 查询条件(用户ID,关键字,审核状态) * @return */ Page getUserTrackPage(Page pageInfo, TrackInfoQuery trackInfoQuery); ``` **TrackInfoServiceImpl实现类** ```java /** * 条件分页查询当前用户声音列表(包含声音统计信息) * * @param pageInfo 分页对象 * @param trackInfoQuery 查询条件(用户ID,关键字,审核状态) * @return */ @Override public Page getUserTrackPage(Page pageInfo, TrackInfoQuery trackInfoQuery) { return trackInfoMapper.getUserTrackPage(pageInfo, trackInfoQuery); } ``` **TrackInfoMapper接口** ```java 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 { /** * 条件分页查询当前用户声音列表(包含声音统计信息) * @param pageInfo * @param trackInfoQuery * @return */ Page getUserTrackPage(Page pageInfo, @Param("vo") TrackInfoQuery trackInfoQuery); } ``` SQL实现: ```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 ``` # 3、修改声音 ![声音-声音修改](assets/声音-声音修改.gif) ## 3.1 回显声音详情 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/35 1. 根据Id获取数据并回显 2. 保存修改之后的数据 **TrackInfoApiController**控制器 ```java /** * 根据声音ID查询声音信息 * * @param id * @return */ @Operation(summary = "根据声音ID查询声音信息") @GetMapping("/trackInfo/getTrackInfo/{id}") public Result 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 实体类。 ```java /** * 修改声音信息 * @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接口** ```java /** * 修改声音信息 * @param id * @param trackInfo * @return */ void updateTrackInfo(TrackInfo trackInfo); ``` **TrackInfoServiceImpl实现类** ```java /** * 修改声音信息 * * @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接口**与实现类 ```java /** * 删除音视频文件 * @param mediaFileId */ void deleteMedia(String mediaFileId); ``` [API Explorer - 云 API - 控制台 (tencent.com)](https://console.cloud.tencent.com/api/explorer?Product=vod&Version=2018-07-17&Action=DeleteMedia) ```java /** * 删除音视频文件 * * @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、删除声音 ![声音-声音删除](assets/声音-声音删除.gif) 涉及的表: track_info,album_info,track_stat,media > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/39 **TrackInfoApiController控制器** ```java /** * 删除声音记录 * @param id * @return */ @Operation(summary = "删除声音记录") @DeleteMapping("/trackInfo/removeTrackInfo/{id}") public Result removeTrackInfo(@PathVariable Long id){ trackInfoService.removeTrackInfo(id); return Result.ok(); } ``` **TrackInfoService接口:** ```java /** * 删除声音记录 * @param id * @return */ void removeTrackInfo(Long id); ``` **TrackInfoServiceImpl实现类** ```java /** * 删除声音记录 * * @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 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 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添加如下依赖 ```xml com.tencentcloudapi tencentcloud-sdk-java 3.1.1014 com.tencentcloudapi tencentcloud-sdk-java-tms 3.1.1010 ``` 文本内容安全接口: - https://console.cloud.tencent.com/api/explorer?Product=tms&Version=2020-12-29&Action=TextModeration 图片内容安全接口: - https://console.cloud.tencent.com/api/explorer?Product=ims&Version=2020-12-29&Action=ImageModeration 音频内容安全接口: - 发起审核任务:https://cloud.tencent.com/document/api/266/80283 - 查询审核结果:https://cloud.tencent.com/document/product/266/33431 ## 5.1 文本审核 1. 在`vodService中`新增方法用于审核文本 ```java /** * 文本审核 * @param content * @return */ String scanText(String content); ``` 2. `VodServiceImpl`实现方法 ```java /** * 文本审核 * * @param content * @return */ @Override public String scanText(String content) { try { //1.实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 Credential cred = new Credential(vodConstantProperties.getSecretId(), vodConstantProperties.getSecretKey()); //2.实例化要请求产品的client对象,clientProfile是可选的 TmsClient client = new TmsClient(cred, vodConstantProperties.getRegion()); //3.实例化一个请求对象,每个接口都会对应一个request对象 TextModerationRequest req = new TextModerationRequest(); String encodeContent = Base64.encode(content); req.setContent(encodeContent); //4.返回的resp是一个TextModerationResponse的实例,与请求对象对应 TextModerationResponse resp = client.TextModeration(req); //5.解析结果 if (resp != null) { String suggestion = resp.getSuggestion(); return suggestion.toLowerCase(); } // 输出json格式的字符串回包 } catch (TencentCloudSDKException e) { log.error("[点播平台]内容安全文本检测异常:{}", e); } return null; } ``` 3. 在保存专辑/声音,更新专辑/声音对文本内容进行内容审核 ## 5.2 图片审核 1. 在**VodService**中新增图片审核方法 ```java /** * 图片审核 * @param file 图片文件 * @return */ String scanImage(MultipartFile file); ``` 2. 在**VodServiceImpl**实现方法 ```java /** * 图片审核 * * @param file 图片文件 * @return */ @Override public String scanImage(MultipartFile file) { try { //1.实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 Credential cred = new Credential(vodConstantProperties.getSecretId(), vodConstantProperties.getSecretKey()); //2.实例化要请求产品的client对象,clientProfile是可选的 ImsClient client = new ImsClient(cred, vodConstantProperties.getRegion()); //3.实例化一个请求对象,每个接口都会对应一个request对象 ImageModerationRequest req = new ImageModerationRequest(); String encodeImage = Base64.encode(file.getInputStream()); req.setFileContent(encodeImage); //4.返回的resp是一个ImageModerationResponse的实例,与请求对象对应 ImageModerationResponse resp = client.ImageModeration(req); //5.解析结果输出json格式的字符串回包 if (resp != null) { String suggestion = resp.getSuggestion(); return suggestion.toLowerCase(); } } catch (Exception e) { log.error("[点播平台内容安全图片检测异常:{}", e); } return null; } ``` 3. 在FileUploadServiceImpl中对上传图片进行审核 ```java /** * 图片(封面、头像)文件上传 * 前端提交文件参数名:file * * @param multipartFile * @return */ @Override public String fileUpload(MultipartFile multipartFile) { //1.业务校验验证图片内容格式是否合法 BufferedImage bufferedImage = null; try { bufferedImage = ImageIO.read(multipartFile.getInputStream()); } catch (IOException e) { throw new RuntimeException(e); } if (bufferedImage == null) { throw new GuiguException(400, "文件格式有误"); } //2.业务校验-验证图片大小合法 int width = bufferedImage.getWidth(); int height = bufferedImage.getHeight(); if (width > 900 || height > 900) { throw new GuiguException(400, "文件大小有误!"); } //TODO 校验图片内容是否合法-判断图片是否存在违规 String suggest = vodService.scanImage(multipartFile); if (!"pass".equals(suggest)) { throw new GuiguException(500, "内容审核失败!"); } try { //3.将文件上传到MInIO //3.1 生成带有文件夹目录文件名称 形式:/日期/随机文件名称.后缀 String folder = "/" + DateUtil.today(); String fileName = IdUtil.randomUUID(); String extName = FileNameUtil.extName(multipartFile.getOriginalFilename()); String objName = folder + "/" + fileName + "." + extName; //3.2 调用minio客户对象上传文件方法 String bucketName = minioConstantProperties.getBucketName(); minioClient.putObject( PutObjectArgs.builder().bucket(bucketName).object(objName).stream( multipartFile.getInputStream(), multipartFile.getSize(), -1) .contentType(multipartFile.getContentType()) .build()); //3.3 拼接上传文件在线路径地址 return minioConstantProperties.getEndpointUrl() + "/" + bucketName + objName; } catch (Exception e) { throw new GuiguException(500, "文件上传失败"); } } ``` ## 5.3 音频审核 ### 5.3.1 发起审核任务 当声音保存后,需要对保存的声音进行内容审核,查询腾讯云点播平台文档得知服务端只能采用异步审核任务实现,后续发起的任务审核结果采用定时任务获取。 tips:因为发起审核后需要再声音表中记录声音对应的审核任务ID,故需要在track_info表中增加字段`review_task_id`在对应的实体类**TrackInfo**增加对应属性: ```java @Schema(description = "发起审核任务ID") @TableField("review_task_id") private String reviewTaskId; ``` #### 5.3.1.1 TrackInfoServiceImpl 修改保存/修改声音方法: ```java /** * 保存声音 * * @param userId 用户ID * @param trackInfo 专辑信息 */ @Override @Transactional(rollbackFor = Exception.class) public void saveTrackInfo(Long userId, TrackInfo trackInfo) { //1.根据专辑ID查询专辑信息-更新专辑信息;复用专辑封面图片 AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId()); //2.新增声音 //2.1 设置声音额外基本信息:用户ID,声音排序序号,状态,来源 trackInfo.setUserId(userId); trackInfo.setOrderNum(albumInfo.getIncludeTrackCount() + 1); trackInfo.setSource(SystemConstant.TRACK_SOURCE_USER); trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS); if (StringUtils.isBlank(trackInfo.getCoverUrl())) { trackInfo.setCoverUrl(albumInfo.getCoverUrl()); } //2.2 远程调用腾讯点播平台获取音频详情信息 TrackMediaInfoVo mediaInfoVo = vodService.getMediaInfo(trackInfo.getMediaFileId()); if (mediaInfoVo != null) { trackInfo.setMediaDuration(BigDecimal.valueOf(mediaInfoVo.getDuration())); trackInfo.setMediaSize(mediaInfoVo.getSize()); trackInfo.setMediaType(mediaInfoVo.getType()); } //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 调用点播平台发起音频文件审核任务(异步审核) String reviewTaskId = vodService.reviewTask(trackInfo.getMediaFileId()); trackInfo.setReviewTaskId(reviewTaskId); trackInfo.setStatus(SystemConstant.TRACK_STATUS_REVIEWING); trackInfoMapper.updateById(trackInfo); } ``` #### 5.3.1.2 VodService ```java /** * 对音视频文件发起审核任务 * @param mediaFileId 文件唯一标识 * @return 发起审核任务ID,用于查看审核结果 */ String reviewTask(String mediaFileId); ``` #### 5.3.1.3 VodServiceImpl ```java /** * 对音视频文件发起审核任务 * * @param mediaFileId 文件唯一标识 * @return 发起审核任务ID,用于查看审核结果 */ @Override public String reviewTask(String mediaFileId) { try { //1.实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 Credential cred = new Credential(vodConstantProperties.getSecretId(), vodConstantProperties.getSecretKey()); //2.实例化要请求产品的client对象,clientProfile是可选的 VodClient client = new VodClient(cred, vodConstantProperties.getRegion()); //3.实例化一个请求对象,每个接口都会对应一个request对象 ReviewAudioVideoRequest req = new ReviewAudioVideoRequest(); req.setFileId(mediaFileId); //4.发起审核任务,返回的resp是一个ReviewAudioVideoResponse的实例,与请求对象对应 ReviewAudioVideoResponse resp = client.ReviewAudioVideo(req); //5.获取审核任务ID if (resp != null) { String taskId = resp.getTaskId(); return taskId; } } catch (TencentCloudSDKException e) { log.error("[点播平台]发起审核任务失败:{}", e); } return null; } ``` ### 5.3.2 定时任务获取审核结果 1. 启动类上开启定时任务,加注解**@EnableScheduling** ```java @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. 新增定时任务类 ```java package com.atguigu.tingshu.album.job; import cn.hutool.core.collection.CollectionUtil; import com.atguigu.tingshu.album.mapper.TrackInfoMapper; import com.atguigu.tingshu.album.service.VodService; import com.atguigu.tingshu.common.constant.SystemConstant; 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; /** * @author: atguigu * @create: 2024-08-05 15:42 */ @Slf4j @Component public class ReviewTaskResultJob { @Autowired private TrackInfoMapper trackInfoMapper; @Autowired private VodService vodService; /** * 每隔5s查询审核中任务ID获取审核结果,根据审核结果更新审核状态 * corn表达式:秒 分 时 日 月 周 [年] */ @Scheduled(cron = "0/5 * * * * ?") public void getAudioReviewTaskResult() { log.info("[定时任务]查询审核中任务ID获取审核结果,根据审核结果更新审核状态"); //1.根据审核状态(审核中)声音列表 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(TrackInfo::getStatus, SystemConstant.TRACK_STATUS_REVIEWING); queryWrapper.select(TrackInfo::getId, TrackInfo::getReviewTaskId); queryWrapper.last("limit 100"); List trackInfoList = trackInfoMapper.selectList(queryWrapper); //2.调用腾讯点播平台获取审核任务ID对应审核结果 if (CollectionUtil.isNotEmpty(trackInfoList)) { for (TrackInfo trackInfo : trackInfoList) { //2.1 根据审核任务ID查询审核结果 String suggest = vodService.getReviewTaskResult(trackInfo.getReviewTaskId()); if (StringUtils.isNotBlank(suggest)) { if ("pass".equals(suggest)) { trackInfo.setStatus(SystemConstant.TRACK_STATUS_PASS); } else if ("block".equals(suggest)) { trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS); } else if ("review".equals(suggest)) { trackInfo.setStatus(SystemConstant.TRACK_STATUS_ARTIFICIAL); } //3.根据审核结果更新审核状态 trackInfoMapper.updateById(trackInfo); } } } } } ``` 3. vodService提供查询审核任务结果方法 ```java //查询审核任务结果:https://cloud.tencent.com/document/product/266/33431 /** * 根据审核任务ID查询审核结果 * @param reviewTaskId * @return */ String getReviewTaskResult(String reviewTaskId); ``` 4. VodServiceImpl 实现 ```java /** * 根据审核任务ID查询审核结果 * * @param reviewTaskId 审核任务ID * @return 审核结果 */ @Override public String getReviewTaskResult(String reviewTaskId) { try { //1.实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 Credential cred = new Credential(vodConstantProperties.getSecretId(), vodConstantProperties.getSecretKey()); //2.实例化要请求产品的client对象,clientProfile是可选的 VodClient client = new VodClient(cred, vodConstantProperties.getRegion()); //3.实例化一个请求对象,每个接口都会对应一个request对象 DescribeTaskDetailRequest req = new DescribeTaskDetailRequest(); req.setTaskId(reviewTaskId); //4.查询审核结果 返回的resp是一个DescribeTaskDetailResponse的实例,与请求对象对应 DescribeTaskDetailResponse resp = client.DescribeTaskDetail(req); //5.解析审核结果 if ("ReviewAudioVideo".equals(resp.getTaskType())) { //5.1 判断音视频审核任务是否完成 if ("FINISH".equals(resp.getStatus())) { //5.2 获取音视频审核结果 ReviewAudioVideoTask reviewAudioVideoTask = resp.getReviewAudioVideoTask(); if (reviewAudioVideoTask != null) { //5.3 获取审核任务结果 ReviewAudioVideoTaskOutput output = reviewAudioVideoTask.getOutput(); if (output != null) { //5.4 返回建议结果 String suggestion = output.getSuggestion(); return suggestion; } } } } } catch (Exception e) { log.error("[点播平台]获取审核结果异常:{}", e); } return null; } ```