docs: 车队关联供应商并展示供应商全名(#6717) #83

已合并
wx 于 2026-08-30 17:54:25 +08:00 将 1 次代码提交从 docs/6717-fleet-team-supplier合并至 main
@@ -0,0 +1,805 @@
---
schema: "hl-changelog/v2"
ticket: "6717"
title: "车队关联供应商并展示供应商全名"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: "v2.1"
verified_at: "2026-08-30"
status_note: "PR #6753 已合并 dev-v3(merge commit 447b92f5);Deploy Panel 任务 2fe21f84(hl-fleet-service,2026-08-30 17:20)与 f29a293f(hl-resource-service,2026-08-30 17:22)均已部署测试服成功。真实 TEST 身份已验证 5 条负向链路全绿 + 列表字段结构正确,测试数据已清理。车队列表操作列的「供应商」按钮需下线,改为在车队新增/编辑弹窗选择供应商。"
updated_at: "2026-08-30"
base: "dev-v3"
---
# 车队管理:车队关联供应商并展示供应商全名
> **存放目录**:
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/2026-08/`
>
> **服务**: hl-fleet-service(端口 8094)/ hl-resource-service(内部依赖)
> **PR**: #6753
> **Issue**: #6717
> **日期**: 2026-08-30
> **影响范围**: 管理后台「车辆管理 - 车队管理」列表/新增/编辑/启用
车队管理新增供应商归属字段(可空),供应商只能随车队新增/编辑一起提交,不再提供单独配置入口。
---
## ⚠️ 关键变化
- 车队列表操作列的「供应商」按钮(`views/fleet/teams/index.vue` 弹窗 `SupplierResourceRelModal`)**需要下线**;供应商关联改为在车队新增/编辑弹窗里选择。
- 「不选供应商不能启动,只能停用」:未关联供应商的车队调用启用接口会被 601108 拦截;新建时不选供应商则车队落停用态。
- 已有订单(名下车辆存在非取消派单)的车队,**不允许更换或清除供应商**;允许首次绑定。
---
## 一、背景
车队此前没有供应商归属字段;供应商域(`supplier_main`,类型字典 `supplier_type`)已支持车队类型供应商(FLEET),但车队主数据与供应商档案之间未建立关联。工单 #6717 要求车队管理列表展示供应商全名,并在车队新增/编辑时明确供应商归属。
**最终口径(2026-08-30 用户确认,覆盖工单原文「必选」语义)**:
1. 供应商字段**不必填**(可选);不选 → 不能启用,只能停用。
2. 车队已有订单时,不允许**更换**或**清除**供应商(含已有订单的历史车队豁免存量,不动存量数据)。
3. 去掉单独配置供应商接口入口:前端车队列表的「供应商」按钮下线;供应商关联只随车队新增/编辑一起提交。
4. 供应商全名以**写时快照**存于 `fleet_team.supplier_name`;列表/详情零 Feign 读,供应商改名后陈旧、重新编辑可刷新。
5. 供应商资格校验走跨服务 Feign(fleet → resource `GET /internal/supplier/{supplierId}/fleet-eligibility`):主体存在 + 状态 ACTIVE + 类型关联含 FLEET。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 分页查询车队 | GET | `/admin/fleet/teams/page` | 响应新增字段 | `records[]` 增加 `supplierId`(String/null) + `supplierName`(String/null) |
| 2 | 查询车队详情 | GET | `/admin/fleet/teams/{fleetTeamId}` | 响应新增字段 | 同 #1 |
| 3 | 新增车队 | POST | `/admin/fleet/teams` | 请求体新增可选字段 + 响应新增字段 | 请求体加 `supplierId`(Long 字符串/可空);响应同 #2 |
| 4 | 编辑车队 | PUT | `/admin/fleet/teams/{fleetTeamId}` | 请求体新增可选字段 + 响应新增字段 + 失败语义 | 请求体加 `supplierId`;更换/清除时已有订单被 601111 拦截;清除且当前 ACTIVE 会强制落 DISABLED |
| 5 | 启用车队 | POST | `/admin/fleet/teams/{fleetTeamId}/enable` | 失败语义新增 | `supplierId==null` 时抛 601108「车队未关联供应商,不能启用」 |
---
## 三、接口详情
### 1. 分页查询车队 `GET /admin/fleet/teams/page`
**VO**: `FleetTeamPageReqVO` / `Result<PageResult<FleetTeamRespVO>>`
#### 使用场景
管理后台「车辆管理 - 车队管理」列表页加载时调用。供应商列新增展示。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `page` | Query | Integer | 否 | ≥1,默认 1 | 页码 |
| `pageSize` | Query | Integer | 否 | 1..100,默认 10 | 每页大小 |
| `keyword` | Query | String | 否 | ≤64 | 车队名称/负责人姓名模糊搜索 |
| `teamType` | Query | String | 否 | `SELF_OPERATED`/`COOPERATIVE` | 车队类型筛选 |
| `status` | Query | String | 否 | `ACTIVE`/`DISABLED` | 状态筛选 |
| `settleType` | Query | String | 否 | `cash`/`sign`/`company` | 付款方式筛选 |
#### 出参 `Result<PageResult<FleetTeamRespVO>>`
`records[]` 每一项 `FleetTeamRespVO` 字段(与改前相比只多两列,其余字段语义不变):
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 车队 ID(雪花序列化为字符串,禁止转 Number) |
| `teamCode` | String | 内部稳定编码(`ft_xxx`) |
| `teamName` | String | 车队名称 |
| `teamType` | String | `SELF_OPERATED`/`COOPERATIVE` |
| `leaderName` | String | 负责人姓名 |
| `leaderPhone` | String | 负责人电话(**分页脱敏**:`138****8000`) |
| `settleType` | String | `cash`/`sign`/`company` |
| `status` | String | `ACTIVE`/`DISABLED` |
| `sortOrder` | Integer | 排序值 |
| `remark` | String | 备注 |
| `vehicleCount` | Integer | 名下车辆总数(含停用) |
| `activeVehicleCount` | Integer | 名下在役车辆数 |
| **`supplierId`** | **String/null** | **关联供应商 ID(雪花字符串;未关联为 null)** |
| **`supplierName`** | **String/null** | **关联供应商全称快照(写时同步,未关联为 null)** |
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
| `updateTime` | String | 最后更新时间 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```http
GET /admin/fleet/teams/page?page=1&pageSize=10&status=ACTIVE
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"total": 2,
"page": 1,
"pageSize": 10,
"records": [
{
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "138****0001",
"settleType": "cash",
"status": "ACTIVE",
"sortOrder": 10,
"remark": "由 fleet_attribution/历史业务数据迁移",
"vehicleCount": 12,
"activeVehicleCount": 9,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-06-01 10:00:00",
"updateTime": "2026-08-30 15:30:00"
},
{
"fleetTeamId": "2102345678901234568",
"teamCode": "ft_2x9k3n",
"teamName": "自有车队",
"teamType": "SELF_OPERATED",
"leaderName": "自有负责人",
"leaderPhone": "139****0002",
"settleType": "company",
"status": "DISABLED",
"sortOrder": 20,
"remark": "",
"vehicleCount": 0,
"activeVehicleCount": 0,
"supplierId": null,
"supplierName": null,
"createTime": "2026-08-01 09:00:00",
"updateTime": "2026-08-30 15:30:00"
}
]
}
}
```
#### 空数据 / 降级响应
无车队时 `records=[]` `total=0`,`code=200`。供应商字段未关联时为 `null`(不是空串),前端按未关联渲染。
#### 错误响应
分页查询无业务错误分支。网关/框架错误:
```json
{ "code": 401, "message": "未登录", "success": false, "data": null }
```
#### 业务边界
- 只读查询;返回的 `supplierId`/`supplierName` 来自 `fleet_team.supplier_id/supplier_name` 列快照,不做实时跨服务取数。
- 供应商改名后列表展示旧名(快照口径);如需最新名称,让用户重新编辑车队保存触发刷新。
- 车队停用车队(`status=DISABLED`)也可被列表查到(不带状态筛选时默认返回全部状态)。
---
### 2. 查询车队详情 `GET /admin/fleet/teams/{fleetTeamId}`
**VO**: `Result<FleetTeamRespVO>`
#### 使用场景
管理后台编辑车队弹窗初始化时调用,回填 `supplierId`/`supplierName` 到供应商选择器。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(雪花,不得转 Number) |
#### 出参 `Result<FleetTeamRespVO>`
字段与 #1 `records[]` 项结构一致(含新增 `supplierId`/`supplierName`):
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 车队 ID(雪花序列化为字符串,禁止转 Number) |
| `teamCode` | String | 内部稳定编码(`ft_xxx`) |
| `teamName` | String | 车队名称 |
| `teamType` | String | `SELF_OPERATED`/`COOPERATIVE` |
| `leaderName` | String | 负责人姓名 |
| `leaderPhone` | String | 负责人电话(**详情返回原值**,编辑场景回填用) |
| `settleType` | String | `cash`/`sign`/`company` |
| `status` | String | `ACTIVE`/`DISABLED` |
| `sortOrder` | Integer | 排序值 |
| `remark` | String | 备注 |
| `vehicleCount` | Integer | 名下车辆总数(含停用) |
| `activeVehicleCount` | Integer | 名下在役车辆数 |
| `supplierId` | String/null | 关联供应商 ID(雪花字符串;未关联为 null) |
| `supplierName` | String/null | 关联供应商全称快照(写时同步) |
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
| `updateTime` | String | 最后更新时间 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```http
GET /admin/fleet/teams/2102345678901234567
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "13800000001",
"settleType": "cash",
"status": "ACTIVE",
"sortOrder": 10,
"remark": "由 fleet_attribution/历史业务数据迁移",
"vehicleCount": 12,
"activeVehicleCount": 9,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-06-01 10:00:00",
"updateTime": "2026-08-30 15:30:00"
}
}
```
#### 空数据 / 降级响应
车队不存在或已软删除(业务错误,见下方错误响应);无空数据分支。
#### 错误响应
车队不存在或已软删除:
```json
{ "code": 601100, "message": "车队不存在", "success": false, "data": null }
```
未登录(网关拦截):
```json
{ "code": 401, "message": "未登录", "success": false, "data": null }
```
#### 业务边界
- 详情接口返回 `leaderPhone` 为原值(编辑场景需要回填),分页接口脱敏。
- `supplierId` 必须按字符串处理,禁止转 JavaScript Number。
---
### 3. 新增车队 `POST /admin/fleet/teams`
**VO**: `FleetTeamSaveReqVO` / `Result<FleetTeamRespVO>`
#### 使用场景
管理后台「车辆管理 - 车队管理」点击「新增车队」,在表单里可选填供应商。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `teamName` | Body | String | 是 | ≤64,唯一 | 车队名称 |
| `teamType` | Body | String | 是 | `SELF_OPERATED`/`COOPERATIVE` | 车队类型 |
| `leaderName` | Body | String | 是 | ≤64 | 负责人姓名 |
| `leaderPhone` | Body | String | 是 | 电话格式正则 | 负责人电话 |
| `settleType` | Body | String | 是 | `cash`/`sign`/`company` | 付款方式 |
| `sortOrder` | Body | Integer | 是 | ≥0 | 排序 |
| `remark` | Body | String | 否 | ≤256 | 备注 |
| **`supplierId`** | Body | **String(Long)/null** | **否** | **`@Positive`** | **关联供应商 ID(雪花字符串;不填=不选供应商,新建车队落 DISABLED)** |
#### 出参 `Result<FleetTeamRespVO>`
响应结构同 #2;`supplierId`/`supplierName` 按请求回填(选了供应商且校验通过则写入快照,否则为 null)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 新车队 ID(雪花序列化为字符串) |
| `supplierId` | String/null | 按请求回填(选了且校验通过) |
| `supplierName` | String/null | 写时同步的供应商全称快照;未选为 null |
| `status` | String | 选供应商=`ACTIVE`;不选=`DISABLED` |
| `createTime` | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
| `updateTime` | String | 创建时间(与 createTime 相同) |
| (其余字段) | - | 与详情 #2 同构 |
#### 请求示例
```http
POST /admin/fleet/teams
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"teamName": "新合作车队B",
"teamType": "COOPERATIVE",
"leaderName": "王队长",
"leaderPhone": "13800138000",
"settleType": "sign",
"sortOrder": 20,
"remark": "新签约车队",
"supplierId": "2091381911266967553"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2103456789012345678",
"teamCode": "ft_3a1b2c",
"teamName": "新合作车队B",
"teamType": "COOPERATIVE",
"leaderName": "王队长",
"leaderPhone": "13800138000",
"settleType": "sign",
"status": "ACTIVE",
"sortOrder": 20,
"remark": "新签约车队",
"vehicleCount": 0,
"activeVehicleCount": 0,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-08-30 16:00:00",
"updateTime": "2026-08-30 16:00:00"
}
}
```
#### 空数据 / 降级响应
未选供应商创建(落 DISABLED,供应商字段为 null):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2103456789012345679",
"teamCode": "ft_3a1b2d",
"teamName": "临时车队",
"teamType": "COOPERATIVE",
"leaderName": "临时负责人",
"leaderPhone": "13900139000",
"settleType": "cash",
"status": "DISABLED",
"sortOrder": 30,
"remark": "",
"vehicleCount": 0,
"activeVehicleCount": 0,
"supplierId": null,
"supplierName": null,
"createTime": "2026-08-30 16:00:00",
"updateTime": "2026-08-30 16:00:00"
}
}
```
#### 错误响应
供应商不存在/未生效/不含车队类型(不写库):
```json
{ "code": 601109, "message": "供应商不存在、未生效或不包含车队类型", "success": false, "data": null }
```
供应商校验依赖故障(Feign 不可用,不写库):
```json
{ "code": 601110, "message": "暂时无法校验供应商,请稍后重试", "success": false, "data": null }
```
车队名称重复(`uk_fleet_team_name` 唯一索引兜底):
```json
{ "code": 601101, "message": "车队名称已存在", "success": false, "data": null }
```
Bean Validation 校验失败:
```json
{ "code": 400, "message": "供应商ID必须为正数", "success": false, "data": null }
```
#### 业务边界
- 幂等:同一 teamName + 请求摘要 10 秒窗口内重复提交只生效一次(@Idempotent key=`fleet:team:create:{teamName}`)。
- 分布式锁:同 teamName 创建串行化(@Lock4j)。
- 选供应商时先 Feign 校验(事务外),通过后写库(事务内),Feign 失败/不合格一律不写库(失败关闭)。
- 不写供应商时新建车队 status=DISABLED,需后续编辑绑定供应商后才能启用。
---
### 4. 编辑车队 `PUT /admin/fleet/teams/{fleetTeamId}`
**VO**: `FleetTeamSaveReqVO` / `Result<FleetTeamRespVO>`
#### 使用场景
管理后台编辑车队弹窗提交。可修改基础字段 + 供应商;**不允许修改 status**(启停用走独立接口)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(不得转 Number) |
| (其余字段) | Body | String | 同新增 | 同新增 | 全字段提交(SaveReqVO 整体语义) |
| `supplierId` | Body | String(Long)/null | 否 | `@Positive` | 供应商 ID;更换/清除受订单围栏 |
#### 出参 `Result<FleetTeamRespVO>`
同 #2;`supplierId`/`supplierName` 反映最新写入值。
| 字段 | 类型 | 说明 |
|------|------|------|
| `fleetTeamId` | String | 车队 ID(路径回显) |
| `supplierId` | String/null | 最新写入值(更换/清除后刷新) |
| `supplierName` | String/null | 最新写入快照(更换时取新供应商 full_name;清除时为 null) |
| `status` | String | 清除供应商且当前 ACTIVE 时强制落 `DISABLED`;其余场景不变 |
| `updateTime` | String | 本次写入时间(秒级严格递增) |
| (其余字段) | - | 与详情 #2 同构 |
#### 请求示例
```http
PUT /admin/fleet/teams/2102345678901234567
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "13800000001",
"settleType": "cash",
"sortOrder": 10,
"remark": "",
"supplierId": "2091381911266967553"
}
```
#### 响应示例
成功(更换供应商且无订单):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"leaderName": "测试负责人甲",
"leaderPhone": "13800000001",
"settleType": "cash",
"status": "ACTIVE",
"sortOrder": 10,
"remark": "",
"vehicleCount": 12,
"activeVehicleCount": 9,
"supplierId": "2091381911266967553",
"supplierName": "内蒙古呼籁旅游服务有限公司",
"createTime": "2026-06-01 10:00:00",
"updateTime": "2026-08-30 16:30:00"
}
}
```
#### 空数据 / 降级响应
清除供应商(原值 → null)且车队无订单:快照清空,若当前 ACTIVE 强制落 DISABLED(保持不变量:ACTIVE ⇒ 已绑供应商)。
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"fleetTeamId": "2102345678901234567",
"teamCode": "ft_2x9k3m",
"teamName": "合作车队A",
"teamType": "COOPERATIVE",
"status": "DISABLED",
"supplierId": null,
"supplierName": null,
"updateTime": "2026-08-30 16:30:00"
}
}
```
#### 错误响应
车队已有订单时更换/清除供应商(含从原供应商换到新供应商、从原供应商清为空):
```json
{ "code": 601111, "message": "车队已关联订单,不能更换供应商", "success": false, "data": null }
```
供应商校验失败(同新增 #3 的 601109/601110);车队不存在:
```json
{ "code": 601100, "message": "车队不存在", "success": false, "data": null }
```
#### 业务边界
- 幂等:同 fleetTeamId + 请求摘要(含 supplierId 参与摘要)10 秒窗口内重复提交只生效一次(@Idempotent key=`fleet:team:update:{fleetTeamId}:{sha256}`)。
- 分布式锁:同 fleetTeamId 编辑串行化(@Lock4j)。
- 更换供应商校验顺序:订单围栏(601111)→ Feign 资格校验(601109/601110)→ 写库。
- 供应商不变时(含 null→null)不触发订单围栏,也不调 Feign。
- 首次绑定(null → 新值)即使已有订单也允许。
- `status` 不在 SaveReqVO,本接口不修改启停状态;清除供应商且当前 ACTIVE 时强制落 DISABLED 是唯一例外。
---
### 5. 启用车队 `POST /admin/fleet/teams/{fleetTeamId}/enable`
**VO**: `Result<Void>`
#### 使用场景
管理后台对已停用车队点击「启用」。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID |
#### 出参 `Result<Void>`
成功时 `data=null`:
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 恒为 `200` 表示成功 |
| `data` | null | 启用接口无返回体 |
#### 空数据 / 降级响应
启用成功恒返回 `{"code":200,"success":true,"data":null}`;无空数据分支。已是启用态时幂等放行不重复写(返回同样结构)。
#### 请求示例
```http
POST /admin/fleet/teams/2102345678901234567/enable
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
#### 错误响应
未关联供应商(新增语义):
```json
{ "code": 601108, "message": "车队未关联供应商,不能启用", "success": false, "data": null }
```
已是启用态(幂等放行,不重复写):
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
车队不存在:`601100`。
#### 业务边界
- 幂等:同 fleetTeamId 重复调用只生效一次(@Idempotent key=`fleet:team:enable:{fleetTeamId}`)。
- 分布式锁:同 fleetTeamId 启停用串行化(@Lock4j key=`fleet:team:status:{fleetTeamId}`)。
- 存量迁移车队(own/coopA/coopB)当前 ACTIVE 且无供应商:enable 若已被置 DISABLED 后会被 601108 拦截,需先编辑绑定供应商再启用。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | 调用 / 结果 |
|---|---|
| ✅ 新增时不选供应商 | `{ "supplierId": null }`(或不传)→ 车队落 DISABLED,待绑定后启用 |
| ✅ 新增时选供应商 | `{ "supplierId": "2091381911266967553" }` → Feign 校验通过后落 ACTIVE |
| ✅ 编辑时供应商不变 | 原 supplierId 原样传回(或省略由后端按原值处理?——必须原样传,SaveReqVO 整份语义) |
| ✅ 首次绑定 | 原 supplierId=null,新 supplierId=有效 FLEET 供应商 → 允许(不受订单围栏) |
| ✅ 供应商改名后刷新快照 | 重新编辑车队并保存(supplierId 原样),快照重新取最新 full_name |
| ❌ 供应商 ID 转 Number | 雪花精度丢失;必须按字符串传输 |
| ❌ 已有订单车队换供应商 | 601111,不写库 |
| ❌ 已有订单车队清供应商 | 601111,不写库 |
| ❌ 清除供应商后期望仍 ACTIVE | 无订单时快照清空+强制 DISABLED;前端收到 200 但 status=DISABLED 属于预期行为 |
### 关键提示(当前 TEST 构建)
- `supplierId`/`supplierName` 在所有响应中均按字符串序列化(`@JsonSerialize(ToStringSerializer)`);**禁止**前端用 `Number()`/`parseInt()`/一元 `+` 转换。
- 供应商全名快照为**写时取数**:`supplier_main.full_name` 改名后列表展示旧值,重新编辑车队保存触发刷新。
- 供应商选择器数据源:可复用 `GET /admin/supplier/items/list?typeCode=FLEET&status=ACTIVE`(mmg 自查前端是否已有该接口封装;无则后端再补)。
---
## 五、数据库行为
- **写操作只影响 `fleet_team` 一行**(新增 insert / 编辑 update / 启停用 update),不跨服务写 `supplier_resource_rel` 或 `supplier_main`。
- 快照列:`supplier_id`(关联 ID)+ `supplier_name`(全称快照)同列写入;清除时同列置 NULL。
- 幂等窗口内重复请求只写一次;锁键串行化同车队写。
- 迁移:`V20260830_001__add_supplier_to_fleet_team.sql` 对 `fleet_team` 加 `supplier_id BIGINT NULL` + `supplier_name VARCHAR(500) NULL` + 索引 `idx_fleet_team_supplier(supplier_id)`;存量车队两列均为 NULL。
- 不动 `supplier_resource_rel`(车队供应商不走资源关系表;关系表仅用于九大资源模块)。
---
## 六、边界行为
- 未登录/登录失效:业务码 `401`(网关拦截)。
- 车队不存在:`601100`。
- 车队名称重复:`601101`(含 DuplicateKeyException 翻译)。
- 车队已停用仍选该车:`601102`(既有口径,本工单不改)。
- 车队下有在役车辆时禁止停用:`601103`(既有口径)。
- 车队已关联车辆时禁止改自有/合作类型:`601104`(既有口径)。
- 车队名下仍有车辆/司机时禁止删除:`601107`(既有口径)。
- 车队未关联供应商禁止启用:`601108`(新增)。
- 供应商不存在/未生效/不含车队类型:`601109`(新增)。
- 供应商校验依赖故障:`601110`(新增,失败关闭不写库)。
- 车队已有订单禁止更换/清除供应商:`601111`(新增)。
- Bean Validation 校验失败:`400`。
- 跨服务 Feign 不可用:写接口一律失败关闭(601110);读接口(列表/详情)读快照列,不受影响。
---
## 六.5、枚举 / 数据字典
### `status`(车队启停状态)
**所属字段**: `FleetTeamRespVO.status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `ACTIVE` | 启用 | 可被车辆选择;**前提:已关联供应商** |
| `DISABLED` | 停用 | 不可被车辆选择;新建未选供应商时默认落此态 |
### `teamType`(车队类型)
**所属字段**: `FleetTeamSaveReqVO.teamType` / `FleetTeamRespVO.teamType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `SELF_OPERATED` | 自有 | 自有车队 |
| `COOPERATIVE` | 合作 | 合作车队 |
### `settleType`(付款方式,字典 `resource_settle_type`)
**所属字段**: `FleetTeamSaveReqVO.settleType` / `FleetTeamRespVO.settleType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `cash` | 现付 | - |
| `sign` | 挂账签单 | - |
| `company` | 公司月结 | - |
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `FleetTeamSaveReqVO.supplierId` | 无 | 新增可选字段,@Positive,参与幂等摘要 |
| `FleetTeamRespVO.supplierId` | 无 | 新增,String/null(ToStringSerializer) |
| `FleetTeamRespVO.supplierName` | 无 | 新增,String/null(快照) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 新建车队默认状态 | ACTIVE | 选供应商=ACTIVE;不选=DISABLED |
| 启用校验 | 只查当前状态 | 增加 supplierId==null → 601108 |
| 编辑车队供应商 | 无此字段 | 有订单禁换/禁清(601111);清除+ACTIVE→强制 DISABLED |
| 供应商配置入口 | 前端有独立「供应商」按钮(调通用资源关系接口) | 下线;改为车队新增/编辑内嵌选择 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(新增可选字段;响应只多两列,前端旧版忽略即兼容)
- **前端是否必须同步上线**: 是(车队列表「供应商」按钮需下线,否则用户仍能从旧入口调通用接口——但通用接口对车队模块本工单起后端侧保留不拦,是前端入口下线)
- **前端 workaround 清理点**: 车队列表的「供应商」操作入口(`views/fleet/teams/index.vue` 中 `supplierRelaShow`/`supplierRelaRow`/`openSupplierRelation` 相关代码)整体删除
---
## 七、不影响范围
- **仅影响**: 管理后台「车辆管理 - 车队管理」列表/新增/编辑/启用 4 个端点 + 详情 1 个端点。
- **零影响**:
- 车辆档案(`fleet_vehicle`)/ 司机档案(`fleet_driver`)/ 派单(`fleet_assignment`)等车队下游域——它们继续经 `FleetTeamService.resolveForVehicle` 解析车队,供应商字段不影响车辆选车队。
- 供应商域九大资源模块(景区/餐厅/备品/组合/游玩项目/酒店/服务/额外成本/服务人员/车辆)的独立供应商关系维护(`/admin/supplier/resource-relations/{module}/{id}/update` 等通用接口保留不动)。
- 订单/对账/看板读路径(只读 fleet_team 既有字段,新增两列不影响)。
- Gateway 路由(`/admin/fleet/**` 通配已覆盖;`/internal/**` 不走网关)。
- Redis/MQ(无新增 key/消息)。
---
## 八、测试环境已验证
真实 TEST 环境实测(2026-08-30,网关 `https://api.test.1814.love:9443`,admin token 走 `/admin/auth/login`):
```
POST /admin/fleet/teams 无供应商新建 → 200, status=DISABLED, supplierId=null, supplierName=null ✓
POST /admin/fleet/teams/{id}/enable 无供应商启用 → 601108「车队未关联供应商,不能启用」 ✓
PUT /admin/fleet/teams/{id} 绑定 DRAFT 供应商 → 601109 ✓
PUT /admin/fleet/teams/{id} 绑定不存在供应商 → 601109 ✓
PUT /admin/fleet/teams/{id} 绑定非 FLEET 类型 ACTIVE 供应商(RESTAURANT) → 601109 ✓
GET /admin/fleet/teams/page → records[] 含 supplierId/supplierName 键(未关联为 null) ✓
```
- 部署:Deploy Panel 任务 `2fe21f84`(hl-fleet-service,2026-08-30 17:20 success)+ `f29a293f`(hl-resource-service,2026-08-30 17:22 success),预期/实际提交均为 `447b92f5143e9ca4381d590d4f868d518cf66e0a`(dev-v3 HEAD)。
- 正向链路(绑定合格 FLEET ACTIVE 供应商 → ACTIVE + 快照写 supplierName):测试服当前无 ACTIVE 状态的 FLEET 类型供应商(供应商审批链要求必备资质,`supplier_type_qualification_rule` 规则表为空,无法造出合格供应商);该路径本地单测已覆盖(`FleetTeamServiceTest#create_withEligibleSupplier_activeAndSnapshot` 等 24 用例全绿),建议 mmg 联调时在真实数据上补验。
- 有订单换供应商(601111)链路:测试服车队均无订单派单可安全构造验证数据,本地单测覆盖(`FleetTeamServiceTest#update_changeSupplierWithOrders_rejected` / `update_clearSupplierWithOrders_rejected`)。
- 本地自动化:fleet 全量 3883 项 0 失败 0 错误(含 FleetRedLineArchTest 13 项门禁);resource 全量 0 失败 0 错误;spotless:check 绿。
- 数据清理:测试车队(352385968453586944)与测试供应商(2093993745543299074)均已删除;未触碰同事真实订单;admin token 已登出。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #6753 | #6717 | 车队关联供应商并展示供应商全名(本次) | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#6717](https://git.1814.love:8443/wx/HL/issues/6717)
- 关联 PR: [wx/HL#6753](https://git.1814.love:8443/wx/HL/pulls/6753)
- 任务设计文档: `docs/tasks/6717-fleet-team-supplier.md`(worktree `D:\work2\HL-v3-0830-fleetsup`)
## 撤回
1. 管理端先恢复车队列表「供应商」操作入口(参照改前版本),保持线上可用。
2. 从最新 `dev-v3` 建独立回退分支,回退 PR #6753 的合并 commit(`72c9018ab` 及其后续如有),验证后经独立 PR 合入。
3. 仅需下线新供应商字段时可先保留两列(快照保留无副作用),只回退 Controller/Service 逻辑与 Feign 校验。
4. 使用 Deploy Panel 滚动部署 `hl-fleet-service` 与 `hl-resource-service`;数据库列保留不删(`supplier_id`/`supplier_name` 允许 NULL,回退后不影响)。
5. 撤回后经 Gateway 验证新增/编辑/启用接口按改前口径通过;车队列表的供应商列展示空白或下线列头。
## 关联 / 联系人
### 链接
- **Issue**: [#6717](https://git.1814.love:8443/wx/HL/issues/6717)
- **PR**: [#6753](https://git.1814.love:8443/wx/HL/pulls/6753)
- **Merge commit**: 待合并后回填
### 联系人
- **后端负责人**: @wx
- **前端联动**: @mmg(下线车队列表「供应商」按钮 + 车队新增/编辑弹窗加供应商选择器)