From a0d62d13f1d537e84e13d8ee4d8b37b27439fa00 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 29 九月 2026 10:48:36 +0800
Subject: [PATCH] feat(finance): 对接课题经费拨付与财务报销模块
---
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