mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 21:00:53 +08:00 
			
		
		
		
	refactor: 💥 适配 ContiNew Starter Log(日志模块)
1.continew-starter 1.0.1-SNAPSHOT => 1.1.0-SNAPSHOT 2.日志表结构及相关管理 UI 变更
This commit is contained in:
		| @@ -56,9 +56,9 @@ public class SysConstants { | |||||||
|     public static final String DEFAULT_PASSWORD = "123456"; |     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 |      * 退出 URI | ||||||
|   | |||||||
| @@ -41,7 +41,6 @@ import cn.hutool.core.collection.CollUtil; | |||||||
| import cn.hutool.core.util.NumberUtil; | import cn.hutool.core.util.NumberUtil; | ||||||
| import cn.hutool.core.util.StrUtil; | 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.constant.StringConstants; | ||||||
| import top.charles7c.continew.starter.core.exception.BadRequestException; | import top.charles7c.continew.starter.core.exception.BadRequestException; | ||||||
| import top.charles7c.continew.starter.core.exception.BusinessException; | import top.charles7c.continew.starter.core.exception.BusinessException; | ||||||
| @@ -65,7 +64,6 @@ public class GlobalExceptionHandler { | |||||||
|     @ExceptionHandler(BadRequestException.class) |     @ExceptionHandler(BadRequestException.class) | ||||||
|     public R handleBadRequestException(BadRequestException e, HttpServletRequest request) { |     public R handleBadRequestException(BadRequestException e, HttpServletRequest request) { | ||||||
|         log.warn("请求地址 [{}],自定义验证失败。", request.getRequestURI(), e); |         log.warn("请求地址 [{}],自定义验证失败。", request.getRequestURI(), e); | ||||||
|         LogContextHolder.setErrorMsg(e.getMessage()); |  | ||||||
|         return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage()); |         return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -77,7 +75,6 @@ public class GlobalExceptionHandler { | |||||||
|         log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e); |         log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e); | ||||||
|         String errorMsg = |         String errorMsg = | ||||||
|             CollUtil.join(e.getConstraintViolations(), StringConstants.CHINESE_COMMA, ConstraintViolation::getMessage); |             CollUtil.join(e.getConstraintViolations(), StringConstants.CHINESE_COMMA, ConstraintViolation::getMessage); | ||||||
|         LogContextHolder.setErrorMsg(errorMsg); |  | ||||||
|         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); |         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -89,7 +86,6 @@ public class GlobalExceptionHandler { | |||||||
|         log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e); |         log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e); | ||||||
|         String errorMsg = CollUtil.join(e.getAllErrors(), StringConstants.CHINESE_COMMA, |         String errorMsg = CollUtil.join(e.getAllErrors(), StringConstants.CHINESE_COMMA, | ||||||
|             DefaultMessageSourceResolvable::getDefaultMessage); |             DefaultMessageSourceResolvable::getDefaultMessage); | ||||||
|         LogContextHolder.setErrorMsg(errorMsg); |  | ||||||
|         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); |         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -101,7 +97,6 @@ public class GlobalExceptionHandler { | |||||||
|         log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e); |         log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e); | ||||||
|         String errorMsg = ExceptionUtils |         String errorMsg = ExceptionUtils | ||||||
|             .exToNull(() -> Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage()); |             .exToNull(() -> Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage()); | ||||||
|         LogContextHolder.setErrorMsg(errorMsg); |  | ||||||
|         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); |         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -113,7 +108,6 @@ public class GlobalExceptionHandler { | |||||||
|         HttpServletRequest request) { |         HttpServletRequest request) { | ||||||
|         String errorMsg = StrUtil.format("参数名:[{}],期望参数类型:[{}]", e.getName(), e.getParameter().getParameterType()); |         String errorMsg = StrUtil.format("参数名:[{}],期望参数类型:[{}]", e.getName(), e.getParameter().getParameterType()); | ||||||
|         log.warn("请求地址 [{}],参数转换失败,{}。", request.getRequestURI(), errorMsg, e); |         log.warn("请求地址 [{}],参数转换失败,{}。", request.getRequestURI(), errorMsg, e); | ||||||
|         LogContextHolder.setErrorMsg(errorMsg); |  | ||||||
|         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); |         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -125,7 +119,6 @@ public class GlobalExceptionHandler { | |||||||
|         log.warn("请求地址 [{}],上传文件失败,文件大小超过限制。", request.getRequestURI(), e); |         log.warn("请求地址 [{}],上传文件失败,文件大小超过限制。", request.getRequestURI(), e); | ||||||
|         String sizeLimit = StrUtil.subBetween(e.getMessage(), "The maximum size ", " for"); |         String sizeLimit = StrUtil.subBetween(e.getMessage(), "The maximum size ", " for"); | ||||||
|         String errorMsg = String.format("请上传小于 %sMB 的文件", NumberUtil.parseLong(sizeLimit) / 1024 / 1024); |         String errorMsg = String.format("请上传小于 %sMB 的文件", NumberUtil.parseLong(sizeLimit) / 1024 / 1024); | ||||||
|         LogContextHolder.setErrorMsg(errorMsg); |  | ||||||
|         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); |         return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -140,7 +133,6 @@ public class GlobalExceptionHandler { | |||||||
|             case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线。"; |             case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线。"; | ||||||
|             default -> "您的登录状态已过期,请重新登录。"; |             default -> "您的登录状态已过期,请重新登录。"; | ||||||
|         }; |         }; | ||||||
|         LogContextHolder.setErrorMsg(errorMsg); |  | ||||||
|         return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg); |         return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -167,7 +159,6 @@ public class GlobalExceptionHandler { | |||||||
|      */ |      */ | ||||||
|     @ExceptionHandler(HttpRequestMethodNotSupportedException.class) |     @ExceptionHandler(HttpRequestMethodNotSupportedException.class) | ||||||
|     public R handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) { |     public R handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) { | ||||||
|         LogContextHolder.setErrorMsg(e.getMessage()); |  | ||||||
|         log.error("请求地址 [{}],不支持 [{}] 请求。", request.getRequestURI(), e.getMethod()); |         log.error("请求地址 [{}],不支持 [{}] 请求。", request.getRequestURI(), e.getMethod()); | ||||||
|         return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), e.getMessage()); |         return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), e.getMessage()); | ||||||
|     } |     } | ||||||
| @@ -178,7 +169,6 @@ public class GlobalExceptionHandler { | |||||||
|     @ExceptionHandler(BusinessException.class) |     @ExceptionHandler(BusinessException.class) | ||||||
|     public R handleServiceException(BusinessException e, HttpServletRequest request) { |     public R handleServiceException(BusinessException e, HttpServletRequest request) { | ||||||
|         log.error("请求地址 [{}],发生业务异常。", request.getRequestURI(), e); |         log.error("请求地址 [{}],发生业务异常。", request.getRequestURI(), e); | ||||||
|         LogContextHolder.setErrorMsg(e.getMessage()); |  | ||||||
|         return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); |         return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -188,7 +178,6 @@ public class GlobalExceptionHandler { | |||||||
|     @ExceptionHandler(RuntimeException.class) |     @ExceptionHandler(RuntimeException.class) | ||||||
|     public R handleRuntimeException(RuntimeException e, HttpServletRequest request) { |     public R handleRuntimeException(RuntimeException e, HttpServletRequest request) { | ||||||
|         log.error("请求地址 [{}],发生系统异常。", request.getRequestURI(), e); |         log.error("请求地址 [{}],发生系统异常。", request.getRequestURI(), e); | ||||||
|         LogContextHolder.setException(e); |  | ||||||
|         return R.fail(e.getMessage()); |         return R.fail(e.getMessage()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -198,7 +187,6 @@ public class GlobalExceptionHandler { | |||||||
|     @ExceptionHandler(Throwable.class) |     @ExceptionHandler(Throwable.class) | ||||||
|     public R handleException(Throwable e, HttpServletRequest request) { |     public R handleException(Throwable e, HttpServletRequest request) { | ||||||
|         log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e); |         log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e); | ||||||
|         LogContextHolder.setException(e); |  | ||||||
|         return R.fail(e.getMessage()); |         return R.fail(e.getMessage()); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -75,20 +75,25 @@ public class LoginUser implements Serializable { | |||||||
|     private String token; |     private String token; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录 IP |      * IP | ||||||
|      */ |      */ | ||||||
|     private String clientIp; |     private String ip; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录地点 |      * IP 归属地 | ||||||
|      */ |      */ | ||||||
|     private String location; |     private String address; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 浏览器 |      * 浏览器 | ||||||
|      */ |      */ | ||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 操作系统 | ||||||
|  |      */ | ||||||
|  |     private String os; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录时间 |      * 登录时间 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -26,13 +26,12 @@ import lombok.NoArgsConstructor; | |||||||
| import cn.dev33.satoken.context.SaHolder; | import cn.dev33.satoken.context.SaHolder; | ||||||
| import cn.dev33.satoken.session.SaSession; | import cn.dev33.satoken.session.SaSession; | ||||||
| import cn.dev33.satoken.stp.StpUtil; | import cn.dev33.satoken.stp.StpUtil; | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
| import cn.hutool.extra.servlet.JakartaServletUtil; | import cn.hutool.extra.servlet.JakartaServletUtil; | ||||||
| import cn.hutool.extra.spring.SpringUtil; | import cn.hutool.extra.spring.SpringUtil; | ||||||
|  |  | ||||||
| import top.charles7c.continew.admin.common.constant.CacheConstants; | 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.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.ExceptionUtils; | ||||||
| import top.charles7c.continew.starter.core.util.IpUtils; | import top.charles7c.continew.starter.core.util.IpUtils; | ||||||
| import top.charles7c.continew.starter.core.util.ServletUtils; | import top.charles7c.continew.starter.core.util.ServletUtils; | ||||||
| @@ -58,11 +57,11 @@ public class LoginHelper { | |||||||
|     public static String login(LoginUser loginUser) { |     public static String login(LoginUser loginUser) { | ||||||
|         // 记录登录信息 |         // 记录登录信息 | ||||||
|         HttpServletRequest request = ServletUtils.getRequest(); |         HttpServletRequest request = ServletUtils.getRequest(); | ||||||
|         loginUser.setClientIp(JakartaServletUtil.getClientIP(request)); |         loginUser.setIp(JakartaServletUtil.getClientIP(request)); | ||||||
|         loginUser.setLocation(IpUtils.getCityInfo(loginUser.getClientIp())); |         loginUser.setAddress(IpUtils.getAddress(loginUser.getIp())); | ||||||
|         loginUser.setBrowser(ServletUtils.getBrowser(request)); |         loginUser.setBrowser(ServletUtils.getBrowser(request)); | ||||||
|         LogContext logContext = LogContextHolder.get(); |         loginUser.setLoginTime(LocalDateTime.now()); | ||||||
|         loginUser.setLoginTime(null != logContext ? logContext.getCreateTime() : LocalDateTime.now()); |         loginUser.setOs(StrUtil.subBefore(ServletUtils.getOs(request), " or", false)); | ||||||
|         // 登录并缓存用户信息 |         // 登录并缓存用户信息 | ||||||
|         StpUtil.login(loginUser.getId()); |         StpUtil.login(loginUser.getId()); | ||||||
|         SaHolder.getStorage().set(CacheConstants.LOGIN_USER_KEY, loginUser); |         SaHolder.getStorage().set(CacheConstants.LOGIN_USER_KEY, loginUser); | ||||||
|   | |||||||
| @@ -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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -16,6 +16,12 @@ | |||||||
|     <description>系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)</description> |     <description>系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)</description> | ||||||
|  |  | ||||||
|     <dependencies> |     <dependencies> | ||||||
|  |         <!-- ContiNew Starter 日志模块 - HttpTracePro(Spring Boot Actuator HttpTrace 定制增强版) --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>top.charles7c.continew</groupId> | ||||||
|  |             <artifactId>continew-starter-log-httptrace-pro</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|         <!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) --> |         <!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>top.charles7c.continew</groupId> |             <groupId>top.charles7c.continew</groupId> | ||||||
|   | |||||||
| @@ -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; |  | ||||||
| } |  | ||||||
| @@ -16,30 +16,29 @@ | |||||||
| 
 | 
 | ||||||
| package top.charles7c.continew.admin.monitor.config; | 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.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 |  * @author Charles7c | ||||||
|  * @since 2022/12/24 23:15 |  * @since 2022/12/24 23:15 | ||||||
|  */ |  */ | ||||||
| @EnableWebMvc |  | ||||||
| @Configuration | @Configuration | ||||||
| @RequiredArgsConstructor | @ConditionalOnEnabledLog | ||||||
| public class WebMvcMonitorConfiguration implements WebMvcConfigurer { | public class LogConfiguration { | ||||||
| 
 | 
 | ||||||
|     private final LogInterceptor logInterceptor; |     /** | ||||||
| 
 |      * 日志持久层接口本地实现类 | ||||||
|     @Override |      */ | ||||||
|     public void addInterceptors(InterceptorRegistry registry) { |     @Bean | ||||||
|         registry.addInterceptor(logInterceptor); |     public LogDao logDao(UserService userService, LogMapper logMapper) { | ||||||
|  |         return new LogDaoLocalImpl(userService, logMapper); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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<>(); |  | ||||||
| } |  | ||||||
| @@ -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(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -92,40 +92,40 @@ public class LogDO implements Serializable { | |||||||
|     private String responseBody; |     private String responseBody; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 请求耗时(ms) |      * 耗时(ms) | ||||||
|      */ |      */ | ||||||
|     private Long elapsedTime; |     private Long timeTaken; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 操作状态 |      * IP | ||||||
|      */ |      */ | ||||||
|     private LogStatusEnum status; |     private String ip; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 客户端IP |      * IP 归属地 | ||||||
|      */ |      */ | ||||||
|     private String clientIp; |     private String address; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * IP归属地 |  | ||||||
|      */ |  | ||||||
|     private String location; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 浏览器 |      * 浏览器 | ||||||
|      */ |      */ | ||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 操作系统 | ||||||
|  |      */ | ||||||
|  |     private String os; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 状态 | ||||||
|  |      */ | ||||||
|  |     private LogStatusEnum status; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 错误信息 |      * 错误信息 | ||||||
|      */ |      */ | ||||||
|     private String errorMsg; |     private String errorMsg; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 异常详情 |  | ||||||
|      */ |  | ||||||
|     private String exceptionDetail; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 创建人 |      * 创建人 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -53,13 +53,13 @@ public class LoginLogResp extends LogResp { | |||||||
|      * 登录 IP |      * 登录 IP | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "登录 IP", example = "192.168.0.1") |     @Schema(description = "登录 IP", example = "192.168.0.1") | ||||||
|     private String clientIp; |     private String ip; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录地点 |      * 登录地点 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "登录地点", example = "中国北京北京市") |     @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") |     @Schema(description = "浏览器", example = "Chrome 115.0.0.0") | ||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 操作系统 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "操作系统", example = "Windows 10") | ||||||
|  |     private String os; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 错误信息 |      * 错误信息 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -50,22 +50,16 @@ public class OperationLogResp extends LogResp { | |||||||
|     private String module; |     private String module; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 操作状态 |      * 操作 IP | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "操作状态(1:成功;2:失败)", type = "Integer", allowableValues = {"1", "2"}, example = "1") |     @Schema(description = "操作 IP", example = "192.168.0.1") | ||||||
|     private LogStatusEnum status; |     private String ip; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 操作IP |  | ||||||
|      */ |  | ||||||
|     @Schema(description = "操作IP", example = "192.168.0.1") |  | ||||||
|     private String clientIp; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 操作地点 |      * 操作地点 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "操作地点", example = "中国北京北京市") |     @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") |     @Schema(description = "浏览器", example = "Chrome 115.0.0.0") | ||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 操作状态 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "操作状态(1:成功;2:失败)", type = "Integer", allowableValues = {"1", "2"}, example = "1") | ||||||
|  |     private LogStatusEnum status; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 错误信息 |      * 错误信息 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -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; |     private String requestHeaders; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -78,16 +78,16 @@ public class SystemLogDetailResp extends LogResp { | |||||||
|     private String responseBody; |     private String responseBody; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 客户端IP |      * IP | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "客户端IP", example = "192.168.0.1") |     @Schema(description = "IP", example = "192.168.0.1") | ||||||
|     private String clientIp; |     private String ip; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * IP归属地 |      * 地址 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "IP归属地", example = "中国北京北京市") |     @Schema(description = "地址", example = "中国北京北京市") | ||||||
|     private String location; |     private String address; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 浏览器 |      * 浏览器 | ||||||
| @@ -96,8 +96,14 @@ public class SystemLogDetailResp extends LogResp { | |||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 请求耗时(ms) |      * 操作系统 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "请求耗时(ms)", example = "58") |     @Schema(description = "操作系统", example = "Windows 10") | ||||||
|     private Long elapsedTime; |     private String os; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 耗时(ms) | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "耗时(ms)", example = "58") | ||||||
|  |     private Long timeTaken; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -54,16 +54,16 @@ public class SystemLogResp extends LogResp { | |||||||
|     private String requestUrl; |     private String requestUrl; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 客户端IP |      * IP | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "客户端IP", example = "192.168.0.1") |     @Schema(description = "IP", example = "192.168.0.1") | ||||||
|     private String clientIp; |     private String ip; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * IP归属地 |      * 地址 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "IP归属地", example = "中国北京北京市") |     @Schema(description = "地址", example = "中国北京北京市") | ||||||
|     private String location; |     private String address; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 浏览器 |      * 浏览器 | ||||||
| @@ -72,20 +72,8 @@ public class SystemLogResp extends LogResp { | |||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 请求耗时(ms) |      * 耗时(ms) | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "请求耗时(ms)", example = "58") |     @Schema(description = "耗时(ms)", example = "58") | ||||||
|     private Long elapsedTime; |     private Long timeTaken; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 错误信息 |  | ||||||
|      */ |  | ||||||
|     @Schema(description = "错误信息") |  | ||||||
|     private String errorMsg; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 异常详情 |  | ||||||
|      */ |  | ||||||
|     @Schema(description = "异常详情") |  | ||||||
|     private String exceptionDetail; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -23,8 +23,6 @@ import java.util.stream.Collectors; | |||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||||
|  |  | ||||||
| import org.springframework.context.event.EventListener; |  | ||||||
| import org.springframework.scheduling.annotation.Async; |  | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
|  |  | ||||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||||
| @@ -63,27 +61,18 @@ public class LogServiceImpl implements LogService { | |||||||
|     private final LogMapper logMapper; |     private final LogMapper logMapper; | ||||||
|     private final CommonUserService commonUserService; |     private final CommonUserService commonUserService; | ||||||
|  |  | ||||||
|     @Async |  | ||||||
|     @EventListener |  | ||||||
|     public void save(LogDO logDO) { |  | ||||||
|         logMapper.insert(logDO); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public PageDataResp<OperationLogResp> page(OperationLogQuery query, PageQuery pageQuery) { |     public PageDataResp<OperationLogResp> page(OperationLogQuery query, PageQuery pageQuery) { | ||||||
|         QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query); |         QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query); | ||||||
|  |  | ||||||
|         // 限定查询信息 |         // 限定查询信息 | ||||||
|         List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(OperationLogResp.class); |         List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(OperationLogResp.class); | ||||||
|         List<String> columnNameList = |         List<String> columnNameList = | ||||||
|             fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX)) |             fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX)) | ||||||
|                 .map(StrUtil::toUnderlineCase).collect(Collectors.toList()); |                 .map(StrUtil::toUnderlineCase).collect(Collectors.toList()); | ||||||
|         queryWrapper.select(columnNameList); |         queryWrapper.select(columnNameList); | ||||||
|  |  | ||||||
|         // 分页查询 |         // 分页查询 | ||||||
|         IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); |         IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); | ||||||
|         PageDataResp<OperationLogResp> pageDataResp = PageDataResp.build(page, OperationLogResp.class); |         PageDataResp<OperationLogResp> pageDataResp = PageDataResp.build(page, OperationLogResp.class); | ||||||
|  |  | ||||||
|         // 填充数据(如果是查询个人操作日志,只查询一次用户信息即可) |         // 填充数据(如果是查询个人操作日志,只查询一次用户信息即可) | ||||||
|         if (null != query.getUid()) { |         if (null != query.getUid()) { | ||||||
|             String nickname = ExceptionUtils.exToNull(() -> commonUserService.getNicknameById(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) { |     public PageDataResp<LoginLogResp> page(LoginLogQuery query, PageQuery pageQuery) { | ||||||
|         QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query); |         QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query); | ||||||
|         queryWrapper.eq("module", "登录"); |         queryWrapper.eq("module", "登录"); | ||||||
|  |  | ||||||
|         // 限定查询信息 |         // 限定查询信息 | ||||||
|         List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(LoginLogResp.class); |         List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(LoginLogResp.class); | ||||||
|         List<String> columnNameList = |         List<String> columnNameList = | ||||||
|             fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX)) |             fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX)) | ||||||
|                 .map(StrUtil::toUnderlineCase).collect(Collectors.toList()); |                 .map(StrUtil::toUnderlineCase).collect(Collectors.toList()); | ||||||
|         queryWrapper.select(columnNameList); |         queryWrapper.select(columnNameList); | ||||||
|  |  | ||||||
|         // 分页查询 |         // 分页查询 | ||||||
|         IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); |         IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); | ||||||
|         PageDataResp<LoginLogResp> pageDataResp = PageDataResp.build(page, LoginLogResp.class); |         PageDataResp<LoginLogResp> pageDataResp = PageDataResp.build(page, LoginLogResp.class); | ||||||
|  |  | ||||||
|         // 填充数据 |         // 填充数据 | ||||||
|         pageDataResp.getList().forEach(this::fill); |         pageDataResp.getList().forEach(this::fill); | ||||||
|         return pageDataResp; |         return pageDataResp; | ||||||
| @@ -118,18 +104,15 @@ public class LogServiceImpl implements LogService { | |||||||
|     @Override |     @Override | ||||||
|     public PageDataResp<SystemLogResp> page(SystemLogQuery query, PageQuery pageQuery) { |     public PageDataResp<SystemLogResp> page(SystemLogQuery query, PageQuery pageQuery) { | ||||||
|         QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query); |         QueryWrapper<LogDO> queryWrapper = QueryHelper.build(query); | ||||||
|  |  | ||||||
|         // 限定查询信息 |         // 限定查询信息 | ||||||
|         List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(SystemLogResp.class); |         List<String> fieldNameList = ReflectUtils.getNonStaticFieldsName(SystemLogResp.class); | ||||||
|         List<String> columnNameList = |         List<String> columnNameList = | ||||||
|             fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX)) |             fieldNameList.stream().filter(n -> !n.endsWith(SysConstants.DESCRIPTION_FIELD_SUFFIX)) | ||||||
|                 .map(StrUtil::toUnderlineCase).collect(Collectors.toList()); |                 .map(StrUtil::toUnderlineCase).collect(Collectors.toList()); | ||||||
|         queryWrapper.select(columnNameList); |         queryWrapper.select(columnNameList); | ||||||
|  |  | ||||||
|         // 分页查询 |         // 分页查询 | ||||||
|         IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); |         IPage<LogDO> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); | ||||||
|         PageDataResp<SystemLogResp> pageDataResp = PageDataResp.build(page, SystemLogResp.class); |         PageDataResp<SystemLogResp> pageDataResp = PageDataResp.build(page, SystemLogResp.class); | ||||||
|  |  | ||||||
|         // 填充数据 |         // 填充数据 | ||||||
|         pageDataResp.getList().forEach(this::fill); |         pageDataResp.getList().forEach(this::fill); | ||||||
|         return pageDataResp; |         return pageDataResp; | ||||||
|   | |||||||
| @@ -66,13 +66,13 @@ public class OnlineUserResp implements Serializable { | |||||||
|      * 登录 IP |      * 登录 IP | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "登录 IP", example = "192.168.0.1") |     @Schema(description = "登录 IP", example = "192.168.0.1") | ||||||
|     private String clientIp; |     private String ip; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录地点 |      * 登录地点 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "登录地点", example = "中国北京北京市") |     @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") |     @Schema(description = "浏览器", example = "Chrome 115.0.0.0") | ||||||
|     private String browser; |     private String browser; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 操作系统 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "操作系统", example = "Windows 10") | ||||||
|  |     private String os; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录时间 |      * 登录时间 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -5,9 +5,10 @@ const BASE_URL = '/monitor/log'; | |||||||
|  |  | ||||||
| export interface LogRecord { | export interface LogRecord { | ||||||
|   id?: number; |   id?: number; | ||||||
|   clientIp: string; |   ip: string; | ||||||
|   location: string; |   address: string; | ||||||
|   browser: string; |   browser: string; | ||||||
|  |   os: string; | ||||||
|   createTime: string; |   createTime: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -22,7 +23,7 @@ export interface OperationLogRecord extends LogRecord { | |||||||
|   module: string; |   module: string; | ||||||
|   description: string; |   description: string; | ||||||
|   status: number; |   status: number; | ||||||
|   errorMsg: string; |   errorMsgString: string; | ||||||
|   createUserString: string; |   createUserString: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -30,8 +31,7 @@ export interface SystemLogRecord extends LogRecord { | |||||||
|   statusCode: number; |   statusCode: number; | ||||||
|   requestMethod: string; |   requestMethod: string; | ||||||
|   requestUrl: string; |   requestUrl: string; | ||||||
|   elapsedTime: number; |   timeTaken: number; | ||||||
|   exceptionDetail?: string; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface SystemLogDetailRecord extends SystemLogRecord { | export interface SystemLogDetailRecord extends SystemLogRecord { | ||||||
|   | |||||||
| @@ -7,9 +7,10 @@ export interface DataRecord { | |||||||
|   token: string; |   token: string; | ||||||
|   username: string; |   username: string; | ||||||
|   nickname: string; |   nickname: string; | ||||||
|   clientIp: string; |   ip: string; | ||||||
|   location: string; |   address: string; | ||||||
|   browser: string; |   browser: string; | ||||||
|  |   os: string; | ||||||
|   loginTime: string; |   loginTime: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -72,9 +72,10 @@ | |||||||
|               </a-tooltip> |               </a-tooltip> | ||||||
|             </template> |             </template> | ||||||
|           </a-table-column> |           </a-table-column> | ||||||
|           <a-table-column title="登录 IP" data-index="clientIp" /> |           <a-table-column title="登录 IP" data-index="ip" /> | ||||||
|           <a-table-column title="登录地点" data-index="location" /> |           <a-table-column title="登录地点" data-index="address" /> | ||||||
|           <a-table-column title="浏览器" data-index="browser" /> |           <a-table-column title="浏览器" data-index="browser" /> | ||||||
|  |           <a-table-column title="终端系统" data-index="os" /> | ||||||
|           <a-table-column title="登录时间" data-index="createTime" /> |           <a-table-column title="登录时间" data-index="createTime" /> | ||||||
|         </template> |         </template> | ||||||
|       </a-table> |       </a-table> | ||||||
|   | |||||||
| @@ -83,8 +83,8 @@ | |||||||
|               </a-tooltip> |               </a-tooltip> | ||||||
|             </template> |             </template> | ||||||
|           </a-table-column> |           </a-table-column> | ||||||
|           <a-table-column title="操作 IP" data-index="clientIp" /> |           <a-table-column title="操作 IP" data-index="ip" /> | ||||||
|           <a-table-column title="操作地点" data-index="location" /> |           <a-table-column title="操作地点" data-index="address" /> | ||||||
|           <a-table-column title="浏览器" data-index="browser" /> |           <a-table-column title="浏览器" data-index="browser" /> | ||||||
|         </template> |         </template> | ||||||
|       </a-table> |       </a-table> | ||||||
|   | |||||||
| @@ -74,21 +74,21 @@ | |||||||
|               }}</span> |               }}</span> | ||||||
|             </template> |             </template> | ||||||
|           </a-table-column> |           </a-table-column> | ||||||
|           <a-table-column title="客户端 IP" data-index="clientIp" /> |           <a-table-column title="IP" data-index="ip" /> | ||||||
|           <a-table-column title="IP 归属地" data-index="location" /> |           <a-table-column title="地址" data-index="address" /> | ||||||
|           <a-table-column title="浏览器" data-index="browser" /> |           <a-table-column title="浏览器" data-index="browser" /> | ||||||
|           <a-table-column title="请求耗时"> |           <a-table-column title="耗时"> | ||||||
|             <template #cell="{ record }"> |             <template #cell="{ record }"> | ||||||
|               <a-tag v-if="record.elapsedTime > 500" color="red" |               <a-tag v-if="record.timeTaken > 500" color="red" | ||||||
|                 >{{ record.elapsedTime }} ms</a-tag |                 >{{ record.timeTaken }} ms</a-tag | ||||||
|               > |               > | ||||||
|               <a-tag v-else-if="record.elapsedTime > 200" color="orange" |               <a-tag v-else-if="record.timeTaken > 200" color="orange" | ||||||
|                 >{{ record.elapsedTime }} ms</a-tag |                 >{{ 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> |             </template> | ||||||
|           </a-table-column> |           </a-table-column> | ||||||
|           <a-table-column title="创建时间" data-index="createTime" /> |           <a-table-column title="请求时间" data-index="createTime" /> | ||||||
|           <a-table-column title="操作" align="center"> |           <a-table-column title="操作" align="center"> | ||||||
|             <template #cell="{ record }"> |             <template #cell="{ record }"> | ||||||
|               <a-button |               <a-button | ||||||
| @@ -99,15 +99,6 @@ | |||||||
|               > |               > | ||||||
|                 <template #icon><icon-eye /></template>详情 |                 <template #icon><icon-eye /></template>详情 | ||||||
|               </a-button> |               </a-button> | ||||||
|               <a-button |  | ||||||
|                 v-if="record.exceptionDetail" |  | ||||||
|                 type="text" |  | ||||||
|                 size="small" |  | ||||||
|                 title="查看异常详情" |  | ||||||
|                 @click="toExceptionDetail(record)" |  | ||||||
|               > |  | ||||||
|                 <template #icon><icon-bug /></template>异常 |  | ||||||
|               </a-button> |  | ||||||
|             </template> |             </template> | ||||||
|           </a-table-column> |           </a-table-column> | ||||||
|         </template> |         </template> | ||||||
| @@ -125,11 +116,11 @@ | |||||||
|       > |       > | ||||||
|         <div style="margin: 10px 0 0 10px"> |         <div style="margin: 10px 0 0 10px"> | ||||||
|           <a-descriptions title="基础信息" :column="2" bordered> |           <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 v-if="loading" :animation="true"> | ||||||
|                 <a-skeleton-line :widths="['200px']" :rows="1" /> |                 <a-skeleton-line :widths="['200px']" :rows="1" /> | ||||||
|               </a-skeleton> |               </a-skeleton> | ||||||
|               <span v-else>{{ systemLog.clientIp }}</span> |               <span v-else>{{ systemLog.ip }}</span> | ||||||
|             </a-descriptions-item> |             </a-descriptions-item> | ||||||
|             <a-descriptions-item label="浏览器"> |             <a-descriptions-item label="浏览器"> | ||||||
|               <a-skeleton v-if="loading" :animation="true"> |               <a-skeleton v-if="loading" :animation="true"> | ||||||
| @@ -137,34 +128,40 @@ | |||||||
|               </a-skeleton> |               </a-skeleton> | ||||||
|               <span v-else>{{ systemLog.browser }}</span> |               <span v-else>{{ systemLog.browser }}</span> | ||||||
|             </a-descriptions-item> |             </a-descriptions-item> | ||||||
|             <a-descriptions-item label="IP 归属地"> |             <a-descriptions-item label="地址"> | ||||||
|               <a-skeleton v-if="loading" :animation="true"> |               <a-skeleton v-if="loading" :animation="true"> | ||||||
|                 <a-skeleton-line :widths="['200px']" :rows="1" /> |                 <a-skeleton-line :widths="['200px']" :rows="1" /> | ||||||
|               </a-skeleton> |               </a-skeleton> | ||||||
|               <span v-else>{{ systemLog.location }}</span> |               <span v-else>{{ systemLog.address }}</span> | ||||||
|             </a-descriptions-item> |             </a-descriptions-item> | ||||||
|             <a-descriptions-item label="请求耗时"> |             <a-descriptions-item label="操作系统"> | ||||||
|               <a-skeleton v-if="loading" :animation="true"> |               <a-skeleton v-if="loading" :animation="true"> | ||||||
|                 <a-skeleton-line :widths="['200px']" :rows="1" /> |                 <a-skeleton-line :widths="['200px']" :rows="1" /> | ||||||
|               </a-skeleton> |               </a-skeleton> | ||||||
|               <span v-else> |               <span v-else>{{ systemLog.os }}</span> | ||||||
|                 <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> |  | ||||||
|             </a-descriptions-item> |             </a-descriptions-item> | ||||||
|             <a-descriptions-item label="创建时间"> |             <a-descriptions-item label="请求时间"> | ||||||
|               <a-skeleton v-if="loading" :animation="true"> |               <a-skeleton v-if="loading" :animation="true"> | ||||||
|                 <a-skeleton-line :widths="['200px']" :rows="1" /> |                 <a-skeleton-line :widths="['200px']" :rows="1" /> | ||||||
|               </a-skeleton> |               </a-skeleton> | ||||||
|               <span v-else>{{ systemLog.createTime }}</span> |               <span v-else>{{ systemLog.createTime }}</span> | ||||||
|             </a-descriptions-item> |             </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> | ||||||
|           <a-descriptions |           <a-descriptions | ||||||
|             title="协议信息" |             title="协议信息" | ||||||
| @@ -256,20 +253,6 @@ | |||||||
|           </a-descriptions> |           </a-descriptions> | ||||||
|         </div> |         </div> | ||||||
|       </a-drawer> |       </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> |     </a-card> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -297,17 +280,16 @@ | |||||||
|     statusCode: 200, |     statusCode: 200, | ||||||
|     responseHeaders: '', |     responseHeaders: '', | ||||||
|     responseBody: '', |     responseBody: '', | ||||||
|     elapsedTime: 0, |     timeTaken: 0, | ||||||
|     clientIp: '', |     ip: '', | ||||||
|     location: '', |     address: '', | ||||||
|     browser: '', |     browser: '', | ||||||
|  |     os: '', | ||||||
|     createTime: '', |     createTime: '', | ||||||
|   }); |   }); | ||||||
|   const total = ref(0); |   const total = ref(0); | ||||||
|   const exceptionDetail = ref(''); |  | ||||||
|   const loading = ref(false); |   const loading = ref(false); | ||||||
|   const visible = ref(false); |   const visible = ref(false); | ||||||
|   const exceptionDetailVisible = ref(false); |  | ||||||
|  |  | ||||||
|   const data = reactive({ |   const data = reactive({ | ||||||
|     // 查询参数 |     // 查询参数 | ||||||
| @@ -362,24 +344,6 @@ | |||||||
|     visible.value = false; |     visible.value = false; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 查看异常详情 |  | ||||||
|    * |  | ||||||
|    * @param record 记录信息 |  | ||||||
|    */ |  | ||||||
|   const toExceptionDetail = async (record: SystemLogRecord) => { |  | ||||||
|     exceptionDetail.value = record.exceptionDetail || ''; |  | ||||||
|     exceptionDetailVisible.value = true; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 关闭异常详情 |  | ||||||
|    */ |  | ||||||
|   const handleExceptionDetailCancel = () => { |  | ||||||
|     exceptionDetail.value = ''; |  | ||||||
|     exceptionDetailVisible.value = false; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * 查询 |    * 查询 | ||||||
|    */ |    */ | ||||||
|   | |||||||
| @@ -63,9 +63,10 @@ | |||||||
|               {{ record.nickname }}({{ record.username }}) |               {{ record.nickname }}({{ record.username }}) | ||||||
|             </template> |             </template> | ||||||
|           </a-table-column> |           </a-table-column> | ||||||
|           <a-table-column title="登录 IP" data-index="clientIp" /> |           <a-table-column title="登录 IP" data-index="ip" /> | ||||||
|           <a-table-column title="登录地点" data-index="location" /> |           <a-table-column title="登录地点" data-index="address" /> | ||||||
|           <a-table-column title="浏览器" data-index="browser" /> |           <a-table-column title="浏览器" data-index="browser" /> | ||||||
|  |           <a-table-column title="终端系统" data-index="os" /> | ||||||
|           <a-table-column title="登录时间" data-index="loginTime" /> |           <a-table-column title="登录时间" data-index="loginTime" /> | ||||||
|           <a-table-column |           <a-table-column | ||||||
|             v-if="checkPermission(['monitor:online:user:delete'])" |             v-if="checkPermission(['monitor:online:user:delete'])" | ||||||
|   | |||||||
| @@ -40,8 +40,8 @@ | |||||||
|             </a-tooltip> |             </a-tooltip> | ||||||
|           </template> |           </template> | ||||||
|         </a-table-column> |         </a-table-column> | ||||||
|         <a-table-column title="操作 IP" data-index="clientIp" /> |         <a-table-column title="操作 IP" data-index="ip" /> | ||||||
|         <a-table-column title="操作地点" data-index="location" /> |         <a-table-column title="操作地点" data-index="address" /> | ||||||
|         <a-table-column title="浏览器" data-index="browser" /> |         <a-table-column title="浏览器" data-index="browser" /> | ||||||
|       </template> |       </template> | ||||||
|       <template #pagination-left> |       <template #pagination-left> | ||||||
|   | |||||||
| @@ -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.model.dto.LoginUser; | ||||||
| import top.charles7c.continew.admin.common.util.SecureUtils; | import top.charles7c.continew.admin.common.util.SecureUtils; | ||||||
| import top.charles7c.continew.admin.common.util.helper.LoginHelper; | 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.model.resp.UserDetailResp; | ||||||
| import top.charles7c.continew.admin.system.service.UserService; | import top.charles7c.continew.admin.system.service.UserService; | ||||||
| import top.charles7c.continew.starter.cache.redisson.util.RedisUtils; | import top.charles7c.continew.starter.cache.redisson.util.RedisUtils; | ||||||
| import top.charles7c.continew.starter.core.util.ExceptionUtils; | import top.charles7c.continew.starter.core.util.ExceptionUtils; | ||||||
| import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.resp.R; | import top.charles7c.continew.starter.extension.crud.model.resp.R; | ||||||
|  | import top.charles7c.continew.starter.log.common.annotation.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 认证 API |  * 认证 API | ||||||
| @@ -115,9 +115,10 @@ public class AuthController { | |||||||
|     @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", |     @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", | ||||||
|         in = ParameterIn.HEADER) |         in = ParameterIn.HEADER) | ||||||
|     @PostMapping("/logout") |     @PostMapping("/logout") | ||||||
|     public R logout() { |     public R<Object> logout() { | ||||||
|  |         Object loginId = StpUtil.getLoginId(-1L); | ||||||
|         StpUtil.logout(); |         StpUtil.logout(); | ||||||
|         return R.ok(); |         return R.ok(loginId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Log(ignore = true) |     @Log(ignore = true) | ||||||
|   | |||||||
| @@ -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.model.resp.LoginResp; | ||||||
| import top.charles7c.continew.admin.auth.service.LoginService; | 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.exception.BadRequestException; | ||||||
| import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.resp.R; | 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.AuthCallback; | ||||||
| import me.zhyd.oauth.model.AuthResponse; | import me.zhyd.oauth.model.AuthResponse; | ||||||
|   | |||||||
| @@ -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.config.properties.LocalStorageProperties; | ||||||
| import top.charles7c.continew.admin.common.constant.CacheConstants; | import top.charles7c.continew.admin.common.constant.CacheConstants; | ||||||
| import top.charles7c.continew.admin.common.model.resp.LabelValueResp; | 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.DeptQuery; | ||||||
| import top.charles7c.continew.admin.system.model.query.MenuQuery; | import top.charles7c.continew.admin.system.model.query.MenuQuery; | ||||||
| import top.charles7c.continew.admin.system.model.query.OptionQuery; | 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.data.mybatis.plus.enums.IBaseEnum; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.query.SortQuery; | 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.extension.crud.model.resp.R; | ||||||
|  | import top.charles7c.continew.starter.log.common.annotation.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 公共 API |  * 公共 API | ||||||
|   | |||||||
| @@ -31,7 +31,6 @@ import org.springframework.web.bind.annotation.PathVariable; | |||||||
| import org.springframework.web.bind.annotation.RequestMapping; | import org.springframework.web.bind.annotation.RequestMapping; | ||||||
| import org.springframework.web.bind.annotation.RestController; | 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.DashboardAccessTrendResp; | ||||||
| import top.charles7c.continew.admin.monitor.model.resp.DashboardGeoDistributionResp; | import top.charles7c.continew.admin.monitor.model.resp.DashboardGeoDistributionResp; | ||||||
| import top.charles7c.continew.admin.monitor.model.resp.DashboardPopularModuleResp; | 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.admin.system.model.resp.DashboardAnnouncementResp; | ||||||
| import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.resp.R; | import top.charles7c.continew.starter.extension.crud.model.resp.R; | ||||||
|  | import top.charles7c.continew.starter.log.common.annotation.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 仪表盘 API |  * 仪表盘 API | ||||||
|   | |||||||
| @@ -29,7 +29,6 @@ import org.springframework.web.bind.annotation.PathVariable; | |||||||
| import org.springframework.web.bind.annotation.RequestMapping; | import org.springframework.web.bind.annotation.RequestMapping; | ||||||
| import org.springframework.web.bind.annotation.RestController; | 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.LoginLogQuery; | ||||||
| import top.charles7c.continew.admin.monitor.model.query.OperationLogQuery; | import top.charles7c.continew.admin.monitor.model.query.OperationLogQuery; | ||||||
| import top.charles7c.continew.admin.monitor.model.query.SystemLogQuery; | 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.query.PageQuery; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp; | 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.extension.crud.model.resp.R; | ||||||
|  | import top.charles7c.continew.starter.log.common.annotation.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 日志管理 API |  * 日志管理 API | ||||||
|   | |||||||
| @@ -29,7 +29,6 @@ import org.springframework.validation.annotation.Validated; | |||||||
| import org.springframework.web.bind.annotation.*; | import org.springframework.web.bind.annotation.*; | ||||||
|  |  | ||||||
| import top.charles7c.continew.admin.common.util.helper.LoginHelper; | 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.query.MessageQuery; | ||||||
| import top.charles7c.continew.admin.system.model.resp.MessageResp; | import top.charles7c.continew.admin.system.model.resp.MessageResp; | ||||||
| import top.charles7c.continew.admin.system.model.resp.MessageUnreadResp; | 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.query.PageQuery; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp; | 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.extension.crud.model.resp.R; | ||||||
|  | import top.charles7c.continew.starter.log.common.annotation.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 消息管理 API |  * 消息管理 API | ||||||
|   | |||||||
| @@ -25,19 +25,21 @@ project: | |||||||
| --- ### 日志配置(重叠部分,优先级高于 logback-spring.xml 中的配置) | --- ### 日志配置(重叠部分,优先级高于 logback-spring.xml 中的配置) | ||||||
| logging: | logging: | ||||||
|   config: classpath:logback-spring.xml |   config: classpath:logback-spring.xml | ||||||
|   ## 系统日志配置 | ## 日志配置 | ||||||
|   system: | continew-starter: | ||||||
|     # 是否启用系统日志 |   log: | ||||||
|     enabled: true |     enabled: true | ||||||
|     # 是否记录内网 IP 操作 |     include: | ||||||
|     includeInnerIp: true |       - DESCRIPTION | ||||||
|     # 排除请求方式 |       - MODULE | ||||||
|     #excludeMethods: |       - REQUEST_HEADERS | ||||||
|     #  - GET |       - REQUEST_BODY | ||||||
|     # 脱敏字段 |       - IP_ADDRESS | ||||||
|     desensitizeFields: |       - BROWSER | ||||||
|       - password |       - OS | ||||||
|       - Authorization |       - RESPONSE_HEADERS | ||||||
|  |       - RESPONSE_BODY | ||||||
|  |       - TIME_TAKEN | ||||||
|  |  | ||||||
| --- ### 接口文档配置 | --- ### 接口文档配置 | ||||||
| springdoc: | springdoc: | ||||||
|   | |||||||
| @@ -123,17 +123,17 @@ CREATE TABLE IF NOT EXISTS `sys_log` ( | |||||||
|     `status_code`      int          NOT NULL                    COMMENT '状态码', |     `status_code`      int          NOT NULL                    COMMENT '状态码', | ||||||
|     `response_headers` text         DEFAULT NULL                COMMENT '响应头', |     `response_headers` text         DEFAULT NULL                COMMENT '响应头', | ||||||
|     `response_body`    mediumtext   DEFAULT NULL                COMMENT '响应体', |     `response_body`    mediumtext   DEFAULT NULL                COMMENT '响应体', | ||||||
|     `elapsed_time`     bigint(20)   NOT NULL                    COMMENT '请求耗时(ms)', |     `time_taken`       bigint(20)   NOT NULL                    COMMENT '耗时(ms)', | ||||||
|     `status`           tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '操作状态(1:成功;2:失败)', |     `ip`               varchar(100) DEFAULT NULL                COMMENT 'IP', | ||||||
|     `client_ip`        varchar(100) DEFAULT NULL                COMMENT '客户端IP', |     `address`          varchar(255) DEFAULT NULL                COMMENT 'IP归属地', | ||||||
|     `location`         varchar(255) DEFAULT NULL                COMMENT 'IP归属地', |  | ||||||
|     `browser`          varchar(100) DEFAULT NULL                COMMENT '浏览器', |     `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 '错误信息', |     `error_msg`        text         DEFAULT NULL                COMMENT '错误信息', | ||||||
|     `exception_detail` mediumtext   DEFAULT NULL                COMMENT '异常详情', |  | ||||||
|     `create_user`      bigint(20)   DEFAULT NULL                COMMENT '创建人', |     `create_user`      bigint(20)   DEFAULT NULL                COMMENT '创建人', | ||||||
|     `create_time`      datetime     NOT NULL                    COMMENT '创建时间', |     `create_time`      datetime     NOT NULL                    COMMENT '创建时间', | ||||||
|     PRIMARY KEY (`id`) USING BTREE, |     PRIMARY KEY (`id`) USING BTREE, | ||||||
|     INDEX `idx_module`(`module`) 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 |     INDEX `idx_create_time`(`create_time`) USING BTREE | ||||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表'; | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表'; | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -12,7 +12,7 @@ | |||||||
|     <parent> |     <parent> | ||||||
|         <groupId>top.charles7c.continew</groupId> |         <groupId>top.charles7c.continew</groupId> | ||||||
|         <artifactId>continew-starter</artifactId> |         <artifactId>continew-starter</artifactId> | ||||||
|         <version>1.0.1-SNAPSHOT</version> |         <version>1.1.0-SNAPSHOT</version> | ||||||
|     </parent> |     </parent> | ||||||
|  |  | ||||||
|     <groupId>top.charles7c.continew</groupId> |     <groupId>top.charles7c.continew</groupId> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user