1. 项目背景与核心价值
私厨服务系统是近年来餐饮O2O领域的新兴模式,它打破了传统餐饮的固定场所限制,让擅长烹饪的个人能够通过平台接单,为周边用户提供定制化的美食服务。这个毕业设计选题巧妙结合了Node.js后端技术和小程序前端生态,构建了一个完整的私厨服务闭环。
我选择这个方向主要基于三点考量:首先,私厨经济在95后、00后群体中接受度极高,市场验证充分;其次,Node.js+小程序的组合能充分发挥全栈JavaScript的优势,降低开发维护成本;最后,系统涉及订单管理、地理围栏、支付对接等典型电商功能,技术覆盖面广。
2. 技术架构设计
2.1 整体技术栈选型
后端采用Express框架搭建RESTful API服务,相比Koa等框架,Express的中间件生态更成熟,适合教学场景。数据库选用MongoDB,主要考虑三点:一是私厨服务的菜品、订单等数据天然适合文档型存储;二是方便后期扩展用户评价等非结构化数据;三是与Node.js的JSON数据交互更高效。
前端采用微信小程序而非H5,原因很实际:小程序即用即走的特性与外卖场景高度契合,且微信支付接口对接更方便。实测表明,小程序在订单状态实时推送方面的体验优势明显。
2.2 核心模块划分
系统划分为五个主要模块:
- 用户服务:处理注册登录、地址管理
- 商品服务:管理私厨菜品上下架
- 订单服务:处理下单、状态流转
- 支付服务:对接微信支付
- 消息服务:处理订单状态变更通知
这种微服务化设计虽然增加了部署复杂度,但为后期扩展留足了空间。比如当私厨用户量增大时,可以单独对订单服务进行横向扩展。
3. 关键实现细节
3.1 地理围栏实现
私厨服务有个特殊需求:用户只能看到3公里范围内的私厨。这在MongoDB中通过地理空间索引实现:
// 建立地理位置索引 db.kitchens.createIndex({ location: "2dsphere" }) // 查询3公里范围内私厨 db.kitchens.find({ location: { $near: { $geometry: { type: "Point", coordinates: [经度, 纬度] }, $maxDistance: 3000 } } })实测发现,在用户量超过1万时,这种查询需要添加复合索引优化性能。我们最终采用location + status的复合索引,使查询速度提升约40%。
3.2 订单状态机设计
订单状态流转是系统的核心逻辑。我们采用有限状态机模式,明确定义了状态转换规则:
stateDiagram-v2 [*] --> 待支付 待支付 --> 已取消: 超时未支付 待支付 --> 已支付: 支付成功 已支付 --> 制作中: 商家接单 制作中 --> 配送中: 开始配送 配送中 --> 已完成: 用户确认 已支付 --> 已退款: 用户申请退款这个设计最大的坑是退款状态的处理。最初我们漏掉了"制作中"状态也能触发退款,导致后期不得不修改状态迁移逻辑。建议在状态机设计阶段就用工具画出完整流程图。
4. 性能优化实践
4.1 订单列表分页优化
当用户订单超过1000单时,传统skip+limit分页会出现性能问题。我们最终采用基于创建时间的游标分页:
// 第一页查询 const firstPage = await Order.find({ userId }) .sort({ createdAt: -1 }) .limit(10) // 获取下一页 const nextPage = await Order.find({ userId, createdAt: { $lt: lastItem.createdAt } }) .sort({ createdAt: -1 }) .limit(10)这种方案在测试中表现稳定,即使面对10万级订单数据,查询时间也能控制在50ms以内。
4.2 图片上传处理
私厨需要上传菜品图片,我们做了三级优化:
- 小程序端使用
wx.compressImage压缩原图 - 服务端使用sharp库生成三档分辨率图片(原图、中图、缩略图)
- 将图片存储迁移到CDN
实测数据显示,这套方案使图片加载速度提升3倍,且流量消耗降低60%。特别要注意的是sharp库的安装,在Windows环境下需要配置Python环境变量:
npm install --global windows-build-tools npm install sharp --save5. 安全防护措施
5.1 接口防刷设计
针对下单接口,我们实现了三层防护:
- 请求频率限制:使用express-rate-limit限制同一IP每分钟最多5次请求
- 业务逻辑校验:验证库存、用户余额等
- 风控系统对接:当异常订单超过阈值时触发人工审核
const rateLimit = require('express-rate-limit') const apiLimiter = rateLimit({ windowMs: 60 * 1000, // 1分钟 max: 5, handler: (req, res) => { res.status(429).json({ code: 429, message: '操作过于频繁' }) } }) app.use('/api/orders', apiLimiter)5.2 敏感数据保护
用户手机号等敏感信息采用加密存储。这里有个实际教训:初期我们直接用mongoose的set方法加密,导致模糊查询失效。最终方案是保留明文的前三位和后两位,中间用*号替代:
userSchema.virtual('displayPhone').get(function() { const phone = this.phone return phone.substring(0,3) + '****' + phone.substring(8) })6. 部署与监控
6.1 PM2集群部署
利用PM2的cluster模式充分发挥多核CPU性能:
pm2 start app.js -i max --name "private-chef"配置日志分割和自动重启:
module.exports = { apps: [{ name: "private-chef", script: "app.js", instances: "max", exec_mode: "cluster", max_memory_restart: "500M", out_file: "./logs/out.log", error_file: "./logs/error.log", log_date_format: "YYYY-MM-DD HH:mm:ss" }] }6.2 异常监控
我们使用Sentry捕获运行时错误,特别注意要过滤掉微信心跳接口的无关报错:
Sentry.init({ dsn: 'your_dsn', beforeSend(event) { if(event.request.url.includes('/wechat/heartbeat')) { return null } return event } })7. 开发经验总结
7.1 微信支付对接要点
微信支付接口有几个易错点:
- 统一下单接口的notify_url必须外网可访问
- 金额单位是分,要处理浮点数精度问题
- 签名算法要求参数按ASCII码排序
我们封装了支付工具类处理这些细节:
class WXPay { static async unifiedOrder(params) { // 处理金额转换 params.total_fee = Math.round(params.total_amount * 100) // 生成签名 params.sign = this._generateSign(params) // 发起请求 return axios.post('https://api.mch.weixin.qq.com/pay/unifiedorder', this._objToXml(params), { headers: { 'Content-Type': 'text/xml' } } ) } }7.2 调试技巧
远程调试时推荐使用ngrok内网穿透:
ngrok http 3000但要注意免费版域名会变化,可能导致微信配置失效。我们最终购买了基础版固定子域名。
对于小程序真机调试,有个实用技巧:在开发者工具中开启"不校验合法域名",可以快速测试本地接口。但上线前一定要记得关闭这个选项。