news 2026/10/13 12:00:14

Node.js+Vue前后端分离实战:付费自习室管理系统完整开发复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Node.js+Vue前后端分离实战:付费自习室管理系统完整开发复盘

上个月帮朋友收拾自习室管理系统时,发现老板的核心诉求特别朴素:顾客能自己扫码选座,坐多久按多久算钱,后台能看到实时座位和收入流水,别再靠人工小本子记账对账。整个项目做下来用到的正是目前最主流的一套组合——Node.js做后端服务,Vue写前端页面,前后端通过JSON接口通信。这篇文章把整套付费自习室管理系统的需求拆解、技术选型、代码实现和部署踩坑完整过一遍,给正在做同类项目或准备拿前后端分离练手的朋友一个可以直接参考的路线。

这类系统听起来简单,实际开发起来有不少细节值得琢磨。座位状态如何保证不并发冲突、预约锁座超时怎么办、计费按分钟还是按小时、后台一个个表格怎么组织,都会直接影响老板日常用起来顺不顺手。我尽量把原理讲透,把能直接用的代码和配置给出来,带新手也能一步步跟下来。

1. 付费自习室到底在管什么:把需求拆成状态机

1.1 自习室运营的真实痛点

我朋友这家自习室开在大学城附近,一百多个座位,分成普通自习区、沉浸小黑屋和带隔断的卡座。以前全靠前台人工登记:顾客来了交押金、记手机号、安排座位,走的时候算时长、退押金。平时人少还行,一到考研季、期末周,高峰期排队的人能把前台围满,漏记错记是家常便饭。

老板想要的东西看起来不复杂:一个网页,顾客进去能看到座位图,哪些空的、哪些有人,自己选一个坐下,系统自动计时,走的时候自动从余额扣钱。后台能看谁在哪个座位坐了多久、今天收了多少钱。但“自动计时”这四个字背后,牵扯的是座位从空闲到占用再到释放的完整状态流转,这套逻辑如果设计不好,后续每一个功能都会写上好几遍。

1.2 功能拆解:用户端和管理端各做什么

先别急着写代码,把功能列出清单来。我按“谁用什么功能”分成两端,然后画了一张非常粗的功能表:

模块用户端管理端
座位查看实时座位状态、预约空闲座位维护座位信息、设置座位区域与类型
订单创建预约、签到入座、结束离座查看全部订单流水、处理异常订单
计费查看余额、支付结算设置计费规则、调价、统计营收
用户注册登录、查看个人消费记录查看用户列表、封禁/解禁账号

这张表看着简单,但它决定了数据库表怎么建。用户端关注的是“我现在能不能坐”,管理端关注的是“哪个座位多少钱、今天赚了多少”。这两类需求对数据的要求完全不同:用户端需要实时查询某个座位的状态,管理端需要按时间段聚合订单金额。所以后端接口要分成两条清晰的链路,一条面向现场操作,一条面向统计报表,混在一起后面会改到怀疑人生。

1.3 核心设计:座位状态机

整个系统最核心的抽象不是“座位”这个实体,而是座位的状态流转。我把每一个座位定义成四种状态:

  • 空闲(IDLE):谁都可以预约
  • 锁定(LOCKED):被用户预约了,但还没签到入场
  • 使用中(OCCUPIED):用户已经扫码坐下来,正在计时
  • 维护(MAINTENANCE):座位故障或暂停使用

座位状态变化只能在合法路径里走:空闲才能变成锁定,锁定按时签到变成使用中,使用中结束订单回到空闲。维护状态可以由管理端随时切换,但正在使用中的座位不能直接被改成维护,必须先强制结束订单。

订单的状态跟随座位走:待支付 → 已锁定 → 使用中 → 已完成,除此之外还有两个结束分支——超时取消和用户主动取消。这看似多余,实际作用很大。自习室经常出现预约了人不到的情况,如果不做超时释放,座位就会白白空着,老板损失的是真金白银。我定的规则是预约锁定15分钟,超时未签到自动释放并标记用户违约,违约三次当天不能再预约。

把状态机画清楚之后,后面写后端接口、写前端按钮、写后台列表,全部围绕这个状态流转展开,前端要禁用的按钮、后端要做的并发校验,统统有据可依。这是整个项目里最值得先做的一步。

2. 为什么是Node.js + Vue:这组技术栈的取舍复盘

2.1 前后端分离解决了什么问题

很多刚接触这类项目的人会问,为什么不直接在服务端用模板渲染页面?我也用过那段时期,但做到车位预约、实时座位图这种交互密集的项目,前后端分离的优势就很明显了:前端专注画页面、管交互,后端专注出接口、管数据,两边可以并行开发,你改前端样式完全不影响后端逻辑。

更实际的好处是一套API多处复用。这位老板后续想上小程序、想在大门放一块实时座位大屏,同一套后端接口都能撑住,前端换壳就行。如果页面和服务端耦合在一起,每换一个端就要重写一套服务端逻辑,想想都头疼。

2.2 Node.js做后端:轻量、异步、生态够用

选择Node.js不是因为它在所有场景都比别的语言强,而是对“自习室管理系统”这种中低并发、逻辑以CRUD为主的业务来说,它恰好是最省事的方案。理由有三点:

第一,学习曲线低。前后端同用一种语言(JavaScript/TypeScript),前端工程师不用再学一套Java或Go,团队沟通成本大幅下降,一个人也能Hold住全栈。

第二,异步IO适合IO密集型操作。管理系统大量操作是查数据库、读写文件、调第三方支付接口,Node.js的事件循环机制在这种场景下吞吐能力足够。自习室一天撑死几千笔订单,远没到性能瓶颈,用Node.js跑得绰绰有余。

第三,生态成熟。Express是最老牌的Web框架,文档多、中间件丰富;Sequelize做MySQL ORM、jsonwebtoken做登录态、dayjs处理时间,全都是现成的轮子,站在npm巨人肩膀上开发效率极高。

你可能会拿Spring Boot来对比,毕竟热搜里“springboot vue前后端分离”出现频率很高。Spring Boot确实更工程化、更好招人,但代价是你得配JDK、配Maven、写大量样板代码,项目启动一次能喝杯咖啡。对个人开发者和小团队来说,Node.js这套从安装到跑通不超过十分钟,试错成本低得多。

2.3 Vue前端:渐进式框架,组件化开发

选了Vue3 + Vite + Element Plus作为前端主力。Vite的启动速度是真快,开发时改代码热更新几乎是秒级反馈,相比旧版Webpack时代的漫长编译舒服太多。Vue3的Composition API让逻辑复用变得直观,一个useSeatMap自定义函数就能把座位图的加载、状态刷新、选中逻辑全部封装起来,一个页面组件只需要十几行核心代码。

组件化开发对管理系统尤其重要。座位图是一个组件、订单表格是一个组件、用户列表是一个组件,各个页面像搭积木一样拼起来。后期要改座位图配色或者加一列数据,只动一个组件,不会波及其他页面。Element Plus则直接提供了表格、表单、弹窗、消息提示等后台管理高频UI组件,省掉了大量手写CSS的时间。这套组合做管理系统,可以说是当前性价比最高的选择之一。

3. 从安装Node.js到跑通Vue项目:环境这关别大意

3.1 安装Node.js的版本选择与环境变量配置

很多新手在环境这步就被卡住,而且卡得很冤枉。Node.js安装我建议直接去官网下LTS长期支持版,不要追最新。最新的非LTS版本可能带了新特性,但也可能带来生态兼容问题,做项目求的是稳,不是赶时髦。

安装过程有一个关键选项必须勾上——Add to PATH。勾上之后,安装向导会自动把Node.js的可执行目录写进系统环境变量,这样你在任意终端窗口敲node -v都能识别。如果忘了勾,就得手动去“系统属性 → 环境变量 → Path”里追加Node.js安装目录,比如D:\Program Files\nodejs\。还有一个很容易被忽略的坑:安装路径尽量不要带空格和中文,有些旧工具会对空格敏感,后面装全局依赖时可能出莫名其妙的错误。

装完以后打开命令行,依次输入这两条命令验证:

node -v npm -v

正常情况下会输出类似v18.20.4和10.7.0这样的版本号。只要能打印出版本号,Node.js和自带的npm包管理器就都Ready了。

3.2 死亡率最高的npm.ps1报错,一次说清楚

如果你在PowerShell里执行npm -v时看到这样一句话:

npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本

这个报错的高发程度在我接到的咨询里排第一,其实它不是Node.js没装好,而是PowerShell的执行策略默认禁止运行脚本。.ps1文件是PowerShell脚本,npm.ps1是npm给我们准备好的命令入口,系统默认的Restricted策略不允许任何脚本运行,于是报错。

解决办法很简单,以管理员身份打开PowerShell,执行:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

RemoteSigned的意思是本地脚本可以运行,从网上下载的脚本需要签名,既解决了npm命令的执行问题,又保留了基本的安全防护。执行完成后可以用Get-ExecutionPolicy确认当前策略已经变成RemoteSigned,然后重新开一个终端窗口,npm命令就能正常跑了。

3.3 依赖下载太慢的解决方案

Node.js装好之后,第一件事推荐先配置npm镜像源。国内直连npm官方源下载依赖经常只有几十KB每秒,装一个大项目能等一晚上。改成国内镜像后速度能提升几十倍:

npm config set registry https://registry.npmmirror.com

配置完成后可以用npm config get registry确认指向正确。这一步属于磨刀不误砍柴工,尤其后面要安装几GB重的node_modules时,你会感谢这个配置。

另一个小建议:有条件就换pnpm。pnpm在安装速度、磁盘占用上比npm有明显优势,而且对依赖的版本管理更严谨。执行npm install -g pnpm装好之后,本项目所有依赖安装都可以用pnpm install代替。当然不换也完全没问题,npm本身完全够用。

3.4 用create-vue把第一个项目跑起来

环境就绪后,用官方脚手架创建Vue项目。早期的Vue CLI已经逐步被更轻量的create-vue取代,直接执行:

npm create vue@latest

命令会让你填项目名、选择是否启用TypeScript、Vue Router、Pinia等特性。按需选择即可,自习室这种规模,我建议勾上Router(前端路由必备),Pinia可以先不勾,后面真需要全局状态再手动引入也不迟。

项目创建完成后,进入项目目录安装依赖并启动开发服务器:

cd vue-seat-studio npm install npm run dev

看到终端里出现Local: http://localhost:5173/,浏览器打开这个地址,一个带Vue官方标识的页面就蹦出来了。现在你已经拥有一个可实时热更新的前端工程,后面的页面代码都在这上面叠加。

4. 后端服务骨架与座位预约计费API的实现

4.1 Express项目结构与数据表设计

后端我用的Express + Sequelize + MySQL组合。项目结构按经典的三层职责划分:

server/ app.js # 入口文件:初始化Express、挂载中间件 config/ database.js # 数据库连接配置 models/ # Sequelize模型定义 user.model.js seat.model.js order.model.js routes/ # 路由注册 user.routes.js seat.routes.js order.routes.js controllers/ # 具体业务逻辑 seat.controller.js order.controller.js middleware/ auth.js # JWT登录校验 utils/ price.js # 计费计算工具

routes只负责把URL映射到controller,controller里写业务逻辑,models定义数据结构,各层各司其职。这样做的直接好处是改动点可控:比如要调整计费规则,只需要改动utils/price.js,完全不需要碰路由和控制器。

数据库表我设计了四张核心表,字段遵循够用即可的原则:

  • users:id、手机号、昵称、余额(整数,单位分)、状态
  • seats:id、区域、座位编号、座位类型、价格组、状态
  • orders:id、用户id、座位id、开始时间、结束时间、金额、状态
  • price_rules:id、区域、时段、单价(单位分/分钟)

金额一律用整数分存储,这是无数前辈踩坑换来的经验。浮点运算下,0.1加0.2可能等于0.30000000000000004,但金额出不起这种误差,用分做整数运算一劳永逸。

4.2 座位可用查询与并发锁座

用户端第一个核心接口是查询空闲座位:

// GET /api/seats/available?area=normal async function getAvailableSeats(req, res) { const { area } = req.query; const seats = await Seat.findAll({ where: { area: area || null, status: 'IDLE' } }); res.json({ code: 0, data: seats }); }

这个接口看似简单,真正的难点在后面——用户点“预约”的一瞬间,座位可能已经被别人抢占了。如果不做并发控制,两个人同时预约同一个座位,数据库又两个请求都读到“空闲”,就会双写成功,现场直接翻车。

解决办法是用事务 + 乐观锁。在创建订单的时候,先执行一条带条件的更新语句,只有座位状态还是“空闲”时才允许改成“锁定”:

const sequelize = require('../config/database'); async function createOrder(req, res) { const { seatId } = req.body; const userId = req.user.id; const t = await sequelize.transaction(); try { // 乐观锁:只有当前状态是IDLE时才允许改成LOCKED const [updated] = await Seat.update( { status: 'LOCKED' }, { where: { id: seatId, status: 'IDLE' }, transaction: t } ); if (updated === 0) { await t.rollback(); return res.json({ code: 1, message: '座位已被抢占,请重新选择' }); } const order = await Order.create({ userId, seatId, startTime: null, endTime: null, amount: 0, status: 'LOCKED' }, { transaction: t }); await t.commit(); res.json({ code: 0, data: order }); } catch (err) { await t.rollback(); res.status(500).json({ code: 1, message: '系统繁忙,请稍后重试' }); } }

这里的关键是where: { id: seatId, status: 'IDLE' }这个条件。两条并发请求同时到达时,数据库行锁会保证只有一条更新成功,更新影响行数为0的那条就知道自己抢失败了。UPDATE语句在InnoDB引擎下本身就是行级锁,天然具备并发安全能力,比先查再改的“SELECT后UPDATE”方案可靠得多。

4.3 签到入座与时长计费计算

预约锁定之后,用户到店签到,座位状态从“锁定”变成“使用中”,订单记录开始时间。然后离场时结算,计算时长和金额。

计费规则是这套系统的核心之一。我给自习室设计的规则为:按分钟计费,普通区0.15元/分钟,沉浸区0.2元/分钟,卡座0.33元/分钟;同时只保留最简复杂度,暂时不做峰谷差价。计价函数抽成独立工具模块:

// utils/price.js const priceMap = { normal: 15, // 分/分钟 immersive: 20, booth: 33 }; function calcPrice(seatType, startTime, endTime) { const minutes = Math.ceil((endTime - startTime) / 60000); const amountInFen = minutes * priceMap[seatType]; return { minutes, amountInFen }; }

这里有个业务细节容易忽略:不满一分钟按一分钟计费。顾客坐了1小时0分1秒,应该收61分钟的钱,而不是60分钟。计费口径必须前后端完全一致,否则用户看到的金额和后台算出来的不一样,迟早要吵起来。

结算接口的逻辑是:找到使用中的订单 → 计算时长金额 → 从用户余额扣款 → 座位改回空闲。扣款前需要检查余额是否足够,不足则提示用户充值后再结算。整个流程同样放在事务里,避免“扣了款但座位没释放”或者“座位释放了但钱没扣到”这种半完成状态。

4.4 超时自动释放的定时任务

预约锁定后15分钟未签到,座位要自动释放。这个功能不需要实时监听,用Node.js的定时任务每5分钟扫一次就够了:

const cron = require('node-cron'); cron.schedule('*/5 * * * *', async () => { const expireTime = Date.now() - 15 * 60 * 1000; const expiredOrders = await Order.findAll({ where: { status: 'LOCKED', createdAt: { [Op.lt]: new Date(expireTime) } } }); for (const order of expiredOrders) { await Seat.update({ status: 'IDLE' }, { where: { id: order.seatId } }); await order.update({ status: 'CANCELLED' }); } });

一个小提醒:这种定时任务在单机部署下没问题,但如果后端做了多实例部署,定时任务会重复执行,需要借助Redis分布式锁或者把任务单独拆成服务。自习室管理系统一般是单实例,这个坑先知道就行,真到多实例那一步再处理。

5. Vue前端从预约页到管理端的落地过程

5.1 路由设计与页面规划

前端路由我规划成一张清晰的路由表,匹配用户端和管理端两类角色。登录用户能访问哪些页面,通过前端路由守卫控制:

const routes = [ { path: '/', component: HomePage }, { path: '/seats', component: SeatMapPage }, // 座位图 { path: '/order/:id', component: OrderDetailPage }, // 订单详情 { path: '/login', component: LoginPage }, { path: '/admin', component: AdminLayout, meta: { role: 'admin' }, children: [ { path: 'seats', component: AdminSeatPage }, { path: 'orders', component: AdminOrderPage }, { path: 'users', component: AdminUserPage } ] } ];

这里/order/:id就用到了路由参数。用户从座位图点击某个已预约的座位,跳转到订单详情页,组件内通过route.params.id拿到订单号,再请求后端查详情。动态路由的价值在于用同一个组件模板适配不同ID的数据展示,不用为每个订单单独写页面。

如果用户是管理员,管理端页面可以通过动态路由按角色加载。比如普通用户访问/admin时路由守卫直接拦截跳转首页,只有role === 'admin'的用户才在路由表中注入管理端菜单。Vue Router的beforeEach守卫在这里充当门卫角色,这也是面试里最高频考察的点之一。

5.2 座位图组件:状态渲染与交互

座位图是整个前端最直观的页面。我用一个二维数组模拟座位的物理布局,每个座位是一个卡片,背景色根据状态切换:

  • 绿色表示空闲,点击弹出预约弹窗
  • 灰色表示座位被他人锁定
  • 蓝色表示使用中,展示已用时长
  • 红色表示维护中,禁止点击

组件模板大致这样写:

<template> <div class="seat-grid"> <div v-for="seat in seats" :key="seat.id" :class="['seat-card', seat.status]" @click="handleSeatClick(seat)"> <span class="seat-no">{{ seat.seatNo }}</span> <span v-if="seat.status === 'OCCUPIED'" class="seat-time"> {{ formatDuration(seat.usedMinutes) }} </span> </div> </div> </template>

状态渲染本身不难,难点在于数据实时性。顾客坐下后,座位图不能一直停留在旧状态。我的方案是前端每30秒轮询一次可用座位接口,把返回的数据刷新到组件里。轮询间隔太短浪费资源,太长显示滞后,30秒对自习室场景体感刚刚好。如果以后要做更实时的体验,可以升级成WebSocket推送,架构上需要后端维护连接状态,复杂度会高一个量级,自习室这类场景我认为轮询已经够用。

5.3 Axios封装与Token注入

所有前端请求都走同一个Axios实例,统一配置BaseURL、超时时间和拦截器。拦截器是这里最重要的封装点:

import axios from 'axios'; const service = axios.create({ baseURL: '/api', // 开发环境走Vite代理,生产环境由Nginx转发 timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); } );

baseURL统一设为/api而不是写死IP地址,目的是让开发环境和生产环境的API地址可以无缝切换。开发时Vite把请求代理到后端,生产时Nginx把/api前缀转发到Node服务,前端代码本身完全不用改。这个细节看起来不起眼,但能帮你在部署阶段少踩一个大坑。

5.4 自定义状态管理:什么时候不需要Pinia

很多教程一上来就是Pinia管理所有状态,但自习室这个项目实际用到的全局状态非常少:登录用户的ID、昵称、余额,一个路由守卫要用,一个顶栏要显示。这种轻量场景完全没必要上重量级状态库,存localStorage就够了。

余额变化算是唯一需要“跨页面通知”的状态:用户在结算页充值成功了,顶栏的余额要立刻更新。我用的方案是封装一个useUser组合式函数,内部维护一个响应式的userInfo对象,任何组件引用这个对象,修改后所有组件自动刷新:

// composables/useUser.js import { ref } from 'vue'; const userInfo = ref(JSON.parse(localStorage.getItem('userInfo')) || null); export function useUser() { function setUser(data) { userInfo.value = data; localStorage.setItem('userInfo', JSON.stringify(data)); } return { userInfo, setUser }; }

这套方案的优点是依赖极少、逻辑直白,组件之间数据同步靠Vue的响应式系统自动完成。等到项目里出现购物车、多角色复杂权限这类真正需要全局管理的状态,再引入Pinia也不迟。一开始就上重方案,只会让新人把大量精力浪费在理解状态库上,反而偏离了业务本身。

6. 联调、部署和这轮开发踩过的坑

6.1 开发期跨域问题:Vite代理配置

前后端分离初期最让人懵的是跨域问题。前端跑在http://localhost:5173,后端跑在http://localhost:3000,浏览器会拦截跨源请求,报一串CORS错误。解决方式不是在后端粗暴开CORS,而是让开发服务器做代理,把/api开头的请求转发到后端。在Vite配置文件里加一段:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } })

然后前端的请求地址统一走/api/xxx,浏览器看到的是同源请求,自然不会拦截。到了生产环境,前端构建出来的静态文件放在Nginx里,同样把/api转发到Node服务,思路和开发代理完全一致,只是换了一层实现。用代理方案还有个好处:后端地址如果变了,只改代理配置,不用改前端每一处请求地址。

6.2 生产部署:Nginx的history路由回退

部署阶段最容易翻车的坑是Vue Router的history模式。开发时一切正常,部署到服务器后直接刷新某个子页面,比如/admin/orders,Nginx返回404。原因很简单:浏览器直接请求这个路径时,Nginx去磁盘上找/admin/orders这个文件,根本不存在。

解决办法是Nginx配置一个回退规则,把所有找不到的路径都指向index.html,让前端路由接管解析:

server { listen 80; server_name your-domain.com; root /var/www/seat-studio/dist; location / { 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 $uri $uri/ /index.html这行就是答案:先找对应静态文件,找不到就统一交给index.html。同时/api的请求反代到Node.js服务,完美完成前后端衔接。这个配置我在多个项目里用过,稳定得一批。

关于Node.js服务的进程管理,配合pm2使用最省心。npm install -g pm2安装后,用pm2 start app.js --name seat-server启动,它能自动重启崩溃的进程,开机自启配置也简单,比我早期用nohup裸跑靠谱太多。

6.3 金额与时间精度的两个隐形坑

一个是金额,上面提到过用分存储,这里补充一点前端展示细节:后端返回的金额单位是分,前端必须统一除以100再展示成元,并且用Math.round四舍五入到两位小数,避免出现1.005这种浮点展示问题。我在这栽过跟头,后台订单统计某天突然多了几分钱的误差,查了半天发现是浮点运算累加导致的精度差。

另一个是时间。Node.js和MySQL之间传递时间,默认走UTC格式,如果前后端直接用字符串比较,很容易出现“提前8小时”或者“晚了8小时”的诡异Bug。我的统一规范是:后端所有时间字段用时间戳存储,前端只负责把时间戳格式化成可读文本。这样彻底绕开时区换算的坑。实际开发中,dayjs这个库处理格式化非常方便,一行代码解决:

dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')

6.4 并发扣款:余额够不够,不能只靠查

用户结算时,前端显示余额100元,后端一查用户账户确实还有100元,正要扣费却发现另一个请求已经把余额花掉了一半。这还是典型的并发覆盖问题。正确的扣款方式是用一条条件更新语句,把“查询余额再扣减”合并成一步原子操作:

const [updated] = await User.update( { balance: Sequelize.literal(`balance - ${amountInFen}`) }, { where: { id: userId, balance: { [Op.gte]: amountInFen } }, transaction: t } );

balance >= amountInFen作为where条件,数据库行锁会保证只有一个扣款请求能成功。影响行数为0说明余额不足,这时再提示用户充值。这套逻辑和座位锁座是同一个思想:永远不要先查再改,而是把判断条件和修改动作合并进一条带约束的SQL。这是并发编程里最需要内化的思维。

6.5 想做桌面端?Electron打包Vue项目的延伸

项目上线后老板又提了个需求:想在自己电脑上装一个桌面版,不依赖浏览器。这个场景正好可以用Electron。热搜里“electron打包vue项目”是高频词,实际操作思路不复杂:用electron-vite构建,把Vue项目打包出的dist目录加载进Electron的窗口,再通过主进程和渲染进程的IPC通信处理窗口大小、托盘退出这类系统级操作。

这里有个容易误解的点:Electron的主进程是Node.js环境,渲染进程是浏览器环境,两者不能直接共享变量,必须通过ipcMain和ipcRenderer做消息通信。比如点击关闭按钮时,渲染进程发一条IPC_CLOSE_WINDOW消息,主进程收到后调用win.close()。理解这个通信机制,Electron的开发就算掌握了八成。

不过我的建议是桌面端在这套系统里优先级放低。自习室业务以手机访问为主,学生到店扫码,桌面管理后台用浏览器完全够用。Electron版更像是一个锦上添花的东西,等核心业务流程稳定后再考虑也不迟。

6.6 顺带的面试知识:路由守卫、自定义v-model与组件通信

做完这个项目,其实把Vue面试里很多高频考点自然带出来了。比如路由守卫,我前面用它做登录拦截和角色权限控制,面试官问“路由守卫有哪些”时,你可以直接拿你这个项目讲:全局前置守卫beforeEach每次都检查token,不存在就跳登录页,存在且目标路径需要管理员权限时就校验角色字段。这样答比背文档生动得多。

再比如自定义v-model,如果你封装过一个座位选择组件,需要在父组件里双向绑定选中的座位编号,最简单的落地方式就是定义一个modelValue的prop,emit一个update:modelValue事件,这就是Vue官方定义的v-model机制。亲手写过一遍,和只看文档理解深度完全不同。

还有组件通信,父子用props和emit,跨层级用provide/inject,全局共享用前文提到的组合式函数,这三层关系梳理清楚,大部分Vue面试题都能从容应对。建议做完项目顺手把这些知识点整理成自己的语言,面试的时候直接拿项目当例子,说服力比背八股文强得多。


这次项目做下来,我最大的体会是:业务状态设计比写代码更花心思。座位状态机、订单状态流转、并发防重这些逻辑一旦理清,后面的编码只是体力活;反过来如果状态没设计好,代码写一半就会开始到处打补丁。自习室老板不需要你炫技,他要的是少出乱子、好对账,所以我把重心始终放在可靠性和数据一致性上。

最后分享一个小技巧:把计费金额的计算抽成独立函数后,我给它写了几个边界用例——不足一分钟按一分钟计、跨午夜时段计算、余额刚好够扣一分不剩,每次改价格策略先跑一遍测试用例再上线。这个小习惯救了我好几次,推荐你也在自己的项目里试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 12:00:11

Docker网络配置详解:从端口映射到跨主机通信实践

Docker——网络配置 在 Docker 的日常使用里&#xff0c;网络配置可能是最让人摸不着头脑、却又绕不开的一环。镜像拉不下来、容器互相访问失败、端口映射不生效、跨主机通信断了——几乎每一个“奇怪”的故障&#xff0c;到最后都能追溯到网络配置层面的某个细节。 这篇文章…

作者头像 李华
网站建设 2026/10/13 11:59:44

OpenCV Mat图像用CDC显示:内存布局、BITMAPINFO与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 11:58:50

Spring Boot 整合 Thymeleaf 实战:模板选型、核心语法与排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 11:58:24

Spring @Lazy注解深度解析:从源码到实战,避开延迟初始化的那些坑

1. 一个所有Spring开发者都容易用错的注解&#xff1a;Lazy的诊断笔记有次在内部技术群里看到一位同事贴了一段代码&#xff1a;他在某个服务类上加了Lazy注解&#xff0c;自信地以为这个Bean会在第一次用到时才创建。结果项目启动日志里明明白白地打印了该Bean的构造方法执行记…

作者头像 李华
网站建设 2026/10/13 11:58:03

分层图最短路与0-1BFS:钥匙锁门类路径规划算法解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 11:56:03

OpenClaw实战:AI浏览器智能体如何替代传统爬虫与RPA

看到标题里的“龙虾”点进来的朋友&#xff0c;估计都对 OpenClaw 有点耳闻。圈里习惯管它叫“龙虾”&#xff0c;倒不是因为它跟海鲜有什么关系&#xff0c;而是这个项目的标志本来长得就像一对钳子&#xff0c;作者的解释也很直接&#xff1a;Claw 就是“钳子”&#xff0c;加…

作者头像 李华