mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 22:57:17 +08:00 
			
		
		
		
	新增:个人中心新增查询操作日志功能,优化日志表结构,并支持关闭记录内网 IP 操作
This commit is contained in:
		| @@ -136,6 +136,7 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | ||||
|   │      │        │  └─ controller   | ||||
|   │      │        │    ├─ auth     # 认证相关 API | ||||
|   │      │        │    ├─ common   # 公共相关 API(例如:验证码 API 等) | ||||
|   │      │        │    ├─ monitor  # 系统监控相关 API | ||||
|   │      │        │    └─ system   # 系统管理相关 API | ||||
|   │      │        └─ ContinewAdminApplication.java  # 启动入口 | ||||
|   │      └─ resources   # 工程配置目录 | ||||
| @@ -159,7 +160,9 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | ||||
|   │      │          ├─ interceptor   # 系统监控相关拦截器 | ||||
|   │      │          ├─ mapper        # 系统监控相关 Mapper | ||||
|   │      │          ├─ model         # 系统监控相关模型 | ||||
|   │      │          │  └─ entity       # 系统监控相关实体对象 | ||||
|   │      │          │  ├─ entity       # 系统监控相关实体对象 | ||||
|   │      │          │  ├─ query        # 系统监控相关查询条件 | ||||
|   │      │          │  └─ vo           # 系统监控相关 VO(View Object) | ||||
|   │      │          └─ service       # 系统监控相关业务接口及实现类 | ||||
|   │      │             └─ impl         # 系统监控相关业务实现类 | ||||
|   │      └─ resources   # 工程配置目录 | ||||
| @@ -197,6 +200,7 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | ||||
|   │          └─ charles7c | ||||
|   │            └─ cnadmin | ||||
|   │              └─ common | ||||
|   │                ├─ annotation   # 公共注解 | ||||
|   │                ├─ config       # 公共配置 | ||||
|   │                │  ├─ jackson      # Jackson 配置 | ||||
|   │                │  ├─ mybatis      # MyBatis Plus 配置 | ||||
| @@ -209,6 +213,7 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | ||||
|   │                ├─ model        # 公共模型 | ||||
|   │                │  ├─ dto          # 公共 DTO(Data Transfer Object) | ||||
|   │                │  ├─ entity       # 公共实体对象 | ||||
|   │                │  ├─ query        # 公共查询条件 | ||||
|   │                │  └─ vo           # 公共 VO(View Object) | ||||
|   │                └─ util         # 公共工具类 | ||||
|   │                  ├─ helper        # 公共 Helper(助手) | ||||
| @@ -226,6 +231,7 @@ continew-admin | ||||
|     │  ├─ api               # 请求接口 | ||||
|     │  │  ├─ auth             # 认证模块 | ||||
|     │  │  ├─ common           # 公共模块 | ||||
|     │  │  ├─ monitor          # 系统监控模块 | ||||
|     │  │  └─ system           # 系统管理模块 | ||||
|     │  ├─ assets            # 静态资源 | ||||
|     │  │  ├─ images           # 图片资源 | ||||
|   | ||||
| @@ -0,0 +1,113 @@ | ||||
| /* | ||||
|  * 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.annotation; | ||||
|  | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
|  | ||||
| /** | ||||
|  * 查询注解 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 18:01 | ||||
|  */ | ||||
| @Target(ElementType.FIELD) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface Query { | ||||
|  | ||||
|     /** | ||||
|      * 属性名(默认和使用该注解的属性的名称一致) | ||||
|      */ | ||||
|     String property() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 查询类型(等值查询、模糊查询、范围查询等) | ||||
|      */ | ||||
|     Type type() default Type.EQUAL; | ||||
|  | ||||
|     /** | ||||
|      * 多属性模糊查询,仅支持 String 类型属性,多个属性之间用逗号分隔 | ||||
|      * <p> | ||||
|      * 例如:@Query(blurry = "username,email") 表示根据用户名和邮箱模糊查询 | ||||
|      * </p> | ||||
|      */ | ||||
|     String blurry() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 查询类型 | ||||
|      */ | ||||
|     enum Type { | ||||
|         /** | ||||
|          * 等值查询,例如:WHERE `age` = 18 | ||||
|          */ | ||||
|         EQUAL, | ||||
|         /** | ||||
|          * 非等值查询,例如:WHERE `age` != 18 | ||||
|          */ | ||||
|         NOT_EQUAL, | ||||
|         /** | ||||
|          * 大于查询,例如:WHERE `age` > 18 | ||||
|          */ | ||||
|         GREATER_THAN, | ||||
|         /** | ||||
|          * 小于查询,例如:WHERE `age` < 18 | ||||
|          */ | ||||
|         LESS_THAN, | ||||
|         /** | ||||
|          * 大于等于查询,例如:WHERE `age` >= 18 | ||||
|          */ | ||||
|         GREATER_THAN_OR_EQUAL, | ||||
|         /** | ||||
|          * 小于等于查询,例如:WHERE `age` <= 18 | ||||
|          */ | ||||
|         LESS_THAN_OR_EQUAL, | ||||
|         /** | ||||
|          * 范围查询,例如:WHERE `age` BETWEEN 10 AND 18 | ||||
|          */ | ||||
|         BETWEEN, | ||||
|         /** | ||||
|          * 左模糊查询,例如:WHERE `nickname` LIKE '%张' | ||||
|          */ | ||||
|         LEFT_LIKE, | ||||
|         /** | ||||
|          * 中模糊查询,例如:WHERE `nickname` LIKE '%雪%' | ||||
|          */ | ||||
|         INNER_LIKE, | ||||
|         /** | ||||
|          * 右模糊查询,例如:WHERE `nickname` LIKE '雪%' | ||||
|          */ | ||||
|         RIGHT_LIKE, | ||||
|         /** | ||||
|          * 包含查询,例如:WHERE `age` IN (10, 20, 30) | ||||
|          */ | ||||
|         IN, | ||||
|         /** | ||||
|          * 不包含查询,例如:WHERE `age` NOT IN (20, 30) | ||||
|          */ | ||||
|         NOT_IN, | ||||
|         /** | ||||
|          * 空查询,例如:WHERE `email` IS NULL | ||||
|          */ | ||||
|         IS_NULL, | ||||
|         /** | ||||
|          * 非空查询,例如:WHERE `email` IS NOT NULL | ||||
|          */ | ||||
|         NOT_NULL,; | ||||
|     } | ||||
| } | ||||
| @@ -41,7 +41,7 @@ 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.dto.LogContext; | ||||
| import top.charles7c.cnadmin.common.model.vo.R; | ||||
| import top.charles7c.cnadmin.common.util.ExceptionUtils; | ||||
| import top.charles7c.cnadmin.common.util.StreamUtils; | ||||
| @@ -179,15 +179,15 @@ public class GlobalExceptionHandler { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 操作日志保存异常信息 | ||||
|      * 在系统日志上下文中保存异常信息 | ||||
|      * | ||||
|      * @param e | ||||
|      *            异常信息 | ||||
|      */ | ||||
|     private void setException(Exception e) { | ||||
|         OperationLog operationLog = LogContextHolder.get(); | ||||
|         if (operationLog != null) { | ||||
|             operationLog.setException(e); | ||||
|         LogContext logContext = LogContextHolder.get(); | ||||
|         if (logContext != null) { | ||||
|             logContext.setException(e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -21,13 +21,13 @@ import java.time.LocalDateTime; | ||||
| import lombok.Data; | ||||
| 
 | ||||
| /** | ||||
|  * 操作日志 | ||||
|  * 系统日志上下文 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/25 8:59 | ||||
|  */ | ||||
| @Data | ||||
| public class OperationLog { | ||||
| public class LogContext { | ||||
| 
 | ||||
|     /** | ||||
|      * 操作人 | ||||
| @@ -43,5 +43,4 @@ public class OperationLog { | ||||
|      * 异常 | ||||
|      */ | ||||
|     private Exception exception; | ||||
| 
 | ||||
| } | ||||
| @@ -0,0 +1,127 @@ | ||||
| /* | ||||
|  * 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.query; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import org.springdoc.api.annotations.ParameterObject; | ||||
| import org.springframework.data.domain.Sort; | ||||
|  | ||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | ||||
| import com.baomidou.mybatisplus.core.metadata.OrderItem; | ||||
| import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
|  | ||||
| /** | ||||
|  * 分页查询条件 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 10:59 | ||||
|  */ | ||||
| @Data | ||||
| @ParameterObject | ||||
| @Schema(description = "分页查询条件") | ||||
| public class PageQuery implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 页码 | ||||
|      */ | ||||
|     @Schema(description = "页码") | ||||
|     private int page; | ||||
|  | ||||
|     /** | ||||
|      * 每页记录数 | ||||
|      */ | ||||
|     @Schema(description = "每页记录数") | ||||
|     private int size; | ||||
|  | ||||
|     /** | ||||
|      * 排序条件 | ||||
|      */ | ||||
|     @Schema(description = "排序条件", example = "sort=published,desc&sort=title,asc") | ||||
|     private String[] sort; | ||||
|  | ||||
|     /** 默认页码:1 */ | ||||
|     private static final int DEFAULT_PAGE = 1; | ||||
|  | ||||
|     /** 默认每页记录数:int 最大值 */ | ||||
|     private static final int DEFAULT_SIZE = Integer.MAX_VALUE; | ||||
|     private static final String DELIMITER = ","; | ||||
|  | ||||
|     public PageQuery() { | ||||
|         this.page = DEFAULT_PAGE; | ||||
|         this.size = DEFAULT_SIZE; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解析排序条件为 Spring 分页排序实体 | ||||
|      * | ||||
|      * @return Spring 分页排序实体 | ||||
|      */ | ||||
|     public Sort getSort() { | ||||
|         if (ArrayUtil.isEmpty(sort)) { | ||||
|             return Sort.unsorted(); | ||||
|         } | ||||
|  | ||||
|         List<Sort.Order> orders = new ArrayList<>(sort.length); | ||||
|         if (sort[0].contains(DELIMITER)) { | ||||
|             // e.g "sort=published,desc&sort=title,asc" | ||||
|             for (String s : sort) { | ||||
|                 String[] sortArr = s.split(DELIMITER); | ||||
|                 Sort.Order order = new Sort.Order(Sort.Direction.valueOf(sortArr[1].toUpperCase()), sortArr[0]); | ||||
|                 orders.add(order); | ||||
|             } | ||||
|         } else { | ||||
|             // e.g "sort=published,desc" | ||||
|             Sort.Order order = new Sort.Order(Sort.Direction.valueOf(sort[1].toUpperCase()), sort[0]); | ||||
|             orders.add(order); | ||||
|         } | ||||
|         return Sort.by(orders); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 基于分页查询条件转换为 MyBatis Plus 分页条件 | ||||
|      * | ||||
|      * @param <T> | ||||
|      *            列表数据类型 | ||||
|      * @return MyBatis Plus 分页条件 | ||||
|      */ | ||||
|     public <T> IPage<T> toPage() { | ||||
|         Page<T> mybatisPage = new Page<>(this.getPage(), this.getSize()); | ||||
|         Sort pageSort = this.getSort(); | ||||
|         if (CollUtil.isNotEmpty(pageSort)) { | ||||
|             for (Sort.Order order : pageSort) { | ||||
|                 OrderItem orderItem = new OrderItem(); | ||||
|                 orderItem.setAsc(order.isAscending()); | ||||
|                 orderItem.setColumn(StrUtil.toUnderlineCase(order.getProperty())); | ||||
|                 mybatisPage.addOrder(orderItem); | ||||
|             } | ||||
|         } | ||||
|         return mybatisPage; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,96 @@ | ||||
| /* | ||||
|  * 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.vo; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import lombok.Data; | ||||
| import lombok.experimental.Accessors; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
|  | ||||
| /** | ||||
|  * 分页信息 | ||||
|  * | ||||
|  * @param <V> | ||||
|  *            列表数据类型 | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/14 23:40 | ||||
|  */ | ||||
| @Data | ||||
| @Accessors(chain = true) | ||||
| @Schema(description = "分页信息") | ||||
| public class PageInfo<V> { | ||||
|  | ||||
|     /** | ||||
|      * 列表数据 | ||||
|      */ | ||||
|     @Schema(description = "列表数据") | ||||
|     private List<V> list; | ||||
|  | ||||
|     /** | ||||
|      * 总记录数 | ||||
|      */ | ||||
|     @Schema(description = "总记录数") | ||||
|     private long total; | ||||
|  | ||||
|     /** | ||||
|      * 基于 MyBatis Plus 分页数据构建分页信息,并将源数据转换为指定类型数据 | ||||
|      * | ||||
|      * @param page | ||||
|      *            MyBatis Plus 分页数据 | ||||
|      * @param targetClass | ||||
|      *            目标类型 Class 对象 | ||||
|      * @param <T> | ||||
|      *            源列表数据类型 | ||||
|      * @param <V> | ||||
|      *            目标列表数据类型 | ||||
|      * @return 分页信息 | ||||
|      */ | ||||
|     public static <T, V> PageInfo<V> build(IPage<T> page, Class<V> targetClass) { | ||||
|         if (page == null) { | ||||
|             return null; | ||||
|         } | ||||
|         PageInfo<V> pageInfo = new PageInfo<>(); | ||||
|         pageInfo.setList(BeanUtil.copyToList(page.getRecords(), targetClass)); | ||||
|         pageInfo.setTotal(page.getTotal()); | ||||
|         return pageInfo; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 基于 MyBatis Plus 分页数据构建分页信息 | ||||
|      * | ||||
|      * @param page | ||||
|      *            MyBatis Plus 分页数据 | ||||
|      * @param <V> | ||||
|      *            列表数据类型 | ||||
|      * @return 分页信息 | ||||
|      */ | ||||
|     public static <V> PageInfo<V> build(IPage<V> page) { | ||||
|         if (page == null) { | ||||
|             return null; | ||||
|         } | ||||
|         PageInfo<V> pageInfo = new PageInfo<>(); | ||||
|         pageInfo.setList(page.getRecords()); | ||||
|         pageInfo.setTotal(pageInfo.getTotal()); | ||||
|         return pageInfo; | ||||
|     } | ||||
| } | ||||
| @@ -16,7 +16,6 @@ | ||||
|  | ||||
| package top.charles7c.cnadmin.common.model.vo; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| import lombok.AccessLevel; | ||||
| @@ -36,9 +35,7 @@ import org.springframework.http.HttpStatus; | ||||
| @Data | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| @Schema(description = "响应信息") | ||||
| public class R<V extends Serializable> implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
| public class R<V> { | ||||
|  | ||||
|     /** 是否成功 */ | ||||
|     @Schema(description = "是否成功") | ||||
| @@ -72,39 +69,39 @@ public class R<V extends Serializable> implements Serializable { | ||||
|         this.data = data; | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> ok() { | ||||
|     public static <V> R<V> ok() { | ||||
|         return new R<>(true, SUCCESS_CODE, "操作成功", null); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> ok(V data) { | ||||
|     public static <V> R<V> ok(V data) { | ||||
|         return new R<>(true, SUCCESS_CODE, "操作成功", data); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> ok(String msg) { | ||||
|     public static <V> R<V> ok(String msg) { | ||||
|         return new R<>(true, SUCCESS_CODE, msg, null); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> ok(String msg, V data) { | ||||
|     public static <V> R<V> ok(String msg, V data) { | ||||
|         return new R<>(true, SUCCESS_CODE, msg, data); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> fail() { | ||||
|     public static <V> R<V> fail() { | ||||
|         return new R<>(false, FAIL_CODE, "操作失败", null); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> fail(String msg) { | ||||
|     public static <V> R<V> fail(String msg) { | ||||
|         return new R<>(false, FAIL_CODE, msg, null); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> fail(V data) { | ||||
|     public static <V> R<V> fail(V data) { | ||||
|         return new R<>(false, FAIL_CODE, "操作失败", data); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> fail(String msg, V data) { | ||||
|     public static <V> R<V> fail(String msg, V data) { | ||||
|         return new R<>(false, FAIL_CODE, msg, data); | ||||
|     } | ||||
|  | ||||
|     public static <V extends Serializable> R<V> fail(int code, String msg) { | ||||
|     public static <V> R<V> fail(int code, String msg) { | ||||
|         return new R<>(false, code, msg, null); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,66 @@ | ||||
| /* | ||||
|  * 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.lang.reflect.Field; | ||||
| import java.lang.reflect.Modifier; | ||||
| import java.util.Arrays; | ||||
|  | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import cn.hutool.core.util.ReflectUtil; | ||||
|  | ||||
| /** | ||||
|  * 反射工具类 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 22:05 | ||||
|  */ | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class ReflectUtils { | ||||
|  | ||||
|     /** | ||||
|      * 获得一个类中所有非静态字段名列表,包括其父类中的字段<br> | ||||
|      * 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。 | ||||
|      * | ||||
|      * @param beanClass | ||||
|      *            类 | ||||
|      * @return 非静态字段名列表 | ||||
|      * @throws SecurityException | ||||
|      *             安全检查异常 | ||||
|      */ | ||||
|     public static String[] getNonStaticFieldsName(Class<?> beanClass) throws SecurityException { | ||||
|         Field[] nonStaticFields = getNonStaticFields(beanClass); | ||||
|         return Arrays.stream(nonStaticFields).map(Field::getName).toArray(String[]::new); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得一个类中所有非静态字段列表,包括其父类中的字段<br> | ||||
|      * 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。 | ||||
|      * | ||||
|      * @param beanClass | ||||
|      *            类 | ||||
|      * @return 非静态字段列表 | ||||
|      * @throws SecurityException | ||||
|      *             安全检查异常 | ||||
|      */ | ||||
|     public static Field[] getNonStaticFields(Class<?> beanClass) throws SecurityException { | ||||
|         Field[] fields = ReflectUtil.getFields(beanClass); | ||||
|         return Arrays.stream(fields).filter(f -> !Modifier.isStatic(f.getModifiers())).toArray(Field[]::new); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,216 @@ | ||||
| /* | ||||
|  * 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.helper; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
|  | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||
| import com.baomidou.mybatisplus.core.toolkit.Wrappers; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.annotation.Query; | ||||
|  | ||||
| /** | ||||
|  * 查询助手 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 18:17 | ||||
|  */ | ||||
| @Slf4j | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class QueryHelper { | ||||
|  | ||||
|     /** | ||||
|      * 根据查询条件构建 MyBatis Plus 查询条件封装对象 | ||||
|      * | ||||
|      * @param query | ||||
|      *            查询条件 | ||||
|      * @param <Q> | ||||
|      *            查询条件数据类型 | ||||
|      * @param <R> | ||||
|      *            查询数据类型 | ||||
|      * @return MyBatis Plus 查询条件封装对象 | ||||
|      */ | ||||
|     public static <Q, R> QueryWrapper<R> build(Q query) { | ||||
|         QueryWrapper<R> queryWrapper = Wrappers.query(); | ||||
|         // 没有查询条件,直接返回 | ||||
|         if (query == null) { | ||||
|             return queryWrapper; | ||||
|         } | ||||
|  | ||||
|         // 获取查询条件中所有的属性(包括私有的和父类的) | ||||
|         List<Field> fieldList = getFieldList(query.getClass(), new ArrayList<>()); | ||||
|         fieldList.forEach(field -> buildQuery(query, field, queryWrapper)); | ||||
|         return queryWrapper; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定类的所有属性(包括私有的和父类的) | ||||
|      * | ||||
|      * @param clazz | ||||
|      *            指定类 | ||||
|      * @param fieldList | ||||
|      *            属性列表 | ||||
|      * @param <Q> | ||||
|      *            查询条件数据类型 | ||||
|      * @return 属性列表(包括私有的和父类的) | ||||
|      */ | ||||
|     public static <Q> List<Field> getFieldList(Class<Q> clazz, List<Field> fieldList) { | ||||
|         if (clazz != null) { | ||||
|             fieldList.addAll(Arrays.asList(clazz.getDeclaredFields())); | ||||
|             getFieldList(clazz.getSuperclass(), fieldList); | ||||
|         } | ||||
|         return fieldList; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 构建 MyBatis Plus 查询条件封装对象 | ||||
|      * | ||||
|      * @param query | ||||
|      *            查询条件 | ||||
|      * @param field | ||||
|      *            属性 | ||||
|      * @param queryWrapper | ||||
|      *            MyBatis Plus 查询条件封装对象 | ||||
|      * @param <Q> | ||||
|      *            查询条件数据类型 | ||||
|      * @param <R> | ||||
|      *            查询数据类型 | ||||
|      */ | ||||
|     private static <Q, R> void buildQuery(Q query, Field field, QueryWrapper<R> queryWrapper) { | ||||
|         boolean accessible = field.isAccessible(); | ||||
|         try { | ||||
|             field.setAccessible(true); | ||||
|             // 没有 @Query,直接返回 | ||||
|             Query queryAnnotation = field.getAnnotation(Query.class); | ||||
|             if (queryAnnotation == null) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // 如果属性值为空,直接返回 | ||||
|             Object fieldValue = field.get(query); | ||||
|             if (ObjectUtil.isEmpty(fieldValue)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // 解析查询条件 | ||||
|             parse(queryAnnotation, field.getName(), fieldValue, queryWrapper); | ||||
|         } catch (Exception e) { | ||||
|             log.error(e.getMessage(), e); | ||||
|         } finally { | ||||
|             field.setAccessible(accessible); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解析查询条件 | ||||
|      * | ||||
|      * @param queryAnnotation | ||||
|      *            查询注解 | ||||
|      * @param fieldName | ||||
|      *            属性名 | ||||
|      * @param fieldValue | ||||
|      *            属性值 | ||||
|      * @param queryWrapper | ||||
|      *            MyBatis Plus 查询条件封装对象 | ||||
|      * @param <R> | ||||
|      *            查询数据类型 | ||||
|      */ | ||||
|     private static <R> void parse(Query queryAnnotation, String fieldName, Object fieldValue, | ||||
|         QueryWrapper<R> queryWrapper) { | ||||
|         // 解析多属性模糊查询 | ||||
|         // 如果设置了多属性模糊查询,分割属性进行条件拼接 | ||||
|         String blurry = queryAnnotation.blurry(); | ||||
|         if (StrUtil.isNotBlank(blurry)) { | ||||
|             String[] propertyArr = blurry.split(","); | ||||
|             queryWrapper.and(wrapper -> { | ||||
|                 for (String property : propertyArr) { | ||||
|                     wrapper.or().like(StrUtil.toUnderlineCase(property), fieldValue); | ||||
|                 } | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 解析单个属性查询 | ||||
|         // 如果没有单独指定属性名,就和使用该注解的属性的名称一致 | ||||
|         // 注意:数据库规范中列采用下划线连接法命名,程序规范中变量采用驼峰法命名 | ||||
|         String property = queryAnnotation.property(); | ||||
|         fieldName = StrUtil.isNotBlank(property) ? property : fieldName; | ||||
|         String columnName = StrUtil.toUnderlineCase(fieldName); | ||||
|         switch (queryAnnotation.type()) { | ||||
|             case EQUAL: | ||||
|                 queryWrapper.eq(columnName, fieldValue); | ||||
|                 break; | ||||
|             case NOT_EQUAL: | ||||
|                 queryWrapper.ne(columnName, fieldValue); | ||||
|                 break; | ||||
|             case GREATER_THAN: | ||||
|                 queryWrapper.gt(columnName, fieldValue); | ||||
|                 break; | ||||
|             case LESS_THAN: | ||||
|                 queryWrapper.lt(columnName, fieldValue); | ||||
|                 break; | ||||
|             case GREATER_THAN_OR_EQUAL: | ||||
|                 queryWrapper.ge(columnName, fieldValue); | ||||
|                 break; | ||||
|             case LESS_THAN_OR_EQUAL: | ||||
|                 queryWrapper.le(columnName, fieldValue); | ||||
|                 break; | ||||
|             case BETWEEN: | ||||
|                 List<Object> between = new ArrayList<>((List<Object>)fieldValue); | ||||
|                 queryWrapper.between(columnName, between.get(0), between.get(1)); | ||||
|                 break; | ||||
|             case LEFT_LIKE: | ||||
|                 queryWrapper.likeLeft(columnName, fieldValue); | ||||
|                 break; | ||||
|             case INNER_LIKE: | ||||
|                 queryWrapper.like(columnName, fieldValue); | ||||
|                 break; | ||||
|             case RIGHT_LIKE: | ||||
|                 queryWrapper.likeRight(columnName, fieldValue); | ||||
|                 break; | ||||
|             case IN: | ||||
|                 if (CollUtil.isNotEmpty((List<Object>)fieldValue)) { | ||||
|                     queryWrapper.in(columnName, (List<Object>)fieldValue); | ||||
|                 } | ||||
|                 break; | ||||
|             case NOT_IN: | ||||
|                 if (CollUtil.isNotEmpty((List<Object>)fieldValue)) { | ||||
|                     queryWrapper.notIn(columnName, (List<Object>)fieldValue); | ||||
|                 } | ||||
|                 break; | ||||
|             case IS_NULL: | ||||
|                 queryWrapper.isNull(columnName); | ||||
|                 break; | ||||
|             case NOT_NULL: | ||||
|                 queryWrapper.isNotNull(columnName); | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -19,10 +19,10 @@ package top.charles7c.cnadmin.common.util.holder; | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.model.dto.OperationLog; | ||||
| import top.charles7c.cnadmin.common.model.dto.LogContext; | ||||
|  | ||||
| /** | ||||
|  * 操作日志上下文持有者 | ||||
|  * 系统日志上下文持有者 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/25 8:55 | ||||
| @@ -30,29 +30,29 @@ import top.charles7c.cnadmin.common.model.dto.OperationLog; | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class LogContextHolder { | ||||
|  | ||||
|     private static final ThreadLocal<OperationLog> LOG_THREAD_LOCAL = new ThreadLocal<>(); | ||||
|     private static final ThreadLocal<LogContext> LOG_THREAD_LOCAL = new ThreadLocal<>(); | ||||
|  | ||||
|     /** | ||||
|      * 存储操作日志 | ||||
|      * 存储系统日志上下文 | ||||
|      * | ||||
|      * @param operationLog | ||||
|      *            操作日志信息 | ||||
|      * @param logContext | ||||
|      *            系统日志上下文信息 | ||||
|      */ | ||||
|     public static void set(OperationLog operationLog) { | ||||
|         LOG_THREAD_LOCAL.set(operationLog); | ||||
|     public static void set(LogContext logContext) { | ||||
|         LOG_THREAD_LOCAL.set(logContext); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取操作日志 | ||||
|      * 获取系统日志上下文 | ||||
|      * | ||||
|      * @return 操作日志信息 | ||||
|      * @return 系统日志上下文信息 | ||||
|      */ | ||||
|     public static OperationLog get() { | ||||
|     public static LogContext get() { | ||||
|         return LOG_THREAD_LOCAL.get(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 移除操作日志 | ||||
|      * 移除系统日志上下文 | ||||
|      */ | ||||
|     public static void remove() { | ||||
|         LOG_THREAD_LOCAL.remove(); | ||||
|   | ||||
| @@ -32,10 +32,10 @@ limitations under the License. | ||||
|     <description>系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)</description> | ||||
|  | ||||
|     <dependencies> | ||||
|         <!-- 公共模块(存放公共工具类,公共配置等) --> | ||||
|         <!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) --> | ||||
|         <dependency> | ||||
|             <groupId>top.charles7c</groupId> | ||||
|             <artifactId>continew-admin-common</artifactId> | ||||
|             <artifactId>continew-admin-system</artifactId> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
| </project> | ||||
| @@ -19,7 +19,7 @@ package top.charles7c.cnadmin.monitor.annotation; | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * 操作日志注解(用于接口方法或类上) | ||||
|  * 系统日志注解(用于接口方法或类上) | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/23 20:00 | ||||
| @@ -30,7 +30,7 @@ import java.lang.annotation.*; | ||||
| public @interface Log { | ||||
|  | ||||
|     /** | ||||
|      * 操作日志描述 | ||||
|      * 日志描述 | ||||
|      */ | ||||
|     String value() default ""; | ||||
|  | ||||
|   | ||||
| @@ -25,28 +25,33 @@ 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") | ||||
| @ConfigurationProperties(prefix = "logging.system") | ||||
| public class LogProperties { | ||||
|  | ||||
|     /** | ||||
|      * 是否启用操作日志 | ||||
|      * 是否启用系统日志 | ||||
|      */ | ||||
|     private Boolean enabled = false; | ||||
|     private Boolean enabled; | ||||
|  | ||||
|     /** | ||||
|      * 是否记录内网 IP 操作 | ||||
|      */ | ||||
|     private Boolean includeInnerIp; | ||||
|  | ||||
|     /** | ||||
|      * 哪些请求方式不记录系统日志 | ||||
|      */ | ||||
|     private List<String> excludeMethods = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * 脱敏字段 | ||||
|      */ | ||||
|     private List<String> desensitize = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * 不记录操作日志的请求方式 | ||||
|      */ | ||||
|     private List<String> excludeMethods = new ArrayList<>(); | ||||
| } | ||||
|   | ||||
| @@ -19,22 +19,24 @@ package top.charles7c.cnadmin.monitor.enums; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| 
 | ||||
| import com.baomidou.mybatisplus.annotation.IEnum; | ||||
| 
 | ||||
| /** | ||||
|  * 操作日志级别枚举 | ||||
|  * 操作结果枚举 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/25 9:09 | ||||
|  */ | ||||
| @Getter | ||||
| @RequiredArgsConstructor | ||||
| public enum LogLevelEnum { | ||||
| public enum LogResultEnum implements IEnum<Integer> { | ||||
| 
 | ||||
|     /** 普通 */ | ||||
|     INFO("普通"), | ||||
|     /** 成功 */ | ||||
|     SUCCESS(1, "成功"), | ||||
| 
 | ||||
|     /** 错误 */ | ||||
|     ERROR("错误"),; | ||||
|     /** 失败 */ | ||||
|     FAILURE(2, "失败"),; | ||||
| 
 | ||||
|     /** 描述 */ | ||||
|     private final Integer value; | ||||
|     private final String description; | ||||
| } | ||||
| @@ -42,20 +42,21 @@ 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.http.HttpStatus; | ||||
| import cn.hutool.json.JSONUtil; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.model.dto.OperationLog; | ||||
| import top.charles7c.cnadmin.common.model.dto.LogContext; | ||||
| 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.enums.LogResultEnum; | ||||
| import top.charles7c.cnadmin.monitor.model.entity.SysLog; | ||||
|  | ||||
| /** | ||||
|  * 操作日志拦截器 | ||||
|  * 系统日志拦截器 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/24 21:14 | ||||
| @@ -87,14 +88,14 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 记录描述 | ||||
|         // 记录日志描述 | ||||
|         this.logDescription(sysLog, handler); | ||||
|         // 记录请求信息 | ||||
|         this.logRequest(sysLog, request); | ||||
|         // 记录响应信息 | ||||
|         this.logResponse(sysLog, response); | ||||
|  | ||||
|         // 保存操作日志 | ||||
|         // 保存系统日志 | ||||
|         SpringUtil.getApplicationContext().publishEvent(sysLog); | ||||
|     } | ||||
|  | ||||
| @@ -102,31 +103,31 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|      * 记录操作时间 | ||||
|      */ | ||||
|     private void logCreateTime() { | ||||
|         OperationLog operationLog = new OperationLog(); | ||||
|         operationLog.setCreateUser(LoginHelper.getUserId()); | ||||
|         operationLog.setCreateTime(LocalDateTime.now()); | ||||
|         LogContextHolder.set(operationLog); | ||||
|         LogContext logContext = new LogContext(); | ||||
|         logContext.setCreateUser(LoginHelper.getUserId()); | ||||
|         logContext.setCreateTime(LocalDateTime.now()); | ||||
|         LogContextHolder.set(logContext); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 记录请求耗时及异常信息 | ||||
|      * | ||||
|      * @return 日志信息 | ||||
|      * @return 系统日志信息 | ||||
|      */ | ||||
|     private SysLog logElapsedTimeAndException() { | ||||
|         OperationLog operationLog = LogContextHolder.get(); | ||||
|         if (operationLog != null) { | ||||
|         LogContext logContext = LogContextHolder.get(); | ||||
|         if (logContext != null) { | ||||
|             LogContextHolder.remove(); | ||||
|             SysLog sysLog = new SysLog(); | ||||
|             sysLog.setCreateTime(operationLog.getCreateTime()); | ||||
|             sysLog.setCreateTime(logContext.getCreateTime()); | ||||
|             sysLog.setElapsedTime(System.currentTimeMillis() - LocalDateTimeUtil.toEpochMilli(sysLog.getCreateTime())); | ||||
|             sysLog.setLogLevel(LogLevelEnum.INFO); | ||||
|             sysLog.setResult(LogResultEnum.SUCCESS); | ||||
|  | ||||
|             // 记录异常信息 | ||||
|             Exception exception = operationLog.getException(); | ||||
|             Exception exception = logContext.getException(); | ||||
|             if (exception != null) { | ||||
|                 sysLog.setLogLevel(LogLevelEnum.ERROR); | ||||
|                 sysLog.setException(ExceptionUtil.stacktraceToString(operationLog.getException(), -1)); | ||||
|                 sysLog.setResult(LogResultEnum.FAILURE); | ||||
|                 sysLog.setException(ExceptionUtil.stacktraceToString(exception, -1)); | ||||
|             } | ||||
|             return sysLog; | ||||
|         } | ||||
| @@ -137,7 +138,7 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|      * 记录日志描述 | ||||
|      * | ||||
|      * @param sysLog | ||||
|      *            日志信息 | ||||
|      *            系统日志信息 | ||||
|      * @param handler | ||||
|      *            处理器 | ||||
|      */ | ||||
| @@ -148,7 +149,7 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|  | ||||
|         if (methodOperation != null) { | ||||
|             sysLog.setDescription( | ||||
|                 StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定操作日志描述"); | ||||
|                 StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定日志描述"); | ||||
|         } | ||||
|         // 例如:@Log("获取验证码") -> 获取验证码 | ||||
|         if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) { | ||||
| @@ -160,7 +161,7 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|      * 记录请求信息 | ||||
|      * | ||||
|      * @param sysLog | ||||
|      *            日志信息 | ||||
|      *            系统日志信息 | ||||
|      * @param request | ||||
|      *            请求对象 | ||||
|      */ | ||||
| @@ -184,18 +185,21 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|      * 记录响应信息 | ||||
|      * | ||||
|      * @param sysLog | ||||
|      *            日志信息 | ||||
|      *            系统日志信息 | ||||
|      * @param response | ||||
|      *            响应对象 | ||||
|      */ | ||||
|     private void logResponse(SysLog sysLog, HttpServletResponse response) { | ||||
|         sysLog.setStatusCode(response.getStatus()); | ||||
|         int status = response.getStatus(); | ||||
|         sysLog.setStatusCode(status); | ||||
|         sysLog.setResponseHeader(this.desensitize(ServletUtil.getHeadersMap(response))); | ||||
|         // 响应体(不记录非 JSON 响应数据) | ||||
|         String responseBody = this.getResponseBody(response); | ||||
|         if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) { | ||||
|             sysLog.setResponseBody(responseBody); | ||||
|         } | ||||
|         // 操作失败:>= 400 | ||||
|         sysLog.setResult(status >= HttpStatus.HTTP_BAD_REQUEST ? LogResultEnum.FAILURE : sysLog.getResult()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -258,7 +262,7 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否要记录操作日志 | ||||
|      * 检查是否要记录系统日志 | ||||
|      * | ||||
|      * @param handler | ||||
|      *            / | ||||
| @@ -267,28 +271,34 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|      * @return true 需要记录,false 不需要记录 | ||||
|      */ | ||||
|     private boolean checkIsNeedRecord(Object handler, HttpServletRequest request) { | ||||
|         // 1、未启用时,不需要记录操作日志 | ||||
|         // 1、未启用时,不需要记录系统日志 | ||||
|         if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // 2、排除不需要记录日志的接口 | ||||
|         // 2、检查是否需要记录内网 IP 操作 | ||||
|         boolean isInnerIp = IpUtils.isInnerIP(ServletUtil.getClientIP(request)); | ||||
|         if (isInnerIp && Boolean.FALSE.equals(operationLogProperties.getIncludeInnerIp())) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // 3、排除不需要记录系统日志的接口 | ||||
|         HandlerMethod handlerMethod = (HandlerMethod)handler; | ||||
|         Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class); | ||||
|         // 2.1 请求方式不要求记录且请求上没有 @Log 注解,则不记录操作日志 | ||||
|         // 3.1 请求方式不要求记录且请求上没有 @Log 注解,则不记录系统日志 | ||||
|         if (operationLogProperties.getExcludeMethods().contains(request.getMethod()) && methodLog == null) { | ||||
|             return false; | ||||
|         } | ||||
|         // 2.2 如果接口上既没有 @Log 注解,也没有 @Operation 注解,则不记录操作日志 | ||||
|         // 3.2 如果接口上既没有 @Log 注解,也没有 @Operation 注解,则不记录系统日志 | ||||
|         Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class); | ||||
|         if (methodLog == null && methodOperation == null) { | ||||
|             return false; | ||||
|         } | ||||
|         // 2.3 如果接口被隐藏,不记录操作日志 | ||||
|         // 3.3 如果接口被隐藏,不记录系统日志 | ||||
|         if (methodOperation != null && methodOperation.hidden()) { | ||||
|             return false; | ||||
|         } | ||||
|         // 2.4 如果接口上有 @Log 注解,但是要求忽略该接口,则不记录操作日志 | ||||
|         // 3.4 如果接口上有 @Log 注解,但是要求忽略该接口,则不记录系统日志 | ||||
|         return methodLog == null || !methodLog.ignore(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,7 +21,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||||
| import top.charles7c.cnadmin.monitor.model.entity.SysLog; | ||||
|  | ||||
| /** | ||||
|  * 操作日志 Mapper | ||||
|  * 系统日志 Mapper | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/22 21:47 | ||||
|   | ||||
| @@ -24,10 +24,10 @@ import lombok.Data; | ||||
| import com.baomidou.mybatisplus.annotation.TableId; | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | ||||
|  | ||||
| import top.charles7c.cnadmin.monitor.enums.LogLevelEnum; | ||||
| import top.charles7c.cnadmin.monitor.enums.LogResultEnum; | ||||
|  | ||||
| /** | ||||
|  * 操作日志实体 | ||||
|  * 系统日志实体 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/25 9:11 | ||||
| @@ -44,18 +44,13 @@ public class SysLog implements Serializable { | ||||
|     @TableId | ||||
|     private Long logId; | ||||
|  | ||||
|     /** | ||||
|      * 日志级别 | ||||
|      */ | ||||
|     private LogLevelEnum logLevel; | ||||
|  | ||||
|     /** | ||||
|      * 日志描述 | ||||
|      */ | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 请求 URL | ||||
|      * 请求URL | ||||
|      */ | ||||
|     private String requestUrl; | ||||
|  | ||||
| @@ -95,12 +90,17 @@ public class SysLog implements Serializable { | ||||
|     private Long elapsedTime; | ||||
|  | ||||
|     /** | ||||
|      * 请求IP | ||||
|      * 操作结果(1成功 2失败) | ||||
|      */ | ||||
|     private LogResultEnum result; | ||||
|  | ||||
|     /** | ||||
|      * 操作IP | ||||
|      */ | ||||
|     private String requestIp; | ||||
|  | ||||
|     /** | ||||
|      * 操作地址 | ||||
|      * 操作地点 | ||||
|      */ | ||||
|     private String location; | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,50 @@ | ||||
| /* | ||||
|  * 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.query; | ||||
|  | ||||
| import static top.charles7c.cnadmin.common.annotation.Query.Type; | ||||
|  | ||||
| import java.io.Serializable; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import org.springdoc.api.annotations.ParameterObject; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.annotation.Query; | ||||
|  | ||||
| /** | ||||
|  * 操作日志查询条件 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 11:43 | ||||
|  */ | ||||
| @Data | ||||
| @ParameterObject | ||||
| @Schema(description = "操作日志查询条件") | ||||
| public class OperationLogQuery implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 操作人 | ||||
|      */ | ||||
|     @Schema(description = "操作人") | ||||
|     @Query(property = "createUser", type = Type.EQUAL) | ||||
|     private Long uid; | ||||
| } | ||||
| @@ -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.monitor.model.vo; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
|  | ||||
| import top.charles7c.cnadmin.monitor.enums.LogResultEnum; | ||||
|  | ||||
| /** | ||||
|  * 操作日志信息 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/14 18:27 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "操作日志信息") | ||||
| public class OperationLogVO implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 日志ID | ||||
|      */ | ||||
|     @Schema(description = "日志ID") | ||||
|     private Long logId; | ||||
|  | ||||
|     /** | ||||
|      * 操作内容 | ||||
|      */ | ||||
|     @Schema(description = "操作内容") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 操作结果(1成功 2失败) | ||||
|      */ | ||||
|     @Schema(description = "操作结果(1成功 2失败)", type = "Integer", allowableValues = {"1", "2"}) | ||||
|     private LogResultEnum result; | ||||
|  | ||||
|     /** | ||||
|      * 操作IP | ||||
|      */ | ||||
|     @Schema(description = "操作IP") | ||||
|     private String requestIp; | ||||
|  | ||||
|     /** | ||||
|      * 操作地点 | ||||
|      */ | ||||
|     @Schema(description = "操作地点") | ||||
|     private String location; | ||||
|  | ||||
|     /** | ||||
|      * 浏览器 | ||||
|      */ | ||||
|     @Schema(description = "浏览器") | ||||
|     private String browser; | ||||
|  | ||||
|     /** | ||||
|      * 操作人 | ||||
|      */ | ||||
|     @JsonIgnore | ||||
|     private Long createUser; | ||||
|  | ||||
|     /** | ||||
|      * 操作人 | ||||
|      */ | ||||
|     @Schema(description = "操作人") | ||||
|     private String createUserString; | ||||
|  | ||||
|     /** | ||||
|      * 操作时间 | ||||
|      */ | ||||
|     @Schema(description = "操作时间") | ||||
|     private LocalDateTime createTime; | ||||
| } | ||||
| @@ -17,7 +17,7 @@ | ||||
| package top.charles7c.cnadmin.monitor.service; | ||||
|  | ||||
| /** | ||||
|  * 操作日志业务接口 | ||||
|  * 系统日志业务接口 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/23 20:12 | ||||
|   | ||||
| @@ -0,0 +1,42 @@ | ||||
| /* | ||||
|  * 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; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.model.query.PageQuery; | ||||
| import top.charles7c.cnadmin.common.model.vo.PageInfo; | ||||
| import top.charles7c.cnadmin.monitor.model.query.OperationLogQuery; | ||||
| import top.charles7c.cnadmin.monitor.model.vo.OperationLogVO; | ||||
|  | ||||
| /** | ||||
|  * 操作日志业务接口 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 21:05 | ||||
|  */ | ||||
| public interface OperationLogService { | ||||
|  | ||||
|     /** | ||||
|      * 分页查询列表 | ||||
|      * | ||||
|      * @param query | ||||
|      *            查询条件 | ||||
|      * @param pageQuery | ||||
|      *            分页查询条件 | ||||
|      * @return 分页信息 | ||||
|      */ | ||||
|     PageInfo<OperationLogVO> list(OperationLogQuery query, PageQuery pageQuery); | ||||
| } | ||||
| @@ -28,7 +28,7 @@ import top.charles7c.cnadmin.monitor.model.entity.SysLog; | ||||
| import top.charles7c.cnadmin.monitor.service.LogService; | ||||
|  | ||||
| /** | ||||
|  * 操作日志业务实现类 | ||||
|  * 系统日志业务实现类 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/23 20:12 | ||||
|   | ||||
| @@ -0,0 +1,90 @@ | ||||
| /* | ||||
|  * 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 java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | ||||
|  | ||||
| import cn.hutool.core.util.StrUtil; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.model.query.PageQuery; | ||||
| import top.charles7c.cnadmin.common.model.vo.PageInfo; | ||||
| import top.charles7c.cnadmin.common.util.ReflectUtils; | ||||
| import top.charles7c.cnadmin.common.util.helper.QueryHelper; | ||||
| import top.charles7c.cnadmin.monitor.mapper.LogMapper; | ||||
| import top.charles7c.cnadmin.monitor.model.entity.SysLog; | ||||
| import top.charles7c.cnadmin.monitor.model.query.OperationLogQuery; | ||||
| import top.charles7c.cnadmin.monitor.model.vo.OperationLogVO; | ||||
| import top.charles7c.cnadmin.monitor.service.OperationLogService; | ||||
| import top.charles7c.cnadmin.system.mapper.UserMapper; | ||||
| import top.charles7c.cnadmin.system.model.entity.SysUser; | ||||
|  | ||||
| /** | ||||
|  * 操作日志业务实现类 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/15 21:05 | ||||
|  */ | ||||
| @Slf4j | ||||
| @Service | ||||
| @RequiredArgsConstructor | ||||
| public class OperationLogServiceImpl implements OperationLogService { | ||||
|  | ||||
|     private final LogMapper logMapper; | ||||
|     private final UserMapper userMapper; | ||||
|  | ||||
|     @Override | ||||
|     public PageInfo<OperationLogVO> list(OperationLogQuery query, PageQuery pageQuery) { | ||||
|         QueryWrapper<SysLog> queryWrapper = QueryHelper.build(query); | ||||
|  | ||||
|         // 限定查询信息 | ||||
|         String[] fieldsName = ReflectUtils.getNonStaticFieldsName(OperationLogVO.class); | ||||
|         List<String> columns = Arrays.stream(fieldsName).map(StrUtil::toUnderlineCase) | ||||
|             .filter(n -> !n.endsWith("string")).collect(Collectors.toList()); | ||||
|         queryWrapper.select(columns); | ||||
|  | ||||
|         // 分页查询 | ||||
|         IPage<SysLog> page = logMapper.selectPage(pageQuery.toPage(), queryWrapper); | ||||
|         PageInfo<OperationLogVO> pageInfo = PageInfo.build(page, OperationLogVO.class); | ||||
|         pageInfo.getList().forEach(this::fill); | ||||
|         return pageInfo; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 填充数据 | ||||
|      * | ||||
|      * @param vo | ||||
|      *            VO | ||||
|      */ | ||||
|     private void fill(OperationLogVO vo) { | ||||
|         Long createUser = vo.getCreateUser(); | ||||
|         if (createUser == null) { | ||||
|             return; | ||||
|         } | ||||
|         SysUser sysUser = userMapper.selectById(createUser); | ||||
|         vo.setCreateUserString(sysUser.getNickname()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								continew-admin-ui/src/api/monitor/operation-log.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								continew-admin-ui/src/api/monitor/operation-log.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import axios from 'axios'; | ||||
| import qs from 'query-string'; | ||||
|  | ||||
| export interface OperationLogRecord { | ||||
|   logId: string; | ||||
|   description: string; | ||||
|   result: number, | ||||
|   requestIp: string, | ||||
|   location: string, | ||||
|   browser: string, | ||||
|   createUserString: string; | ||||
|   createTime: string; | ||||
| } | ||||
|  | ||||
| export interface OperationLogParams extends Partial<OperationLogRecord> { | ||||
|   page: number; | ||||
|   size: number; | ||||
|   sort: Array<string>; | ||||
|   uid: string; | ||||
| } | ||||
|  | ||||
| export interface OperationLogListRes { | ||||
|   list: OperationLogRecord[]; | ||||
|   total: number; | ||||
| } | ||||
|  | ||||
| export function queryOperationLogList(params: OperationLogParams) { | ||||
|   return axios.get<OperationLogListRes>('/monitor/log/operation', { | ||||
|     params, | ||||
|     paramsSerializer: (obj) => { | ||||
|       return qs.stringify(obj); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| @@ -90,5 +90,8 @@ body { | ||||
|     &.pass { | ||||
|       background-color: rgb(var(--green-6)); | ||||
|     } | ||||
|     &.fail { | ||||
|       background-color: rgb(var(--red-6)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import useAppStore from '../app'; | ||||
|  | ||||
| const useLoginStore = defineStore('user', { | ||||
|   state: (): UserState => ({ | ||||
|     userId: 1, | ||||
|     userId: '', | ||||
|     username: '', | ||||
|     nickname: '', | ||||
|     gender: 0, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| export type RoleType = '' | '*' | 'admin' | 'user'; | ||||
| export interface UserState { | ||||
|   userId: number; | ||||
|   userId: string; | ||||
|   username: string; | ||||
|   nickname: string; | ||||
|   gender: number; | ||||
|   | ||||
| @@ -133,7 +133,7 @@ | ||||
|     captchaImgBase64.value = data.img | ||||
|   } | ||||
|   onMounted(() => { | ||||
|     getCaptcha() | ||||
|     getCaptcha(); | ||||
|   }) | ||||
|  | ||||
|   // 记住我 | ||||
|   | ||||
| @@ -0,0 +1,142 @@ | ||||
| <template> | ||||
|   <div class="container"> | ||||
|     <a-table | ||||
|       row-key="id" | ||||
|       :loading="loading" | ||||
|       :pagination="pagination" | ||||
|       :columns="columns" | ||||
|       :data="renderData" | ||||
|       :bordered="false" | ||||
|       size="large" | ||||
|       @page-change="onPageChange" | ||||
|     > | ||||
|       <template #index="{ rowIndex }"> | ||||
|         {{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }} | ||||
|       </template> | ||||
|       <template #result="{ record }"> | ||||
|         <a-space v-if="record.result === 1"> | ||||
|           <a-tag color="green"> | ||||
|             <span class="circle pass"></span> | ||||
|             成功 | ||||
|           </a-tag> | ||||
|         </a-space> | ||||
|         <a-space v-else> | ||||
|           <a-tag color="red"> | ||||
|             <span class="circle fail"></span> | ||||
|             失败 | ||||
|           </a-tag> | ||||
|         </a-space> | ||||
|       </template> | ||||
|       <template #pagination-left> | ||||
|         <a-tooltip content="刷新"> | ||||
|           <div class="action-icon" @click="onRefresh"> | ||||
|             <icon-refresh size="18" /> | ||||
|           </div> | ||||
|         </a-tooltip> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { computed, ref, reactive } from "vue"; | ||||
|   import { useLoginStore } from '@/store'; | ||||
|   import useLoading from '@/hooks/loading'; | ||||
|   import { queryOperationLogList, OperationLogRecord, OperationLogParams } from '@/api/monitor/operation-log'; | ||||
|   import { Pagination } from '@/types/global'; | ||||
|   import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'; | ||||
|  | ||||
|   const { loading, setLoading } = useLoading(true); | ||||
|   const loginStore = useLoginStore(); | ||||
|   const renderData = ref<OperationLogRecord[]>([]); | ||||
|  | ||||
|   const basePagination: Pagination = { | ||||
|     current: 1, | ||||
|     pageSize: 10, | ||||
|   }; | ||||
|   const pagination = reactive({ | ||||
|     ...basePagination, | ||||
|   }); | ||||
|   const columns = computed<TableColumnData[]>(() => [ | ||||
|     { | ||||
|       title: '序号', | ||||
|       dataIndex: 'index', | ||||
|       slotName: 'index', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作时间', | ||||
|       dataIndex: 'createTime', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作内容', | ||||
|       dataIndex: 'description', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作结果', | ||||
|       dataIndex: 'result', | ||||
|       slotName: 'result', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作IP', | ||||
|       dataIndex: 'requestIp', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作地点', | ||||
|       dataIndex: 'location', | ||||
|     }, | ||||
|     { | ||||
|       title: '浏览器', | ||||
|       dataIndex: 'browser', | ||||
|     }, | ||||
|   ]); | ||||
|   const fetchData = async ( | ||||
|     params: OperationLogParams = { uid: loginStore.userId, page: 1, size: 10, sort: ['createTime,desc'] } | ||||
|   ) => { | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       const { data } = await queryOperationLogList(params); | ||||
|       renderData.value = data.list; | ||||
|       pagination.current = params.page; | ||||
|       pagination.total = data.total; | ||||
|     } catch (err) { | ||||
|       // you can report use errorHandler or other | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const onPageChange = (current: number) => { | ||||
|     fetchData({ uid: loginStore.userId, page: current, size: pagination.pageSize, sort: ['createTime,desc'] }); | ||||
|   }; | ||||
|  | ||||
|   const onRefresh = () => { | ||||
|     fetchData({ | ||||
|       uid: loginStore.userId, | ||||
|       page: pagination.current, | ||||
|       size: pagination.pageSize, | ||||
|       sort: ['createTime,desc'], | ||||
|     } as unknown as OperationLogParams); | ||||
|   }; | ||||
|  | ||||
|   fetchData(); | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="less"> | ||||
|   .container { | ||||
|     padding: 0 20px 20px 20px; | ||||
|   } | ||||
|   :deep(.arco-table-th) { | ||||
|     &:last-child { | ||||
|       .arco-table-th-item-title { | ||||
|         margin-left: 16px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   .action-icon { | ||||
|     cursor: pointer; | ||||
|   } | ||||
|  | ||||
|   .action-icon:hover { | ||||
|     color: #0960bd; | ||||
|   } | ||||
| </style> | ||||
| @@ -15,6 +15,9 @@ | ||||
|           <a-tab-pane key="2" :title="$t('userCenter.tab.securitySettings')"> | ||||
|             <SecuritySettings /> | ||||
|           </a-tab-pane> | ||||
|           <a-tab-pane key="3" :title="$t('userCenter.tab.operationLog')"> | ||||
|             <OperationLog /> | ||||
|           </a-tab-pane> | ||||
|         </a-tabs> | ||||
|       </a-col> | ||||
|     </a-row> | ||||
| @@ -25,6 +28,7 @@ | ||||
|   import UserPanel from './components/user-panel.vue'; | ||||
|   import BasicInfo from './components/basic-info.vue'; | ||||
|   import SecuritySettings from './components/security-settings.vue'; | ||||
|   import OperationLog from './components/operation-log.vue'; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
|   | ||||
| @@ -2,6 +2,7 @@ export default { | ||||
|   'menu.user.center': 'User Center', | ||||
|   'userCenter.tab.basicInfo': 'Basic Information', | ||||
|   'userCenter.tab.securitySettings': 'Security Settings', | ||||
|   'userCenter.tab.operationLog': 'Operation Log', | ||||
|  | ||||
|   // user-panel | ||||
|   'userCenter.panel.avatar': 'Avatar', | ||||
|   | ||||
| @@ -2,6 +2,7 @@ export default { | ||||
|   'menu.user.center': '个人中心', | ||||
|   'userCenter.tab.basicInfo': '基础信息', | ||||
|   'userCenter.tab.securitySettings': '安全设置', | ||||
|   'userCenter.tab.operationLog': '操作日志', | ||||
|  | ||||
|   // user-panel | ||||
|   'userCenter.panel.avatar': '头像', | ||||
|   | ||||
| @@ -49,12 +49,6 @@ limitations under the License. | ||||
|             <groupId>top.charles7c</groupId> | ||||
|             <artifactId>continew-admin-monitor</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) --> | ||||
|         <dependency> | ||||
|             <groupId>top.charles7c</groupId> | ||||
|             <artifactId>continew-admin-system</artifactId> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
|  | ||||
|     <build> | ||||
|   | ||||
| @@ -0,0 +1,58 @@ | ||||
| /* | ||||
|  * 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.webapi.controller.monitor; | ||||
|  | ||||
| import lombok.RequiredArgsConstructor; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
|  | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.model.query.PageQuery; | ||||
| import top.charles7c.cnadmin.common.model.vo.PageInfo; | ||||
| import top.charles7c.cnadmin.common.model.vo.R; | ||||
| import top.charles7c.cnadmin.monitor.model.query.OperationLogQuery; | ||||
| import top.charles7c.cnadmin.monitor.model.vo.OperationLogVO; | ||||
| import top.charles7c.cnadmin.monitor.service.OperationLogService; | ||||
|  | ||||
| /** | ||||
|  * 操作日志 API | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/14 18:09 | ||||
|  */ | ||||
| @Tag(name = "操作日志 API") | ||||
| @Validated | ||||
| @RestController | ||||
| @RequiredArgsConstructor | ||||
| @RequestMapping(value = "/monitor/log/operation", produces = MediaType.APPLICATION_JSON_VALUE) | ||||
| public class OperationLogController { | ||||
|  | ||||
|     private final OperationLogService operationLogService; | ||||
|  | ||||
|     @Operation(summary = "分页查询操作日志列表") | ||||
|     @GetMapping | ||||
|     public R<PageInfo<OperationLogVO>> list(@Validated OperationLogQuery query, @Validated PageQuery pageQuery) { | ||||
|         PageInfo<OperationLogVO> pageInfo = operationLogService.list(query, pageQuery); | ||||
|         return R.ok(pageInfo); | ||||
|     } | ||||
| } | ||||
| @@ -20,7 +20,7 @@ continew-admin: | ||||
|     name: Apache-2.0 | ||||
|     url: https://github.com/Charles7c/continew-admin/blob/dev/LICENSE | ||||
|   # 是否本地解析 IP 归属地 | ||||
|   ipAddrLocalParseEnabled: false | ||||
|   ipAddrLocalParseEnabled: true | ||||
|  | ||||
| --- ### 日志配置(重叠部分,优先级高于 logback-spring.xml 中的配置) | ||||
| logging: | ||||
| @@ -29,11 +29,13 @@ logging: | ||||
|   file: | ||||
|     path: @logging.file.path@ | ||||
|   config: classpath:logback-spring.xml | ||||
|   ## 操作日志配置 | ||||
|   operation: | ||||
|     # 是否启用操作日志 | ||||
|   ## 系统日志配置 | ||||
|   system: | ||||
|     # 是否启用系统日志 | ||||
|     enabled: true | ||||
|     # 不记录操作日志的请求方式 | ||||
|     # 是否记录内网 IP 操作 | ||||
|     includeInnerIp: false | ||||
|     # 哪些请求方式不记录系统日志 | ||||
|     #excludeMethods: | ||||
|     #  - GET | ||||
|     # 脱敏字段 | ||||
|   | ||||
| @@ -26,7 +26,6 @@ CREATE TABLE IF NOT EXISTS `sys_user`  ( | ||||
|  | ||||
| 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 '请求方式', | ||||
| @@ -34,14 +33,15 @@ CREATE TABLE IF NOT EXISTS `sys_log` ( | ||||
|     `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 '响应体', | ||||
|     `response_body` mediumtext 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 '操作地址', | ||||
|     `result` tinyint(1) unsigned DEFAULT 1 COMMENT '操作结果(1成功 2失败)', | ||||
|     `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 '异常', | ||||
|     `exception` mediumtext 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='操作日志表'; | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user