From 04f0599efad99e5b9d915ceee3cf6340fd51afd5 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 13 Mar 2026 10:20:25 +0800 Subject: [PATCH] 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 --- sql/2026-03-13-return-order-enhance.sql | 9 + .../returnorder/ReturnOrderController.java | 132 +++++ .../dataobject/returnorder/ReturnOrderDO.java | 62 +++ .../returnorder/ReturnOrderVehicleDO.java | 51 ++ .../asset/enums/ErrorCodeConstants.java | 5 + .../event/DeliveryCompletedEvent.java | 25 + .../event/ReturnOrderEventListener.java | 41 ++ .../event/VehicleStatusEventListener.java | 38 ++ .../returnorder/ReturnOrderService.java | 65 +++ .../returnorder/ReturnOrderServiceImpl.java | 449 ++++++++++++++++++ .../event/ReturnApprovedEvent.java | 27 ++ .../listener/ReturnOrderBpmListener.java | 35 ++ .../processes/asset_return_order.bpmn20.xml | 44 ++ .../asset_vehicle_replacement.bpmn20.xml | 72 +++ 14 files changed, 1055 insertions(+) create mode 100644 sql/2026-03-13-return-order-enhance.sql create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/returnorder/ReturnOrderController.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderDO.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderVehicleDO.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/delivery/event/DeliveryCompletedEvent.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/ReturnOrderEventListener.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/VehicleStatusEventListener.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderService.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderServiceImpl.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/event/ReturnApprovedEvent.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/listener/ReturnOrderBpmListener.java create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_return_order.bpmn20.xml create mode 100644 yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_vehicle_replacement.bpmn20.xml diff --git a/sql/2026-03-13-return-order-enhance.sql b/sql/2026-03-13-return-order-enhance.sql new file mode 100644 index 0000000..3862607 --- /dev/null +++ b/sql/2026-03-13-return-order-enhance.sql @@ -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 '关联验车记录'; diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/returnorder/ReturnOrderController.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/returnorder/ReturnOrderController.java new file mode 100644 index 0000000..8c82b25 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/controller/admin/returnorder/ReturnOrderController.java @@ -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 createReturnOrder(@Valid @RequestBody ReturnOrderSaveReqVO createReqVO) { + return success(returnOrderService.createReturnOrder(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新还车单") + @PreAuthorize("@ss.hasPermission('asset:return-order:update')") + public CommonResult 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 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 getReturnOrder(@RequestParam("id") Long id) { + return success(returnOrderService.getReturnOrderDetail(id)); + } + + @GetMapping("/page") + @Operation(summary = "获得还车单分页") + @PreAuthorize("@ss.hasPermission('asset:return-order:query')") + public CommonResult> getReturnOrderPage(@Valid ReturnOrderPageReqVO pageReqVO) { + PageResult 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 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 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 createFromDelivery(@RequestParam("deliveryOrderId") Long deliveryOrderId, + @RequestParam("vehicleIds") List 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 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 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 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 withdrawApproval(@RequestParam("id") Long id) { + returnOrderService.withdrawApproval(id); + return success(true); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderDO.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderDO.java new file mode 100644 index 0000000..30bda40 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderDO.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderVehicleDO.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderVehicleDO.java new file mode 100644 index 0000000..49ebc3d --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/dal/dataobject/returnorder/ReturnOrderVehicleDO.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/enums/ErrorCodeConstants.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/enums/ErrorCodeConstants.java index 11fe9dc..eb49df4 100644 --- a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/enums/ErrorCodeConstants.java +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/enums/ErrorCodeConstants.java @@ -57,6 +57,11 @@ public interface ErrorCodeConstants { // ========== 还车单管理 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, "验车模板不存在"); diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/delivery/event/DeliveryCompletedEvent.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/delivery/event/DeliveryCompletedEvent.java new file mode 100644 index 0000000..5eb742e --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/delivery/event/DeliveryCompletedEvent.java @@ -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; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/ReturnOrderEventListener.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/ReturnOrderEventListener.java new file mode 100644 index 0000000..2b50dd1 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/ReturnOrderEventListener.java @@ -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() + ); + } + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/VehicleStatusEventListener.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/VehicleStatusEventListener.java new file mode 100644 index 0000000..7589082 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/event/VehicleStatusEventListener.java @@ -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 + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderService.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderService.java new file mode 100644 index 0000000..f4f302b --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderService.java @@ -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 getReturnOrderPage(ReturnOrderPageReqVO pageReqVO); + + void completeInspection(Long id); + + void settleReturnOrder(Long id); + + /** + * 从交车单创建还车单 + */ + Long createFromDelivery(Long deliveryOrderId, List 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); + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderServiceImpl.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderServiceImpl.java new file mode 100644 index 0000000..289b35c --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/ReturnOrderServiceImpl.java @@ -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 vehicles = returnOrderVehicleMapper.selectListByReturnOrderId(id); + respVO.setVehicles(BeanUtils.toBean(vehicles, ReturnOrderVehicleVO.class)); + return respVO; + } + + @Override + public PageResult 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 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 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 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 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 vehicles = returnOrderVehicleMapper.selectListByReturnOrderId(id); + List 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 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()); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/event/ReturnApprovedEvent.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/event/ReturnApprovedEvent.java new file mode 100644 index 0000000..8e1895d --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/event/ReturnApprovedEvent.java @@ -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 vehicleIds; + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/listener/ReturnOrderBpmListener.java b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/listener/ReturnOrderBpmListener.java new file mode 100644 index 0000000..ed80cb3 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/java/cn/iocoder/yudao/module/asset/service/returnorder/listener/ReturnOrderBpmListener.java @@ -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()); + } + +} diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_return_order.bpmn20.xml b/yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_return_order.bpmn20.xml new file mode 100644 index 0000000..c2de420 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_return_order.bpmn20.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + ${approved == true} + + + + ${approved == false} + + + + + + + + + + + diff --git a/yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_vehicle_replacement.bpmn20.xml b/yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_vehicle_replacement.bpmn20.xml new file mode 100644 index 0000000..e9c6ff6 --- /dev/null +++ b/yudao-module-asset/yudao-module-asset-server/src/main/resources/processes/asset_vehicle_replacement.bpmn20.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ${approved == true && replacementType == 2} + + + + + ${approved == true && replacementType == 1} + + + + + ${approved == false} + + + + + + + + + + + + + + + + + ${approved == true} + + + + ${approved == false} + + + + + + + + + + +