从重复请求到订单同步看内容社区源码的处理思路

内容社区发展到一定阶段后,系统中的一次用户操作往往不再只修改一张数据表。

用户点赞一篇图文,除了新增点赞记录,还会改变内容统计、作者获赞数量和消息提醒;用户评论一条短视频,系统需要保存评论、更新评论数,并向内容作者发送通知;用户收藏商品后,个人中心的收藏管理、商品详情页和其他登录终端也应同步展示最新状态。

当项目同时包含图文发布、短视频、多图上传、话题标签、用户关系、即时通讯和在线交易时,真正复杂的部分并不是把接口逐个写出来,而是保证不同模块在高并发、网络重试和服务异常情况下仍能得到相对一致的结果。

以 Spring Boot 内容社区源码为背景,从事件驱动、事务记录、幂等消费和状态机几个方面,讨论内容、互动、通知和订单之间的数据流转方式。

从一次评论操作开始拆分系统行为

假设用户在发现页看到一篇内容,并提交一条评论。

从页面上看,这只是一次普通的评论请求。进入服务端后,至少会产生以下变化:

javascript 复制代码
保存评论记录
更新内容评论数量
生成评论通知
更新作者未读提醒
刷新内容详情缓存
通过 WebSocket 推送消息
记录用户互动行为

如果所有逻辑都写进同一个接口,评论服务就需要直接依赖内容服务、通知服务、缓存组件和 WebSocket 推送组件。

代码可能会逐渐变成下面这种调用结构:

javascript 复制代码
CommentController
    └── CommentService
          ├── commentRepository.insert()
          ├── contentService.increaseCommentCount()
          ├── notificationService.create()
          ├── redisService.deleteContentCache()
          ├── websocketService.push()
          └── behaviorService.save()

这种结构在初期比较直观,但后续增加评论回复、系统通知、点赞提醒、关注提醒和内容推荐行为时,评论服务的职责会不断扩大。

更合理的做法是让评论模块只完成评论业务本身,然后发布一个"评论已创建"的领域事件。其他模块根据需要订阅该事件,各自完成统计、通知和行为记录。

业务事件不等于直接调用异步方法

在 Spring Boot 项目中,可以通过事件对象表达一次已经完成的业务事实。

javascript 复制代码
public record CommentCreatedEvent(
        Long eventId,
        Long commentId,
        Long contentId,
        Long authorId,
        Long senderId,
        Long createdAt
) {
}

这个事件只描述发生了什么,不负责决定所有后续操作。

内容统计模块收到事件后增加评论数,通知模块生成评论通知,用户行为模块记录互动数据,WebSocket 模块再根据用户在线状态决定是否实时推送。

事件驱动可以减少模块之间的直接依赖,但不能简单地在数据库事务中调用一个异步线程。

例如,评论记录尚未提交时就发送异步事件,一旦数据库事务回滚,通知可能已经生成,内容作者会看到一条实际不存在的评论提醒。

因此,业务数据和待发布事件需要具备一致的落库顺序。

使用本地事件表保存待处理任务

一种常见做法是在保存业务数据的同时,向事件表写入一条记录。

评论数据和事件记录位于同一个数据库事务中。只要事务提交成功,事件就不会丢失;事务回滚时,两条记录会一起撤销。

事件表可以包含以下字段:

javascript 复制代码
event_id
event_type
aggregate_id
event_body
event_status
retry_count
next_retry_time
created_time
processed_time

评论服务的核心代码可以写成:

javascript 复制代码
@Transactional(rollbackFor = Exception.class)
public Long createComment(CreateCommentCommand command) {
    ContentComment comment = new ContentComment();
    comment.setContentId(command.contentId());
    comment.setUserId(command.userId());
    comment.setReplyUserId(command.replyUserId());
    comment.setContent(command.content());
    comment.setStatus(CommentStatus.NORMAL);
    commentRepository.insert(comment);

    CommentCreatedEvent event = new CommentCreatedEvent(
            idGenerator.nextId(),
            comment.getId(),
            comment.getContentId(),
            command.contentAuthorId(),
            command.userId(),
            System.currentTimeMillis()
    );

    DomainEventRecord record = new DomainEventRecord();
    record.setEventId(event.eventId());
    record.setEventType("COMMENT_CREATED");
    record.setAggregateId(comment.getId());
    record.setEventBody(jsonService.toJson(event));
    record.setEventStatus(EventStatus.PENDING);
    record.setRetryCount(0);
    eventRepository.insert(record);

    return comment.getId();
}

这里没有直接更新未读数,也没有立即调用 WebSocket。评论服务只保证评论与事件同时写入数据库。

后续可以由 Quartz 定时扫描待处理事件,也可以通过独立的事件发布任务发送到消息队列。即使消息组件暂时不可用,事件仍保留在数据库中,恢复后可以继续处理。

这类方式比较适合需要独立部署、二次开发的内容社区项目,因为事件表结构清晰,排查数据问题时也能看到完整处理状态。

事件消费必须考虑重复执行

事件进入消费端后,并不能假设每条消息只会执行一次。

网络超时、消费者重启、任务重试或消息确认失败,都可能导致同一个事件被再次处理。

假设评论事件第一次已经让评论数增加了 1,但消费结果尚未成功确认,系统随后重试该事件。如果没有幂等控制,评论数会再次增加,最终与实际评论记录数量不一致。

通知模块同样如此。一次评论可能生成两条完全相同的评论通知,用户在安卓 APP 和 H5 网页端都会看到重复提醒。

一种实现方式是建立事件消费记录表,以事件编号和消费者名称作为唯一键。

javascript 复制代码
@Transactional(rollbackFor = Exception.class)
public void handleCommentCreated(CommentCreatedEvent event) {
    String consumerName = "content-statistics-consumer";

    boolean inserted = consumedEventRepository.tryInsert(
            event.eventId(),
            consumerName
    );

    if (!inserted) {
        return;
    }

    contentStatisticsRepository.increaseCommentCount(
            event.contentId(),
            1
    );

    redisTemplate.delete(
            "content:statistics:" + event.contentId()
    );
}

tryInsert 依赖数据库唯一索引实现。如果事件已经被当前消费者处理过,再次插入时会失败,业务逻辑直接结束。

不同消费者可以独立记录消费状态。例如,统计模块处理成功,不代表通知模块也已成功。通过消费者名称区分后,各模块可以分别重试。

这种幂等机制同样适用于点赞、收藏、关注、评论回复、系统通知和订单状态变化。

内容统计不要和行为记录混在一起

内容详情页通常会展示点赞数、评论数和收藏数,用户主页还会展示作品数量、关注数量、粉丝数量等信息。

这些统计值可以单独维护,但不能代替真实关系数据。

例如,用户点赞内容时,应先写入点赞关系表:

javascript 复制代码
content_id
user_id
created_time
status

并为 content_id + user_id 建立唯一约束,防止客户端重复提交造成重复点赞。

内容点赞总数属于统计结果,可以通过事件异步更新。用户是否已经点赞,则应从关系数据或用户维度缓存中读取。

这两类数据的含义不同:

javascript 复制代码
点赞关系回答"谁点赞了哪条内容"
点赞统计回答"这条内容共有多少点赞"

收藏、关注和粉丝关系也是同样的思路。

个人中心中的收藏管理、关注列表和粉丝列表,应以真实关系表为准;发现页和用户主页展示的统计数量,则可以读取缓存或统计表。

这样即使统计数据短时间延迟,也不会影响用户取消收藏、取消关注等关键操作。

Redis缓存失效应由业务事件触发

内容社区中的热门数据通常会放入 Redis,例如:

javascript 复制代码
内容详情
用户主页摘要
商品详情
内容互动统计
热门搜索词
发现页候选内容

缓存并不是写入之后一直等待过期。内容被编辑、下架或删除时,相关缓存需要及时清理;点赞、评论和收藏发生变化后,统计缓存也需要刷新。

如果每个模块都自行拼接缓存键,后续修改缓存结构时容易出现遗漏。

可以让缓存处理模块订阅业务事件:

javascript 复制代码
CONTENT_UPDATED
CONTENT_DELETED
COMMENT_CREATED
COMMENT_DELETED
LIKE_CHANGED
COLLECTION_CHANGED
USER_PROFILE_UPDATED
GOODS_UPDATED

例如,资料编辑完成后发布 USER_PROFILE_UPDATED 事件,缓存模块删除用户主页摘要,内容卡片再次读取作者资料时会重新构建缓存。

这样资料编辑、用户主页、作品列表和评论区头像都能逐步获取一致的数据,不需要由用户模块直接调用多个页面服务。

对于高频统计,可以使用"数据库关系记录 + Redis实时值 + 定时校准"的方式。

Redis 提供较快的读写速度,Quartz 定时任务再按照真实关系表校准点赞数、收藏数和关注数,避免长期误差。

WebSocket只承担实时传输

单聊、评论通知、点赞提醒、关注提醒和系统通知都可以通过 WebSocket 实时发送,但 WebSocket 不应成为消息的唯一存储位置。

用户可能处于离线状态,也可能在微信小程序退到后台后断开连接。如果消息只发送到连接通道,没有写入数据库,用户重新登录后就无法恢复这些通知。

比较稳定的顺序是:

javascript 复制代码
业务事件产生
通知记录写入数据库
更新用户未读数量
检测用户在线连接
在线则通过 WebSocket 推送
离线则等待下次拉取

用户在任意终端读取消息时,服务端更新统一的已读状态。安卓 APP、苹果 APP、微信小程序和 H5 网页端重新获取会话或通知列表后,都能得到相同结果。

单聊消息也可以采用类似结构:

javascript 复制代码
先持久化消息
再更新会话摘要
然后尝试实时推送
最后等待客户端确认

图片消息只保存对象存储地址和资源信息,表情消息保存表情编号或资源地址。消息类型不同,但已读未读、会话排序和离线恢复可以复用同一套基础逻辑。

草稿与正式内容需要不同的数据约束

图文发布、短视频发布和多图上传都支持草稿保存时,草稿不应该提前进入正式内容链路。

草稿可以包含未完成的标题、正文、话题标签、图片列表和商品关联信息,但不参与以下流程:

javascript 复制代码
内容搜索
热门排序
内容推荐
关注页分发
附近页展示
用户作品公开列表

只有正式发布并通过内容审核后,系统才生成 CONTENT_PUBLISHED 事件。

搜索模块收到事件后建立 EasyES 索引,推荐模块加入候选集合,关注关系模块可以向粉丝的关注页写入新的内容标识。

如果内容审核未通过,则保持不可见状态,不生成公开分发事件。用户仍可以在作品管理中看到审核状态,并重新编辑后提交。

这种状态隔离可以避免草稿内容被搜索到,也能减少发布流程中的条件判断。

内容状态适合使用显式状态机

一条内容可能经历草稿、审核中、已发布、已隐藏、审核拒绝和已删除等状态。

如果业务代码中大量使用零散的 if 判断,很容易出现不允许的状态跳转。例如,已经删除的内容被重新审核,或者审核中的内容直接进入推荐列表。

可以明确允许的状态变化:

javascript 复制代码
DRAFT → REVIEWING
REVIEWING → PUBLISHED
REVIEWING → REJECTED
REJECTED → DRAFT
PUBLISHED → HIDDEN
HIDDEN → PUBLISHED
PUBLISHED → DELETED
HIDDEN → DELETED

内容状态改变后,再根据新状态发布对应事件。

PUBLISHED 负责建立索引和进入公开列表,HIDDEN 负责从搜索与推荐中移除,DELETED 则清理缓存、搜索索引和公开关系。

作品管理页面仍可以根据业务要求保留部分历史记录,但发现页、关注页和附近页不再返回不可见内容。

商品交易不能复用内容状态

内容社区中允许内容关联商品后,内容模块和商城模块会产生连接,但两者不应共用同一套状态字段。

内容是否发布,与商品是否上架是两个独立状态。即使内容仍然公开,关联商品也可能已经售罄或下架。

商品详情、购物车、立即购买、订单管理和在线支付应由商城业务独立处理。内容卡片只保存商品编号,在展示时读取商品当前状态。

创建订单时,还需要保存商品快照,包括:

javascript 复制代码
商品名称
商品规格
成交单价
购买数量
商品图片
优惠信息

如果订单只保存商品编号,用户支付后商家修改商品名称或价格,历史订单内容也会随之变化。

支付流程则适合使用订单状态机:

javascript 复制代码
WAIT_PAY
PAID
WAIT_DELIVERY
DELIVERED
COMPLETED
CLOSED
REFUNDING
REFUNDED

支付回调可能重复到达,因此更新订单状态时同样需要幂等判断。只有订单仍处于待支付状态,才能从 WAIT_PAY 转为 PAID

订单支付完成后可以发布 ORDER_PAID 事件,由库存、通知和用户订单统计分别消费,而不是在支付回调接口中直接执行所有后续逻辑。

四端共用后台的关键是服务端状态统一

安卓 APP、苹果 APP、微信小程序和 H5 网页端共用一套后台时,客户端不应该各自维护业务真相。

例如,用户在 H5 中取消收藏后,服务端需要修改统一的收藏关系,并产生收藏变更事件。苹果 APP 再次获取收藏管理列表时,应读取新的服务端结果,而不是继续使用本地旧状态。

浏览记录也应以用户账号为维度保存。用户在微信小程序浏览内容,随后打开安卓 APP,个人中心仍可以展示这条记录。

客户端本地缓存只用于加快页面展示,不能决定最终状态。点赞、收藏、关注、购物车和订单都需要以服务端响应为准。

对于重复提交,可以通过请求编号、业务唯一键或版本号进行控制。

尤其是网络较差时,移动端可能自动重试接口。没有幂等设计的情况下,一次加入购物车可能被执行多次,一次评论也可能生成多条重复记录。

数据查询可以采用读模型降低接口复杂度

用户主页往往需要同时返回资料、作品数量、获赞数量、关注数和粉丝数。如果每次进入页面都临时连接多张表并聚合计算,访问量增加后查询成本会逐步上升。

可以建立面向页面读取的用户统计表:

javascript 复制代码
user_id
published_content_count
received_like_count
following_count
follower_count
collection_count
updated_time

这些数据通过内容发布事件、点赞事件和关注事件逐步更新。

真实关系仍保存在业务表中,统计表只负责页面读取。当统计结果出现异常时,可以通过 Quartz 定时任务重新计算。

内容详情页、用户主页和商品列表都可以采用类似的读模型思路。写入过程保持严格,读取过程根据页面结构组织数据。

这比在每个接口中临时拼接大量跨模块查询更容易维护,也更适合前后端分离和移动端响应式页面。

一条业务链路的完整落地顺序

将前面的设计组合起来,用户评论一条带商品信息的短视频时,后台可以按照下面的顺序处理:

javascript 复制代码
1. 校验用户身份和内容状态
2. 保存评论关系
3. 同事务写入评论创建事件
4. 事件发布任务读取待处理记录
5. 统计模块增加内容评论数
6. 通知模块保存评论提醒
7. 缓存模块清理内容统计缓存
8. 行为模块记录用户互动
9. WebSocket模块尝试实时推送
10. 各消费者保存幂等处理记录
11. Quartz任务处理失败事件和统计校准

用户在页面上只看到一条评论,但系统内部会经过多个相互独立的处理步骤。

这种拆分不要求所有结果在同一毫秒内完成,而是保证核心数据先可靠落库,后续模块能够重复执行、失败重试并最终完成。

在内容社区源码中,图文、短视频、搜索、瀑布流、互动提醒、即时通讯和商城交易并不是简单的功能组合。真正决定系统稳定性的,是业务状态能否清晰表达、事件是否会丢失、重复请求是否安全,以及不同终端是否始终以统一的服务端数据为准。

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

相关推荐
LaughingZhu1 小时前
Product Hunt 每日热榜 | 2026-07-17
前端·数据库·人工智能·经验分享·mysql·chatgpt·html
带刺的坐椅1 小时前
用 Solon TeamAgent 落地供应链异常管理与自动补货
java·ai·llm·agent·solon·供应链
MobotStone1 小时前
五个AI使用习惯让你的效率翻倍
后端
潇凝子潇1 小时前
UnsupportedOperationException
java·前端·数据库
pixcarp1 小时前
GMP调度模型和GC垃圾回收机制解析
后端·学习·golang·内存管理·gc·gmp
qq_33776320192 小时前
国际期货资管系统开发方案|内外盘交易平台源码授权与二次开发支持
java·c语言·开发语言·c++·c#
不定积分要+C_yyy2 小时前
Java关键字final三种用法、核心特性与高频易错点
java·开发语言
xexpertS3 小时前
队列积压治理:如何避免异步系统出现难以恢复的消息积压
java·开发语言
万少8 小时前
用腻了 WorkBuddy 的默认界面?这个开源小工具给它换上了毛玻璃
前端·javascript·后端