日志记录提交
This commit is contained in:
@@ -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, "仅平台端可查看操作日志");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user