From bab71b6bdfd06da34d675c0212c3833a42f6b037 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 7 Sep 2026 04:57:00 +0800 Subject: [PATCH] =?UTF-8?q?changelog:=20#7211=20=E5=AE=9A=E5=88=B6?= =?UTF-8?q?=E5=B8=88=E2=86=94=E5=9B=A2=E6=9C=9F=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E7=AB=99=E5=86=85=E4=BC=9A=E8=AF=9D=20GROUP:{orderId}=E2=80=94?= =?UTF-8?q?=E2=80=94open-group=20=E6=96=B0=E5=A2=9E=E3=80=81=E8=81=8A?= =?UTF-8?q?=E5=A4=A9/=E5=88=97=E8=A1=A8/=E5=B7=B2=E8=AF=BB/=E6=9C=AA?= =?UTF-8?q?=E8=AF=BB=E6=94=B9=E9=80=A0=E3=80=81itinerary=20=E4=B8=8E?= =?UTF-8?q?=E5=9B=A2=E6=9C=9F=E5=90=8D=E5=8D=95=E8=A1=A8=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E3=80=81SSE=20targetPermissionCode=EF=BC=88?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3=C2=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01XYL5S9SsBtkg7aGyFAbrrQ --- ...ˆ团期管理员站内会话GROUP-新增接口-管理后台.md | 607 ++++++++++++++++++ 1 file changed, 607 insertions(+) create mode 100644 changelogs-v2/2026-09/07_7211_定制师团期管理员站内会话GROUP-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/07_7211_定制师团期管理员站内会话GROUP-新增接口-管理后台.md b/changelogs-v2/2026-09/07_7211_定制师团期管理员站内会话GROUP-新增接口-管理后台.md new file mode 100644 index 00000000..ccf8f695 --- /dev/null +++ b/changelogs-v2/2026-09/07_7211_定制师团期管理员站内会话GROUP-新增接口-管理后台.md @@ -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` + +#### 使用场景 +聊天 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