|
|
@ -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 |
|
|
|