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