资讯动态

Web应用收藏功能设计与实现全解析

发布时间:2026/10/23 4:28:10 来源:尧图企业网站定制
1. 收藏管理模块概述收藏功能是现代Web应用中不可或缺的基础模块之一。无论是电商平台、内容社区还是SaaS系统都需要为用户提供便捷的内容收藏能力。这个看似简单的功能背后实际上涉及了数据结构设计、并发控制、缓存策略等多个技术难点。在实际项目中收藏管理模块通常需要处理以下核心需求用户与收藏项的多对多关系维护收藏内容的分类与标签管理收藏列表的分页与排序收藏状态的实时同步收藏数据的统计分析2. 核心数据结构设计2.1 基础表结构CREATE TABLE user_favorites ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, content_id bigint NOT NULL COMMENT 内容ID, content_type tinyint NOT NULL COMMENT 内容类型1-文章 2-商品 3-视频, collection_id bigint DEFAULT NULL COMMENT 收藏夹ID, tags varchar(255) DEFAULT NULL COMMENT 标签多个用逗号分隔, is_private tinyint DEFAULT 0 COMMENT 是否私有, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_content (user_id,content_id,content_type), KEY idx_collection (collection_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 收藏夹表设计CREATE TABLE collections ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, name varchar(50) NOT NULL, description varchar(255) DEFAULT NULL, cover_url varchar(255) DEFAULT NULL, item_count int DEFAULT 0, is_private tinyint DEFAULT 0, sort_order int DEFAULT 0, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3. 核心接口实现3.1 添加收藏接口PostMapping(/favorites) public Result addFavorite(RequestBody FavoriteAddDTO dto) { // 校验内容是否存在 ContentInfo content contentService.getById(dto.getContentId()); if (content null) { return Result.fail(内容不存在); } // 检查是否已收藏 if (favoriteService.exists(dto.getUserId(), dto.getContentId(), dto.getContentType())) { return Result.fail(请勿重复收藏); } // 构建收藏记录 UserFavorite favorite new UserFavorite(); favorite.setUserId(dto.getUserId()); favorite.setContentId(dto.getContentId()); favorite.setContentType(dto.getContentType()); favorite.setCollectionId(dto.getCollectionId()); favorite.setTags(dto.getTags()); favorite.setIsPrivate(dto.getIsPrivate()); // 使用分布式锁防止重复提交 String lockKey fav:lock: dto.getUserId() : dto.getContentId(); try { boolean locked redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS); if (!locked) { return Result.fail(操作太频繁请稍后再试); } // 保存收藏记录 boolean success favoriteService.save(favorite); if (success) { // 更新收藏夹计数 if (dto.getCollectionId() ! null) { collectionService.incrementCount(dto.getCollectionId()); } // 更新内容收藏数 contentService.incrementFavoriteCount(dto.getContentId()); // 发送收藏事件 eventPublisher.publishEvent(new FavoriteEvent(favorite, FavoriteEvent.ACTION_ADD)); } return success ? Result.success() : Result.fail(收藏失败); } finally { redisLock.unlock(lockKey); } }3.2 批量获取收藏状态接口GetMapping(/favorites/status) public Result getFavoriteStatus( RequestParam Long userId, RequestParam ListLong contentIds, RequestParam Integer contentType) { if (contentIds.size() 100) { return Result.fail(单次查询内容ID不能超过100个); } // 先查缓存 String cacheKey user_fav_status: userId : contentType; MapLong, Boolean cachedStatus redisService.hGetAll(cacheKey, contentIds); // 找出未命中的内容ID ListLong missIds contentIds.stream() .filter(id - !cachedStatus.containsKey(id)) .collect(Collectors.toList()); if (!missIds.isEmpty()) { // 批量查询数据库 MapLong, Boolean dbStatus favoriteService.batchCheckFavorite(userId, missIds, contentType); // 更新缓存 redisService.hSet(cacheKey, dbStatus, 1, TimeUnit.HOURS); // 合并结果 cachedStatus.putAll(dbStatus); } return Result.success(cachedStatus); }4. 性能优化策略4.1 多级缓存设计收藏模块的读远多于写适合采用多级缓存策略本地缓存使用Caffeine缓存用户最近访问的收藏列表有效期5分钟Redis缓存收藏状态hash结构存储用户对内容的收藏状态1小时过期收藏列表sorted set存储用户收藏ID按时间排序数据库使用覆盖索引优化查询对collection_id字段建立索引4.2 异步计数方案对于收藏数、收藏夹项目数等计数场景采用Redis计数异步落库方案// 计数服务 public void incrementCount(Long contentId) { // Redis原子递增 String key content:fav_count: contentId; redisTemplate.opsForValue().increment(key); // 发送MQ消息异步更新数据库 mqProducer.send(new CountUpdateMessage(content_favorite, contentId, 1)); } // 消费者端 RabbitListener(queues count_update_queue) public void handleCountUpdate(CountUpdateMessage message) { String key message.getBizType() : message.getBizId(); Long delta redisTemplate.opsForValue().get(key); if (delta ! null) { // 批量更新数据库 contentMapper.updateFavoriteCount(message.getBizId(), delta); // 清除Redis计数 redisTemplate.delete(key); } }5. 常见问题与解决方案5.1 收藏状态不一致问题现象用户取消收藏后界面仍显示已收藏状态解决方案采用先更新数据库再删除缓存策略增加重试机制确保缓存最终一致客户端增加本地状态管理优化用户体验Transactional public boolean cancelFavorite(Long userId, Long contentId, Integer contentType) { // 1. 删除数据库记录 boolean success remove(new QueryWrapperUserFavorite() .eq(user_id, userId) .eq(content_id, contentId) .eq(content_type, contentType)); if (success) { // 2. 删除缓存 String cacheKey user_fav_status: userId : contentType; redisTemplate.hDel(cacheKey, contentId.toString()); // 3. 发送取消收藏事件 eventPublisher.publishEvent(new FavoriteCancelEvent(userId, contentId, contentType)); } return success; }5.2 收藏列表加载慢问题优化方案采用游标分页代替传统分页对收藏列表进行预加载实现无限滚动加载public PageResultUserFavoriteVO queryFavorites(FavoriteQuery query) { // 第一页尝试从缓存获取 if (query.getPageNum() 1) { String cacheKey buildCacheKey(query); ListUserFavoriteVO cached redisService.get(cacheKey); if (cached ! null) { return new PageResult(query.getPageSize(), cached); } } // 数据库查询 PageUserFavorite page lambdaQuery() .eq(UserFavorite::getUserId, query.getUserId()) .eq(query.getCollectionId() ! null, UserFavorite::getCollectionId, query.getCollectionId()) .orderByDesc(UserFavorite::getId) .page(new Page(query.getPageNum(), query.getPageSize())); // 转换为VO ListUserFavoriteVO vos convertToVO(page.getRecords()); // 缓存第一页数据 if (query.getPageNum() 1) { redisService.set(cacheKey, vos, 5, TimeUnit.MINUTES); } return new PageResult(page.getTotal(), vos); }6. 扩展功能实现6.1 收藏内容推荐基于用户收藏历史实现相似内容推荐public ListContentRecommendVO recommendContents(Long userId, int count) { // 1. 获取用户收藏标签 ListString tags favoriteMapper.selectUserTags(userId); // 2. 基于标签匹配相似内容 return contentMapper.selectRecommendByTags(tags, count) .stream() .map(this::convertToRecommendVO) .collect(Collectors.toList()); }6.2 收藏夹分享功能PostMapping(/collections/{id}/share) public Result shareCollection( PathVariable Long id, RequestParam Long userId, RequestParam(required false) String password) { // 验证收藏夹存在且属于该用户 Collection collection collectionService.getById(id); if (collection null || !collection.getUserId().equals(userId)) { return Result.fail(收藏夹不存在); } // 生成分享令牌 String token UUID.randomUUID().toString(); // 保存分享记录 CollectionShare share new CollectionShare(); share.setCollectionId(id); share.setToken(token); share.setPassword(password); share.setExpireTime(LocalDateTime.now().plusDays(7)); share.setCreateTime(LocalDateTime.now()); collectionShareService.save(share); // 返回分享链接 String shareUrl String.format(https://example.com/collections/share?token%s, token); return Result.success(shareUrl); }7. 监控与统计7.1 收藏行为埋点设计Aspect Component public class FavoriteLogAspect { AfterReturning( pointcut execution(* com..favorite.service.*.*(..)) annotation(favoriteLog), returning result) public void afterFavoriteOperation(JoinPoint jp, FavoriteLog favoriteLog, Object result) { try { Object[] args jp.getArgs(); Long userId (Long) args[0]; Long contentId (Long) args[1]; // 发送埋点数据 MapString, Object metrics new HashMap(); metrics.put(userId, userId); metrics.put(contentId, contentId); metrics.put(action, favoriteLog.action()); metrics.put(success, result ! null); metrics.put(timestamp, System.currentTimeMillis()); kafkaTemplate.send(favorite_metrics, JSON.toJSONString(metrics)); } catch (Exception e) { log.error(收藏操作埋点异常, e); } } }7.2 热门收藏内容统计-- 每日热门收藏内容 SELECT content_id, content_type, COUNT(*) AS favorite_count FROM user_favorites WHERE created_at CURRENT_DATE GROUP BY content_id, content_type ORDER BY favorite_count DESC LIMIT 100;8. 安全防护措施8.1 防刷机制Slf4j Service RequiredArgsConstructor public class FavoriteLimitService { private final RedisTemplateString, String redisTemplate; public boolean checkLimit(Long userId) { String key fav:limit: userId; long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, 1, TimeUnit.HOURS); } if (count 100) { log.warn(用户{}收藏操作过于频繁, userId); return false; } return true; } }8.2 敏感词过滤public boolean checkContentSafety(String text) { if (StringUtils.isBlank(text)) { return true; } // 调用内容安全服务 ContentCheckResult result contentSecurityService.checkText(text); return result.isPass(); }9. 测试要点9.1 单元测试用例class FavoriteServiceTest { Test void testAddFavorite() { // 准备测试数据 Long userId 1L; Long contentId 100L; Integer contentType 1; // 模拟依赖 when(contentService.getById(contentId)).thenReturn(new ContentInfo()); when(favoriteService.exists(userId, contentId, contentType)).thenReturn(false); // 执行测试 FavoriteAddDTO dto new FavoriteAddDTO(userId, contentId, contentType); Result result favoriteController.addFavorite(dto); // 验证结果 assertTrue(result.isSuccess()); verify(favoriteService).save(any(UserFavorite.class)); } Test void testAddFavoriteDuplicate() { // 准备测试数据 Long userId 1L; Long contentId 100L; Integer contentType 1; // 模拟已存在 when(contentService.getById(contentId)).thenReturn(new ContentInfo()); when(favoriteService.exists(userId, contentId, contentType)).thenReturn(true); // 执行测试 FavoriteAddDTO dto new FavoriteAddDTO(userId, contentId, contentType); Result result favoriteController.addFavorite(dto); // 验证结果 assertFalse(result.isSuccess()); assertEquals(请勿重复收藏, result.getMessage()); } }9.2 性能测试方案基准测试单机QPS测试使用JMeter模拟100并发持续5分钟重点关注平均响应时间、错误率、资源占用稳定性测试24小时持续运行监控内存泄漏、连接池耗尽等问题异常测试模拟Redis宕机场景模拟数据库连接超时验证降级策略是否生效10. 部署与运维10.1 数据库分表策略随着用户量增长单表可能无法承载所有收藏记录需要设计分表方案public class FavoriteTableSharding implements PreciseShardingAlgorithmLong { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueLong shardingValue) { // 按用户ID分表每100万用户一个表 long userId shardingValue.getValue(); long tableSuffix userId / 1_000_000; return user_favorites_ tableSuffix; } }10.2 灰度发布方案按用户ID分段发布先开放10%的流量观察效果监控关键指标收藏成功率接口响应时间数据库负载11. 项目经验总结在实际开发收藏管理模块时有几个关键点需要特别注意并发控制收藏/取消收藏操作需要加分布式锁防止重复操作导致数据不一致缓存策略收藏状态这类读多写少的数据非常适合缓存但要处理好缓存一致性问题批量操作对于用户首页需要展示大量收藏状态的场景务必实现批量查询接口扩展性设计提前考虑分表方案避免后期数据量大了再重构监控报警对核心接口设置监控及时发现异常情况一个健壮的收藏管理系统应该具备以下特点响应快速核心接口平均RT50ms数据一致确保用户看到的收藏状态与实际一致扩展性强支持水平扩展应对用户增长容错性好部分依赖服务不可用时能降级处理

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑