ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从0到1开发营商环境管理系统:Node.js + Vue实战全记录

从0到1开发营商环境管理系统:Node.js + Vue实战全记录 我们部门前阵子接了一个“营商环境行动计划管理系统”的项目技术栈明确要求 Node.js 和 Vue。这类系统听起来像“政务后台”其实拆开来看就是一个典型的业务流管理平台行动计划编制、任务分解下派、进度反馈、结果督办、数据统计。真正麻烦的不是页面多而是流程状态多、角色权限杂、报表维度乱。这篇文章把我从需求梳理到上线部署的完整过程写出来包括表结构设计、前后端核心代码思路、以及 Node.js 和 Vue 环境里最容易绊倒人的几个坑希望能给正在做同类型“管理平台”的朋友一些参考。1. 接到需求之后先别急着写代码把业务流程拆明白很多人拿到这类项目习惯性先建工程、装依赖、画页面。我做过的管理系统不下十来个得出一个教训业务流梳理不清代码写得越快返工越狠。1.1 核心业务链路从“行动计划”到“任务销号”营商环境行动计划管理系统表面上是“年度计划管理”实际上涉及一套完整的闭环流程。我花了两天时间跟业务方反复确认最终梳理出这条主链路制定年度营商环境行动计划明确目标、责任单位、完成时限将行动项分解为具体任务逐级下派到责任部门和责任人执行人定期填报进展、上传佐证材料分管领导审核进展审核不通过退回重报督办部门对临期、超期任务发送预警提醒任务完成后提交销号申请由管理员终审归档全过程数据汇总生成统计报表用于绩效考核和决策参考这条链路里最关键的是“状态流”。每个任务不是简单的“待办—完成”而是存在待提交、待审核、已退回、待销号、已归档、已逾期等状态。状态之间允许的流转关系必须提前定清楚否则前端按钮会乱、后端接口会漏。1.2 角色权限模型不搞RBAC会把自己绕晕系统涉及的账号类型包括系统管理员、普通用户、部门领导、督办人员。如果只靠“前端判断有没有某个角色”后端接口就是裸奔状态。我建议直接上经典的 RBAC 模型三张核心表用户表关联部门角色表管理员、部门负责人、执行人、督办员权限表按操作细粒度定义例如“task.submit”“task.verify”“task.archive”用户与角色、角色与权限之间分别建关联表。前端路由根据权限码动态生成后端中间件统一校验。这样后续哪怕新增一个“第三方评估账号”只需要在库里插记录代码层面基本不用动。多说一句这类系统的查询统计往往按“部门”和“时间”两个维度纵横交叉所以用户表里一定要冗余一个dept_id字段别只存角色后面报表会用到。1.3 原型确认时容易漏的需求督办预警与材料留痕业务方在提需求时很少会主动说“我要催办功能”但实际用起来催办一定是高频操作。我们最后给任务表加了deadline截止时间和warning_status预警状态字段再用定时任务每天扫描超期未提交的任务自动置为“预警”并在首页待办区置顶显示。材料留痕也很容易被忽略。任务进展填报时业务方要求必须能上传 PDF、图片、Excel 佐证材料。我用的是本地目录存储 数据库记录附件元信息的方式把原始文件名、存储路径、上传人、上传时间都记录下来保证后期审计可追溯。2. 技术栈选定为什么偏偏是 Node.js Vue这类管理系统市面上的主流组合无非是 Java Spring Boot Vue或者 Node.js Vue。既然标题写了 Node.js我就说说实际使用中的选型逻辑。2.1 为什么后端选 Node.js 而不是 Spring Boot部门内部有不少前端工程师但不具备 Java 人力储备。Node.js 的一个巨大优势在于团队里写 Vue 的人可以几乎零成本上手写后端统一的 JavaScript 语言栈不用切换上下文。另外Node.js 的生态里Express 框架足够轻量不需要像 Spring Boot 那样引入一大堆 starter 依赖。对于这种“CRUD 工作流”的管理系统Express 加 SequelizeORM 框架完全够用启动速度快迭代效率高。我用的是 Express 4 Sequelize MySQL 的组合。不选 MongoDB因为任务、计划、部门之间关联查询很多关系型数据库在统计报表上有天然优势。2.2 为什么前端用 Vue 而不是 React选择 Vue 的理由很实际模板语法贴近传统 HTML 开发习惯团队成员上手快Element UI 组件库对“后台管理类”页面覆盖度极高表格、表单、弹窗、日期选择器开箱即用Vue Router 的动态路由能力非常适合这种按角色区分菜单权限的系统关于 Vue 2 还是 Vue 3 的问题如果项目是现在新起的直接用 Vue 3 Vite Pinia。Vue 2 已经停止维护新项目没有必要给自己埋坑。后面所有示例我都基于 Vue 3。2.3 整体目录结构长什么样实际开发时我把前端工程和后端工程分开放在两个目录/backend /src /config # 数据库、JWT 等配置 /models # Sequelize 模型 /controllers # 业务逻辑控制层 /routes # 路由注册 /middlewares # 鉴权、日志等中间件 /uploads # 附件存储目录 app.js # 入口文件 /frontend /src /router # 动态路由 /stores # Pinia 状态管理 /views # 页面组件 /components # 公共组件 /apis # axios 请求封装这种分离方式前后端可以通过接口文档各自并行开发互不阻塞。3. 后端骨架Express 分层设计与权限模型落地后端部分我按照“路由—控制器—模型”三层去组织。路由只负责接收请求控制器处理业务逻辑模型负责数据库交互。3.1 数据库核心表设计直接给出我最终落地的几张核心表有需要可以参考表名用途关键字段plans行动计划主表id, title, dept_id, status, start_date, end_datetasks任务分解表id, plan_id, title, owner_dept_id, owner_user_id, deadline, status, warning_statustask_logs任务进展填报日志id, task_id, user_id, content, attachment_url, created_atattachments附件表id, biz_type, biz_id, file_name, file_path, uploader_id, created_atusers用户表id, username, password_hash, dept_id, real_nameroles角色表id, role_code, role_namepermissions权限表id, perm_code, perm_nameuser_roles用户角色关联user_id, role_idrole_permissions角色权限关联role_id, permission_id这里重点解释一下tasks表里的status字段。可选值我定为pending待提交submitted待审核rejected已退回verify_passed审核通过待销号archived已归档overdue已逾期逾期是一种“终态”也可以理解为一种实时计算出来的状态。我的处理方式是每天凌晨定时任务统一扫描把截止时间小于当前日期且状态不是 archived 的任务置为 overdue。3.2 JWT 鉴权与后端权限控制后端所有接口除了登录接口以外全部要求请求头携带Authorization: Bearer token。Token 签发用jsonwebtoken库密钥放到.env文件里管理不写死在代码中。登录逻辑很简单const jwt require(jsonwebtoken); const User require(../models/User); const bcrypt require(bcryptjs); async function login(req, res) { const { username, password } req.body; const user await User.findOne({ where: { username } }); if (!user) return res.status(401).json({ message: 用户不存在 }); const passwordValid bcrypt.compareSync(password, user.password_hash); if (!passwordValid) return res.status(401).json({ message: 密码错误 }); const token jwt.sign( { userId: user.id, username: user.username, deptId: user.dept_id }, process.env.JWT_SECRET, { expiresIn: 12h } ); res.json({ token, user: { id: user.id, username: user.username, deptId: user.dept_id } }); }密码加密必须用bcryptjs不要用 MD5、SHA1 这类可逆或弱散列算法。明文加盐的强度完全不够看。权限校验中间件我封装了一层function checkPermission(permCode) { return async (req, res, next) { const userId req.userId; // 查询用户所有权限码这一步记得加缓存 const permCodes await getPermCodesByUserId(userId); if (!permCodes.includes(permCode)) { return res.status(403).json({ message: 无操作权限 }); } next(); }; }权限码放在每个路由的中间件位置语义清晰。getPermCodesByUserId必须有缓存否则每个请求都要查库数据库会先顶不住。我用的是一个简单内存 Map键为userId值为权限码数组角色变更时清除对应缓存。单机部署没问题集群部署再换 Redis。3.3 任务流转接口的实现要点任务提交和审核是核心接口我单独说明。执行人提交进展router.post(/tasks/:id/submit, authMiddleware, async (req, res) { const task await Task.findByPk(req.params.id); if (!task) return res.status(404).json({ message: 任务不存在 }); if (task.owner_user_id ! req.userId) { return res.status(403).json({ message: 只能操作自己的任务 }); } if ([archived, overdue].includes(task.status)) { return res.status(400).json({ message: 当前状态不可提交 }); } const { content } req.body; await TaskLog.create({ task_id: task.id, user_id: req.userId, content, created_at: new Date() }); task.status submitted; await task.save(); res.json({ message: 提交成功 }); });一个关键点是业务要求每次提交都保留历史日志所以我不是直接覆盖任务的“最新进展”而是先写task_logs再更新主表状态。这为后续的“进展追溯”留了数据基础。审核退回接口同理审核人调用更新状态为rejected的同时把退回原因写入日志表前端就能展示“某年某月某日某人提交进展被某人退回原因是xxx”。4. 前端页面Vue 路由、看板与表单的落地实现前端这部分我用 Vue 3 Vite Pinia Element Plus。下面挑几个重点模块讲。4.1 动态路由不同角色看到不同菜单系统里管理员能看到“用户管理”“系统设置”部门的执行人只能看到“我的任务”“计划列表”。如果把这些菜单写死在路由表里用户把权限之外的 URL 直接敲浏览器地址栏就能绕过菜单限制非常危险。动态路由的思路分两步第一步登录成功后从后端拉取当前用户的权限码列表const permCodes await getMyPermCodes(); userStore.setPermCodes(permCodes);第二步用router.addRoute按权限码挂载对应的路由const dynamicRoutes [ { path: /user-management, name: UserManagement, component: () import(/views/UserManagement.vue), meta: { permCode: user.manage, title: 用户管理 } }, { path: /task-board, name: TaskBoard, component: () import(/views/TaskBoard.vue), meta: { permCode: task.board, title: 任务看板 } } ]; dynamicRoutes.forEach(route { if (userStore.permCodes.includes(route.meta.permCode)) { router.addRoute(Layout, route); } });注意router.addRoute的第一个参数是父路由的 name保证新路由嵌套在布局组件里。全部加完以后调用router.replace(/)重新进入首页。菜单栏则根据router.options.routes里匹配到的路由动态生成。这样权限控制前后端可以对齐后端接口有中间件拦截前端路由有动态挂载双保险。4.2 任务看板拖拽状态更新任务看板是项目经理最爱提的一个功能把任务卡片按状态列展示允许拖拽变更状态。但实际上“拖拽更新状态”在管理系统中容易产生一个隐患拖拽只能改状态不能改负责人和截止时间而且状态之间不是任意可拖的。我的实现方式是看板组件监听拖拽结束事件调用后端接口时带上当前任务的 id 和目标状态由后端校验状态流转是否合法。比如“已归档”任务就不能被拖回“待提交”。看板页面结构大致如下template div classboard div v-forcolumn in columns :keycolumn.status classboard-column h3{{ column.title }}{{ column.tasks.length }}/h3 draggable :listcolumn.tasks grouptask :animation200 item-keyid endonDragEnd template #item{ element } task-card :taskelement / /template /draggable /div /div /template拖拽组件我用的vuedraggable依赖sortablejs。不必自己手写 HTML5 拖拽事件生态库可以省掉大量兼容性调试。onDragEnd里做的事情是收集移动的目标列 status然后调用接口。4.3 表单校验Vue 里容易忽略的细节管理系统里表单特别多计划创建、任务分解、进展填报都离不开表单。Element Plus 的el-form自带校验规则但要注意几个坑。第一日期范围校验要用数组类型校验规则写法不太一样。第二附件上传组件的v-model绑定值是一个文件列表提交时要把文件列表转换成后端约定的参数格式。第三动态表单项比如任务分解时一行一条子任务必须给每行加唯一的 key否则增删行的时候表单数据会错乱。进展填报表单我专门用了一个自定义组件里面包含多行文本域、附件上传、提交按钮。调接口前先调用formRef.validate()校验通过才允许提交。4.4 针对“npm.ps1 无法加载文件”这类环境的实际处理这里插一个环境配置的坑。很多新手在 Windows 上用 npm 装完依赖运行npm run dev时会报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这并不是 npm 装坏了而是 PowerShell 的脚本执行策略默认是 Restricted禁止运行.ps1脚本文件。解决办法有两个以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned然后输入 Y 确认。以后都用cmd命令行窗口来跑 npm 命令而不是 PowerShell。这个坑我在新同事的电脑上几乎每次都会遇到建议做 Node.js 项目前先统一环境把执行策略改掉能省下不少解释时间。5. 联调与部署跨域、附件路径和上线细节前后端并行开发完就进入联调这个阶段开始暴露环境类和配置类的问题。5.1 跨域配置开发环境别和线上环境混为一谈开发时前端跑在 Vite 的 5173 端口后端在 3000 端口必然跨域。我推荐的做法是 Vite 代理而不是打开后端 CORS 允许所有来源。在vite.config.js中配置代理server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }这样前端请求/api/xxx代理到后端后看起来是同源的不需要后端开 CORS。生产环境部署时我会把前端的dist目录交给后端 Express 托管静态文件请求路径完全同源自然不存在跨域问题。5.2 附件上传的目录问题附件上传是最容易在部署时“跑不通”的环节。开发时直接写死uploads/相对路径上线后才发现 Linux 服务器上进程启动目录不同文件传到了奇怪的位置甚至因为权限不足直接报错。我的处理方式是配置文件中用绝对路径指定上传目录如/data/opt/improve-system/uploads确保该目录存在且有写权限数据库里只存相对路径/uploads/xxx.pdfExpress 静态托管app.use(/uploads, express.static(config.uploadDir));数据库里存的file_path要与静态路由的 URL 对应好前端拿到路径后拼成完整地址直接可访问。5.3 部署环境Node.js 进程守护生产环境部署时我用了 PM2 做进程守护。原因很简单直接node app.js启动的话进程一崩就没人帮拉起服务器重启也不会自动恢复。用 PM2 的几步操作npm install -g pm2 pm2 start app.js --name improve-system pm2 save pm2 startuppm2 startup会生成一条开机自启命令执行后 PM2 托管的进程在服务器重启后会自恢复。日志用pm2 logs improve-system实时查看。5.4 测试环节最容易漏的场景我总结了一下这类系统联调时最容易漏测的场景给大家提个醒任务状态任意流转尤其是“逾期任务不可提交”的边界同一个用户同时身兼执行人和部门领导时菜单和操作按钮的显隐附件名包含中文和空格时下载是否正常断网后重复提交任务是否产生重复数据报表模块的数据权限部门领导是否只能看到本部门数据我测试“重复提交”时发现我们的接口没有做幂等处理连连双击提交按钮会生成两条进展记录。修复方法是前端提交按钮 loading后端也做了一遍校验——同一个任务、同一个用户、一分钟内相同内容不重复落库。6. 运维监控与后续迭代系统上线只是开始系统上线不是终点。作为开发者需要留有一双眼睛盯运行状态。6.1 日志与访问监控我在 Express 应用里加了简单的请求日志中间件把所有请求的 url、method、响应耗时、状态码打印到 PM2 日志文件中。这样一旦用户反馈“系统很慢”我可以通过耗时日志定位到是慢接口还是网络问题。必要的时候再加一层接口耗时统计比如app.use((req, res, next) { const start Date.now(); res.on(finish, () { const cost Date.now() - start; if (cost 800) { console.log([slow] ${req.method} ${req.url} cost ${cost}ms); } }); next(); });超时阈值可以按业务调整我统一定在 800ms超过这个值的接口就需要审视 SQL 写法了。6.2 定期数据归档这类系统的任务日志和附件会随着使用不断增长。上线半年后累积数据显著影响查询速度。我的归档策略是超过一年的已归档任务数据从主表中移到task_logs_archive表附件文件保留索引单独优化。如果是微小型团队做这个项目建议一开始就在任务日志表设计上按created_at建索引避免后期数据量大时慢查询。6.3 后续迭代方向第一版上线后业务方大概率会提几个新需求企业满意度在线调查问卷模块营商环境指标大屏展示通常要集成 ECharts与上级部门系统的数据对接接口移动端适配或微信小程序入口以我们的架构来说Node.js 后端扩展新模块的成本很低Vue 前端加页面路由也快。比较费功夫的是“数据对接接口”初期设计数据库时如果字段名和编码不规范对接时就要做数据清洗。这里我还有一个实际体会像“营商环境”这类偏业务管理的系统用户最在意的反而不是界面炫不炫而是填报是否方便、流程是否少绕路、数据能不能对上。所以在开发过程中要时刻提醒自己别过度设计技术架构业务流程顺畅比什么都重要。我在这类项目上踩过的最大一个坑就是初期把权限模型想得太简单导致后来补 RBAC 时改了差不多两周。如果你现在正打算做类似系统先花时间把流程和角色梳理清楚再动手写代码后面一定会少很多麻烦。
返回列表