186 行
18 KiB
Markdown
186 行
18 KiB
Markdown
# 房务「联系房务」面板真实化:真实在线态 + 抢单前发消息 + claimerId(已上线测试服·可对接)
|
||
|
||
> 变更类型:✅ 新增/修改接口(已部署测试服并 API 验证,可对接)
|
||
> 端类型:管理后台(订单详情·定制师侧「联系房务」聊天面板)
|
||
> 日期:2026-06-22 | 工单:#4223 | PR:#4247 | 服务:hl-user-service(聊天/在线态)、hl-order-service-v3(订单详情/抢单)
|
||
> 关联:本条**取代**同日「需求澄清·实现中」版(那版的"先别删在线态"结论已落地为下方真实契约)。
|
||
|
||
---
|
||
|
||
## 背景
|
||
此前「联系房务」面板的房务专员/在线态是前端假数据。wx 拍板:**在线/离线要做成真的**(不是删功能),并加「抢单前也能发消息、抢单后一并送达房务」。后端已补真实能力,本条是正式对接契约。
|
||
|
||
## 三态总览(前端按此渲染)
|
||
| 抢单状态 | 房务专员 | 在线态 | 发消息 |
|
||
|---|---|---|---|
|
||
| **抢单前**(claimerId=null) | 显示「未知」 | 不显示 | **可发**,存订单房务会话,抢单后送达 |
|
||
| **抢单后**(claimerId 有值) | 真实接单人姓名 | **真实在线/离线**(peerOnline) | 正常聊 |
|
||
| **claimerId==当前登录人**(你既定制师又抢了本单房务) | 不显示「联系房务」 | — | 不能跟自己聊 |
|
||
|
||
## 1. 订单详情新增 `claimerId`
|
||
`GET /v3/admin/order/{id}/itinerary` 的住宿需求摘要 `hotelRequirementBrief` **新增** `claimerId`(接单房务 adminId,**String** 雪花;未抢单为 `null`)。
|
||
- `claimerId == null` → 前端房务专员显示「**未知**」、不显示在线点、「联系房务」可引导但开的是 pending 会话(见 §3)。
|
||
- `claimerId == 当前登录 adminId` → 前端**隐藏/置灰**「联系房务」(本单房务是你自己)。
|
||
|
||
## 2. 聊天出参新增 `peerOnline`(真实在线态)
|
||
`POST /admin/message/chat/open`、`/open-house`、`GET /admin/message/chat/conversations` 出参新增 `peerOnline`(Boolean)。
|
||
- 语义:对方(房务专员)**当前登录后台且有活跃实时(SSE)连接 = 在线**;离线靠 Redis TTL 反映(**near-live,最多约 45s 延迟**,本期不做在线态即时推送)。
|
||
- 前端:`peerOnline==true` 绿点在线 / `false` 灰点离线;抢单前(peer 为占位)恒 false、配合「未知」不显示状态即可。
|
||
|
||
## 3. 新端点 `POST /admin/message/chat/open-house`(HOUSE 会话订单维度)
|
||
**HOUSE 房务会话改为订单维度单会话**(键 `HOUSE:{orderId}`,一单一条;**「联系房务」与「联系定制师」收敛到同一条**,不再分叉)。FLEET/DIRECT 不变。
|
||
|
||
`POST /admin/message/chat/open-house`
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| orderId | String | ✅ | 订单 id(房务会话维度) |
|
||
| peerAdminId | String | 否 | 对端 adminId:**抢单前定制师开聊不传**(pending);抢单后传 `claimerId`(定制师→房务)或 `consultantId`(房务→定制师) |
|
||
|
||
出参(实测):`{conversationKey, peerAdminId, peerName, peerRole, peerOnline, unreadCount, isNew}`。
|
||
- 抢单前不传 peerAdminId → 返 `peerAdminId:0, peerName:null, peerRole:"HOUSE", peerOnline:false`(占位「待接单房务」),定制师即可发消息(走现有 `POST /chat/{conversationKey}/messages`)。
|
||
- `peerAdminId==自己` → 281005(不能跟自己开会话)。
|
||
|
||
## 4. 抢单前发消息 · 抢单/转单后送达(后端自动,前端无需特殊处理)
|
||
- 抢单前定制师在 `HOUSE:{orderId}` 会话发的消息先暂存。
|
||
- 房务**抢单**后:后端自动把历史消息变成该房务的未读 + SSE `im-chat` 通知;定制师/房务双方此后正常聊。
|
||
- **转单**:会话历史跟随交给新房务(前端拉 `GET /chat/{conversationKey}/messages` 即见全历史)。
|
||
- 边界(定制师==房务本人抢单):消息仅作历史、不自发未读。
|
||
- 前端只需:抢单前用 open-house(pending) 让定制师能发;抢单后用 `claimerId` open-house 正常聊。**送达逻辑后端全包**。
|
||
|
||
## 5. curl 实测(2026-06-22 测试服,过网关 9443)
|
||
```bash
|
||
# A) 订单详情 claimerId
|
||
curl 'https://api.test.1814.love:9443/v3/admin/order/2068234602970828802/itinerary' -H 'Authorization: Bearer <token>'
|
||
# → 200, hotelRequirementBrief.claimerId 字段存在(未抢单为 null)
|
||
|
||
# B) open-house(抢单前 pending)
|
||
curl -X POST 'https://api.test.1814.love:9443/admin/message/chat/open-house' \
|
||
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' -d '{"orderId":"2068234602970828802"}'
|
||
# → 200 {"conversationKey":"HOUSE:2068234602970828802","peerAdminId":0,"peerName":null,
|
||
# "peerRole":"HOUSE","peerOnline":false,"unreadCount":0,"isNew":true}
|
||
```
|
||
|
||
## 6. 前端处理建议
|
||
- 房务专员名:`claimerId` 空→「未知」;非空→真实姓名(姓名走会话 `peerName`)。
|
||
- 在线点:读 `peerOnline`(抢单后才有意义);抢单前/未知不显示。
|
||
- 「联系房务」:`claimerId==当前人`→隐藏;否则 open-house(抢单前不传 peerAdminId、抢单后传 claimerId)。
|
||
- 「联系定制师」(房务侧):改走 open-house(传 consultantId),与「联系房务」同一条订单会话。
|
||
- 删除写死的「舒心 在线」假数据。
|
||
|
||
## 7. 影响 / 回滚
|
||
- 兼容性:均为加字段 / 加端点,旧前端不读新字段不受影响。
|
||
- **无 DB 迁移**(复用 admin_message / admin_conversation_member)。回滚 = revert PR #4247 重新部署 hl-user-service + hl-order-service-v3。
|
||
|
||
---
|
||
|
||
## 8. 补充裁决(2026-06-23 · 复核 + 答前端阻塞,本篇为唯一权威)
|
||
|
||
前端反馈本篇与同日另一篇 `23_定制师侧联系房务…管理后台前端.md` 契约打架(两个 open 端点 + 抢单前/后语义相反),无法切共享聊天基础设施。重新实测测试服后裁决:
|
||
|
||
**① 以本篇(22_4223 / open-house)为唯一权威。** 同日那篇 `23_…` **作废已删**——它基于**落后 90 个提交的 stale 代码**误写(写成 `POST /chat/open` + `bizModule/peerAdminId` + 「抢单后才能聊」),与真实部署不符。
|
||
|
||
**② 端点(实测 live,2026-06-23 过网关 9443)**:
|
||
`POST /admin/message/chat/open-house {"orderId":"…"}` → `200 {"conversationKey":"HOUSE:{orderId}","peerAdminId":0,"peerName":null,"peerRole":"HOUSE","peerOnline":false,"unreadCount":0,"isNew":…}`。
|
||
HOUSE 订单会话**只走 open-house**。通用 `/chat/open`(bizModule+peerAdminId,@NotNull)仍在,但用于 FLEET/DIRECT/显式 peer,**HOUSE 别用它**。
|
||
|
||
**③ 门控(更正「抢单后才能聊」的误解)**:定制师侧「联系房务」**抢单前也能发消息**——open-house 不传 peerAdminId → pending 占位「待接单房务」,消息暂存,房务抢单后后端自动送达。所以**抢单前按钮照常可点可发**,只是对端显「未知/待接单房务」、不显在线点;**不要禁用按钮**。仅 `claimerId == 当前登录人` 时隐藏(自己别跟自己聊)。
|
||
(注:与房务侧「房务管家」未抢单隐藏入口不冲突——房务在抢单池里没理由先聊;能抢单前留言的是**定制师**单向投递。)
|
||
|
||
**④ 在线态怎么拿(答「轮询还是 SSE」)**:读响应里的 `peerOnline`(Boolean)(open-house / conversations 都返)。它是**真实在线态**(后端 `adminPresenceService` Redis presence,对方有活跃后台连接=在线),**near-live、约 45s TTL 延迟**。刷新方式 = **轮询 / 重新拉**(re-open-house 或 re-conversations)。**没有专用在线态 SSE 推送**(本期不做)。消息实时到达走站内信 `im-chat` SSE(与在线态是两回事)。抢单前 peer 占位恒 `false`,配「未知」不显在线点即可。
|
||
|
||
**⑤ FLEET 车务聊天:未切真(deferred)。** 代码实证 `ConversationMemberService` 注「fleet 接入 deferred」——FLEET 会话的订单摘要 / 真实对端整合**没做**。通用 `/chat/open` 虽接受 `bizModule=FLEET`,但**无真实车务数据对接**。**前端 FLEET 聊天先别做切真**,等车务接入工单(wx「车务用了再加」)。mock 里没有 SSE/在线态属正常——FLEET 整条都还没切真。
|
||
|
||
---
|
||
|
||
## 9. 切真细节契约(2026-06-23 实测 origin/dev-v3 + LIVE,答前端 Q1/Q2/Q3)
|
||
|
||
### Q1 · peerAdminId 强不强制?后端解析对端吗?
|
||
- **HOUSE 用 `POST /admin/message/chat/open-house`,`peerAdminId` 选填**(不传 = 抢单前 pending 占位「待接单房务」)。**别用通用 `/chat/open`**(它 `peerAdminId` `@NotNull` 强制,只给 FLEET/DIRECT/显式 peer)。
|
||
- **后端不反查 order-v3 解析对端**(`ChatManager.openHouse` javadoc 明示「前端传 peer,不反查 order-v3」)。对端 id **前端传**,且前端数据里都有:
|
||
- 定制师→房务:`peerAdminId = claimerId`(来自 `hotelRequirementBrief.claimerId`;抢单前为 null → 不传 = pending)。
|
||
- 房务→定制师:`peerAdminId = consultantId`(订单/抢单数据里就有)。
|
||
- 「两端收敛同一条会话」靠键 `HOUSE:{orderId}`(与 peer 无关);但建双方成员行需开方各自传对端 id(抢单前定制师不传 → 房务抢单后 order-v3 afterCommit 自动补房务成员行 + 送达历史,前端无需管)。
|
||
- **FLEET**:无 open-fleet,通用 open 强制 `peerAdminId`,且 FLEET deferred → **先别做**。
|
||
- 可选增强:若要前端只传 orderId、后端按 claim/consultant 自动解析对端 → 需后端加 Feign 反查 order-v3,**独立工单待 wx 定**(当前不做,前端传 id 即可跑通)。
|
||
|
||
### Q2 · 响应字段(实测对齐 #4020,别凭注释)
|
||
- 线程 `GET /admin/message/chat/{conversationKey}/messages` → `{conversationKey, hasMore, nextCursor, list[]}`。**是 `list` 不是 `records`**;`list` 按**时间正序**(旧→新,从上往下渲染);游标 `nextCursor` = 本页最小 messageId,作下次 `beforeId` 上翻历史。
|
||
- 消息项 `list[]`:`{messageId, senderAdminId, senderName, senderRole, msgType(TEXT/IMAGE/SYSTEM), priority(NORMAL/URGENT), content, isMine, sentAt}`。
|
||
- **`isMine`** = Boolean,后端按 `senderAdminId == 当前登录人` 算(左右气泡布局用)。
|
||
- **`sentAt`** = **String,格式 `yyyy-MM-dd HH:mm:ss`**(如 `2026-06-16 11:02:31`)——**不是毫秒、不是 ISO-8601**,本地时间到秒、无时区无毫秒。前端按此格式 parse,别当 epoch/ISO。
|
||
|
||
### Q3 · SSE 实时(**已有且 LIVE,别再轮询**)
|
||
- **SSE 端点 `GET /ws/admin-msg/stream`(`text/event-stream`)实测 LIVE(200 + 正确 Content-Type)**。
|
||
- 发消息:后端 `send` 事务 afterCommit 经 Redis Pub/Sub 广播 **CHAT 信令**给收件方 → SSE 实时冒泡(带 senderName / 预览 / priority + conversationKey)。
|
||
- **前端应订阅 `/ws/admin-msg/stream`,收到 CHAT 信令即 live-append / 刷新对应会话**,替换「发送后短轮询 6 次(~8s)」的兜底。「没有 SSE」= 没接,不是没有。
|
||
|
||
### 在线/离线态走 SSE(wx 2026-06-23 新要求 → 后端工单 **#4273**)
|
||
- 现状:`peerOnline` 是**轮询字段**(Redis presence key TTL 45s + SSE 心跳续期),**在线态变更不推送**。
|
||
- wx 拍:改为 **SSE 实时推在线/离线**。已立后端工单 **#4273**(hl-user-service):SSE 连接建/断 → 标在线/离线 → 向会话对端推 `presence` 事件。**前端届时订阅 presence 事件、停 peerOnline 轮询**(保留字段做首屏兜底)。**✅ 已上线测试服,契约见 §12。**
|
||
|
||
---
|
||
|
||
## 12. ✅ 在线/离线实时 SSE 已上线测试服(2026-06-23 · #4273 · PR #4299 · 已部署实测)
|
||
|
||
`peerOnline` 轮询升级为 **SSE 实时推**。前端订阅后**在线点变更实时刷新,停掉 peerOnline 轮询**(`peerOnline` 仅留作首屏兜底)。
|
||
|
||
- **SSE 事件**:`presence`(与 `im-chat`/`im-chat-read` 同一条流 `GET /ws/admin-msg/stream`,实测 200 `text/event-stream`)。
|
||
- **payload**:`{ "type":"PRESENCE", "presenceAdminId":<Long 在线态翻转的人>, "online":<Boolean> }`(另带 `adminId`=收件方路由,前端忽略)。
|
||
- **语义**:当**与你有活跃会话的某对端**(`presenceAdminId`)上线/下线,你的流就收到一条 `presence` → 把会话列表 / 打开的聊天里**那个人的在线点**实时刷成 `online`。只推「与你有会话关系的人」,不会广播无关员工。
|
||
- **前端**:订阅 `presence` 事件 → `presenceAdminId` 找到对应会话/对端 → 设其在线点=`online`。首屏仍用 `peerOnline`(open-house / conversations 返)兜底,之后全靠 `presence` 事件维持,**不再轮询**。
|
||
- **时延**:上线≈即时(建连即推);下线干净断开≈即时、异常掉线≤25s(心跳探测补推)、45s TTL 兜底。
|
||
- **实测**:部署后 `GET /ws/admin-msg/stream` 200 + `text/event-stream`,服务正常启动(新 presence 广播 bean 无循环依赖)。两端实时翻态属前端联调验。
|
||
|
||
---
|
||
|
||
## 10.「已读」是前端写死的 bug + 抢单前应显「留言」(2026-06-23 wx 评审,🐛 前端改,后端无需动)
|
||
|
||
**现象**(wx 评审,定制师侧「联系房务」面板):订单还在抢单池(无房务接单,对端显「待接单房务」),定制师发「你好 测试消息」,气泡下却显示 `14:05 **已读**`。**没有任何房务接单、对端是占位 0,谁来读?「已读」是错的。**
|
||
|
||
**根因(已对 origin/dev-v3 核实)**:**后端从不返回任何「已读/送达」状态**。线程消息 VO 就 §9 Q2 那 9 个字段 `{messageId, senderAdminId, senderName, senderRole, msgType, priority, content, isMine, sentAt}`——**没有 `isRead`/`readAt`/`delivered`/`status`**。`14:05 已读`是**前端在「我方」(`isMine=true`) 气泡下无条件硬拼的**,后端给不了这个信号,所以它永远是假的。
|
||
|
||
**正确显示(前端按 claim 状态自行渲染,后端已有的字段够用)**:
|
||
| 我方消息所处状态 | 判据(前端已有) | 气泡下应显示 |
|
||
|---|---|---|
|
||
| **抢单前**(pending) | `claimerId == null`(或 open-house 返 `peerAdminId:0`/`peerName:null`) | 「**留言**」或「留言·待房务接单」——**不显已读** |
|
||
| **抢单后** | `claimerId` 有值、对端真实 | 「**已送达**」或**什么都不显**——**不显「已读」** |
|
||
| 对端发来的(`isMine=false`) | — | 不需要已读标 |
|
||
|
||
> 一句话:**把写死的「已读」删掉**。抢单前我方消息本质是**留言**(标「留言」);抢单后后端保证已送达接单房务(可标「已送达」)。**后端无读回执,别再伪造「已读」。**
|
||
|
||
**「留言→抢单后送达」后端全包,无需前端管(§4 重申 + 核实)**:
|
||
- 抢单前定制师发的消息 → 后端存进 pending 桶(收件方=占位 0,**不累加任何人未读、不推 SSE**),就是「留言」。
|
||
- 房务**抢单**那一刻 → 后端 `bindHouseClaimer` 自动把这些 pending 留言**整批改判给抢到的房务** + 置未读 + SSE 通知。**这就是你说的「发的消息抢单后一起把消息发过去」,已经实现。**
|
||
- 所以你测试时「房务管家侧看不到留言」(Image #27) 是**对的**——那单**还在抢单池、没人抢**,留言正确地还压在 pending、尚未送达;**等真有房务点「抢这一单」,留言会连同未读角标一起送到 ta 那边**。
|
||
|
||
**房务管家侧(Image #27)抢单池显「抢单后可与定制师沟通」= 设计内门控**(见 `22_4237` item 1:房务未抢单时隐藏聊天入口)。**保持不变**,与上面定制师侧抢单前能留言不冲突——能抢单前**单向留言**的只有定制师;房务在抢单池里没接单、无身份,自然先不聊。抢单后双方正常互聊。
|
||
|
||
**两件待 wx 拍的「可选增强」(前端不必等,先按上表修假已读)**:
|
||
1. **真·已读回执**(抢单后对方真读了才显「已读」)——需后端把 per-message 已读态(基于会话成员 `lastReadMessageId` 水位)暴露进线程 VO,是独立后端工单。当前最小修 = 前端按上表把假已读改成「留言/已送达」即可。
|
||
2. **抢单池给房务「该单有 N 条定制师留言」预览做诱饵**——需后端在抢单池 VO 补 pending 留言计数,独立后端工单。当前抢单后才送达可见。
|
||
|
||
---
|
||
|
||
## 11. ✅ 真已读回执 + 房务侧留言可见 已上线测试服(2026-06-23 · #4288/#4289 · PR #4294 · 已 API 实测)
|
||
|
||
§10 标的两项 wx 已拍并实现上线,**取代 §10 的「待定/显已送达」口径**:
|
||
|
||
### 11.1 真实「已读/未读」回执(#4289)
|
||
- 线程消息出参(`GET /admin/message/chat/{conversationKey}/messages` 的 `list[]`)**新增 `readByPeer`(Boolean)**:仅我方消息(`isMine=true`)有意义 = 对端已读到本条(对端成员已读水位 >= 本消息 id)。抢单前无对端恒 false;对端发来的消息(isMine=false)此字段无意义。
|
||
- **气泡显示口径(升级版,取代 §10)**:
|
||
|
||
| 我方消息状态 | 气泡下显示 |
|
||
|---|---|
|
||
| 抢单前 | 「留言」 |
|
||
| 抢单后 · `readByPeer=false` | 「已送达」 |
|
||
| 抢单后 · `readByPeer=true` | 「已读」 |
|
||
| 对端发来的(`isMine=false`) | 不标 |
|
||
|
||
- **实时刷已读**:对端调 `POST /admin/message/chat/{conversationKey}/read` 后,后端经 SSE 推 **`im-chat-read`** 事件给我方(payload `{conversationKey, readerAdminId, lastReadMessageId}`)→ 前端把本会话 `id <= lastReadMessageId` 的我方气泡刷「已读」。**订阅 `im-chat-read`**(与 `im-chat` 同一条 SSE 流 `/ws/admin-msg/stream`;NOTIFY/CHAT 事件不变向后兼容)。
|
||
- 实测:线程出参已含 `readByPeer`(字段顺序 `…, isMine, readByPeer, sentAt`)。
|
||
|
||
### 11.2 房务侧「内部留言框」显示抢单前留言(#4288)
|
||
- 详见 `22_4237`:房务订单详情 `GET /admin/house/orders/{orderId}` **新增 `messages` 列表** + `tabCounts.messageCount = 1(定制师需求) + N(留言)`。**抢单池/未抢单也返**(房务在池里浏览即见定制师留言)。
|
||
- 实测(订单 2068234602970828802,抢单池态):`tabCounts.messageCount=3`、`messages`=[「你好 测试消息」2026-06-23 14:05、「确认单发我一下」14:32]。
|
||
- `messages[].senderRole` 抢单前留言可能为 `null`(抢单前留言恒来自定制师,前端按「定制师」渲染即可);`senderName` 已是真实姓名。
|