feat(backend): Improve tag management and media resource access

This commit is contained in:
2026-04-22 16:43:40 +08:00
parent 9820d2f020
commit a6ae978ed0
13 changed files with 216 additions and 18 deletions
+2 -2
View File
@@ -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}
+2 -2
View File
@@ -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
@@ -119,4 +119,60 @@ public class ContentManageController {
@PathVariable Long categoryId) {
return contentFeignClient.deleteCategory(categoryId, internalServiceToken);
}
/**
* 获取标签分页
*/
@PreAuthorize("hasAuthority('tag:manage')")
@GetMapping("/tag/page")
public Result<Page<Object>> 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<Void> 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<Void> 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<Void> deleteTag(@PathVariable Long tagId) {
return contentFeignClient.deleteTag(tagId, internalServiceToken);
}
}
@@ -73,6 +73,28 @@ public interface ContentFeignClient {
Result<Void> deleteCategory(@PathVariable("categoryId") Long categoryId,
@RequestHeader("X-Internal-Token") String internalToken);
@GetMapping("/content/tag/page")
Result<Page<Object>> 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<Void> addTag(@RequestBody Object tag,
@RequestHeader("X-Internal-Token") String internalToken,
@RequestHeader("userId") String userId,
@RequestHeader("username") String username);
@PutMapping("/content/tag")
Result<Void> updateTag(@RequestBody Object tag,
@RequestHeader("X-Internal-Token") String internalToken,
@RequestHeader("userId") String userId,
@RequestHeader("username") String username);
@DeleteMapping("/content/tag/{tagId}")
Result<Void> deleteTag(@PathVariable("tagId") Long tagId,
@RequestHeader("X-Internal-Token") String internalToken);
@GetMapping("/content/video/admin/stats")
Result<Object> getVideoStats(@RequestHeader("X-Internal-Token") String internalToken);
}
@@ -58,6 +58,26 @@ public class ContentFeignClientFallbackFactory implements FallbackFactory<Conten
return Result.error("删除分类失败:服务不可用");
}
@Override
public Result<Page<Object>> getTagPage(Integer pageNum, Integer pageSize, String keyword) {
return Result.error("获取标签列表失败:服务不可用");
}
@Override
public Result<Void> addTag(Object tag, String internalToken, String userId, String username) {
return Result.error("添加标签失败:服务不可用");
}
@Override
public Result<Void> updateTag(Object tag, String internalToken, String userId, String username) {
return Result.error("更新标签失败:服务不可用");
}
@Override
public Result<Void> deleteTag(Long tagId, String internalToken) {
return Result.error("删除标签失败:服务不可用");
}
@Override
public Result<Object> getVideoStats(String internalToken) {
return Result.error("获取视频统计失败:服务不可用");
@@ -40,6 +40,18 @@ public interface CommentMapper extends BaseMapper<Comment> {
*/
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<Comment> {
* @return 评论数
*/
int countVideoComments(@Param("videoId") Long videoId);
}
}
@@ -177,10 +177,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> 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<CommentMapper, Comment> impl
}
// 逻辑删除评论(设置状态为0)
comment.setStatus(0);
comment.setUpdatedBy("admin");
comment.setUpdateTime(LocalDateTime.now());
updateById(comment);
commentMapper.updateCommentStatus(commentId, 0, "admin");
updateVideoCommentCountAsync(comment.getVideoId(), -1);
@@ -42,6 +42,14 @@
WHERE id = #{commentId}
</update>
<update id="updateCommentStatus">
UPDATE comment
SET status = #{status},
updated_by = #{updatedBy},
update_time = NOW()
WHERE id = #{commentId}
</update>
<!-- 统计视频评论数 -->
<select id="countVideoComments" resultType="int">
SELECT COUNT(1)
@@ -49,4 +57,4 @@
WHERE video_id = #{videoId}
AND status = 1
</select>
</mapper>
</mapper>
@@ -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;
}
}
}
@@ -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);
@@ -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;
}
}
}
@@ -62,7 +62,14 @@ public class VideoController {
@GetMapping("/public/{id}")
public Result<VideoInfoVO> getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
return videoService.getVideoInfo(id, userId);
Result<VideoInfoVO> result = videoService.getVideoInfo(id, userId);
if (result.getData() == null) {
return result;
}
if (result.getData().getPublishTime() == null) {
return Result.error("视频暂未公开");
}
return result;
}
/**
@@ -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;
}
}