基于Flask和SQLite的体育馆场地预约评价系统设计与实现

📅 发布时间:2026/10/11 13:41:06
基于Flask和SQLite的体育馆场地预约评价系统设计与实现
体育馆场地预约这件事绝大多数高校、小区物业和公司行政还在用微信群接龙纸质登记的方式对付。我去年帮一个场馆做过一套基于Python Flask的免费体育馆场地预约评价系统从需求梳理、表结构设计到上线运行大概花了三周之后又用课余时间修了不少边角问题。这套系统本地跑起来不需要买云服务器不需要买数据库授权后端用Flask、数据库用SQLite就可以负担中小型场馆的预约压力。这篇文章把我整个设计过程拆开讲从业务需求到数据结构再到并发冲突防范、评价体系、后台管理和部署踩坑适合想自己搭建同类系统的人参考也适合拿它当Flask练手项目的初学者。先说清楚免费在这里有两层意思一是预约系统对用户免费开放不涉及商业付费二是整套技术栈是开源免费的Python、Flask、SQLite三个加起来零成本部署在自己机房或者一台旧电脑上就能跑。如果这个场景戳中你那下文应该能帮你省下不少试错时间。1. 先理清预约业务里的隐性需求再谈技术选型很多人在动手前会直接搜会议室预约系统的代码来改这个思路其实有点偏。体育馆预约相比会议室预约有几个完全不同的业务特征场地可以按小时出租给不同的人或队伍一个时间段可能有多个可预约名额用户和场地之间有强烈的按时履约需求不去现场的人会占用实际想运动的人的资源。这些特征决定了数据库表和接口设计都跟通用预约系统不一样。1.1 体育馆预约和会议室预约的本质区别会议室预约的核心是一个时间段只能被一个人占用所以只需要查有没有人订过即可。体育馆不一样比如羽毛球馆有两个场地那么同一时间段可以有两单预约再比如篮球半场可以同时容纳两拨人这时预约模型就变成了容量限制而不是独占限制。这直接影响了场次表的设计。我不建议只存场地时间段当成一个不可再分的预约单位而是要给每个场次加上max_people和booked_count两个字段。用户预约时程序检查的是booked_count max_people而不是简单查重。这里的差别看似很小实际上决定了系统能否应对多人共同使用一个场地的场景。另一个容易被忽略的需求是核销。预约之后到底有没有到场管理员需要在现场确认。没有核销机制的预约系统很快就会积累大量僵尸预约真实到场率下降后面真正想运动的人反而订不到场地。核销功能不仅是为了管理方便更是后续评价系统能够成立的先决条件——只允许已核销的用户打分才能保证评的是实际体验过的场地。1.2 Flask SQLite组合的取舍免费、轻量但要认清边界选型的时候我认真对比过Flask和Django。Django自带Admin后台和ORM看起来好像更省事但对于这个项目它的重量级反而成了负担。Flask的优势在于路由简洁中间件和扩展按需引入一个文件就能把核心接口写清楚读代码的人能很快理解整个预约流程。对中小型项目来说代码可维护性不是靠框架功能多实现的而是靠结构清晰实现的。数据库选择SQLite也是一个务实决定。SQLite是单文件数据库零配置Python标准库直接支持不需要另行安装服务。在预约这类写多读少的场景里只要控制好事务粒度性能完全够用。我实测过在BEGIN IMMEDIATE串行化写入的前提下日常几百人并发预约不会出现数据错乱。真正的边界在于写入吞吐量如果将来日活上千、每分钟几十个预约请求就需要迁移到MySQL但那是后话项目初期完全没必要为了理论上的扩张引入运维成本。2. 五张数据表撑起整套预约体系核心表结构设计复盘项目上线之后我复盘过整个数据库设计觉得最关键的一点是没有贪多只用了五张表就把用户、场地、场次、预约、评价全部串起来了。很多新手在起步阶段容易陷入扩展性焦虑动不动就加各种冗余表和状态字段结果业务逻辑越写越绕。这里我把每张表的字段和设计理由写出来供你参考。2.1 用户、场地、场次、预约、评价的表关系我用的SQLite建表语句大致如下CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, real_name TEXT NOT NULL, phone TEXT, role TEXT DEFAULT user ); CREATE TABLE venues ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, location TEXT, sport_type TEXT, status INTEGER DEFAULT 1 ); CREATE TABLE slots ( id INTEGER PRIMARY KEY AUTOINCREMENT, venue_id INTEGER NOT NULL, slot_date TEXT NOT NULL, start_time TEXT NOT NULL, end_time TEXT NOT NULL, max_people INTEGER DEFAULT 1, booked_count INTEGER DEFAULT 0, status INTEGER DEFAULT 1 ); CREATE TABLE bookings ( id INTEGER PRIMARY KEY AUTOINCREMENT, slot_id INTEGER NOT NULL, user_id INTEGER NOT NULL, status TEXT DEFAULT confirmed, created_at TEXT NOT NULL, UNIQUE(slot_id, user_id) ); CREATE TABLE reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, booking_id INTEGER UNIQUE NOT NULL, user_id INTEGER NOT NULL, venue_id INTEGER NOT NULL, rating INTEGER NOT NULL, content TEXT DEFAULT , created_at TEXT NOT NULL );这张结构里bookings表没有直接存场地ID而是通过slot_id间接关联到venues。这样设计的好处是评价reviews表需要记录场地ID但预约记录本身只关心场次避免数据冗余。当用户评价时系统可以用booking_id反查slot_id再反查venue_id保证评的就是那个实际被订的场地。2.2 字段层面的两个关键约束唯一性与状态流转bookings表上的UNIQUE(slot_id, user_id)是我特意加的作用是防止同一个用户重复预约同一个场次。这个约束在数据库层面就拦截掉了脏数据而不是完全依赖应用代码判断。reviews表上的booking_id UNIQUE更是评价系统防刷分的第一道防线。没有这个约束用户理论上可以针对一条预约提交多个评价刷高某个场地的分数。有了数据库约束无论前端怎么重复提交只能插入一条评价记录。预约的状态流转我控制在三个值confirmed表示已预约未核销checked_in表示管理员现场确认已到场cancelled表示用户主动取消。不建议再细分更多状态否则管理后台的列表筛选逻辑会越来越复杂而实际业务根本不需要那么多中间态。2.3 场次生成策略按周模板生成还是用定时任务这里有一个值得多花点时间想清楚的点场次表slots的数据从哪里来我见过两种方案。方案A是每天晚上用定时任务自动生成第二天或下一周的场次好处是数据库里只有未来可预约的场次不会堆积历史数据。缺点是需要引入定时调度模块比如Flask-APScheduler部署时多一个进程要考虑。方案B是管理员在中后台手动生成一周场次模板比如周一、三、五的篮球场从18:00到22:00每小时一个场次周二、四的羽毛球场同理。优点是逻辑直观管理员能够根据临时安排灵活调整缺点是人工操作有漏生成的可能。我做的是方案B的简化版管理员在后台选择场地、日期、起止时间和容量点击生成后系统按小时拆分成多个slots记录。这样不用引入后台定时任务也符合场馆每周固定开门的实际运营节奏。你如果预期要处理每天凌晨释放新场次这类需求再考虑方案A不迟。3. 预约冲突检测与防重复提交这套系统最核心的代码逻辑整个项目里最值得拿出来讲的就是预约接口。它需要同时处理两件事一是判断场次是否还有剩余名额二是防止并发情况下出现两个人同时订到最后一个名额的覆盖问题。这两个问题处理不好系统上线第一天就会遭骂。3.1 重叠时间段判断公式先理解理论基础如果采用我上面的固定场次方案冲突检测主要看booked_count和max_people。但如果你以后想支持用户自定义起止时间的预约模式就需要时间重叠判断。判断两个时间段是否重叠核心公式是一句很短的逻辑新增预约开始时间 已有预约结束时间 AND 新增预约结束时间 已有预约开始时间这个公式覆盖了所有重叠情况完全包含、部分相交、边界相接。很多人容易漏掉边界条件比如A结束时间是17:00B开始时间也是17:00这两个其实不冲突。用上面的公式就不会误判因为17:00 17:00不成立。数据库查询则写成这样SELECT COUNT(*) FROM bookings WHERE status IN (confirmed, checked_in) AND slot_id ?固定场次模式下一个slot_id就是唯一时间段预约就是抢固定场次的余额不需要再写区间比较逻辑。这种用数据模型简化逻辑的做法我觉得值得推荐——能用表结构表达的规则就不要让代码去硬算。3.2 完整实现从路由到事务的预约接口预约接口的核心代码如下为了便于复现我用的是原生sqlite3模块没有加ORMimport sqlite3 from datetime import datetime from flask import Flask, request, session, jsonify app Flask(__name__) app.secret_key please-change-this-key DB_PATH sports.db app.route(/api/v1/book, methods[POST]) def create_booking(): if user_id not in session: return jsonify({code: 401, msg: 请先登录}) data request.get_json(silentTrue) or {} slot_id int(data.get(slot_id, 0)) with sqlite3.connect(DB_PATH, timeout30) as conn: conn.execute(BEGIN IMMEDIATE) slot conn.execute( SELECT id, max_people, booked_count, status FROM slots WHERE id?, (slot_id,) ).fetchone() if slot is None or slot[3] ! 1: conn.rollback() return jsonify({code: 400, msg: 场次不存在或已关闭}) if slot[2] slot[1]: conn.rollback() return jsonify({code: 400, msg: 该场次名额已满}) conn.execute( INSERT INTO bookings(slot_id, user_id, status, created_at) VALUES(?,?,?,?), (slot_id, session[user_id], confirmed, datetime.now().isoformat()) ) conn.execute( UPDATE slots SET booked_count booked_count 1 WHERE id?, (slot_id,) ) conn.commit() return jsonify({code: 0, msg: 预约成功})这里用conn.execute(BEGIN IMMEDIATE)起到关键作用。它会在执行第一条写操作前就获取数据库写锁直到commit或rollback才释放。这样两个用户同时抢一个场次时后一个事务必然等待前一个先结束然后在自己的事务里重新读取booked_count发现余额不足就直接返回。这就是数据库事务的可串行化效果。3.3 高并发下如何避免两人同时订到同一场次大学或大型社区场馆经常出现热门时段同时几十人抢预约的情况。如果不做任何保护两个请求可能同时读到booked_count9、max_people10然后都各自插入一条预约记录都执行booked_count1最终数据库里就有了11条预约名额超卖。解决这类问题常见的手段有三种我按推荐顺序排一下使用事务写锁像上面代码那样用BEGIN IMMEDIATE把检查-写入-更新合并成原子操作数据库表加唯一约束相当于给业务规则加保险丝即便应用层漏判数据库也会拒绝重复插入在上线早期流量可控时前端加按钮禁用和倒计时降低重复提交概率但不能替代后端防护。前两点必须在代码和表结构里落实第三点是体验层面的补充。我自己刚写完第一版时只依赖了前端JS的防重复点击结果用脚本模拟并发请求一打就穿帮后来才补上事务和唯一约束。这个教训提醒我前端所有限制都只是锦上添花安全边界必须收敛在后端。3.4 前端防重复点击的兜底措施虽说前端不能作为安全保证但也不能完全不做否则用户手一抖连点三次会出现三次请求排队全部成功又接连失败的现象。我是这样处理的预约按钮点击后立即置灰文案变为提交中...同时设置15秒倒计时接口返回成功或失败后才恢复按钮。这个倒计时还有一个额外好处给后端事务处理留出缓冲避免用户在页面无响应错觉下不断刷新产生更多的排队请求。用户反馈里这个小小的交互细节比很多花哨功能更让人安心。4. 评价系统设计让每一分好评差评都真实可信评价模块看着简单不就是评分加备注吗。但实际运营中评价的可信度才是核心难题。没有人希望系统里出现大量空泛好评或者恶意差评那会让后来的人完全失去参考价值。我在设计评价系统时把重点放在什么条件下允许评价和如何防止刷分两个问题上。4.1 评价时机为什么必须限定在已核销之后我定的规则是只有状态为checked_in的预约记录才能评价。这意味着用户必须先到场管理员现场扫码或确认核销预约状态更新之后用户在个人中心才会看到去评价按钮。这个设计直接杜绝了未去体验先打差评和恶意注册账号批量刷评价的问题。评价针对的是真实履约行为不是随意发言。另一个好处是它天然把评价和预约核销两个流程绑定在一起管理员可以通过已核销但未评价的数量判断用户对本次体验是否满意——如果某个场地的核销率很高但评价率很低往往是体验一般但用户懒得吐槽这类数据同样值得关注。4.2 防刷分的三道闸门唯一约束、频率限制、内容过滤第一道闸门是数据库层面的booking_id UNIQUE一条预约只能有一条评价这是硬约束。第二道闸门是应用层面的频率限制。我在评价接口里检查每个用户每天的提交次数超过10条就拒绝。正常用户一天不可能订超过10个场地超过这个量基本可以判定为异常行为。同时如果一个用户给同一场地连续提交多条评价的间隔极短也会触发限制。第三道闸门是内容过滤。评价文本限制在200字以内过滤纯数字、连续重复字符、带联系方式的内容。过滤逻辑不需要引入多复杂的敏感词库几条正则就能覆盖大多数广告和灌水内容。这里的目标不是做出完美的内容审核系统而是把低质量噪声控制在可控范围。4.3 评分聚合与展示逻辑从单条评价到场地口碑分单个用户提交的评价只是孤例真正有价值的是场馆维度上的聚合数据。主页的每个场馆卡片上除了显示场地照片和可预约场次还会显示平均分和评价人数。聚合查询类似这样SELECT venue_id, AVG(rating) AS avg_rating, COUNT(*) AS review_count FROM reviews WHERE venue_id ? GROUP BY venue_id;我还额外统计了评分的星级分布比如5星占多少、4星占多少前端用纯CSS柱状图展示。这样用户能看到的不只是一个抽象平均分而是评分的落点分布。两颗星和三颗星的区别往往比这个场地平均分3.5包含更多信息。4.4 评价数据的运营视角差评如何反哺场地管理评价系统上线最大的收益在运营端。场馆管理员能在后台按场地查看差评明细比如灯光太暗空调没开地面湿滑。有了这些具体反馈管理人员能针对性地安排维护而不是凭感觉决定哪天检修场地。我后来还在后台加了一个简易的差评邮件提醒当某条评价评分小于等于2时系统自动在管理页顶部红字提示管理员处理。这个功能对行政人员特别有用问题能在当天被发现而不是等到月度复盘时才知道场地已经影响了用户体验。5. 管理后台怎么做才实用场地维护、核销与值班确认系统分为用户端和管理员端技术上完全可以放在同一个Flask应用里用角色字段区分访问权限。初期我没有用Flask-Admin扩展而是所有后台页面手写HTML模板因为业务操作就那么几个管理场地、管理场次、查看预约、核销确认。扩展带来的抽象概念反而会增加理解成本。5.1 后台功能清单与权限隔离我用users.role字段区分admin和user利用Flask的before_request钩子做路由保护from functools import wraps def admin_required(f): wraps(f) def wrapper(*args, **kwargs): if session.get(role) ! admin: return 需要管理员权限, 403 return f(*args, **kwargs) return wrapper app.route(/admin/venues) admin_required def admin_venues(): # 场地管理页面 ...后台功能开放给管理员的主要有场地增删改查、场次批量生成、预约列表查看、核销处理。最开始也有人建议加入封禁用户修改用户信息等功能我全部砍掉了。原因很简单体育馆预约系统不是用户管理系统把不常用的功能藏起来后台才能保持清爽。5.2 核销流程设计现场确认不是复杂操作核销流程我做得非常克制。管理员登录后台后打开今日预约页面列表按时间排序显示所有已预约用户包括姓名、手机尾号、场次。管理员在用户到场后点击对应订单的核销按钮预约状态从confirmed变为checked_in。如果有条件可以给每个场地生成一个二维码用户到场后自己扫码核销节省人工。我第一版做的只是人工点按钮因为场馆大多数时候只有一名管理员值班人工操作完全可以应付。二维码方案留给二期再说核心业务逻辑不变——所有核销操作的结果都只是更新状态字段。5.3 场馆运营报表使用率与高峰时段管理后台值得花力气做的其实是简单的数据统计而不只是数据列表。我引入了两个查询每日场馆使用率和时段预约热度。场馆使用率等于当天已预约名额除以总可预约名额时段热度则是统计每个时间段的历史预约次数按降序排列。有了这两个指标管理员可以明显看到周一至周五晚上18点到20点严重饱和而下午14点到16点几乎无人问津。这为调整开放时段、错峰定价或者增设晚间场次提供了量化依据。统计页面不需要引入图表库我用纯CSS柱状条展示比例数据接口从bookings表和slots表聚合即可。上线第一个月管理员就根据这些数据把两个冷门时段改成了免费培训时段场地利用率提升了将近两成。这算得上是整个系统里性价比最高的管理功能。6. 部署到局域网服务器免费方案与踩坑记录写代码是一回事部署上线是另一回事。本地一切正常一放服务器就各种问题这是新手项目最常见的坎。我下面把从零到能访问的完整流程和踩过的坑都列出来避免你再趟一遍。6.1 环境准备与环境隔离首先确认电脑上安装了Python 3.8以上版本。Windows可以在官网下载安装包Linux大部分发行版自带python3。进入项目目录后先建虚拟环境然后再装Flask这一步能防止全局Python环境下多个项目依赖打架python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install flask环境隔离这件事我一开始嫌麻烦直接在系统环境里pip install flask。后来另一个项目需要旧版Flask两个项目互相冲突别提多头疼。养成用虚拟环境的习惯后续维护成本能省一大截。6.2 局域网访问配置与Windows/Linux常见问题Flask开发服务器默认监听127.0.0.1只能在本地访问其他设备根本打不开。要让局域网内其他电脑和手机访问必须绑定所有网卡地址并指定端口if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这里有一个新手最容易栽的坑debugTrue在开发期打开没问题但部署时必须关掉。开着调试模式会让访问者在出错时看到完整的回溯信息甚至会暴露源码路径和相关配置属于安全隐患。如果是Windows服务器还要检查防火墙入站规则放行5000端口Linux服务器则注意占用问题可以用lsof -i:5000查看端口被谁占用。我在一台Linux机器上遇到过nohup python app.py 启动不到半天程序就退出排查后发现是没有正确处理标准输入输出流改用nohup ... app.log 21 后就稳定了。6.3 我实际遇到过的三个故障与修复第一个故障是sqlite3.OperationalError: database is locked。原因是我在后台批量生成场次的函数里连续执行了几千条INSERT把SQLite的写锁攥住不放用户端预约请求只能排队超时报错。解决方法是两件事一是生成场次的操作拆分批次执行不要在同一个事务里写入几千条记录二是在连接SQLite时设置timeout30程序会等锁而不是立刻报错。更彻底的办法是开启SQLite的WAL模式PRAGMA journal_modeWAL;开启WAL后读写可以并行写锁的时间窗口大幅缩短对预约这种读多写少的场景非常友好。实测开启WAL之后数据库锁错误几乎消失。第二个故障是用户预约成功后页面长时间不跳转。排查发现接口里忘记返回JSON了前端一直在等响应。这类问题在Flask里很常见尤其是接口同时处理正常流程和异常分支时容易漏掉return。我后来给所有接口写了统一的响应格式{code: 0, msg: ok}或{code: 400, msg: error}前端拿code判断逻辑清晰很多。第三个故障是会话失效。我最初用固定的secret_key部署后换了一台机器忘记把密钥一起带过去导致所有用户登录状态全部丢失。后来我把密钥改成从环境变量读取import os app.secret_key os.environ.get(SECRET_KEY, dev-only-not-secure)这样就不会因为部署环境不同而出现本地正常、线上就掉登录的怪问题了。6.4 后续扩展方向从预约评价到完整场馆运营系统稳定运行之后再谈扩展优先级应该是这样排预约成功后的通知消息比如微信公众号模板消息或站内信降低用户漏记导致的爽约率场地二维码核销管理员或者用户扫码确认入场线下流程更利落爽约率统计连续爽约几次限制其后续预约这一步能明显提升场地履约率数据可视化大屏把今日预约量、实时在场人数、场地热度投放到场馆门口的大屏上如果用户量真的增长到几百人同时在高峰期预约再把SQLite迁移到MySQL并用Flask-SQLAlchemy替换原生sqlite3应用层不需要大改。我个人体会最深的一点是这类内部管理系统功能多不如做得准。与其设计一堆用不上的智能推荐个性设置不如把预约冲突、核销、评价可信度这三件核心事情做扎实。体育馆预约系统上线后管理员最直观的感受是从每天接收上百条微信消息变成了打开后台看列表点几下核销用户也能清楚看到场地口碑数据约场不再靠猜。如果你也准备动手我建议你第一版就按我这个最小闭环来——五张表、三个接口、两个页面先跑起来再谈优化。