Flask+Vue贫困生资助系统:业务闭环与权限控制实战
2026/9/10 12:25:16 网站建设 项目流程

简介:本资源是一套完整可用的毕业设计级贫困生资助管理系统,面向计算机及相关专业本科生、毕业设计学生及Python全栈初学者,解决高校学生资助管理流程数字化、前后端分离开发实践等实际需求。项目采用Python后端(Django/Flask类框架)与Vue前端技术栈,含可运行源码、配套SQL数据库及完整部署脚本(如安装.bat、运行.bat),所有模块均经本地编译调试,评审得分98分,内容通过助教审定,难度适中且具备教学示范性。压缩包共755个文件,12.96MB,涵盖40个核心Python业务逻辑文件、38个Vue组件页面、162个JS交互脚本、51个CSS样式文件、2个SQL建库脚本,以及SVG图标、JPG/PNG素材、HTML模板等,结构清晰,便于按模块学习与二次开发。目前已有107人下载学习,适合用于毕设参考、课程设计实战或前后端分离项目入门训练。

1. 这不是又一个学生管理系统——它用 Flask + Vue 实现了真实资助业务流闭环

很多同学拿到“贫困生资助系统”毕业设计,第一反应是:不就是增删改查加个登录?但实际评审时被问倒的,往往是那些藏在表单背后的业务逻辑:如何校验家庭经济困难认定等级与助学金档位的映射关系?怎样防止同一学生在多个院系重复申报?资助发放记录如何与财务系统凭证号做可追溯关联?这个 Python 贫困生资助管理系统(评审分98分)恰恰把这些问题落到了代码里——后端用 Flask 构建 RESTful API,前端用 Vue 2.6 + Element UI 实现动态权限路由,数据库采用 MySQL 8.0 设计了student_infofamily_economic_statusgrant_applicationfund_distribution_log四张核心表,并通过外键约束和存储过程保障数据一致性。它适合计算机专业大三下至大四上正在做毕设的学生:代码量适中(后端约 4200 行,前端约 3800 行),无硬编码配置,所有接口路径、数据库连接参数、JWT 密钥均通过.env文件注入;也适合想练手前后端分离项目的初学者——你不需要从零搭环境,运行.bat一键启动开发服务器,安装.bat自动执行pip install -r requirements.txt并初始化数据库表结构。

提示:项目中所有.vue.bak文件是原始开发备份(如IndexMain.vue.bak对应主布局组件),非临时文件;3-build.bat用于生成生产环境静态资源,2-run.bat启动 Flask 开发服务器并监听http://127.0.0.1:5000/api/,前端默认访问http://localhost:8080,跨域已在 Flask 的flask-cors中预配置。

2. Flask 后端服务搭建:从数据库建模到 JWT 权限控制链路

2.1 数据库设计遵循资助业务实体关系,而非简单用户-角色模型

贫困生资助管理的核心矛盾在于:身份认定、申请审核、资金发放、效果反馈四个阶段的数据耦合度高,但操作主体不同(辅导员初审、学院复审、资助中心终审、财务处打款)。因此本系统未采用通用 RBAC 模型,而是按业务域拆分权限表:

-- 学生基本信息(含身份证号哈希值,符合等保要求) CREATE TABLE student_info ( id INT PRIMARY KEY AUTO_INCREMENT, stu_id VARCHAR(12) NOT NULL UNIQUE, -- 学号 name VARCHAR(20) NOT NULL, id_card_hash CHAR(64) NOT NULL, -- SHA256(id_card) college VARCHAR(30), major VARCHAR(30), grade YEAR, status ENUM('在校', '休学', '毕业') DEFAULT '在校' ); -- 家庭经济困难认定结果(由学校统一组织评议,每年更新) CREATE TABLE family_economic_status ( id INT PRIMARY KEY AUTO_INCREMENT, stu_id VARCHAR(12) NOT NULL, assessment_year YEAR NOT NULL, level ENUM('特别困难', '困难', '一般困难') NOT NULL, assessment_unit VARCHAR(50), -- 认定单位(如:XX学院学生工作办公室) FOREIGN KEY (stu_id) REFERENCES student_info(stu_id) ON DELETE CASCADE ); -- 助学金申请单(每次申请生成独立记录,支持撤回) CREATE TABLE grant_application ( id INT PRIMARY KEY AUTO_INCREMENT, app_no VARCHAR(20) NOT NULL UNIQUE, -- 申请编号:GRANT-2024-0001 stu_id VARCHAR(12) NOT NULL, apply_date DATE NOT NULL, grant_type ENUM('国家助学金', '地方助学金', '社会捐助') NOT NULL, amount DECIMAL(10,2) NOT NULL, -- 申请金额(元) status ENUM('待初审', '初审通过', '初审驳回', '复审中', '已终审', '已发放', '已终止') DEFAULT '待初审', reason TEXT, -- 申请理由(富文本) FOREIGN KEY (stu_id) REFERENCES student_info(stu_id) ); -- 资金发放日志(与财务系统对接的关键凭证) CREATE TABLE fund_distribution_log ( id INT PRIMARY KEY AUTO_INCREMENT, app_id INT NOT NULL, distribution_date DATETIME NOT NULL, bank_account VARCHAR(20), -- 收款银行卡号后四位(脱敏) voucher_no VARCHAR(30), -- 财务凭证号(如:CW202405001) actual_amount DECIMAL(10,2) NOT NULL, operator VARCHAR(20), -- 操作人(工号) FOREIGN KEY (app_id) REFERENCES grant_application(id) );

注意:family_economic_status表中assessment_year字段强制要求每年只能有一条有效记录,避免历史认定结果干扰当期资助;grant_application.status状态机严格限制流转路径(例如“已发放”不可退回至“待初审”),该约束在 Flask 的update_application_status()视图函数中通过if-elif链校验,而非仅依赖数据库 CHECK。

2.2 Flask API 层实现三层校验:身份认证 → 业务权限 → 数据完整性

后端使用 Flask 1.1.4 + Flask-SQLAlchemy 2.4.4 + Flask-JWT-Extended 3.25.1,所有敏感接口(如/api/application/submit)必须携带Authorization: Bearer <token>。JWT payload 结构如下:

{ "identity": "2021001", // 工号或学号 "role": "counselor", // 角色:student / counselor / dean / admin "dept": "computer", // 所属院系代码 "exp": 1735689600 // 过期时间戳 }

关键校验逻辑体现在@jwt_required装饰器后的视图函数中。以提交助学金申请为例:

# app/routes/application.py from flask import request, jsonify from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt from app.models import GrantApplication, StudentInfo from app import db @application_bp.route('/submit', methods=['POST']) @jwt_required def submit_application(): current_user = get_jwt_identity() claims = get_jwt() # 第一层:身份认证(JWT 解析成功即通过) # 第二层:业务权限校验 —— 仅在校学生可提交 student = StudentInfo.query.filter_by(stu_id=current_user).first() if not student or student.status != '在校': return jsonify({'code': 403, 'msg': '非在校生无法提交申请'}), 403 # 第三层:数据完整性校验 —— 防止绕过前端校验 data = request.get_json() required_fields = ['grant_type', 'amount', 'reason'] for field in required_fields: if not data.get(field): return jsonify({'code': 400, 'msg': f'缺少必填字段:{field}'}), 400 if not isinstance(data['amount'], (int, float)) or data['amount'] <= 0: return jsonify({'code': 400, 'msg': '申请金额必须为正数'}), 400 # 关键业务规则:同一学年同一类型申请不可重复 existing = GrantApplication.query.filter( GrantApplication.stu_id == current_user, GrantApplication.grant_type == data['grant_type'], db.extract('year', GrantApplication.apply_date) == datetime.now().year ).first() if existing: return jsonify({'code': 409, 'msg': '本学年该类型助学金已提交,请勿重复申请'}), 409 # 创建新申请记录 app = GrantApplication( app_no=f"GRANT-{datetime.now().year}-{str(db.session.query(GrantApplication).count()+1).zfill(4)}", stu_id=current_user, apply_date=datetime.now().date(), grant_type=data['grant_type'], amount=float(data['amount']), reason=data['reason'], status='待初审' ) db.session.add(app) db.session.commit() return jsonify({'code': 200, 'msg': '申请提交成功', 'app_no': app.app_no}), 200
2.2.1 JWT Token 刷新机制规避会话中断

系统未采用长时效 token(避免泄露风险),而是实现双 token 模式:access_token(2小时)+refresh_token(7天)。刷新接口/api/auth/refresh仅验证refresh_token签名及有效期,成功后签发新access_token

# app/routes/auth.py @auth_bp.route('/refresh', methods=['POST']) @jwt_required(refresh=True) # 仅接受 refresh token def refresh_token(): current_user = get_jwt_identity() new_token = create_access_token(identity=current_user) return jsonify({'access_token': new_token})

提示:create_access_token()默认使用app.config['JWT_SECRET_KEY']签名,该密钥在config.py中读取自环境变量JWT_SECRET_KEY安装.bat会自动生成 32 位随机密钥写入.env,杜绝硬编码密钥。

2.3 数据库初始化与迁移:SQLAlchemy-Migrate 替代方案

项目未使用 Flask-Migrate(因毕业设计场景下表结构稳定),而是提供init_db.py脚本完成一次性初始化:

# scripts/init_db.py import os from app import create_app, db from app.models import StudentInfo, FamilyEconomicStatus, GrantApplication app = create_app() with app.app_context(): # 删除所有表(仅开发环境) db.drop_all() # 创建新表 db.create_all() print("✅ 数据库表创建完成") # 插入测试数据(管理员账号:admin/123456) from werkzeug.security import generate_password_hash admin_pwd = generate_password_hash('123456') # ... 插入初始用户、测试学生等 print("✅ 测试数据插入完成")

执行命令:python scripts/init_db.py。该脚本在安装.bat中被调用,确保每次重装环境都能获得干净数据库。

3. Vue 前端工程化实践:从路由守卫到 Axios 请求拦截的完整链路

3.1 基于角色的动态路由生成,解决“菜单权限”与“接口权限”双重校验

前端使用 Vue 2.6.14 + Vue-Router 3.5.3 + Vuex 3.6.2,摒弃静态路由配置,改为在登录成功后拉取用户权限菜单并动态添加:

// src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import store from '@/store' Vue.use(VueRouter) // 异步加载路由组件(减少首屏体积) const Login = () => import('@/views/Login.vue') const IndexMain = () => import('@/views/IndexMain.vue') const ApplicationList = () => import('@/views/ApplicationList.vue') const ApplicationForm = () => import('@/views/ApplicationForm.vue') // 全局路由守卫:检查 token 有效性 router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('access_token') if (!token && to.path !== '/login') { next('/login') } else if (token && to.path === '/login') { next('/') } else { // 校验 token 是否过期(前端粗略判断) try { const payload = JSON.parse(atob(token.split('.')[1])) if (Date.now() >= payload.exp * 1000) { await store.dispatch('user/logout') next('/login') } else { next() } } catch (e) { next('/login') } } }) // 登录后动态添加路由 export function addDynamicRoutes(menus) { menus.forEach(menu => { const route = { path: menu.path, name: menu.name, component: () => import(`@/views/${menu.component}.vue`), meta: { title: menu.title, role: menu.role } } router.addRoute('layout', route) // 添加到名为 'layout' 的父路由下 }) } export default router

menus数据由/api/auth/menu接口返回,结构示例:

[ { "path": "/application/list", "name": "ApplicationList", "component": "ApplicationList", "title": "申请列表", "role": ["student", "counselor", "dean"] }, { "path": "/application/audit", "name": "AuditList", "component": "AuditList", "title": "审核任务", "role": ["counselor", "dean", "admin"] } ]
3.1.1 路由元信息驱动侧边栏渲染

IndexAsideStatic.vue组件根据this.$route.meta.role数组与当前用户角色比对,决定是否显示某菜单项:

<!-- src/components/IndexAsideStatic.vue --> <template> <el-aside width="200px"> <el-menu :default-active="$route.path" class="el-menu-vertical-demo"> <template v-for="menu in filteredMenus"> <el-menu-item :key="menu.path" :index="menu.path" @click="handleMenuClick(menu)"> <i :class="menu.icon"></i> <span slot="title">{{ menu.title }}</span> </el-menu-item> </template> </el-menu> </el-aside> </template> <script> export default { computed: { filteredMenus() { const userRole = this.$store.state.user.role return this.$store.state.menu.list.filter(menu => menu.role.includes(userRole) // 精确匹配角色字符串 ) } } } </script>

3.2 Axios 请求拦截器统一处理 Token 与错误响应

所有 API 请求通过封装的request.js发起,内置请求/响应拦截器:

// src/utils/request.js import axios from 'axios' import { Message } from 'element-ui' import store from '@/store' // 请求拦截器:自动添加 Authorization 头 axios.interceptors.request.use( config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) // 响应拦截器:统一处理业务错误码 axios.interceptors.response.use( response => { const { code, msg, data } = response.data if (code === 200) { return data } else if (code === 401) { // token 过期,触发刷新或登出 store.dispatch('user/logout') Message.error('登录已过期,请重新登录') window.location.href = '/login' } else if (code === 403) { Message.warning(msg || '权限不足') } else { Message.error(msg || '请求失败') } return Promise.reject(new Error(msg)) }, error => { if (error.response?.status === 500) { Message.error('服务器内部错误,请联系管理员') } else if (error.code === 'ECONNABORTED') { Message.error('请求超时,请检查网络') } return Promise.reject(error) } ) export default axios

注意:401错误处理中未直接跳转/login,而是调用store.dispatch('user/logout'),该 action 会清除本地 token 并重置 Vuex 状态,确保后续路由守卫能正确拦截。

3.3 表单级数据校验与状态管理:基于 Element UI 的深度集成

助学金申请表单(ApplicationForm.vue)使用 Element UI 的el-form组件,校验规则定义在data()中:

<template> <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> <el-form-item label="资助类型" prop="grant_type"> <el-select v-model="form.grant_type" placeholder="请选择"> <el-option label="国家助学金" value="国家助学金"></el-option> <el-option label="地方助学金" value="地方助学金"></el-option> </el-select> </el-form-item> <el-form-item label="申请金额(元)" prop="amount"> <el-input v-model.number="form.amount" type="number" min="0"></el-input> </el-form-item> <el-form-item label="申请理由" prop="reason"> <el-input type="textarea" v-model="form.reason" :rows="4"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">提交申请</el-button> </el-form-item> </el-form> </template> <script> export default { data() { return { form: { grant_type: '', amount: null, reason: '' }, rules: { grant_type: [ { required: true, message: '请选择资助类型', trigger: 'change' } ], amount: [ { required: true, message: '请输入申请金额', trigger: 'blur' }, { type: 'number', message: '金额必须为数字', trigger: 'blur' }, { min: 100, max: 5000, message: '金额应在100-5000元之间', trigger: 'blur' } ], reason: [ { required: true, message: '请填写申请理由', trigger: 'blur' }, { min: 20, message: '理由不少于20字', trigger: 'blur' } ] } } } } </script>
3.3.1 提交前二次校验与防重复提交

submitForm方法在调用 API 前执行额外校验,并禁用按钮防止重复点击:

methods: { submitForm() { this.$refs.formRef.validate(valid => { if (valid) { // 禁用提交按钮,防止重复提交 this.submitLoading = true this.$refs.formRef.clearValidate() // 清除上次校验状态 // 调用 API submitApplication(this.form) .then(res => { this.$message.success('申请提交成功!') this.$router.push('/application/list') }) .catch(err => { this.$message.error(err.response?.data?.msg || '提交失败') }) .finally(() => { this.submitLoading = false }) } }) } }

4. 本地开发与部署全流程:从 Windows 双击运行到 Linux 生产环境上线

4.1 Windows 环境下双击批处理文件完成全栈启动

项目根目录提供四个关键.bat文件,其执行逻辑如下:

文件名功能关键命令
安装.bat初始化 Python 环境、安装依赖、创建数据库python -m venv venv
venv\Scripts\activate.bat
pip install -r requirements.txt
python scripts/init_db.py
运行.bat启动 Flask 后端(端口5000)和 Vue 前端(端口8080)start cmd /k "venv\Scripts\activate.bat && python app.py"
start cmd /k "cd frontend && npm run serve"
2-run.bat仅启动 Flask 后端(供前端单独调试)venv\Scripts\activate.bat && python app.py
3-build.bat构建前端生产包(输出到frontend/distcd frontend && npm run build

提示:运行.bat使用start cmd /k分别开启两个命令行窗口,避免前后端进程互相阻塞;app.pyapp.run(debug=True, host='0.0.0.0', port=5000)允许局域网内其他设备访问(便于手机调试)。

4.2 Linux 生产环境部署:Nginx 反向代理 + Gunicorn + Supervisor

将项目部署到阿里云 ECS(CentOS 7.9)的标准流程如下:

4.2.1 后端部署:Gunicorn 替代 Flask 内置服务器
# 1. 安装依赖 sudo yum install python38 python38-pip nginx gcc sudo pip3 install gunicorn supervisor # 2. 创建部署目录并上传代码 sudo mkdir -p /opt/grant-system sudo chown -R $USER:$USER /opt/grant-system # 将源码解压到 /opt/grant-system/ # 3. 创建虚拟环境并安装依赖 cd /opt/grant-system python3.8 -m venv venv source venv/bin/activate pip install -r requirements.txt # 4. 配置 Gunicorn(gunicorn.conf.py) bind = "127.0.0.1:8000" workers = 3 worker_class = "sync" timeout = 30 keepalive = 2 accesslog = "/var/log/grant-access.log" errorlog = "/var/log/grant-error.log" loglevel = "info"

启动命令:gunicorn -c gunicorn.conf.py app:app

4.2.2 Nginx 配置实现前后端分离的静态资源托管与 API 代理
# /etc/nginx/conf.d/grant.conf upstream grant_backend { server 127.0.0.1:8000; } server { listen 80; server_name grant.yourdomain.com; # 前端静态资源(由 3-build.bat 生成) location / { root /opt/grant-system/frontend/dist; try_files $uri $uri/ /index.html; } # API 请求代理到 Gunicorn location /api/ { proxy_pass http://grant_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态文件(如上传的证明材料) location /uploads/ { alias /opt/grant-system/uploads/; } }

重启 Nginx:sudo systemctl restart nginx

4.2.3 Supervisor 进程守护确保服务自启
# /etc/supervisord.d/grant.ini [program:grant-backend] command=/opt/grant-system/venv/bin/gunicorn -c /opt/grant-system/gunicorn.conf.py app:app directory=/opt/grant-system user=root autostart=true autorestart=true redirect_stderr=true stdout_logfile=/var/log/grant-supervisor.log

启用:sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start grant-backend

4.3 常见部署问题排查清单

现象可能原因快速验证命令
访问首页白屏,控制台报Failed to load resource: the server responded with a status of 404 ()Nginx 未正确指向dist目录ls -l /opt/grant-system/frontend/dist检查是否存在index.html
登录后跳转/显示 404Vue Router history 模式需 Nginx 配置try_files检查/etc/nginx/conf.d/grant.conflocation /块是否包含try_files $uri $uri/ /index.html;
API 请求返回 502 Bad GatewayGunicorn 未启动或端口不匹配sudo supervisorctl status查看进程状态;curl http://127.0.0.1:8000/api/auth/ping测试后端连通性
上传文件失败,提示Permission denieduploads目录权限不足sudo chown -R nginx:nginx /opt/grant-system/uploads(CentOS)或www-data:www-data(Ubuntu)

5. 业务级调试技巧:利用数据库快照与前端 Mock 数据快速定位问题

5.1 使用 MySQL 快照还原特定业务场景数据

当需要复现“辅导员初审通过后,学院复审页面不显示该申请”这类问题时,手动构造数据效率低。项目提供data_snapshots/目录,存放按业务阶段命名的 SQL 快照:

  • snapshot_counselor_approved.sql:包含 5 条status='初审通过'的申请记录
  • snapshot_dean_pending.sql:包含 3 条status='初审通过'college='computer'的记录(供计算机学院复审)

还原步骤:

# 停止应用 sudo supervisorctl stop grant-backend # 导入快照(覆盖原表) mysql -u root -p grant_db < data_snapshots/snapshot_dean_pending.sql # 启动应用 sudo supervisorctl start grant-backend

提示:快照文件使用mysqldump --no-create-info --skip-triggers grant_db grant_application生成,仅导出数据不导出表结构,避免与当前表结构冲突。

5.2 前端 Mock 服务绕过后端联调,专注 UI 逻辑验证

frontend/mock目录内置 Mock.js 规则,启动命令npm run mock(需先npm install mockjs):

// frontend/mock/application.js import Mock from 'mockjs' Mock.mock('/api/application/list', 'get', () => { return { code: 200, data: Mock.mock({ 'list|5': [{ 'app_no': '@guid()', 'stu_id': '@string("number", 10)', 'name': '@cname()', 'grant_type': '@pick(["国家助学金", "地方助学金"])', 'amount|1000-5000': 1, 'status': '@pick(["待初审", "初审通过", "复审中"])', 'apply_date': '@date("yyyy-MM-dd")' }] }) } })

此时访问http://localhost:8080,前端将直接消费 Mock 数据,无需启动 Flask 后端。该方式特别适合助教验收 UI 交互、动画效果等前端专项。

5.3 关键业务断点调试:在 Flask 中注入实时日志

当线上环境出现“资金发放成功但财务凭证号未写入”这类偶发问题时,在fund_distribution_log插入逻辑处添加结构化日志:

# app/routes/distribution.py import logging logger = logging.getLogger(__name__) @app.route('/distribute', methods=['POST']) @jwt_required def distribute_fund(): data = request.get_json() # ... 参数校验 log_data = { 'app_id': data['app_id'], 'operator': get_jwt_identity(), 'bank_last4': data.get('bank_account', '')[-4:], 'voucher_no': data.get('voucher_no', ''), 'actual_amount': data['actual_amount'] } logger.info(f"资金发放请求: {log_data}") # 输出到 /var/log/grant-error.log # 执行数据库插入 log = FundDistributionLog(**data) db.session.add(log) db.session.commit() logger.info(f"资金发放成功: app_id={data['app_id']}, voucher_no={data['voucher_no']}") return jsonify({'code': 200, 'msg': '发放成功'})

配合journalctl -u supervisord -f | grep grant实时追踪,可快速定位是前端传参缺失,还是数据库事务未提交。

本文还有配套的精品资源,点击获取

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

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

立即咨询