package cn.iocoder.yudao.module.archive.service.document; 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.framework.security.core.service.SecurityFrameworkService; import cn.iocoder.yudao.module.archive.api.document.dto.ArchiveDocumentCreateReqDTO; import cn.iocoder.yudao.module.archive.controller.admin.document.vo.ArchiveDocumentPageReqVO; import cn.iocoder.yudao.module.archive.controller.admin.document.vo.ArchiveDocumentSaveReqVO; import cn.iocoder.yudao.module.archive.dal.dataobject.category.ArchiveCategoryDO; 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.mysql.document.ArchiveDocumentMapper; import cn.iocoder.yudao.module.archive.dal.redis.ArchiveNoRedisDAO; import cn.iocoder.yudao.module.archive.enums.ArchiveBorrowStatusEnum; import cn.iocoder.yudao.module.archive.enums.ArchiveCategoryStatusEnum; import cn.iocoder.yudao.module.archive.enums.ArchiveDocumentStatusEnum; import cn.iocoder.yudao.module.archive.enums.ArchiveSecretLevelEnum; import cn.iocoder.yudao.module.archive.enums.ArchiveSourceTypeEnum; import cn.iocoder.yudao.module.archive.service.category.ArchiveCategoryService; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDate; import java.util.List; 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 public class ArchiveDocumentServiceImpl implements ArchiveDocumentService { /** 敏感档案(保密级)查看权限点 */ public static final String SENSITIVE_PERMISSION = "archive:document:sensitive"; @Resource private ArchiveDocumentMapper documentMapper; @Resource private ArchiveBorrowMapper borrowMapper; @Resource private ArchiveCategoryService categoryService; @Resource private ArchiveNoRedisDAO archiveNoRedisDAO; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource(name = "ss") private SecurityFrameworkService securityFrameworkService; @Override @Transactional(rollbackFor = Exception.class) public Long createDocument(ArchiveDocumentSaveReqVO createReqVO) { // 1. 校验分类 ArchiveCategoryDO category = validateCategory(createReqVO.getCategoryId()); // 2. 组装档案(密级/载体/归档日期/归档人补默认值) ArchiveDocumentDO document = BeanUtils.toBean(createReqVO, ArchiveDocumentDO.class); document.setId(null); document.setNo(archiveNoRedisDAO.generate(ArchiveNoRedisDAO.DOCUMENT_NO_PREFIX)); document.setSecretLevel(createReqVO.getSecretLevel() != null ? createReqVO.getSecretLevel() : category.getDefaultSecretLevel()); document.setCarrierType(createReqVO.getCarrierType() != null ? createReqVO.getCarrierType() : 3); document.setSourceType(ArchiveSourceTypeEnum.MANUAL.getType()); document.setArchiveDate(createReqVO.getArchiveDate() != null ? createReqVO.getArchiveDate() : LocalDate.now()); document.setArchivistUserId(getLoginUserId()); document.setStatus(ArchiveDocumentStatusEnum.IN_STOCK.getStatus()); documentMapper.insert(document); // 3. 绑定附件 storageAttachmentApi.bindAttachments("file", DOCUMENT_RECORD_TYPE, document.getId(), createReqVO.getBlobIds()); return document.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateDocument(ArchiveDocumentSaveReqVO updateReqVO) { // 1.1 校验档案存在 ArchiveDocumentDO oldDocument = validateDocumentExists(updateReqVO.getId()); // 1.2 借出中的档案不允许修改 if (ArchiveDocumentStatusEnum.BORROWED.getStatus().equals(oldDocument.getStatus())) { throw exception(DOCUMENT_UPDATE_FAIL_BORROWED, oldDocument.getNo()); } // 1.3 敏感档案需要有敏感档案权限才能修改 validateSecretLevelVisible(oldDocument); // 1.4 校验分类 validateCategory(updateReqVO.getCategoryId()); // 2. 更新(密级为空时保留原值,避免被置空) ArchiveDocumentDO updateObj = BeanUtils.toBean(updateReqVO, ArchiveDocumentDO.class); if (updateObj.getSecretLevel() == null) { updateObj.setSecretLevel(oldDocument.getSecretLevel()); } documentMapper.updateById(updateObj); // 3. 增量更新附件 storageAttachmentApi.updateAttachments("file", DOCUMENT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteDocument(Long id) { // 1.1 校验档案存在 ArchiveDocumentDO document = validateDocumentExists(id); // 1.2 借出中或存在未归还借阅单时不允许删除 boolean hasOpenBorrow = CollUtil.isNotEmpty(borrowMapper.selectListByDocumentIdAndStatusIn(id, List.of(ArchiveBorrowStatusEnum.APPROVING.getStatus(), ArchiveBorrowStatusEnum.BORROWED.getStatus()))); if (ArchiveDocumentStatusEnum.BORROWED.getStatus().equals(document.getStatus()) || hasOpenBorrow) { throw exception(DOCUMENT_DELETE_FAIL_STATUS, document.getNo(), ArchiveDocumentStatusEnum.nameOf(document.getStatus())); } // 2. 删除档案与附件 documentMapper.deleteById(id); storageAttachmentApi.deleteAttachmentsByRecord(DOCUMENT_RECORD_TYPE, id); } @Override public ArchiveDocumentDO getDocument(Long id) { ArchiveDocumentDO document = validateDocumentExists(id); validateSecretLevelVisible(document); return document; } @Override public ArchiveDocumentDO getDocumentWithoutPermissionCheck(Long id) { return documentMapper.selectById(id); } @Override public PageResult getDocumentPage(ArchiveDocumentPageReqVO reqVO) { return documentMapper.selectPage(reqVO, !hasSensitivePermission()); } @Override public List getDocumentList(ArchiveDocumentPageReqVO reqVO) { return documentMapper.selectList(reqVO, !hasSensitivePermission()); } @Override @Transactional(rollbackFor = Exception.class) public Long archiveBySource(ArchiveDocumentCreateReqDTO reqDTO) { // 1. 校验来源类型(只允许课题/会员/财务) ArchiveSourceTypeEnum sourceType = ArchiveSourceTypeEnum.valueOf(reqDTO.getSourceType()); if (sourceType == null || sourceType == ArchiveSourceTypeEnum.MANUAL) { throw exception(DOCUMENT_SOURCE_TYPE_UNSUPPORTED, reqDTO.getSourceType()); } // 2. 重复归档校验:同一来源单据只允许生成一份档案 ArchiveDocumentDO exists = documentMapper.selectBySource(reqDTO.getSourceType(), reqDTO.getSourceId()); if (exists != null) { throw exception(DOCUMENT_DUPLICATE_SOURCE, StrUtil.blankToDefault(reqDTO.getSourceNo(), String.valueOf(reqDTO.getSourceId())), exists.getNo()); } // 3. 按分类编码定位档案分类 ArchiveCategoryDO category = categoryService.getCategoryByCode(reqDTO.getCategoryCode()); if (category == null) { throw exception(DOCUMENT_SOURCE_CATEGORY_NOT_EXISTS, reqDTO.getCategoryCode()); } // 4. 组装并落库档案(标题、密级为空时分别取来源标题、分类默认密级) ArchiveDocumentDO document = new ArchiveDocumentDO(); document.setNo(archiveNoRedisDAO.generate(ArchiveNoRedisDAO.DOCUMENT_NO_PREFIX)); document.setTitle(StrUtil.blankToDefault(reqDTO.getTitle(), reqDTO.getSourceTitle())); document.setCategoryId(category.getId()); document.setSecretLevel(reqDTO.getSecretLevel() != null ? reqDTO.getSecretLevel() : category.getDefaultSecretLevel()); // 联动归档默认同时保留电子件与来源关联,载体取「电子+实体」,由档案管理员后续补充实体位置 document.setCarrierType(3); document.setSourceType(reqDTO.getSourceType()); document.setSourceNo(reqDTO.getSourceNo()); document.setSourceId(reqDTO.getSourceId()); document.setSourceTitle(reqDTO.getSourceTitle()); document.setArchiveDate(reqDTO.getArchiveDate() != null ? reqDTO.getArchiveDate() : LocalDate.now()); document.setArchivistUserId(getLoginUserId()); document.setStatus(ArchiveDocumentStatusEnum.IN_STOCK.getStatus()); document.setSummary(reqDTO.getSummary()); document.setTags(reqDTO.getTags()); document.setRemark(reqDTO.getRemark()); documentMapper.insert(document); // 5. 绑定来源单据的附件 storageAttachmentApi.bindAttachments("file", DOCUMENT_RECORD_TYPE, document.getId(), reqDTO.getBlobIds()); return document.getId(); } @Override public Boolean isArchived(Integer sourceType, Long sourceId) { return documentMapper.selectBySource(sourceType, sourceId) != null; } @Override public ArchiveDocumentDO getBySource(Integer sourceType, Long sourceId) { if (sourceType == null || sourceId == null) { return null; } return documentMapper.selectBySource(sourceType, sourceId); } @Override public boolean updateDocumentStatusToBorrowed(Long documentId) { int rows = documentMapper.update(null, new LambdaUpdateWrapper() .eq(ArchiveDocumentDO::getId, documentId) .eq(ArchiveDocumentDO::getStatus, ArchiveDocumentStatusEnum.IN_STOCK.getStatus()) .set(ArchiveDocumentDO::getStatus, ArchiveDocumentStatusEnum.BORROWED.getStatus())); return rows > 0; } @Override public boolean updateDocumentStatusToInStock(Long documentId) { int rows = documentMapper.update(null, new LambdaUpdateWrapper() .eq(ArchiveDocumentDO::getId, documentId) .eq(ArchiveDocumentDO::getStatus, ArchiveDocumentStatusEnum.BORROWED.getStatus()) .set(ArchiveDocumentDO::getStatus, ArchiveDocumentStatusEnum.IN_STOCK.getStatus())); return rows > 0; } // ==================== 校验逻辑 ==================== private ArchiveDocumentDO validateDocumentExists(Long id) { ArchiveDocumentDO document = id != null ? documentMapper.selectById(id) : null; if (document == null) { throw exception(DOCUMENT_NOT_EXISTS); } return document; } /** 校验分类存在且启用 */ private ArchiveCategoryDO validateCategory(Long categoryId) { ArchiveCategoryDO category = categoryService.getCategory(categoryId); if (category == null) { throw exception(DOCUMENT_CATEGORY_NOT_EXISTS); } if (ArchiveCategoryStatusEnum.DISABLE.getStatus().equals(category.getStatus())) { throw exception(CATEGORY_DISABLED, category.getName()); } return category; } /** 敏感档案可见性校验:保密级档案需要 {@code archive:document:sensitive} 权限 */ private void validateSecretLevelVisible(ArchiveDocumentDO document) { if (ArchiveSecretLevelEnum.SECRET.getLevel().equals(document.getSecretLevel()) && !hasSensitivePermission()) { throw exception(DOCUMENT_SENSITIVE_NO_PERMISSION, document.getTitle()); } } private boolean hasSensitivePermission() { return securityFrameworkService.hasPermission(SENSITIVE_PERMISSION); } }