ARTICLE DETAIL

资讯详情

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

Python+Vue前后端分离演唱会购票系统开发实战

Python+Vue前后端分离演唱会购票系统开发实战 想做演唱会、音乐会购票系统的朋友多半是接到了课程设计、毕业设计或者单纯想练手前后端分离开发。这个题目从技术栈上看非常典型Python 负责后端逻辑Vue 负责前端交互Django 或 Flask 提供接口PyCharm 作为开发环境。我前前后后用这套组合写过两三个类似的项目从学生购票到小型场馆订座都跑过踩了不少坑也攒了一些能直接用的经验。这篇就把整个系统的设计思路、核心代码、开发环境和排错方法一次性说清楚照着做你也能交出一个能演示、能答辩、能上线的购票管理平台。1. 项目概述与需求拆解1.1 这个购票系统到底要做什么演唱会、音乐会的购票系统表面看就是“用户选场次、选座位、下单、付款、拿到电子票”。但真落到系统设计上它要比普通的电商系统多一层复杂度座位是二维的一场演出可能有几百上千个座位每个座位在同一场次只能被一个人锁定热门演出还要面对多用户同时抢票的并发压力。再加上后台需要管理演出、场馆、场次、订单、用户整个系统就是一个典型的“前台展示 后台管理 订单交易”全栈项目。我用 Python Vue 实现时把系统分成了两类角色普通用户注册登录、浏览演出列表、查看演出详情、选择场次和座位、创建订单、模拟支付、查看我的订单和电子票。管理员维护演出信息、管理场馆和座位图、查看所有订单、处理退票或改签、统计售票数据。如果这是课程设计把上面这些功能做成页面和接口已经能覆盖“完整的管理系统”这个评价标准。如果是自己想练技术建议在订单模块上多花心思把并发扣库存、座位锁定、事务回滚这些细节做扎实这才是面试官或老师真正会追问的点。1.2 需求模块与核心流程我习惯先列数据流向再写代码。一遍跑通的核心流程是用户注册登录 → 浏览演出 → 选择一个场次 → 进入选座页 → 点击座位此时座位被临时锁定→ 确认订单 → 支付模拟→ 订单完成 → 我的票据生成。管理员流程就简单很多登录后台 → 新增演出关联场馆和场次→ 初始化座位 → 查看订单 → 处理退款。从这些流程里可以拆出下面几个核心模块模块对应功能关键难点用户模块注册、登录、信息修改密码加密、Token 认证演出模块演出列表、详情、场次图片上传、多场次关联场馆与座位模块场馆管理、座位图展示座位二维数据建模、选中状态订单模块创建订单、库存扣减、支付状态并发锁、事务、座位锁定后台管理模块CRUD 接口、数据统计权限控制、分页搜索把模块理清楚之后无论你写 Django 还是 Flask代码结构都不会乱。1.3 技术选型Django还是FlaskVue怎么选标题里同时出现了 Django 和 Flask很多人会纠结。我的建议做多表关联复杂的系统直接上 Django。Django 自带 ORM、Admin、用户认证体系开发购票系统这种模型关联多的项目省掉大量底层工作。Flask 更适合非常轻量的单模块接口如果选 Flask你要自己集成 SQLAlchemy、Flask-RESTful、JWT 扩展代码自由但工程量更大。在我的项目里后端用 Django Django REST Framework理由很实际ORM 管理 User、Order、Seat、Performance 这些外键关系非常顺手。DRF 的序列化器和 ViewSet 能快速生成 RESTful API。Django Admin 可以直接当后台管理页的雏形后期再单独做 Vue 后台也不冲突。如果你想用 Flask只要保持接口语义不变前端完全不用动因为 Vue 只关心 HTTP 接口。前端的 Vue 我建议用 Vue 3 Vue Router Axios Element Plus组件化和生态都成熟表格、表单、弹窗这些管理界面组件都能直接用。如果你还要做后台管理页面可以再配一个 vue-element-admin 风格的后台模板比自己从头画界面快太多。2. 系统架构与数据模型设计2.1 前后端分离的整体架构这个项目我用的是前后端完全分离后端跑在 8000 端口提供 JSON 接口前端跑在 8080 端口开发时通过代理转发请求。整体请求链路是浏览器 → Vue 页面 → Axios 发送请求 → Vue CLI Dev Server 代理 → Django/Flask 接口 → ORM 读写数据库 → 返回 JSON → Vue 渲染页面。这里最容易被新手忽略的是“代理”。如果不配置代理前端直接请求http://localhost:8000/api/...会因为跨域被浏览器拦截。我用 Vue CLI 时会在vue.config.js里写module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8000, changeOrigin: true } } } }这样前端代码里所有请求都写/api/xxx浏览器访问 8080 时Vue 开发服务器会把请求转发到 8000绕开跨域。部署时再用 Nginx 做反向代理把同一域名下的/api转发到后端前端静态文件直接由 Nginx 托管彻底解决跨域。2.2 数据库模型设计详解用 Django 的 ORM 定义模型最关键的几张表是用户、演出、场馆、场次、座位、订单。我先说设计思路再给核心代码。用户表直接继承 Django 自带的AbstractUser不要自己从零建用户表。后续如果要加手机号、昵称、头像扩展字段就行。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone models.CharField(max_length11, blankTrue) avatar models.URLField(blankTrue) class Meta: db_table user场馆与座位场馆有名称、城市、地址。座位属于某个场馆用行号、列号、区域和类型普通/ VIP来定位。class Venue(models.Model): name models.CharField(max_length128) city models.CharField(max_length64) address models.CharField(max_length255) class Seat(models.Model): venue models.ForeignKey(Venue, on_deletemodels.CASCADE, related_nameseats) row models.IntegerField() column models.IntegerField() area models.CharField(max_length50, default内场) seat_type models.CharField(max_length20, choices((normal, 普通), (vip, VIP))) price models.DecimalField(max_digits8, decimal_places2)这里的row和column是座位图的关键。前端拿到一个场次的所有座位后根据row和column在一个二维网格里绘制位置用户点击某个座位时传给后端的是“场次 ID 座位 ID”。演出与场次演出是静态信息名称、艺人、海报、介绍场次才是真正要卖票的对象因为一场演出可能有多场比如周末两天。class Performance(models.Model): title models.CharField(max_length128) artist models.CharField(max_length64) poster models.URLField() description models.TextField() class Session(models.Model): performance models.ForeignKey(Performance, on_deletemodels.CASCADE, related_namesessions) venue models.ForeignKey(Venue, on_deletemodels.CASCADE) start_time models.DateTimeField() # 同一个场次下座位和价格关联到 Session 而不是 Performance这里有个细节同一个座位在不同场次的价格可能不一样比如首场更贵。所以“价格”不应该只挂在 Seat 上最好放到“某个场次下的某个座位”这个中间关系里。简单做法是直接在 Session 下建一个SessionSeat表字段包括 session、seat、status、order。这样状态管理最精确。class SessionSeat(models.Model): session models.ForeignKey(Session, on_deletemodels.CASCADE, related_namesession_seats) seat models.ForeignKey(Seat, on_deletemodels.CASCADE) status models.IntegerField(default0) # 0 可售1 锁定2 已售 order models.ForeignKey(Order, nullTrue, blankTrue, on_deletemodels.SET_NULL) price models.DecimalField(max_digits8, decimal_places2)订单表订单和用户关联还要和票关联。一张订单可以包含多张票也就是多个 SessionSeat。class Order(models.Model): order_no models.CharField(max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameorders) created_at models.DateTimeField(auto_now_addTrue) total_amount models.DecimalField(max_digits10, decimal_places2) status models.IntegerField(default0) # 0 待支付1 已支付2 已取消3 已退款 pay_time models.DateTimeField(nullTrue, blankTrue)这些模型关系理清后后面的接口就都是“对模型的增删改查”。如果一开始关系设计错了后面都要返工最典型的就是把价格挂在 Seat 上结果不同场次改价非常痛苦。2.3 接口协议与统一返回格式前后端分离最难的不是写接口而是约定接口格式。我统一使用 RESTful 风格所有接口返回这样的 JSON{ code: 0, message: success, data: {} }code 0表示成功非 0 表示业务错误。前端 Axios 拦截器统一判断service.interceptors.response.use( response { const res response.data if (res.code ! 0) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { Message.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )这样写的最大好处是前端所有业务逻辑里不需要再重复判断“请求是不是成功”只拿data用就行。如果有人说“我不想用 code直接用 HTTP 状态码”也可以但我还是建议保留业务 code因为比如“座位被抢”这类业务错误用 HTTP 200 code 2 来表达更优雅前端处理逻辑更统一。3. 核心功能实现与踩坑实录3.1 从零搭建 Django 后端与用户认证创建虚拟环境和 Django 项目前面会细说这里直接讲代码。安装依赖pip install django djangorestframework django-cors-headers创建项目和应用django-admin startproject ticket_system cd ticket_system python manage.py startapp users python manage.py startapp performances python manage.py startapp orders用户认证我直接用 DRF 的TokenAuthentication或JWT。课程设计用 JWT 更现代推荐djangorestframework-simplejwt。配置好之后注册和登录接口不需要自己手写 Token 逻辑几行代码就能搞定。from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class MyTokenObtainPairSerializer(TokenObtainPairSerializer): def validate(self, attrs): data super().validate(attrs) data[user_id] self.user.id data[username] self.user.username return data class MyTokenObtainPairView(TokenObtainPairView): serializer_class MyTokenObtainPairSerializer注册接口需要注意密码不能被明文返回所以序列化器里要重写create方法用set_passwordclass UserRegisterSerializer(serializers.ModelSerializer): password serializers.CharField(write_onlyTrue) class Meta: model User fields (id, username, password, email, phone) def create(self, validated_data): user User.objects.create_user( usernamevalidated_data[username], passwordvalidated_data[password], emailvalidated_data.get(email, ), phonevalidated_data.get(phone, ) ) return user这里踩过的坑如果直接用User.objects.create(**validated_data)密码是明文存库登录永远失败。我当年第一次做的时候就被这个坑了半小时。3.2 演出信息、场馆与座位管理后台管理的核心是演唱会的 CRUD。用 DRF 的ModelViewSet可以极大减少代码量class PerformanceViewSet(viewsets.ModelViewSet): queryset Performance.objects.all().order_by(-id) serializer_class PerformanceSerializer pagination_class StandardResultsSetPagination路由用 DRF 的router注册router.register(performances, PerformanceViewSet)这样/api/performances/的 GET、POST、PUT、DELETE 全部自动生成。管理员要做的只是在前端做对应的表单页面。至于“某个场次的座位图”核心接口是class SessionSeatListView(generics.ListAPIView): serializer_class SessionSeatSerializer def get_queryset(self): session_id self.kwargs[session_id] return SessionSeat.objects.filter(session_idsession_id).select_related(seat)返回的座位列表包含seat_row、seat_column、status、price前端拿到后按行列渲染成矩形块就完成了座位图。如果你用的是 Flask结构同样是这样只不过把 ORM 换成 SQLAlchemy序列化换成flask-restful的marshal_with。功能实现没有本质区别。3.3 下单与库存扣减的并发处理这是整个系统最值得写的部分。用户点击“立即购买”时前端会传来一个座位 ID 列表。后端要做三步检查这些座位在当前场次是否全部可售。把这些座位状态置为锁定并生成订单。一段时间未支付则释放座位。最直接的实现是在 Django 里用select_for_update()加行锁from django.db import transaction transaction.atomic def create_order(user_id, session_id, seat_ids): locked_seats SessionSeat.objects.select_for_update().filter( session_idsession_id, seat_id__inseat_ids, status0 ) if locked_seats.count() ! len(seat_ids): raise BusinessError(部分座位已被锁定请重新选择) # 生成唯一订单号 order_no f{time.strftime(%Y%m%d%H%M%S)}{user_id}{random.randint(100, 999)} order Order.objects.create(order_noorder_no, user_iduser_id, status0) total 0 for s in locked_seats: s.status 1 # 锁定 s.order order s.save() total float(s.price) order.total_amount total order.save() return order这里的select_for_update()会在数据库层面锁住符合条件的行直到事务结束。两个用户同时抢同一个座位时第二个请求会等待第一个事务提交然后查到status已经不是 0自然失败。如果没有这个锁使用“先查再改”的普通逻辑两个请求都会觉得自己买到票了最后超卖。如果没有支付就退出前端要主动调用取消接口释放座位后端也可以写一个定时任务把超过 15 分钟未支付的订单状态为 0 的订单还原座位。课程设计阶段用一个释放接口就够。3.4 Vue 前端页面与 API 联调前端页面架构我建议分成公共布局 路由视图。核心页面有首页横幅轮播 热门演出列表演出详情页海报、场次选择选座页面网格座位图 价格区订单确认页选中座位、金额、提交按钮我的订单页订单状态、电子票二维码登录 / 注册页选座页面是重点。拿到座位列表后我按row和column构建二维数组const grid {} seatList.forEach(item { const key ${item.row}-${item.column} grid[key] item })然后在模板中按场馆最大行和列循环div v-forrow in maxRow :keyrow classseat-row div v-forcol in maxCol :keycol classseat-cell div v-ifgrid[${row}-${col}] :class[seat, { selected: selectedSeats.includes(grid[${row}-${col}].id) }] clickclickSeat(grid[${row}-${col}]) {{ row }}-{{ col }} /div /div /div这里有个体验细节用户连续点击多个座位时已经锁定的座位不能选自己选中的座位再点一次要能取消。所以clickSeat里要更新selectedSeats数组并且只允许同一个场次下选择最多 6 张票视项目限制而定。Axios 调用时所有需要登录的接口带上 JWT Tokenservice.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })页面联调阶段最常见的报错是 401原因基本是 Token 过期或者没传到后端。用 Vue Router 的全局前置守卫在进入“我的订单”这类页面时检查有没有 Token没有就跳登录页。4. 开发环境配置与 PyCharm 高效实践4.1 Python、虚拟环境与依赖安装关于 Python 版本我建议直接用 3.10 或 3.11。Django 4.x 和 Flask 2.x 都支持新版本对类型提示和 ORM 特性也更友好。安装 Python 之后一定要创建一个虚拟环境不然项目依赖会污染全局环境python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate进入虚拟环境后再安装依赖pip install django djangorestframework django-cors-headers djangorestframework-simplejwt pillow pip freeze requirements.txtpillow是处理图片上传时必装的Django 的ImageField依赖它。生成 requirements.txt 后换机器部署直接pip install -r requirements.txt就能复现环境。如果你选 Flask则安装pip install flask flask-sqlalchemy flask-restful flask-jwt-extended flask-cors虚拟环境的好处还在于PyCharm 可以把它识别为项目解释器避免出现“在终端装好了包但 PyCharm 里 import 报红”这种常见问题。4.2 创建 Vue 项目并集成 Element UI前端建议用 Vue CLI 或 Vite 创建工程。Vite 更快但如果你在课程设计里需要用到较老的教学资料也可以选 Vue CLI。创建命令npm create vuelatest # 或者旧版 npm install -g vue/cli vue create ticket-frontend创建后安装核心依赖npm install axios vue-router4 element-plus在main.js里注册 Element Plus 和路由import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(ElementPlus) app.use(router) app.mount(#app)Element Plus 能帮我快速搞定表格、表单、分页、弹窗尤其是后台管理页面省去手写 CSS 的工作量。对购票系统这种“管理界面多”的项目非常合适。4.3 PyCharm 运行与调试技巧PyCharm 的配置要点有三个设置解释器Settings → Project → Python Interpreter → 选择虚拟环境venv/bin/python。配置 Django 运行方式Run → Edit Configurations → 添加 Django Server → 填项目路径和端口8000。开启 Debug 模式Debug 运行后在接口函数里打断点可以看每一步变量值比用 print 高效太多了。我在调试下单接口时特别依赖 PyCharm 的断点看select_for_update()锁定后的事务状态。有时候并发问题用 print 根本看不出来必须逐步看 SQL 查询。前端调试建议在 Chrome 里安装 Vue Devtools用 Network 面板看接口请求和响应。如果某个接口报 500第一时间去 PyCharm 的 Run 控制台看异常堆栈不要在前端瞎猜。5. 常见问题排查与部署建议5.1 跨域问题与解决前面提到 devServer 代理但如果你直接请求后端地址还是需要后端允许跨域。Django 装django-cors-headersINSTALLED_APPS [ corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]注意CorsMiddleware要放在CommonMiddleware前面否则可能不生效。Flask 则用flask-corsfrom flask_cors import CORS CORS(app)排错的时候还可以看浏览器 Network 响应头里有没有Access-Control-Allow-Origin。没有就是后端口没配置有但前端报错就可能是请求头里带了不被允许的字段比如 Authorization。5.2 数据库迁移与数据初始化Django 里模型写完执行python manage.py makemigrations python manage.py migrate python manage.py createsuperuser迁移时报错最多的场景是“外键字段类型不一致”。比如 Seat 的 venue 默认外键到 Venue但你在数据库中手工插入了一条 venue_id0 的数据迁移会因为违反约束失败。解决方法是先清理脏数据再迁移或者给外键设置db_constraintFalse但一般不推荐。购票系统如果没有真实演出数据演示效果会非常差。我建议写一个init_data.py脚本自动创建几个演唱会、场馆、座位和测试账号。用 Django 的manage.py shell执行最方便python manage.py shell init_data.py脚本里用get_or_create避免重复插入例如venue, _ Venue.objects.get_or_create(name国家体育场, city北京) for row in range(1, 6): for col in range(1, 6): Seat.objects.get_or_create(venuevenue, rowrow, columncol)5.3 构建部署与上线注意事项前端构建npm run build会在dist目录生成静态文件。部署我推荐 Nginx 托管dist然后把/api反向代理到后端的 Gunicorn 服务server { listen 80; server_name your-domain.com; location / { root /var/www/ticket-frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }后端生产环境不要用 Django 自带的 runserver改用 Gunicornpip install gunicorn gunicorn ticket_system.wsgi:application -b 127.0.0.1:8000部署后常见问题有三个静态文件 404需要python manage.py collectstatic并配置STATIC_ROOT。数据库连接数不够调低数据库连接池或者增加超时时间。前端的路由刷新 404Nginx 里的try_files配置必须保留/index.html这个兜底。我在实际部署时还遇到过 Vue 打包后接口地址忘记改的问题。因为开发时前端请求/api是相对路径到了生产环境仍然请求同域名的/api只要 Nginx 转发配置没问题就行。如果是打包后要请求另一个域名的接口记得在.env.production里配置VUE_APP_BASE_API。5.4 其他高频问题速查问题现象可能原因解决方法前端请求 CORS 报错后端未配置跨域配置django-cors-headers或flask-cors登录后访问订单接口 401Token 未携带或过期检查前端拦截器刷新 Token下单时提示座位已锁定有未支付的订单占用座位调用取消接口释放座位或定时清理图片上传报错缺少 Pillow 或 MEDIA 路径没配pip install pillow配置MEDIA_URLVue 页面刷新 404前端路由为 history 模式Nginxtry_files或改为 hash 模式数据库表时间显示不对时区未设置Django 设置TIME_ZONE Asia/ShanghaiUSE_TZ False提示如果是纯课程设计不需要用 Nginx把后端跑在本地前端用npm run dev演示时浏览器开两个页面照样能说明问题。生产部署只作为加分项。写在最后的实战建议我前后重做过三次购票系统最大的心得体会是先把数据模型和接口约定写清楚再动 UI。这个系统涉及的“演出-场次-座位-订单”关系链很长一旦中间表设计疏忽后面选座、下单、退款每一环都会受影响。如果你只有两三天时间优先保核心流程登录、看演出、选座、下单、订单展示。后台的编辑功能用 Django Admin 先撑住不要一开始掉进管理界面的细节里。另一个容易被忽略的点是座位状态的“锁定”与“释放”。哪怕不做并发锁至少要在订单取消时写一个释放逻辑否则演示时点过的座位永远灰掉观感很差。我在做第一个版本时就吃过这个亏用户下单后直接关掉浏览器座位一直被锁着后台也没法批量恢复只能手动改数据库。如果你打算把这个项目扩展成毕设或者实际运营系统后续可以加短信验证码登录、Redis 缓存热门演出、座位分区定价、支付接口对接、邮件发送电子票。这些方向每个都够单开一篇但基础架构就是我上面讲到的这些把地基打结实往上加功能是很自然的事。最后再分享一个小技巧开发时一定要看 Django 的 Run 面板和浏览器 Network 面板哪个接口状态不对两个地方一对照基本就能定位。别在一堆 print 里找错误用 PyCharm 断点效率高得多。祝你的购票系统一次跑通答辩顺利。
返回列表