作为一名有多年全栈开发经验的工程师,我最近完成了一个基于Flask和Vue.js的家教服务管理系统。这个项目源于我观察到传统家教行业存在的信息不对称、管理效率低下等问题。通过这个系统,家长可以方便地找到合适的家教老师,老师也能展示自己的教学专长,双方可以通过平台完成课程预约、支付和评价的全流程。
这个系统采用了前后端分离的架构,后端使用Python的Flask框架提供RESTful API,前端使用Vue.js构建响应式用户界面。数据库选用MySQL存储业务数据,开发环境使用PyCharm作为主要IDE。整个系统从设计到实现耗时约3个月,目前已在小范围内试运行,效果良好。
在项目初期,我对比了Flask和Django两个Python Web框架的优缺点:
Flask:轻量级、灵活、学习曲线平缓,适合快速开发API服务。它的微内核设计让我们可以根据需要选择扩展,不会引入不必要的功能。
Django:全功能框架,自带ORM、Admin等组件,适合大型项目。但它的"包含电池"理念可能会引入一些我们不需要的功能,增加系统复杂度。
考虑到家教系统的业务逻辑相对明确,且我们需要快速迭代开发,最终选择了Flask作为主要后端框架。不过我们也保留了切换到Django的可能性,因此在代码组织上做了相应设计。
前端部分我们选择了Vue.js 3.x版本,主要基于以下考虑:
配合Vue Router实现前端路由管理,使用Vuex进行状态管理,Element Plus作为UI组件库。这些技术组合在一起形成了一个完整的前端解决方案。
开发工具方面,后端使用PyCharm Professional版,它提供了优秀的Python开发支持,包括:
前端开发则使用VS Code,配合Volar插件提供Vue开发支持。VS Code轻量高效,且有丰富的扩展生态系统。
系统主要分为以下几个功能模块:
我们使用MySQL作为主数据库,通过SQLAlchemy ORM进行数据访问。主要数据表设计如下:
sql复制CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(128) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE,
phone VARCHAR(20),
role ENUM('admin', 'teacher', 'student', 'parent') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
sql复制CREATE TABLE teachers (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
real_name VARCHAR(50) NOT NULL,
gender ENUM('male', 'female', 'other'),
age INT,
education VARCHAR(50),
major VARCHAR(100),
teaching_experience INT COMMENT '教学经验(年)',
certification TEXT COMMENT '资质证书信息',
self_introduction TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
sql复制CREATE TABLE courses (
id INT AUTO_INCREMENT PRIMARY KEY,
teacher_id INT NOT NULL,
subject VARCHAR(50) NOT NULL,
grade_level VARCHAR(20) NOT NULL,
price_per_hour DECIMAL(10,2) NOT NULL,
available_times TEXT COMMENT '可授课时间JSON',
teaching_method ENUM('online', 'offline', 'both') NOT NULL,
address VARCHAR(255) COMMENT '线下授课地址',
status ENUM('published', 'draft', 'closed') DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (teacher_id) REFERENCES teachers(id)
);
sql复制CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id INT NOT NULL,
course_id INT NOT NULL,
teacher_id INT NOT NULL,
order_number VARCHAR(32) NOT NULL UNIQUE,
scheduled_time DATETIME NOT NULL,
duration INT NOT NULL COMMENT '课时(分钟)',
total_amount DECIMAL(10,2) NOT NULL,
status ENUM('pending', 'confirmed', 'completed', 'cancelled') DEFAULT 'pending',
payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (student_id) REFERENCES users(id),
FOREIGN KEY (course_id) REFERENCES courses(id),
FOREIGN KEY (teacher_id) REFERENCES teachers(id)
);
sql复制CREATE TABLE reviews (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL UNIQUE,
student_id INT NOT NULL,
teacher_id INT NOT NULL,
rating TINYINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
content TEXT,
reply TEXT COMMENT '教师回复',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (student_id) REFERENCES users(id),
FOREIGN KEY (teacher_id) REFERENCES teachers(id)
);
数据库设计注意事项:
- 使用外键约束保证数据完整性
- 为常用查询字段添加索引
- 合理选择字段类型和长度
- 添加适当的注释说明字段用途
- 考虑未来可能的扩展需求
我们采用工厂模式组织Flask应用,目录结构如下:
code复制tutor_management/
├── app/
│ ├── __init__.py # 应用工厂
│ ├── auth/ # 认证模块
│ ├── models/ # 数据模型
│ ├── api/ # API路由
│ │ ├── v1/ # API版本1
│ │ │ ├── __init__.py
│ │ │ ├── courses.py
│ │ │ ├── users.py
│ │ │ ├── orders.py
│ │ │ └── reviews.py
│ ├── extensions.py # 扩展初始化
│ └── utils.py # 工具函数
├── config.py # 配置文件
├── requirements.txt # 依赖文件
└── run.py # 启动脚本
我们使用JWT(JSON Web Token)进行用户认证。主要实现步骤:
核心代码示例:
python复制from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
@app.route('/api/auth/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = User.query.filter_by(username=username).first()
if not user or not user.check_password(password):
return jsonify({"msg": "Bad credentials"}), 401
access_token = create_access_token(identity={
'id': user.id,
'role': user.role
})
return jsonify(access_token=access_token)
@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
课程API实现了常见的CRUD操作,并增加了分页、过滤等功能:
python复制from flask_restful import Resource, reqparse
from flask_jwt_extended import jwt_required, get_jwt_identity
class CourseList(Resource):
def __init__(self):
self.parser = reqparse.RequestParser()
self.parser.add_argument('page', type=int, default=1)
self.parser.add_argument('per_page', type=int, default=10)
self.parser.add_argument('subject', type=str)
self.parser.add_argument('grade_level', type=str)
self.parser.add_argument('min_price', type=float)
self.parser.add_argument('max_price', type=float)
def get(self):
args = self.parser.parse_args()
query = Course.query.filter_by(status='published')
if args['subject']:
query = query.filter_by(subject=args['subject'])
if args['grade_level']:
query = query.filter_by(grade_level=args['grade_level'])
if args['min_price']:
query = query.filter(Course.price_per_hour >= args['min_price'])
if args['max_price']:
query = query.filter(Course.price_per_hour <= args['max_price'])
pagination = query.paginate(
page=args['page'],
per_page=args['per_page'],
error_out=False
)
return {
'courses': [course.to_dict() for course in pagination.items],
'total': pagination.total,
'pages': pagination.pages,
'current_page': pagination.page
}
订单处理是系统的核心业务,主要流程包括:
关键代码实现:
python复制class OrderResource(Resource):
@jwt_required()
def post(self):
current_user = get_jwt_identity()
if current_user['role'] not in ['student', 'parent']:
return {'message': 'Only students can create orders'}, 403
data = request.get_json()
course = Course.query.get_or_404(data['course_id'])
# 检查课程是否可用
if course.status != 'published':
return {'message': 'This course is not available'}, 400
# 创建订单
order = Order(
student_id=current_user['id'],
course_id=course.id,
teacher_id=course.teacher_id,
order_number=generate_order_number(),
scheduled_time=data['scheduled_time'],
duration=data['duration'],
total_amount=calculate_amount(course.price_per_hour, data['duration'])
)
db.session.add(order)
db.session.commit()
# 发送通知给教师
send_notification(
user_id=course.teacher_id,
title='New Order Received',
message=f'You have a new order for {course.subject}'
)
return order.to_dict(), 201
前端项目采用Vue CLI创建,主要目录结构:
code复制src/
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── styles/ # 全局样式
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── auth/ # 认证相关页面
│ ├── courses/ # 课程相关页面
│ ├── orders/ # 订单相关页面
│ └── profile/ # 个人中心
├── App.vue # 根组件
└── main.js # 应用入口
使用Pinia进行状态管理,主要store包括:
示例代码:
javascript复制// stores/course.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/api'
export const useCourseStore = defineStore('course', () => {
const courses = ref([])
const pagination = ref({})
const loading = ref(false)
const error = ref(null)
const fetchCourses = async (params = {}) => {
loading.value = true
try {
const response = await api.get('/courses', { params })
courses.value = response.data.courses
pagination.value = {
total: response.data.total,
pages: response.data.pages,
currentPage: response.data.current_page
}
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
return {
courses,
pagination,
loading,
error,
fetchCourses
}
})
课程列表页面实现了分页、过滤和搜索功能:
vue复制<template>
<div class="course-list">
<el-card>
<div class="filter-container">
<el-input
v-model="searchQuery"
placeholder="Search courses..."
style="width: 300px"
@keyup.enter="handleFilter"
/>
<el-select
v-model="filterSubject"
placeholder="Select subject"
clearable
@change="handleFilter"
>
<el-option
v-for="subject in subjects"
:key="subject"
:label="subject"
:value="subject"
/>
</el-select>
<el-button type="primary" @click="handleFilter">
Filter
</el-button>
</div>
</el-card>
<el-row :gutter="20">
<el-col
v-for="course in courses"
:key="course.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<course-card :course="course" />
</el-col>
</el-row>
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useCourseStore } from '@/stores/course'
import CourseCard from '@/components/CourseCard.vue'
const courseStore = useCourseStore()
const searchQuery = ref('')
const filterSubject = ref('')
const currentPage = ref(1)
const pageSize = 10
const courses = computed(() => courseStore.courses)
const total = computed(() => courseStore.pagination.total || 0)
const subjects = ['Math', 'Physics', 'Chemistry', 'English', 'Biology']
const fetchData = () => {
const params = {
page: currentPage.value,
per_page: pageSize,
subject: filterSubject.value,
q: searchQuery.value
}
courseStore.fetchCourses(params)
}
const handleFilter = () => {
currentPage.value = 1
fetchData()
}
const handlePageChange = (page) => {
currentPage.value = page
fetchData()
}
onMounted(() => {
fetchData()
})
</script>
bash复制python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
bash复制pip install -r requirements.txt
bash复制npm install
.env文件:code复制VUE_APP_API_BASE_URL=http://localhost:5000/api
sql复制CREATE DATABASE tutor_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
config.py中:python复制class DevelopmentConfig(Config):
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://username:password@localhost/tutor_management'
由于前后端分离,需要配置CORS:
python复制from flask_cors import CORS
def create_app():
app = Flask(__name__)
CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:8080"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
return app
单元测试:使用pytest测试各个模块的功能
python复制def test_create_order(client, auth_headers, teacher, course):
response = client.post('/api/orders', json={
'course_id': course.id,
'scheduled_time': '2023-12-01 14:00:00',
'duration': 60
}, headers=auth_headers)
assert response.status_code == 201
assert response.json['order_number'] is not None
接口测试:使用Postman或自动化测试脚本测试API接口
前端测试:使用Jest进行组件测试,Cypress进行端到端测试
使用Gunicorn+Nginx部署Flask应用:
安装Gunicorn:
bash复制pip install gunicorn
启动Gunicorn:
bash复制gunicorn -w 4 -b 127.0.0.1:8000 "app:create_app()"
Nginx配置:
nginx复制server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
构建生产版本:
bash复制npm run build
配置Nginx:
nginx复制server {
listen 80;
server_name yourdomain.com;
root /path/to/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://api.yourdomain.com;
}
}
计划集成WebSocket实现师生实时沟通:
在开发这个家教服务管理系统的过程中,我积累了一些宝贵的经验:
API设计:RESTful API的设计要前后端充分沟通,定义好数据格式和状态码。我们使用了Swagger UI来维护API文档,大大提高了协作效率。
状态管理:前端复杂状态的管理是个挑战。我们最初使用Vuex,后来迁移到Pinia,发现组合式API更适合我们的场景。
性能优化:数据库查询是性能瓶颈所在。我们通过添加适当的索引、使用selective loading和缓存策略,显著提高了响应速度。
错误处理:完善的错误处理机制非常重要。我们实现了统一的错误处理中间件,并给前端提供友好的错误信息。
测试覆盖:测试代码的编写时间往往超过预期,但这是值得的。良好的测试覆盖率让我们在重构时更有信心。
一个特别值得分享的技巧是在处理课程预约时间冲突时,我们最初在应用层实现检查逻辑,后来改为数据库约束,性能提升了10倍:
sql复制ALTER TABLE orders ADD CONSTRAINT no_time_overlap
CHECK (NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.teacher_id = teacher_id
AND o.id != id
AND o.status IN ('confirmed', 'pending')
AND scheduled_time < DATE_ADD(scheduled_time, INTERVAL duration MINUTE)
AND DATE_ADD(o.scheduled_time, INTERVAL o.duration MINUTE) > scheduled_time
));
这个项目从零开始构建,经历了需求分析、技术选型、开发测试到部署上线的完整流程。过程中遇到了各种挑战,但通过不断学习和实践,最终交付了一个稳定可用的系统。希望这些经验对正在开发类似项目的同行有所帮助。