首页 / 资讯中心 / 文章详情

微信小程序健康饮食系统开发全流程:从需求拆分到云部署踩坑实录

微信小程序健康饮食系统开发全流程:从需求拆分到云部署踩坑实录 ★ FEATURED ARTICLE
做毕业设计的时候我选了基于微信小程序健康饮食系统的设计与实现这个题目。名字很长但拆开看就是三件事做一个微信小程序围绕健康饮食完成饮食记录和搭配推荐。我当时的预期是用它把大学四年学到的前端知识串一遍实际上做完才发现真正让人成长的是数据建模、逻辑边界和兼容性适配这些看不见的部分。这篇笔记会把项目从需求拆分、技术选型、页面实现到云开发部署、上线踩坑的完整过程写一遍给正在做同类毕设或者想自己做一个健康记录类小工具的同学提供一条可以直接参考的路径。1. 系统整体设计与功能拆分1.1 这个系统的价值点到底在哪里健康饮食类应用在市场上并不少见大厂的小程序、App都有很成熟的生态。但作为个人毕业设计我们的目标不是和大厂比数据量而是通过一个小的闭环把需求分析、系统设计、前后端实现的能力完整展示出来。所以我在最初就定了一个原则不贪多把饮食记录和饮食搭配这两件事做透让用户能看得见自己的变化。核心场景其实很真实。一个普通上班族或大学生想知道今天这样吃合不合理又不想下载一个沉重的健康管理 App微信里打开小程序随手记一笔就够轻量。饮食记录是输入搭配推荐是输出用户记录得越多推荐就越有针对性。这个小而实用的闭环就是整个系统的价值点。答辩的时候也能很清楚地回答你这个东西解决了什么问题。1.2 功能模块清单与取舍最终我整理出来的功能模块如下用户档案身高、体重、年龄、性别、目标减脂/增肌/保持饮食记录早/午/晚/加餐选择食物、填写份量、拍照存证食物库内置常用食材的热量和三大营养素数据智能搭配根据今日已摄入的热量和营养缺口推荐补充食物数据统计近7天热量趋势、三大营养素占比我的历史记录、设置、帮助与反馈一开始我也想过做菜谱社区、食材商城、社交分享这些功能但都被我自己砍掉了。原因很直接社区要有内容运营商城涉及支付资质这些都会让项目重心从健康饮食记录跑偏到别的赛道。尤其是关于微信小程序商城、游戏开发这些方向都很重不适合作为个人毕设去硬碰。工具型小程序最要紧的是把记录与反馈的闭环跑顺其余都算锦上添花。2. 技术选型与工程搭建2.1 原生小程序还是 uni-app网上关于原生小程序 vs uni-app的争论很多。我的结论是如果你的目标是毕设、想深挖微信生态直接选原生如果以后想快速多端发布再考虑 uni-app。原生小程序的优点是运行性能好、微信 API 支持最及时、和微信云开发配合最自然。uni-app 的优势是跨端但打包到微信端之后经常会出现包体积过大、组件兼容需要额外处理的问题。比如很多朋友在打包时遇到source size 2612kb exceed max limit 2mb这类报错就是因为每个安装包包含了不少跨端适配层的代码。原生小程序同样要注意体积但在工程结构上更可控。我选择原生最关键的另一个原因是云开发。微信小程序云开发在原生环境里有最好的支持不需要自己买服务器、备案域名、配置 HTTPS云函数、云数据库、云存储都是一站式的。对一个毕设项目来说这能帮我们省掉大量运维琐事把时间真正留在业务逻辑上。2.2 云开发让后端不再成为瓶颈云开发接入非常简洁在 app.js 里初始化即可。wx.cloud.init({ env: your-env-id, traceUser: true });云开发最方便的是登录态。用户在小程序里默认带着 openid云函数可以直接获取当前用户身份不用自己维护 Session 登录。云数据库的权限规则也很直观我们可以给集合设置仅创建者可读写这样用户天然只能访问自己的数据。对于记录类小程序来说这是最舒服的方案。当然云开发不是没有坑。比如免费额度有上限如果食物库数据量太大频繁读取会消耗读次数。解决办法是把食物库做成按需拉取而不是一次性把整个库传到前端。这一点在后面的性能优化部分会展开。2.3 整体页面架构设计页面结构我采用底部 tabBar 四页签的方案首页今日饮食记录入口展示当日三餐和时间线搭配根据今日摄入数据进行营养缺口分析和推荐统计查看近7天热量曲线和营养素占比我的用户档案、历史记录、设置首页是整个系统的主入口用户进来第一眼应该看到今天吃了什么而不是广告或欢迎语。搭配页放在第二个 Tab是因为这个页面依赖记录数据用户只有先记录后推荐才有价值。统计页放在第三用于建立用户粘性让人看到坚持记录带来的变化。导航栏我选择了自定义而不是使用系统默认导航。因为默认导航栏在非全面屏和全面屏上的高度不一致如果页面里要放复杂的标题或按钮自定义才能做到统一美观。具体的适配计算方式在第三章会详细讲。3. 核心页面设计与交互细节3.1 饮食记录页从选择食物到保存记录记录页的界面结构是顶部显示今日热量小结已摄入 / 目标热量中间是三餐时间线底部悬浮一个添加记录按钮。点击按钮弹出一个半屏面板里面包含三个关键选择餐次、食物、份量。食物选择必须绑定食物库不能完全让用户自由输入否则后续营养计算根本没法做。我实现了一个带搜索功能的食物选择器内置几百种常见食材覆盖主食、肉蛋、蔬菜、水果、奶类等。用户也可以选择自定义食物自己填热量但这个入口被我放得很隐蔽以防止随意添加导致统计数据失真。份量的处理也踩了坑。如果让用户填1个包子热量非常不准确。所以我统一用克作为单位食物库里存储的是每100克的热量和三大营养素。用户输入份量时可以自己填克数或者选择快捷档半碗一碗一拳头一巴掌这类生活中的估量。后端计算时先用快捷档转换成估算克数再算营养。比如function calcNutrition(food, serveGram) { return { calories: Math.round(food.calories * serveGram / 100), protein: Math.round(food.protein * serveGram / 100), fat: Math.round(food.fat * serveGram / 100), carb: Math.round(food.carb * serveGram / 100) }; }保存记录前我会做一次表单校验餐次不能为空、食物必须存在、份量必须大于0。这些看似基础但真正常用。3.2 记录列表分页与加载更多的正确姿势随着记录增多首页按日期分组展示时不可能一次性拉全部数据。我在需求阶段就明确了要做分页加载列表底部的加载更多是必须的。微信小程序实现加载更多最常见的方式是页面滚动触底时触发onReachBottom。但这里有两个新手容易踩的坑第一如果页面内容还没填满一屏onReachBottom永远不会触发第二如果使用自定义导航、页面局部滚动也可能导致onReachBottom不生效。我的做法是在onReachBottom里调分页查询同时给列表加一个底部占位提示onReachBottom() { if (this.data.isLoading || !this.data.hasMore) return; this.loadRecords(); }, async loadRecords() { this.setData({ isLoading: true }); const db wx.cloud.database(); const res await db.collection(records) .where({ _openid: {openid}}) .orderBy(date, desc) .skip(this.data.page * PAGE_SIZE) .limit(PAGE_SIZE) .get(); this.setData({ records: this.data.records.concat(res.data), page: this.data.page 1, hasMore: res.data.length PAGE_SIZE, isLoading: false }); }分页最忌讳的是每次都全量覆盖列表必须用concat追加。另外如果用户新增记录后要刷新我会重置page和records再重新加载否则会出现新记录排在列表顶部但下面还是旧分页内容的错乱。3.3 顶部导航栏高度适配与自定义导航微信小程序的顶部导航栏高度在不同机型上不是固定值。它由状态栏高度 胶囊按钮高度构成胶囊按钮的位置还随屏幕尺寸变化。如果直接把系统默认导航栏关闭然后用固定高度做自定义导航栏很快就会发现某台手机上标题靠下、按钮错位。适配的核心代码是const winInfo wx.getWindowInfo(); const { statusBarHeight } winInfo; const menuButton wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height; const totalHeight statusBarHeight navBarHeight;这里menuButton.top - statusBarHeight是胶囊按钮距状态栏底部的距离通常等于状态栏两侧留白。乘以2是因为上下各有同等间距。这样算出来的navBarHeight才是标题栏的真实高度。自定义导航栏的padding-top用statusBarHeightheight用totalHeight就能适配绝大多数机型。底部的安全区同样要处理全面屏底部会有 home indicator页面内容如果贴底按钮会被遮挡。我在底部操作栏加上padding-bottom: env(safe-area-inset-bottom);这比constant()更适合新系统但最好两个都写上兼容旧版本。3.4 饮食搭配页简单可解释的推荐逻辑饮食搭配页是最容易被人误解成高大上算法的地方但我最终用的是非常朴素的差值补全法而且我认为对一个毕设项目来说这是最聪明的方式。具体逻辑分三步统计当天已摄入的总热量和三大营养素。根据用户档案算出目标热量和推荐营养范围。计算缺口从食物库中挑选能补缺口的食物推荐给用户。比如一个目标摄入 2000 千卡、蛋白质目标 100 克的用户今天只吃够了 1200 千卡、蛋白质 50 克那么系统会查找富含蛋白质且热量适中的食物比如鸡蛋、鸡胸肉、牛奶推荐一组补足方案。推荐时加了几条限制避免结果显得荒谬一次推荐最多 2-3 种食物单个推荐份量不超过 200 克推荐总热量不超过目标缺口的 10%。这样用户可以回到记录页把推荐食物加入记录完成闭环。实际上这种通过差值计算做推荐的逻辑在答辩时非常好讲。因为它透明、可验证老师任意给一组数据你都能推算系统的行为。如果我用一个黑盒神经网络做推荐解释性反而会变差。4. 云端数据模型与核心实现4.1 数据库集合设计云开发里的核心集合我划分成了三个users、foods、records。集合主要字段说明users_openid, height, weight, age, gender, goal, targetCalories, createTime用户档案和目标foodsname, category, calories, protein, fat, carb, unit, isCustom食物库数据每100克营养records_openid, mealType, foodId, foodName, serveGram, calories, protein, fat, carb, date, imageFileID每条饮食记录特别注意records 里我冗余了foodName和各类营养。这不是浪费存储而是为了避免每次展示历史记录时都要关联查询 foods 集合。小程序端在真实场景里查询关联数据是相对昂贵的冗余几个字段能显著提升列表加载速度。食物库集合不需要用户自己去维护所以它的权限规则是所有用户可读仅创建者可写而 users 和 records 集合的权限是仅创建者可读写。这个区分非常重要否则任何用户都能读取或篡改别人的记录。4.2 热量目标计算与营养缺口模型计算热量目标我用的是 Mifflin-St Jeor 公式这是目前比较常用的静息代谢估算方式。男性基础代谢BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄 5女性基础代谢BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄 - 161得到 BMR 后再乘以活动系数久坐 1.2、轻度活动 1.375、中度活动 1.55。最后根据目标做调整减脂在原基础上减少 400 千卡增肌增加 300 千卡保持不变。这里需要声明这只是一个估算不构成医疗建议。我在页面上也放了提示文案避免被理解成诊断结论。蛋白质、脂肪、碳水的基本配比我参考的是常见均衡饮食范围同样只做参考。这个计算过程我放在云函数里执行而不是写死在前端。理由有两个一是公式逻辑如果变化改云端函数所有端都同步二是可以在云函数里顺便做一些更完整的校验。云函数示例exports.main async (event) { const { wxContext } cloud.getWXContext(); const user await getUser(wxContext.OPENID); const bmr user.gender male ? 10 * user.weight 6.25 * user.height - 5 * user.age 5 : 10 * user.weight 6.25 * user.height - 5 * user.age - 161; const target Math.round(bmr * activityFactor adjustByGoal(user.goal)); return { target, bmr }; };4.3 云函数与数据权限边界在小程序端直接操作数据库虽然方便但存在一个隐患如果不小心把查询条件写错可能把集合里所有记录都拉下来或者读取到别人的数据。所以我规定了几类操作必须走云函数计算每日营养汇总涉及聚合查询读取用户档案并计算目标热量写推荐日志或批量导入数据只用云函数访问集合时数据库权限可以设置得更严格。云函数端拿到OPENID后在where条件里强制指定_openid能防一手客户端越权。5. 让项目稳定的调试与问题排查实录5.1 列表加载更多不触发的解决办法这是我在开发中实际碰到的大坑。测试时记录不到10条页面高度一屏就能放下onReachBottom永远不触发分页功能等于摆设。排查思路有两个方向。第一检查页面是否设置了onReachBottomDistance如果页面 content 高度足够默认 50px 的触底阈值通常没问题。第二页面若使用了scroll-view局部滚动onReachBottom不会响应必须改用bindscrolltolower事件并注意它的属性是lower-threshold。如果只是需要让加载更多在真机和模拟器都能看到效果可以在测试阶段给列表尾部加一个view占位让它撑高内容区。后期数据量大了这个占位就可以删除。5.2 图片上传与展示的持久化问题饮食记录要想做得有仪式感拍照是免不了的。我用的是wx.chooseMedia拍摄或相册选择后得到临时路径const res await wx.chooseMedia({ count: 1, mediaType: [image], sourceType: [camera, album], sizeType: [compressed] }); const filePath res.tempFiles[0].tempFilePath;临时路径不能直接拿去创建记录因为过段时间会失效。正确的流程是先传给云存储拿到fileID再存进 recordsconst uploadRes await wx.cloud.uploadFile({ cloudPath: record/${openid}/${Date.now()}.jpg, filePath }); const fileID uploadRes.fileID;存了fileID之后页面image src{{fileID}}是能显示的。但是云存储的临时链接默认有有效期如果用户隔几天再打开历史记录图片可能加载失败。解决办法是不要展示原图而是用云存储的getTempFileURL换取临时链接或在保存时生成一个较长的有效链接缓存起来。不过如果只是毕设演示用fileID直接展示完全够用。5.3 云函数超时和聚合查询优化刚开始做统计接口时我习惯在云函数里直接循环读取某用户的所有记录然后在云端累加。记录数少没问题但历史数据超过几百条后读次数和耗时都会明显上升。此时应该用数据库聚合aggregateconst res await db.collection(records) .aggregate() .match({ date: _.gte(startDate).and(_.lte(endDate)) }) .group({ _id: $date, totalCalories: $.sum($calories) }) .limit(30) .end();聚合查询能在数据库端完成汇总网络传输量小很多。云函数默认的超时时间是3秒默认内存是256MB如果发现执行时间很紧张可以到云开发控制台把超时时间调高到5秒甚至10秒但更好的思路是减少不必要的循环。5.4 常见问题速查表问题常见原因解决方法列表加载更多不触发页面内容不满一屏 / 使用了scroll-view使用bindscrolltolower或撑高内容自定义导航栏在部分机型错位高度计算未考虑胶囊位置用getMenuButtonBoundingClientRect计算图片上传失败未配置云存储权限 / 临时路径直接入库先uploadFile得到fileID再存记录云函数读不出用户数据集合权限只允许客户端访问云函数设置管理端权限并通过OPENID过滤页面展示别人的记录集合权限开放了所有用户可读users和records设为仅创建者可读写食物库太大首次加载卡顿一次性请求全部数据提供搜索接口按需拉取6. 上线审核与体验优化备忘6.1 包体积控制与分包加载微信小程序主包大小限制是2MB。我在第一版时犯了个错把整个食物库的 JSON 文件放在本地代码包里几百种食物数据占掉很多体积再加上页面和图片主包轻松超限。最终我把食物库整个迁到云数据库本地只保留一个常用的空壳页面启动后异步拉取索引和搜索关键词。如果你确实有一部分不常打开的业务可以使用分包加载。把 tabBar 页面放在主包其他页面如历史详情设置放到分包能明显减少首包体积。配置方法是在app.json里加subpackages: [ { root: packageDetail, pages: [pages/history/history, pages/settings/settings] } ]分包的页面间跳转路径要带分包根路径这一点很容易忘记。6.2 审核前需要特别注意的合规点健康类小程序在小程序审核时属于比较敏感的类目。我在提交审核前做了三件事一是类目选择了工具-健康管理而不是医疗-医疗健康。因为我们的功能不涉及诊断、也不提供治疗方案只是记录和参考。页面里的文案也都用了参考建议而不是指导方案。二是补齐了用户隐私保护指引。在公众平台的后台配置了收集用户信息的使用说明在小程序内也要有隐私政策页面。尤其是用户授权头像、位置我们没有用位置但照片可能包含位置所以也要在隐私声明里说明等内容都要提前写清楚。三是不做诱导分享。虽然分享功能本身不违规但毕设项目里如果加分享得会员这类玩法审核大概率被拒。我保留了普通的分享给好友但没有任何诱导成分。这些都是实际踩完坑后的反思真等到提交被拒再改来回要耽误一周。6.3 真机预览与体验优化建议开发工具里跑得好好的真机上一塌糊涂的情况太常见了。我的建议是尽早把预览码发到手机上测试尤其是 iOS 和 Android 各测一轮。最容易发现的问题包括iPhone 底部横条遮挡按钮、部分安卓手机字体渲染差异、小程序胶囊按钮位置差异。我在内测阶段收集了十来个同学的建议印象最深刻的一条是历史记录翻起来太累。于是我给记录列表加了按日期搜索和最近常用食物快捷入口把常用操作从 3 次点击降到 1 次。这种优化看上去不性感但对日常使用的体验提升很明显。不要等到全做完才去给别人试用尤其是毕设项目越早让朋友真机扫一扫越能降低返工成本。最后说一点个人体会。做这个健康饮食小程序最难的三个部分并不是编码而是数据完整性和交互边界一份食物到底算多少热量、用户不按规范填写怎么办、推荐逻辑怎么保证用户不会误解成医疗方案。我在答辩时坦率讲了推荐逻辑是简单的差值算法而不是机器学习老师反而觉得这种小而完整的闭环很可靠。如果你也在做类似的工具型小程序建议先砍掉所有装饰性功能把记录和反馈两个环节打磨顺再考虑扩展。哪怕只有几百人试用你都能学到比课程设计多得多的问题处理能力。
阅读完成 · 觉得有帮助?
咨询建站