Python+Vue大学生旅游管理系统开发实战:从环境配置到部署全攻略
刚接手“PythonVue的大学生去哪旅游管理系统”这个题目的时候估计很多人跟我当时的反应一样这不就是一个典型的课程设计吗用Django或者Flask写个后端Vue搭个前端然后旅游景点增删改查、用户登录注册、路线推荐最后打包成能演示的系统交上去完事。但真做起来你会发现小项目五脏俱全前后端分离的思路、ORM查询、跨域、打包部署哪一环都能把人卡住半天。这个项目我前后带过几个学弟学妹做过也帮人改过不少半成品代码今天就把整个从需求拆解到落地运行的思路、细节和踩坑经验完整写一遍。不管你是准备交课程设计、毕业设计还是单纯想练手前后端分离开发这篇应该都能帮你在开始动手之前先把地图看明白。1. 先把系统拆开看大学生旅游管理系统到底做哪些事很多同学拿到题目第一件事就是打开Pycharm想着赶紧敲代码。我建议先花半小时把需求拆干净搞清楚哪些功能是必须的哪些是凑数的。“大学生去哪旅游”这个题目关键词在“大学生”和“旅游管理”。大学生群体有什么特点预算有限、时间集中周末和寒暑假、喜欢结伴出行、偏向热门打卡地加一些小众平价景点。所以你做的系统不能是那种商务旅游管理后台得有学生味儿。1.1 功能模块的底线划分一个能过关、能演示、能写进论文的基本功能集合我一般建议拆成三个端用户前端注册/登录学生用学号或者手机号注册登录景点展示列表页、详情页带图片、简介、票价、开放时间路线推荐按目的地展示旅游路线包含行程天数、预算区间收藏/评论学生可以收藏景点、在景点下发表评论或攻略个人中心查看自己收藏过的东西、浏览历史管理后台景点管理增删改查景点信息、上下架路线管理维护路线数据用户管理查看注册用户、禁用异常账号评论管理删除违规评论数据层数据库设计用户表、景点表、路线表、评论表、收藏表如果做了浏览记录就是六张表这套功能做完演示的时候逻辑是很完整的从前端注册登录到浏览景点、收藏、评论再到后台管理维护数据一条线走通老师问什么你都有东西可讲。1.2 前后端分离还是混合开发这个题目在知乎和CSDN上搜能看到两种路线一种是不分离用Django的模板系统加Bootstrap/JQuery把页面直接渲染出来。好处是简单不涉及跨域开发效率高适合只要求“能跑”的学生。但坏处是如果老师提前说明了要用Vue那这条路就走不通。另一种就是标题里写的Python Vue 前后端分离。Django/Flask只提供JSON接口前端Vue负责页面展示和交互。这套方案的好处是结构清晰符合企业里真实开发流程毕设答辩时有得聊。坏处是中间多了联调和跨域两个坎很多新手就是栽在这儿的。我个人建议只要标题明确出现了“Vue”就老老实实做前后端分离。一来这是硬性要求二来前后端分离项目写进报告和开题里看起来工作量是实打实的。1.3 Django 还是 Flask我的选择逻辑标题里同时出现了“django”和“flask”如果需求没有特别指定我强烈建议主用Django。理由很直接对比项DjangoFlask自带ORM有且功能完善没有需自己装SQLAlchemyAdmin后台自带改配置就能管理数据需要扩展包配置麻烦用户认证自带User模型和登录逻辑需要Flask-Login路由和视图结构固定适合新手灵活但自由度过高容易写乱课程设计典型场景查资料、问AI、找代码都方便资料同样多但零散Flask轻、灵活适合一个人从零手搓一切但是很多东西需要自己做决策。Django则是“规定好了的路”你沿着走就行。写旅游管理系统这种典型CRUD项目Django能省下大量重复劳动Admin后台甚至可以当简易的管理端用演示的时候能省不少事。如果你确实被要求必须用Flask那也别慌下面的设计思路完全一样只是把Django的Model和View换成Flask的SQLAlchemy和路由函数而已我最后会单独说迁移思路。2. 开工前哨战Pycharm、Python和Vue环境配置的一堆坑环境配置看着简单但是每年都有一大批人卡在这一步。把热词里的“pycharm安装”“python安装教程”“vue安装及环境配置”挨个过一遍我把最容易翻车的细节集中说。2.1 Python版本和Pycharm版本怎么选Python版本我建议用3.8到3.10之间的稳定版本。像3.12这类太新的版本有些第三方库还没跟上pip安装时容易报编译错误。去官网下载安装包时一定记得勾选“Add Python to PATH”这是新手最容易忽略的没勾的话命令行里打python提示不是内部或外部命令后面全都不顺。Pycharm版本上社区版Community是免费的专业版Professional功能全但需要激活。学生的话可以拿学校邮箱去申请免费的教育许可完全合法且省心。这个项目用社区版其实就足够了Vue的前端代码我推荐用Pycharm打开也行或者直接用VSCode看个人习惯。不过既然标题指定了Pycharm你至少要把后端项目放在Pycharm里写。2.2 创建虚拟环境是必须养成的习惯打开Pycharm新建Django项目时解释器选择界面一定要选“New environment using Virtualenv”。有些同学图快直接选了系统自带的Python解释器后面Django装到全局环境里不同项目之间的包互相污染迟早出问题。虚拟环境的好处是每个项目一个独立空间装什么包都不影响其他项目requirements.txt还能源源不断地导出来。以后换电脑、交作业别人拿到你的项目只要pip install -r requirements.txt就能把环境复原。2.3 Node.js和Vue CLI/Vite的版本匹配前端环境难点不在安装在版本。Vue框架从2到3是个坎Vue CLI和Vite的构建方式又不一样。我给学生的统一建议是装Node.js 16以上版本建议直接上18 LTS用npm install -g vue/cli 或者 npm create vuelatest 创建项目新手我建议用Vue CLIwebpack那套而不是Vite虽然启动慢一点但是文档多、资料全报错搜得到答案npm下载慢怎么办别傻傻等。先执行npm config set registry https://registry.npmmirror.com换成国内镜像以后速度立竿见影。这一步不做一个node_modules等半小时很正常。2.4 前端项目放到哪里、怎么打开前后端分离的项目我习惯建一个总的文件夹里面放backend和frontend两个子目录。比如Travel_System/ ├── backend/ # Django项目 └── frontend/ # Vue项目这样整体结构清楚打压缩包交作业也方便。用Pycharm打开backend目录用VSCode或者Pycharm打开frontend目录两边各开一个终端。千万别用Pycharm打开整个总目录然后只写后端这样前端node_modules会被Pycharm扫描半天卡得不行。3. Django后端落地从建项目到数据能查、能删、能返回JSON环境弄好后端这块就开始动手了。这一节我会把从命令行建项目到写模型、视图、接口、解决跨域的完整链路走一遍重点结合“django创建app、django执行查询-删除对象、django cookie设置token、django项目实战新手”这些高频搜索词。3.1 从零创建一个Django项目打开终端激活虚拟环境执行下面这套命令django-admin startproject travel_backend cd travel_backend python manage.py startapp travel这里的travel_backend是项目配置目录travel是我们自己创建的应用所有业务逻辑代码都放在travel目录里。创建完以后一定记得在settings.py的INSTALLED_APPS里加上travel不然后面写的模型、视图不被识别。INSTALLED_APPS [ django.contrib.admin, ... travel, # 加这一行 ]3.2 重点模型设计直接影响后期所有接口写法和数据库查询方式我这人写项目有个习惯模型字段宁可多花半小时想清楚绝不写到一半再加字段。因为Django的ORM在表已经生成之后改字段特别是加了非空约束的字段迁移时经常要处理默认值、数据迁移这些麻烦事。旅游系统的核心模型我这样设计from django.db import models from django.contrib.auth.models import User class ScenicSpot(models.Model): # 景点表 name models.CharField(max_length100, verbose_name景点名称) city models.CharField(max_length50, verbose_name所在城市) price models.DecimalField(max_digits8, decimal_places2, verbose_name门票价格) description models.TextField(verbose_name景点介绍) image models.ImageField(upload_toscenic/, blankTrue, nullTrue, verbose_name景点图片) open_time models.CharField(max_length100, blankTrue, verbose_name开放时间) category models.CharField(max_length20, blankTrue, verbose_name景点分类) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table scenic_spot def __str__(self): return self.name class TravelRoute(models.Model): # 路线表 title models.CharField(max_length100, verbose_name路线标题) days models.IntegerField(verbose_name行程天数) budget models.DecimalField(max_digits8, decimal_places2, verbose_name预算金额) spots models.ManyToManyField(ScenicSpot, verbose_name包含景点) note models.TextField(blankTrue, verbose_name路线说明) class Meta: db_table travel_route class Favorite(models.Model): # 收藏表 user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) spot models.ForeignKey(ScenicSpot, on_deletemodels.CASCADE, verbose_name景点) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table favorite unique_together (user, spot) class Comment(models.Model): # 评论表 user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) spot models.ForeignKey(ScenicSpot, on_deletemodels.CASCADE, verbose_name景点) content models.TextField(verbose_name评论内容) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table travel_comment说几个设计时的关键点name字段的max_length景点名称一般50到100够用但是如果你要放超长中文名或带英文最好给足长度price用DecimalField而不是FloatField浮点精度问题在涉及金额时绝对不能留隐患ImageField需要额外配置MEDIA_ROOT和MEDIA_URL否则图片存不进去展示也读不出来。这个热词里有“vue image能显示pdf吗”说明很多人卡在前端显示图片这块其实后端先要把图片路径正确返回才行外键的on_delete怎么选用户删了他的收藏和评论是否保留我一般选CASCADE——删除用户时把关联数据一并清掉这样不留孤儿数据。但如果你的业务要求保留用户评论就得改成SET_NULL且字段允许为空写完后执行python manage.py makemigrations travel python manage.py migrate python manage.py createsuperuser数据表就建好了后台管理员账号也建好了。3.3 查询与删除Django ORM里高频操作的正确姿势热词里有一条“django执行查询-删除对象”这几乎是所有Django项目的日常操作也是面试官和答辩老师爱问的点。我拆开来讲。查询最常用的几种方式# 获取所有景点 spots ScenicSpot.objects.all() # 筛选某个城市的景点filter返回QuerySet可以一直链式调用 spots ScenicSpot.objects.filter(city北京).filter(price__lte100) # 精确获取单条记录不存在会报DoesNotExist异常要注意捕获 spot ScenicSpot.objects.get(id1) # 模糊查询比如搜索景点名称中包含湖的记录 spots ScenicSpot.objects.filter(name__contains湖) # 排序查询价格从低到高 spots ScenicSpot.objects.order_by(price)filter和get的区别要牢记filter返回的是一个QuerySet对象就算结果只有一条它也是一个集合get返回的是单个模型实例查不到或者查到多条都会抛异常。日常接口里我很少直接用get因为异常处理起来麻烦更多是filter(...).first()取第一条记录查不到就返回None省心得多。删除对象删除分两种场景细节不一样# 场景一删除单个对象实例 spot ScenicSpot.objects.filter(id5).first() if spot: spot.delete() # 场景二按条件批量删除直接对QuerySet调用delete Comment.objects.filter(user_id3).delete() # 场景三关联删除因为外键设置了CASCADE删除景点后收藏和评论也会自动删除 spot ScenicSpot.objects.filter(id5).first() if spot: spot.delete()Django ORM的delete方法会返回一个元组格式是(删除的总条数, {应用名.模型名: 删除条数})。你可以打印出来看看会清楚地看到级联删除了哪些关联数据。这是个非常实用的调试信息。有个坑必须提醒如果你用User模型自带的delete()Django不会走数据库的级联删除而是靠ORM自己模拟。所以在模型里设置了on_deletemodels.CASCADE删除父表对象时子表数据会被一并处理但是你自己写裸SQL或者在Admin里直接删操作就没有这保证了。总之业务代码里的删除操作放心交给ORM就好。3.4 视图层返回JSON接口的正确写法前后端分离项目里Django视图只做一件事接收前端请求查数据库拼成JSON返回。我推荐新手先用函数视图FBV简单直接好理解。下面是一个标准的景点列表接口import json from django.http import JsonResponse from django.views.decorators.http import require_GET, require_POST from django.contrib.auth.decorators import login_required from .models import ScenicSpot require_GET def scenic_list(request): city request.GET.get(city, ) keyword request.GET.get(keyword, ) spots ScenicSpot.objects.all() if city: spots spots.filter(citycity) if keyword: spots spots.filter(name__containskeyword) data [] for spot in spots: data.append({ id: spot.id, name: spot.name, city: spot.city, price: str(spot.price), description: spot.description, image: request.build_absolute_uri(spot.image.url) if spot.image else , open_time: spot.open_time, category: spot.category, }) return JsonResponse({code: 0, data: data, message: success})这里有个小细节值得说DecimalField类型的price直接放进JSON里会报错所以用str()转成字符串。另外图片路径一定要用request.build_absolute_uri拼成绝对路径否则前端拿到的只是“/media/scenic/xx.jpg”没法直接访问。如果你学习能力强也可以尝试用Django REST Framework写CBV这个框架做序列化会省很多代码。但对于旅游管理系统这个体量手写序列化完全够用而且不引入额外框架答辩时对代码的讲解也更可控。3.5 URL路由配置和登录Token方案路由配置不难但是要注意include的用法。在项目根路由里注册子路由from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns [ path(admin/, admin.site.urls), path(api/, include(travel.urls)), # 所有接口都挂上/api前缀 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)然后在travel应用里新建urls.pyfrom django.urls import path from . import views urlpatterns [ path(scenic/list/, views.scenic_list, namescenic_list), # 其他接口按需添加 ]登录和Token这块热词里有“django cookie 设置 token”我建议最直接的做法是用Django自带的session认证加Token校验双轨制登录成功后后端调用request.session保存用户信息同时给前端返回一个token前端存到localStorage后续需要登录状态的接口前端在Header里带上token后端解析一种简单的token生成方式import hashlib import time def generate_token(user): # 生成一个简单的token实际项目可以用itsdangerous或PyJWT raw f{user.id}-{user.username}-{time.time()} return hashlib.sha256(raw.encode(utf-8)).hexdigest()不过说实话课程设计级别的项目我最常用的还是用Django自带的session直接做登录态前端用axios带cookies就行。不需要搞复杂JWT体系。如果一定要token那建议直接装PyJWT库按JWT标准来写不要自己发明加密方案。3.6 跨域问题前后端分离第一道鬼门关自己在浏览器里打开前端页面试接口时十有八九会遇到CORS报错。因为在Django后端跑在8000端口Vue开发服务器跑在8080端口两个端口不同跨域了。解决方法有两个方案方案一开发环境推荐Vue的vue.config.js里配置代理让前端请求都走8080端口转发到8000浏览器感知不到跨域。配置如下const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这样前端请求“/api/scenic/list/”代理服务器自动转发到Django那边后端不用做任何跨域配置。这个方案在开发和打包后部署两种场景下都比较省心。方案二后端装上django-cors-headers在settings.py里配置允许的域名。适合后端接口直接被其他域名的前端调用时使用。如果你做的是演示项目建议方案一为主省得Django那边配CORS时出现options预检请求一堆幺蛾子。4. Vue前端页面搭建、路由跳转和数据联调一条龙后端接口能返回JSON了现在轮到前端上场。这块结合热搜词里“vue路由”、“slot vue”、“vue入门”、“vue样式”这些点我把前端的骨架搭法讲清楚。4.1 创建Vue项目并配置基础结构我用Vue CLI创建项目vue create travel_frontend选择Vue 3版本进入项目后先装好需要的依赖npm install axios vue-router4 element-plusaxios负责发HTTP请求vue-router是路由管理器element-plus是一套组件库用起来省得自己写大量样式。项目的关键目录结构travel_frontend/ ├── src/ │ ├── router/index.js # 路由配置 │ ├── views/ # 页面组件 │ │ ├── HomeView.vue │ │ ├── ScenicList.vue │ │ ├── ScenicDetail.vue │ │ ├── LoginView.vue │ │ ├── RegisterView.vue │ │ └── AdminView.vue │ ├── components/ # 公共组件 │ │ └── ScenicCard.vue │ ├── api/index.js # 接口请求统一封装 │ └── App.vue4.2 路由配置页面跳转和守卫Vue Router是单页应用的核心页面切换不刷新、不重新加载全部资源。路由配置里最容易忽略的是“动态路由”和“路由守卫”。先看基础路由配置import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue import ScenicList from ../views/ScenicList.vue import ScenicDetail from ../views/ScenicDetail.vue import LoginView from ../views/LoginView.vue import RegisterView from ../views/RegisterView.vue const routes [ { path: /, name: home, component: HomeView }, { path: /scenic, name: scenic_list, component: ScenicList }, { path: /scenic/:id, name: scenic_detail, component: ScenicDetail, props: true }, { path: /login, name: login, component: LoginView }, { path: /register, name: register, component: RegisterView }, ] const router createRouter({ history: createWebHistory(), routes }) // 路由守卫没登录时不能访问需要登录的页面 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login }) } else { next() } }) export default router说下动态路由就是“/scenic/:id”这种一个路径对应一个详情页根据id不同展示不同内容。在页面组件里可以用this.$route.params.id或者route.params.id取到这个id然后调后端接口。这里有个新手常见错误上一个详情页的数据会残留因为组件被复用了。解决办法是在watch里监听route变化再次请求数据。4.3 列表页开发从axios封装到渲染数据我习惯先把所有接口调用统一封装到一个文件里避免每个组件都写一个完整的axios配置import axios from axios const request axios.create({ baseURL: /api, // 开发环境走代理所以这里直接用/api timeout: 10000 }) // 请求拦截器自动带上token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { console.error(请求出错, error) return Promise.reject(error) } ) export const getScenicList (params) request.get(/scenic/list/, { params }) export const login (data) request.post(/user/login/, data) export const register (data) request.post(/user/register/, data)然后景点列表页调用接口、渲染数据template div classscenic-page h2热门景点/h2 div classfilter-bar el-select v-modelcity placeholder选择城市 changeloadData el-option label全部 value / el-option label北京 value北京 / el-option label杭州 value杭州 / el-option label成都 value成都 / /el-select /div div classscenic-grid ScenicCard v-forspot in spots :keyspot.id :namespot.name :cityspot.city :pricespot.price :imagespot.image :idspot.id / /div /div /template script setup import { ref, onMounted } from vue import { getScenicList } from ../api/index.js import ScenicCard from ../components/ScenicCard.vue const spots ref([]) const city ref() async function loadData() { const res await getScenicList({ city: city.value }) if (res.code 0) { spots.value res.data } } onMounted(() { loadData() }) /script这里有个细节vue.config.js里的代理配置只对开发服务器有效。如果你把前端打包后用Nginx部署或者扔到Django的static目录里路径就会变成实际后端地址代理配置就不生效了。所以上线部署时要么把接口baseURL直接改成后端IP加端口要么让Nginx同样配置一层反向代理。4.4 详情页、收藏和评论的交互逻辑景点详情页比列表页多几个交互展示景点图片如果是Vue 3直接用:src绑定地址如果后端返回的相对路径前面要拼上后端域名收藏按钮点击后判断用户是否登录没登录跳登录页登录了就调收藏接口评论区展示已有评论用户可以提交新评论这里有个很常见的报错就是图片显示404。排查思路一般两条一是后端MEDIA_ROOT和MEDIA_URL有没有配好二是前端用的地址是不是绝对路径。别一上来就怀疑前端写法先把接口返回的地址复制到浏览器地址栏直接访问能打开就是前端问题打不开就是后端没配好。4.5 Vue视图更新和数据绑定容易踩的小坑Vue 3的reactive封装了响应式数据但是如果你给数组直接用索引赋值比如spots[0] {...}是不会触发视图更新的。要用push、splice或者整个替换数组。这个知识点基本每个Vue项目里都会遇到一次。还有很多人喜欢在模板里写很长的逻辑表达式我看了不少学弟代码一个v-for的循环里嵌套三四个v-if性能差不说可读性也差。建议适当抽成计算属性或者方法代码会清爽很多。4.6 slot插槽的用法组件复用里的小技巧热搜词里有“slot vue”这个知识点在开发公共组件时特别有用。以ScenicCard为例如果你想实现“卡片默认显示景点信息但个别地方需要在卡片底部额外加一块内容比如收藏按钮、删除按钮”就可以用插槽template div classscenic-card img :srcimage :altname / h3{{ name }}/h3 p{{ city }} - ¥{{ price }}/p slot nameextra/slot /div /template使用的时候ScenicCard v-forspot in spots ... template #extra el-button typedanger clickcollectSpot(spot.id)收藏/el-button /template /ScenicCard插槽让你的组件复用性大增一套卡片组件就能在不同页面展示不同操作按钮不用复制粘贴一堆重复代码。5. 联调和部署阶段的实战踩坑这些问题至少浪费我一下午写代码最耗时间的不是写是调。下面几个坑是学生项目里最高频的我在帮人改项目的过程中一遍又一遍地遇到直接列出来对照查。5.1 Django的CSRF验证导致的POST请求403当前端用axios往Django发POST请求时如果Django的CSRF中间件开启默认开启请求会返回403。因为Django要求POST请求必须带csrfmiddlewaretoken而前后端分离项目一般不会自动带上。解决方法一般有三种在视图函数上添加csrf_exempt装饰器from django.views.decorators.csrf import csrf_exempt csrf_exempt require_POST def login_view(request): ...在settings.py里把CsrfViewMiddleware注释掉。这个方法简单粗暴但安全性下降课程设计答辩时如果老师懂行会扣分不推荐。前端请求时获取csrftoken并放到header里。需要后端配置一个获取token的接口比较费事。开发效率优先的话推荐方案一在每个POST接口上加csrf_exempt。等以后去企业实习了再慢慢学完整的CSRF防护逻辑。5.2 图片上传失败页面刷新图片又丢了这个问题的根源通常是前端把图片转成base64之后没有正确处理或者后端ImageField没有配MEDIA_ROOT。一个稳定的做法是后端提供专门的上传接口csrf_exempt require_POST def upload_image(request): file request.FILES.get(image) if not file: return JsonResponse({code: 1, message: 没有接收到文件}) # 直接交给ImageField的存储逻辑 spot ScenicSpot(imagefile) spot.save() url request.build_absolute_uri(spot.image.url) return JsonResponse({code: 0, data: {url: url}})记得在settings.py里配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media不然存不了文件或者存了访问不了。5.3 Vue打包后接口地址写死前端开发时用代理很爽但打包后dist里的文件是纯静态的没有代理环境。如果你直接把dist扔给Django前端请求/api开头的地址就会落到Django的域名下但Django并没有/api代理转发能力。所以打包前一定记得把api/index.js里的baseURL改回后端真实地址比如http://127.0.0.1:8000或者更专业的写法是基于环境变量的判断const baseURL process.env.NODE_ENV production ? http://127.0.0.1:8000/api : /api这个坑查起来最冤前端看着代码没错后端接口也能访问但前端请求过去就是404。5.4 pip安装慢、装不上包怎么办热搜词里有“pycharm怎么安装pandas包”、“python安装sklearn库”说明很多人在Pycharm里手动画包。Pycharm确实提供了一个可视化的包管理界面但有时候会因为网络问题装上半天没反应。命令行才是最快的路子激活虚拟环境后执行pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django国内镜像一把梭。pip如果提示版本太低记得先执行python -m pip install --upgrade pip升级一下。5.5 Django执行查询和删除时报错“TypeError: xxx object is not iterable”或其他常见报错新手问得最多的几个ORM相关报错我总结一下查询没有结果返回None然后继续调用.id属性报AttributeErrorfilter的字段名写错报FieldErrorDjango会提示“Cannot resolve keyword”在模板里遍历一个普通对象而不是QuerySet报TypeError这些都是对ORM返回类型理解不够清楚导致的。记住三点all()和filter()返回QuerySet可以遍历、可以切片get()返回模型实例单数没有列表操作first()返回模型实例或None。6. 后端选型的另一条路如果老师指定了Flask怎么平移说句实在话Flask本身并不难难在于它没有Django那么齐全的“全家桶”什么东西都要自己拼。但如果你已经用Django做完了一版老师突然说要用Flask或者你自己非要两开花那平移思路是清晰的。6.1 Flask的核心依赖替换DjangoFlask生态django.db.modelsORMFlask-SQLAlchemydjango.contrib.auth认证Flask-Login 或自己写装饰器django-views serializerFlask路由函数 手动jsonifydjango-admin后台Flask-Admin可选CSRF中间件Flask-WTF或忽略以查询和删除为例Django的写法spots ScenicSpot.objects.filter(city北京).delete()换成Flask-SQLAlchemyspots ScenicSpot.query.filter_by(city北京).all() for spot in spots: db.session.delete(spot) db.session.commit()6.2 Flask项目的最小结构一个Flask写旅游系统后端的最小代码框架from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///travel.db db SQLAlchemy(app) class ScenicSpot(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100)) city db.Column(db.String(50)) price db.Column(db.Numeric(8, 2)) app.route(/api/scenic/list/, methods[GET]) def scenic_list(): city request.args.get(city, ) query ScenicSpot.query if city: query query.filter_by(citycity) spots query.all() return jsonify({code: 0, data: [{ id: s.id, name: s.name, city: s.city, price: str(s.price) } for s in spots]})Flask的部署比Django简单用自带的开发服务器就能跑如果想要更稳定可以上gunicorn。如果你之前没写过Flask我建议不要在一开始就同时学Django和Flask。先把Django这套跑通Flask作为拓展思路了解一下即可。两个都学只会让人混乱因为它们在设计哲学上的差异比代码上的差异大得多。7. WebSocket和实时推送如何让你的系统在答辩时加印象分当初看到热词里有“django websocket实现后台有数据前端推送”的时候我愣了一下仔细想想这是很多人在做管理系统时会搜索的点。如果你想让自己的旅游系统不止于增删改查加一个实时功能比如“用户提交评论之后所有在线用户都能实时看到新评论”或者“后台某个热门景点票务紧张时给用户推送提醒”就可以用WebSocket来做。7.1 Django实现WebSocket的常见路径Django自带的开发服务器是不支持WebSocket的你需要用Django Channels官方推荐的异步扩展或者用第三方库django-sockjs之类。Channels是主流方案但整套配置对新手有一定门槛需要装channels、channels-redis还要配置ASGI应用。整个思路是这样项目从WSGI模式切换到ASGI模式创建一个WebSocket消费者Consumer处理连接、断开、接收消息前端用Vue里的WebSocket对象建立连接监听消息并实时更新页面7.2 简化版方案不用Channels也能做的“伪实时推送”说实话课程设计里的实时需求用轮询就够用了。所谓轮询就是前端每隔几秒调一次接口看看有没有新数据。虽然不优雅但实现成本极低、稳定可靠setInterval(async () { const res await getCommentList(scenicId) comments.value res.data }, 5000)5秒刷新一次演示时效果几乎等同实时但代码量只有WebSocket方案的十分之一。如果非要真WebSocket学一下我建议单独做个小Demo练手不要一开始就硬塞进旅游系统里。先把Channels的官方文档通读一遍照着教程起一个简单的聊天室再迁移过来。WebSocket本身的坑——比如连接断开重连、心跳保活、身份鉴权——都是独立的复杂度没法跳过。7.3 用轮询还是WebSocket怎么跟答辩老师说答辩时如果被问“为什么你的系统没有用WebSocket”你可以说当前系统对实时性的需求不强评论和收藏的延迟在几秒内不影响体验因此采用了轮询策略来降低系统复杂度如果后续需要做即时消息通知可以平滑升级到WebSocket方案。这个回答既说明你懂WebSocket又会权衡技术选型反而比硬上一个半吊子的WebSocket加分。8. 项目从开发到交付代码整理、数据库备份和演示准备的完整清单很多学生把功能写完就觉得结束了结果交作业或者答辩的时候手忙脚乱。这一节说说我每次带项目时最后一周会盯着学生做的那些事情。8.1 代码整理的规范删除无用代码和注释那种“# 这里试一下”、“print(1)调试”注释该删就删添加基本的注释每个视图函数写清楚接口的作用模型字段写清楚含义整理requirements.txtpip freeze requirements.txt这个文件不整理别人复现你项目的时候第一步就杵在那。README写清楚启动步骤里面至少包含Python版本、如何创建虚拟环境、如何安装依赖、如何迁移数据库、创建后台管理员、启动后端、启动前端。别觉得自己清楚就行两周后你自己也忘了。8.2 数据库和演示数据的准备这里有个很多新手不做但特别重要的步骤预置演示数据。你的系统给老师演示的时候数据库里最好已经有10个以上的景点、5条路线、若干条评论和收藏记录。要是打开个空壳系统全程都在演示注册和添加数据效果会大打折扣。预置数据的方法后台Admin界面手动录入最直观但特别浪费时间写一个management command用脚本批量生成数据用Django Fixture先把数据准备好然后dumpdata到json文件别人只需要loaddata就能复现我个人推荐写个一次性脚本import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, travel_backend.settings) django.setup() from travel.models import ScenicSpot spots_data [ {name: 故宫博物院, city: 北京, price: 60, description: 世界上现存规模最大、保存最为完整的木质结构古建筑群之一。}, {name: 西湖, city: 杭州, price: 0, description: 以其秀丽的湖光山色和众多的名胜古迹而闻名中外。}, {name: 九寨沟, city: 阿坝, price: 169, description: 以翠海、叠瀑、彩林、雪峰、藏情、蓝冰“六绝”著称。}, ] for item in spots_data: ScenicSpot.objects.create(**item)跑一遍脚本数据就有了。这个脚本甚至可以直接放进README里告诉别人怎么初始化演示数据。8.3 答辩演示的完整流程设计演示系统的时候别从注册开始演示那会浪费时间。合理顺序是先打开系统首页展示景点列表点进景点详情展示图片信息、评论演示登录然后收藏一个景点打开后台管理演示添加一个景点并设置上线回到前端刷新页面演示新增的景点出现如果做了搜索演示按城市搜索整套演示控制在5到8分钟逻辑链是完整的前台浏览、用户交互、后台管理、数据联动。答辩老师爱问的几个问题要提前准备“为什么选Django/Flask”——基于开发效率、生态完善度、ORM方便做课程设计体量的系统“为什么用前后端分离”——前端交互体验好、接口可复用、符合工程化实践“数据库表是怎么设计的”——按业务对象拆成用户、景点、路线、评论、收藏说明外键关系和级联逻辑“系统的安全性考虑”——从用户认证、CSRF、权限控制几个维度回答哪怕只做了基础也要能说出来8.4 记录开发过程中的图片和日志毕设和课程设计都需要结题报告或者论文里面要放系统截图。我强烈建议从第一天开始就随手截图建好项目结构后截一张数据库表创建成功截一张每一块功能跑通后截一张前端效果图把过程中遇到的报错截图也存档写“遇到的问题与解决”章节时很用等最后写报告这些素材都是现成的不用回头补。9. 把几星期踩过的坑浓缩成最后几点心得带过几轮做这种选题的学生之后我最大的体会是这类管理系统项目难度不在任何一个单独的技术点上而在把这些技术点串起来的过程。写一个Django接口不难写一个Vue页面也不难但是当你的知识是零散的任何一个中间环节——跨域、联调、打包、部署——都能卡住你一整天。我的具体建议是这样的第一模型设计阶段一定要慢。拿一张纸把你想要的业务实体写下来标清楚字段和外键关系。表结构设计对了后面几乎是水到渠成表结构设计错了改一次连带前端全部要跟着改。第二前端页面不要贪多求全。Vue的组件和页面做一个能跑通的完整闭环列表页→详情页→登录→收藏比堆五个没做完的半成品页面强得多。每次演示和答辩评委看的是逻辑完整性和核心功能是否可用不是页面数量。第三接口返回的数据格式一开始就统一。比如固定用{code: 0, data: ..., message: success}这套包裹结构前端和后端都按这个约定走联调的时候能省很多扯皮的精力。写时间长了就会发现接口约定比接口数量重要得多。第四Git不熟练没关系但一定要学会经常备份。最简单的方式就是定期把整个项目压缩存档或者用Gitee建个私有仓库每完成一个模块就commit一次。关键词别到写坏了一天代码之后才想起来“刚才那版还能跑”。第五如果你时间很紧优先保证后端接口的正确性前端样式可以放一放。功能能跑起来数据能通已经80分了。反过来如果前端花里胡哨但接口调不通演示的时候一戳就穿帮。样式是锦上添花不是雪中送炭。最后说一个我个人的体会做这个项目最有价值的不是那个分数而是你第一次亲手把一个从前到后的完整系统跑通的经验。它让你看见一个Web应用到底是由哪些零件组成的这些东西在未来任何一门和后端、前端有关的工作里都会反复用到。慢慢写扎实跑通每个环节项目结束时你会发现自己已经在不知不觉中跨过了“只会抄代码”到“能独立搭一个系统”的分界线。