1. 项目拆解这套系统到底解决什么问题先说结论教师工作量管理系统核心不是“管理教师”而是把“工作量”这件事从拍脑袋变成可量化、可追溯、可汇总的流程化工具。我在实际开发中接触过不少高校、培训机构的类似需求最常听到的抱怨是工作量统计靠 Excel 来回传期末汇总时格式五花八门核对课时要翻聊天记录领导要报表时又得重新整理。这套基于 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的系统就是冲着这些痛点去的。从标题看它已经明确了技术选型和交付物后端用 SpringBoot2前端用 Vue3持久层用 MyBatis-Plus数据库锁定 MySQL8.0并且带文档。对于想直接拿源码二次开发、或者作为毕业设计/课程项目参考的同学来说这套组合最大的好处是“不折腾”。SpringBoot2 目前生态最成熟网上资料多遇到问题好搜Vue3 是前端主流方向组合式 API 写起来清爽MyBatis-Plus 把单表 CRUD 几乎简化成了零 SQL 配置非常适合业务逻辑偏重、表结构稳定的管理类系统MySQL8.0 在窗口函数、JSON 支持等方面比 5.7 强不少后面做统计报表会用到。适合谁看三种人第一准备做类似管理系统的开发者可以直接抄作业第二学生党拿它当毕业设计或课程项目需要理解业务和技术栈的对应关系第三想从前端或后端单方面转向全栈的开发者用它练习前后端分离项目的完整链路。下面我会从业务设计、技术实现、部署实操、避坑经验四个维度展开尽量把别人文档里不写的东西讲透。2. 业务模型与数据库设计思路2.1 教师工作量包含哪些核心维度大部分教师工作量管理系统业务实体逃不开这几个教师、学期、课程/任务类型、工作量规则、审核记录、汇总统计。这套系统同样如此。我见过一些失败的项目上来就把表建得特别细字段几十个结果填数据的人都不知道每个字段啥意思。正确的做法是先定义清楚“工作量”的计算口径。工作量一般分两部分。一是教学工作量比如课时数、课程门数、实验课折算系数二是非教学工作量比如指导学生竞赛、编写教材、参与专业建设、监考等。不同学校对这两类的权重不一样。所以设计时要留一个“工作量规则表”把每类活动的折算系数、上限、认定方式做成可配置项而不是写死在代码里。标题里提到“管理系统”那么至少要有三个端教师端填写/查看工作量申报、审核端教研室或教务审核、管理端配置规则、查看统计报表。如果你的项目还包含管理员角色那通常是超级管理员统一管理账号和基础数据。2.2 数据库表结构如何设计才合理基于 MyBatis-Plus 的特性表结构设计要尽量规范主键统一用id逻辑删除用deleted时间字段用create_time和update_time这样 MP 的自动填充功能可以直接生效。核心表大致如下用户表sys_user教师账号、姓名、工号、职称、所属教研室/学院、角色。工作量类型表work_type类型名称、基准分值、计量单位、是否需要附件、适用学期。工作量申报表work_apply教师ID、类型ID、学期、工作量描述、申报分值、附件路径、状态草稿/待审核/已通过/已驳回。审核记录表work_audit申报ID、审核人ID、审核意见、审核时间、操作类型。学期表semester学期名称、开始时间、结束时间、是否当前学期。规则配置表rule_config项目类型、系数、上限、说明。这里有个容易踩坑的地方如果系统要支持“多角色审核”比如先教研室审核再教务处审核那么审核记录表里最好加一个audit_level字段表示当前审核到第几级否则流程一复杂代码就乱了。2.3 MyBatis-Plus 在业务中的典型用法用 MP 做查询时最舒服的就是LambdaQueryWrapper。比如筛选某个教师某学期所有“已通过”的申报LambdaQueryWrapperWorkApply wrapper new LambdaQueryWrapper(); wrapper.eq(WorkApply::getTeacherId, teacherId) .eq(WorkApply::getSemesterId, semesterId) .eq(WorkApply::getStatus, approved); ListWorkApply list workApplyMapper.selectList(wrapper);不需要写 XML不需要拼 SQL 字符串类型安全重构字段名也不怕出错。统计每个教师的总工作量时可以直接用 SQL 聚合QueryWrapperWorkApply query new QueryWrapper(); query.select(teacher_id, SUM(score) as total_score) .eq(semester_id, semesterId) .eq(status, approved) .groupBy(teacher_id); ListMapString, Object maps workApplyMapper.selectMaps(query);但这里要注意MP 的selectMaps返回的 Map 里字段名是数据库列名比如teacher_id不是驼峰。想要转成驼峰可以在配置里开启map-underscore-to-camel-case但 Map 模式下不生效需要手动转或者直接用MapKey注解配合实体。我一般建议聚合查询直接写自定义 SQL放在 Mapper 接口里用Select注解可读性反而更好。3. 前后端分离架构与核心模块实现3.1 SpringBoot2 的项目结构和包组织后端建议按模块分包而不是按层分包。按层分包是 controller、service、mapper 三个包平铺一旦业务多就乱。按模块分包比如teacher、work、audit、system每个模块里再有 controller/service/mapper/entity内聚性更好。这套系统的核心模块是work模块负责申报和审核。启动类上SpringBootApplication没什么好说的额外要加的是MapperScan(com.xxx.mapper)把 Mapper 接口扫描进去否则每个 Mapper 都要单独标Mapper。配置文件里注意 MyBatis-Plus 的逻辑删除配置mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: truelogic-delete-field配合实体类里的TableLogic注解删除时自动变成 update查询时自动过滤已删除数据这个设计对管理类系统太重要了因为工作量数据需要留痕不是删了就没了。3.2 Vue3 组合式 API 的前端组织前端用 Vue3最明显的感受是逻辑复用方便。以登录状态管理为例以前 Vue2 的 mixin 写起来不直观现在用组合式函数// useUserStore.js import { reactive, computed } from vue import { loginApi, logoutApi } from /api/user const state reactive({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }) export function useUserStore() { const isLoggedIn computed(() !!state.token) const login async (username, password) { const res await loginApi({ username, password }) state.token res.data.token localStorage.setItem(token, state.token) } const logout () { logoutApi() state.token localStorage.removeItem(token) } return { state, isLoggedIn, login, logout } }组件里直接调用const { state, isLoggedIn, login } useUserStore()清爽得多。注意这个state是模块级单例不同组件引用的是同一个响应式对象天然就是全局状态不需要额外引入 Pinia如果项目不复杂这种写法完全够用。页面组件推荐用script setup语法代码量精简三分之一。路由用 Vue Router 4和 Vue3 匹配。权限控制用路由守卫根据登录状态和后端返回的角色动态生成侧边栏菜单。这里有个细节后端返回的角色不要只存一个role字符串建议存一个数组因为有的老师可能同时兼任教研室主任和任课教师需要多角色处理。3.3 工作量申报与审核的完整流程工作量申报流程看起来简单实际上要处理好几个状态转换。我建议用状态机思想来约束避免用户乱点。状态枚举DRAFT草稿、PENDING待审核、APPROVED已通过、REJECTED已驳回。教师提交申报时先校验当前学期是否开放申报再校验申报分值是否在正常范围内然后状态改为PENDING。审核人打开待审核列表看到的是该学期所有待审核记录可以批量通过或单个驳回。驳回时必须填写理由这个理由会推送给前端教师端能看到修改后重新提交。后端接口设计上审核操作不能用简单的updateStatus一个接口搞定因为每审核一次都要写一条审核记录还要判断审核层级。比如业务规定工作量超过 50 分需要二级审核那审核接口里就要加逻辑if (apply.getScore() 50 apply.getAuditLevel() 1) { apply.setAuditLevel(2); apply.setStatus(PENDING); } else { apply.setStatus(APPROVED); }别小看这段逻辑很多系统就是在这里出问题要么审核层级不生效要么审核记录缺失。建议把状态流转逻辑单独抽成一个WorkFlowService不要堆在 Controller 里。3.4 统计报表与 Excel 导出管理系统最重要的输出是报表。教师端想看自己的课时汇总教务端想看各教研室的工作量对比。这里我用 MyBatis-Plus 的selectMaps写聚合或者直接用自定义 SQL。报表接口返回的数据结构建议为{ semester: 2024-2025-1, teacherId: T1001, teacherName: 张老师, teachingHours: 120, extraWorkScore: 15.5, totalScore: 135.5 }前端用 ECharts 画柱状图或饼图这部分不算难难点在于把后端返回的数据转成前端图表需要的格式比如 x 轴是教师姓名y 轴是总工作量。其实可以在后端就把数据按前端需要的结构组好前端只做一次赋值减少联调成本。Excel 导出建议用 EasyExcel不要用 Apache POI 直接写代码简洁且内存占用低。EasyExcel 的写法EasyExcel.write(response.getOutputStream(), WorkStatsVO.class) .sheet(工作量统计) .doWrite(dataList);注意响应头的设置Content-Type要设为application/vnd.openxmlformats-officedocument.spreadsheetml.sheetContent-Disposition要用attachment;filenamexxx.xlsx并且文件名要做 URL 编码不然中文名会乱码。这个坑我踩过不止一次。4. 部署环境搭建与数据库初始化细节4.1 MySQL8.0 的安装与配置要点很多人本地装的是 MySQL5.7直接导入 8.0 的 sql 文件可能报错因为字符集排序规则不一样。这套系统既然标题写了 MySQL8.0那就按 8.0 来。安装后检查默认认证插件如果是caching_sha2_password某些旧版数据库工具连不上需要改一下ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;但注意 MySQL8.0 其实支持 caching_sha2_password新版的连接驱动和工具都兼容不要为了迁就旧工具而降级。用 Navicat 或 DBeaver 的话升级到最新版即可。数据库初始化时先创建库再设置字符集CREATE DATABASE IF NOT EXISTS teacher_workload DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE teacher_workload; SOURCE /path/to/teacher_workload.sql;utf8mb4 必须用不能省因为老师姓名、课程名里可能有生僻字或 emojiutf8 不够用。4.2 SpringBoot 多环境配置开发、测试、生产环境的数据库地址、Redis 地址肯定不一样。SpringBoot 里用application.yml加application-dev.yml、application-prod.yml区分。启动时指定环境java -jar teacher-workload.jar --spring.profiles.activeprod数据库连接池建议用 HikariCPSpringBoot2 默认就是它不用额外配置。但有几点要调spring: datasource: url: jdbc:mysql://localhost:3306/teacher_workload?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000allowPublicKeyRetrievaltrue这个参数容易被忽略但 MySQL8.0 的 caching_sha2_password 认证下没有它有时会报错加上省心。serverTimezoneAsia/Shanghai是为了防止时间字段差 8 小时。4.3 前端打包与 Nginx 部署Vue3 项目打包命令是npm run build产物在dist目录。前后端分离部署时用 Nginx 托管前端静态文件反向代理后端接口。常见的 Nginx 配置server { listen 80; server_name your-domain.com; root /var/www/teacher-workload/dist; index index.html; location / { try_files $uri $uri/ /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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意proxy_pass http://127.0.0.1:8080/;后面带了一个/这样会把请求的/api/前缀去掉也就是说前端的/api/user/login会被转成http://127.0.0.1:8080/user/login。如果你后端的 Controller 映射是/user/login这样配就对了。如果后端接口都带/api前缀也可以不带斜杠。这个细节很容易因为配置不一致导致 404。5. 常见问题与排查技巧实录5.1 跨域问题排查前后端分离开发时前端在 5173 端口后端在 8080 端口请求必然跨域。解决方式有两种一是后端配置 CORS 过滤器二是用 Nginx 反向代理同源。开发环境推荐用后端的 CORS 配置一次性搞定Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns(*)和allowCredentials(true)同时使用时不能直接用allowedOrigins(*)否则会报错。这是 SpringBoot2.4 之后的版本要求。如果后端配置了 CORS 还是报跨域多半是拦截器拦截了 OPTIONS 请求或者登录过滤器里没有放行 OPTIONS需要在过滤器里加if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return; }5.2 日期字段读取相差 8 小时数据库存储时间Java 里拿出来的时间比正常晚 8 小时或者前端显示的时间不对大概率是时区问题。解决办法数据库连接 URL 里加serverTimezoneAsia/ShanghaiJackson 序列化时格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8实体类里的LocalDateTime字段会自动按照这个格式输出前端展示就没问题了。5.3 EsayExcel 导出中文文件名乱码导出文件名是中文时必须转码String fileName URLEncoder.encode(工作量统计.xlsx, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName);注意有些浏览器要求去掉号可以再处理一下fileName fileName.replaceAll(\\, %20);这个我实测是有效的否则 Chrome 和 Firefox 的显示行为不一样。5.4 MyBatis-Plus 分页查询不生效用 MP 的分页需要配置分页插件否则分页无效甚至查全表。在配置类里加Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }不配置的话Page对象查出来的数据是全量这个坑特别隐形尤其数据量小的时候根本发现不了。6. 我的实操心得与建议这套系统我前后大概花了两周时间完整撸通。第一周做后端第二周做前端并联调。最大的体会是管理类系统的核心不在技术炫技而在状态管理和边界处理。技术栈用得再花哨业务逻辑一团乱用户还是会在审核流程里迷路。有几个小建议给准备复现的朋友第一先跑通最小闭环。不要一上来就调日历控件、拖拽上传附件先把“教师填申报 → 审核人审核 → 教师看结果”这个链路跑通再把其他功能加上。第二数据库脚本一定要包含初始数据。不然新系统登录进去什么都没有用户根本不知道点哪里。至少要有一个管理员账号、一个教师账号、一个学期的数据、两三条申报记录。第三文档很重要。标题写了【含文档】但我在跟很多同学交流时发现大家拿到源码后最懒得看文档结果配置环境时各种踩坑。建议把环境搭建步骤截图放在文档里比写一百句 API 说明都有用。第四关于二次开发最大的扩展点在工作量规则配置。如果学校临时规定“每个老师每学期工作量上限是 300 分”或者“竞赛获奖加分系数从 1.5 调到 2.0”能通过页面改配置而不用改代码才是好系统。建议设计时把规则相关的字段都做成可配置。最后说一个我后来才发现的小问题工作量申报里加了附件功能比如上传 PDF 或图片但有些文件重名会覆盖。最好在存文件时加上时间戳或 UUID 前缀比如20250115_143200_张三_竞赛获奖.pdf这样既能避免覆盖又能通过文件名识别查问题也方便。这个细节源码里可能没有自己动手加上体验提升很明显。如果你也是从零开始搭建类似系统记住一句话先梳理业务再设计表然后写代码。反过来做十有八九会返工。 SEO 优化官网定制响应式建站教育培训建站