☰
基于Python Flask和Django的台球开台管理系统实战
2026/10/19 14:18:06 网站建设 项目流程

球房搬进系统的第一个晚上,最忙的不是开台,而是结账。客人说打了三小时,系统显示两小时四十分,前台大姐拿着手写单跟客人掰扯半天,后面排队的客人一脸不耐烦。这种场景在传统台球俱乐部太常见了,计时靠人盯、计费靠口算、会员余额靠本子记,任何一个环节出错,损失的都是真金白银。所以当我决定做一个基于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*105=52.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(id=table_id) if table.status != TableStatus.FREE: return JsonResponse({"code": 1, "msg": "该桌台当前不可开台"}) session = BillingSession.objects.create( table=table, member_id=member_id or None, start_time=timezone.now(), status=SessionStatus.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(id=session_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(session=session) 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桌和普通桌执行不同的阶梯价格。把这些规则做成数据表,运营人员自己可以调整,才能真正适应实际业务变化。

第三,系统的健壮性设计比功能数量更重要。开台操作要防并发冲突、结账操作要防重复提交、计费计算要保证精确、报表数据要保证一致——这四个方面都比多做几个展示页面有价值得多。我在项目里深深体会到,一套系统的口碑不在于界面多炫酷,而在于半年的运营时间里没有出过一次账目差错。

如果你也想做类似的系统,我建议你先去一家球房实地观察半天,看看前台的工作流程是什么样的、哪些环节是手工操作的、哪些地方容易出错。带着真实的业务观察再动手写代码,你会发现自己设计的系统比凭空想象出来的方案要实用得多。代码层面的东西反而不难,真正难的是对业务场景的理解和建模。这套台球开台系统本质上是在帮球房老板管理时间——桌台时间的售卖是这个生意的核心,让每一分钟都算得清清楚楚,系统就真正创造了价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询