Browse Source

增加领域日记 动物笔记 可见性权限过滤

glx_phase2
glx 3 days ago
parent
commit
6bcc35ced8
  1. 5
      src/main/java/com/youlai/boot/mini/model/query/OwnAdoptionDiaryQuery.java
  2. 3
      src/main/java/com/youlai/boot/mini/model/query/OwnStrayAnimalQuery.java
  3. 3
      src/main/java/com/youlai/boot/mini/model/vo/AdoptionDiaryVO.java
  4. 3
      src/main/java/com/youlai/boot/mini/model/vo/StrayAnimalShortVO.java
  5. 176
      src/main/java/com/youlai/boot/mini/service/impl/AdoptionDiaryServiceImpl.java
  6. 8
      src/main/java/com/youlai/boot/mini/service/impl/MiniFollowServiceImpl.java
  7. 177
      src/main/java/com/youlai/boot/mini/service/impl/StrayAnimalServiceImpl.java
  8. 7
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  9. 7
      src/main/resources/mapper/mini/MiniAdoptionDiaryMapper.xml
  10. 10
      src/main/resources/mapper/mini/MiniStrayAnimalMapper.xml

5
src/main/java/com/youlai/boot/mini/model/query/OwnAdoptionDiaryQuery.java

@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
public class OwnAdoptionDiaryQuery extends AdoptionDiaryQuery{ public class OwnAdoptionDiaryQuery extends AdoptionDiaryQuery{
@ -20,4 +22,7 @@ public class OwnAdoptionDiaryQuery extends AdoptionDiaryQuery{
@Schema(description = "权限", example = "public", hidden = true) @Schema(description = "权限", example = "public", hidden = true)
private String visibility; private String visibility;
@Schema(description = "允许的可见性列表(用于他人页面访问控制)", hidden = true)
private List<String> allowedVisibilities;
} }

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

@ -15,4 +15,7 @@ public class OwnStrayAnimalQuery extends StrayAnimalQuery {
@Schema(description = "创建动物的用户ID", example = "1", hidden = true) @Schema(description = "创建动物的用户ID", example = "1", hidden = true)
private Long creatorId; private Long creatorId;
@Schema(description = "允许的可见性列表(用于他人页面访问控制)", hidden = true)
private java.util.List<String> allowedVisibilities;
} }

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

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

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

@ -10,6 +10,9 @@ public class StrayAnimalShortVO {
@Schema(description = "笔记ID(仅用于瀑布流游标,前端不需要展示)", hidden = true) @Schema(description = "笔记ID(仅用于瀑布流游标,前端不需要展示)", hidden = true)
private Long id; private Long id;
@Schema(description = "作者用户ID", hidden = true)
private Long miniUserId;
@Schema(description = "作者uuid", example = "true") @Schema(description = "作者uuid", example = "true")
private String authorUuid; private String authorUuid;

176
src/main/java/com/youlai/boot/mini/service/impl/AdoptionDiaryServiceImpl.java

@ -37,6 +37,7 @@ import com.youlai.boot.mini.model.query.AdoptionDiaryWaterfallQuery;
import com.youlai.boot.mini.model.query.OwnAdoptionDiaryQuery; import com.youlai.boot.mini.model.query.OwnAdoptionDiaryQuery;
import com.youlai.boot.mini.model.vo.*; import com.youlai.boot.mini.model.vo.*;
import com.youlai.boot.mini.service.AdoptionDiaryService; import com.youlai.boot.mini.service.AdoptionDiaryService;
import com.youlai.boot.mini.service.MiniFollowService;
import com.youlai.boot.mini.service.StrayAnimalService; import com.youlai.boot.mini.service.StrayAnimalService;
import com.youlai.boot.system.mapper.UserMapper; import com.youlai.boot.system.mapper.UserMapper;
import com.youlai.boot.system.model.entity.SysUser; import com.youlai.boot.system.model.entity.SysUser;
@ -76,6 +77,7 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe
private final AuditExecutorService auditExecutorService; private final AuditExecutorService auditExecutorService;
private final ContentAuditService contentAuditService; private final ContentAuditService contentAuditService;
private final ContentAuditTaskService contentAuditTaskService; private final ContentAuditTaskService contentAuditTaskService;
private final MiniFollowService followService;
// 布隆过滤器常量 // 布隆过滤器常量
private static final String BLOOM_VIEW_KEY_PREFIX = "mini:diary:view:bloom:"; private static final String BLOOM_VIEW_KEY_PREFIX = "mini:diary:view:bloom:";
@ -604,16 +606,32 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe
throw new MsgException("用户不存在"); throw new MsgException("用户不存在");
} }
queryParams.setMiniUserId(SecurityUtils.getUserId()); Long viewerId = SecurityUtils.getUserId();
queryParams.setCreatorId(sysUser.getId()); 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);
}
return getDiaryPage(queryParams); return getDiaryPage(queryParams);
} }
@Override @Override
public AdoptionDiaryDetailsVO getDetails(String diaryUuid, Long miniUserId) { public AdoptionDiaryDetailsVO getDetails(String diaryUuid, Long miniUserId) {
// 校验动物是否存在
MiniAdoptionDiary diary = getValidDiary(diaryUuid); MiniAdoptionDiary diary = getValidDiary(diaryUuid);
if (!canViewDetail(diary, miniUserId)) {
throw new MsgException("该日记仅好友可见");
}
AdoptionDiaryDetailsVO adoptionDiaryDetailsVO = new AdoptionDiaryDetailsVO(); AdoptionDiaryDetailsVO adoptionDiaryDetailsVO = new AdoptionDiaryDetailsVO();
if (diary != null) { if (diary != null) {
@ -800,66 +818,43 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe
@Override @Override
public WaterfallResult<AdoptionDiaryVO> getWaterfall(AdoptionDiaryWaterfallQuery query, Long userId) { public WaterfallResult<AdoptionDiaryVO> getWaterfall(AdoptionDiaryWaterfallQuery query, Long userId) {
// 1. 设置默认值,查 N+1 条
int pageSize = query.getPageSize(); int pageSize = query.getPageSize();
int querySize = pageSize + 1; // 取3倍数据,因为后续布隆+可见性两次过滤会丢掉一部分
int fetchSize = pageSize * 3;
// 2. 从DB查询 N+1 条 原始未过滤数据 // 1. 从DB取原始数据(仅按id降序,不含过滤)
List<AdoptionDiaryVO> originalList = baseMapper.getWaterfall( List<AdoptionDiaryVO> rawList = baseMapper.getWaterfall(query.getCursor(), fetchSize, userId);
query.getCursor(),
querySize,
userId
);
// 3. 先基于原始数据计算分页信息(完全不受过滤影响) if (CollUtil.isEmpty(rawList)) {
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); return WaterfallResult.of(Collections.emptyList(), null, true);
} }
// 4.布隆过滤已浏览内容 ========== // DB返回了满3倍数据,说明后面可能还有更多
List<AdoptionDiaryVO> resultList = originalList.stream().limit(pageSize).collect(Collectors.toList()); boolean hasMore = rawList.size() >= fetchSize;
// 仅登录用户做过滤
if (userId != null && CollUtil.isNotEmpty(resultList)) { // 2. 布隆过滤器去重:过滤当前用户已看过的内容
try { List<AdoptionDiaryVO> afterBloom = applyBloomDedup(rawList, userId);
String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId;
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(bloomKey); // 3. 可见性过滤
if (bloomFilter.isExists()) { List<AdoptionDiaryVO> afterVisibility = applyVisibilityFilter(afterBloom, userId);
// 布隆存在正常过滤
resultList = resultList.stream() // 4. 截取 pageSize 条作为本页结果
.filter(item -> !bloomFilter.contains(item.getId())) List<AdoptionDiaryVO> resultList = afterVisibility.stream().limit(pageSize).toList();
.collect(Collectors.toList());
} else { // 5. 游标逻辑
log.debug("用户{}布隆不存在,同步重建浏览记录", userId); Long nextCursor = null;
try { boolean isLastPage = true;
// 同步重建近1年的浏览记录,对接口响应影响极小 if (resultList.size() >= pageSize) {
rebuildUserBloomFromDB(userId, 365); nextCursor = resultList.get(pageSize - 1).getId();
// 重建完成后,立即用新布隆过滤当前页数据,用户第一页就生效 isLastPage = false;
RBloomFilter<Long> newBloomFilter = redissonClient.getBloomFilter(bloomKey); } else if (hasMore) {
if (newBloomFilter.isExists()) { nextCursor = CollUtil.isNotEmpty(resultList)
resultList = resultList.stream() ? resultList.get(resultList.size() - 1).getId()
.filter(item -> !newBloomFilter.contains(item.getId())) : rawList.get(rawList.size() - 1).getId();
.collect(Collectors.toList()); isLastPage = false;
}
log.debug("用户{}布隆同步重建完成,本次已过滤已浏览内容", userId);
} catch (Exception ex) {
// 重建失败跳过过滤,不影响主流程,下次访问自动重试
log.error("用户{}布隆同步重建失败,本次不过滤,下次访问自动重试", userId, ex);
}
}
} catch (Exception e) {
// Redis异常自动降级为不过滤,不影响用户正常使用,恢复后下次访问自动重建
log.error("布隆操作异常,自动降级不过滤,userId:{}, cursor:{}", userId, query.getCursor(), e);
}
} }
// 5. 设置默认图片(原有逻辑不变) // 6. 设置默认图片
if (CollUtil.isNotEmpty(resultList)) { if (CollUtil.isNotEmpty(resultList)) {
resultList.forEach(item -> { resultList.forEach(item -> {
if (StrUtil.isBlank(item.getFirstImageUrl())) { if (StrUtil.isBlank(item.getFirstImageUrl())) {
@ -871,6 +866,77 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe
return WaterfallResult.of(resultList, nextCursor, isLastPage); return WaterfallResult.of(resultList, nextCursor, isLastPage);
} }
private List<AdoptionDiaryVO> applyBloomDedup(List<AdoptionDiaryVO> 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();
}
log.debug("用户{}布隆不存在,同步重建浏览记录", userId);
try {
rebuildUserBloomFromDB(userId, 365);
RBloomFilter<Long> newBloomFilter = redissonClient.getBloomFilter(bloomKey);
if (newBloomFilter.isExists()) {
return list.stream()
.filter(item -> !newBloomFilter.contains(item.getId()))
.toList();
}
} catch (Exception ex) {
log.error("用户{}布隆同步重建失败,本次不过滤,下次访问自动重试", userId, ex);
}
} catch (Exception e) {
log.error("布隆操作异常,自动降级不过滤,userId:{}", userId, e);
}
return list;
}
private List<AdoptionDiaryVO> applyVisibilityFilter(List<AdoptionDiaryVO> 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(AdoptionDiaryVO::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();
}
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(MiniAdoptionDiary diary, Long viewerId) {
if (viewerId != null && viewerId.equals(diary.getMiniUserId())) {
return true;
}
return switch (diary.getVisibility()) {
case "public" -> true;
case "friends" -> viewerId != null && followService.isMutualFollow(viewerId, diary.getMiniUserId());
default -> false;
};
}
@Override @Override
public void rebuildUserBloomFromDB(Long userId) { public void rebuildUserBloomFromDB(Long userId) {
// days传0代表重建所有历史记录 // days传0代表重建所有历史记录

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

@ -99,7 +99,7 @@ public class MiniFollowServiceImpl implements MiniFollowService {
if (CollUtil.isNotEmpty(members)) { if (CollUtil.isNotEmpty(members)) {
return members.stream().map(Long::valueOf).collect(Collectors.toSet()); return members.stream().map(Long::valueOf).collect(Collectors.toSet());
} }
// Cold start: load from DB and cache // 从数据库和缓存加载
List<Long> ids = followMapper.selectFollowingIds(userId); List<Long> ids = followMapper.selectFollowingIds(userId);
if (CollUtil.isNotEmpty(ids)) { if (CollUtil.isNotEmpty(ids)) {
String[] strIds = ids.stream().map(String::valueOf).toArray(String[]::new); String[] strIds = ids.stream().map(String::valueOf).toArray(String[]::new);
@ -118,7 +118,7 @@ public class MiniFollowServiceImpl implements MiniFollowService {
if (CollUtil.isEmpty(candidateIds)) { if (CollUtil.isEmpty(candidateIds)) {
return Collections.emptySet(); return Collections.emptySet();
} }
// I follow them //
Set<Long> iFollow = getFollowingIds(userId); Set<Long> iFollow = getFollowingIds(userId);
if (CollUtil.isEmpty(iFollow)) { if (CollUtil.isEmpty(iFollow)) {
return Collections.emptySet(); return Collections.emptySet();
@ -133,13 +133,13 @@ public class MiniFollowServiceImpl implements MiniFollowService {
if (Boolean.TRUE.equals(theyFollowMe)) { if (Boolean.TRUE.equals(theyFollowMe)) {
mutuals.add(candidateId); mutuals.add(candidateId);
} else { } else {
// Redis miss for their set, fallback to DB // Redis没有,回退到DB判断
Long count = followMapper.selectCount(new LambdaQueryWrapper<MiniUserFollow>() Long count = followMapper.selectCount(new LambdaQueryWrapper<MiniUserFollow>()
.eq(MiniUserFollow::getFollowerId, candidateId) .eq(MiniUserFollow::getFollowerId, candidateId)
.eq(MiniUserFollow::getFollowedId, userId)); .eq(MiniUserFollow::getFollowedId, userId));
if (count != null && count > 0) { if (count != null && count > 0) {
mutuals.add(candidateId); mutuals.add(candidateId);
// warm their cache // 刷新缓存
refreshFollowingCache(candidateId); refreshFollowingCache(candidateId);
} }
} }

177
src/main/java/com/youlai/boot/mini/service/impl/StrayAnimalServiceImpl.java

@ -57,6 +57,7 @@ import com.youlai.boot.mini.model.query.OwnStrayAnimalQuery;
import com.youlai.boot.mini.model.query.WaterfallQuery; import com.youlai.boot.mini.model.query.WaterfallQuery;
import com.youlai.boot.mini.model.vo.*; import com.youlai.boot.mini.model.vo.*;
import com.youlai.boot.mini.service.MiniPointRecordService; import com.youlai.boot.mini.service.MiniPointRecordService;
import com.youlai.boot.mini.service.MiniFollowService;
import com.youlai.boot.mini.service.StrayAnimalService; import com.youlai.boot.mini.service.StrayAnimalService;
import com.youlai.boot.system.mapper.UserMapper; import com.youlai.boot.system.mapper.UserMapper;
import com.youlai.boot.system.model.entity.SysUser; import com.youlai.boot.system.model.entity.SysUser;
@ -129,6 +130,7 @@ public class StrayAnimalServiceImpl extends ServiceImpl<MiniStrayAnimalMapper, M
private final AuditExecutorService auditExecutorService; private final AuditExecutorService auditExecutorService;
private final ContentAuditService contentAuditService; private final ContentAuditService contentAuditService;
private final ContentAuditTaskService contentAuditTaskService; private final ContentAuditTaskService contentAuditTaskService;
private final MiniFollowService followService;
public String getDefaultCatCoverHost() { public String getDefaultCatCoverHost() {
return "https://" + bucketName + "." + endpoint; return "https://" + bucketName + "." + endpoint;
@ -802,6 +804,10 @@ public class StrayAnimalServiceImpl extends ServiceImpl<MiniStrayAnimalMapper, M
MiniStrayAnimalNote appStrayAnimalNote = miniStrayAnimalNoteMapper.selectOne(new LambdaQueryWrapper<MiniStrayAnimalNote>() MiniStrayAnimalNote appStrayAnimalNote = miniStrayAnimalNoteMapper.selectOne(new LambdaQueryWrapper<MiniStrayAnimalNote>()
.eq(MiniStrayAnimalNote::getStrayAnimalId, animal.getId())); .eq(MiniStrayAnimalNote::getStrayAnimalId, animal.getId()));
if (appStrayAnimalNote != null && !canViewDetail(appStrayAnimalNote, miniUserId)) {
throw new MsgException("该笔记仅好友可见");
}
if (appStrayAnimalNote != null) { if (appStrayAnimalNote != null) {
Map<String, Object> param = new HashMap<>(); Map<String, Object> param = new HashMap<>();
param.put("noteId", appStrayAnimalNote.getId()); param.put("noteId", appStrayAnimalNote.getId());
@ -876,8 +882,21 @@ public class StrayAnimalServiceImpl extends ServiceImpl<MiniStrayAnimalMapper, M
throw new MsgException("用户不存在"); throw new MsgException("用户不存在");
} }
queryParams.setMiniUserId(SecurityUtils.getUserId()); Long viewerId = SecurityUtils.getUserId();
queryParams.setCreatorId(sysUser.getId()); 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);
}
return getAnimalPage(queryParams); return getAnimalPage(queryParams);
} }
@ -1005,67 +1024,42 @@ public class StrayAnimalServiceImpl extends ServiceImpl<MiniStrayAnimalMapper, M
@Override @Override
public WaterfallResult<StrayAnimalShortVO> getWaterfall(WaterfallQuery query, Long userId) { public WaterfallResult<StrayAnimalShortVO> getWaterfall(WaterfallQuery query, Long userId) {
// 1. 设置默认值,查 N+1 条
int pageSize = query.getPageSize(); int pageSize = query.getPageSize();
int querySize = pageSize + 1; int fetchSize = pageSize * 3;
// 2. 从DB查询 N+1 条 原始未过滤数据
List<StrayAnimalShortVO> originalList = miniStrayAnimalMapper.getWaterfall(
query.getCursor(),
querySize,
query.getAnimalType(),
userId
);
// 3. 先基于原始数据计算分页信息(完全不受过滤影响) // 1. 从DB取原始数据(仅按id降序,不含过滤)
Long nextCursor = null; List<StrayAnimalShortVO> rawList = miniStrayAnimalMapper.getWaterfall(
boolean isLastPage = true; query.getCursor(), fetchSize, query.getAnimalType(), userId);
if (CollUtil.isNotEmpty(originalList)) {
if (originalList.size() > pageSize) { if (CollUtil.isEmpty(rawList)) {
nextCursor = originalList.get(pageSize - 1).getId();
isLastPage = false;
}
} else {
return WaterfallResult.of(Collections.emptyList(), null, true); return WaterfallResult.of(Collections.emptyList(), null, true);
} }
// 4.布隆过滤已浏览内容 ========== boolean hasMore = rawList.size() >= fetchSize;
List<StrayAnimalShortVO> resultList = originalList.stream().limit(pageSize).collect(Collectors.toList());
// 仅登录用户做过滤 // 2. 布隆过滤器去重
if (userId != null && CollUtil.isNotEmpty(resultList)) { List<StrayAnimalShortVO> afterBloom = applyBloomDedup(rawList, userId);
try {
String bloomKey = BLOOM_VIEW_KEY_PREFIX + userId; // 3. 可见性过滤
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(bloomKey); List<StrayAnimalShortVO> afterVisibility = applyVisibilityFilter(afterBloom, userId);
if (bloomFilter.isExists()) {
// 布隆存在正常过滤 // 4. 截取 pageSize 条作为本页结果
resultList = resultList.stream() List<StrayAnimalShortVO> resultList = afterVisibility.stream().limit(pageSize).toList();
.filter(item -> !bloomFilter.contains(item.getId()))
.collect(Collectors.toList()); // 5. 游标逻辑
} else { Long nextCursor = null;
log.debug("用户{}布隆不存在,同步重建浏览记录", userId); boolean isLastPage = true;
try { if (resultList.size() >= pageSize) {
// 同步重建近1年的浏览记录,对接口响应影响极小 nextCursor = resultList.get(pageSize - 1).getId();
rebuildUserBloomFromDB(userId, 365); isLastPage = false;
// 重建完成后,立即用新布隆过滤当前页数据,用户第一页就生效 } else if (hasMore) {
RBloomFilter<Long> newBloomFilter = redissonClient.getBloomFilter(bloomKey); nextCursor = CollUtil.isNotEmpty(resultList)
if (newBloomFilter.isExists()) { ? resultList.get(resultList.size() - 1).getId()
resultList = resultList.stream() : rawList.get(rawList.size() - 1).getId();
.filter(item -> !newBloomFilter.contains(item.getId())) isLastPage = false;
.collect(Collectors.toList());
}
log.debug("用户{}布隆同步重建完成,本次已过滤已浏览内容", userId);
} catch (Exception ex) {
// 重建失败跳过过滤,不影响主流程,下次访问自动重试
log.error("用户{}布隆同步重建失败,本次不过滤,下次访问自动重试", userId, ex);
}
}
} catch (Exception e) {
// Redis异常自动降级为不过滤,不影响用户正常使用,恢复后下次访问自动重建
log.error("布隆操作异常,自动降级不过滤,userId:{}, cursor:{}", userId, query.getCursor(), e);
}
} }
// 5. 设置默认图片(原有逻辑不变) // 6. 设置默认图片
if (CollUtil.isNotEmpty(resultList)) { if (CollUtil.isNotEmpty(resultList)) {
resultList.forEach(item -> { resultList.forEach(item -> {
if (StrUtil.isBlank(item.getFirstImageUrl())) { if (StrUtil.isBlank(item.getFirstImageUrl())) {
@ -1087,6 +1081,77 @@ public class StrayAnimalServiceImpl extends ServiceImpl<MiniStrayAnimalMapper, M
return WaterfallResult.of(resultList, nextCursor, isLastPage); return WaterfallResult.of(resultList, nextCursor, isLastPage);
} }
private List<StrayAnimalShortVO> applyBloomDedup(List<StrayAnimalShortVO> 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();
}
log.debug("用户{}布隆不存在,同步重建浏览记录", userId);
try {
rebuildUserBloomFromDB(userId, 365);
RBloomFilter<Long> newBloomFilter = redissonClient.getBloomFilter(bloomKey);
if (newBloomFilter.isExists()) {
return list.stream()
.filter(item -> !newBloomFilter.contains(item.getId()))
.toList();
}
} catch (Exception ex) {
log.error("用户{}布隆同步重建失败,本次不过滤,下次访问自动重试", userId, ex);
}
} catch (Exception e) {
log.error("布隆操作异常,自动降级不过滤,userId:{}", userId, e);
}
return list;
}
private List<StrayAnimalShortVO> applyVisibilityFilter(List<StrayAnimalShortVO> 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(StrayAnimalShortVO::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();
}
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(MiniStrayAnimalNote note, Long viewerId) {
if (viewerId != null && viewerId.equals(note.getMiniUserId())) {
return true;
}
return switch (note.getVisibility()) {
case "public" -> true;
case "friends" -> viewerId != null && followService.isMutualFollow(viewerId, note.getMiniUserId());
default -> false;
};
}
/** /**
* 重建用户的所有浏览记录布隆过滤器全量重建 * 重建用户的所有浏览记录布隆过滤器全量重建
* @param userId 用户ID * @param userId 用户ID

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

@ -813,7 +813,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
// 5. 游标逻辑: // 5. 游标逻辑:
// - 过滤后还有 ≥ pageSize 条 → 用最后一条的ID作为下页游标 // - 过滤后还有 ≥ pageSize 条 → 用最后一条的ID作为下页游标
// - 过滤后不够,但DB原始数据是满的 → 用原始数据最后一条ID继续翻(被过滤的内容后面还会出现 // - 过滤后不够,但DB原始数据是满的 → 用resultList最后一条ID(给被过滤帖子二次曝光机会
// - 否则最后一页 // - 否则最后一页
Long nextCursor = null; Long nextCursor = null;
boolean isLastPage = true; boolean isLastPage = true;
@ -821,7 +821,9 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
nextCursor = resultList.get(pageSize - 1).getId(); nextCursor = resultList.get(pageSize - 1).getId();
isLastPage = false; isLastPage = false;
} else if (hasMore) { } else if (hasMore) {
nextCursor = rawList.get(rawList.size() - 1).getId(); nextCursor = CollUtil.isNotEmpty(resultList)
? resultList.get(resultList.size() - 1).getId()
: rawList.get(rawList.size() - 1).getId();
isLastPage = false; isLastPage = false;
} }
@ -970,6 +972,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
.map(UserPostVO::getMiniUserId) .map(UserPostVO::getMiniUserId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// 获取互关用户
Set<Long> mutualFollowIds = followService.filterMutualFollows(viewerId, authorIds); Set<Long> mutualFollowIds = followService.filterMutualFollows(viewerId, authorIds);
return list.stream() return list.stream()
.filter(item -> canView(viewerId, item.getMiniUserId(), item.getVisibility(), mutualFollowIds)) .filter(item -> canView(viewerId, item.getMiniUserId(), item.getVisibility(), mutualFollowIds))

7
src/main/resources/mapper/mini/MiniAdoptionDiaryMapper.xml

@ -74,6 +74,12 @@
<if test="query.visibility != null and query.visibility != ''"> <if test="query.visibility != null and query.visibility != ''">
AND d.visibility = #{query.visibility} AND d.visibility = #{query.visibility}
</if> </if>
<if test="query.allowedVisibilities != null and query.allowedVisibilities.size() > 0">
AND d.visibility IN
<foreach collection="query.allowedVisibilities" item="v" open="(" separator="," close=")">
#{v}
</foreach>
</if>
<!-- 时间范围 --> <!-- 时间范围 -->
<if test="query.createStartTimestamp != null"> <if test="query.createStartTimestamp != null">
AND d.create_timestamp >= #{query.createStartTimestamp} AND d.create_timestamp >= #{query.createStartTimestamp}
@ -158,6 +164,7 @@
) )
SELECT SELECT
ad.id, ad.id,
ad.mini_user_id AS miniUserId,
u.uuid AS authorUuid, u.uuid AS authorUuid,
u.nickname AS authorName, u.nickname AS authorName,
u.avatar AS authorAvatar, u.avatar AS authorAvatar,

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

@ -139,6 +139,7 @@
a.uuid AS animalUuid, a.uuid AS animalUuid,
n.uuid AS animalNoteUuid, n.uuid AS animalNoteUuid,
n.mini_user_id AS miniUserId,
a.animal_type, a.animal_type,
@ -187,6 +188,14 @@
<!-- 指定用户 --> <!-- 指定用户 -->
AND n.mini_user_id = #{queryParams.creatorId} AND n.mini_user_id = #{queryParams.creatorId}
<!-- 可见性过滤 -->
<if test="queryParams.allowedVisibilities != null and queryParams.allowedVisibilities.size() > 0">
AND n.visibility IN
<foreach collection="queryParams.allowedVisibilities" item="v" open="(" separator="," close=")">
#{v}
</foreach>
</if>
<!-- 动物类型 --> <!-- 动物类型 -->
<if test="queryParams.animalType != null and queryParams.animalType != ''"> <if test="queryParams.animalType != null and queryParams.animalType != ''">
AND a.animal_type = #{queryParams.animalType} AND a.animal_type = #{queryParams.animalType}
@ -312,6 +321,7 @@
u.avatar AS authorAvatar, u.avatar AS authorAvatar,
a.uuid AS animalUuid, a.uuid AS animalUuid,
n.uuid AS animalNoteUuid, n.uuid AS animalNoteUuid,
n.mini_user_id AS miniUserId,
a.animal_type, a.animal_type,
fi.source_url AS firstImageUrl, fi.source_url AS firstImageUrl,
n.title, n.title,

Loading…
Cancel
Save