diff --git a/ruoyi-common/src/main/resources/logback.xml b/ruoyi-common/src/main/resources/logback.xml
deleted file mode 100644
index a360583fa..000000000
--- a/ruoyi-common/src/main/resources/logback.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<configuration>
-    <!-- 日志存放路径 -->
-	<property name="log.path" value="/home/ruoyi/logs" />
-    <!-- 日志输出格式 -->
-	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
-
-	<!-- 控制台输出 -->
-	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
-		<encoder>
-			<pattern>${log.pattern}</pattern>
-		</encoder>
-	</appender>
-	
-	<!-- 系统日志输出 -->
-	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
-	    <file>${log.path}/sys-info.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 60天 -->
-			<maxHistory>60</maxHistory>
-		</rollingPolicy>
-		<encoder>
-			<pattern>${log.pattern}</pattern>
-		</encoder>
-		<filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
-            <level>INFO</level>
-            <!-- 匹配时的操作:接收(记录) -->
-            <onMatch>ACCEPT</onMatch>
-            <!-- 不匹配时的操作:拒绝(不记录) -->
-            <onMismatch>DENY</onMismatch>
-        </filter>
-	</appender>
-	
-	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
-	    <file>${log.path}/sys-error.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 60天 -->
-			<maxHistory>60</maxHistory>
-        </rollingPolicy>
-        <encoder>
-            <pattern>${log.pattern}</pattern>
-        </encoder>
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
-            <level>ERROR</level>
-			<!-- 匹配时的操作:接收(记录) -->
-            <onMatch>ACCEPT</onMatch>
-			<!-- 不匹配时的操作:拒绝(不记录) -->
-            <onMismatch>DENY</onMismatch>
-        </filter>
-    </appender>
-	
-	<!-- 用户访问日志输出  -->
-    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
-		<file>${log.path}/sys-user.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 按天回滚 daily -->
-            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
-            <!-- 日志最大的历史 60天 -->
-            <maxHistory>60</maxHistory>
-        </rollingPolicy>
-        <encoder>
-            <pattern>${log.pattern}</pattern>
-        </encoder>
-    </appender>
-	
-	<!-- 系统模块日志级别控制  -->
-	<logger name="com.ruoyi" level="info" />
-	<!-- Spring日志级别控制  -->
-	<logger name="org.springframework" level="warn" />
-
-	<root level="info">
-		<appender-ref ref="console" />
-	</root>
-	
-	<!--系统操作日志-->
-    <root level="info">
-        <appender-ref ref="file_info" />
-        <appender-ref ref="file_error" />
-    </root>
-	
-	<!--系统用户操作日志-->
-    <logger name="sys-user" level="info">
-        <appender-ref ref="sys-user"/>
-    </logger>
-</configuration> 
\ No newline at end of file
diff --git a/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java b/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java
new file mode 100644
index 000000000..c93985e18
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java
@@ -0,0 +1,25 @@
+package cn.iocoder.dashboard.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 全局用户类型枚举
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum {
+
+    MEMBER(1, "会员"), // 面向 c 端,普通用户
+    ADMIN(2, "管理员"); // 面向 b 端,管理后台
+
+    /**
+     * 类型
+     */
+    private final Integer value;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java
new file mode 100644
index 000000000..b36a1df5d
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java
@@ -0,0 +1,30 @@
+package cn.iocoder.dashboard.framework.logger.apilog.config;
+
+import cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter;
+import cn.iocoder.dashboard.framework.web.config.WebProperties;
+import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.Filter;
+
+@Configuration
+public class ApiLogConfiguration {
+
+    /**
+     * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
+     */
+    @Bean
+    public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties) {
+        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties);
+        return createFilterBean(filter, FilterOrderEnum.API_ACCESS_LOG_FILTER);
+    }
+
+    private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
+        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
+        bean.setOrder(order);
+        return bean;
+    }
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java
new file mode 100644
index 000000000..bd839610f
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java
@@ -0,0 +1,71 @@
+package cn.iocoder.dashboard.framework.logger.apilog.core.filter;
+
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
+import cn.iocoder.dashboard.framework.web.config.WebProperties;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * API 访问日志 Filter
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+@Slf4j
+public class ApiAccessLogFilter extends OncePerRequestFilter {
+
+    private final WebProperties webProperties;
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        return !request.getRequestURI().startsWith(webProperties.getApiPrefix());
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        // 获得开始时间
+        Date startTime = new Date();
+        // 提前获得参数,避免 XssFilter 过滤处理
+        Map<String, String> queryString = ServletUtil.getParamMap(request);
+        String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null;
+
+        try {
+            // 继续过滤器
+            filterChain.doFilter(request, response);
+            // 正常执行,记录日志
+            createApiAccessLog(request, startTime, queryString, requestBody, null);
+        } catch (Exception ex) {
+            // 异常执行,记录日志
+            createApiAccessLog(request, startTime, queryString, requestBody, ex);
+            throw ex;
+        }
+    }
+
+    private void createApiAccessLog(HttpServletRequest request, Date startTime,
+                                    Map<String, String> queryString, String requestBody, Exception ex) {
+        try {
+            ApiAccessLogCreateDTO createDTO = this.buildApiAccessLogDTO(request, startTime, queryString, requestBody, ex);
+
+        } catch (Exception e) {
+            log.error("[createApiAccessLog][url({}) 发生异常]", request.getRequestURI(), ex);
+        }
+    }
+
+    private ApiAccessLogCreateDTO buildApiAccessLogDTO(HttpServletRequest request, Date startTime,
+                                                       Map<String, String> queryString, String requestBody, Exception ex) {
+        return null;
+    }
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java
new file mode 100644
index 000000000..be4176de8
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java
@@ -0,0 +1,82 @@
+package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto;
+
+import javax.validation.constraints.NotNull;
+import java.util.Date;
+
+/**
+ * API 访问日志创建 DTO
+ *
+ * @author 芋道源码
+ */
+public class ApiAccessLogCreateDTO {
+
+    /**
+     * 链路追踪编号
+     */
+    private String traceId;
+    /**
+     * 用户编号
+     */
+    private Integer 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 Date startTime;
+    /**
+     * 结束请求时间
+     */
+    @NotNull(message = "结束请求时间不能为空")
+    private Date endTime;
+    /**
+     * 执行时长,单位:毫秒
+     */
+    @NotNull(message = "执行时长不能为空")
+    private Integer duration;
+    /**
+     * 结果码
+     */
+    @NotNull(message = "错误码不能为空")
+    private Integer resultCode;
+    /**
+     * 结果提示
+     */
+    private String resultMsg;
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/package-info.java
new file mode 100644
index 000000000..4d76b11b2
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.dashboard.framework.logger.apilog.core.service;
diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
index 0212294ea..759970631 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
+++ b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
@@ -37,6 +37,5 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
         }
         // 返回成功
         ServletUtils.writeJSON(response, null);
-//        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.OK.value(), "退出成功")));
     }
 }
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
index 405cdfaa8..38a841229 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
@@ -1,11 +1,12 @@
 package cn.iocoder.dashboard.framework.web.config;
 
+import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
 import cn.iocoder.dashboard.framework.web.core.filter.RequestBodyCacheFilter;
 import cn.iocoder.dashboard.framework.web.core.filter.XssFilter;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.core.annotation.Order;
 import org.springframework.util.PathMatcher;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.cors.CorsConfiguration;
@@ -15,10 +16,8 @@ import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 import javax.annotation.Resource;
+import javax.servlet.Filter;
 
-/**
- * Web 配置类
- */
 @Configuration
 @EnableConfigurationProperties({WebProperties.class, XssProperties.class})
 public class WebConfiguration implements WebMvcConfigurer {
@@ -39,8 +38,7 @@ public class WebConfiguration implements WebMvcConfigurer {
      * 创建 CorsFilter Bean,解决跨域问题
      */
     @Bean
-    @Order(Integer.MIN_VALUE)
-    public CorsFilter corsFilter() {
+    public FilterRegistrationBean<CorsFilter> corsFilterBean() {
         // 创建 CorsConfiguration 对象
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
@@ -50,25 +48,29 @@ public class WebConfiguration implements WebMvcConfigurer {
         // 创建 UrlBasedCorsConfigurationSource 对象
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
-        return new CorsFilter(source);
+        return createFilterBean(new CorsFilter(source), FilterOrderEnum.CORS_FILTER);
     }
 
     /**
      * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
      */
     @Bean
-    @Order(Integer.MIN_VALUE)
-    public RequestBodyCacheFilter requestBodyCacheFilter() {
-        return new RequestBodyCacheFilter();
+    public FilterRegistrationBean<RequestBodyCacheFilter> requestBodyCacheFilter() {
+        return createFilterBean(new RequestBodyCacheFilter(), FilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
     }
 
     /**
      * 创建 XssFilter Bean,解决 Xss 安全问题
      */
     @Bean
-    @Order(Integer.MIN_VALUE + 1000) // 需要保证在 RequestBodyCacheFilter 后面
-    public XssFilter xssFilter(XssProperties properties, PathMatcher pathMatcher) {
-        return new XssFilter(properties, pathMatcher);
+    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
+        return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER);
+    }
+
+    private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
+        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
+        bean.setOrder(order);
+        return bean;
     }
 
 }
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java
new file mode 100644
index 000000000..31b25087e
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java
@@ -0,0 +1,22 @@
+package cn.iocoder.dashboard.framework.web.core.enums;
+
+/**
+ * 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ * @author 芋道源码
+ */
+public interface FilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
+
+    int API_ACCESS_LOG_FILTER = -104; // 需要保证在 RequestBodyCacheFilter 后面
+
+    int XSS_FILTER = -103;  // 需要保证在 RequestBodyCacheFilter 后面
+
+    // Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java
index deff00cb4..6bd560299 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java
@@ -1,7 +1,6 @@
 package cn.iocoder.dashboard.framework.web.core.filter;
 
-import cn.hutool.core.util.StrUtil;
-import org.springframework.http.MediaType;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.ContentCachingRequestWrapper;
 
@@ -29,7 +28,7 @@ public class RequestBodyCacheFilter extends OncePerRequestFilter {
     @Override
     protected boolean shouldNotFilter(HttpServletRequest request) {
         // 只处理 json 请求内容
-        return !StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+        return !ServletUtils.isJsonRequest(request);
     }
 
 }
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java
index d7780ed9b..c5256e7af 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java
@@ -6,6 +6,7 @@ import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ReflectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HTMLFilter;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
 import org.springframework.http.MediaType;
 
 import javax.servlet.ReadListener;
@@ -56,7 +57,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
     @Override
     public ServletInputStream getInputStream() throws IOException {
         // 如果非 json 请求,不进行 Xss 处理
-        if (!StrUtil.startWithIgnoreCase(super.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
+        if (!ServletUtils.isJsonRequest(this)) {
             return super.getInputStream();
         }
 
diff --git a/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java
index 31be9a7be..08fe7f58c 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java
+++ b/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java
@@ -9,7 +9,7 @@ import lombok.*;
 import java.util.Date;
 
 /**
- * 定时任务的执行日志 sys_job_log
+ * 定时任务的执行日志
  *
  * @author 芋道源码
  */
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiAccessLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiAccessLogDO.java
new file mode 100644
index 000000000..4ded8181c
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiAccessLogDO.java
@@ -0,0 +1,103 @@
+package cn.iocoder.dashboard.modules.system.dal.dataobject.logger;
+
+import cn.iocoder.dashboard.common.enums.UserTypeEnum;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
+import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.util.Date;
+
+/**
+ * API 访问日志
+ *
+ * @author 芋道源码
+ */
+@TableName("sys_api_access_log")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SysApiAccessLogDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Integer id;
+    /**
+     * 链路追踪编号
+     *
+     * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。
+     */
+    private String traceId;
+    /**
+     * 用户编号
+     */
+    private Integer userId;
+    /**
+     * 用户类型
+     *
+     * 枚举 {@link UserTypeEnum}
+     */
+    private Integer userType;
+    /**
+     * 应用名
+     *
+     * 目前读取 `spring.application.name` 配置项
+     */
+    private String applicationName;
+
+    /**
+     * 请求方法名
+     */
+    private String requestMethod;
+    /**
+     * 访问地址
+     */
+    private String requestUrl;
+    /**
+     * 请求参数
+     *
+     * query: Query String
+     * body: Quest Body
+     */
+    private String requestParams;
+    /**
+     * 用户 IP
+     */
+    private String userIp;
+    /**
+     * 浏览器 UA
+     */
+    private String userAgent;
+
+    /**
+     * 开始请求时间
+     */
+    private Date startTime;
+    /**
+     * 结束请求时间
+     */
+    private Date endTime;
+    /**
+     * 执行时长,单位:毫秒
+     */
+    private Integer duration;
+    /**
+     * 结果码
+     *
+     * 目前使用的 {@link CommonResult#getCode()} 属性
+     */
+    private Integer resultCode;
+    /**
+     * 结果提示
+     *
+     * 目前使用的 {@link CommonResult#getMsg()} 属性
+     */
+    private String resultMsg;
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiErrorLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiErrorLogDO.java
new file mode 100644
index 000000000..af1391677
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiErrorLogDO.java
@@ -0,0 +1,4 @@
+package cn.iocoder.dashboard.modules.system.dal.dataobject.logger;
+
+public class SysApiErrorLogDO {
+}
diff --git a/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java b/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java
index 35f3d5765..3828775c4 100644
--- a/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java
+++ b/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java
@@ -1,6 +1,7 @@
 package cn.iocoder.dashboard.util.servlet;
 
 import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.servlet.ServletUtil;
 import cn.iocoder.dashboard.util.json.JsonUtils;
 import org.springframework.http.MediaType;
@@ -8,6 +9,7 @@ import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
+import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -86,4 +88,8 @@ public class ServletUtils {
         return ServletUtil.getClientIP(request);
     }
 
+    public static boolean isJsonRequest(ServletRequest request) {
+        return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+    }
+
 }