docs(7440): 交接件补 4 个改造接口 + 双侧未读取法 + 会话键不混同 Refs #7440
changelog-filename-gate / validate (push) Successful in 3s

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 无关)。
交接件按源码写,并在字段说明里标注与旧文档描述不一致、以源码为准。
这个提交包含在:
API Changelog Bot
2026-09-11 17:31:04 +08:00
父节点 274f0b16c0
当前提交 5a5ca03e0f
@@ -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` | 新增 | 单团详情页派车总览 | | 2 | 团期配车总览 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/overview` | 新增 | 单团详情页派车总览 |
| 3 | 资源排班 | GET | `/admin/fleet/group-dispatch/resource-schedule` | 新增 | 派单编辑时查资源占用 | | 3 | 资源排班 | GET | `/admin/fleet/group-dispatch/resource-schedule` | 新增 | 派单编辑时查资源占用 |
| 4 | 打开团期配车会话 | POST | `/admin/message/chat/open-group-fleet` | 新增 | 车务↔团期管理员团级会话 | | 4 | 打开团期配车会话 | POST | `/admin/message/chat/open-group-fleet` | 新增 | 车务↔团期管理员团级会话 |
| *内部* | 待配车候选分页 | POST | `/v3/internal/group-batch/vehicle-dispatch-candidates` | 新增 | 服务间接口,前端无需对接 | | 5 | 待配车候选分页 | POST | `/v3/internal/group-batch/vehicle-dispatch-candidates` | 新增 | 服务间接口(order-v3),前端无需对接,随附供核对契约边界 |
| *内部* | 单团用车覆盖 | GET | `/v3/internal/group-batch/{groupBatchId}/vehicle-coverage` | 新增 | 服务间接口,前端无需对接 | | 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<PageResult<GroupBatchVehicleDispatchCandidateDTO>>`
#### 使用场景
供 `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<String> | ❌ | - | 团期状态白名单,不传/空=提供方默认成团后阶段集 |
| 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<LocalDate> | 权威服务日集合(与 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<GroupBatchVehicleCoverageDTO>`
#### 使用场景
供 `hl-fleet-service` 的「团期配车总览」(本文档 2 号端点)内部经 Feign 调用,拉逐户用车覆盖与接送机声明。**仅限服务间调用,前端不直接对接**。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | Long(转String) | 团期主订单 ID |
| batchNo / batchName | String | 团号 / 团名 |
| departDate / endDate | LocalDate | 出团日 / 返团日 |
| serviceDates | List<LocalDate> | 权威服务日集合(与 dispatch-baseline 同源,消费方不另算) |
| requirementConfirmed / vehicleReady | Boolean | 团期两个标志位 |
| orders | List<OrderVehicleCoverageItemDTO> | 逐户覆盖项,见下 |
`orders[]` 逐项字段:`orderId`(Long转String)/ `orderNo` / `customerName` / `headcount`(Integer)/ `vehicleControlStatus` / `travelRequirementId`(Long转String,无有效需求为null)/ `travelRequirementStatus`(无有效需求为null)/ `transferDeclared`(Boolean)/ `transferArrivalDates`(List<LocalDate>,原始声明值)/ `transferDepartureDates`(List<LocalDate>,原始声明值)。
#### 请求示例
```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<ChatThreadRespVO>`
#### 使用场景
会话内上滑加载更早历史消息(首屏消息由 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<ChatMessageRespVO> | 消息列表(按时间正序),逐项含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<ChatMessageSendRespVO>`
#### 使用场景
在 `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<ChatReadRespVO>`
#### 使用场景
把某会话标记为已读到最新,成员水位推进 + `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<ChatBizUnreadBatchRespVO>`
#### 使用场景
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<Long> | ✅ | `@NotEmpty`,最多200个 | 一批业务对象id;`GROUP_FLEET`场景传`groupBatchId`集合 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| unreads | List<ChatBizUnreadItemVO> | 逐项`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(fleet 侧读口)/ 809401(order-v3 内部读口) |
| 日期倒置 | 返600013 | | 日期倒置 | 返600013(fleet 侧读口)/ 809401(order-v3 内部读口) |
| 关键词超长 | 返600013 | | 关键词超长 | 返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 - 未登录 → 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、修改前后对比 ## 六.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`各取一侧 |
--- ---