package com.example.hrlab.audit; import com.example.hrlab.common.Correlation; import com.example.hrlab.common.JsonUtils; import com.example.hrlab.security.SecurityUtils; import jakarta.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Clock; import java.time.OffsetDateTime; import java.util.HexFormat; import java.util.Map; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @Service public class AuditService { private final JdbcTemplate jdbcTemplate; private final Clock clock; public AuditService(JdbcTemplate jdbcTemplate, Clock clock) { this.jdbcTemplate = jdbcTemplate; this.clock = clock; } public void record(String action, String entityType, Object entityId, Object before, Object after) { String beforeJson = JsonUtils.toJson(before == null ? Map.of() : before); String afterJson = JsonUtils.toJson(after == null ? Map.of() : after); String prevHash = jdbcTemplate.query("SELECT record_hash FROM audit_logs ORDER BY id DESC LIMIT 1", rs -> rs.next() ? rs.getString(1) : null); OffsetDateTime occurredAt = OffsetDateTime.now(clock); String correlationId = Correlation.currentId(); String recordHash = hash(prevHash, action, entityType, String.valueOf(entityId), beforeJson, afterJson, correlationId, occurredAt.toString()); RequestInfo requestInfo = requestInfo(); jdbcTemplate.update(""" INSERT INTO audit_logs(actor_user_id, action, entity_type, entity_id, before_json, after_json, ip_addr, user_agent, correlation_id, prev_hash, record_hash, occurred_at) VALUES (?, ?, ?, ?, ?::jsonb, ?::jsonb, ?, ?, ?, ?, ?, ?) """, SecurityUtils.currentUserIdOrNull(), action, entityType, entityId == null ? null : String.valueOf(entityId), beforeJson, afterJson, requestInfo.ip(), requestInfo.userAgent(), correlationId, prevHash, recordHash, occurredAt); } public String hash(String prevHash, String action, String entityType, String entityId, String beforeJson, String afterJson, String correlationId, String occurredAt) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); String material = String.join("|", prevHash == null ? "" : prevHash, action, entityType, entityId == null ? "" : entityId, beforeJson, afterJson, correlationId, occurredAt); return HexFormat.of().formatHex(digest.digest(material.getBytes(StandardCharsets.UTF_8))); } catch (Exception ex) { throw new IllegalStateException("审计哈希计算失败", ex); } } private RequestInfo requestInfo() { if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attrs) { HttpServletRequest request = attrs.getRequest(); String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isBlank()) { ip = request.getRemoteAddr(); } return new RequestInfo(ip, request.getHeader("User-Agent")); } return new RequestInfo(null, null); } private record RequestInfo(String ip, String userAgent) { } }