diff --git a/changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md b/changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md new file mode 100644 index 00000000..38a3cc24 --- /dev/null +++ b/changelogs-v2/2026-08/30_6717_车队关联供应商并展示供应商全名-修改接口-管理后台.md @@ -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>` + +#### 使用场景 + +管理后台「车辆管理 - 车队管理」列表页加载时调用。供应商列新增展示。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `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>` + +`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 +``` + +#### 响应示例 + +```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` + +#### 使用场景 + +管理后台编辑车队弹窗初始化时调用,回填 `supplierId`/`supplierName` 到供应商选择器。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(雪花,不得转 Number) | + +#### 出参 `Result` + +字段与 #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 +``` + +#### 响应示例 + +```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` + +#### 使用场景 + +管理后台「车辆管理 - 车队管理」点击「新增车队」,在表单里可选填供应商。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `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` + +响应结构同 #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 +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` + +#### 使用场景 + +管理后台编辑车队弹窗提交。可修改基础字段 + 供应商;**不允许修改 status**(启停用走独立接口)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID(不得转 Number) | +| (其余字段) | Body | String | 同新增 | 同新增 | 全字段提交(SaveReqVO 整体语义) | +| `supplierId` | Body | String(Long)/null | 否 | `@Positive` | 供应商 ID;更换/清除受订单围栏 | + +#### 出参 `Result` + +同 #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 +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` + +#### 使用场景 + +管理后台对已停用车队点击「启用」。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `fleetTeamId` | Path | String(Long) | 是 | 正整数 ID 字符串 | 目标车队 ID | + +#### 出参 `Result` + +成功时 `data=null`: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | Integer | 恒为 `200` 表示成功 | +| `data` | null | 启用接口无返回体 | + +#### 空数据 / 降级响应 + +启用成功恒返回 `{"code":200,"success":true,"data":null}`;无空数据分支。已是启用态时幂等放行不重复写(返回同样结构)。 + +#### 请求示例 + +```http +POST /admin/fleet/teams/2102345678901234567/enable +Authorization: Bearer +``` + +#### 响应示例 + +```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(下线车队列表「供应商」按钮 + 车队新增/编辑弹窗加供应商选择器)