ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java和Spring Boot的课表日程提醒管理系统设计与实现

基于Java和Spring Boot的课表日程提醒管理系统设计与实现 你是不是也有过这种经历早上八点的课七点五十才从宿舍惊坐起来一路狂奔到教学楼结果发现这节课在实验楼连课本都带错了。作为一名Java后端开发我决定不再靠脑子记课表直接动手做了一个“基于Java的课表日程提醒管理系统”。这个项目的核心就两件事一是把杂乱无章的课程安排变成结构化数据二是在上课之前自动把提醒推到你的手机上。文章会从需求分析、技术选型、数据库设计、核心代码实现、定时任务与消息推送再到部署上线和常见问题排查完整走一遍我实际开发这个系统时的思路和踩坑记录。不管你是正在准备毕业设计的学生还是想拿一个练手项目巩固Spring Boot经验的Java初学者这套方案都可以直接参考和复现。1. 这个系统到底在解决什么问题1.1 目标用户的真实痛点先别急着谈技术做任何系统之前都要先搞清楚“用户是谁”。我最初设想的目标用户是大学生但做着做着发现这套系统完全也可以服务培训机构的学员、考研党、上班族。不同人群痛点的共性非常一致课程安排不规则有的课单周上、有的课双周上有的课连续两周调课光靠日历App手工录入会把人逼疯。更麻烦的是大学和培训机构的课程往往不是按“周一上午9点”这种固定时间来的而是按“第几周、星期几、第几节”来描述课程一旦跨校区、跨教学楼提前提醒的价值就特别大。除了个人用户排课老师和管理员也有需求。他们需要知道某一门课这个学期被安排到了哪些周次也需要看到不同教室、不同时段是否有冲突。虽然我做的这个版本没有做复杂的排课算法但至少提供了课程管理、学期周次管理、教师课程导览这些基础能力让管理员能快速查看到底哪些老师在哪个时间段有课。1.2 系统能力的边界很多人在做课表管理系统时容易犯一个错误想得太大什么都往里塞。结果排课算法、调课审核、学分统计、教务系统对接全部堆到一起最后哪个功能都没做好。我做这个系统时一开始就划定了边界。这个系统要做的核心是四件事课程信息维护、学期周次自动计算、上课前日程提醒、消息记录查询。课程信息维护负责增删改查课表数据学期周次计算负责解决“这周是教学周第几周单双周怎么判断”的问题日程提醒通过定时任务扫描即将开始的课程并按用户配置的提前时间推送消息记录则把每次推送都落库方便用户回看和追溯。系统明确不做的事包括自动排课、与学校教务系统实时同步、出勤打卡、成绩管理。这些功能不是没有价值而是它们各自都能单独做成一个系统。把边界划清楚才能在一个可控的范围内把提醒功能做到足够稳定和好用。后续如果真要对接教务系统只需要把数据导入模块替换成接口同步即可不影响整体架构。2. 技术选型Java生态里最省心的组合2.1 为什么是Spring Boot MyBatis-Plus Quartz这个项目用的是Java那技术选型基本就在Spring生态里打转。我最终选定的组合是Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Quartz Spring Mail。很多同学会疑惑为什么不直接Spring Data JPA或者干脆用MyBatis原生这里我讲一下我的选择逻辑。MyBatis-Plus在中小型管理系统里确实省事单表CRUD基本不需要写XML内置的LambdaQueryWrapper让动态条件查询写起来非常流畅。比如查“某位用户今天所有的课程”一行wrapper就能搞定这比手写SQL快得多。同时它保留了MyBatis对复杂SQL的支持以后真遇到多表关联、统计报表这类需求依然可以写自定义SQL兜底。JPA虽然也能做但它的懒加载和级联操作在处理课程、教师、学期这种多对多关联时踩坑概率更高不如MyBatis-Plus直观可控。定时任务部分我选Quartz而不是Spring自带的Scheduled原因是课程提醒这个场景对任务调度的要求其实比较特殊需要按课程动态生成任务需要在应用重启后任务不丢失最好还能支持集群部署时的任务锁。Spring自带的Scheduled只是固定周期轮询适合写“每分钟扫描一次未提醒的课程”这种简单逻辑但一旦要精确到“每周三第3节课前10分钟触发”就非常别扭。Quartz可以把任务和触发器持久化到数据库天然支持动态创建、暂停、恢复和集群环境下的调度。这也是Java面试里经常被问到的定时任务框架对比点单机小任务用Scheduled规则复杂或者要持久化调度的场景就直接上Quartz分布式场景再考虑XXL-Job。2.2 前端到底要不要前后端分离很多零基础同学一上来就想搞Vue Spring Boot前后端分离觉得这样才“现代”。但我给这个项目的建议是如果没有团队协作和独立部署要求直接用Spring Boot集成Thymeleaf模板引擎做服务端渲染前端用原生JavaScript加一个轻量级日历组件比如FullCalendar。前后端分离适合的是那种有多人协作、接口要复用到多个端Web、小程序、App的项目。而课表管理系统的用户量通常不大界面交互也没有复杂到需要虚拟DOM层面去优化。服务端渲染的好处是开发成本低、部署简单、没有跨域问题打一个jar包就能跑尤其适合课设和毕设场景。如果你已经熟练掌握了Vue那也可以把前端单独拆出来通过RESTful API通信后端设计上我仍然按接口优先的思路来做。只要Controller层返回统一结构前端怎么换都不受影响。3. 数据库设计一张课表该拆成几张表3.1 核心五张表拆解数据库是整个系统的地基设计得好不好直接决定后面代码写起来是流畅还是痛苦。我的设计是五张核心表用户表、学期表、课程表、提醒配置表、消息记录表。另外考虑到有些课要按自定义周次上我给课程表设计了一个weeks字段来存位图数据这个后面单独讲。用户表字段其实很简单id、用户名、密码、真实姓名、角色学生/管理员、邮箱、手机号、微信UnionId预留、创建时间。密码我用BCrypt加密存储不存明文这是最基本的底线。学期表用来记录学年和学期名称比如“2025-2026学年第一学期”同时存学期开始日期和结束日期。这里有一个很关键的设计点学期开始日期是周次计算的原点整个系统的“本周是第几周”全都依赖这个日期。课程表是核心中的核心字段包括课程名称、教师姓名、上课校区、教学楼、教室编号、星期几、开始节次、结束节次、周类型、自定义周次、所属学期、创建人。礼拜几用Integer存1到7分别代表周一到周日。开始节次和结束节次也是Integer代表第几节到第几节。提醒配置表存用户对某门课程的个性化提醒时间比如提前10分钟、提前30分钟。消息记录表则记录每条推送的具体内容、接收人、推送渠道、推送时间、是否已读。3.2 单双周和自定义周次怎么建模课程表设计中最大的坑是周次类型。很多系统只用“单周”和“双周”两种标记这确实应付了大多数场景但实际校园里还有“第2周到第16周每周上其中第9周停课”这种变态安排。如果只靠单双周标记这种场景根本表达不出来。我的方案是同时保留week_type和weeks两个字段。week_type是一个枚举0表示每周都上1表示单周上2表示双周上3表示按自定义周次上。当week_type为3时weeks字段存储一个Integer长度的位图比如第2周、第5周、第8周上课那weeks的二进制就是高位到低位分别对应第1周到第32周对应位为1就表示该周有课。判断某周是否有课只需要把weeks右移week-1位再与1做位与运算效率极高。用位图而不是逗号分隔的字符串是为了避免每次判断都要做字符串分割而且位图还可以非常方便地和数据库的位运算函数配合使用比如MySQL的BIT_COUNT做统计。这种设计在实际开发中帮了我大忙。当定时任务扫描课程时不需要把weeks字段拉到内存里做解析直接在SQL条件里写位运算表达式即可。即使课程数量增长到几万条依然能保持扫描效率。4. 核心代码实现从课表CRUD到自动提醒的完整链路4.1 课程数据模型和Excel导入课程表的实体类我用MyBatis-Plus的注解来做映射表名和字段名直接采用驼峰转下划线省去大量XML配置。实体类上用了TableName(course)主键用TableId(type IdType.AUTO)。这里有一个容易被忽略的小问题week_type和weeks字段如果为null在计算周次时会直接抛NPE。保险的做法是给这两个字段设置数据库默认值实体类里也初始化成0保证任何情况下都有合法值。除了手工录入课程我还实现了Excel导入功能用的阿里EasyExcel比POI原生API省心太多。导入模板包含课程名称、星期几、开始节次、结束节次、周类型等列。解析时我用了一个自定义监听器逐行读取并转换成课程对象遇到格式错误行直接记录到错误列表而不是中断整个导入。因为用户一次性导入几十上百条课程时如果第20行出了问题整批回滚体验非常糟糕。正确做法是跳过坏行导入结束后告诉用户“成功导入90条失败5条失败原因见列表”这样用户修复失败行后可以重新导入。4.2 “第几周”到底怎么算周次计算是本系统最核心的算法之一。网上很多课表系统把周次写死成“开学第N周”但开学日期每周都会变写死就意味着每年要改代码。正确做法是根据学期表的开始日期动态计算。我实现的算法是传入一个日期先拿到这个日期所在的周一然后拿学期开始日期所在的周一两个周一相减除以7再加1得到该日期在当前学期中的周次。这里有一个特别容易出错的地方就是周日和周一的关系。如果以周日作为一周的开始那么同一个周日可能会被归到上一周如果以周一开始周日的归属就完全不同。国内高校习惯周一作为一周起点所以我的所有计算都统一用LocalDate.with(DayOfWeek.MONDAY)来对齐。下面这段代码就是核心计算逻辑我放在了一个WeekCalculator组件里统一管理避免多处重复实现导致口径不一致public int getCurrentWeek(LocalDate date, LocalDate semesterStart) { if (date null || semesterStart null) { throw new IllegalArgumentException(date and semesterStart must not be null); } LocalDate monday date.with(DayOfWeek.MONDAY); LocalDate startMonday semesterStart.with(DayOfWeek.MONDAY); long daysBetween ChronoUnit.DAYS.between(startMonday, monday); int week (int) (daysBetween / 7) 1; return Math.max(week, 0); }判断某门课程在当前周是否上课则要结合课程表的周类型做分支。对于按自定义周次上课的课程就用位图判断。我在Service层写了这样一个方法public boolean isCourseActiveInWeek(Course course, int week) { if (week 0) { return false; } Integer weekType course.getWeekType(); if (weekType null || weekType 0) { return true; } if (weekType 1) { return week % 2 1; } if (weekType 2) { return week % 2 0; } if (weekType 3) { int weeks course.getWeeks(); return ((weeks (week - 1)) 1) 1; } return false; }这段代码看起来简单但实际写的时候需要注意位运算下标越界的问题。week如果大于32或者小于1weeks (week - 1)的结果就不可控了。所以调用前一定要先做范围校验我在真正调用时会先判断week 1 week 32。否则就会出现“学期第40周还有课”这种荒谬数据。4.3 生成当日时间轴和提醒时间差课表管理系统不能只会存储数据还得能把“星期三第3-4节”翻译成具体的上课时间。这个翻译过程依赖作息时间表。我把作息时间表做成了一个配置表不写死在代码里因为不同学校的上课时间差异很大有的是早上8点上课有的8点30还有的学校下午第一节从13点30开始。作息配置表的结构非常简单节次编号、上课时间、下课时间。生成时间轴的核心逻辑是根据当前日期查出当天所有课程再查出这些课程涉及的节次时间按时间排序拼出当天的日程列表。比如现在是“星期三第2节课间”那日程列表里就应该显示第3节课将在10分钟后开始地点在综合楼B区302。这个“距离上课开始还剩多少分钟”其实就是课程开始时间减去当前时间得到的差值单位换算成分钟代码如下public long getMinutesBeforeCourse(LocalDateTime courseStartTime, LocalDateTime now) { if (courseStartTime null || now null) { return -1L; } return Duration.between(now, courseStartTime).toMinutes(); }获取课程的courseStartTime需要把“星期几”和“开始节次”组合起来。我先根据当前日期算出本周一的日期然后加上dayOfWeek - 1天得到具体上课日期再从作息配置表查出该节次的上课时间组合成完整的LocalDateTime对象。这一套写下来逻辑其实很短但涉及LocalDate和LocalTime的拼接很容易踩坑一定要记得做空值校验。5. 定时任务与提醒推送系统的心脏5.1 定时任务的轮询策略提醒功能能不能准点触发取决于定时任务设计得好不好。我最初用的是Spring的Scheduled(cron 0 * * * * ?)每一分钟扫描一次数据库找出所有需要在未来10到30分钟内上课的课程然后逐条推送。这种方案最简单但它有一个问题如果提醒提前量配置得很短比如提前1分钟提醒而扫描任务刚好错过了那一分钟提醒就漏掉了。后来我调整了策略扫描任务每30秒执行一次每次扫描未来45分钟内的课程推送时判断“当前时间是否已经到达提醒时间”只有到达了才推送。这种设计本质上是把“定时精确触发”转成了“高频轮询状态判断”。虽然多消耗了一点数据库查询资源但对课表这种数据量级来说完全无所谓。懒人做法就是把提醒提前量按分钟算扫描任务每次运行查一次课程和时间配置组合再和当前时间比较。如果真的需要准点触发也不会去做秒级调度而是用Quartz按每门课生成对应的Trigger这样触发和业务解耦更彻底。5.2 坚决不做重复推送做提醒系统最尴尬的事用户提前10分钟收到一条提醒提前5分钟又收到一条上课之后还收到一条“该上课了”。这会让用户直接疯掉。防止重复推送的关键是幂等设计。我的做法是在消息记录表上建了一个唯一索引字段组合是用户ID、课程ID、周次、课程开始日期、提醒类型。每次推送前先尝试插入消息记录如果插入成功说明这是第一次推送可以执行发送如果插入时抛出唯一键冲突则说明这条消息已经推送过直接跳过。这个方案比“先查询再插入”更稳因为查询和插入之间有时间窗并发场景下依然可能重复。用唯一索引做约束数据库从底层就保证了幂等。配合事务一起使用相当于给推送系统加了一层保险。不过要注意的是唯一索引字段里千万别把精确到秒的推送时间放进去否则每次推送时间都不同唯一性约束就没意义了。我把唯一约束设计成“用户课程周次课程日期提醒类型”这样同一门课同一周同一天只有一条有效推送记录。5.3 多渠道推送的落地实现提醒消息不能只存在数据库里用户可不会每天打开系统看消息中心。我这个版本实现了三个渠道站内消息、邮件、WebSocket实时通知。站内消息最简单往消息记录表里插一条数据用户登录后有个小铃铛未读消息有红点。邮件提醒用Spring Boot的spring-boot-starter-mail配置好SMTP服务器后用MimeMessageHelper发送HTML邮件内容包含课程名称、上课时间、教室地址。WebSocket则用Spring的TextWebSocketEndpoint和STOMP协议实现用户登录后建立长连接后台扫描到即将上课的课程时直接通过SimpMessagingTemplate推送到用户前端浏览器右下角弹出通知那种即时感比邮件和站内信好太多。这三个渠道我封装了一个RemindSender接口每种渠道一个实现类再通过一个通知管理器按用户配置的渠道组合依次调用。这样做的好处是以后要对接企业微信、钉钉、Server酱这类第三方服务时只要新增一个实现类不用改动核心扫描逻辑。这算是设计模式里策略模式的一个典型应用写代码的时候要刻意给自己留口子。5.4 消息中心的查询和已读回执消息记录这一块我做了分类查询按日期范围查、按消息类型查、按已读未读查。分页用MyBatis-Plus的Page对象前端传当前页和每页条数后端返回总记录数和当前页数据。已读回执就是一条UPDATE语句把is_read改成1同时更新read_time。这里有一个体验细节已读回执不要点一条就刷一次全量列表前端可以做成点击铃铛时批量标记全部已读或者点击单条标记单条已读我用的是后者配合局部刷新用户体验会更好。6. 开发中踩过的坑和排查技巧6.1 项目启动失败排查清单每次做完功能启动时报错大家第一反应都是慌。其实Spring Boot项目的启动失败就那么几类原因按顺序排查效率最高。第一端口被占用。APPLICATION FAILED TO START提示Port 8080 was already in use八成是你上一次的进程没关干净。Windows上用netstat -ano | findstr 8080找到PID再taskkill /F /PID 进程号杀掉就行Linux上则是lsof -i:8080加kill -9。第二数据库连接失败。要么是MySQL没启动要么是密码不对要么是URL参数缺失。serverTimezoneAsia/Shanghai这个参数我必须特别提醒旧版MySQL驱动不加这个参数会直接报时区错误新版MySQL Connector/J 8.0虽然默认时区处理更好但为了保险还是建议显式加上顺便把characterEncodingutf8也写上。第三Maven依赖冲突。Spring Boot的依赖管理已经帮我们做了版本对齐但如果你额外引入了其他第三方库很容易把传递依赖给弄冲突最典型的是jackson-databind版本不一致启动时会报NoSuchMethodError。遇到这种问题用mvn dependency:tree查看依赖树把冲突的依赖用exclusion排除掉。6.2 定时任务为什么没触发定时任务没触发是我在实际运行中遇到最多的故障可能的原因有四个忘了在启动类上添加EnableScheduling注解、cron表达式写错、时区不一致、任务线程异常被吞。前两个比较容易排查看看日志有没有Initializing ExecutorService输出或者把cron表达式放在线Cron生成器里验证一遍就行。第三个时区问题比较隐蔽。如果你把Spring Boot的配置里加了timezone UTC那定时任务的执行时间会被整体偏移8小时课程提醒全都会晚到。排查时不要只看服务器本地时间要确认JVM的默认时区尤其是在Docker容器里很多基础镜像默认是UTC时区。解决办法是在启动命令里加-Duser.timezoneAsia/Shanghai或者在Dockerfile里加ENV TZAsia/Shanghai。第四个问题是最坑的Quartz或者Scheduled的任务抛了异常而异常被任务调度框架吞掉了没有任何日志输出任务后续也不再执行。我发现底层原因是一个数据库字段在特定日期parse时抛了DateTimeParseExceptionQuartz默认的JobListeners并没有打印堆栈。从那以后我给每个定时任务方法的内部都包了一层try-catch捕获所有异常后通过log.error(定时任务执行异常, jobName{}, jobName, e)记录下来StackTrace保存日志文件。经验只有一句话定时任务宁可多打日志也不要让异常静默消失。6.3 时间边界问题这个系统围绕时间运作所以时间边界问题尤其多。最典型的一个场景是周日晚上23点59分用户准备看明天周一的课但系统按照“周一作为一周开始”的规则周次已经是新一周了。如果我在服务端把“当前周次”存到Redis缓存里那就会出问题这个缓存周一凌晨更新之前用户看到的时间轴顺序错乱。我的处理办法是不缓存周次结果每次请求都现场计算。反正这个计算成本极低一次日期差除以7而已没必要为了省这点性能引入一致性问题。另外跨学期时也要处理初始化问题。学期开始前一周如果校内已经有新学期的课程安排用户还想提前看课表系统不能因为当前周次算出来是负数就报错。我在查询接口里做了保护周次小于等于0时返回一个空课表并提示用户“新学期数据尚未激活”。这比弹一个500错误友好得多。6.4 前后端时间格式不一致前后端联调时最常见的就是时间显示成“2025-09-03T08:00:00”这种带T的格式用户看得一脸懵。原因是后端序列化时用了Java默认的LocalDateTime格式前端JS的Date对象解析时间和Spring Boot默认ISO格式有偏差。解决办法是在统一配置类里注册一个Jackson2ObjectMapperBuilderCustomizer把LocalDateTime和LocalDate的序列化格式统一为yyyy-MM-dd HH:mm:ss和yyyy-MM-dd。如果是在Controller方法里用JsonFormat注解逐个字段标注也不是不行但总有漏网之鱼。相信我全局配置比逐字段配置省心十倍。6.5 数据库表字段设计的向后兼容开发过程中我改过一次课程表的字段把原来的week_odd_even单双周标记改成了week_type加weeks的组合结构。这个改动导致所有旧数据都要迁移。如果是线上正式系统这种表结构变更是大忌但项目还在开发期我直接写了SQL脚本把旧数据转换过来。我的建议是所有ALTER TABLE语句都要写成版本化的SQL脚本文件记录在db/migration目录下不要手动去数据库里执行。这样新环境只要跑一遍脚本目录就能复现完整数据库结构也方便后续接手的人理解表结构演变过程。7. 打包部署与后续扩展方向7.1 打包部署的基本流程项目开发完成后部署上线这一步很简单但也有一堆细节。先用mvn clean package -DskipTests打jar包Spring Boot内置的Tomcat会把这个工程变成一个可独立运行的jar。服务器上装好JDK8或JDK11配好JAVA_HOME环境变量然后直接nohup java -jar reminder-system.jar --spring.profiles.activeprod app.log 21 启动。生产环境的数据库配置一定不要放在application.yml里提交到Git用--spring.datasource.passwordxxx或者环境变量注入。很多人第一次部署会卡在这个地方本地能跑服务器上启动报Failed to configure a DataSource。十有八九是生产环境配置文件里的数据库密码没读进去。可以用Value加--spring.config.additional-location的方式来外置配置或者在启动前先用env | grep确认环境变量已经正确载入。部署完可以用curl http://localhost:8080/actuator/health检查服务健康状态我加了Spring Boot Actuator依赖方便随时看服务状态和线程情况。7.2 系统后续可以怎么扩展这套系统做完之后我给它规划了四个后续方向。第一个是移动端适配把前端页面改造成响应式布局或者用UniApp套一个H5壳让用户在手机上也能方便地看课表。第二个是增加微信小程序端小程序里的订阅消息非常适合做课程提醒比发邮件和WebSocket推送更贴近学生用户群体。第三个是增加课程评价和笔记功能让用户在每门课下面记录作业截止时间、考试安排这些信息。第四个是做一个基于课表空闲时间分析的小工具自动找出用户一周里的空闲时段这个功能对社团活动、自习规划很有用。都是基于现有数据模型的小步扩展不会伤筋动骨。最后再说几句实在话这整套系统做完我最想表达的一点是做课表日程提醒管理系统真正的难点从来不在增删改查而在时间计算、任务调度和消息幂等这三个细节上。时间口径不一致用户就会看到错误的上课提醒定时任务异常吞掉提醒就会莫名消失重复推送更是会让用户直接卸载你的应用。如果你正在写类似的Java项目我建议你把70%的调试时间花在这些“看不见的地方”而不是纠结页面按钮好不好看。日志要保留足够长的时间每次定时任务执行前有日志、推送成功有日志、推送失败有日志排查问题时才能快速定位。做一个提醒系统最大的成就感不是代码写了多少行而是用户在课堂上收到提醒时能踏踏实实坐进教室翻开课本。
返回列表