自习室管理系统是一个典型的资源预约类应用,核心要解决座位资源的合理分配与实时状态同步问题。我们采用前后端分离架构,通过组合多个框架的优势来实现系统的高效开发。
后端选择Flask+Django的组合方案主要基于以下考量:
前端选用Vue 3主要看中:
开发工具链配置:
整体架构分为四个层次:
mermaid复制graph TD
A[Vue前端] -->|Axios| B(Flask API)
B -->|调用| C[Django服务]
C -->|ORM| D[(PostgreSQL)]
D -->|缓存| E[(Redis)]
E -->|发布订阅| B
B -->|Socket.IO| A
特别注意:生产环境建议将Flask和Django部署在同一个Python环境中,通过blueprint进行路由分发,避免跨进程通信开销。
用户系统采用RBAC(基于角色的访问控制)模型:
python复制class User(AbstractUser):
ROLE_CHOICES = (
('student', '普通用户'),
('admin', '管理员'),
('super', '超级管理员')
)
mobile = models.CharField(max_length=11, unique=True)
avatar = models.ImageField(upload_to='avatars/')
credit = models.IntegerField(default=100)
role = models.CharField(max_length=10, choices=ROLE_CHOICES)
class Meta:
indexes = [
models.Index(fields=['mobile']),
models.Index(fields=['credit'])
]
自习室空间模型使用Django的PostGIS扩展:
python复制from django.contrib.gis.db import models
class StudyRoom(models.Model):
name = models.CharField(max_length=50)
location = models.PointField(srid=4326)
open_time = models.TimeField()
close_time = models.TimeField()
capacity = models.IntegerField()
def current_available(self):
return self.seats.filter(status='available').count()
使用有限状态机管理座位生命周期:
python复制from django_fsm import FSMField, transition
class Seat(models.Model):
STATUS = (
('available', '可预约'),
('reserved', '已预约'),
('in_use', '使用中'),
('maintenance', '维修中')
)
room = models.ForeignKey(StudyRoom, related_name='seats')
number = models.CharField(max_length=10)
status = FSMField(choices=STATUS, default='available')
last_user = models.ForeignKey(User, null=True)
@transition(field=status, source='available', target='reserved')
def reserve(self, user):
self.last_user = user
return f"座位{self.number}已预约"
@transition(field=status, source='reserved', target='in_use')
def check_in(self):
return f"座位{self.number}开始使用"
实践建议:使用django-fsm库可以清晰定义状态转换条件和约束,比纯手工实现更可靠。
JWT认证方案优化点:
python复制from flask_jwt_extended import (
create_access_token,
create_refresh_token,
jwt_required,
get_jwt_identity
)
@app.route('/auth/login', methods=['POST'])
def login():
# 参数验证省略...
user = User.query.filter_by(username=username).first()
if not user or not user.check_password(password):
return jsonify({"msg": "Bad credentials"}), 401
additional_claims = {
"role": user.role,
"credit": user.credit
}
access_token = create_access_token(
identity=user.id,
additional_claims=additional_claims,
expires_delta=timedelta(hours=2)
)
refresh_token = create_refresh_token(
identity=user.id,
additional_claims=additional_claims
)
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token,
"user_info": user.to_dict()
})
使用Redis分布式锁解决超卖问题:
python复制import redis
from contextlib import contextmanager
redis_conn = redis.StrictRedis()
@contextmanager
def redis_lock(lock_name, timeout=10):
lock = redis_conn.lock(lock_name, timeout=timeout)
acquired = lock.acquire(blocking=True)
try:
if acquired:
yield True
else:
raise Exception("获取锁失败")
finally:
if acquired:
lock.release()
def reserve_seat(seat_id, user_id):
lock_key = f"seat_lock:{seat_id}"
try:
with redis_lock(lock_key):
seat = Seat.query.get(seat_id)
if seat.status != 'available':
return False, "座位已被占用"
# 创建预约记录
reservation = Reservation(
seat_id=seat_id,
user_id=user_id,
reserve_time=datetime.now()
)
db.session.add(reservation)
# 更新座位状态
seat.reserve(user_id)
db.session.commit()
# 异步任务:15分钟后检查签到状态
check_in_task.apply_async(
args=[reservation.id],
countdown=15*60
)
return True, "预约成功"
except Exception as e:
return False, str(e)
Socket.IO服务端配置:
python复制from flask_socketio import SocketIO, emit
socketio = SocketIO(app, cors_allowed_origins="*")
@socketio.on('connect')
def handle_connect():
user = get_authenticated_user()
if user:
join_room(f'user_{user.id}')
emit('system', {'msg': '连接成功'})
else:
disconnect()
def notify_seat_change(seat_id):
seat = Seat.query.get(seat_id)
socketio.emit('seat_update', {
'seat_id': seat.id,
'status': seat.status,
'room_id': seat.room_id
}, room=f'room_{seat.room_id}')
前端事件监听:
javascript复制import { io } from "socket.io-client";
const socket = io(API_BASE_URL, {
auth: {
token: localStorage.getItem('access_token')
}
});
socket.on('seat_update', (data) => {
store.commit('updateSeatStatus', {
seatId: data.seat_id,
status: data.status
});
});
// 加入自习室房间
function joinRoom(roomId) {
socket.emit('join', `room_${roomId}`);
}
javascript复制// Vuex状态管理示例
const seatModule = {
state: () => ({
seatMap: new Map()
}),
mutations: {
updateSeatStatus(state, {seatId, status}) {
state.seatMap.set(seatId, status)
}
},
getters: {
getSeatStatus: (state) => (seatId) => {
return state.seatMap.get(seatId) || 'unknown'
}
}
}
Docker Compose编排文件示例:
yaml复制version: '3.8'
services:
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
db:
image: postgis/postgis:13-3.1
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
backend:
build: ./backend
ports:
- "5000:5000"
depends_on:
- redis
- db
environment:
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@db/postgres
REDIS_URL: redis://redis:6379/0
frontend:
build: ./frontend
ports:
- "8080:80"
depends_on:
- backend
volumes:
redis_data:
pg_data:
数据库层面:
缓存策略:
前端优化:
python复制@pytest.mark.django_db
def test_seat_reservation():
user = UserFactory()
seat = SeatFactory(status='available')
success, msg = reserve_seat(seat.id, user.id)
assert success is True
assert seat.status == 'reserved'
assert seat.last_user == user
集成测试(占比30%):
E2E测试(占比10%):
使用Locust模拟高并发场景:
python复制from locust import HttpUser, task, between
class ReservationUser(HttpUser):
wait_time = between(1, 3)
@task
def reserve_seat(self):
self.client.post("/api/reservations", json={
"seat_id": 1,
"user_id": 1
}, headers={
"Authorization": f"Bearer {self.token}"
})
关键指标监控:
Python环境隔离:
跨域问题解决:
python复制from flask_cors import CORS
CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:8080"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Authorization", "Content-Type"]
}
})
常见问题排查:
调试技巧:
python复制@app.before_request
def log_request_info():
app.logger.debug(f"Headers: {request.headers}")
app.logger.debug(f"Body: {request.get_data()}")
这个项目最关键的架构决策是采用Flask+Django的混合模式,实际开发中发现两个框架的静态文件处理会有冲突,最终解决方案是将Flask作为主入口,Django作为子应用挂载。另外,Vue的响应式系统与WebSocket的状态同步需要特别注意数据更新的时机问题,建议使用Vuex的严格模式来避免直接状态修改。