ARTICLE DETAIL

资讯详情

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

基于Flask与微信小程序的课程提醒系统设计与实现

基于Flask与微信小程序的课程提醒系统设计与实现 讲个项目大二课程多教务系统每次查课表都要登录、跳转、验证手机端体验一言难尽。当时我就想自己写一个基于 Python Flask 的小程序后端配合微信小程序前端做一个“课表查询 上课提醒”的小系统能让我和同学每天早上自动收到当天的课程安排不用再手动翻教务系统。这个项目麻雀虽小五脏俱全后端要管用户、课表数据、课时判断还要定时推送提醒小程序端要展示周课表、支持增删课程、处理登录态最后还得考虑部署上线。整个过程踩了不少坑比如小程序 request 合法域名校验、订阅消息一次性授权、Flask 部署后定时任务不执行等等。今天就把完整的实现思路、关键代码和排错经验完整写出来给想做类似校园工具的同学一份可直接复用的参考。项目对 Python Flask 小白、小程序入门开发者、以及想做一个完整全栈作品的学生都适用。1. 项目整体设计与技术选型1.1 为什么后端选 Flask 而不是 Django 或 FastAPI技术选型是这个项目第一步。当时团队就我一个人写后端业务逻辑说白了就是课表的增删改查、周次计算和定时推送没有复杂的权限体系也没有海量并发。这时候 Flask 的优势非常明显核心框架极轻一个app.py就能起服务路由写法直观ORM 用 SQLAlchemy 完全够用部署起来也不折腾。说下对比感受。Django 功能全但对这种小项目显得笨重自带 admin 后台、ORM、迁移工具学习成本和启动成本都偏高。FastAPI 性能好、自动生成文档但异步模型对 Flask 生态的老玩家来说需要适应而且定时任务、模板渲染这些配套没有 Flask 那么多现成方案。Flask 对于“快速验证想法、一个人维护的小项目”就是最舒服的选项。# 我用的环境 python 3.10 Flask 2.3.x Flask-SQLAlchemy 3.0.x Flask-APScheduler 1.13 requests这里有个经验别一上来就把版本拉最新。Flask 3.x 本身没问题但部分扩展库可能没跟上比如flask-sqlalchemy需要 3.0 以上才兼容 Flask 2.2新手容易在这里遇到莫名其妙的报错。锁版本是个好习惯至少锁大版本。1.2 小程序前端形态与整体架构前端选了微信小程序原生开发。原因一是微信生态最成熟学生群体人手一个微信不用装额外 App二是原生小程序组件完善scroll-view、picker、swiper做课表界面足够用。如果团队要同时上支付宝小程序和抖音小程序那可以考虑 uniapp 一套代码多端编译。但如果你只是做一个校园工具原生开发省去了一层框架的编译调试成本性能也更好。实话实说原生小程序的坑已经够多了没必要再叠一层 uniapp 的 buff。整个系统架构分成三层数据层MySQL 存正式数据开发时可以用 SQLite 顶替表结构完全一样切换成本极低。服务层Flask 提供 REST API负责鉴权、课表 CRUD、周次计算APScheduler 做定时任务触发订阅消息推送。展示层微信小程序发送wx.request请求 Flask 接口渲染课表视图处理用户交互。这个小项目最大的技术难点不在某个单点而在于把“课表”这个现实问题抽象成数据结构和接口逻辑。比如“单双周”“1-16周”“周一到周五第几节”这些描述怎么变成数据库里可查询、可判断的字段这是整个系统设计的核心。1.3 数据库设计与课表数据结构我设计的核心表叫course字段如下字段类型说明idInteger主键自增user_idString(64)用户标识存的是 openidcourse_nameString(100)课程名称teacherString(50)授课教师classroomString(50)上课地点day_of_weekInteger星期几1-7start_sectionInteger开始节次如 1 表示第1节end_sectionInteger结束节次如 2 表示第2节start_weekInteger开始周次end_weekInteger结束周次week_typeInteger0 全周1 单周2 双周created_atDateTime创建时间用user_id区分用户而不是建独立的 user 表是因为小程序天然以 openid 作为用户唯一标识课程数据直接挂在 openid 下最简单。如果你还要做校园班级共享课表再考虑加一张班级表但第一版没必要过度设计。周次字段设计成start_week和end_week区间而不是每个课程都存一个“周次列表”这样存储简洁。查询某节课在某周是否上课只要判断start_week current_week end_week再做单双周过滤即可。注意week_type用整数而不是字符串查询时直接用 0、 1判断比字符串single、double更省空间也更高效。建表 SQL 用 Flask-SQLAlchemy 模型类完成Python 代码里定义好模型启动时自动建表。开发环境我用 SQLite部署时切 MySQL只改一行数据库连接串这点 Flask 做得非常优雅。2. 后端 API 与课时判断核心逻辑2.1 API 接口清单与统一返回格式整个系统只需要 5 个接口全是标准 REST 风格方法路径功能POST/api/login用 wx.login 的 code 换 openid返回 tokenGET/api/courses获取当前用户课表POST/api/courses新增课程PUT/api/courses/修改课程DELETE/api/courses/删除课程GET/api/current_week获取当前周次和单双周信息所有接口返回统一格式{ code: 0, message: success, data: {} }code为 0 表示成功非 0 表示业务错误。这样小程序端只需要写一个公共请求函数统一处理错误提示不用每个页面各写一套。这里我吃过一个亏最初返回格式不规范有的接口返回{status: 1}有的返回{success: true}小程序端对接时简直想砸电脑。规范统一是这类个人项目的第一生产力。2.2 当前周次与单双周计算的完整实现这是整个系统最核心的计算逻辑。学生课表永远跟“第几周”绑定而周次是根据开学日期动态推算的不能写死。我采用“基准日期”方案在系统设置里保存一个semester_start_date开学第一天且规定为周一后端每次计算当前周次时用今天减去基准日期天数差除以 7 加 1 就是周次。from datetime import datetime, date def get_current_week(start_date_str: str) - int: 根据开学日期计算当前周次start_date_str 格式: 2025-03-03 start_date datetime.strptime(start_date_str, %Y-%m-%d).date() today date.today() days_diff (today - start_date).days if days_diff 0: return 0 # 还没开学 current_week days_diff // 7 1 return current_week边界情况要想清楚今天是周日days_diff是 66 // 7 1 1算出来第 1 周没问题。如果基准日期不是周一或者中间有国庆、清明这种调休这个算法会偏差。校园项目可以接受如果有条件最好按学校校历的正则规则表来算。寒假暑假期间days_diff会很大算出来一个很大的周次需要在返回给前端时做拦截大于 25 周直接提示“假期中”。单双周判断更简单def is_odd_week(current_week: int) - bool: return current_week % 2 1在查询课表时对week_type做过滤query Course.query.filter( Course.user_id user_id, Course.start_week current_week, Course.end_week current_week ) if current_week % 2 1: query query.filter(Course.week_type.in_([0, 1])) else: query query.filter(Course.week_type.in_([0, 2]))这样同一门课程“单周在 A 教室、双周在 B 教室”的情况也支持了只要分两天录入两条记录。2.3 上课提醒与小程序订阅消息推送提醒功能用的是微信小程序的订阅消息能力。这里有个重要机制小程序要对某个用户推送消息必须先获得用户的一次性订阅授权。也就是说用户在哪个页面点了“允许”按钮后端才能在特定时机给这个用户推送一条模板消息。具体策略是这样在课程管理页面每次新增课程并保存成功后弹出一个授权组件申请用户授权接收课程提醒。授权一次只能用一次所以课程数量多的话要教育用户每次加课都点一下授权或者做一个“批量订阅”按钮调wx.requestSubscribeMessage传多个模板 ID。后端定时任务我用 Flask-APSchedulerfrom flask_apscheduler import APScheduler scheduler APScheduler() def send_class_reminders(): 每天早上 7 点执行查当天有课的用户推送提醒 today_weekday datetime.now().isoweekday() # 周一1周日7 current_week get_current_week(SEMESTER_START) courses_today Course.query.filter( Course.day_of_week today_weekday, Course.start_week current_week, Course.end_week current_week ).all() # 按 user_id 聚合给每个用户拼当天课程列表 user_courses {} for course in courses_today: # 单双周过滤 if (course.week_type 1 and current_week % 2 0): continue if (course.week_type 2 and current_week % 2 1): continue user_courses.setdefault(course.user_id, []).append(course) for openid, courses in user_courses.items(): send_subscribe_message(openid, courses) scheduler.add_job( funcsend_class_reminders, triggercron, hour7, minute0, idclass_reminder_job ) scheduler.start()消息推送有个现实问题一次性订阅的额度消耗很快。比如周一到周五每天推一次一周就要 5 次授权用户很难持续配合。实际运营中很多系统改用“当天第一节上课前 10 分钟”推送让学生只在早上一次性授权体验会好很多。发送订阅消息要调微信接口https://api.weixin.qq.com/cgi-bin/message/subscribe/send需要拿到 access_token这个 token 有效期 2 小时可以全局缓存定时刷新不用每次都请求。3. 小程序前端实现与调试实战3.1 页面设计与核心交互流程小程序端我做了 3 个 Tab 页面课表页核心页面展示本周所有课程按周一至周日纵向排列每节课显示课程名、教室、节次。支持左右滑动切换上一周/下一周也可以直接回到本周。课程管理页课表的增删改查表单包含课程名、教师、教室、星期、开始节次、结束节次、起始周、结束周、单双周类型。我的页设置开学日期展示当前周次查看版本信息。课表页的数据加载逻辑放在onShow而不是onLoad原因是从课程管理页新增课程返回后课表需要自动刷新。onLoad只在页面首次加载时触发onShow每次显示都会触发。Page({ onShow() { this.loadCourses(); this.loadCurrentWeek(); }, loadCourses() { wx.request({ url: ${app.globalData.baseUrl}/api/courses, method: GET, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { if (res.data.code 0) { this.setData({ courses: res.data.data, weekMap: this.buildWeekMap(res.data.data) }); } } }); } });把原始课程数组转换成weekMap一个以星期几为 key 的对象是为了模板渲染方便。wxml里直接按周一到周日循环读取weekMap[i]渲染列表比在模板里做复杂数据过滤要清爽得多。3.2 用 Charles 抓包调试本地 Flask 接口这项目调试阶段最折腾的是接口联调。小程序开发者工具里可以勾选“不校验合法域名”本地直接请求http://127.0.0.1:5000但这只能解决开发工具里的问题。到了真机预览手机上的小程序依然会校验域名请求直接失败。我用 Charles 做代理抓包排查真机上的请求问题。步骤是这样电脑和手机连同一个 Wi-Fi。电脑打开 Charles在 Proxy - SSL Proxying Settings 里添加localhost:5000或你的局域网IP:5000并开启 SSL Proxying。手机设置里把 Wi-Fi 代理改成电脑的局域网 IP端口填 8888。手机浏览器访问chls.pro/ssl安装并信任 Charles 的根证书。此时手机上的小程序发起的wx.request在 Charles 里能看到完整的请求 URL、请求头和响应体。重要提示抓包调试只用于开发阶段正式环境必须配置 HTTPS 合法域名。而且微信官方对调试模式有明确控制真机上“不校验合法域名”开关只在开发版和体验版生效正式版永远走的是真实域名。所以别寄希望于关校验就能上线。Charles 抓包帮我找到了很多联调问题典型如请求头里 token 没传对后端 401。后端返回中文乱码原因是响应没加Content-Type: application/json; charsetutf-8。小程序端request的data对象和 Flask 端request.get_json()字段名对不上比如前端传className后端读course_name404 都不是直接存了个空值。3.3 登录态与 openid 获取的正确姿势小程序登录逻辑不能照抄网上那些旧代码。微信官方现在的建议是wx.login获取临时 code然后由后端调微信接口换取 openid。完整流程import requests app.route(/api/login, methods[POST]) def login(): data request.get_json() code data.get(code) resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: APP_ID, secret: APP_SECRET, js_code: code, grant_type: authorization_code } ).json() openid resp.get(openid) if not openid: return jsonify(code400, messagecode 无效) # 生成 token 返回给前端后续请求带 token 即可 token generate_token(openid) return jsonify(code0, data{token: token})网上老教程喜欢在小程序端直接调wx.login后把 code 发给后端但后端拿到 code 后如果再传给别人的服务器会有安全问题。实践中的标准做法是code 只用一次后端直接和微信服务器交换 openid整个过程 code 不出后端。前端拿到的只是后端签发的 token后续请求通过Authorization头传递比把 openid 暴露在本地存储里安全得多。4. 部署上线与踩坑记录4.1 Flask 部署到云服务器的标准流程开发完就该部署了。先说结论生产环境不要用flask run直接跑它是个开发服务器单进程、无并发、容易超时稍微有点流量就卡死。个人项目也得讲究基本法。我的部署方案是gunicorn nginx supervisor云服务器用 2C4G 的配置跑这个小系统绰绰有余。# 1. 服务器上创建虚拟环境并安装依赖 python3 -m venv venv source venv/bin/activate pip install flask flask-sqlalchemy flask-apscheduler gunicorn requests # 2. 启动 gunicorn4 个 worker gunicorn -w 4 -b 127.0.0.1:8000 app:app然后配置 nginx 反向代理同时挂上 HTTPS 证书。微信小程序要求所有 request 域名必须是 HTTPS所以这一步躲不开server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }最后用 supervisor 守护 gunicorn 进程进程挂了自动拉起。这套组合拳比直接nohup flask run稳得多而且日志统一由 supervisor 管理排错方便。4.2 高频问题速查表把整个开发和部署过程里遇到的高频问题整理成一个速查表问题现象根本原因解决办法小程序开发工具请求正常真机请求失败真机校验收合法域名配置 HTTPS 正式域名且域名需在微信公众平台添加为 request 合法域名Flask 接口返回中文变问号响应头缺 charset使用jsonify或手动指定Content-Type: application/json; charsetutf-8数据库中文乱码MySQL 表字符集不是 utf8mb4建库时指定CHARACTER SET utf8mb4连接串加?charsetutf8mb4定时任务只在本地跑部署后不执行生产环境没启动 APScheduler确保scheduler.init_app(app)和scheduler.start()在应用工厂中执行今天周次算错开学日期填错或当前时间是周日深夜基准日期统一用周一服务器时区设为Asia/Shanghai订阅消息推不出去用户没授权或 token 过期检查 access_token 缓存刷新逻辑确认用户点击过订阅授权小程序上传代码没反应未设置 AppID 或没预览在 project.config.json 填正式 AppID体验版上传前先预览4.3 从这个小项目延伸出去的方向这个系统如果继续做下去有几个很自然的方向可以扩展。第一个是课程冲突检测。现在新增课程时完全不判断是否和已有课程重叠有同学把两门课录在同一时段自己都不知道。加一个检查接口录入时把同一 user_id 下 day_of_week、节次区间做交集判断有冲突就提示。第二个是空教室查询。课表数据其实可以反过来用某个时段某教室没课就是空教室。这对自习需求极其高频比光秃秃的课表实用得多。第三个是考试周倒计时。很多人课程在第 16 周就结课了系统可以在课程管理时让用户选择“是否考试”然后在“我的”页面展示距离下一场考试的天数。做完这个项目我的个人体会是做一个全栈小系统的核心能力不是把某个框架文档背得多熟而是把一个现实问题拆成数据结构、接口、页面、定时任务四层每一层的边界都清晰了代码自然就好写了。这一套思路放到任何校园工具、个人效率应用上都适用。所以别纠结技术栈是否够新先用自己最顺手的工具把整个链路跑通一遍收获比看十篇教程都大。
返回列表