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

8.2 KiB
Raw Blame History

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