565 lines
22 KiB
Markdown
565 lines
22 KiB
Markdown
# OPSX Design: 在线客服模块技术设计
|
||
|
||
## 变更标识
|
||
- **变更ID**: customer-service-module
|
||
- **版本**: 2.0
|
||
- **状态**: DESIGN_COMPLETE
|
||
- **更新日期**: 2026-01-28
|
||
|
||
---
|
||
|
||
## 1. 架构设计
|
||
|
||
### 1.1 系统架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 客户端层 │
|
||
├─────────────┬─────────────┬─────────────┬─────────────────────────┤
|
||
│ PC端 │ Game端 │ Portal端 │ Admin客服工作台 │
|
||
│ (Vue 2.x) │ (Vue 3.x) │ (uni-app) │ (Layui + WS) │
|
||
│ Element UI │ Vant │ uView │ │
|
||
└──────┬──────┴──────┬──────┴──────┬──────┴───────────┬─────────────┘
|
||
│ │ │ │
|
||
└─────────────┼─────────────┼───────────────────┘
|
||
│ WebSocket (chat.* 事件)
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Socket服务 (TP6 + Swoole) │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ ChatConnect │ │ ChatMessage │ │ ChatSession │ Listeners │
|
||
│ │ Listener │ │ Listener │ │ Listener │ │
|
||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||
│ │ │ │ │
|
||
│ └────────────────┼────────────────┘ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ ChatService │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||
│ │ │ MessageSvc │ │ SessionSvc │ │ AssignSvc │ │ │
|
||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────┬──────────────────────────────────────┘
|
||
│
|
||
┌─────────────────────┼─────────────────────┐
|
||
▼ ▼ ▼
|
||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
│ MySQL │ │ Redis │ │ 文件存储 │
|
||
│ │ │ │ │ │
|
||
│ cg_chat_session │ │ cs:online:* │ │ public/uploads/ │
|
||
│ cg_chat_message │ │ cs:conn:* │ │ chat/ │
|
||
│ cg_chat_quick_ │ │ cs:lock:* │ │ │
|
||
│ reply │ │ cs:queue:* │ │ │
|
||
│ cg_chat_admin_ │ │ cs:agent:* │ │ │
|
||
│ status │ │ │ │ │
|
||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||
```
|
||
|
||
### 1.2 模块职责
|
||
|
||
| 模块 | 职责 | 技术栈 |
|
||
|------|------|--------|
|
||
| **ChatConnect Listener** | 处理WS连接/断开、心跳、鉴权 | Swoole onOpen/onClose |
|
||
| **ChatMessage Listener** | 处理消息收发、ACK、重试 | Swoole onMessage |
|
||
| **ChatSession Listener** | 处理会话创建/分配/转接/结束 | Swoole onMessage |
|
||
| **MessageService** | 消息持久化、查询、状态更新 | ThinkPHP Model |
|
||
| **SessionService** | 会话生命周期管理 | ThinkPHP Model |
|
||
| **AssignService** | 会话分配、负载均衡、队列管理 | Redis + Model |
|
||
|
||
---
|
||
|
||
## 2. 数据库设计
|
||
|
||
### 2.1 表结构 (最终版)
|
||
|
||
```sql
|
||
-- 客服会话表
|
||
CREATE TABLE `cg_chat_session` (
|
||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||
`user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
|
||
`admin_id` int(10) unsigned DEFAULT NULL COMMENT '客服ID',
|
||
`source` tinyint(1) NOT NULL DEFAULT '1' COMMENT '来源: 1=PC 2=Game 3=Portal',
|
||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=待分配 1=进行中 2=已结束',
|
||
`rating` tinyint(1) DEFAULT NULL COMMENT '评分: 1-5',
|
||
`rating_content` varchar(500) DEFAULT NULL COMMENT '评价内容',
|
||
`last_msg_id` bigint(20) unsigned DEFAULT NULL COMMENT '最后消息ID(雪花)',
|
||
`last_msg_time` int(10) unsigned DEFAULT NULL COMMENT '最后消息时间',
|
||
`create_time` int(10) unsigned NOT NULL,
|
||
`update_time` int(10) unsigned NOT NULL,
|
||
`end_time` int(10) unsigned DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_user_status` (`user_id`, `status`),
|
||
KEY `idx_admin_status` (`admin_id`, `status`),
|
||
KEY `idx_status_create` (`status`, `create_time`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服会话表';
|
||
|
||
-- 聊天消息表
|
||
CREATE TABLE `cg_chat_message` (
|
||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||
`msg_id` bigint(20) unsigned NOT NULL COMMENT '消息ID(雪花算法)',
|
||
`session_id` int(10) unsigned NOT NULL COMMENT '会话ID',
|
||
`sender_type` tinyint(1) NOT NULL COMMENT '发送者类型: 1=用户 2=客服',
|
||
`sender_id` int(10) unsigned NOT NULL COMMENT '发送者ID',
|
||
`msg_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '消息类型: 1=文字 2=图片',
|
||
`content` varchar(500) NOT NULL COMMENT '消息内容(文字或图片URL)',
|
||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=pending 1=sent 2=delivered 3=read 4=failed',
|
||
`retry_count` tinyint(1) NOT NULL DEFAULT '0' COMMENT '重试次数',
|
||
`create_time` int(10) unsigned NOT NULL,
|
||
`delivered_time` int(10) unsigned DEFAULT NULL,
|
||
`read_time` int(10) unsigned DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_msg_id` (`msg_id`),
|
||
KEY `idx_session_id` (`session_id`, `id`),
|
||
KEY `idx_session_status` (`session_id`, `status`),
|
||
KEY `idx_sender` (`sender_type`, `sender_id`, `create_time`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';
|
||
|
||
-- 快捷回复表
|
||
CREATE TABLE `cg_chat_quick_reply` (
|
||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||
`category` varchar(50) DEFAULT NULL COMMENT '分类',
|
||
`title` varchar(100) NOT NULL COMMENT '标题',
|
||
`content` text NOT NULL COMMENT '内容',
|
||
`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',
|
||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 0=禁用 1=启用',
|
||
`create_time` int(10) unsigned NOT NULL,
|
||
`update_time` int(10) unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_category_status` (`category`, `status`, `sort`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快捷回复表';
|
||
|
||
-- 客服状态表 (辅助表,主状态在Redis)
|
||
CREATE TABLE `cg_chat_admin_status` (
|
||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||
`admin_id` int(10) unsigned NOT NULL COMMENT '客服ID',
|
||
`max_sessions` int(10) unsigned NOT NULL DEFAULT '10' COMMENT '最大会话数',
|
||
`is_enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用客服功能',
|
||
`last_online_time` int(10) unsigned DEFAULT NULL,
|
||
`create_time` int(10) unsigned NOT NULL,
|
||
`update_time` int(10) unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_admin_id` (`admin_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服状态配置表';
|
||
```
|
||
|
||
### 2.2 索引设计说明
|
||
|
||
| 表 | 索引 | 用途 |
|
||
|----|------|------|
|
||
| cg_chat_session | `idx_user_status` | 查询用户活跃会话 |
|
||
| cg_chat_session | `idx_admin_status` | 查询客服进行中会话 |
|
||
| cg_chat_message | `uk_msg_id` | 消息幂等性校验 |
|
||
| cg_chat_message | `idx_session_id` | 按会话查询消息(分页) |
|
||
| cg_chat_message | `idx_session_status` | 查询会话未读消息 |
|
||
|
||
---
|
||
|
||
## 3. 核心算法设计
|
||
|
||
### 3.1 雪花算法 (消息ID生成)
|
||
|
||
```php
|
||
// app/utils/Snowflake.php
|
||
class Snowflake
|
||
{
|
||
private const EPOCH = 1704067200000; // 2024-01-01 00:00:00 UTC
|
||
private const WORKER_ID_BITS = 5;
|
||
private const DATACENTER_ID_BITS = 5;
|
||
private const SEQUENCE_BITS = 12;
|
||
|
||
private $workerId;
|
||
private $datacenterId;
|
||
private $sequence = 0;
|
||
private $lastTimestamp = -1;
|
||
|
||
public function __construct(int $workerId = 1, int $datacenterId = 1)
|
||
{
|
||
$this->workerId = $workerId & 0x1F; // 5 bits
|
||
$this->datacenterId = $datacenterId & 0x1F; // 5 bits
|
||
}
|
||
|
||
public function nextId(): int
|
||
{
|
||
$timestamp = $this->currentTimeMillis();
|
||
|
||
if ($timestamp === $this->lastTimestamp) {
|
||
$this->sequence = ($this->sequence + 1) & 0xFFF; // 12 bits
|
||
if ($this->sequence === 0) {
|
||
$timestamp = $this->waitNextMillis($this->lastTimestamp);
|
||
}
|
||
} else {
|
||
$this->sequence = 0;
|
||
}
|
||
|
||
$this->lastTimestamp = $timestamp;
|
||
|
||
return (($timestamp - self::EPOCH) << 22)
|
||
| ($this->datacenterId << 17)
|
||
| ($this->workerId << 12)
|
||
| $this->sequence;
|
||
}
|
||
|
||
private function currentTimeMillis(): int
|
||
{
|
||
return (int)(microtime(true) * 1000);
|
||
}
|
||
|
||
private function waitNextMillis(int $lastTimestamp): int
|
||
{
|
||
$timestamp = $this->currentTimeMillis();
|
||
while ($timestamp <= $lastTimestamp) {
|
||
$timestamp = $this->currentTimeMillis();
|
||
}
|
||
return $timestamp;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.2 会话分配算法 (最少会话数 + 分布式锁)
|
||
|
||
```php
|
||
// app/services/chat/AssignService.php
|
||
class AssignService
|
||
{
|
||
private const LOCK_TTL = 3000; // 3秒
|
||
private const LOCK_PREFIX = 'cs:lock:assign:';
|
||
private const ONLINE_PREFIX = 'cs:online:agent:';
|
||
private const LOAD_PREFIX = 'cs:agent:load:';
|
||
|
||
/**
|
||
* 分配会话给客服
|
||
* @param int $userId 用户ID
|
||
* @param int $sessionId 会话ID
|
||
* @return int|null 分配的客服ID,null表示无可用客服
|
||
*/
|
||
public function assignSession(int $userId, int $sessionId): ?int
|
||
{
|
||
$redis = $this->getRedis();
|
||
$lockKey = self::LOCK_PREFIX . $userId;
|
||
|
||
// 1. 获取分配锁 (防止并发双分配)
|
||
$lockValue = uniqid('', true);
|
||
$acquired = $redis->set($lockKey, $lockValue, ['NX', 'PX' => self::LOCK_TTL]);
|
||
|
||
if (!$acquired) {
|
||
// 锁被占用,检查是否已有活跃会话
|
||
return $this->getExistingSessionAgent($userId);
|
||
}
|
||
|
||
try {
|
||
// 2. 检查用户是否已有活跃会话
|
||
$existingAgent = $this->getExistingSessionAgent($userId);
|
||
if ($existingAgent !== null) {
|
||
return $existingAgent;
|
||
}
|
||
|
||
// 3. 获取所有在线客服
|
||
$onlineAgents = $this->getOnlineAgents($redis);
|
||
if (empty($onlineAgents)) {
|
||
// 无在线客服,进入留言队列
|
||
$this->addToOfflineQueue($userId, $sessionId);
|
||
return null;
|
||
}
|
||
|
||
// 4. 选择会话数最少的客服
|
||
$selectedAgent = $this->selectLeastLoadAgent($redis, $onlineAgents);
|
||
if ($selectedAgent === null) {
|
||
// 所有客服已满载
|
||
$this->addToOfflineQueue($userId, $sessionId);
|
||
return null;
|
||
}
|
||
|
||
// 5. 更新会话归属
|
||
$this->bindSessionToAgent($sessionId, $selectedAgent, $redis);
|
||
|
||
// 6. 增加客服负载计数
|
||
$redis->incr(self::LOAD_PREFIX . $selectedAgent);
|
||
|
||
return $selectedAgent;
|
||
|
||
} finally {
|
||
// 释放锁 (仅释放自己持有的锁)
|
||
$this->releaseLock($redis, $lockKey, $lockValue);
|
||
}
|
||
}
|
||
|
||
private function selectLeastLoadAgent(Redis $redis, array $onlineAgents): ?int
|
||
{
|
||
$loads = [];
|
||
foreach ($onlineAgents as $agentId) {
|
||
$load = (int)$redis->get(self::LOAD_PREFIX . $agentId) ?: 0;
|
||
$maxSessions = $this->getAgentMaxSessions($agentId);
|
||
|
||
if ($load < $maxSessions) {
|
||
$loads[$agentId] = $load;
|
||
}
|
||
}
|
||
|
||
if (empty($loads)) {
|
||
return null;
|
||
}
|
||
|
||
// 返回负载最小的客服
|
||
asort($loads);
|
||
return array_key_first($loads);
|
||
}
|
||
|
||
private function releaseLock(Redis $redis, string $key, string $value): void
|
||
{
|
||
// Lua脚本保证原子性:仅当值匹配时才删除
|
||
$script = <<<LUA
|
||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||
return redis.call('del', KEYS[1])
|
||
else
|
||
return 0
|
||
end
|
||
LUA;
|
||
$redis->eval($script, [$key, $value], 1);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 消息重试算法 (指数退避)
|
||
|
||
```php
|
||
// app/services/chat/MessageService.php
|
||
class MessageService
|
||
{
|
||
private const MAX_RETRY = 3;
|
||
private const RETRY_DELAYS = [1000, 2000, 4000]; // ms
|
||
|
||
/**
|
||
* 推送消息并处理重试
|
||
*/
|
||
public function pushMessage(int $msgId, int $targetFd, array $payload): bool
|
||
{
|
||
$redis = $this->getRedis();
|
||
$server = $this->getSwooleServer();
|
||
|
||
for ($retry = 0; $retry <= self::MAX_RETRY; $retry++) {
|
||
// 检查目标连接是否有效
|
||
if (!$server->isEstablished($targetFd)) {
|
||
// 连接已断开,标记为pending等待重连补发
|
||
$this->markMessagePending($msgId);
|
||
return false;
|
||
}
|
||
|
||
// 尝试推送
|
||
$result = $server->push($targetFd, json_encode($payload));
|
||
|
||
if ($result) {
|
||
// 推送成功,更新状态为sent
|
||
$this->updateMessageStatus($msgId, 'sent');
|
||
return true;
|
||
}
|
||
|
||
// 推送失败,记录重试次数
|
||
$this->incrementRetryCount($msgId);
|
||
|
||
if ($retry < self::MAX_RETRY) {
|
||
// 指数退避等待
|
||
usleep(self::RETRY_DELAYS[$retry] * 1000);
|
||
}
|
||
}
|
||
|
||
// 超过最大重试次数,标记为failed
|
||
$this->updateMessageStatus($msgId, 'failed');
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.4 离线队列处理 (余额优先)
|
||
|
||
```php
|
||
// app/services/chat/AssignService.php (续)
|
||
class AssignService
|
||
{
|
||
private const QUEUE_KEY = 'cs:queue:pending';
|
||
|
||
/**
|
||
* 添加到离线队列 (按余额降序)
|
||
*/
|
||
public function addToOfflineQueue(int $userId, int $sessionId): void
|
||
{
|
||
$redis = $this->getRedis();
|
||
|
||
// 获取用户余额
|
||
$user = Db::name('user')->where('id', $userId)->find();
|
||
$balance = $user['money'] ?? 0;
|
||
|
||
// ZSET score 使用负余额实现降序 (余额高的先处理)
|
||
$score = -$balance;
|
||
$member = json_encode(['userId' => $userId, 'sessionId' => $sessionId, 'time' => time()]);
|
||
|
||
$redis->zAdd(self::QUEUE_KEY, $score, $member);
|
||
}
|
||
|
||
/**
|
||
* 客服上线时处理队列
|
||
*/
|
||
public function processOfflineQueue(int $adminId): void
|
||
{
|
||
$redis = $this->getRedis();
|
||
|
||
while (true) {
|
||
// 获取队列中优先级最高的会话 (score最小 = 余额最高)
|
||
$items = $redis->zRange(self::QUEUE_KEY, 0, 0);
|
||
|
||
if (empty($items)) {
|
||
break; // 队列为空
|
||
}
|
||
|
||
$item = json_decode($items[0], true);
|
||
|
||
// 检查客服是否还能接单
|
||
$currentLoad = (int)$redis->get(self::LOAD_PREFIX . $adminId) ?: 0;
|
||
$maxSessions = $this->getAgentMaxSessions($adminId);
|
||
|
||
if ($currentLoad >= $maxSessions) {
|
||
break; // 客服已满载
|
||
}
|
||
|
||
// 尝试分配 (使用分布式锁)
|
||
$assigned = $this->assignSession($item['userId'], $item['sessionId']);
|
||
|
||
if ($assigned === $adminId) {
|
||
// 分配成功,从队列移除
|
||
$redis->zRem(self::QUEUE_KEY, $items[0]);
|
||
|
||
// 通知用户会话已被接入
|
||
$this->notifyUserSessionAssigned($item['userId'], $item['sessionId'], $adminId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 接口设计
|
||
|
||
### 4.1 Admin 后台 HTTP API
|
||
|
||
| 接口 | 方法 | 路径 | 说明 |
|
||
|------|------|------|------|
|
||
| 获取会话列表 | GET | `/admin/chat/sessions` | 支持状态筛选 |
|
||
| 获取会话详情 | GET | `/admin/chat/session/{id}` | 包含用户信息 |
|
||
| 获取消息历史 | GET | `/admin/chat/messages` | 支持分页、搜索 |
|
||
| 结束会话 | POST | `/admin/chat/session/{id}/end` | 手动关闭会话 |
|
||
| 转接会话 | POST | `/admin/chat/session/{id}/transfer` | 转给其他客服 |
|
||
| 快捷回复列表 | GET | `/admin/chat/quick-replies` | CRUD |
|
||
| 添加快捷回复 | POST | `/admin/chat/quick-reply` | - |
|
||
| 编辑快捷回复 | PUT | `/admin/chat/quick-reply/{id}` | - |
|
||
| 删除快捷回复 | DELETE | `/admin/chat/quick-reply/{id}` | - |
|
||
| 客服统计 | GET | `/admin/chat/stats` | 会话数、评分统计 |
|
||
| 导出记录 | GET | `/admin/chat/export` | CSV导出 |
|
||
|
||
### 4.2 用户端 HTTP API
|
||
|
||
| 接口 | 方法 | 路径 | 说明 |
|
||
|------|------|------|------|
|
||
| 获取会话状态 | GET | `/api/chat/status` | 检查是否有活跃会话 |
|
||
| 上传图片 | POST | `/api/chat/upload` | 返回图片URL |
|
||
| 提交评价 | POST | `/api/chat/rate` | 会话评价 |
|
||
| 获取历史消息 | GET | `/api/chat/history` | 重连后拉取 |
|
||
|
||
---
|
||
|
||
## 5. 文件结构
|
||
|
||
### 5.1 Socket 模块
|
||
|
||
```
|
||
Socket/app/
|
||
├── listener/chat/
|
||
│ ├── ChatConnectListener.php # 连接/断开/心跳处理
|
||
│ ├── ChatMessageListener.php # 消息收发处理
|
||
│ └── ChatSessionListener.php # 会话管理处理
|
||
├── services/chat/
|
||
│ ├── ChatService.php # 聊天服务入口
|
||
│ ├── MessageService.php # 消息服务
|
||
│ ├── SessionService.php # 会话服务
|
||
│ └── AssignService.php # 分配服务
|
||
├── models/
|
||
│ ├── ChatSession.php # 会话模型
|
||
│ ├── ChatMessage.php # 消息模型
|
||
│ └── ChatQuickReply.php # 快捷回复模型
|
||
└── utils/
|
||
└── Snowflake.php # 雪花算法
|
||
```
|
||
|
||
### 5.2 Pro 模块 (Admin后台)
|
||
|
||
```
|
||
Pro/application/admin/
|
||
├── controller/
|
||
│ ├── Chat.php # 客服工作台控制器
|
||
│ ├── ChatRecord.php # 聊天记录控制器
|
||
│ └── ChatQuickReply.php # 快捷回复控制器
|
||
└── view/
|
||
├── chat/
|
||
│ ├── index.html # 客服工作台页面
|
||
│ └── record.html # 聊天记录页面
|
||
└── chat_quick_reply/
|
||
├── index.html # 快捷回复列表
|
||
└── add.html # 添加快捷回复
|
||
```
|
||
|
||
### 5.3 前端模块
|
||
|
||
```
|
||
PC/src/components/chat/
|
||
├── ChatWindow.vue # 聊天窗口容器
|
||
├── ChatHeader.vue # 聊天头部(客服信息/关闭)
|
||
├── ChatMessages.vue # 消息列表
|
||
├── ChatMessage.vue # 单条消息气泡
|
||
├── ChatInput.vue # 输入框+发送
|
||
└── ChatRating.vue # 评价组件
|
||
|
||
Game/src/components/chat/
|
||
├── ChatWindow.vue # 聊天窗口(Vant风格)
|
||
├── ChatMessages.vue # 消息列表
|
||
├── ChatMessage.vue # 消息气泡
|
||
└── ChatInput.vue # 输入框
|
||
|
||
Portal/components/chat/
|
||
├── ChatWindow.vue # 聊天窗口(uView风格)
|
||
├── ChatMessages.vue # 消息列表
|
||
└── ChatInput.vue # 输入框
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 安全设计
|
||
|
||
### 6.1 认证与授权
|
||
|
||
| 场景 | 方案 |
|
||
|------|------|
|
||
| 用户端WS连接 | 复用HTTP登录Token,WS握手时校验 |
|
||
| 客服端WS连接 | 复用Admin Session,额外校验客服权限 |
|
||
| 消息发送 | 服务端强制使用鉴权后的身份,禁止伪造senderId |
|
||
|
||
### 6.2 输入校验
|
||
|
||
| 字段 | 校验规则 |
|
||
|------|----------|
|
||
| 文字消息 | 长度≤500,XSS过滤 |
|
||
| 图片上传 | MIME白名单(jpg/png/gif),大小≤2MB |
|
||
| sessionId | 必须属于当前用户或客服 |
|
||
|
||
### 6.3 限流
|
||
|
||
| 维度 | 限制 |
|
||
|------|------|
|
||
| 消息发送 | 每用户每秒最多5条 |
|
||
| 图片上传 | 每用户每分钟最多10张 |
|
||
| WS重连 | 每用户每分钟最多10次 |
|
||
|
||
---
|
||
|
||
*文档版本: 2.0*
|
||
*最后更新: 2026-01-28*
|