首页 / 资讯中心 / 文章详情

基于Python Flask和Django的台球开台管理系统实战

基于Python Flask和Django的台球开台管理系统实战 ★ FEATURED ARTICLE
球房搬进系统的第一个晚上最忙的不是开台而是结账。客人说打了三小时系统显示两小时四十分前台大姐拿着手写单跟客人掰扯半天后面排队的客人一脸不耐烦。这种场景在传统台球俱乐部太常见了计时靠人盯、计费靠口算、会员余额靠本子记任何一个环节出错损失的都是真金白银。所以当我决定做一个基于Python的台球开台系统时核心诉求非常明确把开台、计时、计费、结账这一整条链路数字化让球房老板在前台电脑上就能看清楚每一张台桌的状态、每一个客人的消费时长、每一天的营收流水。这个项目标题里同时出现了Flask和Django不少人第一次看到会觉得奇怪——这俩不是竞争关系吗实际上在我做这套系统的过程中两个框架我都用了各司其职。如果你正打算给自己的球房、棋牌室、台球俱乐部做一套类似的计时计费管理系统或者你是个Python开发者想找一个贴近真实业务场景的Web项目练手这篇文章应该能给你不少参考。1. 项目整体设计与需求拆解1.1 台球开台系统的真实业务场景很多人以为台球开台系统就是一个计时器加一个账单页面真正走进球房跟老板聊过之后才会发现业务规则远比想象中复杂。先说开台。客人进店前台需要知道现在哪些桌空着、哪些桌有人、哪些桌被预订了。开台时要记录的信息包括桌号、开始时间、计费方式按时计费还是按场计费、客人信息散客还是会员、是否需要押金。就这一个动作背后牵扯到桌台状态流转、会员余额校验、押金记录、开台时间戳生成。然后是计时与计费。按时计费是主流但不同桌型价格不一样普通台和VIP台每小时的单价不同不同时段价格也可能不同比如下午时段和晚高峰时段单价不同还有超时保护比如客人开台时押金只够打两个小时系统要不要在快超时时提醒前台这些都是需要业务建模时考虑清楚的细节。结账环节更是一个容易出错的点。客人结账时可能享受会员折扣、可能用了储值卡余额、可能购买了饮料小吃需要一起结算、可能超出了预订时段需要补差价。如果手工计算任何一个环节都容易产生纠纷。系统要做的就是把所有计费规则前置到代码里结账时一键生成明细账单。1.2 为什么同时使用Flask和Django这个项目标题把Flask和Django放在一起其实是两种落地路径的合体。Flask的特点是小、轻、灵活适合快速搭建原型、把核心开台结账逻辑先跑通。它没有强制性的目录结构ORM也可以自由选择对于这种业务流程比较聚焦的中小型系统来说开发效率非常高。我在项目初期用Flask写了一个可运行的原型版本把开台、计时、结账三个核心接口跑通了前后只花了一个周末。Django的特点是重、全、规范自带Admin后台、ORM、迁移机制、认证体系。当项目从原型走向正式部署时Django的这些内置能力非常有用。特别是Admin后台球房老板不可能去写SQL查数据他们需要一个能直接看每日营收、桌台状态、会员列表的管理界面Django Admin几乎零成本就能提供这个能力。所以我的做法是原型阶段用Flask快速验证业务流程正式版本用Django重构把业务逻辑沉淀到稳定的框架体系里。如果你只想做一个内部使用的小工具Flask完全够用如果要做成可以部署给多家球房使用的产品级系统建议直接选Django省掉后面迁移重写的成本。1.3 核心功能清单与数据库设计一个可落地的台球开台系统功能清单大概是这样的桌台管理桌台编号、区域、桌型普通/VIP、每小时单价、状态空闲/使用中/已预订/维护中开台/结台开台记录、计时开始、结账计算、账单生成会员管理会员信息、储值余额、折扣等级、消费记录商品管理饮料、零食等附加商品可以在结账时一并计入统计报表日营收、时段营收、桌台使用率、会员消费排行数据库表的设计是整个系统的地基。我最开始用Flask原型时只建了三张表后来用Django重构时扩展到了六张表核心表结构如下表名关键字段用途说明TableInfoid, name, zone, table_type, hourly_price, status桌台基础信息与状态Memberid, name, phone, balance, discount_rate, created_at会员信息与储值余额BillingSessionid, table_id, member_id, start_time, end_time, total_amount, status开台会话记录一次完整的开台到结账OrderItemid, session_id, item_name, quantity, price附加商品消费明细ChargeRuleid, name, hourly_price, start_time, end_time, applicable_days计费规则支持不同时段不同价格PaymentRecordid, session_id, amount, payment_method, paid_at支付明细在设计BillingSession这张表时我吸取了一个很重要的教训一定要让start_time和end_time同时存在而不是只记录一个开台时间、等结账时再算时长。因为球房可能会遇到客人中途换桌、暂停计时、挂单等情况只有把会话的开始和结束都固化下来后续的计费计算才能有据可循不会因为某个字段为空导致账单计算混乱。2. 技术选型与核心机制分析2.1 Flask与Django在该项目中的差异体验我在做这个项目时先写了Flask版本再用Django重写这让我对两个框架在同一个业务场景下的差异有比较直观的体会。Flask给我最大的感受是“自由”。路由自己定义ORM自己选我用的SQLAlchemy目录结构自己定。对于台球开台这种业务边界清晰、数据表不算多的系统Flask写起来非常畅快。核心接口就是那么几个——开台、计时查询、结账、会员查询不需要复杂的中间件和插件体系Flask的轻量反而让代码更容易维护。Django给我最大的感受是“省心”。Model层面自动生成数据库迁移脚本Admin后台开箱即用ORM自带查询优化。特别是处理会员储值、开台记录这类需要联表查询的数据时Django的ORM用起来比SQLAlchemy更顺手。不过Django的框架约束也比较多比如App的划分、模板的命名规则、中间件的注册方式刚开始写的时候需要适应。如果你是个人开发者给一家球房做内部系统我建议用Flask就够了部署简单一台普通电脑就能跑。如果你是想做一个可以复制的产品——比如未来要部署给十家球房用有统一的账号体系和管理后台那直接上Django更合适。2.2 计费引擎的核心算法与逻辑台球开台系统里最核心的模块不是界面好不好看而是计费引擎算得准不准。我梳理了球房最常见的三种计费场景第一种固定单价按时计费。比如普通台每小时30元客人打了1小时45分钟怎么算有些球房按实际时长精确到分钟计费那么费用就是30/60*10552.5元也就是按分钟折算有些球房按半小时向上取整那么就要按2小时计费也就是60元。这两种规则都需要在系统里配置不能写死。第二种分时段计费。比如周一到周五下午14点到18点是优惠时段每小时20元晚上18点到24点恢复原价30元。如果客人从17点打到19点那么17点到18点按20元算18点到19点按30元算需要把一次消费拆成两个时段分别计算再求和。第三种会员折扣叠加计费。会员等级不同折扣不同。比如金卡会员8折那么按时长计算出原价之后再乘以折扣率。计费引擎的设计我采用了策略模式把不同计费算法封装成独立的函数模块前端在结账时只需传入开台记录ID系统自动从数据库读取桌台价格、计费规则、会员折扣计算出最终应付金额。这个模块我在Flask原型中最早完成后来迁移到Django时几乎没有改动因为业务逻辑是独立的和框架无关。实现计费函数的核心思路是获取开台会话的起止时间按分钟遍历时间段判断每个分钟属于哪个计费时段累加对应单价最后再乘以会员折扣。虽然一分钟一分钟地遍历显得有些笨拙但在台球计时这种场景下会话时长一般不超过几个小时性能完全不是问题而逻辑正确性才是首要的。2.3 为什么桌面端Web界面比小程序更适合在规划这套系统时我也考虑过要不要做成微信小程序或手机App。后来跟球房的实际情况一对照发现Web管理系统才是最合适的形态。球房的前台场景是固定在一台电脑前的。前台人员在电脑上进行开台操作、查看桌台状态大屏、处理结账这些都是高强度、高频率的操作用键盘鼠标完成的效率远高于手机屏幕触控。而且前台电脑通常还要连接小票打印机微信小程序在这方面的兼容性明显不如Web页面。另外球房老板看报表的需求也偏向桌面端。一个显示所有桌台实时状态的管理面板、一个展示近30天营收趋势的统计页在宽屏上呈现的效果和操作体验比手机端强太多。所以这个系统的前端虽然也用了一些JavaScript动态效果但整体上遵循传统的服务端渲染模式页面通过模板渲染输出核心业务操作通过表单提交或AJAX请求完成不依赖复杂的前端框架。这样做的好处是部署简单、维护方便、对浏览器性能要求低一台老旧的办公电脑也能流畅运行。3. 核心模块的实现与实操过程3.1 桌台状态管理的实现细节桌台状态是整个系统的“仪表盘”所有业务动作都围绕桌台状态展开。我把桌台状态设计为四个空闲、使用中、已预订、维护中。状态变化沿固定路径流转空闲→使用中开台、使用中→空闲结台、空闲→已预订预订、已预订→使用中预订客人到店开台、空闲→维护中报修。实现时桌台状态是数据库表TableInfo里的一个整数字段不同数字对应不同状态。前端页面通过定时刷新或WebSocket推送获取桌台列表渲染成一张状态看板。对于大多数球房来说定时刷新足够了——每5秒轮询一次桌台状态接口即便高峰期也能保证信息基本实时。开台接口的核心代码如下这是我用Django重构后的版本# 开台接口 require_http_methods([POST]) def open_table(request): table_id request.POST.get(table_id) member_id request.POST.get(member_id) table TableInfo.objects.select_for_update().get(idtable_id) if table.status ! TableStatus.FREE: return JsonResponse({code: 1, msg: 该桌台当前不可开台}) session BillingSession.objects.create( tabletable, member_idmember_id or None, start_timetimezone.now(), statusSessionStatus.OPEN ) table.status TableStatus.IN_USE table.save() return JsonResponse({code: 0, data: {session_id: session.id}})注意这里用了select_for_update()这是Django ORM提供行级锁的方法。开台操作涉及先查询桌台状态、再修改状态、再创建会话逻辑上不是原子操作。如果两个前台同时操作同一个桌台——虽然现实中不常见但万一发生——就会出现两张开台会话对应同一张桌的问题。加上行级锁之后后到达的请求会等待前一个请求完成有效避免了并发冲突。3.2 计时与结账的完整代码实现计时功能看起来简单实现起来有一个容易忽视的问题前端显示给客人看的时间和数据库记录的时间可能会有偏差。客人在前台大屏看到自己已经打了2小时01分但结账时系统算出1小时58分客人就会觉得被坑了。解决这个问题的思路是系统以数据库记录的开台时间为准前端展示的时间也是从服务端接口获取的而不是在浏览器端通过开台时间到现在时间自行计算。我实现了一个获取当前会话时长接口返回给前端的是已经计算好的分钟数前端只负责展示不负责计算。结账接口的代码包含了计费引擎的调用这是整个系统的核心我也贴出来# 结账接口 require_http_methods([POST]) def close_table(request): session_id request.POST.get(session_id) session BillingSession.objects.get(idsession_id) if session.status SessionStatus.CLOSED: return JsonResponse({code: 1, msg: 该会话已结账}) # 计算台费 end_time timezone.now() duration_minutes (end_time - session.start_time).total_seconds() / 60 # round up to nearest minute duration_minutes int(math.ceil(duration_minutes)) table_price session.table.hourly_price base_amount duration_minutes / 60 * table_price # 会员折扣 discount 1.0 if session.member: discount session.member.discount_rate base_amount round(base_amount * discount, 2) # 附加消费 order_items OrderItem.objects.filter(sessionsession) items_amount sum(item.quantity * item.price for item in order_items) total_amount round(base_amount items_amount, 2) session.end_time end_time session.total_amount total_amount session.status SessionStatus.CLOSED # 扣减会员余额 if session.member: if session.member.balance total_amount: return JsonResponse({code: 1, msg: 会员余额不足}) session.member.balance - total_amount session.member.save() session.save() return JsonResponse({ code: 0, data: { duration_minutes: duration_minutes, base_amount: base_amount, items_amount: items_amount, total_amount: total_amount } })这里有一个细节值得展开分钟数为什么要向上取整因为球房的实际规则里通常打满1分钟也算1分钟的钱。如果1小时30元客人打了5分钟按分钟精确计算是2.5元有些球房觉得无所谓但很多球房会直接按小时计算不足1小时按1小时收。这些规则都可以通过ChargeRule表配置代码里默认按分钟向上取整算是比较通用的方案。3.3 会员储值管理与报表统计的实现会员管理模块里我最看重的是储值扣减的时机。很多新手开发容易犯一个错误开台时就冻结会员的预计费用结账时再按实际费用扣减。设想一个场景会员余额有100元开台时预计2小时费用80元实际打完3小时费用120元那第2小时到第3小时之间系统是不知道余额够不够的。如果不做任何控制结账时才发现余额不足就很尴尬。我的做法是开台时不冻结余额但记录开台时的会员余额快照结账时如果余额不够则提示前台与会员确认是充值还是现金支付。这个做法更符合球房的实际运营逻辑毕竟很多会员是朋友请客不一定用自己的卡结账提前冻结反而会造成操作上的困扰。报表统计方面Django内置ORM配合annotate和aggregate就能完成大部分需求。日营收报表只需按天对PaymentRecord做聚合桌台使用率通过统计BillingSession中每天每张桌的占用时长再除以24小时得出。这些查询在数据量没有达到十万级别之前性能完全不需要优化直接用ORM写清晰的查询语句即可。如果未来数据量上来了再考虑增加索引或引入缓存层。4. 常见问题与排查技巧实录4.1 计费时间偏差与并发开台冲突上线初期遇到最多的问题就是计费时间与实际不符而且都是同一个特点前台说系统计费少了。排查到根因后发现问题不在计费函数而在于开台的“开始时间”不是真正的开台时间。具体来说是操作员先点击“开台”然后引导客人选球杆、选饮料过了两三分钟才在录入框里补充会员信息并保存。这段拖延的时间没有被记录进开台时间导致实际用时统计少了。解决方案是在“开台”按钮点击时就写入开台时间戳后续补充会员信息只是更新会话信息不再动开始时间。并发开台冲突也遇到过。两个前台窗口同时收到两批客人都点了同一张空闲桌的开台按钮其中一个报错“桌台不可用”。这其实是行级锁生效的正常表现用户体验不算好但数据没有错乱。后来在开台按钮点击时先在前端做了置灰处理同时后端保留行级锁兜底双保险之下这个问题几乎不再出现。4.2 浏览器刷新导致重复结账与支付记录问题结账按钮被用户重复点击在现代Web应用中几乎是必然会遇到的问题。第一次点击发出POST请求后如果网络延迟较高用户会紧张地再点一次第二次请求就会重复创建支付记录甚至重复扣减会员余额。后端防御的方法是在结账接口里加幂等校验。我的实现方式是BillingSession表中增加一个closed_at时间字段结账接口第一步就检查这个字段是否为空不为空说明已经结过账直接返回提示。由于同一个会话的两次请求几乎同时到达行级锁在这里也能起到保护作用。前端防御方法是在按钮点击后立即禁用发送请求的按钮并显示一个“处理中”的提示直到请求响应后再恢复。两种方法结合重复结账的问题彻底解决了。顺便说一下这个方案同样适用于开台操作适用场景包括客人到店后前台重复提交开台信息、网络超时后操作员再次提交表单等。幂等设计是Web系统里性价比最高的防御手段值得多花十分钟在一开始就做好。4.3 多时段计费规则的异常排查分时段计费功能上线后出现过一次比较隐蔽的错误晚上21点到22点之间结账的订单费用总是比平时低一截。起初以为是打折逻辑出了问题后来查数据库才发现是ChargeRule表里有一条“22点到24点超值优惠时段”单价配错了只有正常价格的一半。由于这个时段规则是后来人工添加的录入时没有做校验导致系统按错误价格计费。这个排查花了不少时间因为问题不在代码层而在数据层。从那以后我在管理后台增加了一个计费规则自检功能——对每个时段的规则做边界检查确保相邻时段价格差不超过合理范围规则开始时间不重叠。有了这层数据校验这类人工配置错误就能在产生实际影响前被拦截。4.4 新手常见问题速查表问题现象可能原因排查建议计费时间比实际少开台时间写入过晚检查“开台”按钮触发时是否立即写入时间戳同一张桌被重复开台多个前台并发操作使用数据库行级锁前端按钮点击后立即禁用结账后余额未扣减结账请求被重复提交在结账接口增加会话状态幂等校验分时段价格异常计费规则配置错误检查ChargeRule表中时段是否有重叠或单价填错页面桌台状态不更新前端没有定时轮询增加桌台状态轮询建议5秒一次部署后数据库中文乱码数据库字符集未设置UTF-8建库时指定utf8mb4字符集5. 部署方案与二次扩展建议5.1 轻量部署方案这套系统最适合的部署环境是一台普通的Linux服务器不需要高配置2核4G内存就足够应对单店上百张桌台的并发场景。部署方案上我推荐用Gunicorn作为应用服务器Nginx作为反向代理。静态文件由Nginx直接处理动态请求转发给Gunicorn。数据库方面如果桌台数不超过200张、日均结账单不超过500单SQLite其实也够用但我建议还是用MySQL或PostgreSQL后续查询统计报表时会更从容。一个很实用的建议是把数据库每天自动备份到另一个目录或者对象存储然后设置定时任务清理30天前的备份。这个操作很简单但真遇到服务器硬盘损坏或者误删数据时能挽回巨大的损失。球房营业数据是实时积累的丢了开台记录和会员储值余额补都补不回来。5.2 可以继续扩展的方向这套系统做完后下面几个方向是我在实际维护中反复想过的扩展点。在桌台层面可以增加预订功能。客人电话预订桌台前台在系统里把桌台标记为“已预订”并记录预订时间段。到点后桌台状态自动变回空闲避免预订未到造成桌台空置损失。这个功能对周末和晚高峰时段特别有用。在营销层面可以增加优惠券和活动配置。比如新会员注册送一小时体验券、充值满500送100、深夜时段买断优惠。这些规则如果写在代码里每出一个活动就要改一次代码更好的做法是把优惠规则做成数据表运营人员可以在管理后台自行配置。在支付层面可以接入扫码支付。目前结账时是现金或会员余额扣减如果接入微信支付或支付宝支付的线下扫码能力可以进一步减少前台的现金处理压力。球房通常不会有太多零钱客人扫码支付直接到账对账也更清晰。6. 项目实操总结与经验教训整个项目从Flask原型验证到Django正式版落地前后用了不到三周的时间。这中间有些经验我觉得值得拿出来多说几句。第一技术选型要匹配业务的真实规模。如果只是给一家球房做内部系统Flask完全能胜任没必要为了用Django而用Django。但如果你预见到这个系统未来可能要支撑多门店、需要账号权限管理和后台运营配置Django的Admin和内置权限体系能帮你省掉很多重复开发的工作。第二计费规则一定要做成可配置的而不是写死在代码里。每个球房的经营策略都不一样有的按分钟计费有的按半小时取整有的分时段计价有的对VIP桌和普通桌执行不同的阶梯价格。把这些规则做成数据表运营人员自己可以调整才能真正适应实际业务变化。第三系统的健壮性设计比功能数量更重要。开台操作要防并发冲突、结账操作要防重复提交、计费计算要保证精确、报表数据要保证一致——这四个方面都比多做几个展示页面有价值得多。我在项目里深深体会到一套系统的口碑不在于界面多炫酷而在于半年的运营时间里没有出过一次账目差错。如果你也想做类似的系统我建议你先去一家球房实地观察半天看看前台的工作流程是什么样的、哪些环节是手工操作的、哪些地方容易出错。带着真实的业务观察再动手写代码你会发现自己设计的系统比凭空想象出来的方案要实用得多。代码层面的东西反而不难真正难的是对业务场景的理解和建模。这套台球开台系统本质上是在帮球房老板管理时间——桌台时间的售卖是这个生意的核心让每一分钟都算得清清楚楚系统就真正创造了价值。
阅读完成 · 觉得有帮助?
咨询建站