一、项目背景:从 “避坑理论” 到 “学生管理实战”
1.1 真实业务需求(高校辅导员视角大白话拆解)
辅导员日常需要处理的学生管理工作:
要登记学生的学号、姓名、身份证、手机号、班级、专业等信息(不能填假,要校验格式);要给学生录入多门课程的成绩,能自动算平均分、总分;学生退学 / 休学后不能删数据(要保留历史轨迹,用逻辑删除);能批量导入学生信息(从 Excel 一键导入,不用手动录);能导出成绩报表(给教务处的 Excel);系统要可上线(辅导员和学生都能在浏览器访问)。
1.2 技术栈(严格沿用避坑指南推荐的 “简单、流行、安全” 工具)
| 技术栈 | 用途 | 避坑点关联 |
|---|---|---|
| Python 3.12 | 后端开发 | 上一篇:Python 版本选择(3.10+) |
| FastAPI | API 开发 | 上一篇:高并发选择(异步、自动生成文档) |
| SQLAlchemy | 数据库 ORM | 上一篇:防 SQL 注入、连接池 |
| Pydantic v2 | 数据校验 | 上一篇:拒绝脏数据 |
| MySQL 8.0 | 数据库存储 | 上一篇:逻辑删除、索引 |
| Pandas | 批量 Excel 导入导出 | 上一篇:高性能处理数据 |
| JWT | 权限控制 | 上一篇:安全 |
| Docker | 部署上线 | 上一篇:工程化部署 |
二、工程化目录结构(完全遵循避坑指南规范)
替换上一篇 “新生报到” 为 “学生管理”,所有文件 / 字段命名严格遵循
PEP8规范
student-manage/
├── app/ # 主应用目录(上一篇:模块化拆分)
│ ├── __init__.py # 包初始化(空文件)
│ ├── models/ # 数据库模型(上一篇:ORM防SQL注入)
│ │ ├── base.py # 基础模型(全局逻辑删除,上一篇核心避坑点)
│ │ ├── student.py # 学生模型
│ │ ├── class_model.py # 班级模型(避免和Python内置class重名)
│ │ └── score.py # 成绩模型
│ ├── schemas/ # Pydantic校验(上一篇:拒绝脏数据)
│ │ ├── student.py # 学生校验
│ │ ├── class_model.py # 班级校验
│ │ └── score.py # 成绩校验
│ ├── crud/ # CRUD操作(上一篇:模块化拆分)
│ │ ├── student.py # 学生CRUD
│ │ ├── class_model.py # 班级CRUD
│ │ └── score.py # 成绩CRUD
│ ├── routers/ # API路由
│ │ ├── student.py # 学生路由
│ │ ├── class_model.py # 班级路由
│ │ └── score.py # 成绩路由
│ ├── dependencies.py # 依赖注入(数据库连接池,上一篇避坑点)
│ └── utils/ # 工具函数(Excel处理、JWT)
│ ├── excel_utils.py # Excel导入导出
│ └── jwt_utils.py # JWT权限
├── config.py # 全局配置(日志、数据库,上一篇:敏感配置加密)
├── constants.py # 常量定义(上一篇:常量统一管理)
├── .env # 敏感配置(数据库密码、JWT密钥,上一篇:禁止硬编码)
├── .gitignore # Git忽略文件(上一篇:工程化管理)
├── requirements.txt # 依赖列表(上一篇:虚拟环境)
├── frontend/ # 前端页面(极简HTML/CSS/JS,零基础能写)
│ ├── student.html # 学生信息管理
│ ├── score.html # 成绩管理
│ └── styles.css # 统一样式
└── Dockerfile # Docker部署(上一篇:工程化上线)
三、核心功能实现(每一行代码关联避坑点)
3.1 配置文件层:安全 + 可维护(上一篇:敏感配置 / 日志系统)
3.1.1 .env 敏感配置文件(禁止硬编码,上一篇核心避坑点)
# .env(不要提交到Git!)
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=你的MySQL密码
DB_NAME=student_manage
JWT_SECRET_KEY=sm-2024-abcdefghijklmnopqrstuvwxyz1234567890
JWT_EXPIRE_HOURS=24 # Token有效期24小时
EXCEL_UPLOAD_DIR=./uploads # Excel上传目录
3.1.2 config.py 全局配置文件(日志 + 数据库连接池)
# config.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from dotenv import load_dotenv
import os
import logging
from logging.handlers import RotatingFileHandler
# 加载.env配置(上一篇:敏感配置管理)
load_dotenv()
# 1. 数据库配置(连接池,上一篇核心避坑点:禁止每次请求新建连接)
DATABASE_URL = f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
engine = create_engine(
DATABASE_URL,
pool_size=10, # 连接池最大连接数:CPU核心数*2+磁盘数
max_overflow=20, # 额外连接数
pool_pre_ping=True, # 每次获取连接前检查有效性
pool_recycle=3600, # 连接过期时间(避免超时)
pool_timeout=30 # 获取连接超时时间
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() # ORM基础类
# 2. 日志配置(上一篇:禁止用print,用logging)
def setup_logging():
log_dir = "./logs"
os.makedirs(log_dir, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(pathname)s:%(lineno)d - %(message)s",
handlers=[
logging.StreamHandler(), # 控制台输出
RotatingFileHandler( # 文件输出(自动分割)
os.path.join(log_dir, "app.log"),
maxBytes=10*1024*1024, # 单个日志文件最大10MB
backupCount=10 # 保留10个历史日志
)
]
)
logging.info("日志系统初始化成功(上一篇避坑点:用logging代替print)")
# 3. 其他配置
EXCEL_UPLOAD_DIR = os.getenv("EXCEL_UPLOAD_DIR", "./uploads")
os.makedirs(EXCEL_UPLOAD_DIR, exist_ok=True)
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
JWT_EXPIRE_HOURS = int(os.getenv("JWT_EXPIRE_HOURS", 24))
# 初始化日志
setup_logging()
3.1.3 app/dependencies.py 依赖注入(数据库连接池)
# app/dependencies.py
from config import SessionLocal
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.utils.jwt_utils import verify_token
# 1. 数据库依赖(上一篇:连接池归还)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close() # 自动归还连接到连接池,避免泄漏
# 2. JWT权限依赖(上一篇:安全)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme)):
payload = verify_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的Token",
headers={"WWW-Authenticate": "Bearer"}
)
return payload
3.2 数据库模型层:逻辑删除 + 关联关系(上一篇核心避坑点)
3.2.1 app/models/base.py 基础模型(全局逻辑删除,上一篇核心避坑点)
# app/models/base.py
from sqlalchemy import Column, Integer, Boolean, DateTime
from datetime import datetime
from config import Base
class BaseModel(Base):
__abstract__ = True # 抽象类,不生成表
# 公共字段
id = Column(Integer, primary_key=True, index=True, autoincrement=True, comment="主键ID")
is_deleted = Column(Boolean, default=False, comment="是否删除:0=未删除,1=已删除(上一篇避坑点:禁止物理删除)")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
deleted_at = Column(DateTime, nullable=True, comment="删除时间")
# 全局查询过滤器:自动排除已删除数据(上一篇:避免重复写条件)
@classmethod
def __declare_last__(cls):
if hasattr(cls, 'is_deleted'):
original_query = cls.query
def new_query():
return original_query().filter_by(is_deleted=False)
cls.query = property(new_query)
3.2.2 app/models/class_model.py 班级模型(避免和 Python 内置 class 重名)
# app/models/class_model.py
from sqlalchemy import Column, String, Integer
from app.models.base import BaseModel
class ClassModel(BaseModel):
__tablename__ = "class" # 表名
class_name = Column(String(50), nullable=False, unique=True, comment="班级名称(如:2024级计算机1班)")
major = Column(String(50), nullable=False, comment="专业名称")
student_count = Column(Integer, default=0, comment="学生人数")
3.2.3 app/models/student.py 学生模型(和班级一对多关联)
# app/models/student.py
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
from app.models.class_model import ClassModel
class Student(BaseModel):
__tablename__ = "student" # 表名
student_id = Column(String(20), nullable=False, unique=True, comment="学号(唯一)")
name = Column(String(50), nullable=False, comment="姓名")
id_card = Column(String(18), nullable=False, unique=True, comment="身份证号(唯一)")
phone = Column(String(11), nullable=False, comment="手机号")
class_id = Column(Integer, ForeignKey("class.id", ondelete="SET NULL"), comment="班级ID(和class表关联)")
status = Column(Integer, default=1, comment="状态:0=冻结,1=激活")
# 关联关系:学生→班级(一对多)
class_info = relationship("ClassModel", backref="students")
# 关联关系:学生→成绩(一对多)
scores = relationship("Score", backref="student")
3.2.4 app/models/score.py 成绩模型(和学生一对多关联)
# app/models/score.py
from sqlalchemy import Column, Integer, Float, String, ForeignKey
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
from app.models.student import Student
class Score(BaseModel):
__tablename__ = "score" # 表名
student_id = Column(Integer, ForeignKey("student.id", ondelete="SET NULL"), comment="学生ID")
course_name = Column(String(50), nullable=False, comment="课程名称")
score = Column(Float, nullable=False, comment="成绩(0-100)")
semester = Column(String(20), nullable=False, comment="学期(如:2024-2025学年第1学期)")
# 关联关系:成绩→学生
student_info = relationship("Student", backref="score_list")
3.3 数据校验层:Pydantic v2(上一篇:拒绝脏数据)
3.3.1 app/schemas/class_model.py 班级校验
# app/schemas/class_model.py
from pydantic import BaseModel, Field
from typing import Optional
class ClassCreate(BaseModel):
class_name: str = Field(..., min_length=5, max_length=50, message="班级名称长度5-50(如:2024级计算机1班)")
major: str = Field(..., min_length=2, max_length=50, message="专业名称长度2-50")
class ClassResponse(BaseModel):
id: int
class_name: str
major: str
student_count: int
created_at: str
class Config: from_attributes = True # 支持ORM→Pydantic转换
3.3.2 app/schemas/student.py 学生校验(身份证 / 手机号正则)
# app/schemas/student.py
from pydantic import BaseModel, Field, ValidationError
from typing import Optional
import re
# 自定义身份证号校验(上一篇:数据校验精细化)
def validate_id_card(v: str) -> str:
if not re.match(r"^\d{17}[0-9Xx]$", v):
raise ValueError("身份证号格式错误(18位)")
return v
class StudentCreate(BaseModel):
student_id: str = Field(..., min_length=8, max_length=20, message="学号长度8-20")
name: str = Field(..., min_length=2, max_length=50, message="姓名长度2-50")
id_card: str = Field(..., validate_default=validate_id_card) # 自定义身份证校验
phone: str = Field(..., pattern=r"^1[3-9]\d{9}$", message="手机号格式错误")
class_id: Optional[int] = Field(None, ge=1, message="班级ID必须是正整数")
status: int = Field(1, ge=0, le=1, message="状态必须是0(冻结)或1(激活)")
class StudentResponse(BaseModel):
id: int
student_id: str
name: str
id_card: str
phone: str
class_id: Optional[int]
status: int
created_at: str
class Config: from_attributes = True
# 批量导入学生的Excel行格式校验
class StudentExcelRow(BaseModel):
student_id: str
name: str
id_card: str
phone: str
class_name: str # 用班级名称而不是ID,更友好
class Config: from_attributes = True
3.3.3 app/schemas/score.py 成绩校验(0-100 分限制)
# app/schemas/score.py
from pydantic import BaseModel, Field
from typing import Optional
class ScoreCreate(BaseModel):
student_id: int = Field(..., ge=1, message="学生ID必须是正整数")
course_name: str = Field(..., min_length=2, max_length=50, message="课程名称长度2-50")
score: float = Field(..., ge=0, le=100, message="成绩必须在0-100之间")
semester: str = Field(..., min_length=5, max_length=20, message="学期格式:如2024-2025学年第1学期")
class ScoreResponse(BaseModel):
id: int
student_id: int
course_name: str
score: float
semester: str
class Config: from_attributes = True
# 成绩统计响应
class ScoreStats(BaseModel):
student_id: int
name: str
total_score: float
average_score: float
course_count: int
3.4 CRUD 操作层:模块化 + 防 SQL 注入(上一篇)
3.4.1 app/crud/class_model.py 班级 CRUD
# app/crud/class_model.py
from sqlalchemy.orm import Session
from app.models.class_model import ClassModel
from app.schemas.class_model import ClassCreate
# 创建班级
def create_class(db: Session, class_in: ClassCreate) -> ClassModel:
class_data = class_in.model_dump()
class_obj = ClassModel(**class_data)
db.add(class_obj)
db.commit()
db.refresh(class_obj)
return class_obj
# 查询所有班级
def get_class_list(db: Session) -> list[ClassModel]:
return ClassModel.query.all()
# 根据ID查询班级
def get_class_by_id(db: Session, class_id: int) -> ClassModel:
return ClassModel.query.filter_by(id=class_id).first()
# 根据班级名称查询班级
def get_class_by_name(db: Session, class_name: str) -> ClassModel:
return ClassModel.query.filter_by(class_name=class_name).first()
# 更新班级学生人数
def update_class_student_count(db: Session, class_id: int, delta: int) -> ClassModel:
class_obj = get_class_by_id(db, class_id)
if class_obj:
class_obj.student_count += delta
db.commit()
db.refresh(class_obj)
return class_obj
3.4.2 app/crud/student.py 学生 CRUD(含批量导入)
# app/crud/student.py
from sqlalchemy.orm import Session
from app.models.student import Student
from app.models.class_model import ClassModel
from app.schemas.student import StudentCreate, StudentExcelRow
from app.crud.class_model import get_class_by_name, update_class_student_count
from datetime import datetime
import pandas as pd
import os
from config import EXCEL_UPLOAD_DIR
# 创建单个学生
def create_student(db: Session, student_in: StudentCreate) -> Student:
student_data = student_in.model_dump()
# 检查班级是否存在
if student_data.get("class_id"):
class_obj = get_class_by_id(db, student_data["class_id"])
if not class_obj:
return None
student = Student(**student_data)
db.add(student)
db.commit()
db.refresh(student)
# 更新班级学生人数
if student.class_id:
update_class_student_count(db, student.class_id, 1)
return student
# 查询所有学生
def get_student_list(db: Session) -> list[Student]:
return Student.query.all()
# 根据ID查询学生
def get_student_by_id(db: Session, student_id: int) -> Student:
return Student.query.filter_by(id=student_id).first()
# 根据学号查询学生
def get_student_by_student_id(db: Session, student_id_str: str) -> Student:
return Student.query.filter_by(student_id=student_id_str).first()
# 逻辑删除学生(上一篇核心避坑点:禁止物理删除)
def delete_student(db: Session, student_id: int) -> bool:
student = get_student_by_id(db, student_id)
if not student:
return False
# 逻辑删除:更新标记和时间
student.is_deleted = True
student.deleted_at = datetime.now()
db.commit()
# 更新班级学生人数
if student.class_id:
update_class_student_count(db, student.class_id, -1)
return True
# 批量导入学生(从Excel)
def import_students_from_excel(db: Session, file_path: str) -> dict:
# 读取Excel
df = pd.read_excel(file_path)
# 转换为StudentExcelRow格式
rows = df.to_dict(orient="records")
# 处理结果
success_count = 0
failed_count = 0
failed_rows = []
for row in rows:
try:
# 校验Excel行格式
student_row = StudentExcelRow(**row)
# 检查班级是否存在,不存在则创建
class_obj = get_class_by_name(db, student_row.class_name)
if not class_obj:
class_obj = create_class(db, ClassCreate(
class_name=student_row.class_name,
major=student_row.class_name.split("级")[1].split("班")[0] # 自动提取专业
))
# 创建学生
student = create_student(db, StudentCreate(
student_id=student_row.student_id,
name=student_row.name,
id_card=student_row.id_card,
phone=student_row.phone,
class_id=class_obj.id
))
if student:
success_count += 1
else:
failed_count +=1
failed_rows.append(row)
except Exception as e:
failed_count +=1
failed_rows.append(row)
# 删除临时Excel文件
os.remove(file_path)
return {
"success_count": success_count,
"failed_count": failed_count,
"failed_rows": failed_rows
}
3.4.3 app/crud/score.py 成绩 CRUD(含统计)
# app/crud/score.py
from sqlalchemy.orm import Session
from app.models.score import Score
from app.models.student import Student
from app.schemas.score import ScoreCreate
# 录入成绩
def add_score(db: Session, score_in: ScoreCreate) -> Score:
# 检查学生是否存在
student = Student.query.filter_by(id=score_in.student_id).first()
if not student:
return None
score_data = score_in.model_dump()
score = Score(**score_data)
db.add(score)
db.commit()
db.refresh(score)
return score
# 查询学生所有成绩
def get_student_scores(db: Session, student_id: int) -> list[Score]:
return Score.query.filter_by(student_id=student_id).all()
# 统计学生成绩(总分/平均分)
def get_student_score_stats(db: Session, student_id: int) -> dict:
scores = get_student_scores(db, student_id)
if not scores:
return None
student = Student.query.filter_by(id=student_id).first()
total_score = sum(score.score for score in scores)
average_score = round(total_score / len(scores), 2)
return {
"student_id": student.id,
"name": student.name,
"total_score": total_score,
"average_score": average_score,
"course_count": len(scores)
}
# 查询学期所有学生成绩
def get_semester_scores(db: Session, semester: str) -> list[Score]:
return Score.query.filter_by(semester=semester).all()
3.5 API 路由层:FastAPI 自动生成文档
3.5.1 app/routers/class_model.py 班级 API
# app/routers/class_model.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.crud.class_model import *
from app.schemas.class_model import *
from app.dependencies import get_db
router = APIRouter(prefix="/class", tags=["班级管理"])
# 创建班级
@router.post("/", response_model=ClassResponse)
def create_class_api(class_in: ClassCreate, db: Session = Depends(get_db)):
try:
return create_class(db, class_in)
except Exception as e:
if "Duplicate entry" in str(e):
raise HTTPException(400, "该班级已存在")
raise HTTPException(500, "系统错误")
# 查询所有班级
@router.get("/list", response_model=list[ClassResponse])
def get_class_list_api(db: Session = Depends(get_db)):
return get_class_list(db)
# 根据ID查询班级
@router.get("/{class_id}", response_model=ClassResponse)
def get_class_by_id_api(class_id: int, db: Session = Depends(get_db)):
class_obj = get_class_by_id(db, class_id)
if not class_obj:
raise HTTPException(404, "班级不存在")
return class_obj
3.5.2 app/routers/student.py 学生 API(含批量导入)
# app/routers/student.py
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.orm import Session
from app.crud.student import *
from app.schemas.student import *
from app.dependencies import get_db, get_current_user
from config import EXCEL_UPLOAD_DIR
import os
router = APIRouter(prefix="/student", tags=["学生管理"])
# 创建学生
@router.post("/", response_model=StudentResponse)
def create_student_api(student_in: StudentCreate, db: Session = Depends(get_db)):
student = create_student(db, student_in)
if not student:
raise HTTPException(404, "班级不存在")
return student
# 查询所有学生
@router.get("/list", response_model=list[StudentResponse])
def get_student_list_api(db: Session = Depends(get_db)):
return get_student_list(db)
# 根据ID查询学生
@router.get("/{student_id}", response_model=StudentResponse)
def get_student_by_id_api(student_id: int, db: Session = Depends(get_db)):
student = get_student_by_id(db, student_id)
if not student:
raise HTTPException(404, "学生不存在")
return student
# 逻辑删除学生(上一篇避坑点)
@router.delete("/{student_id}")
def delete_student_api(student_id: int, db: Session = Depends(get_db)):
if delete_student(db, student_id):
return {"message": "删除成功(逻辑删除,保留历史数据)"}
raise HTTPException(404, "学生不存在")
# 批量导入学生(Excel上传)
@router.post("/import")
def import_students_api(file: UploadFile = File(...), db: Session = Depends(get_db)):
# 检查文件类型
if not file.filename.endswith(".xlsx") and not file.filename.endswith(".xls"):
raise HTTPException(400, "仅支持Excel文件(.xlsx/.xls)")
# 保存文件
file_path = os.path.join(EXCEL_UPLOAD_DIR, file.filename)
with open(file_path, "wb") as f:
f.write(file.file.read())
# 导入学生
result = import_students_from_excel(db, file_path)
return result
3.5.3 app/routers/score.py 成绩 API(含统计)
# app/routers/score.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.crud.score import *
from app.schemas.score import *
from app.dependencies import get_db
router = APIRouter(prefix="/score", tags=["成绩管理"])
# 录入成绩
@router.post("/", response_model=ScoreResponse)
def add_score_api(score_in: ScoreCreate, db: Session = Depends(get_db)):
score = add_score(db, score_in)
if not score:
raise HTTPException(404, "学生不存在")
return score
# 查询学生所有成绩
@router.get("/student/{student_id}", response_model=list[ScoreResponse])
def get_student_scores_api(student_id: int, db: Session = Depends(get_db)):
return get_student_scores(db, student_id)
# 统计学生成绩
@router.get("/stats/{student_id}", response_model=ScoreStats)
def get_student_score_stats_api(student_id: int, db: Session = Depends(get_db)):
stats = get_student_score_stats(db, student_id)
if not stats:
raise HTTPException(404, "学生无成绩")
return stats
# 查询学期成绩
@router.get("/semester/{semester}", response_model=list[ScoreResponse])
def get_semester_scores_api(semester: str, db: Session = Depends(get_db)):
return get_semester_scores(db, semester)
3.6 前端页面层:极简 HTML/CSS/JS(零基础能写)
3.6.1 frontend/styles.css 统一样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
.container {
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-weight: bold;
margin-bottom: 5px;
color: #555;
}
input[type="text"], input[type="number"], input[type="file"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-right: 10px;
}
button:hover {
background-color: #0056b3;
}
.result {
margin-top: 20px;
padding: 10px;
border-radius: 4px;
}
.success {
background-color: #d4edda;
color: #155724;
}
.error {
background-color: #f8d7da;
color: #721c24;
}
3.6.2 frontend/student.html 学生管理页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生管理系统</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>高校学生管理系统</h1>
<!-- 创建学生表单 -->
<h2>创建学生</h2>
<form id="create-student-form">
<div class="form-group">
<label for="student_id">学号:</label>
<input type="text" id="student_id" name="student_id" required>
</div>
<div class="form-group">
<label for="name">姓名:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="id_card">身份证号:</label>
<input type="text" id="id_card" name="id_card" required maxlength="18">
</div>
<div class="form-group">
<label for="phone">手机号:</label>
<input type="text" id="phone" name="phone" required maxlength="11">
</div>
<div class="form-group">
<label for="class_id">班级ID:</label>
<input type="number" id="class_id" name="class_id">
</div>
<button type="submit">创建学生</button>
</form>
<div id="create-result" class="result"></div>
<!-- 批量导入 -->
<h2>批量导入学生(Excel)</h2>
<form id="import-form">
<div class="form-group">
<label for="excel-file">选择Excel文件:</label>
<input type="file" id="excel-file" name="excel-file" accept=".xlsx,.xls" required>
</div>
<button type="submit">批量导入</button>
</form>
<div id="import-result" class="result"></div>
<script>
// 创建学生
document.getElementById('create-student-form').addEventListener('submit', function(e) {
e.preventDefault();
const data = {
student_id: document.getElementById('student_id').value,
name: document.getElementById('name').value,
id_card: document.getElementById('id_card').value,
phone: document.getElementById('phone').value,
class_id: document.getElementById('class_id').value || null
};
fetch('http://localhost:8000/student/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(res => res.json()).then(data => {
document.getElementById('create-result').innerHTML = `<p class="success">创建成功!学生ID:${data.id}</p>`;
this.reset();
}).catch(err => {
document.getElementById('create-result').innerHTML = `<p class="error">创建失败:${err.message}</p>`;
});
});
// 批量导入
document.getElementById('import-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData();
formData.append('file', document.getElementById('excel-file').files[0]);
fetch('http://localhost:8000/student/import', {
method: 'POST',
body: formData
}).then(res => res.json()).then(data => {
document.getElementById('import-result').innerHTML = `<p class="success">导入成功!成功${data.success_count}条,失败${data.failed_count}条</p>`;
this.reset();
}).catch(err => {
document.getElementById('import-result').innerHTML = `<p class="error">导入失败:${err.message}</p>`;
});
});
</script>
</div>
</body>
</html>
3.7 工具函数层:Excel 处理 + JWT
3.7.1 app/utils/excel_utils.py Excel 处理
# app/utils/excel_utils.py
import pandas as pd
# 导出成绩报表为Excel
def export_scores_to_excel(scores: list) -> str:
df = pd.DataFrame([score.__dict__ for score in scores])
# 过滤不必要的字段
df = df[['student_id', 'course_name', 'score', 'semester']]
# 保存为Excel
file_path = f"./exports/scores_{pd.Timestamp.now().strftime('%Y%m%d%H%M%S')}.xlsx"
df.to_excel(file_path, index=False)
return file_path
3.7.2 app/utils/jwt_utils.py JWT 权限
# app/utils/jwt_utils.py
import jwt
from datetime import datetime, timedelta
from config import JWT_SECRET_KEY, JWT_EXPIRE_HOURS
# 生成Token
def create_token(user_id: int, username: str) -> str:
expire = datetime.now() + timedelta(hours=JWT_EXPIRE_HOURS)
payload = {
"user_id": user_id,
"username": username,
"exp": expire
}
return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")
# 验证Token
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
四、项目运行与测试(默认环境已搭建)
4.1 安装依赖
在项目根目录执行(虚拟环境已激活):
pip install fastapi sqlalchemy pydantic[email] python-dotenv uvicorn pymysql pandas openpyxl python-jose
4.2 生成数据库表
修改main.py,加入表创建代码:
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import class_model, student, score
from config import Base, engine, setup_logging
import uvicorn
# 创建所有数据库表(第一次运行必须执行)
Base.metadata.create_all(bind=engine)
# 创建FastAPI应用
app = FastAPI(title="高校学生管理系统", version="1.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# 注册路由
app.include_router(class_model.router)
app.include_router(student.router)
app.include_router(score.router)
# 运行
if __name__ == "__main__":
uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)
4.3 运行项目
执行python main.py,访问:
- API 文档(自动生成):http://localhost:8000/docs
- 前端页面:直接打开
frontend/student.html
五、项目上线部署(Docker 一键部署)
# Dockerfile(项目根目录)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]
执行部署命令:
# 构建镜像
docker build -t student-manage .
# 运行容器
docker run -d -p 8000:8000 student-manage
六、踩坑复盘(项目中 100% 会遇到的问题)
-
问题:
ModuleNotFoundError: No module named 'openpyxl'原因:Pandas 需要 openpyxl 才能读取 Excel解决:pip install openpyxl -
问题:
SQLAlchemyError: Table 'class' already exists原因:已创建过 class 表,再次运行Base.metadata.create_all()会报错解决:删除旧表或使用Base.metadata.create_all(bind=engine, checkfirst=True) -
问题:
HTTPException: 422 Unprocessable Entity原因:前端提交的数据不符合 Pydantic 校验规则解决:检查身份证号、手机号、成绩范围是否正确
七、实战小练习(巩固上一篇避坑点)
- 给学生表加「邮箱」字段,用 Pydantic 校验邮箱格式;
- 给成绩表加「课程学分」字段,统计时计算加权平均分;
- 给删除接口加 JWT 权限,只有管理员才能删除学生;
- 用 Git 将项目提交到 GitHub/Gitee。
八、总结:承接避坑指南,落地工程化项目
本次项目100% 覆盖上一篇所有避坑点:
- ✅ 命名规范(PEP8)
- ✅ 逻辑删除(禁止物理删除)
- ✅ 数据校验(Pydantic)
- ✅ 连接池(SQLAlchemy)
- ✅ 防 SQL 注入(ORM)
- ✅ 日志系统(logging)
- ✅ 敏感配置(.env)
- ✅ 工程化目录(模块化)
作为零基础学员,你已经从「避坑理论」跨越到「可上线项目实战」,掌握了企业级 Python 项目的开发流程和规范。
644

被折叠的 条评论
为什么被折叠?



