|
|
|
@ -15,17 +15,16 @@ 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.MiniAdoptionDiaryLikeMapper; |
|
|
|
import com.youlai.boot.mini.mapper.MiniAdoptionDiaryMapper; |
|
|
|
import com.youlai.boot.mini.mapper.MiniAdoptionDiaryMediaMapper; |
|
|
|
import com.youlai.boot.mini.mapper.MiniAdoptionDiaryViewMapper; |
|
|
|
import com.youlai.boot.mini.mapper.*; |
|
|
|
import com.youlai.boot.mini.model.dto.DeleteAdoptionDiaryDTO; |
|
|
|
import com.youlai.boot.mini.model.dto.DeleteAdoptionDiaryMediaDTO; |
|
|
|
import com.youlai.boot.mini.model.dto.EditVisibilityDTO; |
|
|
|
import com.youlai.boot.mini.model.entity.*; |
|
|
|
import com.youlai.boot.mini.model.enums.AnimalNoteMediaTypeEnum; |
|
|
|
import com.youlai.boot.mini.model.form.AdoptionDiaryForm; |
|
|
|
import com.youlai.boot.mini.model.form.DiaryCollectForm; |
|
|
|
import com.youlai.boot.mini.model.form.DiaryLikeForm; |
|
|
|
import com.youlai.boot.mini.model.query.AdoptionDiaryWaterfallQuery; |
|
|
|
import com.youlai.boot.mini.model.query.OwnAdoptionDiaryQuery; |
|
|
|
import com.youlai.boot.mini.model.vo.*; |
|
|
|
import com.youlai.boot.mini.service.AdoptionDiaryService; |
|
|
|
@ -35,6 +34,7 @@ import lombok.RequiredArgsConstructor; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.apache.commons.io.FilenameUtils; |
|
|
|
import org.redisson.api.RBloomFilter; |
|
|
|
import org.redisson.api.RLock; |
|
|
|
import org.redisson.api.RedissonClient; |
|
|
|
import org.springframework.beans.factory.annotation.Value; |
|
|
|
import org.springframework.dao.DuplicateKeyException; |
|
|
|
@ -47,6 +47,7 @@ import java.awt.image.BufferedImage; |
|
|
|
import java.io.File; |
|
|
|
import java.util.*; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
@Slf4j |
|
|
|
@Service |
|
|
|
@ -59,6 +60,7 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe |
|
|
|
private final MiniAdoptionDiaryViewMapper miniAdoptionDiaryViewMapper; |
|
|
|
private final RedissonClient redissonClient; |
|
|
|
private final MiniAdoptionDiaryLikeMapper miniAdoptionDiaryLikeMapper; |
|
|
|
private final MiniAdoptionDiaryCollectMapper miniAdoptionDiaryCollectMapper; |
|
|
|
|
|
|
|
// 布隆过滤器常量
|
|
|
|
private static final String BLOOM_VIEW_KEY_PREFIX = "mini:diary:view:bloom:"; |
|
|
|
@ -412,7 +414,7 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe |
|
|
|
// 处理默认封面
|
|
|
|
if (result.getTotal() > 0) { |
|
|
|
result.getRecords().forEach(item -> { |
|
|
|
if (cn.hutool.core.util.StrUtil.isBlank(item.getFirstImageUrl())) { |
|
|
|
if (StrUtil.isBlank(item.getFirstImageUrl())) { |
|
|
|
// 领养日记默认封面图
|
|
|
|
item.setFirstImageUrl(getDefaultCoverHost() + "/default_diary.png"); |
|
|
|
} |
|
|
|
@ -573,4 +575,225 @@ public class AdoptionDiaryServiceImpl extends ServiceImpl<MiniAdoptionDiaryMappe |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public Map<String, Object> toggleDiaryCollect(DiaryCollectForm form, Long userId) { |
|
|
|
Long diaryId = baseMapper.selectIdByUuid(form.getDiaryUuid()); |
|
|
|
if (diaryId == null) { |
|
|
|
throw new MsgException("笔记不存在或已删除"); |
|
|
|
} |
|
|
|
|
|
|
|
long currentTime = System.currentTimeMillis(); |
|
|
|
Boolean currentCollected; |
|
|
|
|
|
|
|
Integer count = miniAdoptionDiaryCollectMapper.selectUserCollectCount(diaryId, userId); |
|
|
|
if (count != null && count > 0) { |
|
|
|
currentCollected = true; |
|
|
|
} else { |
|
|
|
currentCollected = false; |
|
|
|
} |
|
|
|
|
|
|
|
boolean targetCollect = !currentCollected; |
|
|
|
|
|
|
|
if (targetCollect) { |
|
|
|
if (!Boolean.TRUE.equals(currentCollected)) { |
|
|
|
MiniAdoptionDiaryCollect collect = new MiniAdoptionDiaryCollect(); |
|
|
|
collect.setUuid(IdWorker.get32UUID()); |
|
|
|
collect.setDiaryId(diaryId); |
|
|
|
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); |
|
|
|
miniAdoptionDiaryCollectMapper.insertOrUpdateCollect(collect); |
|
|
|
|
|
|
|
baseMapper.incrementCollectCount(diaryId); |
|
|
|
} |
|
|
|
} else { |
|
|
|
if (Boolean.TRUE.equals(currentCollected)) { |
|
|
|
miniAdoptionDiaryCollectMapper.deleteCollect(diaryId, userId, currentTime); |
|
|
|
|
|
|
|
baseMapper.decrementCollectCount(diaryId); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Long collectCount = baseMapper.selectCollectCount(diaryId); |
|
|
|
Map<String, Object> result = new HashMap<>(); |
|
|
|
result.put("isCollected", targetCollect); |
|
|
|
result.put("collectCount", collectCount != null ? collectCount : 0); |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public WaterfallResult<AdoptionDiaryVO> getWaterfall(AdoptionDiaryWaterfallQuery query, Long userId) { |
|
|
|
// 1. 设置默认值,查 N+1 条
|
|
|
|
int pageSize = query.getPageSize(); |
|
|
|
int querySize = pageSize + 1; |
|
|
|
|
|
|
|
// 2. 从DB查询 N+1 条 原始未过滤数据
|
|
|
|
List<AdoptionDiaryVO> originalList = baseMapper.getWaterfall( |
|
|
|
query.getCursor(), |
|
|
|
querySize, |
|
|
|
userId |
|
|
|
); |
|
|
|
|
|
|
|
// 3. 先基于原始数据计算分页信息(完全不受过滤影响)
|
|
|
|
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); |
|
|
|
} |
|
|
|
|
|
|
|
// 4.布隆过滤已浏览内容 ==========
|
|
|
|
List<AdoptionDiaryVO> 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 { |
|
|
|
// 同步重建近1年的浏览记录,对接口响应影响极小
|
|
|
|
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) { |
|
|
|
// Redis异常自动降级为不过滤,不影响用户正常使用,恢复后下次访问自动重建
|
|
|
|
log.error("布隆操作异常,自动降级不过滤,userId:{}, cursor:{}", userId, query.getCursor(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 5. 设置默认图片(原有逻辑不变)
|
|
|
|
if (CollUtil.isNotEmpty(resultList)) { |
|
|
|
resultList.forEach(item -> { |
|
|
|
if (StrUtil.isBlank(item.getFirstImageUrl())) { |
|
|
|
item.setFirstImageUrl(getDefaultCoverHost() + "/default_diary.png"); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
return WaterfallResult.of(resultList, nextCursor, isLastPage); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void rebuildUserBloomFromDB(Long userId) { |
|
|
|
// days传0代表重建所有历史记录
|
|
|
|
rebuildUserBloomFromDB(userId, 0); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
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; |
|
|
|
} |
|
|
|
// startTime为null时查询用户所有历史浏览记录,否则只查指定天数内的
|
|
|
|
List<Long> viewedDiaryIds = miniAdoptionDiaryViewMapper.selectViewedDiaryIdsByUserAndTime(userId, startTime); |
|
|
|
|
|
|
|
if (CollUtil.isEmpty(viewedDiaryIds)) { |
|
|
|
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); |
|
|
|
// 只有配置了过期时间且大于0时才设置,否则永久有效
|
|
|
|
if (bloomExpireDays != null && bloomExpireDays > 0) { |
|
|
|
bloomFilter.expire(bloomExpireDays, TimeUnit.DAYS); |
|
|
|
} |
|
|
|
|
|
|
|
// 批量写入布隆,1000条只需要几毫秒
|
|
|
|
for (Long noteId : viewedDiaryIds) { |
|
|
|
bloomFilter.add(noteId); |
|
|
|
} |
|
|
|
|
|
|
|
log.info("用户{}的浏览布隆重建完成,共写入{}条记录", userId, viewedDiaryIds.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); |
|
|
|
// 1. 先删除旧布隆
|
|
|
|
if (bloomFilter.isExists()) { |
|
|
|
bloomFilter.delete(); |
|
|
|
} |
|
|
|
// 2. 初始化一个空的新布隆,不加载历史浏览记录
|
|
|
|
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 increaseDiaryViewCount(String diaryUuid) { |
|
|
|
try { |
|
|
|
baseMapper.increaseDiaryViewCount(diaryUuid); |
|
|
|
} catch (Exception e) { |
|
|
|
// 浏览量增加失败不影响主流程,仅打日志
|
|
|
|
log.error("增加领养日记{}浏览量失败", diaryUuid, e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|