1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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);
    }
 
}