SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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 仅密级粗粒度、脱敏无」缺口。
|
||||
*
|
||||
* <p>授权查询:
|
||||
* <ul>
|
||||
* <li>GET /by-target?targetType=档案&targetId=1 — 某档案/分类的全部授权条目(供权限配置页展示);</li>
|
||||
* <li>GET /by-grantee?granteeType=部门&granteeKey=工程部 — 某主体可访问的全部条目;</li>
|
||||
* <li>GET /check?targetType=档案&targetId=1&granteeType=个人&granteeKey=张三 — 查某主体对某档案的合并权限位。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>撤销授权: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<List<ArchiveAcl>> byTarget(@RequestParam String targetType,
|
||||
@RequestParam Long targetId) {
|
||||
return ApiResp.ok(aclRepo.findByTargetTypeAndTargetId(targetType, targetId));
|
||||
}
|
||||
|
||||
/** 某授权主体的全部有效授权。 */
|
||||
@GetMapping("/by-grantee")
|
||||
public ApiResp<List<ArchiveAcl>> 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<AclCheckResult> check(@RequestParam String targetType,
|
||||
@RequestParam Long targetId,
|
||||
@RequestParam String granteeType,
|
||||
@RequestParam String granteeKey) {
|
||||
List<ArchiveAcl> 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<ArchiveAcl> 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<ArchiveAcl> get(@PathVariable Long id) {
|
||||
return ApiResp.ok(require(id));
|
||||
}
|
||||
|
||||
/** 全部有效授权列表(供权限管理总表展示)。 */
|
||||
@GetMapping
|
||||
public ApiResp<List<ArchiveAcl>> 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<ArchiveAcl> 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<ArchiveAcl> 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<ArchiveAcl> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user