--- schema: "hl-changelog/v2" ticket: "7211" title: "定制师 ↔ 团期管理员站内会话(GROUP:{orderId})" consumer: "admin" author: "wx(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "86340b24" target_release: "" verified_at: "2026-09-08" status_note: "前端已交付并验证(commit 86340b24):chat.js 新增 openGroupChat(POST /message/chat/open-group,body 仅 {orderId} 字符串不传 peerAdminId,silentError),send 静默正则扩 /^(?:FLEET|GROUP):[^:]+$/、messages/read 对 GROUP 键静默,281002 不透传原文案由 ChatDrawer 统一映射「刷新重试/订单负责人可能已变更」;住宿安排卡加「联系团期管理员」(显隐只看 itinerary.groupBatchId 非空不看 OrderMainVO.groupBatchId,无定制师显「暂无定制师」,角标 itineraryGroupUnreadCount),团期「查看需求」名单表每户加「联系定制师」(仅 group-batch:demand:confirm 持码露出,团队共享「待处理」角标,操作列宽 76→150 必要调宽);抽屉头徽章透传后端 peerRoleLabel,前端不建 GROUP_ADMIN 硬编码映射;SSE JSON 解析宽容未知字段零改动,聊天 Tab unread-total 与消息中心 unread-count 两角标按契约分开;定向 114 例+回归 87 文件 918 例全绿,对抗 review PASS,checkpoint 全绿(Vitest 全量+生产构建)。" updated_at: "2026-09-08" base: "dev-v3" --- # 团期模块:定制师 ↔ 团期管理员站内会话(GROUP:{orderId}) > **服务**: `hl-order-service-v3`、`hl-user-service` > **Issue**: #7211 > **PR**: #7225(order-v3)、#7226(user-service) > **日期**: 2026-09-07 > **影响范围**: 管理后台子订单详情「住宿安排」卡、团期详情「查看需求」名单表 ## ⚠️ 关键变化 **新增团期管理员团队会话通道**:定制师与团期管理员团队(而非指派个人)按子订单维度建立实时对话(会话键 `GROUP:{orderId}`),镜像车务团队制(团队共享未读水位行、定制师广播落池、任一管理员读即全员清零)。 **顶部角标两条规则分开**: 1. 团期管理员团队共享池(`admin_id=0`)未读**只计入**聊天 Tab 与 SSE,**不计入**消息中心角标; 2. 团期管理员回复落定制师个人行(`sender_role=GROUP_ADMIN`),照常计入定制师顶部角标、消息中心与收件箱。 **前端勾选要点**: - `ItineraryVO.groupBatchId` 非空时才显示「联系团期管理员」按钮(**不看** `OrderMainVO.groupBatchId`); - `peerOnline` 按账号在线判定,不承诺"几分钟内回复"; - 无定制师时 `peerName=null` 展示「暂无定制师」; - 281002 权限错误不透传原文案,用「刷新重试 / 订单负责人可能已变更」; - `groupChatUnreadCount` 是团队共享「待处理」,任一管理员读即全员清零。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 开启团期管理员会话(新增) | POST | `/admin/message/chat/open-group` | 新增接口 | 定制师 ↔ 团期管理员团队会话,键 `GROUP:{orderId}` | | 2 | 开启会话(通用,GROUP 分流) | POST | `/admin/message/chat/open` | 修改接口 | `bizModule` 新增 `GROUP`,分流到 open-group(`peerAdminId` 忽略,不产生 pair 键) | | 3 | 会话列表 | GET | `/admin/message/chat/conversations` | 修改接口 | `bizModule` 过滤新增 `GROUP`;`peerRole / peerRoleLabel` 新增 `GROUP_ADMIN / 团期管理员`;持码者团队共享未读 | | 4 | 消息列表 | GET | `/admin/message/chat/{conversationKey}/messages` | 修改接口 | 支持 `GROUP:{orderId}` 键;`senderRole` 新增 `GROUP_ADMIN` | | 5 | 发送消息 | POST | `/admin/message/chat/{conversationKey}/messages` | 修改接口 | 定制师发 → 团队池;管理员发 → 定制师个人行 | | 6 | 标记已读 | POST | `/admin/message/chat/{conversationKey}/read` | 修改接口 | 持码者读 → 团队水位推进、全员清零、定制师收 `im-chat-read` | | 7 | 聊天 Tab 未读总数 | GET | `/admin/message/chat/unread-total` | 修改接口 | 当前角色持码时合入 GROUP 团队池未读 | | 8 | 子订单行程(住宿安排卡) | GET | `/v3/admin/order/{id}/itinerary` | 修改接口 | 新增 `groupBatchId`(按钮显隐依据)、`groupUnreadMessageCount` | | 9 | 团期子订单名单 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 修改接口 | 每项新增 `groupChatUnreadCount`(团队共享「待处理」) | **SSE 契约**(`/ws/admin-msg/stream`,事件 `im-chat` / `im-chat-read`,非 REST):payload 新增 `targetPermissionCode`(String,可空);GROUP 团队信令为 `"group-batch:demand:confirm"`,只投递给当前角色持码的连接(实测:持码 ADMIN 收到,不持码 CUSTOMIZER 收不到,同一账号切到不持码角色后的旧连接也收不到)。`im-chat-read` 示例:`{"adminId":1002,"type":"READ","conversationKey":"GROUP:2096701865117831170","readerAdminId":1001,"lastReadMessageId":"2096701909673943042"}`。前端无需改解析,但请确认 SSE JSON 解析不拒绝未知字段。 **内部 Feign**(不经网关,前端不用):`POST /internal/message/chat/biz-unread-batch` `bizModule` 新增 `GROUP`(`unreadScope` TEAM / PERSONAL);`POST /internal/message/chat/reconcile-fleet-consultant` 路径不变、同时收敛 FLEET 与 GROUP;order-v3 `POST /internal/house/order-chat-summary-batch` 新增 `groupBatchId / groupBatchNo`。 --- ## 三、接口详情 ### 1. 开启团期管理员会话 `POST /admin/message/chat/open-group` **VO**: `ChatOpenGroupReqVO → ChatOpenFullRespVO` #### 使用场景 定制师或团期管理员打开与对方团队的会话;调用方为 hl-ui 子订单详情「住宿安排」卡「联系团期管理员」、团期详情「查看需求」名单表「联系定制师」。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `orderId` | Body | Long(String) | ✓ | @NotNull | 子订单 ID;**不接受** `peerAdminId`(传了被忽略) | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | `conversationKey` | String | `GROUP:{orderId}` | | `isNew` | Boolean | 开方成员行此前是否不存在 | | `peerAdminId` | Long | 定制师视角恒 0(团队占位);管理员视角 = 当前定制师 adminId(无定制师时 0) | | `peerName` | String | 定制师视角「团期管理员」;管理员视角 = 定制师姓名快照,**无定制师时 null**,前端展示「暂无定制师」 | | `peerRole` | String | 定制师视角 `GROUP_ADMIN`;管理员视角 `CUSTOMIZER` | | `peerRoleLabel` | String | 「团期管理员」或「定制师」 | | `peerOnline` | Boolean | 定制师视角 = 任一持码活跃账号在线;管理员视角 = 定制师在线。**按账号在线判定,不按当前角色**,不保证对方能立刻收到消息 | | `unreadCount` | Integer | 恒 0(打开即已读) | | `order` | ChatOrderCardVO | 订单卡:orderNo / teamNo / customerName / destination / tripDays / productName / adultCount / childCount / **groupBatchNo**(String) / **groupBatchId**(String);摘要不可达时为 null | | `thread` | ChatThreadRespVO | 最新消息线索:conversationKey / hasMore / nextCursor / list[] 消息数组 | | `unreadTotal` | Integer | 标记已读后顶部合并未读总数(不含 GROUP 团队池,与 unread-count REST 保持一致) | #### 请求示例 ```json POST /admin/message/chat/open-group { "orderId": 60123456789001 } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "conversationKey": "GROUP:60123456789001", "isNew": false, "peerAdminId": 0, "peerName": "团期管理员", "peerRole": "GROUP_ADMIN", "peerRoleLabel": "团期管理员", "peerOnline": true, "unreadCount": 0, "order": { "orderNo": "HL2609010001", "teamNo": "T20260901001", "customerName": "李四", "destination": "三亚", "tripDays": 4, "productName": "豪华蜜月游", "adultCount": 2, "childCount": 0, "groupBatchNo": "G20260901001", "groupBatchId": "1867000000001" }, "thread": { "conversationKey": "GROUP:60123456789001", "hasMore": false, "nextCursor": null, "list": [] }, "unreadTotal": 0 } } ``` #### 空数据 / 降级响应 摘要不可达或缺失时,`order` 为 null;`thread.list` 可为空。 #### 错误响应 ```json { "code": 281002, "message": "无权访问该会话", "success": false, "data": null } ``` #### 业务边界条目 - 打开即标记已读,`unreadCount` 恒 0 - 团队水位行 `admin_id=0` 自动创建,定制师与管理员各有成员行 - 定制师视角 `peerRole=GROUP_ADMIN`;管理员视角 `peerRole=CUSTOMIZER` - 无定制师的新团期:按钮不显示(`groupBatchId` 为空),`peerName` 返 null - 转单后旧定制师再开 → 281002,新定制师开 → 200 且承接未读 --- --- --- ### 2. 开启会话(通用,GROUP 分流) `POST /admin/message/chat/open` **VO**: `ChatOpenReqVO → ChatOpenFullRespVO` #### 使用场景 旧客户端沿用通用入口时传 `bizModule=GROUP`;新代码直接用 `open-group`。两者同一实现。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `bizModule` | Body | String | ✓ | — | 新增枚举值 `GROUP`(原 HOUSE / FLEET / DIRECT) | | `bizId` | Body | Long(String) | ✓ | — | 团期子订单 ID | | `peerAdminId` | Body | Long(String) | — | — | GROUP 下忽略:团队会话不按对端个人建键 | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | conversationKey | String | `GROUP:{orderId}` | | 其余字段 | — | 与 `open-group` 完全相同(见 1) | #### 请求示例 ```json POST /admin/message/chat/open {"bizModule": "GROUP", "bizId": "2096701865117831170", "peerAdminId": "123"} ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"conversationKey": "GROUP:2096701865117831170", "isNew": false, "peerAdminId": "0", "peerName": "团期管理员", "peerRole": "GROUP_ADMIN", "peerRoleLabel": "团期管理员", "peerOnline": true, "unreadCount": 0, "order": {"orderNo": "HL20260907044734031", "groupBatchNo": "Q202610012052935476548939777", "groupBatchId": "2096412454643802114"}, "thread": {"conversationKey": "GROUP:2096701865117831170", "hasMore": false, "list": []}, "unreadTotal": 1}} ``` #### 空数据 / 降级响应 同 `open-group`:order-v3 不可达时 `order` 为 null(会话仍可开)。 #### 错误响应 ```json {"code": 281002, "message": "无权访问该会话", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 281002 | 非该单当前定制师且当前角色不持 `group-batch:demand:confirm`;转单后旧定制师;order-v3 摘要不可达(失败关闭)。前端不透传原文案,用「刷新重试 / 订单负责人可能已变更」 | | 281015 | 非团期子订单 | | 281012 | bizId 为空 | #### 业务边界条目 - 实测 `peerAdminId=123` 也返回 `GROUP:{orderId}`,库内不出现 `GROUP:{id}:{a}:{b}` pair 键。 --- ### 3. 会话列表 `GET /admin/message/chat/conversations` **VO**: `查询参数 → PageResult` #### 使用场景 聊天 Tab 列表。团期管理员(持码角色)看到所有团期子订单的 GROUP 会话(团队共享未读);定制师看到自己名下团单的 GROUP 会话;不持码 ADMIN / 其他定制师看不到。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `bizModule` | Query | String | — | — | 新增 `GROUP`;不传时非团队模块列表**排除** FLEET / GROUP 团队行(与既有 FLEET 规则一致) | | `pageNo / pageSize` | Query | Integer | — | — | 不变 | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | list[].bizModule | String | `GROUP` | | list[].peerRole / peerRoleLabel | String | 定制师视角 `GROUP_ADMIN` / 团期管理员;管理员视角 `CUSTOMIZER` / 定制师 | | list[].peerName | String | 定制师视角「团期管理员」;管理员视角定制师姓名 | | list[].peerOnline | Boolean | 定制师视角 = 任一持码活跃账号在线(按账号不按当前角色,不保证对方能立刻收到) | | list[].unreadCount | Integer | 持码者为团队共享未读(任一持码者读即全员清零) | | list[].orderNo / teamNo | String | 订单号 / 团号 enrich | #### 请求示例 ```json GET /admin/message/chat/conversations?pageNo=1&pageSize=50&bizModule=GROUP ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"list": [{"conversationKey": "GROUP:2096701865117831170", "bizModule": "GROUP", "orderNo": "HL20260907044734031", "teamNo": "26-4983", "peerRole": "CUSTOMIZER", "peerRoleLabel": "定制师", "peerName": "test_admin", "unreadCount": 0, "peerOnline": true}], "total": 1}} ``` #### 空数据 / 降级响应 不持码的 ADMIN / 其他定制师:`list=[]`(实测 0 条);无角色 token 对团队模块失败关闭返空页。 #### 错误响应 ```json {"code": 401, "message": "Token无效或已过期", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 401 | 未登录 / token 失效(沿用既有) | #### 业务边界条目 - 会话卡 `peerRole` 与消息气泡 `senderRole` 的中文映射都要补 `GROUP_ADMIN → 团期管理员`。 - 未读语义是团队共享「待处理」,别写成「你的未读」。 --- ### 4. 消息列表 `GET /admin/message/chat/{conversationKey}/messages` **VO**: `查询参数 → ChatThreadRespVO` #### 使用场景 打开 GROUP 会话后拉取历史;`conversationKey=GROUP:{orderId}`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `conversationKey` | Path | String | ✓ | — | `GROUP:{orderId}` | | `cursor / pageSize` | Query | — | — | — | 不变 | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | list[].senderRole | String | 新增 `GROUP_ADMIN`(管理员回复);定制师广播为 null | | list[].readByPeer | Boolean | 任一持码者读后为 true(`im-chat-read` 同步推送) | #### 请求示例 ```json GET /admin/message/chat/GROUP:2096701865117831170/messages?pageSize=20 ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"conversationKey": "GROUP:2096701865117831170", "hasMore": false, "nextCursor": null, "list": [{"messageId": "2096701935376617473", "senderAdminId": "1001", "senderName": "admin", "senderRole": "GROUP_ADMIN", "msgType": "TEXT", "content": "管理员回复 1", "isMine": false, "readByPeer": false}]}} ``` #### 空数据 / 降级响应 无消息时 `list=[]`。 #### 错误响应 ```json {"code": 281002, "message": "无权访问该会话", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 281002 | 非该单当前定制师且当前角色不持 `group-batch:demand:confirm`;转单后旧定制师;order-v3 摘要不可达(失败关闭)。前端不透传原文案,用「刷新重试 / 订单负责人可能已变更」 | #### 业务边界条目 - 定制师端消息气泡对 `senderRole=GROUP_ADMIN` 显示「团期管理员」徽章。 --- ### 5. 发送消息 `POST /admin/message/chat/{conversationKey}/messages` **VO**: `ChatMessageSendReqVO → ChatMessageSendRespVO` #### 使用场景 定制师发 → 落团队池(全体持码者共享未读,SSE `im-chat` 定向到持码连接);管理员发 → 落定制师个人行(定制师顶部角标 / 收件箱照常 +1)。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `conversationKey` | Path | String | ✓ | — | `GROUP:{orderId}` | | `content` | Body | String | ✓ | — | 不变 | | `priority / msgType` | Body | String | — | — | 不变 | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | messageId | String | 不变 | #### 请求示例 ```json POST /admin/message/chat/GROUP:2096701865117831170/messages {"content": "请确认第二晚房型"} ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"messageId": "2096701909673943042"}} ``` #### 空数据 / 降级响应 无。 #### 错误响应 ```json {"code": 281002, "message": "无权访问该会话", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 281002 | 非该单当前定制师且当前角色不持 `group-batch:demand:confirm`;转单后旧定制师;order-v3 摘要不可达(失败关闭)。前端不透传原文案,用「刷新重试 / 订单负责人可能已变更」 | #### 业务边界条目 - 转单与 GROUP 发送 / 已读 / 开会话互斥(order-v3 转单入口与 user-service 共用同一把 `GROUP:{orderId}` 锁);实测与转单并发的发送被串行后按新归属拒绝(281002)。 - 新增 GROUP 日志只打标识符,不打客户姓名 / 消息内容。 --- ### 6. 标记已读 `POST /admin/message/chat/{conversationKey}/read` **VO**: `无 body → ChatReadRespVO` #### 使用场景 持码者读 → 团队水位推进、全员未读清零、给定制师补发 `im-chat-read`;定制师读 → 只推进自己行。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `conversationKey` | Path | String | ✓ | — | `GROUP:{orderId}` | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | unreadTotal | Integer | 标记后的顶部合并未读(不含 GROUP 团队池) | #### 请求示例 ```json POST /admin/message/chat/GROUP:2096701865117831170/read ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"unreadTotal": 13}} ``` #### 空数据 / 降级响应 无。 #### 错误响应 ```json {"code": 281002, "message": "无权访问该会话", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 281002 | 非该单当前定制师且当前角色不持 `group-batch:demand:confirm`;转单后旧定制师;order-v3 摘要不可达(失败关闭)。前端不透传原文案,用「刷新重试 / 订单负责人可能已变更」 | #### 业务边界条目 - 实测管理员读后名单表两持码账号 `groupChatUnreadCount` 同时 1→0,定制师收到 `im-chat-read`(`readerAdminId` 为读的管理员)。 --- ### 7. 聊天 Tab 未读总数 `GET /admin/message/chat/unread-total` **VO**: `无参 → ChatUnreadTotalRespVO` #### 使用场景 聊天 Tab 角标。**当前角色持码时合入 GROUP 团队池未读**;消息中心 `GET /admin/message/unread-count` 与顶部合并角标**不含**团队池。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | (无) | — | — | — | — | 无入参 | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | chatUnreadTotal | Integer | 持码者 = 个人未读 + GROUP 团队池未读(+ FLEET 池,车务管理员) | #### 请求示例 ```json GET /admin/message/chat/unread-total ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"chatUnreadTotal": 3}} ``` #### 空数据 / 降级响应 无。 #### 错误响应 ```json {"code": 401, "message": "Token无效或已过期", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 401 | 未登录 / token 失效 | #### 业务边界条目 - 实测定制师广播后持码 ADMIN 的 `chat/unread-total` 2→3,而 `/admin/message/unread-count` 13→13。 - 定制师收到管理员回复 → 个人行 → `unread-count` 0→1,前端不得过滤 `senderRole=GROUP_ADMIN` 的行。 --- ### 8. 子订单行程(住宿安排卡) `GET /v3/admin/order/{id}/itinerary` **VO**: `ItineraryVO` #### 使用场景 子订单详情「住宿安排」卡:「联系团期管理员」按钮显隐 + 角标。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `id` | Path | Long | ✓ | — | 子订单 ID | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | groupBatchId | String | 新增。统一归团解析结果(含历史兼容单 `product_batch_id` 反查);普通单 null。**按钮显隐只看它,不看 `OrderMainVO.groupBatchId`** | | groupUnreadMessageCount | Integer | 新增。当前登录定制师在本单 GROUP 会话的个人未读;非团单 / 降级 / 未登录 0 | #### 请求示例 ```json GET /v3/admin/order/2096701865117831170/itinerary ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": {"groupBatchId": "2096412454643802114", "groupUnreadMessageCount": 1, "unreadMessageCount": 0}} ``` #### 空数据 / 降级响应 普通单:`"groupBatchId": null, "groupUnreadMessageCount": 0`;user-service 不可达 → `groupUnreadMessageCount=0`,其余字段正常。 #### 错误响应 ```json {"code": 582001, "message": "订单不存在", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 582001 | 订单不存在(沿用既有) | #### 业务边界条目 - `groupBatchId` 与 `OrderMainVO.groupBatchId` 语义不同(后者是原始列,历史兼容单可能为空)。 --- ### 9. 团期子订单名单 `GET /v3/admin/order/group-batch/{groupBatchId}/orders` **VO**: `查询参数 → List` #### 使用场景 团期详情「查看需求」名单表每户「联系定制师」+ 角标。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `groupBatchId` | Path | Long | ✓ | — | 团期 ID | | `includeTravelers / includeNeeds / includeCancelled` | Query | Boolean | — | — | 不变 | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | [].groupChatUnreadCount | Integer | 新增。团队共享「待处理」未读(TEAM 口径):任一管理员读即全员清零,**不是**当前登录人个人未读;降级 0 | #### 请求示例 ```json GET /v3/admin/order/group-batch/2096412454643802114/orders ``` #### 响应示例 ```json {"code": 200, "message": "成功", "success": true, "data": [{"orderId": "2096701865117831170", "orderNo": "HL20260907044734031", "groupChatUnreadCount": 1}]} ``` #### 空数据 / 降级响应 user-service 不可达或超时 → 全部 0,主列表照常返回。 #### 错误响应 ```json {"code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "data": null, "success": false} ``` | 码 | 触发 | |---|---| | 589507 | 无 `group-batch:view`(沿用既有守卫) | | 589500 | 团期不存在 | #### 业务边界条目 - 文案用「待处理 / 待回复」而非「未读消息」;本字段只供页面级加载,不要做高频轮询。 --- ## 四、契约约束与正确调用方式 ### 授权规则 会话准入:①当前定制师本人(token 角色 CUSTOMIZER 且 me == consultant_id)或②团期管理员(token 当前角色持权限码 `group-batch:demand:confirm`);均不满足返 281002。 ### 转单锁定 order-v3 转单入口:FLEET 锁 → 判团 → GROUP 锁 → 事务。普通单不取 GROUP 锁。并发互斥。 --- ## 五、数据库行为 - **无表结构变更、无 DDL**(现有列宽已足); - **权限种子** `V20260906_006`(INSERT 权限码 + 关联)由 #7210 落地;若本单先合则自带; - **数据形态**:admin_message 新增 bizModule=GROUP 行;sender_role=NULL(定制师广播)或 GROUP_ADMIN(管理员回复);admin_conversation_member 新增 admin_id=0 团队行与个人行。 --- ## 六、边界行为 - 打开即已读;团队水位行自动创建; - 定制师广播到全体持码账号;无定制师时 peerName=null; - 权限缓存 TTL 10 分钟;撤权生效靠 TTL 或运维 `DEL admin_permissions:v2:role:{roleKey}`; - 团期软删时订单卡 `groupBatchNo=null`;前端用「团期已归档」占位。 --- ## 七、不影响范围 - 车务(FLEET)聊天流程与数据; - 房 / 车需求创建/修改; - 抢单池流程; - 消息中心界面(GROUP 池行暂未纳管,后续单独工单); - 网关路由。 --- ## 八、测试环境已验证 - 部署:dev-v3 `072a476be`(PR-A #7225,order-v3)+ `f4251d946`(PR-B #7226,user-service),测试服 `BRANCH=dev-v3 deploy-backend.sh` 两服务已滚;lock4j 键前缀两服务均默认值。 - 网关实测(团 A `2096412454643802114`,定制师 test_admin / 持码 ADMIN admin / SUPER_ADMIN wx / 不持码 qulili):AC-1 open-group 200(`GROUP:{orderId}`、`peerRole=GROUP_ADMIN`、订单卡 `groupBatchNo=Q202610012052935476548939777` / `groupBatchId`);AC-2 普通单 281015;AC-3 不持码 281002、短信登录临时 CUSTOMIZER(DB 角色 ADMIN)281002、撤码后 `DEL admin_permissions:v2:role:ADMIN` → 281002、恢复 → 200;AC-4 未提需求 200;AC-5 定制师广播落 `admin_id=0` 池行、名单表 TEAM 未读两账号同值 1、管理员读后全员 0 且定制师收 `im-chat-read`;AC-6 管理员回复落定制师个人行 `sender_role=GROUP_ADMIN`、SSE `im-chat` 到达、itinerary `groupUnreadMessageCount=1`、`unread-count` 0→1;AC-7 持码 ADMIN 收到含 `targetPermissionCode` 的 `im-chat`、不持码收不到、切角色旧连接收不到,`chat/unread-total` 2→3 而 `unread-count` 不变;AC-8 会话列表 GROUP 过滤命中(orderNo / teamNo / peerRoleLabel)、不持码 0 条;AC-9 通用 open 同键无 pair 键;AC-10 转单后旧行 ARCHIVED / 新行 ACTIVE / 旧定制师 281002、并发发送被串行后拒绝;AC-10b 历史兼容单 open-group 200 且 itinerary `groupBatchId` 非空;AC-12 / AC-13 字段与上述一致。 --- ## 十、相关文档 - 工单正文:#7211 - 架构方案:#7211 评论「架构方案」 - 接口文档:docs/group/团期模块接口文档-v2.0.html §0C.11.3 --- ## 关联 / 联系人 ### 链接 - **Issue**: [#7211](https://git.1814.love:8443/wx/HL/issues/7211) - **PR(order-v3)**: [#7225](https://git.1814.love:8443/wx/HL/pulls/7225) - **PR(user-service)**: [#7226](https://git.1814.love:8443/wx/HL/pulls/7226) ### 联系人 - **后端负责人**: wx - **前端负责人(hl-ui)**: mmg