docs(changelog-v2): 站内信升级聊天 P1 — 6 个管理端 1:1 会话接口 (#4020)

这个提交包含在:
API Changelog Bot 2026-06-19 10:37:30 +08:00
父节点 76b2e4cc0b
当前提交 b23de77d92

查看文件

@ -0,0 +1,197 @@
# 站内信升级聊天1:1 会话P1 — 6 个管理端接口
- **变更类型**:新增接口
- **端类型**:管理后台
- **日期**2026-06-19
- **Issue**[#4020](https://git.1814.love:8443/wx/HL/issues/4020)
- **PR**[#4028](https://git.1814.love:8443/wx/HL/pulls/4028)+ hotfix [#4030](https://git.1814.love:8443/wx/HL/pulls/4030) / [#4031](https://git.1814.love:8443/wx/HL/pulls/4031)
- **后端负责人**:王骁
---
## ⚠️ 关键说明(前端务必先读)
1. **本期 P1 没有实时推送SSE**。聊天靠「发消息 + 拉线程 + 轮询未读」的请求/响应模式工作;实时角标冒泡(顶部铃铛实时 +1、打开的会话 live append 新气泡)是 **P2** 再上。P1 阶段前端可用**轮询**(如进入会话时拉线程、定时拉 `unread-total`)先把界面跑起来。
2. **能力归属 user-service,复用现有站内信**。聊天行和系统通知行同表(`admin_message`),顶部**合并未读角标**(聊天+通知合计)继续走现有 `GET /admin/message/unread-count`,本次**不变**;本期新增的 `GET /admin/message/chat/unread-total` 只给「聊天 Tab」单独展示聊天未读数。
3. **一个订单 × 一对人 = 一个对话框**(不是整单一个群)。会话靠 `conversationKey` 唯一定位,订单 ID 已编入键;订单详情页「围绕这个订单的全部对话」= 调会话列表接口按 `bizModule=HOUSE&bizId=订单ID` 过滤。
4. **前端不传 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 | 否 | 业务对象 idHOUSE=订单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. 典型调用流程(示例)
```http
# 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 实时(角标实时刷 + 打开会话 live append + URGENT 强提示)+ 敏感词词库完善 | ⏳ 待开发 |
| P3 | 房务order-v3接入配房页开会话 + 订单列表「未读红点」透传 | ⏳ 待开发 |
| P4 | 车务fleet接入 | ⏳ 待开发 |
---
## 8. 注意事项
1. **P1 无实时**:前端先用轮询(拉线程 + 定时拉 `unread-total`)做交互;不要等 SSE,SSE 在 P2。
2. **会话键透传**`conversationKey` 含冒号,作为 path 参数直接用即可encodeURIComponent 也兼容)。
3. **雪花 id 用字符串**`peerAdminId` / `messageId` / `conversationKey` 内嵌的 adminId 均以字符串处理,禁做数值运算(防精度丢失)。
4. **顶部角标不变**:继续用 `GET /admin/message/unread-count`(合并聊天+通知);本期 `unread-total` 仅细分聊天数。
5. **重复 open 安全**:同两人同对象重复 open 返回同一会话(`isNew=false`),可放心在进入会话时每次调用。
---
## 9. 关联 / 联系人
| 项目 | 链接 |
|------|------|
| Issue | https://git.1814.love:8443/wx/HL/issues/4020 |
| PRP1 | https://git.1814.love:8443/wx/HL/pulls/4028 |
| PRhotfix bean 名冲突) | https://git.1814.love:8443/wx/HL/pulls/4030 |
| PRhotfix open 幂等语义) | https://git.1814.love:8443/wx/HL/pulls/4031 |
| 后端负责人 | 王骁 |