接手过不少外包项目,自己也带过团队做内部系统,说句实话,SpringBoot+Vue 这套组合在中小型企业的项目管理系统里,出镜率实在太高了。刚好最近整理了一份基于 Java+MySQL 的企业项目管理系统源码,从技术选型到部署上线全流程跑了一遍。这套东西特别适合拿来当毕业设计、课程设计,或者纯学习练手。今天就把里面的门道、坑点和实操细节一次讲透,想省事的直接抄作业就行。
1. 项目到底做了什么,解决了什么问题
先把这个系统的价值说清楚。企业里管项目最头疼的几件事:项目进度不透明、任务分配靠吼、成员忙闲不均、文档散落各处、高层问起来没人能说清楚当前状态。这套系统核心就是把项目的全生命周期管理搬到线上,从立项、排期、执行、交付到归档全部留痕,同时把任务分解、进度跟踪、成员协作、文档沉淀这些日常动作固化到一套标准流程里。
技术层面,后端用 SpringBoot 提供 RESTful API,前端用 Vue 全家桶搭建 SPA 单页应用,数据库用 MySQL 存业务数据,Redis 做缓存和会话管理。这套组合的好处是:前后端完全分离,接口契约清晰,各自可以独立开发测试;生态成熟,出了问题网上一搜一大把答案;岗位需求量大,学会了这套技术栈找工作也有优势。
适合谁来参考?如果你是准备毕设答辩的在校生,这套系统功能完整度足够撑起“企业级”三个字;如果你刚工作想熟悉主流框架整合,跟着源码过一遍能省不少摸索时间;如果你在带小团队搞内部工具,这套东西改改就能用。
提示:源码基于 JDK 8 + SpringBoot 2.x + Vue 2.x + MySQL 5.7 开发,这套组合兼容性最好,网上资料最全。没必要为了追新一上来就用 JDK 17 和 SpringBoot 3.x,很多老教程和依赖会踩坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统功能模块拆解,每个模块的数据库设计与实现逻辑
这套系统的功能模块不是随便堆砌的,每一个都对应企业管理里一个真实痛点。我把核心模块拆开讲,同时给出关键表结构和实现思路。
2.1 用户认证与权限管理模块
不管什么系统,认证授权永远是第一道门。这块用的技术是 Spring Security + JWT Token,密码加密用 BCrypt,不是 MD5,MD5 现在撞库太容易了,毕设答辩时如果面试官问起为什么选 BCrypt,要能答上来“加盐抗彩虹表”。
数据库这边主要就两张表:用户表 sys_user 存账号密码昵称状态,角色表 sys_role 存角色编码和名称,中间还有一张用户角色关联表。这套设计是标准的 RBAC 模型,即“用户-角色-权限”三层,用户不直接绑权限,而是绑角色。这样做的好处是,新增用户时只要指派角色就能继承一组权限,管理成本低很多。
实现上,登录接口接收用户名密码后,校验通过就生成一个 JWT,把用户ID和角色信息放进去,过期时间设置成 2 小时。前端把 Token 存在本地存储里,每次请求在 Axios 拦截器里把 Token 塞到请求头。后端用过滤器统一解析校验,一步到位。
2.2 项目立项与信息管理模块
这个模块是企业项目管理系统的心脏。项目不是随便谁都能建的,必须走审批流。管理员或项目经理提交立项申请,填项目名称、编号、负责人、预算、计划开始结束时间、项目描述,提交后由高层角色审批,审批通过才正式进入项目库。
表设计上,project 表设计成一张宽表,能冗余的都冗余进去。字段包括项目编码、名称、状态(草稿/审批中/进行中/已延期/已完成/已归档)、优先级、预算金额、实际花费、负责人 ID、客户名称、项目描述、创建时间等。之所以把客户名称直接冗余进来而不是单独建一张客户表,是因为对大多数中小企业的项目管理场景,项目跟着客户走的需求没那么强,一张宽表查询效率更高,代码也更简单。
这里一个关键的实现点是通过自定义查询条件构建器来支持多条件分页搜索,后端用 MyBatis-Plus 的 QueryWrapper 拼条件,前端表格列头带筛选框,支持按状态、负责人、时间范围、关键字模糊查询。项目列表要支持按创建时间倒序、按截止时间正序等排序,避免一次查出全表,分页必须得做。
心得:项目编码不要用自增ID,要显示给业务人员看的话建议用手工规则,比如“PRJ+年月日+三位流水号”,这样打印出来贴在白板上也专业。代码里用 Redis 的 INCR 指令生成当天流水号,简单可靠。
2.3 任务分解与进度跟踪模块
往下拆就是任务管理。一个项目立项后,负责人要把它拆成若干任务包,任务包再拆成具体任务。所以表设计上任务表必须带上级 ID 和项目 ID 双外键,支持多级树形结构。我自己用过的设计是:每个任务有标题、描述、指派人、优先级、预估工时、计划开始/结束时间,实际开始/结束时间、完成进度百分比、任务状态(未开始/进行中/已完成/已挂起)、关联文档 ID 列表等字段。
进度百分比这个字段,不搞自动化计算,就靠负责人和成员手动更新,因为真实企业里“代码写了50%”这种进度很难靠系统算出来。但是报表统计时会用这个字段做汇总,算项目的平均进度、超期任务数、成员负载等。状态流转按固定规则走,不能从已完成退回到未开始,防止乱改。
任务看板用 Kanban 泳道图展示,前端用 Vue.Draggable 实现拖拽换列。拖拽的本质是调用一个更新接口,把任务 ID 和新状态传过去。这里要注意拖拽时做乐观更新,先改本地数据,等确认成功后再刷新,不然在慢网环境下卡片会“飘”回原位,体验很糟糕。
2.4 工时填报与项目成本模块
很多项目管理系统会忽略工时管理,但实际企业里最关心的就是人天成本。这套系统里单独有一张工时表,每天成员填报“项目A今天投入了4小时,做了需求分析”,按周汇总就能算出每个项目的实际人力成本。
表结构很简单:work_hours 表包含任务ID、项目ID、用户ID、投入日期、工时数、工作内容描述。关键点是捅数统计报表,用 SQL 的 GROUP BY 按月、按人、按项目维度汇总,配合一个简单的趋势图组件展示。后端用 MyBatis-Plus 的聚合查询实现,前端用 Apache ECharts 画柱状图和饼图。
这一步能让你在毕设答辩时从“做了个 CRUD”变成“做了个能算账的系统”,档次直接提升。如果时间来得及,建议把项目实际花费累计逻辑也做了,简单方式就是工时表折算人力成本加上物料成本字段,项目详情页显示总成本,好不好用另说,思路是对的。
2.5 项目文档与知识沉淀模块
项目文档管理做的是企业网盘的简化版。用到的文件存储方案是本地磁盘目录存储,数据库存文件的原始名、存储路径、大小、上传人、上传时间、关联项目ID。为什么不用 FastDFS 或者 MinIO?因为部署环境可能要上云,而且毕设演示时本地跑最稳妥,零依赖。
前端文件上传用 Element UI 的 Upload 组件,后端统一接收 MultipartFile,存完返回文件ID,前端再拿 ID 去关联业务数据。下载时加一个 权限校验拦截器,防止越权下载其他项目的合同或立项材料。这个安全细节很多人会漏,我建议一定加上,面试官问到权限设计时这就是亮点。
2.6 消息通知与待办中心
最后是消息通知模块,内部集成了站内信系统。当有任务被指派、项目审批通过/驳回、里程碑到期时,系统自动往收件人的消息表插一条记录,同时待办中心显示未读数。实现用的是 Spring 的事件机制:任务指派时发一个 TaskAssignedEvent,监听器去查收件人,发站内信。没有集成邮件或短信,降低复杂度。
前端在导航栏右上角放一个铃铛图标,轮询一个未读接口,有新消息就显示红点数字。轮询间隔5秒比较合理,太频繁会给服务器造成压力。这块是实现“实时感”的最小成本方案。
3. 技术栈选型与项目工程结构设计
选型决定了开发效率的上限,我把这套系统的技术选型细节和理由一并给出来,方便你答辩时讲清楚“为什么”。
3.1 后端涉及的组件与选型理由
后端基础框架用 SpringBoot 2.7.x,这是 2.x 系列的最后一个大版本,稳定到不能再稳定。ORM 层选了 MyBatis-Plus,不是 MyBatis 原生,原因很简单:单表 CRUD 不需要手写 XML,内置的 BaseMapper 直接继承就能用,分页插件一键配置,代码量能省一半。复杂多表查询再手写注解 SQL,两边兼顾。
安全框架用 Spring Security,虽然配置比 Shiro 繁琐,但 Spring 生态整合无缝,BCrypt 加密、方法级权限注解 @PreAuthorize 这些都是现成的。操作日志用 AOP + 自定义注解记录,谁在什么时候操作了哪张表,切面统一处理,业务代码零侵入。
接口文档用 Knife4j(Swagger 的增强版),比原生 Swagger UI 好看得多,还支持离线文档导出。写接口时注解标注一下,前端联调时直接看文档,省掉大量沟通成本。
3.2 前端工程结构与路由设计
前端用的是 Vue 2.6 + Element UI + Vuex + Vue Router + Axios 这套经典组合。工程目录按功能模块划分,而不是按文件类型堆。我的划分是:
src/api按后端模块拆分接口请求函数,比如api/project.js、api/task.js,封装 Axios 实例src/router路由表,一二级路由嵌套src/storeVuex 分模块管理,用户信息、菜单权限、消息未读数src/views页面组件,按模块建文件夹src/components公共组件,比如文件上传组件、分页组件
路由设计了一块很重要:路由懒加载。用 component: () => import('@/views/project/index.vue') 的方式,按需加载页面打包后的 JS 文件,避免首屏加载几 MB 的大文件。这个在大项目里是刚需,在毕设里体现的是工程素养。
路由守卫用的是全局前置守卫 router.beforeEach,判断本地有没有 Token:
- 没有 Token 就重定向到登录页
- 有 Token 但访问的页面要求特定角色,就把用户角色信息从 Vuex 里取出来比对
- 比对不通过就跳到 403 页面
3.3 前后端接口契约设计
接口设计走 RESTful 风格,统一返回体 Result<T>,包含 code、message、data 三个字段。分页返回对象 PageResult<T> 包含 total、records、current、size。定好契约后,前后端可以用 Mock 数据并行开发,不用等接口写完了才开始做页面。
接口前缀统一 /api,后端在控制器上加上类级 @RequestMapping("/api/project"),前端 Axios 的 baseURL 也设成 /api。部署时用 Nginx 做反向代理,把所有 /api 请求转发到后端服务端口,规避跨域问题,开发环境则用 Vite 或 Vue CLI 的 proxy 配置转发。前后端分离项目,跨域问题基本都靠这个解决,不要在后端代码里全局放开跨域注解,生产环境有安全风险。
4. 核心流程实操:手把手带你落地一个完整功能模块
光说不练假把式,我用“任务指派”这个最核心的功能,从数据库到前端展示串一遍完整流程,这就是你开发时最标准的跳舞模板。
4.1 数据库建表:任务表与关联表
建表语句要斟酌,字段类型都得匹配好。任务表主表重要字段如下:
sql复制CREATE TABLE `task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_code` varchar(32) DEFAULT NULL COMMENT '任务编码',
`project_id` bigint(20) NOT NULL COMMENT '所属项目ID',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父任务ID,0表示根任务',
`task_name` varchar(128) NOT NULL COMMENT '任务名称',
`description` text COMMENT '任务描述',
`assignee_id` bigint(20) DEFAULT NULL COMMENT '指派人ID',
`priority` tinyint(4) DEFAULT '2' COMMENT '优先级 1高 2中 3低',
`status` tinyint(4) DEFAULT '0' COMMENT '状态 0未开始 1进行中 2已完成 3挂起',
`progress` int(11) DEFAULT '0' COMMENT '进度百分比',
`plan_start_time` datetime DEFAULT NULL,
`plan_end_time` datetime DEFAULT NULL,
`actual_start_time` datetime DEFAULT NULL,
`actual_end_time` datetime DEFAULT NULL,
`estimated_hours` decimal(10,2) DEFAULT NULL COMMENT '预估工时',
`actual_hours` decimal(10,2) DEFAULT NULL COMMENT '实际工时',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
KEY `idx_project_id` (`project_id`),
KEY `idx_assignee_id` (`assignee_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='任务表';
强调几个细节。第一,逻辑删除用 deleted 字段,不做物理删除,这样数据不会丢,报表统计时统一过滤 deleted=0。第二,外键逻辑关联不建物理外键,通过普通索引保证查询性能即可,可以用服务层代码保证数据完整性。物理外键在分库分表、大数据量迁移时就是灾难。第三,时间字段要区分“计划”和“实际”,这是后续统计延期的基础数据,如果合并成一个字段,报表就做不准了。
4.2 后端代码实现:三层架构与核心逻辑
后端严格 Controller-Service-Mapper 三层架构。Controller 只做参数接收和结果返回,Service 写业务逻辑,Mapper 负责数据库操作。以创建任务为例,Service 层核心逻辑要有:
java复制@Override
@Transactional(rollbackFor = Exception.class)
public TaskVO createTask(TaskCreateRequest request) {
// 1. 校验项目是否存在且非归档状态
Project project = projectMapper.selectById(request.getProjectId());
if (project == null || project.getStatus() == ProjectStatus.ARCHIVED.getCode()) {
throw new BusinessException("项目不存在或已归档");
}
// 2. 生成任务编码
String taskCode = taskCodeGenerator.generate();
// 3. 组装实体并插入
Task task = new Task();
BeanUtils.copyProperties(request, task);
task.setTaskCode(taskCode);
task.setStatus(TaskStatus.NOT_STARTED.getCode());
task.setProgress(0);
task.setDeleted(0);
taskMapper.insert(task);
// 4. 发送消息通知被指派人
applicationEventPublisher.publishEvent(new TaskAssignedEvent(task));
// 5. 返回视图对象
return taskMapper.selectTaskVOById(task.getId());
}
注意这里用了 @Transactional 事务注解,创建任务的同时要更新项目状态、发通知,任何一个环节失败整体回滚,避免出现“任务建了但项目状态没变”这种脏数据。事件机制让代码解耦,任务创建后不关心通知怎么发,后续想加短信、邮件都只要在监听器里扩展。
Mapper 层复杂查询用注解 SQL。比如查询一个项目的全部任务树,要带项目名、指派人姓名、任务权重字段:
java复制@Select("<script>" +
"SELECT t.*, p.project_name, u.real_name AS assignee_name " +
"FROM task t " +
"LEFT JOIN project p ON t.project_id = p.id " +
"LEFT JOIN sys_user u ON t.assignee_id = u.id " +
"WHERE t.deleted = 0 AND t.project_id = #{projectId} " +
"<if test='status != null and status != -1'> AND t.status = #{status} </if>" +
"ORDER BY t.create_time DESC" +
"</script>")
List<TaskVO> selectTaskVOList(@Param("projectId") Long projectId,
@Param("status") Integer status);
动态 SQL 用 <script> 标签包裹,写 <if> 判断可空条件,这是 MyBatis 比 JPA 灵活的地方。LEFT JOIN 别名在 VO 里接收即可。
4.3 前端页面实现:任务看板与拖拽交互
任务看板页是这套系统的门面。页面布局用 Element UI 的 el-row 和 el-col 分三列对应“未开始/进行中/已完成”三个泳道,每列内部用 v-for 渲染任务卡片。
vue复制<template>
<div class="kanban-container">
<el-row :gutter="16">
<el-col :span="8" v-for="column in columns" :key="column.status">
<div class="kanban-column">
<div class="column-header">
<span>{{ column.title }}</span>
<el-tag size="mini" type="info">{{ column.tasks.length }}</el-tag>
</div>
<draggable
:list="column.tasks"
group="task"
:animation="200"
@change="handleDragChange($event, column)"
>
<div class="task-card" v-for="task in column.tasks" :key="task.id">
<div class="task-title">{{ task.taskName }}</div>
<div class="task-meta">
<span>{{ task.assigneeName }}</span>
<span :class="priorityClass(task.priority)">{{ priorityText(task.priority) }}</span>
</div>
</div>
</draggable>
</div>
</el-col>
</el-row>
</div>
</template>
handleDragChange 收到事件对象后取任务的 id 和目标的 status:
javascript复制async handleDragChange(event, column) {
if (event.added && event.added.element) {
const task = event.added.element
const oldStatus = task.status
const newStatus = column.status
// 乐观更新,先改本地状态
task.status = newStatus
try {
await updateTaskStatus(task.id, newStatus)
this.$message.success('任务状态已更新')
} catch (e) {
// 回滚
task.status = oldStatus
this.$message.error('更新失败,请重试')
}
this.refreshTasks()
}
}
这里核心数据是“乐观更新”。先改状态让界面立刻响应,请求失败再回滚,用户体验比干等接口返回好很多。refreshTasks 用来重新拉取列表,保证跟后端数据一致。
4.4 环境配置与本地启动全流程
拿到源码后本地跑起来,按下面的顺序操作,基本不会有问题:
-
导入数据库:用 Navicat 或命令行执行项目根目录下的
sql/init.sql脚本,里面包含建库建表语句和初始化数据(默认管理员账号密码)。修改application.yml里数据库连接的用户名密码,这步漏了启动必报错。 -
启动后端:用 IDEA 打开后端工程,等待 Maven 依赖下载。注意配置 Maven 仓库镜像用阿里云仓库,依赖下载速度能快很多。配置 Lombok 插件(新版 IDEA 自带),然后运行
Application类。 -
启动前端:用 VSCode 或 IDEA 打开前端工程,终端执行
npm install。如果下载报错,检查 Node 版本是不是 14 以上。安装完成后执行npm run dev,默认端口 8080,浏览器访问即可。 -
本地联调:开发环境下前端服务监听 8080,后端口 9090。前端配置 Vite 代理:
javascript复制// vite.config.js
export default defineConfig({
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:9090',
changeOrigin: true
}
}
}
})
这样请求 /api/login 时自动转发到后端,解决跨域。后端不加 CORS 配置,保持整洁。
5. 生产部署细节与上线避坑指南
本地跑通只是第一步,真正要交作业或者部署到服务器上,有很多细节决定成败。
5.1 前端打包与部署策略
前端构建命令是 npm run build,产物是一个 dist 文件夹。部署有两种方式,各有利弊:
方式一:Nginx 静态托管 + API 反向代理
这是前后端分离部署的首选。在 Nginx 配置里写:
nginx复制server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
重点在于 try_files 这行,解决了 Vue Router 的 history 模式刷新页面时 404 的问题。刷新 /project/list 时 Nginx 找不到这个路径,会回退到 index.html,由前端路由接管。
方式二:前端打包成静态资源放进 SpringBoot
这个方式也叫前后端合并部署,适合毕设演示时一键启动。把 Vue 打包后的静态文件复制到后端 src/main/resources/static 目录下,SpringBoot 内置 Tomcat 会在没匹配到 Controller 的路由时返回这些静态文件。但注意 Vue Router 要用 hash 模式,不然刷新页面照样 404。这个方式最大的好处是打包成一个 jar 就能跑,服务器不用装 Node 环境,但不利于前后端独立迭代。
5.2 数据库初始化与数据备份
上线第一件事,运行数据库初始化脚本并验证数据。生产环境 MySQL 配置文件要开 binlog,为后续增量备份做准备。每天凌晨用 crontab 跑 mysqldump 全量备份,保留最近7天,脚本如下:
bash复制#!/bin/bash
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d%H%M)
mysqldump -uroot -pYourPassword pm_db > $BACKUP_DIR/pm_db_$DATE.sql
find $BACKUP_DIR -type f -mtime +7 -delete
这套系统的数据体量不大,全量备份足够。加一个定时清理老备份的任务,防止磁盘被占满。
5.3 JVM 参数和服务器参数调优
生产环境不建议直接 java -jar 裸奔。用一个 start.sh 脚本:
bash复制#!/bin/bash
nohup java -Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/logs/ heapdump.hprof \
-jar /data/app/pm-system.jar \
--spring.profiles.active=prod \
> /data/logs/app.log 2>&1 &
echo $! > /data/app.pid
内存分配要看服务器配置,2G内存就 Xmx1G,4G内存就 Xmx2G,堆内存不要超过物理内存的一半。G1GC 在 JDK8 211 版本后非常稳,是 JDK8 上服务端的默认推荐。JVM 参数里加上 HeapDump 路径,万一内存溢出有文件可以排查。
启动后用 jps 和 jmap 验证进程和内存:
bash复制jps -l
jmap -heap <pid>
5.4 服务器安全基线设置
部署到公网服务器,安全配置必须到位。MySQL 禁止 root 远程登录,单独建一个应用账号只授权业务库的增删改查权限:
sql复制CREATE USER 'pm_app'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT SELECT, INSERT, UPDATE, DELETE ON pm_db.* TO 'pm_app'@'localhost';
FLUSH PRIVILEGES;
Nginx 层面启用心跳超时、关闭服务器版本号暴露,防止被扫描出具体版本找漏洞:
nginx复制server_tokens off;
client_max_body_size 10m;
proxy_read_timeout 60s;
gzip on;
6. 开发过程中最容易踩的坑,以及实录排查记录
这部分是动了真感情的实操经验,项目开发过程遇到问题不能慌,一步一步排查,其实真不难。
6.1 后端启动报错:“Failed to configure a DataSource”
这个问题在部署环境切换时必现。字面意思是没有配数据源,实际原因可能有三层:
先确认 application.yml 里有没有写对 url 参数。serverTimezone=Asia/Shanghai 这个参数必加,5.7 的 MySQL 驱动不设置东八区会报时区错误。其次确认 Maven 依赖里有没有引入 mysql-connector-java,编译的时候 IDEA 有时不会强制刷新生效依赖。最后检查数据库账号密码没有拼错。顺序排查,5 分钟内能定位。
6.2 Vue 打包后部署到服务器,首页能打开,页面刷新就 404
这是前端路由模式问题。开发模式用 history 模式没有问题,因为 dev server 里自带 fallback。但部署到 Nginx 之后,刷新 /project/1 这个地址,Nginx 会去磁盘找这个路径的文件,找不到就 404。解决方式就是前面写的 try_files $uri $uri/ /index.html。如果后端合并部署就切 hash 模式,用 createWebHashHistory(),URL 上多个 #,不太好看但功能正常。
6.3 前端跨域,浏览器提示 Access-Control-Allow-Origin
前后端分离开发的经典问题。前端 dev server 跑 8080,后端跑 9090,请求直接跨端口。开发环境正确解法不是在 Controller 上加 @CrossOrigin,而是配前端的 proxy。上文 vite.config.js 已经写好了反向代理配置。后端生产环境同样依赖 Nginx 转发,所以后端代码从头到尾不需要出现跨域相关注解,保持纯洁。
6.4 任务拖拽排序保存后顺序错乱
使用 Vue.Draggable 的 list 属性和数据的“顺序字段”绑定有问题,当后端保存新顺序时没有按 sortOrder 排序返回。排查过程中发现是持久化时没有记录元素移动位置。解决方案是在 @change 事件里拿 event.moved.newIndex 和 event.moved.oldIndex,重排前端数组后,把整个列表顺序字段统一更新到后端,后端全部重新排序。列表短,重排代价可忽略。
6.5 查询某个项目的任务列表特别慢,SQL 执行超过 2 秒
现象是任务只有几百条但查询要两秒。排查发现任务表查询没有走索引,导致全表扫描。细看 SQL 发现 WHERE project_id = ? 后面还拼了 OR parent_id = ?,这个 OR 条件让 MySQL 放弃 project_id 的单索引,走了全表。
解决方案就是加一个联合索引:
sql复制ALTER TABLE task ADD INDEX idx_project_status (project_id, status, deleted);
同时重写 SQL,把 OR 拆成 UNION ALL 的两段查询,或者先查根任务再查子任务,在内存里组树,查询从 2 秒降到十几毫秒,效果立竿见影。
6.6 实际环境中经常坏掉的报表导出功能
这套系统里导出功能用的是 Apache POI,导出几百行没问题,数据量上到几千行后就会内存溢出。排查因导出生成完整个工作簿后再一次性输出,数据量太大时 OutOfMemory。后来改成 SXSSFWorkbook 流式 API,控制单 Sheet 最大行数,后台线程生成完成后通过异步通知下载,这样再大的数据导出也不会影响主服务。
6.7 常见问题速查表
把验证过的问题直接整理成表,省得你们翻文档。
| 问题现象 | 排查方向 | 解决方式 |
|---|---|---|
| 后端启动报数据源错误 | 数据库连接配置、驱动依赖、时区参数 | 检查 yml 配置和驱动包,加 serverTimezone 参数 |
| 前端刷新 404 | Nginx 配置、路由模式 | 加 try_files 回退 index.html,或改 hash 模式 |
| 跨域报错 | 前端代理配置、后端 CORS 注解 | 开发用 vite proxy,生产用 Nginx 反代 |
| 拖拽排序错乱 | sort 字段更新逻辑 | 统一重排并整体提交后端更新 |
| 查询慢 | 扫描计划、索引缺失 | 加联合索引,优化 SQL 避免 OR |
| 导出内存溢出 | 数据量、POI 使用方式 | 换 SXSSFWorkbook 流式导出,异步下载 |
| 短时间重复提交 | 前端重复点击 | 提交按钮加 loading 状态,后端幂等校验 |
| 文件上传失败 | 文件大小限制、磁盘权限 | 调大 max-file-size,检查上传目录写权限 |
7. 从毕业设计到企业级系统的差距在哪里,如何继续拓展
严苛地说,这个系统是“企业级”里偏简化版的实现。真实的项目管理系统在权限的细粒度上、流程引擎的复杂度上、数据报表实时性上都要复杂更多。但你拿这个当基础,有几条很好的进阶路径。
7.1 前端工程化方向
页面组件化已经做了基础拆,后续还可以引入 TypeScript 给前端项目加类型约束,变量乱改的坑能少一大半。状态管理库从 Vuex 迁移 Pinia(如果你切 Vue 3),API 风格更简洁,天然支持响应式。单元测试可以用 Vitest 给关键工具函数补用例,上传组件、权限函数这种逻辑以后改起来敢放手。
7.2 后端微服务方向
项目管理系统天然适合微服务拆分,毕竟用户、权限、项目、任务、消息归属不同业务域。把用户权限拆成 auth-service,项目任务拆成 project-service,消息中心独立成 notify-service,用 Nacos 做注册中心,OpenFeign 做服务间调用,网关用 Gateway。这一步做完基本就是市面上要求 3-5 年经验的岗位技术栈实习版。
7.3 数据统计与智能化扩展
现在的报表还是一张静态表,接入定时任务每天汇总生成项目月报。更进一步可以用离线数仓把数据同步到 ClickHouse,配合大屏做实时项目指挥舱。有人问过 springboot 整合 flink 做实时计算,如果项目里需要实时计算告警,那估算工作量就得上 Flink 了。
7.4 流程引擎集成方向
目前审批流是自己写的状态机,固定“提交-审批-通过/驳回”,要支持多级审批、会签、或签、动态会签这些企业真实诉求,就要引入 Flowable 或 Activiti 工作流引擎。BPMN 2.0 流程文件可以在设计器里画好发布,运行时引擎负责流转,跟现有的业务表单对接好就是专业的 OPM 系统了。能力模型完全不同。
8. 写代码一年多,整理这套系统的几点心得体会
最后不写什么宏大总结,就说几个实际感受。
第一,源码里最有价值的不是最终代码,而是 commit 记录。我把每个模块的开发过程按阶段提交,回看时能清楚看到踩坑修复的过程。建议你拿到源码先 reset 到第一个 commit,跟着 commit 顺序手敲一遍,比直接看 final 版理解深得多。这是最容易入门的学法。
第二,写这种管理系统的核心不是炫技,而是数据结构设计和权限边界划分。技术选型人人都能搜到,但一个项目表怎么宽冗余、任务树怎么存、权限怎么继承,这些才是骨架。骨架搭错,后面写再多代码都是在错误架构上堆屎山。
第三,答辩和面试时讲“为什么”比讲“做了什么”值钱得多。为什么选 RBAC 不选 ABAC?因为系统角色层级少、业务权限维度单一,RBAC 维护成本最低。为什么 JWT 有效期定 2 小时而不是 7 天?因为面向内部员工,安全优先级高于登录体验。这种思考深度才是岗位真正看重的。
第四,最想要的其实是完整闭环。很多毕设项目做到“能 CRUD”就停了,如果你能把消息通知闭环、待办闭环、数据报表闭环全部打通,项目整体的完整度会远超同级作品。一个“能跑但缺反馈”和“能跑且有提醒”的差别,使用者上手体验会差非常远。
动手开始敲吧,光看不写是学不会 SpringBoot 和 Vue 的。如果过程中卡在某一步,大概率是环境配置问题,用搜索引擎按关键词找,方案比想象中多得多。
