文件
hl-api-changelog/changelogs-v2/2026-08/31_6843_供应商暂停合作联动停用车队与派单拦截-修改接口-管理后台.md
T
wx 5ce53bcd47
changelog-filename-gate / validate (push) Successful in 1s
docs: 供应商暂停合作联动停用车队与派单拦截(#6843) (#84)
docs: 供应商暂停合作联动停用车队与派单拦截(#6843) (#84)
2026-08-31 14:36:14 +08:00

47 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 6843 供应商暂停合作联动停用车队与派单拦截 admin wx(GIT) 修改接口 deployed verified pending 2026-08-31 PR #6868 已合并 dev-v3(merge commit df78cce5);Deploy Panel 任务 a7300401(hl-fleet-service,2026-08-31 14:02)部署测试服成功,部署 dev-v3 HEAD 2543febee(含本 PR)。TEST 实测 13 条链路全绿:internal 首投 SUSPENDED 停用 1 车队(disabledCount=1)/同 eventId 重投 duplicate=true/非停合作态 ignored=true/缺 eventId 400/错 X-Internal-Token 403/网关不路由 /internal 404;司机候选排除(斯琴 1→0)/车辆候选排除/precheck 新增 warning fleet_team_disabled/改派 605074 实拦且零副作用/重新启用后司机回候选。batch 创建 605074 正向链测试服无可构造数据(全部待派占位行程已结束,先撞 605047),由单测 create_fleetTeamDisabledVehicle_throws605074 / create_driverOnDisabledTeam_throws605074 覆盖,改派路径已实测同源拦截。测试车队/车辆已删除,admin token 已登出。 2026-08-31 dev-v3

车务派单:供应商暂停合作联动停用车队,派单全链路拦截停用车队资源

存放目录:

  • 二期(v3,order-v3 标签的工单)→ changelogs-v2/2026-08/

服务: hl-fleet-service(端口 8087/8187,双实例滚动) PR: #6868 Issue: #6843 作者: wx 更新时间: 2026-08-31 影响范围: 管理后台「车务 - 派单」候选查询 / 预校验 / 创建(批量)/ 改派;新增服务间 internal 联动端点(前端不直接调用)

供应商暂停合作(SUSPENDED)/拉黑(BLACKLIST)后,其名下在启车队被批量联动停用;停用车队的车辆与常驻司机不再可派新单。已派订单不受影响(存量派单的最终确认、撤销取消不挂新校验)。


⚠️ 关键变化

  • precheck 新增 warning type fleet_team_disabled:POST /admin/fleet/assignments/precheck 的 warnings[] 新增该类型(文案「车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车」)。前端若按 type 白名单渲染提示,需把新 type 加入渲染映射,否则该提示会被静默丢弃。
  • 新错误码 605074:「车队已停用不可派新单(请先启用车队或换车)」。派单创建(批量,单派同路径)与改派在写库前硬拦截;前端错误码字典需补 605074 文案映射,建议引导动作 = 启用车队或更换车辆/司机。
  • 司机候选静默排除:POST /admin/fleet/assignments/candidates 的司机候选不再返回「常驻车所属车队已停用」的司机(无新增/删除字段,结果集合缩小;车辆侧自 #6717 起已排除)。
  • 新增 internal 端点 POST /internal/fleet-teams/supplier-status-sync:仅供供应商域(resource 侧,工单 #6844)Feign 调用,前端不直接调用、网关不路由;本文按模板写全契约供服务间对齐。

一、背景

工单 #6843:供应商生命周期进入「停止合作」态(SUSPENDED=暂停合作 / BLACKLIST=拉黑)时,其名下关联车队应联动停用,且停用车队的司机/车辆不得再派新订单。

前置条件已由 #6717 建好:车队主数据持有 supplierId/supplierName 快照,车队停用会合并车辆可派状态(车辆侧候选过滤与 605037 拦截已存在)。本单补齐两环:

  1. 联动入口(fleet 侧接收端):新增 internal 端点接收供应商域的状态同步事件,CAS 批量停用该供应商名下 status=ACTIVE 的车队。与人工停用的差异:供应商侧停用是强制联动,不做「在役车辆」守卫(车队不能再接新单与名下车辆是否仍在役无关)。
  2. 派单链路拦截:候选查询排除(司机侧新增)+ precheck 预警(新增 type)+ 创建/改派锁内硬校验(新错误码 605074,先于车辆/司机自身状态校验 605037/605038 执行)。

供应商侧通知发出端由工单 #6844 实现(本 PR 合并后指派),届时按本文 §三.1 契约 Feign 投递。车队域暂无独立操作日志表,停用结果经响应回执 + 服务端日志留痕。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 供应商状态同步(内部) POST /internal/fleet-teams/supplier-status-sync 新增接口 SUSPENDED/BLACKLIST 批量停用名下在启车队;同 eventId 重投返 duplicate=true 成功回执
2 查询派单候选资源 POST /admin/fleet/assignments/candidates 行为变化 司机候选排除常驻车所属车队已停用的司机(无字段变化,集合缩小)
3 派单预校验冲突 POST /admin/fleet/assignments/precheck 响应新增枚举值 warnings[] 新增 type fleet_team_disabled(只提示不阻断)
4 批量创建派单 POST /admin/fleet/assignments/batch 新增错误码 车队停用硬校验 605074,先于 605037/605038;单派/批量同路径
5 修改派单(改派) POST /admin/fleet/assignments/{assignmentId}/change 新增错误码 换车/换司机到停用车队资源被 605074 拦截;纯费用调整司机侧不查

三、接口详情

1. 供应商状态同步(内部) POST /internal/fleet-teams/supplier-status-sync

VO: SupplierStatusSyncReqVO / Result<SupplierStatusSyncRespVO>

使用场景

供应商域(resource 侧,工单 #6844)在供应商生命周期变更为 SUSPENDED/BLACKLIST 后,经 Feign LB 直连投递本端点;fleet 侧批量停用该供应商名下 status=ACTIVE 的车队,让派单链路(候选排除 + 创建/改派硬校验 605074)不再放行其资源。

/internal 前缀不经网关路由(网关实测返回业务码 404「接口不存在」),X-Internal-Token 由 InternalAuthFilter 统一校验。前端不直接调用本端点。

入参

字段 位置 类型 必填 约束 说明
supplierId Body String(Long) 是 @Positive 供应商 ID(雪花,建议按字符串传输,禁止转 JavaScript Number)
targetStatus Body String 是 ≤32,大小写不敏感 供应商目标状态;仅 SUSPENDED/BLACKLIST 触发联动停用,其余状态忽略
eventId Body String 是 ≤64 事件 ID(幂等键,同一事件重复投递不产生二次变更)
occurredAt Body String/null 否 ISO 日期时间 事件发生时间(观测/日志用;本端不落供应商状态镜像,不做乱序围栏)

出参 Result<SupplierStatusSyncRespVO>

字段 类型 说明
supplierId String 供应商 ID(回显,雪花序列化为字符串)
targetStatus String 供应商目标状态(回显调用方传值)
ignored Boolean 是否忽略(目标状态非 SUSPENDED/BLACKLIST,未做联动)
duplicate Boolean 是否重复投递(true=同 eventId 幂等窗口内已消费,不产生二次变更;重复回执 disabledCount 恒 0、清单恒空)
disabledCount Integer 本次实际停用的车队数(CAS 实际影响行数;并发人工停用时可能小于快照大小;重复投递为 0)
disabledTeamIds String[] 本次停用的车队 ID 列表(停用前快照,雪花字符串,供调用方核对)

请求示例

{
  "supplierId": "8996843000000000001",
  "targetStatus": "SUSPENDED",
  "eventId": "supplier-status-8996843000000000001-SUSPENDED-20260831142000",
  "occurredAt": "2026-08-31T14:20:00"
}

(传输头:POST /internal/fleet-teams/supplier-status-sync,Content-Type: application/json,X-Internal-Token: <服务间令牌>,Feign LB 直连不经网关。)

响应示例

首次投递(TEST 实测:该供应商名下 1 个 ACTIVE 车队被停用,名下有在役车辆仍强制停用——供应商联动不做在役车辆守卫):

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "supplierId": "8996843000000000001",
    "targetStatus": "SUSPENDED",
    "ignored": false,
    "duplicate": false,
    "disabledCount": 1,
    "disabledTeamIds": ["352696323818000384"]
  }
}

空数据 / 降级响应

  • 目标状态非停止合作类(如恢复 ACTIVE):不联动、零写入,正常返回 ignored=true、disabledCount=0、disabledTeamIds=[](恢复合作不自动启用是本单边界,车队需人工启用,安全侧)。
  • 供应商名下无 ACTIVE 车队(含窗口外重投、车队已被人工停用):CAS 守卫自然命中 0 行,返回 ignored=false、disabledCount=0、disabledTeamIds=[],仍属成功回执,调用方不应告警。
  • 同 eventId 幂等窗口(24h)内重复投递:返回 duplicate=true 的成功回执(「已消费」语义),disabledCount=0、disabledTeamIds=[](首次投递的实际停用明细不随幂等键留存,重复回执只承诺「已处理」);供应商侧「失败即重试告警」不应被触发。
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "supplierId": "8996843000000000001",
    "targetStatus": "SUSPENDED",
    "ignored": false,
    "duplicate": true,
    "disabledCount": 0,
    "disabledTeamIds": []
  }
}

错误响应

缺少必填字段(Bean Validation,TEST 实测缺 eventId):

{ "code": 400, "message": "事件ID不能为空", "success": false, "data": null }

X-Internal-Token 缺失或错误(InternalAuthFilter 拦截,原始响应为过滤器直出、字段名为 msg,TEST 实测):

{ "code": 403, "msg": "内部接口禁止外部访问" }

经网关误调(/internal/** 无网关路由,TEST 实测 HTTP 200 传输层 + 业务码 404):

{ "code": 404, "message": "接口不存在: /internal/fleet-teams/supplier-status-sync", "success": false, "data": null }

业务边界

  • 幂等双保险:① @Idempotent 按 eventId 开窗 24h(覆盖 MQ/Feign 延迟重投),窗口内重投由 Controller 转 duplicate=true 成功回执;② 窗口外重投由 Mapper CAS 守卫(仅停用 status=ACTIVE 行)兜底,自然命中 0 行。
  • 状态匹配大小写不敏感(防调用方传小写被静默忽略);回执原样回显调用方传值。
  • 与人工停用的差异:供应商联动是强制停用,不做在役车辆守卫;存量派单/槽位/占用不在联动范围。
  • 本端不落供应商状态镜像、不做乱序围栏——迟到旧事件依赖调用方按事件流顺序投递。
  • supplierId/disabledTeamIds[] 均按字符串序列化(@JsonSerialize(ToStringSerializer)),禁止转 JavaScript Number。

2. 查询派单候选资源 POST /admin/fleet/assignments/candidates

VO: AssignmentCandidateReqVO / Result<AssignmentCandidateRespVO>

使用场景

派单 Step2 选车/选司机时查询候选资源。本单行为变化:司机候选排除「常驻车所属车队已停用」的司机(TEST 实测:司机斯琴常驻车挂到停用车队后,候选从 1 条变为 0 条;重新启用车队后恢复 1 条)。车辆侧排除自 #6717 起已存在(车队停用合并车辆可派状态),本单未改。

请求/响应结构零字段变化——排除是静默的,不返回被排除司机及其原因。

入参

字段 位置 类型 必填 约束 说明
startDate Body String(日期) 是 yyyy-MM-dd 用车开始日期
endDate Body String(日期) 是 yyyy-MM-dd,≥startDate 用车结束日期
orderId Body String(Long)/null 否 - 当前订单 ID;改派排除自身时必填
requirementId Body String(Long)/null 否 - 当前用车需求 ID;改派排除自身时必填
fleetItemIndex Body Integer/null 否 ≥0 需求车型项序号
vehicleKeyword Body String/null 否 - 车辆关键词(车牌/车型/常驻司机姓名或完整手机号)
driverKeyword Body String/null 否 - 司机关键词(姓名/完整手机号/常驻车牌)
fleetTeamId Body String(Long)/null 否 - 按车队过滤车辆候选(雪花字符串)
vehicleTypeId Body String(Long)/null 否 - 按车型大类过滤
driverAvailability Body String 否 AVAILABLE/ALL,默认 ALL 司机可用性筛选
selectedVehicleId Body String(Long)/null 否 - 已选车辆 ID(先选车后选司机场景)
selectedDriverId Body String(Long)/null 否 - 已选司机 ID(先选司机后选车场景)
excludeAssignmentId Body String(Long)/null 否 须属当前订单与需求 改派时排除的当前派单 ID

出参 Result<AssignmentCandidateRespVO>

顶层字段(与改前完全一致,无新增/删除):

字段 类型 说明
vehicles Object 车辆候选分页(records[] + total);停用车队名下车辆继续被排除(#6717 既有口径)
drivers Object 司机候选分页(records[] + total);本单起追加排除常驻车所属车队已停用的司机
fleetTeamFacets Array 车队聚合筛选项
vehicleTypeFacets Array 车型大类聚合筛选项
selectedDriverResidentVehicle Object/null 已选司机的常驻车信息
selectedDriverResidentVehicles Array 已选司机的全部常驻车
selectedVehicleResidentDriver Object/null 已选车辆的常驻司机
selectedRelation Object/null 已选车+司机的常驻关系
suggestedDriverId String(Long)/null 自动代入司机 ID(雪花字符串)
suggestedDriverReason String 自动代入原因码
suggestedDriverMessage String 自动代入原因展示文案
canonicalSnapshot Object Step2 canonical 快照(代际/版本/槽位集合)

drivers.records[] 项关键字段(结构未变,列与联调相关者):

字段 类型 说明
driverId String 司机 ID(雪花字符串,禁止转 Number)
name String 司机姓名
maskedPhone String 司机手机(脱敏,如 135****5001)
driverStatus String 占用态(idle/busy/rest/pending)
season String 赛季态(候选恒 active,其余赛季已过滤)
residentVehicleId String/null 常驻车辆 ID(常驻车所属车队停用时,该司机整体不出现在候选)
residentVehiclePlate String/null 常驻车辆车牌
available Boolean 是否覆盖整个请求日期范围可用
selectable Boolean 本次选车场景下是否可被选中

请求示例

POST /admin/fleet/assignments/candidates
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "startDate": "2026-09-10",
  "endDate": "2026-09-12",
  "driverKeyword": "斯琴"
}

响应示例

司机常驻车所属车队启用时(TEST 实测基线,斯琴在候选内):

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "drivers": {
      "total": 1,
      "records": [
        {
          "driverId": "2065272145289658370",
          "name": "斯琴",
          "maskedPhone": "135****5001",
          "years": 17,
          "driverStatus": "idle",
          "season": "active",
          "licenseType": "A3",
          "licenseExpired": false,
          "available": true,
          "selectable": true
        }
      ]
    }
  }
}

空数据 / 降级响应

车队停用后同一查询(TEST 实测:records=[]、total=0、code=200)——排除是静默的,响应结构不变、不附带被排除原因;前端按「无匹配司机」渲染即可,无需特殊处理。车辆候选同理(车队停用后名下车辆不再返回)。

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "drivers": { "total": 0, "records": [] }
  }
}

错误响应

缺少必填日期(Bean Validation):

{ "code": 400, "message": "用车开始日期不能为空", "success": false, "data": null }

未登录(网关拦截):

{ "code": 401, "message": "未登录", "success": false, "data": null }

业务边界

  • 司机侧排除口径 = 常驻关系(fleet_vehicle.primary_driver_id)反查:司机任一常驻车所属车队为 DISABLED 即被排除,与司机列表「所属车队」筛选同源(同一反查端口,防口径漂移)。
  • 无常驻车的司机不受车队联动约束(司机档案无车队列,本期不加列)。
  • 排除只读过滤,不写任何表;被排除司机在司机档案/列表页不受影响,仅派单候选场景不可见。
  • 车队长停用后重新启用,候选即时恢复(无缓存延迟,TEST 实测启用后同查询恢复 1 条)。

3. 派单预校验冲突 POST /admin/fleet/assignments/precheck

VO: PrecheckReqVO / Result<PrecheckRespVO>

使用场景

Step2 选定车辆 + 司机后、保存前预探冲突。本单变化:warnings[] 新增 type fleet_team_disabled——车辆所属车队停用、或司机常驻车所属车队停用时给出预警(对齐创建/改派锁内 605074 硬校验,precheck 只提示不抛)。

入参

字段 位置 类型 必填 约束 说明
vehicleId Body String(Long) 是 雪花字符串 车辆 ID
driverId Body String(Long) 是 雪花字符串 司机 ID
startDate Body String(日期) 是 yyyy-MM-dd 用车开始日期(闭区间起点)
endDate Body String(日期) 是 yyyy-MM-dd 用车结束日期(闭区间终点)
orderId Body String(Long)/null 否 - 订单 ID(仅日志/上下文)
pickupAt Body String/null 否 - 接客地(城市衔接判定用)
dropoffAt Body String/null 否 - 送客地(城市衔接判定用)
headcount Body Integer/null 否 ≥0 人数(座位不足 warning 判定用)
excludeAssignmentId Body String(Long)/null 否 - 改派预校验排除自身

出参 Result<PrecheckRespVO>

字段 类型 说明
conflict Boolean 是否存在阻断性冲突;资源不可用(含车队停用)时为 true,此时阻断原因在 warnings[] 体现、conflicts[] 可为空
conflicts Array 冲突明细(type/conflictAssignmentId/conflictOrderNo/conflictDateRange/cityJunctionShareCandidate/msg),无冲突为空数组
warnings Array 非阻断提示(type + msg);本单新增 type 值 fleet_team_disabled

warnings[] 项字段:

字段 类型 说明
type String 提示类型;新增 fleet_team_disabled=车队已停用(全集见 §六.5)
msg String 提示文案,新 type 恒为「车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车」

请求示例

POST /admin/fleet/assignments/precheck
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "vehicleId": "2094307514693648385",
  "driverId": "2065272145289658370",
  "startDate": "2026-09-10",
  "endDate": "2026-09-12"
}

响应示例

车队停用后(TEST 实测:车辆侧 vehicle_unavailable 与新增 fleet_team_disabled 同时出现,资源不可用使 conflict=true 但 conflicts=[]):

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "conflict": true,
    "conflicts": [],
    "warnings": [
      { "type": "vehicle_unavailable", "msg": "车辆处于维保或停用状态" },
      { "type": "fleet_team_disabled", "msg": "车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车" }
    ]
  }
}

空数据 / 降级响应

无冲突无提示(车队启用基线,TEST 实测):conflict=false、conflicts=[]、warnings=[]、code=200。precheck 为只读咨询,恒 code=200 不抛业务异常;车辆/司机不存在时也不抛错,而是落 vehicle_unavailable/driver_unavailable warning + vehicle_not_found/driver_not_found conflict 明细(既有 #5629 口径)。

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": { "conflict": false, "conflicts": [], "warnings": [] }
}

错误响应

缺少必填车辆 ID(Bean Validation):

{ "code": 400, "message": "车辆 ID 不能为空", "success": false, "data": null }

未登录(网关拦截):

{ "code": 401, "message": "未登录", "success": false, "data": null }

业务边界

  • precheck 只提示不阻断提交动作本身;最终一致以 create/change 锁内重校验(605074)为准——车务忽略预警强行提交会被 605074 拦截。
  • 车辆所属车队行缺失(历史数据无车队)时 fleet_team_disabled 不触发,仍走 vehicle_unavailable 既有口径。
  • 司机侧判定 = 常驻车所属车队停用;无常驻车司机不产生本 warning。
  • 前端若按 type 白名单渲染 warning,需把 fleet_team_disabled 加入映射(建议样式与 vehicle_unavailable 同级:醒目提示 + 引导换车/启用车队)。Swagger 注解中的 type 枚举列举未同步新值,以本文 §六.5 全集为准。

4. 批量创建派单 POST /admin/fleet/assignments/batch

VO: BatchCreateAssignmentReqVO / Result<BatchAssignmentWriteRespVO>

使用场景

派单 Step2 提交(单派/批量同路径,经同一锁内创建逻辑)。本单变化:新增车队状态硬校验——所选车辆所属车队、或所选司机常驻车所属车队非 ACTIVE 时,写库前抛 605074,先于车辆/司机自身状态校验(605037/605038)执行,避免车队停用被误报成「车辆维保/停用」。

入参

字段 位置 类型 必填 约束 说明
orderId Body String(Long) 是 雪花字符串 订单 ID
requirementId Body String(Long) 是 雪花字符串 用车需求 ID
startDate Body String(日期) 是 yyyy-MM-dd 用车开始日期
endDate Body String(日期) 是 yyyy-MM-dd 用车结束日期
requestId Body String 是 非空,幂等键 批次幂等请求标识(重复提交同 requestId + 同载荷幂等放行)
items Body Array 是 每项含 fleetItemIndex/vehicleId/driverId(均必填) 派车明细;vehicleId/driverId 为雪花字符串
pickupAt Body String/null 否 - 接客地
dropoffAt Body String/null 否 - 送客地
headcount Body Integer/null 否 ≥0 乘客人数(不含司机)
chargeableServiceDates Body Array/null 否 - 收取车费的服务日期
confirmCrossResident Body Boolean/null 否 - 跨常驻车派单显式确认

出参 Result<BatchAssignmentWriteRespVO>

字段 类型 说明
assignments Array 创建结果列表(fleetItemIndex + assignment 写入结果,含 assignmentId 雪花字符串)
failedFleetItemIndex Integer/null 失败的需求项序号(整批事务回滚,任一失败全部不生效)
dailyDifferences Array/null 逐日差异提示

请求示例

POST /admin/fleet/assignments/batch
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "orderId": "2087157006417657857",
  "requirementId": "2087157006845476865",
  "startDate": "2026-09-10",
  "endDate": "2026-09-12",
  "headcount": 4,
  "requestId": "web-step2-<uuid>",
  "items": [
    { "fleetItemIndex": 0, "vehicleId": "2094307514693648385", "driverId": "2065272145289658370" }
  ]
}

响应示例

成功(结构示例;本次 TEST 未跑通正向创建,原因见 §八):

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "assignments": [
      {
        "fleetItemIndex": 0,
        "assignment": { "assignmentId": "2094310000000000001", "assignmentStatus": "assigned" }
      }
    ],
    "failedFleetItemIndex": null,
    "dailyDifferences": null
  }
}

空数据 / 降级响应

本接口为写接口,无空数据分支;整批任一 item 失败即同事务回滚,不产生半批状态。幂等重试(同 requestId + 同载荷)返回首个成功结果,不重复建单。

错误响应

车队已停用(新增;车辆所属车队或司机常驻车所属车队非 ACTIVE,写库前拦截、零写入):

{ "code": 605074, "message": "车队已停用不可派新单(请先启用车队或换车)", "success": false, "data": null }

行程已结束的需求不可再派(既有口径,TEST 实测优先级高于 605074——过期需求先撞本码):

{ "code": 605047, "message": "行程已结束,派车信息只读,不能修改或改派", "success": false, "data": null }

车辆自身维保/停用(既有;车队校验通过后才轮到本码):

{ "code": 605037, "message": "车辆处于维保或停用状态,不能派车", "success": false, "data": null }

业务边界

  • 校验顺序(锁内):需求项占用守卫 → 车队状态(605074) → 车辆状态(605037)→ 司机状态(605038)→ 司机赛季(605006/605013)→ 跨常驻确认 → 档期重叠。
  • 只拦「产生新占用」的创建入口;存量派单的最终确认/撤销取消不挂本校验——供应商事后停用不卡死在途单。
  • 车队行缺失(历史车辆无车队)不归 605074,仍由合并后车辆状态走 605037 老口径。
  • 幂等:同 requestId 重放返回首个结果;分布式锁串行化同需求写入。
  • 行程已结束(605047)、需求版本过期(605905)等前置守卫先于车队校验命中。

5. 修改派单(改派) POST /admin/fleet/assignments/{assignmentId}/change

VO: ChangeAssignmentReqVO / Result<ChangeAssignmentRespVO>

使用场景

车务对在途派单换车/换司机/调整费用。本单变化:改派目标为停用车队资源时写库前抛 605074(TEST 实测:把在途派单改到停用车队的车辆+司机,返回 605074 且原派单零副作用)。纯费用调整(车人未换)不查司机常驻车队——在途单不因司机另一辆常驻车挂的车队被供应商事后停用而卡死费用修订。

入参

字段 位置 类型 必填 约束 说明
assignmentId Path String(Long) 是 雪花字符串 目标派单 ID
effectiveDate Body String(日期) 是 须在派单服务日期范围内 改派生效日期
newVehicleId Body String(Long)/null 否 与 newDriverId 至少其一 新车辆 ID(雪花字符串)
newDriverId Body String(Long)/null 否 与 newVehicleId 至少其一 新司机 ID(雪花字符串)
reason Body String 是 非空 修改原因
requestId Body String 是 非空,幂等键 请求幂等 ID
serviceDates Body Array/null 否 - 指定改派的服务日期子集
vehicleFeeTotal Body Number/null 否 ≥0 手工总车费(需搭配调整原因)
confirmCrossResident Body Boolean/null 否 - 跨常驻车派单显式确认

出参 Result<ChangeAssignmentRespVO>

字段 类型 说明
assignmentId String 新派单 ID(雪花字符串)
assignmentSlotId String 槽位 ID(雪花字符串)
previousAssignmentGroupId String/null 原派车组 ID
newAssignmentGroupId String/null 新派车组 ID
assignmentStatus String 新派单状态
effectiveDate String(日期) 改派生效日期
affectedDays Integer 影响天数
vehicleFeeTotal String(Number)/null 最终总车费
dailyVehicleFees Array/null 逐日车费
warningCode String/null 预警码
warningMessage String/null 预警文案

请求示例

POST /admin/fleet/assignments/2091068072742756354/change
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "effectiveDate": "2026-09-02",
  "newVehicleId": "2094307514693648385",
  "newDriverId": "2065272145289658370",
  "reason": "车队停用前换车",
  "requestId": "web-change-<uuid>"
}

响应示例

成功(结构示例;本次 TEST 的改派请求按预期被 605074 拦截,未产生成功样本):

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "assignmentId": "2094310000000000002",
    "assignmentSlotId": "2094310000000000003",
    "assignmentStatus": "assigned",
    "effectiveDate": "2026-09-02",
    "affectedDays": 1
  }
}

空数据 / 降级响应

本接口为写接口,无空数据分支;幂等重放(同 requestId + 同载荷)返回首个成功回执,不重复改派。改派目标派单自身已取消/已完成时不归本单变化范围,走既有 605066「派单已取消,无法修改,请重新派车」。

错误响应

改派目标资源所属车队已停用(新增,TEST 实测原文;写库前拦截、原派单零副作用):

{ "code": 605074, "message": "车队已停用不可派新单(请先启用车队或换车)", "success": false, "data": null }

生效日期不在派单服务日期范围内(既有):

{ "code": 605028, "message": "生效日期不在派单服务日期范围内", "success": false, "data": null }

派单已取消(既有):

{ "code": 605066, "message": "派单已取消,无法修改,请重新派车", "success": false, "data": null }

业务边界

  • 车队校验先于 605037/605038 执行(与创建同口径);车辆侧车队停用必拦(含纯费用调整场景——该场景 #6717 起本就被合并状态 605037 拦截,此处只是把报错换成更准的 605074)。
  • 纯费用调整(identityUnchanged,车人未换)时司机侧不查常驻车队:在途单司机的另一辆常驻车被联动停用,不影响本单费用修订(「已派订单不受影响」边界)。
  • 换车/换司机(非纯费用调整)时司机常驻车队停用 → 605074。
  • 幂等:同 requestId 重放不重复改派;车锁/司机锁串行化。

四、契约约束与正确调用方式

✅ 正确 / ❌ 错误 payload 对照

场景 调用 / 结果
✅ 供应商域投递停止合作事件 {"supplierId":"...","targetStatus":"SUSPENDED","eventId":"<全局唯一>","occurredAt":"..."} + X-Internal-Token 头,Feign LB 直连
✅ targetStatus 传小写 suspended 大小写不敏感,正常触发联动停用
✅ 同 eventId 网络重试重投 返 duplicate=true 成功回执,按已消费处理,不要告警重试
✅ 非停止合作态(如 ACTIVE 恢复) 返 ignored=true 成功,不联动(恢复合作需人工启用车队)
✅ 前端收到 605074 按文案引导:启用车队(车队管理页)或更换车辆/司机后重试
✅ precheck 渲染 fleet_team_disabled 与 vehicle_unavailable 同级醒目提示;加入 type 白名单映射
❌ 前端直接调 /internal/fleet-teams/supplier-status-sync 网关不路由(业务码 404);该端点仅供服务间 Feign 调用
❌ 调用方对 duplicate=true 回执告警重试 重复投递是「已消费」而非失败,告警重试会造成误报风暴
❌ 复用 eventId 投递不同事件 幂等键冲突会让新事件被当成已消费吞掉;eventId 必须全局唯一(建议含 supplierId+状态+时间戳)
❌ 雪花 ID 转 JavaScript Number supplierId/disabledTeamIds[]/driverId/vehicleId/assignmentId 均为雪花字符串,Number()/parseInt()/一元 + 会精度丢失
❌ 车务忽略 precheck 预警强行提交 创建/改派锁内 605074 硬拦截,零写入

关键提示(当前 TEST 构建)

  • 605074 与 605037 的分工:605074 专指「车队级」停用(多因供应商 SUSPENDED/BLACKLIST 联动);605037 指车辆自身维保/停用。前端错误码字典两个都要保留。
  • 候选排除是静默的:前端不需要也不可能在候选接口里展示「被排除的司机」;如业务需要解释「为什么某司机不可派」,引导至司机档案查看常驻车所属车队状态。
  • 车队重新启用后(车队管理 → 启用,前提是已绑供应商),候选/预校验/创建/改派即时恢复,无缓存延迟。

五、数据库行为

  • internal 联动端点只写 fleet_team 行:CAS 批量 update(status ACTIVE→DISABLED,where 带 status=ACTIVE 守卫),重复/并发自然命中 0 行;不跨服务写 supplier_main/supplier_resource_rel。
  • 幂等键在 Redis:fleet:team:supplier-status-sync:{eventId},TTL 24h(@Idempotent 开窗),窗口内重投不再触库。
  • 派单侧零表结构变更:候选排除 = 查询过滤(只读);创建/改派 605074 = 锁内读校验(只读),不写任何额外表。
  • 本工单无 Flyway 迁移:不加表、不加列(车队-供应商关联列由 #6717 迁移建好)。
  • 联动停用不影响存量 fleet_assignment 行(本单边界:不动在途派单/槽位/占用)。

六、边界行为

  • 未登录/登录失效:业务码 401(网关拦截)。
  • internal 端点 X-Internal-Token 缺失/错误:403(过滤器直出,字段名 msg)。
  • 经网关误调 /internal/**:业务码 404「接口不存在」(无路由,符合预期)。
  • Bean Validation 失败:400(如缺 eventId/vehicleId/startDate)。
  • 车队已停用不可派新单:605074(新增;创建/改派写库前硬拦截)。
  • 车辆维保/停用:605037(既有;车队校验通过后才会命中)。
  • 司机休假/待激活:605038(既有;同上)。
  • 行程已结束只读:605047(既有;过期需求/派单先于 605074 命中,TEST 实测)。
  • 派单已取消不可改派:605066(既有)。
  • 生效日期越出派单服务日期:605028(既有)。
  • 车队已停用仍选该车(车队管理写口径):601102(既有,本单不改)。
  • 跨服务 Feign 不可用:本单 fleet 侧链路不新增 Feign 调用(联动方向是 resource → fleet);候选/预校验/创建/改派均为库内读写,不受 resource 侧可用性影响。

六.5、枚举 / 数据字典

targetStatus(供应商目标状态,internal 端点入参)

所属字段: SupplierStatusSyncReqVO.targetStatus | 类型: String(大小写不敏感)

值 中文 说明
SUSPENDED 暂停合作 触发联动停用名下在启车队
BLACKLIST 拉黑 触发联动停用名下在启车队
DRAFT/VETTING/ACTIVE/FROZEN/ARCHIVED 草稿/审核中/合作中/冻结/归档 忽略(ignored=true),不联动

warnings[].type(precheck 非阻断提示类型)

所属字段: PrecheckRespVO.warnings[].type | 类型: String

值 中文 说明
seats_short 座位不足 既有
vehicle_unavailable 车不可用 既有(车辆不存在/维保/停用;车队停用合并车辆状态后同现)
driver_unavailable 司机不可用 既有
driver_blacklisted 司机已黑名单 既有(提交将被 605006 拦截)
driver_season_not_registered 司机非在册赛季 既有(提交将被 605013 拦截)
fleet_team_disabled 车队已停用 本单新增(提交将被 605074 拦截,引导启用车队或换车)
cross_resident 跨常驻 既有
license_expired 驾照过期 既有
veh_inspect_expired 车辆年检过期 既有
veh_insure_expired 车辆保险过期 既有

车队状态(fleet_team.status)

所属字段: 车队管理相关响应 status | 类型: String

值 中文 说明
ACTIVE 启用 名下资源可派;前提:已关联供应商(#6717/#6811 不变量)
DISABLED 停用 名下车辆/常驻司机不可派新单(本单贯通到派单全链路)

六.6、修改前后对比

字段/枚举级对比

项 改前 改后
POST /internal/fleet-teams/supplier-status-sync 不存在 新增 internal 端点(ReqVO 4 字段 / RespVO 6 字段)
PrecheckRespVO.warnings[].type 值域 9 种既有 type 新增 fleet_team_disabled
派单错误码 无车队级码(车队停用被合并状态误报 605037) 新增 605074「车队已停用不可派新单(请先启用车队或换车)」
candidates 请求/响应字段 现有结构 零字段变化(行为变化:司机候选集合缩小)

行为级对比

行为 改前 改后
供应商暂停/拉黑 车队无感知,名下资源照常可派 联动停用名下在启车队(强制,不做在役车辆守卫)
司机候选(常驻车挂停用车队) 照常返回 静默排除
precheck 车队停用提示 仅 vehicle_unavailable(车辆侧) 追加 fleet_team_disabled(车+司机两侧)
创建/改派选到停用车队资源 报 605037「车辆维保/停用」(语义不准);司机侧可漏过 统一报 605074(先于 605037/605038)
在途单纯费用调整(司机另一常驻车车队被停用) 司机侧无校验 仍不校验(刻意豁免,不卡死在途单)
供应商恢复合作 - 不自动启用车队(人工启用,安全侧)

六.7、影响评估

  • 是否破坏向后兼容: 否。响应结构无字段删改;候选集合缩小属业务收口;新增 warning type 与错误码对旧前端为未知值——旧前端按通用兜底渲染即可,不崩。
  • 前端是否必须同步上线: 建议同批但不强阻断。两项适配:① precheck warning type 白名单加 fleet_team_disabled(不加则该预警被静默丢弃,车务要到提交时才知道被拦);② 错误码字典加 605074 文案与引导动作(不加则按通用错误提示展示)。
  • 前端 workaround 清理点: 无(此前无对应前端绕行逻辑)。
  • 服务间依赖: resource 侧 #6844 未上线前,internal 端点无生产流量,派单拦截仅对人工停用/无供应商自动停用的车队生效——同样符合预期。

七、不影响范围

  • 仅影响: 派单候选查询 / 派单预校验 / 派单创建(批量,单派同路径)/ 派单改派 4 个管理后台端点 + 1 个 internal 端点(fleet 侧)。
  • 零影响:
    • 团批组派(GroupDispatch 独立体系,不写 fleet_assignment,PR 明示不在拦截范围)。
    • 存量派单的最终确认 / 撤销取消 / 行程短信 / 软清等推进类写口(不挂车队状态新校验,供应商事后停用不卡死在途单)。
    • 车队管理 CRUD 端点(/admin/fleet/teams/** 本单未改;启停用/供应商守卫口径仍属 #6717/#6811)。
    • 司机档案 / 车辆档案 / 司机列表「所属车队」筛选(排除逻辑复用同一反查端口,档案域零改动感知)。
    • 供应商域九大资源模块与供应商管理端点(联动发出端 #6844 另行落地)。
    • 小程序端(派单链路为管理后台车务功能)。
    • Gateway 路由(/admin/fleet/assignments/** 通配已覆盖;/internal/** 本就不经网关,无需新增配置)。
    • Redis/MQ(仅新增幂等键 fleet:team:supplier-status-sync:{eventId},无新消息)。

八、测试环境已验证

真实 TEST 环境实测(2026-08-31,网关 https://api.test.1814.love:9443,admin token 走 /admin/auth/login;internal 端点经 SSH 在测试服本机 curl 127.0.0.1:8087 直连,模拟 Feign 调用方):

[internal 链路]
POST /internal/fleet-teams/supplier-status-sync  SUSPENDED 首投
  → 200, ignored=false, duplicate=false, disabledCount=1, disabledTeamIds=["352696323818000384"]  ✓
POST /internal/fleet-teams/supplier-status-sync  同 eventId 重投(幂等)
  → 200, duplicate=true, disabledCount=0, disabledTeamIds=[]  ✓
POST /internal/fleet-teams/supplier-status-sync  targetStatus=ACTIVE(非停合作态)
  → 200, ignored=true, disabledCount=0  ✓
POST /internal/fleet-teams/supplier-status-sync  缺 eventId
  → 400「事件ID不能为空」  ✓
POST /internal/fleet-teams/supplier-status-sync  错误 X-Internal-Token
  → 403「内部接口禁止外部访问」  ✓
POST 网关 /internal/fleet-teams/supplier-status-sync
  → 业务码 404「接口不存在」(/internal 不经网关路由,符合预期)  ✓

[管理后台链路(admin token)]
GET  /admin/fleet/teams/352696323818000384
  → status=DISABLED(联动生效,全栈贯通)  ✓
POST /admin/fleet/assignments/candidates  driverKeyword=斯琴
  → 停用前 records=1 条 → 联动停用后 records=[] total=0(新增排除生效)  ✓
POST /admin/fleet/assignments/candidates  vehicleKeyword=T6843
  → records=[](车辆侧 #6717 既有排除仍生效)  ✓
POST /admin/fleet/assignments/precheck  停用车队车辆+常驻司机
  → warnings 含 {"type":"fleet_team_disabled","msg":"车队已停用不可派新单,提交派单将被拦截,请先启用车队或换车"}  ✓
POST /admin/fleet/assignments/2091068072742756354/change  改派到停用车队车+司机
  → 605074「车队已停用不可派新单(请先启用车队或换车)」,原派单零副作用(事后核 update_time 未变)  ✓
POST /admin/fleet/assignments/batch  停用车队车+司机(过期行程占位)
  → 605047「行程已结束」优先命中(前置守卫先于 605074,符合校验顺序)  ✓
POST /admin/fleet/teams/352696323818000384/enable  重新启用
  → 200;同条件 candidates 斯琴恢复 1 条(恢复路径即时生效)  ✓
  • 部署:Deploy Panel 任务 a7300401(hl-fleet-service,2026-08-31 14:02 success,8087/8187 双实例滚动均 UP),部署 dev-v3 HEAD 2543febee(含本 PR merge commit df78cce550b4f4ea6a9f40352c1dd176b8d8fba6,2026-08-31 13:50 合入)。
  • batch 创建 605074 正向链未能在 TEST 实跑:测试服全部待派(unassigned)占位的行程均已结束,创建请求先撞 605047;构造可派需求需完整订单+需求展开链,超出本次验证范围。该路径由单测覆盖(AssignmentServiceTest#create_fleetTeamDisabledVehicle_throws605074 / create_driverOnDisabledTeam_throws605074,与已实测的改派路径 change_fleetTeamDisabledVehicle_throws605074 共用同一锁内校验方法),建议 mmg 联调时在真实新订单上补验一次。
  • 前置态构造说明:「停用车队 + 在役车辆/常驻司机」状态被人工停用守卫(601103)与车队选择守卫(601102)封锁,只有供应商联动能合法产生——实测经 internal 端点真实产生(正是本端点设计语义);测试车队的「ACTIVE + 已绑供应商」前置态由 DB 直改构造(测试服 fixture)。
  • 本地自动化(PR 自报):目标单测 730 个全绿(AssignmentServiceTest 522 / FleetTeamServiceTest 36 / DriverServiceTest 157 / 新增 Mapper+Controller 测试);全量 mvn -pl hl-fleet-service test 3913 tests 0 失败(含 IT);FleetRedLineArchTest 13/13 绿;spotless:check 通过。
  • 数据清理:测试车队(352696323818000384)、测试车辆(2094307514693648385,蒙A-T6843)均已删除(DB 复核无残留);测试用模拟供应商 ID(8996843000000000001)未在 supplier_main 落库,随车队删除清除;司机斯琴档案未改动(常驻绑定随车辆删除解除);被改派实测的真实派单(2091068072742756354)经核零副作用;admin token 已登出。

九、相关历史 PR

PR Issue 说明 是否仍有效
#6868 #6843 供应商暂停合作联动停用车队 + 派单拦截停用车队司机车辆(本次) ✅ 最新
#6753 #6717 车队关联供应商并展示供应商全名(车队-供应商关联与车辆侧排除的前置) ✅ 已被本单扩展
#6827 #6811 车队保存无供应商自动停用 + 601112 在役车辆守卫(车队停用来源之二) ✅ 并存
- #6844 供应商侧通知发出端(resource → fleet 联动调用方),待指派实现 ⏳ 未开始

十、相关文档

  • 关联 Issue: wx/HL#6843
  • 关联 PR: wx/HL#6868
  • 前置契约: changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md
  • 前置契约: changelogs-v2/2026-08/31_6811_车队保存无供应商时自动停用与在役车辆守卫-修改接口-管理后台.md

撤回

  1. 前端先摘除对 605074 与 fleet_team_disabled 的特化渲染(按通用兜底展示),保持线上可用。
  2. 从最新 dev-v3 建独立回退分支,回退 PR #6868 的合并 commit(df78cce5),验证后经独立 PR 合入。
  3. ⚠️ 已被联动停用的车队不会因回退自动恢复:回退代码只撤联动与拦截逻辑,已落 DISABLED 的车队需在车队管理人工启用(启用前置 = 已绑供应商)。
  4. 仅需止血时也可不回退整单:internal 端点无生产流量前(#6844 未上线)本单对线上唯一可感知影响是人工停用/无供应商车队在派单侧的拦截收口,属预期行为。
  5. 使用 Deploy Panel 滚动部署 hl-fleet-service;本单无数据库迁移,无需回滚脚本。
  6. 撤回后经 Gateway 验证:候选恢复返回停用车队常驻司机、precheck 不再出现 fleet_team_disabled、改派停用车队资源回报 605037 老口径。

关联 / 联系人

链接

  • Issue: #6843
  • PR: #6868
  • Merge commit: df78cce550b4f4ea6a9f40352c1dd176b8d8fba6

联系人

  • 后端负责人: @wx
  • 前端联动: @mmg(① precheck warnings[] type 白名单加 fleet_team_disabled;② 错误码字典加 605074「车队已停用不可派新单(请先启用车队或换车)」+ 引导动作;③ 司机候选静默排除无前端适配量)
  • 服务间联动: resource 侧 #6844 实现方按本文 §三.1 契约投递(supplierId/targetStatus/eventId/occurredAt + X-Internal-Token,重复投递认 duplicate=true 成功回执)