☰
SpringBoot2+Vue3+MyBatis-Plus教师工作量管理系统设计与实现
2026/10/11 6:37:14 网站建设 项目流程

去年年底接了个高校的信息化项目,要做一套教师工作量管理系统。需求听上去不复杂,无非就是把教师每学期承担的教学任务、课时量、工作量系数这些数据管起来,让教务处的老师不用再靠Excel来回传递。但真动手之后才发现,从需求梳理到技术选型,再到最后部署上线,中间全是细节。今天把这个项目的完整实现过程拆出来,从技术栈选择、数据库设计、核心代码写法到部署排查,一条线讲清楚,给准备做同类系统的同学一个可参考的样板。

这个系统的技术组合是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,前后端完全分离,源码附带完整文档,覆盖了权限管理、工作量申报、审核流程、统计报表这些核心模块。如果你正在学Java Web全栈,或者接了类似的实训课设/毕设/小型管理项目,这套东西的架构思路和代码细节可以直接拿来落地。

1. 需求拆解与整体设计思路

1.1 教师工作量管理到底在管什么

开工之前先把业务搞清楚,这一点比选什么技术栈都重要。教师工作量管理,表面看是统计教师上了多少节课,实际上是一套牵扯到课时计算、系数折算、审核确认、绩效核算的完整业务链。

我之前走访了几个教务处的老师,总结下来核心痛点就三个。一是数据分散,每个学院各自统计,表格格式五花八门,期末汇总的时候光是统一格式就要耗掉两三天。二是计算口径不统一,同一个实践环节,有的学院按1.0折算,有的按1.2折算,年底核算工作量的时候争议不断。三是审核流程不透明,教师申报之后不知道卡在哪个环节,只能反复打电话问教务员。

所以这个系统在设计的时候,业务上必须回答三个问题:工作量由谁录入、按什么规则计算、经过什么流程确认。

我的方案是分角色、分流程来处理。教师端负责录入个人工作量申报单,选择本学期的课程或实践环节,系统自动带入基本信息,教师只需要填写实际授课周数、周课时数。学院教务员负责初审,核对课时和系数是否填写合理。教务处管理员负责终审和汇总,确认之后的数据进入统计报表,可以按学院、按教师、按学期多维度查询。整个流程有状态流转,每一步都有操作日志,谁在什么时候做了什么操作全部留痕。

1.2 技术选型:为什么是SpringBoot2 + Vue3 + MyBatis-Plus这套组合

很多同学纠结技术栈,觉得越新越好。我的观点是,项目选型要看场景。高校内部的业务管理系统,特点是并发量不大但业务流程复杂、权限层级多、后续可能有多个学院的信息化系统要对接。这种情况下,稳定、成熟、资料多的技术栈才是最优先的。

后端用SpringBoot 2.7.x搭配JDK8。SpringBoot2到现在依然是Java Web领域应用最广的版本,网上遇到的问题解决方案也最全。JDK8语法在团队协作时基本没有学习成本,lambda表达式和Stream用法大家都很熟。不用JDK17的原因是项目要求部署在一台老一点的服务器上,JDK8对系统资源的占用更可控。

持久层选了MyBatis-Plus,而不是纯MyBatis或者Spring Data JPA。原因很直接:这个系统里有大量单表CRUD操作,比如教师信息维护、学院信息管理、学期配置等,MyBatis-Plus的BaseMapper拿到手就能用,省去写一堆重复XML的时间。而真正复杂的查询,比如按学院统计工作量汇总数据、多表联查审核记录,又可以写自定义SQL放在Mapper.xml里,灵活性不受影响。JPA虽然写起来也快,但遇到复杂统计查询时需要写JPQL或者原生SQL,调试起来不如MyBatis直观。

前端用Vue3 + Element Plus + Vite。Vue3的组合式API(Composition API)在处理多角色页面时比Vue2的选项式API更清爽,尤其是一个页面要同时兼容教师端和管理员端的不同操作按钮时,逻辑复用性明显更好。Element Plus的表格、表单、弹窗组件覆盖了这类管理系统的绝大多数界面需求,不需要再引额外的UI库。

数据库用MySQL8.0。8.0相比5.7的改进很实在,窗口函数做排名统计特别方便,JSON类型支持配合WorkloadDetail这类灵活的明细结构也很好用。而且8.0的默认字符集已经是utf8mb4,不需要像5.7那样安装时还要专门配置。

1.3 系统功能模块划分

整个系统按业务域拆成五个模块,每个模块对应后端一个独立的Controller层逻辑,前端对应一个菜单分组。

第一个是系统管理模块,负责用户管理、角色管理、菜单权限分配。这里用的是经典的RBAC模型,用户挂角色,角色挂菜单权限,通过拦截器校验接口访问权限。第二个是基础数据模块,维护学院信息、专业信息、教师档案、课程库。第三个是工作量管理模块,这是核心,包含工作量申报单的创建、提交、审核、退回、重新提交的完整生命周期。第四个是统计报表模块,支持按学期、按学院、按教师个人汇总工作量数据,支持导出Excel。第五个是系统工具模块,包含操作日志、数据备份等功能。

权限设计上,我不推荐把角色写死在代码里判断。实践做法是给每个接口配置权限标识,比如workload:apply表示工作量申报权限,workload:audit表示工作量审核权限。用户的菜单和按钮,根据其角色关联的权限集合动态渲染,这样如果后续想增加一个"学院院长"角色来查看本学院所有教师的工作量汇总,只需要在数据库里给新角色分配对应权限,不用改动后端代码。

2. 数据库设计与工作量计算规则

2.1 工作量计算的核心逻辑

在设计表结构之前,得先把工作量的计算规则定清楚。不同学校的规则差异很大,我参考了一个通用性较强的口径,方便后续按学校情况调整。

工作量由三个要素决定:课程类型、课时数、折算系数。比如一门专业核心课,每周4课时,共16周,理论课的系数是1.0,那么这门课的工作量就是 4×16×1.0=64 学时。同一门课如果是实践环节,系数可能是1.2,那工作量就变成了76.8学时。还有一种情况是合班授课,一个老师同时给两个班上课,有的学校系数会乘以1.1或1.2,有的学校不折算,这个完全看学校的文件规定。

所以数据库里不能只存一个工作量数字,而是要存足够的原始明细,让系统能够按照配置的计算公式动态算出门课的工作量。我把这个设计成了workload_calculate_config表,字段包括课程类型、工作量类型、折算系数、是否启用。这样每次规则调整,改数据库配置就行,代码层面不用动。

2.2 核心表结构设计详解

整体数据库我设计了9张表,这里挑最核心的4张讲清楚设计思路。

教师表teacher_info,主键自增,关联用户表sys_user的user_id,用来绑定登录账号。冗余了学院ID、职称、入职年份。学院ID冗余是为了查询教师工作量列表时减少一次联表,这种可接受范围内的冗余能显著提升列表页的响应速度。

工作量申报单表workload_apply,这是核心业务表。字段包括申报学期、教师ID、申报状态、学院审核意见、教务处审核意见、提交时间。状态字段是关键,我设计成了tinyint类型,0草稿、1待学院审核、2学院退回、3待教务处审核、4教务处退回、5审核通过。这种状态机的设计,结合update_time时间戳,就能完整还原一张单子的流转过程。

工作量明细表workload_apply_detail,与申报单主表是一对多关系。每条明细包括课程名称、课程类型、授课班级、计划周数、周课时、折算系数、工作量结果。具体计算出来的工作量数值也会冗余存储在这张表里,便于后续统计时直接SUM,不用每条记录都重新计算。

学期配置表semester_config,存学期名称、开始日期、结束日期、是否当前学期。这个设计很实用,因为很多查询和申报操作都限定在当前学期,通过这张表可以统一管理学期切换的逻辑,不会出现在代码里写死学期编号的情况。

2.3 工作量认定流程的状态机设计

刚才提到申报单有状态流转,这里我详细说一下状态机的处理。

我在代码里用一个枚举类WorkflowStatusEnum来定义这些状态和允许的转换路径。比如草稿状态下只能执行提交或删除,提交后进入待学院审核,学院审核通过后进入待教务处审核,学院或教务处任何一个环节退回,状态就变成退回修改。退回状态下教师修改完重新提交,状态再次进入待学院审核。

这里有个实操细节特别容易踩坑:审核操作必须用乐观锁控制并发。教务员A和教务处管理员B可能同时打开一张单子,A点了通过,B也点了通过,如果不加锁,就会出现状态被覆盖的问题。我的做法是在workload_apply表加一个version字段,每次更新时检查version值,不匹配就报错提示"单据状态已变更,请刷新后重试"。这个防并发问题的方法简单有效,比用悲观锁性能更好,也更符合这个系统的并发场景。

3. 后端核心代码实现与关键配置

3.1 SpringBoot2 + MyBatis-Plus的项目骨架搭建

项目结构用的是标准的多模块单工程结构,后端代码按controller、service、mapper、entity、dto、vo分层。实体类用MyBatis-Plus的注解标注表名和主键策略。

application.yml里几个关键配置我贴一下,都是实测踩过坑之后的经验值。

server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/workload_db?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的密码 hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.workload.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这几个配置里,MySQL8.0的驱动类和时区参数是重点。serverTimezone=Asia/Shanghai必须加,否则时间字段的读写会差8小时。allowPublicKeyRetrieval=true这个参数很多人会漏掉,MySQL8.0默认使用caching_sha2_password认证插件,在JDBC连接时如果没配置这个参数,经常会报Public Key Retrieval is not allowed错误。

MyBatis-Plus的逻辑删除配置我也单独说明一下。逻辑删除就是数据不真删,而是通过一个deleted字段标记为已删除。做教师工作量这种系统非常推荐,尤其是申报单这种有审计需求的业务表,一旦误删了数据,物理删除的恢复成本极高。配置之后,MyBatis-Plus会自动在查询语句后面加上AND deleted = 0,更新语句也会带条件,不需要自己手动写。

3.2 分页插件的配置和常见分页坑

工作量列表页、审核列表页、统计列表页都需要分页查询,MyBatis-Plus的分页插件配置如下。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(500L); paginationInnerInterceptor.setOverflow(false); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }

设置MaxLimit(500L)是我加的一个保护机制。管理系统的列表页虽然有分页,但如果有人直接调接口传个size=999999,一次查出来的数据量过大会拖垮数据库。限制单次最大查询500条之后,前端列表再怎么翻页也在可控范围。Overflow(false)的意思是不允许页数溢出,如果请求的页码超过总页数,直接返回空列表而不是自动跳到最后一页,这样对业务数据的语义更安全。

分页还有一个坑:如果分页查询里带了自定义的联表SQL,MyBatis-Plus的Page对象拿到total值有时会不准确。MyBatis-Plus的默认优化器会尝试把原SQL包一层select count(*) from (...) total,但如果你SQL里有GROUP BY,这个count会直接统计分组后的行数而不是原始数据的行数。这种情况下要自己写专门的count查询,用page.setTotal()手动设置。工作量统计报表那个模块就是这种情况,我在Mapper里单独写了selectWorkloadStatisticsCount方法,把嵌套的汇总SQL单独查一次count,确保total正确。

3.3 登录认证与权限控制的落地方式

这个系统选的是经典方案:Token令牌 + 拦截器 + RBAC权限标识。

用户登录成功后,后端生成一个UUID字符串作为token,通过Redis存储,设置过期时间为2小时。前端每次请求在header里带上Authorization: Bearer 你的token,后端拦截器解析token,从Redis里拿到用户ID,再查出用户的权限集合放到ThreadLocal里供业务层使用。

拦截器的实现核心代码如下:

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StrUtil.isBlank(token) || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } String realToken = token.substring(7); LoginUser loginUser = redisUtils.get(RedisKeyConstants.LOGIN_TOKEN_KEY + realToken); if (loginUser == null) { throw new BusinessException(401, "登录已过期,请重新登录"); } // 校验接口权限 HandlerMethod handlerMethod = (HandlerMethod) handler; RequiresPermission requiresPermission = handlerMethod.getMethodAnnotation(RequiresPermission.class); if (requiresPermission != null && !loginUser.getPermissions().contains(requiresPermission.value())) { throw new BusinessException(403, "无权限访问"); } UserContext.set(loginUser); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }

第一行对OPTIONS请求的大写判断和处理是必须的。前后端分离项目必然存在跨域,浏览器会在正式请求前发一个OPTIONS预检请求,这个请求不会带业务token,如果不直接放行,前端就会因为拦截器拦截预检请求而频繁报跨域错误。很多人调试的时候怎么都跨不过去,就是忘了处理这一层。

自定义注解RequiresPermission标记在Controller方法上,形如@RequiresPermission("workload:audit"),拦截器里有这个注解就校验权限。这种写法比较直白,业务开发时扫一眼Controller方法就能看出接口的权限要求。需要注意,权限标识的设计最好遵循模块:操作的格式,工整且不容易重复。

3.4 工作量登记与审核接口的业务逻辑

工作量申报这块,教师在前端填好明细后提交保存。由于明细可能有多条,主表和明细表需要在一个事务里同时写入,我直接设计了DTO接收对象,一张单子的所有明细数据一次性传过来。

Service层核心方法是submitApply,逻辑伪码如下:

@Transactional(rollbackFor = Exception.class) public void submitApply(WorkloadApplyDTO dto) { // 1. 校验该教师在当前学期是否已有未结束的申报单 LambdaQueryWrapper<WorkloadApply> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(WorkloadApply::getTeacherId, dto.getTeacherId()) .eq(WorkloadApply::getSemesterId, dto.getSemesterId()) .ne(WorkloadApply::getDeleted, 1) .in(WorkloadApply::getStatus, Arrays.asList(0, 1, 3)); Long count = applyMapper.selectCount(wrapper); if (count > 0) { throw new BusinessException("当前学期已有进行中的申报单,请勿重复提交"); } // 2. 插入主表数据,状态为待学院审核 WorkloadApply apply = new WorkloadApply(); apply.setTeacherId(dto.getTeacherId()); apply.setSemesterId(dto.getSemesterId()); apply.setStatus(WorkflowStatusEnum.WAIT_COLLEGE_AUDIT.getCode()); applyMapper.insert(apply); // 3. 批量插入明细,逐条计算工作量 List<WorkloadApplyDetail> details = dto.getDetails(); details.forEach(detail -> { BigDecimal workload = calcWorkload(detail); detail.setWorkload(workload); detail.setApplyId(apply.getId()); }); detailService.saveBatch(details); // 4. 记录操作日志 logService.addLog("提交工作量申报", "申报单号:" + apply.getId()); }

这段代码解决了一个容易忽略的业务问题:一个学期教师只能有一张进行中的申报单。如果允许教师分多次提交多张单子,审核人员面对的状态就混乱了。所以提交前先查是否存在状态为草稿、待学院审核、待教务处审核的单子,存在就拒绝新增。这种逻辑放在程序员的位置上看很简单,但需求方经常不会主动提,需要开发者在设计数据库时就预判到。

calcWorkload的计算逻辑也值得一提,它根据明细中的课程类型和学期配置去查workload_calculate_config表,拿到折算系数,然后周课时 * 计划周数 * 系数,结果保留两位小数。这是一个独立的私有方法,方便后续如果计算规则变化,只改这一个方法即可。

审核接口和提交接口类似,但多了并发控制和操作日志记录。审核时把主表status改成对应状态,同时插入一条audit_record表记录,包含审核人、审核意见、审核时间。这条记录表很重要,后续如果出现工作量争议,凭操作日志可以还原完整的审核链条。

4. 前端Vue3实现与接口联调细节

4.1 前端项目结构和请求封装

前端用Vite构建的Vue3项目,目录结构是views(页面)、router(路由)、store(状态管理)、api(接口请求)、utils(工具)、components(公共组件)。其中api目录每个业务模块独立一个文件,例如workload.js、system.js,这样后端接口变动时改动范围小,不牵扯其他模块。

axios请求封装是我每次开发都要最先完成的部分。统一在请求拦截器里加上token,在响应拦截器里统一处理错误码。这个系统里后端统一返回Result对象,结构是{ code, message, data }。code为200才是成功,401跳转登录页,403提示无权限,其他code弹出错误消息。

这个统一包裹格式强烈建议不要省。如果不统一返回格式,前端每个接口调用都要自己判断返回数据是不是有效,写起来繁琐,出错概率也高。统一之后,普通的请求代码就是:

export function getWorkloadPage(params) { return request({ url: '/workload/apply/page', method: 'get', params }) }

页面里调用时直接const res = await getWorkloadPage(params),res.data就是后端返回的数据部分。

4.2 Vue3组合式API在页面里的写法

页面开发里,教师工作量申报页面是最复杂的,它既要支持明细行的动态增删,又要支持草稿保存和提交两种操作。这个场景用Vue3组合式API的reactive处理非常顺手。

核心逻辑大致如下:

const formRef = ref(null) const detailList = ref([]) // 添加明细行 function addRow() { detailList.value.push({ courseName: '', courseType: '', className: '', planWeeks: 0, weekHours: 0, coefficient: 1.0 }) } // 删除明细行 function removeRow(index) { detailList.value.splice(index, 1) } // 保存草稿 async function saveDraft() { const formData = { teacherId: userStore.userId, semesterId: currentSemester.id, details: detailList.value } await saveApplyDraft(formData) ElMessage.success('草稿保存成功') } // 提交审核 async function submitApply() { await formRef.value.validate() // 确认弹窗 await ElMessageBox.confirm('提交后将进入学院审核流程,是否继续?', '提示', { type: 'warning' }) const formData = { teacherId: userStore.userId, semesterId: currentSemester.id, details: detailList.value } await saveApplySubmit(formData) ElMessage.success('提交审核成功') router.push('/workload/myList') }

这里有几个经验点。第一,动态增删行数据的时候,不要用reactive包整个数组再用索引修改,Vue3的响应式代理在数组索引赋值时会有性能问题,用ref包数组然后直接push和splice是最稳的。第二,ElMessage的引入方式在Element Plus里要用全量引入,按需引入时样式容易丢。如果你用unplugin-vue-components做按需引入,别忘了配置ElMessage和ElMessageBox的样式文件手动引入。

4.3 动态菜单和按钮权限的渲染

前端菜单是根据登录用户的权限动态生成的。用户登录后,后端在返回登录信息的同时会返回一个权限标识列表permissions,以及一个符合当前用户角色的菜单树menus。前端拿到菜单数据后在路由守卫router.beforeEach里动态注册路由。

按钮级别的控制,我封装了一个自定义指令v-permission,使用方式:

<el-button v-permission="'workload:audit'" type="primary">审核</el-button>

指令的注册代码如下:

const permissionDirective = { mounted(el, binding) { const required = binding.value const userStore = useUserStore() if (!userStore.permissions.includes(required)) { el.parentNode && el.parentNode.removeChild(el) } } }

用指令比在模板里写v-if判断要干净得多。页面上一堆v-if="userStore.permissions.includes('xxx')",可读性很差,指令一处定义,全页面复用。但注意指令的mounted钩子只在元素创建时执行一次,如果权限数据是异步加载的,要改成在update钩子里再做一次校验,确保权限数据到位后还能再处理一次。

这个系统由于菜单和页面权限都从后端动态返回,实现了所谓的前后端权限联动。假如后续新增了一个"督导查看"角色,只需要在管理后台给该角色配置菜单权限,前端登录后就会自动看到对应的统计页面,不需要重新发版。

4.4 报表可视化与Excel导出的实现

统计报表页面是这个系统里最直观体现"系统价值"的模块。教务处的人打开页面,选学期选学院,就能看到每个教师的工作量汇总排名。我用了Element Plus的表格展示数据,同时配合ECharts画了柱状图和饼图,分别展示各学院工作量对比和工作量类型分布。

图表这块要注意一个细节:ECharts初始化的时候,如果容器还没有宽高数据,或者Tab页切换后容器被重新渲染,图表会显示空白。解决方案是在nextTick里初始化,并在容器大小变化时调用chart.resize()。如果报表页面是放在Tab标签页里的,还要在Tab切换事件里手动触发resize,否则从别的Tab切回来,图表会挤压成一条线。

Excel导出是通过后端接口实现的,没有用前端插件直接生成。后端采用Apache POI生成xlsx文件,设置好响应头Content-Disposition: attachment; filename=xxx.xlsx,前端用window.open或a标签下载触发即可。这里同样有个小坑:文件名如果包含中文,需要做URL编码处理,否则下载下来的文件名会是乱码。我封装了一个downloadFile工具函数,统一处理blob类型和文件名解析,把Content-Disposition里的filename用decodeURIComponent解析出来再赋值给a标签的download属性。

5. 系统部署与问题排查实录

5.1 本地开发环境搭建步骤

拿到源码之后,最快跑起来的顺序是这样的。

第一步准备环境,安装JDK8、Maven3.6+、Node16+、MySQL8.0。第二步初始化数据库,用源码里的sql/init.sql脚本建库建表,这个脚本包含了所有表结构和初始数据,包括管理员账号。建议用Navicat或命令行直接执行,执行之前确认数据库版本是8.0,5.7有部分语法不兼容。

第三步启动后端。修改application.yml里的数据库账号密码,在项目根目录执行mvn spring-boot:run,或者用IDEA打开项目后直接运行启动类。启动成功后控制台会打印端口号,默认8080。

第四步启动前端。在frontend目录下执行npm install,装依赖可能会花几分钟,如果网络不好可以配置淘宝镜像源。依赖装完后执行npm run dev,Vite默认为5173端口,浏览器访问后在登录页输入管理员账号密码。

如果前后端要联调,需要在前端代码里配置代理。Vite的vite.config.js里配置server.proxy,把/api前缀的请求代理到http://localhost:8080,同时把后端context-path里的/api前缀去掉,或者让后端保留然后前端请求时带双前缀。我在联调时踩过一次这个双前缀的坑,后来统一方案是后端设context-path=/api,前端代理时不再额外拼路径,只在axios请求前缀里保留/api。

5.2 部署到Linux服务器的步骤

局域网部署比云服务器简单,但流程基本一致。我用一台CentOS7.9服务器做演示。

后端部分,用mvn clean package -DskipTests打jar包,生成的文件在target目录下。服务器上先装好JDK8和MySQL8.0,数据库脚本执行完后,使用nohup java -jar workload-server.jar --spring.profiles.active=prod > /logs/workload.log 2>&1 &启动服务。--spring.profiles.active=prod会加载application-prod.yml里的生产环境配置,这套配置里数据库密码、日志级别、文件路径都是独立设置的,和开发环境隔离。

前端部分,在本地或服务器上执行npm run build,生成dist目录。然后用Nginx做静态文件服务,并把/api请求反向代理到后端的8080端口。Nginx的核心配置片段如下:

server { listen 80; server_name your-domain; location / { root /usr/local/frontend/dist; index index.html; 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; } }

try_files $uri $uri/ /index.html;这行是前端路由刷新不404的关键。Vue3使用history模式时,直接访问/workload/myList这样的路径,Nginx默认找不到对应文件,如果没配置try_files就会返回404。配置了try_files之后,所有前端路由请求都会回退到index.html,由Vue路由接管。很多同学部署Vue项目时刷新白屏,原因基本都在这。

5.3 常见问题排查实录

我把这套系统开发部署过程中遇到的典型问题整理成一张速查表,按照排查顺序排列,基本覆盖了大多数人的坑。

现象可能原因排查与解决
MySQL连接报Public Key Retrieval is not allowedJDBC连接串缺少allowPublicKeyRetrieval参数URL末尾加上allowPublicKeyRetrieval=true
前端调用后端接口报跨域Nginx反代未生效或后端未允许跨域Nginx配置location /api/代理;后端配置CorsFilter
时间字段相差8小时数据库连接时区未设置URL加serverTimezone=Asia/Shanghai
刷新页面404前端路由与Nginx未配合配置try_files $uri $uri/ /index.html
分页数据total不准分页查询含GROUP BY语句自定义count查询,手动设置page.total
审核并发导致状态异常缺少乐观锁控制version字段+update时检查
导出Excel中文文件名乱码响应头未做URL编码文件名使用URLEncoder.encode并decode
Vite启动后页面访问空白端口冲突或代理未生效检查5173是否被占用,查看vite.config.js代理设置
逻辑删除后数据重复出现唯一索引和逻辑删除共存冲突建表时使用联合唯一索引包含deleted字段
内存溢出导致服务重启jar包启动时没设置JVM参数java -Xms512m -Xmx1024m -jar 启动

第九条逻辑删除和唯一索引的冲突值得展开说一下。比如教师表里的工号字段设置了唯一索引,用户删除一位教师后,这条记录的deleted字段变成1而不是真删除。后续如果重新添加同工号的教师,由于唯一索引仍然生效,插入会失败,因为数据库里已经有一条相同工号的数据,哪怕它标记为已删除。解决方案有两种:一个是把联合唯一索引改成包含deleted字段,UNIQUE KEY uk_teacher_no (teacher_no, deleted),另一种是删除时顺手更新工号字段加个时间戳后缀,把原值让出来。我采用的是第二种,简单直接,不会影响历史查询。

5.4 项目文档的使用方法

源码附带的文档是一个详细的Markdown文件,包含了需求规格说明、数据库设计说明书、接口文档、部署手册。很多人拿到文档后喜欢从头到尾读一遍,这不是最高效的方式。

我的建议是按角色分步骤读。如果是想跑通项目,先看部署手册,照着把系统启动起来。启动成功后打开首页,点一遍各个菜单,对功能模块有宏观认识。然后看数据库设计说明书,重点看表结构和状态字段含义。最后看接口文档,按业务模块对照Controller代码和前端调用去理解数据流。

如果是想基于这套系统二次开发,第1章需求规格说明和第3章数据库设计说明书要精读,后面接口文档适合当作工具书随时翻阅。二次开发时最需要小心的就是不要破坏已有的状态机流转逻辑,宁可在一个流程上扩展,也不要另起炉灶。

6. 项目后续扩展与经验总结

6.1 从教师工作量延伸到绩效考核的扩展思路

这套系统上线运行稳定之后,很自然的扩展方向是衔接年度绩效考核。工作量数据是绩效考核的重要输入,目前系统已经把每个教师每个学期的工作量明细和汇总数据都存进了数据库,绩效考核模块可以直接复用这些数据,再补充科研积分、教学成果、学生评教等维度的数据,就能组成一套比较完整的绩效评分表。

扩展时我建议不要在原有工作量系统里堆功能,而应该拆分成独立的考核模块,通过教师ID和学期ID关联查询。这是我在实际开发中反复验证的架构思路。工作量管理是流程型业务,绩效考核是结果型业务,两者代码逻辑差异很大。如果耦合在一起,工作量申报流程一旦调整,绩效考核逻辑很容易被波及,风险不可控。

6.2 这套代码可以迁移到哪些同类场景

我做完这个项目后的最大感受是,教师工作量管理的业务模型其实可以套用到很多场景。比如企业内部的工时管理系统,核心逻辑是员工申报工时、主管审核、部门汇总,流程模型几乎一致。再比如科研项目的经费报账系统,也是申报、审核、退回、汇总的链条。这套系统的角色权限模型和状态机设计,在这些场景里都可以复用,改改表结构、换换业务字段就行。

代码层面最值得沉淀的是状态机流转和审核日志这套东西,几乎是所有审批类业务的通用骨架。如果后续要做新项目,我会把这部分抽出来做成一个通用流程组件,配置化定义流程节点和角色权限,数据库驱动流程变化,极大减少重复造轮子。

结合我自己带这个项目的经验,有几个最后的提醒。开发这类管理系统,前期的业务调研比写代码更重要,需求方说的"简单统计工作量"背后往往藏着一堆计算规则和管理流程。技术层面,Java Web的技术栈选择不是越新越好,稳定成熟、团队熟悉、坑有解决方案才是优先考量。功能实现层面,权限控制和状态流转这种底层逻辑要一次性设计稳,返工成本是最高的。文档不能等到项目最后才写,每个接口开发完同步记录,后面部署和维护的时候,这些文档能省掉大量回忆和沟通成本。

目前这个系统已经在某高校的教学管理系统里跑了两个学期,教务处那边最满意的是统计报表功能和审核留痕,从Excel时代过渡到线上流程之后,期末工作量核算的时间从原来的两周缩短到两天。技术最终是服务于业务的,把业务理顺了,代码怎么写都不会太偏。

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

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

立即咨询