Spring Boot与异步审核构建仿小红书源码内容发布全流程

仿小红书源码中的内容发布,并不是把标题、正文和图片写入数据库后就结束了。

一篇图文笔记从用户选择图片开始,需要经过文件上传、媒体信息记录、草稿保存、内容审核、状态更新、搜索索引和内容分发。短视频发布还会增加视频封面、播放时长、文件转码、异步审核等处理步骤。

如果把所有操作都放在一个接口中同步执行,只要文件存储、内容审核或搜索服务响应变慢,整个发布接口就会被拖慢。用户可能长时间停留在提交页面,数据库连接和应用线程也会持续被占用。

更合理的处理方式,是把内容发布拆成多个独立环节:

媒体文件先上传,业务数据后提交;草稿与正式作品分开处理;审核任务异步执行;内容通过后再进入搜索和推荐。

在这套流程中,Spring Boot 负责业务编排,对象存储负责图片和视频文件,内容审核服务负责识别不合规信息,Quartz 负责异常任务补偿,EasyES 负责公开内容检索。

从用户点击发布开始,后台发生了什么

以一篇包含六张图片、两个话题标签的图文笔记为例,用户点击发布后,后台并不是只执行一条插入语句。

整个链路可以拆成以下步骤:

javascript 复制代码
选择图片或视频
        ↓
获取文件上传凭证
        ↓
上传至对象存储
        ↓
返回文件地址、宽高和资源标识
        ↓
提交正文、分类、话题及媒体列表
        ↓
创建待审核内容
        ↓
执行文字、图片或视频审核
        ↓
审核通过后公开展示

文件上传和内容发布分离后,媒体上传失败不会直接影响正文数据。

用户上传三张图片后临时退出,也可以把现有内容保存到草稿箱。再次进入发布页面时,系统读取草稿中的正文、图片顺序、分类和话题,不需要重新选择所有文件。

对于短视频,文件上传独立处理后,还可以继续完成封面生成、视频时长读取、清晰度检查和文件转码。

发布接口不直接接收文件流

在仿小红书源码中,一篇内容可能包含多张高清图片,也可能包含体积较大的短视频。

如果客户端把文件流、正文、话题标签和分类信息一次性提交,接口会长时间占用连接。网络出现波动时,还可能出现正文提交成功,但部分图片上传失败的情况。

因此,内容发布接口更适合只接收已经上传完成的媒体信息。

javascript 复制代码
{
  "title": "城市周边徒步记录",
  "contentText": "整理了几处适合周末散步和拍照的位置。",
  "contentType": "IMAGE_TEXT",
  "categoryId": 8,
  "topicIds": [16, 31],
  "mediaList": [
    {
      "fileUrl": "/content/image/2026/07/001.jpg",
      "mediaType": "IMAGE",
      "width": 1080,
      "height": 1440,
      "sortNumber": 1
    },
    {
      "fileUrl": "/content/image/2026/07/002.jpg",
      "mediaType": "IMAGE",
      "width": 1080,
      "height": 1350,
      "sortNumber": 2
    }
  ]
}

媒体数据不能只保存文件地址。

仿小红书源码的发现页通常采用双列瀑布流布局,客户端需要根据封面图片宽高比例提前计算卡片高度。如果接口只返回图片地址,页面需要等待图片加载完成后才能确定高度,容易出现卡片跳动和布局重排。

多图内容还要保存排序字段。用户在发布页面调整图片顺序后,内容详情、作品编辑和个人主页都必须按照相同顺序展示。

短视频媒体可以额外保存以下信息:

  • 视频时长;
  • 视频宽高;
  • 视频封面;
  • 文件大小;
  • 转码状态;
  • 审核状态;
  • 播放地址。

草稿与正式发布不是同一个流程

草稿保存强调灵活,正式发布强调完整。

用户保存草稿时,可能只输入一段正文,还没有选择分类;也可能上传了两张图片,但准备稍后继续补充。此时不应该要求所有字段完整,更不需要立即进入内容审核。

草稿阶段只需要检查基础数据:

校验项目 处理目的
用户身份 防止未登录状态保存
文件归属 防止引用其他用户资源
媒体数量 避免超过系统限制
文件类型 阻止不支持的格式
草稿状态 避免修改已删除数据

用户点击正式发布后,再执行完整校验,包括正文长度、内容分类、话题标签、媒体数量、内容类型和可见范围。

草稿保存后只出现在个人主页的作品管理中,不进入发现页、关注页、附近页、话题页和内容搜索结果。

用状态流转管理内容生命周期

一篇图文或短视频从创建到公开,往往会经历多个阶段。

如果内容表只有"正常"和"删除"两个状态,无法区分草稿、待审核、审核失败和后台下架等场景。

可以将内容状态划分为:

|------------|-----------|
| 状态 | 说明 |
| DRAFT | 草稿,只有作者可见 |
| WAIT_AUDIT | 已提交,等待审核 |
| AUDITING | 审核任务处理中 |
| PUBLISHED | 已公开展示 |
| REJECTED | 审核未通过 |
| HIDDEN | 作者主动隐藏 |
| OFFLINE | 后台下架 |
| DELETED | 逻辑删除 |

状态划分后,不同页面可以使用明确的查询条件。

发现页、关注页、附近页、内容搜索和话题聚合,只返回 PUBLISHED 状态的数据。个人作品管理则可以展示草稿、审核中、已发布和审核未通过的作品。

收藏管理和浏览记录可以保留历史关系,但内容已经删除或下架时,不能继续返回完整详情。

Spring Boot只负责提交核心业务数据

正式发布时,核心事务只处理内容主表、媒体表、话题关系和审核任务,不应在事务中等待审核结果。

javascript 复制代码
@Transactional(rollbackFor = Exception.class)
public Long publish(ContentPublishDTO dto, Long userId) {
    validatePublishRequest(dto, userId);

    Content content = new Content();
    content.setUserId(userId);
    content.setTitle(dto.getTitle());
    content.setContentText(dto.getContentText());
    content.setContentType(dto.getContentType());
    content.setCategoryId(dto.getCategoryId());
    content.setStatus(ContentStatus.WAIT_AUDIT);
    content.setCreateTime(LocalDateTime.now());

    contentMapper.insert(content);

    contentMediaService.saveBatch(
        content.getId(),
        dto.getMediaList()
    );

    contentTopicService.bindTopics(
        content.getId(),
        dto.getTopicIds()
    );

    auditTaskService.create(content.getId());

    return content.getId();
}

这里的关键点是:数据库事务只创建审核任务,不直接执行远程审核请求。

内容审核属于外部调用,响应时间不可控。如果审核过程被放进数据库事务,只要调用超时,数据库连接就会长时间无法释放。

核心数据先完成入库,再由审核任务继续处理,可以降低事务复杂度,也方便记录失败原因和执行次数。

图文与短视频采用不同审核节奏

图文内容和短视频内容虽然共用发布入口,但审核方式并不相同。

图文内容分段审核

一篇图文笔记通常包含标题、正文、话题标签和多张图片。

可以按照以下顺序处理:

javascript 复制代码
标题与正文审核
        ↓
话题文字审核
        ↓
多张图片并行审核
        ↓
汇总全部审核结果
        ↓
更新作品最终状态

只有文字和所有图片都通过,作品才能进入公开状态。

如果六张图片中有一张审核未通过,审核任务需要记录对应媒体 ID 和失败原因。用户进入作品编辑页面后,可以直接定位问题图片并完成替换。

短视频异步审核

短视频审核通常耗时更长,不适合让发布接口同步等待。

系统提交视频审核任务后,可以保存审核任务编号,通过回调接口或定时查询获取结果。视频封面和发布文案可以先完成审核,但视频结果没有确定前,内容不能公开。

审核任务表可以保存:

  • 业务内容 ID;
  • 审核类型;
  • 任务状态;
  • 外部任务编号;
  • 已重试次数;
  • 失败原因;
  • 创建时间;
  • 下次执行时间;
  • 完成时间。

Quartz补偿长期未完成的审核任务

文件存储、内容审核和网络通信都可能发生异常。

例如,审核请求已经发出,但系统在保存结果前重启;或者视频审核已经完成,回调接口暂时不可用。此时内容可能长期停留在审核中。

Quartz 可以定时扫描异常任务:

  1. 长时间处于等待审核状态的内容;
  2. 已创建但尚未执行的审核任务;
  3. 调用失败且未达到重试上限的任务;
  4. 已提交但尚未取得结果的视频任务;
  5. 审核通过但后续处理未完成的内容。
javascript 复制代码
@Component
public class AuditTaskRetryJob {

    @Resource
    private AuditTaskService auditTaskService;

    @Scheduled(cron = "0 */5 * * * ?")
    public void retryAuditTask() {
        List<AuditTask> tasks =
            auditTaskService.findRetryTasks(100);

        for (AuditTask task : tasks) {
            try {
                auditTaskService.execute(task.getId());
            } catch (Exception exception) {
                auditTaskService.markFailed(
                    task.getId(),
                    exception.getMessage()
                );
            }
        }
    }
}

补偿任务不能无限重试。

任务表应记录最大重试次数和下次执行时间。第一次失败后可以间隔一分钟,第二次间隔五分钟,后续逐步延长,避免短时间内频繁调用审核服务。

超过重试上限后,任务可以转为人工处理状态,同时保留错误信息和执行记录。

应用部署多个节点时,还需要增加数据库锁或 Redis 分布式锁,避免同一条任务被多个节点重复处理。

审核通过后才进入公开内容体系

审核通过并不只是把内容状态改成 PUBLISHED

后台还需要继续完成搜索索引、话题聚合、发现页分发和关注通知等操作。

建立EasyES内容索引

内容搜索如果一直使用数据库模糊查询,数据量增长后会对主库产生较大压力,也很难完成关键词相关度排序。

审核通过后,可以将以下字段写入 EasyES:

  • 内容标题;
  • 正文摘要;
  • 作者信息;
  • 内容分类;
  • 话题名称;
  • 发布时间;
  • 内容类型;
  • 点赞数量;
  • 评论数量;
  • 收藏数量。

用户搜索关键词时,可以同时匹配标题、正文和话题标签,再结合发布时间与互动热度排序。

待审核、审核失败、隐藏和下架内容不能进入搜索索引。作品状态失效后,也要及时删除对应索引,避免内容继续出现在搜索结果中。

更新话题聚合

用户发布内容时添加的话题标签,只能在审核通过后计入公开作品数量。

一篇作品可以关联多个话题。例如同时加入"城市徒步"和"周末生活",审核通过后,这篇作品会出现在两个话题页面中。

作品隐藏、删除或下架后,话题聚合数据也要同步更新。

加入发现页候选内容

发现页虽然采用双列瀑布流布局,但决定内容顺序的是后台推荐逻辑。

初期可以根据发布时间、点赞数、评论数、收藏数和浏览完成度计算内容热度。用户近期浏览过的分类、收藏过的话题和关注的作者,也可以作为推荐信号。

已浏览内容可以适当降低推荐权重,减少发现页反复出现相同作品。

互动操作必须先验证作品状态

仿小红书源码中的公开作品支持点赞、评论、回复、收藏和关注作者。

但用户每次执行互动前,后台都要检查作品是否仍处于公开状态。

已经被作者隐藏、后台下架或逻辑删除的内容,不应该继续接受新的点赞和评论。否则页面虽然无法访问,互动数量却还在持续增加。

评论表通常需要记录:

|---------------|--------|
| 字段 | 作用 |
| content_id | 所属作品 |
| user_id | 评论用户 |
| parent_id | 父评论 |
| reply_user_id | 被回复用户 |
| content_text | 评论正文 |
| image_url | 评论图片 |
| audit_status | 评论审核状态 |
| create_time | 创建时间 |

一级评论和回复可以使用同一张表,通过 parent_id 区分层级,通过 reply_user_id 标记被回复用户。

评论成功后生成评论通知,回复则生成回复通知。点赞、关注和系统消息也应写入通知记录。

WebSocket 只负责实时推送,不能替代数据库存储。用户离线期间产生的评论通知、点赞提醒和关注提醒,需要在下次登录后重新读取。

单聊消息也遵循相同原则。文字、图片和表情消息先写入消息表,再通过 WebSocket 推送。接收方进入会话后,后台更新消息已读状态。

Redis只缓存热点,不保存最终状态

发现页、内容详情、热门搜索和用户主页属于高频访问区域,可以使用 Redis 降低数据库压力。

适合缓存的数据包括:

  • 热门作品 ID;
  • 内容详情基础信息;
  • 用户主页统计;
  • 话题热度;
  • 热门搜索词;
  • 点赞、评论和收藏数量;
  • 推荐候选内容;
  • 未读消息数量。

但作品是否审核通过、内容是否删除、商品是否上架、订单是否支付等最终状态,仍然要以数据库为准。

Redis 数据可能过期或丢失,系统必须能够从数据库恢复真实业务状态。

热门搜索可以使用 ZSet 保存关键词和搜索次数,Quartz 定期汇总近期搜索数据,过滤无效词和重复词,再形成热门搜索列表。

内容素材与商品素材复用基础服务

带有商城功能的仿小红书源码,还会涉及商品分类、商品列表、商品详情、商品搜索、购物车、立即购买、订单管理和在线支付。

商品图片与内容图片都可以使用统一的文件上传和对象存储服务,也可以复用图片压缩、格式校验和内容审核能力。

但两类业务状态必须分开。

内容的 PUBLISHED 表示允许公开阅读,商品的 ON_SALE 表示允许下单购买。商品即使可以展示,也可能因为库存不足无法购买;内容则不存在库存和支付问题。

基础能力可以复用,业务模块仍然保持独立:

javascript 复制代码
公共基础服务
├─ 文件上传
├─ 对象存储
├─ 内容审核
├─ 短信服务
└─ 搜索服务

业务模块
├─ 内容发布与互动
├─ 即时通讯
├─ 商品与库存
└─ 订单与支付

EasyES 可以分别建立内容索引和商品索引。

内容搜索关注标题、正文、作者和话题,商品搜索则关注商品名称、分类、价格、库存和上下架状态。

Shiro同时处理权限与数据归属

用户可以在个人主页管理作品、草稿、收藏、浏览记录、关注列表和粉丝列表,但只能操作属于自己的数据。

前端隐藏编辑或删除按钮,并不能代替服务端权限校验。用户仍然可能通过修改接口参数尝试操作其他人的作品。

Shiro 可以在接口层判断用户角色和访问权限,业务服务还要继续验证数据归属。

例如:

  • 普通用户只能修改自己的作品;
  • 内容审核人员可以处理审核任务;
  • 内容管理员可以下架公开内容;
  • 商品管理员可以维护商品信息;
  • 订单管理员可以处理订单状态;
  • 系统管理员可以管理账号和权限。

常见权限标识可以包括:

javascript 复制代码
content:publish
content:update
content:delete
content:audit
content:offline
goods:create
goods:update
order:manage
system:notice

用户拥有 content:update 权限,并不代表可以修改所有内容。业务层仍然要比较当前用户 ID 与作品作者 ID。

如何判断一篇内容真正发布完成

数据库插入成功,只能说明主数据已经保存,不能代表作品已经进入公开内容体系。

一篇图文或短视频真正完成发布,需要满足以下条件:

javascript 复制代码
媒体文件上传完整
        +
内容主数据写入成功
        +
分类与话题关系有效
        +
文字、图片或视频审核通过
        +
内容状态更新为公开
        +
EasyES搜索索引创建完成
        +
发现页和话题页能够正常读取

任何一个环节出现异常,系统都要保留明确状态,并通过任务重试或定时补偿继续处理。

经过这样的拆分,图文发布、短视频发布、多图上传、草稿保存、话题标签、内容搜索和双列瀑布流不再是相互独立的页面功能,而是围绕同一套内容生命周期运行。

Spring Boot负责编排业务流程,对象存储处理媒体文件,内容审核服务控制公开范围,Quartz修复异常任务,EasyES负责内容检索,Redis加速热点数据,Shiro限制操作权限。各项技术保持清晰边界后,仿小红书源码的内容发布链路才能具备更好的可维护性和扩展能力。

系统模块关系的结构梳理湖南宠友信息技术有限公司是一家专注社区交友类产品、企业即时通信软件开发,为企业提供即时通信工具、垂直类内容圈子,自主研发的业界知名友猫产品拥有广大的企业用户群体https://chongyou.info/1/product/xhs.html

相关推荐
蜡台1 小时前
uniapp vue2 转 vue3
前端·javascript·uni-app·vue3·vue2
Listen·Rain1 小时前
Springboot整合RedisStack
java·spring boot·redis
杨运交1 小时前
[046][Crypto模块]Spring Boot 自动配置进阶:按需装配加解密处理器
spring boot
花和满楼1 小时前
K8s 1.36 ImageVolume GA:OCI 镜像不再只能跑容器
java·容器·kubernetes
头茬韭菜1 小时前
3.9 成本预警 — 退出钩子、$5 阈值对话框与多通道成本通知
java·大数据·jvm·ai
重庆小透明2 小时前
今日收获(一些个人思考)
java·spring·大模型·springai·prompt注入
2401_873479402 小时前
如何识别代理IP和伪装流量?用IP情报工具三步穿透住宅代理伪装
网络·数据库·网络协议·tcp/ip·ip
诚信定制8392 小时前
如何启动 Redis 服务:详细步骤指南
数据库·redis·缓存
AllData公司负责人2 小时前
数据同步平台|AIIData数据中台实现MySQL、Hive、Kafka 一键接入Doris
大数据·数据库·hive·mysql·kafka·实时同步