文件
hl-api-changelog/changelogs-v2/2026-09/10_7328_团期房务整团会话GROUP_HOUSE-新增接口-管理后台.md
T
Mimingguang 1e52ba5d26
changelog-filename-gate / validate (push) Failing after 2s
chore(changelog): #7328 frontmatter 回写 verified(0f2f5b1a)
2026-09-11 10:01:17 +08:00

40 KiB
原始文件 Blame 文件历史


schema: "hl-changelog/v2" ticket: "7328" title: "团期房务整团会话 GROUP_HOUSE:新增 open-group-house + 两个跨服务 internal 端点(认领收敛/团期摘要);会话键按团期聚合主键建(非子订单 id),未认领团仅管理员可开、房务侧 281002" consumer: "admin" author: "wx(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "not_required" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "0f2f5b1a" target_release: "" verified_at: "2026-09-11" status_note: "2026-09-10 squash 0f62fb072(PR #7490)已合并 dev-v3 并部署测试服:hl-user-service 与 hl-order-service-v3 均已滚到当前 tip 0f62fb072,两实例均 LISTEN、Nacos healthy=true enabled=true。三个端点均实测打通:open-group-house 未认领态不对称行为已验证——管理员侧 200,fwzz_pure01/shuxin/fwzz_lead01/test_admin 等非当前认领人角色全部 281002;真实 conversation_key=GROUP_HOUSE:2097500233511362561(按团期聚合主键建键);GET /v3/admin/order/group-batch/{groupBatchId} 响应确认带 houseChatUnreadCount。测试数据已清理(admin_message / admin_conversation_member 均 COUNT=0)。⚠️ 与工单转述核对,本次发现并订正三处(第①②处 2026-09-10 首次发布时已订正,第③④处为部署后复核新增):① ChatConversationRespVO(会话列表项)本次新增的是 groupBatchId/groupBatchNo/groupBatchName 三个字段,并没有 departDate(departDate 只在 ChatOrderCardVO/打开会话响应的团期卡上,会话列表接口读不到);② 281016(缺 groupBatchId)在标准 HTTP 请求路径下不可达,ChatOpenGroupHouseReqVO.groupBatchId 有 @NotNull,@Valid 会先在参数绑定阶段返回 400,281016 只是 ChatManager 内部方法的防御性兜底(源码注释原话如此);③ open-group-house 响应对「团未认领」的 peerAdminId 不是 null 而是占位常量 0(ChatManager.buildFullResp 直接回显 DB 成员行原值,不做归一化),只有会话列表接口(ConversationMemberService.fillGroupBatchSummaries)才会归一化成 null,且列表还受 last_message_at IS NOT NULL 过滤,没发过消息的会话根本不出现在列表里,2026-09-10 实测发现原口径「两处均为 null」有误,已在「关键变化」第 2 条订正;④ 网关对未路由的 /internal/** 路径(本单两个 internal 端点均未配 Path 路由)返回的是 HTTP 200 + 业务体 {"code":404,"message":"接口不存在: ..."}(GlobalErrorWebExceptionHandler 路由未命中分支),不是 403——JwtAuthFilter.isInternalPath() 的 403 分支存在但轮不到执行(请求在路由层已被拒),原口径「网关对 /internal/** 一律 403」不准确,已在「七、不影响范围」订正。此外发现一处工单未提及但同一 squash 里的关联变更:既有团期详情接口 GET /v3/admin/order/group-batch/{groupBatchId}(A2)顺带新增只读字段 houseChatUnreadCount,纯新增不影响既有契约,一并写在「关键变化」供前端知悉。 前端已交付(commit 0f2f5b1a):chat.js 加 openGroupHouseChat(键 GROUP_HOUSE:{groupBatchId} 团期聚合主键 String 透传,前端不自拼)+KEY_RE 扩展;ChatDrawer 支持 GROUP_HOUSE(open 分流/会话卡团期文本/未认领 peerAdminId=0 显「团期管理员」不占在线点),281002 复用固定文案不透原文、281017 拦截器透 message;入口①房务看板「联系团期管理员」②团期详情「联系房务」+houseChatUnreadCount 角标(标读/信令重拉,TEAM 口径不重算);两 internal 端点前端不接。" updated_at: "2026-09-10" base: "dev-v3"

团期房务整团会话 GROUP_HOUSE

服务: hl-user-service(open-group-house / reconcile-group-house-claimer)+ hl-order-service-v3(group-batch-chat-summary-batch,二者互为 internal 消费方) PR: #7490 Issue: #7328 日期: 2026-09-10 影响范围: 管理后台「房务↔团期管理员」整团聊天新入口(全新会话模块 GROUP_HOUSE,前端需接 1 个 admin 端点);另 2 个 internal 端点供 order-v3/user-service 互调,前端不接


⚠️ 关键变化

这是一批全新端点,没有存量调用方,但有几处容易踩坑、需要前端与联调方明确知道的地方:

1. 会话键是 GROUP_HOUSE:{groupBatchId},按团期聚合主键建键,不是子订单 id

与已有的 GROUP:{orderId}(#7211,定制师↔团期管理员)不同,GROUP_HOUSE 的键维度是团期聚合主键(order_group_batch.group_batch_id),不是任何一个子订单的 id。前端如果把子订单 id 或团号当成 groupBatchId 传给 open-group-house,不会报错——只会拿到一个查不到数据/或查到另一个团的会话,属于静默查空,排障线索很少,务必用团期详情页/看板页里的团期聚合主键(雪花 id)作为入参。

2. 团未被认领时的不对称,且 peerAdminId 在「打开会话」与「会话列表」两个接口上语义不同(2026-09-10 实测订正)

open-group-house 的准入是「token 当前角色持权限码 group-batch:demand:confirm(超管天然通过)」或「当前角色为 ROOM_MANAGER 且本人即该团当前整团认领人」。团尚未被任何房务认领时:

  • 管理员侧(持权限码的角色)可以正常打开,消息落「团队共享池」,认领发生后由收敛逻辑自动把池里的历史消息改判给新认领人;
  • 房务侧任何人打开都会返回 281002(没有人满足 adminId == claimerId;实测 fwzz_pure01/shuxin/fwzz_lead01/test_admin 等非当前认领人角色全部命中 281002)。

⚠️ peerAdminId/peerName 在两个接口上的归一化程度不一样,前端不能用同一套判空逻辑处理待认领态:

  • open-group-house(管理员打开一个尚未被认领的团):响应体直接回显 DB 成员行原始 peer_admin_id(ChatManager.buildFullResp,无归一化),未认领时是占位常量 0(ChatConstants.PENDING_PEER_ADMIN_ID)。实测:"peerAdminId": 0、"peerName": null、"peerRole": "HOUSE"。不是 null——前端若按 peerAdminId == null 判「待认领」会判失败,实测拿到的是数字/字符串 0,按 id 直接渲染可能出现「管理员 #0」一类脏文案,正确判法是 peerAdminId === 0(或字符串 "0")。
  • GET /admin/message/chat/conversations?bizModule=GROUP_HOUSE&...(我的会话列表):ConversationMemberService.fillOrderSummaries/fillGroupBatchSummaries 会把无认领人的情形显式归一化为 "peerAdminId": null、"peerName": null(ConversationMemberService.java:1173-1182)——只有这条路径才会拿到真正的 null。
  • 且列表接口还有一层前提:没发过消息的会话根本不出现在列表里(selectMyConversations 固定过滤 last_message_at IS NOT NULL,AdminConversationMemberMapper.java:108-109),团刚被认领/尚未认领但还没人发过消息时,这条会话在列表接口里查不到任何一行,不是「查得到但 peerAdminId 为 null」。

前端待认领占位样式的正确写法:open-group-house 响应判 peerAdminId === 0;会话列表响应判 peerAdminId == null——两套判断不能混用,也不能假设「没发过消息=会在列表里显示为待认领行」。

3. GroupHouseClaimerReconcileReqVO.currentClaimerId 没有 @NotNull 是刻意设计(internal,前端不接,但排障/联调需知道)

RELEASE 事件本来就没有新认领人,若给这个字段加上 @NotNull 会让释放事件在参数校验阶段就被拒,会话会永远停在旧认领人身上。这条口径写在下方「四、契约约束与正确调用方式」,防止后人以为这是遗漏而「顺手补全校验」。

4. 会话卡字段在「打开会话」和「会话列表」两个响应体里不对称,列表项没有 departDate

打开会话(open-group-house)返回的团期卡 ChatOrderCardVO 本次新增 groupBatchName + departDate 两个字段(groupBatchId/groupBatchNo 是 #7211 已有字段,本次复用);但「我的会话列表」GET /admin/message/chat/conversations 的会话列表项 ChatConversationRespVO 本次只新增了 groupBatchId/groupBatchNo/groupBatchName 三个字段,没有 departDate。前端如果要在会话列表页也展示出发日,不能假设该字段在列表接口里就有,需要另外从团期详情/打开会话响应取。

5. 顺带变更:既有团期详情接口新增只读未读角标字段(非本单新增接口,纯新增字段)

同一 squash 里,既有的 GET /v3/admin/order/group-batch/{groupBatchId}(A2 团期详情,非本单新增端点)响应体 GroupBatchDetailRespVO 新增了一个只读字段 houseChatUnreadCount(Integer):GROUP_HOUSE 会话的团队共享未读数,口径是 TEAM(任一持 group-batch:demand:confirm 的管理员读过即对全员清零),user-service 不可达或尚无会话时恒为 0(软依赖,不阻断详情主数据,事务外回填)。这是对既有响应体的纯新增字段,向后兼容,不改变该接口任何既有字段/错误码,该接口本身不在本单「新增接口」范围内、不占用下方逐接口详情编号,如需展示「联系房务」按钮的未读角标,直接读取详情响应里的这个字段即可。


一、背景

#7328 是「团期房务」整体方案(#7323 定案)的延续单:在 #7322(团期整团抢单/抢单池)与 #7211(定制师↔团期管理员 GROUP 会话)之上,补上第三方会话——整团认领房务 ↔ 团期管理员团队,与 #7324(房务团期看板与按日订房计划)配套,让房务在配房过程中能直接在系统内联系团期管理员,管理员也能主动联系已认领的房务。

三个新端点分工:POST /admin/message/chat/open-group-house(hl-user-service,管理后台直接调用,打开/找回会话);POST /internal/message/chat/reconcile-group-house-claimer(hl-user-service 提供,hl-order-service-v3 消费,整团认领/接管/释放后收敛会话归属);POST /internal/house/group-batch-chat-summary-batch(hl-order-service-v3 提供,hl-user-service 消费,批量取团期的授权与展示摘要)。三者与既有 FLEET(#4937/#4689)、GROUP(#7211)两个「个人↔团队」会话模块共用同一套 TeamChatModule/TeamChatAccessContext/TeamChatAuthorizationService 框架(CODE_RULES §15.7 禁镜像重复),只在键维度、团队/个人侧角色、前置错误码等处取值不同。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 打开/找回团期房务整团会话 POST /admin/message/chat/open-group-house 新增接口 会话键 GROUP_HOUSE:{groupBatchId};未认领团仅管理员侧可开,房务侧 281002
2 收敛团期房务会话认领人归属 POST /internal/message/chat/reconcile-group-house-claimer 新增接口 order-v3 整团 CAS 成功后 afterCommit 调用,internal,前端不接
3 批量取团期聊天摘要 POST /internal/house/group-batch-chat-summary-batch 新增接口 user-service 授权与会话卡数据源,internal,前端不接,单次最多 200 个团期

三、接口详情

1. 打开/找回团期房务整团会话 POST /admin/message/chat/open-group-house

VO: ChatOpenGroupHouseReqVO → Result<ChatOpenFullRespVO>

(Controller:ChatMessageController.java:170-176;编排:ChatManager.openGroupHouse/openTeamInternal,:404-454;授权:TeamChatAuthorizationService.assertAccess)

使用场景

房务「我的团」看板(#7324 H1/H2)里点「联系团期管理员」、管理后台团期详情(A2,GET /v3/admin/order/group-batch/{groupBatchId})里点「联系房务」时调用,一次性拿到会话元信息 + 团期卡 + 首屏消息 + 合并未读并标记本会话已读。

请求头

@Lock4j:锁名 fleet-order-chat(与 FLEET/GROUP 共用,key='GROUP_HOUSE:' + groupBatchId 各自区分),租约 120 秒,获取超时 3 秒;未加 @Idempotent(与 open/open-house/open-fleet/open-group 同口径,重复调用天然幂等找回)。

入参字段表(ChatOpenGroupHouseReqVO.java)

字段 位置 类型 必填 约束 说明
groupBatchId Body Long 是 @NotNull 团期聚合主键(雪花 id);两侧对端都由后端解析,不接受 peerAdminId 字段

出参字段表 Result<ChatOpenFullRespVO>

ChatOpenFullRespVO 继承 ChatOpenRespVO:

字段 类型 说明
conversationKey String 会话键,形如 GROUP_HOUSE:1930000000000000001
peerAdminId Long 直接回显 DB 成员行原始值,不做归一化;未认领团为占位常量 0(不是 null,见「关键变化」第 2 条),已认领后为真实认领房务 adminId
peerName String 对方姓名快照;团队占位为「团期管理员」/「房务」,真实对端为姓名
peerRole String 对方角色码,GROUP_ADMIN(团队占位/团队侧视角)或 HOUSE(房务视角)
peerRoleLabel String 对方角色中文 label,如「团期管理员」「房务」
peerOnline Boolean 对方是否在线(团队席位按该模块团队侧是否有人在线判定)
unreadCount Integer 本会话未读数,打开即已读,恒为 0
isNew Boolean true=本次新建会话 false=找回已有
order ChatOrderCardVO 团期卡(无订单号、无联系人),见下表;授权摘要不可达/团期未找到时上游已 fail-closed,不会出现 order 为 null 但整体 200 的情况
thread ChatThreadRespVO 首屏消息,最新一页 pageSize=20;上滑加载更早历史走既有 GET /admin/message/chat/{conversationKey}/messages
unreadTotal Integer 标记本会话已读后,我的合并未读总数(NOTIFY+CHAT)

ChatOrderCardVO(团期卡,仅 GROUP_HOUSE 相关字段,其余订单维度字段如 orderNo/customerName/destination/tripDays/adultCount/childCount/requirementId 恒为 null):

字段 类型 说明
productName String 产品名
groupBatchId String(雪花,ToStringSerializer) 团期聚合主键
groupBatchNo String 团号;团期软删/不存在时为 null
groupBatchName String 班期名(本次新增)
bizStatusLabel String 团期状态中文(复用该字段承载团期状态名,未知状态码为 null)
departDate String(yyyy-MM-dd) 出发日(本次新增)

请求示例

POST /admin/message/chat/open-group-house
{ "groupBatchId": "1930000000000000001" }

响应示例(认领房务本人打开:对端=团队占位)

{
  "code": 200, "success": true,
  "data": {
    "conversationKey": "GROUP_HOUSE:1930000000000000001",
    "peerAdminId": "0", "peerName": "团期管理员", "peerRole": "GROUP_ADMIN",
    "peerRoleLabel": "团期管理员", "peerOnline": false, "unreadCount": 0, "isNew": true,
    "order": {
      "productName": "游牧的森林-短途版", "groupBatchId": "1930000000000000001",
      "groupBatchNo": "T2026-0801-01", "groupBatchName": "国庆一期",
      "bizStatusLabel": "资源准备中", "departDate": "2026-10-01"
    },
    "thread": { "conversationKey": "GROUP_HOUSE:1930000000000000001", "hasMore": false, "nextCursor": null, "list": [] },
    "unreadTotal": 0
  }
}

响应示例(管理员打开,团已被认领:对端=真实认领房务)

{
  "code": 200, "success": true,
  "data": {
    "conversationKey": "GROUP_HOUSE:1930000000000000001",
    "peerAdminId": "30001", "peerName": "房务·小呼", "peerRole": "HOUSE",
    "peerRoleLabel": "房务", "peerOnline": true, "unreadCount": 0, "isNew": false,
    "order": {
      "productName": "游牧的森林-短途版", "groupBatchId": "1930000000000000001",
      "groupBatchNo": "T2026-0801-01", "groupBatchName": "国庆一期",
      "bizStatusLabel": "资源准备中", "departDate": "2026-10-01"
    },
    "thread": { "conversationKey": "GROUP_HOUSE:1930000000000000001", "hasMore": true, "nextCursor": 900001,
      "list": [{ "messageId": 900002, "senderAdminId": "30001", "senderName": "房务·小呼", "senderRole": "HOUSE",
        "msgType": "TEXT", "priority": "NORMAL", "content": "这个团标间还差 2 间", "isMine": false,
        "readByPeer": false, "sentAt": "2026-09-10 10:00:00" }] },
    "unreadTotal": 1
  }
}

响应示例(管理员打开,团尚未被认领:peerAdminId 为占位 0,不是 null,2026-09-10 实测)

{
  "code": 200, "success": true,
  "data": {
    "conversationKey": "GROUP_HOUSE:2097500233511362561",
    "peerAdminId": 0, "peerName": null, "peerRole": "HOUSE",
    "peerRoleLabel": "房务", "peerOnline": false, "unreadCount": 0, "isNew": true,
    "order": {
      "productName": "游牧的森林-短途版", "groupBatchId": "2097500233511362561",
      "groupBatchNo": "T2026-0801-01", "groupBatchName": "国庆一期",
      "bizStatusLabel": "资源准备中", "departDate": "2026-10-01"
    },
    "thread": { "conversationKey": "GROUP_HOUSE:2097500233511362561", "hasMore": false, "nextCursor": null, "list": [] },
    "unreadTotal": 0
  }
}

空数据 / 降级响应

无空态:授权通过后一定能拿到会话元信息与团期卡(团期不存在/已软删/已结算/已取消在授权阶段已被 281017 拦截,不会走到组装响应这一步);thread.list 在新建会话/无历史消息时为空数组 [],属正常状态,不是异常。

错误响应

{ "code": 400, "message": "团期id不能为空", "success": false, "data": null }
{ "code": 281002, "message": "无权访问该会话", "success": false, "data": null }
{ "code": 281017, "message": "团期不存在、已结算或已取消,无法发起房务会话", "success": false, "data": null }

(281016「缺少团期上下文」理论上仍是 ChatManager.openGroupHouse 的防御性兜底码,但标准 HTTP 请求会先被 ChatOpenGroupHouseReqVO.groupBatchId 上的 @NotNull 拦成上面的 400,实际走不到 281016,源码注释原话如此——见 ChatErrorCode.java:69-76。)

业务边界

  • 准入两条路:① token 当前角色为 ROOM_MANAGER 且 adminId 等于该团当前整团认领人(摘要 claimerId,未认领时任何 ROOM_MANAGER 都不满足);② token 当前角色持权限码 group-batch:demand:confirm(超管天然通过)。定制师在本模块既非团队侧也非个人侧,一律 281002。
  • 摘要(group-batch-chat-summary-batch)不可达或返回空结果时 fail-closed 为 281002,不会降级放行——因为摘要同时承载「个人侧是谁」与「团队侧准入范围」,拿不到就无法判断请求人是否有权访问。
  • 281017 只作用于 open/send:会话一旦建立,messages(GET)/read/conversations 仍然放行,团期终态后历史仍可审计读取,不因团期结算/取消而不可读。
  • 团队占位对端(peerAdminId="0")不指向团队里任何一个具体人,多个管理员先后打开各自建自己一行(对端=真实认领房务或占位待认领),认领房务侧的团队行只建一次(幂等补建)。
  • peerAdminId=0 的占位语义只在本端点(open-group-house)成立;会话列表接口(GET /admin/message/chat/conversations)对同一场景归一化为 null,两个接口不能共用一套「待认领」判空逻辑(见「关键变化」第 2 条)。

2. 收敛团期房务会话认领人归属 POST /internal/message/chat/reconcile-group-house-claimer

VO: GroupHouseClaimerReconcileReqVO → Result<Boolean>

(Controller:ChatInternalController.java:80-91;编排:TeamChatReconciliationService.reconcilePersonal,:44-69;调用方:hl-order-service-v3 的 HouseGroupBatchChatBindNotifier 经 ChatBindFeignClient.reconcileGroupHouseClaimer)

使用场景

hl-order-service-v3 的整团认领(CLAIM)/接管(TAKEOVER)/释放(RELEASE)三条写口在 CAS 成功、主事务 afterCommit 后调用,把 GROUP_HOUSE:{groupBatchId} 会话的历史未读、成员行与团队侧对端快照收敛到当次事件后的权威认领人;前端不直接调用。

请求头

@Lock4j:锁名 fleet-order-chat,key=module.name() + ':' + groupBatchId(即 GROUP_HOUSE:{groupBatchId}),与 open-group-house/发消息共用同一把锁,保证收敛与聊天读写串行。

入参字段表(GroupHouseClaimerReconcileReqVO.java)

字段 位置 类型 必填 约束 说明
groupBatchId Body Long 是 @NotNull 团期聚合主键
previousClaimerId Body Long 否 — 事件快照里的旧认领人 adminId,仅日志/审计用,不参与判断
currentClaimerId Body Long 否(刻意不加 @NotNull) — 事件快照里的新认领人 adminId;RELEASE 事件传 null;与服务端重读的权威摘要不一致时以摘要为准,见「四、契约约束」
eventType Body String 是 @NotNull;取值 CLAIM/TAKEOVER/RELEASE 仅用于日志与幂等语义标注,不参与收敛分支判断——真正结果完全由服务端重读的 order-v3 权威摘要决定

出参字段表 Result<Boolean>

字段 类型 说明
data Boolean true=该团确有既有会话并已收敛;false=该团尚无既有会话(不制造空成员行,正常业务态,不是错误)

请求示例

POST /internal/message/chat/reconcile-group-house-claimer
{ "groupBatchId": "1930000000000000001", "previousClaimerId": null, "currentClaimerId": "30001", "eventType": "CLAIM" }

响应示例(已收敛)

{ "code": 200, "success": true, "data": true }

空数据 / 降级响应

该团尚无既有会话(还没有人打开过 open-group-house)时返回 { "code": 200, "success": true, "data": false },这是正常业务态,不代表异常;调用方(HouseGroupBatchChatBindNotifier)据此只记 warn 日志,不阻断认领/接管/释放的主流程。

错误响应

{ "code": 400, "message": "groupBatchId 不能为空; eventType 不能为空", "success": false, "data": null }
{ "code": 281002, "message": "无权访问该会话", "success": false, "data": null }

(281002 触发条件:本端点执行期间重读 hl-order-service-v3 的团期摘要(group-batch-chat-summary-batch)失败或返回空结果,与端点 1 的 fail-closed 口径同一个错误码;调用方 hl-order-service-v3 侧不区分本端点返回的业务失败与 data:false,ChatBindFeignClient 只判 !resp.isSuccess() 记 warn,两者在调用方视角都被当作「本次未收敛,下轮反熵/下次打开会话再补」处理。)

业务边界

  • 软依赖:hl-order-service-v3 侧的 ChatBindFeignFallbackFactory 在本端点(或 hl-user-service 整体)不可达时熔断降级为 Result.success(false),不抛异常、不阻断认领/接管/释放主事务;失败只记 warn 日志(HouseGroupBatchChatBindNotifier.doReconcile)。
  • 乱序到达的多个事件不会把归属回退:服务端每次都在会话锁内重新读取 order-v3 权威摘要作为收敛依据,事件体里的 previousClaimerId/currentClaimerId 只是快照,仅用于日志。
  • currentClaimerId 与服务端重读的权威摘要不一致时,以摘要为准,同时记一条 info 级日志(不是错误)。
  • 释放(RELEASE)收敛:归档旧认领人成员行、团队侧各行对端回退占位,消息与历史不动;认领(CLAIM)收敛:把管理员在无人认领期间发进团队池的消息改判给新认领人;接管(TAKEOVER)收敛:归档旧认领人成员行,会话键与历史不变。

3. 批量取团期聊天摘要 POST /internal/house/group-batch-chat-summary-batch

VO: GroupBatchChatSummaryBatchReqVO → Result<GroupBatchChatSummaryBatchRespVO>

(Controller:HouseChatSummaryInternalController.java:60-65;编排:HouseGroupBatchChatSummaryService.summaryBatch;调用方:hl-user-service 的 TeamChatAuthorizationService.fetchGroupBatchSummary)

使用场景

hl-user-service 的 TeamChatAuthorizationService 在 GROUP_HOUSE 会话「开会话/发消息/拉线程/已读/我的会话列表」等全部授权路径上,批量读取团期的权威事实(认领人是谁、能不能联系、会话卡展示什么字段);前端不直接调用。

入参字段表(GroupBatchChatSummaryBatchReqVO.java)

字段 位置 类型 必填 约束 说明
groupBatchIds Body Long[] 是 @NotEmpty + @Size(max=200) 团期聚合主键列表;上限 200 与 hl-user-service 侧授权批的分片大小一致,超限即视为调用方写错了

出参字段表 Result<GroupBatchChatSummaryBatchRespVO>

字段 类型 说明
summaryMap Map<String, GroupBatchChatSummaryItemVO> key=团期 id 的 String 形态(防雪花 id 在 JS 丢精度);入参每个 id 都会建键,即便团期不存在也建键(batchFound=false),调用方无需判断键缺失

GroupBatchChatSummaryItemVO(14 字段):

字段 类型 说明
groupBatchId String(雪花,ToStringSerializer) 回显入参
batchFound Boolean order_group_batch 活跃行是否存在,不存在或已软删为 false
batchNo String 团号;!batchFound 时为 null
batchName String 班期名;!batchFound 时为 null
batchLabel String 第 N 期快照标签,可空
productName String 产品名快照
batchStatus String 团期状态码(九态原样)
batchStatusName String 团期状态中文;未知码为 null
departDate LocalDate 出发日,可空
endDate LocalDate 返团日,可空
requirementConfirmed Boolean 需求整体确认标记
hotelReady Boolean 配房完成标志
contactable Boolean 是否可发起/继续会话:batchFound 且状态 不在 {SETTLED, CANCELLED} 中
claimerId String(雪花,ToStringSerializer) 当前整团认领房务 adminId;未认领为 null;走含软删的读契约,团期归档后原认领房务仍可能非空(供历史会话可读)

请求示例

POST /internal/house/group-batch-chat-summary-batch
{ "groupBatchIds": ["1930000000000000001", "1930000000000000099"] }

响应示例

{
  "code": 200, "success": true,
  "data": {
    "summaryMap": {
      "1930000000000000001": {
        "groupBatchId": "1930000000000000001", "batchFound": true, "batchNo": "T2026-0801-01",
        "batchName": "国庆一期", "batchLabel": "第 1 期", "productName": "游牧的森林-短途版",
        "batchStatus": "RESOURCE_PREPARING", "batchStatusName": "资源准备中",
        "departDate": "2026-10-01", "endDate": "2026-10-05",
        "requirementConfirmed": true, "hotelReady": false,
        "contactable": true, "claimerId": "30001"
      },
      "1930000000000000099": {
        "groupBatchId": "1930000000000000099", "batchFound": false, "batchNo": null, "batchName": null,
        "batchLabel": null, "productName": null, "batchStatus": null, "batchStatusName": null,
        "departDate": null, "endDate": null, "requirementConfirmed": null, "hotelReady": null,
        "contactable": false, "claimerId": null
      }
    }
  }
}

空数据 / 降级响应

无空态:groupBatchIds 非空是入参硬约束(@NotEmpty),空数组会被 Bean Validation 拒在参数绑定阶段;查不到的团期不是「缺键」而是体现为该 id 键值的 batchFound=false(见响应示例第二项)。

错误响应

{ "code": 400, "message": "团期 ID 列表不能为空", "success": false, "data": null }
{ "code": 400, "message": "单次最多查询 200 个团期", "success": false, "data": null }

(本端点没有自定义业务错误码:全程只读、不做鉴权判断,查不到的团期直接体现为 batchFound=false,不抛错。)

业务边界

  • claimerId 走「含软删」的认领人读契约(listHouseClaimersIncludeDeleted),与 batchFound(活跃行判定,@TableLogic 已过滤软删)是两套独立读契约:团期已软删时 batchFound=false 但 claimerId 仍可能非空。
  • contactable = batchFound 且状态不在 {SETTLED, CANCELLED} 中;遇到未知状态码按「不可联系」处理(保守侧,宁可拦住发起,也不因识别不出状态而放行到已结算的团上),同时记 warn 日志。
  • 全程只读、不开事务,不含任何同步 Feign/MQ。

四、契约约束与正确调用方式

正确 / 错误 payload 对照

场景 payload
正确:端点 1 只传 groupBatchId { "groupBatchId": "1930000000000000001" }
错误:端点 1 传 peerAdminId 会被忽略——两侧对端一律由后端解析摘要,传了也不采信,不是 400 也不是生效
正确:前端用 GROUP_HOUSE:{groupBatchId}(团期聚合主键)作为本地会话主键 —
错误:复用 GROUP:{orderId} 或任何子订单 id 拼 GROUP_HOUSE 的键 键空间不同,拿错主键会静默查空(见「关键变化」第 1 条),不会报错提示
正确:(order-v3 侧)RELEASE 事件调端点 2 时 currentClaimerId 留空/传 null { "groupBatchId": "...", "currentClaimerId": null, "eventType": "RELEASE" }
错误:给 GroupHouseClaimerReconcileReqVO.currentClaimerId 补 @NotNull 会让 RELEASE 事件在参数校验阶段被拒,会话永远停在旧认领人身上——这是刻意的空值语义,不是遗漏(见 GroupHouseClaimerReconcileReqVO.java:12-15 类注释)
正确:判断端点 1 返回的团队占位对端 peerAdminId="0" 且 peerRole 为 GROUP_ADMIN/HOUSE 时是团队占位,不是具体人
错误:把 peerAdminId="0" 当真实员工 id 查姓名/头像 占位行没有对应的 admin_user 记录

切换状态时的必要动作

  • 团期从「未认领」变为「已认领」(房务抢单成功)后,hl-order-service-v3 会在 afterCommit 调端点 2 完成收敛;前端下一次调端点 1(重新 open)就能拿到刷新后的 order/peerAdminId,不需要也不应该在本地把旧的「待认领」占位数据继续展示或本地拼接猜测认领人信息。

五、数据库行为

  • 零 Flyway / 零 DDL:本单未新增/修改任何表结构。GROUP_HOUSE 会话复用既有的 admin_conversation_member(成员/水位行)与 admin_message(消息行)两张表,只是新增了 biz_module=GROUP_HOUSE 取值的行,与 FLEET(#4937)/GROUP(#7211)同构。
  • 团队共享未读水位沿用既有机制:admin_id=0 的成员行承载团队共享已读水位,任一持权限码的管理员读过即对全员清零,未引入新的水位承载方式。
  • 收敛端点(端点 2)与开会话/发消息共用同一把 Lock4j 分布式锁(锁名 fleet-order-chat,key=GROUP_HOUSE:{groupBatchId}),保证「收敛认领人归属」与「聊天读写」在同一团期上严格串行,不产生半途状态的成员行。
  • hl-order-service-v3 侧既有 GroupBatchDetailRespVO(A2 团期详情响应)顺带新增只读展示字段 houseChatUnreadCount,纯运行时聚合(事务外同步 Feign 查询 hl-user-service 未读数),不涉及任何新表/新列。

六、边界行为

  • 端点 1:团尚未被认领时,管理员侧可打开(消息落团队共享池),房务侧任何人打开都返 281002;认领发生后,管理员再次打开会自动把 order/peerAdminId 切到认领人本人的信息,不需要额外操作。
  • 端点 1:281017(团期不可联系)只作用于 open/send——会话一旦建立,历史消息仍可通过既有 messages(GET) 分页读取,团期结算/取消后依然可审计读取历史,不会因此把已有会话「锁死」到无法查看。
  • 端点 2:同一批 CLAIM/TAKEOVER/RELEASE 事件即便乱序到达,最终收敛结果都以服务端当次重读的权威摘要为准,不会被过期/乱序事件覆盖回退。
  • 端点 2:该团尚无既有会话(没人打开过端点 1)时返回 data:false,不会为了「记录一下」而制造一条空的会话成员行。
  • 端点 3:groupBatchIds 传超过 200 个直接 400,不落任何库查询;查不到的团期只体现为 batchFound=false,不是错误,也不会缺键。

六.5、枚举 / 数据字典

eventType(端点 2 入参,GroupHouseClaimerReconcileReqVO.eventType)

所属字段: eventType | 类型: String

值 中文 说明
CLAIM 认领 整团认领成功后触发,把团队池里的历史消息改判给新认领人
TAKEOVER 接管 团级接管成功后触发(组长/超管指派新房务),归档旧认领人成员行
RELEASE 释放 整团释放后触发,currentClaimerId 恒为 null,团队侧各行对端回退占位

(仅用于日志与幂等语义标注,不参与收敛分支判断——真正结果完全由服务端重读的 order-v3 权威摘要决定。)

batchStatus / batchStatusName(端点 3 出参,GroupBatchStatus,九态,与 #7324 同一枚举)

所属字段: batchStatus(码)/ batchStatusName(中文) | 类型: String

值 中文 说明
RECRUITING 招募中 —
RESOURCE_PREPARING 资源准备中 —
MATERIAL_PREPARING 物料准备中 —
PENDING_DEPARTURE 待出发 —
TRAVELLING 出行中 —
TRIP_FINISHED 出行完毕 —
REVIEWING 核单中 —
SETTLED 已结算 contactable=false,端点 1 的 open/send 返 281017
CANCELLED 已取消 contactable=false,端点 1 的 open/send 返 281017

错误码新增清单(ChatErrorCode.java,段位 281000-281099 内本单实际新增 2 个常量)

Code 常量名 Message module 触发条件
281016 CHAT_GROUP_HOUSE_BATCH_REQUIRED 缺少团期上下文 user 仅 ChatManager.openGroupHouse 内部/单测直调可达;标准 HTTP 请求因 ChatOpenGroupHouseReqVO.groupBatchId 的 @NotNull 会先被 Bean Validation 拦成 400,实际走不到本码
281017 CHAT_GROUP_HOUSE_NOT_CONTACTABLE 团期不存在、已结算或已取消,无法发起房务会话 user 端点 1 授权通过后,前置条件 contactable=false(团期不存在/已软删/SETTLED/CANCELLED),只作用于 open/send

(复用的既有错误码,非本单新增:281002「无权访问该会话」(CHAT_NOT_MEMBER)本次被端点 1「非会话成员/团队-个人侧准入不满足/摘要不可用 fail-closed」与端点 2「收敛时重读团期摘要失败」两处复用。)


七、不影响范围

  • 既有会话模块 DIRECT/HOUSE/HOUSE_LEAD/FLEET(#4937/#4689)/GROUP(#7211)的开会话、发消息、拉线程、已读、未读端点零改动;ChatConversationRespVO/ChatOrderCardVO 新增的 GROUP_HOUSE 专属字段(groupBatchName/departDate 等)对非 GROUP_HOUSE 会话恒为 null,不影响既有渲染逻辑。
  • 网关路由零改动:open-group-house 挂在既有 ChatMessageController(@RequestMapping("/admin/message/chat"))下,落在 hl-gateway 现有 Path=/admin/message/** 规则内;两个 internal 端点均不经网关(Feign LB 直连)。
  • 两个 internal 端点不经网关路由匹配(hl-gateway/src/main/resources/application.yml 未给 /internal/message/**、/internal/house/** 配任何 Path predicate,只有 /v3/internal/**、/internal/fleet/** 两段专属前缀被路由,本单两个端点都不在其中),消费方走 Feign LB 直连。若误经网关域名访问会得到 HTTP 200 + 业务体 {"code":404,"message":"接口不存在: <path>"}(GlobalErrorWebExceptionHandler,路由未命中 NotFoundException 分支,HTTP 状态码固定 200),不是 403,也不是网络层直接拒绝——JwtAuthFilter.isInternalPath() 里确实有 403("接口不可访问")分支,但请求在路由层就未命中,根本轮不到这个 filter 执行;2026-09-10 之前的口径「网关对 /internal/** 一律 403」不准确,已按实测订正。
  • 零 Flyway / 零 DDL,无需同步任何 H2 schema。
  • hl-order-service-v3 既有团期详情接口(A2)新增的 houseChatUnreadCount 是纯新增只读字段,不改变该接口既有任何字段/错误码语义。
  • 团期需求(H09 系列)、团期抢单池/我的团/接管(#7322)、团期核单/结算、房务团期看板与按日订房计划(#7324)现有端点零改动。

八、测试环境已验证

  • 单元/契约测试已全绿(squash 0f62fb072,PR #7490,已合并 dev-v3):ChatManagerGroupHouseTest、ChatMessageServiceGroupHouseTest、ConversationMemberServiceGroupHouseTest、TeamChatAuthorizationServiceTest、HouseChatSummaryInternalControllerTest、GroupBatchChatSummaryBatchReqVOValidationTest、GroupBatchChatSummaryFeignContractTest(钉住 order-v3/user-service 两侧 GroupBatchChatSummaryItemVO/GroupBatchChatSummaryFeignItemVO 字段对齐,含 departDate/endDate 均为 LocalDate)、GroupHouseClaimerReconcileContractTest、ChatBindFeignFallbackFactoryTest、ChatErrorCodeTest 等。
  • 已部署测试服并实测通过(2026-09-10):hl-user-service、hl-order-service-v3 均已滚动部署到 dev-v3 当前 tip 0f62fb072(PR #7490),两实例均 LISTEN,Nacos healthy=true enabled=true。
  • POST /admin/message/chat/open-group-house:团未认领时管理员侧打开返 200(peerAdminId=0,不是 null,见「关键变化」第 2 条);fwzz_pure01/shuxin/fwzz_lead01/test_admin 等非当前认领人角色全部命中 281002;真实 conversationKey=GROUP_HOUSE:2097500233511362561,按团期聚合主键建键(非子订单 id)与设计一致。
  • POST /internal/message/chat/reconcile-group-house-claimer、POST /internal/house/group-batch-chat-summary-batch:两个 internal 端点均经 Feign LB 直连打通;GET /v3/admin/order/group-batch/{groupBatchId} 响应确认带 houseChatUnreadCount 字段(见「关键变化」第 5 条)。
  • 网关侧复核:两个 internal 端点未配路由,经网关域名访问返 HTTP 200 + {"code":404,"message":"接口不存在: ..."},不是 403(见「七、不影响范围」订正说明)。
  • 测试数据已清理:admin_message、admin_conversation_member 两张表实测数据均 COUNT=0,不残留测试脏数据。
  • 兼容性结论:全部为新增端点,无存量调用方,无向后兼容负担;hl-order-service-v3 A2 团期详情接口新增的 houseChatUnreadCount 字段是非破坏性纯新增。

十、相关文档

  • #7323《团期房务实现方案 v1.0》(错误码段位、模块划分定案)
  • #7322 团期整团抢单与抢单池分流(本单「当前整团认领人」判定的前置依赖)
  • #7211 定制师↔团期管理员 GROUP 会话(本单复用的「个人↔团队」会话框架源头)
  • #7324 房务团期看板与整团按日订房计划 CRUD(本单联调入口所在页面)

关联 / 联系人

链接

联系人

  • 后端负责人: @wx