SpringBoot+Vue前后端分离工作量统计系统设计与部署
这段时间好几个做交付项目的朋友跟我吐槽每个月报工作量不是漏填就是口径对不上项目核算的时候翻聊天记录去补工时。我就花了点时间把这套“前后端分离工作量统计系统”整理了出来技术栈是SpringBoot Vue MyBatis MySQL包含了完整源码和从零到一的部署教程。系统主要解决三个问题成员每天填报工时和工作内容、管理者按人员和项目维度统计工作量、所有数据可回溯可导出。适合需要做研发团队效能统计、项目成本核算的同学参考也适合想系统了解前后端分离项目完整落地过程的读者。下面把设计和实现的关键路径完整梳理一遍。1. 手工月报表的痛这套工作量统计系统到底解决什么问题1.1 统计这件事怎么就从简单变成了一地鸡毛先说说没有这套系统之前的真实场景。团队规模在十个人以内的时候工作量统计通常一个Excel就能搞定每个人在共享表格里填一行任务描述和工时月底负责人汇总一下看起来没问题。一旦人员超过十五个、同时有多个项目在跑Excel的弊端就全出来了单元格格式被改、日期格式五花八门、有人忘了填、有人把任务描述写得很随意汇总的时候根本没法快速核算哪个项目投入了多少人力。更麻烦的是口径统一问题。有人说“我今天干了三个小时”有人说“我今天报了五小时”实际投入到具体项目的工时到底是多少完全对不上。到了月底做项目复盘或人力成本分析时数据严重失真老板要一个各成员工作量报表光整理就耗掉大半天。所以我做这套系统的出发点很简单把填报动作收敛到唯一入口用强制规则保证数据格式统一再用SQL把汇总计算交给数据库而不是靠人手去加。1.2 系统的核心能力和最终交付形态这套工作量统计系统的核心能力归纳下来就四条用户登录与角色权限普通成员和管理员走同一套登录入口角色不同看到的功能和范围不同。成员只能填报和查看自己的记录管理员可以看全员数据、按项目和人员维度拉统计。工作日志填报成员每天选择所属项目、填写任务描述、填入工作时长和工作日期支持修改和删除自己范围内的记录。多维统计汇总按日期区间、按人员、按项目三个维度汇总工时输出数字报表和图表。部署开箱可用后端用Maven打包成可执行jar包前端构建成静态文件部署到内网服务器之后通过Nginx对外提供服务。它的最终交付形态就是一个部署在内网Tomcat服务或独立服务器上的Web应用前端浏览器访问不走公网也可以正常使用。源码层面后端是一个标准的分层项目前端是Vue工程数据库初始化脚本独立提供拿过去就能跑。2. 盘点技术栈为什么是SpringBootVueMyBatisMySQL这个组合2.1 四件套的配合逻辑很多刚接触前后端分离的同学会问为什么不直接用SpringCloud为什么不配个Redis为什么ORM选了MyBatis而不是JPA。对这个场景来说这套组合是最轻、最稳、最好招人的方案。先说SpringBoot。工作量统计系统本质是围绕业务表的增删改查和统计汇总根本不需要微服务那套注册中心、配置中心、网关的复杂体系。SpringBoot能把内嵌Tomcat、自动配置、健康检查这些全部搞定一个依赖引入就少掉一大半繁琐的XML配置非常适合这种中小型内部系统。Vue负责前端交互。统计系统的核心交互是表单填写、表格展示、图表渲染属于典型的单页应用场景。Vue的响应式数据绑定让列表和表单联动变得很直接配合Element UI或Element Plus组件库表格分页、日期选择器都现成可用。MyBatis在这套系统里的价值是可控SQL。统计逻辑比普通CRUD更依赖特定SQL写法比如按日期区间分组求和、按项目聚合、多表联查。MyBatis的XML Mapper可以把这些复杂SQL写得很清晰想问什么问题直接看SQL文本不需要猜JPA自动生成的语句是怎么拼出来的。MySQL就更没悬念了内部系统的数据量级通常远没到需要分布式存储的程度单库单表配InnoDB和合理索引就完全够用运维成本也低。整个组合的核心逻辑是把复杂度控制在“能用、易改、好维护”的范围里而不是追求技术上的花哨。2.2 关键版本与依赖说明我实际落地的版本是这样的组件版本说明JDK1.8后端运行环境稳定兼容SpringBoot2.7.x内嵌Tomcat自动配置MyBatismybatis-spring-boot-starter 2.3.x数据库ORMMySQL5.7或8.0均可生产环境用的8.0Vue3.x Vite前端工程化Element Plus2.xUI组件库ECharts5.x统计图表JWTjjwt 0.9.x登录令牌这里有个经验可以分享前端如果团队里大家更熟Vue2可以直接改用Vue2.6加Element UI逻辑几乎不用动只是API写法有细微差异。我这个版本的代码以Vue3为主迁移成本不高。后端JDK不建议一开始就上17SpringBoot的版本兼容性容易折腾JDK1.8是最不出错的底子。3. 数据库设计先行5张核心表的字段思路与关联关系3.1 表结构总览工作量统计系统的数据模型不需要复杂但五张表的职责边界必须清楚。表结构先设计好后面的业务代码才不会反复推倒重来。user用户表存储账号、密码、姓名、部门、角色。project项目表存储项目名称、描述、状态。work_log工作日志表存储填报主体数据。leave_record可以留作请假/缺勤记录扩展也可暂不建。operation_log操作日志表记录关键数据的增删改动作。最小可用版本必须先有前三种。operation_log可以帮助排查谁改了什么数据团队内部系统很有必要。SQL脚本utf8mb4字符集必须设置否则碰到中文特殊符号容易乱码。3.2 一张表的字段是怎么定下来的以work_log为例work_log是整个系统的核心表字段怎么定直接影响统计准确性。建表脚本如下CREATE TABLE work_log ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 填报人ID, project_id bigint(20) DEFAULT NULL COMMENT 所属项目ID, task_desc varchar(500) NOT NULL COMMENT 任务描述, work_date date NOT NULL COMMENT 工作日期, duration decimal(5,1) NOT NULL DEFAULT 0.0 COMMENT 工作时长单位小时, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_date (user_id, work_date), KEY idx_project_date (project_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工作日志表;字段设计里有几个容易踩坑的点。duration为什么用decimal(5,1)而不是double小时数精确到0.1小时相当于6分钟既能满足统计精确度又避免浮点数求和时的精度脏数据。填报界面可以把工时控件改成可选择0.5、1、2、4等多档配合输入框校验最大程度保证数据规范。work_date用date类型而不是datetime统计天然是按天聚合的日期类型在SQL里可以直接用BETWEEN和GROUP BY省掉一层类型转换。联合索引idx_user_date和idx_project_date这是为统计查询服务的。最常见的查询是“某人在某段时间内的工时”和“某项目在某段时间内的工时”联合索引能有效避免全表扫描。数据量即便涨到几十万条这套索引设计也能保持查询在毫秒级。user表的密码字段固定存BCrypt哈希值比如{bcrypt}$2a$10$...明文密码一律不允许入库。project表除了基础信息外建议加一个status字段1进行中、2已暂停、3已结束避免删数据导致历史日志关联不上。4. 后端搭建与接口实现登录、填报、统计三条链路4.1 工程结构与登录鉴权后端工程目录采用标准分层结构每个包职责单一新接手的人一眼就能看懂com.example.workstats ├── WorkStatsApplication.java ├── common │ ├── Result.java -- 统一返回体 │ ├── JwtUtil.java -- JWT生成与解析 │ └── AuthInterceptor.java -- 登录拦截器 ├── controller │ ├── AuthController.java │ ├── WorkLogController.java │ └── StatsController.java ├── service ├── mapper └── entity登录接口的核心逻辑是这样用户提交账号密码后端查询用户表并用BCryptPasswordEncoder.matches()校验密码校验通过后生成JWT返回前端PostMapping(/api/auth/login) public Result login(RequestBody LoginRequest req) { User user userMapper.findByUsername(req.getUsername()); if (user null || !passwordEncoder.matches(req.getPassword(), user.getPassword())) { return Result.error(账号或密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return Result.success(new LoginResponse(token, user.getRealName(), user.getRole())); }JWT生成的载荷里别放敏感信息只要用户ID、用户名、角色就够了默认过期时间设定为24小时。登录成功后前端在本地存储token每次请求在Header里带Authorization: Bearer xxx。鉴权用拦截器实现而不是引入Spring Security全家桶原因是这个系统只有两类角色拦截器加注解的配置方式足够且可读性更好。拦截器里对/api/auth/login和静态资源路径做放行其余请求都校验token校验失败直接返回401。4.2 填报与查询链路填报接口要解决的第一个问题是数据归属。普通成员提交的日志userId必须从token里取不能信任前端传进来的参数否则会员就能替别人填报。管理员虽然可以查看所有数据但不能替他人新增避免数据伪造。新增日志的Service方法大致如下public void addLog(WorkLog log, Long currentUserId) { if (log.getWorkDate() null || log.getDuration() null || log.getDuration() 0) { throw new BizException(日期和时长必须填写时长必须大于0); } if (log.getDuration() 24) { throw new BizException(单条日志时长不能超过24小时); } log.setUserId(currentUserId); workLogMapper.insert(log); operationLogMapper.insert(ADD_WORK_LOG, currentUserId, log.getId()); }查询链路按角色划分场景。普通成员调用GET /api/log/list后端强制把userId设置为当前登录人再配合分页参数返回管理员可以传userId作为条件查看指定成员的数据不传则默认查全部。分页用MySQL的LIMIT offset, size前端传pageNum和pageSize后端做简单换算。修改和删除同样要校验归属成员只能操作自己的日志管理员可以操作所有日志。这个校验在Controller层和Service层各做一遍后端必须自己兜底不能依赖前端隐藏按钮来保证安全。4.3 统计接口的SQL写法统计是本系统最有含金量的部分。三个核心统计维度对应三条SQL路线。按时间区间统计某成员每日工时select idstatsPersonalDaily resultTypemap SELECT work_date AS date, SUM(duration) AS totalDuration FROM work_log WHERE user_id #{userId} AND work_date BETWEEN #{startDate} AND #{endDate} GROUP BY work_date ORDER BY work_date /select按项目统计某段时间内的工时占比select idstatsByProject resultTypemap SELECT p.project_name AS projectName, COALESCE(SUM(w.duration), 0) AS totalDuration FROM project p LEFT JOIN work_log w ON p.id w.project_id AND w.work_date BETWEEN #{startDate} AND #{endDate} WHERE p.status 1 GROUP BY p.id, p.project_name ORDER BY totalDuration DESC /select注意这里用了LEFT JOIN而不是INNER JOIN。目的很明确没有产生工作量的进行中项目也要显示出来工时显示0否则管理者根本不知道哪些项目处在“挂着但没人投入”的状态。按成员汇总其在一段时间内总工时SQL写法类似只是把分组条件换成user_id和用户姓名表。ECharts图表的数据源就是这些接口返回的Map列表前端不用做任何二次聚合直接塞给图表配置即可。5. 前端页面实现表单、列表与图表可视化5.1 项目目录和路由设计前端用Vue3加Vite创建。目录结构里几个关键位置如下workstats-web/ ├── src │ ├── api │ │ ├── auth.js │ │ ├── log.js │ │ └── stats.js │ ├── router/index.js │ ├── store/user.js │ ├── views │ │ ├── Login.vue │ │ ├── WorkLog.vue │ │ └── StatsView.vue │ └── utils/request.jsutils/request.js用axios做封装统一设置baseURL和请求拦截器。请求拦截器里从localStorage取token并拼到Header响应拦截器里统一处理401状态码遇到token过期就跳转登录页。这一层建议一开始就封装好否则每个页面手写一遍Header逻辑后面改起来很痛苦。路由设计上未登录状态一律重定向到/login登录后动态渲染菜单。管理员和普通成员的路由表可以做成两份管理员额外挂载“全员统计”页面普通成员只有一个“我的统计”页面前端控制显示之外后端接口权限仍然要做二次校验。5.2 页面级交互逻辑工作日志页是使用频率最高的页面拆成两个部分上部分是填报表单下部分是查询列表。关键点在于表单提交后列表数据自动刷新列表的编辑操作回填表单。实现方式很简单提交按钮的loading状态期间请求后端成功后关闭弹窗并重新加载列表。填报表单示例el-form refformRef :modellogForm :rulesrules el-form-item label工作日期 propworkDate el-date-picker v-modellogForm.workDate typedate value-formatYYYY-MM-DD / /el-form-item el-form-item label所属项目 propprojectId el-select v-modellogForm.projectId placeholder请选择项目 el-option v-forp in projectList :keyp.id :labelp.projectName :valuep.id / /el-select /el-form-item el-form-item label任务描述 proptaskDesc el-input v-modellogForm.taskDesc typetextarea :rows3 maxlength200 show-word-limit / /el-form-item el-form-item label工作时长(小时) propduration el-input-number v-modellogForm.duration :min0.5 :max24 :step0.5 / /el-form-item el-form-item el-button typeprimary :loadingsubmitting clickhandleSubmit保存/el-button /el-form-item /el-formel-input-number的step属性设为0.5非常关键能把常见的“1.3小时”“0.7小时”这类难以核算的数据直接挡在门外。规范化的数据录入是统计系统最容易被忽略的设计点前端的约束越多后端的统计越干净。查询列表用el-table列字段包括日期、项目、任务描述、工时、操作。操作列根据角色来控制普通成员只能看到“编辑/删除”管理员额外看到全部数据但编辑权限同样受后端限制。分页用el-pagination组件current-page和page-size与后端请求参数绑定。统计图表页用ECharts。至少有三种图表成员每日工时柱状图、各项目工时占比饼图、成员工作量排行条形图。柱状图代码示例const chart echarts.init(document.getElementById(dailyChart)) chart.setOption({ title: { text: 本月每日工时分布 }, tooltip: { trigger: axis }, xAxis: { type: category, data: dateList }, yAxis: { type: value, name: 工时(h) }, series: [{ type: bar, data: durationList, barWidth: 20 }] })统计接口返回的日期列表和工时时长列表前端直接用数组解析。一个细节后端返回的totalDuration是BigDecimal类型JSON序列化后是数字前端无需转字符串但浮点数精度问题需要留意。把duration统一按小时计返回值保留一位小数图表上显示“3.5h”这种格式观感最好。6. 完整部署教程从打包到内网可访问6.1 初始化MySQL部署的第一步是准备数据库环境。新建数据库和导入脚本按顺序执行写成一个init.sql文件后可以用一条命令完成导入mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS workstats DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p workstats init.sql导入完成后记得执行一条验证SQL确认表结构正常SHOW TABLES; SELECT COUNT(*) FROM user;如果user表为空需要先插入一个管理员账号和初始测试账号。密码使用BCrypt加密可以在后端程序启动后通过临时接口生成也可以直接使用在线BCrypt生成工具生成对应哈希值再插入数据。插入时务必填好role字段管理员填admin普通成员填user。6.2 后端打包与启动后端打包之前先检查application.yml里的数据库连接配置。生产环境的连接串一定不要使用本机的localhost改为服务器实际IP。MySQL 8.0需要在连接串上追加serverTimezoneAsia/Shanghai否则会产生时区差异导致的日期错位问题spring: datasource: url: jdbc:mysql://127.0.0.1:3306/workstats?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword打包命令很简单但跳过测试是必须的本地测试环境如果连不上数据库容易导致打包中断mvn clean package -DskipTests执行成功后target目录下出现workstats.jar。启动用nohup方式挂后台nohup java -jar workstats.jar --server.port8080 app.log 21 启动后先不急着配前端用curl验证接口连通性curl -X POST http://127.0.0.1:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}返回JSON里带token就说明后端和数据库链路完全正常。如果是防火墙拦截检查服务器的8080端口是否对外开放。6.3 Nginx托管前端与反向代理前端构建前需要确认接口代理地址。在Vite项目的.env.production文件里设置VITE_API_BASE_URL/api构建命令npm install npm run build构建产物在dist目录。把整个dist目录上传到服务器的/opt/workstats-web下随后配置Nginx。这份配置涵盖了静态文件托管、接口反向代理、路由history模式兜底三个核心点server { listen 80; server_name your-server-ip; root /opt/workstats-web; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这里有两个细节必须注意。proxy_pass后面带了/api/路径代理时会把原URL的/api前缀保留下来和后端Controller的RequestMapping(/api)正好对应。try_files是Vue Router history模式的生命线没有这一行前端路由刷新时就会报404。配置完重载Nginxnginx -t systemctl reload nginx浏览器直接访问服务器IP看到登录页就代表部署完成。7. 上线后的运维经验避坑记录与扩展建议7.1 踩过的3个典型坑这套系统从开发到稳定的过程中有几个坑非常典型值得单独记录下来。第一个坑是时区问题。最初后端连接串没有加serverTimezoneMySQL 8.0默认时区是UTC前端填报的2025-06-01存入数据库后变成了2025-05-31第2天统计结果整体偏移一天。排查过程不算复杂但问题很隐蔽。解决方式就是连接串统一指定Asia/Shanghai同时work_date字段用date类型从源头避免时区干扰。第二个坑是MyBatis的if test字符串判断。项目里查询接口支持可选日期区间和可选用户筛选XML里写if teststartDate ! null and startDate ! 时之前误写成! 以外还加了and startDate ! null结果前端传了空字符串时SQL正常传null时反而拼出错误条件。后来统一规范参数尽量包一层DTO所有可选字段都用null判断前端如果参数为空就不传该字段。第三个坑是服务器内存吃紧。项目部署在一台只有2G内存的云服务器上MySQL、后端、Nginx挤在一起mvn package时经常OOM。后来把打包本机和运行环境分开本机打包上传jar包服务器上直接用Maven的编译产物避免在低配服务器上跑大型编译任务。如果必须在服务器上编译建议先加swap分区这个问题就迎刃而解。7.2 下一步可以扩展的方向系统上线稳定以后接下去最值得做的扩展有三个方向。第一是Excel导出。统计结果用ECharts看可以但管理层通常更习惯要Excel。后端引入Apache POI或阿里EasyExcel把统计接口返回的数据转成多sheet的Excel文件按项目报表和工作量明细两个sheet导出非常实用。第二是自动提醒填报。团队成员的填报率初期一定不会是100%可以加一个定时任务每天下午5点扫描当天未填报的用户列表通过系统消息或Webhook方式提醒未填报人员。最简单的实现是SpringBoot的Scheduled注解配合定时任务每30分钟执行一次即可。第三是审核机制。目前管理员只能看数据不能修正。更完整的流程是成员提交后管理员有审核通过或驳回的操作驳回时填写原因并通知成员修改。如果需要严谨的成本核算这个功能几乎必然要补上。最后说点个人体会。这类内部管理系统最容易犯的设计错误是一上来就做得很重又是流程引擎又是权限矩阵真正上线以后发现用户根本不买单。顺着团队的使用习惯把填报、统计、看数这三件事做流畅比堆功能有效得多。如果你们团队也为工作量统计发愁完全可以照这套方案先搭一版数据模型简单、部署成本低跑起来再迭代基本不会走弯路。