基于Spring Boot+Vue的培训机构管理系统设计与实现 我接手过一个朋友的培训机构系统改造需求。他开的是少儿编程班三个校区、二十个班、三百多个学员但排课还在用Excel考勤靠教师在群里发照片缴费记录散落在微信转账和纸质收据里。月末对账的时候财务和教务各拿一套数据谁也说服不了谁。这种状态持续了大半年直到有一名学员家长因为课时数对不上投诉他们才下决心搞一套管理系统。那时候我帮他落地了一版Spring Boot Vue的培训机构管理系统从需求梳理、数据库建模、后端接口到前端页面全程走了一遍。这篇文章就把这套系统的设计与实现过程完整拆开讲一遍包含我做毕业设计或商业项目时最常遇到的几个关键决策业务边界怎么划、表结构怎么建、排课冲突怎么校验、报名缴费事务怎么处理、前端怎么跟后端联动、最后怎么部署上线。如果你是正在做类似管理系统设计的同学或者准备给中小培训机构搭内部系统的开发者这篇文章应该能帮你少走不少弯路。1. 培训机构管理系统到底要解决哪些问题在写代码之前得先把业务想清楚。很多管理系统做失败不是技术上做不到而是业务边界没划明白该做的没做不该做的做了一堆。1.1 中小培训机构与K12学校的本质差异培训机构的管理逻辑和正规学校差别非常大。K12学校是固定班级、固定课表、固定学期校长不用操心退费也不用管招生转化率。但培训机构的排课极其灵活一个班可能有春季班、秋季班、寒假班同一个教室白天是幼儿美术课晚上是成人油画课同一个老师周一到周五在机构坐班周六周日跑校区上课。这种灵活度决定了系统不能按学籍管理那套思路设计必须围绕课时和教室资源来建模。另外收费模式也是一大差异。培训机构常见的收费方式有按学期收费、按课时包收费、1对1次卡收费、活动体验课收费等。退了课怎么算钱、请假了课时怎么补、试听转化后的课时怎么赠这些业务规则每个机构都不一样。通用的SaaS教务软件往往强制套用一套规则反而把业务流程搞得更复杂。所以培训机构管理系统尤其是中小机构自研系统第一要务是按自己机构的规则来第二要务才是功能齐全。1.2 角色、流程与系统模块的对应关系我把这套系统的角色梳理为五类超级管理员、教务/校长、教师、财务、学员家长。小机构里财务和校长可能是同一人但系统层面还是建议分开因为权限边界清晰以后出问题才好追责。核心业务流程是这样的市场招生或学员家长上门咨询教务安排试听试听满意后报名分班系统里生成报名单和缴费订单财务确认收款后学员进入班级名单剩余课时入账教务根据教室、教师、班级排课生成课表上课时教师点名系统记录考勤课程过半或学段结束时涉及续费、请假补课、退费结算最后是财务对账和经营数据统计。每一步都要有对应的页面和接口环环相扣。1.3 功能模块清单与优先级排序我做的第一版功能模块如下表模块核心功能主要角色系统管理用户管理、角色权限、菜单管理、数据字典超级管理员学员管理学员档案、家长信息、课时台账、试听记录教务、财务班级管理班级创建、名额管理、开班、结课教务排课管理教室/教师/班级排课、冲突检测、课表查询教务考勤管理教师点名、请假申请、补课标记教师、教务报名缴费报名单、缴费订单、收款、退款教务、财务财务管理收支流水、月度对账、发票登记财务数据统计出勤率、营收趋势、班级续费率、教室利用率校长、教务如果做的是毕业设计或者MVP我建议只做学员、班级、排课、考勤、报名缴费、简单统计这六个核心模块系统管理里留个用户登录和角色判断就够了。公告、审批流、消息通知这类锦上添花的功能等核心流程跑通再加否则项目很容易烂尾。2. 技术选型为什么是Spring Boot Vue这套组合这套系统我选的是非常主流的方案后端Spring Boot前端Vue数据库MySQL。理由不复杂就是稳、生态好、招人容易。但在版本选择上有一些细节值得单独说。2.1 后端Spring Boot版本与关键依赖现在的Spring Boot已经到3.x版本了对应JDK 17。新项目我建议直接用Spring Boot 3.2以上因为Spring Boot 2.7虽然还在维护但已经处于维护末期。3.x和2.7最大的差异在于javax命名空间换成了jakarta比如javax.servlet变成了jakarta.servlet网上大量老教程里的依赖坐标和代码拿过来会报编译错误这个坑很常见。如果教学环境或者服务器只支持JDK 8那就用Spring Boot 2.7不影响这套设计。核心依赖包括spring-boot-starter-web提供接口能力MyBatis-Plus处理数据库操作这个ORM在CRUD密集的管理系统里效率极高Spring Security做认证授权JWT做无状态登录MySQL驱动连数据库Lombok减少样板代码。2.2 前端Vue 3 Vite Element Plus前端部分我用的是Vue 3 Vite Element Plus Pinia Axios。Vue 2已经停止维护新项目没必要再踩。Vite构建比Webpack快很多开发体验好Element Plus是Vue 3对应的组件库后台管理系统的表格、表单、弹窗、布局组件基本开箱即用Pinia替代Vuex做状态管理API更简洁Axios做HTTP请求拦截器里统一处理Token和错误码。这套组合对中小型管理系统来说非常合适。后端写接口前端写页面两边并行开发效率高。有人可能会问为什么不直接用JSP加jQuery那套技术不是不行但前后端代码耦合严重页面复杂以后维护成本非常高而且现在的前端生态里表格、日期选择、权限菜单这些组件都有成熟封装用Vue会省很多事。2.3 数据库与缓存的取舍数据库默认MySQL 8.0字符集用utf8mb4解决中文和生僻字、emoji存入报错的问题。缓存这一层我建议MVP阶段不引入Redis。为什么培训机构管理系统核心是事务性操作排课、缴费、考勤的实时性和一致性的优先级远高于性能几百个学员的小规模并发放MySQL完全扛得住。报表查询如果频率高等系统真出现性能瓶颈了再加Redis也不迟。硬塞一个Redis进来反而要处理缓存穿透、缓存一致性、部署依赖等多一堆问题这对新手很不友好。2.4 为什么不是其他技术方案把Spring Boot Vue和几个常见方案对比一下就更清楚了。用纯JSP Servlet做一个页面里混着HTML和Java代码改个样式都要重启整个应用业务一复杂根本维护不动。用PHP开发是快但后期业务逻辑复杂以后工程质量难保证。用Python的Django或Flask内部系统没问题但招开发和跟现有系统集成时Spring家族的优势更明显。这套系统的核心是稳定、易维护、文档多、生态全Spring Boot Vue正好全占。3. 数据库设计围绕上课和钱两条主线设计表数据库是这套系统最需要花时间的地方。我的核心设计原则是一切表要么围绕上课这条线要么围绕钱这条线两条线在报名缴费时交汇。3.1 实体关系与账号体系设计先理一下实体用户、教师、学员、班级、教室、课程、排课、考勤、订单、支付流水、退款记录。这里有个容易踩坑的设计问题用户表要不要跟教师表、学员表合并我的做法是分开。sys_user表只负责账号、密码、角色、状态teacher表和student表存储业务资料通过user_id关联账号。为什么分开因为一个教师可能同时是学员家属或者某天教师离职但学员档案要保留账号和业务档案解耦之后删除账号不影响历史数据完整性。学员表里要记录家长的姓名和联系电话这是培训机构的特色需求因为通常对接人不是学员本人而是家长。课时台账建议单独设计记录学员每一次获得课时或消耗课时的明细退费时需要回溯。3.2 核心表结构详解直接看关键表的建表语句比讲一堆理论更直观。用户表CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, role varchar(30) NOT NULL COMMENT 角色编码ADMIN/STAFF/TEACHER/FINANCE, status tinyint NOT NULL DEFAULT 1 COMMENT 1-启用 0-禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;班级表CREATE TABLE class_info ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 班级名称如少儿编程启蒙1班, subject varchar(50) DEFAULT NULL COMMENT 科目, teacher_id bigint DEFAULT NULL COMMENT 主教老师ID, capacity int NOT NULL DEFAULT 20 COMMENT 班级容量, remaining_seat int NOT NULL DEFAULT 20 COMMENT 剩余名额, status tinyint NOT NULL DEFAULT 1 COMMENT 1-招生中 2-进行中 3-已结课 4-已停用, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;排课表是这套系统的核心。设计时我用了date加weekday加start_time加end_time的组合把某一天某一节课的具体上课时间完整表达出来。单独用weekday表示每周一上课的话实现固定课表还好遇到节假日调课就会非常痛苦。我最终选择的是具体日期模式教务在系统里手动把整学期的课排出来虽然录入成本高一些但后续处理停课、补课、调课都方便。CREATE TABLE schedule ( id bigint NOT NULL AUTO_INCREMENT, class_id bigint NOT NULL COMMENT 班级ID, teacher_id bigint NOT NULL COMMENT 教师ID冗余方便课表查询, classroom_id bigint NOT NULL COMMENT 教室ID, date date NOT NULL COMMENT 上课日期, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, status tinyint NOT NULL DEFAULT 1 COMMENT 1-正常 2-已取消 3-已完成, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_classroom_date (classroom_id, date), KEY idx_teacher_date (teacher_id, date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单与支付流水表。这里必须强调一个设计原则订单表和支付流水表一定要分开。一个订单可能分多次缴费比如定金加尾款也可能一次缴费对应多个订单的合并支付退款又要单独记录。如果只在一张订单表里改个已支付字段财务对账时什么都查不出来。CREATE TABLE course_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 唯一订单号, student_id bigint NOT NULL COMMENT 学员ID, class_id bigint NOT NULL COMMENT 班级ID, course_count int NOT NULL COMMENT 购买课时数, total_amount decimal(10,2) NOT NULL COMMENT 原价总额, discount_amount decimal(10,2) NOT NULL DEFAULT 0 COMMENT 优惠金额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-退款中 3-已退款 4-已关闭, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE payment_record ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL, amount decimal(10,2) NOT NULL COMMENT 本次收款/退款金额, pay_type tinyint NOT NULL COMMENT 1-微信 2-支付宝 3-现金 4-银行转账, direction tinyint NOT NULL COMMENT 1-收款 2-退款, operator_id bigint NOT NULL COMMENT 操作人, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;金额字段用decimal(10,2)这是很多新手容易忽略的坑。用double存金额可能19.9乘以3会算出59.699999999999996这种数用户一看就炸。数据库用decimalJava代码里用BigDecimal所有金额运算通过BigDecimal完成这是财务数据的基本要求。3.3 状态字段与并发一致性设计订单状态、班级状态、排课状态这三组状态字段要提前定义清楚。我的习惯是用tinyint加数字状态码代码里用常量类或枚举维护而不是直接散落在SQL里。状态流转一定要单向可控比如订单从待支付到已支付再到退款中不能让已退款的订单重新变成已支付。并发控制这块重点在班级报名扣减名额。报名时如果先select查剩余名额再判断是否大于0然后update高并发下会超卖。正确做法是直接在update语句里加条件UPDATE class_info SET remaining_seat remaining_seat - 1 WHERE id ? AND remaining_seat 0受影响行数0就说明名额已满。这个思路跟电商库存扣减一模一样。4. 后端核心功能实现权限拦截、排课冲突与缴费事务后端是整个系统的发动机。这里我挑三个最关键的功能实现细节讲都是实际开发中绕不开的部分。4.1 登录认证与接口权限控制认证用的方案是Spring Security JWT无状态、不需要服务端保存Session对前后端分离架构很友好。核心逻辑是登录接口放行用户提交用户名密码后校验BCrypt密码校验通过后签发一个JWT返回给前端前端后续请求都带Authorization请求头后端过滤器统一解析JWT并设置登录态。Spring Security配置里关键的几点关闭CSRF启用无状态Session放行登录接口其他接口全部要求认证加上EnableMethodSecurity启用方法级权限注解。Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login).permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }接口层面用PreAuthorize(hasRole(ADMIN))来限制管理员接口比如用户管理只能ADMIN角色访问排课、报名这类接口允许STAFF和ADMIN访问考勤允许TEACHER访问。角色权限写死在注解里代码直观维护成本低。4.2 排课冲突检测看似简单实际容易漏排课冲突检测是这类系统的经典难题。业务规则有三条同一教室同一时间段只能上一节课同一教师同一时间段只能在一个教室上课同一班级同一时间段不能同时安排两节课。实现方式是在service层做重叠区间判断写入前校验。时间区间重叠判断的核心条件是新排课的开始时间早于已有排课的结束时间并且新排课的结束时间晚于已有排课的开始时间。这个条件比直接比较startTime等于或者endTime等于要严谨得多。public void checkScheduleConflict(Schedule schedule) { // 教室冲突检查 LambdaQueryWrapperSchedule classroomQuery new LambdaQueryWrapper(); classroomQuery.eq(Schedule::getClassroomId, schedule.getClassroomId()) .eq(Schedule::getDate, schedule.getDate()) .lt(Schedule::getStartTime, schedule.getEndTime()) .gt(Schedule::getEndTime, schedule.getStartTime()); if (schedule.getId() ! null) { classroomQuery.ne(Schedule::getId, schedule.getId()); } if (scheduleMapper.selectCount(classroomQuery) 0) { throw new BizException(该教室在当前时间段已被占用); } // 教师冲突检查逻辑同上条件换成 teacherId // 班级冲突检查逻辑同上条件换成 classId }有个细节需要注意取消状态的排课应该在冲突检查时过滤掉。教务取消了一节课重新排课是正常操作如果不加状态过滤这个坑会让教务骂人。日程排课的状态字段冲突检查只查status等于1的正常记录。4.3 报名缴费流程的事务处理报名缴费涉及多个表的写入创建订单、扣减班级名额、生成支付记录任何一个失败都必须整体回滚不然会出现钱收了但没有课时这种事故。我统一用Transactional(rollbackFor Exception.class)包裹事务内先做校验然后扣名额最后插入订单和支付记录。创建订单时要保证幂等性。前端用户双击提交可能创建两个订单。处理办法是生成唯一订单号利用数据库的唯一索引兜底。即使代码里忘了判断数据库也会拒绝重复订单号。Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderDTO dto) { Student student studentMapper.selectById(dto.getStudentId()); ClassInfo clazz classInfoMapper.selectById(dto.getClassId()); if (student null || clazz null) { throw new BizException(学员或班级不存在); } String orderNo ORD System.currentTimeMillis() RandomUtil.randomNumbers(4); CourseOrder order new CourseOrder(); order.setOrderNo(orderNo); order.setStudentId(dto.getStudentId()); order.setClassId(dto.getClassId()); // 设置课时数、金额用 BigDecimal 计算 orderMapper.insert(order); // 原子扣减名额防止超卖 int updated classInfoMapper.reduceRemainingSeat(dto.getClassId()); if (updated 0) { throw new BizException(班级名额已满); } // 记录首笔支付流水状态为待支付或已收款 return order.getId(); }这里还有一个经验不要把扣减名额放在订单状态确认之前。正确顺序是先创建待支付订单确认收款后再扣减名额或者干脆在确认收款的事务里同时做订单状态更新和名额扣减。否则用户下单不付款名额却被锁死教务还得手动释放。4.4 统计报表的SQL思路报表模块在培训机构里的重要性被很多人低估。校长真正关心的是这个月收了多少钱、哪些班出勤率低、续费率高不高。我实现的核心报表有三张月度营收、出勤率排行、班级人数趋势。月度营收直接聚合支付流水表注意只统计direction为1的收款记录SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(amount) AS revenue FROM payment_record WHERE direction 1 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC;出勤率需要关联考勤表和排课表按班级分组统计总记录数和出勤记录数。这类聚合查询是报表模块的标配SQL的where条件加上时间范围后数据量大时记得建组合索引否则会越跑越慢。为了不让报表查询拖慢主业务统计接口可以加缓存。性能优化策略是报表接口设置较短缓存时间比如5分钟写操作频繁的时段缓存自动失效或者每次核心写操作后主动更新报表缓存。没有Redis的时候用一个ConcurrentHashMap加定时任务也能凑合但代码会有点丑上线后加Redis是更稳的选择。5. 前端Vue落地动态路由、状态管理和排课看板前端部分如果只写增删改查页面几周就能搞定。真正体现工程能力的是路由权限控制和几个复杂页面的交互设计。5.1 Vite项目初始化与目录结构用Vite创建Vue 3项目就一条命令的事npm create vitelatest training-web -- --template vue。安装依赖的时候需要注意Element Plus、Pinia、Axios、Vue Router都是必不可少的。目录结构我习惯这样组织src/ api/ # 每个模块的接口请求封装 router/ # 路由配置 stores/ # Pinia 状态 layout/ # 后台布局侧边栏顶栏 views/ # 页面组件 components/ # 复用组件 utils/ # axios 实例、工具函数utils/request.js里一定要做好axios封装请求拦截器自动从localStorage读取token添加到Authorization头响应拦截器统一处理业务错误码遇到401自动跳转到登录页。这一层不做好后面每个页面都手动处理token和错误代码能写到吐。5.2 路由权限控制与动态菜单权限菜单这块主流做法是前端定义静态路由路由的meta里写roles数组进入页面前通过路由守卫判断当前用户角色是否在允许列表里。这套方案简单可靠适合中小型系统。如果角色的菜单差异特别大可以做成后端返回菜单列表前端用router.addRoute动态注册。登录后要做的第一件事是拉取用户信息存进Pinia。路由守卫的逻辑是没有token一律跳登录页有token但没有用户信息先拉取再放行目标路由配置了roles且当前用户不在其中跳403页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { return next() } if (!token) { return next(/login) } const userStore useUserStore() if (!userStore.roles.length) { userStore.fetchUserInfo().then(() { if (to.meta.roles !to.meta.roles.some(r userStore.roles.includes(r))) { return next(/403) } next() }) } else { next() } })有一个Vite特有的坑必须提醒() import(/views/${component}.vue)这种动态导入路径在Vite里不能完全动态拼变量因为Vite打包时需要静态分析文件路径。更稳的做法是维护一张组件路径映射表后端菜单配置里存映射表的key而不是真实路径。这个坑我踩过一次开发环境没问题一打包就报模块找不到。5.3 排课看板把表格变成课表排课页面是前端交互最复杂的部分。我实现的思路是周视图网格横轴是周一至周日纵轴是时间段行比如08:00-10:00、10:00-12:00这样。后端返回某一周所有排课记录前端根据date加startTime计算坐标把排课数据映射到网格的对应单元格里。这个方案的渲染逻辑不算难卡点在于单元格交互点已经排课的格子弹出课程详情点空白格子弹出新建排课的弹窗弹窗里选择班级、教师、教室和时间。数据回填时因为同一个格子理论上可以存在多节课要处理列表展示。前端代码建议把网格抽成独立组件props只接收scheduleList由父组件负责数据加载和刷新。5.4 报名登记与缴费页面报名登记页面的核心体验是减少选错机会。第一步选择学员用一个可搜索的el-select输入学员姓名自动联想第二步选择班级列出版剩余名额名额为0的班级不可选第三步填购买课时数和实付金额系统自动根据班级单价算出应收金额教务只需要填优惠金额最后弹确认框显示学员、班级、课时数、实付金额确认后调后端下单接口。这里前端要处理的细节是重复提交。下单按钮点击后立即置为loading状态接口成功或失败后再恢复。不要等接口返回才disable网络慢的场景下用户会连点三下创建三个订单。后端即使有幂等兜底前端也要做好基础防护这是体验问题。6. 部署上线多环境配置与两种部署方式的取舍写到一半的好系统如果部署环节没处理好上线那天照样翻车。我分享一下实际部署过程中的经验。6.1 Maven多环境配置开发环境、测试环境、生产环境要分开配置参数最常用的做法是Maven Profile加Spring Profile结合。pom.xml里定义profile用profile.active占位符让Maven打包时替换application.yml里的spring.profiles.active然后每个环境一份application-dev.yml、application-prod.yml文件里面配置各自的数据库地址、Redis地址、日志级别。profiles profile iddev/id propertiesprofile.activedev/profile.active/properties activationactiveByDefaulttrue/activeByDefault/activation /profile profile idprod/id propertiesprofile.activeprod/profile.active/properties /profile /profiles打包时用mvn clean package -Pprod选择生产配置。提醒一个细节pom.xml里加了resource的filtering之后application.yml里如果有符号比如程序参数会被Maven错误替换遇到这种情况要么转义要么避免在配置里用符号。6.2 前端构建后的两种部署方式前端执行npm run build后会生成dist目录。部署方式有两条路我分别都用过。第一种是前后端分离部署Nginx托管前端静态资源反向代理后端接口server { listen 80; server_name training.example.com; root /var/www/training; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这种方式的关键就是try_files那行它解决了Vue Router history模式刷新404的问题。因为前端路由是前端控制的浏览器请求一个真实不存在的路径比如/student/detailNginx会把请求转发给index.html由前端路由接管。第二种是直接集成部署吧dist目录里的文件复制到Spring Boot项目的static目录然后打成单个jar包。这种方式启动简单一个jar就是整个系统特别适合小机构内部部署。但缺点也明显前端每次更新都要重新打后端包而且还需要在后端加一个Controller转发所有非api路径到index.html否则刷新404。两种方式我推荐第一种运维时前后端各自独立更新出问题也好排查。如果服务器条件极其简陋只有一台小机器第二种也能用但记得后端要加路径转发。6.3 上线后的基本运维系统上线后除了功能本身运维基本盘要做好三件事。第一是日志用logback按天滚动切割日志文件保留30天出现问题时先看ERROR级别的日志再看业务日志。第二是数据库备份写个cron脚本每天凌晨备份MySQL备份文件保留最近7天。第三是慢SQL监控MySQL开启慢查询日志超过1秒的SQL捞出来分析通常是统计报表的SQL缺索引或者多表join导致的。7. 盘点我踩过的坑与后续扩展方向技术方案讲完了最后分享几个我这套系统落地过程中真实踩过的坑以及这个系统之后还能怎么扩展。7.1 五个新手必踩的坑第一个坑是前端传时间字符串到后端报400。前端默认传的是ISO格式形如2025-01-10T10:00:00后端LocalDateTime字段直接解析失败。解决办法是在DTO的时间字段上统一加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解或全局配置Jackson的日期格式。第二个坑是MySQL连接串没有指定时区。默认serverTimezone不设置时数据库连上后时间相差8小时排课记录全部对不上。连接串里明确加上serverTimezoneAsia/Shanghai后端的LocalDateTime存储和返回都不会出错。第三个坑是Transactional同类内部调用失效。Service里写了一个methodAmethodA里调同类中的methodBmethodB上有Transactional结果methodB的事务不会生效。因为Spring事务走代理this调用不会经过代理。解决方法是把methodB拆到另一个Service类里或者用TransactionTemplate手动管理事务。第四个坑是前端history路由部署后刷新白屏。开发环境一切正常部署到Nginx后从菜单跳转没问题一按F5就404。这就是没配try_files的原因或者直接集成部署时没加Controller转发。第五个坑是金额计算用double。这个前面已经详细说过任何涉及金额的运算必须用BigDecimal数据库字段用decimal。培训机构对账时一分钱都不能差double的精度问题在金额场景是不可接受的。7.2 这个系统还能往哪些方向扩展第一版系统跑通后可以逐步加这些能力。消息通知是最有价值的上课提醒、请假审批结果、缴费成功通知都可以接微信模板消息或短信服务能显著减少教务的沟通成本。学员端小程序或公众号是第二个扩展点家长自己查课表、看剩余课时、提交请假申请能省下不少前台人工。多校区支持是第三个扩展点业务表加一个branch_id字段报表按校区维度汇总几个校区共用一套系统。如果开始做线上付费就对接微信支付订单流程增加支付回调处理注意回调幂等。回到开头那个朋友的故事这套系统上线后他最大的感受是排课冲突少了、月末对账不用熬夜了。但其实真正帮他省时间的不是功能多少而是把业务流程理顺了。给想做类似系统的读者一个建议先写清楚自己的业务规则再动手建表写代码。系统管理的核心是流程管理不是页面堆砌一个小而稳的MVP比一个烂尾的全功能系统有用得多。