Browse Source

增加用户关注接口

glx_phase2
glx 4 days ago
parent
commit
b21949693e
  1. 1
      src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java
  2. 8
      src/main/java/com/youlai/boot/common/constant/RedisConstants.java
  3. 54
      src/main/java/com/youlai/boot/mini/controller/MiniFollowController.java
  4. 28
      src/main/java/com/youlai/boot/mini/mapper/MiniUserFollowMapper.java
  5. 45
      src/main/java/com/youlai/boot/mini/model/entity/MiniUserFollow.java
  6. 3
      src/main/java/com/youlai/boot/mini/model/query/OwnUserPostQuery.java
  7. 31
      src/main/java/com/youlai/boot/mini/model/vo/FollowUserVO.java
  8. 3
      src/main/java/com/youlai/boot/mini/model/vo/UserPostVO.java
  9. 50
      src/main/java/com/youlai/boot/mini/service/MiniFollowService.java
  10. 183
      src/main/java/com/youlai/boot/mini/service/impl/MiniFollowServiceImpl.java
  11. 183
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  12. 46
      src/main/resources/mapper/mini/MiniUserFollowMapper.xml
  13. 8
      src/main/resources/mapper/mini/MiniUserPostMapper.xml

1
src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java

@ -125,6 +125,7 @@ public class MyBatisPlusGenerator {
,new TableConfig("mini_content_audit_config", IdType.AUTO, "admin")
,new TableConfig("mini_content_audit", IdType.AUTO, "admin")
,new TableConfig("mini_content_audit_task", IdType.AUTO, "admin")
,new TableConfig("mini_user_follow", IdType.AUTO, "mini")
// ,new TableConfig("mini_stray_animal", IdType.AUTO, "mini")

8
src/main/java/com/youlai/boot/common/constant/RedisConstants.java

@ -60,4 +60,12 @@ public interface RedisConstants {
String ROLE_PERMS = "system:role:perms"; // 系统角色和权限映射
}
/**
* 社交模块
*/
interface Social {
String FOLLOWING = "social:follow:following:{}"; // 用户关注列表(userId → Set<followedUserId>)
String FOLLOWERS = "social:follow:followers:{}"; // 用户粉丝列表(userId → Set<followerUserId>)
}
}

54
src/main/java/com/youlai/boot/mini/controller/MiniFollowController.java

@ -0,0 +1,54 @@
package com.youlai.boot.mini.controller;
import com.youlai.boot.common.base.BaseQuery;
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.vo.FollowUserVO;
import com.youlai.boot.mini.service.MiniFollowService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "用户关注相关接口")
@RestController
@RequestMapping("/api/v1/mini/follow")
@RequiredArgsConstructor
public class MiniFollowController {
private final MiniFollowService followService;
@Operation(summary = "关注/取关用户")
@PostMapping("/toggle")
public Result<Map<String, Object>> toggleFollow(@RequestParam String targetUserUuid) {
Long userId = SecurityUtils.getUserId();
boolean following = followService.toggleFollow(userId, targetUserUuid);
return Result.success(Map.of("following", following));
}
@Operation(summary = "检查是否已关注某用户")
@GetMapping("/isFollowing/{targetUserUuid}")
public Result<Map<String, Object>> isFollowing(@PathVariable String targetUserUuid) {
Long userId = SecurityUtils.getUserId();
boolean following = followService.isFollowingByUuid(userId, targetUserUuid);
return Result.success(Map.of("following", following));
}
@Operation(summary = "获取我的关注列表")
@GetMapping("/followingPage")
public PageResult<FollowUserVO> getFollowingPage(BaseQuery query) {
Long userId = SecurityUtils.getUserId();
return PageResult.success(followService.getFollowingPage(userId, query.getPageNum(), query.getPageSize()));
}
@Operation(summary = "获取我的粉丝列表")
@GetMapping("/followerPage")
public PageResult<FollowUserVO> getFollowerPage(BaseQuery query) {
Long userId = SecurityUtils.getUserId();
return PageResult.success(followService.getFollowerPage(userId, query.getPageNum(), query.getPageSize()));
}
}

28
src/main/java/com/youlai/boot/mini/mapper/MiniUserFollowMapper.java

@ -0,0 +1,28 @@
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.MiniUserFollow;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.mini.model.vo.FollowUserVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户关注表 Mapper 接口
*
* @author jwy
* @since
*/
public interface MiniUserFollowMapper extends BaseMapper<MiniUserFollow> {
List<Long> selectFollowingIds(@Param("userId") Long userId);
List<Long> selectFollowerIds(@Param("userId") Long userId);
IPage<FollowUserVO> selectFollowingPage(Page<FollowUserVO> page, @Param("userId") Long userId);
IPage<FollowUserVO> selectFollowerPage(Page<FollowUserVO> page, @Param("userId") Long userId);
}

45
src/main/java/com/youlai/boot/mini/model/entity/MiniUserFollow.java

@ -0,0 +1,45 @@
package com.youlai.boot.mini.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
@Getter
@Setter
@ToString
@Accessors(chain = true)
@TableName("mini_user_follow")
@Schema(description = "用户关注表")
public class MiniUserFollow implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
@Schema(description = "")
private Long id;
@TableField("follower_id")
@Schema(description = "关注者用户ID")
private Long followerId;
@TableField("followed_id")
@Schema(description = "被关注者用户ID")
private Long followedId;
@TableField("create_time")
@Schema(description = "关注时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@TableField("create_timestamp")
@Schema(description = "创建时间毫秒级时间戳")
private Long createTimestamp;
}

3
src/main/java/com/youlai/boot/mini/model/query/OwnUserPostQuery.java

@ -20,4 +20,7 @@ public class OwnUserPostQuery extends UserPostQuery {
@Schema(description = "权限", example = "public", hidden = true)
private String visibility;
@Schema(description = "允许的可见性列表(用于他人页面访问控制)", hidden = true)
private java.util.List<String> allowedVisibilities;
}

31
src/main/java/com/youlai/boot/mini/model/vo/FollowUserVO.java

@ -0,0 +1,31 @@
package com.youlai.boot.mini.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@Schema(description = "关注用户VO")
public class FollowUserVO {
@Hidden
@Schema(description = "用户ID", hidden = true)
private Long userId;
@Schema(description = "用户UUID")
private String userUuid;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "关注时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date followTime;
}

3
src/main/java/com/youlai/boot/mini/model/vo/UserPostVO.java

@ -12,6 +12,9 @@ public class UserPostVO {
@Schema(description = "作品ID", hidden = true)
private Long id;
@Schema(description = "作者用户ID", hidden = true)
private Long miniUserId;
@Schema(description = "作者uuid")
private String authorUuid;

50
src/main/java/com/youlai/boot/mini/service/MiniFollowService.java

@ -0,0 +1,50 @@
package com.youlai.boot.mini.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.boot.mini.model.vo.FollowUserVO;
import java.util.Set;
public interface MiniFollowService {
/**
* 关注/取关切换返回最新状态: true=已关注, false=已取关
*/
boolean toggleFollow(Long userId, String targetUserUuid);
/**
* 检查 userId 是否关注了 targetUserId
*/
boolean isFollowing(Long userId, Long targetUserId);
/**
* 检查 userId 是否关注了指定 UUID 的用户返回两个值following + targetUserId
*/
boolean isFollowingByUuid(Long userId, String targetUserUuid);
/**
* 获取用户关注的所有用户ID集合优先从Redis缓存读取
*/
Set<Long> getFollowingIds(Long userId);
/**
* 检查两人是否互相关注互为好友
*/
boolean isMutualFollow(Long userA, Long userB);
/**
* 从候选用户ID集合中筛选出与userId互相关注的用户ID
*/
Set<Long> filterMutualFollows(Long userId, Set<Long> candidateIds);
/**
* 分页获取关注列表
*/
IPage<FollowUserVO> getFollowingPage(Long userId, int pageNum, int pageSize);
/**
* 分页获取粉丝列表
*/
IPage<FollowUserVO> getFollowerPage(Long userId, int pageNum, int pageSize);
}

183
src/main/java/com/youlai/boot/mini/service/impl/MiniFollowServiceImpl.java

@ -0,0 +1,183 @@
package com.youlai.boot.mini.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.exception.MsgException;
import com.youlai.boot.mini.mapper.MiniUserFollowMapper;
import com.youlai.boot.mini.model.entity.MiniUserFollow;
import com.youlai.boot.mini.model.vo.FollowUserVO;
import com.youlai.boot.mini.service.MiniFollowService;
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.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class MiniFollowServiceImpl implements MiniFollowService {
private final MiniUserFollowMapper followMapper;
private final UserMapper userMapper;
private final StringRedisTemplate stringRedisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleFollow(Long userId, String targetUserUuid) {
SysUser targetUser = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUuid, targetUserUuid)
.eq(SysUser::getIsDeleted, 0));
if (targetUser == null) {
throw new MsgException("用户不存在");
}
Long targetUserId = targetUser.getId();
if (userId.equals(targetUserId)) {
throw new MsgException("不能关注自己");
}
Long exists = followMapper.selectCount(new LambdaQueryWrapper<MiniUserFollow>()
.eq(MiniUserFollow::getFollowerId, userId)
.eq(MiniUserFollow::getFollowedId, targetUserId));
boolean nowFollowing;
if (exists != null && exists > 0) {
followMapper.delete(new LambdaQueryWrapper<MiniUserFollow>()
.eq(MiniUserFollow::getFollowerId, userId)
.eq(MiniUserFollow::getFollowedId, targetUserId));
nowFollowing = false;
} else {
MiniUserFollow follow = new MiniUserFollow();
follow.setFollowerId(userId);
follow.setFollowedId(targetUserId);
follow.setCreateTime(new Date());
follow.setCreateTimestamp(System.currentTimeMillis());
followMapper.insert(follow);
nowFollowing = true;
}
updateRedisCache(userId, targetUserId, nowFollowing);
return nowFollowing;
}
@Override
public boolean isFollowing(Long userId, Long targetUserId) {
String key = RedisConstants.Social.FOLLOWING.replace("{}", String.valueOf(userId));
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, String.valueOf(targetUserId));
if (Boolean.TRUE.equals(isMember)) {
return true;
}
Long count = followMapper.selectCount(new LambdaQueryWrapper<MiniUserFollow>()
.eq(MiniUserFollow::getFollowerId, userId)
.eq(MiniUserFollow::getFollowedId, targetUserId));
return count != null && count > 0;
}
@Override
public boolean isFollowingByUuid(Long userId, String targetUserUuid) {
SysUser targetUser = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUuid, targetUserUuid)
.eq(SysUser::getIsDeleted, 0));
if (targetUser == null) {
throw new MsgException("用户不存在");
}
return isFollowing(userId, targetUser.getId());
}
@Override
public Set<Long> getFollowingIds(Long userId) {
String key = RedisConstants.Social.FOLLOWING.replace("{}", String.valueOf(userId));
Set<String> members = stringRedisTemplate.opsForSet().members(key);
if (CollUtil.isNotEmpty(members)) {
return members.stream().map(Long::valueOf).collect(Collectors.toSet());
}
// Cold start: load from DB and cache
List<Long> ids = followMapper.selectFollowingIds(userId);
if (CollUtil.isNotEmpty(ids)) {
String[] strIds = ids.stream().map(String::valueOf).toArray(String[]::new);
stringRedisTemplate.opsForSet().add(key, strIds);
}
return new HashSet<>(ids);
}
@Override
public boolean isMutualFollow(Long userA, Long userB) {
return isFollowing(userA, userB) && isFollowing(userB, userA);
}
@Override
public Set<Long> filterMutualFollows(Long userId, Set<Long> candidateIds) {
if (CollUtil.isEmpty(candidateIds)) {
return Collections.emptySet();
}
// I follow them
Set<Long> iFollow = getFollowingIds(userId);
if (CollUtil.isEmpty(iFollow)) {
return Collections.emptySet();
}
Set<Long> mutuals = new HashSet<>();
for (Long candidateId : candidateIds) {
if (iFollow.contains(candidateId)) {
String theirKey = RedisConstants.Social.FOLLOWING.replace("{}", String.valueOf(candidateId));
Boolean theyFollowMe = stringRedisTemplate.opsForSet()
.isMember(theirKey, String.valueOf(userId));
if (Boolean.TRUE.equals(theyFollowMe)) {
mutuals.add(candidateId);
} else {
// Redis miss for their set, fallback to DB
Long count = followMapper.selectCount(new LambdaQueryWrapper<MiniUserFollow>()
.eq(MiniUserFollow::getFollowerId, candidateId)
.eq(MiniUserFollow::getFollowedId, userId));
if (count != null && count > 0) {
mutuals.add(candidateId);
// warm their cache
refreshFollowingCache(candidateId);
}
}
}
}
return mutuals;
}
@Override
public IPage<FollowUserVO> getFollowingPage(Long userId, int pageNum, int pageSize) {
Page<FollowUserVO> page = new Page<>(pageNum, pageSize);
return followMapper.selectFollowingPage(page, userId);
}
@Override
public IPage<FollowUserVO> getFollowerPage(Long userId, int pageNum, int pageSize) {
Page<FollowUserVO> page = new Page<>(pageNum, pageSize);
return followMapper.selectFollowerPage(page, userId);
}
private void updateRedisCache(Long followerId, Long followedId, boolean follow) {
String followingKey = RedisConstants.Social.FOLLOWING.replace("{}", String.valueOf(followerId));
String followersKey = RedisConstants.Social.FOLLOWERS.replace("{}", String.valueOf(followedId));
String member = String.valueOf(followedId);
String followerMember = String.valueOf(followerId);
if (follow) {
stringRedisTemplate.opsForSet().add(followingKey, member);
stringRedisTemplate.opsForSet().add(followersKey, followerMember);
} else {
stringRedisTemplate.opsForSet().remove(followingKey, member);
stringRedisTemplate.opsForSet().remove(followersKey, followerMember);
}
}
private void refreshFollowingCache(Long userId) {
String key = RedisConstants.Social.FOLLOWING.replace("{}", String.valueOf(userId));
stringRedisTemplate.delete(key);
getFollowingIds(userId);
}
}

183
src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java

@ -47,6 +47,7 @@ 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.MiniFollowService;
import com.youlai.boot.mini.service.UserPostService;
import com.youlai.boot.system.mapper.UserMapper;
import com.youlai.boot.system.model.entity.SysUser;
@ -79,6 +80,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
private final MiniUserPostCollectMapper miniUserPostCollectMapper;
private final MiniUserPostViewMapper miniUserPostViewMapper;
private final UserMapper userMapper;
private final MiniFollowService followService;
private final RedissonClient redissonClient;
private final ContentAuditConfigService contentAuditConfigService;
private final AuditExecutorService auditExecutorService;
@ -684,6 +686,10 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
throw new MsgException("用户作品不存在");
}
if (!canViewDetail(post, userId)) {
throw new MsgException("该作品仅好友可见");
}
UserPostDetailsVO detailsVO = new UserPostDetailsVO();
Map<String, Object> param = new HashMap<>();
@ -750,65 +756,76 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
throw new MsgException("用户不存在");
}
queryParams.setMiniUserId(SecurityUtils.getUserId());
queryParams.setCreatorId(sysUser.getId());
Long viewerId = SecurityUtils.getUserId();
Long authorId = sysUser.getId();
queryParams.setMiniUserId(viewerId);
queryParams.setCreatorId(authorId);
if (!viewerId.equals(authorId)) {
List<String> allowed = new ArrayList<>();
allowed.add("public");
if (followService.isMutualFollow(viewerId, authorId)) {
allowed.add("friends");
}
queryParams.setAllowedVisibilities(allowed);
}
Page<UserPostVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
return baseMapper.getUserPostPage(page, queryParams);
}
/**
* 瀑布流cursor-based 分页 + 应用层可见性过滤
*
* 流程 pageSize × 3 布隆去重 可见性过滤 截取 pageSize
* 核心SQL 不做可见性过滤 Java 层用 Redis 批量判断好友关系后过滤
*
* @param query cursor=上一页最后一条的ID(首页不传), pageSize=每页条数
* @param userId 当前登录用户ID(可为null, 未登录只看public)
*/
@Override
public WaterfallResult<UserPostVO> getWaterfall(UserPostWaterfallQuery query, Long userId) {
int pageSize = query.getPageSize();
int querySize = pageSize + 1;
// 取3倍数据,因为后续布隆+可见性两次过滤会丢掉一部分
int fetchSize = pageSize * 3;
List<UserPostVO> originalList = baseMapper.getWaterfall(
query.getCursor(),
querySize,
userId
);
// 1. 从DB取原始数据(仅按id降序,不含可见性过滤)
List<UserPostVO> rawList = baseMapper.getWaterfall(query.getCursor(), fetchSize, userId);
Long nextCursor = null;
boolean isLastPage = true;
if (CollUtil.isNotEmpty(originalList)) {
if (originalList.size() > pageSize) {
nextCursor = originalList.get(pageSize - 1).getId();
isLastPage = false;
}
} else {
if (CollUtil.isEmpty(rawList)) {
return WaterfallResult.of(Collections.emptyList(), null, true);
}
List<UserPostVO> resultList = originalList.stream().limit(pageSize).collect(Collectors.toList());
// DB返回了满3倍数据,说明后面可能还有更多
boolean hasMore = rawList.size() >= fetchSize;
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);
}
// 2. 布隆过滤器去重:过滤当前用户已看过的内容
List<UserPostVO> afterBloom = applyBloomDedup(rawList, userId);
// 3. 可见性过滤:
// - 未登录 → 只看 public
// - 已登录 → public 全看,friends 需互相关注,private 仅作者本人
List<UserPostVO> afterVisibility = applyVisibilityFilter(afterBloom, userId);
// 4. 截取 pageSize 条作为本页结果
List<UserPostVO> resultList = afterVisibility.stream().limit(pageSize).toList();
// 5. 游标逻辑:
// - 过滤后还有 ≥ pageSize 条 → 用最后一条的ID作为下页游标
// - 过滤后不够,但DB原始数据是满的 → 用原始数据最后一条ID继续翻(被过滤的内容后面还会出现)
// - 否则最后一页
Long nextCursor = null;
boolean isLastPage = true;
if (resultList.size() >= pageSize) {
nextCursor = resultList.get(pageSize - 1).getId();
isLastPage = false;
} else if (hasMore) {
nextCursor = rawList.get(rawList.size() - 1).getId();
isLastPage = false;
}
// 没有封面的给默认图
if (CollUtil.isNotEmpty(resultList)) {
resultList.forEach(item -> {
if (StrUtil.isBlank(item.getFirstImageUrl())) {
@ -907,5 +924,85 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
}
}
/**
* 布隆过滤器去重过滤已浏览过的内容误判率可控默认0.1%误判最多重复展示一条零危害
* 布隆不存在时尝试从DB重建重建失败则降级不过滤
*/
private List<UserPostVO> applyBloomDedup(List<UserPostVO> list, Long userId) {
if (userId == null || CollUtil.isEmpty(list)) {
return list;
}
try {
String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId;
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(bloomKey);
if (bloomFilter.isExists()) {
return list.stream()
.filter(item -> !bloomFilter.contains(item.getId()))
.toList();
}
rebuildUserBloomFromDB(userId, 365);
RBloomFilter<Long> newBloom = redissonClient.getBloomFilter(bloomKey);
if (newBloom.isExists()) {
return list.stream()
.filter(item -> !newBloom.contains(item.getId()))
.toList();
}
} catch (Exception e) {
log.error("布隆操作异常,降级不过滤, userId:{}", userId, e);
}
return list;
}
/**
* 可见性过滤批量版收集所有作者ID 批量查Redis互关关系 逐条判断
* Redis操作数 = 1(SMEMBERS 我关注的人) + N(SISMEMBER 对方是否关注我)N=去重作者数
*/
private List<UserPostVO> applyVisibilityFilter(List<UserPostVO> list, Long viewerId) {
if (CollUtil.isEmpty(list)) {
return list;
}
if (viewerId == null) {
return list.stream()
.filter(item -> "public".equals(item.getVisibility()))
.toList();
}
Set<Long> authorIds = list.stream()
.map(UserPostVO::getMiniUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> mutualFollowIds = followService.filterMutualFollows(viewerId, authorIds);
return list.stream()
.filter(item -> canView(viewerId, item.getMiniUserId(), item.getVisibility(), mutualFollowIds))
.toList();
}
/**
* 单条可见性判断作者本人全可见 public全可见 friends需互相关注 private仅作者
* @param mutualFollowIds 与当前用户互相关注的作者ID集合调用前已批量算好
*/
private boolean canView(Long viewerId, Long authorId, String visibility, Set<Long> mutualFollowIds) {
if (authorId != null && viewerId.equals(authorId)) {
return true;
}
return switch (visibility != null ? visibility : "public") {
case "public" -> true;
case "friends" -> authorId != null && mutualFollowIds.contains(authorId);
default -> false;
};
}
/**
* 详情页可见性校验单条校验失败直接抛异常拦截越权访问
*/
private boolean canViewDetail(MiniUserPost post, Long viewerId) {
if (viewerId != null && viewerId.equals(post.getMiniUserId())) {
return true;
}
return switch (post.getVisibility()) {
case "public" -> true;
case "friends" -> viewerId != null && followService.isMutualFollow(viewerId, post.getMiniUserId());
default -> false;
};
}
}

46
src/main/resources/mapper/mini/MiniUserFollowMapper.xml

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.mini.mapper.MiniUserFollowMapper">
<select id="selectFollowingIds" resultType="java.lang.Long">
SELECT followed_id
FROM mini_user_follow
WHERE follower_id = #{userId}
</select>
<select id="selectFollowerIds" resultType="java.lang.Long">
SELECT follower_id
FROM mini_user_follow
WHERE followed_id = #{userId}
</select>
<select id="selectFollowingPage" resultType="com.youlai.boot.mini.model.vo.FollowUserVO">
SELECT
u.id AS userId,
u.uuid AS userUuid,
u.nickname,
u.avatar,
f.create_time AS followTime
FROM mini_user_follow f
INNER JOIN sys_user u ON f.followed_id = u.id AND u.is_deleted = 0
WHERE f.follower_id = #{userId}
ORDER BY f.create_time DESC
</select>
<select id="selectFollowerPage" resultType="com.youlai.boot.mini.model.vo.FollowUserVO">
SELECT
u.id AS userId,
u.uuid AS userUuid,
u.nickname,
u.avatar,
f.create_time AS followTime
FROM mini_user_follow f
INNER JOIN sys_user u ON f.follower_id = u.id AND u.is_deleted = 0
WHERE f.followed_id = #{userId}
ORDER BY f.create_time DESC
</select>
</mapper>

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

@ -20,6 +20,7 @@
)
SELECT
p.id,
p.mini_user_id AS miniUserId,
p.uuid AS postUuid,
p.title,
LEFT(p.content, 100) AS content,
@ -66,6 +67,12 @@
<if test="query.visibility != null and query.visibility != ''">
AND p.visibility = #{query.visibility}
</if>
<if test="query.allowedVisibilities != null and query.allowedVisibilities.size() > 0">
AND p.visibility IN
<foreach collection="query.allowedVisibilities" item="v" open="(" separator="," close=")">
#{v}
</foreach>
</if>
<if test="query.createStartTimestamp != null">
AND p.create_timestamp >= #{query.createStartTimestamp}
</if>
@ -142,6 +149,7 @@
)
SELECT
p.id,
p.mini_user_id AS miniUserId,
u.uuid AS authorUuid,
u.nickname AS authorName,
u.avatar AS authorAvatar,

Loading…
Cancel
Save