# 租赁业务全链路设计规格

## 概述

实现租赁业务全链路（合同 → 备车 → 交车 → 替换车 → 还车）生产可用，包括：模块功能补全、跨模块状态联动、BPM 审批流程部署、共享验车模板系统。

## 架构决策

- **方案选择**：方案 C — 轻量模板表 + 记录表，模块内引用
- **验车模板**：共享表（`asset_inspection_template` + `asset_inspection_record`），放在 asset 模块
- **流转机制**：克隆 + 预填，各业务持有独立 `inspection_record_id`
- **事件驱动**：Spring Event 解耦跨模块状态变更
- **BPM 集成**：继承现有 `BpmProcessInstanceStatusEventListener` 模式
- **表名前缀**：统一使用 `asset_` 前缀，与现有 `asset_return_order`、`asset_delivery_order` 等保持一致
- **BPM 字段名**：统一使用 `bpm_instance_id`，与现有 `ContractDO`、`ReturnOrderDO` 保持一致

---

## 第一部分：共享验车模板系统

### 数据库表

#### `asset_inspection_template`（模板定义）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| code | varchar(50) | 模板编码，如 `TPL-BC-001` |
| name | varchar(100) | 模板名称，如"氢能车备车检查模板" |
| biz_type | tinyint | 适用业务：1=备车 2=交车 3=还车（一个模板可适用多种业务） |
| vehicle_type | varchar(50) | 适用车辆类型（如"氢能车"、"电动车"，nullable 表示通用） |
| status | tinyint | 0=禁用 1=启用 |
| remark | varchar(500) | 备注 |
| + BaseDO 字段 | | creator, create_time, updater, update_time, deleted, tenant_id |

**模板匹配规则**：创建验车记录时，先按 `biz_type` + `vehicle_type` 精确匹配，无结果则按 `biz_type` + `vehicle_type IS NULL` 匹配通用模板。

#### `asset_inspection_template_item`（模板检查项）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| template_id | bigint | 关联模板 |
| category | varchar(50) | 分类，如"制动系统"、"外观检查" |
| item_name | varchar(100) | 检查项名称 |
| item_code | varchar(50) | 检查项编码，如 `BRAKE-001` |
| input_type | varchar(20) | 输入类型：`checkbox`(合格/不合格)、`number`(数值)、`text`(文本) |
| sort | int | 排序 |
| required | tinyint | 是否必填：0=否 1=是 |
| + BaseDO | | |

#### `asset_inspection_record`（验车记录实例）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| record_code | varchar(50) | 记录编码，如 `BC-V001-20260313-001` |
| template_id | bigint | 使用的模板 |
| source_type | tinyint | 来源：1=备车 2=交车 3=还车 |
| source_id | bigint | 来源业务ID |
| vehicle_id | bigint | 车辆ID |
| inspector_name | varchar(50) | 检查人 |
| inspection_time | datetime | 检查时间 |
| status | tinyint | 0=待检查 1=检查中 2=已完成 |
| overall_result | tinyint | 总结果：1=合格 2=不合格 |
| remark | varchar(500) | 总备注 |
| cloned_from_id | bigint | 克隆来源记录ID（nullable） |
| + BaseDO | | |

#### `asset_inspection_record_item`（逐项检查结果）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| record_id | bigint | 关联记录 |
| item_code | varchar(50) | 检查项编码（与模板 item 对应） |
| category | varchar(50) | 分类（冗余，便于展示） |
| item_name | varchar(100) | 检查项名称（冗余） |
| input_type | varchar(20) | 输入类型（冗余，便于前端渲染） |
| result | tinyint | 1=合格 2=不合格 3=不适用 |
| value | varchar(200) | 数值/文本类型的输入值 |
| remark | varchar(500) | 该项备注 |
| image_urls | varchar(2000) | 图片URL，JSON数组 |
| + BaseDO | | |

### 克隆流转机制

```
备车完成（record status=已完成）
  ↓ 交车时，查找该 vehicleId 最近的备车 record
  ↓ InspectionRecordService.cloneRecord(sourceRecordId, DELIVERY, newSourceId)
  ↓ 生成新 record + 复制所有 item（result/value 保留，可修改）
  ↓ cloned_from_id = 源 record.id
交车完成
  ↓ 还车时同理克隆交车 record
```

### 后端结构

```
asset-server/
├── dal/dataobject/inspection/
│   ├── InspectionTemplateDO.java
│   ├── InspectionTemplateItemDO.java
│   ├── InspectionRecordDO.java
│   └── InspectionRecordItemDO.java
├── dal/mysql/inspection/
│   ├── InspectionTemplateMapper.java
│   ├── InspectionTemplateItemMapper.java
│   ├── InspectionRecordMapper.java
│   └── InspectionRecordItemMapper.java
├── service/inspection/
│   ├── InspectionTemplateService.java
│   ├── InspectionTemplateServiceImpl.java
│   ├── InspectionRecordService.java
│   └── InspectionRecordServiceImpl.java
├── controller/admin/inspection/
│   ├── InspectionTemplateController.java  (模板 CRUD)
│   └── InspectionRecordController.java    (记录查询)
└── convert/inspection/
    └── InspectionConvert.java
```

### 核心 Service 方法

```java
public interface InspectionRecordService {
    // 根据模板创建空白验车记录
    Long createRecord(Long templateId, Integer sourceType, Long sourceId, Long vehicleId);

    // 克隆已有记录（跨模块流转）
    Long cloneRecord(Long sourceRecordId, Integer newSourceType, Long newSourceId);

    // 更新检查项结果
    void updateRecordItem(Long recordItemId, Integer result, String value, String remark, String imageUrls);

    // 完成验车
    void completeRecord(Long recordId, String inspectorName);

    // 查询记录详情（含所有 item）
    InspectionRecordDetailVO getRecordDetail(Long recordId);

    // 查找车辆最近的某类型验车记录
    InspectionRecordDO getLatestRecord(Long vehicleId, Integer sourceType);
}
```

---

## 第二部分：备车模块精简

### 现状

`VehiclePrepareDO` 有 ~30 个字段，其中 `checkList`(JSON) 存储检查数据。需精简为纯业务字段 + `inspection_record_id` 关联。

### 精简后 `asset_vehicle_prepare` 字段

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| vehicle_id | bigint | 车辆ID |
| plate_no | varchar(20) | 车牌号（冗余） |
| vin | varchar(50) | VIN码（冗余） |
| vehicle_model_id | bigint | 车型ID |
| brand | varchar(50) | 品牌（冗余） |
| model | varchar(50) | 型号（冗余） |
| vehicle_type | varchar(50) | 车辆类型（冗余） |
| parking_lot | varchar(100) | 停车场 |
| preparation_type | varchar(50) | 备车类型 |
| mileage | int | 里程数 |
| hydrogen_remaining | decimal(10,2) | 剩余氢量 |
| hydrogen_unit | varchar(10) | 氢量单位 |
| battery_remaining | decimal(10,2) | 剩余电量 |
| has_body_ad | tinyint | 是否有车身广告 |
| body_ad_photos | varchar(2000) | 车身广告照片（JSON） |
| has_tail_lift | tinyint | 是否有尾板 |
| spare_tire_depth | decimal(5,2) | 备胎深度 |
| spare_tire_photo | varchar(500) | 备胎照片 |
| trailer_plate_no | varchar(20) | 挂车车牌 |
| defect_photos | varchar(2000) | 缺陷照片（JSON） |
| inspection_record_id | bigint | **新增：关联验车记录** |
| remark | varchar(500) | 备注 |
| status | tinyint | 0=待检查 1=已完成 |
| complete_time | datetime | 完成时间 |
| + BaseDO | | |

**移除字段**：`check_list`(JSON)、`contract_id`、`contract_code`、`enlarged_text_photo`

**关键变更**：
- 移除 `contract_id` / `contract_code` — 备车与合同解耦，备车是运维常态
- 移除 `check_list` JSON — 验车数据统一走 `inspection_record`
- 新增 `inspection_record_id` — 关联共享验车记录

**数据迁移**：现有 `asset_vehicle_prepare` 表中 `contract_id`、`contract_code`、`check_list`、`enlarged_text_photo` 字段标记为废弃（ALTER TABLE DROP COLUMN），现有数据通过脚本迁移 `check_list` JSON 到 `asset_inspection_record` + `asset_inspection_record_item`。若现有数据量小或尚未投产，可直接删除旧列无需迁移。

### 备车创建流程

1. 用户选择车辆 → 创建备车单
2. 系统根据 `biz_type=1(备车)` + 车辆的 `vehicleType` 匹配验车模板 → 调用 `InspectionRecordService.createRecord()` 生成记录
3. 备车人员逐项检查填写（前端动态渲染 inspection_record_item，按 category 分组，根据 input_type 渲染不同输入控件）
4. 提交 → 调用 `InspectionRecordService.completeRecord()` → 备车单 status=已完成

---

## 第三部分：交车模块增强

### 数据库变更

`asset_delivery_order` 表变更：

| 操作 | 字段 | 说明 |
|------|------|------|
| 新增 | inspection_record_id (bigint) | 关联验车记录 |
| 保留 | cost_list (JSON) | 费用列表保持原样，不做改动 |

**关于 `inspection_data`**：现有 JSON 字段标记为废弃，新数据不再写入。验车数据统一通过 `inspection_record_id` 关联。待确认无历史依赖后可 DROP COLUMN。

### 交车验车流程

1. 创建交车单时，查找该车最近的备车 `inspection_record`
2. 调用 `InspectionRecordService.cloneRecord()` 克隆为交车记录
3. 预填备车检查结果，交车人员逐项确认/修改
4. 提交验车 → 记录状态=已完成

### 操作列扩展

交车管理页面已完成的交车单，操作列新增：

| 按钮 | 条件 | 行为 |
|------|------|------|
| 还车 | status=已完成 | 弹窗选择该交车单关联的车辆（可多选），调用 `POST /return-order/create-from-delivery`，跳转还车单详情 |
| 替换车 | status=已完成 | 弹窗选择该交车单关联的某辆车，路由跳转替换车新建页，携带 `contractId`, `vehicleId`, `deliveryOrderId` |

---

## 第四部分：替换车模块（从零构建）

### 数据库表

#### `asset_vehicle_replacement`

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| replacement_code | varchar(50) | 替换单编码，如 `TH-20260313-001` |
| replacement_type | tinyint | 1=临时替换 2=永久替换 |
| contract_id | bigint | 合同ID |
| contract_code | varchar(50) | 合同编号（冗余） |
| customer_id | bigint | 客户ID |
| customer_name | varchar(100) | 客户名称（冗余） |
| delivery_order_id | bigint | 来源交车单ID（从交车页面触发时记录） |
| original_vehicle_id | bigint | 原车ID |
| original_plate_no | varchar(20) | 原车车牌号（冗余） |
| original_vin | varchar(50) | 原车VIN（冗余） |
| new_vehicle_id | bigint | 新车ID |
| new_plate_no | varchar(20) | 新车车牌号（冗余） |
| new_vin | varchar(50) | 新车VIN（冗余） |
| replacement_reason | varchar(500) | 替换原因 |
| expected_date | date | 预计替换日期 |
| actual_date | date | 实际替换日期 |
| return_date | date | 临时替换预计归还日期 |
| actual_return_date | date | 临时替换实际归还日期 |
| status | tinyint | 业务状态（见状态流转） |
| approval_status | tinyint | 审批状态 |
| bpm_instance_id | varchar(64) | BPM流程实例ID |
| remark | varchar(500) | 备注 |
| + BaseDO | | |

**说明**：冗余字段使用 `plate_no`（车牌号）而非 `vehicleCode`，因为现有 DO 中车辆标识统一使用 `plateNo` + `vin`。

### 状态流转

```
草稿(0) → 审批中(1) → 审批通过(2) → 执行中(3) → 已完成(4)
                     → 审批驳回(5)
         → 已撤回(6)
```

- 审批通过 + 永久替换 → 自动创建原车还车单壳子，状态进入"执行中"
- 审批通过 + 临时替换 → 状态进入"执行中"
- 执行中 + 临时替换 → 确认换回 → "已完成"
- 执行中 + 永久替换 → 原车还车单完成后 → "已完成"

### 从交车页面快捷触发

交车管理操作列"替换车"按钮 → 路由跳转：
```
/asset/vehicle-replacement/create?contractId=xx&vehicleId=xx&deliveryOrderId=xx
```
替换车表单自动填充合同信息、原车信息（只读），用户只需选择新车、填写原因。

### 后端结构

```
asset-server/
├── dal/dataobject/replacement/
│   └── VehicleReplacementDO.java
├── dal/mysql/replacement/
│   └── VehicleReplacementMapper.java
├── service/replacement/
│   ├── VehicleReplacementService.java
│   ├── VehicleReplacementServiceImpl.java
│   └── listener/
│       └── ReplacementBpmListener.java
├── controller/admin/replacement/
│   ├── VehicleReplacementController.java
│   └── vo/
│       ├── VehicleReplacementSaveReqVO.java
│       ├── VehicleReplacementRespVO.java
│       └── VehicleReplacementPageReqVO.java
└── convert/replacement/
    └── VehicleReplacementConvert.java
```

### 接口设计

| 端点 | 方法 | 说明 |
|------|------|------|
| POST /asset/vehicle-replacement/create | create | 创建替换申请 |
| PUT /asset/vehicle-replacement/update | update | 更新（草稿/驳回） |
| DELETE /asset/vehicle-replacement/delete | delete | 删除（草稿） |
| GET /asset/vehicle-replacement/get | get | 获取详情 |
| GET /asset/vehicle-replacement/page | page | 分页查询 |
| POST /asset/vehicle-replacement/submit | submit | 提交BPM审批 |
| POST /asset/vehicle-replacement/withdraw | withdraw | 撤回审批 |
| POST /asset/vehicle-replacement/confirm-return | confirmReturn | 临时替换确认换回 |

---

## 第五部分：还车模块完善

### 现有架构

还车模块采用主子表结构：
- **`asset_return_order`**（主表）：还车单基本信息、合同/客户关联、审批状态
- **`asset_return_order_vehicle`**（子表）：每辆车的还车详情，含里程、氢量差、各项费用、`checkList`(JSON)

### 数据库变更

#### `asset_return_order`（主表）新增字段

| 字段 | 操作 | 类型 | 说明 |
|------|------|------|------|
| source_type | 新增 | tinyint | 来源：1=手动(从交车触发) 2=替换车触发 |
| source_id | 新增 | bigint | 来源业务ID（替换车申请ID 或 交车单ID） |
| delivery_order_id | 新增 | bigint | 关联交车单ID（追溯来源） |

#### `asset_return_order_vehicle`（子表）变更

| 字段 | 操作 | 类型 | 说明 |
|------|------|------|------|
| inspection_record_id | 新增 | bigint | 关联验车记录（每辆车独立验车记录） |
| check_list | 废弃 | JSON | 不再写入新数据，验车统一走 inspection_record |

**关键设计**：`inspection_record_id` 放在子表 `asset_return_order_vehicle` 上（而非主表），因为一个还车单可包含多辆车，每辆车有独立的验车记录。

### 状态流转

还车单状态（`status` 字段）保持现有值域不变：

```
待验车(0) → 验车完成(1) → 已结算(2)
```

审批通过 `approval_status` 字段（已有）独立管理：

```
待审批(0) → 审批中(1) → 审批通过(2) → 审批驳回(3) → 已撤回(4)
```

**完整流程**：待验车 → 逐车验车 → 全部验车完成(status=1) → 提交审批(approvalStatus=1) → 审批通过(approvalStatus=2)

费用结算（status=2）暂不纳入本期。

### 触发方式

1. **从交车管理操作列触发**：用户在交车单操作列点"还车"→ 弹窗选择车辆 → `POST /return-order/create-from-delivery` → 创建还车单壳子（含选中的车辆子记录）→ 跳转还车单详情页
2. **永久替换审批通过自动创建**：事件监听器创建，source_type=2，自动创建一条主记录 + 一条车辆子记录

### 验车流程

1. 用户进入还车单详情 → 选择某辆车 → 点击"开始验车"
2. 系统查找该车最近的交车 inspection_record → 克隆为还车 record
3. 预填交车检查结果，用户逐项确认/修改
4. 提交单车验车 → 该车辆子记录的 `inspection_record_id` 更新
5. 所有车辆验车完成 → 还车单 status=1(验车完成) → 可提交审批

### 接口设计

| 端点 | 方法 | 说明 |
|------|------|------|
| POST /asset/return-order/create | create | 手动创建 |
| POST /asset/return-order/create-from-delivery | createFromDelivery | 从交车记录创建壳子（参数：deliveryOrderId + vehicleIds[]） |
| PUT /asset/return-order/update | update | 更新 |
| DELETE /asset/return-order/delete | delete | 删除（待验车） |
| GET /asset/return-order/get | get | 获取详情（含车辆子表） |
| GET /asset/return-order/page | page | 分页查询 |
| POST /asset/return-order/start-inspection | startInspection | 开始单车验车（参数：returnOrderVehicleId），克隆交车record |
| POST /asset/return-order/complete-inspection | completeInspection | 完成单车验车 |
| POST /asset/return-order/submit | submit | 提交BPM审批（验车完成后） |
| POST /asset/return-order/withdraw | withdraw | 撤回审批 |

---

## 第六部分：BPM 审批流程

### 需要审批的业务

| 业务 | 流程Key | 触发时机 | 审批通过后动作 |
|------|---------|---------|---------------|
| 租赁合同 | `asset_contract` | 已有，无需新建 | 合同状态→已审批 |
| 替换车申请 | `asset_vehicle_replacement` | 替换申请提交 | 临时→执行中；永久→执行中+创建还车单壳子 |
| 还车 | `asset_return_order` | 验车完成后提交 | 还车单 approvalStatus→审批通过 |

### BPM 集成模式

继承现有 `BpmProcessInstanceStatusEventListener`：

```java
// 替换车审批监听
@Component
public class ReplacementBpmListener extends BpmProcessInstanceStatusEventListener {
    public static final String PROCESS_KEY = "asset_vehicle_replacement";

    @Override
    protected String getProcessDefinitionKey() { return PROCESS_KEY; }

    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        replacementService.updateApprovalStatus(
            Long.parseLong(event.getBusinessKey()), event.getStatus());
    }
}

// 还车审批监听
@Component
public class ReturnOrderBpmListener extends BpmProcessInstanceStatusEventListener {
    public static final String PROCESS_KEY = "asset_return_order";

    @Override
    protected String getProcessDefinitionKey() { return PROCESS_KEY; }

    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        returnOrderService.updateApprovalStatus(
            Long.parseLong(event.getBusinessKey()), event.getStatus());
    }
}
```

### BPMN XML 文件

新增两个流程定义，放在 `resources/processes/`：

**`asset_vehicle_replacement.bpmn20.xml`** — 带网关：发起 → 部门主管 → （永久替换?） → 总经理审批 → 完成

**`asset_return_order.bpmn20.xml`** — 简单串行：发起 → 部门主管审批 → 完成

命名规则与现有 `asset_contract.bpmn20.xml` 一致。

---

## 第七部分：事件驱动 & 跨模块联动

### 事件清单

| 事件类 | 发布者 | 监听者 | 动作 |
|--------|--------|--------|------|
| `DeliveryCompletedEvent` | DeliveryOrderService | VehicleStatusListener | 车辆状态→已交付 |
| `ReplacementApprovedEvent` | ReplacementBpmListener | ReturnOrderEventListener | 永久替换→创建还车单壳子 |
| `ReplacementReturnConfirmedEvent` | ReplacementService | VehicleStatusListener | 临时替换换回→恢复原车状态 |
| `ReturnApprovedEvent` | ReturnOrderBpmListener | VehicleStatusListener | 还车审批通过→车辆状态→可用 |

### 事件监听实现

```java
@Component
public class VehicleStatusEventListener {

    @TransactionalEventListener
    public void onDeliveryCompleted(DeliveryCompletedEvent event) {
        vehicleService.updateStatus(event.getVehicleId(), VehicleStatus.DELIVERED);
    }

    @TransactionalEventListener
    public void onReturnApproved(ReturnApprovedEvent event) {
        // 还车单有多辆车，逐辆更新
        for (Long vehicleId : event.getVehicleIds()) {
            vehicleService.updateStatus(vehicleId, VehicleStatus.AVAILABLE);
        }
    }

    @TransactionalEventListener
    public void onReplacementReturnConfirmed(ReplacementReturnConfirmedEvent event) {
        vehicleService.updateStatus(event.getOriginalVehicleId(), VehicleStatus.AVAILABLE);
    }
}

@Component
public class ReturnOrderEventListener {

    // 使用 @EventListener 而非 @TransactionalEventListener，
    // 确保还车单创建在同一事务中，避免审批已提交但还车单创建失败的不一致
    @EventListener
    public void onReplacementApproved(ReplacementApprovedEvent event) {
        if (event.getReplacementType() == ReplacementType.PERMANENT) {
            returnOrderService.createFromReplacement(
                event.getReplacementId(),
                event.getContractId(),
                event.getOriginalVehicleId()
            );
        }
    }
}
```

**关于事务安全**：`ReturnOrderEventListener.onReplacementApproved()` 使用 `@EventListener`（非 `@TransactionalEventListener`），确保还车单创建与替换车状态更新在同一事务内完成。`@TransactionalEventListener` 默认 `AFTER_COMMIT`，会导致还车单创建在事务外执行，失败时无法回滚。

---

## 前端变更概要

### 共享验车组件

创建通用验车表单组件 `InspectionForm.vue`：
- 按 category 分组展示检查项
- 根据 `input_type` 渲染不同控件：`checkbox` → Radio(合格/不合格/不适用)；`number` → InputNumber；`text` → Input
- 每项支持：备注 + 图片上传
- 只读模式（查看历史记录）和编辑模式
- 被备车/交车/还车表单复用

### 替换车前端

新建 `views/asset/vehicle-replacement/`：
- `index.vue` — 列表页（分页、筛选、操作列）
- `modules/form.vue` — 表单（create/edit/view 三模式）
- `data.ts` — 搜索/列/常量定义

### 备车前端改造

- 移除硬编码 `checkList` 表单
- 改用 `InspectionForm.vue` 组件动态渲染

### 交车/还车前端改造

- 交车表单中验车区域改用 `InspectionForm.vue`
- 交车列表操作列增加"还车"和"替换车"按钮
- 还车表单中每辆车增加"开始验车"按钮 + `InspectionForm.vue`

### 新增 API

```typescript
// inspection.ts
export function getInspectionRecord(id: number)
export function updateInspectionRecordItem(data: {...})
export function completeInspection(recordId: number, inspectorName: string)

// vehicle-replacement.ts
export function createReplacement(data: VehicleReplacementSaveReqVO)
export function updateReplacement(data: VehicleReplacementSaveReqVO)
export function deleteReplacement(id: number)
export function getReplacement(id: number)
export function getReplacementPage(params: VehicleReplacementPageReqVO)
export function submitReplacement(id: number)
export function withdrawReplacement(id: number)
export function confirmReplacementReturn(id: number)

// return-order.ts (补充)
export function createFromDelivery(deliveryOrderId: number, vehicleIds: number[])
export function startVehicleInspection(returnOrderVehicleId: number)
export function completeVehicleInspection(returnOrderVehicleId: number)
export function submitReturnOrder(id: number)
export function withdrawReturnOrder(id: number)
```

---

## 实施顺序

1. **共享验车模板** — SQL建表 → DO/Mapper → Service → Controller → 前端组件
2. **备车精简** — 移除旧字段 → 对接 inspection_record → 前端改造
3. **交车增强** — 新增 inspection_record_id → 废弃 inspection_data → 操作列扩展 → 前端改造
4. **替换车模块** — 全套新建（DO → VO → Mapper → Service → BPM listener → Controller → 前端）
5. **还车完善** — 新增字段 → 对接验车模板（子表级别） → 从交车/替换车触发 → BPM listener → 前端改造
6. **BPM 部署** — 创建 BPMN XML → 部署到引擎
7. **事件驱动** — 事件类 → 监听器 → 车辆状态同步
8. **端到端验证** — `mvn compile` + `pnpm run build:antd` + 全链路测试
