日志记录提交

This commit is contained in:
2026-08-05 14:20:04 +08:00
parent 264458920b
commit 06f7f9d2f2
83 changed files with 3256 additions and 27 deletions
@@ -0,0 +1,47 @@
package com.baiyue.audit.service;
import com.baiyue.audit.dto.AuditIngestRequest;
import com.baiyue.audit.repository.AuditDeadLetterRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/** 定时重放已持久化的审计死信,达到上限后保留为 DEAD 供人工处理。 */
@Component
@ConditionalOnProperty(prefix = "baiyue.audit.rocketmq", name = "dead-letter-compensation-enabled", havingValue = "true")
public class AuditDeadLetterCompensator {
@Resource
private AuditDeadLetterRepository repository;
@Resource
private AuditIngestService service;
@Resource
private ObjectMapper json;
@Value("${spring.application.name:audit-service}-${random.uuid}")
private String owner;
@Value("${baiyue.audit.rocketmq.dead-letter-max-attempts:5}")
private int maxAttempts;
@Scheduled(fixedDelayString = "${baiyue.audit.rocketmq.dead-letter-poll-ms:10000}")
public void compensate() {
repository.releaseExpired();
for (Long id : repository.pendingIds(50)) {
if (!repository.claim(id, owner, 60)) continue;
AuditDeadLetterRepository.DeadRow row = repository.find(id);
if (row == null) continue;
try {
service.ingest(json.readValue(row.payload(), AuditIngestRequest.class));
repository.processed(id, owner);
} catch (Exception exception) {
repository.failed(id, owner, maxAttempts, safe(exception.getMessage()));
}
}
}
private static String safe(String value) {
String message = value == null ? "死信补偿失败" : value;
return message.length() <= 1000 ? message : message.substring(0, 1000);
}
}
@@ -5,6 +5,9 @@ import com.baiyue.audit.dto.AuditIngestRequest;
import com.baiyue.audit.repository.AuditEventRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.UUID;
/**
* 审计事件写入应用服务,在事务内接收并持久化不可变业务审计记录。
@@ -16,7 +19,8 @@ public class AuditIngestService {
@Transactional
public void ingest(AuditIngestRequest event) {
repository.save(event);
public boolean ingest(AuditIngestRequest event) {
String eventId = StringUtils.hasText(event.eventId()) ? event.eventId() : UUID.randomUUID().toString();
return repository.save(event, eventId);
}
}
@@ -0,0 +1,48 @@
package com.baiyue.audit.service;
import com.baiyue.audit.dto.AuditLogQuery;
import com.baiyue.audit.dto.AuditLogResponses;
import com.baiyue.audit.entity.AuditLogEntity;
import com.baiyue.audit.repository.AuditEventRepository;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/** 平台统一操作日志查询服务。 */
@Service
public class AuditLogQueryService {
@Resource
private AuditEventRepository repository;
public PageResult<AuditLogResponses.ListItem> page(AuditLogQuery query, long page, long size) {
PageResult<AuditLogEntity> source = repository.page(query, page, size);
return new PageResult<>(source.records().stream().map(this::toListItem).toList(),
source.page(), source.size(), source.total(), source.pages());
}
public AuditLogResponses.Detail detail(Long id) {
AuditLogEntity entity = repository.findById(id);
if (entity == null) {
throw new BizException(ResultCode.NOT_FOUND, "操作日志不存在");
}
return new AuditLogResponses.Detail(
entity.getId(), entity.getEventId(), entity.getEventType(), entity.getResourceType(), entity.getResourceId(),
entity.getDescription(), entity.getRequestId(), entity.getTraceId(), entity.getTenantId(),
entity.getOperatorTenantId(), entity.getUserId(), entity.getUsername(), entity.getAppCode(),
entity.getClientType(), entity.getHttpMethod(), entity.getRequestUri(), entity.getClientIp(),
entity.getDurationMs(), entity.getSuccess(), entity.getErrorCode(), entity.getErrorMessage(),
entity.getBeforeData(), entity.getAfterData(), entity.getServiceName(), entity.getOccurredAt(),
entity.getCreatedAt());
}
private AuditLogResponses.ListItem toListItem(AuditLogEntity entity) {
return new AuditLogResponses.ListItem(
entity.getId(), entity.getEventId(), entity.getEventType(), entity.getResourceType(), entity.getResourceId(),
entity.getDescription(), entity.getTenantId(), entity.getOperatorTenantId(), entity.getUserId(),
entity.getUsername(), entity.getAppCode(), entity.getClientType(), entity.getRequestId(),
entity.getTraceId(), entity.getServiceName(), entity.getSuccess(), entity.getErrorCode(),
entity.getErrorMessage(), entity.getOccurredAt(), entity.getCreatedAt());
}
}
@@ -0,0 +1,20 @@
package com.baiyue.audit.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
/** 审计日志只允许平台端账号读取,避免商户端通过通用日志接口获取跨账号记录。 */
public final class AuditPlatformScopeGuard {
private AuditPlatformScopeGuard() {
}
public static void requirePlatform(AuthenticatedUser user) {
if (user == null || user.userId() == null) {
throw new BizException(ResultCode.UNAUTHORIZED, "请先登录");
}
if (!"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅平台端可查看操作日志");
}
}
}