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 不能为空,sourceId 长度不能超过 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);
|
}
|
}
|