feat: 牛牛单独限红 - 新增limit_low_nn/limit_high_nn字段,代理端添加/编辑代理和会员支持牛牛限红设置,语言包三语支持
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
# 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*
|
||||
@@ -0,0 +1,396 @@
|
||||
# OPSX Proposal: 在线客服模块集成
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **创建日期**: 2026-01-28
|
||||
- **状态**: SPEC_COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## 1. 上下文 (Context)
|
||||
|
||||
### 1.1 用户需求
|
||||
在OG Live Gaming Platform的admin后台集成在线客服模块,将PC端和移动端(Game/Portal)的客服功能从外链跳转改为内置实时聊天,所有用户消息统一转发到后台由客服人员回复。
|
||||
|
||||
### 1.2 现有实现
|
||||
- **当前方案**: 外链跳转第三方客服平台
|
||||
- PC端: `PC/src/components/updateService/update-service.vue` - 通过 `webconfig.customerLink` 跳转
|
||||
- Portal端: `Portal/pages/user/customservice.vue` - 支持 Telegram/Line 外部链接
|
||||
- **问题**: 无法统一管理用户咨询,缺乏历史记录追溯
|
||||
|
||||
### 1.3 技术环境
|
||||
| 组件 | 技术栈 | 版本 |
|
||||
|------|--------|------|
|
||||
| 后端主服务 | ThinkPHP | 5.x |
|
||||
| WebSocket服务 | ThinkPHP + Swoole | 6.x |
|
||||
| PC前端 | Vue.js + Element UI | 2.x |
|
||||
| Game前端 | Vue.js + Vant | 3.x |
|
||||
| Portal前端 | uni-app + uView | - |
|
||||
| 数据库 | MySQL | 5.7 |
|
||||
| 缓存 | Redis | 5.x |
|
||||
|
||||
---
|
||||
|
||||
## 2. 约束集合 (Constraint Sets)
|
||||
|
||||
### 2.1 硬约束 (Hard Constraints) - 不可违反
|
||||
|
||||
| ID | 约束 | 来源 | 影响 |
|
||||
|----|------|------|------|
|
||||
| HC-01 | 必须使用现有Socket服务(ThinkPHP 6.x + Swoole)作为WebSocket通信基础 | 架构一致性 | 不能引入新的WebSocket服务 |
|
||||
| HC-02 | 数据库表必须使用 `cg_` 前缀 | `application/database.php:23` | 表名规范 |
|
||||
| HC-03 | Admin后台控制器必须继承 `Common` 基类 | `application/admin/controller/Common.php` | 权限控制 |
|
||||
| HC-04 | 前端API请求必须通过现有封装的HTTP客户端 | 各端API封装 | 接口一致性 |
|
||||
| HC-05 | 用户认证必须复用现有Session/Token机制 | 安全性 | 不能引入新认证体系 |
|
||||
|
||||
### 2.2 软约束 (Soft Constraints) - 建议遵循
|
||||
|
||||
| ID | 约束 | 来源 | 建议 |
|
||||
|----|------|------|------|
|
||||
| SC-01 | Admin后台UI应使用Layui框架 | 现有后台风格 | 保持视觉一致性 |
|
||||
| SC-02 | WebSocket事件应遵循现有listener模式 | `Socket/app/listener/` | 代码组织一致 |
|
||||
| SC-03 | 前端组件命名应遵循各端现有规范 | 代码风格 | PascalCase组件名 |
|
||||
| SC-04 | 日志记录应使用现有 `insertAdminLog()` 函数 | `application/helper.php` | 审计追踪 |
|
||||
|
||||
### 2.3 依赖约束 (Dependencies)
|
||||
|
||||
| ID | 依赖项 | 类型 | 说明 |
|
||||
|----|--------|------|------|
|
||||
| DC-01 | cg_user 表 | 数据 | 用户信息关联 |
|
||||
| DC-02 | cg_admin 表 | 数据 | 客服人员账号 |
|
||||
| DC-03 | Redis | 基础设施 | 在线状态、会话分配 |
|
||||
| DC-04 | Socket服务 | 服务 | 实时消息推送 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 需求规格 (Requirements)
|
||||
|
||||
### 3.1 功能需求
|
||||
|
||||
#### REQ-01: 后台客服工作台
|
||||
**场景**: 客服人员登录admin后台,进入客服模块
|
||||
**验收标准**:
|
||||
- [ ] 显示待接入会话列表(按等待时间排序)
|
||||
- [ ] 显示当前处理中的会话列表
|
||||
- [ ] 支持同时处理多个会话(标签页切换)
|
||||
- [ ] 实时显示新消息通知(声音+视觉提示)
|
||||
- [ ] 显示用户基本信息(用户名、余额、来源端)
|
||||
|
||||
#### REQ-02: 会话自动分配
|
||||
**场景**: 用户发起客服咨询
|
||||
**验收标准**:
|
||||
- [ ] 系统自动将用户分配给当前会话数最少的在线客服
|
||||
- [ ] 客服离线时,消息进入待分配队列
|
||||
- [ ] 支持会话转接给其他在线客服
|
||||
- [ ] 记录会话分配历史
|
||||
|
||||
#### REQ-03: 实时消息通信
|
||||
**场景**: 用户与客服进行对话
|
||||
**验收标准**:
|
||||
- [ ] 支持文字消息发送/接收
|
||||
- [ ] 支持图片消息发送/接收(上传+预览)
|
||||
- [ ] 消息实时推送(延迟<500ms)
|
||||
- [ ] 显示消息发送状态(发送中/已送达/已读)
|
||||
- [ ] 支持消息时间戳显示
|
||||
|
||||
#### REQ-04: 离线消息
|
||||
**场景**: 用户发送消息时客服全部离线,或客服回复时用户已离线
|
||||
**验收标准**:
|
||||
- [ ] 离线消息持久化存储
|
||||
- [ ] 用户/客服上线后自动推送未读消息
|
||||
- [ ] 显示未读消息数量角标
|
||||
|
||||
#### REQ-05: 快捷回复
|
||||
**场景**: 客服需要快速回复常见问题
|
||||
**验收标准**:
|
||||
- [ ] 支持预设快捷回复语管理(增删改)
|
||||
- [ ] 支持快捷回复分类
|
||||
- [ ] 一键插入快捷回复内容
|
||||
|
||||
#### REQ-06: 聊天记录查询
|
||||
**场景**: 客服或管理员需要查看历史对话
|
||||
**验收标准**:
|
||||
- [ ] 支持按用户名搜索
|
||||
- [ ] 支持按时间范围筛选
|
||||
- [ ] 支持按客服人员筛选
|
||||
- [ ] 支持导出聊天记录
|
||||
|
||||
#### REQ-07: 用户信息展示
|
||||
**场景**: 客服处理会话时需要了解用户背景
|
||||
**验收标准**:
|
||||
- [ ] 显示用户账号、昵称
|
||||
- [ ] 显示用户余额
|
||||
- [ ] 显示用户来源(PC/Game/Portal)
|
||||
- [ ] 显示用户上级代理信息
|
||||
- [ ] 显示用户最近下注记录(可选)
|
||||
|
||||
#### REQ-08: 会话评价
|
||||
**场景**: 用户结束咨询后对服务进行评价
|
||||
**验收标准**:
|
||||
- [ ] 会话结束后弹出评价窗口
|
||||
- [ ] 支持1-5星评分
|
||||
- [ ] 支持文字评价(可选)
|
||||
- [ ] 后台可查看评价统计
|
||||
|
||||
#### REQ-09: 前端客服入口改造
|
||||
**场景**: 用户在PC/Game/Portal端点击客服按钮
|
||||
**验收标准**:
|
||||
- [ ] PC端: 替换外链跳转为内嵌聊天窗口
|
||||
- [ ] Game端: 添加客服聊天组件
|
||||
- [ ] Portal端: 替换外链页面为聊天页面
|
||||
- [ ] 三端UI风格与各自设计语言一致
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库设计
|
||||
|
||||
### 4.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 '评价内容',
|
||||
`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 `user_id` (`user_id`),
|
||||
KEY `admin_id` (`admin_id`),
|
||||
KEY `status` (`status`),
|
||||
KEY `create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服会话表';
|
||||
|
||||
-- 聊天消息表
|
||||
CREATE TABLE `cg_chat_message` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`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` text NOT NULL COMMENT '消息内容',
|
||||
`is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已读',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `session_id` (`session_id`),
|
||||
KEY `sender_type` (`sender_type`),
|
||||
KEY `is_read` (`is_read`),
|
||||
KEY `create_time` (`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 '状态',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `category` (`category`),
|
||||
KEY `status` (`status`)
|
||||
) 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',
|
||||
`is_online` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否在线',
|
||||
`current_sessions` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '当前会话数',
|
||||
`max_sessions` int(10) unsigned NOT NULL DEFAULT '10' COMMENT '最大会话数',
|
||||
`last_active_time` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `admin_id` (`admin_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服状态表';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术方案
|
||||
|
||||
### 5.1 架构图
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ PC端 │ │ Game端 │ │ Portal端 │
|
||||
│ (Vue 2.x) │ │ (Vue 3.x) │ │ (uni-app) │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
└────────────────┼────────────────┘
|
||||
│ WebSocket
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Socket服务 │
|
||||
│ (TP6 + Swoole) │
|
||||
│ │
|
||||
│ 新增listener: │
|
||||
│ - ChatConnect │
|
||||
│ - ChatMessage │
|
||||
│ - ChatSession │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ MySQL │ │ Redis │ │ 文件存储 │
|
||||
│ 消息持久化│ │ 在线状态 │ │ 图片上传 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Admin后台 │
|
||||
│ (TP5 + Layui) │
|
||||
│ │
|
||||
│ 新增模块: │
|
||||
│ - Chat控制器 │
|
||||
│ - 客服工作台 │
|
||||
│ - 快捷回复管理 │
|
||||
│ - 聊天记录查询 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 WebSocket事件设计
|
||||
|
||||
| 事件名 | 方向 | 说明 |
|
||||
|--------|------|------|
|
||||
| `chat.connect` | Client→Server | 用户/客服建立聊天连接 |
|
||||
| `chat.message` | 双向 | 发送/接收消息 |
|
||||
| `chat.typing` | 双向 | 正在输入状态 |
|
||||
| `chat.read` | Client→Server | 消息已读回执 |
|
||||
| `chat.session.new` | Server→Admin | 新会话通知 |
|
||||
| `chat.session.assign` | Server→Admin | 会话分配通知 |
|
||||
| `chat.session.transfer` | Admin→Server | 会话转接 |
|
||||
| `chat.session.end` | 双向 | 会话结束 |
|
||||
| `chat.admin.online` | Admin→Server | 客服上线 |
|
||||
| `chat.admin.offline` | Admin→Server | 客服下线 |
|
||||
|
||||
### 5.3 文件变更清单
|
||||
|
||||
#### Pro模块 (Admin后台)
|
||||
```
|
||||
application/admin/controller/
|
||||
├── Chat.php # 新增 - 客服工作台控制器
|
||||
├── ChatRecord.php # 新增 - 聊天记录控制器
|
||||
└── ChatQuickReply.php # 新增 - 快捷回复管理控制器
|
||||
|
||||
application/admin/view/
|
||||
├── chat/
|
||||
│ ├── index.html # 新增 - 客服工作台页面
|
||||
│ └── record.html # 新增 - 聊天记录页面
|
||||
└── chat_quick_reply/
|
||||
├── index.html # 新增 - 快捷回复列表
|
||||
└── add.html # 新增 - 添加快捷回复
|
||||
|
||||
application/admin/view/index/index.html # 修改 - 添加客服菜单入口
|
||||
```
|
||||
|
||||
#### Socket模块
|
||||
```
|
||||
app/listener/chat/
|
||||
├── ChatConnect.php # 新增 - 聊天连接处理
|
||||
├── ChatMessage.php # 新增 - 消息处理
|
||||
└── ChatSession.php # 新增 - 会话管理
|
||||
|
||||
app/services/chat/
|
||||
├── ChatService.php # 新增 - 聊天核心服务
|
||||
├── SessionService.php # 新增 - 会话分配服务
|
||||
└── MessageService.php # 新增 - 消息存储服务
|
||||
|
||||
app/models/chat/
|
||||
├── ChatSession.php # 新增 - 会话模型
|
||||
├── ChatMessage.php # 新增 - 消息模型
|
||||
└── ChatQuickReply.php # 新增 - 快捷回复模型
|
||||
```
|
||||
|
||||
#### PC端
|
||||
```
|
||||
src/components/chat/
|
||||
├── ChatWindow.vue # 新增 - 聊天窗口组件
|
||||
├── ChatMessage.vue # 新增 - 消息气泡组件
|
||||
└── ChatInput.vue # 新增 - 输入框组件
|
||||
|
||||
src/components/updateService/
|
||||
└── update-service.vue # 修改 - 改为打开聊天窗口
|
||||
```
|
||||
|
||||
#### Game端
|
||||
```
|
||||
src/components/chat/
|
||||
├── ChatWindow.vue # 新增 - 聊天窗口组件
|
||||
├── ChatMessage.vue # 新增 - 消息气泡组件
|
||||
└── ChatInput.vue # 新增 - 输入框组件
|
||||
|
||||
src/components/
|
||||
└── HallNav.vue # 修改 - 添加客服入口
|
||||
```
|
||||
|
||||
#### Portal端
|
||||
```
|
||||
pages/user/
|
||||
└── customservice.vue # 修改 - 改为聊天页面
|
||||
|
||||
components/chat/
|
||||
├── ChatWindow.vue # 新增 - 聊天窗口组件
|
||||
└── ChatMessage.vue # 新增 - 消息组件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险评估
|
||||
|
||||
| 风险 | 等级 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| WebSocket连接数过多导致服务压力 | 中 | 实现连接池、心跳检测、自动断开空闲连接 |
|
||||
| 消息丢失 | 中 | 消息持久化优先、ACK确认机制 |
|
||||
| 图片上传占用存储 | 低 | 图片压缩、定期清理、可配置存储路径 |
|
||||
| 客服全部离线时用户体验 | 低 | 显示离线提示、支持留言 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 成功判据 (Success Criteria)
|
||||
|
||||
- [ ] 用户可在PC/Game/Portal三端发起客服咨询
|
||||
- [ ] 客服可在Admin后台实时接收和回复消息
|
||||
- [ ] 消息延迟 < 500ms
|
||||
- [ ] 支持文字和图片消息
|
||||
- [ ] 离线消息可正常存储和推送
|
||||
- [ ] 会话自动分配正常工作
|
||||
- [ ] 聊天记录可查询和导出
|
||||
- [ ] 快捷回复功能正常
|
||||
- [ ] 会话评价功能正常
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施阶段建议
|
||||
|
||||
### Phase 1: 基础架构
|
||||
- 数据库表创建
|
||||
- Socket服务聊天事件监听器
|
||||
- 基础消息收发功能
|
||||
|
||||
### Phase 2: Admin后台
|
||||
- 客服工作台UI
|
||||
- 会话管理
|
||||
- 快捷回复管理
|
||||
|
||||
### Phase 3: 前端改造
|
||||
- PC端聊天组件
|
||||
- Game端聊天组件
|
||||
- Portal端聊天页面
|
||||
|
||||
### Phase 4: 增强功能
|
||||
- 离线消息
|
||||
- 会话评价
|
||||
- 聊天记录查询导出
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 1.0*
|
||||
*最后更新: 2026-01-28*
|
||||
@@ -0,0 +1,248 @@
|
||||
# OPSX Specs: 在线客服模块
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **版本**: 2.0
|
||||
- **状态**: SPEC_COMPLETE
|
||||
- **更新日期**: 2026-01-28
|
||||
|
||||
---
|
||||
|
||||
## 1. 确认约束 (Confirmed Constraints)
|
||||
|
||||
### 1.1 会话分配约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-01 | 分配策略 | least-load | 分配给当前会话数最少的在线客服 |
|
||||
| CC-02 | 分配锁超时 | 3000ms | SET NX PX 3000 防止并发双分配 |
|
||||
| CC-03 | 单客服最大会话数 | 10 | 超过后不再分配新会话 |
|
||||
| CC-04 | 分配锁Key | `cs:lock:assign:{userId}` | Redis分布式锁 |
|
||||
|
||||
### 1.2 消息机制约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-05 | 送达确认 | 两段ACK | server-ack(服务端已接收) + peer-ack(对端已送达) |
|
||||
| CC-06 | 重试策略 | 3次指数退避 | 间隔: 1s → 2s → 4s,失败后标记failed |
|
||||
| CC-07 | 消息ID生成 | 雪花算法 | 分布式唯一、有序、高性能 |
|
||||
| CC-08 | 消息长度限制 | 500字符 | 超过截断或拒绝 |
|
||||
| CC-09 | 重连拉取上限 | 50条 | 用户重连后最多拉取50条未读消息 |
|
||||
|
||||
### 1.3 连接管理约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-10 | 心跳间隔 | 30秒 | 客户端每30秒发送ping |
|
||||
| CC-11 | 离线判定 | 60秒 | 60秒无心跳判定为离线 |
|
||||
| CC-12 | 事件命名空间 | `chat.*` | 与游戏事件 `game.*` 隔离 |
|
||||
| CC-13 | 在线状态TTL | 60秒 | Redis Key TTL,心跳续期 |
|
||||
|
||||
### 1.4 离线处理约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-14 | 离线模式 | 留言模式 | 提示"客服不在线",消息入队待处理 |
|
||||
| CC-15 | 队列优先级 | 余额优先 | 按用户余额降序处理 |
|
||||
| CC-16 | 队列Key | `cs:queue:pending` | Redis ZSET,score=用户余额(负数实现降序) |
|
||||
|
||||
### 1.5 图片存储约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-17 | 存储方式 | 本地文件系统 | `public/uploads/chat/YYYYMMDD/` |
|
||||
| CC-18 | 大小限制 | 2MB | 2097152 bytes |
|
||||
| CC-19 | 格式支持 | jpg,png,gif | MIME类型白名单校验 |
|
||||
|
||||
### 1.6 会话生命周期约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-20 | 自动结束 | 禁用 | 仅支持手动关闭会话 |
|
||||
| CC-21 | 评价触发 | 手动 | 用户主动点击评价按钮,无弹窗 |
|
||||
|
||||
---
|
||||
|
||||
## 2. PBT 属性 (Property-Based Testing)
|
||||
|
||||
### 2.1 幂等性属性
|
||||
|
||||
```
|
||||
PROPERTY: MessageIdempotency
|
||||
INVARIANT: ∀ msg1, msg2 ∈ Messages: msg1.msgId = msg2.msgId → DB.count(msgId) = 1
|
||||
BOUNDARY: 并发发送相同msgId消息
|
||||
FALSIFICATION:
|
||||
- 生成随机msgId
|
||||
- 并发100个goroutine/协程发送相同msgId
|
||||
- 断言: SELECT COUNT(*) FROM cg_chat_message WHERE msg_id = ? 返回 1
|
||||
```
|
||||
|
||||
### 2.2 会话唯一性属性
|
||||
|
||||
```
|
||||
PROPERTY: SessionUniqueness
|
||||
INVARIANT: ∀ user ∈ Users: COUNT(sessions WHERE user_id = user AND status = 'active') ≤ 1
|
||||
BOUNDARY: 用户快速多次发起咨询
|
||||
FALSIFICATION:
|
||||
- 选择随机用户
|
||||
- 并发10个请求创建会话
|
||||
- 断言: SELECT COUNT(*) FROM cg_chat_session WHERE user_id = ? AND status IN (0,1) 返回 ≤ 1
|
||||
```
|
||||
|
||||
### 2.3 分配原子性属性
|
||||
|
||||
```
|
||||
PROPERTY: AssignmentAtomicity
|
||||
INVARIANT: 会话分配要么完全成功(session.admin_id != NULL),要么完全失败(session.status = 0)
|
||||
BOUNDARY: 分配过程中客服下线
|
||||
FALSIFICATION:
|
||||
- 开始分配流程
|
||||
- 在分配锁获取后、写入admin_id前,模拟客服下线
|
||||
- 断言: session.admin_id = NULL AND session.status = 0 (待分配)
|
||||
```
|
||||
|
||||
### 2.4 消息顺序性属性
|
||||
|
||||
```
|
||||
PROPERTY: MessageOrdering
|
||||
INVARIANT: ∀ session: messages ORDER BY id ASC = messages ORDER BY create_time ASC
|
||||
BOUNDARY: 高并发消息发送
|
||||
FALSIFICATION:
|
||||
- 发送100条带序号(1-100)的消息
|
||||
- 查询: SELECT content FROM cg_chat_message WHERE session_id = ? ORDER BY id
|
||||
- 断言: 序号严格递增 1,2,3,...,100
|
||||
```
|
||||
|
||||
### 2.5 ACK一致性属性
|
||||
|
||||
```
|
||||
PROPERTY: AckConsistency
|
||||
INVARIANT: ∀ msg: msg.status = 'delivered' → 不会再次推送给同一接收方
|
||||
BOUNDARY: 网络抖动导致ACK延迟
|
||||
FALSIFICATION:
|
||||
- 发送消息并模拟ACK丢失
|
||||
- 客户端重连
|
||||
- 断言: 已标记delivered的消息不在补发列表中
|
||||
```
|
||||
|
||||
### 2.6 负载均衡属性
|
||||
|
||||
```
|
||||
PROPERTY: LoadBalancing
|
||||
INVARIANT: ∀ agents a1, a2 ∈ OnlineAgents: |a1.sessionCount - a2.sessionCount| ≤ 1
|
||||
BOUNDARY: 多用户同时发起咨询
|
||||
FALSIFICATION:
|
||||
- 10个客服在线,各0个会话
|
||||
- 100个用户并发发起咨询
|
||||
- 断言: 每个客服会话数在 [9, 11] 范围内
|
||||
```
|
||||
|
||||
### 2.7 离线消息完整性属性
|
||||
|
||||
```
|
||||
PROPERTY: OfflineMessageIntegrity
|
||||
INVARIANT: ∀ msg sent while all agents offline: msg ∈ PendingQueue
|
||||
BOUNDARY: 客服全部离线时大量消息
|
||||
FALSIFICATION:
|
||||
- 所有客服下线
|
||||
- 发送50条消息
|
||||
- 断言: cg_chat_message.count = 50 AND all status = 'pending'
|
||||
```
|
||||
|
||||
### 2.8 余额排序正确性属性
|
||||
|
||||
```
|
||||
PROPERTY: BalancePriorityOrdering
|
||||
INVARIANT: 队列处理顺序按用户余额降序
|
||||
BOUNDARY: 相同余额用户按时间排序
|
||||
FALSIFICATION:
|
||||
- 插入用户: balance=[100,500,200,500,300]
|
||||
- 客服上线处理
|
||||
- 断言: 处理顺序为 balance 500(先到), 500(后到), 300, 200, 100
|
||||
```
|
||||
|
||||
### 2.9 心跳续期属性
|
||||
|
||||
```
|
||||
PROPERTY: HeartbeatRenewal
|
||||
INVARIANT: 心跳后在线状态TTL重置为60秒
|
||||
BOUNDARY: 心跳边界时间
|
||||
FALSIFICATION:
|
||||
- 建立连接,记录Redis TTL
|
||||
- 等待29秒,发送心跳
|
||||
- 断言: TTL重置为60秒
|
||||
- 等待31秒不发心跳
|
||||
- 断言: 用户被标记为离线
|
||||
```
|
||||
|
||||
### 2.10 图片大小校验属性
|
||||
|
||||
```
|
||||
PROPERTY: ImageSizeValidation
|
||||
INVARIANT: 图片大小 > 2MB 时上传被拒绝
|
||||
BOUNDARY: 边界值 2MB ± 1 byte
|
||||
FALSIFICATION:
|
||||
- 上传 2097151 bytes (2MB-1): 断言成功
|
||||
- 上传 2097152 bytes (2MB): 断言成功
|
||||
- 上传 2097153 bytes (2MB+1): 断言失败,返回错误码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Redis Key 设计
|
||||
|
||||
| Key Pattern | 类型 | TTL | 说明 |
|
||||
|-------------|------|-----|------|
|
||||
| `cs:online:agent:{adminId}` | STRING | 60s | 客服在线状态,心跳续期 |
|
||||
| `cs:conn:user:{userId}` | STRING | 60s | 用户连接映射 fd |
|
||||
| `cs:conn:agent:{adminId}` | STRING | 60s | 客服连接映射 fd |
|
||||
| `cs:session:owner:{sessionId}` | STRING | - | 会话归属客服ID |
|
||||
| `cs:user:active_session:{userId}` | STRING | - | 用户当前活跃会话ID |
|
||||
| `cs:lock:assign:{userId}` | STRING | 3s | 会话分配锁 |
|
||||
| `cs:queue:pending` | ZSET | - | 待处理队列,score=-balance |
|
||||
| `cs:agent:load:{adminId}` | STRING | - | 客服当前会话数 |
|
||||
|
||||
---
|
||||
|
||||
## 4. WebSocket 事件协议
|
||||
|
||||
### 4.1 客户端 → 服务端
|
||||
|
||||
| 事件 | Payload | 说明 |
|
||||
|------|---------|------|
|
||||
| `chat.connect` | `{token, source}` | 建立聊天连接 |
|
||||
| `chat.message.send` | `{sessionId, msgType, content, clientMsgId}` | 发送消息 |
|
||||
| `chat.message.ack` | `{msgId}` | 消息已读回执 |
|
||||
| `chat.typing` | `{sessionId, isTyping}` | 正在输入状态 |
|
||||
| `chat.session.end` | `{sessionId}` | 用户结束会话 |
|
||||
| `chat.session.rate` | `{sessionId, rating, content}` | 会话评价 |
|
||||
| `chat.ping` | `{}` | 心跳 |
|
||||
|
||||
### 4.2 服务端 → 客户端
|
||||
|
||||
| 事件 | Payload | 说明 |
|
||||
|------|---------|------|
|
||||
| `chat.connected` | `{sessionId, agentInfo}` | 连接成功,返回会话信息 |
|
||||
| `chat.message.new` | `{msgId, sessionId, senderType, content, time}` | 新消息 |
|
||||
| `chat.message.server_ack` | `{clientMsgId, msgId, status}` | 服务端确认收到 |
|
||||
| `chat.message.peer_ack` | `{msgId, status}` | 对端已送达/已读 |
|
||||
| `chat.typing` | `{sessionId, isTyping}` | 对方正在输入 |
|
||||
| `chat.session.assigned` | `{sessionId, agentInfo}` | 会话已分配客服 |
|
||||
| `chat.session.ended` | `{sessionId}` | 会话已结束 |
|
||||
| `chat.offline_notice` | `{message}` | 客服离线提示 |
|
||||
| `chat.pong` | `{}` | 心跳响应 |
|
||||
|
||||
### 4.3 客服端专用事件
|
||||
|
||||
| 事件 | 方向 | Payload | 说明 |
|
||||
|------|------|---------|------|
|
||||
| `chat.agent.online` | C→S | `{maxSessions}` | 客服上线 |
|
||||
| `chat.agent.offline` | C→S | `{}` | 客服下线 |
|
||||
| `chat.session.new` | S→C | `{sessionId, userInfo, source}` | 新会话通知 |
|
||||
| `chat.session.transfer` | C→S | `{sessionId, targetAdminId}` | 转接会话 |
|
||||
| `chat.queue.list` | S→C | `{sessions: [...]}` | 待处理队列 |
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 2.0*
|
||||
*最后更新: 2026-01-28*
|
||||
@@ -0,0 +1,443 @@
|
||||
# OPSX Tasks: 在线客服模块实施计划
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **版本**: 2.0
|
||||
- **状态**: ARCHIVED
|
||||
- **更新日期**: 2026-01-29
|
||||
- **完成日期**: 2026-01-29
|
||||
|
||||
---
|
||||
|
||||
## 执行原则
|
||||
|
||||
> **零决策实施**: 以下所有任务均为纯机械执行,无需实施者做任何技术决策。
|
||||
> 所有参数、算法、约束已在 specs.md 和 design.md 中完全定义。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 基础架构 (数据库 + Socket服务)
|
||||
|
||||
### TASK-1.1: 创建数据库表
|
||||
|
||||
**输入**: design.md Section 2.1 表结构SQL
|
||||
|
||||
**执行步骤**:
|
||||
1. 在 `Pro/init.sql` 末尾追加以下4个表的CREATE语句:
|
||||
- `cg_chat_session`
|
||||
- `cg_chat_message`
|
||||
- `cg_chat_quick_reply`
|
||||
- `cg_chat_admin_status`
|
||||
2. 执行SQL创建表
|
||||
|
||||
**验收标准**:
|
||||
- [x] 4个表创建成功
|
||||
- [x] 所有索引创建正确
|
||||
- [x] 字符集为utf8mb4
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.2: 实现雪花算法工具类
|
||||
|
||||
**输入**: design.md Section 3.1 Snowflake代码
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建文件 `Socket/app/utils/Snowflake.php`
|
||||
2. 复制 design.md 中的 Snowflake 类实现
|
||||
3. 配置 workerId=1, datacenterId=1
|
||||
|
||||
**验收标准**:
|
||||
- [x] 生成ID唯一且有序
|
||||
- [x] 10000次调用无重复
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.3: 创建Socket模型类
|
||||
|
||||
**输入**: design.md Section 2.1 表结构
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/models/chat/ChatSession.php`
|
||||
2. 创建 `Socket/app/models/chat/ChatMessage.php`
|
||||
3. 创建 `Socket/app/models/chat/ChatQuickReply.php`
|
||||
4. 创建 `Socket/app/models/chat/ChatAdminStatus.php`
|
||||
|
||||
**验收标准**:
|
||||
- [x] 4个模型类创建完成
|
||||
- [x] 表名前缀正确 `cg_`
|
||||
- [x] 自动时间戳配置正确
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.4: 实现AssignService分配服务
|
||||
|
||||
**输入**:
|
||||
- design.md Section 3.2 分配算法
|
||||
- specs.md Section 1 约束 (CC-01~CC-04)
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/services/chat/AssignService.php`
|
||||
2. 实现以下方法:
|
||||
- `assignSession(int $userId, int $sessionId): ?int`
|
||||
- `getOnlineAgents(): array`
|
||||
- `selectLeastLoadAgent(): ?int`
|
||||
- `addToOfflineQueue(int $userId, int $sessionId): void`
|
||||
- `processOfflineQueue(int $adminId): void`
|
||||
- `releaseLock(): void`
|
||||
3. Redis Key 前缀严格使用 specs.md Section 3 定义
|
||||
|
||||
**约束参数**:
|
||||
- 分配锁超时: 3000ms
|
||||
- 单客服最大会话数: 10
|
||||
- 队列Key: `cs:queue:pending`
|
||||
- 队列Score: `-balance` (余额负数实现降序)
|
||||
|
||||
**验收标准**:
|
||||
- [x] 分配锁防止并发双分配
|
||||
- [x] 最少会话数策略正确
|
||||
- [x] 离线队列按余额排序
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.5: 实现MessageService消息服务
|
||||
|
||||
**输入**:
|
||||
- design.md Section 3.3 重试算法
|
||||
- specs.md Section 1 约束 (CC-05~CC-09)
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/services/chat/MessageService.php`
|
||||
2. 实现以下方法:
|
||||
- `createMessage(array $data): int` (生成雪花ID,入库)
|
||||
- `pushMessage(int $msgId, int $targetFd, array $payload): bool`
|
||||
- `updateMessageStatus(int $msgId, string $status): void`
|
||||
- `getUnreadMessages(int $sessionId, int $limit = 50): array`
|
||||
- `markMessagesAsRead(array $msgIds): void`
|
||||
|
||||
**约束参数**:
|
||||
- 最大重试次数: 3
|
||||
- 重试间隔: [1000, 2000, 4000] ms
|
||||
- 消息长度限制: 500字符
|
||||
- 重连拉取上限: 50条
|
||||
|
||||
**验收标准**:
|
||||
- [x] 消息ID使用雪花算法
|
||||
- [x] 重试策略为指数退避
|
||||
- [x] 消息状态正确流转
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.6: 实现SessionService会话服务
|
||||
|
||||
**输入**: specs.md 约束
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/services/chat/SessionService.php`
|
||||
2. 实现以下方法:
|
||||
- `createSession(int $userId, int $source): int`
|
||||
- `getActiveSession(int $userId): ?array`
|
||||
- `endSession(int $sessionId, int $operatorId): bool`
|
||||
- `transferSession(int $sessionId, int $newAdminId): bool`
|
||||
- `rateSession(int $sessionId, int $rating, ?string $content): bool`
|
||||
|
||||
**约束参数**:
|
||||
- 会话状态: 0=待分配, 1=进行中, 2=已结束
|
||||
- 无自动超时关闭
|
||||
|
||||
**验收标准**:
|
||||
- [x] 同一用户只能有一个活跃会话
|
||||
- [x] 转接会话正确更新admin_id
|
||||
- [x] 评价存储正确
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.7: 实现ChatConnectListener
|
||||
|
||||
**输入**:
|
||||
- specs.md Section 4 WebSocket事件协议
|
||||
- specs.md 约束 (CC-10~CC-13)
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/listener/chat/ChatConnectListener.php`
|
||||
2. 处理事件:
|
||||
- `chat.connect`: 验证Token,注册在线状态
|
||||
- `chat.ping`: 续期在线状态TTL
|
||||
- 断开连接: 清理Redis映射
|
||||
3. 在线状态Key: `cs:online:agent:{adminId}` / `cs:conn:user:{userId}`
|
||||
|
||||
**约束参数**:
|
||||
- 心跳间隔: 30秒
|
||||
- 离线判定: 60秒 (TTL)
|
||||
- 在线状态TTL: 60秒
|
||||
|
||||
**验收标准**:
|
||||
- [x] 连接时验证Token
|
||||
- [x] 心跳正确续期TTL
|
||||
- [x] 断开时清理Redis
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.8: 实现ChatMessageListener
|
||||
|
||||
**输入**: specs.md Section 4 事件协议
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/listener/chat/ChatMessageListener.php`
|
||||
2. 处理事件:
|
||||
- `chat.message.send`: 创建消息 → 返回server_ack → 推送对端 → 等待peer_ack
|
||||
- `chat.message.ack`: 更新消息状态为delivered/read
|
||||
- `chat.typing`: 转发正在输入状态
|
||||
|
||||
**验收标准**:
|
||||
- [x] 两段ACK机制实现
|
||||
- [x] 消息幂等性(msgId去重)
|
||||
- [x] typing状态正确转发
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.9: 实现ChatSessionListener
|
||||
|
||||
**输入**: specs.md Section 4 事件协议
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/listener/chat/ChatSessionListener.php`
|
||||
2. 处理事件:
|
||||
- `chat.session.end`: 结束会话
|
||||
- `chat.session.rate`: 提交评价
|
||||
- `chat.agent.online`: 客服上线,触发队列处理
|
||||
- `chat.agent.offline`: 客服下线
|
||||
- `chat.session.transfer`: 转接会话
|
||||
|
||||
**验收标准**:
|
||||
- [x] 客服上线自动处理离线队列
|
||||
- [x] 转接正确更新会话归属
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.10: 注册Listener到Swoole事件
|
||||
|
||||
**输入**: 现有Socket模块event.php配置
|
||||
|
||||
**执行步骤**:
|
||||
1. 编辑 `Socket/config/event.php`
|
||||
2. 在listen数组中添加聊天相关Listener
|
||||
|
||||
**验收标准**:
|
||||
- [x] Listener正确注册
|
||||
- [x] 不影响现有游戏事件处理
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Admin后台
|
||||
|
||||
### TASK-2.1: 创建Chat控制器
|
||||
|
||||
**输入**: design.md Section 4.1 API
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/controller/Chat.php`
|
||||
2. 继承 `Common` 基类
|
||||
3. 实现方法:
|
||||
- `index()`: 客服工作台页面
|
||||
- `sessions()`: 获取会话列表
|
||||
- `messages()`: 获取消息历史
|
||||
- `endSession()`: 结束会话
|
||||
- `transfer()`: 转接会话
|
||||
- `stats()`: 统计数据
|
||||
|
||||
**验收标准**:
|
||||
- [x] 继承Common基类
|
||||
- [x] 权限校验通过
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.2: 创建ChatQuickReply控制器
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/controller/ChatQuickReply.php`
|
||||
2. 实现CRUD方法:
|
||||
- `index()`: 列表
|
||||
- `add()`: 添加
|
||||
- `edit()`: 编辑
|
||||
- `del()`: 删除
|
||||
|
||||
**验收标准**:
|
||||
- [x] CRUD功能完整
|
||||
- [x] 使用insertAdminLog记录操作
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.3: 创建客服工作台页面
|
||||
|
||||
**输入**: Layui组件规范
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/view/chat/index.html`
|
||||
2. 布局:
|
||||
- 左侧: 会话列表(待接入/进行中)
|
||||
- 中间: 聊天窗口
|
||||
- 右侧: 用户信息面板
|
||||
3. WebSocket连接Admin后台
|
||||
|
||||
**验收标准**:
|
||||
- [x] Layui风格一致
|
||||
- [x] 实时消息收发
|
||||
- [x] 新消息声音提示
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.4: 创建快捷回复管理页面
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/view/chat_quick_reply/index.html`
|
||||
2. 创建 `Pro/application/admin/view/chat_quick_reply/add.html`
|
||||
3. 表格展示: 分类、标题、内容、排序、状态
|
||||
|
||||
**验收标准**:
|
||||
- [x] 列表分页正确
|
||||
- [x] 添加/编辑表单验证
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.5: 创建聊天记录查询页面
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/view/chat/record.html`
|
||||
2. 筛选条件: 用户名、客服、时间范围
|
||||
3. 支持CSV导出
|
||||
|
||||
**验收标准**:
|
||||
- [x] 搜索功能正确
|
||||
- [x] 导出格式正确
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.6: 添加后台菜单入口
|
||||
|
||||
**执行步骤**:
|
||||
1. 在数据库 `cg_auth_rule` 表插入菜单
|
||||
2. 分配权限给客服角色
|
||||
|
||||
**验收标准**:
|
||||
- [x] 菜单显示正确
|
||||
- [x] 权限控制正确
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 前端改造
|
||||
|
||||
### TASK-3.1: PC端聊天组件开发
|
||||
|
||||
**输入**: Element UI组件规范
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `PC/src/components/chat/chat.vue`
|
||||
|
||||
**验收标准**:
|
||||
- [x] WebSocket连接稳定
|
||||
- [x] 消息实时收发
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.2: PC端客服入口改造
|
||||
|
||||
**执行步骤**:
|
||||
1. 修改客服入口,改为打开ChatWindow组件
|
||||
|
||||
**验收标准**:
|
||||
- [x] 点击客服按钮打开聊天窗口
|
||||
- [x] 不再跳转外链
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.3: Game端聊天组件开发
|
||||
|
||||
**状态**: 跳过 (用户确认不需要集成)
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.4: Game端客服入口添加
|
||||
|
||||
**状态**: 跳过 (用户确认不需要集成)
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.5: Portal端聊天页面改造
|
||||
|
||||
**输入**: uView组件规范
|
||||
|
||||
**执行步骤**:
|
||||
1. 修改 `Portal/pages/user/customservice.vue`
|
||||
2. 移除Telegram/Line外链
|
||||
3. 改为内嵌聊天功能
|
||||
|
||||
**验收标准**:
|
||||
- [x] uView风格
|
||||
- [x] uni-app兼容(H5/小程序)
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.6: 前端图片上传接口对接
|
||||
|
||||
**执行步骤**:
|
||||
1. 各端创建图片上传方法
|
||||
2. 调用 `/api/chat/upload` 接口
|
||||
3. 限制: 2MB, jpg/png/gif
|
||||
|
||||
**验收标准**:
|
||||
- [x] 上传前校验大小
|
||||
- [x] 上传后返回URL
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 集成测试
|
||||
|
||||
### TASK-4.1: PBT属性测试
|
||||
|
||||
**状态**: 跳过 (手动测试验证)
|
||||
|
||||
---
|
||||
|
||||
### TASK-4.2: 端到端测试
|
||||
|
||||
**执行步骤**:
|
||||
1. 用户发起咨询 → 客服接入 → 消息往返 → 结束会话 → 评价
|
||||
2. 离线留言 → 客服上线 → 按余额顺序处理
|
||||
3. 断线重连 → 补发未读消息
|
||||
|
||||
**验收标准**:
|
||||
- [x] 完整流程通过
|
||||
- [x] 消息延迟<500ms
|
||||
|
||||
---
|
||||
|
||||
## 实施统计
|
||||
|
||||
### 代码量统计
|
||||
|
||||
| 模块 | 文件数 | 代码行数 |
|
||||
|------|--------|----------|
|
||||
| Pro 控制器 | 2 | 692 |
|
||||
| Pro 视图 | 5 | 1,600 |
|
||||
| Socket Listener | 14 | 1,381 |
|
||||
| Socket Services | 3 | 986 |
|
||||
| Socket Models | 4 | 236 |
|
||||
| Portal 前端 | 1 | 518 |
|
||||
| PC 前端 | 1 | 122 |
|
||||
| **总计** | **30** | **5,535** |
|
||||
|
||||
### 完成情况
|
||||
|
||||
| Phase | 任务数 | 完成 | 跳过 |
|
||||
|-------|--------|------|------|
|
||||
| Phase 1: 基础架构 | 10 | 10 | 0 |
|
||||
| Phase 2: Admin后台 | 6 | 6 | 0 |
|
||||
| Phase 3: 前端改造 | 6 | 4 | 2 (Game端) |
|
||||
| Phase 4: 集成测试 | 2 | 1 | 1 (PBT测试) |
|
||||
| **总计** | **24** | **21** | **3** |
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 2.0*
|
||||
*最后更新: 2026-01-29*
|
||||
*归档日期: 2026-01-29*
|
||||
Reference in New Issue
Block a user