# 【修改接口·管理后台】车辆档案 6 接口（全模块首次推送 changelog）

> **PR**: 无（本次为首次推送，实质接口契约未变更，配合 §1 §3 模块 changelog 一并补齐 fleet 全档案契约）
> **服务**: hl-fleet-service | **更新时间**: 2026-05-21
>
> **存放目录**: `changelogs-v2/2026-05/`
> **影响范围**: 管理后台「车管 / 车辆档案」页（列表 + 详情 + 新增 + 编辑 + 软删 + Excel 批量导入）

---

## ⚠️ 关键说明

车辆档案模块 6 接口自 fleet 服务首次落地（PR #2724）以来契约**未变化**，本次仅作**首次完整 changelog 推送**，让前端 v3 项目仓库一次拿全 6 接口契约。

**与车型管理库（§1 changelog）配套**：车辆档案的 `vehicleModelId` 引用车型管理库的型号 ID，前端可在新增车辆页用 §1.5 / §1.7 拉型号下拉。

---

## 1. 接口背景

车辆档案是车队所有车辆的主数据：车牌 / 车型 / VIN / 行驶证 / 保险 / 年检 / 多类目附件（外观/内饰/保险/年检/营运证/其他 6 类）。

前端"车管 / 车辆档案"页面提供：
- 列表筛选（按车牌、车队、车型大类、状态、保险/年检到期日）
- 详情查看（含附件按类目分组）
- 新增 / 编辑（含附件 diff 语义批量提交）
- 软删（业务流水模块上线后会按"未完成派单"拒删）
- Excel 批量导入

---

## 2. 变更清单

| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 1 | GET | `/admin/fleet/vehicles/page` | 首推 | 分页列表（多条件过滤） |
| 2 | GET | `/admin/fleet/vehicles/{vehicleId}` | 首推 | 详情（含附件） |
| 3 | POST | `/admin/fleet/vehicles` | 首推 | 新增（含附件批量 INSERT） |
| 4 | PUT | `/admin/fleet/vehicles/{vehicleId}` | 首推 | 编辑（附件 diff 语义） |
| 5 | DELETE | `/admin/fleet/vehicles/{vehicleId}` | 首推 | 软删 |
| 6 | POST | `/admin/fleet/vehicles/import` | 首推 | Excel 批量导入 |

---

## 3. 接口详情

### 3.1 分页查询

**GET** `/admin/fleet/vehicles/page`

- **使用场景**：车管车辆档案列表页
- **认证**：管理后台 JWT
- **幂等**：是（只读）

**Query 入参**：

| 字段 | 类型 | 必填 | 默认 | 校验 | 说明 |
|---|---|---|---|---|---|
| `page` | int | ❌ | 1 | `>= 1` | 页码 |
| `pageSize` | int | ❌ | 20 | `1-100` | 每页条数 |
| `keyword` | string | ❌ | — | — | 车牌模糊匹配 |
| `fleet` | string | ❌ | — | `own/coopA/coopB` | 车队精确过滤 |
| `typeKey` | string | ❌ | — | — | 车型大类 key 精确（如 suv） |
| `vehicleStatus` | string | ❌ | — | `idle/busy/maint` | 状态精确 |
| `insureDueBefore` | date | ❌ | — | yyyy-MM-dd | 保险到期日早于此日（含） |
| `inspectDueBefore` | date | ❌ | — | yyyy-MM-dd | 年检到期日早于此日（含） |

**出参**：`Result<PageResult<VehiclePageItemRespVO>>` （按 create_time DESC）

| 字段 | 类型 | 说明 |
|---|---|---|
| `records[].id` | string | 车辆 ID（Long 序列化为 String） |
| `records[].plate` | string | 车牌 |
| `records[].vehicleModelId` | string | 型号 ID |
| `records[].vehicleTypeId` | string | 大类 ID |
| `records[].modelName` | string | 型号名（如丰田汉兰达） |
| `records[].seats` | int | 座位数 |
| `records[].fleet` | string | 车队（own/coopA/coopB） |
| `records[].vehicleStatus` | string | 状态（idle/busy/maint） |
| `records[].insurer` | string | 保险公司 |
| `records[].insureDue` | date | 保险到期日 |
| `records[].inspectDue` | date | 年检到期日 |
| `records[].createTime` | datetime | 创建时间 |
| `total` / `page` / `pageSize` | int | 分页元数据 |

**典型示例 请求**：
```
GET /admin/fleet/vehicles/page?page=1&pageSize=20&fleet=own&insureDueBefore=2025-12-31
Authorization: Bearer <token>
（无请求体）
```

**典型示例 响应**：
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "id": "1234567890123456789",
        "plate": "蒙A-88888",
        "vehicleModelId": "1234567890123456001",
        "vehicleTypeId": "1234567890123456002",
        "modelName": "丰田汉兰达",
        "seats": 7,
        "fleet": "own",
        "vehicleStatus": "idle",
        "insurer": "中国人保财险",
        "insureDue": "2025-12-31",
        "inspectDue": "2025-12-31",
        "createTime": "2025-05-21 14:00:00"
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20
  },
  "msg": "成功"
}
```

**错误码**：400 参数校验 / 401 未登录

---

### 3.2 详情

**GET** `/admin/fleet/vehicles/{vehicleId}`

- **使用场景**：详情页 / 编辑前预填
- **认证**：管理后台 JWT
- **幂等**：是

**路径入参**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vehicleId` | Long | ✅ | 车辆 ID |

**出参**：`Result<VehicleDetailRespVO>`

包含 §3.1 全部字段 + 以下扩展：

| 字段 | 类型 | 说明 |
|---|---|---|
| `vin` | string | 车架号（VIN） |
| `regDate` | date | 注册日期 |
| `policyNo` | string | 保单号 |
| `regCertNo` | string | 行驶证号 |
| `regCertOwner` | string | 行驶证所有人 |
| `regCertUsage` | string | 使用性质 |
| `regCertFrontUrl` | string | 行驶证正面 OSS URL |
| `regCertBackUrl` | string | 行驶证副页 OSS URL |
| `attachments[]` | array | 附件列表（按 category + sortNo 升序） |
| `attachments[].id` | string | 附件 ID |
| `attachments[].category` | string | 类目（见 §6.1） |
| `attachments[].sortNo` | int | 同类目排序（0-based） |
| `attachments[].url` | string | OSS URL |
| `attachments[].mimeType` | string | 例 `image/jpeg` |
| `primaryDriverName` | string\|null | **目前恒为 null**（待业务流水 PR 注入司机姓名） |
| `updateTime` | datetime | — |

**典型示例 响应**：
```json
{
  "code": 200,
  "data": {
    "id": "1234567890123456789",
    "plate": "蒙A-88888",
    "vehicleModelId": "1234567890123456001",
    "vehicleTypeId": "1234567890123456002",
    "modelName": "丰田汉兰达",
    "seats": 7,
    "fleet": "own",
    "vehicleStatus": "idle",
    "vin": "LSVNV2182E2100001",
    "regDate": "2020-01-01",
    "inspectDue": "2025-12-31",
    "insurer": "中国人保财险",
    "policyNo": "PICC-2024-XXX",
    "insureDue": "2025-12-31",
    "regCertNo": "12345678",
    "regCertOwner": "呼籁旅游服务有限公司",
    "regCertUsage": "营运租赁",
    "regCertFrontUrl": "https://oss.example.com/fleet/cert-front.jpg",
    "regCertBackUrl": "https://oss.example.com/fleet/cert-back.jpg",
    "attachments": [
      {
        "id": "1234567890123456900",
        "vehicleId": "1234567890123456789",
        "category": "exterior",
        "sortNo": 0,
        "url": "https://oss.example.com/fleet/ext-1.jpg",
        "mimeType": "image/jpeg"
      }
    ],
    "primaryDriverName": null,
    "createTime": "2025-05-21 14:00:00",
    "updateTime": "2025-05-21 14:00:00"
  },
  "msg": "成功"
}
```

**错误码**：**600110** 车辆不存在 / 401 未登录

---

### 3.3 新增

**POST** `/admin/fleet/vehicles`

- **使用场景**：新增车辆 + 附件批量提交（前端 STS 直传 OSS 后把 URL 数组放主表单一并提交）
- **认证**：管理后台 JWT
- **幂等**：否（重复抛 600100/600107）

**Body 入参**（`VehicleSaveReqVO`）：

| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `plate` | string | ✅ | `@Size(max=16)` | 车牌（全局唯一） |
| `vehicleModelId` | Long | ✅ | — | 车型型号 ID（引用 §1 模块） |
| `fleet` | string | ✅ | `@Pattern(own\|coopA\|coopB)` | 车队 |
| `vehicleStatus` | string | ❌ | `@Pattern(idle\|busy\|maint)` | 状态，默认 idle |
| `vin` | string | ❌ | `@Size(max=32)` | 车架号（传则全局唯一） |
| `regDate` | date | ❌ | — | 注册日期 |
| `inspectDue` | date | ❌ | — | 年检到期日 |
| `insurer` | string | ❌ | `@Size(max=64)` | 保险公司 |
| `policyNo` | string | ❌ | `@Size(max=64)` | 保单号 |
| `insureDue` | date | ❌ | — | 保险到期日 |
| `regCertNo` | string | ❌ | `@Size(max=64)` | 行驶证号 |
| `regCertOwner` | string | ❌ | `@Size(max=128)` | 行驶证所有人 |
| `regCertUsage` | string | ❌ | `@Size(max=32)` | 使用性质 |
| `regCertFrontUrl` | string | ❌ | `@Size(max=512)` | 行驶证正面 OSS URL |
| `regCertBackUrl` | string | ❌ | `@Size(max=512)` | 行驶证副页 OSS URL |
| `attachments[]` | array | ❌ | — | 附件数组（见下） |

**`attachments[]` 单项**（`VehicleAttachmentReqVO`）：

| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `id` | Long | ❌ | — | 附件 ID（新增项不传） |
| `category` | string | ✅ | `@Size(max=32)` | 类目（见 §6.1） |
| `url` | string | ✅ | `@Size(max=512)` | OSS URL |
| `mimeType` | string | ✅ | `@Size(max=64)` | 例 `image/jpeg` |
| `sortNo` | int | ❌ | — | 同类目排序（不传时按入参顺序自动重写 0..N-1） |

**出参**：`Result<VehicleCreateRespVO>`

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 新增车辆 ID |
| `attachmentIds[]` | array<Long> | 附件 ID 列表（按入参顺序） |

**典型示例 请求**：
```
POST /admin/fleet/vehicles
Authorization: Bearer <token>
Content-Type: application/json

{
  "plate": "蒙A-88888",
  "vehicleModelId": "1234567890123456001",
  "fleet": "own",
  "vehicleStatus": "idle",
  "vin": "LSVNV2182E2100001",
  "regDate": "2020-01-01",
  "inspectDue": "2025-12-31",
  "insurer": "中国人保财险",
  "policyNo": "PICC-2024-XXX",
  "insureDue": "2025-12-31",
  "regCertNo": "12345678",
  "regCertOwner": "呼籁旅游服务有限公司",
  "regCertUsage": "营运租赁",
  "regCertFrontUrl": "https://oss.example.com/fleet/cert-front.jpg",
  "regCertBackUrl": "https://oss.example.com/fleet/cert-back.jpg",
  "attachments": [
    { "category": "exterior", "url": "https://oss.example.com/fleet/ext-1.jpg", "mimeType": "image/jpeg" },
    { "category": "exterior", "url": "https://oss.example.com/fleet/ext-2.jpg", "mimeType": "image/jpeg" },
    { "category": "insurance", "url": "https://oss.example.com/fleet/ins.jpg", "mimeType": "image/jpeg" }
  ]
}
```

**典型示例 响应**：
```json
{
  "code": 200,
  "data": {
    "id": "1234567890123456789",
    "attachmentIds": ["1234567890123456900", "1234567890123456901", "1234567890123456902"]
  },
  "msg": "成功"
}
```

**异常 请求**（车牌重复）：
```json
{ "code": 600100, "msg": "车牌已存在", "data": null }
```

**异常 请求**（附件类目张数超上限 `exterior` > 6）：
```json
{ "code": 601003, "msg": "该类目张数已达上限", "data": null }
```

**错误码**：400 参数校验 / **600100** 车牌已存在 / **600107** VIN 已存在 / **600108** 车型不存在 / **601002** 附件类目非法 / **601003** 类目张数达上限 / 401 未登录

---

### 3.4 编辑（附件 diff 语义）

**PUT** `/admin/fleet/vehicles/{vehicleId}`

- **使用场景**：编辑车辆基本信息 + 附件增删改一次性提交
- **认证**：管理后台 JWT
- **幂等**：是

**路径入参**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vehicleId` | Long | ✅ | 车辆 ID |

**Body 入参**：同 §3.3（共用 `VehicleSaveReqVO`）

**附件 diff 语义**：

| 入参 attachments[] 项 | DB 中的对应行 | 行为 |
|---|---|---|
| 有 `id` + DB 命中 | 存在 | **保留**（可更新 sortNo） |
| 无 `id`（新增项） | — | **INSERT** |
| — | 存在但本次未传 | **软删** |

**出参**：`Result<VehicleUpdateRespVO>`

| 字段 | 类型 | 说明 |
|---|---|---|
| `kept` | int | 保留的附件数 |
| `inserted` | int | 新插入的附件数 |
| `softDeleted` | int | 软删的附件数 |

**典型示例 响应**：
```json
{
  "code": 200,
  "data": { "kept": 2, "inserted": 1, "softDeleted": 1 },
  "msg": "成功"
}
```

**错误码**：400 参数校验 / **600100** 车牌已存在 / **600107** VIN 已存在 / **600108** 车型不存在 / **600110** 车辆不存在 / **601002** 类目非法 / **601003** 类目张数达上限 / 401 未登录

---

### 3.5 软删

**DELETE** `/admin/fleet/vehicles/{vehicleId}`

- **使用场景**：下架车辆
- **认证**：管理后台 JWT
- **幂等**：是

**路径入参**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vehicleId` | Long | ✅ | 车辆 ID |

**出参**：`Result<Void>`（`data: null`）

**典型示例 响应**：
```json
{ "code": 200, "data": null, "msg": "成功" }
```

**异常 请求**（车辆有未完成派单 —— **业务流水 PR 落地后才生效**）：
```json
{ "code": 600109, "msg": "车辆有未完成派单，不能删除", "data": null }
```

**错误码**：**600109** 有未完成派单（占位，业务流水 PR 上线后生效）/ **600110** 车辆不存在 / 401 未登录

---

### 3.6 Excel 批量导入

**POST** `/admin/fleet/vehicles/import`

- **使用场景**：运营批量录入车辆
- **认证**：管理后台 JWT
- **幂等**：否（重复行按车牌冲突拒绝单行）

**入参**（multipart/form-data）：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file` | file | ✅ | Excel（`.xlsx` / `.xls`）或 CSV |

**表头约定（第 1 行 12 列，顺序固定）**：

```
车牌 | 车型型号ID | 车队(own/coopA/coopB) | 车架号(VIN) |
注册日期(yyyy-MM-dd) | 年检到期日(yyyy-MM-dd) |
保险公司 | 保单号 | 保险到期日(yyyy-MM-dd) |
行驶证号 | 行驶证所有人 | 使用性质
```

**出参**：`Result<VehicleImportRespVO>`

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int | 总行数（不含表头） |
| `success` | int | 成功导入数 |
| `failed` | int | 失败行数 |
| `errorRows[]` | array | 失败行详情 |
| `errorRows[].row` | int | Excel 行号（从 2 开始，1 为表头） |
| `errorRows[].plate` | string | 车牌（用于定位） |
| `errorRows[].msg` | string | 错误信息（如"车牌已存在"） |

**典型示例 响应**：
```json
{
  "code": 200,
  "data": {
    "total": 10,
    "success": 8,
    "failed": 2,
    "errorRows": [
      { "row": 3, "plate": "蒙A-88888", "msg": "车牌已存在" },
      { "row": 7, "plate": "蒙A-99999", "msg": "车型型号 ID 不存在" }
    ]
  },
  "msg": "成功"
}
```

**错误码**：400 文件解析失败 / 401 未登录

---

## 6. 枚举 / 数据字典

### 6.1 附件类目（`category`，`VehicleAttachmentCategoryEnum`）

**所属字段**：`VehicleAttachmentReqVO.category` / `VehicleAttachmentRespVO.category` | **类型**：`String` | **必填**：✅

**真枚举**（后端硬编码，含同类目张数上限）：

| 值 | 中文 | 张数上限 |
|---|---|---|
| `exterior` | 车辆外观 | 6 |
| `interior` | 车辆内饰 | 3 |
| `insurance` | 保险单据 | 2 |
| `inspection` | 年检单据 | 2 |
| `operation_license` | 营运证 | 2 |
| `other` | 其他附件 | 5 |

> 超出上限触发 **601003** "该类目张数已达上限"。

### 6.2 车队（`fleet`）

**所属字段**：`VehicleSaveReqVO.fleet` / `VehiclePageReqVO.fleet` / `VehiclePageItemRespVO.fleet` | **类型**：`String` | **必填**：✅（新增时）

**`@Pattern` 强校验枚举**：

| 值 | 含义 |
|---|---|
| `own` | 自营车辆 |
| `coopA` | 合作车队 A |
| `coopB` | 合作车队 B |

### 6.3 车辆状态（`vehicleStatus`）

**所属字段**：同 `fleet` | **类型**：`String` | **必填**：❌（默认 idle）

**`@Pattern` 强校验枚举**：

| 值 | 含义 |
|---|---|
| `idle` | 空闲 |
| `busy` | 在途 |
| `maint` | 维修中 |

---

## 7. 错误码（全模块汇总）

| code | 含义 | 触发条件 | 涉及接口 |
|---|---|---|---|
| 400 | 参数校验失败 | 字段缺失/长度/枚举值不合法/MIME 非法 | 所有写入接口 |
| 401 | 未登录 | JWT 无效或缺失 | 全部 6 接口 |
| **600100** | 车牌已存在 | `plate` 全局唯一约束 | §3.3 §3.4 |
| **600107** | VIN 已存在 | `vin` 全局唯一约束 | §3.3 §3.4 |
| **600108** | 车型不存在 | `vehicleModelId` 无效或软删 | §3.3 §3.4 |
| **600109** | 有未完成派单，不能删除 | 业务流水 PR 上线后才触发（目前占位） | §3.5 |
| **600110** | 车辆不存在 | `vehicleId` 无效或软删 | §3.2 §3.4 §3.5 |
| **601001** | 实体不存在 | 附件关联的主车辆不存在 | §3.3 §3.4 |
| **601002** | 附件类目非法 | `category` 不在枚举内 | §3.3 §3.4 |
| **601003** | 该类目张数已达上限 | 超出 §6.1 张数上限 | §3.3 §3.4 |
| **601004** | 文件超出大小限制 | 应用层 + OSS HEAD 校验 | §3.6（导入），§3.3/§3.4 主体不直接校验 |
| **601005** | 文件类型非法 | MIME 不在白名单 | §3.3 §3.4 |

> 错误码段位 600100-600110 归属车辆档案；601001-601005 归属附件管理（车辆/司机共用）。
>
> **注意：与文档 v1.4 的差异** —— 文档原计划 VIN 用 600101 / 车型不存在 600102 / 派单冲突 600103，但因这 3 个段位被车型管理库占用（见 §1 changelog），实际代码避让到 **600107/600108/600109**。本 changelog 反映**代码实际值**。

---

## 9. 业务边界

- ✅ **附件 diff 语义统一**：§3.3 新增时所有项无 id；§3.4 编辑时按 `id` 命中度决定保留/插入/软删
- ✅ **sortNo 自动重写**：附件 `sortNo` 不传时按入参顺序自动写 0..N-1（同 category 内 0-based）
- ✅ **行驶证 OSS URL 留主表**：`regCertFrontUrl` / `regCertBackUrl` 是主表字段（不走附件表），与附件分离
- ✅ **唯一约束**：`plate` / `vin` **全局**唯一（违反抛 600100 / 600107）
- ✅ **车队 / 状态强枚举**：`fleet` `vehicleStatus` 走 `@Pattern` 校验，违反返 400
- ⚠️ **primaryDriverName 占位**：§3.2 详情返 `primaryDriverName: null`，待业务流水 PR 上线后注入司机姓名
- ⚠️ **派单删除保护占位**：§3.5 错误码 600109 在业务流水 PR 上线后才会真触发

---

## 11. 影响评估

- **是否破坏向后兼容**：否（接口契约自首次落地以来未变更）
- **前端是否必须同步上线**：否（本次仅补发 changelog，无代码改动）
- **影响已有数据**：无

---

## 12. 注意事项

- **分页参数名**：`page` / `pageSize`（**不是** pageNo）
- **Long 主键序列化**：`id` / `vehicleModelId` / `vehicleTypeId` / `attachments[].id` 等 Long 字段 JSON 返回为 String，前端**不要**当 Number 解析
- **错误码不连续**：600100 / 600107-600110（中间 600101-600106 是车型管理库的段位），见 §7 注解
- **附件 URL 不可改**：§3.4 编辑时 `attachments[].url` 不可改（后端忽略），只能保留 / 新增 / 软删
- **`primaryDriverName` 当前恒为 null**：业务流水 PR 上线前前端**不要**展示该字段或处理为"未指派"

---

## 13. 关联 / 联系人

### 13.1 链接

- **API 文档**: `docs/order-v3/api/API-SPEC-FLEET-V1.5.html` §2 车辆档案
- **DB 文档**: `docs/order-v3/database/DATABASE-SCHEMA-FLEET-V1.5.html` §2.3 / §2.6
- **同期相关 changelog**:
  - 车型管理库 9 接口（[`21_2785_车型管理库-新增接口-管理后台.md`](./21_2785_车型管理库-新增接口-管理后台.md)）
  - 司机档案 6 接口（[`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md)）

### 13.2 联系人

- **后端负责人**: @yst（腰苏图）
- **前端对接（管理后台）**: 待指派
