ARTICLE DETAIL

资讯详情

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

急救科普小程序开发实战:Flask后端与微信小程序前后端分离

急救科普小程序开发实战:Flask后端与微信小程序前后端分离 急救知识科普小助手这个项目说起来是我给社区志愿服务队义务做的一个小工具。日常培训时我注意到一个很典型的问题大家刷到心肺复苏、海姆立克急救法的视频时觉得“看懂了”真到需要上手的时候顺序、位置、频率又全乱了。单纯做内容号解决不了这个问题于是我把目标锁定在“微信小程序”前端用微信原生框架后端用 Python Flask数据落在 SQLite 里再通过一条条 REST 接口把内容和题目喂给小程序。标题里写成“APP 小程序”其实就是微信生态里的轻应用形态比独立 APP 少走一遍应用商店审核也方便现场扫码使用。这篇文章我会把项目拆开讲一遍重点不是“我做了什么”而是“为什么这么做”以及“遇到问题怎么查”。如果你正准备做一个 Flask 后端 微信小程序的前后分离项目这篇可以作为一份可参考的落地笔记。1. 项目整体设计与功能模块拆解做这种科普类小程序最怕一开始就把功能铺得太开。急救知识的真实使用场景很清晰平时随时翻看、学完自测、遇到紧急情况能快速找到对应处理办法。围绕这三个场景我最终把功能收敛成五块首页分类与推荐、科普文章列表、文章详情、每日答题自测、个人记录中心。1.1 从用户痛点倒推功能清单目标用户不是我这种技术人员而是社区里各个年龄段的居民。所以交互不能复杂打开就要看得懂。首页第一屏只放了四个入口心肺复苏、海姆立克、止血包扎、日常急症。这四个分类几乎覆盖了家庭和公共场所最常见的意外情况。用户点击分类后进入文章列表文章标题要写得像“场景化提示”类似“溺水后为什么要先检查呼吸而不是先控水”而不是“溺水急救知识”。答题模块的初衷是解决“觉得自己会了”的错觉。每次从后端拉 5 道判断题或单选题答完立刻返回解析。题目来源于权威急救指南我会专门留一个字段记录来源方便日后溯源。个人记录中心里收藏和历史浏览选择用微信本地缓存实现。为什么不入库因为小程序没有强制登录多设备同步对科普工具来说不是刚需用wx.setStorageSync反而更省事用户也不需要授权手机号。1.2 技术组合为什么是 Python、Flask、微信小程序这个组合最大的优点是开发效率高。Python 的 Flask 框架非常轻不需要像 Django 那样配置一堆默认模块一个文件就能把接口跑起来。我在家里和服务器上都是 Python 3.10 环境安装完依赖直接flask run就能联调。微信小程序的最大优势是免安装培训时把二维码往屏幕上一放微信扫码就能打开不需要经历“下载 APP—注册—授权”的流失过程。后端选 Flask 而不是 Node、Spring Boot还有一层原因是团队维护成本。志愿队里帮忙做内容维护的人懂 Python后续想加数据可视化、爬取急救资讯或者做关键词分析都方便。小程序端用原生微信框架不引第三方 UI 库页面数量不多组件自己写比追新框架更可控。整套项目就是“一只手掌得住的体积”前端 5 个页面后端 12 个接口一张 SQLite 表文件。2. 后端选型Flask 接口设计、数据库建模与核心代码后端是整个项目的“内容中枢”。如果把小程序比作点餐台Flask 就是后厨它不负责摆盘但所有菜品都得经过它才能上桌。所以后端最重要的事情不是“能跑”而是“接口稳定、数据结构清晰、改内容方便”。2.1 为什么在这个项目里不选 DjangoFlask 和 Django 之间没有绝对的好坏。如果你做的是大型 SaaS 系统Django 全家桶很香但急救知识科普小程序后端只有十几张表、几个查询接口Django 的 admin、auth、模板引擎、表单系统基本都用不上反而会让项目看起来臃肿。Flask 没有强约束怎么组织目录你自己定路由写起来也是最直白的。举个例子文章列表接口在 Flask 里就是这样from flask import request, jsonify app.route(/api/articles) def get_articles(): category_id request.args.get(category) page int(request.args.get(page, 1)) page_size int(request.args.get(pageSize, 10)) # 查询逻辑这种自由度对小团队很友好但代价是项目规范要靠自觉。我的建议是即使是用 Flask也要按模块建目录不要把所有路由堆在同一个app.py里。后面第四部分会给出目录模板。2.2 接口规划一张表说清楚谁调谁写代码之前先把接口定义在纸上。这一步帮你把“小程序页面”和“后端能力”对应起来。这个项目的核心接口表如下方法路径功能参数返回GET/api/categories首页分类无分类 id、名称、图标路径GET/api/articles文章列表category、page、pageSize文章 id、标题、封面、摘要GET/api/articles/文章详情无正文、来源、更新时间GET/api/search关键词搜索keyword、page文章列表GET/api/quiz获取题目size题目列表选项POST/api/quiz/submit提交答案answers 数组得分、每题解析第一版接口就这 6 个够用且不多。文章列表接口里的page和pageSize是给小程序“加载更多”用的实现方式是数据库LIMIT/OFFSET也就是offset (page - 1) * pageSize。比如第二页page2pageSize10就从第 11 条开始取。小程序端在onReachBottom里把 page 加一直到后端返回的hasMore变成false。2.3 数据模型文章、分类、题库三张表后端我用的 ORM 是Flask-SQLAlchemy。数据库直接选 SQLite因为文章和题目数据量不大单文件也好备份。以后要换 MySQL只要把数据库连接串改一下模型代码不用大动。三张核心表的结构如下from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Category(db.Model): __tablename__ category id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse) icon db.Column(db.String(200)) class Article(db.Model): __tablename__ article id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse) summary db.Column(db.String(500)) content db.Column(db.Text, nullableFalse) source db.Column(db.String(200)) category_id db.Column(db.Integer, db.ForeignKey(category.id)) created_at db.Column(db.DateTime, defaultdatetime.now) class Quiz(db.Model): __tablename__ quiz id db.Column(db.Integer, primary_keyTrue) question db.Column(db.String(500), nullableFalse) options db.Column(db.Text, nullableFalse) # JSON 数组字符串 answer db.Column(db.Integer, nullableFalse) # 正确选项下标 analysis db.Column(db.Text)这里需要说明一个常见的坑不要把答案直接暴露在题目接口里。我第一版偷懒在 GET /api/quiz 时把answer和analysis一起返回了结果用浏览器看接口时连正确答案一起看到了小程序端调试时很容易被截屏外传。后来改成“题目返回时隐藏 answer提交后后端判断并返回解析”这个接口才算可用。3. 小程序前端页面架构与关键功能实现小程序端是用户直接接触的部分。有人说前端代码简单但实际做下来页面布局、请求封装、分页加载、题库交互这些细节一个都不能省。下面是我拆解的四大块。3.1 页面结构、tabBar 与顶部导航适配小程序页面先在app.json里注册。这个项目一共五个页面首页、列表页、详情页、答题页、个人中心。其中首页、答题页、个人中心放在底部 tabBar方便用户随时切换。{ pages: [ pages/index/index, pages/list/list, pages/detail/detail, pages/quiz/quiz, pages/mine/mine ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/quiz/quiz, text: 答题 }, { pagePath: pages/mine/mine, text: 我的 } ] } }如果你准备用自定义顶部导航就会遇到“微信小程序顶部导航栏高度”这个热搜问题。自定义导航时内容会被顶到状态栏下面必须手动取状态栏高度。比较稳的写法是在页面 onLoad 里调用wx.getWindowInfo()或wx.getSystemInfoSync()拿到statusBarHeight然后给外层容器加padding-top。iPhone X 这类设备还要处理底部的safe-area-inset-bottom否则页面底部会被小黑条挡住。3.2 列表页“加载更多”的完整实现文章列表页我最想强调的是“加载更多”不要写成一次性拉全部数据。每次进入列表页只请求第一页上滑到底部触发onReachBottom时再请求下一页。完整脚本如下Page({ data: { articles: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.fetchArticles() }, fetchArticles() { if (!this.data.hasMore || this.data.loading) return this.setData({ loading: true }) wx.request({ url: ${getApp().globalData.baseUrl}/api/articles, data: { category: this.data.categoryId, page: this.data.page, pageSize: this.data.pageSize }, success: (res) { const list res.data.data.list this.setData({ articles: this.data.articles.concat(list), page: this.data.page 1, hasMore: res.data.data.hasMore, loading: false }) }, fail: () { this.setData({ loading: false }) wx.showToast({ title: 加载失败, icon: none }) } }) }, onReachBottom() { this.fetchArticles() } })这里有两个经验。第一个是loading标志位必须放在data里不能写成一个普通变量否则在页面异步回调用this时容易丢失状态。第二个是concat而不是直接赋值分页数据要累加不是覆盖。如果直接把第二页数据赋给数组滚动效果会瞬间跳回顶部用户体验非常差。3.3 wx.request 请求封装与 baseUrl 管理直接在每个页面里写wx.request不是不行但页面一多改接口地址就要全项目搜索。我抽了一个utils/request.js把 baseUrl 和请求逻辑集中管理const baseUrl https://api.example.com function request(path, data {}, method GET) { return new Promise((resolve, reject) { wx.request({ url: ${baseUrl}${path}, data, method, header: { content-type: application/json }, success: (res) resolve(res.data), fail: (err) reject(err) }) }) } module.exports { request, baseUrl }这样页面里就可以用async/await写得更清楚。比如详情页拉文章const { request } require(../../utils/request) Page({ async onLoad(options) { const res await request(/api/articles/${options.id}) if (res.code 0) { this.setData({ article: res.data }) } } })需要注意开发环境中baseUrl可能填http://127.0.0.1:5000但微信开发者工具里手机预览时不能直接用电脑的 localhost必须改成局域网 IP 或在真机调试里关掉域名校验。上线后又要换成正式域名所以 better 的做法是把baseUrl放在app.js的globalData里一处改动全端生效。3.4 答题页里的单选框、提交与解析展示答题页用到的是小程序原生radio-group。题目数据从 GET /api/quiz 拉取后渲染成 radio用户选择答案点击提交再拿评分结果。核心结构如下view classquiz-item wx:for{{questions}} wx:keyid view classquestion{{item.question}}/view radio-group bindchangehandleChange>python3 -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt我建议锁版本不改来改去Flask3.0.3 Flask-SQLAlchemy3.1.1 Flask-Cors4.0.0 gunicorn21.2.04.2 工厂函数 Blueprint 管理 Flask 应用很多 Flask 入门教程把路由直接写在app.py这个项目里我拆出来了。使用工厂函数的原因是可以重复创建 app测试和部署都方便。app.py核心如下from flask import Flask from flask_cors import CORS from models import db from api.articles import bp as articles_bp from api.quiz import bp as quiz_bp from api.search import bp as search_bp def create_app(): app Flask(__name__) app.config.from_object(config.Config) db.init_app(app) CORS(app, resources{r/api/*: {origins: *}}) app.register_blueprint(articles_bp) app.register_blueprint(quiz_bp) app.register_blueprint(search_bp) return appBlueprint 里路由用列表包一下方便小程序端调用。比如文章模块from flask import Blueprint, request, jsonify from models import db, Article bp Blueprint(articles, __name__, url_prefix/api/articles) bp.route(, methods[GET]) def article_list(): category request.args.get(category, typeint) page request.args.get(page, 1, typeint) page_size request.args.get(pageSize, 10, typeint) query Article.query if category: query query.filter(Article.category_id category) pagination query.order_by(Article.created_at.desc()).paginate( pagepage, per_pagepage_size, error_outFalse ) return jsonify({ code: 0, data: { list: [a.to_dict() for a in pagination.items], total: pagination.total, hasMore: pagination.has_next, } })这里有一个小细节Blueprint 的url_prefix/api/articles搭配空路由规则可以匹配/api/articles。如果写bp.route(/)默认strict_slashes会把缺少斜杠的请求做 308 重定向小程序端可能会出现一次额外请求调试时容易误判为接口有问题。4.3 CORS、调试开关与小程序的合法域名限制关于跨域有一个反直觉的知识微信小程序环境不是浏览器XMLHttpRequest的 CORS 限制对它影响不大。但项目里我还是加了 Flask-CORS因为开发时我会用浏览器直接打后端接口看 JSON浏览器没有 CORS 就会拦截。配置比较简单CORS(app, resources{r/api/*: {origins: *}})生产环境建议把origins收紧成自己小程序的域名或留空。需要留意另一个更现实的限制微信小程序wx.request要求正式域名必须是 HTTPS而且在微信公众平台配置 request 合法域名后才能正常访问。本地开发阶段可以在开发者工具的“详情-本地设置”勾选“不校验合法域名、TLS 版本以及 HTTPS 证书”真机预览时也要打开调试模式但这些都是临时方案上线前必须换正式 HTTPS 域名。4.4 Linux 服务器部署Gunicorn Nginx后端部署我用的方案是 Gunicorn 跑 FlaskNginx 做反向代理和 HTTPS 终止。先在服务器项目目录安装依赖然后启动pip install gunicorn gunicorn -w 4 -b 127.0.0.1:5000 app:create_app()-w 4表示四个 worker 进程。对小项目来说四个足够太多反而占内存。Nginx 配置核心如下server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/example.crt; ssl_certificate_key /etc/nginx/ssl/example.key; location / { 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_pass地址要写127.0.0.1:5000不要写 localhost某些服务器上 localhost 可能解析到 IPv6 导致连接失败。第二Nginx 需要把proxy_set_header Host带上否则 Flask 生成的某些重定向 URL 会拿机器内网 IP。第三服务器安全组要放行 80/443 端口而 5000 端口不要直接暴露在公网让 Nginx 统一入口。5. 联调与上线中的常见问题排查这一部分是我真正想分享的干货。前后端联调时越简单的错越容易让人耗一下午。我把实际踩过的坑整理成了一张问题速查表。现象可能原因解决办法wx.request 报 “不在以下 request 合法域名列表中”域名未配置或开发环境校验未关闭开发阶段勾选“不校验合法域名”上线前配置 HTTPS 域名请求一直 fail但浏览器能打开接口小程序端 url 写错、端口没放开、用了 localhost确认 baseUrl真机用局域网 IP 测试接口返回 404Flask 路由带不带斜杠不一致Blueprint 空路由或用strict_slashesFalse返回中文变成 \uXXXXFlask JSON 默认 ensure_ascii设置app.json.ensure_ascii False列表加载更多时数据重复page 未在成功后自增或 loading 未置 true按 3.2 的脚本加 loading 和 page 自增自定义导航内容被状态栏挡住没有处理顶部安全区读取 statusBarHeight 设置 padding-top5.1 开发阶段如何快速定位接口问题第一次联调时我建议先做最小联通测试。小程序里什么都不写就放一个按钮点击时请求后端/api/categories把res打印到控制台。这个测试排除了页面渲染、样式、组件交互的干扰只验证“小程序能不能拿到后端数据”。如果能拿到问题大概率在业务逻辑如果打不通只需要检查三层URL 是否拼对、端口是否可访问、Flask 是否真的启动了。排查真机请求时我常用抓包工具看请求和响应。手机上打开小程序的调试模式电脑上用 Charles 或 Whistle 把 HTTPS 流量接进来能非常直观地看到 wx.request 发出去的实际请求头和响应体。抓包不是用来跳过问题而是确认问题发生在哪一层是 DNS 解析失败、TCP 连接失败、HTTP 返回 5xx还是 JSON 解析异常。问题定位到层之后解法基本不用搜就能写出来。5.2 为什么小程序请求成功但页面渲染不出数据这个问题我在第一版遇到过。接口返回正常控制台也能打印res.data.data.list但页面列表还是空的。最后发现原因是 setData 里的字段名和 WXML 里的字段名不一致WXML 写的是item.title接口返回字段叫name。页面拿到数据后item.title永远是 undefined视图自然不会显示。所以在写小程序页面时明确规定后端返回字段的命名规则。我统一用驼峰articleId、pageSize、hasMore。后端 Python 字典里也写articleId不要写article_id。这样两边不用做字段映射前后端各看一遍就能对得上。如果你想偷懒可以封装一个to_dict()方法在返回前端前把字段名统一好别把数据库字段名直接暴露出去。5.3 调试阶段端口被占用和进程残留Flask 开发服务器跑起来后如果代码改完重启不干净旧进程还占着 5000 端口新进程会报Address already in use。Linux 下我一般这样处理lsof -i:5000 kill -9 进程PID在 Windows 下则是netstat -ano | findstr :5000找到 PID 后taskkill /F /PID。如果接口返回数据一直是旧的很可能是浏览器或小程序端缓存了 JSON。小程序wx.request默认不走浏览器缓存但调试模式下偶尔会有旧包资源过几分钟再试或者清缓存即可。6. 急救科普内容上线前后必须注意的事最后想聊的不是代码而是内容。急救科普不是普通资讯内容错了后果很严重。作为开发者你可以不参与医学内容创作但必须保证内容有来源、有明显免责提示、有反馈修改机制。6.1 内容来源与审核流程项目里的每一篇文章我都要求标注来源比如“参考红十字会急救指南”“参考 2020 心肺复苏指南”并在底部标注更新日期。题目解析也标注来源因为用户会对有出处的答案更有信任感。如果你用 AI 辅助生成科普文案千万不要直接发布尤其是涉及按压深度、按压频率、AED 使用流程、药物剂量这类数据必须人工逐条核对。内容审核我采用双人交叉审核一个人写稿另一个人按原文核对关键操作步骤数是否一致。文章上线前先放在后台“草稿列表”里确认没问题再置为可见。小程序端加一个“报错”入口用户发现内容问题时能提交反馈再通过后台修改记录追踪。6.2 免责声明和紧急求助引导一定要放在首页首页除了入口我留出了一个常驻提示条本工具仅用于急救知识科普不能替代专业医疗建议遇到紧急情况请立即拨打急救电话并尽量获取现场专业人员帮助。这个提示不是“免责甩锅”而是科普工具的底线。小程序不能在用户真的发生意外时才告诉他“这只是学习资料”。另外在详情页底部固定展示“紧急求助”按钮点击后直接调起电话拨打急救号码。虽然微信小程序配合wx.makePhoneCall很简单但很多科普类小程序都忽略了这一环。它不影响核心功能却会在关键时刻给用户多一个动作选项。6.3 后续可以扩展的方向这个项目做完后我留了几个扩展点没动。一个是语音播报具体做法是后端返回文章纯文本小程序端用微信同声传译插件转语音方便开车或做家务时收听。另一个是答题排行榜需要引入简单的 openid 登录让用户能看到自己的历史成绩。还有一个是内容定时更新把急救知识文章按失效日期提醒定期复查心肺复苏指南是否有新版。当前版本最让大家意外喜欢的其实是“图文卡片”。海姆立克急救法拆成一张张带图片的步骤卡首页放一键进入后面培训时照着卡片一步一步讲比看长文章轻松很多。这也让我意识到急救科普小程序的技术难度真不高真正决定项目价值的是内容是否清楚、步骤是否可执行、紧急场景下是否真正帮得上忙。
返回列表