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.ItAccountAuditRecord;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.repository.ItAccountAuditRecordRepository;
import com.kaidi.oa.repository.SysUserRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 信息部·账号权限审计 + HR 联动(需求 §4 权限与账号审计 / §6 HR联动)。
*
*
两大能力:
*
* - 手动审计:IT 人员定期对系统账号逐条核查,填写审计结论(通过/异常),
* 形成账号权限定期审计报告({@code GET /report})。
* - HR 联动触发:员工离职/入职/岗位变动由 HR 系统或 IT 人员调用
* {@code POST /hr-trigger} ,自动 enable/disable SysUser 账号,并留下审计记录
* (triggerSource=HR离职/HR入职/HR岗位变动)。这就是"离职→系统账号自动disable"
* 的跨模块联动落地,而非前端只有一个静态字段。
*
*
*
*
* - {@code POST /hr-trigger} 触发 HR 联动(enable/disable/adjust 账号);
* - {@code POST /manual-audit} 手动审计一条账号记录;
* - {@code GET /report} 账号权限审计报告(按日期/来源汇总,附全账号状态快照);
* - {@code GET /logs} 全部审计/联动日志;
*
*
* 写口含账号信息,默认受 default-deny(ADMIN/APPROVER) 保护;读侧含账号状态,登记 SENSITIVE_READ_PREFIXES。
*/
@RestController
@RequestMapping("/api/oa/it-account-audit")
public class ItAccountAuditController {
private final ItAccountAuditRecordRepository auditRepo;
private final SysUserRepository userRepo;
public ItAccountAuditController(ItAccountAuditRecordRepository auditRepo,
SysUserRepository userRepo) {
this.auditRepo = auditRepo;
this.userRepo = userRepo;
}
// ---------- HR 联动触发 ----------
public record HrTriggerRequest(
Long userId, String loginName,
String hrEventType, String hrEventDesc,
String operator, String note) {
}
/**
* HR 联动触发:入职/离职/岗位变动自动操作系统账号,并写审计日志。
* hrEventType:HR入职(→ 启用账号)/ HR离职(→ 禁用账号)/ HR岗位变动(审计记录岗位调整)。
* userId 或 loginName 至少提供一个来定位账号。
*/
@Transactional
@PostMapping("/hr-trigger")
public ApiResp hrTrigger(@RequestBody HrTriggerRequest req) {
if (req.hrEventType() == null || req.hrEventType().isBlank()) {
throw new ApiException(400, "hrEventType 不能为空(HR入职/HR离职/HR岗位变动)");
}
SysUser user = resolveUser(req.userId(), req.loginName());
boolean beforeEnabled = user.isEnabled();
String beforeState = beforeEnabled ? "已启用" : "已禁用";
String action;
String afterState;
switch (req.hrEventType()) {
case "HR离职" -> {
user.setEnabled(false);
action = "禁用账号";
afterState = "已禁用";
}
case "HR入职" -> {
user.setEnabled(true);
action = "启用账号";
afterState = "已启用";
}
case "HR岗位变动" -> {
// 岗位变动不直接改 enabled,仅留审计记录供 IT 人员人工核查权限是否合理。
action = "调整权限";
afterState = beforeState;
}
default -> throw new ApiException(400, "不支持的 hrEventType:" + req.hrEventType()
+ "(HR入职/HR离职/HR岗位变动)");
}
userRepo.save(user);
ItAccountAuditRecord rec = new ItAccountAuditRecord();
rec.setTargetUserId(user.getId());
rec.setTargetUserName(user.getDisplayName());
rec.setTargetLoginName(user.getLoginName());
rec.setTriggerSource(req.hrEventType());
rec.setHrEventDesc(req.hrEventDesc());
rec.setAction(action);
rec.setBeforeState(beforeState);
rec.setAfterState(afterState);
rec.setOperator(req.operator() == null ? "系统自动" : req.operator());
rec.setAuditDate(LocalDate.now().toString());
rec.setNote(req.note());
rec.setCreatedAt(Instant.now());
return ApiResp.ok(auditRepo.save(rec));
}
// ---------- 手动审计 ----------
public record ManualAuditRequest(
Long userId, String loginName,
String action, String auditConclusion,
String beforeRoles, String afterRoles,
String operator, String note) {
}
/**
* 手动账号权限审计:IT 人员对指定账号做人工核查,记录审计结论。
* action:审查通过 / 审查异常 / 注销账号 / 禁用账号 / 启用账号 / 调整权限。
*/
@Transactional
@PostMapping("/manual-audit")
public ApiResp manualAudit(@RequestBody ManualAuditRequest req) {
if (req.action() == null || req.action().isBlank()) {
throw new ApiException(400, "审计动作(action) 不能为空");
}
SysUser user = resolveUser(req.userId(), req.loginName());
String beforeState = user.isEnabled() ? "已启用" : "已禁用";
String afterState = beforeState;
// 部分动作直接操作账号
if ("注销账号".equals(req.action()) || "禁用账号".equals(req.action())) {
user.setEnabled(false);
afterState = "已禁用";
userRepo.save(user);
} else if ("启用账号".equals(req.action())) {
user.setEnabled(true);
afterState = "已启用";
userRepo.save(user);
}
ItAccountAuditRecord rec = new ItAccountAuditRecord();
rec.setTargetUserId(user.getId());
rec.setTargetUserName(user.getDisplayName());
rec.setTargetLoginName(user.getLoginName());
rec.setTriggerSource("手动审计");
rec.setAction(req.action());
rec.setBeforeState(beforeState);
rec.setAfterState(afterState);
rec.setBeforeRoles(req.beforeRoles());
rec.setAfterRoles(req.afterRoles());
rec.setOperator(req.operator());
rec.setAuditConclusion(req.auditConclusion());
rec.setAuditDate(LocalDate.now().toString());
rec.setNote(req.note());
rec.setCreatedAt(Instant.now());
return ApiResp.ok(auditRepo.save(rec));
}
// ---------- 审计报告 + 日志 ----------
@GetMapping("/logs")
public ApiResp> logs(
@RequestParam(required = false) String triggerSource,
@RequestParam(required = false) String action) {
if (triggerSource != null && !triggerSource.isBlank()) {
return ApiResp.ok(auditRepo.findByTriggerSource(triggerSource));
}
if (action != null && !action.isBlank()) {
return ApiResp.ok(auditRepo.findByAction(action));
}
return ApiResp.ok(auditRepo.findAll());
}
public record AccountSnapshot(Long userId, String displayName, String loginName,
boolean enabled, String title) {
}
public record AuditReport(String generatedAt, int totalUsers, int enabledCount, int disabledCount,
int hrTriggerCount, int manualAuditCount, int anomalyCount,
List disabledAccounts,
Map actionDistribution,
List recentLogs) {
}
/**
* 账号权限审计报告(IT 治理合规核心):
* 全量系统账号状态快照(已禁用账号清单)、HR联动次数、手动审计次数、
* 异常数、动作分布、近期审计日志。
*/
@GetMapping("/report")
public ApiResp report() {
List allUsers = userRepo.findAll();
int enabled = 0, disabled = 0;
List disabledList = new ArrayList<>();
for (SysUser u : allUsers) {
if (u.isEnabled()) {
enabled++;
} else {
disabled++;
disabledList.add(new AccountSnapshot(u.getId(), u.getDisplayName(),
u.getLoginName(), false, u.getTitle()));
}
}
List allLogs = auditRepo.findAll();
int hrCount = 0, manualCount = 0, anomalyCount = 0;
Map actionDist = new LinkedHashMap<>();
for (ItAccountAuditRecord r : allLogs) {
if ("手动审计".equals(r.getTriggerSource())) {
manualCount++;
} else {
hrCount++;
}
if ("审查异常".equals(r.getAction())) {
anomalyCount++;
}
if (r.getAction() != null) {
actionDist.merge(r.getAction(), 1, Integer::sum);
}
}
// 取最近 20 条日志(按 createdAt 倒排)
List sorted = allLogs.stream()
.sorted((a, b) -> b.getCreatedAt() != null && a.getCreatedAt() != null
? b.getCreatedAt().compareTo(a.getCreatedAt()) : 0)
.limit(20).toList();
return ApiResp.ok(new AuditReport(
Instant.now().toString(),
allUsers.size(), enabled, disabled,
hrCount, manualCount, anomalyCount,
disabledList, actionDist, sorted));
}
// ---------- helpers ----------
private SysUser resolveUser(Long userId, String loginName) {
if (userId != null) {
return userRepo.findById(userId)
.orElseThrow(() -> new NotFoundException("系统账号不存在,userId=" + userId));
}
if (loginName != null && !loginName.isBlank()) {
return userRepo.findByLoginName(loginName)
.orElseThrow(() -> new NotFoundException("系统账号不存在,loginName=" + loginName));
}
throw new ApiException(400, "必须提供 userId 或 loginName 来定位账号");
}
}