ARTICLE DETAIL

资讯详情

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

Django在线电影票务系统源码解析:锁座位与并发控制实战

Django在线电影票务系统源码解析:锁座位与并发控制实战 做在线电影票务系统之前我其实犹豫了很久。市面上现成的模板不少但能真正跑通“选座、锁定、下单、支付状态回写”这条完整闭环的不多尤其是并发场景下座位不超卖那一块很多号称完整的项目其实都是糊弄的。我这次拿到的一套django在线电影票购买系统源码内部编号84025属于典型的教学型工程结构清晰、注释完整既有前台用户购票场景也有后台排片管理场景。用Django做这类系统最舒服的一点是ORM和自带Admin能省掉大量重复的CRUD但真正考验功力的恰恰是订单状态流转和座位锁释放这些“看不见”的细节。这篇文章我会从头到尾把这套系统拆开讲从数据模型到并发控制再到你拿到源码后怎么本地跑起来、怎么排查常见的坑尽量做到让一个刚学完Django基础的新手也能直接复现甚至能在这个基础上做二次开发。1. 系统全景拆解在线票务系统到底在解决什么问题1.1 业务链路与角色分析在线电影票购买系统表面上看起来就是个“选电影、选场次、选座位、下单”的流程但背后涉及的其实是两条业务主线一条是用户的购票链路另一条是运营方的排片管理链路。从用户视角看整个链路是这样的用户进入系统后先浏览当前热映电影列表点进某部电影详情页查看该电影在不同影厅、不同时间段的排片场次选定一个场次后进入座位图选座提交订单后生成待支付订单支付成功后获得电子票凭证。这里每一步都牵扯到状态变化场次有“售票中、已售罄、已结束”等状态座位有“空闲、锁定、已售”三种状态订单有“待支付、已支付、已取消、已退款”等状态。从运营方角度看管理员需要维护影院信息、影厅座位布局需要给电影排片需要处理异常订单超时未支付释放座位、手动退款等还需要一些基础的数据统计——比如某部电影的票房占比、某场次的上座率。这套源码里对应的模块划分很清晰基本遵循了Django app的拆分惯例films负责电影信息与排片showtimes管理场次与影厅orders处理订单与支付状态users管登录注册另外还有一个后台dashboard模块做基础统计。这种按业务域拆app的做法对于中大型项目来说几乎是必须的——如果所有模型堆在一个models.py里后期维护起来会非常痛苦。1.2 为什么用Django而不是其他框架选择Django做这类业务系统核心原因有三个。第一是ORM带来的开发效率。选座系统中要频繁做多表联查比如“查某个场次的所有已售座位”在Django里一行Seat.objects.filter(showtimeshowtime, statussold)就能搞定不需要手写SQL拼接错误率低很多。第二是自带Admin后台。对于运营方管理系统这类内部工具Django Admin可以直接通过配置list_display、list_filter、search_fields生成一个能用且好看的后台省掉一整个前端工程。第三是Django的Session、Auth、CSRF等安全机制都是内置的做用户登录和表单提交时能少操很多心。当然Django也有它的短板比如默认的同步视图模型在写WebSocket实时推送时不够直接但这套系统用Django Channels做了补充后面会专门讲。总体而言Django适合“业务规则较重、CRUD密集、需要强约束”的系统票务系统恰好就是这个类型。注意这套源码在拆分app时做得很标准如果你拿到的版本里所有内容挤在一个app里建议自己动手拆一遍这个过程本身就是非常好的Django进阶训练。1.3 源码结构速览与功能清单我先把拿到手的源码目录结构做个梳理。解压后根目录通常是这样的movie_ticket_project/ ├── manage.py ├── requirements.txt ├── config/ # 主配置模块含settings、urls、wsgi、asgi ├── apps/ │ ├── films/ # 电影与排片 │ ├── showtimes/ # 场次与影厅座位 │ ├── orders/ # 订单与支付记录 │ ├── users/ # 用户认证 │ └── dashboard/ # 后台统计 ├── static/ ├── media/ # 电影海报上传目录 ├── templates/ └── db.sqlite3 # 开发用数据库通常自带一些测试数据功能清单方面前台用户端包含注册登录、电影列表与搜索、电影详情、场次选择、座位图可视化选座、订单创建与支付模拟、订单列表与详情、退票请求。后台管理端包含电影信息维护、影厅与座位布局配置、场次排映、订单管理、基础票房统计。这套功能覆盖度作为课程设计或者毕业设计是完全够用的如果要上生产环境还需要补支付网关对接、短信通知、分布式部署等但骨架和核心业务逻辑已经有了。2. 数据层设计ORM模型与表关系是整套系统的骨架2.1 核心模型设计思路看一套Django项目源码我建议第一件事不是看视图函数而是打开models.py。因为所有业务逻辑最终都是围绕数据模型转的模型设计得清爽后面的视图和表单都顺模型设计得拧巴后面全是补丁。这套系统里有几个核心模型我先讲清楚它们的关系Movie电影片名、简介、时长、上映日期、海报图、状态。Cinema影院与Hall影厅影院下有多个影厅影厅的关键属性是座位布局几行几列。Showtime场次某部电影在某影厅的某时间场次关联电影、影厅包含开始时间、票价。Seat座位属于某个影厅包含行列号但在票务系统里座位必须和场次绑定才有“当前状态”所以更合理的设计是建一张ShowtimeSeat关系表记录“某场次的某座位的状态”。这套源码里用了一张中间表来处理思路是对的。Order订单关联用户、场次、座位、状态、金额、创建时间。Payment支付记录关联订单记录支付方式、交易号、支付时间。我用代码形式做个示意不完全照搬源码体现结构逻辑class Seat(models.Model): hall models.ForeignKey(Hall, on_deletemodels.CASCADE, related_nameseats) row models.PositiveSmallIntegerField(verbose_name行) column models.PositiveSmallIntegerField(verbose_name列) class Meta: unique_together (hall, row, column) class ShowtimeSeat(models.Model): showtime models.ForeignKey(Showtime, on_deletemodels.CASCADE, related_nameseat_snapshots) seat models.ForeignKey(Seat, on_deletemodels.CASCADE) status models.CharField(max_length10, choices( (free, 空闲), (locked, 锁定), (sold, 已售) ), defaultfree) order models.ForeignKey(Order, nullTrue, blankTrue, on_deletemodels.SET_NULL)为什么座位状态不能直接放在Seat表里而要用ShowtimeSeat这种关联快照表因为一个物理座位在不同场次里状态是完全独立的。今天14:00场的3排5座卖了不代表明天20:00场的3排5座也被占了。如果把状态直接写在座位表上两个场次就互相污染了。这就是我在文章开头说的“觉得简单其实细节很多”的地方。2.2 ORM查询与删除对象的那些坑很多新手看源码时看到filter()和delete()觉得很简单实际用起来有不少隐性雷区。这里单独拎出来说。第一是查询结果集是惰性的。Seat.objects.filter(statusfree)执行后不会立刻请求数据库只有当你遍历它、取长度、或者转成列表时才会真正执行SQL。如果你在视图里先过滤然后往QuerySet里追加了对象再重新取一次结果可能和预期不一样。第二是批量删除的陷阱。用QuerySet.delete()时Django是直接执行批量SQL删除不会调用你重写过的Model.delete()方法。也就是说如果你在Order模型里重写了delete()用于释放座位或取消关联操作用orders.filter(useruser).delete()这种方式批量删时那些自定义业务逻辑是不会生效的。这是源码里很容易踩到的一个点。正确做法是遍历单个调用delete()或者先在Python层面做处理再批量删关联表。第三是级联删除的方向要心里有数。ForeignKey默认是on_deleteCASCADE父表记录删了子表自动跟着删。比如删掉一个Showtime所有关联的ShowtimeSeat都会自动删除这个有时候是你想要的有时候却会误伤数据。在订单表里我更建议用PROTECT或者SET_NULL防止手滑删了用户导致订单连带消失。2.3 事务与数据一致性票务系统中订单创建不是一个单一写操作它至少涉及三步检查座位状态、将座位标记为锁定、创建订单记录。这三步必须放在一个数据库事务里任何一个环节失败都要全部回滚否则就会出现“订单没生成但座位被锁了”或者“座位状态没改但订单已经产生了”这种脏数据。Django里使用事务有两种常用写法一种是装饰器一种是上下文管理器from django.db import transaction transaction.atomic def create_order(user, showtime_seat): with transaction.atomic(): seat ShowtimeSeat.objects.select_for_update().get(pkshowtime_seat.pk) if seat.status ! free: raise SeatNotAvailable(座位已被锁定或售出) seat.status locked seat.save() order Order.objects.create( useruser, showtimeseat.showtime, seatseat, statuspending, amountseat.showtime.price, ) return order这里select_for_update()是关键。它的作用是给选中的那行数据加上数据库行级锁直到事务结束才释放。并发场景下两个用户同时抢同一个座位时第二个请求会阻塞等待等第一个事务提交后再读这时候读到的状态就已经是locked了然后抛异常提示“座位被抢”。如果不加这把锁两个请求同时读到free然后同时写单库场景下大概率会超卖——这就是很多“能跑但不敢上线”的源码的硬伤。注意select_for_update()必须放在事务里面才生效单独在QuerySet上调它是不会锁行的。此外SQLite在高并发下的锁行为不像MySQL/PostgreSQL那么完整本地测试时问题不大生产部署建议切到PostgreSQL。3. 核心业务逻辑选座、锁座与实时推送的联动3.1 座位选座的交互流程用户进入选座页面时前端拿到当前场次下所有ShowtimeSeat的状态列表然后渲染成一张座位图。空闲座位可点击选中按惯例通常会选一个代表“暂选”的中间态提交订单时再真正锁定已售和已被别人锁定的座位则置灰不可点击。这套源码的前端选座用的是纯HTMLCSS渲染没有引入复杂的前端框架这对教学项目来说是合理的。如果你做二次开发可以考虑用Vue或React重写这块因为原生的DOM操作在座位数量较多时显得有些吃力。但在后端选座的下单接口设计才是真正的重点前端传showtime_id和seat_id列表后端接口负责校验、锁座、创建订单。三个关键校验一个都不能少座位是否存在且属于该场次座位当前状态是否为free当前用户是否已有一个未支付的同场次订单防止重复占座。这套源码里第3个校验不一定做得完善但作为二次开发点建议自己补上用户在订单待支付期间再次进同场次选座应该直接提示“您有未支付订单请先完成支付”。3.2 超时未支付的座位自动释放锁座之后用户迟迟不付款怎么办如果不释放一段时间后所有座位都会被僵尸订单占满系统就废了。生产系统里一般用延迟队列或者定时任务来做自动释放简单的方案是Celery beat每隔几分钟扫描一次超时订单把超过15分钟仍未支付的订单取消座位状态改回free。我这里比较推荐一个轻量方案不需要引入Celery那么重的组件在订单模型上增加一个expire_time字段每次创建订单时设置为now 15min。然后做两件事选座页面查询时顺便把所有statuspending且expire_time now()的订单批量标记为取消对应座位改回free支付接口里同样先做一次超时检查如果订单已超时直接拒绝支付并提示重新下单。这种“懒清理”的方式在中小规模场景下完全够用避免了额外维护一个定时任务组件的成本。源码里没有把超时机制做得这么细如果你要拿去部署或者答辩建议自己补上这一段这会是展示你项目深度的加分项。3.3 Django Channels实现座位状态实时推送这里必须重点说一下WebSocket因为这是这套系统里比较亮眼的扩展点也是很多人问“Python后端能不能做实时推送”的典型场景。场景很直观用户A和用户B同时打开同一个场次的选座页面A下单锁定了3排5座B的页面上3排5座必须在几秒内变成灰色不能等B刷新页面后才看到变化。这个需求用传统的HTTP轮询也能实现但效率和实时性都差一些。用WebSocket后端可以在座位状态变更时主动把消息推给所有正在浏览该场次的客户端。Django本身不支持原生WebSocket需要借助channels库。架构上要改动两层第一层配置ASGI。# config/asgi.py import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from apps.showtimes import routing os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack( URLRouter(routing.websocket_urlpatterns) ), })同时要在settings.py里增加ASGI_APPLICATION config.asgi.application CHANNEL_LAYERS { default: { BACKEND: channels.layers.InMemoryChannelLayer, }, }InMemoryChannelLayer只适合开发调试多进程部署时会失效生产环境需要换成channels_redis。这个是很容易被忽略的坑一定要记住。第二层写消费者Consumer并在订阅事件里推送座位变化。我写一个简化版的消费者示例对应“后端有数据前端自动收到推送”的需求import json from channels.generic.websocket import AsyncWebsocketConsumer class SeatConsumer(AsyncWebsocketConsumer): async def connect(self): self.showtime_id self.scope[url_route][kwargs][showtime_id] self.group_name fshowtime_{self.showtime_id} await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def seat_status_change(self, event): # 对外发送座位状态消息 await self.send(text_datajson.dumps({ seat_id: event[seat_id], status: event[status], }))在锁座业务里座位状态改变后调用下面这段就能把变更实时广播出去async_to_sync(channel_layer.group_send)( fshowtime_{showtime_id}, { type: seat_status_change, seat_id: seat_id, status: locked, } )前端JavaScript侧用原生的WebSocket对象建立连接并监听消息收到消息后把对应座位的DOM节点置灰。整套逻辑不算复杂但它把“选座页面所有用户保持同步”这个体验做出来了。这套能跑通的源码在这个点上给了我不少惊喜建议你拿到手后直接在这个基础上改一版联调试试。提示如果你把Channels接入到现有项目里别忘了runserver已经不够用了需要改用daphne或者uvicorn启动ASGI应用例如daphne -b 0.0.0.0 -p 8000 config.asgi:application。4. 从零到跑通拿到源码后的完整实操流程4.1 环境准备与依赖安装第一步先把运行环境准备好。我建议用Python 3.10以上版本因为较新的Django版本已经不再兼容Python 3.7以下的环境。打开终端按顺序执行# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate # 进入解压后的源码目录安装依赖 cd movie_ticket_project pip install -r requirements.txtrequirements.txt里通常包含Django、channels、channels-redis、Pillow处理图片等基础包。装完之后可以用pip list确认一下版本重点关注Django和Channels的版本兼容性。如果项目主页标注了Django版本最好严格按标注来Django大版本之间的模板语法差异不大但中间件配置和ORM细节有些改动混用版本容易出莫名其妙的问题。4.2 配置settings与本地数据库进入config/settings.py有几处必须手动改或至少确认的SECRET_KEY源码里默认给了一个开发用密钥本地跑没问题上线前一定要改用环境变量注入随机值不要硬编码。ALLOWED_HOSTS本地开发写成[*]就行但部署时建议写明域名或IP列表避免安全风险。DATABASES源码默认连SQLitedb.sqlite3就在项目根目录下本地跑最简单。如果你打算上生产建议切到PostgreSQL或MySQL把HOST、PORT、USER、PASSWORD配好。LANGUAGE_CODE和TIME_ZONE建议设置成zh-hans和Asia/Shanghai否则后台时间会显示成UTC面对排片时间时会非常别扭。MEDIA_URL和STATIC_URL确认配置文件里已经设置了静态文件目录和媒体文件目录否则海报上传和前端样式都会出问题。改完之后执行数据库迁移python manage.py makemigrations python manage.py migrate如果源码自带的db.sqlite3本身已经有初始数据这一步骤是可选的但为了理解数据表结构我建议还是自己跑一遍迁移再用python manage.py shell去逐表看看内容。4.3 创建超级管理员并初始化数据后台管理是这类系统的刚需所以必须创建一个超级管理员账号python manage.py createsuperuser按提示输入用户名、邮箱、密码。启动开发服务器python manage.py runserver 0.0.0.0:8000然后浏览器访问http://127.0.0.1:8000/admin用刚创建的管理员账号登录就能看到Django Admin里所有模型的管理入口。去看一下ShowtimeSeat模型你会看到每个座位行数据都带状态字段——这就是前面说的座位状态是跟随场次的快照不是全局共享的。如果源码附带初始数据比如电影、影厅、场次这些演示数据一般会以fixtures目录下的JSON或YAML文件形式提供可以通过这样导入python manage.py loaddata fixtures/initial_data.json导入后去前台页面逛一圈确认电影列表、详情页、选座页都能正常打开数据没有报错。4.4 源码二次开发的关键入口把项目跑通只是起步。如果你要基于这套源码做课程设计或毕设建议按以下顺序自己动手过一遍代码首先是config/urls.py看主路由如何分发到各个app。Django里app自己定义urls.py主路由用include()引用这套源码在这个层面做得比较规整。跟着路由找到视图函数把“请求怎么进来、数据怎么处理、模板怎么渲染”串起来是理解整个项目的捷径。其次是models.py里的ForeignKey和related_name。追踪一个场次数据是怎么被加载出来的先查Showtime再查ShowtimeSeat再关联Seat拿到行列号。理解了这个链路你在前端看到的座位图后端逻辑就差不过已经完全清楚了。然后是forms或序列化部分。很多教学项目在表单层会直接把前端传参塞进request.POST硬解析虽然能用但不优雅。如果你后续想扩展成API接口给小程序或者App用建议用serializersDRF重写一层。我在跑通这套源码之后整体感受是它的核心逻辑确实是“能跑且逻辑通”对比很多只堆页面没有业务闭环的“竞争对手”这套在业务完整性上明显高一截。但对新手来说别急着改代码先把Admin后台里的数据加一遍、改一遍再下单走一遍完整流程等对数据流转有体感了再动手改任何东西都不迟。5. 常见问题与排查技巧实录5.1 页面样式丢失或图片不显示本地runserver开发模式下Django会自动处理静态文件。但如果你把DEBUG改成False或者在部署环境里用Nginx托管静态文件时经常会出现CSS、JS全部404的情况。解决办法是在settings里设置:STATIC_ROOT os.path.join(BASE_DIR, staticfiles)执行collectstatic收集所有静态文件到指定目录再由Nginx或反向代理托管。媒体文件海报等也要配置MEDIA_ROOT和MEDIA_URL如果你在Admin上传了海报但页面不显示八成是这两个配置没配对。5.2 迁移时报错“django.db.migrations.exceptions.InconsistentMigrationHistory”这个报错很经典一般是因为数据库里已经有表了但迁移记录不完整或被人为删过迁移文件导致的。新手最容易犯的错是数据库中有大量数据然后动了模型字段直接删掉旧迁移文件想“重建”结果Django发现数据库结构和迁移历史对不上。排查思路是先看迁移记录表用SQLite的可视化工具或直接命令行查看django_migrations表。如果差异不大可以手动补上对应的迁移文件或者干脆备份数据后用python manage.py migrate appname zero重置该app的迁移会清空表结构谨慎使用。5.3 同一场次座位出现超卖出现这种问题的原因基本就一个下单逻辑里没有用select_for_update()锁行多个并发请求同时读到座位free状态同时执行写入。要复现这个问题可以开多个浏览器窗口同时下单同一座位或者在测试里用并发脚本模拟。解决了方法在前面已经写了事务加锁是关键。另外还可以再加一层乐观锁在ShowtimeSeat表上加一个version整数字段每次更新时比对版本号版本号不一致则更新失败。两种方式可以结合用悲观锁管住数据库层面乐观锁做最后兜底。5.4 时间显示比本地时间慢8小时如果你把TIME_ZONE设置成了UTC而业务上显示的排片时间来自数据库时间字段Django默认开启USE_TZTrue时会存带时区的时间前端渲染的时候需要转换时区很多模板过滤器不会自动转于是页面显示的就是UTC时间比中国时间慢8个小时。建议直接把TIME_ZONE改成Asia/Shanghai同时确认数据库连接配置里也指定了时区。如果你对时间处理没有特殊需求最简单的方式是保持USE_TZTrue在模板渲染时用自定义模板过滤器转换到本地时区如果你完全不想处理时区可以设置USE_TZFalse但这样会失去一些高级时间功能生产环境不建议这样干。5.5 Websocket连接一直失败先确认是否使用daphne或uvicorn启动ASGI应用而不是用默认的runserver——单纯runserver不会加载Channels的WebSocket路由。再检查ALLOWED_HOSTS如果包含不完整会导致WebSocket握手失败。还有一个容易被忽略的是代理层如果你在Nginx后面需要额外配置Upgrade头包括Connection: Upgrade。本地测试时如果localhost连不上可以试试127.0.0.1有时是浏览器对WebSocket跨域的校验策略问题。注意CHANNEL_LAYERS如果用默认的InMemoryChannelLayer重启服务或开多个worker进程后group和channel信息会丢失跨进程推送会失败。生产至少要换成channels_redis并配置Redis地址。5.6 常用问题速查表症状可能原因处理办法后台登录后无样式静态文件未收集执行collectstatic并让反向代理指向STATIC_ROOT上传海报不显示MEDIA_ROOT未配置配置媒体目录和URLNginx需要增加media路由migrate有新改动但不生效迁移文件缺失或冲突检查django_migrations表必要时重置该app迁移同一座位被买两次未使用行级锁事务内select_for_update()锁行时间显示偏8小时TIME_ZONE为UTC改为Asia/Shanghai并确认数据库时区WebSocket握手失败ASGI启动方式错误使用daphne或uvicorn启动订单状态一直pending支付回调未实现检查支付模拟接口是否更新订单状态登录后跳转回到登录页Session或token校验失败检查login_required逻辑及cookie配置5.7 几个值得注意的调试技巧我自己调试这套源码时的体会是不要在视图函数里到处print用Django自带日志更舒服。在settings里配日志输出到控制台和文件ORM执行的SQL也能通过设置LOGGING里的django.db.backends显示出来。打开SQL日志之后你会发现很多性能瓶颈很明显比如N1查询——循环遍历每个场次时单独查票价或座位数这种问题一眼就能看出来。另外建议用Django Debug Toolbar它对新手排查页面渲染慢、SQL查询次数多、模板渲染出错都极有帮助。装上之后页面侧边栏直接显示各种性能指标比盲猜快太多了。6. 二次开发方向与个人实操体会前面讲了这么多这套django在线电影票购买系统的核心已经拆得很透了。但如果你拿到的源码只是跑通用那价值还没有完全发挥出来。我把自己在实操过程中的几个改进方向列出来你可以按自己的时间和需要选着做。第一个建议是接入真实支付。源码里的“支付”大概率是模拟支付点一下就变成已支付状态。你可以对接一个支付沙箱环境比如支付宝沙箱或者微信支付沙箱把Payment模型补充上交易流水号、支付回调URL校验、异步通知处理。这个改动比较大但做明白之后你对Web开发的“金钱链路”会有一个完整的认知。第二个建议是增加场次的座位分区和票价策略。真实影院里同一个影厅、同一场次不同区域的价格可能是不同的比如IMAX厅中间区域比两侧贵。这个在数据模型上需要把Seat增加zone字段然后ShowtimeZonePrice表记录每个场次每个区域的票价。改完这个排片功能将会更贴近生产。第三个建议是给选座页面做可视化座位图优化。源码用的是简单HTML表格模拟座位图你可以用Canvas或者SVG做一张更直观的座位布局图支持拖拽缩放双击选座已选座位高亮结算栏动态刷新总价。这些交互不复杂但视觉效果提升非常明显尤其适合做项目展示环节。第四个建议是加上一份简单的数据看板。利用Dashboardapp展示每日票房、热门影片Top10、场次上座率曲线、订单转化率等。数据模型里本身有这些字段写几个聚合查询和图表渲染就能凑一个不错的运营后台。我个人在跑通这套源码后最大的体会是Django项目学习的核心不在某一个知识点而在于把模型关系、事务、认证、模板、部署这些零碎的东西串成一个整体。很多人学Django时就是把每个章节的例子都过了一遍真到做项目时发现无从下手——缺的恰恰是“从一个需求出发把完整体验做出来”的训练。这个电影票系统就是一个很好的挂载点你可以往里面加需求也可以在里面修bug甚至用不同框架重写一遍前端每一次改动都能强化你对整套链路的心智模型。如果你也是拿到源码就跑一遍没有深究的建议跟着我前面几节的方式打开models.py把每张表画一遍关系图打开views.py把每个视图的请求路径走一遍再顺手试几个并发请求你会发现以前不清楚的地方会一点点清晰起来。这才是这套源码对个人最有价值的用法。
返回列表