Python Django 酒店管理系统:数据建模、事务并发与部署实践
简介一份基于Python Django框架的酒店管理系统毕业设计文档面向计算机相关专业学生及中小型酒店信息化开发人员重点解决客房预订、订单管理、客户消费等核心流程的数字化问题。资源为单个docx文件压缩包大小约2.37MB全文以规范论文结构呈现涵盖绪论、可行性分析、UML用例建模、系统功能设计、类图/顺序图/活动图、数据库设计及网络接口设计等完整章节。已有71人学习浏览。读者可直接参考其模块化开发思路与技术选型包括Django ORM操作、MySQL数据库设计、Vue前端渲染以及Pycharm环境下的项目实现过程文档附有清晰的需求分析与用例规约表便于快速理解系统架构与业务逻辑。对于需要完成酒店管理类课题或Web系统设计的开发者这份材料具有较强的实践参考价值。1. 做基于 Python Django 的伊人酒店管理系统先立模型再谈功能做基于 Python Django 的伊人酒店管理系统这类项目我见过太多第一版把精力全花在页面上、做到一半才发现房间状态和订单状态在数据库里对不上的案例。这个题目的技术难点从来不在 CRUD而在把房态流转和订单生命周期用数据模型锁死。Django 在这个场景里的优势很直接ORM 把房间、订单、客户映射成数据库表Django Admin 十分钟就能撑起内部管理前台再补模板和接口整条链路能压得很短适合中小型酒店的日常运营也适合课程设计直接落地。下面按做这类系统最常用的路径从 models 出发依次讲数据建模、入住退房的业务闭环、MTV 模式的模板应用、CSV 导出最后落到 Windows 下的 waitress nginx 部署。2. 数据模型设计用 Django ORM 把酒店的房间、订单、客户建牢2.1 核心模型RoomType、Room、Customer、Order 的字段选择酒店管理系统的数据量通常不大但表关系很典型一个房型下有多个房间一个客户可以下多个订单一个订单锁定一个房间。我一般从四个模型起步先把项目和 Django 应用建出来python -m venv venv venv\Scripts\activate pip install django django-admin startproject hotel_sys . python manage.py startapp hotel虚拟环境把依赖隔离在项目目录里避免污染全局 Pythonstartapp hotel生成应用骨架记得把hotel加进settings.py的INSTALLED_APPS否则后面的迁移不会建表。接着在模型文件里定义四张表# hotel/models.py from django.db import models class RoomType(models.Model): 房型标间、大床房、套间等 name models.CharField(房型名, max_length50, uniqueTrue) price models.DecimalField(门市价, max_digits10, decimal_places2) bed_count models.PositiveSmallIntegerField(床位数, default1) area models.PositiveSmallIntegerField(面积(㎡), default20) class Meta: db_table hotel_room_type def __str__(self): return self.name class Room(models.Model): 物理房间房态是业务核心字段 ROOM_STATUS ( (0, 空闲), (1, 已预订), (2, 入住中), (3, 维修中), ) number models.CharField(房间号, max_length10, uniqueTrue) room_type models.ForeignKey(RoomType, on_deletemodels.PROTECT, verbose_name房型) floor models.PositiveSmallIntegerField(楼层, default1) status models.SmallIntegerField(房态, choicesROOM_STATUS, default0) class Meta: db_table hotel_room def __str__(self): return f{self.number} {self.room_type.name} class Customer(models.Model): name models.CharField(姓名, max_length50) id_card models.CharField(证件号, max_length18, uniqueTrue) phone models.CharField(手机号, max_length20, blankTrue) class Meta: db_table hotel_customer class Order(models.Model): 订单贯穿预订到入住再到退房的全流程 ORDER_STATUS ( (0, 待入住), (1, 入住中), (2, 已退房), (3, 已取消), ) order_no models.CharField(订单号, max_length32, uniqueTrue, editableFalse) customer models.ForeignKey(Customer, on_deletemodels.PROTECT, verbose_name客户) room models.ForeignKey(Room, on_deletemodels.PROTECT, verbose_name房间) check_in_date models.DateField(入住日期) check_out_date models.DateField(退房日期) status models.SmallIntegerField(订单状态, choicesORDER_STATUS, default0) amount models.DecimalField(实收金额, max_digits10, decimal_places2, default0) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table hotel_order字段选择上有一个容易踩的坑金额不要用FloatField。Python 浮点数的二进制表示误差在累计计算时会被放大结算差几毛钱财务会找你麻烦DecimalField是正确选择。两个外键都用on_deletemodels.PROTECT含义是只要还有订单引用房间和客户就删不掉这对管理系统的审计很重要——历史数据要的是删不掉不是级联删光。房态和订单状态选SmallIntegerField占 2 字节比较、索引、迁移都比字符串稳妥。核心字段的选型对照如下需求推荐字段理由价格、实收金额DecimalField十进制精确计算避免浮点误差房态、订单状态SmallIntegerField choices查询快、迁移稳界面文案可随时改订单号CharField(uniqueTrue)对账、客服查询都靠它证件号、房间号CharField(uniqueTrue)业务唯一键加唯一约束防止重复数据2.2 为什么状态字段用 IntegerField choices而不是 CharField写状态字段时最容易犯的错是直接存中文数据库里存空闲入住中。乍看直观换环境就出事MySQL 编码不一致、输入法混进全角空格、查询时多写一个字查不出结果。用IntegerField choices库里只有 0/1/2/3业务代码比较数字常量界面上用get_status_display()取中文将来老板要把空闲改成可售改 choices 文案就行一行数据都不用动。提示choices 只负责 Django 层校验和 Admin 下拉展示数据库本身没有 CHECK 约束。担心脏数据可以在模型save()里加判断或者用 PostgreSQL 的CheckConstraintMySQL 场景下应用层兜底即可。2.3 迁移与后台注册makemigrations 到 admin模型写完先别急着写视图跑一遍迁移把表建出来python manage.py makemigrations hotel python manage.py migrate python manage.py createsuperusermakemigrations生成迁移脚本migrate执行建表和字段变更。之后每次改模型字段重复两条命令Django 生成增量迁移文件老数据不会丢。接着把模型注册到 Admin后台管理界面就立起来了# hotel/admin.py from django.contrib import admin from .models import RoomType, Room, Customer, Order admin.register(Room) class RoomAdmin(admin.ModelAdmin): list_display (number, room_type, floor, status) list_filter (status, room_type) search_fields (number,)list_display决定列表页显示哪些列list_filter在右侧生成按房态、房型的筛选器前台查空房页面完全可以复用这套筛选逻辑。到这一步能靠 Django Admin 维护数据的系统骨架已经跑通了。3. 入住与退房的业务闭环Django 视图里的状态流转与并发控制3.1 URL 路由与创建订单把预订、入住、退房拆成独立接口酒店系统的业务动作就三个预订、入住、退房。每个动作都要同时改订单状态和房态中间任何一步失败都不能留下脏数据。路由先拆细# hotel/urls.py from django.urls import path from . import views urlpatterns [ path(rooms/, views.room_list, nameroom_list), path(orders/create/, views.create_order, namecreate_order), path(orders/int:order_id/checkin/, views.check_in, namecheck_in), path(orders/int:order_id/checkout/, views.check_out, namecheck_out), ]int:order_id是路径转换器自动把 URL 里的数字转成 int 传给视图传非数字直接 404。订单 id 走路径参数而不是查询串接口语义更清晰也方便后续接扫码入住这类前端。创建订单是第一个事务操作选房、登记客户、占房、生成订单号一步都不能漏。# hotel/views.py import random from django.db import transaction from django.http import JsonResponse from django.utils import timezone from .models import Room, Customer, Order transaction.atomic def create_order(request): room Room.objects.select_for_update().get(pkrequest.GET.get(room)) if room.status ! 0: return JsonResponse({code: 1, msg: 房间已被占用}) customer, created Customer.objects.get_or_create( id_cardrequest.POST[id_card], defaults{name: request.POST[name], phone: request.POST.get(phone, )}, ) room.status 1 room.save(update_fields[status]) order_no timezone.now().strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999)) Order.objects.create( order_noorder_no, customercustomer, roomroom, check_in_daterequest.POST[check_in], check_out_daterequest.POST[check_out], status0, ) return JsonResponse({code: 0, msg: 预订成功})get_or_create按证件号复用老客户档案避免同一证件号重复建档时撞上唯一约束defaults里的字段只在新建时写入老客户不会覆盖原信息。订单号用时间戳加四位随机数生成同秒并发撞号概率不高要求绝对唯一就换成uuid4().hex[:16]。这里对房间行加了select_for_update()锁防止两个人同时预订同一间房——这个问题在 3.2 是重点。3.2 用 transaction.atomic select_for_update 防止同一房间重复入住两个人同时在前台给同一间房开房是这类系统最容易暴露的并发问题。只靠if room.status 0判断不够两个请求可能同时读到空闲然后都通过检查最后同一间房被开出去两次。Django 里要锁行transaction.atomic def check_in(request, order_id): order Order.objects.select_for_update().get(pkorder_id) if order.status ! 0: return JsonResponse({code: 1, msg: 订单状态不允许入住}) room Room.objects.select_for_update().get(pkorder.room_id) if room.status ! 0: return JsonResponse({code: 1, msg: 房间当前不可用}) room.status 2 room.save(update_fields[status]) order.status 1 order.save(update_fields[status]) return JsonResponse({code: 0, msg: 入住成功})transaction.atomic保证函数内要么全部提交要么全部回滚中途抛异常不会出现订单办了入住但房间还是已预订的中间态。select_for_update()在数据库层对选中的行加写锁第二个请求会阻塞到第一个事务提交然后读到新状态把重复开房变成不可能。锁的顺序要固定先订单后房间全系统所有涉及这两张表的写操作保持一致否则并发高时会死锁。这里还有两个性能细节order.room_id直接用外键 id不触发关联查询save(update_fields[...])只更新指定列减少写放大。酒店是典型的写多读少场景这两个习惯能把事务时间压到毫秒级。3.3 退房结算与过期订单清理日期计算与 delete 的执行细节退房要算天数、算金额、还原房态同样放进事务from django.utils import timezone def check_out(request, order_id): with transaction.atomic(): order Order.objects.select_for_update().get(pkorder_id) if order.status ! 1: return JsonResponse({code: 1, msg: 订单不是入住状态}) days (timezone.localdate() - order.check_in_date).days days max(days, 1) amount order.room.room_type.price * days order.amount amount order.status 2 order.save(update_fields[amount, status]) order.room.status 0 order.room.save(update_fields[status]) return JsonResponse({code: 0, data: {days: days, amount: str(amount)}})timezone.localdate()取服务器当天日期比datetime.now()规范Django 开启USE_TZTrue时混用会踩时区坑。max(days, 1)兜底当天入住当天退的边界情况。金额计算里order.room.room_type会发两条 SQL订单量大时要在查询时补select_related(room__room_type)。退房之外还有一类高频操作清理过期没入住的订单。Django 里删除对象有两种写法效果差别很大# 删除 30 天前已取消的订单 deleted, details Order.objects.filter( status3, check_in_date__lttimezone.localdate() - timezone.timedelta(days30), ).delete()queryset.delete()是批量删除返回(删除条数, 明细字典)但它不会触发模型里的delete()方法覆盖逻辑也不会更新任何auto_now字段。订单表被别的表外键引用时默认会级联删除关联数据所以批量清理上线前先filter(...).count()确认影响行数。单条删除用order.delete()走模型实例方法适合带审计日志的场景。订单状态机可以整理成一张表视图和前端都按这张表流转动作订单状态变化房态变化事务要求创建订单0 待入住0→1 已预订必须办理入住0→1 入住中1→2 入住中必须办理退房1→2 已退房2→0 空闲必须取消订单0→3 已取消1→0 空闲必须4. MTV 模式在前台的应用模板渲染、Admin 定制与 CSV 导出4.1 MTV 模式里 Template 只负责渲染业务逻辑留在 ViewDjango 的 MTV 和 MVC 本质是同一套分工只是命名不同M 是 ModelT 是 TemplateV 是 View。很多人问 MTV 的 M、T、V 各有什么作用一句话就能说清模型管数据视图管业务模板管展示三层各干各的模板里拿不到request对象也就没法写业务逻辑。好处是前台模板可以交给别人维护后端改查询条件不影响页面结构。房态列表页是最典型的 MTV 应用# hotel/views.py from django.shortcuts import render from .models import Room def room_list(request): rooms Room.objects.select_related(room_type).order_by(floor, number) return render(request, hotel/room_list.html, {rooms: rooms}){# hotel/templates/hotel/room_list.html #} table classtable table-hover thead trth房间号/thth房型/thth楼层/thth价格/thth房态/thth操作/th/tr /thead tbody {% for room in rooms %} tr td{{ room.number }}/td td{{ room.room_type.name }}/td td{{ room.floor }}F/td td{{ room.room_type.price }}/td td{{ room.get_status_display }}/td td{% if room.status 0 %}a href{% url create_order %}?room{{ room.id }}预订/a{% endif %}/td /tr {% endfor %} /tbody /table模板里只出现循环、取值和{% if %}判断房态为 1已预订或 2入住中时预订链接自动消失。{{ room.get_status_display }}是 choices 字段自带的方法把 0/1/2/3 渲染成中文不需要自己在模板里写映射字典。注意视图里的select_related(room_type)它用一条 JOIN 把房型带出来模板里的room.room_type.name不再发 SQL。不加这行100 个房间的列表页会变成 101 条查询这就是 N1 问题上线后在低配服务器上直接表现为页面卡顿。4.2 自定义 Django Adminlist_display、list_filter、actions前台页面没写完之前Django Admin 就是内部人员的操作系统。把订单后台配好很多管理动作就不用单独写页面了# hotel/admin.py from django.contrib import admin from .models import Order admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (order_no, customer, room, check_in_date, check_out_date, status, amount) list_filter (status, check_in_date) search_fields (order_no, customer__name, room__number) date_hierarchy check_in_date actions [mark_cancelled] admin.action(description将选中订单标记为已取消) def mark_cancelled(self, request, queryset): queryset.update(status3)search_fields里的customer__name是跨表查询写法前台按客户名搜订单就照抄这个表达式date_hierarchy在列表页顶部生成按入住日期下钻的日历组件actions定义批量操作选中多张订单一键取消。queryset.update()是 SQL 级更新不经过模型save()批量改状态用它最合适但它不会触发auto_now字段需要记录操作时间就手动加updated_attimezone.now()。常用 Admin 配置项的作用如下Admin 配置作用酒店场景list_display列表页展示列订单号、客户、房间、金额list_filter右侧筛选器按订单状态、入住日期筛选search_fields顶部搜索框按客户姓名、订单号、房号搜date_hierarchy日期下钻按入住日期查某一天订单actions批量操作批量取消、批量转维修4.3 用 StreamingHttpResponse 导出订单 CSV 对账单财务月底要对账后台加一个导出 CSV 的接口。Django 里HttpResponse是一次性输出到内存StreamingHttpResponse边生成边输出订单量大时用后者内存占用是常量。响应头里content_type和Content-Disposition是两个关键参数import csv from django.http import StreamingHttpResponse def export_orders_csv(request): def generate_rows(): yield [订单号, 客户, 房间号, 入住日期, 退房日期, 金额] orders Order.objects.select_related(customer, room).iterator() for o in orders: yield [o.order_no, o.customer.name, o.room.number, o.check_in_date.isoformat(), o.check_out_date.isoformat(), str(o.amount)] response StreamingHttpResponse(generate_rows(), content_typetext/csv) response[Content-Disposition] attachment; filenameorders.csv return responsegenerate_rows()是生成器yield一行写一行配合.iterator()分批从数据库取数据几万条订单不会撑爆内存。content_typetext/csv告诉浏览器这是 CSV 文件Content-Disposition里的attachment触发下载而不是页面内打开filename指定默认文件名。文件名带中文时filename要转成 RFC 5987 的filename*UTF-8...格式否则 Windows 浏览器下载后文件名乱码。导出接口记得加登录权限校验对账单不能裸奔。5. Windows 10 上用 waitress nginx 部署 Django 酒店管理系统5.1 waitress 启动 WSGI 服务Windows 上没有官方的 gunicorn最常见的生产组合是 waitress 跑 WSGI 服务nginx 在前面接收外部流量并转发给 waitress。开发环境用runserver验证功能后切到 waitresspip install waitress waitress-serve --host127.0.0.1 --port8000 hotel_sys.wsgi:applicationhotel_sys.wsgi:application是项目配置里的 WSGI 入口--host127.0.0.1表示只监听本机外部流量全部走 nginx不要让 waitress 直接暴露到公网。上线前改三处配置settings.py里DEBUGFalse、ALLOWED_HOSTS填域名或 IP、跑python manage.py collectstatic把静态文件收拢到STATIC_ROOT。5.2 nginx 入口转发与静态文件server { listen 80; server_name hotel.example.com; location /static/ { alias D:/hotel/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }HTML、CSS、JS 这类静态文件直接让 nginx 从磁盘读不经过 DjangoDjango 只处理动态请求。proxy_pass指向 waitress 的监听地址请求从 80 端口进来转发到本机 8000 端口。proxy_set_header三行把客户端真实 IP 放进请求头传给后端否则 Django 日志里记录的全是 127.0.0.1审计和封禁都没法做X-Forwarded-For的取值是客户端 IP, 入口 IP列表后端取第一个逗号前的值即可。5.3 上线前跑一遍的验证与索引优化部署完成我用一条 curl 确认链路curl -s -o /dev/null -w %{http_code} %{time_total}s\n http://hotel.example.com/rooms/输出200 0.05s左右说明 nginx 转发和 waitress 响应都正常返回 502 去查 waitress 进程是否还活着返回 400 检查ALLOWED_HOSTS。页面性能用django-debug-toolbar逐个页面看 SQL 条数核心列表页超过 20 条查询就回头补select_related和prefetch_related。最后一步给高频查询字段建索引class Order(models.Model): order_no models.CharField(订单号, max_length32, uniqueTrue, db_indexTrue) customer models.ForeignKey(Customer, on_deletemodels.PROTECT, db_indexTrue) check_in_date models.DateField(入住日期, db_indexTrue)加完跑python manage.py makemigrations hotel python manage.py migrate生效。db_indexTrue生成普通 B-tree 索引覆盖按订单号精确查、按客户聚合、按入住日期范围筛选三类查询。几百个订单的酒店系统在这个规模下配合第 3 章的状态机约束和锁机制入住、退房、对账、导出整条链路不会再出现可感知的慢查询。本文还有配套的精品资源点击获取