车队保存无供应商时自动停用与在役车辆守卫(#6811)
changelog-filename-gate / validate (push) Successful in 2s

这个提交包含在:
API Changelog Bot
2026-08-31 10:46:03 +08:00
父节点 787b9a007b
当前提交 e28f675c8f
@@ -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<FleetTeamRespVO>`
#### 使用场景
车务 → 车队管理 → 列表行「编辑」→ 弹窗改车队名称/类型/负责人/电话/付款方式/排序/备注/供应商后点「保存」。本次仅状态副作用与错误码变化,路径、请求字段、字段类型与必填性都不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| 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<FleetTeamRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| 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