ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python全栈实战:基于Flask+Vue3的电影订票系统设计与实现

Python全栈实战:基于Flask+Vue3的电影订票系统设计与实现 电影订票购票系统在每年的毕业设计选题里几乎就是常青树级别的存在。我前前后后帮人搭过好几套也带着学弟学妹完整做过这个题目听起来不复杂但真正把选座、下单、座位冲突、支付状态这些环节全部跑通逻辑自洽、没有明显漏洞的版本其实并不算多。这篇文章我就以 Python Flask Vue3 这套技术栈为例把电影订票购票系统的设计与实现完整拆一遍从需求梳理、工程搭建、数据库设计到核心业务代码和前后端联调都覆盖到尽量把每一步背后的原因也讲清楚。如果你正在做课设、毕设或者想通过一个全栈项目入门 Vue3 和 Flask 开发这篇内容应该能帮你少走不少弯路。1. 项目整体设计与思路拆解1.1 为什么选 Flask Vue3而不是别的组合先说后端。Flask 在 Python 的 Web 框架里属于轻量派它不像 Django 那样默认帮你配好 Admin 后台、ORM、表单、认证这一整套而是给你一个核心骨架路由、中间件、ORM 都按需扩展。很多人觉得 Flask 太自由了反而不知道从哪入手但对电影订票这种规模适中、边界清晰的系统来说这个自由度刚刚好你需要 SQLAlchemy 就用 Flask-SQLAlchemy需要登录令牌就加 Flask-JWT-Extended每个模块都可以按自己的习惯组织不会像刚接触 Spring Boot 那样被一堆注解和容器概念绕晕。前端选 Vue3 也是同样的逻辑。现在前端三大框架里Vue3 的中文资料多、上手曲线平缓组合式 APIComposition API写业务逻辑比 Vue2 的选项式 API 更清晰配合 Vite 的冷启动速度开发体验相当好。而且 Vue3 生态现在已经很成熟了Vue Router 做路由、Pinia 做状态管理、Element Plus 做后台界面正好覆盖这个项目的需求。相比之下React 的 JSX 和状态管理方案对新手不算友好Angular 就更不用说了学成本高出一大截。有一个点可以提前说明前后端分离是这个项目最合理的架构选择。Flask 只负责提供 RESTful API 和业务逻辑Vue3 负责页面渲染和交互两者通过 JSON 通信。这样做的好处是前端开发时可以完全用 Vite 的 mock 数据或者代理转发来调试后端可以单独用 Postman 测接口两个人协作时互不阻塞。就算是一个人单干分离之后代码可维护性也明显更好——改页面不用碰后端加接口不用翻前端。1.2 功能模块怎么划分电影订票系统的功能可以分成两条业务线用户侧和管理侧。用户侧的核心流程是登录 → 浏览电影 → 查看场次 → 选座 → 创建订单 → 模拟支付 → 查看订单。这里每一步都可以展开注册登录手机号或用户名注册密码哈希存储登录后发 JWT 令牌电影展示海报封面的列表页支持按电影名称搜索、按类型和上映状态筛选电影详情电影介绍、演员、时长、上映日期、评分页面上直接列出当日或全部场次选座页面根据影厅的行列数渲染座位图已售座位灰色不可选用户点击座位选中或取消右侧实时显示选中数量和总价订单管理未支付、已支付、已取消三种状态的订单列表可以对未支付订单执行支付或取消管理侧根据答辩需求可大可小我建议至少做成电影信息管理新增、修改、下架、场次管理维护上映时间和影厅、订单查看。如果项目要求里没有强制后台功能那后台可以精简毕竟核心评分点在业务逻辑上。这里有个容易被忽略的点选座是整个系统的核心难点因为涉及并发冲突。两个用户同时看中同一个座位如何保证只有一个人能下单成功这个问题答不清楚答辩时基本会被问倒。后面我会专门用一节来讲怎么用数据库约束来兜底。1.3 前后端工程目录规划建议前端和后端分成两个独立目录比如movie-ticket-frontend和movie-ticket-backend不要混在一起。后端按模块组织不是把所有代码堆在一个app.py里那样项目一复杂就完全没法维护。我常用的后端结构movie-ticket-backend/ ├── app.py # 应用入口 ├── config.py # 配置数据库地址、JWT密钥、过期时间 ├── requirements.txt ├── models/ # SQLAlchemy 模型 │ ├── __init__.py │ ├── user.py │ ├── movie.py │ ├── session.py │ ├── order.py │ └── seat.py ├── api/ # 蓝图路由 │ ├── __init__.py │ ├── auth.py │ ├── movie.py │ ├── order.py │ └── admin.py └── utils/ ├── db.py # SQLAlchemy 实例 └── response.py # 统一返回格式前端用 Vite 的默认模板生成再补充src/api、src/router、src/store、src/views这些目录。这种结构在老师眼里是工程化的体现比一个 Python 文件写完所有接口要加分不少。2. 环境准备与工程初始化2.1 Python 环境搭建的几个关键点Python 安装这个环节我不展开太多但有几个坑必须提醒Windows 安装时一定要勾选Add Python to PATH不然终端里打python会提示找不到命令装了多个 Python 版本时建议在命令行用python -m venv venv而不是直接依赖全局环境因为换一台电脑或者换项目时依赖冲突会让人崩溃。虚拟环境创建好之后Windows 下激活命令是venv\Scripts\activateMac 和 Linux 是source venv/bin/activate。激活后命令行前面会出现(venv)标识这就对了。另外提一句 PyCharm 的事情社区版在新建项目时确实没有 Flask 的模板选项但这不是什么问题手动创建venv然后装 Flask 一样跑最多就是少了图形化的启动配置纯命令行就能解决。后端依赖推荐这几个版本号可以根据当前 PyPI 上最新的微调flask3.0.0 flask-sqlalchemy3.1.2 flask-jwt-extended4.6.0 flask-cors4.0.1 pymysql1.1.0 cryptography42.0.5其中cryptography是 PyMySQL 连接 MySQL 时做认证用的很多教程里没提导致连接报错这里提前写进去。2.2 Flask 最小工程初始化装完依赖后先写一个最小的 Flask 应用验证环境。在app.py里from flask import Flask from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost:3306/movie_ticket?charsetutf8mb4 app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db SQLAlchemy(app) app.route(/) def index(): return {message: 后端服务运行中}终端执行python app.py浏览器访问http://127.0.0.1:5000能看到 JSON 返回就说明环境 OK。注意数据库先手动创建好用命令CREATE DATABASE movie_ticket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里charsetutf8mb4不是可选项。MySQL 默认的utf8在 MySQL 5.7 及以前其实是utf8mb3不支持四字节的 emoji 和生僻字订单号里如果带了特殊字符存进去会报错所以连接串和数据库都统一用utf8mb4。2.3 Vue3 前端工程搭建前端用 Vite 生成项目是最快的npm create vitelatest movie-ticket-frontend -- --template vue cd movie-ticket-frontend npm install npm install vue-router4 pinia axios element-plusVite 模板默认生成的是 Vue3 组合式 API 的写法自带src/main.js、src/App.vue这些基础文件。装完 Element Plus 后在main.js里全局引入即可import { createApp } from vue import { createPinia } from pinia 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(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)这里有个容易卡住的点Vue3 项目默认用的是createWebHistory还是createWebHashHistory如果你的前端是单独用 Vite dev server 跑并且打算直接用BrowserRouter风格的历史模式刷新某个子路由时会出现 404因为 Vite 的 dev server 默认没有做 history 回退。最简单的解决方案是用createWebHashHistory虽然 URL 里带个#但在课设场景下完全够用而且部署到任何静态服务器都不会出问题。如果确实要用历史模式就要在 Vite 配置里加server.historyApiFallback或者部署时用 Nginx 做 try_files 回退这属于部署细节后面再讲。前端配置里还有一个必做项开发环境的代理转发。在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://localhost:5000, changeOrigin: true } } } })这样前端请求/api/xxx会被自动转发到 Flask 的5000端口前端代码里不需要写http://localhost:5000这个完整地址也绕开了跨域问题。这是开发环境最推荐的联调方式。3. 数据库设计与核心接口实现3.1 数据表设计从用户到订单的完整链路先规划核心字段下面是最小可用的设计表名核心字段说明userid, username, password_hash, phone, create_time用户表username 唯一movieid, title, cover_url, director, actors, genre, duration, release_date, rating, status电影信息status 控制上下架hallid, name, rows, cols影厅rows/cols 决定座位布局sessionid, movie_id, hall_id, start_time, end_time, price, status场次对应某部电影在某影厅的放映ordersid, order_no, user_id, session_id, total_price, status, expire_time订单表status: 0 待支付 / 1 已支付 / 2 已取消order_seatid, order_id, session_id, row_num, col_num订单座位明细决定哪些座位被占用order_seat表是整个选座系统的核心。它记录了一张订单买了哪些座位那么某个场次的哪些座位已被占用这个查询就变成了查order_seat里存在哪些(session_id, row_num, col_num)组合同时这条记录对应的orders.status不是已取消。这是最简单也最可靠的做法。订单表里expire_time是给待支付订单设定一个超时时间比如创建订单后 15 分钟未支付这个订单自动失效座位需要释放。这里不要简单地把expire_time字段删掉然后每次都去算创建时间加 15 分钟因为那样每次查询都要做时间运算可读性差而且如果业务上要调整超时时长改数据又会很麻烦。直接把过期时间作为字段存下来一次性算好是更工程化的做法。3.2 SQLAlchemy 模型定义与初始化在models/__init__.py里统一导入所有模型避免循环导入。以订单和座位为例from datetime import datetime from utils.db import db class Order(db.Model): __tablename__ orders id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse, comment订单号) user_id db.Column(db.Integer, nullableFalse, indexTrue) session_id db.Column(db.Integer, nullableFalse, indexTrue) total_price db.Column(db.Numeric(8, 2), nullableFalse) status db.Column(db.SmallInteger, default0, comment0待支付,1已支付,2已取消) expire_time db.Column(db.DateTime, nullableFalse) create_time db.Column(db.DateTime, defaultdatetime.now) pay_time db.Column(db.DateTime) class OrderSeat(db.Model): __tablename__ order_seat __table_args__ ( db.UniqueConstraint(session_id, row_num, col_num, nameuk_session_seat), ) id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) order_id db.Column(db.Integer, nullableFalse) session_id db.Column(db.Integer, nullableFalse, indexTrue) row_num db.Column(db.Integer, nullableFalse) col_num db.Column(db.Integer, nullableFalse)注意UniqueConstraint(session_id, row_num, col_num)这一行它就是防超卖的第一道防线。如果两个事务同时插入同一个座位数据库唯一约束会直接拒绝其中一个应用层捕获到这个异常就能返回座位已被选中。模型定义好后在app.py里用db.create_all()可以自动建表但要注意create_all()不会修改已有表结构如果你中途改了字段需要手动迁移。课设阶段建议直接删表重建反正有初始数据脚本不要引入 Flask-Migrate那玩意对于这种规模的项目是负担。3.3 API 接口清单与统一返回格式接口设计直接决定前后端联调的顺畅程度。我常用的返回格式是{ code: 0, message: success, data: {} }code为 0 表示成功非 0 表示业务失败前端统一在 Axios 拦截器里判断。主要接口清单如下方法路径功能说明POST/api/auth/register注册body 带 username, password, phonePOST/api/auth/login登录返回 JWT tokenGET/api/movies电影列表支持 keyword 搜索、分页GET/api/movies/电影详情附带该电影场次列表GET/api/sessions/ /seats场次已售座位返回已占用座位的坐标数组POST/api/orders创建订单body 带 session_id, seatsGET/api/orders我的订单按用户返回订单列表POST/api/orders/ /pay模拟支付将待支付订单改为已支付DELETE/api/orders/取消订单释放座位管理员接口按需再加比如 POST /api/admin/movies 新增电影、PUT /api/admin/sessions 修改场次。建议增删改查必须配套不然电影数据只能靠数据库手动插后台管理就成了摆设。4. 核心业务逻辑实现4.1 用户认证JWT 登录态管理用户注册时密码绝不能明文存库必须哈希。用werkzeug.security提供的generate_password_hash生成哈希值校验时用check_password_hash。Flask 里没有内置登录会话机制这里直接选flask-jwt-extended因为它的 API 简单默认就是 Bearer Token 模式。注册和登录可以放在同一个api/auth.py蓝图里from flask import Blueprint, request, jsonify from werkzeug.security import generate_password_hash, check_password_hash from flask_jwt_extended import create_access_token from models.user import User from utils.db import db bp Blueprint(auth, __name__, url_prefix/api/auth) bp.post(/register) def register(): data request.get_json() username data.get(username, ).strip() password data.get(password, ) if not username or not password: return jsonify(code1, message用户名和密码不能为空) if len(username) 3 or len(password) 6: return jsonify(code1, message用户名至少3位密码至少6位) if User.query.filter_by(usernameusername).first(): return jsonify(code1, message用户名已存在) user User(usernameusername) user.password password db.session.add(user) db.session.commit() return jsonify(code0, message注册成功) bp.post(/login) def login(): data request.get_json() username data.get(username, ).strip() password data.get(password, ) user User.query.filter_by(usernameusername).first() if not user or not user.check_password(password): return jsonify(code1, message用户名或密码错误) token create_access_token(identitystr(user.id)) return jsonify(code0, message登录成功, data{ token: token, user: {id: user.id, username: user.username} })JWT 的identity存的是用户 ID 字符串。后面在需要登录才能访问的接口上用jwt_required()装饰再通过get_jwt_identity()拿到当前用户 ID。这是一个非常标准的流程前后端联调时只需要在前端把 token 放到请求头的Authorization: Bearer token里。4.2 选座与订单创建防超卖的并发处理这是全项目技术含量最高的一部分。先说清楚需求用户在选座页选了一批座位提交时系统要检查这些座位是否全部可售如果可以创建订单并锁定座位如果其中有座位已经被别人先下单则整个订单创建失败前端提示用户重新选座。这里最容易犯的错误是先查再插先查数据库看座位有没有被占没有就插入。但高并发下两个请求可能同时查出来座位空闲然后都去插入结果就超卖了。要避免这种情况最稳妥的做法是让数据库来保证唯一性而不是靠应用层判断。order_seat表上的唯一约束(session_id, row_num, col_num)就是用来解决这个问题的。下单时直接把所有座位插入order_seat如果某个座位已经被占用数据库会抛出IntegrityError在异常处理里回滚事务并提示用户即可。不需要加锁也不需要 Redis数据库的约束就是最可靠的兜底。核心下单接口代码bp.post(/orders) jwt_required() def create_order(): data request.get_json() user_id int(get_jwt_identity()) session_id data.get(session_id) seat_list data.get(seats) if not session_id or not seat_list: return jsonify(code1, message参数不完整) # 校验场次是否存在且在售 session Session.query.get(session_id) if not session or session.status ! 1: return jsonify(code1, message场次不存在或已下架) # 校验座位是否超出影厅范围 hall session.hall for seat in seat_list: if seat[row] 1 or seat[row] hall.rows: return jsonify(code1, message座位超出影厅范围) if seat[col] 1 or seat[col] hall.cols: return jsonify(code1, message座位超出影厅范围) # 生成订单号避免并发重复 order_no datetime.now().strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999)) total_price session.price * len(seat_list) try: order Order( order_noorder_no, user_iduser_id, session_idsession_id, total_pricetotal_price, status0, expire_timedatetime.now() timedelta(minutes15) ) db.session.add(order) db.session.flush() for seat in seat_list: order_seat OrderSeat( order_idorder.id, session_idsession_id, row_numseat[row], col_numseat[col] ) db.session.add(order_seat) db.session.commit() except IntegrityError: db.session.rollback() return jsonify(code1, message部分座位已被其他人选中请重新选座) return jsonify(code0, message订单创建成功, data{ order_id: order.id, order_no: order.order_no, expire_time: order.expire_time })这里我用了db.session.flush()而不是直接commit()目的是先拿到数据库自增的order.id再插入座位明细。如果直接commit后再插座位事务已经被提交座位插入失败时订单已经入库就会出现订单存在但没有座位明细的脏数据。如果一个订单里选了多个座位这里的所有座位插入是在同一个事务里的任何一个座位冲突整个事务回滚订单就不会产生。这一点一定要在答辩时讲清楚它直接体现了你对事务隔离级别和一致性的理解。4.3 模拟支付与超时释放支付功能在课设里不需要接真实支付接口做一个模拟支付即可。把订单状态从 0 改成 1写入pay_time字段bp.post(/orders/int:order_id/pay) jwt_required() def pay_order(order_id): user_id int(get_jwt_identity()) order Order.query.filter_by(idorder_id, user_iduser_id).first() if not order: return jsonify(code1, message订单不存在) if order.status ! 0: return jsonify(code1, message当前状态不可支付) if order.expire_time datetime.now(): order.status 2 # 标记为已取消 db.session.commit() return jsonify(code1, message订单已超时请重新下单) order.status 1 order.pay_time datetime.now() db.session.commit() return jsonify(code0, message支付成功)超时释放座位的处理我建议用懒释放策略而不是写一个定时任务。懒释放的意思是每次创建订单或查询座位时先把当前用户属于自己的、已经超时未支付的订单状态置为取消然后这个订单下的座位自然就释放了。用一个简单的更新语句Order.query.filter( Order.status 0, Order.expire_time datetime.now() ).update({status: 2}) db.session.commit()这样不需要引入 Celery、APScheduler 这类定时任务框架额外依赖少逻辑也直白。如果你一定要用定时任务也可以加一个 APScheduler 每小时跑一次清理超时订单但对课设来说懒释放已经够了。5. Vue3 前端实现与前后端联调5.1 前端路由规划与导航守卫前端路由建议这样设计路径组件说明/Home.vue电影列表页/movie/:idMovieDetail.vue电影详情与场次/seat/:sessionIdSeatSelect.vue选座页/ordersOrderList.vue我的订单/loginLogin.vue登录/registerRegister.vue注册/adminAdmin.vue后台管理可选导航守卫用来做登录校验。需要登录才能访问的页面在路由 meta 里标记requiresAuth: true然后在router.beforeEach里检查本地有没有 tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })登录成功后再跳回redirect参数指向的页面这个交互细节很加分很多教程里没写但实际做出来很自然。5.2 Axios 封装统一处理 token 和返回码写一个src/api/request.js对 axios 做二次封装。核心是请求拦截器里加 token响应拦截器里统一判断业务码import axios from axios import { ElMessage } from element-plus 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 { const res response.data if (res.code ! 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络请求异常请稍后重试) return Promise.reject(error) } ) export default request注意这里baseURL: /api配合 Vite 的代理前端代码里请求路径直接写/movies就行不用感知后端地址。如果有人部署时需要把前端静态文件直接交给后端托管这种写法也完全兼容——只要把前端 build 出来的dist目录放到 Flask 的static目录再把根路由指到index.html即可。5.3 选座页面的实现思路选座页是全项目前端交互最复杂的页面。逻辑拆开看其实就三块生成座位网格、处理选中态、提交订单。先拉取场次信息和已售座位const session ref({}) const soldSeats ref([]) const selectedSeats ref([]) async function loadSeats() { const res await request.get(/sessions/${route.params.sessionId}/seats) session.value res.data.session soldSeats.value res.data.soldSeats // [{row: 3, col: 5}, ...] }座位网格用一个二维循环渲染根据行列数生成Array.from({ length: session.hall.rows })作为行数每行再根据cols生成列。每个座位是否已售用soldSeats数组判断是否被当前用户选中用selectedSeats判断。点击事件里先判断是否已售是就 return否则切换选中状态。这里就是vue3 computed发挥作用的地方。选中数量和总价不需要一个变量一个变量地去手动更新直接用 computed 派生const selectedCount computed(() selectedSeats.value.length) const totalPrice computed(() selectedCount.value * Number(session.value.price))当selectedSeats变化时这两个计算属性自动更新模板里直接绑定即可。这比在每次点击方法里手动维护count和price两个变量要干净得多也是 Vue3 组合式 API 的核心用法。提交订单时把session_id和座位坐标数组发给后端async function submitOrder() { if (selectedSeats.value.length 0) { ElMessage.warning(请先选择座位) return } const res await request.post(/orders, { session_id: Number(route.params.sessionId), seats: selectedSeats.value.map(s ({ row: s.row, col: s.col })) }) ElMessage.success(订单创建成功请在15分钟内完成支付) router.push(/orders?highlight${res.data.order_id}) }6. 踩坑实录与排查技巧6.1 跨域问题开发环境最闹心的拦路虎前后端分离项目第一个报错十有八九是跨域。浏览器访问http://localhost:5173的页面去请求http://localhost:5000的接口因为端口不同浏览器会拦截响应。网上大多数教程让你在 Flask 里装flask-cors这种方案确实能解决但我建议优先用 Vite 的代理方式也就是前面vite.config.js里那个proxy配置。代理的原理是浏览器只跟5173端口通信Vite dev server 收到/api开头的请求后在服务器端转发到5000端口这样浏览器视角下所有请求都是同源的根本不触发跨域。如果确实要在 Flask 里开 CORS注意flask-cors默认会放开所有域名和请求头本地开发无所谓但如果部署到公网建议把origins限定为你的前端域名避免安全风险。6.2 中文乱码与时间时区问题数据库中文乱码基本都出在字符集。记住三步建库时指定utf8mb4连接串里带charsetutf8mb4JSON 接口本身就支持 Unicode 所以前端不会乱码。如果你发现接口返回的中文是\uXXXX这种转义序列那只是浏览器显示问题解决方案是在 Flask 的app.json.ensure_ascii False或者返回时指定jsonify(...)的默认行为实际上 Flask 3.x 默认已经不转义了遇到再处理不迟。时间时区是另一个容易踩的点MySQL 的DATETIME不带时区Flask 的datetime.now()取的是服务器本地时间如果你部署的服务器和用户不在同一个时区时间显示会差 8 个小时。课设阶段最简单的方式是统一用本地时间前后端显示都不做时区转换保证逻辑一致即可。要注意的是前端拿到的时间是 ISO 格式显示时需要格式化如果后端返回的是字符串前端直接用字符串渲染就行别再做new Date()转换否则时区问题会更加明显。6.3 座位并发冲突的兜底策略前面说过了唯一约束 事务是最可靠的方案。但实际测试时你会发现一个问题如果两张订单在同一毫秒同时插同一个座位怎么办答案是数据库的索引机制会保证只有一个插入成功另一个事务会卡在索引检查上然后抛IntegrityError。你拿两个终端同时 curl 下单接口多测几次就能看到一次成功、一次失败的预期结果。另一个容易被忽略的情况是用户选了座位订单创建成功后一直不支付座位处于锁定状态。此时另一个用户想选这个座位系统应该明确提示已被锁定而不是也让他创建成功。因为order_seat里已经有记录所以查询已售座位时自然包含这个座位。但要注意已取消的订单下座位要释放否则会造成座位永远被占。这就是为什么查询已售座位时要 JOIN 订单表并过滤掉status 2的订单。6.4 Vue3 路由跳转不刷新页面这个问题的经典场景是从电影 A 的详情页跳转到电影 B 的详情页URL 变了但页面内容没变因为 Vue3 复用了同一个组件实例onMounted不会重新执行。解决方法有两种一种是在组件内监听路由参数变化watch( () route.params.id, (newId, oldId) { if (newId ! oldId) { loadMovie(newId) } }, { immediate: true } )另一种更简单粗暴的方法是给路由组件加keyrouter-view :keyroute.fullPath /这样每次路径变化都会强制重挂载组件。缺点是页面会重新渲染浪费一点性能但课设规模完全无所谓。我习惯用第二种逻辑简单不容易漏。6.5 开发调试与部署建议Flask 调试时打开debugTrue可以拿到详细的报错信息但生产环境必须关掉。部署时建议用gunicorn来跑 Flaskgunicorn -w 4 -b 0.0.0.0:5000 app:app前端执行npm run build生成dist目录然后用 Nginx 托管静态文件并把/api路径反向代理到 Flask 的5000端口。这样整套系统就跑在同一个端口上了跨域问题也彻底消失。最后说点实际的体会。这种带状态流转的系统最值得花时间的地方不是页面做得多花哨而是把订单和座位的状态机理清楚什么时候锁定座位、什么时候释放、支付失败怎么办、支付成功又怎么更新。把这些边界情况全部测一遍比多写几个花哨页面更能体现你对系统的理解。我记得第一次做的时候只考虑了下单成功后座位锁定完全没处理超时释放结果测试时选了一堆座位第二天再看全部还是锁定的连自己都没法买票了。后来花了一晚上把懒释放逻辑补上、把order_seat表加了唯一约束才算是把这个问题真正解决。答辩时老师最常问的也是这几个点如果两个人同时选同一个座位怎么办订单 15 分钟不支付座位会不会释放支付到一半用户关掉页面呢你能把这几个问题答明白项目就成功了七成。如果后续想继续扩展可以考虑引入 Redis 做座位的临时锁、对接真实支付回调、用 WebSocket 推送座位实时余量这些都是很自然的进阶方向。
返回列表