☰
基于SpringBoot+Vue的科研项目验收管理系统实战解析
2026/10/14 22:33:37 网站建设 项目流程

做项目管理相关系统这几年,我一直觉得验收环节是最容易被忽略的:申报的时候大家都很认真,结题的时候才发现材料散、流程乱、进度全靠催。今天要聊的这套基于SpringBoot+Vue的科研项目验收管理系统,就是专门把项目申报、过程跟踪、验收申请、专家评审这一条链路理顺的完整工程。后端走Java+MySQL+MyBatis这套经典组合,前端用Vue+Element生态,源码结构完整,能跑通“提交—审核—评审—反馈”的完整闭环。

这套系统很适合三类人参考:一是准备做毕业设计或课程设计的学生,二是单位内部需要自建科研管理小工具的后端开发者,三是刚接触前后端分离项目、想找一份完整工程来拆解学习的新人。先说清楚,项目标准命名是“科研项目验收管理系统”,标题里多出来的那个“管理系统”就当是写标题时的肌肉记忆,大家知道是指同一个东西就行。

1. 从一个“烂尾”的验收流程说起

1.1 科研项目验收为什么这么难管

很多人没接触过科研项目全流程管理,觉得验收不就是“交材料、请专家、开个会”吗?实际完全不是。一个科研项目从立项到结题的周期往往横跨一到三年,中间有开题、中期检查、阶段性成果提交、财务执行情况核对,最后才到验收。验收又要准备项目总结报告、成果清单、经费决算表、用户使用证明等一堆材料,线下操作时这些文件散落在各处,版本经常对不上。

我参与过的实际场景里,最头疼的并不是某个技术环节,而是“流程的不可见”:项目处在哪个阶段、卡在谁手里、材料缺什么,全靠打电话问。科研管理部门想汇总验收进度,就得翻Excel台账;专家评审意见收集回来,还要手工录入和汇总打分。这类工作本身没什么技术含量,但量一大就极其耗费人力。

这正是开发一套科研项目验收管理系统的直接动机。系统的本质不是做一个花哨的业务软件,而是把验收过程中那些“默认靠默契完成”的节点显性化:谁该提交什么、谁该审批、谁该评审、结果如何流转,全部在系统里留痕。

1.2 系统到底要解决哪些具体问题

从业务角度看,这套系统解决的是四个问题:

第一,项目台账统一化。以往一个科研项目的信息散落在申报书、合同、结题报告里,系统把这些信息收敛到一张项目主表中,项目负责人、预算、起止时间、当前状态一眼可见。

第二,验收流程线上化。从科研人员发起验收申请,到管理员审核、分配专家、专家填写评审意见,再到最终结果反馈,每一步都有明确的操作入口和状态标记。

第三,材料管理规范化。验收申请书、项目总结报告、成果附件等统一走文件上传下载,按业务类型和业务ID归档,避免“材料在微信聊天记录里找不回来”的尴尬。

第四,评审结果可追溯。每个专家对项目打的分、填写的意见都落库,管理员可以按项目维度汇总,也方便后续审计或复查。

1.3 系统边界与适用场景

设计这套系统时,我刻意把范围收住了:不做财务预算管理,不做论文成果库,不做消息推送平台,只聚焦“项目验收”这一条主线。原因是科研管理系统这类项目最怕一做就大,一旦把所有业务模块都塞进来,数据模型会变得非常复杂,开发周期和测试成本都会难以控制。

实际应用场景可以是高校某学院的科研项目验收,也可以是企业研究部门的内部课题管理,甚至是一个课题组的共享项目管理工具。对毕业设计来说,这个边界刚好合适:既能展示完整的前后端开发能力,又不需要去实现脱离业务核心的冗余功能。

2. 技术选型:为什么是SpringBoot+Vue+MyBatis+MySQL

2.1 前后端分离的取舍

过去做管理系统常见做法是Thymeleaf服务端渲染,页面跳转靠Controller转发,开发效率不高,前端逻辑和后端逻辑耦合在一起。这次选择前后端分离,原因很实际:Vue负责页面交互和状态管理,SpringBoot只提供JSON接口,两边可以独立开发、独立测试,部署时也可以分开扩容。

前后端分离也会引入跨域、鉴权、接口联调等问题,但这些都是有成熟方案的。对于这套系统来说,分离最大的收益是“接口复用”:手机端或者未来要做的数据大屏,都可以直接复用后端接口,不需要重新做一套服务端渲染页面。

2.2 后端为什么选SpringBoot和MyBatis

SpringBoot现在已经是Java Web开发的事实标准。内嵌Tomcat、自动配置、约定大于配置,省掉了大量XML配置工作。对于单体业务系统来说,SpringBoot足够的稳定性,社区资料也多,遇到问题容易搜到方案。

MyBatis的选择则是基于SQL控制力。验收管理涉及大量多表查询:项目表要关联用户表拿负责人姓名,验收申请要关联项目表和专家评审表,统计汇总时还要按多个条件动态过滤。MyBatis允许手写SQL和动态SQL,遇到这种场景比JPA的自动生成SQL更直观,排查问题时也能一眼看出SQL写得对不对。

至于为什么不是MyBatis-Plus,其实两种都行。如果项目里大多数是单表CRUD,MyBatis-Plus能省一半代码;但很多教学习惯会要求手写Mapper XML,完整展示SQL功底。我在这套系统的核心模块里保留原生MyBatis写法,在用户管理等简单模块上保留了通用Mapper的写法,两种风格都能看到。

2.3 前端Vue生态怎么搭配

前端技术栈锁定为Vue生态。基础版本可以是Vue2+Element-UI,也可以用Vue3+Element-Plus,核心思路完全一致。我下面的示例以Vue2为基线,如果你用Vue3,把依赖换成Element-Plus,再把部分生命周期和响应式语法调整一下即可。

状态管理用Vuex(Vue3则可用Pinia),路由用Vue Router,HTTP请求用Axios。这套组合在管理系统领域非常成熟,表格、表单、弹窗、分页这些常用组件都有现成方案,开发体验比手写DOM高好几个量级。

2.4 整体架构与部署形态

系统整体是单体的前后端分离架构,不是微服务。单体意味着一个SpringBoot应用包揽所有后端功能,前端通过Nginx或者开发服务器访问API。对科研验收管理这种用户量和并发量都有限的场景,单体架构足够,而且部署、运维、排错都简单得多。

部署形态上,一套典型的线上环境就是:MySQL存数据,后端打成一个Jar包运行在服务器上,前端通过npm run build打包成静态资源交给Nginx托管,再把/api路径反向代理到后端服务。整个部署只需要一台云服务器或者学校服务器主机就能跑起来。

技术组件用途选型理由
SpringBoot后端应用框架自动配置、内嵌容器、生态完善
MySQL关系型数据库开源免费、通用性强、适合中小系统
MyBatisORM持久层框架手写SQL灵活可控,适合多表关联和动态查询
Vue + Element前端框架与UI组件库组件丰富,适合管理系统页面快速搭建
AxiosHTTP请求库拦截器机制方便统一处理鉴权与异常
JWT登录状态维持无状态、跨域友好、适合前后端分离

3. 系统设计:从业务流程到数据库落表

3.1 角色权限与业务闭环

权限模型我用了最常见的RBAC简化版:用户表里直接存角色字段,不搞用户—角色—权限三张表。原因很直白:系统里只有三种角色,管理员、科研负责人、评审专家,权限差异主要体现在菜单可见性和接口操作范围上,引入完整RBAC反而增加不必要的复杂度。

三种角色各有一条清晰的业务线:

科研负责人(普通用户)负责创建项目、维护项目信息、提交验收申请、上传验收材料。

管理员负责审核验收申请、分配评审专家、查看评审进度、导出汇总结果,同时可以管理用户和项目基础数据。

评审专家登录后只能看到分配给自己的待评审任务,可以下载附件、在线打分、填写意见。

业务闭环是:项目创建完成后,状态为“进行中”;科研负责人发起验收申请,状态变为“待审核”;管理员审核通过后,系统为验收申请关联一到多位专家,状态变为“评审中”;所有专家提交评审意见后,管理员根据打分和意见汇总,得出“验收通过”或“验收不通过”结论;项目状态随之更新并归档。

3.2 数据库表设计

核心表设计是整个系统的基础。我按业务实体拆分成六张核心表:用户表、项目表、验收申请表、专家评审表、附件表,以及一张用于记录项目状态变更历史的日志表。

用户表字段不多,id、username、password、real_name、role、dept_name、phone、email,角色用整数或字符串区分即可,密码存的是加密后的密文。

项目表相对复杂一些,主要字段包括project_name、project_code(项目编号)、leader_id(关联用户表的负责人ID)、budget、start_date、end_date、project_type、status、description。其中status是整个流程的核心状态字段。

验收申请表承担了流程主线的职责:apply_code、project_id、apply_user_id、apply_title、apply_content、submit_time、audit_status、audit_user_id、audit_time、audit_comment。每条验收申请都挂在一个项目下,一个项目可以多次申请验收(比如第一次未通过,整改后再申请)。

专家评审表记录每次评审行为:apply_id、expert_id、score、review_comment、review_status、review_time。这里要注意,一条验收申请可能分配给多个专家,所以是申请和专家多对多的关系,通过这张中间表关联。

附件表的设计比较通用:biz_type标识附件属于哪个业务模块,biz_id指向对应业务记录ID,file_name保存原始文件名,file_path保存服务器上的存储路径,upload_user_id负责记录上传人。这张表的好处是,项目附件、验收申请附件、评审意见附件都可以共用一套逻辑。

给一段简化建表SQL做参考:

CREATE TABLE `project` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `project_code` varchar(32) NOT NULL COMMENT '项目编号', `project_name` varchar(128) NOT NULL COMMENT '项目名称', `leader_id` bigint(20) DEFAULT NULL COMMENT '项目负责人ID', `budget` decimal(12,2) DEFAULT NULL COMMENT '经费预算', `start_date` date DEFAULT NULL COMMENT '开始日期', `end_date` date DEFAULT NULL COMMENT '结束日期', `project_type` varchar(32) DEFAULT NULL COMMENT '项目类型', `status` tinyint(4) DEFAULT '1' COMMENT '项目状态:1进行中 2待验收 3已通过 4未通过', `description` text COMMENT '项目描述', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_leader_id` (`leader_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研项目表';

3.3 状态机设计为什么这么重要

在写业务代码之前,我强烈建议先把状态流转图画清楚。这个系统里最容易出bug的地方恰恰是状态处理:科研人员还没提交验收申请,专家就能看到评审任务;管理员没审核通过,项目状态就变成已通过;项目已经归档,前端又允许发起新的验收申请。这些都是因为状态缺少统一管控。

我在实现时给状态流转集中封装了一个状态机校验方法,每次业务操作前先校验当前状态是否允许该操作。例如:

private void checkProjectStatus(Long projectId, int expectedStatus) { Project project = projectMapper.selectById(projectId); if (project == null) { throw new BusinessException("项目不存在"); } if (project.getStatus() != expectedStatus) { throw new BusinessException("当前项目状态不允许执行该操作"); } }

这个方法的本质是把流程规则固化成代码,不让非法状态流转发生。虽然代码量多了一点,但后续排查问题会省很多力气。

4. 核心功能实现拆解:后端篇

4.1 分层设计与统一返回结构

后端代码按照Controller、Service、Mapper三层组织。Controller只负责接收参数和返回结果,不写业务逻辑;Service层处理业务规则和事务;Mapper层负责数据库操作。这样做的好处是职责清晰,遇到问题时能快速定位。

所有接口统一返回一个Result对象,结构包含code、message、data三个字段。成功时code=200,业务异常时code为业务错误码,未登录时code=401。对应前端Axios拦截器只需要处理这一个结构。

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "操作成功"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } }

异常处理也统一交给全局异常处理器。业务异常自定义了一个BusinessException,兜底异常统一返回“系统异常,请稍后重试”。这能有效避免前端看到一堆莫名其妙的堆栈信息。

4.2 MyBatis多表联查与动态SQL

验收管理系统的查询场景非常考验SQL功底。以“项目列表分页查询”为例,需求是:根据项目名称模糊搜索、根据状态筛选、根据负责人姓名关联查询,同时分页展示。这就要求项目表project关联用户表sys_user,并且查询条件是动态拼接的。

MyBatis的<where>和<if>标签在这里体现得淋漓尽致。下面是一段简化版Mapper XML:

<select id="selectProjectPage" resultType="com.example.entity.ProjectVO"> SELECT p.*, u.real_name AS leader_name FROM project p LEFT JOIN sys_user u ON p.leader_id = u.id <where> <if test="projectName != null and projectName != ''"> AND p.project_name LIKE CONCAT('%', #{projectName}, '%') </if> <if test="status != null"> AND p.status = #{status} </if> <if test="leaderName != null and leaderName != ''"> AND u.real_name LIKE CONCAT('%', #{leaderName}, '%') </if> </where> ORDER BY p.create_time DESC </select>

这里有一个值得强调的细节:<where>标签会自动去掉第一个AND,这样拼SQL时就不用担心多余前缀。LIKE CONCAT('%', #{projectName}, '%')是防止SQL注入的写法,避免直接用'%' + projectName + '%'字符串拼接。

分页用的是PageHelper插件,Controller里调用PageHelper.startPage(pageNum, pageSize),紧接着的查询就会自动带上LIMIT,并返回总记录数。注意PageHelper只能作用在下一条SQL上,如果你在中间穿插了其他SQL,很容易出现分页错乱的问题。

4.3 登录鉴权与JWT实战

前后端分离场景下的认证方案,我选择了JWT。流程是这样的:用户提交用户名密码,后端验证通过后生成一个带过期时间的Token返回给前端;前端把Token存在本地,每次请求在Authorization头里带着;后端通过拦截器解析Token,从中取出用户ID和角色信息。

密码不能明文存储。传统做法是用MD5加盐,但现在更推荐BCrypt。Spring Security自带BCrypt工具,不需要为了个加密引入整个Security框架,单独引一下工具类就行。

public String generateToken(User user) { long expire = System.currentTimeMillis() + 1000 * 60 * 60 * 24; // 24小时过期 return Jwts.builder() .setSubject(user.getUsername()) .claim("userId", user.getId()) .claim("role", user.getRole()) .setExpiration(new Date(expire)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }

拦截器里放行登录接口、静态资源等白名单路径,其余请求都先执行Token解析。解析成功后把用户信息放进ThreadLocal,Service层可以直接用CurrentUser.get()拿到当前登录人,省得每个接口都从参数里传用户ID。

使用JWT要特别注意过期时间。24小时过期比较合理,太短会导致用户体验差,太长有安全风险。如果前端拿到401响应,统一跳转登录页重新登录。

4.4 文件上传、下载与验收材料管理

验收系统离不开附件。这里我实现了通用的上传接口:前端用Element的el-upload组件选文件,后端接住MultipartFile,先校验文件类型和大小,再把文件写到服务器的指定目录,文件命名改成UUID,避免重名覆盖。

文件写入后,把业务类型、业务ID、文件原始名、存储路径插入附件表。同时约定上传接口的请求参数中必须带上bizType和bizId,这样才能保证附件与业务数据绑定。

写入文件的核心代码大致是:

String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; File dest = new File(uploadDir + File.separator + newFileName); file.transferTo(dest); Attachment attach = new Attachment(); attach.setBizType(bizType); attach.setBizId(bizId); attach.setFileName(originalFilename); attach.setFilePath(newFileName); attachmentMapper.insert(attach);

下载时需要注意路径安全问题。前端传参数通常是附件ID,后端根据ID查出存储路径再拼接完整路径,而不是直接让前端传文件路径。这样能避免用户构造路径去读取服务器上不属于自己的文件。

另外,验收管理经常需要导出汇总表。我用EasyExcel做导出一张“项目验收汇总表”,内容包括项目编号、名称、负责人、验收申请时间、审核状态、各专家评分、综合评价等。EasyExcel底层还是POI,但内存占用比POI低,写大数据量列表时能少很多GC问题。

5. 前端Vue实现:从登录到验收管理

5.1 工程搭建与基础封装

前端用官方脚手架初始化项目后,先装好依赖。Element-UI提供表格、表单、弹窗、Upload上传组件,基本覆盖管理系统的常用界面。Axios需要做一层封装,统一处理请求头注入和错误拦截。

Axios封装的核心点在于请求拦截器:每次请求前从localStorage取出Token并加入请求头;响应拦截器里判断HTTP状态码和业务code,遇到401就清空本地登录信息并跳转登录页。

service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('登录已过期')) } return res }, error => { return Promise.reject(error) } )

5.2 登录状态与菜单权限控制

登录页面调用/api/user/login,拿到Token和用户信息后,写入本地存储,再跳转到首页。路由守卫在每次路由跳转前检查是否已登录,避免用户直接输入URL跳到内部页面。

菜单权限则简单粗暴一些:前端根据用户角色字段,判断哪些菜单可见。管理员看到“项目管理”“验收审批”“评审分配”“用户管理”,专家只看到“我的评审任务”,科研负责人只看到“我的项目”“验收申请”。这种静态权限方案够用,但如果你未来要把权限控制做到按钮级别,就需要后端在接口层做同样的校验,不能只靠前端隐藏菜单。

5.3 验收申请与专家评审核心页面

验收申请页面的核心是一个表单加一个上传组件。科研负责人选择“我的项目”里状态为“进行中”的项目,填写验收标题和内容,再上传项目总结报告、成果清单等附件,提交后生成一条状态为“待审核”的验收申请记录。

列表页则是典型的Table加搜索条件加分页。筛选条件包括项目名称、验收状态、申请时间区间。管理员在列表里可以看到所有验收申请,点击“审核”按钮弹出确认框,审核通过后进入专家分配页面。

专家分配是管理员的重点工作页面。页面展示尚未评审的验收申请,管理员为每条申请勾选二到三位专家,系统为每个专家生成一条评审任务。这里的关键是提交时做去重:同一个专家不能重复分配给同一个验收申请,否则专家登录后会出现两条相同任务。

专家评审页面相对简单:专家登录后进入“我的评审任务”,点击某条任务可以看到项目基本信息、下载附件、填写评分和评审意见,提交后评审状态变为“已提交”。管理员在后台通过项目维度汇总所有专家的评分意见,决定最终验收结论。

前端关键逻辑片段:

submitReview() { if (!this.form.score || !this.form.reviewComment) { this.$message.warning('请填写评分和评审意见') return } submitReviewApi(this.form).then(res => { this.$message.success('评审提交成功') this.$router.push('/expert/tasks') }) }

6. 常见问题与排查实录

6.1 跨域问题与接口联调

前后端分离开发的第一道坎就是跨域。开发时前端跑在localhost:8081,后端跑在localhost:8080,直接用Axios请求后端接口会被浏览器拦截。解决办法有两种:后端加全局CORS配置,或者前端通过Vue脚手架代理转发。

我更推荐开发期用前端代理,因为后端不需要做任何CORS改动,上线后Nginx反代也不存在跨域。Vue工程里配置:

// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

这样前端请求/api/user/login就会被代理到后端真正地址。上线后由Nginx把/api前缀的请求转发到SpringBoot进程,逻辑完全一样。

6.2 MyBatis动态SQL与分页的坑

动态SQL最容易踩的坑有三个。第一,<if test="status != null">能正确处理整数0值,但如果你用字符串类型判断!= '',就要注意传入0时是否会进入条件,这是新手很容易看迷糊的地方。第二,多表联查时如果两个表有同名字段,必须在SQL里用表别名区分,否则MyBatis映射到实体的字段值会混乱。第三,PageHelper分页时,如果查询语句后面还跟着其他查询语句,分页插件可能作用到错误的位置,导致数据错乱。

这些坑的排查思路都是先去日志里看打印出的SQL。MyBatis配置map-underscore-to-camel-case=true再把SQL打印级别设为DEBUG,基本能解决大多数问题。

6.3 部署与文件路径问题

本地跑得好好的,部署到服务器后文件无法上传下载,这个问题我也遇到过。原因是代码里用了相对路径,或者把文件存到了src/main/resources下面,打包成Jar之后路径失效。

解决办法是把文件存储目录配置到外部,比如application.yml里写file.upload-dir=/data/upload/,同时在启动服务前确保目录存在,并且运行Jar的账号对该目录有写权限。下载时通过配置项拼接完整路径,不要依赖当前工作目录。

6.4 并发验收与数据权限

人多的时候会同时提交多条验收申请,最直接的影响是可能产生重复数据。我为验收申请表加了一个业务唯一标识:一个项目只允许存在一条“待审核”状态的验收申请。实现方式是建唯一索引比较困难,因为要过滤状态,更实用的做法是Service层加分布式锁或者数据库层面做条件性唯一约束。

数据权限方面重点是“专家只能看到分配给自己的评审任务”。我在查询语句中强制加入专家ID条件,这个ID从JWT解析得到,前端传参只能传验收申请ID,不能传专家ID来绕过限制。这样即使前端接口被恶意调用,后端也能保证数据隔离。

7. 踩过坑之后的几点扩展建议

如果让我再重新做一遍这套系统,有几件事我肯定会提前安排。一是数据字典设计,项目类型、审核状态、评审等级这些枚举值应该有一张数据字典表统一定义,而不是散落在后端代码的魔法值里。二是操作日志,目前只做了状态变更记录,其实用户的每一次增删改查都值得留痕,这个需求在真实项目里几乎一定会被提出来。三是消息通知,验收申请被审核、评审专家被分配,这类事件如果能自动触发邮件或站内信提醒,用户对系统的感知会明显好很多。

功能扩展方向上,可以加一个项目结题看板,用统计图表展示各阶段项目数量、评审通过率、专家工作负载。也可以把流程引擎引入进来,把状态流转配置化,做成与业务无关的通用审批流。不过这些都是后话,第一版还是先踏踏实实把主流程跑通更稳妥。

最后分享一个我自己的经验:拿到这种系统需求,不要急着写代码。先把角色列出来,再把每个角色要执行的操作用一句话描述清楚,最后把项目状态流转图画出来。这三件事做完,后端表格怎么设计、前端页面怎么分布、接口怎么定义,基本就一目了然了。如果你打算拿这套系统做毕设或者课设,也建议从主流程开始实现,先把“提交—审核—评审—出结果”跑通,再回来补充用户管理、数据统计这些外围功能。先把主路走通,比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询