很多人第一次接触客服系统开发时,都会产生一个致命的误解——这不就是个聊天功能吗?三年前我们团队也是这么想的,直到真正开始构建TWT Chat这个商业级SaaS客服系统时,才发现这个认知有多天真。一个完整的客服系统本质上是一个复杂的实时业务协作平台,聊天界面只是冰山露出水面的那10%。
客服系统的核心挑战不在于实现消息收发,而在于处理以下几个关键业务场景:
我们最初用WebSocket+MySQL的经典IM架构快速实现了原型,但在第一个企业客户接入时就遭遇了灾难性的性能问题。这迫使我们重新思考整个系统架构。
从v1到v3,我们的架构经历了三次重大重构:
code复制v1架构(简单IM模式):
前端 → WebSocket → 消息服务 → MySQL
v2架构(引入状态管理):
接入层 → 网关集群 →
├─ 状态服务
├─ 消息服务
└─ 业务服务 → 分库MySQL
v3架构(完整SaaS方案):
全局负载 → 租户路由 →
├─ 连接网关(长连接管理)
├─ 状态引擎(分布式状态机)
├─ 消息总线(可靠投递)
└─ 业务微服务 →
├─ 热数据:Redis+分片MySQL
└─ 冷数据:Elasticsearch+对象存储
最让我们意外的是"在线状态"这个看似简单的功能。初期我们用布尔值isOnline字段判断坐席状态,结果发现:
最终我们设计了三维状态模型:
typescript复制interface AgentState {
// 网络连接状态
connection: 'connected' | 'disconnected' | 'reconnecting';
// 用户活跃状态
activity: 'active' | 'idle' | 'away';
// 业务可用状态
capacity: {
maxSessions: number;
currentSessions: number;
manualStatus: 'available' | 'busy' | 'offline';
}
}
实现要点:
客服场景对消息丢失是零容忍的。我们设计的投递保障机制包括:
写入阶段:
同步阶段:
补偿机制:
关键经验:不要依赖TCP的可靠性,要在应用层实现完整的ACK机制。我们甚至为重要消息实现了"三次握手"流程(发送→接收→阅读确认)。
初期我们简单地在每个SQL查询加上tenant_id条件,很快就遇到性能问题。现在的多租户实现包含多个层次:
数据层:
代码层:
运维层:
当同时在线用户突破1万时,系统开始出现明显的卡顿。通过性能分析发现几个关键瓶颈:
慢查询TOP3:
优化方案:
sql复制-- 原查询(耗时1200ms+)
SELECT * FROM sessions
WHERE agent_id=? AND status='pending'
ORDER BY created_at DESC;
-- 优化后(添加复合索引)
CREATE INDEX idx_agent_status ON sessions(agent_id, status, created_at);
-- 未读数改用预聚合
UPDATE user_stats SET unread_count=unread_count+1
WHERE user_id=? AND tenant_id=?;
其他关键优化:
客服工作台需要实时展示数十种状态变化,我们最初用Redux管理状态,很快陷入"状态地狱"。重构后的方案:
状态管理架构:
code复制[WebSocket] → [消息中间件] → [状态机] → [UI组件]
↗ ↖
[乐观更新] [冲突解决]
关键技术点:
示例代码(冲突解决):
javascript复制function mergeStates(serverState, localState) {
// 时间戳优先
if(serverState.updatedAt > localState.updatedAt) {
return {...serverState, unread: localState.unread};
}
// 保留本地未读计数
return {
...localState,
someField: serverState.someField
};
}
客服系统面临独特的安全挑战:
我们的防御措施包括:
接入层:
认证授权:
内容安全:
面向欧洲客户需要特别注意:
技术实现:
java复制// 自动清理过期数据
@Scheduled(cron = "0 0 3 * * ?")
public void purgeExpiredData() {
// GDPR要求最多保留6个月
LocalDate cutoff = LocalDate.now().minusMonths(6);
sessionRepo.deleteByCreatedAtBefore(cutoff);
// 匿名化处理而不是物理删除
messageRepo.anonymizeExpiredMessages(cutoff);
}
为了避免为每个客户定制开发,我们建立了三级配置体系:
技术实现:
yaml复制# 配置定义示例
autoreply:
enabled: true
rules:
- trigger: "no_reply_5m"
template: "您好,请问还在吗?"
conditions:
- "!is_working_hours"
channels: ["web","mobile"]
核心系统通过插件机制支持扩展:
示例插件接口:
typescript复制interface SessionAssignPlugin {
name: string;
priority: number;
match(session: Session, agents: Agent[]): boolean;
assign(session: Session, agents: Agent[]): AssignmentResult;
}
主要成本来自:
优化措施:
通过自动化降低人力成本:
教训:早期没有严格定义领域边界,导致频繁的接口变更。现在采用:
建立完整的文档体系:
经过生产验证的技术栈:
我们踩过的坑:
必须监控的核心指标:
我们建立的改进机制:
经过三年迭代,TWT Chat目前支撑着日均百万级会话量。对于考虑自研客服系统的团队,我的建议是:
最后想说的是,自研客服系统确实能获得完全的掌控权,但也要对其中隐藏的复杂度有充分预期。如果您的核心业务不是即时通讯,不妨考虑基于成熟方案进行二次开发,这可能比从零开始更经济高效。