From f87ce86307056f63f79312178037cc8714eb2b13 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 29 九月 2026 14:17:52 +0800
Subject: [PATCH] feat(docs): 添加档案管理模块业务流程文档

---
 yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/service/fund/ResearchFundRecordServiceImpl.java |  174 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 159 insertions(+), 15 deletions(-)

diff --git a/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/service/fund/ResearchFundRecordServiceImpl.java b/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/service/fund/ResearchFundRecordServiceImpl.java
index c1189bd..60fcd7b 100644
--- a/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/service/fund/ResearchFundRecordServiceImpl.java
+++ b/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/service/fund/ResearchFundRecordServiceImpl.java
@@ -1,7 +1,13 @@
 package cn.iocoder.yudao.module.research.service.fund;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.finance.api.reimbursement.FinanceReimbursementApi;
+import cn.iocoder.yudao.module.finance.api.reimbursement.dto.FinanceReimbursementCreateReqDTO;
+import cn.iocoder.yudao.module.finance.api.reimbursement.dto.FinanceReimbursementRespDTO;
+import cn.iocoder.yudao.module.finance.api.reimbursement.dto.FinanceReimbursementUpdateReqDTO;
 import cn.iocoder.yudao.module.research.controller.admin.fund.vo.ResearchFundRecordSaveReqVO;
 import cn.iocoder.yudao.module.research.dal.dataobject.application.ResearchApplicationDO;
 import cn.iocoder.yudao.module.research.dal.dataobject.fund.ResearchFundRecordDO;
@@ -10,6 +16,7 @@
 import cn.iocoder.yudao.module.research.enums.ResearchApplicationStatusEnum;
 import cn.iocoder.yudao.module.research.enums.ResearchFundStatusEnum;
 import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -17,16 +24,21 @@
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static cn.iocoder.yudao.module.research.enums.ErrorCodeConstants.*;
 
 /**
  * 缁忚垂鎷ㄤ粯璁板綍 Service 瀹炵幇绫�
  *
- * 涓�鏈熷彧鍦ㄦ湰妯″潡鍐呰惤鍦版嫧娆捐褰曪紙鎵规/閲戦/鏃ユ湡/鏀舵鏂�/鍑瘉闄勪欢锛夛紝涓嶄笌璐㈠姟妯″潡鍚屾锛�
+ * 姣忔潯鎷ㄦ璁板綍閮戒細鑷姩鐢熸垚骞舵彁浜や竴寮犲搴旂殑璐㈠姟鎶ラ攢鍗曪紙1:1锛岄噾棰濅竴鑷达級锛�
+ * 鎶ラ攢鍗曞鎵圭粨鏋滅敱 {@code ResearchFundStatusListener} 鍥炲啓鏈彴璐︾姸鎬侊細
+ * 寰呮嫧浠�(0) 鈫� 鎶ラ攢瀹℃壒涓�(15) 鈫� 宸叉嫧浠�(20) / 宸查┏鍥�(30)锛涜储鍔$鎾ゅ洖鍒欓��鍥炲緟鎷ㄤ粯(0)銆�
  * 绱鎷ㄤ粯閲戦浠ョ敵鎶ュ崟 budget_amount锛堢敵璇烽绠楋級涓轰笂闄愩��
  */
 @Service
@@ -35,6 +47,9 @@
 public class ResearchFundRecordServiceImpl implements ResearchFundRecordService {
 
     public static final String FUND_RECORD_TYPE = "research_fund_record";
+
+    /** 璐㈠姟鎶ラ攢鍗曠姸鎬侊細鑽夌 */
+    private static final Integer FINANCE_STATUS_DRAFT = 0;
 
     @Resource
     private ResearchFundRecordMapper fundRecordMapper;
@@ -45,22 +60,50 @@
     @Resource
     private StorageAttachmentApi storageAttachmentApi;
 
+    @Resource
+    private FinanceReimbursementApi financeReimbursementApi;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createFundRecord(ResearchFundRecordSaveReqVO createReqVO) {
         ResearchApplicationDO application = validateApplicationApproved(createReqVO.getApplicationId());
-        validateVoucherRequired(createReqVO);
+        validateVoucherRequired(createReqVO.getBlobIds(), createReqVO.getBatchName());
 
         // 绱鏍¢獙锛氭柊鐧昏閲戦 + 璇ョ敵鎶ュ崟宸叉湁璁板綍閲戦锛屼笉寰楄秴杩囩敵璇烽绠�
-        BigDecimal registered = sumAmount(fundRecordMapper.selectListByApplicationId(application.getId()), null);
+        List<ResearchFundRecordDO> existList = fundRecordMapper.selectListByApplicationId(application.getId());
+        BigDecimal registered = sumAmount(existList, null);
         validateAmountNotExceeded(createReqVO.getAmount(), registered, application.getBudgetAmount());
 
+        // 1. 钀借棰樹晶鍙拌处锛堢姸鎬佸厛缃緟鎷ㄤ粯锛屾姤閿�鎻愪氦鎴愬姛鍚庢敼涓烘姤閿�瀹℃壒涓級
         ResearchFundRecordDO fundRecord = BeanUtils.toBean(createReqVO, ResearchFundRecordDO.class,
                 in -> in.setId(null));
+        fundRecord.setStatus(ResearchFundStatusEnum.PENDING.getStatus());
         fundRecordMapper.insert(fundRecord);
 
+        // 2. 鍑瘉闄勪欢鍚屾椂缁戝畾鍒拌棰樺彴璐︼紙璐㈠姟渚ф姤閿�鍗曠敱 finance 妯″潡鑷缁戝畾鍚屼竴鎵� blob锛�
         storageAttachmentApi.bindAttachments("file", FUND_RECORD_TYPE,
                 fundRecord.getId(), createReqVO.getBlobIds());
+
+        // 3. 鑷姩鐢熸垚骞舵彁浜よ储鍔℃姤閿�鍗�
+        String title = buildReimbursementTitle(application, createReqVO, existList.size() + 1);
+        FinanceReimbursementCreateReqDTO reqDTO = new FinanceReimbursementCreateReqDTO();
+        reqDTO.setTitle(title);
+        reqDTO.setTotalAmount(createReqVO.getAmount());
+        reqDTO.setApplyDate(createReqVO.getPayDate() != null ? createReqVO.getPayDate() : LocalDate.now());
+        reqDTO.setRemark(createReqVO.getRemark());
+        reqDTO.setBlobIds(createReqVO.getBlobIds());
+        reqDTO.setExpenseDescription(title);
+        reqDTO.setApplicantId(getLoginUserId());
+        Long reimbursementId = financeReimbursementApi.createReimbursementAndSubmit(reqDTO).getCheckedData();
+        FinanceReimbursementRespDTO reimbursement =
+                financeReimbursementApi.getReimbursement(reimbursementId).getCheckedData();
+
+        // 4. 鍥炲啓璐㈠姟鎶ラ攢鍗曚俊鎭苟杩涘叆銆屾姤閿�瀹℃壒涓��
+        fundRecordMapper.updateById(new ResearchFundRecordDO()
+                .setId(fundRecord.getId())
+                .setStatus(ResearchFundStatusEnum.APPROVING.getStatus())
+                .setReimbursementId(reimbursementId)
+                .setReimbursementNo(reimbursement != null ? reimbursement.getNo() : null));
         return fundRecord.getId();
     }
 
@@ -68,21 +111,51 @@
     @Transactional(rollbackFor = Exception.class)
     public void updateFundRecord(ResearchFundRecordSaveReqVO updateReqVO) {
         ResearchFundRecordDO exist = validateFundRecordExists(updateReqVO.getId());
-        if (!ResearchFundStatusEnum.PENDING.getStatus().equals(exist.getStatus())) {
+        if (!isEditable(exist.getStatus())) {
             throw exception(FUND_UPDATE_FAIL_STATUS,
                     exist.getId(), ResearchFundStatusEnum.nameOf(exist.getStatus()));
         }
         // 璁板綍褰掑睘鐢虫姤鍗曚笉鍙彉鏇达紝闃叉鎶婃嫧娆炬尓鍒板埆鐨勮棰樹笅
         ResearchApplicationDO application = validateApplicationApproved(exist.getApplicationId());
-        validateVoucherRequired(updateReqVO);
+        validateVoucherRequired(updateReqVO.getBlobIds(), updateReqVO.getBatchName());
 
-        BigDecimal registered = sumAmount(fundRecordMapper.selectListByApplicationId(exist.getApplicationId()),
-                exist.getId());
+        BigDecimal registered = sumAmount(
+                fundRecordMapper.selectListByApplicationId(exist.getApplicationId()), exist.getId());
         validateAmountNotExceeded(updateReqVO.getAmount(), registered, application.getBudgetAmount());
 
-        ResearchFundRecordDO updateObj = BeanUtils.toBean(updateReqVO, ResearchFundRecordDO.class);
-        updateObj.setApplicationId(exist.getApplicationId());
-        fundRecordMapper.updateById(updateObj);
+        // 1. 璐㈠姟渚т慨鏀规姤閿�鍗曞苟閲嶆柊鎻愪氦瀹℃壒锛堝厛鍋氾紝澶辫触鍒欒棰樹晶涓嶈惤浠讳綍鍙樻洿锛�
+        Long reimbursementId = exist.getReimbursementId();
+        if (reimbursementId == null) {
+            throw exception(FUND_REIMBURSEMENT_NOT_EXISTS, exist.getId(), "绌�");
+        }
+        FinanceReimbursementRespDTO reimbursement =
+                financeReimbursementApi.getReimbursement(reimbursementId).getCheckedData();
+        if (reimbursement == null) {
+            throw exception(FUND_REIMBURSEMENT_NOT_EXISTS, exist.getId(), reimbursementId);
+        }
+        String title = buildReimbursementTitle(application, updateReqVO, null);
+        FinanceReimbursementUpdateReqDTO reqDTO = new FinanceReimbursementUpdateReqDTO();
+        reqDTO.setReimbursementId(reimbursementId);
+        reqDTO.setTitle(title);
+        reqDTO.setTotalAmount(updateReqVO.getAmount());
+        reqDTO.setApplyDate(updateReqVO.getPayDate() != null ? updateReqVO.getPayDate() : LocalDate.now());
+        reqDTO.setRemark(updateReqVO.getRemark());
+        reqDTO.setBlobIds(updateReqVO.getBlobIds());
+        reqDTO.setExpenseDescription(title);
+        financeReimbursementApi.updateReimbursementAndResubmit(reqDTO).getCheckedData();
+
+        // 2. 鍥炲啓璇鹃渚у彴璐︼細娓呯┖椹冲洖淇℃伅锛屽洖鍒般�屾姤閿�瀹℃壒涓��
+        fundRecordMapper.update(null, new LambdaUpdateWrapper<ResearchFundRecordDO>()
+                .eq(ResearchFundRecordDO::getId, exist.getId())
+                .set(ResearchFundRecordDO::getBatchName, updateReqVO.getBatchName())
+                .set(ResearchFundRecordDO::getAmount, updateReqVO.getAmount())
+                .set(ResearchFundRecordDO::getPayDate, updateReqVO.getPayDate())
+                .set(ResearchFundRecordDO::getPayee, updateReqVO.getPayee())
+                .set(ResearchFundRecordDO::getRemark, updateReqVO.getRemark())
+                .set(ResearchFundRecordDO::getStatus, ResearchFundStatusEnum.APPROVING.getStatus())
+                .set(ResearchFundRecordDO::getReimbursementNo, reimbursement.getNo())
+                .set(ResearchFundRecordDO::getRejectReason, "")
+                .set(ResearchFundRecordDO::getRejectTime, null));
 
         storageAttachmentApi.updateAttachments("file", FUND_RECORD_TYPE,
                 updateReqVO.getId(), updateReqVO.getBlobIds());
@@ -97,6 +170,20 @@
                     exist.getId(), ResearchFundStatusEnum.nameOf(exist.getStatus()));
         }
 
+        // 鑱斿姩娓呯悊璐㈠姟渚ц崏绋挎姤閿�鍗曪紙宸茶璐㈠姟渚у垹闄ゆ椂鐩存帴璺宠繃锛�
+        Long reimbursementId = exist.getReimbursementId();
+        if (reimbursementId != null) {
+            FinanceReimbursementRespDTO reimbursement =
+                    financeReimbursementApi.getReimbursement(reimbursementId).getCheckedData();
+            if (reimbursement != null && reimbursement.getStatus() != null) {
+                if (!FINANCE_STATUS_DRAFT.equals(reimbursement.getStatus())) {
+                    throw exception(FUND_REIMBURSEMENT_STATUS_CHANGED,
+                            reimbursement.getNo(), financeStatusName(reimbursement.getStatus()));
+                }
+                financeReimbursementApi.deleteReimbursement(reimbursementId);
+            }
+        }
+
         fundRecordMapper.deleteById(id);
         storageAttachmentApi.deleteAttachmentsByRecord(FUND_RECORD_TYPE, id);
     }
@@ -106,7 +193,39 @@
         return fundRecordMapper.selectListByApplicationId(applicationId);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateFundStatusByReimbursement(Long reimbursementId, Integer bpmStatus, String reason) {
+        ResearchFundRecordDO fundRecord = fundRecordMapper.selectByReimbursementId(reimbursementId);
+        if (fundRecord == null) {
+            log.warn("[updateFundStatusByReimbursement] 鏈壘鍒板叧鑱旀姤閿�鍗�({})鐨勭粡璐规嫧浠樿褰�", reimbursementId);
+            return;
+        }
+
+        boolean rejected = BpmTaskStatusEnum.REJECT.getStatus().equals(bpmStatus);
+        LambdaUpdateWrapper<ResearchFundRecordDO> update = new LambdaUpdateWrapper<ResearchFundRecordDO>()
+                .eq(ResearchFundRecordDO::getId, fundRecord.getId());
+        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmStatus)) {
+            update.set(ResearchFundRecordDO::getStatus, ResearchFundStatusEnum.PAID.getStatus());
+        } else if (rejected) {
+            update.set(ResearchFundRecordDO::getStatus, ResearchFundStatusEnum.REJECTED.getStatus());
+        } else {
+            // 鎾ゅ洖/鍙栨秷锛氶��鍥炪�屽緟鎷ㄤ粯銆嶏紝渚涜棰樹晶淇敼鍚庨噸鏂板彂璧锋姤閿�
+            update.set(ResearchFundRecordDO::getStatus, ResearchFundStatusEnum.PENDING.getStatus());
+        }
+        update.set(ResearchFundRecordDO::getRejectReason,
+                        rejected ? StrUtil.blankToDefault(reason, "璐㈠姟鏈~鍐欓┏鍥炲師鍥�") : "")
+                .set(ResearchFundRecordDO::getRejectTime, rejected ? LocalDateTime.now() : null);
+        fundRecordMapper.update(null, update);
+    }
+
     // =================== 鍐呴儴鏂规硶 ===================
+
+    /** 鍙湁銆屽緟鎷ㄤ粯銆嶆垨銆屽凡椹冲洖銆嶇殑鍙拌处鎵嶈兘淇敼骞堕噸鏂板彂璧锋姤閿� */
+    private boolean isEditable(Integer status) {
+        return ResearchFundStatusEnum.PENDING.getStatus().equals(status)
+                || ResearchFundStatusEnum.REJECTED.getStatus().equals(status);
+    }
 
     private ResearchFundRecordDO validateFundRecordExists(Long id) {
         ResearchFundRecordDO fundRecord = fundRecordMapper.selectById(id);
@@ -129,12 +248,22 @@
         return application;
     }
 
-    /** 鐘舵�佷负銆屽凡鎷ㄤ粯銆嶆椂蹇呴』涓婁紶鍑瘉闄勪欢 */
-    private void validateVoucherRequired(ResearchFundRecordSaveReqVO reqVO) {
-        if (ResearchFundStatusEnum.PAID.getStatus().equals(reqVO.getStatus())
-                && CollUtil.isEmpty(reqVO.getBlobIds())) {
-            throw exception(FUND_VOUCHER_REQUIRED);
+    /** 鐧昏涓庝慨鏀规嫧娆鹃兘蹇呴』涓婁紶鍑瘉锛氬嚟璇佷細闅忚嚜鍔ㄧ敓鎴愮殑璐㈠姟鎶ラ攢鍗曢�佷氦璐㈠姟瀹℃壒 */
+    private void validateVoucherRequired(List<Long> blobIds, String batchName) {
+        if (CollUtil.isEmpty(blobIds)) {
+            throw exception(FUND_VOUCHER_REQUIRED, StrUtil.isBlank(batchName) ? "鏈" : batchName);
         }
+    }
+
+    /** 鎶ラ攢鍗曟爣棰橈細璇鹃銆旇棰樺悕銆曠涓�鏈熺粡璐规嫧娆� / 璇鹃銆旇棰樺悕銆曠2娆$粡璐规嫧娆� */
+    private String buildReimbursementTitle(ResearchApplicationDO application,
+                                           ResearchFundRecordSaveReqVO reqVO, Integer seq) {
+        String batch = reqVO.getBatchName();
+        if (StrUtil.isBlank(batch)) {
+            int no = seq != null ? seq : fundRecordMapper.selectListByApplicationId(application.getId()).size() + 1;
+            batch = "绗�" + no + "娆�";
+        }
+        return "璇鹃銆�" + application.getTitle() + "銆�" + batch + "缁忚垂鎷ㄦ";
     }
 
     /** 绱娴嚭鏍¢獙锛氭湰娆� + 宸茬櫥璁帮紙鎺掗櫎 excludeId 鑷韩锛変笉寰楄秴杩囩敵璇烽绠� */
@@ -159,4 +288,19 @@
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
     }
 
+    /** 璐㈠姟鎶ラ攢鍗曠姸鎬佷腑鏂囧悕锛坒inance 妯″潡鏈澶栨毚闇茬姸鎬佹灇涓撅紝姝ゅ鎸夌姸鎬佸�艰浆鍙鏂囨锛� */
+    private String financeStatusName(Integer status) {
+        if (status == null) {
+            return "鏈煡";
+        }
+        return switch (status) {
+            case 0 -> "鑽夌";
+            case 15 -> "瀹℃壒涓�";
+            case 20 -> "宸查�氳繃";
+            case 35 -> "宸查┏鍥�";
+            case 40 -> "宸插綊妗�";
+            default -> String.valueOf(status);
+        };
+    }
+
 }

--
Gitblit v1.9.3