ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的贫困生资助管理系统:Flask+Vue前后端分离实战解析

基于Python的贫困生资助管理系统:Flask+Vue前后端分离实战解析 简介这是一份面向计算机相关专业毕业设计及项目实战学习者的Python贫困生资助管理系统完整工程。系统采用前后端分离架构包含完整源码、数据库及运行配置覆盖学生资助申请、审核管理、信息维护等典型业务模块难度适中适合作为课程设计、毕业设计参考或二次开发练习。压缩包共755个文件大小12.96MB文件类型涵盖Python后端源码与编译文件、Vue前端组件、样式脚本、页面图标素材、SQL数据库脚本以及安装启动构建等批处理脚本目录结构清晰便于导入后按模块学习。项目经过严格调试和导师指导审定评审分98分可运行性有较好保障附带数据库和备份文件能快速还原环境帮助读者理解资助管理业务流程与前后端交互方式。目前已有108人学习下载适合需要完整系统案例、希望快速跑通项目的计算机专业学生。1. 贫困生资助管理系统Python 前后端分离到底在解决什么每到毕业设计季总有人拿着一句“基于 Python 的贫困生资助管理系统”来问我这不就是增删改查吗有什么好做的实际上这个题目比表面看起来要“有货”得多。它的核心不是写几个页面而是要把学生信息、贫困认定、资助项目、发放记录这几条业务线串成一条完整的数据流同时用前后端分离的架构把代码组织清楚——这在答辩时既能讲业务又能讲工程属于性价比很高的选题。如果你正在做类似系统或者想用一个真实业务把 Python Web 开发练扎实这套系统的标准实现路径是后端用 Flask 提供 RESTful 接口前端用 Vue 调用接口渲染页面MySQL 存放所有业务数据身份认证用 JWT。后面我会把技术选型、数据库设计、核心接口、常见坑一次讲透让你照着就能搭起来。2. 选型与技术栈为什么是 Flask Vue而不是一锅烩的模板渲染2.1 为什么是 Python 而不是 Java 的 Spring Boot近两年“若依框架前后端分离”“springboot vue 前后端分离”是热搜常客Java 系的后台管理系统确实成熟。但放在贫困生资助管理这个毕业设计题目上我仍然建议优先选 Python原因很实际第一Python 代码量小一个人在三四周内能写完业务闭环第二答辩时讲 Flask 的路由和 SQLAlchemy 映射比讲 Spring 的 IOC、AOP 容器更容易让评委听懂第三导师如果只要求“能跑、有数据库、前后端分离”Python 完全满足。用表格看会更清楚对比项Flask Vue本方案Spring Boot Vue代码量后端 1500 行以内能覆盖全部业务同样功能往往 3000 行起步学习曲线路由、ORM、装饰器三个概念就能上手要理解 starter、容器、配置体系答辩讲解从 request 到 SQL 的路径非常直白容易陷入框架概念解释部署复杂度一个 python 进程 nginxJAR 包 更重的配置适合人群毕设、短周期项目已有 Java 基础或求职需要当然这不是说 Spring Boot 不好。如果你确定以后走 Java 方向或者导师明确要求企业级框架那选 Spring Boot 也合理。但就“毕业设计基于 Python 贫困生资助管理系统”这个标题而言Python 是更聪明的选择——它把精力留给业务设计而不是框架配置。2.2 Flask、Django、FastAPI 三选一怎么定确定了 Python 之后Web 框架还有得选。Django 自带 Admin 后台、ORM 和认证体系做管理系统非常省事但它的“全家桶”风格对前后端分离来说反而累赘——你不需要模板渲染也不需要 Admin 自动生成页面那些都是给服务端渲染准备的。FastAPI 性能好、自带接口文档但生态里做后台管理系统的示例远没有 Flask 多遇到问题更难搜到答案。所以我的选择是 Flask理由有三条。其一Flask 只保留路由和请求处理的核心能力其余用扩展按需加载结构贴近“接口层 业务层 数据层”的经典分层其二Flask 的 request、jsonify、Blueprint 这几个 API 非常稳定网上关于 Flask Vue MySQL 的完整案例极多踩坑容易找到对照其三一个项目的代码量有限用 Flask 写出来的文件数量少导师抽查代码时你解释起来也不费劲。有同学会纠结“用 Flask 是不是显得不够高级”。答辩的关键是对自己写的每一行代码都说得清楚而不是框架名字唬人。Flask 能讲到的东西并不少路由注册、请求钩子、上下文、ORM 会话管理这些足够撑起一场 20 分钟的答辩。2.3 “前后端分离”在这个项目里的具体含义很多毕设把“前后端分离”挂嘴边但实际代码还是后端用 render_template 拼 HTML。真正的分离是指后端只输出 JSON 数据前端通过 axios 发异步请求拿到数据后自己渲染 DOM。这个系统里有三个页面最能体现这种模式一是登录页前端把用户名密码 POST 给后端后端校验后返回一段 JWT 字符串前端把 Token 存到 localStorage之后每个请求都在 Header 里带上它二是贫困生认定页前端把表单数据打包成 JSON 发给后端后端写入数据库后返回“提交成功”和记录 ID三是资助记录页前端请求/api/subsidy/records?page1size10后端返回分页数据和总条数前端自己翻页。这样做的好处是前端和后端可以完全独立开发。你甚至可以用 Postman 先调通所有接口再让 Vue 去对接答辩时如果前页面样式出问题也不影响你演示接口的完整性。后面第 4 章我会给出这些接口的具体实现第 5 章会专门讲分离模式下最常踩的跨域问题。2.4 项目目录结构怎么摆目录结构直接影响后面所有代码的组织方式。我常用的布局是把后端、前端、数据库脚本三个部分平级放让导师一眼看出这是前后端分离的项目。你可以在项目根目录执行下面这段命令快速创建poverty_aid/ ├── backend/ # Flask 后端服务 │ ├── run.py # 启动入口: python run.py │ ├── config.py # 数据库连接、密钥等配置 │ ├── models.py # SQLAlchemy ORM 模型 │ ├── auth.py # JWT 生成与校验装饰器 │ ├── views/ │ │ ├── __init__.py │ │ ├── auth_api.py # 登录、修改密码 │ │ ├── student_api.py # 学生信息、贫困认定 │ │ └── subsidy_api.py # 资助项目与发放记录 │ └── requirements.txt ├── frontend/ # Vue 前端工程 │ ├── src/ │ │ ├── api/ # axios 请求封装 │ │ ├── router/ # 前端路由 │ │ ├── views/ # 登录、认定、记录等页面 │ │ └── main.js │ └── package.json └── db/ └── init.sql # 建库建表脚本说明一下就几个关键文件的职责。config.py里集中放 MySQL 连接串和 JWT 密钥不要在业务代码里到处写数据库地址models.py放所有 ORM 映射类和views下的接口文件分开db/init.sql保存完整的建表脚本方便导师在另一台机器上快速重建数据库。我见过不少项目把所有代码堆在一个app.py里跑是能跑但答辩时讲不清楚“分层”这件事很吃亏。3. 数据库设计资助系统的核心表与一套可复现的建表 SQL3.1 先理清业务边界一套完整流程要几张表贫困生资助管理系统听起来是“一张学生表加一张发放表”但真正落到流程上至少要覆盖四个环节账号登录、贫困生认定、资助项目配置、资助发放记录。为了保证每个环节都有据可查我设计了六张核心表。第一张是user存登录账号和角色角色区分管理员、辅导员、学生三种第二张是student存学生的学号、学院、专业、家庭住址等基本信息第三张是poverty_application存贫困生认定申请包括家庭年收入、困难等级、申请理由和审核状态第四张是subsidy_project存资助项目比如国家助学金、学费减免、临时困难补助第五张是subsidy_record存实际发放记录关联学生和项目第六张是review_log存审核日志记录谁在什么时间把申请改成了什么状态。这样设计的直接好处是贫困认定和资助发放解耦。一个学生可能先被认定为“特别困难”之后参与多个资助项目认定信息只需要在poverty_application里存一份发放记录则在subsidy_record里任意多条如果将来要统计“某个学院共发放了多少补助”用subsidy_record关联student就能算出来不需要翻页面。3.2 建库建表 SQL六张表的完整脚本以下是可直接执行的 MySQL 建表脚本。注意建库语句已经指定了utf8mb4字符集避免后面出现中文乱码的隐患CREATE DATABASE IF NOT EXISTS poverty_aid DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE poverty_aid; -- 用户账号表三类角色共用 CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(255) NOT NULL COMMENT SHA256 哈希值, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 2 COMMENT 0管理员 1辅导员 2学生, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户账号表; -- 学生信息表 CREATE TABLE student ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL COMMENT 关联 user.id, student_no VARCHAR(20) NOT NULL COMMENT 学号, college VARCHAR(50) NOT NULL COMMENT 学院, major VARCHAR(50) NOT NULL COMMENT 专业, class_name VARCHAR(50) NOT NULL COMMENT 班级, family_address VARCHAR(255) DEFAULT COMMENT 家庭地址, family_income DECIMAL(10,2) DEFAULT 0 COMMENT 家庭年收入, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 贫困生认定申请表 CREATE TABLE poverty_application ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, student_id INT UNSIGNED NOT NULL COMMENT 关联 student.id, hardship_level TINYINT NOT NULL COMMENT 1一般困难 2困难 3特别困难, apply_reason VARCHAR(500) NOT NULL COMMENT 申请理由, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, reviewer_id INT UNSIGNED DEFAULT NULL COMMENT 审核人 user.id, review_comment VARCHAR(255) DEFAULT NULL COMMENT 审核意见, review_time DATETIME DEFAULT NULL COMMENT 审核时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_student_status (student_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT贫困生认定申请表; -- 资助项目表 CREATE TABLE subsidy_project ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, project_name VARCHAR(100) NOT NULL COMMENT 项目名称, amount DECIMAL(10,2) NOT NULL COMMENT 资助标准金额, quota INT NOT NULL DEFAULT 0 COMMENT 名额数量, is_active TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资助项目表; -- 资助发放记录表 CREATE TABLE subsidy_record ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, student_id INT UNSIGNED NOT NULL COMMENT 关联 student.id, project_id INT UNSIGNED NOT NULL COMMENT 关联 subsidy_project.id, amount DECIMAL(10,2) NOT NULL COMMENT 实际发放金额, batch_no VARCHAR(30) NOT NULL COMMENT 批次号如 2025-01, operator_id INT UNSIGNED NOT NULL COMMENT 操作人 user.id, grant_time DATETIME NOT NULL COMMENT 发放时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_student (student_id), KEY idx_project (project_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资助发放记录表; -- 审核日志表 CREATE TABLE review_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, application_id INT UNSIGNED NOT NULL COMMENT 关联 poverty_application.id, action VARCHAR(20) NOT NULL COMMENT 如 submit/approve/reject, operator_id INT UNSIGNED NOT NULL COMMENT 操作人 user.id, comment VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_application (application_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审核日志表;这个脚本在设计上有几处要重点说明。所有表都用InnoDB引擎理由是它支持事务和外键资助发放这种写操作必须保证要么成功要么回滚不能用 MyISAM。金额字段全部使用DECIMAL(10,2)这个会在第 5 章详细说现在先记住和钱有关的字段永远别用 FLOAT。状态字段用TINYINT而不是字符串既节省空间也避免写错单词导致程序判断失效对应的中文含义在代码里用常量定义而不是散落在各处。此外poverty_application里建了复合索引idx_student_status覆盖“查看某个学生的所有申请记录”这个高频查询“审核后不可物理删除”也是一条设计约束——审核日志表的存在就是为了让每次状态变更都能追溯。3.3 字段选型不然后悔的三个决策第一个决策是用户表不直接存明文密码而是存哈希值。Flask 里可以用hashlib.sha256加盐处理也可以用werkzeug.security.generate_password_hash后者内部已经带了随机盐答辩时讲“密码不落库”是一个加分点。注意password字段要留到 255 长度因为 Werkzeug 生成的哈希字符串比较长设成 50 会直接存不进去。第二个决策是学生信息是否为独立表。有些简单实现会把学生的学院专业直接塞进user表这样其实也能跑但“学生”和“账号”在业务上是两个概念一个学生账户可能被禁用但学生档案应该保留辅导员账号不关联学生表却需要登录系统。分表之后user只管登录认证student管档案资料职责清晰。第三个决策是hardship_level用数字而不是让用户填文本。如果允许填“非常困难”“一般困难”“困难”这样的自由文本统计时就会发现同一个意思有七八种写法。用 TINYINT 定义三档前端下拉框的选项和后端代码里的枚举一一对应数据质量才有保证。3.4 种子数据让系统第一次启动就能登录建完表之后数据库是空的直接启动后端连登录都做不了。我通常在init.sql末尾追加一段种子数据插入默认管理员和一个测试资助项目INSERT INTO user (username, password, real_name, role) VALUES (admin, 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918, 系统管理员, 0); INSERT INTO subsidy_project (project_name, amount, quota) VALUES (国家助学金一等, 4400.00, 50), (国家助学金二等, 3300.00, 80), (学费减免, 2000.00, 30);这里admin的密码哈希值对应的是admin这个明文的 SHA256 结果是我本地生成的你在自己的项目里务必换成自己的密码哈希。注意一个细节subsidy_project的quota字段用于后续校验发放名额是否超出插入种子数据时最好给几个不同的值方便前端分页和统计时展示效果。种子数据的价值在于第一次source init.sql之后前后端立刻有数据可看不用再手工往库里插记录。4. 用 Flask 把资助业务写成接口登录、申请与审核的关键代码4.1 ORM 模型与 JWT 验证骨架数据库表建好了后端第一步是把表映射成 ORM 模型。这里以Student和PovertyApplication为例其余模型完全同理# models.py from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class Student(db.Model): __tablename__ student id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, nullableFalse) student_no db.Column(db.String(20), uniqueTrue, nullableFalse) college db.Column(db.String(50), nullableFalse) major db.Column(db.String(50), nullableFalse) class_name db.Column(db.String(50), nullableFalse) family_address db.Column(db.String(255)) family_income db.Column(db.Numeric(10, 2), default0) class PovertyApplication(db.Model): __tablename__ poverty_application id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, nullableFalse) hardship_level db.Column(db.Integer, nullableFalse) apply_reason db.Column(db.String(500), nullableFalse) status db.Column(db.Integer, default0) reviewer_id db.Column(db.Integer) review_comment db.Column(db.String(255)) review_time db.Column(db.DateTime) created_at db.Column(db.DateTime, defaultdb.func.now())模型字段和init.sql的表结构一一对应类型上注意两点金额字段用Numeric(10, 2)对应数据库的DECIMAL时间字段用DateTime而不是String否则排序和区间查询会出问题。db.func.now()是让数据库生成当前时间避免应用服务器和数据库服务器时钟不一致导致记录时间错乱。认证部分用 JWT。这里用 PyJWT 手写一个简单的签发与校验逻辑比引入整个 flask-jwt-extended 更轻也方便答辩时讲清楚原理# auth.py import jwt import datetime from functools import wraps from flask import request, jsonify SECRET_KEY your-secret-key-change-me # 实际项目从 config.py 读取 def generate_token(user_id, role): payload { user_id: user_id, role: role, exp: datetime.datetime.utcnow() datetime.timedelta(hours12) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def login_required(f): wraps(f) def wrapper(*args, **kwargs): token request.headers.get(Authorization, ) if not token.startswith(Bearer ): return jsonify({code: 401, msg: 未提供认证令牌}), 401 try: payload jwt.decode(token.split( )[1], SECRET_KEY, algorithms[HS256]) request.user_id payload[user_id] request.user_role payload[role] except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: 登录已过期}), 401 except Exception: return jsonify({code: 401, msg: 无效令牌}), 401 return f(*args, **kwargs) return wrappergenerate_token把用户 ID 和角色塞进 payloadexp设置 12 小时过期login_required装饰器负责从请求头取 Token、验签、把用户信息挂到request上。这里有个细节前端传 Token 的格式是Authorization: Bearer token所以后端要先判断前缀再取第二部分很多项目在这里直接切片导致报错第 5 章会再提到。4.2 登录接口校验密码并签发 Token登录是系统的入口接口逻辑非常直观查用户、比对密码哈希、生成 Token。注意这里比对密码用的是第 3 章种子数据里对应的哈希算法# views/auth_api.py from flask import Blueprint, request, jsonify from werkzeug.security import check_password_hash from models import db, User from auth import generate_token auth_bp Blueprint(auth, __name__, url_prefix/api/auth) auth_bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username, ).strip() password data.get(password, ) user User.query.filter_by(usernameusername).first() if not user or not check_password_hash(user.password, password): return jsonify({code: 400, msg: 用户名或密码错误}), 400 token generate_token(user.id, user.role) return jsonify({code: 200, token: token, role: user.role, real_name: user.real_name})request.get_json()从请求体解析 JSON注意前端 axios 必须设置Content-Type: application/json否则这里拿到的可能为空。校验密码时用check_password_hash它能识别哈希里的盐值如果你想用第 3 章种子数据里那种 SHA256 固定哈希就需要自己实现比对不推荐直接用 Werkzeug 的封装更省事。登录接口成功与否通过 HTTP 状态码区分失败返回 400成功返回 200。前端拿到token后存入 localStorage之后所有请求在 axios 拦截器里统一加Authorization头不用每个请求单独处理。4.3 贫困生认定申请与审核状态机接口认定业务有两个核心接口学生提交申请、辅导员审核。提交申请时后端要检查该学生是否已有“待审核”状态的记录防止重复提交# views/student_api.py from flask import Blueprint, request, jsonify from models import db, PovertyApplication from auth import login_required student_bp Blueprint(student, __name__, url_prefix/api/student) student_bp.route(/apply, methods[POST]) login_required def apply(): if request.user_role ! 2: return jsonify({code: 403, msg: 仅学生可提交认定申请}), 403 data request.get_json() student_id data.get(student_id) hardship_level data.get(hardship_level) reason data.get(apply_reason, ).strip() if not student_id or not hardship_level: return jsonify({code: 400, msg: 学生ID与困难等级必填}), 400 exists PovertyApplication.query.filter_by( student_idstudent_id, status0).first() if exists: return jsonify({code: 400, msg: 存在待审核申请请勿重复提交}), 400 app PovertyApplication(student_idstudent_id, hardship_levelhardship_level, apply_reasonreason) db.session.add(app) db.session.commit() return jsonify({code: 200, id: app.id, msg: 申请提交成功})login_required装饰器保证未登录请求直接 401request.user_role是在装饰器里挂上的接口里通过角色判断权限。这里提交时没有校验hardship_level的范围更严格的写法是判断必须属于{1, 2, 3}防止前端绕过页面直接调接口塞进一个 99。数据库里状态默认 0提交后前端列表页就能看到最新记录。审核接口的逻辑是状态流转只允许“待审核”状态被修改为“通过”或“驳回”并记录审核人和审核时间。这种只允许特定状态迁移的设计能让数据不容易乱student_bp.route(/review, methods[POST]) login_required def review(): if request.user_role not in (0, 1): return jsonify({code: 403, msg: 无审核权限}), 403 data request.get_json() application_id data.get(application_id) action data.get(action) # approve 或 reject comment data.get(comment, ) application PovertyApplication.query.get(application_id) if not application: return jsonify({code: 404, msg: 申请记录不存在}), 404 if application.status ! 0: return jsonify({code: 400, msg: 该申请已审核不能重复操作}), 400 application.status 1 if action approve else 2 application.reviewer_id request.user_id application.review_comment comment application.review_time db.func.now() db.session.commit() return jsonify({code: 200, msg: 审核完成})这里有两个容易被忽略的细节。一是审核动作没有直接信任前端传的“目标状态”而是用action参数配合服务端判断避免前端乱传状态值二是审核完成后没有再往review_log表写日志正式版本应该补上这一步但为了保持接口代码可读这里先省略你可以在答辩时说明“日志表通过钩子函数写入”的设计思路。4.4 分页查询资助记录一个参数都不能少资助记录页是数据量最大、最需要分页的接口。前端传page和size后端返回当前页数据加总条数# views/subsidy_api.py from flask import Blueprint, request, jsonify from models import db, SubsidyRecord, Student from auth import login_required subsidy_bp Blueprint(subsidy, __name__, url_prefix/api/subsidy) subsidy_bp.route(/records, methods[GET]) login_required def records(): page request.args.get(page, 1, typeint) size request.args.get(size, 10, typeint) student_id request.args.get(student_id, typeint) if page 1 or size 1 or size 100: return jsonify({code: 400, msg: 分页参数不合法}), 400 query db.session.query(SubsidyRecord) if student_id: query query.filter(SubsidyRecord.student_id student_id) total query.count() rows query.order_by(SubsidyRecord.grant_time.desc()) \ .offset((page - 1) * size).limit(size).all() data [] for row in rows: stu db.session.get(Student, row.student_id) data.append({ id: row.id, student_no: stu.student_no if stu else , amount: str(row.amount), batch_no: row.batch_no, grant_time: row.grant_time.strftime(%Y-%m-%d) }) return jsonify({code: 200, total: total, page: page, size: size, list: data})request.args.get的第三个参数typeint是 Flask 的便捷功能能自动把查询字符串转成 int转失败时返回默认值省去手工int()和 try-except。分页用offset limit组合total从count()获取返回的amount用str()包了一层是因为Numeric类型取出后是Decimal直接序列化为 JSON 会报错转成字符串最省事前端展示时再把它当数字用。grant_time.strftime(%Y-%m-%d)把时间格式化成前端友好格式避免了datetime序列化问题。前端拿到{total, page, list}后计算总页数、控制上一页下一页按钮这是很标准的分页对接模式。注意这里对size做了上限 100 的限制避免有人一次拉走全表数据也方便答辩时说明你考虑了接口的健壮性。5. 联调与运行期的 5 个高频坑从跨域到乱码的排查记录5.1 跨域报错前端请求被浏览器拦下现象Vue 开发服务器跑在localhost:5173Flask 跑在localhost:5000前端用 axios 请求登录接口浏览器控制台报No Access-Control-Allow-Origin header is present请求根本没到后端。原因浏览器的同源策略拦截了跨端口请求。这就是前后端分离项目和传统模板渲染最大的区别——开发模式下两个服务端口不同必须由后端显式声明允许跨域。解决使用 Flask-CORS 扩展在后端初始化时配置白名单。我在开发环境会把所有来源放开方便调试部署到正式环境再收紧from flask_cors import CORS CORS(app, origins[http://localhost:5173], methods[GET, POST, PUT, DELETE], allow_headers[Content-Type, Authorization])origins只填前端开发服务器的地址allow_headers必须包含Authorization否则前端带了 Token 后跨域预检请求一样会失败。如果遗漏这一项你会遇到“登录成功但其他请求全部 401”的诡异现象问题不在鉴权而在跨域头。5.2 中文乱码与 JSON 返回的 ensure_ascii现象数据库表里中文正常但接口返回的中文变成\u5b66\u751f这种 Unicode 转义序列前端直接渲染成乱码或者更严重的是建库时没指定字符集所有中文写入后变成问号。原因第一层是 Flask 的jsonify默认开启ensure_asciiTrue中文会被转义成 ASCII 编码这在调试器里看着是乱码但浏览器解析后其实正常第二层是 MySQL 建库时如果没指定utf8mb4默认latin1会导致数据在写入阶段就损坏这种损坏是不可逆的。解决前端显示乱码在后端配置里关掉 ASCII 转义即可app Flask(__name__) app.config[JSON_AS_ASCII] False # Flask 2.3 之前 app.json.ensure_ascii False # Flask 2.3 及之后如果是写入阶段就乱码基本只能删表重建。所以第 3 章的建库 SQL 里我特别写了DEFAULT CHARACTER SET utf8mb4这是一条“后悔药”式的防护——先建对库后面省掉一堆麻烦。还要顺带检查 MySQL 连接串是否带了charsetutf8mb4参数例如mysqlpymysql://root:password127.0.0.1/poverty_aid?charsetutf8mb4缺了这个参数即使库建对了写入时仍可能不走 UTF-8。5.3 金额字段用 FLOAT 翻车精度丢失现象一条资助记录金额填 4400.00查询出来后变成 4399.999999前端合计时总额对不上。原因FLOAT 是浮点数二进制无法精确表示 0.1 这样的十进制小数。这在展示金额时是硬伤涉及金额统计时误差会被放大让系统看起来非常不专业。解决数据库层面用DECIMAL(10,2)ORM 模型用Numeric(10,2)这两个类型都是定点数按十进制存储和计算不会丢失精度。另外提醒一句ORM 取出Decimal后直接jsonify会报TypeError记住在第 4 章里str()转字符串的做法。答辩时讲出“金额用定点数不用浮点数”这个点是能体现专业度的。5.4 外键删除冲突学生删不掉现象管理员在后台删除一个学生数据库报Cannot delete or update a parent row或者前端提示删除失败。原因第 3 章的建表 SQL 里subsidy_record和poverty_application都通过业务字段关联student.id虽然在 DDL 中没写外键约束但业务上存在依赖。如果直接物理删除学生历史发放记录会变成悬空数据统计立刻出错。解决我的习惯是这类系统不提供物理删除只做状态软删除——在student表加一个is_deleted字段默认 0删除时置为 1查询时统一过滤is_deleted 0。这样历史记录永远可追溯也满足“资助数据必须留痕”的要求。如果导师要求必须能删那删除的同时要级联清理关联记录但这种方式一旦误操作无法恢复不建议。数据库里保留历史比“看着干净”更重要。5.5 “在我电脑上能跑”的依赖版本问题现象代码拷到另一台电脑pip install -r requirements.txt之后启动报错比如 SQLAlchemy 版本不对导致db.session.get不存在或者 PyMySQL 版本不兼容连不上数据库。原因requirements.txt 里没锁版本装出来的是最新版而项目可能是在旧版本下写的。这类问题最“玄学”明明同一份代码环境不同结果完全不同。解决在项目开发稳定后执行pip freeze requirements.txt把当前环境的精确版本导出来并手动检查核心库的版本信息。我通常会在文件头部加上注释标明测试时的 Python 版本例如# Python 3.10后面每一行都锁定版本号。再有经验的人也不能保证换了环境的兼容性锁版本是毕设代码里最值得花两分钟做的事。6. 毕业设计如何自证可用本地复现步骤与一套验收清单6.1 本地跑通前后端的最小命令到一个全新的环境把系统跑起来应该按“建库 → 起后端 → 起前端”三步走每步都有明确的验证点。先进入db目录执行建库脚本mysql -u root -p db/init.sql执行完可以用mysql -u root -p -e USE poverty_aid; SHOW TABLES;确认六张表都在再顺手查一眼user表里有没有那条默认管理员。数据库没问题后启动后端cd backend pip install -r requirements.txt python run.py看到Running on http://127.0.0.1:5000之后用浏览器直接访问http://127.0.0.1:5000/api/auth/login确认页面返回 JSON 格式的错误提示说明 Flask 已经正常处理请求。最后在另一个终端启动前端cd frontend npm install npm run serve浏览器打开 Vite 给的地址http://localhost:5173用默认管理员登录。如果登录后页面能进入主界面说明三个环节已经全部打通。这里有一个很容易忽略的点后端的run.py里要写app.run(host127.0.0.1, port5000, debugTrue)而 CORS 白名单里要写http://localhost:5173而不是127.0.0.1:5173两者端口相同但主机名不同浏览器会把它们当成不同的源白名单不匹配时接口一样被拦。6.2 用一张验收清单证明系统可用演示时最怕的情况是漫无目的地点击自己都不知道下一步该做什么。我习惯在做完功能后列一张验收清单演示时按顺序走每一行对应一个可观察的结果验收项操作步骤预期结果登录鉴权输入错误密码登录提示“用户名或密码错误”登录鉴权用管理员账号登录进入系统首页接口返回 Token贫困认定用学生账号提交认定申请列表出现“待审核”记录审核流转用辅导员账号通过该申请状态变为“已通过”记录审核时间重复提交拦截再次提交同一学生申请提示“存在待审核申请”资助发放选择已认定学生发放资助发放记录出现金额正确分页查询翻页查看发放记录每页条数与总数相符权限控制用学生账号访问审核接口返回 403 无权限这张表的好处是每一条都有明确的“预期结果”演示时照着操作系统行为是预先设计好的现场出现意外也能立刻定位是哪个环节坏了。我每次都把这张表贴在项目的 README 里导师拷走代码后自己也能按表验证这比写一大段功能描述更有说服力。真正花时间写这个系统的时候我做事的顺序一直是“先固定数据库表结构再写接口最后调前端”顺序颠倒的话改一处字段往往要动三层代码。这套流程我做过不止一次希望帮到你。本文还有配套的精品资源点击获取
返回列表