微信点餐小程序开发实战:从WXML到支付回调的完整指南
2026/9/16 3:00:49 网站建设 项目流程

简介:微信点餐小程序开发资源包,面向希望快速上手小程序开发的初中级开发者与餐饮行业技术从业者。内容覆盖从工具搭建、前端页面到后端接口与数据库设计的完整链路,提炼了微信开发者工具使用、WXML/WXSS框架、数据绑定与事件处理、微信支付及分享API调用、后端RESTful接口规划、数据库表结构设计、UI适配、测试发布与安全防护等关键知识点。压缩包共1129个文件,约28.87MB,其中包含427个PHP后端文件、138个HTML页面、113个JS脚本、38个CSS样式表、129个PNG与113个JPG图片资源、9个Markdown文档及2个SQL数据文件等,便于对照完整项目结构进行学习。已有1258人学习下载。通过源码阅读和资源内文档,可了解点餐系统中菜品管理、购物车、订单提交与支付流程等模块的实现思路,并为二次开发或课程设计提供可直接借鉴的工程参考。

1. 微信点餐小程序的工程骨架:不只是换皮的前端页面

很多人拿到微信点餐小程序源码时,第一反应是打开开发者工具把页面跑起来,却发现根目录是一堆bootstrap.cssueditor.cssvideo-js.css之类后台管理端静态资源。这说明项目并不是一个纯前端模板,而是“商家管理后台 + 小程序端 + 后端 API”三层结构。点餐业务的核心难点也不在样式,而在微信登录态、支付回调、订单状态流转这三条链路是否打通。如果你正在做毕业设计或接商用点餐项目,先理解这份资源里哪些文件属于服务端渲染的后台、哪些属于小程序端,才不会在后面联调时被 404 和wx.request失败反复打断。

2. 小程序端结构层与样式层:WXML/WXSS 的差异落地

2.1 微信开发者工具里初始化点餐项目:结构、目录和调试技巧

微信开发者工具是绕不开的第一站。导入项目后,我建议先把基础库版本调到 2.30.0 以上,因为后续分享朋友圈、canvas 2d 绘制海报这些能力对基础库有要求。初始化后的典型目录结构如下:

├── app.js ├── app.json ├── app.wxss ├── pages │ ├── index │ ├── menu │ ├── cart │ └── order ├── components │ └── dish-card └── utils ├── request.js └── config.js

说明:pages下每个页面由.js.json.wxml.wxss四个同名文件组成,这是微信小程序的基础约定。app.json里注册页面路径,pages/index/index作为入口页面。utils/config.js建议统一维护baseURL,避免在几十个页面里写死开发地址。

调试技巧:在开发者工具的“详情-本地设置”里勾选“不校验合法域名”,开发阶段可以跳过域名校验。但上线前必须在 mp.weixin.qq.com 的“开发管理-服务器域名”里配置request合法域名,且必须是 HTTPS 协议。支付回调的域名也要提前加白,否则模拟器里没问题,真机上会静默失败。

提示:拿到项目源码后,第一件事是全局搜索http://,把所有开发环境地址改成https://,并统一收敛到utils/config.js。很多学员在这里踩坑:个别页面写死了 IP 地址,换环境后只有部分接口能用。

如果你更习惯 Vue 语法,用 uni-app 配合 HBuilderX 也能把点餐业务跑起来,但原生 WXML 暴露的细节更多,排查问题时你能看到微信框架底层的编译产物,所以我下面用原生语法讲实现,这样不同技术栈的人都能理解数据是怎么从接口流转到渲染层的。

2.2 页面搭建:从菜品卡片到购物车角标的 WXML 片段

以菜品列表页为例,卡片需要展示图片、名称、价格、销量,以及右下角的“加入购物车”按钮。核心 WXML 结构如下:

<view class="menu-item" wx:for="{{dishList}}" wx:key="id"> <image src="{{item.cover}}" mode="aspectFill" /> <view class="info"> <text class="name">{{item.name}}</text> <text class="sales">已售 {{item.sales}}</text> <text class="price">¥{{item.price}}</text> </view> <button class="add-btn">Page({ data: { dishList: [], }, async onLoad() { await this.fetchDishList(); }, async fetchDishList() { const { data } = await wx.request({ url: `${getApp().globalData.baseURL}/api/dishes`, }); const list = data.map((d) => ({ id: d.id, name: d.dish_name, cover: d.cover_url, price: Number(d.price).toFixed(2), sales: d.sold_count, })); this.setData({ dishList: list }); }, onAddToCart(e) { const id = e.currentTarget.dataset.id; // 这里可以派发一个全局事件,让购物车角标更新 console.log('add dish:', id); }, });

说明:这里把后端字段dish_name映射成前端字段name,是一种低成本防御。如果数据库字段变化,只需要改这一处 map;如果后端字段重复,也不会把脏数据直接渲染到页面。setData更新数据是异步的,有大小限制,购物车数量这种高频变化应该增量更新,不要每次把整个列表塞进 setData。

2.3 WXSS 样式与事件绑定:让按钮“可点”的核心参数

WXSS 和 CSS 大部分语法一致,但有一些差异会直接影响点餐页面的布局。下面是我常用的一张对比表:

能力HTML/CSSWXML/WXSS差异影响
基础组件div/spanview/texttext内不能嵌套 view,会影响富文本排版
尺寸单位px/remrpx2rpx 等于 1px(以 750 设计稿为基准),适配不同屏宽
事件绑定onclickbindtap / catchtapcatchtap阻止冒泡,用于阻止点击购物车触发卡片跳转
条件渲染v-ifwx:if / wx:elif组件是直接编译成不同节点,不能做动态组件

其中 rpx 的换算规则是:在任何屏幕宽度下,750rpx等于屏幕宽度。所以设计稿如果是 750 宽,直接按标注的 px 尺寸写 rpx 值即可。

事件绑定方面,bindtap是冒泡事件,catchtap会阻止冒泡。点餐场景里,菜品卡片本身bindtap跳转详情,而加号按钮必须用catchtap="onAddToCart",否则点击加号会同时触发卡片跳转,体验非常糟糕。按钮还有一个容易忽略的属性hover-class,设置成button-hover能提供按压反馈,默认样式下很多开发者以为按钮坏了,其实只是没有按态效果。

样式重置也是点餐项目的必备动作。小程序button自带边框和默认背景,通常我会在app.wxss里统一覆盖:

.add-btn { width: 56rpx; height: 56rpx; line-height: 56rpx; padding: 0; border-radius: 50%; background: #ff6a00; color: #ffffff; font-size: 36rpx; text-align: center; border: none; } .add-btn::after { border: none; }

说明:button::after是微信框架生成的伪元素边框,不重置的话,即使设置了border: none,真机上仍会有一条细边。这个问题在表单类组件里同样存在,属于小程序样式体系里最容易踩的坑之一。掌握上述差异后,前端页面基本不会出现“样式对不上”和“事件重复触发”的低级问题。

3. 后端接口与数据库建模:订单和菜品的数据存取怎么设计才不别扭

3.1 选型:Node.js + Express + MySQL 的点餐接口为什么够用

点餐业务的数据模型稳定,但对订单一致性和库存准确性要求高。我一般选择 Node.js + Express + MySQL 这套组合,原因是:微信支付回调的报文是 XML 格式,Node.js 在处理异步回调签名时比 PHP 顺手;Express 路由组织简单,接口数量在 20 个以内时完全够用;MySQL 的事务能力可以保证下单不超卖。如果你的团队更熟 Python,用 Django 也能做,但下面的 API 设计思路是通用的。

后端接口规划如下:

方法路径功能请求参数
GET/api/dishes获取菜品列表category 可选
POST/api/orders创建订单openid, tableNo, dishList
GET/api/orders/:id查询订单详情id
PUT/api/orders/:id/status更新订单状态status
POST/api/pay/notify微信支付回调XML 报文

说明:openid来自小程序登录态,tableNo是用户扫描桌台码获得的桌号。创建订单时,后端必须校验dishList非空、数量为整数、价格以数据库为准,不能信任前端传入的totalAmount。这张 API 表也可以直接作为前后端联调时的工作清单。

3.2 建表与初始化数据:SQL 里的字段约束和索引取舍

订单和菜品直接相关的表至少有四张:菜品表、订单表、订单明细表、用户表。下面的 SQL 是核心部分:

CREATE TABLE dishes ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, dish_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, cover_url VARCHAR(255) DEFAULT '', stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, openid VARCHAR(64) NOT NULL, table_no VARCHAR(16) DEFAULT '', total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3已完成 4已取消', paid_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_openid (openid), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

说明:order_no必须唯一,后续微信支付查询订单和退款都要靠它。金额使用DECIMAL(10,2),不用 FLOAT/DOUBLE,否则累计金额会出现精度误差。status使用 TINYINT 并写注释,查询性能和可读性都要比字符串好。索引方面,openidstatus是高频筛选项,各建一个普通索引即可;不要给文本字段建索引,会导致写入变慢。

订单明细表需要联合外键,但物理外键我一般不加,因为点餐系统在删除菜品时需要降级处理,物理外键会让运营操作变得耗时。逻辑外键配合索引就够。

3.3 RESTful 接口实现:点餐、查单、改状态的关键代码

创建订单是所有接口里最需要写清楚的一个。下面是一段 Express 路由示例:

const express = require('express'); const router = express.Router(); const db = require('../db'); // 创建订单 router.post('/api/orders', async (req, res) => { const { openid, tableNo, dishList } = req.body; const conn = await db.getConnection(); try { await conn.beginTransaction(); let total = 0; for (const item of dishList) { const [rows] = await conn.query( 'SELECT price, stock FROM dishes WHERE id = ? AND status = 1 FOR UPDATE', [item.id] ); if (!rows.length) return res.status(404).json({ success: false, message: '菜品不存在' }); const dish = rows[0]; if (dish.stock < item.count) { await conn.rollback(); return res.json({ success: false, message: `菜品 ${item.id} 库存不足` }); } total += dish.price * item.count; } const orderNo = 'PO' + Date.now() + Math.random().toString(36).slice(2, 6).toUpperCase(); const [result] = await conn.query( 'INSERT INTO orders (order_no, openid, table_no, total_amount) VALUES (?, ?, ?, ?)', [orderNo, openid, tableNo, total] ); await conn.commit(); res.json({ success: true, orderId: result.insertId, orderNo, totalAmount: total }); } catch (err) { await conn.rollback(); res.status(500).json({ success: false, message: err.message }); } finally { conn.release(); } });

说明:这里用conn.getConnection()拿到的连接才能调beginTransaction。注意SELECT ... FOR UPDATE会对菜品行加锁,防止两个用户同时下单导致超卖。?占位符是参数化查询,不需要手动拼接字符串,从根源上避免 SQL 注入。orderNo用时间戳加随机串,演示够用;并发量高的场景建议用 Redisincr生成连续序号,再拼接日期前缀。

3.4 联调时常见的状态码与参数错误排查

前后端联调时,最多的报错集中在三类:

第一,openid为空导致创建订单失败。原因是登录接口没有先跑,或者 token 过期。前端需要在request.js里统一判断登录态,过期后重新走wx.login

第二,金额不一致。前端展示的价格和后端计算的价格差 1 分钱,常见原因是前端用toFixed(2),后端用Math.round(total * 100),四舍五入时机不一致。解决方案是前端只展示,后端做最终计算。

第三,支付回调不触发。先检查微信支付商户平台是否配置了回调域名,再看 Nginx 日志有没有收到微信服务器的 POST 请求。如果收到但返回 500,多半是回调里数据库更新失败。我会在回调接口里加一个order_no查重的逻辑,保证重复通知时幂等。

// 回调通知处理 const payDone = await db.query('UPDATE orders SET status = 1, paid_at = NOW() WHERE order_no = ? AND status = 0', [orderNo]); if (payDone.affectedRows === 0) { // 已经处理过或订单不存在,直接返回成功 res.end('<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>'); }

说明:affectedRows为 0 说明订单状态不是待支付,可能是重复回调或者订单不存在。此时同样返回 SUCCESS,避免微信服务器反复重试。这是支付回调最常见的幂等处理。

4. 微信登录、支付与分享 API 对接的完整链路

4.1 wx.login 获取 code,后端 code2Session 换 openid

小程序端获得用户身份的标准流程是:wx.login拿到临时code,后端拿到code去微信服务器换取openid。前端示例:

wx.login({ success: async (res) => { const { data } = await wx.request({ url: 'https://your.domain.com/api/wx/login', method: 'POST', data: { code: res.code }, }); if (data.success) { wx.setStorageSync('token', data.token); wx.setStorageSync('openid', data.openid); } }, });

对应后端实现:

const axios = require('axios'); router.post('/api/wx/login', async (req, res) => { const { code } = req.body; const { appid, secret } = config.wx; const { data } = await axios.get( `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code` ); if (data.errcode) { return res.status(401).json({ success: false, message: data.errmsg }); } // 生产环境应使用 JWT 生成 token,并设置过期时间 res.json({ success: true, openid: data.openid, token: createJwt(data.openid) }); });

说明:code2Session必须由后端发起,secret一旦暴露,任何人都能冒充用户身份。返回的openid是用户在本小程序内的唯一标识,但不包含用户头像、昵称等资料,这些信息需要在获取用户信息时单独授权。注意微信在基础库更新后,不再推荐通过wx.getUserInfo直接拿个人信息,点餐场景非必须时建议不采集。

4.2 微信支付:统一下单与 wx.requestPayment 的参数映射

支付链路最容易出错的部分是签名。后端需要按微信支付规则生成paySign,并在前端调起收银台。核心逻辑如下:

const crypto = require('crypto'); function makeSign(params, mchKey) { const str = Object.keys(params) .filter((k) => params[k] !== '' && k !== 'sign') .sort() .map((k) => `${k}=${params[k]}`) .join('&') + `&key=${mchKey}`; return crypto.createHash('md5').update(str).digest('hex').toUpperCase(); } router.post('/api/pay/unifiedorder', async (req, res) => { const { orderNo } = req.body; const order = await db.getOrderByNo(orderNo); const params = { appid: config.wx.appid, mch_id: config.wx.mchId, nonce_str: Math.random().toString(36).slice(2), body: '微信点餐-' + orderNo, out_trade_no: orderNo, total_fee: Math.round(order.total_amount * 100), spbill_create_ip: req.ip, notify_url: config.wx.notifyUrl, trade_type: 'JSAPI', openid: order.openid, }; params.sign = makeSign(params, config.wx.mchKey); // 将 params 转 XML 后 POST 到 https://api.mch.weixin.qq.com/pay/unifiedorder const payParams = { appId: params.appid, timeStamp: String(Math.floor(Date.now() / 1000)), nonceStr: params.nonce_str, package: `prepay_id=${prepayId}`, signType: 'MD5', }; payParams.paySign = makeSign(payParams, config.wx.mchKey); res.json({ success: true, payParams }); });

说明:total_fee单位是分,后端从数据库查出元金额后必须乘 100 并取整。前端拿到payParams后这样调起支付:

wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: 'MD5', paySign: payParams.paySign, success: () => { wx.navigateTo({ url: `/pages/order/detail?id=${orderId}` }); }, fail: (err) => { if (err.errMsg && err.errMsg.indexOf('cancel') > -1) { // 用户主动取消,不提示错误 } else { wx.showToast({ title: '支付失败,请重试', icon: 'none' }); } }, });

说明:fail并不仅仅代表支付失败,用户取消、网络中断都会进入这里。一定要通过errMsg区分 cancel 和其他错误,否则用户一看弹窗被提示“支付失败”,会以为钱扣了但没支付成功,产生咨询压力。真实扣款是否成功,最终以回调通知为准。

4.3 分享到会话与朋友圈:open-type 配置

下单完成后,引导用户分享是点餐小程序提升复购率最简单的手段。在 WXML 里放置:

<button open-type="share" class="share-btn">分享给好友</button>

然后在当前页面 JS 里定义:

Page({ onShareAppMessage() { return { title: '这家店可以微信点餐,免排队', path: `/pages/index/index?tableNo=${this.data.tableNo}`, }; }, onShareTimeline() { return { title: '微信点餐小程序,扫码即点', query: `tableNo=${this.data.tableNo}`, }; }, });

说明:onShareTimeline只在基础库 2.11.0 及以上支持,分享朋友圈时不支持自定义图片,默认截图页面。path里不要带 http 链接,必须是小程序内部页面路径。如果页面里有wx.hideShareMenu,需要先移除。另外,分享参数tableNo能帮助识别用户来自哪个桌台,方便商家核对订单。

4.4 用 Charles 抓包定位支付回调里的“疑难杂症”

遇到支付回调不更新订单,很多开发者无从下手。我一般会先在小程序开发者工具里看wx.requestPayment的返回,确认支付是否成功;再用 Charles 配置 SSL 代理,抓取小程序发起的/api/pay/unifiedorder和回调请求。抓包时注意手机和电脑连接同一个局域网,配置好代理后安装 Charles 的 CA 证书。这是微信小程序调试的常规手段,能清楚看到请求头、回包和签名内容。如果抓包发现回调请求根本没到后端,检查微信支付商户平台的回调 URL;如果到了但验签失败,重点检查证书序列、nonce_strsign的排序规则。

5. 上线前的安全加固与性能优化:我验过这些坑必须提前填

5.1 防 SQL 注入和 XSS:从参数化查询到 WXML 转义

点餐系统涉及真实支付,安全底线一定要守住。后端所有数据库操作都使用参数化查询,不使用字符串拼接 SQL。前端 WXML 的插值{{}}默认会对字符串进行转义,特殊字符不会被解析成 HTML 标签,这天然挡掉了一部分反射型 XSS。但富文本场景必须单独处理。

5.2 发布流程:体验版→审核→正式版的检查点

阶段检查项
开发版充满电、关闭代理,在不同机型上走通下单支付全流程
体验版添加体验成员,测试微信支付真实回调,检查订单通知
审核类目选择“餐饮-点餐外卖”,截图核心功能,提供测试账号
正式版观察错误监控,查看退款和取消订单日志

个人开发者在体验版阶段容易忽略支付回调,因为大多数模拟器不支持微信支付。建议把体验版二维码发给同事,用 Android 和 iOS 各支付一次一毛钱订单,验证回调更新后再取消订单。

5.3 从 UEditor 富文本到小程序样式的迁移技巧

后台资源里的ueditor.css是富文本编辑器 UEditor 的样式。运营在后台编辑“店铺公告”或“菜品详情”时,会写入大量带class的 HTML 片段。小程序端的rich-text组件并不加载外联 CSS,导致这些片段渲染出来没有样式。我习惯在后端增加一个清洗中间件,用sanitize-html过滤白名单标签和允许的样式:

npm install sanitize-html
const sanitizeHtml = require('sanitize-html'); function cleanRichText(html) { return sanitizeHtml(html, { allowedTags: ['p', 'br', 'img', 'strong', 'em'], allowedAttributes: { img: ['src', 'style'] }, allowedStyles: { img: { width: [/^\d+px$/], height: [/^\d+px$/] }, }, }); }

说明:allowedTags限制只有段落、图片和加粗斜体这类基础标签会保留;allowedStyles只允许图片的宽高内联样式,其它classstyle全部剔除。这样rich-text在小程序端渲染时既不会丢图片,也不会被后台的 CSS 污染布局。这段清洗逻辑放在接口层,前端不用重复处理。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询