ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Node.js+Vue的创业资助管理系统开发实战

基于Node.js+Vue的创业资助管理系统开发实战 1. 项目背景与整体设计思路1.1 需求端是怎么拆出来的大学生创业资助这个场景稍微接触过高校业务的人都知道痛点非常集中。线下纸质申报材料一大堆学生填表跑流程辅导员初审、学院盖章、学校复审中间任何一个环节材料丢了或者格式不对都要重新来一遍。管理老师最头疼的不是审批本身而是材料在哪儿、流程走到哪了、这个月一共提交了多少份这些本该几十秒就能回答的问题。所以这个系统的核心需求拆到最后就三件事申报数字化、审批流程化、数据可视化。学生端要能提交材料、查进度审批端要能看清单、做审核、留意见管理端要能出统计、管用户。技术上的难点反而不在功能多复杂而在于三类角色的数据权限怎么隔离、文件材料怎么安全地上传下载、审批状态怎么流转不乱套。我用的是最务实的一套方案前后端完全分离Node.js 负责接口和业务逻辑Vue 负责页面交互ElementUI 负责把后台管理界面快速搭起来。选这套组合不是因为技术多新而是因为高校信息中心这类场景最看重的是可维护性和上手成本。Node.js 的生态成熟中间件齐全一套代码从开发到部署都很顺Vue 在国内开发者群体里的文档和案例多到数不清学生助理接手项目也不会有太高的学习门槛。1.2 技术选型Node.js Vue ElementUI 的组合逻辑先说说为什么不用 Spring Boot 或者 Django。不是说它们不好而是对于大学生创业资助管理这种业务规模——日均请求量几百次、用户数几百到几千、部署环境可能是学校的一台普通服务器——Node.js 的单线程事件循环模型完全够用而且开发效率非常高。JavaScript 前后端同语言意味着后端写接口的人也能看懂前端的报错前端同学也能帮忙排查服务端逻辑这对小型团队来说是实打实的时间节省。Vue 这边选 Vue 2 ElementUI而不是最新的 Vue 3 Element Plus有一个很现实的原因ElementUI 的组件稳定、资料多很多高校现有的旧系统也是这套技术栈日后维护和交接更方便。ElementUI 的表单验证、表格分页、弹窗通知这些组件几乎覆盖了后台管理系统 80% 的交互需求不需要自己造轮子。整个架构就是这样前端Vue 2 Vue Router Vuex Axios ElementUI后端Node.js Express JWT multer mysql2数据库MySQL 8.0部署前端打包成静态文件交给 Nginx后端用 PM2 守护进程这套组合最舒服的地方在于前端页面组件化以后新增一个审批页面就像搭积木后端按路由模块拆分每个业务模块独立维护数据库表的关系也不算复杂四张表就能覆盖核心业务。下面我把具体实现拆开讲。2. 后端实现Node.js 的接口设计与数据模型2.1 数据库建模四张表搞定核心业务我最早设计数据库的时候差点把表拆到八张后来发现过度设计反而给自己找麻烦。创业资助管理系统的核心实体就四个用户、申报项目、审批记录、附件材料。围绕这四个实体设计四张表关系清晰写 SQL 也顺手。用户表users的字段大概长这样CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(255) NOT NULL COMMENT 密码存的是bcrypt加密后的值, role ENUM(student,reviewer,admin) NOT NULL DEFAULT student COMMENT 角色, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, student_no VARCHAR(20) DEFAULT NULL COMMENT 学号学生角色必填, department VARCHAR(100) DEFAULT NULL COMMENT 所属院系, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );密码我用的 bcryptjs不是 MD5 也不是明文。有些教程为了演示简单直接存明文但在真实项目里这是绝对的安全底线问题。bcrypt 加盐哈希虽然计算稍慢但对于这种登录频率不高的系统完全不是瓶颈。申报项目表projects是核心中的核心CREATE TABLE projects ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL COMMENT 申报人ID, project_name VARCHAR(200) NOT NULL COMMENT 项目名称, category VARCHAR(50) NOT NULL COMMENT 项目类别科技创新/电子商务/文化创意等, budget DECIMAL(10,2) NOT NULL COMMENT 申请资助金额, summary TEXT COMMENT 项目简介, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1待初审 2待复审 3已通过 4已驳回, current_reviewer_id INT DEFAULT NULL COMMENT 当前审批人ID, submit_time DATETIME DEFAULT NULL COMMENT 提交时间, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );审批记录表approvals专门记每一次审批动作谁在什么时间对哪个项目做了什么决定、留了什么意见。这张表的价值在于审计——出了问题可以追溯领导问起来也能说清楚每个项目经历了什么。附件表attachments存文件的路径、原名、大小和所属项目数据库里不放文件本身文件放在服务器的 uploads 目录下。注意status字段用数字不用字符串是因为数字在索引和比较时更高效而且状态机的转移逻辑用数字判断更简洁。但是数字不直观所以后端一定要维护一个状态映射常量不要散落在各种业务代码里。2.2 登录认证与权限控制的落地写法认证方案选的是 JWT不是 Session。原因是前后端分离架构下Session 要处理跨域 cookie 的问题而 JWT 无状态、天然适合接口鉴权。用户登录成功后后端签一个 token 返回前端存在 localStorage 里之后每次请求通过Authorization: Bearer token带上。后端写一个统一的鉴权中间件// middleware/auth.js const jwt require(jsonwebtoken); const { secret } require(../config); module.exports function (req, res, next) { const authHeader req.headers.authorization || ; const token authHeader.split( )[1]; if (!token) { return res.status(401).json({ code: 401, message: 未登录或登录状态已失效 }); } try { const decoded jwt.verify(token, secret); req.user decoded; // 挂载用户信息到请求对象 next(); } catch (err) { return res.status(401).json({ code: 401, message: token无效或已过期 }); } };权限控制我这里用了最简单也最不容易出错的方案中间件工厂。写一个requireRole函数返回一个中间件在需要特定角色的接口上直接挂// middleware/requireRole.js module.exports function requireRole(...roles) { return function (req, res, next) { if (!req.user) { return res.status(401).json({ code: 401, message: 未登录 }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ code: 403, message: 没有权限访问 }); } next(); }; };接口使用示例// routes/application.js const express require(express); const auth require(../middleware/auth); const requireRole require(../middleware/requireRole); router.post(/, auth, requireRole(student), createApplication); router.get(/pending, auth, requireRole(reviewer), getPendingList); router.get(/statistics, auth, requireRole(admin), getStatistics);这种做法的好处是权限逻辑收敛在中间件里每个接口只需要声明谁能调不用在业务代码里到处写 if 判断。我实际开发中踩过的一个坑是前端路由做了权限控制但有些接口忘记加中间件导致学生直接调接口就能获取审批列表。所以前端的路由守卫只是体验优化真正的权限控制必须做在后端这句话我在代码注释里写了好几遍。2.3 审批流程的状态机设计创业资助审批的流程我设计成四级草稿、待初审、待复审、通过/驳回。这里有三个关键设计点。第一学生修改已提交项目的限制。状态是草稿时学生可以随意编辑和删除一旦提交变成待初审项目就锁定不能改了。这个用一条更新语句的前置条件实现UPDATE projects SET ... WHERE id ? AND status 0如果影响行数为 0 就说明状态已改变返回错误。这种乐观锁的思路比先查再更安全还能避免并发问题。第二审批动作的原子性。审核通过或驳回时要同时做两件事更新projects表的status和current_reviewer_id插入一条approvals记录。这两步必须放在一个事务里否则可能出现项目状态变了但审批记录没写进去的情况。用 mysql2 的connection.beginTransaction()包一下就行。第三驳回之后允许重新提交。被驳回的项目状态是 4学生重新编辑后再提交状态回到 1审批记录保留历史。这条逻辑很多人会漏掉——驳回之后学生就卡死了这是我在测试阶段发现的流程漏洞。状态流转我用了一个常量对象管理// constants/status.js const PROJECT_STATUS { DRAFT: 0, PENDING_FIRST: 1, PENDING_SECOND: 2, APPROVED: 3, REJECTED: 4 };所有涉及状态流转的地方都引用这个常量坚决不用魔法数字。后来加功能的时候只需要改这个文件全局状态判断就不会乱。3. 前端实现Vue ElementUI 的核心页面3.1 工程搭建与路由权限控制前端用 Vue CLI 创建项目安装 ElementUI 按需引入避免全量打包体积过大。babel.config.js里配好babel-plugin-component然后在main.js里只注册常用的组件。路由设计上我分了三层公共页面登录页、学生端页面、审批端页面。用 Vue Router 的beforeEach守卫做导航拦截// router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token); const user JSON.parse(localStorage.getItem(user) || {}); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } // 角色与路由meta的匹配 if (to.meta.roles !to.meta.roles.includes(user.role)) { next(/403); return; } next(); });前端路由守卫能拦截大部分越权访问但就像前面说的它不能代替后端权限。这里我遇到一个实际场景审批端页面做了角色校验但打包部署后用户手动修改 localStorage 里的 user 对象把 role 改成 reviewer前端就被骗过了。所以我在main.js的全局响应拦截器里加了一层校验如果后端返回 403统一跳转到错误页并清除本地登录信息。Axios 封装也是必做的。我在utils/request.js里统一配置了 baseURL、超时时间、请求拦截器自动携带 token和响应拦截器统一处理 401、403 和业务错误码。这样每个页面的请求代码就非常简洁// api/application.js import request from ../utils/request; export function getMyApplications(params) { return request.get(/api/application/my, { params }); } export function submitApplication(data) { return request.post(/api/application, data); }调试的时候建议装一下 Vue Devtools查看 Vuex 状态和路由信息非常方便特别是排查状态更新和路由跳转的问题时能节省大量时间。3.2 申报表单校验、联动与文件上传申报表单是整个系统交互最复杂的页面没有之一。字段多、校验规则多、还有附件上传新手写这个页面容易把代码堆到上千行。我的经验是把表单拆成 Component每个业务区块一个子组件主页面只负责兜底提交。先说表单校验。ElementUI 的el-form自带的rules验证非常强大必填、长度、格式都能覆盖。这里分享一个很多人不知道的技巧自定义校验器里可以异步校验比如项目名称在后端查重const validateNameUnique (rule, value, callback) { checkProjectName(value).then(res { if (res.data.exists) { callback(new Error(项目名称已被使用)); } else { callback(); } }); };字段联动也是常见的需求。比如选择项目类别为科技创新时附件上传区域要额外显示产品原型图的上传入口选择电子商务时要显示店铺链接输入框。我用v-if加计算属性实现避免用watch去手动改一堆数据代码可读性会好很多。文件上传是这里的重头戏。ElementUI 的el-upload配action属性指向后端接口但要注意两个细节。第一必须带上 token——el-upload默认不发Authorization头需要用headers属性手动加el-upload :actionuploadUrl :headers{ Authorization: Bearer token } :before-uploadbeforeUpload :on-successhandleUploadSuccess namefile 第二文件大小的前后端双重校验。前端在beforeUpload里做一次拦截超过 10MB 直接提示后端 multer 配置里也要有limits: { fileSize: 10 * 1024 * 1024 }兜底。我遇到过前端校验被绕过直接传一个超大文件把服务器磁盘占满的情况就是因为只做了前端拦截。3.3 审批工作台表格、筛选与操作反馈审批页面本质就是一个增强版的表格页但 ElementUI 的表格有非常多的细节值得优化。最常用的是show-overflow-tooltip属性让长文本在单元格里省略展示鼠标悬浮显示完整内容——这就是热搜词里文字超出隐藏鼠标悬浮显示全的文字的标准解法。审批列表我设计了三个视角待我审批、我已审批、全部项目。待我审批用tabs切换每个 tab 下是独立的el-table数据源不同但列配置几乎一样。我把表格列配置抽成数组通过计算属性动态组合避免写三份几乎一样的模板代码。筛选区用 ElementUI 的表单组件组合状态用el-select项目类别用el-select时间范围用el-date-picker。搜索结果用keyup.enter.native绑定查询也会在日期选择器change时自动触发。这里有个体验细节筛选项变化后要先把分页页码重置为 1不然停留在第 5 页的时候筛选出的数据只有几页会出现空列表这个 bug 很隐蔽。审批操作我用了两种交互单个审批在行内放通过和驳回按钮批量审批用el-table的selection-change记录选中行然后一键批量通过。批量操作要特别注意后端接口设计成接收数组ids一次更新多条记录并且要检查审批人角色有没有权限处理这些项目防止越权批量操作。审批弹窗里放意见输入框el-input typetextarea和一个申请表预览区。预览 PDF 用el-dialog内嵌iframe虽然样式朴素但胜在稳定。如果以后要升级可以换 pdf.js 做更精细的预览控制但前提是兼容性验证要充分。4. 开发过程中踩过的坑和解决办法4.1 Windows 下 npm 脚本执行报错的坑在 Windows 上用 npm 全局安装依赖后运行命令经常遇到这样的报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这个报错的本质是 PowerShell 的执行策略限制不是 npm 本身有问题。很多初学者第一次遇到直接懵掉以为 Node.js 装坏了其实解法很简单。以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned然后输入Y确认即可。RemoteSigned的意思是本地脚本可以运行从网上下载的脚本必须有数字签名才运行安全性和便利性之间取了一个平衡。如果没有管理员权限也可以在当前用户级别设置Set-ExecutionPolicy -Scope CurrentUser RemoteSigned或者干脆绕过 PowerShell直接用 CMD 运行 npm 命令。我个人建议是配置好执行策略因为后续还有很多脚本需要通过 npm 触发总是切 CMD 很别扭。注意Set-ExecutionPolicy Restricted是最严格的安全策略如果你在公司电脑上操作可能 IT 部门主动配置了这个策略这时候保险的做法是用-Scope CurrentUser只影响当前账户不动系统级配置。4.2 前后端联调的跨域与代理问题前后端分离开发时前端跑在http://localhost:8080后端跑在http://localhost:3000前端请求后端必然触发跨域。解决方案有几套我推荐最省事的那套开发环境用 Vue CLI 的代理后端不用改任何代码。在vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } };这样前端请求/api/application时开发服务器会自动转发到http://localhost:3000/api/application浏览器里看到的请求是同源的不会报跨域错误。但要注意生产环境没有 devServer前端打包后的静态文件放在 Nginx 下请求/api时需要在 Nginx 层加反向代理location /api/ { proxy_pass http://127.0.0.1:3000/; }我最早没配 Nginx 代理上线后所有接口请求 404排查了半天才发现是前端请求地址写死了http://localhost:3000部署到服务器路径全错。正确做法是前端所有请求都用相对路径/api让代理层做转发这样代码在开发环境和生产环境不用改。4.3 表格、上传组件的高频使用错误ElementUI 的表格有太多隐藏的坑我说几个印象最深的。el-table的v-loading指令挂载位置不对会导致加载动画不显示但有遮罩层用户会以为页面卡死了。正确用法是挂在el-table的自身上不是包在外部 div 上加载状态结束后自动消失。还有就是表格数据更新后视图不刷新。如果直接修改tableData[0][status] 3Vue 2 的响应式系统是检测不到这种索引变更的。需要用this.$set(tableData, index, newItem)或者对整个数组重新赋值。这个坑太常见了React 开发者转 Vue 时也容易踩。上传组件还有一个高频问题上传成功或失败后没有清理失败列表用户再次选择同一个文件时el-upload会因为file-list中还残留旧的记录导致重复上传或状态异常。处理方式是在on-success和on-error回调中根据file.response的返回码决定保留还是移除这条记录。4.4 环境配置和版本兼容的周年庆现场Node.js 的版本兼容问题是每个开发者都会遇到又不太当回事的坑。我之前在项目里用了某一个新版本的依赖包结果部署到学校服务器上发现服务器装的是很老的 Node.js 版本运行直接报语法错误。解决方案是项目根目录加上.nvmrc文件写明版本号比如写入16.15.0。开发时用 nvm 切到对应版本部署时也照这个版本装环境。Vue 和 ElementUI 的版本配套也值得注意。Vue 2 必须配 ElementUI 2.xVue 3 配的是 Element Plus。我见过有人把 Element Plus 装进 Vue 2 项目启动直接白屏。如果你用的是 Vue 3 项目别犹豫上 Element Plus 就行组件 API 基本平移只是个别属性改名了比如slot写法的变化。顺带一提如果你以后想给这个系统加一个创业培训视频的模块Vue 播放 m3u8 视频流可以在video标签里用 hls.js或者直接用vue-video-player插件不要被各种混乱的教程带偏。但那是后话先把主体功能做稳定。5. 打包部署与上线后的优化思考5.1 打包要注意的路由模式问题前端开发时我用的是 Vue Router 的 history 模式因为地址栏干净不带着#号。但打包部署后就出问题了在某个页面刷新Nginx 返回 404因为服务器上根本没有/applications/1这个物理路径。解决方案两种看部署环境选择。第一种是把路由改成 hash 模式createWebHashHistory地址带上#/applications/1刷新不会 404但丑一点。第二种是保持 history 模式在 Nginx 配置里加入location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }try_files把所有路由都指向index.html前端路由接管后再渲染对应页面刷新就不会 404 了。推荐用第二种Nginx 配置一行解决体验也好得多。打包构建时还有一个小细节publicPath要设置成相对路径或绝对路径。默认/没问题但如果你把前端部署在服务器的子路径下比如http://server/edu/不设置publicPath的话所有静态资源的引用路径都会错。项目里有几个中文字体文件也是在这里踩的坑路径一错字体加载失败页面里全是回退的宋体。5.2 上线后的数据校验与安全加固系统上线不等于开发结束反而真正的优化才刚开始。上线后我做了三件重要的事建议所有类似管理系统都照着做一遍。第一后端接口的入参校验。前端表单有校验但接口本身不能依赖前端传值。我用 express-validator 在路由层做参数校验字符串长度、必填字段、枚举值合法性都能在进入业务代码之前拦截掉。否则一个恶意请求直接 POST 一段超长文本MySQL 报错不说还可能引发内存问题。第二文件上传的类型白名单。multer 的fileFilter里只允许application/pdf、图片的几种 MIME Type其他一律拒绝。之前有次测试我试着传了一个.html文件文件被存到了 uploads 目录如果不加限制用户就能在域名下直接访问到这个 HTML这是非常严重的安全隐患。正确做法是上传目录禁止执行脚本同时在 Nginx 里对/uploads路径设置location ~* \.(php|jsp|asp)$ { deny all; }。第三敏感信息接口的日志记录。审批操作、密码修改这类高风险接口我用一个简单的中间件把操作人、操作时间、操作内容写入日志表。这个在排查问题时特别管用——有一次一个项目莫名其妙被驳回查日志发现是审核老师误点了旁边的按钮有日志就好解释否则两边互相扯皮。5.3 数据统计可视化的小优化管理后台的统计页面最开始就是一堆数字卡片总申报数、待审批数、已通过数、资助总金额。后来领导提需求要看各院系的申报数量对比和资助金额趋势于是我用el-card ECharts 做了两个图表。统计接口按院系分组统计SQL 大致是SELECT department, COUNT(*) AS cnt, SUM(budget) AS total_budget FROM projects WHERE status 3 GROUP BY department ORDER BY cnt DESC;前端用 ECharts 的柱状图和折线图展示几分钟就能接入。要注意 ECharts 的图表容器需要有明确的高度否则图表不渲染。我在页面上固定了styleheight: 400px并用window.onresize监听窗口变化调用chart.resize()适配重绘。写在最后的几点体会这个系统从前到后我完整做了一遍最大的感触是技术栈本身不难真正花时间的是业务逻辑的梳理和那些零零碎碎的坑。每一个看起来很简单的功能模块背后都有对应的边界条件要考虑。比如审批状态的流转我一开始只设计了提交和审批测试时发现漏了驳回后重新提交撤回修改审批人变更几种情况。比如文件上传我在开发环境测试都正常部署后才发现 Nginx 的client_max_body_size默认只有 1MB学生上传项目计划书直接 413 错误后来在配置里改成了 20MB 才解决。像这样的问题还有很多。我之所以在这篇博文里花那么长篇幅写踩坑过程是因为网上的教程基本都是教你怎么把代码跑起来很少有人告诉你生产环境里真正的坑在哪里。希望这篇内容能帮你少走几个月的弯路。另外一个很实用的建议开发这种管理系统先把业务表结构和状态流转画清楚再动手写代码。我在数据库设计上花了三天后面所有功能的开发速度都非常快。反过来我见过很多同学一上来就写接口写到一半发现表结构缺字段又回头改数据库改完数据库又要改前后端代码整个进度拖了一倍不止。最后分享一个小技巧做这种管理系统前端用 ElementUI 的el-cascader做院系组织选择、el-upload做附件上传、el-table做数据列表、el-dialog做详情弹窗、el-form做筛选表单这五个组件能覆盖掉你 90% 的页面需求。把它们的常用属性和事件看熟开发效率会提升非常明显。
返回列表