去年我帮一个做舞蹈和少儿体适能培训的朋友整理门店管理流程,发现他们的排课表还停留在Excel加微信群,课时统计经常对不上数,月底对账要花两三天。后来我把这套流程做成了一个基于SpringBoot和Vue的“艺体培训机构业务管理系统”,既帮他解决了实际经营问题,顺带也作为毕业设计交付了完整论文。今天就把整个项目的需求拆解、技术选型、核心实现和踩坑记录完整分享出来,想复刻类似的管理系统,或者正在考虑用同类选题做毕设的同学,可以直接拿走参考。
这个系统适合三类人:一是培训机构的管理者,想理解业务怎么信息化;二是全栈开发的入门者,想找一个不太复杂但五脏俱全的项目练手;三是准备做毕业设计、正在纠结技术栈和论文结构的学生。整个项目不算大,但覆盖了学员管理、课程排课、签到扣课时、收费报表、权限控制这些真实业务场景,麻雀虽小,该有的东西都有了。
1. 项目概述与需求拆解
1.1 艺体培训机构到底需要什么
艺体培训和普通的K12学科培训不一样,它的业务模型相对特殊。一个学员可能同时学舞蹈和跆拳道,也可能报了二十节一对一的钢琴课再加十节小班绘画课。课程类型多、计费方式杂、课时包占据主导,这些都会直接影响系统设计。
我把朋友那边的业务痛点总结成了四件事:
- 学员档案散乱,谁报了什么课、还剩多少课时,全靠本子记,换个人就说不清。
- 排课冲突严重,同一个老师可能被排到两个教室同时开课,或者同一个时间段不同班型抢一个场地。
- 签到和课时扣减脱节,孩子来没来、扣的是哪个课包的课时,全凭前台记忆。
- 收费明细混乱,续费了多少钱、赠送了多少课时、退费怎么处理,月底统计时经常扯皮。
所以这套系统本质上是一个围绕“学员-课程-课时-钱包”的闭环管理系统。核心不是做出多少花哨页面,而是把业务数据串起来,让每一次签到都能准确扣减对应课包课时,让每一笔收费都能追溯到学员和课程。
1.2 技术选型:为什么是SpringBoot加Vue
选技术栈时我也纠结过,是做成单体JSP系统,还是用现在主流的前后端分离。最后选了SpringBoot加Vue这套组合,主要原因是它贴合实际开发场景,也适合当毕设。
SpringBoot的优点,说白了就是“开箱即用”。它把Spring繁琐的XML配置全部自动化了,内嵌Tomcat,打包成jar就能跑。配合MyBatis-Plus操作数据库,写增删改查的效率极高,我不用把时间浪费在框架配置上,可以把精力放在业务逻辑上。
Vue这边,我选了Vue3加TypeScript,配合Element Plus组件库和Vite构建工具。Vue的响应式数据绑定让页面开发效率很高,尤其中后台管理系统里大量表格、表单、弹窗,Element Plus基本都能直接拼装出来。Vue3的Composition API在组织复杂业务逻辑时比Vue2的Options API清爽不少,代码复用也更方便。
有人会纠结Vue2还是Vue3,我的建议是直接上Vue3。虽然网上Vue2的老教程多,但Vue3是当前主流,而且Element Plus这些新组件库只支持Vue3,毕业设计用新版本写进论文,技术前瞻性也更说得过去。
1.3 论文选题怎么和项目结合
标题末尾的“lunwen”其实就是论文,这算这个项目的另一个主线。我的经验是,不要把论文当成项目做完后的补写材料,而是把它当成“需求调研-设计-实现-测试”这条主线的记录。开题时就把系统要实现的功能、技术选型、数据库设计、核心模块流程这些框架定下来,后面做项目时顺手收集截图、测试数据、问题记录,写论文时就很轻松,不需要临时回忆。
2. 系统整体设计与架构
2.1 功能模块划分
一个完整的艺体培训机构业务管理系统,我把它拆成七个模块,每个模块对应一个独立的后端Controller和一组前端页面。
| 模块 | 核心功能 | 说明 |
|---|---|---|
| 系统管理 | 用户登录、角色权限、菜单管理 | 基于JWT实现认证,RBAC控制操作权限 |
| 学员管理 | 学员基本信息、报名记录、课时余额 | 支持艺术类和体育类学员分类 |
| 教师管理 | 教师档案、授课方向、课时费结算 | 教师绑定课程和排课记录 |
| 课程管理 | 课程类型、课程包、定价规则 | 区分一对一和班课,支持不同课包 |
| 排课管理 | 排课、调课、教室冲突检测 | 按教师和教室双向校验时间冲突 |
| 签到与课时扣减 | 扫码签到、手工签到、扣减课包课时 | 防止重复签到,课时不足时拦截 |
| 收费与报表 | 收费订单、退费、课时消耗统计、教师工资统计 | 金额用BigDecimal,报表按月份聚合 |
这样的模块划分,既覆盖了培训机构日常经营的闭环,也不至于把系统做得无限大。每个模块之间的数据流转都是单向清晰的:学员报名课程包,课程包产生排课,排课触发签到,签到扣减课时,收费记录对应课包订单。
2.2 前后端分离架构与项目结构
前后端分离是现在研发团队的主流做法,前端专注于页面交互,后端专注于接口。项目结构上,我前端的目录是按页面模块划分的,后端的目录是按经典的三层架构划分的。
前端Vue项目结构大概是这样的:
src/ main.ts App.vue api/ # 接口请求封装 member.ts course.ts schedule.ts finance.ts auth.ts views/ login/index.vue layout/index.vue member/index.vue # 学员列表 member/detail.vue # 学员详情与课时流水 schedule/calendar.vue # 排课日历 finance/order.vue # 收费订单 router/ index.ts # 路由配置 stores/ user.ts # Pinia用户状态 utils/ request.ts # Axios封装后端的SpringBoot项目结构,我遵循了标准的分层方式:
com.example.artsport controller/ # 接口层,接收参数,返回统一结果 service/ # 业务层,处理核心业务逻辑 mapper/ # 数据访问层,MyBatis-Plus接口 entity/ # 数据库实体 dto/ # 接口入参和返回对象 config/ # 全局配置,比如跨域、拦截器 common/ # 统一返回、异常、工具类后端分层的作用是让职责边界清晰。Controller不写业务逻辑,只做参数接收和返回包装;Service层处理事务和业务规则;Mapper层只做数据访问。这样写论文的时候,架构图、时序图都能从代码里直接提炼出来,不会出现“代码和论文对不上”的尴尬。
前后端数据交互我用的是统一的JSON格式:
{ "code": 0, "message": "success", "data": {} }前端Axios封装里统一拦截这个结构,接口报错时统一提示Message.error,这样代码量少,体验也一致。
2.3 数据库设计要点
数据库是整个系统的地基,表结构设计得好不好,直接决定后面业务逻辑好不好写。我结合艺体培训业务,设计了这些核心表。
学员表(member)比较关键,它要存学员的基本信息和当前课时总余额:
CREATE TABLE `member` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `phone` VARCHAR(20) NOT NULL, `category` TINYINT NOT NULL COMMENT '1-艺术类 2-体育类', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-停用', `balance_hours` DECIMAL(10,1) NOT NULL DEFAULT 0, `remark` VARCHAR(255), `created_time` DATETIME );课程包表(course_package)和订单表(payment_order)是财务闭环的关键。一个学员在系统里可能会有多个未消耗完的课程包,比如“48节中国舞课包”和“20节轮滑课包”,所以不能只用一个总余额字段去扣,而是要把课时余额落实到每个课程包上。
签到表(sign_in)记录了每次上课的扣减明细:
CREATE TABLE `sign_in` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `schedule_id` BIGINT NOT NULL, `member_id` BIGINT NOT NULL, `package_id` BIGINT NOT NULL COMMENT '扣减的课程包', `deduct_hours` DECIMAL(5,1) NOT NULL, `sign_time` DATETIME NOT NULL, UNIQUE KEY `uk_schedule_member` (`schedule_id`, `member_id`) );这里我特别加了唯一索引,防止一个学员同一节课被重复签到。数据库层的约束远比代码层的判断可靠,能拦住很多并发重复请求。
排课表(course_schedule)则承担了时间维度的核心问题:
CREATE TABLE `course_schedule` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `course_id` BIGINT NOT NULL, `teacher_id` BIGINT NOT NULL, `room_id` BIGINT NOT NULL, `start_time` DATETIME NOT NULL, `end_time` DATETIME NOT NULL, `max_students` INT NOT NULL, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-未上课 2-已完成 3-已取消' );表和表之间的关系不复杂,但设计时要多想业务场景。比如退费时怎么恢复课时余额,转班时怎么迁移课时,学员在多个课包之间怎么选择优先扣减。这些细节都在数据库字段里提前做了支持,后面实现就没那么痛苦。
3. 核心模块实现细节与实操要点
3.1 学员课时管理:如何避免超扣
课时余额是钱,扣多了学员不答应,扣少了机构吃亏。最基础的要求是:每次扣减课时前判断余额是否足够,并且保证扣减操作不能被并发请求搞乱。
我在扣课时的方法上加了事务控制,同时使用SELECT FOR UPDATE对学员所在课程包的课时记录加行锁:
@Transactional public void deductHours(Long memberId, Long packageId, BigDecimal deductHours) { CoursePackage cp = coursePackageMapper.selectByIdForUpdate(packageId); if (cp.getRemainHours().compareTo(deductHours) < 0) { throw new BizException("课包剩余课时不足"); } cp.setRemainHours(cp.getRemainHours().subtract(deductHours)); coursePackageMapper.updateById(cp); }为什么用SELECT FOR UPDATE而不是直接UPDATE后判断受影响行数?因为这里是先读取再扣减,两步操作在并发下会产生“竞态条件”。两个请求同时读到课时剩余3,各自扣3,最后都通过校验,课时就变成负数了。加行锁之后,第二个请求必须等第一个事务提交才能读到最新数据,串行执行,问题就解决了。
金额字段一律用BigDecimal,禁止用double或float。艺体培训机构的课时余额经常出现0.5、1.5这样的数字,float的二进制浮点误差会在累计对账时被无限放大。我踩过一次坑,报表里加起来总是差0.01元,折腾了半天,发现是double精度丢失。
3.2 排课与签到:冲突检测的正确姿势
排课冲突检测是这类系统里最常见也最容易被忽略的功能。一开始我想的是遍历所有已排课程,逐条判断时间是否重叠,后来发现数据量超过几百条后,接口响应越来越慢。正确的姿势是把冲突判断下沉到数据库去处理。
查询教师在同一时间是否已有课程:
SELECT COUNT(*) FROM course_schedule WHERE teacher_id = #{teacherId} AND status IN (1, 2) AND start_time < #{endTime} AND end_time > #{startTime}这个SQL判断重叠区间的逻辑很经典:两段时间存在交集的条件,就是新课程的开始时间早于已有课程的结束时间,并且新课程的结束时间晚于已有课程的开始时间。教室冲突检测同理,把teacher_id换成room_id就可以。
签到功能我保留了两种方式:一种是老师在系统里点选学员名单批量签到,另一种是学员到店后在前台页面扫码签到。无论哪种方式,落库时都做两件事:插入签到流水,扣减对应课包的课时。这两件事必须放在同一个事务里,要么同时成功,要么同时回滚,不能用先扣课时再回头插流水的方式。
接收签到请求时,接口要做幂等控制。我的办法是不依赖前端按钮禁不禁用,而是在数据库层面用schedule_id和member_id的唯一索引兜底,重复提交时插入失败,事务回滚,课时也不会被重复扣。
3.3 收费与报表:从订单粒度聚合数据
收费模块看起来就是记录一笔钱,但机构的经营分析全部依赖这些订单数据。我在订单表里设计了课程包、课时数、原价、实付金额、赠送课时这些字段,退费时生成负数订单并标记原订单关联ID,这样对账时能把正向和负向订单都拎出来。
月度课时消耗统计,用一条聚合SQL就能实现:
SELECT MONTH(sign_time) AS month, COUNT(DISTINCT member_id) AS attend_member_count, SUM(deduct_hours) AS total_hours FROM sign_in WHERE YEAR(sign_time) = #{year} GROUP BY MONTH(sign_time)教师工资核算更复杂一些,不同老师可能按课时费分成比例不同。我在教师表里增加了commission_rate字段,工资统计模块读取排课签到记录,分别按一对一和班课的单价规则汇总。这个功能虽然代码量不大,但很能体现系统对培训机构的价值,也是我论文里特色模块的素材。
聚合报表的性能也值得注意。机构跑一年后,签到记录和订单记录会到几万条,直接在业务表上做聚合查询不是不可以,但要保证所有过滤条件都走索引,不要在字段上加函数导致索引失效。如果数据量更大,可以设计一张日结报表表,每天定时任务汇总当天的课时消耗和收费金额,查询时直接读汇总表,速度会快很多。
3.4 权限控制:用RBAC满足三种角色
这个系统的用户角色分成管理员、教务和教师三种,管理员管一切,教务可以排课和收费,教师只能查看自己的课表和进行点名签到。权限设计用的是经典的RBAC模型,也就是用户、角色、菜单三层关系。
后端我用Spring Security加JWT做认证和授权。登录接口校验用户名密码成功后签发JWT,后续请求在Header里带token,拦截器解析token并把用户角色放进去。在Controller方法上使用@PreAuthorize注解做接口级权限控制。
前端这边,难点在于动态路由。管理员的菜单和教师的菜单不一样,不能把全部路由一股脑注册到当前Router里,需要根据用户的角色动态过滤。我的做法是登录接口返回用户信息和权限码列表,前端拿到后按权限码过滤路由表,再通过addRoute动态注册到Router实例。这样用户直接在地址栏输入一个无权访问的路径,也会因为路由不存在而跳转404。
4. 开发环境配置与常见问题排查
4.1 SpringBoot版本太高?先想清楚再选
这个项目我最初一上手就用了当时最新的SpringBoot 3.2,结果踩了不少坑。SpringBoot 3.x是基于JDK17开发的,很多老教程用的JDK8,对应的javax包也全部改成了jakarta包,参考网上的代码时,光改import就花了不少时间。
比如用MyBatis-Plus时,SpringBoot 2.x对应的版本是3.x,SpringBoot 3.x需要MyBatis-Plus的spring-boot3-starter版本,两个坐标不一样,直接复制老配置会启动失败。
我给你的建议是:如果是为了快速完成系统,优先选SpringBoot 2.7.18加JDK8或者JDK11,这个版本资料最多,遇到问题随便搜都能找到答案;如果想写进论文体现技术新度,再选SpringBoot 3.2加JDK17,但要做好部分依赖需要单独适配的心理准备。
| 组合 | SpringBoot | JDK | MyBatis-Plus | 适合人群 |
|---|---|---|---|---|
| 稳妥型 | 2.7.18 | 1.8 | mybatis-plus-boot-starter 3.5.3.1 | 讲求出成果 |
| 主流型 | 3.2.x | 17 | mybatis-plus-spring-boot3-starter 3.5.5 | 想写技术亮点 |
还要提醒一句,千万别用刚发布的SNAPSHOT版本,别当小白鼠。选一个已经稳定使用半年的版本,比追求新版本省心得多。
4.2 Vue安装与环境配置的碎碎念
Vue项目的环境配置,卡住过太多人了。Node.js版本太老,Vite起不来;版本太新,又有可能和某些依赖不兼容。我目前用的是Node.js 18 LTS版本,配npm,创建Vue3项目时用npm create vite@latest命令,模板选vue-ts。
安装依赖时最头疼的是网络问题。我的经验是配置镜像源,把npm registry切换成国内镜像地址,安装效率会高很多,也不需要额外工具。这一步属于基础环境处理,如果提示权限错误,先检查Node.js是否安装成功,再检查是否有残留进程占用文件。
开发Vue项目我用的是IDEA配Vue插件。IDEA社区版其实足够用,新建Vue项目时直接通过内置前端脚手架生成,运行npm脚本也方便。不过IDEA对Vue单文件组件的提示不如VS Code灵活,两个编辑器我都试过,最终留在IDEA是因为后端和前端在同一个IDE里切来切去比较方便,不会总在窗口间跳。
4.3 开发期联调:跨域和接口规范
前后端分离开发时,前端在5173端口跑Vite,后端在8080端口跑SpringBoot,直接发请求必然会有跨域问题。我在后端写了一个全局CorsConfig配置,允许本地开发地址访问,并放开所有请求头和请求方法。这个方案在开发环境简单粗暴,但在生产环境不注意会有风险,生产环境更推荐把前端业务统一放在同一个后端服务下托管资源。
接口规范这块,我踩过不少坑。前期我和一起做前端的伙伴没有把接口返回结构完全统一,导致前端拿到数据后要判断多种情况,代码写得很脏。后来后端所有接口一律返回统一结构:成功code为0,失败code为非0,业务错误通过全局异常处理器统一拦截并返回提示信息。前端只需要在Axios拦截器里判断一次code,就能把所有接口错误处理逻辑收敛掉。
4.4 部署上线:宝塔容器部署的实操记录
系统开发完成后,我在Linux服务器上用容器方式部署了整套系统。后端是SpringBoot打成jar包,然后写一个Dockerfile镜像,基础镜像用Java运行时版本,jar包放进去后暴露8080端口。前端是Vite打包生成的dist目录,用一个轻量级Web服务托管静态页面。
FROM openjdk:17-jdk-slim WORKDIR /app COPY target/artsport-admin.jar /app/app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]前端静态页面部署在宿主机的一个目录下,由服务器管理面板托管,配置好HTTPS证书和站点指向dist目录即可。后端镜像运行后,前端页面里的接口地址改成后端服务的公网地址。通讯安全上,我在后端配置了跨域白名单,只允许前端站点的域名请求,其他来源一律拒绝。这样做有个好处,后端不直接暴露在公网端口给别处调用,降低了被乱刷接口的风险。
数据库我用的是MySQL容器,但容器里的数据卷一定要挂载到宿主机目录,否则容器一删数据就全没了。我首次部署时犯过这个错误,升级镜像时忘记挂载数据卷,数据直接没了,还好有备份救回来。从此以后任何有状态服务都必须挂数据卷,没得商量。
5. 论文写作思路与答辩重点
5.1 论文结构怎么搭
如果你是要拿这个系统交毕业设计论文,我建议按下面这个结构走:
- 绪论:写研究背景和意义,重点突出中小型培训机构信息化水平低、市面产品价格高、通用软件不适配的问题。
- 需求分析:画用例图,把管理员、教务、教师三个角色的用例列出来,描述清楚每个模块的功能需求和非功能需求。
- 系统设计:画系统架构图、功能架构图、数据库ER图,把核心表结构和字段设计讲清楚。
- 系统实现:按模块写关键业务逻辑,必须有核心代码、运行截图、流程说明。
- 系统测试:写测试用例表,把正常场景和异常场景各列几条,附上测试结果截图。
论文最容易丢分的地方是“设计”和“实现”脱节。论文里的模块,代码里必须真实现,不要论文写了一个高大上的雪花算法ID,代码里用的是MyBatis-Plus的自增ID,这种硬伤答辩一被追问就露馅。
我的建议是把核心业务逻辑画成流程图和时序图,比如“学员签到和课时扣减”这个流程,从接收请求、校验课包、扣减课时、返回结果,用一张时序图画出来,论文的可读性立刻提高一个档次。画图工具不挑,能用就行,但图一定要规范。
5.2 答辩高频问题与应对思路
答辩老师不一定会把你的代码跑起来看,他们更喜欢问设计思路和实现细节。有些问题几乎是必问的,提前想好答案,现场就不慌。
为什么选择SpringBoot加Vue?可以从开发效率、生态成熟度、前后端分离趋势回答,顺便提一句这种技术栈在企业中有广泛应用。
课时超扣问题怎么解决的?这就用到了我之前讲的事务和行锁,回答时强调数据库约束和并发控制,再举一个具体的并发例子。
怎么保证数据一致性?除了事务,还要说唯一索引兜底、状态校验、统一异常回滚机制。
权限是怎么实现的?把RBAC模型和JWT流程讲清楚,最好手画一下用户、角色、菜单的关系。
系统的性能瓶颈在哪?如果数据量大了怎么办?是一个常见拓展问题。可以从数据库索引、分页查询、报表预聚合、Redis缓存几个方向回答,不用真的实现,但思路要对。
论文里的“总结与展望”也不要写空话,比如“未来系统可以引入消息队列处理并发签到请求,使用缓存预热热门课程数据”,这些都是可落地的方向,说明你思考过系统的扩展性,比写“系统具有很好的应用前景”这种废话有说服力得多。
最后再送给准备做类似项目的朋友一句话:这个系统最核心的价值不在页面多漂亮,而在业务闭环的严谨程度。艺体培训行业的学员、课包、排课、签到、收费是一条完整的数据链,咬合得越紧,系统越稳。我个人实际开发的经验是,不要一上来就想把所有功能做全,先跑通“学员报名→排课→签到→扣课时→收费”这个最小闭环,再往上面叠加报表、权限、教师工资这些辅助功能,项目推进起来会顺很多。希望这篇分享能让你少踩几个坑,把这套系统做得比我的更好。