longz
2026-07-13 5882ba8863d77fdcc168b176c3ffb30e8a20556a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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;
 
/**
 * 统一记录控制器写操作日志。
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class OpLogAuditAspect {
 
    private final OpLogApplicationService opLogApplicationService;
    private final ObjectMapper objectMapper;
 
    @Pointcut("within(com.sinata.appointment.controller..*)")
    public void controllerWriteOperation() {}
 
    @AfterReturning(pointcut = "controllerWriteOperation()", returning = "ret")
    public void afterWrite(JoinPoint jp, Object ret) {
        try {
            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(
                    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("clientIp", resolveClientIp(request));
        payload.put("userAgent", request.getHeader("User-Agent"));
        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 String resolveClientIp(HttpServletRequest request) {
        String[] candidateHeaders = {
                "X-Forwarded-For",
                "X-Real-IP",
                "Proxy-Client-IP",
                "WL-Proxy-Client-IP"
        };
        for (String header : candidateHeaders) {
            String value = request.getHeader(header);
            if (!StringUtils.hasText(value)) {
                continue;
            }
            String first = value.split(",")[0].trim();
            if (StringUtils.hasText(first) && !"unknown".equalsIgnoreCase(first)) {
                return first;
            }
        }
        return request.getRemoteAddr();
    }
 
    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;
    }
}