package cn.iocoder.yudao.module.research.service.fund; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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; import cn.iocoder.yudao.module.research.dal.mysql.application.ResearchApplicationMapper; import cn.iocoder.yudao.module.research.dal.mysql.fund.ResearchFundRecordMapper; 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 jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.research.enums.ErrorCodeConstants.*; /** * 经费拨付记录 Service 实现类 * * 一期只在本模块内落地拨款记录(批次/金额/日期/收款方/凭证附件),不与财务模块同步; * 累计拨付金额以申报单 budget_amount(申请预算)为上限。 */ @Service @Validated @Slf4j public class ResearchFundRecordServiceImpl implements ResearchFundRecordService { public static final String FUND_RECORD_TYPE = "research_fund_record"; @Resource private ResearchFundRecordMapper fundRecordMapper; @Resource private ResearchApplicationMapper applicationMapper; @Resource private StorageAttachmentApi storageAttachmentApi; @Override @Transactional(rollbackFor = Exception.class) public Long createFundRecord(ResearchFundRecordSaveReqVO createReqVO) { ResearchApplicationDO application = validateApplicationApproved(createReqVO.getApplicationId()); validateVoucherRequired(createReqVO); // 累计校验:新登记金额 + 该申报单已有记录金额,不得超过申请预算 BigDecimal registered = sumAmount(fundRecordMapper.selectListByApplicationId(application.getId()), null); validateAmountNotExceeded(createReqVO.getAmount(), registered, application.getBudgetAmount()); ResearchFundRecordDO fundRecord = BeanUtils.toBean(createReqVO, ResearchFundRecordDO.class, in -> in.setId(null)); fundRecordMapper.insert(fundRecord); storageAttachmentApi.bindAttachments("file", FUND_RECORD_TYPE, fundRecord.getId(), createReqVO.getBlobIds()); return fundRecord.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateFundRecord(ResearchFundRecordSaveReqVO updateReqVO) { ResearchFundRecordDO exist = validateFundRecordExists(updateReqVO.getId()); if (!ResearchFundStatusEnum.PENDING.getStatus().equals(exist.getStatus())) { throw exception(FUND_UPDATE_FAIL_STATUS, exist.getId(), ResearchFundStatusEnum.nameOf(exist.getStatus())); } // 记录归属申报单不可变更,防止把拨款挪到别的课题下 ResearchApplicationDO application = validateApplicationApproved(exist.getApplicationId()); validateVoucherRequired(updateReqVO); 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); storageAttachmentApi.updateAttachments("file", FUND_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteFundRecord(Long id) { ResearchFundRecordDO exist = validateFundRecordExists(id); if (!ResearchFundStatusEnum.PENDING.getStatus().equals(exist.getStatus())) { throw exception(FUND_DELETE_FAIL_STATUS, exist.getId(), ResearchFundStatusEnum.nameOf(exist.getStatus())); } fundRecordMapper.deleteById(id); storageAttachmentApi.deleteAttachmentsByRecord(FUND_RECORD_TYPE, id); } @Override public List getFundRecordListByApplicationId(Long applicationId) { return fundRecordMapper.selectListByApplicationId(applicationId); } // =================== 内部方法 =================== private ResearchFundRecordDO validateFundRecordExists(Long id) { ResearchFundRecordDO fundRecord = fundRecordMapper.selectById(id); if (fundRecord == null) { throw exception(FUND_NOT_EXISTS); } return fundRecord; } /** 只有已立项的课题才能登记经费拨付 */ private ResearchApplicationDO validateApplicationApproved(Long applicationId) { ResearchApplicationDO application = applicationMapper.selectById(applicationId); if (application == null) { throw exception(APPLICATION_NOT_EXISTS); } if (!ResearchApplicationStatusEnum.APPROVED.getStatus().equals(application.getStatus())) { throw exception(FUND_RECORD_FAIL_STATUS, application.getNo(), ResearchApplicationStatusEnum.nameOf(application.getStatus())); } return application; } /** 状态为「已拨付」时必须上传凭证附件 */ private void validateVoucherRequired(ResearchFundRecordSaveReqVO reqVO) { if (ResearchFundStatusEnum.PAID.getStatus().equals(reqVO.getStatus()) && CollUtil.isEmpty(reqVO.getBlobIds())) { throw exception(FUND_VOUCHER_REQUIRED); } } /** 累计浮出校验:本次 + 已登记(排除 excludeId 自身)不得超过申请预算 */ private void validateAmountNotExceeded(BigDecimal amount, BigDecimal registered, BigDecimal budget) { BigDecimal current = amount == null ? BigDecimal.ZERO : amount; BigDecimal total = registered.add(current); BigDecimal limit = budget == null ? BigDecimal.ZERO : budget; if (total.compareTo(limit) > 0) { throw exception(FUND_AMOUNT_EXCEEDED, current.toPlainString(), registered.toPlainString(), total.toPlainString(), limit.toPlainString()); } } private BigDecimal sumAmount(List list, Long excludeId) { if (CollUtil.isEmpty(list)) { return BigDecimal.ZERO; } return list.stream() .filter(item -> !Objects.equals(item.getId(), excludeId)) .map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount()) .reduce(BigDecimal.ZERO, BigDecimal::add); } }