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,252 @@
|
||||
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.ItDlpPolicy;
|
||||
import com.kaidi.oa.repository.ItDlpPolicyRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 信息部·网络与信息安全·DLP(数据防泄漏)策略规则台账(需求 §4 数据安全·DLP策略管理)。
|
||||
*
|
||||
* <p>可配置字段级脱敏规则与自动拦截策略台账,IT 管理员在此维护 DLP 规则条目并对接
|
||||
* 数据导出审批(ItDataExportController)形成完整 DLP 管控闭环。
|
||||
*
|
||||
* <p>功能点:
|
||||
* <ul>
|
||||
* <li>CRUD:新建/编辑草稿 DLP 策略;</li>
|
||||
* <li>POST /{id}/enable 启用:草稿/停用 → 启用(须指定审批人);</li>
|
||||
* <li>POST /{id}/disable 停用:启用 → 停用;</li>
|
||||
* <li>POST /{id}/hit 命中记录:内部回调,记录命中时刻并自增 hitCount(联动数据导出审批流);</li>
|
||||
* <li>GET /active 获取当前所有启用规则(按优先级排序);</li>
|
||||
* <li>GET /stats 策略统计看板(类型分布/命中排行/启用数)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>读口含策略明细(含安全拦截规则,属安全机密),见 AuthInterceptor SENSITIVE_READ_PREFIXES(需追加)。
|
||||
* 写口默认受 default-deny(ADMIN/APPROVER) 保护。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/it-dlp-policies")
|
||||
public class ItDlpPolicyController {
|
||||
|
||||
private final ItDlpPolicyRepository repo;
|
||||
|
||||
public ItDlpPolicyController(ItDlpPolicyRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
// ---------- 列表 / 详情 ----------
|
||||
|
||||
@GetMapping
|
||||
public ApiResp<List<ItDlpPolicy>> list(
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String policyType,
|
||||
@RequestParam(required = false) String scope) {
|
||||
if (status != null && !status.isBlank()) {
|
||||
return ApiResp.ok(repo.findByStatus(status));
|
||||
}
|
||||
if (policyType != null && !policyType.isBlank()) {
|
||||
return ApiResp.ok(repo.findByPolicyType(policyType));
|
||||
}
|
||||
if (scope != null && !scope.isBlank()) {
|
||||
return ApiResp.ok(repo.findByScope(scope));
|
||||
}
|
||||
return ApiResp.ok(repo.findAll());
|
||||
}
|
||||
|
||||
/** 获取当前所有启用规则(按优先级升序,优先级1最高)。 */
|
||||
@GetMapping("/active")
|
||||
public ApiResp<List<ItDlpPolicy>> active() {
|
||||
return ApiResp.ok(repo.findByStatusOrderByPriorityAsc("启用"));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResp<ItDlpPolicy> get(@PathVariable Long id) {
|
||||
return ApiResp.ok(require(id));
|
||||
}
|
||||
|
||||
// ---------- 创建 / 编辑 ----------
|
||||
|
||||
public record PolicyRequest(
|
||||
String policyName, String policyType, String scope,
|
||||
String triggerCondition, String ruleDetail, String affectedFields,
|
||||
Integer priority, String actionOnHit,
|
||||
String createdBy, String effectiveDate, String expiryDate, String remark) {
|
||||
}
|
||||
|
||||
/** 新建 DLP 策略(置草稿,待审批后启用)。 */
|
||||
@PostMapping
|
||||
public ApiResp<ItDlpPolicy> create(@RequestBody PolicyRequest req) {
|
||||
if (req.policyName() == null || req.policyName().isBlank()) {
|
||||
throw new ApiException(400, "策略名称(policyName) 不能为空");
|
||||
}
|
||||
if (req.ruleDetail() == null || req.ruleDetail().isBlank()) {
|
||||
throw new ApiException(400, "规则描述(ruleDetail) 不能为空");
|
||||
}
|
||||
Instant now = Instant.now();
|
||||
ItDlpPolicy p = new ItDlpPolicy();
|
||||
p.setPolicyNo("DLP-" + LocalDate.now().toString().replace("-", "")
|
||||
+ "-" + String.format("%03d", (repo.count() + 1) % 1000));
|
||||
p.setPolicyName(req.policyName());
|
||||
p.setPolicyType(req.policyType() == null ? "字段脱敏" : req.policyType());
|
||||
p.setScope(req.scope() == null ? "全局" : req.scope());
|
||||
p.setTriggerCondition(req.triggerCondition());
|
||||
p.setRuleDetail(req.ruleDetail());
|
||||
p.setAffectedFields(req.affectedFields());
|
||||
p.setPriority(req.priority() == null ? 5 : req.priority());
|
||||
p.setActionOnHit(req.actionOnHit() == null ? "记录日志" : req.actionOnHit());
|
||||
p.setStatus("草稿");
|
||||
p.setCreatedBy(req.createdBy());
|
||||
p.setEffectiveDate(req.effectiveDate() == null ? LocalDate.now().toString() : req.effectiveDate());
|
||||
p.setExpiryDate(req.expiryDate());
|
||||
p.setRemark(req.remark());
|
||||
p.setHitCount(0);
|
||||
p.setCreatedAt(now);
|
||||
p.setUpdatedAt(now);
|
||||
return ApiResp.ok(repo.save(p));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
public ApiResp<ItDlpPolicy> update(@PathVariable Long id, @RequestBody PolicyRequest req) {
|
||||
ItDlpPolicy p = require(id);
|
||||
if ("启用".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "启用中的策略不可直接编辑,请先停用");
|
||||
}
|
||||
if (req.policyName() != null && !req.policyName().isBlank()) p.setPolicyName(req.policyName());
|
||||
if (req.policyType() != null) p.setPolicyType(req.policyType());
|
||||
if (req.scope() != null) p.setScope(req.scope());
|
||||
if (req.triggerCondition() != null) p.setTriggerCondition(req.triggerCondition());
|
||||
if (req.ruleDetail() != null && !req.ruleDetail().isBlank()) p.setRuleDetail(req.ruleDetail());
|
||||
if (req.affectedFields() != null) p.setAffectedFields(req.affectedFields());
|
||||
if (req.priority() != null) p.setPriority(req.priority());
|
||||
if (req.actionOnHit() != null) p.setActionOnHit(req.actionOnHit());
|
||||
if (req.effectiveDate() != null) p.setEffectiveDate(req.effectiveDate());
|
||||
if (req.expiryDate() != null) p.setExpiryDate(req.expiryDate());
|
||||
if (req.remark() != null) p.setRemark(req.remark());
|
||||
p.setUpdatedAt(Instant.now());
|
||||
return ApiResp.ok(repo.save(p));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
ItDlpPolicy p = require(id);
|
||||
if ("启用".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "启用中的策略不可删除,请先停用");
|
||||
}
|
||||
repo.deleteById(id);
|
||||
return ApiResp.ok(null);
|
||||
}
|
||||
|
||||
// ---------- 状态机 ----------
|
||||
|
||||
public record EnableRequest(String approvedBy) {
|
||||
}
|
||||
|
||||
/** 启用策略:草稿/停用 → 启用(须指定 IT 主管审批人)。 */
|
||||
@Transactional
|
||||
@PostMapping("/{id}/enable")
|
||||
public ApiResp<ItDlpPolicy> enable(@PathVariable Long id, @RequestBody EnableRequest req) {
|
||||
ItDlpPolicy p = require(id);
|
||||
if ("启用".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "策略已是启用状态");
|
||||
}
|
||||
if (req.approvedBy() == null || req.approvedBy().isBlank()) {
|
||||
throw new ApiException(400, "启用须指定审批人(approvedBy)");
|
||||
}
|
||||
p.setApprovedBy(req.approvedBy());
|
||||
p.setStatus("启用");
|
||||
p.setUpdatedAt(Instant.now());
|
||||
return ApiResp.ok(repo.save(p));
|
||||
}
|
||||
|
||||
/** 停用策略:启用 → 停用。 */
|
||||
@PostMapping("/{id}/disable")
|
||||
public ApiResp<ItDlpPolicy> disable(@PathVariable Long id) {
|
||||
ItDlpPolicy p = require(id);
|
||||
if (!"启用".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "仅「启用」策略可停用,当前:" + p.getStatus());
|
||||
}
|
||||
p.setStatus("停用");
|
||||
p.setUpdatedAt(Instant.now());
|
||||
return ApiResp.ok(repo.save(p));
|
||||
}
|
||||
|
||||
/** 命中记录(内部回调):自增 hitCount、记录最近命中时刻(联动数据导出审批流程)。 */
|
||||
@Transactional
|
||||
@PostMapping("/{id}/hit")
|
||||
public ApiResp<ItDlpPolicy> hit(@PathVariable Long id) {
|
||||
ItDlpPolicy p = require(id);
|
||||
if (!"启用".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "仅「启用」策略可记录命中");
|
||||
}
|
||||
p.setHitCount((p.getHitCount() == null ? 0 : p.getHitCount()) + 1);
|
||||
p.setLastHitAt(Instant.now());
|
||||
p.setUpdatedAt(Instant.now());
|
||||
return ApiResp.ok(repo.save(p));
|
||||
}
|
||||
|
||||
// ---------- 统计看板 ----------
|
||||
|
||||
public record PolicyStats(
|
||||
int total, int active, int draft, int disabled,
|
||||
Map<String, Integer> byType, Map<String, Integer> byScope,
|
||||
List<ItDlpPolicy> topHit) {
|
||||
}
|
||||
|
||||
/** DLP 策略统计看板:总数/启用/草稿/停用、按类型/范围分布、命中次数 TOP5。 */
|
||||
@GetMapping("/stats")
|
||||
public ApiResp<PolicyStats> stats() {
|
||||
List<ItDlpPolicy> all = repo.findAll();
|
||||
int active = 0, draft = 0, disabled = 0;
|
||||
Map<String, Integer> byType = new LinkedHashMap<>();
|
||||
Map<String, Integer> byScope = new LinkedHashMap<>();
|
||||
|
||||
for (ItDlpPolicy p : all) {
|
||||
switch (p.getStatus() == null ? "草稿" : p.getStatus()) {
|
||||
case "启用" -> active++;
|
||||
case "草稿" -> draft++;
|
||||
case "停用" -> disabled++;
|
||||
}
|
||||
if (p.getPolicyType() != null) {
|
||||
byType.merge(p.getPolicyType(), 1, Integer::sum);
|
||||
}
|
||||
if (p.getScope() != null) {
|
||||
byScope.merge(p.getScope(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
|
||||
// 命中 TOP5(按 hitCount 降序)
|
||||
List<ItDlpPolicy> topHit = all.stream()
|
||||
.filter(p -> p.getHitCount() != null && p.getHitCount() > 0)
|
||||
.sorted((a, b) -> Integer.compare(
|
||||
b.getHitCount() == null ? 0 : b.getHitCount(),
|
||||
a.getHitCount() == null ? 0 : a.getHitCount()))
|
||||
.limit(5)
|
||||
.toList();
|
||||
|
||||
return ApiResp.ok(new PolicyStats(all.size(), active, draft, disabled, byType, byScope, topHit));
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private ItDlpPolicy require(Long id) {
|
||||
return repo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("DLP policy not found: " + id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user