1. 项目概述与设计思路
演唱会票务管理系统作为连接演出方与观众的数字化桥梁,其核心价值在于解决传统票务管理中的三大痛点:信息不对称、购票流程繁琐、现场管理低效。我们采用前后端分离架构,通过Python+Django/Flask后端与Vue.js前端的组合,构建了一个高可用、易扩展的现代化解决方案。
技术选型上,后端选择Django REST Framework(DRF)而非原生Django,主要基于以下考量:
- DRF内置的序列化器可自动处理JSON转换,使API开发效率提升40%+
- 完善的权限控制系统(如TokenAuthentication)开箱即用
- Browsable API特性极大方便了开发调试
- 与Django ORM深度集成,支持复杂查询的快速实现
前端采用Vue 3组合式API开发,相比选项式API具有:
- 更好的TypeScript支持
- 逻辑关注点更集中的代码组织方式
- 更灵活的逻辑复用能力(composables)
- 性能优化后的响应式系统
数据库设计采用MySQL作为主存储,关键表结构设计如下:
sql复制CREATE TABLE `events` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL COMMENT '演出名称',
`venue_id` BIGINT NOT NULL COMMENT '场馆ID',
`start_time` DATETIME NOT NULL COMMENT '开始时间',
`duration` INT NOT NULL COMMENT '演出时长(分钟)',
`poster_url` VARCHAR(255) COMMENT '海报URL',
`description` TEXT COMMENT '详情描述',
`status` TINYINT DEFAULT 1 COMMENT '1-待售 2-在售 3-售罄 4-取消',
INDEX `idx_venue_time` (`venue_id`, `start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 核心功能模块实现
2.1 用户认证系统
采用JWT+Refresh Token双令牌机制保障安全:
- 登录成功返回access_token(30分钟过期)和refresh_token(7天过期)
- 前端在axios拦截器中自动处理token刷新
- 敏感操作需二次验证(如支付密码)
关键代码示例(Django):
python复制# settings.py
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True # 刷新时生成新refresh_token
}
# views.py
class LoginView(TokenObtainPairView):
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 添加自定义用户信息
user = serializer.user
data = serializer.validated_data
data['user'] = {
'id': user.id,
'username': user.username,
'avatar': user.avatar.url if user.avatar else None
}
return Response(data)
2.2 票务库存管理
实现分布式锁防止超卖:
python复制# utils/redis_lock.py
import redis
from contextlib import contextmanager
redis_conn = redis.StrictRedis()
@contextmanager
def acquire_lock(lock_name, timeout=10):
identifier = str(uuid.uuid4())
end = time.time() + timeout
while time.time() < end:
if redis_conn.setnx(f'lock:{lock_name}', identifier):
redis_conn.expire(f'lock:{lock_name}', timeout)
try:
yield identifier
finally:
if redis_conn.get(f'lock:{lock_name}') == identifier:
redis_conn.delete(f'lock:{lock_name}')
return
time.sleep(0.001)
raise Exception("获取锁超时")
# views.py
def create_order(request):
seat_id = request.data['seat_id']
with acquire_lock(f'seat_{seat_id}'):
seat = Seat.objects.select_for_update().get(id=seat_id)
if seat.status != 'available':
return Response({'error': '座位已售出'}, status=400)
# 创建订单逻辑...
2.3 在线选座系统
前端使用Canvas实现可视化选座:
vue复制<!-- SeatMap.vue -->
<template>
<div class="seat-map">
<canvas ref="canvas" @click="handleSeatClick"></canvas>
<div class="legend">
<span v-for="(item, i) in legend" :key="i">
<span class="color-box" :style="{backgroundColor: item.color}"></span>
{{ item.label }}
</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
seats: { type: Array, required: true },
venue: { type: Object, required: true }
})
const canvas = ref(null)
const selectedSeats = ref(new Set())
const drawSeats = () => {
const ctx = canvas.value.getContext('2d')
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
// 绘制座位逻辑
props.seats.forEach(seat => {
ctx.fillStyle = getSeatColor(seat)
ctx.fillRect(seat.x, seat.y, seat.width, seat.height)
if (selectedSeats.value.has(seat.id)) {
ctx.strokeStyle = '#FFD700'
ctx.lineWidth = 3
ctx.strokeRect(seat.x-2, seat.y-2, seat.width+4, seat.height+4)
}
})
}
const handleSeatClick = (e) => {
const rect = canvas.value.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const clickedSeat = props.seats.find(seat =>
x >= seat.x && x <= seat.x + seat.width &&
y >= seat.y && y <= seat.y + seat.height
)
if (clickedSeat) {
if (selectedSeats.value.has(clickedSeat.id)) {
selectedSeats.value.delete(clickedSeat.id)
} else {
if (selectedSeats.value.size >= 6) {
alert('最多选择6个座位')
return
}
selectedSeats.value.add(clickedSeat.id)
}
drawSeats()
emit('selection-change', Array.from(selectedSeats.value))
}
}
onMounted(() => {
canvas.value.width = props.venue.map_width
canvas.value.height = props.venue.map_height
drawSeats()
})
</script>
3. 支付系统集成
3.1 支付流程设计
- 前端生成支付参数 → 2. 调用支付SDK → 3. 异步通知处理 → 4. 订单状态更新
微信支付签名算法示例:
python复制def wxpay_sign(params, api_key):
sorted_params = sorted(params.items(), key=lambda x: x[0])
query_str = '&'.join([f'{k}={v}' for k, v in sorted_params if v])
query_str += f'&key={api_key}'
return hashlib.md5(query_str.encode('utf-8')).hexdigest().upper()
3.2 支付结果轮询机制
javascript复制// frontend/src/utils/payment.js
export const checkPaymentStatus = async (orderId) => {
let retry = 0
const maxRetry = 10
const interval = 3000
return new Promise((resolve, reject) => {
const timer = setInterval(async () => {
try {
const res = await api.get(`/orders/${orderId}/status`)
if (res.data.status === 'paid') {
clearInterval(timer)
resolve(true)
} else if (retry++ >= maxRetry) {
clearInterval(timer)
resolve(false)
}
} catch (err) {
clearInterval(timer)
reject(err)
}
}, interval)
})
}
4. 性能优化实践
4.1 缓存策略
python复制# decorators.py
from django.core.cache import caches
def cache_response(timeout=60*5, key_prefix='view_cache'):
def decorator(view_func):
def wrapped(request, *args, **kwargs):
cache_key = f"{key_prefix}:{request.get_full_path()}"
data = caches['default'].get(cache_key)
if data is not None:
return JsonResponse(data)
response = view_func(request, *args, **kwargs)
if response.status_code == 200:
caches['default'].set(cache_key, response.data, timeout)
return response
return wrapped
return decorator
# views.py
@cache_response(timeout=60*15)
def event_list(request):
events = Event.objects.filter(status='onsale').select_related('venue')
serializer = EventSerializer(events, many=True)
return Response(serializer.data)
4.2 数据库查询优化
- 使用
select_related和prefetch_related减少查询次数 - 复杂查询添加适当的数据库索引
- 分页查询使用cursor-based pagination
python复制# 优化后的查询示例
def get_event_detail(event_id):
return Event.objects.select_related('venue') \
.prefetch_related(
Prefetch('sessions', queryset=Session.objects.filter(start_time__gte=now())),
Prefetch('performers', queryset=Performer.objects.order_by('priority'))
) \
.get(id=event_id)
5. 部署架构
采用Docker Compose实现微服务化部署:
yaml复制version: '3.8'
services:
web:
build: ./backend
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
volumes:
- ./backend:/app
ports:
- "8000:8000"
depends_on:
- redis
- db
environment:
- DJANGO_SETTINGS_MODULE=config.production
frontend:
build: ./frontend
ports:
- "8080:80"
volumes:
- ./frontend:/app
- /app/node_modules
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
db_data:
redis_data:
6. 踩坑经验与解决方案
-
跨域问题:
- 现象:前端开发时出现CORS错误
- 解决:配置DRF的CORS_ORIGIN_WHITELIST,同时在生产环境使用Nginx反向代理
-
时区混乱:
- 现象:数据库时间与显示时间不一致
- 方案:统一使用UTC存储,前端按用户时区转换
python复制# settings.py TIME_ZONE = 'UTC' USE_TZ = True -
微信支付证书加载失败:
- 现象:Docker容器内无法读取证书文件
- 解决:将证书放在项目根目录,通过绝对路径引用
-
Vue响应式丢失:
- 现象:数组直接赋值导致视图不更新
- 方案:使用
reactive或ref包装数据
javascript复制const seats = reactive([]) const loadSeats = async () => { const res = await api.get('/seats') seats.splice(0, seats.length, ...res.data) } -
高并发下的库存超卖:
- 现象:秒杀活动出现超卖
- 终极方案:Redis原子操作+Lua脚本
lua复制-- decr_stock.lua local key = KEYS[1] local quantity = tonumber(ARGV[1]) local stock = tonumber(redis.call('GET', key)) if stock >= quantity then redis.call('DECRBY', key, quantity) return 1 else return 0 end
这个项目从技术选型到最终部署,每个环节都需要考虑实际业务场景。比如在票务系统中,支付超时处理要比普通电商更严格——通常演唱会门票15分钟内未支付就会自动释放。同时要做好压力测试,特别是热门演出开票时,瞬时流量可能是平时的百倍以上。