ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+Vue前后端分离:演唱会门票预约系统开发实战

Python+Vue前后端分离:演唱会门票预约系统开发实战 做演唱会门票售票预约系统是个挺有意思的项目功能不算复杂但该有的模块一个不少——用户认证、场次展示、选座、预约下单、订单状态流转。我用 Python 做后端Django 和 Flask 两条路线都走了一遍前端用 VueIDE 用的是 PyCharm。这篇文章把从零搭建到前后端联调的完整过程捋一遍包含表结构设计、核心接口实现、抢票并发控制思路、以及我在实际开发里踩过的一些坑。适合正在学 Django/Flask 的读者参考也适合想完整走一遍前后端分离项目的同学抄作业。1. 项目整体设计与思路拆解1.1 这个项目到底要解决什么问题演唱会的售票预约场景核心痛点就三个一是信息展示用户要能快速看到有哪些场次、票价、剩余票量二是选座下单用户要能直观选座并提交预约三是订单管理用户能查看、取消自己的预约记录管理员要能控制每场演出的可售座位数。对应到系统模块上就是用户端注册登录、演出列表、演出详情、选座页、订单页和管理端场次管理、座位管理、订单查看。前后端分离是不二之选后端只用出 JSON 接口前端 Vue 负责渲染和交互两边的开发可以并行推进。我这套项目后端用 Django 实现了一套完整的版本同时把 Flask 的替代写法也整理了出来同一个需求两种框架都能落地区别只在代码组织和生态选择。1.2 为什么技术栈选 Python Vue以及 PyCharm 在里面的角色Python 作为后端语言的优点是生态成熟、上手快Django 自带 ORM、Admin 后台、认证体系Flask 则以轻量灵活著称。Vue 选它是看中它的组件化开发模式和渐进式框架定位从 Vue 2 到 Vue 3 生态都已经很稳配 Element Plus 或者 Ant Design Vue 做后台管理界面效率非常高。PyCharm 在整个开发流程里的作用容易被低估。它不只是编辑器更是集成了虚拟环境管理、数据库工具、调试器和 HTTP Client 的完整工作台。我的开发流程基本是PyCharm 里建好 Python 虚拟环境配置好解释器写好模型后用自带终端执行迁移命令调试接口时直接用 HTTP Client 发请求验证甚至 Vue 前端代码也在同一个 IDE 里编辑。一个工具覆盖全链路省去了来回切换的开销。提示不要纠结Django 和 Flask 到底选哪个汴京问题。实操经验是——如果你希望快速生成完整可用的后台管理界面、用户认证、模型迁移选 Django如果项目以后要做微服务拆分、或者你更倾向自由组装扩展选 Flask。本文后面两条路都给出关键代码按需取用。2. 技术选型Django 与 Flask 在售票场景下的取舍2.1 Django 路线自带体系的完整方案Django 的项目结构非常清晰一个项目包含多个应用每个应用负责一个业务域。售票系统我拆了三个应用users 负责用户events 负责演出场次和座位orders 负责订单。用 Django 写这个系统的优势是很多能力是开箱即用的。创建项目和应用pip install django djangorestframework django-admin startproject ticket_system cd ticket_system python manage.py startapp users python manage.py startapp events python manage.py startapp orders然后在 settings.py 里注册这三个应用和 REST frameworkINSTALLED_APPS [ # ... rest_framework, users, events, orders, ]Django 自带的 User 模型可以直接扩展使用我通过 OneToOne 字段关联一个 Profile 来存手机号等额外信息。用户注册登录用 JWT 方案配合 djangorestframework-simplejwt认证接口基本不用自己写。后台管理这个地方是 Django 的真正杀伤力。我把 Event 和 Order 注册到 admin.py管理方直接获得了增删改查界面对于演唱会门票这种需要快速上线的预约系统来说等于白送了一套管理后台。对自己开发的小项目来说这个价值很大——省出来的时间全部可以花在核心业务流程上。2.2 Flask 路线灵活组装的轻量方案Flask 的设计哲学是不强制核心只负责路由和请求响应其他功能通过扩展组装。同一个售票系统用 Flask 实现我通常会加入 Flask-SQLAlchemyORM、Flask-Migrate数据库迁移、Flask-JWT-Extended令牌认证、Flask-CORS跨域处理。pip install flask flask-sqlalchemy flask-migrate flask-jwt-extended flask-corsFlask 的应用结构更自由推荐按功能模块拆 Blueprintfrom flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager from flask_cors import CORS app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///ticket.db app.config[JWT_SECRET_KEY] your-secret-key db SQLAlchemy(app) jwt JWTManager(app) CORS(app)业务模块用 Blueprint 注册from flask import Blueprint events_bp Blueprint(events, __name__) events_bp.route(/api/events) def list_events(): # 查询演出列表 pass events_bp.route(/api/events/int:event_id, methods[GET]) def event_detail(event_id): # 查询单个演出详情 passFlask 对开发者要求更高的自律性——没有自动的 Admin 后台需要自己写管理页面或者接一个简单的管理模板没有内置的 ORM但换来的是更小的项目体积和更高的自由度。初学阶段我更建议先用 Django 走通全流程然后再把关键接口用 Flask 重写一遍体会两种框架在约定和自由之间的差别。2.3 PyCharm 环境准备与项目导入PyCharm 安装完成之后第一件事是配置解释器。我习惯在 PyCharm 底部 Terminal 里创建虚拟环境python -m venv venv然后 File Settings Project Python Interpreter选择刚才创建的 venv。之后安装依赖就不会污染全局 Python 环境。注意一个细节如果本机装了多个 Python 版本创建虚拟环境之前先执行python --version确认默认版本。如果系统里 Python 3.8 和 3.11 混着用在后端开发中容易因为新特性兼容问题翻车建议全部代码统一跑在同一个版本上。Vue 前端项目的打开方式是 File Open选中前端目录PyCharm 会自动识别 package.json。它的优势在于前后端代码在一个 IDE 窗口里管理切换上下文没有障碍。3. 数据库设计与核心模型3.1 四张核心表的关系拆解售票系统的数据模型核心是用户、场次、座位、订单四张表。它们的关系不复杂一个用户有多个订单一个订单对应一个场次的多张票座位表中每个座位绑定一个场次。用表结构画出来是这样表名关键字段说明usersid, username, password_hash, phone, email用户账户信息eventsid, title, artist, venue, show_time, price_levels, total_seats, sold_count演唱会场次信息seatsid, event_id, zone, row_number, seat_number, status座位状态ordersid, user_id, event_id, status, created_at, total_price预约订单以 Django ORM 为例模型定义如下from django.db import models from django.contrib.auth.models import User class Event(models.Model): title models.CharField(max_length200) # 演唱会名称 artist models.CharField(max_length100) # 歌手/乐队 venue models.CharField(max_length200) # 场馆 show_time models.DateTimeField() # 演出时间 total_seats models.IntegerField() # 总座位数 sold_count models.IntegerField(default0) # 已售数量 price_detail models.JSONField(defaultdict) # 价格档位 {看台A: 480, 内场B: 980} class Seat(models.Model): STATUS_CHOICES [ (available, 可售), (locked, 锁定), (sold, 已售), ] event models.ForeignKey(Event, related_nameseats, on_deletemodels.CASCADE) zone models.CharField(max_length50) # 区域 row_number models.CharField(max_length10) # 排号 seat_number models.CharField(max_length10) # 座位号 status models.CharField(max_length10, choicesSTATUS_CHOICES, defaultavailable) class Meta: unique_together (event, zone, row_number, seat_number)3.2 座位与余票字段设计的关键细节座位字段的unique_together是个容易踩坑的地方。如果不在数据库层面对座位唯一性做约束并发情况下同一个座位被下两单是迟早的事。这个约束的价值在于即使应用层逻辑有 bug数据库也会拒绝重复数据。status字段用字符串还是数字也有讲究。我推荐用带语义的字符串做 choices因为直接看数据库就能懂。千万不要直接用数字 0/1/2 表示一周后自己维护的时候完全想不起来哪个数字对应哪个状态。订单表建议加一个expire_time字段处理锁座超时释放这个需求。演唱会门票的预约选座通常用户选座后不是立即支付而是预留一段时间。我设置预约超时时间为 15 分钟超过时间如果未完成支付座位状态自动从 locked 恢复为 available。这个定时任务不必用 Celery 那么重的方案Django 里写一个管理命令management command定时执行即可Flask 里则用 flask-apscheduler 或者直接依赖请求时懒检查过期订单。数据库选型上开发环境用 SQLite 完全够用一行配置搞定PyCharm 的 Database 工具可以直接打开 SQLite 文件查看表数据。上线部署建议迁到 PostgreSQL 或 MySQL代码层面只需要改连接字符串。4. 后端 API 核心实现与抢票逻辑4.1 接口清单与序列化处理按照前端页面倒推后端需要提供这些接口方法路径功能权限POST/api/auth/register用户注册公开POST/api/auth/login用户登录返回 JWT公开GET/api/events演出列表含余票状态公开GET/api/events/演出详情含座位图公开GET/api/events/ /seats某场次的座位状态公开POST/api/orders创建预约订单锁定座位登录GET/api/orders/my当前用户的订单列表登录POST/api/orders/ /cancel取消预约、释放座位登录GET/api/orders/订单详情登录Django REST Framework 里这些接口用 ViewSet 组织非常顺手。以 events 为例from rest_framework import viewsets, serializers class EventSerializer(serializers.ModelSerializer): class Meta: model Event fields [id, title, artist, venue, show_time, total_seats, sold_count, price_detail] def get_remaining(self, obj): return obj.total_seats - obj.sold_count class EventViewSet(viewsets.ReadOnlyModelViewSet): queryset Event.objects.all() serializer_class EventSerializer只读接口用ReadOnlyModelViewSet就够了创建订单这种写操作单独放到 orders 应用里用 APIView 实现。这样职责清晰代码也好维护。4.2 抢票下单的并发控制核心难点售票系统开发中真正让人头疼的是并发问题。同一场演出开票前 10 秒可能进来几千个请求其中一大半指向同一个热门内场区域。如果代码写成先查剩余票数够了就减少库存再创建订单这种朴素逻辑并发条件下超卖是必然的。我的处理分三层第一层是数据库事务。Django 里使用select_for_update()对 Seat 行加锁确保同一时刻只有一个事务能修改某个座位from django.db import transaction from rest_framework.views import APIView class CreateOrderView(APIView): def post(self, request): event_id request.data.get(event_id) seat_ids request.data.get(seat_ids) with transaction.atomic(): seats Seat.objects.select_for_update().filter( id__inseat_ids, event_idevent_id ) for seat in seats: if seat.status ! available: return Response({error: f座位 {seat.id} 已被锁定}, status400) # 批量更新座位状态 Seat.objects.filter(id__inseat_ids).update(statuslocked) # 更新已售数量 Event.objects.filter(idevent_id).update( sold_countmodels.F(sold_count) len(seat_ids) ) order Order.objects.create( userrequest.user, event_idevent_id, statuspending ) order.seats.set(seat_ids) return Response(OrderSerializer(order).data)select_for_update()在 SQLite 下效果有限因为 SQLite 的并发写能力很弱。代码层面仍然要写因为切换到 MySQL/PostgreSQL 时这套逻辑是通用的。如果项目上线后真实流量很高建议加一层 Redis 预扣库存先把热门座位 ID 写入 Redis Set用户点击下单时先在 Redis 里判断座位是否被抢走抢到才真正落到数据库。Redis 的原子操作比数据库锁抗压能力高一个数量级。第二层是状态机的严格管理。座位从 available 到 locked 到 sold不允许跳变。取消预约时只有 locked 的座位才能回滚到 available。这层逻辑通过模型方法统一封装class Seat(models.Model): def lock(self): if self.status ! available: raise ValueError(f座位不可锁定: {self.status}) self.status locked self.save(update_fields[status]) def release(self): if self.status ! locked: raise ValueError(f座位不可释放: {self.status}) self.status available self.save(update_fields[status]) def mark_sold(self): if self.status ! locked: raise ValueError(f座位不可售出: {self.status}) self.status sold self.save(update_fields[status])第三层是接口重试与幂等。用户点击提交预约时网络抖动前端容易重复提交订单就会重复创建。解决办法是前端在按钮点击后立即置灰禁用同时后端接收一个幂等键比如前端生成的随机 UUID用该键做唯一约束重复请求直接返回已存在的订单数据。Flask 里实现同样的接口核心事务逻辑用 SQLAlchemy 的 session 控制from flask_jwt_extended import jwt_required, get_jwt_identity orders_bp.route(/api/orders, methods[POST]) jwt_required() def create_order(): user_id get_jwt_identity() data request.get_json() event_id data[event_id] seat_ids data[seat_ids] try: seats db.session.execute( db.select(Seat).where(Seat.id.in_(seat_ids), Seat.event_id event_id) .with_for_update() ).scalars().all() for seat in seats: if seat.status ! available: return {error: f座位 {seat.id} 已被锁定}, 400 for seat in seats: seat.status locked order Order(user_iduser_id, event_idevent_id, statuspending) db.session.add(order) db.session.flush() order.seats.extend(seats) db.session.commit() return {order_id: order.id}, 201 except Exception: db.session.rollback() return {error: 创建订单失败}, 5004.3 数据库查询中的性能注意事项Django 初学者经常踩的一个坑是列表页 N1 查询。演出列表页除了查 Event 还要查关联的 Seat 数量如果不加优化查 20 场演出就要多打 20 条 SQL。用select_related或prefetch_related解决queryset Event.objects.all().select_related(venue).prefetch_related(seats)数据库删对象这一点也值得一提。Django 里Model.objects.filter(...).delete()和Model.objects.all().delete()都是批量删除删除前确认关联的外键行为——models.CASCADE会连带删掉子表数据。我遇到过不止一次在管理后台测试删除演出现场时把整个场次的座位记录全部删光恢复都不好恢复。所以在开发前期凡是涉及关联删除的操作建议先开一个 Django shell 做一次完整的事务演练确认级联行为符合预期再放进接口。5. Vue 前端开发与接口联调5.1 项目搭建、路由与组件设计前端用 Vue 3 Vite Vue Router Axios Element Plus 这套组合npm 命令如下npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router axios element-plusVue Router 路由配置对应的是页面结构。页面划分直接影响开发进度我这样规划// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(../views/EventList.vue) }, { path: /event/:id, component: () import(../views/EventDetail.vue) }, { path: /event/:id/seat, component: () import(../views/SeatSelect.vue), meta: { requiresAuth: true } }, { path: /orders, component: () import(../views/OrderList.vue), meta: { requiresAuth: true } }, { path: /login, component: () import(../views/Login.vue) }, { path: /register, component: () import(../views/Register.vue) }, ] const router createRouter({ history: createWebHistory(), routes, }) // 全局前置守卫检查登录状态 router.beforeEach((to, from, next) { if (to.meta.requiresAuth !localStorage.getItem(token)) { next(/login) } else { next() } }) export default router路由守卫是前后端分离项目里身份控制的关键一环。后端接口必须加权限校验前端路由守卫只是用户体验层面的拦截——没有 token 的用户直接跳转到登录页而不是等接口 401 了再被动处理。路由懒加载用 dynamic import初始包小首屏加载快这个优化建议保留。组件方面选座页是核心。我用一个二维数组描述座位图每个座位渲染成一个可点击的 div区域基本信息由后端返回。座位状态映射const seatStatusMap { available: { label: 可售, class: seat-available, disabled: false }, locked: { label: 已锁定, class: seat-locked, disabled: true }, sold: { label: 已售, class: seat-sold, disabled: true }, }选座时的交互逻辑是点击可选座位加入待选列表高亮显示再次点击取消确认后调用 /api/orders 接口创建订单。这里有一个体验细节——选座和提交是两个步骤选好座后不要直接创建订单而要先展示确认弹窗让用户看到票价合计后再提交减少误操作导致的锁座。5.2 Axios 请求封装与 token 处理Axios 请求封装是个必做工作。统一配置 baseURL、超时时间、请求拦截器自动带 token、响应拦截器统一处理错误码// src/utils/request.js import axios from axios import router from ../router const request axios.create({ baseURL: /api, timeout: 15000, }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request统一 token 处理省去了在每个接口重复写 Authorization 头的麻烦。响应拦截器里对 401 的全局处理也很关键——token 过期后任何接口都会自动跳登录页。所谓vue 播放 m3u8之类的东西在这个项目里用不上。如果后续要加演唱会直播或者宣传片功能可以考虑 hls.js 处理 m3u8 视频流但核心预约流程不需要。嵌入式 PDF 也不在功能范围里。做项目要聚焦别被边缘需求带偏。5.3 本地联调跨域问题一次性解决前后端分离开发最典型的问题是跨域。前端跑在 5173 端口后端 Django 跑在 8000Flask 跑在 5000直接发请求会被浏览器拦截。一个干净利落的方案是用 Vite 的 proxy 代理配置后在开发环境中前端请求/api前缀的接口都会转发到后端地址浏览器视角里是同源的不触发 CORS// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, // Django 后端Flask 则改 5000 changeOrigin: true, }, }, }, })使用 Vite proxy 等于在开发层把跨域问题屏蔽掉了后端的 CORS 中间件只在部署到生产环境、前后端完全分离域名时真正需要。有些同学开发时改了 Django 的 ALLOWED_HOSTS 和 CORS_WHITELIST其实没必要Vite proxy 方案最直接。注意代理配置生效的前提是前端请求路径确实以 /api 开头。如果后端路由不是这个前缀代理规则就没法匹配这是个很隐蔽的问题。统一约定 /api 前缀写在团队规范里。5.4 动态路由与页面状态管理热词里提到vue 动态路由vue 插槽这两个点在实际项目中都有用到。动态路由的一个典型场景是管理员的后台菜单——不同角色拥有不同权限前端根据角色动态注册路由。售票系统的用户端和管理端权限差异大管理端可以单独拆一个路由模块登录后根据用户角色动态添加。Vue 插槽主要用于组件复用。比如订单列表中的状态标签不同状态显示不同颜色和文案如果用插槽封装成 StatusTag 组件业务逻辑改动时只需要维护一处。在一个多接口的前端项目中组件复用是降低维护成本最重要的手段这点建议从一开始就坚持而不是写完了才回头重构。6. 实操过程从零搭建到前后端联调6.1 环境准备与项目初始化清单完整的实操顺序整理成一份清单照着走不会出错安装 Python 3.10/3.11在官网下载安装包勾选 Add to PATH。验证命令python --version。安装 PyCharm 社区版免费或专业版打开 PyCharm 后配置好 Python 解释器。创建后端项目目录在 PyCharm 终端执行虚拟环境初始化并安装 Django/Flask 依赖。初始化前端项目安装 Node.jsVite 要求 Node 16npm create vite初始化 Vue 项目。后端写模型、迁移数据库、创建超级管理员。后端写接口先用 PyCharm HTTP Client 或浏览器验证接口可用。前端写页面、配置代理。联调先在本地用两套服务跑通全流程。以 Django 后端为例数据库迁移的命令要在 PyCharm 终端执行python manage.py makemigrations python manage.py migrate python manage.py createsuperuser这三步完成数据库表就建好了admin 后台也能登录。6.2 一次典型的全流程跑通记录我开发时按这个顺序走先写好 users 应用的注册登录接口用 HTTP Client 发 POST 请求拿 token然后写 events 应用的两个查询接口确认返回 JSON 数据再写 orders 应用的下单和取消接口。接口层面全部用工具验证过才开始写 Vue 页面。写前端的时候一个页面接一个接口先用 Axios 直连后端验证成功后再搬到组件里。比如 EventList.vue 页面这样拉取数据script setup import { onMounted, ref } from vue import request from ../utils/request const events ref([]) const loading ref(false) onMounted(async () { loading.value true try { events.value await request.get(/events) } finally { loading.value false } }) /script template div el-card v-forevt in events :keyevt.id classevent-card h3{{ evt.title }}/h3 p{{ evt.artist }} | {{ evt.venue }} | {{ evt.show_time }}/p p剩余票量{{ evt.total_seats - evt.sold_count }}/p el-button click$router.push(/event/${evt.id})查看详情/el-button /el-card /div /template每次接口从后端拿到真实数据时及时在 PyCharm 的调试工具里确认入参出参。前后端联调阶段 90% 的问题都可以通过看请求到底发到哪了、返回了什么来解决。打开浏览器开发者工具的 Network 面板如果请求标红看状态码和响应体——404 多半是路径写错了500 就去翻后端日志CORS 错误检查代理配置401/403 检查 token。速度最快的排查方式一定是从网络层开始不要先把代码翻一遍。6.3 PyCharm 在日常开发中的高效用法这套流程里 PyCharm 给我的帮助集中在三处第一是数据库工具。右侧 Database 面板连上 SQLite 或 MySQL 后能直接查看表数据、执行 SQL省掉了单独打开数据库客户端的功夫。改完模型后顺手查一下表结构是否符合预期效率很高。第二是调试器。Django 接口返回错误时在视图函数里打断点用调试模式启服务就能看到每个变量具体值。尤其是下单接口的事务逻辑断点调试可以完整看到 select_for_update 的上锁效果。第三是 HTTP Client。在 .http 文件里写好接口测试用例比如创建订单输入非法座位 ID取消不存在的订单等等之后每次改完代码点一下就能回归测试比 Postman 更贴近代码。热词里有pycharm 安装 ai 插件pycharm 插件推荐实际上 AI 插件辅助写正则表达式、生成 SQL 这类琐碎工作是可行的但核心业务逻辑仍然建议自己逐个字段检查毕竟售票系统涉及真金白银和用户体验。7. 常见问题与排查技巧实录7.1 环境与启动问题速查问题现象解决办法python 命令无法识别终端提示 not found安装时勾选 Add to PATH或者重新安装pip 下载慢或失败安装依赖超时换国内镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名数据库迁移时找不到表django.db.utils.OperationalError先执行 makemigrations 再 migrate不要跳步确认 settings.py 里 DATABASES 配置正确Django 启动报端口占用Error: That port is already in uselsof -i:8000找到占用进程并 kill或换端口Vue 启动失败npm install 报 peer deps 冲突按提示加--legacy-peer-deps重装或统一依赖版本前端请求 404打开 Network 面板发现请求 URL 少前缀在 axios 请求地址前统一加 /api确认路由定义7.2 接口联调阶段的隐蔽问题让我挑三个实际调试时最头疼的问题详细展开。第一个是后端日期格式的时区问题。Django 默认 USE_TZTrue拿到的时间是 UTC 的 ISO 字符串前端显示出来比北京时间慢了 8 小时。解决办法是 settings.py 里确认TIME_ZONE Asia/Shanghai并注意 Django 在 USE_TZTrue 时存储到数据库仍然是 UTC。专门做序列化时可以用:import pytz from rest_framework import serializers class EventSerializer(serializers.ModelSerializer): show_time serializers.DateTimeField(format%Y-%m-%d %H:%M, default_timezonepytz.timezone(Asia/Shanghai))这个坑在小项目里不明显一旦上线部署到服务器时区问题立刻暴露。建议一开始就明确所有时间字段都按用户当地时区前端转换前端可以写一个全局过滤器统一格式化。第二个是 SQLite 在并发写场景下的锁问题。我本地模拟高并发时发现多个线程同时写 SQLite 会报 database is locked。因为 SQLite 同一时刻只允许一个写事务。开发阶段能用但高并发验证做不了比较稳妥的做法是尽早把数据库切到 MySQL 或 PostgreSQL。Django 切换只需改 settings 配置数据迁移用 fixture 导出再导入。Flask 切换 SQLAlchemy 连接串也简单。不要等到上线前才发现 SQLite 扛不住。第三个是 Vue 打包后路由刷新 404。前端npm run build之后部署到 Nginx刷新子页面路径时出现 404这是 SPA 的典型问题——静态服务器不知道前端路由。因为 dev 环境 Vite 已经处理了 history fallback生产环境需要 Nginx 配置try_fileslocation / { try_files $uri $uri/ /index.html; }这一条配置能解决所有刷新 404 的问题。如果后端也部署在同一域名下还需要把 /api 路径反向代理到后端服务。7.3 并发抢票数据一致性经验回到最核心的抢票场景。实测体会是写代码前先把并发模型想清楚比事后加锁重要得多。真实项目里我建议按优先级这样设计Redis 预扣库存承载瞬时流量。可售座位数存 Redis用户进入选座页时从 Redis 读取实时余票提交订单时通过 Lua 脚本原子性地将座位从 available 集合移动到 locked 集合。数据库行锁保证最终一致性。Redis 被击穿或者数据不一致时数据库事务中的select_for_update()是兜底防线。定时任务释放过期锁座。兜住用户锁座后长时间不支付的极端情况保证座位可以回归库存池。这三层如果不要求系统能顶住十万级并发其实可以简化为一条经验数据库事务加行锁已经足够应付一个演唱会项目的正常流量几千到几万用户同时在线。真正的难点反而在于把状态机逻辑写对、把锁的粒度控制好不要锁整张表只锁需要修改的那几个座位行。7.4 几个值得保留的实战技巧最后分享几个我做完这个项目后最想保留的小技巧后端返回错误信息时统一封装成{code: 40000, message: 座位已被抢走}这样的结构。不要把异常堆栈直接返回给前端既暴露服务器信息又不利于前端展示。统一错误码的好处是前端响应拦截器可以根据 code 做全局处理而不是反复判断 HTTP 状态码。座位图的渲染成本在座位数量多的时候是个隐形问题。一个大型体育馆可能有几千个座位一次性渲染全部 DOM 节点会导致页面卡顿。正确做法是只渲染可视区域内的座位虚拟滚动或者在选座页先渲染当前选中区域的座位切换区域再加载。开发时我用 Element Plus 的分页标签做区域切换每个区域 200 个座位左右渲染压力完全可接受。运营后台里售罄的场次应该自动隐藏或标注。这个逻辑可以放在后端查询时处理sold_count total_seats的场次 filter 掉。前端也做第二层保险显示已售罄标签点击按钮置灰。双重保障避免用户尝试下单却发现没座位。数据库备份这件事要养成习惯。开发过程中改模型、跑迁移、乱删数据是常态我因为这个项目交过学费——一次误操作把测试环境订单全删了幸好提前导出了 fixture。PyCharm 里配置一个定期数据库导出任务非常简单花两分钟配好能省掉很多后悔药。8. 部署与后续扩展方向8.1 前后端分离部署方案部署这个项目我推荐用一台轻量云服务器2核4G 起步装好 Nginx 和 Python 环境。前端打包后将 dist 目录丢到 Nginx 静态目录后端用 Gunicorn 跑 Django 或 Flask 服务Nginx 反向代理 /api 到对应的端口。Django 静态文件和 Media 文件的处理在部署时容易让人困惑。如果 Django 的 admin 后台也要用需要执行python manage.py collectstatic并且让 Nginx 直接服务这些 static 文件否则 admin 样式会全部丢失。Flask 的静态资源处理相对简单因为 Flask 项目往往只提供 API前端页面完全由 Vue 负责。8.2 项目后续可以怎么扩展这套售票预约系统的骨架很清晰扩展方向也明确管理后台目前只是 DTRF 的 ReadOnly 接口可以加一个 Vue 的管理端页面支持场次创建、座位批量生成、订单明细查看支付环节当前是模拟支付用户确认后直接标记已支付后续可以接入微信支付或支付宝的预下单流程如果要做真实抢票建议引入 Redis 和消息队列把库存扣减和订单写入解耦。就到这里说说我做完整个项目的最大感受前后端分离项目的复杂度不在于单点技术多深而在于把数据模型、接口契约、前端页面、并发边界理顺。如果这篇文章能帮你在自己的售票预约系统开发中少踩几个坑那这份记录就没白写。
返回列表