基于Django的旅游信息管理系统:从源码到部署的完整实战

发布时间:2026/9/13 15:44:47
基于Django的旅游信息管理系统:从源码到部署的完整实战
简介基于Django的旅游信息管理系统源码是一份面向毕业设计及Python Web初学者的完整项目。系统覆盖用户注册登录与权限管理、景点信息维护、线路规划、预订服务、评论评分及后台管理等典型模块贯穿Django的MVT架构、ORM数据库操作、表单处理与安全防护机制适合理解企业级Web应用的开发流程与工程组织方式。资源包共788个文件大小18.41MB以45个Python源码、53个Vue组件、164个JavaScript脚本及53个CSS样式为主同时包含HTML页面、SQL数据库脚本、图片图标素材与一键启动批处理文件目录结构清晰。已有159人学习下载。借助源码可掌握Django与Vue前后端协同开发的思路学习通过Admin后台管理数据、处理用户会话与权限并参考内置数据库脚本和启动命令快速搭建运行环境为独立完成毕业设计或进阶Django开发提供扎实的实践参考。1. 旅游信息管理系统源码难点从来不在增删改查打开任何一个软件下载站输入Django 旅游信息管理系统能搜出几十个版本差不多的源码包。它们都叫这个名字功能列表也都写着景点管理、线路推荐、订单预订、后台管理。真正拉开差距的是这套源码能不能在你本机跑起来、能不能换数据库、能不能改造成自己想要的业务模型。多数人下载后卡在第一步依赖装不上、编码报错、admin后台样式丢失、静态文件404。这个标题里最有价值的不是旅游信息管理六个字而是Python和Django的组合方式。旅游信息管理系统是一个典型的中量级Web业务系统它同时涉及多表关联查询、文件上传、后台权限管理、前台模板渲染恰好覆盖了Django框架最常用、面试最常问的能力面。对有3年以上Python经验的人来说这套系统是理解Django ORM与Admin机制的最佳样本对刚入行的开发者它是能把会Python语法转化成会做Web项目的完整链路。2. 从源码包到能跑Django项目结构与本地最小启动2.1 拿到压缩包后先看目录结构再动手源码zip解压后第一件事不是急着配数据库而是先花两分钟看目录长什么样。一个标准的Django项目哪怕是别人打包的也躲不开这几样东西manage.py、项目配置目录通常叫config或与项目同名、业务应用目录apps或直接平铺的scenic、hotel、order之类、模板目录templates、静态文件目录static以及依赖清单文件requirements.txt。unzip tourism_django.zip cd tourism_django ls -la ├── manage.py ├── requirements.txt ├── config/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── scenic/ │ ├── routes/ │ └── order/ ├── templates/ └── static/这段命令先解压再列目录。manage.py是所有Django管理命令的入口无论是启动开发服务器、执行数据库迁移还是创建超级管理员都要通过它。config/settings.py是整个项目的总开关数据库、安装的应用、模板路径、语言时区都在这一个文件里集中配置。apps目录下按业务拆分的子应用每条业务线有自己的models.py、views.py、urls.py这是Django推荐的分层方式——按业务域划分而不是按技术层划分。2.2 虚拟环境、Python版本与依赖安装的连带坑这一步踩坑率极高。很多人直接pip install -r requirements.txt结果报错一大片。原因通常是Python版本与Django版本不匹配或者项目依赖了mysqlclient这类需要系统编译库的包。我一般会先确认Python版本建议用3.8到3.11之间的版本太老或太新都会在某个包上出问题。然后创建虚拟环境python3.10 -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txtvenv是Python自带的虚拟环境工具python3.10指定解释器版本source venv/bin/activate把当前shell切换到虚拟环境中。后续所有pip安装的包都落在venv目录内部不会污染系统Python。upgrade pip先升级安装器本身能减少很多莫名其妙的is not a supported wheel错误。如果requirements.txt里锁了Django 3.2 LTS那Python 3.10完全兼容如果锁的是Django 4.xPython 3.8以上也能跑。2.3 settings.py里必须核对的三类配置项依赖装完先别急着runserver。打开settings.py核对三个地方# config/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, apps.scenic, apps.routes, apps.order, ] DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai第一项是INSTALLED_APPS写完业务应用后必须在这里登记否则Django完全不知道apps.scenic存在makemigrations时也找不到对应模型。第二项是数据库连接源码包默认用SQLite好处是零配置、文件型数据库适合本地起步。第三项是语言和时区LANGUAGE_CODE设为zh-hans才能让admin后台显示中文TIME_ZONE不设成Asia/Shanghai的话DateTimeField的自动写入时间会跟北京时间差8小时。2.4 迁移、创建管理员、启动一条链配置核对完毕执行以下命令完成数据库初始化和登录账户创建python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000makemigrations会扫描INSTALLED_APPS中每个应用的models.py把模型类翻译成迁移文件相当于生成一张待执行的数据库变更清单。migrate才是真正把变更清单落到数据库里的动作包括Django自带的用户表、权限表也会在这一步创建。createsuperuser交互式地让你输入用户名、邮箱、密码这是进入admin后台的凭证。runserver 0.0.0.0:8000中0.0.0.0表示监听所有网卡允许局域网其他机器访问开发服务器默认为127.0.0.1时只能本机访问。启动成功后访问http://127.0.0.1:8000/admin能看到后台登录页。这里能登录说明项目骨架已经活了。接下来的一切都是在验证这个骨架里长了哪些器官。3. 核心模型与Admin后台景点、线路、订单的数据层设计3.1 Django ORM模型设计一张表就是一个Python类旅游信息管理系统的数据模型不外乎几个核心业务对象景点ScenicSpot、旅游线路TourRoute、酒店Hotel、下单记录Order。它们之间的关系很典型一条线路包含多个景点一个用户可以下多笔订单。这种一对多与多对多的组合是Django ORM设计能力的分水岭。# apps/scenic/models.py from django.db import models from django.contrib.auth.models import User class ScenicSpot(models.Model): name models.CharField(景点名称, max_length128) city models.CharField(所在城市, max_length64) level models.CharField(景区等级, max_length16, choices[ (A, A级), (AA, AA级), (AAA, AAA级), (AAAA, AAAA级), (AAAAA, AAAAA级), ], defaultA) price models.DecimalField(门票价格, max_digits10, decimal_places2) img models.ImageField(景点图片, upload_tospot/, blankTrue) detail models.TextField(景点介绍, blankTrue) class Meta: db_table scenic_spot def __str__(self): return self.name class TourRoute(models.Model): name models.CharField(线路名称, max_length128) days models.PositiveIntegerField(行程天数) spots models.ManyToManyField(ScenicSpot, related_nameroutes, verbose_name包含景点) price models.DecimalField(线路价格, max_digits10, decimal_places2) class Meta: db_table tour_route class Order(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameorders, verbose_name下单用户) route models.ForeignKey(TourRoute, on_deletemodels.PROTECT, verbose_name预订线路) num models.PositiveIntegerField(预订人数, default1) status models.CharField(订单状态, max_length16, choices[ (unpaid, 待支付), (paid, 已支付), (canceled, 已取消), ], defaultunpaid) created_at models.DateTimeField(下单时间, auto_now_addTrue) class Meta: db_table order这里的每个字段都有讲究。DecimalField用于价格而不是FloatField是因为浮点数做金额计算会产生精度误差max_digits10, decimal_places2表示最多10位数字且保留2位小数足够覆盖万元级票价和线路价。ManyToManyField自动生成中间关联表related_nameroutes让ScenicSpot对象可以通过.routes.all()反查出包含该景点的所有线路。on_deletemodels.CASCADE在用户删除时级联删掉他的订单符合业务语义models.PROTECT用在订单和线路上防止删掉一条仍有订单关联的线路这是受保护删除策略。3.2 Django Admin界面美化list_display、list_filter与search_fieldsadmin后台是这套源码里看得见的部分。默认注册模型后后台列表页只显示__str__的返回值信息量不足。注册并配置展示字段是每个人拿到源码后必做的第一项定制# apps/scenic/admin.py from django.contrib import admin from .models import ScenicSpot, TourRoute, Order admin.register(ScenicSpot) class ScenicSpotAdmin(admin.ModelAdmin): list_display (id, name, city, level, price) list_filter (city, level) search_fields (name, city) list_editable (price,) list_per_page 20 admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (id, user, route, num, status, created_at) list_filter (status, created_at) search_fields (user__username, route__name)list_display把元组里的字段各渲染成一列list_filter会在右侧生成按城市和等级过滤的筛选面板search_fields指定搜索框匹配的字段。注意user__username这种写法双下划线在Django ORM里表示跨表查询即通过Order的user外键去匹配User.username字段。list_editable可以直接在列表页修改价格。但有一个限制list_editable的字段不能同时出现在list_display里作为第一个字段因为第一个字段默认被用作修改入口链接。原因是Django的admin会为第一个列生成指向编辑页的a标签若该列也可编辑链接与输入框会冲突。3.3 执行查询与删除对象双下划线和非级联的边界Django学科里objects.all()和get()谁都会用真正容易出错的是带条件的删除和跨表过滤# apps/scenic/views.py 示例 orders Order.objects.filter( user__usernametestuser, statuspaid ) deleted, _ Order.objects.filter(statuscanceled).delete()filter(user__usernametestuser)通过外键链访问用户表并精确匹配用户名等价于SQL里的JOIN加上WHERE。.delete()不是所有情况下都返回删除条数deleted返回一个二元组第一项是删除总条数。注意on_deletemodels.CASCADE会让Django递归删除关联记录而PROTECT会直接抛出ProtectedError异常。这对不同业务语义的模型做不同配置是模型层设计成熟与否的重要标志。4. 前台浏览与业务闭环URLconf、视图、模板的联动实现4.1 URLconf里的反向解析url模板标签与reverse函数前台页面加载出来的过程是浏览器请求一个URLDjango根据urls.py里的路由规则把请求交给对应的视图函数视图返回渲染后的HTML。这套机制里path中新增的参数和name命名是双向绑定的关键# config/urls.py from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(scenic/, include(apps.scenic.urls)), ]# apps/scenic/urls.py from django.urls import path from . import views app_name scenic urlpatterns [ path(, views.scenic_list, namescenic_list), path(detail/int:pk/, views.scenic_detail, namescenic_detail), ]app_name scenic把当前应用的URL命名空间隔离出来避免多个应用出现同名路由时冲突。int:pk是路径转换器只匹配纯数字并把值以整数类型传给视图函数。这样设计后模板里可以用{% url scenic:scenic_detail spot.id %}生成链接视图里用reverse(scenic:scenic_detail, args[spot.id])重定向即使以后改了URL路径只要name不变所有引用处自动适配。4.2 视图与模板的配合分页、搜索与图片展示列表页要解决的核心问题是景点多怎么办。一次性渲染全部景点数据量上来了页面就卡。Django里最常用的方案是内置的Paginator# apps/scenic/views.py from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from django.shortcuts import render from .models import ScenicSpot def scenic_list(request): query request.GET.get(q, ).strip() spots_all ScenicSpot.objects.all().order_by(id) if query: spots_all spots_all.filter( models.Q(name__icontainsquery) | models.Q(city__icontainsquery) ) paginator Paginator(spots_all, 9) page_num request.GET.get(page, 1) try: page_obj paginator.page(page_num) except PageNotAnInteger: page_obj paginator.page(1) except EmptyPage: page_obj paginator.page(paginator.num_pages) return render(request, scenic/list.html, { page_obj: page_obj, query: query, })这段逻辑分三步走。用户提交搜索词后icontains实现了大小写不敏感的包含匹配models.Q用|表示OR条件两个字段有任何一个命中就返回。Paginator(spots_all, 9)把查询集切片成每页9条page_obj本身就是包含当前页数据的对象。页面渲染时模板里用{% for spot in page_obj %}遍历数据用{% if page_obj.has_previous %}控制上一页按钮的显隐。EmptyPage捕获用户手动把page999的情况回落到最后一页避免直接抛404。搜索框和分页按钮的组合是这类前台页面的标配核心思路是查询集不急着求值等Paginator切完才真正执行SQL。Queryset是惰性的这为分页和组合过滤提供了天然的性能优势也让过滤条件能够串联叠加而不产生多个查询。4.3 权限与操作闭环登录后才能下单订单模块把未登录用户和已登录用户区分开Django用装饰器就能完成这个控制from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404 login_required def create_order(request, route_id): if request.method POST: route get_object_or_404(TourRoute, idroute_id) num int(request.POST.get(num, 1)) Order.objects.create( userrequest.user, routeroute, numnum, ) return redirect(route:route_list) return render(request, order/create.html, {route: get_object_or_404(TourRoute, idroute_id)})login_required默认行为是重定向到登录页登录后在request.user上保留用户对象。get_object_or_404是objects.get()的安全版本查不到时抛404而不是DoesNotExist异常。Order.objects.create()一步完成实例化和保存比自己save()少一行。这里的核心业务逻辑是把登录用户选定线路预订人数三项拼装成订单记录。前台浏览、下单、后台管理、订单状态变更整套系统就是由列表页查询 → 详情页展示 → 带权限提交订单 → admin处理订单状态这个闭环串起来的。Django把这条链路拆解成URL路由、视图函数、模板、ORM四次跳转每个跳转点都能独立测试也是这套源码最值得学习的地方。5. 静态文件丢失、数据库切换与宝塔部署的三个收尾技巧5.1 切MySQL数据库mysqlclient的安装与utf8mb4源码默认SQLite但生产环境通常换MySQL。改动集中在settings.py的DATABASES配置以及一套需要额外安装的驱动DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: tour_db, USER: tour_user, PASSWORD: 你的强密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }换驱动时提到mysqlclient很多人在Linux上装这个包会直接报错因为缺少mysql_config和编译头文件。Ubuntu系统的解决方式sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pip install mysqlclientdefault-libmysqlclient-dev提供了连接MySQL的C客户端库build-essential提供了gcc编译器。mysqlclient是一个C扩展包不是纯Python实现所以编译依赖比pymysql重得多。如果项目里确实用pymysql在__init__.py写入import pymysql; pymysql.install_as_MySQLdb()也可以但性能与兼容性上多数项目仍青睐mysqlclient。切换后务必执行python manage.py migrate重建表结构SQLite生成的db.sqlite3文件无法直接搬进MySQL。5.2 admin后台样式丢失STATIC_ROOT与collectstatic跑runserver时admin后台样式正常一旦改用Nginx提供服务后台就剩一堆纯文本链接。这是静态文件收集环节没做。Django在开发阶段由runserver自动服务静态文件生产模式下需要统一收集并交给Nginx托管python manage.py collectstaticcollectstatic把所有应用包括admin自带的static目录里的静态文件复制到STATIC_ROOT指定的目录。配置上要同时设好三件套STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / mediaMEDIA_ROOT与MEDIA_URL是用户上传文件的存放位置和访问前缀给ImageField上传的图片用。Nginx配置里把/static/指向staticfiles目录把/media/指向media目录即可。很多源码包里的图片是ImageField的外链或者本地占位图部署时忘记设置MEDIA_ROOT的话前台图片会全部裂开。5.3 宝塔部署DjangoGunicorn Nginx的转发关系宝塔面板等于把Nginx配置、Python环境、进程守护可视化。部署Django的常规链路是Gunicorn作为Python应用服务器运行wsgi应用Nginx反向代理接收外部请求并转发给Gunicorn。用gunicorn启动项目cd /www/wwwroot/tourism_django source venv/bin/activate gunicorn config.wsgi:application --bind 127.0.0.1:8001 --workers 3config.wsgi:application指到项目里config/wsgi.py中的application对象。--bind只在本地端口监听不直接暴露公网--workers按照CPU核心数一般设为2*核数1。随后宝塔的Nginx站点配置里把server_name和proxy_pass串起来location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /www/wwwroot/tourism_django/staticfiles/; }proxy_set_header X-Forwarded-For比较关键Django的request.META[REMOTE_ADDR]在反代模式下拿到的是Nginx的地址不是真实访客IP这些请求头补齐之后视图里才能正确记录日志和调试。测试命令直接跑curl -I http://127.0.0.1:8001看返回头是不是200排除Gunicorn的独立故障后再检查Nginx。5.4 图片与周边资源的绝对化处理最后补一个小技巧settings里增加MEDIA相关的模板标签使用模板中图片的地址应当写为{{ spot.img.url }}而不是{{ spot.img}}ImageField.url会在前面拼接上MEDIA_URL避免图片相对路径导致页面上全部404。这套源码的完整度往往就体现在这些细节里。从本地SQLite跑通到换MySQL再到Nginx服务静态文件和Gunicorn处理动态请求三步走完一个能出门演示基于Django的旅游信息管理系统才算真正落地。本文还有配套的精品资源点击获取