工作量统计这套东西几乎每家公司都逃不过。有人用Excel有人用企业微信接龙还有人靠部门助理手工汇总结果月底对账的时候一地鸡毛——数据对不上、口径不统一、跨部门协作全靠催。我去年接手了一套基于 SpringBoot Vue3 MyBatis MySQL 的工作量统计系统前后端分离的架构从数据库设计到前后端联调整个链路自己撸了一遍。今天把这套系统的源码思路、核心实现和踩坑记录整理出来给正在做类似项目或者准备做课程设计、毕业设计的同学一个参考。这套项目能做的事很明确员工填报日常工作量、管理员维护任务类型和权重、按部门/按个人/按时间段统计产出、导出报表。适合正在学 Java 后端开发的人、准备找工作面试的人、以及需要用实际项目练手的前端开发者。核心关键词就是 SpringBoot、Vue3、MyBatis、分页插件、MySQL下面逐个环节拆开讲。1. 项目整体设计为什么是这套技术栈1.1 工作量统计系统的业务痛点先想清楚业务再动手写代码。工作量统计听起来简单实际做起来有几个坑绕不开第一是数据来源杂。每个员工填报的工作类型不一样有的人一天做 8 小时开发有的人一天只填 2 条任务如果没有明确的任务类型和计量单位统计出来的数字没有可比性。第二是统计口径乱。按工时算还是按任务条数算权重是怎么定的月度统计要不要排除周末这些需求如果没有在数据库层面设计好后面写 SQL 会非常痛苦。第三是填报体验差。如果前端页面转圈三秒才提交一条数据员工基本不会配合用最后又回到 Excel 老路上去。所以我在设计这套系统时把核心定位定为填报简单、统计灵活、数据口径统一。前端只负责录入和展示后端通过配置化方式维护任务类型、权重系数、部门关系所有统计逻辑都收敛到 Service 层和 SQL 里避免出现“每个页面各算各的这种失控局面。1.2 技术选型背后的理由这套系统的技术栈组合很典型Java 8 SpringBoot 2.7 MyBatis 3.5 MySQL 8.0 Vue3 Element Plus。SpringBoot 选它不是因为跟风而是因为它的自动配置机制能极大减少样板代码。一个工作量统计系统本质上就是增删改查加统计报表SpringBoot 内置的 Web 容器、Jackson 序列化、事务管理、参数校验基本覆盖了我 80% 的需求剩下的 20% 靠自定义配置补齐。MyBatis 在敏捷迭代的中小型管理系统里比 JPA 好用。工作量统计的 SQL 往往比较复杂多表关联、条件动态拼接、分组聚合MyBatis 的 XML 文件可以直接写出可读性很高的 SQL。JPA 那种先查出来再内存过滤的方式在这种报表场景性能不够也不直观。Vue3 选择组合式 API更符合我写逻辑的习惯。组件里的统计卡片、趋势图表、分页表格用 Composition 方式组织代码比 Options API 清爽很多尤其是多个统计图联动的时候setup 里的响应式数据可以做到数据流一目了然。MySQL 这边工作量统计的数据量级通常不会太大几万到几十万条记录MySQL 8.0 的窗口函数、JSON 函数、通用表表达式足够应付。如果数据增长到百万级别再加个 Redis 缓存热点统计结果也来得及。1.3 前后端分离的架构布局整个项目分成三个独立的部分开发时各自跑在本地前端 Vue3 应用端口 5173负责所有页面渲染和用户交互。工程目录用 Vite 构建按模块分包api 目录放接口请求views 目录放页面components 目录放复用组件router 定义路由守卫。后端 SpringBoot 服务端口 8080只提供 RESTful 接口不处理任何页面渲染。工程结构按经典三层分包controller 接收请求参数、service 处理业务逻辑、mapper 与数据库交互。MySQL 数据库端口 3306定义 5 张核心业务表加 3 张基础表。初始化脚本单独维护在项目的 sql 目录下方便新环境一键部署。前后端通过 HTTP JSON 通信。前端 axios 实例统一配置 baseURL、请求拦截器、响应拦截器后端统一返回 Result 结构体状态码 200 表示成功非 200 表示业务异常。跨域问题在开发环境用 Vite 代理解决生产环境通过 Nginx 反向代理解决。这套架构的核心优势是职责清晰前端不碰数据库后端不管页面跳转任何一端出问题都不会拖垮另一端。对团队开发来说前端同事和后端同事可以并行开工不用互相等。2. 数据库设计工作量统计的根基2.1 表结构设计五张核心表工作量统计系统的表结构一定要慎重设计因为一旦上线改表结构会牵连后端代码、前端页面、统计 SQL 全部调整。我经过几轮调整后最终保留了这几张核心表。第一张是用户表 t_user字段包括 id、username、password、real_name、department_id、role、status。角色我用整数区分1 表示管理员2 表示普通员工这样权限控制写起来最直接不需要引入 Spring Security 的复杂体系。第二张是部门表 t_department字段包括 id、dept_name、parent_id。parent_id 指向自身支持树形结构。统计的时候如果要按一级部门汇总可以通过递归查询或者直接在 Java 代码里组装树结构。第三张是任务类型表 t_task_type字段包括 id、type_name、unit、weight、status。这里的 weight 是工作量计算的关键比如开发任务的权重是 1.0一个任务算 1.0 个工作量文档编写权重是 0.5写两份文档才算 1.0 个工作量。这种设计让统计口径在源头就统一了。第四张是工作量填报主表 t_work_log字段包括 id、user_id、task_type_id、work_date、content、workload、create_time。workload 是员工填写的原始工时后端会根据任务类型的权重计算出标准工作量。第五张是工作量明细表 t_work_log_detail这张表用来记录填报时勾选的多项任务。其实我一开始没有设计这张表后来发现一个员工一天可能填报多条不同任务类型的工作主表一对一的结构撑不住加上明细表之后填报页面才顺畅。2.2 工作量统计 SQL 的设计思路统计报表的核心 SQL 一定要写清楚因为报表的查询条件千变万化可能是按部门、按人员、按日期范围、按任务类型SQL 必须设计成可拼接的动态查询。月度工作量统计的 SQL 大致如下根据 t_work_log 表按员工分组、按月份过滤关联 t_user 拿到姓名和部门关联 t_task_type 拿到任务类型名称和权重最终计算出总工作量和明细条数。这里有一个关键点工作量等于填写的实际值乘以权重的累计值而不是简单地对 workload 求和。为了支持趋势图展示我还在 SQL 里按天分组统计出每天的总工作量这样前端拿到的是一个时间序列数据直接丢给 ECharts 就能绘制折线图。这里要重点提醒一下分页查询的注意事项。统计报表的列表页如果不用分页插件数据量大之后数据库压力会直线上升。MyBatis 分页插件 PageHelper 是行业标配使用起来非常简单在查询方法之前调用 PageHelper.startPage(pageNum, pageSize)然后紧跟的分页查询就会自动拼接 LIMIT 语句并返回 PageInfo 对象。但要注意startPage 之后只能紧跟一条查询语句如果中间插入其他查询分页就会失效。这个坑我踩过后面第 5 节会详细讲。2.3 MySQL 初始化与配置文件项目落地前先把数据库准备好。我习惯把建库建表脚本放在项目根目录的 sql/init.sql 里内容包括创建数据库、创建表、创建测试数据。这样任何人拉取代码后只需要一条命令就能初始化环境。数据库连接配置写在 application.yml 里。这里有几个值得注意的地方MySQL 8.0 的驱动类名要用 com.mysql.cj.jdbc.DriverURL 要带上 useSSLfalse 和 serverTimezoneAsia/Shanghai 参数否则启动时会报时区错。还有 allowMultiQueriestrue这个参数对多语句批量执行很重要。连接池我用的 HikariCPSpringBoot 2.7 默认集成的就是这个连接池。配置 maximum-pool-size 为 20minimum-idle 为 5连接超时 30000ms远超单机小系统需求。如果并发量上来可以调整但要注意数据库服务器的连接数上限别把 MySQL 压垮了。3. 后端核心实现SpringBoot 与 MyBatis 的正确姿势3.1 项目目录结构与分层规范我习惯按功能模块分包而不是按技术层分包。按技术层分包的问题是controller 目录下堆了几百个文件根本不知道哪个类对应哪个功能。按功能分包每个包内自包含 controller、service、mapper、entity阅读代码时能以功能为单位整体理解。这里给出一个直观的结构图com.example.workload ├── WorkloadApplication.java ├── common │ ├── Result.java │ ├── PageResult.java │ └── GlobalExceptionHandler.java ├── config │ ├── WebMvcConfig.java │ └── PageHelperConfig.java ├── module │ ├── user │ │ ├── controller/UserController.java │ │ ├── service/UserService.java │ │ ├── mapper/UserMapper.java │ │ └── entity/User.java │ ├── worklog │ │ ├── controller/WorkLogController.java │ │ ├── service/WorkLogService.java │ │ ├── mapper/WorkLogMapper.java │ │ ├── mapper/WorkLogMapper.xml │ │ └── entity/WorkLog.java │ └── statistics │ ├── controller/StatisticsController.java │ └── service/StatisticsService.java3.2 MyBatis 核心配置与分页插件用法MyBatis 的配置虽然 SpringBoot 自动搞定了大部分但有几个参数我在真实项目中手动调整过值得拿出来说。第一个是驼峰命名映射。数据库字段用下划线命名Java 实体用驼峰命名如果不在配置里开启 map-underscore-to-camel-case查出来的字段会全部是 null。这个配置忘掉的人太多了联调的时候前端看到 null 一脸懵排查半天才发现是这个问题。第二个是 XML 文件的 mapper-locations 配置。如果 XML 文件没有放在默认的 classpath:mapper/ 目录下必须在 application.yml 里指定明确的路径。第三个是 type-aliases-package。配置了这个包名后XML 文件里的 resultType 和 parameterType 可以不写全限定类名直接写 User 这种短类名代码清爽很多。分页插件的用法我用一个具体例子说明。假设工作日志列表页需要分页展示前端传 pageNum1、pageSize10后端 Service 方法如下先调用 PageHelper.startPage(1, 10)再调用 mapper 查询列表最后用 PageInfo 包装结果返回。PageInfo 里包含了 total、pageNum、pageSize、list 等字段前端可以直接使用。需要注意的分页坑PageHelper 的 startPage 只对紧接着的后一条查询语句生效。如果代码里先查了一个配置项再查列表分页就会作用到配置项查询上导致列表没有分页、配置项查询反而被加了 LIMIT。解决方法是强制把 startPage 和列表查询写在一起中间不插入任何其他操作。3.3 通用返回体与异常处理前后端分离架构中接口的返回格式如果不统一前端每个页面都要做不同的解析逻辑非常痛苦。我在项目里定义了一个 Result 通用返回体包含字段 code、message、data。成功时 code 为 200失败时 code 为 500 或其他业务码message 描述错误原因。全局异常处理器用 RestControllerAdvice 注解实现。业务异常类 BusinessException 可以根据场景抛不同错误码和消息参数校验异常也可以在这里统一捕获并转成 Result.fail() 返回。这样做的好处是前端响应拦截器只需要看 code 字段来判断是否弹出错误提示不用每个接口单独处理 catch。再补充一个细节日志打印。我在 Controller 层每个接口入口打印请求参数Service 层打印关键业务操作Mapper 层默认能打印 SQL 日志配置 mybatis.configuration.log-impl 为 StdOutImpl。开发阶段直接把 SQL 打到控制台排查问题时效率提升非常多。生产环境建议关闭或者使用异步日志框架避免 IO 影响接口性能。3.4 跨域配置与接口安全开发时期前后端分离前端跑 5173后端跑 8080两个端口不一样浏览器会有跨域拦截。解决方式有两种全局配置实现 WebMvcConfigurer 接口的 addCorsMappings 方法统一放行 /api/** 路径或者在 Controller 上加 CrossOrigin 注解。我选择前者因为全局配置一处搞定不用每个 Controller 都加注解。跨域配置里有几个参数容易配错allowedOriginPatterns 是用于允许指定来源如果写 allowedOrigins 直接填 *在部分版本下会报错。allowedMethods 必须包含 GET、POST、PUT、DELETE否则前端调了 PUT 接口照样报跨域。这些细节虽然不起眼但实测中是高频踩坑点。接口安全没有用 Spring Security因为这套系统的用户量小内部系统不需要复杂的 OAuth2 流程。我用一个简单的 Token 拦截器实现登录校验登录成功后生成 UUID token 存到 Redis前端每次请求在 Header 里携带 Authorization拦截器校验通过才放行。工作量统计系统的核心是数据准确性安全级别做到内部系统够用即可。4. 前端 Vue3 实现从搭建到功能落地4.1 Vue3 脚手架与工程初始化前端我用 Vite 创建项目执行 npm create vitelatest vue3-workload-ui -- --template vue 初始化工程。相比 Vue CLIVite 的开发服务器启动速度快很多代码热更新秒级响应开发体验好太多。项目依赖方面核心库包括 vue-router、pinia、axios、element-plus、echarts。Element Plus 按需引入可以减小打包体积但我在小项目中直接全局引入省事个人项目对体积不敏感代码可维护性优先。Vite 的关键配置是开发服务器代理。在 vite.config.js 里配置 server.proxy把 /api 前缀的请求代理到 http://localhost:8080这样前端代码里只写 /api/worklog/list不需要写全路径。后端接口统一加 /api 前缀方便代理匹配也方便后期 Nginx 路由转发。4.2 Axios 封装与请求拦截器axios 封装是整个前端项目的地基。我单独建了一个 src/utils/request.js 文件导出一个配置好的 axios 实例。基础配置有三块baseURL 设置为 /api超时时间 15000ms请求头默认携带 token。请求拦截器负责从 localStorage 或 Pinia 中读取 token添加到 Authorization 头响应拦截器负责统一处理返回数据如果 code 为 200直接返回 data 字段如果 code 为 401跳转登录页并清除本地缓存如果 code 为 500弹出错误提示。封装完成之后业务代码里只需要这样调用import request from /utils/request export function getWorkLogList(params) { return request({ url: /worklog/list, method: get, params }) }这样的好处是后端接口调整时只需要改 api 目录下的文件页面组件不用动。如果接口从 /worklog/list 改成 /worklogs/list只改一处就够了。4.3 工作量填报与统计展示页面填报页是整个系统的核心页面也是员工最常用的页面。我用 Element Plus 的 el-form 组件实现表单日期选择器默认当天任务类型下拉框的数据从后端接口动态加载工作量输入框默认带单位提示。表单校验用 rules 配置日期必填、任务类型必填、工作量必须大于 0。提交前使用 formRef.validate() 校验全部通过后再调用后端接口。这里有一个提升填写效率的小技巧提交成功后保留日期和用户信息只清空任务类型和工作量这样连续填报多条数据会顺手很多。统计展示页我用了四个维度今日总工作量卡片、本月总工作量卡片、部门排名表格、近 7 日工作量趋势折线图。卡片数据由统计接口一次返回表格和图表分别调用列表接口。ECharts 的图表配置里折线图的 X 轴是日期数组Y 轴是工作量数值数据格式直接后端组装好在接口里返回前端不需要做额外的数据转换。4.4 页面权限与路由守卫工作量统计系统虽然简单但角色权限还是要有。管理员能看到所有员工的填报记录和统计报表普通员工只能看到自己的数据。前端路由配置里给不同路由设置 meta.requiresAdmin 属性路由守卫里判断当前用户角色如果不是管理员则重定向到首页。路由守卫的实现代码router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.path /login) { next(/) } else if (to.meta.requiresAdmin userStore.role ! 1) { next(/403) } else { next() } })这样一个简单但完整的前端权限控制就实现了。后端的接口同样有角色校验前端路由守卫只是提升用户体验不能替代后端安全控制。5. 常见问题与排查技巧实录5.1 开发过程中遇到的典型问题做一个全栈项目遇到的坑比想象中的多。我挑几个印象最深的列出来这些问题几乎每个做过前后端分离的人都能共鸣。第一个是日期格式问题。前端传的日期是 2025-01-06后端接到的却是 2025-01-06T00:00:00.00000:00存进 MySQL 后再查出来前端显示的日期多了时区偏移。这个问题的根因是 Jackson 的日期序列化默认格式和北京时间不一致。解决方法是添加全局配置在 application.yml 里设置 spring.jackson.date-formatyyyy-MM-dd HH:mm:ss 和 time-zoneGMT8同时在实体类的 LocalDate 字段上加 JsonFormat(pattern yyyy-MM-dd)。第二个是 MyBatis 查询结果为 null。明明 SQL 在 Navicat 里能查到数据接口返回却是 null。排查后发现是实体类字段名和数据库字段名对不上比如数据库是 create_time实体类是 createTime虽然开启了驼峰转换但 XML 里的 resultType 写错了位置导致映射失效。解决办法是把 resultType 改成 resultMap 或者确认实体类字段命名规范。第三个是分页数据错乱。列表页第一页数据正常第二页开始重复或者缺失。排查下来发现是 PageHelper 的 startPage 后面隔了一条查询语句分页串到了错误的 SQL 上。修复方式就是严格执行 startPage 紧跟着要分页的查询这一原则。5.2 排查经验速查表我把高频问题整理成了表格方便后来者直接对照排查。问题现象排查方向解决方法接口返回 404检查 Controller 的 RequestMapping 路径和方法注解在浏览器直接访问路径看后端日志是否打印了请求映射前端跨域报错检查 CORS 配置和 Vite 代理开发环境用 Vite 代理生产环境用 Nginx 转发避免后端暴露 8080 端口中文乱码检查数据库字符集和数据源 URL数据库创建时指定 utf8mb4URL 加 characterEncodingutf8数据库连接超时检查连接池配置和 MySQL 最大连接数调整 HikariCP 的 maximum-pool-size避免连接泄漏接口响应慢用日志定位 SQL 执行时间打开 MyBatis 日志确认是否有慢查询检查是否需要索引字段映射 null查看 SQL 执行结果开启驼峰映射检查 resultType 与 resultMap 的区别登录状态失效检查 token 过期时间和拦截器逻辑统一在响应拦截器里处理 401 跳转登录页打包后接口 404检查 Nginx 配置的前端路由配置 try_files 指令确保 SPA 前端路由回退到 index.html5.3 值得记住的避坑经验最后分享几个我实际玩过之后觉得最有价值的经验。第一分页插件的版本必须和 MyBatis 对应。SpringBoot 2.7 的 MyBatis 版本是 3.5.xPageHelper 要用 PageHelper-SpringBoot-Starter 1.4.x 以上版本不然可能出现分页被识别成普通查询的情况。版本不匹配出现的错误日志很隐蔽不会明确说分页错误只会表现为数据量不对或者插件不生效。第二工作量统计系统的数据准确性比功能多少更重要。这意味着数据库字段类型要精确定义比如 workload 用 DECIMAL(10,2) 代替 DOUBLE避免浮点误差导致的对不上账。统计 SQL 结果用 Map 返回时列名别用中文前端解析容易出幺蛾子。第三前端 ECharts 图表在组件销毁时要调用 dispose 方法释放内存。如果页面频繁切换ECharts 实例不销毁会占用大量内存导致卡顿。可以在组件 onUnmounted 钩子里执行 chart.dispose()这就是个小动作不做好后期上线必卡。第四如果是用于课程设计或毕业设计一定要在 README 文件里写清楚环境搭建步骤。数据库初始化脚本、后端启动命令、前端安装命令、默认账号密码写清晰之后评阅老师和同学都能快速跑起来这是印象分比功能炫技重要得多。我在实际开发中有一个体会工作量统计系统的难点从来不在某个单独的技术点而在于把填报、统计、展示这条链路打通每个环节的细节都得有人管。SpringBoot 负责接口的稳定Vue3 负责交互的顺畅MyBatis 负责 SQL 的高效MySQL 负责数据的可靠——四个角色配合好系统才能真正好用。如果后续想扩展可以加一个按季度对比报表用 ECharts 画柱状图加折线图双 Y 轴展示也可以加 Excel 导出功能用 POI 把月度统计一览表导出给管理层。还有一点工作量统计结合自动化部署用 Docker Compose 把 MySQL、后端、前端、Nginx 四个镜像编排起来整套系统一键启动省掉所有手动配置环境的时间。这些扩展点我现在已经在规划了等落地完再写一篇记录。
