news 2026/9/14 14:49:21

Django+Vue景区票务系统:高并发库存扣减与实时余票同步

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+Vue景区票务系统:高并发库存扣减与实时余票同步

简介:这是一套基于Django与Vue.js全栈开发的旅游景区管理系统源码,面向Python Web开发初学者与中小型旅游类项目开发者,解决景区门票在线管理、用户预订及后台运营一体化需求。资源包含392个文件,涵盖32个Python后端逻辑文件、28个Vue前端组件、154个JPEG/JPG景区图片素材、35个TS类型脚本及1个SQL数据库初始化文件,整体压缩包25.02MB,结构清晰分为server(Django后端)与web(Vue前端)两大目录,便于分层学习与二次开发。已有278人学习下载,资源提供完整前后端代码、可直接运行的MySQL数据脚本(含shop数据库建表与初始数据)、详细项目说明文档,以及首页、景区详情、门票购买、订单管理、分类/标签/评论/日志等全功能模块实现,适合用于课程设计、毕设参考或旅游SaaS系统快速原型搭建。

1. 用 Django + Vue 搭建旅游景区管理系统,不是堆技术,而是解决「票务流程割裂、数据难同步、前端交互卡顿」这三类真实痛点

很多景区还在用 Excel 管理门票库存、靠人工核销入场码、后台改个票价要发邮件通知前端——这种模式在节假日瞬时并发超 500 请求时必然崩盘。本项目标题里的「Django + Vue」不是为了凑热门词,而是明确采用「Django 做强一致性事务后端 + Vue 做高响应式前端」的分工逻辑:Django 负责订单原子性扣减、库存锁、支付回调幂等校验;Vue 负责实时渲染余票数、动态加载景区导览图、游客端扫码核销动画反馈。它面向的是中小型文旅单位(如县级文旅局下属景区、5A级分园)的技术实施者——你不需要从零写支付网关,但必须能看懂models.pyTicketInventoryselect_for_update()用法;你不必精通 Vue3 Composition API 全套语法,但得会改api/ticket.jsgetAvailableTickets()的请求拦截器加 token。项目附带的.sql文件不是简单建表脚本,而是含初始景区分类、门票阶梯定价策略、管理员角色权限预置的生产级数据快照,可直接导入 MySQL 8.0+ 或 PostgreSQL 12+ 运行。


2. Django 后端:用原生 ORM 构建门票核心模型,避开常见事务陷阱与权限漏洞

2.1 设计符合景区业务逻辑的数据库模型,重点处理「多类型门票+时段库存+实名核销」三重约束

景区门票远不止「成人票/学生票」两种。实际需支持:

  • 类型维度:日票、年卡、夜场票、联票(如「黄山+宏村」组合)
  • 时间维度:固定日期票(如「2024-10-01」)、有效期内任选日(如「购票后30天内有效」)、时段票(如「14:00-17:00 场次」)
  • 核销维度:二维码核销(需绑定身份证号)、人脸识别核销(需对接硬件SDK)、窗口人工核销(需操作员工号留痕)

对应 Djangomodels.py关键字段设计如下:

# models.py from django.db import models from django.contrib.auth.models import User class ScenicArea(models.Model): name = models.CharField(max_length=100, verbose_name="景区名称") code = models.CharField(max_length=20, unique=True, verbose_name="景区编码") # 用于API路由和二维码生成 address = models.TextField(verbose_name="地址") class TicketType(models.Model): SCENE_TYPE_CHOICES = [ ('day', '日票'), ('year', '年卡'), ('night', '夜场票'), ('combo', '联票'), ] name = models.CharField(max_length=50, verbose_name="票种名称") scene_type = models.CharField(max_length=10, choices=SCENE_TYPE_CHOICES) is_time_limited = models.BooleanField(default=False, verbose_name="是否限时票") # 控制是否显示时段选择 is_realname_required = models.BooleanField(default=True, verbose_name="是否实名制") class TicketInventory(models.Model): scenic_area = models.ForeignKey(ScenicArea, on_delete=models.CASCADE, related_name="inventories") ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE, related_name="inventories") date = models.DateField(null=True, blank=True, verbose_name="适用日期") # 仅对固定日期票有效 time_slot = models.CharField(max_length=20, null=True, blank=True, verbose_name="适用时段") # 如 "14:00-17:00" total_stock = models.PositiveIntegerField(verbose_name="总库存") available_stock = models.PositiveIntegerField(verbose_name="可用库存") class Meta: unique_together = ['scenic_area', 'ticket_type', 'date', 'time_slot'] # 防止重复库存记录

提示unique_together是关键约束。若漏掉time_slot,同一景区同一天的多个时段库存会冲突;若漏掉date,年卡类无日期限制的票种则无法复用该模型。实际部署前务必用python manage.py makemigrations && python manage.py migrate生成迁移文件,并检查0001_initial.pyunique_together是否生效。

2.2 实现高并发下的库存扣减,用select_for_update()防止超卖而非简单available_stock -= 1

单纯ticket.available_stock -= 1在并发场景下必然超卖。正确做法是在数据库层面加行锁:

# views.py from django.db import transaction from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt import json @csrf_exempt def create_order(request): if request.method != 'POST': return JsonResponse({'error': 'Method not allowed'}, status=405) data = json.loads(request.body) scenic_code = data.get('scenic_code') ticket_type_id = data.get('ticket_type_id') date_str = data.get('date') # 可为空,年卡类不传 time_slot = data.get('time_slot') # 可为空 try: with transaction.atomic(): # 锁定库存记录,防止并发修改 inventory = TicketInventory.objects.select_for_update().get( scenic_area__code=scenic_code, ticket_type_id=ticket_type_id, date=date_str or None, time_slot=time_slot or None ) if inventory.available_stock <= 0: return JsonResponse({'error': '库存不足'}, status=400) # 扣减库存(此时其他请求已被阻塞) inventory.available_stock -= 1 inventory.save() # 创建订单(省略具体字段) order = Order.objects.create( scenic_area=inventory.scenic_area, ticket_type=inventory.ticket_type, user=request.user, status='paid' ) return JsonResponse({'order_id': order.id, 'remaining': inventory.available_stock}) except TicketInventory.DoesNotExist: return JsonResponse({'error': '票种不可用'}, status=404) except Exception as e: return JsonResponse({'error': str(e)}, status=500)

参数说明select_for_update()默认在当前事务结束前持有行锁。若使用 MySQL,需确保表引擎为 InnoDB;若用 PostgreSQL,需注意FOR UPDATEREAD COMMITTED隔离级别下行为一致。此处未加nowait=True是因业务允许短暂等待(如用户刷新页面),若需立即失败,可改为select_for_update(nowait=True)并捕获DatabaseError

2.3 配置 Django Admin 界面,用list_filtersearch_fields快速定位异常订单

景区管理员日常需快速查「某天某景区退票率过高」或「某身份证号重复购票」,原生 Admin 需定制:

# admin.py from django.contrib import admin from .models import Order, TicketInventory @admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ['id', 'user', 'scenic_area', 'ticket_type', 'status', 'created_at', 'realname'] list_filter = [ 'status', 'scenic_area__name', # 关联外键过滤 ('created_at', admin.DateFieldListFilter), # 时间范围筛选 ] search_fields = ['user__username', 'realname', 'id_number'] # 支持身份证号模糊搜索 date_hierarchy = 'created_at' # 顶部添加时间导航条 # 重写保存逻辑,确保实名信息入库 def save_model(self, request, obj, form, change): if not change and obj.realname and obj.id_number: # 新建订单时校验身份证号格式(简化版) if len(obj.id_number) != 18: self.message_user(request, '身份证号格式错误', level='error') return super().save_model(request, obj, form, change)

注意search_fieldsuser__username利用了 Django 的双下划线语法穿透外键,比手动写get_user_display()更高效;date_hierarchy自动生成「按年/月/日」的折叠导航,比list_filter的日期选择器更符合管理员直觉操作。


3. Vue 前端:用 Axios 封装请求 + Vuex 持久化用户态,解决「页面刷新丢失登录态、余票数不同步」问题

3.1 构建可复用的 API 请求模块,自动注入 token 并统一处理 401 登录过期

Vue 项目中若每个组件都手动axios.get('/api/tickets/', {headers: {Authorization: ...}}),维护成本极高。应封装为api/index.js

// src/api/index.js import axios from 'axios' // 从 localStorage 读取 token(登录后由 login 接口写入) const getToken = () => localStorage.getItem('auth_token') const apiClient = axios.create({ baseURL: '/api/', // Django 后端 API 前缀 timeout: 10000, }) // 请求拦截器:自动添加 Authorization 头 apiClient.interceptors.request.use( config => { const token = getToken() if (token) { config.headers.Authorization = `Token ${token}` } return config }, error => Promise.reject(error) ) // 响应拦截器:统一处理 401 apiClient.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { localStorage.removeItem('auth_token') window.location.href = '/login/' // 跳转登录页 } return Promise.reject(error) } ) export default apiClient

逻辑说明baseURL: '/api/'表明所有请求路径自动补前缀,调用apiClient.get('tickets/')实际请求/api/tickets/interceptors.responsewindow.location.href是硬跳转,比 Vue Router 的router.push()更可靠,避免路由守卫未生效导致白屏。

3.2 使用 Vuex 持久化用户信息与景区列表,避免每次刷新都重新请求

游客端首页需展示「附近景区」,管理端需记住「当前操作景区」。Vuex 默认内存存储,刷新即丢,需配合vuex-persistedstate

npm install vuex-persistedstate
// src/store/index.js import Vue from 'vue' import Vuex from 'vuex' import createPersistedState from 'vuex-persistedstate' Vue.use(Vuex) export default new Vuex.Store({ state: { user: null, // 登录用户信息 scenicAreas: [], // 缓存景区列表,避免首页重复请求 currentScenic: null, // 当前选中的景区对象 }, mutations: { SET_USER(state, user) { state.user = user }, SET_SCENIC_AREAS(state, areas) { state.scenicAreas = areas }, SET_CURRENT_SCENIC(state, scenic) { state.currentScenic = scenic } }, plugins: [ createPersistedState({ key: 'tourism-vue-store', paths: ['user', 'scenicAreas', 'currentScenic'] // 仅持久化这三个字段 }) ] })

参数说明paths显式声明需持久化的 state 字段,避免将临时 loading 状态也存入 localStorage;key设置为唯一字符串,防止与其他 Vue 项目冲突;createPersistedState默认使用localStorage,若需兼容低版本浏览器可传入storage: window.sessionStorage

3.3 实现余票数实时更新,用 WebSocket 替代轮询降低服务器压力

Vue 页面显示「黄山风景区 · 日票剩余 128 张」,若每秒setInterval(() => api.get('/tickets/'), 1000),1000 用户同时访问将产生 1000 QPS。Django Channels 提供轻量级 WebSocket 支持:

# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer from asgiref.sync import sync_to_async from .models import TicketInventory class TicketStockConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() # 订阅特定景区票种的库存变更 scenic_code = self.scope['url_route']['kwargs'].get('scenic_code') if scenic_code: await self.channel_layer.group_add( f'stock_{scenic_code}', self.channel_name ) async def disconnect(self, close_code): scenic_code = self.scope['url_route']['kwargs'].get('scenic_code') if scenic_code: await self.channel_layer.group_discard( f'stock_{scenic_code}', self.channel_name ) async def send_stock_update(self, event): # 接收库存更新事件并推送给前端 await self.send(text_data=json.dumps({ 'type': 'stock_update', 'scenic_code': event['scenic_code'], 'available_stock': event['available_stock'] }))

Vue 组件中建立连接:

<!-- components/TicketDisplay.vue --> <script> export default { data() { return { stock: 0, ws: null } }, mounted() { this.initWebSocket() }, beforeDestroy() { if (this.ws) this.ws.close() }, methods: { initWebSocket() { const scenicCode = this.$route.params.scenicCode this.ws = new WebSocket(`ws://${window.location.host}/ws/stock/${scenicCode}/`) this.ws.onmessage = (event) => { const data = JSON.parse(event.data) if (data.type === 'stock_update') { this.stock = data.available_stock } } } } } </script>

注意:Django Channels 需额外安装channels包并配置ASGI_APPLICATION;WebSocket URL 中ws://协议必须与前端页面协议一致(HTTP 页面不能连wss://);beforeDestroy生命周期钩子确保组件卸载时关闭连接,防止内存泄漏。


4. 数据库与部署:用提供的 SQL 文件初始化结构,规避 MySQL 8.0+ 密码认证兼容性问题

4.1 导入 SQL 文件前必做的三件事:确认字符集、禁用严格模式、验证外键约束

项目附带的.sql文件通常基于 MySQL 5.7 生成,在 MySQL 8.0+ 上直接source tourism.sql可能报错。需先执行:

-- 步骤1:切换到 utf8mb4 字符集(支持 emoji 和生僻字) ALTER DATABASE tourism CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 步骤2:临时禁用严格模式(避免 datetime 默认值报错) SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES','')); -- 步骤3:检查外键是否启用(Django 迁移依赖此功能) SELECT @@FOREIGN_KEY_CHECKS; -- 应返回 1

然后导入:

mysql -u root -p tourism < tourism_init.sql

提示tourism_init.sql中若含CREATE TABLE ... ENGINE=InnoDB DEFAULT CHARSET=utf8;,需手动替换为utf8mb4,否则中文可能乱码;STRICT_TRANS_TABLES在 MySQL 8.0.19+ 默认开启,禁用后需重启 MySQL 生效,生产环境建议在my.cnf中永久配置sql_mode = "NO_ENGINE_SUBSTITUTION"

4.2 Django 连接 MySQL 8.0+,必须安装mysqlclient并指定auth_plugin

MySQL 8.0 默认使用caching_sha2_password认证插件,旧版mysqlclient不兼容:

# 先卸载旧版本 pip uninstall mysqlclient # 安装支持新认证的版本(>=2.1.0) pip install "mysqlclient>=2.1.0" # 配置 settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'tourism', 'USER': 'tourism_user', 'PASSWORD': 'your_secure_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'auth_plugin': 'mysql_native_password', # 关键!强制使用旧认证 } } }

参数说明auth_plugin参数告诉mysqlclientmysql_native_password插件连接,绕过caching_sha2_password的握手问题;若仍报错Authentication plugin 'caching_sha2_password' cannot be loaded,需在 MySQL 中为用户重置密码:ALTER USER 'tourism_user'@'%' IDENTIFIED WITH mysql_native_password BY 'new_password';

4.3 Vue 打包后静态资源部署,用 Nginx 代理 API 请求避免跨域

Vue CLI 默认打包到dist/目录,Django 静态文件服务性能差,应交由 Nginx:

# /etc/nginx/sites-available/tourism server { listen 80; server_name tourism.example.com; # 静态资源直接由 Nginx 服务 location /static/ { alias /var/www/tourism/dist/static/; expires 1y; add_header Cache-Control "public, immutable"; } # Vue Router history 模式回退 location / { root /var/www/tourism/dist; try_files $uri $uri/ /index.html; } # API 请求反向代理到 Django location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

注意try_files $uri $uri/ /index.html;是 Vue Router history 模式的必需配置,否则刷新/ticket/123页面会 404;proxy_pass末尾的/表示路径重写,/api/tickets/会被转发为http://127.0.0.1:8000/tickets/,与 Djangourls.pypath('api/tickets/', ...)匹配。


5. 关键调试技巧:用 Django Debug Toolbar 查慢查询,用 Chrome Network 验证 Vue 请求头

5.1 定位「门票列表加载慢」问题:在 Django Admin 中启用 Debug Toolbar

/admin/tourism/ticketinventory/页面打开缓慢,不是盲目优化前端,而是先确认是否数据库查询拖慢:

# settings.py(仅开发环境) INSTALLED_APPS += ['debug_toolbar'] MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] INTERNAL_IPS = ['127.0.0.1'] # urls.py if settings.DEBUG: import debug_toolbar urlpatterns += [path('__debug__/', include(debug_toolbar.urls))]

访问 Admin 页面后,右上角出现黄色小图标,点击进入「SQL」标签页,可看到:

TimeSQL Query
124msSELECT * FROM tourism_ticketinventory WHERE scenic_area_id = 1 AND ticket_type_id = 2
89msSELECT * FROM tourism_scenicarea WHERE id = 1

若第一条查询耗时 >100ms,说明缺少索引:

-- 为高频查询字段添加复合索引 CREATE INDEX idx_inventory_scenic_type ON tourism_ticketinventory (scenic_area_id, ticket_type_id);

验证方法:执行EXPLAIN SELECT * FROM tourism_ticketinventory WHERE scenic_area_id = 1 AND ticket_type_id = 2;,若key列显示idx_inventory_scenic_typerows显著减少,则索引生效。

5.2 解决「Vue 登录后无法获取用户信息」:用 Chrome DevTools 检查请求头与响应体

登录成功后调用/api/user/返回 401,但 token 已存入 localStorage。此时打开 Chrome Network 标签页:

  • 找到/api/user/请求 → 点击 → 查看Headers标签页
  • 检查Request Headers中是否有Authorization: Token abc123...
  • 若无,说明apiClient.interceptors.request未生效,检查getToken()是否返回空字符串(可能 localStorage key 名写错)
  • 若有,再看Response标签页,若返回{"detail":"Invalid token."},说明 Django 的TokenAuthentication未正确配置,检查settings.pyREST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES']是否包含'rest_framework.authentication.TokenAuthentication'

技巧:在 Network 面板右键请求 → 「Copy as cURL」,粘贴到终端执行,可排除浏览器缓存干扰;若 cURL 成功而浏览器失败,问题必在前端 JS 逻辑而非后端。

5.3 验证库存扣减是否真正防超卖:用 Apache Bench 模拟并发请求

不要依赖人工点按钮测试,用ab工具压测:

# 模拟 100 个用户并发请求创建订单(库存初始为 50) ab -n 100 -c 100 -H "Authorization: Token your_token_here" \ -p order_data.json -T "application/json" \ "http://localhost:8000/api/orders/"

order_data.json内容:

{"scenic_code": "huangshan", "ticket_type_id": 1, "date": "2024-10-01"}

压测结束后检查数据库:

SELECT available_stock FROM tourism_ticketinventory WHERE scenic_area_id = 1 AND ticket_type_id = 1 AND date = '2024-10-01';

结果应为0(初始 50 - 成功下单 50),若大于 0 说明有请求被拒绝(正常),若小于 0 说明超卖(严重 bug)。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:48:10

DBViewer实战:把数据库工作台搬进浏览器的完整指南

DBViewer这个名字一听就懂——把数据库工作台搬进浏览器。最近我在处理一个远程协作的临时项目&#xff0c;几个同事分散在不同城市&#xff0c;数据库分布在测试机和客户内网&#xff0c;过去那种“谁要查数据就各自装一个桌面客户端、再拷贝一份连接配置”的做法彻底行不通。…

作者头像 李华
网站建设 2026/9/14 14:47:26

微信小游戏五子棋源码拆解:Canvas渲染与AI评分指南

简介&#xff1a;这份微信小游戏源码实现单机五子棋对战&#xff0c;适合刚接触微信小游戏开发的新手&#xff0c;也适合快速了解小游戏工程结构的学习者参考。压缩包共6个文件&#xff0c;主体为3个js脚本&#xff0c;分别涉及入口启动、主循环与棋盘逻辑处理&#xff1b;2个j…

作者头像 李华
网站建设 2026/9/14 14:45:34

华为MateBook E频率限制的三层技术解析

1. 问题不是“芯片不行”&#xff0c;而是“调度策略被重写” 华为 MateBook E 2022 这台设备刚发布时&#xff0c;我第一时间拆机、跑分、压测&#xff0c;结果当场愣住——它用的明明是 Intel Core i7-1265U&#xff08;10核12线程&#xff0c;P核E核混合架构&#xff09;&am…

作者头像 李华
网站建设 2026/9/14 14:45:01

日本留学中介模式解析:免费与收费的优劣对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:44:58

H5购物静态页面实战:跨端兼容与纯前端交互闭环

简介&#xff1a;本资源是一套基于HTML5技术构建的电商网站前端静态页面集合&#xff0c;面向Web前端初学者、UI设计师及H5开发实践者&#xff0c;旨在帮助理解现代移动端电商界面的结构设计、交互逻辑与响应式实现。资源包含首页、商品分类页、商品详情页、购物车、结算页及个…

作者头像 李华