高校学科竞赛报名系统开发实战:Flask+uniapp微信小程序完整实现
发布时间:2026/9/26 7:10:12来源:尧图网络
高校学科竞赛报名这件事在校期间我作为参赛选手被折腾过后来在教务处帮过忙再后来直接接手开发这个管理系统。在很多高校里学科竞赛的报名、审核、作品提交、成绩录入还停留在“Excel接力赛”的阶段学生填表发给老师老师汇总成一个大表再挨个核对格式还要防着漏交、重复提交、命名不规范。这套流程能跑但很痛苦而且一到比赛高峰期各种信息错乱是家常便饭。我当时接到这个需求——做一个面向高校学科竞赛的参赛申请管理系统技术栈定的是Python Flask uniapp 微信小程序。后端用Flask提供REST API前端通过uniapp编译成微信小程序覆盖学生报名、指导老师审核、管理员配置竞赛、作品上传、成绩公布几个核心闭环。这篇文章就围绕这个项目聊聊我从零搭建这套系统的完整思路、核心细节、实现过程以及真正跑起来之后踩过的那些坑。1. 项目整体设计与思路拆解1.1 场景痛点与核心需求解析做系统之前我先梳理了一下用户到底是谁、他们想要什么。这个系统里有三类核心角色学生需要在线填写报名信息、选择竞赛项目、上传报名材料、提交参赛作品、查看审核状态和最终成绩。他们的核心诉求是“别让我跑来跑去交纸质表”。指导老师/学院负责人需要审核学生提交的报名数据和作品填写推荐意见确认参赛资格。管理员校赛/院系管理员维护竞赛目录、设置报名时间窗口、分组评审、录入成绩、导出统计报表。除了角色业务上还有几个硬性要求同一个学生同一场竞赛只能报一次名防止重复超过截止时间不能再提交一个团队赛允许多人组队队长统一提交作品上传有大小限制审核流转的状态要清晰可追溯。这个需求的本质其实就是一个带状态流转的表单收集与审核系统。概念的抽象很重要因为这决定了你选型时是找现成的低代码平台还是自己从零写。这次选择从零开发核心原因有两个一是学校网络环境相对封闭第三方SaaS平台数据出校不便、政策上有顾虑二是业务规则往后肯定要加比如按学院限额、成员资格校验、评审分组等自研系统扩展起来更主动。1.2 技术选型为什么是Flask uniapp这套组合不是随便拍的每个组件都有它的理由。先看后端。Flask在Python后端框架里属于“小而灵活”的路线。相比Django的全家桶风格Flask只保留路由、请求上下文、模板渲染这些最小核心数据库、表单、认证全部交给扩展组件自己拼装。我这次用的是Flask Flask-SQLAlchemy Flask-Migrate PyMySQL Flask-CORS数据库选MySQL。这个组合的好处是一旦项目跑起来每一层你都能完全掌控排错也不用在框架的复杂机制里绕来绕去。关键是Flask的灵活性能让我们把接口设计得和业务一一对应不会出现“框架帮你做了太多事反而绑手绑脚”的尴尬局面。再往前看为什么不用Node.js或者Java选择Python另一个现实因素是团队里做数据处理和模型训练的同事都用Python让后端和后续的数据分析能力比如评奖统计、作品查重共用一套语言生态沟通成本最低。如果你是一个人单干Python入门曲线也相对平缓遇到问题社区资料极其丰富。再看前端。uniapp是Vue语法衍生出来的跨端框架一套代码可以编译到微信小程序、App、H5等多个平台。这次目标是微信小程序所以uniapp是我觉得最合适的方案。直接用微信原生小程序开发也可以但对团队里不熟悉小程序的成员来说学习成本高一点而且代码没法复用到后续可能的App版本。uniapp保留了Vue单文件组件的开发习惯同时能调用微信小程序的API比如uni.login获取登录凭证、uni.uploadFile上传文件、uni.request发起请求过渡是很平滑的。这里要特别提一下热搜里那句“uniapp封装H5如何指向2个域名”。我们在实践中确实遇到过类似的场景同一个跨端包在微信小程序里走的是后端A的域名而在H5里因为要跨域后端地址是另一个网关。处理方式很简单在uniapp项目根目录的config.js里配置两套环境变量// config.js export const ENV { // 小程序编译环境 MP: { BASE_URL: https://api.school.edu.cn }, // H5编译环境 H5: { BASE_URL: https://h5api.school.edu.cn } }然后根据编译平台动态读取// request.js import { ENV } from /config.js // #ifdef MP-WEIXIN const BASE_URL ENV.MP.BASE_URL // #endif // #ifdef H5 const BASE_URL ENV.H5.BASE_URL // #endif#ifdef是uniapp的条件编译注释编译成小程序时只保留小程序段编译成H5时只保留H5段实际测试下来非常稳这也算是uniapp替代多套原生代码的一个显著优势。1.3 系统架构与数据流转设计系统整体分三层前端小程序层、Flask API层、MySQL数据层。小程序层负责一切用户交互报名表单、竞赛列表、审核进度、作品上传。它不直接读写数据库所有操作都通过HTTP请求调用后端的REST API。Flask API层是系统的中枢承担身份认证、业务逻辑、文件存储、数据校验。MySQL数据层存储用户、竞赛、报名单、作品文件路径和成绩记录等结构化数据。数据流大概是这样的学生打开小程序微信授权登录后端通过微信的code2session接口换取openid并生成自定义token返回前端。学生浏览竞赛列表选择某一项赛事点击“我要报名”前端向后端提交报名表单数据。Flask校验报名时间窗口是否开放、该用户是否已经报名校验通过后写入报名表并生成一个报名单号。学生上传作品文件Flask接收文件后保存到服务器指定目录这里存本地磁盘有条件可以上OSS并把文件路径写入数据库。指导老师登录后查看待审核列表点击“通过”或“驳回”Flask更新状态字段并记录操作时间。管理员在后台管理系统配置竞赛信息学生端小程序实时同步可见。整套系统的核心就是这张报名表的状态机草稿 → 已提交 → 指导老师审核中 → 已通过/已驳回 → 作品已提交 → 评审中 → 已出成绩数据库里用status字段标识当前状态每个状态的迁移都对应一组后端接口的幂等操作。这个设计让整个业务线索非常清晰后续做报表统计、状态追溯都很方便。2. 核心细节解析与实操要点2.1 Flask后端的数据模型设计数据库表设计是决定这个项目后期好不好扩展的关键。我建了6张核心表user用户、competition竞赛、entry报名单、entry_member团队成员、work作品、review_record审核记录。先说user表。这张表不只是存用户名密码由于对接了微信小程序登录还需要存openid、unionid、昵称、头像、学号、学院、专业、年级、角色。这里的角色我用的是role字段取值范围是student、teacher、admin基于整型值做存储性能更好但直接用字符串可读性强项目规模不大优先选可读性。class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) openid db.Column(db.String(64), uniqueTrue, indexTrue) nickname db.Column(db.String(64)) avatar db.Column(db.String(255)) student_id db.Column(db.String(32)) college db.Column(db.String(64)) major db.Column(db.String(64)) grade db.Column(db.String(16)) role db.Column(db.String(16), defaultstudent) created_at db.Column(db.DateTime, defaultdatetime.now)competition表存竞赛的基本信息、报名开始时间、截止时间、竞赛等级国家级/省级/校级、允许的最大团队人数、竞赛说明文档链接。这里有一个非常容易踩坑的点时间字段必须统一用DateTime类型并且所有的时间比较都放在后端做不要信任前端传来的时间。前端传过来的时间是用户本地的和服务器时间有偏差一旦有人把手机时间调快就有了钻空子的机会。entry表表示一次报名。字段包括competition_id、leader_id、team_name、status、teacher_id指导老师、submitted_at等。entry_member表则是团队成员关联表记录哪些user在哪个报名单里。work表存作品相关信息。作品文件路径file_path、文件大小file_size、作品名称work_name、提交时间submit_time、版本号version。为什么要版本号因为学生很可能需要覆盖提交作品尤其是在截止日前反复修改。一次报名对应多个作品版本取最新一版用于评审这样既保留轨迹又不会让评审拿到错误版本。review_record表做审核流水。字段包括entry_id、reviewer_id、action通过/驳回、comment意见、created_at。这张表在出现纠纷的时候作用巨大谁审核的、什么时候审核的、写了什么意见全部留痕。高校场景下这种审计能力是刚需。2.2 微信小程序登录与Flask侧身份认证微信小程序登录是整套系统的门外汉第一道关。流程上非常标准小程序端调用uni.login拿到临时code前端把code发给后端后端拿着code去微信接口服务换openid和session_key拿到openid后在数据库里查用户是否存在不存在就自动注册一个随后生成自己的登录凭证返回给小程序端。这里有两个容易被忽略的细节。第一wx.login返回的code有效期只有5分钟且只能用一次。所以后端接口拿到code后必须立即调用微信接口不要做任何耗时的中间操作。我一开始图省事在微信换openid前先往本地日志表里写了一行记录结果在高并发下偶尔会出现“code无效”的错误排查了半天定位到这个问题。日志写操作虽然只有几十毫秒但在密集报名时段仍然可能挤压code有效期窗口。第二不要用自己的token去替代微信登录态而完全抛弃session_key。登录成功之后后端可以生成一个随机token返回前端后续请求通过Authorization头携带。这个token在服务端映射到用户ID。session_key则留着解密手机号和微信运动等敏感数据用短期内不需要。最终实现放在一个装饰器里统一做身份校验from functools import wraps import jwt def login_required(f): wraps(f) def decorated_function(*args, **kwargs): auth_header request.headers.get(Authorization) if not auth_header: return api_response(code401, message未登录或登录已过期) try: payload jwt.decode(auth_header, SECRET_KEY, algorithms[HS256]) current_user User.query.get(payload[user_id]) if not current_user: return api_response(code401, message用户不存在) request.current_user current_user except jwt.ExpiredSignatureError: return api_response(code401, message登录已过期) except jwt.InvalidTokenError: return api_response(code401, message无效的登录凭证) return f(*args, **kwargs) return decorated_function登录态有效期我设的是7天配合请求拦截器实现每分钟自动续期。小程序端一旦收到后端401响应统一跳转到登录页重新授权这个过程用户无感体验流畅。2.3 报名与作品上传的接口设计接口设计遵循RESTful风格POST表示创建PUT/PATCH表示更新GET表示查询。报名模块的核心接口是POST /api/entry/create创建报名单POST /api/entry/id/submit提交报名状态从草稿→已提交POST /api/entry/id/upload上传作品GET /api/entry/id查询报名详情GET /api/entry/my/list我参与的报名列表创建报名单的参数里除了基本信息还会传is_team字段区分个人赛和团队赛。如果是团队赛后续会调用成员邀请接口来增删队友。这里有个业务约束个人不能同时参与同一竞赛的两支队伍。所以里创建一个逻辑创建报名单时检查一下数据库如果当前用户在同一个competition_id下已存在status ! cancelled的记录就拒绝创建。上传作品是重头戏接口用uni.uploadFile配合Flask的request.files接收文件。核心校验有四个维度文件扩展名白名单、文件大小上限我设为200MB可配置、文件类型检查不只是扩展名还读文件头防止有人改了扩展名传不可执行文件、以及并发覆盖控制。ALLOWED_EXTENSIONS {zip, rar, pdf, doc, docx, ppt, pptx, mp4, jpg, png} def allowed_file(filename): return . in filename and filename.rsplit(., 1)[1].lower() in ALLOWED_EXTENSIONS app.route(/api/entry/int:entry_id/upload, methods[POST]) login_required def upload_work(entry_id): entry Entry.query.get_or_404(entry_id) if entry.status ! approved: return api_response(code403, message当前状态不允许上传作品) file request.files.get(file) if not file: return api_response(code400, message未接收到文件) if not allowed_file(file.filename): return api_response(code400, message不支持的文件类型) # 实际文件类型校验 ... file_size 0 # 流式写入服务器磁盘 ...文件命名这一条特别注意永远不要用前端传来的原始文件名直接落盘会有两个问题一个是中文名或特殊字符可能导致服务器存储异常另一个是同名文件相互覆盖。我的做法是在后端生成一个UUID 时间戳的新文件名把原始文件名记录到数据库字段里下载的时候再响应给客户端。2.4 uniapp小程序的页面结构与状态管理小程序端页面结构按角色划分学生端首页展示竞赛列表Banner轮播、倒计时组件、报名入口我的报名列表、状态标签、详情个人信息头像、学号、学院。报名流程页竞赛详情 → 填写表单 → 添加成员 → 提交成功。表单用uni-forms做校验比如手机号11位、学号8位、邮箱格式。作品上传页选择文件、上传进度条、上传成功/失败状态、历史版本列表。管理端教师/管理员审核列表、报名详情、审核通过/驳回按钮、成绩录入弹窗、统计报表页。页面之间传参用uni.navigateTo的url带参数但有一个红线不要在URL里传大段对象或者用户敏感数据。正确的姿势是URL只传ID页面加载后再通过uni.request从后端拉详情。这既是安全考虑也能避免页面回退时数据陈旧。状态管理这一层我用Vuex配合uni.setStorageSync做了一个简单的持久化token管理。具体实现是登录成功后把token和用户基本信息写入Vuex同时存一份到Storage。每次uni.request的请求头动态绑定token。一个关键点是token失效的全局处理在响应拦截器里判断HTTP状态码如果是401调用一个全局的logout方法清掉本地缓存并跳转登录页避免用户看到一堆报错弹窗。// request.js 拦截器 export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: uni.getStorageSync(token), Content-Type: application/json }, success: (res) { if (res.data.code 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) return } resolve(res.data) }, fail: (err) reject(err) }) }) }2.5 审核流程与状态机实现审核流程是整个系统业务逻辑上最容易出错的部分因为它涉及状态的并发修改。我在实现时把状态机的所有合法迁移定义成了一个字典ALLOWED_TRANSITIONS { draft: [submitted], submitted: [teacher_reviewing], teacher_reviewing: [approved, rejected], approved: [work_uploaded, cancelled], work_uploaded: [under_review, work_uploaded], # 允许覆盖上传状态不变 rejected: [submitted], # 驳回后允许重新提交 under_review: [final_approved, final_rejected], final_approved: [score_published], }每次状态变更先校验是否在ALLOWED_TRANSITIONS里不在就返回错误。这一步看似繁琐实际上是防呆的关键。有一次我临时为了赶进度在审核通过接口里直接改了entry.status approved忘了走统一的状态变更函数结果负责驳回的同事那边状态永远不同步后来花了几个小时排查。自从引入状态机校验这类问题基本绝迹。还有一个细节学生提交作品后状态应该由谁来推进我的设计是学生上传作品后状态从approved变成work_uploaded等到管理员点击“开始评审”批量把该竞赛下所有work_uploaded的记录变成under_review。评审完成后管理员在后台录入每个学生或队伍的成绩系统自动将状态改为score_published学生端可见成绩。这里引入了一个被热搜词提及的操作——“微信小程序设置缓存时间”。我们处理的方式是前端拉取竞赛列表时设置一个5分钟的缓存减少后端压力但是审核状态、成绩这些关键数据不走缓存永远实时拉取。否则学生会看到审核通过了但前端还显示“待审核”的诡异状态那是很尴尬的。3. 实操过程与核心环节实现3.1 从零初始化Flask项目新项目不用flask-bootstrap之类的脚手架我习惯手动搭一个目录结构好处是每一层都清楚出了问题定位快。project/ ├── app/ │ ├── __init__.py # 初始化Flask应用、注册蓝图 │ ├── config.py # 配置数据库、文件目录、密钥 │ ├── models/ # SQLAlchemy模型 │ ├── api/ # API蓝图 │ │ ├── auth.py │ │ ├── competition.py │ │ ├── entry.py │ │ ├── work.py │ │ └── admin.py │ ├── utils/ # 装饰器、文件校验、微信工具 │ └── services/ # 核心业务逻辑层 ├── uploads/ # 作品文件存储目录 ├── requirements.txt └── run.py用工厂函数模式初始化Flask应用def create_app(): app Flask(__name__) app.config.from_object(Config) db.init_app(app) migrate.init_app(app, db) CORS(app, supports_credentialsTrue) from app.api.auth import auth_bp from app.api.competition import competition_bp from app.api.entry import entry_bp from app.api.work import work_bp from app.api.admin import admin_bp app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(competition_bp, url_prefix/api/competition) app.register_blueprint(entry_bp, url_prefix/api/entry) app.register_blueprint(work_bp, url_prefix/api/work) app.register_blueprint(admin_bp, url_prefix/api/admin) return app蓝图的划分是为了让路由管理不混乱。系统功能变多以后最怕的是路由都堆在一个文件里找接口要翻几百行排查效率极低。3.2 开发环境配置与依赖清单requirements.txt我盯了一个固定清单Flask2.2.5 Flask-SQLAlchemy3.0.5 Flask-Migrate4.0.4 PyMySQL1.0.2 Flask-CORS4.0.0 PyJWT2.8.0 requests2.31.0 python-dotenv1.0.0 gunicorn20.1.0版本一定要锁死。不止一次踩过“本地好端端的服务器上装成另一个大版本直接跑崩”的坑尤其是Flask-SQLAlchemy和SQLAlchemy的2.x大版本变动API差异会直接导致ORM查询风格报错。本地开发跑起来很简单python -m venv venv source venv/bin/activate # Windows是venv\Scripts\activate pip install -r requirements.txt flask --app run.py db init flask --app run.py db migrate -m initial migration flask --app run.py db upgrade python run.py数据库连接串配置放.env文件因为里面有账号密码绝不能提交到Git仓库DATABASE_URLmysqlpymysql://root:your_passwordlocalhost:3306/competition_db?charsetutf8mb4 SECRET_KEYyour-secret-key WX_APPIDyour-wx-appid WX_SECRETyour-wx-secret这里有个不得不提的细节连接串必须加charsetutf8mb4否则用户昵称里带个Emoji表情直接写入报错。UTF-8在很多MySQL版本里实际上是utf8mb3存不了四字节Emoji这个问题极其隐蔽用户输入一个表情就会触发数据库异常。3.3 微信小程序前端开发从HBuilderX到真机预览前端这一侧我用的开发工具是HBuilderX熟练的可以直接用Vue CLI创建uniapp项目但对多数人来说HBuilderX的集成体验更省心。新建项目选择“uni-app”模板然后引入项目所需的页面、组件和工具函数。页面结构大概是pages/ ├── index/index.vue # 首页竞赛列表 ├── competition/detail.vue # 竞赛详情 ├── entry/form.vue # 报名表单 ├── entry/myList.vue # 我的报名 ├── work/upload.vue # 作品上传 ├── admin/reviewList.vue # 审核列表 ├── admin/reviewDetail.vue # 审核详情 ├── admin/scoreInput.vue # 成绩录入 └── login/login.vue # 登录页报名表单是最容易让前端写崩的页面。字段多、校验规则多、提交反馈要即时。我使用的是uni-forms配合rules校验对象const rules { studentName: { rules: [{ required: true, errorMessage: 请输入姓名 }] }, studentId: { rules: [{ required: true, errorMessage: 请输入学号 }, { pattern: /^\d{8,12}$/, errorMessage: 学号格式不正确 }] }, phone: { rules: [{ required: true, errorMessage: 请输入手机号 }, { pattern: /^1[3-9]\d{9}$/, errorMessage: 手机号格式不正确 }] }, email: { rules: [{ required: true, errorMessage: 请输入邮箱 }, { format: email, errorMessage: 邮箱格式不正确 }] }, college: { rules: [{ required: true, errorMessage: 请选择学院 }] }, teacher: { rules: [{ required: true, errorMessage: 请输入指导老师姓名 }] } }每次用户提交先走前端uniForms.validate()通过后再发起请求。前端校验不能替代后端校验但能省去大量无效请求用户也不用等服务器往返就能发现填错。小程序真机预览必须把“微信开发者工具”的本地服务端口打开在HBuilderX里点“运行到微信开发者工具”。这一步卡过我不少时间还有个小坑用HBuilderX创建的uniapp项目默认结构能正常编译但如果你手动改了项目路径或者换了电脑一定要在“manifest.json - 小程序配置 - 微信小程序AppID”里确认填的是自己的AppID而不是测试号。用测试号会导致无法调用部分需要企业认证的API比如获取手机号。3.4 数据表迁移与初始化数据数据库迁移用Flask-Migrate核心是处理好“模型改动的版本记录”。每次修改模型文件后执行flask --app run.py db migrate -m add work version field flask --app run.py db upgrademigrate会自动对比模型和数据库的差异生成迁移脚本。这里有个提示迁移脚本生成后一定要人工过一遍尤其是字段重命名或删除时自动脚本可能把数据弄丢。我们有一次给entry表加外键自动生成的迁移脚本里先删表再重建差点把已经录入的几十条测试数据冲掉改成手动编写的ALTER TABLE才保住数据。初始化数据的话我在系统部署时写了一个seed.py脚本用来插入管理员账号、默认学院列表、常见竞赛项目如“全国大学生数学建模竞赛”“挑战杯”等。这套种子数据在后端初始化时跑一次可以节省很多手工录入时间。3.5 文件上传大文件分块与断点续传方案作品文件动辄几十MB甚至上百MB比如数学建模的论文PDF、程序设计赛的源码压缩包在小程序环境里一次性上传风险比较高。uni.uploadFile原生接口虽然简单但对弱网用户极不友好经常传一半失败然后整个文件作废重新传。考虑到大部分校赛场景的网络环境还是校园WiFi或者流量我采用的是Flask后端接收分块上传的方案。思路是前端把文件切成每块5MB大小循环调用后端接口后端把每个分块写到临时目录最后一个分块上传完成后触发合并。这里贴一下后端合并文件的核心逻辑app.route(/api/work/int:entry_id/upload_chunk, methods[POST]) login_required def upload_chunk(entry_id): chunk_index request.form.get(chunkIndex, typeint) total_chunks request.form.get(totalChunks, typeint) file_id request.form.get(fileId) chunk request.files.get(chunk) temp_dir os.path.join(app.config[UPLOAD_TEMP_DIR], str(entry_id), file_id) os.makedirs(temp_dir, exist_okTrue) chunk.save(os.path.join(temp_dir, fchunk_{chunk_index})) # 所有分块都传完了合并 if chunk_index total_chunks - 1: final_path os.path.join(app.config[UPLOAD_DIR], f{uuid4().hex}.zip) with open(final_path, wb) as outfile: for i in range(total_chunks): chunk_path os.path.join(temp_dir, fchunk_{i}) if not os.path.exists(chunk_path): return api_response(code500, message分块缺失请重传) with open(chunk_path, rb) as infile: outfile.write(infile.read()) # 清理临时目录 shutil.rmtree(temp_dir) # 数据库记录作品信息 ... return api_response(code200, message上传完成) return api_response(code200, message分块接收成功)前端配合做的是每次上传前先向后端发一个“创建上传任务”请求拿到fileId然后循环分块。如果某个分块失败可以单独重传该分块不用整体重来。这个方案实测下来上传一个100MB的视频作品在稳定校园网环境下大约几十秒完成弱网环境下体验也远比整包上传好。3.6 部署上线从开发机到服务器本地开发用的python run.py是Flask自带的开发服务器只能用于调试。部署到服务器我用的方案是Gunicorn Nginx。Gunicorn启动Flask应用gunicorn -w 4 -b 127.0.0.1:8000 app:create_app()用4个worker进程处理并发请求Nginx做反向代理静态文件上传的作品文件直接由Nginx提供不走Python进程。这里有个性能痛点千万不要把作品文件直接存在Flask的静态目录里并让Flask去读。文件下载走Python太慢高并发下直接打满CPU。让Nginx直接alias指向uploads目录下载效率提升了一个数量级。server { listen 80; server_name api.school.edu.cn; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /var/www/competition/uploads/; } }HTTPS证书的配置用的是Lets Encrypt免费证书设置自动续期。微信小程序后台要求所有请求域名必须是HTTPS且已在“request合法域名”里配置所以HTTPS是硬需求没有商量的余地。4. 常见问题与排查技巧实录4.1 微信小程序request请求报“域名不合法”这个问题几乎每个人都会碰到。开发阶段你可以到微信开发者工具里勾选“不校验合法域名”方便本地调试。但正式上线如果不处理好所有请求都会失败。解决方案分三步第一把生产环境的后端域名加进微信公众平台后台的“开发管理 - 服务器域名 - request合法域名”第二确保域名备案和HTTPS证书有效第三如果用了uni.uploadFile还需要在uploadFile合法域名里同样配置一条。我之前只配了request域名忘记配uploadFile域名结果作品上传功能一到真机就失败开发工具里却一切正常折腾了一下午才定位到。解决办法是把HTTP请求的401响应和Token失效统一封装前端请求拦截器处理这类异常时调用uni.reLaunch跳转登录页而不是原地弹窗提示。用户感知到的是“重新进场”而不会看到一个刺眼的红色报错。4.2 Flask跨域问题与CORS配置会话保持之后如果你用了from flask import session做登录态管理那你必须把密码也配置好同时前端必须允许携带Cookie。不过我在这个项目里用的是JWT请求头Authorization传token跨域问题比Cookie模式简单很多。开发阶段为了方便调试我直接全开CORSfrom flask_cors import CORS CORS(app, supports_credentialsTrue)上线前收紧策略只允许学校域名的请求。注意Flask-CORS的supports_credentialsTrue必须和前端请求的withCredentials/credentials: include保持同步否则浏览器会拦截响应。4.3 数据库连接断掉MySQL的8小时问题部署之后出现过一次诡异故障系统刚上线时一切正常第二周早上有学生反馈报名失败后端日志里全是Lost connection to MySQL server during query。原因很经典MySQL默认的wait_timeout是8小时连接池里的连接空闲超过这个时间就会被服务端关闭但Flask-SQLAlchemy的连接池并不知道继续拿着失效连接去查询于是报错。解决方案有两个一是改MySQL配置把wait_timeout调大到一天二是让连接池自动回收过期连接。我用的是后者SQLALCHEMY_ENGINE_OPTIONS { pool_size: 10, pool_recycle: 3600, pool_timeout: 30, pool_pre_ping: True }pool_pre_ping是神器每次从连接池拿连接前先ping一下如果失效就重建。设置pool_recycle为3600秒确保连接不会存活超过MySQL的回收时间。这两种手段双管齐下之后再没出现过这个问题。4.4 微信小程序视频下载与缓存控制热搜词里有“微信小程序中的视频下载”和“微信小程序设置缓存时间”我在作品展示模块里正好涉及。我的作品展示页面需要支持视频播放和缓存。微信小程序的video组件默认不支持直接下载视频文件它只负责播放。因此我在上传作品时单独设计了一个资源列表接口返回视频的URL以及授权凭证。同时在播放器组件上配置custom-cache属性让视频在后台合理缓存。这看起来简单但要做对关键是不要把视频文件的URL直接暴露给未登录用户否则别人可以直接拿到链接到处转传。我的做法是在后端生成带签名的临时URL有效期30分钟过期自动失效def generate_signed_url(file_path): expire_at int(time.time()) 1800 token hashlib.md5(f{file_path}-{expire_at}-{SECRET_KEY}.encode()).hexdigest() return f/api/work/preview?path{file_path}expire{expire_at}sign{token}这个设计有效限制了作品的传播范围尤其是在校赛这种对知识产权比较敏感的场合。4.5 前端列表性能优化避免一次性拉全量数据竞赛列表和报名列表随着使用时间推进会越来越长。一开始我的分页接口写得懒直接Entry.query.all()返回所有数据到了有几百条报名记录时前端在小程序里渲染列表已经肉眼可见地卡顿。优化成Flask-SQLAlchemy分页查询后page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) paginated Entry.query.filter_by(...).paginate( pagepage, per_pageper_page, error_outFalse ) return api_response(code200, data{ items: [entry.to_dict() for entry in paginated.items], total: paginated.total, page: page, pages: paginated.pages })配合前端的onReachBottom触底加载下一页滚动加载的体验非常顺滑。如果数据量更大还可以在后端加select * from entry where id cursor order by id desc limit 10的游标分页但在校赛这种量级几千条以内用传统分页就够了。4.6 条件编译小程序和H5行为差异化热搜词里还有一条“uniapp 开发 微信小程序 vs android / ios / 鸿蒙”。uniapp的跨端优势不能神化有些API在微信小程序里能用但在H5里没有反之亦然。我在这个项目里深有体会的是uni.chooseMessageFile这个API——它在微信小程序中可以选取聊天文件但在H5端根本没有这个API。所以有条件地使用条件编译来隔离差异// 选择作品文件 chooseFile() { // #ifdef MP-WEIXIN uni.chooseMessageFile({ count: 1, extension: [.zip, .pdf, .doc, .docx, .mp4], success: (res) { this.selectedFile res.tempFiles[0] } }) // #endif // #ifdef H5 // H5端用input[typefile]即可 // #endif }条件编译处理不当会造成“编译到某个平台直接白屏”的问题。我的经验是关键平台的差异代码一定要真机验证不要只在开发者工具上看效果。开发者工具模拟的API行为和真机有些细微差别比如蓝牙、扫码、音视频播放这类能力模拟器往往是“看起来能用真机才暴露问题”。4.7 审核死锁与并发修改问题这个问题排查起来比较有意思。两位指导老师同时审核同一个学生报名时后端的逻辑可能产生死锁。比如老师A先读取了这个报名单老师B也读取了A更新状态为“通过”B紧接着把状态覆盖为“驳回”。最后的记录取决于谁后写而不是谁先审核这显然不合理。解决方案有两层。第一层是乐观锁在entry表加一个version字段每次更新前检查当前版本号是否和读取时一致不一致则提示“数据已被他人修改请刷新后重试”。entry Entry.query.filter_by(identry_id, versionversion).first() if not entry: return api_response(code409, message数据已被其他操作修改请刷新) # 更新业务字段 entry.status new_status entry.version 1 db.session.commit()第二层是状态机前置校验。因为状态机的合法迁移集合已经限制了脏操作即使并发到来不符合迁移规则的状态更新也会被拦截。这两个策略配合下来审核操作基本不会出现脏写。5. 系统测试与上线后的体验反馈5.1 功能测试重点清单上线前我做了一套针对核心功能的测试清单覆盖了每个可能出问题的场景学生首次微信授权登录能自动注册并绑定学号。同一个学生同一竞赛重复报名会被拦截。团队报名中队长退出后队员能看到报名单并继承队长权限。截止时间到达后报名按钮置灰后端接口同样拒绝写入。作品上传断网再恢复分块能从断点继续传。驳回的报名单允许学生修改后重新提交状态重置为审核中。管理员结束后台评审学生端查询接口返回最终成绩和证书编号。并发审核同一报名单时后提交的一方收到版本冲突提示。这套清单不需要写自动化脚本手动过一遍耗时大半天但对一个中小型校赛系统来说已经能覆盖绝大多数风险。5.2 真实上线场景中的性能表现系统上线后经历了学校一年一度的程序设计大赛报名高峰在一个晚上集中爆发。学生宿舍晚间统一开放报名后一分钟内大约有500个请求涌入后端4个Gunicorn worker的配置扛住了这个量级响应时间维持在200ms以内。数据库端的报名唯一约束在并发下也发挥了作用没有出现一条重复报名。高峰期之后我看了后端日志发现真正的性能瓶颈不在API处理而在文件上传时磁盘IO和静态文件下载的带宽占用。这也验证了为什么文件目录必须交给Nginx托管而不是让Python进程来处理。6. 经验总结与后续扩展建议6.1 几个踩坑之后形成的习惯这套系统做完以后我给自己总结了几条铁律写在这里对后来者应该也有用。第一永远让后端做最终的数据校验。前端校验再丰富也只是用户体验优化不是数据安全的防线。有一次我临时把前端一个必填字段去掉了校验结果后端接口也有个逻辑漏洞没拦住空值数据库里就多了一条没有姓名只有学号的脏数据最后靠写SQL清理。从那以后所有CRUD接口里都套了一个统一的参数校验器绝不信任前端传来的任何字段。第二所有时间字段统一存UTC时间展示层再转本地时区。这个习惯一开始觉得麻烦但经历了一次服务器时区设置错误导致报名截止时间错乱的事故后发现这个习惯是无价的。后端存储用datetime.utcnow()返回给前端的ISO时间带时区偏移前端拿到以后用dayjs转本地时间展示逻辑无比清晰。第三写任何接口都先想清楚幂等性。用户在小程序里可能因为网络抖动重复点击提交按钮如果你的接口没有幂等控制数据库里就会插入多条重复记录。我的方案是前端在提交按钮点击后立即置灰同时后端在事务里做唯一索引校验双保险。6.2 可扩展的方向从校赛系统到竞赛全流程平台这个系统目前的边界是“参赛申请与作品管理”但高校学科竞赛的完整生命周期不止这些。后续可以扩展的方向包括自动生成参赛成绩汇总报表对接教务系统、证书在线生成和下载、校内选拔赛的在线评委打分系统、优秀作品展示库。其中评委打分这块最值得做评委进入系统后只能看到匿名化编号的作品打分后自动汇总排名全程留痕能极大减轻学科竞赛组织者的工作负担。还有一个实用的小功能——微信服务通知。学生报名成功、审核通过、作品被退回、成绩公布这四个节点都通过订阅消息推送到学生微信。这门技术在小程序端只需要维护一个template_id后端在学生操作对应事件时调用微信的订阅消息接口即可学生端的提醒体验会显著优于小程序内小红点。6.3 针对高校运维环境的两点贴心建议最后补两个比较实际的部署建议。第一高校的服务器可能在内网环境外网访问需要通过防火墙策略所以一定要提前跟信息化中心确认好后端端口是否对外开放、HTTPS证书如何申请、是否需要走统一认证网关。这些在项目启动前沟通清楚能省掉上线前的一大堆麻烦。第二定期备份数据库。校赛系统虽然是中小型应用但学生数据和企业数据一样需要重视。我的方案是写一个crontab任务每天凌晨把MySQL库dump成SQL文件保留最近30天的备份。这一条建议无足轻重但真到数据丢了的时候你会庆幸自己有备份。这套系统从开发到上线前后用了一个半月代码量不算大但麻雀虽小五脏俱全微信登录、角色权限、复杂表单、文件上传、状态机、条件编译、性能优化、部署调试该经历的坑几乎都经历了一遍。如果让我重新做一遍我大概率还是会选同样的技术栈不是因为它是最潮的而是因为它足够简单直接能让我把精力集中在解决业务问题上。希望这篇拆解能帮到正在做类似高校管理系统的朋友少走一点弯路。
网站建设高端定制企业官网