☰
基于Node.js与Vue的高校科研管理系统设计与实现
2026/10/3 10:02:16 网站建设 项目流程

高校科研管理系统,单听名字你可能会觉得是个特别大的行政平台,真做一遍就会发现,核心要啃的是纵向项目管理这条业务线。我这次用 Node.js 配合 Vue 框架,做了一套前后端分离的高校科研管理系统源码,把纵向项目的申报、立项、中期检查、结题验收、经费台账、成果登记全部串在一个系统里,还带角色权限和统计报表。整个项目从环境搭建到部署跑通,实用价值和教学价值都不低,适合正在做毕业设计、课程设计的同学参考,也适合学校里需要一个轻量科研管理工具的技术团队直接二次开发。下面我把设计思路、数据库结构、核心代码和踩过的坑完整盘一遍。

1. 项目定位与整体设计思路

1.1 纵向项目到底在管什么

高校科研项目的分类,决定了系统功能怎么规划。横向项目是企业或社会机构委托的,合同驱动,资金来自企业,管理相对灵活;纵向项目则是政府立项拨款的项目,比如国家自然科学基金、国家社科基金、省部级课题、厅局级项目。这类项目的显著特点是流程长、要求多:申报要交标书,立项要等评审,执行期间还有中期检查,最后要结题验收,每一步都有对应的材料和时间节点。

我在做需求调研的时候专门问过高校科研处的老师,大家的痛点高度一致。教师这边,记不清自己名下有几个在研项目、经费余额还剩多少、中期材料什么时候截止;科研处这边,所有材料靠邮箱和微信收,项目状态无从追踪,领导问起来只能翻 Excel。所以这套系统的功能重点不是炫技,而是把两条最核心的诉求落地:教师能随时看到自己的项目全景,管理员能随时看到每个项目的状态、审批记录和历史变更。

基于这个定位,我把业务范围圈定在纵向项目的全生命周期上,再往周边延伸出经费和成果两个关联模块。横向项目没有纳入,不是做不了,而是会让角色权限、状态节点都复杂一倍,先聚焦才能保证主线清晰。这套系统如果能跑顺,对使用者来说最直接的变化就是:老师不再需要自己记台账,查经费、查节点、查成果一键搞定;科研处也不用一封封翻邮件,审批流和统计报表都在系统里。

1.2 技术选型:为什么是Node.js + Vue

技术栈方面,选了 Node.js + Express + MySQL 做后端,Vue 做前端。这个组合在高校管理和企业后台类系统里非常常见,原因很实在。

第一是场景匹配。科研管理系统本质上是高密度的增删改查和审批流,没有视频处理、数据分析这类重计算场景,Node.js 的单线程事件循环完全扛得住,Express 的中间件模型让路由和鉴权写起来非常顺手。第二是开发效率。Vue 的组件化、指令和模板语法对前后端分离的页面开发特别友好,一个列表页从搭架子到联调完,熟练的话半天内能搞定。第三是部署成本。整个项目都是 JavaScript 生态,服务器上只需要一个 Node 运行时,不像 Java 系要配 JDK、Tomcat 或者打包成 jar,对小团队和教学场景来说省事太多。

当然,技术选型没有绝对的对错。如果是大型校级平台,要对接统一身份认证、多个外部系统,Spring Boot + Vue 会更沉稳,事务管理、监控体系、团队招聘都更成熟。但就我们这套源码的定位来说,Node.js 已经把性价比做到了最优。尤其对以毕设、课设为目标的同学,前端本来就是 Vue,后端再学一套 Java 全家福会严重拖慢进度;反过来用 Node.js,前后端语言统一,一个人能同时维护两端,节奏会舒服很多。后面如果有人想把接口逻辑平移到其他语言,数据库设计和接口设计都是通用的,迁移成本很低。

2. 核心功能拆解与数据库设计

2.1 角色权限与功能模块划分

系统分了四个角色:系统管理员、科研处管理员、院系科研秘书、教师用户。三个业务角色在权限上有明确的数据范围差异。教师只能管理和查看自己作为负责人的项目;院系秘书可以浏览本院所有项目,但只能提交不能审批;科研处管理员可以对全校项目的审批流操作;系统管理员负责账号、字典和系统参数维护。

功能模块按科研业务习惯拆成六个:项目管理、经费管理、成果管理、审批中心、统计报表和系统管理。项目管理是主战场,经费和成果挂在项目下面,审批中心是状态流转的中枢。页面层面,左侧菜单按角色过滤,后端接口再按角色做二次校验,不能只靠前端隐藏按钮,这一点在联调阶段真的需要反复检查,访问控制的最后一道防线必须放在服务端。

我刚做这套系统的时候犯过一个错误:觉得菜单隐藏了,接口就没必要再做角色判断。结果测试时直接拿一个教师账号调管理员的审批接口,居然成功了。从那以后我定了一条规矩,前端控制的是体验,后端控制的是安全,每条敏感接口第一行先查角色,不匹配直接返回 403,这个习惯一直沿用到现在。

2.2 数据库表设计:核心表结构与状态机

数据库我用了六张主表:用户表、项目表、审批记录表、经费明细表、成果表、学院(部门)表。核心是项目表,其它表都通过外键关系挂在它周围。

CREATE TABLE projects ( id INT PRIMARY KEY AUTO_INCREMENT, project_no VARCHAR(32) NOT NULL COMMENT '项目编号', name VARCHAR(200) NOT NULL COMMENT '项目名称', level VARCHAR(20) COMMENT '项目级别:国家级/省部级/厅局级', leader_id INT NOT NULL COMMENT '负责人用户ID', dept_id INT COMMENT '所属学院ID', budget DECIMAL(12,2) DEFAULT 0 COMMENT '立项总经费', start_date DATE COMMENT '开始日期', end_date DATE COMMENT '结束日期', status INT DEFAULT 0 COMMENT '状态:0草稿1已提交2审核中3通过4进行中5待结题6已结题-1驳回', created_at DATETIME, updated_at DATETIME );

审批记录表长这样:

CREATE TABLE approvals ( id INT PRIMARY KEY AUTO_INCREMENT, project_id INT NOT NULL COMMENT '项目ID', operator_id INT NOT NULL COMMENT '操作人ID', action VARCHAR(20) COMMENT 'submit/approve/reject', opinion TEXT COMMENT '审批意见', created_at DATETIME );

我特别想强调两个设计细节。第一,所有金额字段一律用 DECIMAL,不要用 FLOAT。科研经费动不动就涉及分、角、元的精度问题,浮点数的二进制表示会让你在对账的时候吃大亏。第二,项目编号不要直接暴露自增 id,我在新建项目时按“年份 + 学院编码 + 三位流水号”生成 project_no,比如 2024-CS-001,用户看着友好,导出报表也好排序。

审批记录表是另一个容易被忽略但极其重要的表。很多新手会在项目表里存一个 operator 字段,谁最后审批就覆盖谁,这是错的。用 approvals 表把每一条审批行为都记下来,查询详情时把审批历史渲染成时间线,整个流转过程一目了然。项目状态的值我也做了严格约定,状态转换的校验统一放在后端 Service 层,前端只能通过 submit、approve 这两个接口触发流转,不能直接改 status 字段。

2.3 接口设计:RESTful 与统一返回格式

接口按 RESTful 风格设计,返回结构统一为 { code, msg, data },分页接口固定接收 page 和 pageSize,返回 { list, total }。这样前端 axios 拦截器可以统一处理业务码,不用每个页面各写一套错误弹窗。

核心接口清单:

  • POST /api/auth/login 登录获取 token
  • GET /api/projects 项目分页列表,支持关键词、状态、级别、学院筛选
  • POST /api/projects 新建项目
  • PUT /api/projects/:id 编辑项目,仅草稿和驳回状态可改
  • POST /api/projects/:id/submit 提交审批
  • POST /api/projects/:id/approve 审批通过或驳回
  • GET /api/projects/:id 项目详情,含经费汇总和成果列表
  • GET /api/stats/summary 按学院、年份、经费维度统计

审批行为和普通更新必须拆成两个接口,是我在设计时坚持的原则。普通更新只改业务字段,审批行为必须落审批记录,并且只能走 submit/approve 这套通道。这样做的好处是,任何时间点都能回答领导那个经典的灵魂拷问:这个项目现在到哪一步了?全部历史都在表里躺着,谁批的、批了什么意见,一查便知。

3. 实操过程:核心环节一一步步实现

3.1 Node.js 环境配置与后端初始化

先从环境说起。Node.js 我建议直接去官网下载 LTS 版本,Windows 下 msi 包一路点下一步就能装好。装完打开命令行敲 node -v 和 npm -v,能看到版本号就说明环境没问题。

这里有个 Windows 用户踩得最多的坑:在 PowerShell 里执行 npm 命令,报错“无法加载文件 npm.ps1,因为在此系统上禁止运行脚本”。这不是 Node 没装好,是 PowerShell 的执行策略默认限制脚本运行。我一般不推荐贸然改系统策略,最省事的办法是换个终端,用 CMD 或者 Git Bash 执行 npm;如果团队统一用 PowerShell,那就需要以管理员身份执行 Set-ExecutionPolicy RemoteSigned,把当前用户的执行策略放开。

后端初始化我直接手工创建项目,不用脚手架,反而更能看清依赖结构:

mkdir project-server cd project-server npm init -y npm install express mysql2 cors jsonwebtoken bcryptjs dayjs

express 负责路由和中间件,mysql2 连数据库,cors 解决开发期跨域,jsonwebtoken 签发和校验登录态,bcryptjs 做密码哈希,dayjs 统一格式化时间。就这些,没有多余依赖。入口文件 app.js 很简洁,注册中间件、挂路由、接统一 404 兜底。

const express = require('express') const cors = require('cors') const app = express() app.use(cors()) app.use(express.json()) app.use('/api/auth', require('./routes/auth')) app.use('/api/projects', require('./routes/projects')) app.use((req, res) => { res.status(404).json({ code: 404, msg: '接口不存在' }) }) app.listen(3000, () => { console.log('server running at http://localhost:3000') })

3.2 登录鉴权与接口权限控制

登录采用 JWT 方案。用户密码用 bcryptjs 哈希后入库,登录时把用户 ID 和角色签进 token,设置 2 小时过期。签发 token 的代码只有几行:

const token = jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET || 'dev_secret', { expiresIn: '2h' } )

所有需要登录的接口都过一层 authMiddleware,从请求头取 Bearer token,校验通过就把用户信息挂到 req.user 上,后续接口直接读。

const jwt = require('jsonwebtoken') module.exports = function (req, res, next) { const token = (req.headers.authorization || '').replace('Bearer ', '') if (!token) return res.status(401).json({ code: 401, msg: '未登录' }) try { req.user = jwt.verify(token, process.env.JWT_SECRET || 'dev_secret') next() } catch (e) { res.status(401).json({ code: 401, msg: '登录已过期' }) } }

这里有个细节容易出错:前端请求头里的空格可能不规范,所以我用 replace('Bearer ', '') 而不是 split(' ')[1],后者在 token 前面有多个空格时会取错位置,这种问题排查起来相当隐蔽。

3.3 前端 Vue 项目搭建与路由设计

前端用 Vue CLI 创建项目,然后装 element-ui、axios、vue-router。当前如果用的是 Vue 3,就把 element-ui 换成 element-plus,api 和组件名基本兼容,特别是表格和表单这两大件,迁移成本很低。

npm install -g @vue/cli vue create project-web cd project-web npm install element-ui axios vue-router

路由分两级:一级是登录页,一级是带侧边栏的主布局。主布局里用 children 嵌套业务页面,这样菜单的展开收起和面包屑都方便处理。核心路由配置如下:

const routes = [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/projects', children: [ { path: 'projects', component: ProjectList }, { path: 'projects/create', component: ProjectForm }, { path: 'projects/:id', component: ProjectDetail } ] } ]

动态路由在项目里我用得比较多的是详情页这类场景,:id 参数在组件里通过 $route.params.id 读取,再调详情接口把数据填进表单。下一步如果要做按钮级权限,可以再把菜单和路由表从后端接口拉取,按角色动态 addRoute,代码结构上现在就已经预留了这个扩展位。

3.4 联调代理、请求封装与打包部署

开发环境的跨域问题,我用 vue.config.js 里的代理解决,前端访问 /api/xxx,代理转发到后端 3000 端口,浏览器感知不到跨域:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } }

axios 请求封装是前端工程化的必修课。我在 src/utils/request.js 里统一做了两件事:请求拦截器自动把 localStorage 里的 token 拼到 Authorization 头;响应拦截器统一处理业务码,拿到 401 就清掉 token 跳回登录页,其它错误按 msg 字段弹提示。

axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = 'Bearer ' + token return config }) axios.interceptors.response.use( res => res.data, err => { if (err.response && err.response.status === 401) { localStorage.removeItem('token') location.href = '/login' } return Promise.reject(err) } )

生产环境的部署也很顺。前端 vue build 之后生成 dist 目录,后端在 Express 里加一段静态资源托管和 history 路由兜底,整个系统就算一个服务,不再依赖 devServer:

const path = require('path') app.use(express.static(path.join(__dirname, 'dist'))) app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'dist/index.html')))

4. 常见问题与排查技巧实录

4.1 环境与依赖安装问题速查

现象常见原因解决方案
PowerShell 执行 npm 报 npm.ps1 无法加载执行策略限制脚本用 CMD/Git Bash;或管理员执行 Set-ExecutionPolicy RemoteSigned
npm install 报 ERESOLVE 依赖冲突包版本与 npm 解析器不兼容加 --legacy-peer-deps 重装
启动服务报 EADDRINUSE : 3000端口被占用换端口,或查找占用进程后杀掉

再补一个实际遇到的细节,npm 安装依赖报 ERESOLVE 时,不要盲目删除 lock 文件,先试 --legacy-peer-deps,大多数情况下是包本身新旧版本有 peer 依赖冲突,跟你的代码没有关系。

4.2 前后端联调高频问题

联调阶段最容易卡人的是三个问题。第一个是后端 req.body 一直 undefined,大概率是忘了在入口注册 express.json() 中间件,加上就好。第二个是跨域报错 Access-Control-Allow-Origin,开发环境检查代理配置,如果后端直接对外,记得全局启用 cors 中间件。第三个是登录接口通了但业务接口全 401,排查 token 有没有通过拦截器正确拼上 Authorization 头,以及后端是否按大小写或格式要求解析。

我调试接口的习惯是,先用 Apifox 或 Postman 单独验证后端接口,确认逻辑和入参没问题,再回前端页面里测。前端报错如果提示网络层或跨域,就去看 Network 面板里的请求详情,一般能直接定位到是代理、请求头、还是后端路由没匹配上。

4.3 业务数据的坑与处理经验

业务数据层面有三个坑,我在这套系统里都踩过。一是浮点金额误差,比如经费明细里出现了 0.30000000000000004 这种数,解决方式前面说过,数据库字段用 DECIMAL,JS 计算时用整数分做运算。二是审批并发,两个管理员同时审批同一个项目,可能出现状态被后提交的人覆盖的脏数据,我在项目表加了 version 字段做乐观锁,update 时带上 WHERE version = ?,受影响行数为 0 就提示“项目状态已变更,请刷新后重试”。三是导出 Excel 中文文件名乱码,前后端没统一编码,我在下载接口里用 encodeURIComponent 处理文件名,前端 a 标签再拼接,基本能一次性解决。

5. 一点实操心得与复用建议

整套系统做下来,我体会最深的一点是,业务系统的代码难度永远小于业务流程梳理难度。状态机、角色权限、审批记录,这些东西在校验和表设计时多花半小时,后面联调和维护能省出三天。如果再让我重做一次,我会把角色权限直接升级成 RBAC,让角色和菜单、按钮权限都存数据库,而不是像现在这样写在代码枚举里,新角色上线会更灵活。

还有一个值得提的优化方向是通知。目前系统保持简单,老师提交项目后,管理员要登录系统才看得到待办。如果后续要做通知,建议优先做任务中心的轮询和红点提醒,等用户量确实上来,再上 WebSocket 或者消息队列,不要一开始就给自己上重武器。

这套源码的复用性我认为相当好。把项目类别从纵向换成横向,经费字段改成合同额,一个横向项目管理版本就出来了;把审批流模块单独抽出来,还可以套到用章申请、设备采购、请假审批等场景里。垂直行业靠业务积累,通用代码能力就是这样一套套沉淀下来的。

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

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

立即咨询