25 changed files with 1514 additions and 8 deletions
@ -1,14 +1,41 @@ |
|||||
package com.youlai.boot.mini.mapper; |
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.youlai.boot.mini.model.entity.MiniUserPost; |
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
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 接口 |
* 用户作品表 Mapper 接口 |
||||
* |
* |
||||
* @author jwy |
* @author jwy |
||||
* @since |
* @since |
||||
*/ |
*/ |
||||
public interface MiniUserPostMapper extends BaseMapper<MiniUserPost> { |
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); |
||||
|
|
||||
} |
} |
||||
|
|||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -1,7 +1,53 @@ |
|||||
package com.youlai.boot.mini.service; |
package com.youlai.boot.mini.service; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
import com.baomidou.mybatisplus.extension.service.IService; |
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.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> { |
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); |
||||
} |
} |
||||
|
|||||
@ -1,15 +1,715 @@ |
|||||
package com.youlai.boot.mini.service.impl; |
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.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.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.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.mini.service.UserPostService; |
||||
|
import com.youlai.boot.system.mapper.UserMapper; |
||||
|
import com.youlai.boot.system.model.entity.SysUser; |
||||
import lombok.RequiredArgsConstructor; |
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
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.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 |
@Service |
||||
@RequiredArgsConstructor |
@RequiredArgsConstructor |
||||
@Slf4j |
|
||||
public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUserPost> implements UserPostService { |
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue