
上个月我接了个挺典型的题目给中小学做一套课后服务管理系统。技术要求写得清清楚楚——Java 打底Spring Boot 做主干再把 SSMSpring SpringMVC MyBatis串进来用最后还要配好源码、文档、调试说明和讲解材料。乍一看是个“老八股”组合但真正动手之后才发现课后托管、选课报名、考勤签到、费用结算、家长通知这些业务叠在一起坑一点都不少。这篇文章把我从需求拆解到上线调试的完整过程理一遍哪些模块怎么做、哪些参数怎么定、哪些坑必须绕开都会写到。如果你正准备做同类型系统或者在学习 Spring Boot SSM 的整合方式这篇应该能帮你省下不少时间。1. 业务先行课后服务管理系统到底在解决什么问题1.1 课后托管场景的核心痛点别急着写代码先看这个系统服务的对象是谁。中小学生课后服务现在已经是很多学校的刚性需求下午放学之后还有一段“真空时间”学校要组织托管、兴趣班、作业辅导家长要缴费、要了解孩子在哪、在做什么。这套系统本质上是把“手工登记表”换成一套线上流程。我在实际接触这类项目时听到最多的抱怨就三件事第一报名靠 Excel 和微信群接龙主办老师统计到半夜第二考勤靠点名孩子到了没有、中途走了没有没人说得清第三费用结算是月底拿着表格手工算科目多、单价不一样、请假退费规则还分好几种经常对不上账。所以系统里最核心的模块不是花哨的界面而是这几条业务闭环服务项目发布 → 学生报名 → 考勤记录 → 费用结算 → 家长通知。1.2 角色权限与业务闭环设计我设计的角色划分是四类系统管理员学校教务、授课教师、学生家长、学生。实际开发中“学生”往往不单独登录而是合并到“家长”维度里家长账号可以绑定多个孩子这一点一开始就要想清楚不然后面表结构会很别扭。角色和业务动作的对应关系我列成了表格方便后面做菜单权限时直接落地角色核心操作数据范围系统管理员维护学期、服务项目、教师、班级审核报名查看全校营收与考勤报表全校授课教师查看自己名下的课程表、学员名单发起考勤登记课时完成情况本人课程家长查看服务项目、在线报名/退选查看考勤与费用账单在线确认支付本人孩子学生大多并入家长端少数系统会做独立小程序/App 端查看课表与考勤本人把这条闭环理清楚后数据库表和接口几乎是被业务推着走的不太会出现“先建一个用户表再说”的迷茫状态。2. 技术选型Spring Boot 和 SSM 到底怎么共存2.1 为什么会出现 Spring Boot SSM 这种组合有同学问过我Spring Boot 自己就够用了为什么还要往里面塞 SSM这套路其实在高校实训和毕业设计里非常常见因为两个东西并不冲突。SSM 是指 Spring SpringMVC MyBatis 三个框架的整合方式而 Spring Boot 是一个能简化 Spring 配置的快速开发框架。Spring Boot 自动配置了 Spring MVC 和 MyBatis 的运行环境所以正确说法是项目底层使用的是 SSM 技术栈Spring Boot 负责把它们的配置过程简化掉。我推荐的结构是Spring Boot 2.x 版本 MyBatis 3.x MyBatis Spring Boot Starter在控制层用 SpringMVC 注解Controller / RestController、RequestMapping在数据层用 MyBatis 的 Mapper 接口机制。这样既能享受 Spring Boot 的自动配置又能保留 SSM 那套清晰的 Model 层 — Service 层 — Controller 层分层结构。Spring Boot 3.x 改成了 jakarta 命名空间兼容性和老教材差异大我做的这个项目直接选 2.7 系列稳定、教程多、坑少。2.2 工程分层与数据表设计经验我习惯的包结构是controller、service、mapper、entity、dto、common再加一个 config。entity 对应数据库表dto 负责和前端交互controller 不直接拿 entity 返回避免把密码、内部字段泄漏出去。这个习惯在多人协作或答辩时是加分项因为面试官一看就知道你分得清领域模型和传输模型的区别。数据库方面我设计的主要表有这些sys_user登录账号区分角色类型的字段 rolestudent 与 parent学生信息、家长账号绑定关系course服务项目课程名称、上课时间、地点、年级限制、人数上限、费用方式sign_up_record报名记录学生、课程、学期、状态attendance_record考勤记录日期、到课状态、签到教师fee_bill费用账单账单编号、金额、状态、支付时间notice / message公告与消息推送记录这里有一个特别值得说的细节课程的时间段字段如果表设计成“周一 16:30-17:30”后面做“判断学生是否同时报了冲突课程”的逻辑会很痛苦。我最后是把课程时间拆成了 week_day、start_time、end_time 三个字段再用程序去查并发冲突。如果你图省事只存一个字符串后期校验会非常痛苦。3. 核心功能模块的实现拆解3.1 在线报名与选课冲突校验报名模块是整个系统里最容易出并发 bug 的地方因为家长会在同一时间段扎堆提交。学校常见的规则是每门课有人数上限、可报名年级段限制、同一学生不能在同一时间段报两门课。我的接口逻辑是这样处理的先查询当前课程状态与已报名人数再校验学生已有报名的时段是否重叠如果通过就执行插入。听起来不难但多个请求同时进来时两个事务可能同时读到“还有名额”同时插入成功导致超员。解决办法是双保险数据库层面给course_id, student_id加唯一索引防止同一学生同一课程重复报名业务层面用 select ... for update 锁住课程记录或者用版本号乐观锁来控制人数扣减。代码核心可以简化成下面这种思路Transactional public boolean signUp(Long courseId, Long studentId) { // 1. 锁定课程记录避免并发超报 Course course courseMapper.selectForUpdate(courseId); if (course.getEnrolledCount() course.getMaxCount()) { throw new BusinessException(该课程名额已满); } // 2. 校验学生已有报名的时间冲突查 sign_up_record join course ListCourse signedCourses signUpMapper.selectCoursesByStudent(studentId); boolean conflict signedCourses.stream() .anyMatch(c - timeConflict(c, course)); if (conflict) { throw new BusinessException(你已报名同时段的其他课程); } // 3. 增加已报名人数并插入记录 courseMapper.increaseCount(courseId); signUpMapper.insert(new SignUpRecord(courseId, studentId)); return true; }实际项目里我还会加上报名状态机待审核 / 已通过 / 已拒绝 / 已退选。不是所有学校都允许“即报即得”有些课程需要教务老师人工审核所以状态字段不能省。3.2 考勤签到时间窗口、课程校验与状态回写考勤模块看起来简单做起来比想象中繁琐。教师端发起签到后系统不是无脑记录“已到”而是要走一套简单的校验签到课程必须属于当前教师、签到时间必须在课程开始前后设定的时间窗口内、同一个学生当天只能有一条考勤记录。我采用的逻辑是课程开始前 30 分钟到课程结束后 30 分钟为签到开放窗口超出窗口教师只能标记“补签”系统记录补签操作人。每个学生的考勤状态我用字典值来记录0 未签到、1 正常、2 迟到、3 缺勤、4 请假。请假申请是家长在移动端提前发起的审核通过后教师端会看到该学生在请假名单里点名时会自动跳过。这里要补充一个容易被忽略的点考勤表不能只存“状态”和“学生ID”必须加上课程实例概念。因为同一门课每周上两次如果考勤只关联课程不关联具体上课日期统计出勤率时就只能靠时间去猜数据很容易乱。我单独建了 course_session 表课程的具体上课记录每次上课生成一条 session考勤记录挂在 session 下面查询和统计都干净很多。3.3 费用结算计费规则与账单生成费用这模块为什么单独拿出来说因为学校计费规则五花八门。有的按次扣费一节课 20 元上几次扣几次有的按月固定收费请假不退有的按学期整包收费中间退课按剩余课时比例退费。我的做法是抽象出一个计费策略字段charge_type1 按次 / 2 按月 / 3 按学期。报名时生成对应的账单初始记录每次考勤结束后如果是“按次计费”就产生一笔费用明细到月底或学期末系统批量汇总生成 fee_bill。退费规则用配置表维护比如“开课两次内可全额退费”“超过两次只退 50%”避免把规则写死在业务代码里每次改规则都重新发布。账单编号我建议大家自己生成而不是用自增 ID因为要打印给家长格式类似202506 开头 日期 随机码。数据库里用唯一索引约束账单编号再配合定时任务去处理付款超时未支付的账单设置为“已取消”。整个费用模块在答辩演示时很出效果因为能清楚看到报名、考勤、费用三者是怎么联动起来的。3.4 家长通知与消息模块发通知是这个系统里家长感知最强的功能。新课程上线、报名审核结果、考勤异常、费用账单生成这四类事件都应该触发消息推送。一开始我想直接接第三方推送但考虑到大多数演示环境没有商户号我设计了一个消息接口层先推送站内信和短信模板占位接口未来扩展微信小程序推送时只需要新增实现类不需要改动业务代码。消息模块的核心表很简单message_record接收人、标题、内容、类型、状态、发送时间。业务代码只需要调用一个统一 sending 方法内部异步写入数据库并调用渠道接口。这个抽象在项目文档里写出来评委和面试官都认可因为体现的是可扩展的设计思维。4. 实操环节从项目搭建到核心配置一步步跑起来4.1 项目初始化的两种方式我初始化 Spring Boot 项目时如果手头网络方便直接用官网的 Initializr 生成如果环境受限就手动建一个 Maven 工程把下面几个关键依赖塞进 pom.xmldependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意 mybatis-spring-boot-starter 的版本要和 Spring Boot 版本兼容这一条我栽过跟头Spring Boot 2.6 配 MyBatis Starter 2.1.x 会出现循环依赖报错统一升到 2.3.x 之后一切正常。另外如果要用分页插件加 com.github.pagehelper:pagehelper-spring-boot-starter 就行底层用的也是 MyBatis 拦截器机制。4.2 application.yml 里最容易忽略的配置Spring Boot 的配置看着简单真跑起来问题一堆。我这份配置是实打实调试通过的server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/after_school?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.afterschool.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl三个重点说明一下。第一url 里的 serverTimezone 必须加MySQL 8.x 不加会直接时间报错allowPublicKeyRetrieval 如果不加MySQL 8 的 caching_sha2_password 认证方式在部分连接工具里会连不上。第二map-underscore-to-camel-case 必须开我数据库字段习惯用下划线命名Java 属性用驼峰不开这个映射查出来的对象全是 null。第三jackson.date-format 只对 java.util.Date 生效如果实体里用了 LocalDateTime就要额外加 JsonFormat 或者配置 JavaTimeModule这是一个非常隐蔽的坑我后面章节会细说。4.3 登录态与权限控制的落地方式这类管理系统一般不建议裸用 Session因为现在前端大概率是独立部署的后端只用 API。我用的是 JWT SpringBoot 拦截器的方案登录成功生成 token客户端后续请求把它放在请求头 Authorization 里拦截器解析 token 后把用户信息写入 ThreadLocal业务代码通过静态方法获取当前用户。只做登录校验还不够还要做角色鉴权。我的做法是在 PathVariable 或请求体的用户信息上判断比如管理员才能调用课程添加接口教师只能修改自己名下的考勤记录。如果你不想重复写 if可以自定义一个 RequireRole 注解配合 Spring AOP 来做但我觉得在小系统里把代码写清楚比过度设计重要所以只做了一层简单的 BaseController 校验方法能复用的场景直接用。5. 常见问题与排查技巧实录5.1 SSM 整合中三个高频启动报错第一个是 “Invalid bound statement (not found)”启动后调用 Mapper 接口就报这个。原因基本都是 MyBatis 扫描不到 XML 文件。检查 mapper-locations 路径是否和实际 XML 目录一致另一个办法是检查接口方法和 XML 里的 namespace、id 是否一一对应。最基础但最有效的排查方式是先打开 MyBatis 的控制台日志看执行的 SQL 到底拼成了什么。第二个是依赖冲突导致的 “ClassNotFoundException”。Spring Boot 项目同时引入 spring-boot-starter-web 和旧版 spring-webmvc jar 时偶尔会炸用 mvn dependency:tree 检查重复依赖把不该出现的排除掉。这个问题在多人协作项目里很常见因为同事习惯直接把整个仓库的 pom 片段粘过来。第三个是事务注解不生效。加了 Transactional 却不回滚先确认方法是不是 public再确认有没有被同类内部方法调用——Spring 的代理机制管不了同类方法自调用。我当时在退选方法里调用了同类的一个私有扣减方法报错时数据库数据已经变了后来把相关逻辑拆分重构成独立方法再把 Transactional 放到 Controller 调用的那层问题立刻消失。5.2 JSON 日期格式对不上前端日历我在 4.2 提到过 LocalDateTime 的坑这里展开说。实体里如果用 LocalDateTimeJackson 默认序列化出来是一种带 T 的 ISO 格式比如 2025-06-12T16:30:00前端控件解析经常出问题。最直接的办法是在实体字段上加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime startTime;如果你用的是 Spring Boot 2.6还要注意 spring.jackson.date-format 对 LocalDateTime 不生效别浪费时间去全局配置老老实实用 JsonFormat或者注册一个 Jackson 自定义序列化器批量处理。5.3 跨域问题与前端的对接口径后端的端口是 8080前端页面在 5173 或 8081 时浏览器会拦截跨域请求。解决方式两种后端在 Config 类里加 CORS 映射或者用 Nginx 做同源代理。我比较推荐后者因为上线时后端本来就需要 Nginx 反代开发环境里直接在 Spring Boot 里加个 CORS 配置反而更快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否则浏览器还是会报错。另外一个经常被忽略的点是如果加了拦截器拦截器里不能拦截 OPTIONS 预检请求不然前端还是会看到跨域错误。5.4 部署上线时的资源与内存问题部署我一般用两种方式。第一后端打成 jar 包直接扔服务器跑命令是nohup java -jar after-school-system.jar --spring.profiles.activeprod app.log 21 第二用 Docker 封装可以直接通过端口映射来暴露服务。但我在 Docker 容器里遇到过一个麻烦镜像里缺少时区配置导致时间和本机差 8 小时。用 Dockerfile 时记得加 ENV TZAsia/Shanghai。云服务器内存如果只有 2GJVM 参数建议加上 -Xms512m -Xmx512m避免把整台机器压垮。6. 交付文档与调试文档的组织方式6.1 文档里一定要写清楚的六块内容这套项目的交付物里除了源码最值钱的就是 LW 文档和调试文档。很多人在写文档时容易套模板但评审真正看重的是你能不能把业务逻辑和技术方案讲明白。我归纳下来一份合格的项目文档至少要覆盖这六块需求分析明确角色、业务流程、用例图最好每个用例配一个业务表格。可行性分析与功能清单区分核心功能、扩展功能不要把所有按钮都列为功能模块。数据库设计包含 ER 图、数据字典、关键字段的说明比如考勤状态码、计费类型码。关键模块设计说明挑报名、考勤、费用三个最核心的模块画业务时序图和接口清单。系统测试写清楚功能测试用例和边界测试比如“课程人数已满时报名会怎样”“同一时段重复报名会怎样”。部署与运维说明环境要求、启动步骤、配置修改项、常见报错对照表。调试文档是很多人忽略的部分。我习惯在调试文档里记录三样东西环境差异说明本地数据库版本、JDK 版本、服务器区别、典型启动错误与解决办法、接口调试的 Postman 集合转储文件或 curl 命令示例。这对应了题目里“调试文档”这部分的真正价值——拿到你项目的人不用问你就知道怎么跑起来。6.2 系统讲解与答辩的核心策略讲解系统时不需要把每个接口背一遍。我建议抓两条主线一条是业务流从管理员发布课程开始到家长报名、教师考勤、生成账单、推送通知收尾把整个闭环串起来另一条是技术流重点讲项目中遇到的真实问题和你怎么解决的比如并发报名超员、日期格式、事务不回滚。能让听讲的人记住的从来不是你用了多少技术而是你“踩坑之后如何解决”的过程。讲解时我还有一个小技巧先演示核心场景再打开关键代码讲解为什么这么写。比如打开报名接口直接说明“这里为什么需要事务”“为什么加了数据库唯一索引”比对着 PPT 念概念要扎实得多。6.3 源码组织与交接规范最后提醒一下关于源码交接的问题。给项目命名不要用乱码一样的 test123直接定义 clear 的工程名比如 after-school-management。README 里写清楚 JDK 版本、MySQL 版本、前端框架、启动步骤最好附带一个简单的数据库初始化 SQL。而且我会把初始化数据做成统一的 SQL 文件用管理员账号、测试教师账号、两位测试学生数据先跑通这样接手的人一启动就能看到效果不用从零去录数据。整个项目做完我对这套 Spring Boot SSM 组合的最深体会是框架只是骨架真正决定项目质量的是对业务细节的表单设计、状态管理、异常分支和数据一致性。尤其是课后服务这种业务规则琐碎的场景把“报名并发”“考勤时间窗口”“计费策略”这类小逻辑想清楚系统的水平就上去了。如果你也在做同类的管理系统希望这篇能让你少走几圈弯路尤其是第五章那些报错排查一次能省下大半天的时间。