仿小红书类社区系统图文、短视频、多图、话题、搜索、收藏、浏览记录、发现页、关注页、附近页,看起来是很多功能,其实大部分都围绕同一个对象:内容。
一开始把图文、视频、话题、互动各做一套,第一版确实快,后面接搜索、推荐和多端数据时就会越来越难维护。所以这类社区源码,我更关注的是一条完整链路:内容怎么存、媒体怎么挂、审核什么时候做、搜索索引什么时候写,以及发现页怎么连续往下刷。

一、内容表先统一,图文和短视频别各玩各的
很多项目第一版只有图文,表名直接叫 article。后面加短视频,又补一张 video。
功能少的时候没问题,一旦点赞、评论、收藏、浏览记录都接进来,麻烦就开始了。
比如收藏到底是:
article_collect
还是:
video_collect
评论表是不是也要分两套?
搜索的时候图文和视频又要分别查,最后再合并排序。
所以如果项目从一开始就确定图文、短视频都会有,直接抽一张内容主表更省事。
javascript
CREATE TABLE community_content (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
content_type TINYINT NOT NULL COMMENT '1图文 2短视频',
title VARCHAR(200),
content TEXT,
cover_url VARCHAR(500),
status TINYINT DEFAULT 0 COMMENT '0待审核 1正常 2驳回',
like_count INT DEFAULT 0,
comment_count INT DEFAULT 0,
collect_count INT DEFAULT 0,
view_count INT DEFAULT 0,
publish_time DATETIME,
create_time DATETIME
);
多图不要继续往这张表里塞 image1、image2、image3。
单独做媒体表就行,保存 content_id、media_url、media_type、width、height、sort。
这里 width 和 height 我觉得比很多人想象中更重要。
双列瀑布流如果只拿到图片 URL,前端要等图片加载完才知道高度,页面刚打开时经常会出现卡片往下跳。后端直接把原图宽高一起返回,前端在图片加载前就能把卡片高度算出来,首屏会稳定很多。
短视频也可以继续挂在媒体表,只是多几个字段,例如视频封面、时长。
话题标签也是类似做法,内容表里不要直接存一串 #摄影#旅行#露营,最好还是内容表、话题表、中间关系表分开。
这一步前面多写一点代码,后面做话题页、搜索和推荐时反而省事。

二、多图发布这块,Java 服务尽量别负责"搬文件"
社区项目的上传量一般不小。
一条普通图文可能 6 张图,短视频更不用说。如果客户端先把文件传给 Java 服务,Java 再转到腾讯云对象存储,应用服务器会白白吃掉不少带宽和内存。
业务量小的时候看不出来,图片大一点、并发一上来,这种中转就比较浪费。
更常用的方式是先拿上传凭证,客户端直接传对象存储。
流程其实不复杂:
用户选图或者视频,先请求临时上传凭证,上传腾讯云 COS,拿到文件地址后,再提交发布接口。
真正的发布接口只接这种业务数据:
javascript
{
"contentType": 1,
"content": "整理了一组夜景照片",
"topicIds": [18, 27],
"mediaList": [
{
"url": "/2026/08/a1.jpg",
"width": 1080,
"height": 1440
},
{
"url": "/2026/08/a2.jpg",
"width": 1080,
"height": 1350
}
]
}
后台拿到以后,先写内容主表,再批量写媒体关系。
草稿也不用另外搞一套发布流程。
内容表增加一个草稿状态就够了,保存草稿时不进搜索、不进推荐;真正点击发布以后再切到待审核。
这样编辑草稿、继续上传图片、删除某张图片,都是围绕同一个 content_id 做。
还有一个细节,上传成功不等于内容就能公开。
对象存储里有文件,只代表文件已经上传了,跟内容状态不是一回事。

三、审核、搜索、推荐这三个顺序别反了
用户提交内容以后,我比较建议先落库,再做审核。
状态先设为"待审核",腾讯云内容审核通过以后,再把内容改成正常。
搜索索引也放在这个时间点之后。
否则会出现一种很尴尬的情况:MySQL 里的内容还是待审核,Elasticsearch 已经能搜到了。
发现页如果又直接从推荐池读,也可能把还没审核完的内容放出去。
所以顺序最好固定:
发布成功 → 待审核 → 审核通过 → 更新内容状态 → 写搜索索引 → 进入发现页或推荐池。
审核接口本身不一定要卡着发布请求同步执行。
短视频审核时间有时候会长一些,用户没必要一直等着接口返回。
发布接口先返回"审核中",后台再异步跑审核任务就可以。
项目里已经用了 Quartz 的话,前期甚至不一定马上引入 MQ,可以先做一张审核任务表,定时扫描没有处理完的记录。
后面量大了再把审核、索引同步拆成消息任务。
这个调整对前端基本没影响,但后台会舒服很多。

四、搜索做到一定量以后,LIKE 基本还是要换
内容只有几千条的时候,MySQL LIKE '%关键词%' 确实够用。
很多项目第一次做搜索也都是这样。
问题是数据继续增加以后,除了速度,中文搜索本身也不太好处理。
比如用户搜索"城市夜景",他可能希望搜到标题里只有"夜景摄影"的内容,也可能希望搜到话题里带"街拍"的内容。
这时候就不是简单 LIKE 能解决的了。
EasyES 可以放在这里。
内容审核通过以后,把需要搜索的字段同步过去,例如标题、正文、内容类型、话题、发布时间。
javascript
LambdaEsQueryWrapper<ContentDocument> wrapper =
new LambdaEsQueryWrapper<>();
wrapper.multiMatch(
keyword,
ContentDocument::getTitle,
ContentDocument::getContent
);
wrapper.eq(ContentDocument::getStatus, 1);
wrapper.orderByDesc(ContentDocument::getPublishTime);
List<ContentDocument> list = contentMapper.selectList(wrapper);
但 Elasticsearch 不要反过来当主数据库用。
点赞状态、收藏状态、内容是否下架,这些还是业务库说了算。
搜索服务主要负责一件事:根据关键词快速找到对应内容。
后面哪怕索引损坏或者需要重建,也能从 MySQL 重新生成。
热门搜索则没必要每次打开页面都去日志表 GROUP BY。
关键词搜索次数直接累加到 Redis,用一个有序集合做排名就够用了。定时再把需要长期保存的数据回库。

五、双列瀑布流表面是前端问题,实际坑经常在分页
瀑布流 UI 本身不算难。
左边一列、右边一列,根据当前高度把下一张卡片放到短的一边,再配合前面保存的图片宽高,基本就能做出来。
真正容易踩坑的是后台分页。
普通后台管理页面习惯用 page=1、page=2,最后变成 LIMIT offset,size。
放在社区发现页里就不太合适。
用户一直往下刷,offset 会越来越大。更麻烦的是推荐排序本身会变。
一条内容刚开始排在第 50,突然多了不少点赞,权重升到第 20。
用户刚好在这个过程中加载下一页,就可能遇到重复内容。
所以发现页一般更适合游标方式。
第一次拿 20 条,后台把最后一条记录的排序值作为 nextCursor 返回。
下一次客户端带着这个 cursor 继续请求,不再关心"这是第几页"。
排序可以用推荐分、发布时间、内容 ID 一起做。
例如:
推荐分倒序,发布时间倒序,ID 倒序。
这里最好不要只拿发布时间当游标。
同一秒内如果发布了多条内容,只靠时间很容易碰到重复值,最后还是得把 ID 带进去做兜底。
关注页其实也可以继续用同样方式,只不过它的数据来源变成"我关注的人发布的内容"。
附近页再多一个位置过滤。
三个页面返回的数据结构尽量别拆成三套,前端的内容卡片也能直接复用。

六、Redis用在热点数据上,不要什么都往里塞
社区类系统有不少字段变化频率挺高。
浏览量、点赞数、收藏数就是典型例子。
如果每一次浏览都马上执行:
view_count = view_count + 1
访问量上来以后,会产生很多意义并不大的高频更新。
这种数据可以先放 Redis,再通过 Quartz 定时回写 MySQL。
但这里也别走到另一个极端。
Redis 里有一个 likeCount=100,并不能说明当前用户点没点过赞。
"点赞总数"和"谁点过赞"是两件事。
用户点赞关系最好还是有真实记录,至少需要保存 user_id 和 content_id,否则取消点赞、防重复点赞、个人点赞记录都不好处理。
浏览记录也是一样。
一方面它是个人中心里的功能,用户可以查看最近看过什么;另一方面它还能给发现页做简单去重。
刚看完的内容,刷新以后又连续刷到两三次,体验会很明显。
第一版推荐没有必要马上上复杂算法。
按发布时间、点赞、评论、收藏、浏览,再叠加一点内容分类和话题兴趣,已经能跑出一个基础版本。
例如用户最近经常看摄影内容,发现页就适当多给摄影相关分类和话题一点权重。
等后面真实行为数据多了,再考虑把推荐逻辑继续拆开。

四端共用后台时,真正统一的是数据,不是页面
Android APP、iOS APP、微信小程序和 H5 四端共用一套后台,这个结构本身没有太多特殊的地方。
关键是不要把业务数据留在某一个客户端。
用户在 APP 收藏的内容,换到 H5 登录同一个账号以后,应该还能看到。
关注关系、粉丝关系、浏览记录也是一样。
这些数据都应该围绕统一的用户 ID 存在服务端。
接口层也尽量保持一致。
发现页、内容详情、点赞、收藏、用户主页,四个终端调同一套接口。
真正需要单独适配的一般是相册选择、相机、定位、微信登录、支付这些终端能力。
后台没必要为了 Android 再写一套 /app/content,为了小程序再写 /mini/content。
接口一旦这样拆开,后面字段改一次就要改好几处。
单聊和通知则单独放到 WebSocket 这一层处理。
评论通知、点赞提醒、关注提醒,本质上都是业务动作完成以后产生一条通知。数据库先保存,WebSocket 负责在线推送。
消息已读未读也是一样,不能只依赖连接状态。
用户断线重连以后,历史消息和未读数还得能重新查出来。
商品分类、商品列表、购物车、订单、支付这些模块没必要直接塞进内容主链路。内容如果需要关联商品,保存一层内容和商品关系即可,订单和支付还是走商城自己的业务表。
这种拆法后面做作品管理、收藏管理、浏览记录、关注列表和粉丝列表时,基本都还是围绕内容 ID、用户 ID 两套关系继续查,不需要再重新建立一套数据模型。