Files
oneos-backend/docs/superpowers/plans/2026-03-13-rental-full-chain.md
kkfluous d5c3ed373f docs: add rental full-chain implementation plan
27 tasks across 6 chunks: inspection template system, prepare simplification,
delivery enhancement, replacement vehicle module, return order improvements,
BPM flows, event-driven linkage, and frontend changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:53:59 +08:00

1264 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 租赁业务全链路实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让租赁全链路合同→备车→交车→替换车→还车按原型跑通含共享验车模板、BPM审批、事件驱动联动。
**Architecture:** 共享验车模板系统4张表放在 asset 模块,备车/交车/还车通过 `inspection_record_id` 关联。替换车模块从零构建。跨模块联动通过 Spring Event 解耦。BPM 审批继承现有 `BpmProcessInstanceStatusEventListener` 模式。
**Tech Stack:** Spring Boot, MyBatis-Plus (`BaseMapperX`, `LambdaQueryWrapperX`), MapStruct, Flowable BPM, Vue 3 + TypeScript + Ant Design Vue + VXE Table
**Spec:** `docs/superpowers/specs/2026-03-13-rental-full-chain-design.md`
**Key paths:**
- Backend base: `yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/` (below as `$BE`)
- Backend API module: `yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/` (below as `$API`)
- Frontend base: `../oneos-frontend/apps/web-antd/src/` (below as `$FE`)
- DB: 连接信息见 application-local.yaml 配置
- ErrorCodeConstants 位于 `$BE/enums/ErrorCodeConstants.java`server 模块,非 API 模块)
- 所有 DO 类遵循 ContractDO 注解模式:`@TableName`, `@KeySequence`, `@Data`, `@EqualsAndHashCode(callSuper = true)`, `@ToString(callSuper = true)`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor`, extends `BaseDO`
---
## Chunk 1: 数据库 + 验车模板后端
### Task 1: SQL 建表 — 验车模板 4 张表
**Files:**
- Create: `sql/2026-03-13-inspection-tables.sql`
- [ ] **Step 1: 编写 SQL**
```sql
-- 验车模板定义
CREATE TABLE IF NOT EXISTS `asset_inspection_template` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(50) NOT NULL COMMENT '模板编码',
`name` varchar(100) NOT NULL COMMENT '模板名称',
`biz_type` tinyint NOT NULL COMMENT '适用业务1=备车 2=交车 3=还车',
`vehicle_type` varchar(50) DEFAULT NULL COMMENT '适用车辆类型nullable=通用)',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '0=禁用 1=启用',
`remark` varchar(500) DEFAULT NULL,
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT b'0',
`tenant_id` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='验车模板定义';
-- 验车模板检查项
CREATE TABLE IF NOT EXISTS `asset_inspection_template_item` (
`id` bigint NOT NULL AUTO_INCREMENT,
`template_id` bigint NOT NULL COMMENT '关联模板',
`category` varchar(50) NOT NULL COMMENT '分类',
`item_name` varchar(100) NOT NULL COMMENT '检查项名称',
`item_code` varchar(50) NOT NULL COMMENT '检查项编码',
`input_type` varchar(20) NOT NULL DEFAULT 'checkbox' COMMENT '输入类型checkbox/number/text',
`sort` int NOT NULL DEFAULT 0,
`required` tinyint NOT NULL DEFAULT 1 COMMENT '0=否 1=是',
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT b'0',
`tenant_id` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_template_id` (`template_id`)
) ENGINE=InnoDB COMMENT='验车模板检查项';
-- 验车记录
CREATE TABLE IF NOT EXISTS `asset_inspection_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`record_code` varchar(50) NOT NULL COMMENT '记录编码',
`template_id` bigint NOT NULL COMMENT '使用的模板',
`source_type` tinyint NOT NULL COMMENT '来源1=备车 2=交车 3=还车',
`source_id` bigint NOT NULL COMMENT '来源业务ID',
`vehicle_id` bigint NOT NULL COMMENT '车辆ID',
`inspector_name` varchar(50) DEFAULT NULL COMMENT '检查人',
`inspection_time` datetime DEFAULT NULL COMMENT '检查时间',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0=待检查 1=检查中 2=已完成',
`overall_result` tinyint DEFAULT NULL COMMENT '1=合格 2=不合格',
`remark` varchar(500) DEFAULT NULL,
`cloned_from_id` bigint DEFAULT NULL COMMENT '克隆来源记录ID',
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT b'0',
`tenant_id` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_vehicle_source` (`vehicle_id`, `source_type`)
) ENGINE=InnoDB COMMENT='验车记录';
-- 验车记录检查项结果
CREATE TABLE IF NOT EXISTS `asset_inspection_record_item` (
`id` bigint NOT NULL AUTO_INCREMENT,
`record_id` bigint NOT NULL COMMENT '关联记录',
`item_code` varchar(50) NOT NULL COMMENT '检查项编码',
`category` varchar(50) NOT NULL COMMENT '分类',
`item_name` varchar(100) NOT NULL COMMENT '检查项名称',
`input_type` varchar(20) NOT NULL DEFAULT 'checkbox' COMMENT '输入类型',
`result` tinyint DEFAULT NULL COMMENT '1=合格 2=不合格 3=不适用',
`value` varchar(200) DEFAULT NULL COMMENT '数值/文本输入值',
`remark` varchar(500) DEFAULT NULL,
`image_urls` varchar(2000) DEFAULT NULL COMMENT '图片URL JSON数组',
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT b'0',
`tenant_id` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_record_id` (`record_id`)
) ENGINE=InnoDB COMMENT='验车记录检查项结果';
```
- [ ] **Step 2: 执行 SQL**
通过 PyMySQL 连接数据库执行(连接信息见 application-local.yaml
- [ ] **Step 3: Commit**
```bash
git add sql/2026-03-13-inspection-tables.sql
git commit -m "sql: create inspection template and record tables"
```
---
### Task 2: 验车模板枚举类
**Files:**
- Create: `$API/enums/inspection/InspectionSourceTypeEnum.java`
- Create: `$API/enums/inspection/InspectionStatusEnum.java`
- Create: `$API/enums/inspection/InspectionResultEnum.java`
- [ ] **Step 1: 创建 InspectionSourceTypeEnum**
```java
package cn.iocoder.yudao.module.asset.enums.inspection;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum InspectionSourceTypeEnum {
PREPARE(1, "备车"),
DELIVERY(2, "交车"),
RETURN(3, "还车");
private final Integer type;
private final String name;
}
```
- [ ] **Step 2: 创建 InspectionStatusEnum**
```java
package cn.iocoder.yudao.module.asset.enums.inspection;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum InspectionStatusEnum {
PENDING(0, "待检查"),
IN_PROGRESS(1, "检查中"),
COMPLETED(2, "已完成");
private final Integer status;
private final String name;
}
```
- [ ] **Step 3: 创建 InspectionResultEnum**
```java
package cn.iocoder.yudao.module.asset.enums.inspection;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum InspectionResultEnum {
PASS(1, "合格"),
FAIL(2, "不合格"),
NA(3, "不适用");
private final Integer result;
private final String name;
}
```
- [ ] **Step 4: Commit**
```bash
git add yudao-module-asset/yudao-module-asset-api/
git commit -m "feat: add inspection enums"
```
---
### Task 3: 验车模板 DO + Mapper
**Files:**
- Create: `$BE/dal/dataobject/inspection/InspectionTemplateDO.java`
- Create: `$BE/dal/dataobject/inspection/InspectionTemplateItemDO.java`
- Create: `$BE/dal/dataobject/inspection/InspectionRecordDO.java`
- Create: `$BE/dal/dataobject/inspection/InspectionRecordItemDO.java`
- Create: `$BE/dal/mysql/inspection/InspectionTemplateMapper.java`
- Create: `$BE/dal/mysql/inspection/InspectionTemplateItemMapper.java`
- Create: `$BE/dal/mysql/inspection/InspectionRecordMapper.java`
- Create: `$BE/dal/mysql/inspection/InspectionRecordItemMapper.java`
- [ ] **Step 1: 创建 InspectionTemplateDO**
Follow `ContractDO` pattern: `@TableName("asset_inspection_template")`, `@KeySequence("asset_inspection_template_seq")`, `@Data`, `@EqualsAndHashCode(callSuper = true)`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor`, extends `BaseDO`.
Fields: `id`(@TableId), `code`, `name`, `bizType`(Integer), `vehicleType`, `status`(Integer), `remark`.
- [ ] **Step 2: 创建 InspectionTemplateItemDO**
`@TableName("asset_inspection_template_item")`. Fields: `id`, `templateId`, `category`, `itemName`, `itemCode`, `inputType`, `sort`(Integer), `required`(Integer).
- [ ] **Step 3: 创建 InspectionRecordDO**
`@TableName("asset_inspection_record")`. Fields: `id`, `recordCode`, `templateId`, `sourceType`(Integer), `sourceId`, `vehicleId`, `inspectorName`, `inspectionTime`(LocalDateTime), `status`(Integer), `overallResult`(Integer), `remark`, `clonedFromId`.
- [ ] **Step 4: 创建 InspectionRecordItemDO**
`@TableName("asset_inspection_record_item")`. Fields: `id`, `recordId`, `itemCode`, `category`, `itemName`, `inputType`, `result`(Integer), `value`, `remark`, `imageUrls`.
- [ ] **Step 5: 创建 4 个 Mapper**
Each extends `BaseMapperX<XxxDO>` with `@Mapper` annotation.
`InspectionTemplateMapper` — add selectPage + match methods:
```java
default PageResult<InspectionTemplateDO> selectPage(InspectionTemplatePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InspectionTemplateDO>()
.likeIfPresent(InspectionTemplateDO::getCode, reqVO.getCode())
.likeIfPresent(InspectionTemplateDO::getName, reqVO.getName())
.eqIfPresent(InspectionTemplateDO::getBizType, reqVO.getBizType())
.eqIfPresent(InspectionTemplateDO::getStatus, reqVO.getStatus())
.orderByDesc(InspectionTemplateDO::getId));
}
default InspectionTemplateDO selectByBizTypeAndVehicleType(Integer bizType, String vehicleType) {
return selectOne(new LambdaQueryWrapperX<InspectionTemplateDO>()
.eq(InspectionTemplateDO::getBizType, bizType)
.eqIfPresent(InspectionTemplateDO::getVehicleType, vehicleType)
.eq(InspectionTemplateDO::getStatus, 1)
.orderByDesc(InspectionTemplateDO::getId)
.last("LIMIT 1"));
}
default InspectionTemplateDO selectDefaultByBizType(Integer bizType) {
return selectOne(new LambdaQueryWrapperX<InspectionTemplateDO>()
.eq(InspectionTemplateDO::getBizType, bizType)
.isNull(InspectionTemplateDO::getVehicleType)
.eq(InspectionTemplateDO::getStatus, 1)
.orderByDesc(InspectionTemplateDO::getId)
.last("LIMIT 1"));
}
```
`InspectionTemplateItemMapper` — add:
```java
default List<InspectionTemplateItemDO> selectByTemplateId(Long templateId) {
return selectList(new LambdaQueryWrapperX<InspectionTemplateItemDO>()
.eq(InspectionTemplateItemDO::getTemplateId, templateId)
.orderByAsc(InspectionTemplateItemDO::getSort));
}
```
`InspectionRecordMapper` — add:
```java
default InspectionRecordDO selectLatestByVehicleAndSourceType(Long vehicleId, Integer sourceType) {
return selectOne(new LambdaQueryWrapperX<InspectionRecordDO>()
.eq(InspectionRecordDO::getVehicleId, vehicleId)
.eq(InspectionRecordDO::getSourceType, sourceType)
.eq(InspectionRecordDO::getStatus, 2) // 已完成
.orderByDesc(InspectionRecordDO::getId)
.last("LIMIT 1"));
}
```
`InspectionRecordItemMapper` — add:
```java
default List<InspectionRecordItemDO> selectByRecordId(Long recordId) {
return selectList(new LambdaQueryWrapperX<InspectionRecordItemDO>()
.eq(InspectionRecordItemDO::getRecordId, recordId)
.orderByAsc(InspectionRecordItemDO::getId));
}
default void deleteByRecordId(Long recordId) {
delete(new LambdaQueryWrapperX<InspectionRecordItemDO>()
.eq(InspectionRecordItemDO::getRecordId, recordId));
}
```
- [ ] **Step 6: Compile 验证**
```bash
mvn compile -pl yudao-module-asset/yudao-module-asset-server -am -DskipTests
```
- [ ] **Step 7: Commit**
```bash
git add yudao-module-asset/
git commit -m "feat: add inspection template and record DO/Mapper"
```
---
### Task 4: 验车模板 VO + Convert
**Files:**
- Create: `$BE/controller/admin/inspection/vo/InspectionTemplateSaveReqVO.java`
- Create: `$BE/controller/admin/inspection/vo/InspectionTemplateRespVO.java`
- Create: `$BE/controller/admin/inspection/vo/InspectionTemplatePageReqVO.java`
- Create: `$BE/controller/admin/inspection/vo/InspectionTemplateItemVO.java`
- Create: `$BE/controller/admin/inspection/vo/InspectionRecordDetailVO.java`
- Create: `$BE/controller/admin/inspection/vo/InspectionRecordItemUpdateReqVO.java`
- Create: `$BE/convert/inspection/InspectionConvert.java`
- [ ] **Step 1: 创建 VO 类**
**InspectionTemplateSaveReqVO** — Fields: `id`(Long), `code`(@NotBlank), `name`(@NotBlank), `bizType`(@NotNull Integer), `vehicleType`, `status`(Integer), `remark`, `items`(List<InspectionTemplateItemVO>).
**InspectionTemplateItemVO** — Fields: `id`, `category`(@NotBlank), `itemName`(@NotBlank), `itemCode`(@NotBlank), `inputType`, `sort`, `required`.
**InspectionTemplateRespVO** — Fields: `id`, `code`, `name`, `bizType`, `vehicleType`, `status`, `remark`, `createTime`, `items`(List<InspectionTemplateItemVO>).
**InspectionTemplatePageReqVO** extends `PageParam` — Fields: `code`, `name`, `bizType`, `vehicleType`, `status`.
**InspectionRecordDetailVO** — Fields: `id`, `recordCode`, `templateId`, `sourceType`, `sourceId`, `vehicleId`, `inspectorName`, `inspectionTime`, `status`, `overallResult`, `remark`, `clonedFromId`, `createTime`, `items`(List<InspectionRecordItemVO>).
Inner **InspectionRecordItemVO** — Fields: `id`, `itemCode`, `category`, `itemName`, `inputType`, `result`, `value`, `remark`, `imageUrls`.
**InspectionRecordItemUpdateReqVO** — Fields: `id`(@NotNull), `result`(Integer), `value`, `remark`, `imageUrls`.
- [ ] **Step 2: 创建 InspectionConvert**
```java
@Mapper
public interface InspectionConvert {
InspectionConvert INSTANCE = Mappers.getMapper(InspectionConvert.class);
InspectionTemplateDO convert(InspectionTemplateSaveReqVO bean);
InspectionTemplateRespVO convert(InspectionTemplateDO bean);
PageResult<InspectionTemplateRespVO> convertPage(PageResult<InspectionTemplateDO> page);
InspectionTemplateItemDO convertItem(InspectionTemplateItemVO bean);
List<InspectionTemplateItemDO> convertItemList(List<InspectionTemplateItemVO> list);
List<InspectionTemplateItemVO> convertItemVOList(List<InspectionTemplateItemDO> list);
InspectionRecordDetailVO convertRecord(InspectionRecordDO bean);
InspectionRecordDetailVO.InspectionRecordItemVO convertRecordItem(InspectionRecordItemDO bean);
List<InspectionRecordDetailVO.InspectionRecordItemVO> convertRecordItemList(List<InspectionRecordItemDO> list);
}
```
- [ ] **Step 3: Compile 验证**
- [ ] **Step 4: Commit**
---
### Task 5: 验车记录 Service
**Files:**
- Create: `$BE/service/inspection/InspectionTemplateService.java`
- Create: `$BE/service/inspection/InspectionTemplateServiceImpl.java`
- Create: `$BE/service/inspection/InspectionRecordService.java`
- Create: `$BE/service/inspection/InspectionRecordServiceImpl.java`
- [ ] **Step 1: 创建 InspectionTemplateService 接口**
Methods: `createTemplate`, `updateTemplate`, `deleteTemplate`, `getTemplate`, `getTemplatePage`, `matchTemplate(Integer bizType, String vehicleType)`.
- [ ] **Step 2: 实现 InspectionTemplateServiceImpl**
`matchTemplate` 逻辑: 先按 bizType+vehicleType 精确匹配 → 无结果则 bizType+vehicleType IS NULL 匹配通用模板。CRUD 标准实现。保存时同步保存/更新 items先删后插
- [ ] **Step 3: 创建 InspectionRecordService 接口**
```java
public interface InspectionRecordService {
Long createRecord(Long templateId, Integer sourceType, Long sourceId, Long vehicleId);
Long cloneRecord(Long sourceRecordId, Integer newSourceType, Long newSourceId);
void updateRecordItem(InspectionRecordItemUpdateReqVO updateReqVO);
void completeRecord(Long recordId, String inspectorName);
InspectionRecordDetailVO getRecordDetail(Long recordId);
InspectionRecordDO getLatestRecord(Long vehicleId, Integer sourceType);
}
```
- [ ] **Step 4: 实现 InspectionRecordServiceImpl**
**createRecord**: 根据 templateId 查询所有 item → 创建 record生成 recordCode → 批量创建 record_item从 template_item 复制 code/category/name/inputTyperesult/value 为 null
**recordCode 生成**: `{prefix}-{yyyyMMdd}-{seq}`prefix 根据 sourceType 决定BC/JC/HC
**cloneRecord**: 查源 record + items → 创建新 recordclonedFromId=源id新 sourceType/sourceId→ 复制所有 items保留 result/value/remark/imageUrls
**completeRecord**: 校验 record 存在 → 更新 status=2, inspectorName, inspectionTime=now()。检查必填项是否都有 result没有则抛异常。
**getRecordDetail**: 查 record + items → 组装 VO。
- [ ] **Step 5: Compile 验证**
- [ ] **Step 6: Commit**
---
### Task 6: 验车模板 Controller + 错误码
**Files:**
- Create: `$BE/controller/admin/inspection/InspectionTemplateController.java`
- Create: `$BE/controller/admin/inspection/InspectionRecordController.java`
- Modify: `$BE/enums/ErrorCodeConstants.java`
- [ ] **Step 1: 添加错误码**
`ErrorCodeConstants.java` 末尾追加:
```java
// ========== 验车模板管理 1-008-011-000 ==========
ErrorCode INSPECTION_TEMPLATE_NOT_EXISTS = new ErrorCode(1_008_011_000, "验车模板不存在");
ErrorCode INSPECTION_RECORD_NOT_EXISTS = new ErrorCode(1_008_011_001, "验车记录不存在");
ErrorCode INSPECTION_RECORD_ALREADY_COMPLETED = new ErrorCode(1_008_011_002, "验车记录已完成");
ErrorCode INSPECTION_RECORD_REQUIRED_ITEMS_INCOMPLETE = new ErrorCode(1_008_011_003, "必填检查项未全部完成");
ErrorCode INSPECTION_TEMPLATE_MATCH_FAILED = new ErrorCode(1_008_011_004, "未找到匹配的验车模板");
// ========== 替换车管理 1-008-012-000 ==========
ErrorCode VEHICLE_REPLACEMENT_NOT_EXISTS = new ErrorCode(1_008_012_000, "替换车申请不存在");
ErrorCode VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_UPDATE = new ErrorCode(1_008_012_001, "当前状态不允许修改");
ErrorCode VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_012_002, "当前状态不允许删除");
ErrorCode VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_SUBMIT = new ErrorCode(1_008_012_003, "当前状态不允许提交审批");
ErrorCode VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_WITHDRAW = new ErrorCode(1_008_012_004, "当前状态不允许撤回");
ErrorCode VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_CONFIRM = new ErrorCode(1_008_012_005, "当前状态不允许确认换回");
```
- [ ] **Step 2: 创建 InspectionTemplateController**
`@RequestMapping("/asset/inspection-template")`。标准 CRUD 端点create, update, delete, get, page。权限前缀 `asset:inspection-template:xxx`
- [ ] **Step 3: 创建 InspectionRecordController**
`@RequestMapping("/asset/inspection-record")`。端点:
- `GET /get?id=` — 获取详情
- `PUT /update-item` — 更新检查项结果
- `POST /complete?id=&inspectorName=` — 完成验车
- [ ] **Step 4: Compile 验证**
- [ ] **Step 5: Commit**
```bash
git add yudao-module-asset/
git commit -m "feat: add inspection template/record service and controller"
```
---
## Chunk 2: 备车精简 + 交车增强(后端)
### Task 7: 备车表结构变更 SQL
**Files:**
- Create: `sql/2026-03-13-prepare-simplify.sql`
- [ ] **Step 1: 编写并执行 SQL**
```sql
-- 备车表新增 inspection_record_id移除废弃字段
ALTER TABLE asset_vehicle_prepare
ADD COLUMN inspection_record_id bigint DEFAULT NULL COMMENT '关联验车记录' AFTER defect_photos;
ALTER TABLE asset_vehicle_prepare
DROP COLUMN IF EXISTS check_list,
DROP COLUMN IF EXISTS contract_id,
DROP COLUMN IF EXISTS contract_code,
DROP COLUMN IF EXISTS enlarged_text_photo;
```
注意:`DROP COLUMN IF EXISTS` 在 MySQL 8.0.13+ 支持。若版本较低,先用 `SHOW COLUMNS` 检查后再 DROP。
- [ ] **Step 2: Commit**
---
### Task 8: 更新 VehiclePrepareDO + Mapper + Service
**Files:**
- Modify: `$BE/dal/dataobject/prepare/VehiclePrepareDO.java` — 移除 `contractId`, `contractCode`, `checkList`, `enlargedTextPhoto`,新增 `inspectionRecordId`
- Modify: `$BE/dal/mysql/prepare/VehiclePrepareMapper.java` — 移除 contractId 相关查询条件
- Modify: `$BE/service/prepare/VehiclePrepareServiceImpl.java` — createPrepare 时调用 InspectionRecordService.createRecord()completePrepare 时调用 completeRecord()
- Modify: `$BE/controller/admin/prepare/vo/` — 对应 VO 移除废弃字段,新增 inspectionRecordId
- [ ] **Step 1: 更新 VehiclePrepareDO**
删除字段: `contractId`, `contractCode`, `checkList`, `enlargedTextPhoto`
新增字段: `private Long inspectionRecordId;`
- [ ] **Step 2: 更新 VOs**
SaveReqVO/RespVO 同步移除废弃字段,新增 `inspectionRecordId`
- [ ] **Step 3: 更新 Service — 创建时生成验车记录**
`VehiclePrepareServiceImpl.createPrepare()` 方法中,`vehiclePrepareMapper.insert(prepare)` 之后添加:
```java
@Resource
private InspectionRecordService inspectionRecordService;
@Resource
private InspectionTemplateService inspectionTemplateService;
// 创建备车单后,自动创建验车记录
InspectionTemplateDO template = inspectionTemplateService.matchTemplate(
InspectionSourceTypeEnum.PREPARE.getType(), vehicleType);
if (template != null) {
Long recordId = inspectionRecordService.createRecord(
template.getId(), InspectionSourceTypeEnum.PREPARE.getType(),
prepare.getId(), prepare.getVehicleId());
prepare.setInspectionRecordId(recordId);
vehiclePrepareMapper.updateById(prepare);
}
```
- [ ] **Step 4: Compile 验证**
- [ ] **Step 5: Commit**
---
### Task 9: 交车表结构变更 + 后端更新
**Files:**
- Create: `sql/2026-03-13-delivery-order-inspection.sql`
- Modify: `$BE/dal/dataobject/delivery/DeliveryOrderDO.java` — 新增 `inspectionRecordId`
- Modify: `$BE/service/delivery/DeliveryOrderServiceImpl.java` — 创建交车单时克隆备车验车记录
- [ ] **Step 1: SQL**
```sql
ALTER TABLE asset_delivery_order
ADD COLUMN inspection_record_id bigint DEFAULT NULL COMMENT '关联验车记录' AFTER cost_list;
```
- [ ] **Step 2: 更新 DeliveryOrderDO**
新增 `private Long inspectionRecordId;`。不删除 `inspectionData` 字段(废弃但保留兼容)。
**注意**: 当前交车单结构中 `inspectionData` 在主表上,`inspection_record_id` 也放在主表以保持一致。若交车单需支持多车各自验车,后续可迁移到 `DeliveryOrderVehicleDO` 子表。
- [ ] **Step 3: 更新 Service — 创建时克隆备车验车记录**
`DeliveryOrderServiceImpl` 的创建交车单方法中(`createDeliveryOrder` 或类似方法),`deliveryOrderMapper.insert(deliveryOrder)` 之后添加:
```java
// 查找该车最近的备车验车记录
InspectionRecordDO prepareRecord = inspectionRecordService.getLatestRecord(
vehicleId, InspectionSourceTypeEnum.PREPARE.getType());
if (prepareRecord != null) {
Long recordId = inspectionRecordService.cloneRecord(
prepareRecord.getId(), InspectionSourceTypeEnum.DELIVERY.getType(), deliveryOrder.getId());
deliveryOrder.setInspectionRecordId(recordId);
deliveryOrderMapper.updateById(deliveryOrder);
}
```
- [ ] **Step 4: Compile 验证**
- [ ] **Step 5: Commit**
---
## Chunk 3: 替换车模块(后端完整)
### Task 10: 替换车 SQL
**Files:**
- Create: `sql/2026-03-13-vehicle-replacement.sql`
- [ ] **Step 1: 编写并执行 SQL**
```sql
CREATE TABLE IF NOT EXISTS `asset_vehicle_replacement` (
`id` bigint NOT NULL AUTO_INCREMENT,
`replacement_code` varchar(50) NOT NULL COMMENT '替换单编码',
`replacement_type` tinyint NOT NULL COMMENT '1=临时 2=永久',
`contract_id` bigint NOT NULL,
`contract_code` varchar(50) DEFAULT NULL,
`customer_id` bigint DEFAULT NULL,
`customer_name` varchar(100) DEFAULT NULL,
`delivery_order_id` bigint DEFAULT NULL COMMENT '来源交车单ID',
`original_vehicle_id` bigint NOT NULL,
`original_plate_no` varchar(20) DEFAULT NULL,
`original_vin` varchar(50) DEFAULT NULL,
`new_vehicle_id` bigint DEFAULT NULL,
`new_plate_no` varchar(20) DEFAULT NULL,
`new_vin` varchar(50) DEFAULT NULL,
`replacement_reason` varchar(500) DEFAULT NULL,
`expected_date` date DEFAULT NULL,
`actual_date` date DEFAULT NULL,
`return_date` date DEFAULT NULL COMMENT '临时替换预计归还日期',
`actual_return_date` date DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0=草稿 1=审批中 2=审批通过 3=执行中 4=已完成 5=审批驳回 6=已撤回',
`approval_status` tinyint NOT NULL DEFAULT 0 COMMENT '审批状态(与 ContractDO 保持一致的双字段模式status 跟踪业务生命周期approval_status 跟踪 BPM 审批状态)',
`bpm_instance_id` varchar(64) DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL,
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT b'0',
`tenant_id` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='替换车申请';
```
- [ ] **Step 2: Commit**
---
### Task 11: 替换车枚举
**Files:**
- Create: `$API/enums/replacement/ReplacementTypeEnum.java` — TEMPORARY(1), PERMANENT(2)
- Create: `$API/enums/replacement/ReplacementStatusEnum.java` — DRAFT(0), APPROVING(1), APPROVED(2), EXECUTING(3), COMPLETED(4), REJECTED(5), WITHDRAWN(6)
- [ ] **Step 1: 创建枚举类**
- [ ] **Step 2: Commit**
---
### Task 12: 替换车 DO + Mapper + VO + Convert
**Files:**
- Create: `$BE/dal/dataobject/replacement/VehicleReplacementDO.java`
- Create: `$BE/dal/mysql/replacement/VehicleReplacementMapper.java`
- Create: `$BE/controller/admin/replacement/vo/VehicleReplacementSaveReqVO.java`
- Create: `$BE/controller/admin/replacement/vo/VehicleReplacementRespVO.java`
- Create: `$BE/controller/admin/replacement/vo/VehicleReplacementPageReqVO.java`
- Create: `$BE/convert/replacement/VehicleReplacementConvert.java`
- [ ] **Step 1: 创建 VehicleReplacementDO**
`@TableName("asset_vehicle_replacement")`. 所有字段按 spec 的表结构。
- [ ] **Step 2: 创建 Mapper**
```java
@Mapper
public interface VehicleReplacementMapper extends BaseMapperX<VehicleReplacementDO> {
default PageResult<VehicleReplacementDO> selectPage(VehicleReplacementPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<VehicleReplacementDO>()
.likeIfPresent(VehicleReplacementDO::getReplacementCode, reqVO.getReplacementCode())
.eqIfPresent(VehicleReplacementDO::getReplacementType, reqVO.getReplacementType())
.eqIfPresent(VehicleReplacementDO::getContractId, reqVO.getContractId())
.likeIfPresent(VehicleReplacementDO::getCustomerName, reqVO.getCustomerName())
.eqIfPresent(VehicleReplacementDO::getStatus, reqVO.getStatus())
.orderByDesc(VehicleReplacementDO::getId));
}
}
```
- [ ] **Step 3: 创建 VOs**
**SaveReqVO**: `id`, `replacementType`(@NotNull), `contractId`(@NotNull), `contractCode`, `customerId`, `customerName`, `deliveryOrderId`, `originalVehicleId`(@NotNull), `originalPlateNo`, `originalVin`, `newVehicleId`, `newPlateNo`, `newVin`, `replacementReason`, `expectedDate`, `returnDate`, `remark`.
**RespVO**: 所有 SaveReqVO 字段 + `replacementCode`, `actualDate`, `actualReturnDate`, `status`, `approvalStatus`, `bpmInstanceId`, `createTime`, `creator`.
**PageReqVO** extends PageParam: `replacementCode`, `replacementType`, `contractId`, `customerName`, `status`.
- [ ] **Step 4: 创建 Convert**
标准 MapStruct convert: `convert(SaveReqVO)→DO`, `convert(DO)→RespVO`, `convertPage`.
- [ ] **Step 5: Compile 验证**
- [ ] **Step 6: Commit**
---
### Task 13: 替换车 Service + BPM Listener + 事件类
**Files:**
- Create: `$BE/service/replacement/VehicleReplacementService.java`
- Create: `$BE/service/replacement/VehicleReplacementServiceImpl.java`
- Create: `$BE/service/replacement/listener/ReplacementBpmListener.java`
- Create: `$BE/service/replacement/event/ReplacementApprovedEvent.java`
- Create: `$BE/service/replacement/event/ReplacementReturnConfirmedEvent.java`
- [ ] **Step 1: 创建事件类**
**ReplacementApprovedEvent**:
```java
@Getter
@AllArgsConstructor
public class ReplacementApprovedEvent {
private final Long replacementId;
private final Integer replacementType;
private final Long contractId;
private final Long originalVehicleId;
}
```
**ReplacementReturnConfirmedEvent**:
```java
@Getter
@AllArgsConstructor
public class ReplacementReturnConfirmedEvent {
private final Long replacementId;
private final Long originalVehicleId;
}
```
- [ ] **Step 2: 创建 Service 接口**
Methods: `createReplacement`, `updateReplacement`, `deleteReplacement`, `getReplacement`, `getReplacementPage`, `submitApproval`, `withdrawApproval`, `updateApprovalStatus(Long id, Integer bpmStatus)`, `confirmReturn`.
- [ ] **Step 3: 实现 ServiceImpl**
关键方法:
**createReplacement**: 生成 replacementCode (`TH-yyyyMMdd-seq`),设 status=DRAFT。
**submitApproval**: 校验 status=DRAFT/REJECTED → 调用 `bpmProcessInstanceApi.createProcessInstance()` → 设 status=APPROVING。
**updateApprovalStatus**: BPM 回调。APPROVE → status=APPROVED → 发布 `ReplacementApprovedEvent` → status=EXECUTING。REJECT → status=REJECTED。CANCEL → status=WITHDRAWN。
**confirmReturn**: 校验 status=EXECUTING + type=TEMPORARY → status=COMPLETED → 发布 `ReplacementReturnConfirmedEvent`
- [ ] **Step 4: 创建 ReplacementBpmListener**
```java
@Component
public class ReplacementBpmListener extends BpmProcessInstanceStatusEventListener {
public static final String PROCESS_KEY = "asset_vehicle_replacement";
@Resource
private VehicleReplacementService replacementService;
@Override
protected String getProcessDefinitionKey() { return PROCESS_KEY; }
@Override
protected void onEvent(BpmProcessInstanceStatusEvent event) {
replacementService.updateApprovalStatus(
Long.parseLong(event.getBusinessKey()), event.getStatus());
}
}
```
- [ ] **Step 5: Compile 验证**
- [ ] **Step 6: Commit**
---
### Task 14: 替换车 Controller
**Files:**
- Create: `$BE/controller/admin/replacement/VehicleReplacementController.java`
- [ ] **Step 1: 创建 Controller**
`@RequestMapping("/asset/vehicle-replacement")`。端点create, update, delete, get, page, submit, withdraw, confirm-return。权限前缀 `asset:vehicle-replacement:xxx`
- [ ] **Step 2: Compile 验证**
- [ ] **Step 3: Commit**
---
## Chunk 4: 还车模块完善 + 事件驱动(后端)
### Task 15: 还车表结构变更 SQL
**Files:**
- Create: `sql/2026-03-13-return-order-enhance.sql`
- [ ] **Step 1: 编写并执行 SQL**
```sql
ALTER TABLE asset_return_order
ADD COLUMN source_type tinyint DEFAULT NULL COMMENT '来源1=手动 2=替换车触发' AFTER bpm_instance_id,
ADD COLUMN source_id bigint DEFAULT NULL COMMENT '来源业务ID' AFTER source_type,
ADD COLUMN delivery_order_id bigint DEFAULT NULL COMMENT '关联交车单ID' AFTER source_id;
ALTER TABLE asset_return_order_vehicle
ADD COLUMN inspection_record_id bigint DEFAULT NULL COMMENT '关联验车记录' AFTER defect_photos;
```
- [ ] **Step 2: Commit**
---
### Task 16: 更新还车 DO + Service
**Files:**
- Modify: `$BE/dal/dataobject/returnorder/ReturnOrderDO.java` — 新增 `sourceType`, `sourceId`, `deliveryOrderId`
- Modify: `$BE/dal/dataobject/returnorder/ReturnOrderVehicleDO.java` — 新增 `inspectionRecordId`
- Modify: `$BE/service/returnorder/ReturnOrderService.java` — 新增 `createFromDelivery`, `createFromReplacement`, `startVehicleInspection`, `completeVehicleInspection`, `submitApproval`, `withdrawApproval`, `updateApprovalStatus`
- Modify: `$BE/service/returnorder/ReturnOrderServiceImpl.java`
- Create: `$BE/service/returnorder/listener/ReturnOrderBpmListener.java`
- Create: `$BE/service/returnorder/event/ReturnApprovedEvent.java`
- [ ] **Step 1: 更新 DO 字段**
ReturnOrderDO 新增: `private Integer sourceType;`, `private Long sourceId;`, `private Long deliveryOrderId;`
ReturnOrderVehicleDO 新增: `private Long inspectionRecordId;`
- [ ] **Step 2: 创建 ReturnApprovedEvent**
```java
@Getter
@AllArgsConstructor
public class ReturnApprovedEvent {
private final Long returnOrderId;
private final List<Long> vehicleIds;
}
```
- [ ] **Step 3: 新增 Service 方法**
**createFromDelivery(deliveryOrderId, vehicleIds[])**:
- 查交车单获取合同/客户信息
- 创建还车单主记录sourceType=1, deliveryOrderId=deliveryOrderId
- 生成 orderCode
- 为每个 vehicleId 创建 ReturnOrderVehicleDO 子记录
- 返回还车单ID
**createFromReplacement(replacementId, contractId, vehicleId)**:
- 创建还车单主记录sourceType=2, sourceId=replacementId
- 创建一条车辆子记录
- 返回还车单ID
**startVehicleInspection(returnOrderVehicleId)**:
- 查找该车最近的交车 inspection_record → 克隆为还车 record
- 更新 ReturnOrderVehicleDO.inspectionRecordId
**completeVehicleInspection(returnOrderVehicleId)**:
- 调用 inspectionRecordService.completeRecord()
- 检查该还车单所有车辆是否都验车完成 → 是则更新还车单 status=1(验车完成)
**submitApproval(id)**: 校验 status=1(验车完成) → 调用 BPM → approvalStatus=1
**updateApprovalStatus**: BPM 回调。APPROVE → approvalStatus=2 → 发布 ReturnApprovedEvent。
- [ ] **Step 4: 创建 ReturnOrderBpmListener**
同 ReplacementBpmListener 模式PROCESS_KEY = "asset_return_order"。
- [ ] **Step 5: 更新还车 Controller**
新增端点:`create-from-delivery`, `start-inspection`, `complete-inspection`, `submit`, `withdraw`
- [ ] **Step 6: Compile 验证**
- [ ] **Step 7: Commit**
---
### Task 17: 事件监听器 — 跨模块联动
**Files:**
- Create: `$BE/service/delivery/event/DeliveryCompletedEvent.java`
- Create: `$BE/service/event/VehicleStatusEventListener.java`
- Create: `$BE/service/event/ReturnOrderEventListener.java`
- [ ] **Step 1: 创建 DeliveryCompletedEvent**
```java
@Getter
@AllArgsConstructor
public class DeliveryCompletedEvent {
private final Long deliveryOrderId;
private final Long vehicleId;
}
```
- [ ] **Step 2: 创建 VehicleStatusEventListener**
```java
@Component
@Slf4j
public class VehicleStatusEventListener {
@Resource
private VehicleBaseMapper vehicleBaseMapper; // 或对应的车辆 Service
@TransactionalEventListener
public void onDeliveryCompleted(DeliveryCompletedEvent event) {
log.info("交车完成,更新车辆状态: vehicleId={}", event.getVehicleId());
// 更新车辆状态为已交付
}
@TransactionalEventListener
public void onReturnApproved(ReturnApprovedEvent event) {
log.info("还车审批通过,更新车辆状态: vehicleIds={}", event.getVehicleIds());
for (Long vehicleId : event.getVehicleIds()) {
// 更新车辆状态为可用
}
}
@TransactionalEventListener
public void onReplacementReturnConfirmed(ReplacementReturnConfirmedEvent event) {
log.info("临时替换换回,恢复原车状态: vehicleId={}", event.getOriginalVehicleId());
// 更新车辆状态为可用
}
}
```
- [ ] **Step 3: 创建 ReturnOrderEventListener**
```java
@Component
@Slf4j
public class ReturnOrderEventListener {
@Resource
private ReturnOrderService returnOrderService;
@EventListener // 非 @TransactionalEventListener确保同事务
public void onReplacementApproved(ReplacementApprovedEvent event) {
if (event.getReplacementType().equals(ReplacementTypeEnum.PERMANENT.getType())) {
log.info("永久替换审批通过,自动创建还车单: replacementId={}", event.getReplacementId());
returnOrderService.createFromReplacement(
event.getReplacementId(),
event.getContractId(),
event.getOriginalVehicleId()
);
}
}
}
```
- [ ] **Step 4: 在交车完成时发布事件**
`DeliveryOrderServiceImpl``completeDeliveryOrder()` 方法(或标记交车单为已完成的方法)中,状态更新为已完成之后添加:
```java
@Resource
private ApplicationEventPublisher eventPublisher;
// 交车完成后发布事件
eventPublisher.publishEvent(new DeliveryCompletedEvent(deliveryOrder.getId(), vehicleId));
```
- [ ] **Step 5: Compile 验证**
- [ ] **Step 6: Commit**
---
### Task 18: BPM 流程定义 XML + 部署
**注意**: BPMN XML 文件需在应用启动前就位。Task 13/16 的 BPM Listener 和本 Task 的 XML 必须一起部署,不要在中间重启应用。
**Files:**
- Create: `$BE/../resources/processes/asset_vehicle_replacement.bpmn20.xml`
- Create: `$BE/../resources/processes/asset_return_order.bpmn20.xml`
- [ ] **Step 1: 创建替换车审批流程 XML**
参照现有 `asset_contract.bpmn20.xml`process id=`asset_vehicle_replacement`
流程: startEvent → 部门主管 → gateway(永久替换?) → 总经理审批 → endEvent。
- [ ] **Step 2: 创建还车审批流程 XML**
process id=`asset_return_order`。简单串行: startEvent → 部门主管 → endEventApprove / endEventReject。
- [ ] **Step 3: Compile 验证**
- [ ] **Step 4: Commit**
```bash
git add .
git commit -m "feat: add replacement/return BPM process definitions"
```
---
### Task 19: 后端整体编译验证
- [ ] **Step 1: Full compile**
```bash
mvn compile -DskipTests
```
修复所有编译错误直到通过。
- [ ] **Step 2: Commit 修复**
---
## Chunk 5: 前端 — API + 共享组件
### Task 20: 前端 API 文件
**Files:**
- Create: `$FE/api/asset/inspection.ts`
- Create: `$FE/api/asset/vehicle-replacement.ts`
- Modify: `$FE/api/asset/return-order.ts` — 新增接口
- [ ] **Step 1: 创建 inspection.ts**
```typescript
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace InspectionApi {
export interface Template {
id?: number;
code: string;
name: string;
bizType: number;
vehicleType?: string;
status: number;
remark?: string;
items?: TemplateItem[];
}
export interface TemplateItem {
id?: number;
category: string;
itemName: string;
itemCode: string;
inputType: string;
sort: number;
required: number;
}
export interface RecordDetail {
id: number;
recordCode: string;
templateId: number;
sourceType: number;
sourceId: number;
vehicleId: number;
inspectorName?: string;
inspectionTime?: string;
status: number;
overallResult?: number;
remark?: string;
items: RecordItem[];
}
export interface RecordItem {
id: number;
itemCode: string;
category: string;
itemName: string;
inputType: string;
result?: number;
value?: string;
remark?: string;
imageUrls?: string;
}
}
export function getInspectionTemplatePage(params: PageParam) {
return requestClient.get<PageResult<InspectionApi.Template>>('/asset/inspection-template/page', { params });
}
export function getInspectionTemplate(id: number) {
return requestClient.get<InspectionApi.Template>(`/asset/inspection-template/get?id=${id}`);
}
export function createInspectionTemplate(data: InspectionApi.Template) {
return requestClient.post('/asset/inspection-template/create', data);
}
export function updateInspectionTemplate(data: InspectionApi.Template) {
return requestClient.put('/asset/inspection-template/update', data);
}
export function deleteInspectionTemplate(id: number) {
return requestClient.delete(`/asset/inspection-template/delete?id=${id}`);
}
export function getInspectionRecord(id: number) {
return requestClient.get<InspectionApi.RecordDetail>(`/asset/inspection-record/get?id=${id}`);
}
export function updateInspectionRecordItem(data: { id: number; result?: number; value?: string; remark?: string; imageUrls?: string }) {
return requestClient.put('/asset/inspection-record/update-item', data);
}
export function completeInspection(id: number, inspectorName: string) {
return requestClient.post(`/asset/inspection-record/complete?id=${id}&inspectorName=${inspectorName}`);
}
```
- [ ] **Step 2: 创建 vehicle-replacement.ts**
标准 CRUD + submit/withdraw/confirmReturn。参照 customer.ts 模式。
- [ ] **Step 3: 更新 return-order.ts**
新增: `createFromDelivery(deliveryOrderId, vehicleIds)`, `startVehicleInspection(returnOrderVehicleId)`, `completeVehicleInspection(returnOrderVehicleId)`, `submitReturnOrder(id)`, `withdrawReturnOrder(id)`.
- [ ] **Step 4: Commit**
---
### Task 21: 共享验车组件 InspectionForm.vue
**Files:**
- Create: `$FE/views/asset/components/InspectionForm.vue`
- [ ] **Step 1: 创建组件**
Props: `recordId`(number), `readonly`(boolean), `onComplete`(callback)。
功能:
- 调用 `getInspectionRecord(recordId)` 获取数据
-`category` 分组,使用 `a-collapse` 展示
- 每个 item 根据 `inputType` 渲染:
- `checkbox``a-radio-group`(合格/不合格/不适用)
- `number``a-input-number`
- `text``a-input`
- 每项有备注 input + 图片上传
- 编辑模式下 onChange 调用 `updateInspectionRecordItem` 保存
- readonly 模式禁用所有输入
- "完成验车"按钮调用 `completeInspection`
- [ ] **Step 2: Commit**
---
## Chunk 6: 前端 — 各模块页面
### Task 22: 替换车前端页面
**Files:**
- Create: `$FE/views/asset/vehicle-replacement/index.vue`
- Create: `$FE/views/asset/vehicle-replacement/data.ts`
- Create: `$FE/views/asset/vehicle-replacement/modules/form.vue`
- [ ] **Step 1: 创建 data.ts**
搜索表单: replacementCode, replacementType, customerName, status。
列定义: replacementCode, replacementType, contractCode, customerName, originalPlateNo, newPlateNo, status, expectedDate, createTime, 操作。
状态常量: `REPLACEMENT_TYPE_OPTIONS`, `REPLACEMENT_STATUS_OPTIONS`
- [ ] **Step 2: 创建 index.vue**
参照 supplier/index.vue 模式useVbenVxeGrid + 搜索 + 操作列(查看/编辑/删除/提交审批/撤回/确认换回,按 status 条件显示)。
- [ ] **Step 3: 创建 form.vue**
三模式 (create/edit/view)。使用 useVbenModal + useVbenForm。
字段: replacementType(Select), contractId(Select), originalVehicleId(Select 带车辆列表), newVehicleId(Select), replacementReason(TextArea), expectedDate(DatePicker), returnDate(DatePicker, 仅临时显示), remark。
create 模式: 检查 URL query params (`contractId`, `vehicleId`, `deliveryOrderId`),有则自动填充并 readonly。
- [ ] **Step 4: Commit**
---
### Task 23: 备车前端改造
**Files:**
- Modify: `$FE/views/asset/vehicle-prepare/modules/form.vue` — 移除硬编码 checkList改用 InspectionForm 组件
- Modify: `$FE/views/asset/vehicle-prepare/data.ts` — 移除 contractId 相关字段
- Modify: `$FE/api/asset/vehicle-prepare.ts` — 更新接口定义
- [ ] **Step 1: 更新 API 定义**
VehiclePrepare 接口移除 `contractId`, `contractCode`, `checkList`,新增 `inspectionRecordId`
- [ ] **Step 2: 更新 data.ts**
移除 contractId/contractCode 搜索字段和列。
- [ ] **Step 3: 改造 form.vue**
- 移除 checkList 相关的表格/抽屉代码
- 在表单末尾添加 `<InspectionForm :record-id="formData.inspectionRecordId" :readonly="isView" />` 组件
- 如果 inspectionRecordId 不存在(旧数据),显示提示信息
- [ ] **Step 4: Commit**
---
### Task 24: 交车前端改造
**Files:**
- Modify: `$FE/views/asset/delivery-order/index.vue` — 操作列增加"还车"和"替换车"按钮
- Modify: `$FE/views/asset/delivery-order/modules/form.vue` — 验车区域改用 InspectionForm
- [ ] **Step 1: 操作列扩展**
在 index.vue 的 getRowActions 中,对 status=已完成 的记录增加:
```typescript
{ label: '还车', onClick: () => handleReturn(row) },
{ label: '替换车', onClick: () => handleReplacement(row) },
```
**handleReturn**: 弹窗显示交车单关联车辆列表checkbox 多选)→ 确认后调用 `createFromDelivery(row.id, selectedVehicleIds)` → 跳转还车单详情。
**handleReplacement**: 弹窗显示交车单关联车辆列表(单选)→ 确认后 `router.push({ path: '/asset/vehicle-replacement', query: { contractId, vehicleId, deliveryOrderId } })`
- [ ] **Step 2: 验车区域改造**
form.vue 中如果有 inspectionRecordId使用 InspectionForm 组件替代原有 inspectionData JSON 表单。
- [ ] **Step 3: Commit**
---
### Task 25: 还车前端改造
**Files:**
- Modify: `$FE/views/asset/return-order/index.vue` — 操作列增加"提交审批"、"撤回"
- Modify: `$FE/views/asset/return-order/modules/form.vue` — 每辆车增加"开始验车"按钮 + InspectionForm
- [ ] **Step 1: 更新 index.vue 操作列**
按 status 和 approvalStatus 条件显示: 查看(始终)、编辑(待验车)、删除(待验车)、提交审批(验车完成+未提交)、撤回(审批中)。
- [ ] **Step 2: 改造 form.vue 验车区域**
车辆子表格中每行增加:
- "开始验车"按钮(当 inspectionRecordId 为空时显示)→ 调用 `startVehicleInspection`
- InspectionForm 组件(当 inspectionRecordId 存在时显示)
- "完成验车"按钮 → 调用 `completeVehicleInspection`
- [ ] **Step 3: Commit**
---
### Task 26: 前端构建验证
- [ ] **Step 1: Build**
```bash
cd ../oneos-frontend && pnpm run build:antd
```
修复所有 TypeScript/构建错误直到通过。
- [ ] **Step 2: Commit 修复**
---
### Task 27: 全链路端到端验证
- [ ] **Step 1: 后端编译**
```bash
cd ../oneos-backend && mvn compile -DskipTests
```
- [ ] **Step 2: 前端构建**
```bash
cd ../oneos-frontend && pnpm run build:antd
```
- [ ] **Step 3: 功能检查清单**
验证以下流程可正常操作:
1. 验车模板 CRUD — 创建模板+检查项
2. 备车 — 创建备车单 → 自动生成验车记录 → 完成验车
3. 交车 — 创建交车单 → 克隆备车验车记录 → 完成验车 → 操作列显示还车/替换车
4. 替换车 — 从交车页面触发 → 自动填充 → 提交审批
5. 还车 — 从交车页面触发 → 创建壳子 → 开始验车(克隆交车记录) → 完成验车 → 提交审批
6. 永久替换审批通过 → 自动创建还车单
- [ ] **Step 4: Final commit**
```bash
git add .
git commit -m "feat: complete rental full-chain implementation"
```