1. 为什么我选择做一个外卖点餐小程序
先说结论:如果只挑一个练手项目来吃透微信小程序开发,"外卖点餐系统"比绝大多数项目都合适。原因有三:点餐购物车是典型的页面状态管理场景,订单流转考验网络请求与接口设计,支付环节能完整体验微信生态的登录、支付、订阅消息闭环。
这个项目的标题叫"微信小程序 #项目笔记# | 从0到1实现外卖点餐系统小程序",看这个名字就知道,这不是一个只做静态展示的页面,而是一个从账号注册到正式上线全流程的完整项目。我做完这套系统之后最大的感受是:表面上是写了一个点餐页面,实际上是把微信小程序的页面框架、组件通信、全局状态、后端交互、异步接口、上线审核全部过了一遍。
这套系统能做什么?一句话概括:用户端打开小程序,看到附近商家,点进店铺选菜品,加购物车,提交订单,微信支付,商家后台收到新订单,处理出餐状态,用户实时看到进度。如果你正在学习小程序开发,或者想自己做一个能真实上线的商业项目作为作品集,这套系统可以作为很好的参考。
适合谁来看这篇笔记?刚学完小程序基础语法、想做整合项目的人;以及后端开发想了解小程序端完整实现方式的人。我会把每一步是怎么想、为什么这样选、踩过哪些坑都记录下来,尽量还原真实开发过程,而不是给一份"百度都能搜到"的代码汇总。
2. 技术选型与整体架构设计
2.1 前端框架:原生小程序还是uni-app
开发小程序第一步要做的决定,就是用什么框架来写前端。我当时的候选有三个:微信原生语法、uni-app、Taro。
先说结论,我选了原生语法。原因其实很简单:这个项目要调的微信API很多,登录、支付、订阅消息、地图定位。用原生框架可以少一层编译转换,报错排查时直接对应官方文档,不至于出现"uni-app 里写的代码编译成小程序后行为不一致"这类问题。很多做外包的朋友喜欢用 uni-app,因为能一把梭多端,但纯小程序项目上原生方案更稳,维护也直观。
还有一点在做决定时要考虑进去:你的队友技术栈是什么样的。如果团队是 Vue 或 React 背景,选用 uni-app 或 Taro 确实能让上手难度降低一截。但若是我这样以小程序为唯一交付目标的小项目,原生的 WXML/WXSS/JS 三件套是官方第一优先支持的,坑最少,资料最多。
2.2 后端方案选型:自建接口还是云开发
外卖点餐系统必须要后端,至少要能保存商品、订单、用户信息。后端方案我当时认真纠结过两条路,也推荐你根据自己情况来选。
第一条路是微信云开发。云端数据库、云函数、存储一条龙,不用自己买服务器,不需要域名备案,对新手极其友好。如果你点开微信开发者工具发现"云开发"按钮不见了,那可能是因为你的小程序账号没开通云开发服务,需要在云开发控制台创建环境才行。云开发的坑在于:项目做大了之后,集合之间的关联查询、事务操作会变得别扭,调试也相对受限。
第二条路是自己写接口服务。我用的是轻量级 Node.js 加 Express,配一个 MySQL 数据库,部署在一台小服务器上。这样做的好处是接口逻辑完全可控,订单状态机、库存扣减、支付回调这些业务希望在服务端自己写起来更顺手。
我最终选了自建接口。原因是外卖点餐系统的核心是订单流转,服务端逻辑占大头,我希望把控制权留在自己手里。云开发适合业务逻辑简单、以数据存取为主的应用,而"加购物车、下单、扣库存、改订单状态"这种带事务的操作,自建接口做起来更清晰。
2.3 整体架构与目录结构规划
技术方案确定后,第一步要画的是数据流图。我最担心的点有两个:购物车数据要在小程序端共享,服务端数据库要在订单状态变更时第一时间通知小程序端。端的整体架构是这样的:
小程序端(原生) ├── pages/home // 首页·商家列表 ├── pages/shop // 店铺页·菜品列表与分类 ├── pages/cart // 购物车页 ├── pages/order-confirm // 确认订单页 ├── pages/order-list // 订单列表页 ├── pages/order-detail // 订单详情页 ├── pages/user // 个人中心 ├── components // 自定义组件:商品卡片、购物车栏、SKU弹窗 ├── utils // 登录、请求封装、支付工具 └── store // 购物车全局状态管理 服务端(Node.js + Express + MySQL) ├── routes // 路由层:用户、商家、商品、订单、支付 ├── controllers // 控制层:参数校验、业务分发 ├── services // 业务层:下单事务、状态机流转 ├── models // 数据模型 └── utils // 微信登录、支付签名、回调验签为什么页面要分成这几层?因为外卖系统的用户路径其实是固定的:先逛店、再选菜、下单、付款、等餐、收到餐。每个环节对应页面,页面之间是强流程关系,不适合用 tab 页到处乱跳。另外,购物车那一栏在店铺页底部常驻,需要依赖全局状态,所以我把 store 单独拎出来,避免每个页面各存一份导致数据不同步。
3. 登录、点餐与购物车核心实现
3.1 用户登录与openid的获取流程
微信小程序与传统网页最大的不同是:它没有一个显式的账号密码登录框,而是以微信身份作为用户体系。用户打开小程序后,我们调用 wx.login 拿到一个临时 code,然后把 code 传给后端,后端带着 code、appid、appsecret 去微信接口换 openid。
有朋友问过:这个 code 是不是就是 token?不是。code 有效期很短,就这么一次使用机会,用完即焚。拿到 openid 后,后端应当用自己的逻辑生成 session/token 返回给小程序端,后续所有需要登录的请求都带这个 token,而不是每次都拿 code 去换 openid。服务端实现大概是这样:
// 小程序端 wx.login({ success: (res) => { // 拿到 code 后发给后端 wx.request({ url: 'https://api.example.com/auth/login', data: { code: res.code }, success: (res) => { const { token, userInfo } = res.data.data; wx.setStorageSync('token', token); // 保存登录态 } }); } });// 服务端 Node.js 实现 const appid = '你的appid'; const secret = '你的appsecret'; const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`; const res = await axios.get(url); const { openid, session_key } = res.data; // 用自己的逻辑生成 token,存到 session 表或数据库 const token = generateToken(openid);很多新手在这一步容易掉进一个坑:把 appsecret 直接写在代码里。小程序的代码包本质上是公开的,用开发者工具就能反编译出来,一旦 appsecret 被提取,别人就能冒充你的小程序操作接口。正确的做法是 appsecret 只放在服务端环境变量里,小程序端永远不涉及这个敏感信息。
关于登录态还有一个要注意的点:现在微信官方也在推手机号快捷登录、头像昵称填写能力,但外卖点餐对用户信息要求没那么严格,只要有个 openid 能区分用户是谁就够用了。如果你想让用户体验更好,可以把 wx.getUserProfile 得到的头像昵称上传到自己的数据库,展示在个人中心。我是做到二期才加的这个功能,前期主流程优先级更高。
3.2 商家与菜品数据懒加载
外卖点餐的首页是商家列表,一般的实现是进入首页直接请求商家列表接口,然后把所有商家和菜品一次性拉回来。这种实现方式简单,但有个问题:菜品一多数据包体积很大,弱网场景下拉取时间非常久。
我后来把回到首页的请求改成了懒加载:首页只拉商家基础信息和评分、起送价这些概览数据,等用户点进某个商家,再根据 shopId 拉取这个商家的完整菜品列表。这样首次进入页面的速度快很多,商家框架渲染出来之后,菜品数据异步到达,用户感知不到明显卡顿。
懒加载的实现并不复杂,在店铺页 onLoad 时根据路径参数带上 shopId 去请求即可:
Page({ data: { shopId: '', categories: [], products: [], currentCategoryId: '', loading: true }, onLoad(options) { if (options.shopId) { this.setData({ shopId: options.shopId }); this.fetchShopDetail(); this.fetchMenu(); } }, async fetchMenu() { const res = await request.get(`/api/shop/${this.data.shopId}/products`); const products = res.data; // 把菜品按分类整理,供左侧分类栏使用 this.setData({ categories: buildCategories(products), products, loading: false }); } });这里有个细节我想专门说一下:菜品数据的结构组织。外卖菜单是"左侧分类、右侧菜品"的经典布局,如果前端只按一个扁平的数组来存菜品,分类筛选每条 select 时都要重新遍历过滤,性能在小店场景能接受,但分类一多会明显卡顿。我自己是把数组预处理好,按 categoryId 分组成一个对象,切换分类时直接取对象里的数组,复杂度降成 O(1)。
3.3 购物车状态管理,最简单也最麻烦
购物车在外卖点餐系统里是个非常核心的状态管理问题。它和电商购物车不太一样:外卖购物车是要显示在店铺页底部的,点开之后有商品明细、有加号减号、有清空按钮,而且每家店的购物车是独立的。这意味着购物车数据必须在页面之间、组件之间共享,不能只存在某一个页面的 data 里。
微信原生的全局状态管理有几种做法:globalData、storage、自定义事件总线。我最后用的是 globalData 搭配事件订阅的模式。购物车的数据放 globalData,操作购物车时更新 globalData,同时触发一个自定义事件,店铺页底部购物车栏和侧滑购物车面板都会监听这个事件去刷新自己。
// store/cart.js const cart = { // 购物车数据:{ shopId: { productId: { product, count } } } data: {}, listeners: [], setItem(shopId, product, count) { if (!this.data[shopId]) this.data[shopId] = {}; if (count <= 0) { delete this.data[shopId][product.id]; } else { this.data[shopId][product.id] = { product, count }; } this.emit(); }, subscribe(callback) { this.listeners.push(callback); }, emit() { this.listeners.forEach(fn => fn(this.data)); } };为什么不用 storage 每次存?因为 storage 是异步读写的,高频加购操作容易产生读写竞争,也可能影响性能。globalData 是内存态,操作即时生效,等用户最终确认订单时再一次性写入 storage,作为数据兜底。
购物车还有一个隐藏逻辑:跨店清空。外卖不能同时在两家店下单,用户从 A 店加了菜品,再进 B 店加菜,A 店的购物车应该被清空。这个逻辑如果不做,用户等下单时才发现混乱。我是在 setItem 时判断传入的 shopId 和当前购物车里的 shopId 不一致,就先清空原来的。
3.4 菜品SKU和规格选择弹窗
点餐系统里常见的"规格"需求就是:一杯奶茶可以选择大杯、中杯、小杯,或者加冰、去冰。这个功能看起来简单,但实现时要注意的细节很多。你可能在小程序里见过那些点弹窗后菜品区分变形的界面,多半是弹窗组件没有处理滚动穿透。
我处理的方式是:每个配置项存一个规格分组,用户每次点击某个选项,就把这个分组的值记录下来,选完后把多个分组的值联合生成一个 skuId。之后加购物车都是针对这个确定的 skuId,比如 specName="大杯+去冰"。服务端在生成订单详情时,直接用 skuId 查找对应的价格、库存。
这里要提醒一下:规格弹窗的层级和滚动问题,在 iOS 上特别明显。弹窗打开后,底层的页面内容还会跟着手指滚动,这就产生了"滚动穿透"。解决办法是给弹窗组件加 catchtouchmove,或者设置 page-meta 的 page-style overflow: hidden。只给弹窗单独设 fixed 定位是不够的,必须处理底层页面的事件冒泡。具体代码可以这样写:
<view class="sku-mask" catchtouchmove="noop" wx:if="{{show}}"></view> <view class="sku-panel" catchtouchmove="noop" wx:if="{{show}}"> <!-- 规格选项 --> </view>noop() {}这样弹窗区域内的滚动不会传导到底层页面。苹果手机在微信小程序里不能进行滑动滚动这个问题,很多时候都和这种事件冒泡机制有关,后面我会专门在问题排查里再展开。
4. 下单、支付与订单状态流转
4.1 从购物车到确认订单页
购物车侧滑面板、店铺页底部购物车栏,这些交互做完之后,用户点结算,接下来进入确认订单页。这个页面主要做三件事:展示商品清单、选择收货地址、计算配送费和优惠。最后把这三块数据组织成一个订单请求体发送到后端。
确认订单页的数据处理有一个要小心的地方:商品信息、价格这类数据,不能直接信任小程序端传来的值,后端必须重新根据商品的 skuId 去数据库核对价格。原因很简单,小程序的页面和接口是可以被篡改的,一个熟练的用户完全可以通过抓包工具修改菜品价格。外卖点餐的体量,黑产未必盯上,但从一开始就养成"服务端校验所有价格数据"的习惯,总没有错。
我在这个页面做了金额的实时计算展示,因为涉及到配送费、打包费,直接在 data 里维护一个 totals 对象,每次商品数量变化或配送方式变化时调用 computed 方法重新计算。注意 setData 的粒度,不要一整块大对象刷,局部更新 totals.xxx 就行。
确认订单页还需要处理的一个是"备注"输入框。外卖系统中用户经常需要备注"不要辣""餐具多一份"等,这个字段在下单时随订单保存下来,商家端展示订单时会看到。这块逻辑不复杂,主要是我建议把备注字数控制在 50 字以内,超出部分截断,避免后期给商家打印小票带来格式问题。
4.2 后端统一下单与微信支付对接
微信支付可以算得上外卖点餐系统中小程序端最复杂的模块。不是代码本身难,而是流程繁琐、坑多,涉及小程序端、后端、微信支付服务端三方交互。
支付流程的核心链路是这样的:
- 小程序端把订单号发给后端,请求"预下单"接口。
- 后端查到订单,调微信支付的统一下单接口,传入 appid、mch_id、openid、订单金额、回调地址等参数。
- 微信支付返回预支付交易会话标识(prepay_id)。
- 后端把这个 prepay_id 和签名信息返回给小程序端。
- 小程序端拿这些参数调 wx.requestPayment,拉起支付面板。
- 用户输入密码,支付成功,微信支付服务端回调后端通知的接口。
- 回调接口收到支付通知,验签成功,将订单状态改为"已支付"。
第二步里涉及一个重要概念:签名。微信支付的所有接口都需要用商户密钥对参数做 MD5/HMAC-SHA256 签名,微信在收到请求后会验签。如果签名算法写的顺序不对、字符编码不对,或者 URL 编码的 key 没按字典序排列,都会报通信参数错误。
我在写签名工具时踩过一个很经典的坑:参数拼接时,数组空值字段的处理。按照微信官方要求,参与签名的参数值不能为空,所以要先过滤掉空数组项,再按 key 的字典序升序排列,拼成 query string,末尾加上&key=商户密钥,然后做 MD5,转大写。这一步细节很多,我建议直接用一个封装好的函数,别每次都手写。
function getSign(params, key) { const keys = Object.keys(params) .filter(k => params[k] !== '' && params[k] !== null && params[k] !== undefined) .sort(); const str = keys.map(k => `${k}=${params[k]}`).join('&'); return crypto.createHash('md5').update(`${str}&key=${key}`).digest('hex').toUpperCase(); }支付回调的地址必须是外网可访问的 HTTPS 地址,不能带端口。而且回调地址所在域名要求是已经备案的,这一点在测试阶段就很容易卡住。如果你只是本地开发,调试支付几乎不可能,我就是因为这个原因,在开发阶段先把支付逻辑留了 debug 开关:模拟支付成功的回调,等部署到测试服务器后再走真实支付流程。
4.3 订单状态机设计
点餐系统上线之后,订单状态的变更会比开发时想象中复杂得多。我第一次做的时候直接在 order 表加了一个 status 字段,各种 if else 去判断,越到后面越乱。后来重构的时候才认真设计了一套状态机。
我的订单状态分这几个关键节点:
| 状态 | 含义 | 可触发事件 | 目标状态 |
|---|---|---|---|
| PENDING_PAYMENT | 待支付 | 用户支付成功 / 超时未支付 | PAID / 自动关闭订单 |
| PAID | 已支付 | 商家接单 | PREPARING |
| PREPARING | 制作中 | 商家出餐 | DELIVERING |
| DELIVERING | 配送中 | 用户确认收货 / 骑手标记送达 | COMPLETED |
| COMPLETED | 已完成 | 无 | 终态 |
| CANCELLED | 已取消 | 无 | 终态 |
状态流转的原则是:每一步都只允许从一个状态到另一个确定的状态,其他非法跳转直接拒绝。比如已支付订单不能直接改为已完成,必须经过 PREPARING、DELIVERING。这个约束在服务端做了一层校验,防止前端在接口层乱传状态。
超时未支付自动关闭订单这个功能,最靠谱的实现方式不是用定时任务去轮询所有订单,而是利用微信支付的"关闭订单"能力和数据库里的时间判断。在用户下单时记录 created_at,前端轮询或后端定时任务发现超过 15 分钟未支付,就将状态改为 CANCELLED,同时调微信支付关闭订单接口防止用户再支付。这种需求和微信支付限时接口是配套的。
4.4 订阅消息通知用户
外卖点餐的一个重要体验是:用户下单后,商家接单、出餐,这些状态变化最好通过微信订阅消息通知用户,不然用户得一直盯着小程序页面刷新。
订阅消息在小程序端的实现流程是:
- 用户在小程序端授权"接收订单状态通知"。
- 后端在下单成功后调用微信的 subscribeMessage.send 接口,给用户发送订阅消息。
- 用户收到消息,点击可跳转到小程序对应页面。
这里需要注意:订阅消息模版需要在微信公众后台申请。不同行业类目可用的模版不一样,餐饮类目下一般有"订单状态提醒""商家接单通知"这些可用模版。申请时要填具体参数名,比如订单号、订单状态、提醒内容等。
我在开发阶段经常遇到一个问题:订阅消息发送成功了,但用户收不到。排查后发现是授权次数的问题:用户每授权一次,只意味着可以给用户发一次订阅消息。如果业务里有多个状态变更都要发消息,必须让用户多次确认授权,或者使用"总是保持以上选择"的长效订阅。这块逻辑要在产品层面设计好,不然一路发到第几条就断了。
5. 后端接口设计与数据库建模
5.1 数据表结构设计
外卖点餐系统的数据库核心表可以分成几组:用户相关、商家与商品相关、订单相关。我给出的具体表结构比较简洁,尽量不做过度设计,但每一张表都有它存在的理由。
用户表至少要有:openid、昵称、头像、手机号、创建时间。这里的 openid 是用户全局唯一的标识,配合用户自己在系统内维护的 token 使用。
商家表:商家名称、logo、评分、起送价、配送费、营业状态。现实中商家信息是商家后台维护的,个人开发可以先用 SQL 脚本初始化一批模拟店铺。
商品表:商家ID、分类ID、名称、价格、图片、月售量、库存、上下架状态。如果商品有规格,单独建一张 sku 表,关联商品 ID、规格描述、价格、库存。
订单表和订单明细表是外卖系统的重头戏。订单表放订单号、用户ID、商家ID、总金额、配送费、打包费、优惠金额、实付金额、收货地址、备注、订单状态、支付时间、完成时间。订单明细表放订单ID、商品ID、SKU信息、单价、数量、小计。
我建表的习惯是:所有核心业务表都加 created_at 和 updated_at 两个时间字段,这在排查数据和做统计报表时会非常有用。还有订单号一定不要用自增 ID 暴露给用户,订单号可以拼上时间戳和随机数,方便客服按订单号搜索。
5.2 订单接口与防并发处理
外卖点餐最典型的并发场景是:同一个 SKU 库存只剩 1 件,两个用户同时下单。如果不做任何控制,可能出现两个订单都扣掉了 1 件库存,数据账不平。
处理办法主要是两种:乐观锁和悲观锁。乐观锁是在商品表加一个 version 字段,更新库存时带上 version 条件,如果 version 对不上就说明数据被改过了,重新查库存逻辑再来。悲观锁是直接在数据库行上取锁,但在高并发外卖出单场景下,锁粒度太大会造成性能瓶颈。
我实际用的是"预扣库存"方案:下单时先锁定库存,支付超时释放库存。在服务端逻辑上,这里必须把"检查库存、扣减库存、创建订单"放在同一个数据库事务里,任何一步失败都整体回滚,保证数据一致性。这段伪代码可以说明思路:
await db.transaction(async (tx) => { // 1. 查 SKU 库存 const sku = await tx.select().from('sku').where({ id: skuId }).forUpdate(); if (sku.stock < count) throw new Error('库存不足'); // 2. 扣库存 await tx.update('sku').set({ stock: sku.stock - count }).where({ id: skuId }); // 3. 生成订单主表和明细 await tx.insert('order', { orderNo, userId, ... }); await tx.insert('order_item', { orderNo, skuId, count, ... }); });forUpdate()是把这个事务内选中的行锁住,事务提交前其他修改操作会阻塞,这是比较保险的方式。这里有一个进阶建议:如果后续要做秒杀类高并发场景,要引入 Redis 库存预扣异步队列,但外卖点餐的场景一般用不着。
5.3 RESTful 接口设计
API 设计我遵循的是 RESTful 风格,资源和操作分开。核心接口如下:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/shops | 获取商家列表 |
| GET | /api/shops/:id/products | 获取商家菜品 |
| POST | /api/orders | 创建订单 |
| GET | /api/orders | 获取当前用户的订单列表 |
| GET | /api/orders/:id | 获取订单详情 |
| POST | /api/orders/:id/pay | 发起支付预下单 |
| POST | /api/orders/:id/confirm | 用户确认收货 |
每个接口在我的服务端实现里都有统一的返回结构:
{ "code": 0, "message": "success", "data": {} }code 为 0 表示成功,非 0 表示错误。在"message"里带上用户能看懂的提示,比如"库存不足"。小程序端用一个统一的 request 封装处理 token 注入、错误码拦截、loading 展示,这样每个页面请求接口时就不用重复处理这些逻辑了。
在微信小程序里,所有请求域名都必须配置到小程序后台的 request 合法域名里,而且要求 HTTPS。开发调试阶段,可以在开发者工具里勾选"不校验合法域名",但真机预览和上线前必须把域名配置好。我踩过一个坑:本地开发用了 localhost 后端,开发者工具没问题,一用真机调试发现所有请求都失败了。查了半天,发现真机根本访问不了电脑的 localhost,需要把后端接口地址改成电脑在同一局域网下的 IP。如果你也是这种情况,把http://localhost:3000改成http://192.168.x.x:3000,并且保证手机和电脑连接同一个 Wi-Fi,就能解决。
5.4 地址管理模块
订单收货地址我做成用户在小程序端手动填写和维护。地址字段至少包括:联系人、手机号、所在地区、详细地址。微信小程序提供了wx.chooseAddress接口可以直接获取用户在微信里保存的收货地址,这个功能实现起来很简单,而且用户体验很好,用户不用在小程序里再填一遍地址。
要注意的是,wx.chooseAddress获取的地址格式和自家系统里存储的字段不一定完全吻合,需要做一层适配映射。用户在微信里维护的地址可能不一定更新,所以拿到地址后还是要允许用户在确认订单页编辑。
外卖配送还有一个常被忽略的需求:定位当前经纬度。用wx.getLocation获取用户当前位置,再把经纬度传给后端,由后端或地图服务计算配送范围。如果你接入的是高德地图,从微信小程序跳转到高德 App 进行导航,也是常见需求,这需要用到wx.openLocation;如果直接在小程序里调用地图组件,则不需要跳出去。这里常见的问题是,很多人在开发工具里拿到的定位是正常的,一到苹果真机上位置就偏了。这种情况通常是因为没在 app.json 里声明permission中的scope.userLocation用途说明,以及 iOS 上定位权限弹窗没正常弹出导致。在 app.json 里加上:
{ "permission": { "scope.userLocation": { "desc": "你的位置信息将用于配送范围校验" } } }6. 实操中遇到的高频问题与排查记录
6.1 真机调试请求无法到达后端
这个问题的表现形式是:在微信开发者工具里一切正常,但用预览扫码后在真机上打开,所有请求全部失败,Network 面板显示请求直接报错。
排查思路按优先级来:
- 确认后端地址是否为局域网 IP,而不是 localhost。
- 确认手机和电脑是否在同一局域网并可以互通。
- 确认小程序后台是否已配置 request 合法域名,并在开发者工具里"详情-本地设置"把"不校验合法域名"勾上用于调试预览。
- 确认后端接口允许跨域,因为小程序请求默认要求后端开启 CORS 响应头。
我在这个坑上花了一个下午。最后发现是后端 Express 应用没配 CORS 响应头,浏览器端和小程序开发者工具对跨域的限制策略不一样,开发者工具对部分情况容忍度较高,但真机上是严格按照配置执行的。
6.2 苹果手机上不能滑动滚动,页面卡死
这个问题在小程序里很常见,尤其表现在 iOS 真机上。出现"苹果手机在微信小程序不能进行滑动滚动"的情况,通常原因有几类:
- 页面上某个元素意外获得了 focus,比如 input 组件,键盘弹起后页面滚动被锁。
- 某些 CSS 属性如
overflow: hidden误用在了page层。 - 自定义弹窗的
catchtouchmove触发了事件上浮,底层页面无法滚动。
排查时先确认是不是所有页面都卡死,还是只有特定页面。如果是特定页面,检查这页的page元素 CSS style 是否被设置过度滚动限制。弹窗组件要确保关闭时清理掉position: fixed的遮罩层。
还有一种隐蔽情况,scroll-view 作为外层容器时高度没有设置好,内容超出后 iOS 上的滚动惯性触发不了。给 scroll-view 设置固定高度,并且使用enable-flex属性配合 flex 布局时更要注意。
6.3 开发者工具里没有云开发入口
很多人第一次打开微信开发者工具,发现界面上找不到"云开发"按钮,以为是工具坏了。出现这种情况通常是:当前小程序项目不是云开发模板创建的项目,或者当前账号没有开通云开发环境。云开发是按环境计费的,需要先在小程序公众平台开通云开发服务,再在开发者工具里点云开发图标,按提示创建环境。如果你不是非要云开发,这套自建后端方案完全不受这个功能影响。
不过顺带说一句,我在开发阶段确实遇到一个情况:开发者工具更新版本之后,云开发的入口位置变了。如果在工具栏的缩略图标里找不到,可以试试顶部菜单的"工具"选项,有些版本把它收进了二级菜单。
6.4 基础库版本与兼容性问题
小程序基础库版本直接决定某些 API 和组件是否可用。微信开发者工具默认会使用最新基础库版本,但真机的用户不一定都升级了最新版。所以开发时要留意项目里用了哪些较新的 API,然后到小程序公众平台把最低基础库版本设置到一个合适的值。
我设置最低基础库版本时比较谨慎:如果某个 API 只在 2.20.x 以后才支持,而我的用户群里还有相当一部分人使用旧版本微信,那我宁愿换一种实现方式,也不盲目调高最低版本,避免用户一进来就白屏。
兼容性问题是隐形的,它不会在开发者工具里暴露。我用过最笨但有效的方式,就是借几台不同年代的老手机装体验版,实际跑一遍主流程。这东西比读文档效率高得多。还有一个技巧:在 app.json 的debug模式打开后,控制台会打印详细的页面跳转和 setData 日志,定位基础库相关报错会有帮助。
6.5 小程序审核与上线注意事项
小程序做完之后不是直接就能上线,微信公众平台的审核是很多初学者的噩梦。审核失败的原因常见的有这么几类:
- 涉及支付功能,但类目选择不对。外卖点餐必须选择餐饮服务类目,上传对应的资质文件。个人主体不能开通微信支付,这一点如果做商业项目就很痛,实际开发前一定要确认主体类型。
- 页面存在"测试"字样、未完善的占位内容。审核员会截图反馈,我第一次因为首页放了一个调试用的 banner 图片被判为"页面功能不完整"。
- 涉及用户隐私:如果使用了用户手机号、位置信息,必须在小程序后台配置《用户隐私保护指引》,否则真机调试或审核时会被拦截。隐私弹窗、公告等也要在页面里体现。
- 小程序名称和实际功能不符:外卖点餐的小程序,名称里如果包含"官方""旗舰"这类词容易被打回。
审核被拒别慌,重点是看拒绝原因截图和文字描述。我第一次被打回是因为没有隐私政策页面,后来在个人中心里加了一个静态的隐私协议页面,问题就解决了。审核时长一般是半天到两天,类目资质越清晰,通过越快。
6.6 常见问题速查表
最后整理一个我在开发全过程中遇到问题的速查表,方便你开发时快速定位:
| 问题现象 | 可能原因 | 处理办法 |
|---|---|---|
| 真机预览请求失败 | 后端地址使用了 localhost | 改为局域网 IP 或真实域名 |
| 请求报跨域错误 | 后端未配置 CORS | 服务端加响应头 |
| 系统提示 appsecret 泄露 | 前端代码中混入密钥 | 移除敏感信息,换成环境变量,重置密钥 |
| 购物车数量不同步 | 页面引用了局部变量 | 改用 globalData + 事件监听 |
| 弹窗滚动穿透 | 未阻止底层事件冒泡 | 遮罩层和弹窗加 catchtouchmove |
| iOS 无法滚动 | page 样式或 scroll-view 高度问题 | 检查样式和容器高度 |
| 支付失败报签名错误 | 参数或签名算法有问题 | 用官方签名校验工具比对 |
| 订单支付成功但状态未更新 | 回调地址不可达或验签失败 | 检查回调公网可达性 |
| 订阅消息收不到 | 授权次数已用完或模版参数错误 | 重新检查授权逻辑和模版字段 |
| 审核页面被拒 | 有"测试"字样或隐私协议缺少 | 补充完善页面和隐私声明 |
7. 项目的后续扩展方向
到这里,从 0 到 1 的外卖点餐小程序核心流程就完整了。但我每次做完一个项目都会复盘:这个项目做到什么程度算"完整",什么方向还能继续深挖?外卖点餐系统也不例外,后面可以做的方向还挺多的。
第一个方向是商家端。用户端只是外卖的一半,真正运营需要商家能管理菜品、上下架商品、接单改状态。商家端可以做成独立的小程序,或者用微信公众号内的 H5 页面来管理。我在开发时把商家接口都留好了,后面套一个管理后台模板就能用。
第二个方向是接单通知与打印小票。商家收到新订单后,如果能通过订阅消息收到通知,甚至对接云打印机自动出单,整个闭环会更接近商业系统。这块我还没深入做,但技术上无非是整合第三方打印服务商的接口。
第三个方向是营销玩法。满减活动、优惠券、新客立减、会员积分,这些玩法在外卖平台里几乎是标配。数据模型上要加优惠活动表、用户优惠券表,下单时先计算优惠金额,再算出实付金额,逻辑会复杂一些,但项目含金量会明显提升。
第四个方向是数据分析。点餐系统天然会产生订单数据,按时间维度统计商家销量、菜品排行、用户复购率等数据,是非常实用的功能。后端可以做一些聚合查询的接口,前端画折线图展示趋势。如果你对数据可视化感兴趣,可以在小程序里用 canvas 手写简单的折线图,或者引入第三方图表组件库。
我在做这个小程序的过程中,最大的体会是:写页面只是表象,真正花时间的地方都在数据流转和状态管理上。外卖点餐系统恰好把这些问题都覆盖了一遍,做完之后你再看其他行业的小程序业务,大多能互相迁移。如果你正在纠结用什么项目练手,这个选题不会亏。