领取优惠


若依毕业设计实战:从模块定制到生产级部署的完整路径

摘要:许多学生在使用若依(RuoYi)框架完成毕业设计时,常陷入“能跑但不可维护”的困境:权限配置混乱、前后端耦合严重、缺乏真实业务场景适配。本文基于实战经验,详解如何在若依基础上进行模块化改造,实现角色-菜单-API三级权限解耦,并集成JWT令牌刷新、操作日志审计等生产级功能。读者将掌握一套可复用的二次开发模板,显著提升系统健壮性与答辩竞争力。


1. 学生常见痛点:为什么“能跑”≠“能交付”

  1. 权限模型僵化
    若依默认“角色-菜单”二级模型,在毕设场景里一旦遇到“同一角色不同数据范围”就抓瞎。例如“班主任”只能看本班成绩,但校级管理员要看全部,硬编码 if(role==='admin') 很快变成灾难。

  2. 代码冗余 & 逆向工程黑箱
    很多同学直接跑 ruoyi-generator 一键生成代码,结果 Controller/Service/Mapper 三层堆满重复 getByIdlistAll,后期需求一改,牵一发动全身,Git 提交记录全是“fix again”。

  3. 前后端耦合
    把 Vue 文件直接塞进 resources/static 里,打包一次 3 min,上线发现路径 404,又回夹带 baseUrl = window.location.origin 这种“玄学补丁”。

  4. 缺失真实业务场景
    为了“有图有真相”,硬塞一个商城秒杀模块,但订单状态机、库存扣减全是伪代码,老师一问“超卖怎么解决”就原地爆炸。


2. 技术选型:MyBatis-Plus vs JPA 的适配成本

  1. 若依原生 MyBatis 的优劣

    • 优点:SQL 可控、分页插件成熟、二级缓存好配
    • 缺点:手写 XML 费手,字段一改全文搜索替换
  2. 换 MyBatis-Plus 的代价

    • 只需把 mybatis 依赖换成 mybatis-plus-boot-starter,旧 XML 零改动即可运行
    • 代码生成器模板里把 Mapper.java 的基类换成 BaseMapper<T>,即可砍掉 80% 的 *Mapper.xml
    • 注意:若依自带的 DataScopeInterceptor 依赖原生 MappedStatement,需要重写一个 PlusDataScopeInterceptor,成本 1 h 以内
  3. 换 Spring Data JPA 的代价

    • 需要把 BaseEntity@TableId(type = IdType.AUTO) 改成 @GeneratedValue
    • 所有 *Mapper 接口要新写 @Entity@Repository
    • 若依的 PageUtils 与 JPA 的 Pageable 不是一一对应,分页封装得重写
    • 结论:毕业设计周期 3 个月以内,不建议折腾 JPA,MyBatis-Plus 是性价比之王

3. 核心改造 1:三级权限解耦

目标:角色 → 菜单 → API 按钮,三表分离,支持“同角色不同数据行级权限”。

  1. 数据模型微调
    sys_rolesys_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  预留,给前端按钮用
    
  2. 自定义注解 @RequiresPermissionsExt

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequiresPermissionsExt {
        String[] value() default {};   // 需要权限字符数组
        DataScope dataScope() default DataScope.ALL;
    }
    
  3. AOP 拦截实现逻辑

    • 前置:读取 SecurityUtils.getLoginUser().getUserId()
    • 查询 sys_role_menu_scope 拿到当前用户在该菜单下的 dept_ids
    • dept_ids 塞进 ThreadLocal<List<Long>>
    • MyBatis-Plus 拦截器读取 ThreadLocal,自动拼 AND dept_id IN (...)
    • 后置:清理 ThreadLocal 防止内存泄漏
  4. 使用示例

    @RequiresPermissionsExt(value={"student:score:view"}, dataScope=DataScope.DEPT)
    @GetMapping("/score")
    public TableDataInfo list(ScoreDTO dto){
        return scoreService.selectPage(dto);
    }
    

    至此,同一“班主任”角色登录,只能看到本班成绩;换成“教学秘书”角色,即可看到全院数据,代码零侵入。


4. 核心改造 2:JWT 刷新 & 并发登录控制

  1. 刷新拦截器(关键注释已写)

    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;
        }
    }
    
  2. 并发登录踢下线

    • 登录成功后 RedisUtils.setCacheObject("user:kick:" + userId, token, 过期时间)
    • 每次请求比较 Redistoken 是否一致,不一致返回 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 注入双重保险

  1. XSS 全局过滤器
    继承 HttpServletRequestWrapper,重写 getParametergetInputStream,用 Jsoup.clean 过滤白名单标签。

  2. SQL 注入
    MyBatis-Plus 自带 #{} 占位符已 99% 免疫,但代码里还有同学写 order by ${sort} 排序,务必把 sort 字段拉白名单校验:

    if (!ArrayUtils.contains(whitelist, sort)) throw new BadRequestException("非法排序字段");
    
  3. 操作日志审计
    利用 @Log 注解 + AOP,把 ip、url、method、params、userId、time 写表,答辩时老师最爱问“你怎么追踪误删数据”,直接甩日志表。


7. 生产环境避坑指南

  1. 数据库命名规范

    • 统一小写下划线,主键 bigint(20) 自增
    • 所有表必须 create_time datetime DEFAULT CURRENT_TIMESTAMP
    • 索引命名 idx_表名_字段名,唯一索引 uk_表名_字段名,方便 DBA 快速定位
  2. Swagger 文档暴露
    生产环境务必加网关拦截:

    spring:
      profiles:
        active: prod
    knife4j:
      production: true   # 关闭接口页面
    

    或者 Nginx 层直接 return 404 /swagger-ui.html

  3. 冷启动提速

    • 关闭 spring.devtools.restart
    • BeanValidation 换成 hibernate.validatorprecompile 版本
    • 使用 spring-context-indexer 生成 spring.components,本地启动从 12 s 降到 7 s,服务器 2 vCPU 4 G 首次启动 25 s → 15 s
  4. 日志切割
    使用 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.jspublicPath 改成 ./,再用 GitHub Actions 自动打包推镜像,答辩演示时直接打开公网域名,老师点赞“像企业级”。


9. 结语:把 Demo 变产品的下一步

走完上面的改造,你的“若依毕业设计”已经脱离“课程级别”,具备可维护、可扩展、可交付的雏形。但别急着关机睡觉,最后留两个小作业:

  1. 把 README 写成产品白皮书:背景、功能、架构图、部署手册、接口示例,一个都不能少——导师一看就知道“这学生能毕业”。
  2. 做一次“假压力测试”:用 wrk -t12 -c400 -d30s 打 1 w 并发,把 QPS、RT、错误率截屏贴报告里,老师想挑刺都找不到角度。

毕业设计不是终点,而是你把“演示系统”转化为“可交付产品”的第一块跳板。动手重构你的若依项目,下次面试官问“有没有上线经验”,你可以把域名甩给他,顺便讲讲 JWT 刷新、并发踢人、数据权限三级解耦——offer 稳了。

领取优惠


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐