Python 项目实战 2:落地可上线的高校学生管理系统(两万字完整版,承接避坑指南)

2025博客之星年度评选已开启 10w+人浏览 2.7k人参与

一、项目背景:从 “避坑理论” 到 “学生管理实战”

1.1 真实业务需求(高校辅导员视角大白话拆解)

辅导员日常需要处理的学生管理工作:

要登记学生的学号、姓名、身份证、手机号、班级、专业等信息(不能填假,要校验格式);要给学生录入多门课程的成绩,能自动算平均分、总分;学生退学 / 休学后不能删数据(要保留历史轨迹,用逻辑删除);能批量导入学生信息(从 Excel 一键导入,不用手动录);能导出成绩报表(给教务处的 Excel);系统要可上线(辅导员和学生都能在浏览器访问)。

1.2 技术栈(严格沿用避坑指南推荐的 “简单、流行、安全” 工具)

技术栈用途避坑点关联
Python 3.12后端开发上一篇:Python 版本选择(3.10+)
FastAPIAPI 开发上一篇:高并发选择(异步、自动生成文档)
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,访问:


五、项目上线部署(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% 会遇到的问题)

  1. 问题ModuleNotFoundError: No module named 'openpyxl'原因:Pandas 需要 openpyxl 才能读取 Excel解决pip install openpyxl

  2. 问题SQLAlchemyError: Table 'class' already exists原因:已创建过 class 表,再次运行Base.metadata.create_all()会报错解决:删除旧表或使用Base.metadata.create_all(bind=engine, checkfirst=True)

  3. 问题HTTPException: 422 Unprocessable Entity原因:前端提交的数据不符合 Pydantic 校验规则解决:检查身份证号、手机号、成绩范围是否正确


七、实战小练习(巩固上一篇避坑点)

  1. 给学生表加「邮箱」字段,用 Pydantic 校验邮箱格式;
  2. 给成绩表加「课程学分」字段,统计时计算加权平均分;
  3. 给删除接口加 JWT 权限,只有管理员才能删除学生;
  4. 用 Git 将项目提交到 GitHub/Gitee。

八、总结:承接避坑指南,落地工程化项目

本次项目100% 覆盖上一篇所有避坑点

  • ✅ 命名规范(PEP8)
  • ✅ 逻辑删除(禁止物理删除)
  • ✅ 数据校验(Pydantic)
  • ✅ 连接池(SQLAlchemy)
  • ✅ 防 SQL 注入(ORM)
  • ✅ 日志系统(logging)
  • ✅ 敏感配置(.env)
  • ✅ 工程化目录(模块化)

作为零基础学员,你已经从「避坑理论」跨越到「可上线项目实战」,掌握了企业级 Python 项目的开发流程和规范。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值