这个项目标题列出来就很有年代感,Python + Vue + Django/Flask + PyCharm,几乎是国内高校毕业设计和课程设计里最常见的一套组合拳。不管你是被导师指定了这个题目,还是自己想在简历里补一个完整的全栈项目,这套技术栈到现在依然相当能打。我在实际带项目过程中接触过不少同类系统,也帮人排查过各种选座、锁座、支付状态不同步的坑,这篇就把在线电影票购买系统从选型到落地完整捋一遍,照着做基本能跑通。
先说清楚这个系统到底在解决什么问题。以用户视角看,它就是一个普通的卖票网站:浏览正在热映的电影、查看某场次的座位图、选座下单、模拟支付、在“我的订单”里看到购票记录。但真把一个这样的系统做出来,背后涉及的东西一点都不简单——前后端数据交互、数据库设计、并发场景下的座位锁定、订单状态机、以及前端路由和状态管理,每一块都是实打实的Web开发基本功。这篇文章主要面向两类人:一是要做毕设或课设的学生,二是想通过一个完整项目把Python Web和Vue串起来练习的开发者。我会把技术选型、核心实现、踩坑排查都讲清楚,内容以Django为主,Flask作为对比方案穿插说明。
1. 项目设计与技术选型:为什么是这套组合
1.1 Python后端 + Vue前端的分工逻辑
在线电影票购买系统天然适合前后端分离的架构,因为它的用户交互远比普通的内容展示网站复杂。想象一下选座这个操作——用户点一个座位,前端要立刻反馈颜色变化,同时把座位ID记下来;提交订单时后端要校验这个座位是否仍然可用,并且在数据库里把它锁住。这些交互如果靠传统的JQuery刷新页面来实现,体验会非常割裂,代码也会越写越乱。
Vue在这里承担的是“界面与交互”这一层。它的响应式数据绑定让座位图的渲染和维护变得非常自然:你只需要维护一个座位数组,用户点击时修改数组里对应座位对象的selected属性,视图就自动跟着变了,完全不需要手动操作DOM。Django或Flask承担的是“业务逻辑与数据”这一层,处理用户注册登录、电影和场次信息的增删改查、订单的创建与状态更新。
这两层之间通过HTTP接口沟通,前端用Axios发送异步请求,后端返回JSON数据。这种模式的直接好处是:前端和后端可以并行开发,只要提前约定好接口格式,两边的开发进度互不阻塞。我实际带项目时通常先定义好接口文档,然后让一个人负责Vue页面,另一个人负责Django的API,最后联调阶段再集中处理细节问题。
1.2 Django还是Flask?我的决策依据
这是每个做Python Web项目的人都会纠结的问题,而且热搜词里Django和Flask同时出现,说明很多人其实没想清楚。我的判断标准其实很简单:项目需要用到后台管理的复杂度和ORM的成熟度有多高。
Django的优势在于全家桶式的完整性。自带Admin后台、内置ORM、用户认证体系、CSRF防护,甚至连数据库迁移工具都给你配好了。我前面提到后台管理员需要维护电影信息、排片场次,用Django的Admin那真是开箱即用——注册几个Model就能获得一个可以增删改查的后台页面,这对毕设演示和验收来说极有说服力。而且Django的ORM在复杂查询和关联查询上做得非常成熟,比如查“某部电影当前可售的所有场次”,用select_related或prefetch_related就能避免N+1查询问题,这在以数据处理为核心的电影票系统里是刚需。
Flask则适合什么场景呢?如果你的项目除了一个简单的接口服务之外没有太多后台管理的诉求,或者说你更想自己掌控每个组件而不是接受全家桶的约束,那么Flask的轻量和灵活就是优点。但相应地,ORM要自己选(通常配SQLAlchemy)、后台要自己写或者装第三方库、用户认证也要自己搭。对在线电影票这种业务逻辑清晰、实体关系明确的系统来说,用Flask意味着很多东西要从零组装,反而拖慢进度。
在项目标题里同时出现两个框架,我理解是题目本身给了候选方案。我的建议是:如果你想要一个交付速度快、后期维护省心的项目,选Django;如果你想展示自己对框架底层更细粒度的掌控能力、或者对“轻量优先”有执念,可以选Flask。下文会以Django为主线展开,在对应的功能点上我会标注如果用Flask应该怎么替换。
1.3 前后端分离架构下的目录规划
前后端分离意味着你实际上在同一台机器上跑两个服务,这在刚开始接触时容易懵。我习惯用这样的目录结构:
movie-ticket-system/ ├── backend/ # Django项目根目录 │ ├── manage.py │ ├── requirements.txt │ └── ticket_backend/ # Django项目配置 │ ├── settings.py │ └── urls.py │ ├── apps/ │ │ ├── users/ # 用户模块 │ │ ├── movies/ # 电影与场次模块 │ │ └── orders/ # 订单模块 │ └── db.sqlite3 │ └── frontend/ # Vue项目根目录 ├── package.json ├── vite.config.js # 开发服务器配置,含代理 └── src/ ├── router/ # 路由 ├── views/ # 页面组件 ├── components/ # 公共组件(座位图、电影卡片) └── store/ # Pinia或Vuex状态管理开发阶段两个服务分别跑在各自的端口上,前端通常跑在5173(Vite默认),后端跑在8000。联调时通过Vite的代理配置把/api前缀的请求转发到8000端口,这样就能绕开跨域问题,具体配置我在后面实操部分会贴出来。
2. 核心功能拆解与数据模型设计
2.1 数据表设计与字段取舍
在线电影票系统的实体关系其实非常典型:用户、电影、场次、座位、订单。我在设计数据表时遵循一个原则:能用关联关系表达的,绝不冗余存储;凡是牵扯到钱的字段,优先用更精确的类型。
具体落到这张项目上,一般有六张核心表。
用户表直接用Django自带的User模型扩展,新增手机号和积分字段就行,没必要自己造轮子重写认证。电影表要存片名、海报、简介、时长、上映日期、预告片地址(这里就是那个m3u8链接的来历),场次表关联电影和影院放映厅,存放映时间、基础价格、放映厅名称。
座位这块是比较容易出现设计分歧的地方。我见过有人单独建一张大表存所有座位,每个座位一个记录,但这会导致数据量看起来很大且管理困难。更合理的做法是:座位是场次的属性,而不是电影的属性。我在场次表里用一个seat_map字段存储整个放映厅的座位布局JSON,而单独建一张Seat表来记录某个场次下每个座位的状态。
具体来说,Seat表设计成这样:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | 主键 | 座位记录ID |
| session | 外键 | 关联场次 |
| row | 整数 | 排号 |
| col | 整数 | 列号 |
| status | 整数/枚举 | 0可选,1已锁定,2已售出 |
| lock_expire_time | 时间 | 座位锁定的过期时间 |
这样的设计把座位状态和锁定时间绑在一起,为并发控制打底。订单表存用户、场次、座位、总价和状态,用OrderItem关联一个订单可以包含多个座位,这在购买两张连座票时尤其方便。
字段类型的坑我提一句,余额和价格这类涉及金额的字段,在Python/Django中建议使用DecimalField而非FloatField,否则会出现浮点误差,一分钱对不上在系统里是硬伤。
2.2 选座锁座的并发处理方案
这个环节是整个系统的技术核心,也是最容易在答辩时被问住的点。我反复跟人讲:在线选座的难点从来不在界面,而在并发——两个人同时点了同一个座,系统必须保证只有一个人能下单成功。
业界的方案通常是乐观锁或悲观锁二选一。Django里实现悲观锁很直接,用select_for_update()在事务内锁定行记录:
from django.db import transaction @transaction.atomic def lock_seats(session_id, seat_ids): with transaction.atomic(): seats = Seat.objects.select_for_update().filter( session_id=session_id, id__in=seat_ids, status=0 # 只锁可选座位 ) if len(seats) != len(seat_ids): return False, "部分座位已被锁定或售出" # 把选中的座位暂时锁定 from django.utils import timezone from datetime import timedelta expire_time = timezone.now() + timedelta(minutes=5) seats.update(status=1, lock_expire_time=expire_time, locked_by=user_id) return True, "锁定成功"这里有几个容易踩的坑。第一,select_for_update()必须和transaction.atomic()一起用,否则锁不会生效,Django会直接报错警告。第二,既然选了悲观锁,就不要在锁已经持有的情况下去做耗时操作(比如调用外部支付接口),这会严重影响后面的请求排队。第三,用户可能选好座位后一直不下单,需要给锁加一个过期时间,我上面设置了5分钟,到期后通过定时任务或者在用户下一次查询座位时清理掉过期锁定,把座位状态重置回可选。
如果项目并发量不大,乐观锁是更轻量的选择。实现方式是在Seat表上加一个version字段,更新时对比版本号:
updated = Seat.objects.filter( id=seat_id, status=0, version=current_version ).update(status=1, version=current_version + 1) # 影响行数为0,说明版本不对,座位已被别人抢走这个方案不依赖数据库行锁,性能更好,但对前端交互的要求更高——冲突发生时必须有个重试机制或者直接提示用户“座位被抢走了,请重新选座”。
2.3 票种价格与订单状态流转
电影票通常有不同价格档位,比如学生票、成人票、早场特惠、VIP厅加价。这些业务规则不一定要编码进死逻辑里,更优雅的做法是在场次表或电影表里预留一个票价策略字段,订单创建时根据当前时间和用户身份动态计算。
我在项目里给场次表加了一个price_adjust字段,格式是JSON,支持类似{"weekday_discount": 0.8, "vip_surcharge": 15}的结构。计算总价时,先取基础票价,再套用这些策略:
def calc_price(price, session, show_time, user): final_price = price if show_time.weekday() < 5: # 周一至周五 final_price = final_price * session.price_adjust.get("weekday_discount", 1) if session.hall_type == "vip": final_price += session.price_adjust.get("vip_surcharge", 0) return max(final_price, Decimal("0.01"))订单状态我建议用整数枚举存储,而不是用字符串裸存。定义好0待支付 | 1已支付 | 2已取消 | 3已退款 | 4已完成这样的状态机,所有状态流转统一放在一个地方处理,避免出现“订单莫名其妙从待支付变成已完成”这种事故。一个素人最容易犯的错误是直接在视图函数里给订单状态赋值,这样代码散落各处,排查问题时要命。
3. 实操过程:从环境到核心功能落地
3.1 PyCharm环境搭建与项目初始化
先说环境。PyCharm装哪个版本是个老问题,我建议直接用社区版就够用,专业版的主要好处是内置Vue插件和数据库工具,但社区版配上VSCode写前端也完全能跑。官网下载安装包时注意选对应操作系统的版本,一路默认安装即可,没有任何需要特别设置的选项。
Python环境这块,我的建议是统一用3.10或3.11版本,既能兼容新版Django,又不会遇到某些库在新版本编译失败的问题。装好之后务必在PyCharm的设置里给指定项目单独创建一个虚拟环境,不要直接装在全局Python里,因为不同项目依赖的库版本互相冲突时,你会体验到什么叫灾难现场。
用命令行创建虚拟环境然后进入项目,具体是这样的:
# 在项目根目录创建虚拟环境 python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate # 安装Django和Django REST Framework pip install django djangorestframework然后创建项目和应用。Django里项目(project)相当于整个站的配置壳子,应用(app)是具体的业务模块。一个电影票系统拆成users、movies、orders三个应用就很合理,命令行操作:
django-admin startproject ticket_backend cd ticket_backend python manage.py startapp users python manage.py startapp movies python manage.py startapp orders建好应用之后记得去settings.py的INSTALLED_APPS里注册,这一步是新手高频漏项,漏了之后数据库迁移会报很多奇怪的错误。
3.2 数据库迁移与后台配置
做好表定义之后执行迁移命令,这是在Django里把模型变成真实数据库表的标准流程:
python manage.py makemigrations python manage.py migrate执行完这两个命令,可以顺手把Admin后台配一下。在admin.py里注册电影和场次模型:
from django.contrib import admin from .models import Movie, Session @admin.register(Movie) class MovieAdmin(admin.ModelAdmin): list_display = ('title', 'duration', 'release_date') search_fields = ('title',) @admin.register(Session) class SessionAdmin(admin.ModelAdmin): list_display = ('movie', 'show_time', 'hall', 'price') list_filter = ('show_time',)注册完之后创建超级管理员账号:
python manage.py createsuperuser这一步的价值在于:你的系统刚搭完骨架就拥有了可用的内容管理后台。往后台里添加几部电影和场次,前端页面立刻就有数据可展示,前后端联调的时候不用再靠手工往数据库塞记录。
3.3 Django REST Framework接口实现
前后端分离的关键就是提供一套清晰的API。DRF(Django REST Framework)把这件事变得非常规范化,核心概念是Serializer序列化器和ViewSet视图集。
以电影列表接口为例,定义序列化器:
from rest_framework import serializers from .models import Movie class MovieSerializer(serializers.ModelSerializer): class Meta: model = Movie fields = ['id', 'title', 'poster', 'intro', 'duration', 'release_date', 'trailer_url']然后在视图集里写接口逻辑:
from rest_framework import viewsets, permissions from rest_framework.decorators import action from .models import Movie, Session from .serializers import MovieSerializer from .services import get_session_seat_map class MovieViewSet(viewsets.ReadOnlyModelViewSet): queryset = Movie.objects.all() serializer_class = MovieSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] @action(detail=True, methods=['get']) def sessions(self, request, pk=None): movie = self.get_object() session_list = Session.objects.filter(movie=movie, start_time__gte=timezone.now()) return Response([...])URL配置用的是DRF的DefaultRouter:
from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'movies', MovieViewSet) urlpatterns = [ path('api/', include(router.urls)), ]这里有个细节我需要提醒一下。接口权限建议默认设成IsAuthenticatedOrReadOnly,意思是未登录用户可以看列表、看详情,但提交订单、锁定座位这些写操作必须登录。具体到下单接口,在订单创建的ViewSet里把permission_classes设为[permissions.IsAuthenticated]即可。这样做既是业务逻辑的要求,也能让答辩时讲解权限设计更有底气。
3.4 Vue项目搭建与页面实现
Vue这边我推荐用Vite作为脚手架,创建项目很简单:
npm create vite@latest frontend -- --template vue cd frontend npm install npm install axios vue-router pinia入口配置稍微啰嗦一下。先配置开发服务器代理,在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', changeOrigin: true } } } })配好之后,前端请求/api/movies/就会自动转发到Django的8000端口,跨域问题的开发阶段解决方案就是它。
路由这块,电影票系统有几个页面完全可以打成动态路由。首页和电影列表用静态路由没什么问题,但电影详情、场次选座、订单中心这些页面,可以设置成带参数的路由:
const routes = [ { path: '/', name: 'home', component: () => import('../views/Home.vue') }, { path: '/movies/:id', name: 'movie-detail', component: () => import('../views/MovieDetail.vue'), props: true }, { path: '/session/:id/seat', name: 'seat-select', component: () => import('../views/SeatSelect.vue'), props: true }, { path: '/orders', name: 'orders', component: () => import('../views/Orders.vue'), meta: { requiresAuth: true } } ]Vue Router的动态路由其实就是用:id这种形式来传递参数,组件里通过route.params.id拿到电影ID,然后请求详情接口。这个思路在答辩时是加分的,说明你对路由机制有理解,而不是只会写死页面。
状态管理用Pinia,它比Vuex更简洁。我习惯把用户信息、购物车、当前选中的座位列表放在Store里,因为选座页面和订单确认页面都需要访问这些共享数据,通过组件Props一层层传太痛苦。
Pinia加购物车选座的草图:
import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ sessionId: null, selectedSeats: [] }), actions: { addSeat(seat) { if (!this.selectedSeats.find(s => s.id === seat.id)) { this.selectedSeats.push(seat) } }, removeSeat(seatId) { this.selectedSeats = this.selectedSeats.filter(s => s.id !== seatId) } } })Vue插槽在这个系统里的用武之地是电影卡片。你可能听说过slot,其实它就是一个占位符机制。我通常把电影卡片做成一个通用组件,海报、片名、评分是固定的,但右下角的按钮区域留给外部通过插槽注入——首页显示“购票”,收藏页显示“取消收藏”:
<MovieCard v-for="movie in movies" :movie="movie"> <template #action> <button @click="goBuy(movie.id)">购票</button> </template> </MovieCard>在MovieCard.vue内部,只需要在合适位置写一个<slot name="action"></slot>,外部的内容就自然地填充进来了。这是插槽最典型的应用,也是Vue组件化的精髓之一。
3.5 预告片播放与m3u8协议
很多电影网站会把预告片存成m3u8格式的流媒体文件,这种格式的播放方式和普通mp4不太一样。m3u8本质是一个文本索引文件,里面列了一串ts分片文件的地址,播放器一边下载索引一边切换播放分片,好处是支持边下边播、码率自适应。
Vue前端播放m3u8最省事的方案是直接用hls.js这个库。安装:
npm install hls.js在Vue组件里这样封装:
<template> <video ref="videoEl" controls muted playsinline></video> </template> <script setup> import Hls from 'hls.js' import { ref, onMounted } from 'vue' const props = defineProps({ src: { type: String, required: true } }) const videoEl = ref(null) onMounted(() => { const video = videoEl.value if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(props.src) hls.attachMedia(video) hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: hls.startLoad() break case Hls.ErrorTypes.MEDIA_ERROR: hls.recoverMediaError() break default: hls.destroy() } } }) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // 兼容Safari原生支持 video.src = props.src } }) </script>这里最值得注意的坑是:m3u8文件往往指向相对路径的分片,如果资源不在同一个域名下,播放器请求分片时会报404。解决思路有两个,要么在后端反向代理所有分片请求到同一个源,要么在数据库里就存完整的绝对URL。我推荐后者,存储上多占一点空间,播放时省心太多。
3.6 我的订单页面与Flask对比方案
订单列表页面实现思路比较直接:调接口拿到当前用户的所有订单,按时间倒序排列,每条订单展示电影名、场次时间、座位号、金额和状态。已支付但未开始的订单提供“退票”按钮,后端接到退票请求后把订单状态改为已退款,并把对应座位状态重置为可选。
如果你最终决定用Flask方案,那么这个系统的后端结构会是另一种风格。Flask+RESTX可以搭建出类似的API,ORM层通常配合SQLAlchemy,但后台管理、用户认证这些需要额外接入Flask-Admin和Flask-Login。从实际体验来看,用Flask实现同样的功能点,代码量大约多出三分之一,因为很多Django内置的能力都要找第三方库拼装。当然好处是你会对每个组件的原理更清楚,而且项目整体更轻量。如果只是想快速出一个能演示的完整项目,我仍然推荐优先考虑Django;Flask更合适你已经有一个很明确的轻量级API需求、不需要复杂后台的场景。
4. 常见问题与排查技巧实录
4.1 跨域请求被拦截
前后端分离项目里几乎人人都遇到过跨域报错。浏览器的安全策略默认拦截跨域请求,前端跑在5173端口,后端跑在8000端口,这两者之间就是跨域。
开发阶段最简单的解决方式是配置Vite代理,我把这段配置贴在前面了,changeOrigin: true那行很关键。但如果你不做代理而是直接用Axios去向http://127.0.0.1:8000/api/movies/发请求,就会看到浏览器控制台里报一个CORS错误。这时要么回退到代理方案,要么给Django装django-cors-headers库并配置CORS_ALLOW_ALL_ORIGINS = True。需要提醒的是,生产环境千万别把CORS_ALLOW_ALL_ORIGINS设成这样,正确的做法是指定前端域名白名单。
判断是否跨域问题有个简单的技巧:用Postman或浏览器地址栏直接访问接口地址,如果能正常返回JSON,但页面里请求失败,十有八九就是跨域或者代理配置的问题。
4.2 Django ORM查询和删除对象的细节坑
“django执行查询-删除对象”是热搜词,说明这个点确实困扰了不少人。ORM的删除操作有两个层级,一个是删除单条记录,一个是批量删除,两者行为差异很大。
删除单条记录用:
order = Order.objects.get(id=order_id) order.delete()但get()方法在你查询不存在的记录时会抛DoesNotExist异常,不加处理就直接500。更稳健的写法是用filter和first:
order = Order.objects.filter(id=order_id).first() if order: order.delete() else: return Response({"error": "订单不存在"}, status=404)批量删除则需要注意性能:
OrderItem.objects.filter(order_id=order_id).delete()批量删除不会触发Model里的delete()方法覆盖逻辑,也不会通过信号机制,所以如果要保证某些清理逻辑执行,比如删除订单后要同步更新座位状态,务必手动处理,或者在后面增加清理逻辑,比如:
with transaction.atomic(): order = Order.objects.select_for_update() order.status = 3 # 已退款 order.save() OrderItem.objects.filter(order_id=order.id).update(status=0) # 释放座位ORM查询时的懒加载机制也值得了解。Movie.objects.all()其实不会立刻查数据库,只有真正遍历或者调用时才执行查询,所以你可以放心地链式调用filter、exclude等条件而不必担心多次查库。配合select_related和prefetch_related可以大幅减少关联查询的数据库开销。
4.3 m3u8播放不了的头号原因
这段我前面讲了一部分原理,这里说排查思路。平时最常见的m3u8播放失败原因是跨域和路径问题,但还有一个隐蔽的坑:HLS的分片索引文件本身可能需要在HTTP头里返回正确的Content-Type,虽然大部分情况下浏览器能猜出来,但如果你的视频服务器(比如Nginx)返回的是application/octet-stream,播放器可能会拒绝解析。解决方式是手动调整响应头:
location ~ \.(m3u8)$ { add_header Content-Type application/vnd.apple.mpegurl; }另外,HLS.js对较老的Safari浏览器有特殊处理逻辑,如果你的运行环境恰好是macOS,需要检测原生canPlayType('application/vnd.apple.mpegurl')的情况并给video.src直接赋m3u8地址,这些我在上面的代码里都已经覆盖了。
4.4 Flask和FastAPI的选择迷茫
既然热词里同时出现了Flask和FastAPI的比较,我也替你把这一层讲透。实际在电影票这类以CRUD为主的业务系统里,FastAPI的优势其实不明显——它的异步性能确实好,但是你的数据库瓶颈往往不在这层异步上,而且Django和Flask的生态更成熟。FastAPI的优势更适合那种需要高并发、大量IO等待的场景,比如实时推送、AI模型调用服务。从学习成本角度看,如果你已经会Flask的基础,迁移到FastAPI很轻松,但没必要为一个毕设项目再做一次框架选型实验。
回到本项目中,如果你确定用Flask,那么安装之后核心的接口写法大概是:
from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///ticket.db' db = SQLAlchemy(app) CORS(app) @app.route('/api/movies/') def movie_list(): movies = Movie.query.all() return jsonify([...])注意这里没有像DRF那样现成的Serializer,序列化工作基本都要自己手写,所以看起来代码风格会更原始、更直接。
4.5 PyCharm使用中的几个高频雷区
最后说说开发环境本身。PyCharm社区版装好后,有两个高频雷区值得提前避开。第一个是解释器选择:打开项目后如果右下角提示No interpreter,需要到Settings里选择前面创建的venv解释器,否则所有依赖库都导不进去。第二个是插件问题:很多人喜欢装一堆插件,但插件冲突会导致项目卡顿甚至无法启动。我日常只用几个关键插件,比如.env文件支持、Docker支持、Vue官方插件,其他不需要的全部卸载。
关于PyCharm的正版授权,学生可以从官方申请免费的教育授权,如果你已经工作,社区版对付这种体量的项目完全够用,不要为了专业版去搞破解这类操作。专业版和社区版最大的差异在于专业版内置了Vue插件、数据库工具、远程开发支持,但对这个项目来说,社区版配合VSCode或者Vim写Vue前端也没有任何问题。
日常开发还有一个我觉得很实用的配置:在PyCharm的Run/Debug Configuration里把manage.py runserver的默认参数加上--noreload,这样改代码不会自动重启整个Django进程,跑测试和调接口时的反馈会更快更稳定。
结语与个人补充
我连续做了好几个类似的全栈项目之后最大的感受是:在线电影票购买系统并不是一个“写完就完事”的作业,它实际上是Web开发里那些通用难点(用户体系、数据建模、并发控制、前后端联调、流媒体协议)的一个浓缩容器。把这些点逐个攻破,后面你去做任何业务系统都会顺畅得多。
如果你按这篇文章的思路把项目做完,这里还有三个可以继续扩展的方向。一是把座位锁定时长改成可配置项,并在前端显示倒计时,这是商业系统很常见的交互细节。二是引入Redis做缓存,把电影的列表和详情缓存起来,减少数据库压力。三是把支付环节接入真实的第三方支付沙箱环境,比如支付宝沙箱,这样整个项目的完整度会再上一个台阶。
最后再提一个答辩或面试常被问到的点:一定要能说清楚你选的锁方案为什么适配你的系统。我在文章里讲了两类锁,Django的select_for_update是数据库层面的悲观锁,它准确但会阻塞;乐观锁通过版本号实现,高效但有冲突回退成本。你的系统规模、并发预期决定了哪种方案更适合,能把这个逻辑讲清楚,比背下来一堆概念更有说服力。