前阵子我把一套“nodejs基于vue的上门做饭私厨到家服务系统”完整地做了出来,从需求梳理、数据库设计到前后端编码、联调部署,折腾了小半个月。项目代号叫 d5235,听着像个内部工程编号,其实就是我本地仓库的一个标识,后面文章里我也会一直用这个代号指代它。
这个系统的业务其实很好理解:用户不想去餐厅、不想点外卖,希望有厨师带着食材直接到家里做一桌家常菜;厨师可以在平台上注册、上架自己的拿手菜、接收订单并为用户上门服务;平台负责把双方的需求匹配起来,处理预约时间、订单状态、支付款项、评价体系这些东西。市面上的私厨软件逻辑大抵如此,但真把一个能跑的版本从零做出来,涉及到的技术点比想象中多得多:用户端、厨师端、管理后台,再加上定位匹配、订单流转、实时通知、支付流程对接,每一块都有值得展开讲的细节。
如果你正在准备用 Vue + Node.js 做全栈项目,或者想复现一个带完整业务闭环的到家服务类系统,这篇文章基本能当成一份保姆级参考。我会把选型原因、表结构、核心接口、前端页面实现、联调坑点都串起来讲,尤其是一些常规文档里不会写的细节,比如订单状态机怎么设计才不容易乱、npm 环境报错怎么处理、前后端联调时接口跨域怎么解决,这些都会单独拿出来说。
1. 系统需求与整体技术架构设计
1.1 系统定位与核心业务链路
先说说这个项目要解决的核心诉求。传统餐饮服务里,用户要么堂食、要么外卖,但这两者都解决不了“我想在家吃一顿有锅气的家常菜”这个需求。私厨上门服务把厨师这个核心供给资源直接调度到用户家中,本质上是一个“服务上门”的交易撮合平台。
为了让这个模式跑通,系统至少要覆盖三段流程:
- 用户侧:浏览菜品和厨师信息、提交预约、在线支付、查看订单进度、服务完成后评价。
- 厨师侧:录入菜品、设置可服务时间、接收新订单通知、接单/拒单、服务开始与完成确认。
- 平台侧:管理用户与厨师账号、审核菜品上下架、处理退款投诉、统计订单数据。
整个核心业务链路是“用户下单 → 系统推荐附近厨师 → 厨师接单 → 按约上门服务 → 确认完成 → 用户评价 → 订单结算”。每个环节看起来简单,但落到代码上就是一套完整的状态流转、权限控制和数据持久化逻辑。
做这类系统最容易犯的错就是只盯着“下单”和“支付”两个高频动作,忽略了服务过程的中间状态。用户和厨师真正吵架的地方往往发生在“已接单但厨师迟到”“服务中但用户临时加菜”“服务完成后谁先确认”这些环节。所以我在设计时把订单状态拆得很细,后面第 2.3 节会专门讲。
1.2 技术选型:为什么是 Node.js + Vue
项目标题已经定了技术栈是 Node.js 和 Vue,但为什么这两个组合适合做这类系统,值得展开聊聊。
后端选 Node.js,最大的理由是业务复杂度适中,用 JavaScript 一门语言打通前后端开发效率极高。私厨平台的接口量级大概在三五十个左右,没有特别重型的计算任务,也没有复杂的并发场景,Node.js 的事件驱动模型和丰富的 npm 生态足够覆盖。尤其在做订单推送这类需要 WebSocket 长连接的功能时,Node.js 配合 socket.io 几乎是无痛实现。
有朋友可能问,为什么不选 Spring Boot?说实话,如果团队里都是 Java 背景,选 Spring Boot 没有问题。但对于个人开发者或者小团队来说,Spring Boot 的项目体积和启动成本明显更重,写一个 CRUD 接口要配的注解和依赖也比 Express 多不少。Node.js 的 Express/Koa 中间件模型很轻,路由、鉴权、日志按需加载,非常适合快速迭代拿到结果。
前端选 Vue 3,则是看重它的渐进式特性和模板语法。Vue 的模板比 JSX 对新手友好,Composition API 又能在组件逻辑复杂时保持代码整齐。Vue Router 做页面路由、Pinia 做状态管理、Element Plus 提供现成组件库,这几个组合下来大概一个小时就能把后台管理页的骨架搭好。另外一个实际原因是 Vue 生态里和 Node.js 配合的资料非常多,遇到问题基本搜得到答案,对独立开发者很友好。
1.3 系统总体架构与模块划分
系统整体采用前后端分离架构,前端 Vue 3 应用运行在浏览器里,通过 HTTP 和 WebSocket 两种方式与后端通信。后端 Node.js 提供 RESTful API 和实时通知能力,数据层采用 MySQL 存储业务数据,Redis 可选用来做验证码缓存和 Token 管理。
按业务域划分,系统包含六个主要模块:
- 用户模块:注册登录、收货地址维护、个人资料。
- 厨师模块:入驻申请、菜品管理、可服务时段设置。
- 订单模块:下单、支付、接单、服务流转、取消与退款。
- 评价模块:订单完成后的互评体系。
- 消息模块:订单状态变化的实时通知。
- 管理模块:后台统计、分类管理、内容审核。
前端也对应拆成三个独立应用:用户端、厨师端、管理后台。因为 Vue 项目本身是同一个代码仓库,我用路由和权限来区分不同角色的访问范围,这样开发和部署都更省事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 功能模块拆解与数据库设计
2.1 角色体系与功能地图
系统里一共有三类角色,分别是普通用户、厨师、管理员。角色之间不是单纯的平级关系,而是围绕订单形成一种三角形协作关系。
普通用户的核心操作:注册登录、查看厨师与菜品列表、根据定位搜索附近厨师、提交预约单、在线支付、查看订单状态、服务完成后评价、售后申请。
厨师的核心操作:提交入驻资料、维护菜品库(上架、下架、修改价格和描述)、设置可服务时间范围、收到新订单通知、接单/拒单、服务开始/完成确认、查看收益。
管理员的核心操作:用户与厨师审核、菜品分类管理、订单全流程查看、异常订单处理、平台数据报表。
功能地图的意义在于,它决定了数据库表和接口清单的数量。我在建模阶段拿着这个功能地图逐个打勾,确保前端页面、后端接口、数据表三层是严格对应的,基本不会出现“页面做了但没接口”或者“表建了但用不上”的尴尬情况。
2.2 数据表设计与关系说明
数据库我选了 MySQL 8.x,表结构采用 InnoDB 引擎,字符集统一 utf8mb4。整套系统一共设计了 9 张核心表,它们在业务上的关系可以这样理解:用户表和厨师表是两类参与者,菜品表挂在厨师名下,订单表是交易核心,订单明细表记录用户点了哪些菜,地址表、评价表、消息表则分别支撑履约、反馈和通知。
| 表名 | 核心字段 | 用途说明 |
|---|---|---|
| user | id, phone, password, nickname, avatar, role | 用户/厨师账号统一存放,role 区分角色 |
| chef_profile | id, user_id, real_name, id_card, service_area, intro | 厨师扩展信息,接单前由管理员审核 |
| dish | id, chef_id, name, price, image, category, status | 厨师发布的菜品 |
| address | id, user_id, contact, phone, detail, longitude, latitude | 用户收货地址,下单时选择 |
| orders | id, order_no, user_id, chef_id, address_id, service_time, status, amount, remark | 主订单表,存一条订单的主干信息 |
| order_item | id, order_id, dish_id, dish_name, price, quantity | 订单明细,记录每个菜品快照 |
| review | id, order_id, from_id, to_id, content, score | 评价表,订单完成后由双方填写 |
| message | id, user_id, content, type, is_read, create_time | 站内信/通知消息 |
| settlement | id, chef_id, order_id, amount, fee, status | 厨师结算表,记录每单应结金额与平台抽成 |
设计时有个细节值得说明:订单明细表里的 dish_name 和 price 为什么不直接关联菜品表?因为菜品价格和名称是会变的,如果只存菜品 ID,后来厨师改了价格,历史订单也会跟着变,这对结算和售后都是灾难。所以明细表里必须以快照方式冗余菜品名称和价格,这也是电商类系统建模的通用准则。
厨师入驻表独立成 chef_profile,而不是直接塞在 user 表里,是因为用户的角色有可能只有一个,但厨师的资质信息(身份证、服务区域、介绍)字段比较多。拆开之后,普通用户表保持精简,厨师扩展信息也更容易按需加载。
2.3 订单状态机的设计
订单状态是整个系统的核心枢纽,设计得好不好直接决定了代码写起来顺不顺畅。我第一次做的时候以为订单状态搞个“待支付、已支付、已完成”就够了,后来真正跑业务才发现根本撑不住。用户取消、厨师拒单、超时未接、退款申请这些分支场景一进来,状态表马上就不够用了。
这里分享一下我在 d5235 项目里使用的订单状态集合,一共 10 个状态:
- 待支付:用户提交订单后生成,支付完成后进入下一步。
- 待接单:支付成功,平台开始等待厨师确认。
- 已接单:厨师确认接单,准备按约上门。
- 服务中:厨师开始服务,用户界面出现实时进度。
- 待确认:厨师完成服务,等待用户确认。
- 已完成:用户确认完成,订单进入结算和评价环节。
- 已取消:用户主动取消或系统超时取消。
- 拒单:厨师拒绝接单,系统自动为订单寻找其他厨师。
- 退款中:发生售后,进入退款流程。
- 已退款:退款完成,订单关闭。
这套状态机的设计原则,我在后面还会提。原则上就是把所有可能的业务分支都放到状态转移里,避免在业务代码里用字符串到处乱判断,否则后面维护状态会非常痛苦。
3. Vue3 前端核心实现
3.1 项目初始化与工程目录
前端部分我用 Vite + Vue 3 + Pinia + Vue Router + Element Plus 这套组合,Vite 启动速度快,热更新体验比 Webpack 时代舒服太多,能明显减少开发过程中的等待成本。
初始化方式很简单,直接用 npm 命令创建:
bash复制npm create vue@latest
创建过程中按照提示选择 Router、Pinia、ESLint、Prettier 等选项,完成后把目录结构调整到下面的结构:
text复制src/
api/ # 接口请求封装
http.js # axios 实例
user.js # 用户相关接口
order.js # 订单相关接口
chef.js # 厨师相关接口
assets/ # 静态资源
components/ # 公共组件
router/ # 路由配置
stores/ # Pinia 状态管理
views/
user/ # 用户端页面
chef/ # 厨师端页面
admin/ # 管理后台页面
utils/ # 工具函数
App.vue
main.js
这个目录结构有几个好处:按业务域分目录而不是按技术类型堆文件,后续新增功能时能快速定位修改范围;api 目录单独拆出来,前端页面代码里不直接写 axios 调用,统一走 api 函数,接口变更时只需要改一处。
3.2 路由设计、状态管理与接口请求封装
路由层面,我先定义了三个基础布局:用户端布局、厨师端布局、管理端布局。不同角色进入系统后,通过路由守卫控制访问范围。
以下是一个简化版的路由配置,重点展示角色权限控制的写法:
javascript复制// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '../stores/user'
const routes = [
{
path: '/',
component: () => import('../layouts/UserLayout.vue'),
children: [
{ path: '', name: 'home', component: () => import('../views/user/Home.vue') },
{ path: 'chefs', name: 'chefList', component: () => import('../views/user/ChefList.vue') },
{ path: 'order/:id', name: 'orderDetail', component: () => import('../views/user/OrderDetail.vue') }
]
},
{
path: '/chef',
component: () => import('../layouts/ChefLayout.vue'),
meta: { role: 'chef' },
children: [
{ path: 'orders', name: 'chefOrders', component: () => import('../views/chef/Orders.vue') },
{ path: 'dishes', name: 'chefDishes', component: () => import('../views/chef/DishManage.vue') }
]
},
{
path: '/admin',
component: () => import('../layouts/AdminLayout.vue'),
meta: { role: 'admin' },
children: [
{ path: 'users', name: 'adminUsers', component: () => import('../views/admin/UserList.vue') },
{ path: 'orders', name: 'adminOrders', component: () => import('../views/admin/OrderList.vue') }
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
const requiredRole = to.meta.role
if (requiredRole && userStore.role !== requiredRole) {
next('/login')
return
}
next()
})
状态管理用 Pinia,主要维护三块内容:用户登录态与基本信息、当前的订单上下文、消息未读数。Pinia 比 Vuex 的写法简洁很多,去掉了 mutations 这一层,直接在 actions 里改 state,用起来特别顺手。
javascript复制// src/stores/user.js
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi } from '../api/user'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null
}),
actions: {
async login(phone, password) {
const res = await loginApi({ phone, password })
this.token = res.data.token
localStorage.setItem('token', this.token)
this.userInfo = res.data.userInfo
},
async fetchUserInfo() {
if (!this.token) return
const res = await getUserInfoApi()
this.userInfo = res.data
}
}
})
接口请求封装这一步经常被新手忽略,但它对后期维护至关重要。我在 axios 封装里统一配置了 baseURL、超时时间、请求拦截器和响应拦截器,请求拦截器负责携带 Token,响应拦截器负责统一处理业务错误和登录过期。
javascript复制// src/api/http.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
const http = axios.create({
baseURL: '/api',
timeout: 10000
})
http.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
http.interceptors.response.use(
res => {
const data = res.data
if (data.code !== 0) {
ElMessage.error(data.msg || '请求失败')
return Promise.reject(new Error(data.msg))
}
return data
},
err => {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(err.message || '网络异常')
return Promise.reject(err)
}
)
export default http
3.3 关键页面与业务交互实现
首页是整个用户端最核心的展示页面,承担了“找厨师”这个最高频的需求。我在首页放了搜索栏、分类筛选、按距离排序的厨师列表三个模块。定位功能通过浏览器 Geolocation API 获取经纬度,传给后端后由后端进行距离排序和候选厨师推荐。
下单页面是逻辑最复杂的页面。用户选择厨师、选择菜品、选择服务时间段、填写地址、备注口味需求,前端需要拼装出一个完整的订单对象。这里我重点处理了菜品数量和价格的同步计算,防止用户在添加多个菜品后总价算错。
厨师端接单页用了 socket.io-client 与服务端建立 WebSocket 长连接,新订单进来时页面会实时弹出提醒,厨师点击接单后状态立即更新。这块前端实现不复杂,真正需要注意的是断线重连机制。我封了一个小的 socket 管理器,监听 connect 和 disconnect 事件,断线时自动重连并重新订阅订单消息频道。
4. Node.js 后端接口与业务逻辑实现
4.1 Express 项目搭建与中间件
后端我选 Express 4.x,虽然 Koa 的洋葱模型更灵活,但 Express 的中间件生态更丰富,尤其在做文件上传、静态资源托管、session 处理的时候能找到大量现成方案。
初始化项目和基础目录结构:
bash复制npm init -y
npm install express mysql2 sequelize jsonwebtoken bcryptjs socket.io cors
目录结构:
text复制server/
app.js # 应用入口
config/
db.js # 数据库配置
jwt.js # JWT 密钥配置
models/ # Sequelize 模型
routes/ # 路由定义
controllers/ # 业务逻辑处理
middlewares/ # 自定义中间件
utils/ # 工具函数
入口文件里的中间件注册顺序很讲究,我把 CORS、日志、路由、错误处理按照职责依次挂载:
javascript复制// app.js
const express = require('express')
const cors = require('cors')
const mongoose = require('mongoose')
const app = express()
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// 路由
app.use('/api/auth', require('./routes/auth'))
app.use('/api/chef', require('./routes/chef'))
app.use('/api/order', require('./routes/order'))
app.use('/api/user', require('./routes/user'))
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err)
res.status(500).json({ code: 500, msg: '服务器内部错误' })
})
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})
4.2 核心接口:认证、订单与厨师匹配
认证接口采用 JWT 方案,用户在注册或登录后,后端签发一个 Token 返回给前端,前端后续请求通过 Authorization 头带上。密码加密用 bcryptjs,绝对不允许明文存储,这是最基本的底线。
注册接口的核心逻辑:
javascript复制// controllers/authController.js
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { User } = require('../models')
const { jwtSecret } = require('../config/jwt')
exports.register = async (req, res) => {
const { phone, password, nickname } = req.body
if (!phone || !password) {
return res.status(400).json({ code: 400, msg: '手机号和密码不能为空' })
}
const exist = await User.findOne({ where: { phone } })
if (exist) {
return res.status(400).json({ code: 400, msg: '手机号已注册' })
}
const hashedPassword = await bcrypt.hash(password, 10)
const user = await User.create({
phone,
password: hashedPassword,
nickname: nickname || '用户' + phone.slice(-4)
})
res.json({ code: 0, data: { id: user.id } })
}
创建订单接口是业务的核心,它要完成的动作很多:校验菜品归属、计算总金额、生成唯一订单号、写入订单主表和明细表、推送消息给厨师端。为了保证数据一致性,我把这些操作包在事务里执行,避免出现“主表写入成功但明细表失败”的情况。
javascript复制// controllers/orderController.js
const { sequelize, Order, OrderItem, Dish } = require('../models')
const { generateOrderNo } = require('../utils/orderNo')
exports.createOrder = async (req, res) => {
const t = await sequelize.transaction()
try {
const { chefId, items, addressId, serviceTime, remark } = req.body
const userId = req.user.id
let totalAmount = 0
const orderItemsData = []
for (const item of items) {
const dish = await Dish.findOne({ where: { id: item.dishId, chef_id: chefId }, transaction: t })
if (!dish || dish.status !== 1) {
throw new Error(`菜品 ${item.dishId} 不存在或已下架`)
}
const subtotal = dish.price * item.quantity
totalAmount += subtotal
orderItemsData.push({
dish_id: dish.id,
dish_name: dish.name,
price: dish.price,
quantity: item.quantity
})
}
const order = await Order.create({
order_no: generateOrderNo(),
user_id: userId,
chef_id: chefId,
address_id: addressId,
service_time: serviceTime,
remark,
amount: totalAmount,
status: 'PENDING_PAY'
}, { transaction: t })
for (const item of orderItemsData) {
await OrderItem.create({ ...item, order_id: order.id }, { transaction: t })
}
await t.commit()
res.json({ code: 0, data: { orderId: order.id, amount: totalAmount } })
} catch (err) {
await t.rollback()
res.status(400).json({ code: 400, msg: err.message })
}
}
这里有一点很重要:金额计算绝对不能依赖前端传过来的 totalAmount,后端必须根据数据库里的菜品价格重新计算,否则用户改一下前端参数就能以错误价格下单。这是我在实际项目中坚持的原则,也是面试里常问的安全意识问题。
厨师匹配接口是整个系统的价值所在。用户下单后没有指定厨师的情况下,系统要根据用户的服务地址经纬度,从附近厨师的经纬度集合里筛选最近的人。距离计算我用的是 Haversine 公式,精度足够而且实现简单。
javascript复制// utils/distance.js
function haversineDistance(lat1, lon1, lat2, lon2) {
const R = 6371
const dLat = toRadians(lat2 - lat1)
const dLon = toRadians(lon2 - lon1)
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}
如果人数规模上来以后,应用层遍历所有厨师算距离的方案就会变慢,到时候可以引入 MySQL 空间索引或者 Redis GEO。我在 d5235 中先用简单方案把链路跑通,但心里要清楚这个扩展方向。
4.3 实时通知与支付模拟
订单状态变化时,需要实时通知相关人员。厨师端要尽快知道有新订单,用户端要第一时间看到厨师接单、服务开始、服务完成等状态更新。这里用 socket.io 实现,在应用入口挂载时把 socket 实例和 Express 的 HTTP Server 绑定在一起。
javascript复制// socket.js
const { Server } = require('socket.io')
function initSocket(server) {
const io = new Server(server, {
cors: { origin: '*' }
})
io.on('connection', socket => {
const userId = socket.handshake.query.userId
if (userId) {
socket.join(`user_${userId}`)
}
})
return io
}
module.exports = initSocket
推送消息的工具函数:
javascript复制// utils/push.js
function pushToUser(io, userId, event, data) {
io.to(`user_${userId}`).emit(event, data)
}
module.exports = pushToUser
支付这块我没有真实接入微信或支付宝的商户接口,而是实现了“模拟支付”。真实接入时只需要替换 pay 方法,把请求转发到支付平台的统一下单接口,然后通过回调地址接收支付结果。在本地调试阶段用模拟支付可以避免资质审核、回调穿透等一堆麻烦,先把业务闭环打通。
5. 环境搭建、联调排错与常见问题
5.1 Node.js 与 npm 环境配置要点
这个项目对 Node.js 版本没有特殊要求,18 或 20 LTS 版本都行。安装后第一件事是确认环境变量有没有配上,如果打开命令行执行 node -v 提示找不到命令,多半是安装时没有勾选追加 PATH 选项,或者用了压缩包解压方式但没手动配置系统环境变量。
安装完 Node.js,npm 最好顺手配置一下国内镜像源,否则安装依赖的时候速度会非常慢,而且容易超时失败。执行下面的命令把 registry 指到国内镜像:
bash复制npm config set registry https://registry.npmmirror.com
这里多说一句:设置镜像源是 npm 的常规操作,跟某类“特殊网络访问方式”完全是两码事,这个大家正常操作就行。
很多人在 Windows 上遇到的“npm.ps1 无法加载文件”问题,本质上不是 npm 坏了,而是 PowerShell 默认的执行策略禁止运行脚本。解决办法有两个:
第一个办法,以管理员身份打开 PowerShell,执行:
powershell复制Set-ExecutionPolicy RemoteSigned
这个命令的含义是允许下载的脚本在被信任之后运行。执行完重新打开终端就正常了。
第二个更省事的办法,直接改用 Git Bash 或 CMD 来执行 npm 命令,完全绕开 PowerShell 的脚本策略。我个人的习惯是,Windows 上统一用 Git Bash 做开发终端,既能用 node/npm 命令,又能用 Linux 风格的命令,舒服很多。
5.2 跨域与端口问题
前后端分离模式下,跨域问题几乎是必踩的坑。前端运行在 5173 端口,后端运行在 3000 端口,浏览器会默认拦截跨域请求。虽然有在后端加 cors() 中间件这种解决方案,但更推荐的做法是利用 Vite 的代理功能,让前端发出的请求统一走相对路径 /api,由 Vite 开发服务器把它转发到后端。
javascript复制// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})
这样配置之后,前端代码里的 baseURL 写成 '/api' 即可,浏览器里看到的请求都是同源的,完全没有跨域问题。生产环境再用 Nginx 把 /api 前缀代理到 Node.js 服务,同一个域名下分发,一举两得。
端口冲突也是高频问题。后端默认 3000 端口可能被别的程序占用,启动时报 EADDRINUSE。这个时候要么改端口,要么查找并杀掉占用进程。Windows 下可以执行:
bash复制netstat -ano | findstr :3000
taskkill /PID 进程号 /F
5.3 数据集与前后端联调技巧
很多人在项目快写完的时候才发现没有数据可以展示,页面一片空白,根本看不出效果。我推荐在开发阶段写一个 seed 脚本,往数据库里预先填充一批厨师、菜品、地址和订单数据,这样前端开发和后端联调都能直接在真实数据上进行。
我的 seed 脚本里准备了 6 个厨师、每个厨师 5 道菜品、3 个用户地址,以及 10 条覆盖不同状态的测试订单。前端调列表页、详情页、订单状态页都有现成数据可以看,效率提升非常明显。
前后端联调时,我第一次遇到的配合问题是字段命名不一致。后端返回的是下划线风格 user_id,前端组件里却写的是驼峰 userId,导致始终拿不到数据。后来我在前端封装了一个字段映射函数,或者在 SQL 查询时用 AS 语句直接把别名改成驼峰,两边的沟通成本降了下来。这种看着不起眼的细节,实际写代码时消耗的时间比写逻辑还多。
6. 部署上线与进一步优化方向
6.1 构建、部署与基础运维
前后端开发完成后,部署环节我也做了完整流程。前端执行 npm run build 后生成 dist 目录,包含所有编译静态文件。后端代码直接复制到服务器,使用 pm2 进行进程守护,保证 Node.js 服务异常退出后能自动重启。
Nginx 配置的关键点有两个:一是把前端的静态资源直接指向 dist 目录,二是把 /api 开头的请求反向代理到后端的 3000 端口。同时加上 gzip 压缩、静态资源缓存策略,能明显提升页面打开速度。
nginx复制server {
listen 80;
server_name your-domain.com;
root /var/www/dist;
index index.html;
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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
数据库建议每天凌晨做一次定时备份,用 mysqldump 导出到备份目录并保留最近七天份,避免误操作或者服务器故障时数据丢失。
6.2 业务扩展与安全加固
系统已经跑通了完整核心链路,但离真正商业落地还有不少距离。从我的经验看,后续值得投入的方向有三个。
第一是安全加固。目前注册接口还没有做图形验证码或滑块验证,容易被脚本批量注册;接口层面也缺少限流机制,被攻击时容易被打满。建议接入验证码服务,同时用 express-rate-limit 做 IP 级别的接口限流。
第二是智能匹配。目前厨师推荐逻辑还是简单的按距离排序,后续可以引入评分权重、菜品匹配度、接单率这些因子,做一个综合排序推荐算法。用户把服务时间填好之后,系统还可以根据厨师的忙碌日历自动给出可约排期,这事比单纯的推荐更有商业价值。
第三是多端适配。小程序端的转化率远高于 H5 页面,如果要把项目推向真实市场,优先做微信小程序版本是更合理的选择。好在接口已经全部 RESTful 化,小程序端只需要复用后端接口,前端用 uni-app 或原生小程序重写渲染层即可。
最后再分享一个小技巧,如果你也在做类似的系统,我强烈建议花半天时间把订单状态机画清楚再动手写代码。状态少了会漏业务,状态多了会让前后端联调变得繁琐,找到一个合适的粒度是这类业务系统的灵魂。我第一次做的时候低估了这部分的重要性,后期补状态、改接口、调页面,浪费了远不止半天的时间。把这个状态表做成前后端共享的文档随时对照,后面所有模块的开发都会顺畅很多。
