资讯动态

Django旅游网站实战:高并发订单与地理库存系统

发布时间:2026/10/27 23:34:50 来源:尧图企业网站定制
简介这是一套基于Python与Django框架构建的旅游主题网站完整工程实践资源面向计算机专业本科生、Web开发初学者及毕业设计选题者聚焦餐饮旅游类业务场景的全栈开发训练。资源包含2001个文件主体为83个核心Python后端逻辑文件、119个JavaScript交互脚本、38个CSS样式文件及1627张景点/餐饮相关JPG图片素材辅以Vue组件、SVG图标、字体文件及SQLite3数据库整体压缩包达132.64MB结构完整、即开即用。已有144人下载学习适用于课程设计、工程实训或毕设原型开发。读者可直接部署运行获得含用户管理、景点展示、线路预订、后台管理等模块的可运行系统同时深入理解Django MTV架构、静态资源组织规范、前后端协同逻辑及旅游类网站典型数据模型设计。1. 为什么一个“旅游自主网站”要用 Django 而不是 Flask 或纯 HTML——它真能扛住节假日流量洪峰吗去年十一黄金周我帮本地一家中型旅行社上线了一个「自主旅游网站」用户能查线路、比价格、选出发日期、填游客信息、在线支付、生成电子合同后台还能导出 Excel 行程单、自动发短信提醒、对接第三方酒店库存接口。上线第三天单日 PV 突破 12 万订单峰值达 837 单/小时。很多人第一反应是“这不就是个带表单的静态页用 Vue Node.js 就够了啊。”但实际跑起来才发现Django 不是“重”而是把「旅游业务里那些藏在角落里的脏活累活」提前打包好了——比如多景区并发库存扣减时的事务隔离、不同线路间价格策略的动态组合儿童价/老人价/拼团价/早鸟价、用户提交后因支付超时需自动回滚订单并释放名额、甚至同一身份证号在 24 小时内不能重复预订同一班次大巴……这些都不是“前端交互”问题而是领域逻辑密集、状态流转复杂、数据一致性要求极高的典型 Web 应用场景。Python Django 的组合恰恰在 ORM 原生支持事务、Admin 后台开箱即用、中间件可插拔控制请求生命周期、信号机制解耦业务钩子等方面提供了远超“快速原型”的工程纵深。它适合的不是“做个首页展示”而是真正要上线、要收款、要对账、要审计、要抗压的旅游自主网站。如果你正卡在“功能堆得动但维护越来越喘”或“总在重复写登录校验/权限判断/Excel 导出/分页逻辑”那这个标题不是教学 Demo而是一条已被验证过的落地路径。2. 从零启动用 Django 4.2 搭建旅游网站最小可行骨架2.1 创建项目与核心 App 划分别一上来就建tourism这个大包新手常犯的错误是django-admin startproject travel_site之后立刻python manage.py startapp tourism然后把所有模型、视图、模板全塞进去。结果三个月后models.py超过 2000 行views.py里混着支付回调、短信发送、PDF 生成、库存校验……改一个字段要 grep 全局测一次要跑 47 个 TestCase。旅游业务天然具备清晰的领域边界我们按职责拆解为 5 个独立 App每个 App 对应一个明确业务域App 名职责是否必须关键模型示例core用户体系、基础配置、全局工具函数✅ 必须UserProfile,SiteConfigdestination景区、线路、交通方式、住宿类型等静态资源管理✅ 必须ScenicSpot,Route,TransportTypebooking订单生命周期创建→支付→确认→出票→取消→退款✅ 必须BookingOrder,BookingItem,PaymentRecordinventory实时库存控制班次余位、房间数、导游配额✅ 必须InventorySlot,InventoryLogcontent游记、攻略、问答、Banner 广告位⚠️ 可选TravelNote,FAQ,AdBanner提示core和destination是强依赖booking依赖前两者inventory必须与booking强耦合否则库存超卖content可后期接入。这种划分让python manage.py makemigrations时只影响相关模块CI 测试也能按 App 隔离运行。执行命令创建结构# 创建项目注意 Python 3.10Django 4.2 django-admin startproject travel_site . python manage.py startapp core python manage.py startapp destination python manage.py startapp booking python manage.py startapp inventory python manage.py startapp content然后在travel_site/settings.py中注册 AppsINSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 自研 Apps按依赖顺序排列 core, destination, booking, inventory, content, ]2.2 设计第一个关键模型ScenicSpot—— 为什么它必须带geo_location字段旅游网站的核心是「地点」但多数教程只建name,description,image。这会导致两个致命问题① 用户搜索“离我最近的古镇”时无法计算距离② 后期想做“周边 5km 景点推荐”或“路线规划”时数据库毫无地理索引能力。Django 本身不原生支持空间数据但django.contrib.gis是官方扩展非第三方只需 3 步启用安装 PostGISPostgreSQL 扩展或 SQLite Spatial开发用# Ubuntu 下安装 PostGIS生产环境推荐 sudo apt-get install postgis postgresql-14-postgis-3 # 创建带空间扩展的数据库 sudo -u postgres psql -c CREATE DATABASE travel_gis; sudo -u postgres psql -d travel_gis -c CREATE EXTENSION postgis;在settings.py中替换数据库后端并启用 GISDATABASES { default: { ENGINE: django.contrib.gis.db.backends.postgis, # 关键不是 django.db.backends.postgresql NAME: travel_gis, USER: postgres, PASSWORD: your_password, HOST: localhost, PORT: 5432, } } INSTALLED_APPS [django.contrib.gis] # 必须加在destination/models.py中定义带地理坐标的模型from django.contrib.gis.db import models as gis_models from django.db import models class ScenicSpot(gis_models.Model): name models.CharField(max_length100, verbose_name景点名称) description models.TextField(verbose_name简介) # 使用 PointField 存储经纬度WGS84 坐标系 geo_location gis_models.PointField( srid4326, # WGS84 标准GPS 设备通用 verbose_name地理位置, help_text点击地图选择位置或手动输入经度,纬度如116.397428,39.90923 ) address models.CharField(max_length200, verbose_name详细地址) is_active models.BooleanField(defaultTrue, verbose_name是否上架) class Meta: verbose_name 景点 verbose_name_plural 景点管理 # 添加空间索引大幅提升距离查询性能 indexes [gis_models.Index(fields[geo_location])] def __str__(self): return self.name参数说明srid4326是全球通用坐标系确保你用高德/百度地图 API 获取的坐标能直接入库PointField会自动在 PostgreSQL 中创建geometry(Point,4326)类型字段indexes行是性能关键——没有它ScenicSpot.objects.filter(geo_location__distance_lte(point, D(km5)))查询会全表扫描。2.3 Admin 后台定制让运营人员 5 分钟学会上架新线路Django Admin 是被严重低估的生产力工具。旅游网站的运营人员不是程序员他们需要✅ 看到「线路」列表时同时显示该线路包含几个景点、总天数、最低成团人数✅ 编辑线路时能拖拽排序景点顺序而非手动填序号✅ 新增线路时自动填充「创建时间」「最后更新人」✅ 敏感操作如下架需二次确认。在destination/admin.py中实现from django.contrib import admin from django.contrib.admin import ModelAdmin from django.contrib.gis.admin import OSMGeoAdmin from .models import ScenicSpot, Route, RouteStop admin.register(ScenicSpot) class ScenicSpotAdmin(OSMGeoAdmin): # 继承 OSMGeoAdmin 才能地图可视化编辑 list_display (name, address, is_active) list_filter (is_active,) search_fields (name, address) # 地图控件配置 default_lon 116.404 # 北京中心经度 default_lat 39.915 # 北京中心纬度 default_zoom 12 admin.register(Route) class RouteAdmin(admin.ModelAdmin): list_display (name, get_spot_count, total_days, min_group_size, is_active) list_filter (is_active, category) search_fields (name, description) # 自动填充创建人和时间 prepopulated_fields {slug: (name,)} # URL 友好路径 readonly_fields (created_at, updated_at, created_by) def save_model(self, request, obj, form, change): if not change: # 新建时 obj.created_by request.user obj.updated_by request.user super().save_model(request, obj, form, change) def get_spot_count(self, obj): return obj.stops.count() # 关联 RouteStop 计数 get_spot_count.short_description 包含景点数 admin.register(RouteStop) class RouteStopAdmin(admin.ModelAdmin): list_display (route, scenic_spot, order, day_number) list_editable (order, day_number) # 直接在列表页拖拽修改顺序 ordering (route, order)逻辑说明OSMGeoAdmin提供 OpenStreetMap 地图界面运营人员点地图即可设置geo_locationlist_editable让order字段可直接在列表页修改避免进详情页反复保存save_model钩子自动记录操作人省去手动填created_by字段get_spot_count是自定义方法通过stops.count()关联查询比在模型里加property更高效避免 N1 查询。3. 订单与库存如何防止“100 人抢最后一张票”导致超卖3.1 订单创建流程为什么BookingOrder必须用select_for_update()锁行旅游订单不是普通电商订单。一张订单可能含多个BookingItem如1 张机票 2 间酒店 1 个导游服务且每个 Item 对应不同库存源航班系统、酒店 PMS、导游排班表。若用普通create()高并发下极易出现用户 A 查库存有 1 间房 → 用户 B 同时查也有 1 间 → A 创建订单扣减 → B 创建订单也扣减 → 库存变 -1。正确做法是在事务内用select_for_update()锁定库存记录确保“查扣”原子性# booking/models.py from django.db import models, transaction from django.core.exceptions import ValidationError class BookingOrder(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (confirmed, 已确认), (cancelled, 已取消), ] user models.ForeignKey(core.UserProfile, on_deletemodels.CASCADE) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue) total_amount models.DecimalField(max_digits10, decimal_places2) class BookingItem(models.Model): order models.ForeignKey(BookingOrder, on_deletemodels.CASCADE, related_nameitems) inventory_type models.CharField(max_length20) # hotel_room, flight_seat, guide_shift inventory_id models.PositiveIntegerField() # 对应 inventory_app 中的主键 quantity models.PositiveIntegerField(default1) price models.DecimalField(max_digits10, decimal_places2) # inventory/models.py class InventorySlot(models.Model): inventory_type models.CharField(max_length20) # 同 BookingItem.inventory_type inventory_id models.PositiveIntegerField() # 同 BookingItem.inventory_id date models.DateField() available_quantity models.PositiveIntegerField(default0) reserved_quantity models.PositiveIntegerField(default0) # 已被 pending 订单占用 # booking/services.py def create_booking_order(user, items_data): items_data 示例 [ {type: hotel_room, id: 101, date: 2024-06-15, qty: 2}, {type: guide_shift, id: 201, date: 2024-06-15, qty: 1} ] with transaction.atomic(): # 1. 创建订单主表 order BookingOrder.objects.create( useruser, total_amountcalculate_total(items_data), statuspending ) # 2. 逐个处理 item锁定对应库存槽 for item_data in items_data: slot InventorySlot.objects.select_for_update().get( inventory_typeitem_data[type], inventory_iditem_data[id], dateitem_data[date] ) # 3. 检查可用量注意reserved_quantity 已计入不可用 if slot.available_quantity item_data[qty]: raise ValidationError(f库存不足{item_data[type]} {item_data[id]} 在 {item_data[date]} 仅剩 {slot.available_quantity} 个) # 4. 扣减并更新 reserved_quantity slot.reserved_quantity item_data[qty] slot.save() # 5. 创建订单明细 BookingItem.objects.create( orderorder, inventory_typeitem_data[type], inventory_iditem_data[id], quantityitem_data[qty], priceget_price(item_data) ) return order关键点select_for_update()在 PostgreSQL 中生成SELECT ... FOR UPDATE语句锁住查询到的行其他事务必须等待锁释放才能读写该行transaction.atomic()确保整个流程要么全部成功要么全部回滚reserved_quantity字段是防超卖的核心设计——它记录“已被 pending 订单占用但未最终支付”的数量避免支付失败后库存永久丢失。3.2 支付超时自动释放用 Django Q 框架实现可靠定时任务用户下单后未支付库存不能一直被锁着。常见错误是用time.sleep()或 crontab 脚本轮询但 crontab 无法保证精确到秒且多实例部署时易重复执行。Django Q 是更可靠的方案轻量级、支持数据库锁、可分布式安装并配置pip install django-q# settings.py INSTALLED_APPS [django_q] Q_CLUSTER { name: DjangORM, workers: 4, recycle: 500, timeout: 60, compress: True, save_limit: 200, queue_limit: 500, cpu_affinity: 1, label: Django Q, redis: { host: 127.0.0.1, port: 6379, db: 0, } }编写超时检查任务# booking/tasks.py from django_q.models import Schedule from django_q.tasks import async_task, schedule from django.utils import timezone from datetime import timedelta from .models import BookingOrder, InventorySlot def release_expired_orders(): 释放创建超过 15 分钟且未支付的订单 cutoff_time timezone.now() - timedelta(minutes15) expired_orders BookingOrder.objects.filter( statuspending, created_at__ltcutoff_time ).select_related(user) for order in expired_orders: # 回滚所有关联的库存 for item in order.items.all(): try: slot InventorySlot.objects.get( inventory_typeitem.inventory_type, inventory_iditem.inventory_id, dateget_date_from_item(item) # 你需要根据 item 类型提取日期 ) slot.reserved_quantity - item.quantity slot.save() except InventorySlot.DoesNotExist: pass # 库存记录可能已被清理跳过 order.status cancelled order.save() # 启动定时任务首次运行时调用 def setup_release_task(): schedule( booking.tasks.release_expired_orders, schedule_typeSchedule.MINUTES, minutes1, # 每分钟检查一次 repeats-1, # 永久重复 )参数说明schedule_typeSchedule.MINUTES表示按分钟调度minutes1是检查频率repeats-1表示无限重复select_related(user)减少 N1 查询任务内对每个BookingItem单独查InventorySlot并扣减reserved_quantity确保幂等性即使任务重复执行也不会多扣。4. 避坑旅游网站开发中 5 个血泪经验换来的高频问题4.1 现象Admin 后台上传图片后前端显示 404原因Django 默认不提供静态文件服务MEDIA_URL和MEDIA_ROOT配置缺失且未在urls.py中添加媒体文件路由。解决①settings.py中设置import os MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)②travel_site/urls.py中追加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)③ 生产环境必须用 Nginx 代理/media/到MEDIA_ROOT目录绝不能依赖 Django 的static()函数性能差且不安全。4.2 现象ScenicSpot地图编辑器加载空白控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED原因OSMGeoAdmin默认使用 OpenStreetMap 的免费瓦片服务但国内访问不稳定且无 CDN 加速。解决切换为高德地图瓦片需申请 Key# settings.py GIS_OPENLAYMAPS_URL https://webst0{s}.is.autonavi.com/appmaptile?langzh_cnsize1scale1style8x{x}y{y}z{z}ltype11 # 高德街道图 # 在 admin 中指定瓦片源 admin.register(ScenicSpot) class ScenicSpotAdmin(OSMGeoAdmin): map_template admin/osm.html # 自定义模板 # 或直接覆盖默认瓦片 URL需修改源码或用自定义 widget更稳妥方案用django-leaflet替代OSMGeoAdmin它支持灵活配置多种地图源。4.3 现象BookingOrder创建时偶发IntegrityError: duplicate key value violates unique constraint原因多个请求同时创建订单BookingOrder.objects.create()生成的id冲突尤其用 UUID 作主键时若未设defaultuuid.uuid4。解决① 确保主键为自增整数Django 默认② 若必须用 UUID在models.py中显式声明import uuid from django.db import models class BookingOrder(models.Model): id models.UUIDField(primary_keyTrue, defaultuuid.uuid4, editableFalse) # ...③根本解法用数据库序列PostgreSQL或select_for_update锁定订单号生成逻辑避免并发冲突。4.4 现象InventorySlot的available_quantity字段在高并发下出现负数原因available_quantity - qty是非原子操作CPU 先读值、再计算、再写入中间被其他线程修改。解决改用F()表达式from django.db.models import F # 错误写法非原子 slot.available_quantity - item.quantity slot.save() # 正确写法原子减法 InventorySlot.objects.filter( idslot.id, available_quantity__gteitem.quantity ).update(available_quantityF(available_quantity) - item.quantity)注意update()返回影响行数需检查是否为 1否则说明库存不足。4.5 现象Route模型中stops关系在 Admin 列表页显示RelatedObjectDoesNotExist原因RouteStop是中间模型多对多 throughAdmin 默认不预加载关联对象route.stops.all()触发 N1 查询且stops.count()在列表页每行都执行。解决在RouteAdmin中用prefetch_relatedadmin.register(Route) class RouteAdmin(admin.ModelAdmin): list_select_related (category,) # 一对一预加载 list_prefetch_related (stops__scenic_spot,) # 多对多预加载 def get_queryset(self, request): qs super().get_queryset(request) return qs.prefetch_related(stops__scenic_spot) # 关键 def get_spot_count(self, obj): return obj.stops.count() # 此时已预加载无额外查询5. 进阶实战用 Django Channels 实现“实时余位提醒”与“订单状态推送”旅游网站最让用户焦虑的是“刚选好行程提交前发现余位没了”。传统页面刷新太挫而 WebSocket 是唯一解。Django Channels 将其无缝集成进 Django 生态无需另起 Node.js 服务。5.1 架构设计为什么用 Redis 作 Channel Layer 而不用内存Channels 支持多种 Channel Layer通信后端InMemoryChannelLayer仅单进程、RedisChannelLayer分布式必备、RabbitMQChannelLayer企业级。旅游网站必选 Redis因为多台 Web 服务器需共享同一消息队列Celery 异步任务如支付回调需向用户 WebSocket 发送消息Redis 的 Pub/Sub 机制天然适配广播场景如“某线路余位变化”推送给所有监听该线路的用户。安装与配置pip install channels channels-redis# settings.py ASGI_APPLICATION travel_site.asgi.application CHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: { hosts: [(127.0.0.1, 6379)], }, }, }5.2 实现“余位实时监控”前端监听 后端广播步骤 1定义消费者Consumer# booking/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer from asgiref.sync import sync_to_async from .models import InventorySlot class InventoryConsumer(AsyncWebsocketConsumer): async def connect(self): self.route_id self.scope[url_route][kwargs][route_id] self.group_name finventory_route_{self.route_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 receive(self, text_data): data json.loads(text_data) if data.get(action) subscribe: # 前端传 route_id后端查当前余位并推送 available await self.get_available_count(data[route_id]) await self.send(text_datajson.dumps({ action: initial_state, available: available })) sync_to_async def get_available_count(self, route_id): # 此处需根据 route_id 查关联的所有 InventorySlot如酒店、交通 # 简化示例假设 route_id 直接对应 hotel_room 的 inventory_id from inventory.models import InventorySlot return InventorySlot.objects.filter( inventory_typehotel_room, inventory_idroute_id ).aggregate(totalmodels.Sum(available_quantity))[total] or 0步骤 2路由配置# booking/routing.py from django.urls import re_path from . import consumers websocket_urlpatterns [ re_path(rws/inventory/(?Proute_id\w)/$, consumers.InventoryConsumer.as_asgi()), ]# travel_site/routing.py from django.urls import path, re_path from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack import booking.routing application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack( URLRouter([ path(ws/inventory/, include(booking.routing.websocket_urlpatterns)), ]) ), })步骤 3前端监听Vue 示例script export default { data() { return { ws: null, availableCount: 0, routeId: this.$route.params.id } }, mounted() { this.initWebSocket() }, beforeUnmount() { this.ws?.close() }, methods: { initWebSocket() { const wsUrl ws://${window.location.host}/ws/inventory/${this.routeId}/ this.ws new WebSocket(wsUrl) this.ws.onopen () { this.ws.send(JSON.stringify({ action: subscribe, route_id: this.routeId })) } this.ws.onmessage (event) { const data JSON.parse(event.data) if (data.action initial_state) { this.availableCount data.available } else if (data.action update) { this.availableCount data.available // 弹窗提醒用户余位变化 if (data.available 3) { alert(⚠️ 余位紧张当前仅剩 ${data.available} 个) } } } } } } /script步骤 4库存变更时主动推送关键当InventorySlot更新时如支付成功、订单取消需主动通知所有监听该线路的客户端# inventory/models.py from django.db.models.signals import post_save from django.dispatch import receiver from channels.layers import get_channel_layer from asgiref.sync import async_to_sync receiver(post_save, senderInventorySlot) def notify_inventory_change(sender, instance, **kwargs): channel_layer get_channel_layer() # 构造组名与 Consumer 中一致 group_name finventory_route_{instance.inventory_id} async_to_sync(channel_layer.group_send)( group_name, { type: inventory_update, # 对应 consumer 中的 handler 方法 available: instance.available_quantity } )# booking/consumers.py class InventoryConsumer(AsyncWebsocketConsumer): # ... connect/disconnect/receive ... # 处理 group_send 发来的消息 async def inventory_update(self, event): await self.send(text_datajson.dumps({ action: update, available: event[available] }))为什么这是旅游网站的“后悔药”用户不再需要手动刷新页面看余位系统在库存变化瞬间毫秒级推送到所有打开该线路页的浏览器。我们上线后用户因“余位消失”导致的投诉下降 73%订单转化率提升 11.2%。这不是炫技而是把“信任感”具象化——让用户相信他看到的数字就是此刻真实的数字。我坚持在每个旅游项目里把 Channels 作为标配哪怕初期只用它推订单状态。因为用户不会记住你用了什么框架但会记住“我刚点提交手机就响了——订单已确认”。这种确定性是 Python Django 给旅游业务最扎实的交付承诺。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑