新增:新增系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等),新增操作日志引擎,记录 HTTP 请求信息
This commit is contained in:
parent
78e84e8941
commit
727850933f
43
README.md
43
README.md
@ -77,22 +77,43 @@ continew-admin # 全局通用项目配置及依赖版本管理
|
||||
│ │ └─ ContinewAdminApplication.java # 启动入口
|
||||
│ └─ resources # 工程配置目录
|
||||
│ └─ db.changelog.v0.0.1 # 数据库脚本文件
|
||||
├─ continew-admin-system # 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)
|
||||
├─ continew-admin-monitor # 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)
|
||||
│ └─ src
|
||||
│ └─ main
|
||||
│ ├─ java # 工程源文件代码目录
|
||||
│ │ └─ top
|
||||
│ │ └─ charles7c
|
||||
│ │ └─ cnadmin
|
||||
│ │ ├─ auth # 认证相关业务及配置
|
||||
│ │ │ ├─ config # 认证相关配置
|
||||
│ │ └─ monitor
|
||||
│ │ ├─ annotation # 系统监控相关注解
|
||||
│ │ ├─ config # 系统监控相关配置
|
||||
│ │ │ └─ properties # 系统监控相关配置属性
|
||||
│ │ ├─ enums # 系统监控相关枚举
|
||||
│ │ ├─ filter # 系统监控相关过滤器
|
||||
│ │ ├─ interceptor # 系统监控相关拦截器
|
||||
│ │ ├─ mapper # 系统监控相关 Mapper
|
||||
│ │ ├─ model # 系统监控相关模型
|
||||
│ │ │ └─ entity # 系统监控相关实体对象
|
||||
│ │ └─ service # 系统监控相关业务接口及实现类
|
||||
│ │ └─ impl # 系统监控相关业务实现类
|
||||
│ └─ resources # 工程配置目录
|
||||
│ └─ mapper # MyBatis Mapper XML 文件目录
|
||||
├─ continew-admin-system # 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)
|
||||
│ └─ src
|
||||
│ └─ main
|
||||
│ ├─ java # 工程源文件代码目录
|
||||
│ │ └─ top
|
||||
│ │ └─ charles7c
|
||||
│ │ └─ cnadmin
|
||||
│ │ ├─ auth # 系统认证相关业务及配置
|
||||
│ │ │ ├─ config # 系统认证相关配置
|
||||
│ │ │ │ ├─ satoken # Sa-Token 配置
|
||||
│ │ │ │ └─ properties # 认证相关配置属性
|
||||
│ │ │ ├─ model # 认证相关模型
|
||||
│ │ │ │ ├─ request # 认证相关请求对象
|
||||
│ │ │ │ └─ vo # 认证相关 VO(View Object)
|
||||
│ │ │ └─ service # 认证相关业务接口及实现类
|
||||
│ │ │ └─ impl # 认证相关业务实现类
|
||||
│ │ │ │ └─ properties # 系统认证相关配置属性
|
||||
│ │ │ ├─ model # 系统认证相关模型
|
||||
│ │ │ │ ├─ request # 系统认证相关请求对象
|
||||
│ │ │ │ └─ vo # 系统认证相关 VO(View Object)
|
||||
│ │ │ └─ service # 系统认证相关业务接口及实现类
|
||||
│ │ │ └─ impl # 系统认证相关业务实现类
|
||||
│ │ └─ system # 系统管理相关业务及配置
|
||||
│ │ ├─ mapper # 系统管理相关 Mapper
|
||||
│ │ ├─ model # 系统管理相关模型
|
||||
@ -112,6 +133,7 @@ continew-admin # 全局通用项目配置及依赖版本管理
|
||||
│ ├─ config # 公共配置
|
||||
│ │ ├─ jackson # Jackson 配置
|
||||
│ │ ├─ mybatis # MyBatis Plus 配置
|
||||
│ │ ├─ threadpool # 线程池配置
|
||||
│ │ └─ properties # 公共配置属性
|
||||
│ ├─ consts # 公共常量
|
||||
│ ├─ exception # 公共异常
|
||||
@ -121,7 +143,8 @@ continew-admin # 全局通用项目配置及依赖版本管理
|
||||
│ │ ├─ entity # 公共实体对象
|
||||
│ │ └─ vo # 公共 VO(View Object)
|
||||
│ └─ util # 公共工具类
|
||||
│ └─ helper # 公共 Helper(助手)
|
||||
│ ├─ helper # 公共 Helper(助手)
|
||||
│ └─ holder # 公共 Holder(持有者)
|
||||
```
|
||||
|
||||
### License
|
||||
|
@ -110,6 +110,12 @@ limitations under the License.
|
||||
</dependency>
|
||||
|
||||
<!-- ################ 工具库相关 ################ -->
|
||||
<!-- 第三方封装 Ip2region(离线 IP 数据管理框架和定位库,支持亿级别的数据段,10 微秒级别的查询性能,提供了许多主流编程语言的 xdb 数据管理引擎的实现) -->
|
||||
<dependency>
|
||||
<groupId>net.dreamlu</groupId>
|
||||
<artifactId>mica-ip2region</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j(前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案) -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.cnadmin.common.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.exception.ServiceException;
|
||||
|
||||
/**
|
||||
* 异步任务执行配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 22:33
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@EnableAsync(proxyTargetClass = true)
|
||||
public class AsyncConfiguration implements AsyncConfigurer {
|
||||
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
/**
|
||||
* 异步任务执行时,使用 Java 内置线程池
|
||||
*/
|
||||
@Override
|
||||
public Executor getAsyncExecutor() {
|
||||
return scheduledExecutorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步任务执行时的异常处理
|
||||
*/
|
||||
@Override
|
||||
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
|
||||
return (throwable, method, objects) -> {
|
||||
throwable.printStackTrace();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Exception message - ").append(throwable.getMessage()).append(", Method name - ")
|
||||
.append(method.getName());
|
||||
if (ArrayUtil.isNotEmpty(objects)) {
|
||||
sb.append(", Parameter value - ").append(Arrays.toString(objects));
|
||||
}
|
||||
throw new ServiceException(sb.toString());
|
||||
};
|
||||
}
|
||||
}
|
@ -25,6 +25,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
|
||||
/**
|
||||
* 项目配置属性
|
||||
*
|
||||
@ -72,4 +75,13 @@ public class ContinewAdminProperties {
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private License license;
|
||||
|
||||
/**
|
||||
* 是否本地解析 IP 归属地
|
||||
*/
|
||||
public static final boolean IP_ADDR_LOCAL_PARSE_ENABLED;
|
||||
|
||||
static {
|
||||
IP_ADDR_LOCAL_PARSE_ENABLED = Convert.toBool(SpringUtil.getProperty("continew-admin.ipAddrLocalParseEnabled"));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.cnadmin.common.config.threadpool;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import top.charles7c.cnadmin.common.util.ExceptionUtils;
|
||||
|
||||
/**
|
||||
* 线程池配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 23:13
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class ThreadPoolConfiguration {
|
||||
|
||||
private final ThreadPoolProperties threadPoolProperties;
|
||||
/** 核心(最小)线程数 = CPU 核心数 + 1 */
|
||||
private final int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
|
||||
|
||||
/**
|
||||
* Spring 内置线程池:ThreadPoolTaskExecutor
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
|
||||
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
// 核心(最小)线程数
|
||||
executor.setCorePoolSize(corePoolSize);
|
||||
// 最大线程数
|
||||
executor.setMaxPoolSize(corePoolSize * 2);
|
||||
// 队列容量
|
||||
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
|
||||
// 活跃时间
|
||||
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
|
||||
// 配置当池内线程数已达到上限的时候,该如何处理新任务:不在新线程中执行任务,而是由调用者所在的线程来执行
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Java 内置线程池:ScheduledExecutorService(适用于执行周期性或定时任务)
|
||||
*/
|
||||
@Bean
|
||||
public ScheduledExecutorService scheduledExecutorService() {
|
||||
return new ScheduledThreadPoolExecutor(corePoolSize,
|
||||
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
|
||||
new ThreadPoolExecutor.CallerRunsPolicy()) {
|
||||
|
||||
@Override
|
||||
protected void afterExecute(Runnable runnable, Throwable throwable) {
|
||||
super.afterExecute(runnable, throwable);
|
||||
ExceptionUtils.printException(runnable, throwable);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.cnadmin.common.config.threadpool;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 线程池配置属性
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 23:06
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "thread-pool")
|
||||
public class ThreadPoolProperties {
|
||||
|
||||
/**
|
||||
* 队列容量
|
||||
*/
|
||||
private int queueCapacity;
|
||||
|
||||
/**
|
||||
* 活跃时间
|
||||
*/
|
||||
private int keepAliveSeconds;
|
||||
}
|
@ -38,9 +38,12 @@ import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.exception.BadRequestException;
|
||||
import top.charles7c.cnadmin.common.exception.ServiceException;
|
||||
import top.charles7c.cnadmin.common.model.dto.OperationLog;
|
||||
import top.charles7c.cnadmin.common.model.vo.R;
|
||||
import top.charles7c.cnadmin.common.util.ExceptionUtils;
|
||||
import top.charles7c.cnadmin.common.util.StreamUtils;
|
||||
import top.charles7c.cnadmin.common.util.holder.LogContextHolder;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
@ -58,6 +61,7 @@ public class GlobalExceptionHandler {
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R handleException(Exception e, HttpServletRequest request) {
|
||||
this.setException(e);
|
||||
log.error("请求地址'{}',发生未知异常", request.getRequestURI(), e);
|
||||
return R.fail(e.getMessage());
|
||||
}
|
||||
@ -68,10 +72,22 @@ public class GlobalExceptionHandler {
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public R handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||
this.setException(e);
|
||||
log.error("请求地址'{}',发生系统异常", request.getRequestURI(), e);
|
||||
return R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截业务异常
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(ServiceException.class)
|
||||
public R handleServiceException(ServiceException e, HttpServletRequest request) {
|
||||
this.setException(e);
|
||||
log.error("请求地址'{}',发生业务异常", request.getRequestURI(), e);
|
||||
return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截自定义验证异常-错误请求
|
||||
*/
|
||||
@ -147,4 +163,17 @@ public class GlobalExceptionHandler {
|
||||
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", request.getRequestURI(), e.getMessage());
|
||||
return R.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源");
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志保存异常信息
|
||||
*
|
||||
* @param e
|
||||
* 异常信息
|
||||
*/
|
||||
private void setException(Exception e) {
|
||||
OperationLog operationLog = LogContextHolder.get();
|
||||
if (operationLog != null) {
|
||||
operationLog.setException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.cnadmin.common.model.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 操作日志
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/25 8:59
|
||||
*/
|
||||
@Data
|
||||
public class OperationLog {
|
||||
|
||||
/**
|
||||
* 操作人
|
||||
*/
|
||||
private Long createUser;
|
||||
|
||||
/**
|
||||
* 操作时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 异常
|
||||
*/
|
||||
private Exception exception;
|
||||
|
||||
}
|
@ -16,10 +16,14 @@
|
||||
|
||||
package top.charles7c.cnadmin.common.util;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 异常工具类
|
||||
@ -27,9 +31,38 @@ import lombok.NoArgsConstructor;
|
||||
* @author Charles7c
|
||||
* @since 2022/12/21 20:56
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ExceptionUtils {
|
||||
|
||||
/**
|
||||
* 打印线程异常信息
|
||||
*
|
||||
* @param runnable
|
||||
* 线程执行内容
|
||||
* @param throwable
|
||||
* 异常
|
||||
*/
|
||||
public static void printException(Runnable runnable, Throwable throwable) {
|
||||
if (throwable == null && runnable instanceof Future<?>) {
|
||||
try {
|
||||
Future<?> future = (Future<?>)runnable;
|
||||
if (future.isDone()) {
|
||||
future.get();
|
||||
}
|
||||
} catch (CancellationException e) {
|
||||
throwable = e;
|
||||
} catch (ExecutionException e) {
|
||||
throwable = e.getCause();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (throwable != null) {
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果有异常,返回 null
|
||||
*
|
||||
|
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.cnadmin.common.util;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.config.properties.ContinewAdminProperties;
|
||||
|
||||
import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
|
||||
import net.dreamlu.mica.ip2region.core.IpInfo;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 20:00
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class IpUtils {
|
||||
|
||||
/**
|
||||
* 太平洋网开放 API,查询 IP 归属地
|
||||
*/
|
||||
private static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true";
|
||||
|
||||
/**
|
||||
* 根据IP获取详细地址
|
||||
*
|
||||
* @param ip
|
||||
* IP地址
|
||||
* @return 详细地址
|
||||
*/
|
||||
public static String getCityInfo(String ip) {
|
||||
if (ContinewAdminProperties.IP_ADDR_LOCAL_PARSE_ENABLED) {
|
||||
return getLocalCityInfo(ip);
|
||||
} else {
|
||||
return getHttpCityInfo(ip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 IP 获取详细地址(网络解析)
|
||||
*
|
||||
* @param ip
|
||||
* IP地址
|
||||
* @return 详细地址
|
||||
*/
|
||||
public static String getHttpCityInfo(String ip) {
|
||||
if (isInnerIP(ip)) {
|
||||
return "内网IP";
|
||||
}
|
||||
String api = String.format(IP_URL, ip);
|
||||
JSONObject object = JSONUtil.parseObj(HttpUtil.get(api));
|
||||
return object.get("addr", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 IP 获取详细地址(本地解析)
|
||||
*
|
||||
* @param ip
|
||||
* IP 地址
|
||||
* @return 详细地址
|
||||
*/
|
||||
public static String getLocalCityInfo(String ip) {
|
||||
if (isInnerIP(ip)) {
|
||||
return "内网IP";
|
||||
}
|
||||
Ip2regionSearcher ip2regionSearcher = SpringUtil.getBean(Ip2regionSearcher.class);
|
||||
IpInfo ipInfo = ip2regionSearcher.memorySearch(ip);
|
||||
if (ipInfo != null) {
|
||||
return ipInfo.getAddress();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为内网IPv4
|
||||
*
|
||||
* @param ip
|
||||
* IP 地址
|
||||
* @return 是否为内网IP
|
||||
*/
|
||||
public static boolean isInnerIP(String ip) {
|
||||
ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip);
|
||||
return NetUtil.isInnerIP(ip);
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.cnadmin.common.util;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
|
||||
/**
|
||||
* Servlet 工具类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 20:00
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ServletUtils {
|
||||
|
||||
/**
|
||||
* 获取请求对象
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static HttpServletRequest getRequest() {
|
||||
return getServletRequestAttributes().getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应对象
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static HttpServletResponse getResponse() {
|
||||
return getServletRequestAttributes().getResponse();
|
||||
}
|
||||
|
||||
private static ServletRequestAttributes getServletRequestAttributes() {
|
||||
return (ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器及其版本信息
|
||||
*
|
||||
* @param request
|
||||
* 请求信息
|
||||
* @return 浏览器及其版本信息
|
||||
*/
|
||||
public static String getBrowser(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
|
||||
return userAgent.getBrowser().getName() + " " + userAgent.getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应所有的头(header)信息
|
||||
*
|
||||
* @param response
|
||||
* 响应对象{@link HttpServletResponse}
|
||||
* @return header值
|
||||
*/
|
||||
public static Map<String, Collection<String>> getHeaderMap(HttpServletResponse response) {
|
||||
final Map<String, Collection<String>> headerMap = new HashMap<>();
|
||||
|
||||
final Collection<String> names = response.getHeaderNames();
|
||||
for (String name : names) {
|
||||
headerMap.put(name, response.getHeaders(name));
|
||||
}
|
||||
return headerMap;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.cnadmin.common.util.holder;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import top.charles7c.cnadmin.common.model.dto.OperationLog;
|
||||
|
||||
/**
|
||||
* 操作日志上下文持有者
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/25 8:55
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class LogContextHolder {
|
||||
|
||||
private static final ThreadLocal<OperationLog> LOG_THREAD_LOCAL = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 存储操作日志
|
||||
*
|
||||
* @param operationLog
|
||||
* 操作日志信息
|
||||
*/
|
||||
public static void set(OperationLog operationLog) {
|
||||
LOG_THREAD_LOCAL.set(operationLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志
|
||||
*
|
||||
* @return 操作日志信息
|
||||
*/
|
||||
public static OperationLog get() {
|
||||
return LOG_THREAD_LOCAL.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除操作日志
|
||||
*/
|
||||
public static void remove() {
|
||||
LOG_THREAD_LOCAL.remove();
|
||||
}
|
||||
}
|
41
continew-admin-monitor/pom.xml
Normal file
41
continew-admin-monitor/pom.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>continew-admin</artifactId>
|
||||
<groupId>top.charles7c</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-admin-monitor</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 公共模块(存放公共工具类,公共配置等) -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c</groupId>
|
||||
<artifactId>continew-admin-common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 操作日志注解(用于接口方法或类上)
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 20:00
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Log {
|
||||
|
||||
/**
|
||||
* 操作日志描述
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 是否忽略日志记录
|
||||
*/
|
||||
boolean ignore() default false;
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
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.cnadmin.monitor.interceptor.LogInterceptor;
|
||||
|
||||
/**
|
||||
* 监控模块 Web MVC 配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/24 23:15
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcMonitorConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private final LogInterceptor logInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(logInterceptor);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.cnadmin.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.operation")
|
||||
public class LogProperties {
|
||||
|
||||
/**
|
||||
* 是否启用操作日志
|
||||
*/
|
||||
private Boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 脱敏字段
|
||||
*/
|
||||
private List<String> desensitize = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 不记录操作日志的请求方式
|
||||
*/
|
||||
private List<String> excludeMethods = new ArrayList<>();
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 操作日志级别枚举
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/25 9:09
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum LogLevelEnum {
|
||||
|
||||
/** 普通 */
|
||||
INFO("普通"),
|
||||
|
||||
/** 错误 */
|
||||
ERROR("错误"),;
|
||||
|
||||
/** 描述 */
|
||||
private final String description;
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
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(HttpServletRequest request, HttpServletResponse response, 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.interceptor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
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.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.model.dto.OperationLog;
|
||||
import top.charles7c.cnadmin.common.util.IpUtils;
|
||||
import top.charles7c.cnadmin.common.util.ServletUtils;
|
||||
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
|
||||
import top.charles7c.cnadmin.common.util.holder.LogContextHolder;
|
||||
import top.charles7c.cnadmin.monitor.annotation.Log;
|
||||
import top.charles7c.cnadmin.monitor.config.properties.LogProperties;
|
||||
import top.charles7c.cnadmin.monitor.enums.LogLevelEnum;
|
||||
import top.charles7c.cnadmin.monitor.model.entity.SysLog;
|
||||
|
||||
/**
|
||||
* 操作日志拦截器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/24 21:14
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LogInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final LogProperties operationLogProperties;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
|
||||
@NotNull Object handler) {
|
||||
if (!checkIsNeedRecord(handler, request)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 记录操作时间
|
||||
this.logCreateTime();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
|
||||
@NotNull Object handler, Exception e) {
|
||||
// 记录请求耗时及异常信息
|
||||
SysLog sysLog = this.logElapsedTimeAndException();
|
||||
if (sysLog == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录描述
|
||||
this.logDescription(sysLog, handler);
|
||||
// 记录请求信息
|
||||
this.logRequest(sysLog, request);
|
||||
// 记录响应信息
|
||||
this.logResponse(sysLog, response);
|
||||
|
||||
// 保存操作日志
|
||||
SpringUtil.getApplicationContext().publishEvent(sysLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作时间
|
||||
*/
|
||||
private void logCreateTime() {
|
||||
OperationLog operationLog = new OperationLog();
|
||||
operationLog.setCreateUser(LoginHelper.getUserId());
|
||||
operationLog.setCreateTime(new Date());
|
||||
LogContextHolder.set(operationLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求耗时及异常信息
|
||||
*
|
||||
* @return 日志信息
|
||||
*/
|
||||
private SysLog logElapsedTimeAndException() {
|
||||
OperationLog operationLog = LogContextHolder.get();
|
||||
if (operationLog != null) {
|
||||
LogContextHolder.remove();
|
||||
SysLog sysLog = new SysLog();
|
||||
sysLog.setCreateTime(operationLog.getCreateTime());
|
||||
sysLog.setElapsedTime(System.currentTimeMillis() - sysLog.getCreateTime().getTime());
|
||||
sysLog.setLogLevel(LogLevelEnum.INFO);
|
||||
|
||||
// 记录异常信息
|
||||
Exception exception = operationLog.getException();
|
||||
if (exception != null) {
|
||||
sysLog.setLogLevel(LogLevelEnum.ERROR);
|
||||
sysLog.setException(ExceptionUtil.stacktraceToString(operationLog.getException(), -1));
|
||||
}
|
||||
return sysLog;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志描述
|
||||
*
|
||||
* @param sysLog
|
||||
* 日志信息
|
||||
* @param handler
|
||||
* 处理器
|
||||
*/
|
||||
private void logDescription(@NotNull SysLog sysLog, Object handler) {
|
||||
HandlerMethod handlerMethod = (HandlerMethod)handler;
|
||||
Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class);
|
||||
Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class);
|
||||
|
||||
if (methodOperation != null) {
|
||||
sysLog.setDescription(
|
||||
StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定操作日志描述");
|
||||
}
|
||||
// 例如:@Log("获取验证码") -> 获取验证码
|
||||
if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) {
|
||||
sysLog.setDescription(methodLog.value());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求信息
|
||||
*
|
||||
* @param sysLog
|
||||
* 日志信息
|
||||
* @param request
|
||||
* 请求对象
|
||||
*/
|
||||
private void logRequest(@NotNull SysLog sysLog, @NotNull HttpServletRequest request) {
|
||||
sysLog.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString()
|
||||
: request.getRequestURL().append("?").append(request.getQueryString()).toString());
|
||||
sysLog.setRequestMethod(request.getMethod());
|
||||
sysLog.setRequestHeader(this.desensitize(ServletUtil.getHeaderMap(request)));
|
||||
String requestBody = this.getRequestBody(request);
|
||||
if (StrUtil.isNotBlank(requestBody)) {
|
||||
sysLog.setRequestBody(this.desensitize(
|
||||
JSONUtil.isTypeJSON(requestBody) ? JSONUtil.parseObj(requestBody) : ServletUtil.getParamMap(request)));
|
||||
}
|
||||
sysLog.setRequestIp(ServletUtil.getClientIP(request));
|
||||
sysLog.setLocation(IpUtils.getCityInfo(sysLog.getRequestIp()));
|
||||
sysLog.setBrowser(ServletUtils.getBrowser(request));
|
||||
sysLog.setCreateUser(sysLog.getCreateUser() == null ? LoginHelper.getUserId() : sysLog.getCreateUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录响应信息
|
||||
*
|
||||
* @param sysLog
|
||||
* 日志信息
|
||||
* @param response
|
||||
* 响应对象
|
||||
*/
|
||||
private void logResponse(SysLog sysLog, HttpServletResponse response) {
|
||||
sysLog.setStatusCode(response.getStatus());
|
||||
sysLog.setResponseHeader(this.desensitize(ServletUtils.getHeaderMap(response)));
|
||||
// 响应体(不记录非 JSON 响应数据)
|
||||
String responseBody = this.getResponseBody(response);
|
||||
if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) {
|
||||
sysLog.setResponseBody(responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据脱敏
|
||||
*
|
||||
* @param waitDesensitizeData
|
||||
* 待脱敏数据
|
||||
* @return 脱敏后的 JSON 字符串数据
|
||||
*/
|
||||
private String desensitize(Map waitDesensitizeData) {
|
||||
String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
|
||||
try {
|
||||
if (CollUtil.isEmpty(waitDesensitizeData)) {
|
||||
return desensitizeDataStr;
|
||||
}
|
||||
|
||||
for (String desensitizeProperty : operationLogProperties.getDesensitize()) {
|
||||
waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> "****************");
|
||||
waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> "****************");
|
||||
waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> "****************");
|
||||
}
|
||||
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 (wrapper != null) {
|
||||
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 (wrapper != null) {
|
||||
responseBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
|
||||
}
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否要记录操作日志
|
||||
*
|
||||
* @param handler
|
||||
* /
|
||||
* @param request
|
||||
* /
|
||||
* @return true 需要记录,false 不需要记录
|
||||
*/
|
||||
private boolean checkIsNeedRecord(Object handler, HttpServletRequest request) {
|
||||
// 1、未启用时,不需要记录操作日志
|
||||
if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2、排除不需要记录日志的接口
|
||||
HandlerMethod handlerMethod = (HandlerMethod)handler;
|
||||
Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class);
|
||||
// 2.1 请求方式不要求记录且请求上没有 @Log 注解,则不记录操作日志
|
||||
if (operationLogProperties.getExcludeMethods().contains(request.getMethod()) && methodLog == null) {
|
||||
return false;
|
||||
}
|
||||
// 2.2 如果接口上既没有 @Log 注解,也没有 @Operation 注解,则不记录操作日志
|
||||
Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class);
|
||||
if (methodLog == null && methodOperation == null) {
|
||||
return false;
|
||||
}
|
||||
// 2.3 如果接口被隐藏,不记录操作日志
|
||||
if (methodOperation != null && methodOperation.hidden()) {
|
||||
return false;
|
||||
}
|
||||
// 2.4 如果接口上有 @Log 注解,但是要求忽略该接口,则不记录操作日志
|
||||
return methodLog == null || !methodLog.ignore();
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import top.charles7c.cnadmin.monitor.model.entity.SysLog;
|
||||
|
||||
/**
|
||||
* 操作日志 Mapper
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/22 21:47
|
||||
*/
|
||||
public interface LogMapper extends BaseMapper<SysLog> {}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.model.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import top.charles7c.cnadmin.monitor.enums.LogLevelEnum;
|
||||
|
||||
/**
|
||||
* 操作日志实体
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/25 9:11
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_log")
|
||||
public class SysLog implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 日志ID
|
||||
*/
|
||||
@TableId
|
||||
private Long logId;
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
private LogLevelEnum logLevel;
|
||||
|
||||
/**
|
||||
* 日志描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 请求 URL
|
||||
*/
|
||||
private String requestUrl;
|
||||
|
||||
/**
|
||||
* 请求方式
|
||||
*/
|
||||
private String requestMethod;
|
||||
|
||||
/**
|
||||
* 请求头
|
||||
*/
|
||||
private String requestHeader;
|
||||
|
||||
/**
|
||||
* 请求体
|
||||
*/
|
||||
private String requestBody;
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private Integer statusCode;
|
||||
|
||||
/**
|
||||
* 响应头
|
||||
*/
|
||||
private String responseHeader;
|
||||
|
||||
/**
|
||||
* 响应体
|
||||
*/
|
||||
private String responseBody;
|
||||
|
||||
/**
|
||||
* 请求耗时(ms)
|
||||
*/
|
||||
private Long elapsedTime;
|
||||
|
||||
/**
|
||||
* 请求IP
|
||||
*/
|
||||
private String requestIp;
|
||||
|
||||
/**
|
||||
* 操作地址
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 浏览器
|
||||
*/
|
||||
private String browser;
|
||||
|
||||
/**
|
||||
* 异常
|
||||
*/
|
||||
private String exception;
|
||||
|
||||
/**
|
||||
* 操作人
|
||||
*/
|
||||
private Long createUser;
|
||||
|
||||
/**
|
||||
* 操作时间
|
||||
*/
|
||||
private Date createTime;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.service;
|
||||
|
||||
/**
|
||||
* 操作日志业务接口
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 20:12
|
||||
*/
|
||||
public interface LogService {
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.cnadmin.monitor.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import top.charles7c.cnadmin.monitor.mapper.LogMapper;
|
||||
import top.charles7c.cnadmin.monitor.model.entity.SysLog;
|
||||
import top.charles7c.cnadmin.monitor.service.LogService;
|
||||
|
||||
/**
|
||||
* 操作日志业务实现类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 20:12
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LogServiceImpl implements LogService {
|
||||
|
||||
private final LogMapper logMapper;
|
||||
|
||||
@Async
|
||||
@EventListener
|
||||
public void save(SysLog sysLog) {
|
||||
logMapper.insert(sysLog);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="top.charles7c.cnadmin.monitor.mapper.LogMapper">
|
||||
</mapper>
|
@ -44,6 +44,12 @@ limitations under the License.
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c</groupId>
|
||||
<artifactId>continew-admin-monitor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c</groupId>
|
||||
|
@ -19,6 +19,8 @@ continew-admin:
|
||||
license:
|
||||
name: Apache-2.0
|
||||
url: https://github.com/Charles7c/continew-admin/blob/dev/LICENSE
|
||||
# 是否本地解析 IP 归属地
|
||||
ipAddrLocalParseEnabled: false
|
||||
|
||||
--- ### 日志配置(重叠部分,优先级高于 logback-spring.xml 中的配置)
|
||||
logging:
|
||||
@ -27,6 +29,17 @@ logging:
|
||||
file:
|
||||
path: @logging.file.path@
|
||||
config: classpath:logback-spring.xml
|
||||
## 操作日志配置
|
||||
operation:
|
||||
# 是否启用操作日志
|
||||
enabled: true
|
||||
# 不记录操作日志的请求方式
|
||||
#excludeMethods:
|
||||
# - GET
|
||||
# 脱敏字段
|
||||
desensitize:
|
||||
- password
|
||||
- Authorization
|
||||
|
||||
--- ### 接口文档配置
|
||||
springdoc:
|
||||
@ -169,4 +182,13 @@ spring:
|
||||
# 反序列化配置(JSON -> Bean)
|
||||
deserialization:
|
||||
# 允许反序列化不存在的属性
|
||||
fail_on_unknown_properties: false
|
||||
fail_on_unknown_properties: false
|
||||
|
||||
--- ### 线程池配置
|
||||
thread-pool:
|
||||
# 是否启用线程池
|
||||
enabled: true
|
||||
# 队列容量
|
||||
queueCapacity: 128
|
||||
# 活跃时间
|
||||
keepAliveSeconds: 300
|
||||
|
@ -22,4 +22,27 @@ CREATE TABLE IF NOT EXISTS `sys_user` (
|
||||
UNIQUE INDEX `uk_email`(`email`) USING BTREE,
|
||||
INDEX `idx_createUser`(`create_user`) USING BTREE,
|
||||
INDEX `idx_updateUser`(`update_user`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||
|
||||
-- changeset Charles7c:2
|
||||
CREATE TABLE IF NOT EXISTS `sys_log` (
|
||||
`log_id` bigint(20) unsigned AUTO_INCREMENT COMMENT '日志ID',
|
||||
`log_level` varchar(255) DEFAULT NULL COMMENT '日志级别',
|
||||
`description` varchar(255) DEFAULT NULL COMMENT '日志描述',
|
||||
`request_url` varchar(512) NOT NULL DEFAULT '' COMMENT '请求URL',
|
||||
`request_method` varchar(10) DEFAULT NULL COMMENT '请求方式',
|
||||
`request_header` text COMMENT '请求头',
|
||||
`request_body` text DEFAULT NULL COMMENT '请求体',
|
||||
`status_code` int(11) unsigned DEFAULT NULL COMMENT '状态码',
|
||||
`response_header` text DEFAULT NULL COMMENT '响应头',
|
||||
`response_body` text DEFAULT NULL COMMENT '响应体',
|
||||
`elapsed_time` bigint(20) unsigned DEFAULT NULL COMMENT '请求耗时(ms)',
|
||||
`request_ip` varchar(255) DEFAULT NULL COMMENT '请求IP',
|
||||
`location` varchar(512) DEFAULT NULL COMMENT '操作地址',
|
||||
`browser` varchar(255) DEFAULT NULL COMMENT '浏览器',
|
||||
`exception` text DEFAULT NULL COMMENT '异常',
|
||||
`create_user` bigint(20) unsigned DEFAULT NULL COMMENT '操作人',
|
||||
`create_time` datetime NOT NULL COMMENT '操作时间',
|
||||
PRIMARY KEY (`log_id`) USING BTREE,
|
||||
INDEX `idx_createUser`(`create_user`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
|
||||
|
16
pom.xml
16
pom.xml
@ -30,6 +30,7 @@ limitations under the License.
|
||||
|
||||
<modules>
|
||||
<module>continew-admin-webapi</module>
|
||||
<module>continew-admin-monitor</module>
|
||||
<module>continew-admin-system</module>
|
||||
<module>continew-admin-common</module>
|
||||
</modules>
|
||||
@ -50,6 +51,7 @@ limitations under the License.
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
|
||||
<!-- ### 工具库相关 ### -->
|
||||
<ip2region.version>2.7.6</ip2region.version>
|
||||
<knife4j.version>4.0.0</knife4j.version>
|
||||
<redisson.version>3.19.0</redisson.version>
|
||||
<easy-captcha.version>1.6.2</easy-captcha.version>
|
||||
@ -118,6 +120,13 @@ limitations under the License.
|
||||
</dependency>
|
||||
|
||||
<!-- ################ 工具库相关 ################ -->
|
||||
<!-- 第三方封装 Ip2region(离线 IP 数据管理框架和定位库,支持亿级别的数据段,10 微秒级别的查询性能,提供了许多主流编程语言的 xdb 数据管理引擎的实现) -->
|
||||
<dependency>
|
||||
<groupId>net.dreamlu</groupId>
|
||||
<artifactId>mica-ip2region</artifactId>
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j(前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案) -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
@ -156,6 +165,13 @@ limitations under the License.
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c</groupId>
|
||||
<artifactId>continew-admin-monitor</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c</groupId>
|
||||
|
Loading…
Reference in New Issue
Block a user