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

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

View File

@ -56,9 +56,9 @@ public class SysConstants {
public static final String DEFAULT_PASSWORD = "123456";
/**
* 登录 URI
* 账号登录 URI
*/
public static final String LOGIN_URI = "/auth/login";
public static final String LOGIN_URI = "/auth/account";
/**
* 退出 URI

View File

@ -41,7 +41,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import top.charles7c.continew.admin.common.util.holder.LogContextHolder;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.exception.BadRequestException;
import top.charles7c.continew.starter.core.exception.BusinessException;
@ -65,7 +64,6 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BadRequestException.class)
public R handleBadRequestException(BadRequestException e, HttpServletRequest request) {
log.warn("请求地址 [{}],自定义验证失败。", request.getRequestURI(), e);
LogContextHolder.setErrorMsg(e.getMessage());
return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage());
}
@ -77,7 +75,6 @@ public class GlobalExceptionHandler {
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
String errorMsg =
CollUtil.join(e.getConstraintViolations(), StringConstants.CHINESE_COMMA, ConstraintViolation::getMessage);
LogContextHolder.setErrorMsg(errorMsg);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
@ -89,7 +86,6 @@ public class GlobalExceptionHandler {
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
String errorMsg = CollUtil.join(e.getAllErrors(), StringConstants.CHINESE_COMMA,
DefaultMessageSourceResolvable::getDefaultMessage);
LogContextHolder.setErrorMsg(errorMsg);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
@ -101,7 +97,6 @@ public class GlobalExceptionHandler {
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
String errorMsg = ExceptionUtils
.exToNull(() -> Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
LogContextHolder.setErrorMsg(errorMsg);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
@ -113,7 +108,6 @@ public class GlobalExceptionHandler {
HttpServletRequest request) {
String errorMsg = StrUtil.format("参数名:[{}],期望参数类型:[{}]", e.getName(), e.getParameter().getParameterType());
log.warn("请求地址 [{}],参数转换失败,{}。", request.getRequestURI(), errorMsg, e);
LogContextHolder.setErrorMsg(errorMsg);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
@ -125,7 +119,6 @@ public class GlobalExceptionHandler {
log.warn("请求地址 [{}],上传文件失败,文件大小超过限制。", request.getRequestURI(), e);
String sizeLimit = StrUtil.subBetween(e.getMessage(), "The maximum size ", " for");
String errorMsg = String.format("请上传小于 %sMB 的文件", NumberUtil.parseLong(sizeLimit) / 1024 / 1024);
LogContextHolder.setErrorMsg(errorMsg);
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
@ -140,7 +133,6 @@ public class GlobalExceptionHandler {
case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线。";
default -> "您的登录状态已过期,请重新登录。";
};
LogContextHolder.setErrorMsg(errorMsg);
return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg);
}
@ -167,7 +159,6 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
LogContextHolder.setErrorMsg(e.getMessage());
log.error("请求地址 [{}],不支持 [{}] 请求。", request.getRequestURI(), e.getMethod());
return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), e.getMessage());
}
@ -178,7 +169,6 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public R handleServiceException(BusinessException e, HttpServletRequest request) {
log.error("请求地址 [{}],发生业务异常。", request.getRequestURI(), e);
LogContextHolder.setErrorMsg(e.getMessage());
return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
@ -188,7 +178,6 @@ public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public R handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("请求地址 [{}],发生系统异常。", request.getRequestURI(), e);
LogContextHolder.setException(e);
return R.fail(e.getMessage());
}
@ -198,7 +187,6 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class)
public R handleException(Throwable e, HttpServletRequest request) {
log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e);
LogContextHolder.setException(e);
return R.fail(e.getMessage());
}
}

View File

@ -75,20 +75,25 @@ public class LoginUser implements Serializable {
private String token;
/**
* 登录 IP
* IP
*/
private String clientIp;
private String ip;
/**
* 登录地点
* IP 归属地
*/
private String location;
private String address;
/**
* 浏览器
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 登录时间
*/

View File

@ -26,13 +26,12 @@ import lombok.NoArgsConstructor;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import top.charles7c.continew.admin.common.constant.CacheConstants;
import top.charles7c.continew.admin.common.model.dto.LogContext;
import top.charles7c.continew.admin.common.model.dto.LoginUser;
import top.charles7c.continew.admin.common.util.holder.LogContextHolder;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.core.util.IpUtils;
import top.charles7c.continew.starter.core.util.ServletUtils;
@ -58,11 +57,11 @@ public class LoginHelper {
public static String login(LoginUser loginUser) {
// 记录登录信息
HttpServletRequest request = ServletUtils.getRequest();
loginUser.setClientIp(JakartaServletUtil.getClientIP(request));
loginUser.setLocation(IpUtils.getCityInfo(loginUser.getClientIp()));
loginUser.setIp(JakartaServletUtil.getClientIP(request));
loginUser.setAddress(IpUtils.getAddress(loginUser.getIp()));
loginUser.setBrowser(ServletUtils.getBrowser(request));
LogContext logContext = LogContextHolder.get();
loginUser.setLoginTime(null != logContext ? logContext.getCreateTime() : LocalDateTime.now());
loginUser.setLoginTime(LocalDateTime.now());
loginUser.setOs(StrUtil.subBefore(ServletUtils.getOs(request), " or", false));
// 登录并缓存用户信息
StpUtil.login(loginUser.getId());
SaHolder.getStorage().set(CacheConstants.LOGIN_USER_KEY, loginUser);

View File

@ -1,87 +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.common.util.holder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import top.charles7c.continew.admin.common.model.dto.LogContext;
/**
* 系统日志上下文持有者
*
* @author Charles7c
* @since 2022/12/25 8:55
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LogContextHolder {
private static final ThreadLocal<LogContext> LOG_THREAD_LOCAL = new ThreadLocal<>();
/**
* 存储系统日志上下文
*
* @param logContext
* 系统日志上下文信息
*/
public static void set(LogContext logContext) {
LOG_THREAD_LOCAL.set(logContext);
}
/**
* 获取系统日志上下文
*
* @return 系统日志上下文信息
*/
public static LogContext get() {
return LOG_THREAD_LOCAL.get();
}
/**
* 移除系统日志上下文
*/
public static void remove() {
LOG_THREAD_LOCAL.remove();
}
/**
* 在系统日志上下文中保存异常信息
*
* @param e
* 异常信息
*/
public static void setException(Throwable e) {
LogContext logContext = get();
if (null != logContext) {
logContext.setErrorMsg(e.getMessage());
logContext.setException(e);
}
}
/**
* 在系统日志上下文中保存错误信息非未知异常不记录异常信息只记录错误信息
*
* @param errorMsg
* 错误信息
*/
public static void setErrorMsg(String errorMsg) {
LogContext logContext = get();
if (null != logContext) {
logContext.setErrorMsg(errorMsg);
}
}
}

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;

View File

@ -66,13 +66,13 @@ public class OnlineUserResp implements Serializable {
* 登录 IP
*/
@Schema(description = "登录 IP", example = "192.168.0.1")
private String clientIp;
private String ip;
/**
* 登录地点
*/
@Schema(description = "登录地点", example = "中国北京北京市")
private String location;
private String address;
/**
* 浏览器
@ -80,6 +80,12 @@ public class OnlineUserResp implements Serializable {
@Schema(description = "浏览器", example = "Chrome 115.0.0.0")
private String browser;
/**
* 操作系统
*/
@Schema(description = "操作系统", example = "Windows 10")
private String os;
/**
* 登录时间
*/

View File

@ -5,9 +5,10 @@ const BASE_URL = '/monitor/log';
export interface LogRecord {
id?: number;
clientIp: string;
location: string;
ip: string;
address: string;
browser: string;
os: string;
createTime: string;
}
@ -22,7 +23,7 @@ export interface OperationLogRecord extends LogRecord {
module: string;
description: string;
status: number;
errorMsg: string;
errorMsgString: string;
createUserString: string;
}
@ -30,8 +31,7 @@ export interface SystemLogRecord extends LogRecord {
statusCode: number;
requestMethod: string;
requestUrl: string;
elapsedTime: number;
exceptionDetail?: string;
timeTaken: number;
}
export interface SystemLogDetailRecord extends SystemLogRecord {

View File

@ -7,9 +7,10 @@ export interface DataRecord {
token: string;
username: string;
nickname: string;
clientIp: string;
location: string;
ip: string;
address: string;
browser: string;
os: string;
loginTime: string;
}

View File

@ -72,9 +72,10 @@
</a-tooltip>
</template>
</a-table-column>
<a-table-column title="登录 IP" data-index="clientIp" />
<a-table-column title="登录地点" data-index="location" />
<a-table-column title="登录 IP" data-index="ip" />
<a-table-column title="登录地点" data-index="address" />
<a-table-column title="浏览器" data-index="browser" />
<a-table-column title="终端系统" data-index="os" />
<a-table-column title="登录时间" data-index="createTime" />
</template>
</a-table>

View File

@ -83,8 +83,8 @@
</a-tooltip>
</template>
</a-table-column>
<a-table-column title="操作 IP" data-index="clientIp" />
<a-table-column title="操作地点" data-index="location" />
<a-table-column title="操作 IP" data-index="ip" />
<a-table-column title="操作地点" data-index="address" />
<a-table-column title="浏览器" data-index="browser" />
</template>
</a-table>

View File

@ -74,21 +74,21 @@
}}</span>
</template>
</a-table-column>
<a-table-column title="客户端 IP" data-index="clientIp" />
<a-table-column title="IP 归属地" data-index="location" />
<a-table-column title="IP" data-index="ip" />
<a-table-column title="" data-index="address" />
<a-table-column title="浏览器" data-index="browser" />
<a-table-column title="请求耗时">
<a-table-column title="耗时">
<template #cell="{ record }">
<a-tag v-if="record.elapsedTime > 500" color="red"
>{{ record.elapsedTime }} ms</a-tag
<a-tag v-if="record.timeTaken > 500" color="red"
>{{ record.timeTaken }} ms</a-tag
>
<a-tag v-else-if="record.elapsedTime > 200" color="orange"
>{{ record.elapsedTime }} ms</a-tag
<a-tag v-else-if="record.timeTaken > 200" color="orange"
>{{ record.timeTaken }} ms</a-tag
>
<a-tag v-else color="green">{{ record.elapsedTime }} ms</a-tag>
<a-tag v-else color="green">{{ record.timeTaken }} ms</a-tag>
</template>
</a-table-column>
<a-table-column title="创建时间" data-index="createTime" />
<a-table-column title="请求时间" data-index="createTime" />
<a-table-column title="操作" align="center">
<template #cell="{ record }">
<a-button
@ -99,15 +99,6 @@
>
<template #icon><icon-eye /></template>详情
</a-button>
<a-button
v-if="record.exceptionDetail"
type="text"
size="small"
title="查看异常详情"
@click="toExceptionDetail(record)"
>
<template #icon><icon-bug /></template>异常
</a-button>
</template>
</a-table-column>
</template>
@ -125,11 +116,11 @@
>
<div style="margin: 10px 0 0 10px">
<a-descriptions title="基础信息" :column="2" bordered>
<a-descriptions-item label="客户端 IP">
<a-descriptions-item label="IP">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['200px']" :rows="1" />
</a-skeleton>
<span v-else>{{ systemLog.clientIp }}</span>
<span v-else>{{ systemLog.ip }}</span>
</a-descriptions-item>
<a-descriptions-item label="浏览器">
<a-skeleton v-if="loading" :animation="true">
@ -137,34 +128,40 @@
</a-skeleton>
<span v-else>{{ systemLog.browser }}</span>
</a-descriptions-item>
<a-descriptions-item label="IP 归属地">
<a-descriptions-item label="">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['200px']" :rows="1" />
</a-skeleton>
<span v-else>{{ systemLog.location }}</span>
<span v-else>{{ systemLog.address }}</span>
</a-descriptions-item>
<a-descriptions-item label="请求耗时">
<a-descriptions-item label="操作系统">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['200px']" :rows="1" />
</a-skeleton>
<span v-else>
<a-tag v-if="systemLog.elapsedTime > 500" color="red">
{{ systemLog.elapsedTime }} ms
</a-tag>
<a-tag v-else-if="systemLog.elapsedTime > 200" color="orange">
{{ systemLog.elapsedTime }} ms
</a-tag>
<a-tag v-else color="green"
>{{ systemLog.elapsedTime }} ms</a-tag
>
</span>
<span v-else>{{ systemLog.os }}</span>
</a-descriptions-item>
<a-descriptions-item label="创建时间">
<a-descriptions-item label="请求时间">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['200px']" :rows="1" />
</a-skeleton>
<span v-else>{{ systemLog.createTime }}</span>
</a-descriptions-item>
<a-descriptions-item label="耗时">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['200px']" :rows="1" />
</a-skeleton>
<span v-else>
<a-tag v-if="systemLog.timeTaken > 500" color="red">
{{ systemLog.timeTaken }} ms
</a-tag>
<a-tag v-else-if="systemLog.timeTaken > 200" color="orange">
{{ systemLog.timeTaken }} ms
</a-tag>
<a-tag v-else color="green"
>{{ systemLog.timeTaken }} ms</a-tag
>
</span>
</a-descriptions-item>
</a-descriptions>
<a-descriptions
title="协议信息"
@ -256,20 +253,6 @@
</a-descriptions>
</div>
</a-drawer>
<!-- 异常详情区域 -->
<a-modal
title="异常详情"
:visible="exceptionDetailVisible"
width="83%"
:footer="false"
top="30px"
unmount-on-close
render-to-body
@cancel="handleExceptionDetailCancel"
>
<pre>{{ exceptionDetail }}</pre>
</a-modal>
</a-card>
</div>
</template>
@ -297,17 +280,16 @@
statusCode: 200,
responseHeaders: '',
responseBody: '',
elapsedTime: 0,
clientIp: '',
location: '',
timeTaken: 0,
ip: '',
address: '',
browser: '',
os: '',
createTime: '',
});
const total = ref(0);
const exceptionDetail = ref('');
const loading = ref(false);
const visible = ref(false);
const exceptionDetailVisible = ref(false);
const data = reactive({
//
@ -362,24 +344,6 @@
visible.value = false;
};
/**
* 查看异常详情
*
* @param record 记录信息
*/
const toExceptionDetail = async (record: SystemLogRecord) => {
exceptionDetail.value = record.exceptionDetail || '';
exceptionDetailVisible.value = true;
};
/**
* 关闭异常详情
*/
const handleExceptionDetailCancel = () => {
exceptionDetail.value = '';
exceptionDetailVisible.value = false;
};
/**
* 查询
*/

View File

@ -63,9 +63,10 @@
{{ record.nickname }}{{ record.username }}
</template>
</a-table-column>
<a-table-column title="登录 IP" data-index="clientIp" />
<a-table-column title="登录地点" data-index="location" />
<a-table-column title="登录 IP" data-index="ip" />
<a-table-column title="登录地点" data-index="address" />
<a-table-column title="浏览器" data-index="browser" />
<a-table-column title="终端系统" data-index="os" />
<a-table-column title="登录时间" data-index="loginTime" />
<a-table-column
v-if="checkPermission(['monitor:online:user:delete'])"

View File

@ -40,8 +40,8 @@
</a-tooltip>
</template>
</a-table-column>
<a-table-column title="操作 IP" data-index="clientIp" />
<a-table-column title="操作地点" data-index="location" />
<a-table-column title="操作 IP" data-index="ip" />
<a-table-column title="操作地点" data-index="address" />
<a-table-column title="浏览器" data-index="browser" />
</template>
<template #pagination-left>

View File

@ -43,13 +43,13 @@ import top.charles7c.continew.admin.common.constant.CacheConstants;
import top.charles7c.continew.admin.common.model.dto.LoginUser;
import top.charles7c.continew.admin.common.util.SecureUtils;
import top.charles7c.continew.admin.common.util.helper.LoginHelper;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.admin.system.model.resp.UserDetailResp;
import top.charles7c.continew.admin.system.service.UserService;
import top.charles7c.continew.starter.cache.redisson.util.RedisUtils;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.core.util.validate.ValidationUtils;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.annotation.Log;
/**
* 认证 API
@ -115,9 +115,10 @@ public class AuthController {
@Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx",
in = ParameterIn.HEADER)
@PostMapping("/logout")
public R logout() {
public R<Object> logout() {
Object loginId = StpUtil.getLoginId(-1L);
StpUtil.logout();
return R.ok();
return R.ok(loginId);
}
@Log(ignore = true)

View File

@ -32,10 +32,10 @@ import cn.dev33.satoken.stp.StpUtil;
import top.charles7c.continew.admin.auth.model.resp.LoginResp;
import top.charles7c.continew.admin.auth.service.LoginService;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.starter.core.exception.BadRequestException;
import top.charles7c.continew.starter.core.util.validate.ValidationUtils;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.annotation.Log;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;

View File

@ -45,7 +45,6 @@ import cn.hutool.core.util.StrUtil;
import top.charles7c.continew.admin.common.config.properties.LocalStorageProperties;
import top.charles7c.continew.admin.common.constant.CacheConstants;
import top.charles7c.continew.admin.common.model.resp.LabelValueResp;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.admin.system.model.query.DeptQuery;
import top.charles7c.continew.admin.system.model.query.MenuQuery;
import top.charles7c.continew.admin.system.model.query.OptionQuery;
@ -59,6 +58,7 @@ import top.charles7c.continew.starter.core.util.validate.ValidationUtils;
import top.charles7c.continew.starter.data.mybatis.plus.enums.IBaseEnum;
import top.charles7c.continew.starter.extension.crud.model.query.SortQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.annotation.Log;
/**
* 公共 API

View File

@ -31,7 +31,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.admin.monitor.model.resp.DashboardAccessTrendResp;
import top.charles7c.continew.admin.monitor.model.resp.DashboardGeoDistributionResp;
import top.charles7c.continew.admin.monitor.model.resp.DashboardPopularModuleResp;
@ -40,6 +39,7 @@ import top.charles7c.continew.admin.monitor.service.DashboardService;
import top.charles7c.continew.admin.system.model.resp.DashboardAnnouncementResp;
import top.charles7c.continew.starter.core.util.validate.ValidationUtils;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.annotation.Log;
/**
* 仪表盘 API

View File

@ -29,7 +29,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.admin.monitor.model.query.LoginLogQuery;
import top.charles7c.continew.admin.monitor.model.query.OperationLogQuery;
import top.charles7c.continew.admin.monitor.model.query.SystemLogQuery;
@ -41,6 +40,7 @@ import top.charles7c.continew.admin.monitor.service.LogService;
import top.charles7c.continew.starter.extension.crud.model.query.PageQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.annotation.Log;
/**
* 日志管理 API

View File

@ -29,7 +29,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.charles7c.continew.admin.common.util.helper.LoginHelper;
import top.charles7c.continew.admin.monitor.annotation.Log;
import top.charles7c.continew.admin.system.model.query.MessageQuery;
import top.charles7c.continew.admin.system.model.resp.MessageResp;
import top.charles7c.continew.admin.system.model.resp.MessageUnreadResp;
@ -38,6 +37,7 @@ import top.charles7c.continew.admin.system.service.MessageUserService;
import top.charles7c.continew.starter.extension.crud.model.query.PageQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp;
import top.charles7c.continew.starter.extension.crud.model.resp.R;
import top.charles7c.continew.starter.log.common.annotation.Log;
/**
* 消息管理 API

View File

@ -25,19 +25,21 @@ project:
--- ### 日志配置(重叠部分,优先级高于 logback-spring.xml 中的配置)
logging:
config: classpath:logback-spring.xml
## 系统日志配置
system:
# 是否启用系统日志
## 日志配置
continew-starter:
log:
enabled: true
# 是否记录内网 IP 操作
includeInnerIp: true
# 排除请求方式
#excludeMethods:
# - GET
# 脱敏字段
desensitizeFields:
- password
- Authorization
include:
- DESCRIPTION
- MODULE
- REQUEST_HEADERS
- REQUEST_BODY
- IP_ADDRESS
- BROWSER
- OS
- RESPONSE_HEADERS
- RESPONSE_BODY
- TIME_TAKEN
--- ### 接口文档配置
springdoc:

View File

@ -123,17 +123,17 @@ CREATE TABLE IF NOT EXISTS `sys_log` (
`status_code` int NOT NULL COMMENT '状态码',
`response_headers` text DEFAULT NULL COMMENT '响应头',
`response_body` mediumtext DEFAULT NULL COMMENT '响应体',
`elapsed_time` bigint(20) NOT NULL COMMENT '请求耗时ms',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '操作状态1成功2失败',
`client_ip` varchar(100) DEFAULT NULL COMMENT '客户端IP',
`location` varchar(255) DEFAULT NULL COMMENT 'IP归属地',
`time_taken` bigint(20) NOT NULL COMMENT '耗时ms',
`ip` varchar(100) DEFAULT NULL COMMENT 'IP',
`address` varchar(255) DEFAULT NULL COMMENT 'IP归属地',
`browser` varchar(100) DEFAULT NULL COMMENT '浏览器',
`os` varchar(100) DEFAULT NULL COMMENT '操作系统',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1成功2失败',
`error_msg` text DEFAULT NULL COMMENT '错误信息',
`exception_detail` mediumtext DEFAULT NULL COMMENT '异常详情',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_module`(`module`) USING BTREE,
INDEX `idx_client_ip`(`client_ip`) USING BTREE,
INDEX `idx_ip`(`ip`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';

View File

@ -12,7 +12,7 @@
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
</parent>
<groupId>top.charles7c.continew</groupId>