有人把毕业设计题目定为“酒店在线预订系统”上来就问我要全套源码。我其实一直不太推荐直接拿现成代码交差因为这类题目真正的价值在于把 Flask 后端、Vue 前端、数据库设计、接口联调这一整条链路打通。我花了两周多做了一个可本地部署的版本技术栈就是 Flask Vue数据库用 SQLite简单直接没有任何重型中间件。这里把我设计这套系统时的完整思路、核心代码、遇到的坑和排查过程都写出来给准备做类似全栈项目的朋友一个参考。这套系统有几块硬骨头房态和订单的冲突检测、前后端分离后的跨域与鉴权、订单流程的状态机设计。单独拆开看都不难但组合在一起就会冒出各种小问题。我会直接用实际代码和报错信息说话尽量减少水分。1. 从需求到选型为什么是 Flask Vue 而不是一套模板到底1.1 酒店预订系统的核心业务边界做系统之前先划清业务边界。酒店预订系统不是电商系统核心流程就两条用户查房、下单预订管理员管房态、管订单。如果一上来就上支付、退款、积分体系项目复杂度会迅速失控。我最后圈定的范围是用户注册登录后按入住日期、离店日期、房型筛选可订房间。提交预订时锁定房间防止同一时间段被重复下单。用户可查看自己的订单管理员可管理房间信息、确认或取消订单。所有数据落地 SQLite支持本地一键启动。这个范围既能覆盖全栈的核心知识点又不会因为范围过大导致论文写不深。1.2 为什么后端选 Flask 而不是 Django这个问题几乎每个做这类题目的人都会纠结。我的结论是如果你的项目需要 7 天内能跑通、代码要能写清楚每一步逻辑Flask 的轻量反而是优势。Django 自带 Admin 后台、ORM、认证体系开发效率确实高但对新手来说它做了太多“黑盒”工作答辩时如果被问到底层原理往往答不上来。Flask 这边路由、请求上下文、ORM 映射都是显式写出来的你能清楚说出每一次 HTTP 请求经过了什么。配合 Flask-SQLAlchemy 做 ORM、Flask-JWT-Extended 做登录态、Flask-CORS 解决跨域基本就是一套标准的轻量后端范式。而且 Flask 写接口遵循 RESTful 风格哪个接口创建了资源、哪个接口修改了状态看一眼路由函数的名字和 HTTP 方法就一目了然。1.3 为什么前端选 Vue 而不是后端模板渲染如果是纯 Flask 项目用 Jinja2 渲染模板当然也行但酒店预订系统里有很多交互密集的场景日期范围选择、房间卡片动态筛选、订单状态的即时变化提醒。这类场景用 Vue 的响应式数据绑定写起来自然得多也比一段一段的 jQuery 操作 DOM 要清晰。我这里选的是 Vue 3 Vite Element Plus。Element Plus 提供了现成的日期选择器、表格、表单校验能省下大量造轮子的时间。Vite 的冷启动速度比 Webpack 快太多开发环境下改代码即时刷新这对调试前端样式和接口联调非常重要。1.4 数据库模型设计三张表撑起整个业务酒店预订系统的核心数据模型我用三张表就够用。表结构设计的时候我特意把“时间”字段设计成日期字符串而不是时间戳因为酒店预订按天计算不需要精确到时分秒。表名字段说明usersid, username, password_hash, phone, create_timepassword_hash 存储哈希后的密码不存明文roomsid, room_number, room_type, price, max_occupancy, descriptionroom_type 比如大床房、双床房、套房bookingsid, user_id, room_id, check_in_date, check_out_date, total_price, status, create_timestatus 有 pending、confirmed、checked_in、cancelled 四种bookings 表是核心。这里有个关键点预订记录的 check_out_date 表示离店日期该日期当天房间已经释放可以接待下一位客人。例如入住 8 月 1 日、离店 8 月 3 日那么 8 月 3 日晚上这个房间就是可订的。这个语义如果不统一后面写冲突检测代码必然出错。# models.py 核心模型定义 from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) phone db.Column(db.String(20)) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Room(db.Model): __tablename__ rooms id db.Column(db.Integer, primary_keyTrue) room_number db.Column(db.String(10), uniqueTrue, nullableFalse) room_type db.Column(db.String(50), nullableFalse) price db.Column(db.Float, nullableFalse) max_occupancy db.Column(db.Integer, default2) class Booking(db.Model): __tablename__ bookings id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) room_id db.Column(db.Integer, db.ForeignKey(rooms.id), nullableFalse) check_in_date db.Column(db.String(20), nullableFalse) check_out_date db.Column(db.String(20), nullableFalse) total_price db.Column(db.Float, nullableFalse) status db.Column(db.String(20), defaultconfirmed) create_time db.Column(db.DateTime, defaultdatetime.utcnow)字段类型这里特别注意一下日期字段用 String 而不用 DateTime是因为 SQLite 对 DateTime 的支持比较弱日期范围查询时字符串比较反而更简洁。反正格式统一为YYYY-MM-DD字典序和日期序完全一致。2. 预订冲突检测一个 SQL 条件解决了时间重叠的难题2.1 需求拆解什么样的订单算冲突订房冲突检测是这套系统的核心算法题面试也常问。先定义清楚场景新订单要订某房间入住日期为 A离店日期为 B已存在的订单入住日期为 C离店日期为 D。当且仅当新订单的日期区间和已有订单的日期区间有重叠时新订单不可创建。举例来说已有订单8 月 5 日入住8 月 8 日离店。新订单 A8 月 8 日入住8 月 10 日离店。不冲突因为原订单 8 日当天已释放房间。新订单 B8 月 7 日入住8 月 9 日离店。冲突因为占用日期交集为 7 日。新订单 C8 月 6 日入住8 月 7 日离店。冲突因为 6 日当晚原订单仍在住。新订单 D8 月 4 日入住8 月 5 日离店。不冲突因为 5 日晚上房间全部释放。这里很容易写错的地方就是把等号判断也当成冲突。如果原订单 8 月 8 日离店新订单 8 月 8 日入住这是合法的翻房场景必须允许。2.2 冲突检测 SQL 的两种写法检测一个房间在给定日期范围内是否被预订SQL 条件有两种等价写法。第一种是逻辑上的直接翻译N.check_in E.check_out AND N.check_out E.check_in。第二种是先查所有时间区间再在 Python 里过滤。对于本地 SQLite 数据库来说直接用 ORM 查询让数据库做区间判断是最省事的from sqlalchemy import and_, or_ def check_room_available(room_id, check_in, check_out): # 排除已取消的订单 active_statuses [confirmed, checked_in] conflict Booking.query.filter( Booking.room_id room_id, Booking.status.in_(active_statuses), Booking.check_in_date check_out, # 已有入住日期 新订单离店日期 Booking.check_out_date check_in # 已有离店日期 新订单入住日期 ).first() return conflict is None这个查询逻辑是两个区间不重叠等价于一个区间的结束时间 另一个区间的开始时间。所以冲突就是已有入住 新离店 AND 已有离店 新入住一个范围交叉条件即可。注意这里的比较操作只要 SQLite 存储的日期字符串格式固定字符串大小比较结果和日期先后完全一致。这也是我坚持用YYYY-MM-DD格式存储的原因。2.3 总价计算与前端预验证价格计算软件设计上有个常见的坑前端算一次总价只用于展示后端必须再算一次作为最终结果绝不能再把前端传上来的 total_price 直接入库。一旦用户通过浏览器开发者工具改了请求体里的价格系统就会以错误价格成交。正确的做法是后端根据房间单价和日期差重新乘法def calculate_total_price(room_price, check_in, check_out): date_format %Y-%m-%d check_in_date datetime.strptime(check_in, date_format) check_out_date datetime.strptime(check_out, date_format) days (check_out_date - check_in_date).days return room_price * days我在前端订单确认页用到了 Element Plus 的DatePicker组件配置disabledDate函数让入住日期以前的日期、以及离店日期之前的日期都不可选这样能在交互上提前拦截大多数非法时间段后端再校验兜底。el-date-picker v-modeldateRange typedaterange range-separator至 start-placeholder入住日期 end-placeholder离店日期 :disabled-datedisabledDate value-formatYYYY-MM-DD /const disabledDate (date) { // 只允许选择今天及以后的日期 return date.getTime() Date.now() - 24 * 60 * 60 * 1000; };3. Vue 前端与 Flask 后端联调环境的坑、鉴权的坑、请求的坑3.1 Vite 代理解决开发环境跨域前后端分离开发时前端跑在 5173 端口Flask 跑在 5000 端口直接通过 axios 请求后端接口会被 CORS 拦截。解决方案有两种后端启用 Flask-CORS 打开跨域或在前端开发服务器配置代理。我在开发时两种都配了但部署时更建议用前端代理。Vite 的代理配置在vite.config.js中import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这样前端代码里请求路径直接写/api/roomsVite 会把请求转发到http://127.0.0.1:5000/rooms。好处是开发环境前端代码中不需要写死后端地址部署时只要用 Nginx 把/api前缀转发到 Flask 服务即可前后端代码都不用改。3.2 登录鉴权JWT 还是 Session酒店预订系统的用户登录态问题一开始我用了 Flask 自带的 Session配合secret_key给客户端签发了 session cookie。逻辑很简单但前后端分离的时候 Session 下有几个麻烦事要处理 CSRF 防护、跨域时 Cookie 的SameSite属性配置容易漏、移动端或其他客户端无法直接携带 Cookie。后来我改成了 JWTJSON Web Token方案。用户登录成功后后端签发一个带过期时间的 token 字符串前端拿到后存储在 localStorage 里。之后所有请求在 axios 拦截器中统一加上Authorization: Bearer token头后端通过 Flask-JWT-Extended 的jwt_required()装饰器保护需要登录的接口。# auth.py 登录接口 from flask_jwt_extended import create_access_token app.route(/auth/login, methods[POST]) def login(): data request.get_json() username data.get(username) password data.get(password) user User.query.filter_by(usernameusername).first() if user and user.check_password(password): token create_access_token(identityuser.id) return jsonify(access_tokentoken, usernameuser.username) return jsonify(message用户名或密码错误), 401前端 axios 拦截器统一加 token 的做法也很关键不需要每个业务接口都写一遍鉴权代码// request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })3.3 订单列表状态显示与操作交互前端“我的订单”页面是整个系统交互逻辑最多的页面。有几种状态需要展示待确认、已确认、已入住、已取消。管理员端还需要对订单执行“确认”和“取消”操作要让操作按钮按状态显示在不同位置。我的做法是在页面里用el-tag组件结合状态枚举映射显示标签颜色const statusMap { pending: { label: 待确认, type: warning }, confirmed: { label: 已确认, type: success }, checked_in: { label: 已入住, type: primary }, cancelled: { label: 已取消, type: info } }然后订单卡片根据状态决定显示哪些操作按钮。还有一个细节取消订单后原房间的时间段必须自动释放供其他用户预订。这要求后端取消接口只修改订单状态而不删除订单记录这样用户仍能看到历史订单记录同时房间冲突检测会自动跳过已取消的订单。app.route(/bookings/int:booking_id/cancel, methods[POST]) jwt_required() def cancel_booking(booking_id): user_id get_jwt_identity() booking Booking.query.filter_by(idbooking_id, user_iduser_id).first() if not booking: return jsonify(message订单不存在), 404 if booking.status not in [pending, confirmed]: return jsonify(message当前状态不可取消), 400 booking.status cancelled db.session.commit() return jsonify(message订单已取消)这里有个值得思考的细节取消订单时我没有校验“入住日期是否已过”。如果入住日期已过且用户刚确认直接允许取消其实会造成酒店损失但业务复杂度会上升。课程设计层面保持状态机简单即可不做逆向检查但可以在前端根据日期禁用“已过期订单的取消按钮”。4. 前后端分离后部署本地跑通才是王道4.1 Flask 与 Vue 的目录组织和生产构建开发环境前后端分开跑没问题但演示和答辩时最好做到启动一个 Flask 进程就能同时提供后端 API 和前端页面。这时需要构建 Vue 项目并把构建产物交给 Flask 托管。Vue 构建命令很简单npm run build构建完成后Vue 生成dist目录。Flask 这边只需要增加一个静态文件路由把前端路由都导向index.html同时让 API 请求继续走自己的路由规则。这个配置看似简单但有个经典陷阱如果直接app.route(/)返回index.html那么 Vue Router 使用 history 模式时刷新某个子路径比如/roomsFlask 会返回 404。解决方式是配置一个 catch-all 路由只要路径不是/api开头都返回index.html。# app.py 部署配置 from flask import send_from_directory, render_template import os # 生产环境托管前端构建产物 app.route(/, defaults{path: }) app.route(/path:path) def serve_frontend(path): dist_dir os.path.join(os.path.dirname(__file__), dist) if path and os.path.exists(os.path.join(dist_dir, path)): return send_from_directory(dist_dir, path) return send_from_directory(dist_dir, index.html)4.2 Linux 服务器部署 Flask 应用的踩坑记录把项目部署到 Linux 服务器时我踩了一堆环境相关的坑逐个记录一下坑一Python 版本不对。服务器自带的可能还是 3.8但我本地开发用了 3.11 的语法特性。解决方法很简单部署前在项目根目录写清楚依赖版本然后服务器上建虚拟环境重新安装依赖python3 -m venv venv source venv/bin/activate pip install -r requirements.txt坑二Flask 默认开发服务器不能用于生产。用app.run()启动时 Flask 会自动开启 debug 模式和 reloader这在服务器上会红屏刷日志不仅性能差还会暴露报错详情。部署时用 gunicorn 启动 WSGI 应用gunicorn -w 2 -b 0.0.0.0:5000 app:app坑三SQLite 文件路径问题。数据库文件若使用相对路径sqlite:///hotel.db从项目根目录启动没问题但用 systemd 服务或 cron 任务启动时当前目录不同Flask 可能创建出另一个空数据库文件。最稳妥的方案是使用绝对路径如下import os BASE_DIR os.path.abspath(os.path.dirname(__file__)) app.config[SQLALCHEMY_DATABASE_URI] sqlite:/// os.path.join(BASE_DIR, hotel.db)4.3 应用启动报错 ModuleNotFoundError 排查过程部署时遇到最典型的一个报错是ModuleNotFoundError: No module named flask_sqlalchemy。其实这是虚拟环境和全局环境混淆导致的。排查过程如下先确认which python指向虚拟环境路径。再pip list查看当前环境是否装了相关包。发现虚拟环境没问题但启动还是报错最后发现是 gunicorn 直接调用了系统 Python没有激活虚拟环境。解决方式是让 gunicorn 使用虚拟环境里的可执行文件/path/to/project/venv/bin/gunicorn -w 2 -b 0.0.0.0:5000 app:app实际排查这类问题建议先跑一句完整路径/path/to/project/venv/bin/python app.py如果能起来说明环境本身没问题只是启动方式错了。5. 前端环境配置与 Vue 开发中的常见问题5.1 VSCode 中配置 Vue 开发环境Vue 项目的开发环境配置其实很容易出错尤其是刚接触 VSCode Vue 的人。一个重要经验是不要试图在系统全局安装 Vue CLI 来创建项目。不同项目的 Vue 版本差异会导致全局脚手架版本错乱。正确做法是用npm create vuelatest它会自动在项目目录安装对应的 Vue 依赖完全不污染全局环境。npm create vuelatest hotel-frontend创建过程中按需选择 Router、Pinia、ESLint 等特性。VSCode 中还需要安装 Volar 插件Vue 官方推荐的语法高亮和类型检查插件禁用旧的 Vetur 插件避免冲突。5.2 Vue 路由的传参与动态路由酒店预订系统中有一个常见需求从房间列表页点击某个房间卡片后跳转到房间详情页并在详情页带出该房间的信息。这里用了 Vue Router 的路径参数。// router/index.js const routes [ { path: /rooms/:id, name: RoomDetail, component: () import(../views/RoomDetail.vue) } ]跳转时使用router.push({ name: RoomDetail, params: { id: roomId }})在RoomDetail.vue中获取参数import { useRoute } from vue-router const route useRoute() const roomId route.params.id一个常见误区是尝试在 URL 参数里传递整个房间对象不仅会让 URL 变得臃肿刷新页面后还取不到对象数据。正确做法是只传 ID详情页再通过接口获取完整数据。这里还涉及一个动态路由的问题如果用户访问/rooms/999不存在的房间 ID前端会渲染出空白详情页。我实现了一个兜底逻辑接口返回 404 时前端跳转到 404 页面或房间列表页并弹出提示。5.3 环境变量与 API 地址管理虽然开发环境用了 Vite 代理但有时候确实需要直接访问后端地址比如在手机上调试。这种情况下前端写死http://localhost:5000会失效。正确处理方式是在项目根目录创建.env.development和.env.production文件配置不同的后端接口地址。相关文件内容VITE_API_BASE_URL/api生产环境VITE_API_BASE_URLhttp://your-server-ip:5000/api然后在 axios 请求中使用import.meta.env.VITE_API_BASE_URL作为 baseURL。javascript const baseURL import.meta.env.VITE_API_BASE_URL || /apiconst request axios.create({ baseURL: baseURL, timeout: 10000 })这样开发环境和生产环境用不同配置构建时自动切换非常爽。 ## 6. 从演示到答辩功能验证清单与后续优化方向 ### 6.1 完整的功能验证流程 答辩和演示之前我给系统整理了一份功能测试清单按顺序一步步走确保所有环节都通顺。这能避免演示时遇到尴尬情况 | 步骤 | 操作 | 预期结果 | |---|---|---| | 1 | 注册新账号并登录 | 登录状态写入 token跳转到首页 | | 2 | 选择日期范围入住 8 月 8 日离店 8 月 10 日 | 房间列表刷新展示可订房间 | | 3 | 进入 101 房详情提交预订 | 下单成功订单状态为待确认 | | 4 | 再次用同一个房间、同一时间提交预订 | 后端返回“该房间已被预订”的错误提示 | | 5 | 管理员后台确认第 3 步订单 | 订单状态变为已确认 | | 6 | 用户订单列表查看该订单 | 显示已确认且总价正确 | | 7 | 用户取消订单重新预订同房间同时间 | 预订成功说明房间时间片已释放 | | 8 | 用不存在的账号或错误密码登录 | 返回 401 错误信息前端弹出提示 | 这套流程走完核心链路就覆盖了。其中第 4 步和第 7 步最考验系统逻辑建议演示前重点测完。 ### 6.2 性能与并发课程设计级别的取舍 酒店预订系统天然是并发敏感的系统。多个人同时抢同一房间的最后余量如果处理不好会出现超卖。我的系统基于 SQLite Flask 开发服务器本质是单进程服务同一时刻只能有一个请求真正执行写操作所以冲突检测在逻辑上不会出现并发问题。 但如果换成 Gunicorn 多 worker 部署Flask 会有多个进程同时访问 SQLite就可能出现两个请求同时通过冲突检测、同时写入订单的情况。处理办法是给资源表加行级锁或使用数据库事务把“查冲突”和“写订单”包在同一个原子操作里。SQLite 的 BEGIN IMMEDIATE 事务可以通过会话隔离范围达到这个效果。不过做毕业设计的话单 worker 部署加上 SQLite 已经足够稳定不必深挖并发。 ### 6.3 可以继续完善的优化方向 如果能顺利跑通上面的所有流程系统完整度已经相当高了。后续还可以扩展的方向按性价比排序 1. **房间图片上传与展示**给 rooms 表增加图片路径字段管理员端加上传入口。前端卡片上用图片组件替代纯文字。 2. **房态日历视图**用一个日历组件按月展示每个房间每天的预订情况直观管理房态。 3. **订单消息通知**管理员确认/取消订单时通过站内消息通知用户用户可在前端顶部看到未读消息数。 4. **对接真实支付**接入支付平台的沙箱环境完成下单后跳转支付支付成功回调更新订单状态。 ## 7. 我踩过的坑和开发习惯分享 最后分享几个开发过程中最值得记住的教训。 第一个是 **Flask 和 Vue 的“端口世界观”不同**。Flask 默认 5000 端口Vite 默认 5173 端口两者必须通过代理或 CORS 建立连接。开发时我被 5000 端口被占用折磨过多次查了才发现是 AirPlay 接收器占用了 5000。遇到端口占用先别乱改端口先查一下是谁在用必要时用 lsof -i :5000 确认。 第二个是 **前后端联调时要固定“数据字典”**。例如订单状态字符串前端和后端用同一个枚举文本。如果后端返回的是 confirmed 而前端判断的是 已确认那联调时必然会出错。我在最开始就定好了状态枚举前端直接映射为中文标签后期改动成本很小。 第三个是 **不要在原数据库上改表结构**。开发期间我常常发现某张表需要加个字段就直接用 SQLite 可视化工具改了表。后面再次运行 Flask db migrate 时反而出现模型和数据库结构不一致的问题。后来我的做法是需要修改表结构时直接把 SQLite 文件删掉重新初始化反正是演示用的假数据重新初始化成本很低。如果真的要保留数据则会先备份文件再重建表避免后续的坑。 第四个是关于 Vue 3 的 v-model 绑定技巧。Element Plus 的日期选择器组件绑定的是一个数组 dateRange但后端接口需要的是两个单独的字段 check_in 和 check_out。起初我直接在提交函数里获取 dateRange[0] 和 dateRange[1]后来发现绑定时用计算属性拆分更优雅 javascript const dateRange ref([]) const checkInDate computed(() dateRange.value[0] || ) const checkOutDate computed(() dateRange.value[1] || )提交订单时直接使用checkInDate.value和checkOutDate.value语义非常清晰。说回整个项目其实酒店在线预订系统真正的价值不在于“酒店”这个业务场景而在于它是典型的“带状态、带校验、带权限”的全栈系统。读懂房态冲突检测、状态机流转、前后端部署这些本质问题这套骨架换个业务场景照样能用比如自习室预约、共享工位预订、会议室排期。这也是很多老师喜欢让学生们选这个题目的原因它不是单纯练界面而是真正在练业务逻辑设计。