
简介基于PHP打造的辰光PHP客服系统多商户全开源源码面向需要自建在线客服平台的企业或开发者。系统支持多商户入驻与管理涵盖实时聊天、客服工作台、工单/留言处理、API接口等模块可用于电商、SaaS等多个业务场景适合有一定PHP基础的开发者进行二次开发与定制。整个资源包共2000个文件约25.07MB以138个PHP核心文件为主搭配241个HTML页面、131个CSS样式、339个JS脚本以及大量PNG图标/图片素材同时包含141个Markdown文档和SQL数据库脚本便于查看说明、导入数据与部署调试。已有291人学习资源保留了完整的目录结构与开源授权文件开箱即用能够帮助开发者快速搭建一套可扩展的多商户客服系统并根据实际业务需求调整功能、界面与交互逻辑。1. 为什么一个开源多商户PHP客服系统还值得自己部署从压缩包文件名和源码结构看这是一套由辰光PHP团队整理的客服tb多商户开源项目适用范围并不是单一网站而是平台型场景一个站点内有多个商家每个商家需要独立的客服账号、会话记录和统计。很多人第一反应是直接用现成的SaaS客服但SaaS的报价往往按坐席数、会话量叠加小商户还好一旦商户量上百年费就压过开发成本了。这套基于PHP的源码给了另一种选择开源意味着你能看到完整PHP代码自己控制部署节点、数据库和消息队列。对已有PHP开发经验的团队来说改造一套多商户客服系统的工作量远小于从零写一个。下面按代码结构、安装、二次开发和性能加固四个角度拆一下这个项目内容偏实操适合正在评估客服系统选型的中小团队也适合想拿PHP源码做外包交付的开发者。2. 拆解辰光客服的代码结构、权限模型与会话状态在动手搭之前我习惯先把压缩包里的目录结构扫一遍。展开网上常见的“辰光php客服tb多商户全开源源码”压缩包后你会发现它不是随便堆积的PHP文件而是带了一层轻量级分层结构。表面上能看到AUTHORS、style.css、amazeui.css、amazeui.min.css、materialdesignicons.min.css、bootstrap.min.css这些前端资源以及若干入口文件。这里很多人会误以为前端只靠Bootstrap其实从amazeui.css的命名能看出项目用了AmazeUI作为移动端优先的UI框架bootstrap更多是为了兼容旧插件。文件列表里还混进了一个pimple.c这个并不是PHP扩展我倾向于认为是仓库迁移时的误提交真实依赖是Composer里的Pimple一个轻量级依赖注入容器。2.1 入口文件、控制器与模板的分离方式多商户客服系统最容易翻车的地方不是聊天消息存储而是权限边界。常规PHP项目会在header里判断session这里如果也这么做多商户环境下非常容易被水平越权。拆了几处关键文件后看到的结构是这样的目录/文件作用/admin平台管理后台维护商户、客服、坐席组/merchant商户端入口商户在这里查看自己的会话列表/kefu客服工作台处理实时会话和历史工单/api对外接口比如创建会话、拉取离线消息/config数据库、缓存、第三方推送的配置/vendorComposer依赖包括Pimple等这种入口分离配合后端对会话ID的归属校验才算把多商户的模型撑起来。实际开发中我一般会再加一层中间件把每次请求的merchant_id和当前登录用户的merchant_id比对避免商户A的客服拿到商户B的会话参数。2.2 商户和客服的权限字段落在哪从数据库表命名看典型的是merchant、merchant_user、session、message、service_group这一类表。核心权限不放在XML或PHP常量里而是通过用户表的role字段和merchant_id共同决定。role的值决定他能不能看到工单、能不能分配会话、能不能看到统计导出merchant_id决定了数据范围。这里的关键是查询条件里必须同时带merchant_id不能只查user_id否则客服离职后被删掉账号历史会话就查不到了。还有一类容易被忽视的数据是客服组。多商户客服系统中一个商户下面可以有多个客服组比如售前、售后。辰光这套源码里用service_group表维护组和成员关系分配新会话时先查组内当前在线客服数量再挑负载最低的一个。判断“在线”依赖sessions表里的last_active时间戳而不是简单的登录状态因为浏览器标签页关掉后PHP session不一定立刻失效。2.3 会话状态机与消息表的索引设计会话表一般会有status字段常见取值是waiting、chatting、closed、transfer。状态流转大致是客户发起咨询 - waiting客服接入 - chatting客服转接或关闭 - transfer/closed。源码里如果逻辑比较朴素可能在同一张表里update很频繁这就需要关注索引。我拆开默认安装SQL后看到message表通常长这样CREATE TABLE message ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, session_id INT UNSIGNED NOT NULL, from_type TINYINT NOT NULL COMMENT 1-客户, 2-客服, from_id INT UNSIGNED NOT NULL, content TEXT, created_at DATETIME NOT NULL, KEY idx_session_created (session_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表语句在辰光客服里很有代表性message并没有直接和merchant_id关联而是通过session_id间接关联。这样做的好处是会话中的历史消息读取非常快索引只需覆盖session_id和created_at就能按时间顺序翻出整个聊天记录。隐患是如果一段话跨多个商户靠session_id反查商户时需要join一次session表。在并发量不高的内部使用场景下没有问题如果你要承接每日几十万条消息的量级建议在message表里冗余merchant_id并在查询时直接带上merchant_id条件减少join。这里没有给完整目录树因为不同转发源里的目录结构略有差异但核心的入口分离、权限字段和会话状态机这三个点能帮你快速判断这套源码是否值得继续折腾。3. 本地搭建与安装环境、配置、数据库初始化把源码下载下来之后第一件事不是双击MySQL导入而是确认PHP版本和扩展。辰光PHP客服tb多商户全开源源码里用了Pimple和若干现代语法建议PHP 7.4以上最好直接用PHP 8.2。官方文件里没有明确标注版本但从代码里的??、?-等语法推断低于7.4会直接抛语法错误。3.1 环境准备PHP扩展和Composer依赖安装前先执行php -v看版本再检查扩展php -v php -m | grep -E pdo|mbstring|openssl|curl|json如果输出里缺了pdo_mysql、mbstring、curl在Ubuntu上补装sudo apt install php8.2-cli php8.2-mysql php8.2-mbstring php8.2-curl php8.2-xml php8.2-zip为什么强调这些扩展pdo_mysql负责数据库连接curl用于客服系统主动向外部接口推送消息mbstring处理utf8mb4下的中文长度截断。少了curl商户后台绑定第三方推送时会直接报“Class CurlHandle not found”之类的错。依赖这块大多数合集的vendor目录是完整的。如果你从Git仓库重新拉源码需要在项目根目录执行composer install --no-dev执行耗时取决于网络国内环境建议先给Composer配阿里云镜像。这里多说一句不要在生产环境执行composer update那会把依赖锁文件的版本冲掉二开项目很容易因为Pimple大版本升级导致服务容器报错。3.2 配置数据库连接和初始导入在config目录下找到database.php或config.php最核心的是下面几项return [ host 127.0.0.1, port 3306, database chenguang_kefu, username kefu_user, password 改成强密码, charset utf8mb4, prefix cg_, ];prefix是表前缀。默认前缀是cg_但你接手时往往会遇到两个项目共用一个数据库的情况改成你自己习惯的前缀比如kc_能避免表名冲突。改这个值不会影响业务逻辑因为所有SQL都是通过统一的模型层拼接前缀。建库和导入数据我用命令行比phpMyAdmin更省事mysql -uroot -p -e CREATE DATABASE chenguang_kefu DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -uroot -p chenguang_kefu install.sql导入完成后重点检查三张表admin_user、merchant、merchant_user。初始管理员账号一般就在install.sql里常见的默认账号是admin/admin123但不同来源打包的哈希盐不一样。如果登录失败不要急着重装直接执行UPDATE cg_admin_user SET password MD5(your_new_password) WHERE username admin;MySQL的MD5函数算出的32位字符串是这套源码比较常用的密码存储方式。如果有加盐逻辑需要看PasswordHelper里的实现别直接套用上面的SQL。3.3 Nginx和Apache的伪静态配置客服系统的前端页面有大量API请求如果没有正确配置伪静态消息发送接口可能404。以下是我在Nginx下常用的配置片段server { listen 80; server_name kefu.example.com; root /var/www/chenguang-kefu/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; } location ~* \.(css|js|png|jpg|gif|svg|ico)$ { expires 7d; access_log on; } }注意root目录指向public还是项目根目录取决于这套源码是否把index.php放在根目录。如果文件列表里直接出现style.css说明index.php和assets在同一层root应写项目根目录不要强行套用Laravel的public目录结构。Apache下的.htaccess也很简单RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php/$1 [L]Apache开启mod_rewrite后同样能满足前端路由。3.4 安装时的常见报错排查最常遇到的三个问题一是安装后页面白屏直接看PHP错误日志和Nginx error.log大概率是vendor目录不完整或者PHP版本低于7.4二是初始化数据库时报“Unknown collation: utf8mb4_unicode_520_ci”说明本机MySQL版本太老把SQL文件里的排序规则改成utf8mb4_general_ci即可三是客服端能登录但商户端列表空白多半是session目录权限不对检查storage目录是否可写。这一章把环境准备、配置、伪静态和报错都过了一遍。到这里系统应该能跑起来下一章进入二开实战。4. 二次开发实战自定义客服分流与Webhook队列推送系统跑通后接下来做的事才是真正值钱的部分把默认的“进来一个会话随机分配给在线客服”改成业务上合理的方式。多商户客服场景里电商平台最典型的需求是不同来源的客户进不同分组未读消息超过阈值时通过Webhook通知商户。4.1 自定义会话分流规则源码默认的分流逻辑通常在KefuController里的assignNextKefu方法中。我先说明默认策略它能做的范围一是找当前商户下面status为online的客服二是按会话数最少的优先。问题在于它没有考虑客服分组。如果要做售前和售后分流思路是给会话表增加source_type字段比如1表示售前2表示售后然后在分配逻辑里加一道查询条件public function assignKefu($merchantId, $groupId) { $kefu $this-db-query( SELECT u.id FROM cg_service_user u LEFT JOIN cg_service_group_member gm ON u.id gm.user_id WHERE u.merchant_id :merchant_id AND u.status 1 AND gm.group_id :group_id AND (u.current_sessions u.max_sessions) ORDER BY u.current_sessions ASC, u.last_online_time DESC LIMIT 1, [ merchant_id $merchantId, group_id $groupId, ] ); return $kefu[0][id] ?? 0; }这段代码比默认实现多了两个关键条件通过gm.group_id限定客服所在的组通过current_sessions max_sessions过滤掉满载客服。注意这里把status1当作在线实际项目里建议改成查最近10秒内有没有心跳记录否则客服挂机但浏览器没关负载还是会被派过去。参数说明$merchantId取自商户后台登录态$_SESSION[merchant_id]$groupId需要你在商户后台增加一个单选字段或者在会话创建时根据URL来源参数判断。前端的来源参数一般是?sourcepre_sale传到后端时做一层映射不要直接把参数拼进SQL。4.2 接入Webhook推送新会话通知很多电商客户希望客户不排队先让后端推送消息让客服在企微或钉钉里收到提醒。Webhook是最容易对接的方式。在消息保存成功后插入一个事件调用private function pushWebhook($sessionId, $customerMsg) { $webhookUrl $this-config[webhook_url] ?? ; if (empty($webhookUrl)) { return; } $payload json_encode([ session_id $sessionId, merchant_id $this-currentMerchantId, message mb_substr($customerMsg, 0, 200), timestamp time(), ]); $ch curl_init($webhookUrl); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_POSTFIELDS $payload, CURLOPT_HTTPHEADER [Content-Type: application/json], CURLOPT_TIMEOUT 3, CURLOPT_CONNECTTIMEOUT 2, CURLOPT_RETURNTRANSFER true, ]); $response curl_exec($ch); curl_close($ch); }这里有个容易踩的坑在创建消息的HTTP请求里直接调用curl如果对方Webhook响应慢客服端发送消息会一起变卡。短期可以用CURLOPT_TIMEOUT设成3秒兜底长期建议把这部分放进PHP队列。源码里如果没有现成队列最简单的改造是用Redis列表先lpush任务再写一个守护进程消费。这里体现的正是PHP队列的常见用法很多网上教程直接把consumer写进crontab每分钟跑一次但那样延迟太高不适合客服场景。4.3 通过Redis队列处理离线消息离线消息的处理常见做法是客户不在线时消息先存message表同时把session_id塞进Redis的待处理队列。客服登录后从队列取出未读session批量标记已读。$redis-lpush(kefu:unread_sessions, json_encode([ session_id $sessionId, merchant_id $merchantId, time time(), ]));消费端逻辑while ($data $redis-brpop(kefu:unread_sessions, 5)) { $payload json_decode($data[1], true); $this-markSessionUnread($payload[session_id]); }brpop的第二个参数是阻塞时间5秒内没有新任务就返回null。相比简单轮询阻塞队列能减少对MySQL的压力。这里建议将队列内容做去重避免同一条消息被多个worker重复消费。我一般在session_id外再加一个message_id字段消费前用sismember检查是否已处理。通过这个改造系统就不再是简单的“用户发消息、客服刷新”而是有了典型的智能客服中台雏形。对应到电商行业智能客服中心的应用场景实时性、可追查性、可扩展性都会上一个台阶。5. 性能与安全缓存、防注入与多商户隔离装好、改好之后就要考虑压测和加固了。很多开源的PHP客服系统能跑通但一到商户大会报500问题往往出在缓存策略和SQL单点查询上。5.1 给热点查询加Redis缓存会话列表和未读数是读多写少的典型代表。以商户后台首页为例每次打开都统计每个客服的排队会话数默认实现大概率是SELECT service_user_id, COUNT(*) FROM cg_session WHERE status waiting GROUP BY service_user_id;商户量少没问题多商户并发时这个聚合查询会拖垮数据库。我建议把统计结果缓存到Redis缓存时间设定为10秒是为了在客服接入会话后状态变更与页面刷新之间保持一个可接受的延迟。如果你把时间设成60秒商户会反馈“客户都接进来了排队数还没变”。这里要区分缓存数据和实时数据的边界在线状态必须实时排队数允许延迟10秒。5.2 SQL注入与XSS过滤客服系统中聊天内容是最大的注入入口。有些开发者在渲染聊天记录时直接拼接HTML客户发一条scriptalert(1)/script就能打到客服后台。正确的做法分两步写入时原文保存输出时过滤。$safeContent htmlspecialchars($rawContent, ENT_QUOTES, UTF-8);htmlspecialchars会把双引号和单引号都转义适合用在消息气泡里。特别注意不要对富文本消息直接过滤否则会把合法的图片标签删掉。如果项目里支持富文本用HTMLPurifier的配置脚本或者至少把script、iframe、onerror白名单外的事件全部剥掉。5.3 多商户隔离的验证技巧最后一个要提的点是验证“多商户隔离”是否真的到位。我通常在完成二开后写一个安全测试清单测试项操作预期结果会话越权商户A登录后手动改session_id为商户B的会话ID返回无权限不展示任何聊天记录客服跨商户商户A的客服取商户B的merchant_id接口返回merchant_not_matchAPI鉴权不带token请求/api/create_session返回401XSS注入在聊天框输入script标签客服端原样展示文本不执行脚本手动测试时抓一个包习惯在浏览器开发者工具里改请求参数观察服务端返回码和响应体。如果改掉session_id后只返回了空数组而不是403说明查询条件里漏了merchant_id过滤这是最危险的越权漏洞。多数开源源码的模型层只写了where idxxx需要你统一补一层where merchant_id xxx并把这条路加进CI检查。如果没有漏掉merchant_id过滤再跑一轮WebSocket压测这一步通常能看到稳定性的真正天花板。本文还有配套的精品资源点击获取