
上个月刚交付了一个家校通平台家长端是微信小程序教师和管理员用 Vue 搭的管理后台后端统一走 Flask 提供的 API。这类项目在校园场景里需求量不小通知公告、作业发布与提交、成绩查询、请假审批、班级通讯录加上家长和老师之间的留言私信。业务本身不复杂但角色多、权限杂真正做起来才发现坑全藏在细节里。如果你准备做课程设计、毕业设计或者公司里正在接类似的 To B 小项目这套“微信小程序 Flask Vue”的组合值得参考。我把整个项目的拆解思路、数据库设计、三端联调的关键代码以及部署时容易翻车的地方整理成文照着做能少走不少弯路。1. 项目整体架构与设计思路1.1 为什么是“小程序 Flask Vue”这套组合先回答一个很多人会问的问题为什么不用 uniapp 直接一套代码搞定小程序和 App为什么不用 Django 或者 Spring Boot为什么管理后台不用现成的若依理由其实很实际。家校通的目标用户是家长家长端使用频率最高的场景是“打开微信就能看”微信小程序天然符合这个习惯不用额外装 App也不存在 Android、iOS、鸿蒙三端适配的成本。如果你只是做一个校园内部工具uniapp 的优势并没有想象中大原生小程序反而调试更直接页面切换、缓存、订阅消息这些能力踩坑更少。后端选 Flask 而不是 Django核心原因是项目体量。家校通的接口数量大概在 30 到 50 个数据量不算大Flask 的轻量特性正好匹配。Django 自带 Admin、ORM、迁移工具功能很强但对这种小型项目来说偏重配置成本高。Flask 加上 Flask-SQLAlchemy、Flask-JWT-Extended、Flask-CORS 这几个扩展半小时就能把骨架搭起来。Flask 的 Blueprint 机制又能很好地按业务模块拆分不会因为轻量就变成一坨乱代码。管理后台选 Vue 3 Element Plus 的原因更简单Vue 对中后台业务的支持太成熟了Element Plus 的表格、表单、弹窗、树形控件基本覆盖了家校通所有管理界面需求。Vue Router 做菜单路由Pinia 存登录状态和用户信息axios 统一封装请求整个后台在工程化程度上比 jQuery 时代清晰一个量级。而且 Vue 是渐进式框架团队里有人不熟 TypeScript 的话直接用 JavaScript 写也完全没压力。这套组合真正的好处是关注点分离小程序端只负责家长体验Vue 端只负责内部管理Flask 只提供标准化 API。三者通过 HTTP JSON 通信天然适合前后端并行开发。1.2 三端角色与核心业务链路家校通平台的角色非常清晰一共四类系统管理员、教师班主任和任课老师、家长、学生。管理员负责班级和教师账号管理教师负责发布通知和作业、审批请假、录入成绩家长负责接收通知、查看成绩、提交请假、和老师沟通。学生多数情况下不直接登录或者只做只读查询。核心业务链路可以用一条“通知发布”的流程说清楚教师在 Vue 后台选择目标班级填标题、正文、附件点击发布。Flask 接口收到请求后写一条 notice 记录同时往 notice_read 关联表里批量插入该班级所有家长的未读记录。家长打开小程序首页请求通知列表时后端根据当前用户 ID 关联查出这些通知并标记每条通知的已读状态。家长点进详情后前端调一个已读接口把未读记录改成已读。这个链路看起来简单但设计时有一个很多人容易搞错的地方通知和已读状态千万不要都堆在 notice 表里。如果通知是按班级群发的一条通知对应几十个家长正确的做法是单独建一张 notice_read 关联表记录notice_id和user_id谁读了谁没读一目了然也方便以后做“一键催读”功能。其他业务链路也是同样的模式作业发布后学生在小程序端可以提交图片或文字回复请假单提交后状态为待审批教师审批后状态变为通过或驳回成绩由教师按班级导入或录入家长只能看到自己孩子的成绩这条规则必须在后端做数据过滤不能指望前端隐藏菜单。2. 数据库设计与 Flask 后端核心实现2.1 数据模型与关系设计家校通的数据量不大但表之间的关系值得认真设计。我这里的表结构是从一个实际跑了两学期的项目里精简出来的你完全可以直接抄作业。先看用户表。我不建议把家长、教师、管理员拆成三张表而是用一张 user 表加 role 字段区分原因很简单这三类角色有大量公共字段比如用户名、密码哈希、姓名、头像、手机号。拆表反而会让登录和消息通知的逻辑复杂化每次都要判断角色然后查不同表。中小型项目里一张 user 表加一个 role 字段是最高效的做法。class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) real_name db.Column(db.String(32), nullableFalse) role db.Column(db.String(16), nullableFalse) # admin / teacher / parent / student class_id db.Column(db.Integer, db.ForeignKey(class_info.id)) phone db.Column(db.String(20)) created_at db.Column(db.DateTime, defaultdatetime.now)班级表和学生表是家校通的骨架。班级表里存年级和班名比如“三年级2班”学生表通过 class_id 关联班级。这里有一个比较关键的设计家长和学生是多对多关系一个家长可以关联多个孩子一个孩子也可能由父母双方同时关注。parent_student db.Table(parent_student, db.Column(parent_id, db.Integer, db.ForeignKey(user.id)), db.Column(student_id, db.Integer, db.ForeignKey(user.id)) )作业、成绩、请假这三张表都有共同的 owner 概念但业务语义完全不同。作业表关联班级和教师成绩表关联学生和课程请假表关联学生和审批教师。注意成绩表一定要冗余一个 exam_name 字段因为同一门课一个学期可能考很多次家长端展示时需要按考试名称分组。通知相关的表设计我这里单独强调一下因为这是家校联系的核心。notice 表存通知内容本身一共有两类可见范围全校和指定班级。class_id 为 NULL 时表示全校有值表示指定班级。notice_read 关联表存每个用户的已读状态这样查询“未读通知数”只需要一条 SQLSELECT COUNT(*) FROM notice_read WHERE user_id ? AND is_read 0。2.2 接口设计与 JWT 鉴权机制Flask 后端采用 Blueprint 按业务模块拆分我习惯的做法是每个模块一个文件加一个前缀/api/auth管登录注册/api/notices管通知/api/homeworks管作业/api/grades管成绩/api/leaves管请假/api/messages管私信/api/upload管文件上传。每个模块内部再用 MethodView 或者装饰器区分 GET、POST、PUT 请求。接口设计遵循一条原则按资源命名不用动词。比如获取通知列表是GET /api/notices发布通知是POST /api/notices审批请假是PUT /api/leaves/id/approve。统一返回格式是{ code: 0, data: ..., msg: ok }code 为 0 表示成功非 0 表示业务错误。小程序端和 Vue 端只需要判断 code 即可不用依赖 HTTP 状态码做业务判断。鉴权用 JWT 而不是 Session这是从一开始就确定的。微信小程序端没有 Cookie 机制Session 的维护在小程序里非常别扭每次请求都要手动带 Cookie 或者自己封装 sessionid。JWT 把用户身份信息加密放在 token 里小程序端存到 wx.setStorageSyncVue 端存到 localStorage每次请求时放到 Authorization 头里后端统一解密校验。def jwt_required(fn): wraps(fn) def wrapper(*args, **kwargs): auth_header request.headers.get(Authorization, ) token auth_header.replace(Bearer , ) try: payload jwt.decode(token, current_app.config[SECRET_KEY], algorithms[HS256]) g.user_id payload[uid] g.user_role payload[role] except jwt.InvalidTokenError: return jsonify({code: 401, msg: 登录已过期请重新登录}), 401 return fn(*args, **kwargs) return wrapper密码存储必须用哈希不能明文。Flask 的 werkzeug.security 提供了 generate_password_hash 和 check_password_hash用法简单安全性也有保障。密码字段我建议和用户表分开不要用同一个模型里直接的 password 字段避免不小心序列化出去。接口权限控制上我踩过一个很深的坑只在装饰器里判断角色还不够还要在接口内部做数据范围过滤。比如一个教师调用成绩列表接口不能只校验“是教师”还要校验这些成绩是不是这个教师教的班级。家长查询成绩时后端必须根据家长关联的学生 ID 过滤否则换个学生 ID 就能看到别人的成绩这种越权漏洞在校园场景里尤其致命。2.3 已读状态、私信与附件上传实现已读状态是家校联系里很微妙的需求。老师在后台发布通知后最关心的就是“有多少家长看了”。这时候 notice_read 关联表的作用就体现出来了。发布通知时后端拿到目标班级的所有家长 ID批量插入未读记录。这里有个性能细节不要用 for 循环一条条插入应该用一条 bulk insert。SQLAlchemy 的db.session.bulk_insert_mappings或者直接拼接INSERT INTO ... VALUES (...), (...)都能搞定几十条数据差距不明显但如果一个学校有几千个家长循环插入会明显拖慢响应。私信功能不需要搞 WebSocket 长连接那是即时通讯的玩法。家校通里的私信本质上就是留言用一张 message 表就行每次请求拉取最近聊天记录轮询间隔 1 到 2 秒已经足够。message 表的核心字段就三个from_user_id、to_user_id、content。为了减少联表查询我会在接口里返回时带上发送者姓名和头像前端直接用。附件上传是另一个容易翻车的点。小程序端 wx.uploadFile 传到 Flask 时Flask 的request.files接收文件文件名我用 uuid 重命名避免中文文件名带来的编码问题也防止路径穿越攻击。文件保存路径不要在代码里写死绝对路径而是通过 config 配置一个 UPLOAD_FOLDER部署时方便调整。上传成功后返回相对路径比如/static/uploads/abc.jpg前端拼上 baseURL 就能访问。3. 微信小程序端开发要点3.1 登录鉴权与全局请求封装小程序的登录流程和普通 web 登录不一样。不能用账号密码表单那种传统方式正确做法是wx.login()获取临时 code把 code 发给后端后端调用微信接口换 openid然后生成 JWT 返回给小程序。这里要注意一个设计决策登录主体是“手机号”还是“微信 openid”我的建议是双轨制首次进入时让家长输入手机号和验证码后端把手机号和 openid 绑定后续直接静默登录。这样做的好处是一个家长换手机或者换微信后只要手机号不变账号数据就不会丢。const http ({ url, method GET, data {} }) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method, data, timeout: 8000, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { if (res.statusCode 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/index }) return } resolve(res.data) }, fail: (err) reject(err) }) }) }上面的请求封装是每个小程序项目的标配。关键点有两个一是 token 放在 header 的 Authorization 字段二是当接口返回 401 时统一跳转登录页避免每个页面都做一遍过期处理。token 的缓存时间一定要设置不要永久有效。实践中我一般让后端签发的 token 有效期 7 天小程序本地缓存时也对应记录过期时间戳提前一天提示用户重新登录。小程序顶部导航栏高度是个很烦人的兼容问题iPhone X 及以上机型有刘海普通安卓机的高度又不一样。我在项目里封装了一个工具函数通过wx.getSystemInfoSync()拿到 statusBarHeight 和 menuButton 的 boundingClientRect动态计算导航栏高度。自定义导航栏虽然麻烦但视觉效果和专业度比默认导航栏高一个档次值得做。3.2 首页、消息列表与未读提醒设计家长打开小程序后的第一屏不是功能菜单而是“今天有什么消息”。首页布局我建议分成三个区域顶部是孩子信息卡片中间是待办提醒未读通知数、待提交作业数、审批中的请假数下面是常用功能入口网格。孩子信息卡片要支持多孩子切换。很多家庭不止一个孩子在同一个学校这时候卡片左上角放一个切换按钮当前选中的孩子 ID 作为全局变量缓存到 storage 里。切换孩子后首页所有数据、通知列表、作业列表都要刷新成对应孩子的数据。这个细节非常影响体验开发时要记得在后端接口里加 student_id 参数。未读提醒的交互设计上我踩过一个小坑不要实时轮询。每天都跑几千次查询对小型 Flask 服务压力不小。我的方案是只在onShow生命周期里请求一次未读统计接口返回未读数后通过wx.setTabBarBadge在消息 Tab 上显示小红点数字。这样用户每次切 Tab 或回首页都会刷新实时性对于家校场景完全够用。订阅消息功能也值得做。老师发布作业或审批请假后通过wx.requestSubscribeMessage引导家长订阅这类消息后续老师再次发布时后端调用微信订阅消息接口推送提醒。这里需要去微信公众平台申请模板测试号没有这个能力要在正式小程序里配置。3.3 作业提交、请假审批等表单流程表单类流程是家校通里最容易写乱的部分。作业提交页面看起来简单显示作业标题和内容下面一个提交按钮点击后弹出拍照或从相册选择图片上传后提交文字说明。但这里有两个隐藏需求一是学生可能提交多张图片二是家长可能只提交文字说明比如口头回复已收到。我的做法是图片上传走独立接口选择完图片立即上传返回图片 URL 数组前端暂存到 data 里。用户点“提交作业”时一次性把文字内容和图片 URL 数组通过 POST 提交到作业接口。这样做的优势是避免提交时网络慢导致的多文件上传失败也方便断点重试。请假审批的状态流转要设计清楚。请假申请有三个状态0 待审批、1 已通过、2 已驳回。小程序端家长能看到当前状态标签被驳回时要显示驳回原因。教师端在 Vue 后台的审批列表里操作通过后系统自动生成一条“请假已通过”的通知写到 notice 表里家长不需要刷新就能在小程序里看到状态变化。表单校验不要全依赖后端。小程序端在提交前先做一遍格式校验日期不能早于今天、原因不能为空、图片数量不超过 9 张减少无效请求。但后端也必须校验不能信任任何客户端传过来的数据这才是真正的安全底线。4. Vue 管理后台开发要点4.1 工程初始化与环境配置管理后台用 Vue 3 Vite Element Plus这套环境配置现在非常成熟但还是有不少新手卡在依赖安装上。npm install报错大概率是 node 版本问题Vite 4 以上需要 Node 16.20 以上最好直接用 Node 18 LTS。Element Plus 安装完别忘了引入样式完整引入的方式适合中小项目不用纠结按需引入的性能优化管理后台本身访问量就不大。npm create vitelatest school-admin -- --template vue cd school-admin npm install npm install element-plus element-plus/icons-vue npm install vue-router pinia axios项目目录结构我习惯按模块划分而不是按文件类型划分。views 目录下按业务模块建子目录notice、homework、grade、leave、user、dashboard。每个模块下放 index.vue 和对应的子组件。api 目录下也按模块分文件比如 notice.js 里集中封装所有通知相关的接口调用。这样开发时改动范围清晰后期维护也不会迷路。环境变量配置要区分开发和生产。Vite 默认支持.env.development和.env.production我在里面定义VITE_API_BASE指向后端地址。开发时指向http://localhost:5000构建时指向线上域名。axios 的 baseURL 直接读取这个环境变量就不会出现“开发环境跑得好好的一部署就全是 404”这种经典问题。4.2 路由、权限与 axios 拦截器设计Vue 管理后台的权限控制是另一个容易踩坑的重灾区。家校通后台有管理员、教师两种角色教师又分班主任和任课教师不同角色的菜单不一样。我的方案是路由表里用两个层级constantRoutes 放登录页和 404 页asyncRoutes 放业务页面每个路由对象带 roles 字段。用户登录后后端返回角色信息前端根据角色过滤 asyncRoutes再用router.addRoute动态添加。Vue Router 传参有两种方式query 和 params我在项目里踩过 params 的坑页面刷新后参数丢了。因为 params 传参依赖路由记忆刷新页面后会丢失。凡是需要在详情页通过参数拉数据的统一用 query虽然 URL 会多一串参数但可靠性最重要。在作业列表页跳转作业详情时router.push({ path: /homework/detail, query: { id: row.id } })详情页里route.query.id就能拿到刷新也不会丢。axios 的封装除了统一携带 token还要做统一的错误提示。响应拦截器里判断code字段非 0 时用 ElMessage 弹错误信息401 时清空本地登录态并跳转登录页。有一个细节上传文件的接口不能用 json 请求要用 FormData而且不要手动去设置 Content-Type让浏览器自动带上 boundary 标记否则后端解析 multipart 表单会出错。service.interceptors.request.use((config) { config.headers.Authorization Bearer ${localStorage.getItem(token)} return config })4.3 高频业务页面的实现技巧通知发布页是教师使用频率最高的页面我把表单设计成三个部分基本信息标题、正文、发布范围全校或指定班级、附件上传。发布范围用 Element Plus 的级联选择器先选年级再选班级。指定班级支持多选因为一次通知可能同时发给多个班。提交时把班级 ID 数组传给后端后端遍历数组写 notice_read 记录。班级和作业管理页面用 el-table 展示数据行内操作按钮注意区分权限。班主任角色只能管理自己班级的学生管理员可以管理所有班级这个数据范围过滤在 Vue 端做不了必须靠后端接口返回的数据。前端拿到数据后不要试图在前端 filter那是自欺欺人一旦有人抓包就能绕过。成绩录入页面我用了 el-table 的可编辑单元格教师按班级选择科目后表格列出班级学生直接在成绩列输入分数。这里有一个交互细节录入成绩后要有个“暂存”和“提交”的区分暂存不调用接口提交时才批量发送。因为教师经常录到一半被其他事情打断如果每条数据都实时提交反而容易出现半行数据。5. 联调、部署与常见问题排查5.1 本地联调的正确姿势三端联调在一台电脑上就能完成。后端先启动记得flask run --host0.0.0.0 --port5000监听所有网卡否则手机访问不到。Vue 后台npm run dev浏览器访问 localhost:5173。小程序开发者工具里把 BASE_URL 配成http://localhost:5000并且勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这样本地开发时能直接访问。真机预览是另一个场景。手机上的小程序不能通过 localhost 访问开发机需要把 BASE_URL 改成开发机的局域网 IP比如http://192.168.1.100:5000。同时确保手机和电脑在同一个 WiFi 下并且 Windows 防火墙允许 5000 端口访问。这一步我经常忘记调防火墙结果手机上一直报网络错误排查半天发现是防火墙拦截。真正上线前微信公众平台要把 request 合法域名配成后端线上地址而且必须是 HTTPS。开发环境和正式环境的 BASE_URL 切换我建议通过小程序的自定义构建环境变量或者一个常量配置文件统一管理不要在每个页面里硬编码地址。5.2 高频报错与解决方案速查表现象常见原因解决方式小程序请求报 errno 600001合法域名未配置或路径不合法开发时勾选不校验域名上线前配置 HTTPS 合法域名接口返回 401token 过期或请求头没带 token检查 axios / wx.request 拦截器重新登录Vue 请求后端跨域报错后端未开启 CORS安装 flask-cors初始化时配置 allow_origins上传图片后页面显示 404存储路径与访问路径不一致上传存相对路径访问时拼接服务器地址页面刷新后内容丢失Vue Router 用了 params 传参改用 query 传参小程序导航栏高度错乱未适配刘海屏动态计算 statusBarHeight menuButton 高度SQLite 数据库被锁多线程并发写开启 WAL 模式或用 gunicorn 单 worker 线程池数据量大时换 MySQLFlask 上传文件中文名乱码文件名编码问题统一用 uuid 重命名存储文件名排查时第一件事不是看代码而是打开开发者工具的 Network 面板和 Vue 的浏览器 Network 面板对照看请求有没有发出去、返回了什么。大多数联调问题都是请求地址不对、参数格式不对、请求头缺失这三类盯住 Network 面板能解决百分之八十的问题。5.3 部署与上线注意事项正式部署建议在一台 2C4G 的云服务器上跑。后端用 gunicorn 启动不推荐 Flask 自带的开发服务器性能和并发都不够。启动命令我常用的参数是gunicorn -w 2 -b 127.0.0.1:5000 app:app两个 worker 足够应付几百个家长同时在线的小型系统。前面用 Nginx 做反向代理/api路径代理到 5000 端口静态文件直接交给 Nginx 处理。之前提到的附件路径问题部署时要统一规划。后端配置 UPLOAD_FOLDER 指向一个数据盘目录比如/data/school/uploadsNginx 配置location /static/ { alias /data/school/uploads/; }。这样上传的文件和代码分离以后更新系统时不会误删数据备份也只备份这一个目录就够了。微信小程序上线要求所有请求走 HTTPSNginx 上需要配置 SSL 证书。证书可以在云服务商免费申请配置完成后用在线工具测一下证书链是否完整。小程序后台把 HTTPS 域名加到 request 合法域名列表里注意不要用 IP 地址必须是备案过的域名。如果后续用户量增长明显SQLite 会先撑不住这时候把数据库平滑迁移到 MySQL。SQLAlchemy 的模型定义基本不用改只需要修改连接字符串把sqlite:///school.db换成 MySQL 的地址重新建表。迁移时注意db.Column的类型兼容性比如 SQLite 里的 Boolean 字段在 MySQL 里要确认映射正确。最后再分享两个小细节我在这类项目上吃过最大的亏不是技术方案选错而是权限边界没想清楚就动手。家校通这种系统一旦上线家长手里就握着孩子的数据和老师的联系方式数据安全比功能丰富重要得多。我的建议是开发前先写一页权限清单什么角色能看什么数据、能操作什么动作后端每个接口必须做数据范围过滤这个习惯会让你的项目少掉一半安全漏洞。另一个实用的经验是所有上传文件一律用 uuid 重命名不要用原始文件名。中文文件名在跨平台存储时经常出问题而且重名文件会互相覆盖。我现在的做法是uuid.uuid4().hex os.path.splitext(filename)[1]这样既保留了文件扩展名又彻底解决了重名和编码问题。你如果正在做类似的家校项目先把这两点做进去后面省下的不止是加班时间还有半夜被家长电话叫醒的惊吓。