package jnpf.dmsService; import jnpf.dmsEntity.permission.entity.DmsFilePermissionEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity; import jnpf.dmsEntity.permission.enums.DmsActorType; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsPrincipalType; import jnpf.dmsEntity.permission.enums.DmsResourceScope; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.permission.enums.DmsSourceType; import jnpf.dmsEntity.permission.form.PermissionGrantBatchForm; import jnpf.dmsEntity.permission.form.PermissionGrantItemForm; import jnpf.dmsEntity.permission.form.PermissionRevokeForm; import jnpf.dmsEntity.permission.vo.DmsPermissionGrantVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsIntegration.audit.DmsAuditRequestSignResolver; import jnpf.dmsMapper.DmsFilePermissionMapper; import jnpf.dmsMapper.DmsPermissionResourceMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsPermission.DmsPermissionProperties; import jnpf.dmsPermission.DmsPrincipalValidator; 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.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class DmsPermissionGrantService { private static final String FOLDER_CREATOR_CREATE_SOURCE_PREFIX = "folder-creator-create:"; private static final String FOLDER_CREATOR_CREATE_REASON = "目录创建者默认 CREATE 权限"; private static final String WORKFLOW_BORROW_READ_REASON = "文件借阅申请审批通过"; private static final String WORKFLOW_DOWNLOAD_REASON = "文件下载申请审批通过"; private static final String WORKFLOW_PRINT_REASON = "文件打印申请审批通过"; private final DmsCurrentIdentity currentIdentity; private final DmsPermissionProperties properties; private final DmsPrincipalValidator principalValidator; private final DmsPermissionResourceMapper resourceMapper; private final DmsFilePermissionMapper permissionMapper; private final DmsAuditAdapter auditAdapter; @Transactional public List grant(PermissionGrantBatchForm form) { currentIdentity.requirePermissionAdministrator(); if (form.getItems().size() > properties.getMaxGrantBatchSize()) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "授权批量大小不能超过 " + properties.getMaxGrantBatchSize()); } String tenantId = currentIdentity.tenantId(); String sourceType = form.getSourceType().name(); String sourceId = form.getSourceId().trim(); List items = normalizeAndValidate(form.getItems()); permissionMapper.lockSource(tenantId, sourceType, sourceId); List existing = permissionMapper.selectBySource(tenantId, sourceType, sourceId); if (!existing.isEmpty()) { verifyIdempotentRetry(existing, items); return existing.stream().map(this::toVO).collect(Collectors.toList()); } validateResources(tenantId, items); principalValidator.validate(items); List created = new ArrayList<>(); try { for (PermissionGrantItemForm item : items) { DmsFilePermissionEntity permission = toEntity(tenantId, sourceType, sourceId, item); created.add(permissionMapper.insertReturning(permission)); } } catch (DuplicateKeyException ex) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "授权来源或授权项与已有数据冲突"); } auditAdapter.permissionChange("PERMISSION_GRANT", "授予 DMS 资源权限", Collections.emptyList(), created, DmsAuditRequestSignResolver.resolve(form)); return created.stream().map(this::toVO).collect(Collectors.toList()); } @Transactional public DmsFilePermissionEntity ensureFolderCreatorCreatePermission(String tenantId, UUID folderId, String creatorUserId) { String sourceType = "SYSTEM"; String sourceId = FOLDER_CREATOR_CREATE_SOURCE_PREFIX + folderId; permissionMapper.lockSource(tenantId, sourceType, sourceId); List existing = permissionMapper.selectBySource(tenantId, sourceType, sourceId); if (!existing.isEmpty()) { if (existing.size() != 1 || !sameFolderCreatorCreatePermission( existing.get(0), tenantId, folderId, creatorUserId, sourceId)) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "目录创建者默认权限来源与已有授权不一致"); } return existing.get(0); } DmsFilePermissionEntity permission = new DmsFilePermissionEntity(); permission.setTenantId(tenantId); permission.setResourceType(DmsResourceType.FOLDER.name()); permission.setResourceId(folderId); permission.setResourceScope(DmsResourceScope.SELF.name()); permission.setPrincipalType(DmsPrincipalType.USER.name()); permission.setPrincipalId(creatorUserId); permission.setIncludeChildOrgs(false); permission.setActionCode(DmsPermissionAction.CREATE.name()); permission.setSourceType(sourceType); permission.setSourceId(sourceId); permission.setValidFrom(new Date()); permission.setGrantReason(FOLDER_CREATOR_CREATE_REASON); permission.setGrantedByType(DmsActorType.USER.name()); permission.setGrantedById(creatorUserId); try { permission = permissionMapper.insertReturning(permission); } catch (DuplicateKeyException ex) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "目录创建者默认权限与已有授权冲突"); } auditAdapter.permissionChange("PERMISSION_GRANT", "授予 DMS 目录创建者默认权限", Collections.emptyList(), Collections.singletonList(permission), Collections.emptyList()); return permission; } @Transactional public List ensureWorkflowDownloadPermissions( String tenantId, String sourceId, String applicantUserId, List fileVersionIds, String serviceId, List relatedSigns) { return ensureWorkflowFilePermissions(tenantId, sourceId, applicantUserId, fileVersionIds, serviceId, relatedSigns, DmsPermissionAction.DOWNLOAD, WORKFLOW_DOWNLOAD_REASON, "下载", null); } @Transactional public List ensureWorkflowDownloadPermissions( String tenantId, String sourceId, String applicantUserId, List fileVersionIds, Date validTo, String serviceId, List relatedSigns) { return ensureWorkflowFilePermissions(tenantId, sourceId, applicantUserId, fileVersionIds, serviceId, relatedSigns, DmsPermissionAction.DOWNLOAD, WORKFLOW_DOWNLOAD_REASON, "下载", validTo); } @Transactional public List ensureWorkflowPrintPermissions( String tenantId, String sourceId, String applicantUserId, List fileVersionIds, String serviceId, List relatedSigns) { return ensureWorkflowFilePermissions(tenantId, sourceId, applicantUserId, fileVersionIds, serviceId, relatedSigns, DmsPermissionAction.PRINT, WORKFLOW_PRINT_REASON, "打印", null); } @Transactional public List ensureWorkflowPrintPermissions( String tenantId, String sourceId, String applicantUserId, List fileVersionIds, Date validTo, String serviceId, List relatedSigns) { return ensureWorkflowFilePermissions(tenantId, sourceId, applicantUserId, fileVersionIds, serviceId, relatedSigns, DmsPermissionAction.PRINT, WORKFLOW_PRINT_REASON, "打印", validTo); } @Transactional public List ensureWorkflowBorrowReadPermissions( String tenantId, String sourceId, String borrowerUserId, List fileVersionIds, Date validTo, String serviceId, List relatedSigns) { return ensureWorkflowFilePermissions(tenantId, sourceId, borrowerUserId, fileVersionIds, serviceId, relatedSigns, DmsPermissionAction.READ, WORKFLOW_BORROW_READ_REASON, "借阅读取", validTo); } private List ensureWorkflowFilePermissions( String tenantId, String sourceId, String applicantUserId, List fileVersionIds, String serviceId, List relatedSigns, DmsPermissionAction action, String grantReason, String actionLabel, Date validTo) { String normalizedTenantId = requiredText(tenantId, "租户ID不能为空"); String normalizedSourceId = requiredText(sourceId, "工作流授权来源不能为空"); String normalizedApplicant = requiredText(applicantUserId, actionLabel + "申请人不能为空"); String normalizedServiceId = requiredText(serviceId, "授权服务ID不能为空"); if (normalizedSourceId.length() > 200) { invalid("工作流授权来源不能超过 200 个字符"); } if (fileVersionIds == null || fileVersionIds.isEmpty()) { invalid(actionLabel + "授权文件不能为空"); } List resourceIds = new ArrayList<>(new LinkedHashSet<>(fileVersionIds)); if (resourceIds.size() != fileVersionIds.size()) { invalid(actionLabel + "申请包含重复文件版本"); } if (resourceIds.size() > properties.getMaxGrantBatchSize()) { invalid("授权批量大小不能超过 " + properties.getMaxGrantBatchSize()); } List items = new ArrayList<>(); for (UUID fileVersionId : resourceIds) { if (fileVersionId == null) { invalid(actionLabel + "授权文件版本ID不能为空"); } PermissionGrantItemForm item = new PermissionGrantItemForm(); item.setResourceType(DmsResourceType.FILE_VERSION); item.setResourceId(fileVersionId); item.setResourceScope(DmsResourceScope.SELF); item.setPrincipalType(DmsPrincipalType.USER); item.setPrincipalId(normalizedApplicant); item.setActionCode(action); item.setValidTo(validTo); if (action == DmsPermissionAction.DOWNLOAD) { item.setMaxUseCount(1L); } item.setGrantReason(grantReason); items.add(item); } items = normalizeAndValidate(items); String sourceType = DmsSourceType.WORKFLOW.name(); permissionMapper.lockSource(normalizedTenantId, sourceType, normalizedSourceId); List existing = permissionMapper.selectBySource( normalizedTenantId, sourceType, normalizedSourceId); if (!existing.isEmpty()) { verifyIdempotentRetry(existing, items); boolean wrongActor = existing.stream().anyMatch(permission -> !DmsActorType.SERVICE.name().equals(permission.getGrantedByType()) || !normalizedServiceId.equals(permission.getGrantedById())); if (wrongActor) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "工作流" + actionLabel + "授权来源与已有授权执行方不一致"); } return existing; } validateResources(normalizedTenantId, items); principalValidator.validate(items); List created = new ArrayList<>(); try { for (PermissionGrantItemForm item : items) { DmsFilePermissionEntity permission = toEntity(normalizedTenantId, sourceType, normalizedSourceId, item, DmsActorType.SERVICE.name(), normalizedServiceId); created.add(permissionMapper.insertReturning(permission)); } } catch (DuplicateKeyException ex) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "工作流" + actionLabel + "授权来源或授权项与已有数据冲突"); } auditAdapter.permissionChange("PERMISSION_GRANT", "授予工作流申请人 DMS " + actionLabel + "权限", Collections.emptyList(), created, relatedSigns == null ? Collections.emptyList() : relatedSigns); return created; } @Transactional public List revoke(PermissionRevokeForm form) { currentIdentity.requirePermissionAdministrator(); String tenantId = currentIdentity.tenantId(); String actorId = currentIdentity.userId(); String reason = form.getReason().trim(); List ids = new ArrayList<>(new LinkedHashSet<>(form.getPermissionIds())); ids.sort(Comparator.comparing(UUID::toString)); List locked = new ArrayList<>(); List changedBefore = new ArrayList<>(); for (UUID id : ids) { DmsFilePermissionEntity permission = permissionMapper.selectByTenantAndIdForUpdate(tenantId, id); if (permission == null) { throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "权限不存在: " + id); } if (permission.getRevokedAt() != null && (!DmsActorType.USER.name().equals(permission.getRevokedByType()) || !actorId.equals(permission.getRevokedById()) || !reason.equals(permission.getRevokeReason()))) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "权限已被其他请求撤销: " + id); } locked.add(permission); } for (DmsFilePermissionEntity permission : locked) { if (permission.getRevokedAt() == null) { changedBefore.add(permission); int changed = permissionMapper.revoke(tenantId, permission.getId(), DmsActorType.USER.name(), actorId, reason); if (changed != 1) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "权限撤销发生并发冲突: " + permission.getId()); } } } List result = ids.stream() .map(id -> permissionMapper.selectByTenantAndId(tenantId, id)).collect(Collectors.toList()); if (!changedBefore.isEmpty()) { Set changedIds = changedBefore.stream().map(DmsFilePermissionEntity::getId) .collect(Collectors.toSet()); List changedAfter = result.stream() .filter(item -> changedIds.contains(item.getId())).collect(Collectors.toList()); auditAdapter.permissionChange("PERMISSION_REVOKE", "撤销 DMS 资源权限", changedBefore, changedAfter, DmsAuditRequestSignResolver.resolve(form)); } return result.stream().map(this::toVO).collect(Collectors.toList()); } private List normalizeAndValidate(List source) { List result = new ArrayList<>(); Set identities = new HashSet<>(); for (PermissionGrantItemForm original : source) { PermissionGrantItemForm item = copy(original); validateItem(item); String identity = identity(item); if (!identities.add(identity)) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "同一授权批次包含重复授权项"); } result.add(item); } result.sort(Comparator.comparing(this::identity)); return result; } private PermissionGrantItemForm copy(PermissionGrantItemForm value) { PermissionGrantItemForm result = new PermissionGrantItemForm(); result.setResourceType(value.getResourceType()); result.setResourceId(value.getResourceId()); result.setResourceScope(value.getResourceScope()); result.setPrincipalType(value.getPrincipalType()); result.setPrincipalId(value.getPrincipalId().trim()); result.setIncludeChildOrgs(value.isIncludeChildOrgs()); result.setActionCode(value.getActionCode()); result.setValidFrom(value.getValidFrom()); result.setValidTo(value.getValidTo()); result.setMaxUseCount(value.getMaxUseCount()); result.setGrantReason(trimToNull(value.getGrantReason())); return result; } private void validateItem(PermissionGrantItemForm item) { if (item.getResourceType() != DmsResourceType.FOLDER && item.getResourceScope() != DmsResourceScope.SELF) { invalid("Document 和 FileVersion 只允许 SELF 范围"); } if (item.getActionCode() == DmsPermissionAction.CREATE && item.getResourceType() != DmsResourceType.FOLDER) { invalid("CREATE 只允许授予 Folder"); } if (item.isIncludeChildOrgs() && item.getPrincipalType() != DmsPrincipalType.ORG) { invalid("includeChildOrgs 只适用于 ORG 主体"); } if (item.getMaxUseCount() != null && item.getPrincipalType() != DmsPrincipalType.USER) { invalid("按次权限只允许授予 USER 主体"); } Date effectiveFrom = item.getValidFrom() == null ? new Date() : item.getValidFrom(); if (item.getValidTo() != null && !item.getValidTo().after(effectiveFrom)) { invalid("validTo 必须晚于 validFrom"); } } private void validateResources(String tenantId, List items) { List ids = items.stream().map(PermissionGrantItemForm::getResourceId).distinct() .collect(Collectors.toList()); Map resources = resourceMapper.selectByTenantAndIds(tenantId, ids) .stream().collect(Collectors.toMap(DmsPermissionResourceEntity::getId, Function.identity())); for (PermissionGrantItemForm item : items) { DmsPermissionResourceEntity resource = resources.get(item.getResourceId()); if (resource == null || !item.getResourceType().name().equals(resource.getResourceType())) { throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "授权资源不存在或类型不一致: " + item.getResourceId()); } } } private void verifyIdempotentRetry(List existing, List requested) { if (existing.size() != requested.size() || existing.stream().anyMatch(row -> row.getRevokedAt() != null)) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "source_id 已存在,但授权集合数量不同或包含已撤销权限"); } Map byIdentity = existing.stream().collect(Collectors.toMap( this::identity, Function.identity(), (left, right) -> left, LinkedHashMap::new)); for (PermissionGrantItemForm item : requested) { DmsFilePermissionEntity row = byIdentity.get(identity(item)); if (row == null || !sameDefinition(row, item)) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "source_id 已存在,但完整授权集合与本次请求不一致"); } } } private boolean sameDefinition(DmsFilePermissionEntity row, PermissionGrantItemForm item) { return (item.getValidFrom() == null || sameInstant(row.getValidFrom(), item.getValidFrom())) && sameInstant(row.getValidTo(), item.getValidTo()) && Objects.equals(row.getMaxUseCount(), item.getMaxUseCount()) && Objects.equals(row.getGrantReason(), item.getGrantReason()); } private boolean sameFolderCreatorCreatePermission(DmsFilePermissionEntity row, String tenantId, UUID folderId, String creatorUserId, String sourceId) { return tenantId.equals(row.getTenantId()) && DmsResourceType.FOLDER.name().equals(row.getResourceType()) && folderId.equals(row.getResourceId()) && DmsResourceScope.SELF.name().equals(row.getResourceScope()) && DmsPrincipalType.USER.name().equals(row.getPrincipalType()) && creatorUserId.equals(row.getPrincipalId()) && Boolean.FALSE.equals(row.getIncludeChildOrgs()) && DmsPermissionAction.CREATE.name().equals(row.getActionCode()) && "SYSTEM".equals(row.getSourceType()) && sourceId.equals(row.getSourceId()) && row.getValidTo() == null && row.getMaxUseCount() == null && FOLDER_CREATOR_CREATE_REASON.equals(row.getGrantReason()) && DmsActorType.USER.name().equals(row.getGrantedByType()) && creatorUserId.equals(row.getGrantedById()); } private DmsFilePermissionEntity toEntity(String tenantId, String sourceType, String sourceId, PermissionGrantItemForm item) { return toEntity(tenantId, sourceType, sourceId, item, DmsActorType.USER.name(), currentIdentity.userId()); } private DmsFilePermissionEntity toEntity(String tenantId, String sourceType, String sourceId, PermissionGrantItemForm item, String actorType, String actorId) { DmsFilePermissionEntity entity = new DmsFilePermissionEntity(); entity.setTenantId(tenantId); entity.setResourceType(item.getResourceType().name()); entity.setResourceId(item.getResourceId()); entity.setResourceScope(item.getResourceScope().name()); entity.setPrincipalType(item.getPrincipalType().name()); entity.setPrincipalId(item.getPrincipalId()); entity.setIncludeChildOrgs(item.isIncludeChildOrgs()); entity.setActionCode(item.getActionCode().name()); entity.setSourceType(sourceType); entity.setSourceId(sourceId); entity.setValidFrom(item.getValidFrom() == null ? new Date() : item.getValidFrom()); entity.setValidTo(item.getValidTo()); entity.setMaxUseCount(item.getMaxUseCount()); entity.setGrantReason(item.getGrantReason()); entity.setGrantedByType(actorType); entity.setGrantedById(actorId); return entity; } private String identity(PermissionGrantItemForm item) { return item.getResourceType() + "|" + item.getResourceId() + "|" + item.getResourceScope() + "|" + item.getPrincipalType() + "|" + item.getPrincipalId() + "|" + item.isIncludeChildOrgs() + "|" + item.getActionCode(); } private String identity(DmsFilePermissionEntity item) { return item.getResourceType() + "|" + item.getResourceId() + "|" + item.getResourceScope() + "|" + item.getPrincipalType() + "|" + item.getPrincipalId() + "|" + item.getIncludeChildOrgs() + "|" + item.getActionCode(); } private boolean sameInstant(Date left, Date right) { return left == null ? right == null : right != null && left.toInstant().equals(right.toInstant()); } private String trimToNull(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private String requiredText(String value, String message) { String normalized = trimToNull(value); if (normalized == null) { invalid(message); } return normalized; } private void invalid(String message) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, message); } public DmsPermissionGrantVO toVO(DmsFilePermissionEntity value) { DmsPermissionGrantVO result = new DmsPermissionGrantVO(); result.setId(value.getId()); result.setResourceType(value.getResourceType()); result.setResourceId(value.getResourceId()); result.setResourceScope(value.getResourceScope()); result.setPrincipalType(value.getPrincipalType()); result.setPrincipalId(value.getPrincipalId()); result.setIncludeChildOrgs(value.getIncludeChildOrgs()); result.setActionCode(value.getActionCode()); result.setSourceType(value.getSourceType()); result.setSourceId(value.getSourceId()); result.setValidFrom(value.getValidFrom()); result.setValidTo(value.getValidTo()); result.setMaxUseCount(value.getMaxUseCount()); result.setUsedCount(value.getUsedCount()); result.setGrantReason(value.getGrantReason()); result.setRevokedAt(value.getRevokedAt()); result.setRevokeReason(value.getRevokeReason()); result.setRevision(value.getRevision()); result.setCreatedAt(value.getCreatedAt()); result.setUpdatedAt(value.getUpdatedAt()); return result; } }