大学食堂一到饭点就排长队,你想吃的档口永远挤满了人,外卖进不了校门,取个快递还得穿过整个生活区。这个需求憋到毕业设计或者接单的时候,就变成了我要说的这套"微信小程序校园自动点餐系统带跑腿"。它的定位很清晰:学生通过小程序提前选好菜品、预约时间、自动下单,食堂档口接单备餐;跑腿模块则处理拿快递、送文件、代买零食这类零散需求。后端用Python,小程序端用微信原生,前后端分离。这篇文章我会把从需求拆解、数据库设计、后端接口、前端页面,到微信支付v3对接和真机调试的完整过程写出来,还会专门整理一份实际开发中踩过的坑和排查记录,给正在做类似项目或者毕业设计的同学一份能直接照着抄的参考。
1. 项目定位与整体思路拆解
1.1 校园点餐+跑腿,解决的三个核心问题
先说清楚这个项目到底在解决什么问题。第一个是"到店排队时间不可控",上课到食堂的时间就那20分钟,高峰期点个炒饭排在十几个人的队伍后面很正常。第二个是"出餐时间无法预约",很多档口其实可以提前备餐,但缺少一个渠道让用户提前预定并把取餐时间错开。第三个是"校园内末端物流是空白",外卖只送到校门口,快递在驿站,文件在行政楼,这些"最后几百米"的需求一直没人做系统化承接。
自动点餐系统解决第一条和第二条,跑腿模块补充第三条。这两个业务放在同一个小程序里,互相导流:点餐的用户顺手发个跑腿单,跑腿的人完成后可能顺便在小程序里买饭。从运营角度看,两个业务的用户群体高度重叠,合在一起能降低获客成本。
但代价是系统复杂度上来了。点餐是标准的B2C模式,跑腿是C2C加轻担保交易,这两套逻辑在订单模型、结算方式、纠纷处理上都不一样。如果一开始不做区分,后面代码会非常拧巴。我当时的做法是把两个业务在订单表层面完全拆开,共用用户体系和支付通道,但各自的订单表、状态机、结算逻辑独立维护。
1.2 技术选型:为什么是微信小程序+Python
前端用微信小程序,这个基本没什么悬念。校园场景下用户不愿意为了点个饭专门下载App,小程序用完即走,不需要安装,转发群聊也方便。而且微信支付的闭环在小程序里是最顺的,用户已经习惯了在小程序里完成支付。
后端选Python不是因为它最强,而是因为它最适合这个场景。Flask框架搭一个前后端分离的API服务,一周时间就能把所有接口写完。Python在字符串处理、定时任务、支付签名这些环节都有现成库,写起来效率极高。如果同样的项目换乘SpringBoot,代码量至少翻一倍,对于单人开发或者两三个人组队的学生项目来说,开发速度是决定性因素。
有一个点很多人会忽略:为什么不用云开发?微信云开发确实能把后端和数据库都省了,但它有几个硬伤。第一是云函数冷启动在饭点高峰期会拖慢响应;第二是跑腿订单、钱包结算这类需要事务性操作逻辑不太适合在云函数里写;第三是如果你后面要接其他平台(比如独立App或网页端),云开发迁移成本很高。所以我还是选择了自己搭服务,前端小程序、后端Flask、数据库MySQL、缓存Redis,部署在一台轻量云服务器上。
1.3 系统功能模块的整体规划
整个系统规划成四个端:学生端小程序、商家端小程序、跑腿端小程序、Web管理后台。你可能觉得三个小程序太复杂了,其实商家端、跑腿端都可以通过小程序里的"角色切换"来实现,不一定非得是独立的线上包。我当时是做了一个包含全部页面的大包,用户登录后根据角色字段显示不同的TabBar和页面入口。这样做的好处是打包和审核都只有一套,用户不需要额外下载商家版或骑手版。
核心功能模块我列一下:
- 用户模块:微信登录、学号绑定、学生身份认证
- 点餐模块:菜品浏览、分类筛选、规格选择、购物车、预约下单
- 商家模块:档口管理、菜品上下架、接单出餐、营业时间设置
- 跑腿模块:发布跑腿需求、抢单接单、进度更新、送达确认
- 支付模块:微信支付v3、订单支付、退款、跑腿赏金结算
- 消息模块:订阅消息通知、订单状态变更推送、自动点餐提醒
- 管理端:用户审核、订单管理、数据统计、投诉处理
这套模块划分下来,项目的骨架就立住了。听起来功能不少,但真正核心的其实是订单流转和支付这两条线,只要这两块不出问题,其他模块都是锦上添花。
2. 需求分析与数据模型设计
2.1 四类核心角色与业务场景梳理
这系统里有四类人:学生用户、档口商家、跑腿人员、平台管理员。每类角色的核心诉求完全不同。
学生用户要的是"快"和"省心"。走进食堂打开小程序,几下点完餐,约定12点10分去取,到那刚好做好。跑腿场景里,用户发单要能快速描述清楚需求,"帮我去菜鸟驿站取一个快递,取件码2418,送到3号宿舍楼下",价格用户自己定。
档口商家的核心诉求是"减少沟通成本"。订单打印要清晰,味道要求、餐具数量这些备注不能漏。商家要能看到今日订单总量、预估备餐时间,最好还能按订单状态筛选,只看待出餐的。
跑腿人员的核心诉求是"顺路赚钱"。空闲的学生会在小程序里刷附近的跑腿单,距离近、赏金高的单子会被秒抢。跑腿端要有抢单提醒、路径规划入口(跳转外部地图)、送达拍照证明。
平台管理员需要的是"兜底能力"。订单异常要能介入处理,用户投诉要有通道,商家资质审核要能后台操作。还有一个隐藏需求:数据统计。哪家档口卖得最好、哪个时段的单量最多、跑腿热门区域在哪里,这些数据对后续运营非常重要。
2.2 核心表结构设计
数据库设计我建议用MySQL,字符集用utf8mb4,排序规则utf8mb4_unicode_ci。表结构上,我按业务模块拆分成四个Schema区:
- 用户与认证:user、student_identity
- 点餐业务:shop、food、food_spec、cart、order、order_item
- 跑腿业务:errand_order、errand_reply
- 支付结算:payment、refund、wallet、wallet_log
重点是订单相关表,我把点餐订单和跑腿订单完全分开。点餐订单表的核心字段:
CREATE TABLE `orders` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号:业务前缀+时间戳+随机数', `user_id` int(11) NOT NULL COMMENT '下单用户', `shop_id` int(11) NOT NULL COMMENT '档口/商家ID', `total_amount` int(11) NOT NULL COMMENT '总金额,单位分', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已接单 3备餐中 4待取餐 5已完成 6已取消 7退款中', `pickup_time` datetime DEFAULT NULL COMMENT '用户期望取餐时间', `remark` varchar(255) DEFAULT NULL COMMENT '备注:口味、餐具等', `cancel_reason` varchar(255) DEFAULT NULL COMMENT '取消原因', `is_auto` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否自动下单', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_shop_status` (`shop_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点餐订单表';金额字段我一律用int存分,不用float或decimal。为什么?浮点数在计算金额时会有精度丢失问题,虽然decimal能解决,但Python操作decimal要处处转类型,很烦。用int存分,前端展示的时候除以100转成元,后端所有计算都是整数运算,永远不会出现0.1+0.2不等于0.3这种问题。
跑腿订单表则完全不同,它多了一个"接单人"的概念。订单状态从"待接单"流转到"配送中"再到"已完成",中间还可能插入"取消""申诉"等状态。另外因为跑腿单的金额由用户自行设置,所以有一个price字段,后面还要加market_price来做对比,提示用户参考价,防止漫天要价。
菜品分类表有个小设计:用两个字段做层级。parent_id=0的是顶级分类(比如"快餐""饮品"),parent_id不为0的是二级分类("盖浇饭""炒饭")。这样平台运营可以灵活调整分类层级,而不需要改代码。
2.3 订单状态机与并发抢单约束
状态机是整个系统的灵魂。我吃过亏的是,早期图省事直接用status一个字段表示状态,后面越写越乱,各种状态不合法,比如用户都能把"已完成"的订单取消掉。后来老老实实做了状态机,用代码层面强制约束:
点餐订单的合法流转:待支付 -> 已支付 -> 已接单 -> 备餐中 -> 待取餐 -> 已完成。中间任意节点都可以进入已取消(用户主动取消、超时未支付自动关单、商家拒单)。退款中只能从已支付或已接单进入。每个流转动作都在service层做校验,不允许跨状态跳跃。
跑腿订单的抢单并发问题必须单独说。跑腿单被几十个人同时看到,大家同时点击"抢单",如果后端是「先select status再update」,在高并发下会卖出同一单,也就是两个人同时抢到。解决办法是用条件更新,让数据库自己保证原子性:
# 抢单的原子操作:只有status=0(待接单)的订单能更新成功 updated = ErrandOrder.query.filter_by( id=order_id, status=0 ).update({ "status": 1, "taker_id": current_user.id, "accepted_at": datetime.now() }) db.session.commit() if updated == 0: return error("手慢了,订单已被别人接走") else: # 更新成功,抢占到单 return success("抢单成功")这里的关键是update语句里的条件status=0。数据库的行级锁保证同一时刻只有一个事务能更新成功,其他事务更新影响行数为0,自然就不会重复派单。这个方案比用Redis分布式锁简单得多,对于单个服务实例完全够用。
3. 后端接口与核心业务逻辑实现
3.1 Flask项目结构与接口设计
Flask项目我用的是标准的工厂模式,目录结构如下:
project/ ├── app/ │ ├── __init__.py # 应用工厂 │ ├── extensions.py # db、redis等扩展实例 │ ├── controllers/ # 路由层 │ ├── services/ # 业务逻辑层 │ ├── models/ # 数据模型 │ ├── utils/ # 工具函数(签名、金额、分页等) │ └── config.py # 配置 ├── migrations/ # 数据库迁移 ├── run.py # 启动入口 └── requirements.txt接口设计遵循REST风格,统一返回格式{code, msg, data}。接口按模块分前缀:
| 模块 | 接口路径 | 说明 |
|---|---|---|
| 认证 | POST /api/user/login | 微信登录换token |
| 认证 | GET /api/user/identity | 获取/校验学号身份 |
| 点餐 | GET /api/shop/list | 档口列表 |
| 点餐 | GET /api/food/list?shop_id=1 | 菜品列表 |
| 点餐 | POST /api/order/create | 创建订单 |
| 点餐 | POST /api/order/auto/plan | 创建预约自动下单 |
| 点餐 | POST /api/order/pay | 发起支付 |
| 跑腿 | POST /api/errand/create | 发布跑腿单 |
| 跑腿 | GET /api/errand/nearby | 附近跑腿单 |
| 跑腿 | POST /api/errand/grab | 抢单 |
| 跑腿 | POST /api/errand/finish | 确认送达 |
有一个设计原则:controller层只做参数校验和结果返回,所有业务逻辑都放到service层。比如创建订单这个动作,它要检查菜品库存、计算金额、生成订单号、清空购物车、发消息通知商家,这好几件事不能堆在视图函数里,否则后面维护就是噩梦。
3.2 微信登录鉴权流程
小程序的登录流程官方文档已经讲得很清楚了,实际开发时容易出错的是token的生成和校验。前端调用wx.login()拿到code,后端拿着code去微信的jscode2session接口换openid和session_key。openid是用户的唯一标识,你可以用它查数据库,如果没有这个用户就自动注册。
@app.route("/api/user/login", methods=["POST"]) def login(): code = request.json.get("code") url = "https://api.weixin.qq.com/sns/jscode2session" params = { "appid": AppConfig.APP_ID, "secret": AppConfig.APP_SECRET, "js_code": code, "grant_type": "authorization_code" } resp = requests.get(url, params=params).json() openid = resp.get("openid") if not openid: return jsonify({"code": 400, "msg": "登录失败"}) user = User.query.filter_by(openid=openid).first() if not user: user = User(openid=openid, nickname="微信用户", avatar="") db.session.add(user) db.session.commit() # 生成自定义token,并缓存到Redis token = secrets.token_hex(32) redis_client.setex(f"token:{token}", 7 * 24 * 3600, str(user.id)) return jsonify({"code": 0, "data": {"token": token, "user": user.to_dict()}})token要自己生成,不要直接用微信给的session_key当token,那不安全也不可控。我习惯用secrets.token_hex(32)生成token,存Redis并设置7天过期。前端每次请求在Header里带Authorization: Bearer <token>,后端的装饰器从Redis里查token对应的user_id。
学号认证这个环节容易被忽略。校园服务的核心壁垒是"只有本校学生能用"。最开始我只做了简单的登录,结果社会上的人也能下单,商家没法判断来取餐的是不是本校学生。后来加了学号绑定:用户在"我的-身份认证"里输入学号和姓名,后端跟学校教务系统对不上,或者用学校统一身份认证接口校验。校验通过后用户增加verified=1标记,未认证用户不能下单,只能浏览。
3.3 自动点餐的定时下单逻辑
自动点餐是区分"普通点餐小程序"的关键。这个功能不是单纯的定时器,它包含三层设计:
第一层是预约下单。用户在晚上10点把明天的午饭选好,加入购物车,设置期望取餐时间12:10,选择"预约下单"。系统只创建一条预约记录,不会立刻生成正式订单。
第二层是定时任务检查。系统用APScheduler起一个后台任务,每30秒扫描预约表里有没有到点的预约记录。到了设定时间前10分钟,系统会向用户推送一条订阅消息,提醒"您的预定订单即将自动提交,如不需下单请点击取消"。如果用户没有取消,到点后自动创建正式订单并调用支付。
第三层是防误触机制。自动下单虽然省事,但如果用户忘记取消就会产生"被动消费"。所以预约记录里增加了双重保险:一个cancel_deadline字段记录最晚取消时间,过了这个时间就不能手动取消;另外如果用户账户余额不足或者支付失败,自动订单会进入"待支付"状态,并不会强制扣款,商家也不会接单。
from apscheduler.schedulers.background import BackgroundScheduler scheduler = BackgroundScheduler() def auto_submit_job(): """定时扫描预约订单,到点自动转为正式订单""" now = datetime.now() plans = AppointmentPlan.query.filter( AppointmentPlan.auto_time <= now, AppointmentPlan.status == "pending" ).all() for plan in plans: # 检查用户是否在取消时限内主动取消 if plan.status == "cancelled": continue # 创建正式订单 order = create_order_from_plan(plan) if order: plan.status = "submitted" plan.order_id = order.id else: plan.status = "failed" db.session.commit() # 推送订阅消息提醒用户 send_subscribe_message(plan.user_id, "您的自动点餐订单已提交", order.order_no) scheduler.add_job(auto_submit_job, 'interval', seconds=30) scheduler.start()这个定时任务有个坑:如果服务重启或者部署了多个实例,任务会重复执行或者错过执行。解决方法是加一个分布式锁,或者用@scheduler.scheduled_job配合misfire_grace_time参数。对于学生项目,单实例部署加上30秒扫描一次的频率已经足够稳定了。
3.4 跑腿订单接单防并发实现
跑腿订单的并发问题前面已经讲了条件更新的方案。这里补充另外几个业务细节:
接单后进度更新要有校验。跑腿员点击"我已取到货",后端要校验订单状态是"接单成功"而不是其他状态。跑腿员点击"送达确认",后端要校验当前登录用户就是接单人。这些校验逻辑都不能省,否则会出现A接了单、B把状态改成已完成的神奇BUG。
送达确认的凭证问题。为了防止纠纷,送达时要求跑腿员拍照上传,同时用户端也要点击"确认收到"。如果跑腿员上传了送达照片,但用户超过30分钟没确认,系统自动标记为"已送达"。这个自动确认机制要写在状态机里,用户协议里提前说明。
跑腿费的结算也很讲究。跑腿订单的金额分成三部分:用户支付的跑腿费、跑腿员实际获得的报酬、平台抽取的佣金。佣金比例建议按单算,比如10%,但设置一个最低佣金1元。结算不是实时的,而是走"钱包余额",订单完成满24小时后才能提现,这样能给纠纷处理留出时间窗口。钱包表单独设计,所有金额变动写流水,不能直接在余额字段上加加减减不留痕迹。
4. 小程序前端与关键页面实现
4.1 点餐页:菜单分类与规格单选框
点餐页是整个前端最复杂的部分。页面框架是左侧分类、右侧菜品列表的经典布局。左侧分类是scroll-view,右侧菜品列表也是scroll-view,两个滚动区域需要联动:点击左侧分类,右侧滚动到对应分类的位置;右侧滚动到某个分类时,左侧高亮对应的分类。
菜品规格选择用到了微信小程序的单选框组件radio-group。一个菜品可能有多组规格,比如"大份/小份""加辣/微辣/不辣"。每组规格都是一个radio-group,选中的结果汇总到selectedSpecs对象里:
<!-- 规格弹窗 --> <view class="spec-mask" wx:if="{{showSpec}}"> <view class="spec-panel"> <image src="{{currentFood.image}}" mode="aspectFill" /> <view class="food-name">{{currentFood.name}}</view> <radio-group wx:for="{{currentFood.specGroups}}" wx:key="name" bindchange="handleSpecChange" >const getNavBarInfo = () => { const windowInfo = wx.getWindowInfo() const menuRect = wx.getMenuButtonBoundingClientRect() // 状态栏高度 const statusBarHeight = windowInfo.statusBarHeight // 导航栏高度 = (胶囊按钮上边界 - 状态栏高度) * 2 + 胶囊高度 const navHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height // 胶囊按钮的右侧边距 const menuRight = windowInfo.windowWidth - menuRect.right return { statusBarHeight, navHeight, menuRight } }为什么要用胶囊按钮的位置来反推导航栏高度?因为不同机型的胶囊按钮位置不一样,iPhone X和iPhone 14 Pro Max的屏幕圆角、刘海高度都不同,用固定值适配不了所有机型。最稳妥的方法是读取胶囊按钮的边界坐标,结合状态栏高度算出导航栏的实际高度。
页面布局时,把padding-top设为statusBarHeight + navHeight,搜索框和导航按钮放在这个空间里。滚动区域的height也要减去导航栏高度,否则内容会被顶出可视区。
5. 微信支付v3对接与避坑指南
5.1 JSAPI支付的完整调用链路
微信支付v3和小程序支付硬相关的是JSAPI支付。完整调用链路是:前端下单请求 -> 后端创建订单 -> 后端调用微信支付v3下单API -> 拿到prepay_id -> 后端构造支付参数签名 -> 前端wx.requestPayment -> 支付完成 -> 微信服务器回调后端接口 -> 后端更新订单状态。
后端调下单API的代码:
import requests import json def create_jsapi_payment(openid, order): url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi" body = { "appid": AppConfig.WX_APP_ID, "mchid": AppConfig.WX_MCH_ID, "description": "校园点餐-订单" + order.order_no, "out_trade_no": order.order_no, "notify_url": AppConfig.PAY_NOTIFY_URL, "amount": { "total": order.total_amount, # 单位分 "currency": "CNY" }, "payer": { "openid": openid } } headers = { "Authorization": build_auth_header("POST", "/v3/pay/transactions/jsapi", json.dumps(body)), "Content-Type": "application/json", "Accept": "application/json" } resp = requests.post(url, json=body, headers=headers) return resp.json() # 里面包含 prepay_id这里的Authorization头是重点。微信支付v3要求用商户私钥对请求做签名,构建逻辑如下:
- 生成随机字符串nonce_str
- 把请求方法、请求路径(带query参数)、请求时间戳、nonce_str、请求body拼接成一个字符串
- 用商户API私钥(apiclient_key.pem)对拼接字符串做SHA256withRSA签名
- 把签名结果和微信支付平台证书序列号一起放进Authorization头
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding import time, uuid, base64 def build_auth_header(method, path, body): timestamp = str(int(time.time())) nonce_str = uuid.uuid4().hex message = f"{method}\n{path}\n{timestamp}\n{nonce_str}\n{body}\n" # 加载商户私钥进行签名 with open(AppConfig.APICLIENT_KEY_PATH, "rb") as f: private_key = serialization.load_pem_private_key(f.read(), password=None) signature = private_key.sign( message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256() ) signature_b64 = base64.b64encode(signature).decode("utf-8") return ( f'WECHATPAY2-SHA256-RSA2048 ' f'mchid="{AppConfig.WX_MCH_ID}",' f'nonce_str="{nonce_str}",' f'signature="{signature_b64}",' f'timestamp="{timestamp}",' f'serial_no="{AppConfig.API_CERT_SERIAL_NO}"' )这个签名逻辑是接入v3最常出错的地方。我建议直接把上面这段封装成工具函数,不要每次手写。如果签名不对,微信会返回401,错误信息里会提示"签名错误",排查的时候可以先验证四件事:时间戳是不是当前时间、nonce_str是否每次不同、私钥是不是商户API证书对应的私钥、证书序列号是不是从证书里解析出来的。
5.2 签名构造与回调验签
支付完成后,微信服务器会往你配置的notify_url发一个POST请求,内容是加密的支付结果。后端必须在回调里做两件事:解密通知数据、验证微信签名。
v3的回调通知是加密的,加密方式是AES-256-GCM,密钥是APIv3密钥(32位字符串),初始化向量是通知数据里的resource.nonce,附加认证数据是resource.associated_data。
from cryptography.hazmat.primitives.ciphers.aead import AESGCM def decrypt_notify_body(body): resource = body["resource"] aesgcm = AESGCM(AppConfig.API_V3_KEY.encode("utf-8")) plaintext = aesgcm.decrypt( resource["nonce"].encode("utf-8"), # nonce 作为IV base64.b64decode(resource["ciphertext"]), # 密文 resource.get("associated_data", "").encode("utf-8") ) return json.loads(plaintext) def pay_notify_handler(): body = request.get_data(as_text=True) headers = request.headers # 1. 验证微信签名 verify_wechat_signature(headers, body) # 2. 解密通知内容 notify_data = decrypt_notify_body(json.loads(body)) # 3. 处理业务:更新订单状态 out_trade_no = notify_data["out_trade_no"] transaction_id = notify_data["transaction_id"] update_order_paid(out_trade_no, transaction_id) # 4. 返回成功应答 return jsonify({"code": "SUCCESS", "message": "成功"})回调处理必须做幂等。微信回调可能会重试多次,如果每次回调都往订单表插一条记录或者重复修改状态,数据就乱了。幂等的做法是:处理前先查订单状态,如果已经是"已支付"就跳过更新,直接返回成功。
回调地址一定要用HTTPS,这是微信支付的硬性要求,还得在外网能访问到。开发调试的时候可以用内网穿透工具把本地服务映射到公网临时地址,联调完再换成正式域名。
5.3 支付权限审核的硬性门槛
这个坑很多同学做毕设不会遇到,但一旦打算上线或者接真实商户需求就会卡住。微信小程序要使用支付功能,需要满足几个条件:
第一,小程序主体必须是企业或者个体工商户,个人主体没有支付接口权限。第二,小程序服务类目要包含对应的行业资质。比如做餐饮点餐,需要提供食品经营许可证;做跑腿配送,需要提供相关的增值电信业务经营许可证或者物流类资质。第三,小程序提审的时候,审核人员会模拟下单流程,如果发现没有真实商品或者支付流程不完整,可能会驳回。
比较常见的场景是"由于小程序违规,支付功能暂时无法使用",支付功能被封禁。很多个人开发者图省事,随便找了一个资质不全的主体上线点餐功能,结果被判定违规,支付权限被收回。实际上,微信对于"虚拟支付"和"无资质的实物交易"查得很严。
应对办法有三个层级:
- 开发和演示阶段:用测试商户号(微信支付沙箱环境),配置好回调逻辑,但不发起真实扣款。
- 保证主体和类目合规:营业执照的经营范围必须包含餐饮服务或相关类目,食品经营许可证要提前办。
- 简化支付模型:对于学生项目、毕设展示,可以在后台配置"模拟支付开关",测试时走模拟支付不调微信接口。注意这个开关只能用于测试环境,上线正式环境必须关闭。
支付是强依赖外部权限的模块,一定要提前跟商户、跟微信公众平台确认类目,不要等到项目做完了才去申请,审核周期可能会拖死你的上线计划。
6. 真机调试与常见问题排查实录
6.1 常见报错速查表
我在实际调试过程中整理了下面这些高频报错,按频率排序:
| 报错信息 / 现象 | 可能原因 | 解决办法 |
|---|---|---|
| request:fail url not in domain list | 请求域名未配置到小程序后台 | 在小程序管理后台-开发设置-服务器域名里添加request合法域名 |
| 单选框点击后无反应 | radio-group的bindchange没触发 | 检查是否用e.detail.value取值,注意data-index用currentTarget取 |
| 自定义导航栏在不同机型上偏移 | 直接用固定px值算高度 | 改用wx.getWindowInfo() + wx.getMenuButtonBoundingClientRect()动态计算 |
| 页面空白,请求报405 | 前后端请求方法不一致 | 用开发者工具Network面板对比请求方法是GET还是POST |
| 真机可以,模拟器不行 | 使用了本地缓存或本地IP地址 | 真机无法访问localhost,必须用公网HTTPS地址 |
| 支付参数报商家参数格式错误 | 金额字段传了元而不是分 | 检查所有金额是否统一为分,避免float精度丢失 |
| 回调接收不到 | 回调地址不是HTTPS | 换HTTPS域名,并在后台配置好notify_url |
| 编译报错 Cannot read property of undefined | 数据还没返回就用了 | 模板里用wx:if判断数据存在再渲染 |
「url not in domain list」这个问题新手必踩。小程序对网络请求域名有白名单限制,只有在小程序后台配置的域名才能请求,而且必须是HTTPS。开发调试时可以在开发者工具里勾选"不校验合法域名",但真机预览时必须配置好。
6.2 PC端微信小程序抓包调试
小程序开发会遇到一个问题:模拟器里一切正常,真机上却有诡异的数据差异。这时候就需要抓包看真实请求。移动端抓包要在手机上配代理,很麻烦,而且现在微信客户端对安装证书管得严,抓到HTTPS内容特别费劲。但PC端微信小程序就简单多了,用Burp Suite就能搞定。
PC端打开微信,扫码登录后,点开小程序,小程序界面就是一个桌面窗口。此时小程序的网络请求走的是普通HTTP(S)流量。配置Burp Suite监听一个端口,比如8080,在微信给小程序配置网络代理指向本地的Burp监听端口,再安装Burp的CA证书到系统信任区,就能解密看到HTTPS请求。
抓包能帮你确认几个模拟器里看不到的问题:
- 真机上请求头带了什么额外信息
- 微信支付云控接口在不同环境下的差异
- 某些机型缓存的旧接口数据导致的显示异常
有一点要提醒:抓包只是开发调试的一部分,一定要在开发者工具的Network面板、后端日志、抓包工具三个数据源之间交叉对比。我遇到过前端说发送了请求、后端却说没收到的情况,最后抓包发现是请求被服务端防火墙拦截了。
6.3 视频组件与顶栏适配的小坑
点餐系统里如果有商家宣传视频,用的是video组件。如果你把video组件嵌在swiper里做轮播,iOS真机上全屏播放时会出现错位问题,视频画面偏到屏幕外,点退出全屏页面又抖一下。这个是iOS WebView对video全屏弹层的兼容性问题,微信官方一直没有彻底修复。
规避方案有两个:一是不要在swiper组件里直接嵌套video,改成覆盖式:用一个透明的swiper图片轮播盖在视频上,用户点击图片才跳转到独立页面播放视频;二是把video的show-fullscreen-btn属性设为false,禁用组件自带的全屏按钮,自己写一个全屏播放的模拟层。
自定义顶栏的小坑也顺带说一句。有的页面你会用position: fixed做顶部吸顶效果,但自定义导航栏高度算出后,别忘了给页面内容区加一个对应高度的padding-top。还有一种更隐蔽的情况:iPhone的键盘弹起会把页面顶起来,如果导航栏是fixed定位,它不受影响,但内容区会被键盘遮住,可以在app.json里配置"window": {"disableScroll": true},或者给输入框设置adjust-position属性来控制。
从我个人的实际经验来看,这个项目做到后面最花时间的不是写新功能,而是排查这些真机兼容性问题。所以强烈建议从一开始就用真机做阶段性验证,别看模拟器跑通了就以为万事大吉,很多让你头疼的BUG都是"模拟器复现不了、真机一戳就炸"的类型。真机上多戳几遍,比你后面统一排查省两倍时间。这个系统的核心链路是"选餐-预约-自动下单-支付-跑腿流转",把这套链路在真机上完整跑通,项目就稳住了一大半。