package jnpf.dmsService; import jnpf.dmsEntity.permission.entity.DmsAccessExclusionEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity; import jnpf.dmsEntity.permission.enums.DmsActorType; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.permission.form.AccessExclusionBatchForm; import jnpf.dmsEntity.permission.form.AccessExclusionItemForm; import jnpf.dmsEntity.permission.form.AccessExclusionRevokeForm; import jnpf.dmsEntity.permission.vo.DmsAccessExclusionVO; import jnpf.dmsEntity.permission.vo.DmsCursorPageVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsMapper.DmsAccessExclusionMapper; import jnpf.dmsMapper.DmsPermissionResourceMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsCursorCodec; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsPermission.DmsPermissionProperties; import jnpf.dmsPermission.DmsPrincipalValidator; import jnpf.dmsPermission.DmsUserDisplayNameResolver; 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.Date; 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 DmsAccessExclusionService { private final DmsCurrentIdentity currentIdentity; private final DmsPermissionProperties properties; private final DmsPrincipalValidator principalValidator; private final DmsPermissionResourceMapper resourceMapper; private final DmsAccessExclusionMapper exclusionMapper; private final DmsCursorCodec cursorCodec; private final DmsAuditAdapter auditAdapter; private final DmsUserDisplayNameResolver userDisplayNameResolver; @Transactional public List create(AccessExclusionBatchForm form) { currentIdentity.requirePermissionAdministrator(); validateBatchForm(form); String tenantId = currentIdentity.tenantId(); String actorId = currentIdentity.userId(); String sourceType = form.getSourceType().name(); String sourceId = form.getSourceId().trim(); List items = normalizeAndValidate(form.getItems()); exclusionMapper.lockSource(tenantId, sourceType, sourceId); List existing = exclusionMapper.selectBySource( tenantId, sourceType, sourceId); if (!existing.isEmpty()) { verifyIdempotentRetry(existing, items); return existing.stream().map(this::toVO).collect(Collectors.toList()); } validateResources(tenantId, items); principalValidator.validateUsers(items.stream().map(AccessExclusionItemForm::getUserId) .collect(Collectors.toCollection(LinkedHashSet::new))); List entities = items.stream() .map(item -> toEntity(tenantId, actorId, sourceType, sourceId, item)) .collect(Collectors.toList()); try { if (exclusionMapper.insertBatch(entities) != entities.size()) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "排除集合未完整写入"); } } catch (DuplicateKeyException ex) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "排除来源或排除项与已有数据冲突"); } List created = exclusionMapper.selectBySource( tenantId, sourceType, sourceId); if (created.size() != entities.size()) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "排除集合写入后不完整"); } for (DmsAccessExclusionEntity exclusion : created) { auditAdapter.dataChange("CREATE", "创建 DMS 用户读取排除", "dms_access_exclusions", exclusion.getId().toString(), exclusion.getExcludeReason(), null); } return created.stream().map(this::toVO).collect(Collectors.toList()); } @Transactional(readOnly = true) public DmsCursorPageVO page(UUID resourceId, String userId, String cursor, Integer pageSize) { currentIdentity.requirePermissionAdministrator(); String normalizedUserId = trimToNull(userId); if (normalizedUserId != null && normalizedUserId.length() > 100) { invalid("userId 长度不能超过 100"); } int limit = pageSize(pageSize); DmsCursorCodec.Cursor decoded = cursorCodec.decode(cursor); List rows = exclusionMapper.selectPage(currentIdentity.tenantId(), resourceId, normalizedUserId, decoded.getTime(), decoded.getId(), limit + 1); boolean hasMore = rows.size() > limit; if (hasMore) { rows = rows.subList(0, limit); } DmsCursorPageVO result = new DmsCursorPageVO<>(); List values = rows.stream().map(this::toVO).collect(Collectors.toList()); Map userNames = userDisplayNameResolver.resolve(values.stream() .map(DmsAccessExclusionVO::getUserId).collect(Collectors.toList())); values.forEach(value -> value.setUserName(userNames.get(value.getUserId()))); result.setList(values); result.setHasMore(hasMore); if (hasMore && !rows.isEmpty()) { DmsAccessExclusionEntity last = rows.get(rows.size() - 1); result.setNextCursor(cursorCodec.encode(last.getCreatedAt(), last.getId())); } return result; } @Transactional public List revoke(AccessExclusionRevokeForm form) { currentIdentity.requirePermissionAdministrator(); if (form == null || form.getExclusionIds() == null || form.getExclusionIds().isEmpty() || form.getExclusionIds().size() > properties.getMaxExclusionBatchSize()) { invalid("排除撤销批量大小必须在 1 到 " + properties.getMaxExclusionBatchSize() + " 之间"); } String reason = trimToNull(form.getReason()); if (reason == null || reason.length() > 1000) { invalid("撤销原因不能为空且长度不能超过 1000"); } if (form.getExclusionIds().stream().anyMatch(Objects::isNull)) { invalid("排除 ID 不能为空"); } String tenantId = currentIdentity.tenantId(); String actorId = currentIdentity.userId(); List ids = new ArrayList<>(new LinkedHashSet<>(form.getExclusionIds())); ids.sort(Comparator.comparing(UUID::toString)); List locked = exclusionMapper.selectByTenantAndIdsForUpdate(tenantId, ids); if (locked.size() != ids.size()) { throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "一个或多个读取排除不存在"); } if (locked.stream().anyMatch(row -> row.getRevokedAt() != null)) { throw DmsPermissionException.of(DmsPermissionError.EXCLUSION_ALREADY_REVOKED, "读取排除只允许撤销一次"); } int changed = exclusionMapper.revokeBatch(tenantId, ids, DmsActorType.USER.name(), actorId, reason); if (changed != ids.size()) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "读取排除撤销发生并发冲突"); } List revoked = exclusionMapper.selectByTenantAndIds(tenantId, ids); for (DmsAccessExclusionEntity exclusion : revoked) { auditAdapter.dataChange("UPDATE", "撤销 DMS 用户读取排除", "dms_access_exclusions", exclusion.getId().toString(), reason, null); } return revoked.stream().map(this::toVO).collect(Collectors.toList()); } private void validateBatchForm(AccessExclusionBatchForm form) { if (form == null || form.getSourceType() == null || form.getSourceId() == null || form.getSourceId().trim().isEmpty() || form.getSourceId().trim().length() > 200) { invalid("排除来源类型和 sourceId 不能为空,sourceId 长度不能超过 200"); } if (form.getItems() == null || form.getItems().isEmpty() || form.getItems().size() > properties.getMaxExclusionBatchSize()) { invalid("排除批量大小必须在 1 到 " + properties.getMaxExclusionBatchSize() + " 之间"); } } private List normalizeAndValidate(List source) { List result = new ArrayList<>(); Set identities = new HashSet<>(); for (AccessExclusionItemForm original : source) { if (original == null || original.getResourceType() == null || original.getResourceId() == null) { invalid("排除资源类型和资源 ID 不能为空"); } if (original.getResourceType() != DmsResourceType.DOCUMENT && original.getResourceType() != DmsResourceType.FILE_VERSION) { invalid("读取排除只支持 Document 和 FileVersion"); } String userId = trimToNull(original.getUserId()); if (userId == null || userId.length() > 100) { invalid("排除用户不能为空且长度不能超过 100"); } String reason = trimToNull(original.getExcludeReason()); if (reason != null && reason.length() > 1000) { invalid("排除原因长度不能超过 1000"); } Date effectiveFrom = original.getValidFrom() == null ? new Date() : original.getValidFrom(); if (original.getValidTo() != null && !original.getValidTo().after(effectiveFrom)) { invalid("validTo 必须晚于 validFrom"); } AccessExclusionItemForm item = new AccessExclusionItemForm(); item.setResourceType(original.getResourceType()); item.setResourceId(original.getResourceId()); item.setUserId(userId); item.setValidFrom(original.getValidFrom()); item.setValidTo(original.getValidTo()); item.setExcludeReason(reason); if (!identities.add(identity(item))) { invalid("同一排除批次包含重复排除项"); } result.add(item); } result.sort(Comparator.comparing(this::identity)); return result; } private void validateResources(String tenantId, List items) { List ids = items.stream().map(AccessExclusionItemForm::getResourceId).distinct() .collect(Collectors.toList()); Map resources = resourceMapper.selectByTenantAndIds(tenantId, ids) .stream().collect(Collectors.toMap(DmsPermissionResourceEntity::getId, Function.identity())); for (AccessExclusionItemForm 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 (AccessExclusionItemForm item : requested) { DmsAccessExclusionEntity row = byIdentity.get(identity(item)); if (row == null || !sameDefinition(row, item)) { throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, "source_id 已存在,但完整排除集合与本次请求不一致"); } } } private boolean sameDefinition(DmsAccessExclusionEntity row, AccessExclusionItemForm item) { return (item.getValidFrom() == null || sameInstant(row.getValidFrom(), item.getValidFrom())) && sameInstant(row.getValidTo(), item.getValidTo()) && Objects.equals(row.getExcludeReason(), item.getExcludeReason()); } private DmsAccessExclusionEntity toEntity(String tenantId, String actorId, String sourceType, String sourceId, AccessExclusionItemForm item) { DmsAccessExclusionEntity entity = new DmsAccessExclusionEntity(); entity.setTenantId(tenantId); entity.setResourceType(item.getResourceType().name()); entity.setResourceId(item.getResourceId()); entity.setUserId(item.getUserId()); entity.setActionCode(DmsPermissionAction.READ.name()); entity.setSourceType(sourceType); entity.setSourceId(sourceId); entity.setValidFrom(item.getValidFrom() == null ? new Date() : item.getValidFrom()); entity.setValidTo(item.getValidTo()); entity.setExcludeReason(item.getExcludeReason()); entity.setExcludedByType(DmsActorType.USER.name()); entity.setExcludedById(actorId); return entity; } private String identity(AccessExclusionItemForm item) { return item.getResourceType() + "|" + item.getResourceId() + "|" + item.getUserId() + "|READ"; } private String identity(DmsAccessExclusionEntity item) { return item.getResourceType() + "|" + item.getResourceId() + "|" + item.getUserId() + "|" + item.getActionCode(); } private boolean sameInstant(Date left, Date right) { return left == null ? right == null : right != null && left.toInstant().equals(right.toInstant()); } private int pageSize(Integer requested) { int value = requested == null ? properties.getDefaultPageSize() : requested; if (value < 1 || value > properties.getMaxPageSize()) { invalid("pageSize 必须在 1 到 " + properties.getMaxPageSize() + " 之间"); } return value; } private DmsAccessExclusionVO toVO(DmsAccessExclusionEntity value) { DmsAccessExclusionVO result = new DmsAccessExclusionVO(); result.setId(value.getId()); result.setResourceType(value.getResourceType()); result.setResourceId(value.getResourceId()); result.setUserId(value.getUserId()); result.setActionCode(value.getActionCode()); result.setSourceType(value.getSourceType()); result.setSourceId(value.getSourceId()); result.setValidFrom(value.getValidFrom()); result.setValidTo(value.getValidTo()); result.setExcludeReason(value.getExcludeReason()); result.setExcludedByType(value.getExcludedByType()); result.setExcludedById(value.getExcludedById()); result.setRevokedAt(value.getRevokedAt()); result.setRevokedByType(value.getRevokedByType()); result.setRevokedById(value.getRevokedById()); result.setRevokeReason(value.getRevokeReason()); result.setRevision(value.getRevision()); result.setCreatedAt(value.getCreatedAt()); result.setUpdatedAt(value.getUpdatedAt()); result.setStatus(status(value)); return result; } private String status(DmsAccessExclusionEntity value) { Date now = new Date(); if (value.getRevokedAt() != null) { return "REVOKED"; } if (value.getValidFrom().after(now)) { return "PENDING"; } if (value.getValidTo() != null && !now.before(value.getValidTo())) { return "EXPIRED"; } return "ACTIVE"; } 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); } }