news 2026/9/8 6:03:31

前后端分离家校互联系统:Python FastAPI + Vue 3 全栈开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前后端分离家校互联系统:Python FastAPI + Vue 3 全栈开发实战

1. 项目概述与整体构思

1.1 家校互联系统到底在解决什么问题

先说个场景。家里有娃上学的朋友应该都体会过,班级群里每天刷几百条消息,老师发通知、家长问作业、要接龙、要打卡,信息乱成一锅粥。老师这边更是头疼,同一个通知要发家长群、学生群、工作群,统计谁看了没看、谁签了字没签字,全靠人工催。学校管理层想看各班出勤情况、成绩分布,也得等班主任手动汇总。

家校互联管理系统就是干这个的。它把老师、学生、家长三方角色放到同一套系统里,让通知公告有固定入口,让作业、成绩、考勤这些数据有统一的记录和查询通道,不再靠微信群里的“爬楼”和接龙。之所以用 Python 加 Vue 这套组合来做,是因为它既能满足校园场景对数据处理和权限控制的要求,又能提供足够流畅的浏览器端交互体验,后续维护和二次开发的门槛也不高。

这套系统适合谁参考?如果你是正在做毕业设计的学生,或者接了个中小学校信息化定制需求的外包开发者,再或者想了解前后端分离项目落地全流程的 Python 学习者,这文章里的内容都能直接用得上。我会从架构设计、数据库建模、后端接口实现、前端页面开发、联调部署几个环节完整走一遍,把我实际开发中踩过的坑和优化过的细节都写出来。

1.2 为什么是 Python 后端加 Vue 前端

选技术栈之前,我其实对比过几套方案。第一种是 Django 自带模板渲染,前后端不分家,优点是开发快、省事,但问题是页面交互一多,JavaScript 代码就越写越乱,维护成本会失控。第二种是 Node.js 全家桶,Express 或 Koa 加 Vue,开发体验也不错,但如果团队里 Python 更熟练,或者需要用到 pandas、numpy 这类数据处理库(比如做成绩分析),Node.js 就没那么顺手了。

最终我选了 Python 加 Vue 的前后端分离架构。后端用 Flask 或 FastAPI 提供纯 API 接口,前端用 Vue 3 加 Element Plus 搭建页面,通过 HTTP 请求交互数据。这个分工很清晰:后端只负责数据处理、权限校验、业务逻辑,前端只负责页面渲染和用户交互。两边可以并行开发,前端工程师和后端工程师只需要约定好接口文档,互不阻塞。

用生活化的类比来解释:后端就像餐厅后厨,菜品(数据)在这里加工、调味、装盘;前端就像餐厅的菜单和服务员,顾客(用户)能看到什么、点什么菜、菜上桌后长什么样,全是前端的事。两者通过传菜窗口(API 接口)衔接,传菜窗口的规矩(接口格式)定好了,前厅后厨各干各的,互不添乱。

1.3 功能模块的完整拆解

一个能真正落地的家校互联系统,至少要包含以下模块:

用户管理模块:学生、老师、家长三类角色,支持注册、登录、密码找回、个人信息维护。其中家长的账号需要和对应学生绑定,这是家校互联的核心关系。

通知公告模块:老师发布通知,可以选择发送给全班、全年级或指定学生家长;已读未读状态跟踪,谁看了谁没看一目了然,不用再在群里刷屏询问。

作业管理模块:老师按班级发布作业,附上截止时间;学生提交作业状态;家长可以查看作业内容和完成情况,方便在家督促。

成绩管理模块:老师录入考试成绩,系统自动生成个人成绩单和班级成绩分布统计,家长只能看到自己孩子的成绩,避免隐私泄露。

考勤管理模块:记录学生到校、离校时间,支持请假申请和审批流程,家长端可以实时看到孩子在校的到离校状态。

家校沟通模块:站内信或留言板功能,家长和老师可以实时沟通,避免双方都保存对方手机号造成的信息泄露风险。

考虑到实际开发周期,我在做这套系统时把通知公告、作业管理、成绩管理、考勤记录作为核心优先实现,家校沟通拆分到后续迭代版本。这样既保证核心功能完整,又不会让第一版的工程量失控。

2. 数据库设计与核心表结构

2.1 角色权限体系的设计思路

家校互联系统的数据关系比普通管理系统复杂,难在角色之间的层级和关联。我见过不少项目把 teacher、student、parent 各建一张独立表,这样做看似简单,但后续权限控制会很痛苦:一个用户到底属于哪个角色?跨角色操作怎么处理?

我的做法是拆成 user 表和 role 表,再加 user_role 关联表,用户和角色是多对多关系。虽然初期只用了三种固定角色,但多对多设计给未来留了扩展余地,比如年级主任、教务管理员这些新角色,只需要往 role 表插记录,再给用户分配即可,不需要动表结构。

用户表设计如下:

CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, real_name VARCHAR(64) NOT NULL, email VARCHAR(128) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, avatar_url VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

密码字段存的是哈希值而不是明文,我用 Werkzeug 自带的 generate_password_hash 函数处理,它内部使用 PBKDF2 算法加盐。就算数据库被拖走,攻击者也拿不到明文密码,这是最基本的底线。

2.2 核心业务表的关系拆解

围绕“家校互联”这个核心,业务表主要分四块:

学生信息表(student):记录学生基本信息和所属班级。

班级表(class_info):记录年级、班级名称、班主任信息。

家长关联表(parent_student):家长用户和学生之间的绑定关系,这是家校互联的枢纽。一个家长可以绑定多个孩子,一个孩子也可以有多个家长绑定,比如父母双方都注册了账号。

业务数据表:通知表(notice)、作业表(homework)、成绩表(score)、考勤表(attendance)等。

家校关联表的设计我用了中间表而不是直接在 student 表加 parent_id 字段,原因是一个学生可能有父母双方账号,直接加字段就只能存一个家长,另一个家长就没法关联了。用中间表就能完美支持多对多关系。

student 表的核心字段:

CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(32) UNIQUE NOT NULL, class_id INT NOT NULL, gender TINYINT DEFAULT 0, birthday DATE DEFAULT NULL, enroll_date DATE DEFAULT NULL, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (class_id) REFERENCES class_info(id) );

parent_student 中间表:

CREATE TABLE parent_student ( id INT PRIMARY KEY AUTO_INCREMENT, parent_user_id INT NOT NULL, student_id INT NOT NULL, relation VARCHAR(16) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parent_user_id) REFERENCES user(id), FOREIGN KEY (student_id) REFERENCES student(id), UNIQUE KEY uk_parent_student (parent_user_id, student_id) );

2.3 成绩表和考勤表的字段陷阱

成绩表最容易犯的错误是“一个字段存一个学生所有科目的成绩”,比如语文、数学、英语各建一列。这样做第一版确实省事,但后面加科目就得改表结构,成绩分析维度也定死了。我改用 EAV 式的行存储,每条记录表示某个学生在某次考试中某一科目的得分:

CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, exam_name VARCHAR(128) NOT NULL, subject VARCHAR(32) NOT NULL, score DECIMAL(5,2) NOT NULL, full_score DECIMAL(5,2) DEFAULT 100, exam_date DATE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_exam_subject (student_id, exam_name, subject) );

这样设计后,查询某次考试某班所有学生的数学成绩,一条 SQL 就能搞定;统计某个学生历次考试各科成绩变化趋势,也只需要按 student_id 和 exam_name 查询。唯一需要适应的是向程序员同学解释“一行不是一个学生的所有成绩,而是一个学生的单科成绩”。

考勤表我按“每日一条”的方式设计,而不是“每次上下学一条”。因为大多数学校只需要记录学生当天是否到校、是否迟到、是否请假这几个状态,按事件记录会导致一个学生一天产生多条记录,查询时间列表时会很繁琐:

CREATE TABLE attendance ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, attendance_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 1, check_in_time TIME DEFAULT NULL, check_out_time TIME DEFAULT NULL, remark VARCHAR(255) DEFAULT NULL, UNIQUE KEY uk_student_date (student_id, attendance_date) );

status 字段用 0 和 1 两个值就够:0 代表异常或缺勤,1 代表正常。具体异常原因写到 remark 里,比如“病假”“事假”“迟到15分钟”。

3. 后端接口设计与关键技术实现

3.1 框架选型:Flask 还是 FastAPI

我实际开发时最终选了 FastAPI,原因是它原生支持异步、自带 OpenAPI 接口文档、用 Pydantic 做参数校验和序列化,开发效率和代码可读性都比 Flask 高不少。学校场景的并发量不大,异步的优势更多体现在 IO 密集操作上,比如同时调用多个数据源、发送通知邮件等。

Flask 用了十来年,生态成熟、资料多,如果你更熟悉 Flask 也没问题,这套业务逻辑迁移过去并不复杂。核心的差异点在于 FastAPI 用类型注解声明请求和响应的数据结构,Flask 需要手动解析 request 对象。简单说,FastAPI 更适合新手学习,因为它强制你定义数据模型,这个习惯养成了,后面维护代码会轻松很多。

项目结构我按模块分包:

backend/ ├── app/ │ ├── main.py # FastAPI 实例和路由注册 │ ├── config.py # 配置项 │ ├── database.py # 数据库连接和会话管理 │ ├── models/ # SQLAlchemy 模型 │ │ ├── user.py │ │ ├── student.py │ │ ├── notice.py │ │ └── score.py │ ├── schemas/ # Pydantic 数据模型 │ │ ├── user.py │ │ ├── notice.py │ │ └── common.py │ ├── routers/ # 路由处理器 │ │ ├── auth.py │ │ ├── notice.py │ │ └── score.py │ ├── services/ # 业务逻辑层 │ │ └── auth_service.py │ └── utils/ # 工具函数 │ └── dependencies.py ├── requirements.txt └── run.py

3.2 用户认证:JWT 方案怎么落地

家校互联系统的认证方案,我直接选了 JWT(JSON Web Token),而不是 Session。因为前后端分离架构下,后端不能依赖 Cookie 自动携带 Session ID,JWT 把用户身份信息加密后放在请求头 Authorization 字段传给前端,前端拿到后每次请求带上即可。JWT 是无状态的,服务器不需要存储会话数据,水平扩展时不需要同步 Session。

登录接口的核心逻辑:

@router.post("/login") async def login(user_data: LoginRequest, db: Session = Depends(get_db)): user = db.query(User).filter(User.username == user_data.username).first() if not user or not check_password_hash(user.password_hash, user_data.password): raise HTTPException(status_code=400, detail="用户名或密码错误") if user.status != 1: raise HTTPException(status_code=403, detail="账号已被禁用") token = jwt.encode( {"user_id": user.id, "role": get_user_role(user.id, db), "exp": datetime.utcnow() + timedelta(hours=24)}, settings.SECRET_KEY, algorithm="HS256" ) return {"access_token": token, "token_type": "bearer"}

JWT 的过期时间我设置成 2 小时,但前端会做“静默续期”:在请求拦截器里检测 token 剩余有效期,如果低于 30 分钟,后端会下发一个新的 token。用户无感知地持续登录,不需要频繁输入密码。

这里的核心是密码校验。前端传过来的是明文密码,后端再用 check_password_hash 做校验。传输过程必须走 HTTPS,否则等于把密码裸奔在网络里。

3.3 网关与依赖注入:如何保护需要登录的接口

FastAPI 的依赖注入机制在这里发挥了关键作用。我封装了一个 get_current_user 依赖函数,通过解析请求头里的 Authorization 拿到 JWT,解析成功后返回当前用户对象,然后被各个接口声明为依赖:

def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): credentials_exception = HTTPException( status_code=401, detail="登录状态已失效,请重新登录", headers={"WWW-Authenticate": "Bearer"} ) try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) user_id = payload.get("user_id") if user_id is None: raise credentials_exception except jwt.PyJWTError: raise credentials_exception user = db.query(User).filter(User.id == user_id).first() if user is None: raise credentials_exception return user

角色控制用依赖继承实现。比如只有老师能发布通知,就在发布接口上加:

def require_teacher(current_user: User = Depends(get_current_user)): if not is_teacher(current_user.id): raise HTTPException(status_code=403, detail="该操作仅限教师") return current_user

这样每个接口都清楚地声明了权限要求,业务层不需要反复检查身份,代码干净很多。

3.4 通知已读状态怎么高效统计

通知的已读未读统计是个容易忽略但很坑的功能点。如果用“在通知表加一个已读用户数”的字段,更新时会存在并发问题;如果每次统计都扫一遍全量数据,数据量大了会卡。我的方案是单独建 notice_read 关联表:

CREATE TABLE notice_read ( id INT PRIMARY KEY AUTO_INCREMENT, notice_id INT NOT NULL, user_id INT NOT NULL, read_at DATETIME DEFAULT NULL, UNIQUE KEY uk_notice_user (notice_id, user_id) );

学生或家长访问通知详情时,后端自动在这个表插入一条记录。统计已读用户数时,对某个 notice_id 统计这个表的行数就是已读人数,再用班级总人数减去已读数就是未读人数。如果用户重复访问,UNIQUE 约束保证不会插入重复记录,但需要注意处理“已读过再看”的场景,我用了 INSERT ... ON DUPLICATE KEY UPDATE 避免报错:

db.execute( notice_read.insert().values(notice_id=notice_id, user_id=user_id, read_at=datetime.utcnow()) .on_conflict_do_update(constraint="uk_notice_user", set_={"read_at": datetime.utcnow()}) )

3.5 CORS(跨域资源共享)代理的两种方式

开发时前端跑在 5173 端口,后端跑在 8000 端口,浏览器跨域请求会被拦截。解决办法有两种:后端配置 CORS 中间件,或者前端配置开发代理。两者我都做了,但建议理解其区别。

后端配置 CORS 是最终方案,部署后必须开启。我用 fastapi.middleware.cors 的 CORSMiddleware,设置 allow_origins 为前端域名列表、allow_methods 为所有常用方法、allow_headers 为所有请求头:

app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173", "http://你的服务器域名"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

开发时前端配置 Vite 代理可以避免浏览器发 OPTIONS 预检请求,请求由 Node 服务器转发,调试体验更平滑。两种方式并不互斥,我建议开发期用代理,部署后靠后端 CORS 配置。

4. 前端项目搭建与实现细节

4.1 Vue 3 项目初始化与依赖安装

前端我用了 Vue 3 加 Vite 加 Pinia 加 Vue Router,UI 组件库选 Element Plus。用 Vite 而不是 Vue CLI,是因为 Vite 冷启动速度极快,文件修改热更新几乎无延迟,开发体验比 Webpack 时代的 Vue CLI 好了太多。

初始化项目:

npm create vue@latest

Vite 脚手架会交互式询问是否安装 TypeScript、Vue Router、Pinia、ESLint 等,我选择 JavaScript 版本加 Vue Router 和 Pinia。下一步安装依赖并启动开发服务器:

cd jiaxiaotong npm install npm install element-plus axios npm run dev

Element Plus 引入方式我选了完整引入而非按需自动导入,虽然最终打包体积会大一些,但对管理系统这种内部使用的项目,开发效率和稳定性更重要,没必要在首屏加载上较劲。如果确实在意体积,后续可以用 unplugin-vue-components 做按需引入。

4.2 项目目录结构与核心配置

Vue 项目的目录组织如下:

frontend/ ├── src/ │ ├── api/ # 所有后端接口的请求封装 │ │ ├── auth.js │ │ ├── notice.js │ │ ├── score.js │ │ └── http.js # axios 实例和拦截器 │ ├── router/ │ │ └── index.js # 路由表 │ ├── stores/ │ │ └── user.js # Pinia 用户状态管理 │ ├── views/ │ │ ├── login/ │ │ ├── notice/ │ │ ├── score/ │ │ ├── attendance/ │ │ └── dashboard/ │ ├── components/ # 公共组件 │ │ └── Layout.vue │ └── App.vue ├── vite.config.js └── package.json

axios 封装是我比较在意的一个环节。所有请求统一走一个 http.js,里面配置了 baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器自动附加 token,响应拦截器统一处理错误码,比如 401 时自动跳转登录页:

// src/api/http.js import axios from 'axios' import { useUserStore } from '@/stores/user' import router from '@/router' const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 15000 }) http.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) http.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.logout() router.push('/login') } return Promise.reject(error) } )

这个封装能省掉大量重复代码。每条 API 调用只需要写请求方法、路径和参数,不需要关心 token 怎么带、错误怎么处理。

4.3 登录页与路由守卫的实现

登录页是进入系统的第一道门,看起来简单,但交互细节很多。密码框要支持回车提交、表单校验要给出明确错误提示、登录过程中要禁用重复点击。我用 Element Plus 的 el-form 加 rules 做校验,字段必填加格式校验:

<el-form :model="loginForm" :rules="loginRules" ref="loginRef" label-width="0px"> <el-form-item prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="User" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password @keyup.enter="handleLogin" /> </el-form-item> <el-form-item> <el-button type="primary" :loading="loading" @click="handleLogin" style="width: 100%;">登 录</el-button> </el-form-item> </el-form>

路由守卫是防止未登录用户访问系统内部页面的核心机制。Vue Router 的全局前置守卫在每次路由跳转前执行,检查目标路由是否需要认证。我在路由表里给需要保护的页面加上 meta.requiresAuth: true,守卫里判断是否已登录:

router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.meta.requiresAuth) { if (!userStore.token) { next('/login') return } if (to.meta.roles && !to.meta.roles.includes(userStore.role)) { next('/403') return } } next() })

角色控制在前端同样重要。比如老师才能看到“发布通知”按钮,学生家长看不到成绩管理页的录入入口。我用了自定义指令 v-permission,无权限的元素直接移出 DOM,避免只是隐藏按钮导致用户通过调试工具强行触发接口。

4.4 通知模块:列表、详情、已读标记的完整链路

通知模块是麻雀虽小五脏俱全的代表,它串联了列表查询、详情阅读、已读状态跟踪、新增发布这四种典型的 CRUD 操作。先看列表页的核心逻辑:

  • 老师端:加载我发布的全部通知,展示标题、发布时间、已读/未读数。
  • 家长端:加载发送给我的通知列表,展示是否已读的标记。

列表页用 el-table 展示,配合 el-pagination 分页。状态列用 el-tag 显示“已读/未读”,老师可以看汇总数字:

<el-table :data="noticeList" v-loading="loading"> <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip /> <el-table-column prop="created_at" label="发布时间" width="180" /> <el-table-column label="状态" width="100"> <template #default="{ row }"> <el-tag :type="row.is_read ? 'success' : 'warning'">{{ row.is_read ? '已读' : '未读' }}</el-tag> </template> </el-table-column> <el-table-column label="操作" width="120"> <template #default="{ row }"> <el-button type="primary" link @click="openDetail(row)">查看</el-button> </template> </el-table-column> </el-table>

点击“查看”时调用通知详情接口,后端返回通知内容的同时,如果当前用户是接收者且未读过,就写入 notice_read 表标记已读。这里有个细节:详情返回给老师端时应该展示已读用户列表(谁看了、谁没看),给家长端时只返回该家长自己的已读状态,避免家长看到其他家长的隐私信息。

4.5 成绩查看与数据可视化

成绩查看模块体现了前后端配合的价值。家长登录后看自己孩子的成绩,需要的东西很明确:总分、班级排名、各科分数、是否进步。老师端看整个班级的成绩分布,需要平均分、及格率、分数段分布。

后端要按角色返回不同的数据视图。同一个 API 路径,后端通过 JWT 里的角色信息判断返回的是家长视图还是老师视图。家长视图限制只能查自己绑定学生,老师视图可以查整个班级。

成绩折线图展示历次考试各科变化趋势,我用 ECharts 绘制。ECharts 体积不小,按需引入:

import * as echarts from 'echarts/core' import { LineChart, BarChart } from 'echarts/charts' import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers'

这样打包体积从 1MB 降到 400KB 左右,对管理系统完全够用。

4.6 考勤日历与状态标记

考勤模块我用了 Element Plus 的 el-calendar 组件,在日历上按天标记出勤状态:绿色代表正常、红色代表缺勤、黄色代表请假。点击任意日期,弹出当天考勤详情的抽屉,显示到校时间和离校时间。

日历数据从后端接口加载,接口返回指定月份每一天的考勤状态。前端处理时需要注意日历组件渲染的是整个月面板,上个月和下个月的日期也会混入面板,判断日期是否属于当前月份再决定是否展示状态标记,这个坑我后面在常见问题里也会提到。

<el-calendar v-model="currentMonth"> <template #date-cell="{ data }"> <div class="attendance-cell"> <span>{{ data.day.split('-')[2] }}</span> <div v-if="attendanceMap[data.day]"> <el-tag :type="statusTypeMap[attendanceMap[data.day]]" size="small"> {{ statusTextMap[attendanceMap[data.day]] }} </el-tag> </div> </div> </template> </el-calendar>

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

5.1 成就了无数开发者的跨域问题

前后端分离项目,跨域是绕不开的第一道坎。我实际开发时遇到的表现是:前端登录请求发出去了,后端也能收到,但浏览器报 CORS error,请求看不到响应。排查步骤是先在浏览器开发者工具的 Network 面板里看请求的 status 和响应头,确认是否返回了 Access-Control-Allow-Origin 头。

FastAPI 配置完 CORSMiddleware 仍然报错的话,需要检查 allow_origins 是否写成了字符串而不是数组。正确的是列表形式,就算只有一个来源也要用列表。另外 allow_credentials 设成 True 时,allow_origins 不能为 *,必须指定具体域名。这个细节能卡住很多人。

开发代理是另一个隐蔽的坑。Vite 配置了代理之后,前端请求 /api 开头的路径会转发到后端地址,但如果 axios 的 baseURL 直接写成了完整的 http://localhost:8000,代理就失效了。我一开始也这么干过,后来统一把所有请求都写成相对路径 /api/xxx,由代理决定转发到哪个后端,部署时由 Nginx 转发给后端服务,前端代码完全不用改。

5.2 JWT 过期与前端静默续期的坑

JWT 过期后的用户体验是个大问题。如果前端只在拦截到 401 后跳登录页,用户可能刚写完一条通知内容点击保存,结果被强制踢回登录页,输入的内容全部丢失。我的优化方案是在请求拦截器里做 token 有效期检查,距离过期时间小于 30 分钟时,自动调用刷新接口获取新 token,同时缓存当前请求的 Promise,等新 token 拿到后再继续原请求。

刷新接口用 refresh_token 换取新的 access_token,refresh_token 的有效期设成 7 天。用户只要每天打开系统,token 都会自动续期,根本感觉不到过期这回事。七天不登录,理论上 refresh_token 也失效,用户需要重新登录,这个阈值对校内系统是合理的。

5.3 el-calendar 月份判断的经典 Bug

Element Plus 的 el-calendar 会渲染当前日期所在月份的完整六周,包括上个月和下个月的日期。我第一次写考勤模块时,天真地以为传给接口的月份参数能直接拿到所有考勤数据,结果日历上上个月和下个月的日期也显示了考勤状态,而且点击上个月日期时,界面展示的周几次序可能和实际不符。

解决办法是在 v-model 绑定的 currentMonth 变化时,先计算出该月份的第一天和最后一天,接口只返回这个范围内的数据。前端渲染时判断某个日期是否在当前月份,不在就不显示状态标记。这是 Element Plus 日历组件的一个老问题,网上搜“vue element 日历判断月份大于当前月的月份不能选择”能搜出一堆讨论,根本原因就在组件会渲染非当前月份日期。

5.4 打包后 index.html 白屏排查

Vue 项目 npm run build 完成后,直接把 dist 目录扔到服务器,发现访问首页是白屏。控制台报资源 404。原因几乎都是静态资源路径问题。Vite 默认构建后的资源路径是绝对路径 /assets/xxx.js,如果你把前端文件放到服务器子路径,或者用 File 协议直接打开,资源就找不到了。

解决办法是在 vite.config.js 里设置 base: './',这样构建后的资源路径变成相对路径,无论是放根目录还是子目录都能正确加载。

export default defineConfig({ base: './', plugins: [vue()] })

另外一个白屏原因是部署方式选了 history 路由模式,但服务器没有配置 fallback。Vue Router 用 history 模式时,用户刷新 /notice/123 这个路径,服务器会去找这个路径对应的文件,找不到就 404。生产环境我用的是 Nginx,需要加 try_files 配置:

location / { try_files $uri $uri/ /index.html; }

开发时 Vite 内置了对 history 模式的支持,所以这个问题只在部署后才暴露,排查起来颇为折腾。

5.5 Python 环境与依赖安装的错误集锦

后端部署时的环境问题也值得单独记录。最常见的是服务器上 Python 版本过旧,比如 CentOS 自带的 Python 3.6,连 FastAPI 最新版本都不支持。建议用 pyenv 或 conda 管理 Python 版本,我在服务器上安装 Python 3.10 后,用 venv 创建虚拟环境,再 pip install -r requirements.txt。

如果有 C 扩展依赖编译失败,比如 mysqlclient 连接 MySQL 时需要安装系统级开发包,在 Ubuntu 上是 libmysqlclient-dev,在 CentOS 上是 mysql-devel。装不上就下载慢,或者干脆换用 PyMySQL 纯 Python 驱动,兼容性最好。我用 SQLAlchemy 时,把连接串换成了:

DATABASE_URL = "mysql+pymysql://user:password@localhost:3306/jiaxiaotong?charset=utf8mb4"

对中小型系统,PyMySQL 性能完全够用,省掉编译的麻烦。

6. 部署上线与关键调优手段

6.1 后端服务如何平滑启动与守护

后端在服务器上不能直接跑 python run.py 就撒手不管,SSH 断开进程就没了。我用 Gunicorn 加 Uvicorn worker 来跑 FastAPI,Gunicorn 是进程管理器,可以配置 worker 数量,进程崩溃后自动重启:

gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --daemon

-w 4 表示 4 个 worker 进程,校内系统并发量不大,这个配置足够。--daemon 让 Gunicorn 在后台运行,即使关闭 SSH 终端服务也不会停。更重要的是注册成 systemd 服务,服务器重启后能自动拉起:

[Unit] Description=JiaXiaoTong Backend Service After=network.target [Service] User=www-data WorkingDirectory=/var/www/jiaxiaotong/backend ExecStart=/var/www/jiaxiaotong/backend/venv/bin/gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target

6.2 前端静态文件与 Nginx 配置

前端构建完的 dist 目录,我用 Nginx 托管静态文件,同时配置反向代理把 /api 开头的请求转发到后端的 8000 端口。这样一个域名同时服务前端页面和后端接口,不需要额外开放 8000 端口给外部访问,减少攻击面。

完整的 Nginx 配置示例:

server { listen 80; server_name jiaxiaotong.example.com; root /var/www/jiaxiaotong/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { expires 30d; add_header Cache-Control "public, immutable"; } }

这里有个容易踩的小坑:proxy_pass 后面如果带了路径 /,会将 /api/notice/xxx 转发成 /notice/xxx,如果后端路由注册前缀是 /api,则不能带路径,直接写 proxy_pass http://127.0.0.1:8000; 即可。我每次配置都会纠结这个,最佳实践是后端路由加统一的 /api 前缀,Nginx 转发时不要改写路径。

6.3 数据库备份与索引优化

学校系统数据量不会暴涨,但也不代表可以掉以轻心。我设置了定时任务每天凌晨三点用 mysqldump 备份数据库,保留最近七天的备份文件:

mysqldump -u root -p jiaxiaotong --single-transaction --quick | gzip > /var/backups/jiaxiaotong_$(date +%Y%m%d).sql.gz find /var/backups -name "jiaxiaotong_*.sql.gz" -mtime +7 -delete

查询性能上,最容易出现慢查询的场景是通知列表按班级查看已读未读统计。我给外键字段都建了索引,包括 notice_read 表的 notice_id 和 user_id,给 score 表的 student_id 和 exam_name 建了联合索引。索引不是越多越好,每个索引都会拖慢写操作,需要在单表数据量超过一万行以后才响应式地加,而不是一开始就无脑建。

6.4 网络安全基线配置

校园系统涉及未成年人隐私,安全红线必须拉满。后端接口全部走 HTTPS,证书用 Let's Encrypt 免费签发,Nginx 配置 301 跳转把 HTTP 请求导到 HTTPS。用户密码用 PBKDF2 加盐哈希,即使 MySQL 被拖库也无法反解明文密码。

接口层我做了一层简单的频率限制,登录接口限制同一 IP 每分钟最多尝试 10 次,防止暴力破解。通知接口限制每个用户每分钟最多请求 30 次,防止脚本刷接口。FastAPI 的 slowapi 库实现这个只需要几行代码:

from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter @router.post("/login") @limiter.limit("10/minute") async def login(request: Request, user_data: LoginRequest): ...

7. 项目扩展建议与个人心得体会

7.1 后续可以加的功能模块

第一版系统跑通后,可以沿着几个方向扩展。第一个方向是数据可视化大屏,把全校考勤、成绩分布、通知阅读率汇总到一个大屏页面上,给学校管理层展示用。第二个方向是消息推送,通知公告除了站内信以外,可以对接企业微信或钉钉的机器人接口,把关键信息推到老师手机上。

第三个方向是作业拍照上传。现在很多学校的作业都是纸质版,老师想留档就得一张张拍照,很麻烦。如果系统支持学生在 App 或手机浏览器里拍照上传作业,老师在线批改打分,体验会好很多。手机端方案可以用 Vue 的移动端适配,也可以用 H5 在外部的 WebView 环境里运行,核心思路是一致的。

7.2 我自己踩过的最大的坑

这个项目我前前后后改了三个版本,最大的教训不是技术问题,而是需求沟通问题。第一版我按照自己的想法做了很多功能,比如在线聊天、视频会议,结果学校根本用不上,反而是通知已读统计这种我觉得简单到不值得做的功能,他们天天在用。

做定制类系统,一定要先跟用户确认角色权限模型。我第二版重构时,把权限模型从“写死三种角色”改成“用户角色多对多动态分配”,后面加教务管理员、年级组长这些角色时只用加记录,没有改表结构,节省了至少两周的重构时间。

7.3 给后来者的上手建议

如果你准备用这套技术栈做类似的练习或毕设项目,我建议按这个顺序推进:先做后端的大多数接口,用 Postman 或 Swagger 调试通过,再做前端页面。因为后端是数据源头,接口返回的字段定了,前端才能往下写。同时配合 FastAPI 自带的 /docs 接口文档,前后端联调极其顺畅,一个小技巧是保持接口文档接口的打开状态,这样可以时刻对照请求参数和响应格式。

前端开发时,先把 axios 封装和路由守卫拿下,这是骨架,也是后来所有页面的基础设施。然后逐个模块推进,建议从登录功能开始,流程最短但是最核心,通了之后做通知模块,路径最短能覆盖列表和详情。以此为基础,扩展出成绩、考勤等功能,代码结构很容易保持一致,不会出现前面模块乱写、后面模块无从下手的情况。

最后再分享一个小技巧:开发时把后端日志级别调成 DEBUG,MySQL 查询日志也打开,前端报错时能快速定位是接口问题、数据问题还是渲染问题。我调试过很多次才发现前端报的数据 undefined 其实是后端返回的字段名拼错了,这类问题靠日志能节省大量排查时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 6:03:22

OpenCV单目测距实战:从相机标定到实时距离计算

简介&#xff1a;这是一份基于OpenCV与Python实现相机到物体距离测量的极简项目资源&#xff0c;面向计算机视觉初学者与需要快速实现单目测距功能的开发者。资源核心采用三角形相似度原理&#xff0c;使用前需要先标定两个关键参数——标记物体的真实宽度&#xff08;或高度&a…

作者头像 李华
网站建设 2026/9/8 6:03:21

用八种软件结构风格实现KWIC:设计图与代码实战

简介&#xff1a;一份面向软件工程学习者与开发者的KWIC系统实现资料&#xff0c;围绕管道过滤器、虚拟机、仓库、黑板、事件驱动、分层、面向对象、客户端-服务器八种软件结构风格&#xff0c;分别给出可运行的Java实现代码、设计图与要求文档&#xff0c;用于理解不同结构风格…

作者头像 李华
网站建设 2026/9/8 6:03:00

Kotlin中缀函数全解析:语法、优先级、性能与DSL实战

pairOf("id", 1001)和"id" to 1001之间&#xff0c;差的只是几个字符&#xff0c;但读起来的感受完全不一样。第一次在 Kotlin 代码里看到mapOf("name" to "kotlin")的时候&#xff0c;大多数人都会愣一下&#xff1a;这个to是关键字吗…

作者头像 李华
网站建设 2026/9/8 6:02:41

VOC格式中国交通数据集实战:4000张图片转YOLOv8训练全流程

简介&#xff1a;面向自动驾驶与智能交通研究的中国交通数据集&#xff0c;基于PASCAL VOC标准构建&#xff0c;收录4000张涵盖城市道路、高速路与乡村路等多种场景的交通图像&#xff0c;标注了车辆、行人、交通标志、道路等关键元素&#xff0c;可用于目标检测、实例分割、语…

作者头像 李华
网站建设 2026/9/8 6:02:21

自制准直驱执行器:行走机器人关节的力控与调参实战

很多开发者从机械臂、舵机或者普通电机转入行走机器人领域后&#xff0c;最先劝退自己的往往不是步态算法&#xff0c;而是最底层的关节执行器。关节空载响应很好&#xff0c;一带负载就发抖、发热&#xff0c;甚至一受冲击就扫齿&#xff0c;这类现象在四足、双足机器人项目里…

作者头像 李华