ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP+JavaScript构建台球厅一体化系统:CRM、收银、小程序与会员星球实战

PHP+JavaScript构建台球厅一体化系统:CRM、收银、小程序与会员星球实战 简介这是一套面向台球场馆数字化运营的全功能后端源码专为中小型台球馆管理者、PHP开发者及小程序对接工程师设计解决客户管理混乱、收银效率低、会员服务割裂、线上预约缺失等典型业务痛点。资源共388个文件主体为354个PHP后端逻辑文件含CRM用户模型、收银事务处理、小程序API接口及星球模块服务层辅以12个.gitignore等版本控制配置、3个JS前端交互脚本、以及.crt/.pem证书文件支撑安全通信整体压缩包仅911KB轻量易部署。已有327人学习下载适合希望快速落地台球行业SaaS化管理的开发者。源码结构清晰内置Laravel风格artisan命令行工具、.env.example环境配置模板、composer.json依赖清单及readme.txt部署指南涵盖台位智能预约、电子会员卡、积分营销、赛事活动管理等核心业务模块并提供完整RESTful接口支持小程序端调用开箱即用。接手台球厅系统改版我用PHPJavaScript把CRM、收银、小程序和会员星球串成了一套后端去年下半年接了一个台球厅的系统改版项目标题写得很长——基于PHP和JavaScript的CRM收银小程序星球新改版台球后端设计源码。当时第一反应是这不就是个收银加会员管理的小项目吗但真正把需求理完之后我发现这个场景比想象中复杂得多。台球厅看起来经营模式简单实际上同时牵扯到计时计费、桌台状态流转、会员等级折扣、商品销售、线上预约、社群运营还有一堆日常营销活动。这套系统的设计核心是用PHP做后端业务逻辑JavaScript承担小程序端和管理后台的交互逻辑再通过一套标准的接口协议把CRM客户管理、收银订单计费、小程序C端入口、星球模块会员私域内容全部打通。本文把我实际改造过程中的技术选型、架构设计、关键模块的落地方式以及踩过的坑一次说清楚。适合正在做门店数字化系统、或者准备从单一收银系统向线上线下私域一体化转型的开发者参考。1. 项目整体设计与思路拆解1.1 台球厅经营管理到底缺什么台球厅这个生意表面看就是出租球桌按小时收费。但真正经营起来老板要面对的问题很具体怎么防止老客户流失怎么让客户在非高峰时段也愿意来怎么让客户充值的钱变成长期锁客的工具还有就是会员的数据散落在收银机、Excel甚至老板的脑子里根本没法做精准营销。传统单机版收银系统能解决算钱的问题但完全解决不了管人和做运营的问题。客户今天来打了两小时球消费信息记在收银系统里然后呢没有然后了。你不知道他喜欢打美式还是斯诺克不知道他习惯下午来还是晚上来更没法在他两周没来的时候推一张优惠券把他拉回来。这就是CRM系统在台球场景下的价值把每一次消费、每一次充值、每一次预约变成客户档案的一部分让经营者能看到完整的客户生命周期。另一个痛点是私域流量的承载。星球这个模块说白了就是会员的私域社群门店可以把赛事公告、新手教学、优惠活动发在里边会员可以在里边约球、晒战绩、攒积分。没有这个模块之前台球厅的社群可能就是一个微信群消息半天就被刷没了活动报名靠接龙积分靠手工记录运营成本极高。1.2 四位一体的系统架构这次改版的核心目标就是把四个原本割裂的模块装进同一套后端系统里。我把整体架构拆成四个层次来看**CRM层客户关系管理**负责会员档案、等级体系、标签画像、储值余额、积分流水。它解决的是客户是谁的问题。**收银层交易闭环**负责桌台计时、商品售卖、订单结算、优惠核销。它解决的是钱怎么收的问题。**小程序层C端入口**负责扫码开台、在线预约、会员中心、历史账单查询。它解决的是客户怎么自助的问题。**星球层私域运营**负责内容发布、活动报名、积分任务、公告推送。它解决的是怎么让客户持续来的问题。这四个模块不是四条独立的线而是共享同一套会员数据、同一套订单数据、同一套积分资产。CRM里产生的标签会指导营销推送小程序里的预约会自动生成待结算订单收银台的结算结果会反写进会员消费记录星球里的活动报名又能带动新一轮的预约和消费。这样一套闭环设计才是新改版和老系统之间最本质的区别。1.3 新改版要解决的老问题改版之前这套系统的问题非常典型。第一是各模块数据不通收银数据在本地数据库小程序端是另一个外包团队做的会员数据两边各存各的经常出现小程序上看到余额和门店端不一致的情况。第二是代码结构乱PHP文件里直接写HTMLSQL语句拼在业务逻辑里改一个功能要动三个文件。第三是计费逻辑比较死板只能按整点计费到了非整点就不知道怎么处理更别提会员折扣、时段优惠、节假日调价这些复杂规则。改版的时候我列了一个优先级清单先统一数据模型把会员、桌台、订单、商品这四个核心域的数据结构重新设计再重写收银计时引擎把计费规则做成可配置化然后才是小程序端的重构和星球模块的新增。整个改版过程花了大概两个月其中数据库设计和计费引擎的改造占掉了将近一半时间。事实证明这两个部分也是最容易出问题的地方。2. 技术选型与核心架构解析2.1 为什么用PHP打底这套系统的后端主语言是PHP老代码用的是ThinkPHP 3.2.3框架。改版的时候团队内部其实讨论过要不要换语言比如用Java或者Go重写。但评估下来有几个现实因素让PHP依然是更合理的选择第一台球厅系统的业务逻辑并不复杂核心是CRUD加计费计算PHP的开发和部署效率在这种体量的项目上优势明显。第二老系统积累了完整的数据库结构和业务数据直接迁移比推倒重写风险低得多。第三市面上能招到的PHP开发者数量多后期维护成本可控。第四PHP的生态里ThinkPHP、Laravel这类框架足够支撑门店级系统的并发需求——一家台球厅同时在线用户撑死几百人远没到需要考虑高并发架构的级别。我保留了ThinkPHP框架做为主体但做了一次渐进式重构。把所有业务逻辑从控制器里拆出来放到Service层SQL全部换成查询构造器或者模型关联接口输出统一走一个响应函数。这套重构完成后后续加功能、改计费规则都轻松很多。2.2 JavaScript在小程序和管理后台中的分工JavaScript在这套系统里承担两个角色一个是微信小程序端的核心开发语言一个是管理后台前端也就是门店收银和运营人员用的页面的交互逻辑。小程序端我用了原生小程序语法没有引入uni-app或者Taro这类跨端框架。原因很简单这个项目只需要微信小程序不需要额外兼容支付宝或者抖音小程序。原生语法在包体积、组件调用、性能调优上都有优势还省去了编译链路的排错成本。再说微信小程序的API本身就够完善扫码wx.scanCode、定位wx.getLocation、支付wx.requestPayment、订阅消息wx.requestSubscribeMessage都是现成的能力。管理后台的前端用的是Vue 2加Element UI。选Vue的原因主要是它和原生小程序的开发思路有相似之处数据绑定、组件化团队转型成本低。管理后台的核心页面包括收银台、桌台状态面板、会员列表、商品管理、活动配置和星球内容管理。2.3 接口设计与数据返回规范既然确定了前后端分离的架构接口设计就成了最重要的约束。我定了一套统一的数据返回格式// 公共响应函数 function apiReturn($code 0, $message success, $data []) { header(Content-Type: application/json); echo json_encode([ code $code, message $message, data $data ]); exit; } // 成功返回 apiReturn(0, success, [order_id 10086]); // 失败返回 apiReturn(1001, 桌台已占用, null);这套格式的好处是前端拿到任何接口的返回都能统一处理。code为0表示成功非0表示业务异常message给用户看提示data装业务数据。HTTP状态码我故意没有过度依赖因为有些微信小程序的基础库对非2xx的响应处理不太友好统一用200返回再在业务层里判断code反而更省心。关于PHP数组对象的问题这里特别提醒一下接口返回时如果数据是纯数组json_encode出来就是JSON数组如果是关联数组或者对象json_encode出来的就是JSON对象。前端JS拿到后如果判断不出来可以用Array.isArray()来区分。2.4 数据库核心表设计思路数据库是这套系统的地基表结构设计不好后面全是坑。我整理了五张核心表它们是整个系统的骨架表名核心字段说明memberid, openid, name, phone, level_id, balance, points, source会员主表存基础信息和资产table_infoid, name, type, hourly_rate, status, location桌台信息表status区分空闲/占用/清洁order_infoid, order_no, member_id, table_id, open_time, close_time, total_fee, discount_fee, status订单主表记录每一次开台到结账order_itemid, order_id, goods_id, goods_name, price, count订单明细表记录酒水商品消费member_levelid, level_name, discount, min_points会员等级表和member做关联除此之外还有积分流水表、充值记录表、优惠券表、活动表、星球内容表等辅助表。整体设计的核心原则是订单永远关联会员ID没有会员也至少用openid或者手机号关联这样每一笔消费都能回溯到客户CRM才有数据可用。3. 核心模块实操从CRM到收银到小程序3.1 CRM客户管理模块CRM模块的落地我重点做了三件事会员等级自动升降级、消费行为标签、储值余额管理。会员等级设计得很简单四级体系普通会员、银卡、金卡、黑卡。等级表里配置了对应的折扣率普通会员不打折银卡9.5折金卡9折黑卡8.5折。等级不靠手动改而是根据累计充值金额或者累计消费积分自动触发。这块逻辑我放在了Service层class MemberLevelService { public function refreshLevel($memberId) { $member MemberModel::find($memberId); $totalRecharge RechargeModel::where(member_id, $memberId) -where(status, 1) -sum(amount); // 按累计充值金额自动匹配等级 $level MemberLevelModel::where(min_recharge, , $totalRecharge) -orderBy(min_recharge, desc) -find(); if ($level $member[level_id] ! $level[id]) { $member[level_id] $level[id]; $member-save(); } return $level; } }消费行为标签是CRM里比较出效果的功能。我根据订单表的数据给会员自动打上标签比如经常晚上来订单open_time在18点之后的比例超过70%、喜欢打斯诺克关联的桌台类型为斯诺克的比例最高、两周未到店最近一次订单时间距今天数大于14天。这些标签在运营侧非常有用运营人员可以直接筛选出标签两周未到店的会员群发优惠券拉活效率比盲目群发高很多。3.2 计时收费与桌台状态管理收银模块是台球厅系统的核心也是最容易出Bug的地方。计费逻辑再强调一遍台球按小时收费但客户很少恰好打满整数小时所以计费规则必须支持向上取整或者按分钟计费两种模式而且这个规则必须可配置。我设计了一套计费引擎核心逻辑是开台时记录open_time结账时根据当前时间和桌台类型计算应收费。支持三种计费模式按小时向上取整不足1小时按1小时、按分钟计费精确到分钟、包时段计费比如夜间通宵场。public function calculateFee($tableId, $orderId) { $table TableModel::find($tableId); $order OrderModel::find($orderId); $openTime strtotime($order[open_time]); $closeTime time(); $minutes ceil(($closeTime - $openTime) / 60); $fee 0; if ($table[billing_mode] 1) { // 按小时向上取整 $hours ceil($minutes / 60); $fee $hours * $table[hourly_rate]; } elseif ($table[billing_mode] 2) { // 按分钟计费 $fee $minutes * ($table[hourly_rate] / 60); } // 如果有关联会员计算折扣 $memberId $order[member_id]; if ($memberId) { $member MemberModel::find($memberId); $level MemberLevelModel::find($member[level_id]); $discount $level ? $level[discount] : 10; $fee round($fee * $discount / 10, 2); } // 叠加优惠券 $couponId $order[coupon_id]; if ($couponId) { $coupon CouponModel::find($couponId); $fee max(0, $fee - $coupon[face_value]); } return $fee; }这段代码把订单表里的状态、会员折扣、优惠券叠加都处理了。实际上线后还补了一个最小计费时长的配置比如开台不满半小时按半小时收费这是门店老板提的需求说有些客人来了打十分钟就走按小时收又太贵按分钟收门店亏取了个中间值。桌台状态管理我用了一个简单的状态机空闲0→ 使用中1→ 清洁中2→ 空闲。开台时把状态改成使用中结账后改成清洁中保洁阿姨在后台点清洁完成再改回空闲。这个流程很简单但非常实用收银员能在后台一眼看出哪些桌子可以用哪些正在打扫。3.3 小程序端扫码开台与预约小程序端的核心场景有三个扫码开台、在线预约、会员中心。扫码开台的流程是这样的每张球桌上贴一个带参数的小程序码客户用微信扫一扫进入小程序带上桌台的ID参数scene小程序把table_id传给后端开台接口后端校验桌台状态是空闲后创建一笔订单并把桌台状态改为使用中。public function openTable($memberId, $tableId) { $table TableModel::find($tableId); if ($table[status] ! 0) { apiReturn(1002, 这个桌台当前不可用, null); } $order OrderModel::create([ order_no TB . date(YmdHis) . rand(1000, 9999), member_id $memberId, table_id $tableId, open_time date(Y-m-d H:i:s), status 1 // 1进行中 2已结账 3已取消 ]); $table-status 1; $table-save(); apiReturn(0, 开台成功, [order_id $order[id]]); }这里有个细节小程序码扫出来之后用户如果还没登录会先跳转到授权登录页拿到openid之后先查这个openid有没有绑定手机号没绑定的弹窗让用户留手机号。这一步非常关键因为手机号才是CRM系统里识别客户的主要IDopenid只是微信侧的标识换了手机或者换微信就找不回来了。在线预约稍微复杂一点涉及到时间段的冲突判断。我用的方案是后端在预约接口里做一次重叠区间查询查该桌台在预约时间段内有没有status为1的订单或者预约表中有没有pending状态的记录有就提示用户换时间段。会员中心就比较常规了展示余额、积分、优惠券、消费记录。还有一个比较受门店欢迎的功能是历史账单客户能看到每次打球的时长、费用、消费明细透明度高了之后客户对储值的信任度会明显提升。3.4 星球模块的内容与积分设计星球模块是整个系统里最有运营想象空间的部分。它的定位是门店的私域内容社区我参考了知识星球的形式做了一个轻量版的台球俱乐部星球。功能上包括门店公告教练排班、比赛通知、技术教学短视频和图文、约球广场会员发帖找球友、积分任务。积分设计是星球模块的核心我设定了几种积分获取方式首次完善资料50积分每日签到5积分连续签到第7天额外20消费1元1积分发帖被精华30积分邀请好友注册100积分积分可以兑换台时抵扣券、购买商品时抵现或者兑换球房周边。这个设计打通了星球内容和消费行为——客户为了攒积分会更频繁地打开小程序打开就会看到门店的活动从而带动消费。技术实现上星球内容表核心是content_id、member_id、content_type公告/教学/约球/动态、title、content、images、like_count、comment_count。点赞和评论各建一张表用content_id关联。由于并发量不大我直接用MySQL自增主键没有引入Redis来缓存点赞数。不过在查询列表的时候我用了PHP的分页查询加上在content_id上建索引实测小程序端加载列表的速度在200毫秒以内够用了。4. 常见问题与排查技巧实录4.1 PHP接口返回数组对象前端拿到却是字符串这个坑基本上每个做前后端分离的人都会遇到。接口返回的数据有些字段在PHP里是整数但前端拿到发现是字符串。比如member_id明明数据库存的是intjson_encode出来却变成字符串10086。原因很简单PDO的默认配置里MySQL的int字段通过查询会返回字符串。解决方式是连接MySQL时设置PDO的ATTR_EMULATE_PREPARES为false并且把ATTR_STRINGIFY_FETCHES设置为false让MySQL驱动返回原生int类型new PDO($dsn, $user, $pass, [ PDO::ATTR_EMULATE_PREPARES false, PDO::ATTR_STRINGIFY_FETCHES false ]);ThinkPHP里可以在数据库配置文件中加上PDO [ PDO::ATTR_EMULATE_PREPARES false, PDO::ATTR_STRINGIFY_FETCHES false ]这个配置加上之后前端再判断金额、积分、余额这些字段时就不用担心类型不一致了。4.2 JavaScript闭包与隐式转换的经典坑在管理后台的前端代码里我踩过最多的是闭包问题。最经典的就是循环里绑事件// 错误写法 for (var i 0; i tableList.length; i) { $(#table_ tableList[i].id).click(function() { openTable(tableList[i].id); // 所有点击拿到的都是最后一个值 }); } // 正确写法用let替代var for (let i 0; i tableList.length; i) { $(#table_ tableList[i].id).click(function() { openTable(tableList[i].id); }); }var是函数级作用域循环结束后i已经变成了最大值let是块级作用域每次循环都会生成一个新的绑定。这个特性在JavaScript的for循环闭包问题里属于必考点。另一个比较隐蔽的是隐式转换。做会员筛选的时候我发现了一个匪夷所思的问题输入框里填了100积分筛选出来一批积分为0的会员。排查下来发现是隐式转换的锅把字符串和数值用比较时JavaScript会把两边都转成数值再比较。但如果是把空字符串和0比较结果是true于是积分为0或者什么都没填的记录也被筛选出来了。后来统一改成严格等于并强制把输入框的值用parseInt转换问题才彻底解决。4.3 小程序分包异步化与其他分包中的组件引用小程序端踩的坑是分包异步化的问题。项目做到后期小程序主包的体积已经接近2MB限制我把预约页面和星球模块挪到了分包里。但挪过去之后发现星球模块里引用的一个自定义组件富文本编辑器还在主包里而分包页面不能直接引用主包的组件反过来可以。微信官方提供的解决方案是分包异步化主要两种方式方式一require.async动态加载// 在分包页面里异步加载主包的模块 const editorModule require.async(/components/editor/editor.js);方式二useSp组件跨分包引用{ component: true, usingComponents: { editor: /components/editor/index } }在实际操作中我推荐用分包异步化配置中的independent: trueasync选项组合把富文本编辑器做成独立分包然后再在星球模块的分包里异步引用。这样既减少了主包体积又不会因为组件缺失导致页面白屏。4.4 并发刷卡计费的脏数据问题最后一个要提醒的是并发问题。台球厅高峰期收银员快速操作收银台或者客户在小程序端同时扫码开台后端容易出现同一张桌子被开出去两次的脏数据。我的解决方案是加了一个简单的乐观锁开台接口执行前先做一次状态CASCompare And Set操作只有当桌台状态还是0空闲时才允许改成1使用中。$affectedRows Db::name(table_info) -where(id, $tableId) -where(status, 0) -update([status 1]); if ($affectedRows 0) { apiReturn(1002, 桌台已被占用请刷新重试, null); }这个方法比先查再改可靠得多因为update的where条件里加了status0只有其他请求还没改状态时当前请求才能更新成功。如果两个请求同时进来数据库层面只会有一个update影响行数为1另一个影响行数为0自然被挡掉了。类似的并发控制我也用在了会员充值和优惠券核销上。这套系统从改版到上线再到稳定运行中间经历了太多折腾。我个人最大的体会是门店级系统的复杂度不在于技术而在于业务细节。一个计费规则就牵扯到最小计费时长、折扣叠加顺序、优惠券使用门槛、整数/小数取舍一堆问题。把业务规则梳理清楚技术方案反而是次要的。另外想提醒的就是星球这类私域模块别把它做成一个花架子。积分、内容、活动必须和消费行为深度绑定才能形成运营闭环。如果只是让客户多一个看公告的入口那这个模块基本没有运营价值。最后分享一个小技巧不管系统多小接口层的错误码一定要从一开始就规范化。我前期为了省事很多地方直接返回字符串错误信息后期前端对接时改起来非常痛苦。现在统一维护一份错误码文档前端根据code提示对应的文案联调效率高了很多。这套系统的完整源码可以在合法渠道获取有需要的读者不妨结合自己的场景做二次开发。本文还有配套的精品资源点击获取
返回列表