
简介这是一套基于Django与Vue.js全栈开发的旅游景区管理系统源码面向Python Web开发初学者与中小型旅游类项目开发者解决景区门票在线管理、用户预订及后台运营一体化需求。资源包含392个文件涵盖32个Python后端逻辑文件、28个Vue前端组件、154个JPEG/JPG景区图片素材、35个TS类型脚本及1个SQL数据库初始化文件整体压缩包25.02MB结构清晰分为serverDjango后端与webVue前端两大目录便于分层学习与二次开发。已有278人学习下载资源提供完整前后端代码、可直接运行的MySQL数据脚本含shop数据库建表与初始数据、详细项目说明文档以及首页、景区详情、门票购买、订单管理、分类/标签/评论/日志等全功能模块实现适合用于课程设计、毕设参考或旅游SaaS系统快速原型搭建。1. 用 Django Vue 搭建旅游景区管理系统不是堆技术而是解决「票务流程割裂、数据难同步、前端交互卡顿」这三类真实痛点很多景区还在用 Excel 管理门票库存、靠人工核销入场码、后台改个票价要发邮件通知前端——这种模式在节假日瞬时并发超 500 请求时必然崩盘。本项目标题里的「Django Vue」不是为了凑热门词而是明确采用「Django 做强一致性事务后端 Vue 做高响应式前端」的分工逻辑Django 负责订单原子性扣减、库存锁、支付回调幂等校验Vue 负责实时渲染余票数、动态加载景区导览图、游客端扫码核销动画反馈。它面向的是中小型文旅单位如县级文旅局下属景区、5A级分园的技术实施者——你不需要从零写支付网关但必须能看懂models.py里TicketInventory的select_for_update()用法你不必精通 Vue3 Composition API 全套语法但得会改api/ticket.js里getAvailableTickets()的请求拦截器加 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_length100, verbose_name景区名称) code models.CharField(max_length20, uniqueTrue, verbose_name景区编码) # 用于API路由和二维码生成 address models.TextField(verbose_name地址) class TicketType(models.Model): SCENE_TYPE_CHOICES [ (day, 日票), (year, 年卡), (night, 夜场票), (combo, 联票), ] name models.CharField(max_length50, verbose_name票种名称) scene_type models.CharField(max_length10, choicesSCENE_TYPE_CHOICES) is_time_limited models.BooleanField(defaultFalse, verbose_name是否限时票) # 控制是否显示时段选择 is_realname_required models.BooleanField(defaultTrue, verbose_name是否实名制) class TicketInventory(models.Model): scenic_area models.ForeignKey(ScenicArea, on_deletemodels.CASCADE, related_nameinventories) ticket_type models.ForeignKey(TicketType, on_deletemodels.CASCADE, related_nameinventories) date models.DateField(nullTrue, blankTrue, verbose_name适用日期) # 仅对固定日期票有效 time_slot models.CharField(max_length20, nullTrue, blankTrue, 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.py中unique_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}, status405) 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__codescenic_code, ticket_type_idticket_type_id, datedate_str or None, time_slottime_slot or None ) if inventory.available_stock 0: return JsonResponse({error: 库存不足}, status400) # 扣减库存此时其他请求已被阻塞 inventory.available_stock - 1 inventory.save() # 创建订单省略具体字段 order Order.objects.create( scenic_areainventory.scenic_area, ticket_typeinventory.ticket_type, userrequest.user, statuspaid ) return JsonResponse({order_id: order.id, remaining: inventory.available_stock}) except TicketInventory.DoesNotExist: return JsonResponse({error: 票种不可用}, status404) except Exception as e: return JsonResponse({error: str(e)}, status500)参数说明select_for_update()默认在当前事务结束前持有行锁。若使用 MySQL需确保表引擎为 InnoDB若用 PostgreSQL需注意FOR UPDATE在READ COMMITTED隔离级别下行为一致。此处未加nowaitTrue是因业务允许短暂等待如用户刷新页面若需立即失败可改为select_for_update(nowaitTrue)并捕获DatabaseError。2.3 配置 Django Admin 界面用list_filter和search_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, 身份证号格式错误, levelerror) return super().save_model(request, obj, form, change)注意search_fields中user__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.response中window.location.href是硬跳转比 Vue Router 的router.push()更可靠避免路由守卫未生效导致白屏。3.2 使用 Vuex 持久化用户信息与景区列表避免每次刷新都重新请求游客端首页需展示「附近景区」管理端需记住「当前操作景区」。Vuex 默认内存存储刷新即丢需配合vuex-persistedstatenpm 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 状态也存入 localStoragekey设置为唯一字符串防止与其他 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( fstock_{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( fstock_{scenic_code}, self.channel_name ) async def send_stock_update(self, event): # 接收库存更新事件并推送给前端 await self.send(text_datajson.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_APPLICATIONWebSocket 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 ... ENGINEInnoDB DEFAULT CHARSETutf8;需手动替换为utf8mb4否则中文可能乱码STRICT_TRANS_TABLES在 MySQL 8.0.19 默认开启禁用后需重启 MySQL 生效生产环境建议在my.cnf中永久配置sql_mode NO_ENGINE_SUBSTITUTION。4.2 Django 连接 MySQL 8.0必须安装mysqlclient并指定auth_pluginMySQL 8.0 默认使用caching_sha2_password认证插件旧版mysqlclient不兼容# 先卸载旧版本 pip uninstall mysqlclient # 安装支持新认证的版本2.1.0 pip install mysqlclient2.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参数告诉mysqlclient用mysql_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页面会 404proxy_pass末尾的/表示路径重写/api/tickets/会被转发为http://127.0.0.1:8000/tickets/与 Djangourls.py中path(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 Query124msSELECT * FROM tourism_ticketinventory WHERE scenic_area_id 1 AND ticket_type_id 289msSELECT * 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_type且rows显著减少则索引生效。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.py中REST_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。本文还有配套的精品资源点击获取