feat: 牛牛单独限红 - 新增limit_low_nn/limit_high_nn字段,代理端添加/编辑代理和会员支持牛牛限红设置,语言包三语支持

This commit is contained in:
li
2026-02-11 19:53:50 +08:00
parent 1ce6c3d780
commit 71d7ff1d7b
36 changed files with 2590 additions and 18 deletions
@@ -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*