简介这份项目管理系统是一套基于 Java Spring Boot MySQL Vue 的前后端分离源码包适合需要快速搭建内部项目管理工具的开发人员、毕业设计学生或中小团队技术管理者。系统围绕项目全生命周期管理设计内置工时统计、原型预览、效果图管理等核心业务支持员工工时上报、项目工时汇总与人工成本核算能够以报表形式动态呈现项目投入情况帮助企业实时掌握进度与人效。资源压缩包共 1055 个文件约 9.63MB主要涵盖 410 个 Java 后端源码、132 个 Vue 前端页面及 149 个 JS 脚本并配有 7 个 SQL 初始化数据库脚本、yml 环境配置、scss 样式与构建运行脚本目录结构完整前后端模块清晰能直接用于二次开发或本地部署。除完整源码外还包含部分项目文档、图标与静态资源适合学习 Spring Boot 与 Vue 整合、工时业务建模及权限管理实现。该资源已有 409 人学习具有一定的实战参考价值可帮助读者快速理解从数据库设计到前后端联调的整体实现思路。1. 团队工时成本管理为什么值得用一套轻量系统团队规模过十工时统计靠 Excel 基本就失灵了——表格版本对不上、填报格式不统一月底对账往往要对到半夜。重型 OA 和 ERP 虽然带工时模块但配置流程和权限体系让中小团队望而却步。无鱼工时管理系统正是面向这个缝隙后端 Spring Boot 暴露 REST 接口Vue 负责页面交互MySQL 存业务数据覆盖项目创建、工时上报、日报汇总、工时统计和人工成本核算五个核心场景。适合按人天计价、需要按月核算项目投入的研发团队、外包项目组和咨询公司。源码包里同时包含构建脚本和前端构建产物拉下来就能本地跑通重点是把它启动之后再改到自己项目的场景里用。2. Spring Boot MySQL Vue 的架构分层与工时数据模型2.1 三个子系统的职责切分无鱼工时管理系统在技术选型上是典型的前后端分离结构三层各司其职。Spring Boot 层运行在 8080 端口可配置负责接收前端请求并完成业务校验。登录认证、项目增删改查、工时记录的写入与审批、统计聚合、日报生成全部在这一层完成。数据访问走 MyBatisSQL 写在 XML 里数据库连接和 MyBatis 驼峰映射等配置集中在 application.yml。MySQL 层保存所有持久化数据。开发环境默认连接本地 3306 实例sql 目录下的初始化脚本一次性创建全部表包含关联表、索引和少量初始数据导入后即可开始联调。字符集统一用 utf8mb4避免项目名称里的生僻字或 emoji 写入报错。Vue 层是浏览器端单页应用页面按功能拆成工作台、项目列表、工时填报、统计报表、原型预览、效果图管理六个模块由 vue-router 控制路由。开发阶段通过 axios 把 /api 前缀请求转发到 Spring Boot生产构建后由 Spring Boot 静态托管 dist 目录。前后端通过 JSON 交换数据字段名统一为驼峰风格数据库表中是下划线风格由 MyBatis 的map-underscore-to-camel-case: true配置自动完成映射。提示源码包里出现的 app.183cabda.css、chunk-*.js 这类文件名是前端构建工具对静态资源做的哈希命名。改动前端源码后重新构建会生成新哈希文件旧文件可直接清理不影响运行。2.2 工时统计的 ER 模型与建表 SQL系统中的核心实体是一个三元组人员、项目、工时。围绕它扩展出日报汇总表和附件表。先看三张核心表工时记录表是重中之重。CREATE TABLE t_work_report ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL COMMENT 上报人ID, project_id INT UNSIGNED NOT NULL COMMENT 所属项目ID, work_date DATE NOT NULL COMMENT 工作日期, work_hours DECIMAL(4,1) NOT NULL COMMENT 当日工时支持0.5小时, work_detail VARCHAR(500) DEFAULT COMMENT 工作内容描述, audit_status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已提交 2已审批, audit_remark VARCHAR(200) DEFAULT COMMENT 审批备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date_project (user_id, work_date, project_id), KEY idx_project_date (project_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工时填报记录表;work_hours 用 DECIMAL(4,1) 而不是整数是因为实际填报中经常出现 0.5 小时粒度整数类型只能放弃这一层精度。UNIQUE KEY 直接约束同一用户同一日期同一项目只存在一条记录从数据库层面防止前端重复提交导致数据翻倍。idx_project_date 则是专供按项目聚合工时的查询使用的复合索引。项目表 t_project 里有几个值得注意的字段业务含义如下字段类型业务含义project_statusTINYINT0 进行中1 暂停2 收尾收尾项目不再允许填报estimate_hoursDECIMAL(9,1)立项时预估的总工时用于和实际统计做对比start_date / end_dateDATE项目计划周期日报和预算判断都会引用deletedTINYINT软删除标记所有业务查询默认过滤 deleted0日报表 t_daily_summary 以日期为维度汇总每天的工时总量、参与人数和活跃项目数。它是定时任务算好落库的结果表不是明细表的视图目的是让统计页面避免反复扫描明细数据。2.3 索引与慢查询路径工时填报系统高频的查询有三类个人月视图、项目月汇总、全公司日汇总。对应 SQL 分别是-- 个人月度工时精确命中用户日期 联合唯一索引 SELECT work_date, work_hours, work_detail FROM t_work_report WHERE user_id 1 AND work_date BETWEEN 2025-01-01 AND 2025-01-31; -- 项目维度汇总命中 项目ID日期 复合索引 SELECT work_date, SUM(work_hours) FROM t_work_report WHERE project_id 5 AND work_date 2025-01-01 GROUP BY work_date;第一条语句走 uk_user_date_project 的最左前缀 user_id work_date第二条走 idx_project_date。如果新增了全公司日汇总报表考虑补一个单列索引 (work_date)这个索引体积小覆盖查询时还能跳过回表。十万行数据量以内这些索引足够用。真正需要注意的是统计接口里 JOIN 顺序写反先全表扫再过滤还是先过滤再连表执行计划差异能在百毫秒量级。通过 EXPLAIN 查看 Extra 列出现 Using filesort 或 Using temporary 时要调整 ORDER BY 和 GROUP BY 与索引的匹配顺序。3. 工时上报与统计的后端实现3.1 上报接口参数校验与防重复提交前端工作台发起一次工时上报请求落到 WorktimeController第一步是接收 WorkReportDTO 并执行基础校验public class WorkReportDTO { NotNull(message 用户ID不能为空) private Integer userId; NotNull(message 项目ID不能为空) private Integer projectId; NotNull(message 工作日期不能为空) JsonFormat(pattern yyyy-MM-dd) private LocalDate workDate; DecimalMin(value 0.5, message 最小工时为0.5小时) DecimalMax(value 24.0, message 单日工时不能超过24小时) private BigDecimal workHours; Size(max 500, message 工作内容描述最长500字) private String workDetail; }DTO 注解解决格式问题业务规则仍在 Service 层判断包括 workDate 是否晚于今天、项目是否处于收尾状态、userId 是否存在。这里有一个 Java 面试里常被问到的场景——同一用户当天对同一项目重复提交。常见做法是先查一次再决定 insert 还是 update但两个请求并发到达时会同时通过查询最终插入两条产生脏数据。更稳妥的方式是依赖表上的唯一索引直接捕获 DuplicateKeyExceptiontry { workReportMapper.insert(dto); } catch (DuplicateKeyException e) { throw new BizException(该日期该项目已存在填报记录请走修改流程); }Service 层不再单独查库逻辑既短又能防并发。需要注意 DuplicateKeyException 是 Spring 转译后的异常MyBatis 执行 SQL 时底层抛出的 DuplicateKeyException 会被 Spring 统一包装捕获时用org.springframework.dao.DuplicateKeyException。3.2 项目工时汇总JOIN GROUP BY 的写法统计报表页最常调用的接口是GET /api/stats/project?startDate...endDate...后端聚合项目维度的总工时。MyBatis 的 XML 里可以这样写SELECT p.id AS project_id, p.project_name, DATE_FORMAT(r.work_date, %Y-%m) AS stat_month, IFNULL(SUM(r.work_hours), 0) AS total_hours, COUNT(DISTINCT r.user_id) AS participant_cnt FROM t_project p LEFT JOIN t_work_report r ON r.project_id p.id AND r.audit_status 2 AND r.work_date BETWEEN #{startDate} AND #{endDate} WHERE p.deleted 0 GROUP BY p.id, p.project_name, stat_month ORDER BY stat_month DESC, total_hours DESC;用 LEFT JOIN 而不是 INNER JOIN是为了让当月没有任何填报的项目也出现在结果里total_hours 输出 0报表上仍然能看到这条项目记录而不是直接消失。audit_status 2 的条件写在 JOIN 的 ON 子句而不是 WHERE 子句否则 LEFT JOIN 会被数据库优化器改写成 INNER JOIN无填报记录的项目全被过滤掉这是排查统计丢行时最先要检查的地方。GROUP BY 里同时带上 p.id是为了兼容 MySQL 的 ONLY_FULL_GROUP_BY 模式同时保证项目重名时不会合并为一行。接口返回后前端把 total_hours 渲染成柱状图participant_cnt 显示在 tooltip 中查看者一眼能看出哪个项目投入人天最高。3.3 定时任务执行日报汇总与缓存失效每日日报并非实时聚合明细而是在夜间由定时任务算好写入 t_daily_summary第二天报表直接读结果表避免统计页面高峰期反复扫描明细表。实现方式Scheduled(cron 0 30 22 * * ?) public void generateDailySummary() { if (!distributedLock.tryLock(daily-summary, 5, TimeUnit.MINUTES)) { return; } try { LocalDate today LocalDate.now(); ListDailySummaryItem items reportMapper.aggregateByDay(today); if (items.isEmpty()) { return; } dailySummaryMapper.replaceIntoBatch(items); redisTemplate.delete(stat:company:daily); redisTemplate.delete(stat:project:month); } finally { distributedLock.unlock(daily-summary); } }cron 表达式0 30 22 * * ?表示每天 22:30 执行一次。分布式锁在单机部署时可有可无但项目做成多实例后两台机器会同时执行汇总任务需要借助 Redis 锁保证同一时间只有一个实例在跑。数据落库后再删除两个热点缓存 key第二天报表接口首次请求会重新从 MySQL 加载数据并写入缓存保证看到的不是昨天的旧值。项目成本核算同样可以在后端完成直接对比预估工时和实际工时SELECT p.estimate_hours, IFNULL(SUM(r.work_hours), 0) AS actual_hours, ROUND(IFNULL(SUM(r.work_hours), 0) / NULLIF(p.estimate_hours, 0) * 100, 1) AS used_percent FROM t_project p LEFT JOIN t_work_report r ON r.project_id p.id WHERE p.id #{projectId} GROUP BY p.id;NULLIF(p.estimate_hours, 0) 的目的在于预估工时为 0 时避免除零异常。used_percent 输出的是一个百分比数值前端可以直接绑定到项目进度条的 width 属性超过 100% 时标红表示该项目实际工时已经超支。4. Vue 前端工时填报、图表与原型/效果图管理4.1 工时填报页面的组件化与路由参数联动前端工时填报页是「表单 当日记录表格 周汇总图表」的组合。Vue Router 中项目详情页带参数跳转的写法如下const routes [ { path: /, redirect: /workbench }, { path: /workbench, component: Workbench, meta: { title: 工作台 } }, { path: /timesheet, component: Timesheet, meta: { title: 工时填报 } }, { path: /project/:projectId, component: ProjectDetail, props: true, meta: { title: 项目详情 } } ];props: true 让 /project/5 这样的路由直接把 projectId 数字传给组件的 props省去在组件内部反复写 useRoute().params.projectId。工时填报表单里日期控件默认今天并禁用未来日期工时输入框采用 0.5 步进这两个约束能减少大部分脏数据a-form-item label工作日期 a-date-picker v-model:valueform.workDate :disabled-dated d d Date.now() value-formatYYYY-MM-DD / /a-form-item a-form-item label工时(小时) a-input-number v-model:valueform.workHours :min0.5 :max24 :step0.5 / /a-form-itemdisabled-date 配合日期控件禁用未来日期input-number 限制工时只能落在 0.5 到 24 之间。提交时把按钮 loading 置为 true接口返回后再恢复防止快速连点产生重复请求。即便前端漏了这层防护后端唯一索引仍然会兜底拦截双保险避免了脏数据进入统计。4.2 统计图表的数据绑定与刷新统计页用 ECharts 绘制项目工时柱状图和人员投入折线图。数据流是页面挂载后请求接口拿到数组后调用 setOption组件卸载时释放实例。const loadStats async () { const { data } await axios.get(/api/stats/project, { params: { startDate: range.value[0], endDate: range.value[1] } }); chartRef.value.setOption({ xAxis: { type: category, data: data.map(i i.statMonth) }, series: [{ name: 工时, type: bar, data: data.map(i i.totalHours) }] }); };chartRef 是模板中div refchartRef styleheight: 320px/div的引用ECharts 实例初始化必须在 DOM 渲染完成后进行。日期范围变化时重新调用 loadStats只替换 series.data避免整张图表重新创建带来的闪烁。容易踩的坑是 ECharts 实例重复创建。onMounted 里 initonUnmounted 里 dispose这是干净的做法。如果统计页被 keep-alive 缓存那么 onActivated 生命周期再触发一次 loadStats切回页面时就能拿到最新数据不需要整页 reload。4.3 原型预览和效果图管理的文件组织原型预览和效果图管理在业务上都属于项目附件的呈现。后端用一张 t_attachment 表统一管理文件元数据CREATE TABLE t_attachment ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, project_id INT UNSIGNED NOT NULL, file_name VARCHAR(255) NOT NULL, file_type TINYINT NOT NULL COMMENT 1原型包 2效果图 3文档, file_path VARCHAR(255) NOT NULL, create_user INT UNSIGNED, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_project (project_id, file_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;原型包上传后解压到 upload/prototype/{projectId}/ 目录前端页面通过 iframe 指向 /api/file/preview?idxx后端将文件流写给浏览器渲染。效果图是图片文件前端使用 img 直接展示配合缩略图懒加载组件避免一次加载几十张大图导致页面卡顿。预览接口一定要做权限校验先根据参数 id 查出附件记录再验证当前登录用户是否属于该项目成员列表否则任意登录用户拿到 id 就能浏览其他项目的原型这在内部系统里是典型的越权漏洞。5. 源码包启动与二开避坑的三点建议5.1 build.bat、package.bat、run-web.bat 的分工源码包在 Windows 环境下预置了三个批处理脚本各自解决的问题不同脚本核心动作使用时机build.batMaven 打包后端 jar同时执行 npm run build 构建前端合并到 release 目录首次拿到源码想生成一份完整可发布包package.bat仅对后端执行 clean package -DskipTests复制 jar 到 release只改了 Java 代码前端无需重新构建run-web.bat启动后端 jar由内嵌 Tomcat 携带前端 dist 静态文件日常演示或部署到服务器build.bat 的关键在于检查上一步的 errorlevelMaven 失败时立即退出避免带着残缺的 jar 继续执行前端构建echo off cd /d %~dp0 call mvn clean package -DskipTests if errorlevel 1 exit /b 1 cd web call npm run build if errorlevel 1 exit /b 1 copy /y dist\* ..\release\web\ echo BUILD FINISHED5.2 首次启动的排错对照第一次拉代码跑起来最常出问题的三个位置集中在 MySQL 连接、前端依赖和端口占用启动报Public Key Retrieval is not allowed连接字符串加allowPublicKeyRetrievaltrue并确认 MySQL 驱动版本与 8.x 服务端匹配前端依赖卡在任何依赖安装上把 npm registry 切换到国内镜像后重新 install8080 被占用执行netstat -ano | findstr :8080拿到 PID 后结束进程或在 application.yml 中修改 server.port数据库时区偏差连接串追加serverTimezoneAsia/Shanghai并检查 MySQL 全局 time_zone5.3 二次开发值得优先改的三个位置在这个系统上继续迭代优先改三处。第一给 t_work_report 增加 cost_hourly 字段或关联岗位工资表把工时统计扩展成人工成本核算这是管理层使用系统时最关心的数据。第二把日报定时任务改为支持显式触发在 Controller 中增加一个 POST /api/daily/regenerate 接口项目经理修正数据后可以立即重新生成当天汇总不需要等到夜里。第三把 /api/file/preview 的文件路径解析改为 Path.resolve并且限定只能访问 upload 目录内的文件避免路径穿越安全扫描时更干净。这三个改动都不影响原有流程但对交付质量和后续审计有实际帮助。本文还有配套的精品资源点击获取