package com.ruoyi.approve.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.approve.bean.dto.ApprovalInstanceDto; import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeApproverDto; import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto; import com.ruoyi.approve.bean.dto.FinReimbursementDto; import com.ruoyi.approve.bean.vo.FinReimbursementVo; import com.ruoyi.approve.mapper.ApprovalInstanceMapper; import com.ruoyi.approve.mapper.FinReimbursementDetailMapper; import com.ruoyi.approve.mapper.FinReimbursementMapper; import com.ruoyi.approve.mapper.FinReimbursementTravelMapper; import com.ruoyi.approve.pojo.*; import com.ruoyi.basic.enums.ApplicationTypeEnum; import com.ruoyi.basic.enums.RecordTypeEnum; import com.ruoyi.basic.utils.FileUtil; import com.ruoyi.approve.service.*; import com.ruoyi.common.enums.TypeEnums; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.OrderUtils; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.project.system.service.ISysNoticeService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; /** *

* 报销单主表 服务实现类 *

* * @author 芯导软件(江苏)有限公司 * @since 2026-05-21 09:56:15 */ @Service @RequiredArgsConstructor public class FinReimbursementServiceImpl extends ServiceImpl implements FinReimbursementService { private static final String BILL_STATUS_DRAFT = "DRAFT"; private static final String BILL_STATUS_IN_APPROVAL = "IN_APPROVAL"; private static final String NODE_STATUS_WAITING = "WAITING"; private final ApprovalInstanceMapper approvalInstanceMapper; private final ApprovalInstanceService approvalInstanceService; private final ApprovalInstanceNodeService approvalInstanceNodeService; private final ApprovalTaskService approvalTaskService; private final ApprovalRecordService approvalRecordService; private final FinReimbursementMapper finReimbursementMapper; private final FinReimbursementTravelMapper finReimbursementTravelMapper; private final FinReimbursementDetailMapper finReimbursementDetailMapper; private final FileUtil fileUtil; private final ISysNoticeService sysNoticeService; @Override public IPage listPage(FinReimbursementDto finReimbursementDto, Page page) { IPage finReimbursementVoIPage = finReimbursementMapper.listPage(finReimbursementDto, page); finReimbursementVoIPage.getRecords().forEach(vo -> { vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, vo.getId())); }); return finReimbursementVoIPage; } @Override @Transactional(rollbackFor = Exception.class) public Boolean add(FinReimbursementDto finReimbursementDto) { String billStatus = validateAddParam(finReimbursementDto); // 生成报销单号 String billNo = OrderUtils.countTodayByCreateTime(finReimbursementMapper, "BXD", "bill_no"); List details = finReimbursementDto.getDetails(); BigDecimal totalAmount = details.stream() .map(FinReimbursementDetail::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); FinReimbursement reimbursement = buildReimbursement(finReimbursementDto, billNo, totalAmount, billStatus); // 保存报销单主表 boolean saved = this.save(reimbursement); if (!saved || reimbursement.getId() == null) { throw new ServiceException("新增报销单失败"); } Long reimbursementId = reimbursement.getId(); // 保存差旅报销扩展信息(报销类型为差旅报销时) FinReimbursementTravel travel = finReimbursementDto.getTravel(); if (isTravelReimbursement(finReimbursementDto.getReimbursementType())) { travel.setReimbursementId(reimbursementId); int travelRows = finReimbursementTravelMapper.insert(travel); if (travelRows != 1) { throw new ServiceException("新增差旅报销扩展信息失败"); } } // 保存报销单明细 for (int i = 0; i < details.size(); i++) { FinReimbursementDetail detail = details.get(i); detail.setId(null); detail.setReimbursementId(reimbursementId); detail.setRowNo(i + 1); int detailRows = finReimbursementDetailMapper.insert(detail); if (detailRows != 1) { throw new ServiceException("新增报销单明细失败"); } } if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) { startApproval(reimbursement, finReimbursementDto); } fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs()); return true; } @Override @Transactional(rollbackFor = Exception.class) public Boolean update(FinReimbursementDto finReimbursementDto) { String billStatus = validateUpdateParam(finReimbursementDto); Long reimbursementId = finReimbursementDto.getId(); FinReimbursement existing = finReimbursementMapper.selectById(reimbursementId); if (existing == null) { throw new ServiceException("报销单不存在"); } // 计算明细汇总金额 List details = finReimbursementDto.getDetails(); BigDecimal totalAmount = details.stream() .map(FinReimbursementDetail::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 更新主表 FinReimbursement reimbursement = buildReimbursement( finReimbursementDto, existing.getBillNo(), totalAmount, billStatus ); reimbursement.setId(reimbursementId); int mainRows = finReimbursementMapper.updateById(reimbursement); if (mainRows != 1) { throw new ServiceException("更新报销单主表失败"); } // 查询数据库中已有的明细 List existingDetails = finReimbursementDetailMapper.selectList( new LambdaQueryWrapper() .eq(FinReimbursementDetail::getReimbursementId, reimbursementId)); Set existingDetailIds = existingDetails.stream() .map(FinReimbursementDetail::getId) .filter(Objects::nonNull) .collect(Collectors.toSet()); // 新明细中有ID的 → 更新;无ID的 → 新增 Set submittedDetailIds = new HashSet<>(); for (int i = 0; i < details.size(); i++) { FinReimbursementDetail detail = details.get(i); detail.setReimbursementId(reimbursementId); detail.setRowNo(i + 1); if (detail.getId() != null && existingDetailIds.contains(detail.getId())) { finReimbursementDetailMapper.updateById(detail); submittedDetailIds.add(detail.getId()); } else { detail.setId(null); finReimbursementDetailMapper.insert(detail); } } // 数据库中已有但新明细中没有的 → 删除 for (Long existingId : existingDetailIds) { if (!submittedDetailIds.contains(existingId)) { finReimbursementDetailMapper.deleteById(existingId); } } // 差旅扩展:有则更新,无则新增 FinReimbursementTravel existingTravel = finReimbursementTravelMapper.selectOne( new LambdaQueryWrapper() .eq(FinReimbursementTravel::getReimbursementId, reimbursementId) .last("LIMIT 1")); FinReimbursementTravel travel = finReimbursementDto.getTravel(); if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && travel != null) { travel.setReimbursementId(reimbursementId); if (existingTravel != null) { travel.setId(existingTravel.getId()); finReimbursementTravelMapper.updateById(travel); } else { travel.setId(null); finReimbursementTravelMapper.insert(travel); } } resetApprovalFlow(existing.getApprovalInstanceId(), reimbursementId); if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) { reimbursement.setApprovalInstanceId(null); startApproval(reimbursement, finReimbursementDto); } fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs()); return true; } @Override public FinReimbursementVo detail(Long id) { if (id == null ) { throw new ServiceException("报销单ID不能为空"); } FinReimbursement reimbursement = finReimbursementMapper.selectById(id); if (reimbursement == null) { throw new ServiceException("报销单不存在"); } FinReimbursementVo vo = new FinReimbursementVo(); vo.setId(reimbursement.getId()); vo.setBillNo(reimbursement.getBillNo()); vo.setReimbursementType(reimbursement.getReimbursementType()); vo.setExpenseType(reimbursement.getExpenseType()); vo.setApplicantId(reimbursement.getApplicantId()); vo.setApplicantCode(reimbursement.getApplicantCode()); vo.setApplicantName(reimbursement.getApplicantName()); vo.setApplicantDeptId(reimbursement.getApplicantDeptId()); vo.setApplicantDeptName(reimbursement.getApplicantDeptName()); vo.setReason(reimbursement.getReason()); vo.setApplyAmount(reimbursement.getApplyAmount()); vo.setDetailTotalAmount(reimbursement.getDetailTotalAmount()); vo.setPayeeName(reimbursement.getPayeeName()); vo.setPayeeAccount(reimbursement.getPayeeAccount()); vo.setPayeeBank(reimbursement.getPayeeBank()); vo.setApprovalInstanceId(reimbursement.getApprovalInstanceId()); vo.setApproveProcessId(reimbursement.getApproveProcessId()); vo.setBillStatus(reimbursement.getBillStatus()); vo.setApprovedTime(reimbursement.getApprovedTime()); vo.setPaidTime(reimbursement.getPaidTime()); vo.setAccountExpenseId(reimbursement.getAccountExpenseId()); vo.setRemark(reimbursement.getRemark()); vo.setTenantId(reimbursement.getTenantId()); vo.setCreateUser(reimbursement.getCreateUser()); vo.setCreateTime(reimbursement.getCreateTime()); vo.setUpdateUser(reimbursement.getUpdateUser()); vo.setUpdateTime(reimbursement.getUpdateTime()); vo.setDeptId(reimbursement.getDeptId()); vo.setDeleted(reimbursement.getDeleted()); vo.setDetails(finReimbursementDetailMapper.selectList( new LambdaQueryWrapper() .eq(FinReimbursementDetail::getReimbursementId, reimbursement.getId()) .orderByAsc(FinReimbursementDetail::getRowNo) )); if (isTravelReimbursement(reimbursement.getReimbursementType())) { vo.setTravel(finReimbursementTravelMapper.selectOne( new LambdaQueryWrapper() .eq(FinReimbursementTravel::getReimbursementId, reimbursement.getId()) .last("LIMIT 1") )); } vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, reimbursement.getId())); //审批记录返回 vo.setTasks(approvalTaskService.list(new LambdaQueryWrapper().eq(ApprovalTask::getInstanceId, reimbursement.getApprovalInstanceId()))); vo.setRecords(approvalRecordService.list(new LambdaQueryWrapper().eq(ApprovalRecord::getInstanceId, reimbursement.getApprovalInstanceId()))); return vo; } @Override @Transactional(rollbackFor = Exception.class) public Boolean delete(List ids) { fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, ids); //先删除明细 finReimbursementDetailMapper.delete(new LambdaQueryWrapper().in(FinReimbursementDetail::getReimbursementId, ids)); //删除差旅 finReimbursementTravelMapper.delete(new LambdaQueryWrapper().in(FinReimbursementTravel::getReimbursementId, ids)); //删除主表 int rows = finReimbursementMapper.delete(new LambdaQueryWrapper().in(FinReimbursement::getId, ids)); return rows == ids.size(); } private String validateUpdateParam(FinReimbursementDto finReimbursementDto) { if (finReimbursementDto == null || finReimbursementDto.getId() == null) { throw new ServiceException("报销单ID不能为空"); } if (finReimbursementDto.getReimbursementType() == null) { throw new ServiceException("报销类型不能为空"); } String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus()); if (billStatus == null) { throw new ServiceException("单据状态只支持 DRAFT 或 IN_APPROVAL"); } if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) { validateApprovalNodes(finReimbursementDto.getNodes()); } List details = finReimbursementDto.getDetails(); if (details == null || details.isEmpty()) { throw new ServiceException("报销单明细不能为空"); } for (FinReimbursementDetail detail : details) { if (detail == null) { throw new ServiceException("报销单明细不能为空"); } if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException("报销单明细金额必须大于0"); } } if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) { throw new ServiceException("差旅报销必须填写差旅扩展信息"); } if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) { throw new ServiceException("非差旅报销不允许填写差旅扩展信息"); } return billStatus; } private String validateAddParam(FinReimbursementDto finReimbursementDto) { if (finReimbursementDto == null) { throw new ServiceException("报销单数据不能为空"); } if (finReimbursementDto.getReimbursementType() == null) { throw new ServiceException("报销类型不能为空"); } String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus()); if (billStatus == null) { throw new ServiceException("单据状态只支持 DRAFT 或 IN_APPROVAL"); } if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) { validateApprovalNodes(finReimbursementDto.getNodes()); } List details = finReimbursementDto.getDetails(); if (details == null || details.isEmpty()) { throw new ServiceException("报销单明细不能为空"); } for (FinReimbursementDetail detail : details) { if (detail == null) { throw new ServiceException("报销单明细不能为空"); } if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException("报销单明细金额必须大于0"); } } if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) { throw new ServiceException("差旅报销必须填写差旅扩展信息"); } if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) { throw new ServiceException("非差旅报销不允许填写差旅扩展信息"); } return billStatus; } private FinReimbursement buildReimbursement(FinReimbursementDto finReimbursementDto, String billNo, BigDecimal totalAmount, String billStatus) { FinReimbursement reimbursement = new FinReimbursement(); reimbursement.setId(null); reimbursement.setBillNo(billNo); reimbursement.setReimbursementType(finReimbursementDto.getReimbursementType()); reimbursement.setExpenseType(finReimbursementDto.getExpenseType()); reimbursement.setApplicantId(finReimbursementDto.getApplicantId()); reimbursement.setApplicantCode(finReimbursementDto.getApplicantCode()); reimbursement.setApplicantName(finReimbursementDto.getApplicantName()); reimbursement.setApplicantDeptId(finReimbursementDto.getApplicantDeptId()); reimbursement.setApplicantDeptName(finReimbursementDto.getApplicantDeptName()); reimbursement.setReason(finReimbursementDto.getReason()); reimbursement.setApplyAmount(finReimbursementDto.getApplyAmount()); reimbursement.setDetailTotalAmount(totalAmount); reimbursement.setPayeeName(finReimbursementDto.getPayeeName()); reimbursement.setPayeeAccount(finReimbursementDto.getPayeeAccount()); reimbursement.setPayeeBank(finReimbursementDto.getPayeeBank()); reimbursement.setRemark(finReimbursementDto.getRemark()); reimbursement.setTenantId(finReimbursementDto.getTenantId()); reimbursement.setApproveProcessId(null); reimbursement.setBillStatus(billStatus); return reimbursement; } private void startApproval(FinReimbursement reimbursement, FinReimbursementDto finReimbursementDto) { Long businessType = resolveBusinessType(finReimbursementDto.getReimbursementType()); ApprovalInstanceDto approvalInstanceDto = new ApprovalInstanceDto(); approvalInstanceDto.setInstanceNo(OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no")); approvalInstanceDto.setBusinessId(reimbursement.getId()); approvalInstanceDto.setTemplateId(null); approvalInstanceDto.setTemplateName(TypeEnums.getLabelByValue(businessType) + "审批"); approvalInstanceDto.setBusinessType(businessType); approvalInstanceDto.setTitle("报销单号:" + reimbursement.getBillNo()); approvalInstanceDto.setApplicantId(reimbursement.getApplicantId() != null ? reimbursement.getApplicantId() : SecurityUtils.getUserId()); approvalInstanceDto.setApplicantName(reimbursement.getApplicantName() != null ? reimbursement.getApplicantName() : SecurityUtils.getLoginUser().getNickName()); approvalInstanceDto.setApplyTime(LocalDateTime.now()); approvalInstanceDto.setStatus("PENDING"); approvalInstanceDto.setCurrentLevel(1); boolean approvalSaved = approvalInstanceService.save(approvalInstanceDto); if (!approvalSaved || approvalInstanceDto.getId() == null) { throw new ServiceException("发起审批失败"); } List firstTasks = createApprovalNodes(approvalInstanceDto, finReimbursementDto.getNodes()); sendApproveNotice(approvalInstanceDto, firstTasks); FinReimbursement update = new FinReimbursement(); update.setId(reimbursement.getId()); update.setApprovalInstanceId(approvalInstanceDto.getId()); update.setBillStatus(BILL_STATUS_IN_APPROVAL); int rows = finReimbursementMapper.updateById(update); if (rows != 1) { throw new ServiceException("回填审批实例失败"); } } private List createApprovalNodes(ApprovalInstanceDto approvalInstanceDto, List nodes) { List firstTasks = Collections.emptyList(); for (int i = 0; i < nodes.size(); i++) { ApprovalTemplateNodeDto nodeDto = nodes.get(i); ApprovalInstanceNode instanceNode = new ApprovalInstanceNode(); instanceNode.setInstanceId(approvalInstanceDto.getId()); instanceNode.setLevelNo(nodeDto.getLevelNo()); instanceNode.setApproveType(nodeDto.getApproveType()); instanceNode.setStatus(i == 0 ? "PENDING" : NODE_STATUS_WAITING); instanceNode.setStartTime(i == 0 ? LocalDateTime.now() : null); instanceNode.setDeleted((byte) 0); approvalInstanceNodeService.save(instanceNode); List tasks = nodeDto.getApprovers().stream().map(approver -> { ApprovalTask task = new ApprovalTask(); task.setInstanceId(approvalInstanceDto.getId()); task.setNodeId(instanceNode.getId()); task.setLevelNo(instanceNode.getLevelNo()); task.setApproverId(approver.getApproverId()); task.setApproverName(approver.getApproverName()); task.setTaskStatus("PENDING"); task.setIsRead((byte) 0); task.setDeleted((byte) 0); return task; }).collect(Collectors.toList()); approvalTaskService.saveBatch(tasks); if (i == 0) { firstTasks = tasks; ApprovalRecord record = new ApprovalRecord(); record.setInstanceId(approvalInstanceDto.getId()); record.setNodeId(instanceNode.getId()); record.setOperatorId(approvalInstanceDto.getApplicantId()); record.setOperatorName(approvalInstanceDto.getApplicantName()); record.setAction("SUBMIT"); record.setComment("发起审批"); record.setDeleted((byte) 0); approvalRecordService.save(record); } } return firstTasks; } private void validateApprovalNodes(List nodes) { if (nodes == null || nodes.isEmpty()) { throw new ServiceException("提交审批时审批节点不能为空"); } for (int i = 0; i < nodes.size(); i++) { ApprovalTemplateNodeDto node = nodes.get(i); if (node == null) { throw new ServiceException("审批节点不能为空"); } if (node.getLevelNo() == null) { node.setLevelNo(i + 1); } if (!StringUtils.hasText(node.getApproveType())) { throw new ServiceException("审批节点审批方式不能为空"); } List approvers = node.getApprovers(); if (approvers == null || approvers.isEmpty()) { throw new ServiceException("审批节点审批人不能为空"); } for (ApprovalTemplateNodeApproverDto approver : approvers) { if (approver == null || approver.getApproverId() == null) { throw new ServiceException("审批人不能为空"); } } } } private void sendApproveNotice(ApprovalInstanceDto instance, List tasks) { if (instance == null || tasks == null || tasks.isEmpty()) { return; } List approverIds = tasks.stream() .map(ApprovalTask::getApproverId) .filter(id -> id != null && id > 0) .distinct() .collect(Collectors.toList()); if (approverIds.isEmpty()) { return; } String title = "报销审批"; String message = "审批单号 " + instance.getInstanceNo() + " 需要您审批"; String jumpPath = "/approvalInstance?id=" + instance.getId(); sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath); } private void resetApprovalFlow(Long approvalInstanceId, Long reimbursementId) { if (approvalInstanceId == null) { return; } approvalInstanceService.delete(Collections.singletonList(approvalInstanceId)); int rows = finReimbursementMapper.update( null, Wrappers.lambdaUpdate() .eq(FinReimbursement::getId, reimbursementId) .set(FinReimbursement::getApprovalInstanceId, null) ); if (rows != 1) { throw new ServiceException("重置审批流程失败"); } } private Long resolveBusinessType(Byte reimbursementType) { return isTravelReimbursement(reimbursementType) ? TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode() : TypeEnums.EXPENSE_APPROVAL.getCode(); } private String normalizeBillStatus(String billStatus) { if (billStatus == null) { return BILL_STATUS_DRAFT; } String normalized = billStatus.trim().toUpperCase(); if (BILL_STATUS_DRAFT.equals(normalized) || BILL_STATUS_IN_APPROVAL.equals(normalized)) { return normalized; } return null; } private boolean isTravelReimbursement(Byte reimbursementType) { return Byte.valueOf((byte) 1).equals(reimbursementType); } }