
简介这是一套面向计算机专业本科生的毕业设计级酒店预订系统实战项目基于Python全栈技术栈构建适用于课程设计、毕设选题与Web开发能力进阶学习。项目采用B/S架构后端以Django框架实现业务逻辑与数据管理前端使用Vue.js构建响应式界面完整覆盖用户预订、订单处理、客房运营及后台管理全流程。资源包共391个文件含32个核心Python后端模块、28个Vue组件、49个TypeScript脚本、145张JPEG与37张PNG格式的界面截图及图标资源以及SVG矢量图、LESS样式文件和系统配置文档等总大小33.19MB结构清晰分为serverDjango与webVue两大目录。目前已有249人学习下载提供开箱即用的源码、完整依赖清单、标准化部署说明及典型页面预览图便于快速理解MVC分层设计、前后端分离实践与酒店业务建模逻辑。1. 为什么用 Django Vue 做酒店预订系统比纯 Flask 或原生 JS 更稳、更易毕业答辩这不是一个“炫技型”全栈项目而是一个边界清晰、交付可控、答辩有话说的毕业设计典型路径后端用 Django 提供强约束的 REST API 和 Admin 后台前端用 Vue 实现响应式预订流程房型筛选、日期选择、订单提交、用户中心两者通过标准 JSON 接口解耦。很多同学踩坑在「用 Flask 自搭路由Jinja 模板硬写页面」结果登录态混乱、跨域报错、分页逻辑散落各处也有用 React 但 npm run build 后丢进 static 目录发现静态资源 404——而 Django Vue 的组合天然适配「前后端分离但部署一体」的毕设场景Vue 打包产物直接扔进 Django 的static/Django 负责路由兜底和 API 鉴权连 Nginx 都不用配。我带过 12 届毕设90% 用这套方案的同学在答辩时能清晰讲出「为什么选 Django 而不是 FastAPI缺少 Admin 和 ORM 内置权限」「为什么 Vue 3 Composition API 比 Options API 更适合管理预订表单状态」——这比堆功能更重要。如果你的目标是两周跑通核心流程、三天调通支付模拟、答辩前两天还能加个房态日历图那这个标题不是套话是经过验证的最小可行技术栈。2. 搭建 Django 后端从零初始化到可运行的预订 API2.1 创建虚拟环境与 Django 项目骨架Python 3.8 是硬性要求酒店预订涉及日期计算、并发库存扣减、用户权限分级Django 3.2 的AsyncView和select_for_update()对这些场景支持更成熟。务必避开 Python 3.12部分 Django 插件尚未兼容。以下命令在终端执行# 创建独立虚拟环境避免污染全局 Python python -m venv venv_hotel source venv_hotel/bin/activate # Linux/macOS # venv_hotel\Scripts\activate.bat # Windows # 升级 pip 并安装核心依赖 pip install --upgrade pip pip install django4.2.13 djangorestframework3.14.0 django-crispy-forms2.0 django-filter23.5提示django4.2.13是 Django 4.2.x 的最后一个 LTS 版本官方维护至 2026 年毕设项目首选。不要盲目追新到 5.xasgiref依赖冲突会让你在答辩前夜 debug 到凌晨。2.2 初始化核心 Apphotels、bookings、users三模块拆分逻辑Django 项目结构清晰度直接决定答辩时能否说清「数据流向」。按业务域而非技术层建 App# 在 manage.py 同级目录执行 django-admin startproject hotel_booking . python manage.py startapp hotels python manage.py startapp bookings python manage.py startapp users然后在settings.py中注册# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 第三方 rest_framework, crispy_forms, django_filters, # 自定义 hotels, bookings, users, ]2.3 定义酒店与房型模型用DateTimeField和DecimalField精确表达业务语义酒店预订最怕「时间精度丢失」和「价格四舍五入错误」。Django 模型必须显式声明字段类型和约束# hotels/models.py from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator class Hotel(models.Model): name models.CharField(max_length100, verbose_name酒店名称) address models.TextField(verbose_name地址) star_rating models.DecimalField( max_digits2, decimal_places1, validators[MinValueValidator(1), MaxValueValidator(5)], verbose_name星级 ) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class RoomType(models.Model): hotel models.ForeignKey(Hotel, on_deletemodels.CASCADE, related_nameroom_types) name models.CharField(max_length50, verbose_name房型名称) # 如 豪华大床房 price_per_night models.DecimalField( max_digits8, decimal_places2, verbose_name nightly price ) capacity models.PositiveSmallIntegerField(default2, verbose_name容纳人数) available_count models.PositiveIntegerField(default10, verbose_name当前可订数量) class Meta: unique_together (hotel, name) # 同一酒店下房型名唯一 def __str__(self): return f{self.hotel.name} - {self.name}参数说明max_digits8, decimal_places2确保价格支持 999999.99 元避免 float 导致的0.1 0.2 ! 0.3unique_together防止同一酒店重复添加“标准间”related_nameroom_types让hotel.room_types.all()可读性远超hotel.roomtype_set.all()。2.4 编写预订 API用 DRF Serializer 处理日期校验与库存原子扣减预订接口必须解决三个关键问题用户选的入住/离店日期是否合法入住不能早于今天、离店必须晚于入住所选房型在该时段是否还有库存扣减库存时防止超卖数据库行锁。# bookings/serializers.py from rest_framework import serializers from datetime import date from .models import Booking from hotels.models import RoomType class BookingCreateSerializer(serializers.ModelSerializer): room_type_id serializers.PrimaryKeyRelatedField( querysetRoomType.objects.all(), sourceroom_type, write_onlyTrue ) class Meta: model Booking fields [room_type_id, check_in, check_out, guest_name, guest_phone] def validate(self, data): check_in data[check_in] check_out data[check_out] if check_in date.today(): raise serializers.ValidationError(入住日期不能早于今天) if check_out check_in: raise serializers.ValidationError(离店日期必须晚于入住日期) if (check_out - check_in).days 30: raise serializers.ValidationError(单次预订最长30天) # 库存预检查询该房型在该时段已被预订的数量 from django.db.models import Sum booked_count Booking.objects.filter( room_typedata[room_type], check_in__ltcheck_out, check_out__gtcheck_in, statusconfirmed ).aggregate(totalSum(quantity))[total] or 0 if booked_count data[room_type].available_count: raise serializers.ValidationError(该房型在所选时段已售罄) return data def create(self, validated_data): # 关键使用 select_for_update() 加行锁避免并发超卖 room_type validated_data[room_type] with transaction.atomic(): room_type RoomType.objects.select_for_update().get(idroom_type.id) if room_type.available_count 0: raise serializers.ValidationError(库存不足请刷新重试) room_type.available_count - 1 room_type.save() booking Booking.objects.create(**validated_data, statusconfirmed) return booking逻辑说明validate()中的check_in__ltcheck_out和check_out__gtcheck_in是 Django ORM 的「区间重叠判断」标准写法比手动循环查每一天高效百倍select_for_update()必须包裹在transaction.atomic()内否则锁无效booked_count计算的是「已确认订单」不包含pending或cancelled状态符合真实业务。2.5 配置 URL 与视图用ModelViewSet快速暴露 CRUD但禁用危险操作# bookings/views.py from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from .models import Booking from .serializers import BookingCreateSerializer, BookingListSerializer class BookingViewSet(viewsets.ModelViewSet): queryset Booking.objects.filter(statusconfirmed) serializer_class BookingListSerializer # 用于 list/retrieve def get_serializer_class(self): if self.action create: return BookingCreateSerializer return self.serializer_class def create(self, request, *args, **kwargs): serializer self.get_serializer(datarequest.data) serializer.is_valid(raise_exceptionTrue) self.perform_create(serializer) headers self.get_success_headers(serializer.data) return Response(serializer.data, statusstatus.HTTP_201_CREATED, headersheaders) # 禁用 delete 和 update毕设阶段用状态机替代 def destroy(self, request, *args, **kwargs): return Response({detail: 不支持直接删除订单请联系客服取消}, statusstatus.HTTP_405_METHOD_NOT_ALLOWED) def update(self, request, *args, **kwargs): return Response({detail: 订单信息不可修改请重新提交}, statusstatus.HTTP_405_METHOD_NOT_ALLOWED)# bookings/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from . import views router DefaultRouter() router.register(rbookings, views.BookingViewSet, basenamebooking) urlpatterns [ path(api/v1/, include(router.urls)), ]为什么禁用destroy和update毕设答辩时老师会问「如果用户要取消订单怎么保证财务一致性」——你答「用status字段标记为 cancelled并触发退款逻辑」比「直接删库」专业十倍。这就是用状态机思维替代 CRUD 思维的体现。3. 构建 Vue 前端从 Vue CLI 初始化到与 Django API 对接3.1 初始化 Vue 3 项目并配置代理解决跨域开发期关键Vue 3.4 的script setup语法让组件逻辑更聚焦且vite-plugin-pages支持文件系统路由比手写router/index.js更适合毕设快速迭代# 在项目根目录外新建 frontend 文件夹 cd .. vue create frontend # 选择Manually select features → TS, Router, Pinia, ESLint # 使用 Vue 3.x, History mode for router, ESLint Prettier cd frontend npm install axios1.6.7 vueuse/core10.6.1然后配置vite.config.ts解决开发期跨域// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://127.0.0.1:8000, // Django 开发服务器地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })注意rewrite: (path) path.replace(/^\/api/, )是关键它让 Vue 请求/api/v1/bookings/时Vite 代理转发到http://127.0.0.1:8000/v1/bookings/而不是.../api/v1/bookings/Django 路由没/api前缀。很多同学卡在这一步反复看到 404。3.2 封装 Axios 实例统一处理 Token、错误拦截与 Loading 状态酒店预订需要用户登录态Django 默认用 Session但 Vue 前端更适合用 Token简化跨域。我们用djangorestframework-simplejwt# Django 端安装 pip install djangorestframework-simplejwt# settings.py from datetime import timedelta REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], } SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours1), REFRESH_TOKEN_LIFETIME: timedelta(days7), }Vue 端封装请求// src/utils/request.ts import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api/v1/, timeout: 10000, }) // 请求拦截自动携带 token request.interceptors.request.use( (config) { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) Promise.reject(error) ) // 响应拦截统一错误处理 request.interceptors.response.use( (response) response, (error) { if (error.response?.status 401) { localStorage.removeItem(access_token) window.location.href /login } else if (error.response?.data?.detail) { ElMessage.error(error.response.data.detail) } else { ElMessage.error(网络错误请检查服务是否启动) } return Promise.reject(error) } ) export default request3.3 实现房型列表页用useAsyncState管理加载状态与分页!-- src/views/RoomList.vue -- script setup langts import { ref, onMounted } from vue import request from /utils/request import type { RoomType } from /types const roomTypes refRoomType[]([]) const loading ref(true) const page ref(1) const total ref(0) const loadRooms async () { loading.value true try { const res await request.get(/hotels/room-types/, { params: { page: page.value } }) roomTypes.value res.data.results total.value res.data.count } catch (err) { console.error(err) } finally { loading.value false } } onMounted(() { loadRooms() }) /script template div classroom-list h2可预订房型/h2 el-skeleton :loadingloading animated div v-forroom in roomTypes :keyroom.id classroom-card h3{{ room.hotel.name }} - {{ room.name }}/h3 p¥{{ room.price_per_night }}/晚 · {{ room.capacity }}人/p el-button typeprimary clickgoToBooking(room.id) 预订 /el-button /div /el-skeleton /div /template关键点el-skeleton是 Element Plus 的骨架屏组件比v-if!loading更专业——它告诉评委「我在做性能优化」而不是「我还没写完」。3.4 实现预订表单用useForm管理校验与提交!-- src/views/BookingForm.vue -- script setup langts import { reactive, ref } from vue import { ElMessage } from element-plus import request from /utils/request interface FormData { room_type_id: number check_in: string check_out: string guest_name: string guest_phone: string } const form reactiveFormData({ room_type_id: 0, check_in: , check_out: , guest_name: , guest_phone: }) const submitting ref(false) const submit async () { if (!form.check_in || !form.check_out) { ElMessage.error(请选择入住和离店日期) return } submitting.value true try { await request.post(/bookings/, form) ElMessage.success(预订成功请查收短信通知) // 重置表单 Object.assign(form, { room_type_id: 0, check_in: , check_out: , guest_name: , guest_phone: }) } catch (err) { console.error(err) } finally { submitting.value false } } /script template el-form :modelform label-width120px submit.preventsubmit el-form-item label入住日期 el-date-picker v-modelform.check_in typedate placeholder选择日期 :disabled-datedate date new Date() / /el-form-item el-form-item label离店日期 el-date-picker v-modelform.check_out typedate placeholder选择日期 :disabled-datedate date new Date(form.check_in) / /el-form-item el-form-item label客人姓名 el-input v-modelform.guest_name / /el-form-item el-form-item label手机号 el-input v-modelform.guest_phone / /el-form-item el-form-item el-button typeprimary :loadingsubmitting提交预订/el-button /el-form-item /el-form /template玄学细节:disabled-datedate date new Date(form.check_in)中的是关键——离店日期必须严格大于入住日期会导致同一天无法预订酒店行业规则至少住一晚。4. 前后端联调与部署让 Vue 打包产物被 Django 正确服务4.1 Vue 打包配置输出到 Django 的 static 目录路径需对齐Vite 默认打包到dist/但 Django 需要静态资源在static/下。修改vite.config.ts// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], build: { outDir: ../hotel_booking/static/frontend, // 注意路径回到 Django 项目根目录进入 static/frontend emptyOutDir: true, }, server: { port: 3000, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })然后执行cd frontend npm run build # 生成文件到 ../hotel_booking/static/frontend/4.2 Django 配置静态文件服务STATICFILES_DIRS与collectstatic流程# settings.py import os from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent.parent # 回到项目根目录 STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / hotel_booking / static, # Django 自身静态文件如 admin CSS BASE_DIR / hotel_booking / static / frontend, # Vue 打包产物 ] STATIC_ROOT BASE_DIR / staticfiles # 生产环境 collectstatic 输出目录创建hotel_booking/urls.py路由兜底# hotel_booking/urls.py from django.contrib import admin from django.urls import path, include, re_path from django.views.generic import TemplateView from django.conf import settings from django.conf.urls.static import static urlpatterns [ path(admin/, admin.site.urls), path(api/v1/, include(bookings.urls)), path(api/v1/, include(hotels.urls)), # Vue 路由兜底所有非 API 请求都返回 index.html re_path(r^.*$, TemplateView.as_view(template_nameindex.html)), ] static(settings.STATIC_URL, document_rootsettings.STATIC_ROOT)为什么用re_path(r^.*$, ...)Vue Router 是 history 模式访问/booking/123时浏览器发请求到 DjangoDjango 必须返回index.html再由 Vue Router 解析路径。若用path()会 404。4.3 开发与生产环境切换用.env控制 DEBUG 和 ALLOWED_HOSTS创建.env文件Django 项目根目录# .env DEBUGTrue ALLOWED_HOSTS127.0.0.1,localhost SECRET_KEYyour-secret-key-here-change-in-production然后在settings.py顶部加载# settings.py import os from pathlib import Path from decouple import config # pip install python-decouple BASE_DIR Path(__file__).resolve().parent.parent.parent DEBUG config(DEBUG, defaultFalse, castbool) ALLOWED_HOSTS config(ALLOWED_HOSTS, default127.0.0.1).split(,) SECRET_KEY config(SECRET_KEY)血泪经验ALLOWED_HOSTS必须包含localhostVue 开发服务器域名和127.0.0.1Django 本地服务 IP缺一不可。否则Cross-Origin Request Blocked错误会让你怀疑人生。4.4 运行全流程一条命令启动 DjangoVue 自动热更新# 终端 1启动 Django cd hotel_booking python manage.py runserver 8000 # 终端 2启动 Vue 开发服务器自动代理到 Django cd frontend npm run dev此时访问http://localhost:3000即可看到 Vue 页面所有 API 请求自动转发到http://localhost:8000/api/...无需任何额外配置。5. 避坑指南90% 毕设同学栽在这 5 个地方5.1 现象Vue 页面空白控制台报Failed to fetch或net::ERR_CONNECTION_REFUSED原因Vue 开发服务器未启动或 Django 服务未运行或代理配置错误如target地址写成http://localhost:8000但 Django 绑定在127.0.0.1:8000解决先curl http://127.0.0.1:8000/api/v1/hotels/确认 Django API 可访问再检查vite.config.ts中proxy的target是否为http://127.0.0.1:8000最后确认npm run dev输出的Local:地址是http://localhost:3000不是http://127.0.0.1:3000某些系统 hosts 会干扰。5.2 现象Django Admin 登录后跳转http://localhost:3000/admin/报 404原因Django 的LOGIN_REDIRECT_URL默认指向/admin/但 Vue 路由未配置/admin路径且re_path(r^.*$, ...)优先级高于path(admin/, ...)解决在hotel_booking/urls.py中把re_path放在所有path之后或在settings.py中设置LOGIN_REDIRECT_URL /admin/确保登录后强制走 Django Admin 路由。5.3 现象预订成功但数据库available_count没减少或并发测试时出现超卖原因select_for_update()未在transaction.atomic()内执行或room_type查询未加锁如用get_object_or_404替代select_for_update().get()解决确保select_for_update()调用紧跟在transaction.atomic()内检查room_type RoomType.objects.select_for_update().get(id...)不能先get()再select_for_update()并发测试用locust脚本不要只靠人工点两次。5.4 现象Vue 打包后http://localhost:8000/static/frontend/assets/index-xxx.js404原因STATICFILES_DIRS路径配置错误或npm run build输出目录与 Django 预期不符解决运行python manage.py findstatic frontend看是否能找到index.html若找不到检查STATICFILES_DIRS是否指向../hotel_booking/static/frontend注意..是相对路径确认vite.config.ts的outDir与STATICFILES_DIRS中路径完全一致。5.5 现象登录后localStorage.getItem(access_token)为空但 Django 返回了 token原因Django JWT 登录接口未正确返回access字段或 Vue 请求未处理响应体解决Django 登录视图必须返回{access: xxx, refresh: yyy}Vue 登录代码中res.data.access必须赋值给localStorage不能漏掉.data检查request.post(/auth/login/, {username, password})的响应结构是否匹配。6. 毕设加分技巧用 Django Admin 实现房态日历图与导出报表6.1 在 Django Admin 中嵌入房态日历用django-admin-interface替换默认皮肤很多同学以为 Admin 只能查数据其实它是毕设「可视化亮点」的捷径。安装django-admin-interfacepip install django-admin-interface# settings.py INSTALLED_APPS [ admin_interface, colorfield, # ... 其他 app ] X_FRAME_OPTIONS SAMEORIGIN SILENCED_SYSTEM_CHECKS [security.W019]然后在hotels/admin.py中定制# hotels/admin.py from django.contrib import admin from django.urls import reverse from django.utils.html import format_html from .models import Hotel, RoomType, Booking admin.register(RoomType) class RoomTypeAdmin(admin.ModelAdmin): list_display [name, hotel, price_per_night, available_count, bookings_today] list_filter [hotel, name] search_fields [name, hotel__name] def bookings_today(self, obj): from datetime import date count Booking.objects.filter( room_typeobj, check_in__ltedate.today(), check_out__gtdate.today(), statusconfirmed ).count() url reverse(admin:bookings_booking_changelist) f?room_type__id__exact{obj.id} return format_html(a href{}{} 间占用/a, url, count) bookings_today.short_description 今日占用效果Admin 列表页直接显示「今日占用」链接点击跳转到该房型的所有订单评委一眼看到你懂业务闭环。6.2 导出 Excel 报表用django-import-export一键导出预订明细pip install django-import-export# bookings/admin.py from import_export import resources from import_export.admin import ExportActionModelAdmin from .models import Booking class BookingResource(resources.ModelResource): class Meta: model Booking fields (id, room_type__hotel__name, room_type__name, check_in, check_out, guest_name, status, created_at) admin.register(Booking) class BookingAdmin(ExportActionModelAdmin): resource_classes [BookingResource] list_display [id, room_type, check_in, check_out, guest_name, status] list_filter [status, room_type__hotel, check_in] search_fields [guest_name, guest_phone]答辩话术「老师这个导出功能不是为了炫技而是酒店实际运营中前台每天要导出 Excel 给财务对账。我用django-import-export实现既满足需求又保证数据格式规范。」6.3 用django-apscheduler实现每日房态同步任务进阶可选如果想展示「自动化运维能力」可以加一个定时任务每天凌晨同步房态pip install django-apscheduler# bookings/tasks.py from apscheduler.schedulers.background import BackgroundScheduler from django_apscheduler.jobstores import DjangoJobStore from django_apscheduler.models import DjangoJobExecution from django_apscheduler import util from bookings.models import Booking def sync_room_availability(): 每日同步房型可用数量 from hotels.models import RoomType for room_type in RoomType.objects.all(): # 计算今日被占用数量 occupied Booking.objects.filter( room_typeroom_type, check_in__ltetimezone.now().date(), check_out__gttimezone.now().date(), statusconfirmed ).count() room_type.available_count max(0, room_type.total_count - occupied) room_type.save() # apps.py 中启动调度器 class BookingsConfig(AppConfig): default_auto_field django.db.models.BigAutoField name bookings def ready(self): from django_apscheduler.jobstores import DjangoJobStore from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.executors.pool import ThreadPoolExecutor from django_apscheduler import util scheduler BackgroundScheduler( executors{default: ThreadPoolExecutor(20)}, job_defaults{coalesce: False, max_instances: 3} ) scheduler.add_jobstore(DjangoJobStore(), default) scheduler.add_job( sync_room_availability, cron, hour0, minute0, idsync_room_availability, replace_existingTrue ) scheduler.start()我的习惯毕设答辩时我从不主动提「我用了 APScheduler」但当老师问「房态数据怎么保证实时」我会打开bookings/tasks.py指着sync_room_availability函数说「我们每晚零点自动校准一次比实时监听更稳定也避免了 WebSocket 的复杂度。」——把技术选择包装成「权衡取舍」比堆名词高明得多。希望帮到你。本文还有配套的精品资源点击获取