每年毕设选题季,总有学弟学妹抱着手机来问我同一个问题:“小程序做什么题目比较好过,最好还能附带源码?”我给的回答里一直有一个高频选项——本地美食推荐类小程序。这次要说的“面向江西美食推荐的微信小程序——毕设附源码51545”,就是这类项目里很典型的一个。它没有多高深的技术,但胜在业务链路完整、数据好整理、演示效果好,从前端界面到交互逻辑再到本地存储全部能串起来,老师问什么都有东西答。这篇文章就把从选题、设计、开发到答辩的完整经验写出来,适合正在纠结毕业设计选题、准备用微信小程序当主项目的同学参考。
1. 选题逻辑:为什么“江西美食推荐”是毕设的稳妥答案
1.1 从毕设评分维度反推选题价值
很多同学选毕设题目,第一反应是“我要做一个很酷的东西”,但站在评审老师的角度看,毕业设计考察的是几个非常务实的东西:工作量够不够、业务逻辑是否完整、技术点是否有展示空间、演示是否流畅。
美食推荐类小程序在这几个维度上几乎都能拿分。数据层面,美食天然有内容可写——菜品名称、所属地市、辣度、价格、评分、推荐理由,这些字段组合起来就是一张体面的数据表。业务层面,“浏览菜品→查看详情→搜索筛选→收藏→管理收藏”是一条完整的用户动线,每一步都有明确的交互反馈。技术层面,一个正经的小程序至少涉及页面路由、数据绑定、事件处理、本地存储、条件渲染、列表分页等知识,这已经足够覆盖本科毕设要求的知识点跨度。
“江西”这个地域限定也不是随便选的。地域特色越强的选题,内容库越不容易空。赣菜本身以鲜辣、香浓著称,南昌拌粉、瓦罐汤、莲花血鸭、三杯鸡这些菜品自带认知度,评委看演示的时候容易产生共鸣。相比之下,泛泛的“美食推荐”反而难做,因为内容太杂,反而没有记忆点。
1.2 江西美食的内容地图与功能规划
做这类项目,第一步不是写代码,而是先把内容地图画出来。我当时把江西各地市的代表性美食整理成了表格,这一步直接决定了后面数据模型长什么样。
| 地市 | 代表菜品示例 | 菜品类型 |
|---|---|---|
| 南昌 | 南昌拌粉、瓦罐汤、藜蒿炒腊肉 | 主食/汤品/热菜 |
| 赣州 | 赣南小炒鱼、三杯鸡 | 热菜 |
| 萍乡 | 莲花血鸭 | 热菜 |
| 九江 | 九江茶饼、庐山石鸡 | 小吃/热菜 |
| 景德镇 | 瓷泥煨鸡、冷粉 | 热菜/主食 |
| 吉安 | 永新血鸭、艾米果 | 热菜/小吃 |
| 上饶 | 婺源汽糕、糊豆腐 | 小吃/热菜 |
| 抚州 | 临川牛杂、南丰蜜桔 | 小吃/水果 |
内容是骨架,功能是血肉。整理完内容后,我给项目定了六个功能模块:首页推荐流、分类浏览、搜索、菜品详情、收藏管理、个人中心。这里没有做得太复杂,刻意砍掉了评论、下单、支付这些超出毕设合理范围的功能——加了反而容易暴露逻辑漏洞,答辩时被追问到答不上来。
2. 技术选型与工程结构:原生开发比跨端框架更适合做毕设
2.1 原生小程序和 uni-app 的取舍
现在打开任何一篇小程序教程,都会有人告诉你用 uni-app、Taro 这类跨端框架,理由是“一套代码多端复用”。这个说法对商业项目成立,但对毕设来说未必是最优解。
我当时在原生微信小程序和 uni-app 之间犹豫过,最后选了原生。原因有三点。第一,答辩现场评委大概率只关心你是否理解代码逻辑,原生项目的Page()、setData、wx.request这些概念都是微信官方文档原话,讲起来不容易被质疑。第二,原生开发者工具的“编译预览”“真机调试”“性能面板”都是开箱即用的,省去了配置调试环境的功夫。第三,毕设源码交付时,老师可能会拿过去自己打开看,原生项目用微信开发者工具直接导入就能跑,不依赖 node_modules 那一堆依赖,容错率高得多。
| 对比维度 | 原生微信小程序 | uni-app |
|---|---|---|
| 上手门槛 | 低,直接按文档写 | 中,需要理解 Vue 语法 |
| 调试工具 | 微信开发者工具自带 | 需要配合 HBuilderX |
| 源码可读性 | 高,结构直白 | 中,多包一层编译逻辑 |
| 多端复用 | 不支持 | 支持 |
| 答辩讲解成本 | 低 | 稍高 |
当然,如果你已经熟练掌握了 Vue,用 uni-app 也不是不行。只是对一个追求“稳定过审、顺利完成”的毕设来说,原生的稳妥优势明显更大。
2.2 数据模型设计:美食推荐的核心字段怎么定
小程序的数据交互路径决定了数据模型的设计方式。我的做法是:先用本地data目录放一份 JSON 作为静态数据源,让项目跑通全流程;后续如果老师问“数据能不能换”,再平滑切换到云开发数据库。这里先说数据库集合的字段设计,因为这是整套推荐的基石。
{ "_id": "dish_001", "name": "南昌拌粉", "region": "南昌", "regionCode": "360100", "category": "主食", "spicyLevel": 2, "price": 12, "rating": 4.8, "imageUrl": "/assets/images/nanchang-banfen.jpg", "tags": ["早餐", "本地人推荐", "便宜大碗"], "description": "南昌人一天的开始,往往从一碗拌粉开始。", "recommendReason": "游客到南昌的第一顿推荐,粉条筋道,萝卜干和花生米是灵魂。", "createTime": 1600000000 }每个字段都是有讲究的。spicyLevel用来做口味筛选,江西人吃辣,外地人未必扛得住,这个字段让筛选功能有了真实意义。tags数组用来做关键词匹配搜索,也方便首页按标签猜你喜欢。regionCode用行政区划编码,为以后按城市定位推荐预留了扩展空间。rating是排序依据,首页默认流就可以按它排。recommendReason这块文案是点睛之笔——同样是菜品,有推荐理由的数据比光秃秃的菜名看起来专业得多。
2.3 源码工程目录与前端的骨架搭法
拿到源码第一件事,应该是先看目录结构。整个工程沿用了微信小程序的标准分包思路,核心页面全部放在pages下,公共资源抽到assets,业务数据集中在data目录。
├── pages │ ├── index # 首页:推荐流 + 分类入口 │ ├── list # 列表页:分类浏览 + 分页加载 │ ├── detail # 详情页:菜品介绍 + 收藏按钮 │ ├── search # 搜索页:关键词匹配 │ └── mine # 个人中心:收藏列表 ├── data │ └── dishes.js # 江西美食数据源 ├── assets │ └── images # 菜品图片 ├── utils │ ├── api.js # 接口请求封装 │ └── auth.js # 登录态处理 ├── app.js ├── app.json └── app.wxssapp.json是路由的注册中心,新页面必须在这里登记才能跳转。很多第一次写小程序的人在这个文件上踩坑,页面写好了一编译报错“页面文件未找到”,十有八九是忘在pages数组里注册了。关于跳转还有一个细节:wx.navigateTo只能跳转非 tabBar 页面,像首页这种 tabBar 页面要用wx.switchTab,如果混用了会没有任何反应,这也是一个典型的低级错误。
3. 核心页面与推荐流程的实现拆解
3.1 首页:推荐流是怎么“推荐”出来的
首页是这个项目的门面。我设计了三个区块:顶部轮播图展示精选菜品,中间是分类导航(按地市、按菜品种类、按辣度三组标签),底部是“猜你喜欢”推荐卡片流。
推荐逻辑没有做得很玄乎,而是刻意用了可解释的规则:每次进入首页,系统从数据源里随机取 6 条菜品作为“今日推荐”,再按评分从高到低取 10 条作为“人气榜单”。随机逻辑用一行Math.random()就能实现,但为了让同一天内刷新页面不觉得重复,我按日期生成了一个随机种子,当天随机序列固定,第二天自动换一批新菜。这个细节在答辩演示时非常加分——评委看到下拉刷新后推荐内容变了,会自然产生兴趣,这时候你就可以顺势讲随机种子算法。
// utils/random.js function getDailyRecommend(list) { const day = new Date().toDateString(); let seed = 0; for (let i = 0; i < day.length; i++) { seed = (seed * 31 + day.charCodeAt(i)) % 997; } // 用固定种子打乱数组,保证同一天内结果稳定 const shuffled = list.slice().sort(() => { seed = (seed * 9301 + 49297) % 233280; return seed - 116640; }); return shuffled.slice(0, 6); }强调一下setData的性能习惯:小程序每次setData都会触发视图层重新渲染,首页一次性渲染十几张图片本身没问题,但要注意图片懒加载。image组件直接加lazy-load属性,页面滑动体验会明显改善,这个属性官方文档里写得很隐蔽,很多新手根本不知道。
3.2 列表页:上拉加载更多与分页的正确姿势
列表页承担的是“分类浏览 + 分页加载”功能。毕设数据量不大,但分页逻辑必须做出来,因为这是高频考点。我在页面里定义三个状态字段:page当前页码、pageSize每页条数、hasMore是否还有下一页。
onReachBottom是页面触底时触发的生命周期方法,在它里面做数据追加。这里有一个非常容易犯的错——没有做“防重复请求”处理。用户快速上拉两次,page可能连续加了两次,产生重复数据。我当时的处理是在请求前加一个loading布尔锁,锁住就 return,请求完成才解锁。另外要记得在onPullDownRefresh里把page重置为 1,否则下拉刷新后依然从第 3 页开始加载,列表会越刷越乱。
onReachBottom() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); const nextPage = this.data.page + 1; // 从本地数据源切片模拟分页 const nextList = getDishes(this.data.currentFilter, nextPage, this.data.pageSize); this.setData({ page: nextPage, dishList: this.data.dishList.concat(nextList), hasMore: nextList.length === this.data.pageSize, loading: false }); }3.3 搜索页:防抖与多维匹配
搜索页的核心不是 UI,而是匹配逻辑和性能处理。我在数据源里把name、region、tags三个字段拼成一个可搜索文本,用indexOf判断关键词是否命中。这样用户搜“南昌”能匹配到所有南昌菜品,搜“辣”能匹配到所有 tags 里含辣的菜,搜“拌粉”也能精确命中名字。
搜索框的bindinput事件触发频率极高,每敲一个字符都会触发一次。如果每次触发都去遍历全部数据,卡片多的项目会卡顿。我加了一个 300ms 的防抖函数,用户停下输入后再真正执行搜索。防抖属于那种“代码三行,但答辩能讲五分钟”的知识点,面试官和评委都很吃这一套。
3.4 详情页与收藏功能的本地存储方案
详情页的信息结构是:大图、名称、评分、地市标签、辣度标签、价格、推荐理由、详细介绍,底部放一个收藏按钮。这台页面的数据通过wx.navigateTo的 URL 参数传递菜品的id,然后在onLoad里从数据源中找到对应记录。这里要注意 URL 传参是字符串类型,数字型的id在 onLoad 里拿到的会变成字符串,如果不做类型转换,直接拿去比对数据源很容易查不到。
收藏功能我用了本地缓存方案,没有上云开发数据库。理由是毕设场景下,本地缓存的链路最短,演示最稳定,而且能避开“登录授权”“用户唯一标识”这一堆复杂的前置条件。具体实现是维护一个缓存数组wx.getStorageSync('favorites'),收藏就 push 进去,取消收藏就 filter 掉。详情页和“我的”页面共用这一份缓存,通过wx.setStorageSync写入后,后者在onShow里重新读取,就能做到实时同步。
这个方案不是没有缺点。换设备、清缓存都会丢失收藏数据,但毕设演示在同一个开发者工具里完成,完全够用。我在源码注释里写了切换云开发数据库的改造点,答辩时被问“能不能做成多端同步”,直接拿注释里的方案讲就行。
4. 江西特色内容库的建设:比写代码更费时间的部分
4.1 各地市美食清单怎么整理才够专业
说实话,这套项目里最耗时间的不是代码,而是那几十道江西菜品的数据整理。我的经验是:不要凭印象写,按照地市逐个过,每道菜至少补全五类信息——名称、所属区域、类型、口味标记和推荐理由。
比如写“莲花血鸭”的时候,我不光写了它是萍乡特色,还标记了辣度 5 级、口味关键词“鲜辣”,推荐理由写的是“鸭血与鸭肉同炒,出锅前浇一勺本地米酒去腥增香”。这种细节数据让整个项目有“做完了”的质感,而不是随便填了几个菜名凑数。
数据全部手工录入确实枯燥,但这里有个省力技巧:用表格工具先把 Excel 整理好,再写个小脚本批量转成 JSON 格式。手动在 JS 文件里维护几十条 JSON 容易漏逗号、漏引号,我第一版数据就是这么炸的——整个页面白屏,控制台报错定位半天才发现是dishes.js里某一行少了一个逗号。后来改成先在 Excel 维护数据,再统一转 JSON,这种低级错误再没出现过。
4.2 图片资源的版权与体积双重避坑
美食类小程序绕不开图片。一开始我打算从菜谱网站直接抓图,后来想了想,版权风险太大,毕设项目虽然不商用,但源码可能传 GitHub 或发给他人,留下版权隐患不划算。最终方案是自己在本地用开源图片处理工具做了几组风格统一的示例图,再配合少量购买的正版图库素材,确保每道菜都有图,风格也整齐。
图片体积是一个更大的坑,具体我放在后面的“踩坑”部分细说。这里先给一个原则:所有放入assets的图片必须经过压缩,单张建议控制在 80KB 以内,建议统一使用 WebP 格式,同等质量下体积大约是 JPEG 的 60% 到 70%。开发工具里看着没问题,一真机预览就卡成幻灯片,十有八九是图片体积闹的。
5. 开发过程中踩过的坑:每一个都是常见高频问题
5.1 主包 2MB 限制与图片体积超限
小程序主包大小限制是 2MB,这个限制卡掉了无数项目。我当时第一次打包就看到了类似 “source size 2612kb exceed max limit 2mb” 的报错——注意单位,报错里的 2612kb 就是主包的实际体积,超了 600 多 KB。罪魁祸首就是那批没有压缩的菜品图片。
解决思路有三个。第一是压缩图片,把单张 400KB 的图压到 80KB 以内,这一步就能砍掉一半体积。第二是把图片全部从本地挪到云端,改成外链地址,这是最彻底的方案,但要求服务器或者图床稳定,毕设阶段慎用。第三是合理使用分包加载,核心页面放主包,不常用的页面(比如隐私政策、关于项目)放分包,也能降低主包压力。
我最终选择的是“压缩 + 瘦身”组合:图片全部压到 WebP,移除了项目里所有用不到的素材,最终把体积压到 1.7MB 左右,才算彻底解决了这个问题。
5.2 自定义导航栏的高度适配与安全区
默认导航栏样式太丑,我把首页和详情页改成了自定义导航栏。改完发现一个问题:不同手机的“胶囊按钮”(右上角那三个点)位置不一样,顶部导航标题要么偏了,要么直接和胶囊重叠。
正确做法是用wx.getMenuButtonBoundingClientRect()获取胶囊的位置信息,再拿到状态栏高度,动态计算导航栏的高度和标题位置。不要写死 48px 或 64px,每一款机型都不一样。另外底部要适配safe-area,否则在 iPhone 上底部操作栏会顶进 Home Indicator 区域,按钮点击区域变小不说,看起来非常业余。
const menuRect = wx.getMenuButtonBoundingClientRect(); const systemInfo = wx.getSystemInfoSync(); const navBarHeight = menuRect.bottom + (menuRect.top - systemInfo.statusBarHeight) * 2;这套代码每个页面几乎都要用,我封装到了utils/nav.js,全局调用。答辩的时候你可以主动提一句“这里考虑了不同机型的适配”,评委立刻就会点头——这属于标准的工程化思维。
5.3 模拟器表现正常,真机却出问题的差异排查
开发阶段一切美好,我第一次点“真机预览”的时候,列表页图片加载不出来,刷新后偶尔又能显示。排查到最后发现是缓存策略问题——开发工具默认清缓存,真机上图片走本地缓存,旧缓存和新数据冲突导致显示异常。
这种“模拟器和真机不一致”的问题没有一劳永逸的解法,只能靠每次改动后用真机过一遍核心流程。日常联调我还会配合抓包工具看请求和响应,比如 Charles 就能看到小程序实际发出的请求和返回数据,排查接口数据异常比盲猜快得多。这类工具配置一次以后都会很省心。
5.4 接口域名白名单与开发环境调试
小程序正式版的wx.request只能请求已经配置在后台白名单里的 HTTPS 域名,本地联调时可以在开发者工具右上角勾选“不校验合法域名”。这个选项一开始找不到的话,直接在详情设置里搜“域名”两个字就能定位。
提交审核上线还需要 ICP 备案的域名和 HTTPS 证书,学生个人主体做小程序还有部分类目的限制。毕设演示阶段用测试号即可,不需要去走认证流程,等真正要上线运营时再补企业和认证手续。这部分我建议在答辩时主动说明,既显示你了解完整链路,又不影响项目本身能跑通。
6. 答辩演示动线与源码交付说明
6.1 演示的节奏控制
演示环节不要一上来就点页面,应该先花三十秒讲清楚“我要解决什么问题”,再按用户动线走流程:进入首页看推荐 → 点击菜品进详情 → 收藏一道菜 → 返回 → 搜索“辣” → 找到对应筛选结果 → 进入个人中心看到收藏 → 演示下拉刷新和触底加载。整套动作控制在 5 分钟以内。
有一个细节很加分:演示前把微信开发者工具的“模拟操作”调成 iPhone 12 之类的机型比例,字体大小调到合适档位。现场投影时,字号太小是最常见的翻车点,评委看不清你的界面,后边的内容再好都会打折扣。
6.2 评委高频问题与答题思路
- “你的推荐算法是怎么实现的?”——不要硬说自己是机器学习和协同过滤。就老实讲:数据源里的评分、标签、区域三个维度决定了推荐顺序,再加上每日随机种子实现“换一批”效果。这个回答真实、可复现、无懈可击。
- “数据是死的还是从服务器来的?”——先承认当前演示使用的是本地静态数据源,再讲数据源接口设计成
getDishes()这个函数,内部封装了数据访问。如果要切换云开发,只需改这个函数内部逻辑,页面层完全不用动。这个“面向接口编程”的抽象思路是答辩的加分项。 - “这个项目能直接商用吗?”——这是一个展示认知边界的问题。可以回答:目前的推荐逻辑是离线的,商用需要接入真实商家数据、增加后台管理系统、加入地图标点和订单链路,这些已经梳理成后续的扩展方向。
6.3 源码的导入与二次开发方向
源码交付包以 51545 为编号压缩成一个 ZIP,解压后直接进微信开发者工具,选择“导入项目”,填入测试号 AppID 就能编译运行。开发者工具会提示“未配置合法域名”,在详情设置里勾掉校验后一切正常。
拿到源码后最推荐的二次开发方向有两个。第一是说“数据源替换”,把data/dishes.js里的江西美食数据换成你自己城市的特色菜品,一个晚上的功夫,项目就变成了“面向XX美食推荐”,演示时亲和力直接拉满。第二是“云开发接入”,把收藏从本地缓存换到云数据库,用户换设备不丢数据,这个改造虽然有一些工作量,但是一个很有意思的进阶项目。
说实话,美食推荐类小程序作为毕设,最大的优势并不是技术有多前沿,而是内容天然有看头,评委看演示时不会犯困。我做了不少小程序项目,回头再看这套 51545,依然觉得它在“投入产出比”上是排名靠前的选择。如果你准备拿它当参考,我的建议是先完整跑通一遍流程,理解每个页面为什么这么设计,然后替换成你自己熟悉的地域内容。你越是能讲清楚“为什么做了这个功能”,答辩的分数就越理想。