大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Python的学生选课管理系统(FastAPI+Vue3)。

项目介绍

随着高等教育规模的不断扩大与学分制改革的深入推进,传统以人工和纸质表格为主的学生选课方式已经难以满足现代高校教务管理的需要,存在选课效率低、信息更新不及时、数据统计困难、课程容量与上课时间冲突难以控制等问题。为解决上述问题,本文设计并实现了一套基于Python的学生选课管理系统,采用前后端分离的架构模式,后端使用FastAPI框架,前端使用Vue3框架,数据库采用MySQL,构建了一个高效、稳定、易用的在线选课平台。

本系统根据使用对象的不同划分为管理员、教师和学生三种角色。管理员负责用户、学生、教师、院系、课程、选课记录及公告等信息的维护,并可通过可视化图表查看系统运行的统计数据;教师可以查看自己所授课程及选课学生名单,并完成成绩录入;学生可以在线浏览课程、进行选课与退课、查看个人课表和成绩。在选课业务中,系统对课程开放状态、课程容量、重复选课以及上课时间冲突进行了严格校验,保证了选课结果的准确性与合理性。

在技术实现上,后端基于FastAPI构建RESTful风格的接口,利用Pydantic完成数据校验,借助SQLAlchemy ORM实现对象与关系数据库的映射,并采用JWT令牌结合角色权限控制保证接口安全;前端基于Vue3的组合式API进行开发,结合Element Plus组件库、Vue Router路由管理、Pinia状态管理以及ECharts图表库,构建了美观、响应迅速的用户界面。

经过测试,系统功能完整、运行稳定、操作便捷,较好地实现了选课管理的信息化目标,对同类教务管理系统的开发具有一定的参考价值。

源码下载 

链接: https://pan.baidu.com/s/1PiuAdeUpSvu21IJei_qx8Q?pwd=nybh
提取码: 1234

系统展示

核心代码

"""
院系管理路由模块
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session

from database import get_db
from models import User, College
from schemas import CollegeCreate, CollegeUpdate, CollegeInfo, ResponseModel, PageResult
from utils.security import require_roles, get_current_user

router = APIRouter(prefix="/colleges", tags=["院系管理"])


@router.get("", summary="院系列表")
def list_colleges(
    page: int = Query(1, ge=1),
    size: int = Query(100, ge=1, le=100),
    keyword: str = Query(""),
    db: Session = Depends(get_db),
    _: User = Depends(get_current_user),
):
    """分页查询院系列表"""
    query = db.query(College)
    if keyword:
        query = query.filter(
            (College.name.like(f"%{keyword}%")) | (College.code.like(f"%{keyword}%"))
        )
    total = query.count()
    items = query.order_by(College.id).offset((page - 1) * size).limit(size).all()
    return ResponseModel(data=PageResult(
        total=total,
        items=[CollegeInfo.model_validate(c) for c in items],
    ))


@router.post("", summary="创建院系")
def create_college(
    data: CollegeCreate,
    db: Session = Depends(get_db),
    _: User = Depends(require_roles(["admin"])),
):
    """创建新院系"""
    if db.query(College).filter(College.code == data.code).first():
        raise HTTPException(status_code=400, detail="院系编码已存在")
    college = College(**data.model_dump())
    db.add(college)
    db.commit()
    db.refresh(college)
    return ResponseModel(data=CollegeInfo.model_validate(college), message="创建成功")


@router.put("/{college_id}", summary="更新院系")
def update_college(
    college_id: int,
    data: CollegeUpdate,
    db: Session = Depends(get_db),
    _: User = Depends(require_roles(["admin"])),
):
    """更新院系信息"""
    college = db.query(College).filter(College.id == college_id).first()
    if not college:
        raise HTTPException(status_code=404, detail="院系不存在")
    for key, value in data.model_dump(exclude_unset=True).items():
        setattr(college, key, value)
    db.commit()
    db.refresh(college)
    return ResponseModel(data=CollegeInfo.model_validate(college), message="更新成功")


@router.delete("/{college_id}", summary="删除院系")
def delete_college(
    college_id: int,
    db: Session = Depends(get_db),
    _: User = Depends(require_roles(["admin"])),
):
    """删除院系"""
    college = db.query(College).filter(College.id == college_id).first()
    if not college:
        raise HTTPException(status_code=404, detail="院系不存在")
    db.delete(college)
    db.commit()
    return ResponseModel(message="删除成功")
<template>
  <div class="page-container">
    <div class="page-header">
      <h2>课程管理</h2>
      <el-button type="primary" @click="openDialog()"><el-icon><Plus /></el-icon> 新增课程</el-button>
    </div>
    <div class="content-card">
      <div class="search-bar">
        <el-input v-model="query.keyword" placeholder="搜索课程名称/编号" clearable style="width: 220px" @clear="loadData" />
        <el-select v-model="query.course_type" placeholder="类型" clearable style="width: 120px" @change="loadData">
          <el-option label="必修" value="必修" /><el-option label="选修" value="选修" />
        </el-select>
        <el-button type="primary" @click="loadData"><el-icon><Search /></el-icon> 搜索</el-button>
      </div>
      <el-table :data="tableData" stripe border style="width: 100%">
        <el-table-column prop="course_no" label="编号" min-width="100" />
        <el-table-column prop="name" label="课程名称" min-width="150" />
        <el-table-column prop="teacher_name" label="授课教师" min-width="100" />
        <el-table-column prop="college_name" label="院系" min-width="140" />
        <el-table-column prop="credit" label="学分" width="70" />
        <el-table-column prop="course_type" label="类型" width="80">
          <template #default="{ row }"><el-tag :type="row.course_type === '必修' ? 'danger' : ''">{{ row.course_type }}</el-tag></template>
        </el-table-column>
        <el-table-column label="选课/容量" width="100">
          <template #default="{ row }">{{ row.selected_count }}/{{ row.capacity }}</template>
        </el-table-column>
        <el-table-column prop="class_day" label="星期" width="70" />
        <el-table-column prop="class_section" label="节次" width="90" />
        <el-table-column prop="location" label="地点" min-width="120" />
        <el-table-column prop="status" label="状态" width="80">
          <template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? '开放' : '关闭' }}</el-tag></template>
        </el-table-column>
        <el-table-column label="操作" width="160" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" link @click="openDialog(row)">编辑</el-button>
            <el-popconfirm title="确定删除?" @confirm="handleDelete(row.id)">
              <template #reference><el-button type="danger" link>删除</el-button></template>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-wrap">
        <el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @change="loadData" />
      </div>
    </div>

    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑课程' : '新增课程'" width="600px" destroy-on-close>
      <el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
        <el-row :gutter="16">
          <el-col :span="12"><el-form-item label="课程编号" prop="course_no"><el-input v-model="form.course_no" :disabled="isEdit" /></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="课程名称" prop="name"><el-input v-model="form.name" /></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="授课教师" prop="teacher_id">
            <el-select v-model="form.teacher_id" style="width: 100%"><el-option v-for="t in teachers" :key="t.id" :label="t.name" :value="t.id" /></el-select>
          </el-form-item></el-col>
          <el-col :span="12"><el-form-item label="所属院系" prop="college_id">
            <el-select v-model="form.college_id" style="width: 100%"><el-option v-for="c in colleges" :key="c.id" :label="c.name" :value="c.id" /></el-select>
          </el-form-item></el-col>
          <el-col :span="12"><el-form-item label="学分"><el-input-number v-model="form.credit" :min="0.5" :max="10" :step="0.5" style="width: 100%" /></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="学时"><el-input-number v-model="form.hours" :min="16" :max="128" style="width: 100%" /></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" :max="500" style="width: 100%" /></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="课程类型"><el-select v-model="form.course_type" style="width: 100%"><el-option label="必修" value="必修" /><el-option label="选修" value="选修" /></el-select></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="学期"><el-input v-model="form.semester" /></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="星期"><el-select v-model="form.class_day" style="width: 100%"><el-option v-for="d in days" :key="d" :label="d" :value="d" /></el-select></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="节次"><el-input v-model="form.class_section" /></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="地点"><el-input v-model="form.location" /></el-form-item></el-col>
          <el-col :span="24"><el-form-item label="简介"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="开放" inactive-text="关闭" /></el-form-item></el-col>
        </el-row>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
/** 课程管理页面 */
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { courseApi, collegeApi, teacherApi } from '@/api'

const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
const tableData = ref([])
const colleges = ref([])
const teachers = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const isEdit = ref(false)
const editId = ref(null)
const formRef = ref(null)
const query = reactive({ page: 1, size: 10, keyword: '', course_type: '' })
const form = reactive({ course_no: '', name: '', teacher_id: null, college_id: null, credit: 2, hours: 32, capacity: 50, semester: '2025-2026-1', course_type: '选修', class_day: '周一', class_section: '1-2节', location: '', description: '', status: 1 })
const rules = { course_no: [{ required: true, message: '请输入编号' }], name: [{ required: true, message: '请输入名称' }], teacher_id: [{ required: true, message: '请选择教师' }], college_id: [{ required: true, message: '请选择院系' }] }

async function loadData() {
  const res = await courseApi.list(query)
  tableData.value = res.data.items
  total.value = res.data.total
}

async function loadOptions() {
  const [cRes, tRes] = await Promise.all([collegeApi.list({ size: 100 }), teacherApi.list({ size: 100 })])
  colleges.value = cRes.data.items
  teachers.value = tRes.data.items
}

function openDialog(row) {
  isEdit.value = !!row
  editId.value = row?.id
  Object.assign(form, row ? { ...row } : { course_no: '', name: '', teacher_id: null, college_id: null, credit: 2, hours: 32, capacity: 50, semester: '2025-2026-1', course_type: '选修', class_day: '周一', class_section: '1-2节', location: '', description: '', status: 1 })
  dialogVisible.value = true
}

async function handleSubmit() {
  await formRef.value.validate()
  if (isEdit.value) { await courseApi.update(editId.value, form); ElMessage.success('更新成功') }
  else { await courseApi.create(form); ElMessage.success('创建成功') }
  dialogVisible.value = false
  loadData()
}

async function handleDelete(id) {
  await courseApi.remove(id)
  ElMessage.success('删除成功')
  loadData()
}

onMounted(() => { loadData(); loadOptions() })
</script>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐