IM、离线消息体设计

773 阅读4分钟

前两个星期接了网易云信的 IM,用于给自己系统 IM 发消息用,思考了下消息体的结构,写篇文章记录下。

@Data
@SuperBuilder
@NoArgsConstructor
public class ChatMsgAttachmentV2 {
    @ApiModelProperty(notes = "消息ID")
    private String msgId;
    @ApiModelProperty(notes = "消息类型 chat单聊/notice通知/broadcast广播/...")
    private String msgType;
    @ApiModelProperty(notes = "消息类型 instead代发(系统代替用户发送)/expand扩展()...")
    private String msgSubType;
    @ApiModelProperty(notes = "消息内容类型 决定下面bean的抽象结构 text文本/image图片/order订单/goods商品/miniprogram小程序/...详见ContentTypeEnum")
    private String contentType;
    @ApiModelProperty(notes = "消息内容类型 决定下面bean的实体结构 详见AbsContentData类中定义")
    private String contentSubType;
    @ApiModelProperty(notes = "业务类型 user用户/order订单/activity活动/...")
    private String businessType;
    @ApiModelProperty(notes = "触发事件 user_follow用户关注/order_create订单创建/order_pay订单支付/first_chat首次聊天...")
    private String triggerEvent;
    @ApiModelProperty(notes = "发送人id")
    private Long senderId;
    @ApiModelProperty(notes = "接收人id")
    private Long receiverId;
    @ApiModelProperty(notes = "会话ID")
    private String conversationId;
    @ApiModelProperty(notes = "发送时间戳(毫秒)")
    private Long timestamp;

    @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,                     // 类型标识使用类型名
            include = JsonTypeInfo.As.EXTERNAL_PROPERTY,    // 类型标识在外层 JSON 属性中
            property = "contentSubType"                     // 类型标识字段名称
    )
    @JsonSubTypes({
            // 新增的类型一定要补充!!!否则前端无法自动序列化
            @JsonSubTypes.Type(value = BaseTextContentData.class, name = "base_text"),//基础文本
            @JsonSubTypes.Type(value = ChatTextContentData.class, name = "chat_text"),//聊天文本
            @JsonSubTypes.Type(value = GreetChatTextContentData.class, name = "greet_chat_text"),//初次打招呼聊天文本
            @JsonSubTypes.Type(value = ConcernTextContentData.class, name = "concern_text"),//关注消息 模拟系统账号发送的聊天消息
            @JsonSubTypes.Type(value = AuditTextContentData.class, name = "audit_text"),//审核文本
            @JsonSubTypes.Type(value = SysConversationTextContentData.class, name = "sys_conversation_text"),//会话列表两个系统会话
            @JsonSubTypes.Type(value = ChatImageContentData.class, name = "chat_image"),//聊天图片
            @JsonSubTypes.Type(value = GoodsContentData.class, name = "goods"),//商品
            @JsonSubTypes.Type(value = OrderContentData.class, name = "order")//订单
    })
    @ApiModelProperty(notes = "消息体")
    private AbsContentData contentData;
}

逐一分析分段,解释为什么这么设计。

msgId

就是消息 id,保证全局唯一,用雪花算法生成。

msgType

消息类型 chat单聊/notice通知/broadcast广播/...。

我的业务场景涉及订单变动的通知,xxx 关注了你这些,可以用这个字段和 IM 聊天里面的消息做区分。

IM 消息点击要进聊天页面,而系统消息点击,进的是系统消息列表。

msgSubType

消息类型 instead代发(系统代替用户发送)/expand扩展()...

这个字段有意思,我把它设计成更消息子类型。我最初设计的场景是,如果检测到用户发的消息有诈骗风险,就会提醒对方,但是我不想使用系统消息插入到两个人的会话,这对 conversationId 有影响。

所以解决方案要么使用代发,比如系统使用 A 给 B 再发一条风险提醒;或是扩展消息,把原消息额外加个扩展,自动更改为扩展消息结构体。代发是更好的解决方案。

但是为了这个场景,额外新增了一个字段,值得吗?代发的场景也不适用于其他分类,把代发归属于哪一层,是个问题。除了风险消息,还有自动打招呼消息、离线自动回复消息,都是代发。

或许可以考虑放到 triggerEvent。嗯,好像可行。到底是什么场景触发的代发,要更分得更细,不能笼统的称之为代发。可以具体拆成聊天风险、自动打招呼、离线自动回复等等。

如果是风险,可以让前端根据 contentSubType 展示文本、或是弹窗、或是图片等展示形式。

我觉得这里的设计还不够优雅,有经验的小伙伴可以一起评论区讨论下。

contentType

消息内容类型 决定下面bean的抽象结构 text文本/image图片/order订单/goods商品/miniprogram小程序/...详见ContentTypeEnum。

这里仅仅是抽象结构,还不是具体的结构。

contentSubType

消息内容类型 决定下面bean的实体结构 详见AbsContentData类中定义。

根据 contentType 的具体实现,应对后续可能的所有的数据结构,而且也能根据不同的 contentType 做业务。比如离线推送的时候,如果上文本则展示具体的文本内容;如果是图片则展示图片消息等。

前端的展示样式,也由这个字段控制,不要放到其他字段,统一约束,后期好扩展和维护。

businessType

业务类型 user用户/order订单/activity活动/...。

这是个业务保留字段,暂时没有使用场景。

triggerEvent

触发事件 user_follow用户关注/order_create订单创建/order_pay订单支付/first_chat首次聊天...

这也是保留字段,暂时没有使用场景。

后面几个就不说了,都能看懂,发送人、接收人、会话id、发送时间。

contentData

这个具体的消息体,他是个抽象类,有很多实现,根据不同的 contentSubType 序列化成不同的类型,用的 JackSon 的 JsonTypeInfo 和 JsonSubTypes 和注解,如果系统内要序列化,记得用 JackSon,比如从数据库查出来转成 Bean 的时候。

我在设计的时候,看到有人用不同的字段接受消息内容,比如文本消息叫 textBean,图片消息叫 imgBean,但我这里设计成统一叫 contentData,用 contentSubType 区分。

因为我这类型特别多,如果用不同的属性,那每次都要改动 ChatMsgAttachmentV2,违反开闭原则。