简介:这是一套面向民宿酒店行业从业者与PHP开发者的多门店预订管理系统源码,基于ThinkPHP后端与uniapp、uView前端技术栈构建,可帮助业务方快速搭建从房源搜索、选择、预订到支付的全链条服务平台,适合具备一定PHP与Vue基础、希望缩短项目周期的开发者二次开发使用。压缩包共743个文件,约59.78MB,其中169个php文件承载后端业务逻辑,159个vue与139个js文件构成前端页面与交互,另有155个png及html、json、scss、sql等资源,覆盖界面素材、配置与数据库脚本。系统除基础预订流程外,还集成WIFI连接、用户吐槽与周边信息等特色模块,兼顾住客体验与运营反馈。源码版本迭代至1.0.14,功能模块相对完整,目录结构清晰,便于按模块检索与二次修改。目前已有41人学习下载,适合需要快速落地民宿预订平台的开发者参考借鉴。
1. 民宿酒店预订管理系统源码:WIFI 连接、吐槽与周边信息怎么落地
很多做民宿的老板找我聊,开口就是「有没有一套能直接跑的民宿酒店预订管理系统源码,最好带 WIFI 连接、吐槽和周边信息」。这个需求听起来像三件事拼在一起,其实背后是一条完整的住客体验链路:客人订房、到店连 WIFI、住下后想吐槽、出门前查周边。市面上现成的 SaaS 要么按年收费,要么功能砍得只剩订房,二次开发还得看厂商脸色。自己拿一套源码改,反而能把 WIFI 连接、吐槽、周边信息这些「非标」功能捏进同一个后台。这篇笔记就按我实际搭过的一套方案,把技术选型、数据库设计、接口实现和踩过的坑讲清楚,适合想自建系统的小团队和接私活的开发者。
2. 技术选型与数据库设计:为什么用 Python + MySQL 而不是现成 CMS
2.1 选型理由:民宿场景对源码的三个硬要求
民宿和连锁酒店不一样,房间少、老板自己管、客人多是散客。这种场景对系统的要求集中在三点:部署要轻、改起来要快、数据要能自己攥着。现成的 PHP CMS 插件多,但改一个「吐槽」字段要翻半天模板;Java 方案稳,但一个小民宿养不起 Tomcat 那套运维。我一般会选 Python 的 Flask 或 FastAPI 做后端,前端用 Vue 或直接服务端渲染,数据库 MySQL 或 SQLite 起步。
WIFI 连接这个功能容易被误解成「系统去连 WIFI」,其实不是。系统要做的是把每个房间的 WIFI 名称和密码存下来,客人订房成功后自动推送到订单详情页,到店扫码就能看到。吐槽功能是一个带审核的留言板,周边信息则是一组可后台维护的 POI 数据。这三块都不复杂,难的是和订单状态联动。
提示:如果只是单店,SQLite 足够;多店或要对接美团、携程的订单推送,直接上 MySQL,别等数据乱了再迁。
2.2 数据库表设计:五张核心表撑起全部功能
先把表结构定下来,后面写接口就是填空。下面是我实际用的建表语句,字段做了精简,保留了和 WIFI、吐槽、周边直接相关的部分。
-- 民宿房间表:WIFI 信息挂在这里 CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '房间名,如 201 海景房', price DECIMAL(10,2) NOT NULL, wifi_ssid VARCHAR(64) DEFAULT NULL COMMENT 'WIFI 名称', wifi_password VARCHAR(64) DEFAULT NULL COMMENT 'WIFI 密码', status TINYINT DEFAULT 1 COMMENT '1可订 0停用' ); -- 订单表:关联房间,住客凭订单看 WIFI CREATE TABLE booking ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, guest_name VARCHAR(32) NOT NULL, phone VARCHAR(20) NOT NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES room(id) ); -- 吐槽表:带审核状态,防止恶意内容直接上墙 CREATE TABLE feedback ( id INT PRIMARY KEY AUTO_INCREMENT, booking_id INT DEFAULT NULL COMMENT '可关联订单,也可匿名', content TEXT NOT NULL, rating TINYINT DEFAULT 5 COMMENT '1-5 星', audit_status TINYINT DEFAULT 0 COMMENT '0待审 1通过 2驳回', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 周边信息表:分类存 POI CREATE TABLE poi ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL COMMENT '餐饮/景点/交通', distance_m INT DEFAULT 0 COMMENT '距离民宿米数', description VARCHAR(255) DEFAULT NULL );这五张表里,room表的wifi_ssid和wifi_password是 WIFI 连接功能的数据源,feedback表的audit_status是吐槽功能的安全阀,poi表的category和distance_m决定周边信息怎么排序展示。建表时注意wifi_password别用明文存生产环境,后面接口章节会讲怎么处理。
2.3 项目目录结构:照着建就能跑
选型定了、表建好了,目录结构直接影响后面改代码的速度。我习惯按功能分模块,不按技术分层,这样加一个「周边」功能不用翻三个文件夹。
minisu/ ├── app.py # 入口,注册蓝图 ├── config.py # 数据库、密钥配置 ├── models.py # 所有表模型 ├── routes/ │ ├── booking.py # 订房接口 │ ├── wifi.py # WIFI 信息接口 │ ├── feedback.py # 吐槽接口 │ └── poi.py # 周边信息接口 ├── static/ # 前端静态文件 └── templates/ # 页面模板routes下每个文件对应一个功能,wifi.py只负责根据订单号返回对应房间的 WIFI 信息,feedback.py只管吐槽的增查和审核。这样拆的好处是,哪天老板说「WIFI 密码要改成动态的」,你只动wifi.py和room表,不会碰坏订房逻辑。
3. WIFI 连接功能实现:从订单到密码的完整链路
3.1 接口设计:一个订单号换 WIFI 信息
WIFI 连接的核心逻辑是「凭订单查密码」。客人到店后打开订单页,前端拿订单 ID 调后端接口,后端校验订单有效后返回该房间的 WIFI 名称和密码。这里有个安全细节:不能让人随便传个订单号就把所有房间密码刷出来,所以要校验订单状态和入住日期。
# routes/wifi.py from flask import Blueprint, request, jsonify from models import db, Booking, Room from datetime import date wifi_bp = Blueprint('wifi', __name__) @wifi_bp.route('/api/wifi/<int:booking_id>') def get_wifi(booking_id): booking = Booking.query.get(booking_id) if not booking: return jsonify({'code': 404, 'msg': '订单不存在'}), 404 # 只有入住当天及之后才能看,防止提前泄露 if date.today() < booking.check_in: return jsonify({'code': 403, 'msg': '未到入住日期'}), 403 room = Room.query.get(booking.room_id) if not room or not room.wifi_ssid: return jsonify({'code': 404, 'msg': '该房间未配置 WIFI'}), 404 return jsonify({ 'code': 0, 'data': { 'ssid': room.wifi_ssid, 'password': room.wifi_password } })这段代码的关键在date.today() < booking.check_in这个判断。没有它,任何人拿到订单号就能提前把密码拿走。参数上,booking_id走路径传参,前端从订单详情页直接取。返回结构用code区分状态,前端好处理。如果民宿有多栋楼、多个 WIFI,就在room表加个building字段,接口里按楼返回。
3.2 密码存储:别用明文,但也别过度设计
很多源码包直接把 WIFI 密码明文存数据库,小民宿觉得无所谓。但一旦数据库被拖,客人隐私和网络安全都是问题。我一般用对称加密存,密钥放环境变量,接口返回时解密。不用 bcrypt 那种哈希,因为哈希不可逆,你没法把密码还原给客人。
# config.py import os from cryptography.fernet import Fernet # 密钥从环境变量读,别写死在代码里 FERNET_KEY = os.environ.get('FERNET_KEY', Fernet.generate_key()) cipher = Fernet(FERNET_KEY) # 存的时候 encrypted = cipher.encrypt(b'my_wifi_password') # 取的时候 plain = cipher.decrypt(encrypted).decode()Fernet是对称加密,加解密用同一个密钥。部署时把FERNET_KEY写进.env或系统环境变量,代码里只读不写。这样即使源码泄露,没有密钥也解不出密码。注意密钥丢了密码就找不回,得让老板自己备份一份。
3.3 前端展示:扫码直达 WIFI 信息页
后端接口有了,前端怎么让客人最省事地看到?我的做法是在房间贴一张二维码,扫码后带上订单参数跳转到 WIFI 页。二维码用 Python 的qrcode库批量生成,每个房间一张。
import qrcode def gen_room_qrcode(room_id, base_url): # 二维码内容指向 WIFI 查询页,带上房间号 url = f"{base_url}/wifi?room_id={room_id}" img = qrcode.make(url) img.save(f"qrcodes/room_{room_id}.png")客人扫码后进入一个简单页面,输入手机号后四位验证,或者直接凭已登录的订单会话展示 WIFI。这里不硬性要求扫码即出密码,加一层轻验证能防住隔壁房间乱扫。生成二维码时base_url用实际部署域名,本地测试用127.0.0.1:5000就行。
4. 吐槽与周边信息:两个容易被做废的功能怎么救回来
4.1 吐槽功能:审核机制比留言框重要
吐槽功能做不好就是垃圾广告墙。我见过太多源码包,吐槽就是一个textarea加一个提交按钮,结果上线三天全是刷单广告。正确的做法是「提交即入库、审核后才展示」,并且给每条吐槽绑定订单,没住过的人不能评。
# routes/feedback.py from flask import Blueprint, request, jsonify from models import db, Feedback, Booking feedback_bp = Blueprint('feedback', __name__) @feedback_bp.route('/api/feedback', methods=['POST']) def add_feedback(): data = request.get_json() booking_id = data.get('booking_id') # 必须关联有效订单才能吐槽 booking = Booking.query.get(booking_id) if not booking: return jsonify({'code': 403, 'msg': '请先完成预订'}), 403 fb = Feedback( booking_id=booking_id, content=data.get('content', '').strip()[:500], rating=int(data.get('rating', 5)), audit_status=0 # 默认待审 ) db.session.add(fb) db.session.commit() return jsonify({'code': 0, 'msg': '提交成功,审核后展示'})audit_status=0是默认待审,后台管理页用audit_status=1过滤出已通过的展示。content截断到 500 字,防止有人塞长文。rating限制 1 到 5,前端做滑块。审核后台不用做得复杂,一个列表加「通过/驳回」两个按钮就够,老板自己每天花两分钟点一下。
4.2 周边信息:分类 + 距离排序才是可用状态
周边信息最容易做成一个静态页面,写死几家饭店就完事。但客人要的是「离我最近的」和「按类型筛」。所以poi表里category和distance_m两个字段必须用起来,接口支持按分类查、按距离排。
# routes/poi.py from flask import Blueprint, request, jsonify from models import Poi poi_bp = Blueprint('poi', __name__) @poi_bp.route('/api/poi') def list_poi(): category = request.args.get('category') query = Poi.query if category: query = query.filter_by(category=category) # 按距离从近到远排 items = query.order_by(Poi.distance_m.asc()).all() return jsonify({ 'code': 0, 'data': [{ 'name': p.name, 'category': p.category, 'distance': p.distance_m, 'desc': p.description } for p in items] })category走查询参数,前端做几个标签页:餐饮、景点、交通。order_by(Poi.distance_m.asc())保证最近的排前面。distance_m这个字段需要老板自己填,或者接地图 API 算,小民宿手动填几个常去的地方就够。别接太重的地图 SDK,加载慢还费钱。
4.3 三个功能怎么串在同一个订单流程里
WIFI、吐槽、周边不是三个独立页面,它们应该挂在订单详情页下面。客人订完房,订单页显示三块:WIFI 信息(入住日可见)、我的吐槽入口、周边推荐。这样客人不用记三个入口,老板也不用教。实现上就是订单详情接口一次性返回wifi_ready布尔值、feedback_id和poi_list,前端按状态渲染。
# 订单详情聚合返回 @booking_bp.route('/api/booking/<int:bid>') def booking_detail(bid): b = Booking.query.get(bid) room = Room.query.get(b.room_id) wifi_ready = date.today() >= b.check_in and room.wifi_ssid is not None fb = Feedback.query.filter_by(booking_id=bid).first() pois = Poi.query.order_by(Poi.distance_m.asc()).limit(5).all() return jsonify({ 'code': 0, 'data': { 'room': room.name, 'wifi_ready': wifi_ready, 'has_feedback': fb is not None, 'poi_top5': [p.name for p in pois] } })这个聚合接口减少了前端请求次数,也避免客人到处找功能。wifi_ready控制 WIFI 模块是否可点,has_feedback控制吐槽按钮显示「去吐槽」还是「已评价」,poi_top5只推最近五个,想看全部再跳周边页。
5. 避坑与排查:源码跑起来后最容易翻车的五件事
5.1 现象:WIFI 密码接口返回 403,但客人明明在入住日
原因:服务器时区和数据库日期对不上。date.today()取的是服务器本地时间,如果服务器是 UTC,而订单存的是北京时间,入住当天早上八点前会判定为「未到入住日」。
解决:统一用datetime.now()并显式指定时区,或者订单日期存字符串YYYY-MM-DD比较。我一般把服务器时区设成Asia/Shanghai,数据库连接串加?charset=utf8mb4和时区参数。
5.2 现象:吐槽提交成功但前台不显示
原因:audit_status默认 0,前台查询只查了audit_status=1,但后台审核页没做,老板不知道要去哪审。
解决:后台加一个/admin/feedback?status=0的列表页,登录后能看到待审列表。或者更简单,提交后给老板发一封邮件提醒。别指望老板自己找。
5.3 现象:周边信息按距离排序,但距离都是 0
原因:distance_m字段建表时DEFAULT 0,老板录入时没填,全默认 0,排序等于没排。
解决:录入表单把distance_m设为必填,或者用地图 API 根据地址自动算。手动录入时给个提示「步行几分钟约等于多少米」,降低填写门槛。
5.4 现象:二维码扫出来是127.0.0.1,客人打不开
原因:生成二维码时base_url用了本地测试地址,部署后没改。
解决:base_url从配置文件读,部署时改成实际域名或公网 IP。生成二维码的脚本单独跑,别混在启动流程里。
5.5 现象:多个客人同时提交吐槽,数据库报锁等待
原因:SQLite 并发写会锁库,MySQL 默认隔离级别下高并发也可能锁。
解决:小民宿用 SQLite 就把feedback提交做成队列或加timeout;用 MySQL 的话把audit_status加索引,减少全表扫描。更彻底的是吐槽提交走异步任务,先入 Redis 队列再落库,但小项目没必要,加个try/except重试就行。
6. 进阶技巧:用订单状态机把 WIFI、吐槽、周边串成自动化流程
前面几章把三个功能单独讲完了,但真正让系统「活」起来的是订单状态机。我习惯给订单加一个status字段:0待入住、1已入住、2已退房、3已评价。WIFI 信息在1时开放,吐槽在2之后才能提交,周边信息在1和2期间推送。这样不用人工干预,客人该看什么自动就看到了。
# 订单状态流转 def update_booking_status(booking_id, new_status): b = Booking.query.get(booking_id) old = b.status # 只有已退房才能评价 if new_status == 3 and old != 2: return {'code': 403, 'msg': '退房后才能评价'} b.status = new_status db.session.commit() # 退房后自动发一条吐槽邀请 if new_status == 2: send_feedback_invite(b.phone) return {'code': 0}send_feedback_invite可以接短信或微信模板消息,内容带上订单号和吐槽链接。这样吐槽率能翻一倍,因为客人刚退房时最有表达欲。WIFI 信息在状态变成1时,前端订单页自动从灰色变成可点。周边信息则在1和2期间显示在订单页底部,退房后隐藏。
验证这套流程是否跑通,我一般用三个测试用例:新订单状态 0 时 WIFI 不可见、状态 1 时可见、状态 2 时吐槽入口出现。用pytest写三个断言,每次改代码跑一遍,比手动点靠谱。
def test_wifi_visible_after_checkin(): # 模拟入住当天 b = create_booking(check_in=date.today()) update_booking_status(b.id, 1) resp = client.get(f'/api/wifi/{b.id}') assert resp.json['code'] == 0这套状态机不复杂,但能把三个散功能拧成一股绳。我踩过的最大坑是状态字段用字符串存,后来改成整型枚举,查询和判断都清爽了。源码这东西,跑起来只是第一步,能随着业务改还不乱,才算没白搭。希望帮到你。
本文还有配套的精品资源,点击获取