ARTICLE DETAIL

资讯详情

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

Python Flask 与 uniapp 构建微信小程序饮食健康管理系统实战

Python Flask 与 uniapp 构建微信小程序饮食健康管理系统实战 前段时间完整做了一套饮食健康管理系统技术栈就是标题里那套Python Flask 做后端接口uniapp 编译成微信小程序做前端。从数据库设计、API 开发、小程序页面搭建到服务器部署上线整个链路自己走了一遍。这中间踩了不少坑也有了一些真实可用的经验。这篇就围绕这套系统把完整的技术方案、关键实现细节和那些资料里不会写的实战注意点梳理出来给正在做同类项目、或者准备用这套技术栈接小程序的读者一个参考。标题里的关键词很明确Python、Flask、uniapp、微信小程序。这套组合在个人项目和中小型团队项目里非常常见。Flask 轻量灵活适合快速搭建 APIuniapp 一套代码能出微信小程序、H5 和 App微信小程序又有天然的分发渠道。三者配合前期开发效率高后期维护也不至于太痛苦。下面按我实际做项目的顺序从选型、后端设计、前端开发、营养计算逻辑、联调排错到部署上线一段一段说清楚。1. 为什么选 Flask uniapp 这套组合架构选型的真实考量1.1 Flask 在这个项目里到底够不够用先说后端。饮食健康管理系统的核心业务其实不复杂用户管理、食物食材数据、饮食记录、营养统计、推荐逻辑。这些功能属于典型的 CRUD 加一些算法逻辑每天数据量也在个人用户量级Flask 完全能撑住。我当时也犹豫过要不要上 DjangoDjango 自带 Admin 后台、ORM、认证体系看似省事但项目里很多业务和 Django 默认的那套并不匹配反而要花时间去覆盖默认行为。FastAPI 性能理论更好但生态和社区资料相对少国内中小项目里 Flask 的教程和踩坑案例丰富得多遇到问题好查。最终定的就是 Flask SQLAlchemy MySQL开发环境先用 SQLite部署切 MySQL。Flask 的蓝图中后期会越用越爽。把所有路由按业务拆成 auth、food、meal、statistics、recommend 等模块app 工厂模式初始化扩展代码结构一目了然。这套系统不是一次性玩具后续加功能、改表结构、加接口清晰的工程组织能省下大量排查时间。1.2 uniapp 比原生微信小程序省在哪前端为什么用 uniapp 而不是直接写原生小程序最直接的理由是同一套代码可以编译到微信小程序、支付宝小程序、H5、App后面如果有跨端需求不用推翻重写。uniapp 用 Vue 语法开发组件化和数据绑定比原生 WXML 舒服尤其列表渲染、条件渲染、计算属性这些写起来非常顺手。组件库层面我用的是 uview-plus官方插件市场直接导入 HBuilderX。这个库对 uniapp 的兼容性做得不错表单、弹窗、日历、图表选择器、加载动画这些高频组件都有。饮食记录页面里的日期选择、份量选择、营养进度条基本靠 uview-plus 省了一半开发量。有一点要注意uview-plus 主要是给 Vue3 版本的 uniapp 用的项目创建时如果选了 Vue2建议自己确认版本兼容省得后面一堆报错。1.3 前后端分离下的整体架构这套系统的整体架构很清晰前端uniapp 项目运行在微信小程序容器里通过 HTTP/HTTPS 请求后端 API后端Flask 提供 RESTful API负责业务逻辑、数据校验、鉴权、营养计算数据层MySQL 存储用户、食材、饮食记录等结构化数据Redis 暂时没引入但已经预留了缓存设计的扩展点数据流转大概是用户在小程序端录入一顿饭的内容前端把食物名称、重量、用餐时间等通过 POST 请求发给 FlaskFlask 校验后写入数据库同时调用营养计算服务按食材成分表算出这顿饭的卡路里、蛋白质、碳水、脂肪返回给前端展示用户查看统计页时Flask 再按日期范围聚合这些数据生成当天的营养汇总和趋势图数据。这套架构的好处是职责边界清晰。前端只管页面交互后端管业务和数据数据库层管持久化。后续如果要做 App 端前端代码几乎不用改如果要做 Web 管理后台直接复用同一套 API 就行。2. 先设计数据模型再写接口核心业务拆解与 Flask 工程组织2.1 饮食健康系统必须有哪几张核心表动手写接口前我花了比较多的时间设计数据表后期改字段的成本远高于一开始多想几步。这套系统我最终沉淀下来四张核心表users用户表记录微信 openid、昵称、身高、体重、年龄、性别、活动等级、健康目标减脂/增肌/保持。这些是后续算推荐摄入量的必要参数foods食材食物表记录名称、分类、每 100 克所含热量、蛋白质、碳水化合物、脂肪、膳食纤维等。来自公开食物成分数据我做了导入脚本清洗入库meals一餐记录表记录用户、餐次类型早/午/晚/加餐、用餐时间、备注meal_items餐食明细表记录某餐下具体吃了哪个食物、吃了多少克通过外键关联 meals 和 foods为什么要拆 meal 和 meal_items 两张表而不是在一条记录里塞数组字段因为一餐可能包含米饭、鸡胸肉、蔬菜好几项拆开之后统计非常灵活既可以按餐维度看一顿饭的热量也可以按食物维度统计某类食物吃了多少次。meal_items 里冗余存一份计算后的热量和三大营养素值这样统计的时候不用每次都去 join foods 联表再算一遍。以下是 SQLAlchemy 模型的核心代码直接贴出来可以作为参考from datetime import datetime from extensions import db class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) openid db.Column(db.String(64), uniqueTrue, nullableFalse) nickname db.Column(db.String(64), default) avatar db.Column(db.String(255), default) gender db.Column(db.SmallInteger, default0) # 0未知 1男 2女 height db.Column(db.Float, default0) # 单位cm weight db.Column(db.Float, default0) # 单位kg age db.Column(db.SmallInteger, default0) activity_level db.Column(db.SmallInteger, default1) # 1轻度 2中度 3重度 goal db.Column(db.String(16), defaultkeep) # gain/lose/keep created_at db.Column(db.DateTime, defaultdatetime.now) def to_dict(self): return { id: self.id, nickname: self.nickname, avatar: self.avatar, gender: self.gender, height: self.height, weight: self.weight, age: self.age, activity_level: self.activity_level, goal: self.goal, created_at: self.created_at.strftime(%Y-%m-%d %H:%M:%S), } class Food(db.Model): __tablename__ foods id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse) category db.Column(db.String(32), default) calorie db.Column(db.Float, default0) # 每100g大卡 protein db.Column(db.Float, default0) # 每100g克数 carbohydrate db.Column(db.Float, default0) fat db.Column(db.Float, default0) fiber db.Column(db.Float, default0) created_at db.Column(db.DateTime, defaultdatetime.now) class Meal(db.Model): __tablename__ meals id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) meal_type db.Column(db.String(16), nullableFalse) # breakfast/lunch/dinner/snack note db.Column(db.String(255), default) eaten_at db.Column(db.DateTime, defaultdatetime.now) class MealItem(db.Model): __tablename__ meal_items id db.Column(db.Integer, primary_keyTrue) meal_id db.Column(db.Integer, db.ForeignKey(meals.id), nullableFalse) food_id db.Column(db.Integer, db.ForeignKey(foods.id), nullableFalse) weight_grams db.Column(db.Float, nullableFalse) # 实际食用克数 calorie db.Column(db.Float, default0) protein db.Column(db.Float, default0) carbohydrate db.Column(db.Float, default0) fat db.Column(db.Float, default0)字段类型和注释建议一开始就写清楚。Flask 的 SQLAlchemy 模型字段默认值、索引、外键关系这些部署到线上再补是很折腾的事情。2.2 Flask 工程用蓝图拆模块别把所有路由堆在一个文件刚学 Flask 的时候习惯把路由全写在 app.py 里项目一大就完全失控。这套系统我换成工厂模式加蓝图project/ ├── app.py # 应用入口create_app() ├── extensions.py # db、jwt 等扩展实例 ├── config.py # 配置数据库地址、密钥等 ├── blueprints/ │ ├── auth.py # 登录注册、个人信息 │ ├── food.py # 食材库查询、搜索 │ ├── meal.py # 饮食记录 CRUD │ ├── statistics.py # 营养统计 │ └── recommend.py # 饮食推荐 ├── services/ │ ├── nutrition_service.py │ └── recommend_service.py ├── models/ │ ├── user.py │ ├── food.py │ └── meal.py └── utils/ ├── response.py # 统一返回格式 └── auth_decorator.py # 登录校验装饰器app.py 里注册蓝图的样子from flask import Flask from extensions import db from blueprints import auth, food, meal, statistics, recommend def create_app(): app Flask(__name__--) app.config.from_object(config.Config) db.init_app(app) app.register_blueprint(auth.bp, url_prefix/api/auth) app.register_blueprint(food.bp, url_prefix/api/foods) app.register_blueprint(meal.bp, url_prefix/api/meals) app.register_blueprint(statistics.bp, url_prefix/api/statistics) app.register_blueprint(recommend.bp, url_prefix/api/recommend) return app这样每一个蓝图的文件里只管自己那部分路由后期定位接口、加中间件、做版本控制都方便。接口前缀统一带 /api后面如果要做版本升级直接加 /v1、/v2 也容易。2.3 一个完整 API 从请求到返回的链路设计以记录一顿饭为例前端请求 POST /api/meals请求体大概是{ meal_type: breakfast, eaten_at: 2025-06-15 08:30:00, note: 在家吃的, items: [ { food_id: 101, weight_grams: 200 }, { food_id: 205, weight_grams: 150 } ] }Flask 端接收 JSON做的事按顺序拆成了四步第 1 步解析参数检查 meal_type 是否合法、items 是否为空、每个 food_id 是否存在第 2 步为每一条明细换算营养值计算逻辑是 food 表里的每百克数值乘实际克数再除以 100第 3 步写入 meals 表和 meal_items 表注意放在同一个事务里避免只写了一半第 4 步返回创建成功后的餐次信息和汇总营养数据前端可以直接用这个结果刷新展示我在项目里做了统一的返回格式比如 {code: 0, msg: ok, data: {...}}。code 不等于 0 就表示出错msg 是给用户看的友好提示data 是业务数据。这样前端拦截器只要判断 code 就能统一处理错误省掉每个页面单独处理异常。验证失败时返回 code4001 之类的业务码和 HTTP 状态码分开方便后续排查。分页接口也用统一格式列表数据返回 {total: 100, page: 1, page_size: 20, items: [...]}前端拿 total 判断是否还有更多拿 items 渲染列表。2.4 微信登录与 JWT 鉴权小程序的认证就是这么做的小程序端没有传统的用户名密码登录微信生态的标准做法是 wx.login 换取 code后端拿 code 到微信接口换 openid然后签发自己的 token。流程是小程序调用 wx.login 拿到临时 code前端把 code POST 给 Flask 的 /api/auth/loginFlask 用 code 调微信的 code2session 接口得到 openid 和 session_key以 openid 作为用户唯一标识查库或新建用户Flask 用自己的密钥签发一个 JWT token返回给前端之后前端每次请求在 header 里带 Authorization: Bearer Flask 在接口校验装饰器里解析 token识别用户身份。这个方案不用维护服务端 session小程序端拿到 token 后可以本地保存过期了重新登录就行。以下是登录接口核心代码import requests from flask import Blueprint, request, jsonify from extensions import db, jwt_token from models.user import User bp Blueprint(auth, __name__) bp.post(/login) def login(): data request.get_json() code data.get(code) if data else if not code: return jsonify({code: 4001, msg: 缺少code, data: None}) # 用code换openid resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: app.config[APP_ID], secret: app.config[APP_SECRET], js_code: code, grant_type: authorization_code, }, timeout5, ).json() openid resp.get(openid) if not openid: return jsonify({code: 4002, msg: 微信登录失败, data: None}) user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid) db.session.add(user) db.session.commit() token jwt_token.generate(payload{uid: user.id}) return jsonify({code: 0, msg: ok, data: {token: token, user: user.to_dict()}})鉴权装饰器放在 utils/auth_decorator.py 里受保护接口统一加 login_required。Flask 里装饰器要保留函数名和元信息定义时记得加 wraps否则接口名全变成装饰器函数名问题很难发现。3. 小程序端从 HBuilderX 起步页面结构、组件选型与请求封装3.1 HBuilderX 创建 uniapp 项目和微信开发者工具对接前端我用 HBuilderX 创建 uniapp 项目选择默认模板。这里有一个版本选择很关键如果要用 uview-plus项目需要基于 Vue3 版本。创建时 HBuilderX 会让你选 Vue2 还是 Vue3我当时直接选了 Vue3后面导入 uview-plus 基本没遇到兼容问题。在 manifest.json 里的微信小程序配置里需要填上自己的小程序 AppID。没有的话微信公众平台注册一个或者先用测试号。AppID 是连接微信开发者工具的关键填错或者不填预览、上传、真机调试都会出问题。HBuilderX 写完代码点运行到小程序模拟器会自动唤起微信开发者工具加载编译产物。需要注意的是uniapp 编译出来的文件通常在项目的 unpackage/dist/dev/mp-weixin 目录微信开发者工具导入这个目录。用 HBuilderX 自动集成的方式会处理这些但如果是多人协作其他人也得装 HBuilderX 和微信开发者工具环境对齐要做成一份文档不然新人光环境就能折腾半天。3.2 页面结构与 tabBar 设计小程序端页面我按业务拆了五个主要页面首页今日饮食概览、当日摄入热量和目标的差距、昨日对比记录页快速录入三餐和加餐搜索食材添加统计页按周/月查看热量摄入趋势、三大营养素占比推荐页基于用户目标生成的每日推荐热量和示例食谱我的页个人信息、身高体重目标设置tabBar 配置在 pages.json 里小程序页面的底部导航栏就靠它控制。每个 tab 页都要配图标没有图标的话 tabBar 会显示得很简陋。图标用 81px 81px 的 PNG放在 static/tabbar 目录不能直接用网络图这是小程序端的一个硬性细节第一次做容易踩到。pages.json 里一个 tab 页面大概长这样{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 今日概览 } }, { path: pages/record/record, style: { navigationBarTitleText: 饮食记录 } }, { path: pages/statistics/statistics, style: { navigationBarTitleText: 营养统计 } }, { path: pages/recommend/recommend, style: { navigationBarTitleText: 饮食推荐 } }, { path: pages/mine/mine, style: { navigationBarTitleText: 我的 } } ], tabBar: { color: #999999, selectedColor: #22b573, backgroundColor: #ffffff, list: [ { pagePath: pages/index/index, text: 概览 }, { pagePath: pages/record/record, text: 记录 }, { pagePath: pages/statistics/statistics, text: 统计 }, { pagePath: pages/recommend/recommend, text: 推荐 }, { pagePath: pages/mine/mine, text: 我的 } ] } }3.3 请求封装一次封装全项目受益如果每个页面里直接调 uni.request项目大了之后会非常痛苦请求地址分散、token 加到哪不统一、错误提示各写各的、加载弹窗满天飞。我用一个 request.js 统一封装所有请求核心逻辑包含四个部分baseURL 统一管理开发环境指向本地局域网 IP生产环境指向正式域名自动携带 token从本地存储读取并加到 header统一处理 code非 0 直接 uni.showToast 提示用户前端不用每处重复写错误判断支持显示/隐藏全局加载动画避免页面里到处手动调 uni.showLoading这个项目的请求封装核心代码可以作为参考// utils/request.js const BASE_URL https://your-server.com/api export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || , }, success: (res) { if (res.data.code 0) { resolve(res.data.data) } else if (res.data.code 401) { // token 过期跳回登录页 uni.showToast({ title: 请重新登录, icon: none }) uni.reLaunch({ url: /pages/login/login }) reject(res.data) } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) }, }) }) }封装的收益在写过二三十个接口之后特别明显。后续接口加鉴权、加参数签名、加埋点只用改这一个文件不用逐个页面改。请求封装值得在最开始就写好别等代码多了再回头补。3.4 列表加载更多onReachBottom 触发分页的完整写法饮食记录的历史列表是典型的长列表不可能一次查出全部标准做法是分页加载滚动到底部拉下一页。微信小程序提供了 onReachBottom 生命周期在 pages.json 里设置 onReachBottomDistance 控制触底距离页面里实现分页逻辑export default { data() { return { list: [], page: 1, pageSize: 10, total: 0, loading: false, isFinished: false, } }, onLoad() { this.fetchList() }, onReachBottom() { if (this.isFinished || this.loading) return this.fetchList() }, methods: { async fetchList() { this.loading true try { const res await this.$request({ url: /meals, data: { page: this.page, pageSize: this.pageSize }, }) this.list this.page 1 ? res.items : this.list.concat(res.items) this.total res.total this.page this.isFinished this.list.length this.total if (this.isFinished) { uni.showToast({ title: 没有更多了, icon: none }) } } finally { this.loading false } }, }, }页面上加一个判断isFinished 为 true 就显示没有更多了。注意 onReachBottom 触发的频率可能很高loading 变量和 isFinished 变量必须提前判断否则用户快速滑动时同一页数据会请求好几遍会出现数据重复这是列表分页最常见的 bug。3.5 顶部导航栏高度与自定义导航适配微信小程序的导航栏在不同机型上高度不一样尤其有刘海屏和灵动岛之后直接使用默认导航栏虽然省事但要做自定义标题、胶囊按钮右侧扩展就必须手动适配顶部安全区。推荐的做法是用 uniapp 提供的能力获取系统信息const systemInfo uni.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight // 状态栏高度页面布局里给自定义导航栏预留 padding-top高度等于状态栏高度加上 44px 左右的胶囊区域。实际操作里可以封一个 NavBar 组件把返回按钮、标题、右侧操作位都做进去所有页面统一用它而不是每个页面自己写一遍这样样式不一致的问题就能从根上避免。4. 营养采集与统计推荐让系统会算而不只是会记4.1 食物成分数据的来源与营养换算饮食健康管理系统和普通记账本最大的区别在于每一条饮食记录背后要挂营养数值。数据源我用的是公开的食物成分数据库里面每 100 克食材对应的热量、蛋白质、碳水、脂肪、膳食纤维都是现成的。但原始数据比较乱需要清洗比如有重复项、单位不统一、部分生熟状态混在一起。我做了一个 Python 清洗脚本把数据规范化导入 foods 表分类、名称、营养素字段逐一核对。用户录制饮食的时候选择食物和克数后端换算函数很直接def compute_nutrition(food, weight_grams): rate weight_grams / 100.0 return { food_id: food.id, weight_grams: weight_grams, calorie: round(food.calorie * rate, 1), protein: round(food.protein * rate, 2), carbohydrate: round(food.carbohydrate * rate, 2), fat: round(food.fat * rate, 2), fiber: round(food.fiber * rate, 2), }这里有个经验在 meal_items 表里把换算后的营养值存下来比每次都联表算要高效得多而且后续如果食物库里的成分数据被修正历史记录的统计结果不会跟着变化这个很重要。做过一次数据修正导致历史统计全部漂移之后我就坚定用记录时固化数值的方案了。4.2 按天聚合一天吃得好不好一眼看懂统计页面需要按天、按周、按月汇总。SQLAlchemy 里按日期分组聚合核心是拿到某天所有 meal_items 数据再把营养值求和from sqlalchemy import func from models.meal import Meal, MealItem result ( db.session.query( func.date(Meal.eaten_at), func.sum(MealItem.calorie), func.sum(MealItem.protein), func.sum(MealItem.carbohydrate), func.sum(MealItem.fat), ) .join(MealItem, MealItem.meal_id Meal.id) .filter(Meal.user_id current_user.id) .filter(Meal.eaten_at start_date) .filter(Meal.eaten_at end_date timedelta(days1)) .group_by(func.date(Meal.eaten_at)) .all() )前端图表可以直接拿这些数据渲染。uview-plus 本身有部分图表组件但复杂的折线图、环形图我建议用 ucharts 的 uniapp 版本渲染性能在微信小程序里也能接受。统计页把今日摄入量 vs 推荐量做成环形图、把近 7 天热量趋势做成折线图用户对自身的饮食结构能直观感受到。三大营养素占比也很有意思。很多人每天只看热量其实蛋白质、碳水、脂肪的比例对健康影响很大。我在统计页加了百分比显示计算逻辑很简单蛋白热量按每克 4 大卡、碳水每克 4 大卡、脂肪每克 9 大卡换算然后求占比。实测反馈里用户往往惊讶于自己脂肪摄入比例不低这比单纯给个热量数字有意义得多。4.3 推荐逻辑的实现BMR 加减乘除就是最朴素的方案饮食推荐功能我用的算法是基础代谢率加活动系数加目标调整。基础代谢率采用 Mifflin-St Jeor 公式男性BMR 10 体重(kg) 6.25 身高(cm) - 5 年龄(岁) 5女性BMR 10 体重(kg) 6.25 身高(cm) - 5 年龄(岁) - 161然后用活动系数校准总能量消耗 TDEE久坐人群乘 1.2轻度活动乘 1.375中度活动乘 1.55重度活动乘 1.725。最后根据用户目标做调整增肌每天多摄入 200 到 300 大卡减脂每天少摄入 300 到 500 大卡保持就维持 TDEE。这套逻辑并不神秘但有一个关键细节所有数值必须在用户完善了身高体重年龄性别等资料之后才能计算。我第一次实测的时候本机测试账号没填这些字段接口直接报除以零后来加了资料完整度校验残缺资料返回提示让用户先去完善。前端在推荐页也得有引导逻辑不能光展示一个空状态。三大营养素的分配比率我按比较通用的方案蛋白质 20%-30%碳水 45%-55%脂肪 20%-30%。根据每日总热量换算出各营养素的克数再结合食物库推荐食材。推荐结果页面会显示你今天的推荐摄入量是 2100 大卡其中蛋白质 120g、碳水 260g、脂肪 60g并推荐若干符合这个模型的食材组合。注意这里做的是基于通用公式的参考值不是医学营养处方。对有疾病或者特殊身体状况的用户应该提示他们咨询专业人士。接口文档和页面里也要附带这一句提示避免工具被误解为医疗建议。5. 前后端联调与真机调试资料里不会写的坑5.1 本地联调微信开发者工具如何访问 Flask 本地服务开发阶段最大的一道坎就是Flask 跑在本地电脑小程序在微信开发者工具里两者怎么通。步骤不难Flask 启动时 host 设为 0.0.0.0监听所有网卡在 cmd 或 ipconfig 里查到电脑的局域网 IP比如 192.168.1.100request.js 里的 BASE_URL 改成 http://192.168.1.100:5000/api微信开发者工具右上角详情-本地设置勾选不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书这个不校验合法域名是开发阶段的开关线上环境必须关闭并在微信公众平台配置正式的 request 合法域名。很多人卡在本地联调就是不知道这个开关。真机预览又不同。手机和电脑连同一个 WiFi小程序真机调试时把本地服务地址指向电脑的局域网 IP。如果手机访问不到先排查电脑防火墙是不是拦了 5000 端口实际中这个原因占了大半。Windows 防火墙默认对 Python 公网入站是拦截的需要手动放行。5.2 Android 和 iOS 的 HTTP 明文限制开发阶段用 http 没有问题但到了安卓真机上可能会遇到请求直接失败的情况。原因是 Android 9.0 之后默认禁止明文 HTTP 流量。uniapp 项目需要在 manifest.json 里找到 App 端的 android 配置开启android:usesCleartextTraffic或者在网络安全配置里允许特定域名的明文流量。这一步在开发阶段用开发者工具完全看不出来只有上了真机才暴露。iOS 对 http 的限制主要是在 App Store 审核环节微信小程序内部的 web-view 检测规则也要注意。我的建议是开发联调可以走 http IP但准备提审或体验版时就尽快切到正式 HTTPS 域名免得后面所有问题混合在一起排查起来非常费劲。5.3 页面列表加载更多和性能的坑列表页数据量变多之后小程序端会有两个明显问题一次性渲染几百条数据滚动卡顿搜索食材时频繁输入触发请求后端被刷爆第一个问题用分页加 onReachBottom 已经能解决大部分另外列表项的图片要加 lazy-load 属性。第二个问题需要在输入框搜索接口上加防抖前端用 setTimeout 延时 300ms 再发请求只保留最后一次结果实测下来请求量能减少大概一半。后端也要加一个基础缓存逻辑热门食材列表、食物搜索的 top 100 这些不常变化的接口可以在内存里缓存一段时间或者直接上 Redis。饮食记录接口不用缓存每次都要最新数据。5.4 搜索请求的防抖写法食物搜索是用户高频操作输入鸡会边打字边发请求接口压力很碎。前端常规做法是加防抖export default { data() { return { keyword: , timer: null, results: [] } }, watch: { keyword(newVal) { clearTimeout(this.timer) this.timer setTimeout(() { this.searchFood(newVal) }, 300) }, }, }防抖逻辑很短但对于用户体验和后端压力都有好处。实际开发中这类细节积累多了项目质量才会显得不一样。6. 从开发机到线上部署Flask 项目的完整上线实操6.1 上线环境准备服务器、Python 虚拟环境与依赖管理开发完本地流程后下一步就是部署到服务器。我用的是一台 Linux 服务器整个准备过程整理如下安装 Python 3.10 和 pip装 python3-venv 创建虚拟环境别直接往系统 Python 里装依赖避免版本冲突把代码传到服务器在项目目录用虚拟环境重新安装 requirements.txt 里的依赖数据库切换到 MySQL安装 MySQL 服务端建库建账号注意字符集要选 utf8mb4否则 emoji 和中文生僻字会乱码这算是一个容易忽略的隐藏坑在 config.py 里用环境变量管理数据库地址、微信 AppID、AppSecret、JWT 密钥不要把密钥提交到代码仓库依赖管理有个建议用 pip freeze 导出 requirements.txt 时里面可能会带出很多用不到的传递依赖。交付前可以先创建新虚拟环境按 requirements.txt 装一遍验证能跑通避免部署时缺包或者版本不匹配。6.2 Gunicorn gevent 跑起 Flask 服务直接用 Flask 内置的 app.run() 跑服务只适合开发环境上线必须用 WSGI 服务器。我用的 Gunicorn 加 gevent worker启动命令如下gunicorn -w 4 -k gevent -b 127.0.0.1:5000 app:create_app()参数解释-w 4开 4 个 worker 进程多核 CPU 可以多开但不要盲目开太多内存会爆-k gevent协程 worker适合 Flask 这种 IO 密集型的应用-b 127.0.0.1:5000绑定本机端口不要直接暴露到公网由前一层 Nginx 转发进程管理用 systemd 配置一个服务保证崩溃自动重启、开机自启。实际配置 systemd unit 文件的时候要注意 Environment 里指定环境变量路径用绝对路径日志输出到指定文件。6.3 Nginx 反向代理与 HTTPS微信小程序上线的硬要求小程序正式环境下request 合法域名必须备案且必须是 HTTPS。这一步是微信的硬性规定不能跳过。我用的方案是 Nginx 做反向代理加 SSL 证书。Nginx 配置核心思路监听 80 端口做 HTTP 跳转到 HTTPS监听 443 端口启用 SSL证书用免费的 ACME 证书就行记得配自动续期将 /api 前缀的请求转发到 127.0.0.1:5000 也就是 Gunicorn静态文件如前端打包产物由 Nginx 直接服务不用经过 Flask配置片段如下server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }微信公众平台后台配置 request 合法域名时填 https://your-domain.com然后小程序重新编译上传即可。如果出现不在以下 request 合法域名列表中基本就是域名没配置或者配置没生效等一两分钟再试。6.4 上线后最容易出问题的地方部署完成不代表万事大吉有几个问题我在上线后陆续暴露出来第一是时区问题。服务器时区如果没设置成 Asia/ShanghaiFlask 的 datetime.now 生成的时间会差 8 小时饮食记录的历史数据全乱。解决方法是服务器设置时区并且在写时间时统一用 Python 的 timezone 处理。第二是数据库备份。上线第一周就要把自动备份脚本跑起来我后来写了每天凌晨用 mysqldump 打包数据库并保留最近 7 天备份的定时任务。备份脚本看似简单真到数据丢失那天才觉得是救命稻草。第三是日志监控。Gunicorn 的访问日志和错误日志分别输出到文件Flask 应用里用 logging 模块把业务异常记录下来上线初期我靠日志定位了不少线上问题。也可以接入 Sentry 之类错误监控但小项目先做好文件日志和简单告警就够用。第四是版本迭代时接口兼容。小程序发版和接口更新很难做到完全同步老版本小程序还在线上使用时后端接口尽量不要做破坏性修改。新增字段用可选参数删接口要提前通知前端发布新版本后再下掉这是前后端分离项目的基本纪律。部署之后整个系统跑通了用户在微信小程序搜索到这个小程序打开后授权登录完善身体数据每天记录三餐查看营养统计和推荐饮食。Flask 后端稳定处理请求和算数据数据库在凌晨默默备份Nginx 在入口转发着每个请求。这一套链路从头到尾自己搭起来还是很有成就感的。回头整理一个关键经验不管是 Flask 的工程组织、小程序端的请求封装还是营养数据的记录时固化好的设计看似多花了一点前期时间但后期省下来的时间远远不止这些。饮食健康管理这个方向还有不少可以扩展的地方比如对接智能体脂秤数据、生成一周食谱计划、按营养素缺口推荐补充食材底层这套架构完全能支撑。给后面做同类项目的读者一个建议先花一周把数据模型和接口约定设计扎实再开始写页面你后面会感谢当时的自己。
返回列表