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<ResearchFundRecordDO> 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<ResearchFundRecordDO> 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);
|
}
|
|
}
|