package jnpf.dmsService; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.enums.DmsPersonalFolderType; import jnpf.dmsEntity.permission.form.DmsPersonalFolderBorrowForm; import jnpf.dmsEntity.permission.form.DmsPersonalFolderDownloadForm; import jnpf.dmsEntity.permission.form.DmsPersonalFolderFavoriteForm; import jnpf.dmsEntity.permission.form.DmsPersonalFolderFileForm; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.permission.vo.DmsPersonalFolderAddResultVO; import jnpf.dmsEntity.permission.vo.DmsPersonalFolderFileVO; import jnpf.dmsEntity.permission.vo.DmsPersonalFolderVO; import jnpf.dmsEntity.print.form.DmsWorkflowPrintFilesGrantForm; import jnpf.dmsEntity.print.vo.DmsWorkflowPrintFilesGrantResultVO; import jnpf.dmsMapper.DmsPersonalFolderMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsFileSearchPolicy; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsPermission.DmsPermissionExecutionContext; import jnpf.dmsPermission.DmsPermissionGuard; import jnpf.dmsPermission.DmsPermissionOperation; import jnpf.dmsPermission.DmsPermissionProperties; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.UUID; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class DmsPersonalFolderService { private static final String BORROW_PERMISSION_SOURCE_PREFIX = "personal-folder-borrow:"; private static final String DOWNLOAD_PERMISSION_SOURCE_PREFIX = "personal-folder-download:"; private static final String PRINT_PERMISSION_SOURCE_PREFIX = "workflow-print-files:"; private final DmsCurrentIdentity currentIdentity; private final DmsPermissionProperties properties; private final DmsPersonalFolderMapper personalFolderMapper; private final DmsPermissionGrantService permissionGrantService; private final DmsPermissionGuard permissionGuard; public List folders() { currentIdentity.requireUser(); return Arrays.stream(DmsPersonalFolderType.values()) .map(type -> new DmsPersonalFolderVO(type.name(), type.getLabel())) .collect(Collectors.toList()); } @Transactional(readOnly = true) public List files(String folderType, String keyword, Integer pageSize) { UserInfo user = currentIdentity.requireUser(); DmsPersonalFolderType type = folderType(folderType); String normalizedKeyword = DmsFileSearchPolicy.normalizeKeyword(keyword); return personalFolderMapper.selectFiles( currentIdentity.tenantId(user), user.getUserId().trim(), type.name(), normalizedKeyword, pageSize(pageSize)); } @Transactional(rollbackFor = Exception.class) @AuditLog(action = "ADD_FAVORITE_SNAPSHOT", label = "收藏文件加入个人文件夹", bizModule = "DMS", bizType = "DMS_PERSONAL_FOLDER", bizCodeExpr = "", bizCodeRequired = false, targetTable = "dms_personal_folder_items", targetIdExpr = "#p0.fileVersionId", extraExpr = "{'documentId': #p0.documentId, 'operationId': #p0.operationId}") public DmsPersonalFolderAddResultVO addFavoriteSnapshot( DmsPersonalFolderFavoriteForm form) { UserInfo user = currentIdentity.requireUser(); if (form == null || form.getDocumentId() == null || form.getFileVersionId() == null || form.getOperationId() == null) { throw invalidRequest("收藏文件ID、文件版本ID和操作ID不能为空"); } String tenantId = currentIdentity.tenantId(user); String userId = user.getUserId().trim(); DmsPersonalFolderFileForm.FileRelation relation = new DmsPersonalFolderFileForm.FileRelation(); relation.setDocumentId(form.getDocumentId()); relation.setFileVersionId(form.getFileVersionId()); validateRelations(tenantId, Collections.singletonList(relation)); DmsPermissionExecutionContext context = new DmsPermissionExecutionContext(); context.setResourceType(DmsResourceType.FILE_VERSION); context.setResourceId(form.getFileVersionId()); context.setActionCode(DmsPermissionAction.READ); context.setOperationId(form.getOperationId()); int added = permissionGuard.execute(context, new DmsPermissionOperation() { @Override public Integer execute() { return insertFavoriteSnapshot(); } @Override public Integer replay() { return insertFavoriteSnapshot(); } private int insertFavoriteSnapshot() { return personalFolderMapper.insertFavoriteSnapshot( tenantId, userId, form.getDocumentId(), form.getFileVersionId()); } }); return new DmsPersonalFolderAddResultVO(1, added); } @Transactional(rollbackFor = Exception.class) @AuditLog(action = "ADD_BORROW_SNAPSHOT", label = "借阅文件加入个人文件夹", bizModule = "DMS", bizType = "DMS_PERSONAL_FOLDER", bizCodeExpr = "", bizCodeRequired = false, targetTable = "dms_personal_folder_items", targetIdExpr = "#p0.files[0].fileVersionId", targetIdsExpr = "#p0.files.![fileVersionId]", extraExpr = "{'userId': #p0.userId, 'serviceId': #p1, 'files': #p0.files}") public DmsPersonalFolderAddResultVO addBorrowSnapshots( DmsPersonalFolderBorrowForm form, String serviceId) { UserInfo currentUser = currentIdentity.requireUser(); String tenantId = currentIdentity.tenantId(currentUser); String userId = requiredText(form == null ? null : form.getUserId(), "借阅用户ID不能为空"); List files = distinctFiles(form, "借阅"); validateRelations(tenantId, files); grantBorrowReadPermissions(tenantId, userId, files, serviceId); int added = personalFolderMapper.insertBorrowSnapshots(tenantId, userId, files); return new DmsPersonalFolderAddResultVO(form.getFiles().size(), added); } @Transactional(rollbackFor = Exception.class) @AuditLog(action = "ADD_DOWNLOAD_SNAPSHOT", label = "下载文件加入个人文件夹", bizModule = "DMS", bizType = "DMS_PERSONAL_FOLDER", bizCodeExpr = "", bizCodeRequired = false, targetTable = "dms_personal_folder_items", targetIdExpr = "#p0.files[0].fileVersionId", targetIdsExpr = "#p0.files.![fileVersionId]", extraExpr = "{'userId': #p0.userId, 'serviceId': #p1, 'files': #p0.files}") public DmsPersonalFolderAddResultVO addDownloadSnapshots( DmsPersonalFolderDownloadForm form, String serviceId) { UserInfo currentUser = currentIdentity.requireUser(); String tenantId = currentIdentity.tenantId(currentUser); String userId = requiredText(form == null ? null : form.getUserId(), "下载用户ID不能为空"); List files = distinctFiles(form, "下载"); validateRelations(tenantId, files); grantDownloadPermissions(tenantId, userId, files, serviceId, form.getRequestId()); int added = personalFolderMapper.insertDownloadSnapshots(tenantId, userId, files); return new DmsPersonalFolderAddResultVO(form.getFiles().size(), added); } @Transactional(rollbackFor = Exception.class) public DmsWorkflowPrintFilesGrantResultVO grantPrintPermissions( DmsWorkflowPrintFilesGrantForm form, String serviceId) { UserInfo currentUser = currentIdentity.requireUser(); String tenantId = currentIdentity.tenantId(currentUser); String userId = requiredText(form == null ? null : form.getUserId(), "打印用户ID不能为空"); List files = distinctFiles(form, "打印"); validateRelations(tenantId, files); grantPrintPermissions(tenantId, userId, files, serviceId); return new DmsWorkflowPrintFilesGrantResultVO(form.getFiles().size(), files.size()); } private List distinctFiles( DmsPersonalFolderFileForm form, String actionLabel) { if (form == null || form.getFiles() == null || form.getFiles().isEmpty()) { throw invalidRequest(actionLabel + "文件不能为空"); } if (form.getFiles().size() > 200) { throw invalidRequest("单次最多添加200个" + actionLabel + "文件"); } Map byVersion = new LinkedHashMap<>(); Map versionByDocument = new LinkedHashMap<>(); for (DmsPersonalFolderFileForm.FileRelation file : form.getFiles()) { if (file == null || file.getDocumentId() == null || file.getFileVersionId() == null) { throw invalidRequest(actionLabel + "文件ID和文件版本ID不能为空"); } DmsPersonalFolderFileForm.FileRelation normalized = copyWithValidatedValidity(file); UUID previousVersion = versionByDocument.putIfAbsent( normalized.getDocumentId(), normalized.getFileVersionId()); if (previousVersion != null && !previousVersion.equals(normalized.getFileVersionId())) { throw invalidRequest("同一文件不能关联多个" + actionLabel + "版本:" + normalized.getDocumentId()); } DmsPersonalFolderFileForm.FileRelation previous = byVersion.putIfAbsent( normalized.getFileVersionId(), normalized); if (previous != null && !Objects.equals( previous.getValidTo(), normalized.getValidTo())) { throw invalidRequest("同一文件版本不能设置多个" + actionLabel + "有效期:" + normalized.getFileVersionId()); } } return new ArrayList<>(byVersion.values()); } private DmsPersonalFolderFileForm.FileRelation copyWithValidatedValidity( DmsPersonalFolderFileForm.FileRelation file) { DmsPersonalFolderFileForm.FileRelation normalized = new DmsPersonalFolderFileForm.FileRelation(); normalized.setDocumentId(file.getDocumentId()); normalized.setFileVersionId(file.getFileVersionId()); normalized.setValidTo(optionalFuture(file.getValidTo())); return normalized; } private void grantBorrowReadPermissions( String tenantId, String userId, List files, String serviceId) { Map> filesByValidity = groupByValidity(files); for (Map.Entry> entry : filesByValidity.entrySet()) { Date validTo = entry.getKey() == null ? null : new Date(entry.getKey()); List fileVersionIds = versionIds(entry.getValue()); permissionGrantService.ensureWorkflowBorrowReadPermissions( tenantId, permissionSourceId(BORROW_PERMISSION_SOURCE_PREFIX, userId, fileVersionIds, validTo), userId, fileVersionIds, validTo, serviceId, Collections.emptyList()); } } private void grantDownloadPermissions( String tenantId, String userId, List files, String serviceId, String requestId) { Map> filesByValidity = groupByValidity(files); for (Map.Entry> entry : filesByValidity.entrySet()) { Date validTo = entry.getKey() == null ? null : new Date(entry.getKey()); List fileVersionIds = versionIds(entry.getValue()); List permissions = permissionGrantService.ensureWorkflowDownloadPermissions( tenantId, permissionSourceId(DOWNLOAD_PERMISSION_SOURCE_PREFIX, requestId == null || requestId.trim().isEmpty() ? userId : userId + "|request:" + requestId.trim(), fileVersionIds, validTo), userId, fileVersionIds, validTo, serviceId, Collections.emptyList()); if (permissions != null) { for (DmsPersonalFolderFileForm.FileRelation file : entry.getValue()) { permissions.stream().filter(permission -> file.getFileVersionId().equals(permission.getResourceId())) .findFirst().ifPresent(permission -> file.setValidTo(permission.getValidTo())); } } } } private void grantPrintPermissions( String tenantId, String userId, List files, String serviceId) { Map> filesByValidity = groupByValidity(files); for (Map.Entry> entry : filesByValidity.entrySet()) { Date validTo = entry.getKey() == null ? null : new Date(entry.getKey()); List fileVersionIds = versionIds(entry.getValue()); permissionGrantService.ensureWorkflowPrintPermissions( tenantId, permissionSourceId(PRINT_PERMISSION_SOURCE_PREFIX, userId, fileVersionIds, validTo), userId, fileVersionIds, validTo, serviceId, Collections.emptyList()); } } private Map> groupByValidity( List files) { Map> filesByValidity = new TreeMap<>(Comparator.nullsLast(Long::compareTo)); for (DmsPersonalFolderFileForm.FileRelation file : files) { Long validTo = file.getValidTo() == null ? null : file.getValidTo().getTime(); filesByValidity.computeIfAbsent(validTo, ignored -> new ArrayList<>()) .add(file); } return filesByValidity; } private List versionIds(List files) { return files.stream().map(DmsPersonalFolderFileForm.FileRelation::getFileVersionId) .collect(Collectors.toList()); } private void validateRelations( String tenantId, List files) { List versionIds = files.stream() .map(DmsPersonalFolderFileForm.FileRelation::getFileVersionId) .collect(Collectors.toList()); List readableVersions = personalFolderMapper .selectReadableVersions(tenantId, versionIds); if (readableVersions == null) { readableVersions = new ArrayList<>(); } Map versions = readableVersions.stream() .collect(Collectors.toMap(DmsFileVersionEntity::getId, value -> value)); for (DmsPersonalFolderFileForm.FileRelation file : files) { DmsFileVersionEntity version = versions.get(file.getFileVersionId()); if (version == null) { throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "文件版本不存在或不可用:" + file.getFileVersionId()); } if (!file.getDocumentId().equals(version.getDocumentId())) { throw invalidRequest("文件与文件版本关联不一致:" + file.getFileVersionId()); } } } private DmsPersonalFolderType folderType(String value) { try { return DmsPersonalFolderType.valueOf(value == null ? "" : value.trim().toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "个人文件夹类型无效"); } } private int pageSize(Integer requested) { int value = requested == null ? properties.getDefaultPageSize() : requested; if (value < 1 || value > properties.getMaxPageSize()) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "pageSize 必须在 1 到 " + properties.getMaxPageSize() + " 之间"); } return value; } private String requiredText(String value, String message) { if (value == null || value.trim().isEmpty()) { throw invalidRequest(message); } return value.trim(); } private Date optionalFuture(Date value) { if (value == null) { return null; } Date copy = new Date(value.getTime()); if (!copy.after(new Date())) { throw invalidRequest("有效期必须晚于当前时间"); } return copy; } private String permissionSourceId(String prefix, String userId, List fileVersionIds, Date validTo) { List sortedIds = new ArrayList<>(fileVersionIds); sortedIds.sort(Comparator.comparing(UUID::toString)); String validity = validTo == null ? "PERMANENT" : Long.toString(validTo.getTime()); String canonical = userId + "|" + validity + "|" + sortedIds.stream() .map(UUID::toString).collect(Collectors.joining(",")); UUID fingerprint = UUID.nameUUIDFromBytes(canonical.getBytes(StandardCharsets.UTF_8)); return prefix + fingerprint; } private DmsPermissionException invalidRequest(String message) { return DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, message); } }