ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

uniapp+SpringBoot打造健康饮食运动管理小程序

uniapp+SpringBoot打造健康饮食运动管理小程序 说实话之前我一直觉得个人健康类小程序是个“看起来简单、做起来琐碎”的方向直到自己用 uniapp Vue SpringBoot 把一套个人健康饮食运动信息管理小程序真正落地之后才意识到这类项目的难点根本不在某个单独的技术点而是前后端怎么把一个“用户的日常”串起来。这篇文章不会去抄官方文档我会从需求拆解、表设计、接口聚合、小程序端交互处理和实际上线踩坑这几个角度把整套实现思路和代码细节都摊开讲适合正在做类似毕业设计、个人项目或者想低成本验证健康赛道想法的开发者参考。1. 先从需求聊起我要做的这套个人健康饮食运动管理到底管什么1.1 用户痛点与功能范围圈定做之前我特意去问了一圈周围想减肥、想增肌、想控糖的朋友发现大家挂在嘴边的不是“我要计算卡路里”而是“我今天吃了什么”、“我有没有动够”这两件事。需求的本质其实很朴素用户需要一个随手就能记录、到点能看结果的东西。所以我把范围收敛成三个核心闭环记录用户录入一顿饭吃了哪些食物、一份运动做了什么、时长多长。计算系统根据录入内容计算出热量摄入、三大营养素碳水、蛋白质、脂肪、运动消耗。反馈用图表把当天的摄入和消耗对比展示用周报看一周趋势。至于社区、商城、社交分享这类重运营功能MVP阶段我全部砍掉。个人健康管理产品的第一生命周期是“让用户持续记录”而不是“让用户逛起来”。1.2 MVP阶段的模块划分结合 uniapp 的页面体系和 SpringBoot 的工程结构我把整体拆成了四个模块模块职责前端页面后端接口用户模块注册、登录、个人基础信息身高体重年龄登录页、我的页/api/user/**饮食模块食物库检索、饮食记录、营养分析饮食打卡页、记录列表页/api/diet/**运动模块运动项目选择、运动记录、消耗计算运动打卡页、运动日历/api/sport/**统计模块当日汇总、趋势图表首页仪表盘、周报页/api/stats/**这套划分的核心逻辑是饮食和运动在物理世界里是独立发生的但在用户感知里是同一个目标下的两条路径。后端保持模块独立前端在“首页仪表盘”做数据聚合这样既不会把接口搅成一锅粥又能让用户看到“摄入 vs 消耗”的统一视图。2. 技术选型的底层逻辑为什么不凑齐“原生三件套”而选 uniapp SpringBoot2.1 uniapp 在跨端场景的真实收益目标平台是微信小程序但我心里很清楚健康管理类产品大概率不会只守一个小程序。用户可能会问“有没有 App”也可能需要 H5 版本方便分享。如果每个端都写一套原生对个人开发者来说维护成本是灾难性的。用 uniapp 的收益有三层语法层基于 Vue 的响应式开发体验页面结构、组件化、数据绑定都是前端工程师熟悉的那套。编译层一套代码可以编译出微信小程序、App、H5虽然不能保证 100% 零差异但对信息管理这类以表单、列表、图表为主的业务覆盖度已经非常可观。生态层插件市场里有现成的图表组件、日历组件、城市选择器省去了大量从零造的功夫。我这次主要用 uniapp 编译到微信小程序同时留了 App 端的产物入口原因就是不想把路堵死。2.2 SpringBoot 在个人项目里的角色与优势后端选 SpringBoot 不是因为它“流行”而是因为它能让我把大量精力放在业务上而不是基建上内嵌 Tomcat打包成 jar 直接跑不需要额外配服务器。Spring MVC 处理 REST 接口天然顺手。Spring Data 生态成熟搭配 MyBatis-Plus 做单表 CRUD 几乎不用写 SQL。自带参数校验、全局异常处理接口层的规范性很容易保证。一个小细节个人项目最容易翻车的是数据库版本和 JDK 版本不匹配。Spring Boot 3.x 要求 JDK 17如果你的服务器还是 JDK 8强行用 3.x 会遇到各种兼容问题。我这次用的是 Spring Boot 2.7.x JDK 8稳是第一位的。2.3 这个组合的边界在哪里选型必须知道它的短板不然上线之后会被反噬。uniapp 在复杂原生交互比如实时心率、复杂蓝牙协议上会很吃力需要写原生插件。健康管理涉及到的智能硬件对接要提前做技术预判。SpringBoot 单体部署对个人项目是优点因为简单但如果未来用户量起来拆分微服务就是另一套成本了。这里我只做单体不做微服务。一句话总结我的取舍在 MVP 阶段工具链的连贯性比某个端的极致体验更重要。3. 后端设计核心数据模型与自动建表的落地细节3.1 食物库、饮食记录、运动记录三张核心表怎么设计数据表设计是整个后端的地基。餐食记录、运动记录这类数据都是线性增长的所以要特别注意索引设计和字段冗余。食物库表food字段类型说明idbigint主键namevarchar(64)食物名称unitvarchar(32)单位100g / 1个 / 1杯caloriesdecimal(8,2)热量kcalproteindecimal(8,2)蛋白质gfatdecimal(8,2)脂肪gcarbsdecimal(8,2)碳水gcategoryvarchar(32)分类主食/肉类/蔬菜/水果/饮品饮食记录表diet_record字段类型说明idbigint主键user_idbigint用户ID索引food_idbigint食物IDfood_namevarchar(64)冗余食物名称避免联表amountdecimal(8,2)摄入量单位数caloriesdecimal(8,2)该条记录的总热量冗余计算值meal_typetinyint1早餐 2午餐 3晚餐 4加餐record_datedate记录日期与 user_id 建联合索引create_timedatetime创建时间运动记录表sport_record字段类型说明idbigint主键user_idbigint用户ID索引sport_namevarchar(64)运动名称duration_minint运动时长分钟caloriesdecimal(8,2)消耗热量按 MET 值计算record_datedate记录日期create_timedatetime创建时间这份表设计里有一个刻意为之的做法diet_record冗余了food_name和calories。原因很简单食物库的食物营养成分如果被管理员修改了历史饮食记录不应该跟着变否则用户的“昨天吃了什么”就失真了。记录表只存“当时的事实”这是健康数据类应用的底线原则。3.2 用 MyBatis 应对“表不存在自动建表”的需求个人项目部署到新环境最尴尬的事就是忘了执行 SQL 脚本。这里我用了一个很实用的拦截器方案Component public class TableCheckRunner implements ApplicationRunner { Resource private JdbcTemplate jdbcTemplate; private static final String CREATE_TABLE_FOOD_SQL CREATE TABLE IF NOT EXISTS food (...); Override public void run(ApplicationArguments args) { jdbcTemplate.execute(CREATE_TABLE_FOOD_SQL); jdbcTemplate.execute(CREATE_TABLE_DIET_RECORD_SQL); jdbcTemplate.execute(CREATE_TABLE_SPORT_RECORD_SQL); } }关键点是CREATE TABLE IF NOT EXISTS应用启动时执行一次表不存在就建存在就跳过。再配合初始化数据脚本把常见食物的热量数据写进去新环境跑起来就能直接用。这套方案比引入 Flyway 轻量得多。Flyway 适合团队协作的版本管理但个人项目往往只有一个人维护自动建表 初始化数据就够用了。等真到了需要修改表结构的阶段再补 Flyway 也不晚。3.3 接口设计一天的饮食/运动数据如何聚合返回前端首页需要一个“当日总览”接口设计成一次请求返回当天的所有汇总数据避免前端发四五次请求。{ code: 200, data: { date: 2025-06-18, targetCalories: 1800, diet: { items: [ { foodName: 鸡胸肉, amount: 150, calories: 165, mealType: 2 } ], totalCalories: 650, protein: 45.2, fat: 18.4, carbs: 80.5 }, sport: { items: [], totalCalories: 220 }, netCalories: 430 } }对应的核心查询逻辑是在 Service 层完成的public DaySummaryVO getDaySummary(Long userId, LocalDate date) { DaySummaryVO vo new DaySummaryVO(); // 查询当日饮食记录 ListDietRecord dietList dietRecordMapper.selectByUserAndDate(userId, date); // 查询当日运动记录 ListSportRecord sportList sportRecordMapper.selectByUserAndDate(userId, date); // 汇总计算 BigDecimal dietCalories dietList.stream() .map(DietRecord::getCalories) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal sportCalories sportList.stream() .map(SportRecord::getCalories) .reduce(BigDecimal.ZERO, BigDecimal::add); vo.setDietCalories(dietCalories); vo.setSportCalories(sportCalories); vo.setNetCalories(dietCalories.subtract(sportCalories)); // 保留摄入明细前端绘图用 vo.setDietItems(dietList); return vo; }这里要注意聚合查询虽然简单但数据量上来之后“按日期查询 内存求和”的方式会拖慢接口响应。前期个人项目可以这么干如果以后数据到了百万级就要考虑把汇总结果做成每日定时任务预计算而不是每次都现算。4. 小程序端核心功能实现记录、计算与展示4.1 饮食打卡页从食物库选择到卡路里计算小程序端的核心页面是饮食打卡页。用户在这页完成三件事搜食物、选食物、填份量。我在实现时把搜索和选择做在同一个页面避免页面跳转打断记录节奏。template view classdiet-page input v-modelkeyword placeholder输入食物名称搜索 confirm-typesearch confirmhandleSearch / scroll-view scroll-y classfood-list view classfood-item v-foritem in foodList :keyitem.id tapselectFood(item) text{{ item.name }}/text text classdesc{{ item.calories }} kcal / {{ item.unit }}/text /view /scroll-view !-- 份量选择弹窗 -- popup :showshowAmountPopup closeshowAmountPopup false view text{{ selectedFood.name }}/text input typedigit v-modelamount placeholder输入份量 / picker :rangemealTypeNames changemealTypeChange text{{ mealTypeNames[mealType] }}/text /picker button tapsubmitDietRecord保存/button /view /popup /view /template提交保存的时候前端算好热量直接传给后端后端校验后落库const calories (parseFloat(this.amount) * this.selectedFood.caloriesPerUnit).toFixed(2);这个计算放在前端是为了让用户立即看到“我要吃掉多少热量”后端接口同样会做一次校验同时把数据写进diet_record。4.2 运动模块步数读取与手动录入的取舍运动打卡有两种录入方式手动选择运动项目填时长或者调用微信的getWeRunData获取微信步数。这里我使用了wx.getWeRunData来读取用户微信运动数据uni.getWeRunData({ success(res) { const stepData res.encryptedData; // 需要后端用 session_key 解密 uni.request({ url: /api/sport/decrypt-step, method: POST, data: { encryptedData: res.encryptedData, iv: res.iv }, }); } });不建议手动利用步数转换成卡路里因为通用公式存在较大误差。我采用的方式是步数直接覆盖运动时长分钟是用户在页面上手填的。接口单独提供步数接口记录当天微信运动步数不强行参与“运动消耗”计算。这样可以避免“我今天走了多少步就能抵消一块炸鸡”这种误导性极强的换算。手动录入运动项目则用 MET 值计算消耗热量(kcal) MET值 × 体重(kg) × 运动时长(h)。例如跑步6分速MET 大约是 9.8一个 60kg 的人跑半小时消耗 9.8 × 60 × 0.5 294 kcal。4.3 数据可视化用 canvas 绘制当天营养摄入占比统计模块用 canvas 画了一个环形图直观展示三大营养素的占比。uniapp 小程序端 canvas 和浏览器 H5 的 API 有很大差异小程序用的是CanvasContext需要特别注意。const ctx uni.createCanvasContext(nutritionChart); const data [ { name: 碳水, value: 55, color: #4CAF50 }, { name: 蛋白质, value: 25, color: #FF9800 }, { name: 脂肪, value: 20, color: #F44336 } ]; let startAngle 0; data.forEach(item { const angle (item.value / 100) * 2 * Math.PI; ctx.beginPath(); ctx.moveTo(100, 100); ctx.arc(100, 100, 80, startAngle, startAngle angle); ctx.setFillStyle(item.color); ctx.fill(); startAngle angle; }); ctx.draw();营养占比的算法很简单用户当天的碳水、蛋白质、脂肪摄入量分别除以三者总和得到百分比。真实情况下真正专业的营养学建议会看“供能比”——碳水和蛋白质每克提供 4 kcal脂肪每克提供 9 kcal按供能比算出来的占比才有营养学意义。5. 微信小程序端真实踩坑从加载页到键盘遮挡再到扫码5.1 修改初次加载页面配置入口页很多人第一次用 uniapp 编译微信小程序发现启动页面不是自己想要的那个原因很简单pages.json里pages数组第一个元素就是小程序启动后的首页。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 健康首页 } }, { path: pages/diet/diet, style: { navigationBarTitleText: 饮食打卡 } } ] }这里有个坑如果你把某个页面路径写在后面但它是 tabBar 页面微信小程序要求 tabBar 页面必须在pages数组的前几个位置。否则会警告“tabBar 页面必须在 pages 数组的前几位”导致 tabBar 渲染异常。我改启动页时顺手加了一个condition配置方便开发时快速跳到某个页面调试不需要点半天按钮condition: { current: 0, list: [ { name: 直接跳转饮食页, path: pages/diet/diet, query: } ] }5.2 软键盘遮挡输入内容和查询结果的解决方案“uniapp 微信小程序手机软键盘会遮挡住查询内容”这个问题是我做食物搜索时遇到的。页面结构是一个搜索框加列表手机弹起软键盘后列表底部被键盘完全盖住用户看不到查询结果。最直接的解决办法是给页面根节点设置adjust-position相关的适配。微信小程序里页面配置项mp-weixin: { adjustPosition: true }或者调用uni.pageScrollTo滚动到可视区域。我实际的处理分两步第一步在pages.json里给饮食页增加disableScroll: false确保页面可以滚动。第二步在输入框获取焦点时监听键盘高度动态调整底部占位元素高度inputFocus(e) { this.inputBottom e.detail.keyboardHeight || 0; }, inputBlur() { this.inputBottom 0; }然后在模板底部加一个view :style{ height: inputBottom px }/view占位。这个方法在真机上实测有效比硬调adjust-position更可控。5.3 扫码返回一串数字而不是链接的处理思路有用户反馈扫码后返回一串数字我当时排查了一遍才发现问题出在自己身上。正常情况下uniapp 的uni.scanCode返回的result是扫描内容可能是 URL、文本也可能是数字。如果扫的是商品条形码返回的result就是一段纯数字比如条码编号并不包含链接结构。健康管理小程序里我保留了一个增值能力扫商品条码定位食物库中的对应食物。但食物库的数据不可能覆盖所有商品条码所以接口设计要兜底GetMapping(/food/barcode/{barcode}) public Result getFoodByBarcode(PathVariable String barcode) { FoodVO food foodMapper.selectByBarcode(barcode); if (food null) { return Result.error(未找到该条码对应的食物); } return Result.success(food); }如果是 URL就正常展示如果是数字就先尝试当条码查询查不到就提示用户手动搜索。这里注意条码扫描要接真实数据库查询不要在前端做规则匹配否则只有一串数字完全没意义。5.4 自定义分享好友与动态设置标题健康记录这种内容天然适合分享打卡图所以“自定义分享”是刚需。微信小程序的分享有三种方式右上角菜单默认分享需要onShareAppMessage声明。自定义按钮分享通过open-typeshare触发。朋友圈分享小程序里需要配置onShareTimeline且页面有path参数。我实现时在用户点击页面底部的“分享打卡”按钮后动态生成当前用户的当日打卡图然后通过自定义按钮调用分享onShareAppMessage() { const today this.formatDate(this.selectedDate); return { title: 我今日摄入${this.totalCalories}kcal消耗${this.sportCalories}kcal快来和我一起打卡, path: /pages/index/index?shareDate${today}from${this.userInfo.id}, imageUrl: this.sharePosterUrl }; }动态设置标题这件事微信小程序提供了wx.setNavigationBarTitle方法。例如用户切到某个历史日期时把标题改成“2025年6月18日记录”wx.setNavigationBarTitle({ title: ${this.selectedDate} 饮食运动记录 });注意事项如果页面配置了navigationStyle: custom那这个 hook 就不再起作用因为整个导航栏都是自己画的。个人项目为了省时间建议保留默认导航栏只改标题文字。6. 打包、上架与性能细节从开发者工具到安卓应用市场6.1 uniapp 打包的两种路径uniapp 打包到微信小程序常规流程是 HBuilderX 里点击“发行 → 小程序-微信”生成dist/build/mp-weixin目录然后用微信开发者工具打开这个目录。如果要做成安卓 App有两种路径路径优势劣势HBuilderX 云打包不装安卓 SDK免费使用HBuilderX账号额度包体积大首次加载较慢本地离线打包可定制 Android 原生层代码包体可控需要配置 Android Studio、SDK、签名文件流程繁琐个人项目前期推荐云打包。但注意云打包必须配置好应用的 AppID否则生成的包无法在安卓应用市场上架。6.2 manifest 配置与权限声明manifest.json是 uniapp 的“神经系统”。从模块权限到小程序 AppID 都需要在这里配置。最容易漏的是微信小程序配置。在 HBuilderX 的 manifest 可视化界面中找到“微信小程序配置”填入小程序的 AppID。然后要确保mp-weixin的requiredPrivateInfos配置了需要调用的隐私接口{ mp-weixin: { appid: wx你的AppID, setting: { urlCheck: false }, requiredPrivateInfos: [getWeRunData, chooseLocation] } }很多问题就是这里漏配置导致的点击“获取微信步数”一直报do not have permission。把requiredPrivateInfos配好并在微信公众平台开通对应接口权限问题就消失了。6.3 真机调试的连接问题与建议“uniapp 运行到微信开发者工具上没反应”这个坑常见原因是微信开发者工具的服务端口没开需要在“设置 → 安全设置”里开启服务端口。HBuilderX 和微信开发者工具版本不匹配编译产物有问题。项目路径存在中文或空格也会导致工具无法监听。我的建议很简单先用微信开发者工具手动导入dist/build/mp-weixin目录看看是否报错。能正常加载再考虑联调热更新不要一上来就点 HBuilderX 的“运行到小程序模拟器”。真机调试时用微信开发者工具的“预览”功能生成二维码手机扫码进入然后打开 vConsole 看日志定位接口请求。小程序端跨域问题在真机上基本不存在因为请求是微信服务器转发的但要注意后端接口必须用 HTTPS并且域名要配置在小程序后台的 request 合法域名里。如果是开发阶段可以在开发者工具里勾选“不校验合法域名”真机上没法跳过这一关。7. 健康数据类小程序上线后的几点个人体会做这套系统最大的收获不是学会了 uniapp 打包也不是记住了 SpringBoot 的自动建表写法而是想明白了健康数据类产品的一大核心心法数据记录的门槛要低数据反馈的路径要短。用户打开小程序、找到食物、输入份量这个流程如果超过三步流失率就会大幅上升。这也是为什么我没有做“先补充餐次名称再选食物”的流程而是提供默认的餐次选择用户可以默认选中再快速修改。另外在部署和维护层面个人项目一定要留下监控眼。SpringBoot 端可以用 Spring Boot Actuator 暴露健康检查接口前端小程序则可以接一个第三方的错误监控 SDK这样用户反馈 bug 的时候你能第一时间定位是接口问题、数据库问题还是前端渲染问题。别指望等用户自己截一堆图再描述那种效率极低。最后再分享一个小技巧健康数据类的核心是“人与数据的交互”所以后端接口的响应体最好统一封装成ResultT结构包含code/message/data三个字段前端小程序封装一个request.js统一处理错误码和登录过期这样后面加新接口的成本会低很多。这个项目往后的扩展方向我会优先考虑接入微信的“健康”数据授权把体重、睡眠、心率这些数据也纳入记录体系。不过那一步就要涉及原生插件和更多隐私合规的细节了是另一个阶段的工程了。
返回列表