
1. 项目立项高校工作量统计为什么需要一个专属系统每年期末教务处和二级学院最头疼的事情之一就是教师工作量核算。我见过太多学校还在用Excel表格层层上报——系里填一张表、学院汇总一张表、教务处再合并一张表中间只要有一个数据填错、一个公式拖错工作量数据就要返工重来。更麻烦的是跨学院的兼课教师、不同职称的课时系数、实践环节和理论课程的折算比例这些规则散落在各个文件里靠人工判断难免有偏差。我参与设计这个基于 Spring Boot 的高校教师工作量管理系统时最初就是从这些实际痛点出发的。它不是一个简单的课时计数工具而是一套覆盖数据录入、规则计算、多级审核、结果统计与导出的完整业务流程平台。系统要解决的核心问题有三个一是把分散的填报数据集中化告别Excel传来传去二是把计算规则固化到系统里减少人为误差和争议三是让审核过程留痕院系、教务处各司其职最终数据可追溯。这个系统适合谁来参考如果你是在校学生准备做毕业设计这个题目从业务复杂度、技术栈选择到工作量拆分都非常合适——它不像电商系统那样业务发散边界清晰能完整展示Spring Boot Web开发的各项能力。如果你是高校信息化部门的技术人员这篇文章里的数据建模思路和规则引擎设计也值得借鉴。2. 需求梳理工作量计算规则到底有多复杂在写第一行代码之前我把高校工作量计算的相关规则和业务流程做了完整梳理。这一步非常关键因为工作量系统的难点从来不在增删改查而在规则的正确落地。2.1 工作量的构成维度高校教师的工作量通常由两大部分构成教学工作量和其他工作量。教学工作量是主体包括理论教学、实验教学、课程设计、毕业设计论文指导、实习实训指导等其他工作量包括监考、教学建设与改革项目、指导学生竞赛等。不同学校计分方式差异很大但大部分会落到这样一个基准公式上某门课程的工作量 课程计划学时 × 课程类型系数 × 班级规模系数 × 职称系数或分配系数听起来不复杂但真正落地时问题就来了。比如课程类型系数理论课可能是1.0实验课是0.8体育课是0.9双语课可能要乘1.5——这些系数不是写死在程序里的而是由教务处每年根据教学计划动态调整。再比如班级规模系数30人以内取1.0超过30人每增加10人系数增加0.05封顶1.3——这种分段函数在Excel里要靠嵌套IF实现维护起来极其痛苦。2.2 系统角色的权限边界我梳理了四类核心角色每类角色的关注点完全不同教师提交工作量申报数据查看自己的审核进度和最终核算结果。他们要的是操作简单、反馈及时不希望填写十几个字段。系部负责人审核本系教师的教学工作量处理争议数据。他们需要列表式审批能批量通过、批量打回还要能回复意见。教务处管理员维护系数规则、管理教学任务数据、审核跨院系工作量、最终确认全校数据。他们是系统的超级用户配置能力要求最高。系统管理员账号管理、角色分配、系统参数配置。权限控制必须是硬性的不能出现教师登录后进入管理后台的情况。系统必须从架构层面就把权限边界理清楚否则后面每加一个功能都要为谁能点这个按钮打架。2.3 一个容易被忽略的流程细节跨院系兼课我在需求调研阶段特别关注了跨院系兼课场景。很多学校有双肩挑教师或者基础课教师给多个学院上课这时候工作量数据需要同时被两个学院确认但最终归属又有明确约定。系统在设计时需要支持一条申报记录关联多个开课学院并设置工作量归属学院字段避免核算时重复计算或漏算。3. 技术选型Spring Boot 为核心但不止于 Spring Boot技术选型直接决定了系统的开发效率和后续维护成本。下面是我最终采用的技术栈和选择理由。3.1 后端Spring Boot 2.7 MyBatis-PlusSpring Boot 是这套系统的绝对核心。为什么选它而不是 SSM 手写整合因为 Spring Boot 的自动装配机制把 Spring MVC、数据源、事务管理、Jackson 序列化这些基础设施全部做好了我可以把精力集中在业务上。版本上我选的是 2.7.x而不是最新的 3.x。原因主要有两个一是很多高校的机房环境还在用 JDK 8Spring Boot 3 要求 JDK 17 起步兼容性压力大二是 MyBatis-Plus、Shiro 等生态组件对 Spring Boot 2.7 的适配最成熟踩坑最少。如果你的教务系统服务器是内网环境、不便频繁升级 JDK选 2.7 更稳妥。持久层用 MyBatis-Plus 是我的个人偏好。它的 LambdaQueryWrapper 能避免字符串硬编码字段名分页插件和代码生成器也能大幅提升效率。特别是工作量申报记录这种涉及大量条件查询的表MyBatis-Plus 的条件构造器比手写 XML 方便得多。这里还涉及一个 Spring Boot 的核心概念——自动装配。当时有个新同事问我为什么引入 spring-boot-starter-web 依赖后Tomcat 自动就启动了这就是 Spring Boot 的EnableAutoConfiguration在起作用。它会根据 classpath 下的依赖自动注册对应的 Bean比如检测到spring-webmvc就自动配置DispatcherServlet检测到DataSource就自动配置JdbcTemplate。理解这一点后出问题时你才知道该去哪里排查。3.2 前端Thymeleaf Bootstrap 还是前后端分离这是个老生常谈的纠结。我的建议是如果系统面向的是校内用户、访问量有限、不需要复杂交互用 Thymeleaf Bootstrap 做服务端渲染就够了。开发效率高不需要单独部署前端环境也不会遇到跨域问题。但如果你把系统定位为长期运营的学校级平台或者团队里有专门的前端前后端分离Spring Boot Vue会更合理。这篇文章以服务端渲染为主但会把数据接口设计成 RESTful 风格Controller 层返回 JSON 数据这样后续即使要改造分离架构后端代码改动量也不大。在 Controller 设计里我习惯把页面跳转和数据请求分开跳转页面返回 View 名称数据请求统一ResponseBody返回ResultT包装对象。3.3 安全与身份认证为什么用 Sa-Token 而不是 Shiro/Spring Security权限这块我踩过不少坑。Spring Security 功能强大但学习曲线陡峭配置类动辄上百行Shiro 相对轻量但已经多年不更新而且对 Spring Boot 2.7 的整合文档偏旧。后来我换成了 Sa-Token——它最大的优势是简单到极致登录、权限校验、Token 续期、踢人下线都是几行代码搞定。给一个直观的对比同样实现登录后访问接口// Spring Security 需要 WebSecurityConfigurerAdapter UserDetailsService PasswordEncoder // Shiro 需要 Realm ShiroFilterFactoryBean 各种配置 // Sa-Token 的做法 RestController public class LoginController { PostMapping(/login) public ResultString login(RequestBody LoginDTO dto) { User user userService.login(dto.getUsername(), dto.getPassword()); if (user ! null) { StpUtil.login(user.getId()); // 一行代码完成登录生成Token return Result.ok(StpUtil.getTokenValue()); } return Result.err(账号或密码错误); } GetMapping(/workload/my) public ResultListWorkloadRecord myRecords() { Long userId StpUtil.getLoginIdAsLong(); // 获取当前登录用户 return Result.ok(workloadService.listByTeacherId(userId)); } }拦截器配置也简单Configuration public class SaTokenConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkLogin())) .addPathPatterns(/**) .excludePathPatterns(/login, /assets/**, /error); registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkRole(admin))) .addPathPatterns(/admin/**) .excludePathPatterns(/admin/login); } }对于毕业设计级别的项目Sa-Token 的注解式鉴权SaCheckRole、SaCheckPermission能大幅减少代码量而且文档是中文的遇到问题查起来非常快。3.4 数据库MySQL 8.0 合理的索引设计数据库我选的 MySQL 8.0。它支持窗口函数、公用表表达式CTE在统计报表场景下非常好用。字符集统一 utf8mb4排序规则 utf8mb4_general_ci避免中文乱码和 emoji 存储问题。高版本的 Spring Boot 默认使用com.mysql.cj.jdbc.Driver驱动类名跟 5.x 不一样很多人第一次配置数据源报错就是因为这个。另外 8.0 的时区配置也更严格连接串里需要加serverTimezoneAsia/Shanghai。这些都是老生常谈但每次都能在论坛上看到有人问。4. 数据库设计工作量系统的表结构和设计思路工作量管理系统最核心的就是数据库设计。设计得合理后续所有计算和统计都顺畅设计得草率后期每个功能都要靠大量代码拼接。4.1 核心表一览表名用途说明关键字段t_user系统用户表id, username, password, real_name, role_type, dept_idt_dept院系/部门表dept_id, dept_name, parent_idt_course教学任务表course_id, course_name, course_hours, course_type, dept_idt_workload_rule计算规则表rule_id, rule_name, rule_type, express_content, enabledt_workload_record工作量申报记录表record_id, user_id, semester, course_id, coefficient_info, workload_hours, statust_workload_audit审核记录表audit_id, record_id, auditor_id, audit_result, audit_comment, audit_timet_workload_stat工作量汇总表stat_id, user_id, semester, total_hours, confirm_status其中t_workload_record是整个系统的核心status 字段我用一个 int 表示状态机0草稿1已提交待系部审核2系部通过待教务处审核3教务处通过4已打回需修改。审核记录表单独拆出来是为了保留完整的审核链路方便老师查看谁在什么时候打回了我的记录理由是什么。4.2 工作量记录表的关键设计很多初学着喜欢把课程类型系数班级规模系数这些计算结果直接存进冗余字段这没问题但我建议同时保存计算依据。比如额外增加一个calc_log字段TEXT类型记录的是类似这样的 JSON 字符串{ baseHours: 32, courseTypeCoeff: 1.2, classSizeCoeff: 1.05, titleCoeff: 1.1, total: 44.35 }这样做的价值在于当老师对计算结果有疑问时系统可以展示完整的算账过程而不是只丢给人一个数字。这个设计一度让我们的工作量争议处理效率提升很多因为 80% 的疑问直接在前端就能得到解答不用再找管理员后台查数据。4.3 规则表的动态化设计工作量系统最怕的是规则变化。今年实验课系数0.9明年改成0.85难道要改代码重新上线所以我设计了t_workload_rule规则表用规则表达式驱动计算。规则表主要存储两种类型的规则基础系数类例如理论课1.0实验课0.8上机课0.9本质是一组键值对。我把它存为express_content字段里的 JSON 字符串{theory:1.0,experiment:0.8,computer:0.9}管理员在后台改这个 JSON 即可。分段函数类例如班级规模系数我实现了一个简易的表达式引擎支持存储并解析类似base:30,coeff:1.0;step:10,increment:0.05,max:1.3这样的分段规则。计算引擎的工作流程是从教学任务表拿到课程的基准学时读取规则表组装出各类系数再计算出最终工作量写入t_workload_record。Spring Boot 的ConfigurationProperties可以把规则加载到内存缓存中但需要注意添加Scheduled定时刷新避免管理员改了规则但系统不生效。我见过有些项目图省事把系数直接写死在代码常量类里这样的系统一旦上线后续每次规则调整都是一次生产事故。规则表 配置页面的设计成本不高但收益很大。5. 后端核心实现工作量计算引擎和审核流程后端设计可以分三个核心模块来理解基础 CRUD 层、工作量计算引擎、审批流引擎。这里重点展开后两个。5.1 工作量计算引擎策略模式 工厂模式工作量计算虽然公式统一但课程类型繁多每种类型的系数来源和计算细节不完全一致。我用策略模式把不同课程类型的计算逻辑解耦。public interface WorkloadCalculator { // 返回支持的课程类型 String supportType(); // 计算工作量 double calculate(WorkloadContext context); } Component public class TheoryCourseCalculator implements WorkloadCalculator { Override public String supportType() { return theory; } Override public double calculate(WorkloadContext context) { double baseHours context.getCourse().getHours(); double typeCoeff context.getRuleValue(theory); // 从规则引擎读取 double classCoeff getClassSizeCoeff(context.getClassSize()); double titleCoeff getTitleCoeff(context.getTeacherTitle()); double total baseHours * typeCoeff * classCoeff * titleCoeff; // 保留两位小数避免浮点精度问题 return Math.round(total * 100) / 100.0; } } Component public class ExperimentCourseCalculator implements WorkloadCalculator { // 实验课可能会有分组折算逻辑例如每组人数上限 Override public double calculate(WorkloadContext context) { // 计算逻辑与理论课不同 return ...; } }然后用一个工厂类来分发Component public class WorkloadCalculatorFactory { Autowired private ListWorkloadCalculator calculatorList; private MapString, WorkloadCalculator calculatorMap; PostConstruct public void init() { calculatorMap calculatorList.stream() .collect(Collectors.toMap(WorkloadCalculator::supportType, c - c)); } public WorkloadCalculator getCalculator(String courseType) { WorkloadCalculator calculator calculatorMap.get(courseType); if (calculator null) { throw new ServiceException(暂不支持该课程类型: courseType); } return calculator; } }为什么用策略模式而不是写一堆 if-else因为新增一种课程类型比如线上线下混合课程时策略模式只需要新增一个实现类并注册为 Spring Bean不需要改动任何已有代码。这种可扩展性对高校系统特别重要因为教学形态变化太快了。Spring Boot 的Autowired ListWorkloadCalculator这种注入方式也很值得掌握——它会自动把容器中所有 WorkloadCalculator 的实现类收集到一起工厂初始化时构建映射关系后续任何时间新增实现类都能自动生效。5.2 审核流程简易状态机实现审核流程本质上是一个状态机。我在WorkloadAuditService里维护状态流转逻辑Service public class WorkloadAuditServiceImpl implements WorkloadAuditService { Override public void audit(Long recordId, Long auditorId, Integer auditResult, String comment) { WorkloadRecord record workloadRecordMapper.selectById(recordId); // 从当前状态判断可执行的操作 if (record.getStatus() 1 auditResult 2) { // 系部审核通过流转到教务处 record.setStatus(2); } else if (record.getStatus() 1 auditResult 4) { // 系部打回 record.setStatus(4); } else if (record.getStatus() 2 auditResult 3) { // 教务处最终通过 record.setStatus(3); } else { throw new ServiceException(非法的审核操作: 当前状态不能执行该操作); } // 保存审核记录 WorkloadAudit audit new WorkloadAudit(); audit.setRecordId(recordId); audit.setAuditorId(auditorId); audit.setAuditResult(auditResult); audit.setAuditComment(comment); audit.setAuditTime(new Date()); workloadAuditMapper.insert(audit); workloadRecordMapper.updateById(record); } }我没有引入 Activity、Flowable 这类工作流引擎原因很简单这个系统的审批链路是固定的系部-教务处不需要自定义流程节点状态机足够且更轻量。毕业设计答辩时你把为什么不用工作流引擎讲清楚反而是一个亮点——说明你懂技术选型的边界而不是什么技术热门用什么。5.3 批量操作与事务管理系部负责人经常要批量审核一个老师的多条申报记录或者批量通过一个专业的多门课程。这里有个典型的 Spring 事务坑批量操作必须保证全部成功或者全部回滚不能出现审核了 10 条、成功了 8 条、2 条失败但状态无法回退的情况。我使用注解式事务Transactional(rollbackFor Exception.class)来保证数据一致性。注意rollbackFor必须显式声明否则默认只回滚 RuntimeException而 ServiceException继承 Exception不会触发回滚。另外还要区分两类操作Override Transactional(rollbackFor Exception.class) public void batchAudit(ListLong recordIds, Integer auditResult, String comment) { for (Long recordId : recordIds) { audit(recordId, getCurrentUserId(), auditResult, comment); } }批量操作里如果有一条数据状态异常导致异常抛出事务会整体回滚前面已更新的数据也会还原。5.4 数据统计与报表SQL 聚合 Excel 导出工作量核算的最终产物是统计报表。教师端查看个人汇总教务处查看全校汇总同时需要导出发给人事处作为绩效依据。我的汇总实现思路是以 t_workload_record 为准按教师和学期分组聚合SELECT user_id, semester, COUNT(*) AS course_count, SUM(workload_hours) AS total_hours FROM t_workload_record WHERE status 3 GROUP BY user_id, semester导出 Excel 我用的 EasyExcel。网上很多人都推荐 POI但 POI 写 Excel 的代码量偏大内存消耗也高。EasyExcel 是基于 POI 封装的写复杂的表头和数据填充简洁很多PostMapping(/admin/export) public void exportWorkload(HttpServletResponse response) throws IOException { ListWorkloadStatVO list workloadStatService.getConfirmedStats(); ExcelWriter writer EasyExcel.write(response.getOutputStream(), WorkloadStatVO.class) .build(); WriteSheet sheet EasyExcel.writerSheet(工作量汇总).build(); writer.write(list, sheet); writer.finish(); }导出这块有个细节要提醒文件名中文乱码问题。Excel 导出文件名如果是中文需要做 URL 编码否则下载时会出现乱码。用URLEncoder.encode(fileName, UTF-8)解决但有些浏览器老的 IE 内核还会拆解引号如果测试环境是老旧系统干脆用英文文件名 前端映射中文显示名最省事。5.5 安全设计密码加密与权限校验教师工作量的数据敏感度不低涉及工资绩效。密码存储不能是明文我用 Spring Security Crypto 里的BCryptPasswordEncoder单独做加密工具即使引入 Sa-Token 做权限也不影响单独使用加密工具类。BCrypt 加密的特点是每次加密结果不同但校验时能通过算法比对安全性比 MD5 高一个量级。Component public class PasswordUtil { private final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); public String encode(String rawPassword) { return encoder.encode(rawPassword); } public boolean matches(String rawPassword, String encodedPassword) { return encoder.matches(rawPassword, encodedPassword); } }权限校验上除了 Sa-Token 的拦截器我还在菜单层面做了动态权限控制——通过表格t_role_menu存储角色和菜单的关联后端渲染菜单时根据角色过滤。这个用 MyBatis-Plus 的selectMenuByRole查询配合 Bootstrap 的侧边栏动态渲染即可。6. 前端页面与交互教师申报与审核列表的落地实现前端我用 Thymeleaf Bootstrap 4 jQuery 3这套组合足够应付管理系统的大多数页面需求。6.1 页面结构设计页面围绕两条主线展开录入与申报线教师端有一个工作量申报页选择学期、课程系统自动带出课程学时和类型教师只需确认或补充备注。提交前展示试算结果——这就是前面calc_log发挥作用的地方。审核与管理线系部和管理员有独立的审核列表页展示待审核记录支持详情打开、批量操作、填写审核意见。Bootstrap 的卡片Card布局很适合作审核详情页上半部分放申报信息下半部分放审核意见。教师端首页放一张学期工作量的汇总卡片一眼能看到本学期已确认学时数和未提交草稿数。6.2 与后端的交互fetch 还是 AjaxjQuery 的$.ajax在管理系统开发中依然很常见但原生fetch已经足够好用代码量还在可控范围。我会在公共 JS 文件里封装一个postData函数统一处理请求头、错误状态码和跳转async function postData(url, data) { const response await fetch(url, { method: POST, headers: { Content-Type: application/json, satoken: getCookie(satoken) }, body: JSON.stringify(data) }); if (response.status 401) { window.location.href /login; throw new Error(未登录); } return response.json(); }注意这里有个细节Sa-Token 默认从请求头读取 Token所以前端每次请求都必须在 header 里带上satoken。如果后续切换到 Cookie 模式需要修改 Sa-Token 配置这个视部署环境而定。6.3 试算功能前后端联动展示计算依据试算功能是教师页面最受欢迎的功能。教师在页面选择课程后前端把所选课程和教师信息发送到后端/workload/calc/preview接口PostMapping(/calc/preview) public ResultCalcPreviewVO preview(RequestBody CalcRequestDTO dto) { WorkloadContext context WorkloadContext.builder() .course(courseService.getById(dto.getCourseId())) .teacherTitle(userService.getTitleById(getCurrentUserId())) .classSize(dto.getClassSize()) .build(); double total calculatorFactory.getCalculator(context.getCourse().getType()).calculate(context); return Result.ok(new CalcPreviewVO(total, context.getCalcDetail())); }返回的CalcPreviewVO包含总工时和明细公式链前端渲染成一个只读的表格老师一眼看到32课时 × 理论课程1.2 × 班级人数系数1.05 40.32。这个功能其实代码量不大但对系统的好评度提升极为明显。7. 部署与运维打包、环境变量和常见故障排除系统开发完成后部署上线也是一门学问。校内系统通常部署在校内服务器或者一台虚拟机上资源有限我分享一下最务实的部署方案。7.1 打包配置Maven 多环境 Profile开发环境、测试环境、生产环境的数据库地址、端口、日志级别往往不同。我在application.yml里用spring.profiles.active指定当前环境每个环境一个配置文件# application-dev.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/workload_db?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8 username: root password: 123456 logging: level: com.example: debug # application-prod.yml server: port: 8080 spring: datasource: url: jdbc:mysql://192.168.1.100:3306/workload_db?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8 username: workload password: ${DB_PASSWORD} logging: level: com.example: info生产环境的数据库密码不硬编码用${DB_PASSWORD}从环境变量读取这样即使代码仓库泄露数据库也不会直接暴露。打包命令用 Maven 的 profile 参数mvn clean package -DskipTests -Pprod7.2 部署方式jar 包 vs 外置 TomcatSpring Boot 内置 Tomcat打成可执行 jar 包是最推荐的部署方式。Linux 服务器上用nohup java -jar workload-system.jar 启动即可。如果需要注册为系统服务可以写一个 systemd 服务文件[Unit] DescriptionWorkload Management System Afternetwork.target [Service] Userworkload ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /opt/workload/workload-system.jar Restartalways SuccessExitStatus143 [Install] WantedBymulti-user.target这里-Xms和-Xmx是 JVM 堆内存设置。高校系统并发量通常不大256M 到 512M 足够不要盲目把堆内存调得过大否则容易造成系统资源紧张。关于怎么将 Spring Boot jar 反编译成项目这个网上热门问题我多说一句如果你拿到一个没有源码的 jar想反编译成可用的工程推荐用cfr或fernflower这类工具。它们能还原出 Java 代码但无法还原 Maven 依赖和配置文件的原始结构只能作为研究工具不适合作为拿回源码继续开发的路径。正规的团队协作应该把源码管理在 Git 上而不是靠反编译。如果你在维护一个没有源码的老项目更务实的做法是基于现有 jar 做接口层面的兼容开发。7.3 部署后常见问题排查我把自己在多个项目中遇到的部署问题整理成一个对照表方便大家排查现象可能原因解决思路端口被占用服务器上已有其他服务占用 8080修改server.port或用netstat -anp | grep 8080查占用进程数据库连接失败数据库 IP、端口、账号、密码不匹配先 ping 通数据库主机再检查服务是否启动最后确认用户权限页面中文乱码字符集配置不一致检查 MySQL 表字符集是否为 utf8mb4连接串是否带characterEncodingutf8静态资源 404拦截器把静态资源拦截了在 Sa-Token 拦截器excludePathPatterns中加入/assets/**、/js/**上传文件过大报错Spring Boot 默认单文件上传限制 1MB在配置中设置spring.servlet.multipart.max-file-size7.4 服务器环境安全提示校内系统常部署在校园网内但安全依然不能麻痹。至少做到以下几点MySQL 不要用 root 远程登录创建专用账号并只授予必要库表权限修改默认的 3306 端口降低被扫描概率系统后台登录增加验证码防止暴力破解日志按日期切割避免磁盘被日志占满。这些操作不需要很高的成本但能挡住绝大多数常见风险。8. 编码中的细节陷阱事务、浮点精度和 MyBatis-Plus 的坑这部分内容来自我的实际踩坑记录每一个都曾经在深夜折磨过我。8.1 浮点运算精度问题工作量计算涉及课时整数乘以各种系数小数如果直接用double运算16.0 * 1.05 可能得到 16.799999999999997。虽然大多数时候四舍五入后结果正确但一旦出现 0.01 的误差老师核算时就是一笔说不清的糊涂账。我的规范是涉及金额、课时、工时的数值一律用 BigDecimal 计算。但 BigDecimal 也不是随便用new BigDecimal(1.05)得到的值不是精确的 1.05必须用BigDecimal.valueOf(1.05)或在构造时传入字符串1.05。计算工作量时系数从规则表读出来也是 String 类型直接用new BigDecimal(ruleValue)构造最安全。8.2 批量审核时出现状态已被更新的并发问题审核人员往往不止一个假如系主任和教务管理员同时打开同一条记录一个人点了通过另一个人也点了通过后者的提交就会覆盖前者的状态造成审核记录与最终状态不一致。解决思路是给状态更新加一个乐观锁。MyBatis-Plus 支持Version注解public class WorkloadRecord { Version private Integer version; }更新时带上版本号条件如果版本号不匹配MyBatis-Plus 会抛出异常事务回滚。我在审核接口里捕获这个异常提示该记录已被其他人处理请刷新后重试。这段代码我强烈建议加上因为实际使用中审核并发是必然事件。8.3 分页插件的配置顺序MyBatis-Plus 分页插件必须在填充器MetaObjectHandler之后注册否则可能因为 Bean 初始化顺序问题导致分页查询失灵。配置分页插件时PaginationInnerInterceptor需要指明数据库类型为 MySQLConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); // 防止一次查出全表 interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(500L)这个配置很多教程不会写但非常重要——防手滑、防恶意请求。没有它别人调用分页接口时把size改成 10000你的数据库或许还能扛改成 1000000 问题就来了。给它一个上限一劳永逸。8.4 逻辑删除、字段填充与代码生成器工作量记录不允许物理删除涉及审计要求我用 MyBatis-Plus 逻辑删除实体类加TableLogic注解数据库加deleted字段。逻辑删除的意义在于管理员误删后还能找回同时保留完整的申报数据链这对期末对账非常有用。查询时 MyBatis-Plus 会自动追加deleted 0条件不需要手动处理。另外推荐配置字段自动填充。创建时间、更新时间这种字段如果每个 Service 都手动setCreateTime(new Date())代码重复且容易漏掉。用 MetaObjectHandlerComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, Date.class, new Date()); this.strictInsertFill(metaObject, updateTime, Date.class, new Date()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, Date.class, new Date()); } }实体类对应的字段加上TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)。8.5 关于生成 CRUD 代码的效率问题很多人在 Spring Boot 项目里手写 Controller、Service、Mapper效率低还容易出低级错误。MyBatis-Plus 提供代码生成器一键生成实体、Mapper、Service、Controller 的基础代码。我的习惯是生成后手动改造 Service 层——因为生成代码只覆盖标准单表操作工作量规则引擎、审核状态流转、统计报表这些核心业务逻辑完全要自己写。代码生成不是偷懒而是把时间留给真正有难度、有业务价值的部分。这与抄袭是两回事工具提高的是重复劳动的效率核心逻辑仍然需要自己理解和掌控。9. 测试用例设计不把工作量算错是底线工作量系统是数据敏感型系统测试用例的设计比其他管理系统要求更高。我总结几类必须覆盖的关键场景。9.1 正常计算类用例理论课标准班级30人讲师职称验证工作量 课时 × 1.0 × 1.0 × 1.0。双语理论课45人班级副教授职称验证叠加系数和多段阶梯系数是否正确。实验课分组折算场景比如课程计划 20 学时分 4 组每组 15 人验证折算逻辑。9.2 状态流转类用例教师提交草稿后不能再次编辑只能撤回。系部审核通过后教务处才能看到记录系部审核打回后教师需要重新编辑后再次提交状态必须回溯到待系部审核。教务处审核通过的记录任何角色不得在界面上修改或删除只能走退回重算流程。9.3 异常边界类用例班级规模系数封顶逻辑60人和100人班级系数应该一致封顶1.3。未配置规则的课程类型计算时提示明确的异常信息不能强行给个 0。批量选中的 50 条记录中有 1 条状态已变化被其他人审核过整个批次事务回滚不能出现部分成功部分失败。9.4 性能类用例虽然校内系统并发不高但期末高峰期可能存在十几位审核员同时操作几十位老师同时录入。建议在部署前用 Jmeter 做一个 100 并发的基础压测确认登录接口和查询接口响应时间可控。如果发现数据库查询慢优先检查核心查询条件如user_id semester status是否建立了联合索引。这是我见过最多的性能瓶颈点一个正确的联合索引比任何缓存都管用。10. 系统验收之后我的复盘与改进建议系统上线运行一个学期后我复盘了整个过程。最初想解决的核心痛点——Excel 传来传去、规则不统一、核算有争议——确实都解决了。但在实际使用中也暴露了一些设计阶段没有充分预判的问题这些都是后续迭代的方向。第一个问题是数据导入导出需求远比预想强烈。每学期初教务系统里已有的教学任务数据需要导入到工作量系统避免教师重复录入。我建议下一步增加 Excel 模板导入功能但要做严格的模板校验不允许脏数据直接入库。另一个方向是打通学校已有的统一身份认证教师直接用校园账号登录而不是在每套系统里记住一套密码。第二个问题是移动端适配。寒假期间老师在家申报工作量用手机打开桌面版页面体验很差。虽然高校内部系统不一定需要做完整的 App但至少要把 Bootstrap 栅格布局做得更弹性关键页面申报页、审核页对手机端友好一些这个工程量不算大收益却很直接。第三个设计改进是规则的版本化管理。现在规则表虽然支持动态修改但缺少版本记录——如果本学期已经计算了 50% 的课时教务处临时调整了某个系数已算的数据要不要重算、按哪个版本重算目前系统里没有明确的机制。建议下一步给每条规则增加生效开始日期和生效结束日期计算时始终使用当前学期对应时间的版本这样才能彻底解决规则变更带来的历史数据不一致问题。还有一个看似不起眼但实际很重要的改进消息通知。以前一条申报记录被系部打回时教师根本不知道要自己进系统刷新才看到。下一步计划接入站内信或者企业微信通知审核状态一旦变化就主动推送给相关教师。代码量不大但对使用体验的提升很明显。从技术上说这套基于 Spring Boot 的 Web 系统并不炫技用的都是主流、稳重的方案Spring Boot 负责后端整体架构MyBatis-Plus 负责数据持久化Sa-Token 负责认证授权Thymeleaf 加 Bootstrap 负责页面展示。真正花心思的地方在于业务规则的建模和审核流程的设计。对于准备做信息管理系统这类毕业设计或者校内项目的朋友我的建议是把规则梳理清楚、把表结构设计好、把状态流转管明白这三个层面做扎实了系统自然就立得住。技术手段永远是为业务流程服务的这句话在做这类系统时体会特别深。