diff --git a/changelogs-v2/2026-06/19_4020_站内信升级聊天P1-1对1会话6个管理端接口-新增接口-管理后台.md b/changelogs-v2/2026-06/19_4020_站内信升级聊天P1-1对1会话6个管理端接口-新增接口-管理后台.md new file mode 100644 index 0000000..96d0b0c --- /dev/null +++ b/changelogs-v2/2026-06/19_4020_站内信升级聊天P1-1对1会话6个管理端接口-新增接口-管理后台.md @@ -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 | 否 | 业务对象 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. 典型调用流程(示例) + +```http +# 1) 打开会话(房务给订单 70123 的定制师留言) +POST /admin/message/chat/open +Authorization: Bearer +{ "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 | +| PR(P1) | https://git.1814.love:8443/wx/HL/pulls/4028 | +| PR(hotfix bean 名冲突) | https://git.1814.love:8443/wx/HL/pulls/4030 | +| PR(hotfix open 幂等语义) | https://git.1814.love:8443/wx/HL/pulls/4031 | +| 后端负责人 | 王骁 |