
简介这是一套面向Python全栈初学者与教学实践者的学籍管理系统完整项目源码融合Flask后端开发与微信小程序前端解决高校或培训机构对轻量级、跨终端学籍管理工具的落地需求。资源共179个文件包含14个核心Python后端逻辑文件含Flask路由、数据库模型与API接口、49个小程序JS交互逻辑与WXML结构文件、29个HTML页面模板及20个CSS样式文件辅以SQLite数据库脚本sql、配置文件conf和静态资源png/gif/svg等整体压缩包仅1.34MB结构清晰、开箱即用。已有149人学习下载项目覆盖学生/课程/成绩/考勤/权限五大功能模块并提供supervisord进程管理配置、Bootstrap/Layui前端框架集成及Dockerfile容器化支持便于快速部署调试与二次开发。1. 用 Flask 搭学籍管理后端微信小程序当入口这不是教务系统复刻而是教育场景里最轻量、可快速上线的师生数据协同方案很多学校信息中心或教师团队想管学生基本信息、班级归属、成绩录入和查询但买商业系统要审批、部署重、定制慢用 Excel 又难协同、无权限、易出错。这时候“Python 学籍管理系统Flask 小程序”就不是一句技术堆砌——它是一条明确路径用 Python 写一个 RESTful API 服务Flask只暴露/students,/classes,/grades等标准接口微信小程序不存数据只调这些接口做增删查改。前后端物理隔离数据库如 SQLite 或 MySQL只对 Flask 服务开放小程序通过wx.request安全通信。适合 500 人以内中职/小学/培训机构场景开发周期可压缩到 3 天原型 2 天联调。新手能从pip install flask开始跑通熟手则关注 JWT 鉴权粒度、小程序 wx.login 与 Flask session 绑定、以及批量导入 Excel 的字段映射鲁棒性。2. Flask 后端设计从路由定义、数据库建模到 REST 接口返回规范2.1 为什么选 Flask 而非 Django 或 FastAPI教育类内部系统对高并发要求低日活通常 200但对开发速度、调试透明度和部署轻量级敏感。Django 自带 ORM 和 Admin但默认结构重、学习曲线陡且 Admin 后台与小程序前端无直接关联反而增加维护面FastAPI 虽快但依赖 Pydantic 类型校验在学籍字段如“是否住校”为布尔、“入学年份”为整数、“备注”为可空文本简单场景下额外类型声明反成冗余。Flask 的核心优势在于路由即视图函数无隐藏层SQLAlchemy ORM 可按需启用WSGI 部署兼容 Nginx uWSGI 或直接用flask run --host0.0.0.0快速验证。实际项目中我一般用 Flask-SQLAlchemy 管理模型用 Flask-Migrate 做数据库版本控制避免手动 SQL 迁移出错。2.1.1 初始化 Flask 应用与配置分离# app.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app Flask(__name__) # 从环境变量读取配置避免硬编码密码 app.config[SQLALCHEMY_DATABASE_URI] sqlite:///school.db # 开发用 # app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://user:passlocalhost/school # 生产用 app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False app.config[SECRET_KEY] dev-key-change-in-prod # 用于 session 加密 db SQLAlchemy(app) migrate Migrate(app, db)提示SQLALCHEMY_DATABASE_URI是关键参数。SQLite 适合单机演示和小规模部署文件在./school.dbMySQL 则必须提前建库并授权用户。若用 MySQL需安装pymysqlpip install pymysql且确保user用户对school库有SELECT, INSERT, UPDATE, DELETE权限。2.2 学籍核心模型设计兼顾业务语义与查询效率学籍管理不是纯 CRUD需体现“学生-班级-课程-成绩”四层关系。但小程序端常只查某班学生列表或某生成绩单因此模型设计要避免过度嵌套。以下为最小可行模型含外键约束和索引建议# models.py from datetime import datetime from app import db class ClassInfo(db.Model): __tablename__ class_info id db.Column(db.Integer, primary_keyTrue) class_name db.Column(db.String(50), nullableFalse, indexTrue) # 索引加速按班级查 grade_level db.Column(db.String(20), nullableFalse) # 如“高一”、“三年级” created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Student(db.Model): __tablename__ student id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(30), nullableFalse) student_id db.Column(db.String(20), uniqueTrue, nullableFalse, indexTrue) # 学号唯一且高频查询 gender db.Column(db.String(10), nullableTrue) # “男”/“女”/None class_id db.Column(db.Integer, db.ForeignKey(class_info.id), nullableFalse) phone db.Column(db.String(20), nullableTrue) avatar_url db.Column(db.String(255), nullableTrue) # 小程序上传头像后存 URL created_at db.Column(db.DateTime, defaultdatetime.utcnow) class GradeRecord(db.Model): __tablename__ grade_record id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(student.id), nullableFalse, indexTrue) subject db.Column(db.String(30), nullableFalse) # 如“数学”、“语文” score db.Column(db.Float, nullableFalse) # 允许小数如 89.5 term db.Column(db.String(20), nullableFalse) # “2024-2025学年第一学期” created_at db.Column(db.DateTime, defaultdatetime.utcnow)2.2.1 数据库迁移与初始化运行以下命令生成初始迁移脚本并应用flask db init flask db migrate -m init tables for student management flask db upgrade注意flask db命令依赖FLASK_APPapp.py环境变量。Windows 下执行set FLASK_APPapp.pyLinux/macOS 下执行export FLASK_APPapp.py。迁移后school.db文件将包含三张表及外键约束可用sqlite3 school.db .schema验证结构。2.3 REST 接口实现遵循小程序调用习惯的 JSON 返回格式小程序wx.request默认期望code、data、msg三字段结构且data为数组或对象。Flask 接口需统一包装避免前端反复判断。以下为/api/students查询接口示例支持分页和班级筛选# routes.py from flask import jsonify, request from app import app, db from models import Student, ClassInfo app.route(/api/students, methods[GET]) def get_students(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 20, typeint) class_id request.args.get(class_id, typeint) query Student.query if class_id: query query.filter_by(class_idclass_id) pagination query.paginate(pagepage, per_pageper_page, error_outFalse) students [{ id: s.id, name: s.name, student_id: s.student_id, gender: s.gender, class_name: s.class_info.class_name if s.class_info else , phone: s.phone, avatar_url: s.avatar_url or } for s in pagination.items] return jsonify({ code: 0, data: { list: students, total: pagination.total, page: page, per_page: per_page }, msg: success })2.3.2 关键参数说明与调试技巧request.args.get(...)用于获取 URL 查询参数如?class_id3page2typeint强制转换避免字符串比较错误pagination.items是当前页数据列表pagination.total是总记录数小程序分页控件依赖此值s.class_info.class_name利用了 SQLAlchemy 的关系加载需在Student模型中定义class_info db.relationship(ClassInfo, backrefstudents)避免 N1 查询返回code0表示成功小程序可统一用if (res.data.code 0)判断比检查res.statusCode 200更可靠HTTP 状态码可能被代理覆盖。3. 微信小程序前端对接从登录态绑定、数据请求到列表渲染全流程3.1 小程序登录与 Flask 后端 Session 绑定小程序无法直接使用 Cookie但可通过wx.login()获取临时 code再由前端传给 Flask 接口换取 session 标识。常见误区是直接把openid当 token 存储这存在安全风险openid可被伪造。正确做法是小程序调用wx.login()→ 发送 code 到/api/login→ Flask 用 code 向微信服务器换取openid→ 生成短期有效 token如 JWT返回 → 小程序存储 token 并在后续请求 header 中携带。# routes.py 新增 login 接口 import requests import jwt from datetime import datetime, timedelta app.route(/api/login, methods[POST]) def login(): data request.get_json() code data.get(code) if not code: return jsonify({code: -1, msg: code required}), 400 # 向微信服务器换取 openid url fhttps://api.weixin.qq.com/sns/jscode2session?appid{APPID}secret{APP_SECRET}js_code{code}grant_typeauthorization_code res requests.get(url) wx_data res.json() if openid not in wx_data: return jsonify({code: -2, msg: invalid code}), 401 # 生成 JWT token有效期 24 小时 token jwt.encode({ openid: wx_data[openid], exp: datetime.utcnow() timedelta(hours24) }, app.config[SECRET_KEY], algorithmHS256) return jsonify({code: 0, data: {token: token}, msg: login success})提示APPID和APP_SECRET需在微信公众号平台或小程序后台获取绝不可硬编码在前端或 Flask 源码中。生产环境应通过环境变量注入如os.getenv(WECHAT_APPID)。3.2 小程序端请求封装与错误处理在utils/request.js中统一封装wx.request自动添加 token 并处理通用错误// utils/request.js function request(url, options {}) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: https://your-domain.com url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else if (res.data.code -1) { // token 过期触发重新登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络错误, icon: none }); reject(err); } }); }); } module.exports { request };3.2.1 学生列表页面 WXML 渲染逻辑pages/student/list.wxml使用wx:for渲染关键点在于bindtap传递student.id跳转详情页并用wx:if控制头像占位符view classstudent-list block wx:for{{students}} wx:keyid navigator url/pages/student/detail?id{{item.id}} classstudent-item image src{{item.avatar_url || /images/avatar-default.png}} classavatar / view classinfo view classname{{item.name}}/view view classmeta text{{item.student_id}}/text text{{item.class_name}}/text /view /view /navigator /block /view注意wx:for的wx:key必须是唯一字段如id否则列表滚动时会出现渲染错乱image的src属性若为空字符串会报错故用||提供默认占位图路径。3.3 成绩录入表单提交处理多科成绩的批量保存小程序端用form组件收集成绩提交时需将多个科目成绩打包为数组。Flask 接口接收后需校验每科分数范围0–100、去重同一学生同科目同学期不能重复录入并原子化写入# routes.py app.route(/api/grades, methods[POST]) def save_grades(): data request.get_json() student_id data.get(student_id) grades data.get(grades, []) # [{subject: 数学, score: 95}, ...] if not student_id or not grades: return jsonify({code: -1, msg: student_id and grades required}), 400 # 批量校验 for g in grades: if not isinstance(g.get(score), (int, float)) or not (0 g[score] 100): return jsonify({code: -2, msg: finvalid score: {g.get(score)}}), 400 # 删除该生本学期已有记录避免重复 GradeRecord.query.filter_by( student_idstudent_id, termdata.get(term, 2024-2025第一学期) ).delete() # 批量插入 records [ GradeRecord( student_idstudent_id, subjectg[subject], scoreg[score], termdata.get(term, 2024-2025第一学期) ) for g in grades ] db.session.add_all(records) db.session.commit() return jsonify({code: 0, msg: grades saved})3.3.1 小程序端表单提交代码片段// pages/grade/submit.js const request require(../../utils/request.js); Page({ data: { subjects: [语文, 数学, 英语, 物理, 化学], scores: {} }, onInput(e) { const subject e.currentTarget.dataset.subject; this.setData({ [scores.${subject}]: e.detail.value }); }, onSubmit() { const grades Object.entries(this.data.scores) .filter(([_, score]) score ! ) .map(([subject, score]) ({ subject, score: parseFloat(score) })); request(/api/grades, { method: POST, data: { student_id: this.data.studentId, term: 2024-2025第一学期, grades } }).then(() { wx.showToast({ title: 提交成功, icon: success }); wx.navigateBack(); }); } });4. 部署与联调Nginx 反向代理、HTTPS 配置及小程序域名白名单实操4.1 Flask 生产部署uWSGI Nginx 最小化配置开发时flask run可用但生产必须用 uWSGI 管理进程。先安装pip install uwsgi再创建uwsgi.ini# uwsgi.ini [uwsgi] http :5000 master true processes 2 threads 2 module wsgi:app callable app vacuum true die-on-term true logto /var/log/uwsgi/school.log其中wsgi.py是入口文件# wsgi.py from app import app if __name__ __main__: app.run()启动命令uwsgi --ini uwsgi.ini。此时 Flask 服务监听http://127.0.0.1:5000但需 Nginx 反向代理以支持 HTTPS 和静态资源。4.1.1 Nginx 配置要点静态文件托管与 API 转发# /etc/nginx/sites-available/school.conf server { listen 80; server_name school.yourdomain.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name school.yourdomain.com; ssl_certificate /etc/letsencrypt/live/school.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/school.yourdomain.com/privkey.pem; # 小程序静态资源如图片、字体直接由 Nginx 提供 location /static/ { alias /var/www/school/static/; expires 1h; } # API 请求转发到 uWSGI location /api/ { include uwsgi_params; uwsgi_pass 127.0.0.1:5000; # 与 uwsgi.ini 的 http 端口一致 uwsgi_read_timeout 300; } # 根路径返回 404因所有页面由小程序承载 location / { return 404; } }提示uwsgi_pass地址必须与 uWSGI 监听地址一致uwsgi_read_timeout设为 300 秒避免批量导入 Excel 时超时中断SSL 证书用 Certbot 自动续期certbot --nginx -d school.yourdomain.com即可。4.2 微信小程序后台配置服务器域名与业务域名白名单微信强制要求所有wx.request域名必须在「小程序管理后台 开发管理 开发者工具 服务器域名」中备案。备案域名必须满足仅支持 HTTPSHTTP 会被拦截不能带端口如https://school.yourdomain.com:5000无效不能是 IP 地址https://192.168.1.100不允许request、uploadFile、downloadFile需分别添加。域名类型填写内容说明request 合法域名https://school.yourdomain.com所有 API 接口前缀uploadFile 合法域名https://school.yourdomain.com头像上传走同一域名downloadFile 合法域名https://school.yourdomain.com如导出 Excel 报表注意备案后需等待 5 分钟生效且修改后需重新提交审核仅域名变更无需重审整个小程序。若测试时遇到request:fail url not in domain list首先检查 Nginx 是否已启用 HTTPS 并返回 200其次确认域名拼写与备案完全一致包括 www 前缀。4.3 联调排错三板斧curl 模拟、日志定位、小程序真机调试当小程序调用/api/students返回空数据按顺序排查用 curl 模拟请求绕过小程序curl -H Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... \ https://school.yourdomain.com/api/students?class_id1若返回正常则问题在小程序端 token 未携带或 URL 拼写错误若返回 500则看 Flask 日志/var/log/uwsgi/school.log。检查 Flask 日志中的 SQL 错误 日志中若出现sqlalchemy.exc.NoResultFound说明class_id1在class_info表中不存在需先插入班级数据若出现OperationalError: no such table则是数据库迁移未执行或school.db路径错误。小程序真机调试开启“调试基础库” 在开发者工具中勾选「调试基础库」真机上打开「设置 关于小程序 版本信息」右上角「…」→「切换调试基础库」选择最新版。此操作可暴露更多底层错误如wx.request的ssl_error。5. 进阶技巧Excel 批量导入学生数据的健壮性处理与字段映射容错5.1 使用 pandas 解析 Excel自动识别表头并映射字段小程序上传 Excel 后Flask 接口需解析.xlsx文件。直接用openpyxl易出错如合并单元格、空行而pandas对表格结构容忍度高。关键在于不依赖固定列序而用表头文字匹配字段名并支持中文别名如“学号”、“学生编号”都映射到student_id# routes.py import pandas as pd from werkzeug.utils import secure_filename app.route(/api/students/import, methods[POST]) def import_students(): file request.files.get(file) if not file or not file.filename.endswith((.xlsx, .xls)): return jsonify({code: -1, msg: only xlsx/xls allowed}), 400 # 用 pandas 读取跳过空行自动推断表头 try: df pd.read_excel(file, dtypestr) # 全部读为字符串避免数字变科学计数 except Exception as e: return jsonify({code: -2, msg: fexcel parse error: {str(e)}}), 400 # 字段映射字典Excel 表头 → 模型字段 field_map { 姓名: name, 学号: student_id, 性别: gender, 班级: class_name, # 注意Excel 里是班级名但模型存的是 class_id 电话: phone } # 提取有效列忽略无关列 valid_cols [col for col in df.columns if col in field_map] if not valid_cols: return jsonify({code: -3, msg: no valid columns found}), 400 # 构建学生列表 students [] for _, row in df.iterrows(): # 查找班级 ID根据班级名 class_name str(row.get(班级, )).strip() class_obj ClassInfo.query.filter_by(class_nameclass_name).first() if not class_obj: return jsonify({code: -4, msg: fclass {class_name} not exists}), 400 # 构造学生数据空值转 None student_data {} for excel_col, model_field in field_map.items(): if excel_col 班级: continue # 已处理 class_id val str(row.get(excel_col, )).strip() student_data[model_field] val if val else None student_data[class_id] class_obj.id students.append(Student(**student_data)) # 批量插入捕获重复学号错误 try: db.session.bulk_save_objects(students) db.session.commit() return jsonify({code: 0, msg: f{len(students)} students imported}) except Exception as e: db.session.rollback() if UNIQUE constraint failed in str(e): return jsonify({code: -5, msg: duplicate student_id found}), 400 return jsonify({code: -6, msg: fdb error: {str(e)}}), 5005.1.1 小程序端上传代码与错误提示增强// pages/student/import.js wx.chooseMessageFile({ count: 1, type: file, success: (res) { const file res.tempFiles[0]; if (!file.name.endsWith(.xlsx) !file.name.endsWith(.xls)) { wx.showToast({ title: 仅支持 Excel 文件, icon: none }); return; } const uploadTask wx.uploadFile({ url: https://school.yourdomain.com/api/students/import, filePath: file.path, name: file, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (uploadRes) { const data JSON.parse(uploadRes.data); if (data.code 0) { wx.showToast({ title: data.msg, icon: success }); } else { wx.showToast({ title: data.msg, icon: none }); } } }); } });提示wx.uploadFile的name参数必须与 Flaskrequest.files.get(file)的 key 一致header中的Authorization不能省略否则后端无法校验登录态。5.2 导入失败时返回具体行号与错误原因上述代码仅返回笼统错误如“班级不存在”但实际运维需要定位到第几行出错。改进方式是在循环中逐行校验并收集错误# 在 import_students 函数内替换循环部分 errors [] for idx, row in df.iterrows(): try: # ... 同上字段提取与校验逻辑 ... students.append(Student(**student_data)) except Exception as e: errors.append(f第{idx 2}行: {str(e)}) # idx 从 0 开始Excel 行号 idx 2含表头 if errors: return jsonify({ code: -7, msg: import failed with errors, errors: errors }), 400这样小程序可展示详细错误“第5行: 班级‘高三(2)班’不存在”管理员立刻知道哪一行数据需修正无需反复试错。本文还有配套的精品资源点击获取