| | |
| | | package com.sinata.appointment.audit; |
| | | |
| | | import cn.dev33.satoken.stp.StpUtil; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.sinata.appointment.config.MiniStpUtil; |
| | | import com.sinata.appointment.service.OpLogApplicationService; |
| | | import jakarta.servlet.http.HttpServletRequest; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.JoinPoint; |
| | | import org.aspectj.lang.reflect.MethodSignature; |
| | | import org.aspectj.lang.annotation.AfterReturning; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | import org.aspectj.lang.annotation.Pointcut; |
| | | import java.lang.reflect.Method; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.context.request.RequestAttributes; |
| | | import org.springframework.web.context.request.RequestContextHolder; |
| | | import org.springframework.web.context.request.ServletRequestAttributes; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | /** |
| | | * 配置类写操作审计(示例):复杂场景可在 ApplicationService 内显式记录。 |
| | | * 统一记录控制器写操作日志。 |
| | | */ |
| | | @Slf4j |
| | | @Aspect |
| | |
| | | public class OpLogAuditAspect { |
| | | |
| | | private final OpLogApplicationService opLogApplicationService; |
| | | private final ObjectMapper objectMapper; |
| | | |
| | | @AfterReturning( |
| | | pointcut = |
| | | "execution(* com.sinata.appointment.service.ConfigApplicationService.save*(..))", |
| | | returning = "ret") |
| | | public void afterConfigSave(JoinPoint jp, Object ret) { |
| | | @Pointcut("within(com.sinata.appointment.controller..*)") |
| | | public void controllerWriteOperation() {} |
| | | |
| | | @AfterReturning(pointcut = "controllerWriteOperation()", returning = "ret") |
| | | public void afterWrite(JoinPoint jp, Object ret) { |
| | | try { |
| | | Long op = StpUtil.isLogin() ? StpUtil.getLoginIdAsLong() : null; |
| | | HttpServletRequest request = currentRequest(); |
| | | if (request == null) { |
| | | return; |
| | | } |
| | | if (!isWriteMethod(request.getMethod())) { |
| | | return; |
| | | } |
| | | String uri = request.getRequestURI(); |
| | | String sourceType = resolveSourceType(uri); |
| | | if (!StringUtils.hasText(sourceType)) { |
| | | return; |
| | | } |
| | | Long operatorUserId = resolveOperatorUserId(sourceType); |
| | | opLogApplicationService.recordAsync( |
| | | op, |
| | | "CONFIG", |
| | | jp.getSignature().getName(), |
| | | jp.getArgs().length > 0 ? jp.getArgs()[0].getClass().getSimpleName() : "UNKNOWN", |
| | | "", |
| | | "{}"); |
| | | sourceType, |
| | | operatorUserId, |
| | | resolveModule(jp, uri), |
| | | resolveAction(jp), |
| | | jp.getTarget().getClass().getSimpleName(), |
| | | uri, |
| | | buildDetailJson(request, jp)); |
| | | } catch (Exception e) { |
| | | log.debug("audit skip: {}", e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private boolean isWriteMethod(String method) { |
| | | if (!StringUtils.hasText(method)) { |
| | | return false; |
| | | } |
| | | return "POST".equalsIgnoreCase(method) |
| | | || "PUT".equalsIgnoreCase(method) |
| | | || "PATCH".equalsIgnoreCase(method) |
| | | || "DELETE".equalsIgnoreCase(method); |
| | | } |
| | | |
| | | private HttpServletRequest currentRequest() { |
| | | RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); |
| | | if (attributes instanceof ServletRequestAttributes servletRequestAttributes) { |
| | | return servletRequestAttributes.getRequest(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String resolveSourceType(String uri) { |
| | | if (!StringUtils.hasText(uri)) { |
| | | return null; |
| | | } |
| | | if (uri.startsWith("/api/mini/")) { |
| | | return OpLogApplicationService.SOURCE_MINI_EMPLOYEE; |
| | | } |
| | | if (uri.startsWith("/api/")) { |
| | | return OpLogApplicationService.SOURCE_ADMIN; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Long resolveOperatorUserId(String sourceType) { |
| | | if (OpLogApplicationService.SOURCE_MINI_EMPLOYEE.equals(sourceType)) { |
| | | return MiniStpUtil.isLogin() ? MiniStpUtil.getLoginIdAsLong() : null; |
| | | } |
| | | return StpUtil.isLogin() ? StpUtil.getLoginIdAsLong() : null; |
| | | } |
| | | |
| | | private String resolveModule(JoinPoint jp, String uri) { |
| | | Tag tag = jp.getTarget().getClass().getAnnotation(Tag.class); |
| | | if (tag != null && StringUtils.hasText(tag.name())) { |
| | | return tag.name().trim(); |
| | | } |
| | | if (!StringUtils.hasText(uri)) { |
| | | return "UNKNOWN"; |
| | | } |
| | | String trimmed = uri.startsWith("/api/") ? uri.substring(5) : uri; |
| | | String[] segments = trimmed.split("/"); |
| | | if (segments.length >= 2 && "mini".equalsIgnoreCase(segments[0])) { |
| | | return segments[1].toUpperCase(); |
| | | } |
| | | return segments.length > 0 ? segments[0].toUpperCase() : "UNKNOWN"; |
| | | } |
| | | |
| | | private String resolveAction(JoinPoint jp) { |
| | | if (jp.getSignature() instanceof MethodSignature methodSignature) { |
| | | Method method = methodSignature.getMethod(); |
| | | Operation operation = method.getAnnotation(Operation.class); |
| | | if (operation != null && StringUtils.hasText(operation.summary())) { |
| | | return operation.summary().trim(); |
| | | } |
| | | } |
| | | return jp.getSignature().getName(); |
| | | } |
| | | |
| | | private String buildDetailJson(HttpServletRequest request, JoinPoint jp) { |
| | | Map<String, Object> payload = new LinkedHashMap<>(); |
| | | payload.put("method", request.getMethod()); |
| | | payload.put("uri", request.getRequestURI()); |
| | | payload.put("handler", jp.getSignature().toShortString()); |
| | | payload.put("args", summarizeArgs(jp.getArgs())); |
| | | try { |
| | | return objectMapper.writeValueAsString(payload); |
| | | } catch (Exception ex) { |
| | | return "{\"uri\":\"" + request.getRequestURI() + "\"}"; |
| | | } |
| | | } |
| | | |
| | | private Object[] summarizeArgs(Object[] args) { |
| | | if (args == null || args.length == 0) { |
| | | return new Object[0]; |
| | | } |
| | | Object[] summarized = new Object[args.length]; |
| | | for (int i = 0; i < args.length; i++) { |
| | | Object arg = args[i]; |
| | | if (arg == null) { |
| | | summarized[i] = null; |
| | | continue; |
| | | } |
| | | if (arg instanceof String || arg instanceof Number || arg instanceof Boolean) { |
| | | String value = String.valueOf(arg); |
| | | summarized[i] = |
| | | value.length() > 60 |
| | | ? value.substring(0, 60) + "..." |
| | | : value; |
| | | continue; |
| | | } |
| | | summarized[i] = arg.getClass().getSimpleName(); |
| | | } |
| | | return summarized; |
| | | } |
| | | } |