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}
|
- MYSQL_PASSWORD=${MYSQL_ROOT_PASSWORD:-root123456}
|
||||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-redis_HA_Cluster_2024}
|
- 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}
|
- 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_HOST=192.168.1.101
|
||||||
- MINIO_PORT=9000
|
- MINIO_PORT=9100
|
||||||
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||||
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||||
- MINIO_BUCKET=${MINIO_BUCKET}
|
- 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}
|
- 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_HOST=rmqnamesrv
|
||||||
- ROCKETMQ_PORT=9876
|
- ROCKETMQ_PORT=9876
|
||||||
- MINIO_HOST=minio
|
- MINIO_HOST=192.168.1.101
|
||||||
- MINIO_PORT=9000
|
- MINIO_PORT=9100
|
||||||
- MINIO_ACCESS_KEY=minioadmin
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
- MINIO_SECRET_KEY=minioadmin123
|
- MINIO_SECRET_KEY=minioadmin123
|
||||||
- MINIO_BUCKET=video-platform
|
- MINIO_BUCKET=video-platform
|
||||||
|
|||||||
+56
@@ -119,4 +119,60 @@ public class ContentManageController {
|
|||||||
@PathVariable Long categoryId) {
|
@PathVariable Long categoryId) {
|
||||||
return contentFeignClient.deleteCategory(categoryId, internalServiceToken);
|
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,
|
Result<Void> deleteCategory(@PathVariable("categoryId") Long categoryId,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@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")
|
@GetMapping("/content/video/admin/stats")
|
||||||
Result<Object> getVideoStats(@RequestHeader("X-Internal-Token") String internalToken);
|
Result<Object> getVideoStats(@RequestHeader("X-Internal-Token") String internalToken);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -58,6 +58,26 @@ public class ContentFeignClientFallbackFactory implements FallbackFactory<Conten
|
|||||||
return Result.error("删除分类失败:服务不可用");
|
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
|
@Override
|
||||||
public Result<Object> getVideoStats(String internalToken) {
|
public Result<Object> getVideoStats(String internalToken) {
|
||||||
return Result.error("获取视频统计失败:服务不可用");
|
return Result.error("获取视频统计失败:服务不可用");
|
||||||
|
|||||||
+13
-1
@@ -40,6 +40,18 @@ public interface CommentMapper extends BaseMapper<Comment> {
|
|||||||
*/
|
*/
|
||||||
int increaseReplyCount(@Param("commentId") Long commentId, @Param("count") int count);
|
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 评论数
|
* @return 评论数
|
||||||
*/
|
*/
|
||||||
int countVideoComments(@Param("videoId") Long videoId);
|
int countVideoComments(@Param("videoId") Long videoId);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -177,10 +177,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 逻辑删除评论(设置状态为0)
|
// 逻辑删除评论(设置状态为0)
|
||||||
comment.setStatus(0);
|
commentMapper.updateCommentStatus(id, 0, userId.toString());
|
||||||
comment.setUpdatedBy(userId.toString());
|
|
||||||
comment.setUpdateTime(LocalDateTime.now());
|
|
||||||
updateById(comment);
|
|
||||||
|
|
||||||
updateVideoCommentCountAsync(comment.getVideoId(), -1);
|
updateVideoCommentCountAsync(comment.getVideoId(), -1);
|
||||||
|
|
||||||
@@ -449,10 +446,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 逻辑删除评论(设置状态为0)
|
// 逻辑删除评论(设置状态为0)
|
||||||
comment.setStatus(0);
|
commentMapper.updateCommentStatus(commentId, 0, "admin");
|
||||||
comment.setUpdatedBy("admin");
|
|
||||||
comment.setUpdateTime(LocalDateTime.now());
|
|
||||||
updateById(comment);
|
|
||||||
|
|
||||||
updateVideoCommentCountAsync(comment.getVideoId(), -1);
|
updateVideoCommentCountAsync(comment.getVideoId(), -1);
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,14 @@
|
|||||||
WHERE id = #{commentId}
|
WHERE id = #{commentId}
|
||||||
</update>
|
</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 id="countVideoComments" resultType="int">
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
@@ -49,4 +57,4 @@
|
|||||||
WHERE video_id = #{videoId}
|
WHERE video_id = #{videoId}
|
||||||
AND status = 1
|
AND status = 1
|
||||||
</select>
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -50,12 +50,66 @@ public class MinioUtils {
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||||||
}
|
}
|
||||||
|
ensureBucketPublicRead(bucketName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建存储桶失败:", e);
|
log.error("创建存储桶失败:", e);
|
||||||
throw new RuntimeException("创建存储桶失败", 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;
|
return objectName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
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.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
@@ -53,6 +54,18 @@ public class SecurityConfig {
|
|||||||
"/content/category/admin/**"
|
"/content/category/admin/**"
|
||||||
).access((authentication, context) ->
|
).access((authentication, context) ->
|
||||||
new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token"))))
|
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()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|||||||
+8
-1
@@ -88,6 +88,13 @@ public class TagController {
|
|||||||
if (userIdObj instanceof Long) {
|
if (userIdObj instanceof Long) {
|
||||||
return (Long) userIdObj;
|
return (Long) userIdObj;
|
||||||
}
|
}
|
||||||
|
String userIdHeader = request.getHeader("userId");
|
||||||
|
if (userIdHeader != null && !userIdHeader.isBlank()) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(userIdHeader);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -62,7 +62,14 @@ public class VideoController {
|
|||||||
@GetMapping("/public/{id}")
|
@GetMapping("/public/{id}")
|
||||||
public Result<VideoInfoVO> getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) {
|
public Result<VideoInfoVO> getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) {
|
||||||
Long userId = getUserIdFromRequest(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 duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频状态
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分类名称
|
* 分类名称
|
||||||
*/
|
*/
|
||||||
@@ -82,4 +87,4 @@ public class VideoListVO implements Serializable {
|
|||||||
* 发布时间
|
* 发布时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime publishTime;
|
private LocalDateTime publishTime;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user