基于Node.js与Vue的幼儿园管理系统:前后端分离开发实战 去幼儿园接孩子的时候我看见门口保安抱着一本厚厚的登记簿每个来接孩子的家长都要排队签名写下时间、和孩子的关系。旁边几个家长在翻手机相册——老师把当天拍的照片发在微信群里几十张照片乱成一团想找自己家孩子得翻半天。那会儿我就在想很多幼儿园的信息化程度其实还停留在“微信群纸质表”的阶段。这个项目——nodejs基于Vue的幼儿园管理系统——就是冲这个痛点去的后端用Node.js前端用Vue把幼儿档案、晨检考勤、请假审批、收费管理、食谱公告这些日常事务搬到线上。文章适合两类人读一是准备做前后端分离项目的开发者想找个完整案例参考二是手里有幼儿园信息化需求的从业者想搞清楚一套系统到底应该长什么样。我用了一周多的时间把项目从零搭起来中间踩了不少坑尤其是Node.js环境配置和考勤数据设计这两块。下面按我的实际开发顺序来讲从业务梳理到技术选型再到前后端落地和上线部署完整过一遍。这套思路不止适用于幼儿园管理系统换成一所学校、一家托育中心甚至一个小型机构的内部管理系统照样能用。1. 幼儿园管理系统不是“幼儿版OA”业务模块与数据流向先理清我承认一开始我犯过一个典型错误拿到这个项目就想着怎么建表、怎么写接口结果做到一半发现考勤和缴费的逻辑对不上反复改了两版。后来我坐下来老老实实把业务画了一遍才发现幼儿园管理系统和普通企业OA最大的区别——它的核心对象不是一个“员工”而是一个“孩子”所有业务都围绕孩子的在园状态在转。1.1 四类角色四个完全不同的使用场景园长、带班老师、保健医、家长这四类人天天要用系统但诉求完全不同。园长关心的是全园的整体情况一共有多少个孩子、每个班多少人、当月收费完成了多少、有没有孩子长期缺勤。他要的是统计数据不是一个一个点开看详情。所以园长端的首页一定要有汇总卡片和图表而不是把员工列表或者考勤流水堆给他。带班老师是系统里使用频率最高的人。每天早上一入园要记录每个孩子的到园时间中午要发食谱下午放学要记录离园时间平时还要替家长提交请假申请、把孩子的日常表现记录下来。老师的操作必须快最好一个页面能把一个班三十个孩子的到园状态全看完、点一下就完成标记而不是像填表那样一个一个录入。保健医的角色比较特殊。每天早上晨检要给孩子测体温、看嗓子、查手部有异常的要记录在案。这个角色的数据是独立一层的它不参与日常考勤业务但对异常情况的记录必须完整——哪天哪个孩子有什么症状、做了哪些处理都要能追溯。家长的使用频率不高但非常在意。他们要看的就三件事孩子今天有没有安全到园、中午吃了什么、有没有请假记录。所以家长端做得越简单越好打开App或者网页第一眼就能看到孩子今天的在园状态这种设计比任何花哨的功能都管用。1.2 六大核心业务模块梳理下来这个系统最终被拆成了六个模块刚好覆盖幼儿园管理的日常。幼儿档案管理孩子的姓名、性别、出生日期、过敏史、紧急联系人。这个模块是所有业务的基础考勤、缴费、请假都要关联到幼儿ID上。班级与教职工管理班级的年级、人数上限、带班老师老师的账号、角色、所带班级。这一层相当于系统的组织架构。考勤管理每天入园和离园时间记录、缺勤标记、体温记录。这是核心模块后面我会重点讲它的表结构设计。请假审批家长提交请假申请老师审批系统自动关联考勤状态。收费管理保育费、餐费、杂费等不同收费项目记录应收、实收、缴费状态。这个模块最容易被低估但真正做起来会发现状态非常多。食谱与公告每天三餐两点的内容、班级通知、园所公告。家长端的主要信息流来源。1.3 一条贯穿全园的主线数据流把所有模块串起来核心数据流是这样的一个孩子入园时创建档案被分配到一个班级每天早上到园产生一条考勤记录下午离园考勤记录补上离园时间如果孩子当天请假请假审批通过后影响考勤状态月末根据孩子的在园天数和收费标准生成缴费单整个过程产生的数据会汇总到园长端的统计页面。这条主线理清楚之后数据库表的结构其实就呼之欲出了以幼儿表为中心向外延伸考勤表、请假表、缴费表班级表和用户表作为组织基础。有了这个全局视图后面的开发就顺了。2. 为什么是Node.js Vue而不是Spring Boot React一次务实的选型复盘技术选型这种事网上吵得不可开交实际做项目的时候没那么多讲究。我当时在Java和Node.js之间犹豫过最后选了Node.js Vue核心原因是三个字省事情。2.1 后端从Java换成Node.js省掉的到底是什么Spring Boot在中小型管理系统里当然能用而且很成熟但它的成本对于这个项目来说有点偏高。一个最简单的CRUD接口Java那边要写实体类、Mapper、Service、Controller四层还要处理Maven依赖和打包配置。Node.js这边用Express一个路由文件加一个处理函数就完了前后端的语言还统一全是JavaScript新手或者小团队维护起来少学一门语言。性能方面幼儿园管理系统的并发量其实很低一个普通规模的园所可能也就二三十个老师加几百个家长在用峰值QPS到不了两位数。Node.js的事件驱动模型在这种场景下完全够用根本用不着担心所谓的“单线程瓶颈”。真要到了需要横向扩展的时候那时候再拆服务也不迟。2.2 Vue在前端三巨头里的位置前端框架我选Vue而不是React一个重要原因是Vue的模板语法对我来说更直观。写React要用JSX一个按钮的显隐都要思考是用state还是用条件渲染Vue的v-if、v-for、v-model就像在写HTML的自然延伸业务人员看代码都能大致猜出意思。幼儿园管理系统这种业务逻辑不复杂、但表单和列表特别多的项目Vue这种“约定大于配置”的风格特别合适。组件库方面我选了Element Plus它在中后台管理系统里的生态太成熟了表格、表单、弹窗、日期选择器全都有现成的样式也统一。对比Ant Design VueElement Plus的中文文档更全遇到问题一搜就有答案这对开发效率的帮助是实打实的。2.3 数据存储用MySQL而不是MongoDB的原因数据这块我直接用了MySQL。理由很朴素这个系统的数据关联性太强了。一个孩子属于一个班级一个班级有多个孩子一条考勤记录关联一个孩子一条缴费单关联一个孩子和一笔费用。这种关系型数据用表和表之间的外键关联、用SQL的JOIN去查询心智负担最低。MongoDB的文档模型在内容管理、日志收集这类场景是强项但在“孩子的缴费记录要和考勤天数对账”这种需求面前还是MySQL更顺手。数据库版本我用的是MySQL 5.7为什么不选8.0因为5.7太稳定了网上资料多很多云数据库默认版本就是5.7后面部署迁移也少踩坑。当然这只代表我的个人习惯8.0在性能和功能上更强如果你是全新环境用8.0也没问题。3. 环境搭建踩坑实录npm.ps1无法加载、Node版本陷阱的完整排错过程这个项目最让人头疼的其实不是业务代码而是最基础的环境配置。网上搜“npm.ps1无法加载”能搜出一堆报错帖子我本地也遇到了折腾了半小时才解决。这里把过程完整写出来希望能帮你省掉这个时间。3.1 版本选择LTS不是越新越好Node.js官网的下载页上会有两个版本一个Current一个LTS。很多新手一看Current是最新版就下载了我在项目早期就吃过这个亏装的Node.js版本太新和项目的依赖兼容性出了问题。我的建议是装LTS版本。LTS全称是Long Term Support意思是长期维护版稳定性和兼容性都有保障。以我当时的环境为例Node.js 18.x就是很稳妥的选择Vue 3和Express都兼容得很好。新版本虽然功能多但在企业项目里“能用且稳定”比“功能新”重要得多。3.2 npm.ps1被禁止运行脚本的两种解法装完Node.js之后在VS Code的终端里执行npm -v大概率会遇到下面这个报错npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。 有关详细信息请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。这个问题的原因是PowerShell的执行策略默认是Restricted不允许运行未经签名的脚本文件。npm.ps1是npm在PowerShell环境下的脚本文件所以被拦了。解法一以管理员身份打开PowerShell执行下面这行命令然后重新打开终端Set-ExecutionPolicy -ExecutionPolicy RemoteSignedRemoteSigned的含义是本地创建的脚本可以运行从网上下载的脚本如果没有数字签名则不允许运行。npm.ps1是随Node.js安装到本地的所以可以正常执行。这个方法一劳永逸但要注意权限——如果你公司的电脑是由IT统一管控的可能没有管理员权限那就用第二种。解法二不用PowerShell改用CMD或者Git Bash。在VS Code里通过Ctrl Shift P打开命令面板输入“Terminal: Select Default Profile”选择Command Prompt然后再试npm -v问题直接就没了。这个方法不修改系统设置只是绕开了PowerShell的脚本限制。我当时用了解法一因为后续还要在PowerShell环境跑很多命令比如设置环境变量一次性解决比较省心。3.3 环境变量与全局路径配置如果说npm.ps1的报错是第一大坑环境变量配置就是第二大坑。很多人装完Node.js发现node -v能输出但npm -v提示找不到命令这就是环境变量没配置好。Windows下正确做法是右键“此电脑”-属性-高级系统设置-环境变量新建一个NODE_HOME变量指向Node.js的安装目录然后在Path里添加%NODE_HOME%和%NODE_HOME%\node_modules\npm\bin。配置完后要重新开一个终端窗口配置才会生效。还有一个我后来才注意到的细节npm默认的全局安装路径在系统盘如果你用npm全局安装工具比如npm install -g pm2可能会因为权限问题安装失败。我的做法是修改npm的全局路径把它指到项目目录或者自定义的目录npm config set prefix D:\develop\nodejs_global这样全局安装的工具就不会一股脑塞进系统盘了也方便后面统一管理。3.4 前后端项目初始化目录环境准备就绪后我开始初始化项目结构。前端我用Vite创建Vue 3项目后端用Express脚手架目录结构如下elx46-kinder/ ├── frontend/ # Vue 3 Vite Element Plus │ ├── src/ │ │ ├── api/ # axios接口封装 │ │ ├── router/ # 动态路由配置 │ │ ├── store/ # Pinia状态管理 │ │ ├── views/ # 页面组件 │ │ └── main.js ├── backend/ # Node.js Express │ ├── src/ │ │ ├── routes/ # 路由定义 │ │ ├── controllers/ # 业务处理 │ │ ├── models/ # 数据模型 │ │ └── middleware/ # 鉴权中间件 │ └── app.js └── database/ # 建表SQL脚本前后端分离的好处在这里体现得很明显开发的时候前端跑npm run dev后端跑npm run server互不干扰联调的时候后端只负责出接口文档前端只负责接数据。这也是我强烈推荐的管理系统项目结构逻辑边界清晰后面单独替换哪一层都不至于动全局。4. 后端从零搭建Express MySQL的核心表设计与接口实现后端我用了Express框架这也是Node.js生态里最经典的Web框架。数据库的连接我选择了mysql2这个驱动因为它在原生mysql驱动的基础上支持了Promise配合async/await写起来非常舒服。中间踩了一个数据库连接的坑一开始没用连接池每个请求都新建连接结果并发一上来就有连接超时。后来改成mysql.createPool()问题就消失了。4.1 五张核心表的设计思路数据库设计是整个项目的根基我花的时间最多。以下几张核心表的结构基本能覆盖前面说的业务主线。classes 班级表字段类型说明idint主键自增namevarchar(50)班级名称如“小一班”gradevarchar(20)年级托班/小班/中班/大班head_teacher_idint带班老师关联用户表capacityint最大人数children 幼儿表字段类型说明idint主键自增namevarchar(30)幼儿姓名gendertinyint1男 2女birthdaydate出生日期class_idint所属班级guardian_namevarchar(30)主要接送人姓名guardian_phonevarchar(20)接送人电话avatarvarchar(200)头像图片路径allergyvarchar(255)过敏史无则空statustinyint1在读 2转出 3休学attendance 考勤表字段类型说明idint主键自增child_idint幼儿IDdatedate考勤日期morning_timedatetime入园时间NULL表示未记录evening_timedatetime离园时间NULL表示未记录temperaturedecimal(4,1)晨检体温statustinyint1正常 2迟到 3请假 4缺勤leaves 请假表字段类型说明idint主键自增child_idint幼儿IDstart_datedate请假开始日期end_datedate请假结束日期reasonvarchar(500)请假原因statustinyint1待审批 2通过 3拒绝apply_timedatetime申请时间payments 缴费表字段类型说明idint主键自增child_idint幼儿IDfee_typevarchar(50)费用类型保育费/餐费/杂费amountdecimal(10,2)金额due_datedate应缴日期pay_statustinyint1待缴 2已缴 3逾期pay_timedatetime实缴时间这几张表的设计有几个关键决策。第一考勤表是“一条记录包含入园和离园两个时间”而不是来一次记一次、走一次记一次。这样月底统计每个月的出勤天数时SELECT COUNT(*) FROM attendance WHERE date BETWEEN ... AND ...一条SQL就搞定了性能也好。第二请假表和考勤表是分开的请假审批通过后由审批接口去写考勤表的status字段这样考勤记录不会在请假期间缺失统计出勤率时也不会误算。第三缴费表里用pay_status字段记录一个枚举状态而不是直接删记录这样月底对账时能看到每一笔费用的完整生命周期。4.2 JWT登录鉴权中间件登录鉴权我直接用了JWT。相比Session方案JWT不需要在服务端存储会话状态对前后端分离的项目来说请求头带上Token就能鉴权实现起来也简单。签发Token的逻辑在登录接口里const jwt require(jsonwebtoken); const SECRET_KEY process.env.JWT_SECRET || elx46_secret; function signToken(user) { return jwt.sign( { id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: 7d } ); }中间件里校验Token的逻辑function authMiddleware(req, res, next) { const header req.headers.authorization; if (!header) { return res.status(401).json({ code: 401, message: 缺少凭证 }); } try { const token header.startsWith(Bearer ) ? header.slice(7) : header; req.user jwt.verify(token, SECRET_KEY); next(); } catch (err) { return res.status(401).json({ code: 401, message: 凭证无效或已过期 }); } }这个中间件我注册在除了登录和注册之外的所有路由上。后来又加了一层roleGuard中间件用来限制只有特定角色才能访问某些接口比如只有老师和管理员能提交考勤家长只能查看。字典级别的角色权限靠中间件就够了如果哪天角色权限变得复杂再上CASL这类权限库也不迟。我当时没加原因是幼儿园管理系统的权限层级其实很清晰管理员管所有老师管自己班家长管自家孩子。4.3 考勤打卡接口的实现细节考勤是整个系统里最核心的接口设计时我特意让它既能支持“老师手动点选”又能支持“将来的硬件打卡对接”。接口定义是两个入园打卡和离园打卡。// POST /api/attendance/check-in router.post(/check-in, authMiddleware, async (req, res) { const { childId } req.body; const today new Date().toISOString().split(T)[0]; // 查今天是否已有考勤记录 let record await db.query( SELECT * FROM attendance WHERE child_id ? AND date ?, [childId, today] ); if (record.length 0) { // 已有记录更新入园时间避免重复创建 await db.query( UPDATE attendance SET morning_time NOW() WHERE id ?, [record[0].id] ); } else { await db.query( INSERT INTO attendance (child_id, date, morning_time, status) VALUES (?, ?, NOW(), 1), [childId, today] ); } res.json({ code: 0, message: 打卡成功 }); });这里的核心思路是“当日首次打卡创建记录之后打卡更新记录”避免产生重复数据。离园接口的逻辑类似只是更新的是evening_time。这个接口如果要接硬件设备只要把childId换成读卡器输出的ID即可后端逻辑不用大改。4.4 幼儿照片上传与静态资源映射幼儿的头像和体检报告这类文件我用multer来处理。图片上传之后存放在后端的uploads目录然后通过静态资源中间件暴露出去const express require(express); const path require(path); app.use(/uploads, express.static(path.join(__dirname, uploads)));这里有一个我后来部署时才发现的坑本地开发时图片上传到uploads目录前端直接访问http://localhost:3000/uploads/xxx.jpg没问题但部署到服务器上前端和后端往往不在同一个端口甚至同一个域名下图片路径就要用完整的URL。我的做法是在上传接口的返回值里直接拼接完整的访问路径避免前端自己拼路径导致拼错。5. 前端页面落地Vue动态路由、考勤交互与角色权限控制前端是用户直接接触的部分体验好不好直接决定系统能不能用起来。我用Vue 3 Vite Element Plus Pinia搭建重点处理了三块动态路由、考勤交互、接口封装。5.1 动态路由不同角色进入系统看到不同菜单管理系统最忌讳把所有菜单一股脑展示给所有用户。家长看到“班级管理”是没意义的老师看到“全园报表”也没必要。我用Vue Router的动态路由功能登录后根据用户的角色动态添加路由表。先定义好各角色对应的路由模块// router/routes.js const commonRoutes [ { path: /login, component: Login }, { path: /profile, component: Profile } ]; const adminRoutes [ { path: /dashboard, component: Dashboard }, { path: /children, component: ChildrenManage }, { path: /classes, component: ClassesManage }, { path: /payments, component: PaymentsManage }, { path: /statistics, component: Statistics } ]; const teacherRoutes [ { path: /dashboard, component: TeacherDashboard }, { path: /attendance, component: AttendanceManage }, { path: /leaves, component: LeaveHandle }, { path: /recipes, component: RecipeManage } ]; const parentRoutes [ { path: /home, component: HomePage }, { path: /my-child, component: MyChild }, { path: /leave-apply, component: LeaveApply } ];登录成功后const roleMap { admin: adminRoutes, teacher: teacherRoutes, parent: parentRoutes }; function assembleRoutes(role) { return [...commonRoutes, ...roleMap[role]]; }路由和侧边栏菜单的渲染是配套的。菜单数据用路由表生成meta.title和meta.icon控制显示名称和图标。这样权限管理和菜单管理用的是同一份数据改一处两处都变不会出现菜单和权限对不上的情况。5.2 考勤打卡页面的交互状态考勤页面是整个系统里交互最重的页面。我的设计思路是默认展示当天所有未打卡的孩子列表每个孩子是一个卡片卡片上有“入园打卡”和“离园打卡”两个按钮。为了让老师一眼看清状态我用三种颜色区分孩子状态灰色代表未到园绿色代表已入园蓝色代表已离园。点击按钮后调用接口拿到响应后更新状态。这里我注意到一个问题如果老师一下子要为好几个孩子打卡每次都弹loading窗会很烦。所以我只在按钮上做了一个小小的状态变化——点击后按钮文字变成“已打卡”同时按钮禁用不弹任何全局提示。全部打卡完页面顶部显示“当前已打卡 28/30 人”让老师心里有数。体温记录我用了一个内联样式入园打卡时弹出一个小输入框默认36.5度老师可以快速修改。这个数据是给保健医看的所以只要求精确到小数点后一位。5.3 Axios拦截器与请求封装前后端联调时最烦的是每个接口都要手动带Token、手动处理错误。我用Axios的拦截器统一处理了这两件事。import axios from axios; import router from /router; import { useUserStore } from /store/user; const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }); service.interceptors.request.use(config { const userStore useUserStore(); if (userStore.token) { config.headers.Authorization Bearer ${userStore.token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code 401) { userStore.clearToken(); router.push(/login); return Promise.reject(new Error(Unauthorized)); } return res; }, error { ElMessage.error(error.response?.data?.message || 请求失败); return Promise.reject(error); } );请求封装放到src/api目录下每个模块一个文件比如attendance.js里封装考勤相关接口export function checkIn(data) { return service.post(/attendance/check-in, data); }这样页面里调接口只需要import { checkIn } from /api/attendance代码干净也统一后端改地址只改一处baseURL就行。5.4 家长端的核心页面孩子今天吃什么、几点走家长端的首页我设计成“信息流打卡状态”的组合。顶部是一张卡片显示孩子的头像、姓名、班级、今日状态。状态用大字展示“已入园 08:05”或“已离园 16:30”。如果今天是请假状态则显示“请假中”。这些数据来自一个聚合接口后端一次查询把孩子的考勤记录、当天食谱、最近公告都返回给前端避免页面多次请求。食谱区域展示今天的三餐两点每餐带一张图片。这里我在后端用了一个不太起眼的优化食谱数据每天生成一条但不删除历史家长可以往前面翻几天的记录。这个功能虽然简单但家长反馈很好因为老人接孩子时经常问“中午吃了什么”有了历史记录就不用再问老师了。请假申请在家长端是一个表单页面选择日期范围、填写原因、提交。提交后页面显示“待审批”状态审批结果由老师端操作后状态会更新家长下次打开页面时能看到。这里我用的是轮询——在页面加载时拉取一次最新状态没有做WebSocket。一天加载一次开销可以忽略。6. 部署上线清单Nginx、PM2、定时备份缺一不可项目开发完只是第一步部署上线才是真正考验基本功的地方。我第一次部署的时候吃了不少亏这里把完整的流程和需要注意的点写清楚。6.1 构建产物与前端部署前端部署不复杂。在frontend目录下执行npm run build构建完成会生成一个dist目录里面是纯静态文件。把这个目录上传到服务器用Nginx托管即可。Nginx配置里将Vue项目配置为SPA模式——所有路由都指向index.html由前端路由接管页面切换。server { listen 80; server_name your-domain.com; root /var/www/elx46/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }这里有个坑Vue Router在history模式下刷新某个子路由页面会404。原因很简单服务器上根本没有/attendance这个路径对应的文件。没有配置try_files的话刷新后就是白屏。我第一版部署就踩了这个坑后来加上try_files $uri $uri/ /index.html;才解决。如果你不想配置这个规则也可以改用hash模式的路由但URL里会多个#不太好看。6.2 后端服务的守护与日志后端是Node.js进程不能直接挂在终端里跑否则关掉SSH终端服务就没了。我用了PM2来做进程守护。npm install -g pm2 cd backend pm2 start app.js --name elx46-backend pm2 save pm2 startuppm2 startup是一个容易被忽略但非常重要的命令它会把PM2注册成系统服务这样服务器重启后Node.js进程会自动拉起不用人工干预。如果你用的是Windows服务器PM2的用法略有不同但整体思路一致。PM2的日志是我排查问题的主要手段。pm2 logs elx46-backend看日志能快速定位接口报错、数据库连接异常等大部分问题。我还养成了一个习惯每个接口的catch块里都会打一条错误日志带上请求参数。这样即使前端没反馈具体问题我翻日志也知道是哪条数据出的错。6.3 图片资源与跨域问题前端部署在80端口后端服务跑在3000端口这就带来了跨域问题。开发阶段我在Vite里配了代理生产环境则用Nginx的反向代理解决。location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样前端请求/api/attendance/check-inNginx会把它转发到后端的3000端口前端和后端看起来就是同源的跨域问题彻底解决。图片资源的访问也一样Nginx单独配一个/uploads的location指向后端的上传目录。还有一个细节上传目录一定要在代码部署目录之外。比如代码在/var/www/elx46/backend上传目录建议放在/var/www/elx46/uploads这样以后升级后端代码直接替换backend目录就行上传的文件不会丢。6.4 数据库备份策略数据无价尤其是幼儿考勤和缴费记录绝对不能丢。我的备份策略是每天凌晨执行一次mysqldump保留最近30天的备份文件。写一个简单的shell脚本#!/bin/bash BACKUP_DIR/backup/mysql DATE$(date %Y%m%d) mysqldump -u root -ppassword elx46 $BACKUP_DIR/elx46_$DATE.sql find $BACKUP_DIR -name elx46_*.sql -mtime 30 -delete然后用crontab调度0 2 * * * /usr/local/bin/backup_kinder.sh实测下来凌晨2点执行备份对业务毫无影响30天的保留期也够用了。如果服务器上有重要的图片文件用rsync同步到另一台机器或对象存储那是更稳妥的方案。7. 项目交付后我重新审视的几个细节系统交付之后我又把代码整体翻了一遍发现有几个地方当初做得不够好这里一并写出来当作复盘。第一考勤统计的粒度太粗。我最初只在考勤表里区分了“正常、迟到、请假、缺勤”但“迟到”的定义其实有讲究——九点前入园算正常九点到九点半算迟到九点半之后可能就算缺勤。当时我只是把入园时间和预设值比较写死了一个判断。现在想想这个阈值其实应该允许管理员在后台配置因为每个幼儿园的入园时间不一样。如果你要沿用我的设计记得把阈值抽成配置项。第二收费模块缺少“退费”场景。实际运营中孩子有时候只上几天学家长会要求按天退费。我当时只做了正向收费退费只能用“填写负数金额”这种别扭的方式处理。严谨的做法是单独设计一张退款表记录退款申请、审批、打款状态。好在这个需求上线后一个月才被提出来我在第二版补上了。第三过敏史字段的校验太松。这个字段虽然简单但直接关系孩子安全。家长填写时容易误填或者漏填我的表单校验只做了非空判断。后来加了二次确认弹窗——“请确认已填写孩子的过敏史信息”虽然多了一步操作但保健医那边放心多了。第四日志审计做得不够。谁在几点修改了哪个孩子的考勤记录这是家长和园长都很在意的事情。我第一版完全没有做操作日志后来有一次家长质疑考勤数据被篡改我翻遍数据库日志也没法证明“没改过”。第二版加了一张操作日志表每次修改记录保存操作人、操作时间、旧值、新值。这个功能不复杂但异常重要。这些虽然都是细节但真实跑起来之后它们恰恰是“系统能不能被信任”的关键。最后这个项目从环境配置到部署上线前后花了不到两周。老实说技术上的难点不多核心还是花在业务理解上——幼儿园管理系统要处理的不是一个一个孤立的功能而是一整条“入园-考勤-请假-缴费-反馈”的闭环。把这条链路理清了、把表关系设计对了后面的代码反而是水到渠成的事。如果你打算复刻这个项目我的建议是先不要急着写代码找一间真的幼儿园聊一聊看看老师每天是怎么做晨检的、家长是怎么接孩子的、月底财务是怎么对账的。那些实际流程中的麻烦才是你的系统真正要解决的东西。技术选型可以用我这个方案也可以换成你熟悉的其他栈但业务建模的思路是通用的。等你做完了回头再看这份记录应该会觉得这几天的折腾值得。