Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/ItDlpPolicyController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 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>
2026-06-15 19:19:15 +08:00

253 lines
10 KiB
Java

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));
}
}