基于ThinkPHP的在线客服系统源码开源下载:前后端分离+WebSocket实时通讯

在企业级 PHP 生态中,ThinkPHP 凭借其符合 PSR 规范、MVC 架构清晰、学习曲线平缓等特性,一直是快速构建业务系统的首选框架。然而,ThinkPHP 原生基于 HTTP 短连接,无法直接承载 WebSocket 长连接——这是做客服系统时第一个必须正视的技术现实。行业内成熟的解决方案是:ThinkPHP 负责业务逻辑与 RESTful API,Swoole 或 Workerman 作为常驻进程承载 WebSocket 长连接服务,两者通过 Redis 做连接映射与消息路由。CRMChat(Swoole4 + TP6)、php-customer-service-system(ThinkPHP 8 + Workerman)等开源项目均采用了这一架构范式。

源码:zxkfym.top

本文将以源码级视角,拆解基于 ThinkPHP 的在线客服系统是如何实现前后端分离与 WebSocket 实时通讯的,覆盖架构设计、核心模块实现、消息路由机制与生产部署要点。

一、为什么 ThinkPHP 不能直接跑 WebSocket

1

 

很多初学者会尝试在 Controller 里 new Swoole\WebSocket\Server,这是典型的认知陷阱。
ThinkPHP 的 HTTP 请求是短生命周期:请求进来 → 执行完 → 进程销毁。而 WebSocket 必须长期驻留、监听端口、维护连接池。如果把 Server 实例放在控制器里,响应一结束,整个 Server 就被 GC 回收,客户端立刻收到 net::ERR_CONNECTION_REFUSED
⚠️ 真实后果:用户登录后刚点开聊天页,消息就收不到;重连逻辑再完善也救不回来。
正确的路径是:必须用独立常驻进程启动 WebSocket 服务。在 TP6/TP8 生态中,有两种主流选择:
  • topthink/think-swoole:通过 php think swoole:server 启动,Swoole 4 协程引擎承载 WebSocket,适合高并发场景
  • GatewayWorker / Workerman:通过 php start.php start -d 启动独立 Socket 服务,更加轻量、易调试
CRMChat 选择前者(TP6 + Swoole4 + Redis + Vue + MySQL),php-customer-service-system 选择后者(ThinkPHP 8 + Workerman + Uniapp + Vue)。两者本质都是业务层与通信层解耦

二、系统总体架构

2

 

以 CRMChat 为代表的高性能方案,整体架构分为四层:
┌─────────────────────────────────────────────────────────┐
│  用户接入层                                              │
│  PC网页 │ H5移动端 │ 微信小程序 │ 公众号 │ APP嵌入        │
└─────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────┐
│  Nginx 反向代理层                                        │
│  负载均衡 │ SSL终止 │ 静态资源 │ WebSocket Upgrade 转发   │
└─────────────────────────────────────────────────────────┘
                            │
              ┌─────────────┴─────────────┐
              ▼                           ▼
┌──────────────────────────┐  ┌──────────────────────────┐
│  Swoole WebSocket 服务    │  │  ThinkPHP 6 业务层         │
│  消息路由 │ 会话管理       │  │  控制器 │ 服务层 │ 模型层   │
│  心跳检测 │ FD映射表       │  │  验证器 │ 事件监听 │ RBAC    │
└──────────────────────────┘  └──────────────────────────┘
              │                           │
              ▼                           ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ Redis   │  │ MySQL   │  │  队列    │
        │ 缓存/映射│  │ 持久化   │  │  异步    │
        └─────────┘  └─────────┘  └─────────┘
前后端完全分离
  • 后端:ThinkPHP 6 提供 RESTful API,所有接口返回统一 JSON 格式(通过 success() / fail() 方法)
  • 前端:Vue CLI 构建的 PC 管理后台 + H5 客服端
  • 通信:Axios 调用 REST API 处理业务;WebSocket 处理实时消息

三、源码结构与核心模块

3.1 典型目录结构

参考 php-customer-service-system 的多应用模式:
app/
├── admin/              # 后台管理模块
│   ├── controller/
│   │   ├── Chats.php       # 聊天管理(核心)
│   │   ├── Customer.php    # 客户管理
│   │   ├── Set.php         # 系统设置
│   │   └── ...
│   ├── model/              # 数据模型
│   └── validate/           # 验证器
├── api/                # API 接口模块
│   ├── controller/
│   │   ├── Chats.php       # 对话接口
│   │   ├── Customer.php    # 客户接口
│   │   └── Unread.php      # 未读接口
├── websocket/          # WebSocket 事件处理(Swoole/Workerman)
└── common/             # 公共函数
config/
├── swoole.php          # Swoole 配置(WebSocket 服务参数)
├── im.php              # 即时通讯配置(端口、IP)
└── database.php        # 数据库配置
public/                  # Web 入口
websocket/               # Workerman 启动脚本(独立进程)

3.2 Swoole WebSocket 服务配置

config/swoole.php 是 WebSocket 服务的骨架,缺字段或路径错,服务根本起不来
return [
    'http' => [
        'enable' => true,
        'host' => '0.0.0.0',
        'port' => 9501,
        'worker_num' => swoole_cpu_num(),  // 建议为 CPU 核心数的 1-2 倍
        'options' => [
            'package_max_length' => 10 * 1024 * 1024,  // 10MB
        ],
    ],
    'websocket' => [
        'enable' => true,           // 必须显式开启
        'route' => false,
        'handler' => \think\swoole\websocket\Handler::class,
        'ping_interval' => 25000,   // 心跳间隔
        'ping_timeout' => 60000,    // 超时时间
        'room' => [
            'type' => 'table',
            'table' => [
                'room_rows' => 8192,
                'room_size' => 2048,
                'client_rows' => 4096,
                'client_size' => 2048,
            ],
        ],
        'listen' => [
            'open' => \app\webscoket\Connect::class,
            'event' => \app\webscoket\Event::class,
            'close' => \app\webscoket\Close::class,
        ]
    ]
];

3.3 连接生命周期管理

3

 

WebSocket 的三个核心事件:onOpenonMessageonClose。这是整个实时通信的基石。
连接建立(onOpen)
public function onOpen($server, $req) {
    $fd = $req->fd;                    // 客户端唯一标识
    $uid = $req->get['uid'];           // 用户 ID
    $token = $req->get['token'];       // 登录 Token
    
    // 1. 校验 token 合法性(调用 TP 业务层接口或查 Redis)
    if (!$this->verifyToken($token)) {
        $server->push($fd, json_encode([
            'status' => 2, 'message' => 'token 已过期'
        ]));
        $server->close($fd);
        return;
    }
    
    // 2. 绑定 fd ↔ uid 映射到 Redis
    // 键名建议:ws:session:{user_id}
    $redis->hSet('ws:conn_map', $uid, $fd);
    $redis->hSet('ws:conn_map', "fd:{$fd}", $uid);
    
    // 3. 更新用户在线状态
    $redis->hSet('user:status', $uid, 'online');
    
    echo "用户 {$uid} 建立了连接,标识为 {$fd}\n";
}
接收消息(onMessage)
public function onMessage($server, $frame) {
    $fd = $frame->fd;
    $message = json_decode($frame->data, true);
    
    switch ($message['type']) {
        case 'chat':       // 聊天消息
            $this->handleChat($server, $message);
            break;
        case 'ping':       // 心跳
            $server->push($fd, json_encode(['type' => 'pong']));
            break;
        case 'typing':     // 正在输入
            $this->broadcastTyping($message);
            break;
    }
}

private function handleChat($server, $message) {
    $toUid = $message['to_uid'];
    $redis = $this->redis;
    
    // 1. 查询接收方 fd
    $toFd = $redis->hGet('ws:conn_map', $toUid);
    
    // 2. 在线则实时推送
    if ($toFd && $server->isEstablished($toFd)) {
        $server->push($toFd, json_encode([
            'type' => 'chat',
            'from_uid' => $message['from_uid'],
            'content' => $message['content'],
            'timestamp' => time()
        ]));
    } else {
        // 3. 离线则存入离线消息队列
        $redis->lPush("ws:offline:{$toUid}", 
            json_encode($message));
    }
    
    // 4. 异步写入 MySQL(通过 Task Worker 或消息队列)
    $this->saveMessageToDB($message);
}
连接关闭(onClose)
public function onClose($server, $fd) {
    $redis = $this->redis;
    $uid = $redis->hGet('ws:conn_map', "fd:{$fd}");
    
    if ($uid) {
        // 清理连接映射
        $redis->hDel('ws:conn_map', $uid);
        $redis->hDel('ws:conn_map', "fd:{$fd}");
        $redis->hSet('user:status', $uid, 'offline');
        
        // 通知相关客服用户下线
        $this->notifyAgentUserOffline($uid);
    }
}

四、消息路由与会话分配

客服系统的核心不是"群聊",而是精准的点对点消息路由 + 智能坐席分配

4.1 FD 映射与消息流转

系统在 Swoole 服务中维护一个全局的 FD 映射表(借助 Redis Hash 实现跨进程共享):
ws:conn_map:{
  "user_1001": 1024,        // 用户 1001 的连接 fd
  "agent_2001": 1025,       // 客服 2001 的连接 fd
  "fd:1024": "user_1001",   // 反向映射
  "fd:1025": "agent_2001"
}
消息流转过程:
  1. 访客发送消息至 WebSocket 服务
  2. 服务解析消息类型和目标客服 ID
  3. 查询 Redis 获取客服对应 FD
  4. 若客服在线则实时推送,否则存入离线消息队列
  5. 同时将消息异步写入 MySQL

4.2 坐席智能分配策略

CRMChat 支持三种会话分配策略:
  1. 空闲优先:选择当前会话数最少、状态为在线的客服
  2. 随机分派:在所有在线客服中随机选择一个
  3. 分组指派:根据访客来源/问题类型匹配对应技能组
分配逻辑示例:
public function dispatchAgent($visitorUid, $group = null) {
    $redis = $this->redis;
    
    // 查 Redis 中在线客服列表
    $agents = $redis->hGetAll('agent:status');
    $available = [];
    
    foreach ($agents as $agentId => $status) {
        if ($status !== 'online') continue;
        
        // 计算当前会话数
        $sessionCount = $redis->zScore('agent:load', $agentId);
        
        // 分组过滤
        if ($group && !$this->inGroup($agentId, $group)) continue;
        
        $available[$agentId] = $sessionCount;
    }
    
    if (empty($available)) return null;  // 无空闲坐席,进入排队
    
    // 空闲优先:选择会话数最少的
    asort($available);
    return array_key_first($available);
}

五、前后端分离的接口协作

4

 

5.1 REST API 与 WebSocket 的职责划分

REST API(ThinkPHP Controller 处理)
  • 用户登录、签发 JWT Token
  • 客服排班、状态变更
  • 历史会话查询、聊天记录分页
  • 满意度评价提交
  • 话术库管理、客户标签管理
WebSocket(Swoole/Workerman 处理)
  • 实时消息收发
  • 在线状态同步
  • 输入中(typing)通知
  • 新消息提醒推送

5.2 前端连接示例(Vue + WebSocket)

// 建立 WebSocket 连接
const token = store.state.user.token
const ws = new WebSocket(`ws://localhost:9501?uid=${userId}&token=${token}`)

ws.onopen = () => {
    console.log('WebSocket 连接建立')
    // 启动心跳
    this.startHeartbeat()
}

ws.onmessage = (event) => {
    const data = JSON.parse(event.data)
    switch (data.type) {
        case 'chat':
            // 追加消息到会话列表
            store.commit('appendMessage', data)
            // 播放提示音
            this.playNotification()
            break
        case 'pong':
            // 心跳回应
            break
        case 'agent_offline':
            // 客服离线通知
            this.handleAgentOffline(data)
            break
    }
}

ws.onclose = () => {
    console.log('连接断开,尝试重连...')
    setTimeout(() => this.reconnect(), 3000)
}

六、核心业务功能实现

6.1 数据库核心表结构

-- 访客表
CREATE TABLE `visitor` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `uuid` VARCHAR(64) UNIQUE,        -- 访客唯一标识
  `ip` VARCHAR(45),
  `user_agent` TEXT,
  `source_page` VARCHAR(255),       -- 来源页
  `created_at` DATETIME,
  `last_active_at` DATETIME
);

-- 客服表
CREATE TABLE `agent` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT,
  `nickname` VARCHAR(50),
  `max_session` INT DEFAULT 5,      -- 最大并发会话数
  `current_session` INT DEFAULT 0,  -- 当前会话数
  `status` TINYINT DEFAULT 1,       -- 1-在线 0-离线 2-忙碌
  `group_id` INT                    -- 技能组 ID
);

-- 会话表
CREATE TABLE `chat_session` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `visitor_id` BIGINT,
  `agent_id` BIGINT,
  `start_time` DATETIME,
  `end_time` DATETIME,
  `status` TINYINT,                 -- 1-进行中 2-已结束
  `evaluate_score` TINYINT          -- 评价分数
);

-- 消息表
CREATE TABLE `chat_message` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `session_id` BIGINT,
  `sender_type` TINYINT,            -- 1-访客 2-客服
  `sender_id` BIGINT,
  `receiver_id` BIGINT,
  `content` TEXT,
  `msg_type` TINYINT DEFAULT 1,     -- 1-文本 2-图片 3-文件
  `send_time` DATETIME,
  `is_read` TINYINT DEFAULT 0,
  INDEX idx_session (`session_id`),
  INDEX idx_sender (`sender_id`)
) PARTITION BY RANGE (TO_DAYS(send_time));  -- 按月分区

6.2 会话转接实现

会话转接不是前端点个按钮就完事,而是服务端一次连接迁移 + 状态广播 + 前端重连引导的组合动作
public function transferSession($sessionId, $fromAgentId, $toAgentId) {
    // 1. 更新数据库会话归属
    Db::name('chat_session')
        ->where('id', $sessionId)
        ->update(['agent_id' => $toAgentId]);
    
    // 2. 更新 Redis 中的会话映射
    $redis = $this->redis;
    $visitorUid = $this->getVisitorUidBySession($sessionId);
    
    // 3. 通知原客服
    $fromFd = $redis->hGet('ws:conn_map', "agent_{$fromAgentId}");
    if ($fromFd) {
        $this->server->push($fromFd, json_encode([
            'type' => 'transfer_out',
            'session_id' => $sessionId,
            'to_agent' => $toAgentId
        ]));
    }
    
    // 4. 通知新客服
    $toFd = $redis->hGet('ws:conn_map', "agent_{$toAgentId}");
    if ($toFd) {
        $this->server->push($toFd, json_encode([
            'type' => 'transfer_in',
            'session_id' => $sessionId,
            'visitor_uid' => $visitorUid,
            'history' => $this->getSessionHistory($sessionId)
        ]));
    }
    
    // 5. 通知访客(前端引导重连到新客服)
    $visitorFd = $redis->hGet('ws:conn_map', $visitorUid);
    if ($visitorFd) {
        $this->server->push($visitorFd, json_encode([
            'type' => 'agent_changed',
            'new_agent_id' => $toAgentId,
            'message' => '您的问题正在转接给其他客服...'
        ]));
    }
}

七、高可用与生产部署

5

 

7.1 Nginx 反向代理配置

前端通过 wss://yourdomain.com/ws 连接,而非裸 IP+端口(否则 HTTPS 页面无法建立 ws 连接):
server {
    listen 443 ssl;
    server_name kefu.example.com;
    
    # SSL 配置
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # 静态资源与 API
    location / {
        proxy_pass http://127.0.0.1:9501;  # ThinkPHP HTTP 服务
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # WebSocket 反向代理
    location /ws {
        proxy_pass http://127.0.0.1:9502;  # Swoole WebSocket 服务
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;          # 长连接超时
    }
}

7.2 服务启动与守护

Swoole 方案
# 开发环境(前台运行,方便看日志)
php think swoole:server

# 生产环境(守护进程化)
php think swoole:server -d
Workerman 方案
cd websocket/
php start.php start -d

7.3 性能优化要点

  1. worker_num 调优:设置为 CPU 核心数的 1-2 倍
  2. 心跳配置heartbeat_check_interval=60heartbeat_idle_time=600
  3. Redis 连接池:避免每次消息都查数据库,用户会话状态缓存到 Redis
  4. 消息表分区:按月份分区,避免单表过大
  5. 异步落库:通过 Task Worker 或消息队列异步写入 MySQL,避免阻塞主进程

7.4 安全加固

  • 传输层:全链路 WSS(SSL/TLS 加密)
  • 鉴权:WebSocket 握手时校验 JWT Token,无效连接直接关闭
  • Origin 校验:防止跨站 WebSocket 连接
  • 限流:单用户最大并发会话数限制,防止恶意占用
  • 敏感词过滤:消息内容入库前做违禁词检测
  • RBAC 权限:基于角色的权限控制,细粒度到按钮级操作
  • 操作审计:登录日志、操作日志全程留存

八、技术选型对比

面对不同的业务需求,如何在 Swoole 与 Workerman 之间选择?
 
维度
Swoole + think-swoole
Workerman / GatewayWorker
编程模型
协程异步
事件回调
性能
更高(协程调度)
高(事件驱动)
学习曲线
较陡(需理解协程)
平缓
生态
TP 官方集成好
独立 Socket 服务,与 TP 解耦更彻底
适用场景
高并发、复杂业务
轻量部署、快速迭代
代表项目
CRMChat(TP6+Swoole4)
php-customer-service-system(TP8+Workerman)
💡 选型建议:如果追求极致性能和协程特性,选 Swoole;如果希望 WebSocket 服务与 TP 完全解耦、独立部署运维更简单,选 Workerman。

九、AI 赋能与未来演进

现代 ThinkPHP 客服系统正在快速集成 AI 能力:
  • 智能回复:通过后台设置机器人知识库,系统根据关键词自动回复用户
  • 语义理解:基于 NLP 的意图识别,自动分配至对应技能组
  • 人机协作:AI 处理常规问题,复杂问题无缝转接人工客服
  • RAG 知识库:上传企业文档训练专属知识库,回答准确率显著提升

十、源码学习路径建议

如果你想基于开源项目做二次开发,建议按以下顺序阅读源码:
  1. 从入口开始composer.jsonconfig/swoole.php(或 config/im.php)→ WebSocket 启动脚本
  2. 理解连接生命周期onOpenonMessageonClose 三个事件处理器
  3. 深入业务层app/api/controller/Chats.phpapp/admin/controller/Chats.php → Model 层
  4. 前端对照:Vue 组件中的 WebSocket 客户端初始化 → Vuex/Pinia 中的消息处理 → 组件渲染
  5. 扩展点:消息协议定义、ChannelHandler 链、自定义中间件
⚠️ 二次开发注意:生产环境务必将 setAllowedOriginPatterns("*") 改为具体域名;JWT 有效期建议 2 小时并配合黑名单机制;WebSocket 端点建议加上频率限制防刷;消息落库必须异步,避免阻塞主进程。

写在最后:基于 ThinkPHP 的在线客服系统,其技术精髓在于"分"——业务与通信分离、前后端分离、连接与数据分离。ThinkPHP 本身不擅长长连接,但它擅长业务逻辑组织、ORM、验证器、RBAC 等企业级特性;Swoole/Workerman 擅长常驻内存、高并发连接管理。两者的结合不是简单的"1+1",而是通过 Redis 这个"中间件"实现了完美的职责解耦。
CRMChat 用 Swoole4 + TP6 + Redis + Vue + MySQL 证明了这套架构的工业级可行性;php-customer-service-system 用 ThinkPHP 8 + Workerman 展示了轻量方案的灵活性。无论选择哪条路径,掌握"业务层与通信层解耦"这一核心设计思想,才是读懂这类源码的真正钥匙。在这个 AI 与原生私有化部署并行的新时代,这套架构依然具有极强的生命力和扩展空间。
posted @ 2026-08-07 16:56  lincodey  阅读(11)  评论(0)    收藏  举报