Pro/openspec/archive/customer-service-module/specs.md

249 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 ZSETscore=用户余额(负数实现降序) |
### 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*