diff --git a/docs/superpowers/plans/2026-03-13-rental-full-chain.md b/docs/superpowers/plans/2026-03-13-rental-full-chain.md new file mode 100644 index 0000000..a3d4479 --- /dev/null +++ b/docs/superpowers/plans/2026-03-13-rental-full-chain.md @@ -0,0 +1,1263 @@ +# 租赁业务全链路实施计划 + +> **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` with `@Mapper` annotation. + +`InspectionTemplateMapper` — add selectPage + match methods: +```java +default PageResult selectPage(InspectionTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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() + .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() + .eq(InspectionTemplateDO::getBizType, bizType) + .isNull(InspectionTemplateDO::getVehicleType) + .eq(InspectionTemplateDO::getStatus, 1) + .orderByDesc(InspectionTemplateDO::getId) + .last("LIMIT 1")); +} +``` + +`InspectionTemplateItemMapper` — add: +```java +default List selectByTemplateId(Long templateId) { + return selectList(new LambdaQueryWrapperX() + .eq(InspectionTemplateItemDO::getTemplateId, templateId) + .orderByAsc(InspectionTemplateItemDO::getSort)); +} +``` + +`InspectionRecordMapper` — add: +```java +default InspectionRecordDO selectLatestByVehicleAndSourceType(Long vehicleId, Integer sourceType) { + return selectOne(new LambdaQueryWrapperX() + .eq(InspectionRecordDO::getVehicleId, vehicleId) + .eq(InspectionRecordDO::getSourceType, sourceType) + .eq(InspectionRecordDO::getStatus, 2) // 已完成 + .orderByDesc(InspectionRecordDO::getId) + .last("LIMIT 1")); +} +``` + +`InspectionRecordItemMapper` — add: +```java +default List selectByRecordId(Long recordId) { + return selectList(new LambdaQueryWrapperX() + .eq(InspectionRecordItemDO::getRecordId, recordId) + .orderByAsc(InspectionRecordItemDO::getId)); +} + +default void deleteByRecordId(Long recordId) { + delete(new LambdaQueryWrapperX() + .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** — Fields: `id`, `category`(@NotBlank), `itemName`(@NotBlank), `itemCode`(@NotBlank), `inputType`, `sort`, `required`. + +**InspectionTemplateRespVO** — Fields: `id`, `code`, `name`, `bizType`, `vehicleType`, `status`, `remark`, `createTime`, `items`(List). + +**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). + +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 convertPage(PageResult page); + InspectionTemplateItemDO convertItem(InspectionTemplateItemVO bean); + List convertItemList(List list); + List convertItemVOList(List list); + InspectionRecordDetailVO convertRecord(InspectionRecordDO bean); + InspectionRecordDetailVO.InspectionRecordItemVO convertRecordItem(InspectionRecordItemDO bean); + List convertRecordItemList(List 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/inputType,result/value 为 null)。 + +**recordCode 生成**: `{prefix}-{yyyyMMdd}-{seq}`,prefix 根据 sourceType 决定(BC/JC/HC)。 + +**cloneRecord**: 查源 record + items → 创建新 record(clonedFromId=源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 { + default PageResult selectPage(VehicleReplacementPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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 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>('/asset/inspection-template/page', { params }); +} +export function getInspectionTemplate(id: number) { + return requestClient.get(`/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(`/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 相关的表格/抽屉代码 +- 在表单末尾添加 `` 组件 +- 如果 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" +```