简介这是一款开箱即用的酒店管理系统面向酒店运营者、中小型酒店管理者及计算机相关专业学习者涵盖后台管理、官方网站与微信小程序三大版块可一站式解决客房预订、订单流转、订餐管理与支付退款等运营需求。系统支持实时房间动态展示与信息推送帮助管理者快速掌握房态并改善对客服务微信小程序端则打通在线下单、订餐、支付与退款流程提升了客户操作便捷度。资源共包含1477个文件以JavaScript、WXML/WXSS、JSON等前后端代码文件为主辅以图片、样式与配置文件整体约51.88MB适合直接部署或二次开发学习。目前已有59人浏览学习对想要快速搭建数字化酒店平台、研究小程序支付与订单闭环的开发者来说是一份结构完整、可直接落地的参考资料。1. 开箱即用的酒店管理系统先跑通后台、网站、小程序三端闭环一家 30 间房的民宿或精品酒店买一套主流酒店管理系统商用 PMS年费上万自己从零写又没时间。常见做法是拿一套“开箱即用的酒店管理系统”起步后台管房态和订单网站做展示与在线预订微信小程序给住客查订单、订餐、收通知。这个标题指的就是这类项目。真正决定项目值不值得投入的不是界面好不好看而是房间动态实时性和订单状态一致性这两件事。适合中小酒店自运维、接活团队交付也适合拿来做酒店管理系统毕业设计的底子往智慧酒店管理系统方向演进也顺路。下面按我跑通类似项目的落地路径展开照做能少走很多弯路。2. 把三端跑起来技术选型与最小启动命令2.1 三端技术栈为什么是 Django Vue3 原生小程序开门见山放结论。后台管理端用 Vue3 Element Plus Pinia Vite接口层用 Python Django Django REST Framework门户网站用 Vue3要 SEO 就换 Nuxt3微信小程序用原生语法实时通道用 Django Channels Redis。这个组合不是最炫的但对“开箱即用”这个目标最友好Django 自带 Admin 后台房型、餐品、公告这类低频维护可以直接开箱用不用每个表都写页面DRF 的 ModelViewSet 能在半小时内把核心资源全部暴露成 REST APIElement Plus 的表格、表单、标签页直接对应订单管理、房态管理这些中后台场景小程序用原生避开 uni-app 那层编译黑匣子出问题能直接定位到微信开发者工具的控制台。替代方案不是没有常见的是 Node.js或者直接套一套 vue3 后台管理系统模板再配 Java 后端。Java 体系适合大型连锁但对中小酒店太重Node 做实时推送很顺但后台管理、权限、ORM 都要自己拼。Django 是这几个需求里配套最齐的。如果团队已经以 Vue 为绝对主力小程序端换 uni-app 开发也能跑代价是多一层编译遇到渲染差异时排查成本更高。技术选型没有标准答案先跑通三端闭环最重要选什么都行唯一能吃的后悔药就是别把架构搞复杂。端推荐技术栈替代方案选择理由后台管理Vue3 Element Plus PiniaReact Ant DesignElement Plus 对订单列表、房态网格这类中后台场景开箱即用门户网站Vue3要 SEO 用 Nuxt3Django 模板渲染与后台共用组件与 API 规范访客端实时性要求低微信小程序原生 WXML/WXSS/JSuni-app免去编译层黑匣子联调时排错路径最短后端 APIDjango DRF ChannelsNode.js ExpressAdmin/ORM/迁移/权限一套齐Channels 天然支持房间分组推送如果你只是做内部 demo后台可以直接用 Django Admin 顶着它连登录都自带。但正式让前台用还是要 Vue3 写一套Django Admin 的表格交互对酒店前台来说太反人类。2.2 项目结构后端、后台管理端、门户站、小程序四个子包代码包根目录一般长这样hotel-management/ ├── backend/ # Django 项目API Admin Channels │ ├── apps/ │ │ ├── rooms/ # 房型 / 房间 / 房态日志 │ │ ├── orders/ # 订单中心 │ │ └── catering/ # 订餐管理 │ ├── config/ # settings / urls / asgi │ └── manage.py ├── admin-web/ # Vue3 后台管理端 ├── portal-web/ # 门户网站 └── mini-program/ # 微信小程序原生代码这个结构把四个子项目拆开每个都能独立启动、独立部署。后端是最核心的三个端都只跟后端 API 通信端与端之间不直接依赖这样后面加一个公众号 H5 或者抖音小程序只需要新增一个前端壳。后端依赖最小集建议这样锁Django4.2,5.0 djangorestframework channels4.0 channels-redis4.1 django-cors-headers redis5.0 psycopg2-binaryDjango 4.2 是当前支持周期较长的稳定线Channels 4.x 配合 Daphne 跑 ASGI。开发阶段用 SQLite 起步生产环境换 PostgreSQL代码层不用改只改 DATABASES 配置。2.3 三分钟启动序列迁移、跑后端、起前端拿到项目后不要先看代码先把服务拉起来确认环境是通的cd backend python -m venv venv source venv/bin/activate pip install -r requirements.txt cp .env.example .env # 按本机改数据库与 Redis 地址 python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000参数说明runserver 绑 0.0.0.0 是为了让局域网里的手机小程序能访问到本机如果只在电脑上调试绑 127.0.0.1 即可。.env 里重点改 DJANGO_SECRET_KEY、DATABASE_URL、REDIS_URL 三项其他保持默认。后端起来之前先确认本机 Redis 在跑否则后面 WebSocket 一推就报错。后端起来后另开两个终端起前端cd admin-web npm install npm run dev # 默认占 5173 端口Vite 把 /api 和 /ws 前缀转发到 8000 cd mini-program # 用微信开发者工具导入目录AppID 可以先用自己的测试号 # 本地开发把 request 合法域名校验关掉这里有一个很多人一开始会忽略的点后台管理端的数据不是直接请求 http://localhost:8000而是通过 Vite 的转发配置到 Django所以 admin-web 的 vite.config 里要把 /api 和 /ws 前缀都处理掉。小程序没有转发这一层需要在小程序代码里把 baseURL 直接指向局域网 IP比如 http://192.168.1.10:8000/api并勾选“不校验合法域名”。提示起服务后发现页面能开但接口 404先看前缀是否一致。后端 API 统一走 /api/WebSocket 统一走 /ws/前端所有请求都以这两个前缀开头能少掉一半联调问题。3. 数据库建模房间动态、订单中心、订餐管理怎么落表3.1 房型与房间一张 status 字段撑起实时房态图实时房间动态说白了就是房间的状态一变所有在线的后台、前台、小程序客户端都要同步刷新。状态的源头是数据库里的 rooms 表。房间模型最少包含这些字段# backend/apps/rooms/models.py from django.db import models class RoomType(models.Model): name models.CharField(max_length50, verbose_name房型名称) base_price models.DecimalField(max_digits10, decimal_places2, verbose_name挂牌价) member_price models.DecimalField(max_digits10, decimal_places2, verbose_name会员价) bed_type models.CharField(max_length20, verbose_name床型) capacity models.IntegerField(default2, verbose_name可住人数) area models.IntegerField(default25, verbose_name面积(平方米)) class Room(models.Model): STATUS_CHOICES [ (available, 空闲), (occupied, 入住), (dirty, 脏房), (maintenance, 维修), (reserved, 预留), ] room_number models.CharField(max_length10, uniqueTrue, verbose_name房间号) room_type models.ForeignKey(RoomType, on_deletemodels.PROTECT, verbose_name房型) floor models.IntegerField(verbose_name楼层) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultavailable) updated_at models.DateTimeField(auto_nowTrue)逻辑说明status 用字符串枚举而不是布尔因为真实酒店里“维修”“脏房”和“空闲”一样常见uniqueTrue 保证房号唯一这是后续所有订单判断“这间房能不能订”的前提。updated_at 用来做最后变更时间前端展示“几分钟前更新”直接取它。参数说明price 用 DecimalField 而不是 FloatField房价涉及金额结算FloatField 的小数误差会在月结时暴雷capacity 和 area 留出来是为了门户网站的筛选器“可住 2 人”“30 平米以上”能直接查。关于“实时”房间表本身不带实时能力实时靠的是这张表的变更事件。做法是给 Room 加一个状态变更记录表class RoomStatusLog(models.Model): room models.ForeignKey(Room, on_deletemodels.CASCADE) from_status models.CharField(max_length20) to_status models.CharField(max_length20) operator models.CharField(max_length50) created_at models.DateTimeField(auto_now_addTrue)每条状态变更落一条日志房态图上方的时间轴、值班交接、纠纷追责全靠它。3.2 订单中心状态机字段放在一张表还是拆预订与入住中小酒店不用把预订单和入住单拆成两张表一张 orders 表加状态字段就够了。常见做法是class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付/待入住), (checked_in, 已入住), (checked_out, 已离店), (cancelled, 已取消), (refunded, 已退款), ] CHANNEL_CHOICES [(backend, 后台), (portal, 网站), (mini, 小程序)] order_no models.CharField(max_length32, uniqueTrue) channel models.CharField(max_length20, choicesCHANNEL_CHOICES) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) room models.ForeignKey(Room, on_deletemodels.PROTECT) guest_name models.CharField(max_length50) guest_phone models.CharField(max_length20) check_in_date models.DateField() check_out_date models.DateField() total_amount models.DecimalField(max_digits10, decimal_places2) created_at models.DateTimeField(auto_now_addTrue)关键在状态机pending 只能流向 paid 或 cancelledpaid 才能流向 checked_inchecked_in 只能流向 checked_out。换房操作不是改 Order.room 字段那么简单而是先建一张 room_change_log再通过统一的“换房服务”去改房间状态和订单房间号否则会出现“订单在 A 房、房间状态显示 B 房有人住”的脏数据。订单与房间怎么联动下单成功把房间置为 reserved 或直接 paidcheck_in 时置 occupiedcheck_out 时先置 dirty脏房保洁打扫完再由前台改成 available。这个链条要放在一个事务里完成不能“订单状态改了但房间状态没改”。3.3 订餐模块菜单、订餐单、明细三张表就够订餐管理的场景很标准住客在小程序上看菜单、下单餐厅收单、出餐、送到房间最后费用挂房账或单独支付。表结构如下class Meal(models.Model): name models.CharField(max_length100) category models.CharField(max_length20) # 早餐/正餐/饮品/夜宵 price models.DecimalField(max_digits8, decimal_places2) is_available models.BooleanField(defaultTrue) class MealOrder(models.Model): STATUS_CHOICES [(submitted, 已下单), (preparing, 制作中), (delivered, 已送达), (finished, 已完成)] order_no models.CharField(max_length32, uniqueTrue) room models.ForeignKey(Room, nullTrue, on_deletemodels.SET_NULL) guest_name models.CharField(max_length50, blankTrue) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultsubmitted) remark models.CharField(max_length200, blankTrue) total_amount models.DecimalField(max_digits8, decimal_places2) created_at models.DateTimeField(auto_now_addTrue) class MealOrderItem(models.Model): meal_order models.ForeignKey(MealOrder, related_nameitems, on_deletemodels.CASCADE) meal models.ForeignKey(Meal, on_deletemodels.PROTECT) quantity models.IntegerField(default1) price models.DecimalField(max_digits8, decimal_places2) # 下单时快照价注意 MealOrderItem.price 存的是下单那一刻的餐品价格而不是关联查询 Meal.price这样以后菜单改价历史订单金额不受影响。快照价是一种很常见但容易漏掉的细节。订餐流的信息推送点是状态变化尤其“制作中 - 已送达”这一步住客在小程序上能看到实时状态这就是“实时信息推送”在订餐模块的落地形态。3.4 用 DRF 把这套模型暴露成 API统一响应与权限模型建好之后接口层直接用 DRF 的 ModelViewSet 出# backend/apps/rooms/apis.py from rest_framework import viewsets, serializers from .models import Room, RoomType class RoomSerializer(serializers.ModelSerializer): room_type_name serializers.CharField(sourceroom_type.name, read_onlyTrue) class Meta: model Room fields [id, room_number, floor, status, room_type, room_type_name] class RoomViewSet(viewsets.ModelViewSet): queryset Room.objects.select_related(room_type).all() serializer_class RoomSerializer filterset_fields [status, floor]配一个统一响应包装让三个端不用各自处理错误结构def custom_response(dataNone, messageok, code0): return {code: code, message: message, data: data} class StandardViewSet(viewsets.ModelViewSet): def list(self, request, *args, **kwargs): page self.paginate_queryset(self.get_queryset()) data self.get_serializer(page, manyTrue).data if page else self.get_serializer(self.get_queryset(), manyTrue).data return Response(custom_response(datadata))逻辑说明三个端共用一个响应结构 {code, message, data}小程序端 wx.request 的封装只要判断 code 是否为 0不用再解析 DRF 默认的 {count, results} 结构。安全边界上写接口加 DRF 的 IsAuthenticated读接口对门户网站放开小程序用 token 鉴权具体封装放在下一章。4. 实时房间动态与消息推送从数据库变更到三端实时刷新4.1 请求封装与登录态三端统一 token 规范所有实时推送的前置条件是“知道你是谁”。小程序 wx.request 的最简封装如下// mini-program/utils/request.js const BASE_URL http://192.168.1.10:8000/api function request(path, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) || wx.request({ url: BASE_URL path, method, data, header: { Authorization: Bearer token }, success(res) { if (res.data.code 0) { resolve(res.data.data) } else if (res.statusCode 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/index }) reject(res.data) } else { reject(res.data) } }, fail: reject }) }) } module.exports { request, BASE_URL }逻辑说明每次请求自动带 token401 统一踢回登录页业务层只关心 data 字段。开发阶段 BASE_URL 写局域网 IP上线前换成正式域名。后端用 simplejwt 发 token登录接口返回 access 和 refresh小程序在收到 401 时先尝试用 refresh 换新 token 再重放原请求这套“自动续期”对三个端是通用的。实时连接也一样小程序里 wx.connectSocket 的 header 带上同一个 tokenDjango Channels 的 middleware 验完 token 才允许建立连接。4.2 后端推送链路Django Channels 把房间变更推给所有在线端这一节就是标题里“后台有数据前端实时刷”的核心链路。整条链路是某个接口改了 Room.status → 触发 group_send → Redis 转发 → 所有连接了该 group 的前端收到消息。# backend/config/asgi.py import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path from apps.rooms.consumers import RoomStatusConsumer os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: URLRouter([path(ws/room-status/, RoomStatusConsumer.as_asgi())]), })consumers 端# backend/apps/rooms/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class RoomStatusConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name room_status await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def room_status_changed(self, event): await self.send(text_datajson.dumps({ type: room_status_changed, room_id: event[room_id], status: event[status], updated_at: event[updated_at], }))触发点在业务接口里from asgiref.sync import async_to_sync from channels.layers import get_channel_layer channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( room_status, {type: room_status_changed, room_id: room.id, status: room.status, updated_at: str(room.updated_at)} )逻辑说明consumer 的 room_status_changed 方法名对应 event 里的 type 字段Channels 会根据 type 字符串去找同名方法。group_name 在示例里是固定的“room_status”如果以后按楼层分组推送改成 froom_status_floor_{room.floor} 即可。settings 里要配 channel layerCHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: {hosts: [(127.0.0.1, 6379)]}, } }参数说明Redis 是必须的生产别用 InMemoryChannelLayer多进程下会丢消息。Redis 挂掉时 Channels 推送直接抛异常所以 Redis 要单独监控。4.3 后台与小程序两个前端的 WebSocket 写法与心跳重连后台管理端用 Vue3 写房态图连接和断线重连是一个反复踩坑的点。最简单的可靠写法// admin-web/src/composables/useRoomSocket.js let socket null let heartbeatTimer null export function connectRoomSocket(onMessage) { if (socket socket.readyState WebSocket.OPEN) return const protocol location.protocol https: ? wss : ws socket new WebSocket(${protocol}://${location.host}/ws/room-status/) socket.onopen () { heartbeatTimer setInterval(() socket.send(ping), 30000) } socket.onmessage (e) { if (e.data ! pong) onMessage(JSON.parse(e.data)) } socket.onclose () { clearInterval(heartbeatTimer) socket null setTimeout(() connectRoomSocket(onMessage), 3000) } }心跳包的作用是让 Nginx 和 Redis 知道连接还活着30 秒一次服务端收到 ping 回 pong客户端忽略 pong 即可。断线 3 秒后自动重连但要注意 onclose 里 setTimeout 不要叠加重连否则网络抖动会连环重连。小程序端写法类似但用的是 wx 的 API// mini-program/utils/socket.js let socketTask null function connectRoomSocket(onMessage) { if (socketTask) return socketTask wx.connectSocket({ url: ws://192.168.1.10:8000/ws/room-status/, header: { Authorization: Bearer wx.getStorageSync(token) } }) socketTask.onMessage((res) { const data JSON.parse(res.data) if (data.type room_status_changed) onMessage(data) }) socketTask.onClose(() { socketTask null setTimeout(() connectRoomSocket(onMessage), 3000) }) }这里有个小程序特有问题wx.connectSocket 不支持像浏览器里那样直接查 readyState所以用 socketTask 是否为空判断是否已连接onClose 里置空再重连。4.4 微信订阅消息与门户轮询一个合规推送一个离线兜底WebSocket 只能在用户“正在打开页面”时推送。用户在房间外、小程序被切到后台时微信官方给的能力是订阅消息。注意它的规则必须由用户点击行为触发 wx.requestSubscribeMessage每次授权只能推一条消息推送模板需要在小程序后台申请。// 用户点击“确认订餐”后触发 wx.requestSubscribeMessage({ tmplIds: [餐品送达通知的模板ID], success(res) { if (res[餐品送达通知的模板ID] accept) { request(/api/mini/subscribe-authorized/, POST, { template_id: 餐品送达通知的模板ID, result: accept }) } } })后端推送时用 access_token 调用 subscribeMessage.send模板里的字段要和用户在订单里填的房间号、餐品名对应。这是一个血泪经验密集区很多团队把订阅消息当成免费短信来用结果发现用户不点按钮就永远拿不到订阅授权。合规的玩法是在用户最可能想收到通知的场景去触发授权比如订餐支付成功后紧接着弹订阅授权而不是在首页弹。门户网站的访客量大、实时性要求低没必要给每个访客开 WebSocket。最常见做法是 15 秒轮询一次房态接口setInterval(async () { const res await fetch(/api/rooms/?statusavailable) // 更新页面上剩余可订房间数 }, 15000)轮询间隔的权衡5 秒刷新太频繁数据库压力大30 秒以上又会让“只剩 3 间”这种文案显得迟钝。15 秒是预览页和门户站比较平衡的值。后台管理端不要用轮询必须走 WebSocket因为前台改房态的频次高轮询会明显滞后。5. 常见问题排查三端联调最容易翻车的五个位置5.1 WebSocket 一推就断Nginx 没带 Upgrade 头现象本地起 Django 和前端WebSocket 一切正常部署到服务器后前端 console 报 WebSocket connection failed连接建立后几秒内就关闭。原因Nginx 把 80/443 端口的流量转发给 Django 时默认不转发 Upgrade 和 Connection 头WebSocket 握手失败。REST 接口正常只有带 Upgrade 的请求会挂。解决在 Nginx 配置里给 WebSocket 路径单独加转发头location /ws/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }proxy_read_timeout 也要调默认 60 秒如果服务端和客户端之间 60 秒没有数据Nginx 会主动断开再好的心跳也扛不住这个超时。长连接服务建议至少 3600 秒。如果用 docker-composeproxy_pass 里的地址要换成后端服务名。5.2 小程序真机连不上本地服务域名、HTTPS 与局域网 IP 的三角关系现象开发者工具里接口一切正常预览到手机上就全部 request fail。原因三个条件同时满足才能连通。第一微信要求正式环境必须是 HTTPS 和 wss且域名已备案并配置在小程序后台的 request 合法域名里第二本地真机调试时“不校验合法域名”开关只在调试模式时生效关掉调试立刻断第三手机和电脑要在同一个局域网BASE_URL 写的是电脑的局域网 IP不是 localhost。解决开发阶段手机开启调试模式BASE_URL 改成本机局域网 IP真机预览用手机浏览器访问 http://局域网IP:8000/api/health/ 确认能通。上线前把 BASE_URL 换成正式 HTTPS 域名小程序后台添加 request 合法域名和 socket 合法域名。另外后端要配好跨域Django 里加 django-cors-headersCORS_ALLOWED_ORIGINS 把门户站域名和后台管理端域名都写进去。小程序端没有 CORS 概念但从浏览器发出的请求都必须过这一关。5.3 并发订房产生重复订单房间状态更新缺了一把锁现象门户站“秒杀”场景下两个用户几乎同时预订同一间房两边都下单成功数据库里出现两笔 paid 订单指向同一房间。原因先查 Room.status 再创建 Order 这两步之间有间隙两个请求都读到 available然后各自建单。解决创建订单时用 select_for_update 锁行并且把“校验状态 改状态 建订单”放进一个事务from django.db import transaction transaction.atomic def create_order(room_id, guest_name, check_in, check_out): room Room.objects.select_for_update().get(pkroom_id) if room.status not in (available, dirty): raise ValueError(房间已被占用) room.status reserved room.save() return Order.objects.create( roomroom, guest_nameguest_name, check_in_datecheck_in, check_out_datecheck_out, statuspaid, channelportal, )注意 select_for_update 在事务里才生效开事务原子块是必须的数据库也要用支持行锁的引擎SQLite 的锁粒度是库级别的开发够用压测和生产必须换 PostgreSQL。5.4 换房后房态卡死状态流转没走统一出口现象前台把客人从 301 换到 302订单显示在 302但 301 的状态还是 occupied302 还是 dirty两间房都不可卖。原因换房时直接改了 Order.room 字段没有同时改两张房间表的状态。这个问题的隐蔽性在于单步操作在界面上都成功只有对账时才会暴露。解决换房必须通过一个统一的服务函数执行不能在前端拼两个接口调用def change_room(order, new_room): old_room order.room old_room.status dirty old_room.save() new_room.status occupied new_room.save() order.room new_room order.save() RoomStatusLog.objects.create(roomold_room, to_statusdirty, operatorfrontdesk) RoomStatusLog.objects.create(roomnew_room, to_statusoccupied, operatorfrontdesk)这个函数也要包在事务里。所有涉及房态变化的操作check_in、check_out、换房、维修都必须走这种统一出口避免“绕过状态机”的前端拼接口写法。5.5 房价算错时区配置让入住晚数变成负数现象订单显示住了一晚上但金额按两晚收或者客人凌晨 1 点入住离店日期计算错。原因Django 默认 TIME_ZONE UTC如果系统时间用 UTC而入住日期是本地时间算出来的日期边界会偏移如果只设置 TIME_ZONE 不设置 USE_TZDjango 又会用本地时间做 naive datetime两种配置混着用最容易出错。解决结算按日期而不是按小时计算服务端统一把“入住晚数”定义成 check_out_date - check_in_date 的天数差值不依赖当前时刻。settings 里明确时区TIME_ZONE Asia/Shanghai USE_TZ True注意 USE_TZTrue 时DateTimeField 存的是带时区的 UTC 时间前端展示要转回上海时间如果整个团队都不熟悉时区转换最保守的做法是 USE_TZFalse让数据库直接存本地时间代价是将来做跨时区业务还要再改。我一般建议订单结算相关的日期字段一律用 DateField不存 DateTimeField能从根上避开一半问题。6. 从“能跑”到“能扛”并发压测脚本与容器化收尾验证一个酒店管理系统不是看页面是否流畅而是看并发下房态是否一致。下面这个脚本模拟 20 个用户同时抢同一间房跑完检查订单数超过 1 笔就是有问题的# scripts/test_concurrent_order.py import threading import requests BASE http://127.0.0.1:8000/api TOKEN 你的测试token def grab_room(): headers {Authorization: fBearer {TOKEN}} payload {room_id: 101, check_in: 2025-06-01, check_out: 2025-06-03} try: r requests.post(f{BASE}/orders/, jsonpayload, headersheaders, timeout5) print(r.status_code, r.json()) except ValueError: print(r.status_code, r.text) threads [threading.Thread(targetgrab_room) for _ in range(20)] for t in threads: t.start() for t in threads: t.join() # 跑完后查询订单接口room_id101 且未取消的订单应该只有 1 笔跑脚本前先确认 Django 的 debug 模式关掉否则异常会在终端刷屏而不是返回 JSON。并发压测出了重复订单优先检查第 5.3 节的 select_for_update 是否真的包在了事务里以及数据库引擎是不是不支持行锁。脚本需要 requests 库记得先 pip install requests。最后一件事是部署。开箱即用的项目最常见的失败是“本地秒开线上 500”。建议直接上 docker-compose四个服务Nginx、Djangodaphne、Redis、PostgreSQL。Django 容器里跑两条命令一条 migrate一条 daphne -b 0.0.0.0 -p 8000 config.asgi:applicationNginx 同时托管 Vue3 打包后的静态文件和转发 /api、/ws/ 这两个前缀。docker-compose up -d --build我自己的习惯是把上面这些验证做成一个 shell 脚本每次改完数据库模型先跑迁移、再跑压测、最后看 Redis 里有没有积压的 channel 消息。曾经有一次线上订餐推送丢单查到最后是 Redis 没配持久化重启全丢从那以后我再也不敢不检查 Redis 的 maxmemory 和持久化策略。这个方案的边界也很清楚它适合中小体量酒店房间数几百间以内、并发下单不高的场景再往后要上专业的商用 PMS 系统。希望帮到你。本文还有配套的精品资源点击获取