大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Python的微信小程序健身房预约管理系统(FastAPI+Vue3) 。

项目介绍
随着全民健身战略持续推进,城市健身房在课程排期、会员预约和会员卡销售等日常运营中,对信息化手段的依赖明显增强。传统的前台口头预约、纸质登记和表格统计方式,容易出现名额冲突、信息不透明和数据难以及时汇总等问题,难以满足会员随时随地预约课程的需求。针对上述问题,本文设计并实现了一套微信小程序健身房预约管理系统。系统采用前后端分离架构:会员端基于微信小程序实现注册登录、课程浏览、在线预约、会员卡购买、签到评价和个人中心等功能;管理端基于 Vue3 与 Element Plus 实现数据统计、会员教练课程排期维护、预约订单管理以及公告评价管理;服务端基于 Python 语言和 FastAPI 框架提供 RESTful 接口,结合 JWT 完成身份认证,使用 SQLAlchemy 访问 MySQL 数据库 db_gym。
在需求分析阶段,本文从可行性、功能需求和非功能需求三个方面梳理了会员、管理员两类角色的业务目标,并给出系统总体功能结构。在设计阶段,采用 Chen 氏 E-R 方法分别绘制各实体的属性图以及实体之间的关系图,完成了管理员、会员用户、教练、课程、课程排期、预约、会员卡套餐、订单、用户会员卡、公告和评价共十一张数据表的逻辑结构与物理结构设计。在实现阶段,重点阐述了 FastAPI 路由与中间件、预约扣次与名额控制、模拟支付生成会员卡、Vue3 管理端仪表盘以及小程序端登录注册等关键模块的实现过程,并给出相应核心代码。测试结果表明,系统主要业务流程运行稳定,预约名额控制与会员卡次数扣减逻辑正确,能够为中小型健身房提供一套可用的数字化预约管理方案。
源码下载
链接: https://pan.baidu.com/s/1VHGffQwmJpFcB3NyKepvSA?pwd=1234
提取码: 1234
系统展示








核心代码
"""
评价业务实现类
"""
from datetime import datetime
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from common.exceptions import ServiceException
from deps import LoginUser
from models import Comment, Reservation, User
from schemas import CommentDTO, CommentVO
from services.reservation_service import reservation_service
class CommentService:
"""评价业务"""
def fill_comment_detail(self, db: Session, comment: Comment) -> CommentVO:
"""填充评价关联信息"""
vo = CommentVO.model_validate(comment)
if comment.user_id:
user = db.query(User).filter(User.id == comment.user_id).first()
if user is not None:
vo.nickname = user.nickname
if comment.reservation_id:
reservation = db.query(Reservation).filter(Reservation.id == comment.reservation_id).first()
if reservation is not None:
detail = reservation_service.fill_reservation_detail(db, reservation)
vo.course_name = detail.course_name
return vo
def add_comment(self, db: Session, login_user: LoginUser, dto: CommentDTO) -> None:
"""添加评价(仅已完成预约可评价)"""
if login_user.role != "user":
raise ServiceException(403, "请先登录会员账号")
reservation = db.query(Reservation).filter(Reservation.id == dto.reservation_id).first()
if reservation is None:
raise ServiceException("预约不存在")
if login_user.user_id != reservation.user_id:
raise ServiceException(403, "无权评价他人预约")
if reservation.status is None or reservation.status != 3:
raise ServiceException("仅已完成预约可评价")
count = db.query(Comment).filter(Comment.reservation_id == dto.reservation_id).count()
if count > 0:
raise ServiceException("该预约已评价,请勿重复提交")
comment = Comment(
user_id=login_user.user_id,
reservation_id=dto.reservation_id,
score=dto.score,
content=dto.content,
create_time=datetime.now(),
)
db.add(comment)
db.commit()
def page_list(
self,
db: Session,
page_num: int,
page_size: int,
user_id: Optional[int],
reservation_id: Optional[int],
) -> Tuple[int, List[CommentVO]]:
"""分页查询评价(含详情)"""
q = db.query(Comment)
if user_id is not None:
q = q.filter(Comment.user_id == user_id)
if reservation_id is not None:
q = q.filter(Comment.reservation_id == reservation_id)
total = q.count()
records = (
q.order_by(Comment.create_time.desc())
.offset((page_num - 1) * page_size)
.limit(page_size)
.all()
)
return total, [self.fill_comment_detail(db, item) for item in records]
comment_service = CommentService()
<template>
<div class="page-container">
<div class="page-header">
<span class="page-title">课程管理</span>
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>新增课程</el-button>
</div>
<div class="search-card">
<el-form :inline="true" :model="query">
<el-form-item label="课程名">
<el-input v-model="query.name" placeholder="课程名称" clearable />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="query.category" placeholder="课程分类" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable style="width: 120px">
<el-option label="上架" :value="1" />
<el-option label="下架" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData"><el-icon><Search /></el-icon>搜索</el-button>
<el-button @click="resetQuery"><el-icon><Refresh /></el-icon>重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-card">
<el-table :data="tableData" v-loading="loading" stripe border>
<el-table-column prop="id" label="ID" min-width="70" />
<el-table-column label="封面" min-width="80">
<template #default="{ row }">
<img v-if="row.cover" :src="getImageUrl(row.cover)" class="table-img" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="name" label="课程名称" min-width="140" />
<el-table-column prop="category" label="分类" min-width="100" />
<el-table-column prop="duration" label="时长(分钟)" min-width="110" />
<el-table-column prop="price" label="价格(元)" min-width="100" />
<el-table-column prop="intro" label="简介" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" min-width="80">
<template #default="{ row }">
<el-tag :type="getStatusTagType('common', row.status)">{{ row.status === 1 ? '上架' : '下架' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" min-width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrap">
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@change="loadData"
/>
</div>
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑课程' : '新增课程'" width="560px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="课程名称" prop="name">
<el-input v-model="form.name" placeholder="请输入课程名称" />
</el-form-item>
<el-form-item label="分类" prop="category">
<el-input v-model="form.category" placeholder="如:瑜伽、有氧" />
</el-form-item>
<el-form-item label="封面">
<el-upload :show-file-list="false" :http-request="handleUpload" accept="image/*">
<img v-if="form.cover" :src="getImageUrl(form.cover)" class="table-img" style="width:80px;height:80px" />
<el-button v-else type="primary" plain>上传封面</el-button>
</el-upload>
</el-form-item>
<el-form-item label="时长(分钟)">
<el-input-number v-model="form.duration" :min="1" :max="300" />
</el-form-item>
<el-form-item label="价格(元)">
<el-input-number v-model="form.price" :min="0" :precision="2" :step="10" />
</el-form-item>
<el-form-item label="简介">
<el-input v-model="form.intro" type="textarea" :rows="3" placeholder="课程简介" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :value="1">上架</el-radio>
<el-radio :value="0">下架</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { courseApi, fileApi } from '@/api'
import { getImageUrl, formatDateTime, getStatusTagType } from '@/utils/format'
/**
* 课程管理页面
*/
const loading = ref(false)
const submitLoading = ref(false)
const dialogVisible = ref(false)
const isEdit = ref(false)
const tableData = ref([])
const total = ref(0)
const formRef = ref(null)
const query = reactive({ pageNum: 1, pageSize: 10, name: '', category: '', status: null })
const defaultForm = () => ({ id: null, name: '', cover: '', category: '', duration: 60, intro: '', price: 0, status: 1 })
const form = reactive(defaultForm())
const rules = {
name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
category: [{ required: true, message: '请输入分类', trigger: 'blur' }]
}
const loadData = async () => {
loading.value = true
try {
const data = await courseApi.page(query)
tableData.value = data.records
total.value = data.total
} finally {
loading.value = false
}
}
const resetQuery = () => {
Object.assign(query, { pageNum: 1, name: '', category: '', status: null })
loadData()
}
const handleAdd = () => {
isEdit.value = false
Object.assign(form, defaultForm())
dialogVisible.value = true
}
const handleEdit = (row) => {
isEdit.value = true
Object.assign(form, { ...row })
dialogVisible.value = true
}
const handleUpload = async ({ file }) => {
form.cover = await fileApi.upload(file)
}
const handleSubmit = async () => {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) await courseApi.update(form)
else await courseApi.add(form)
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
dialogVisible.value = false
loadData()
} finally {
submitLoading.value = false
}
}
const handleDelete = (row) => {
ElMessageBox.confirm(`确定删除课程「${row.name}」吗?`, '提示', { type: 'warning' })
.then(async () => {
await courseApi.delete(row.id)
ElMessage.success('删除成功')
loadData()
}).catch(() => {})
}
onMounted(() => loadData())
</script>
43万+

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



