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.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.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 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, "授权来源或授权项与已有数据冲突"); } for (DmsFilePermissionEntity permission : created) { auditAdapter.dataChange("CREATE", "创建 DMS 文件权限", "dms_file_permissions", permission.getId().toString(), permission.getGrantReason(), null); } 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.dataChange("CREATE", "创建 DMS 目录创建者默认权限", "dms_file_permissions", permission.getId().toString(), FOLDER_CREATOR_CREATE_REASON, null); return permission; } @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<>(); 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) { int changed = permissionMapper.revoke(tenantId, permission.getId(), DmsActorType.USER.name(), actorId, reason); if (changed != 1) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "权限撤销发生并发冲突: " + permission.getId()); } auditAdapter.dataChange("UPDATE", "撤销 DMS 文件权限", "dms_file_permissions", permission.getId().toString(), reason, null); } } return ids.stream().map(id -> toVO(permissionMapper.selectByTenantAndId(tenantId, id))) .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) { 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(DmsActorType.USER.name()); entity.setGrantedById(currentIdentity.userId()); 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 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; } }