模型设计的目标,是承载变化,而不是预判所有变化。
本文是《面向对象开发实践之消息中心设计》系列的第二篇。
第一篇中,我重点讨论了如何用面向对象的方法启动一个消息中心设计 。这一篇将更进一步,聚焦在一个更"脏"、也更容易失控的部分:消息模型设计。
在多数项目中,消息中心之所以难维护,并不是因为技术有多复杂,而是模型一开始就承载了过多变化。本文会从"最小可行模型"开始,一步步说明:
- 消息模型最初只需要解决什么问题
- 为了适配哪些真实业务场景,我们引入了哪些字段和结构
- 哪些设计是"必须的",哪些是"刻意延后"的
一条消息最本质是什么?
如果抛开 UI、样式、跳转、未读数等需求,一条消息最本质只回答三个问题:
-
给谁看的?
-
什么时候产生的?
-
发生了什么?
对应到模型中,最小消息单元可以极度克制:
java
class Message {
Long id; // 消息ID
Long userId; // 接收用户
String title; // 消息标题
String content; // 文本内容
LocalDateTime sendTime;
}
这个模型几乎什么都不能做 ,但它有一个重要意义:它定义了"消息"与"业务"的最小边界。所有后续设计,都是在这个边界之外有意识地扩展,而不是一开始就把所有业务塞进来。
第一轮拓展:为什么需要「消息分类」?
很快我们会遇到第一个真实需求:
- 用户想单独查看「系统消息」
- 活动消息不应该和评论提醒混在一起
- 不同分类的未读数要分开统计
这时,引入 消息分类(Category) 是非常自然的:
java
enum MessageCategory {
SYSTEM,
ACTIVITY,
INTERACTION,
ORDER
}
模型随之演进:
java
class Message {
Long id;
Long userId;
MessageCategory category;
String title;
String content;
LocalDateTime sendTime;
}
设计取舍
- ✅ 分类是用户视角的概念
- ❌ 它不等同于业务类型(不要直接用业务枚举)
这一层的目的很单纯:支撑列表查询与未读数统计
第二轮扩展:业务关联,但不侵入业务
接下来几乎一定会出现的问题是:
"这条消息是关于哪条评论 / 哪个活动的?"
一个常见但危险的做法是:
java
class CommentMessage extends Message {
Comment comment;
}
这种设计的问题在于:
- 消息与业务生命周期强绑定
- 业务删除 / 变更会直接影响历史消息
正确做法:只存业务标识,不存业务对象
java
enum MessageBizType {
COMMENT,
LIKE,
MENTION,
VIOLATION,
LOTTERY
}
class Message {
Long id;
Long userId;
MessageCategory category;
MessageBizType bizType;
Long bizId; // 业务主键
String title;
String content;
LocalDateTime sendTime;
}
这个设计解决了什么?
-
消息可以长期存在
-
业务可以自由演进、删除、归档
-
是否实时查询业务,由"渲染阶段"决定
这是一个非常关键的边界划分。
内容结构化:为"部分跳转文字"做准备
在第一篇中提到过这样的需求:
"社区规范""修改>>" 需要支持点击跳转
如果 content 只是纯字符串,这种需求几乎无法优雅支持。
首先定义跳转类型
java
public enum MessageJumpType {
NONE, // 无跳转
URL, // 外链
APP_ROUTE, // 应用内部路由
MINI_PROGRAM, // 小程序路径
ACTIVITY_PAGE, // 自动跳消息归属活动页面
}
定义跳转配置模型:
java
public class MessageJumpConfig {
private String url; // URL 跳转
private String route; // App 内部路由
private String miniProgramPath; // 小程序路径
private Map<String,Object> params; // 路由参数
// 工具构造方法
public static MessageJumpConfig url(String url) { ... }
public static MessageJumpConfig route(String route) { ... }
public static MessageJumpConfig mini(String path) { ... }
}
针对消息文本中部分文本可点击跳转(类似超链接),这类消息通常称为富文本消息(Rich Text Message):
但不能直接给整个富文本,否则跳转逻辑难以做成标准化,所以我们要做 结构化富文本(Structured Rich Text)。
核心设计方案
将文本拆成多个"片段(Segment)",每个片段可以是;
TEXT:普通文本LINK:可点击的跳转链接
结构如下:
java
public class RichTextSegment {
private RichTextType type; // TEXT / LINK
private String text; // 段落文本
private MessageJumpConfig jumpConfig; // LINK 时使用
}
卡片内容的 text 字段改为支持富文本列表:
java
public class RichTextContent {
private List<RichTextSegment> segments;
}
消息模型中不再直接存展示文本,而是存 可渲染结构:
java
class Message {
...
RichTextContent content;
}
设计价值:
-
支持部分文字跳转
-
支持前端自由渲染
-
后续可扩展为富文本 / 高亮 / icon / 图片
消息支持图片
常见的消息图片分为两种:
- 第一种是在消息最上方,宽度撑开展示,或者展示在左侧或者右侧,这种暂且可以认为是同一种类型,后面可以用布局来切换展示位置;
- 第二种是在消息中间,作为内容的一部分
基于前面的内容,我们来支持这两种样式。首先把图片作为一个对象,定义图片的最小单元:
java
class MessageImage {
private String image;
}
同样,这里再支持图片点击跳转:
java
class MessageImage {
private String url;
private MessageJumpConfig jumpConfig;
}
可以在消息内容中,增加该类型,来支持图片展示:
java
public class RichTextSegment {
private RichTextType type; // TEXT / LINK / IMAGE
private String text; // 段落文本
private MessageJumpConfig jumpConfig; // LINK 时使用
private MessageImage image; // 支持图片
}
同样在消息体中,增加该字段,来实现第一种展示形式:
java
class Message {
...
MessageImage image;
}
布局与样式:模型不关心"长什么样"
面对多种卡片样式,一个常见误区是:在 Message 里加一堆 UI 字段
更优雅的做法,可以提前约定多种卡片结构布局,当需要前端按照什么样式来渲染,返回该样式即可。这里定义一个消息布局:
例如这里约定两种渲染样式:
-
样式一:图片(宽度撑开,可省略)+ 标题(左对齐,可省略)+ 内容(左对齐)+ 时间(右对齐)

-
样式二:标题(左对齐) + 时间(右对齐)+ 内容(左对齐)+ 图片(宽高固定,右对齐)

这里定义消息布局,消息体中增加布局配置支持:
java
enum MessageLayout {
IMAGE_TITLE_TEXT,
TITLE_TEXT_RIMAGE
}
class Message {
...
MessageLayout layout;
}
关键点:
-
模型只描述布局类型
-
具体怎么渲染,由 Renderer 决定
DO / DTO / VO 的分层思考
DO:持久化模型
- 面向存储
- 字段稳定、可索引
DTO:传输与创建
- 用于创建消息
- 不暴露内部字段
- 可封装行为
VO:展示模型
- 已完成渲染
- 直接面向前端
java
class MessageVO {
Long id;
boolean read;
MessageCategory category;
MessageLayout layout;
MessageImage image;
RichTextContent content;
LocalDateTime sendTime;
}
这是隔离变化的关键一环。
哪些设计被我刻意延后了?
在这一阶段,刻意没有:引入 MQ、引入复杂状态机、做多级缓存、过度拆表
原因很简单:模型设计的目标,是承载变化,而不是预判所有变化。
结语:模型不是"想清楚一次",而是"允许演进"
一个好的消息模型,并不是字段多、覆盖全,而是:
- 有清晰边界
- 能容纳不确定性
- 不被某个业务绑死
在下一篇文章中,我会进一步展开:
- 不同消息类型(评论 / 点赞 / @ / 违规 / 活动 / 订单)的建模方式
- Renderer 如何与模型协作
- 消息创建、去重与渲染的完整链路
这是一个从模型走向行为的过程。
如果你正在做类似系统,希望这篇文章能帮你在"加字段之前",多想一步。