ARTICLE DETAIL

资讯详情

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

Flask+微信小程序实战:课程题库学习小程序开发全攻略

Flask+微信小程序实战:课程题库学习小程序开发全攻略 我最近刚把一个典型的“讲师学员”学习类小程序完整跑通了。后端是Python Flask写的数据接口前端是微信小程序核心功能就两块一块是学习视频课程一块是知识题库。讲师能上传视频、维护题目和看学员数据学员可以看课、刷题、练错题整体就是一个面向培训、教育和内部考核场景的轻量SaaS工具。这个项目适合谁参考如果你正准备做教育类小程序或者你是后端工程师想用Flask快速给小程序提供API又或者你手上正好有一个“课程题库”的业务需求不知道怎么拆模块那这篇整理值得看完。我会把方案选型、数据库设计、核心接口、部署踩坑全部按实操顺序写出来代码都是可以直接抄走改用的。1. 项目整体设计与方案选型1.1 为什么后端选Flask而不是Django或Node先说结论这个项目用Flask是非常合适的原因有三个。第一业务逻辑不算重主要就是课程管理、题库管理、答题判分、学习记录这几条链路不需要Django自带的Admin后台、ORM迁移体系、用户认证体系全套上来杀鸡不用牛刀。第二Python生态里做算法、做数据处理、做爬虫的同事很多Flask的门槛比Spring Boot低一个后端同学半天就能把骨架搭起来。第三Flask轻量意味着可控路由、蓝图、请求钩子都是显式的出问题好排查。我见过很多团队一上来就上Django REST Framework结果model、serializer、viewset层层封装等到小程序端需要一些特殊联调接口时反而被框架的写法绑架。Flask没这个毛病。当然如果项目后来要上复杂后台管理、自动化权限、工作流引擎那再迁Django也不迟前期用Flask快速验证业务才是正确的节奏。1.2 为什么学员端用小程序而不是App或H5这个项目选微信小程序核心理由是分发成本极低。学员扫码就能进不用装App不用注册账号微信授权一键登录对企业培训这种场景非常友好。讲师端我建议做成小程序内的一个角色切换页面不要单独再开发一个App否则维护成本翻倍。管理后台可以用Web因为讲师上传视频、批量导入题库这些操作需要大屏和文件上传web端配合Flask后台更顺手。这里有个架构细节前端分两端学员小程序和讲师管理页但后端API只维护一套。小程序端通过wx.request调用Flask接口管理页通过axios调用同一套接口只是登录接口区分角色再在服务端做权限校验。这样模块边界清晰也方便后期把管理页换成小程序里的讲师Tab。1.3 整体功能模块怎么拆我当时把项目拆成了六个模块用户认证、课程管理、视频学习、题库练习、答题记录、统计面板。每个模块对应一组Flask蓝图避免所有路由堆在app.py里。小程序端则对应TabBar首页课程列表、学习中心、我的。讲师在个人中心里进入讲师模式能看到“我上传的课程”“我出的题”“学员答题统计”。这种结构的优势是每一块都可以独立开发、独立自测。比如先把用户认证跑通再接课程列表再接视频播放再接题库练习最后做统计。联调时如果某一环出错能快速定位是前端传参问题、后端逻辑问题还是数据库数据问题。2. 核心细节与数据模型设计2.1 三张核心表用户、课程、题目后端数据模型是整个项目的地基设计得好后面开发效率翻倍。我最终落地的表结构大致如下用户表user核心字段id、openid、nickname、avatar、role学员/讲师/管理员、created_at。openid是微信小程序用户的唯一标识role字段控制权限。课程表course核心字段id、title、cover_url、intro、price、category、lecturer_id、status、created_at。课程表存的是课程元信息不存视频本体。题目表question核心字段id、course_id可空表示通用题还是绑定课程、type单选/多选/判断、content、options_json、answer、analysis、difficulty、created_by、created_at。我用options_json存选项格式是JSON数组比如[A, B, C, D]对应的文本answer字段单独存正确答案。为什么不拆成选项表因为选择题的选项基本不会单独被查询和修改存JSON字段实现起来最简单查询也不用JOIN。2.2 视频不直接存MySQL只存URL和时长如果直接把MP4文件传送到Flask服务器再让小程序播放那带宽很快被打满而且Flask同步服务器天然不适合处理大文件流。我采用的是视频上传到OSS对象存储数据库里只存object_key、cover_url、video_url、duration三列。前端拿到video_url后直接用video组件播放。这里有个注意点小程序video组件的src支持HTTPS的mp4地址但如果你用OSS的私有BucketURL会带签名参数签名过期后播放会失败。我的做法是视频上传完成后生成一个长期有效的公开读URL或使用CDN加速域名避免学员学习中途视频加载失败。2.3 题库字段设计与防刷机制题库除了基础字段还要考虑答题场景。我的方案是增加answer_index字段用数字存正确选项下标分析字段analysis存答案解析difficulty字段分1到3级方便按难度抽题。提交答案的接口不能返回答案明文只能在判分后返回对错和解析否则学员用抓包工具看一眼接口就全抄了。防刷方面做了两个限制同一用户对同一道题提交答案后服务端记录答题日志单用户每分钟答题次数超过阈值就暂时限流。这个限流逻辑用Flask的before_request钩子加内存字典就能实现不需要上Redis单机演示完全够用。2.4 讲师权限与普通学员权限的边界用户角色我用role字段区分接口层用一个自定义装饰器来判断。比如lecturer_required装饰器内部逻辑是解析token取出user_id查库拿到role如果是2讲师或3管理员就放行否则返回403。这个装饰器统一放在utils/auth.py里所有涉及课程创建、题目编辑的接口都挂上有效防止越权。权限边界一定要尽早做别等接口写完了再补。因为小程序端虽然也能用按钮显隐控制操作入口但懂技术的人直接改请求参数就能调接口服务端校验才是真正的安全边界。3. 实操过程与核心环节实现3.1 环境准备与Flask工程结构开发环境建议用Python 3.8以上版本。先创建虚拟环境python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install flask flask-sqlalchemy flask-cors flask-jwt-extended pymysql工程结构上不要把路由全写在app.py我用蓝图的目录划分如下project/ ├── app.py # 应用入口注册所有蓝图 ├── config.py # 数据库、密钥等配置 ├── models/ │ ├── __init__.py │ ├── user.py │ ├── course.py │ └── question.py ├── blueprints/ │ ├── auth.py │ ├── course.py │ └── question.py ├── utils/ │ ├── auth.py # JWT校验装饰器 │ └── response.py # 统一返回格式 └── requirements.txtapp.py里最关键的启动代码就十几行from flask import Flask from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() def create_app(): app Flask(__name__) app.config.from_object(config.Config) CORS(app) db.init_app(app) from blueprints.auth import auth_bp from blueprints.course import course_bp from blueprints.question import question_bp app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(course_bp, url_prefix/api/course) app.register_blueprint(question_bp, url_prefix/api/question) return app用工厂函数创建应用好处是方便写单元测试也方便多个环境切换配置。数据库连接建议用MySQL生产环境不要用SQLite虽然小程序轻量用户少但SQLite在高并发写入时容易锁库报错。我一开始图省事用了SQLite结果答题记录一多就出现database is locked后来老老实实换成MySQL。3.2 核心接口一微信登录换取Token小程序端通过wx.login拿到code传给Flask后端后端再调用微信接口换取openid。开发环境可以直接用mock数据但生产环境必须走真实接口。关键代码auth_bp.route(/wxlogin, methods[POST]) def wxlogin(): code request.json.get(code) url https://api.weixin.qq.com/sns/jscode2session params { appid: appid, secret: secret, js_code: code, grant_type: authorization_code } resp requests.get(url, paramsparams).json() openid resp.get(openid) user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid, nickname微信用户, role1) db.session.add(user) db.session.commit() token create_access_token(identitystr(user.id)) return jsonify({code: 0, token: token, role: user.role})这里的create_access_token来自flask_jwt_extended默认生成的token有效期可以配置建议设成7天用户频繁打开小程序时不用每次重新登录。3.3 核心接口二课程列表分页小程序端首页是课程列表一定要做分页不然视频课程一多一次返回几十条数据不仅加载慢小程序渲染也会卡。接口设计如下course_bp.route(/list, methods[GET]) def course_list(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) pagination Course.query.filter_by(status1) \ .order_by(Course.created_at.desc()) \ .paginate(pagepage, per_pageper_page, error_outFalse) items [c.to_dict() for c in pagination.items] return jsonify({ code: 0, data: { items: items, has_more: pagination.has_next } })has_more这个字段很关键小程序端通过它判断是否还能上拉加载更多。如果只返回总数前端还要自己算不如后端直接给状态字段省事。3.4 核心接口三提交答案判分这是题库模块最核心的接口逻辑并不复杂拿到题目ID和用户答案查题目比对写答题记录返回对错和解析。下面是我实际用的判分代码片段question_bp.route(/submit, methods[POST]) jwt_required() def submit_answer(): user_id get_jwt_identity() question_id request.json.get(question_id) user_answer request.json.get(answer) # 例如 A 或 [A,C] q Question.query.get(question_id) if not q: return jsonify({code: 1, msg: 题目不存在}) if q.type multi: correct set(q.answer_index_list()) set(user_answer) else: correct (q.answer user_answer) record AnswerRecord(user_iduser_id, question_idquestion_id, user_answerstr(user_answer), is_correctcorrect) db.session.add(record) db.session.commit() return jsonify({ code: 0, data: { is_correct: correct, correct_answer: q.answer if not correct else None, analysis: q.analysis if not correct else } })细节上注意两点多选判分必须动态判断因为岗位和机构的题型规则不同有的多选题漏选也给一半分这需要扩展一个score字段写答案记录一定要先查库再比对不能拿前端传的正确答案比对否则别人可以直接传正确值。3.5 小程序端对接实操小程序前端我是用原生微信开发者工具写的不用uni-app的理由也很简单项目只针对微信端没必要引入一套跨端框架增加调试复杂度。但如果你是那种同时要上支付宝小程序、抖音小程序的团队那用uni-app打包会省事很多。请求封装是必须的不能一个页面写一个wx.request。我封装了一个request.jsconst request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: https://api.example.com${url}, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { if (res.data.code 0) { resolve(res.data.data) } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: reject }) }) }这里有个容易踩的坑请求超时时间默认60秒视频相关的接口还好但如果是讲师上传视频到客户端不建议通过这个封装走因为大文件上传要用wx.uploadFile而且上传到对象存储时请求耗时可能非常长。首页课程列表上拉加载更多WXML里onReachBottom绑定方法然后分页请求把新数据concat到旧数据后面。很多新手会犯一个错误没有用loading状态锁住请求导致用户快速滚动时连续触发好几次相同页的请求数据重复。我在onReachBottom里加一个this.data.isLoading判断来防止重复请求。3.6 Flask部署到服务器gunicorn nginx开发环境跑通之后部署生产环境有几个必要步骤。首先不能用Flask自带的开发服务器对外服务并发能力太弱。我用gunicorn启动gunicorn -w 4 -b 127.0.0.1:5000 app:app4个worker是比较合适的值太多反而会因为MySQL连接数过高出问题。nginx配置反向代理把80端口转发到5000端口server { listen 80; server_name api.example.com; 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; } }小程序正式环境要求所有请求域名必须HTTPS且ICP备案所以服务器上还需要配置SSL证书。部署完后第一件事就是用curl测试接口通不通不要急着在小程序里调试我每次部署完都是一条条测接口等全部返回正常了再打开小程序测试。4. 常见问题与排查技巧实录4.1 小程序请求失败合法域名和HTTPS证书小程序开发工具默认勾选了“不校验合法域名”所以开发阶段什么都通。但真机预览或正式版时如果请求域名不在小程序后台配置的downloadFile合法域名列表里就会直接报request:fail url not in domain list。这不是代码问题而是配置问题。解决办法是登录小程序公众平台在小程序后台的“开发管理-开发设置-服务器域名”里把API域名加到request合法域名把视频和图片的CDN域名加到downloadFile合法域名。注意域名只支持HTTPS且不能带端口。如果你调试时直接填了http://127.0.0.1:5000正式环境肯定不行。还有一个隐蔽问题SSL证书过期。我遇到过一次接口突然全部超时排查了半天才发现是证书到期curl都会提示证书验证失败。所以建议在服务器上写个定时任务监控证书有效期或者直接使用自动续期的免费证书方案。4.2 抓包调试Charles如何看小程序的请求小程序报错的时候光靠开发者工具的console还不够建议用专业抓包工具看完整的请求体和响应体。我用的是Charles它可以查看HTTPS加密请求的具体内容。操作步骤是电脑和手机连同一个WiFi手机HTTP代理指向电脑IP的8888端口然后在Charles里安装并信任SSL证书。具体流程是Help菜单里选择SSL Proxying找到Install Charles Root Certificate先装到电脑上再用手机浏览器访问chls.pro/ssl下载证书并安装。最后在Proxy-SSL Proxying Settings里添加域名和端口号这样小程序发出的HTTPS请求就能在Charles里看到明文。这里强调一下抓包只用于调试自己开发的小程序、定位自己服务的接口问题不要拿它去抓别人的线上商业应用这是基本边界。我在调试登录接口时正是通过抓包发现小程序前端把wx.login的code传给后端后后端返回的token没有在header里带Authorization前缀导致后续接口全部401查得快很多。4.3 数据库连接超时导致接口偶发报错Flask SQLAlchemy部署到线上后会有一个非常经典的坑MySQL的wait_timeout默认是8小时如果连接空闲超过这个时间MySQL会主动断开连接但SQLAlchemy连接池里还留着这个死连接下次请求时就会报OperationalError: Lost connection to MySQL server during query。解决办法是配置SQLAlchemy的pool_pre_ping确保每次请求前先探测一下连接是否有效SQLALCHEMY_ENGINE_OPTIONS { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True, }另外在部署脚本里也顺手设置MySQL的wait_timeout两边都处理这个坑基本就消失了。这类偶发问题最烦人因为它不是必现的很多人会误以为是服务器配置问题最后其实是连接池生命周期管理的问题。4.4 视频加载不了或播放卡顿视频服务常见的坑有三个。第一域名问题视频URL的域名必须加到小程序后台downloadFile合法域名否则video组件直接加载失败。第二防盗链问题如果你的视频存放在支持防盗链的OSS/CDN上默认会校验Referer小程序的请求Referer是服务商域名如果没配置白名单就会403。第三MIME类型问题有些对象存储默认Content-Type设置不对返回的是application/octet-stream小程序video组件也可能无法播放。处理思路是视频URL统一走CDN加速域名上传时强制设置Content-Type为video/mp4并在CDN侧关闭Referer校验或放行小程序域名。课程封面图也存在同样的问题图片加载空白时先看后台返回的URL能不能在浏览器里直接打开大多数问题都是域名白名单或防盗链导致的代码本身没毛病。4.5 常见问题速查表现象可能原因排查手段真机请求失败开发者工具正常合法域名未配置检查小程序后台request合法域名接口报401token过期或未传Authorization检查请求头token字段和JWT有效期数据库偶发报错Lost connection连接池死连接配置pool_pre_ping和pool_recycle视频播放白屏域名未加downloadFile白名单添加视频域名到合法域名图片加载404防盗链或URL签名过期使用公开读Bucket或CDN签名课程列表重复数据上拉加载缺少loading锁onReachBottom里加防重复请求判断Flask启动报Address already in use端口被占用换端口或kill占用进程5. 后续还能怎么扩展5.1 从单机演示到生产级系统还需要什么现在这套Flask代码从功能角度已经能跑通完整业务流程但距离生产级系统还差几块拼图。第一是对象存储视频文件一定不要放在服务器本地磁盘改成OSS或COS网络传输和存储容量都有保障第二是日志和监控Flask的每个请求都应该记录request_id、耗时、状态码接口出错时能快速定位第三是数据库备份我建议每天用mysqldump增量备份业务数据防止误删或服务器故障导致数据丢失第四是HTTPS证书自动化续期如果手动续签每年总有几个月会担心它过期。如果用户量上来还要考虑把Flask的同步worker换成异步架构或者直接用Flask Celery处理一些耗时操作比如批量导入题库Excel、批量生成学习报告。这些功能现阶段用同步接口能撑住但等并发上来了再重构代价会成倍增加。5.2 根据个人经验给新手的几条实操建议第一不要一开始就把所有功能都写完我建议按这个顺序迭代登录授权 - 课程列表 - 课程详情和视频播放 - 题库练习 - 答题记录 - 讲师上传功能 - 统计面板。每完成一个阶段就同步一次小程序端保证随时有一个可演示的版本这个习惯对项目推进特别重要。第二接口返回格式一定要统一。我的格式是{code: 0, msg: success, data: ...}成功code为0失败code非0。前端封装的request会检查这个code不用每次写一遍错误处理。如果团队里有人把成功code定义为200有人定义为1前端联调时就是灾难现场。第三权限控制必须在后端做。小程序前端隐藏讲师管理入口只是用户体验不是安全边界。所有“讲师才能操作”的功能后端必须校验token对应的用户角色否则你辛苦做的课程管理接口就是摆设。第四题目答案不要明文下发给前端。有些开发者图省事课程详情里直接把题目列表和答案一起返回学员抓个包就能全抄了。标准做法是题库接口只返回题目和选项提交答案后服务端判断并返回结果这样才能保证刷题的真实性。最后再分享一个我自己用得很顺手的技巧为了降低联调成本我在Flask后端加了一个demo模式env设为demo时wxlogin接口不调用微信服务直接根据前端传的mockCode生成固定openid。这样不用每次都用真机扫码登录小程序开发工具里点一下就能进入调试状态速度比走微信登录流程快很多。这个小技巧看起来不起眼但对开发体验的提升非常明显建议有同样需求的朋友直接抄走。
返回列表