企业级学习平台源码实战:SpringBoot+Vue+RBAC权限与表设计

发布时间:2026/10/13 3:39:24
企业级学习平台源码实战:SpringBoot+Vue+RBAC权限与表设计
拿到一套带完整源码的企业级系统最怕什么不是代码跑不起来而是你根本不知道这套系统应该怎么用、表为什么这么建、权限卡在哪一层。我最近把一套基于SpringBootVueMyBatis架构、搭配MySQL数据库的企业级智能学习平台管理系统源码从头到尾过了一遍从初始化SQL到前端页面再到部署配置全部走通中间踩了不少坑也理清了很多设计层面的逻辑。这篇就用实战复盘的视角来讲这套系统它到底包含哪些核心模块、这套技术选型为什么经典、数据库表之间怎么关联、三种角色的权限如何在前后端同时落地、以及我在本地跑通和联调过程中遇到的具体问题。如果你正打算做在线教育平台或者企业内部培训系统这篇应该能帮你省掉不少摸索时间。1. 这套系统解决的不是看视频上课而是培训管理全链路1.1 三个端口各管一摊事这类学习平台和市面上那种个人向的视频网站完全是两码事。个人向产品只需要用户注册、看视频、评论本质是一个内容前台但企业级学习平台的核心是管理二字系统天然拆成三个角色入口各看各的界面各调各的接口。我把三个端口的分工整理了一下端口主要使用者核心功能典型页面学员端普通学员/员工浏览课程、选课、观看视频、参加考试、查看成绩、错题本、学习记录课程列表、课程详情、播放页、考试页讲师端讲师/培训老师课程与章节管理、视频上传、题库维护、组卷、发布考试、批改作业课程管理页、题目编辑页、试卷配置页管理端系统管理员用户管理、角色权限分配、课程审核、公告维护、数据统计大屏用户列表、角色配置、统计报表三个端口共用同一个后端和数据库但接口路径不同、权限边界不同前端工程也分成三套路由来组织。源码里的目录结构也是这样设计的业务模块按course、exam、user、statistics分包controller、service、mapper层层隔离看代码的时候非常容易定位。1.2 一条核心业务闭环串起全部功能我梳理这套源码时发现它的功能虽然模块化但业务上是一条完整闭环管理员配置人员和权限 → 讲师创建课程并发布考试 → 学员选课学习 → 学习进度被记录 → 考试答题生成成绩 → 错题进入错题本 → 管理端统计看板汇总所有数据。以一场企业内部培训为例管理员先导入学员和讲师账号给讲师分配课程管理权限讲师创建课程后录入章节课时上传视频同时在题库里录题目、组一套试卷并发布学员在学员端选这门课看完几个课时后参加考试。考试提交后系统自动判定客观题简答题由讲师手工批改。最终管理端能看到完课率、考试通过率、平均学习时长等聚合数据。这个闭环走通之后才叫企业级学习平台否则就只是几个功能页面的拼凑。1.3 企业级到底体现在哪几个细节很多初学者拿到这类源码很容易忽略企业级这三个字的分量。我在实际跑这套系统时专门留意了四个细节第一权限是分层的。不只是登录了就能进而是菜单权限、按钮权限都做了区分。学员看不到讲师的管理入口讲师也看不到管理端的用户管理这些都是由后端接口权限加前端路由权限双重控制的。第二有操作审计的痕迹。比如删除课程、修改用户角色这类敏感操作源码里设计了日志记录能追踪到是哪个用户在什么时间做了什么操作这对企业合规审计非常重要。第三统一异常处理和统一返回体。所有接口返回固定的code、message、data结构前端axios拦截器统一处理错误弹提示不会出现一个接口返回一种格式的情况。第四模块化程度足够高。课程、考试、用户、统计之间耦合度低后续如果要加支付、加直播、加问卷调查都是在现有模块基础上扩展而不是推倒重来。2. 技术选型复盘为什么是SpringBootVueMyBatis这套组合2.1 Spring Boot MyBatis选择可控而不是省事这套源码的后端是Spring Boot MyBatis而不是Spring Boot JPA这是有讲究的。学习平台这类管理系统多表联查非常多课程列表要关联分类名、讲师名、选课人数考试记录要关联试卷信息和学员信息统计报表要按时间聚合多条数据。MyBatis的SQL完全由开发者手写复杂查询怎么写、怎么优化心里一清二楚。我对比过JPA在这类场景下的体验单表CRUD确实省事但一旦涉及三表以上关联JPQL和Criteria API的调试成本会明显上升而且生成的SQL不一定走你想要的索引。反过来MyBatis的XML里写动态SQL非常直观比如课程列表的筛选条件分类、状态、关键词用where和if标签组合拼接逻辑一眼就能看懂后期调SQL只要对着MySQL的执行计划改就行。Spring Boot本身没什么好争议的它内置Tomcat、自动配置、起步依赖让项目从配置到启动都非常顺。相比早年的SSH框架要写一堆XML配置Spring Boot只需要一个application.yml就能把数据源、MyBatis、日志全部配置完开发效率和可维护性完全不在一个量级。2.2 Vue Element UI管理系统前端的稳妥方案前端用的是Vue加Element UI组件库。选择这个组合的原因很实在Element UI的表格、表单、弹窗、分页组件对管理系统来说太完善了几乎不需要自己造轮子。课程管理页的表格展示、角色配置的树形控件、统计页的图表90%都是组件拼装。Vue本身的双向绑定和组件化也很贴合这类系统的开发节奏。一个页面就是一个组件页面上不同模块拆成子组件数据通过props和事件传递状态管理用Vuex存用户信息和菜单权限刷新页面后重新拉取。这套模式在大量管理后台项目中得到过验证社区资料也全遇到问题搜一下基本都有答案。有人会问为什么不用Vue 3和Element Plus实话说如果从零开发Vue 3 Element Plus也完全可以Vite的构建速度还更快。但这套源码是基于Vue 2稳定生态搭建的第三方的兼容性、历史模板的成熟度、以及团队上手成本都更可控。企业项目里稳妥能用永远比版本最新重要。2.3 可以直接照抄的版本组合我在本地跑通这套源码用的版本组合是经过验证的列出来供参考组件版本说明JDK1.8稳定且兼容性最好几乎所有jar都能跑Spring Boot2.7.x长期维护版本适合企业级业务系统MyBatis Starter2.3.x配合Spring Boot 2.7使用正常MySQL5.7或8.0数据库字符集必须用utf8mb4Vue2.6.x配合Element UI 2.15.xElement UI2.15.x组件全文档成熟Node.js16.x编译Vue 2工程够用不用上高版本Nginx1.20前后端分离部署时用提示别一上来非要上最新版。JDK 17虽然性能更好但一些老版本的Maven插件和第三方包会出现兼容问题排查成本高对这套管理系统来说没有任何必要。3. 从建表开始讲学习平台核心表结构的设计逻辑3.1 用户、角色、菜单三张基础表RBAC的地基这套系统的权限模型是经典的RBAC用户-角色-权限设计。基础表有三张user用户表、role角色表、menu菜单/权限表再加上两张关联表user_role和role_menu实现用户和权限的多对多关系。用户表的核心字段包括username、passwordBCrypt加密后存储、real_name、phone、status1启用0禁用、create_time。角色表则是role_name和role_code比如admin、teacher、student。菜单表是树形结构parent_id指向父菜单perms字段存按钮权限标识比如course:delete、user:resetPwd。我在看这张表设计时有一个明显的感受它把菜单权限和按钮权限统一进了menu表。菜单树用来生成前端导航按钮的perms标识用来做页面内的操作控制。前端根据perms判断是否渲染删除审核按钮后端拦截器校验接口时也对照同一份perms一套数据两处使用不会出现权限前后端对不上的问题。3.2 课程表到学习进度一条从course到study_record的链路课程相关表是这套系统里关联路径最长的。从上层往下依次是course_category课程分类、course课程主表、course_chapter章、course_section节/课时、user_course选课关系表、study_record学习记录表。课程主表存title、cover、category_id、teacher_id关联用户表的讲师、level难度、status草稿/已发布/下架、study_count。章节表结构很直接章下面挂节节下面存video_url、duration、is_free是否试看。user_course表记录学员选了哪门课字段是user_id course_id progress。真正的学到哪里了则记录在study_record表里每看完一个课时就插入或更新一条记录字段包括user_id、section_id、watch_duration、is_finished。这里要重点说progress。很多源码会把学习进度直接算好存在user_course表但这套系统聪明的一点是进度字段只是一个可查询的冗余值真正的依据是 study_record 表的明细。讲师端想要看这个学员到底看了几个课时、每节看了多久查询明细表就能还原真实情况避免出现进度显示80%但学员其实只看过开头五分钟的问题。算进度的方法是已完成的课时数 ÷ 课程总课时数 × 100%前端在课程列表中直接展示这个字段。3.3 考试模块题库、试卷、答卷、错题本四张表的关系考试模块的表设计比课程表更复杂因为涉及同一道题可以出现在不同试卷里的多对多关系。核心表有这么几张exam_question是题库表字段包括question_type单选、多选、判断、填空、简答、content题干、options选项通常用JSON字符串存储比如[A. Spring Boot, B. Django, ...]、answer标准答案、analysis答案解析、score默认分值。exam_paper是试卷表字段包括title、course_id、total_score、pass_score、duration考试时长单位分钟、status。试卷和题目之间通过中间表paper_question关联记录这道题在这张卷子里是第几题、分值是多少这样同一道题在不同试卷里可以有不同分值。学员参加考试后产生两张表user_exam记录一次考试的总体情况比如得分、提交时间、是否通过user_answer是答题明细表记录每道题学员选了什么、是否正确。系统在交卷时逐题比对答案客观题自动判分简答题设为待人工批改。错题本的设计也值得看一下。它不是考试时即时生成的而是在交卷后批量把答错的题复制到wrong_book表带上学员id和题目id。这样学员端查询错题时不需要实时扫描历史答卷查询速度快很多属于典型的空间换时间设计。3.4 建表阶段最容易犯的三个错误我在这套源码基础上改功能时遇到过三个表设计上的坑写出来给准备照着改的读者提个醒第一个错误是用varchar存布尔状态。有人习惯把状态存成0/1甚至yes/no占空间不说还没法用数据库约束保证值合法。正确做法是用tinyint(1)值为0和1必要时加上默认值和注释。这套源码里所有状态字段都是tinyint看表结构时很省心。第二个错误是大文本字段不分类型。题干、答案解析这种内容用text没问题但把视频URL也设计成varchar(255)就会出问题——实际上OSS签名URL经常超过255个字符实践里建议用varchar(500)或者直接text。第三个错误是该建的联合索引没建。比如user_course表经常按user_id查选课列表再按course_id关联课程表如果只在user_id上建索引走user_id course_id联合查询时效率会差一截。正确做法是建uniq_idx_user_course(user_id, course_id)联合唯一索引既保证重复选课不出现又能加速查询。4. 权限不是登录后才能进那么简单RBAC的前后端落地4.1 后端如何用拦截器加JWT令牌做第一道防线这套系统的后端权限控制方案很轻量没有引入Spring Security而是用拦截器 JWT Redis自己实现了一套。这样做的好处是逻辑透明、依赖少、排查问题方便对固定三种角色的管理系统来说完全够用。流程是这样的用户登录成功后后端用userId 角色代码 过期时间生成一个JWT令牌返回给前端同时把用户的权限标识perms列表写入Redis。前端每次请求都在Header里带Authorization: Bearer token。后端的HandlerInterceptor里统一解析token校验签名和有效期然后从Redis查这个用户拥有的权限标识。方法的权限校验是通过自定义注解实现的比如在讲师添加课程的接口上标注RequirePermission(course:add)拦截器会先校验登录再校验当前用户是否拥有该权限标识。权限不足时直接返回403前端弹出无权限操作的提示。这种注解方式的好处是权限和接口绑定得很清晰代码里一眼就能看到每个接口的访问要求。4.2 前端路由守卫和动态菜单的实现思路前端的权限控制我一开始以为是简单的登录了就能看所有路由实际跑起来才发现细节不少。这套系统的前端做了两层控制。第一层是路由守卫。router.beforeEach里做判断白名单页面登录页直接放行没有token的统一跳登录页有token但访问了已登录页面时再根据用户角色判断路由是否在允许列表内。实现上不是在路由表里写死角色而是登录后从后端接口拉取当前用户的菜单树前端在守卫里比对当前路径是否匹配菜单数据。第二层是按钮权限。菜单树里每个按钮的perms标识前端封装了一个v-permission自定义指令比如v-permissioncourse:delete当前用户没有这个权限标识时该按钮的DOM直接不渲染。这层控制避免了一个很常见的尴尬场景学员登录后看到了删除课程按钮点了才报403。这里有一个容易踩的坑刷新页面后动态路由会丢失。因为菜单树是登录时异步拉取的刷新浏览器后Vuex数据清空如果不在全局守卫里重新拉取菜单再addRoutes用户就会被踢回登录页或者白屏。解决方法是把用户信息和菜单树存到localStorage刷新后先校验本地存储再重新注册路由。4.3 最容易翻车的水平越权以及我那次的排查过程权限控制最容易被忽略的是水平越权问题。垂直越权是学员访问了管理员的接口通常靠注解能拦住水平越权则是学员A访问了学员B的数据这种问题接口层面看着很正常但数据层面没有做归属校验。我之前在这个系统里排查过一个真实问题学员端查看学习记录时明明应该只显示自己的记录但通过修改URL里的recordId参数能查到别人的学习时间。根因是SQL直接按前端传入的记录ID查询没有带当前登录用户的user_id作为筛选条件。排查链路大概是这样的先在浏览器Network面板发现请求参数里带了recordId后端接收后直接执行SELECT * FROM study_record WHERE id ?返回了数据。再去看service层代码发现controller里没有从上下文获取当前用户idmapper的参数里也没有用户id字段。修复方案很简单controller里从UserContext.getUserId()取值传给servicemapper的SQL改成WHERE id ? AND user_id ?。注意判断数据归属时永远不要信任前端传过来的userId。所有涉及当前用户身份的信息都应该从后端安全的上下文对象里获取这是权限设计的铁律。5. 前后端联调中踩过的坑接口契约、时间序列化与文件上传5.1 统一返回体先定契约再看代码前后端分离的项目第一件要做的事就是约定接口返回结构。这套系统的约定是统一的Result对象code表示业务状态码message是给前端的提示文案data才是真正的业务数据。所有接口无论成功失败都返回这个结构前端axios的响应拦截器统一处理。我之前见过不少项目接口返回风格不统一有的成功时直接返回数据失败时返回一个错误页有的接口字段叫data有的叫rows联调时前端工程师得在每个接口里单独做兼容非常痛苦。这个系统用统一返回体之后前端只需要封装一次code为200时取出data非200时用message弹提示并做对应状态码处理。状态码的约定也要明确。我建议按这套规则来200表示成功400表示参数校验失败401表示未登录或令牌过期403表示无权限500表示服务端异常。注意401和403的区别前者是你是谁我不知道后者是我知道你是谁但你没资格做这件事。前端拦截器在拿到401时跳登录页拿到403时只弹提示不跳转这个区分在业务里很关键。5.2 LocalDateTime、驼峰映射和413这三个经典问题联调过程中我踩了三个不得不记录的问题每一个都能让看似写对的接口直接罢工。第一个是LocalDateTime的序列化格式问题。Spring Boot默认把时间序列化成2025-01-15T10:30:00这种ISO格式前端直接显示这个字符串非常丑而且带字母T不符合业务习惯。解决方案是在application.yml里配置全局Jackson格式化spring.jackson.date-format: yyyy-MM-dd HH:mm:ss和time-zone: GMT8。这样后端返回的时间就是2025-01-15 10:30:00前端直接展示就行。第二个是下划线字段和驼峰字段的映射问题。数据库字段是create_timeJava实体如果用了驼峰命名createTimeMyBatis默认情况下查出来是null。这个系统在配置里开了map-underscore-to-camel-case: true问题不大。但如果你后期自己加表记得在全局配置里确认这个开关否则会白白浪费半天排查时间。第三个是文件上传超过大小限制。讲师上传视频封面上传图片时如果Spring的multipart默认限制1MB没调大一点的封面图直接报错Nginx还会返回413。解决方案是后端配置spring.servlet.multipart.max-file-size: 50MB和max-request-size: 50MB同时在前端上传组件上同步加上大小校验提前拦截而不是留给后端报错。5.3 分页和明细列表的性能细节管理系统里最常用的接口是分页列表。这套系统用的是PageHelper插件调用方式很简单PageHelper.startPage(pageNum, pageSize)往下写正常的查询方法插件自动拦截SQL并生成COUNT查询和LIMIT语句。但PageHelper好用不等于可以乱用我遇到过两个和它有关的坑。第一个是COUNT查询被拖慢。分页列表关联了多张表PageHelper生成的COUNT语句会包裹整个查询如果原SQL里有复杂的JOIN和where条件COUNT也慢。解决方式是尽量把查询拆小列表主查询只查当前页需要展示的字段明细字段用单独接口查询或者手写COUNT语句去掉不必要的LEFT JOIN。第二个是分页参数不生效的问题。PageHelper的startPage必须紧跟着Mapper查询方法中间不能有其他查询操作否则分页会被吃掉或者作用到错误的SQL上。很多人忘记这一点调试时发现列表页返回了全量数据。这类问题排查经验是先看SQL日志如果发现没有LIMIT基本就是这个原因。6. 部署上线前的体检清单从跑不起来到Nginx反向代理6.1 按这个顺序排查80%的启动问题都能解决每次拿到别人写的源码第一步永远是让它在本地跑起来。我按这套系统实际操作下来发现启动问题的排查顺序其实是有规律的按下面这个顺序走能省很多时间第一步确认数据库脚本是否完整执行。导入SQL时最容易出问题的是字符集和版本不兼容MySQL 8.0导入5.7的脚本一般没问题反过来可能报语法错误导入前一定要确认库的字符集是utf8mb4否则中文数据乱码。第二步检查后端配置文件。重点看application.yml里的数据库地址、用户名、密码、Redis地址以及端口配置。本地默认端口8080如果被占用会启动失败改成8081或者别的端口即可。第三步启动顺序。如果是带Redis的系统先确保Redis服务已启动再启动后端。很多人的问题其实是Redis没起来后端启动时连接超时报错信息又容易误导人。第四步前端依赖安装。Node版本过高时老工程里一些依赖会安装失败建议直接使用Node 16。安装依赖用npm安装完成后npm run dev起开发服务端口通常5173或8080。第五步登录验证。如果登录接口一直报401排查方向不是前端而是JWT的密钥和过期时间配置。有时候是服务器时间和本地时间不同步导致令牌验证失败这个坑在跨时区部署时特别常见。6.2 Nginx配置前端history路由和/api反向代理生产环境部署时前端打包后的静态文件放在Nginx里后端jar包独立跑在某个端口用Nginx做反向代理。这套系统我用了标准的history路由配置加API代理核心配置如下server { listen 80; server_name your-domain.com; # 前端静态文件 root /opt/learning-platform/dist; index index.html; # history路由回退 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有两个细节值得注意。第一try_files这行是必须的否则用户刷新某个页面路径时会报404因为前端是history路由真实路径在服务器上不存在文件必须回退到index.html让前端路由接管。第二proxy_pass http://127.0.0.1:8080/;末尾的斜杠也很关键它会把匹配到的/api/前缀去掉再转发到后端比如/api/course/list会变成后端接口/course/list这样前后端接口路径就能保持一致不用在后端额外加/api前缀。6.3 学习记录表的数据归档策略部署上线后还有一个躲不开的问题数据量增长。这套系统里增长最快的是study_record学习记录表每看一个课时就写一条按一万人规模算每人每天看3节课一天就是3万条数据一年上千万条。这个量级在单表上做查询即使有索引也会越来越慢。我建议的做法不是一上来就分库分表而是先做按月归档。在study_record表上增加一个study_month字段格式如202501每月初做一个定时任务把超过三个月的明细数据搬运到study_record_archive_YYYYMM这样的归档表里。业务查询只查热表报表查询走归档表。这样热表的数据量始终控制在一个可控范围内归档表哪怕不建过多索引也能接受。另一个思路是定期清理无效数据。比如超过两年且已经毕业/离职用户的学习记录如果业务上没有强审计要求可以直接清理或者只保留聚合统计数据比如总学习时长、完成课次数丢弃明细。具体保留多少取决于企业的审计要求但架构上一定要提前预留这个能力别等数据量把服务拖垮了才想办法。做完上面的所有检查这套系统的日常运维基本就没有什么隐患了。我个人的体会是这类企业级管理系统难点从来不是某个单一功能而是各个模块之间的数据流转、权限控制和性能边界。把这套源码完整跑通之后再去接直播、接支付、接考试系统思路都会清晰很多。如果你们团队正好也在搭类似的平台建议先照着这套架构把主链路搭出来再逐步加业务细节——先把骨架立稳后面的功能才不会散。