From 620bb4712a31791231c4381581f0f60088f079fe Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 27 五月 2026 14:03:45 +0800
Subject: [PATCH] Merge branch 'refs/heads/dev_New_pro' into dev_宁夏_英泽防锈

---
 src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java |  544 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 544 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java b/src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java
new file mode 100644
index 0000000..305ea9d
--- /dev/null
+++ b/src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java
@@ -0,0 +1,544 @@
+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;
+
+/**
+ * <p>
+ * 鎶ラ攢鍗曚富琛� 鏈嶅姟瀹炵幇绫�
+ * </p>
+ *
+ * @author 鑺杞欢锛堟睙鑻忥級鏈夐檺鍏徃
+ * @since 2026-05-21 09:56:15
+ */
+@Service
+@RequiredArgsConstructor
+public class FinReimbursementServiceImpl extends ServiceImpl<FinReimbursementMapper, FinReimbursement> 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<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page) {
+        IPage<FinReimbursementVo> 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", finReimbursementDto.getCreateTime() != null ? finReimbursementDto.getCreateTime() : LocalDateTime.now());
+        List<FinReimbursementDetail> 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<FinReimbursementDetail> 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<FinReimbursementDetail> existingDetails = finReimbursementDetailMapper.selectList(
+                new LambdaQueryWrapper<FinReimbursementDetail>()
+                        .eq(FinReimbursementDetail::getReimbursementId, reimbursementId));
+        Set<Long> existingDetailIds = existingDetails.stream()
+                .map(FinReimbursementDetail::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        // 鏂版槑缁嗕腑鏈塈D鐨� 鈫� 鏇存柊锛涙棤ID鐨� 鈫� 鏂板
+        Set<Long> 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<FinReimbursementTravel>()
+                        .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, 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("鎶ラ攢鍗旾D涓嶈兘涓虹┖");
+        }
+
+        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<FinReimbursementDetail>()
+                        .eq(FinReimbursementDetail::getReimbursementId, reimbursement.getId())
+                        .orderByAsc(FinReimbursementDetail::getRowNo)
+        ));
+
+        if (isTravelReimbursement(reimbursement.getReimbursementType())) {
+            vo.setTravel(finReimbursementTravelMapper.selectOne(
+                    new LambdaQueryWrapper<FinReimbursementTravel>()
+                            .eq(FinReimbursementTravel::getReimbursementId, reimbursement.getId())
+                            .last("LIMIT 1")
+            ));
+        }
+        vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, reimbursement.getId()));
+        //瀹℃壒璁板綍杩斿洖
+        vo.setTasks(approvalTaskService.list(new LambdaQueryWrapper<ApprovalTask>().eq(ApprovalTask::getInstanceId, reimbursement.getApprovalInstanceId())));
+        vo.setRecords(approvalRecordService.list(new LambdaQueryWrapper<ApprovalRecord>().eq(ApprovalRecord::getInstanceId, reimbursement.getApprovalInstanceId())));
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean delete(List<Long> ids) {
+        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, ids);
+        //鍏堝垹闄ゆ槑缁�
+        finReimbursementDetailMapper.delete(new LambdaQueryWrapper<FinReimbursementDetail>().in(FinReimbursementDetail::getReimbursementId, ids));
+        //鍒犻櫎宸梾
+        finReimbursementTravelMapper.delete(new LambdaQueryWrapper<FinReimbursementTravel>().in(FinReimbursementTravel::getReimbursementId, ids));
+        //鍒犻櫎涓昏〃
+        int rows = finReimbursementMapper.delete(new LambdaQueryWrapper<FinReimbursement>().in(FinReimbursement::getId, ids));
+        return rows == ids.size();
+    }
+
+    private String validateUpdateParam(FinReimbursementDto finReimbursementDto) {
+        if (finReimbursementDto == null || finReimbursementDto.getId() == null) {
+            throw new ServiceException("鎶ラ攢鍗旾D涓嶈兘涓虹┖");
+        }
+        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<FinReimbursementDetail> 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<FinReimbursementDetail> 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.getCreateTime() != null ? approvalInstanceDto.getCreateTime() : LocalDateTime.now()));
+        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<ApprovalTask> 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<ApprovalTask> createApprovalNodes(ApprovalInstanceDto approvalInstanceDto, List<ApprovalTemplateNodeDto> nodes) {
+        List<ApprovalTask> 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<ApprovalTask> 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<ApprovalTemplateNodeDto> 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<ApprovalTemplateNodeApproverDto> 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<ApprovalTask> tasks) {
+        if (instance == null || tasks == null || tasks.isEmpty()) {
+            return;
+        }
+        List<Long> 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(FinReimbursement existing, Long reimbursementId) {
+        if (existing == null || existing.getApprovalInstanceId() == null) {
+            return;
+        }
+        Long approvalInstanceId = existing.getApprovalInstanceId();
+        if (!"REJECTED".equals(existing.getBillStatus())) {
+            approvalInstanceService.delete(Collections.singletonList(approvalInstanceId));
+        }
+        clearApprovalBinding(reimbursementId);
+    }
+
+    private void clearApprovalBinding(Long reimbursementId) {
+        int rows = finReimbursementMapper.update(
+                null,
+                Wrappers.<FinReimbursement>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);
+    }
+}

--
Gitblit v1.9.3