做幼儿园管理系统这个项目(内部代号 elx46),前后断断续续花了三周时间。技术栈选的就是 Node.js 加 Vue,后端用 Node.js 提供接口,前端用 Vue 写管理后台,给一家小型私立幼儿园搭了一套真正能跑起来的日常管理系统。最初的痛点很具体:园长不想再用 Excel 登记幼儿信息,接送孩子的时候家长在群里喊话、老师凭记忆认人,收费记录也经常对不上账。系统做完之后,档案、考勤、接送、收费这些琐碎事情才算收拢到了一起。这篇内容会把我在选型、表设计、接口开发、前端页面、部署上线过程中踩过的坑和能直接照搬的做法拆开讲一遍。适合正在做毕业设计、想给小型园所做信息化改造,或者打算用 Node.js + Vue 练手全栈的朋友参考。
1. 整体设计与技术选型:为什么是 Node.js + Vue
1.1 选型思路:不用重框架,用最顺手的组合
做这套系统之前,我也纠结过是不是直接上 Spring Boot,后来衡量了一下实际场景,还是决定用 Node.js。原因很实际:这类管理系统的核心是增删改查、权限控制、文件上传和简单的数据统计,没有特别复杂的计算逻辑,Node.js 的 Express 框架写接口非常快,前端同学也能很快上手维护。如果用 Spring Boot,光环境配置和项目骨架就要折腾不少时间,对一个两三周就要交付的项目来说,性价比不高。
前端选 Vue 而不是 React,是因为 Vue 的中文资料多、学习曲线平缓,配合 Element UI 做后台管理界面几乎是现成的。Vue 的单文件组件和组织方式也让页面拆得很清楚,登录页、幼儿档案页、考勤页、收费页各管各的,不会写成一团乱麻。整套组合可以理解成:Vue 管页面展示和数据交互,Node.js 管业务逻辑和数据库操作,两边通过 JSON 格式的接口通信。开发时前后端可以各干各的,不用等对方,联调阶段再合并,效率提升很明显。
还有一点是部署成本。Node.js 应用在服务器上只需要装一个运行环境,进程管理用 PM2,反向代理用 Nginx,整个链路比 Java 那套轻很多。对于小园所或者课程设计场景,这种轻量部署方式更友好,出了问题也好排查。
1.2 模块拆解:从晨检到离园的业务闭环
幼儿园管理系统的模块划分,我是按照幼儿园一天的实际流程来的。早上家长送孩子到园,老师要做晨检,记录体温和健康状况;孩子进入班级后,考勤状态变成已入园;下午家长来接,需要在门卫或者老师那里确认身份,系统记录离园时间和接送人。这个流程听起来简单,但涉及的角色和状态切换不少。
系统里最终拆成了这几个核心模块:幼儿档案管理、班级管理、教职工管理、每日考勤、接送记录、收费管理、公告通知、健康晨检记录。每个模块对应一个或几个数据表,页面端就是对这些表的新增、修改、查询和删除。比如幼儿档案模块,除了基本信息,还要上传头像和体检报告;收费模块要记录每月的托费、餐费、杂费,并且区分已交和未交状态;公告通知模块则是园方发布放假、活动、食谱等信息的地方,家长登录后能看到自己孩子班级下的通知。
模块之间不是孤立的。孩子从入园到离园,考勤表里要记录状态,接送记录表要关联幼儿 ID 和家长 ID,收费表要关联班级信息用于生成统计报表。设计的时候把这些关系画成一张简单的脑图,后面写接口和页面就不会反复改表结构。
1.3 数据库设计与关系:先定表,再写代码
我选的是 MySQL,因为园方后续可能要跑报表,MySQL 比 SQLite 更适合并发场景。核心表大概有这些:admin 用户表(同时存老师和管理员)、class 班级表、student 幼儿表、attendance 考勤表、pickup_record 接送记录表、payment 收费表、notice 公告表、health_check 晨检表。
用户表里的角色字段 role 用来区分 admin、teacher、parent,不同角色登录后看到的功能菜单不一样。幼儿表里除了姓名、性别、出生日期,还要存班级 ID、家长手机号、家庭住址、过敏史等字段。考勤表里每天每个孩子一条记录,字段包括幼儿 ID、日期、入园时间、离园时间、状态。接送记录表更特殊一点,一次接送可能涉及多个接送人,所以我把接送人信息直接冗余在表里,同时记录接送人和幼儿的关系,比如爸爸、妈妈、爷爷等。
表关系主要是外键关联,student 表关联 class 表,attendance 表关联 student 表,payment 表关联 student 表。实际建表的时候我并没有开数据库外键约束,而是在应用层控制逻辑关系,这样后期做数据迁移或者批量导入的时候更灵活。这个取舍在小项目里很实用,只要接口层把校验做够,就不容易出现脏数据。
2. 核心功能实现与难点拆解
2.1 幼儿档案模块:一张表如何撑起核心信息管理
幼儿档案是整个系统使用频率最高的模块,因为它牵涉到考勤、收费、接送等多个流程。字段设计上除了姓名、性别、出生日期、入园日期这些基本信息,还必须包含班级 ID、家长姓名、联系电话、家庭住址、紧急联系人、过敏史和备注。过敏史这个字段千万别省,实际使用中老师最关注的往往是孩子能不能吃某种食物、有没有药物过敏。
照片上传我用的是 multer 中间件,后端接收图片文件后存到服务器 uploads 目录,数据库中只存访问路径。这里有个经验:不要把图片直接转成 base64 塞进数据库,否则数据表会膨胀得非常快,接口响应也会变慢。统一用静态资源服务器或者 Nginx 映射 uploads 目录来处理图片访问,页面加载速度会明显好很多。
查询列表必须支持关键词搜索和分页,搜索条件包括姓名、班级、入园日期范围。分页参数拿 page 和 pageSize,后端用 limit 和 offset 实现。还应该加一个导出功能,老师经常需要把班级名单导成 Excel,后端可以用 node-xlsx 生成文件返回下载链接。这个功能虽然不起眼,但实际用起来满意度很高。
2.2 考勤与接送闭环:状态流转怎么设计
考勤和接送是幼儿园系统的灵魂功能。每天孩子到园后,老师在前端点“入园”,系统生成当天考勤记录,状态为已入园;离园时点“离园”,同时记录离园时间。如果孩子当天请病假或者事假,老师需要单独标记请假状态,这样月底统计出勤率的时候可以直接按状态汇总。
接送模块更有意思。国内幼儿园对安全要求高,不是谁来都能接走孩子。系统里我做了接送人白名单机制:每个孩子可以维护多个接送人,包含姓名、电话、与孩子关系。家长来接时,老师搜索孩子姓名或者手机号,系统展示白名单内所有接送人,老师核对身份后选择实际来接的人,生成接送记录。如果是白名单以外的人,接口直接拒绝并提示联系家长确认。
这里最关键的一点是事务处理。考勤状态从已入园变成已离园时,必须同时更新考勤表和写入接送记录,如果两步操作中间出错误,会出现孩子已离园但接送记录缺失的情况。Node.js 里我用数据库事务包裹这两步操作,任何一步失败就整体回滚。这也是我后面运维时觉得最值得的一次设计。
2.3 权限控制:admin、teacher、parent 三级角色
系统里有三类账号:超级管理员、老师、家长。管理员可以管理所有班级和所有的数据,老师只能管理自己负责的班级,家长登录后只能看到自己孩子的信息。这个权限模型不复杂,但容易做漏,尤其是家长账号和幼儿的关联关系。
后端我用 JWT 做登录态。用户登录成功后,服务端签发一个包含用户 ID、角色、班级 ID、有效期等信息的 token,前端每次请求在 Authorization 头里带上。后端写一个 auth 中间件,解析 token、校验角色,并在需要时把用户信息挂到 req 对象上。
前端同样要做路由守卫。Vue Router 的 beforeEach 钩子里检查本地有没有 token,没有就跳登录页;有 token 再根据角色判断当前路由是否允许访问。管理员和老师的菜单不一样,家长的菜单更简单,这里我用了一个动态路由的方式,根据角色过滤路由表后 addRoutes。前端守卫只是体验优化,真正的安全边界必须放在后端,这个原则一定不能省。
2.4 Vue 里播放 m3u8:一次历练出的经验
园方有需求:希望能在系统里看监控回放和教学视频,视频文件大多是 m3u8 格式的分片流。一开始我用原生 video 标签直接播,发现兼容性很差,后来换成了 vue-video-player 配合 hls.js 来解决。
具体做法是:先安装 hls.js,在 Vue 组件里监听视频地址变化,如果是 m3u8 后缀就用 Hls 对象加载,并绑定到 video 元素上;如果浏览器原生支持,就直接设置 video 的 src。播放器样式和进度条就交给 vue-video-player 处理。这里最坑的是跨域问题,m3u8 列表文件和 ts 分片文件都必须在服务端配置跨域头,否则播放器虽然能请求到索引文件,但加载分片时会报错。如果文件在 Nginx 上,需要在 location 里加上 Access-Control-Allow-Origin 头。
还有一个细节:有些 m3u8 地址带鉴权 token,过期时间很短,前端拿到的播放地址可能几分钟后就失效。这种情况不要在前端拼地址,而是让后端接口生成短时有效的播放地址,前端只管接收和播放。幼儿园的视频监控属于敏感信息,这种临时地址能降低泄露风险。
3. 从零到能跑的完整实操过程
3.1 环境准备:Node.js 安装和环境变量配置
Node.js 的安装本身没什么难度,但有一个极其常见的问题,几乎每个新手都会遇到:在 PowerShell 里执行 npm 命令时报错“无法加载文件 ...npm.ps1,因为在此系统上禁止运行脚本”。这是 PowerShell 的执行策略默认禁止运行脚本导致的,并不是 Node.js 装坏了。
解决办法有两种。第一种是在当前项目目录下使用 cmd 终端执行 npm 命令,绕过 PowerShell 的脚本策略,这对于偶尔跑一下命令比较省事。第二种一劳永逸的做法是在 PowerShell 里执行Set-ExecutionPolicy RemoteSigned,然后选 Yes,这样本机创建的脚本和来自可信来源的脚本都可以运行。我这里更建议第二种,因为在 VS Code 里经常要启动 dev server,每次都切 cmd 很麻烦。
Node.js 环境变量一般安装包会自动配好,但如果你用了免安装版,要手动把 Node.js 根目录加到 PATH。我自己习惯检查三件事:node -v是否输出版本号,npm -v是否正常,以及 npm 的全局路径是否是预期目录。npm 默认下载慢,我会设置国内镜像源:npm config set registry https://registry.npmmirror.com。这一步能省下大量等待时间。
3.2 后端工程初始化与核心接口
后端我用的 Express,项目结构按照模块来分:routes放接口路由,controllers放业务逻辑,models放数据库操作,middleware放鉴权和错误处理。先初始化项目:
mkdir server && cd server npm init -y npm install express mysql2 cors jsonwebtoken multer入口文件不用写得很复杂,核心是把中间件和路由挂载上去。举个例子,登录接口的实现逻辑是:接收用户名和密码,查数据库比对,匹配后生成 JWT 返回给前端。
const express = require('express'); const jwt = require('jsonwebtoken'); const db = require('./models/db'); const app = express(); app.use(express.json()); app.use(cors()); app.post('/api/login', async (req, res) => { const { username, password } = req.body; const [rows] = await db.query( 'SELECT * FROM admin WHERE username = ? AND password = ?', [username, password] ); if (rows.length === 0) { return res.status(401).json({ message: '用户名或密码错误' }); } const user = rows[0]; const token = jwt.sign( { id: user.id, role: user.role, classId: user.class_id }, 'your-secret-key', { expiresIn: '12h' } ); res.json({ token, user: { id: user.id, username: user.username, role: user.role } }); }); app.listen(3000, () => console.log('API server running on 3000'));注意登录密码不能明文存,生产环境至少要用 bcrypt 做哈希。上面代码只是演示流程,真正写项目时要把查询用户和校验密码拆成独立函数,方便后期加日志和限流。
幼儿列表接口要支持分页和过滤,写起来也不复杂。查询参数里带上 page、pageSize、keyword、classId,SQL 用 LIKE 模糊搜索,返回结果同时包含总条数和当前页数据。分页数据格式统一成{ list, total },前端拿到后可以直接渲染表格和分页器。
3.3 前端 Vue 工程初始化与页面落地
前端我用的 Vue CLI 创建项目,组件库选 Element UI,HTTP 请求库选 axios,状态管理用 Vuex。创建命令:
vue create web cd web npm install element-ui axios vuex vue-router入口文件需要注册 Element UI 和路由。我习惯把 axios 实例单独封装在utils/request.js里,统一设置 baseURL、超时时间和请求拦截器。请求拦截器里从 localStorage 取 token,加到 Authorization 头;响应拦截器里判断状态码,401 时清空登录信息并跳转登录页。
后台页面布局用的是 Element UI 的el-container,左侧菜单根据角色动态渲染。拿幼儿档案页举例,就是一个搜索栏加表格加分页器。搜索栏放关键词输入框和班级选择器,表格列展示孩子基本信息,操作列放编辑和查看按钮。页面对应的方法就是调用接口获取数据,再绑定到表格数据源上。
刚开始用 Vue 的人容易把数据处理全堆在组件里,页面一多就乱。我这边每个模块都会建对应的 API 文件,比如api/student.js里统一放getStudentList、addStudent、updateStudent等方法,页面组件只负责调用。这样接口路径改动时只需要改一个文件,维护成本低很多。
3.4 联调与部署:从本地到服务器
开发环境下前后端分离最常遇到跨域问题。解决办法不是在 axios 里写完整后端地址,而是在 Vue CLI 的vue.config.js里配置 dev server 代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } }这样前端请求/api/login时会被代理到http://localhost:3000/api/login,浏览器看起来是同源请求,可以避免跨域。上线时我用 Nginx 托管前端打包产物,并把/api反向代理到 Node.js 服务:
server { listen 80; server_name your-domain.com; location / { root /var/www/web/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个关键配置是try_files ... /index.html,否则 Vue Router 使用 history 模式时,刷新非首页路径会直接 404。部署完用 PM2 启动 Node.js 进程,设置开机自启,整个系统就能长期跑了。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
下面这个表是我在开发和维护过程中遇到最多的问题,基本覆盖了新手最容易卡住的几个点。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| npm 命令在 PowerShell 里报“禁止运行脚本” | PowerShell 执行策略限制 | 管理员身份运行Set-ExecutionPolicy RemoteSigned,或用 cmd 执行 npm |
| npm install 速度极慢 | 默认源在国外 | 设置镜像源npm config set registry https://registry.npmmirror.com |
| 前端请求后端接口报跨域 | 前后端端口不同 | 开发环境用 devServer.proxy,生产环境用 Nginx 反向代理 |
| 本地路由正常,部署后刷新页面 404 | Nginx 没配置 try_files | 在 location / 中加入try_files $uri $uri/ /index.html |
| 图片上传成功后访问不到 | 静态资源路径没映射 | Nginx 添加 uploads 目录的 location,或后端用 express.static 托管 |
| m3u8 播放只有声音没有画面 | ts 分片跨域或解码格式问题 | 服务端配置跨域头,并确保分片地址可公网访问 |
这些问题里最容易被人忽视的是第一条。很多新人看到“禁止运行脚本”就以为 Node.js 安装出问题了,重装好几遍还是一样的报错,其实只是 PowerShell 策略在做拦截。如果你只是临时用,直接在 cmd 里运行 npm 就行,但长期开发还是改一下执行策略更省心。
4.2 三个印象最深的调试经历
第一个是 npm 脚本问题。当时我在 VS Code 里敲npm run serve,结果终端直接报错:npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1。第一次遇到时我也挺懵,后来查资料才知道是 PowerShell 的ExecutionPolicy惹的祸。我当前项目要求团队成员各自电脑都能跑,所以统一让他们把策略改成 RemoteSigned,同时提醒团队成员不要随意运行来历不明的脚本。
第二个是 Vue Router 刷新 404。本地开发时路由很正常,但打包部署到 Nginx 后,只要用户停留在某个子页面刷新,就会变成 404。原因是 Vue Router 的 history 模式会把路由交给前端处理,但 Nginx 默认找不到对应的真实文件。最后在配置里加了一行try_files $uri $uri/ /index.html,问题立即解决。这个知识点不复杂,但没踩过坑的人确实想不到。
第三个是 m3u8 播放的跨域问题。前端明明拿到了 m3u8 地址,播放器也初始化了,但一直卡在加载中。我用浏览器开发者工具看 Network,发现 ts 分片请求全部被 CORS 拦截。后来我在视频文件所在的服务端统一加了跨域头,播放立刻恢复正常。这个过程让我意识到,处理视频流的时候不仅要看页面报错,还要多关注网络面板里每个分片请求的状态。
4.3 上线后还要注意的两件事
系统跑起来之后,真正的考验才开始。第一件事是数据备份。幼儿园的数据量不大,但每天都有考勤和接送记录,一旦数据库损坏,恢复成本非常高。我写了一个简单的定时任务,每天凌晨把 MySQL 数据库导出成 SQL 文件,保留最近 30 天,备份文件存到另一块磁盘或者云存储。这个习惯很笨,但能救命。
第二件事是账号安全。幼儿园系统里保存的是孩子的个人信息和接送记录,属于比较敏感的数据。管理员密码不能太简单,家长账号要绑定手机号,后端接口要做简单的频率限制,防止被爆破。生产环境的 JWT 密钥也不要写死在代码里,最好通过环境变量注入。虽然小园所的访问量不大,但这些安全习惯还是要养成,后面接手任何项目都用得上。
5. 一点个人体会
这个项目做完之后,我有几点感受特别深。第一次去园所部署的时候,老师们其实不太习惯用系统,他们更信任原来的纸质登记本能理解,所以我特意把晨检和接送页面做得特别简单,按钮能少就少,大图标加大字号,尽量让老师扫一眼就能知道下一步点哪里。技术上的坑可以慢慢填,但如果一线用户觉得系统难用,再完善的功能也白搭。
如果让我重做一次,我会从一开始就把“操作日志”加上。现在日志模块是后来补的,虽然能用,但审计粒度不够细,比如某个收费记录被谁修改过、改之前是什么值,这些信息目前查不到。对于涉及钱和幼儿安全的功能,操作留痕特别重要,建议所有做类似系统的朋友第一版就加上。
最后再分享一个小技巧:开发时不要把前端打包地址写死,用一个环境变量区分开发、测试、生产。我吃过一次亏,在本地测试完直接打包上传,发现所有接口请求都指向了 localhost,排查了半天才反应过来是环境变量没切。这个项目让我最大的收获不是某个框架写得多熟,而是明白了系统最终要为人服务,用户顺手才是真的好。