内容社区系统的后端并不是把图文、短视频、圈子、聊天、会员、商品、订单分别写成接口就结束了。
当内容发布、互动行为、即时通讯、积分任务、商品交易、后台审核、多端展示同时存在时,更重要的是让不同业务之间通过稳定的技术机制协作,而不是让模块之间互相调用、互相依赖。
在 Spring Boot 内容社区源码中,比较适合抽象出三类底层能力:
javascript
统一状态机:管理内容、订单、审核、会员等状态流转
领域事件:解耦发布、评论、点赞、通知、积分、搜索同步
多端协议:让 APP、小程序、H5 共用统一数据结构

1. 先把业务动作抽象出来
内容社区里有很多操作看起来不同,但从后端角度看,本质都是"用户行为触发业务状态变化"。
比如:
javascript
发布图文 → 内容进入待审核状态
上传短视频 → 文件进入审核流程
评论动态 → 生成互动记录和消息通知
加入圈子 → 生成成员关系
发送私信 → 消息落库并推送
购买虚拟商品 → 订单支付后触发自动发货
完成任务 → 生成积分流水
这些行为不适合全部写在一个 Service 方法里。
更好的方式是先完成当前业务的核心写入,然后通过领域事件通知其他模块继续处理。
例如内容发布只负责保存内容和状态;搜索模块监听内容发布事件后同步 EasyES 索引;通知模块监听评论事件后推送 WebSocket 消息;积分模块监听任务完成事件后写入积分流水。
这样可以减少模块之间的直接依赖。

2. 内容状态机不要只靠数字字段
很多项目会在内容表里放一个 status 字段,然后在不同业务里直接写 0、1、2、3。
这种写法早期简单,但后期维护成本很高。
内容社区通常至少需要区分审核状态和展示状态:
javascript
审核状态:待审核、审核通过、审核拒绝、人工复审
展示状态:草稿、已发布、已隐藏、已删除、已下架
两类状态不能混在一起。
一篇文章审核通过后可以展示;如果用户删除文章,只应该改变展示状态,而不是改审核状态。短视频、问答、圈子动态、挂商品内容也可以复用这套状态规则。
可以用枚举明确状态含义。
javascript
public enum PublishStatus {
DRAFT(0, "草稿"),
PUBLISHED(1, "已发布"),
HIDDEN(2, "已隐藏"),
DELETED(3, "已删除"),
OFFLINE(4, "已下架");
private final Integer code;
private final String desc;
PublishStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
如果状态流转比较复杂,可以继续封装状态流转方法。
例如只有审核通过的内容才能发布,已删除内容不能再次进入首页流,已下架商品不能出现在内容卡片里。

3. 领域事件负责模块解耦
内容社区系统中,一个动作经常会影响多个模块。
用户发布短视频后,除了内容表要写入,文件资源要记录,内容审核要处理,搜索索引要同步,作者主页要展示,话题页也可能需要聚合。
如果在发布接口里连续调用十几个 Service,代码会很快变得臃肿。
领域事件可以把这类链路拆开。
示例:定义内容发布事件。
javascript
public class ContentPublishedEvent {
private final Long contentId;
private final Long userId;
private final Integer contentType;
public ContentPublishedEvent(Long contentId, Long userId, Integer contentType) {
this.contentId = contentId;
this.userId = userId;
this.contentType = contentType;
}
public Long getContentId() {
return contentId;
}
public Long getUserId() {
return userId;
}
public Integer getContentType() {
return contentType;
}
}
发布内容时,只处理主流程。
javascript
@Service
public class ContentPublishService {
@Resource
private ApplicationEventPublisher eventPublisher;
@Transactional
public Long publish(ContentPublishDTO dto) {
Long contentId = saveContentMain(dto);
saveContentExt(dto, contentId);
eventPublisher.publishEvent(
new ContentPublishedEvent(
contentId,
dto.getUserId(),
dto.getContentType()
)
);
return contentId;
}
}
搜索、通知、积分、任务中心可以分别监听这个事件。
这样内容模块不需要知道 EasyES 怎么同步,也不需要知道积分任务怎么计算。

4. 事件监听要考虑事务提交
领域事件不是简单"发出去就完了"。
如果内容保存失败,但事件已经被消费,就会产生脏数据。
因此,监听器通常应该在事务提交后执行。
javascript
@Component
public class ContentEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onContentPublished(ContentPublishedEvent event) {
// 1. 同步搜索索引
// 2. 刷新作者主页统计
// 3. 处理话题、圈子聚合
// 4. 写入任务进度
}
}
对于搜索同步、积分发放、通知推送这类链路,还可以进一步引入消息队列或任务表。
如果同步失败,通过 Quartz 定时扫描失败记录并补偿,避免一次异常影响整条业务链路。
5. 即时通讯也可以事件化
私信、单聊、群聊和系统通知通常使用 WebSocket 实时推送。
但 WebSocket 只负责在线推送,消息本身仍然应该先落库。
发送消息时可以拆成三步:
javascript
消息写入数据库
↓
更新会话和未读数
↓
发布消息推送事件
这样单聊、群聊、评论通知、点赞通知、订单通知可以使用统一的推送通道。
在线用户通过 WebSocket 接收;离线用户下次进入 APP、小程序或 H5 时,通过接口拉取未读消息。
在多端共用后台的场景里,消息状态要统一存储。
用户在 H5 阅读过私信后,APP 端不应该继续显示未读;用户在小程序进入群聊后,iOS 端会话未读数也应该同步减少。

6. 多端接口要返回统一卡片协议
内容社区前端可能同时存在 uniapp APP、微信小程序、H5 页面和后台管理端。
后端接口不应该为每个端维护一套数据结构,而应该输出统一协议。
例如首页动态流里可能出现图文、短视频、文章、问答、投票、商品卡片、圈子动态。
这些内容虽然 UI 不同,但都可以抽象成"卡片"。
javascript
@Data
public class FeedCardVO {
private Long bizId;
private Integer contentType;
private String cardType;
private String title;
private String coverUrl;
private AuthorVO author;
private StatVO stat;
private Map<String, Object> extra;
@Data
public static class AuthorVO {
private Long userId;
private String nickname;
private String avatar;
}
@Data
public static class StatVO {
private Long likeCount;
private Long commentCount;
private Long collectCount;
private Long viewCount;
}
}
前端根据 cardType 判断渲染方式。
短视频卡片读取 videoUrl 和 duration,文章卡片读取 summary 和 coverUrl,商品关联内容读取 goodsId 和价格展示字段,圈子动态读取 circleId 和圈子名称。
这样可以让安卓 APP、iOS APP、小程序和 H5 共用一套接口,只在展示层处理差异。

7. 积分和会员不直接侵入主业务
内容社区中的任务中心、积分商城、VIP 特权、账号认证、头像挂件等模块,通常会和发布、评论、购物、邀请、加入圈子等行为关联。
如果在每个业务接口里手动写积分逻辑,后期规则会变得很难维护。
更好的方式是把用户行为转成事件。
javascript
发布内容事件 → 判断是否完成发布任务
评论事件 → 判断互动任务
订单支付事件 → 判断消费任务
邀请注册事件 → 判断邀请任务
认证通过事件 → 刷新用户权益
积分模块只关心事件和规则,不直接关心内容、订单、圈子内部怎么实现。
会员权益也可以通过统一权限或权益判断组件控制,例如是否允许使用某类装扮、是否展示认证标识、是否拥有特定身份权益。
8. 商品交易要使用独立状态流
内容可以挂载商品,短视频可以展示商品卡片,达人主页可以展示带货数据,圈子里也可以出现实物商品、虚拟商品或服务商品。
但交易状态不能放在内容模块里处理。
交易模块应该独立维护:
javascript
商品状态
SKU 状态
库存状态
订单状态
支付状态
退款状态
发货状态
佣金状态
例如虚拟商品支付完成后,可以通过订单支付成功事件触发自动发货;服务商品可以进入报价单流程;拼团和秒杀可以通过活动状态机控制时间、库存和订单关系。
内容模块只需要保存 goods_id,展示商品卡片时再读取交易模块提供的安全字段。
这样内容状态和交易状态互不污染,后续调整商品规则时不会影响内容发布链路。

9. 搜索索引只做检索,不做业务主库
全局搜索通常会覆盖动态、文章、短视频、问答、话题、圈子、用户、商品等对象。
这些数据可以通过 EasyES 同步到 Elasticsearch,但搜索索引不应该成为业务主库。
索引里只保存检索和列表展示需要的字段:
javascript
biz_id
data_type
title
content
cover_url
author_id
topic_name
circle_name
goods_name
hot_score
audit_status
publish_status
create_time
详情页仍然回源 MySQL。
这样可以保证搜索负责"找到数据",业务库负责"确认数据状态和完整详情"。
当内容被删除、商品下架、圈子隐藏、用户资料修改时,都应该触发索引更新事件。
如果事件处理失败,再通过 Quartz 做补偿扫描,保证搜索结果和业务状态最终一致。
10. 后台权限要配合业务状态
后台管理不是简单 CRUD。
内容审核、圈子管理、商品管理、订单处理、会员配置、积分商城、佣金结算、多商户入驻,都需要权限控制和状态判断共同完成。
Shiro 可以控制接口权限,例如:
javascript
content:audit
circle:manage
goods:update
order:handle
merchant:audit
commission:settle
但接口权限之外,还需要数据权限。
圈主只能管理自己的圈子,商户只能查看自己的订单,审核角色只能处理权限范围内的内容。
这类判断通常要放在 Service 层,而不是只依赖 Controller 注解。

11. 推荐的执行链路
可以把内容社区中的核心流程整理成下面这种结构:
javascript
用户行为
↓
业务模块写入主数据
↓
状态机校验状态流转
↓
发布领域事件
↓
搜索、通知、积分、统计、任务分别监听
↓
失败记录进入补偿任务
↓
多端通过统一接口读取最新数据
这种结构可以让图文、短视频、问答、圈子、私信、群聊、会员、积分、商品、订单、多商户等模块保持相对独立。
业务扩展时,不需要在原有接口里不断追加判断,而是通过事件、状态和统一协议进行组合。
从技术角度看,Spring Boot 负责组织业务模块,Redis 负责高频数据和状态缓存,WebSocket 负责实时消息推送,EasyES 负责聚合搜索,Quartz 负责补偿任务,Shiro 负责权限边界,Druid 负责数据库连接和 SQL 监控。
内容社区源码的可维护性,不只取决于功能数量,更取决于底层结构是否清晰。
当状态机、领域事件和多端接口协议稳定后,内容发布、互动通知、即时通讯、积分任务、商品交易和搜索同步才能在同一套后端体系中有序协作。