谷粒随享
学习目标:
功能入口:运行小程序-->我的-->创作中心-->声音-->点击 + 添加声音
业务需求:当创作者新增专辑后,创作者将自己录制的音视频文件保存到声音。
需求:点击添加声音的时候会触发一个查询所有专辑列表,主要目的是为了让专辑与声音进行挂钩!
主要是根据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);
}
腾讯云点播服务面向音视频、图片等媒体,提供制作上传、存储、转码、媒体处理、媒体 AI、加速分发播放、版权保护等一体化高品质媒体服务。使用腾讯云服务可以为音视频文件的存储、传输、处理和分发提供可靠、高效和安全的解决方案。这可以节省你自己构建和维护存储基础设施的成本和精力,并为用户提供更好的音视频体验。
快速接入流程:云点播 快速入门-文档中心-腾讯云 (tencent.com)
微信扫码登录
关注公众号
搜索云点播
微信认证
实名认证
立即开通服务(点播平台、内容审核服务)
右边:点击访问管理
准备工作:
在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
通过配置类读取(已提供)
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控制器:
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 {mediaFileId:"文件唯一标识",mediaUrl:"在线播放地址"}
*/
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 {mediaFileId:"文件唯一标识",mediaUrl:"在线播放地址"}
*/
@Override
public Map<String, String> 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<String, String> 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)-应用管理-点击主应用-媒资管理-音视频管理看是否有音频
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/31
前端传递Json 字符串,此时我们可以使用封装好的TrackInfoVo实体类进行接收,方便处理数据
涉及到的表:
track_info 声音信息表
track_stat 声音统计表
album_info 专辑表
TrackInfoApiController控制器
/**
* 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接口
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 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实现类
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 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 调用点播平台发起音频文件审核任务(异步审核)
}
}
初始化统计数据
@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)
VodService
/**
* 获取点播平台音频详情信息
* @param mediaFileId 文件唯一标识
* @return
*/
TrackMediaInfoVo getMediaInfo(String mediaFileId);
VodServiceImpl
/**
* 获取点播平台音频详情信息
*
* @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;
}
功能入口:运行小程序-->我的-->创作中心-->声音-->查询当前用户的声音列表,如下图所示:
查询分析:需要根据用户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="getUserTrackPage" resultType="com.atguigu.tingshu.vo.album.TrackListVo">
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>
<if test="vo.userId != null">
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>
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/35
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);
}
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);
}
}
涉及的表: 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());
}
在service-album
模块pom.xml添加如下依赖
<!-- <dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-vod</artifactId>
<version>3.1.1002</version>
</dependency>-->
<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>
文本内容安全接口:
图片内容安全接口:
音频内容安全接口:
在vodService中
新增方法用于审核文本
/**
* 文本审核
* @param content
* @return
*/
String scanText(String content);
VodServiceImpl
实现方法
/**
* 文本审核
*
* @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;
}
在保存专辑/声音,更新专辑/声音对文本内容进行内容审核
在VodService中新增图片审核方法
/**
* 图片审核
* @param file 图片文件
* @return
*/
String scanImage(MultipartFile file);
在VodServiceImpl实现方法
/**
* 图片审核
*
* @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;
}
在FileUploadServiceImpl中对上传图片进行审核
/**
* 图片(封面、头像)文件上传
* 前端提交文件参数名: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, "文件上传失败");
}
}
当声音保存后,需要对保存的声音进行内容审核,查询腾讯云点播平台文档得知服务端只能采用异步审核任务实现,后续发起的任务审核结果采用定时任务获取。
tips:因为发起审核后需要再声音表中记录声音对应的审核任务ID,故需要在track_info表中增加字段review_task_id
在对应的实体类TrackInfo增加对应属性:
@Schema(description = "发起审核任务ID")
@TableField("review_task_id")
private String reviewTaskId;
修改保存/修改声音方法:
/**
* 保存声音
*
* @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);
}
/**
* 对音视频文件发起审核任务
* @param mediaFileId 文件唯一标识
* @return 发起审核任务ID,用于查看审核结果
*/
String reviewTask(String mediaFileId);
/**
* 对音视频文件发起审核任务
*
* @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;
}
启动类上开启定时任务,加注解@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);
}
}
新增定时任务类
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<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getStatus, SystemConstant.TRACK_STATUS_REVIEWING);
queryWrapper.select(TrackInfo::getId, TrackInfo::getReviewTaskId);
queryWrapper.last("limit 100");
List<TrackInfo> 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);
}
}
}
}
}
vodService提供查询审核任务结果方法
//查询审核任务结果:https://cloud.tencent.com/document/product/266/33431
/**
* 根据审核任务ID查询审核结果
* @param reviewTaskId
* @return
*/
String getReviewTaskResult(String reviewTaskId);
VodServiceImpl 实现
/**
* 根据审核任务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;
}