diff --git a/ruoyi-modules/ruoyi-content/pom.xml b/ruoyi-modules/ruoyi-content/pom.xml index 2f9d045f4..dbd6f002e 100644 --- a/ruoyi-modules/ruoyi-content/pom.xml +++ b/ruoyi-modules/ruoyi-content/pom.xml @@ -29,7 +29,11 @@ minio 8.2.1 - + + com.squareup.okhttp3 + okhttp + 3.14.9 + com.tencentcloudapi @@ -158,6 +162,26 @@ spring-rabbit + + + + + + + + + + + + + + + + + + + + diff --git a/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/controller/admin/VlogUploadController.java b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/controller/admin/VlogUploadController.java new file mode 100644 index 000000000..7cba65770 --- /dev/null +++ b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/controller/admin/VlogUploadController.java @@ -0,0 +1,201 @@ +package com.wzj.soopin.content.controller.admin; + +import com.tencentcloudapi.vod.v20180717.models.*; +import com.wzj.soopin.content.result.GraceJSONResult; +import com.wzj.soopin.content.service.VlogUploadService; +import com.wzj.soopin.content.utils.TencentCloudUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; + +@Slf4j +@Api(tags = "管理端-视频上传接口") +@RequestMapping("admin/vlog/upload") +@RestController +public class VlogUploadController { + + @Autowired + private VlogUploadService vlogUploadService; + + @Autowired + private TencentCloudUtil tencentCloudUtil; + + @ApiOperation("获取腾讯云点播视频列表") + @GetMapping("/list") + public GraceJSONResult getVodList( + @ApiParam(value = "页码", defaultValue = "1") @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @ApiParam(value = "每页记录数", defaultValue = "20") @RequestParam(required = false, defaultValue = "20") Integer pageSize, + @ApiParam(value = "文件状态,可选值:Normal(正常)、SystemForbidden(平台封禁)、Forbidden(主动封禁)") @RequestParam(required = false) String[] status, + @ApiParam(value = "文件类型,可选值:Video(视频)、Audio(音频)、Image(图片)") @RequestParam(required = false) String[] categories, + @ApiParam(value = "媒体文件来源,可选值:Upload(上传)、Record(直播录制)等") @RequestParam(required = false) String[] sourceTypes, + @ApiParam(value = "媒体文件封装格式,如:mp4、mov等") @RequestParam(required = false) String[] mediaTypes, + @ApiParam(value = "文件名,支持模糊搜索") @RequestParam(required = false) String[] names, + @ApiParam(value = "文件名前缀") @RequestParam(required = false) String[] namePrefixes, + @ApiParam(value = "文件描述,支持模糊搜索") @RequestParam(required = false) String[] descriptions, + @ApiParam(value = "标签") @RequestParam(required = false) String[] tags, + @ApiParam(value = "分类ID") @RequestParam(required = false) Long[] classIds, + @ApiParam(value = "存储地区,如:ap-guangzhou") @RequestParam(required = false) String[] storageRegions, + @ApiParam(value = "创建时间范围-开始时间,格式:yyyy-MM-dd'T'HH:mm:ss'Z'") @RequestParam(required = false) String createTimeAfter, + @ApiParam(value = "创建时间范围-结束时间,格式:yyyy-MM-dd'T'HH:mm:ss'Z'") @RequestParam(required = false) String createTimeBefore, + @ApiParam(value = "排序字段,目前仅支持:CreateTime") @RequestParam(required = false, defaultValue = "CreateTime") String sortField, + @ApiParam(value = "排序方式,可选值:Desc(降序)、Asc(升序)") @RequestParam(required = false, defaultValue = "Desc") String sortOrder, + @ApiParam(value = "需要返回的信息类型,可选值:basicInfo(基础信息)、metaData(元信息)、transcodeInfo(转码信息)等") @RequestParam(required = false) String[] filters) { + try { + // 验证密钥配置 + String secretId = tencentCloudUtil.getSecretId(); + String secretKey = tencentCloudUtil.getSecretKey(); + if (secretId == null || secretId.isEmpty() || secretKey == null || secretKey.isEmpty()) { + log.error("腾讯云密钥未配置"); + return GraceJSONResult.errorMsg("腾讯云密钥未配置"); + } + + // 创建请求对象 + SearchMediaRequest req = new SearchMediaRequest(); + + // 设置分页参数 + req.setOffset((long) ((pageNum - 1) * pageSize)); + req.setLimit((long) pageSize); + + // 设置排序 + SortBy sort = new SortBy(); + sort.setField(sortField); + sort.setOrder(sortOrder); + req.setSort(sort); + + // 设置过滤条件 + if (categories != null && categories.length > 0) { + req.setCategories(categories); + } + if (sourceTypes != null && sourceTypes.length > 0) { + req.setSourceTypes(sourceTypes); + } +// if (mediaTypes != null && mediaTypes.length > 0) { +// req.setMediaTypes(mediaTypes); +// } + if (names != null && names.length > 0) { + req.setNames(names); + } + if (namePrefixes != null && namePrefixes.length > 0) { + req.setNamePrefixes(namePrefixes); + } + if (descriptions != null && descriptions.length > 0) { + req.setDescriptions(descriptions); + } + if (tags != null && tags.length > 0) { + req.setTags(tags); + } + if (classIds != null && classIds.length > 0) { + req.setClassIds(classIds); + } + if (storageRegions != null && storageRegions.length > 0) { + req.setStorageRegions(storageRegions); + } +// if (createTimeAfter != null && !createTimeAfter.isEmpty()) { +// req.setCreateTimeAfter(createTimeAfter); +// } +// if (createTimeBefore != null && !createTimeBefore.isEmpty()) { +// req.setCreateTimeBefore(createTimeBefore); +// } + if (filters != null && filters.length > 0) { + req.setFilters(filters); + } + + // 调用腾讯云API获取视频列表 + log.info("开始调用腾讯云 SearchMedia API"); + SearchMediaResponse resp = vlogUploadService.searchMedia(req); + log.info("腾讯云 SearchMedia API 调用成功"); + + // 处理响应结果 + Map result = new HashMap<>(); + result.put("totalCount", resp.getTotalCount()); + result.put("pageNum", pageNum); + result.put("pageSize", pageSize); + + List> mediaList = new ArrayList<>(); + if (resp.getMediaInfoSet() != null) { + for (MediaInfo mediaInfo : resp.getMediaInfoSet()) { + Map mediaMap = new HashMap<>(); + // 基础信息 + MediaBasicInfo basicInfo = mediaInfo.getBasicInfo(); + mediaMap.put("fileId", mediaInfo.getFileId()); + mediaMap.put("name", basicInfo.getName()); + mediaMap.put("description", basicInfo.getDescription()); + mediaMap.put("createTime", basicInfo.getCreateTime()); + mediaMap.put("updateTime", basicInfo.getUpdateTime()); + mediaMap.put("expireTime", basicInfo.getExpireTime()); + mediaMap.put("classId", basicInfo.getClassId()); + mediaMap.put("className", basicInfo.getClassName()); + mediaMap.put("classPath", basicInfo.getClassPath()); + mediaMap.put("coverUrl", basicInfo.getCoverUrl()); + mediaMap.put("type", basicInfo.getType()); + mediaMap.put("mediaUrl", basicInfo.getMediaUrl()); + mediaMap.put("status", basicInfo.getStatus()); + mediaMap.put("storageRegion", basicInfo.getStorageRegion()); + mediaMap.put("category", basicInfo.getCategory()); + mediaMap.put("storageClass", basicInfo.getStorageClass()); + mediaMap.put("tagSet", basicInfo.getTagSet()); + mediaList.add(mediaMap); + } + } + result.put("list", mediaList); + + return GraceJSONResult.ok(result); + } catch (Exception e) { + log.error("获取视频列表失败", e); + return GraceJSONResult.errorMsg("获取视频列表失败: " + e.getMessage()); + } + } + + /** + * 视频审核接口 + */ + @PostMapping("/audit") + public GraceJSONResult auditVlog(@RequestParam String vlogId, + @RequestParam Integer status, + @RequestParam(required = false) String reason) { + try { + vlogUploadService.auditVlog(vlogId, status, reason); + return GraceJSONResult.ok(); + } catch (Exception e) { + log.error("视频审核失败", e); + return GraceJSONResult.errorMsg("视频审核失败: " + e.getMessage()); + } + } + + /** + * 视频删除接口 + */ + @PostMapping("/delete") + public GraceJSONResult deleteVlog(@RequestParam String vlogId) { + try { + vlogUploadService.deleteVlog(vlogId); + return GraceJSONResult.ok(); + } catch (Exception e) { + log.error("视频删除失败", e); + return GraceJSONResult.errorMsg("视频删除失败: " + e.getMessage()); + } + } + + /** + * 视频详情接口 + */ + @GetMapping("/detail") + public GraceJSONResult vlogDetail(@RequestParam String vlogId) { + try { + return GraceJSONResult.ok(vlogUploadService.getVlogDetail(vlogId)); + } catch (Exception e) { + log.error("获取视频详情失败", e); + return GraceJSONResult.errorMsg("获取视频详情失败: " + e.getMessage()); + } + } +} diff --git a/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/service/VlogUploadService.java b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/service/VlogUploadService.java new file mode 100644 index 000000000..a7abd3071 --- /dev/null +++ b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/service/VlogUploadService.java @@ -0,0 +1,42 @@ +package com.wzj.soopin.content.service; + +import com.tencentcloudapi.vod.v20180717.models.PullUploadRequest; +import com.tencentcloudapi.vod.v20180717.models.PullUploadResponse; +import com.tencentcloudapi.vod.v20180717.models.SearchMediaRequest; +import com.tencentcloudapi.vod.v20180717.models.SearchMediaResponse; + +import java.util.Map; + +public interface VlogUploadService { + +// /** +// * 拉取视频上传 +// * +// * @param req 拉取上传请求参数 +// * @return 拉取上传响应 +// */ +// PullUploadResponse pullUpload(PullUploadRequest req) throws Exception; +// +// /** +// * 获取视频上传任务状态 +// * +// * @param taskId 任务ID +// * @return 任务状态信息 +// */ +// Map getTaskStatus(String taskId) throws Exception; + + /** + * 搜索媒体文件 + * + * @param req 搜索请求 + * @return 搜索响应 + */ + SearchMediaResponse searchMedia(SearchMediaRequest req); + + // 新增:视频审核 + void auditVlog(String vlogId, Integer status, String reason); + // 新增:视频删除 + void deleteVlog(String vlogId); + // 新增:视频详情 + Object getVlogDetail(String vlogId); +} diff --git a/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/service/impl/VlogUploadServiceImpl.java b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/service/impl/VlogUploadServiceImpl.java new file mode 100644 index 000000000..7e1f5259b --- /dev/null +++ b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/service/impl/VlogUploadServiceImpl.java @@ -0,0 +1,243 @@ +package com.wzj.soopin.content.service.impl; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.vod.v20180717.VodClient; +import com.tencentcloudapi.vod.v20180717.models.DescribeTaskDetailRequest; +import com.tencentcloudapi.vod.v20180717.models.DescribeTaskDetailResponse; +import com.tencentcloudapi.vod.v20180717.models.PullUploadRequest; +import com.tencentcloudapi.vod.v20180717.models.PullUploadResponse; +import com.tencentcloudapi.vod.v20180717.models.SearchMediaRequest; +import com.tencentcloudapi.vod.v20180717.models.SearchMediaResponse; +import com.wzj.soopin.content.domain.vo.CommentVO; +import com.wzj.soopin.content.service.VlogUploadService; +import com.wzj.soopin.content.utils.TencentCloudUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class VlogUploadServiceImpl implements VlogUploadService { + + @Autowired + private TencentCloudUtil tencentCloudUtil; + + @Autowired + private com.wzj.soopin.content.mapper.VlogMapper vlogMapper; + @Autowired + private com.wzj.soopin.content.service.CommentService commentService; + @Autowired + private com.wzj.soopin.content.utils.RedisOperator redis; + @Autowired + private com.wzj.soopin.content.mapper.VlogMapperCustom vlogMapperCustom; + +// @Override +// public PullUploadResponse pullUpload(PullUploadRequest req) throws Exception { +// // 获取VOD客户端实例 +// VodClient client = tencentCloudUtil.getVodClient(); +// // 返回的resp是一个PullUploadResponse的实例,与请求对象对应 +// return client.PullUpload(req); +// } + + @Override + public SearchMediaResponse searchMedia(SearchMediaRequest req) { + try { + // 获取VOD客户端实例 + VodClient client = tencentCloudUtil.getVodClient(); + + // 设置默认值 + if (req.getOffset() == null) { + req.setOffset(0L); + } + if (req.getLimit() == null) { + req.setLimit(10L); + } + + // 添加日志,打印请求参数 +// log.info("SearchMediaRequest 请求参数: {}", req.toJsonString()); + + // 添加调试信息 + log.info("请求时间戳: {}", System.currentTimeMillis()); + log.info("请求参数详情 - Offset: {}, Limit: {}, Sort: {}, Filters: {}", + req.getOffset(), + req.getLimit(), +// req.getSort() != null ? req.getSort().toJsonString() : "null", + req.getFilters() != null ? String.join(",", req.getFilters()) : "null"); + + // 验证请求参数 + validateSearchRequest(req); + + SearchMediaResponse response = client.SearchMedia(req); + log.info("SearchMedia API 调用成功,返回结果数: {}", response.getTotalCount()); + return response; + } catch (TencentCloudSDKException e) { + log.error("搜索媒体文件失败: {}", e.getMessage(), e); + // 添加更详细的错误信息 + if (e.getMessage().contains("signature")) { + log.error("签名验证失败,请检查:\n1. 密钥是否正确\n2. 服务器时间是否准确\n3. 时区设置是否正确"); + } else if (e.getMessage().contains("InvalidParameterValue")) { + log.error("参数值错误,请检查:\n1. 分页参数 Offset 和 Limit 是否有效\n2. 搜索条件是否合法\n3. 排序参数是否正确"); + } else if (e.getMessage().contains("UnauthorizedOperation")) { + log.error("未授权操作,请检查:\n1. 密钥是否有权限\n2. 应用 ID 是否正确\n3. 是否开通了点播服务"); + } + throw new RuntimeException("搜索媒体文件失败: " + e.getMessage()); + } + } + + /** + * 验证搜索请求参数 + */ + private void validateSearchRequest(SearchMediaRequest req) { + // 验证分页参数 + if (req.getOffset() < 0) { + throw new IllegalArgumentException("Offset 不能小于 0"); + } + if (req.getLimit() < 1 || req.getLimit() > 50) { + throw new IllegalArgumentException("Limit 必须在 1-50 之间"); + } + if (req.getOffset() + req.getLimit() > 5000) { + throw new IllegalArgumentException("Offset + Limit 不能超过 5000"); + } + + // 验证搜索条件 + if (req.getFileIds() != null && req.getFileIds().length > 10) { + throw new IllegalArgumentException("FileIds 数组长度不能超过 10"); + } + if (req.getNames() != null && req.getNames().length > 10) { + throw new IllegalArgumentException("Names 数组长度不能超过 10"); + } + if (req.getNamePrefixes() != null && req.getNamePrefixes().length > 10) { + throw new IllegalArgumentException("NamePrefixes 数组长度不能超过 10"); + } + if (req.getDescriptions() != null && req.getDescriptions().length > 10) { + throw new IllegalArgumentException("Descriptions 数组长度不能超过 10"); + } + if (req.getClassIds() != null && req.getClassIds().length > 10) { + throw new IllegalArgumentException("ClassIds 数组长度不能超过 10"); + } + if (req.getTags() != null && req.getTags().length > 16) { + throw new IllegalArgumentException("Tags 数组长度不能超过 16"); + } + if (req.getSourceTypes() != null && req.getSourceTypes().length > 10) { + throw new IllegalArgumentException("SourceTypes 数组长度不能超过 10"); + } + if (req.getStreamIds() != null && req.getStreamIds().length > 10) { + throw new IllegalArgumentException("StreamIds 数组长度不能超过 10"); + } + if (req.getStorageRegions() != null && req.getStorageRegions().length > 20) { + throw new IllegalArgumentException("StorageRegions 数组长度不能超过 20"); + } +// if (req.getMediaTypes() != null && req.getMediaTypes().length > 10) { +// throw new IllegalArgumentException("MediaTypes 数组长度不能超过 10"); +// } +// if (req.getStatus() != null && req.getStatus().length > 10) { +// throw new IllegalArgumentException("Status 数组长度不能超过 10"); +// } +// if (req.getReviewResults() != null && req.getReviewResults().length > 10) { +// throw new IllegalArgumentException("ReviewResults 数组长度不能超过 10"); +// } +// if (req.getTrtcSdkAppIds() != null && req.getTrtcSdkAppIds().length > 10) { +// throw new IllegalArgumentException("TrtcSdkAppIds 数组长度不能超过 10"); +// } +// if (req.getTrtcRoomIds() != null && req.getTrtcRoomIds().length > 10) { +// throw new IllegalArgumentException("TrtcRoomIds 数组长度不能超过 10"); +// } + } + + // 视频审核 + @Override + public void auditVlog(String vlogId, Integer status, String reason) { + // 这里假设 status=1为通过,status=2为不通过 + // 可根据实际业务调整 + // 直接更新数据库状态 + // 你可以用 VlogMapper 或 VlogService + // 这里只做伪代码示例 + // vlogMapper.updateStatus(vlogId, status, reason); + log.info("审核视频 vlogId={}, status={}, reason={}", vlogId, status, reason); + } + + // 视频删除 + @Override + public void deleteVlog(String vlogId) { + // 逻辑删除或物理删除 + // vlogMapper.deleteById(vlogId); + log.info("删除视频 vlogId={}", vlogId); + } + + // 视频详情 + @Override + public Object getVlogDetail(String vlogId) { + try { + // 1. 优先从 MySQL 获取视频基础信息 + Map paramMap = new HashMap<>(); + paramMap.put("vlogId", vlogId); + List vlogList = vlogMapperCustom.getVlogDetailById(paramMap); + com.wzj.soopin.content.domain.vo.IndexVlogVO vlog = vlogList != null && !vlogList.isEmpty() ? vlogList.get(0) : null; + + com.tencentcloudapi.vod.v20180717.models.MediaInfo mediaInfo = null; + if (vlog == null) { + // 若本地无,尝试从云点播获取 + com.tencentcloudapi.vod.v20180717.models.DescribeMediaInfosRequest req = new com.tencentcloudapi.vod.v20180717.models.DescribeMediaInfosRequest(); + req.setFileIds(new String[]{vlogId}); + VodClient client = tencentCloudUtil.getVodClient(); + com.tencentcloudapi.vod.v20180717.models.DescribeMediaInfosResponse resp = client.DescribeMediaInfos(req); + if (resp.getMediaInfoSet() != null && resp.getMediaInfoSet().length > 0) { + mediaInfo = resp.getMediaInfoSet()[0]; + } + } + + // 2. 点赞数量:优先 Redis,无则 MySQL + String likeCountsStr = redis.get(com.wzj.soopin.content.domain.base.BaseInfoProperties.REDIS_VLOG_BE_LIKED_COUNTS + ":" + vlogId); + int likeCounts; + if (likeCountsStr != null) { + likeCounts = Integer.parseInt(likeCountsStr); + } else if (vlog != null && vlog.getLikeCounts() != null) { + likeCounts = vlog.getLikeCounts(); + } else { + likeCounts = 0; + } + + // 3. 评论数量:优先 Redis,无则 MySQL + String commentCountsStr = redis.get(com.wzj.soopin.content.domain.base.BaseInfoProperties.REDIS_VLOG_COMMENT_COUNTS + ":" + vlogId); + int commentCounts; + if (commentCountsStr != null) { + commentCounts = Integer.parseInt(commentCountsStr); + } else if (vlog != null && vlog.getCommentsCounts() != null) { + commentCounts = vlog.getCommentsCounts(); + } else { + commentCounts = 0; + } + + // 4. 评论内容:只查 MySQL + List commentList = new ArrayList<>(); + try { + com.wzj.soopin.content.utils.PagedGridResult pagedGridResult = commentService.queryVlogComments(vlogId, null, 1, 10); + if (pagedGridResult != null && pagedGridResult.getRows() != null) { + @SuppressWarnings("unchecked") + List tempList = (List) pagedGridResult.getRows(); + if (tempList != null) { + commentList = tempList; + } + } + } catch (Exception e) { + log.warn("获取评论列表失败: {}", e.getMessage()); + } + + // 5. 统一返回 + java.util.Map detail = new java.util.HashMap<>(); + detail.put("videoInfo", vlog != null ? vlog : mediaInfo); + detail.put("likeCounts", likeCounts); + detail.put("commentCounts", commentCounts); + detail.put("commentList", commentList); + return detail; + } catch (Exception e) { + log.error("获取视频详情失败", e); + return null; + } + } +} diff --git a/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudProperties.java b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudProperties.java index 429c7ad5a..cb82bc914 100644 --- a/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudProperties.java +++ b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudProperties.java @@ -2,7 +2,6 @@ package com.wzj.soopin.content.utils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; /** @@ -13,9 +12,34 @@ import org.springframework.stereotype.Component; */ @Component @Data +@ConfigurationProperties(prefix = "tencent.cloud") public class TencentCloudProperties { + /** + * 密钥ID + */ private String secretId; + + /** + * 密钥Key + */ private String secretKey; + /** + * 区域 + */ + private String region; + + /** + * 点播配置 + */ + private Vod vod = new Vod(); + + @Data + public static class Vod { + /** + * 应用ID + */ + private String appId; + } } diff --git a/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudUtil.java b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudUtil.java new file mode 100644 index 000000000..ebbddbb84 --- /dev/null +++ b/ruoyi-modules/ruoyi-content/src/main/java/com/wzj/soopin/content/utils/TencentCloudUtil.java @@ -0,0 +1,176 @@ +package com.wzj.soopin.content.utils; + +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.vod.v20180717.VodClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * 腾讯云工具类 + */ +@Slf4j +@Component +public class TencentCloudUtil { + + @Autowired + private TencentCloudProperties properties; + /** + * 获取VOD客户端实例 + * + * @return VodClient实例 + */ + public VodClient getVodClient() { + // 从配置中获取值 + String secretId = properties.getSecretId(); + String secretKey = properties.getSecretKey(); + String region = properties.getRegion(); + String appId = properties.getVod().getAppId(); + + // 详细的配置检查日志 + log.info("开始初始化腾讯云 VOD 客户端"); + log.info("配置检查 - SecretId长度: {}, SecretKey长度: {}, Region: {}, AppId: {}", + secretId != null ? secretId.length() : 0, + secretKey != null ? secretKey.length() : 0, + region, + appId); + + // 检查密钥格式 + if (secretId == null || secretId.isEmpty()) { + throw new IllegalArgumentException("SecretId 不能为空"); + } + if (secretKey == null || secretKey.isEmpty()) { + throw new IllegalArgumentException("SecretKey 不能为空"); + } + if (region == null || region.isEmpty()) { + throw new IllegalArgumentException("Region 不能为空"); + } + + // 检查密钥格式 + if (secretId.contains(" ")) { + log.warn("SecretId 包含空格,这可能导致签名验证失败"); + secretId = secretId.trim(); + } + if (secretKey.contains(" ")) { + log.warn("SecretKey 包含空格,这可能导致签名验证失败"); + secretKey = secretKey.trim(); + } + + // 输出服务器时间和时区信息 + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Shanghai")); + LocalDateTime utcNow = LocalDateTime.now(ZoneId.of("UTC")); + log.info("服务器时间 - 本地: {}, UTC: {}, 时区: {}", + now.format(DateTimeFormatter.ISO_DATE_TIME), + utcNow.format(DateTimeFormatter.ISO_DATE_TIME), + ZoneId.systemDefault().getId()); + + // 检查密钥编码 + try { + byte[] secretIdBytes = secretId.getBytes(StandardCharsets.UTF_8); + byte[] secretKeyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + + // 检查密钥格式 + if (secretIdBytes.length != 36) { + log.warn("SecretId 长度异常: {}, 预期长度: 36", secretIdBytes.length); + } + if (secretKeyBytes.length != 64) { + log.warn("SecretKey 长度异常: {}, 预期长度: 64", secretKeyBytes.length); + } + + // 检查密钥字符 + if (!secretId.matches("^[A-Za-z0-9]+$")) { + log.warn("SecretId 包含非字母数字字符"); + } + if (!secretKey.matches("^[A-Za-z0-9]+$")) { + log.warn("SecretKey 包含非字母数字字符"); + } + + log.info("密钥编码检查 - SecretId UTF-8长度: {}, Base64: {}", + secretIdBytes.length, + Base64.getEncoder().encodeToString(secretIdBytes)); + log.info("密钥编码检查 - SecretKey UTF-8长度: {}, Base64: {}", + secretKeyBytes.length, + Base64.getEncoder().encodeToString(secretKeyBytes)); + } catch (Exception e) { + log.error("密钥编码检查失败", e); + throw new IllegalArgumentException("密钥编码检查失败: " + e.getMessage()); + } + + try { + // **添加日志,确认Credential和VodClient实例化前使用的实际密钥和AppId** + log.info("实例化 Credential 和 VodClient - SecretId: {}, SecretKey长度: {}, Region: {}, AppId: {}", + secretId != null ? secretId.substring(0, 4) + "****" : "null", // 隐藏部分密钥 + secretKey != null ? secretKey.length() : 0, + region, + appId); + + // 实例化一个认证对象 + Credential cred = new Credential(secretId, secretKey); + + // 实例化要请求产品的client对象 + VodClient client = new VodClient(cred, region); + log.info("腾讯云 VOD 客户端初始化成功 (地域参数: {})", region); + return client; + } catch (Exception e) { + log.error("初始化腾讯云 VOD 客户端失败", e); + throw new RuntimeException("初始化腾讯云 VOD 客户端失败: " + e.getMessage(), e); + } + } + + /** + * 获取密钥ID + * + * @return 密钥ID + */ + public String getSecretId() { + return properties.getSecretId(); + } + + /** + * 获取密钥Key + * + * @return 密钥Key + */ + public String getSecretKey() { + return properties.getSecretKey(); + } + + /** + * 获取应用ID + * + * @return 应用ID + */ + public String getAppId() { + return properties.getVod().getAppId(); + } + + /** + * 获取视频播放地址 + * + * @param fileId 视频文件ID + * @return 播放地址 + */ + public String getVideoPlayUrl(String fileId) { + // TODO: 实现获取视频播放地址的逻辑 + return null; + } + + /** + * 获取视频封面地址 + * + * @param fileId 视频文件ID + * @return 封面地址 + */ + public String getVideoCoverUrl(String fileId) { + // TODO: 实现获取视频封面地址的逻辑 + return null; + } +}