package cn.iocoder.yudao.module.archive.service.borrow; 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.controller.admin.borrow.vo.ArchiveBorrowPageReqVO; import cn.iocoder.yudao.module.archive.controller.admin.borrow.vo.ArchiveBorrowReturnReqVO; import cn.iocoder.yudao.module.archive.controller.admin.borrow.vo.ArchiveBorrowSaveReqVO; import cn.iocoder.yudao.module.archive.dal.dataobject.borrow.ArchiveBorrowDO; import cn.iocoder.yudao.module.archive.dal.dataobject.document.ArchiveDocumentDO; import cn.iocoder.yudao.module.archive.dal.mysql.borrow.ArchiveBorrowMapper; import cn.iocoder.yudao.module.archive.dal.redis.ArchiveNoRedisDAO; import cn.iocoder.yudao.module.archive.enums.ArchiveBorrowStatusEnum; import cn.iocoder.yudao.module.archive.enums.ArchiveDocumentStatusEnum; import cn.iocoder.yudao.module.archive.service.document.ArchiveDocumentService; 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.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.module.archive.enums.ErrorCodeConstants.*; /** * 档案借阅单 Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class ArchiveBorrowServiceImpl implements ArchiveBorrowService { /** 档案借阅审批的 BPM 流程分类编码 */ public static final String BORROW_APPROVE_CATEGORY_CODE = "archive_borrow_approve"; @Resource private ArchiveBorrowMapper borrowMapper; @Resource private ArchiveDocumentService documentService; @Resource private ArchiveNoRedisDAO archiveNoRedisDAO; @Resource private BpmProcessInstanceApi processInstanceApi; @Resource private BpmCategoryService bpmCategoryService; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Override @Transactional(rollbackFor = Exception.class) public Long createBorrow(ArchiveBorrowSaveReqVO createReqVO) { // 1. 校验借阅人、归还期限、档案可借 validateBorrower(createReqVO.getBorrowerName()); validateReturnDeadline(createReqVO); ArchiveDocumentDO document = validateDocumentBorrowable(createReqVO.getDocumentId(), null); // 2. 组装借阅单(档案编号/标题取快照,避免档案改名后历史单据失真) ArchiveBorrowDO borrow = BeanUtils.toBean(createReqVO, ArchiveBorrowDO.class); borrow.setId(null); borrow.setNo(archiveNoRedisDAO.generate(ArchiveNoRedisDAO.BORROW_NO_PREFIX)); borrow.setDocumentNo(document.getNo()); borrow.setDocumentTitle(document.getTitle()); borrow.setStatus(ArchiveBorrowStatusEnum.DRAFT.getStatus()); borrowMapper.insert(borrow); return borrow.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateBorrow(ArchiveBorrowSaveReqVO updateReqVO) { // 1.1 校验借阅单存在且状态允许修改 ArchiveBorrowDO oldBorrow = validateBorrowExists(updateReqVO.getId()); if (!ArchiveBorrowStatusEnum.DRAFT.getStatus().equals(oldBorrow.getStatus()) && !ArchiveBorrowStatusEnum.REJECTED.getStatus().equals(oldBorrow.getStatus())) { throw exception(BORROW_UPDATE_FAIL_STATUS, oldBorrow.getNo(), ArchiveBorrowStatusEnum.nameOf(oldBorrow.getStatus())); } // 1.2 校验借阅人、归还期限、档案可借(排除自身) validateBorrower(updateReqVO.getBorrowerName()); validateReturnDeadline(updateReqVO); ArchiveDocumentDO document = validateDocumentBorrowable(updateReqVO.getDocumentId(), updateReqVO.getId()); // 2. 更新(档案编号/标题刷新为最新快照) ArchiveBorrowDO updateObj = BeanUtils.toBean(updateReqVO, ArchiveBorrowDO.class); updateObj.setDocumentNo(document.getNo()); updateObj.setDocumentTitle(document.getTitle()); borrowMapper.updateById(updateObj); } @Override public void deleteBorrow(Long id) { // 1. 校验借阅单存在且为草稿 ArchiveBorrowDO borrow = validateBorrowExists(id); if (!ArchiveBorrowStatusEnum.DRAFT.getStatus().equals(borrow.getStatus())) { throw exception(BORROW_DELETE_FAIL_STATUS, borrow.getNo(), ArchiveBorrowStatusEnum.nameOf(borrow.getStatus())); } borrowMapper.deleteById(id); } @Override public ArchiveBorrowDO getBorrow(Long id) { return borrowMapper.selectById(id); } @Override public PageResult getBorrowPage(ArchiveBorrowPageReqVO reqVO) { return borrowMapper.selectPage(reqVO); } @Override public PageResult getOverdueBorrowPage(ArchiveBorrowPageReqVO reqVO) { return borrowMapper.selectOverduePage(reqVO); } @Override @Transactional(rollbackFor = Exception.class) public void submitBorrow(Long id, String processDefinitionKey) { // 1.1 校验借阅单存在且状态允许提交 ArchiveBorrowDO borrow = validateBorrowExists(id); if (!ArchiveBorrowStatusEnum.DRAFT.getStatus().equals(borrow.getStatus()) && !ArchiveBorrowStatusEnum.REJECTED.getStatus().equals(borrow.getStatus())) { throw exception(BORROW_SUBMIT_FAIL_STATUS, borrow.getNo(), ArchiveBorrowStatusEnum.nameOf(borrow.getStatus())); } // 1.2 校验档案仍可借(提交前二次校验,避免草稿期间被他人借走) validateDocumentBorrowable(borrow.getDocumentId(), borrow.getId()); // 1.3 先解析可用流程:没有已发布流程时直接失败,避免留下无法提交的孤儿草稿 String definitionKey = resolveProcessDefinitionKey(processDefinitionKey); // 2. 发起流程并回写状态 String processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(), new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(definitionKey) .setBusinessKey(String.valueOf(id))); borrowMapper.updateById(new ArchiveBorrowDO() .setId(id) .setStatus(ArchiveBorrowStatusEnum.APPROVING.getStatus()) .setProcessInstanceId(processInstanceId) // 重新提交时清空上一次的驳回信息 .setRejectReason("") .setRejectTime(null)); } @Override public List> getApproveProcessDefinitionList() { validateApproveCategory(); List definitionInfoList = bpmProcessDefinitionService.getProcessDefinitionInfoListByCategory(BORROW_APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { return Collections.emptyList(); } 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); } } 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 returnBorrow(ArchiveBorrowReturnReqVO returnReqVO) { // 1. 校验借阅单存在且为借出中 ArchiveBorrowDO borrow = validateBorrowExists(returnReqVO.getId()); if (!ArchiveBorrowStatusEnum.BORROWED.getStatus().equals(borrow.getStatus())) { throw exception(BORROW_RETURN_FAIL_STATUS, borrow.getNo(), ArchiveBorrowStatusEnum.nameOf(borrow.getStatus())); } // 2. 条件更新借阅单为已归还,避免并发重复归还 int rows = borrowMapper.update(null, new LambdaUpdateWrapper() .eq(ArchiveBorrowDO::getId, borrow.getId()) .eq(ArchiveBorrowDO::getStatus, ArchiveBorrowStatusEnum.BORROWED.getStatus()) .set(ArchiveBorrowDO::getStatus, ArchiveBorrowStatusEnum.RETURNED.getStatus()) .set(ArchiveBorrowDO::getReturnTime, LocalDateTime.now()) .set(ArchiveBorrowDO::getReturnConfirmUserId, getLoginUserId()) .set(ArchiveBorrowDO::getReturnRemark, returnReqVO.getReturnRemark())); if (rows == 0) { throw exception(BORROW_RETURN_FAIL_STATUS, borrow.getNo(), ArchiveBorrowStatusEnum.nameOf(borrow.getStatus())); } // 3. 档案状态回到「在库」 if (!documentService.updateDocumentStatusToInStock(borrow.getDocumentId())) { log.warn("[returnBorrow] 借阅单({}) 归还成功,但档案({}) 状态未从「借出中」更新为「在库」,请人工核对", borrow.getNo(), borrow.getDocumentNo()); } } @Override @Transactional(rollbackFor = Exception.class) public void updateBorrowAuditStatus(Long id, Integer bpmResult, String reason) { ArchiveBorrowDO borrow = borrowMapper.selectById(id); if (borrow == null) { log.warn("[updateBorrowAuditStatus] 借阅单({}) 不存在,忽略 BPM 结果回写", id); return; } if (!ArchiveBorrowStatusEnum.APPROVING.getStatus().equals(borrow.getStatus())) { log.warn("[updateBorrowAuditStatus] 借阅单({}) 不处于审批中状态,忽略 BPM 结果回写", borrow.getNo()); return; } // 审批通过:档案条件更新为「借出中」;若档案已被他人借出,条件更新失败则整体回滚,避免出现「借阅单已借出但档案未锁定」的脏状态 if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) { if (!documentService.updateDocumentStatusToBorrowed(borrow.getDocumentId())) { throw exception(BORROW_DOCUMENT_LOCKED, borrow.getDocumentNo()); } borrowMapper.updateById(new ArchiveBorrowDO() .setId(id) .setStatus(ArchiveBorrowStatusEnum.BORROWED.getStatus()) .setApproveTime(LocalDateTime.now())); return; } // 驳回 if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) { borrowMapper.updateById(new ArchiveBorrowDO() .setId(id) .setStatus(ArchiveBorrowStatusEnum.REJECTED.getStatus()) .setRejectReason(reason) .setRejectTime(LocalDateTime.now())); return; } // 撤回 / 取消:回到草稿,清空流程实例 borrowMapper.updateById(new ArchiveBorrowDO() .setId(id) .setStatus(ArchiveBorrowStatusEnum.DRAFT.getStatus()) .setProcessInstanceId("") .setRejectReason("") .setRejectTime(null)); } // ==================== 校验逻辑 ==================== private ArchiveBorrowDO validateBorrowExists(Long id) { ArchiveBorrowDO borrow = id != null ? borrowMapper.selectById(id) : null; if (borrow == null) { throw exception(BORROW_NOT_EXISTS); } return borrow; } private void validateBorrower(String borrowerName) { if (StrUtil.isBlank(borrowerName)) { throw exception(BORROW_BORROWER_REQUIRED); } } private void validateReturnDeadline(ArchiveBorrowSaveReqVO reqVO) { if (reqVO.getReturnDeadline() != null && reqVO.getBorrowDate() != null && reqVO.getReturnDeadline().isBefore(reqVO.getBorrowDate())) { throw exception(BORROW_DEADLINE_BEFORE_DATE, reqVO.getReturnDeadline(), reqVO.getBorrowDate()); } } /** * 校验档案可借:档案存在、处于在库、且没有未归还的借阅单 * * @param excludeBorrowId 需要排除的借阅单编号(更新 / 重新提交时传自身) */ private ArchiveDocumentDO validateDocumentBorrowable(Long documentId, Long excludeBorrowId) { ArchiveDocumentDO document = documentId != null ? documentService.getDocumentWithoutPermissionCheck(documentId) : null; if (document == null) { throw exception(BORROW_DOCUMENT_NOT_EXISTS); } if (!ArchiveDocumentStatusEnum.IN_STOCK.getStatus().equals(document.getStatus())) { throw exception(BORROW_DOCUMENT_NOT_IN_STOCK, document.getNo(), ArchiveDocumentStatusEnum.nameOf(document.getStatus())); } List openBorrows = borrowMapper.selectListByDocumentIdAndStatusIn(documentId, List.of(ArchiveBorrowStatusEnum.APPROVING.getStatus(), ArchiveBorrowStatusEnum.BORROWED.getStatus())); for (ArchiveBorrowDO openBorrow : openBorrows) { if (excludeBorrowId != null && excludeBorrowId.equals(openBorrow.getId())) { continue; } throw exception(BORROW_DOCUMENT_ALREADY_BORROWED, document.getNo(), openBorrow.getNo()); } return document; } private void validateApproveCategory() { List categories = bpmCategoryService.getCategoryListByCode( Collections.singletonList(BORROW_APPROVE_CATEGORY_CODE)); if (CollUtil.isEmpty(categories)) { throw exception(BORROW_BPM_CATEGORY_NOT_EXISTS); } } /** 解析可用的流程定义 key:优先使用前端传入且确实已发布的最新流程,否则取分类下最新已发布流程 */ private String resolveProcessDefinitionKey(String processDefinitionKey) { List> definitionList = getApproveProcessDefinitionList(); if (CollUtil.isEmpty(definitionList)) { throw exception(BORROW_BPM_PROCESS_DEFINITION_NOT_EXISTS); } if (StrUtil.isNotBlank(processDefinitionKey)) { for (Map item : definitionList) { if (processDefinitionKey.equals(String.valueOf(item.get("key")))) { return processDefinitionKey; } } } return String.valueOf(definitionList.get(0).get("key")); } }