做全栈项目这么多年我越来越觉得“外卖订餐”这类业务是最适合练手的实战场景用户端要处理商品浏览、购物车、下单支付、订单状态跟踪商家端要管理菜品、接单、出餐配送端要处理接单、取餐、送达。一个系统把三方角色全部串起来几乎覆盖了 Web 后端和小程序前端的全部核心知识点。最近我就带着这套 Vue 3 Node.js 微信小程序的网上订餐配送系统仓库标识 71ypffik完整跑了一遍从零到上线的流程今天把整个思路和踩坑记录整理出来想入门全栈或者准备做毕业设计、接私活的朋友都可以直接参考。这套系统的核心链路很简单用户在微信小程序里浏览附近餐厅、选菜加购物车、提交订单并支付商家在管理后台接单备餐配送员接单后按照订单地址完成配送用户实时看到订单状态从“已支付”到“配送中”再到“已完成”的变化。技术栈上用 Vue 3 承担后台管理页面和部分 H5 页面用 Node.js Express 搭建 RESTful API 服务小程序端用原生微信小程序框架开发数据库选 MySQL 存储业务数据。我之所以推荐这个组合是因为它足够“接地气”Vue 3 是目前前端招聘和项目里最主流的技术之一Node.js 让前端开发者不需要换语言就能搞定后端小程序则是国内餐饮业务的核心流量入口。三个技术点单独拎出来都不算新鲜但把它们串成一个完整系统时你就会遇到跨域、鉴权、状态同步、支付回调、消息推送这些真正值钱的问题。1. 项目整体设计与技术方案选型动工之前我最关心的不是代码怎么写而是这套系统到底要拆成几个端、每个端用什么技术、它们之间怎么通信。很多新手一上来就急着写接口结果做到一半发现角色权限混乱、端与端之间数据对不上返工成本极高。1.1 为什么选 Vue 3 Node.js 小程序这个组合先说选型逻辑。Vue 3 相比 Vue 2 最大的变化是 Composition API 和响应式系统的重写用 setup 语法组织业务代码比 Options API 更清爽特别是订单列表、购物车这种状态复杂的页面逻辑复用性明显提升。Node.js 这边我选择 Express 而不是 Koa 或 NestJS原因是 Express 生态最成熟、中间件机制直观、学习曲线平缓对于订餐系统这种 CRUD 密集的业务完全够用不需要引入重型框架增加心智负担。小程序端为什么不用 uni-app 或 Taro 而用原生因为原生小程序语法虽然写起来啰嗦但是调试最直接、性能最好、对微信 API 的封装最少你能清楚知道每个接口的调用成本。对于想彻底搞懂小程序运行机制的人来说从原生入手是最好的选择。后面如果要做多端发布再重构成 uni-app 也不迟业务逻辑层的代码是可以复用的。这套系统最终拆成三个子工程小程序端用户点餐、下单、支付、订单跟踪、个人中心管理后台Vue 3 Element Plus商家菜品管理、订单处理、数据统计后端服务Node.js Express MySQL统一提供 RESTful API三个工程放在同一个仓库里用 monorepo 管理虽然部署时要分别处理但开发时共享类型定义和工具函数非常方便。目录结构大致是ordering-system/ ├── server/ # Node.js 后端 │ ├── src/ │ │ ├── routes/ # 路由层 │ │ ├── controllers/ # 控制器 │ │ ├── services/ # 业务逻辑层 │ │ ├── models/ # 数据模型 │ │ ├── middlewares/ # 中间件鉴权、日志、错误处理 │ │ └── utils/ # 工具函数 │ └── app.js ├── admin/ # Vue 3 管理后台 │ ├── src/ │ │ ├── views/ # 页面组件 │ │ ├── router/ # 路由配置 │ │ ├── stores/ # Pinia 状态管理 │ │ └── api/ # 接口请求封装 ├── miniprogram/ # 微信小程序 │ ├── pages/ │ ├── components/ │ └── utils/ └── docs/ # 项目文档1.2 系统角色划分与核心业务流程订餐系统有四个核心角色用户、商家、配送员、管理员。注意这里不是简单地做四套登录而是要在同一套用户体系里用角色字段区分权限。我用users表存所有账号用role字段区分customer、merchant、delivery和admin后端通过中间件对每个接口做角色校验。核心流程我用一句话概括用户下单 - 系统生成订单并扣除库存 - 商家接单 - 系统分配配送员 - 配送员取餐送达 - 用户确认收货。整个流程涉及三个端的实时状态同步所以订单状态字段 Design 得特别小心。我定义了一套订单状态机每个状态之间的转换都有明确触发条件状态值含义触发条件下一状态0待支付用户提交订单11已支付待接单用户完成支付2 或 32商家已接单商家点击接单33配送中配送员确认取餐44已完成用户确认收货或系统自动确认55已取消用户取消/超时未支付终态6售后中用户发起售后4 或 6这个状态机是整个项目的“骨架”数据库表设计、前端页面渲染、后端接口逻辑全部围绕它展开。我最开始犯的错误是只用一个status字段存数字没有记录状态变更时间导致后面做订单超时提醒和配送时效统计时抓瞎。后来加了order_status_logs表每次状态变化都插一条记录排查问题时清晰多了。2. 数据库设计与核心业务模型数据库设计决定了这个项目能走多远。订餐系统的表不算复杂但表和表之间的关系比较绕尤其是菜品、规格、购物车、订单、订单明细这几张表之间的关联设计不好后面写 SQL 会非常痛苦。2.1 用户、餐厅与菜品的表结构设计用户表简单核心字段是openid微信唯一标识、nickname、avatar_url、phone、role、status。这里有个细节小程序端登录拿到的openid应该是用户表的唯一索引而不是用自增id做业务关联因为所有小程序接口都靠openid识别用户身份。餐厅表restaurants要包含名称、logo、评分、月销量、起送价、配送费、营业状态、经纬度、公告。经纬度字段我踩过坑如果直接用FLOAT存储做附近餐厅查询时精度完全不够计算距离会出现几百米的偏差。正确做法是用DECIMAL(10, 7)存储经纬度配合 Haversine 公式做距离计算。菜品表dishes是核心中的核心我设计的字段包括CREATE TABLE dishes ( id INT PRIMARY KEY AUTO_INCREMENT, restaurant_id INT NOT NULL, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, image_url VARCHAR(255), price DECIMAL(10, 2) NOT NULL, original_price DECIMAL(10, 2), stock INT DEFAULT 0, sales INT DEFAULT 0, status TINYINT DEFAULT 1, -- 1上架 0下架 sort_order INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );菜品规格是个容易被忽略的点。比如一杯奶茶有“中杯/大杯”“正常冰/少冰/去冰”这些东西不能直接塞进菜品表里。我单独建了dish_specs表用 JSON 字段存储规格组和规格项{ groups: [ { name: 杯型, items: [{ name: 中杯, price_delta: 0 }, { name: 大杯, price_delta: 4 }] }, { name: 温度, items: [{ name: 正常冰 }, { name: 去冰 }] } ] }订单明细表里每次下单都冗余一份规格快照防止商家后面修改规格导致历史订单数据错乱。2.2 购物车、订单与配送单的核心表设计购物车表设计比较反直觉我一开始用carts表存用户的购物车发现高并发时频繁读写这张表性能很差而且每次加购都要先查询再更新事务控制很麻烦。后来改成用 Redis 存购物车以user_id为 keyvalue 直接存菜品列表 JSON性能提升非常明显。小程序端每次进入购物车页面直接从缓存读秒开。当然Redis 方案要处理好持久化问题纯内存方案服务重启就丢了好在购物车本身不是关键数据丢了用户重新加一遍就行可接受的成本很低。订单主表orders负责记录订单的全局信息CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, merchant_id INT NOT NULL, delivery_man_id INT DEFAULT NULL, total_amount DECIMAL(10, 2) NOT NULL, delivery_fee DECIMAL(10, 2) DEFAULT 0, discount_amount DECIMAL(10, 2) DEFAULT 0, pay_amount DECIMAL(10, 2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, address_snapshot TEXT NOT NULL, remark VARCHAR(255), payment_time DATETIME, accept_time DATETIME, delivery_time DATETIME, finish_time DATETIME, cancel_time DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );订单明细表order_items存的是下单那一刻的菜品快照虽然菜品表里已经有价格了但订单明细必须冗余菜名、价格、图片、规格等字段。理由很简单商家改价、下架、删除菜品后历史订单依然要能完整展示如果关联查询菜品表数据早就变了。配送单我选择不单独建表而是把配送信息挂在订单表上。因为每笔订单最多只有一个配送员一对一关系不需要额外扩展。真正复杂的配送调度逻辑多订单合并配送、路径规划是后期优化方向第一版先把状态字段和位置上报接口做好就够了。3. 后端 Node.js 服务端实现后端是整个系统的心脏所有端的数据操作都要经过这里。我在实践中的体会是后端代码的目录结构比代码本身更重要因为项目规模一大如果 controller 里混着业务逻辑和 SQL 查询改一个功能要翻几百行代码心态直接崩。3.1 Express 框架搭建与中间件配置我用 Express 5写本文时已稳定搭建服务入口文件app.js大概长这样const express require(express); const cors require(cors); const helmet require(helmet); const morgan require(morgan); const { rateLimit } require(express-rate-limit); const app express(); app.use(helmet()); app.use(cors({ origin: [https://admin.example.com, https://api.example.com], credentials: true })); app.use(express.json({ limit: 10mb })); app.use(morgan(combined)); // 接口限流防止刷单和恶意请求 const limiter rateLimit({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, }); app.use(/api, limiter); // 路由挂载 app.use(/api/auth, require(./routes/auth)); app.use(/api/restaurants, require(./routes/restaurants)); app.use(/api/orders, require(./routes/orders)); app.use(/api/dishes, require(./routes/dishes)); app.use(/api/delivery, require(./routes/delivery)); // 统一错误处理中间件 app.use((err, req, res, next) { console.error(err.stack); res.status(err.status || 500).json({ code: err.code || INTERNAL_ERROR, message: err.message || 服务器内部错误 }); }); app.listen(3000, () { console.log(Server running on port 3000); });中间件顺序是有讲究的helmet要放在最外层处理安全头cors必须在路由之前express.json()必须放在所有需要解析 body 的路由之前错误处理中间件必须放在所有路由之后、listen之前。顺序错了轻则功能异常重则安全漏洞。3.2 用户登录鉴权与微信小程序登录流程小程序登录流程比较特殊用户在小程序端调用wx.login()拿到临时code后端拿着code请求微信接口换取openid和session_key。这里有两个关键点第一wx.login获取的code只能用一次且有效期只有五分钟所以后端接口要设计成幂等的同一个code重复请求要报错而不是创建多个用户。第二session_key是敏感信息不能下发到小程序端也不能写进日志。后端应该用openid作为用户身份标识自己签发 JWT 给小程序端后续请求都带 JWT 而不是session_key。我实现的登录接口核心逻辑const axios require(axios); const jwt require(jsonwebtoken); async function wxLogin(code) { const appid process.env.WX_APPID; const secret process.env.WX_SECRET; const url https://api.weixin.qq.com/sns/jscode2session?appid${appid}secret${secret}js_code${code}grant_typeauthorization_code; const { data } await axios.get(url); if (data.errcode) { throw new Error(微信登录失败: ${data.errmsg}); } let user await User.findOne({ where: { openid: data.openid } }); if (!user) { user await User.create({ openid: data.openid, nickname: 微信用户, role: customer }); } const token jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: 7d } ); return { token, user }; }鉴权中间件我封装了一个authGuard在需要登录的接口上直接使用。注意这里要区分“是否需要登录”和“是否需要特定角色”两个问题我用两个中间件分别处理authGuard只负责解析 JWT、塞用户信息到req.userrequireRole(merchant)负责检查角色权限。这样的设计在写业务接口时特别灵活。3.3 核心 API 接口设计与下单流程实现整个系统里我设计了大约三十个接口按模块划分认证 3 个、餐厅 5 个、菜品 6 个、购物车 3 个、订单 8 个、配送 5 个。这里列出几个核心接口的请求响应设计接口方法入参返回/api/auth/wx-loginPOST{ code }{ token, userInfo }/api/restaurants/nearbyGET{ latitude, longitude, page }{ list, total }/api/ordersPOST{ items, address, remark }{ orderNo, payParams }/api/orders/:idGET-{ orderDetail }/api/orders/:id/cancelPOST-{ success }/api/delivery/acceptPOST{ orderId }{ success }下单接口是整个后端最复杂的一个因为涉及事务。用户提交订单后后端要依次做校验菜品状态和库存、计算价格、创建订单主表记录、批量写入订单明细、扣减菜品库存、清空购物车。任何一步失败都要回滚不能让用户的钱付了但订单没生成。我用 Sequelize 的transaction实现const transaction await sequelize.transaction(); try { // 1. 校验菜品 const dishes await Dish.findAll({ where: { id: items.map(i i.dishId) }, transaction }); // 2. 计算价格 let total 0; for (const item of items) { const dish dishes.find(d d.id item.dishId); if (!dish || dish.status ! 1) { throw new Error(菜品 ${item.dishId} 不存在或已下架); } if (dish.stock item.quantity) { throw new Error(菜品 ${dish.name} 库存不足); } total dish.price * item.quantity; } // 3. 创建订单 const order await Order.create({ orderNo: generateOrderNo(), userId: req.user.id, merchantId: merchantId, totalAmount: total, payAmount: total, status: 0, addressSnapshot: JSON.stringify(address), }, { transaction }); // 4. 写入明细 await OrderItem.bulkCreate(orderItems, { transaction }); // 5. 扣库存 for (const item of items) { await Dish.decrement(stock, { by: item.quantity, where: { id: item.dishId }, transaction }); } await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; }这里有一个重要的性能问题高并发下单时Dish.decrement是并发安全的因为它是原子操作。但是“先查库存再扣减”这个组合会有超卖风险两个请求同时查到库存剩 1然后同时下单最后库存变成 -1。解决办法是把“查库存”和“扣库存”合并成一个原子操作用UPDATE dishes SET stock stock - ? WHERE id ? AND stock ?受影响行数为 1 才算扣成功。4. 小程序前端核心页面与关键逻辑小程序端的用户体验直接决定系统的成败。外卖用户没有耐心等页面加载也没有耐心研究复杂的操作流程每一个多余步骤都会导致订单流失。我在开发小程序端时最关注的就是页面加载速度和交互路径最短化。4.1 小程序项目结构搭建与分包加载微信小程序有主包和分包的概念主包体积限制 2MB分包总计不超过 20MB。订餐系统页面不多但我还是用了分包因为首页、点餐页、订单列表这些核心页面放主包个人中心、售后、关于等低频页面放分包能显著提升首屏加载速度。我的小程序页面结构miniprogram/ ├── pages/ │ ├── index/ # 首页餐厅列表/附近餐厅 │ ├── restaurant/ # 餐厅详情菜品分类、菜品列表、加购物车 │ ├── cart/ # 购物车 │ ├── checkout/ # 确认订单 │ ├── order-list/ # 订单列表 │ ├── order-detail/ # 订单详情含配送状态地图 │ └── mine/ # 个人中心 ├── components/ │ ├── dish-card/ # 菜品卡片 │ ├── stepper/ # 数量加减器 │ ├── spec-selector/ # 规格选择弹窗 │ └── address-picker/ # 地址选择器 ├── utils/ │ ├── request.js # 请求封装 │ ├── auth.js # 登录管理 │ └── cart.js # 购物车本地缓存首页的餐厅列表我用了小程序的onPullDownRefresh做下拉刷新配合onReachBottom做上拉加载更多。这里有个性能优化点首次加载时只请求第一页的 10 家餐厅滚动到底部再请求下一页避免一次性渲染大量图片导致页面卡顿。4.2 点餐页的规格选择和购物车交互餐厅详情页是小程序端最复杂的页面左边是菜品分类列表右边是菜品列表点击菜品弹出规格选择弹窗确定后加入购物车。这个页面的交互有非常多的细节规格弹窗用position: fixed的底部弹层实现遮罩层点击关闭。弹窗里需要展示规格组的选项用户每选择一个规格项价格实时更新。这里有个坑小程序原生组件的picker模式不支持这种多规格组合选择所以我用自定义组件实现。购物车的核心是本地缓存 后端同步双轨制。用户加购时先更新本地缓存购物车角标立即 1让用户有即时反馈退出餐厅页或进入结算页时才把购物车数据同步到服务端。这个策略避免了频繁的网络请求同时保证用户体验。需要注意同步时机不能放在onUnload因为小程序页面销毁时网络请求可能还没发出去我实测在结算页点击“去结算”时同步最稳妥。步骤条式的购物车交互其实很值得研究底部固定一个购物车栏显示总价和“去结算”按钮总价随加购实时计算。点击购物车栏弹出半屏购物车列表支持修改数量、删除菜品。这个交互在美团、饿了么上已经很成熟我照着他们的模式实现了一遍用户零学习成本。4.3 订单状态轮询与配送地图展示用户下单后最关心的就是“我的外卖到哪了”。我采用轮询方案订单详情页进入后开启定时器每 3 秒请求一次订单详情接口获取最新状态状态变化时更新 UI。页面隐藏或卸载时清除定时器避免无效请求。// 订单详情页轮询 onShow() { this.startPolling(); }, onHide() { this.stopPolling(); }, startPolling() { this.pollingTimer setInterval(() { this.fetchOrderDetail(); }, 3000); }, stopPolling() { if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer null; } }轮询的缺点是无法做到实时推送但对于订餐系统完全够用。如果要做实时性更强的商家接单提醒、骑手位置更新应该接入 WebSocket 或小程序自带的wx.connectSocket但那样要做心跳检测、断线重连和消息协议设计复杂度会上升一个级别。第一版用轮询是最务实的做法。配送地图用的是腾讯地图微信小程序 SDK把配送员的经纬度实时标在地图上。这里有个权限问题用户必须授权地理位置否则无法展示配送轨迹。要注意的是wx.getLocation接口在用户拒绝授权后不能重复弹窗必须引导用户到设置页手动开启。5. 配送环节与订单状态流转实现配送是整个系统里最容易出问题的环节因为涉及真实世界的物理移动和不确定因素。我花了不少时间设计配送相关的逻辑确保系统在异常情况下也能兜底。5.1 配送员的接单与配送流程设计配送员端的核心页面是“可接单列表”和“我的配送任务”。可接单列表展示附近待配送订单配送员点击“接单”后订单状态从“商家已接单”变为“配送中”同时订单绑定配送员 ID。这里有个竞态条件问题多个配送员同时点击同一个订单的“接单”按钮只有一个能成功。我用的方案是条件更新const result await Order.update( { deliveryManId: req.user.id, status: 3, deliveryTime: now }, { where: { id: orderId, status: 2, // 只有当前状态是 2 才允许更新 deliveryManId: null } } ); if (result[0] 0) { throw new Error(订单已被其他配送员接走); }如果更新影响的行数为 0说明订单已经被别人抢走了直接提示用户即可。这个方案比“先查询再更新”安全得多不需要加锁也不会有超卖问题。配送状态流转设计为商家出餐后点击“通知配送员”订单置为“待配送”配送员接单后调用“确认取餐”接口订单状态变为“配送中”到达用户地址后点击“确认送达”订单状态变为“已完成”。每一步都有时间戳记录方便后续做配送时效分析。5.2 用户取消订单与超时自动取消用户可能因为各种原因取消订单但取消的规则必须和订单状态强绑定待支付状态状态 0用户可随时取消不扣费已支付待接单状态 1用户可取消但要走“退款申请”流程商家确认后退款商家已接单状态 2不允许用户直接取消需要联系商家协调配送中状态 3不允许取消我实现了一个后台定时任务每 5 分钟扫描一次待支付订单超过 15 分钟未支付的自动置为已取消。注意这里不能用“用户发起支付时就判断超时”因为微信支付的回调可能延迟很久定时任务才是可靠的兜底手段。6. 常见问题与排查技巧实录整个项目做完我从环境搭建到联调上线踩了不少坑。下面这些问题是读者反馈和我在实践中遇到最多的整理成速查表希望能帮大家少走弯路。6.1 环境配置与依赖装不上怎么处理最典型的问题是安装 Node.js 后运行 npm 命令报错npm : 无法加载文件 ... npm.ps1因为在此系统上禁止运行脚本。这是 Windows PowerShell 的脚本执行策略限制导致的解决办法是用管理员身份打开 PowerShell执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned这个命令允许本机运行的脚本但依然阻止远程下载的未签名脚本是相对安全的折中方案。如果公司电脑有安全策略限制不能修改也可以改用 CMD 或 Git Bash 运行 npm 命令完全不受这个限制。另外国内用户直接 npm install 经常因为网络问题装不上依赖我的建议是配置淘宝镜像源npm config set registry https://registry.npmmirror.com如果还是装不上优先检查 Node.js 版本是否过旧。Vue 3 要求 Node.js 16.0我一开始用 Node 14 跑了半天各种依赖版本冲突换到 Node 18 LTS 后一次通过。6.2 接口跨域、端口占用与联调问题小程序端请求后端接口不涉及浏览器跨域问题因为小程序的请求是在微信客户端内部发起的不受浏览器同源策略限制。但 Vue 3 管理后台跑在浏览器里跨域是必然问题。我的处理方式是在后端统一开启 CORS而不是在前端配置代理因为生产环境前端是静态资源服务器代理不生效。端口占用也是高频问题启动 Node 服务时报EADDRINUSE说明 3000 端口被占了。排查方法netstat -ano | findstr :3000 taskkill /PID 进程号 /F另外管理后台开发时我用 Vite 的 proxy 配置转发/api到本地 3000 端口这样前端代码里请求地址统一写/api/xxx上线后由 Nginx 统一转发代码不用改。6.3 小程序真机调试与上线审核注意事项小程序开发工具里跑好好的真机一测就各种问题这是常见现象。最典型的几个开发版小程序过期需要在开发者工具重新扫码。这是因为测试号的 session 过期了重新编译上传即可。真机无法请求本地后端接口。手机访问http://localhost:3000指向的是手机自己不是电脑。解决办法是在微信开发者工具里勾选“不校验合法域名”并且把后端接口地址改成电脑的局域网 IP手机和电脑连同一个 Wi-Fi。正式上线必须配置 HTTPS 域名而且域名要备案。我建议开发阶段就把 API 地址抽成配置文件上线时全局替换成正式域名。小程序审核有一个容易忽略的坑涉及餐饮业务必须有《食品经营许可证》等资质文件否则提审会被拒。个人主体的小程序不能做这类交易类目需要企业主体。如果只是为了学习和演示可以用测试号绕过这个限制但正式发布就必须合规。6.4 前后端联调时的调试技巧联调阶段最容易出现的问题不是代码逻辑错误而是数据不一致。比如前端传的字段名和后端接口定义的不一样或者后端返回的数据结构前端没按预期解析。我的习惯是前端所有网络请求统一封装到一个request.js文件里所有接口函数集中管理后端接口文档用 Apifox 维护每次改接口先更新文档再改代码。联调时打开浏览器开发者工具的 Network 面板和后端的morgan日志两边对照着看请求和响应问题基本几分钟就能定位。另外后端接口返回格式必须统一。我的约定是成功返回{ code: 0, message: success, data: {} }失败返回{ code: 40001, message: 订单不存在, data: null }前端在request.js里拦截统一 codecode ! 0时弹出 toast 提示不用每个业务接口单独处理错误分支。7. 项目部署与上线经验开发全部完成后我踩过的部署坑比开发时还多。这里挑几个典型的分享一下部署方案不一定适合所有人但思路可以借鉴。7.1 前后端分离部署的基本思路订餐系统的部署拓扑大概是一台云服务器2核 4G 就够用上面跑 Nginx、Node.js 服务和 MySQL 数据库。小程序静态资源不用部署因为微信平台会托管Vue 3 管理后台打包成静态文件由 Nginx 直接托管。Nginx 配置核心片段server { listen 80; server_name api.example.com; location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name admin.example.com; root /var/www/admin-dist; index index.html; location / { try_files $uri $uri/ /index.html; } }生产环境建议用 PM2 管理 Node.js 进程npm install -g pm2 pm2 start app.js --name ordering-server pm2 save pm2 startup用 PM2 的好处是进程崩溃自动重启、日志统一管理、开机自启动配置方便比自己写守护脚本靠谱得多。7.2 数据库备份与安全加固订餐系统涉及真实交易数据备份是必须做的。我写了每天凌晨自动导出 SQL 的定时任务mysqldump -u root -p ordering_system /backup/ordering_$(date %Y%m%d).sql保留最近 30 天的备份用 crontab 定时清理旧的备份文件避免磁盘打满。安全方面有几个基础操作MySQL 不要用 root 连业务库单独建账号只授权业务库权限Node.js 服务不要用 root 用户运行环境变量里不要把密钥写死在代码里用.env文件管理并加入.gitignore生产环境开启 HTTPS可以用免费的证书服务也可以买云服务商提供的证书配置到 Nginx 上即可。跑完一整个项目我个人最大的体会是全栈项目真正的难点不是某个单一技术而是如何把多个技术栈流畅地串联在一起。你花一周时间能学会 Vue 的语法花三天能看懂 Node.js 的 Express 框架但把用户登录态、购物车状态、订单状态机、配送消息通知这些跨端数据流理顺需要的是系统的架构思维。这套订餐系统做完之后我对“前端到底在做什么”的理解都变了——前端不只是画页面而是整个业务流程中最贴近用户的一环。如果你也在做类似的综合项目建议先从数据库设计开始把核心状态机画清楚再动手写任何一行代码。