简介:这是一套基于uniapp开发的美团外卖微信小程序实战源码,面向希望掌握小程序全栈开发与AI应用的学生及进阶开发者,可作为课程设计、毕业设计或项目立项的参考。项目覆盖商家店铺商品列表、授权登录、微信支付等核心电商流程,并融入人工智能评论识别功能,帮助理解从界面搭建到业务闭环的完整实现思路。压缩包共274个文件,约2.31MB,以png、jpg图片资源为主,配合json配置、js逻辑、vue组件、wxml与wxss页面样式,另有map、svg及少量md、scss、css文件,结构清晰便于按模块查阅。目前已有342人学习下载,具备一定参考热度。读者可从中获取小程序页面组织方式、支付与登录授权对接逻辑、AI评论识别接入思路,以及可运行、可修改复刻的完整代码基础,适合在此基础上扩展出个性化外卖或电商类应用。
1. 从一份外卖小程序源码说起:AI 评论识别到底能落地到什么程度
外卖类微信小程序的技术栈其实早就不是秘密,真正让开发者卡住的往往不是页面写不出来,而是评论区的语义识别、商家商品列表的渲染性能、授权登录的静默续期、微信支付的回调幂等这几件事凑在一起时,整个链路就开始互相打架。我最近拿到一份基于 uniapp 的外卖小程序工程,里面把 AI 评论识别、店铺商品列表、授权登录、微信支付这几块都串了起来,跑通之后发现它解决的核心问题很具体:让一个中小团队在不自建 NLP 服务的前提下,把用户评论自动分成好评、差评、投诉建议三类,并驱动商家侧的运营动作。
这套东西适合谁?适合已经会用 uniapp 写页面、但对微信生态的登录态管理和支付回调还不够熟的前端;也适合想给现有外卖类小程序加一层评论语义过滤的后端同学。它不适合指望开箱即用直接上线的人,因为 AI 识别那部分需要你自己接一个推理接口,源码里给的是调用骨架和数据结构,不是训练好的模型。下面我按实际跑通的顺序,把每个模块的选型理由、关键参数和踩过的坑讲清楚。
2. 授权登录与静默续期:uniapp 里怎么把微信登录态管明白
2.1 为什么不能只用 wx.login 换 openid
很多教程讲到微信登录就停在wx.login拿 code、后端换 openid 这一步,但外卖小程序有个硬需求:用户可能半小时前授权过,现在回来下单,你不能让他重新点一次授权弹窗。微信的登录态实际由三层组成——code换来的openid和session_key、前端持有的自定义 token、以及wx.checkSession能查到的会话有效性。只做第一层,用户切后台再回来就会掉登录。
常见做法是:前端在onLaunch里先调wx.checkSession,如果会话失效就静默走一次wx.login,把新 code 发给后端换新 token,整个过程不弹授权框。只有需要拿昵称头像时才调wx.getUserProfile,而且这个接口现在必须由用户点击触发,不能自动调。
2.2 登录态续期的代码骨架与参数说明
// utils/auth.js const TOKEN_KEY = 'mall_token'; const TOKEN_EXPIRE = 2 * 60 * 60 * 1000; // 2小时,与后端保持一致 export async function ensureLogin() { const cached = uni.getStorageSync(TOKEN_KEY); // 先看本地 token 是否还在有效期内 if (cached && Date.now() - cached.time < TOKEN_EXPIRE) { return cached.token; } // 本地过期,检查微信会话是否还有效 const sessionValid = await new Promise((resolve) => { wx.checkSession({ success: () => resolve(true), fail: () => resolve(false) }); }); if (!sessionValid) { // 会话失效,静默重新 login const { code } = await new Promise((resolve, reject) => { wx.login({ success: resolve, fail: reject }); }); const res = await uni.request({ url: 'https://your-api.com/auth/login', method: 'POST', data: { code } }); const token = res.data.token; uni.setStorageSync(TOKEN_KEY, { token, time: Date.now() }); return token; } // 会话有效但本地 token 过期,用旧 token 换新 token const res = await uni.request({ url: 'https://your-api.com/auth/refresh', method: 'POST', header: { Authorization: `Bearer ${cached.token}` } }); uni.setStorageSync(TOKEN_KEY, { token: res.data.token, time: Date.now() }); return res.data.token; }这段代码的关键在于三个判断顺序:先查本地 token 有效期,再查微信会话,最后才决定是重新 login 还是 refresh。TOKEN_EXPIRE这个值必须和后端签发时的过期时间一致,否则会出现前端认为有效、后端已经拒绝的尴尬情况。wx.checkSession的失败回调里不要直接弹授权,而是走静默 login,这是外卖场景下体验差异最大的一个细节。
2.3 授权登录里最容易翻车的两个点
第一个坑是uni.request在 uniapp 里对header的合并行为。如果你在全局拦截器里已经加了Authorization,又在单个请求里传了同名 header,不同版本的 uniapp 表现不一致,有的会覆盖有的会合并。稳妥做法是统一在拦截器里注入 token,业务代码不手写 Authorization。
第二个坑是wx.login的 code 只能用一次,且五分钟内有效。如果你在ensureLogin里并发调了两次,第二次的 code 换不到 openid。解决办法是在模块顶层加一个 pending promise,同一时刻只允许一个 login 请求在飞。
3. AI 评论识别接入:不训练模型也能做语义分类
3.1 选型:为什么用推理接口而不是本地模型
外卖小程序的评论量级通常在每天几百到几千条,这个量级完全没必要在本地跑模型。常见做法是接一个文本分类的推理接口,把评论原文发过去,拿回标签和置信度。源码里给的结构是{ content, score, label },label 分 positive、negative、complaint 三类。选推理接口的理由很直接:不用管模型部署、不用管 GPU、按量付费成本可控,而且分类规则调整时只改 prompt 或阈值,不用重新训练。
如果你非要在本地跑,也不是不行,但 uniapp 的运行环境是微信小程序,包体积限制 2MB 主包,塞一个量化后的文本分类模型基本不现实。所以我的建议是:前端只做展示和触发,识别逻辑全部放后端或云函数。
3.2 评论识别的调用链路与阈值参数
// services/commentAI.js const AI_ENDPOINT = 'https://your-api.com/ai/classify'; const CONFIDENCE_THRESHOLD = 0.75; // 低于此值归为「待人工复核」 export async function classifyComment(content) { if (!content || content.trim().length < 4) { return { label: 'neutral', score: 0, needReview: true }; } const res = await uni.request({ url: AI_ENDPOINT, method: 'POST', data: { text: content.trim(), // 限制长度,避免超长评论拖慢推理 max_len: 256 } }); const { label, score } = res.data; // 置信度不足时打上待复核标记,不直接展示分类结果 if (score < CONFIDENCE_THRESHOLD) { return { label, score, needReview: true }; } return { label, score, needReview: false }; }CONFIDENCE_THRESHOLD这个参数是整套识别里最需要调的。设太高,大量评论进人工复核队列,运营扛不住;设太低,误判直接展示给商家,会引起投诉。我的经验是先用 0.75 跑一周,统计待复核比例,如果超过 15% 就降到 0.7,如果误判投诉多就升到 0.8。max_len设 256 是因为外卖评论很少有超过这个长度的,截断后推理延迟能稳定在 300ms 以内。
3.3 识别结果怎么驱动商家侧动作
分类结果不是拿来好看的,要落到具体动作上。差评和投诉建议类评论,后端收到后应该立刻推一条订阅消息给商家,同时在商家后台的评论列表里置顶。好评类可以攒着做店铺评分展示。这里有个细节:needReview为 true 的评论不要直接推给商家,先进入运营复核池,避免误伤。
另外,评论识别和商品列表是联动的。如果某个商品的差评率连续三天超过阈值,可以在商品列表接口里给这个商品打一个「口碑预警」标记,前端渲染时用不同颜色区分。这个联动逻辑源码里没有直接给,但数据结构是留了口的,加一个warning字段就行。
4. 商家店铺商品列表:渲染性能与数据结构的取舍
4.1 列表接口的分页与字段裁剪
外卖小程序的商品列表和普通电商不一样,它要展示店铺信息、商品分类、商品图片、价格、月售、标签,数据量一大首屏就卡。常见做法是接口分页返回,每页 20 条,但字段必须裁剪。我见过有人把商品的详情描述、规格参数全塞进列表接口,结果一个请求返回 200KB,小程序解析都要一秒多。
正确的做法是列表接口只返回渲染必需的字段:id、name、price、image、monthly_sales、tags。详情字段等用户点进商品页再单独请求。源码里的列表接口结构就是按这个思路设计的,tags是个字符串数组,前端用join(' ')渲染成标签行。
4.2 长列表渲染的优化参数
// pages/shop/goods-list.vue 中的关键配置 export default { data() { return { goodsList: [], page: 1, pageSize: 20, hasMore: true, loading: false }; }, methods: { async loadMore() { if (this.loading || !this.hasMore) return; this.loading = true; const res = await uni.request({ url: 'https://your-api.com/shop/goods', data: { page: this.page, size: this.pageSize } }); // 用 concat 而不是 push 展开,避免大数组操作触发多次视图更新 this.goodsList = this.goodsList.concat(res.data.list); this.hasMore = res.data.list.length === this.pageSize; this.page += 1; this.loading = false; } } };pageSize设 20 是个平衡点。设 10 请求太频繁,设 50 单次渲染压力大。hasMore的判断用返回条数是否等于 pageSize,而不是靠总数字段,因为总数字段在高并发下可能不准。concat替代push展开是为了减少 Vue 的响应式触发次数,这个在长列表里体感很明显。
4.3 商品图片的懒加载与占位
商品列表里图片是最耗资源的。uniapp 的image组件支持lazy-load属性,但只在部分平台生效。稳妥做法是配合intersection-observer自己判断可视区域,或者用uni.createIntersectionObserver。占位图建议用纯色块而不是 loading 动画,因为动画会持续触发重绘,在低端机上反而更卡。
5. 微信支付回调:幂等、验签与订单状态机
5.1 支付流程的四个状态与回调幂等
微信支付的完整链路是:前端调uni.requestPayment拉起支付,后端收到微信的回调通知,更新订单状态,前端轮询或通过 WebSocket 感知状态变化。这里最核心的问题是回调可能重复推送,如果你的更新逻辑不是幂等的,就会出现一笔订单扣两次库存的情况。
幂等的实现方式是在订单表加一个transaction_id唯一索引,回调进来先查这个 id 是否已处理,已处理直接返回成功。源码里的订单状态机是pending -> paid -> delivered -> completed,回调只负责把pending改成paid,其他状态由业务逻辑推进。
5.2 回调验签与订单更新的代码骨架
// 后端回调处理伪代码(Node.js 风格) async function handlePayNotify(req, res) { const { transaction_id, out_trade_no, result_code } = req.body; // 第一步:验签,失败直接返回,不处理业务 if (!verifySign(req.body)) { return res.status(400).send('sign error'); } // 第二步:幂等检查 const existed = await db.query( 'SELECT id FROM orders WHERE transaction_id = ?', [transaction_id] ); if (existed.length > 0) { return res.send('success'); // 已处理,直接确认 } // 第三步:更新订单状态,带条件更新防止并发 const result = await db.query( 'UPDATE orders SET status = ?, transaction_id = ? WHERE out_trade_no = ? AND status = ?', ['paid', transaction_id, out_trade_no, 'pending'] ); if (result.affectedRows === 0) { // 状态不是 pending,说明已被其他回调处理 return res.send('success'); } // 第四步:触发后续业务,如通知商家、扣减库存 await notifyMerchant(out_trade_no); res.send('success'); }验签必须在业务处理之前,这是铁律。幂等检查用transaction_id而不是out_trade_no,因为同一笔订单可能因为用户重复支付产生多个 transaction_id。更新语句里的AND status = 'pending'是乐观锁,防止两个回调同时进来都通过幂等检查后重复更新。
5.3 前端支付结果确认的轮询策略
前端调完uni.requestPayment后,success 回调只代表用户完成了支付动作,不代表后端已经收到回调。所以前端需要轮询订单状态。轮询间隔建议 1 秒、2 秒、3 秒递增,最多轮 5 次,超过就提示用户稍后在订单列表查看。不要用固定 500ms 轮询,会给后端造成不必要的压力。
6. 避坑与排查:这套组合拳里最容易出问题的五个地方
6.1 登录态和支付态不同步
现象:用户支付成功后,订单页显示未登录,要求重新授权。原因:支付回调更新了后端订单,但前端本地 token 刚好过期,ensureLogin重新 login 后拿到的 openid 和支付时的 openid 不一致(极少数情况是用户切换了微信号)。解决:支付前强制调一次ensureLogin,确保 token 有效;订单查询接口用out_trade_no而不是用户身份来查,避免登录态影响订单可见性。
6.2 AI 识别接口超时拖垮评论列表
现象:评论列表加载很慢,有时直接超时。原因:评论列表接口里同步调了 AI 识别,每条评论都等推理结果。解决:识别改成异步,评论先展示原文,识别结果通过 WebSocket 或轮询补上。或者后端做缓存,同一条评论只识别一次。
6.3 商品列表分页重复数据
现象:上拉加载时出现重复商品。原因:page递增和请求返回的顺序不一致,或者后端分页用了offset但数据在请求间隙有新增。解决:用游标分页替代 offset 分页,或者前端用id去重。源码里用的是 offset,数据量不大时没问题,但商家上新频繁时要注意。
6.4 支付回调验签失败但日志看不出原因
现象:回调一直返回验签失败,但本地用同样的参数验签能过。原因:微信回调的 body 是原始字符串,如果你用了框架的 body parser 先解析成对象再验签,签名就对不上。解决:验签必须用原始 body 字符串,在 body parser 之前拦截。
6.5 授权登录弹窗在 iOS 和安卓表现不一致
现象:安卓上wx.getUserProfile正常弹窗,iOS 上没反应。原因:iOS 对用户手势触发的要求更严格,如果弹窗调用被包在 async 函数里,手势上下文丢失。解决:把wx.getUserProfile放在按钮的@click里直接调,不要经过 await。
7. 进阶技巧:用评论识别结果反哺商品排序
跑通基础链路之后,我习惯做一件事:把评论识别的结果聚合到商品维度,做一个简单的口碑分,然后把这个分数混进商品列表的排序权重里。具体做法是每天凌晨跑一次批处理,统计每个商品近 7 天的评论标签分布,差评率低于 5% 的加 0.1 分,高于 20% 的减 0.2 分,分数存到商品的rank_score字段。列表接口排序时用rank_score DESC, monthly_sales DESC。
这个技巧的价值在于它让 AI 识别不只是个展示功能,而是真正影响了流量分配。参数上,差评率的阈值和加减分幅度需要根据店铺实际评论量调,评论少于 10 条的商品不建议参与,否则统计噪声太大。批处理的执行时间选在凌晨是因为这时候数据库压力小,而且评论数据已经稳定。
验证方法很简单:跑两周后对比一下高口碑商品和低口碑商品的点击率、下单转化率,如果高口碑组的转化明显更好,说明排序权重起作用了。我自己的经验是,这个改动对转化率的提升通常在 3% 到 8% 之间,取决于品类。但别指望它立竿见影,评论数据的积累需要时间,冷启动阶段效果不明显是正常的。
最后说个血泪教训:评论识别的阈值和排序权重不要同时调,否则你根本不知道是哪个改动带来的变化。一次只动一个参数,观察一周再动下一个。希望帮到你。
本文还有配套的精品资源,点击获取