Compare commits

...

6 Commits

Author SHA1 Message Date
kkfluous
04f0599efa feat(asset): enhance return order with inspection, BPM approval, and event-driven cross-module linkage
- Add sourceType/sourceId/deliveryOrderId to return order table and DO
- Add inspectionRecordId to return order vehicle table and DO
- Add createFromDelivery/createFromReplacement methods to auto-create return orders
- Add startVehicleInspection/completeVehicleInspection for per-vehicle inspection flow
- Add submitApproval/withdrawApproval/updateApprovalStatus for BPM workflow
- Create ReturnApprovedEvent and ReturnOrderBpmListener
- Create DeliveryCompletedEvent for future vehicle status tracking
- Create VehicleStatusEventListener (TODO stubs for vehicle status updates)
- Create ReturnOrderEventListener to auto-create return order on permanent replacement approval
- Add BPM process definitions for replacement (with GM escalation) and return order approval

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:20:25 +08:00
kkfluous
b93ea71174 feat(asset): add vehicle replacement module with BPM approval workflow
Implement complete replacement vehicle management (替换车) supporting
temporary and permanent vehicle replacements under rental contracts,
with BPM-based approval flow, event-driven architecture, and CRUD APIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:11:50 +08:00
kkfluous
46485289a2 feat(asset): integrate inspection records with prepare and delivery order
- Add inspection_record_id column to asset_vehicle_prepare and asset_delivery_order tables
- Add inspectionRecordId field to VehiclePrepareDO, DeliveryOrderDO, and their RespVOs
- Auto-create inspection record from matched template when creating a prepare record
- Auto-complete inspection record when completing a prepare record
- Clone prepare inspection record to delivery order on delivery order creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:06:55 +08:00
kkfluous
1dca703caa feat(asset): add inspection template and record system for vehicle checks
Implement the shared inspection template system backend (Chunk 1) including:
- 4 database tables: template, template_item, record, record_item
- 3 enums: InspectionSourceType, InspectionStatus, InspectionResult
- 4 DO classes, 4 Mapper classes with query methods
- 6 VO classes for request/response
- MapStruct converter for DO/VO conversions
- Template service: CRUD, match by bizType+vehicleType
- Record service: create from template, clone, update items, complete
- 2 REST controllers with permission annotations
- Error codes for inspection and replacement modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:02:19 +08:00
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
kkfluous
072196aad4 docs: add rental full-chain design spec
Covers shared inspection template system, vehicle prepare simplification,
delivery enhancement, replacement vehicle module (new), return order
improvements, BPM approval flows, and event-driven cross-module linkage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:42:06 +08:00
64 changed files with 6185 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,587 @@
# 租赁业务全链路设计规格
## 概述
实现租赁业务全链路(合同 → 备车 → 交车 → 替换车 → 还车生产可用包括模块功能补全、跨模块状态联动、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 | 克隆来源记录IDnullable |
| + 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) | 图片URLJSON数组 |
| + BaseDO | | |
### 克隆流转机制
```
备车完成record status=已完成)
↓ 交车时,查找该 vehicleId 最近的备车 record
↓ InspectionRecordService.cloneRecord(sourceRecordId, DELIVERY, newSourceId)
↓ 生成新 record + 复制所有 itemresult/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` + 全链路测试

View File

@@ -0,0 +1,3 @@
-- 交车单表增加验车记录关联字段
ALTER TABLE asset_delivery_order
ADD COLUMN inspection_record_id bigint DEFAULT NULL COMMENT '关联验车记录';

View File

@@ -0,0 +1,3 @@
-- 备车表增加验车记录关联字段
ALTER TABLE asset_vehicle_prepare
ADD COLUMN inspection_record_id bigint DEFAULT NULL COMMENT '关联验车记录' AFTER defect_photos;

View File

@@ -0,0 +1,9 @@
-- 还车单增强添加来源类型、来源ID、关联交车单ID
ALTER TABLE asset_return_order
ADD COLUMN source_type tinyint DEFAULT NULL COMMENT '来源1=手动 2=替换车触发',
ADD COLUMN source_id bigint DEFAULT NULL COMMENT '来源业务ID',
ADD COLUMN delivery_order_id bigint DEFAULT NULL COMMENT '关联交车单ID';
-- 还车车辆增强:添加验车记录关联
ALTER TABLE asset_return_order_vehicle
ADD COLUMN inspection_record_id bigint DEFAULT NULL COMMENT '关联验车记录';

View File

@@ -0,0 +1,32 @@
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,
`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='替换车申请';

View File

@@ -0,0 +1,88 @@
-- =============================================
-- 验车模板与验车记录表
-- 创建时间: 2026-03-13
-- =============================================
-- 验车模板
CREATE TABLE IF NOT EXISTS `asset_inspection_template` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`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 '车辆类型',
`status` tinyint DEFAULT 1 COMMENT '状态0=禁用 1=启用)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='验车模板';
-- 验车模板检查项
CREATE TABLE IF NOT EXISTS `asset_inspection_template_item` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`template_id` bigint NOT NULL COMMENT '模板ID',
`category` varchar(50) NOT NULL COMMENT '分类',
`item_name` varchar(100) NOT NULL COMMENT '检查项名称',
`item_code` varchar(50) NOT NULL COMMENT '检查项编码',
`input_type` varchar(20) DEFAULT 'checkbox' COMMENT '输入类型',
`sort` int DEFAULT 0 COMMENT '排序',
`required` tinyint DEFAULT 1 COMMENT '是否必填0=否 1=是)',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_template_id` (`template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='验车模板检查项';
-- 验车记录
CREATE TABLE IF NOT EXISTS `asset_inspection_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`record_code` varchar(50) NOT NULL COMMENT '记录编码',
`template_id` bigint NOT NULL COMMENT '模板ID',
`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 DEFAULT 0 COMMENT '状态0=待检 1=检查中 2=已完成)',
`overall_result` tinyint DEFAULT NULL COMMENT '总体结果1=通过 2=不通过 3=不适用)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`cloned_from_id` bigint DEFAULT NULL COMMENT '克隆来源记录ID',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_vehicle_source` (`vehicle_id`, `source_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='验车记录';
-- 验车记录检查项
CREATE TABLE IF NOT EXISTS `asset_inspection_record_item` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`record_id` bigint NOT NULL COMMENT '记录ID',
`item_code` varchar(50) NOT NULL COMMENT '检查项编码',
`category` varchar(50) NOT NULL COMMENT '分类',
`item_name` varchar(100) NOT NULL COMMENT '检查项名称',
`input_type` varchar(20) DEFAULT 'checkbox' COMMENT '输入类型',
`result` tinyint DEFAULT NULL COMMENT '检查结果1=通过 2=不通过 3=不适用)',
`value` varchar(200) DEFAULT NULL COMMENT '检查值',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`image_urls` varchar(2000) DEFAULT NULL COMMENT '图片URL列表',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_record_id` (`record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='验车记录检查项';

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.asset.enums.inspection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 验车结果枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum InspectionResultEnum {
PASS(1, "通过"),
FAIL(2, "不通过"),
NA(3, "不适用");
/**
* 结果
*/
private final Integer result;
/**
* 名称
*/
private final String name;
public static InspectionResultEnum valueOf(Integer result) {
return Arrays.stream(values())
.filter(item -> item.getResult().equals(result))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.asset.enums.inspection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 验车来源类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum InspectionSourceTypeEnum {
PREPARE(1, "备车"),
DELIVERY(2, "交车"),
RETURN(3, "还车");
/**
* 类型
*/
private final Integer type;
/**
* 名称
*/
private final String name;
public static InspectionSourceTypeEnum valueOf(Integer type) {
return Arrays.stream(values())
.filter(item -> item.getType().equals(type))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.asset.enums.inspection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 验车状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum InspectionStatusEnum {
PENDING(0, "待检"),
IN_PROGRESS(1, "检查中"),
COMPLETED(2, "已完成");
/**
* 状态
*/
private final Integer status;
/**
* 名称
*/
private final String name;
public static InspectionStatusEnum valueOf(Integer status) {
return Arrays.stream(values())
.filter(item -> item.getStatus().equals(status))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.asset.enums.replacement;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 替换车状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum ReplacementStatusEnum {
DRAFT(0, "草稿"),
APPROVING(1, "审批中"),
APPROVED(2, "审批通过"),
EXECUTING(3, "执行中"),
COMPLETED(4, "已完成"),
REJECTED(5, "审批驳回"),
WITHDRAWN(6, "已撤回");
/**
* 状态
*/
private final Integer status;
/**
* 名称
*/
private final String name;
public static ReplacementStatusEnum valueOf(Integer status) {
return Arrays.stream(values())
.filter(item -> item.getStatus().equals(status))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.asset.enums.replacement;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 替换车类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum ReplacementTypeEnum {
TEMPORARY(1, "临时替换"),
PERMANENT(2, "永久替换");
/**
* 类型
*/
private final Integer type;
/**
* 名称
*/
private final String name;
public static ReplacementTypeEnum valueOf(Integer type) {
return Arrays.stream(values())
.filter(item -> item.getType().equals(type))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.asset.controller.admin.delivery.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 交车单 Response VO")
@Data
public class DeliveryOrderRespVO {
@Schema(description = "主键")
private Long id;
@Schema(description = "交车单编码")
private String orderCode;
@Schema(description = "交车任务ID")
private Long taskId;
@Schema(description = "任务编码")
private String taskCode;
@Schema(description = "合同ID")
private Long contractId;
@Schema(description = "合同编码")
private String contractCode;
@Schema(description = "项目名称")
private String projectName;
@Schema(description = "客户ID")
private Long customerId;
@Schema(description = "客户名称")
private String customerName;
@Schema(description = "交车日期")
private LocalDateTime deliveryDate;
@Schema(description = "交车人")
private String deliveryPerson;
@Schema(description = "交车地点")
private String deliveryLocation;
@Schema(description = "被授权人姓名")
private String authorizedPersonName;
@Schema(description = "被授权人电话")
private String authorizedPersonPhone;
@Schema(description = "E签宝状态")
private Integer esignStatus;
@Schema(description = "交车照片")
private String deliveryPhotos;
@Schema(description = "司机姓名")
private String driverName;
@Schema(description = "司机身份证")
private String driverIdCard;
@Schema(description = "司机手机号")
private String driverPhone;
@Schema(description = "交检清单JSON")
private String inspectionData;
@Schema(description = "费用信息JSON")
private String costList;
@Schema(description = "关联验车记录ID")
private Long inspectionRecordId;
@Schema(description = "状态")
private Integer status;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "车辆列表")
private List<DeliveryOrderVehicleVO> vehicles;
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionRecordDetailVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionRecordItemUpdateReqVO;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 验车记录 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - 验车记录管理")
@RestController
@RequestMapping("/asset/inspection-record")
@Validated
public class InspectionRecordController {
@Resource
private InspectionRecordService inspectionRecordService;
@GetMapping("/get")
@Operation(summary = "获得验车记录详情")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:inspection-record:query')")
public CommonResult<InspectionRecordDetailVO> getRecordDetail(@RequestParam("id") Long id) {
return success(inspectionRecordService.getRecordDetail(id));
}
@PutMapping("/update-item")
@Operation(summary = "更新验车记录检查项")
@PreAuthorize("@ss.hasPermission('asset:inspection-record:update')")
public CommonResult<Boolean> updateRecordItem(@Valid @RequestBody InspectionRecordItemUpdateReqVO updateReqVO) {
inspectionRecordService.updateRecordItem(updateReqVO);
return success(true);
}
@PostMapping("/complete")
@Operation(summary = "完成验车记录")
@Parameter(name = "id", description = "记录ID", required = true, example = "1")
@Parameter(name = "inspectorName", description = "检查人", required = true, example = "张三")
@PreAuthorize("@ss.hasPermission('asset:inspection-record:update')")
public CommonResult<Boolean> completeRecord(@RequestParam("id") Long id,
@RequestParam("inspectorName") String inspectorName) {
inspectionRecordService.completeRecord(id, inspectorName);
return success(true);
}
}

View File

@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.*;
import cn.iocoder.yudao.module.asset.convert.inspection.InspectionConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateDO;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 验车模板 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - 验车模板管理")
@RestController
@RequestMapping("/asset/inspection-template")
@Validated
public class InspectionTemplateController {
@Resource
private InspectionTemplateService inspectionTemplateService;
@PostMapping("/create")
@Operation(summary = "创建验车模板")
@PreAuthorize("@ss.hasPermission('asset:inspection-template:create')")
public CommonResult<Long> createTemplate(@Valid @RequestBody InspectionTemplateSaveReqVO createReqVO) {
return success(inspectionTemplateService.createTemplate(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新验车模板")
@PreAuthorize("@ss.hasPermission('asset:inspection-template:update')")
public CommonResult<Boolean> updateTemplate(@Valid @RequestBody InspectionTemplateSaveReqVO updateReqVO) {
inspectionTemplateService.updateTemplate(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除验车模板")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:inspection-template:delete')")
public CommonResult<Boolean> deleteTemplate(@RequestParam("id") Long id) {
inspectionTemplateService.deleteTemplate(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得验车模板详情")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:inspection-template:query')")
public CommonResult<InspectionTemplateRespVO> getTemplate(@RequestParam("id") Long id) {
return success(inspectionTemplateService.getTemplate(id));
}
@GetMapping("/page")
@Operation(summary = "获得验车模板分页")
@PreAuthorize("@ss.hasPermission('asset:inspection-template:query')")
public CommonResult<PageResult<InspectionTemplateRespVO>> getTemplatePage(@Valid InspectionTemplatePageReqVO pageReqVO) {
PageResult<InspectionTemplateDO> pageResult = inspectionTemplateService.getTemplatePage(pageReqVO);
return success(InspectionConvert.INSTANCE.convertPage(pageResult));
}
}

View File

@@ -0,0 +1,94 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 验车记录详情 Response VO
*/
@Schema(description = "管理后台 - 验车记录详情 Response VO")
@Data
public class InspectionRecordDetailVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "记录编码", example = "BC-20260313-001")
private String recordCode;
@Schema(description = "模板ID", example = "1")
private Long templateId;
@Schema(description = "来源类型1=备车 2=交车 3=还车)", example = "1")
private Integer sourceType;
@Schema(description = "来源ID", example = "1")
private Long sourceId;
@Schema(description = "车辆ID", example = "1")
private Long vehicleId;
@Schema(description = "检查人", example = "张三")
private String inspectorName;
@Schema(description = "检查时间")
private LocalDateTime inspectionTime;
@Schema(description = "状态0=待检 1=检查中 2=已完成)", example = "0")
private Integer status;
@Schema(description = "总体结果1=通过 2=不通过 3=不适用)", example = "1")
private Integer overallResult;
@Schema(description = "备注")
private String remark;
@Schema(description = "克隆来源记录ID", example = "1")
private Long clonedFromId;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "检查项列表")
private List<InspectionRecordItemVO> items;
/**
* 验车记录检查项 VO
*/
@Schema(description = "验车记录检查项 VO")
@Data
public static class InspectionRecordItemVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "检查项编码", example = "BODY_PAINT")
private String itemCode;
@Schema(description = "分类", example = "外观检查")
private String category;
@Schema(description = "检查项名称", example = "车身漆面")
private String itemName;
@Schema(description = "输入类型", example = "checkbox")
private String inputType;
@Schema(description = "检查结果1=通过 2=不通过 3=不适用)", example = "1")
private Integer result;
@Schema(description = "检查值", example = "正常")
private String value;
@Schema(description = "备注")
private String remark;
@Schema(description = "图片URL列表")
private String imageUrls;
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
/**
* 验车记录检查项更新 Request VO
*/
@Schema(description = "管理后台 - 验车记录检查项更新 Request VO")
@Data
public class InspectionRecordItemUpdateReqVO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "主键ID不能为空")
private Long id;
@Schema(description = "检查结果1=通过 2=不通过 3=不适用)", example = "1")
private Integer result;
@Schema(description = "检查值", example = "正常")
private String value;
@Schema(description = "备注")
private String remark;
@Schema(description = "图片URL列表")
private String imageUrls;
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 验车模板检查项 VO
*/
@Schema(description = "管理后台 - 验车模板检查项 VO")
@Data
public class InspectionTemplateItemVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "外观检查")
@NotBlank(message = "分类不能为空")
private String category;
@Schema(description = "检查项名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "车身漆面")
@NotBlank(message = "检查项名称不能为空")
private String itemName;
@Schema(description = "检查项编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "BODY_PAINT")
@NotBlank(message = "检查项编码不能为空")
private String itemCode;
@Schema(description = "输入类型", example = "checkbox")
private String inputType;
@Schema(description = "排序", example = "0")
private Integer sort;
@Schema(description = "是否必填0=否 1=是)", example = "1")
private Integer required;
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 验车模板分页查询 Request VO
*/
@Schema(description = "管理后台 - 验车模板分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class InspectionTemplatePageReqVO extends PageParam {
@Schema(description = "模板编码", example = "TPL-BC-001")
private String code;
@Schema(description = "模板名称", example = "备车检查模板")
private String name;
@Schema(description = "业务类型1=备车 2=交车 3=还车)", example = "1")
private Integer bizType;
@Schema(description = "车辆类型", example = "重卡")
private String vehicleType;
@Schema(description = "状态0=禁用 1=启用)", example = "1")
private Integer status;
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 验车模板 Response VO
*/
@Schema(description = "管理后台 - 验车模板 Response VO")
@Data
public class InspectionTemplateRespVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "模板编码", example = "TPL-BC-001")
private String code;
@Schema(description = "模板名称", example = "备车检查模板")
private String name;
@Schema(description = "业务类型1=备车 2=交车 3=还车)", example = "1")
private Integer bizType;
@Schema(description = "车辆类型", example = "重卡")
private String vehicleType;
@Schema(description = "状态0=禁用 1=启用)", example = "1")
private Integer status;
@Schema(description = "备注", example = "备车检查模板")
private String remark;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "检查项列表")
private List<InspectionTemplateItemVO> items;
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.asset.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 验车模板创建/更新 Request VO
*/
@Schema(description = "管理后台 - 验车模板创建/更新 Request VO")
@Data
public class InspectionTemplateSaveReqVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "TPL-BC-001")
@NotBlank(message = "模板编码不能为空")
private String code;
@Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "备车检查模板")
@NotBlank(message = "模板名称不能为空")
private String name;
@Schema(description = "业务类型1=备车 2=交车 3=还车)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "业务类型不能为空")
private Integer bizType;
@Schema(description = "车辆类型", example = "重卡")
private String vehicleType;
@Schema(description = "状态0=禁用 1=启用)", example = "1")
private Integer status;
@Schema(description = "备注", example = "备车检查模板")
private String remark;
@Schema(description = "检查项列表")
@Valid
private List<InspectionTemplateItemVO> items;
}

View File

@@ -0,0 +1,106 @@
package cn.iocoder.yudao.module.asset.controller.admin.prepare.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 备车记录 Response VO")
@Data
public class VehiclePrepareRespVO {
@Schema(description = "主键")
private Long id;
@Schema(description = "车辆ID")
private Long vehicleId;
@Schema(description = "车牌号")
private String plateNo;
@Schema(description = "VIN码")
private String vin;
@Schema(description = "车型ID")
private Long vehicleModelId;
@Schema(description = "品牌")
private String brand;
@Schema(description = "型号")
private String model;
@Schema(description = "车辆类型")
private String vehicleType;
@Schema(description = "停车场")
private String parkingLot;
@Schema(description = "合同ID")
private Long contractId;
@Schema(description = "合同编码")
private String contractCode;
@Schema(description = "整备类型")
private String preparationType;
@Schema(description = "里程(km)")
private Integer mileage;
@Schema(description = "剩余氢量")
private BigDecimal hydrogenRemaining;
@Schema(description = "氢量单位")
private String hydrogenUnit;
@Schema(description = "剩余电量(%)")
private BigDecimal batteryRemaining;
@Schema(description = "是否有车身广告")
private Boolean hasBodyAd;
@Schema(description = "广告照片")
private String bodyAdPhotos;
@Schema(description = "放大字照片")
private String enlargedTextPhoto;
@Schema(description = "是否有尾板")
private Boolean hasTailLift;
@Schema(description = "备胎胎纹深度(mm)")
private BigDecimal spareTireDepth;
@Schema(description = "备胎照片")
private String spareTirePhoto;
@Schema(description = "挂车牌号")
private String trailerPlateNo;
@Schema(description = "瑕疵照片")
private String defectPhotos;
@Schema(description = "关联验车记录ID")
private Long inspectionRecordId;
@Schema(description = "检查清单JSON")
private String checkList;
@Schema(description = "备注")
private String remark;
@Schema(description = "状态")
private Integer status;
@Schema(description = "完成时间")
private LocalDateTime completeTime;
@Schema(description = "创建者")
private String creator;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.asset.controller.admin.replacement;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.*;
import cn.iocoder.yudao.module.asset.convert.replacement.VehicleReplacementConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.replacement.VehicleReplacementDO;
import cn.iocoder.yudao.module.asset.service.replacement.VehicleReplacementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 替换车申请 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - 替换车管理")
@RestController
@RequestMapping("/asset/vehicle-replacement")
@Validated
public class VehicleReplacementController {
@Resource
private VehicleReplacementService replacementService;
@PostMapping("/create")
@Operation(summary = "创建替换车申请")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:create')")
public CommonResult<Long> createReplacement(@Valid @RequestBody VehicleReplacementSaveReqVO createReqVO) {
return success(replacementService.createReplacement(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新替换车申请")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:update')")
public CommonResult<Boolean> updateReplacement(@Valid @RequestBody VehicleReplacementSaveReqVO updateReqVO) {
replacementService.updateReplacement(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除替换车申请")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:delete')")
public CommonResult<Boolean> deleteReplacement(@RequestParam("id") Long id) {
replacementService.deleteReplacement(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得替换车申请详情")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:query')")
public CommonResult<VehicleReplacementRespVO> getReplacement(@RequestParam("id") Long id) {
VehicleReplacementDO replacement = replacementService.getReplacement(id);
return success(VehicleReplacementConvert.INSTANCE.convert(replacement));
}
@GetMapping("/page")
@Operation(summary = "获得替换车申请分页")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:query')")
public CommonResult<PageResult<VehicleReplacementRespVO>> getReplacementPage(@Valid VehicleReplacementPageReqVO pageReqVO) {
PageResult<VehicleReplacementDO> pageResult = replacementService.getReplacementPage(pageReqVO);
return success(VehicleReplacementConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/submit")
@Operation(summary = "提交替换车审批")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:update')")
public CommonResult<String> submitApproval(@RequestParam("id") Long id) {
return success(replacementService.submitApproval(id));
}
@PostMapping("/withdraw")
@Operation(summary = "撤回替换车审批")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:update')")
public CommonResult<Boolean> withdrawApproval(@RequestParam("id") Long id) {
replacementService.withdrawApproval(id);
return success(true);
}
@PostMapping("/confirm-return")
@Operation(summary = "确认换回(临时替换)")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:update')")
public CommonResult<Boolean> confirmReturn(@RequestParam("id") Long id) {
replacementService.confirmReturn(id);
return success(true);
}
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.asset.controller.admin.replacement.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 替换车申请分页查询 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - 替换车申请分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class VehicleReplacementPageReqVO extends PageParam {
@Schema(description = "替换单编码", example = "TH-20260313-001")
private String replacementCode;
@Schema(description = "替换类型1=临时 2=永久)", example = "1")
private Integer replacementType;
@Schema(description = "合同ID", example = "1")
private Long contractId;
@Schema(description = "客户名称(模糊搜索)", example = "上海某某科技")
private String customerName;
@Schema(description = "状态0=草稿 1=审批中 2=审批通过 3=执行中 4=已完成 5=审批驳回 6=已撤回)", example = "0")
private Integer status;
}

View File

@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.asset.controller.admin.replacement.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 替换车申请 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - 替换车申请 Response VO")
@Data
public class VehicleReplacementRespVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "替换单编码", example = "TH-20260313-001")
private String replacementCode;
@Schema(description = "替换类型1=临时 2=永久)", example = "1")
private Integer replacementType;
@Schema(description = "合同ID", example = "1")
private Long contractId;
@Schema(description = "合同编码", example = "HT-2026-0001")
private String contractCode;
@Schema(description = "客户ID", example = "1")
private Long customerId;
@Schema(description = "客户名称", example = "上海某某科技")
private String customerName;
@Schema(description = "来源交车单ID", example = "1")
private Long deliveryOrderId;
@Schema(description = "原车辆ID", example = "1")
private Long originalVehicleId;
@Schema(description = "原车牌号", example = "沪A12345")
private String originalPlateNo;
@Schema(description = "原车架号", example = "LVHRU1867N5012345")
private String originalVin;
@Schema(description = "新车辆ID", example = "2")
private Long newVehicleId;
@Schema(description = "新车牌号", example = "沪B67890")
private String newPlateNo;
@Schema(description = "新车架号", example = "LVHRU1867N5067890")
private String newVin;
@Schema(description = "替换原因", example = "车辆故障需维修")
private String replacementReason;
@Schema(description = "预计替换日期", example = "2026-03-15")
private LocalDate expectedDate;
@Schema(description = "实际替换日期", example = "2026-03-15")
private LocalDate actualDate;
@Schema(description = "临时替换预计归还日期", example = "2026-04-15")
private LocalDate returnDate;
@Schema(description = "实际归还日期", example = "2026-04-15")
private LocalDate actualReturnDate;
@Schema(description = "状态0=草稿 1=审批中 2=审批通过 3=执行中 4=已完成 5=审批驳回 6=已撤回)", example = "0")
private Integer status;
@Schema(description = "审批状态", example = "0")
private Integer approvalStatus;
@Schema(description = "BPM流程实例ID", example = "123456")
private String bpmInstanceId;
@Schema(description = "备注", example = "紧急替换")
private String remark;
@Schema(description = "创建时间", example = "2026-03-13 10:00:00")
private LocalDateTime createTime;
@Schema(description = "创建者", example = "admin")
private String creator;
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.asset.controller.admin.replacement.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
* 替换车申请创建/更新 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - 替换车申请创建/更新 Request VO")
@Data
public class VehicleReplacementSaveReqVO {
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "替换类型1=临时 2=永久)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "替换类型不能为空")
private Integer replacementType;
@Schema(description = "合同ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "合同不能为空")
private Long contractId;
@Schema(description = "合同编码", example = "HT-2026-0001")
private String contractCode;
@Schema(description = "客户ID", example = "1")
private Long customerId;
@Schema(description = "客户名称", example = "上海某某科技")
private String customerName;
@Schema(description = "来源交车单ID", example = "1")
private Long deliveryOrderId;
@Schema(description = "原车辆ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "原车辆不能为空")
private Long originalVehicleId;
@Schema(description = "原车牌号", example = "沪A12345")
private String originalPlateNo;
@Schema(description = "原车架号", example = "LVHRU1867N5012345")
private String originalVin;
@Schema(description = "新车辆ID", example = "2")
private Long newVehicleId;
@Schema(description = "新车牌号", example = "沪B67890")
private String newPlateNo;
@Schema(description = "新车架号", example = "LVHRU1867N5067890")
private String newVin;
@Schema(description = "替换原因", example = "车辆故障需维修")
private String replacementReason;
@Schema(description = "预计替换日期", example = "2026-03-15")
private LocalDate expectedDate;
@Schema(description = "临时替换预计归还日期", example = "2026-04-15")
private LocalDate returnDate;
@Schema(description = "备注", example = "紧急替换")
private String remark;
}

View File

@@ -0,0 +1,132 @@
package cn.iocoder.yudao.module.asset.controller.admin.returnorder;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.asset.controller.admin.returnorder.vo.*;
import cn.iocoder.yudao.module.asset.dal.dataobject.returnorder.ReturnOrderDO;
import cn.iocoder.yudao.module.asset.service.returnorder.ReturnOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 还车管理")
@RestController
@RequestMapping("/asset/return-order")
@Validated
public class ReturnOrderController {
@Resource
private ReturnOrderService returnOrderService;
@PostMapping("/create")
@Operation(summary = "创建还车单")
@PreAuthorize("@ss.hasPermission('asset:return-order:create')")
public CommonResult<Long> createReturnOrder(@Valid @RequestBody ReturnOrderSaveReqVO createReqVO) {
return success(returnOrderService.createReturnOrder(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新还车单")
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Boolean> updateReturnOrder(@Valid @RequestBody ReturnOrderSaveReqVO updateReqVO) {
returnOrderService.updateReturnOrder(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除还车单")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:delete')")
public CommonResult<Boolean> deleteReturnOrder(@RequestParam("id") Long id) {
returnOrderService.deleteReturnOrder(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得还车单详情")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:query')")
public CommonResult<ReturnOrderRespVO> getReturnOrder(@RequestParam("id") Long id) {
return success(returnOrderService.getReturnOrderDetail(id));
}
@GetMapping("/page")
@Operation(summary = "获得还车单分页")
@PreAuthorize("@ss.hasPermission('asset:return-order:query')")
public CommonResult<PageResult<ReturnOrderRespVO>> getReturnOrderPage(@Valid ReturnOrderPageReqVO pageReqVO) {
PageResult<ReturnOrderDO> pageResult = returnOrderService.getReturnOrderPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ReturnOrderRespVO.class));
}
@PutMapping("/complete-inspection")
@Operation(summary = "完成验车")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Boolean> completeInspection(@RequestParam("id") Long id) {
returnOrderService.completeInspection(id);
return success(true);
}
@PutMapping("/settle")
@Operation(summary = "结算还车单")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Boolean> settleReturnOrder(@RequestParam("id") Long id) {
returnOrderService.settleReturnOrder(id);
return success(true);
}
@PostMapping("/create-from-delivery")
@Operation(summary = "从交车单创建还车单")
@PreAuthorize("@ss.hasPermission('asset:return-order:create')")
public CommonResult<Long> createFromDelivery(@RequestParam("deliveryOrderId") Long deliveryOrderId,
@RequestParam("vehicleIds") List<Long> vehicleIds) {
return success(returnOrderService.createFromDelivery(deliveryOrderId, vehicleIds));
}
@PostMapping("/start-inspection")
@Operation(summary = "开始单车验车")
@Parameter(name = "returnOrderVehicleId", description = "还车车辆ID", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Long> startVehicleInspection(@RequestParam("returnOrderVehicleId") Long returnOrderVehicleId) {
return success(returnOrderService.startVehicleInspection(returnOrderVehicleId));
}
@PostMapping("/complete-vehicle-inspection")
@Operation(summary = "完成单车验车")
@Parameter(name = "returnOrderVehicleId", description = "还车车辆ID", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Boolean> completeVehicleInspection(@RequestParam("returnOrderVehicleId") Long returnOrderVehicleId) {
returnOrderService.completeVehicleInspection(returnOrderVehicleId);
return success(true);
}
@PostMapping("/submit")
@Operation(summary = "提交审批")
@Parameter(name = "id", description = "还车单ID", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Boolean> submitApproval(@RequestParam("id") Long id) {
returnOrderService.submitApproval(id);
return success(true);
}
@PostMapping("/withdraw")
@Operation(summary = "撤回审批")
@Parameter(name = "id", description = "还车单ID", required = true)
@PreAuthorize("@ss.hasPermission('asset:return-order:update')")
public CommonResult<Boolean> withdrawApproval(@RequestParam("id") Long id) {
returnOrderService.withdrawApproval(id);
return success(true);
}
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.asset.convert.inspection;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.*;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.*;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 验车模板/记录 Convert
*
* @author 芋道源码
*/
@Mapper
public interface InspectionConvert {
InspectionConvert INSTANCE = Mappers.getMapper(InspectionConvert.class);
InspectionTemplateDO convert(InspectionTemplateSaveReqVO bean);
InspectionTemplateRespVO convert(InspectionTemplateDO bean);
PageResult<InspectionTemplateRespVO> convertPage(PageResult<InspectionTemplateDO> page);
List<InspectionTemplateItemDO> convertItemList(List<InspectionTemplateItemVO> list);
InspectionTemplateItemDO convertItem(InspectionTemplateItemVO bean);
List<InspectionTemplateItemVO> convertItemVOList(List<InspectionTemplateItemDO> list);
InspectionTemplateItemVO convertItemVO(InspectionTemplateItemDO bean);
InspectionRecordDetailVO convertRecordDetail(InspectionRecordDO bean);
List<InspectionRecordDetailVO.InspectionRecordItemVO> convertRecordItemVOList(List<InspectionRecordItemDO> list);
InspectionRecordDetailVO.InspectionRecordItemVO convertRecordItemVO(InspectionRecordItemDO bean);
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.asset.convert.replacement;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementSaveReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.replacement.VehicleReplacementDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 替换车申请 Convert
*
* @author 芋道源码
*/
@Mapper
public interface VehicleReplacementConvert {
VehicleReplacementConvert INSTANCE = Mappers.getMapper(VehicleReplacementConvert.class);
VehicleReplacementDO convert(VehicleReplacementSaveReqVO bean);
VehicleReplacementRespVO convert(VehicleReplacementDO bean);
PageResult<VehicleReplacementRespVO> convertPage(PageResult<VehicleReplacementDO> page);
}

View File

@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.delivery;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 交车单 DO
*/
@TableName("asset_delivery_order")
@KeySequence("asset_delivery_order_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeliveryOrderDO extends BaseDO {
@TableId
private Long id;
private String orderCode;
private Long taskId;
private String taskCode;
private Long contractId;
private String contractCode;
private String projectName;
private Long customerId;
private String customerName;
private LocalDateTime deliveryDate;
private String deliveryPerson;
private String deliveryLocation;
private Long authorizedPersonId;
private String authorizedPersonName;
private String authorizedPersonPhone;
private String authorizedPersonIdCard;
private String esignFlowId;
private Integer esignStatus;
private String deliveryPhotos;
private String driverName;
private String driverIdCard;
private String driverPhone;
private String inspectionData;
private String costList;
/**
* 关联验车记录ID
*/
private Long inspectionRecordId;
/**
* 状态0=待完成 1=已完成)
*/
private Integer status;
}

View File

@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.inspection;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 验车记录 DO
*
* @author 芋道源码
*/
@TableName("asset_inspection_record")
@KeySequence("asset_inspection_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InspectionRecordDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 记录编码
*/
private String recordCode;
/**
* 模板ID
*/
private Long templateId;
/**
* 来源类型1=备车 2=交车 3=还车)
*/
private Integer sourceType;
/**
* 来源ID
*/
private Long sourceId;
/**
* 车辆ID
*/
private Long vehicleId;
/**
* 检查人
*/
private String inspectorName;
/**
* 检查时间
*/
private LocalDateTime inspectionTime;
/**
* 状态0=待检 1=检查中 2=已完成)
*/
private Integer status;
/**
* 总体结果1=通过 2=不通过 3=不适用)
*/
private Integer overallResult;
/**
* 备注
*/
private String remark;
/**
* 克隆来源记录ID
*/
private Long clonedFromId;
}

View File

@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.inspection;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 验车记录检查项 DO
*
* @author 芋道源码
*/
@TableName("asset_inspection_record_item")
@KeySequence("asset_inspection_record_item_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InspectionRecordItemDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 记录ID
*/
private Long recordId;
/**
* 检查项编码
*/
private String itemCode;
/**
* 分类
*/
private String category;
/**
* 检查项名称
*/
private String itemName;
/**
* 输入类型
*/
private String inputType;
/**
* 检查结果1=通过 2=不通过 3=不适用)
*/
private Integer result;
/**
* 检查值
*/
private String value;
/**
* 备注
*/
private String remark;
/**
* 图片URL列表
*/
private String imageUrls;
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.inspection;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 验车模板 DO
*
* @author 芋道源码
*/
@TableName("asset_inspection_template")
@KeySequence("asset_inspection_template_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InspectionTemplateDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 模板编码
*/
private String code;
/**
* 模板名称
*/
private String name;
/**
* 业务类型1=备车 2=交车 3=还车)
*/
private Integer bizType;
/**
* 车辆类型
*/
private String vehicleType;
/**
* 状态0=禁用 1=启用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.inspection;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 验车模板检查项 DO
*
* @author 芋道源码
*/
@TableName("asset_inspection_template_item")
@KeySequence("asset_inspection_template_item_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InspectionTemplateItemDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 模板ID
*/
private Long templateId;
/**
* 分类
*/
private String category;
/**
* 检查项名称
*/
private String itemName;
/**
* 检查项编码
*/
private String itemCode;
/**
* 输入类型
*/
private String inputType;
/**
* 排序
*/
private Integer sort;
/**
* 是否必填0=否 1=是)
*/
private Integer required;
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.prepare;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 备车记录 DO
*/
@TableName("asset_vehicle_prepare")
@KeySequence("asset_vehicle_prepare_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VehiclePrepareDO extends BaseDO {
@TableId
private Long id;
private Long vehicleId;
private String plateNo;
private String vin;
private Long vehicleModelId;
private String brand;
private String model;
private String vehicleType;
private String parkingLot;
private Long contractId;
private String contractCode;
private String preparationType;
private Integer mileage;
private BigDecimal hydrogenRemaining;
private String hydrogenUnit;
private BigDecimal batteryRemaining;
private Boolean hasBodyAd;
private String bodyAdPhotos;
private String enlargedTextPhoto;
private Boolean hasTailLift;
private BigDecimal spareTireDepth;
private String spareTirePhoto;
private String trailerPlateNo;
private String defectPhotos;
/**
* 关联验车记录ID
*/
private Long inspectionRecordId;
private String checkList;
private String remark;
/**
* 状态0=待提交 1=已完成)
*/
private Integer status;
private LocalDateTime completeTime;
}

View File

@@ -0,0 +1,142 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.replacement;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
* 替换车申请 DO
*
* @author 芋道源码
*/
@TableName("asset_vehicle_replacement")
@KeySequence("asset_vehicle_replacement_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VehicleReplacementDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 替换单编码
*/
private String replacementCode;
/**
* 替换类型1=临时 2=永久)
*/
private Integer replacementType;
/**
* 合同ID
*/
private Long contractId;
/**
* 合同编码(冗余)
*/
private String contractCode;
/**
* 客户ID
*/
private Long customerId;
/**
* 客户名称(冗余)
*/
private String customerName;
/**
* 来源交车单ID
*/
private Long deliveryOrderId;
/**
* 原车辆ID
*/
private Long originalVehicleId;
/**
* 原车牌号(冗余)
*/
private String originalPlateNo;
/**
* 原车架号(冗余)
*/
private String originalVin;
/**
* 新车辆ID
*/
private Long newVehicleId;
/**
* 新车牌号(冗余)
*/
private String newPlateNo;
/**
* 新车架号(冗余)
*/
private String newVin;
/**
* 替换原因
*/
private String replacementReason;
/**
* 预计替换日期
*/
private LocalDate expectedDate;
/**
* 实际替换日期
*/
private LocalDate actualDate;
/**
* 临时替换预计归还日期
*/
private LocalDate returnDate;
/**
* 实际归还日期
*/
private LocalDate actualReturnDate;
/**
* 状态0=草稿 1=审批中 2=审批通过 3=执行中 4=已完成 5=审批驳回 6=已撤回)
*/
private Integer status;
/**
* 审批状态
*/
private Integer approvalStatus;
/**
* BPM流程实例ID
*/
private String bpmInstanceId;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.returnorder;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 还车单 DO
*/
@TableName("asset_return_order")
@KeySequence("asset_return_order_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReturnOrderDO extends BaseDO {
@TableId
private Long id;
private String orderCode;
private Long contractId;
private String contractCode;
private String projectName;
private Long customerId;
private String customerName;
private LocalDateTime returnDate;
private String returnPerson;
private String returnLocation;
private String returnReason;
private String returnReasonDesc;
private BigDecimal totalRefundAmount;
private BigDecimal depositRefund;
private BigDecimal hydrogenRefund;
private BigDecimal otherCharges;
private String returnPhotos;
/**
* 状态0=待验车 1=验车完成 2=已结算)
*/
private Integer status;
private Integer approvalStatus;
private String bpmInstanceId;
/**
* 来源1=手动 2=替换车触发
*/
private Integer sourceType;
/**
* 来源业务ID
*/
private Long sourceId;
/**
* 关联交车单ID
*/
private Long deliveryOrderId;
}

View File

@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.returnorder;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
* 还车车辆 DO
*/
@TableName("asset_return_order_vehicle")
@KeySequence("asset_return_order_vehicle_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReturnOrderVehicleDO extends BaseDO {
@TableId
private Long id;
private Long returnOrderId;
private Long vehicleId;
private String plateNo;
private String vin;
private String brand;
private String model;
private Integer returnMileage;
private BigDecimal returnHydrogenLevel;
private BigDecimal deliveryHydrogenLevel;
private BigDecimal hydrogenDiff;
private BigDecimal hydrogenUnitPrice;
private BigDecimal hydrogenRefundAmount;
private String checkList;
private String defectPhotos;
private BigDecimal vehicleDamageFee;
private BigDecimal toolDamageFee;
private BigDecimal unpaidMaintenanceFee;
private BigDecimal unpaidRepairFee;
private BigDecimal violationFee;
private BigDecimal otherFee;
/**
* 关联验车记录ID
*/
private Long inspectionRecordId;
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.asset.dal.mysql.inspection;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 验车记录检查项 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InspectionRecordItemMapper extends BaseMapperX<InspectionRecordItemDO> {
default List<InspectionRecordItemDO> selectByRecordId(Long recordId) {
return selectList(new LambdaQueryWrapperX<InspectionRecordItemDO>()
.eq(InspectionRecordItemDO::getRecordId, recordId)
.orderByAsc(InspectionRecordItemDO::getId));
}
default int deleteByRecordId(Long recordId) {
return delete(new LambdaQueryWrapperX<InspectionRecordItemDO>()
.eq(InspectionRecordItemDO::getRecordId, recordId));
}
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.asset.dal.mysql.inspection;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 验车记录 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InspectionRecordMapper extends BaseMapperX<InspectionRecordDO> {
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"));
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.asset.dal.mysql.inspection;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 验车模板检查项 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InspectionTemplateItemMapper extends BaseMapperX<InspectionTemplateItemDO> {
default List<InspectionTemplateItemDO> selectByTemplateId(Long templateId) {
return selectList(new LambdaQueryWrapperX<InspectionTemplateItemDO>()
.eq(InspectionTemplateItemDO::getTemplateId, templateId)
.orderByAsc(InspectionTemplateItemDO::getSort));
}
default int deleteByTemplateId(Long templateId) {
return delete(new LambdaQueryWrapperX<InspectionTemplateItemDO>()
.eq(InspectionTemplateItemDO::getTemplateId, templateId));
}
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.asset.dal.mysql.inspection;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplatePageReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 验车模板 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InspectionTemplateMapper extends BaseMapperX<InspectionTemplateDO> {
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)
.last("LIMIT 1"));
}
default InspectionTemplateDO selectDefaultByBizType(Integer bizType) {
return selectOne(new LambdaQueryWrapperX<InspectionTemplateDO>()
.eq(InspectionTemplateDO::getBizType, bizType)
.isNull(InspectionTemplateDO::getVehicleType)
.eq(InspectionTemplateDO::getStatus, 1)
.last("LIMIT 1"));
}
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.asset.dal.mysql.replacement;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementPageReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.replacement.VehicleReplacementDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 替换车申请 Mapper
*
* @author 芋道源码
*/
@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));
}
@Select("SELECT replacement_code FROM asset_vehicle_replacement WHERE replacement_code LIKE CONCAT(#{prefix}, '%') AND deleted = 0 ORDER BY replacement_code DESC LIMIT 1")
String selectMaxReplacementCodeByPrefix(@Param("prefix") String prefix);
}

View File

@@ -23,4 +23,59 @@ public interface ErrorCodeConstants {
// ========== 供应商管理 1-008-004-000 ==========
ErrorCode SUPPLIER_NOT_EXISTS = new ErrorCode(1_008_004_000, "供应商不存在");
// ========== 车辆租赁合同管理 1-008-005-000 ==========
ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_008_005_000, "合同不存在");
ErrorCode CONTRACT_CODE_DUPLICATE = new ErrorCode(1_008_005_001, "合同编码已存在");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_UPDATE = new ErrorCode(1_008_005_002, "当前合同状态不允许修改");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_005_003, "当前合同状态不允许删除");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_SUBMIT = new ErrorCode(1_008_005_004, "当前合同状态不允许提交审批");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_WITHDRAW = new ErrorCode(1_008_005_005, "当前合同状态不允许撤回");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_TERMINATE = new ErrorCode(1_008_005_006, "当前合同状态不允许终止");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_RENEW = new ErrorCode(1_008_005_007, "当前合同状态不允许续签");
ErrorCode CONTRACT_END_DATE_BEFORE_START_DATE = new ErrorCode(1_008_005_008, "合同结束日期不能早于开始日期");
ErrorCode CONTRACT_VEHICLE_TIME_CONFLICT = new ErrorCode(1_008_005_009, "该车辆在此时间段内已有有效合同");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_CONVERT = new ErrorCode(1_008_005_010, "当前合同状态不允许变更");
ErrorCode CONTRACT_STATUS_NOT_ALLOW_ADD_VEHICLE = new ErrorCode(1_008_005_011, "当前合同状态不允许新增车辆");
ErrorCode CONTRACT_TYPE_NOT_TRIAL = new ErrorCode(1_008_005_012, "只有试用合同才能转正式");
// ========== 车辆登记 1-008-006-000 ==========
ErrorCode VEHICLE_REGISTRATION_NOT_EXISTS = new ErrorCode(1_008_006_000, "车辆登记不存在");
// ========== 备车管理 1-008-007-000 ==========
ErrorCode VEHICLE_PREPARE_NOT_EXISTS = new ErrorCode(1_008_007_000, "备车记录不存在");
ErrorCode VEHICLE_PREPARE_ALREADY_COMPLETED = new ErrorCode(1_008_007_001, "备车记录已完成,不允许修改");
// ========== 交车任务管理 1-008-008-000 ==========
ErrorCode DELIVERY_TASK_NOT_EXISTS = new ErrorCode(1_008_008_000, "交车任务不存在");
ErrorCode DELIVERY_TASK_ALREADY_DELIVERED = new ErrorCode(1_008_008_001, "交车任务已完成交车");
ErrorCode DELIVERY_TASK_SUSPENDED = new ErrorCode(1_008_008_002, "交车任务已挂起");
// ========== 交车单管理 1-008-009-000 ==========
ErrorCode DELIVERY_ORDER_NOT_EXISTS = new ErrorCode(1_008_009_000, "交车单不存在");
ErrorCode DELIVERY_ORDER_ALREADY_COMPLETED = new ErrorCode(1_008_009_001, "交车单已完成");
// ========== 还车单管理 1-008-010-000 ==========
ErrorCode RETURN_ORDER_NOT_EXISTS = new ErrorCode(1_008_010_000, "还车单不存在");
ErrorCode RETURN_ORDER_ALREADY_SETTLED = new ErrorCode(1_008_010_001, "还车单已结算");
ErrorCode RETURN_ORDER_VEHICLE_NOT_EXISTS = new ErrorCode(1_008_010_002, "还车车辆不存在");
ErrorCode RETURN_ORDER_STATUS_NOT_ALLOW_SUBMIT = new ErrorCode(1_008_010_003, "当前状态不允许提交审批");
ErrorCode RETURN_ORDER_STATUS_NOT_ALLOW_WITHDRAW = new ErrorCode(1_008_010_004, "当前状态不允许撤回");
ErrorCode RETURN_ORDER_INSPECTION_ALREADY_STARTED = new ErrorCode(1_008_010_005, "该车辆验车已开始");
ErrorCode RETURN_ORDER_INSPECTION_NOT_STARTED = new ErrorCode(1_008_010_006, "该车辆验车未开始");
// ========== 验车模板管理 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, "当前状态不允许确认换回");
}

View File

@@ -0,0 +1,182 @@
package cn.iocoder.yudao.module.asset.service.delivery;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.asset.controller.admin.delivery.vo.*;
import cn.iocoder.yudao.module.asset.dal.dataobject.delivery.*;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordDO;
import cn.iocoder.yudao.module.asset.dal.mysql.delivery.*;
import cn.iocoder.yudao.module.asset.enums.inspection.InspectionSourceTypeEnum;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionRecordService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.asset.enums.ErrorCodeConstants.*;
@Service
@Validated
public class DeliveryOrderServiceImpl implements DeliveryOrderService {
@Resource
private DeliveryOrderMapper deliveryOrderMapper;
@Resource
private DeliveryOrderVehicleMapper deliveryOrderVehicleMapper;
@Resource
private DeliveryTaskMapper deliveryTaskMapper;
@Resource
private DeliveryTaskVehicleMapper deliveryTaskVehicleMapper;
@Resource
private InspectionRecordService inspectionRecordService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createDeliveryOrder(DeliveryOrderSaveReqVO createReqVO) {
// Get task info
DeliveryTaskDO task = deliveryTaskMapper.selectById(createReqVO.getTaskId());
if (task == null) {
throw exception(DELIVERY_TASK_NOT_EXISTS);
}
if (task.getTaskStatus().equals(1)) {
throw exception(DELIVERY_TASK_SUSPENDED);
}
// Generate order code
String orderCode = task.getTaskCode() + "O0001";
DeliveryOrderDO order = BeanUtils.toBean(createReqVO, DeliveryOrderDO.class);
order.setOrderCode(orderCode);
order.setTaskCode(task.getTaskCode());
order.setContractId(task.getContractId());
order.setContractCode(task.getContractCode());
order.setProjectName(task.getProjectName());
order.setCustomerId(task.getCustomerId());
order.setCustomerName(task.getCustomerName());
order.setStatus(0); // pending
order.setEsignStatus(0);
deliveryOrderMapper.insert(order);
// Insert vehicles
if (createReqVO.getVehicles() != null) {
for (DeliveryOrderVehicleVO vehicleVO : createReqVO.getVehicles()) {
DeliveryOrderVehicleDO vehicle = BeanUtils.toBean(vehicleVO, DeliveryOrderVehicleDO.class);
vehicle.setOrderId(order.getId());
deliveryOrderVehicleMapper.insert(vehicle);
}
// 从第一辆车的备车验车记录克隆交车验车记录
DeliveryOrderVehicleVO firstVehicle = createReqVO.getVehicles().get(0);
if (firstVehicle.getVehicleId() != null) {
InspectionRecordDO prepareRecord = inspectionRecordService.getLatestRecord(
firstVehicle.getVehicleId(), InspectionSourceTypeEnum.PREPARE.getType());
if (prepareRecord != null) {
Long inspectionRecordId = inspectionRecordService.cloneRecord(
prepareRecord.getId(), InspectionSourceTypeEnum.DELIVERY.getType(), order.getId());
deliveryOrderMapper.updateById(DeliveryOrderDO.builder()
.id(order.getId()).inspectionRecordId(inspectionRecordId).build());
}
}
}
return order.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateDeliveryOrder(DeliveryOrderSaveReqVO updateReqVO) {
DeliveryOrderDO order = validateDeliveryOrderExists(updateReqVO.getId());
if (order.getStatus().equals(1)) {
throw exception(DELIVERY_ORDER_ALREADY_COMPLETED);
}
DeliveryOrderDO updateObj = BeanUtils.toBean(updateReqVO, DeliveryOrderDO.class);
deliveryOrderMapper.updateById(updateObj);
// Update vehicles
if (updateReqVO.getVehicles() != null) {
deliveryOrderVehicleMapper.deleteByOrderId(updateReqVO.getId());
for (DeliveryOrderVehicleVO vehicleVO : updateReqVO.getVehicles()) {
DeliveryOrderVehicleDO vehicle = BeanUtils.toBean(vehicleVO, DeliveryOrderVehicleDO.class);
vehicle.setOrderId(updateReqVO.getId());
deliveryOrderVehicleMapper.insert(vehicle);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteDeliveryOrder(Long id) {
validateDeliveryOrderExists(id);
deliveryOrderMapper.deleteById(id);
deliveryOrderVehicleMapper.deleteByOrderId(id);
}
@Override
public DeliveryOrderDO getDeliveryOrder(Long id) {
return deliveryOrderMapper.selectById(id);
}
@Override
public DeliveryOrderRespVO getDeliveryOrderDetail(Long id) {
DeliveryOrderDO order = validateDeliveryOrderExists(id);
DeliveryOrderRespVO respVO = BeanUtils.toBean(order, DeliveryOrderRespVO.class);
List<DeliveryOrderVehicleDO> vehicles = deliveryOrderVehicleMapper.selectListByOrderId(id);
respVO.setVehicles(BeanUtils.toBean(vehicles, DeliveryOrderVehicleVO.class));
return respVO;
}
@Override
public PageResult<DeliveryOrderDO> getDeliveryOrderPage(DeliveryOrderPageReqVO pageReqVO) {
return deliveryOrderMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void completeDeliveryOrder(Long id) {
DeliveryOrderDO order = validateDeliveryOrderExists(id);
if (order.getStatus().equals(1)) {
throw exception(DELIVERY_ORDER_ALREADY_COMPLETED);
}
// Update order status
deliveryOrderMapper.updateById(DeliveryOrderDO.builder().id(id).status(1).build());
// Mark task vehicles as delivered
List<DeliveryOrderVehicleDO> orderVehicles = deliveryOrderVehicleMapper.selectListByOrderId(id);
for (DeliveryOrderVehicleDO ov : orderVehicles) {
deliveryTaskVehicleMapper.updateById(
DeliveryTaskVehicleDO.builder()
.id(ov.getTaskVehicleId())
.isDelivered(true)
.build()
);
}
// Check if all task vehicles are delivered, update task delivery status
DeliveryTaskDO task = deliveryTaskMapper.selectById(order.getTaskId());
if (task != null) {
List<DeliveryTaskVehicleDO> taskVehicles = deliveryTaskVehicleMapper.selectListByTaskId(task.getId());
boolean allDelivered = taskVehicles.stream().allMatch(v -> Boolean.TRUE.equals(v.getIsDelivered()));
if (allDelivered) {
deliveryTaskMapper.updateById(DeliveryTaskDO.builder().id(task.getId()).deliveryStatus(1).build());
}
}
}
private DeliveryOrderDO validateDeliveryOrderExists(Long id) {
DeliveryOrderDO order = deliveryOrderMapper.selectById(id);
if (order == null) {
throw exception(DELIVERY_ORDER_NOT_EXISTS);
}
return order;
}
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.asset.service.delivery.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 交车完成事件
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public class DeliveryCompletedEvent {
/**
* 交车单ID
*/
private final Long deliveryOrderId;
/**
* 车辆ID
*/
private final Long vehicleId;
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.asset.service.event;
import cn.iocoder.yudao.module.asset.enums.replacement.ReplacementTypeEnum;
import cn.iocoder.yudao.module.asset.service.replacement.event.ReplacementApprovedEvent;
import cn.iocoder.yudao.module.asset.service.returnorder.ReturnOrderService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 还车单事件监听器
* 监听替换车审批通过事件,自动创建还车单
*
* @author 芋道源码
*/
@Component
@Slf4j
public class ReturnOrderEventListener {
@Resource
private ReturnOrderService returnOrderService;
/**
* 永久替换审批通过时,自动创建还车单
* 使用 @EventListener 而非 @TransactionalEventListener确保在同一事务中执行
*/
@EventListener
public void onReplacementApproved(ReplacementApprovedEvent event) {
if (event.getReplacementType().equals(ReplacementTypeEnum.PERMANENT.getType())) {
log.info("[onReplacementApproved] permanent replacement, creating return order: replacementId={}",
event.getReplacementId());
returnOrderService.createFromReplacement(
event.getReplacementId(),
event.getContractId(),
event.getOriginalVehicleId()
);
}
}
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.asset.service.event;
import cn.iocoder.yudao.module.asset.service.delivery.event.DeliveryCompletedEvent;
import cn.iocoder.yudao.module.asset.service.replacement.event.ReplacementReturnConfirmedEvent;
import cn.iocoder.yudao.module.asset.service.returnorder.event.ReturnApprovedEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* 车辆状态事件监听器
* 负责在业务事件发生时更新车辆状态
*
* @author 芋道源码
*/
@Component
@Slf4j
public class VehicleStatusEventListener {
@TransactionalEventListener
public void onDeliveryCompleted(DeliveryCompletedEvent event) {
log.info("[onDeliveryCompleted] vehicleId={}", event.getVehicleId());
// TODO: update vehicle status to DELIVERED when vehicle status management is implemented
}
@TransactionalEventListener
public void onReturnApproved(ReturnApprovedEvent event) {
log.info("[onReturnApproved] vehicleIds={}", event.getVehicleIds());
// TODO: update vehicle status to AVAILABLE when vehicle status management is implemented
}
@TransactionalEventListener
public void onReplacementReturnConfirmed(ReplacementReturnConfirmedEvent event) {
log.info("[onReplacementReturnConfirmed] vehicleId={}", event.getOriginalVehicleId());
// TODO: update vehicle status to AVAILABLE
}
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.asset.service.inspection;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionRecordDetailVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionRecordItemUpdateReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordDO;
import jakarta.validation.Valid;
/**
* 验车记录 Service 接口
*
* @author 芋道源码
*/
public interface InspectionRecordService {
/**
* 创建验车记录(根据模板生成检查项)
*/
Long createRecord(Long templateId, Integer sourceType, Long sourceId, Long vehicleId);
/**
* 克隆验车记录(从已有记录克隆,保留结果数据)
*/
Long cloneRecord(Long sourceRecordId, Integer newSourceType, Long newSourceId);
/**
* 更新验车记录检查项
*/
void updateRecordItem(@Valid InspectionRecordItemUpdateReqVO updateReqVO);
/**
* 完成验车记录
*/
void completeRecord(Long recordId, String inspectorName);
/**
* 获取验车记录详情
*/
InspectionRecordDetailVO getRecordDetail(Long recordId);
/**
* 获取最新已完成的验车记录
*/
InspectionRecordDO getLatestRecord(Long vehicleId, Integer sourceType);
}

View File

@@ -0,0 +1,267 @@
package cn.iocoder.yudao.module.asset.service.inspection;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionRecordDetailVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionRecordItemUpdateReqVO;
import cn.iocoder.yudao.module.asset.convert.inspection.InspectionConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordItemDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateItemDO;
import cn.iocoder.yudao.module.asset.dal.mysql.inspection.InspectionRecordItemMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.inspection.InspectionRecordMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.inspection.InspectionTemplateItemMapper;
import cn.iocoder.yudao.module.asset.enums.inspection.InspectionSourceTypeEnum;
import cn.iocoder.yudao.module.asset.enums.inspection.InspectionStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.asset.enums.ErrorCodeConstants.*;
/**
* 验车记录 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class InspectionRecordServiceImpl implements InspectionRecordService {
@Resource
private InspectionRecordMapper inspectionRecordMapper;
@Resource
private InspectionRecordItemMapper inspectionRecordItemMapper;
@Resource
private InspectionTemplateItemMapper inspectionTemplateItemMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRecord(Long templateId, Integer sourceType, Long sourceId, Long vehicleId) {
// 查询模板检查项
List<InspectionTemplateItemDO> templateItems = inspectionTemplateItemMapper.selectByTemplateId(templateId);
// 生成记录编码
String recordCode = generateRecordCode(sourceType);
// 创建验车记录
InspectionRecordDO record = InspectionRecordDO.builder()
.recordCode(recordCode)
.templateId(templateId)
.sourceType(sourceType)
.sourceId(sourceId)
.vehicleId(vehicleId)
.status(InspectionStatusEnum.PENDING.getStatus())
.build();
inspectionRecordMapper.insert(record);
// 批量插入检查项(从模板复制)
if (!templateItems.isEmpty()) {
List<InspectionRecordItemDO> recordItems = new ArrayList<>(templateItems.size());
for (InspectionTemplateItemDO templateItem : templateItems) {
InspectionRecordItemDO recordItem = InspectionRecordItemDO.builder()
.recordId(record.getId())
.itemCode(templateItem.getItemCode())
.category(templateItem.getCategory())
.itemName(templateItem.getItemName())
.inputType(templateItem.getInputType())
.build();
recordItems.add(recordItem);
}
inspectionRecordItemMapper.insertBatch(recordItems);
}
return record.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long cloneRecord(Long sourceRecordId, Integer newSourceType, Long newSourceId) {
// 查询源记录
InspectionRecordDO sourceRecord = inspectionRecordMapper.selectById(sourceRecordId);
if (sourceRecord == null) {
throw exception(INSPECTION_RECORD_NOT_EXISTS);
}
// 查询源检查项
List<InspectionRecordItemDO> sourceItems = inspectionRecordItemMapper.selectByRecordId(sourceRecordId);
// 生成记录编码
String recordCode = generateRecordCode(newSourceType);
// 创建新记录
InspectionRecordDO newRecord = InspectionRecordDO.builder()
.recordCode(recordCode)
.templateId(sourceRecord.getTemplateId())
.sourceType(newSourceType)
.sourceId(newSourceId)
.vehicleId(sourceRecord.getVehicleId())
.status(InspectionStatusEnum.PENDING.getStatus())
.clonedFromId(sourceRecordId)
.build();
inspectionRecordMapper.insert(newRecord);
// 复制检查项(保留结果数据)
if (!sourceItems.isEmpty()) {
List<InspectionRecordItemDO> newItems = new ArrayList<>(sourceItems.size());
for (InspectionRecordItemDO sourceItem : sourceItems) {
InspectionRecordItemDO newItem = InspectionRecordItemDO.builder()
.recordId(newRecord.getId())
.itemCode(sourceItem.getItemCode())
.category(sourceItem.getCategory())
.itemName(sourceItem.getItemName())
.inputType(sourceItem.getInputType())
.result(sourceItem.getResult())
.value(sourceItem.getValue())
.remark(sourceItem.getRemark())
.imageUrls(sourceItem.getImageUrls())
.build();
newItems.add(newItem);
}
inspectionRecordItemMapper.insertBatch(newItems);
}
return newRecord.getId();
}
@Override
public void updateRecordItem(InspectionRecordItemUpdateReqVO updateReqVO) {
// 校验检查项存在
InspectionRecordItemDO item = inspectionRecordItemMapper.selectById(updateReqVO.getId());
if (item == null) {
throw exception(INSPECTION_RECORD_NOT_EXISTS);
}
// 更新检查项
InspectionRecordItemDO updateObj = InspectionRecordItemDO.builder()
.id(updateReqVO.getId())
.result(updateReqVO.getResult())
.value(updateReqVO.getValue())
.remark(updateReqVO.getRemark())
.imageUrls(updateReqVO.getImageUrls())
.build();
inspectionRecordItemMapper.updateById(updateObj);
// 如果记录状态为待检,更新为检查中
InspectionRecordDO record = inspectionRecordMapper.selectById(item.getRecordId());
if (record != null && InspectionStatusEnum.PENDING.getStatus().equals(record.getStatus())) {
InspectionRecordDO updateRecord = InspectionRecordDO.builder()
.id(record.getId())
.status(InspectionStatusEnum.IN_PROGRESS.getStatus())
.build();
inspectionRecordMapper.updateById(updateRecord);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void completeRecord(Long recordId, String inspectorName) {
// 校验记录存在
InspectionRecordDO record = inspectionRecordMapper.selectById(recordId);
if (record == null) {
throw exception(INSPECTION_RECORD_NOT_EXISTS);
}
// 校验未完成
if (InspectionStatusEnum.COMPLETED.getStatus().equals(record.getStatus())) {
throw exception(INSPECTION_RECORD_ALREADY_COMPLETED);
}
// 校验必填项是否全部完成
List<InspectionRecordItemDO> items = inspectionRecordItemMapper.selectByRecordId(recordId);
// 查询模板检查项获取必填信息
List<InspectionTemplateItemDO> templateItems = inspectionTemplateItemMapper.selectByTemplateId(record.getTemplateId());
for (InspectionTemplateItemDO templateItem : templateItems) {
if (Integer.valueOf(1).equals(templateItem.getRequired())) {
// 查找对应的记录检查项
boolean hasResult = items.stream()
.filter(item -> item.getItemCode().equals(templateItem.getItemCode()))
.anyMatch(item -> item.getResult() != null);
if (!hasResult) {
throw exception(INSPECTION_RECORD_REQUIRED_ITEMS_INCOMPLETE);
}
}
}
// 计算总体结果:如果有任一不通过则整体不通过,否则通过
Integer overallResult = 1; // 默认通过
for (InspectionRecordItemDO item : items) {
if (item.getResult() != null && item.getResult() == 2) {
overallResult = 2; // 不通过
break;
}
}
// 更新记录状态
InspectionRecordDO updateRecord = InspectionRecordDO.builder()
.id(recordId)
.status(InspectionStatusEnum.COMPLETED.getStatus())
.inspectorName(inspectorName)
.inspectionTime(LocalDateTime.now())
.overallResult(overallResult)
.build();
inspectionRecordMapper.updateById(updateRecord);
}
@Override
public InspectionRecordDetailVO getRecordDetail(Long recordId) {
InspectionRecordDO record = inspectionRecordMapper.selectById(recordId);
if (record == null) {
return null;
}
InspectionRecordDetailVO detailVO = InspectionConvert.INSTANCE.convertRecordDetail(record);
// 查询检查项
List<InspectionRecordItemDO> items = inspectionRecordItemMapper.selectByRecordId(recordId);
detailVO.setItems(InspectionConvert.INSTANCE.convertRecordItemVOList(items));
return detailVO;
}
@Override
public InspectionRecordDO getLatestRecord(Long vehicleId, Integer sourceType) {
return inspectionRecordMapper.selectLatestByVehicleAndSourceType(vehicleId, sourceType);
}
/**
* 生成记录编码
* 格式: {BC|JC|HC}-yyyyMMdd-{seq}
*/
private String generateRecordCode(Integer sourceType) {
String prefix;
InspectionSourceTypeEnum sourceTypeEnum = InspectionSourceTypeEnum.valueOf(sourceType);
if (sourceTypeEnum == null) {
prefix = "YC";
} else {
switch (sourceTypeEnum) {
case PREPARE:
prefix = "BC";
break;
case DELIVERY:
prefix = "JC";
break;
case RETURN:
prefix = "HC";
break;
default:
prefix = "YC";
}
}
String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
int seq = ThreadLocalRandom.current().nextInt(100, 999);
return prefix + "-" + dateStr + "-" + String.format("%03d", seq);
}
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.asset.service.inspection;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplatePageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplateSaveReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplateRespVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateDO;
import jakarta.validation.Valid;
/**
* 验车模板 Service 接口
*
* @author 芋道源码
*/
public interface InspectionTemplateService {
/**
* 创建验车模板
*/
Long createTemplate(@Valid InspectionTemplateSaveReqVO createReqVO);
/**
* 更新验车模板
*/
void updateTemplate(@Valid InspectionTemplateSaveReqVO updateReqVO);
/**
* 删除验车模板
*/
void deleteTemplate(Long id);
/**
* 获取验车模板详情(含检查项)
*/
InspectionTemplateRespVO getTemplate(Long id);
/**
* 获取验车模板分页
*/
PageResult<InspectionTemplateDO> getTemplatePage(InspectionTemplatePageReqVO pageReqVO);
/**
* 匹配验车模板
* 优先精确匹配 bizType + vehicleType其次匹配 bizType + vehicleType IS NULL 的默认模板
*/
InspectionTemplateDO matchTemplate(Integer bizType, String vehicleType);
}

View File

@@ -0,0 +1,126 @@
package cn.iocoder.yudao.module.asset.service.inspection;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplatePageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplateSaveReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.inspection.vo.InspectionTemplateRespVO;
import cn.iocoder.yudao.module.asset.convert.inspection.InspectionConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateItemDO;
import cn.iocoder.yudao.module.asset.dal.mysql.inspection.InspectionTemplateItemMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.inspection.InspectionTemplateMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.asset.enums.ErrorCodeConstants.*;
/**
* 验车模板 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class InspectionTemplateServiceImpl implements InspectionTemplateService {
@Resource
private InspectionTemplateMapper inspectionTemplateMapper;
@Resource
private InspectionTemplateItemMapper inspectionTemplateItemMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTemplate(InspectionTemplateSaveReqVO createReqVO) {
// 插入模板
InspectionTemplateDO template = InspectionConvert.INSTANCE.convert(createReqVO);
inspectionTemplateMapper.insert(template);
// 插入检查项
if (createReqVO.getItems() != null && !createReqVO.getItems().isEmpty()) {
List<InspectionTemplateItemDO> items = InspectionConvert.INSTANCE.convertItemList(createReqVO.getItems());
items.forEach(item -> item.setTemplateId(template.getId()));
inspectionTemplateItemMapper.insertBatch(items);
}
return template.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTemplate(InspectionTemplateSaveReqVO updateReqVO) {
// 校验存在
validateTemplateExists(updateReqVO.getId());
// 更新模板
InspectionTemplateDO updateObj = InspectionConvert.INSTANCE.convert(updateReqVO);
inspectionTemplateMapper.updateById(updateObj);
// 删除旧检查项,插入新检查项
inspectionTemplateItemMapper.deleteByTemplateId(updateReqVO.getId());
if (updateReqVO.getItems() != null && !updateReqVO.getItems().isEmpty()) {
List<InspectionTemplateItemDO> items = InspectionConvert.INSTANCE.convertItemList(updateReqVO.getItems());
items.forEach(item -> item.setTemplateId(updateReqVO.getId()));
inspectionTemplateItemMapper.insertBatch(items);
}
}
@Override
public void deleteTemplate(Long id) {
// 校验存在
validateTemplateExists(id);
// 删除模板
inspectionTemplateMapper.deleteById(id);
// 删除检查项
inspectionTemplateItemMapper.deleteByTemplateId(id);
}
@Override
public InspectionTemplateRespVO getTemplate(Long id) {
InspectionTemplateDO template = inspectionTemplateMapper.selectById(id);
if (template == null) {
return null;
}
InspectionTemplateRespVO respVO = InspectionConvert.INSTANCE.convert(template);
// 查询检查项
List<InspectionTemplateItemDO> items = inspectionTemplateItemMapper.selectByTemplateId(id);
respVO.setItems(InspectionConvert.INSTANCE.convertItemVOList(items));
return respVO;
}
@Override
public PageResult<InspectionTemplateDO> getTemplatePage(InspectionTemplatePageReqVO pageReqVO) {
return inspectionTemplateMapper.selectPage(pageReqVO);
}
@Override
public InspectionTemplateDO matchTemplate(Integer bizType, String vehicleType) {
// 优先精确匹配 bizType + vehicleType
if (vehicleType != null) {
InspectionTemplateDO template = inspectionTemplateMapper.selectByBizTypeAndVehicleType(bizType, vehicleType);
if (template != null) {
return template;
}
}
// 其次匹配默认模板vehicleType IS NULL
InspectionTemplateDO defaultTemplate = inspectionTemplateMapper.selectDefaultByBizType(bizType);
if (defaultTemplate != null) {
return defaultTemplate;
}
throw exception(INSPECTION_TEMPLATE_MATCH_FAILED);
}
private void validateTemplateExists(Long id) {
InspectionTemplateDO template = inspectionTemplateMapper.selectById(id);
if (template == null) {
throw exception(INSPECTION_TEMPLATE_NOT_EXISTS);
}
}
}

View File

@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.asset.service.prepare;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.asset.controller.admin.prepare.vo.VehiclePreparePageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.prepare.vo.VehiclePrepareSaveReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.prepare.VehiclePrepareDO;
import cn.iocoder.yudao.module.asset.dal.mysql.prepare.VehiclePrepareMapper;
import cn.iocoder.yudao.module.asset.enums.inspection.InspectionSourceTypeEnum;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionRecordService;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionTemplateService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.asset.enums.ErrorCodeConstants.*;
@Service
@Validated
public class VehiclePrepareServiceImpl implements VehiclePrepareService {
@Resource
private VehiclePrepareMapper vehiclePrepareMapper;
@Resource
private InspectionTemplateService inspectionTemplateService;
@Resource
private InspectionRecordService inspectionRecordService;
@Override
public Long createVehiclePrepare(VehiclePrepareSaveReqVO createReqVO) {
VehiclePrepareDO prepare = BeanUtils.toBean(createReqVO, VehiclePrepareDO.class);
vehiclePrepareMapper.insert(prepare);
// 自动匹配验车模板并创建验车记录
InspectionTemplateDO template = inspectionTemplateService.matchTemplate(
InspectionSourceTypeEnum.PREPARE.getType(), prepare.getVehicleType());
if (template != null) {
Long inspectionRecordId = inspectionRecordService.createRecord(
template.getId(), InspectionSourceTypeEnum.PREPARE.getType(),
prepare.getId(), prepare.getVehicleId());
// 回写验车记录ID
vehiclePrepareMapper.updateById(VehiclePrepareDO.builder()
.id(prepare.getId()).inspectionRecordId(inspectionRecordId).build());
}
return prepare.getId();
}
@Override
public void updateVehiclePrepare(VehiclePrepareSaveReqVO updateReqVO) {
validateVehiclePrepareExists(updateReqVO.getId());
VehiclePrepareDO updateObj = BeanUtils.toBean(updateReqVO, VehiclePrepareDO.class);
vehiclePrepareMapper.updateById(updateObj);
}
@Override
public void deleteVehiclePrepare(Long id) {
validateVehiclePrepareExists(id);
vehiclePrepareMapper.deleteById(id);
}
@Override
public VehiclePrepareDO getVehiclePrepare(Long id) {
return vehiclePrepareMapper.selectById(id);
}
@Override
public PageResult<VehiclePrepareDO> getVehiclePreparePage(VehiclePreparePageReqVO pageReqVO) {
return vehiclePrepareMapper.selectPage(pageReqVO);
}
@Override
public void completeVehiclePrepare(Long id) {
VehiclePrepareDO prepare = validateVehiclePrepareExists(id);
if (prepare.getStatus().equals(1)) {
throw exception(VEHICLE_PREPARE_ALREADY_COMPLETED);
}
vehiclePrepareMapper.updateById(VehiclePrepareDO.builder()
.id(id).status(1).completeTime(LocalDateTime.now()).build());
// 同时完成关联的验车记录
if (prepare.getInspectionRecordId() != null) {
inspectionRecordService.completeRecord(prepare.getInspectionRecordId(), null);
}
}
private VehiclePrepareDO validateVehiclePrepareExists(Long id) {
VehiclePrepareDO prepare = vehiclePrepareMapper.selectById(id);
if (prepare == null) {
throw exception(VEHICLE_PREPARE_NOT_EXISTS);
}
return prepare;
}
}

View File

@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.asset.service.replacement;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementPageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementSaveReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.replacement.VehicleReplacementDO;
import jakarta.validation.Valid;
/**
* 替换车申请 Service 接口
*
* @author 芋道源码
*/
public interface VehicleReplacementService {
/**
* 创建替换车申请
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createReplacement(@Valid VehicleReplacementSaveReqVO createReqVO);
/**
* 更新替换车申请
*
* @param updateReqVO 更新信息
*/
void updateReplacement(@Valid VehicleReplacementSaveReqVO updateReqVO);
/**
* 删除替换车申请
*
* @param id 编号
*/
void deleteReplacement(Long id);
/**
* 获得替换车申请
*
* @param id 编号
* @return 替换车申请
*/
VehicleReplacementDO getReplacement(Long id);
/**
* 获得替换车申请分页
*
* @param pageReqVO 分页查询
* @return 替换车申请分页
*/
PageResult<VehicleReplacementDO> getReplacementPage(VehicleReplacementPageReqVO pageReqVO);
/**
* 提交审批
*
* @param id 编号
* @return 流程实例ID
*/
String submitApproval(Long id);
/**
* 撤回审批
*
* @param id 编号
*/
void withdrawApproval(Long id);
/**
* 更新审批状态BPM回调
*
* @param id 编号
* @param bpmStatus BPM状态
*/
void updateApprovalStatus(Long id, Integer bpmStatus);
/**
* 确认换回(临时替换)
*
* @param id 编号
*/
void confirmReturn(Long id);
}

View File

@@ -0,0 +1,273 @@
package cn.iocoder.yudao.module.asset.service.replacement;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementPageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.replacement.vo.VehicleReplacementSaveReqVO;
import cn.iocoder.yudao.module.asset.convert.replacement.VehicleReplacementConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.replacement.VehicleReplacementDO;
import cn.iocoder.yudao.module.asset.dal.mysql.replacement.VehicleReplacementMapper;
import cn.iocoder.yudao.module.asset.enums.replacement.ReplacementStatusEnum;
import cn.iocoder.yudao.module.asset.enums.replacement.ReplacementTypeEnum;
import cn.iocoder.yudao.module.asset.service.replacement.event.ReplacementApprovedEvent;
import cn.iocoder.yudao.module.asset.service.replacement.event.ReplacementReturnConfirmedEvent;
import cn.iocoder.yudao.module.asset.service.replacement.listener.ReplacementBpmListener;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.asset.enums.ErrorCodeConstants.*;
/**
* 替换车申请 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class VehicleReplacementServiceImpl implements VehicleReplacementService {
@Resource
private VehicleReplacementMapper replacementMapper;
@Resource
private BpmProcessInstanceApi bpmProcessInstanceApi;
@Resource
private ApplicationEventPublisher eventPublisher;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createReplacement(VehicleReplacementSaveReqVO createReqVO) {
// 转换并插入
VehicleReplacementDO replacement = VehicleReplacementConvert.INSTANCE.convert(createReqVO);
// 生成替换单编码
replacement.setReplacementCode(generateReplacementCode());
// 设置初始状态
replacement.setStatus(ReplacementStatusEnum.DRAFT.getStatus());
replacement.setApprovalStatus(0);
replacementMapper.insert(replacement);
return replacement.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateReplacement(VehicleReplacementSaveReqVO updateReqVO) {
// 校验存在
VehicleReplacementDO existing = validateReplacementExists(updateReqVO.getId());
// 校验状态:只有草稿、审批驳回状态才能修改
if (!ReplacementStatusEnum.DRAFT.getStatus().equals(existing.getStatus())
&& !ReplacementStatusEnum.REJECTED.getStatus().equals(existing.getStatus())) {
throw exception(VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_UPDATE);
}
// 转换并更新
VehicleReplacementDO updateObj = VehicleReplacementConvert.INSTANCE.convert(updateReqVO);
replacementMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteReplacement(Long id) {
// 校验存在
VehicleReplacementDO replacement = validateReplacementExists(id);
// 校验状态:只有草稿状态才能删除
if (!ReplacementStatusEnum.DRAFT.getStatus().equals(replacement.getStatus())) {
throw exception(VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_DELETE);
}
replacementMapper.deleteById(id);
}
@Override
public VehicleReplacementDO getReplacement(Long id) {
return replacementMapper.selectById(id);
}
@Override
public PageResult<VehicleReplacementDO> getReplacementPage(VehicleReplacementPageReqVO pageReqVO) {
return replacementMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String submitApproval(Long id) {
// 校验存在
VehicleReplacementDO replacement = validateReplacementExists(id);
// 校验状态:只有草稿、审批驳回状态才能提交审批
if (!ReplacementStatusEnum.DRAFT.getStatus().equals(replacement.getStatus())
&& !ReplacementStatusEnum.REJECTED.getStatus().equals(replacement.getStatus())) {
throw exception(VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_SUBMIT);
}
// 创建流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("replacementCode", replacement.getReplacementCode());
variables.put("replacementType", replacement.getReplacementType());
variables.put("contractCode", replacement.getContractCode());
variables.put("customerName", replacement.getCustomerName());
variables.put("originalPlateNo", replacement.getOriginalPlateNo());
variables.put("newPlateNo", replacement.getNewPlateNo());
// 创建流程实例
BpmProcessInstanceCreateReqDTO reqDTO = new BpmProcessInstanceCreateReqDTO();
reqDTO.setProcessDefinitionKey(ReplacementBpmListener.PROCESS_KEY);
reqDTO.setBusinessKey(String.valueOf(id));
reqDTO.setVariables(variables);
Long userId = SecurityFrameworkUtils.getLoginUserId();
String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, reqDTO).getData();
// 更新状态
VehicleReplacementDO updateObj = new VehicleReplacementDO();
updateObj.setId(id);
updateObj.setStatus(ReplacementStatusEnum.APPROVING.getStatus());
updateObj.setBpmInstanceId(processInstanceId);
replacementMapper.updateById(updateObj);
return processInstanceId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void withdrawApproval(Long id) {
// 校验存在
VehicleReplacementDO replacement = validateReplacementExists(id);
// 校验状态:只有审批中状态才能撤回
if (!ReplacementStatusEnum.APPROVING.getStatus().equals(replacement.getStatus())) {
throw exception(VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_WITHDRAW);
}
// 更新状态
VehicleReplacementDO updateObj = new VehicleReplacementDO();
updateObj.setId(id);
updateObj.setStatus(ReplacementStatusEnum.WITHDRAWN.getStatus());
replacementMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateApprovalStatus(Long id, Integer bpmStatus) {
// 校验存在
VehicleReplacementDO replacement = validateReplacementExists(id);
VehicleReplacementDO updateObj = new VehicleReplacementDO();
updateObj.setId(id);
if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmStatus)) {
// 审批通过 → 直接进入执行中
updateObj.setStatus(ReplacementStatusEnum.EXECUTING.getStatus());
updateObj.setActualDate(LocalDate.now());
replacementMapper.updateById(updateObj);
// 发布审批通过事件
eventPublisher.publishEvent(new ReplacementApprovedEvent(
replacement.getId(),
replacement.getReplacementType(),
replacement.getContractId(),
replacement.getOriginalVehicleId()
));
// 永久替换审批通过后直接完成
if (ReplacementTypeEnum.PERMANENT.getType().equals(replacement.getReplacementType())) {
VehicleReplacementDO completeObj = new VehicleReplacementDO();
completeObj.setId(id);
completeObj.setStatus(ReplacementStatusEnum.COMPLETED.getStatus());
replacementMapper.updateById(completeObj);
}
} else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmStatus)) {
updateObj.setStatus(ReplacementStatusEnum.REJECTED.getStatus());
replacementMapper.updateById(updateObj);
} else if (BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmStatus)) {
updateObj.setStatus(ReplacementStatusEnum.WITHDRAWN.getStatus());
replacementMapper.updateById(updateObj);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmReturn(Long id) {
// 校验存在
VehicleReplacementDO replacement = validateReplacementExists(id);
// 校验状态:只有执行中状态才能确认换回
if (!ReplacementStatusEnum.EXECUTING.getStatus().equals(replacement.getStatus())) {
throw exception(VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_CONFIRM);
}
// 校验类型:只有临时替换才能确认换回
if (!ReplacementTypeEnum.TEMPORARY.getType().equals(replacement.getReplacementType())) {
throw exception(VEHICLE_REPLACEMENT_STATUS_NOT_ALLOW_CONFIRM);
}
// 更新状态
VehicleReplacementDO updateObj = new VehicleReplacementDO();
updateObj.setId(id);
updateObj.setStatus(ReplacementStatusEnum.COMPLETED.getStatus());
updateObj.setActualReturnDate(LocalDate.now());
replacementMapper.updateById(updateObj);
// 发布换回确认事件
eventPublisher.publishEvent(new ReplacementReturnConfirmedEvent(
replacement.getId(),
replacement.getOriginalVehicleId()
));
}
// ==================== 私有方法 ====================
/**
* 校验替换车申请是否存在
*/
private VehicleReplacementDO validateReplacementExists(Long id) {
VehicleReplacementDO replacement = replacementMapper.selectById(id);
if (replacement == null) {
throw exception(VEHICLE_REPLACEMENT_NOT_EXISTS);
}
return replacement;
}
/**
* 生成替换单编码
*/
private String generateReplacementCode() {
// 格式TH-yyyyMMdd-NNN
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String prefix = "TH-" + dateStr + "-";
String maxCode = replacementMapper.selectMaxReplacementCodeByPrefix(prefix);
int nextSeq = 1;
if (maxCode != null && maxCode.startsWith(prefix)) {
try {
String seqStr = maxCode.substring(prefix.length());
nextSeq = Integer.parseInt(seqStr) + 1;
} catch (Exception e) {
log.warn("解析替换单编码失败: {}", maxCode, e);
}
}
return prefix + String.format("%03d", nextSeq);
}
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.asset.service.replacement.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 替换车审批通过事件
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public class ReplacementApprovedEvent {
/**
* 替换车申请ID
*/
private final Long replacementId;
/**
* 替换类型1=临时 2=永久)
*/
private final Integer replacementType;
/**
* 合同ID
*/
private final Long contractId;
/**
* 原车辆ID
*/
private final Long originalVehicleId;
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.asset.service.replacement.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 替换车换回确认事件
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public class ReplacementReturnConfirmedEvent {
/**
* 替换车申请ID
*/
private final Long replacementId;
/**
* 原车辆ID
*/
private final Long originalVehicleId;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.asset.service.replacement.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.asset.service.replacement.VehicleReplacementService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
* 替换车审批状态监听器
*
* @author 芋道源码
*/
@Component
public class ReplacementBpmListener extends BpmProcessInstanceStatusEventListener {
/**
* 流程定义 Key
*/
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());
}
}

View File

@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.asset.service.returnorder;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.returnorder.vo.ReturnOrderPageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.returnorder.vo.ReturnOrderRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.returnorder.vo.ReturnOrderSaveReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.returnorder.ReturnOrderDO;
import jakarta.validation.Valid;
import java.util.List;
public interface ReturnOrderService {
Long createReturnOrder(@Valid ReturnOrderSaveReqVO createReqVO);
void updateReturnOrder(@Valid ReturnOrderSaveReqVO updateReqVO);
void deleteReturnOrder(Long id);
ReturnOrderDO getReturnOrder(Long id);
ReturnOrderRespVO getReturnOrderDetail(Long id);
PageResult<ReturnOrderDO> getReturnOrderPage(ReturnOrderPageReqVO pageReqVO);
void completeInspection(Long id);
void settleReturnOrder(Long id);
/**
* 从交车单创建还车单
*/
Long createFromDelivery(Long deliveryOrderId, List<Long> vehicleIds);
/**
* 从替换车创建还车单(永久替换审批通过时)
*/
Long createFromReplacement(Long replacementId, Long contractId, Long vehicleId);
/**
* 开始单车验车(创建或克隆验车记录)
*/
Long startVehicleInspection(Long returnOrderVehicleId);
/**
* 完成单车验车
*/
void completeVehicleInspection(Long returnOrderVehicleId);
/**
* 提交审批
*/
void submitApproval(Long id);
/**
* 撤回审批
*/
void withdrawApproval(Long id);
/**
* 更新审批状态BPM 回调)
*/
void updateApprovalStatus(Long id, Integer bpmStatus);
}

View File

@@ -0,0 +1,449 @@
package cn.iocoder.yudao.module.asset.service.returnorder;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.asset.controller.admin.returnorder.vo.*;
import cn.iocoder.yudao.module.asset.dal.dataobject.contract.ContractDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.delivery.DeliveryOrderDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.delivery.DeliveryOrderVehicleDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionRecordDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.inspection.InspectionTemplateDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.returnorder.ReturnOrderDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.returnorder.ReturnOrderVehicleDO;
import cn.iocoder.yudao.module.asset.dal.mysql.contract.ContractMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.delivery.DeliveryOrderMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.inspection.InspectionRecordMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.delivery.DeliveryOrderVehicleMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.returnorder.ReturnOrderMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.returnorder.ReturnOrderVehicleMapper;
import cn.iocoder.yudao.module.asset.enums.inspection.InspectionSourceTypeEnum;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionRecordService;
import cn.iocoder.yudao.module.asset.service.inspection.InspectionTemplateService;
import cn.iocoder.yudao.module.asset.service.returnorder.event.ReturnApprovedEvent;
import cn.iocoder.yudao.module.asset.service.returnorder.listener.ReturnOrderBpmListener;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.asset.enums.ErrorCodeConstants.*;
@Service
@Validated
@Slf4j
public class ReturnOrderServiceImpl implements ReturnOrderService {
@Resource
private ReturnOrderMapper returnOrderMapper;
@Resource
private ReturnOrderVehicleMapper returnOrderVehicleMapper;
@Resource
private DeliveryOrderMapper deliveryOrderMapper;
@Resource
private DeliveryOrderVehicleMapper deliveryOrderVehicleMapper;
@Resource
private ContractMapper contractMapper;
@Resource
private InspectionRecordMapper inspectionRecordMapper;
@Resource
private InspectionRecordService inspectionRecordService;
@Resource
private InspectionTemplateService inspectionTemplateService;
@Resource
private BpmProcessInstanceApi bpmProcessInstanceApi;
@Resource
private ApplicationEventPublisher eventPublisher;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createReturnOrder(ReturnOrderSaveReqVO createReqVO) {
// Generate order code: HC + timestamp
String orderCode = "HC" + System.currentTimeMillis();
ReturnOrderDO order = BeanUtils.toBean(createReqVO, ReturnOrderDO.class);
order.setOrderCode(orderCode);
order.setSourceType(1); // 手动创建
order.setStatus(0); // pending inspection
order.setApprovalStatus(0); // draft
returnOrderMapper.insert(order);
// Insert vehicles
if (createReqVO.getVehicles() != null) {
for (ReturnOrderVehicleVO vehicleVO : createReqVO.getVehicles()) {
ReturnOrderVehicleDO vehicle = BeanUtils.toBean(vehicleVO, ReturnOrderVehicleDO.class);
vehicle.setReturnOrderId(order.getId());
returnOrderVehicleMapper.insert(vehicle);
}
}
// Calculate total refund
calculateTotalRefund(order.getId());
return order.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateReturnOrder(ReturnOrderSaveReqVO updateReqVO) {
ReturnOrderDO order = validateReturnOrderExists(updateReqVO.getId());
if (order.getStatus().equals(2)) {
throw exception(RETURN_ORDER_ALREADY_SETTLED);
}
ReturnOrderDO updateObj = BeanUtils.toBean(updateReqVO, ReturnOrderDO.class);
returnOrderMapper.updateById(updateObj);
// Update vehicles
if (updateReqVO.getVehicles() != null) {
returnOrderVehicleMapper.deleteByReturnOrderId(updateReqVO.getId());
for (ReturnOrderVehicleVO vehicleVO : updateReqVO.getVehicles()) {
ReturnOrderVehicleDO vehicle = BeanUtils.toBean(vehicleVO, ReturnOrderVehicleDO.class);
vehicle.setReturnOrderId(updateReqVO.getId());
returnOrderVehicleMapper.insert(vehicle);
}
}
calculateTotalRefund(updateReqVO.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteReturnOrder(Long id) {
validateReturnOrderExists(id);
returnOrderMapper.deleteById(id);
returnOrderVehicleMapper.deleteByReturnOrderId(id);
}
@Override
public ReturnOrderDO getReturnOrder(Long id) {
return returnOrderMapper.selectById(id);
}
@Override
public ReturnOrderRespVO getReturnOrderDetail(Long id) {
ReturnOrderDO order = validateReturnOrderExists(id);
ReturnOrderRespVO respVO = BeanUtils.toBean(order, ReturnOrderRespVO.class);
List<ReturnOrderVehicleDO> vehicles = returnOrderVehicleMapper.selectListByReturnOrderId(id);
respVO.setVehicles(BeanUtils.toBean(vehicles, ReturnOrderVehicleVO.class));
return respVO;
}
@Override
public PageResult<ReturnOrderDO> getReturnOrderPage(ReturnOrderPageReqVO pageReqVO) {
return returnOrderMapper.selectPage(pageReqVO);
}
@Override
public void completeInspection(Long id) {
ReturnOrderDO order = validateReturnOrderExists(id);
if (order.getStatus().equals(2)) {
throw exception(RETURN_ORDER_ALREADY_SETTLED);
}
returnOrderMapper.updateById(ReturnOrderDO.builder().id(id).status(1).build());
}
@Override
public void settleReturnOrder(Long id) {
ReturnOrderDO order = validateReturnOrderExists(id);
if (order.getStatus().equals(2)) {
throw exception(RETURN_ORDER_ALREADY_SETTLED);
}
returnOrderMapper.updateById(ReturnOrderDO.builder().id(id).status(2).build());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createFromDelivery(Long deliveryOrderId, List<Long> vehicleIds) {
// 查询交车单信息
DeliveryOrderDO deliveryOrder = deliveryOrderMapper.selectById(deliveryOrderId);
if (deliveryOrder == null) {
throw exception(DELIVERY_ORDER_NOT_EXISTS);
}
// 创建还车单
ReturnOrderDO order = new ReturnOrderDO();
order.setOrderCode("HC" + System.currentTimeMillis());
order.setSourceType(1); // 手动(从交车单创建)
order.setDeliveryOrderId(deliveryOrderId);
order.setContractId(deliveryOrder.getContractId());
order.setContractCode(deliveryOrder.getContractCode());
order.setProjectName(deliveryOrder.getProjectName());
order.setCustomerId(deliveryOrder.getCustomerId());
order.setCustomerName(deliveryOrder.getCustomerName());
order.setStatus(0); // 待验车
order.setApprovalStatus(0); // 草稿
returnOrderMapper.insert(order);
// 查询交车单车辆,筛选指定的 vehicleIds
List<DeliveryOrderVehicleDO> deliveryVehicles = deliveryOrderVehicleMapper.selectListByOrderId(deliveryOrderId);
for (DeliveryOrderVehicleDO dv : deliveryVehicles) {
if (vehicleIds.contains(dv.getVehicleId())) {
ReturnOrderVehicleDO vehicle = new ReturnOrderVehicleDO();
vehicle.setReturnOrderId(order.getId());
vehicle.setVehicleId(dv.getVehicleId());
vehicle.setPlateNo(dv.getPlateNo());
vehicle.setVin(dv.getVin());
vehicle.setBrand(dv.getBrand());
vehicle.setModel(dv.getModel());
returnOrderVehicleMapper.insert(vehicle);
}
}
return order.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createFromReplacement(Long replacementId, Long contractId, Long vehicleId) {
// 查询合同信息
ContractDO contract = contractMapper.selectById(contractId);
if (contract == null) {
throw exception(CONTRACT_NOT_EXISTS);
}
// 创建还车单
ReturnOrderDO order = new ReturnOrderDO();
order.setOrderCode("HC" + System.currentTimeMillis());
order.setSourceType(2); // 替换车触发
order.setSourceId(replacementId);
order.setContractId(contractId);
order.setContractCode(contract.getContractCode());
order.setProjectName(contract.getProjectName());
order.setCustomerId(contract.getCustomerId());
order.setCustomerName(contract.getCustomerName());
order.setStatus(0); // 待验车
order.setApprovalStatus(0); // 草稿
returnOrderMapper.insert(order);
// 创建还车车辆(原车辆)
// TODO: 查询车辆信息获取 plateNo/vin/brand/model暂时只设置 vehicleId
ReturnOrderVehicleDO vehicle = new ReturnOrderVehicleDO();
vehicle.setReturnOrderId(order.getId());
vehicle.setVehicleId(vehicleId);
returnOrderVehicleMapper.insert(vehicle);
return order.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long startVehicleInspection(Long returnOrderVehicleId) {
// 查找还车车辆
ReturnOrderVehicleDO vehicle = returnOrderVehicleMapper.selectById(returnOrderVehicleId);
if (vehicle == null) {
throw exception(RETURN_ORDER_VEHICLE_NOT_EXISTS);
}
if (vehicle.getInspectionRecordId() != null) {
throw exception(RETURN_ORDER_INSPECTION_ALREADY_STARTED);
}
Long inspectionRecordId;
// 尝试查找最新的交车验车记录进行克隆
InspectionRecordDO latestRecord = inspectionRecordService.getLatestRecord(
vehicle.getVehicleId(), InspectionSourceTypeEnum.DELIVERY.getType());
if (latestRecord != null) {
// 克隆交车验车记录
inspectionRecordId = inspectionRecordService.cloneRecord(
latestRecord.getId(),
InspectionSourceTypeEnum.RETURN.getType(),
returnOrderVehicleId);
} else {
// 没有交车记录,尝试匹配模板创建新记录
InspectionTemplateDO template = inspectionTemplateService.matchTemplate(
InspectionSourceTypeEnum.RETURN.getType(), null);
if (template == null) {
throw exception(INSPECTION_TEMPLATE_MATCH_FAILED);
}
inspectionRecordId = inspectionRecordService.createRecord(
template.getId(),
InspectionSourceTypeEnum.RETURN.getType(),
returnOrderVehicleId,
vehicle.getVehicleId());
}
// 更新还车车辆的验车记录ID
ReturnOrderVehicleDO updateObj = new ReturnOrderVehicleDO();
updateObj.setId(returnOrderVehicleId);
updateObj.setInspectionRecordId(inspectionRecordId);
returnOrderVehicleMapper.updateById(updateObj);
return inspectionRecordId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void completeVehicleInspection(Long returnOrderVehicleId) {
// 查找还车车辆
ReturnOrderVehicleDO vehicle = returnOrderVehicleMapper.selectById(returnOrderVehicleId);
if (vehicle == null) {
throw exception(RETURN_ORDER_VEHICLE_NOT_EXISTS);
}
if (vehicle.getInspectionRecordId() == null) {
throw exception(RETURN_ORDER_INSPECTION_NOT_STARTED);
}
// 完成验车记录
Long userId = SecurityFrameworkUtils.getLoginUserId();
String inspectorName = userId != null ? String.valueOf(userId) : "system";
inspectionRecordService.completeRecord(vehicle.getInspectionRecordId(), inspectorName);
// 检查该还车单的所有车辆是否都完成了验车
List<ReturnOrderVehicleDO> allVehicles = returnOrderVehicleMapper.selectListByReturnOrderId(vehicle.getReturnOrderId());
boolean allCompleted = allVehicles.stream().allMatch(v -> {
if (v.getInspectionRecordId() == null) {
return false;
}
// 当前正在完成的车辆视为已完成
if (v.getId().equals(returnOrderVehicleId)) {
return true;
}
// 检查验车记录状态2=已完成)
InspectionRecordDO record = inspectionRecordMapper.selectById(v.getInspectionRecordId());
return record != null && Integer.valueOf(2).equals(record.getStatus());
});
if (allCompleted) {
// 所有车辆验车完成,更新还车单状态
returnOrderMapper.updateById(ReturnOrderDO.builder()
.id(vehicle.getReturnOrderId())
.status(1) // 验车完成
.build());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitApproval(Long id) {
ReturnOrderDO order = validateReturnOrderExists(id);
// 校验状态:验车完成才能提交审批
if (!Integer.valueOf(1).equals(order.getStatus())) {
throw exception(RETURN_ORDER_STATUS_NOT_ALLOW_SUBMIT);
}
// 校验审批状态:草稿、已驳回、已撤回才能提交
if (order.getApprovalStatus() != null && order.getApprovalStatus() == 1) {
throw exception(RETURN_ORDER_STATUS_NOT_ALLOW_SUBMIT);
}
// 创建流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("orderCode", order.getOrderCode());
variables.put("contractCode", order.getContractCode());
variables.put("customerName", order.getCustomerName());
// 创建流程实例
BpmProcessInstanceCreateReqDTO reqDTO = new BpmProcessInstanceCreateReqDTO();
reqDTO.setProcessDefinitionKey(ReturnOrderBpmListener.PROCESS_KEY);
reqDTO.setBusinessKey(String.valueOf(id));
reqDTO.setVariables(variables);
Long userId = SecurityFrameworkUtils.getLoginUserId();
String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, reqDTO).getData();
// 更新审批状态
ReturnOrderDO updateObj = new ReturnOrderDO();
updateObj.setId(id);
updateObj.setApprovalStatus(1); // 审批中
updateObj.setBpmInstanceId(processInstanceId);
returnOrderMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void withdrawApproval(Long id) {
ReturnOrderDO order = validateReturnOrderExists(id);
// 校验审批状态:只有审批中才能撤回
if (!Integer.valueOf(1).equals(order.getApprovalStatus())) {
throw exception(RETURN_ORDER_STATUS_NOT_ALLOW_WITHDRAW);
}
// 更新审批状态
ReturnOrderDO updateObj = new ReturnOrderDO();
updateObj.setId(id);
updateObj.setApprovalStatus(4); // 已撤回
returnOrderMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateApprovalStatus(Long id, Integer bpmStatus) {
ReturnOrderDO order = validateReturnOrderExists(id);
ReturnOrderDO updateObj = new ReturnOrderDO();
updateObj.setId(id);
if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmStatus)) {
updateObj.setApprovalStatus(2); // 审批通过
returnOrderMapper.updateById(updateObj);
// 发布审批通过事件
List<ReturnOrderVehicleDO> vehicles = returnOrderVehicleMapper.selectListByReturnOrderId(id);
List<Long> vehicleIds = vehicles.stream()
.map(ReturnOrderVehicleDO::getVehicleId)
.collect(Collectors.toList());
eventPublisher.publishEvent(new ReturnApprovedEvent(id, vehicleIds));
} else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmStatus)) {
updateObj.setApprovalStatus(3); // 审批拒绝
returnOrderMapper.updateById(updateObj);
} else if (BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmStatus)) {
updateObj.setApprovalStatus(4); // 已撤回
returnOrderMapper.updateById(updateObj);
}
}
// ==================== 私有方法 ====================
private ReturnOrderDO validateReturnOrderExists(Long id) {
ReturnOrderDO order = returnOrderMapper.selectById(id);
if (order == null) {
throw exception(RETURN_ORDER_NOT_EXISTS);
}
return order;
}
private void calculateTotalRefund(Long orderId) {
ReturnOrderDO order = returnOrderMapper.selectById(orderId);
if (order == null) return;
List<ReturnOrderVehicleDO> vehicles = returnOrderVehicleMapper.selectListByReturnOrderId(orderId);
BigDecimal totalHydrogenRefund = vehicles.stream()
.map(v -> v.getHydrogenRefundAmount() != null ? v.getHydrogenRefundAmount() : BigDecimal.ZERO)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal depositRefund = order.getDepositRefund() != null ? order.getDepositRefund() : BigDecimal.ZERO;
BigDecimal otherCharges = order.getOtherCharges() != null ? order.getOtherCharges() : BigDecimal.ZERO;
BigDecimal totalRefund = depositRefund.add(totalHydrogenRefund).subtract(otherCharges);
returnOrderMapper.updateById(ReturnOrderDO.builder()
.id(orderId)
.hydrogenRefund(totalHydrogenRefund)
.totalRefundAmount(totalRefund)
.build());
}
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.asset.service.returnorder.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* 还车单审批通过事件
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public class ReturnApprovedEvent {
/**
* 还车单ID
*/
private final Long returnOrderId;
/**
* 还车单中的所有车辆ID
*/
private final List<Long> vehicleIds;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.asset.service.returnorder.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.asset.service.returnorder.ReturnOrderService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
* 还车单审批状态监听器
*
* @author 芋道源码
*/
@Component
public class ReturnOrderBpmListener extends BpmProcessInstanceStatusEventListener {
/**
* 流程定义 Key
*/
public static final String PROCESS_KEY = "asset_return_order";
@Resource
private ReturnOrderService returnOrderService;
@Override
protected String getProcessDefinitionKey() {
return PROCESS_KEY;
}
@Override
protected void onEvent(BpmProcessInstanceStatusEvent event) {
returnOrderService.updateApprovalStatus(Long.parseLong(event.getBusinessKey()), event.getStatus());
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="asset_return_order" name="还车单审批" isExecutable="true">
<startEvent id="startEvent" name="开始"/>
<sequenceFlow sourceRef="startEvent" targetRef="deptManagerApproval"/>
<!-- 部门主管审批 -->
<userTask id="deptManagerApproval" name="部门主管审批" flowable:candidateGroups="dept_manager">
<extensionElements>
<flowable:formProperty id="approved" name="是否同意" type="boolean" required="true"/>
<flowable:formProperty id="comment" name="审批意见" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow sourceRef="deptManagerApproval" targetRef="gateway1"/>
<!-- 网关:判断部门主管是否同意 -->
<exclusiveGateway id="gateway1"/>
<sequenceFlow sourceRef="gateway1" targetRef="endEventApprove">
<conditionExpression xsi:type="tFormalExpression">${approved == true}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="gateway1" targetRef="endEventReject">
<conditionExpression xsi:type="tFormalExpression">${approved == false}</conditionExpression>
</sequenceFlow>
<!-- 审批通过结束 -->
<endEvent id="endEventApprove" name="审批通过"/>
<!-- 审批拒绝结束 -->
<endEvent id="endEventReject" name="审批拒绝"/>
</process>
</definitions>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="asset_vehicle_replacement" name="替换车审批" isExecutable="true">
<startEvent id="startEvent" name="开始"/>
<sequenceFlow sourceRef="startEvent" targetRef="deptManagerApproval"/>
<!-- 部门主管审批 -->
<userTask id="deptManagerApproval" name="部门主管审批" flowable:candidateGroups="dept_manager">
<extensionElements>
<flowable:formProperty id="approved" name="是否同意" type="boolean" required="true"/>
<flowable:formProperty id="comment" name="审批意见" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow sourceRef="deptManagerApproval" targetRef="gateway1"/>
<!-- 网关:判断部门主管是否同意 -->
<exclusiveGateway id="gateway1"/>
<!-- 同意且为永久替换 → 总经理审批 -->
<sequenceFlow sourceRef="gateway1" targetRef="gmApproval">
<conditionExpression xsi:type="tFormalExpression">${approved == true &amp;&amp; replacementType == 2}</conditionExpression>
</sequenceFlow>
<!-- 同意且为临时替换 → 直接通过 -->
<sequenceFlow sourceRef="gateway1" targetRef="endEventApprove">
<conditionExpression xsi:type="tFormalExpression">${approved == true &amp;&amp; replacementType == 1}</conditionExpression>
</sequenceFlow>
<!-- 不同意 → 拒绝 -->
<sequenceFlow sourceRef="gateway1" targetRef="endEventReject">
<conditionExpression xsi:type="tFormalExpression">${approved == false}</conditionExpression>
</sequenceFlow>
<!-- 总经理审批 -->
<userTask id="gmApproval" name="总经理审批" flowable:candidateGroups="general_manager">
<extensionElements>
<flowable:formProperty id="approved" name="是否同意" type="boolean" required="true"/>
<flowable:formProperty id="comment" name="审批意见" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow sourceRef="gmApproval" targetRef="gateway2"/>
<!-- 网关:判断总经理是否同意 -->
<exclusiveGateway id="gateway2"/>
<sequenceFlow sourceRef="gateway2" targetRef="endEventApprove">
<conditionExpression xsi:type="tFormalExpression">${approved == true}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="gateway2" targetRef="endEventReject">
<conditionExpression xsi:type="tFormalExpression">${approved == false}</conditionExpression>
</sequenceFlow>
<!-- 审批通过结束 -->
<endEvent id="endEventApprove" name="审批通过"/>
<!-- 审批拒绝结束 -->
<endEvent id="endEventReject" name="审批拒绝"/>
</process>
</definitions>