Flask+uni-app实战:从零搭建班级事务管理微信小程序
2026/9/24 23:14:31 网站建设 项目流程

最近帮一个朋友做了个班级事务管理系统,顺手把这个项目从零到上线的完整思路和踩坑记录整理了出来。项目本身不算复杂,但涉及的技术栈比较典型:后端用 Python Flask,前端用 uni-app 开发微信小程序,做一款面向班委、同学和辅导员的班级事务协同工具。如果你正好在规划类似的校园工具类小程序,或者想看看 Flask + uni-app 的前后端分离项目实际怎么落地,这篇文章应该能帮你省不少时间。

这个系统解决的痛点很直接:班级日常事务太多,请假要私聊、通知要在群里翻记录、班费收支靠 Excel、活动报名靠接龙,信息散得乱七八糟。真正动手做的时候发现,这些事情完全可以用一个小程序集中管理,让每个角色都有自己的操作入口,数据一旦沉淀下来,后面统计和分析也方便。

1. 系统整体设计与技术选型思路

1.1 需求拆解:班级事务系统到底要管什么

开发之前先把需求理清楚。班级事务听起来笼统,拆开来看其实就几类:通知公告、请假报备、活动报名、班费收支、任务分工。

通知公告最简单,就是班委发、同学看,难点在于已读反馈怎么统计。请假报备涉及审批流程,同学提交、班长或辅导员审批,状态要可跟踪。活动报名需要统计人数,有时还要限制名额、记录报名时间。班费收支要有明细、有分类,期末能出个结算表。任务分工主要是值日表、活动分工这类场景。

角色上分三类:普通同学、班委(含班长、团支书、学委等)、辅导员或管理员。不同角色的权限差异很大,同学只能发起申请和查看自己的事务,班委可以发布通知、审批申请、管理班费,辅导员则要能看到班里所有事务数据。这套权限模型想清楚之后,后面的数据库设计和接口设计就顺了。

1.2 为什么选 Flask + uni-app 这套组合

技术选型纠结过一段时间。当时的候选方案有 Django + 原生小程序、Spring Boot + 原生小程序、Flask + uni-app。

最终定了 Flask + uni-app,理由很实在。Flask 对中小型工具类系统非常友好,本身轻量,扩展机制灵活,加上 SQLAlchemy 做 ORM,开发效率很高。这个系统的业务逻辑不算深,Flask 完全能抗住,没必要上 Django 那样的大而全框架。Spring Boot 和 Java 更是杀鸡用牛刀,而且学习成本和部署成本对个人项目来说都偏高。

前端选 uni-app 的核心原因是代码复用价值。uni-app 基于 Vue 语法,一套代码可以同时编译到微信小程序、H5、App 等多个平台。也就是说,如果我后续想出一个网页版给辅导员在电脑上用,或者打包一个安卓端 APP,这套前端代码基本不用重写,只要在提示适配上下点功夫。这对于一个小团队甚至个人开发者来说,效率优势太明显了。

微信小程序本身有天然的传播优势,学生不用单独安装 APP,微信里一搜就能用,完全符合校园场景的使用习惯。小程序的双端逻辑配合 Flask 的轻量后端,整套方案在开发和维护成本之间取了比较平衡的点。

1.3 技术栈全景与项目结构规划

后端方面,核心组合是 Flask 2.x + Flask-SQLAlchemy + PyMySQL + Flask-CORS。数据库选 MySQL 8.0,因为学院服务器上已有这套环境,用现成的比较稳妥。鉴权这块没有引入复杂的 OAuth 框架,用的是微信小程序登录返回的 code 换取 openid,再配合自签 token 做接口鉴权,后面会详细展开。

前端方面,uni-app 使用 Vue3 语法版本,编译目标设为微信小程序。UI 库选了 uView Plus,在小程序环境里表现稳定,表单组件、弹窗、消息提示这些常用组件都很齐全,省去了自己写一堆基础组件的功夫。

部署架构是一台轻量云服务器,Ubuntu 22.04,上面跑 Nginx 做反向代理,后端进程用 Gunicorn 启动,数据库用 MySQL 8.0。小程序端要求 HTTPS 接口,所以服务器上还配了 SSL 证书。

整体目录结构按前后端分开放:

class-affair-system/ ├── backend/ │ ├── app.py │ ├── config.py │ ├── models.py │ ├── api/ │ │ ├── auth.py │ │ ├── affair.py │ │ ├── notice.py │ │ ├── approve.py │ │ └── user.py │ ├── utils/ │ │ ├── token.py │ │ └── response.py │ └── requirements.txt ├── frontend/ │ ├── pages/ │ │ ├── index/ │ │ ├── publish/ │ │ ├── detail/ │ │ ├── audit/ │ │ └── mine/ │ ├── utils/ │ │ └── request.js │ └── manifest.json └── deploy/ ├── class_system.service ├── nginx.conf └── gunicorn_config.py

后端目录按业务模块拆分成 auth、affair、notice、approve、user 几个蓝图,前端按页面拆目录,整体结构清晰,维护起来不费劲。

2. Flask 后端核心设计与 API 实现

2.1 数据库设计:用几张表撑起一套事务流

数据库是整个系统的地基,这一层设计得不好,后面写接口时处处受限。我用了四张核心表,外加一张用户表。

用户表 user 的字段包含 id、openid、nickname、avatar、real_name、role、class_id、created_at。openid 是微信用户的唯一标识,role 字段存角色标识(1为普通同学、2为班委、3为辅导员),class_id 关联班级表。

班级表 class 很简单,就是 id、class_name、grade、college。

事务表 affair 是系统的核心表,字段包括 id、user_id(发起人)、affair_type(事务类型:请假、活动报名、班费申请、任务分工等)、title、content、attachment_url、class_id、status、created_at、updated_at。status 字段很关键,我用 0 表示草稿、1 表示待审批、2 表示已通过、3 表示已驳回。这个状态字段直接决定整个事务流的分支走向。

审批表 approval 记录每一次审批操作,字段有 id、affair_id、approver_id、action(通过或驳回)、comment、created_at。之所以单独建表而不是在 affair 表里直接存审批结果,是为了保留审批历史。比如一个请假申请被班长驳回后,又修改重新提交,审批记录就能完整呈现整个流转过程,方便后续追溯。

通知表 notice 结构也不复杂,id、title、content、publisher_id、class_id、publish_time、read_count。已读明细单独用 notice_read 表记录,每条通知对应多个已读记录,避免重复统计。

建表时的几个实际考量点:

事务表和通知表都冗余存了 class_id,而不是通过 user 表间接关联。理由是查询列表时可以直接按班级过滤,不用多做一次 join,查询性能在小数据量下差异不明显,但 SQL 写起来简洁很多。

附件字段存的是系统内相对路径,而不是完整 URL。这样迁移服务器时不用改数据库,配合 Nginx 静态映射就能直接访问。

2.2 用户登录与鉴权实现

微信小程序的后端登录流程是固定的套路:前端调用 wx.login 获取临时 code,把 code 传给后端,后端拿 code 去微信接口换取 openid 和 session_key,然后以后端自己的逻辑创建用户会话。

核心代码大致长这样:

@app.route('/api/auth/login', methods=['POST']) def login(): data = request.get_json() code = data.get('code') appid = current_app.config['WX_APPID'] secret = current_app.config['WX_SECRET'] resp = requests.get( 'https://api.weixin.qq.com/sns/jscode2session', params={ 'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code' } ).json() openid = resp.get('openid') if not openid: return jsonify({'code': 1, 'msg': '登录失败,无法获取openid'}), 500 user = User.query.filter_by(openid=openid).first() if not user: default_class = Class.query.first() user = User( openid=openid, nickname='微信用户', role=1, class_id=default_class.id if default_class else None ) db.session.add(user) db.session.commit() token = generate_token(user.id, user.role) return jsonify({ 'code': 0, 'data': { 'token': token, 'user_info': { 'id': user.id, 'nickname': user.nickname, 'avatar': user.avatar, 'role': user.role, 'class_id': user.class_id } } })

这里有几个细节容易踩坑。

微信端换 openid 的接口是https://api.weixin.qq.com/sns/jscode2session,用 requests 直接请求时要注意网络超时设置,毕竟这是后端依赖第三方服务的关键路径,接口超时会导致整个登录流程失败。我在代码里加了超时参数timeout=5,并且在请求失败时返回友好提示。

token 生成用的是 itsdangerous 里的 TimedJSONWebSignatureSerializer,设置 7 天有效期。每次请求都从 Header 里取 Authorization 字段,解析出 user_id 和 role,再用这个用户身份处理请求。这样设计的好处是后端无状态,多个进程共享同一套 token 校验逻辑,部署多个 Gunicorn worker 时不会有会话不一致的问题。

首次登录自动注册是默认行为。用户在小程序里第一次打开就自动创建账号,体验上最顺畅,不需要先填一堆注册表单。缺点是有大量垃圾账号风险,所以我在后续版本里加了完善资料的引导,让用户主动补全真实姓名和头像。

2.3 核心 API 实现:事务发布与审批流转

事务发布是班委和同学都用得最多的接口。以请假申请为例,前端提交事务类型、标题、内容、附件路径,后端做基础校验后插入 affair 表,状态默认为待审批。

@app.route('/api/affair/create', methods=['POST']) @login_required def create_affair(): user_id = g.user_id data = request.get_json() affair_type = data.get('affair_type') title = data.get('title').strip() content = data.get('content').strip() attachment_url = data.get('attachment_url', '') if not title or not content: return jsonify({'code': 1, 'msg': '标题和内容不能为空'}), 400 affair = Affair( user_id=user_id, affair_type=affair_type, title=title, content=content, attachment_url=attachment_url, class_id=g.user.class_id, status=1 ) db.session.add(affair) db.session.commit() if affair_type in ('leave', 'expense', 'activity'): approval = Approval( affair_id=affair.id, approver_id=get_approver_for_class(g.user.class_id), status='pending' ) db.session.add(approval) db.session.commit() return jsonify({'code': 0, 'data': {'id': affair.id}})

审批接口的逻辑核心是更新 affair 表状态,同时在 approval 表写一条审批记录。为了保证这两步的原子性,必须用事务包起来:

@app.route('/api/affair/approve', methods=['POST']) @login_required @app.route('/api/affair/approve', methods=['POST']) def approve_affair(): data = request.get_json() affair_id = data.get('affair_id') action = data.get('action') # 'approve' 或 'reject' comment = data.get('comment', '') affair = Affair.query.get(affair_id) if not affair: return jsonify({'code': 1, 'msg': '事务不存在'}), 404 # 权限校验:只有班委和辅导员可以审批 if g.user.role not in (2, 3): return jsonify({'code': 1, 'msg': '无审批权限'}), 403 try: affair.status = 2 if action == 'approve' else 3 approval = Approval( affair_id=affair.id, approver_id=g.user.id, action=action, comment=comment ) db.session.add(approval) db.session.commit() except Exception as e: db.session.rollback() return jsonify({'code': 1, 'msg': f'审批失败:{str(e)}'}), 500 return jsonify({'code': 0, 'msg': '操作成功'})

审批逻辑里最需要注意的点是权限控制。我在装饰器@login_required基础上,接口内部再做一次角色校验。这样即使未来前端页面入口放出来了,后端也能拦住越权请求。权限这块我吃过亏,早期版本只在前端隐藏审批按钮,结果接口被同学直接调出来发审批请求,好在当时数据量小没出乱子,后来补上了后端校验才放心。

事务列表接口要有分页和状态筛选。我用了一个简单的pagepage_size参数,配合status参数实现筛选。这里有个设计细节需要注意:普通同学只能看到自己发起的申请,班委和辅导员可以看到整个班级的申请。这个逻辑在查询时直接用 where 条件区分,避免把班级数据透传给普通同学。

2.4 文件上传与静态资源处理

班级事务里经常要传附件,比如请假条的截图、活动海报、票据照片等。文件上传在 Flask 里用 request.files 接收,然后保存到服务器指定目录。

@app.route('/api/upload', methods=['POST']) @login_required def upload_file(): file = request.files.get('file') if not file: return jsonify({'code': 1, 'msg': '未接收到文件'}), 400 # 校验文件类型和大小 allowed_ext = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx'} ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else '' if ext not in allowed_ext: return jsonify({'code': 1, 'msg': '不支持的文件类型'}), 400 if file.content_length and file.content_length > 10 * 1024 * 1024: return jsonify({'code': 1, 'msg': '文件大小不能超过10MB'}), 400 filename = f"{uuid.uuid4().hex}.{ext}" upload_dir = current_app.config['UPLOAD_DIR'] file.save(os.path.join(upload_dir, filename)) return jsonify({ 'code': 0, 'data': { 'url': f'/uploads/{filename}' } })

文件名用 uuid 重新生成,而不是直接用用户传递的文件名。这样做的好处是避免文件名冲突,也规避了路径穿越攻击的风险。攻击者如果直接把../../etc/passwd作为文件名传过来,旧版代码可能会把这个路径拼进保存路径导致安全问题。用 uuid 之后,这个风险直接被抹掉了。

静态资源的访问,开发环境直接用 Flask 的 static 路由映射,生产环境则交给 Nginx 处理,后端只负责把文件写到磁盘。

2.5 CORS 与跨域配置:前后端联调头一关

前端跑在微信开发者工具里,后端跑在本机 5000 端口,小程序请求本地接口时会遇到跨域问题。虽然微信小程序不像浏览器那样受同源策略严格限制,但在开发者工具里调试时还是会遇到。后端加一层 Flask-CORS 最省心:

from flask_cors import CORS app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "*"}})

生产环境上,小程序请求的是 HTTPS 域名,由 Nginx 统一反向代理到后端,同时 Nginx 处理好 CORS 头,后端代码里就可以去掉 CORS 配置,减少不必要的暴露面。开发和生产分开处理跨域,是前后端分离项目里的常见做法。

3. uni-app 小程序前端开发细节

3.1 开发工具初始化与 manifest 配置

uni-app 项目我用 HBuilderX 创建,模板选择默认的 uni-ui 模板,Vue3 版本。创建完成后第一件事是配置 manifest.json。

微信小程序相关的配置集中在 manifest.json 的mp-weixin节点里。appid 填自己申请的微信小程序 appid,没有的话先去微信公众平台注册。

{ "mp-weixin": { "appid": "你的小程序appid", "setting": { "urlCheck": false, "es6": true, "minified": true }, "usingComponents": true, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于班级活动定位" } } } }

开发阶段有个小技巧:urlCheck设为 false 可以让开发者工具不校验 request 合法域名,方便直接请求本地 IP 或未备案的测试域名。但注意这只是开发阶段的便利设置,真机预览和发布时必须改回 true,否则会请求失败。

如果需要扫码功能,在 manifest.json 里加上扫码的权限声明,小程序端用uni.scanCode接口唤起扫码。班级场景里扫码常用于活动签到,这个功能上线后效果不错。

首次在微信开发者工具里打开项目时,如果遇到空白页或语法报错,多半是编译缓存问题。工具栏里点"清除缓存并重新编译"基本能解决。

3.2 请求封装与全局状态管理

小程序端的请求层是开发体验的关键。我封装了一个request.js工具模块,统一处理 baseURL、token 注入、响应拦截和错误提示。

// utils/request.js const BASE_URL = 'https://api.你的域名.com' export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else if (res.statusCode === 401) { // token 过期,跳转登录 uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) reject(new Error('登录已过期')) } else { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络连接异常', icon: 'none' }) reject(err) } }) }) }

这个封装里最关键的是 token 统一注入。每个请求都从本地存储里取 token,挂到 Authorization 头里,后端接口通过装饰器统一校验。这样前端每个接口不用单独写 token 逻辑,省了很多重复代码。

用户登录态的管理我用了 Vuex(uni-app 内置支持)。登录成功后把用户信息和 token 同时写入 storage 和 Vuex store,页面里直接访问 store 中的用户信息,避免每个页面都从 storage 读。

页面间传参方面,小程序的页面栈机制决定了页面跳转用uni.navigateTo并带 id 参数,目标页面的onLoad(options)里拿参数后,再调用接口加载详情。这个流程是固定的,没什么坑,但要注意参数长度限制,复杂对象最好序列化后存到全局变量或缓存里。

3.3 页面设计与交互实现

首页是整个小程序的门面,也是用户进来第一个看到的东西,我设计成"事务列表 + 分类 tab"的布局。顶部是四个 tab:全部、请假、活动、班费,下面跟着时间倒序的事务列表。

列表用的组件是 uni-app 内置的 scroll-view,配合 onReachBottom 做触底加载更多。分页参数维护在页面的 data 里,每次加载成功页码加一。这个分页交互实现起来不复杂,但要注意防重复加载:在请求中状态加一个锁,避免触底事件连续触发导致重复请求。

发布页是操作最频繁的页面。表单包含事务类型选择、标题输入、内容文本域、附件上传四个部分。表单校验用 uni-app 内置的 uni-forms 组件,配置 rules 规则后,前端就能做基础校验,减少无效请求。

附件上传部分,用 uni.chooseImage 选择图片,然后调用 uni.uploadFile 上传到后端:

uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => { const tempFilePath = res.tempFilePaths[0] uni.uploadFile({ url: BASE_URL + '/api/upload', filePath: tempFilePath, name: 'file', header: { 'Authorization': uni.getStorageSync('token') }, success: (uploadRes) => { const data = JSON.parse(uploadRes.data) if (data.code === 0) { // 保存返回的路径到表单数据 } } }) } })

这里容易踩的坑是 uni.uploadFile 返回的 data 是字符串,不是对象,必须先 JSON.parse 才能取到 url。另外,小程序真机上sourceType里的 camera 才可以调起摄像头,这是 H5 和小程序的差异点。

审批页是班委使用频率最高的页面。事务详情展示申请人的基本信息、事务内容、附件预览和审批历史。底部是审批操作区,通过和驳回两个按钮,驳回时可以填写审批意见。整个页面核心就是一个事务详情接口 + 一个审批接口,交互上注意加个 loading 状态,防止用户重复点击提交。

顶部导航栏在小程序里有个细节,就是不同机型状态栏高度不一样。为了避免自定导航时内容顶到状态栏,我用了一个公共方法来获取状态栏高度:

export function getStatusBarHeight() { return new Promise((resolve) => { uni.getSystemInfo({ success: (res) => { resolve(res.statusBarHeight || 44) } }) }) }

然后在页面 mounted 时设置占位 view 的高度,这样自定义导航就能适配所有机型,不再出现 iPhone 上顶着刘海的问题。

3.4 微信小程序端的差异化处理

uni-app 说是多端复用,但实际开发中还是有不少小程序特有的坑。

域名校验是第一个坎。微信小程序要求所有 request 请求的 URL 都必须在小程序后台配置为合法域名,否则真机上报错。开发阶段可以关掉 urlCheck,但上线前必须把线上域名配好。

图片资源也有域名校验。<image>组件的 src 如果指向未配置的 downloadFile 合法域名,图片加载不出来。所以我在 upload 接口返回的路径基础上,请求时拼上完整的域名,同时在小程序后台把downloadFile 合法域名配置好。

分享功能在小程序里比较特殊。微信小程序默认没有分享按钮,需要显式调用uni.showShareMenu或配置页面onShareAppMessage生命周期。我做了个班级活动分享功能,同学可以分享活动详情页到班级群,通过这个实现了一波简单的裂变传播。

小程序的生命周期和 H5 有差异,onShow在每次从后台切换到前台时都会触发,这个时间点适合做数据刷新。我在详情页的 onShow 里重新拉取列表数据,保证从详情页返回列表时数据是最新的,体验上比用户手动下拉刷新顺滑。

4. 部署上线与小程序发布流程

4.1 Flask 后端部署到云服务器

后端部署看着简单,但真到了服务器上,环境配置能卡住不少人。我这里用的是 Ubuntu 22.04 云服务器,按步骤走完整套流程。

先装基础环境:

sudo apt update sudo apt install -y python3-pip python3-venv nginx mysql-server

项目代码拉取到服务器,在项目目录下创建虚拟环境并安装依赖:

cd /var/www/class_system python3 -m venv venv source venv/bin/activate pip install -r requirements.txt

requirements.txt 里最关键的是这几个包:

Flask==2.3.3 Flask-SQLAlchemy==3.1.1 PyMySQL==1.1.0 Flask-Cors==4.0.1 gunicorn==21.2.0 itsdangerous==2.1.2 requests==2.31.0

这里有个坑,PyMySQL 需要配套安装 cryptography,不然连 MySQL 时会报认证错误。用 pip 安装 PyMySQL 之前先确认 cryptography 有没有装,不行就一起装:

pip install PyMySQL cryptography

数据库方面,先创建 MySQL 库和账号:

CREATE DATABASE class_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'class_user'@'localhost' IDENTIFIED BY '你的密码'; GRANT ALL PRIVILEGES ON class_system.* TO 'class_user'@'localhost'; FLUSH PRIVILEGES;

然后让 Flask 连上数据库。config.py 里配置 SQLALCHEMY_DATABASE_URI,注意 MySQL 8.0 默认认证插件是 caching_sha2_password,老版本 PyMySQL 连不上,要么升级 PyMySQL 到 1.x,要么在 MySQL 里把对应用户的认证方式改成 mysql_native_password。

4.2 Gunicorn + Nginx 配置

Flask 自带的开发服务器不适合生产环境,直接用 Gunicorn 启动。创建一个 gunicorn_config.py:

bind = "127.0.0.1:5000" workers = 3 timeout = 60 accesslog = "/var/log/class_system/access.log" errorlog = "/var/log/class_system/error.log"

这里 workers 设置为 3,是考虑到服务器配置只有 2C4G,开太多 worker 反而会抢占资源。如果服务器性能更好,可以按 CPU 核数加。

启动命令:

gunicorn -c gunicorn_config.py app:app

为了让它常驻后台且开机自启,我配了一个 systemd 服务。在/etc/systemd/system/class_system.service里写:

[Unit] Description=Class System Flask App After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/var/www/class_system ExecStart=/var/www/class_system/venv/bin/gunicorn -c /var/www/class_system/gunicorn_config.py app:app Restart=always [Install] WantedBy=multi-user.target

然后执行:

sudo systemctl daemon-reload sudo systemctl enable class_system sudo systemctl start class_system

Nginx 配置反向代理。因为是给小程序用的接口,需要配 HTTPS。这里以域名api.你的域名.com为例:

server { listen 443 ssl; server_name api.你的域名.com; ssl_certificate /etc/nginx/ssl/你的证书.crt; ssl_certificate_key /etc/nginx/ssl/你的证书.key; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态上传文件直接由 Nginx 服务 location /uploads/ { alias /var/www/class_system/uploads/; } }

注意这个location /uploads/配置非常关键。如果不做这个映射,上传的附件会走 Flask 接口返回,既慢又占后端资源。

微信小程序强制要求接口必须是 HTTPS,证书我用的免费版,申请和部署整个流程大概花了一个小时。证书有效期为三个月,记得在到期前自动续期,不然小程序突然调不了接口,排查半天才发现是证书过期了。

4.3 微信小程序注册、提审与发布

小程序前端开发完成后,HBuilderX 里点"发行 -> 小程序-微信",就会生成编译后的微信小程序代码,目录在unpackage/dist/dev/mp-weixin。然后用微信开发者工具打开这个目录上传版本。

提审之前有几件事必须核对清楚。第一,manifest.json 里的 appid 要和微信公众平台的 appid 保持一致。第二,服务器域名在小程序后台配置好:request 合法域名和 downloadFile 合法域名都要加。第三,小程序分类要选对,涉及校园教育类的要选"教育"类目,如果涉及请假等个人事务数据,还需要配置隐私保护指引。

在微信公众平台里,开发管理 -> 开发设置 -> 服务器域名,把线上 API 域名添加进去。这里有个细节,小程序域名要求备案且是 HTTPS,域名还不能带端口号。

隐私协议这块容易被忽略。小程序后台要求填写用户隐私保护指引,包括收集哪些用户信息、用途说明等。我们收集了微信昵称和头像作为用户资料,还可能需要位置信息(活动签到场景),这些都要在隐私指引里写明。如果不填,小程序审核时会直接被拒。

提交审核前最好先在体验版上完整测一遍核心流程:登录、发布事务、审批、上传附件、查看列表。体验版和正式版逻辑完全一样,只是访问入口不同。我习惯在体验版阶段拉上几个班委同学一起测试,他们能发现很多我开发者视角下看不到的问题。

审核通过后点击"发布",小程序就正式上线了。微信审核时间一般在 1-7 天,教育类目审核还算顺利,第一次提审两天不到就过了。

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

5.1 开发阶段高频问题速查表

问题可能原因解决方案
Flask 接口返回中文乱码未设置 JSON 编码Flask 返回 JSON 时设置app.config['JSON_AS_ASCII'] = False
小程序请求提示 URL 不在合法域名列表后台未配置 request 合法域名微信公众平台配置服务器域名,开发工具临时关掉 urlCheck
uni-app 真机预览连不上本地接口手机和电脑不在同一局域网后端启动时绑定0.0.0.0,手机和电脑连同一个 Wi-Fi
上传附件后前端拿不到图片路径没拼完整域名后端返回相对路径,前端拼接 BASE_URL 后再渲染
清理了微信开发者工具缓存还是空白编译目标配置问题在 manifest.json 中检查 vue 版本和编译配置

跨域问题在开发时也很常见。我早期在本地调试时,小程序开发者工具请求http://localhost:5000/api/affair/list一直被 CORS 拦截,后来确认是 Flask 端没加 CORS 头。加了 Flask-CORS 扩展后,问题直接就解决了。

小程序图片上传还有一个常见坑:上传到服务器后图片不能立即显示,尤其在 H5 端。原因是后端返回的 URL 缺少域名前缀,小程序端 image 组件没法解析这个相对路径。解决方式是在响应层统一处理,把相对路径转成完整 URL。

5.2 生产环境真实踩坑记录

第一个印象深刻的坑是附件路径错误。部署到服务器后,发现上传的附件总是 404。排查了半天,最后发现是 Flask 的UPLOAD_DIR配置的是相对于项目目录的路径,但 systemd 启动的 WorkingDirectory 指向的目录不同,导致上传文件写到了其他位置,而 Nginx 的 alias 映射目录又不对。

解决方案是把上传路径和 Nginx 映射路径都改成绝对路径,配置文件里写死,避免任何相对路径解析带来的不确定性。顺手在代码里加了启动时自动创建 uploads 目录的逻辑,防止目录不存在时报错。

第二个坑是数据库连接池断连。系统跑了一段时间后,凌晨就会出现接口 500 错误,报错是Lost connection to MySQL server during query。原因是 MySQL 的 wait_timeout 默认是 8 小时,长时间没请求后连接被断开,SQLAlchemy 的连接池还持有旧连接。

解决方式很简单,SQLAlchemy 连接池加两个参数:pool_pre_ping=Truepool_recycle=3600pool_pre_ping每次取连接时先 ping 一下,连接断了就重新建立;pool_recycle强制一小时回收一次连接,防止 MySQL 主动断开。

第三个容易忽略的是 token 过期后的无感重登设计。学生用户可能隔了一周再打开小程序,token 已过期,直接跳登录页重登体验非常差。我后面改成了在请求层拦截 401,先静默调一次uni.login获取新 code,再走一遍 login 接口自动刷新 token,刷新失败才跳转登录页。

5.3 排查工具与方法

排查问题要有章法,盲猜只会浪费时间。我的调试武器库主要这几样。

Flask 端,先看日志。systemd 服务日志用journalctl -u class_system -f实时查看,能看到 Python 完整报错堆栈。开发环境把app.debug = True打开,出错时浏览器会直接显示详细堆栈信息。SQLAlchemy 还能配置输出 SQL 日志,排查数据库层问题时很管用。

小程序端,调试利器是微信开发者工具的 vConsole,可以看到小程序端所有网络请求、控制台日志和本地缓存内容。真机预览时,开启调试模式也会出现 vConsole,同样可以看完整日志。页面逻辑问题通过打断点观察 data 里的数据变化最直观。

接口排查用 curl 最快。部署后可以用类似命令测试线上接口:

curl -X POST https://api.你的域名.com/api/affair/list \ -H "Content-Type: application/json" \ -H "Authorization: 你的token" \ -d '{"page": 1, "page_size": 10, "status": 1}'

这样能区分问题是出在前端还是后端。前端报错但 curl 请求返回正常,那就是前端代码逻辑问题;curl 也返回 500,那就专心排查后端。

优化建议里有一个简单有效的:数据库加索引。affair 表的 status 和 class_id 字段经常作为查询条件,privacy 和权限维度过滤很多,加索引后接口响应速度明显提升。还有 user 表的 openid 字段也应该有唯一索引,保证一个 openid 对应一个用户。

6. 一些个人体会和小建议

做完整个项目回头复盘,最想给大家提的几条经验是心态和方法上的。

第一,第一版不要贪大求全。一开始我列了一堆功能:班级圈、投票、问卷、签到地图、学期报告。如果真按这个范围做,项目周期至少翻两倍。后来砍到只剩通知、事务、审批、个人中心四个主模块,整个系统从开发到上线大概用了三周时间,核心流程跑通之后,用户反馈和真实需求才逐渐浮现出来,后续再基于反馈加功能,方向比闭门造车准确得多。

第二,先跑通主流程再回头补细节。我开发时的顺序是登录 -> 发布事务 -> 审批流转 -> 列表展示,这条链路通了以后,通知模块和班费模块就是加几张表、几个接口的事,成本很低。如果一上来就纠结某个页面样式调得美不美,主流程迟迟没跑通,项目容易陷入无底洞。

第三,和真实用户一起测。班级事务管理系统这种校园工具类项目,最真实的反馈来自同学。体验版阶段我拉了班里几个同学直接用,他们在使用过程中提出了很多我完全没想到的需求,比如希望申请被驳回时能收到通知、希望班费账单能导出 Excel、希望活动报名后能在详情页看到自己是否已报名。这些建议让系统迭代方向更贴近实际使用场景。

后续如果继续迭代这个项目,我会考虑加两个方向。一个是数据可视化,把班级事务按类型、时间维度做成统计分析页面,让辅导员在期末时能直观看到学期情况;另一个是消息推送,利用小程序的订阅消息功能,实现审批状态变更、活动提醒的主动触达,把班委从"每天追着问进度"的状态里解放出来。

最后说一个很重要的点,技术选型和架构设计只是为了解决实际问题服务,不是越复杂越好。这个系统用 Flask + uni-app 的组合,是因为它匹配当前场景的复杂度,学习成本低、开发效率高、部署方便。如果你的项目比这个大得多,团队协作复杂,那可能需要微服务或者更重量级的框架,但这套轻量方案在校园工具类场景里已经足够合格。希望这篇从需求梳理到上线排坑的完整记录,能帮你少走一些弯路。

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

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

立即咨询