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.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;
|
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 cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
|
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";
|
|
/** 归档来源类型: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
|
private FinanceReimbursementItemMapper reimbursementItemMapper;
|
|
@Resource
|
private FinanceNoRedisDAO noRedisDAO;
|
|
@Resource
|
private StorageAttachmentApi storageAttachmentApi;
|
|
@Resource
|
private ArchiveDocumentApi archiveDocumentApi;
|
|
@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<FinanceReimbursementItemDO> 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<FinanceReimbursementItemDO> 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<FinanceReimbursementDO> getReimbursementPage(FinanceReimbursementPageReqVO pageReqVO) {
|
return reimbursementMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public List<FinanceReimbursementItemDO> 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<FinanceReimbursementItemDO> 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 String archiveReimbursement(Long id) {
|
// 1. 校验存在 + 状态
|
FinanceReimbursementDO reimbursement = validateReimbursementExists(id);
|
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. 更新为已归档(历史已归档但尚未生成档案条目的,跳过状态更新,仅补生成档案)
|
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)
|
.setArchiveDocumentId(archiveDocumentId));
|
return StrUtil.blankToDefault(archiveNo(id), "未知");
|
}
|
|
@Override
|
public List<Map<String, Object>> getReimbursementApproveProcessDefinitionList() {
|
// 1. 校验分类和流程定义是否存在
|
validateReimbursementApproveCategoryAndProcessDefinition();
|
|
// 2. 获取分类下的流程定义信息
|
List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
|
.getProcessDefinitionInfoListByCategory(REIMBURSEMENT_APPROVE_CATEGORY_CODE);
|
if (CollUtil.isEmpty(definitionInfoList)) {
|
return Collections.emptyList();
|
}
|
|
// 3. 遍历获取流程定义详情,过滤激活状态,保留最新版本
|
Map<String, ProcessDefinition> 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<Map<String, Object>> result = new ArrayList<>();
|
for (ProcessDefinition pd : latestVersionMap.values()) {
|
Map<String, Object> 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;
|
}
|
|
/** 查询来源单据已生成的档案编号(未归档时返回 null) */
|
private String archiveNo(Long reimbursementId) {
|
ArchiveDocumentRespDTO archive = archiveDocumentApi
|
.getBySource(ARCHIVE_SOURCE_TYPE_FINANCE, reimbursementId).getCheckedData();
|
return archive != null ? archive.getNo() : null;
|
}
|
|
/**
|
* 解析新增时的报销单号:去掉首尾空格后若为空,则由系统按 BX + 日期 + 6 位流水生成,
|
* 否则使用用户自定义单号。
|
* <p>
|
* 注意:只由空格(含全角空格)组成的输入等同于未填,不会作为单号落库。
|
*/
|
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<FinanceReimbursementItemDO> buildItems(Long reimbursementId,
|
List<FinanceReimbursementItemSaveReqVO> 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<BpmCategoryDO> 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;
|
}
|
|
}
|