From ee9a358b11cf38435d683dd6111e973d2faa7894 Mon Sep 17 00:00:00 2001
From: YunaiV <>
Date: Sun, 3 Jan 2021 03:21:35 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20SpringMVC=20=E7=BB=84?=
 =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=BB=9F=E4=B8=80=20/api/?=
 =?UTF-8?q?=20=E5=89=8D=E7=BC=80=E7=9A=84=E5=B0=81=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 http-client.env.json                          |   3 +-
 ruoyi-ui/.env.development                     |   1 +
 ruoyi-ui/src/api/login.js                     |   4 +-
 ruoyi-ui/src/utils/request.js                 |   6 +-
 .../config/SwaggerAutoConfiguration.java      |   4 +-
 .../web/config/WebConfiguration.java          |  80 ++++++
 .../framework/web/config/WebProperties.java   |  27 ++
 .../core/handler/GlobalExceptionHandler.java  | 257 ++++++++++++++++++
 .../framework/web/core/package-info.java      |   1 +
 .../dashboard/framework/web/package-info.java |   4 +
 .../《芋道 Spring Boot SpringMVC 入门》.md    |   1 +
 src/main/resources/application.yaml           |   2 +
 12 files changed, 381 insertions(+), 9 deletions(-)
 create mode 100644 src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
 create mode 100644 src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java
 create mode 100644 src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java
 create mode 100644 src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java
 create mode 100644 src/main/java/cn/iocoder/dashboard/framework/web/package-info.java
 create mode 100644 src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md

diff --git a/http-client.env.json b/http-client.env.json
index b733618d3..8cad13896 100644
--- a/http-client.env.json
+++ b/http-client.env.json
@@ -1,5 +1,6 @@
 {
   "local": {
-    "baseUrl": "http://127.0.0.1:8080"
+    "baseUrl": "http://127.0.0.1:8080/api",
+    "token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MDk2ODE2MzEsInN1YiI6ImE3ZGE1MWE2YWUyYTQxOWRhNmExYTlkYmJiMTVmZjc4In0.RXG7alSz64lE9oPSgbnYT_KsX7kvoHVhF5oHxXHztr1KjsttOqOppSmHGBYFI7Y75bsjEBSxSqbGsS1O1S2b1w"
   }
 }
diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development
index 0cb1c6131..069b536cd 100644
--- a/ruoyi-ui/.env.development
+++ b/ruoyi-ui/.env.development
@@ -3,6 +3,7 @@ ENV = 'development'
 
 # 若依管理系统/开发环境
 VUE_APP_BASE_API = '/dev-api'
+# VUE_APP_BASE_API = '/api'
 
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES = true
diff --git a/ruoyi-ui/src/api/login.js b/ruoyi-ui/src/api/login.js
index 9971357ed..f65942a99 100644
--- a/ruoyi-ui/src/api/login.js
+++ b/ruoyi-ui/src/api/login.js
@@ -34,7 +34,7 @@ export function logout() {
 // 获取验证码
 export function getCodeImg() {
   return request({
-    url: '/captchaImage',
+    url: '/captcha/get-image',
     method: 'get'
   })
-}
\ No newline at end of file
+}
diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js
index ae89f24de..2e246035d 100644
--- a/ruoyi-ui/src/utils/request.js
+++ b/ruoyi-ui/src/utils/request.js
@@ -8,7 +8,7 @@ axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
 // 创建axios实例
 const service = axios.create({
   // axios中请求配置有baseURL选项,表示请求URL公共部分
-  baseURL: process.env.VUE_APP_BASE_API,
+  baseURL: process.env.VUE_APP_BASE_API + '/api/', // 此处的 /api/ 地址,原因是后端的基础路径为 /api/
   // 超时
   timeout: 10000
 })
@@ -76,13 +76,13 @@ service.interceptors.response.use(res => {
       })
       return Promise.reject('error')
     } else {
-      return res.data
+      return res.data.data // 第二层 data 才是后端返回的 CommonResult.data
     }
   },
   error => {
     console.log('err' + error)
     let { message } = error;
-    if (message == "Network Error") {
+    if (message === "Network Error") {
       message = "后端接口连接异常";
     }
     else if (message.includes("timeout")) {
diff --git a/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
index 5b423c9f2..b550511af 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
+++ b/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
@@ -57,9 +57,7 @@ public class SwaggerAutoConfiguration {
                 .paths(PathSelectors.any())
                 .build()
                 .securitySchemes(securitySchemes())
-                .securityContexts(securityContexts())
-//                .pathMapping() TODO 芋艿:稍后解决,统一 api 前缀
-                ;
+                .securityContexts(securityContexts());
     }
 
     /**
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
new file mode 100644
index 000000000..187b336a9
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
@@ -0,0 +1,80 @@
+package cn.iocoder.dashboard.framework.web.config;
+
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.fastjson.support.config.FastJsonConfig;
+import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.annotation.Resource;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Web 配置类
+ */
+@Configuration
+@EnableConfigurationProperties(WebProperties.class)
+public class WebConfiguration implements WebMvcConfigurer {
+
+    @Resource
+    private WebProperties webProperties;
+
+    @Override
+    public void configurePathMatch(PathMatchConfigurer configurer) {
+        configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
+                clazz.isAnnotationPresent(RestController.class)
+                && clazz.getPackage().getName().contains("cn.iocoder.dashboard"));
+    }
+
+    // ========== MessageConverter 相关 ==========
+
+    @Override
+    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+        // 创建 FastJsonHttpMessageConverter 对象
+        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
+        // 自定义 FastJson 配置
+        FastJsonConfig fastJsonConfig = new FastJsonConfig();
+        fastJsonConfig.setCharset(Charset.defaultCharset()); // 设置字符集
+        fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, // 剔除循环引用
+                SerializerFeature.WriteNonStringKeyAsString); // 解决 Integer 作为 Key 时,转换为 String 类型,避免浏览器报错
+        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
+        // 设置支持的 MediaType
+        fastJsonHttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
+        // 添加到 converters 中
+        converters.add(0, fastJsonHttpMessageConverter); // 注意,添加到最开头,放在 MappingJackson2XmlHttpMessageConverter 前面
+    }
+
+    // ========== Filter 相关 ==========
+
+    /**
+     * 创建 CorsFilter Bean,解决跨域问题
+     */
+    @Bean
+    @Order(Integer.MIN_VALUE)
+    public CorsFilter corsFilter() {
+        // 创建 CorsConfiguration 对象
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        config.addAllowedOriginPattern("*"); // 设置访问源地址
+        config.addAllowedHeader("*"); // 设置访问源请求头
+        config.addAllowedMethod("*"); // 设置访问源请求方法
+        // 创建 UrlBasedCorsConfigurationSource 对象
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
+        return new CorsFilter(source);
+    }
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java
new file mode 100644
index 000000000..bffac3fd3
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java
@@ -0,0 +1,27 @@
+package cn.iocoder.dashboard.framework.web.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+
+import javax.validation.constraints.NotNull;
+
+@ConfigurationProperties(prefix = "yudao.web")
+@Validated
+@Data
+public class WebProperties {
+
+    /**
+     * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
+     *
+     *
+     * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
+     *      这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
+     *
+     * @see WebConfiguration#configurePathMatch(PathMatchConfigurer)
+     */
+    @NotNull(message = "API 前缀不能为空")
+    private String apiPrefix;
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java
new file mode 100644
index 000000000..27d8ba69c
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java
@@ -0,0 +1,257 @@
+package cn.iocoder.dashboard.framework.web.core.handler;
+
+import cn.iocoder.dashboard.common.exception.GlobalException;
+import cn.iocoder.dashboard.common.exception.ServiceException;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.ValidationException;
+
+import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*;
+
+/**
+ * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
+ */
+@RestControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler {
+
+    /**
+     * 处理所有异常,主要是提供给 Filter 使用
+     * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
+     *
+     * @param request 请求
+     * @param ex 异常
+     * @return 通用返回
+     */
+    public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
+        if (ex instanceof MissingServletRequestParameterException) {
+            return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
+        }
+        if (ex instanceof MethodArgumentTypeMismatchException) {
+            return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
+        }
+        if (ex instanceof MethodArgumentNotValidException) {
+            return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
+        }
+        if (ex instanceof BindException) {
+            return bindExceptionHandler((BindException) ex);
+        }
+        if (ex instanceof ConstraintViolationException) {
+            return constraintViolationExceptionHandler((ConstraintViolationException) ex);
+        }
+        if (ex instanceof ValidationException) {
+            return validationException((ValidationException) ex);
+        }
+        if (ex instanceof NoHandlerFoundException) {
+            return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
+        }
+        if (ex instanceof HttpRequestMethodNotSupportedException) {
+            return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
+        }
+        if (ex instanceof ServiceException) {
+            return serviceExceptionHandler((ServiceException) ex);
+        }
+        if (ex instanceof GlobalException) {
+            return globalExceptionHandler(request, (GlobalException) ex);
+        }
+        return defaultExceptionHandler(request, ex);
+    }
+
+    /**
+     * 处理 SpringMVC 请求参数缺失
+     *
+     * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
+     */
+    @ExceptionHandler(value = MissingServletRequestParameterException.class)
+    public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
+        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
+        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
+    }
+
+    /**
+     * 处理 SpringMVC 请求参数类型错误
+     *
+     * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String
+     */
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
+        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
+        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
+    }
+
+    /**
+     * 处理 SpringMVC 参数校验不正确
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
+        log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
+        FieldError fieldError = ex.getBindingResult().getFieldError();
+        assert fieldError != null; // 断言,避免告警
+        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
+    }
+
+    /**
+     * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
+     */
+    @ExceptionHandler(BindException.class)
+    public CommonResult<?> bindExceptionHandler(BindException ex) {
+        log.warn("[handleBindException]", ex);
+        FieldError fieldError = ex.getFieldError();
+        assert fieldError != null; // 断言,避免告警
+        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
+    }
+
+    /**
+     * 处理 Validator 校验不通过产生的异常
+     */
+    @ExceptionHandler(value = ConstraintViolationException.class)
+    public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
+        log.warn("[constraintViolationExceptionHandler]", ex);
+        ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
+        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
+    }
+
+    /**
+     * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
+     */
+    @ExceptionHandler(value = ValidationException.class)
+    public CommonResult<?> validationException(ValidationException ex) {
+        log.warn("[constraintViolationExceptionHandler]", ex);
+        // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
+        return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确");
+    }
+
+    /**
+     * 处理 SpringMVC 请求地址不存在
+     *
+     * 注意,它需要设置如下两个配置项:
+     * 1. spring.mvc.throw-exception-if-no-handler-found 为 true
+     * 2. spring.mvc.static-path-pattern 为 /statics/**
+     */
+    @ExceptionHandler(NoHandlerFoundException.class)
+    public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
+        log.warn("[noHandlerFoundExceptionHandler]", ex);
+        return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
+    }
+
+    /**
+     * 处理 SpringMVC 请求方法不正确
+     *
+     * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
+     */
+    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+    public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
+        log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
+        return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
+    }
+
+    /**
+     * 处理业务异常 ServiceException
+     *
+     * 例如说,商品库存不足,用户手机号已存在。
+     */
+    @ExceptionHandler(value = ServiceException.class)
+    public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
+        log.info("[serviceExceptionHandler]", ex);
+        return CommonResult.error(ex.getCode(), ex.getMessage());
+    }
+
+    /**
+     * 处理全局异常 ServiceException
+     *
+     * 例如说,Dubbo 请求超时,调用的 Dubbo 服务系统异常
+     */
+    @ExceptionHandler(value = GlobalException.class)
+    public CommonResult<?> globalExceptionHandler(HttpServletRequest req, GlobalException ex) {
+        // 系统异常时,才打印异常日志
+        if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) {
+            // 插入异常日志
+            this.createExceptionLog(req, ex);
+        // 普通全局异常,打印 info 日志即可
+        } else {
+            log.info("[globalExceptionHandler]", ex);
+        }
+        // 返回 ERROR CommonResult
+        return CommonResult.error(ex);
+    }
+
+    /**
+     * 处理系统异常,兜底处理所有的一切
+     */
+    @ExceptionHandler(value = Exception.class)
+    public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
+        log.error("[defaultExceptionHandler]", ex);
+        // 插入异常日志
+        this.createExceptionLog(req, ex);
+        // 返回 ERROR CommonResult
+        return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage());
+    }
+
+    // TODO 芋艿:增加异常日志
+    public void createExceptionLog(HttpServletRequest req, Throwable e) {
+//        // 插入异常日志
+//        SystemExceptionLogCreateDTO exceptionLog = new SystemExceptionLogCreateDTO();
+//        try {
+//            // 增加异常计数 metrics TODO 暂时去掉
+////            EXCEPTION_COUNTER.increment();
+//            // 初始化 exceptionLog
+//            initExceptionLog(exceptionLog, req, e);
+//            // 执行插入 exceptionLog
+//            createExceptionLog(exceptionLog);
+//        } catch (Throwable th) {
+//            log.error("[createExceptionLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
+//        }
+    }
+
+//    // TODO 优化点:后续可以增加事件
+//    @Async
+//    public void createExceptionLog(SystemExceptionLogCreateDTO exceptionLog) {
+//        try {
+//            systemExceptionLogRpc.createSystemExceptionLog(exceptionLog);
+//        } catch (Throwable th) {
+//            log.error("[addAccessLog][插入异常日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
+//        }
+//    }
+//
+//    private void initExceptionLog(SystemExceptionLogCreateDTO exceptionLog, HttpServletRequest request, Throwable e) {
+//        // 设置账号编号
+//        exceptionLog.setUserId(CommonWebUtil.getUserId(request));
+//        exceptionLog.setUserType(CommonWebUtil.getUserType(request));
+//        // 设置异常字段
+//        exceptionLog.setExceptionName(e.getClass().getName());
+//        exceptionLog.setExceptionMessage(ExceptionUtil.getMessage(e));
+//        exceptionLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
+//        exceptionLog.setExceptionStackTrace(ExceptionUtil.getStackTrace(e));
+//        StackTraceElement[] stackTraceElements = e.getStackTrace();
+//        Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
+//        StackTraceElement stackTraceElement = stackTraceElements[0];
+//        exceptionLog.setExceptionClassName(stackTraceElement.getClassName());
+//        exceptionLog.setExceptionFileName(stackTraceElement.getFileName());
+//        exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
+//        exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
+//        // 设置其它字段
+//        exceptionLog.setTraceId(MallUtils.getTraceId())
+//                .setApplicationName(applicationName)
+//                .setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
+//                .setQueryString(HttpUtil.buildQueryString(request))
+//                .setMethod(request.getMethod())
+//                .setUserAgent(HttpUtil.getUserAgent(request))
+//                .setIp(HttpUtil.getIp(request))
+//                .setExceptionTime(new Date());
+//    }
+
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java
new file mode 100644
index 000000000..1d87e7d62
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.dashboard.framework.web.core;
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java
new file mode 100644
index 000000000..f4b8100a3
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 针对 SpringMVC 的基础封装
+ */
+package cn.iocoder.dashboard.framework.web;
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md b/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md
new file mode 100644
index 000000000..82c1fe55f
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md	
@@ -0,0 +1 @@
+<http://www.iocoder.cn/Spring-Boot/SpringMVC/?dashboard>
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 34d01a504..95f968d37 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -20,6 +20,8 @@ spring:
 
 # 芋道配置项,设置当前项目所有自定义的配置
 yudao:
+  web:
+    api-prefix: /api
   security:
     token-header: Authorization
     token-secret: abcdefghijklmnopqrstuvwxyz