前几天帮某开发者调试一套基于Spring Boot的Android成人教育学习系统,连续搞了两个晚上,最后把问题定位到时间格式和明文网络权限上。这种组合其实是很经典的入门级全栈项目:后端用Spring Boot提供接口,前端用Android原生App承载课程、考试、学习记录这些核心场景。今天这篇文章不打算只罗列功能清单,而是把整套系统的需求边界、后端实现、Android端落地、部署运行以及联调踩坑过程完整拆开,给正在复现或打算二次开发的读者一份可以直接参考的实战记录。
1. 需求拆解与技术选型:成人教育场景下的功能边界
1.1 这个系统到底要解决什么问题
成人教育学员和普通全日制学生的使用习惯差异很明显:多数人是在职状态,学习时间碎片化,更需要的是“随时打开就能学、能测、能追踪进度”的工具型App,而不是一个信息展示门户。我在梳理这个项目需求时,把核心功能收敛成四条主线:
- 用户身份管理:学员注册登录、个人信息维护、管理员对用户的管理。
- 课程内容线:课程分类、课程列表、课程详情、章节视频点播、课件资料下载。
- 在线考试线:成套试卷练习、单选/多选/判断/简答等题型、自动评分、历史成绩查询。
- 学习档案线:学习时长记录、章节完成进度、个人学习统计。
再加上公告通知这类辅助功能,系统的边界就清楚了:不做社交、不做直播、不做复杂的排课系统,先保证“学、练、测、查”这条业务闭环能稳定跑通。很多项目做砸不是因为功能太少,而是一上来就想塞太多东西,导致前后端数据契约混乱、表结构改来改去。先把上面四条主线定死,后面的设计和编码都会顺畅很多。
1.2 技术选型:为什么是Spring Boot + Android + MySQL
后端选Spring Boot几乎是这个场景的默认答案。它内嵌了Tomcat,省去单独配置服务器的步骤;自动配置机制让数据源、MyBatis、事务等整合成本大幅降低。对这套系统来说,单体内核完全够用,没必要上微服务,那只会增加部署和调试的复杂度。
移动端选Android原生而不是H5套壳,主要考虑三点:一是视频播放体验,原生的Media3播放器在硬解、倍速、后台播放上的表现比WebView稳定;二是考试答题页需要频繁保存本地状态,原生端的Activity生命周期管理和本地存储更可控;三是对接系统级能力(比如通知、文件下载)时不需要绕桥接层。当然,如果是跨平台团队,Flutter或uni-app也可以考虑,但就这个项目的代码结构和学习目的来说,Android原生 + Java是最直接的选择。
数据层用MySQL + MyBatis-Plus。MySQL处理课程、试卷、用户这类结构化数据非常稳,MyBatis-Plus又能把单表CRUD的样板代码降到极低,让我能把更多精力放在业务逻辑上。认证方案选了JWT而非传统Session:移动端没有Cookie机制,JWT无状态、可携带用户信息,在App请求头里传Token是当前的主流做法。
1.3 数据库表结构设计:围绕用户、课程、考试三条主线
数据库设计是整个系统的地基,表结构没想清楚,后面写接口时一定到处打补丁。这个项目里我拆成了8张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户表 | username, password, role, phone, status |
| course | 课程表 | title, cover, category_id, difficulty, intro |
| course_chapter | 课程章节表 | course_id, title, video_url, duration, sort |
| exam_paper | 试卷表 | title, category, total_score, duration, status |
| exam_question | 题目表 | paper_id, type, content, options, answer, score |
| exam_record | 考试记录表 | user_id, paper_id, total_score, submit_time |
| learning_record | 学习记录表 | user_id, course_id, chapter_id, duration |
| notice | 公告表 | title, content, create_time |
设计时几个关键点需要特别注意:
- 课程与章节是一对多关系,章节表通过course_id关联课程。章节里的video_url存的是对象存储平台上的视频地址,而不是把视频文件塞进数据库。
- 试卷与题目是一对多关系,题目表用paper_id关联试卷。options字段直接存JSON字符串,比如多选四个选项
["A.选项一","B.选项二"],这样前端渲染灵活,后端也不需要专门建选项子表。 - exam_record只存用户某次交卷的总分和提交时间,不冗余每道题的对错。如果需要查看答题明细,可以再增加exam_answer_detail表按记录ID存储,属于可扩展点。
- learning_record是高频写入表,必须给user_id、course_id、chapter_id建联合索引,否则数据量上来之后统计接口会非常慢。
user表的password字段记得存加密后的密文,不要存明文。这个项目用的是BCrypt加密,Spring Security Crypto库直接引入即可。
2. 后端实现:Spring Boot如何支撑课程、考试与学习记录
2.1 工程骨架与通用响应设计
后端工程按常见的分层结构组织:controller接收请求、service处理业务、mapper访问数据库、entity对应表、dto做参数校验和返回封装、common放统一响应工具类和异常处理。
所有接口出参使用统一的响应结构,这块必须从第一个接口就坚持,否则Android端解析数据时会非常痛苦:
public class RespResult<T> { private Integer code; private String msg; private T data; public static <T> RespResult<T> success(T data) { RespResult<T> result = new RespResult<>(); result.code = 200; result.msg = "ok"; result.data = data; return result; } public static <T> RespResult<T> error(Integer code, String msg) { RespResult<T> result = new RespResult<>(); result.code = code; result.msg = msg; return result; } }Android端拿到响应后先判断code是否等于200,再解析data。统一响应结构的好处是前端拦截器可以集中处理登录过期、权限不足等异常情况,而不是每个页面各自判断。
分页返回也建议统一封装,课程列表、公告列表、考试记录这些高频分页场景都复用同一个结构。我在common包下定义了一个PageResult类,包含total、pages、records三个字段,配合MyBatis-Plus的Page对象使用,查询逻辑非常清爽。
2.2 JWT登录与用户角色权限控制
这套系统的用户分为学员和管理员,权限控制的核心是JWT Token的签发与校验。登录流程是:用户名密码验证通过后,生成一个包含userId和role的Token返回给客户端;客户端后续每次请求在Header里带上Authorization: Bearer <token>;后端通过拦截器解析Token,并把当前用户信息放到ThreadLocal里供Service层使用。
拦截器的核心逻辑大概是这样:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); Long userId = Long.valueOf(claims.get("userId").toString()); String role = claims.get("role").toString(); UserContext.set(userId, role); return true; } catch (Exception e) { // Token无效或过期 response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"unauthorized\"}"); return false; } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }在WebMvcConfigurer里注册拦截器,并配置放行路径:登录接口、注册接口、课程列表、课程详情这些公开查询接口放行,交卷、学习记录上报、个人中心等业务接口全部拦截。角色校验不用重复写在每个Controller里,可以定义一个@RequireRole注解配合拦截器二次校验,或者直接在Service层用UserContext.getRole()判断。
当初做这个设计时有个容易忽略的坑:ThreadLocal在当前请求线程结束后必须清理,否则Tomcat线程池复用时会导致用户身份串号。所以在afterCompletion里调用UserContext.clear()一定要写。
2.3 课程点播与章节管理接口
课程模块的接口设计围绕三个关键点:列表分页、详情聚合、章节播放鉴权。
课程列表接口接收pageNum、pageSize、categoryId、keyword参数,返回分页的课程摘要信息。课程详情接口返回课程基本信息 + 章节列表,这里存在一个典型问题:直接返回Entity会把不该暴露的字段也带出去。我实际处理时定义了CourseDetailVO,只包含title、cover、intro、difficulty、chapterList这些必要字段,视频URL也只在用户登录后返回,避免未登录用户直接抓包拿到课程资源地址。
章节视频的点播地址加了一层有效期控制:后端生成一个带过期时间的临时播放地址返回给客户端,客户端拿到后立即交给播放器加载。有效期一般设置成2小时,既能满足正常学习场景,又能防止地址被无限转发。这个设计在文档里可能只是一句话,但实际处理权限和资源保护时非常有用。
2.4 在线考试:组卷、答题与自动评分
考试模块是业务逻辑最重的一块,核心流程是:学员选择试卷 -> 后端生成考试记录 -> 前端按题目类型渲染 -> 学员交卷 -> 后端遍历题目自动评分 -> 返回总分和得分明细。
题库表exam_question通过type字段区分单选、多选、判断、简答。单选和判断的答案是单个字母或关键字,多选是逗号分隔的字母组合,简答题的评分则用关键词命中率计算:后台给每道简答题配置一组关键词和分值占比,学员答案命中关键词就给对应比例的分。
交卷接口是整个系统里唯一需要严格事务控制的接口:
@Transactional(rollbackFor = Exception.class) public ExamSubmitResult submitExam(Long userId, Long paperId, List<AnswerItem> answers) { ExamRecord record = new ExamRecord(); record.setUserId(userId); record.setPaperId(paperId); // 遍历答案并计算得分 int total = 0; for (AnswerItem item : answers) { ExamQuestion q = questionMapper.selectById(item.getQuestionId()); int score = scoreQuestion(q, item.getAnswer()); total += score; // 插入答题明细 examAnswerMapper.insert(buildAnswerDetail(record.getId(), q.getId(), item.getAnswer(), score)); } record.setTotalScore(total); examRecordMapper.insert(record); return new ExamSubmitResult(record.getId(), total); }这里的细节是:先插入ExamRecord拿到自增ID,再插入每道题的答题明细,所有操作必须在一个事务里。如果评分到一半失败,前面插入的答题明细也要一起回滚,宁可让学员重新交卷也不能出现半个答卷存档。
自动评分还有一个边界情况:每题的分值可能是1.5分、2.5分这种小数,如果后端用double累加会出现精度问题,我放在后面的踩坑实录里专门讲。
2.5 学习记录上报与防刷设计
学习记录接口需要处理高频写入和重复上报。客户端在播放视频时每隔15秒上报一次当前播放进度和章节ID,后端拿到请求后先查询是否已有该用户该章节的学习记录:
- 有记录则累加本次观看时长并更新最后观看位置。
- 没记录则新插入一条。
接口要做到幂等,避免客户端网络重发导致时长被重复累加。做法是给「userId + courseId + chapterId」加唯一索引,上报逻辑用先查询再插入或更新的方式。
防刷时长是个容易忽略但必须考虑的点。有些学员挂机刷时长,短期内上报大量值,会让学习统计失去参考意义。我在Service层加了几道防线:
- 单次上报的增量时长超过10分钟直接拒绝。
- 每个章节每日累计学习时长上限为章节视频时长的1.2倍。
- 视频倍速播放产生的时长按实际播放时长计算,不允许一次上报超出视频总时长。
虽然没法做到100%准确,但配合这些规则,足够让大多数正常用户数据保持可信。这个项目的统计报表并不是核心亮点,但如果数据被刷得乱七八糟,个人中心和课程完成度展示就会变成摆设。
3. Android端:从登录到学习完课的产品链路落地
3.1 客户端整体架构与网络层封装
Android端我采用的是简洁的分层思路:Activity负责页面交互,网络请求统一走Retrofit + OkHttp,数据模型用Java Bean对应后端返回的JSON。业务简单时没必要引入太重的MVVM框架,但网络层一定要统一封装,否则每个页面写一套请求逻辑会让日志排查变成灾难。
网络层的核心是统一拦截器,主要负责两件事:给所有请求自动附加Token、统一处理后端返回码。
public class AuthInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); Request request = original.newBuilder() .header("Authorization", "Bearer " + TokenManager.getToken()) .method(original.method(), original.body()) .build(); return chain.proceed(request); } }Retrofit的Service接口定义尽量贴近后端接口,课程列表、登录、交卷等操作各对应一个方法。响应体的泛型统一用RespResult<Xxx>,这样Retrofit会自动把data字段解析成具体的业务模型。这里要多说一句:后端字段命名和移动端要保持一致,要么都用驼峰,要么都用下划线,并配置好Jackson的命名策略,避免出现前端拿不到字段这种低级问题。
3.2 登录态与会话保持细节
Token不能只保存在内存里,Android进程被杀后Token就丢了,用户需要重新登录,这种体验很糟糕。项目里我用SharedPreferences保存Token和用户基本信息,登录成功后立刻写入,Application启动时读取。
每次请求需要判断Token是否存在。Token过期时后端会返回401,网络层拦截到401后需要做两件事:清空本地登录态、跳转到登录页。这里有个细节:跳转不能直接startActivity,因为栈里可能有一堆页面,正确的做法是清空Activity任务栈,把登录页作为新的根页面。实际项目里用Intent的FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK就能解决。
个人中心的用户信息建议异步刷新而不是完全依赖本地缓存。这样即便头像、昵称在后端被管理员修改过,学员重新打开App时也能看到最新数据。
3.3 课程列表、课程详情与视频播放
课程列表用RecyclerView展示卡片式课程信息,封面图用Glide加载。课程详情页进入后,根据courseId请求详情接口,拿到章节列表后用RecyclerView渲染,点击章节后跳转到播放页。
视频播放器用的是Android官方的Media3播放器,相比自己封装MediaPlayer省大量工作。播放页的核心逻辑包括:
- 根据章节ID请求临时播放地址。
- 播放器初始化并加载地址,支持横竖屏切换。
- 监听播放进度,每15秒上报一次学习记录。
- 页面销毁时释放播放器资源。
播放器的生命周期处理要特别注意:在onPause中暂停播放并上报当前进度,在onDestroy中释放播放器实例,否则会出现返回页面后声音还在播放的经典问题。如果项目里加入了断点续播,需要把最后播放位置存到本地SharedPreferences或数据库,下次进入时直接seek到上次位置。
3.4 考试模块的状态保存与交卷确认
考试答题页是Android端最容易写崩的页面,因为它既有复杂的界面状态,又有严格的生命周期要求。我实现时把试卷的题目列表一次性加载到内存,用一个数组保存学员每道题的答案,视图层根据当前题号切换显示。
考试过程中可能发生来电、切到后台、系统回收Activity等意外情况,答题状态不能只存在内存里。项目中选择把答题进度实时写入本地数据库,每切换一道题就更新当前题目的答案。恢复页面时从数据库读取已答记录,避免学员答了20道题、切回桌面两分钟再进来发现答案全丢的严重事故。
交卷按钮必须二次确认,用弹窗提示“交卷后不可修改”。交卷接口调用成功后,后端返回总分和正确题数,页面跳转到成绩页并清空本地答题缓存;如果网络请求失败,已经填写的答案仍然保留在本地,学员可以重新点击交卷,而不是丢失所有作答。
3.5 学习进度展示与本地缓存
个人中心展示学习时长、已学课程数、待完成考试等统计信息。这些数据来自两个学习记录接口:学习总览和近期学习明细。页面加载时做简单的本地缓存,缓存时间设为5分钟,避免每次打开个人中心都触发网络请求。
离线场景下,如果网络请求失败,可以展示缓存中最有一次的数据,同时提示“当前网络不可用,展示的是缓存数据”。这个处理虽然简单,但对用户体验的提升非常明显,尤其是考试间隙或通勤路上这类弱网环境。
学习进度还可以配合课程详情页做“继续学习”入口:根据最近一条学习记录回到上次学习的章节。这个功能让我觉得整个学习档案线真正闭环了,学员打开App不会迷茫,直接看到自己学到哪里。
4. 从零跑通:环境准备、部署联调与交付物使用
4.1 版本搭配:JDK、MySQL、Android Gradle怎么选
跑通这类项目最怕的就是环境版本不匹配。很多初学者按最新的Spring Boot 3.x教程搭环境,然后发现项目里全是2.x的依赖配置,各种报错。这里整理一套稳妥的版本组合:
| 环境 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 兼容性最好,绝大多数教程和依赖都支持 |
| Spring Boot | 2.7.x | 稳定成熟,内嵌Tomcat配置简单 |
| MySQL | 5.7或8.0 | 注意驱动版本配合,8.0连接串需要指定时区 |
| Maven | 3.6+ | 管理后端依赖 |
| Android Studio | 较新稳定版即可 | compileSdk 33左右 |
| Retrofit / OkHttp | 2.9 / 3.12+ | Android端网络层标配 |
| 对象存储SDK | 按需集成 | 用于视频和封面存储 |
特别注意:Spring Boot 3.x要求JDK 17,项目的依赖如果还是基于2.x,不要直接换版本,否则MyBatis、JWT等库的API可能完全不同。先跑通再升级,升级是另一个话题。
4.2 数据库初始化与后端配置
拿到源码后第一步是建库并导入SQL脚本。用数据库客户端工具执行项目附带的database.sql,执行成功后确认核心表都已经创建。然后修改application.yml里的数据源配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/edu_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss jwt: secret: your-jwt-secret-key expire: 604800JWT密钥建议改成至少32位的随机字符串,不要用默认值,否则Token容易被人伪造。配置完成后,在项目根目录执行mvn spring-boot:run,看到Tomcat started的日志基本说明后端已经起来了。也可以用mvn package打成jar包,再通过java -jar xxx.jar运行,适合部署到服务器。
4.3 Android Studio联调:模拟器地址与真机局域网
Android端联调时最容易卡在BaseUrl配置上。后端跑在电脑上,Android模拟器与宿主机不是同一个网络,不能直接访问localhost:8080。模拟器访问电脑的固定地址是10.0.2.2:8080,这个地址在项目network层要写成:
public class ApiConfig { public static final String BASE_URL = "http://10.0.2.2:8080/"; }真机调试时情况又不一样:手机需要和电脑处于同一WiFi,而且BaseUrl要改成电脑的局域网IP,比如http://192.168.1.123:8080/。还要注意电脑防火墙必须放行8080端口,同时后端启动时监听地址要是0.0.0.0而不是默认的localhost,否则手机访问不到。
第一次真机联调建议先用浏览器在手机上访问一下http://<电脑IP>:8080/api/health这类公开接口,能打开再启动App,可以快速判断网络链路是否通。
4.4 文档、运行视频和讲解视频的正确使用姿势
这类项目交付物一般包括源码、数据库脚本、运行视频和讲解视频。我的建议是使用顺序正好反过来:先跟着文档把环境装好、把项目跑起来,遇到跑不通的地方再看运行视频确认操作细节,最后在系统跑通的基础上看讲解视频理解设计和代码。
运行视频通常录的是“后端启动 + Android端安装 + 核心功能演示”,它最大的价值是告诉你预期效果长什么样。如果你跑起来之后界面和视频里不一致,说明某个环节漏了。讲解视频则是按模块讲代码,适合在自己读代码遇到卡点时定向回看,而不是从头到尾一口气看完。
文档里如果没有环境版本信息,优先看pom.xml和build.gradle里的版本声明,以实际依赖为准。
5. 踩坑实录:前后端联调中最常见的五个问题
5.1 后端返回时间比实际慢了8小时
某次联调时,Android端显示的学习记录时间是下午两点,但数据库里存的时间明明是晚上十点,两个时间刚好差8小时。第一反应是数据库出问题了,但用SQL直接查发现日期字段是对的,最后定位到是Jackson序列化时区问题:后端默认用UTC时区序列化日期,而Android端解析后没有做时区转换。
解决方法是双管齐下:在application.yml中配置Jackson时区为GMT+8,同时JDBC连接串指定serverTimezone=Asia/Shanghai。改完重启后端,再查看接口返回的时间就正确了。这个坑很隐蔽,因为不仔细看根本注意不到,而且不同机器的默认时区可能还不一样,所以配置时区一定要显式声明。
5.2 Android 9及以上无法访问HTTP明文接口
前端日志一直报“CLEARTEXT communication to 10.0.2.2 not permitted by network security policy”,这是Android 9(API 28)开始默认禁止明文HTTP流量的结果。项目开发环境用的全是http://地址,所以请求直接被系统拦截。
最简单的解决方式是在AndroidManifest.xml的application标签上加上:
<application android:usesCleartextTraffic="true" ...>生产环境不建议这样全局放开,更规范的方案是配置network_security_config.xml,只允许特定开发域名走明文。这个坑基本是Android新手必踩,而且很容易被误判成后端CORS或网络故障,浪费一个晚上才找到真因。
5.3 跨域配置缺失导致接口请求被拦截
有段时间用Web端后台管理页面调试接口,请求发出去后端日志显示正常处理,但浏览器里报CORS错误。这是因为浏览器同源策略拦截了跨域响应,而后端没有返回允许跨域的响应头。
虽然正式App端不受浏览器同源策略影响,但开发阶段经常要用Swagger、Web管理端联调,所以Spring Boot端建议直接加全局跨域配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }allowCredentials(true)时allowedOrigins不能写成*,要用allowedOriginPatterns。这个细节如果不注意,配置了却仍然报CORS错误,会让人非常恼火。
5.4 视频与图片加载失败:URL拼接才是罪魁祸首
课程封面上传成功后,后台返回的对象存储地址是https://xxx.example.com/file/2332.jpg,但App里请求的地址变成了http://api.example.com/https://xxx.example.com/file/2332.jpg。前端加载失败时第一反应是服务器文件没传上去,实际上把存储地址又拼了一次前缀。
这类问题最好的排查方式是:把Android端日志里的完整URL复制到电脑浏览器直接访问,看看能不能打开。能打开说明后端返回地址本身没问题,问题出在客户端拼地址;不能打开再检查对象存储权限和文件是否存在。图片和视频加载类问题,十次里有七次是URL拼接问题,先把日志里的URL看明白再动手改代码。
5.5 自动评分出现1.999999这种诡异分数
某次考试模块自测时,一道3分题目的得分被人为记录成了2.999999。原因是评分代码用double做累加,浮点数运算本来就存在误差,多次累加后误差被放大。虽然页面展示时四舍五入看不出来,但存入数据库后就暴露出真实精度。
解决方案有两种:一种是得分计算全程用BigDecimal,另一种更简单粗暴——把分数统一乘以10存整数,3分存30,交卷结果展示时再除以10。考虑到题目分值大多是0.5的倍数,乘10正好能精确表示,实现成本低,还不容易出现BigDecimal比较和舍入的麻烦。经过这个坑之后,我在这套系统里凡涉及小数金额、分数累计的地方,一律用整数最小单位存储。
6. 拿到源码后如何真正消化:阅读顺序与二次开发建议
6.1 推荐的源码阅读路线
拿到一套完整项目,不要从第一个文件逐个读到最后一个,那样效率太低。我的建议是先跑起来,然后按三条请求链路走读代码:
- 登录链路:Android登录按钮 -> Retrofit接口 -> Spring Boot Controller -> Service -> JWT签发。
- 课程查询链路:课程列表页 -> 分页接口 -> MyBatis查询 -> VO封装返回。
- 考试交卷链路:交卷按钮 -> 答题明细上传 -> 事务评分 -> 结果回写。
每条链路都在关键位置打断点,看数据怎么流转、字段怎么映射。这三条链路走完,整个项目的前后端协作方式基本就掌握了。剩下的公告、个人中心、学习记录都是同类模式,复用同样的方法论即可。
6.2 低成本改造:改名换肤与模块裁剪
如果想把这个项目变成自己的作品,先做视觉层面的低成本改造。Android端修改应用名称、替换Logo、调整主题色;后端同步修改系统标题、公告文案和默认数据。包名修改可以用Android Studio的Refactor重命名功能,但要注意AndroidManifest.xml和所有import语句里的包路径同步更新,改完一定要清理缓存重新构建。
业务层面,如果不需要考试模块,把Android端的考试入口页面和后端考试相关Controller、Service、Mapper删掉即可。但记得数据库里考试相关的表也可以一并清掉,不然留着空表不碍事但显得不专业。反过来,如果要增强考试模块,可以加随机抽题、倒计时提醒、错题本这三个功能,每个都能单独写成小的锻炼项目。
6.3 三个值得尝试的扩展方向
这套系统跑通之后,有几个扩展方向投入产出比很高:
- 直播课堂:在课程模块上增加直播房间表,集成第三方直播SDK,学员端支持回放。需要新增直播预告、直播状态流转、弹幕等表设计。
- 课程购买与支付:现有课程多数是免费点播,要商业化需要引入订单表、支付流水表,对接主流支付SDK。后端的回调处理和订单状态机是主要难点。
- 消息推送:接入第三方推送服务,在公告发布、考试发布时推送通知。需要维护设备Token表,并把推送触发点嵌入公告创建和试卷发布逻辑。
这三个方向都有清晰的业务边界,适合作为后续扩展练习。每完成一个,对Spring Boot和Android的综合理解都会明显上一个台阶。
最后说点个人体会。这类系统的单个技术难点其实不高,但它把Spring Boot、MySQL、Android原生、网络请求、Token鉴权、事务处理这些零散知识点串成了一条完整的业务链路,这比单独看框架文档有价值得多。我建议复现的时候别满足于“能跑”,而是亲手去改一个功能,比如把固定试卷改成随机抽题、把学习记录改成可视化图表。改的过程会逼着你把前后端的数据契约、状态同步、异常处理全部重新过一遍,这些才算真正内化成自己的经验。