feat(backend): Improve tag management and media resource access
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
+56
@@ -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);
|
||||
}
|
||||
|
||||
+20
@@ -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);
|
||||
|
||||
/**
|
||||
* 统计视频评论数
|
||||
*
|
||||
|
||||
+2
-8
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有存储桶
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
+7
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -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;
|
||||
|
||||
/**
|
||||
* 分类名称
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user