Browse Source

增加用户作品模块接口

glx_phase2
glx 2 weeks ago
parent
commit
abbe200341
  1. 2
      .gitignore
  2. 173
      src/main/java/com/youlai/boot/mini/controller/UserPostController.java
  3. 8
      src/main/java/com/youlai/boot/mini/mapper/MiniUserPostCollectMapper.java
  4. 8
      src/main/java/com/youlai/boot/mini/mapper/MiniUserPostLikeMapper.java
  5. 29
      src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMapper.java
  6. 8
      src/main/java/com/youlai/boot/mini/mapper/MiniUserPostMediaMapper.java
  7. 7
      src/main/java/com/youlai/boot/mini/mapper/MiniUserPostViewMapper.java
  8. 20
      src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostDTO.java
  9. 24
      src/main/java/com/youlai/boot/mini/model/dto/DeleteUserPostMediaDTO.java
  10. 14
      src/main/java/com/youlai/boot/mini/model/form/UserPostCollectForm.java
  11. 31
      src/main/java/com/youlai/boot/mini/model/form/UserPostForm.java
  12. 14
      src/main/java/com/youlai/boot/mini/model/form/UserPostLikeForm.java
  13. 23
      src/main/java/com/youlai/boot/mini/model/query/OwnUserPostQuery.java
  14. 18
      src/main/java/com/youlai/boot/mini/model/query/UserPostQuery.java
  15. 13
      src/main/java/com/youlai/boot/mini/model/query/UserPostWaterfallQuery.java
  16. 17
      src/main/java/com/youlai/boot/mini/model/vo/UserPostDetailsVO.java
  17. 35
      src/main/java/com/youlai/boot/mini/model/vo/UserPostMediaVO.java
  18. 61
      src/main/java/com/youlai/boot/mini/model/vo/UserPostVO.java
  19. 46
      src/main/java/com/youlai/boot/mini/service/UserPostService.java
  20. 702
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  21. 30
      src/main/resources/mapper/mini/MiniUserPostCollectMapper.xml
  22. 30
      src/main/resources/mapper/mini/MiniUserPostLikeMapper.xml
  23. 183
      src/main/resources/mapper/mini/MiniUserPostMapper.xml
  24. 16
      src/main/resources/mapper/mini/MiniUserPostMediaMapper.xml
  25. 10
      src/main/resources/mapper/mini/MiniUserPostViewMapper.xml

2
.gitignore

@ -17,3 +17,5 @@ docker/*/data/
docker/minio/config
docker/xxljob/logs
application-youlai.yml
.claude
CLAUDE.md

173
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<List<String>> saveFile(
@RequestPart(name = "images", required = false) List<MultipartFile> images,
@RequestPart(name = "videos", required = false) List<MultipartFile> videos
) {
List<String> 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<String> 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<Void> 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<Void> 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<Void> saveMediaSource(
@PathVariable String postUuid,
@RequestPart(name = "images", required = false) List<MultipartFile> images,
@RequestPart(name = "videos", required = false) List<MultipartFile> 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<Void> 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<Void> delete(@Valid @RequestBody DeleteUserPostDTO dto) {
userPostService.delete(dto);
return Result.success();
}
@Operation(summary = "获取自己创建的作品列表")
@GetMapping(value = "/getSelfCreatedPage")
public PageResult<UserPostVO> getSelfCreatedPage(OwnUserPostQuery queryParams) {
return PageResult.success(userPostService.getSelfCreatedPage(queryParams));
}
@Operation(summary = "获取用户作品详情")
@RequestMapping(value = "/getDetails/{postUuid}", method = RequestMethod.GET)
public Result<UserPostDetailsVO> getDetails(@PathVariable String postUuid) {
return Result.success(userPostService.getDetails(postUuid));
}
@Operation(summary = "获取某个用户创建的作品列表")
@GetMapping(value = "/getOthersCreatedPage/{authorUuid}")
public PageResult<UserPostVO> 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<Map<String, Object>> toggleUserPostLike(
@Valid @RequestBody UserPostLikeForm form
) {
Long userId = SecurityUtils.getUserId();
Map<String, Object> 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<Map<String, Object>> toggleUserPostCollect(
@Valid @RequestBody UserPostCollectForm form
) {
Long userId = SecurityUtils.getUserId();
Map<String, Object> result = userPostService.toggleUserPostCollect(form, userId);
return Result.success(result);
}
@Operation(summary = "获取用户作品瀑布流")
@GetMapping(value = "/waterfall")
public Result<WaterfallResult<UserPostVO>> getWaterfall(
@Valid UserPostWaterfallQuery query
) {
Long userId = SecurityUtils.getUserId();
WaterfallResult<UserPostVO> 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();
}
}

8
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<MiniUserPostCollect> {
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);
}

8
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<MiniUserPostLike> {
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);
}

29
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<MiniUserPost> {
IPage<UserPostVO> getUserPostPage(Page<UserPostVO> 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<UserPostVO> getWaterfall(@Param("cursor") Long cursor, @Param("pageSize") int pageSize, @Param("miniUserId") Long miniUserId);
void incrementViewCount(String postUuid);
}

8
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<MiniUserPostMedia> {
List<UserPostMediaVO> getMediaByPostIdAndType(Map<String, Object> param);
}

7
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<MiniUserPostView> {
List<Long> selectViewedPostIdsByUserAndTime(@Param("userId") Long userId, @Param("startTime") Long startTime);
}

20
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;
}

24
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;
}

14
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;
}

31
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<String> mediaUrlList;
}

14
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;
}

23
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;
}

18
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;
}

13
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;
}

17
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<UserPostMediaVO> images;
@Schema(description = "视频信息", example = "[]")
private List<UserPostMediaVO> videos;
}

35
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;
}

61
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;
}

46
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<MiniUserPost> {
List<String> saveFile(List<MultipartFile> images, List<MultipartFile> videos);
String saveUserPost(UserPostForm formData);
void updateUserPost(String postUuid, UserPostForm formData);
void deleteMediaSource(DeleteUserPostMediaDTO dto);
void saveMediaSource(String postUuid, List<MultipartFile> images, List<MultipartFile> videos);
void updateVisibility(String postUuid, EditVisibilityDTO dto);
void delete(DeleteUserPostDTO dto);
IPage<UserPostVO> getSelfCreatedPage(OwnUserPostQuery queryParams);
UserPostDetailsVO getDetails(String postUuid);
IPage<UserPostVO> getOthersCreatedPage(String authorUuid, OwnUserPostQuery queryParams);
Map<String, Object> toggleUserPostLike(UserPostLikeForm form, Long userId);
Map<String, Object> toggleUserPostCollect(UserPostCollectForm form, Long userId);
WaterfallResult<UserPostVO> getWaterfall(UserPostWaterfallQuery query, Long userId);
void resetCurrentUserBloom(Long userId);
void increaseUserPostViewCount(String postUuid);
}

702
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<MiniUserPostMapper, MiniUserPost> 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<String> saveFile(List<MultipartFile> images, List<MultipartFile> videos) {
long currentTimestamp = System.currentTimeMillis();
List<String> 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<MiniUserPostMedia> 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<String> mediaUuidList = dto.getMediaUuidList();
// 校验作品是否存在
MiniUserPost post = lambdaQuery()
.eq(MiniUserPost::getUuid, dto.getPostUuid())
.eq(MiniUserPost::getDeleted, false)
.one();
if (post == null) {
throw new MsgException("用户作品不存在");
}
// 查询要删除的媒体资源
LambdaQueryWrapper<MiniUserPostMedia> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(MiniUserPostMedia::getUuid, mediaUuidList)
.eq(MiniUserPostMedia::getPostId, post.getId())
.eq(MiniUserPostMedia::getDeleted, false);
List<MiniUserPostMedia> mediaList = miniUserPostMediaMapper.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(mediaList)) {
// 删除阿里云OSS源文件
List<String> storageKeyList = mediaList.stream()
.map(MiniUserPostMedia::getStorageKey)
.toList();
aliyunFileService.deleteMultiFile(storageKeyList);
long currentTimestamp = System.currentTimeMillis();
// 逻辑删除数据库记录
miniUserPostMediaMapper.update(
null,
new LambdaUpdateWrapper<MiniUserPostMedia>()
.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<MultipartFile> images, List<MultipartFile> 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<MiniUserPost> 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<MiniUserPost> 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<String, Object> 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<String, Object> result = new HashMap<>();
result.put("isLiked", targetLike);
result.put("likeCount", likeCount != null ? likeCount : 0);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> 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<String, Object> result = new HashMap<>();
result.put("isCollected", targetCollect);
result.put("collectCount", collectCount != null ? collectCount : 0);
return result;
}
@Override
public IPage<UserPostVO> getSelfCreatedPage(OwnUserPostQuery queryParams) {
Long userId = SecurityUtils.getUserId();
queryParams.setMiniUserId(userId);
queryParams.setCreatorId(userId);
Page<UserPostVO> 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<String, Object> param = new HashMap<>();
param.put("postId", post.getId());
param.put("mediaType", AnimalNoteMediaTypeEnum.IMAGE.name().toLowerCase());
List<UserPostMediaVO> images = miniUserPostMediaMapper.getMediaByPostIdAndType(param);
if (CollectionUtils.isNotEmpty(images)) {
images.forEach(item -> item.setPostUuid(post.getUuid()));
}
param.put("mediaType", AnimalNoteMediaTypeEnum.VIDEO.name().toLowerCase());
List<UserPostMediaVO> 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<UserPostVO> getOthersCreatedPage(String authorUuid, OwnUserPostQuery queryParams) {
SysUser sysUser = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUuid, authorUuid)
.eq(SysUser::getIsDeleted, 0));
if (sysUser == null) {
throw new MsgException("用户不存在");
}
queryParams.setMiniUserId(SecurityUtils.getUserId());
queryParams.setCreatorId(sysUser.getId());
Page<UserPostVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
return baseMapper.getUserPostPage(page, queryParams);
}
@Override
public WaterfallResult<UserPostVO> getWaterfall(UserPostWaterfallQuery query, Long userId) {
int pageSize = query.getPageSize();
int querySize = pageSize + 1;
List<UserPostVO> 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<UserPostVO> resultList = originalList.stream().limit(pageSize).collect(Collectors.toList());
if (userId != null && CollUtil.isNotEmpty(resultList)) {
try {
String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId;
RBloomFilter<Long> 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<Long> 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<Long> viewedPostIds = miniUserPostViewMapper.selectViewedPostIdsByUserAndTime(userId, startTime);
if (CollUtil.isEmpty(viewedPostIds)) {
return;
}
String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId;
RBloomFilter<Long> 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<Long> 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);
}
}
}

30
src/main/resources/mapper/mini/MiniUserPostCollectMapper.xml

@ -5,5 +5,35 @@
<mapper namespace="com.youlai.boot.mini.mapper.MiniUserPostCollectMapper">
<select id="selectUserCollectCount" resultType="java.lang.Integer">
SELECT COUNT(1)
FROM mini_user_post_collect
WHERE post_id = #{postId}
AND mini_user_id = #{userId}
AND is_deleted = 0
</select>
<insert id="insertOrUpdateCollect" parameterType="com.youlai.boot.mini.model.entity.MiniUserPostCollect">
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)
</insert>
<update id="deleteCollect">
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
</update>
</mapper>

30
src/main/resources/mapper/mini/MiniUserPostLikeMapper.xml

@ -5,5 +5,35 @@
<mapper namespace="com.youlai.boot.mini.mapper.MiniUserPostLikeMapper">
<select id="selectUserLikeCount" resultType="java.lang.Integer">
SELECT COUNT(1)
FROM mini_user_post_like
WHERE post_id = #{postId}
AND mini_user_id = #{userId}
AND is_deleted = 0
</select>
<insert id="insertOrUpdateLike" parameterType="com.youlai.boot.mini.model.entity.MiniUserPostLike">
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)
</insert>
<update id="deleteLike">
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
</update>
</mapper>

183
src/main/resources/mapper/mini/MiniUserPostMapper.xml

@ -5,5 +5,188 @@
<mapper namespace="com.youlai.boot.mini.mapper.MiniUserPostMapper">
<select id="getUserPostPage" resultType="com.youlai.boot.mini.model.vo.UserPostVO" databaseId="mysql">
WITH first_image AS (
SELECT
m.post_id,
m.source_url,
ROW_NUMBER() OVER (
PARTITION BY m.post_id
ORDER BY m.id ASC
) AS rn
FROM mini_user_post_media m
WHERE m.media_type = 'image'
AND m.is_deleted = 0
)
SELECT
p.id,
p.uuid AS postUuid,
p.title,
LEFT(p.content, 100) AS content,
p.visibility,
p.view_count AS viewCount,
p.like_count AS likeCount,
p.comment_count AS commentCount,
p.collect_count AS collectCount,
p.create_time AS createTime,
u.uuid AS authorUuid,
u.nickname AS authorName,
u.avatar AS authorAvatar,
fi.source_url AS firstImageUrl,
<if test="query.miniUserId != null">
EXISTS (
SELECT 1
FROM mini_user_post_like l
WHERE l.post_id = p.id
AND l.mini_user_id = #{query.miniUserId}
AND l.is_deleted = 0
) AS isLiked,
EXISTS (
SELECT 1
FROM mini_user_post_collect c
WHERE c.post_id = p.id
AND c.mini_user_id = #{query.miniUserId}
AND c.is_deleted = 0
) AS isCollected,
</if>
<if test="query.miniUserId == null">
0 AS isLiked,
0 AS isCollected,
</if>
p.create_timestamp
FROM mini_user_post p
INNER JOIN sys_user u ON p.mini_user_id = u.id AND u.is_deleted = 0
LEFT JOIN first_image fi ON fi.post_id = p.id AND fi.rn = 1
<where>
p.is_deleted = 0
AND p.mini_user_id = #{query.creatorId}
<if test="query.keyword != null and query.keyword != ''">
AND (p.title LIKE CONCAT('%', #{query.keyword}, '%') OR p.content LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
<if test="query.visibility != null and query.visibility != ''">
AND p.visibility = #{query.visibility}
</if>
<if test="query.createStartTimestamp != null">
AND p.create_timestamp >= #{query.createStartTimestamp}
</if>
<if test="query.createEndTimestamp != null">
AND #{query.createEndTimestamp} >= p.create_timestamp
</if>
</where>
ORDER BY p.create_timestamp DESC
</select>
<select id="selectIdByUuid" resultType="java.lang.Long">
SELECT id
FROM mini_user_post
WHERE uuid = #{uuid}
AND is_deleted = 0
</select>
<update id="incrementLikeCount">
UPDATE mini_user_post
SET like_count = like_count + 1
WHERE id = #{postId}
AND is_deleted = 0
</update>
<update id="decrementLikeCount">
UPDATE mini_user_post
SET like_count = like_count - 1
WHERE id = #{postId}
AND is_deleted = 0
AND like_count > 0
</update>
<select id="selectLikeCount" resultType="java.lang.Long">
SELECT like_count
FROM mini_user_post
WHERE id = #{postId}
AND is_deleted = 0
</select>
<update id="incrementCollectCount">
UPDATE mini_user_post
SET collect_count = collect_count + 1
WHERE id = #{postId}
AND is_deleted = 0
</update>
<update id="decrementCollectCount">
UPDATE mini_user_post
SET collect_count = collect_count - 1
WHERE id = #{postId}
AND is_deleted = 0
AND collect_count > 0
</update>
<select id="selectCollectCount" resultType="java.lang.Long">
SELECT collect_count
FROM mini_user_post
WHERE id = #{postId}
AND is_deleted = 0
</select>
<select id="getWaterfall" resultType="com.youlai.boot.mini.model.vo.UserPostVO" databaseId="mysql">
WITH first_image AS (
SELECT
m.post_id,
m.source_url,
ROW_NUMBER() OVER (
PARTITION BY m.post_id
ORDER BY m.id ASC
) AS rn
FROM mini_user_post_media m
WHERE m.media_type = 'image'
AND m.is_deleted = 0
)
SELECT
p.id,
u.uuid AS authorUuid,
u.nickname AS authorName,
u.avatar AS authorAvatar,
p.uuid AS postUuid,
fi.source_url AS firstImageUrl,
p.title,
p.content,
p.visibility,
p.view_count AS viewCount,
p.like_count AS likeCount,
p.comment_count AS commentCount,
<if test="miniUserId != null">
EXISTS (
SELECT 1
FROM mini_user_post_like l
WHERE l.post_id = p.id
AND l.mini_user_id = #{miniUserId}
AND l.is_deleted = 0
) AS isLiked,
EXISTS (
SELECT 1
FROM mini_user_post_collect c
WHERE c.post_id = p.id
AND c.mini_user_id = #{miniUserId}
AND c.is_deleted = 0
) AS isCollected,
</if>
p.create_time AS createTime,
p.collect_count AS collectCount
FROM mini_user_post p
INNER JOIN sys_user u ON p.mini_user_id = u.id
LEFT JOIN first_image fi ON fi.post_id = p.id AND fi.rn = 1
WHERE p.is_deleted = 0
<if test="cursor != null">
AND p.id &lt; #{cursor}
</if>
ORDER BY p.id DESC
LIMIT #{pageSize}
</select>
<update id="incrementViewCount">
UPDATE mini_user_post
SET view_count = view_count + 1
WHERE uuid = #{postUuid}
AND is_deleted = 0
</update>
</mapper>

16
src/main/resources/mapper/mini/MiniUserPostMediaMapper.xml

@ -5,5 +5,21 @@
<mapper namespace="com.youlai.boot.mini.mapper.MiniUserPostMediaMapper">
<select id="getMediaByPostIdAndType" resultType="com.youlai.boot.mini.model.vo.UserPostMediaVO">
SELECT
uuid AS postMediaUuid,
media_type,
source_url,
storage_key,
thumbnail_url,
width,
height,
duration
FROM
mini_user_post_media
WHERE
post_id = #{postId} AND media_type = #{mediaType} AND is_deleted = 0
ORDER BY id desc
</select>
</mapper>

10
src/main/resources/mapper/mini/MiniUserPostViewMapper.xml

@ -5,5 +5,15 @@
<mapper namespace="com.youlai.boot.mini.mapper.MiniUserPostViewMapper">
<select id="selectViewedPostIdsByUserAndTime" resultType="java.lang.Long">
SELECT post_id
FROM mini_user_post_view
WHERE mini_user_id = #{userId}
<if test="startTime != null">
AND create_timestamp >= #{startTime}
</if>
AND is_deleted = 0
ORDER BY create_timestamp DESC
</select>
</mapper>

Loading…
Cancel
Save