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