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>
This commit is contained in:
kkfluous
2026-03-13 10:20:25 +08:00
parent b93ea71174
commit 04f0599efa
14 changed files with 1055 additions and 0 deletions

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,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,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

@@ -57,6 +57,11 @@ public interface ErrorCodeConstants {
// ========== 还车单管理 1-008-010-000 ========== // ========== 还车单管理 1-008-010-000 ==========
ErrorCode RETURN_ORDER_NOT_EXISTS = new ErrorCode(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_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 ========== // ========== 验车模板管理 1-008-011-000 ==========
ErrorCode INSPECTION_TEMPLATE_NOT_EXISTS = new ErrorCode(1_008_011_000, "验车模板不存在"); ErrorCode INSPECTION_TEMPLATE_NOT_EXISTS = new ErrorCode(1_008_011_000, "验车模板不存在");

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,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>