文件
hl-api-changelog/changelogs-v2/2026-09/22_7982_共用关系同批准入多个新成员不再误报605001-修复-管理后台.md
T
API Changelog Bot和Claude Opus 5 71ad3dbfcd
changelog-filename-gate / validate (push) Failing after 1s
docs(7982): 订正共用关系确认端点的契约段落——首版字段名与错误码文案有误
首版(3328730)的「接口详情」章把入参 resourceType 写成了不存在的 dimension、
漏掉 serviceDate 与 costBearer 两个必填字段、引用了两个并不存在的 VO 类名
(CreateShareGroupReqVO / ShareGroupCreateRespVO)、响应示例里给出了 VO 上不存在
的 id / createdAt / admissionAt 字段,并把 605001 的提示文案写成了另一段文字
(真实文案是「派单冲突:该车日期段已派」)。照首版的请求示例构造的请求发不出去
——字段名对不上,且缺两个必填字段。

本版每一个字段、每一条错误码文案均取自 dev-v3 上的源码(VO 的 @ApiModelProperty
声明与 IErrorCode.of 定义),并逐处标注出处文件。同时补上首版缺失的内容:600009
这个前端会先撞上的码、三步校验顺序、605036 跨常驻车派单需确认的前端交互、以及
「防重与幂等是两件事」的区分。

删除首版那张声称来自测试环境实测的读数表:其数据无法溯源到任何一次真实调用
(首版构造请求所用的字段名在服务端并不存在,该请求不可能被受理)。修复行为的
证据来源改为如实标注为源码与真库集成测试 ShareGroupOverlapProjectionIntegrationTest,
并写明测试环境上不存在可供对跑的修复前环境(修复 2026-09-19 已合入)。

gateway_status=verified 的依据同步换成 2026-09-22 经网关的实调读数(HTTP 200 +
业务码 600009,配不存在路径的阴性对照 code=404,两者可分辨),并在 status_note
与正文里都写明它的覆盖边界:只验到端点可达性与契约反序列化,未验证修复行为本身。

Refs #7982

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 03:31:04 +08:00

23 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 7982 fleet: 确认共用关系时同批准入多个新成员不再误报冲突码 605001(行为修复,无接口字段变化) admin wx(GIT) 修复 deployed verified not_required backend_status=deployed:修复 PR #7983(`e7cecb6d7`)+ 测试补强 PR #8096(`970f9a187`)已合入 dev-v3;fleet 测试服部署点 `bd19b79c8`,`git merge-base --is-ancestor e7cecb6d7 bd19b79c8` 与 `970f9a187 bd19b79c8` **均为真**(2026-09-22 逐条复核)。gateway_status=verified:2026-09-22 经网关实调该端点 `POST https://api.test.1814.love:9443/admin/fleet/group-dispatch/batches/1/share-groups`,HTTP 200 + 业务码 600009(刻意使用不存在的 groupBatchId=1,第一道校验即失败关闭);同轮阴性对照打在不存在的路径上返回 `code=404 接口不存在`,两者可分辨 ⇒ 路由、鉴权(需 VEHICLE_MANAGER 角色)、请求体反序列化三项均通,且全程未触达写库路径。**该验证的覆盖边界**:只验到端点可达性与契约反序列化,**未验证本次修复的行为本身**(同批多个待准入成员不再误报 605001)——那需要真实团期与派单夹具;修复行为的证据来源是源码与真库集成测试 `ShareGroupOverlapProjectionIntegrationTest`。frontend_status=not_required:接口形状(路径、入参、响应字段、错误码)**完全未变**,本次仅修复冲突判定的内部投影缺列。 2026-09-22 dev-v3

fleet: 确认共用关系时同批准入多个新成员不再误报冲突码 605001(行为修复,无接口字段变化)

存放目录: 二期(fleet 车务)→ changelogs-v2/2026-09/

🔴 2026-09-22 订正说明(首版字段写错,请重新核对)

本文件首版(提交 3328730)的「接口详情」章把请求/响应契约写错了,具体:把入参 resourceType 写成了不存在的 dimension、漏掉 serviceDate 与 costBearer 两个必填字段、引用了两个不存在的 VO 类名(CreateShareGroupReqVO / ShareGroupCreateRespVO)、响应示例里给出了 VO 上不存在的 id / createdAt / admissionAt 字段,并把 605001 的提示文案写成了另一段文字。照首版的请求示例构造的请求发不出去(缺必填字段 + 字段名不匹配)。

若已照首版写过代码,请按本版重新核对字段名。 本版每一个字段、每一个错误码文案均取自 dev-v3 分支上的源码(VO 的 @ApiModelProperty 声明与 IErrorCode.of(...) 定义),下文逐处标注了出处文件。

⚠️ 关键变化

接口契约零变化——路径、方法、请求参数、响应字段、错误码全部不动。

变的是冲突判定的投影。从本次修复上线起:

  1. 确认共用关系时,同一个 POST 请求里传入 2 个或更多尚未占用该资源的 ASSIGNMENT 成员、且没有既有共用关系可对标时,从第二个成员开始不再误报错误码 605001(605001 = 派单冲突:该车日期段已派);
  2. 修复后同一请求正常受理,全部成员入库成该关系的成员行。

🔴 这个误报在上述场景下是必现的,不是偶发:根因是一处数据投影恒缺列(见下),每次走到这条回退路径都会做错判定。

一、背景

缺陷形态

确认共用关系时的冲突判定有两层:

  1. 第一层:按既有共用关系逐一检查新增成员是否与它们的成员产生冲突;
  2. 第二层(回退):若找不到既有关系可对标,则按新增成员之间的相互关系判定冲突。

第一层是正常路径,拿得到全量数据。第二层回退靠「按需求认对端」——用成员行的 requirement_id 去认出另一侧是谁。但组行视图与候选面的数据投影里没有把 requirement_id 选出来,于是这条回退路径从上线起就恒失效。

为什么偏偏卡在这一列:成员正在改绑的那段窗口里,成员表存的还是旧的派车行,按 ID 查对端必然落空,requirement_id 是那段窗口里唯一还认得出对端的键——而它恰好是被投影丢掉的那一列。

⇒ 在同批多成员且无既有关系可对标的场景下,从第二个成员开始每一个都认不出对端,必定返回 605001。

这个判断的证据来源

证据是源码与真库集成测试(ShareGroupOverlapProjectionIntegrationTest),不是测试环境上的前后对跑读数。 修复已于 2026-09-19 合入 dev-v3,测试环境此后一直处于修复后的状态,客观上不存在可供对跑的修复前环境。

本文件不提供任何声称来自测试环境实测的修复前/修复后请求读数。(首版曾给出一张这样的表格,已在本版删除——那张表的数据无法溯源到任何一次真实调用。)

修复

PR #7983(e7cecb6d7):把 requirement_id 补进组行视图与候选面的投影列清单,让「按需求认对端」的回退真正取得到那一列。提交标题逐字为「组行/候选面投影补 requirement_id——「按需求认对端」的回退从未生效过」。

测试补强 PR #8096(970f9a187):补真库集成测试。

二、变更接口清单

本条不新增、不修改、不删除任何 HTTP 接口,也不改动任何请求/响应字段与错误码。

下列既有端点的冲突判定行为得到修复(契约形状不变):

METHOD Path 变化
POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups 同批确认多个尚未占用的 ASSIGNMENT 成员时,冲突判定不再误报 605001;多成员共用关系可一次请求完成

三、接口详情

1. 确认团期车辆共用关系 POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups

VO:ShareGroupConfirmReqVO → Result<ShareGroupRespVO>(字段无增删改)

出处:hl-fleet-service/src/main/java/com/hulalv/fleet/dispatch/controller/GroupDispatchShareController.java, @RequestMapping("/admin/fleet/group-dispatch") + @PostMapping("/batches/{groupBatchId}/share-groups")。 网关路由复用既有 - Path=/admin/fleet/** → lb://hl-fleet-service(hl-gateway/src/main/resources/application.yml),本单不新增路由。 权限点:写口 fleet:group-dispatch:write(读口 fleet:group-dispatch:view),本单不新增权限点。

使用场景

管理后台「团期配车」页,车务确认「本团这一个服务日、这一辆车(或这一名司机),由下列几方共用」。

⚠️ 端点名是「确认」不是「创建」,这不是措辞问题:members 是成员全集而不是增量——同一 (团, 日, 维度, 资源) 重复提交按新全集覆盖,返回同一个 shareGroupId,不抛错。前端不要按「新增一个成员就调一次」来用。

⚠️ 这个端点是复合原子操作,不是一次纯粹的关系登记

服务端在同一事务里依次做三件事:先落授权 → 再把尚未占用的成员派进这辆车 → 由准入检查读到刚落的授权而放行 → 最后回读断言收口。中途任一步失败整体回滚,不留「关系已建但没占上车」或「占上车但没关系」的中间态。

之所以不能拆成「先各自派好车、再来建关系」:跨城场景下第二户根本派不进来(占用准入跨城即拒)。

入参(本次无变化)

字段与文案出处:hl-fleet-service/.../dispatch/vo/ShareGroupConfirmReqVO.java、ShareGroupMemberReqVO.java 的 @ApiModelProperty。

字段 类型 必填 说明
serviceDate String yyyy-MM-dd ✅ 共用发生的服务日;必须落在该团基线 serviceDates 内,窗外抛 602104
resourceType String ✅ 资源维度:VEHICLE / DRIVER;车与司机分别建关系
resourceId Long ✅ 车辆 ID 或司机 ID
members List ✅ 成员全集(不是增量),2-20 个,越界抛 602100 / 602101
members[i].sourceType String ✅ ASSIGNMENT=逐户接送派单 / GROUP_DISPATCH=团级配车行
members[i].sourceId Long ✅ fleet_assignment.assignment_id 或 fleet_group_dispatch.dispatch_id
members[i].admissionIntent String ❌ OCCUPYING=已占着这辆车 / PENDING_ADMISSION=本次一并派入。仅供前端交互提示
costBearer String ✅ 成本承担方:GROUP=记团级整车 / ORDER=记指定户;缺失或非法抛 602107
costBearerOrderId Long 条件必填 costBearer=ORDER 时必填,且必须是成员中某个 ASSIGNMENT 的订单
remark String ❌ 确认备注,≤ 200 字
confirmCrossResident Boolean ❌ 跨常驻车派单确认,见下方 605036;不传或 false 都表示不确认

🔴 admissionIntent 服务端完全不读、不校验、不落库。 成员到底是「已经占着这辆车」还是「本次一并派入」,一律按库里的实际占用现查现判。它纯粹是给前端做交互提示(按钮文案、二次确认)用的。填错不会改变服务端行为,也不会因此报错。

出参(本次无变化)

字段出处:hl-fleet-service/.../dispatch/vo/ShareGroupRespVO.java、ShareGroupMemberRespVO.java 的 @ApiModelProperty。

字段 类型 说明
shareGroupId String 共用关系 ID(雪花 ID,@JsonSerialize(ToStringSerializer) 按字符串序列化)
groupBatchId String 运营团期 ID(字符串序列化)
serviceDate String yyyy-MM-dd 共用发生的服务日
resourceType String 资源维度:VEHICLE / DRIVER
resourceId String 车辆或司机 ID(字符串序列化)
status String 关系状态:ACTIVE / RELEASED(没有 PENDING 这个取值)
costBearer String 成本承担方:GROUP / ORDER
costBearerOrderId String costBearer=ORDER 时的承担订单 ID(字符串序列化;否则为 null)
costSourceRefNo String 车费来源引用 = SHARE-{shareGroupId}
members List 成员全集
members[i].sourceType String ASSIGNMENT / GROUP_DISPATCH
members[i].sourceId String 成员来源 ID(字符串序列化)
members[i].requirementId String 用车需求 ID(ASSIGNMENT 成员的准入授权键;团级配车行为空)
members[i].orderId String 订单 ID(ASSIGNMENT 成员)
confirmedBy String 确认人 adminId(字符串序列化)
confirmedAt String 确认时间(服务端类型 LocalDateTime,按全局 Jackson 约定序列化)
version Integer 乐观锁版本号
history List 变更历史;仅查询端点带 includeReleased=true 时返回,本确认端点的响应里为 null

⚠️ 成员出参只有上面 4 个字段:sourceType / sourceId / requirementId / orderId。成员行没有独立的 id,也没有任何时间戳字段。

⚠️ costSourceRefNo 按关系身份生成:同一关系生命周期内恒定,改 costBearer、重复确认都不变;关系解除后在同一槽重建会拿到新值。核团比对以它为准。

请求示例

POST /admin/fleet/group-dispatch/batches/8801/share-groups HTTP/1.1
Host: api.test.1814.love:9443
Content-Type: application/json
Authorization: Bearer {token}

{
  "serviceDate": "2026-09-12",
  "resourceType": "VEHICLE",
  "resourceId": 1,
  "costBearer": "GROUP",
  "remark": "本车这一天承接这两户的接送",
  "members": [
    {
      "sourceType": "GROUP_DISPATCH",
      "sourceId": 88001,
      "admissionIntent": "OCCUPYING"
    },
    {
      "sourceType": "ASSIGNMENT",
      "sourceId": 88002,
      "admissionIntent": "PENDING_ADMISSION"
    },
    {
      "sourceType": "ASSIGNMENT",
      "sourceId": 88003,
      "admissionIntent": "PENDING_ADMISSION"
    }
  ]
}

响应示例

⚠️ 以下是按 VO 的 @ApiModelProperty 声明与其 example 值构造的字段结构示意,不是某一次实测报文。 真实的 ID 是 19 位雪花,示例里的短 ID 沿用 VO 的 example 取值,长度不代表真实形态——前端一律按字符串处理。

{
  "code": 200,
  "message": "成功",
  "data": {
    "shareGroupId": "77001",
    "groupBatchId": "8801",
    "serviceDate": "2026-09-12",
    "resourceType": "VEHICLE",
    "resourceId": "1",
    "status": "ACTIVE",
    "costBearer": "GROUP",
    "costBearerOrderId": null,
    "costSourceRefNo": "SHARE-77001",
    "members": [
      {
        "sourceType": "GROUP_DISPATCH",
        "sourceId": "88001",
        "requirementId": null,
        "orderId": null
      },
      {
        "sourceType": "ASSIGNMENT",
        "sourceId": "88002",
        "requirementId": "5501",
        "orderId": "70123"
      },
      {
        "sourceType": "ASSIGNMENT",
        "sourceId": "88003",
        "requirementId": "5502",
        "orderId": "70124"
      }
    ],
    "confirmedBy": "1001",
    "confirmedAt": "2026-09-12 10:30:00",
    "version": 1,
    "history": null
  },
  "success": true
}

错误码

文案出处:GroupDispatchShareErrorCode.java(602 段)与 AssignmentErrorCode.java(605 段)里的 IErrorCode.of(...) 定义,逐字。 HL 约定:业务失败与入参校验一律走 HTTP 200,错误码在响应信封的 code 字段里(鉴权/系统异常除外)。

code 服务端文案(逐字) 触发条件与前端处理
600009 团期配车权威基线不可用,请稍后重试或检查团期状态 团期不存在,或其服务日未设置/不完整。这是最先触发的一道校验(见下方校验顺序),不是参数格式错
602100 共用关系成员不能少于 2 个 members 少于 2 个。共用关系至少要两方
602101 共用关系成员不能超过 20 个 members 超过 20 个
602102 成员在该资源日无活跃占用: {0} 提交前回读断言发现成员没占上车,整体回滚
602103 成员派单不属于本团或团期身份未知: {0} group_batch_id 与 requirement_id 任一为 NULL 一律拒——未知不得当同团通过
602104 服务日不在团期服务日窗内: {0} 有意的边界:窗外日没有团级配车行可挂靠,仍走 R3-EX 城市衔接
602105 团级配车成员不属于本团或当日非活跃: {0} GROUP_DISPATCH 成员的校验
602106 成员已属于另一个共用关系: {0} 要求先解除原关系
602107 成本承担方缺失或非法 costBearer 未传或取值非法
602109 共用关系已被并发修改,请刷新后重试 乐观锁冲突,提示用户刷新
605001 派单冲突:该车日期段已派 本次修复的误报点。修复后它回归真实语义,见下方「影响评估」
605036 司机与车辆不是常驻组合,请确认跨常驻车派单后重试({0}:{1};车辆 {2};司机 {3}) 跨常驻车派单需人工确认,见下

校验顺序(决定你先看到哪个错误码)

服务端 confirm() 的前两步在事务之外,顺序是固定的:

  1. resourceType 合法性;
  2. 团期权威基线——一次同步 Feign 向 order-v3 取该团的服务日窗。取不到 → 600009;取到了但 serviceDate 不在窗内 → 602104;
  3. 以上都过了,才进入事务做成员数(602100/602101)、成员归属(602103/602105/602106)、成本承担方(602107)等校验,以及实际的授权、派单、准入写入。

⚠️ 所以传一个不存在的 groupBatchId 时,先撞上的是 600009 而不是成员相关的校验码——排查时不要据此认为成员参数已经通过了校验。

🔴 605036 跨常驻车派单需确认(前端必须实现的交互)

端点第 3 步把成员改派进共用资源时走的是既有派单写路径,它有一道提示型守卫——满足下列任一条即判「跨常驻」,未确认一律拒:

  1. 这辆共用车已设常驻司机,且与该成员当前司机不是同一人;
  2. 该成员当前司机本身是别的车的常驻司机。

⚠️ 两条是「或」的关系:共用车没设常驻司机(primary_driver_id 为空)只躲开①,②照样会触发。

⚠️ 车务在页面上选的是「一辆车」,但 605036 说的是司机——resourceType=VEHICLE 时被派入的司机不是车务选的,是该成员原派单上的司机(服务端按成员原样带过去,本端点不改司机)。所以 605036 的消息会点名「哪条派单 / 跨常驻的具体形态 / 车牌 / 司机姓名」。

前端处理方式:拿到 605036 后向车务展示 message 原文并询问「确认跨常驻车派单?」,确认后带 confirmCrossResident=true 原样重发同一份请求即可。失败路径会释放 10 秒防重键,不会撞「请勿重复提交」。不传或传 false 都表示不确认,服务端不会替你确认。

🔴 防重与幂等是两件事

  1. 10 秒内重复提交同一份成员全集会被防重窗口拒绝——前端按「请勿重复提交」提示,不要当失败报红;
  2. 窗口之外重复提交同一份全集会正常受理,那是成功,返回同一个 shareGroupId。

防重键 = serviceDate : resourceType : resourceId : 成员集合排序摘要。所以:

  • 只改 costBearer 不改成员时,键不变,10 秒内会被挡下(这是保护不是缺陷);
  • 改了成员全集就是另一次业务操作,立刻受理,车务加一个成员不必等 10 秒;
  • 成员先排序再拼摘要——前端两次提交的成员顺序不同但集合相同时那是同一份计划,不会绕过防重窗口;
  • confirmCrossResident 不参与防重键(它不是业务身份的一部分)。

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

本接口在表层契约上无任何变化。修复前后的入参格式、响应字段、HTTP 状态码、业务错误码全部一致——差别仅在内部冲突判定所依赖的数据投影是否完整。

场景 修复前 修复后
同批 2+ 个尚未占用的 ASSIGNMENT 成员、无既有关系可对标 605001(误报) 正常受理
有既有共用关系可对标(走第一层判定) 正常 正常(不变)
入参字段、响应字段、错误码清单 — 完全不变

五、数据库行为

涉及的表:fleet_group_dispatch_share_group(关系主体)、fleet_group_dispatch_share_member(成员行)、fleet_group_dispatch_share_log(变更历史)。

修复后,同批多个尚未占用的成员会全部入库成该关系的成员行。修复前,从第二个此类成员开始会抛 605001,整个请求在同一事务里回滚,写入不落地。

六、边界行为

  • 未登录 → 网关拦截(鉴权异常不走业务信封)
  • 无 fleet:group-dispatch:write 权限 → 鉴权拦截
  • members 少于 2 个 / 超过 20 个 → 602100 / 602101
  • serviceDate 不在团期服务日窗内 → 602104
  • resourceType 不是 VEHICLE 或 DRIVER → 参数校验失败(@NotBlank 只校验非空,取值合法性由服务端业务校验兜底)
  • 成员已属于另一个共用关系 → 602106(需先解除)
  • 重复提交同一份成员全集 → 窗口内被防重拒绝;窗口外正常受理并返回同一个 shareGroupId

七、影响评估

  • 是否破坏向后兼容:否——请求/响应字段、错误码全部不变。
  • 前端是否必须同步上线:否——管理后台无需改任何代码即可获得修复效果。
  • 可以清理的 workaround:若此前为了绕开 605001 而实现了「一个成员发一次请求」的逻辑,现在可以改回一次请求传全部成员。⚠️ 注意 members 是全集不是增量,逐个发送的写法在语义上本来就不等价(后一次会覆盖前一次的全集)。
  • 605001 语义的恢复:该码此前会在合法请求上误报,修复后它回归为真实派单冲突的表示(派单冲突:该车日期段已派)——收到它就意味着确实存在冲突,可以按真实冲突提示用户。

八、不影响范围

  • 任何接口的请求参数、响应字段、错误码清单——全部不变。
  • 既有共用关系的查询、解除等其它端点。
  • 605001 以外其它错误码的触发口径。

九、验证状态

后端部署(已核实):修复 PR #7983(e7cecb6d7)+ 测试补强 PR #8096(970f9a187)均已合入 dev-v3;fleet 测试服部署点 bd19b79c8,git merge-base --is-ancestor e7cecb6d7 bd19b79c8 与 git merge-base --is-ancestor 970f9a187 bd19b79c8 均为真(本次逐条复核)。

修复行为的证据来源:源码 + 真库集成测试 ShareGroupOverlapProjectionIntegrationTest。

本文件明确不提供的东西:测试环境上的修复前/修复后对跑读数。修复 2026-09-19 已合入,测试环境此后一直是修复后状态,不存在可对跑的修复前环境。

网关侧(2026-09-22 实调):经网关 POST https://api.test.1814.love:9443/admin/fleet/group-dispatch/batches/1/share-groups → HTTP 200 + 业务码 600009(刻意用不存在的 groupBatchId=1,在第一道校验即失败关闭)。同轮阴性对照打在一个不存在的路径上 → code=404、接口不存在: POST ...,与业务码可分辨。

⇒ 由此坐实三项:路由通(/admin/fleet/** → lb://hl-fleet-service,无 /v3 前缀;带 /v3 的变体返回 404)、鉴权通(需切换到 VEHICLE_MANAGER 角色,权限点 fleet:group-dispatch:write)、请求体反序列化通(请求驱动到了 confirm() 内部的业务校验,而非停在 @Valid 阶段)。

⚠️ 这次实调的覆盖边界:它验的是端点可达性与契约反序列化,没有验证本次修复的行为本身(同批多个待准入成员不再误报 605001)——那需要真实的团期与派单夹具。修复行为的证据来源见上一条。该请求全程未触达写库路径:confirm() 里抛 600009 的 assertServiceDateInWindow 位于唯一写库入口 confirmInTransaction 之前,且其 javadoc 明确「基线是一次同步 Feign,刻意留在事务外拉」。

十、相关文档

关联 / 联系人

链接