尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SSM框架下的兴趣班与延时班管理系统设计与实现

SSM框架下的兴趣班与延时班管理系统设计与实现 简介基于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离校 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;考勤表有唯一键 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 groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.10/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependencymybatis-spring 的版本不能乱升级2.0.x 对应 Spring 51.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 iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/interest_class_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.mapper/ /bean tx:annotation-driven transaction-managertransactionManager/ bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /beandataSource 用的是 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 的完整调用链一个查询班级列表的接口会贯穿三层业务写在 ServiceSQL 写在 Mapper XMLRestController RequestMapping(/api/class) public class ClassController { Autowired private ClassService classService; GetMapping(/list) public Result list(RequestParam(required false) Integer courseType) { ListClassInfoVO list classService.listClasses(courseType); return Result.success(list); } }Service public class ClassServiceImpl implements ClassService { Autowired private ClassInfoMapper classInfoMapper; Override public ListClassInfoVO listClasses(Integer courseType) { return classInfoMapper.selectByType(courseType); } }select idselectByType resultTypecom.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 testcourseType ! null AND co.course_type #{courseType} /if /where ORDER BY c.start_date DESC /selectController 里用 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(ListAttendance list) { if (list null || list.isEmpty()) return; int batchSize 200; for (int i 0; i list.size(); i batchSize) { ListAttendance sub list.subList(i, Math.min(i batchSize, list.size())); attendanceMapper.batchInsert(sub); } }insert idbatchInsert parameterTypelist INSERT INTO attendance (student_id, class_id, course_type, att_date, is_present, operator_id) VALUES foreach collectionlist itemitem separator, (#{item.studentId}, #{item.classId}, #{item.courseType}, #{item.attDate}, #{item.isPresent}, #{item.operatorId}) /foreach ON DUPLICATE KEY UPDATE is_present VALUES(is_present) /insertforeach 的 separator 设置为逗号MyBatis 会拼成一条多 VALUES 的插入语句。配合之前建表时的唯一键 uk_stu_class_dateON 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 为 0COUNT() 会把缺勤也数进去。这类统计如果数据量大group by 会拖慢报表速度。项目里常见做法是建一张 attendance_monthly 汇总表每天晚上用定时任务重算报表只查汇总表。6. 本地跑通这套系统的三个验证技巧6.1 用最小数据量做全链路冒烟测试导入源码和数据库后不要直接点「启动」先按下面的步骤验证环境是否正常验证对象操作预期结果数据库连接用 Navicat 执行SELECT 1返回 1建表脚本运行项目的 .sql 文件无报错能看到 10 张左右核心表Maven 依赖执行mvn clean package -DskipTestsBUILD 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一份项目里事务标注越密集的地方越是核心业务。报名和考勤都会在这些位置出现重点阅读这些方法的调用链比看任何文档都更能理解这套系统当初的设计意图和边界条件。本文还有配套的精品资源点击获取
返回列表