首页 / 资讯中心 / 文章详情

SpringBoot+Vue工作量统计系统设计与实现:从CRUD到闭环管理

SpringBoot+Vue工作量统计系统设计与实现:从CRUD到闭环管理 ★ FEATURED ARTICLE
1. 先别急着写代码这个工作量统计系统到底要解决什么问题我做过不少Java全栈项目但真正让我觉得“值得拿出来讲”的工作量统计系统算一个。原因很简单它看起来像是个普通的CRUD但真正落地之后你会发现牵涉到的业务逻辑、统计口径、前后端交互、权限控制远比想象中复杂。尤其当你面对的是一个真实团队、真实项目、真实工时数据的时候一个数字算错整个统计报表就废了。这套系统是基于SpringBoot Vue MySQL MyBatis做的完整实现源码全部可用。它解决的核心问题是团队成员每天、每周、每月的工作量到底怎么量化统计项目投入的工时分布是否合理哪些任务严重超时哪些人长期满负荷这些数据如果靠手工填Excel收集和汇总就是一场灾难如果只做个简单的增删改查页面又很难满足实际管理需求。所以系统的设计目标很明确把工作量填报、审核、统计、可视化、导出这一整条链路打通让数据从录入到出报表形成闭环。适合谁来参考如果你是刚学完SpringBoot和Vue、想找一个完整项目练手的人这套系统的代码结构和技术栈非常典型如果你是在公司需要做一套类似的内部管理工具这套系统的数据模型和统计思路可以直接借鉴。文章后面会从需求分析、数据库设计、后端接口、前端页面、部署上线、踩坑实录六个部分逐一展开尽量把设计决策背后的原因也讲清楚。2. 需求拆解与功能边界不要一上来就建表很多新手拿到这类项目第一反应是打开Navicat建表。这其实是最大的坑。工作量统计系统的核心不是“表怎么建”而是“工作量怎么定义”。这个定义搞不清楚后面所有统计逻辑都是空中楼阁。2.1 工作量的定义方式填时长还是填任务我见过两种主流方式一种是员工每天填报“我今天干了8小时”系统按分钟累计另一种是员工填报“我今天完成了哪些任务”每个任务预估工时系统按任务完成度折算。第一种实现简单但数据质量很差——人们填时长的时候普遍会“填满”导致报表里全是8小时看不出真实产出。第二种更接近项目管理软件比如Jira、禅道的逻辑但实现起来需要维护任务池、分配机制、审核机制复杂度上了一个台阶。我最终选择的是混合模式员工每天按“任务 投入时长”的方式填报工作量一条记录绑定一个任务任务可以预先把预估工时和负责人定义好也可以临时新建。系统的统计维度分三层按人统计每个人投入了多少时间、按项目统计每个项目消耗了多少时间、按任务统计每个任务实际花了多少时间和预估比是超支还是结余。这个设计的好处是填报的粒度可粗可细但统计的口径是统一的。2.2 角色权限谁能填、谁能看、谁能改工作量数据天然带有敏感性权限模型必须明确。这套系统设计了三种角色普通员工、项目经理、管理员。普通员工只能填报自己的工作量查看自己的日报和周报。项目经理可以查看自己负责项目下的所有工作量数据审核成员的填报记录。管理员全局数据可见可以管理用户、项目、任务字典且可以修正异常数据。权限控制是在后端做的用了Spring Boot的拦截器 自定义注解前端根据登录用户的角色渲染不同菜单和按钮。有人会问为什么不用Spring Security因为这个系统没有复杂的OAuth2需求简单的Token鉴权就够了引入重型框架反而增大理解成本。项目里的拦截器逻辑非常清晰适合初学者读懂整个鉴权链路的实现。2.3 统计口径自然周、自然月还是按项目周期工作量统计最容易扯皮的就是时间范围。项目里我做了三套统计维度按日看某个人某天填了什么时长多少有没有异常比如一天填了超过12小时。按周看本周每人累计工时对比本周预期工时按工作日×8小时计算判断负荷情况。按月看月度总工时分布按项目和人员两个维度交叉统计。这里涉及到一个关键设计决策统计范围是自然周/自然月而不是项目启动之后的第N周。虽然自然周会和项目迭代周期错位导致数据不够“项目化”但绝大多数管理层习惯看自然周报表因为要和考勤、工资核算周期对齐。系统的灵活性在于我预留了按项目时间段筛选的选项可以自由选择任意起止日期来生成统计报表。3. 技术选型与关键决策为什么是SpringBoot Vue而不是其他组合标题已经点明了技术栈SpringBoot Vue MySQL MyBatis。这几乎是Java全栈项目最主流的组合了但每选一个组件背后都有对应的取舍逻辑。3.1 后端SpringBoot的版本选择与MyBatis的定位SpringBoot我用的是2.7.x版本没有上3.x。为什么因为3.x基于Jakarta EE规范很多老的开源组件和插件需要额外适配而2.7作为2.x的最后一个稳定分支兼容性好、资料多、踩坑成本低。如果你的机器上装的是JDK 17那用3.x问题不大如果是JDK 8那老老实实2.7就对了。持久层用了MyBatis而不是MyBatis-Plus。原因是我比较在意SQL的可控性工作量统计涉及大量分组聚合查询MyBatis的XML配置可以精确控制每一行SQL配合where、if这些动态标签比MyBatis-Plus的Wrapper链式调用更直观。当然MyBatis-Plus在做单表CRUD时效率很高但在这个项目里复杂报表SQL占了很大比重MyBatis反而更顺手。数据库连接池用的Druid监控页面在开发阶段非常好用能直接看到每一条SQL的执行时间。如果你是第一次跑这个项目可以在Druid的后台页面观察慢SQL对优化报表查询很有帮助。3.2 前端Vue 2还是Vue 3这个项目用的是Vue 2 Element UI。可能有人会觉得都2024年了怎么还不上Vue 3 Element Plus坦白说Vue 3 Vite Element Plus的组合现在确实更主流但选择Vue 2的理由也很现实项目里用到的一些第三方控件和表格插件在Vue 2生态下最稳定而且公司存量项目不少还是Vue 2维护学一遍这套结构可以直接上手工作。如果你是自学我更建议先把Vue 2的基础弄通再看Vue 3的差异因为两者的核心概念组件、路由、状态管理是相通的迁移成本并不高。前端工程化部分用了Vue CLI不是Vite。Vite虽然快但Vue CLI的webpack配置对新手更友好很多问题在网上都有现成答案。这里提醒一下Vue环境配置有个高频坑Node版本和依赖版本不匹配建议Node 14到16之间比较稳装Element UI和node-sass的时候能省心很多。3.3 认证方案JWT还是Session登录认证这块我用的JWT。流程是登录成功后后端生成Token返回给前端前端存到localStorageaxios请求拦截器统一在Header里带Authorization: Bearer token后端通过拦截器解析Token获取用户信息。这个方案的优点是无状态、横向扩展容易缺点是Token泄露后无法主动失效所以项目里给Token设置了过期时间24小时并在前端路由守卫里做了登录态检查。如果你对JWT不熟可以把它理解成一张“服务器签过名的通行证”。服务器不记录这张通行证的状态只通过签名验证真伪。所以一旦发出去在到期之前都是有效的。这里有个细节用户修改密码后理论上应该让旧Token失效但JWT天然做不到这一点。项目里我做了个折中——用户信息表里存一个token_version字段JWT里带上这个版本号密码修改后版本号加一旧Token自然校验失败。这算是个比较轻量的“踢人下线”方案。4. 数据库设计工作量统计的命脉在维度建模这部分是整套系统的地基。表建得合理后面写SQL就是顺手的事表建得不合理每个统计页面都得堆一堆临时逻辑。4.1 核心表结构系统里一共有5张核心业务表用户表sys_user、项目表project、任务表task、工作量填报记录表work_log、项目成员关联表project_member。另外有角色表、菜单权限表这些相对独立。工作量填报记录表是最核心的我列出它的字段设计字段名类型说明idbigint主键user_idbigint填报人task_idbigint关联任务project_idbigint关联项目log_datedate工作日期work_hoursdecimal(4,1)投入工时精确到0.5work_contentvarchar(500)工作内容描述statustinyint0待审核 1已通过 2已驳回create_timedatetime创建时间work_hours用decimal(4,1)而不是float是因为浮点数在累加统计时会出现精度误差用定点数最稳妥。status字段为审核流预留了状态位项目经理处理后数据才进入统计视图。这里建议加一条约束同一user_id log_date task_id 只允许一条记录避免同一人同一天重复填报同一个任务。如果业务上允许一个人一天在多个任务上切换那就把task_id换成多对多的方式但当前版本的设定是“一天一条一天一件主要任务”统计口径更清晰。4.2 统计报表的SQL设计工作量统计系统里最值得研究的是报表SQL。比如“按月统计每人总工时”SELECT u.real_name, DATE_FORMAT(w.log_date, %Y-%m) AS month, SUM(w.work_hours) AS total_hours FROM work_log w LEFT JOIN sys_user u ON w.user_id u.id WHERE w.status 1 AND w.log_date BETWEEN #{startDate} AND #{endDate} GROUP BY u.real_name, DATE_FORMAT(w.log_date, %Y-%m) ORDER BY month, total_hours DESC这段SQL的关键在于WHERE w.status 1——只统计审核通过的数据防止无效记录污染报表。项目里还用了MyBatis的动态SQL处理多条件筛选比如按项目、按时间范围、按人员姓名模糊查询select idselectWorkLogStats resultTypecom.example.vo.WorkLogStatsVO SELECT p.project_name, u.real_name, SUM(w.work_hours) AS total_hours FROM work_log w LEFT JOIN project p ON w.project_id p.id LEFT JOIN sys_user u ON w.user_id u.id where if testprojectId ! null and projectId ! AND w.project_id #{projectId} /if if testuserId ! null and userId ! AND w.user_id #{userId} /if if teststartDate ! null and startDate ! AND w.log_date gt; #{startDate} /if if testendDate ! null and endDate ! AND w.log_date lt; #{endDate} /if AND w.status 1 /where GROUP BY p.project_name, u.real_name /select4.3 一个容易被忽略的索引优化数据量小的时候怎么查都很快数据量一旦涨到几十万条报表SQL就会变慢。我在work_log表上建了联合索引idx_user_date (user_id, log_date)因为绝大多数统计查询都是先定位“谁”再定位“时间范围”。如果项目维度查询更多可以再加idx_project_date (project_id, log_date)。这里的关键是索引顺序等值条件放前面范围条件放后面才能命中索引。很多人一上来就给所有字段单独建索引结果MySQL优化器反而不走索引这是新手很容易忽略的点。5. 后端核心实现接口设计、业务逻辑与审核状态机后端部分我从Controller、Service、Mapper三层把核心逻辑拆开讲重点讲工作量填报的校验逻辑和审核状态流转。5.1 整体分层与接口清单项目采用标准的Controller-Service-Mapper分层。Controller只负责接收参数和返回结果Service层写业务规则Mapper层写SQL。返回结果统一封装成ResultT对象结构是{code, msg, data}。code为200时表示成功401表示未登录或Token失效500表示服务端异常。前端axios的响应拦截器统一处理这些状态码403时自动跳转登录页。接口清单大致如下POST /api/auth/login登录返回Token和用户信息POST /api/workLog/add填报工作量GET /api/workLog/myLogs查询我的填报记录分页PUT /api/workLog/audit审核工作量项目经理/管理员GET /api/stats/personMonth按月统计个人工时GET /api/stats/projectOverview项目投入分布GET /api/task/listByProject按项目查任务列表POST /api/task/add新增任务GET /api/user/list用户列表管理员每个接口的参数校验都用了JSR 303注解比如NotNull、DecimalMin(value 0.5)减少Service层的脏数据判断。5.2 填报接口的校验链条/api/workLog/add这个接口看似简单但涉及好几层校验用户是否登录由拦截器保证。任务是否存在、任务是否属于当前用户所在项目查task表比对project_member表。工作日期是否可以填未来日期结合实际业务当天只能填报当天及之前的数据不允许预填未来工时避免月底突击填报造成数据失真。是否重复填报查work_log表如果同一天同一任务已有记录则更新而不是新增。代码里有一段关键逻辑WorkLog existing workLogMapper.selectByUserAndDateAndTask( userId, workLog.getLogDate(), workLog.getTaskId()); if (existing ! null) { existing.setWorkHours(workLog.getWorkHours()); existing.setWorkContent(workLog.getWorkContent()); workLogMapper.updateById(existing); return Result.success(已更新当日填报记录); }用户通常不会记得自己“填过没有”与其报错不如静默转换成更新体验会好很多。这个细节是我在实际使用中摸索出来的。5.3 审核状态机待审核、通过、驳回审核接口的逻辑是一个简单的状态机只有状态为0待审核的记录允许被流转到1已通过或2已驳回已通过的记录如果要修正必须走“驳回重填”的流程。后端实现时加了一层防护int rows workLogMapper.auditWithStatusCheck( id, targetStatus, ReviewStatus.PENDING.getCode(), operatorId); if (rows 0) { return Result.error(记录状态已变化刷新后重试); }通过UPDATE ... WHERE status 0这种乐观锁方式保证并发操作下不会出现状态错乱。初学者容易忽略这种情况两个审核人同时打开同一条记录一个点了通过一个点了驳回如果都用“无条件更新”后操作的会覆盖先操作的。加上状态条件更新行数为0的那一方就知道冲突了。5.4 全局异常处理与统一返回项目中写了一个RestControllerAdvice全局异常处理器捕获业务异常、参数校验异常和兜底异常。比如填报表单中workHours填了23.5超过法定工时上限业务层抛出的异常会被统一包装成Result.error(工时超出合理范围)返回前端。这样Controller里就不用散落一堆try-catch代码整洁很多。注意异常提示信息一定要面向用户、说人话。后端程序员写日志时习惯输出“IllegalArgumentException”但前端弹窗也这么显示用户根本看不懂。这个项目里的所有异常提示都做了文案转化操作体验完全不一样。6. 前端Vue实现从页面结构到数据可视化前端这部分的工作量其实比后端还大。菜单、路由、权限控制、表单校验、图表展示每一块都有可讲的东西。6.1 前端项目结构与基础搭建前端用的是Vue CLI创建的标准工程目录结构大致如下src/ ├── api/ # 接口封装 │ ├── workLog.js │ ├── stats.js │ └── user.js ├── assets/ ├── components/ # 通用组件 │ ├── Sidebar.vue │ └── PageHeader.vue ├── router/index.js # 路由配置 ├── store/ # Vuex状态 ├── views/ │ ├── Login.vue │ ├── Dashboard.vue │ ├── workLog/ # 填报、审核页面 │ ├── project/ # 项目管理 │ ├── stats/ # 统计报表页面 │ └── system/ # 用户管理页面 └── utils/request.js # axios封装utils/request.js里重点做了三件事请求拦截器加Token、响应拦截器统一处理错误码、401时清空登录态并跳登录页。这是所有页面正常工作的基础。6.2 路由守卫与菜单权限Vue Router的beforeEach守卫检查是否存在Token没有就跳转到/login。不过单纯靠“有没有Token”来判断登录态是不严谨的Token过期了怎么办我在响应拦截器里做了“静默处理”当后端返回401时清除本地登录状态刷新页面前跳转登录页。同时路由的meta字段记录角色要求在守卫里做二次验证router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); return; } if (to.meta.roles !userStore.hasRole(to.meta.roles)) { next(/403); return; } next(); });侧边栏菜单是遍历路由表动态生成的项目经理只能看到“项目进度”“审核”菜单普通员工看不到“系统管理”。这样菜单本身和路由权限联动不需要维护两份配置。6.3 工作量填报表单的细节填报页面用的是Element UI的el-form比较关键的是两个组件选择日期用el-date-picker限制不可选择未来日期工时用el-input-number设置step0.5、min0.5、max12。任务选择用级联下拉先选项目再选该项目下的任务。这里有个交互上的坑级联下拉如果数据量大每次切换项目都要重新请求任务列表很卡。我用了el-select的remote远程搜索输入关键字再请求接口既避免全量加载又提高查找效率。如果你在写类似功能可以直接抄这个思路。6.4 图表展示与统计报表页面统计页面用了ECharts。第一个图表是“个人月度工时柱状图”数据格式是[{month: 2024-05, totalHours: 128}]前端按月份动态生成x轴。第二个是“项目投入饼图”展示各项目的工时占比。第三个是“人员负荷雷达图”展示一个成员在多个项目上的时间分配。ECharts的坑主要在数据格式化上。后端返回的工时是decimal类型前端拿到的其实是字符串图表的数值轴需要parseFloat再塞进去。另外饼图的百分比是小数的形式需要自己转成百分比字符串series[0].data projectStats.map(item ({ name: item.projectName, value: parseFloat(item.totalHours) }));如果你不想自己写这些格式化逻辑直接用图表库帮带的formatter回调也完全可以。只是要注意金额、工时这类数据在图表里一定要统一单位不要前台显示“128”后台其实是“128小时”月份和年份也要标注清楚。这种细节看起来小却是报表页面专业与否的分水岭。6.5 前端本地调试必装的工具这里分享一个调试经验Vue项目开发时脑子里一定要有Vue Devtools这个插件。Chrome应用商店直接搜“Vue Devtools”安装即可装完之后可以在控制台面板直接查看组件树、Vuex状态、路由信息。我见过太多人搞了半天页面报错看不到具体原因其实用Devtools一看数据流哪里断了就清楚了。还有一个常见问题是vue.config.js的devServer.proxy配置开发时把/api前缀代理到localhost:8080后端服务能避免跨域问题。注意如果你在Windows上开发vue项目依赖安装失败是很常见的事。优先检查Node版本然后把npm镜像源切换成国内镜像能省下大量排队时间。7. 打包、部署与线上问题排查实录这部分我踩过不少坑挑几个最有代表性的问题展开。系统本身是个前后端分离项目部署方式也比较典型后端打jar包、前端npm run build后丢到Nginx。7.1 后端打包以及数据库迁移SpringBoot项目用Maven打包mvn clean package -DskipTests生成target/work-stat-system.jar。建议打包前检查一下application.yml里的数据库连接配置确认是测试环境还是生产环境库。数据库迁移我用的比较简单——SQL脚本文件直接执行没有引入Flyway这类版本管理工具。项目小的时候可以这样做但如果你打算把这套系统作为长期迭代的基础建议尽早引入Flyway否则上线后改了字段没有记录回头排查历史数据问题会非常痛苦。7.2 jar包能直接改吗反编译的思路有人会问拿到一个jar包能不能反编译回来改源码。开发阶段自己打包的源码都在不存在这个问题但如果是接手别人的项目只有部署好的jar包那确实需要反编译。常用工具是IDEA自带的反编译插件Java Decompiler或者jadx、cfr这类工具。把jar包的class文件拖进去能还原出大部分代码但注释、泛型信息、常量名会丢失MyBatis的XML文件通常在resources目录下直接被读出来比class更完整。我的经验是反编译只能用于理解逻辑别指望能还原成可维护的源码最终还是要设法找到原始代码仓库。7.3 MySQL连接常见错误SSL和时区问题项目里MySQL连接串配置如下jdbc:mysql://localhost:3306/work_stat?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiuseSSLfalse是关键——如果MySQL版本较高默认尝试SSL连接而本地开发通常没有配置证书会报SSL connection error。serverTimezone要显式指定否则MySQL 8.x默认时区与本地不一致时间字段查出来会差8小时。这两个参数我几乎每次搭新环境都要遇到建议直接固定写在连接串里统一维护。7.4 MyBatis查询出现的参数类型问题一个高频报错是IllegalArgumentException: Mapped Statements collection already contains value——这是因为XML里的id和Mapper接口里的方法名没对上。还有一个很容易踩的坑是or和and的拼接动态SQL里如果位置放错生成的SQL逻辑会完全偏离预期。调试方式很简单把MyBatis的日志级别改成DEBUG控制台直接打印SQL和参数。mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl看到实际执行的SQL比对着XML猜靠谱一万倍。我排查过的很多“查不到数据”问题最后都是参数没传进去或者类型不匹配控制台日志一眼就能看出端倪。7.5 Nginx部署前端的配置前端打包后生成dist目录Nginx配置里做两个关键设置将根路径指向dist目录并将/api开头的请求反向代理到后端服务server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/work-stat/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }特别注意当使用Vue Router的history模式时要加一条try_files $uri $uri/ /index.html;否则刷新非首页路由会404。如果你用hash模式则没有这个烦恼但URL会带#号看着不够正规。项目里我用的hash模式完全是因为Nginx配置不熟的人更不容易出问题求稳优先。7.6 全局过滤器与XSS防护从热搜词能看到很多人关注SpringBoot全局过滤器处理XSS攻击的问题。这套系统在登录和填报接口上做了基本的XSS过滤做法是写一个OncePerRequestFilter包装HttpServletRequest对请求参数中的script等标签做转义处理。实现并不复杂但能挡掉大部分反射型XSS攻击。如果你处理的是文件上传场景比如上传PDF建议在上传接口单独校验文件扩展名和MIME类型不要依赖全局过滤器覆盖一切——安全防护永远是多层而不是靠一个过滤器打天下。8. 项目跑起来的完整手记建库、启动、验收最后把这套系统从零启动的完整流程梳理一遍方便你拿到源码后按部就班地跑起来。8.1 初始化数据库打开Navicat新建数据库work_stat字符集选utf8mb4。导入项目里的init.sql脚本会建好全表结构和基础数据管理员账号admin、演示用户、示例项目和任务。这里提醒一下字符集一定要选utf8mb4而不是utf8因为utf8mb4能完整支持emoji等四字节字符避免某些特殊文本插入时报错。过程中如果遇到表导入失败多半是SQL文件版本和MySQL版本不兼容把SQL文件里的ENGINEInnoDB DEFAULT CHARSETutf8mb4这些行删掉再执行基本就好了。8.2 启动后端用IDEA打开后端工程等待Maven依赖下载完成。修改application.yml中的数据库账号密码然后运行WorkStatApplication.java的main方法。看到“Netty started on port 8080”的日志就说明启动成功。这时可以用浏览器访问http://localhost:8080/api/auth/login测试接口返回JSON说明后端OK。8.3 启动前端前端工程目录执行npm install npm run serve默认端口8080很容易和后端冲突配置中我设置了前端跑在3000端口也建议你单独改动vue.config.js里的port。浏览器打开http://localhost:3000用admin账号登录进入系统后先填几条工作量数据再到报表页看统计结果整体流程就通了。8.4 验收清单系统跑通后建议做一遍这样的验收普通员工账号只能看到自己的填报记录不能看到别人数据。尝试重复填报同一天同一任务系统应提示已更新而不是报错。填报超过12小时后端应拒绝并返回友好提示。项目经理审核通过一条记录后个人月报表里的总数应立刻更新。直接用未登录状态访问/api/stats/personMonth应返回401前端自动跳转登录页。这套验收流程走下来基本能覆盖核心链路。如果你在实操中遇到其他的坑欢迎留言交流我会持续补充到项目文档里。我个人在实际项目中使用这套工作量统计系统的大半年时间里最大的体会是工作时间填报的门槛一定要足够低员工才愿意持续用。如果填一次要五分钟两周后大家就开始找借口不填了。所以我的建议是二次开发时优先做这两个优化方向一是通过企业微信或钉钉机器人每天下班前推送填报提醒二是把日报和周报自动生成省掉员工重复写汇报文档的时间。技术层面的路已经铺好剩下的就是结合团队实际情况把使用习惯培养起来。
阅读完成 · 觉得有帮助?
咨询建站