diff --git a/changelogs-v2/2026-08/31_6811_车队保存无供应商时自动停用与在役车辆守卫-修改接口-管理后台.md b/changelogs-v2/2026-08/31_6811_车队保存无供应商时自动停用与在役车辆守卫-修改接口-管理后台.md new file mode 100644 index 00000000..4178f6a7 --- /dev/null +++ b/changelogs-v2/2026-08/31_6811_车队保存无供应商时自动停用与在役车辆守卫-修改接口-管理后台.md @@ -0,0 +1,293 @@ +--- +schema: "hl-changelog/v2" +ticket: "6811" +title: "车队保存(无供应商)自动落停用;仍有在役车辆时以 601112 拒绝保存" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "2026-08-31" +status_note: "#6717 只在「本次保存清除供应商」时强制停用,存量无供应商车队原样再保存仍停留启用。本单把守卫改为按保存结果判定,并对齐 disable 的在役车辆不变量新增 601112。TEST 已实测两条分支。" +updated_at: "2026-08-31" +base: "dev-v3" +generated: "2026-08-31T10:45:00+08:00" +--- + +# 车队编辑保存:无供应商时自动落停用,仍有在役车辆则以 601112 拒绝 + +> **服务**: hl-fleet-service (端口 8087/8187) +> **PR**: #6827 +> **Issue**: #6811 +> **日期**: 2026-08-31 +> **影响范围**: 管理后台车务 → 车队管理 → 编辑车队保存 + +--- + +## ⚠️ 关键变化 + +- 保存车队时若**结果为无供应商**(`supplierId` 传 `null` 或不传),后端不再允许车队停留在「启用」:无在役车辆时自动把状态落为 `DISABLED`,前端保存成功后需按响应/列表刷新后的状态展示,不能沿用提交前的「启用」。 +- 上一版(#6717)只在「本次保存把已绑供应商清空」时才强制停用,存量无供应商车队原样再保存不变状态。**该行为已收口**:现在按保存结果判定,`null → null` 同样强制停用。 +- 新增错误码 **601112**:无供应商需自动停用但车队仍有在役车辆时,保存整体失败、零写入。 + +--- + +## 一、背景 + +`fleet_team` 的不变量是 **ACTIVE ⇒ 已绑供应商**。此前新建(无供应商落停用)和启用(无供应商 601108 拒绝)都有守卫,唯独编辑保存漏了「结果无供应商」这一路,导致管理后台车队列表出现「供应商列为 —(无供应商)+ 状态启用」的行,且反复保存也不收敛。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 修改车队 | PUT | `/admin/fleet/teams/{fleetTeamId}` | 状态副作用 + 新增错误码 | 保存结果无供应商时自动落停用;仍有在役车辆则 601112 拒绝 | + +--- + +## 三、接口详情 + +### 1. 修改车队 `PUT /admin/fleet/teams/{fleetTeamId}` + +**VO**: `FleetTeamSaveReqVO` → `Result` + +#### 使用场景 + +车务 → 车队管理 → 列表行「编辑」→ 弹窗改车队名称/类型/负责人/电话/付款方式/排序/备注/供应商后点「保存」。本次仅状态副作用与错误码变化,路径、请求字段、字段类型与必填性都不变。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| fleetTeamId | Path | String(雪花 ID) | ✅ | - | 车队 ID,字符串传递,禁止数值化 | +| teamName | Body | String | ✅ | 非空、全局唯一 | 车队名称 | +| teamType | Body | String | ✅ | `SELF_OPERATED` / `COOPERATIVE` | 已关联车辆时不可切换 | +| leaderName | Body | String | ✅ | 非空 | 负责人 | +| leaderPhone | Body | String | ✅ | 手机号 | 负责人电话 | +| settleType | Body | String | ✅ | `cash` / `sign` / `company` | 付款方式(资源付款方式字典) | +| sortOrder | Body | Integer | ✅ | ≥ 0 | 排序 | +| remark | Body | String | ❌ | ≤ 256 | 备注,空串按 null 落库 | +| supplierId | Body | String(雪花 ID) | ❌ | 供应商需生效且含 FLEET 类型 | **不传或传 null = 不选供应商**,触发本次自动停用规则 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| fleetTeamId | String | 车队 ID | +| teamName | String | 车队名称 | +| teamType | String | `SELF_OPERATED` / `COOPERATIVE` | +| status | String | **`ACTIVE` / `DISABLED`;保存结果无供应商时返回 `DISABLED`** | +| settleType | String | 付款方式 | +| sortOrder | Integer | 排序 | +| supplierId | String / null | 关联供应商 ID,null = 未关联 | +| supplierName | String / null | 供应商全称快照,供应商为 null 时同步清空 | +| vehicleCount | Number | 名下车辆总数 | +| activeVehicleCount | Number | 在役车辆数 | + +#### 请求示例 + +无供应商的车队原样保存(触发自动停用): + +```json +{ + "teamName": "测试车队P4-1787131721", + "teamType": "COOPERATIVE", + "leaderName": "测试负责人", + "leaderPhone": "13900000001", + "settleType": "cash", + "sortOrder": 99, + "remark": "由 fleet_attribution/历史业务数据迁移,负责人待完善", + "supplierId": null +} +``` + +#### 响应示例 + +无供应商 + 无在役车辆 → 200,且 `status` 已变 `DISABLED`: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "fleetTeamId": "348397854705979392", + "teamName": "测试车队P4-1787131721", + "teamType": "COOPERATIVE", + "status": "DISABLED", + "settleType": "cash", + "sortOrder": 99, + "supplierId": null, + "supplierName": null, + "vehicleCount": 0, + "activeVehicleCount": 0 + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +本接口为写接口,不返回空集。供应商资格校验依赖(resource 侧)不可用时**失败关闭**,不降级放行: + +```json +{ + "code": 601110, + "message": "暂时无法校验供应商,请稍后重试", + "success": false, + "data": null +} +``` + +#### 错误响应 + +新增(本次)——无供应商需自动停用但仍有在役车辆: + +```json +{ + "code": 601112, + "message": "车队未关联供应商需自动停用,但仍有在役车辆,请先关联供应商或转移在役车辆", + "success": false, + "data": null +} +``` + +本接口其他错误码(本次未变,供自包含联调): + +| code | message | 触发条件 | +|------|---------|----------| +| 601100 | 车队不存在 | `fleetTeamId` 无对应车队 | +| 601101 | 车队名称已存在 | `teamName` 与其他车队重名 | +| 601104 | 车队已关联车辆,不能修改自有/合作类型 | 名下有车辆时改 `teamType` | +| 601106 | 付款方式不是有效的资源付款方式 | `settleType` 不在 `cash`/`sign`/`company` | +| 601109 | 供应商不存在、未生效或不包含车队类型 | 绑定的 `supplierId` 不合格 | +| 601110 | 暂时无法校验供应商,请稍后重试 | 供应商资格校验依赖不可用 | +| 601111 | 车队已关联订单,不能更换供应商 | 已绑供应商的车队换绑/清除且名下车辆有非取消派单 | + +#### 业务边界 + +- 鉴权:需登录且具备车务车队管理权限;未登录网关返 401。 +- **状态不由前端入参驱动**:请求体没有 `status` 字段,启停仍由 `/disable`、`/enable` 切;唯一例外是本次的自动停用副作用。 +- 供应商未变时不调供应商资格校验,也不查订单围栏;仅当 `supplierId` 发生变化才校验。 +- 已绑供应商的车队换绑或清除时,名下车辆若有非取消派单一律 601111(该守卫先于 601112 判定)。 +- 失败零写入:601112、601111、601109、601110 均在写库前抛出,名称、排序、备注等字段都不会部分保存。 +- 并发:同一 `fleetTeamId` 的保存互斥(分布式锁),重复提交不会产生中间态。 +- 已停用且无供应商的车队保存时保持停用,不报错。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误 payload 对照 + +| 场景 | payload 与结果 | +|------|---------------| +| ✅ 绑定合法供应商保存 | `{"supplierId": "2094238127517319170", ...}` → 200,`status` 保持原值 | +| ✅ 无供应商 + 无在役车辆 | `{"supplierId": null, ...}` → 200,`status=DISABLED` | +| ✅ 已停用 + 无供应商 | `{"supplierId": null, ...}` → 200,`status` 保持 `DISABLED` | +| ❌ 无供应商 + 有在役车辆 | `{"supplierId": null, ...}` → 601112,零写入 | +| ❌ 靠不传 `supplierId` 保留原供应商 | 不传等同传 `null`,会被当成「清除供应商」 | + +### 切换状态时的必要动作 + +- 编辑回显后提交必须**原样回传详情里的 `supplierId`**;字段缺失就是清除语义,不存在「不传 = 不改」。 +- 保存成功后不能沿用提交前的状态展示:用响应体 `data.status` 或重拉列表/详情。 +- 收到 601112 时弹后端 message 即可;修复路径是「选一个合法供应商」或「先把在役车辆转走/停用」。 + +--- + +## 五、数据库行为 + +| 前端提交 | 保存后外部可观察结果 | +|----------|--------------------| +| `supplierId` 合法值 | 车队关联该供应商,`supplierName` 刷为当前全称快照,状态不变 | +| `supplierId=null`,无在役车辆 | 供应商关联与全称快照同时置空,状态变 `DISABLED` | +| `supplierId=null`,有在役车辆 | 零写入(名称/排序/备注等也不保存) | + +本次无表结构变更、无 Flyway 迁移,存量脏数据不做批量刷状态(下次编辑保存时自然收口)。 + +--- + +## 六、边界行为 + +- 未登录 → 401(网关拦截)。 +- 车队不存在 → 601100。 +- 供应商资格依赖降级 → 601110 失败关闭,不静默放行。 +- 存量无供应商且仍启用的车队:不会被后台任务批量改状态,只在下次编辑保存时收口。 +- 已绑供应商的车队保存行为与之前一致,无新增拦截。 +- 供应商改名后快照陈旧:仍需重新编辑车队刷新(行为未变)。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| 请求体字段 | 无变化 | 无变化(路径、字段名、类型、必填性全部不变) | +| 响应 `data.status` | 仅当本次清除已绑供应商时可能返回 `DISABLED` | **只要保存结果无供应商且原为启用,就返回 `DISABLED`** | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 存量无供应商车队(`null → null`)原样保存 | 保持「启用」 | 无在役车辆 → 自动「停用」;有在役车辆 → 601112 | +| 清除已绑供应商(`X → null`) | 启用→停用(不查在役车辆) | 有在役车辆时改为 601112 拒绝,不再产出「停用车队挂在役车辆」脏态 | +| 绑定/换绑合法供应商 | 200,状态不变 | 不变 | + +## 六.7、影响评估 + +- **是否破坏向后兼容**:否(请求/响应结构不变,新增一个错误码与一个状态副作用)。 +- **前端是否必须同步上线**:否。旧前端不报错,但若保存后不刷新列表,页面会短暂显示陈旧的「启用」。 +- **前端 workaround 清理点**:若前端曾为「无供应商仍显示启用」做过本地推断或提示,可改为直读后端 `status`。 + +--- + +## 七、不影响范围 + +- **仅影响**:管理后台车队编辑保存(`PUT /admin/fleet/teams/{fleetTeamId}`)。 +- **零影响**: + - 新建车队 `POST /admin/fleet/teams`(无供应商本来就落停用) + - 启用/停用 `POST /admin/fleet/teams/{id}/enable`、`/disable` + - 车队列表、详情、options 读接口字段 + - 车辆、司机、派单、看板、对账等其他车务接口 + - 存量数据(不批量刷状态) + +--- + +## 八、测试环境已验证 + +部署:`dev-v3` 合入 PR #6827(merge `740fdb834`)后部署 hl-fleet-service(8087/8187 滚动,两实例健康)。 + +``` +PUT /admin/fleet/teams/348397854705979392 (无供应商 + 启用 + activeVehicleCount=0) + → 200 成功,保存后 GET 详情 status=DISABLED, supplierId=null ✓ + +PUT /admin/fleet/teams/348398422098841600 (无供应商 + 启用 + activeVehicleCount=1) + → 601112 「车队未关联供应商需自动停用,但仍有在役车辆…」 + 保存后 GET 详情 status=ACTIVE(零写入) ✓ + +PUT /admin/fleet/teams/2026072200010000001 (已绑供应商 + 有订单, 清除供应商) + → 601111 既有订单围栏优先,行为未变 ✓ +``` + +验证用车队:`348397854705979392`(测试车队P4-1787131721)、`348398422098841600`(测试车队P4-1787131856,验证用临时车辆已删除并回到 0 车)。 + +单测与构建:`mvn -pl hl-fleet-service -am verify` → Tests run 3887 / Failures 0 / Errors 0(Skipped 5 为既有 Release-E 跳过项);`mvn -pl hl-fleet-service spotless:check` → 799 文件 0 违规。 + +--- + +## 十、相关文档 + +- 前置契约:`changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md` +- 本次 PR:[#6827](https://git.1814.love:8443/wx/HL/pulls/6827) + +## 关联 / 联系人 + +- Issue:[#6811](https://git.1814.love:8443/wx/HL/issues/6811) +- 后端联系人:@wx