diff --git a/.gitignore b/.gitignore index 18f34bd..84b9e58 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ docker/*/data/ docker/minio/config docker/xxljob/logs application-youlai.yml +.claude +CLAUDE.md diff --git a/src/main/java/com/youlai/boot/mini/controller/UserPostController.java b/src/main/java/com/youlai/boot/mini/controller/UserPostController.java index 4f3db43..3ab5c7f 100644 --- a/src/main/java/com/youlai/boot/mini/controller/UserPostController.java +++ b/src/main/java/com/youlai/boot/mini/controller/UserPostController.java @@ -1,10 +1,34 @@ package com.youlai.boot.mini.controller; +import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.common.annotation.RepeatSubmit; +import com.youlai.boot.common.enums.ActionTypeEnum; +import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.common.result.PageResult; +import com.youlai.boot.common.result.Result; +import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.model.dto.DeleteUserPostDTO; +import com.youlai.boot.mini.model.dto.DeleteUserPostMediaDTO; +import com.youlai.boot.mini.model.dto.EditVisibilityDTO; +import com.youlai.boot.mini.model.form.UserPostCollectForm; +import com.youlai.boot.mini.model.form.UserPostForm; +import com.youlai.boot.mini.model.form.UserPostLikeForm; +import com.youlai.boot.mini.model.query.OwnUserPostQuery; +import com.youlai.boot.mini.model.query.UserPostWaterfallQuery; +import com.youlai.boot.mini.model.vo.UserPostDetailsVO; +import com.youlai.boot.mini.model.vo.UserPostVO; +import com.youlai.boot.mini.model.vo.WaterfallResult; import com.youlai.boot.mini.service.UserPostService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; @Tag(name = "用户作品的相关接口") @RestController @@ -14,4 +38,149 @@ public class UserPostController { private final UserPostService userPostService; + @Operation(summary = "添加用户作品时先保存文件") + @PostMapping(value = "/save/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.INSERT) + public Result> saveFile( + @RequestPart(name = "images", required = false) List images, + @RequestPart(name = "videos", required = false) List videos + ) { + List urlList = userPostService.saveFile(images, videos); + return Result.success(urlList); + } + + @Operation(summary = "添加用户作品信息") + @PostMapping(value = "/save") + @RepeatSubmit + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.INSERT) + public Result saveUserPost(@Valid @RequestBody UserPostForm formData) { + String postUuid = userPostService.saveUserPost(formData); + return Result.success(postUuid); + } + + @Operation(summary = "编辑用户作品(不包含图片/视频媒体资源)") + @PostMapping(value = "/update/{postUuid}") + @RepeatSubmit + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.UPDATE) + public Result updateUserPost( + @PathVariable String postUuid, + @Valid @RequestBody UserPostForm formData + ) { + userPostService.updateUserPost(postUuid, formData); + return Result.success(); + } + + @Operation(summary = "编辑用户作品时,删除媒体资源") + @PostMapping(value = "/update/deleteMediaSource") + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.DELETE) + public Result deleteMediaSource(@Valid @RequestBody DeleteUserPostMediaDTO dto) { + userPostService.deleteMediaSource(dto); + return Result.success(); + } + + @Operation(summary = "编辑用户作品时,添加媒体资源", description = "比如补充图片、补充视频") + @PostMapping(value = "/update/saveMediaSource/{postUuid}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @RepeatSubmit + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.UPDATE) + public Result saveMediaSource( + @PathVariable String postUuid, + @RequestPart(name = "images", required = false) List images, + @RequestPart(name = "videos", required = false) List videos + ) { + userPostService.saveMediaSource(postUuid, images, videos); + return Result.success(); + } + + @Operation(summary = "编辑用户作品可见范围") + @PostMapping(value = "/update/visibility/{postUuid}") + @RepeatSubmit(expire = 1) + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.UPDATE) + public Result updateVisibility( + @PathVariable String postUuid, + @Valid @RequestBody EditVisibilityDTO dto + ) { + userPostService.updateVisibility(postUuid, dto); + return Result.success(); + } + + @Operation(summary = "删除用户作品") + @PostMapping(value = "/delete") + @RepeatSubmit + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.DELETE) + public Result delete(@Valid @RequestBody DeleteUserPostDTO dto) { + userPostService.delete(dto); + return Result.success(); + } + + @Operation(summary = "获取自己创建的作品列表") + @GetMapping(value = "/getSelfCreatedPage") + public PageResult getSelfCreatedPage(OwnUserPostQuery queryParams) { + return PageResult.success(userPostService.getSelfCreatedPage(queryParams)); + } + + @Operation(summary = "获取用户作品详情") + @RequestMapping(value = "/getDetails/{postUuid}", method = RequestMethod.GET) + public Result getDetails(@PathVariable String postUuid) { + return Result.success(userPostService.getDetails(postUuid)); + } + + @Operation(summary = "获取某个用户创建的作品列表") + @GetMapping(value = "/getOthersCreatedPage/{authorUuid}") + public PageResult getOthersCreatedPage( + @PathVariable String authorUuid, + OwnUserPostQuery queryParams + ) { + return PageResult.success(userPostService.getOthersCreatedPage(authorUuid, queryParams)); + } + + @Operation(summary = "用户作品点赞/取消点赞接口") + @PostMapping(value = "/like/toggle") + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.UPDATE) + public Result> toggleUserPostLike( + @Valid @RequestBody UserPostLikeForm form + ) { + Long userId = SecurityUtils.getUserId(); + Map result = userPostService.toggleUserPostLike(form, userId); + return Result.success(result); + } + + @Operation(summary = "用户作品收藏/取消收藏接口") + @PostMapping(value = "/collect/toggle") + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.UPDATE) + public Result> toggleUserPostCollect( + @Valid @RequestBody UserPostCollectForm form + ) { + Long userId = SecurityUtils.getUserId(); + Map result = userPostService.toggleUserPostCollect(form, userId); + return Result.success(result); + } + + @Operation(summary = "获取用户作品瀑布流") + @GetMapping(value = "/waterfall") + public Result> getWaterfall( + @Valid UserPostWaterfallQuery query + ) { + Long userId = SecurityUtils.getUserId(); + WaterfallResult result = userPostService.getWaterfall(query, userId); + return Result.success(result); + } + + @Operation(summary = "重置当前用户的浏览记录过滤规则", description = "重置后瀑布流会重新展示所有看过的内容") + @PostMapping(value = "/resetViewBloom") + public Result resetViewBloom() { + Long userId = SecurityUtils.getUserId(); + userPostService.resetCurrentUserBloom(userId); + return Result.success("重置成功"); + } + + @Operation(summary = "增加用户作品浏览量") + @PostMapping(value = "/increase-view") + @Log(module = LogModuleEnum.USER_POST_INFO, value = ActionTypeEnum.UPDATE) + @RepeatSubmit(expire = 1) + public Result increaseViewCount(@RequestParam String postUuid) { + userPostService.increaseUserPostViewCount(postUuid); + return Result.success(); + } + + } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostCollectMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostCollectMapper.java index c6d7cf3..d4272ee 100644 --- a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostCollectMapper.java +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostCollectMapper.java @@ -2,13 +2,19 @@ package com.youlai.boot.mini.mapper; import com.youlai.boot.mini.model.entity.MiniUserPostCollect; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; /** * 用户作品收藏表 Mapper 接口 * * @author jwy -* @since +* @since */ public interface MiniUserPostCollectMapper extends BaseMapper { + Integer selectUserCollectCount(@Param("postId") Long postId, @Param("userId") Long userId); + + void insertOrUpdateCollect(MiniUserPostCollect collect); + + void deleteCollect(@Param("postId") Long postId, @Param("userId") Long userId, @Param("currentTime") long currentTime); } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostLikeMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostLikeMapper.java index c4cc0e1..d27e98a 100644 --- a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostLikeMapper.java +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostLikeMapper.java @@ -2,13 +2,19 @@ package com.youlai.boot.mini.mapper; import com.youlai.boot.mini.model.entity.MiniUserPostLike; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; /** * 用户作品点赞表 Mapper 接口 * * @author jwy -* @since +* @since */ public interface MiniUserPostLikeMapper extends BaseMapper { + Integer selectUserLikeCount(@Param("postId") Long postId, @Param("userId") Long userId); + + void insertOrUpdateLike(MiniUserPostLike like); + + void deleteLike(@Param("postId") Long postId, @Param("userId") Long userId, @Param("currentTime") long currentTime); } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMapper.java index 6e418be..dd9d061 100644 --- a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMapper.java +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMapper.java @@ -1,14 +1,41 @@ package com.youlai.boot.mini.mapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.mini.model.entity.MiniUserPost; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.youlai.boot.mini.model.query.OwnUserPostQuery; +import com.youlai.boot.mini.model.vo.UserPostVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; /** * 用户作品表 Mapper 接口 * * @author jwy -* @since +* @since */ public interface MiniUserPostMapper extends BaseMapper { + IPage getUserPostPage(Page page, @Param("query") OwnUserPostQuery query); + + Long selectIdByUuid(String postUuid); + + void incrementLikeCount(@Param("postId") Long postId); + + void decrementLikeCount(@Param("postId") Long postId); + + Long selectLikeCount(@Param("postId") Long postId); + + void incrementCollectCount(@Param("postId") Long postId); + + void decrementCollectCount(@Param("postId") Long postId); + + Long selectCollectCount(@Param("postId") Long postId); + + List getWaterfall(@Param("cursor") Long cursor, @Param("pageSize") int pageSize, @Param("miniUserId") Long miniUserId); + + void incrementViewCount(String postUuid); + } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMediaMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMediaMapper.java index 9885cb5..7b13f23 100644 --- a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMediaMapper.java +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMediaMapper.java @@ -2,13 +2,19 @@ package com.youlai.boot.mini.mapper; import com.youlai.boot.mini.model.entity.MiniUserPostMedia; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.youlai.boot.mini.model.vo.UserPostMediaVO; + +import java.util.List; +import java.util.Map; /** * 用户作品资源表 Mapper 接口 * * @author jwy -* @since +* @since */ public interface MiniUserPostMediaMapper extends BaseMapper { + List getMediaByPostIdAndType(Map param); + } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostViewMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostViewMapper.java index 4ebdb73..54bbf05 100644 --- a/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostViewMapper.java +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserPostViewMapper.java @@ -2,13 +2,18 @@ package com.youlai.boot.mini.mapper; import com.youlai.boot.mini.model.entity.MiniUserPostView; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; /** * 用户作品浏览记录表 Mapper 接口 * * @author jwy -* @since +* @since */ public interface MiniUserPostViewMapper extends BaseMapper { + List selectViewedPostIdsByUserAndTime(@Param("userId") Long userId, @Param("startTime") Long startTime); + } diff --git a/src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostDTO.java b/src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostDTO.java new file mode 100644 index 0000000..8c2cba3 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostDTO.java @@ -0,0 +1,20 @@ +package com.youlai.boot.mini.model.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Data +public class DeleteUserPostDTO { + + @NotEmpty(message = "uuid不能为空") + @ArraySchema( + arraySchema = @Schema(description = "用户作品uuid列表", requiredMode = Schema.RequiredMode.REQUIRED), + schema = @Schema(description = "用户作品uuid") + ) + private List<@NotBlank(message = "uuid不能为空") String> postUuidList; +} diff --git a/src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostMediaDTO.java b/src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostMediaDTO.java new file mode 100644 index 0000000..47a0d8c --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostMediaDTO.java @@ -0,0 +1,24 @@ +package com.youlai.boot.mini.model.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Data +public class DeleteUserPostMediaDTO { + + @NotEmpty(message = "媒体资源uuid不能为空") + @ArraySchema( + arraySchema = @Schema(description = "用户作品媒体资源uuid列表", requiredMode = Schema.RequiredMode.REQUIRED), + schema = @Schema(description = "媒体资源uuid") + ) + private List<@NotBlank(message = "uuid不能为空") String> mediaUuidList; + + @NotBlank(message = "作品uuid不能为空") + @Schema(description = "用户作品uuid", requiredMode = Schema.RequiredMode.REQUIRED) + private String postUuid; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/UserPostCollectForm.java b/src/main/java/com/youlai/boot/mini/model/form/UserPostCollectForm.java new file mode 100644 index 0000000..bf6e552 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/UserPostCollectForm.java @@ -0,0 +1,14 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "用户作品收藏请求参数") +public class UserPostCollectForm { + + @NotBlank(message = "作品UUID不能为空") + @Schema(description = "作品UUID", requiredMode = Schema.RequiredMode.REQUIRED, example = "a1b2c3d4e5f6g7h8i9j0") + private String postUuid; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/UserPostForm.java b/src/main/java/com/youlai/boot/mini/model/form/UserPostForm.java new file mode 100644 index 0000000..f62eed7 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/UserPostForm.java @@ -0,0 +1,31 @@ +package com.youlai.boot.mini.model.form; + +import com.youlai.boot.common.annotation.EnumValid; +import com.youlai.boot.mini.model.enums.VisibilityEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +@Data +@Schema(description = "用户作品表单对象") +public class UserPostForm { + + @NotBlank(message = "作品标题不能为空") + @Length(max = 200, message = "标题不能超过200个字符") + @Schema(description = "作品标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + @Schema(description = "作品正文内容") + private String content; + + @NotBlank(message = "可见范围不能为空") + @EnumValid(enumClass = VisibilityEnum.class, message = "可见范围不合法") + @Schema(description = "可见范围:public-公开,private-仅自己,friends-仅好友") + private String visibility; + + @Schema(description = "上传的媒体资源URL列表") + private List mediaUrlList; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/UserPostLikeForm.java b/src/main/java/com/youlai/boot/mini/model/form/UserPostLikeForm.java new file mode 100644 index 0000000..067d709 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/UserPostLikeForm.java @@ -0,0 +1,14 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "用户作品点赞请求参数") +public class UserPostLikeForm { + + @NotBlank(message = "作品UUID不能为空") + @Schema(description = "作品UUID", requiredMode = Schema.RequiredMode.REQUIRED, example = "a1b2c3d4e5f6g7h8i9j0") + private String postUuid; +} diff --git a/src/main/java/com/youlai/boot/mini/model/query/OwnUserPostQuery.java b/src/main/java/com/youlai/boot/mini/model/query/OwnUserPostQuery.java new file mode 100644 index 0000000..5e2116f --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/query/OwnUserPostQuery.java @@ -0,0 +1,23 @@ +package com.youlai.boot.mini.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class OwnUserPostQuery extends UserPostQuery { + + @Schema(description = "登录用户ID", example = "1", hidden = true) + private Long miniUserId; + + @Schema(description = "创建作品的用户ID", example = "1", hidden = true) + private Long creatorId; + + @Schema(description = "关键词", example = "标题内容", hidden = true) + private String keyword; + + @Schema(description = "权限", example = "public", hidden = true) + private String visibility; + +} diff --git a/src/main/java/com/youlai/boot/mini/model/query/UserPostQuery.java b/src/main/java/com/youlai/boot/mini/model/query/UserPostQuery.java new file mode 100644 index 0000000..9c4b8e6 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/query/UserPostQuery.java @@ -0,0 +1,18 @@ +package com.youlai.boot.mini.model.query; + +import com.youlai.boot.common.base.BaseQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class UserPostQuery extends BaseQuery { + + @Schema(description = "起始登记时间,毫秒级时间戳", example = "1776426078459") + private Long createStartTimestamp; + + @Schema(description = "截止登记时间,毫秒级时间戳", example = "1776426078459") + private Long createEndTimestamp; + +} diff --git a/src/main/java/com/youlai/boot/mini/model/query/UserPostWaterfallQuery.java b/src/main/java/com/youlai/boot/mini/model/query/UserPostWaterfallQuery.java new file mode 100644 index 0000000..8733da0 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/query/UserPostWaterfallQuery.java @@ -0,0 +1,13 @@ +package com.youlai.boot.mini.model.query; + +import com.youlai.boot.common.base.BaseQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class UserPostWaterfallQuery extends BaseQuery { + + @Schema(description = "游标(上一页最后一条的ID,首次请求不传)", example = "100") + private Long cursor; + +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/UserPostDetailsVO.java b/src/main/java/com/youlai/boot/mini/model/vo/UserPostDetailsVO.java new file mode 100644 index 0000000..7196b43 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/UserPostDetailsVO.java @@ -0,0 +1,17 @@ +package com.youlai.boot.mini.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class UserPostDetailsVO { + + @Schema(description = "图片信息", example = "[]") + private List images; + + @Schema(description = "视频信息", example = "[]") + private List videos; + +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/UserPostMediaVO.java b/src/main/java/com/youlai/boot/mini/model/vo/UserPostMediaVO.java new file mode 100644 index 0000000..9b88876 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/UserPostMediaVO.java @@ -0,0 +1,35 @@ +package com.youlai.boot.mini.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class UserPostMediaVO { + + @Schema(type = "string", description = "资源ID") + private String postMediaUuid; + + @Schema(type = "string", description = "用户作品UUID") + private String postUuid; + + @Schema(description = "媒体类型,image-图片,video-视频") + private String mediaType; + + @Schema(description = "资源URL") + private String sourceUrl; + + @Schema(description = "对象存储中的key") + private String storageKey; + + @Schema(description = "缩略图URL(视频需要)") + private String thumbnailUrl; + + @Schema(description = "宽度(像素)") + private Integer width; + + @Schema(description = "高度(像素)") + private Integer height; + + @Schema(description = "时长(秒,视频用)") + private Integer duration; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/UserPostVO.java b/src/main/java/com/youlai/boot/mini/model/vo/UserPostVO.java new file mode 100644 index 0000000..56d92fa --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/UserPostVO.java @@ -0,0 +1,61 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Data +public class UserPostVO { + + @Schema(description = "作品ID", hidden = true) + private Long id; + + @Schema(description = "作者uuid") + private String authorUuid; + + @Schema(description = "作者昵称") + private String authorName; + + @Schema(description = "作者头像") + private String authorAvatar; + + @Schema(description = "作品UUID", requiredMode = Schema.RequiredMode.REQUIRED) + private String postUuid; + + @Schema(description = "封面图片url") + private String firstImageUrl; + + @Schema(description = "作品标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + @Schema(description = "作品内容摘要") + private String content; + + @Schema(description = "可见性范围:public-公开,private-仅自己,friends-仅好友", example = "public") + private String visibility; + + @Schema(description = "浏览数") + private Integer viewCount; + + @Schema(description = "点赞数") + private Integer likeCount; + + @Schema(description = "评论数") + private Integer commentCount; + + @Schema(description = "收藏数") + private Integer collectCount; + + @Schema(description = "当前用户是否已点赞") + private Boolean isLiked; + + @Schema(description = "当前用户是否收藏") + private Boolean isCollected; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + +} diff --git a/src/main/java/com/youlai/boot/mini/service/UserPostService.java b/src/main/java/com/youlai/boot/mini/service/UserPostService.java index 3fd6ec1..1ffe2ea 100644 --- a/src/main/java/com/youlai/boot/mini/service/UserPostService.java +++ b/src/main/java/com/youlai/boot/mini/service/UserPostService.java @@ -1,7 +1,53 @@ package com.youlai.boot.mini.service; +import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; +import com.youlai.boot.mini.model.dto.DeleteUserPostDTO; +import com.youlai.boot.mini.model.dto.DeleteUserPostMediaDTO; +import com.youlai.boot.mini.model.dto.EditVisibilityDTO; import com.youlai.boot.mini.model.entity.MiniUserPost; +import com.youlai.boot.mini.model.form.UserPostForm; +import com.youlai.boot.mini.model.form.UserPostCollectForm; +import com.youlai.boot.mini.model.form.UserPostLikeForm; +import com.youlai.boot.mini.model.query.OwnUserPostQuery; +import com.youlai.boot.mini.model.query.UserPostWaterfallQuery; +import com.youlai.boot.mini.model.vo.UserPostDetailsVO; +import com.youlai.boot.mini.model.vo.UserPostVO; +import com.youlai.boot.mini.model.vo.WaterfallResult; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; public interface UserPostService extends IService { + + List saveFile(List images, List videos); + + String saveUserPost(UserPostForm formData); + + void updateUserPost(String postUuid, UserPostForm formData); + + void deleteMediaSource(DeleteUserPostMediaDTO dto); + + void saveMediaSource(String postUuid, List images, List videos); + + void updateVisibility(String postUuid, EditVisibilityDTO dto); + + void delete(DeleteUserPostDTO dto); + + IPage getSelfCreatedPage(OwnUserPostQuery queryParams); + + UserPostDetailsVO getDetails(String postUuid); + + IPage getOthersCreatedPage(String authorUuid, OwnUserPostQuery queryParams); + + Map toggleUserPostLike(UserPostLikeForm form, Long userId); + + Map toggleUserPostCollect(UserPostCollectForm form, Long userId); + + WaterfallResult getWaterfall(UserPostWaterfallQuery query, Long userId); + + void resetCurrentUserBloom(Long userId); + + void increaseUserPostViewCount(String postUuid); } diff --git a/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java index d89b8e9..818b5fc 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java @@ -1,15 +1,715 @@ package com.youlai.boot.mini.service.impl; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.youlai.boot.common.exception.MsgException; +import com.youlai.boot.common.util.FileUtils; +import com.youlai.boot.common.util.JavaVCUtils; +import com.youlai.boot.common.util.RandomNumberUtils; +import com.youlai.boot.file.service.impl.AliyunFileService; +import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.mapper.MiniUserPostCollectMapper; +import com.youlai.boot.mini.mapper.MiniUserPostLikeMapper; import com.youlai.boot.mini.mapper.MiniUserPostMapper; +import com.youlai.boot.mini.mapper.MiniUserPostMediaMapper; +import com.youlai.boot.mini.mapper.MiniUserPostViewMapper; +import com.youlai.boot.mini.model.dto.DeleteUserPostDTO; +import com.youlai.boot.mini.model.dto.DeleteUserPostMediaDTO; +import com.youlai.boot.mini.model.dto.EditVisibilityDTO; import com.youlai.boot.mini.model.entity.MiniUserPost; +import com.youlai.boot.mini.model.entity.MiniUserPostCollect; +import com.youlai.boot.mini.model.entity.MiniUserPostLike; +import com.youlai.boot.mini.model.entity.MiniUserPostMedia; +import com.youlai.boot.mini.model.enums.AnimalNoteMediaTypeEnum; +import com.youlai.boot.mini.model.form.UserPostCollectForm; +import com.youlai.boot.mini.model.form.UserPostForm; +import com.youlai.boot.mini.model.form.UserPostLikeForm; +import com.youlai.boot.mini.model.query.OwnUserPostQuery; +import com.youlai.boot.mini.model.query.UserPostWaterfallQuery; +import com.youlai.boot.mini.model.vo.UserPostDetailsVO; +import com.youlai.boot.mini.model.vo.UserPostMediaVO; +import com.youlai.boot.mini.model.vo.UserPostVO; +import com.youlai.boot.mini.model.vo.WaterfallResult; import com.youlai.boot.mini.service.UserPostService; +import com.youlai.boot.system.mapper.UserMapper; +import com.youlai.boot.system.model.entity.SysUser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; +import org.redisson.api.RBloomFilter; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j @Service @RequiredArgsConstructor -@Slf4j public class UserPostServiceImpl extends ServiceImpl implements UserPostService { + + private final AliyunFileService aliyunFileService; + private final MiniUserPostMediaMapper miniUserPostMediaMapper; + private final MiniUserPostLikeMapper miniUserPostLikeMapper; + private final MiniUserPostCollectMapper miniUserPostCollectMapper; + private final MiniUserPostViewMapper miniUserPostViewMapper; + private final UserMapper userMapper; + private final RedissonClient redissonClient; + + private static final String BLOOM_VIEW_KEY_PREFIX = "mini:userPost:view:bloom:"; + private static final String BLOOM_REBUILD_LOCK_PREFIX = "lock:rebuild:userPost:bloom:"; + + private static final String OSS_IMAGE_DIR = "user_post/image/"; + private static final String OSS_VIDEO_DIR = "user_post/video/"; + private static final String OSS_THUMBNAIL_DIR = "user_post/thumbnail/"; + + @Value("${oss.aliyun.endpoint}") + private String endpoint; + + @Value("${oss.aliyun.bucket-name}") + private String bucketName; + + @Value("${bloom.view.expected-insertions:1000}") + private Integer bloomExpectedInsertions; + + @Value("${bloom.view.fpp:0.001}") + private Double bloomFpp; + + @Value("${bloom.view.expire-days:}") + private Integer bloomExpireDays; + + @Override + public List saveFile(List images, List videos) { + long currentTimestamp = System.currentTimeMillis(); + List urlList = new ArrayList<>(); + + // 处理图片 + if (CollUtil.isNotEmpty(images)) { + for (MultipartFile image : images) { + try { + String objectName = OSS_IMAGE_DIR + + currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) + + "." + + FilenameUtils.getExtension(image.getOriginalFilename()); + String url = aliyunFileService.uploadFile(objectName, image.getInputStream()); + + MiniUserPostMedia media = new MiniUserPostMedia(); + media.setUuid(UUID.randomUUID().toString()); + media.setMediaType(AnimalNoteMediaTypeEnum.IMAGE.name().toLowerCase()); + media.setSourceUrl(url); + media.setStorageKey(objectName); + BufferedImage imageInfo = ImageIO.read(image.getInputStream()); + media.setWidth(imageInfo.getWidth()); + media.setHeight(imageInfo.getHeight()); + media.setCreateTimestamp(currentTimestamp); + media.setCreateTime(new Date(currentTimestamp)); + media.setCreateBy(SecurityUtils.getUserId()); + + int result = miniUserPostMediaMapper.insert(media); + if (result > 0) { + urlList.add(url); + } + } catch (Exception e) { + log.error("用户作品图片上传失败", e); + } + } + } + + // 处理视频 + if (CollUtil.isNotEmpty(videos)) { + String tmpPath = System.getProperty("user.dir") + "/tmp"; + for (MultipartFile video : videos) { + try { + String fileName = currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); + String objectName = OSS_VIDEO_DIR + + fileName + + "." + + FilenameUtils.getExtension(video.getOriginalFilename()); + String url = aliyunFileService.uploadFile(objectName, video.getInputStream()); + + MiniUserPostMedia media = new MiniUserPostMedia(); + media.setUuid(UUID.randomUUID().toString()); + media.setMediaType(AnimalNoteMediaTypeEnum.VIDEO.name().toLowerCase()); + media.setSourceUrl(url); + media.setStorageKey(objectName); + media.setCreateTimestamp(currentTimestamp); + media.setCreateTime(new Date(currentTimestamp)); + media.setCreateBy(SecurityUtils.getUserId()); + + // 时长 + FileUtils.saveFile(video, tmpPath, fileName); + String videoPath = tmpPath + File.separator + fileName; + double duration = JavaVCUtils.getVideoDuration(videoPath); + media.setDuration((int) Math.ceil(duration)); + + // 缩略图 + BufferedImage thumbnail = JavaVCUtils.getVideoThumbnail(videoPath, 1); + String thumbnailFileName = currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); + String thumbnailObjectName = OSS_THUMBNAIL_DIR + thumbnailFileName + ".png"; + String thumbnailUrl = aliyunFileService.uploadFile(thumbnailObjectName, + FileUtils.bufferedImageToInputStream(thumbnail, "png")); + media.setThumbnailUrl(thumbnailUrl); + + int result = miniUserPostMediaMapper.insert(media); + if (result > 0) { + urlList.add(url); + } + + FileUtils.delete(videoPath); + } catch (Exception e) { + log.error("用户作品视频上传失败", e); + } + } + } + + return urlList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String saveUserPost(UserPostForm formData) { + long currentTimestamp = System.currentTimeMillis(); + Long userId = SecurityUtils.getUserId(); + + // 1. 保存用户作品基本信息 + MiniUserPost post = new MiniUserPost(); + post.setUuid(UUID.randomUUID().toString()); + post.setMiniUserId(userId); + post.setTitle(formData.getTitle()); + post.setContent(formData.getContent()); + post.setVisibility(formData.getVisibility()); + post.setViewCount(0); + post.setLikeCount(0); + post.setCommentCount(0); + post.setCollectCount(0); + post.setCreateBy(userId); + post.setCreateTime(new Date(currentTimestamp)); + post.setCreateTimestamp(currentTimestamp); + post.setDeleted(false); + save(post); + + // 2. 关联之前上传的媒体文件 + if (CollUtil.isNotEmpty(formData.getMediaUrlList())) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.in(MiniUserPostMedia::getSourceUrl, formData.getMediaUrlList()) + .isNull(MiniUserPostMedia::getPostId) + .set(MiniUserPostMedia::getPostId, post.getId()); + miniUserPostMediaMapper.update(null, wrapper); + } + + return post.getUuid(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserPost(String postUuid, UserPostForm formData) { + MiniUserPost post = lambdaQuery().eq(MiniUserPost::getUuid, postUuid).one(); + if (post == null) { + return; + } + long currentTimestamp = System.currentTimeMillis(); + post.setTitle(formData.getTitle()); + post.setContent(formData.getContent()); + post.setVisibility(formData.getVisibility()); + post.setUpdateBy(SecurityUtils.getUserId()); + post.setUpdateTime(new Date(currentTimestamp)); + post.setUpdateTimestamp(currentTimestamp); + updateById(post); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMediaSource(DeleteUserPostMediaDTO dto) { + List mediaUuidList = dto.getMediaUuidList(); + + // 校验作品是否存在 + MiniUserPost post = lambdaQuery() + .eq(MiniUserPost::getUuid, dto.getPostUuid()) + .eq(MiniUserPost::getDeleted, false) + .one(); + if (post == null) { + throw new MsgException("用户作品不存在"); + } + + // 查询要删除的媒体资源 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(MiniUserPostMedia::getUuid, mediaUuidList) + .eq(MiniUserPostMedia::getPostId, post.getId()) + .eq(MiniUserPostMedia::getDeleted, false); + + List mediaList = miniUserPostMediaMapper.selectList(queryWrapper); + + if (CollectionUtils.isNotEmpty(mediaList)) { + // 删除阿里云OSS源文件 + List storageKeyList = mediaList.stream() + .map(MiniUserPostMedia::getStorageKey) + .toList(); + aliyunFileService.deleteMultiFile(storageKeyList); + + long currentTimestamp = System.currentTimeMillis(); + + // 逻辑删除数据库记录 + miniUserPostMediaMapper.update( + null, + new LambdaUpdateWrapper() + .in(MiniUserPostMedia::getUuid, mediaUuidList) + .eq(MiniUserPostMedia::getPostId, post.getId()) + .set(MiniUserPostMedia::getDeleted, true) + .set(MiniUserPostMedia::getUpdateTimestamp, currentTimestamp) + ); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMediaSource(String postUuid, List images, List videos) { + MiniUserPost post = lambdaQuery().eq(MiniUserPost::getUuid, postUuid).one(); + if (post == null) { + return; + } + long currentTimestamp = System.currentTimeMillis(); + + // 处理图片 + if (CollUtil.isNotEmpty(images)) { + for (MultipartFile image : images) { + try { + String objectName = OSS_IMAGE_DIR + + currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) + + "." + + FilenameUtils.getExtension(image.getOriginalFilename()); + String url = aliyunFileService.uploadFile(objectName, image.getInputStream()); + + MiniUserPostMedia media = new MiniUserPostMedia(); + media.setUuid(UUID.randomUUID().toString()); + media.setPostId(post.getId()); + media.setMediaType(AnimalNoteMediaTypeEnum.IMAGE.name().toLowerCase()); + media.setSourceUrl(url); + media.setStorageKey(objectName); + BufferedImage imageInfo = ImageIO.read(image.getInputStream()); + media.setWidth(imageInfo.getWidth()); + media.setHeight(imageInfo.getHeight()); + media.setCreateTimestamp(currentTimestamp); + media.setCreateTime(new Date(currentTimestamp)); + media.setCreateBy(SecurityUtils.getUserId()); + miniUserPostMediaMapper.insert(media); + } catch (Exception e) { + log.error("用户作品补充图片上传失败", e); + } + } + } + + // 处理视频 + if (CollUtil.isNotEmpty(videos)) { + String tmpPath = System.getProperty("user.dir") + "/tmp"; + for (MultipartFile video : videos) { + try { + String fileName = currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); + String objectName = OSS_VIDEO_DIR + + fileName + + "." + + FilenameUtils.getExtension(video.getOriginalFilename()); + String url = aliyunFileService.uploadFile(objectName, video.getInputStream()); + + MiniUserPostMedia media = new MiniUserPostMedia(); + media.setUuid(UUID.randomUUID().toString()); + media.setPostId(post.getId()); + media.setMediaType(AnimalNoteMediaTypeEnum.VIDEO.name().toLowerCase()); + media.setSourceUrl(url); + media.setStorageKey(objectName); + media.setCreateTimestamp(currentTimestamp); + media.setCreateTime(new Date(currentTimestamp)); + media.setCreateBy(SecurityUtils.getUserId()); + + // 时长 + FileUtils.saveFile(video, tmpPath, fileName); + String videoPath = tmpPath + File.separator + fileName; + double duration = JavaVCUtils.getVideoDuration(videoPath); + media.setDuration((int) Math.ceil(duration)); + + // 缩略图 + BufferedImage thumbnail = JavaVCUtils.getVideoThumbnail(videoPath, 1); + String thumbnailFileName = currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); + String thumbnailObjectName = OSS_THUMBNAIL_DIR + thumbnailFileName + ".png"; + String thumbnailUrl = aliyunFileService.uploadFile(thumbnailObjectName, + FileUtils.bufferedImageToInputStream(thumbnail, "png")); + media.setThumbnailUrl(thumbnailUrl); + + miniUserPostMediaMapper.insert(media); + FileUtils.delete(videoPath); + } catch (Exception e) { + log.error("用户作品补充视频上传失败", e); + } + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateVisibility(String postUuid, EditVisibilityDTO dto) { + Long userId = SecurityUtils.getUserId(); + + MiniUserPost post = lambdaQuery() + .eq(MiniUserPost::getUuid, postUuid) + .eq(MiniUserPost::getMiniUserId, userId) + .eq(MiniUserPost::getDeleted, false) + .one(); + + if (post == null) { + throw new MsgException("用户作品不存在"); + } + + long currentTimestamp = System.currentTimeMillis(); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(MiniUserPost::getUuid, postUuid) + .set(MiniUserPost::getVisibility, dto.getVisibility()) + .set(MiniUserPost::getUpdateTime, new Date(currentTimestamp)) + .set(MiniUserPost::getUpdateTimestamp, currentTimestamp); + update(updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(DeleteUserPostDTO dto) { + Long userId = SecurityUtils.getUserId(); + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.in(MiniUserPost::getUuid, dto.getPostUuidList()) + .eq(MiniUserPost::getMiniUserId, userId) + .eq(MiniUserPost::getDeleted, false) + .set(MiniUserPost::getDeleted, true) + .set(MiniUserPost::getUpdateTime, new Date()) + .set(MiniUserPost::getUpdateTimestamp, System.currentTimeMillis()); + + update(null, updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map toggleUserPostLike(UserPostLikeForm form, Long userId) { + Long postId = baseMapper.selectIdByUuid(form.getPostUuid()); + if (postId == null) { + throw new MsgException("用户作品不存在或已删除"); + } + + long currentTime = System.currentTimeMillis(); + Boolean currentLiked; + + Integer count = miniUserPostLikeMapper.selectUserLikeCount(postId, userId); + if (count != null && count > 0) { + currentLiked = true; + } else { + currentLiked = false; + } + + boolean targetLike = !currentLiked; + + if (targetLike) { + if (!Boolean.TRUE.equals(currentLiked)) { + MiniUserPostLike like = new MiniUserPostLike(); + like.setUuid(IdWorker.get32UUID()); + like.setPostId(postId); + like.setMiniUserId(userId); + like.setCreateBy(userId); + like.setCreateTimestamp(currentTime); + like.setCreateTime(new Date(currentTime)); + like.setUpdateBy(userId); + like.setUpdateTimestamp(currentTime); + like.setUpdateTime(new Date(currentTime)); + like.setDeleted(false); + miniUserPostLikeMapper.insertOrUpdateLike(like); + + baseMapper.incrementLikeCount(postId); + } + } else { + if (Boolean.TRUE.equals(currentLiked)) { + miniUserPostLikeMapper.deleteLike(postId, userId, currentTime); + + baseMapper.decrementLikeCount(postId); + } + } + + Long likeCount = baseMapper.selectLikeCount(postId); + Map result = new HashMap<>(); + result.put("isLiked", targetLike); + result.put("likeCount", likeCount != null ? likeCount : 0); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map toggleUserPostCollect(UserPostCollectForm form, Long userId) { + Long postId = baseMapper.selectIdByUuid(form.getPostUuid()); + if (postId == null) { + throw new MsgException("用户作品不存在或已删除"); + } + + long currentTime = System.currentTimeMillis(); + Boolean currentCollected; + + Integer count = miniUserPostCollectMapper.selectUserCollectCount(postId, userId); + if (count != null && count > 0) { + currentCollected = true; + } else { + currentCollected = false; + } + + boolean targetCollect = !currentCollected; + + if (targetCollect) { + if (!Boolean.TRUE.equals(currentCollected)) { + MiniUserPostCollect collect = new MiniUserPostCollect(); + collect.setUuid(IdWorker.get32UUID()); + collect.setPostId(postId); + collect.setMiniUserId(userId); + collect.setCreateBy(userId); + collect.setCreateTimestamp(currentTime); + collect.setCreateTime(new Date(currentTime)); + collect.setUpdateBy(userId); + collect.setUpdateTimestamp(currentTime); + collect.setUpdateTime(new Date(currentTime)); + collect.setDeleted(false); + miniUserPostCollectMapper.insertOrUpdateCollect(collect); + + baseMapper.incrementCollectCount(postId); + } + } else { + if (Boolean.TRUE.equals(currentCollected)) { + miniUserPostCollectMapper.deleteCollect(postId, userId, currentTime); + + baseMapper.decrementCollectCount(postId); + } + } + + Long collectCount = baseMapper.selectCollectCount(postId); + Map result = new HashMap<>(); + result.put("isCollected", targetCollect); + result.put("collectCount", collectCount != null ? collectCount : 0); + return result; + } + + @Override + public IPage getSelfCreatedPage(OwnUserPostQuery queryParams) { + Long userId = SecurityUtils.getUserId(); + queryParams.setMiniUserId(userId); + queryParams.setCreatorId(userId); + + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + return baseMapper.getUserPostPage(page, queryParams); + } + + @Override + public UserPostDetailsVO getDetails(String postUuid) { + MiniUserPost post = lambdaQuery() + .eq(MiniUserPost::getUuid, postUuid) + .eq(MiniUserPost::getDeleted, false) + .one(); + if (post == null) { + throw new MsgException("用户作品不存在"); + } + + UserPostDetailsVO detailsVO = new UserPostDetailsVO(); + + Map param = new HashMap<>(); + param.put("postId", post.getId()); + + param.put("mediaType", AnimalNoteMediaTypeEnum.IMAGE.name().toLowerCase()); + List images = miniUserPostMediaMapper.getMediaByPostIdAndType(param); + if (CollectionUtils.isNotEmpty(images)) { + images.forEach(item -> item.setPostUuid(post.getUuid())); + } + + param.put("mediaType", AnimalNoteMediaTypeEnum.VIDEO.name().toLowerCase()); + List videos = miniUserPostMediaMapper.getMediaByPostIdAndType(param); + if (CollectionUtils.isNotEmpty(videos)) { + videos.forEach(item -> item.setPostUuid(post.getUuid())); + } + + detailsVO.setImages(images); + detailsVO.setVideos(videos); + + return detailsVO; + } + + @Override + public IPage getOthersCreatedPage(String authorUuid, OwnUserPostQuery queryParams) { + SysUser sysUser = userMapper.selectOne(new LambdaQueryWrapper() + .eq(SysUser::getUuid, authorUuid) + .eq(SysUser::getIsDeleted, 0)); + if (sysUser == null) { + throw new MsgException("用户不存在"); + } + + queryParams.setMiniUserId(SecurityUtils.getUserId()); + queryParams.setCreatorId(sysUser.getId()); + + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + return baseMapper.getUserPostPage(page, queryParams); + } + + @Override + public WaterfallResult getWaterfall(UserPostWaterfallQuery query, Long userId) { + int pageSize = query.getPageSize(); + int querySize = pageSize + 1; + + List originalList = baseMapper.getWaterfall( + query.getCursor(), + querySize, + userId + ); + + Long nextCursor = null; + boolean isLastPage = true; + if (CollUtil.isNotEmpty(originalList)) { + if (originalList.size() > pageSize) { + nextCursor = originalList.get(pageSize - 1).getId(); + isLastPage = false; + } + } else { + return WaterfallResult.of(Collections.emptyList(), null, true); + } + + List resultList = originalList.stream().limit(pageSize).collect(Collectors.toList()); + + if (userId != null && CollUtil.isNotEmpty(resultList)) { + try { + String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId; + RBloomFilter bloomFilter = redissonClient.getBloomFilter(bloomKey); + if (bloomFilter.isExists()) { + resultList = resultList.stream() + .filter(item -> !bloomFilter.contains(item.getId())) + .collect(Collectors.toList()); + } else { + log.debug("用户{}布隆不存在,同步重建浏览记录", userId); + try { + rebuildUserBloomFromDB(userId, 365); + RBloomFilter newBloomFilter = redissonClient.getBloomFilter(bloomKey); + if (newBloomFilter.isExists()) { + resultList = resultList.stream() + .filter(item -> !newBloomFilter.contains(item.getId())) + .collect(Collectors.toList()); + } + log.debug("用户{}布隆同步重建完成,本次已过滤已浏览内容", userId); + } catch (Exception ex) { + log.error("用户{}布隆同步重建失败,本次不过滤,下次访问自动重试", userId, ex); + } + } + } catch (Exception e) { + log.error("布隆操作异常,自动降级不过滤,userId:{}, cursor:{}", userId, query.getCursor(), e); + } + } + + if (CollUtil.isNotEmpty(resultList)) { + resultList.forEach(item -> { + if (StrUtil.isBlank(item.getFirstImageUrl())) { + item.setFirstImageUrl(getDefaultCoverHost() + "/default_diary.png"); + } + }); + } + + return WaterfallResult.of(resultList, nextCursor, isLastPage); + } + + private String getDefaultCoverHost() { + return "https://" + bucketName + "." + endpoint; + } + + public void rebuildUserBloomFromDB(Long userId) { + rebuildUserBloomFromDB(userId, 0); + } + + public void rebuildUserBloomFromDB(Long userId, int days) { + if (userId == null) { + return; + } + Long startTime = null; + if (days > 0) { + startTime = System.currentTimeMillis() - (long) days * 24 * 60 * 60 * 1000; + } + List viewedPostIds = miniUserPostViewMapper.selectViewedPostIdsByUserAndTime(userId, startTime); + + if (CollUtil.isEmpty(viewedPostIds)) { + return; + } + + String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId; + RBloomFilter bloomFilter = redissonClient.getBloomFilter(bloomKey); + RLock lock = redissonClient.getLock(BLOOM_REBUILD_LOCK_PREFIX + userId); + + try { + if (lock.tryLock(3, TimeUnit.SECONDS)) { + try { + if (bloomFilter.isExists()) { + bloomFilter.delete(); + } + bloomFilter.tryInit(bloomExpectedInsertions, bloomFpp); + if (bloomExpireDays != null && bloomExpireDays > 0) { + bloomFilter.expire(bloomExpireDays, TimeUnit.DAYS); + } + + for (Long postId : viewedPostIds) { + bloomFilter.add(postId); + } + + log.info("用户{}的作品浏览布隆重建完成,共写入{}条记录", userId, viewedPostIds.size()); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("重建用户{}的作品布隆时获取锁异常", userId, e); + } catch (Exception e) { + log.error("重建用户{}的作品布隆失败", userId, e); + } + } + + @Override + public void resetCurrentUserBloom(Long userId) { + if (userId == null) { + throw new MsgException("用户未登录"); + } + try { + String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId; + RBloomFilter bloomFilter = redissonClient.getBloomFilter(bloomKey); + if (bloomFilter.isExists()) { + bloomFilter.delete(); + } + bloomFilter.tryInit(bloomExpectedInsertions, bloomFpp); + if (bloomExpireDays != null && bloomExpireDays > 0) { + bloomFilter.expire(bloomExpireDays, TimeUnit.DAYS); + } + log.info("用户{}的作品布隆过滤器重置成功,已清空历史过滤规则,新浏览内容将继续记录", userId); + } catch (Exception e) { + log.error("重置用户{}的作品布隆过滤器失败", userId, e); + throw new MsgException("重置失败,请稍后重试"); + } + } + + @Override + public void increaseUserPostViewCount(String postUuid) { + try { + baseMapper.incrementViewCount(postUuid); + } catch (Exception e) { + log.error("增加用户作品{}浏览量失败", postUuid, e); + } + } + + } diff --git a/src/main/resources/mapper/mini/MiniUserPostCollectMapper.xml b/src/main/resources/mapper/mini/MiniUserPostCollectMapper.xml index 77a3e84..210082a 100644 --- a/src/main/resources/mapper/mini/MiniUserPostCollectMapper.xml +++ b/src/main/resources/mapper/mini/MiniUserPostCollectMapper.xml @@ -5,5 +5,35 @@ + + + + INSERT INTO mini_user_post_collect + (uuid, post_id, mini_user_id, create_time, create_timestamp, create_by, update_time, update_timestamp, update_by, is_deleted) + VALUES + (#{uuid}, #{postId}, #{miniUserId}, #{createTime}, #{createTimestamp}, #{createBy}, #{updateTime}, #{updateTimestamp}, #{updateBy}, #{deleted}) + ON DUPLICATE KEY UPDATE + is_deleted = 0, + update_time = VALUES(update_time), + update_timestamp = VALUES(update_timestamp), + update_by = VALUES(update_by) + + + + UPDATE mini_user_post_collect + SET is_deleted = 1, + update_time = NOW(), + update_timestamp = #{currentTime}, + update_by = #{userId} + WHERE post_id = #{postId} + AND mini_user_id = #{userId} + AND is_deleted = 0 + diff --git a/src/main/resources/mapper/mini/MiniUserPostLikeMapper.xml b/src/main/resources/mapper/mini/MiniUserPostLikeMapper.xml index 362b741..db728d5 100644 --- a/src/main/resources/mapper/mini/MiniUserPostLikeMapper.xml +++ b/src/main/resources/mapper/mini/MiniUserPostLikeMapper.xml @@ -5,5 +5,35 @@ + + + + INSERT INTO mini_user_post_like + (uuid, post_id, mini_user_id, create_time, create_timestamp, create_by, update_time, update_timestamp, update_by, is_deleted) + VALUES + (#{uuid}, #{postId}, #{miniUserId}, #{createTime}, #{createTimestamp}, #{createBy}, #{updateTime}, #{updateTimestamp}, #{updateBy}, #{deleted}) + ON DUPLICATE KEY UPDATE + is_deleted = 0, + update_time = VALUES(update_time), + update_timestamp = VALUES(update_timestamp), + update_by = VALUES(update_by) + + + + UPDATE mini_user_post_like + SET is_deleted = 1, + update_time = NOW(), + update_timestamp = #{currentTime}, + update_by = #{userId} + WHERE post_id = #{postId} + AND mini_user_id = #{userId} + AND is_deleted = 0 + diff --git a/src/main/resources/mapper/mini/MiniUserPostMapper.xml b/src/main/resources/mapper/mini/MiniUserPostMapper.xml index a773f8f..caaf7bb 100644 --- a/src/main/resources/mapper/mini/MiniUserPostMapper.xml +++ b/src/main/resources/mapper/mini/MiniUserPostMapper.xml @@ -5,5 +5,188 @@ + + + + + + UPDATE mini_user_post + SET like_count = like_count + 1 + WHERE id = #{postId} + AND is_deleted = 0 + + + + UPDATE mini_user_post + SET like_count = like_count - 1 + WHERE id = #{postId} + AND is_deleted = 0 + AND like_count > 0 + + + + + + UPDATE mini_user_post + SET collect_count = collect_count + 1 + WHERE id = #{postId} + AND is_deleted = 0 + + + + UPDATE mini_user_post + SET collect_count = collect_count - 1 + WHERE id = #{postId} + AND is_deleted = 0 + AND collect_count > 0 + + + + + + + + UPDATE mini_user_post + SET view_count = view_count + 1 + WHERE uuid = #{postUuid} + AND is_deleted = 0 + diff --git a/src/main/resources/mapper/mini/MiniUserPostMediaMapper.xml b/src/main/resources/mapper/mini/MiniUserPostMediaMapper.xml index cbf4b28..701bd69 100644 --- a/src/main/resources/mapper/mini/MiniUserPostMediaMapper.xml +++ b/src/main/resources/mapper/mini/MiniUserPostMediaMapper.xml @@ -5,5 +5,21 @@ + diff --git a/src/main/resources/mapper/mini/MiniUserPostViewMapper.xml b/src/main/resources/mapper/mini/MiniUserPostViewMapper.xml index faa1a23..2eab95b 100644 --- a/src/main/resources/mapper/mini/MiniUserPostViewMapper.xml +++ b/src/main/resources/mapper/mini/MiniUserPostViewMapper.xml @@ -5,5 +5,15 @@ +