From b93ea7117445643c19640193f059cf0abceed74d Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 13 Mar 2026 10:11:50 +0800 Subject: [PATCH] feat(asset): add vehicle replacement module with BPM approval workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- sql/2026-03-13-vehicle-replacement.sql | 32 ++ .../replacement/ReplacementStatusEnum.java | 41 +++ .../replacement/ReplacementTypeEnum.java | 36 +++ .../VehicleReplacementController.java | 102 +++++++ .../vo/VehicleReplacementPageReqVO.java | 35 +++ .../vo/VehicleReplacementRespVO.java | 93 ++++++ .../vo/VehicleReplacementSaveReqVO.java | 72 +++++ .../VehicleReplacementConvert.java | 26 ++ .../replacement/VehicleReplacementDO.java | 142 +++++++++ .../replacement/VehicleReplacementMapper.java | 33 +++ .../VehicleReplacementService.java | 84 ++++++ .../VehicleReplacementServiceImpl.java | 273 ++++++++++++++++++ .../event/ReplacementApprovedEvent.java | 35 +++ .../ReplacementReturnConfirmedEvent.java | 25 ++ .../listener/ReplacementBpmListener.java | 35 +++ 15 files changed, 1064 insertions(+) create mode 100644 sql/2026-03-13-vehicle-replacement.sql create mode 100644 yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementStatusEnum.java create mode 100644 yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementTypeEnum.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/VehicleReplacementController.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementPageReqVO.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementRespVO.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementSaveReqVO.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/convert/replacement/VehicleReplacementConvert.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/replacement/VehicleReplacementDO.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/mysql/replacement/VehicleReplacementMapper.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementService.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementServiceImpl.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementApprovedEvent.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementReturnConfirmedEvent.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/listener/ReplacementBpmListener.java diff --git a/sql/2026-03-13-vehicle-replacement.sql b/sql/2026-03-13-vehicle-replacement.sql new file mode 100644 index 0000000..1b30ad2 --- /dev/null +++ b/sql/2026-03-13-vehicle-replacement.sql @@ -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='替换车申请'; diff --git a/yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementStatusEnum.java b/yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementStatusEnum.java new file mode 100644 index 0000000..07b2386 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementStatusEnum.java @@ -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); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementTypeEnum.java b/yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementTypeEnum.java new file mode 100644 index 0000000..95a3475 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-api/src/main/java/cn/iocoder/yudao/module/asset/enums/replacement/ReplacementTypeEnum.java @@ -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); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/VehicleReplacementController.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/VehicleReplacementController.java new file mode 100644 index 0000000..4582688 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/VehicleReplacementController.java @@ -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 createReplacement(@Valid @RequestBody VehicleReplacementSaveReqVO createReqVO) { + return success(replacementService.createReplacement(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新替换车申请") + @PreAuthorize("@ss.hasPermission('asset:vehicle-replacement:update')") + public CommonResult 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 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 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> getReplacementPage(@Valid VehicleReplacementPageReqVO pageReqVO) { + PageResult 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 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 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 confirmReturn(@RequestParam("id") Long id) { + replacementService.confirmReturn(id); + return success(true); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementPageReqVO.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementPageReqVO.java new file mode 100644 index 0000000..746cb6b --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementPageReqVO.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementRespVO.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementRespVO.java new file mode 100644 index 0000000..1538ee5 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementRespVO.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementSaveReqVO.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementSaveReqVO.java new file mode 100644 index 0000000..2af286c --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/replacement/vo/VehicleReplacementSaveReqVO.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/convert/replacement/VehicleReplacementConvert.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/convert/replacement/VehicleReplacementConvert.java new file mode 100644 index 0000000..06e4db6 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/convert/replacement/VehicleReplacementConvert.java @@ -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 convertPage(PageResult page); + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/replacement/VehicleReplacementDO.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/replacement/VehicleReplacementDO.java new file mode 100644 index 0000000..fbb140e --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/replacement/VehicleReplacementDO.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/mysql/replacement/VehicleReplacementMapper.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/mysql/replacement/VehicleReplacementMapper.java new file mode 100644 index 0000000..8481dfd --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/mysql/replacement/VehicleReplacementMapper.java @@ -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 { + + default PageResult selectPage(VehicleReplacementPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(VehicleReplacementDO::getReplacementCode, reqVO.getReplacementCode()) + .eqIfPresent(VehicleReplacementDO::getReplacementType, reqVO.getReplacementType()) + .eqIfPresent(VehicleReplacementDO::getContractId, reqVO.getContractId()) + .likeIfPresent(VehicleReplacementDO::getCustomerName, reqVO.getCustomerName()) + .eqIfPresent(VehicleReplacementDO::getStatus, reqVO.getStatus()) + .orderByDesc(VehicleReplacementDO::getId)); + } + + @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); + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementService.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementService.java new file mode 100644 index 0000000..3dcfc96 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementService.java @@ -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 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); + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementServiceImpl.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementServiceImpl.java new file mode 100644 index 0000000..9afc7c3 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/VehicleReplacementServiceImpl.java @@ -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 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 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); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementApprovedEvent.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementApprovedEvent.java new file mode 100644 index 0000000..72f2f8e --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementApprovedEvent.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementReturnConfirmedEvent.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementReturnConfirmedEvent.java new file mode 100644 index 0000000..fc659b1 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/event/ReplacementReturnConfirmedEvent.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/listener/ReplacementBpmListener.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/listener/ReplacementBpmListener.java new file mode 100644 index 0000000..627b4a3 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/replacement/listener/ReplacementBpmListener.java @@ -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()); + } + +}