☰
Flask+uni-app构建微信小程序精品课程在线学习互动系统
2026/10/14 11:30:46 网站建设 项目流程

一直做Web后台开发,接手“Python基于Flask+uni-app微信小程序的计算机精品课程在线学习互动系统演示 聊天”这个项目前,我心里其实很没底。原因很简单:这套系统不只是一个放课程视频的网站,而是一套集课程资源、学习进度、师生答疑和实时聊天于一体的微信小程序解决方案,而且还专门提出了“演示”要求,方便在汇报、答辩和试用阶段快速跑通完整闭环。后来逐步把技术栈锁定在Flask、uni-app和微信小程序三者上,思路才开始变得顺了。

这个项目的价值在于:它把传统网课系统里最容易忽略的“学习过程”和“互动反馈”补上了。学生端打开小程序,能看到精品课程列表,点进去学习章节视频,随手写笔记、做练习,还能进入聊天室提问;教师端在后台维护课程、回答学生提问、发布公告;管理员则能一键重置演示数据。整体下来,学习不再是单向的播放行为,而是有记录、有反馈、有交流的闭环,这也是为什么它能在课程展示和实际教学中都能打动人。

1. 项目概述与目标设定

1.1 为什么做“在线学习互动”而不是简单放视频

先说最核心的问题:普通“网课系统”到底缺什么?

我拆过好几套类似的课程平台,发现大多数只做到“课程列表+视频播放+习题答案”,学生看没看、看到哪里、遇到问题怎么办,这些关键信息一概没有。所谓“精品课程”,如果学生的学习轨迹无法被感知,教学效果就很难量化。

这个项目定的目标是三个:

  • 精品课程全流程在线学习:课程详情、章节视频、课件资料、课后练习都能在微信小程序里打开;
  • 学习过程可跟踪:学生学完一章,后端要记录“是否学完、学习时长、最后一次播放位置”,方便教师查看学习统计;
  • 强互动设计:课程下挂一个学习聊天室,学生可以发言提问,教师能回复和管理,形成师生与生生之间的即时交流。

“演示”这个词也提醒我一件重要的事:系统不能光能跑,还得展示得漂亮。开发过程中我特意做了一键初始化演示数据的功能,把三套风格不同的精品课程、几十条典型聊天记录、多个角色账号提前埋好,任何时候重置一下,系统立刻回到“有人用过且很热闹”的状态,展示效果比空荡荡的数据好得多。这也成了我后来做课程类系统的一个习惯:演示数据有时候比真实数据更能说明问题。

1.2 技术组合的合理性判断

选型阶段我其实不是没有纠结过。后端方向有Java Spring Boot、Node.js、Python Flask三个备选,最后定了Flask。理由很实际:项目定位是轻量级学习互动系统,不需要重型微服务架构;Flask的ORM(SQLAlchemy)写起来简洁清楚,教学演示时想要解释接口逻辑也容易;而且团队人员对Python更熟悉,后期改成“课程资源+统计报表”之类的功能迭代速度更快。Flask的Blueprint模块刚好能把课程、用户、聊天这三个业务域拆开管理,互不干扰。

前端选了uni-app,最核心的原因是“一套代码、多端编译”。这次需求明确指到微信小程序,但难保后续不想出一个App版本或者H5版本,用uni-app写一次,以后可以直接编译到支付宝小程序、抖音小程序或者App,改动成本极低。更重要的是,uni-app对Vue语法支持很友好,页面结构、组件化开发思路和Web前端非常接近,团队上手快。

微信小程序作为载体则解决了“触达”的问题。学生对下载独立App普遍有心理门槛,但在微信里搜索打开一个小程序几乎没有成本。再加上小程序的分享能力,课程页面能直接转给同学群,学习场景的传播路径会顺畅很多。这套组合的特点是:后端轻、前端灵活、入口轻便,适合“课程学习+互动聊天”这种偏内容业务的中小型系统。

2. 业务架构与功能拆分

2.1 三条核心业务流程

整个系统提炼出来就三条主流程:学习流、互动流、演示流。

学习流的起点是课程列表页。学生进入小程序首页,后端返回已上架的精品课程,包含封面、教师、简介、章节数、学习人数。学生点击课程进入详情页,课程下面排布章节列表,每个章节关联视频地址和讲义URL。播放器是微信小程序的video组件,我设置了一个“章节学完后自动点亮”的小功能,同时监听播放事件,当用户播放到视频末端或手动点击“完成学习”按钮,就向后端上报进度。

互动流的核心是聊天室。每门精品课程挂一个独立聊天室,学生发送消息后,后端先把消息写入数据库,再通过实时通道推送给同课程的其他在教室成员。教师端在后台聊天管理页面能看到所有消息,并能针对某个问题回复或置顶。这样教学互动就不散落在微信群聊里,而是沉淀在课程体系内,数据有迹可循。

演示流是这套系统的一个亮点。运行一个初始化命令后,系统自动备份现有数据,然后在空库中填充预置课程和聊天记录。我特意保证了初始化脚本可以重复执行,不会因为演示中途操作过产生脏数据。这样无论是答辩还是给客户演示,我都能在一分钟内把系统恢复到“刚装修好的样板间”状态。

2.2 角色权限设计与小程序的天然约束

系统里一共三类角色:管理员、教师、学生。

角色主要权限小程序端表现
管理员用户管理、课程管理、数据重置、全局公告能看到管理入口,跳转后台页面
教师课程内容维护、答案回复、聊天消息管理聊天室多一个“教师回答”按钮
学生学习课程、提交练习、发送聊天消息常规学习与互动操作

这里碰到一个实际约束:微信小程序没有传统意义上的“后台管理员页面”,它本质上是一个面向C端用户的客户端。所以我把管理功能拆成两条线:教师和课程管理这类“重操作”放在Web管理端做,而小程序端只做“查看数据、即时回复”这类轻量操作。服务器通过登录时下发的角色码(role)控制接口权限,接口层统一校验,前端菜单再根据角色动态显隐。

再补充一个容易踩的坑:小程序端不能完全信任前端隐藏菜单,因为请求是可以被网友抓包或调试工具伪造的。必须在Flask后端每个需要权限的接口上加装饰器判断当前用户角色,否则“学生身份拿到教师接口”这种事迟早会出问题。

3. 后端开发实录:Flask如何支撑课程与聊天

3.1 表结构与模型定义

项目用的是Flask-SQLAlchemy,数据库选的MySQL,演示环境中也可以切换到SQLite,因为SQLAlchemy这一层已经屏蔽了底层差异。核心表我设计成六张:用户表、课程表、章节表、学习记录表、聊天消息表、公告表。课程和章节是父子关系,学习记录通过用户ID和章节ID联合判断是否完成,聊天消息表按course_id分组。

from datetime import datetime from app import db class Course(db.Model): __tablename__ = 'course' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(120), nullable=False) intro = db.Column(db.Text) cover = db.Column(db.String(255)) teacher = db.Column(db.String(50)) status = db.Column(db.Integer, default=1) # 1上架 0下架 created_at = db.Column(db.DateTime, default=datetime.now) chapters = db.relationship('Chapter', backref='course', lazy='dynamic') class Chapter(db.Model): __tablename__ = 'chapter' id = db.Column(db.Integer, primary_key=True) course_id = db.Column(db.Integer, db.ForeignKey('course.id')) title = db.Column(db.String(120)) video_url = db.Column(db.String(255)) attach_url = db.Column(db.String(255)) order_no = db.Column(db.Integer)

聊天消息表需要注意几个细节:除了常规的用户ID、消息内容、发送时间,还要有一个type字段区分系统提示、普通消息和教师回复;为了让未读功能好用,我加了room_id而不是所有消息都堆在同一个大池子里。

class ChatMessage(db.Model): __tablename__ = 'chat_message' id = db.Column(db.Integer, primary_key=True) course_id = db.Column(db.Integer, index=True) user_id = db.Column(db.Integer, index=True) nickname = db.Column(db.String(50)) msg_type = db.Column(db.Integer, default=1) # 1文本 2系统 3教师回复 content = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.now, index=True)

这里我给sender_id和course_id都建了索引。聊天记录一旦跑起来增长速度很快,加上索引以后,拉取历史记录和按课程分组的性能提升非常明显,这在后期真实数据量上来后特别有用。

3.2 接口设计与鉴权流程

接口设计整体走RESTful风格,全部挂在/api前缀下。下面是我整理的主要接口清单:

方法路径说明
POST/api/auth/login登录,返回token与角色
GET/api/course/list分页获取课程列表
GET/api/course/detail?id=课程详情与章节列表
POST/api/course/progress上报章节学习进度
GET/api/chat/history?course_id=&page=分页拉取聊天历史
POST/api/chat/send发送聊天消息
DELETE/api/chat/message?id=删除违规消息(管理员)

登录鉴权我选的是JWT方案,没有用Flask默认的Session。小程序端每次请求在头部带Authorization: Bearer <token>,后端通过装饰器解析token得到当前用户。之所以不用Session,是因为微信小程序与Flask后端之间的域名隔离、请求模型更贴近“无状态”逻辑,JWT天然适合这种前后端分离的架构。

from functools import wraps from flask import request, jsonify import jwt def login_required(f): @wraps(f) def wrapper(*args, **kwargs): auth = request.headers.get('Authorization') if not auth or not auth.startswith('Bearer '): return jsonify(code=401, msg='未登录'), 401 token = auth.split(' ')[1] try: payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) request.user_id = payload['uid'] request.user_role = payload['role'] except jwt.ExpiredSignatureError: return jsonify(code=401, msg='登录已过期'), 401 except jwt.InvalidTokenError: return jsonify(code=401, msg='无效令牌'), 401 return f(*args, **kwargs) return wrapper

JWT有效期我设成了7天,并做成“剩余3天时自动续签”的机制。小程序端每次接入时检查token过期时间,快要过期就静默调用刷新接口,用户体验上根本感知不到登录态的变化。

3.3 聊天的数据模型与消息推送

聊天是这个项目的核心互动模块,值得多花点篇幅。

数据模型如上,重点是消息的“推”与“拉”如何配合。项目初期我考虑过用Flask-SocketIO实现WebSocket双向推送,但后来发现微信小程序和Socket.IO之间握手有些兼容细节需要调,而且部署时得额外保证WebSocket端口可用,于是我用了一个更稳妥的混合方案:

  • 在线轻量提醒:页面在聊天室时,前端每5秒调用一次增量拉取接口,只拿“当前最大消息ID之后的新消息”,这个接口很轻,对服务器压力几乎可以忽略;
  • 离线与历史记录:进入聊天室先调用分页接口拉取最近50条,再向上翻页加载更早的记录,避免一次性传大量数据导致小程序白屏。

增量拉取的实现其实很巧妙。前段维护一个lastMsgId,请求时带上它,后端查询WHERE course_id = ? AND id > ? ORDER BY id ASC LIMIT 50,这样基本能做到准实时,而代码逻辑比维护WebSocket简单得多。后来如果不满足于5秒延迟,可以在Flask后台单独开一个WebSocket服务,和REST接口共享同一个聊天消息表,前段判断环境自动切换协议,这样既保留了轻量模式,也为高实时需求留了升级路径。

@app.route('/api/chat/messages', methods=['GET']) @login_required def chat_messages(): course_id = request.args.get('course_id', type=int) last_id = request.args.get('last_id', 0, type=int) page = request.args.get('page', 1, type=int) if course_id is None: return jsonify(code=400, msg='缺少课程参数'), 400 query = ChatMessage.query.filter_by(course_id=course_id) if last_id: messages = query.filter(ChatMessage.id > last_id).order_by( ChatMessage.id.asc()).limit(50).all() else: messages = query.order_by(ChatMessage.id.desc()).paginate( page=page, per_page=20, error_out=False).items data = [{ 'id': m.id, 'nickname': m.nickname, 'msg_type': m.msg_type, 'content': m.content, 'created_at': m.created_at.strftime('%Y-%m-%d %H:%M:%S'), } for m in messages] return jsonify(code=0, data=data)

4. 前端开发实录:uni-app如何编译成微信小程序

4.1 页面结构与导航设计

uni-app的项目结构很直观,页面都放在pages目录下,pages.json负责路由和导航栏配置。我规划了4个主要页面模块:首页课程列表、课程详情页、聊天室页面、个人中心页面。再加上用户登录页和教师管理后台的Web端入口,整体导航层级不超过三层,保证学生在小程序里能快速找到入口。

{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "精品课程" } }, { "path": "pages/course/detail", "style": { "navigationBarTitleText": "课程详情" } }, { "path": "pages/chat/room", "style": { "navigationBarTitleText": "学习交流" } }, { "path": "pages/mine/mine", "style": { "navigationBarTitleText": "个人中心" } } ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "课程" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }

tabBar只放课程和我的两个入口,聊天室不需要独立tab,因为聊天默认是挂在课程详情下面的,这个设计是为了保证用户进入聊天室的路径足够短,不用在多个一级页面之间来回跳。

4.2 课程学习模块的实现

课程学习模块要解决两件事:内容展示和学习状态记录。

课程列表页使用uni.request请求后端接口,拿到数据后渲染卡片列表。因为小程序有这个加载性能要求,我这个列表加了分页加载和下拉刷新,每次请求20条,触底时自动请求下一页,同时通过onReachBottom生命周期函数触发加载,避免用户一直无限滚动导致页面卡顿。

// request.js 封装统一请求 const BASE_URL = 'https://api.mysite.dev/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: { 'Authorization': 'Bearer ' + uni.getStorageSync('token'), 'Content-Type': 'application/json' }, success: (res) => { if (res.statusCode === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) return } if (res.data.code === 0) resolve(res.data) else reject(res.data) }, fail: (err) => reject(err) }) }) }

我做了个request.js统一封装,所有接口请求都走这个文件。好处是到了后面要加统一loading、统一错误提示、统一token处理时,只需要改一个文件就够了,不用到每个页面里零散修正。

学习状态这块,我在课程详情页监听video组件的bindtimeupdate事件,每15秒向后端上报一次播放位置;当播放到视频末尾时标记“本章完成”。这样做的好处是,就算学生中途退出,课程列表里的进度条依然能精确显示他看到哪里,下次打开可以直接续播。

4.3 聊天页面的组件化思路

聊天室页面的实现是前端工作的重中之重。页面结构我拆成三块:顶部是课程介绍栏,中间是消息列表scroll-view,底部是输入框和发送按钮。所有聊天消息通过一个独立的聊天组件管理,后续如果要在首页增加“最近聊天”提醒,只需复用这个组件的核心逻辑。

聊天消息采用scroll-into-view定位到底部,新消息来时判断“用户是否处于底部”,如果已经滚上去查看历史消息,就不强制拉回底部,避免用户阅读被强行打断。因为我用的增量轮询方案,聊天室显示时每5秒触发一次setInterval,页面离开聊天室时在onUnload里清除定时器,避免后台空转浪费流量。

输入框我加了一个小优化:监听键盘弹起时自动把消息列表滚动到底部,同时调整输入框位置。这个小细节看起来不起眼,实操中如果漏掉,学生打字的时候最新的消息会被键盘挡得严严实实,体验非常差。

5. 演示模式的设计与操作方法

5.1 一键初始化演示数据

为了这个“演示”功能,我在后端单独写了一个Flask CLI命令,执行flask init-demo就能立刻把真实数据备份并替换为演示数据。这个命令做了三件事:把现有数据完整导出备份;清空业务表;写入预设的课程、章节、聊天记录、公告。

flask init-demo

在演示数据的设计上,我特意做到“有热度”:三门课程中有一门学习人数500+,聊天室里预先填充30条典型消息,包括学生提问、教师回答、普通闲聊、系统欢迎语等。这样外人一打开聊天室,第一眼看到的是活跃讨论氛围,而不是做演示时尴尬地等待第一个人发消息。

为了防止误操作覆盖真实数据,init-demo命令执行前会强制要求输入confirm,并且自动在backup/目录下生成带时间戳的备份文件。万一演示过程中把数据弄乱了,还可以通过flask restore-demo把之前备份的真实数据恢复回来。

5.2 演示账号与权限控制

演示模式下系统内置三个固定账号:学生账号student、教师账号teacher、访客账号guest。密码统一为123456。访客账号只能看课程列表和前两章内容,无法发送聊天消息;学生账号可以学习全部课程、发消息、做练习;教师账号在学生权限基础上多出回复消息、管理课程评论的权限。

我在登录接口做完特殊判断:如果当前是演示模式,并且使用内置账号登录,系统会下发一个带demo: true标志的JWT,前端识别到这个标志后会在页面顶部显示“演示模式”标签,提醒操作者当前看到的是演示数据。这个细节在正式场合很加分,因为访问者会明确知道自己看的是演示环境,不会误认为系统只有这点数据。

5.3 完整演示流程

以一次答辩演示为例,我最常用的操作流程是:

  1. 先执行flask init-demo重置数据;
  2. 微信开发者工具打开小程序,使用学生账号登录;
  3. 首页展示精品课程列表,介绍课程封面、学习人数等数据;
  4. 进入课程详情页,打开章节视频播放,点击“完成本章”,展示学习进度从0变成100%;
  5. 去聊天室发一条消息“老师,请问这道题怎么解?”,演示消息实时出现在聊天列表;
  6. 退出学生账号,切换到教师账号,在聊天室里回复该消息,展示教师回复样式和置顶效果;
  7. 回到个人中心,展示学习统计:完成章节数、平均停留时长、最近学习时间;
  8. 最后切到访客账号,演示受限权限:不能发言、看不到部分课程,说明系统权限控制确实生效。

6. 常见问题与排查技巧实录

6.1 小程序网络白名单与本地调试

微信小程序有一个很折磨人的限制:正式环境下,所有wx.request的域名必须是备案过的,并且必须配置在微信公众平台的“服务器域名”白名单里。开发阶段有个偷懒方案:在微信开发者工具的“详情-本地设置”里勾选“不校验合法域名”,这样就能直接请求本地计算机网络地址上的Flask服务。真机预览或者发给别人体验时,必须把后端部署到带HTTPS证书的正式服务器,并把api.mysite.dev这类域名加入到白名单,否则所有请求都会被拦。

这里最容易导致微信小程序出现“请求失败”。我调试时曾因为只在开发者工具里不校验域名,然后直接用手机扫码预览,结果页面转圈半天,控制台提示request:fail url not in domain list。后来我养成一个习惯:任何上传测试版本前,先用真机预览跑一遍核心链路,确认域名、证书、白名单都配置无误再发布。

6.2 跨域与CORS配置

用Flask后端和小程序通信时,跨域问题主要出现在Web管理端的前后端联调阶段。小程序和Flask之间因为平台机制不会走浏览器同源策略,但Web管理端是浏览器环境,如果前端跑在localhost:8080,后端跑在localhost:5000,就会触发CORS。我采用flask-cors解决。

from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": [ "http://localhost:8080", "https://admin.mysite.dev" ]}})

生产环境不建议直接用origins=*,我见过不少团队图省事全放开,结果接口被其他网站拿来乱请求,白白消耗服务器资源。正确做法是在配置里把可信任的管理后台域名和前端页面域名都列出来,一点不麻烦。

6.3 登录态过期与重复跳转

JWT过期后会返回401,小程序端如果每处接口都自己处理,会出现一个非常难看的场景:用户同一个操作触发了多个并发请求,这些请求同时返回401,页面被连续跳转登录页好几次。我在request.js里加了标记位处理:第一个401出现时记录isRedirecting,后续401直接忽略,等跳转完成后统一清除状态。

还有一个实操经验:用户停留在聊天室页面时token过期,他发送消息会失败。如果不做处理,消息输入框就静默失败,用户以为发出去了,其实后台根本没收到。我在发送接口的错误回调里做了提示,并引导用户先跳转登录再返回,消息内容先保存在本地storage,登录成功后自动带出重新发送,这个细节常被忽略,但对真实用户体验很重要。

6.4 聊天消息丢失、乱序与WebSocket断线

聊天模块用增量轮询时,我踩过一个排序坑。早期的查询语句用了ORDER BY created_at DESC,后端返回的lastMsgId递增,但几秒内多条消息的created_at是同一秒,导致增量拉取时新消息有时排在老消息前面,滚动次序看起来是错乱的。后来统一改成按自增主键id排序,因为同一秒内的多条消息,id大概率是严格递增的,不会再出现顺序颠倒。

如果项目后续升级到WebSocket方案,断线重连是绕不开的话题。移动端网络不稳定,WebSocket连接经常在切后台、锁屏、进出电梯时断开。一定要做心跳检测:前端每30秒发送一个ping消息,如果60秒内没有收到pong,主动关闭连接并重连;后端也要设置空闲超时时间,主动回收死连接,否则服务器连接数会被僵尸连接占满。

6.5 视频播放兼容性

小程序video组件对视频编码支持有限,我在自己测试过程中发现,编码为H.264的MP4文件最稳,WebM和AVI在一些安卓机型上无法播放。做演示用的时候,如果课程视频是从其他地方转载的,最好用格式工厂或ffmpeg统一转码成H.264 + AAC音频的MP4,然后把文件放到支持HTTPS访问的对象存储中,视频链接才能在小程序里正常播放。

另外视频播放地址不能直接使用未备案的域名,也不能使用带http://明文资源,小程序对资源域名的校验和接口域名是一样严格的。本地开发时,我经常把视频文件放到Flask的static/videos目录下,通过后端接口返回相对路径,然后开发者工具开不校验域名,顺手解决开发环境的视频加载问题。生产环境就要老老实实把视频域名加白名单。

7. 实测心得与后续优化空间

7.1 比较实用的设计习惯

这个项目跑了两个月,有几个经验我觉得放到类似系统里一样适用。

第一个重要经验是基于软删除和备份实现的可恢复性。聊天消息和学习记录这类数据很敏感,演示或误操作时一旦被物理删除,找回成本极高。后来我在所有核心表增加了is_deleted字段,删除操作只打标记,列表查询默认过滤已删除记录,管理后台还能看到被删除的消息内容和删除人,审计也更方便。

第二个经验是所有的统计类数据要异步聚合。学习进度、课程热门排行这类数据,不一定要每次请求都实时查数据库。我是简单地在课程表上冗余了一个learn_count字段,每有用户完成学习就+1,虽然牺牲了一点点实时一致性,但是课程列表页不用每次计算COUNT,加载速度明显提升。对于并发量再高的场景,可以考虑定时任务或消息队列异步更新。

第三个经验是聊天内容必须有过滤机制。微信小程序的内容安全审核对“用户生成内容”盯得比较紧,我在发送消息后调用了云端内容检测接口,检测不通过的文本直接拒绝入库。这个处理防止了不少演示时因为输入不当内容导致的尴尬场面,合规角度也安全得多。

7.2 后续还可以往哪些方向扩展

课程学习互动系统的价值远不止当前这些功能。我个人觉得下面几个方向值得继续深挖:

  • 接入AI助教:聊天室里学生提问后,可以先由大模型自动生成一个初步回答候选项,教师一键采纳或修改后发布,既保留人的判断,又减轻人工回复压力;
  • 学习数据大屏:把学生完成率、章节停留时长、聊天活跃度汇总成可视化看板,教师能一眼看出哪些章节难度异常,哪些学生需要单独关注;
  • 多租户扩展:现在项目只服务一门精品课程,如果做成平台,每所学校或院系都可以注册自己的空间,数据隔离的上层设计要提前考虑。

最后说一点我个人的真实感受:开发这类学习系统,功能做完永远只是第一步,更难的是让学生愿意用、教师愿意维护。小程序端一定要把交互做得足够顺手,把聊天入口做得足够浅,学生打开课程就顺手看到交流区;后台管理端要大幅度降低操作成本,课程上传、章节调整最好都能在几个点击内完成。只有两端的使用成本都降下来,在线学习互动才能真正跑起来,而不是在部署完演示阶段就变成无人问津的空壳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询