Browse Source

【优化】增强访问日志,支持是否记录、脱敏、操作信息等功能

YunaiV 1 year ago
parent
commit
132c1cc828
20 changed files with 484 additions and 297 deletions
  1. 17 13
      sql/mysql/ruoyi-vue-pro.sql
  2. 0 46
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java
  3. 24 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java
  4. 12 3
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
  5. 65 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/annotations/ApiAccessLog.java
  6. 51 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/enums/OperateTypeEnum.java
  7. 167 27
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java
  8. 67 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java
  9. 0 85
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLog.java
  10. 5 2
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java
  11. 1 3
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
  12. 0 107
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLog.java
  13. 5 2
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java
  14. 1 3
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
  15. 5 4
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
  16. 19 1
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java
  17. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java
  18. 21 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java
  19. 5 1
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/create_tables.sql
  20. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/NotifyMessageController.java

+ 17 - 13
sql/mysql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80200 (8.2.0)
  File Encoding         : 65001
 
- Date: 30/03/2024 20:42:06
+ Date: 03/04/2024 19:07:31
 */
 
 SET NAMES utf8mb4;
@@ -327,9 +327,13 @@ CREATE TABLE `infra_api_access_log`  (
   `application_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名',
   `request_method` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求方法名',
   `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求地址',
-  `request_params` varchar(8000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求参数',
+  `request_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '请求参数',
+  `response_body` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '响应结果',
   `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户 IP',
   `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '浏览器 UA',
+  `operate_module` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作模块',
+  `operate_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作名',
+  `operate_type` tinyint NULL DEFAULT 0 COMMENT '操作分类',
   `begin_time` datetime NOT NULL COMMENT '开始请求时间',
   `end_time` datetime NOT NULL COMMENT '结束请求时间',
   `duration` int NOT NULL COMMENT '执行时长',
@@ -343,7 +347,7 @@ CREATE TABLE `infra_api_access_log`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_create_time`(`create_time` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 35832 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 35920 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
 
 -- ----------------------------
 -- Records of infra_api_access_log
@@ -385,7 +389,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 16429 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 16462 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -494,7 +498,7 @@ CREATE TABLE `infra_config`  (
 -- Records of infra_config
 -- ----------------------------
 BEGIN;
-INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'biz', 1, '用户管理-账号初始密码', 'sys.user.init-password', '123456', b'0', '初始化密码 123456', 'admin', '2021-01-05 17:03:48', '1', '2024-02-28 22:54:14', b'0');
+INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'biz', 1, '用户管理-账号初始密码', 'sys.user.init-password', '123456', b'0', '初始化密码 123456', 'admin', '2021-01-05 17:03:48', '1', '2024-04-03 17:22:28', b'0');
 INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'url', 2, 'MySQL 监控的地址', 'url.druid', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:33:38', b'0');
 INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 'url', 2, 'SkyWalking 监控的地址', 'url.skywalking', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:57:03', b'0');
 INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 'url', 2, 'Spring Boot Admin 监控的地址', 'url.spring-boot-admin', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:52:07', b'0');
@@ -690,7 +694,7 @@ CREATE TABLE `infra_file`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1301 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file
@@ -1416,7 +1420,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3054 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 3066 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -2453,7 +2457,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
   INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6332 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 6366 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2575,7 +2579,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1430 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1441 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2615,7 +2619,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 11964 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 12000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -5305,7 +5309,7 @@ CREATE TABLE `system_sms_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 946 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 947 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
 
 -- ----------------------------
 -- Records of system_sms_log
@@ -5475,7 +5479,7 @@ INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `c
 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (151, '大租户', 126, '土豆大', NULL, 0, 'https://tudou.iocoder.cn', 111, '2023-12-08 00:00:00', 10, '1', '2023-12-02 23:35:05', '1', '2023-12-08 23:39:56', b'0');
 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (152, '新租户', 127, '土豆', NULL, 0, 'http://xx.iocoder.cn', 111, '2025-12-31 00:00:00', 50, '1', '2023-12-30 11:43:17', '1', '2023-12-30 11:43:17', b'0');
 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (153, '小明的租户', 128, 'xiaoming', '15601691301', 0, 'xiaoming.iocoder.cn', 111, '2025-12-01 00:00:00', 100, '1', '2024-02-27 21:58:25', '1', '2024-02-28 22:53:54', b'0');
-INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (154, 'hh', 129, 'hh', NULL, 0, 'http://hh.iocoder.cn', 111, '2024-04-30 00:00:00', 123, '1', '2024-03-30 17:52:59', '1', '2024-03-30 17:52:59', b'0');
+INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (154, 'hh', 129, 'hh', NULL, 0, 'http://hh.iocoder.cn', 111, '2024-04-30 00:00:00', 123, '1', '2024-03-30 17:52:59', '1', '2024-04-03 15:06:42', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -5619,7 +5623,7 @@ CREATE TABLE `system_users`  (
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png', 0, '0:0:0:0:0:0:0:1', '2024-03-30 17:18:34', 'admin', '2021-01-05 17:03:47', NULL, '2024-03-30 17:18:34', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png', 0, '127.0.0.1', '2024-04-03 17:31:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-04-03 17:31:00', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-03-18 21:09:04', '', '2021-01-13 23:50:35', NULL, '2024-03-18 21:09:04', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$KhExCYl7lx6eWWZYKsibKOZ8IBJRyuNuCcEOLQ11RYhJKgHmlSwK.', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-26 07:11:35', '', '2021-01-21 02:13:53', NULL, '2024-03-26 07:11:35', b'0', 1);

+ 0 - 46
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java

@@ -1,46 +0,0 @@
-package cn.iocoder.yudao.framework.common.util.spring;
-
-import cn.hutool.core.bean.BeanUtil;
-import org.springframework.aop.framework.AdvisedSupport;
-import org.springframework.aop.framework.AopProxy;
-import org.springframework.aop.support.AopUtils;
-
-/**
- * Spring AOP 工具类
- *
- * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
- */
-public class SpringAopUtils {
-
-    /**
-     * 获取代理的目标对象
-     *
-     * @param proxy 代理对象
-     * @return 目标对象
-     */
-    public static Object getTarget(Object proxy) throws Exception {
-        // 不是代理对象
-        if (!AopUtils.isAopProxy(proxy)) {
-            return proxy;
-        }
-        // Jdk 代理
-        if (AopUtils.isJdkDynamicProxy(proxy)) {
-            return getJdkDynamicProxyTargetObject(proxy);
-        }
-        // Cglib 代理
-        return getCglibProxyTargetObject(proxy);
-    }
-
-    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
-        Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0");
-        AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised");
-        return advisedSupport.getTargetSource().getTarget();
-    }
-
-    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
-        AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h");
-        AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised");
-        return advisedSupport.getTargetSource().getTarget();
-    }
-
-}

+ 24 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.common.util.spring;
+
+import cn.hutool.extra.spring.SpringUtil;
+
+import java.util.Objects;
+
+/**
+ * Spring 工具类
+ *
+ * @author 芋道源码
+ */
+public class SpringUtils extends SpringUtil {
+
+    /**
+     * 是否为生产环境
+     *
+     * @return 是否生产环境
+     */
+    public static boolean isProd() {
+        String activeProfile = getActiveProfile();
+        return Objects.equals("prod", activeProfile);
+    }
+
+}

+ 12 - 3
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.apilog.config;
 
 import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter;
+import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
 import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
 import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
 import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
@@ -10,23 +11,26 @@ import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
 import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
 import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
+import jakarta.servlet.Filter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
-
-import jakarta.servlet.Filter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 @AutoConfiguration(after = YudaoWebAutoConfiguration.class)
-public class YudaoApiLogAutoConfiguration {
+public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
 
     @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
         return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
     }
 
     @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
         return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
     }
@@ -49,4 +53,9 @@ public class YudaoApiLogAutoConfiguration {
         return bean;
     }
 
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new ApiAccessLogInterceptor());
+    }
+
 }

+ 65 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/annotations/ApiAccessLog.java

@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.framework.apilog.core.annotations;
+
+import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 访问日志注解
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiAccessLog {
+
+    // ========== 开关字段 ==========
+
+    /**
+     * 是否记录访问日志
+     */
+    boolean enable() default true;
+    /**
+     * 是否记录请求参数
+     *
+     * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭
+     */
+    boolean requestEnable() default true;
+    /**
+     * 是否记录响应结果
+     *
+     * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开
+     */
+    boolean responseEnable() default false;
+    /**
+     * 敏感参数数组
+     *
+     * 添加后,请求参数、响应结果不会记录该参数
+     */
+    String[] sanitizeKeys() default {};
+
+    // ========== 模块字段 ==========
+
+    /**
+     * 操作模块
+     *
+     * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性
+     */
+    String operateModule() default "";
+    /**
+     * 操作名
+     *
+     * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性
+     */
+    String operateName() default "";
+    /**
+     * 操作分类
+     *
+     * 实际并不是数组,因为枚举不能设置 null 作为默认值
+     */
+    OperateTypeEnum[] operateType() default {};
+
+}

+ 51 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/enums/OperateTypeEnum.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.framework.apilog.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 操作日志的操作类型
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum OperateTypeEnum {
+
+    /**
+     * 查询
+     */
+    GET(1),
+    /**
+     * 新增
+     */
+    CREATE(2),
+    /**
+     * 修改
+     */
+    UPDATE(3),
+    /**
+     * 删除
+     */
+    DELETE(4),
+    /**
+     * 导出
+     */
+    EXPORT(5),
+    /**
+     * 导入
+     */
+    IMPORT(6),
+    /**
+     * 其它
+     *
+     * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识
+     */
+    OTHER(0);
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+
+}

+ 167 - 27
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java

@@ -1,38 +1,56 @@
 package cn.iocoder.yudao.framework.apilog.core.filter;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLog;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.apilog.core.annotations.ApiAccessLog;
+import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
 import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
-import lombok.extern.slf4j.Slf4j;
-
+import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.method.HandlerMethod;
+
 import java.io.IOException;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
 import java.util.Map;
 
+import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
  * API 访问日志 Filter
  *
+ * 目的:记录 API 访问日志到数据库中
+ *
  * @author 芋道源码
  */
 @Slf4j
 public class ApiAccessLogFilter extends ApiRequestFilter {
 
+    private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
+
     private final String applicationName;
 
     private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
@@ -44,6 +62,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
     }
 
     @Override
+    @SuppressWarnings("NullableProblems")
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
             throws ServletException, IOException {
         // 获得开始时间
@@ -66,45 +85,166 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
 
     private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
                                     Map<String, String> queryString, String requestBody, Exception ex) {
-        ApiAccessLog accessLog = new ApiAccessLog();
+        ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
         try {
-            this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
+            boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex);
+            if (!enable) {
+                return;
+            }
             apiAccessLogFrameworkService.createApiAccessLog(accessLog);
         } catch (Throwable th) {
             log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
         }
     }
 
-    private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, LocalDateTime beginTime,
+    private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime,
                                       Map<String, String> queryString, String requestBody, Exception ex) {
+        // 判断:是否要记录操作日志
+        HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD);
+        ApiAccessLog accessLogAnnotation = null;
+        if (handlerMethod != null) {
+            accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class);
+            if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) {
+                return false;
+            }
+        }
+
         // 处理用户信息
-        accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
-        accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
+        accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request))
+                .setUserType(WebFrameworkUtils.getLoginUserType(request));
         // 设置访问结果
         CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
         if (result != null) {
-            accessLog.setResultCode(result.getCode());
-            accessLog.setResultMsg(result.getMsg());
+            accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg());
         } else if (ex != null) {
-            accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode());
-            accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
+            accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode())
+                    .setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
         } else {
-            accessLog.setResultCode(0);
-            accessLog.setResultMsg("");
-        }
-        // 设置其它字段
-        accessLog.setTraceId(TracerUtils.getTraceId());
-        accessLog.setApplicationName(applicationName);
-        accessLog.setRequestUrl(request.getRequestURI());
-        Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build();
-        accessLog.setRequestParams(toJsonString(requestParams));
-        accessLog.setRequestMethod(request.getMethod());
-        accessLog.setUserAgent(ServletUtils.getUserAgent(request));
-        accessLog.setUserIp(ServletUtils.getClientIP(request));
+            accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg("");
+        }
+        // 设置请求字段
+        accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName)
+                .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod())
+                .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request));
+        String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null;
+        Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE;
+        if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false
+            Map<String, Object> requestParams = MapUtil.<String, Object>builder()
+                    .put("query", sanitizeMap(queryString, sanitizeKeys))
+                    .put("body", sanitizeJson(requestBody, sanitizeKeys)).build();
+            accessLog.setRequestParams(toJsonString(requestParams));
+        }
+        Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE;
+        if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true
+            accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys));
+        }
         // 持续时间
-        accessLog.setBeginTime(beginTime);
-        accessLog.setEndTime(LocalDateTime.now());
-        accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
+        accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now())
+                .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
+
+        // 操作模块
+        if (handlerMethod != null) {
+            Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class);
+            Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class);
+            String operateModule = accessLogAnnotation != null ? accessLogAnnotation.operateModule() :
+                    tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null;
+            String operateName = accessLogAnnotation != null ? accessLogAnnotation.operateName() :
+                    operationAnnotation != null ? operationAnnotation.summary() : null;
+            OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ?
+                    accessLogAnnotation.operateType()[0] : parseOperateLogType(request);
+            accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType());
+        }
+        return true;
+    }
+
+    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========
+
+    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
+        RequestMethod requestMethod = RequestMethod.resolve(request.getMethod());
+        if (requestMethod == null) {
+            return OperateTypeEnum.OTHER;
+        }
+        switch (requestMethod) {
+            case GET:
+                return OperateTypeEnum.GET;
+            case POST:
+                return OperateTypeEnum.CREATE;
+            case PUT:
+                return OperateTypeEnum.UPDATE;
+            case DELETE:
+                return OperateTypeEnum.DELETE;
+            default:
+                return OperateTypeEnum.OTHER;
+        }
+    }
+
+    // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
+
+    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
+        if (CollUtil.isNotEmpty(map)) {
+            return null;
+        }
+        if (sanitizeKeys != null) {
+            MapUtil.removeAny(map, sanitizeKeys);
+        }
+        MapUtil.removeAny(map, SANITIZE_KEYS);
+        return JsonUtils.toJsonString(map);
+    }
+
+    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
+        if (StrUtil.isEmpty(jsonString)) {
+            return null;
+        }
+        try {
+            JsonNode rootNode = JsonUtils.parseTree(jsonString);
+            sanitizeJson(rootNode, sanitizeKeys);
+            return JsonUtils.toJsonString(rootNode);
+        } catch (Exception e) {
+            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
+            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
+            return jsonString;
+        }
+    }
+
+    private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
+        if (commonResult == null) {
+            return null;
+        }
+        String jsonString = toJsonString(commonResult);
+        try {
+            JsonNode rootNode = JsonUtils.parseTree(jsonString);
+            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
+            return JsonUtils.toJsonString(rootNode);
+        } catch (Exception e) {
+            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
+            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
+            return jsonString;
+        }
+    }
+
+    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
+        // 情况一:数组,遍历处理
+        if (node.isArray()) {
+            for (JsonNode childNode : node) {
+                sanitizeJson(childNode, sanitizeKeys);
+            }
+            return;
+        }
+        // 情况二:非 Object,只是某个值,直接返回
+        if (!node.isObject()) {
+            return;
+        }
+        //  情况三:Object,遍历处理
+        Iterator<Map.Entry<String, JsonNode>> iterator = node.properties().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<String, JsonNode> entry = iterator.next();
+            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
+                || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
+                iterator.remove();
+                continue;
+            }
+            sanitizeJson(entry.getValue(), sanitizeKeys);
+        }
     }
 
 }

+ 67 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.framework.apilog.core.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StopWatch;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.Map;
+
+/**
+ * API 访问日志 Interceptor
+ *
+ * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class ApiAccessLogInterceptor implements HandlerInterceptor {
+
+    public static String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD";
+
+    private static String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+        // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用
+        HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null;
+        if (handlerMethod != null) {
+            request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod);
+        }
+
+        // 打印 request 日志
+        if (!SpringUtils.isProd()) {
+            Map<String, String> queryString = ServletUtils.getParamMap(request);
+            String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
+            if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
+                log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
+            } else {
+                log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
+                        StrUtil.nullToDefault(requestBody, queryString.toString()));
+            }
+            // 计时
+            StopWatch stopWatch = new StopWatch();
+            stopWatch.start();
+            request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
+        }
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+        // 打印 response 日志
+        if (!SpringUtils.isProd()) {
+            StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
+            stopWatch.stop();
+            log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]",
+                    request.getRequestURI(), stopWatch.getTotalTimeMillis());
+        }
+    }
+
+}

+ 0 - 85
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLog.java

@@ -1,85 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import lombok.Data;
-
-import jakarta.validation.constraints.NotNull;
-import java.time.LocalDateTime;
-
-/**
- * API 访问日志
- *
- * @author 芋道源码
- */
-@Data
-public class ApiAccessLog {
-
-    /**
-     * 链路追踪编号
-     */
-    private String traceId;
-    /**
-     * 用户编号
-     */
-    private Long userId;
-    /**
-     * 用户类型
-     */
-    private Integer userType;
-    /**
-     * 应用名
-     */
-    @NotNull(message = "应用名不能为空")
-    private String applicationName;
-
-    /**
-     * 请求方法名
-     */
-    @NotNull(message = "http 请求方法不能为空")
-    private String requestMethod;
-    /**
-     * 访问地址
-     */
-    @NotNull(message = "访问地址不能为空")
-    private String requestUrl;
-    /**
-     * 请求参数
-     */
-    @NotNull(message = "请求参数不能为空")
-    private String requestParams;
-    /**
-     * 用户 IP
-     */
-    @NotNull(message = "ip 不能为空")
-    private String userIp;
-    /**
-     * 浏览器 UA
-     */
-    @NotNull(message = "User-Agent 不能为空")
-    private String userAgent;
-
-    /**
-     * 开始请求时间
-     */
-    @NotNull(message = "开始请求时间不能为空")
-    private LocalDateTime beginTime;
-    /**
-     * 结束请求时间
-     */
-    @NotNull(message = "结束请求时间不能为空")
-    private LocalDateTime endTime;
-    /**
-     * 执行时长,单位:毫秒
-     */
-    @NotNull(message = "执行时长不能为空")
-    private Integer duration;
-    /**
-     * 结果码
-     */
-    @NotNull(message = "错误码不能为空")
-    private Integer resultCode;
-    /**
-     * 结果提示
-     */
-    private String resultMsg;
-
-}

+ 5 - 2
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.framework.apilog.core.service;
 
+import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
+
 /**
  * API 访问日志 Framework Service 接口
  *
@@ -10,7 +12,8 @@ public interface ApiAccessLogFrameworkService {
     /**
      * 创建 API 访问日志
      *
-     * @param apiAccessLog API 访问日志
+     * @param reqDTO API 访问日志
      */
-    void createApiAccessLog(ApiAccessLog apiAccessLog);
+    void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO);
+
 }

+ 1 - 3
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.framework.apilog.core.service;
 
-import cn.hutool.core.bean.BeanUtil;
 import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 import lombok.RequiredArgsConstructor;
@@ -20,8 +19,7 @@ public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkSe
 
     @Override
     @Async
-    public void createApiAccessLog(ApiAccessLog apiAccessLog) {
-        ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class);
+    public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) {
         apiAccessLogApi.createApiAccessLog(reqDTO);
     }
 

+ 0 - 107
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLog.java

@@ -1,107 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import lombok.Data;
-
-import jakarta.validation.constraints.NotNull;
-import java.time.LocalDateTime;
-
-/**
- * API 错误日志
- *
- * @author 芋道源码
- */
-@Data
-public class ApiErrorLog {
-
-    /**
-     * 链路编号
-     */
-    private String traceId;
-    /**
-     * 账号编号
-     */
-    private Long userId;
-    /**
-     * 用户类型
-     */
-    private Integer userType;
-    /**
-     * 应用名
-     */
-    @NotNull(message = "应用名不能为空")
-    private String applicationName;
-
-    /**
-     * 请求方法名
-     */
-    @NotNull(message = "http 请求方法不能为空")
-    private String requestMethod;
-    /**
-     * 访问地址
-     */
-    @NotNull(message = "访问地址不能为空")
-    private String requestUrl;
-    /**
-     * 请求参数
-     */
-    @NotNull(message = "请求参数不能为空")
-    private String requestParams;
-    /**
-     * 用户 IP
-     */
-    @NotNull(message = "ip 不能为空")
-    private String userIp;
-    /**
-     * 浏览器 UA
-     */
-    @NotNull(message = "User-Agent 不能为空")
-    private String userAgent;
-
-    /**
-     * 异常时间
-     */
-    @NotNull(message = "异常时间不能为空")
-    private LocalDateTime exceptionTime;
-    /**
-     * 异常名
-     */
-    @NotNull(message = "异常名不能为空")
-    private String exceptionName;
-    /**
-     * 异常发生的类全名
-     */
-    @NotNull(message = "异常发生的类全名不能为空")
-    private String exceptionClassName;
-    /**
-     * 异常发生的类文件
-     */
-    @NotNull(message = "异常发生的类文件不能为空")
-    private String exceptionFileName;
-    /**
-     * 异常发生的方法名
-     */
-    @NotNull(message = "异常发生的方法名不能为空")
-    private String exceptionMethodName;
-    /**
-     * 异常发生的方法所在行
-     */
-    @NotNull(message = "异常发生的方法所在行不能为空")
-    private Integer exceptionLineNumber;
-    /**
-     * 异常的栈轨迹异常的栈轨迹
-     */
-    @NotNull(message = "异常的栈轨迹不能为空")
-    private String exceptionStackTrace;
-    /**
-     * 异常导致的根消息
-     */
-    @NotNull(message = "异常导致的根消息不能为空")
-    private String exceptionRootCauseMessage;
-    /**
-     * 异常导致的消息
-     */
-    @NotNull(message = "异常导致的消息不能为空")
-    private String exceptionMessage;
-
-
-}

+ 5 - 2
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.framework.apilog.core.service;
 
+import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+
 /**
  * API 错误日志 Framework Service 接口
  *
@@ -10,7 +12,8 @@ public interface ApiErrorLogFrameworkService {
     /**
      * 创建 API 错误日志
      *
-     * @param apiErrorLog API 错误日志
+     * @param reqDTO API 错误日志
      */
-    void createApiErrorLog(ApiErrorLog apiErrorLog);
+    void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO);
+
 }

+ 1 - 3
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.framework.apilog.core.service;
 
-import cn.hutool.core.bean.BeanUtil;
 import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
 import lombok.RequiredArgsConstructor;
@@ -20,8 +19,7 @@ public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkServ
 
     @Override
     @Async
-    public void createApiErrorLog(ApiErrorLog apiErrorLog) {
-        ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class);
+    public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
         apiErrorLogApi.createApiErrorLog(reqDTO);
     }
 

+ 5 - 4
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.web.core.handler;
 import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLog;
 import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@@ -11,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -46,6 +46,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
 @Slf4j
 public class GlobalExceptionHandler {
 
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final String applicationName;
 
     private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
@@ -237,10 +238,10 @@ public class GlobalExceptionHandler {
 
     private void createExceptionLog(HttpServletRequest req, Throwable e) {
         // 插入错误日志
-        ApiErrorLog errorLog = new ApiErrorLog();
+        ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
         try {
             // 初始化 errorLog
-            initExceptionLog(errorLog, req, e);
+            buildExceptionLog(errorLog, req, e);
             // 执行插入 errorLog
             apiErrorLogFrameworkService.createApiErrorLog(errorLog);
         } catch (Throwable th) {
@@ -248,7 +249,7 @@ public class GlobalExceptionHandler {
         }
     }
 
-    private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) {
+    private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
         // 处理用户信息
         errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
         errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));

+ 19 - 1
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java

@@ -44,8 +44,11 @@ public class ApiAccessLogCreateReqDTO {
     /**
      * 请求参数
      */
-    @NotNull(message = "请求参数不能为空")
     private String requestParams;
+    /**
+     * 响应结果
+     */
+    private String responseBody;
     /**
      * 用户 IP
      */
@@ -57,6 +60,21 @@ public class ApiAccessLogCreateReqDTO {
     @NotNull(message = "User-Agent 不能为空")
     private String userAgent;
 
+    /**
+     * 操作模块
+     */
+    private String operateModule;
+    /**
+     * 操作名
+     */
+    private String operateName;
+    /**
+     * 操作分类
+     *
+     * 枚举,参见 OperateTypeEnum 类
+     */
+    private Integer operateType;
+
     /**
      * 开始请求时间
      */

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java

@@ -48,6 +48,10 @@ public class ApiAccessLogRespVO {
     @ExcelProperty("请求参数")
     private String requestParams;
 
+    @Schema(description = "响应结果")
+    @ExcelProperty("响应结果")
+    private String responseBody;
+
     @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
     @ExcelProperty("用户 IP")
     private String userIp;
@@ -56,6 +60,19 @@ public class ApiAccessLogRespVO {
     @ExcelProperty("浏览器 UA")
     private String userAgent;
 
+    @Schema(description = "操作模块", requiredMode = Schema.RequiredMode.REQUIRED, example = "商品模块")
+    @ExcelProperty("操作模块")
+    private String operateModule;
+
+    @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建商品")
+    @ExcelProperty("操作名")
+    private String operateName;
+
+    @Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "操作分类", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.OPERATE_TYPE)
+    private Integer operateType;
+
     @Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("开始请求时间")
     private LocalDateTime beginTime;

+ 21 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.infra.dal.dataobject.logger;
 
+import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
@@ -70,6 +71,10 @@ public class ApiAccessLogDO extends BaseDO {
      * body: Quest Body
      */
     private String requestParams;
+    /**
+     * 响应结果
+     */
+    private String responseBody;
     /**
      * 用户 IP
      */
@@ -81,6 +86,21 @@ public class ApiAccessLogDO extends BaseDO {
 
     // ========== 执行相关字段 ==========
 
+    /**
+     * 操作模块
+     */
+    private String operateModule;
+    /**
+     * 操作名
+     */
+    private String operateName;
+    /**
+     * 操作分类
+     *
+     * 枚举 {@link OperateTypeEnum}
+     */
+    private Integer operateType;
+
     /**
      * 开始请求时间
      */
@@ -93,6 +113,7 @@ public class ApiAccessLogDO extends BaseDO {
      * 执行时长,单位:毫秒
      */
     private Integer duration;
+
     /**
      * 结果码
      *

+ 5 - 1
yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/create_tables.sql

@@ -94,8 +94,12 @@ CREATE TABLE IF NOT EXISTS "infra_api_access_log" (
     "request_method" varchar(16) not null default '',
     "request_url" varchar(255) not null default '',
     "request_params" varchar(8000) not null default '',
+    "response_body" varchar(8000) not null default '',
     "user_ip" varchar(50) not null,
     "user_agent" varchar(512) not null,
+    `operate_module`           varchar(50)   NOT NULL,
+    `operate_name`             varchar(50)   NOT NULL,
+    `operate_type`     bigint(4)     NOT NULL DEFAULT '0',
     "begin_time" timestamp not null,
     "end_time" timestamp not null,
     "duration" integer not null,
@@ -108,7 +112,7 @@ CREATE TABLE IF NOT EXISTS "infra_api_access_log" (
     "deleted" bit not null default false,
     "tenant_id" bigint not null default  '0',
     primary key ("id")
-    ) COMMENT 'API 访问日志表';
+) COMMENT 'API 访问日志表';
 
 CREATE TABLE IF NOT EXISTS "infra_api_error_log" (
     "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/NotifyMessageController.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.system.controller.admin.notify;
 
+import cn.iocoder.yudao.framework.apilog.core.annotations.ApiAccessLog;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -88,6 +89,7 @@ public class NotifyMessageController {
 
     @GetMapping("/get-unread-count")
     @Operation(summary = "获得当前用户的未读站内信数量")
+    @ApiAccessLog(enable = false) // 由于前端会不断轮询该接口,记录日志没有意义
     public CommonResult<Long> getUnreadNotifyMessageCount() {
         return success(notifyMessageService.getUnreadNotifyMessageCount(
                 getLoginUserId(), UserTypeEnum.ADMIN.getValue()));