hl-api-changelog/changelogs-v2/2026-06/22_4223_房务联系房务面板真实化-在线态+抢单前发消息+claimerId-上线测试服-管理后台.md

18 KiB

房务「联系房务」面板真实化:真实在线态 + 抢单前发消息 + 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-houseGET /admin/message/chat/conversations 出参新增 peerOnline(Boolean)。

  • 语义:对方(房务专员)当前登录后台且有活跃实时(SSE)连接 = 在线;离线靠 Redis TTL 反映(near-live,最多约 45s 延迟,本期不做在线态即时推送)。
  • 前端:peerOnline==true 绿点在线 / false 灰点离线;抢单前peer 为占位)恒 false、配合「未知」不显示状态即可。

3. 新端点 POST /admin/message/chat/open-houseHOUSE 会话订单维度)

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

# 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/openbizModule+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-housepeerAdminId 选填(不传 = 抢单前 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 不是 recordslist时间正序(旧→新,从上往下渲染);游标 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/streamtext/event-stream)实测 LIVE200 + 正确 Content-Type
  • 发消息:后端 send 事务 afterCommit 经 Redis Pub/Sub 广播 CHAT 信令给收件方 → SSE 实时冒泡(带 senderName / 预览 / priority + conversationKey
  • 前端应订阅 /ws/admin-msg/stream,收到 CHAT 信令即 live-append / 刷新对应会话,替换「发送后短轮询 6 次(~8s)」的兜底。「没有 SSE」= 没接,不是没有。

在线/离线态走 SSEwx 2026-06-23 新要求 → 后端工单 #4273

  • 现状:peerOnline轮询字段Redis presence key TTL 45s + SSE 心跳续期),在线态变更不推送
  • wx 拍:改为 SSE 实时推在线/离线。已立后端工单 #4273hl-user-serviceSSE 连接建/断 → 标在线/离线 → 向会话对端推 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。首屏仍用 peerOnlineopen-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/status14: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}/messageslist[]新增 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=3messages=[「你好 测试消息」2026-06-23 14:05、「确认单发我一下」14:32]。
  • messages[].senderRole 抢单前留言可能为 null(抢单前留言恒来自定制师,前端按「定制师」渲染即可);senderName 已是真实姓名。