package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.ArchiveAcl; import com.kaidi.oa.repository.ArchiveAclRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.time.LocalDate; import java.util.List; /** * 资料室·细粒度权限管理(Module 6「细粒度权限」缺口:LOGIC_GAP → 补深)。 * 每条 ArchiveAcl 为「某授权主体(部门/岗位/项目组/个人)」对「某档案/某分类」的操作位授权 * (可见/预览/下载/打印/改/删/授权),支持按部门/岗位/项目组动态授权,expireDate 到期失效。 * 补审计「细粒度权限无、ArchiveController 仅密级粗粒度、脱敏无」缺口。 * *

授权查询: *

* *

撤销授权:PUT /{id}/revoke(enabled=false),保留记录留痕而不物理删除。 * 写口受 default-deny(ADMIN/APPROVER) 保护。 */ @RestController @RequestMapping("/api/oa/archive-acls") public class ArchiveAclController { private final ArchiveAclRepository aclRepo; public ArchiveAclController(ArchiveAclRepository aclRepo) { this.aclRepo = aclRepo; } /** 某档案或分类下的全部(含已撤销)授权条目。 */ @GetMapping("/by-target") public ApiResp> byTarget(@RequestParam String targetType, @RequestParam Long targetId) { return ApiResp.ok(aclRepo.findByTargetTypeAndTargetId(targetType, targetId)); } /** 某授权主体的全部有效授权。 */ @GetMapping("/by-grantee") public ApiResp> byGrantee(@RequestParam String granteeType, @RequestParam String granteeKey) { return ApiResp.ok(aclRepo.findByGranteeTypeAndGranteeKey(granteeType, granteeKey) .stream().filter(a -> Boolean.TRUE.equals(a.getEnabled())).toList()); } /** * 合并权限检查:查某主体对某档案的合并权限位(取所有 enabled 条目按 OR 合并 7 个操作位)。 * 用于前端在档案预览页决定是否显示下载/打印按钮。 */ @GetMapping("/check") public ApiResp check(@RequestParam String targetType, @RequestParam Long targetId, @RequestParam String granteeType, @RequestParam String granteeKey) { List acls = aclRepo.findByTargetTypeAndTargetId(targetType, targetId) .stream() .filter(a -> Boolean.TRUE.equals(a.getEnabled()) && granteeType.equals(a.getGranteeType()) && granteeKey.equals(a.getGranteeKey())) .toList(); // 检查是否到期 String today = LocalDate.now().toString(); List effective = acls.stream() .filter(a -> a.getExpireDate() == null || a.getExpireDate().compareTo(today) >= 0) .toList(); boolean canView = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanView())); boolean canPreview = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanPreview())); boolean canDownload = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanDownload())); boolean canPrint = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanPrint())); boolean canEdit = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanEdit())); boolean canDelete = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanDelete())); boolean canGrant = effective.stream().anyMatch(a -> Boolean.TRUE.equals(a.getCanGrant())); return ApiResp.ok(new AclCheckResult(granteeType, granteeKey, targetType, targetId, canView, canPreview, canDownload, canPrint, canEdit, canDelete, canGrant)); } public record AclCheckResult( String granteeType, String granteeKey, String targetType, Long targetId, boolean canView, boolean canPreview, boolean canDownload, boolean canPrint, boolean canEdit, boolean canDelete, boolean canGrant) { } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(require(id)); } /** 全部有效授权列表(供权限管理总表展示)。 */ @GetMapping public ApiResp> list() { return ApiResp.ok(aclRepo.findByEnabledTrue()); } public record AclRequest( String granteeType, String granteeKey, String targetType, Long targetId, String targetName, Boolean canView, Boolean canPreview, Boolean canDownload, Boolean canPrint, Boolean canEdit, Boolean canDelete, Boolean canGrant, String expireDate, String grantedBy, String remark) { } /** 新增授权条目。 */ @PostMapping public ApiResp create(@RequestBody AclRequest req) { if (req.granteeType() == null || req.granteeType().isBlank()) { throw new ApiException(400, "授权主体类型(granteeType) 不能为空"); } if (req.granteeKey() == null || req.granteeKey().isBlank()) { throw new ApiException(400, "授权主体标识(granteeKey) 不能为空"); } if (req.targetType() == null || req.targetId() == null) { throw new ApiException(400, "授权目标(targetType/targetId) 不能为空"); } ArchiveAcl a = new ArchiveAcl(); applyAcl(a, req); a.setEnabled(Boolean.TRUE); a.setGrantDate(LocalDate.now().toString()); a.setCreatedAt(Instant.now()); return ApiResp.ok(aclRepo.save(a)); } /** 修改授权条目。 */ @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody AclRequest req) { ArchiveAcl a = require(id); applyAcl(a, req); return ApiResp.ok(aclRepo.save(a)); } /** 撤销授权(enabled→false,留痕不删除)。 */ @PostMapping("/{id}/revoke") public ApiResp revoke(@PathVariable Long id) { ArchiveAcl a = require(id); if (!Boolean.TRUE.equals(a.getEnabled())) { throw new ApiException(409, "该授权条目已撤销"); } a.setEnabled(Boolean.FALSE); return ApiResp.ok(aclRepo.save(a)); } private void applyAcl(ArchiveAcl a, AclRequest req) { if (req.granteeType() != null) a.setGranteeType(req.granteeType().trim()); if (req.granteeKey() != null) a.setGranteeKey(req.granteeKey().trim()); if (req.targetType() != null) a.setTargetType(req.targetType().trim()); if (req.targetId() != null) a.setTargetId(req.targetId()); if (req.targetName() != null) a.setTargetName(req.targetName().trim()); if (req.canView() != null) a.setCanView(req.canView()); if (req.canPreview() != null) a.setCanPreview(req.canPreview()); if (req.canDownload() != null) a.setCanDownload(req.canDownload()); if (req.canPrint() != null) a.setCanPrint(req.canPrint()); if (req.canEdit() != null) a.setCanEdit(req.canEdit()); if (req.canDelete() != null) a.setCanDelete(req.canDelete()); if (req.canGrant() != null) a.setCanGrant(req.canGrant()); if (req.expireDate() != null) a.setExpireDate(req.expireDate().isBlank() ? null : req.expireDate().trim()); if (req.grantedBy() != null) a.setGrantedBy(req.grantedBy().trim()); if (req.remark() != null) a.setRemark(req.remark()); } private ArchiveAcl require(Long id) { return aclRepo.findById(id) .orElseThrow(() -> new NotFoundException("archive acl not found: " + id)); } }