ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Django的旅游管理系统实战:从模型设计到部署全流程

基于Django的旅游管理系统实战:从模型设计到部署全流程 简介这是基于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_length50, verbose_name分类名称) description models.TextField(blankTrue, 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_length100, verbose_name景点名称) city models.CharField(max_length50, verbose_name所在城市) image models.ImageField(upload_tospots/%Y/%m/, blankTrue, verbose_name展示图片) description models.TextField(blankTrue, 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_length100, verbose_name线路名称) category models.ForeignKey(Category, on_deletemodels.CASCADE, related_nameroutes, verbose_name所属分类) spots models.ManyToManyField(Spot, related_nameroutes, verbose_name途经景点) start_city models.CharField(max_length50, verbose_name出发城市) days models.PositiveIntegerField(default1, verbose_name行程天数) price models.DecimalField(max_digits10, decimal_places2, verbose_name单人价格) stock models.PositiveIntegerField(default0, verbose_name剩余名额) created_at models.DateTimeField(auto_now_addTrue, 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。前面数据模型里有ImageFieldDjango 处理图片上传没有 Pillow 会直接报错。如果只想先把项目跑起来、图片字段以后再说可以先不装但后续一旦给景点上传图片就会踩坑。所以建议一步到位。Django 装好之后先新建项目再创建各个业务 appdjango-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_deletemodels.CASCADE。这里我专门拎出来讲因为这是初学者最容易忽略、但影响最恶劣的地方。on_delete的作用是当外键指向的那一行被删除时当前这一行该怎么处理。CASCADE是级联删除意思是管理员在后台删除一个分类所有属于这个分类的线路会被一并删除如果再往下级联线路关联的订单也会被删除。这在有些业务里是灾难。比如订单表关联线路如果线路被删了历史订单数据还存在这时候把Order.route上的外键设置成CASCADE就会导致管理员手滑删一条线路全部相关订单瞬间被清空而且没有任何提示。对于不可逆的删除操作我更推荐用PROTECT或SET_NULL# 订单表外键设计示例 route models.ForeignKey(Route, on_deletemodels.PROTECT, related_nameorders)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, flatTrue).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 数据展示中的查询优化前台首页要展示所有线路每一条线路又关联着多个景点。直接遍历线路然后逐个取景点的写法会产生严重的 N1 查询问题数据库连接次数等于“1 条列表查询 N 条关联查询”页面一卡就是几十次请求。Django 的解决方案就是prefetch_relateddef 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(usernameusername).exists(): return render(request, users/register.html, {error: 用户名已存在}) user User.objects.create_user(usernameusername, passwordpassword) 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, usernameusername, passwordpassword) 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, nameroute_list), path(route/int:pk/, views.route_detail, nameroute_detail), ]int:pk会把 URL 里的数字提取出来作为pk参数传给视图函数。视图里直接Route.objects.get(pkpk)就可以拿到对应的线路。这里建议用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_deletemodels.CASCADE, related_nameorders) route models.ForeignKey(Route, on_deletemodels.PROTECT, related_nameorders) travel_date models.DateField(verbose_name出发日期) num_people models.PositiveIntegerField(default1, verbose_name出行人数) total_price models.DecimalField(max_digits10, decimal_places2, verbose_name订单总价) status models.CharField(max_length20, choicesStatus.choices, defaultStatus.PENDING, verbose_name订单状态) created_at models.DateTimeField(auto_now_addTrue, verbose_name下单时间)订单提交视图中有一个细节值得强调计算总价时一定不要用前端传过来的金额而是从数据库中重新读取线路单价再算。# 注意total_price 必须由服务端计算 order Order.objects.create( userrequest.user, routeroute, travel_datetravel_date, num_peoplenum_people, total_priceroute.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(userrequest.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 图片上传 404Media 文件的静态服务坑开发环境里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_rootsettings.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__icontainskeyword) | Q(city__icontainskeyword)) return render(request, spots/spot_list.html, {spots: spots})icontains是大小写不敏感的模糊包含查询SQL 里面对应的是LIKE %keyword%。搜索功能看着小但对体验提升非常明显也是面试官比较喜欢的加分点。做的时候注意对keyword做空值处理否则搜索框什么都不填会查出全表数据。5.5 模板里的 forloop 和 empty 用法后台列表页里经常会遇到“当前分类下还没有线路”这种提示Django 模板引擎里有个很优雅的写法{% for route in routes %} div classcard{{ route.name }}/div {% empty %} p该分类下暂无线路请等待管理员更新。/p {% endfor %}{% empty %}会在routes为空时自动渲染避免了你先在视图里写if routes再写else的重复套路也让模板代码更清晰整洁。6. 部署小记与环境依赖管理项目开发完成之后本地跑通只是第一步。如果想部署到服务器上展示还要处理好依赖导出和 WSGI 配置。项目的第三方依赖我用以下命令生成requirements.txtpip freeze requirements.txt文件内容大致长这样Django4.2.7 pillow10.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 课程设计或练手项目不怕功能少怕的是功能之间逻辑不自洽。小步快跑、每一步都让数据链路完整闭环比堆砌一堆花哨功能要重要得多。希望这篇实战拆解能帮你少走一些我在开发时走过的弯路。本文还有配套的精品资源点击获取
返回列表