| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- package com.dms.service;
- import com.dms.common.DmsException;
- import com.dms.common.Errors;
- import com.dms.common.Payloads;
- import com.dms.mapper.BorrowRequestMapper;
- import com.dms.mapper.DocumentMapper;
- import com.dms.mapper.NotificationMapper;
- import com.dms.mapper.OrganizationMapper;
- import com.dms.mapper.PermissionMapper;
- import com.dms.mapper.UserMapper;
- import com.dms.model.BorrowRequestEntity;
- import com.dms.model.BorrowRequestItemEntity;
- import com.dms.model.DocumentEntity;
- import com.dms.model.NotificationEntity;
- import com.dms.model.OrganizationEntity;
- import com.dms.model.PermissionEntity;
- import com.dms.model.UserEntity;
- import com.dms.security.AuthContext;
- import com.dms.security.Reauth;
- import com.dms.storage.StoragePaths;
- import com.dms.storage.UploadStorage.StagedUpload;
- import jakarta.servlet.http.HttpServletRequest;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.time.Duration;
- import java.time.LocalDateTime;
- import java.time.OffsetDateTime;
- import java.time.ZoneOffset;
- import java.time.format.DateTimeFormatter;
- import java.time.format.DateTimeParseException;
- import java.util.ArrayList;
- import java.util.LinkedHashMap;
- import java.util.LinkedHashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import org.springframework.core.io.FileSystemResource;
- import org.springframework.core.io.Resource;
- import org.springframework.http.HttpHeaders;
- import org.springframework.http.ResponseEntity;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import org.springframework.web.multipart.MultipartFile;
- /**
- * 借阅申请审批流(V22迁移,契约1.37)。
- *
- * <p>借阅授权入口由手工配置改为申请-审批:无查看且无下载权限(密级足够)的用户
- * 发起借阅申请,一批用户共用一张审批单(必传 PDF/图片,契约1.72);申请以 PENDING 待办
- * 通知所有对该文档有授权权限(CONFIG_PERMISSION,矩阵反查+内置超管)的用户,
- * 任一审批人同意/拒绝即定(先到先得),其余待办自动失效;通过后逐条目落地
- * doc_permission(复用借阅 ACL 模型,到期/回收站语义自动继承),凭证以引用
- * 方式挂接各条目;申请人收到结果通知。</p>
- */
- @Service
- public class BorrowRequestService {
- private static final Set<String> REQUEST_FIELDS = Set.of("entries", "expiresAt");
- private static final Set<String> ENTRY_FIELDS = Set.of("subjectId", "canView", "canDownload");
- private static final String[] ACTION_FIELDS = {"canView", "canDownload"};
- /** 与借阅授权同口径的借阅时限:最长7天 + 60秒拾取舍容忍差。 */
- private static final Duration MAX_BORROW = Duration.ofDays(7);
- private static final Duration BORROW_TOLERANCE = Duration.ofSeconds(60);
- /** 对齐 Python isoformat(timespec="milliseconds") + "Z"。 */
- private static final DateTimeFormatter MILLIS_ISO_UTC =
- DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
- /** 申请条目(校验归一后的视图)。 */
- private record EntryRequest(long subjectId, boolean canView, boolean canDownload) {
- }
- private final DocumentMapper documentMapper;
- private final PermissionMapper permissionMapper;
- private final UserMapper userMapper;
- private final OrganizationMapper organizationMapper;
- private final BorrowRequestMapper borrowRequestMapper;
- private final NotificationMapper notificationMapper;
- private final AuthorizationService authorizationService;
- private final AuditService auditService;
- private final Reauth reauth;
- private final VoucherService voucherService;
- private final StoragePaths storagePaths;
- public BorrowRequestService(DocumentMapper documentMapper,
- PermissionMapper permissionMapper,
- UserMapper userMapper,
- OrganizationMapper organizationMapper,
- BorrowRequestMapper borrowRequestMapper,
- NotificationMapper notificationMapper,
- AuthorizationService authorizationService,
- AuditService auditService,
- Reauth reauth,
- VoucherService voucherService,
- StoragePaths storagePaths) {
- this.documentMapper = documentMapper;
- this.permissionMapper = permissionMapper;
- this.userMapper = userMapper;
- this.organizationMapper = organizationMapper;
- this.borrowRequestMapper = borrowRequestMapper;
- this.notificationMapper = notificationMapper;
- this.authorizationService = authorizationService;
- this.auditService = auditService;
- this.reauth = reauth;
- this.voucherService = voucherService;
- this.storagePaths = storagePaths;
- }
- // ------------------------------------------------------------ 提交申请
- /**
- * 提交借阅申请:一批用户共用一张审批单(必传 PDF/图片,契约1.72),落库后向所有
- * 有授权权限的用户发 PENDING 待办通知。文件提升放事务成功之后。
- */
- public Map<String, Object> createRequest(long documentId, Map<String, Object> payload,
- MultipartFile file,
- HttpServletRequest request) {
- // 契约1.66:调阅申请不再要求二次认证,改为强制上传审批单(无凭证不受理)
- if (file == null || file.isEmpty()) {
- throw Errors.invalidArgument("调阅申请必须上传审批单(PDF 或图片)");
- }
- List<EntryRequest> entries = parseEntries(payload);
- LocalDateTime expiresAt = parseExpires(payload.get("expiresAt"));
- // 非锁定预读仅取密级定凭证存储域;完整校验在 doCreate 事务内(审批单已必传)
- DocumentEntity preview = documentMapper.findByIdNotDeleted(documentId);
- StagedUpload staged = voucherService.stageVoucher(file,
- preview == null ? null : preview.getSecurityLevel());
- Map<String, Object> result;
- try {
- result = doCreate(documentId, entries, expiresAt, staged, request);
- if (staged != null) {
- staged.promote();
- }
- } catch (RuntimeException ex) {
- if (staged != null) {
- staged.cleanupAll();
- }
- throw ex;
- }
- return result;
- }
- @Transactional
- protected Map<String, Object> doCreate(long documentId, List<EntryRequest> entries,
- LocalDateTime expiresAt, StagedUpload staged,
- HttpServletRequest request) {
- AuthContext context = AuthContext.get();
- DocumentEntity document = documentMapper.findByIdNotDeleted(documentId);
- if (document == null) {
- throw Errors.resourceNotFound("文档不存在");
- }
- if ("ATTACHMENT".equals(document.getDocumentType())) {
- throw Errors.attachmentHasNoAcl();
- }
- if ("SUB_PLAN".equals(document.getDocumentType())) {
- throw Errors.subPlanPermissionInherited();
- }
- authorizationService.requireSecurityClearance(document, context);
- // 申请门槛:仅对文档既无查看又无下载权限的用户可发起(前端按钮同口径)
- List<String> allowed = authorizationService.planAllowedActions(document, context,
- authorizationService.evaluatePlanAccess(document, context));
- if (allowed.contains("VIEW") || allowed.contains("DOWNLOAD")) {
- throw Errors.invalidArgument("你已拥有该文档的查看或下载权限,无需调阅");
- }
- Map<Long, UserEntity> subjects = lockedSubjects(entries);
- for (Map.Entry<Long, UserEntity> row : subjects.entrySet()) {
- UserEntity user = row.getValue();
- if (!authorizationService.clearanceSatisfied(user.getSecurityLevel(),
- document.getSecurityLevel())) {
- Map<String, Object> details = Payloads.orderedMap();
- details.put("subjectId", String.valueOf(row.getKey()));
- details.put("subjectName", user.getRealName());
- throw Errors.securityLevelForbidden();
- }
- if (borrowRequestMapper.countPendingBySubject(document.getId(), row.getKey()) > 0) {
- throw new DmsException("BORROW_REQUEST_DUPLICATE", 409,
- "该用户对该文档已有待审批的调阅申请", Payloads.orderedMap());
- }
- }
- LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
- BorrowRequestEntity entity = new BorrowRequestEntity();
- entity.setDocumentId(document.getId());
- entity.setRequesterId(context.userId());
- entity.setRequesterName(context.realName());
- entity.setExpiresAt(expiresAt);
- if (staged != null) {
- entity.setVoucherFileName(staged.originalFileName());
- entity.setVoucherStoragePath(staged.relativePath());
- entity.setVoucherFileSize(staged.fileSize());
- entity.setVoucherUploadedAt(now);
- }
- entity.setStatus("PENDING");
- borrowRequestMapper.insertRequest(entity);
- List<Map<String, Object>> auditEntries = new ArrayList<>();
- for (EntryRequest entry : entries) {
- BorrowRequestItemEntity item = new BorrowRequestItemEntity();
- item.setRequestId(entity.getId());
- item.setSubjectId(entry.subjectId());
- item.setSubjectName(subjects.get(entry.subjectId()).getRealName());
- item.setCanView(entry.canView());
- item.setCanDownload(entry.canDownload());
- borrowRequestMapper.insertItem(item);
- Map<String, Object> auditItem = Payloads.orderedMap();
- auditItem.put("subjectId", String.valueOf(entry.subjectId()));
- auditItem.put("subjectName", subjects.get(entry.subjectId()).getRealName());
- auditItem.put("canView", entry.canView());
- auditItem.put("canDownload", entry.canDownload());
- auditEntries.add(auditItem);
- }
- Map<Long, String> approvers = resolveApprovers(document, context.userId());
- for (Map.Entry<Long, String> approver : approvers.entrySet()) {
- notificationMapper.insert(pendingNotification(entity, document,
- approver.getKey(), approver.getValue()));
- }
- Map<String, Object> auditDetail = Payloads.orderedMap();
- auditDetail.put("documentId", String.valueOf(document.getId()));
- auditDetail.put("requesterId", String.valueOf(context.userId()));
- auditDetail.put("requesterName", context.realName());
- auditDetail.put("entries", auditEntries);
- auditDetail.put("expiresAt", PermissionService.iso(expiresAt));
- auditDetail.put("voucherFileName", entity.getVoucherFileName());
- auditDetail.put("notified", approvers.size());
- auditService.insert(auditService.businessAudit(request,
- AuditService.Action.BORROW_REQUEST_CREATE, "BORROW_REQUEST",
- entity.getId(), document.getDocumentName(),
- auditService.toJson(auditDetail)));
- Map<String, Object> result = Payloads.orderedMap();
- result.put("requestId", String.valueOf(entity.getId()));
- result.put("notified", approvers.size());
- return result;
- }
- // ------------------------------------------------------------ 查询
- /** 申请详情:申请人本人或对该文档有授权权限的用户可见(审批人查看审批单)。 */
- @Transactional(readOnly = true)
- public Map<String, Object> getRequestDetail(long requestId, HttpServletRequest request) {
- AuthContext context = AuthContext.get();
- BorrowRequestEntity entity = borrowRequestMapper.findById(requestId);
- if (entity == null) {
- throw Errors.resourceNotFound("调阅申请不存在");
- }
- DocumentEntity document = requireVisibleDocument(entity, context);
- List<Map<String, Object>> items = new ArrayList<>();
- for (BorrowRequestItemEntity item : borrowRequestMapper.findItems(entity.getId())) {
- Map<String, Object> values = Payloads.orderedMap();
- values.put("subjectId", String.valueOf(item.getSubjectId()));
- values.put("subjectName", item.getSubjectName());
- values.put("canView", Boolean.TRUE.equals(item.getCanView()));
- values.put("canDownload", Boolean.TRUE.equals(item.getCanDownload()));
- items.add(values);
- }
- Map<String, Object> result = Payloads.orderedMap();
- result.put("requestId", String.valueOf(entity.getId()));
- result.put("documentId", String.valueOf(document.getId()));
- result.put("documentName", document.getDocumentName());
- result.put("requesterId", String.valueOf(entity.getRequesterId()));
- result.put("requesterName", entity.getRequesterName());
- result.put("entries", items);
- result.put("expiresAt", PermissionService.iso(entity.getExpiresAt()));
- result.put("voucherFileName", entity.getVoucherFileName());
- result.put("voucherFileSize", entity.getVoucherFileSize());
- result.put("status", entity.getStatus());
- result.put("handledById", entity.getHandledBy() == null
- ? null : String.valueOf(entity.getHandledBy()));
- result.put("handledByName", entity.getHandledByName());
- result.put("handledAt", entity.getHandledAt() == null
- ? null : PermissionService.iso(entity.getHandledAt()));
- result.put("createdAt", PermissionService.iso(entity.getCreatedAt()));
- return result;
- }
- /** 申请审批单下载(attachment 直出,PDF/图片按扩展名映射类型,权限同详情)。 */
- @Transactional(readOnly = true)
- public ResponseEntity<Resource> downloadRequestVoucher(long requestId,
- HttpServletRequest request) {
- AuthContext context = AuthContext.get();
- BorrowRequestEntity entity = borrowRequestMapper.findById(requestId);
- if (entity == null) {
- throw Errors.resourceNotFound("调阅申请不存在");
- }
- requireVisibleDocument(entity, context);
- if (entity.getVoucherStoragePath() == null
- || entity.getVoucherStoragePath().isEmpty()) {
- throw Errors.resourceNotFound("该申请未上传审批单");
- }
- Path path = storagePaths.resolveStoragePath(entity.getVoucherStoragePath());
- if (!Files.isRegularFile(path)) {
- throw Errors.resourceNotFound("审批单文件不存在或已被清理");
- }
- String name = entity.getVoucherFileName() != null
- ? entity.getVoucherFileName() : "voucher.pdf";
- return ResponseEntity.ok()
- .contentType(VoucherService.voucherMediaType(name))
- .header(HttpHeaders.CONTENT_DISPOSITION,
- FileReadService.attachmentDisposition(name))
- .header("X-Content-Type-Options", "nosniff")
- .body(new FileSystemResource(path));
- }
- // ------------------------------------------------------------ 审批联动(通知 handle 触发)
- /**
- * 任一审批人处理即定(先到先得):PENDING 行锁守卫 + 审批人实时鉴权
- * (防止通知发出后角色/密级变化导致越权);通过则逐条目替换式落地借阅
- * ACL 并以引用挂接审批单;其余审批人待办同步失效;申请人收结果通知。
- */
- @Transactional
- public void handleApproval(long requestId, String action, long notificationId,
- HttpServletRequest request) {
- reauth.require(request);
- AuthContext context = AuthContext.get();
- BorrowRequestEntity entity = borrowRequestMapper.findForUpdate(requestId);
- if (entity == null) {
- throw Errors.resourceNotFound("调阅申请不存在");
- }
- if (!"PENDING".equals(entity.getStatus())) {
- throw Errors.dataVersionConflict("该调阅申请已被处理");
- }
- DocumentEntity document = documentMapper.findByIdNotDeleted(entity.getDocumentId());
- if (document == null) {
- throw Errors.resourceNotFound("文档不存在");
- }
- // 审批人运行时鉴权:与详情/授权配置同口径(密级前置 + CONFIG_PERMISSION 位)
- authorizationService.requirePlanAction(document, context, "CONFIG_PERMISSION");
- LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
- boolean approved = "AGREE".equals(action);
- String finalStatus = approved ? "APPROVED" : "REJECTED";
- int updated = borrowRequestMapper.updateHandled(requestId, finalStatus,
- context.userId(), context.realName(), now);
- if (updated == 0) {
- throw Errors.dataVersionConflict("该调阅申请已被处理");
- }
- List<BorrowRequestItemEntity> items =
- borrowRequestMapper.findItems(requestId);
- if (approved) {
- grantPermissions(document, entity, items, context, now);
- }
- notificationMapper.settlePendingByBiz(requestId, "APPROVAL", notificationId, action, now);
- NotificationEntity result = new NotificationEntity();
- result.setUserId(entity.getRequesterId());
- result.setTitle(approved ? "调阅申请已通过" : "调阅申请已拒绝");
- result.setContent("你申请调阅的《" + document.getDocumentName() + "》已被 "
- + context.realName() + (approved ? " 通过" : " 拒绝"));
- result.setNotifyType("APPROVAL");
- result.setAction("NONE");
- result.setStatus("UNREAD");
- result.setBizId(requestId);
- notificationMapper.insert(result);
- Map<String, Object> auditDetail = Payloads.orderedMap();
- auditDetail.put("requestId", String.valueOf(requestId));
- auditDetail.put("documentId", String.valueOf(document.getId()));
- auditDetail.put("requesterId", String.valueOf(entity.getRequesterId()));
- auditDetail.put("granted", approved ? items.size() : 0);
- auditService.insert(auditService.businessAudit(request,
- approved ? AuditService.Action.BORROW_REQUEST_APPROVE
- : AuditService.Action.BORROW_REQUEST_REJECT,
- "BORROW_REQUEST", requestId, document.getDocumentName(),
- auditService.toJson(auditDetail)));
- }
- // ------------------------------------------------------------ 内部实现
- /** 锁读申请用户并校验存在/启用,返回主体ID→用户(对齐 PermissionService.lockedSubjectNames,无实例状态)。 */
- private Map<Long, UserEntity> lockedSubjects(List<EntryRequest> entries) {
- Set<Long> userIds = new LinkedHashSet<>();
- for (EntryRequest entry : entries) {
- userIds.add(entry.subjectId());
- }
- if (userIds.isEmpty()) {
- throw Errors.invalidArgument("entries不能为空");
- }
- Map<Long, UserEntity> users = new LinkedHashMap<>();
- for (UserEntity user : userMapper.listActiveByIdsForUpdate(userIds)) {
- if ("ENABLED".equals(user.getStatus())) {
- users.put(user.getId(), user);
- }
- }
- if (!users.keySet().equals(userIds)) {
- throw Errors.resourceNotFound("调阅用户不存在、已删除或已禁用");
- }
- return users;
- }
- /** 审批人反查:矩阵 can_config_permission=1(密级×OWN/OTHER 口径对齐)+ 内置超管。 */
- private Map<Long, String> resolveApprovers(DocumentEntity document, long requesterId) {
- String docOrgPath = null;
- if (document.getOrganizationId() != null) {
- OrganizationEntity docOrg =
- organizationMapper.findByIdNotDeleted(document.getOrganizationId());
- docOrgPath = docOrg == null ? null : docOrg.getOrgPath();
- }
- Map<Long, String> approvers = new LinkedHashMap<>();
- for (Map<String, Object> row
- : borrowRequestMapper.listConfigApprovers(document.getSecurityLevel(),
- docOrgPath, requesterId)) {
- approvers.put(((Number) row.get("id")).longValue(),
- (String) row.get("real_name"));
- }
- UserEntity admin = userMapper.findActiveByUsername(
- AdminPermission.SUPER_ADMIN_USERNAME);
- if (admin != null && "ENABLED".equals(admin.getStatus())
- && admin.getId() != requesterId) {
- approvers.put(admin.getId(), admin.getRealName());
- }
- if (approvers.isEmpty()) {
- throw Errors.invalidArgument("暂无可审批该申请的用户,请联系管理员");
- }
- return approvers;
- }
- /** 审批通过:软删同人旧活跃 ACL 后按申请条目替换式插入,凭证引用挂接各条目。 */
- private void grantPermissions(DocumentEntity document, BorrowRequestEntity entity,
- List<BorrowRequestItemEntity> items,
- AuthContext context, LocalDateTime now) {
- Map<Long, PermissionEntity> activeBySubject = new LinkedHashMap<>();
- for (PermissionEntity permission
- : permissionMapper.listActiveByDocumentForUpdate(document.getId())) {
- activeBySubject.put(permission.getSubjectId(), permission);
- }
- for (BorrowRequestItemEntity item : items) {
- PermissionEntity existing = activeBySubject.get(item.getSubjectId());
- if (existing != null) {
- permissionMapper.softDeleteById(existing.getId(), now, context.userId());
- }
- PermissionEntity granted = new PermissionEntity();
- granted.setDocumentId(document.getId());
- granted.setSubjectId(item.getSubjectId());
- granted.setSubjectName(item.getSubjectName());
- granted.setCanView(Boolean.TRUE.equals(item.getCanView()));
- granted.setCanDownload(Boolean.TRUE.equals(item.getCanDownload()));
- granted.setExpiresAt(entity.getExpiresAt());
- granted.setVoucherFileName(entity.getVoucherFileName());
- granted.setVoucherStoragePath(entity.getVoucherStoragePath());
- granted.setVoucherFileSize(entity.getVoucherFileSize());
- granted.setVoucherUploadedAt(entity.getVoucherUploadedAt());
- granted.setCreatedBy(context.userId());
- granted.setUpdatedBy(context.userId());
- permissionMapper.insert(granted, now);
- }
- documentMapper.touchDocument(document.getId(), context.userId(),
- context.realName(), now);
- }
- /** 申请人可见性判定:本人直通;否则按文档授权位正向校验(与通知触达口径一致)。 */
- private DocumentEntity requireVisibleDocument(BorrowRequestEntity entity,
- AuthContext context) {
- DocumentEntity document = documentMapper.findByIdNotDeleted(entity.getDocumentId());
- if (document == null) {
- throw Errors.resourceNotFound("文档不存在");
- }
- if (entity.getRequesterId() != context.userId()) {
- // 审批流不走文档密码锁:审批人在通知内查看/处理申请属于配置通道
- authorizationService.requirePlanAction(document, context, "CONFIG_PERMISSION");
- }
- return document;
- }
- private static NotificationEntity pendingNotification(BorrowRequestEntity entity,
- DocumentEntity document,
- long approverId,
- String approverName) {
- NotificationEntity notification = new NotificationEntity();
- notification.setUserId(approverId);
- notification.setTitle("调阅审批申请");
- notification.setContent(entity.getRequesterName() + " 申请调阅《"
- + document.getDocumentName() + "》,请审批");
- notification.setNotifyType("APPROVAL");
- notification.setAction("PENDING");
- notification.setStatus("UNREAD");
- notification.setBizId(entity.getId());
- return notification;
- }
- /** 对齐 PermissionService.parseExpires:Z 结尾 UTC 解析 + 借阅时限校验(顶层字段)。 */
- private static LocalDateTime parseExpires(Object value) {
- if (!(value instanceof String text) || !text.endsWith("Z")) {
- throw Errors.invalidArgument("expiresAt必须是UTC时间字符串(Z结尾)");
- }
- OffsetDateTime parsed;
- try {
- parsed = OffsetDateTime.parse(
- text.substring(0, text.length() - 1) + "+00:00",
- DateTimeFormatter.ISO_OFFSET_DATE_TIME);
- } catch (DateTimeParseException ex) {
- throw Errors.invalidArgument("expiresAt不是有效的时间格式");
- }
- LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
- LocalDateTime expires = parsed.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime();
- if (!expires.isAfter(now)) {
- throw Errors.invalidArgument("expiresAt必须晚于当前时间");
- }
- if (expires.isAfter(now.plus(MAX_BORROW).plus(BORROW_TOLERANCE))) {
- throw Errors.invalidArgument("expiresAt调阅时限最长为7天");
- }
- return expires;
- }
- /** 条目解析:字段集精确匹配、字符串ID/布尔位校验、主体去重。 */
- private static List<EntryRequest> parseEntries(Map<String, Object> payload) {
- if (payload == null || !payload.keySet().equals(REQUEST_FIELDS)) {
- throw Errors.invalidArgument("请求字段必须且只能包含entries和expiresAt");
- }
- if (!(payload.get("entries") instanceof List<?> rawEntries)) {
- throw Errors.invalidArgument("entries必须是数组");
- }
- List<EntryRequest> entries = new ArrayList<>();
- Set<Long> seen = new LinkedHashSet<>();
- for (int index = 0; index < rawEntries.size(); index++) {
- Object rawEntry = rawEntries.get(index);
- if (!(rawEntry instanceof Map<?, ?> item)
- || !item.keySet().equals(ENTRY_FIELDS)) {
- throw Errors.invalidArgument(
- "entries[" + index + "]字段必须完整且不能包含未知字段");
- }
- long subjectId = Payloads.stringId(item.get("subjectId"),
- "entries[" + index + "].subjectId", false);
- for (String field : ACTION_FIELDS) {
- if (!(item.get(field) instanceof Boolean)) {
- throw Errors.invalidArgument(
- "entries[" + index + "]." + field + "必须是boolean");
- }
- }
- if (seen.contains(subjectId)) {
- Map<String, Object> details = Payloads.orderedMap();
- details.put("subjectId", String.valueOf(subjectId));
- throw new DmsException("DUPLICATE_PERMISSION_SUBJECT", 400,
- "权限主体不能重复", details);
- }
- seen.add(subjectId);
- entries.add(new EntryRequest(subjectId,
- (Boolean) item.get("canView"), (Boolean) item.get("canDownload")));
- }
- return entries;
- }
- }
|