去年在带计算思维与人工智能课程时我一度非常抗拒“再做一个在线学习系统”这个想法。市面上的学习平台已经够多了为什么还要重复造轮子直到真正跟着课程团队整理了一轮作业和练习反馈我才意识到通用平台根本接不住课程的具体需求公式推导不好输入、编程题只能提交附件、老师想看学生薄弱的知识维度却只有考试成绩连最基础的“章节知识点掌握度”都要靠人肉统计。于是我用SpringBootVue从零搭了一个小型学习网站把“学、练、评、统计”四件事一次性串了起来。这篇文章是完整的设计与实现复盘既讲业务建模也讲在线代码评测子服务怎么落地还覆盖了不少上线前的坑适合想快速搭建课程类学习系统、同时希望代码质量和扩展性足够的朋友参考。1. 这个网站要解决什么问题从业务场景反推项目定位1.1 被通用学习平台逼出来的真实需求写第一行代码之前我认真蹲过几轮课程辅导现场。发现学生其实不缺视频、不缺PPT最缺的是一个“能练、能立刻知道对不对、能知道自己哪里弱”的地方。普通学习平台确实可以布置作业但计算思维课要用到的练习大多是课程组独家整理的逻辑推理题、算法描述题、小规模代码题通用题库根本不允许老师快速录入这种自定义题型也不支持“提交Python代码后自动跑测试用例并返回结果”的能力。需求就这样被动地清晰起来学生学习时需要一个“内容-练习-反馈”闭环而不是只有资料下载。老师需要能独立维护课程内容和题库不能每次改动都找开发改代码。课程负责人需要看到班级维度的掌握度统计而不是只有一张成绩表。所以这个网站不是要做成慕课也不是要做成复杂社区而是定位成一个“轻量、可定制、能支撑日常教学”的配套学习工具。1.2 为什么选型SpringBootVue技术与场景的匹配SpringBoot负责哪里后端的接口开发效率、权限控制、数据访问都非常快。Java生态在线评测这类需要严格控制子进程、处理并发任务的场景里成熟库也够用不用自己造轮子。MyBatis-Plus做单表CRUD和分页查询Spring Security做登录鉴权JWT无状态化会话这些组合属于“被验证过很多次”的标准搭配踩坑成本低。Vue负责哪里章节学习页、练习作答页、统计可视化页都是高交互页面Vue的组件化和响应式模型在这种场景下优势明显。Vue 3配合Vite构建开发调试速度比老的Webpack方案快不少Element Plus的表格、表单、树组件能直接覆盖80%的管理端界面需求。为什么不做微服务或前后端不分离的MVC项目规模摆在那里没必要引入过度架构。前后端分离让部署更灵活前端构建产物可以交给Nginx托管后端以独立服务运行后续做负载均衡或拆评测服务也比较顺。而传统后端模板渲染的方式在这种强交互页面下维护成本反而更高。1.3 角色与核心流程学、练、评三段式闭环整个系统只保留三种角色避免一开始就陷入复杂的组织权限模型学生浏览课程章节、学习知识点、做章节练习、提交代码题、查看答案解析和评测反馈、查看自己各能力维度的成长雷达图。教师维护课程章节和知识点、维护题库与答案、发布练习、查看班级学习统计、导出成绩。管理员用户管理、角色分配、系统参数配置、评测任务状态监控。核心业务闭环就是一句话学节点 → 练题目 → 收反馈 → 查统计。课程内容按章节组织一个章节挂若干知识点每个知识点挂若干练习学生学完一个知识点后必须做对应练习才能算“完成”练习结果会反过来更新掌握度掌握度再聚合到学生个人维度雷达和班级统计每个环节都是数据联动的。后面所有设计和编码都是围绕这个闭环展开的。2. 数据表怎么设计围绕“学练评”闭环拆解模型2.1 功能模块矩阵先列功能矩阵再建表避免边写边改。我设计时主要分成四大模块模块核心功能主要使用角色课程内容课程管理、章节树、知识点内容维护教师、学生练习评测题目管理、答题提交、自动评测、结果反馈学生、教师学习统计个人掌握度、班级统计、雷达图数据学生、教师系统管理用户、角色、权限、参数配置管理员2.2 核心表结构拆解数据模型我分成两类一类是“资产表”比如课程、章节、题目另一类是“活动表”比如作答记录、学习进度、评测任务。这样分类方便理解也方便设计索引和清理策略。用户与角色用户表和角色表是标准RBAC结构。这里我没有做细粒度的菜单权限表而是直接在角色上绑定可访问的菜单路由角色就三种多了反而不好维护。课程章节体系课程表、章节表、知识点表三级嵌套。章节表通过parent_id支持无限层级但实际业务里最多三层课程-章-节。知识点表挂在最底层节点上这样每个练习都能定位到某个知识点统计时才能从练习对错反推到能力短板。题目表题目表是重点字段设计直接影响评测模块的复杂度question_type区分选择题、判断题、填空题、代码题。option_json选择题选项统一JSON存储前端直接渲染。answer_text选择题填选项值代码题不存答案文本而是关联一组测试用例。analysis_text答案解析练习后展示给学生。difficulty简单、中等、困难用于后续推荐题目。作答记录与学习进度作答记录表记录每次提交的答案、正误、耗时学习进度表记录学生每个知识点的完成状态。为了防止重复学习刷新统计进度表对user_id knowledge_point_id建唯一索引状态取最新一条。2.3 建表时定下的几个约定项目开始时我给自己定了几条规矩后期省了非常多麻烦所有表加逻辑删除字段deleted不做物理删除避免误操作后数据无法恢复。唯一索引优先于代码判断。比如进度表那对唯一组合如果没有数据库约束并发提交时很容易产生重复记录。题目选项、题目解析这类长度不确定的字段优先用text或json类型不要一开始就卡死在varchar(255)。时间字段全部统一为datetime在JDBC连接串里固定serverTimezone避免服务器和数据库时区不一致导致时间偏移。3. 后端实现重点SpringBoot里的权限与业务边界3.1 JWT登录链路与权限校验登录逻辑不复杂用户提交用户名密码后端校验成功后生成JWT返回前端。前端之后所有请求在Header里带Authorization。之所以用JWT而不是Session是因为前后端分离后接口要给多个端复用SessionCookie在跨域和移动端场景下都很别扭。核心代码大致是这样的PostMapping(/login) public RString login(RequestBody LoginDTO dto) { // 实际会通过UserDetailsService查用户并比对加密密码 UsernamePasswordAuthenticationToken token new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()); Authentication auth authenticationManager.authenticate(token); String jwt jwtUtil.generateToken(auth.getName()); return R.ok(jwt); }Spring Security里需要重点处理两个地方一是放行/api/auth/login、静态资源、Swagger文档二是放行OPTIONS预检请求。很多前后端分离项目第一次联调失败都是因为预检请求被安全链拦掉了。另外角色校验我用PreAuthorize(hasRole(TEACHER))这种注解方式方法级别控制足够清晰。3.2 课程和练习接口的树形组装学生端经常要打开课程章节树一开始我很容易写成“先查所有章再循环查每个章下面所有节”这就是典型的N1查询问题。设计时改成一次性把当前课程下所有章节查出来在Java内存里按parentId组装成树接口直接返回组装好的结构。N1变成两次查询响应速度快很多代码也更简单。练习题列表的加载也做了拆分学生进入章节看到的只是题目概要点击某一题再加载题干与选项避免一个接口返回大量JSON前端渲染吃紧。3.3 掌握度统计的逻辑不能简单算正确率学习统计是本项目最有业务价值的部分也是最容易做错的。我没有简单用“答对次数/总次数”当掌握度因为这样刷题也能刷出高分不能反映真实水平。实际规则是每个知识点下取最近10次作答记录。首次作答正确加分权重最高连续正确比断续正确更有说服力。代码题以所有测试用例一次通过才算正确。最终掌握度映射到0-100分低于40分在雷达图上标记为薄弱项。这个逻辑全部放在后端服务层前端只负责把分数渲染出来。统计口径在后端统一老师和管理端看到的数据就一致了。3.4 教师端题目批量导入让老师逐条录题目根本不现实所以我做了一个Excel导入功能。教师先下载模板按模版填题干、选项、答案和解析再上传。解析时用EasyExcel逐行读取校验失败的行会把错误原因返回给前端比如“第5行代码题缺少测试用例”方便老师修改后重新上传。这个小功能直接决定了后期题库的丰富速度远比花里胡哨的在线编辑器实用。4. 在线代码评测模块整个项目里最费脑子的地方4.1 评测方案选型一开始就差点做翻车代码题的自动评测是计算思维与人工智能学习网站里最有技术含量的模块。第一版我天真地把执行逻辑直接放在SpringBoot主服务里收到用户代码后直接Runtime.getRuntime().exec()执行结果一个死循环就把整个应用卡死测试同学跑十分钟就复现了“服务无响应”。后来调整了架构拆出独立评测子服务。设计思路是主应用只负责保存提交记录把评测请求落到一张judge_task表状态设为“待评测”评测服务是一个独立进程定期扫描这张表拿到任务后在工作目录里编译运行用户代码再把结果回写。没有引入消息队列是因为课程规模内的并发量完全可以用表轮询扛住架构能少一个组件就少一个。4.2 评测执行流程从提交到结果回写整个流程可以概括成五步学生提交代码主应用保存源码并创建评测任务。评测服务轮询到新任务根据题目配置的编程语言选择对应执行器。执行器将代码写入临时文件调用对应编译器完成编译。编译成功后逐个运行测试用例每个测试用例都开独立子进程并设置超时。汇总所有用例结果回写judge_result前端轮询后展示。单个测试用例的执行器核心逻辑简化后是这样Process process processBuilder.start(); boolean finished process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); return JudgeResult.timeout(); }这里有个很容易犯的错误waitFor超时后只是等待超时不会自动杀掉子进程。如果不手动destroyForcibly()用户代码里的死循环会一直占着CPU评测队列会被拖垮。杀掉进程之后还要检查子进程组必要时通过kill命令清理这是实际压测才发现的细节。4.3 安全与资源限制不能拿主力服务器裸奔在线运行用户代码天然有安全风险。虽然项目定位是课程内部使用用户都是自己的学生但“不信任用户代码”这条底线必须守住。我做了三层限制低权限运行在服务器上单独创建judge用户评测服务用这个用户启动执行子进程目录权限全部隔离。资源限制每个执行器通过ulimit和ProcessBuilder设置CPU时间、内存大小和文件写入大小防止恶意占用。超时兜底每个测试用例的超时时间默认2秒整个提交的评测总时间不超过30秒。如果想更彻底地隔离可以后续把评测子服务容器化每个任务启动一个一次性容器执行代价是物理机资源开销会大不少。当前项目的并发量用低权限用户超时限制已经足够稳定。4.4 测试用例与用户反馈设计不能只告诉学生“错”练习反馈如果只返回“答案错误”学生根本不知道问题出在哪。我给评测结果增加了明确分类编译错误、运行超时、运行时异常、输出不匹配。前端拿到分类后渲染不同提示。编译错误时后端截取编译器输出的前10行返回因为完整堆栈对初学者是一堆噪音前几行就足够定位语法问题。输出不匹配时如果题目配置允许会把期望输出和实际输出展示出来方便学生自己对比。这里也做了保护不在前端展示完整代码的测试点细节避免学生直接抄答案。5. Vue前端实现重点让学习体验有一点“产品感”5.1 前端工程结构与请求封装前端用Vue 3 Vite Pinia目录结构按业务模块划分而不是按文件类型堆在一起src/ api/ // 接口请求定义 assets/ // 静态资源 components/ // 公共组件 router/ // 路由配置 stores/ // Pinia状态 views/ admin/ // 管理端页面 student/ // 学生端页面 common/ // 公共页面请求封装是所有前后端联调的基础。axios实例统一加上请求拦截器和响应拦截器请求拦截器自动从localStorage读token并放进Header响应拦截器处理业务状态码遇到401自动跳转登录页遇到业务错误用消息组件弹出提示。5.2 动态路由与菜单权限三种角色的可访问页面完全不同路由不能写死。我采用的方式是登录成功拿到用户角色后根据角色路由表动态注册路由。const roleRoutes { STUDENT: studentRoutes, TEACHER: teacherRoutes, ADMIN: adminRoutes, }; router.addRoute(roleRoutes[userRole]);这种方式还有个额外好处学生路由里根本不会有管理页面代码即使学生手动改地址访问管理页路径路由不存在自然就跳回404前端权限上先挡了一道。真正的数据安全当然还是以后端接口鉴权为准前端路由只是体验层面的区分。5.3 章节学习页和练习页的关键交互章节学习页采用左树右内容布局左边是课程章节树按课程-章-节三级展示右边是当前节点的知识点内容。学生点某个练习后右侧切换到练习面板。顶部有“上一节/下一节”按钮学生不需要总是回到树里去点。练习作答页有两个交互细节需要注意。第一题目列表和答题卡联动右侧显示所有题目编号已答的变绿色当前作答的高亮最后统一提交。第二对于代码题我接了一个轻量代码编辑器组件并提供“自动保存草稿”功能。学生写着写着刷新页面代码还在localStorage里这个体验在练习场景里很加分。5.4 学习统计可视化用雷达图呈现计算思维维度计算思维在教学内容里被拆成几个维度分解、模式识别、抽象、算法设计、评估。后端会根据每个维度绑定的知识点答题数据归一化出0-100分的分值前端用ECharts雷达图展示。学生看到的是自己的五维能力图老师看到的是班级平均雷达一眼就能发现班级整体薄弱的维度直接决定下一次课讲什么。这个图在后来的课程复盘中被使用了。6. 部署与性能优化开发能跑只是第一步6.1 Nginx托管前端与反向代理部署方案很常规前端npm run build出静态文件Nginx托管并监听80端口SpringBoot以jar包方式运行在8100端口Nginx把所有/api请求反向代理到后端。这样浏览器端无跨域问题因为前后端在同一个域名下。server { listen 80; server_name learn.example.com; root /var/www/learning-site/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8100; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files那行很重要。Vue Router如果是history模式用户刷新一个子路由页面时如果Nginx没找到对应静态文件会返回404。加上try_files $uri $uri/ /index.html;后所有未知路径都回退到入口index.html由前端路由接管。6.2 数据库索引与查询优化首版首页接口比较慢排查原因是聚合统计接口一次性把所有课程的所有统计信息都查出来了里面大量数据页面根本用不到。优化两件事统计接口增加时间范围参数只查当前学期。高频查询条件加上数据库索引比如章节按course_id查进度按user_id knowledge_point_id查作答记录按user_id question_id查。ALTER TABLE study_progress ADD INDEX idx_user_point (user_id, knowledge_point_id); ALTER TABLE answer_record ADD INDEX idx_user_question (user_id, question_id);加完索引后首页接口从800ms降到200ms以内。对于课程型网站这个体量来说SQL写法和索引设计的效果远大于引入缓存组件。热点章节内容后面如果访问量大再上Redis缓存也不迟。6.3 评测服务的并发控制评测子服务刚上线时压了一批学生同时提交代码评测服务直接把服务器CPU跑满了。原因很简单所有任务一到就并发执行十几个编译进程同时跑物理机根本扛不住。解决方案是给评测服务加一个信号量最多允许同时运行2个评测任务其余任务在队列里等待。网页端则在轮询状态那里加“排队中”提示学生看到不会以为网站挂了。7. 踩坑记录五个影响上线但文档里很少写的问题7.1 跨域配置了还报错问题出在预检请求前端开发服务器跑8080端口后端8100端口本地联调必跨域。后端用CorsFilter配置了允许跨域结果GET请求正常POST请求却时不时报错。抓包才发现非简单请求会先发一个OPTIONS预检请求CorsFilter虽然放行了跨域规则但请求还没走到Filter就被Spring Security安全链拦住了。解决方式很直接在Security配置里放行所有OPTIONS请求。7.2 Long型主键传给前端后精度丢失数据库主键用的雪花IDJava后端返回给前端时JS安全整数上限就约9007199254740991一旦ID超过这个值最后几位会变成0页面详情接口ID不匹配直接报错。这类问题在纯后端联调时根本发现不了一定要浏览器控制台看实际请求数据才能发现。统一解决方式是在Jackson配置里把Long类型序列化为字符串Jackson2ObjectMapperBuilderCustomizer customizer builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); };7.3 时间显示差了8个小时数据入库后时间没问题页面展示却少了8小时。原因是JDBC连接串没有指定时区数据库用的UTC时间服务器本地是东八区。在连接参数里固定serverTimezoneAsia/Shanghai后问题消失。时间处理这类问题最好从一开始就在前端统一展示服务端ISO字符串前端不要自己做时区换算。7.4 前端刷新后登录状态丢失第一版把用户信息存在Pinia里一刷新页面Pinia数据清空路由守卫判断未登录就把人踢回登录页。解决方案是token存localStorage用户基本信息存localStorage路由守卫发现本地有token但状态为空时调用接口重新拉取用户信息再放行。这里有个细节不要直接把用户对象全量存localStorage只需要id、名称、角色其他信息失效没关系。7.5 评测任务提交后前端一直在“等待”评测服务第一次上线时学生提交代码后页面一直显示“提交中”前端轮询没有写超时和失败处理。后来给轮询加了三段状态排队中、评测中、已完成同时前端在60秒没拿到结果时提示“评测服务暂不可用请稍后查看”这样就算后端评测卡住学生也知道发生了什么而不是干等。8. 复盘从“能交差的Demo”到“真正给课程用”8.1 先跑通最小闭环功能再慢慢叠加我这个项目最大的体会是不要一开始就想着把推荐算法、知识图谱、直播之类的大功能装进去。第一版只做了课程浏览、选择题练习、统计三个核心功能代码题评测是第二版才加上的。最小闭环跑通之后老师和学生给出的真实反馈远比闭门造车有效。有人用起来了功能迭代才有方向。8.2 评测服务的安全隔离值得更早做如果重新做一次我会在项目一开始就把评测子服务设计成独立的可停靠模块而不是急着在主服务里验证代码。评测模块牵涉到子进程管理、资源限制、结果回写这些问题越早隔离后期重构成本越低。容器化隔离方案更适合在课程并发量上来之后引入对单体服务器方案仍然是最稳妥的起步选择。8.3 容易被低估的“内容维护体验”技术做完之后才发现真正决定这个网站能否被长期使用的不是学生端界面多酷炫而是老师愿不愿意维护内容。Excel批量导入题目、章节拖拽排序、题干预览这些不起眼的功能反而被老师们用得最多。后续扩展方向也很明确根据学生弱点推荐练习、根据历年做题数据自动生成阶段性测验、把更多编程语言接入评测执行器这些都是围绕数据积累之后自然浮现出来的可能性。做完这个项目再回头看SpringBootVue这个组合确实很适合课程类学习网站。如果让我给一个建议先把“学练评统计”的最小闭环搭稳再把评测子服务的安全边界做好其余功能都可以等真实用户告诉你该做什么。