liu
11 小时以前 5d34bdd3c426bb2ca5f6627424ae67984f745603
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/reimbursement/FinanceReimbursementServiceImpl.java
@@ -2,8 +2,12 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.archive.api.document.ArchiveDocumentApi;
import cn.iocoder.yudao.module.archive.api.document.dto.ArchiveDocumentCreateReqDTO;
import cn.iocoder.yudao.module.archive.api.document.dto.ArchiveDocumentRespDTO;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
@@ -20,6 +24,9 @@
import cn.iocoder.yudao.module.finance.dal.redis.FinanceNoRedisDAO;
import cn.iocoder.yudao.module.finance.enums.FinanceReimbursementStatusEnum;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
@@ -57,6 +64,15 @@
     */
    public static final String REIMBURSEMENT_RECORD_TYPE = "finance_reimbursement";
    /** 归档来源类型:4-财务(对应 archive 模块 source_type) */
    private static final Integer ARCHIVE_SOURCE_TYPE_FINANCE = 4;
    /** 归档密级:3-保密(财务档案默认保密) */
    private static final Integer ARCHIVE_SECRET_LEVEL_SECRET = 3;
    /** 财务档案分类编码(对应 archive_category.code) */
    private static final String ARCHIVE_CATEGORY_CODE_FINANCE = "finance";
    @Resource
    private FinanceReimbursementMapper reimbursementMapper;
    @Resource
@@ -67,6 +83,12 @@
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private ArchiveDocumentApi archiveDocumentApi;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
@@ -81,21 +103,26 @@
        // 1. 报销单号:允许用户自定义,未填时由系统生成
        String no = resolveCreateNo(createReqVO.getNo());
        // 2.1 插入报销单
        // 2.1 申请人:支持代他人发起;未指定时默认当前登录用户(代录人由 creator 审计字段记录)
        Long applicantId = createReqVO.getApplicantId() != null
                ? createReqVO.getApplicantId() : getLoginUserId();
        validateApplicant(applicantId);
        // 2.2 插入报销单
        FinanceReimbursementDO reimbursement = BeanUtils.toBean(createReqVO, FinanceReimbursementDO.class, in -> in
                .setNo(no)
                .setStatus(FinanceReimbursementStatusEnum.DRAFT.getStatus())
                .setApplicantId(getLoginUserId())
                .setApplicantId(applicantId)
                .setApplyDate(createReqVO.getApplyDate() != null ? createReqVO.getApplyDate() : LocalDate.now()));
        reimbursementMapper.insert(reimbursement);
        // 2.2 插入报销明细
        // 2.3 插入报销明细
        List<FinanceReimbursementItemDO> items = buildItems(reimbursement.getId(), createReqVO.getItems());
        if (CollUtil.isNotEmpty(items)) {
            reimbursementItemMapper.insertBatch(items);
        }
        // 2.3 绑定发票附件
        // 2.4 绑定发票附件
        storageAttachmentApi.bindAttachments("file", REIMBURSEMENT_RECORD_TYPE,
                reimbursement.getId(), createReqVO.getBlobIds());
@@ -116,6 +143,11 @@
        // 2.1 更新报销单
        FinanceReimbursementDO updateObj = BeanUtils.toBean(updateReqVO, FinanceReimbursementDO.class);
        // 申请人:代他人发起时允许修改实际申请人;不传则保持原值
        if (updateReqVO.getApplicantId() != null) {
            validateApplicant(updateReqVO.getApplicantId());
            updateObj.setApplicantId(updateReqVO.getApplicantId());
        }
        // 报销单号:未填表示不变更,填了则校验后覆盖
        String customNo = StrUtil.trimToNull(updateReqVO.getNo());
        if (customNo == null || customNo.equals(reimbursement.getNo())) {
@@ -189,10 +221,12 @@
            throw exception(REIMBURSEMENT_SUBMIT_FAIL_ITEM_EMPTY);
        }
        // 2. 创建 BPM 流程实例
        // 2. 创建 BPM 流程实例,把实际申请人与代录人写入流程变量,供审批流展示与路由使用
        Map<String, Object> variables = buildProcessVariables(reimbursement, userId);
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setVariables(variables)
                        .setBusinessKey(String.valueOf(id)));
        // 3. 更新状态为审批中
@@ -204,18 +238,44 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void archiveReimbursement(Long id) {
        // 1. 校验存在 + 已通过状态
    public String archiveReimbursement(Long id) {
        // 1. 校验存在 + 状态
        FinanceReimbursementDO reimbursement = validateReimbursementExists(id);
        if (!FinanceReimbursementStatusEnum.APPROVED.getStatus().equals(reimbursement.getStatus())) {
            throw exception(REIMBURSEMENT_ARCHIVE_FAIL_STATUS);
        boolean archived = FinanceReimbursementStatusEnum.ARCHIVED.getStatus().equals(reimbursement.getStatus());
        if (archived && reimbursement.getArchiveDocumentId() != null) {
            throw exception(REIMBURSEMENT_ALREADY_ARCHIVED, reimbursement.getNo(),
                    StrUtil.blankToDefault(archiveNo(id), "未知"));
        }
        if (!archived && !FinanceReimbursementStatusEnum.APPROVED.getStatus().equals(reimbursement.getStatus())) {
            throw exception(REIMBURSEMENT_ARCHIVE_FAIL_STATUS, reimbursement.getNo(),
                    FinanceReimbursementStatusEnum.nameOf(reimbursement.getStatus()));
        }
        // 2. 更新为已归档
        // 2. 更新为已归档(历史已归档但尚未生成档案条目的,跳过状态更新,仅补生成档案)
        if (!archived) {
            reimbursementMapper.updateById(new FinanceReimbursementDO()
                    .setId(id)
                    .setStatus(FinanceReimbursementStatusEnum.ARCHIVED.getStatus())
                    .setArchiveTime(LocalDateTime.now()));
        }
        // 3. 按来源单据生成档案条目(财务档案默认「保密」级),并回写档案 ID
        List<Long> blobIds = storageAttachmentApi.listAttachments(REIMBURSEMENT_RECORD_TYPE, id).stream()
                .map(StorageBlobRespDTO::getId).collect(Collectors.toList());
        Long archiveDocumentId = archiveDocumentApi.archiveBySource(new ArchiveDocumentCreateReqDTO()
                .setSourceType(ARCHIVE_SOURCE_TYPE_FINANCE)
                .setSourceId(id)
                .setSourceNo(reimbursement.getNo())
                .setSourceTitle(reimbursement.getTitle())
                .setCategoryCode(ARCHIVE_CATEGORY_CODE_FINANCE)
                .setSecretLevel(ARCHIVE_SECRET_LEVEL_SECRET)
                .setSummary("财务报销凭证归档")
                .setBlobIds(blobIds)).getCheckedData();
        reimbursementMapper.updateById(new FinanceReimbursementDO()
                .setId(id)
                .setStatus(FinanceReimbursementStatusEnum.ARCHIVED.getStatus())
                .setArchiveTime(LocalDateTime.now()));
                .setArchiveDocumentId(archiveDocumentId));
        return StrUtil.blankToDefault(archiveNo(id), "未知");
    }
    @Override
@@ -283,6 +343,13 @@
        return reimbursement;
    }
    /** 查询来源单据已生成的档案编号(未归档时返回 null) */
    private String archiveNo(Long reimbursementId) {
        ArchiveDocumentRespDTO archive = archiveDocumentApi
                .getBySource(ARCHIVE_SOURCE_TYPE_FINANCE, reimbursementId).getCheckedData();
        return archive != null ? archive.getNo() : null;
    }
    /**
     * 解析新增时的报销单号:去掉首尾空格后若为空,则由系统按 BX + 日期 + 6 位流水生成,
     * 否则使用用户自定义单号。
@@ -312,6 +379,35 @@
        }
    }
    /**
     * 校验实际申请人存在且为启用状态
     */
    private void validateApplicant(Long applicantId) {
        AdminUserRespDTO applicant = applicantId != null ? adminUserApi.getUser(applicantId) : null;
        if (applicant == null || !CommonStatusEnum.ENABLE.getStatus().equals(applicant.getStatus())) {
            throw exception(REIMBURSEMENT_APPLICANT_NOT_EXISTS, applicantId);
        }
    }
    /**
     * 构建报销审批流程变量:实际申请人 + 代录(发起)人
     *
     * <p>审批流中可通过 applicantName 展示实际申请人,通过 initiatorName 展示代录人,
     * 并通过 isProxy 判断是否为代他人发起
     */
    private Map<String, Object> buildProcessVariables(FinanceReimbursementDO reimbursement, Long submitUserId) {
        Long applicantId = reimbursement.getApplicantId();
        AdminUserRespDTO applicant = applicantId != null ? adminUserApi.getUser(applicantId) : null;
        AdminUserRespDTO submitter = submitUserId != null ? adminUserApi.getUser(submitUserId) : null;
        Map<String, Object> variables = new HashMap<>(8);
        variables.put("applicantId", applicantId);
        variables.put("applicantName", applicant != null ? applicant.getNickname() : "");
        variables.put("initiatorId", submitUserId);
        variables.put("initiatorName", submitter != null ? submitter.getNickname() : "");
        variables.put("isProxy", applicantId != null && !applicantId.equals(submitUserId));
        return variables;
    }
    private List<FinanceReimbursementItemDO> buildItems(Long reimbursementId,
                                                         List<FinanceReimbursementItemSaveReqVO> itemVOs) {
        if (CollUtil.isEmpty(itemVOs)) {