From 5a5ca03e0f92ab373e7704b6f0f589372b4e2e4b Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Fri, 11 Sep 2026 17:31:04 +0800 Subject: [PATCH] =?UTF-8?q?docs(7440):=20=E4=BA=A4=E6=8E=A5=E4=BB=B6?= =?UTF-8?q?=E8=A1=A5=204=20=E4=B8=AA=E6=94=B9=E9=80=A0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20+=20=E5=8F=8C=E4=BE=A7=E6=9C=AA=E8=AF=BB=E5=8F=96=E6=B3=95?= =?UTF-8?q?=20+=20=E4=BC=9A=E8=AF=9D=E9=94=AE=E4=B8=8D=E6=B7=B7=E5=90=8C?= =?UTF-8?q?=20Refs=20#7440?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AC-19 要求的三块内容原先在文件里 grep 零命中,校验器也只解析出 4 个端点: - 新增「改造接口」4 节(messages 拉取/投递、read 双侧水位、biz-unread-batch 新增 TEAM_FLEET 取值),每节含方法/路径/路径参数/请求参数/响应/错误码 - 写死「双侧未读要分别取」:车务侧传 unreadScope=TEAM_FLEET、团期管理员侧传 TEAM, 传错不报错、会拿到对方的未读数 - 写明 GROUP_FLEET:{groupBatchId} 与「接送机沟通仍走 FLEET:{orderId}」,两个入口不混同 - 两条 internal 接口的清单编号由 *内部* 改为裸 5/6——校验器对非裸编号的端点标题 是静默跳过的,改前它们根本没进校验 - 补错误码汇总表(281018/281019/600012-600014/809400/809401,文案逐字抄源码) - 六.6「修改前后对比」由 N/A 换成 4 个改造端点的行为级对比 端点数 4 → 10(6 新增 + 4 改造),与工单口径一致。 另留痕一处信源冲突:biz-unread-batch 的 adminId,工单正文写「PERSONAL 时必填」, 而 ChatBizUnreadBatchReqVO.java:23-25 是 @NotNull(恒必填,与 unreadScope 无关)。 交接件按源码写,并在字段说明里标注与旧文档描述不一致、以源码为准。 --- ..._团期车务派单只读入口-新增接口-管理后台.md | 559 +++++++++++++++++- 1 file changed, 552 insertions(+), 7 deletions(-) diff --git a/changelogs-v2/2026-09/11_7440_团期车务派单只读入口-新增接口-管理后台.md b/changelogs-v2/2026-09/11_7440_团期车务派单只读入口-新增接口-管理后台.md index 10a759b4..448877b5 100644 --- a/changelogs-v2/2026-09/11_7440_团期车务派单只读入口-新增接口-管理后台.md +++ b/changelogs-v2/2026-09/11_7440_团期车务派单只读入口-新增接口-管理后台.md @@ -29,6 +29,10 @@ base: "dev-v3" 本单仅含只读入口,不含配车写入、不含派单进度回写(配车写入与派单进度回写在后续工单)。 +🔴 **双侧未读必须分开取,传错不报错、只是拿到对方的数字**:车务侧红点取 `unreadScope=TEAM_FLEET`(`admin_id=-1` 水位),团期管理员侧取 `unreadScope=TEAM`(`admin_id=0` 水位)。同一个 `groupBatchId`、两个 scope 会各自返回一个看着都合理的数——接口不会因为传错而报错或返0,前端按当前登录角色选对 scope 是唯一正确性保障。详见 10 号端点。 + +🔴 **`GROUP_FLEET:{groupBatchId}` 与 `FLEET:{orderId}` 是两条互不相干的会话,别混成一个入口**:`GROUP_FLEET:{groupBatchId}` 是本单新建的团期配车团级会话(车务团队 ↔ 团期管理员团队);接送机沟通**仍走已有的 `FLEET:{orderId}`**(一个订单一条车务会话),本单**不改**该链路——对团期子订单调 `open-fleet` 仍返回 `FLEET:{orderId}`,不会因为该订单属于某个团期就产生任何 `GROUP_FLEET` 记录。 + --- ## 一、背景 @@ -45,8 +49,12 @@ base: "dev-v3" | 2 | 团期配车总览 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | 新增 | 单团详情页派车总览 | | 3 | 资源排班 | GET | `/admin/fleet/group-dispatch/resource-schedule` | 新增 | 派单编辑时查资源占用 | | 4 | 打开团期配车会话 | POST | `/admin/message/chat/open-group-fleet` | 新增 | 车务↔团期管理员团级会话 | -| *内部* | 待配车候选分页 | POST | `/v3/internal/group-batch/vehicle-dispatch-candidates` | 新增 | 服务间接口,前端无需对接 | -| *内部* | 单团用车覆盖 | GET | `/v3/internal/group-batch/{groupBatchId}/vehicle-coverage` | 新增 | 服务间接口,前端无需对接 | +| 5 | 待配车候选分页 | POST | `/v3/internal/group-batch/vehicle-dispatch-candidates` | 新增 | 服务间接口(order-v3),前端无需对接,随附供核对契约边界 | +| 6 | 单团用车覆盖 | GET | `/v3/internal/group-batch/{groupBatchId}/vehicle-coverage` | 新增 | 服务间接口(order-v3),前端无需对接,随附供核对契约边界 | +| 7 | 拉线程消息 | GET | `/admin/message/chat/{conversationKey}/messages` | 改造 | 对 `GROUP_FLEET:` 键授权分流,路径/入参/响应结构不变 | +| 8 | 发一条消息 | POST | `/admin/message/chat/{conversationKey}/messages` | 改造 | 新增团级投递分支,路径/入参/响应结构不变 | +| 9 | 标记已读到最新 | POST | `/admin/message/chat/{conversationKey}/read` | 改造 | 双侧水位各自推进,路径/入参/响应结构不变 | +| 10 | 批量业务对象未读 | POST | `/internal/message/chat/biz-unread-batch` | 改造 | `unreadScope` 新增 `TEAM_FLEET` 取值,请求体不加字段 | --- @@ -377,15 +385,540 @@ GET /admin/fleet/group-dispatch/resource-schedule?resourceType=VEHICLE&resourceI - 双团队会话 - 前置:团期活跃 +--- + +### 5. 待配车候选分页 `POST /v3/internal/group-batch/vehicle-dispatch-candidates` + +**VO**: `GroupBatchVehicleDispatchCandidateReqDTO → Result>` + +#### 使用场景 + +供 `hl-fleet-service` 的「待配车团期清单」(本文档 1 号端点)内部经 Feign 调用,拉团期主数据与权威服务日;**仅限服务间调用,前端不直接对接**,本节随附是为让消费方核对契约边界,不是要求前端联调本接口。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| departDateFrom | Body | LocalDate | ❌ | - | 出发日下界(含),不传=不限 | +| departDateTo | Body | LocalDate | ❌ | - | 出发日上界(含),不传=不限;与 From 同传须 From≤To,否则 809401 | +| keyword | Body | String | ❌ | trim 后 ≤50 字符 | 团号/团名模糊,超长 809401 | +| requirementConfirmed | Body | Boolean | ❌ | - | 只返需求已确认的团期,不传=不限 | +| vehicleReady | Body | Boolean | ❌ | - | 只返配车未就绪的团期,不传=不限 | +| batchStatuses | Body | List | ❌ | - | 团期状态白名单,不传/空=提供方默认成团后阶段集 | +| page | Body | Integer | ❌ | ≥1,不传默认1 | 传了但<1抛809401(不静默纠正) | +| pageSize | Body | Integer | ❌ | 1-100,不传默认20 | 传了但越界抛809401(不截断) | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(转String) | 团期主订单 ID | +| batchNo | String | 团号 | +| batchName | String | 团名 | +| batchStatus | String | 团期生命周期状态(透传) | +| departDate | LocalDate | 出团日 | +| endDate | LocalDate | 返团日 | +| serviceDates | List | 权威服务日集合(与 dispatch-baseline 同源同算法) | +| enrolledOrders | Integer | 在团子订单数 | +| enrolledPeople | Integer | 在团人数 | +| requirementConfirmed | Boolean | 整团需求是否已确认 | +| vehicleReady | Boolean | 配车是否已就绪 | + +#### 请求示例 + +```json +{ + "departDateFrom": "2026-09-01", + "departDateTo": "2026-09-30", + "requirementConfirmed": true, + "vehicleReady": false, + "page": 1, + "pageSize": 20 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "total": 12, + "list": [ + { + "groupBatchId": "1934567890123456789", + "batchNo": "GB-26-0912-01", + "batchName": "额吉的故乡 9/12 团", + "batchStatus": "RESOURCE_PREPARING", + "departDate": "2026-09-12", + "endDate": "2026-09-16", + "serviceDates": ["2026-09-12", "2026-09-13", "2026-09-14", "2026-09-15", "2026-09-16"], + "enrolledOrders": 6, + "enrolledPeople": 17, + "requirementConfirmed": true, + "vehicleReady": false + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "data": {"total": 0, "list": []}, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 809401, + "message": "查询参数非法", + "success": false +} +``` + +#### 业务边界 + +- **本 DTO 无 Bean Validation 注解**(`GroupBatchVehicleDispatchCandidateReqDTO` javadoc 明写),全部校验在 `GroupBatchVehicleDispatchQueryService` 的私有方法里 fail-closed 完成;绕过该 Service 直接构造本 DTO 调用的消费方不受任何保护。 +- page/pageSize 越界**不做静默纠正/截断**,一律 809401——截断会让调用方以为拿到了完整结果。 +- 出发日区间倒挂在 SQL 上恒空结果集,本端点选择直接判 809401 而不是静默返空页。 +- 本端点**不含任何配车进度字段**(已排车日数/进度枚举/接送机未配计数),这些是 fleet 侧业务表的事实,由 fleet 拿到候选后自己在内存合并。 + +--- + +### 6. 单团用车覆盖 `GET /v3/internal/group-batch/{groupBatchId}/vehicle-coverage` + +**VO**: `Long → Result` + +#### 使用场景 + +供 `hl-fleet-service` 的「团期配车总览」(本文档 2 号端点)内部经 Feign 调用,拉逐户用车覆盖与接送机声明。**仅限服务间调用,前端不直接对接**。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(转String) | 团期主订单 ID | +| batchNo / batchName | String | 团号 / 团名 | +| departDate / endDate | LocalDate | 出团日 / 返团日 | +| serviceDates | List | 权威服务日集合(与 dispatch-baseline 同源,消费方不另算) | +| requirementConfirmed / vehicleReady | Boolean | 团期两个标志位 | +| orders | List | 逐户覆盖项,见下 | + +`orders[]` 逐项字段:`orderId`(Long转String)/ `orderNo` / `customerName` / `headcount`(Integer)/ `vehicleControlStatus` / `travelRequirementId`(Long转String,无有效需求为null)/ `travelRequirementStatus`(无有效需求为null)/ `transferDeclared`(Boolean)/ `transferArrivalDates`(List,原始声明值)/ `transferDepartureDates`(List,原始声明值)。 + +#### 请求示例 + +```http +GET /v3/internal/group-batch/1934567890123456789/vehicle-coverage +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "groupBatchId": "1934567890123456789", + "batchNo": "GB-26-0912-01", + "batchName": "额吉的故乡 9/12 团", + "departDate": "2026-09-12", + "endDate": "2026-09-16", + "serviceDates": ["2026-09-12", "2026-09-13", "2026-09-14", "2026-09-15", "2026-09-16"], + "requirementConfirmed": true, + "vehicleReady": false, + "orders": [ + { + "orderId": "1934567890123400000", + "orderNo": "26-0503", + "customerName": "赵先生", + "headcount": 3, + "vehicleControlStatus": "PENDING_REVIEW", + "travelRequirementId": "1934567890123400001", + "travelRequirementStatus": "PENDING_REVIEW", + "transferDeclared": true, + "transferArrivalDates": ["2026-09-11"], + "transferDepartureDates": ["2026-09-17"] + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无(团期不存在直接 809400,不返回空对象)。 + +#### 错误响应 + +```json +{ + "code": 809400, + "message": "团期不存在", + "success": false +} +``` + +#### 业务边界 + +- 团期不存在或已软删一律 809400,fleet 侧据此失败关闭,**不得渲染成空覆盖**。 +- `transferArrivalDates` / `transferDepartureDates` 给**未过滤的原始声明值**——不得复用 `PickupDropoffGateResolver.arrivalPickupRequiredDates` 那套与行程日取交集的口径,那会把窗外(到达日=出发日前一天、离开日=返团日后一天)的接送日整体滤掉,导致车务看到的缺口凭空少一半。 +- `travelRequirementId` / `travelRequirementStatus` **当前口径是该户唯一活跃用车需求,不区分 TRAVEL/TRANSFER**(`order_vehicle_requirement.requirementType` 目前只有 HOTEL/VEHICLE/ALL,接送机是同一行需求上的 pickup_required/dropoff_required 标志位),待 #7441 引入需求类型区分后收口,字段名与当前语义暂不完全一致。 + +--- + +**改造接口**(以下 4 个端点路径、入参、响应结构**全部不变**,变的是授权判定 / 投递分支 / 已读水位 / `unreadScope` 可选值——前端按现有对接方式不动代码即可,仅需理解下列行为差异) + +### 7. 拉线程消息(授权分流) `GET /admin/message/chat/{conversationKey}/messages` + +**VO**: `ChatThreadPageReqVO → Result` + +#### 使用场景 + +会话内上滑加载更早历史消息(首屏消息由 open 系列接口已带出)。本次改造只影响 `GROUP_FLEET:{groupBatchId}` 键的鉴权路径,FLEET/GROUP/GROUP_HOUSE/DIRECT 四类既有键的调用方式与行为**逐字不变**。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| conversationKey | Path | String | ✅ | - | 会话键;本单场景传 `GROUP_FLEET:{groupBatchId}` | +| beforeId | Query | Long | ❌ | - | 游标,拉该messageId之前的历史;首屏不传=拉最新一页 | +| pageSize | Query | Integer | ❌ | 1-50,默认20 | 每页条数 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| conversationKey | String | 会话键 | +| hasMore | Boolean | 是否还有更早历史 | +| nextCursor | Long | 下次游标(本页最小messageId),无更多为空 | +| list | List | 消息列表(按时间正序),逐项含messageId/senderAdminId/senderName/senderRole/msgType/priority/content/isMine/readByPeer/sentAt | + +#### 请求示例 + +```http +GET /admin/message/chat/GROUP_FLEET:1934567890123456789/messages?pageSize=20 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "conversationKey": "GROUP_FLEET:1934567890123456789", + "hasMore": false, + "nextCursor": null, + "list": [ + { + "messageId": 900001, + "senderAdminId": 205, + "senderName": "车务·老王", + "senderRole": "VEHICLE_TEAM", + "msgType": "TEXT", + "priority": "NORMAL", + "content": "9/12 那天大巴上午先送机场", + "isMine": true, + "readByPeer": false, + "sentAt": "2026-09-11 09:02:31" + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "data": {"conversationKey": "GROUP_FLEET:1934567890123456789", "hasMore": false, "nextCursor": null, "list": []}, + "success": true +} +``` + +#### 错误响应 + +```json +{ + "code": 281018, + "message": "无权访问该团期配车会话", + "success": false +} +``` + +#### 业务边界 + +- **改前**:无 `GROUP_FLEET` 键这回事。**改后**:`teamChatAuthorizationService.assertConversationAccess` 对 `GROUP_FLEET:` 前缀分流到团级授权——按 `groupBatchId` 判团期存在,再判当前 token 角色是否属于车务侧(`VEHICLE_MANAGER`)或团期管理员侧(持 `group-batch:demand:confirm`),两者都不是返 **281018**;**不解析订单、不查订单摘要**。 +- **281019 在本端点不生效**——团期终态/不存在只卡 `open` 与 `send`,历史消息仍可审计读取。 +- FLEET/GROUP/GROUP_HOUSE/DIRECT 四类既有键的授权行为逐字不变,281001(会话不存在)/281002(非成员)两个既有码继续适用。 + +--- + +### 8. 发一条消息(新增团级投递分支) `POST /admin/message/chat/{conversationKey}/messages` + +**VO**: `ChatMessageSendReqVO → Result` + +#### 使用场景 + +在 `GROUP_FLEET` 会话发一条文本/图片消息。本次改造只新增一条团级投递分支,`ChatMessageSendReqVO` 的字段与既有校验**逐字不变**。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| conversationKey | Path | String | ✅ | - | 会话键 | +| content | Body | String | ✅ | `@NotBlank`;长度>1000 抛281011(Service层校验,非Bean Validation注解) | 文本内容;命中本地敏感词替换为`*`,不报错 | +| priority | Body | String | ❌ | `NORMAL` / `URGENT` | 优先级,默认NORMAL | +| msgType | Body | String | ❌ | `TEXT` / `IMAGE` / `SYSTEM` | 消息类型,默认TEXT | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| messageId | Long | 新消息ID | +| conversationKey | String | 会话键 | +| sentAt | String | 发送时间(yyyy-MM-dd HH:mm:ss) | + +#### 请求示例 + +```json +{ + "content": "9/12 那天大巴上午先送机场", + "msgType": "TEXT", + "priority": "NORMAL" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "messageId": 900001, + "conversationKey": "GROUP_FLEET:1934567890123456789", + "sentAt": "2026-09-11 09:02:31" + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无。 + +#### 错误响应 + +```json +{ + "code": 281011, + "message": "消息内容不合法", + "success": false +} +``` + +```json +{ + "code": 281018, + "message": "无权访问该团期配车会话", + "success": false +} +``` + +#### 业务边界 + +- **改前**:无 `GROUP_FLEET` 分支。**改后**:`ChatMessageService.sendInternal`(`:216`)对 `GROUP_FLEET` 键新增判据 `dualTeam = authorizedTeam && keyModule.isDualTeam()`(`:230`):收件方**恒取对侧占位adminId**——车务发→`admin_message.admin_id=0`(`TEAM_ADMIN_ID`,团期管理员侧)、`sender_role=VEHICLE_TEAM`;团期管理员发→`admin_id=-1`(`TEAM_FLEET_ADMIN_ID`,车务侧)、`sender_role=GROUP_ADMIN`;`biz_module`/`biz_type`落`GROUP_FLEET`、`biz_id`落`groupBatchId`。**bump对侧团队行未读、refresh本侧团队行预览。** +- FLEET/GROUP 的既有投递路径(`teamPool`/`peer`计算)**逐字不变**,不受本次改造影响。 +- 281004(限流)与281011(内容非法,>1000字符或空白)两个既有码继续适用,判定仍在`ChatMessageService`而非VO注解层。 + +--- + +### 9. 标记已读到最新(双侧水位各自推进) `POST /admin/message/chat/{conversationKey}/read` + +**VO**: `无请求体 → Result` + +#### 使用场景 + +把某会话标记为已读到最新,成员水位推进 + `unread_count=0`。本次改造只影响双团队会话(`GROUP_FLEET`)的水位推进范围。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| conversationKey | Path | String | ✅ | - | 会话键 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| conversationKey | String | 会话键 | +| unreadCount | Integer | 标记后该会话未读数(恒0) | +| lastReadMessageId | Long | 已读水位(最新messageId),无消息为空 | + +#### 请求示例 + +```http +POST /admin/message/chat/GROUP_FLEET:1934567890123456789/read +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "conversationKey": "GROUP_FLEET:1934567890123456789", + "unreadCount": 0, + "lastReadMessageId": 900001 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无(无消息时 `lastReadMessageId` 为null,`unreadCount` 仍为0)。 + +#### 错误响应 + +```json +{ + "code": 281018, + "message": "无权访问该团期配车会话", + "success": false +} +``` + +#### 业务边界 + +- 🔴 **这是本单对既有实现的关键修正**:`ChatMessageService.markTeamReadInternal`(`:941`/`:958` 两个重载)对`GROUP_FLEET`(`isDualTeam()`为true,判据见`:995`/`:1008`)**只推进本侧那一条团队水位、只清本侧收件池行,对侧未读一条不动**——车务标已读推`admin_id=-1`那行,团期管理员标已读推`admin_id=0`那行,互不覆盖。 +- FLEET/GROUP 两个既有团队会话(两者都是单团队+定制师个人侧)的已读口径**逐字不变**,仍推进共享的`TEAM_ADMIN_ID=0`那一条水位。 +- READ 回执按对侧角色/权限码定向广播(车务标已读→广播给团期管理员侧连接;反之亦然)。 + +--- + +### 10. 批量业务对象未读(`unreadScope` 新增 `TEAM_FLEET`) `POST /internal/message/chat/biz-unread-batch` + +**VO**: `ChatBizUnreadBatchReqVO → Result` + +#### 使用场景 + +fleet 团期配车清单(本文档 1 号端点)批量取一批团期的车务侧未读,渲染清单行内红点。**仅限内部 Feign 调用**,fleet 侧 Feign 客户端 `board/port/ChatUnreadFeignClient.java` 签名不变。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| adminId | Body | Long | ✅ | `@NotNull` | 查谁的未读(列表当前登录人)——⚠️源码为**恒必填**,不因`unreadScope`取值而变(与部分旧文档「PERSONAL时才必填」的描述不一致,以`ChatBizUnreadBatchReqVO.java:23-25`为准) | +| bizModule | Body | String | ✅ | `@NotNull` | 业务分类:`HOUSE`/`FLEET`/`GROUP`/`GROUP_HOUSE`/`GROUP_FLEET`,本单场景传`GROUP_FLEET` | +| unreadScope | Body | String | ❌ | 正则`TEAM\|PERSONAL\|TEAM_FLEET` | **本单新增可选值`TEAM_FLEET`**;`FLEET`/`GROUP`/`GROUP_HOUSE`/`GROUP_FLEET`默认`TEAM`,`HOUSE`忽略本字段 | +| bizIds | Body | List | ✅ | `@NotEmpty`,最多200个 | 一批业务对象id;`GROUP_FLEET`场景传`groupBatchId`集合 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| unreads | List | 逐项`bizId`(String)+`unreadCount`(Integer);无会话的对象统一返0 | + +#### 请求示例 + +```json +{ + "adminId": 205, + "bizModule": "GROUP_FLEET", + "unreadScope": "TEAM_FLEET", + "bizIds": [1934567890123456789, 1934567890123456790] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "data": { + "unreads": [ + {"bizId": "1934567890123456789", "unreadCount": 2}, + {"bizId": "1934567890123456790", "unreadCount": 0} + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "data": {"unreads": []}, + "success": true +} +``` + +#### 错误响应 + +本端点无专属业务错误码,走 `@Valid` 统一 400(`GlobalExceptionHandler.handleValidation`,HTTP 状态码仍是 200): + +```json +{ + "code": 400, + "message": "unreadScope 仅支持 TEAM / PERSONAL / TEAM_FLEET", + "success": false +} +``` + +(`adminId`/`bizModule` 缺失、`bizIds` 为空或超 200 个同走该通道,文案分别取自 `ChatBizUnreadBatchReqVO.java` 对应字段的校验注解 message,源码逐字。) + +#### 业务边界 + +- 🔴 **双侧未读要分别取,传错拿到的是对方的数字,不是报错、不是0**:车务侧红点必须传`unreadScope=TEAM_FLEET`(取`admin_id=-1`水位);团期管理员侧必须传`TEAM`(取`admin_id=0`水位)。两个scope对同一个`groupBatchId`会返回**两个不同但看起来都合理**的数字,前端按角色选对scope是唯一正确性保障,接口本身不会因为传错而报错。 +- `bizModule`传非`GROUP_FLEET`时传`TEAM_FLEET`按`TEAM`处理并记WARN日志,不抛错(`ChatMessageService.java:1134` `fleetTeamScope && !module.isDualTeam()`分支)。 +- `TeamChatModule.bizModules()`(现返回四值`["FLEET","GROUP","GROUP_HOUSE","GROUP_FLEET"]`)纳入`GROUP_FLEET`只影响候选源排除逻辑,不改变本端点的计数口径。 + + --- ## 四、契约约束与正确调用方式 +### 错误码汇总(本单新增 7 个,逐条实测见各接口详情的「错误响应」) + +| 码 | 服务 | 符号 | 文案(源码逐字) | 触发条件 | +|---|---|---|---|---| +| 600012 | hl-fleet-service | `GROUP_BATCH_BASELINE_UNREACHABLE` | 团期配车基线不可达,请稍后重试 | order-v3 内部读口 Feign 降级或返回非成功,fleet 侧一律失败关闭(1/2 号端点) | +| 600013 | hl-fleet-service | `SCHEDULE_QUERY_PARAM_INVALID` | 排班查询参数非法: {0} | 日期区间倒挂 / 分页越界 / 跨度超31天 / 枚举非法(1/3 号端点) | +| 600014 | hl-fleet-service | `SCHEDULE_RESOURCE_TYPE_UNSUPPORTED` | 不支持的资源类型: {0} | `resourceType` 不是 `VEHICLE`/`DRIVER`(3 号端点) | +| 809400 | hl-order-service-v3 | `GROUP_BATCH_NOT_FOUND` | 团期不存在 | `groupBatchId` 查无团期或已软删(6 号端点,内部) | +| 809401 | hl-order-service-v3 | `QUERY_PARAM_INVALID` | 查询参数非法 | page<1 / pageSize不在1-100 / 出发日区间倒挂 / keyword超50字符,四种情形共用同一个码(5 号端点,内部) | +| 281018 | hl-user-service | `CHAT_GROUP_FLEET_ACCESS_DENIED` | 无权访问该团期配车会话 | 当前 token 角色既非车务(`VEHICLE_MANAGER`)也不持团期管理员权限码(`group-batch:demand:confirm`)(4/7/8/9 号端点) | +| 281019 | hl-user-service | `CHAT_GROUP_FLEET_NOT_CONTACTABLE` | 团期不存在、已结算或已取消,无法发起配车会话 | `groupBatchId` 查无活跃团期,或 `batch_status ∈ {SETTLED, CANCELLED}`;**只卡 open/send,messages/read 仍放行**(4 号端点) | + +### 场景对照 + | 场景 | 结果 | |------|------| -| 分页越界 | 返600013 | -| 日期倒置 | 返600013 | -| 关键词超长 | 返600013 | +| 分页越界 | 返600013(fleet 侧读口)/ 809401(order-v3 内部读口) | +| 日期倒置 | 返600013(fleet 侧读口)/ 809401(order-v3 内部读口) | +| 关键词超长 | 返600013(fleet 侧读口)/ 809401(order-v3 内部读口) | +| 车务侧/团期管理员侧均不满足准入 | 返281018(open-group-fleet / messages / send / read 四个端点统一) | +| 团期已结算/取消/不存在,尝试 open 或 send | 返281019;已建立的历史会话 messages/read 仍可用 | +| 车务侧红点误传 `unreadScope=TEAM`(或反之) | **不报错**,返回对方那一侧的未读数——业务边界,非契约错误 | --- @@ -398,7 +931,10 @@ GET /admin/fleet/group-dispatch/resource-schedule?resourceType=VEHICLE&resourceI ## 六、边界行为 - 未登录 → 401 -- 团期不存在 → 600012 +- 团期不存在 → 600012(fleet 侧只读口)/ 809400(order-v3 内部读口)/ 281019(user-service 会话 open/send) +- 接送机沟通不新开会话:对团期子订单调 `open-fleet` 仍返回 `FLEET:{orderId}`,不产生任何 `GROUP_FLEET` 记录 +- `GROUP_FLEET` 一侧标记已读只推进本侧团队水位,对侧未读不受影响(见 9 号端点业务边界) +- `unreadScope=TEAM_FLEET`(车务侧)与 `TEAM`(团期管理员侧)是两个不同的数,传错不报错、只是拿到对方的未读数(见 10 号端点业务边界) --- @@ -416,7 +952,16 @@ GET /admin/fleet/group-dispatch/resource-schedule?resourceType=VEHICLE&resourceI ## 六.6、修改前后对比 -N/A +> 本单 `change_type=新增接口`;下列 4 个端点是新增 `GROUP_FLEET` 会话能力附带触发的既有端点分支改造,路径/入参/响应结构均未变,故未单独出一版「修改接口」changelog,仍在此列出改前改后对比以免这条变化被埋没。 + +### 行为级对比 + +| 端点 | 改前 | 改后 | +|------|------|------| +| `GET .../{conversationKey}/messages` | 只支持 FLEET/GROUP/GROUP_HOUSE/DIRECT 键,按订单+定制师解析授权 | 新增 `GROUP_FLEET:` 键分流:按团期存在性+车务角色/团期管理员权限码判定,不满足返281018 | +| `POST .../{conversationKey}/messages` | 收件方按「订单定制师 vs 团队」解析 | 新增团级分支:收件方恒取**对侧**占位adminId(车务发→团期管理员侧水位,反之亦然) | +| `POST .../{conversationKey}/read` | 团队会话已读统一推进`admin_id=0`一条水位 | `GROUP_FLEET`双侧各自推进:本侧标已读只清本侧池行,对侧不受影响 | +| `POST /internal/.../biz-unread-batch` | `unreadScope`只支持`TEAM`/`PERSONAL` | 新增`TEAM_FLEET`(车务侧团队水位,`admin_id=-1`),`GROUP_FLEET`场景`TEAM`/`TEAM_FLEET`各取一侧 | ---