辰光PHP多商户客服系统部署与二次开发实战指南
2026/9/16 2:01:48 网站建设 项目流程

简介:基于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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段建表语句在辰光客服里很有代表性: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过滤掉满载客服。注意这里把status=1当作在线,实际项目里建议改成查最近10秒内有没有心跳记录,否则客服挂机但浏览器没关,负载还是会被派过去。

参数说明:$merchantId取自商户后台登录态,$_SESSION['merchant_id'];$groupId需要你在商户后台增加一个单选字段,或者在会话创建时根据URL来源参数判断。前端的来源参数一般是?source=pre_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,客户发一条<script>alert(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_match
API鉴权不带token请求/api/create_session返回401
XSS注入在聊天框输入script标签客服端原样展示文本,不执行脚本

手动测试时抓一个包习惯:在浏览器开发者工具里改请求参数,观察服务端返回码和响应体。如果改掉session_id后只返回了空数组而不是403,说明查询条件里漏了merchant_id过滤,这是最危险的越权漏洞。多数开源源码的模型层只写了where id=xxx,需要你统一补一层where merchant_id = xxx,并把这条路加进CI检查。如果没有漏掉merchant_id过滤,再跑一轮WebSocket压测,这一步通常能看到稳定性的真正天花板。

本文还有配套的精品资源,点击获取

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

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

立即咨询