文件
hl-api-changelog/changelogs-v2/2026-09/11_7440_团期车务派单只读入口-新增接口-管理后台.md
T
2026-09-13 10:54:55 +08:00

40 KiB
原始文件 Blame 文件历史

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 7440 团期车务派单只读入口——看板三端点 + 内部读接口 + GROUP_FLEET 双团会话 admin wx(GIT) 新增接口 deployed verified verified mmg 5aea88d4 2026-09-13 2026-09-11 订正:六.8 节的角色授予范围已随 PR #7514(V20260911_007)收回 ADMIN,正文已更新为当前状态;接口契约、错误码与行为均未变。 前端已交付(2026-09-13):新建 fleet/group-dispatch 只读模块(待配车清单+单团总览+联系团期管理员 GROUP_FLEET 会话);resource-schedule 排班端点本轮缓建(本单无配车编辑入口);权限可见即可点+403 兜底,不接 fleet:group-dispatch:view 显隐。 2026-09-11 dev-v3

fleet/order-v3/user: 团期车务派单只读入口

服务: hl-fleet-service / hl-order-service-v3 / hl-user-service
PR: #7506
Issue: #7440


⚠️ 关键变化

本单仅含只读入口,不含配车写入、不含派单进度回写(配车写入与派单进度回写在后续工单)。

🔴 双侧未读必须分开取,传错不报错、只是拿到对方的数字:车务侧红点取 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 记录。


一、背景

本单新增3个admin只读端点供fleet读取团期需求主数据(order-v3)与服务日,以及2个/internal/端点供fleet内部调用。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 待配车团期清单 GET /admin/fleet/group-dispatch/pending-batches 新增 管理后台车务页首屏清单
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 新增 车务↔团期管理员团级会话
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 取值,请求体不加字段

三、接口详情

1. 待配车团期清单 GET /admin/fleet/group-dispatch/pending-batches

VO: GroupDispatchPendingBatchPageReqVO → Result<PageResult<GroupDispatchPendingBatchRespVO>>

使用场景

管理后台车务-团期配车页面的首屏清单。

入参字段表

字段 位置 类型 必填 约束 说明
departDateFrom Query LocalDate ❌ - 出发日下界
departDateTo Query LocalDate ❌ - 出发日上界
keyword Query String ❌ ≤50字符 团号/团名
dispatchProgress Query String ❌ NOT_STARTED/PARTIAL/FULL 进度过滤
page Query Integer ❌ ≥1 页码(默认1)
pageSize Query Integer ❌ 1-100 每页条数(默认20)

出参字段表

字段 类型 说明
groupBatchId Long 团期ID
batchNo String 团号
batchName String 团名
departDate LocalDate 出团日
dispatchProgress String 配车进度
unreadCount Integer 车务未读数

请求示例

{
  "departDateFrom": "2026-09-01",
  "departDateTo": "2026-09-30",
  "page": 1,
  "pageSize": 20
}

响应示例

{
  "code": 200,
  "data": {
    "total": 15,
    "records": [
      {
        "groupBatchId": 1934567890123456789,
        "batchNo": "GB-26-0912-01",
        "batchName": "额吉的故乡 9/12 团",
        "departDate": "2026-09-12",
        "dispatchProgress": "PARTIAL",
        "unreadCount": 3
      }
    ]
  },
  "success": true
}

空数据 / 降级响应

{
  "code": 200,
  "data": {"total": 0, "records": []},
  "success": true
}

错误响应

{
  "code": 600013,
  "message": "排班查询参数非法: departDateFrom 不能晚于 departDateTo",
  "success": false
}
{
  "code": 600012,
  "message": "团期配车基线不可达,请稍后重试",
  "success": false
}

降级链路:600013 由本端点自身入参校验抛出(page<1/pageSize不在1-100/出发日区间倒挂/keyword超50字符/dispatchProgress枚举非法,五种情形共用同一个码)。600012 是本端点对外唯一的「上游不可达」码——内部经 Feign 调用 order-v3 团期用车候选口(POST /v3/internal/group-batch/vehicle-dispatch-candidates),该内部接口对同一批入参约束会独立抛 809401 查询参数非法(GroupBatchVehicleReadErrorCode.java:38),但本端点已先行校验挡在前面(不会把非法参数带给 Feign),且 fetchCandidatesOrFail(GroupDispatchQueryService.java:314-336)对 Feign 返回的任何非成功 Result(含 809401)一律失败关闭转成 600012——调用方看不到 809401 原始码。

业务边界

  • 进度是fleet内存过滤,故先分页再过滤,单页可能少于pageSize
  • unreadCount软依赖,user-service不可达时退0

2. 团期配车总览 GET /admin/fleet/group-dispatch/batches/{groupBatchId}/overview

VO: Long → Result<GroupDispatchOverviewRespVO>

使用场景

单团详情页的派车总览。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ - 团期ID

出参字段表

字段 类型 说明
groupBatchId Long 团期ID
serviceDates List 权威服务日
days List 逐日派车状态
transferPendingTotal Integer 全团接送机未配计数
conversationKey String 会话键

请求示例

GET /admin/fleet/group-dispatch/batches/1934567890123456789/overview

响应示例

{
  "code": 200,
  "data": {
    "groupBatchId": 1934567890123456789,
    "serviceDates": ["2026-09-12", "2026-09-13", "2026-09-14"],
    "days": [
      {"serviceDate": "2026-09-12", "dispatched": true}
    ],
    "transferPendingTotal": 3,
    "conversationKey": "GROUP_FLEET:1934567890123456789"
  },
  "success": true
}

空数据 / 降级响应

无。

错误响应

{
  "code": 600012,
  "message": "团期配车基线不可达,请稍后重试",
  "success": false
}

降级链路(🔴 本端点对外只有 600012 这一个码):内部经 Feign 调用 order-v3 单团用车覆盖口(GET /v3/internal/group-batch/{groupBatchId}/vehicle-coverage),团期不存在或已软删时该内部接口会抛 809400 团期不存在(GroupBatchVehicleReadErrorCode.java:28);但 fetchCoverageOrFail(GroupDispatchQueryService.java:526-537)把 Feign 返回的任何非成功 Result(含 809400)一律失败关闭转成 600012——本端点不透出 809400,调用方只会看到 600012,不要按「overview 会返 809400」处理。

业务边界

  • serviceDates是权威口径,fleet不得自己铺行程日

3. 资源排班 GET /admin/fleet/group-dispatch/resource-schedule

VO: ResourceScheduleListReqVO → Result<List<ResourceScheduleRespVO>>

使用场景

派单编辑时查询车/司机的排班占用。

入参字段表

字段 位置 类型 必填 约束 说明
resourceType Query String ✅ VEHICLE/DRIVER 资源维度
resourceIds Query List ✅ 1-50个 资源ID集合
dateFrom Query LocalDate ✅ - 起始日
dateTo Query LocalDate ✅ ≤31天 结束日

出参字段表

字段 类型 说明
resourceType String 资源维度
resourceId Long 资源ID
resourceLabel String 车牌/司机名
days List 排班日历

请求示例

GET /admin/fleet/group-dispatch/resource-schedule?resourceType=VEHICLE&resourceIds=1&dateFrom=2026-09-12&dateTo=2026-09-30

响应示例

{
  "code": 200,
  "data": [
    {
      "resourceType": "VEHICLE",
      "resourceId": 1,
      "resourceLabel": "蒙A12345",
      "days": []
    }
  ],
  "success": true
}

空数据 / 降级响应

返回空days列表。

错误响应

{
  "code": 600014,
  "message": "不支持的资源类型: BOAT",
  "success": false
}
{
  "code": 600013,
  "message": "排班查询参数非法: 查询跨度不能超过 31 天",
  "success": false
}

触发条件:600014 仅在 resourceType 不是 VEHICLE/DRIVER 时抛出(示例中 BOAT 为非法值占位)。600013 覆盖:resourceIds 为空 / 超 50 个、dateFrom/dateTo 为空、日期倒置、跨度超 31 天,五种情形共用同一个码。本端点只读本域两张业务表,不经 Feign 调用 order-v3,故与 809400/809401 无关。

业务边界

  • 不分页,规模由参数上限封死
  • 返回顺序必须与入参顺序相同

4. 打开团期配车会话 POST /admin/message/chat/open-group-fleet

VO: ChatOpenGroupFleetReqVO → Result<ChatOpenFullRespVO>

使用场景

派单看板打开团期的车务↔团期管理员会话。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Body Long ✅ - 团期ID

出参字段表

字段 类型 说明
conversationKey String 会话键
conversationId Long 会话ID
currentSide String GROUP_ADMIN / VEHICLE_TEAM

请求示例

{
  "groupBatchId": 1934567890123456789
}

响应示例

{
  "code": 200,
  "data": {
    "conversationKey": "GROUP_FLEET:1934567890123456789",
    "conversationId": 1934567890123456789,
    "currentSide": "GROUP_ADMIN"
  },
  "success": true
}

空数据 / 降级响应

无。

错误响应

{
  "code": 281018,
  "message": "无权访问该团期配车会话",
  "success": false
}
{
  "code": 281019,
  "message": "团期不存在、已结算或已取消,无法发起配车会话",
  "success": false
}

分工:281018 = 有登录态但当前角色既非 VEHICLE_MANAGER(车务侧)也不持 group-batch:demand:confirm(团期管理员侧,超管天然通过)——准入完全按 token 当前角色判,与是否为会话成员无关(TeamChatAuthorizationService.java:158-169)。281019 = 团期本身不可联系(不存在/已软删/已结算/已取消),前置条件与 GROUP_HOUSE 用的 281017 同源(TeamChatModule.java:75)。281019 只卡 open(本端点)/ send,messages/read 仍放行(requirePrecondition 参数为 false,见 ChatMessageController.java 的 /{conversationKey}/messages)——终态团期的历史会话仍可审计读取,前端不要因为拿到过 281019 就连历史消息一起屏蔽。

281016「缺少团期上下文」(ChatErrorCode.java:76)存在但正常路径触发不到:groupBatchId 由请求体 VO 的 @NotNull + Controller 的 @Valid 先挡成 100001,281016 只在 internal / 单测直调 Service 时才会真抛(见 ChatManager.java:444-446 注释),前端按正常表单提交不会遇到它。

业务边界

  • 双团队会话
  • 前置:团期活跃

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 ❌ - 团期状态白名单,不传/空=提供方默认成团后阶段集
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 配车是否已就绪

请求示例

{
  "departDateFrom": "2026-09-01",
  "departDateTo": "2026-09-30",
  "requirementConfirmed": true,
  "vehicleReady": false,
  "page": 1,
  "pageSize": 20
}

响应示例

{
  "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
}

空数据 / 降级响应

{
  "code": 200,
  "data": {"total": 0, "list": []},
  "success": true
}

错误响应

{
  "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 权威服务日集合(与 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,原始声明值)。

请求示例

GET /v3/internal/group-batch/1934567890123456789/vehicle-coverage

响应示例

{
  "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,不返回空对象)。

错误响应

{
  "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 消息列表(按时间正序),逐项含messageId/senderAdminId/senderName/senderRole/msgType/priority/content/isMine/readByPeer/sentAt

请求示例

GET /admin/message/chat/GROUP_FLEET:1934567890123456789/messages?pageSize=20

响应示例

{
  "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
}

空数据 / 降级响应

{
  "code": 200,
  "data": {"conversationKey": "GROUP_FLEET:1934567890123456789", "hasMore": false, "nextCursor": null, "list": []},
  "success": true
}

错误响应

{
  "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)

请求示例

{
  "content": "9/12 那天大巴上午先送机场",
  "msgType": "TEXT",
  "priority": "NORMAL"
}

响应示例

{
  "code": 200,
  "data": {
    "messageId": 900001,
    "conversationKey": "GROUP_FLEET:1934567890123456789",
    "sentAt": "2026-09-11 09:02:31"
  },
  "success": true
}

空数据 / 降级响应

无。

错误响应

{
  "code": 281011,
  "message": "消息内容不合法",
  "success": false
}
{
  "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),无消息为空

请求示例

POST /admin/message/chat/GROUP_FLEET:1934567890123456789/read

响应示例

{
  "code": 200,
  "data": {
    "conversationKey": "GROUP_FLEET:1934567890123456789",
    "unreadCount": 0,
    "lastReadMessageId": 900001
  },
  "success": true
}

空数据 / 降级响应

无(无消息时 lastReadMessageId 为null,unreadCount 仍为0)。

错误响应

{
  "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 ✅ @NotEmpty,最多200个 一批业务对象id;GROUP_FLEET场景传groupBatchId集合

出参字段表

字段 类型 说明
unreads List 逐项bizId(String)+unreadCount(Integer);无会话的对象统一返0

请求示例

{
  "adminId": 205,
  "bizModule": "GROUP_FLEET",
  "unreadScope": "TEAM_FLEET",
  "bizIds": [1934567890123456789, 1934567890123456790]
}

响应示例

{
  "code": 200,
  "data": {
    "unreads": [
      {"bizId": "1934567890123456789", "unreadCount": 2},
      {"bizId": "1934567890123456790", "unreadCount": 0}
    ]
  },
  "success": true
}

空数据 / 降级响应

{
  "code": 200,
  "data": {"unreads": []},
  "success": true
}

错误响应

本端点无专属业务错误码,走 @Valid 统一 400(GlobalExceptionHandler.handleValidation,HTTP 状态码仍是 200):

{
  "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(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(或反之) 不报错,返回对方那一侧的未读数——业务边界,非契约错误

五、数据库行为

本单仅含查询接口,无写操作。


六、边界行为

  • 未登录 → 401
  • 团期不存在 → 600012(fleet 侧只读口)/ 809400(order-v3 内部读口)/ 281019(user-service 会话 open/send)
  • 接送机沟通不新开会话:对团期子订单调 open-fleet 仍返回 FLEET:{orderId},不产生任何 GROUP_FLEET 记录
  • GROUP_FLEET 一侧标记已读只推进本侧团队水位,对侧未读不受影响(见 9 号端点业务边界)
  • unreadScope=TEAM_FLEET(车务侧)与 TEAM(团期管理员侧)是两个不同的数,传错不报错、只是拿到对方的未读数(见 10 号端点业务边界)

六.5、枚举

dispatchProgress

值 中文
NOT_STARTED 未启动
PARTIAL 部分完成
FULL 已完成

六.6、修改前后对比

本单 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各取一侧

六.7、影响评估

  • 是否破坏向后兼容: 否
  • 前端是否必须同步上线: 否

六.8、菜单与权限

  • 菜单挂载(V20260910_006__add_group_batch_fleet_dispatch_menu.sql):在既有「车务管理」(path=/fleet)节点下新增二级目录「团期配车」(menu_type=D,path=/fleet/group-dispatch,icon=Van,sort_order=46),其下唯一三级菜单同名「团期配车」(menu_type=M,path=component=/fleet/group-dispatch);菜单可见性(sys_role_menu)当前授予 SUPER_ADMIN / VEHICLE_MANAGER 两个角色。⚠️ 2026-09-11 订正:本脚本原本还授予了 ADMIN,已由 V20260911_007__fix_group_dispatch_admin_grants.sql(PR #7514,14:04:26 部署)收回。原因见八节末尾那条红字——/admin/fleet/** 的门禁只放行 VEHICLE_MANAGER/SUPER_ADMIN,给 ADMIN 菜单等于造一个看得见、点进去必 403 的死入口。前端不要按「ADMIN 能看到团期配车」设计任何逻辑。
  • 权限标识(V20260910_007__add_group_batch_fleet_permission.sql):新增权限码 fleet:group-dispatch:view(resource_type=FLEET_GROUP_DISPATCH,action=VIEW),覆盖本单三个只读端点(待配车团期清单/团期配车总览/资源排班);三级菜单的 permission_code 字段(V20260910_006 第66行)写的也是这同一个码。admin_role_permission 当前授权 VEHICLE_MANAGER / SUPER_ADMIN(原本含 ADMIN,同由 V20260911_007 收回,理由同上);团期管理员侧不在此授权范围内(他们看配车进度走团期管理页,不进车务页面)。

七、不影响范围

  • 房务、C端、既有会话

八、测试环境已验证

环境与提交:测试服部署 hl-fleet-service / hl-order-service-v3 @ 861ebff0c,hl-user-service @ 30761683e(与前两者不同提交号是部署窗口内 dev-v3 尖端继续前移所致;已核实中间那 5 个提交对 hl-user-service/hl-fleet-service 零文件改动,且改动集中在 hl-common-feign,与本单改的 hl-common-core/dto/fleet 零重叠,契约不受影响)。

网关实测(6 个端点,每条两轮不同角色):

端点 角色 结果
GET /admin/fleet/group-dispatch/pending-batches
GET /admin/fleet/group-dispatch/batches/{id}/overview
GET /admin/fleet/group-dispatch/resource-schedule
VEHICLE_MANAGER 200,返回真实数据
同上三个 ROOM_MANAGER / ADMIN 403「无权限访问车务管理,请切换到车务角色」
POST /v3/internal/group-batch/vehicle-dispatch-candidates
GET /v3/internal/group-batch/{id}/vehicle-coverage
任意(含带 token) 403「接口不可访问」——网关 JwtAuthFilter 按 /v3/internal/** 路径前缀在鉴权之前就拦掉,与 token/角色无关,外部不可达(符合预期,这两个是服务间接口)
POST /admin/message/chat/open-group-fleet VEHICLE_MANAGER 200,两次调用 isNew 分别为 true/false(首开建会话、二次找回同一条,幂等生效)

🔴 ADMIN 角色拿到的是 403,不是 200——fleet 三个只读端点的访问控制是 #5626 的粗粒度角色门禁(仅放行 VEHICLE_MANAGER / SUPER_ADMIN),不看权限码 fleet:group-dispatch:view(六.8 节新增的那个)。前端如果按「持有该权限码就能调」设计菜单可见性之外的接口调用逻辑,ADMIN 角色会在实调时翻车——必须同时满足角色门禁。

GET /admin/fleet/group-dispatch/pending-batches → 200 ✓(VEHICLE_MANAGER)

九、相关历史 PR

PR Issue 说明
#7506 #7440 本单

十、相关文档

关联 / 联系人

链接

联系人

  • 后端负责人: @wx