ARTICLE DETAIL

资讯详情

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

Python+Flask+微信小程序:课表查询与上课提醒系统全解析

Python+Flask+微信小程序:课表查询与上课提醒系统全解析 上课这件事说起来全是泪。早上第一节睡过头、下午的课换教室没留意、选修课隔周才上结果记错了周次——这些场景每个大学生都经历过。做这个课表查询和上课提醒系统起因就是我自己也深受其害加上后台收到不少同学留言问有没有课表类的工具索性用PythonFlask搭了一套后端前端用微信小程序实现课表展示和消息提醒完整跑通后把源码和部署心得整理出来。这篇内容适合正在做毕设选题、或者想练手全栈项目的同学参考我会把需求拆解、技术选型、核心代码逻辑、部署细节到踩坑记录一次性讲清楚。整套系统分三层小程序负责展示和交互Flask提供API接口和数据管理数据库存用户、课程和提醒记录。核心功能就是课表的多维展示按周次、星期、节次、上课前的微信订阅消息提醒以及一套能让非技术同学也能快速导入课表的后台逻辑。下面我从需求梳理开始逐步还原整个开发过程。1. 项目需求拆解学生课表管理的真实痛点在哪做项目最忌讳一上来就写代码。课表系统听起来简单但把需求掰开揉碎之后你会发现坑比想象中多。1.1 核心功能清单这个系统最终要满足的需求可以归纳成四条课表查询按学期、周次、星期几展示课程支持切换查看单周或双周课程。上课提醒用户在微信端授权订阅消息系统在每节课开始前N分钟推送提醒。课程管理支持手动添加、导入和删除课程字段包括课程名、教师、教室、起始周、结束周、单双周类型、星期、节次。用户体系通过微信登录绑定openid保证每个人看到的是自己的课表。看起来不复杂但“周次”和“节次”这两个概念是隐藏难点。大学课表有单周、双周、1-8周、9-16周这些灵活规则节次又和具体上下课时间绑定数据建模稍不留神就会出逻辑漏洞。1.2 用户场景分析我调研了一圈身边同学的使用习惯发现课表查询系统有这么几类典型用法每天早上看一眼“今天上什么课”这个场景只需要按星期几展示。考前复习时想知道“这门课这周有没有”需要支持周次维度。换教室、调课频繁的同学需要快速编辑单条课程。老师临时停课用户希望能在小程序里看到备注信息。这些场景决定了系统不能只做一个静态课表还得有“当前周次”这个概念。实际开发中我设置了一个配置项开学日期。程序根据当前日期减开学日期除以7取整加1就得出了当前是第几周。这个换算逻辑是我从学校教务系统的时间规则里总结出来的放到系统里后课表展示才真正“活”了。1.3 非功能性需求这种学生项目大家往往只关注功能忽略了非功能需求。但课表查询有明显的峰值流量特征——早上7点到8点、中午12点到13点、傍晚17点到18点是三个高峰每次持续二十分钟左右。Flask默认的开发服务器根本扛不住所以生产环境必须用gunicorn这类WSGI服务器并且要考虑接口的响应速度。我把每个接口的查询都做了索引优化课表接口实测响应控制在200毫秒以内基本能做到秒开。2. Flask后端设计框架选型、数据建模与API规划技术选型这部分我觉得有必要把Flask和几个常用框架放在一起对比毕竟后台每天都有同学问“为什么选Flask而不是FastAPI”。2.1 为什么是Flask而不是FastAPI或Django网上一搜“flask 与 fastapi 比较”能翻出大量对比文章我结合实际开发体验说几点Flask生态成熟文档齐全网上案例极多遇到问题很容易搜到解决方案这对学生项目来说特别重要。FastAPI的优势在自动生成OpenAPI文档和异步性能但课表系统是典型的轻量级CRUD应用瓶颈在数据库和微信接口调用不在Web框架本身异步带来的收益不明显。Django自带Admin后台和ORM确实开发效率高但Django默认的“全家桶”风格在这个规模的项目里显得重而且学习曲线对新手不那么友好。最后留了Flask另外装Flask-SQLAlchemyORM、Flask-Migrate数据库迁移、Flask-JWT-Extendedtoken认证这几个扩展既保持轻量又不至于什么都自己造轮子。2.2 项目目录与代码结构我的Flask项目结构如下course-system-backend/ ├── app.py # 应用入口创建Flask实例 ├── config.py # 配置文件数据库连接、小程序appid等 ├── models/ │ ├── __init__.py │ ├── user.py # 用户模型 │ ├── course.py # 课程模型 │ └── reminder.py # 提醒记录模型 ├── api/ │ ├── __init__.py │ ├── auth.py # 登录、token签发 │ ├── course.py # 课表增删改查 │ └── reminder.py # 订阅消息发送相关 ├── services/ │ ├── week_service.py # 周次换算逻辑 │ └── reminder_sender.py # 定时任务 消息推送 ├── requirements.txt └── wsgi.py # gunicorn入口把业务逻辑拆到services目录是我这次特意养成的习惯。以前写Flask项目喜欢全塞在路由函数里看起来是省事但定时任务、API路由都要调用“计算当前周次”时就发现代码根本没法复用。单独抽出week_service后谁需要谁导入清爽很多。2.3 数据库表结构与关键字段设计一共三张表用户表、课程表、提醒记录表。课程表是核心字段设计如下字段类型说明idint主键user_idint所属用户course_namevarchar(50)课程名称teachervarchar(30)教师locationvarchar(50)教室week_startint起始周week_endint结束周week_typetinyint0每周, 1单周, 2双周day_of_weektinyint星期几1-7start_sectiontinyint开始节次end_sectiontinyint结束节次remarkvarchar(255)备注这里的难点在week_type字段。一开始我图省事只存“起始周”和“结束周”结果遇到“1-8周单周上课”这种课程直接傻眼。后来加了week_type课程在week_start到week_end之间的判断规则变成如果是单周课程且周次-week_start为偶数则该周上课。换算逻辑在代码里反复验证过覆盖了“单周起始”“双周起始”“每周都有”三种情况。提醒记录表也很关键。它不只是记录“是否提醒过”还保存了消息发送状态待发送、已发送、发送失败。定时任务扫描这张表避免了重复发送的幂等性问题。2.4 API接口规划接口遵循REST风格统一返回JSONPOST /api/auth/login # 微信登录code换openid和token GET /api/courses # 获取当前用户课表支持week参数 POST /api/courses # 新增课程 PUT /api/courses/id # 修改课程 DELETE /api/courses/id # 删除课程 GET /api/reminders # 获取提醒配置 POST /api/reminders # 开启/关闭提醒设置提前时间 POST /api/reminder/test # 发送测试订阅消息课表查询接口我设计了week参数不传就按当前周次返回GET /api/courses?week3。这样小程序端可以自由切换“本周”“下周”也给“第几周”的透传留了余地。3. 小程序端实现课表渲染、登录流程与提醒触发小程序端的技术选型很多人问“用uniapp不好吗”。我的看法是如果你的目标平台只有微信原生小程序永远是第一选择性能好、调试工具友好、没有编译层带来的奇怪兼容性问题。uniapp适合未来可能上支付宝、抖音小程序的场景但代价是某些微信原生能力比如订阅消息的授权交互封装得不够灵活。这个项目用了原生。3.1 课表页面的渲染逻辑课表页面的核心是把课程数组渲染成七列的表格式布局。我采用的方法是从后端拿到的课程数据先在前端按day_of_week分组然后根据start_section和end_section计算纵向跨度。每节课的节次占用高度计算节次跨度乘以单节高度再加上间隙。这个计算直接决定了课表网格的对齐效果。// 计算课程块高度 const sectionHeight 80; const gap 4; const span course.end_section - course.start_section 1; course.height span * sectionHeight (span - 1) * gap;实测下来的经验是节次高度的单位不能写死应该根据设备宽度动态计算。iPhone SE和小米13的屏幕宽度差了近一倍固定高度在窄屏上会出现压缩变形。3.2 周次切换与当前周定位小程序顶部固定一个周次选择器左侧是“上一周”“下一周”箭头中间显示“第X周”。进入页面时调用后端接口拿到当前周次默认选中本周。这里有个小细节学期初和学期末学生更关心“这学期一共有多少周”。我加了一个学期总周数配置项周次选择器最大不能超过总周数不然用户一路往下翻会出现空白课表。3.3 微信登录与手机号绑定登录流程用的是微信官方推荐的静默登录wx.login拿到code传给后端后端拿着code向微信接口换取openid同时签发自己的token。网上很多方案是前端存openid但openid属于敏感标识直接暴露存在安全隐患。我这边后端返回的是JWT tokenopenid留在服务端前端所有请求带上Authorization请求头即可。手机号绑定这一步需要特别提醒微信在2023年后调整了规则getPhoneNumber接口必须在小程序后台申请短信验证能力个人主体小程序无法直接调用“获取手机号”接口。如果你是小程序开发者做毕设可以用一个替代方案让用户手动填写手机号后端发验证码。这个方案对个人开发者最省心否则卡在资质审核上会很头疼。3.4 提醒开关的前端交互提醒功能的入口在课表页右上角点击后弹出配置面板选择提前时间5分钟/10分钟/15分钟/30分钟然后点击“开启提醒”按钮。这一步会触发wx.requestSubscribeMessage申请订阅消息授权。这里有个必须知道的微信规则订阅消息分为“一次性订阅”和“长期订阅”。长期订阅仅限特定类目普通开发者能用的只有一次性订阅。也就是说用户每授权一次只能给他推送一条消息。为了覆盖一学期的提醒我做了“批量订阅”方案用户点击开启时引导连续授权多次最多一个按钮可以绑定多次模板调用。这个交互需要处理得委婉一些不然用户会觉得烦。我的做法是授权完成后弹一个提示“已为您开启本周提醒下周课程提醒会在每日首次打开时自动补发订阅次数”。4. 上课提醒的完整链路定时任务、订阅消息与边界处理提醒功能是整个系统里最值得讲的一块。它涉及定时任务、第三方API调用、异常处理三块知识也是面试时最容易被追问的部分。4.1 整体方案选型定时任务方案我对比过三个系统crontab简单可靠但无法控制任务内部逻辑比如任务执行到一半挂了怎么补偿。Celery调度能力强支持分布式但引入Redis、消息队列对一个课表项目来说过于庞大了。APScheduler轻量支持内存和数据库两种job存储能跑在Flask进程内够用且好维护。最后选了APScheduler。它有个让我很安心的特性——job misfire处理。比如服务器在提醒时间点恰好CPU占满任务延迟执行了APScheduler会根据misfire_grace_time配置决定是立即补跑还是跳过。4.2 定时扫描与Redis锁先说一下我遇到的一个实际坑带--reload参数启动gunicorn时Flask应用会跑在两个进程里APScheduler如果跟着应用启动会创建两份任务导致重复扫描、重复发提醒。解决方案是在多进程部署时使用Redis锁任务执行前先抢锁抢到才干活。def scan_and_send(): # 尝试获取锁防止多进程重复执行 lock_key reminder_scan_lock lock_acquired redis_client.set(lock_key, 1, nxTrue, ex60) if not lock_acquired: return now datetime.now() current_week week_service.get_current_week() weekday now.isoweekday() # 查询所有需要在这个时刻提醒的记录 reminders Reminder.query.filter_by( statuspending, send_time__ltenow timedelta(minutes10) ).all() for reminder in reminders: send_subscribe_message(reminder)定时频率我设置成每30秒扫描一次。开始觉得这个频率太高后来想到提醒提前量是按分钟计的30秒的粒度才能保证消息在“提前15分钟”准时发出而不是延迟一分钟引起用户吐槽。4.3 微信订阅消息发送的核心代码微信订阅消息的发送分两步获取access_token然后调用订阅消息发送接口。access_token的有效期是2小时但官方建议“全局缓存临近过期刷新”不能每次发送都重新获取。我的实现里加了一层Flask-Caching做缓存有效期设为7000秒留了200秒缓冲避免临界过期。def send_subscribe_message(openid, template_id, page, data): token get_access_token() url https://api.weixin.qq.com/cgi-bin/message/subscribe/send headers {Content-Type: application/json} payload { touser: openid, template_id: template_id, page: page, data: data, miniprogram_state: formal } resp requests.post( url, params{access_token: token}, jsonpayload, headersheaders, timeout5 ) result resp.json() if result.get(errcode) ! 0: # 记录失败原因进入补偿逻辑 log_reminder_fail(openid, template_id, result)这里特别要注意timeout5。微信接口偶尔会卡顿没有超时设置的话一个慢请求会把整个扫描任务拖住后面的课程全得排队。4.4 边界情况的处理以下这些边界情况我是在测试过程中慢慢补上的课程在学期中临时调课引入了remark字段运维方在小程序后台改课前端直接展示备注不搞复杂的调课状态机。用户已经不在上课比如中途退课但提醒记录还在。我加了一个判断扫描时检查课程是否还存在不存在就撤销待发送的提醒。考试周大部分学校第17、18周是考试周所有课程停止。这个可以在学期配置里设置“考试周区间”扫描任务在考试周全部跳过。这套边界逻辑看似零碎但正是这些细节决定了系统是不是真的“能用”。很多课表项目demo能跑一上真实数据就各种错提醒问题就出在没有处理单双周和调课这些特殊情况。5. 部署上线实录gunicorn、nginx与微信域名校验后端写好了小程序端调通了下一步就是部署上线。这一阶段我花的时间不比写代码少但踩完坑之后回头看其实整个流程完全可以照着走一遍。5.1 服务器环境准备我用的是一台2核4G的云服务器操作系统Ubuntu 22.04。先在服务器上装好Python 3.10、pip、nginx、Redis然后创建虚拟环境mkdir -p /opt/course-system cd /opt/course-system python3 -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt里的关键依赖版本如下flask2.3.3 flask-sqlalchemy3.1.1 flask-jwt-extended4.5.3 apscheduler3.10.4 requests2.31.0 redis5.0.1 gunicorn21.2.05.2 gunicorn配置与启动gunicorn启动命令gunicorn -w 2 -b 127.0.0.1:8000 wsgi:app --daemon --pid /tmp/gunicorn.pid这里解释一下配置思路-w 2表示两个worker进程2核机器跑2个worker刚好。--daemon后台运行配合pid文件方便管理。注意不要加--reload那是开发模式用的生产环境加它会平白多出“扫任务重复跑”的问题。测试时验证接口通不通curl -X POST http://127.0.0.1:8000/api/auth/login \ -H Content-Type: application/json \ -d {code:test}5.3 nginx反向代理与HTTPS证书nginx配置的主要作用是反向代理和静态资源缓存。Flask本身性能没问题但静态文件交给nginx处理能减轻Python进程的负担。server { listen 443 ssl; server_name yourdomain.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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }HTTPS证书我用的是Let‘s Encrypt的免费证书配合certbot自动续期一年下来没操过心。小程序后台的“服务器域名”配置里必须把https://yourdomain.com加进request合法域名否则真机调试时请求根本发不出去。6. 开发过程踩坑记录从时区错乱到消息重发最后这部分我整理了几个开发过程中让我抓狂的问题。这些问题网上资料少遇到了只能自己慢慢排查希望写出来能帮你跳过这些坑。6.1 时区问题导致提醒时间错乱第一次测试提醒功能时我设置的提醒时间是8:00结果消息在9:30才到。排查了半天发现服务器是UTC时区而我的代码里用的datetime.now()拿到的是UTC时间换算到北京时间正好差了8小时。解决方案所有时间计算统一使用Asia/Shanghai时区在Flask的配置里设置from datetime import datetime import pytz LOCAL_TZ pytz.timezone(Asia/Shanghai) now datetime.now(LOCAL_TZ)同时数据库里存时间戳时统一用带时区的时间展示层再格式化。这个教训告诉我涉及时间的系统从第一行代码就要想好时区策略越早统一越省事。6.2 重复推送一场让人哭笑不得的“连环call”有次测试我同时开了两个gunicorn worker和Flask的debug模式结果一分钟内收到了三条相同的上课提醒。原因在6.2节里已经说了多进程重复扫描但真正让我理解透彻的是“幂等设计”这个思路。除了Redis锁我还在数据库层面加了唯一约束一张提醒记录表里course_id scheduled_date remind_type三个字段联合唯一。即使并发请求漏过了锁数据库也会拒绝重复插入。双保险下来重复发送的问题彻底消失。6.3 小程序canvas导出课表图片的坑很多同学希望把课表导出成图片发到群里。这个功能我用的是小程序canvas 2d接口。踩坑点是新版canvas接口中用wx.createCanvasContext创建的是旧版绘制到一半会发现位置偏移或者模糊。正确的做法是用新版接口const query wx.createSelectorQuery(); query.select(#courseCanvas).fields({ node: true, size: true }) .exec((res) { const canvas res[0].node; const ctx canvas.getContext(2d); // 绘制逻辑 });另外画布尺寸要按wx.getSystemInfoSync()返回的像素比做适配否则导出图片在部分安卓机上会模糊。6.4 Flask debug模式与定时任务冲突这是让我最意外的一个坑Flask开启debugTrue时代码一改动开发服务器会自动重启但APScheduler也会随之重新初始化。如果你在debug模式下调试定时任务会发现任务被初始化两次每30秒的扫描任务实际变成了每15秒。这不是代码问题是Werkzeug的reloader机制在作怪。我的建议是开发时不要开debug模式调试定时任务或者用use_reloaderFalse关掉自动重启。生产环境根本用不到debug这个问题只会在开发期纠缠你。后续还能怎么扩展课表查询和上课提醒这套基础功能跑通之后可扩展的方向还挺多的。比如接入学校的教务系统实现课表自动同步省去手动录入的麻烦。做成“课程伴侣”增加作业截止日期管理、考试倒计时、成绩查询入口。把提醒能力扩展成“自定义事件提醒”不局限于上课还能提醒交作业、开会、抢课。我自己的下一步计划是把“课程分享”功能做出来——同学之间可以一键分享某一节课的完整信息省得每次都在微信里手打教室号。这个系统目前的架构是FlaskSQLAlchemy微信小程序扩展新功能基本就是“加一张表、加一个接口、加一个页面”的事开发效率会越来越高。从找选题到系统上线这个项目前后花了三周。如果你也正在做类似的课表系统希望这篇内容能帮你少走弯路。等你的系统上线了记得回来告诉我你踩了哪些新的坑我们评论区见。
返回列表