package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsFolderEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity; import jnpf.dmsEntity.permission.enums.DmsFolderReorderDirection; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.permission.form.FolderCopyForm; import jnpf.dmsEntity.permission.form.FolderAuditSignForm; import jnpf.dmsEntity.permission.form.FolderMoveForm; import jnpf.dmsEntity.permission.form.FolderRenameForm; import jnpf.dmsEntity.permission.form.FolderReorderForm; import jnpf.dmsEntity.permission.vo.DmsPermissionResourceVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsIntegration.audit.DmsAuditRequestSignResolver; import jnpf.dmsIntegration.audit.DmsFolderAuditEvent; import jnpf.dmsIntegration.audit.DmsFolderAuditSnapshot; import jnpf.dmsMapper.DmsFolderMapper; import jnpf.dmsMapper.DmsPermissionResourceMapper; import jnpf.dmsMapper.DmsPermissionRevisionMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; @Service @RequiredArgsConstructor public class DmsFolderManagementService { private static final String COPY_SOURCE_PREFIX = "web-folder-copy:"; private static final String WEB_FOLDER_REGISTRAR = "jnpf-dms-web"; private final DmsCurrentIdentity currentIdentity; private final DmsFolderMapper folderMapper; private final DmsPermissionResourceMapper resourceMapper; private final DmsPermissionRevisionMapper permissionRevisionMapper; private final DmsPermissionGrantService permissionGrantService; private final DmsAuditAdapter auditAdapter; @Transactional public DmsPermissionResourceVO rename(UUID id, FolderRenameForm form) { Actor actor = actor(); DmsFolderEntity snapshot = requireFolder(actor.tenantId, id); lockSiblingSets(actor.tenantId, snapshot.getParentId()); DmsFolderEntity folder = requireLockedFolder(actor.tenantId, id); requireRevision(folder, form.getRevision()); String name = form.getName().trim(); if (name.equals(folder.getName())) { return requireView(actor.tenantId, id); } if (folderMapper.siblingNameExists(actor.tenantId, folder.getParentId(), name, id)) { throw conflict("同级目录名称已存在", null); } String oldPath = folderMapper.selectPath(actor.tenantId, id); if (folderMapper.rename(actor.tenantId, id, name, folder.getRevision(), actor.userId) != 1) { throw revisionConflict(); } auditAdapter.folderChange(folderEvent(id, "UPDATE", "重命名 DMS 文件夹", folderSnapshot(folder, oldPath), folderSnapshot(actor.tenantId, id, folder.getParentId(), name, folder.getRevision() + 1, folderMapper.selectPath(actor.tenantId, id)), form.getRevision(), form)); return requireView(actor.tenantId, id); } @Transactional public DmsPermissionResourceVO reorder(UUID id, FolderReorderForm form) { Actor actor = actor(); DmsFolderEntity snapshot = requireFolder(actor.tenantId, id); lockSiblingSets(actor.tenantId, snapshot.getParentId()); DmsFolderEntity folder = requireLockedFolder(actor.tenantId, id); requireRevision(folder, form.getRevision()); DmsFolderEntity adjacent = form.getDirection() == DmsFolderReorderDirection.UP ? folderMapper.selectPreviousForUpdate(actor.tenantId, folder.getParentId(), folder.getSortCode(), folder.getId()) : folderMapper.selectNextForUpdate(actor.tenantId, folder.getParentId(), folder.getSortCode(), folder.getId()); if (adjacent == null) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, form.getDirection() == DmsFolderReorderDirection.UP ? "目录已经位于同级第一项" : "目录已经位于同级最后一项"); } long folderSort = folder.getSortCode(); long adjacentSort = adjacent.getSortCode(); if (folderMapper.reorder(actor.tenantId, folder.getId(), adjacentSort, folder.getRevision(), actor.userId) != 1 || folderMapper.reorder(actor.tenantId, adjacent.getId(), folderSort, adjacent.getRevision(), actor.userId) != 1) { throw revisionConflict(); } auditAdapter.dataChange("UPDATE", "调整 DMS 同级目录排序", "dms_folders", id.toString(), null, null); return requireView(actor.tenantId, id); } @Transactional public DmsPermissionResourceVO move(UUID id, FolderMoveForm form) { Actor actor = actor(); DmsFolderEntity snapshot = requireFolder(actor.tenantId, id); if (snapshot.getParentId() == null) { throw DmsPermissionException.of(DmsPermissionError.INVALID_RESOURCE_PARENT, "根目录不能剪切移动"); } requireFolder(actor.tenantId, form.getParentId()); if (Objects.equals(snapshot.getParentId(), form.getParentId())) { requireRevision(snapshot, form.getRevision()); return requireView(actor.tenantId, id); } if (folderMapper.targetIsSelfOrDescendant(actor.tenantId, id, form.getParentId())) { throw DmsPermissionException.of(DmsPermissionError.INVALID_RESOURCE_PARENT, "目录不能移动到自身或其后代目录"); } lockSiblingSets(actor.tenantId, snapshot.getParentId(), form.getParentId()); DmsFolderEntity folder = requireLockedFolder(actor.tenantId, id); requireLockedFolder(actor.tenantId, form.getParentId()); requireRevision(folder, form.getRevision()); if (folderMapper.siblingNameExists(actor.tenantId, form.getParentId(), folder.getName(), null)) { throw conflict("目标目录下已存在同名目录", null); } long sortCode = folderMapper.nextSortCode(actor.tenantId, form.getParentId()); String oldPath = folderMapper.selectPath(actor.tenantId, id); if (folderMapper.move(actor.tenantId, id, form.getParentId(), sortCode, folder.getRevision(), actor.userId) != 1) { throw revisionConflict(); } permissionRevisionMapper.bump(actor.tenantId); auditAdapter.folderChange(folderEvent(id, "UPDATE", "移动 DMS 文件夹", folderSnapshot(folder, oldPath), folderSnapshot(actor.tenantId, id, form.getParentId(), folder.getName(), folder.getRevision() + 1, folderMapper.selectPath(actor.tenantId, id)), form.getRevision(), form)); return requireView(actor.tenantId, id); } @Transactional public DmsPermissionResourceVO copy(UUID id, FolderCopyForm form) { Actor actor = actor(); DmsFolderEntity source = requireFolder(actor.tenantId, id); requireFolder(actor.tenantId, form.getParentId()); if (folderMapper.targetIsSelfOrDescendant(actor.tenantId, id, form.getParentId())) { throw DmsPermissionException.of(DmsPermissionError.INVALID_RESOURCE_PARENT, "目录不能复制到自身或其后代目录"); } String requestId = form.getRequestId().trim(); String rootSourceId = copySourceId(requestId, id); resourceMapper.lockSource(actor.tenantId, "SYSTEM", rootSourceId); DmsPermissionResourceEntity existing = resourceMapper.selectBySource( actor.tenantId, "SYSTEM", rootSourceId); if (existing != null) { DmsFolderEntity existingFolder = folderMapper.selectByTenantAndId(actor.tenantId, existing.getId()); if (existingFolder == null || !Objects.equals(existingFolder.getParentId(), form.getParentId()) || !actor.userId.equals(existingFolder.getCreatedBy())) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "复制请求 ID 已用于其他目标或用户"); } return requireView(actor.tenantId, existing.getId()); } List subtree = folderMapper.selectSubtree(actor.tenantId, id); if (subtree.isEmpty()) { throw notFound(id); } lockSiblingSets(actor.tenantId, form.getParentId()); String rootName = uniqueCopyName(actor.tenantId, form.getParentId(), source.getName()); long rootSortCode = folderMapper.nextSortCode(actor.tenantId, form.getParentId()); Map copiedIds = new HashMap<>(); UUID copiedRootId = null; try { for (DmsFolderEntity original : subtree) { UUID copiedId = resourceMapper.nextUuidV7(); UUID copiedParentId = original.getId().equals(id) ? form.getParentId() : copiedIds.get(original.getParentId()); if (copiedParentId == null) { throw DmsPermissionException.of(DmsPermissionError.INVALID_RESOURCE_PARENT, "复制目录结构不完整"); } DmsFolderEntity copy = new DmsFolderEntity(); copy.setId(copiedId); copy.setTenantId(actor.tenantId); copy.setParentId(copiedParentId); copy.setName(original.getId().equals(id) ? rootName : original.getName()); copy.setCreatedBy(actor.userId); copy.setSortCode(original.getId().equals(id) ? rootSortCode : original.getSortCode()); copy.setRevision(0L); copy.setDeleteMark(null); folderMapper.insert(copy); insertRegistry(actor.tenantId, copiedId, copySourceId(requestId, original.getId())); permissionGrantService.ensureFolderCreatorCreatePermission( actor.tenantId, copiedId, actor.userId); copiedIds.put(original.getId(), copiedId); if (original.getId().equals(id)) { copiedRootId = copiedId; } } } catch (DuplicateKeyException ex) { throw conflict("复制目录时发生名称或资源冲突", ex); } auditAdapter.dataChange("CREATE", "复制 DMS 目录结构", "dms_folders", Objects.requireNonNull(copiedRootId).toString(), null, "{\"folderCount\":" + subtree.size() + "}"); return requireView(actor.tenantId, copiedRootId); } @Transactional public void delete(UUID id, long revision) { Actor actor = actor(); DmsFolderEntity snapshot = requireFolder(actor.tenantId, id); if (snapshot.getParentId() == null) { throw DmsPermissionException.of(DmsPermissionError.INVALID_RESOURCE_PARENT, "根目录不能删除"); } lockSiblingSets(actor.tenantId, snapshot.getParentId()); DmsFolderEntity folder = requireLockedFolder(actor.tenantId, id); requireRevision(folder, revision); if (folderMapper.hasContents(actor.tenantId, id)) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "只能删除不包含子目录和文件的空目录"); } String oldPath = folderMapper.selectPath(actor.tenantId, id); if (folderMapper.softDelete(actor.tenantId, id, folder.getRevision(), actor.userId) != 1) { throw revisionConflict(); } permissionRevisionMapper.bump(actor.tenantId); auditAdapter.folderChange(folderEvent(id, "DELETE", "删除 DMS 文件夹", folderSnapshot(folder, oldPath), null, revision, null)); } private String uniqueCopyName(String tenantId, UUID parentId, String sourceName) { String base = sourceName.length() <= 294 ? sourceName : sourceName.substring(0, 294); String candidate = base + " - 副本"; int suffix = 2; while (folderMapper.siblingNameExists(tenantId, parentId, candidate, null)) { String ending = " - 副本 (" + suffix++ + ")"; int baseLength = Math.min(sourceName.length(), 300 - ending.length()); candidate = sourceName.substring(0, baseLength) + ending; if (suffix > 1000) { throw conflict("无法为复制目录生成唯一名称", null); } } return candidate; } private void insertRegistry(String tenantId, UUID id, String sourceId) { DmsPermissionResourceEntity registry = new DmsPermissionResourceEntity(); registry.setId(id); registry.setTenantId(tenantId); registry.setResourceType(DmsResourceType.FOLDER.name()); registry.setSourceType("SYSTEM"); registry.setSourceId(sourceId); registry.setRegisteredBy(WEB_FOLDER_REGISTRAR); resourceMapper.insert(registry); } private String copySourceId(String requestId, UUID sourceId) { return COPY_SOURCE_PREFIX + requestId + ":" + sourceId; } private Actor actor() { currentIdentity.requirePermissionAdministrator(); UserInfo user = currentIdentity.requireUser(); return new Actor(currentIdentity.tenantId(user), user.getUserId().trim()); } private DmsFolderEntity requireFolder(String tenantId, UUID id) { DmsFolderEntity folder = folderMapper.selectByTenantAndId(tenantId, id); if (folder == null) { throw notFound(id); } return folder; } private DmsFolderEntity requireLockedFolder(String tenantId, UUID id) { DmsFolderEntity folder = folderMapper.selectByTenantAndIdForUpdate(tenantId, id); if (folder == null) { throw notFound(id); } return folder; } private void requireRevision(DmsFolderEntity folder, Long expectedRevision) { if (expectedRevision == null || !expectedRevision.equals(folder.getRevision())) { throw revisionConflict(); } } private DmsPermissionResourceVO requireView(String tenantId, UUID id) { DmsPermissionResourceVO view = resourceMapper.selectView(tenantId, id); if (view == null) { throw notFound(id); } return view; } private void lockSiblingSets(String tenantId, UUID... parentIds) { List ids = new ArrayList<>(); for (UUID parentId : parentIds) { if (!ids.contains(parentId)) { ids.add(parentId); } } ids.sort(Comparator.comparing(id -> id == null ? "" : id.toString())); for (UUID parentId : ids) { folderMapper.lockSiblingSet(tenantId, parentId); } } private DmsPermissionException revisionConflict() { return DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "目录已被其他管理员修改,请刷新后重试"); } private DmsPermissionException notFound(UUID id) { return DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "目录不存在或已删除: " + id); } private DmsPermissionException conflict(String message, Throwable cause) { DmsPermissionException exception = DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, message); if (cause != null) { exception.initCause(cause); } return exception; } private DmsFolderAuditEvent folderEvent(UUID id, String actionCode, String actionLabel, DmsFolderAuditSnapshot before, DmsFolderAuditSnapshot after, Long requestRevision, FolderAuditSignForm signForm) { return DmsFolderAuditEvent.builder() .folderId(id) .actionCode(actionCode) .actionLabel(actionLabel) .before(before) .after(after) .requestRevision(requestRevision) .relatedSigns(DmsAuditRequestSignResolver.resolve( signForm == null ? null : signForm.getAuditSignParameters())) .entryType("MENU") .entryId("dms-folder-management") .entryName("DMS 文件夹管理") .build(); } private DmsFolderAuditSnapshot folderSnapshot(DmsFolderEntity folder, String path) { return folderSnapshot(folder.getTenantId(), folder.getId(), folder.getParentId(), folder.getName(), folder.getRevision(), path); } private DmsFolderAuditSnapshot folderSnapshot(String tenantId, UUID id, UUID parentId, String name, Long revision, String path) { return DmsFolderAuditSnapshot.builder() .id(id) .parentId(parentId) .name(name) .parentName(parentName(tenantId, parentId)) .revision(revision) .path(path) .build(); } private String parentName(String tenantId, UUID parentId) { if (parentId == null) { return "根目录"; } DmsFolderEntity parent = folderMapper.selectByTenantAndId(tenantId, parentId); return parent == null ? null : parent.getName(); } private static class Actor { private final String tenantId; private final String userId; private Actor(String tenantId, String userId) { this.tenantId = tenantId; this.userId = userId; } } }