2026/10/6 21:24:06

Django+Vue全栈实战:电影票购买系统设计与开发

Django+Vue全栈实战:电影票购买系统设计与开发 1. 项目整体设计与技术选型思考1.1 需求定位与系统模块拆分电影票购买系统是一个特别适合练手、同时又容易做深的全栈项目。表面上看就是“查电影、选场次、选座位、付款、出票”这么一条链路但真正拆开之后你会发现它涉及用户认证、数据建模、高并发选座、订单状态机、支付回调等多个专业话题。我这次做这个项目目标很明确用 Python 做后端接口用 Vue 做交互页面在 PyCharm 里完成开发调试最后能跑出一个可以演示、可以扩展的成品。先把功能模块切开避免一头扎进代码里出不来。整个系统我划分为五个核心模块会员模块管用户注册、登录、个人信息电影模块管影片信息、海报、上映日期场次模块管电影排片、放映厅、票价选座购票模块管座位图展示、座位锁定、订单生成模拟支付模块负责把订单状态从“待支付”推到“已支付”。管理员侧另加一个简单的影院后台用来维护电影和场次数据。这个划分方式的好处是前后端可以各管各的后端不会变成一个臃肿的文件前端也能按页面拆组件。之所以强调模块划分是因为我发现很多初学者做项目喜欢一股脑把所有逻辑写到一个文件里。开发到后边连自己都分不清哪里改的是电影、哪里改的是订单。模块经过划分后你可以把问题快速定位到具体一环这对后面引入单元测试、团队分工、甚至重构都非常重要。1.2 Django 与 Flask 到底怎么选这是最容易卡壳的问题。题目里同时出现了 Django 和 Flask原因也比较常见Django 适合做“全套全家桶”Flask 适合做“轻量自定义”。那这个电影购票项目到底用哪个我建议按你的项目复杂度来而不是按哪个框架更火来。对比项DjangoFlask自带组件ORM、Admin、Auth、表单、迁移大部分需要第三方扩展开发速度一开始搭模型和后台很快前期代码量少但自主拼装API 生态Django REST Framework 成熟稳定Flask-RESTful / Flask-Restx学习曲线有一些概念要先理解入门容易架构靠自律适合规模中大型、多表关联、后台需求多中小型接口服务、自定义程度高我在实际项目里用的是 Django Django REST Framework理由很直接电影、场次、订单、用户这几张表之间有关系Django 的 ORM 和 Model 机制能够非常顺手地处理外键和一对多关系再加上 Django Admin 那个免费后台我可以少写不少管理页面。但如果你希望整个后端只有四十行代码就能启动或者你只想专注写一堆独立接口而不关心后台管理那 Flask 更合适。Flask 的灵活度确实高只是数据迁移、用户认证、权限控制这些都要你自己搭配。比如你可以在 Flask 中用 Flask-SQLAlchemy 来写 ORM用 Flask-Migrate 做迁移用 Flask-JWT-Extended 做登录态认证。配置起来也很快只是没有 Django 那种“开了箱就有一整套”的省事。这里给大家一个比较务实的建议如果你的毕设或简历项目重点在“完整的业务闭环”选 Django如果你的重点在“接口设计与高并发优化”选 Flask。两种选择都正确但别在开发中途频繁切换框架那才是灾难。1.3 为什么前端选 Vue以及整体架构怎么理解前端从传统的 Jinja2 模板切到 Vue最大的变化是把界面拆成了“数据驱动”的组件。电影票的选座交互尤其适合 Vue座位状态无非是可选、选中、已售三种状态在 Vue 里我只需要用一个数组维护选中座位的编号然后让模板根据这个数组自动切换 class界面就会实时刷新。整个项目的最终结构是前后端分离Django 只对外提供 JSON 接口不渲染 HTML 页面Vue 负责构建页面和交互用 axios 访问后端的api地址拿数据。开发阶段两个服务可以同时跑在本地前端 8080 端口、后端 8000 端口通过代理转发接口请求部署阶段则可以把前端打包成静态文件由 Nginx 托管后端用 uWSGI/Gunicorn 启动。这套架构的好处是前后端可以独立开发、独立部署也很符合现在主流业务团队的工作方式。2. 环境准备与开发工具配置2.1 Python 环境安装与依赖管理工欲善其事必先利其器。我建议先把 Python 版本统一到 3.10 或更高。命令行里输入python --version确认当前版本版本过低要先更新。之后我强烈建议你在项目目录下建一个虚拟环境不要全局安装依赖。# 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境macOS / Linux source venv/bin/activate # 安装后端依赖 pip install django djangorestframework django-cors-headers pymysql为什么不推荐全局安装原因很简单Django 项目一旦多了不同项目依赖的版本会互相打架。今天 A 项目需要 Django 3.2明天 B 项目可能就要 Django 5.0全局环境会变得混乱。虚拟环境把每个项目的依赖完全隔离想删就删想重建也不影响其他项目。依赖装完以后最好把包清单导出来方便别人 clone 之后快速安装pip freeze requirements.txt后面换电脑或者换环境部署时用pip install -r requirements.txt就能把依赖一次装齐。2.2 Vue 开发环境初始化Vue 这边需要先有 Node.js。到这里检查一下版本建议 Node 16 以上。然后我用 Vue CLI 这类官方脚手架创建项目# 全局安装 Vue CLI npm install -g vue/cli # 创建前端项目 vue create cinema-frontend创建过程中会有几个交互选项我通常选择默认预设后面再手动补充。如果你更想用 Vite 的形式启动项目也可以执行npm create vuelatestVite 在冷启动速度上会更快对开发体验的提升非常明显。但不管选用哪种方式都要记得项目初始化完以后先跑一遍npm install npm run serve页面能正常打开一个 Vue 默认首页的时候再进行下一步的开发省得后面集成时还要排查环境层面的问题。这里有一个新手常见误区看到别人直接复制一个node_modules文件夹过来就以为项目没问题了。不同系统、不同 Node 版本下依赖的二进制文件可能不兼容正确做法是只保留package.json和package-lock.json然后在本地重新npm install。2.3 PyCharm 里开发前后端项目的技巧PyCharm 是我这几年用得比较顺手的 Python IDE社区版已经足够做 Django 项目开发。打开项目以后第一件事是把 Python 解释器切换到刚才创建的venv环境。具体路径是File → Settings → Project: xxx → Python Interpreter选择venv下的python.exe这样 PyCharm 里的终端、运行配置都会默认使用这个虚拟环境。前端代码在 PyCharm 里写也没有问题把frontend目录作为一个 Node.js 项目来识别。为了方便我会在编辑器的终端里启动npm run serve或者给 PyCharm 配置一个 npm 的运行任务。这样按一下运行按钮就能启动前端也能看到日志输出。PyCharm 还有几个提高效率的配置点代码自动保存、Git 集成、TODO 注释高亮、数据库工具窗口。如果你用的是专业版甚至可以直接在 PyCharm 里连接 MySQL查看表结构和查询结果非常适合后端调试。3. 后端数据库设计、接口开发与业务逻辑3.1 数据表与字段设计思路电影购票系统最关键的艺术就在于数据库表设计。我按照业务链路整理成五张核心表用户表 Userid、username、password_hash、mobile、created_at。密码不能存明文这是常识。Django 自带的make_password和check_password可以直接处理。电影表 Movieid、title、poster_url、duration、summary、release_date、status。status字段用来控制影片是即将上映还是正在热映因为影院不会把同一部电影永远排下去。场次表 Sessionid、movie外键指向电影、hall、start_time、price。这里为什么要把场次单独拆出来因为同一部电影一天可能有多场不同时间、不同影厅的价格还可能不一样把场次挂到电影下面形成一对多关系购票才有意义。座位表 Seatid、session外键指向场次、row、column、status。注意这里的座位并不是一个静态的影厅座位字典而是每个场次都会生成一套独立座位记录。这样做虽然会多出一点数据量但好处是能精确记录每个座位在每场中的锁定状态不会出现“两场比赛共享同一组状态”的错误。订单表 Orderid、order_no、user外键指向用户、session外键指向场次、seats、amount、status、created_at。seats字段可以存座位编号的拼接字符串比如3排5座,3排6座同时用status标记待支付、已支付、已取消。这里最需要强调的是一个常见反例有人会把“座位是否已售”直接写到场次的 JSON 字段里或者把所有订单存成一个文本字段。这样做开发demo 跑起来是很快但一旦要查“某场还剩多少个座位”或者“这个用户买过哪些电影票”就会非常痛苦。表结构清晰一点后面写接口和统计都会轻松。3.2 REST API 设计与路由实现我使用 Django REST Framework 做接口层先列出来核心路由方法路径功能权限POST/api/auth/register/用户注册匿名POST/api/auth/login/用户登录匿名GET/api/movies/获取电影列表匿名GET/api/movies/{id}/获取电影详情匿名GET/api/sessions/?movie_idxx获取电影场次匿名GET/api/sessions/{id}/seats/获取场次座位匿名POST/api/orders/创建订单登录用户POST/api/orders/{id}/pay/模拟支付登录用户GET/api/orders/我的订单列表登录用户在 Django 里路由配置写在urls.py而每个接口的逻辑放在视图中。我建议用 DRF 的APIView或ViewSet因为它们能让你少写很多序列化代码。举个例子电影列表接口可以这样写# movies/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import Movie from .serializers import MovieSerializer class MovieListView(APIView): def get(self, request): movies Movie.objects.filter(statusTrue) serializer MovieSerializer(movies, manyTrue) return Response(serializer.data, statusstatus.HTTP_200_OK)如果你选 Flask对应的写法是用蓝图和类视图# main.py 的 Flask 版本示意 from flask import Blueprint, jsonify from models import Movie movie_bp Blueprint(movie, __name__) movie_bp.route(/api/movies/) def movie_list(): movies Movie.query.filter_by(statusTrue).all() return jsonify([m.to_json() for m in movies])Flask 胜在小巧但没有 DRF 的 Serializer 那么多现成组件字段校验需要自己写出错时反而要多花时间。我这里两种代码都放出来方便你对比后选择顺手的那套。3.3 查询、删除对象与 ORM 使用细节那个热搜词里刚好有“django 执行查询-删除对象”我就多展开一点。Django ORM 的删除操作看起来是Model.objects.get(...).delete()一行代码但背后有很关键的细节。比如我要删除一个已经有人下单的场次单纯删除Session会导致Order里的外键变成一个“悬空引用”业务上来讲这是不允许的。正确的做法是先判断是否有依赖数据或者设置数据库外键的级联策略。Django 模型里可以用on_deletemodels.CASCADE让关联数据一并删除也可以用PROTECT阻止删除被引用的对象。# 错误示例直接删除可能有订单的场次 Session.objects.get(id1).delete() # 更合理的做法先检查依赖订单 order_count Order.objects.filter(session_id1).count() if order_count 0: Session.objects.get(id1).delete() else: raise ValidationError(该场次已有订单无法删除)另一个常见坑是delete()会返回一个(total, {app.model: count})的元组。如果你在视图里用return Response(result)前端拿到的字段会比较奇怪它是类似{cinema.Order: 2}的字典。我在早期写接口时常常忽略这个返回值导致前端解析数据时莫名报错。查询方面也顺手提一句filter()返回的是QuerySet它是惰性的只有真正取数据时才执行 SQL。所以不要频繁对同一个结果做len()操作也不要在一个for循环里反复查询数据库。能一次性select_related或者prefetch_related就把关联数据查出来能少写很多低效代码。3.4 订单状态与模拟支付逻辑真正影院系统会对接微信、支付宝支付但做项目练手时我们会用模拟支付。模拟支付的核心是保证业务状态流转合理而不是真的去扣钱。我在订单模型里定义了状态常量class Order(models.Model): STATUS_PENDING 0 STATUS_PAID 1 STATUS_CANCELLED 2 status models.IntegerField(defaultSTATUS_PENDING)用户创建订单时前端传过来session_id和seats列表后端先开启一个事务把对应场次中的目标座位标记为锁定状态再创建订单。这样做是为了防止两个用户同时选中同一排座位并创建同一张票。我在 Django 视图里用transaction.atomic()包裹整个逻辑。为什么要加事务假设用户 A 和用户 B 同时点击“提交订单”如果不加锁两边都会把同一个座位改成“已占用”最后产生两张重复订单。事务配合select_for_update()可以把目标座位行锁定直到事务提交后其他请求才能读到最新状态。这是选座系统里最重要的一段代码值得认真体会。支付接口就更简单了用户点击“确认支付”后端把订单状态从待支付改成已支付同时给前端返回一个模拟的支付成功回调。如果要增加真实感你还可以在后面加一个生成订单二维码的步骤但核心逻辑并不复杂。4. 前端Vue 页面搭建与核心交互4.1 路由设计与页面清单Vue 前端我按用户正常使用路径设计页面路由规划如下路径页面功能说明/loginLogin.vue登录 / 注册切换/Home.vue电影列表与搜索/movie/:idMovieDetail.vue电影详情、场次列表/session/:id/seatSeatSelection.vue电影选座/order/confirmOrderConfirm.vue订单确认、提交付款/ordersOrderList.vue我的订单列表/adminAdmin.vue后台管理电影/场次维护这里我用到了 Vue Router 的动态路由比如/movie/:id和/session/:id/seat。动态路由的好处是参数可以从route.params拿到一个页面可以复用给多部电影、多个场次不用每个电影写一套组件。如果你后面要做路由权限控制也可以直接用 Vue Router 的beforeEach钩子配合登录信息判断页面是否能访问。4.2 选座组件的实现选座是前端最核心的交互。影厅座位通常设计成一个矩形网格但前端不能硬编码出每一个座位正确做法是根据后端返回的座位列表动态渲染。我在SeatSelection.vue里这样设计template div classseat-map div v-forrow in rows :keyrow classseat-row span classrow-label{{ row }} 排/span div v-forseat in getRow(row) :keyseat.pk classseat :classseatClass(seat) clicktoggleSeat(seat) {{ seat.column }} /div /div /div /template script export default { data() { return { seats: [], selectedSeats: [], }; }, computed: { rows() { // 根据座位数据生成行号列表 return [...new Set(this.seats.map(item item.row))].sort(); }, }, methods: { getRow(row) { return this.seats.filter(item item.row row); }, seatClass(seat) { if (seat.status 1) return disabled; if (this.selectedSeats.includes(seat.pk)) return selected; return available; }, toggleSeat(seat) { if (seat.status 1) return; const index this.selectedSeats.indexOf(seat.pk); if (index -1) { this.selectedSeats.splice(index, 1); } else { this.selectedSeats.push(seat.pk); } }, }, }; /script同样一块逻辑如果不用 Vue就得手动 DOM 操作每次点击座位都要getElementById然后找到座位元素改class数据流极其别扭。Vue 的做法是让“数据”成为唯一事实来源页面上显示什么完全由selectedSeats数组决定交互代码少了很多。这也是我在这种强交互页面上推荐 Vue 的原因。座位状态这里做了一个拆分固定不可选的座位比如前排特殊座用status1表示“售出/锁定”前端点击直接忽略用户选中的座位高亮显示提交订单后后端再次校验座位避免前端跳过校验直接造假。4.3 axios 封装与前后端联调前端页面不会直接和后端对话我把所有请求统一封装在一个api.js文件里。这样封装的好处是自己可以在一个集中位置统一处理 baseURL、token、错误提示。核心代码如下// src/api/request.js import axios from axios; import router from ../router; const request axios.create({ baseURL: /api, timeout: 10000, }); 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) { router.push(/login); } return Promise.reject(error); } ); export default request;开发阶段Vue 服务跑在 8080后端 Django 跑在 8000直接跨端口调用会碰见 CORS 问题。我常用的解决办法是配置 Vue CLI 的代理把/api开头的请求全部转发到 8000// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, };这样前端请求地址里不用写死具体的 IP 和端口开发环境用代理转发部署时用 Nginx 反向代理切换环境只改代理配置代码里不需要动。相信我把环境差异留在环境配置层解决会省掉大量联调时间的麻烦。5. 常见问题与排查技巧5.1 跨域问题三种解法与现场经验前后端分离项目最经典的问题就是跨域。在后端接口没加允许跨域的配置之前前端浏览器控制台会报出一行 CORS 相关的红色错误。这个问题有几种解法我把它们按推荐顺序总结一下。第一种后端装django-cors-headers在settings.py里配置CORS_ALLOW_ALL_ORIGINS True。这是最快的方式适合开发阶段但上线时不好直接全部放开一般会改成CORS_ALLOWED_ORIGINS白名单。第二种前端开发服务器做代理就是我上面提到的vue.config.js配置。这种做法的原理是浏览器只和前端服务器通信然后由服务器去请求后端浏览器的同源策略就被绕开了。这种方法对生产环境也能沿用只要把 Nginx 的location /api转发到后端端口就好。第三种如果你部署时前后端不在同一个域名就必须在后端配置 CORS 白名单并在前端用环境变量区分接口地址。注意 Cookie 跨域时还要设置withCredentials: true否则登录状态依然无法保存。我在实际项目里遇到过一种特别让人抓狂的情况前端axios明明写了baseURL: /api但打开页面还是报错 404。后来检查发现我改了代理配置以后忘了重启npm run serveVue CLI 并没有热重载代理配置。所以提醒一下vue.config.js修改后开发服务器需要重启才能生效。5.2 日期时间、图片资源与几个前端细节电影票系统绕不开日期和时间。Django 默认会把DateTimeField输出成 UTC 时间字符串前端要显示成本地时区的“2025-06-20 19:30”就得做格式化。我的处理方式是后端把场次时间统一转成时间戳或者格式化字符串再返回不在前端折腾时区换算。如果你想让接口直接给可读格式可以在 Serializer 里这样写class SessionSerializer(serializers.ModelSerializer): start_time serializers.DateTimeField(format%Y-%m-%d %H:%M) class Meta: model Session fields __all__关于图片和文件显示的问题热搜词里还提到了“Vue image 能显示 pdf 吗”和“Vue 播放 m3u8 免安装”。这里一并说点经验图片用img :srcposter_url即可但要注意某些接口返回的链接是相对地址需要拼接好域名PDF 直接显示在页面里可以用iframe :srcpdfUrl浏览器自带 PDF 预览插件就能展示不需要引第三方组件m3u8 这类流媒体地址在 Vue 里更推荐装video.js或hls.js不需要安装额外桌面播放器网页内就能处理切片播放。你可能觉得这和电影票系统不太沾边但现实是电商类项目常常会遇到资源文件预览问题。掌握了这几个小技巧后续无论做课程平台、视频站点还是文档管理系统都能用上。5.3 Django/Flask 部署时容易忽略的坑项目做完以后把它跑在本地还不能算真正完成部署上线是另一道坎。Django 部署时最容易被忽视的是ALLOWED_HOSTS没有填写域名。本地调试时一般填localhost或127.0.0.1真正放到服务器上以后不把服务器公网 IP 或域名填进去外部访问一定会报Invalid HTTP_HOST错误。DEBUG False也是必须改的。如果忘了关数据库查询错误、路由异常都会把完整堆栈直接打印到页面上这在真实环境里既是安全隐患也非常毁观感。静态文件在 DEBUG 模式下是 Django 自己处理的关掉以后就得靠 Nginx 托管static目录。Flask 的部署思路类似但因为没有自带服务器生产环境下要用gunicorn或uwsgi来启动项目gunicorn -w 4 -b 0.0.0.0:8000 main:app注意main:app指的是main.py文件里的app实例-w 4表示开启四个 worker 进程这个参数可以按服务器 CPU 核心数和项目内存占用调整。如果开了四个 worker 后内存告急就该减少 worker 数量而不是盲目加进程。5.4 开发调试中的数据库、缓存和代码版本问题电影票系统在模拟并发选座的时候经常会发现数据库连接不够用或者操作卡顿。用 Django 默认的 SQLite 跑项目完全没问题但一旦到了多人并发测试阶段SQLite 的写锁会变得很敏感。建议尽早切换到 MySQL 或 PostgreSQL数据库连接池和事务隔离级别都更适合真实业务。缓存方面如果电影列表是热门接口每次都去数据库里查一遍压力其实不小。可以加一层 Redis 缓存把列表数据缓存几分钟。这个优化做起来不难但对接口响应时间提升有帮助。当然做项目的时候不需要一开始就上缓存先把核心链路跑通然后根据压力测试结果再加。另外我强烈建议从一开始就给项目配好 Git。每天只提交一两次或者每完成一个功能提交一次。开发过程中做过的查询、改过的接口万一后面写错了随时可以回溯到上一个稳定的版本。我自己就吃过不少没在项目早期建 Git 仓库的亏等到后期出问题想对比旧代码才发现根本没有留档。6 实操后的几点经验与后续扩展方向几个项目做下来我对这套技术栈有了比较清楚的感觉Django 适合快速产出完整业务模型Vue 适合做交互较多、状态变化明显的页面PyCharm 则是把这些串起来的高效开发容器。如果你是按这个方向去学我建议先别贪多把最基础的一条购票链路完整做通再逐步加搜索、筛选、优惠券、后台管理这些外延功能。从代码质量的角度讲有几个地方值得你花时间琢磨一是序列化层的校验逻辑别不校验直接入库二是选座事务要反复测试最好真开两个浏览器窗口模拟抢座三是订单号生成不要用自增 id而是用时间戳加随机数或者雪花算法防止未来别人看到你的订单号能推断出交易量。如果你后续想把这个项目加得更有竞争力可以考虑这几个方向给电影加评分和影评功能接一个真实的在线支付沙箱环境用 WebSocket 做热门电影的实时余票广播把管理后台做成独立的 Vue 单页让普通用户和运营人员彻底分开。我在实际测试中发现选座交易这部分做到稳定之后加任何外延功能都不会伤筋动骨因为模块之间的边界已经清楚了。最后分享一个小技巧前后端联调时经常因为接口返回的数据结构和前端预期的差一个字段导致页面白屏。我习惯在接新接口时先在浏览器控制台打印一遍返回结果确认字段名和类型再开始写页面绑定。这样看起来多花一步实际能省掉大量来回沟通的成本。希望你也能在动手写代码前先把字段和交互想清楚那样写出来的项目会顺畅很多。