若依毕业设计实战:从模块定制到生产级部署的完整路径
摘要:许多学生在使用若依(RuoYi)框架完成毕业设计时,常陷入“能跑但不可维护”的困境:权限配置混乱、前后端耦合严重、缺乏真实业务场景适配。本文基于实战经验,详解如何在若依基础上进行模块化改造,实现角色-菜单-API三级权限解耦,并集成JWT令牌刷新、操作日志审计等生产级功能。读者将掌握一套可复用的二次开发模板,显著提升系统健壮性与答辩竞争力。
1. 学生常见痛点:为什么“能跑”≠“能交付”
权限模型僵化
若依默认“角色-菜单”二级模型,在毕设场景里一旦遇到“同一角色不同数据范围”就抓瞎。例如“班主任”只能看本班成绩,但校级管理员要看全部,硬编码if(role==='admin')很快变成灾难。代码冗余 & 逆向工程黑箱
很多同学直接跑ruoyi-generator一键生成代码,结果Controller/Service/Mapper三层堆满重复getById、listAll,后期需求一改,牵一发动全身,Git 提交记录全是“fix again”。前后端耦合
把 Vue 文件直接塞进resources/static里,打包一次 3 min,上线发现路径 404,又回夹带baseUrl = window.location.origin这种“玄学补丁”。缺失真实业务场景
为了“有图有真相”,硬塞一个商城秒杀模块,但订单状态机、库存扣减全是伪代码,老师一问“超卖怎么解决”就原地爆炸。
2. 技术选型:MyBatis-Plus vs JPA 的适配成本
若依原生 MyBatis 的优劣
- 优点:SQL 可控、分页插件成熟、二级缓存好配
- 缺点:手写 XML 费手,字段一改全文搜索替换
换 MyBatis-Plus 的代价
- 只需把
mybatis依赖换成mybatis-plus-boot-starter,旧 XML 零改动即可运行 - 代码生成器模板里把
Mapper.java的基类换成BaseMapper<T>,即可砍掉 80% 的*Mapper.xml - 注意:若依自带的
DataScopeInterceptor依赖原生MappedStatement,需要重写一个PlusDataScopeInterceptor,成本 1 h 以内
- 只需把
换 Spring Data JPA 的代价
- 需要把
BaseEntity的@TableId(type = IdType.AUTO)改成@GeneratedValue - 所有
*Mapper接口要新写@Entity与@Repository - 若依的
PageUtils与 JPA 的Pageable不是一一对应,分页封装得重写 - 结论:毕业设计周期 3 个月以内,不建议折腾 JPA,MyBatis-Plus 是性价比之王
- 需要把
3. 核心改造 1:三级权限解耦
目标:角色 → 菜单 → API 按钮,三表分离,支持“同角色不同数据行级权限”。
数据模型微调
在sys_role与sys_menu之间增加sys_role_menu_scope表,核心字段:id bigint PK role_id bigint FK→sys_role menu_id bigint FK→sys_menu dept_ids varchar 数据范围,逗号分隔部门 ID api_tag varchar 预留,给前端按钮用自定义注解
@RequiresPermissionsExt@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermissionsExt { String[] value() default {}; // 需要权限字符数组 DataScope dataScope() default DataScope.ALL; }AOP 拦截实现逻辑
- 前置:读取
SecurityUtils.getLoginUser().getUserId() - 查询
sys_role_menu_scope拿到当前用户在该菜单下的dept_ids - 把
dept_ids塞进ThreadLocal<List<Long>> - MyBatis-Plus 拦截器读取
ThreadLocal,自动拼AND dept_id IN (...) - 后置:清理
ThreadLocal防止内存泄漏
- 前置:读取
使用示例
@RequiresPermissionsExt(value={"student:score:view"}, dataScope=DataScope.DEPT) @GetMapping("/score") public TableDataInfo list(ScoreDTO dto){ return scoreService.selectPage(dto); }至此,同一“班主任”角色登录,只能看到本班成绩;换成“教学秘书”角色,即可看到全院数据,代码零侵入。
4. 核心改造 2:JWT 刷新 & 并发登录控制
刷新拦截器(关键注释已写)
public class JwtRefreshInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = JwtUtils.getToken(request); Claims claims = JwtUtils.getClaims(token); // 1. 剩余时间 < 10 min 则刷新 if (claims.getExpiration().getTime() - System.currentTimeMillis() < 10 * 60 * 1000) { String userKey = claims.get("userKey").toString(); LoginUser loginUser = SpringUtils.getBean(TokenService.class) .getLoginUser(userKey); // 2. 颁发新令牌,旧令牌加入 Redis 黑名单 5 min String newToken = JwtUtils.createToken(loginUser); response.setHeader("Authorization", newToken); RedisUtils.setCacheObject(Constants.JWT_BLACK + token, "1", 5 * 60); } return true; } }并发登录踢下线
- 登录成功后
RedisUtils.setCacheObject("user:kick:" + userId, token, 过期时间) - 每次请求比较
Redis中token是否一致,不一致返回 401 - 支持开关配置:
single.login=true/false
- 登录成功后
5. 前端路由守卫:让按钮级权限也“动”起来
// permission.js router.beforeEach(async (to, from, next) => { // 1. 白名单 if (whiteList.indexOf(to.path) !== -1) { next(); return } // 2. 取用户信息 const userStore = useUserStore() if (!userStore.token) { next(`/login?redirect=${to.fullPath}`); return } // 3. 动态路由未加载则拉菜单 if (!userStore.hasRoutes) { const accessRoutes = await userStore.generateRoutes() accessRoutes.forEach(r => router.addRoute(r)) next({ ...to, replace: true }) } else { next() } })按钮级权限指令:
app.directive('permission', { mounted(el, binding) { const { value } = binding const permissions = useUserStore().permissions if (value && !permissions.includes(value)) { el.parentNode && el.parentNode.removeChild(el) } } })模板用法:
<el-button v-permission="'student:score:export'">导出成绩</el-button>6. 安全加固:XSS + SQL 注入双重保险
XSS 全局过滤器
继承HttpServletRequestWrapper,重写getParameter、getInputStream,用Jsoup.clean过滤白名单标签。SQL 注入
MyBatis-Plus 自带#{}占位符已 99% 免疫,但代码里还有同学写order by ${sort}排序,务必把sort字段拉白名单校验:if (!ArrayUtils.contains(whitelist, sort)) throw new BadRequestException("非法排序字段");操作日志审计
利用@Log注解 + AOP,把ip、url、method、params、userId、time写表,答辩时老师最爱问“你怎么追踪误删数据”,直接甩日志表。
7. 生产环境避坑指南
数据库命名规范
- 统一小写下划线,主键
bigint(20)自增 - 所有表必须
create_time datetime DEFAULT CURRENT_TIMESTAMP - 索引命名
idx_表名_字段名,唯一索引uk_表名_字段名,方便 DBA 快速定位
- 统一小写下划线,主键
Swagger 文档暴露
生产环境务必加网关拦截:spring: profiles: active: prod knife4j: production: true # 关闭接口页面或者 Nginx 层直接
return 404 /swagger-ui.html冷启动提速
- 关闭
spring.devtools.restart - 把
BeanValidation换成hibernate.validator的precompile版本 - 使用
spring-context-indexer生成spring.components,本地启动从 12 s 降到 7 s,服务器 2 vCPU 4 G 首次启动 25 s → 15 s
- 关闭
日志切割
使用logback-spring.xml按 100 MB 切割,保留 30 天,防止磁盘爆满把服务拖死。
8. 部署脚本:一条命令上云
#!/bin/bash # build.sh mvn clean package -Dmaven.test.skip=true docker build -t ruoyi-project:1.0.0 . docker run -d --name ruoyi-prod \ -e SPRING_PROFILES_ACTIVE=prod \ -e MYSQL_URL=jdbc:mysql://10.0.0.8:3306/ruoyi_prod \ -p 8080:8080 ruoyi-project:1.0.0前端独立容器:
FROM nginx:alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf把vue.config.js的publicPath改成./,再用 GitHub Actions 自动打包推镜像,答辩演示时直接打开公网域名,老师点赞“像企业级”。
9. 结语:把 Demo 变产品的下一步
走完上面的改造,你的“若依毕业设计”已经脱离“课程级别”,具备可维护、可扩展、可交付的雏形。但别急着关机睡觉,最后留两个小作业:
- 把 README 写成产品白皮书:背景、功能、架构图、部署手册、接口示例,一个都不能少——导师一看就知道“这学生能毕业”。
- 做一次“假压力测试”:用
wrk -t12 -c400 -d30s打 1 w 并发,把 QPS、RT、错误率截屏贴报告里,老师想挑刺都找不到角度。
毕业设计不是终点,而是你把“演示系统”转化为“可交付产品”的第一块跳板。动手重构你的若依项目,下次面试官问“有没有上线经验”,你可以把域名甩给他,顺便讲讲 JWT 刷新、并发踢人、数据权限三级解耦——offer 稳了。