简介:基于SSM框架的兴趣班和延时班管理系统源码与数据库,专为计算机相关专业课程设计、期末大作业和毕业设计准备,适合需要完成Java Web项目实战练习的在校学生,也适合以SSM整合开发为课题的学习者参考。系统已通过导师指导并调试运行,压缩包整体约101.35MB,包含项目源码、数据库脚本、软件工具、项目说明、演示视频等,结构清晰,可直接导入IDE运行,也可作为毕设底稿进行二次开发。核心功能围绕校园兴趣班和延时班管理展开:管理员负责权限分配、班级分类、教师与学生账号管理,并支持数据下载;教师可安排开展时间、发布课程安排、上传文字图片视频、管理班级学生和评论;学生能自主加入班级、查看教师发布内容并参与互动。已有129人学习下载。对于希望快速上手SSM整合开发、理解多角色权限设计和班级业务流的人来说,这份项目从数据库到界面、从后台到演示均有完整覆盖,是课程设计或毕业答辩的实用参考。
1. 延时班名单还在手工核对?这就是那套 SSM 系统想解决的问题
培训机构排课最头疼的往往不是周末兴趣班,而是周一到周五放学后的延时班。一张报名表对应七八个兴趣班,延时班又按天、按周重复计费,课时记录叠在一起,月底统计经常要拿 Excel 来回比对。基于 SSM 框架的兴趣班和延时班管理系统,就是把这类业务的报名、排课、考勤、计费数据结构化:Spring 管对象和事务,SpringMVC 管接口和页面跳转,MyBatis 管数据库读写,再加上一份设计好的 MySQL 库表。对做毕业设计、接培训项目外包或给机构做内部系统的开发者来说,这套源码的价值不在页面多好看,而在业务表怎么拆、考勤和课时怎么联动。下面会从业务模型、建表、框架整合到核心接口实现,把这条链路完整推演一遍。
2. 先理清兴趣班和延时班的业务差异,再谈建表和代码
2.1 两类业务在数据模型上的核心差异
兴趣班通常是「按学期开课、每周固定课时」的课程包,比如周六上午 9 点到 10 点的绘画班,一个学期 16 节课。学生报名后,考勤粒度是「某人某周上没上课」,费用在报名时一次性结算。延时班则完全不同,它的业务单位是「天」:周一到周五放学后,家长按天或按月购买看护时段,每天的到班情况都要记录,月底按实际到班天数核算。这意味着课程表和考勤表必须能兼容两种频率,而不是为兴趣班单独建一套、为延时班再建另一套。
一个可行的建模方式是课程表用 type 字段区分:1 表示兴趣班,2 表示延时班。兴趣班在课程班次表里写每周星期几、开始时间、结束时间、总课时数;延时班在班级表里直接配置生效日期范围,不需要拆成一条条的班次记录。这样查询时可以用同一张表做基础筛选,再按 type 走不同的子逻辑。
2.2 从报名到结算的主数据流
学生报名后产生一条报名记录,报名状态分为待支付、已支付、已退班。兴趣班支付成功后生成课时账户,比如 16 节课,每考勤一次扣 1;延时班则按月份生成应出勤天数,每天考勤成功后写一条明细,月底汇总。数据流转顺序是:课程发布 → 学生选课报名 → 支付回调更新订单状态 → 教务分班 → 教师点名/打卡 → 课时扣减或考勤统计 → 生成报表与费用结算。
这套流程里最容易被忽略的是「退班」和「换课」。兴趣班开课后学生退班,剩余课时要折算金额退回;延时班月中不再来,已经产生但尚未结算的考勤明细不能直接删除,要保留并标记为退费状态。所以报名表和考勤表里都要预留 status 或 refund_status 字段,不能只靠 delete 记录解决。
2.3 开班、停班和调课的状态机设计
班级状态建议设计为:未开始、进行中、已结课、已停班。兴趣班支持调课,一次调课会改班次表里的星期和时段;延时班遇到机构临时停托,需要在考勤生成时跳过指定日期。调课和停班都不能破坏已有考勤数据,否则历史统计会失真。
我的做法是给班级状态加一个 changed_from 字段,记录调课前的原始班次索引;停班单独建一张 holiday 表,按日期范围排除考勤计划。读取考勤时先查班级状态,再关联 holiday 表判断某天是否有效。这样做的代价是查询多一次关联,但换来了业务上的可追溯性,对培训机构财务对账很重要。
3. 数据库设计比框架本身更值得细读:核心表结构与字段取舍
3.1 课程、班级、学生三张基础表的建表 SQL
拿到项目源码后我一般先看数据库脚本,因为 SSM 项目的业务逻辑全部围绕表结构展开。基础表建议按下面的结构设计:
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32) NOT NULL UNIQUE COMMENT '学号', student_name VARCHAR(64) NOT NULL, parent_phone VARCHAR(20), enroll_date DATE, status TINYINT DEFAULT 1 COMMENT '1在读 0离校' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(128) NOT NULL, course_type TINYINT NOT NULL COMMENT '1兴趣班 2延时班', tuition DECIMAL(10,2) NOT NULL, total_hours INT DEFAULT 0 COMMENT '兴趣班总课时', remark VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE class_info ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, class_name VARCHAR(128) NOT NULL, teacher_id INT NOT NULL, max_students INT DEFAULT 20, start_date DATE, end_date DATE, week_day TINYINT COMMENT '兴趣班: 1-7表示周一至周日', begin_time TIME, end_time TIME, status TINYINT DEFAULT 0, CONSTRAINT fk_class_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;student 表用 student_no 做业务唯一键而不是直接依赖自增主键,是为了让导入 Excel 学生名单时可以按学号做幂等核对。course 表用 course_type 把两类课程放在同一张表, tuition 用 DECIMAL 而不是 FLOAT,避免金额浮点误差。class_info 表里 weekday、begin_time、end_time 是兴趣班的排课维度,而延时班只使用 start_date 和 end_date,按天展开。
延时班的日排课不推荐为每一天生成一条记录,那样数据量会很大:一个班 30 个学生、一学期 100 天就是 3000 条排课记录,查询反而变慢。正确做法是让考勤表在写入时按日期补齐,排课信息仅作为校验条件。
3.2 考勤表与课时流水表的设计
兴趣班和延时班共用一张考勤表但语义不同。兴趣班关注「这节课来没来」,延时班关注「这天在不在托」。
CREATE TABLE attendance ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, class_id INT NOT NULL, course_type TINYINT NOT NULL, att_date DATE NOT NULL, att_time VARCHAR(16) COMMENT '兴趣班时段,如 09:00-10:00', is_present TINYINT DEFAULT 1 COMMENT '1出勤 0缺勤', operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_class_date (student_id, class_id, att_date, att_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;考勤表有唯一键 uk_stu_class_date,作用比索引更重:它防止同一天对同一个学生重复点名。延时班一天可能只点一次,att_time 传空字符串;兴趣班一天可能有两节不同课,用 att_time 区分。
与此配套的是课时流水表,兴趣班每次考勤后插入一条扣减流水,延时班则单独在月底生成费用汇总。扣减操作必须与考勤写入在同一个事务里,否则会出现「考勤记了但课时没扣」的不一致状态。
3.3 索引、冗余与金额字段的常见误区
很多初改这套项目的人会把所有查询字段都加索引,结果写多读少的考勤表在批量插入时被索引拖慢。我的做法是:考勤表只保留唯一键和 class_id 的单列索引,att_date 不进索引。查询某天全校出勤时,过滤条件落在 class_id 上,配合 MySQL 的 range 扫描足够快。报表统计走离线汇总表,不在业务表上加复杂联合索引。
金额字段只用 DECIMAL(10,2),对外展示时保留两位,内部运算用分做整数。项目里如果出现 FLOAT 或 DOUBLE 类型的 price 字段,建议直接改掉:一次 0.1 + 0.2 的精度误差在费用计算里就会造成对不上账。可以写个 SQL 检查:
SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'your_db' AND DATA_TYPE IN ('float','double');这个查询很实用,拿到源码后第一件事就做这个检查,把浮点字段统一替换成 DECIMAL。数据量不大时用 UPDATE 语句直接转即可,量大的话要新建字段再迁移。
4. 手写 SSM 整合骨架:Spring、SpringMVC、MyBatis 的配合方式
4.1 pom.xml 依赖版本搭配是源码能否跑起来的第一道关卡
很多人在拿到源码后卡在启动阶段,因为 pom.xml 里依赖版本不一致:Spring 4 的配置拿到 Spring 5 下会报错,javax.servlet 和 jakarta.servlet 混用会直接启动失败。一个稳定的组合是 Spring 5.3.x + SpringMVC 5.3.x + MyBatis 3.5.x + mybatis-spring 2.0.x,配 Tomcat 8.5 或 9.0。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency>mybatis-spring 的版本不能乱升级,2.0.x 对应 Spring 5,1.3.x 才对应 Spring 4。如果项目显示 SqlSessionFactory 创建失败,优先检查 mybatis-spring 和 spring-jdbc 版本是否配对。数据库驱动用 mysql-connector-java 8.0.x,对应 MySQL 5.7 和 8.0 都兼容。
4.2 Spring 与 MyBatis 配置的常见写法
下面是一份能跑通的最小配置,核心是让 Spring 管理数据源和事务,让 MyBatis 的 mapper 接口被扫描到:
<!-- spring-db.xml --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/interest_class_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mapper"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>dataSource 用的是 DriverManagerDataSource,这个类只适合开发环境;部署到正式环境时换用 druid 连接池,url、username、password 抽到 jdbc.properties 里。mapperLocations 指向 classpath 下的 XML 文件路径,如果源码里把 Mapper 接口和 XML 分开放,注意 target/classes 里是否真的把 XML 打进去了,Maven 默认只打包 java 目录下的 .java 文件,XML 放在 src/main/resources 里才行。
4.3 Controller-Service-Mapper 的完整调用链
一个查询班级列表的接口会贯穿三层,业务写在 Service,SQL 写在 Mapper XML:
@RestController @RequestMapping("/api/class") public class ClassController { @Autowired private ClassService classService; @GetMapping("/list") public Result list(@RequestParam(required = false) Integer courseType) { List<ClassInfoVO> list = classService.listClasses(courseType); return Result.success(list); } }@Service public class ClassServiceImpl implements ClassService { @Autowired private ClassInfoMapper classInfoMapper; @Override public List<ClassInfoVO> listClasses(Integer courseType) { return classInfoMapper.selectByType(courseType); } }<select id="selectByType" resultType="com.example.vo.ClassInfoVO"> SELECT c.id, c.class_name, c.max_students, c.begin_time, co.course_name FROM class_info c LEFT JOIN course co ON c.course_id = co.id <where> <if test="courseType != null"> AND co.course_type = #{courseType} </if> </where> ORDER BY c.start_date DESC </select>Controller 里用 @RequestParam 接收可空参数,Service 层不做多余处理直接透传给 Mapper。MyBatis 的 标签自动处理第一个条件前面的 AND,这是最常用的动态 SQL 写法。#{courseType} 使用预编译占位符,能防止 SQL 注入;如果手滑写成 ${courseType},参数会直接拼进 SQL,这就是常见的注入点。
5. 三块核心业务代码:报名、延时班考勤、统计报表
5.1 报名接口的容量控制与幂等处理
报名是这类系统里最容易出错的口子。学生重复提交、班级人数超限、兴趣班课时和延时班费用混在一起,都需要在 Service 层挡住。核心逻辑是先查班级当前人数,再检查该学生是否已报名,最后在事务里插入报名记录。
@Service public class EnrollServiceImpl implements EnrollService { @Autowired private EnrollmentMapper enrollMapper; @Autowired private ClassInfoMapper classInfoMapper; @Override @Transactional(rollbackFor = Exception.class) public void enroll(EnrollRequest req) { ClassInfo cls = classInfoMapper.selectById(req.getClassId()); if (cls == null || cls.getStatus() != 0) { throw new BizException("班级不存在或不在报名期"); } Integer count = enrollMapper.countByClassId(req.getClassId()); if (count >= cls.getMaxStudents()) { throw new BizException("班级人数已满"); } Integer existed = enrollMapper.countByStudentAndClass(req.getStudentId(), req.getClassId()); if (existed > 0) { throw new BizException("该学生已报名此班级"); } Enrollment record = new Enrollment(); record.setStudentId(req.getStudentId()); record.setClassId(req.getClassId()); record.setStatus(0); enrollMapper.insert(record); } }@Transactional(rollbackFor = Exception.class) 必须写明 rollbackFor,因为 Spring 默认只在抛出 RuntimeException 时回滚,而自定义的 BizException 如果继承 Exception,默认不会触发回滚。这里先做人数校验再做重复校验,因为人数校验更可能失败,先走代价更小的查询。
如果用 select count 判断人数有并发风险,两个请求同时读到 19 人、同时插入第 20、21 条数据。正式做法是对 class_info 表加 version 字段做乐观锁,或者给 enrollment 表加 (student_id, class_id) 唯一键再捕获 DuplicateKeyException。
5.2 延时班考勤的批量写入与防重
延时班考勤是按班批量生成的:老师选中一个班、选一个日期,然后勾选到班学生。批量插入如果循环单条 insert,性能差且事务碎片化。常见做法是用 MyBatis 的 foreach 批量执行:
public void batchAttendance(List<Attendance> list) { if (list == null || list.isEmpty()) return; int batchSize = 200; for (int i = 0; i < list.size(); i += batchSize) { List<Attendance> sub = list.subList(i, Math.min(i + batchSize, list.size())); attendanceMapper.batchInsert(sub); } }<insert id="batchInsert" parameterType="list"> INSERT INTO attendance (student_id, class_id, course_type, att_date, is_present, operator_id) VALUES <foreach collection="list" item="item" separator=","> (#{item.studentId}, #{item.classId}, #{item.courseType}, #{item.attDate}, #{item.isPresent}, #{item.operatorId}) </foreach> ON DUPLICATE KEY UPDATE is_present = VALUES(is_present) </insert>foreach 的 separator 设置为逗号,MyBatis 会拼成一条多 VALUES 的插入语句。配合之前建表时的唯一键 uk_stu_class_date,ON DUPLICATE KEY UPDATE 让重复点名变成更新操作,而不是报错。batchSize 控制在 200 左右比较稳妥,MySQL 对单条 SQL 的长度和参数个数有限制,超过 1000 条容易触达 max_allowed_packet 上限。
这里要注意一个隐患:ON DUPLICATE KEY UPDATE 会把「二次提交」变成「修改出勤状态」,如果业务上需要保留第一次点名结果、拒绝重复操作,就不能用这个语法,而是先 select 判断再 insert。这个取舍要看项目需求,延时班场景下老师可能需要更正点错的状态,所以允许覆盖更合理。
5.3 兴趣班剩余课时与延时班月度结算的统计 SQL
报表是这类系统的价值出口。兴趣班要看每个学生的剩余课时,延时班要按学生和月份统计出勤天数。
SELECT s.student_name, c.course_name, (c.total_hours - IFNULL(a.used_hours, 0)) AS remain_hours FROM student s JOIN enrollment e ON s.id = e.student_id JOIN course c ON e.course_id = c.id LEFT JOIN ( SELECT student_id, COUNT(*) AS used_hours FROM attendance WHERE course_type = 1 AND is_present = 1 GROUP BY student_id ) a ON s.id = a.student_id WHERE e.status = 1;SELECT student_id, DATE_FORMAT(att_date, '%Y-%m') AS month, SUM(is_present) AS present_days FROM attendance WHERE course_type = 2 AND att_date BETWEEN #{startDate} AND #{endDate} GROUP BY student_id, DATE_FORMAT(att_date, '%Y-%m') ORDER BY month DESC;第一个查询用 LEFT JOIN 子查询统计考勤次数,关键在 IFNULL 处理从未考勤的学生,避免剩余课时显示为 NULL。第二个查询按 DATE_FORMAT 截取月份分组,出勤天数用 SUM(is_present) 而不是 COUNT(),因为缺勤记录 is_present 为 0,COUNT() 会把缺勤也数进去。
这类统计如果数据量大,group by 会拖慢报表速度。项目里常见做法是建一张 attendance_monthly 汇总表,每天晚上用定时任务重算,报表只查汇总表。
6. 本地跑通这套系统的三个验证技巧
6.1 用最小数据量做全链路冒烟测试
导入源码和数据库后,不要直接点「启动」,先按下面的步骤验证环境是否正常:
| 验证对象 | 操作 | 预期结果 |
|---|---|---|
| 数据库连接 | 用 Navicat 执行SELECT 1 | 返回 1 |
| 建表脚本 | 运行项目的 .sql 文件 | 无报错,能看到 10 张左右核心表 |
| Maven 依赖 | 执行mvn clean package -DskipTests | BUILD SUCCESS |
| 登录接口 | POST /login 传 admin/123456 | 返回 token 或成功标识 |
| 报名链路 | 创建一个兴趣班,选一名学生报名 | 报名记录出现在 enrollment 表 |
这五步通过后再去点页面。很多人先启动项目再连数据库,报错后分不清是端口占用、依赖缺失还是数据库连不上,这个顺序能快速把问题隔离。
6.2 配置文件里最容易忽略的四个参数
这类项目跑不起来的案例我见过不少,集中在几个配置点上。第一个是 MySQL 连接串里的 serverTimezone,不设置会报时区异常;第二个是 Tomcat 的 JDK 版本,Spring 5 要求 JDK 8 以上,低版本启动直接报 UnsupportedClassVersionError;第三个是 druid 或 c3p0 连接池的 initialSize 配置太大,本机 MySQL 连接数不够时启动慢;第四个是本机 MySQL 认证插件问题,MySQL 8 默认 caching_sha2_password,老驱动连不上,要么换 mysql-connector-java 8.0 驱动,要么把用户改为 mysql_native_password。
检查顺序建议是:先看 pom.xml 里 spring-web 能否解析,再看 target/classes 里有没有 mapper 的 XML,最后看控制台的完整异常栈而不是只看第一行。
6.3 从源码里快速定位业务入口的技巧
拿到一份不熟悉的 SSM 源码,不要从 pom.xml 开始逐行读,先看 controller 包下的类名。Controller 的文件名和 @RequestMapping 路径能直接告诉你系统有哪些功能模块。比如 ClassController 对应班级模块、AttendanceController 对应考勤模块。找到入口后按 Controller → Service 接口 → ServiceImpl → Mapper 接口 → XML 的顺序往下追,一条链路读通后再看下一条,比按包名全读效率高很多。
最后一个小技巧:搜索所有 @Transactional 注解的 ServiceImpl,一份项目里事务标注越密集的地方,越是核心业务。报名和考勤都会在这些位置出现,重点阅读这些方法的调用链,比看任何文档都更能理解这套系统当初的设计意图和边界条件。
本文还有配套的精品资源,点击获取