1. 若依前后端分离框架概述
若依(RuoYi)是一套基于Spring Boot和Vue.js的前后端分离权限管理系统,在中小型企业级应用开发中广受欢迎。作为一个全栈开发者,我在最近的项目中采用了若依框架进行开发,期间遇到了不少典型问题,这里记录下几个具有代表性的bug修复过程。
若依前后端分离版的核心架构由以下部分组成:
- 后端:Spring Boot + MyBatis/MyBatis-Plus + Redis
- 前端:Vue 2.x + Element UI + Axios
- 认证:基于JWT的Token认证机制
- 权限:RBAC(基于角色的访问控制)模型
提示:若依框架的默认数据库脚本会创建sys_user、sys_role、sys_menu等核心表,理解这些表结构对后续问题排查至关重要。
2. 跨域问题排查与修复
2.1 现象描述
在本地开发环境中,前端Vue项目运行在8080端口,后端Spring Boot运行在8088端口。当前端调用后端接口时,浏览器控制台出现CORS错误:
code复制Access to XMLHttpRequest at 'http://localhost:8088/api/login'
from origin 'http://localhost:8080' has been blocked by CORS policy
2.2 排查过程
- 首先检查后端代码中的CORS配置。若依默认在
config/CorsConfig.java中已经配置了允许跨域:
java复制@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
-
发现配置看似正确,但问题仍然存在。通过浏览器开发者工具检查网络请求,发现OPTIONS预检请求返回403状态码。
-
进一步排查发现是Spring Security的CSRF保护机制拦截了请求。虽然开发环境可以禁用CSRF,但生产环境需要保留,因此需要更精细的配置。
2.3 解决方案
修改SecurityConfig.java中的配置:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用CSRF对OPTIONS请求的限制
.csrf().ignoringAntMatchers(HttpMethod.OPTIONS, "/**")
.and()
.authorizeRequests()
// 其他配置保持不变...
}
同时在前端vue.config.js中配置代理,避免开发环境跨域:
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8088',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
3. 动态菜单加载异常问题
3.1 问题现象
用户登录后,左侧菜单栏部分菜单项重复显示,且某些权限正确的菜单项未显示。
3.2 原因分析
- 检查数据库
sys_menu表,发现存在多个菜单项具有相同的menu_name但不同的menu_id。 - 查看后端代码
SysMenuController.java,发现菜单查询逻辑没有问题:
java复制@GetMapping("getRouters")
public AjaxResult getRouters() {
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
- 问题出在前端的菜单处理逻辑。查看
src/store/modules/permission.js,发现菜单去重逻辑有缺陷。
3.3 解决方案
修改前端菜单处理逻辑:
javascript复制// 在permission.js中修改buildSidebar方法
function buildSidebar(menus) {
const uniqueMenus = [];
const menuMap = new Map();
menus.forEach(menu => {
if (!menuMap.has(menu.path)) {
menuMap.set(menu.path, true);
uniqueMenus.push(menu);
}
});
return uniqueMenus;
}
同时修复数据库中的重复菜单项:
sql复制-- 查找重复菜单项
SELECT menu_name, COUNT(*)
FROM sys_menu
GROUP BY menu_name
HAVING COUNT(*) > 1;
-- 根据实际情况删除或修改重复项
4. 按钮权限控制失效问题
4.1 问题描述
在某个页面中,即使当前用户没有某个按钮的权限,该按钮仍然显示并可点击。
4.2 排查过程
- 检查前端按钮权限指令
v-hasPermi的使用方式:
vue复制<el-button v-hasPermi="['system:user:add']">新增用户</el-button>
- 查看指令定义
src/directive/permission/hasPermi.js:
javascript复制import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding
const permissions = store.getters && store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
const hasPermissions = permissions.some(permission => {
return permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}
- 发现权限判断逻辑正确,问题出在权限数据获取环节。检查Vuex中的permissions状态,发现未正确更新。
4.3 解决方案
- 确保登录后正确获取权限数据。修改
src/api/login.js中的登录方法:
javascript复制export function login(data) {
return request({
url: '/login',
method: 'post',
data: data
}).then(res => {
// 登录成功后立即获取权限信息
store.dispatch('GetInfo').then(() => {
// 权限信息获取完成后再跳转
router.push({ path: '/' })
})
return res
})
}
- 在Vuex的
user.js中确保权限信息正确存储:
javascript复制const actions = {
GetInfo({ commit }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const { roles, permissions } = response
commit('SET_ROLES', roles)
commit('SET_PERMISSIONS', permissions)
resolve(response)
}).catch(error => {
reject(error)
})
})
}
}
5. 文件上传路径配置问题
5.1 问题现象
在Windows开发环境上传文件正常,但部署到Linux服务器后文件上传失败。
5.2 原因分析
- 检查若依的文件上传配置
application.yml:
yaml复制# 文件路径配置
file:
path: D:/ruoyi/uploadPath
- 显然,Windows风格的路径在Linux系统中无法识别。
- 查看文件上传代码
LocalFileUploadServiceImpl.java,发现路径处理没有考虑跨平台兼容性。
5.3 解决方案
- 修改配置文件为相对路径:
yaml复制file:
path: ./uploadPath
- 在代码中处理路径兼容性:
java复制public String getDefaultBaseDir() {
String baseDir = RuoYiConfig.getProfile();
if (StringUtils.isEmpty(baseDir)) {
baseDir = System.getProperty("user.dir") + File.separator + "uploadPath";
}
return baseDir;
}
- 确保部署时uploadPath目录有写入权限:
bash复制mkdir -p /deploy/path/uploadPath
chmod 777 /deploy/path/uploadPath
6. Redis缓存异常导致重复登录
6.1 问题描述
用户登录后,短时间内重复登录会创建多个Token,导致之前的会话失效。
6.2 排查过程
- 检查登录逻辑
SysLoginService.java:
java复制public String login(String username, String password) {
// 验证用户名密码...
// 生成Token
String token = TokenService.createToken(loginUser);
// 缓存用户信息
redisCache.setCacheObject(getTokenKey(token), loginUser);
return token;
}
- 发现当用户已登录时,系统没有检查现有Token并清除。
- 查看Token生成逻辑,发现没有对同一用户的多个Token进行管理。
6.3 解决方案
修改登录逻辑,确保同一用户只有一个有效Token:
java复制public String login(String username, String password) {
// 验证用户名密码...
// 清除该用户已有的Token
String oldTokenKey = "login_tokens:" + loginUser.getUserid();
String oldToken = redisCache.getCacheObject(oldTokenKey);
if (oldToken != null) {
redisCache.deleteObject(getTokenKey(oldToken));
}
// 生成新Token
String token = TokenService.createToken(loginUser);
redisCache.setCacheObject(getTokenKey(token), loginUser);
// 存储用户与Token的映射关系
redisCache.setCacheObject(oldTokenKey, token);
return token;
}
7. 数据库连接池配置优化
7.1 问题现象
系统在高并发时出现数据库连接获取超时错误。
7.2 原因分析
检查若依默认的Druid连接池配置application-druid.yml:
yaml复制# 数据源配置
spring:
datasource:
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
这些配置对于高并发场景来说过于保守。
7.3 优化方案
根据服务器资源和预期并发量调整配置:
yaml复制spring:
datasource:
druid:
initial-size: 10
min-idle: 10
max-active: 100
max-wait: 5000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
同时添加监控配置:
yaml复制 stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: admin
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
8. 前端性能优化实践
8.1 问题现象
随着功能模块增加,前端打包体积过大,导致首屏加载缓慢。
8.2 优化方案
- 路由懒加载:修改
router/index.js
javascript复制const Login = () => import('@/views/login')
const Home = () => import('@/views/home')
- 按需引入Element UI组件:创建
src/plugins/element.js
javascript复制import Vue from 'vue'
import {
Button,
Table,
Pagination
// 只引入需要的组件
} from 'element-ui'
Vue.use(Button)
Vue.use(Table)
Vue.use(Pagination)
- 配置Gzip压缩:修改
vue.config.js
javascript复制const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: {
plugins: [
new CompressionPlugin({
test: /\.js$|\.html$|\.css$/,
threshold: 10240,
deleteOriginalAssets: false
})
]
}
- 使用CDN加速:在
public/index.html中添加
html复制<!-- 引入Vue和Element UI的CDN -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/theme-chalk/index.css">
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/index.js"></script>
然后在vue.config.js中配置externals:
javascript复制configureWebpack: {
externals: {
'vue': 'Vue',
'element-ui': 'ELEMENT'
}
}
9. 部署时的常见问题
9.1 Nginx配置问题
若依前后端分离项目部署时,Nginx配置需要注意以下几点:
nginx复制server {
listen 80;
server_name yourdomain.com;
# 前端项目
location / {
root /home/ruoyi/projects/front/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 后端API代理
location /api/ {
proxy_pass http://localhost:8088/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
9.2 生产环境HTTPS配置
- 申请SSL证书(如Let's Encrypt)
- 修改Nginx配置:
nginx复制server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 其他配置同上...
}
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
9.3 前端路由刷新404问题
确保Nginx配置中包含:
nginx复制location / {
try_files $uri $uri/ /index.html;
}
10. 开发环境优化建议
- 后端热部署:在pom.xml中添加devtools依赖
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
- 前端开发代理:完善
vue.config.js的代理配置
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8088',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
// 解决WebSocket代理问题
ws: true
}
}
}
- 数据库版本控制:集成Flyway管理数据库变更
xml复制<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
在application.yml中配置:
yaml复制spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
- API文档集成:完善Swagger配置
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.ruoyi.web.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("若依接口文档")
.description("RuoYi前后端分离版API文档")
.version("1.0")
.build();
}
}
访问地址:http://localhost:8088/swagger-ui.html
