简介:面向Python毕业设计或课程设计需求的完整旅游网站项目包,基于Python 3.7与MySQL开发,前后端代码完备,包含项目源码、数据库脚本及运行所需工具。项目经严格调试,可在PyCharm中直接配置依赖运行。
包内共813个文件,压缩后大小20.77MB,以JS、CSS、HTML、Vue等前端类型为主,辅以GIF、JPG、SVG等静态资源,同时包含SQL数据库脚本与bat自动化部署脚本(安装、运行、构建),目录结构清晰,便于按模块阅读和二次开发。
目前已有55位学习者下载查看,特别适合需要完整项目参考的应届毕业生与Python学习者。资源提供了从环境配置、数据库导入到前后端联调的全套方案,可帮助理解旅游网站常见的用户管理、景点展示、订单流程等模块实现,省去从零搭建的时间成本。
1. Python毕业设计做旅游网站:为什么大家选它,又为什么容易翻车
在毕业设计辅导和答辩现场,Python课程设计做旅游网站是我见过最多的选题之一,它的热是有道理的:用户注册登录、景点展示、线路推荐、订单预订、评论管理,每一块拆开都是标准的数据库增删改查,论文目录天然能按功能模块展开,工作量正好卡在两三个月能交付的区间里。但真正把这套项目跑起来,翻车的也不少——下载回来的源码Django版本对不上,迁移文件冲突,MySQL中文字符集报错,演示前静态页面整个打不开,这些问题我在线下一对一辅导里几乎每周都会碰到一次。这篇文章就顺着一个能完整运行的旅游网站方案,把技术选型、数据模型、核心功能实现和验收前最值得检查的坑位讲清楚。适合正在做Python毕业设计的同学,也适合帮毕业生做辅导的人参考。
2. 选型与数据模型:先用五张表把旅游网站的骨架立住
2.1 Django还是Flask:从答辩表现角度如何选框架
先回答选型问题。旅游网站这个题目,看起来Flask和Django都能做,但适用面差别很大。Flask的精简意味着路由、数据库连接、用户会话都要自己设计,页面个性化程度高,适合喜欢掌控细节的人;而Django自带Admin后台、auth认证、ORM映射,常规CRUD功能直接能用框架能力覆盖。以毕设交付为场景,我一般强烈建议用Django。
原因很清楚。第一,旅游网站功能模块多,用户、景点、订单、评论、收藏,每一块都需要后台维护数据。Django安装完成后自带一个可用的管理后台,你在Admin里直接可以给景点添加封面图、改价格、看订单,等于管理端功能不用从头写。第二,Django的auth模块已经实现了注册登录、密码加密和session管理,这些在答辩时是必问功能,自己实现的密码存储往往经不起追问。第三,从写论文的角度看,Django的MTV结构对应关系很清晰,models对应数据库设计、views对应功能、templates对应页面,每一节都有明确归属。
Flask也并非不能做,只是你要在项目前期把额外组件都准备好:用SQLAlchemy写ORM,用Flask-Login做认证,用Flask-Admin做后台。这些组件本身不复杂,但组合起来的版本兼容问题要比Django集中在一个框架里更繁琐。假如你已经在Flask上写了一大半,就不要再临时换框架;如果还在起步阶段,选Django能明显省出写后台的时间。
版本建议也顺带在这里说掉。这个方案我一般固定在一套稳定的组合上:Python 3.8或者3.9,Django 4.2,MySQL 5.7或8.0。Python 3.8是很多毕设电脑上已经装好的版本,Django 4.2对它兼容性很好,真遇到Python 3.12新机器时再考虑升级,不要在毕设期间追版本,稳比新重要。
| 对比项 | Django | Flask |
|---|---|---|
| 后台管理 | 自带Admin,直接维护数据和订单 | 需自己搭Flask-Admin或写页面 |
| 用户认证 | 自带auth,注册登录开箱即用 | 需接Flask-Login并管理会话 |
| 开发速度 | 功能多时开发效率高 | 轻量小项目效率高 |
| 论文契合度 | MTV结构与章节对应清楚 | 自由度高,需要额外解释设计 |
| 常见坑 | 依赖版本集中 | 第三方组件版本组合容易冲突 |
2.2 数据建模:景点、订单、评论三条模型代码与字段参数详解
旅游网站功能越想越多,但落到提交流程上的核心数据其实不多。我建议第一版只建五张业务表:用户直接用Django内置User表,景点表存基础信息,订单表管理用户预约,评论表承载景区口碑,收藏表或者线路表按题目方向二选一。下面这段是景点、订单、评论三个模型的代码,它们基本覆盖了旅游网站的数据主体。
from django.db import models from django.contrib.auth.models import User class Scenic(models.Model): name = models.CharField(max_length=100, verbose_name='景点名称') location = models.CharField(max_length=255, verbose_name='所在地区') price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='门票价格') description = models.TextField(verbose_name='景点介绍') cover = models.ImageField(upload_to='scenic/%Y%m/', verbose_name='封面图') stock = models.IntegerField(default=0, verbose_name='每日可预约人数') created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '景点' verbose_name_plural = verbose_name ordering = ['-created_time'] def __str__(self): return self.name class Order(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') scenic = models.ForeignKey(Scenic, on_delete=models.CASCADE, verbose_name='景点') visit_date = models.DateField(verbose_name='游玩日期') quantity = models.PositiveIntegerField(default=1, verbose_name='预约人数') status = models.CharField( max_length=20, choices=( ('pending', '待支付'), ('paid', '已支付'), ('canceled', '已取消'), ), default='pending', verbose_name='订单状态', ) created_time = models.DateTimeField(auto_now_add=True, verbose_name='下单时间') class Meta: verbose_name = '订单' verbose_name_plural = verbose_name ordering = ['-created_time'] def __str__(self): return f'{self.user.username}-{self.scenic.name}' class Comment(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') scenic = models.ForeignKey(Scenic, on_delete=models.CASCADE, verbose_name='景点') content = models.TextField(verbose_name='评论内容') score = models.PositiveSmallIntegerField(default=5, verbose_name='评分') created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '评论' verbose_name_plural = verbose_name ordering = ['-created_time']这段模型有几个参数值得细说。第一,ForeignKey里的on_delete=models.CASCADE表示用户或景点被删除时,关联订单和评论一并删除,开发阶段这样最直观。如果你希望删掉景点后还保留订单记录,可以改成models.SET_NULL并把scenic设置为null=True,但那样在查询订单详情时要额外处理scenic为空的情况。第二,Order.status用choices限定字符串,后续做支付状态流转只需要改字段值,不需要改动表结构;用Django迁移时,choices修改也会自动记录到迁移文件里,答辩时可以拿出这个点说明你是如何控制订单状态的。第三,upload_to='scenic/%Y%m/'表示景点封面图按年、月分目录存放,避免大量图片堆在同一个文件夹里拖慢Admin后台。
推荐你顺手把Scenic和Order注册到admin.py里。注册之后,在2.3创建的管理员账号就能直接录入景点数据、模拟后台改订单状态,这是旅游网站毕设“管理端”最快的实现路径。
# travel_app/admin.py from django.contrib import admin from .models import Scenic, Order, Comment @admin.register(Scenic) class ScenicAdmin(admin.ModelAdmin): list_display = ('name', 'location', 'price', 'stock') search_fields = ('name', 'location') admin.site.register(Order) admin.site.register(Comment)这里的@admin.register装饰器把景点模型注册到后台,list_display决定后台列表页显示哪些列,search_fields直接给Admin页面加一个搜索框。答辩时老师问你后台是怎么做的,你演示这些配置动作就能解释清楚,不用展示一大堆自己写的管理页面。
2.3 初始化项目把数据库接上:settings配置与迁移命令
模型定义完了,下面就是让这套东西真正跑起来的最小操作序列。我平时习惯在命令行创建项目而不是用IDE的“新建项目”按钮,原因是我见过不少同学因为IDE自动生成的项目目录多一层嵌套,导致相对导入和整个项目入口在本地跑不起来。命令行操作每一步都能看见,出错了也知道在哪一步。
# 在项目根目录创建并激活虚拟环境 python -m venv venv venv\Scripts\activate # 安装依赖,用可信的镜像源避免超时 pip install django==4.2.14 pymysql Pillow # 创建项目travel_project和应用travel_app django-admin startproject travel_project . python manage.py startapp travel_app执行完上面命令,项目根目录会同时出现travel_project配置目录和travel_app业务目录。一般解压一个旅游网站毕设包后,你看到的目录结构也是这两层:外层是项目配置,内层是业务模块,static和templates分别放静态资源和页面模板。
数据库选用MySQL时,在settings.py里做如下配置。需要注意,Django默认使用SQLite,如果只是快速验证,可以先不改成MySQL,等演示前再切;不过毕业设计通常要求演示关系型数据库,所以我把MySQL的配置一并写出来。
# travel_project/settings.py 部分配置 import pymysql pymysql.install_as_MySQLdb() DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'travel', 'USER': 'root', 'PASSWORD': '你的数据库密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } } LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'travel_app', ]连接层配置好后,执行迁移和创建管理员:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser以上参数里,pymysql.install_as_MySQLdb()是让PyMySQL在Django 4.2中扮演MySQLdb的角色,没有MySQL原生客户端库时也能正常连接。DATABASES的OPTIONS指定charset为utf8mb4,数据库创建时也要用utf8mb4;如果库里用的是默认latin1,这里写什么都会被字符集问题拦下来,具体坑位在第4章会展开。USE_TZ=True表示Django按UTC存储时间、按TIME_ZONE展示本地时间,设置成Asia/Shanghai后,用户在页面上看到的是北京时间,不会出现上午下单变成凌晨时间的问题。createsuperuser创建的管理员账号就是登录/admin后台维护景点数据的入口。
到这里,一个没有任何业务页面的旅游网站骨架已经能跑起来了。下一步要把业务页面接进去。
3. 核心功能落地:注册登录、景点列表与订单闭环的Django代码
3.1 注册登录退出:auth模块三种视图代码与模板写法
用户认证是旅游网站逃不掉的部分,Django自带auth模块,注册使用create_user完成,登录使用authenticate和login。我给出的这套视图没有用Django自带的LoginView类视图,而是写函数视图,好处是逻辑直观,答辩时讲session建立的过程更清楚。
# travel_app/views.py from django.contrib.auth import login, logout, authenticate from django.contrib.auth.models import User from django.shortcuts import render, redirect def register(request): if request.method == 'POST': username = request.POST.get('username', '').strip() password = request.POST.get('password', '') password2 = request.POST.get('password2', '') if not username or not password: return render(request, 'register.html', {'error': '用户名和密码不能为空'}) if password != password2: return render(request, 'register.html', {'error': '两次密码输入不一致'}) if User.objects.filter(username=username).exists(): return render(request, 'register.html', {'error': '该用户名已被注册'}) user = User.objects.create_user(username=username, password=password) login(request, user) return redirect('scenic_list') return render(request, 'register.html') def user_login(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('scenic_list') return render(request, 'login.html', {'error': '用户名或密码错误'}) return render(request, 'login.html') def user_logout(request): logout(request) return redirect('scenic_list')这段代码的重点在于create_user和authenticate。create_user会把密码做哈希后存入数据库,展示数据时数据库中看不到原始密码,答辩时会问“你密码怎么存的”,这个答案就是现成的。authenticate内部会检查用户名、密码以及用户是否激活,比自己写User.objects.filter再额外判断is_active靠谱。register视图里加了密码两次校验和用户名查重,这是演示时最容易被点到的参数校验点。
注册登录页面模板里,每个form表单都要写出{% csrf_token %},否则提交时Django会返回403。这里贴一个登录表单的最小写法:
<!-- templates/login.html --> <form method="post" action="{% url 'user_login' %}"> {% csrf_token %} <input type="text" name="username" placeholder="用户名" required> <input type="password" name="password" placeholder="密码" required> <button type="submit">登录</button> </form>urls.py也需要同步映射这几个视图:
# travel_project/urls.py from django.urls import path from travel_app import views urlpatterns = [ path('register/', views.register, name='register'), path('login/', views.user_login, name='user_login'), path('logout/', views.user_logout, name='user_logout'), path('', views.scenic_list, name='scenic_list'), ]urls.py里的name参数就是模板中{% url 'user_login' %}的依据,路由改名后模板里的名称要同步改,否则页面会报NoReverseMatch错误。这部分如果用的是别人的源码包,经常出现的就是模板里引用的路由名称和urls.py不一致,排查时优先检查name。
3.2 景点列表与详情页:分页、地区筛选与翻页参数保持
旅游网站的主页就是景点列表。这个页面要同时解决分页和筛选两个问题。下面这个视图按创建时间倒序排列景点,并根据URL中的location参数过滤地区,然后通过Paginator做到每页8条记录。
# travel_app/views.py from django.core.paginator import Paginator from django.shortcuts import render, get_object_or_404 from .models import Scenic def scenic_list(request): location = request.GET.get('location', '').strip() scenic_queryset = Scenic.objects.all().order_by('-created_time') if location: scenic_queryset = scenic_queryset.filter(location__contains=location) paginator = Paginator(scenic_queryset, 8) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) return render(request, 'scenic_list.html', { 'page_obj': page_obj, 'location': location, }) def scenic_detail(request, scenic_id): scenic = get_object_or_404(Scenic, pk=scenic_id) comments = scenic.comment_set.all().order_by('-created_time')[:10] return render(request, 'scenic_detail.html', { 'scenic': scenic, 'comments': comments, })这里有两个参数值得注意。第一,filter(location__contains=location)对应SQL里的LIKE '%关键字%',适合毕设这种轻量级搜索,但对于用户输入特殊字符没有任何转义处理,演示搜索时不要输入引号等容易出错的符号。第二,Paginator(page_obj)在模板中要同时使用,导航用page_obj.has_previous和page_obj.previous_page_number。列表页模板还要注意把location参数拼在翻页链接上,否则用户已经按地区筛选后翻到第2页,条件就丢掉了。
<!-- templates/scenic_list.html --> <div class="scenic-grid"> {% for scenic in page_obj %} <div class="scenic-card"> <img src="{{ scenic.cover.url }}" alt="{{ scenic.name }}"> <h3><a href="{% url 'scenic_detail' scenic.id %}">{{ scenic.name }}</a></h3> <p>{{ scenic.location }} · ¥{{ scenic.price }}</p> </div> {% endfor %} </div> {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}&location={{ location }}"> 上一页 </a> {% endif %}scenic_detail视图里通过scenic.comment_set.all()拿到该景点下的所有评论。这个命名是Django默认的反向查询,如果你在Comment模型中定义了related_name='comments',那就不用加任何后缀,直接在模板中写scenic.comments.all。对毕设来说,默认写法已经能用,加related_name的好处是模板里写起来更接近英文语法。
3.3 评论与下单:外键关联数据怎么串起来
评论和订单都依赖当前登录用户。视图上加@login_required,未登录用户会被重定向到登录页,这就在流程上保证了外键字段一定不是空的。
from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render, get_object_or_404 from .models import Scenic, Order, Comment @login_required def add_comment(request, scenic_id): scenic = get_object_or_404(Scenic, pk=scenic_id) if request.method == 'POST': content = request.POST.get('content', '').strip() score = request.POST.get('score', 5) if content: Comment.objects.create( user=request.user, scenic=scenic, content=content, score=score, ) return redirect('scenic_detail', scenic_id=scenic.id) return redirect('scenic_detail', scenic_id=scenic.id) @login_required def create_order(request, scenic_id): scenic = get_object_or_404(Scenic, pk=scenic_id) if request.method == 'POST': visit_date = request.POST.get('visit_date', '') quantity = int(request.POST.get('quantity', 1)) if not visit_date: return render(request, 'order_form.html', {'error': '请选择游玩日期'}) if quantity > scenic.stock: return render(request, 'order_form.html', {'error': '超出当日可预约人数'}) Order.objects.create( user=request.user, scenic=scenic, visit_date=visit_date, quantity=quantity, status='pending', ) return redirect('order_list') return render(request, 'order_form.html', {'scenic': scenic}) @login_required def order_list(request): orders = request.user.order_set.all().order_by('-created_time') return render(request, 'order_list.html', {'orders': orders})create_order里最容易被忽略的是逆向外键查询。request.user.order_set.all()表示按订单表里user字段找到当前用户的所有订单,与前面景点评论的comment_set逻辑一致。int(request.POST.get('quantity', 1))在页面上输入非数字时会抛ValueError,演示时手动输入不会遇到,但给导师做自动化测试脚本时可能遇到,建议后面加一个try判断把quantity转换成int再继续。status固定为pending,要模拟支付就在订单列表上加一个确认按钮,调用一次update(status='paid')即可,不要把支付流程做得过于复杂,那不是旅游网站毕设的核心价值点。
3.4 扩展线路或收藏模块:ManyToMany关联的加字段方式
如果你的题目叫“旅游网站”而不是“景点门票预订网站”,通常还要放进线路规划模块。最省事的做法是把线路表定义成和Scenic并列的模型,线路字段包含标题、天数、价格、简介;线路和景点用ManyToManyField关联。然后订单表里增加一个可空的外键,指向线路,这样下单时选择景点或线路都不会破坏原有表结构。
class Route(models.Model): name = models.CharField(max_length=100, verbose_name='线路名称') days = models.PositiveIntegerField(default=1, verbose_name='行程天数') price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='价格') description = models.TextField(verbose_name='线路介绍') scenics = models.ManyToManyField(Scenic, blank=True, verbose_name='包含景点') created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')这样扩展之后,后台Admin自然出现线路管理页面,景点数量也不会超过原来的管理范围。要是你的题目里还要求民宿预订,就在Order里新增一个model字段区分订单类型,不要扩出第三套订单流程。这个阶段还有一个实用做法:写一个简单的数据填充脚本,用for循环生成十几条景点记录,避免演示时后台数据空空荡荡。
4. 验收前的避坑检查:环境、数据、演示机三个层面的常见问题
4.1 Python环境变量没配置,命令行执行python直接报错
现象:项目运行前,在cmd窗口输python,返回“不是内部或外部命令”。或者双击启动脚本,窗口一亮就关。原因:安装Python时没有勾选Add Python to PATH,系统环境变量里没有Python路径;也有的是电脑上装了多个Python版本,PATH顺序把旧版排在了前面。
解决:不要急着重装,先找哪个解释器在生效。
where python python --version如果where python找不到,去Python安装目录找到python.exe,把目录追加到系统环境变量Path中,然后新开cmd执行python --version确认版本号。安装Python时,安装向导有个“Add python.exe to PATH”的复选框,很多同学都是在这里把坑埋下来的。在线辅导时,我见到的绝大部分“项目跑不起来”都出在这一层,还没进入写代码阶段。
4.2 MySQL字符集不一致,中文内容写入乱码
现象:页面能正常打开,但提交中文数据时报告Incorrect string value,或者数据库里存的内容是一排问号和乱码。原因:创建数据库时没有指定字符集,MySQL默认用了latin1,而Django侧写入的是utf8mb4编码,两边对不上。从别人那里导入的备份文件恢复后字符集丢失,也是这个现象。
解决:先确认当前库的字符集。
SHOW CREATE DATABASE travel;如果结果里看不到utf8mb4,就把库重建成utf8mb4。项目还没写数据的阶段直接删库重建最干净;已经有数据的,先导出备份再重建。
CREATE DATABASE travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;重建之后同步修改settings.py里数据连接的OPTIONS,charset保持utf8mb4。还要注意一个细节:mysqlclient和PyMySQL的版本要匹配对应Python版本,Python 3.9下装pymysql比较顺利,Python 3.12下可能因为Django对PyMySQL的版本要求报错,这时按提示把pymysql升级到较新版本。
4.3 静态文件图片全挂,MEDIA路由未配置
现象:列表页HTML结构在,但整页没有样式,圆形图、封面图全部裂开。原因:Django开发服务器默认不开静态文件路由,必须在settings里配置STATIC_URL、STATICFILES_DIRS,还要在根urls.py里通过static()把media文件映射出去。
解决:在settings.py补充以下配置。
import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ] MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')然后在urls.py里接入路由:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0]) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这里有一个典型误区:把STATIC_ROOT和STATICFILES_DIRS搞混。开发阶段用STATICFILES_DIRS告诉Django去哪里找源码目录下的静态文件;collectstatic部署时才需要STATIC_ROOT。如果两者弄反,演示时会报Static file not found,但改了半天也没解决。图片上传是否成功,看MEDIA_ROOT目录里是否生成新的子目录和文件即可。
4.4 requirements.txt版本未锁定,换台电脑ModuleNotFoundError
现象:代码在自己电脑跑得很正常,同步到同学电脑后ModuleNotFoundError不断跳出来,或者同一段代码在两台机器上结果不一样。原因:requirements.txt里写的是django>=4.0这种宽松范围,pip自动选择的最新版本可能包含接口变化;更常见的是,很多人连requirements.txt都没有,靠一条条pip install记不清装了什么。
解决:在项目功能全部测试通过后,用pip freeze生成锁定版本的依赖清单。
pip freeze > requirements.txt重新安装时,逐项核对requirements.txt里的版本。比如pymysql版本过低,pymysql.install_as_MySQLdb()会报找不到MySQLdb模块;Pillow版本过低,图片字段打开时报告初始化失败。如果项目里还用了图像压缩或验证码库,比如numpy、opencv-python这类,在生成requirements后检查一下是否有“No module named 'cv2'”的报错,有就直接追加一行opencv-python的指定版本再重新安装。此时确认VSCode右下角选择的Python解释器是在venv虚拟环境里,而不是宿主机Python,否则pip装的位置和运行的位置不是同一个环境,怎样安装都会继续报错。PyCharm和VSCode都有这个解释器选择问题,打开项目时默认可能会采用全局解释器。
4.5 页面提交报403、404、500,看日志按Traceback定位
现象:点击登录或提交评论按钮,页面返回403 Forbidden,或者反过来返回500 error。原因:403基本是模板form里没有{% csrf_token %},Django的CSRF防护默认开启;500是视图代码异常,并不是配置问题;404是URL设计问题,详情页的scenic_id没有匹配到记录也会404。
解决:403就在form表单第一行加{% csrf_token %}。500看控制台输出的Traceback,异常信息会指明是views.py第几行报错,按行号去改比在页面上猜可靠得多。404要检查urls.py里路径的参数名,比如视图函数叫scenic_detail(request, scenic_id),URL里的int:scenic_id就必须和参数名一致,一个名字对不上就给你404。查看日志时建议直接跑python manage.py runserver,开着控制台做演示,出错时能当场看到完整报错,而不是只能看到页面上简单的“Server Error”。
5. 答辩前一晚:用一次全链路走查给项目做最终验证
做旅游网站毕设,功能实现容易,验收演示才是真正放大问题的地方。我养成了一个习惯:答辩前夜把数据库清空,把项目当新产品从零再交付一遍。这不是重写代码,而是把运行链路完整走一遍,确认从zip包解压到页面可访问的每一步都不会卡住。
清数据库的标准做法是:把travel库删除,然后重建一个同名utf8mb4库,依次执行makemigrations、migrate、createsuperuser,用新账号登录Admin后台录入景点和线路测试数据。这一套操作等价于在一台干净电脑上第一次部署项目,做完后你提交的zip包就能在任何一台电脑上按同样顺序复现,不会出现只在自己电脑能跑的尴尬。
第二个技巧是固定演示数据和演示账号。管理员账号之外,再创建一个普通用户test,密码设简单点,避免演示时临时想密码出错。景点封面图放几张本地jpg,不要外链图片,地址一失效,页面立刻破相。演示过程中不要现场上传图片,提前在后台传好,把时间留给核心流程。
第三个技巧是把演示步骤控制在八分钟以内。注册、登录、景点列表、详情页、下单、后台改价格、查看订单状态,这七步足以完整展示工作量。演示时开着console,发现页面报错直接看Traceback,如果是小改动就现场改,改完刷新;如果是结构性问题,就用事先准备好的“备用演示流程”跳过那个页面,先保住整体节奏。这套走查方法我每次指导学生都用,它不会帮你写代码,但可以让你在答辩台上少经历一段手心出汗的空白时间。希望帮到你。
本文还有配套的精品资源,点击获取