refactor: 💥 适配 ContiNew Starter Log(日志模块)

1.continew-starter 1.0.1-SNAPSHOT => 1.1.0-SNAPSHOT
2.日志表结构及相关管理 UI 变更
This commit is contained in:
2023-12-17 14:07:44 +08:00
parent 349899b4fc
commit 9bf015059b
35 changed files with 308 additions and 893 deletions

View File

@@ -16,6 +16,12 @@
<description>系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)</description>
<dependencies>
<!-- ContiNew Starter 日志模块 - HttpTraceProSpring Boot Actuator HttpTrace 定制增强版) -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log-httptrace-pro</artifactId>
</dependency>
<!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) -->
<dependency>
<groupId>top.charles7c.continew</groupId>

View File

@@ -1,57 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.admin.monitor.annotation;
import java.lang.annotation.*;
/**
* 系统日志注解(用于接口方法或类上,辅助 Spring Doc OpenAPI3 使用效果最佳)
*
* @author Charles7c
* @since 2022/12/23 20:00
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
/**
* 日志描述(仅用于接口方法上)
* <p>
* 读取顺序:(越靠后优先级越高)<br>
* 1、读取对应接口方法上的 @Operation(summary="描述") 内容<br>
* 2、读取对应接口方法上的 @Log("描述") 内容<br>
* </p>
*/
String value() default "";
/**
* 所属模块(用于接口方法或类上)
* <p>
* 读取顺序:(越靠后优先级越高)<br>
* 1、读取对应接口类上的 @Tag(name = "模块") 内容<br>
* 2、读取对应接口类上的 @Log(module = "模块") 内容<br>
* 3、读取对应接口方法上的 @Log(module = "模块") 内容
* </p>
*/
String module() default "";
/**
* 是否忽略日志记录(用于接口方法或类上)
*/
boolean ignore() default false;
}

View File

@@ -16,30 +16,29 @@
package top.charles7c.continew.admin.monitor.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.continew.admin.monitor.interceptor.LogInterceptor;
import top.charles7c.continew.admin.monitor.mapper.LogMapper;
import top.charles7c.continew.admin.system.service.UserService;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.httptracepro.autoconfigure.ConditionalOnEnabledLog;
/**
* 监控模块 Web MVC 配置
* 日志配置
*
* @author Charles7c
* @since 2022/12/24 23:15
*/
@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebMvcMonitorConfiguration implements WebMvcConfigurer {
@ConditionalOnEnabledLog
public class LogConfiguration {
private final LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
/**
* 日志持久层接口本地实现类
*/
@Bean
public LogDao logDao(UserService userService, LogMapper logMapper) {
return new LogDaoLocalImpl(userService, logMapper);
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.admin.monitor.config;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.scheduling.annotation.Async;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import top.charles7c.continew.admin.auth.model.req.AccountLoginReq;
import top.charles7c.continew.admin.common.constant.SysConstants;
import top.charles7c.continew.admin.monitor.enums.LogStatusEnum;
import top.charles7c.continew.admin.monitor.mapper.LogMapper;
import top.charles7c.continew.admin.monitor.model.entity.LogDO;
import top.charles7c.continew.admin.system.service.UserService;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import top.charles7c.continew.starter.log.common.model.LogRequest;
import top.charles7c.continew.starter.log.common.model.LogResponse;
/**
* 日志持久层接口本地实现类
*
* @author Charles7c
* @since 2023/12/16 23:55
*/
@RequiredArgsConstructor
public class LogDaoLocalImpl implements LogDao {
private final UserService userService;
private final LogMapper logMapper;
@Async
@Override
public void add(LogRecord logRecord) {
LogDO logDO = new LogDO();
logDO.setDescription(logRecord.getDescription());
String module = logRecord.getModule();
logDO.setModule(
StrUtil.isNotBlank(module) ? logRecord.getModule().replace("API", StringConstants.EMPTY).trim() : null);
logDO.setCreateTime(LocalDateTime.ofInstant(logRecord.getTimestamp(), ZoneId.systemDefault()));
logDO.setTimeTaken(logRecord.getTimeTaken().toMillis());
// 请求信息
LogRequest logRequest = logRecord.getRequest();
logDO.setRequestMethod(logRequest.getMethod());
String requestUrl = logRequest.getUri().toString();
logDO.setRequestUrl(requestUrl);
Map<String, List<String>> requestHeaders = logRequest.getHeaders();
logDO.setRequestHeaders(JSONUtil.toJsonStr(requestHeaders));
String requestBody = logRequest.getBody();
logDO.setRequestBody(requestBody);
logDO.setIp(logRequest.getIp());
logDO.setAddress(logRequest.getAddress());
logDO.setBrowser(logRequest.getBrowser());
logDO.setOs(StrUtil.subBefore(logRequest.getOs(), " or", false));
// 响应信息
LogResponse logResponse = logRecord.getResponse();
Integer statusCode = logResponse.getStatus();
logDO.setStatusCode(statusCode);
logDO.setResponseHeaders(JSONUtil.toJsonStr(logResponse.getHeaders()));
String responseBody = logResponse.getBody();
logDO.setResponseBody(responseBody);
// 状态
logDO.setStatus(statusCode >= HttpStatus.HTTP_BAD_REQUEST ? LogStatusEnum.FAILURE : LogStatusEnum.SUCCESS);
if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) {
R result = JSONUtil.toBean(responseBody, R.class);
if (!result.isSuccess()) {
logDO.setStatus(LogStatusEnum.FAILURE);
logDO.setErrorMsg(result.getMsg());
}
// 操作人
if (StrUtil.contains(requestUrl, SysConstants.LOGOUT_URI)) {
Long loginId = Convert.toLong(result.getData(), -1L);
logDO.setCreateUser(-1 != loginId ? loginId : null);
}
}
// 操作人
if (StrUtil.contains(requestUrl, SysConstants.LOGIN_URI)) {
AccountLoginReq loginReq = JSONUtil.toBean(requestBody, AccountLoginReq.class);
logDO.setCreateUser(
ExceptionUtils.exToNull(() -> userService.getByUsername(loginReq.getUsername()).getId()));
} else if (!StrUtil.contains(requestUrl, SysConstants.LOGOUT_URI) && MapUtil.isNotEmpty(requestHeaders)
&& requestHeaders.containsKey(HttpHeaders.AUTHORIZATION)) {
String authorization = requestHeaders.get(HttpHeaders.AUTHORIZATION).get(0);
String token = authorization.replace(SaManager.getConfig().getTokenPrefix() + StringConstants.SPACE,
StringConstants.EMPTY);
logDO.setCreateUser(Convert.toLong(StpUtil.getLoginIdByToken(token)));
}
logMapper.insert(logDO);
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.admin.monitor.config.properties;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 系统日志配置属性
*
* @author Charles7c
* @since 2022/12/24 23:04
*/
@Data
@Component
@ConfigurationProperties(prefix = "logging.system")
public class LogProperties {
/**
* 是否启用系统日志
*/
private Boolean enabled;
/**
* 是否记录内网 IP 操作
*/
private Boolean includeInnerIp;
/**
* 排除请求方式(哪些请求方式不记录系统日志)
*/
private List<String> excludeMethods = new ArrayList<>();
/**
* 脱敏字段
*/
private List<String> desensitizeFields = new ArrayList<>();
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.admin.monitor.filter;
import java.io.IOException;
import java.util.Objects;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
/**
* 系统日志过滤器(缓存请求和响应体过滤器)
*
* <p>
* 由于 requestBody 和 responseBody 分别对应的是 InputStream 和 OutputStream由于流的特性读取完之后就无法再被使用了。 所以,需要额外缓存一次流信息。
* </p>
*
* @author Charles7c
* @since 2022/12/24 21:16
*/
@Component
public class LogFilter extends OncePerRequestFilter implements Ordered {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
// 包装流,可重复读取
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(request, response);
updateResponse(response);
}
/**
* 更新响应(不操作这一步,会导致接口响应空白)
*
* @param response
* 响应对象
* @throws IOException
* /
*/
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
}
}

View File

@@ -1,380 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.admin.monitor.interceptor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import top.charles7c.continew.admin.auth.model.req.AccountLoginReq;
import top.charles7c.continew.admin.common.constant.SysConstants;
import top.charles7c.continew.admin.common.model.dto.LogContext;
import top.charles7c.continew.admin.common.util.helper.LoginHelper;
import top.charles7c.continew.admin.common.util.holder.LogContextHolder;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.admin.monitor.config.properties.LogProperties;
import top.charles7c.continew.admin.monitor.enums.LogStatusEnum;
import top.charles7c.continew.admin.monitor.model.entity.LogDO;
import top.charles7c.continew.admin.system.service.UserService;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.core.util.IpUtils;
import top.charles7c.continew.starter.core.util.ServletUtils;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
/**
* 系统日志拦截器
*
* @author Charles7c
* @since 2022/12/24 21:14
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {
private final UserService userService;
private final LogProperties operationLogProperties;
private static final String ENCRYPT_SYMBOL = "****************";
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) {
if (this.isNeedRecord(handler, request)) {
// 记录时间
this.logCreateTime();
}
return true;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, Exception e) {
// 记录请求耗时及异常信息
LogDO logDO = this.logElapsedTimeAndException();
if (null == logDO) {
return;
}
HandlerMethod handlerMethod = (HandlerMethod)handler;
// 记录所属模块
this.logModule(logDO, handlerMethod);
// 记录日志描述
this.logDescription(logDO, handlerMethod);
// 记录请求信息
this.logRequest(logDO, request);
// 记录响应信息
this.logResponse(logDO, response);
// 保存系统日志
SpringUtil.getApplicationContext().publishEvent(logDO);
}
/**
* 记录时间
*/
private void logCreateTime() {
LogContext logContext = new LogContext();
logContext.setCreateUser(LoginHelper.getUserId());
logContext.setCreateTime(LocalDateTime.now());
LogContextHolder.set(logContext);
}
/**
* 记录请求耗时及异常详情
*
* @return 系统日志信息
*/
private LogDO logElapsedTimeAndException() {
LogContext logContext = LogContextHolder.get();
if (null == logContext) {
return null;
}
try {
LogDO logDO = new LogDO();
logDO.setCreateTime(logContext.getCreateTime());
logDO.setElapsedTime(System.currentTimeMillis() - LocalDateTimeUtil.toEpochMilli(logDO.getCreateTime()));
logDO.setStatus(LogStatusEnum.SUCCESS);
// 记录错误信息(非未知异常不记录异常详情,只记录错误信息)
String errorMsg = logContext.getErrorMsg();
if (StrUtil.isNotBlank(errorMsg)) {
logDO.setStatus(LogStatusEnum.FAILURE);
logDO.setErrorMsg(errorMsg);
}
// 记录异常详情
Throwable exception = logContext.getException();
if (null != exception) {
logDO.setStatus(LogStatusEnum.FAILURE);
logDO.setExceptionDetail(ExceptionUtil.stacktraceToString(exception, -1));
}
return logDO;
} finally {
LogContextHolder.remove();
}
}
/**
* 记录所属模块
*
* @param logDO
* 系统日志信息
* @param handlerMethod
* 处理器方法
*/
private void logModule(LogDO logDO, HandlerMethod handlerMethod) {
Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
// 例如:@Tag(name = "部门管理") -> 部门管理
// (本框架代码规范)例如:@Tag(name = "部门管理 API") -> 部门管理
if (null != classTag) {
String name = classTag.name();
logDO.setModule(
StrUtil.isNotBlank(name) ? name.replace("API", StringConstants.EMPTY).trim() : "请在该接口类上指定所属模块");
}
// 例如:@Log(module = "部门管理") -> 部门管理
if (null != classLog && StrUtil.isNotBlank(classLog.module())) {
logDO.setModule(classLog.module());
}
if (null != methodLog && StrUtil.isNotBlank(methodLog.module())) {
logDO.setModule(methodLog.module());
}
}
/**
* 记录日志描述
*
* @param logDO
* 系统日志信息
* @param handlerMethod
* 处理器方法
*/
private void logDescription(LogDO logDO, HandlerMethod handlerMethod) {
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
// 例如:@Operation(summary="新增部门") -> 新增部门
if (null != methodOperation) {
logDO.setDescription(StrUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
}
// 例如:@Log("新增部门") -> 新增部门
if (null != methodLog && StrUtil.isNotBlank(methodLog.value())) {
logDO.setDescription(methodLog.value());
}
}
/**
* 记录请求信息
*
* @param logDO
* 系统日志信息
* @param request
* 请求对象
*/
private void logRequest(LogDO logDO, HttpServletRequest request) {
logDO.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString() : request
.getRequestURL().append(StringConstants.QUESTION_MARK).append(request.getQueryString()).toString());
String method = request.getMethod();
logDO.setRequestMethod(method);
logDO.setRequestHeaders(this.desensitize(JakartaServletUtil.getHeaderMap(request)));
String requestBody = this.getRequestBody(request);
logDO.setCreateUser(ObjectUtil.defaultIfNull(logDO.getCreateUser(), LoginHelper.getUserId()));
String requestURI = request.getRequestURI();
if (requestURI.startsWith("/oauth")) {
logDO.setCreateUser(null);
}
if (null == logDO.getCreateUser() && SysConstants.LOGIN_URI.equals(requestURI)) {
AccountLoginReq loginReq = JSONUtil.toBean(requestBody, AccountLoginReq.class);
logDO.setCreateUser(
ExceptionUtils.exToNull(() -> userService.getByUsername(loginReq.getUsername()).getId()));
}
if (StrUtil.isNotBlank(requestBody)) {
if (JSONUtil.isTypeJSONObject(requestBody)) {
requestBody = this.desensitize(JSONUtil.parseObj(requestBody));
} else if (JSONUtil.isTypeJSONArray(requestBody)) {
JSONArray requestBodyJsonArr = JSONUtil.parseArray(requestBody);
List<JSONObject> requestBodyJsonObjList = new ArrayList<>(requestBodyJsonArr.size());
for (Object requestBodyJsonObj : requestBodyJsonArr) {
requestBodyJsonObjList
.add(JSONUtil.parseObj(this.desensitize(JSONUtil.parseObj(requestBodyJsonObj))));
}
requestBody = JSONUtil.toJsonStr(requestBodyJsonObjList);
} else {
requestBody = this.desensitize(JakartaServletUtil.getParamMap(request));
}
logDO.setRequestBody(requestBody);
}
logDO.setClientIp(JakartaServletUtil.getClientIP(request));
logDO.setLocation(IpUtils.getCityInfo(logDO.getClientIp()));
logDO.setBrowser(ServletUtils.getBrowser(request));
}
/**
* 记录响应信息
*
* @param logDO
* 系统日志信息
* @param response
* 响应对象
*/
private void logResponse(LogDO logDO, HttpServletResponse response) {
int status = response.getStatus();
logDO.setStatusCode(status);
logDO.setStatus(status >= HttpStatus.HTTP_BAD_REQUEST ? LogStatusEnum.FAILURE : logDO.getStatus());
logDO.setResponseHeaders(this.desensitize(JakartaServletUtil.getHeadersMap(response)));
// 响应体(不记录非 JSON 响应数据)
String responseBody = this.getResponseBody(response);
if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) {
logDO.setResponseBody(responseBody);
// 业务状态码优先级高
try {
R result = JSONUtil.toBean(responseBody, R.class);
logDO.setStatusCode(result.getCode());
logDO.setStatus(result.isSuccess() ? LogStatusEnum.SUCCESS : LogStatusEnum.FAILURE);
} catch (Exception ignored) {
}
}
}
/**
* 数据脱敏
*
* @param waitDesensitizeData
* 待脱敏数据
* @return 脱敏后的 JSON 字符串数据
*/
@SuppressWarnings("unchecked")
private String desensitize(Map waitDesensitizeData) {
String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
try {
if (CollUtil.isEmpty(waitDesensitizeData)) {
return desensitizeDataStr;
}
for (String desensitizeProperty : operationLogProperties.getDesensitizeFields()) {
waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL);
waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL);
waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL);
}
return JSONUtil.toJsonStr(waitDesensitizeData);
} catch (Exception ignored) {
}
return desensitizeDataStr;
}
/**
* 获取请求体
*
* @param request
* 请求对象
* @return 请求体
*/
private String getRequestBody(HttpServletRequest request) {
String requestBody = "";
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (null != wrapper) {
requestBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return requestBody;
}
/**
* 获取响应体
*
* @param response
* 响应对象
* @return 响应体
*/
private String getResponseBody(HttpServletResponse response) {
String responseBody = "";
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (null != wrapper) {
responseBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return responseBody;
}
/**
* 是否要记录系统日志
*
* @param handler
* 处理器
* @param request
* 请求对象
* @return true 需要记录false 不需要记录
*/
private boolean isNeedRecord(Object handler, HttpServletRequest request) {
// 1、未启用时不需要记录系统日志
if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) {
return false;
}
// 2、检查是否需要记录内网 IP 操作
boolean isInnerIp = IpUtils.isInnerIp(JakartaServletUtil.getClientIP(request));
if (isInnerIp && Boolean.FALSE.equals(operationLogProperties.getIncludeInnerIp())) {
return false;
}
// 3、排除不需要记录系统日志的接口
HandlerMethod handlerMethod = (HandlerMethod)handler;
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
// 3.1 如果接口方法上既没有 @Log 注解,也没有 @Operation 注解,则不记录系统日志
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (null == methodLog && null == methodOperation) {
return false;
}
// 3.2 请求方式不要求记录且接口方法上没有 @Log 注解,则不记录系统日志
if (null == methodLog && operationLogProperties.getExcludeMethods().contains(request.getMethod())) {
return false;
}
// 3.3 如果接口被隐藏,不记录系统日志
if (null != methodOperation && methodOperation.hidden()) {
return false;
}
// 3.4 如果接口方法或类上有 @Log 注解,但是要求忽略该接口,则不记录系统日志
if (null != methodLog && methodLog.ignore()) {
return false;
}
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
return null == classLog || !classLog.ignore();
}
}

View File

@@ -92,40 +92,40 @@ public class LogDO implements Serializable {
private String responseBody;
/**
* 请求耗时ms
* 耗时ms
*/
private Long elapsedTime;
private Long timeTaken;
/**
* 操作状态
* IP
*/
private LogStatusEnum status;
private String ip;
/**
* 客户端IP
* IP 归属地
*/
private String clientIp;
/**
* IP归属地
*/
private String location;
private String address;
/**
* 浏览器
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 状态
*/
private LogStatusEnum status;
/**
* 错误信息
*/
private String errorMsg;
/**
* 异常详情
*/
private String exceptionDetail;
/**
* 创建人
*/

View File

@@ -53,13 +53,13 @@ public class LoginLogResp extends LogResp {
* 登录 IP
*/
@Schema(description = "登录 IP", example = "192.168.0.1")
private String clientIp;
private String ip;
/**
* 登录地点
*/
@Schema(description = "登录地点", example = "中国北京北京市")
private String location;
private String address;
/**
* 浏览器
@@ -67,6 +67,12 @@ public class LoginLogResp extends LogResp {
@Schema(description = "浏览器", example = "Chrome 115.0.0.0")
private String browser;
/**
* 操作系统
*/
@Schema(description = "操作系统", example = "Windows 10")
private String os;
/**
* 错误信息
*/

View File

@@ -50,22 +50,16 @@ public class OperationLogResp extends LogResp {
private String module;
/**
* 操作状态
* 操作 IP
*/
@Schema(description = "操作状态1成功2失败", type = "Integer", allowableValues = {"1", "2"}, example = "1")
private LogStatusEnum status;
/**
* 操作IP
*/
@Schema(description = "操作IP", example = "192.168.0.1")
private String clientIp;
@Schema(description = "操作 IP", example = "192.168.0.1")
private String ip;
/**
* 操作地点
*/
@Schema(description = "操作地点", example = "中国北京北京市")
private String location;
private String address;
/**
* 浏览器
@@ -73,6 +67,12 @@ public class OperationLogResp extends LogResp {
@Schema(description = "浏览器", example = "Chrome 115.0.0.0")
private String browser;
/**
* 操作状态
*/
@Schema(description = "操作状态1成功2失败", type = "Integer", allowableValues = {"1", "2"}, example = "1")
private LogStatusEnum status;
/**
* 错误信息
*/

View File

@@ -56,7 +56,7 @@ public class SystemLogDetailResp extends LogResp {
/**
* 请求头
*/
@Schema(description = "请求头", example = "{\"Origin\": \"https://cnadmin.charles7c.top\",...}")
@Schema(description = "请求头", example = "{\"Origin\": [\"https://cnadmin.charles7c.top\"],...}")
private String requestHeaders;
/**
@@ -78,16 +78,16 @@ public class SystemLogDetailResp extends LogResp {
private String responseBody;
/**
* 客户端IP
* IP
*/
@Schema(description = "客户端IP", example = "192.168.0.1")
private String clientIp;
@Schema(description = "IP", example = "192.168.0.1")
private String ip;
/**
* IP归属
* 地
*/
@Schema(description = "IP归属", example = "中国北京北京市")
private String location;
@Schema(description = "", example = "中国北京北京市")
private String address;
/**
* 浏览器
@@ -96,8 +96,14 @@ public class SystemLogDetailResp extends LogResp {
private String browser;
/**
* 请求耗时ms
* 操作系统
*/
@Schema(description = "请求耗时ms", example = "58")
private Long elapsedTime;
@Schema(description = "操作系统", example = "Windows 10")
private String os;
/**
* 耗时ms
*/
@Schema(description = "耗时ms", example = "58")
private Long timeTaken;
}

View File

@@ -54,16 +54,16 @@ public class SystemLogResp extends LogResp {
private String requestUrl;
/**
* 客户端IP
* IP
*/
@Schema(description = "客户端IP", example = "192.168.0.1")
private String clientIp;
@Schema(description = "IP", example = "192.168.0.1")
private String ip;
/**
* IP归属
* 地
*/
@Schema(description = "IP归属", example = "中国北京北京市")
private String location;
@Schema(description = "", example = "中国北京北京市")
private String address;
/**
* 浏览器
@@ -72,20 +72,8 @@ public class SystemLogResp extends LogResp {
private String browser;
/**
* 请求耗时ms
* 耗时ms
*/
@Schema(description = "请求耗时ms", example = "58")
private Long elapsedTime;
/**
* 错误信息
*/
@Schema(description = "错误信息")
private String errorMsg;
/**
* 异常详情
*/
@Schema(description = "异常详情")
private String exceptionDetail;
@Schema(description = "耗时ms", example = "58")
private Long timeTaken;
}

View File

@@ -23,8 +23,6 @@ import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -63,27 +61,18 @@ public class LogServiceImpl implements LogService {
private final LogMapper logMapper;
private final CommonUserService commonUserService;
@Async
@EventListener
public void save(LogDO logDO) {
logMapper.insert(logDO);
}
@Override
public PageDataResp<OperationLogResp> page(OperationLogQuery query, PageQuery pageQuery) {
QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query);
// 限定查询信息
List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(OperationLogResp.class);
List<String> columnNameList =
fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX))
.map(StrUtil::toUnderlineCase).collect(Collectors.toList());
queryWrapper.select(columnNameList);
// 分页查询
IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper);
PageDataResp<OperationLogResp> pageDataResp = PageDataResp.build(page, OperationLogResp.class);
// 填充数据(如果是查询个人操作日志,只查询一次用户信息即可)
if (null != query.getUid()) {
String nickname = ExceptionUtils.exToNull(() -> commonUserService.getNicknameById(query.getUid()));
@@ -98,18 +87,15 @@ public class LogServiceImpl implements LogService {
public PageDataResp<LoginLogResp> page(LoginLogQuery query, PageQuery pageQuery) {
QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query);
queryWrapper.eq("module", "登录");
// 限定查询信息
List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(LoginLogResp.class);
List<String> columnNameList =
fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX))
.map(StrUtil::toUnderlineCase).collect(Collectors.toList());
queryWrapper.select(columnNameList);
// 分页查询
IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper);
PageDataResp<LoginLogResp> pageDataResp = PageDataResp.build(page, LoginLogResp.class);
// 填充数据
pageDataResp.getList().forEach(this::fill);
return pageDataResp;
@@ -118,18 +104,15 @@ public class LogServiceImpl implements LogService {
@Override
public PageDataResp<SystemLogResp> page(SystemLogQuery query, PageQuery pageQuery) {
QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query);
// 限定查询信息
List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(SystemLogResp.class);
List<String> columnNameList =
fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX))
.map(StrUtil::toUnderlineCase).collect(Collectors.toList());
queryWrapper.select(columnNameList);
// 分页查询
IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper);
PageDataResp<SystemLogResp> pageDataResp = PageDataResp.build(page, SystemLogResp.class);
// 填充数据
pageDataResp.getList().forEach(this::fill);
return pageDataResp;