
简介这是一套基于Python与Django框架实现的旅游自主网站完整项目源码面向希望入门或进阶Web开发的学习者可作为毕业设计、课程设计、大作业或工程实训的参考方案帮助解决从零搭建旅游类网站时缺乏完整案例的问题。压缩包共收录2001个文件整体约132.64MB其中以1627个jpg图片资源为主用于景点展示与页面配图83个py文件承载Django后端逻辑119个js与38个css、19个vue文件负责前端交互与样式另有sqlite3数据库、xml配置及字体图标等资源结构较为完整。目前已有144人学习下载。项目涵盖旅游信息展示、页面路由与数据交互等模块读者可借此了解Django项目目录组织、前后端资源配合方式与数据库设计思路并在此基础上进行二次开发或功能扩展适合作为旅游类Web项目的实践起点。1. 旅游自主网站到底自主在哪从 Django 的 MTV 说起很多人第一次听到「旅游自主网站」这个词脑子里浮现的是携程、马蜂窝那种体量的平台觉得个人开发者根本碰不了。其实这里的「自主」不是指商业上跟大平台竞争而是指内容自主、路线自主、数据结构自主——你能自己定义一条线路包含几个景点、每个景点挂几张图、用户怎么收藏和评论而不是被某个 SaaS 后台的固定字段框死。这类站点最适合的场景是地方文旅工作室要做一个县域旅游推介站、户外俱乐部要管理自己的活动线路库、或者你单纯想拿一个「有真实业务模型」的项目练 Django 全栈。我做过几个类似的东西血泪经验是旅游站点的难点从来不在页面好不好看而在数据模型怎么设计。景点、线路、用户收藏、评论、订单这几张表之间的关系一旦设计歪了后面每加一个功能都要改表结构。Django 自带的 ORM 和 admin 后台恰好能把这件事的成本压到很低你只要把 model 写对后台管理界面几乎白送。这篇就按「模型设计 → 视图与路由 → 模板与静态资源 → 部署与排错」的顺序把一套能跑起来的旅游自主网站拆开讲清楚新手能照着敲熟手能对照着看自己的边界在哪。2. 数据模型先立住景点、线路、用户三张核心表怎么设计2.1 为什么旅游站点必须先想清楚实体关系旅游业务的核心实体其实就三个景点Spot、线路Route、用户User。但坑在于「线路」和「景点」是多对多关系——一条线路包含多个景点一个景点也可能出现在多条线路里。新手最容易犯的错是把景点 ID 直接塞进线路表的一个字段里用逗号分隔比如spots 1,3,5。这样写当时爽等到要查「包含景点 A 的所有线路」时就得全表扫描加字符串匹配性能直接崩。正确做法是用 Django 的ManyToManyField让 ORM 帮你建中间表。另外线路还需要一个「顺序」概念——同样是景点 A、B、C先去 A 还是先去 C 是两条不同的线路所以中间表要带order字段。Django 默认的 M2M 不带额外字段这时候要用through参数指定自定义中间模型。这是整个项目里最值得花时间想清楚的一步。2.2 用 models.py 定义可扩展的表结构下面是我一般会用的模型骨架放在tour/models.py里from django.db import models from django.contrib.auth.models import User class Spot(models.Model): 景点表旅游站点最基础的实体 name models.CharField(景点名称, max_length100) city models.CharField(所在城市, max_length50, db_indexTrue) description models.TextField(景点介绍, blankTrue) cover models.ImageField(封面图, upload_tospots/, blankTrue) ticket_price models.DecimalField(门票价, max_digits8, decimal_places2, default0) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] verbose_name 景点 def __str__(self): return f{self.city}-{self.name} class Route(models.Model): 线路表一条线路串起多个景点 title models.CharField(线路标题, max_length120) author models.ForeignKey(User, on_deletemodels.CASCADE, related_nameroutes) summary models.CharField(一句话简介, max_length200, blankTrue) days models.PositiveSmallIntegerField(建议天数, default1) spots models.ManyToManyField(Spot, throughRouteSpot, related_nameroutes) is_published models.BooleanField(是否发布, defaultFalse) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] indexes [models.Index(fields[is_published, -created_at])] def __str__(self): return self.title class RouteSpot(models.Model): 线路-景点中间表带顺序字段 route models.ForeignKey(Route, on_deletemodels.CASCADE) spot models.ForeignKey(Spot, on_deletemodels.CASCADE) order models.PositiveSmallIntegerField(第几站, default1) stay_minutes models.PositiveIntegerField(建议停留分钟, default60) class Meta: ordering [order] unique_together (route, spot) # 同一条线路不重复挂同一景点这段代码有几个参数值得单独说。db_indexTrue加在city上是因为旅游站点最常见的筛选就是「按城市查景点」没索引的话数据量一上来查询就慢。is_published和created_at建了联合索引对应首页「已发布线路按时间倒序」这个高频查询。unique_together防止同一条线路重复挂同一个景点这是业务约束写在数据库层比写在视图层可靠。on_deletemodels.CASCADE表示作者注销时其线路一并删除如果你希望保留内容改成SET_NULL并给author加nullTrue。2.3 迁移与后台两条命令换来一个管理界面模型写完执行迁移python manage.py makemigrations tour python manage.py migrate然后在tour/admin.py里注册让 Django admin 直接接管增删改查from django.contrib import admin from .models import Spot, Route, RouteSpot class RouteSpotInline(admin.TabularInline): model RouteSpot extra 1 # 默认多给一行空白方便连续添加景点 admin.register(Route) class RouteAdmin(admin.ModelAdmin): list_display (title, author, days, is_published, created_at) list_filter (is_published, days) search_fields (title, summary) inlines [RouteSpotInline] admin.site.register(Spot)RouteSpotInline是关键它让你在编辑一条线路时直接在同一个页面里增删景点并调整顺序不用来回跳转。list_filter和search_fields是后台管理的效率开关数据上千条以后没有它们基本没法用。到这里一个能录入景点、编排线路的后台就成型了前端还没写但数据层已经稳了。3. 视图与路由把线路列表、详情、收藏串起来3.1 用类视图还是函数视图先看查询复杂度Django 的视图有两种写法函数视图FBV和类视图CBV。旅游站点的列表页、详情页结构高度相似用ListView和DetailView能省掉大量重复代码。但一旦涉及「当前用户是否收藏了这条线路」这种带登录态的查询CBV 的get_context_data就要重写。我的习惯是纯展示用 CBV带复杂业务逻辑的用 FBV不强行统一。先看列表页用ListView加筛选# tour/views.py from django.views.generic import ListView, DetailView from django.db.models import Prefetch from .models import Route, RouteSpot class RouteListView(ListView): model Route template_name tour/route_list.html context_object_name routes paginate_by 12 # 每页 12 条配合三列栅格 def get_queryset(self): qs Route.objects.filter(is_publishedTrue).select_related(author) city self.request.GET.get(city) if city: # 线路里只要有一个景点在该城市就命中 qs qs.filter(spots__citycity).distinct() return qs.prefetch_related( Prefetch(routespot_set, querysetRouteSpot.objects.select_related(spot)) )select_related(author)解决的是「列表里显示作者名」时的 N1 查询问题——不加它12 条线路就要多查 12 次用户表。prefetch_related配合Prefetch对象把每条线路的景点一次性拉出来避免模板里循环访问route.spots.all时反复查库。distinct()是因为按城市筛选走的是 M2M 关联一条线路可能匹配多个景点导致重复行。这几个优化不做本地开发感觉不出来一上线数据量上来页面就卡成幻灯片。3.2 详情页与收藏功能一个 POST 搞定状态切换详情页除了展示线路和景点通常还要有收藏按钮。收藏模型单独建class Favorite(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) route models.ForeignKey(Route, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, route)视图用 FBV 写收藏切换逻辑直白from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import get_object_or_404 login_required def toggle_favorite(request, route_id): if request.method ! POST: return JsonResponse({ok: False, msg: method not allowed}, status405) route get_object_or_404(Route, pkroute_id, is_publishedTrue) fav, created Favorite.objects.get_or_create(userrequest.user, routeroute) if not created: fav.delete() return JsonResponse({ok: True, favorited: False}) return JsonResponse({ok: True, favorited: True})get_or_create返回的created布尔值直接告诉你这次是新增还是已存在省掉一次查询。前端用fetch发 POST拿到favorited字段切换按钮样式即可。注意login_required对 AJAX 请求默认会重定向到登录页返回的是 HTML 而不是 JSON前端会解析失败。稳妥做法是自定义一个装饰器未登录时返回 401 JSON让前端跳登录。这个坑我踩过排查了半天才发现是重定向在作怪。3.3 路由配置与 URL 命名tour/urls.py里给每个视图起名字模板里用{% url %}反向解析别硬编码路径from django.urls import path from . import views app_name tour urlpatterns [ path(, views.RouteListView.as_view(), nameroute_list), path(route/int:pk/, views.RouteDetailView.as_view(), nameroute_detail), path(route/int:route_id/favorite/, views.toggle_favorite, nametoggle_favorite), ]app_name加上命名空间后模板里写{% url tour:route_detail route.pk %}。这样以后改 URL 结构模板不用动。项目根urls.py用include(tour.urls)挂进来再配好MEDIA_URL和STATIC_URL的静态文件路由开发阶段就能正常显示图片了。4. 模板与静态资源让页面能看、图片能传、样式不打架4.1 模板继承一个 base.html 管住全站骨架旅游站点的页面结构高度一致——顶部导航、内容区、底部。用模板继承把公共部分抽到base.html!-- templates/base.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1 title{% block title %}旅游自主网站{% endblock %}/title {% load static %} link relstylesheet href{% static css/main.css %} /head body header classsite-header a href{% url tour:route_list %}首页/a {% if user.is_authenticated %} span你好{{ user.username }}/span {% else %} a href{% url login %}登录/a {% endif %} /header main classcontainer {% block content %}{% endblock %} /main script src{% static js/favorite.js %}/script /body /html子模板只写{% block content %}里的东西。{% load static %}必须放在使用{% static %}之前这个顺序错了模板直接报错。user.is_authenticated在模板里可以直接用Django 的认证上下文处理器默认开启。4.2 图片上传与 MEDIA 配置新手最容易卡住的地方景点封面图用ImageField但光有字段不够还要装 Pillow 并配好 MEDIApip install Pillowsettings.py里加MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media根urls.py开发阶段追加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)模板里用{{ spot.cover.url }}取图片地址。注意static()只在DEBUGTrue时生效上线后必须交给 Nginx 之类的服务器处理否则图片 404。这个「本地好好的一部署图片全裂」的问题几乎每个新手都会遇到一次。4.3 收藏按钮的前端交互favorite.js里处理点击用 CSRF token 发 POSTfunction getCookie(name) { const v document.cookie.match((^|;)\\s* name \\s*\\s*([^;])); return v ? v.pop() : ; } document.querySelectorAll(.fav-btn).forEach(btn { btn.addEventListener(click, async () { const routeId btn.dataset.routeId; const resp await fetch(/route/${routeId}/favorite/, { method: POST, headers: { X-CSRFToken: getCookie(csrftoken) }, }); if (resp.status 401) { window.location.href /accounts/login/; return; } const data await resp.json(); btn.textContent data.favorited ? 已收藏 : 收藏; }); });Django 的 CSRF 保护对 POST 请求是强制的X-CSRFToken头必须带上值从 cookie 里取。401 分支对应前面说的未登录情况直接跳登录页。这段代码不长但把「登录态 CSRF 状态切换」三件事都覆盖了是旅游站点交互部分的最小闭环。5. 避坑与排查上线前必须过的五道坎5.1 现象列表页按城市筛选后出现重复线路原因Route和Spot是多对多一条线路挂两个同城景点时filter(spots__citycity)会返回两行相同线路。解决查询链末尾加.distinct()。如果还慢说明该建索引了给Spot.city加db_indexTrue。5.2 现象后台能传图前端页面图片 404原因开发时static()路由没配或者MEDIA_ROOT路径写错。解决确认urls.py里追加了static(settings.MEDIA_URL, ...)且DEBUGTrue。上线后改用 Nginx 的location /media/指向MEDIA_ROOT别指望 Django 在生产环境伺服静态文件。5.3 现象收藏按钮点了没反应控制台报 403原因POST 请求没带 CSRF token。解决检查favorite.js里X-CSRFToken头是否取到了csrftokencookie。如果 cookie 取不到确认模板里渲染表单时用了{% csrf_token %}或者视图用了ensure_csrf_cookie。5.4 现象线路详情页加载特别慢景点越多越慢原因模板里循环route.spots.all时每条线路都查一次库典型的 N1。解决视图里用prefetch_related预取模板里改用route.routespot_set.all并访问.spot配合select_related(spot)一次查完。5.5 现象迁移时报错「no such table」或字段冲突原因改了 model 没生成迁移或者多人协作时迁移文件冲突。解决先makemigrations再migrate别手动改数据库。协作时迁移文件要提交到版本库冲突了手动合并或重建。生产环境改表前务必备份migrate不是后悔药。6. 进阶技巧用 Django 的查询优化和缓存把首页压到 200ms 内前面几章把功能跑通了但一个旅游站点如果首页要 2 秒才出来用户早跑了。这一章讲两个我实际用过的提速手段都是 Django 自带、不用引额外中间件的。第一个是only()和defer()。列表页其实只需要线路的标题、简介、天数不需要把description这种大文本字段全拉出来。在get_queryset里加.only(id, title, summary, days, created_at)数据库传输量能降一大截。注意only之后如果模板里访问了没列出的字段Django 会再查一次库所以字段列表要跟模板对齐这个边界得自己盯住。第二个是缓存。首页的线路列表对所有人一样适合整页缓存from django.views.decorators.cache import cache_page cache_page(60 * 5) # 缓存 5 分钟 def home(request): ...或者在settings.py里配CACHES用本地内存CACHES { default: { BACKEND: django.core.cache.backends.locmem.LocMemCache, LOCATION: tour-cache, } }本地内存缓存适合单进程开发和小流量站点多进程部署要换成 Redis 或 Memcached否则每个进程各存一份命中率上不去。缓存时间设多长是个权衡设太短没效果设太长用户发了新线路看不到。我的习惯是列表页 5 分钟详情页 10 分钟用户相关的收藏页不缓存。验证优化效果别靠感觉用 Django Debug Toolbar 看每个页面的 SQL 条数和耗时。装好之后如果列表页 SQL 条数随数据量线性增长说明还有 N1 没解决如果 SQL 条数稳定在个位数那基本就到位了。我一般会盯着这个面板把首页 SQL 压到 5 条以内耗时压到 200ms 以内再考虑上线。最后一个习惯每次改完 model 或查询先跑一遍python manage.py check --deploy它会提示你哪些生产配置没设对比如DEBUG没关、SECRET_KEY硬编码、ALLOWED_HOSTS为空。这些提示不解决功能问题但能帮你避开上线后最尴尬的几类事故。希望帮到你。本文还有配套的精品资源点击获取