资讯详情

微信小程序+Flask+SQLite:健身饮食计划管理应用全栈实战

📅 2026/9/26 22:57:23 | 华诺云谱 👁 阅读
微信小程序+Flask+SQLite:健身饮食计划管理应用全栈实战
最近用业余时间做了一个健身运动饮食计划管理的小程序应用技术栈就是标题里写的微信小程序做前端Python Flask 做后端数据存储用的轻量级数据库本地就能跑起来。整个项目从需求梳理、接口设计、小程序页面开发到部署上线走完了一整套流程中间踩了不少坑也把不少模糊的概念彻底理清了。这篇就把整个设计和实现过程掰开揉碎讲一遍包括为什么会选这套组合、核心功能怎么拆、接口怎么封装、部署时附件路径为什么总是出错希望能给正在做类似项目的朋友一些参考。1. 项目整体设计与技术选型思路1.1 为什么是“小程序 Flask”这对组合先说结论这个组合特别适合工具类、轻量级、使用频率高但单次使用时间短的场景。我当初定方案的时候候选的有 H5 网页版、原生 App、uni-app 跨端方案和微信小程序原生开发。挨个分析了一遍H5 网页版开发门槛最低但没法调用微信登录能力用户在手机浏览器里输入网址也麻烦留存率低。更麻烦的是消息通知、拍照上传这类能力在浏览器里做受限制很多。原生 App功能自由度最高但开发周期长上架审核成本高。对一个个人项目来说双端开发和后续维护的负担太重。**uni-app 跨端能一套代码出小程序和 App听着很香但调试链路长很多原生组件要条件编译出了问题定位成本高。而且既然是做健身饮食管理核心使用场景就是微信里即用即走没必要为了一个场景去维护多端代码。微信小程序原生开发可以直接用微信登录能力用户不用额外注册打开微信就能用符合健身打卡这种高频但短时的使用场景。后端选择 Flask 而不是 Django、Spring Boot主要考虑三点轻量。这个项目本质上是一组 RESTful API 少量文件处理能力不需要 Django 自带的后台管理系统、ORM 全功能管理那套重组件。灵活。Flask 用装饰器写路由非常直观代码量比 Django 少很多一个小项目用 Flask 维护起来很清爽。Python 生态的扩展空间。项目的推荐匹配功能要做分词和相似度计算Python 的 jieba、scikit-learn 这些库可以直接拿来用后续加数据分析、生成报表也方便。所以技术方案最终定为层级技术选型用途客户端微信小程序原生页面展示、用户交互、本地缓存服务端Python Flask提供 RESTful API、处理业务逻辑数据库SQLite存储用户、食物库、运动库、计划、记录部署gunicorn nginx生产环境提供稳定服务1.2 前后端数据流转与整体架构整个应用的架构不复杂一句话就能说清用户在小程序端操作小程序通过封装好的wx.request把请求发到 Flask 后端后端处理完业务逻辑后返回 JSON 数据小程序拿到数据后通过setData渲染到页面上。四个核心模块分别是用户模块微信登录获取 openid、个人信息维护、目标设定减脂/增肌/保持运动模块运动项目库、训练计划管理、运动打卡记录饮食模块食物库、饮食计划管理、热量与营养计算推荐模块根据用户的饮食偏好和运动目标做关键词匹配推荐这里我想强调一个设计思路前后端分离。Flask 后端只做 API不做模板渲染。很多人刚接触 Flask 的时候会去写render_template(index.html)这种代码但在小程序场景下完全用不上。小程序页面本身就是独立的后端只需要输出 JSON前端用setData绑定数据。这样做的好处是接口可以复用以后想出 App 端或者 Web 端后端一套接口就够了。热词里有人搜“flask如何绑定到网页元素”其实就是对 Flask 的角色理解有些偏差在前后端分离的架构里Flask 不用关心前端元素它只负责把数据按约定格式吐出去。2. 核心功能拆解与数据库设计2.1 用户登录与个人信息管理小程序登录的整个流程我建议第一次做的朋友直接按这个套路走小程序端调用wx.login()获取临时登录凭证code小程序把code发送到 Flask 后端的/api/login接口Flask 端拿code去微信接口服务换openid和session_key后端用openid查用户表不存在就自动注册存在就更新最近登录时间后端生成一个自定义 token 返回给小程序小程序存入wx.setStorageSync(token, ...)后续所有请求小程序在 header 里带上 token后端校验通过后就能识别用户身份这里有个技术细节要注意微信的接口调用凭证appid和secret绝对不能写在小程序前端代码里这个 secret 一旦暴露任何人都能冒充你的小程序调用接口。正确做法是把它配置在 Flask 后端的环境变量或配置文件中由后端统一去请求微信服务器。个人信息部分我会让用户填写身高、体重、年龄、性别、运动频率和目标。这些参数后面算 BMR基础代谢率和 TDEE每日总热量消耗都会用到是整个饮食计划的核心输入。2.2 运动计划与饮食计划怎么设计运动计划部分我的思路是分三层运动项目库维护一批基础运动比如卧推、深蹲、跑步、瑜伽。每个运动记录它的类型力量/有氧/柔韧性、主要锻炼部位、单位时间消耗热量千卡/小时。训练计划表用户创建自己的训练计划指定每周哪几天练、练什么项目、组数次数或时长。打卡记录用户每天把实际完成的运动记录到打卡表里记录实际消耗热量。饮食计划部分的逻辑类似食物库维护常见食物的热量和三大营养素蛋白质、脂肪、碳水化合物数据。一开始可以内置几十种常见食物后续允许用户自己添加。饮食计划表每天配置早中晚三餐吃什么、吃多少克。饮食记录表记录实际摄入自动汇总当天的总热量和营养比例。热量计算是整个饮食模块的逻辑核心给新手朋友一个可以直接用的公式BMR 基础代谢率Mifflin-St Jeor 公式男性BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄(岁) 5 女性BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄(岁) - 161TDEE 每日总热量消耗TDEE BMR × 活动系数 活动系数久坐1.2轻度运动1.375中度运动1.55高强度1.725有了 TDEE再根据用户目标设定每日摄入目标减脂TDEE - 300~500 千卡保持TDEE增肌TDEE 200~300 千卡这套计算逻辑直接写在 Flask 后端每次用户更新个人信息或查看当日计划时动态计算数据始终保持最新。2.3 数据库表设计参考我是用 SQLite 存的为了轻量不需要单独部署数据库服务。整个项目的核心表大概有这几张表名用途关键字段user用户表openid, nickname, height, weight, age, gender, goal, activity_levelfood食物库name, calories, protein, fat, carbohydrate, unit, categoryexercise运动项目库name, type, body_part, calories_per_hour, descriptionplan计划表user_id, plan_type(运动/饮食), plan_date, contentrecord打卡记录表user_id, record_type, item_id, quantity, record_datediet_log饮食记录user_id, food_id, amount, meal, log_dateworkout_log运动记录user_id, exercise_id, duration, calories, log_date这里我踩过的一个坑是一开始把计划和记录混在一张表里结果查询历史记录、生成统计报表的时候 SQL 写得非常别扭。后来拆成plan和record/_log两张表各管各的逻辑清楚太多了。计划是“预设要做什么”记录是“实际做了什么”数据形态不一样千万别偷懒塞一起。2.4 推荐匹配功能是怎么实现的项目里我加了一个“智能推荐”功能用来给用户推荐合适的运动项目和食物这一块思路跟我在网上看到的校园失物招领平台的关键词匹配思路有相通之处。我用的方案是中文分词 TF-IDF 特征提取 余弦相似度计算。举个例子用户输入“想瘦肚子”后端把这句话用 jieba 分词拆成[瘦, 肚子]然后跟运动库里的每一条做相似度计算。运动库里“卷腹”的描述是“锻炼腹部肌肉有助于减小腰围”“跑步”的描述是“全身有氧运动”分词后计算余弦相似度明显“卷腹”跟“想瘦肚子”的相似度高于“跑步”于是推荐结果把卷腹排在最前面。这个方案的好处是不需要手动维护复杂的规则表靠相似度打分自动排序关键词覆盖越全、描述语料越丰富推荐越准技术上不重个人项目完全跑得动对于无效信息过滤我在推荐前先做了一步把用户输入里的停用词比如“我想”、“有没有”、“推荐一下”用停用词表过滤掉再进行分词和计算。这个步骤极大的提升了匹配精度比如“我想找适合增肌的食物”过滤停用词后变成“增肌 食物”比带着噪音词直接计算准确率高很多。3. Flask 后端接口实现与部署3.1 API 设计与请求封装后端接口的规划我坚持一个原则接口命名按照资源来组织返回格式全局统一。统一返回格式定义成{ code: 0, msg: success, data: {} }code为 0 表示成功非 0 表示各种错误。小程序端请求封装里统一判断code省去每个页面重复处理错误逻辑。核心接口清单接口路径方法用途/api/loginPOST微信登录换 openid/api/user/profileGET/PUT获取/更新个人信息/api/food/searchGET搜索食物支持关键词/api/food/recommendGET基于饮食目标推荐食物/api/exercise/listGET获取运动项目列表/api/exercise/recommendGET基于目标推荐运动/api/plan/getGET获取某天的计划/api/plan/savePOST保存计划/api/record/addPOST添加打卡记录/api/record/statsGET获取某段时间的统计3.2 关键接口代码实现示例先看最核心的用户登录接口。Flask 这边的实现大概是这样import hashlib import requests from flask import Blueprint, request, jsonify from config import APP_ID, APP_SECRET user_bp Blueprint(user, __name__) def generate_token(openid): raw f{openid}{APP_SECRET} return hashlib.sha256(raw.encode(utf-8)).hexdigest() user_bp.route(/api/login, methods[POST]) def login(): data request.get_json() code data.get(code) if not code: return jsonify({code: 1, msg: 缺少code, data: {}}) # 用code换openid resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: APP_ID, secret: APP_SECRET, js_code: code, grant_type: authorization_code }, timeout5 ) result resp.json() openid result.get(openid) if not openid: return jsonify({code: 2, msg: 微信登录失败, data: result}) # 查库不存在则自动注册 user query_user_by_openid(openid) if not user: create_user(openid) token generate_token(openid) # 缓存token与用户的映射关系 save_token(token, openid) return jsonify({code: 0, msg: success, data: {token: token}})这里有个细节requests.get一定要设timeout不设的话微信接口万一挂了你的 Flask 请求会一直阻塞小程序端所有登录请求全卡住用户体验是灾难级的。再放一个推荐接口的代码展示关键词匹配的核心思路import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def filter_stopwords(text, stopwords): words jieba.lcut(text) return .join([w for w in words if w not in stopwords]) def recommend_items(user_input, item_list, stopwords): # 过滤停用词后的用户输入 query filter_stopwords(user_input, stopwords) # 每条item包含 id, name, description corpus [item[description] for item in item_list] corpus.append(query) # 最后一条是用户的查询 vectorizer TfidfVectorizer() tfidf_matrix vectorizer.fit_transform(corpus) sim_scores cosine_similarity(tfidf_matrix[-1], tfidf_matrix[:-1])[0] ranked sorted(zip(item_list, sim_scores), keylambda x: x[1], reverseTrue) return [item for item, score in ranked[:5] if score 0]这个函数的核心逻辑就是把所有候选物品的描述和用户的输入放到一起做 TF-IDF 向量化然后计算用户输入跟每条候选的余弦相似度最后按分数排序返回前几名。相似度低于阈值的就过滤掉避免推荐一些完全无关的内容。3.3 本地开发与服务器部署本地开发阶段我建议用虚拟环境管理依赖避免把系统 Python 环境搞乱cd project_root python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install flask flask-cors gunicorn requests scikit-learn jieba flask run --host0.0.0.0 --port5000--host0.0.0.0是为了让局域网内的手机也能访问到你的开发服务器不然真机调试小程序的时候手机请求不到你电脑上的 Flask 服务。生产环境部署我用的方案是gunicorn nginx 反向代理。Flask 自带的开发服务器性能一般生产环境千万别直接拿来扛流量。部署步骤大概是# 在服务器上安装依赖 pip install gunicorn flask requests scikit-learn jieba # 用 gunicorn 启动4个worker进程 gunicorn -w 4 -b 127.0.0.1:8000 app:app # nginx 配置反向代理把80端口的请求转发到8000端口小程序端有个硬性要求所有请求的接口必须是 HTTPS 且域名已备案同时要在微信公众平台后台配置“request 合法域名”。想省钱的话可以用免费证书申请 HTTPS然后把 nginx 配置好小程序才能真正跑通线上请求。3.4 附件路径问题排查一个最容易翻车的隐患热词里有人搜“windows flask项目部署到服务器上附件路径错误”我太有共鸣了这个坑我踩得结结实实。问题是这样本地 Windows 开发时我直接用相对路径存储生成的饮食计划文件比如UPLOAD_FOLDER uploads/本地跑得好好的文件都存到了项目目录下的 uploads 文件夹。一旦部署到 Linux 服务器用 gunicorn 启动后问题来了——相对路径是相对于「当前工作目录」的而 gunicorn 的工作目录可能跟你的项目目录不一致。结果就是文件找不到、附件上传失败、生成的计划文档路径 404。排查思路其实不复杂就看三点第一用绝对路径而不是相对路径。Flask 给了一个标准姿势用app.root_path来拼路径import os from flask import current_app UPLOAD_FOLDER os.path.join(current_app.root_path, uploads) app.config[UPLOAD_FOLDER] UPLOAD_FOLDERapp.root_path是 Flask 应用实例所在目录不管你在哪个目录下启动服务这个路径都是稳定的用它拼出来的绝对路径不会因为工作目录变化而出错。第二检查目录是否存在且有写权限。Linux 下很容易遇到文件权限问题项目目录属于 root你用普通用户启动 gunicorn 的话根本没有写权限。直接启动前先给目录授权mkdir -p /www/app/uploads chmod 755 /www/app/uploads第三nginx 要正确配置静态文件访问。如果你的附件是通过 URL 直接访问的nginx 要单独配置一条 location 规则把/uploads/开头的请求映射到磁盘上的物理路径。这个问题的本质是“相对路径对环境敏感绝对路径对环境不敏感”。写代码的时候养成用绝对路径、用app.root_path拼接的好习惯部署阶段就能省掉一大半的附件路径排查工作。4. 微信小程序端实现要点与常见问题4.1 页面结构与组件使用小程序端的页面规划我用了底部 tabBar 四栏结构首页今日概览显示热量摄入进度、运动打卡情况运动运动项目列表、运动计划、打卡入口饮食三餐打卡、食物搜索、热量统计我的个人信息、目标设定、历史数据小程序的表单组件有几个地方值得单独说一下。单选框很多新手直接在radio-group里放radio然后用bindchange事件取值。这里有个细节radio-group的bindchange返回的event.detail.value是字符串类型的 value不是对象。如果你 value 设置的是数字记得做类型转换不然后面比较的时候会出现1 ! 1这种诡异问题。顶部导航栏高度适配小程序的胶囊按钮右上角那个“...”)位置在wx.getMenuButtonBoundingClientRect()可以拿到导航栏高度可以用wx.getSystemInfoSync()拿状态栏高度然后自己计算const menuButton wx.getMenuButtonBoundingClientRect() const statusBarHeight wx.getSystemInfoSync().statusBarHeight const navBarHeight menuButton.height (menuButton.top - statusBarHeight) * 2如果你的页面用了自定义导航栏这个计算出来的高度就是导航栏的真实高度不然自定义头部挡住胶囊按钮就尴尬了。日期选择器用在小程序里可以直接用picker的modedate模式不用自己去写日历组件。4.2 请求封装与数据缓存策略小程序的wx.request没有全局拦截器所以我在项目里封装了一个统一的请求模块const request (url, method, data) { const token wx.getStorageSync(token) return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: Bearer ${token} }, timeout: 8000, success: res { if (res.data.code 0) { resolve(res.data.data) } else { // 统一错误处理比如 token 失效跳转登录页 wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: err { wx.showToast({ title: 网络请求失败, icon: none }) reject(err) } }) }) }这样封装好之后页面里调用就非常清爽了const data await request(/api/plan/get, GET, { date: 2025-06-01 })缓存策略方面我用的是“时间戳 过期判断”方案。热词里有人搜“微信小程序设置缓存时间”其实就是自己手动实现过期逻辑// 写缓存时带上时间戳 const setCache (key, value, expireHours) { const cache { value, expireAt: Date.now() expireHours * 3600 * 1000 } wx.setStorageSync(key, cache) } // 读缓存时判断是否过期 const getCache (key) { const cache wx.getStorageSync(key) if (!cache) return null if (Date.now() cache.expireAt) { wx.removeStorageSync(key) return null } return cache.value }我把食物库这类不常变化的数据设置为 24 小时缓存用户每天的饮食计划设置为 12 小时缓存。这样既保证了数据不是陈旧的也不会每次进入页面都发请求。4.3 常见问题排查与注意事项我把实际开发中遇到的高频问题整理成一张速查表做类似项目的朋友可以直接对照问题现象排查思路解决方案真机调试请求失败手机和电脑是否同一局域网是否用了http://localhost改成本机局域网 IPFlask 启动加--host0.0.0.0线上请求报“域名不合法”小程序后台未配置 request 合法域名配置 HTTPS 域名并确保域名已备案登录接口返回invalid codecode 只能使用一次可能是重复请求确认wx.login的 code 是新鲜的且只发一次附件路径 404相对路径或静态文件配置问题用app.root_path拼绝对路径nginx 配置静态目录单选框值取不到bindchange没写或事件绑定层级错误事件绑定在radio-group上不是单个radio上缓存一直不更新没设置过期时间或永远读取旧缓存用时间戳实现过期策略定期清理Flask 启动报端口被占用本地 5000 端口被其他进程占用换端口flask run --port5001或杀掉占用进程我再补充一个容易被忽略但实际很重要的点Flask 生产环境下一定要关掉调试模式。app.run(debugTrue)在本地开发很好用代码改了自动重载但一旦部署到服务器还开着调试模式相当于把服务器的错误堆栈完整暴露给任何人看而且调试模式本身性能极差。生产环境用 gunicorn 启动的时候根本没有debugTrue这回事这也是我推荐用 gunicorn 而不是python app.py来跑生产服务的原因之一。另一个经验是关于Flask 的跨域问题。小程序端请求后端接口理论上wx.request不受浏览器同源策略限制所以你用 Flask 写接口时不需要特意配置 CORS。但如果以后你同一个后端还要给网页版用那就得用flask-cors来处理跨域。我当时提前加上了这个库后来确实派上了用场。还有个安全问题值得提一嘴所有涉及用户输入的地方都要做基础过滤。虽然小程序端没有 SQL 注入的经典场景但后端接口如果直接把用户输入拼进 SQL 查询用参数化查询是最稳妥的做法。Flask 的 ORM 或原生 SQL 的占位符都能防护这个问题。最后说说我对这个项目后续扩展的想法也算是做完之后的经验沉淀。整个应用最值得升级的点有三个一是把打卡数据做成图表统计运动量和热量摄入按周按月可视化这块用 ECharts 在小程序端就能实现后端只需要提供统计数据接口二是把推荐算法做得更细致比如把用户的饮食记录作为反馈信号推荐结果根据用户实际吃了什么来动态调整这就变成一个简单的协同过滤了三是扩展成多端复用因为后端是完全独立的一套 API以后想出 H5 版或者 uni-app 版前端重新写一套页面就行后端几乎不用动。我个人在实际操作中的体会是这个小程序加 Flask 的组合特别适合做个人独立开发的中小型工具类应用。微信小程序解决了获客和分发问题用户不用下载 App打开就能用Flask 足够轻量一个人维护完全没负担Python 生态又给后续加功能留足了空间。对想练手全栈开发的朋友来说用这个组合做一个完整项目从微信登录、数据库设计、接口开发到小程序页面实现一整条链路走下来收获的东西比单纯看教程多得多。如果你也在做类似的项目建议先把登录和数据库设计这两块想清楚后面基本就顺了。写完这些我再给一个小技巧开发阶段可以在 Flask 里写一个简单的请求日志装饰器把每个请求的路径、参数、耗时打到控制台。调试小程序和联调接口的时候这个日志能帮你快速定位是前端没传到参数还是后端逻辑出了问题。等部署上线了再把这个装饰器关掉就行。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑