分享一套锋哥原创的基于Python的实验室预约管理系统(FastAPI+Vue3)
大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Python的实验室预约管理系统(FastAPI+Vue3)

项目介绍
随着高校教学与科研规模的不断扩大,各类实验室已成为高校开展实践教学、科学研究的重要资源。然而,传统的实验室预约方式仍以人工登记、电话预约或纸质表单为主,普遍存在信息不透明、预约冲突频繁、审批流程繁琐、统计困难等问题,难以满足现代高校对实验室资源高效管理与合理调配的需求。为解决上述问题,本文设计并实现了一个基于 Python 的实验室预约管理系统,采用 FastAPI 与 Vue3 前后端分离架构。
本系统后端采用 Python 语言与高性能异步 Web 框架 FastAPI 进行开发,使用 Pydantic 完成数据校验,通过 SQLAlchemy 操作 MySQL 数据库,并基于 JWT 实现登录认证与权限控制;前端采用 Vue3 框架,结合 Vite 构建工具、Element Plus 组件库、Vue Router 路由、Pinia 状态管理以及 Axios 网络请求,构建了响应式、组件化的用户交互界面。系统按照角色划分为普通用户端与管理员后台,实现了用户注册登录、个人信息管理、实验室浏览查询、在线预约申请、预约审批、我的预约管理、公告管理以及预约统计分析等核心功能。
论文按照软件工程的开发流程,依次完成了系统的需求分析、可行性分析、总体架构设计、功能模块设计、数据库设计以及各功能模块的编码实现与系统测试。测试结果表明,本系统运行稳定、功能完善、界面友好、响应迅速,能够有效规范实验室预约流程,提高实验室资源利用率与管理效率,具有较好的实用价值。
源码下载
链接: https://pan.baidu.com/s/1Rdq9ttTp_9Hdn4fOZUyYsg?pwd=1234
提取码: 1234
系统展示






核心代码
"""
预约管理路由
提供预约提交、查询、取消和审批接口
"""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from database import get_db
from models.user import User
from models.lab import Lab
from models.time_slot import TimeSlot
from models.reservation import Reservation
from schemas.reservation import ReservationCreate, ReservationReview, ReservationResponse
from schemas.common import ResponseModel, PageResult
from utils.security import get_current_user, get_admin_user
router = APIRouter(prefix="/api/reservation", tags=["预约管理"])
def _build_reservation_response(r: Reservation) -> ReservationResponse:
"""构建预约响应对象,填充关联信息"""
resp = ReservationResponse.model_validate(r)
if r.user:
resp.username = r.user.username
resp.real_name = r.user.real_name
if r.lab:
resp.lab_name = r.lab.name
resp.lab_no = r.lab.lab_no
if r.time_slot:
resp.slot_name = r.time_slot.name
return resp
@router.post("/create", summary="提交预约")
def create_reservation(
req: ReservationCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
用户提交实验室预约
校验实验室状态和时间段冲突
"""
# 检查实验室是否存在且可用
lab = db.query(Lab).filter(Lab.id == req.lab_id).first()
if not lab:
raise HTTPException(status_code=404, detail="实验室不存在")
if lab.status != 1:
raise HTTPException(status_code=400, detail="该实验室正在维护中,无法预约")
# 检查时间段是否存在
slot = db.query(TimeSlot).filter(TimeSlot.id == req.slot_id).first()
if not slot:
raise HTTPException(status_code=404, detail="时间段不存在")
# 检查时间冲突(同一实验室、同一日期、同一时间段,待审核或已通过的不允许重复)
conflict = db.query(Reservation).filter(
Reservation.lab_id == req.lab_id,
Reservation.slot_id == req.slot_id,
Reservation.reserve_date == req.reserve_date,
Reservation.status.in_(["pending", "approved"])
).first()
if conflict:
raise HTTPException(status_code=400, detail="该时段已被预约,请选择其他时段")
reservation = Reservation(
user_id=current_user.id,
lab_id=req.lab_id,
slot_id=req.slot_id,
reserve_date=req.reserve_date,
purpose=req.purpose,
status="pending"
)
db.add(reservation)
db.commit()
db.refresh(reservation)
return ResponseModel(message="预约提交成功,等待审核", data=_build_reservation_response(reservation))
@router.get("/my", summary="我的预约")
def my_reservations(
page: int = Query(1, ge=1),
size: int = Query(10, ge=1, le=100),
status: str = Query("", description="状态筛选"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""查询当前用户的预约列表"""
query = db.query(Reservation).filter(Reservation.user_id == current_user.id)
if status:
query = query.filter(Reservation.status == status)
total = query.count()
items = query.order_by(Reservation.id.desc()).offset((page - 1) * size).limit(size).all()
return ResponseModel(data=PageResult(
total=total,
items=[_build_reservation_response(r) for r in items]
))
@router.get("/list", summary="预约列表(管理员)")
def list_reservations(
page: int = Query(1, ge=1),
size: int = Query(10, ge=1, le=100),
status: str = Query("", description="状态筛选"),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
_: User = Depends(get_admin_user)
):
"""管理员分页查询所有预约"""
query = db.query(Reservation)
if status:
query = query.filter(Reservation.status == status)
if keyword:
query = query.join(User).join(Lab).filter(
(User.real_name.like(f"%{keyword}%")) | (Lab.name.like(f"%{keyword}%"))
)
total = query.count()
items = query.order_by(Reservation.id.desc()).offset((page - 1) * size).limit(size).all()
return ResponseModel(data=PageResult(
total=total,
items=[_build_reservation_response(r) for r in items]
))
@router.put("/cancel/{reservation_id}", summary="取消预约")
def cancel_reservation(
reservation_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""用户取消自己的预约(仅待审核状态可取消)"""
reservation = db.query(Reservation).filter(
Reservation.id == reservation_id,
Reservation.user_id == current_user.id
).first()
if not reservation:
raise HTTPException(status_code=404, detail="预约不存在")
if reservation.status != "pending":
raise HTTPException(status_code=400, detail="只能取消待审核的预约")
reservation.status = "canceled"
db.commit()
return ResponseModel(message="预约已取消")
@router.put("/review/{reservation_id}", summary="审核预约(管理员)")
def review_reservation(
reservation_id: int,
req: ReservationReview,
db: Session = Depends(get_db),
_: User = Depends(get_admin_user)
):
"""管理员审核预约(通过/驳回)"""
reservation = db.query(Reservation).filter(Reservation.id == reservation_id).first()
if not reservation:
raise HTTPException(status_code=404, detail="预约不存在")
if reservation.status != "pending":
raise HTTPException(status_code=400, detail="该预约已处理,无法重复审核")
if req.status == "approved":
# 再次检查冲突
conflict = db.query(Reservation).filter(
Reservation.lab_id == reservation.lab_id,
Reservation.slot_id == reservation.slot_id,
Reservation.reserve_date == reservation.reserve_date,
Reservation.status == "approved",
Reservation.id != reservation_id
).first()
if conflict:
raise HTTPException(status_code=400, detail="该时段已有通过的预约,无法批准")
reservation.status = req.status
reservation.reject_reason = req.reject_reason if req.status == "rejected" else None
reservation.review_time = datetime.now()
db.commit()
msg = "审核通过" if req.status == "approved" else "已驳回"
return ResponseModel(message=msg)
@router.get("/check", summary="检查时段是否可用")
def check_availability(
lab_id: int = Query(...),
slot_id: int = Query(...),
reserve_date: str = Query(...),
db: Session = Depends(get_db)
):
"""检查指定实验室、日期、时间段是否可预约"""
conflict = db.query(Reservation).filter(
Reservation.lab_id == lab_id,
Reservation.slot_id == slot_id,
Reservation.reserve_date == reserve_date,
Reservation.status.in_(["pending", "approved"])
).first()
return ResponseModel(data={"available": conflict is None})
<template>
<div class="page-container">
<div class="page-header">
<h2>实验室管理</h2>
<p>管理实验室信息和状态</p>
</div>
<div class="search-bar">
<el-input v-model="keyword" placeholder="搜索名称/编号" clearable style="width: 240px" @keyup.enter="loadData" />
<el-button type="primary" @click="loadData"><el-icon><Search /></el-icon> 搜索</el-button>
<el-button type="success" @click="openDialog()"><el-icon><Plus /></el-icon> 新增实验室</el-button>
</div>
<div class="card-box">
<el-table :data="labs" stripe>
<el-table-column label="图片" width="80">
<template #default="{ row }">
<el-image v-if="row.image" :src="row.image" :preview-src-list="[row.image]" fit="cover" style="width: 50px; height: 50px; border-radius: 4px" />
<el-icon v-else :size="24" color="#c0c4cc"><Picture /></el-icon>
</template>
</el-table-column>
<el-table-column prop="lab_no" label="编号" width="100" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="category" label="类别" width="100" />
<el-table-column prop="location" label="位置" />
<el-table-column prop="capacity" label="容量" width="80" />
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">{{ 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-button type="danger" link @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrap">
<el-pagination v-model:current-page="page" :total="total" layout="total, prev, pager, next" @current-change="loadData" />
</div>
</div>
<el-dialog v-model="dialogVisible" :title="editId ? '编辑实验室' : '新增实验室'" width="560px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="编号" prop="lab_no"><el-input v-model="form.lab_no" /></el-form-item>
<el-form-item label="名称" prop="name"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="类别"><el-input v-model="form.category" /></el-form-item>
<el-form-item label="位置"><el-input v-model="form.location" /></el-form-item>
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" /></el-form-item>
<el-form-item label="图片">
<div class="image-upload">
<el-upload
class="lab-uploader"
:show-file-list="false"
accept="image/jpeg,image/png,image/gif"
:http-request="handleUpload"
>
<img v-if="form.image" :src="form.image" class="lab-preview" />
<el-icon v-else class="lab-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button v-if="form.image" type="danger" link @click="form.image = ''">删除图片</el-button>
</div>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="1">可用</el-radio>
<el-radio :label="0">维护中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
/**
* 实验室管理页面
* 管理员对实验室进行增删改查
*/
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { labApi, uploadApi } from '@/api'
const labs = ref([])
const keyword = ref('')
const page = ref(1)
const total = ref(0)
const dialogVisible = ref(false)
const editId = ref(null)
const formRef = ref(null)
const saving = ref(false)
const form = reactive({ lab_no: '', name: '', category: '', location: '', capacity: 30, image: '', description: '', status: 1 })
const rules = {
lab_no: [{ required: true, message: '请输入编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
}
async function loadData() {
const res = await labApi.list({ page: page.value, size: 10, keyword: keyword.value })
labs.value = res.data.items
total.value = res.data.total
}
function openDialog(row = null) {
editId.value = row?.id || null
Object.assign(form, { lab_no: '', name: '', category: '', location: '', capacity: 30, image: '', description: '', status: 1 })
if (row) Object.assign(form, { ...row, image: row.image || '' })
dialogVisible.value = true
}
async function handleUpload({ file }) {
const formData = new FormData()
formData.append('file', file)
const res = await uploadApi.file(formData)
form.image = res.data.url
ElMessage.success('上传成功')
}
async function handleSave() {
await formRef.value.validate()
saving.value = true
try {
if (editId.value) await labApi.update(editId.value, form)
else await labApi.create(form)
ElMessage.success('保存成功')
dialogVisible.value = false
loadData()
} finally { saving.value = false }
}
async function handleDelete(id) {
await ElMessageBox.confirm('确定删除该实验室吗?', '提示', { type: 'warning' })
await labApi.delete(id)
ElMessage.success('删除成功')
loadData()
}
onMounted(loadData)
</script>
<style scoped>
.pagination-wrap { display: flex; justify-content: flex-end; margin-top: 16px; }
.image-upload {
display: flex;
align-items: flex-end;
gap: 12px;
}
.lab-uploader :deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 8px;
cursor: pointer;
overflow: hidden;
transition: border-color 0.3s;
}
.lab-uploader :deep(.el-upload:hover) {
border-color: #409eff;
}
.lab-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.lab-preview {
width: 120px;
height: 120px;
object-fit: cover;
display: block;
}
</style>
更多推荐



所有评论(0)