package com.vincent.rsf.httpaudit.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.vincent.rsf.httpaudit.entity.HttpAuditLog; import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; import com.vincent.rsf.httpaudit.props.HttpAuditProperties; import lombok.extern.slf4j.Slf4j; import org.opensearch.client.Request; import org.opensearch.client.ResponseException; import org.opensearch.client.RestClient; import org.springframework.scheduling.annotation.Scheduled; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Date; /** * 审计日志定期清理 */ @Slf4j public class HttpAuditCleanupService { private final HttpAuditLogMapper httpAuditLogMapper; private final HttpAuditProperties props; private final RestClient openSearchRestClient; private final ObjectMapper objectMapper = new ObjectMapper(); public HttpAuditCleanupService(HttpAuditLogMapper httpAuditLogMapper, HttpAuditProperties props, RestClient openSearchRestClient) { this.httpAuditLogMapper = httpAuditLogMapper; this.props = props; this.openSearchRestClient = openSearchRestClient; } @Scheduled(cron = "${http-audit.cleanup-cron:0 30 2 * * ?}") public void cleanup() { if (!props.isCleanupEnabled()) { return; } int retentionDays = props.getCleanupRetentionDays(); if (retentionDays <= 0) { log.warn("http-audit 清理已跳过,retentionDays 配置无效:{}", retentionDays); return; } LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays); Date cutoffDate = Date.from(cutoff.atZone(ZoneId.systemDefault()).toInstant()); if (props.usesMysqlLogStorage()) { cleanupMysql(cutoffDate, retentionDays); } if (props.usesOpenSearchLogStorage() && openSearchRestClient != null) { cleanupOpenSearch(cutoffDate, retentionDays); } } private void cleanupMysql(Date cutoffDate, int retentionDays) { try { int count = httpAuditLogMapper.delete(new LambdaQueryWrapper() .lt(HttpAuditLog::getCreateTime, cutoffDate)); if (count > 0) { log.info("http-audit MySQL 清理完成,删除 {} 条,保留天数 {}", count, retentionDays); } } catch (Exception e) { log.warn("http-audit MySQL 清理失败", e); } } private void cleanupOpenSearch(Date cutoffDate, int retentionDays) { try { String index = props.getOpenSearch().getIndexName(); String cutoffIso = cutoffDate.toInstant().atOffset(ZoneOffset.UTC).toString(); ObjectNode body = objectMapper.createObjectNode(); ObjectNode query = objectMapper.createObjectNode(); ObjectNode range = objectMapper.createObjectNode(); range.set("create_time", objectMapper.createObjectNode().put("lt", cutoffIso)); query.set("range", range); body.set("query", query); Request request = new Request("POST", "/" + index + "/_delete_by_query?refresh=false&conflicts=proceed"); request.setJsonEntity(objectMapper.writeValueAsString(body)); openSearchRestClient.performRequest(request); log.debug("http-audit OpenSearch delete_by_query 已提交 retentionDays={}", retentionDays); } catch (ResponseException ex) { int code = ex.getResponse() != null ? ex.getResponse().getStatusLine().getStatusCode() : -1; if (code == 404) { log.debug("http-audit OpenSearch 清理跳过,索引不存在"); } else { log.warn("http-audit OpenSearch 清理失败 status={}", code, ex); } } catch (Exception e) { log.warn("http-audit OpenSearch 清理失败", e); } } }