changelog: #7211 定制师↔团期管理员站内会话 GROUP:{orderId}——open-group 新增、聊天/列表/已读/未读改造、itinerary 与团期名单表新增字段、SSE targetPermissionCode(新增接口·管理后台)
changelog-filename-gate / validate (push) Successful in 2s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYL5S9SsBtkg7aGyFAbrrQ
这个提交包含在:
API Changelog Bot
2026-09-07 04:57:00 +08:00
共同撰写人 Claude Fable 5.1
父节点 1548a68d4b
当前提交 bab71b6bdf
@@ -0,0 +1,607 @@
---
schema: "hl-changelog/v2"
ticket: "7211"
title: "定制师 ↔ 团期管理员站内会话(GROUP:{orderId})"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-07"
status_note: "后端已合 dev-v3(PR #7225 order-v3、#7226 user-service)并部署测试服,网关实测 AC-1~AC-13 全部通过;GROUP 团队池不计入消息中心角标,定制师收到管理员回复照常计入。"
updated_at: "2026-09-07"
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<ChatConversationRespVO>`
#### 使用场景
聊天 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<GroupBatchOrderItemRespVO>`
#### 使用场景
团期详情「查看需求」名单表每户「联系定制师」+ 角标。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `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