ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Flask和Vue3的新生报到管理系统开发实践

基于Flask和Vue3的新生报到管理系统开发实践 每学期开学那两周是辅导员最忙乱的时候。新生报到信息登记、核对录取数据、统计到校率、导出台账如果全靠Excel手工来回倒腾一个专业的百来号人就能让人忙到凌晨。我在实际工作中帮几位辅导员朋友做过一套“新生报到管理系统”后端用PythonFlask前端用Vue 3前后端分离。这篇文章把整套系统的设计思路和实现过程完整写出来包括我踩过的坑和最后沉淀下来的实操方法给正在做同类管理系统的同学一个可复用的参考。这套内容适合谁看一类是毕业设计、课程设计选了“XX管理系统”方向的同学另一类是工作中确实需要快速搭建内部管理工具的开发者。不需要你有多深的算法底子Python基础语法清楚、能看懂Vue组件的基本写法跟着本文把代码敲一遍就能跑通。我会从方案选型、数据库设计、接口设计一路讲到前端页面实现和联调排错全程没有藏着掖着的地方。1. 项目整体设计与思路拆解1.1 报到管理系统的真实痛点先说说为什么需要这么一个系统。新生报到场景下辅导员的日常工作其实就三类一是核对信息把录取名单上的学生信息和到校现场登记的信息一一比对二是追踪进度实时知道“现在有多少人已经办完手续”“哪些学生还没来报到”三是汇总上报学校要数据的时候能立刻拿出准确的报道路报表。这三件事听起来简单但用传统方式做起来很痛苦。录取名单是从招生办导出的Excel文件新生报到时要填写纸质登记表辅导员统计进度靠的是在纸上打勾或在表格里手动标记。一旦学生人数超过两百这套流程的效率会断崖式下降——表格格式不统一、信息核对容易漏、进度统计严重滞后数据到处散落想追责都找不到源头。所以这套系统的核心目标非常明确把“名单数据、报到状态、进度统计”这三大块全部线上化辅导员打开浏览器就能看到当前报到情况新生信息录入后自动落库报到状态实时更新统计报表一键导出。这个定位决定了系统的架构不会太复杂但业务逻辑必须紧扣真实场景不能光是教科书式的增删改查。1.2 为什么前后端分离是合理选择我在设计之初也考虑过用 Django 自带模板直接渲染页面或者用 Flask Jinja2 做服务端渲染。对于只有一个辅导员角色、页面不超过十个的内部工具来说这种单体方案确实能少写不少代码。但权衡之后我还是选择了前后端分离理由有三点。第一是团队协作和维护的便利性。前后端分离之后前端只关心数据展示和交互后端只关心接口和业务逻辑两边的职责划分干净利落。如果后面学校要求给学工处加一个总览页面、给新生加一个自助查询页面前端可以单独扩容完全不影响后端接口。第二是前端生态的成熟度。Vue 3 Element Plus 的组合在表格、表单、日期选择、弹窗这些管理后台常用组件上非常成熟写出来的界面比手工拼模板美观得多开发效率也高。而且 Vue 的单文件组件机制让页面结构和样式封装得更干净后期维护不用在一堆混着模板和 SQL 的 Python 文件里翻找。第三是接口复用。报到数据将来不只是辅导员要用学院领导要看汇总、宿管要看入住数据而这些系统可能都不是同一个团队在维护。做成前后端分离后后端接口随时可以被其他系统调用只要做好权限控制就行。1.3 技术选型Python后端框架的取舍Python 后端框架我重点对比了 Flask、Django 和 FastAPI 三个。对于一个新生报到管理系统我的建议是优先 Flask下面说说我对比的结论。Django 功能全面自带的 ORM、Admin 后台、认证系统都是开箱即用但正因为它太“重”对新手来说学习曲线反而更陡。你只是想做一个报到登记功能Django 会强制你把项目拆成 app、配置 settings、理解中间件和信号很多刚起步的人光看到那个目录结构就懵了。而且 Django 的 Admin 后台虽然方便但默认的样式和交互并不完全贴合这个业务场景后期改造的工作量不比从零写少。FastAPI 性能好、自动生成 OpenAPI 文档、支持异步这些都是加分项。但它的优势主要体现在高并发和高性能场景比如内部工具只有几十个辅导员同时用根本跑不满它的性能。而且 FastAPI 的异步语法对刚接触 Web 开发的同学来说是一个额外的理解成本调试排错也比 Flask 复杂。Flask 最核心的优势就两个字轻量。它不限定项目结构一切按你的习惯来学习成本极低。一个主文件就能启动一个能跑的服务后续要加功能再按模块拆分这种渐进式的节奏对新手和中小项目都非常友好。配合 Flask-SQLAlchemy 做 ORM、Flask-CORS 解决跨域完全够用了。前端我选的是 Vue 3 Vite Element Plus。Vue 2 已经停止维护新项目没必要考虑。Vite 作为构建工具冷启动和热更新速度比 webpack 快一个量级开发体验好很多。Element Plus 是 Element UI 的 Vue 3 版本表格、表单、消息提示这些组件直接拿来用省去很多样式上的折腾。提示如果是正式交付给学校使用建议后端再加一层 Token 认证Flask-JWT-Extended不同角色分配不同权限如果只是毕业设计演示用简单的 Token 生成方式也够用但需要在论文里交代清楚。2. 数据库设计与接口规范先把地基打牢2.1 业务表结构到底怎么设计数据库是整个系统最不能将就的部分。表结构设计得合理后面写接口、写页面都会非常顺手要是表结构有硬伤后期改起来牵一发动全身。我在这个项目里一共设计了五张核心表简单说明一下每张表的职责。学生表student是基线数据存放所有新生的基本信息包括学号、姓名、性别、身份证号、录取专业、班级、联系方式等。这张表的数据主要来源于招生办提供的 Excel 名单系统启动时通过导入功能批量写入。报到记录表report_record记录每个学生的报到状态包括是否已完成报到、报到时间、办理人、备注信息。为什么把报到记录单独拆出来而不是直接在学生表里加一个 is_reported 字段因为报到状态可能会有多次变更比如学生先登记了基本信息后来才提交完整的报到材料或者报到后又出现信息修正的情况。单独建表可以把每一次操作都留痕后续需要审计追踪时就有了依据。专业表major和班级表class是维度表用来维护专业名称和班级名称避免在学生表里大量重复存储中文文本。报到时经常需要按专业或班级筛选名单这两张表能大幅提高查询效率也让前端下拉框的数据来源更规范。用户表user存登录账号考虑到当前版本主要给辅导员使用我只做了管理员角色。加载到系统中时预置一个账号后续如果需要扩展到学院领导查看权限可以在 user 表加一个 role 字段搞定。2.2 一张表字段的细节实现示例拿学生表来说字段设计时我特意注意了几个容易忽略的点。学号用字符串而不是整数因为学号前面常有0用整数类型会把前导零丢掉身份证号长度固定18位但数据库里我留了varchar(20)防止偶发的港澳台通行证号码或历史格式问题把导入流程卡住性别字段用 tinyint 存储0表示男、1表示女前端显示时再映射成文字这样数据存储更洁净也不会出现“男/女/未知”这类不统一的口径。报到记录表是这张表里最关键的。我用的是上课前能想到的最稳定的组合学生ID、报到状态、报到时间、操作人、备注。报到状态用一个整数枚举0表示未报到1表示已报到2表示延期报到3表示放弃入学资格。为什么用枚举而不是直接存储中文因为直接存字符串后续如果要调整文案比如“放弃入学”改成“自动退学”就得写一堆 UPDATE 语句用枚举的话前端映射表一改就完事后端统计也方便。班级表相对简单包含班级名称、所属专业ID、辅导员姓名、入学年份。班级名称看起来冗余但实际使用中很关键。后端接口经常需要做“班级维度”的聚合统计如果学生表只有专业ID而没有班级ID统计就得跨多张表 join性能和维护成本都会上升。2.3 后端 API 风格与接口清单接口设计我完全采用 RESTful 风格资源用名词复数操作对应 HTTP 方法。下面是一份完整的接口清单照着清单开发能把前后端的分工边界划得特别清楚。方法路径功能说明POST/api/auth/login辅导员登录返回 tokenGET/api/students分页获取学生列表支持姓名/学号/专业筛选POST/api/students/import上传 Excel 文件批量导入学生名单GET/api/students/export按条件导出学生报到数据GET/api/students/获取单个学生详细信息PUT/api/students/编辑学生基础信息POST/api/students/ /report登记某学生的报到状态GET/api/stats/summary获取整体报到进度统计GET/api/stats/major获取按专业维度的报到统计GET/api/stats/class获取按班级维度的报到统计这个接口清单是我们和前端约定的“契约”。后端把接口定义好、返回格式统一前端就可以先按 Mock 数据开发页面等后端接口就绪后再无缝联调。返回格式我统一封装成{ code: 0, message: ok, data: {...} }code 为 0 代表成功非 0 代表业务异常。这个格式约定很常见遇到错误时前端弹个消息提示就行非常省事。3. 前端页面搭建Vue 3 Element Plus 实战3.1 项目初始化和依赖安装前端项目我用 Vite 初始化一行命令就能搞定npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia element-plus axios这里插一句为什么用 Vite 而不是 Vue CLI。Vue CLI 基于 webpack冷启动在大型项目上要等好几秒热更新有时候也会卡一下。Vite 基于原生 ES Module启动速度几乎秒开开发体验好了不是一点半点。如果你是初学别被网上老教程带偏直接上 Vite 就好。装完依赖之后在 main.js 里做基础配置把 Element Plus 和样式引入import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)3.2 路由和状态管理规划路由我按业务场景分成了登录页、主页、学生管理、报到登记、统计报表、名单导入六个模块。先看一个简化的 router 配置import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layout/MainLayout.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(../views/Dashboard.vue) }, { path: students, component: () import(../views/StudentList.vue) }, { path: report, component: () import(../views/ReportManage.vue) }, { path: import, component: () import(../views/ImportData.vue) }, ], }, ]这里用了路由懒加载component 用箭头函数返回 import按需加载页面组件首屏加载速度会明显快一些。状态管理我推荐用 Pinia它是 Vue 官方推荐的新一代状态库比 Vuex 更轻量、TypeScript 支持也更友好。这个项目里我把用户登录信息和 token 存起来方便后续请求拦截器读取import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , username: , }), actions: { setLoginInfo(token, username) { this.token token this.username username localStorage.setItem(token, token) }, logout() { this.token this.username localStorage.removeItem(token) }, }, })3.3 报到进度总览与信息看板实现报到进度总览是辅导员打开系统后第一眼看到的页面核心价值就是“一眼看全局”。我把它做成一个 KPI 卡片 图表组合的形式。KPI 卡片展示几个关键数字总人数、已报到人数、报到率、未报到人数。前端用 axios 请求/api/stats/summary接口拿到数据后用 Element Plus 的 Row/Col 布局铺四张卡片。这里要提醒一个细节——报到率建议保留一位小数直接用(reported / total * 100).toFixed(1)计算即可但要注意分母为零的情况需要先判断 total 是否为 0。按专业维度的统计我用了 ECharts 的柱状图。每个专业一根柱子显示已报到的学生数。ECharts 本身是一个独立的图表库和 Vue 没有强绑定但社区里有现成的vue-echarts封装组件用起来更顺手。安装方式npm install echarts vue-echarts在统计页面里引入后通过组件属性的方式把配置项传进去template v-chart :optionchartOption autoresize styleheight: 400px / /template script setup import { computed } from vue import VChart from vue-echarts import { use } from echarts/core import { CanvasRenderer } from echarts/renderers import { BarChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent } from echarts/components use([CanvasRenderer, BarChart, GridComponent, TooltipComponent, LegendComponent]) const props defineProps({ data: { type: Array, default: () [] }, }) const chartOption computed(() ({ tooltip: { trigger: axis }, xAxis: { type: category, data: props.data.map((item) item.major_name) }, yAxis: { type: value, minInterval: 1 }, series: [ { type: bar, data: props.data.map((item) item.reported_count), itemStyle: { color: #409EFF }, }, ], })) /script3.4 学生列表与报到登记页面学生列表页面是整个系统最核心的交互页面。顶部是一排筛选条件姓名关键字、学号、专业下拉框、报到状态下拉框下方是 Element Plus 的 el-table 展示学生信息。报到状态我用 el-tag 组件显示不同颜色的标签——已报到是绿色、未报到是红色、延期报到是橙色一张表格扫过去马上就能识别异常状态。每一行操作区放两个按钮一个是“编辑”一个是“报到登记”。点击报到登记后弹出一个 Dialog 表单里面填报到状态、备注信息提交后调POST /api/students/id/report接口成功就刷新表格数据。这里有一个非常实用的操作批量报到。新生报到时往往一个班级的学生陆续到齐辅导员不可能一个个点击登记。所以在表格前面加上多选列支持勾选多个人之后点“批量报到”后端接口接收一个 ID 列表一次循环更新多人的报到状态。这个功能在实际使用中好评度很高。前端批量报到接口的 axios 调用async function batchReport(ids, status) { const { data } await axios.post(/api/students/batch-report, { ids, status, }) if (data.code 0) { ElMessage.success(批量报到成功) fetchStudentList() } else { ElMessage.error(data.message) } }4. 实操过程与核心环节实现4.1 后端项目结构的组织方式Flask 的后端项目结构我建议从一开始就不要写成单文件。哪怕功能再简单也要按模块拆不然后面改需求的时候会在一个几百行的文件里翻来翻去。我的目录结构如下backend/ ├── app.py # 应用入口 ├── config.py # 配置项 ├── models/ │ ├── __init__.py │ ├── student.py # 学生模型 │ └── report.py # 报到记录模型 ├── resources/ │ ├── __init__.py │ ├── auth.py # 登录认证接口 │ ├── student.py # 学生管理接口 │ ├── report.py # 报到登记接口 │ └── stats.py # 统计接口 ├── utils/ │ ├── __init__.py │ ├── response.py # 统一返回封装 │ └── excel.py # Excel 导入导出 └── requirements.txt这里每个模块的职责很清晰。models 只负责定义数据库表结构resources 只处理请求参数和调用逻辑utils 放一些跨模块通用的工具函数。这样以后加新的业务模块时不会乱成一锅粥。4.2 核心后端接口的代码实现看一个最关键的接口学生报到登记。这个接口会做三件事校验参数、更新学生状态、写入报到记录。完整代码如下from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required, get_jwt_identity from models import db, Student, ReportRecord from utils.response import success, error report_bp Blueprint(report, __name__) report_bp.route(/api/students/int:student_id/report, methods[POST]) jwt_required() def create_report(student_id): data request.get_json() status data.get(status) remark data.get(remark, ) if status not in [0, 1, 2, 3]: return error(无效的报到状态) student Student.query.get(student_id) if student is None: return error(学生不存在) # 更新学生表中的报到状态 student.report_status status # 写入报到记录留痕 record ReportRecord( student_idstudent_id, statusstatus, remarkremark, operatorget_jwt_identity() ) db.session.add(record) db.session.commit() return success({student_id: student_id, status: status})整个接口逻辑非常直白没有花哨的写法。我特意把“更新状态”和“写记录”放在同一个事务里提交这样即便中途出错数据库也不会出现状态和记录不一致的情况。4.3 学生名单 Excel 导入的实现Excel 导入是一个绕不过去的功能因为辅导员手里的数据一定是从旧系统或者 Excel 文件里来的。这个功能的实现思路前端用 el-upload 组件上传文件后端用 openpyxl 或 pandas 读取 Excel逐行校验后写入数据库。安装依赖pip install openpyxl pandas后端接收文件的代码from flask import request import pandas as pd student_bp.route(/api/students/import, methods[POST]) def import_students(): file request.files[file] if not file: return error(请上传文件) # 使用 pandas 读取 Excel跳过表头 df pd.read_excel(file, engineopenpyxl) # 期待列学号、姓名、性别、身份证号、专业、班级、联系方式 required_columns [学号, 姓名, 性别, 专业, 班级] for col in required_columns: if col not in df.columns: return error(f缺少必要列: {col}) success_count 0 error_messages [] for index, row in df.iterrows(): student_id str(row[学号]).strip() if not student_id: error_messages.append(f第{index 2}行: 学号为空) continue # 简单校验学号不能重复 if Student.query.filter_by(student_nostudent_id).first(): error_messages.append(f第{index 2}行: 学号 {student_id} 已存在) continue student Student( student_nostudent_id, namestr(row[姓名]).strip(), gender0 if str(row[性别]).strip() 男 else 1, major_namestr(row[专业]).strip(), class_namestr(row[班级]).strip(), phonestr(row.get(联系方式, )).strip(), id_card_nostr(row.get(身份证号, )).strip(), report_status0 ) db.session.add(student) success_count 1 db.session.commit() return success({ success_count: success_count, errors: error_messages[:50] # 最多返回前50条错误明细 })导入功能一定要做预处理。最典型的问题就是 Excel 里学号列是数字格式pandas 读进来后会变成 float比如2024001变成2024001.0所以 str(row[学号]).strip() 前还应该加一步转 int 再转 str 的清理逻辑。我在代码里直接用str(int(row[学号]))处理这样能避免小数点问题。实际项目中身份证号经常是科学计数法显示pandas 读进来就变成了类似4.10422e17的形式这就要用str(int(float(row[身份证号])))来还原。这种细节很容易让人崩溃但处理一次后面就顺了。4.4 报到统计接口的实现按专业统计报到率的接口用一条 GROUP BY 就能完成stats_bp.route(/api/stats/major, methods[GET]) def stats_by_major(): rows db.session.query( Student.major_name, db.func.count(Student.id).label(total), db.func.sum(db.case((Student.report_status 1, 1), else_0)).label(reported) ).group_by(Student.major_name).all() result [ { major_name: r.major_name, total_count: r.total, reported_count: r.reported or 0, rate: round(r.reported / r.total * 100, 1) if r.total else 0 } for r in rows ] return success(result)这里的小技巧是直接在后端把 rate 算好前端拿到数据就能渲染不用每个前端重新算一遍。如果数据量大分页和聚合条件复杂还可以考虑把统计结果缓存起来但在这个业务体量下完全没必要。4.5 前后端联调的启动配置联调阶段最大的坑就是跨域问题。前端跑在 5173 端口后端跑在 5000 端口浏览器会直接拦截跨域请求。解决方式有两种。第一种是后端开启 CORS。安装 flask-cors 后注册一下就行from flask_cors import CORS CORS(app)但这种方式有个小毛病——它会把 CORS 头默认设置成允许所有来源生产环境里不建议这么干。更好的做法是明确指定允许的域名CORS(app, resources{r/api/*: {origins: http://localhost:5173}})第二种方式是前端配置 Vite 代理开发环境下把所有 /api 请求转发到后端端口// vite.config.js export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true, }, }, }, })我推荐开发阶段用第二种方式因为前端请求的是相对路径后面部署到生产环境时只要把前端静态文件交给后端托管或者用 Nginx 把 /api 转发到后端服务前端代码不需要任何改动。5. 常见问题与排查技巧实录5.1 跨域请求报错浏览器提示 CORS 缺失这个问题的表现非常典型前端请求发出去了后端日志也显示接收到了但浏览器控制台报Access to XMLHttpRequest has been blocked by CORS policy。上面的 CORS 配置方法能解决 90% 的情况但还有 10% 是出在复杂请求上。当请求携带自定义头比如 Authorization或使用 PUT/DELETE 方法时浏览器会先发一个 OPTIONS 预检请求。如果后端没有处理 OPTIONS 请求就会报错。Flask-CORS 默认会处理这种情况但如果你用的是自定义装饰器需要留意是否把 OPTIONS 方法也拦截掉了。我自己遇到过一种情况全局加app.before_request做 token 校验OPTIONS 请求还没进路由就被 token 校验挡住了导致预检失败。解决方法是放行所有 OPTIONS 请求。这个坑虽然不大但排查起来很耗时间。5.2 前端请求报 404但接口路径看起来没问题这种情况多半是路由或代理配置有误。先用 curl 直接访问后端接口确认接口本身能通再检查 Vite 代理是否配置正确。我个人的排查顺序是第一看后端是否启动了、端口是否正确第二用 curl 测试接口返回第三看前端浏览器 Network 面板里实际请求的 URL 是什么。很多时候问题出在 frontend 请求地址写死了http://localhost:5000/api/xxx而代理配置只匹配/api两者冲突导致请求被双重处理。5.3 Excel 导入中文乱码用 pandas 读 Excel 一般不会出现乱码但如果辅导员给的文件是 CSV 格式就有编码问题。CSV 文件可能是 GBK 编码直接用 pandas 默认的 UTF-8 读取就会乱码。解决办法是读取时指定编码df pd.read_csv(file, encodinggbk)但这里有个新的坑——文件编码不一定都是 GBK有的文件已经是 UTF-8 了。所以更稳妥的做法是先尝试 UTF-8失败后用 GBK 再读一遍。我用一个小函数封装def read_csv_with_fallback(file): try: return pd.read_csv(file, encodingutf-8) except UnicodeDecodeError: return pd.read_csv(file, encodinggbk)这样不管文件是哪种编码都能正确读出来。这个小函数看起来不起眼但在实际使用中避免了无数次 “为什么我的数据全是乱码” 的崩溃瞬间。5.4 前端 npm run dev 启动失败端口被占Vite 默认端口是 5173如果同时开了多个前端项目端口就会被占用。这时候可以用一行命令换一个端口npm run dev -- --port 5174如果是老项目占用了端口但你想用回原端口可以强制关掉占用端口的进程。在命令行里找到进程号后结束它即可。这个操作在不同平台上有不同命令开发环境用顺手了之后基本不会因为这种问题浪费超过五分钟。5.5 部署阶段图片静态资源加载失败前端 build 之后默认的静态资源路径是/assets/xxx.js如果部署在 Nginx 的子目录下路径就会全部失效。解决办法是修改vite.config.js的base配置export default defineConfig({ base: ./, // 使用相对路径 plugins: [vue()], })这个坑我踩过一次当时是部署到校园网某个子路径下前端页面白屏打开控制台一看全是Failed to load resource。改完 base 配置后刷新就正常了。如果以后要部署到 CDN 或者独立域名再把 base 改回来就行。5.6 数据库连接超时的小问题如果用的 SQLite基本不用操心连接超时问题但如果是 MySQL空闲一段时间后重新访问会报MySQL server has gone away。这个多半是数据库的 wait_timeout 配置太短后端连接池还持有旧的连接。解决方式是在 SQLAlchemy 连接池设置上做一点调整加上 pre_ping 参数让它在拿连接前先检查连接是否可用。这个细节虽小但对系统长期稳定运行来说很关键。写在最后的一点实操心得这套系统从零到跑通我一个人大概花了三个完整的工作日。第一天搭后端环境、建表、写核心接口第二天搭前端框架、写页面组件第三天联调、修跨域和 Excel 导入的问题。如果是两个人配合前端和后端各负责一块时间能压缩到两天左右。关于这个项目我最想分享的一点是不要急着写代码先把接口清单和数据库表结构理清楚。我在最早的一版里没有拆分报到记录表结果后面要加“报到状态变更历史”功能时前端和后端都要大改白白返工了一天。表结构设计其实就是在为未来的需求留余地好的表结构是系统能持续迭代的基础。如果你正在用这套思路做自己的管理系统建议从接口清单开始一页页把每个 API 的入参出参写清楚再开始写数据库建模。这种流程看上去慢但真正进入编码阶段后你会发现思路清晰的人写代码天然就快因为不用边写边想“这里数据从哪来、那里逻辑怎么串”。最后再分享一个小技巧开发时经常用浏览器的 Network 面板观察接口响应时间如果发现某个接口慢多半是 SQL 查询没有走索引可以看一下数据库的执行计划。比如学生表的学号字段一定记得建唯一索引身份证号加普通索引这样随着数据量增长性能也有保障。这种性能细节在毕业设计答辩时提出来也是加分项。
返回列表