From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master

---
 jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsAccessExclusionService.java |  345 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 345 insertions(+), 0 deletions(-)

diff --git a/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsAccessExclusionService.java b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsAccessExclusionService.java
new file mode 100644
index 0000000..a83ab04
--- /dev/null
+++ b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsAccessExclusionService.java
@@ -0,0 +1,345 @@
+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<DmsAccessExclusionVO> 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<AccessExclusionItemForm> items = normalizeAndValidate(form.getItems());
+
+        exclusionMapper.lockSource(tenantId, sourceType, sourceId);
+        List<DmsAccessExclusionEntity> 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<DmsAccessExclusionEntity> 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<DmsAccessExclusionEntity> 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<DmsAccessExclusionVO> 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<DmsAccessExclusionEntity> 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<DmsAccessExclusionVO> result = new DmsCursorPageVO<>();
+        List<DmsAccessExclusionVO> values = rows.stream().map(this::toVO).collect(Collectors.toList());
+        Map<String, String> 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<DmsAccessExclusionVO> 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<UUID> ids = new ArrayList<>(new LinkedHashSet<>(form.getExclusionIds()));
+        ids.sort(Comparator.comparing(UUID::toString));
+        List<DmsAccessExclusionEntity> 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<DmsAccessExclusionEntity> 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 涓嶈兘涓虹┖锛宻ourceId 闀垮害涓嶈兘瓒呰繃 200");
+        }
+        if (form.getItems() == null || form.getItems().isEmpty()
+                || form.getItems().size() > properties.getMaxExclusionBatchSize()) {
+            invalid("鎺掗櫎鎵归噺澶у皬蹇呴』鍦� 1 鍒� " + properties.getMaxExclusionBatchSize() + " 涔嬮棿");
+        }
+    }
+
+    private List<AccessExclusionItemForm> normalizeAndValidate(List<AccessExclusionItemForm> source) {
+        List<AccessExclusionItemForm> result = new ArrayList<>();
+        Set<String> 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<AccessExclusionItemForm> items) {
+        List<UUID> ids = items.stream().map(AccessExclusionItemForm::getResourceId).distinct()
+                .collect(Collectors.toList());
+        Map<UUID, DmsPermissionResourceEntity> 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<DmsAccessExclusionEntity> existing,
+                                       List<AccessExclusionItemForm> requested) {
+        if (existing.size() != requested.size() || existing.stream().anyMatch(row -> row.getRevokedAt() != null)) {
+            throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
+                    "source_id 宸插瓨鍦紝浣嗘帓闄ら泦鍚堟暟閲忎笉鍚屾垨鍖呭惈宸叉挙閿�鎺掗櫎");
+        }
+        Map<String, DmsAccessExclusionEntity> 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);
+    }
+}

--
Gitblit v1.8.0