☰
SpringBoot+Vue教育培训机构办公管理系统实战:架构设计与源码解析
2026/10/6 14:02:25 网站建设 项目流程

教育培训机构的数字化管理,一直是中小机构最头疼的问题:学员信息散落在Excel表格里,课程排期靠微信群接龙,报销审批抱着一堆纸质单据满楼跑。市面上的SaaS系统倒是省事,但数据在别人手里,功能还未必匹配自己机构的实际流程。我最近在反复打磨一套教育培训机构线上办公管理系统的完整源码,技术栈是SpringBoot + Vue + MyBatis + MySQL,前后端分离,覆盖招生线索、学员管理、课程排课、考勤审批、财务报表这条完整业务闭环。这篇博文把这套系统的架构设计、核心模块实现、数据库建模和从零部署的全过程拆开讲一遍,正在做教育管理系统、或者打算给机构搭内部办公系统的朋友,可以直接拿来当参考蓝本。

先交代一下背景。这套系统不是实验室里交作业的demo,是从一家连锁培训机构的真实业务里一点点磨出来的。机构里有市场、销售、教务、财务、教师这几类角色,各有各的诉求:市场要看线索转化率,销售要跟进意向学员,教务要排课防冲突,教师要看课表签到,财务要对课时消耗算收入。如果这些角色各用各的表格,数据一定是越做越乱。这套系统要解决的,就是把这些角色的工作流收拢到一个统一的管理后台里。

1. 项目概述与整体设计思路

1.1 系统定位:三条业务线收拢到同一套数据模型

教育培训机构的日常运营,本质上就三条线在跑:

  • 业务线:市场线索 → 跟进回访 → 体验课 → 报名缴费 → 续费转介绍
  • 教学线:课程设置 → 排课 → 上课签到 → 课时消耗 → 作业与评价
  • 后勤线:员工考勤 → 请假审批 → 报销与采购 → 公告通知

管理混乱,往往不是某一件事没做好,而是三条线的数据没打通。比如销售报了一个班,教务不知道是哪个校区的班,财务不知道这笔钱对应多少课时。这套系统在数据模型层面就把三条线串起来:学员表引用线索ID,报名单引用课程ID和排课ID,课时流水引用报名单ID。这样一查课消,就能从“哪个学员→哪个班→哪次课→消了多少课时”一路追踪到底,不会出现账目对不上的情况。

做实体设计的时候,有一个细节值得拿出来说:所有核心表都保留deleted逻辑删除标记和created_at、updated_at时间字段。管理系统的数据是资产,删除记录和单据这种操作,物理删表后想恢复比登天还难。逻辑删除虽然让查询SQL里多了一个deleted = 0条件,但换来的数据安全是完全值得的。

1.2 技术选型:为什么是SpringBoot + Vue + MyBatis + MySQL

管理类系统有个特点:并发不高、逻辑不复杂,但业务关系多、权限要求细。这就决定了架构选择的方向是稳定、成熟、好招人、好维护,而不是一味追新。选这套组合,我的理由如下:

技术组件定位选型理由
SpringBoot后端基础框架自动化配置减掉大量样板代码,内嵌Tomcat部署简单,Spring Security做权限控制成熟
Vue前端框架组件化开发适合后台多页面场景,配合Element UI能快速搭建表单和表格
MyBatis持久层框架SQL完全可控,复杂统计查询直接写原生SQL优化,比ORM框架更灵活
MySQL数据库免费成熟,运维资料遍地都是,中小机构几十万行数据量轻松扛住

有人会问:为什么不用微服务?教育办公系统的用户量级通常就是几十上百个员工同时在线,单体应用完全够用,微服务反而引入注册中心、配置中心、分布式事务一堆复杂度,对一个小团队来说是灾难。也有人问:为什么不用JPA?JPA在简单CRUD上确实省事,但一旦遇到课消报表这种七八张表关联的统计场景,写JPQL或者Specification远不如原生SQL直观可控。MyBatis在复杂查询上的掌控感,是这个项目选它最直接的原因。

前端技术选型上,这套工程用的Vue 2 + Element UI。我知道Vue 3已经出来很久了,但为什么教育信息化这个圈子里大量项目还在Vue 2?原因很现实:稳定组件生态、团队熟悉度、老项目维护成本。管理系统页面形态相对固定,表格、表单、弹窗、树形控件就是全部主角,Vue 2的响应式机制配合Element UI已经极其顺手。这不是技术落后,是合适的工具用在合适的场景。

1.3 源码目录结构与模块划分

拿到源码,第一件事别急着跑,先把目录结构看明白。

edu-office-system/ ├── backend/ │ ├── edu-admin/ # 后端主工程 │ │ ├── src/main/java/com/edu/office/ │ │ │ ├── controller/ # 接口层 │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── mapper/ # MyBatis Mapper接口 │ │ │ ├── entity/ # 实体类 │ │ │ ├── config/ # 配置类(安全、跨域、拦截器) │ │ │ └── common/ # 统一返回体、异常处理、工具类 │ │ └── src/main/resources/ │ │ ├── mapper/ # MyBatis XML映射文件 │ │ └── application.yml # 核心配置文件 │ └── sql/ │ ├── edu_office_init.sql # 表结构与基础数据 │ └── edu_office_sample.sql # 模拟业务数据 ├── frontend/ │ ├── edu-web/ # Vue前端工程 │ │ ├── src/ │ │ │ ├── api/ # 接口请求封装 │ │ │ ├── router/ # 路由配置 │ │ │ ├── store/ # Vuex状态管理 │ │ │ ├── views/ # 页面组件 │ │ │ └── components/ # 通用组件 │ │ └── package.json └── README.md

backend按com.edu.office分controller/service/mapper/entity/config/common几层,是比较标准的单体分层:controller只做参数接收和结果封装,service承载业务逻辑,mapper只管数据访问。common里放着统一返回体Result<T>、全局异常处理器、分页工具类这些基础设施。这样分层的好处是职责单一,新人接手时看代码位置就能猜出大概功能。

frontend的src/permission.js是我最想让你先看的文件,它在vue-router的全局前置守卫里做登录态校验和动态路由注入,保证了不同角色登录后看到的菜单和能访问的页面完全不同。sql目录下两个文件分工明确:init脚本是表结构加账号、角色、菜单等基础数据,sample脚本是模拟业务数据。测试系统时先把sample导入,就能看到完整的假数据效果。

2. 核心技术栈深度解析

2.1 SpringBoot核心配置:版本匹配是第一道坎

我见过太多人栽在版本上。SpringBoot 3.x已经要求JDK 17,但很多机构服务器还跑着JDK 8,字节码版本直接不兼容,启动就报UnsupportedClassVersionError。这套源码基于SpringBoot 2.7.x + JDK 8构建,是目前生产环境最稳妥的组合之一。对应的MyBatis集成包用mybatis-spring-boot-starter2.3.x,MySQL驱动用mysql-connector-java8.0.x,三个版本互相兼容,不会出现奇怪的依赖冲突。

核心配置文件application.yml长这样:

server: port: 8080 servlet: context-path: /edu-api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/edu_office?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.edu.office.entity configuration: map-underscore-to-camel-case: true cache-enabled: true logging: level: com.edu.office.mapper: debug

几个细节必须解释清楚。spring.jackson.date-format写成yyyy-MM-dd HH:mm:ss是给Jackson全局定日期格式,不然前端拿到的默认是ISO格式时间戳,处理起来很麻烦。mybatis.configuration.map-underscore-to-camel-case必须开启,这样数据库的create_time才能自动映射到实体的createTime,否则查出来全是null。logging.level.com.edu.office.mapper: debug能把MyBatis打印的SQL和参数输出到控制台,排查问题效率高一截——这个配置上线前记得改回info级别,不然日志文件会被刷爆。

再说一遍SpringBoot版本的事:如果你本地电脑习惯用JDK 17或者SpringBoot 3.x,不要直接把依赖升上去。3.x里javax包名改成了jakarta,Spring Security的配置方式也变了,整个MyBatis集成、拦截器、工具类都可能编译不过。用多版本JDK管理工具,项目级指定JDK 8,这是最省心的路线。

2.2 Vue前端架构:动态路由与权限控制的实现思路

管理系统的前端难点从来不是页面多,而是不同角色看到的系统不一样。销售不该看到财务的成本报表,教师不该看到学员的缴费记录。这靠的不是把菜单藏起来,而是路由层面的权限控制。

这套源码在router目录下把路由拆成constantRoutes和asyncRoutes两份:constantRoutes是登录页、404这类所有人可见的路由,asyncRoutes是带meta.roles权限标记的业务路由。用户登录后,前端根据用户角色动态过滤asyncRoutes,再通过router.addRoutes注入到当前路由实例,同时根据过滤结果动态生成左侧菜单。

// src/permission.js - 全局前置守卫核心逻辑 router.beforeEach(async (to, from, next) => { const token = store.getters.token if (token) { if (to.path === '/login') { next({ path: '/' }) } else if (!store.getters.hasUserInfo) { const userInfo = await store.dispatch('getUserInfo') const accessRoutes = await store.dispatch('generateRoutes', userInfo.roles) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } else { next() } } else { next('/login') } })

这里有个非常隐蔽的坑:动态路由注入后,第一次导航可能命中不了新加的路由,所以要在addRoutes之后用next({ ...to, replace: true })强制重新导航一次。不写这行,刷新页面经常会白屏或者跳404,排半天查不出原因。这也是vue-router动态路由被问得最多的一个问题,面试和工作里都是高频。

菜单权限的实现同样走这份过滤后的路由表:Vuex里存一份sidebarRouters,侧边栏组件递归渲染。每个路由的meta里带上title和icon,二级路由自动做父子嵌套,不用为每个页面单独写菜单配置。新增一个页面,只要在asyncRoutes里加一条路由记录加上权限标记,菜单和权限自动就有了。

2.3 MyBatis持久层:SQL可控与缓存策略

MyBatis在这个项目里承担所有数据访问。XML映射文件放在resources/mapper下,每张核心表对应一个XxxMapper.xml。为什么不全部用注解?因为项目里有大量多表关联和动态条件查询,XML里写动态SQL的<if>、<where>、<foreach>比注解里的字符串拼接直观得多,而且改SQL不用重新编译Java代码。

拿学员线索的分页条件查询举个例子,状态、跟进人、时间范围都是可选项:

<select id="selectFollowList" resultType="com.edu.office.entity.StuStudent"> SELECT * FROM stu_student <where> <if test="status != null"> AND status = #{status} </if> <if test="assignUserId != null"> AND assign_user_id = #{assignUserId} </if> <if test="startTime != null"> AND created_at &gt;= #{startTime} </if> <if test="endTime != null"> AND created_at &lt;= #{endTime} </if> </where> ORDER BY next_follow_time ASC LIMIT #{offset}, #{pageSize} </select>

<where>标签会自动处理第一个条件前面的AND,不用手动拼“WHERE 1=1”这种丑陋写法。参数全部走#{}预编译,不存在SQL注入问题。很多人喜欢用${}拼表名或排序字段,那是安全红线,能不用就不用。

再说缓存。MyBatis一级缓存是SqlSession级别的,默认开启,同一个会话里多次查同一条SQL只查一次库;二级缓存是namespace级别的,默认关闭,需要<cache/>标签显式开启。但这类频繁增删改的后台业务系统,我强烈建议不要开二级缓存——教育办公系统数据实时性要求高,今天排课明天教室冲突,缓存稍微一脏就是重大事故。真要扛查询压力,应该用Redis做更可控的缓存层,而不是依赖MyBatis的二级缓存。所以application.yml里cache-enabled虽然是true,实际上mapper XML里都没有加<cache/>,二级缓存等于没启用,这个开关保留只是为了兼容某些调试场景。

课消报表这种多表统计,MyBatis的优势就体现出来了。直接写原生SQL关联五张表,数据库引擎做聚合比应用层循环快一两个数量级:

SELECT c.course_name, COUNT(DISTINCT s.id) AS student_cnt, SUM(cl.consume_hours) AS total_hours FROM crs_schedule sc JOIN crs_class c ON sc.class_id = c.id JOIN stu_signup sg ON sg.class_id = c.id JOIN stu_student s ON sg.student_id = s.id LEFT JOIN crs_consume_log cl ON cl.signup_id = sg.id WHERE sc.start_time BETWEEN #{startTime} AND #{endTime} GROUP BY c.id

2.4 MySQL数据库设计:核心表结构与字段约束

这套系统的核心库edu_office一共30多张表,按业务域可以分成四组:用户权限(sys_user、sys_role、sys_menu、sys_user_role)、学员业务(stu_student、stu_follow_log、stu_signup)、教务教学(crs_course、crs_class、crs_schedule、crs_consume_log)、行政后勤(oa_leave、oa_reimburse、oa_notice)。

拿学员表举例,完整的建表语句是这样:

CREATE TABLE stu_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', student_no VARCHAR(32) NOT NULL UNIQUE COMMENT '学员编号', name VARCHAR(50) NOT NULL COMMENT '姓名', phone VARCHAR(20) NOT NULL COMMENT '手机号', source_channel VARCHAR(20) COMMENT '来源渠道:转介绍/线上广告/地推', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1在读 2停课 3结业 4流失', assign_user_id BIGINT COMMENT '当前跟进销售ID', next_follow_time DATETIME COMMENT '下次跟进时间', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1删除', KEY idx_phone (phone), KEY idx_status_assign (status, assign_user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员表';

设计要点:学生编号设置唯一索引,业务上方便线下对账;phone建索引是因为按手机号查学员是高频操作;组合索引(status, assign_user_id)覆盖“某个销售查看自己名下在读学员”这个高频查询场景。InnoDB是默认存储引擎,支持事务和外键,课消流水和财务对账必须依赖事务。utf8mb4字符集必须强调,如果建表用了旧的utf8,学员姓名里的生僻字就可能存不进去或者显示成乱码,这是老MySQL版本最常见的坑。

3. 核心业务模块设计与实现

3.1 学员管理与招生线索:状态驱动销售流程

招生这块,系统里最核心的是线索状态机。一条市场线索从进系统到成交,要经历“新建 → 跟进中 → 已试听 → 已报名 → 已缴费”几个状态,中间随时可能掉到“已流失”。状态流转不是随便改的,后端service里做了校验,比如已流失的线索不能直接改成已缴费,必须重新走跟进流程。这个约束在业务上很重要,否则销售为了业绩可以把流失线索直接复活,数据就失真了。

跟进功能的实现是写跟进日志。每条跟进记录存了下次跟进时间、跟进内容、跟进方式(电话/微信/面谈),销售首页会显示“今日待跟进”列表,按next_follow_time排序提醒。跟进日志表stu_follow_log用student_id建索引,列表查询毫秒级返回,体感很流畅。

这个模块还有一个容易被忽略的细节:线索分配。新线索进系统时,默认分配给当前负责该渠道的销售,如果销售离职或者休假,管理员可以批量转移线索归属。这个批量转移接口用了事务控制,一次性更新几百条记录的assign_user_id,中间任何一条失败就整体回滚,避免出现线索被分配给两个人或者没人接手的中间状态。

3.2 课程排课与课消流水:冲突检测与课时扣减

排课是教务模块里最容易写错逻辑的地方。排课表的基本字段是教室ID、教师ID、开始时间、结束时间、关联课程班级。新增排课时,service层要做一个冲突校验:同一教室、时间重叠不能排;同一教师、时间重叠不能排。这个校验看起来简单,写起来容易漏——一定要在SQL层面做重叠区间判断,不能只查当天有没有课然后在前端判断。因为如果两个教务同时操作,前端校验挡不住并发写入。

判断区间重叠的SQL很经典:

SELECT COUNT(*) FROM crs_schedule WHERE teacher_id = #{teacherId} AND #{startTime} < end_time AND start_time < #{endTime} AND deleted = 0

这条SQL的关键是重叠判定条件:新课程的[start, end]与已有课程的[start, end]有交集,等价于“新开始时间小于已有结束时间 且 新结束时间大于已有开始时间”。把这个条件记牢了,排课冲突检测就不会写错。同理,教室冲突检测只要把teacher_id换成room_id就行。

课消逻辑是财务对账的基础:学员报名课程后,系统记录总课时,每上一次课,在crs_consume_log表插入一条消耗记录,同时更新学员剩余课时。这一增一删必须在同一个事务里完成,我用@Transactional注解包住。如果事务边界没控制好,就会出现课消明细有记录但剩余课时没减的问题,月底财务对账哭都来不及。

线上课堂这块,如果机构开了直播课,排课记录里可以挂一个直播地址。前端播放m3u8流媒体格式时,直接引入hls.js就能在浏览器里播放,不需要额外安装插件或者依赖特定播放器。这个方案对机构来说成本低,运维也省事。

3.3 审批流与办公协同:状态机的通用设计

行政办公模块的审批(请假、报销、采购)本质是同一套状态机:草稿 → 提交 → 审批中 → 通过/驳回。这套源码没有为每种审批单独写一套代码,而是抽象了oa_form通用表加类型字段,表单内容用JSON存,审批动作走统一的approve接口。这样新增一个审批类型不需要改表结构,只加一条配置就行。这是一个非常值得借鉴的设计思路。

CREATE TABLE oa_form ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_type VARCHAR(30) NOT NULL COMMENT '类型:leave/reimburse/purchase', applicant_id BIGINT NOT NULL COMMENT '申请人', status TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1审批中 2通过 3驳回', form_content JSON COMMENT '表单内容JSON', current_approver_id BIGINT COMMENT '当前审批人', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批单通用表';

审批流权限上区分了普通员工和管理员:普通员工提交申请后只能看到自己的单子,部门负责人能看到本部门全部单子,财务角色看报销单时能看到金额字段,其他人隐藏。数据权限不是只在前端做菜单隐藏,后端service查询时自动拼接部门过滤条件。用一个DataScopeAspect切面,根据当前登录人的部门ID自动注入查询条件,防止越权查看别的部门数据。管理系统里这种接口级的数据权限校验,比单纯的前端控制要可靠得多。

驳回和撤回是审批模块最容易忽略的边界。设计上规定:审批中且当前审批人非本人,不能撤回;已驳回的单子重新编辑后走重新提交,状态从草稿重新开始;审批通过的单子不允许修改表单内容,只能走红冲反审批流程。这些规则都是在状态机里写死的,避免业务上扯皮。

3.4 报表统计:MySQL聚合查询的正确姿势

管理系统到最后,老板看的一定是报表。这套系统里报表模块有三张核心统计:招生漏斗(线索→试听→报名→缴费每一层的转化率)、课消统计(按校区/课程/月度分组统计消耗课时和剩余课时)、财务汇总(应收、实收、退款、课时费消耗收入)。

招生漏斗的数据,用一条聚合SQL就能算出来:

SELECT SUM(CASE WHEN status >= 2 THEN 1 ELSE 0 END) AS followed_cnt, SUM(CASE WHEN status >= 3 THEN 1 ELSE 0 END) AS trial_cnt, SUM(CASE WHEN status >= 4 THEN 1 ELSE 0 END) AS signup_cnt, SUM(CASE WHEN status >= 5 THEN 1 ELSE 0 END) AS paid_cnt FROM stu_student WHERE source_channel = 'online_ad' AND created_at >= '2025-01-01'

报表SQL能用聚合函数就别在Java里做二次计算,数据库引擎做count比应用层循环快一两个数量级。写报表查询的经验:先在EXPLAIN里看有没有走索引,再核对聚合字段的数据类型,比如count返回的是BIGINT,Java里接Long而不是Integer,小了会溢出。

报表模块的查询条件统一封装成ReportQuery对象,时间范围、校区、课程类型、渠道来源都是可选参数,后端用PageHelper做分页。报表数据量大时,最有效的优化是“预处理”:每天晚上定时任务把当天的课消和缴费数据按维度汇总到报表中间表,白天老板看报表直接查中间表,毫秒级返回,不用每次都去扫流水明细表。

4. 从零搭建到部署上线

4.1 环境准备:JDK、Maven、Node、MySQL版本怎么搭

先列一套可以拍胸脯说跑得通的组合:

组件推荐版本说明
JDK1.8生产环境兼容性最好的版本,机构服务器普遍在用
Maven3.6.x与SpringBoot 2.7.x配合稳定
Node.js14.x / 16.x对应Vue CLI 4/5的依赖要求
MySQL5.7或8.0两版都行,驱动版本记得对应调整
IDEA2022+开发工具,社区版也够用

MySQL 5.7在Windows上的安装,建议直接下载mysqld压缩包手动初始化,别用安装版,安装版经常在服务注册和权限初始化上出幺蛾子。手动初始化流程其实就三步:解压、执行mysqld --initialize-insecure生成data目录和免密root、执行mysqld --install注册成Windows服务。整个过程五分钟搞定,比安装版可控得多。MySQL 8.0的安装类似,但驱动必须用com.mysql.cj.jdbc.Driver,连接串里要加serverTimezone参数,这块下面排查章节细说。

Node版本这块同样有讲究。Vue CLI 5要求Node 12以上,但Node 18、20这些新版本装依赖时经常报OpenSSL错误,最省事的就是用nvm切到14或16,装完依赖再切回来也不影响。

4.2 导入数据库与修改配置

把sql目录下两个脚本按顺序导入。先在MySQL里创建库再导数据,避免脚本里没有CREATE DATABASE语句时傻眼:

mysql -uroot -p -e "CREATE DATABASE edu_office DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p edu_office < edu_office_init.sql mysql -uroot -p edu_office < edu_office_sample.sql

导入完成后验证一下:select count(*) from sys_user;能查到初始账号,select count(*) from stu_student;能查到示例学员,说明导入成功。接下来改application.yml里的数据库账号密码。密码里如果包含特殊字符,比如@、#、$,直接拼在JDBC连接串里会被解析成连接参数,大概率连不上。正确做法是放进spring.datasource.password里让框架处理,别往url里塞。

4.3 后端启动:IDEA里配端口和启动类

用IDEA打开backend/edu-admin目录,等Maven把依赖拉完。找到主启动类EduApplication,右键Run。如果默认端口不是想要的,不用改代码,在IDEA的Run Configuration里加VM options:-Dserver.port=8081,或者直接改application.yml。框架读取配置的优先级是VM options大于配置文件,想知道当前生效的是哪个端口,看启动日志里的Tomcat started on port(s): 8080那行最靠谱。

启动过程常见报错:端口被占用,改端口或者杀掉占用进程;数据库连接失败,多半是密码错了或者MySQL服务没起来;Mapper找不到,检查target目录下有没有把XML复制出来,Maven有时会漏打包resources里的mapper文件,需要在pom.xml的build里加上:

<resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources>

4.4 前端构建与跨域联调

前端工程在frontend/edu-web,命令:

npm install npm run serve

默认跑在8080端口,和后端8080冲突,所以前端开发服务器一般是8081。开发模式下跨域问题通过vue.config.js里的devServer.proxy解决:

devServer: { port: 8081, proxy: { '/edu-api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端请求/edu-api/xxx会自动转发到后端,浏览器里看不到跨域报错。有个小坑:changeOrigin必须设为true,不然后端拿到的请求头里Host还是8081,某些场景下会出现奇怪的会话问题。生产环境不需要这个proxy,改成Nginx反代即可。

4.5 部署上线:两种方式选哪个

方式一:前后端分离部署,jar包加Nginx。后端jar包用nohup java -jar edu-admin.jar &启动,Nginx配置把/edu-api反代到8080端口,/指向前端dist目录:

server { listen 80; server_name edu.example.com; root /data/www/edu-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /edu-api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

方式二:Vue打包后塞进SpringBoot。npm run build生成dist,把dist里全部文件拷到后端src/main/resources/static目录,重新打包成jar。这样只跑一个进程、只占一个端口,部署运维最省事,适合机构内部用、服务器资源紧张的场景。缺点也明显:前后端耦合了,前端一更新就要重新打jar重启后端。我的建议是:如果只是给机构内部二三十个人用,方式二最省心;如果后续要做线上对外业务,老老实实上方式一。

5. 常见问题与排查技巧实录

5.1 数据库层的幺蛾子

问题1:启动报Failed to configure a DataSource。检查application.yml里spring.datasource的配置有没有生效,最常见是IDE里启动时激活的profile不对,配置写到了application-dev.yml但启动时没指定dev。SpringBoot的配置加载机制是约定大于配置,没指定profile时只加载默认的application.yml。

问题2:MySQL 8.0连不上,报Public Key Retrieval is not allowed。驱动是8.x时,连接串里要加allowPublicKeyRetrieval=true&useSSL=false,这是MySQL 8默认加密连接导致的。前面配置里已经写上了,直接抄就行。

问题3:中文乱码。检查三处:数据库表字符集是不是utf8mb4、JDBC连接串有没有characterEncoding=utf8、操作系统文件编码是不是UTF-8。三处全对了基本不会乱。

5.2 MyBatis层的经典坑

坑1:查出来全是null。90%是驼峰映射没开。mybatis.configuration.map-underscore-to-camel-case: true加上这行世界安静。

坑2:动态SQL的and多了或者少了。用<where>标签包住动态条件,它会智能处理第一个条件前的AND/OR。<if>里也别写“and xxx = 1”这种硬编码,条件不成立时SQL最后会多个裸and,直接报语法错误。

坑3:分页查询排序字段拼接。orderBy字段如果来自前端参数,一定要做白名单校验,只允许传表里真实存在的列名,否则就有注入风险。教育系统里虽然不涉及核心机密,但该防的还是要防,这是基本功。

5.3 前端联调与构建问题

问题1:登录后刷新页面404。动态路由注入后没做重新导航,代码里加next({ ...to, replace: true })解决。另外Nginx部署时也要配try_files $uri $uri/ /index.html;,否则history模式刷新就404,这是前端路由部署的通病。

问题2:npm install卡死或者报ERESOLVE。多半是node版本太新或太旧,Vue CLI项目建议nvm切到14/16再装。如果报ERESOLVE unable to resolve dependency tree,可以试试npm install --legacy-peer-deps。

问题3:接口能访问但页面数据不出来。打开浏览器F12看Network面板,如果响应是401/403,检查请求头里Authorization带没带token,本地调试可以看Vuex里token有没有正确写入。前后端联调,九成问题是token和跨域,这两大件排查完再看看数据权限配置。

5.4 版本与性能的提醒

最后提醒一个容易被忽视的点:SpringBoot版本不要盲目升。这个源码是2.7.x,如果你本地电脑装的是JDK 17或者习惯用SpringBoot 3.x,直接把依赖升上去会出现组件不兼容。建议用多版本JDK管理工具,项目级指定JDK 8,别动框架版本。真要用3.x,那意味着整个MyBatis集成、Spring Security配置、部分API都要跟着升级测试,工程量不小。

性能方面,这种管理系统的瓶颈基本不在代码,而在数据库索引。把慢查询日志打开,看到执行时间超过1秒的SQL拉出来EXPLAIN一下,缺索引补索引,少写SELECT *,分页查询加LIMIT,性能基本就够了。几十个并发的管理后台,优化手段不需要什么分布式缓存、读写分离,那是杀鸡用牛刀。

写到这里,这套系统的代码逻辑和部署流程就都过了一遍。我个人做教育行业管理系统最大的体会是:技术栈永远不是瓶颈,难点在业务建模。你这张表把谁作为主键、状态流转谁允许谁不允许、课消和财务的勾稽关系怎么设计,这些想清楚了,写代码只是时间问题。这套源码里踩过的坑都写进去了,希望看到这篇的朋友能少走几步弯路。如果照着跑起来遇到问题,或者在自己机构场景里发现了新的边界情况,可以拿出来交流。这类系统迭代到后面,才是真正有意思的阶段,比如加一个学员端小程序、接入在线支付、做自动排课算法,这些都是很好的扩展方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询