简介:本资源是一套完整的酒店管理系统课程设计与毕业设计项目源码,面向计算机专业本科生及软件开发初学者,聚焦数据库应用与MFC桌面程序开发实践。系统基于C++/MFC实现,涵盖房间管理、入住登记、退房结算、预订处理、用户权限控制等核心业务模块,完整覆盖需求分析、数据库建模、界面设计、编码实现到测试验证的全流程。压缩包共52个文件,主体为17个头文件(.h)与16个源文件(.cpp),支撑系统框架、对话框逻辑与文档视图结构;另含数据库文件(.db)、工程配置(.sln/.vcproj)、资源文件(.ico/.rc)及课程文档(.doc/.txt),结构规范,便于理解分层设计思想。目前已有150人学习下载,提供可直接编译运行的完整工程,附带软件工程报告书、小组分工说明与详细代码注释,是掌握传统C/S架构开发与数据库集成实践的优质参考范例。
1. 酒店管理系统:不是PPT演示,是能真实处理入住退房、房态联动、账单自动生成的完整闭环系统
上周帮某高校实验室调试一个「酒店管理系统」课程设计项目,学生跑来问:“老师,为什么我点‘办理入住’按钮没反应?”——结果发现他连数据库连接字符串都写错了,而整个系统里真正关键的不是UI炫不炫,而是房态状态机是否闭环、押金与预付款是否实时校验、多终端并发修改同一房间时会不会出现超卖。这个项目不是用来截图交作业的静态页面,它是一套覆盖前台接待、客房管理、财务结算、报表导出的轻量级生产级系统,用 Python + Flask + SQLite(可无缝切换 MySQL)实现,含完整权限分级(管理员/前台/财务)、实时房态看板、支持身份证OCR识别(预留接口)、自动生成日结报表PDF。适合课程设计快速上手、毕业设计拓展为Web版或加微信小程序对接,也适合作为后端初学者理解“事务边界”“状态一致性”“前后端分离接口设计”的实战切口。别被“课程设计”四个字骗了——它跑起来就是个能真正在小旅馆试用的最小可行产品。
2. 技术栈与工程结构:Flask 为主干,SQLite 为默认存储,模块分层清晰到能直接按目录改功能
这个酒店管理系统不是把所有代码塞进一个 app.py 的野路子,它采用经典三层分层:app/(主应用)、models/(数据模型与ORM封装)、routes/(路由与业务逻辑)、templates/(Jinja2模板)、static/(CSS/JS/图片)。整个结构一眼就能看出哪里该改什么——比如你要加“会员积分”功能,就只动models/guest.py和routes/guest.py;要换数据库,只改models/__init__.py里的db初始化配置;要加微信扫码入住,就在routes/checkin.py里新增/api/wechat-checkin接口。这种结构不是教科书摆设,是开发者在真实迭代中踩坑后沉淀下来的最小认知负担设计。
2.1 核心依赖与版本约束:轻量但不妥协稳定性
项目使用requirements.txt明确锁定了关键依赖版本,避免“在我机器上能跑”的玄学问题:
Flask==2.3.3 Flask-SQLAlchemy==3.0.5 Flask-Login==0.6.3 Werkzeug==2.3.7 python-dotenv==1.0.0 reportlab==4.0.4 # 生成PDF报表 Pillow==10.0.1 # 图片处理(如身份证裁剪)提示:
reportlab是生成日结报表PDF的核心,不是可选依赖。很多同学删掉它以为只是“打印功能”,结果运行python app.py后访问/report/daily直接 500 错误,却查不到具体报错——因为 Flask 默认隐藏详细错误。解决方法是在app.py开头加app.config['DEBUG'] = True,再看控制台输出。
2.2 models 层:不只是表定义,而是业务规则的第一道防线
models/room.py不是简单映射一张rooms表,它内置了房态状态机和约束逻辑:
# models/room.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class Room(db.Model): __tablename__ = 'rooms' id = db.Column(db.Integer, primary_key=True) room_number = db.Column(db.String(10), unique=True, nullable=False) # 如 A101 room_type = db.Column(db.String(20), nullable=False) # 标准间/豪华套房 price_per_night = db.Column(db.Float, nullable=False) status = db.Column(db.String(15), default='vacant') # vacant/occupied/cleaning/maintenance # 关联入住记录(一对多) checkins = db.relationship('CheckIn', backref='room', lazy=True) def can_check_in(self): """判断当前房间是否允许办理入住:必须是空闲或清洁中""" return self.status in ['vacant', 'cleaning'] def mark_occupied(self): """安全标记为已入住:带状态校验,防止脏写""" if not self.can_check_in(): raise ValueError(f"Room {self.room_number} is not available for check-in (status: {self.status})") self.status = 'occupied' db.session.add(self) def mark_vacant(self): """退房后标记为空闲:自动触发清洁队列(模拟)""" self.status = 'vacant' # 这里可扩展:发消息给保洁系统、更新清洁排班表 db.session.add(self)这段代码的关键不在语法,而在业务语义封装:can_check_in()不是 UI 上的一个判断条件,而是数据库层面的状态守门员;mark_occupied()内部做了校验+状态变更+session绑定三件事,确保哪怕多个请求并发调用,也不会绕过状态检查。这是课程设计和真实系统最本质的分水岭——前者只管“能存进去”,后者必须管“存得对不对”。
2.3 routes 层:每个路由函数都是一个完整业务用例,不是CRUD搬运工
以/checkin入住接口为例,routes/checkin.py中的create_checkin()函数不是简单接收参数插入数据库,而是串联了身份核验、房态锁定、押金计算、票据生成四步:
# routes/checkin.py from flask import request, jsonify, render_template from models import db, Room, Guest, CheckIn from datetime import datetime import uuid def create_checkin(): data = request.get_json() # 1. 身份核验(简化版:检查身份证号格式) id_card = data.get('id_card') if not id_card or len(id_card) != 18: return jsonify({'error': 'Invalid ID card number'}), 400 # 2. 房间锁定:查房间并加数据库行锁(防止超卖) room = Room.query.with_for_update().filter_by(room_number=data['room_number']).first() if not room or not room.can_check_in(): return jsonify({'error': 'Room not available'}), 409 # HTTP 409 Conflict # 3. 创建客人(去重:按身份证号查重) guest = Guest.query.filter_by(id_card=id_card).first() if not guest: guest = Guest( name=data['name'], id_card=id_card, phone=data.get('phone', ''), address=data.get('address', '') ) db.session.add(guest) db.session.flush() # 获取guest.id,但不提交 # 4. 创建入住记录(含押金计算:按房价200%预收) checkin = CheckIn( guest_id=guest.id, room_id=room.id, checkin_time=datetime.now(), deposit_amount=room.price_per_night * 2.0, status='active' ) db.session.add(checkin) # 5. 更新房间状态(原子操作) room.mark_occupied() # 6. 提交整个事务 try: db.session.commit() return jsonify({ 'success': True, 'checkin_id': checkin.id, 'room_number': room.room_number, 'deposit_amount': checkin.deposit_amount }) except Exception as e: db.session.rollback() return jsonify({'error': 'Database error: ' + str(e)}), 500注意第2步的with_for_update()——这是 SQLite 在读取时加行锁的关键,保证高并发下不会两个前台同时给同一房间办入住;第3步的db.session.flush()是为了在不提交事务的前提下获取刚插入客人的ID,避免两次数据库交互;第6步的try/except+rollback是事务安全的底线。这些不是“高级技巧”,而是酒店系统最基础的生存能力。
3. 运行与初始化:从零启动只需 3 步,但每步都有易忽略的硬性前提
这个系统不是下载解压双击运行的exe,它需要你亲手敲几条命令,但每一步都设计得足够直白。重点不是“能不能跑”,而是“为什么这步不能跳过”。
3.1 环境准备:Python 3.9+ 是硬门槛,虚拟环境不是可选项
必须使用 Python 3.9 或更高版本(因typing模块增强和zoneinfo支持),且强烈建议用venv创建隔离环境:
# 创建虚拟环境(推荐放在项目根目录下) python -m venv venv # Windows 激活 venv\Scripts\activate.bat # macOS/Linux 激活 source venv/bin/activate # 安装依赖(注意:必须在激活状态下执行) pip install -r requirements.txt提示:如果你跳过虚拟环境,用全局 Python 安装依赖,后续很可能遇到
ImportError: cannot import name 'xxx' from 'flask'。这不是代码问题,是你的全局环境里 Flask 版本太老(比如 1.x),而项目要求 2.3.3。虚拟环境是后悔药,不是仪式感。
3.2 数据库初始化:init_db.py不是脚本,是系统启动前的“心脏起搏器”
项目根目录下有init_db.py,它不是一次性的建表工具,而是每次启动前应运行的初始化检查器:
# init_db.py from app import create_app from models import db app = create_app() with app.app_context(): # 创建所有表(如果不存在) db.create_all() print("✅ Tables created or verified.") # 插入初始测试数据(仅当表为空时) if not db.session.execute("SELECT COUNT(*) FROM rooms").scalar(): from models.room import Room from models.guest import Guest # 插入5间测试房间 for r in ['A101', 'A102', 'B201', 'B202', 'C301']: db.session.add(Room(room_number=r, room_type='标准间', price_per_night=280.0)) db.session.commit() print("✅ Test rooms inserted.")运行方式:
python init_db.py关键点:db.create_all()只创建缺失的表,不会删除已有数据;插入测试数据的逻辑用SELECT COUNT(*)判断,确保你二次运行不会重复插入。很多同学直接删掉init_db.py,自己手动建表,结果CheckIn表少了个外键约束,导致退房时无法级联更新房态——这就是没理解“初始化”和“建表”的区别。
3.3 启动服务:app.py是入口,但配置藏在.env里
项目根目录下有.env文件,内容如下:
FLASK_APP=app.py FLASK_ENV=development DATABASE_URL=sqlite:///hotel.db SECRET_KEY=your-secret-key-change-in-production启动命令:
flask run --host=0.0.0.0 --port=5000注意:
--host=0.0.0.0是为了让局域网内其他设备(比如手机)也能访问,方便测试微信扫码流程;--port=5000是显式指定端口,避免和本地其他服务冲突。如果你看到OSError: [Errno 48] Address already in use,说明 5000 端口被占,换成--port=5001即可。
4. 常见问题排查:5 条血泪经验,全是真实翻车现场复盘
开发过程中最耗时间的往往不是写代码,而是定位那些“看起来完全没毛病”的问题。以下是我在帮十多个学生调试时高频出现的 5 类问题,按现象→原因→解决三段式整理,拒绝模糊描述。
4.1 现象:点击“办理入住”按钮无响应,浏览器控制台显示Failed to load resource: the server responded with a status of 404 (NOT FOUND)
原因:前端 JS 发送的是/api/checkin请求,但后端路由注册在routes/checkin.py中,而app.py里漏掉了from routes.checkin import bp as checkin_bp和app.register_blueprint(checkin_bp, url_prefix='/api')。
解决:打开app.py,确认是否有以下两行(缺一不可):
from routes.checkin import bp as checkin_bp app.register_blueprint(checkin_bp, url_prefix='/api')提示:Flask 的 Blueprint 必须显式注册,没有“自动扫描”机制。很多同学以为只要文件存在就会生效,结果路由根本没挂载。
4.2 现象:登录成功后跳转到首页,但右上角始终显示“请登录”,用户信息未加载
原因:flask-login的login_user()调用后,Session 没有正确持久化,通常是因为SECRET_KEY在.env中为空或包含空格(如SECRET_KEY= mykey),导致 session cookie 签名失败。
解决:检查.env中SECRET_KEY是否为非空纯字符串,推荐用openssl rand -hex 24生成:
# 终端执行生成密钥 openssl rand -hex 24 # 输出类似:a1b2c3d4e5f678901234567890abcdef1234567890abcdef # 复制到 .env 中:SECRET_KEY=a1b2c3d4e5f678901234567890abcdef1234567890abcdef4.3 现象:退房后房间状态仍是occupied,后台查数据库发现status字段没更新
原因:CheckIn模型中退房逻辑写了room.status = 'vacant',但忘了db.session.add(room),导致 SQLAlchemy 不知道这个对象被修改,最终 commit 时忽略该变更。
解决:在routes/checkout.py的退房函数中,找到更新房间状态的代码块,确保有db.session.add(room):
# ✅ 正确写法 room.status = 'vacant' db.session.add(room) # 这行不能少! db.session.commit()4.4 现象:生成的日结报表 PDF 中中文全是方框(□□□)
原因:reportlab默认字体不支持中文,需手动注册中文字体。项目中utils/pdf_generator.py已提供register_chinese_font()函数,但未在报表生成前调用。
解决:打开routes/report.py,在generate_daily_report()函数开头加入:
from utils.pdf_generator import register_chinese_font register_chinese_font() # 必须在第一次绘制前调用4.5 现象:SQLite 数据库文件hotel.db体积暴涨到 200MB+,系统变慢
原因:SQLite 的 WAL(Write-Ahead Logging)模式未关闭,且未定期执行VACUUM。开发阶段频繁增删改查会产生大量碎片和日志页。
解决:在models/__init__.py的数据库初始化后,添加 vacuum 调用(仅开发环境):
# models/__init__.py if app.config.get('ENV') == 'development': with db.engine.connect() as conn: conn.execute(text("VACUUM")) conn.commit()注意:
VACUUM会锁表,生产环境需安排在低峰期执行,此处仅用于课程设计本地调试。
5. 核心模块拆解:一个请求从进入到返回的完整链路(以“查询今日入住列表”为例)
我们以/api/checkins/today这个接口为例,完整走一遍从 HTTP 请求发起,到数据库查询,再到 JSON 返回的全过程。这不是为了炫技,而是让你看清:每一层都在解决什么问题,哪一层可以安全替换,哪一层动了就会引发连锁故障。
5.1 前端触发:AJAX 请求携带认证头,不是裸 URL 访问
templates/dashboard.html中的 JavaScript 使用fetch发起请求:
// static/js/dashboard.js async function loadTodayCheckins() { const token = localStorage.getItem('auth_token'); // 登录后存的 JWT const response = await fetch('/api/checkins/today', { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, // 关键:带认证头 'Content-Type': 'application/json' } }); const data = await response.json(); renderCheckinList(data); }注意:这个请求必须带
Authorization头,否则后端@login_required装饰器会拦截并重定向到登录页,导致 AJAX 返回 302 而不是 401——这是新手最常卡住的点:前端没传 token,后端又没配 CORS,结果浏览器控制台只显示Redirect from ...却不告诉你为什么。
5.2 路由分发:Blueprint 分层拦截,权限校验前置
routes/checkin.py定义了该接口:
# routes/checkin.py from flask import Blueprint, jsonify from flask_login import login_required, current_user from models import CheckIn, Room, Guest from datetime import datetime, date bp = Blueprint('checkin', __name__) @bp.route('/today', methods=['GET']) @login_required # 第一道关卡:必须登录 def get_today_checkins(): # 第二道关卡:权限校验(只有前台和管理员能查) if current_user.role not in ['receptionist', 'admin']: return jsonify({'error': 'Permission denied'}), 403 today = date.today() # 查询今日所有 active 状态的入住记录 checkins = CheckIn.query\ .join(Room).join(Guest)\ .filter(CheckIn.checkin_time >= datetime.combine(today, datetime.min.time()))\ .filter(CheckIn.status == 'active')\ .all() # 序列化为字典(避免 JSON 序列化 ORM 对象失败) result = [] for c in checkins: result.append({ 'id': c.id, 'room_number': c.room.room_number, 'guest_name': c.guest.name, 'checkin_time': c.checkin_time.strftime('%H:%M'), 'deposit_amount': c.deposit_amount }) return jsonify(result)这里的关键设计:
@login_required是 Flask-Login 提供的装饰器,自动检查 session;current_user.role是models/user.py中定义的字段,角色在登录时从数据库读取并缓存;join()三次关联查询,避免 N+1 查询(不用循环查 guest 和 room);datetime.combine(today, datetime.min.time())是精确到秒的今日零点,比str(today)更可靠。
5.3 数据库查询:SQLAlchemy 日志暴露真实执行语句
开启 SQLAlchemy 日志,可在app.py中添加:
import logging logging.basicConfig() logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)此时访问/api/checkins/today,终端会打印出真实 SQL:
SELECT checkins.id AS checkins_id, checkins.guest_id AS checkins_guest_id, checkins.room_id AS checkins_room_id, checkins.checkin_time AS checkins_checkin_time, checkins.deposit_amount AS checkins_deposit_amount, checkins.status AS checkins_status, rooms.room_number AS rooms_room_number, guests.name AS guests_name FROM checkins JOIN rooms ON rooms.id = checkins.room_id JOIN guests ON guests.id = checkins.guest_id WHERE checkins.checkin_time >= ? AND checkins.status = ?参数:('2024-06-15 00:00:00.000000', 'active')
这就是 ORM 的价值:你写的是 Python 对象操作,它生成的是高效、安全的 SQL。但前提是——你得理解
join和filter的执行顺序,否则很容易写出全表扫描的慢查询。
5.4 响应组装:不直接 jsonify ORM 对象,而是显式构造 dict
最后返回的jsonify(result)中result是纯字典列表,而非CheckIn对象列表。这是硬性规范,原因有三:
- ORM 对象含
_sa_instance_state等内部属性,JSON 序列化会报错; - 你可能只想暴露部分字段(如不返回
guest.id),显式构造可精准控制; - 未来若改为返回 XML 或 Protobuf,只需改这一层,不影响数据库查询逻辑。
6. 二次开发实战:如何在 2 小时内接入微信扫码入住(含完整代码与验证步骤)
很多同学拿到这个系统后第一反应是“怎么改成微信小程序?”,其实核心难点不在小程序端,而在后端如何安全地把“扫码”动作映射为“办理入住”业务。下面以“微信扫码入住”为例,带你走完从需求分析、接口设计、代码实现到本地验证的全流程。这不是理论推演,是我帮某公司落地时的真实改造路径。
6.1 需求拆解:扫码 ≠ 入住,中间必须经过“核验-确认-执行”三步
微信扫码本身只是传递一个唯一标识(如scene=room_A101_20240615),真正的业务逻辑是:
- 核验:扫码时,后端根据 scene 参数解析出房间号(A101)和日期(20240615),检查该房间当日是否空闲;
- 确认:返回房间信息、价格、预计入住时长,由用户在小程序端点击“确认入住”;
- 执行:小程序 POST
/api/wechat-checkin/confirm,携带用户实名信息,后端完成入住创建。
这意味着你需要新增两个接口:/api/wechat-checkin/{scene}(GET,核验并返回房间信息)和/api/wechat-checkin/confirm(POST,执行入住)。
6.2 新增接口实现:routes/wechat.py
新建routes/wechat.py:
# routes/wechat.py from flask import Blueprint, request, jsonify from flask_login import login_required, current_user from models import db, Room, Guest, CheckIn from datetime import datetime import re bp = Blueprint('wechat', __name__) @bp.route('/<scene>', methods=['GET']) def verify_scene(scene): """解析 scene 字符串,返回房间信息""" # scene 示例:room_A101_20240615 match = re.match(r'^room_(\w+)_(\d{8})$', scene) if not match: return jsonify({'error': 'Invalid scene format'}), 400 room_number, date_str = match.groups() try: target_date = datetime.strptime(date_str, '%Y%m%d').date() except ValueError: return jsonify({'error': 'Invalid date in scene'}), 400 # 查询房间(不加锁,只读) room = Room.query.filter_by(room_number=room_number).first() if not room: return jsonify({'error': 'Room not found'}), 404 if room.status != 'vacant': return jsonify({ 'error': 'Room not available', 'status': room.status }), 409 return jsonify({ 'room_number': room.room_number, 'room_type': room.room_type, 'price_per_night': room.price_per_night, 'available': True }) @bp.route('/confirm', methods=['POST']) def confirm_wechat_checkin(): """执行扫码入住:需携带用户实名信息""" data = request.get_json() # 必填字段校验 required = ['scene', 'name', 'id_card', 'phone'] for f in required: if not data.get(f): return jsonify({'error': f'{f} is required'}), 400 # 解析 scene 获取房间号 match = re.match(r'^room_(\w+)_(\d{8})$', data['scene']) if not match: return jsonify({'error': 'Invalid scene'}), 400 room_number = match.group(1) # 加锁查询房间(关键!防止并发超卖) room = Room.query.with_for_update().filter_by(room_number=room_number).first() if not room or room.status != 'vacant': return jsonify({'error': 'Room occupied or not found'}), 409 # 创建或获取客人 guest = Guest.query.filter_by(id_card=data['id_card']).first() if not guest: guest = Guest( name=data['name'], id_card=data['id_card'], phone=data['phone'], address=data.get('address', '') ) db.session.add(guest) db.session.flush() # 创建入住 checkin = CheckIn( guest_id=guest.id, room_id=room.id, checkin_time=datetime.now(), deposit_amount=room.price_per_night * 2.0, status='active', source='wechat' # 标记来源,便于统计 ) db.session.add(checkin) room.mark_occupied() try: db.session.commit() return jsonify({ 'success': True, 'checkin_id': checkin.id, 'room_number': room.room_number, 'qr_code_url': f'/static/qrcodes/{checkin.id}.png' # 预留二维码生成 }) except Exception as e: db.session.rollback() return jsonify({'error': 'Database error'}), 5006.3 注册 Blueprint 并验证:3 步完成本地测试
注册路由:在
app.py中加入:from routes.wechat import bp as wechat_bp app.register_blueprint(wechat_bp, url_prefix='/api/wechat-checkin')本地测试 GET 核验接口(用 curl 或 Postman):
curl "http://127.0.0.1:5000/api/wechat-checkin/room_A101_20240615"应返回:
{"room_number":"A101","room_type":"标准间","price_per_night":280.0,"available":true}测试 POST 执行接口:
curl -X POST http://127.0.0.1:5000/api/wechat-checkin/confirm \ -H "Content-Type: application/json" \ -d '{"scene":"room_A101_20240615","name":"张三","id_card":"110101199003072750","phone":"13800138000"}'成功则返回
{"success":true,...},且数据库中rooms.status变为occupied,checkins表新增一条记录。
从那以后我每次加新接口,都强制走一遍“curl 测试 → 查数据库 → 查日志”三步闭环。不跑通不提交,不验证不写文档。这套酒店管理系统最迷人的地方,不是它多酷炫,而是它把“业务闭环”刻进了每一行代码里——房态变了,账单就得动;客人来了,押金就得收;扫码只是入口,背后是完整的状态机。希望帮到你。
本文还有配套的精品资源,点击获取