package cn.iocoder.yudao.module.finance.service.reimbursement; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.finance.controller.admin.reimbursement.vo.FinanceReimbursementPageReqVO; import cn.iocoder.yudao.module.finance.controller.admin.reimbursement.vo.FinanceReimbursementSaveReqVO; import cn.iocoder.yudao.module.finance.controller.admin.reimbursement.vo.item.FinanceReimbursementItemSaveReqVO; import cn.iocoder.yudao.module.finance.dal.dataobject.reimbursement.FinanceReimbursementDO; import cn.iocoder.yudao.module.finance.dal.dataobject.reimbursement.FinanceReimbursementItemDO; import cn.iocoder.yudao.module.finance.dal.mysql.reimbursement.FinanceReimbursementItemMapper; import cn.iocoder.yudao.module.finance.dal.mysql.reimbursement.FinanceReimbursementMapper; 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 jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.*; /** * 报销单 Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class FinanceReimbursementServiceImpl implements FinanceReimbursementService { /** * BPM 报销审批分类编码 */ private static final String REIMBURSEMENT_APPROVE_CATEGORY_CODE = "finance_reimbursement_approve"; /** * 附件关联的业务类型 */ public static final String REIMBURSEMENT_RECORD_TYPE = "finance_reimbursement"; @Resource private FinanceReimbursementMapper reimbursementMapper; @Resource private FinanceReimbursementItemMapper reimbursementItemMapper; @Resource private FinanceNoRedisDAO noRedisDAO; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource private BpmProcessInstanceApi processInstanceApi; @Resource private BpmCategoryService bpmCategoryService; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Override @Transactional(rollbackFor = Exception.class) public Long createReimbursement(FinanceReimbursementSaveReqVO createReqVO) { // 1. 报销单号:允许用户自定义,未填时由系统生成 String no = resolveCreateNo(createReqVO.getNo()); // 2.1 插入报销单 FinanceReimbursementDO reimbursement = BeanUtils.toBean(createReqVO, FinanceReimbursementDO.class, in -> in .setNo(no) .setStatus(FinanceReimbursementStatusEnum.DRAFT.getStatus()) .setApplicantId(getLoginUserId()) .setApplyDate(createReqVO.getApplyDate() != null ? createReqVO.getApplyDate() : LocalDate.now())); reimbursementMapper.insert(reimbursement); // 2.2 插入报销明细 List items = buildItems(reimbursement.getId(), createReqVO.getItems()); if (CollUtil.isNotEmpty(items)) { reimbursementItemMapper.insertBatch(items); } // 2.3 绑定发票附件 storageAttachmentApi.bindAttachments("file", REIMBURSEMENT_RECORD_TYPE, reimbursement.getId(), createReqVO.getBlobIds()); return reimbursement.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateReimbursement(FinanceReimbursementSaveReqVO updateReqVO) { // 1.1 校验存在 FinanceReimbursementDO reimbursement = validateReimbursementExists(updateReqVO.getId()); // 1.2 只有草稿、已驳回状态才能修改 Integer status = reimbursement.getStatus(); if (!FinanceReimbursementStatusEnum.DRAFT.getStatus().equals(status) && !FinanceReimbursementStatusEnum.REJECTED.getStatus().equals(status)) { throw exception(REIMBURSEMENT_UPDATE_FAIL_NOT_DRAFT, reimbursement.getNo()); } // 2.1 更新报销单 FinanceReimbursementDO updateObj = BeanUtils.toBean(updateReqVO, FinanceReimbursementDO.class); // 报销单号:未填表示不变更,填了则校验后覆盖 String customNo = StrUtil.trimToNull(updateReqVO.getNo()); if (customNo == null || customNo.equals(reimbursement.getNo())) { updateObj.setNo(reimbursement.getNo()); } else { validateCustomNoFormat(customNo); if (reimbursementMapper.selectByNo(customNo) != null) { throw exception(REIMBURSEMENT_CUSTOM_NO_EXISTS, customNo); } updateObj.setNo(customNo); } reimbursementMapper.updateById(updateObj); // 2.2 更新明细:先删后增 reimbursementItemMapper.deleteByReimbursementId(updateReqVO.getId()); List items = buildItems(updateReqVO.getId(), updateReqVO.getItems()); if (CollUtil.isNotEmpty(items)) { reimbursementItemMapper.insertBatch(items); } // 2.3 更新发票附件 storageAttachmentApi.updateAttachments("file", REIMBURSEMENT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteReimbursement(Long id) { // 1. 校验存在 + 草稿状态 FinanceReimbursementDO reimbursement = validateReimbursementExists(id); if (!FinanceReimbursementStatusEnum.DRAFT.getStatus().equals(reimbursement.getStatus())) { throw exception(REIMBURSEMENT_DELETE_FAIL_NOT_DRAFT, reimbursement.getNo()); } // 2. 删除明细 reimbursementItemMapper.deleteByReimbursementId(id); // 3. 删除主表 reimbursementMapper.deleteById(id); // 4. 删除发票附件 storageAttachmentApi.deleteAttachmentsByRecord(REIMBURSEMENT_RECORD_TYPE, id); } @Override public FinanceReimbursementDO getReimbursement(Long id) { return reimbursementMapper.selectById(id); } @Override public PageResult getReimbursementPage(FinanceReimbursementPageReqVO pageReqVO) { return reimbursementMapper.selectPage(pageReqVO); } @Override public List getReimbursementItemListByReimbursementId(Long reimbursementId) { return reimbursementItemMapper.selectListByReimbursementId(reimbursementId); } @Override @Transactional(rollbackFor = Exception.class) public void submitReimbursement(Long id, String processDefinitionKey, Long userId) { // 1. 校验存在 + 可提交状态(草稿或已驳回) FinanceReimbursementDO reimbursement = validateReimbursementExists(id); Integer status = reimbursement.getStatus(); if (!FinanceReimbursementStatusEnum.DRAFT.getStatus().equals(status) && !FinanceReimbursementStatusEnum.REJECTED.getStatus().equals(status)) { throw exception(REIMBURSEMENT_SUBMIT_FAIL_STATUS); } // 1.2 校验必须有明细 List items = reimbursementItemMapper.selectListByReimbursementId(id); if (CollUtil.isEmpty(items)) { throw exception(REIMBURSEMENT_SUBMIT_FAIL_ITEM_EMPTY); } // 2. 创建 BPM 流程实例 String processInstanceId = processInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(processDefinitionKey) .setBusinessKey(String.valueOf(id))); // 3. 更新状态为审批中 reimbursementMapper.updateById(new FinanceReimbursementDO() .setId(id) .setStatus(FinanceReimbursementStatusEnum.APPROVING.getStatus()) .setProcessInstanceId(processInstanceId)); } @Override @Transactional(rollbackFor = Exception.class) public void archiveReimbursement(Long id) { // 1. 校验存在 + 已通过状态 FinanceReimbursementDO reimbursement = validateReimbursementExists(id); if (!FinanceReimbursementStatusEnum.APPROVED.getStatus().equals(reimbursement.getStatus())) { throw exception(REIMBURSEMENT_ARCHIVE_FAIL_STATUS); } // 2. 更新为已归档 reimbursementMapper.updateById(new FinanceReimbursementDO() .setId(id) .setStatus(FinanceReimbursementStatusEnum.ARCHIVED.getStatus()) .setArchiveTime(LocalDateTime.now())); } @Override public List> getReimbursementApproveProcessDefinitionList() { // 1. 校验分类和流程定义是否存在 validateReimbursementApproveCategoryAndProcessDefinition(); // 2. 获取分类下的流程定义信息 List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(REIMBURSEMENT_APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { return Collections.emptyList(); } // 3. 遍历获取流程定义详情,过滤激活状态,保留最新版本 Map latestVersionMap = new HashMap<>(); for (BpmProcessDefinitionInfoDO info : definitionInfoList) { ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId()); if (pd == null || pd.isSuspended()) { continue; } ProcessDefinition existing = latestVersionMap.get(pd.getKey()); if (existing == null || pd.getVersion() > existing.getVersion()) { latestVersionMap.put(pd.getKey(), pd); } } // 4. 返回流程定义列表 List> result = new ArrayList<>(); for (ProcessDefinition pd : latestVersionMap.values()) { Map item = new HashMap<>(); item.put("id", pd.getId()); item.put("key", pd.getKey()); item.put("name", pd.getName()); result.add(item); } return result; } @Override @Transactional(rollbackFor = Exception.class) public void updateReimbursementAuditStatus(Long id, Integer bpmResult) { // 1. 校验存在 FinanceReimbursementDO reimbursement = validateReimbursementExists(id); if (!FinanceReimbursementStatusEnum.APPROVING.getStatus().equals(reimbursement.getStatus())) { log.warn("[updateReimbursementAuditStatus] 报销单({}) 不处于审批中状态", id); return; } // 2. 根据审批结果更新状态 Integer newStatus = convertBpmResultToStatus(bpmResult); if (newStatus != null) { reimbursementMapper.updateById(new FinanceReimbursementDO() .setId(id).setStatus(newStatus)); } } // ==================== 内部方法 ==================== private FinanceReimbursementDO validateReimbursementExists(Long id) { FinanceReimbursementDO reimbursement = reimbursementMapper.selectById(id); if (reimbursement == null) { throw exception(REIMBURSEMENT_NOT_EXISTS); } return reimbursement; } /** * 解析新增时的报销单号:去掉首尾空格后若为空,则由系统按 BX + 日期 + 6 位流水生成, * 否则使用用户自定义单号。 *

* 注意:只由空格(含全角空格)组成的输入等同于未填,不会作为单号落库。 */ private String resolveCreateNo(String customNo) { String no = StrUtil.trimToNull(customNo); if (no == null) { no = noRedisDAO.generate(FinanceNoRedisDAO.REIMBURSEMENT_NO_PREFIX); if (reimbursementMapper.selectByNo(no) != null) { throw exception(REIMBURSEMENT_NO_EXISTS); } return no; } validateCustomNoFormat(no); if (reimbursementMapper.selectByNo(no) != null) { throw exception(REIMBURSEMENT_CUSTOM_NO_EXISTS, no); } return no; } /** 自定义单号中间不允许出现空格(首尾空格已在解析时去掉) */ private void validateCustomNoFormat(String no) { if (StrUtil.containsAny(no, ' ', '\t', ' ')) { throw exception(REIMBURSEMENT_CUSTOM_NO_INVALID, no); } } private List buildItems(Long reimbursementId, List itemVOs) { if (CollUtil.isEmpty(itemVOs)) { return Collections.emptyList(); } return itemVOs.stream() .map(item -> BeanUtils.toBean(item, FinanceReimbursementItemDO.class, in -> in.setReimbursementId(reimbursementId))) .collect(Collectors.toList()); } private void validateReimbursementApproveCategoryAndProcessDefinition() { List categories = bpmCategoryService.getCategoryListByCode( Collections.singletonList(REIMBURSEMENT_APPROVE_CATEGORY_CODE)); if (CollUtil.isEmpty(categories)) { throw exception(REIMBURSEMENT_BPM_CATEGORY_NOT_EXISTS); } } /** * 转换 BPM 审批结果为报销单状态 */ private Integer convertBpmResultToStatus(Integer bpmResult) { if (cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) { return FinanceReimbursementStatusEnum.APPROVED.getStatus(); } else if (cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) { return FinanceReimbursementStatusEnum.REJECTED.getStatus(); } else if (cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) { return FinanceReimbursementStatusEnum.DRAFT.getStatus(); } return null; } }