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/DmsPermissionGrantService.java |  356 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 356 insertions(+), 0 deletions(-)

diff --git a/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsPermissionGrantService.java b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsPermissionGrantService.java
new file mode 100644
index 0000000..4a9f186
--- /dev/null
+++ b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/DmsPermissionGrantService.java
@@ -0,0 +1,356 @@
+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<DmsPermissionGrantVO> 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<PermissionGrantItemForm> items = normalizeAndValidate(form.getItems());
+        permissionMapper.lockSource(tenantId, sourceType, sourceId);
+        List<DmsFilePermissionEntity> 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<DmsFilePermissionEntity> 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<DmsFilePermissionEntity> 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<DmsPermissionGrantVO> revoke(PermissionRevokeForm form) {
+        currentIdentity.requirePermissionAdministrator();
+        String tenantId = currentIdentity.tenantId();
+        String actorId = currentIdentity.userId();
+        String reason = form.getReason().trim();
+        List<UUID> ids = new ArrayList<>(new LinkedHashSet<>(form.getPermissionIds()));
+        ids.sort(Comparator.comparing(UUID::toString));
+        List<DmsFilePermissionEntity> 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<PermissionGrantItemForm> normalizeAndValidate(List<PermissionGrantItemForm> source) {
+        List<PermissionGrantItemForm> result = new ArrayList<>();
+        Set<String> 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<PermissionGrantItemForm> items) {
+        List<UUID> ids = items.stream().map(PermissionGrantItemForm::getResourceId).distinct()
+                .collect(Collectors.toList());
+        Map<UUID, DmsPermissionResourceEntity> 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<DmsFilePermissionEntity> existing,
+                                       List<PermissionGrantItemForm> requested) {
+        if (existing.size() != requested.size() || existing.stream().anyMatch(row -> row.getRevokedAt() != null)) {
+            throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
+                    "source_id 宸插瓨鍦紝浣嗘巿鏉冮泦鍚堟暟閲忎笉鍚屾垨鍖呭惈宸叉挙閿�鏉冮檺");
+        }
+        Map<String, DmsFilePermissionEntity> 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;
+    }
+}

--
Gitblit v1.8.0