diff --git a/backend/docker-compose-node-01.yml b/backend/docker-compose-node-01.yml index 5269f0c..3784c1e 100644 --- a/backend/docker-compose-node-01.yml +++ b/backend/docker-compose-node-01.yml @@ -372,8 +372,8 @@ services: - MYSQL_PASSWORD=${MYSQL_ROOT_PASSWORD:-root123456} - REDIS_PASSWORD=${REDIS_PASSWORD:-redis_HA_Cluster_2024} - REDIS_SENTINEL_NODES=${REDIS_SENTINEL_NODES:-192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379} - - MINIO_HOST=minio - - MINIO_PORT=9000 + - MINIO_HOST=192.168.1.101 + - MINIO_PORT=9100 - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - MINIO_BUCKET=${MINIO_BUCKET} diff --git a/backend/docker-compose-node-02.yml b/backend/docker-compose-node-02.yml index f7472dc..4d3bf6f 100644 --- a/backend/docker-compose-node-02.yml +++ b/backend/docker-compose-node-02.yml @@ -381,8 +381,8 @@ services: - REDIS_SENTINEL_NODES=${REDIS_SENTINEL_NODES:-192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379} - ROCKETMQ_HOST=rmqnamesrv - ROCKETMQ_PORT=9876 - - MINIO_HOST=minio - - MINIO_PORT=9000 + - MINIO_HOST=192.168.1.101 + - MINIO_PORT=9100 - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin123 - MINIO_BUCKET=video-platform diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java index b7dfc70..7ba84d2 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java @@ -119,4 +119,60 @@ public class ContentManageController { @PathVariable Long categoryId) { return contentFeignClient.deleteCategory(categoryId, internalServiceToken); } + + /** + * 获取标签分页 + */ + @PreAuthorize("hasAuthority('tag:manage')") + @GetMapping("/tag/page") + public Result> getTagPage( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "size", defaultValue = "10") Integer size, + @RequestParam(value = "keyword", required = false) String keyword) { + return contentFeignClient.getTagPage(page, size, keyword); + } + + /** + * 添加标签 + */ + @OperationLog("添加标签") + @PreAuthorize("hasAuthority('tag:manage')") + @PostMapping("/tag") + public Result addTag(@RequestBody Object tag, jakarta.servlet.http.HttpServletRequest request) { + Object adminId = request.getAttribute("adminId"); + Object username = request.getAttribute("username"); + return contentFeignClient.addTag( + tag, + internalServiceToken, + String.valueOf(adminId instanceof Long ? adminId : 0L), + String.valueOf(username == null ? "admin" : username) + ); + } + + /** + * 更新标签 + */ + @OperationLog("更新标签") + @PreAuthorize("hasAuthority('tag:manage')") + @PutMapping("/tag") + public Result updateTag(@RequestBody Object tag, jakarta.servlet.http.HttpServletRequest request) { + Object adminId = request.getAttribute("adminId"); + Object username = request.getAttribute("username"); + return contentFeignClient.updateTag( + tag, + internalServiceToken, + String.valueOf(adminId instanceof Long ? adminId : 0L), + String.valueOf(username == null ? "admin" : username) + ); + } + + /** + * 删除标签 + */ + @OperationLog("删除标签") + @PreAuthorize("hasAuthority('tag:manage')") + @DeleteMapping("/tag/{tagId}") + public Result deleteTag(@PathVariable Long tagId) { + return contentFeignClient.deleteTag(tagId, internalServiceToken); + } } diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java index 88e4716..32eea5a 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java @@ -73,6 +73,28 @@ public interface ContentFeignClient { Result deleteCategory(@PathVariable("categoryId") Long categoryId, @RequestHeader("X-Internal-Token") String internalToken); + @GetMapping("/content/tag/page") + Result> getTagPage( + @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, + @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(value = "keyword", required = false) String keyword); + + @PostMapping("/content/tag") + Result addTag(@RequestBody Object tag, + @RequestHeader("X-Internal-Token") String internalToken, + @RequestHeader("userId") String userId, + @RequestHeader("username") String username); + + @PutMapping("/content/tag") + Result updateTag(@RequestBody Object tag, + @RequestHeader("X-Internal-Token") String internalToken, + @RequestHeader("userId") String userId, + @RequestHeader("username") String username); + + @DeleteMapping("/content/tag/{tagId}") + Result deleteTag(@PathVariable("tagId") Long tagId, + @RequestHeader("X-Internal-Token") String internalToken); + @GetMapping("/content/video/admin/stats") Result getVideoStats(@RequestHeader("X-Internal-Token") String internalToken); } diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java index f1d3401..a63994f 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java @@ -58,6 +58,26 @@ public class ContentFeignClientFallbackFactory implements FallbackFactory> getTagPage(Integer pageNum, Integer pageSize, String keyword) { + return Result.error("获取标签列表失败:服务不可用"); + } + + @Override + public Result addTag(Object tag, String internalToken, String userId, String username) { + return Result.error("添加标签失败:服务不可用"); + } + + @Override + public Result updateTag(Object tag, String internalToken, String userId, String username) { + return Result.error("更新标签失败:服务不可用"); + } + + @Override + public Result deleteTag(Long tagId, String internalToken) { + return Result.error("删除标签失败:服务不可用"); + } + @Override public Result getVideoStats(String internalToken) { return Result.error("获取视频统计失败:服务不可用"); diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/mapper/CommentMapper.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/mapper/CommentMapper.java index a60ec49..c7a8aca 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/mapper/CommentMapper.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/mapper/CommentMapper.java @@ -40,6 +40,18 @@ public interface CommentMapper extends BaseMapper { */ int increaseReplyCount(@Param("commentId") Long commentId, @Param("count") int count); + /** + * 更新评论状态 + * + * @param commentId 评论ID + * @param status 状态 + * @param updatedBy 更新人 + * @return 影响行数 + */ + int updateCommentStatus(@Param("commentId") Long commentId, + @Param("status") Integer status, + @Param("updatedBy") String updatedBy); + /** * 统计视频评论数 * @@ -47,4 +59,4 @@ public interface CommentMapper extends BaseMapper { * @return 评论数 */ int countVideoComments(@Param("videoId") Long videoId); -} \ No newline at end of file +} diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java index f4c2a66..a40acbf 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java @@ -177,10 +177,7 @@ public class CommentServiceImpl extends ServiceImpl impl } // 逻辑删除评论(设置状态为0) - comment.setStatus(0); - comment.setUpdatedBy(userId.toString()); - comment.setUpdateTime(LocalDateTime.now()); - updateById(comment); + commentMapper.updateCommentStatus(id, 0, userId.toString()); updateVideoCommentCountAsync(comment.getVideoId(), -1); @@ -449,10 +446,7 @@ public class CommentServiceImpl extends ServiceImpl impl } // 逻辑删除评论(设置状态为0) - comment.setStatus(0); - comment.setUpdatedBy("admin"); - comment.setUpdateTime(LocalDateTime.now()); - updateById(comment); + commentMapper.updateCommentStatus(commentId, 0, "admin"); updateVideoCommentCountAsync(comment.getVideoId(), -1); diff --git a/backend/video-comment/src/main/resources/mapper/CommentMapper.xml b/backend/video-comment/src/main/resources/mapper/CommentMapper.xml index 82f4d66..d694fcc 100644 --- a/backend/video-comment/src/main/resources/mapper/CommentMapper.xml +++ b/backend/video-comment/src/main/resources/mapper/CommentMapper.xml @@ -42,6 +42,14 @@ WHERE id = #{commentId} + + UPDATE comment + SET status = #{status}, + updated_by = #{updatedBy}, + update_time = NOW() + WHERE id = #{commentId} + + - \ No newline at end of file + diff --git a/backend/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java b/backend/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java index c4eb866..9f0414b 100644 --- a/backend/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java +++ b/backend/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java @@ -50,12 +50,66 @@ public class MinioUtils { if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } + ensureBucketPublicRead(bucketName); } catch (Exception e) { log.error("创建存储桶失败:", e); throw new RuntimeException("创建存储桶失败", e); } } + /** + * 确保存储桶支持匿名读取,便于前端直接访问视频和封面资源 + */ + public void ensureBucketPublicRead(String bucketName) { + try { + String policy = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetBucketLocation", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::%s" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::%s/*" + ] + } + ] + } + """.formatted(bucketName, bucketName); + minioClient.setBucketPolicy( + SetBucketPolicyArgs.builder() + .bucket(bucketName) + .config(policy) + .build() + ); + } catch (Exception e) { + log.error("设置存储桶公开读取策略失败:bucket={}", bucketName, e); + throw new RuntimeException("设置存储桶公开读取策略失败", e); + } + } + /** * 列出所有存储桶 */ @@ -244,4 +298,4 @@ public class MinioUtils { return objectName; } -} \ No newline at end of file +} diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/config/SecurityConfig.java b/backend/video-content/src/main/java/com/nuaa/video/content/config/SecurityConfig.java index 8032ab1..d43e98c 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/config/SecurityConfig.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/config/SecurityConfig.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -53,6 +54,18 @@ public class SecurityConfig { "/content/category/admin/**" ).access((authentication, context) -> new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token")))) + .requestMatchers( + HttpMethod.POST, "/content/tag" + ).access((authentication, context) -> + new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token")))) + .requestMatchers( + HttpMethod.PUT, "/content/tag" + ).access((authentication, context) -> + new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token")))) + .requestMatchers( + HttpMethod.DELETE, "/content/tag/*" + ).access((authentication, context) -> + new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token")))) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/controller/TagController.java b/backend/video-content/src/main/java/com/nuaa/video/content/controller/TagController.java index e151b59..7ddba04 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/controller/TagController.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/controller/TagController.java @@ -88,6 +88,13 @@ public class TagController { if (userIdObj instanceof Long) { return (Long) userIdObj; } + String userIdHeader = request.getHeader("userId"); + if (userIdHeader != null && !userIdHeader.isBlank()) { + try { + return Long.parseLong(userIdHeader); + } catch (NumberFormatException ignored) { + } + } return null; } -} \ No newline at end of file +} diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java b/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java index ad6d1a6..a0f8e28 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java @@ -62,7 +62,14 @@ public class VideoController { @GetMapping("/public/{id}") public Result getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) { Long userId = getUserIdFromRequest(request); - return videoService.getVideoInfo(id, userId); + Result result = videoService.getVideoInfo(id, userId); + if (result.getData() == null) { + return result; + } + if (result.getData().getPublishTime() == null) { + return Result.error("视频暂未公开"); + } + return result; } /** diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoListVO.java b/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoListVO.java index b471ffb..769cff3 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoListVO.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoListVO.java @@ -53,6 +53,11 @@ public class VideoListVO implements Serializable { */ private Integer duration; + /** + * 视频状态 + */ + private Integer status; + /** * 分类名称 */ @@ -82,4 +87,4 @@ public class VideoListVO implements Serializable { * 发布时间 */ private LocalDateTime publishTime; -} \ No newline at end of file +}