Session、Message 与通知
oak-general-business 里有两套容易混淆的“消息”:
- 一套是围绕
Session/SessionMessage的会话消息,更像客服、聊天或对象会话; - 另一套是围绕
Message/Notification的系统消息,更像站内信、模板通知或跨渠道推送。
把这两套能力拆开理解,是读懂这一章的关键。
主要对象
这部分实际涉及的实体不少:
Session:一条会话,绑定到某个entity/entityId;SessionMessage:会话里的具体消息;Message:系统级消息;Notification:消息在具体渠道上的发送记录;MessageType:业务消息类型;MessageTypeTemplate:消息类型和微信模板的映射;MessageTypeSmsTemplate:消息类型和短信模板的映射。
从职责上可以先这样记:
Session/SessionMessage负责“人与对象之间的会话过程”;Message/Notification负责“系统要发出去的一条通知”。
组件
这部分已经带了不少现成组件:
src/components/session/listsrc/components/session/cellsrc/components/session/headersrc/components/session/forMessagesrc/components/session/messageNumbersrc/components/session/sessionMessagesrc/components/message/listsrc/components/message/detailsrc/components/message/cellsrc/components/message/simpleListsrc/components/my/messagesrc/components/sessionMessage/listsrc/components/sessionMessage/upsertsrc/components/sessionMessage/cellsrc/components/messageTypeTemplate/listsrc/components/messageTypeSmsTemplate/listsrc/components/messageTypeSmsTemplate/tab
也就是说,这部分不仅有实体和后台逻辑,连前端展示层都已经准备了不少基础积木。
aspect / endpoint / feature
这部分直接暴露的后端 aspect 主要是:
createSession
它定义在 src/aspects/session.ts,用于按当前应用类型和消息来源创建或获取会话,并在需要时级联创建 sessionMessage。
这一章没有独立的 session endpoint 或 message endpoint,但它会通过微信相关 endpoint 间接进入。最典型的入口就是 src/endpoints/wechat.ts,公众号或小程序消息回调最终会调用 createSession(...)。
此外,这一章还会和 features.template 发生关系。features.template 并不直接发送消息,但会负责:
- 获取消息类型列表;
- 同步微信模板;
- 同步短信模板。
所以消息类型和模板映射虽然在数据层属于“消息域”,但真正的同步入口在 Template feature 和对应 aspect 里。
后台规则
这一章的核心后台逻辑主要分布在几组 trigger / checker 文件里:
src/triggers/session.tssrc/triggers/sessionMessage.tssrc/triggers/message.tssrc/triggers/notification.tssrc/checkers/message.ts
默认行为包括:
- 创建
session时,通知订阅了 session 列表变化的前端事件; - 创建
sessionMessage时,更新关联session.lmts; - 创建
sessionMessage后,在提交事务后再做消息推送。
而 Message / Notification 这一支的规则是:
- 创建
message时,会先根据userSystem、消息权重、限制渠道等信息自动拆出notification; - 创建
notification后,在事务提交成功之后才真正发送; notification成功或失败后,会继续反向更新message.iState;medium权重消息在其它渠道都失败时,还会尝试补发短信;- 非 root 场景下查询
message时,checkers/message.ts会自动把结果收窄到当前systemId可见的数据。
这也是 Oak 的一个典型用法:真正需要推送外部消息的逻辑,放在提交之后,而不是直接塞进前端组件里。
注入点
这一章没有单独的 session feature,它的注入方式主要是:
- 后端通过
ogb0Triggers注入会话和消息的自动维护规则; - 前端组件直接围绕实体数据树工作;
- 模板同步能力通过
features.template注入。
这说明它更像一组已经接到 Oak 运行时上的基础设施,而不是一套必须从某个总入口手动初始化的业务模块。
项目中如何接入
Session 这章在项目里最常见的接法有三种:
- 做客服/私信后台时,直接复用
src/components/session/*和src/components/sessionMessage/* - 做微信消息接入时,让
wechat endpoint自动调用createSession(...)生成或更新会话 - 做系统通知中心时,直接创建
message记录,让message / notification触发链自动拆渠道并发送
换句话说,这一章通常不是“项目自己设计会话模型”,而是“项目只决定从哪里进入、展示在哪个页面”。
session/list 常用参数
oak-general-business/src/components/session/list/index.ts 这一组组件其实比看起来更灵活。项目里最常用的参数有:
entityFilterentityFilterSubStrentityDisplayentityProjectionsessionIddialogonItemClick
其中有两种非常不同的工作模式:
- 不传
entityFilter这时组件会默认按当前 userId查自己的会话列表,并订阅${DATA_SUBSCRIBER_KEYS.sessionList}-u-${userId}这类数据事件; - 传
entityFilter这时它会按业务对象维度查会话,并根据entityFilterSubStr订阅对应的数据事件。
entityDisplay + entityProjection 这组参数也很关键。它们决定“列表上显示的是用户名字,还是某个业务对象名字”。如果你是在做“对象会话列表”而不是“我的私信列表”,通常就应该一起传这两个参数,让组件把会话里关联对象名称补出来。
还有两个很实用的行为:
- 如果父层传了
sessionId,组件会默认把这一条设成选中态; - 如果传了
onItemClick,点击会话后不会直接跳/session/sessionMessage,而是把控制权交给父组件。
这意味着 session/list 既可以做独立页左侧会话栏,也可以做弹窗里的嵌入式会话选择器。
session/forMessage 的真实职责
这组组件在原文里还没展开,但它非常适合做“消息页顶部的当前会话头”。oak-general-business/src/components/session/forMessage/index.ts 当前最关键的参数是:
sessionIdisEntityentityDisplayentityProjection
它的真实行为是:
- 如果传了
sessionId,就直接从 cache 里把这条session及其用户 / 关联对象信息读出来; - 如果
sessionId发生变化,会自动重新取当前会话; isEntity = true时,标题优先按会话里的用户信息显示;isEntity = false时,则会调用你传入的entityDisplay([session])来决定显示名。
也就是说,这个组件的价值不是“再包一个 header”,而是把“当前聊天对象叫什么”这件事统一收口了。
my/message 的真实职责
如果只是做个人中心里的“未读消息入口”,通常没必要直接挂整页消息列表,my/message 就够了。这个组件的真实行为很集中:
- 进入时直接按当前登录用户
userId统计message.visitState === 'unvisited' - 维护一个未读数量
count - 点击后默认跳
/message/list
也就是说,它更像一个“消息角标入口组件”,适合放在:
- 我的页面
- 个人中心快捷入口区
- TabBar 上方的消息入口卡片
而真正的消息列表页、消息详情页,再分别交给 message/list、message/detail。
真实项目里的启动注册
这一块最值得直接参考 haina-busi 和 taicang 的例程:
haina-busi/src/routines/messageType.tstaicang/src/routines/registMessageType.ts
它们都会先执行:
registerMessageType(Object.keys(MessageTypes));
而 haina-busi/src/routines/start.ts 还继续注册了:
registerMessageHandler('wish', wishHandler);
registerNotificationHandler('wish', wishNotificationHandler);
这说明公共包把“消息对象、会话对象、通知链”准备好了,但真正有哪些消息类型、由谁来发送、由谁来处理,仍然应该由项目层在启动时注入。
组件适合落在哪里
从 taicang 的现有页面看:
message/list适合放在管理台消息列表页message/detail适合放在消息详情页- 会话相关组件更适合放在客服页、公众号消息页、系统通知页
项目层通常只做路由、筛选条件和业务跳转,不建议重写消息列表本身。
如果再按组合关系细一点拆,会更接近真实项目:
- 左侧会话栏:
session/list - 顶部当前会话信息:
session/forMessage - 中间消息流:
sessionMessage/list - 底部发送框:
sessionMessage/upsert - 个人中心未读入口:
my/message
这样拼出来的聊天页,基本就是公共包当前已经准备好的标准组合。
使用示例
1. 在微信回调里自动创建或更新会话
oak-general-business/src/endpoints/wechat.ts 会在收到公众号或小程序消息时调用:
await createSession(
{
data,
type: 'wechatPublic',
entity: 'application',
entityId: applicationId,
},
context
);
这一步会自动查找已有 session,必要时补出新的 sessionMessage 和 extraFile。
2. 客服页直接复用会话组件族
如果项目要做一个标准的聊天后台,最推荐的组合是:
src/components/session/listsrc/components/session/sessionMessagesrc/components/sessionMessage/listsrc/components/sessionMessage/upsert
这样会话列表、消息列表、消息发送框、图片上传和数据订阅都能直接复用,不需要项目层自己重新拼一套。
3. 创建系统消息,让通知链自动工作
如果你要给用户发系统通知,项目层更推荐直接创建 message,而不是自己手工落 notification:
await context.operate('message', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
entity: 'order',
entityId: orderId,
userId,
type: 'orderPaid',
weight: 'high',
title: '订单已支付',
content: '你的订单已经支付成功',
channels: ['wechatPublic', 'email'],
},
}, {});
后续的 notification 创建、提交后发送、状态回写,都会由 triggers/message.ts 和 triggers/notification.ts 自动完成。
使用建议
最简单的判断标准是:
- 需要围绕某个对象做持续会话,用
Session/SessionMessage; - 需要给用户发系统通知或跨渠道消息,用
Message/Notification; - 需要做模板映射,就把业务消息类型维护在
MessageType一侧,再绑定微信模板或短信模板。
这样拆开以后,后续对微信、短信等具体渠道的理解也会清晰很多。