Java SSM+Flask在线教育系统实战:从架构设计到核心模块实现

发布时间:2026/10/13 16:49:01
Java SSM+Flask在线教育系统实战:从架构设计到核心模块实现
谈到在线教育系统可能很多人第一反应是“不就是课程列表加视频播放嘛”可真要动手做一遍你会发现用户、课程、订单、支付、学习进度、数据统计、权限控制这些东西一环扣一环任何一个环节没想清楚落地时都会被狠狠教育一下。我最近刚好参与了一个基于 Java SSM Flask 的在线教育系统项目覆盖了在线学习平台最核心的模块包括课程管理、视频学习、在线考试、订单支付和学习行为分析。这类项目在网上也被叫做网络教育系统、远程教育系统、在线课程系统、在线教学系统叫法不同背后的业务骨架其实高度相似。如果你正准备做毕业设计、课程设计或者想接一个在线教育类的外包项目这篇内容会比较对路。我会直接把技术选型、表结构设计、关键代码思路、调试文档整理这些经验铺开讲少谈虚的多聊实际踩过的坑。1. 在线教育系统的整体设计思路1.1 这类系统到底要解决什么问题用户视角下的在线教育系统很简单注册登录浏览课程下单购买在线看视频做练习看学习进度。但运营方视角完全不一样平台要管课程分类、上下架、视频存储、订单对账、教师讲师管理、学生学习数据统计、防止有人绕过支付白嫖课程。所以一个能交付的项目绝对不是“前端套个模板、后端写几个接口”那么简单。我们当时把需求拆成两个端用户端和管理端。用户端面向学生提供课程检索、课程详情、视频学习、个人中心、订单列表这些功能管理端面向平台运营人员和讲师提供课程发布、章节维护、视频资源上传、订单查询、用户管理、学习数据报表。学习数据和分析这部分我们单独放到了 Flask 服务里处理因为推荐算法和统计分析用 Python 生态写起来顺手很多。很多项目做到最后烂尾不是代码写不出来而是看不清业务边界。在线教育系统里最容易被忽略的是“学习过程记录”。很多人只做了课程列表和下单结果用户买了课之后看了多久、看到哪一节、有没有完成这些数据完全没存。这样做出来的系统只是个电商壳子离“教育”两个字还差很远。1.2 为什么选用 SSM Flask 混合架构先解释一下 SSMSpring SpringMVC MyBatis是非常经典的 Java 后端组合。Spring 负责对象管理和事务SpringMVC 负责 Web 层路由和参数绑定MyBatis 负责 SQL 操作。这套组合在高校课程和企业旧系统里非常常见稳定、资料多、招人好招。多了一个 Flask是因为我们有几个功能用 Java 写很别扭。Flask 这边主要做三件事学习行为统计、课程推荐、视频文件相关的辅助处理。Python 在处理分词、余弦相似度计算、定时统计这些任务时比 Java 短一大截。举个例子给学生推荐同分类课程如果用 Java 写需要手写集合运算、排序、分组用 Python 的话几行 pandas 操作就能完成。但这不代表 Flask 比 SSM 好而是它们适合的场景不同。服务间通信我们用最普通的 HTTP REST。Java 端作为主后端Flask 作为分析辅助端。SSM 把用户学习日志写入数据库Flask 定时拉取这些日志做统计再把结果写回独立的分析表。前端需要推荐结果时由 SSM 接口去调 Flask 的推荐接口。这样两边互不干扰Java 挂了不影响 Flask 继续算数据Flask 挂了也不影响用户买课看视频。1.3 核心功能模块到底应该怎么切我把系统按业务域划分成七个模块模块核心功能归属服务用户模块注册、登录、个人信息、角色权限SSM课程模块分类、课程信息、章节、视频资源SSM订单模块下单、支付回调、订单状态管理SSM学习模块学习记录、视频进度、完成状态SSM Flask考试模块题库、在线答题、成绩记录SSM管理后台课程审核、用户管理、数据报表SSM Flask数据分析模块学习统计、推荐、标签聚合Flask这个划分方式也是后来写文档、画架构图的依据。每个模块有独立的数据库表区域模块之间通过服务接口或者任务调度来协作而不是所有功能揉在一个 Service 里。2. 技术选型拆解SSM 和 Flask 各自的分工2.1 SSM 端为什么不能丢很多刚接触这个项目的人会问都 2025 年了为什么不直接上 Spring Boot我的回答是如果是从零做上线项目Spring Boot 确实更方便但如果这个项目的定位是教学、课程设计、毕业设计或者需要接一个已经跑了多年的 SSM 遗留系统那 SSM 反而是更合理的起点。SSM 的价值在于把 Spring 的核心思想暴露得很明显。你亲自配置过 applicationContext.xml、spring-mvc.xml、mybatis-config.xml才能理解 IoC 和 AOP 到底帮我们省了哪些事。SpringMVC 的 DispatcherServlet 怎么拦截请求、HandlerMapping 怎么找到 Controller、MyBatis 的 Mapper 代理怎么生成 SQL这些都是面试高频题也是排查线上问题的基础。在实际编码中我们用 SSM 写核心业务接口非常顺手。比如下单流程要在事务里同时更新订单表、扣减课程库存、记录流水只要在 Service 方法上标注TransactionalSpring 就会把数据库操作的提交和回滚统一管起来。这种强事务能力是 Flask 单薄的服务端不太好替代的。2.2 Flask 端处理哪些脏活累活Flask 最大的优势是“轻”和“自由”。它可以很快地起一个服务也不需要像 Java 那样写一堆配置类。我们用它做了几个比较实际的辅助功能。第一个是学习行为聚合。SSM 每次收到视频心跳上报都会往 learning_log 表插入一条记录。一个热门课程一天可能有几十万条日志直接查明细做统计非常慢。Flask 每天凌晨通过 crontab 调用聚合脚本把日志表的数据按用户、课程、日期汇总到 learning_stats 表前台报表直接查聚合表速度能快上百倍。第二个是课程推荐。我们没上复杂的机器学习模型用的是基于标签的推荐根据用户最近 30 天学习时长最长的课程分类找出同分类下热度排名前 10 的课程返回。这个逻辑在 Flask 里实现只需要几十行代码但放到 Java 工程里要写一堆 POJO、Mapper、Service反而显得笨重。第三个是视频截图处理。讲师上传课程视频后Flask 服务用 OpenCV 抽取几帧画面作为课程封面候选图。这种调用本地系统命令和图像库的操作Java 也能做但依赖安装和跨平台问题会多一些Python 显然更舒服。2.3 两个服务之间怎么通信才优雅服务间通信最常见的有两种方案共享数据库和 HTTP 接口调用。我们一开始图省事让 Flask 直接读写 SSM 的主库表结果后来发现耦合太重改表结构的时候两边都要同步改非常痛苦。后来收紧了权限Flask 只能读取日志表、课程表、用户表分析结果单独写到 stats 前缀的表中不碰业务主表。实时性要求高的推荐查询则由 SSM 通过 RestTemplate 调用 Flask 接口。我们约定了一个简单的鉴权头Flask 每个接口要求带上X-Internal-TokenSSM 启动时从配置中心读取 token调用时动态拼在请求头里。这样外部用户无法直接访问 Flask 端口只能通过 SSM 转发。// SSM 端调用 Flask 推荐接口的简化示例 RestTemplate restTemplate new RestTemplate(); HttpHeaders headers new HttpHeaders(); headers.set(X-Internal-Token, flaskToken); HttpEntityString entity new HttpEntity(headers); String url http://127.0.0.1:5000/api/recommend?userId userId; ResponseEntityRecommendResult resp restTemplate.exchange( url, HttpMethod.GET, entity, RecommendResult.class);这里有一个非常容易踩的坑RestTemplate 默认超时时间很长如果 Flask 服务没启动Java 接口会一直卡住直到超时。我们后来单独抽了一个 HttpClient 配置类设置连接超时 3 秒、读取超时 5 秒并且在调用失败时降级返回热门课程列表而不是直接把异常抛给前端。这种容错设计在联调时能救你很多次。3. 数据库设计与核心表结构3.1 基础业务表用户、课程、分类数据库设计是这类项目最容易翻车的地方。我见过不少人的表只建了 user、course、order 三张表最后所有额外信息都靠 JSON 字段硬塞调试起来想死的心都有。对于在线教育系统我建议至少按下面的表结构来起步。表名用途关键字段user用户信息id, username, password, role, statuscourse_category课程分类id, name, parent_id, sort_ordercourse课程信息id, category_id, teacher_id, title, cover_url, price, statuscourse_chapter章节id, course_id, title, sort_ordercourse_video视频资源id, chapter_id, title, video_url, duration, free_flagorder_info订单主表id, order_no, user_id, total_amount, statusorder_item订单明细id, order_id, course_id, pricelearning_log学习日志明细id, user_id, course_id, video_id, watch_seconds, create_timelearning_stats学习统计聚合id, user_id, course_id, total_seconds, finish_count课程表里的 price 用 decimal 还是 int很多新手习惯用 decimal(10,2)但涉及支付对账时我更推荐用 int 类型存储“分”。比如 99.90 元就存 9990。这样订单金额计算时不会出现浮点数精度问题和支付通道的金额单位也保持一致。user 表里的 role 字段也非常关键。我们用 1 表示学生2 表示讲师3 表示管理员。权限拦截在拦截器里统一判断而不是每个 Controller 里写死。这样以后加一个“助教”角色只需要改一处常量配置。3.2 课程章节和视频资源怎么设计课程内容是一个树形结构课程下挂多个章节章节下挂多个视频。设计时给章节和视频各加一个 sort_order 字段专门用于排序。不要用自增 id 排序因为视频上移下移后 id 会乱套。视频资源表里存的是视频 URL而不是视频二进制。这是很多项目一开始容易犯的错把视频文件传到服务器本地目录然后数据库存一个相对路径。这在本地测试没问题但一旦服务器磁盘满了或者需要多机部署就会立刻崩掉。我建议视频统一上传到云存储数据库只保存最终访问 URL。如果是本地演示环境那也要把视频目录单独放到非项目工程目录下用 nginx 做静态文件映射不能放在 Tomcat 的 webapps 下面。课程状态 status 字段建议至少支持 0 和 10 表示未上架草稿1 表示已上架。讲师编辑课程时先保存草稿审核通过后置为 1。这个状态控制在上线后非常重要不然前台一搜索就能看到讲师没写完的残缺课。下面是简化后的建表 SQL 示例字段我省略了一些但核心的排序和状态字段必须带出来。CREATE TABLE course_chapter ( id INT NOT NULL AUTO_INCREMENT, course_id INT NOT NULL, title VARCHAR(255) NOT NULL, sort_order INT DEFAULT 0, PRIMARY KEY (id) ); CREATE TABLE course_video ( id INT NOT NULL AUTO_INCREMENT, chapter_id INT NOT NULL, title VARCHAR(255) NOT NULL, video_url VARCHAR(512) NOT NULL, duration INT DEFAULT 0, free_flag TINYINT DEFAULT 0, sort_order INT DEFAULT 0, PRIMARY KEY (id) );3.3 订单与支付模块的表关系在线教育系统的订单不同于普通电商一个订单可能包含多个课程比如用户把“Java 基础”和“Java 进阶”放在购物车一起提交。所以订单表拆分成 order_info 和 order_item 两张表主表记录总金额和状态明细表记录每个课程的金额。订单状态的流转要提前约定好。我们用整数状态0 待支付1 已支付2 已取消3 已退款。支付回调成功后先根据订单号查订单判断当前状态是否已经是 1。如果是就说明回调重复了直接忽略。这种幂等处理不做的话用户支付成功后由于网络抖动触发了两次回调订单就可能被重复处理用户权益多送一份对账也会出现差额。CREATE TABLE order_info ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id INT NOT NULL, total_amount INT NOT NULL, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT NULL, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) );订单号我这里用的是唯一索引因为订单号必须绝对唯一。很多同学喜欢在代码里用System.currentTimeMillis()直接生成订单号并发稍微高一点就可能重复。至少要加上随机数和用户 ID 后缀或者直接用数据库的雪花算法生成。注意不要把 order_info 的 id 直接当订单号暴露给前端很容易被遍历和薅羊毛。3.4 学习进度记录的关键学习记录是教育系统和普通电商最大的区别点。用户看视频不是一锤子买卖今天看了 5 分钟明天接着从上次的位置继续看。所以 learning_log 表要记录 watch_seconds 和视频总时长。我们做一个视频心跳上报接口前端播放器每隔 10 秒上报一次当前播放进度后端收到后先查一遍是否有同用户同视频的记录。如果有则累加观看秒数同时更新最近观看位置如果没有则插入新记录。这个接口是系统里调用频率最高的接口需要做好性能优化只插入或更新必要字段不要动不动把整张表查一遍。因为日志表数据量会非常大我们按天做了分表计划比如learning_log_20250115。数据分析的时候Flask 定时任务只读当天新增的数据避免一上来就扫全表。这个设计前期看着麻烦但数据量到了百万级之后你会庆幸自己提前做了。4. 实操从零搭建可运行的在线教育系统4.1 环境准备和工程目录结构如果你要本地复现这个系统我建议先准备一台带 8G 内存的电脑或者一台 2 核 4G 的云服务器。假设你用的是 Windows 或 MacJava 8 以上、Maven 3.6、MySQL 5.7、Python 3.8 都要装好。Flask 服务建议建独立虚拟环境避免和系统 Python 包冲突。后端工程我按 Maven 多模块组织但为了不过度设计也可以保持单模块。单模块用起来更直观适合课程设计。项目里核心目录大概长这样online-edu ├── online-edu-common # 工具类、统一返回体、异常码 ├── online-edu-admin # 管理后台接口 ├── online-edu-api # 用户端接口 ├── online-edu-service # 业务逻辑层 ├── online-edu-dao # MyBatis Mapper ├── online-edu-flask # Flask 分析服务 └── sql # 初始化 SQL 脚本SpringMVC 的配置我习惯用 Java Config 而不是 XML但如果你提交的课程设计文档要求体现 XML 配置能力保留一部分 XML 也没问题。无非是在Configuration类里引入ImportResource两者可以共存。Flask 服务目录就比较简单flask_edu ├── app.py # 入口和所有接口路由 ├── recommend.py # 推荐逻辑 ├── stats.py # 统计逻辑 ├── requirements.txt └── config.py # 数据库连接等配置4.2 用户登录和权限控制的实现套路用户登录接口在很多项目里被写得很随意接收用户名密码查库比对成功后返回一个 userId。这样没有任何会话状态前端调用其他接口时根本不知道你是谁。我们的做法是用 Redis 维护登录令牌。登录成功后生成一个 UUID 作为 token以 token 为 key、userId 为 value 写入 Redis并设置过期时间 30 分钟。前端之后每次请求都在 Header 里带Authorization: token。SpringMVC 拦截器拦截所有/api/**请求先取 token再查 Redis查不到就返回 401查到就放行。密码存储千万别用明文也别用 MD5。MD5 撞库太容易了。我们用的 BCrypt每次加密自动带盐数据库中存的是$2a$10$...这样的哈希值。BCrypt 的缺点是加密速度慢但这反而让暴力破解成本变高对教育系统这种场景完全够用。// BCrypt 加密示例 String encodedPwd BCrypt.hashpw(rawPassword, BCrypt.gensalt()); boolean matched BCrypt.checkpw(rawPassword, storedPassword);权限控制方面我们定义了一个RequireRole注解在需要讲师权限的接口上标注。拦截器里读取当前用户的 role不匹配就返回 403。这样比在每个方法里写 if 判断干净得多改权限逻辑也只要改一处。4.3 课程列表和视频播放接口的要点课程列表接口要支持按分类、价格区间、关键字搜索、分页排序。这种多条件查询是 MyBatis 的强项。动态 SQL 比在 Java 里拼字符串安全得多别担心性能MySQL 对简单条件查询的执行计划优化得很好。select idselectCoursePage resultTypeCourseVO select c.id, c.title, c.cover_url, c.price, c.view_count from course c where c.status 1 if testcategoryId ! null and c.category_id #{categoryId} /if if testkeyword ! null and keyword ! and c.title like concat(%, #{keyword}, %) /if order by c.view_count desc limit #{offset}, #{pageSize} /select很多项目在视频播放这个环节处理得很粗糙。如果视频文件是 mp4 且体积不大确实可以直接返回文件地址给前端用video标签播放。但有一定用户量之后我强烈建议做防盗链。最简单的方案是生成一个带签名的播放地址比如 URL 后面拼上expires时间戳signmd5(token路径过期时间)。后端在拦截器里校验签名过期就拒绝访问。这种方式实现成本低能挡住大部分盗链但也别指望它多安全核心的视频版权保护还需要更复杂的 DRM 方案。课程详情页里还要注意免费试看逻辑。很多课程第一节是免费试看剩余章节需要购买后才能播放。我们在 course_video 表用free_flag字段控制前端播放前调用一个“是否可以播放”的接口后端判断用户是否购买或视频是否免费未购买且非免费就返回 403。这个判断逻辑一定要放在后端不能靠前端隐藏播放按钮否则直接拿视频 URL 就能看。4.4 基于 Flask 的学习分析和推荐接口怎么落地Flask 服务里我们最核心的接口是/api/recommend它接收 userId返回推荐课程列表。推荐策略我前面说过用的是基于用户最感兴趣分类的热门课程。这个策略虽然朴素但实测效果不错用户能看到和自己最近学习内容相关的课程。推荐逻辑实现分三步第一步查学习统计表找到用户最近 30 天学习时长最长的课程分类第二步查课程表找到该分类下状态为上架且评分最高的 10 门课第三步过滤掉用户已经购买过的课程。前两步都可以直接用 SQL 完成第三步用 Python 做一次集合去重。app.route(/api/recommend, methods[GET]) def recommend(): user_id request.args.get(userId, typeint) if not user_id: return jsonify(code400, msg缺少参数) top_category query_top_category(user_id) if not top_category: return jsonify(code200, dataquery_hot_courses(limit6)) recommend_list query_courses_by_category(top_category, limit10) bought_ids query_bought_course_ids(user_id) result [c for c in recommend_list if c[id] not in bought_ids][:6] return jsonify(code200, dataresult)这里还有个小技巧Flask 接口返回的数据结构要和 SSM 端统一。我们约定所有接口返回{ code: 200, msg: ok, data: ... }不管哪一端前端都能用同一套逻辑处理。跨语言协作时最怕各搞一套返回格式前端联调会疯掉的。4.5 演示环境和交付文档怎么准备这类带有源码和调试文档的项目最后交到对方手里能不能顺利跑起来往往比功能本身还重要。我们当时整理了一套三层文档第一层是部署文档写清楚 JDK、MySQL、Redis、Python 环境的版本号数据库初始化脚本的执行顺序以及两个后端服务启动的先后顺序第二层是接口文档用表格列出每个接口的请求方式、路径、参数、返回示例第三层是演示脚本描述模拟真实用户从注册到下单看视频的完整流程。调试文档我建议把常见启动报错也写一写比如端口被占用、数据库连接失败、Redis 连接不上、Flask 缺少依赖包。这些内容看似基础但能帮使用者省下大量问问题的时间。演示视频录制时不要只录界面点击要把 Swagger 接口測試或者命令行启动日志也录进去不然别人光看到页面不知道后端到底起了没有出了问题很难判断是哪一层的事。5. 常见问题排查与性能调优5.1 跨域与前后端联调问题现在很多项目前端单独跑在 Vue 或 React 的开发服务器上调用后端接口时一定会遇到跨域。SSM 端要配置好 CORS。这里有一个容易踩的坑SpringMVC 的拦截器先于 CORS 过滤器执行如果拦截器直接返回了 401前端会拿到跨域错误而不是业务错误。解决方式是在拦截器里放行OPTIONS请求。因为浏览器发起跨域请求前会先发一个预检请求如果后端把预检请求拦截了真实请求根本发不出去。5.2 SSM 和 Flask 数据不一致问题我们在项目早期遇到过推荐课程和实际课程状态不同步的问题。原因是 Flask 的推荐结果会缓存到 Redis缓存时间设置成 24 小时。如果某门推荐课程在这期间被下架了用户点击后还是能看到课程详情然后购买按钮变灰。看起来不算严重但用户会疑惑为什么推荐一个买不了的课。后来我们做了两件事推荐接口返回课程 ID 列表详情由 SSM 端实时查询并过滤下架课程缓存时间缩短到 1 小时同时在课程上下架时主动删除对应缓存 key。这样做后即使缓存脏了最多也就是多一次数据库查询不会把已下架课程暴露给用户。5.3 下单并发与库存扣减如果课程有库存限制比如某个训练营只招 100 人下单时就要防止超卖。最简单的方法是乐观锁更新订单状态前校验课程库存是否大于 0然后执行带条件的更新。update course set stock stock - 1 where id #{courseId} and stock 0;影响行数为 0 就说明库存不足这时候再给前端返回“课程已售罄”。这样处理并发时不用显式加锁性能也比悲观锁好很多。需要注意事务边界扣减库存和创建订单必须在同一个事务里否则库存扣了、订单没建成功会出现大事故。如果项目并发量真的很高Redis 预扣减是更稳的方案先把库存加载到 Redis下单时用DECR命令扣减扣成功后再异步写数据库订单。但这个方案复杂度高还涉及到 Redis 和 MySQL 的数据最终一致性问题课程设计和中小型项目不建议一上来就搞。5.4 视频播放卡顿和存储策略视频卡顿是这类系统最常见的投诉。原因往往不是服务器带宽不够而是没有做浏览器兼容和格式转换。不同浏览器对视频编码的支持不一样Safari 对 MP4 支持好有些安卓浏览器对 H.264 支持不好。所以商用系统一般会把视频转成 HLS 格式也就是 m3u8 切片前端用 hls.js 播放。这个方案能大幅减少首屏加载时间因为播放器只需要下载当前几分钟的切片。但如果只是课设演示就不需要搞这么复杂直接传 MP4注意视频分辨率不要太高一般 720P 足够。把视频文件放到 nginx 静态目录下同时配置sendfile开启减轻 Tomcat 压力。这个方案简单也能跑得动。内存方面也要注意Tomcat 默认最大线程数 200Flask 开发服务器只能单线程跑。演示时如果同时有十几个人访问Flask 里的推荐接口会非常慢。我建议用 gunicorn 启动 Flask 服务并配置 2~4 个 worker这样演示体验会好很多。5.5 文档里最容易被忽略的坑很多源码类项目交付后跑不起来大部分原因是环境版本不一致。Java 8 的编译产物丢到 Java 17 环境跑大概率直接报错。所以在调试文档首页一定要用醒目字体标注经过验证的环境组合。例如JDK 1.8 Tomcat 9 MySQL 5.7 Python 3.8 Redis 5.x这是我们已经验证过的组合。数据库版本也很关键。MySQL 8 的连接驱动和 MySQL 5.7 不一样如果你用 8 的驱动去连 5.7虽然大多数情况下能跑但时区设置和排序规则可能会出怪问题。我建议初始化数据库前先校验一下连接串里的serverTimezone参数否则查询时间字段可能出现 8 小时偏移。6. 项目实施过程中的真正心得回头再看这个项目我最大的感受是技术选型没有绝对的好与坏只有合不合适。SSM 和 Flask 混搭看起来有点“不伦不类”但它确实解决了当时的实际问题Java 强于事务和业务建模Python 强于数据分析和算法两边各管一段反而比硬凑一个技术栈舒服得多。很多人做在线教育系统会一头扎进视频播放、订单支付这些看起来很炫的功能里但我建议你先花三天时间把表结构设计清楚。我们后期改得最痛苦的恰恰是那些最初没想清楚的字段。比如 user 表没有加手机号字段后面接短信登录时只能临时加列course 表没有加 is_free 字段免费课没法标记只能靠价格为 0 来暗示但这样报表统计时容易误判。最后分享一个小经验任何系统交付时都要准备一个“一键初始化数据库”的脚本里面包含建库、建表、插入管理员账号和演示数据。我在实际项目里见过太多人因为导入 SQL 时手忙脚乱表顺序错了、外键对不上白白耗掉半天时间。把 SQL 脚本按“表结构 - 基础数据 - 演示数据”三个文件分开并且每个文件开头都写上用途说明后面跑起来会顺畅很多。如果你也正在做类似项目希望这篇内容能帮你少走几步弯路。在线教育系统表面上是 CRUD但真正做下来了你对用户权限、订单事务、数据分析、跨服务协作的理解都会上升一个台阶。