基于Spring Boot+Redis的操作系统日常促学系统设计与实现

发布时间:2026/10/7 10:28:00
基于Spring Boot+Redis的操作系统日常促学系统设计与实现 做了个“操作系统日常促学系统”可能是最近被课程设计催出来的标准动作。操作系统这门课知识点又散又抽象进程管理、内存管理、文件系统、死锁这几块上课听懂了隔三天就还回去大半。期末全靠突击突击又只看得上重点考完就忘。所以我就想着用 Java 把整套“学习操作系统”的过程做成一个日常促学系统每天定点给任务、打卡签个到、随机出几道题用外部节奏把学习习惯给带起来。这套系统从需求到落地就是一个标准的 Java Web 全栈项目Spring Boot 做后端MyBatis-Plus 操作 MySQLRedis 扛打卡防重和分布式锁前端拿 Vue 搭了个轻量页面。整体代码量不大但“设计”上花的精力比“实现”多得多。如果你正在纠结课程设计选题或者想把 JVM、数据库、Redis 这些 Java 技术栈串成一个完整项目练手这篇东西应该能给你省不少事。我会把需求怎么拆、表怎么建、关键代码怎么落以及我实测踩过的坑都写清楚。1. 系统整体设计与技术选型1.1 促学系统到底在解决什么问题先想清楚一件事这个系统不是“在线考试系统”也不是“课程资料站”。促学系统的核心目标是让用户每天都能做一点跟操作系统学习相关的动作并且能直观看到自己坚持了多久、掌握了多少。所以设计的第一原则是“闭环”任务下发、用户执行、反馈记录、成果可视化四段缺一不可。针对操作系统这门课的实际情况我拆了四类日常动作阅读任务每天给你一段教材章节范围内的知识点摘要看完标记完成。练习任务围绕某个知识点出几道选择题直接提交判分。测验任务从题库里按章节权重随机抽一套题模拟小测。复习任务把过去一周的错题重新拿出来做一遍。也就是说每天的“促学任务”不是拍脑袋定的而是通过一个任务模板配置出来。管理员可以在后台设定周一三五侧重进程管理周二四侧重内存管理周末做综合测验。系统在每天零点按模板把任务账单生成给所有用户用户第二天在页面上看到自己的待办清单。1.2 技术栈选型Spring Boot 3 MyBatis-Plus 为什么够用这个项目定位是日常促学不是高并发电商系统所以在技术栈上我刻意选了“最稳、最通用、资料最多”的组合而不是为了炫技引入一堆中间件后端Spring Boot 3.x MyBatis-PlusRESTful APIJWT 做登录态。数据库MySQL 8.0innodb 引擎。缓存与锁Redis 6.x主要用 String 结构做打卡防重和定时任务分布式锁。定时任务Spring 自带 Scheduled没有专门引入 Quartz。因为场景只有“每天生成任务”和“每天提醒打卡”两个Spring 调度足够。前端Vue 3 Element Plus分为用户端和管理端。为什么不用 Spring WebFlux、不搞微服务一句话成本不匹配。Spring Boot 传统模型对新手和课程设计场景极其友好排错资料多MyBatis-Plus 能让 CRUD 代码量少一半Redis 只在一个很小的范围里介入不会把系统复杂度抬起来。这个系统的重点在于业务逻辑闭环而不是架构复杂度。1.3 功能模块清单与边界划分整个系统分四块边界一定画清楚不然后面代码会烂用户模块注册、登录、个人设置。这里我只做了基于用户名的登录没接短信邮件验证码因为促学系统的用户是校内学生没必要把登录复杂度拉高。学习任务模块任务模板管理、每日任务生成、今日任务列表。这是全系统的“发动机”模板配置决定用户每天拿到什么任务生成逻辑决定什么时间点任务落地。打卡与测验模块日常打卡、补卡策略、随机出题、判分、错题记录。这是“闭环”里的执行环节用户在这里花掉每天的学习时间。统计模块连续打卡天数、任务完成率、知识点掌握度、学习日历。这是“反馈”环节人都是靠反馈才能坚持的。管理后台单独维护知识点、题库和任务模板用户端不接触这些维护功能只看到每天生成的结果。权限上用最简单的角色字段区分 user 和 admin。2. 数据库设计与业务模型2.1 核心表结构设计与 DDL数据库是这种促学系统的地基表设计好了业务逻辑写起来非常顺。我最终拆了六张表用户表 user、知识点表 knowledge_point、题库表 question、日常任务表 daily_task、打卡记录表 check_in_record、测验记录表 quiz_record。下面挑三张核心表给出 DDL。CREATE TABLE daily_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 为空表示公共任务否则是个人任务, task_date DATE NOT NULL COMMENT 任务归属日期, task_type TINYINT NOT NULL DEFAULT 1 COMMENT 1学习 2练习 3测验 4复习, knowledge_point_id BIGINT COMMENT 关联知识点练习或测验时使用, question_count INT NOT NULL DEFAULT 0 COMMENT 题目数量, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待完成 1已完成 2已过期, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, task_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE check_in_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, check_date DATE NOT NULL COMMENT 打卡日期注意是业务日期非创建时间, task_count INT NOT NULL DEFAULT 0 COMMENT 当天任务总数, finished_count INT NOT NULL DEFAULT 0 COMMENT 已完成数量, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_check_date (user_id, check_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE quiz_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, task_id BIGINT NOT NULL COMMENT 关联那次任务, score DECIMAL(5,2) NOT NULL COMMENT 百分制得分, question_ids VARCHAR(2048) NOT NULL COMMENT 题目id列表逗号分隔, answer_snapshot JSON COMMENT 用户答案快照用于复盘, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个设计要点值得说明。第一daily_task 和 check_in_record 都建了“用户日期”的唯一索引这是防重复的最后一道物理防线。第二打卡记录里存的是 check_date 这种“业务日期”而不是直接拿 created_at 当打卡日期。因为服务器可能跨时区部署用户也可能跨天操作按业务日期统计才能保证“连续打卡”算得准。2.2 每日任务数据模型全局任务还是个人任务任务生成这块最开始的方案是“管理员每天手动静默指定”做了一半发现不对因为用户一旦多了管理员根本没法针对每个人去配置。后来改成了“全局任务模板 按用户实例化”的模式。任务模板 task_template 表只存任务类型、关联知识点范围、题目数量、适用章节不存具体用户。每天零点一个定时任务扫描所有活跃用户根据他们的学习进度选择匹配的模板生成当天的 daily_task 记录。这样设计的好处是模板维护成本极低一个学期只需要配置二十来条模板而每个用户每天都有一条属于自己的任务账单进度可以独立统计。daily_task 里我特意留了 user_id 字段并且允许为空。全局公共任务比如每周日的综合测验可以只生成一条公共记录用户打卡时再单独生成 check_in_record避免重复数据堆量。实际写代码时查询逻辑要加一句“user_id 当前用户 OR user_id IS NULL”。2.3 状态字段和冗余字段的取舍状态字段我统一用 TINYINT不用字符串。0 待完成、1 已完成、2 已过期这比存储 pending/finished/expired 更省空间查询走索引也好看。代码里再用枚举类做映射不直接在业务代码里写魔法数字public enum TaskStatus { TODO(0), DONE(1), EXPIRED(2); private final int code; TaskStatus(int code) { this.code code; } }另外quiz_record 里存了 question_ids 和 answer_snapshot 两个“冗余”字段。按正规范式来说用户答案应该拆一张子表但促学系统体量不大用 JSON 快照能减少大量关联查询复盘时直接把记录捞出来就是一个完整的答题现场。我建议在课程设计这种规模的项目里适度冗余是可以接受的只要你别在核心的业务流水表上也全盘 JSON 化就行。3. 核心功能实现细节3.1 定时任务生成每日学习计划核心逻辑是一个 Spring 的 Scheduled 方法每天零点执行。cron 表达式必须写成六位秒分时日月周很多人在这里翻车。Component Slf4j public class DailyTaskSchedule { Resource private DailyTaskMapper dailyTaskMapper; Resource private UserMapper userMapper; Resource private TaskTemplateMapper taskTemplateMapper; Scheduled(cron 0 0 0 * * ?) public void generateTodayTask() { ListLong activeUserIds userMapper.selectActiveUserIds(); for (Long userId : activeUserIds) { ListTaskTemplate templates taskTemplateMapper.selectMatchedTemplates(userId, LocalDate.now()); if (CollectionUtils.isEmpty(templates)) { log.warn(userId{} 没有匹配的任务模板, userId); continue; } for (TaskTemplate template : templates) { DailyTask task new DailyTask(); task.setUserId(userId); task.setTaskDate(LocalDate.now()); task.setTaskType(template.getTaskType()); task.setKnowledgePointId(template.getKnowledgePointId()); task.setQuestionCount(template.getQuestionCount()); dailyTaskMapper.insert(task); } } log.info(每日任务生成完毕用户数{}, activeUserIds.size()); } }这里的 selectMatchedTemplates 不是简单查表而是根据 userId 在 knowledge_point 上的历史掌握度判断今天推到哪个章节。比如用户最近在“死锁”这个知识点上错误率高那模板匹配逻辑就优先返回死锁相关的练习模板。我的做法是给知识点表加了一个 difficulty 和 weight 字段匹配时算一下加权评分。3.2 打卡逻辑Redis 防重 数据库唯一索引兜底打卡是整个系统最容易被并发打穿的地方。用户手一抖多点两下提交前端虽然能禁用按钮但后端必须兜住。我采用了两层防护第一层是 Redis SetNX。用户在点击打卡时后端先生成一个唯一的 keycheckin:{userId}:{yyyy-MM-dd}。用 setIfAbsent 去占位占位成功说明今天是第一次打卡可以继续业务逻辑占位失败直接返回“今日已打卡”。第二层是数据库唯一索引 uk_user_check_date。万一 Redis 数据被清空或者多实例之间出现极端情况插入时也会因为唯一索引冲突而被数据库拦下来。public void checkIn(Long userId) { LocalDate today LocalDate.now(); String lockKey String.format(checkin:%d:%s, userId, today); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofHours(24)); if (Boolean.FALSE.equals(locked)) { throw new BizException(今日已打卡不用重复提交); } try { ListDailyTask tasks dailyTaskMapper.selectTodayTasks(userId, today); int total tasks.size(); int finished countFinished(tasks); CheckInRecord record new CheckInRecord(); record.setUserId(userId); record.setCheckDate(today); record.setTaskCount(total); record.setFinishedCount(finished); checkInRecordMapper.insert(record); } catch (Exception ex) { redisTemplate.delete(lockKey); throw ex; } }这里有一个细节很多人容易忽略Redis 的 setIfAbsent 和 MySQL 的事务并不是原子的。即使 Redis 占位成功后面 insert 可能因为网络问题失败所以 catch 里必须把占位 key 删掉否则用户今天的打卡资格就莫名其妙被吞了。如果 insert 本身抛了 DuplicateKeyException说明数据库已经有一条记录此时也把 Redis key 删掉但不要继续往下写入。3.3 随机测验与错题本随机出题我一开始用 ORDER BY RAND() LIMIT n表小的时候很爽数据量过万就开始拖慢查询。后来改成“按知识点权重抽样”先根据用户近期的薄弱知识点确定要抽哪些章节再从每个章节里用主键区间随机取题。关键代码大概是这样的逻辑public ListQuestion pickQuestions(Long userId, Long knowledgePointId, int count) { ListQuestion pool questionMapper.selectByKnowledgePoint(knowledgePointId); if (pool.size() count) { return pool; } // 用 Random 选出一个子集避免每次出题一样 Collections.shuffle(pool, new SecureRandom()); return pool.subList(0, count); }判分部分单选题直接比对选项多选题用集合判断是否完全一致判断题比对布尔值。用户提交后系统算出一个百分制得分得分低于 60 时自动把涉及的知识点 id 记到用户的“薄弱区”表里并且把做错的题目写入错题本。错题本还有一个用途每周日的复习任务会优先从错题本里选题而不是重新出新课的题。3.4 连续打卡和知识点掌握度统计统计最容易踩的性能坑是循环查数据库。比如要算用户连续打卡天数如果你写一个 for 循环每天查一次是否存在记录连续 60 天就要查 60 次。小系统无所谓但我还是建议写成一条 SQL 解决public int calcContinuousDays(Long userId) { return checkInRecordMapper.countContinuousDays(userId, LocalDate.now().minusDays(60), LocalDate.now()); }对应的 SQL 用窗口函数对按日期排好序的记录做分组判断SELECT COUNT(*) FROM ( SELECT check_date, DATE_SUB(check_date, INTERVAL ROW_NUMBER() OVER (ORDER BY check_date) DAY) AS grp FROM check_in_record WHERE user_id #{userId} AND check_date #{today} ) t WHERE grp DATE_SUB(#{today}, INTERVAL (SELECT COUNT(*) - 1 FROM ... ) DAY)当然如果 MyBatis-Plus 里写窗口函数不顺手直接内存里把这段日期的打卡日期列表查出来用一个 HashSet 去数连续天数也完全够用。原理很简单从今天往回走今天打过卡就 days1昨天也打过就继续哪一天断了就停止。知识点掌握度我采用了加权滑动平均公式是score 最近一次正确权重 * 0.5 前一次正确权重 * 0.3 更早一次 * 0.2正确权重就是这道题如果做对了算 1做错了算 0。这样出来的掌握度比简单算正确率更能反映“最近变好了还是变差了”用户看到图表时也更容易产生继续学的动力。4. 实操过程从零到一跑通全流程4.1 环境准备与项目初始化这个项目的开发环境我建议统一成以下版本能省掉很多莫名其妙的问题JDK 17Spring Boot 3 要求 JDK 17 起步Maven 3.8MySQL 8.0Redis 6.x 或者 7.xNode.js 16给前端 Vue 项目用创建后端工程时用 Spring Initializr 勾选 Web、MySQL Driver、MyBatis-Plus如果你的初始化器没有该选项就手动在 pom.xml 里加依赖。Spring Boot 3 下 MyBatis-Plus 要用 3.5.3 版本老版本在自动配置上会出问题。pom.xml 里几个关键依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.11.5/version /dependency配置文件 application.yml 里MySQL 连接串一定要加 serverTimezone 和 useUnicode否则本地跑得好好的部署到服务器上会出现时区差 8 小时和中文乱码问题spring: datasource: url: jdbc:mysql://localhost:3306/study_os?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 keepalive-time: 30000 data: redis: host: localhost port: 63794.2 完整打卡链路代码走读把核心的打卡链路串起来看这大概是整个系统最精华的部分。Controller 入口很简单RestController RequestMapping(/api/checkin) public class CheckInController { Resource private CheckInService checkInService; PostMapping(/today) public ResultVoid checkIn(RequestAttribute Long currentUserId) { checkInService.checkIn(currentUserId); return Result.success(); } }Service 里除了前面说的 Redis DB 双保险还有一个隐含逻辑打卡前要校验今天的任务有没有做完。促学系统的规则是“全完成才能打卡”这样才能保证打卡记录是有含金量的。但也有人会觉得这个规则太严格容易劝退所以我留了一个开关后台可以配置“部分完成也允许打卡但完成率低于 80% 时连续天数不增加”。这个开关的实现不算复杂就是在打卡 Service 里加一个分支判断if (checkInConfig.getRequireAllFinished()) { if (finished total) { throw new BizException(今日任务未全部完成打卡失败); } } else { double rate total 0 ? 0 : (double) finished / total; if (rate checkInConfig.getMinRate()) { throw new BizException(今日完成率过低不计入连续打卡); } }这个设计在答辩或者演示的时候是个加分项能体现你考虑过用户激励和流失率的问题。4.3 前端页面交互要点前端我用了 Vue 3 Element Plus最核心的页面就三个今日任务页、打卡日历页、统计分析页。今日任务页是用户每天早上打开系统看到的第一屏。任务列表按类型分组阅读任务显示知识点摘要练习和测验任务显示“开始答题”按钮。完成一个任务后按钮变成绿色列表右上角实时显示今日完成率。打卡日历页是促学系统的灵魂类似于 GitHub 的贡献图。用户能一眼看到自己这星期、这个月坚持了几天。日历的数据来自后端一个接口GET /api/stats/calendar?userIdxxxmonth2025-06返回的数据是[{date: 2025-06-01, status: 1}, ...]前端用热力图颜色渲染。实测下来这个页面的留存刺激效果是全场最强的很多用户就是为了让日历格子变得连续而回来打卡。4.4 部署到 Linux 服务器的几个步骤部署这一块我踩过最多的坑。项目本身不难但 Linux 环境下的 Java 进程和数据库时区如果没配好很容易出现“本地能跑、服务器乱七八糟”的情况。我的部署方式是用 Docker Compose 一次性把 MySQL、Redis、后端、前端全拉起来。关键就两点容器里的时区统一设置为 Asia/Shanghai否则 MySQL 容器和 Java 容器之间可能出现时间差。后端配置文件用环境变量注入数据库密码不要把明文密码提交到仓库里。version: 3.8 services: mysql: image: mysql:8.0 environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - ./data/mysql:/var/lib/mysql redis: image: redis:7-alpine command: redis-server --appendonly yes backend: build: ./backend depends_on: - mysql - redis environment: MYSQL_HOST: mysql REDIS_HOST: redis frontend: build: ./frontend ports: - 8080:80前端构建产物放到 Nginx 镜像里后端接口用/api前缀Nginx 里加一条 location 规则反向代理到后端的 8080 端口。5. 常见问题与排查技巧实录5.1 定时任务不触发的三类原因第一类忘了在主类上加 EnableScheduling。这个看起来低级但确实容易漏尤其是把定时任务类写完以后没回主类确认。第二类cron 表达式写错。Spring 的 cron 是六位“秒 分 时 日 月 周”如果你按 Linux crontab 的五位写法写就是静默失败日志不报错。第三类服务器时区问题。如果容器是 UTC 时区0 0 0 * * ?会在北京时间早上 8 点触发用户看到的就是任务生成晚了 8 小时。排查手段也很直接启动日志看有没有ScheduledExecutorService相关的初始化再加一个临时测试方法每分钟打印一次时间戳确认调度本身没问题。5.2 打卡重复提交和丢失问题重复提交我们已经用 Redis SetNX DB 唯一索引挡住了。但还有一种情况用户在断网情况下提交前端走了超时重试结果第一次其实已经成功写库重试触发 Redis 占位失败用户被误判为“重复打卡”。这个问题的解法是接口做幂等校验打卡接口可以接受一个前端生成的请求唯一 ID后端先查这个 ID 是否处理过处理过就直接返回成功。打卡任务丢失的典型原因是在定时任务里直接遍历用户列表某个用户 insert 失败导致整个循环中断。必须在循环里加 try-catch单个用户失败不影响后面用户生成任务。5.3 时区、数据库连接、跨域等环境类问题这些问题虽然不烧脑但破坏力极大。MySQL 8 默认连接超时时间是 8 小时如果项目部署后有一段时间没人访问HikariCP 连接池里的连接可能已经被 MySQL 杀掉下一次请求就报 connection is not available。解决方法是 HikariCP 配置keepalive-time: 30000和max-lifetime: 1800000让连接池定期保活。跨域问题在大前端分离的项目里必现。开发环境用 Vite 的 proxy 把 /api 转发到 localhost:8080生产环境用 Nginx 反代不要在 Spring 里搞一堆 CorsFilter 允许所有来源那样既不安全也容易在部署时产生奇怪的问题。5.4 集群部署下的定时任务重复执行如果系统要部署多个后端实例做负载均衡Scheduled 的定时任务会在每个实例上都执行一次每天生成任务就会出现两份。最简单的解决办法是给任务生成逻辑加一把 Redis 分布式锁String lockKey scheduler:dailyTaskGenerate; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofMinutes(2)); if (Boolean.FALSE.equals(locked)) { log.info(另一台实例正在生成任务本次跳过); return; } try { doGenerate(); } finally { redisTemplate.delete(lockKey); }注意不要把锁的过期时间设得太短。任务生成如果超过 2 分钟锁自动释放后另一台实例会闯入所以过期时间要根据用户量评估。我这个系统里用户量不大2 分钟绰绰有余。做了这一套系统我每次跟别人讲“促学”心里都有个很明确的结论促学系统最难的从来不是某个算法而是怎么把“任务、执行、反馈、激励”这个闭环转起来让用户愿意每天都回来。技术层面的 Scheduled、Redis、MyBatis-Plus都是给这个业务闭环服务的工具而已。如果你要交课程设计这套系统可以扩展的方向也很多把随机测验升级成智能推荐题目、把前端图表换成答题热力图、加一个好友对战排行榜甚至把任务模板改成可拖拽的配置界面。我个人测试下来最有成就感的是看到用户连续打卡天数超过 30 天以后真的会形成一种“不想断签”的惯性这比任何技术上的优化都说明系统设计对了。