13 KiB
站内信升级聊天(1:1 会话)P1 — 6 个管理端接口
🛑 前端暂不需要接入(2026-06-19 更新,最高优先级)
本次仅「后端 P1」就绪,前端聊天 UI 集成暂缓——收到本文先不用动工。 本文档是后端接口的提前留档/参考,不是开工通知;待产品排期、正式通知前端接入时再据此对接。下方接口说明仅供后端联调与未来参考。
⚠️ 关键说明(前端正式接入时再读)
- 本期 P1 没有实时推送(SSE)。聊天靠「发消息 + 拉线程 + 轮询未读」的请求/响应模式工作;实时角标冒泡(顶部铃铛实时 +1、打开的会话 live append 新气泡)是 P2 再上。前端正式接入时可用轮询(如进入会话时拉线程、定时拉
unread-total)先把界面跑起来。 - 能力归属 user-service,复用现有站内信。聊天行和系统通知行同表(
admin_message),顶部合并未读角标(聊天+通知合计)继续走现有GET /admin/message/unread-count,本次不变;本期新增的GET /admin/message/chat/unread-total只给「聊天 Tab」单独展示聊天未读数。 - 一个订单 × 一对人 = 一个对话框(不是整单一个群)。会话靠
conversationKey唯一定位,订单 ID 已编入键;订单详情页「围绕这个订单的全部对话」= 调会话列表接口按bizModule=HOUSE&bizId=订单ID过滤。 - 前端不传 adminId / 姓名。当前操作人由网关注入的 token 解析;发件人姓名后端快照写入,直接展示。
1. 背景
房务(定制师↔房务·配房沟通)、车务(车管↔定制师·派车沟通)都需要内部员工之间的 1:1 实时聊天。后端把现有「单向系统通知站内信」升级为「通知 + 1:1 聊天」统一消息中心:同一套收件箱 / 未读 / (未来)实时通道,新增「谁发的 + 成线程 + 双向」三要素。
本期 P1 交付 6 个管理端接口(admin 后台用),是聊天的核心读写能力。SSE 实时(P2)、跨服务接入与业务列表红点透传(P3 房务 / P4 车务)后续迭代。
2. 变更清单
| 序号 | 接口 | 说明 | 变更类型 |
|---|---|---|---|
| 1 | POST /admin/message/chat/open |
打开 / 找回 1:1 会话(幂等) | ✨ 新增接口 |
| 2 | GET /admin/message/chat/conversations |
我的会话列表(支持按订单聚合) | ✨ 新增接口 |
| 3 | GET /admin/message/chat/{conversationKey}/messages |
拉线程消息(游标分页) | ✨ 新增接口 |
| 4 | POST /admin/message/chat/{conversationKey}/messages |
发一条消息 | ✨ 新增接口 |
| 5 | POST /admin/message/chat/{conversationKey}/read |
标记已读到最新 | ✨ 新增接口 |
| 6 | GET /admin/message/chat/unread-total |
我的聊天未读总数 | ✨ 新增接口 |
- 认证:全部 Bearer JWT(管理后台 Token),经网关
https://api.test.1814.love:9443。 - 网关路由:复用现有
/admin/message/**路由,无新增。 - DDL:
admin_message扩 8 列 + 新表admin_conversation_member(前端无感)。 - 顶部合并角标:仍走
GET /admin/message/unread-count,本次不变。
3. 核心概念:conversationKey(会话键)
conversationKey = {bizModule}:{bizId}:{minAdminId}:{maxAdminId}
例: HOUSE:70123:1001:2046065465288949761
bizModule:HOUSE(房务)/FLEET(车务)/DIRECT(纯私聊,无业务对象)。bizId:业务对象 id(房务=订单ID,车务=派车单/车辆ID);DIRECT时后端按0处理。- 两人 adminId 小的在前(后端规范化),保证 A↔B 双向命中同一会话,不分叉。
- 前端拿到
conversationKey后,作为路径参数透传(含冒号,直接放进 URL path,无需手动编码也可;若用 encodeURIComponent 编码同样可用)。
4. 接口详情
4.1 POST /admin/message/chat/open — 打开 / 找回会话
幂等:同两人同业务对象重复调用返回同一个 conversationKey(isNew=false),不会重复建会话。
入参(RequestBody)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
bizModule |
String | 是 | HOUSE / FLEET / DIRECT |
bizId |
String | 否 | 业务对象 id(HOUSE=订单ID;DIRECT 留空) |
peerAdminId |
String | 是 | 对端员工 adminId(雪花,字符串透传) |
出参 data
| 字段 | 类型 | 说明 |
|---|---|---|
conversationKey |
String | 规范化会话键 |
peerAdminId |
String | 对方 adminId |
peerName |
String | 对方姓名快照 |
peerRole |
String | 对方角色(P1 admin 端开会话暂为 null,P3 业务侧接入后带值) |
unreadCount |
Integer | 我在该会话的未读数 |
isNew |
Boolean | true=新建,false=找回已存在 |
4.2 GET /admin/message/chat/conversations — 我的会话列表
| 查询参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
bizModule |
String | 否 | 过滤业务分类,空=全部 |
bizId |
Long | 否 | 过滤业务对象。传 bizModule=HOUSE&bizId=订单ID 即「围绕这个订单的全部对话」 |
pageNo |
Integer | 否 | 页码,默认 1 |
pageSize |
Integer | 否 | 每页,默认 20 |
返回分页 data.records[],每项:conversationKey / bizModule / bizId / peerAdminId / peerName / peerRole / unreadCount / lastMessagePreview / lastMessageAt / status,按 lastMessageAt 倒序。
4.3 GET /admin/message/chat/{conversationKey}/messages — 拉线程(游标分页)
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
conversationKey |
Path | String | 是 | 会话键 |
beforeId |
Query | String | 否 | 游标:拉该 messageId 之前的历史(首屏不传=拉最新一页) |
pageSize |
Query | Integer | 否 | 每页,默认 20 |
返回 data:conversationKey / hasMore / nextCursor(下次 beforeId)/ list[]。每条消息:messageId / senderAdminId / senderName / senderRole / msgType / priority / content / isMine(是否我发的,用于左右气泡)/ sentAt。列表按时间正序(从旧到新)。
4.4 POST /admin/message/chat/{conversationKey}/messages — 发一条
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
content |
String | 是 | 文本内容(≤ 1000 字;命中本地敏感词后端替换为 *,不报错) |
priority |
String | 否 | NORMAL(默认)/ URGENT(加急;P1 不触发实时强提示,P2 起生效) |
msgType |
String | 否 | TEXT(默认)/ IMAGE(content 存图 URL) |
返回 data:messageId / conversationKey / sentAt。前端不传发件人/收件人,后端按会话键解析。
4.5 POST /admin/message/chat/{conversationKey}/read — 标记已读到最新
无请求体。把该会话标记为已读到最新:该会话未读清零、已读水位推进、对应站内信行置已读(顶部合并角标相应回落)。返回 data:conversationKey / unreadCount(=0)/ lastReadMessageId。
4.6 GET /admin/message/chat/unread-total — 我的聊天未读总数
无参数。返回 data.chatUnreadTotal(仅 kind=CHAT 的未读数,供「聊天 Tab」单独展示)。
顶部合并角标(聊天+通知)请继续用
GET /admin/message/unread-count,本端点不替代它。
5. 错误码
| 错误码 | 说明 | 触发 |
|---|---|---|
281002 |
无权访问该会话 | 操作人不是该会话成员(越权) |
281005 |
不能与自己创建会话 | open 时 peerAdminId == 当前登录人 |
281010 |
目标员工不存在或已离职 | open 的 peerAdminId 无效 |
281011 |
消息内容不合法 | content 为空或超 1000 字 |
281004 |
发送过于频繁,请稍后再试 | 发消息超限流(每人 20 条/分钟) |
HTTP 恒 200,业务结果看
code(成功code=200、success=true)。
6. 典型调用流程(示例)
# 1) 打开会话(房务给订单 70123 的定制师留言)
POST /admin/message/chat/open
Authorization: Bearer <admin-token>
{ "bizModule": "HOUSE", "bizId": "70123", "peerAdminId": "2046065465288949761" }
--> data: { "conversationKey": "HOUSE:70123:1001:2046065465288949761", "peerName": "cz", "isNew": true, "unreadCount": 0 }
# 2) 发消息
POST /admin/message/chat/HOUSE:70123:1001:2046065465288949761/messages
{ "content": "标间超预算了,客人要不要升一档?", "priority": "NORMAL" }
--> data: { "messageId": "2067795811529314306", "sentAt": "2026-06-19 10:25:13" }
# 3) 拉线程
GET /admin/message/chat/HOUSE:70123:1001:2046065465288949761/messages?pageSize=20
--> data.list[]: 含双方消息,每条带 senderName / isMine
# 4) 标记已读
POST /admin/message/chat/HOUSE:70123:1001:2046065465288949761/read
--> data: { "unreadCount": 0, "lastReadMessageId": "2067795811529314306" }
7. 本期范围与后续
| 阶段 | 内容 | 状态 |
|---|---|---|
| P1 | 6 个 admin 端点(open / 列表 / 线程 / 发消息 / 已读 / 聊天未读) | ✅ 已上测试服 |
| P2 | SSE 实时推送(CHAT 走新增 im-chat 事件,含 URGENT priority)+ 敏感词 DFA(Hutool) |
✅ 已上测试服(后端就绪,前端接入时用,见下「SSE 实时契约」) |
| P3 | 房务(order-v3)接入:房务「我的接单」列表 + 订单详情的 unreadMessageCount 接真聊天未读(经 internal Feign 透传,原占位恒 0) |
✅ 已上测试服(后端透传就绪,见下「P3 房务红点」) |
| P4 | 车务(fleet)接入 | ⏳ 待开发 |
P3 房务红点(后端已就绪)
房务「我的接单」列表项 HouseMyOrderItemRespVO.unreadMessageCount 与订单详情 tabCounts.unreadMessageCount 从此返真聊天未读(此前恒 0 占位):order-v3 服务端组装列表/详情时经 Feign 调 user-service POST /internal/message/chat/biz-unread-batch 批量取「当前登录房务在该订单(bizModule=HOUSE)上的会话未读」。前端无需改调用——字段同名,值从 0 变真;可据 unreadMessageCount > 0 显红点、按它排序(列表 sortBy=unreadMessageCount,desc 已支持)。user 不可达时降级回 0,不阻断列表。仍待前端排期接入展示。
SSE 实时契约(P2 后端已就绪,前端接入时用)
复用现有 SSE 长连接 GET /ws/admin-msg/stream(EventSource,网关注入鉴权),不新增连接。一条连接上按 event 名分发:
message事件(现有系统通知,行为不变):前端原有监听不用动。im-chat事件(P2 新增,聊天专用):对端在线时实时收到,payload:
{
"type": "CHAT",
"adminId": "收件方adminId",
"unreadCount": 9, // 合并未读(聊天+通知),直接刷顶部角标
"conversationKey": "HOUSE:70123:101:205",
"senderName": "房务·小呼",
"preview": "标间超预算了…",
"priority": "URGENT" // URGENT 时前端做强提示(弹窗/声音)
}
前端用法:监听 im-chat → unreadCount 刷顶部角标;conversationKey 命中当前打开的会话则 live append 新气泡;priority=URGENT 强提示。P2 是后端能力,前端接入仍待排期通知。
8. 注意事项
- P1 无实时:前端先用轮询(拉线程 + 定时拉
unread-total)做交互;不要等 SSE,SSE 在 P2。 - 会话键透传:
conversationKey含冒号,作为 path 参数直接用即可(encodeURIComponent 也兼容)。 - 雪花 id 用字符串:
peerAdminId/messageId/conversationKey内嵌的 adminId 均以字符串处理,禁做数值运算(防精度丢失)。 - 顶部角标不变:继续用
GET /admin/message/unread-count(合并聊天+通知);本期unread-total仅细分聊天数。 - 重复 open 安全:同两人同对象重复 open 返回同一会话(
isNew=false),可放心在进入会话时每次调用。
9. 关联 / 联系人
| 项目 | 链接 |
|---|---|
| Issue | wx/HL#4020 |
| PR(P1) | wx/HL#4028 |
| PR(hotfix bean 名冲突) | wx/HL#4030 |
| PR(hotfix open 幂等语义) | wx/HL#4031 |
| 后端负责人 | 王骁 |