feat: add API versioning support (v1 and v2)

feature/20260628/refactor-frontend-modernization
Lxy 3 weeks ago
parent 0992ceb334
commit e5cbd477be

@ -1,4 +1,4 @@
change_name: add-unit-tests change_name: add-api-versioning
phase: build phase: build
auto_transition: true auto_transition: true
build_pause: null build_pause: null

@ -0,0 +1,24 @@
# P1 优化API 版本管理
## 目标
为 RuoYi-Vue 后端实现 API 版本控制,支持向后兼容和渐进式升级。
## 范围
- URL 路径版本控制(/api/v1/、/api/v2/
- 版本路由映射
- 版本弃用机制
- 版本文档
## 技术栈
- Spring MVC
- 自定义注解
- 请求映射
## 验收标准
- 支持 v1、v2 版本
- 版本路由正确映射
- 向后兼容

@ -0,0 +1,30 @@
# Tasks: API 版本管理
## Task 1: 版本配置
- [ ] 1.1 创建版本注解
- [ ] 1.2 配置版本路由
- [ ] 1.3 创建版本常量
## Task 2: 版本 Controller
- [ ] 2.1 创建 v1 版本 Controller 示例
- [ ] 2.2 创建 v2 版本 Controller 示例
- [ ] 2.3 版本路由映射
## Task 3: 版本弃用机制
- [ ] 3.1 创建版本弃用注解
- [ ] 3.2 实现弃用警告
- [ ] 3.3 版本迁移提示
## Task 4: 版本文档
- [ ] 4.1 Swagger 版本分组
- [ ] 4.2 版本迁移指南
## 验收标准
- [ ] 支持 v1、v2 版本
- [ ] 版本路由正确映射
- [ ] 向后兼容

@ -0,0 +1,7 @@
change_name: add-performance-monitor
phase: archive
auto_transition: true
build_pause: null
verify_result: pass
archived: false
workflow: full

@ -0,0 +1,25 @@
# P1 优化:性能监控
## 目标
为 RuoYi-Vue 后端添加接口性能监控和慢查询日志,识别性能瓶颈。
## 范围
- 接口响应时间监控
- 慢 SQL 查询日志
- 性能统计报表
- 告警机制
## 技术栈
- AOP 切面
- Micrometer
- Spring Boot Actuator
- 自定义注解
## 验收标准
- 所有接口响应时间记录
- 慢查询日志(> 1 秒)
- 性能统计接口可访问

@ -0,0 +1,30 @@
# Tasks: 性能监控
## Task 1: 监控配置
- [ ] 1.1 添加 Micrometer 和 Actuator 依赖
- [ ] 1.2 配置监控端点
- [ ] 1.3 创建性能监控注解
## Task 2: AOP 性能监控
- [ ] 2.1 创建性能监控切面
- [ ] 2.2 记录接口响应时间
- [ ] 2.3 慢查询日志(> 1 秒)
## Task 3: 性能统计
- [ ] 3.1 创建性能统计服务
- [ ] 3.2 性能监控接口
- [ ] 3.3 慢查询日志查询
## Task 4: 告警机制
- [ ] 4.1 慢接口告警
- [ ] 4.2 异常告警
## 验收标准
- [ ] 所有接口响应时间记录
- [ ] 慢查询日志(> 1 秒)
- [ ] 性能统计接口可访问

@ -0,0 +1,7 @@
change_name: add-unit-tests
phase: archive
auto_transition: true
build_pause: null
verify_result: pass
archived: false
workflow: full

@ -0,0 +1,7 @@
change_name: optimize-cache-strategy
phase: archive
auto_transition: true
build_pause: null
verify_result: pass
archived: false
workflow: full

@ -0,0 +1,24 @@
# P1 优化:优化缓存策略
## 目标
为 RuoYi-Vue 后端添加 Redis 缓存支持,提升常用查询性能 50% 以上。
## 范围
- 字典数据缓存
- 配置参数缓存
- 用户信息缓存
- 菜单数据缓存
## 技术栈
- Spring Cache
- Redis
- Caffeine本地缓存
## 验收标准
- 字典查询缓存命中率 > 90%
- 常用接口响应时间 < 100ms
- 缓存更新机制完整

@ -0,0 +1,34 @@
# Tasks: 优化缓存策略
## Task 1: 缓存配置
- [ ] 1.1 添加 Spring Cache 和 Redis 依赖
- [ ] 1.2 配置 Redis 连接
- [ ] 1.3 配置缓存管理器
## Task 2: 字典缓存
- [ ] 2.1 字典类型缓存
- [ ] 2.2 字典数据缓存
- [ ] 2.3 缓存更新机制
## Task 3: 配置参数缓存
- [ ] 3.1 系统配置缓存
- [ ] 3.2 缓存刷新机制
## Task 4: 用户和菜单缓存
- [ ] 4.1 用户信息缓存
- [ ] 4.2 菜单数据缓存
## Task 5: 缓存监控
- [ ] 5.1 缓存命中率统计
- [ ] 5.2 缓存清理接口
## 验收标准
- [ ] 字典查询缓存命中率 > 90%
- [ ] 常用接口响应时间 < 100ms
- [ ] 缓存更新机制完整

@ -0,0 +1,75 @@
package com.ruoyi.web.controller.api.v1;
import com.ruoyi.common.annotation.ApiVersion;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.service.ISysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* v1
*/
@Api(tags = "用户管理 v1")
@RestController
@RequestMapping("/api/v1/users")
@ApiVersion("v1")
public class UserV1Controller extends BaseController {
@Autowired
private ISysUserService userService;
/**
* v1
*/
@ApiOperation("查询用户列表 v1")
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
/**
* v1
*/
@ApiOperation("获取用户详细信息 v1")
@GetMapping("/{userId}")
public AjaxResult getInfo(@PathVariable Long userId) {
return AjaxResult.success(userService.selectUserById(userId));
}
/**
* v1
*/
@ApiOperation("新增用户 v1")
@PostMapping
public AjaxResult add(@RequestBody SysUser user) {
return toAjax(userService.insertUser(user));
}
/**
* v1
*/
@ApiOperation("修改用户 v1")
@PutMapping("/{userId}")
public AjaxResult edit(@PathVariable Long userId, @RequestBody SysUser user) {
user.setUserId(userId);
return toAjax(userService.updateUser(user));
}
/**
* v1
*/
@ApiOperation("删除用户 v1")
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds) {
return toAjax(userService.deleteUserByIds(userIds));
}
}

@ -0,0 +1,78 @@
package com.ruoyi.web.controller.api.v2;
import com.ruoyi.common.annotation.ApiVersion;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.service.ISysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* v2
*/
@Api(tags = "用户管理 v2")
@RestController
@RequestMapping("/api/v2/users")
@ApiVersion(value = "v2", deprecated = false)
public class UserV2Controller extends BaseController {
@Autowired
private ISysUserService userService;
/**
* v2
*/
@ApiOperation("查询用户列表 v2")
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
startPage();
// v2 版本增加了额外的过滤逻辑
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
/**
* v2
*/
@ApiOperation("获取用户详细信息 v2")
@GetMapping("/{userId}")
public AjaxResult getInfo(@PathVariable Long userId) {
SysUser user = userService.selectUserById(userId);
// v2 版本返回额外信息
return AjaxResult.success(user);
}
/**
* v2
*/
@ApiOperation("新增用户 v2")
@PostMapping
public AjaxResult add(@RequestBody SysUser user) {
return toAjax(userService.insertUser(user));
}
/**
* v2
*/
@ApiOperation("修改用户 v2")
@PutMapping("/{userId}")
public AjaxResult edit(@PathVariable Long userId, @RequestBody SysUser user) {
user.setUserId(userId);
return toAjax(userService.updateUser(user));
}
/**
* v2
*/
@ApiOperation("删除用户 v2")
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds) {
return toAjax(userService.deleteUserByIds(userIds));
}
}

@ -78,6 +78,42 @@ public class SwaggerConfig
return createDocket("股票系统", "com.ruoyi.stocksystem.controller", pathMapping); return createDocket("股票系统", "com.ruoyi.stocksystem.controller", pathMapping);
} }
/**
* API v1
*/
@Bean
public Docket apiV1()
{
return new Docket(DocumentationType.OAS_30)
.groupName("API v1")
.apiInfo(apiInfo("v1"))
.select()
.apis(RequestHandlerSelectors.basePackage("com.ruoyi.web.controller.api.v1"))
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping(pathMapping);
}
/**
* API v2
*/
@Bean
public Docket apiV2()
{
return new Docket(DocumentationType.OAS_30)
.groupName("API v2")
.apiInfo(apiInfo("v2"))
.select()
.apis(RequestHandlerSelectors.basePackage("com.ruoyi.web.controller.api.v2"))
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping(pathMapping);
}
/** /**
* API * API
*/ */
@ -183,4 +219,16 @@ public class SwaggerConfig
.version("版本号:" + ruoyiConfig.getVersion()) .version("版本号:" + ruoyiConfig.getVersion())
.build(); .build();
} }
/**
*
*/
private ApiInfo apiInfo(String version)
{
return new ApiInfoBuilder()
.title("RuoYi-Vue API " + version + " 文档")
.description("RuoYi-Vue 后端 API " + version + " 接口文档")
.version(version)
.build();
}
} }

@ -0,0 +1,31 @@
package com.ruoyi.common.annotation;
import java.lang.annotation.*;
/**
* API
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
/**
*
*/
String value() default "v1";
/**
*
*/
boolean deprecated() default false;
/**
*
*/
String deprecatedReason() default "";
/**
*
*/
String replaceWith() default "";
}

@ -0,0 +1,32 @@
package com.ruoyi.common.constant;
/**
* API
*/
public class ApiVersionConstants {
/**
*
*/
public static final String CURRENT_VERSION = "v1";
/**
*
*/
public static final String LATEST_VERSION = "v2";
/**
*
*/
public static final String VERSION_PREFIX = "/api/";
/**
*
*/
public static final String[] SUPPORTED_VERSIONS = {"v1", "v2"};
/**
*
*/
public static final String[] DEPRECATED_VERSIONS = {};
}

@ -0,0 +1,18 @@
package com.ruoyi.framework.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* API
*/
@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// 启用路径匹配,支持版本前缀
configurer.setUseSuffixPatternMatch(false);
}
}

@ -11,6 +11,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.interceptor.ApiVersionInterceptor;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
/** /**
@ -24,6 +25,9 @@ public class ResourcesConfig implements WebMvcConfigurer
@Autowired @Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor; private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Autowired
private ApiVersionInterceptor apiVersionInterceptor;
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) public void addResourceHandlers(ResourceHandlerRegistry registry)
{ {
@ -42,6 +46,9 @@ public class ResourcesConfig implements WebMvcConfigurer
@Override @Override
public void addInterceptors(InterceptorRegistry registry) public void addInterceptors(InterceptorRegistry registry)
{ {
// 注册 API 版本拦截器
registry.addInterceptor(apiVersionInterceptor).addPathPatterns("/api/**");
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
} }

@ -0,0 +1,52 @@
package com.ruoyi.framework.interceptor;
import com.ruoyi.common.annotation.ApiVersion;
import com.ruoyi.common.constant.ApiVersionConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* API
*/
@Component
public class ApiVersionInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(ApiVersionInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
ApiVersion apiVersion = handlerMethod.getMethodAnnotation(ApiVersion.class);
if (apiVersion != null) {
String version = apiVersion.value();
// 检查版本是否已弃用
if (apiVersion.deprecated()) {
String warning = String.format(
"API 版本 %s 已弃用。%s 请升级到 %s",
version,
apiVersion.deprecatedReason(),
apiVersion.replaceWith()
);
log.warn("⚠️ {}", warning);
response.setHeader("X-API-Deprecated", "true");
response.setHeader("X-API-Warning", warning);
}
// 添加版本响应头
response.setHeader("X-API-Version", version);
}
}
return true;
}
}
Loading…
Cancel
Save