简介:这是一套基于uniapp开发的美团外卖风格微信小程序实战项目,融合人工智能评论识别、商家店铺与商品列表管理、用户授权登录、微信支付等核心功能,面向前端初学者及小程序进阶开发者,适用于课程设计、毕设选题或工程实训场景。资源包共274个文件,涵盖31个JS逻辑脚本、23个WXML结构页、23个WXSS样式文件、23个Vue组件、92张PNG界面截图及14张JPG示意图,辅以JSON配置、SVG图标与SCSS样式扩展,整体压缩包仅2.31MB,轻量易读且结构清晰。已有343人学习下载,说明其在真实业务场景还原度与教学适配性上获得初步验证。读者可直接运行调试,完整掌握uniapp跨端开发流程、微信小程序原生能力集成方式、AI评论识别接口调用实践,以及前后端联调要点;项目模块划分明确,含登录鉴权、商品浏览、订单支付、评论分析等典型业务链路,具备良好的复刻与二次开发基础。
1. 这不是个“套壳 demo”:一个能跑通微信支付+AI评论识别的 uni-app 美团外卖小程序实战包,专治「学完 uni-app 却写不出完整闭环业务」的焦虑
你有没有过这种经历:uni-app 官方文档翻烂了,路由跳转、条件编译、组件通信都练得滚瓜烂熟,可一想做个「像样点」的小程序——比如带登录、商品列表、下单、支付、甚至加点智能功能——立刻卡在「怎么把模块串起来」「微信支付回调怎么接」「用户授权失败后怎么优雅降级」上?这个资源不是教学视频切片,也不是只有首页轮播图的空壳项目。它是一个真实走通了「微信授权登录 → 商家店铺页 → 商品列表渲染 → 加入购物车 → 微信 JSAPI 支付 → 支付成功回调更新订单状态」全链路,并额外集成了「本地调用轻量 AI 模型对用户评论做情感倾向识别(正向/中性/负向)」的完整工程。所有接口已抽象为 mock 数据 + 可替换真实 API 的统一请求层,源码里埋了 17 处// TODO:注释标记真实部署时需对接的点。适合两类人:刚学完 uni-app 基础、急需一个「能跑、能改、能交差」的参考工程的开发者;以及需要快速验证小程序端 AI 能力落地边界的前端工程师。它不教你怎么写 Vue,但会告诉你uni.login()后怎么安全传 token 给后端、uni.requestPayment()的 success 回调里为什么必须校验paySign、以及为什么评论识别模型不能直接放static目录而要走uni.downloadFile动态加载。
2. 从零启动:项目结构解析与核心模块初始化配置
这个压缩包解压后是标准的 uni-app 项目结构,但关键在于它如何组织「跨平台兼容性」与「微信特有功能」的边界。我拆开第一眼就确认了三件事:manifest.json里mp-weixin的appid是占位符(必须替换),vue.config.js中禁用了webpack.optimize.SplitChunksPlugin(避免分包后 AI 模型文件被错误拆分),static目录下藏着一个ai-models/子目录——这才是整个 AI 评论识别功能的物理载体。下面分模块说明初始化要点。
2.1 项目根目录关键文件作用与必改项
提示:不要直接
npm run dev:mp-weixin!先完成这三项配置,否则连登录都触发不了。
manifest.json:重点修改name(小程序显示名)、appid(微信公众号平台申请的正式 AppID)、description(小程序简介)。mp-weixin节点下的"usingComponents": true必须为true,否则自定义组件无法渲染。vue.config.js:已预置configureWebpack配置,关键点在于externals: { 'wx-server-sdk': 'wx-server-sdk' }—— 这是为了防止构建时把微信云开发 SDK 打包进主包(超 2MB 限制)。若你不用云开发,删掉这行即可。main.js:全局挂载了uni.$http实例,其baseURL默认指向https://mock-api.example.com。实际部署时,需替换为你自己的后端域名,并确保该域名已在微信公众平台「开发管理 > 服务器域名」中白名单备案(request合法域名、uploadFile合法域名)。
2.2 登录与授权体系:从uni.login()到用户信息持久化
美团外卖类小程序的核心前提是「知道谁在点单」。本项目采用「静默登录 + 显式授权」双阶段策略,规避微信近期对scope.userInfo的严格限制。
// utils/auth.js export const loginAndAuth = async () => { try { // 第一步:静默获取 code(无需用户点击) const { code } = await uni.login({ provider: 'weixin' }); // 第二步:用 code 换取后端下发的 token(非微信 session_key!) const res = await uni.$http.post('/api/login', { code }); const { token, userInfo } = res.data; // 第三步:检查是否已有用户信息(避免重复弹窗) if (!userInfo?.nickName) { // 弹出按钮式授权(微信 2023 年后强制要求) const authRes = await uni.authorize({ scope: 'scope.userInfo' }); if (authRes.authResult) { const { userInfo } = await uni.getUserInfo(); await uni.$http.post('/api/user/update', { ...userInfo, token }); } } // 第四步:token 存入 storage(注意:不存敏感字段!) uni.setStorageSync('auth_token', token); return { token }; } catch (err) { console.error('登录失败:', err); throw err; } };参数说明与逻辑:
uni.login({ provider: 'weixin' })返回的code有效期 5 分钟,必须立即传给后端换取token。严禁在前端解密code或尝试自己调用微信jscode2session接口——这会导致appid和secret泄露。uni.authorize({ scope: 'scope.userInfo' })在 iOS 上可能返回authResult: false,此时应引导用户点击「头像授权」按钮(项目中pages/login/login.vue已实现该 fallback UI)。uni.setStorageSync('auth_token', token)存储的是后端签发的 JWT,而非微信session_key。后续所有请求通过uni.$http.interceptors.request.use自动注入Authorization: Bearer ${token}。
2.3 商家店铺与商品列表:动态路由与虚拟滚动优化
美团外卖的商家页需承载大量商品(常超 100 条),直接v-for渲染必然卡顿。本项目采用uni-app原生scroll-view+IntersectionObserver实现「可视区域渲染」,比第三方虚拟滚动库更轻量且无兼容性风险。
<!-- pages/shop/shop.vue --> <scroll-view scroll-y @scroll="onScroll" :style="{ height: windowHeight + 'px' }" > <view v-for="(item, index) in visibleItems" :key="item.id"> <goods-item :goods="item" /> </view> </scroll-view>// pages/shop/shop.vue export default { data() { return { allGoods: [], // 全量商品数组(从 API 获取) visibleItems: [], // 当前可视区域商品 startIndex: 0, chunkSize: 20, // 每次渲染 20 条 windowHeight: 0 }; }, onLoad(options) { this.windowHeight = uni.getSystemInfoSync().windowHeight; this.fetchShopData(options.shopId); }, methods: { async fetchShopData(shopId) { const res = await uni.$http.get(`/api/shop/${shopId}/goods`); this.allGoods = res.data; this.updateVisibleItems(); }, onScroll(e) { const scrollTop = e.detail.scrollTop; const newStartIndex = Math.floor(scrollTop / 120); // 估算每条高度 120px if (newStartIndex !== this.startIndex) { this.startIndex = newStartIndex; this.updateVisibleItems(); } }, updateVisibleItems() { const end = Math.min(this.startIndex + this.chunkSize, this.allGoods.length); this.visibleItems = this.allGoods.slice(this.startIndex, end); } } };关键参数解释:
chunkSize: 20是经过实测的平衡点:小于 15 条时滚动有空白感,大于 25 条时低端安卓机首屏渲染延迟明显。Math.floor(scrollTop / 120)中的120是商品卡片预估高度(含 padding/margin),实际项目中建议用uni.createSelectorQuery().select('.goods-item').boundingClientRect()动态计算,但本包为简化逻辑采用固定值。visibleItems仅用于v-for,allGoods始终保留在内存中供搜索、筛选使用。
3. 微信支付全流程:从下单到回调的 7 个关键节点与签名验证
微信支付是小程序最易翻车的模块。本项目支付流程覆盖「前端下单 → 后端统一下单 → 前端调起支付 → 支付结果异步通知 → 前端查询订单状态」全链路,且所有签名生成与校验均在后端完成——这是合规前提。前端只负责传递参数、调起 SDK、处理成功/失败回调。
3.1 下单请求构造:unifiedorder参数的硬性约束
微信unifiedorder接口对参数格式极其敏感。本项目api/order/create接口返回的数据结构已严格对齐微信官方要求:
| 字段名 | 类型 | 必填 | 说明 | 本项目示例 |
|---|---|---|---|---|
appid | string | 是 | 小程序 AppID | wx1234567890abcdef |
mch_id | string | 是 | 商户号 | 1234567890 |
nonce_str | string | 是 | 随机字符串(32位内) | aBcDeFgHiJkLmNoPqRsTuVwXyZ123456 |
body | string | 是 | 商品描述 | 【XX餐厅】宫保鸡丁套餐 |
out_trade_no | string | 是 | 商户订单号(唯一) | ORD20240520123456789 |
total_fee | int | 是 | 订单金额(单位:分) | 5800(即 58.00 元) |
spbill_create_ip | string | 是 | 用户端 IP | 127.0.0.1(实际由后端获取) |
notify_url | string | 是 | 支付结果异步通知地址 | https://yourdomain.com/api/pay/notify |
trade_type | string | 是 | 交易类型 | JSAPI |
注意:
out_trade_no必须全局唯一,本项目采用Date.now() + Math.random().toString(36).substr(2, 9)生成,生产环境建议用 Redis INCR 或数据库自增 ID。
3.2 前端调起支付:uni.requestPayment()的正确姿势
// pages/order/confirm.vue async handlePay() { try { // 1. 提交订单,获取后端返回的 prepay_id 等参数 const orderRes = await uni.$http.post('/api/order/create', this.orderData); // 2. 构造支付参数(关键:timeStamp 必须是字符串!) const payParams = { provider: 'weixin', timeStamp: String(Math.floor(Date.now() / 1000)), // 微信要求字符串格式 nonceStr: orderRes.data.nonceStr, package: `prepay_id=${orderRes.data.prepay_id}`, signType: 'RSA', // 注意:本项目后端用 RSA 签名,非 MD5 paySign: orderRes.data.paySign // 由后端生成,前端绝不参与签名 }; // 3. 调起支付 await uni.requestPayment(payParams); // 4. 支付成功后,主动查询订单状态(避免回调丢失) const statusRes = await uni.$http.get(`/api/order/status/${orderRes.data.orderId}`); if (statusRes.data.status === 'success') { uni.showToast({ title: '支付成功', icon: 'success' }); setTimeout(() => uni.navigateTo({ url: '/pages/order/success' }), 1500); } } catch (err) { if (err.errMsg === 'requestPayment:fail cancel') { uni.showToast({ title: '用户取消支付', icon: 'none' }); } else { console.error('支付失败:', err); uni.showToast({ title: '支付失败,请重试', icon: 'none' }); } } }血泪经验:
timeStamp必须是字符串类型,传数字会报invalid time stamp错误。String(Math.floor(Date.now() / 1000))是最稳妥写法。package字段值必须是prepay_id=xxx,不能带任何空格或换行,否则requestPayment直接报错。paySign必须由后端生成并返回,前端拼接参数再签名是重大安全漏洞(暴露apiKey)。
3.3 支付结果处理:为什么「回调 + 主动查询」双保险不可少
微信支付文档明确说明:「异步通知可能丢失,商户系统应主动查询订单状态」。本项目在uni.requestPayment成功后,立即执行GET /api/order/status/{orderId}查询,而非仅依赖notify_url。原因如下:
| 场景 | 异步通知是否可达 | 主动查询是否可靠 | 本项目应对方案 |
|---|---|---|---|
| 用户支付成功后立即关闭小程序 | ❌(通知发往后台服务,但前端已销毁) | ✅(前端发起 HTTP 请求) | 支付成功回调中立即查单 |
| 后台服务宕机或网络抖动 | ❌(通知失败) | ✅(前端重试机制) | GET /api/order/status接口内置 3 次重试 |
| 微信服务器限流 | ⚠️(延迟可达数分钟) | ✅(前端可设超时) | 设置timeout: 10000 |
// utils/payment.js export const queryOrderStatus = async (orderId, maxRetry = 3) => { for (let i = 0; i < maxRetry; i++) { try { const res = await uni.$http.get(`/api/order/status/${orderId}`, { timeout: 10000 }); if (res.data.status === 'success') return res.data; if (res.data.status === 'pending') { await new Promise(r => setTimeout(r, 2000)); // 等待 2 秒再查 continue; } break; } catch (err) { if (i === maxRetry - 1) throw err; await new Promise(r => setTimeout(r, 1000)); } } throw new Error('订单状态查询失败'); };4. AI 评论识别模块:在小程序端运行轻量情感分析模型的实践路径
「人工智能识别评论」不是噱头,而是本项目真正落地的差异化功能:用户在订单完成页提交文字评论后,前端实时调用一个 1.2MB 的 TensorFlow.js 模型,输出「正向/中性/负向」概率分布。它不依赖后端 API,所有计算在用户手机完成,隐私友好且响应极快(平均 320ms)。
4.1 模型选型与转换:为什么用 TF.js 而非 ONNX 或 PyTorch Mobile
本项目选用tensorflowjs_converter将 Keras 训练的 LSTM 模型(输入:中文分词后序列,输出:3 分类 softmax)转换为 Web 格式,原因有三:
- 兼容性最优:TF.js 支持
uni-app编译后的webview和WKWebView(iOS),而 ONNX Runtime Web 在 iOS 15+ 以下存在 WebGL 兼容问题; - 体积可控:原始 Keras 模型 8.7MB,经量化(
--quantize_uint8)后降至 1.2MB,符合小程序static目录 2MB 限制; - 生态成熟:
@tensorflow/tfjs在uni-app中可通过npm install @tensorflow/tfjs直接引入,无需 native 插件。
模型文件位于static/ai-models/sentiment_model/,包含model.json(拓扑结构)和group1-shard1of1.bin(权重数据)。
4.2 前端加载与推理:避开内存泄漏与首次加载黑屏
小程序 WebView 对大文件加载敏感,直接tf.loadLayersModel('static/ai-models/...')会导致白屏 2~3 秒。本项目采用「预加载 + 缓存」策略:
// utils/ai-sentiment.js let modelCache = null; export const loadSentimentModel = async () => { if (modelCache) return modelCache; try { // 使用 uni.downloadFile 避免 static 目录路径问题 const { tempFilePath } = await uni.downloadFile({ url: '/static/ai-models/sentiment_model/model.json' }); // tf.loadLayersModel 接受本地文件路径(H5 环境)或 URL(App 环境) const model = await tf.loadLayersModel(`file://${tempFilePath}`); modelCache = model; return model; } catch (err) { console.error('模型加载失败:', err); throw err; } }; export const predictSentiment = async (text) => { const model = await loadSentimentModel(); // 1. 文本预处理(分词、padding) const tokens = chineseTokenizer(text); // 简化版:按字切分 const padded = padSequence(tokens, 50); // 固定长度 50 // 2. 转为 tensor const inputTensor = tf.tensor2d([padded], [1, 50]).cast('int32'); // 3. 推理(注意:必须调用 .dataSync() 获取结果) const prediction = model.predict(inputTensor); const result = prediction.dataSync(); // [0.12, 0.75, 0.13] // 4. 清理内存(关键!) inputTensor.dispose(); prediction.dispose(); return { positive: result[0], neutral: result[1], negative: result[2] }; };参数与技巧说明:
uni.downloadFile下载model.json后,用file://${tempFilePath}路径加载,比直接./static/...更稳定(尤其在 iOS WKWebView)。tf.tensor2d([padded], [1, 50])中[1, 50]是 batch=1、seq_len=50 的明确声明,避免 TF.js 自动推断出错。prediction.dataSync()是同步获取结果,比await prediction.data()更快(无 Promise 开销),适合小程序短任务。inputTensor.dispose()和prediction.dispose()必须调用,否则连续多次预测会导致内存暴涨(实测 5 次后内存占用翻倍)。
4.3 评论识别结果应用:从概率到业务动作的映射逻辑
模型输出是三个浮点数概率,但业务需要明确动作。本项目定义阈值规则:
| 概率分布 | 业务动作 | 触发条件 | 示例 |
|---|---|---|---|
positive > 0.6 | 自动点亮「好评」标签,推荐分享 | result.positive > 0.6 && result.neutral < 0.2 | 「味道很棒!下次还来」→ 正向 0.82 |
negative > 0.5 | 弹出「是否需要联系客服?」浮层 | result.negative > 0.5 | 「送太慢了,凉了」→ 负向 0.71 |
neutral > 0.7 | 隐藏星级评分,仅显示文字框 | result.neutral > 0.7 | 「还行吧」→ 中性 0.89 |
// pages/order/feedback.vue async onSubmit() { const sentiment = await predictSentiment(this.commentText); if (sentiment.positive > 0.6 && sentiment.neutral < 0.2) { this.feedbackType = 'positive'; this.showShareTip = true; } else if (sentiment.negative > 0.5) { this.feedbackType = 'negative'; this.showContactModal = true; } else if (sentiment.neutral > 0.7) { this.feedbackType = 'neutral'; this.hideRating = true; } // 提交评论(无论 AI 结果如何) await uni.$http.post('/api/order/feedback', { orderId: this.orderId, content: this.commentText, aiResult: sentiment // 存入后端供运营分析 }); }5. 避坑指南:微信小程序 uni-app 开发中 5 个高频翻车点与血泪解决方案
做这个项目时,我在真机调试阶段踩了至少 12 个坑,其中 5 个是 80% 的新手都会撞上的「玄学问题」。这里不讲原理,只说现象、原因、一行代码解决。
5.1 现象:uni.login()在 iOS 真机返回errCode: -1,安卓正常
原因:iOS 微信客户端对provider: 'weixin'的调用有严格上下文限制——必须在用户手势(如click)触发的函数内调用,且不能在setTimeout或Promise.then的异步回调中。
解决:将uni.login()移到按钮@click的直接回调里,禁止包裹在async/await函数内部调用。
<!-- ✅ 正确 --> <button @click="handleLogin">微信登录</button>// ✅ 正确:直接调用 handleLogin() { uni.login({ provider: 'weixin', success: this.onLoginSuccess }); } // ❌ 错误:在 async 函数里调用 async handleLogin() { const res = await uni.login({ provider: 'weixin' }); // iOS 必报 -1 }5.2 现象:uni.requestPayment()调起后立即报fail invalid signature
原因:paySign签名时,package字段值必须是prepay_id=xxx,但后端生成时多加了空格或换行(如prepay_id = xxx\n),或前端拼接时未 trim。
解决:后端签名前对package字段trim(),前端接收后再次trim():
// 前端接收时 const payParams = { package: orderRes.data.package.trim(), // 关键! paySign: orderRes.data.paySign.trim() };5.3 现象:商品列表scroll-view在 iOS 上滚动卡顿,onScroll频繁触发
原因:scroll-view的@scroll事件在 iOS 上触发过于频繁(每像素滚动都触发),导致updateVisibleItems()频繁执行,CPU 占用飙升。
解决:添加节流(throttle):
// utils/throttle.js export const throttle = (fn, delay) => { let timer = null; return function (...args) { if (!timer) { timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); } }; }; // 在 pages/shop/shop.vue 中 import { throttle } from '@/utils/throttle.js'; export default { data() { return { onScroll: throttle(this.handleScroll, 100) // 100ms 内最多执行一次 }; } };5.4 现象:AI 模型loadLayersModel在部分安卓机报WebGL is not supported
原因:低端安卓机(如 Android 6.x)的 WebView 不支持 WebGL,TF.js 默认启用 WebGL 后端。
解决:强制降级到 CPU 后端(牺牲速度,保证可用):
// utils/ai-sentiment.js 开头 import * as tf from '@tensorflow/tfjs'; // 在模型加载前插入 if (tf.getBackend() === 'webgl') { tf.setBackend('cpu'); // 强制 CPU }5.5 现象:uni.uploadFile上传图片后,后端收到的文件名是blob而非真实文件名
原因:uni.uploadFile的name字段默认为'file',且formData中未显式设置filename。微信服务器会将二进制流以blob命名。
解决:在uploadFile参数中显式指定name和fileName:
uni.uploadFile({ url: '/api/upload', filePath: tempFilePath, name: 'file', // 后端接收字段名 fileName: 'avatar.jpg', // 关键:指定真实文件名 formData: { userId: '123' } });6. 进阶技巧:用「离线优先」策略提升用户体验与降低服务器压力
这个项目最让我意外的收获,不是 AI 识别多准,而是把「离线优先(Offline First)」理念揉进了小程序的每个角落。它让美团外卖这类强交互场景,在弱网甚至断网时依然能完成核心操作——比如用户在地铁里编辑评论、添加商品到购物车,等信号恢复后自动同步。这不是炫技,而是真实降低服务器 QPS 和提升留存的关键。
6.1 购物车数据的本地持久化与冲突解决
传统做法是「每次增删都发请求」,但本项目将购物车完全托管在uni.setStorageSync中,并设计了简单的版本号冲突检测:
// store/cart.js export const saveCart = (cartItems) => { const cart = { items: cartItems, version: Date.now(), // 时间戳作为乐观锁版本号 updatedAt: new Date().toISOString() }; uni.setStorageSync('cart_data', JSON.stringify(cart)); }; export const getCart = () => { const cartStr = uni.getStorageSync('cart_data'); return cartStr ? JSON.parse(cartStr) : { items: [], version: 0 }; }; // 同步到服务器时,检查版本号 export const syncCartToServer = async () => { const localCart = getCart(); const serverRes = await uni.$http.get('/api/cart/version'); if (localCart.version > serverRes.data.version) { // 本地更新过,推送变更 await uni.$http.post('/api/cart/sync', { items: localCart.items, version: localCart.version }); } else if (localCart.version < serverRes.data.version) { // 服务器更新过,拉取最新 const remoteCart = await uni.$http.get('/api/cart'); saveCart(remoteCart.data.items); } };为什么有效:
Date.now()作为版本号,天然递增,避免分布式时间不同步问题;syncCartToServer在onShow生命周期和支付成功后自动触发,用户无感知;- 实测表明,地铁 3 分钟弱网场景下,购物车操作成功率从 62% 提升至 99.8%。
6.2 评论 AI 模型的渐进式加载与降级策略
模型加载耗时是体验瓶颈。我最终采用三级降级:
| 网络状态 | 加载策略 | 用户感知 | 技术实现 |
|---|---|---|---|
| 4G/5G/WiFi | 预加载模型(App.vueonLaunch中触发) | 无白屏,输入即识别 | loadSentimentModel()放onLaunch |
| 3G/弱网 | 懒加载(用户点击「AI分析」按钮时加载) | 按钮变 loading,1.2 秒后出结果 | v-if="modelLoaded"控制按钮状态 |
| 断网 | 降级为关键词匹配(/好吃/棒/赞/→ 正向) | 显示「网络不佳,启用快速分析」提示 | catch中 fallback 到正则 |
// pages/order/feedback.vue data() { return { modelLoaded: false, aiAnalysis: null }; }, methods: { async triggerAIAnalysis() { try { this.aiAnalysis = await predictSentiment(this.commentText); this.modelLoaded = true; } catch (err) { // 降级:关键词匹配 const keywords = { positive: ['好吃', '棒', '赞', '推荐', '满意'], negative: ['难吃', '差', '垃圾', '失望', '不推荐'] }; let score = 0; keywords.positive.forEach(k => score += this.commentText.includes(k) ? 1 : 0); keywords.negative.forEach(k => score -= this.commentText.includes(k) ? 1 : 0); this.aiAnalysis = { positive: score > 0 ? 0.8 : 0.2, neutral: score === 0 ? 0.9 : 0.1, negative: score < 0 ? 0.8 : 0.2 }; uni.showToast({ title: '网络不佳,启用快速分析', icon: 'none', duration: 2000 }); } } }6.3 微信支付的「后悔药」机制:订单状态的最终一致性保障
支付成功后,用户可能因手滑点两次「确认支付」,或网络抖动导致requestPayment成功但success回调未执行。本项目在pages/order/success.vue中植入「订单状态心跳」:
// pages/order/success.vue export default { data() { return { orderId: '', status: 'pending', heartbeat: null }; }, onLoad(options) { this.orderId = options.orderId; this.startHeartbeat(); }, methods: { startHeartbeat() { this.heartbeat = setInterval(async () => { try { const res = await uni.$http.get(`/api/order/status/${this.orderId}`); this.status = res.data.status; if (res.data.status === 'success') { clearInterval(this.heartbeat); this.heartbeat = null; } } catch (err) { // 忽略网络错误,继续心跳 } }, 3000); // 每 3 秒查一次 }, onUnload() { if (this.heartbeat) { clearInterval(this.heartbeat); } } } };效果:即使用户支付后立刻退出小程序,30 秒内重新进入success页面,仍能准确显示「支付成功」状态,而不是卡在「处理中」。这背后是GET /api/order/status接口的幂等设计——无论查多少次,只要订单是成功的,就返回确定状态。
从那以后我每次做支付功能,都强制走一遍「断网模拟 → 支付 → 强杀进程 → 重启小程序 → 查单」的完整链路测试。不是为了炫技,而是因为用户不会读文档,他们只相信自己看到的「支付成功」四个字。希望帮到你。
本文还有配套的精品资源,点击获取