简介:这是一套面向计算机专业本科生的毕业设计级酒店预订系统实战项目,基于Python全栈技术栈构建,适用于课程设计、毕设选题与Web开发能力进阶学习。项目采用B/S架构,后端以Django框架实现业务逻辑与数据管理,前端使用Vue.js构建响应式界面,完整覆盖用户预订、订单处理、客房运营及后台管理全流程。资源包共391个文件,含32个核心Python后端模块、28个Vue组件、49个TypeScript脚本、145张JPEG与37张PNG格式的界面截图及图标资源,以及SVG矢量图、LESS样式文件和系统配置文档等,总大小33.19MB,结构清晰分为server(Django)与web(Vue)两大目录。目前已有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 后端:从零初始化到可运行的预订 API
2.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 django==4.2.13 djangorestframework==3.14.0 django-crispy-forms==2.0 django-filter==23.5提示:
django==4.2.13是 Django 4.2.x 的最后一个 LTS 版本,官方维护至 2026 年,毕设项目首选。不要盲目追新到 5.x,asgiref依赖冲突会让你在答辩前夜 debug 到凌晨。
2.2 初始化核心 App:hotels、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_length=100, verbose_name="酒店名称") address = models.TextField(verbose_name="地址") star_rating = models.DecimalField( max_digits=2, decimal_places=1, validators=[MinValueValidator(1), MaxValueValidator(5)], verbose_name="星级" ) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name class RoomType(models.Model): hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name='room_types') name = models.CharField(max_length=50, verbose_name="房型名称") # 如 "豪华大床房" price_per_night = models.DecimalField( max_digits=8, decimal_places=2, verbose_name=" nightly price" ) capacity = models.PositiveSmallIntegerField(default=2, verbose_name="容纳人数") available_count = models.PositiveIntegerField(default=10, verbose_name="当前可订数量") class Meta: unique_together = ('hotel', 'name') # 同一酒店下房型名唯一 def __str__(self): return f"{self.hotel.name} - {self.name}"参数说明:
max_digits=8, decimal_places=2确保价格支持 999999.99 元,避免 float 导致的0.1 + 0.2 != 0.3;unique_together防止同一酒店重复添加“标准间”;related_name='room_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( queryset=RoomType.objects.all(), source='room_type', write_only=True ) 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_type=data['room_type'], check_in__lt=check_out, check_out__gt=check_in, status='confirmed' ).aggregate(total=Sum('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(id=room_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, status='confirmed') return booking逻辑说明:
validate()中的check_in__lt=check_out和check_out__gt=check_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(status='confirmed') 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(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) # 禁用 delete 和 update,毕设阶段用状态机替代 def destroy(self, request, *args, **kwargs): return Response({"detail": "不支持直接删除订单,请联系客服取消"}, status=status.HTTP_405_METHOD_NOT_ALLOWED) def update(self, request, *args, **kwargs): return Response({"detail": "订单信息不可修改,请重新提交"}, status=status.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(r'bookings', views.BookingViewSet, basename='booking') 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 axios@1.6.7 @vueuse/core@10.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(hours=1), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), }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 lang="ts"> import { ref, onMounted } from 'vue' import request from '@/utils/request' import type { RoomType } from '@/types' const roomTypes = ref<RoomType[]>([]) 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 class="room-list"> <h2>可预订房型</h2> <el-skeleton :loading="loading" animated> <div v-for="room in roomTypes" :key="room.id" class="room-card"> <h3>{{ room.hotel.name }} - {{ room.name }}</h3> <p>¥{{ room.price_per_night }}/晚 · {{ room.capacity }}人</p> <el-button type="primary" @click="goToBooking(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 lang="ts"> 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 = reactive<FormData>({ 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 :model="form" label-width="120px" @submit.prevent="submit"> <el-form-item label="入住日期"> <el-date-picker v-model="form.check_in" type="date" placeholder="选择日期" :disabled-date="date => date < new Date()" /> </el-form-item> <el-form-item label="离店日期"> <el-date-picker v-model="form.check_out" type="date" placeholder="选择日期" :disabled-date="date => date <= new Date(form.check_in)" /> </el-form-item> <el-form-item label="客人姓名"> <el-input v-model="form.guest_name" /> </el-form-item> <el-form-item label="手机号"> <el-input v-model="form.guest_phone" /> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitting">提交预订</el-button> </el-form-item> </el-form> </template>玄学细节:
:disabled-date="date => 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_name='index.html')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)为什么用
re_path(r'^.*$', ...)?
Vue Router 是 history 模式,访问/booking/123时浏览器发请求到 Django,Django 必须返回index.html,再由 Vue Router 解析路径。若用path()会 404。
4.3 开发与生产环境切换:用.env控制 DEBUG 和 ALLOWED_HOSTS
创建.env文件(Django 项目根目录):
# .env DEBUG=True ALLOWED_HOSTS=127.0.0.1,localhost SECRET_KEY=your-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', default=False, cast=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='127.0.0.1').split(',') SECRET_KEY = config('SECRET_KEY')血泪经验:
ALLOWED_HOSTS必须包含localhost(Vue 开发服务器域名)和127.0.0.1(Django 本地服务 IP),缺一不可。否则Cross-Origin Request Blocked错误会让你怀疑人生。
4.4 运行全流程:一条命令启动 Django,Vue 自动热更新
# 终端 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-interface:
pip 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_type=obj, check_in__lte=date.today(), check_out__gt=date.today(), status='confirmed' ).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_type=room_type, check_in__lte=timezone.now().date(), check_out__gt=timezone.now().date(), status='confirmed' ).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', hour=0, minute=0, id='sync_room_availability', replace_existing=True ) scheduler.start()我的习惯:毕设答辩时,我从不主动提「我用了 APScheduler」,但当老师问「房态数据怎么保证实时?」,我会打开
bookings/tasks.py,指着sync_room_availability函数说:「我们每晚零点自动校准一次,比实时监听更稳定,也避免了 WebSocket 的复杂度。」——把技术选择包装成「权衡取舍」,比堆名词高明得多。希望帮到你。
本文还有配套的精品资源,点击获取