简介:这是基于Python与Django框架开发的旅游管理系统完整项目资源,适合Web开发初学者、毕业设计或课程设计使用者参考学习。系统围绕模型、视图、模板、URL路由、表单处理、权限认证、支付集成等Django核心知识点展开,涵盖目的地管理、旅行团展示、用户注册登录、在线预订等典型业务模块,能帮助读者理解一个综合型Web应用从数据建模到前后端交互的完整实现思路。压缩包共1412个文件,约114.28MB,以csv数据文件、js脚本、css样式、jpg/png图片、py与pyc程序文件、html页面等为主,并包含sqlite3数据库及多份Git对象文件,整体目录结构完整,便于按模块对照代码进行系统学习。目前已有154人浏览学习。通过该项目,读者可获得一套可运行的旅游管理网站源码、前端页面与后端逻辑实现,以及Django项目工程化的组织方式和实际部署参考,适合边阅读边调试,快速掌握基于Django的业务系统开发方法。
1. 项目概述:先看这个系统到底在解决什么问题
老实说,旅游管理系统这类项目在开发领域已经不算新鲜了,但正是因为它“麻雀虽小五脏俱全”,反而很适合用来练手。我这次做的这套系统,核心目标就一个:用 Python 生态里最成熟的 Web 框架 Django,把旅游业务中常见的线下人工流程搬到线上,让普通用户能查景点、看路线、下订单,让后台管理员能管内容、管订单、管用户,两边各干各的活,互不干扰。
如果你正准备做毕业设计,或者刚学完 Django 基础、想找一个能完整落地的实战项目,这篇文章值得你从头到尾看一遍。我会把系统从零搭建的思路、数据模型怎么设计、核心功能怎么一步步实现、以及实际开发中那些不踩一遍根本不会知道的坑,全部拆开来讲。
先说结论:用 Django 做这类管理系统,最大的优势不是“快”,而是“稳”。Django 自带 Admin 后台、ORM 数据库操作、表单校验、认证系统,这些全是现成的轮子,省下的时间足够你专注去打磨业务逻辑。但轮子多也意味着坑多,如果从一开始没想清楚数据模型和模块边界,写到后面大概率要返工。我这次就把整个设计过程还原出来,你按着走能少走不少弯路。
2. 整体设计与思路拆解
2.1 需求分析:功能边界比技术选型更重要
动手写代码之前,必须先想清楚一个问题:这个系统到底给谁用?我接触到不少初学者,一说“旅游管理系统”就直接开写用户登录、景点展示、订单支付,结果功能堆了一堆,每个模块都只写了半截,最后连自己都说不清楚系统的核心流程是什么。
我自己的习惯是先画一张简单的角色和功能对照表,把所有参与者和他们需要做的事情理清楚,再开始设计表结构。这套旅游系统我拆成了两类角色:
| 角色 | 核心需求 | 涉及功能模块 |
|---|---|---|
| 普通游客 | 浏览景点信息、查看旅游线路、提交预订订单 | 首页展示、景点列表、线路详情、订单提交、订单查询 |
| 后台管理员 | 维护景点资料、管理线路分类、处理订单状态 | Admin后台、数据管理、订单状态更新 |
注意,我在这里刻意没有做“用户注册登录”之外的复杂权限体系,也没有接入真实支付。原因很简单:对于一门课设或练手项目,过度设计是最大的时间杀手。Django 自带的auth模块已经提供了用户表和会话管理,我用它来做游客的注册、登录、身份识别,完全够用。等这套流程跑通了,后面要加角色权限、加支付接口,都是往这张表上做增量,而不是重构地基。
2.2 为什么是 Django:框架选型的现实考量
现在 Python 里做 Web 开发的框架不少,FastAPI、Flask、Tornado 各有拥趸。但就“旅游管理系统”这种传统业务为主的场景,我首选还是 Django。
三个理由,每一句都是实际对比后的感受:
第一,Django 自带 Admin 后台。旅游管理系统的后台管理需求非常典型:景点要增删改查,订单要状态变更,线路要上下架。Django 的 Admin 只需要你写几行admin.site.register的代码,就能把对应数据表的管理页面全部生成出来,连界面都不用自己画。用 Flask 的话,这几个后端页面全得手写,工作量翻一倍不止。
第二,Django 的 ORM 对复杂查询的支持很顺手。旅游业务里有一个高频需求:用户在前台选了一条线路,系统要同时关联出线路下的景点、出发城市、价格、余位。这类跨表查询在 Django ORM 里就是select_related和prefetch_related的事,写出来的查询代码既直观又不容易出 SQL 注入的问题。
第三,Django 的项目结构天生清晰。app按业务模块拆分,一个景点模块一个 app,一个订单模块一个 app,文件组织就是一张最直白的设计图。多维护几个人、隔几个月再回来看,你依然能快速找到每一段业务代码在哪。
当然,Django 的缺点也得认:启动慢、模板语法不够灵活、异步支持需要额外配。但在这个项目规模下,这些缺点完全不影响最终体验,所以结论很明确:选 Django,踏实。
2.3 数据模型设计:表关系就是业务逻辑的缩影
设计数据库表的时候,我遵循一个原则:让表之间的关联关系能直接反映业务流转的规则,而不是硬塞一堆万能字段。
这套系统的核心表我一共设计了五张,外加 Django 默认的用户表:
Category:线路分类表,比如“周边游”“国内长线”“出境游”,冗余不多,想加字段随时再加;Spot:景点信息表,存储景点名称、所在城市、简介、封面图片、所属分类,是整张系统的“货架”;Route:旅游线路表,关联出发城市、途经景点(多对多关系)、天数、价格、余位等;Order:订单表,关联用户和线路,记录下单时间、出发时间、人数、总价、订单状态;Review:用户评论表,关联用户和线路,用来做基础的口碑展示。
这里面最有意思的是Route和Spot的多对多关系。一条旅游线路通常会覆盖多个景点,反过来一个景点也可以出现在多条线路里,所以中间加了一张关联表,Django 里直接用ManyToManyField就能自动生成。我在设计的时候特意给这张中间表留了手动扩展的余地,后续如果你想让“某个景点在某条线路中的排序”可配置,只需要把through参数指定到一张自定义表就行。
下面是一段简化的模型代码示例,展示核心字段的实现思路:
from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=50, verbose_name="分类名称") description = models.TextField(blank=True, verbose_name="分类描述") class Meta: verbose_name = "线路分类" verbose_name_plural = verbose_name def __str__(self): return self.name class Spot(models.Model): name = models.CharField(max_length=100, verbose_name="景点名称") city = models.CharField(max_length=50, verbose_name="所在城市") image = models.ImageField(upload_to="spots/%Y/%m/", blank=True, verbose_name="展示图片") description = models.TextField(blank=True, verbose_name="景点简介") class Meta: verbose_name = "景点" verbose_name_plural = verbose_name def __str__(self): return self.name class Route(models.Model): name = models.CharField(max_length=100, verbose_name="线路名称") category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="routes", verbose_name="所属分类") spots = models.ManyToManyField(Spot, related_name="routes", verbose_name="途经景点") start_city = models.CharField(max_length=50, verbose_name="出发城市") days = models.PositiveIntegerField(default=1, verbose_name="行程天数") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单人价格") stock = models.PositiveIntegerField(default=0, verbose_name="剩余名额") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: verbose_name = "旅游线路" verbose_name_plural = verbose_name def __str__(self): return self.name表设计完成之后,一定要回头验证一件事:从用户下单到管理员处理订单,这些表之间能不能用一条又一条的关联串成完整的链路?我下单,Order里有用户外键和线路外键,管理员一进 Admin 后台就能看到“谁、在什么时间、订了哪条线路、付了多少钱、现在什么状态”。如果链路中间断了一环,比如订单表里没有留下线路快照,那用户后来改价或者线路下架,历史订单就全乱套了。这种细节,才是设计功力真正见分晓的地方。
3. 核心细节解析与实操要点
3.1 环境准备:这一关是新手最容易放弃的地方
很多人卡在项目启动前,根本不是代码问题,而是 Python 环境一团乱。我先说一套我目前用得最顺的环境搭建步骤:
先装 Python。Windows 用户到 Python 官网下载 3.10 或 3.11 版本,安装时务必勾选“Add Python to PATH”,这一步漏了后面python命令全是不识别的。装好之后打开命令行,用python --version验证一下。
然后用虚拟环境隔离项目依赖。这一步很多人会跳过,我强烈不建议。Django 项目跑起来依赖一堆第三方包,如果不做环境隔离,等同时维护两三个项目的时候,包版本冲突能把人逼疯。我的习惯是:
# 创建虚拟环境,vtenv 是环境目录名,可以随意换 python -m venv vtenv # Windows 激活虚拟环境 vtenv\Scripts\activate # macOS / Linux 激活虚拟环境 source vtenv/bin/activate # 安装 Django,我用的是 4.x 稳定版本 pip install django pillow这里解释一下为什么额外装pillow。前面数据模型里有ImageField,Django 处理图片上传没有 Pillow 会直接报错。如果只想先把项目跑起来、图片字段以后再说,可以先不装,但后续一旦给景点上传图片就会踩坑。所以建议一步到位。
Django 装好之后,先新建项目,再创建各个业务 app:
django-admin startproject travel_project cd travel_project python manage.py startapp spots python manage.py startapp routes python manage.py startapp orders python manage.py startapp users很多人会困惑一个项目里到底建几个 app 合适。我的判断标准很简单:这个模块在未来的系统里是否会被独立复用到其他项目?spots和routes能独立成 app,是因为景点库和线路库本身有独立的业务价值;users更多是为了扩展用户资料,其实直接用 Django 内置的auth也行;orders则是核心业务流之一。总之,宁可 app 多一些、各自职责纯粹,也不要一个 app 里塞一堆互不相干的 model。
3.2 数据模型的坑:外键的 on_delete 选错会让数据一夜蒸发
在 2.3 节的模型示例里,已经出现了on_delete=models.CASCADE。这里我专门拎出来讲,因为这是初学者最容易忽略、但影响最恶劣的地方。
on_delete的作用是:当外键指向的那一行被删除时,当前这一行该怎么处理。CASCADE是级联删除,意思是管理员在后台删除一个分类,所有属于这个分类的线路会被一并删除;如果再往下级联,线路关联的订单也会被删除。这在有些业务里是灾难。
比如订单表关联线路,如果线路被删了,历史订单数据还存在,这时候把Order.route上的外键设置成CASCADE,就会导致管理员手滑删一条线路,全部相关订单瞬间被清空,而且没有任何提示。对于不可逆的删除操作,我更推荐用PROTECT或SET_NULL:
# 订单表外键设计示例 route = models.ForeignKey(Route, on_delete=models.PROTECT, related_name="orders")PROTECT表示如果有订单引用着这条线路,删除线路时 Django 会直接抛一个ProtectedError,阻止删除操作,强制你先把关联关系处理干净。对于订单这类重要业务数据,这是最安全的兜底方案。
3.3 Admin 后台配置:业务管理的核心阵地
Django Admin 是这套系统管理端的灵魂。景点、线路、分类这些数据,前台页面的展示只是一层皮,真正的维护入口全在 Admin 里。
注册模型很简单:
from django.contrib import admin from .models import Category, Spot @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ("id", "name", "description") @admin.register(Spot) class SpotAdmin(admin.ModelAdmin): list_display = ("id", "name", "city", "category_name") list_filter = ("city", "routes__category") search_fields = ("name", "city") def category_name(self, obj): categories = obj.routes.values_list("category__name", flat=True).distinct() return " / ".join(categories) category_name.short_description = "关联分类"上面这段代码里有个细节:Spot本身没有分类字段,分类是挂在Route上的。如果我想在景点列表里直接看到这个景点属于什么分类,就得上category_name这个自定义方法,通过反向关系去查。这个用法很常见,建议收藏。
list_filter里的routes__category用了跨表过滤,在 Django Admin 里是双下划线查关联模型字段的标准写法。搜索、过滤、排序一配置好,后台的数据管理体验就能接近一个小型内容管理系统。
3.4 前台页面的渲染思路:模板继承和模板标签
前台页面我用的是 Django 自带模板系统,没有上 Vue 或 React。原因还是那个:这类管理系统的核心价值在数据流转,不是交互动画。服务端渲染对 SEO 更友好,开发调试也更直观。
如果你希望页面更生动一些,可以稍微引入一点前端库,但千万不要做成前后端分离。前后端分离意味着要同时维护两套代码、处理跨域、约定接口格式,对这套系统的体量来说是纯粹的负担。
模板文件组织上,我用了一个base.html作为全局母版,顶部是导航栏,底部是版权信息,中间用{% block content %}{% endblock %}留出子页面填充区域。每个功能页面对应一个模板文件,继承母版后只写自己独有的部分。一整套路面的 UI 框架走 Bootstrap 5 的 CDN,足够覆盖列表、卡片、表格、表单这些常见组件。
3.5 数据展示中的查询优化
前台首页要展示所有线路,每一条线路又关联着多个景点。直接遍历线路然后逐个取景点的写法,会产生严重的 N+1 查询问题:数据库连接次数等于“1 条列表查询 + N 条关联查询”,页面一卡就是几十次请求。
Django 的解决方案就是prefetch_related:
def route_list(request): routes = Route.objects.prefetch_related("spots").select_related("category").all() return render(request, "routes/route_list.html", {"routes": routes})prefetch_related会把所有关联景点一次性查出来,再在 Python 内存里做关联;select_related则是连表查询,适合处理一对一和一对一外键场景。这一行代码能减少的数据库查询次数,往往能让页面响应时间从一秒级降到几十毫秒级。
4. 实操过程与核心功能实现
4.1 注册与登录:Django 内置认证模块的快速接入
用户模块我是直接基于 Django 的auth应用改的。它已经提供好了用户表User,字段包括用户名、密码、邮箱、姓名等,密码也是哈希存储。我需要做的就是写几个视图和模板,把登录、注册、登出这三个动作接到业务里去。
注册视图的核心逻辑:
from django.shortcuts import render, redirect from django.contrib.auth.models import User from django.contrib.auth import login, authenticate, logout def register_view(request): if request.method == "POST": username = request.POST.get("username").strip() password = request.POST.get("password") confirm_password = request.POST.get("confirm_password") if not username or not password: return render(request, "users/register.html", {"error": "用户名和密码不能为空"}) if password != confirm_password: return render(request, "users/register.html", {"error": "两次输入的密码不一致"}) if User.objects.filter(username=username).exists(): return render(request, "users/register.html", {"error": "用户名已存在"}) user = User.objects.create_user(username=username, password=password) login(request, user) return redirect("route_list") return render(request, "users/register.html")create_user这个方法是User模型自带的,它会自动把明文密码转换成哈希值存储,千万不要直接用create或者手动给password字段赋值,否则后期登录校验一定会出问题。
登录视图更简单:
def login_view(request): if request.method == "POST": username = request.POST.get("username") password = request.POST.get("password") user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect("route_list") return render(request, "users/login.html", {"error": "用户名或密码错误"}) return render(request, "users/login.html")4.2 路线列表与景点详情页
前台首页我做了两个核心展示区域:最上面是热门线路推荐,下面是全部线路列表。每条线路卡片上展示线路名称、所属分类、价格、已售/余位数量。点进去能看到线路详情页,包含途经景点列表和预订入口。
URL 配置的核心是动态参数:
from django.urls import path from . import views urlpatterns = [ path("", views.route_list, name="route_list"), path("route/<int:pk>/", views.route_detail, name="route_detail"), ]<int:pk>会把 URL 里的数字提取出来,作为pk参数传给视图函数。视图里直接Route.objects.get(pk=pk)就可以拿到对应的线路。这里建议用get_object_or_404,查不到时自动返回 404 页面,省得自己写异常处理。
4.3 订单提交与状态管理
订单模块是整个系统业务上最重要的一环。用户在线路详情页选择出发日期和出行人数,提交之后生成订单。提交前先检查两件事:库存是否充足、用户是否已登录。
订单状态我用了一个整数字段加 Choices 枚举,而不是直接在代码里散落各种字符串:
class Order(models.Model): class Status(models.TextChoices): PENDING = "pending", "待支付" PAID = "paid", "已支付" CANCELLED = "cancelled", "已取消" user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="orders") route = models.ForeignKey(Route, on_delete=models.PROTECT, related_name="orders") travel_date = models.DateField(verbose_name="出发日期") num_people = models.PositiveIntegerField(default=1, verbose_name="出行人数") total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价") status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING, verbose_name="订单状态") created_at = models.DateTimeField(auto_now_add=True, verbose_name="下单时间")订单提交视图中,有一个细节值得强调:计算总价时一定不要用前端传过来的金额,而是从数据库中重新读取线路单价再算。
# 注意:total_price 必须由服务端计算 order = Order.objects.create( user=request.user, route=route, travel_date=travel_date, num_people=num_people, total_price=route.price * num_people, )原因很简单,前端的数据可以随意伪造。如果不重新查库,用户把单价改成 0.01 再提交,订单总价就会失真,后面核对账目时根本无从追溯。
库存扣减我这里做了最简单的方案:下单时检查route.stock >= num_people,满足条件就route.stock -= num_people后保存。真实生产环境要考虑并发超卖问题,可以通过数据库的select_for_update()行级锁或事务来解决,但在课程设计级别,先把流程跑通,再谈并发安全。
4.4 用户订单查询页面
登录用户查看“我的订单”时,只需要按request.user过滤当前用户下的订单:
def my_orders(request): orders = ( Order.objects.filter(user=request.user) .select_related("route") .order_by("-created_at") ) return render(request, "orders/my_orders.html", {"orders": orders})模板里遍历订单列表,每个订单展示线路名称、出行日期、人数、总价、状态。这里的状态字段是英文枚举值,显示上我做了映射:order.get_status_display(),Django 会自动把枚举值转换成可读的中文标签,不用自己写if判断。
5. 常见问题与排查技巧实录
5.1 迁移报错:为什么每次修改模型都要执行 makemigrations
开发过程中我改了几次模型字段,比如给Route加了start_city,给Order加了travel_date。每次改完模型,必须依次执行:
python manage.py makemigrations python manage.py migratemakemigrations会根据模型变化生成迁移脚本,migrate才真正把改动同步到数据库。很多人只执行了makemigrations就以为完事了,结果重启服务时报“column not found”的错,原因就在这。
如果迁移时报“No changes detected”,先确认INSTALLED_APPS里有没有把对应的 app 加进去。
5.2 图片上传 404:Media 文件的静态服务坑
开发环境里,ImageField上传的图片默认存放在项目的media目录,但 Django 默认不提供 media 目录的静态访问。我的做法是在项目的urls.py里加一段:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ...所有 url ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)然后在settings.py里配置:
MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media"这一套配完,页面上{{ route.image.url }}才能正常加载图片。如果忘记这段,模板里图片地址会生成/media/xxx.jpg,但服务器会返回 404,而且后台没有任何报错,排查起来最隐蔽。
5.3 登录校验的两种实现方式
如果某些页面必须登录才能访问,直接在视图函数顶部加@login_required装饰器最简单:
from django.contrib.auth.decorators import login_required @login_required(login_url="/users/login/") def my_orders(request): ...这样未登录用户会被自动重定向到登录页,登录成功后继续跳回原访问页面。不过要注意,如果login_url不写,Django 会默认跳到/accounts/login/,而你的登录页实际在/users/login/,匹配不上就会报 404,这是新手常见问题之一。
5.4 搜索功能怎么做
景区列表页加一个搜索框是很常见的需求。我用的是 Django 的Q对象,实现按名称和城市的模糊匹配:
from django.db.models import Q def spot_search(request): keyword = request.GET.get("keyword", "") spots = Spot.objects.filter(Q(name__icontains=keyword) | Q(city__icontains=keyword)) return render(request, "spots/spot_list.html", {"spots": spots})icontains是大小写不敏感的模糊包含查询,SQL 里面对应的是LIKE '%keyword%'。搜索功能看着小,但对体验提升非常明显,也是面试官比较喜欢的加分点。做的时候注意对keyword做空值处理,否则搜索框什么都不填,会查出全表数据。
5.5 模板里的 forloop 和 empty 用法
后台列表页里经常会遇到“当前分类下还没有线路”这种提示,Django 模板引擎里有个很优雅的写法:
{% for route in routes %} <div class="card">{{ route.name }}</div> {% empty %} <p>该分类下暂无线路,请等待管理员更新。</p> {% endfor %}{% empty %}会在routes为空时自动渲染,避免了你先在视图里写if routes再写else的重复套路,也让模板代码更清晰整洁。
6. 部署小记与环境依赖管理
项目开发完成之后,本地跑通只是第一步。如果想部署到服务器上展示,还要处理好依赖导出和 WSGI 配置。
项目的第三方依赖,我用以下命令生成requirements.txt:
pip freeze > requirements.txt文件内容大致长这样:
Django==4.2.7 pillow==10.1.0换到新环境后,执行pip install -r requirements.txt就能一键装齐依赖。这里提醒一点:pip freeze会把当前环境的所有包都列出来,如果你在虚拟环境里装了一些和项目无关的包,它们也会被一并写进文件里。讲究一点的话,可以用pipreqs生成只包含项目真实依赖的清单,但课程设计/练手项目不必那么较真。
部署层面,我选择的是用gunicorn搭配 Nginx 反向代理的方式:Nginx 管静态文件和反向代理,gunicorn 跑 Django 应用。首次部署时踩的最多的坑是ALLOWED_HOSTS没有修改,默认只允许本地访问,部署到服务器上访问域名会直接报 DisallowedHost。改成这样就能解决问题:
ALLOWED_HOSTS = ["*"]生产环境这样写有安全风险,但演示项目和课程设计阶段,图省事就直接放开;如果想要安全,就只写自己服务器的 IP 或域名。
7. 后续扩展方向与我的个人体会
这套系统做完之后,如果想继续提升,我个人觉得有几个方向值得尝试:给系统加上基于 Redis 的缓存,把首页和线路详情这种高频访问页面的查询结果扔进缓存,性能能明显提升;给订单模块接入真实的第三方支付模拟接口,将订单状态机跑完整,从待支付到已支付再到退款关闭,这个流程可以让你对状态机设计有更深理解;把前后端分离提上日程,用 Django REST Framework 把后端 API 化,前端再用 Vue 或 React 重写一层,这套系统就能进化成真正的生产级架构。
最后再分享一点实操中的个人心得体会。这个项目让我最受益的地方,不是最后功能都跑通了,而是在跑通的过程中想明白了一件事:代码的数量从来不是项目难度的核心,如何让数据和数据之间的关系始终清晰、可控、不随业务迭代而腐烂,才是系统设计的永恒课题。Django 给了你 Admin、ORM、Model 这些工具,但工具只是地基,真正决定系统能走多远的是你如何规划模块、设计字段、处理异常。
如果你现在也正在做一个 Django 课程设计或练手项目,不怕功能少,怕的是功能之间逻辑不自洽。小步快跑、每一步都让数据链路完整闭环,比堆砌一堆花哨功能要重要得多。希望这篇实战拆解能帮你少走一些我在开发时走过的弯路。
本文还有配套的精品资源,点击获取