Java+SSM+Flask在线教育系统架构设计与核心模块解析 做了几个管理系统之后我开始重新思考在线教育项目里的技术选型。标题里的“基于JavaSSMFlask在线教育系统”乍一看像技术大杂烩但把在线学习平台的业务拆开看你会发现这正是很多开发团队实际在用的组合JavaSSM负责课程、订单、后台用户管理等重业务链路Flask负责统计报表、学习行为收集这类轻量扩展服务。这篇内容适合三种人看准备做毕业设计的在校生、想搭建在线课程系统的自学开发者、以及接手类似远程教育系统源码但不知道怎么讲解交付的小团队。我会把架构逻辑、表结构、核心接口、调试文档要点和讲解思路一条条摊开讲尽量让你拿到标题里的源码包和LW文档后有底气去改、去讲、去演示。1. 为什么一前一后两套服务而不是一套框架走到底1.1 在线教育业务里最容易踩的需求分层在线教育平台表面看是“课程列表视频播放用户中心”真落到需求文档里会多出一大堆边角料课程分类要支持多级嵌套讲师要单独维护主页订单要处理支付回调、退款、对账学习进度要记录到每一小节甚至视频的播放位置后台还要做数据看板和内容审核。如果把所有功能都堆在一个单体应用里一开始开发效率确实高但到后期会明显难受。比如课程列表页和后台管理页可能有多套权限规则统计模块又需要频繁调整SQL聚合逻辑改一处就要重新编译整个项目。另一方面如果拆成太多微服务对一个小型在线教育系统来说完全是过度设计连部署都要多买好几台机器。我的经验是把它分成两层JavaSSM作为主后端承担用户、课程、订单、权限这些核心业务Flask作为辅助服务承担学习行为日志接收、数据统计接口、简单推荐规则。两套服务之间通过HTTP接口通信数据库可以共享同一个MySQL但业务上尽量各写各的表别在一个Service里混着写。1.2 SSM与Flask各自适合解决什么问题先说SSM。Spring MVC负责接口路由Spring管理对象和事务MyBatis负责SQL映射。这套组合的特点是“稳”事务控制、声明式缓存、拦截器这些能力都很成熟适合处理订单支付这种一不留神就会出数据问题的场景。SSM的缺点是写一个小接口要经过Controller、Service、Mapper三层代码量大一些但这也恰好是规范的地方。再说Flask。它的路由和视图非常轻量一个Python文件几十行就能跑起来。放在在线教育系统里非常适合做三类事情接收前端上报的学习行为数据比如用户看了哪个视频、看了多久、拖动了多少进度。提供统计报表查询接口按天输出注册数、课程销量、学习时长等聚合结果。跑简单的推荐算法根据用户最近课程分类偏好排序返回候选课程ID。选择Flask还有一个现实原因很多在线教育系统的需求说明书里都有“可视化统计”和“智能推荐”模块用Java写也不是不行但Python的数据处理库更齐全写起来更顺手。整个系统对外是一个在线教育平台内部代码结构却可以像“主服务辅助插件”一样灵活演进。2. 核心模块拆解从课程浏览到视频学习的学习闭环2.1 课程中心、讲师和视频资源表结构在线教育系统的数据模型最先要确定的不是用户表而是课程表。因为课程结构天然带层级一门课程下面有多个章节每个章节下再挂视频或文档这种树形结构如果设计不好后面写查询会非常痛苦。我习惯用这样一组表来组织课程域表名说明关键字段t_course_category课程分类支持多级cat_id, parent_id, cat_name, sort_not_teacher讲师信息teacher_id, teacher_name, avatar, intro, statust_course课程主表course_id, title, cat_id, teacher_id, cover, price, statust_course_section课程章节与视频小节section_id, course_id, parent_id, video_url, duration, free_flag其中t_course_section的parent_id字段很关键它让同一张表既能表达章也能表达节。章下面的小节通过parent_id指向章ID章本身parent_id为0。这样前端在课程详情页按顺序渲染目录时一条SQL就能查全。课程状态字段status我建议用整数枚举0草稿、1待审核、2已上架、3已下架。后台管理员审核后修改状态前端只查询status2的课程避免出现“草稿课程被用户看到”的低级事故。这里还要考虑课程的免费和收费。free_flag用来标记某个小节是否可免费试看订单金额则存在课程主表里。视频文件URL一般存相对路径配合部署环境的文件服务或对象存储地址拼接。本地调试时直接放在Tomcat的upload目录下也能跑通但正式交付时要改成可配置的前缀。2.2 订单、支付回执与学习进度的状态机设计订单模块最容易写乱因为支付状态不是只有“已支付”和“未支付”两种。在线教育平台的订单至少要经历待支付、已支付、已取消、已退款四个状态。有些需求还要加“部分退款”“退款中”但学习类课程一般是一次性买断做四种状态就够。我用一张表记录订单CREATE TABLE t_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, course_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, pay_state TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, pay_time DATETIME, cancel_time DATETIME, refund_state TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;pay_state的取值必须写进设计文档0待支付1已支付2已取消3已退款。false? 我见过不少项目把支付状态设计成布尔值结果退款需求一来就傻眼。状态迁移规则也提前定好只有待支付能取消只有已支付能退款支付回执到达时如果订单已经取消必须拒绝处理而不是强行改成已支付。学习进度表可以单独设计不需要和订单表混在一起字段含义备注user_id学生ID联合主键一部分course_id课程ID联合主键一部分section_id最后学习到的小节每次上报覆盖last_position视频播放位置秒用于断点续播total_duration已学总时长用于统计报表update_time最近学习时间用于排序推荐学习进度表按用户课程小节联合主键设计防止同一学生重复学习同一小节产生多条垃圾数据。上报接口收到请求后优先update无记录再insert。3. SSM主链路怎么搭从登录到课程学习的完整请求流3.1 Controller-Service-Mapper分层在业务上的真实作用很多同学拿到SSM源码后只看到Controller里调用ServiceService里调用Mapper但不知道每层到底该写什么。我以课程详情接口为例把职责分清楚。Controller层只做三件事接收参数、校验基本格式、包装返回结果。它不应该直接拼SQL也不应该处理业务逻辑。课程详情接口的Controller写法通常是这样的Controller RequestMapping(/course) public class CourseController { Autowired private CourseService courseService; ResponseBody RequestMapping(/detail) public Result detail(RequestParam Integer courseId, HttpSession session) { Integer userId (Integer) session.getAttribute(userId); CourseDetailVO vo courseService.getCourseDetail(courseId, userId); return Result.ok(vo); } }Service层承载业务规则。比如课程详情接口Service需要判断当前用户是否已经购买该课程这会决定接口返回的章节列表里哪些小节带播放地址哪些被隐藏。查询时先取课程基本信息再查课程章节列表最后根据用户权限拼装VO对象。如果直接让Mapper去联三张表一次查出来SQL会很长而且权限判定逻辑会被SQL掩盖不利于后续扩展。Mapper层只负责单表或明确约定的多表查询。MyBatis的XML文件里保持SQL简洁能拆的拆分。查询课程列表时如果需要join分类表取分类名可以用嵌套查询或者join但不要动辄“SELECT *”把字段写清楚。3.2 登录鉴权与视频学习拦截器在线教育系统的权限逻辑重点不是管理员后台而是“哪些用户能看哪些课程”。视频URL如果直接经过Nginx或Tomcat静态资源映射暴露出去别人拿到链接就能下载或播放所以需要一层拦截器来控制访问。Spring MVC里我用HandlerInterceptor实现登录验证和课程鉴权改造点有两个。第一个是登录拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(userId) null) { response.sendRedirect(/login); return false; } return true; } }第二个是视频资源鉴权。更好的做法是视频URL不直接指向静态文件而是通过一个受保护的接口派发临时播放地址。我的思路是这样的客户端请求/course/play?sectionIdxxx。后端先判断用户是否登录再判断该用户是否购买了当前课程。通过后根据sectionId查视频路径重定向到实际文件地址。如果未购买返回JSON错误提示。有人会说我直接在前端隐藏播放按钮就行。实际项目中这样根本拦不住因为接口地址是公开的只要猜出参数就能直接访问。视频播放鉴权必须放在后端接口层至少要做到每个播放请求都校验一次订单状态不能把订单状态存在Session里然后连续放行。4. Flask辅助服务统计报表与动态推荐接口怎么设计4.1 把学习记录上报到独立的Flask服务为什么学习记录要单独走Flask而不是直接写在SSM里因为学习记录有两个特点频率高、关联少。学生每看一个视频小节就会上报一次中途还会定时上报播放位置这些数据不参与订单事务没必要占用Java端的事务连接和时间片。放到Flask里既能减轻主服务压力也能用Python快速处理后续的聚合统计。设计约定如下前端定时向Flask服务POST /api/study/report参数包括userId、courseId、sectionId、duration、position、token。Flask端先校验token再按联合主键更新学习进度。app.route(/api/study/report, methods[POST]) def study_report(): data request.get_json() user_id data.get(userId) course_id data.get(courseId) section_id data.get(sectionId) duration data.get(duration) position data.get(position) if not all([user_id, course_id, section_id]): return jsonify(code400, msg参数不完整), 400 upsert_study_progress(user_id, course_id, section_id, duration, position) return jsonify(code200, msgok)这里有个容易忽视的问题前端上报要带上唯一请求标识否则网络重试或用户快速拖动进度条会导致重复上报统计数据虚高。这个标识可以放在请求头里Flask端每次处理前检查Redis集合是否已经存在该ID存在就跳过。做到接口幂等统计报表才可信。4.2 Java调用Flask接口时要注意的数据一致性问题主服务和辅助服务之间是跨系统通信最容易出现的是数据不一致。比如用户在Java端完成了支付Flask端立刻查订单统计时如果订单数据还没从Java数据库同步过来统计就会漏单。解决方式不是硬上消息队列而是约定数据边界。我的习惯是订单、课程、用户数据只由Java端写库。Flask端需要订单维度数据时优先实时调用Java端提供的查询接口而不是直接读Java库表。学习进度这种高频数据只由Flask端写Java端需要展示“最近学过什么”时调用Flask接口查询。有人会说Java端和Flask端都配了同一个MySQL数据源Flask直接读t_order表不就行了。确实能读但很危险Flask项目里的SQL一旦没有事务约束很容易误写。更稳妥的做法是Flask只读自己服务范围内的表比如t_study_progress订单表通过Java接口查询。这样就算Flask服务挂掉也不会把主业务数据库搞脏。Java端调用Flask接口通常在课程详情页加载学习进度时使用。可以写一个简单的RestTemplate调用RestTemplate restTemplate new RestTemplate(); String url http://127.0.0.1:5000/api/study/progress?userId userId courseId courseId; StudyProgress progress restTemplate.getForObject(url, StudyProgress.class);Flask接口返回JSON时字段命名统一用驼峰风格Java端直接映射到实体类就方便。因为Flask是PythonJava是强类型语言字段类型要特别注意比如时间字段直接返回格式化字符串不要在JSON里返回一个难解析的datetime对象。5. 调试文档应该包含的真实问题从起不来到锁冲突5.1 环境问题、端口和跨域十次调试九次先栽在这做在线教育系统调试遇到的第一类坑往往是环境而不是代码。我整理一个高频问题对照表可以直接抄进LW文档的调试记录章节里。现象根本原因处理方式Tomcat启动报端口被占用本地有其他程序占用8080用netstat查看端口PID并释放或改Tomcat端口MySQL连接报错Public Key Retrieval is not allowedMySQL8默认认证插件问题JDBC连接串加allowPublicKeyRetrievaltrue中文乱码数据库字符集不是utf8mb4建库时指定utf8mb4ApplicationContext里面设置characterEncoding前端请求404SpringMVC拦截路径配置不对检查web.xml和DispatcherServlet的url-pattern前端请求跨域端口不同导致Origin不一致开发环境加CORS配置Flask端用flask-cors允许指定来源Flask端口与Java端口冲突两台服务都用了5000段附近端口统一规则Java用8080Flask用5000部署时用nginx反向代理跨域问题在本地开发时几乎必现。原因是Vue或HTML页面跑在某个端口Java和Flask服务分别跑在另外两个端口浏览器默认不允许跨端口读数据。SpringMVC端加CorsFilterFlask端加flask-cors两者配置的允许来源不要写“*”生产环境写具体域名开发环境可以放行localhost。5.2 一个典型的并发订单排查链路订单支付接口的调试资料最能体现一个项目有没有做过真实业务。我遇到过的典型问题是并发下单时重复创建订单。两个请求同时进来都判断用户没有购买当前课程然后都执行了insert最终产生两笔待支付订单。排查链路分四步走第一步看日志。日志里出现两个线程同时打印“当前用户未购买该课程”说明判断逻辑本身没错问题是判断和插入之间没有互斥。第二步看事务。Service方法加了Transactional但如果是MySQL默认的可重复读隔离级别两个事务在并发插入时会各自看到旧数据判断结果都是“未购买”。第三步定位锁。修复方式不是把隔离级别改成串行化那样性能太差。常规做法是给用户和课程的组合加唯一索引insert时捕获DuplicateKeyException或者用数据库的for update锁住用户课程记录再判断。第四步验证。用并发测试脚本同时发起20个下单请求观察最终t_order表中只有一条有效订单且只产生一次支付回调响应。这个排查过程完整写进调试文档远比只贴一个“问题已解决”有说服力。评审或接手的人能从排查链路里看出你理解并发控制而不是靠运气调好。5.3 LW文档里调试记录该写到什么程度LW文档在不少交付场景里指的是“设计说明文档”但很多人把它写成了用户手册通篇只有“点击登录、输入账号”。更合理的内容结构是这样需求概述平台解决什么问题目标用户是谁。总体设计系统架构图、功能模块划分、技术选型理由。数据库设计每张表的作用、字段含义、状态枚举、索引设计。接口文档每个核心接口的请求路径、参数、返回示例。调试问题记录调试过程中的异常现象、定位过程、解决代码、复现步骤。操作手册如何初始化数据库、启动服务、部署上线。重点说一下接口文档和调试记录。接口文档不要贴大段源码而是给出请求示例和返回示例哪怕只有五个核心接口也比罗列十个没用的接口强。调试记录建议用表格列出时间、现象、原因、解决方案、验证结果五项。这部分内容在讲解时会被反复提问写清楚就是你做过真实系统的证据。6. 源码包、LW文档与演示讲解怎么组织才不像临时凑数6.1 源码与文档的目录规划交付一个在线教育系统光有一堆源码文件是不够的。我见过很多项目的源码包打开后Java代码和Flask代码混在一起数据库脚本又放在“新建文件夹”里接手的人根本不知道从哪开始。好的交付包推荐按这样分目录online-education-system/ ├── docs/ │ ├── 需求说明.md │ ├── 设计说明.md │ ├── 数据库设计.md │ ├── 接口文档.md │ └── 调试记录.md ├── database/ │ ├── init.sql │ └── test_data.sql ├── backend-ssm/ │ ├── src/ │ ├── pom.xml │ └── README.md ├── service-flask/ │ ├── app.py │ ├── requirements.txt │ └── config.py ├── frontend/ │ └── pages/ └── 部署说明.md前端如果用的是纯HTMLJS就放到frontend目录如果用Vue就单独提一层目录说明编译方式。init.sql里只放建表和初始化数据不要和测试数据混在一起。test_data.sql专门放演示用的课程、学生、订单数据方便答辩时直接演示。6.2 演示脚本怎么设计先看角色后看数据流讲解在线教育系统顺序比内容更重要。最常见的错误是一上来就讲登录页好看、课程列表好看讲完连业务逻辑都没碰。我的建议是按“角色数据流”来走用学生账号登录浏览课程列表筛选分类打开一门免费试听课强调前端页面调用Java端接口获取课程数据。选择一门收费课程模拟下单。如果本地没有支付环境就演示管理员后台把订单置为已支付再从前端刷新看到课程解锁。播放视频拖动进度条通过调试工具展示网络请求里有Flask服务上报学习记录的请求。用管理员账号登录后台查看用户列表、课程审核、统计报表强调报表数据来自Flask聚合计算。最后展示数据库在MySQL中查t_order、t_study_progress验证刚才的操作确实落库。这个流程讲完整条业务链路就闭合了。评审或受众能直观看到Java与Flask各干了什么而不是只听你念PPT。6.3 交付前必须做掉的检查清单在整理源码和文档时我会强制自己过一遍下面的检查清单避免在演示当场翻车。检查项说明数据库脚本能从头执行清空数据库后按顺序跑init.sql和test_data.sql确保没有报错默认账号明确README或操作手册里写清楚学生、管理员账号密码不要让人去找文件路径无本机绝对路径代码里不要出现C:/xxx或/home/xxx改成相对路径或用配置项Flask依赖固定版本requirements.txt写清版本避免换环境后库版本不一致Java依赖能离线编译如果交付环境没网尽量保留maven本地仓库依赖包讲解与代码一致项目代码里不要出现与讲解完全不符的功能名称或模块最后再分享一个个人经验我会把LW文档当成“团队新成员交接手册”来写而不是只给评审看。写完调试记录时顺手把常见启动步骤也写进去半年后自己再打开这个项目不用费劲回忆“当初那个端口是多少”。如果你将来要把这套在线教育系统扩展成正式的在线教学系统这份文档就是你最值钱的积累。