### USER 你是一位资深的产品经理,对基于FreeSWITCH的呼叫中心系统有着丰富的产品经验。我现在想做一个企业的呼叫中心系统,除了可以实现基本的呼入呼出外,还要实现自动外呼和机器人客服。你可以向我问一些启发式的问题澄清需求,最终生成一份需求文档。 ### ASSISTANT 您好!很高兴能帮助您规划这个呼叫中心系统。为了生成一份完整且符合您实际业务场景的需求文档,我需要通过一系列问题来深入了解您的需求。 让我们从最基础的业务背景开始: --- ## 第一轮:业务背景与目标 **1. 关于您的企业和目标客户:** - 这个呼叫中心系统是给**您自己企业内部使用**,还是作为**SaaS产品对外销售**给其他企业? - 您的企业/目标客户属于哪个行业?(如:电商、金融、教育、医疗、政务热线等) **2. 关于规模预期:** - 初期预计需要支持多少个**坐席(人工客服)**同时工作? - 预计的**日呼叫量**大概是多少?(呼入+呼出) **3. 关于核心业务痛点:** - 您目前为什么想要建设这个系统?是**从零开始建设**,还是**替换现有系统**? - 如果是替换,现有系统最让您不满意的地方是什么? --- 请先回答这几个基础问题,我会根据您的回答继续深入探讨呼入呼出、自动外呼、机器人客服等具体模块的细节需求。🎯 ### USER 1.企业内部使用,企业属于服务行业,2.预计支持 150个坐席同时工作,日呼量50000 ,3.从零开始建设 ### ASSISTANT 感谢您的回答!150坐席、日呼叫量5万是一个中大型呼叫中心的规模,服务行业的场景也非常典型。 --- ## 第二轮:业务场景细化 **4. 关于服务业务类型:** - 您的服务行业具体是哪类?(如:家政服务、汽车4S店/维修、物业管理、旅游预订、餐饮连锁、上门维修等) - 您的客户群体主要是**ToC(个人消费者)**还是**ToB(企业客户)**? **5. 关于呼入呼出业务占比:** - 目前预估的**呼入和呼出比例**大概是多少?(如:呼入70%呼出30%) - 呼入的主要场景是什么?(如:咨询、投诉、预约、售后等) - 呼出的主要场景是什么?(如:回访、营销推广、催收/催缴、通知提醒等) **6. 关于自动外呼的核心诉求:** - 您希望自动外呼解决什么问题? - [ ] 提高人工坐席效率(预测式外呼,减少等待) - [ ] 完全无人化批量通知(如:短信+语音通知) - [ ] 营销筛选(机器人先打,意向客户转人工) - [ ] 其他:______ **7. 关于机器人客服的定位:** - 您希望机器人客服承担什么角色? - **呼入场景**:IVR导航、自助查询、常见问题解答、转人工前预处理? - **呼出场景**:自动回访、满意度调查、标准化通知? - 机器人是作为**第一道接待**(先机器人再转人工),还是**人工为主机器人辅助**? --- 请您根据实际业务情况回答,这些信息将帮助我明确系统的功能优先级和架构设计方向。💡 ### USER 服务行业类型:道路救援、家政服务,代驾服务,客户群体:ToC,呼入40%呼出60%,呼入的场景:叫服务,咨询、投诉,呼出场景:回访、营销推广、催收/催缴、通知提醒等,关于自动外呼的核心诉求:提高人工坐席效率和完全无人化批量通知;机器人客服的定位:**呼入场景**:IVR导航、常见问题解答、转人工前预处理,呼出场景:标准化通知、自动回访、满意度调查。先机器人再转人工 ### ASSISTANT 非常清晰!您的业务场景很典型——**道路救援、家政、代驾**都是对**响应速度要求极高**的服务行业,而且ToC场景意味着客户体验至关重要。 --- ## 第三轮:关键业务流程与系统集成 **8. 关于呼入核心流程(叫服务场景):** - 当客户来电叫服务时,坐席需要**实时派单**给服务人员(救援师傅、家政阿姨、代驾司机)吗? - 您是否已有**业务系统/派单系统/工单系统**需要与呼叫中心对接? - 如果有,是什么系统?(自研、第三方SaaS如:易企秀、工单宝等) - 如果没有,是否需要呼叫中心系统**内置工单模块**? **9. 关于客户来电识别:** - 客户来电时,您希望系统能自动**识别客户身份**吗? - [ ] 通过来电号码匹配历史客户资料 - [ ] 弹出历史服务记录、订单记录 - [ ] 显示客户标签(VIP、黑名单、投诉倾向等) - 这意味着系统需要对接您的**CRM或会员系统**,您目前有这类系统吗? **10. 关于服务工作时间:** - 您的呼叫中心是**7×24小时**运营,还是**固定工作时间**? - 道路救援类业务通常是24小时的,非工作时间来电如何处理? - [ ] 全部由机器人接待+记录,次日回拨 - [ ] 安排夜班值班坐席 - [ ] 紧急情况直接转值班手机 **11. 关于多业务线管理:** - 道路救援、家政服务、代驾服务是**共用一个号码**,通过IVR分流?还是**不同业务独立号码**? - 坐席是**混合技能组**(一个坐席可接多种业务),还是**专属技能组**(救援组、家政组、代驾组分开)? --- ## 第四轮:自动外呼深入 **12. 关于预测式外呼(提高人工效率):** - 外呼营销时,是否需要**预测式拨号**(系统自动批量拨打,接通后才转坐席)? - 对于**接通率和坐席利用率**有什么期望指标吗? **13. 关于无人化批量通知:** - 典型的通知场景有哪些? - [ ] 服务人员即将到达通知 - [ ] 订单确认/取消通知 - [ ] 账单催缴提醒 - [ ] 营销活动通知 - 通知需要**纯语音播报**,还是需要**交互确认**(如:按1确认,按2取消)? - 通知失败后是否需要**自动重试**或**转短信**? **14. 关于外呼数据来源:** - 外呼名单从哪里来? - [ ] 从业务系统自动推送 - [ ] 人工Excel导入 - [ ] API实时触发(如:订单完成自动触发回访) --- 请您挑重点回答,我们离完整需求越来越近了!🚀 ### USER 已经有了自己的CRM系统,需要呼叫中心系统集成CRM系统,客户来电叫服务时,识别客户身份,CRM系统弹屏,坐席在CRM系统上创建工单。呼叫中心是**7×24小时**运营,每个时间段都有坐席人员。不同业务独立号码,坐席**专属技能组**(救援组、家政组、代驾组分开)。外呼营销时,需要**预测式拨号,**对于**接通率和坐席利用率要能够统计。**典型的通知场景有:服务人员已经派出通知、服务人员即将到达通知,账单催缴提醒,通知需要**纯语音播报。**外呼名单从业务系统来。注意:坐席的操作方式是,是操作CRM系统或者业务系统,通过API的形式调用呼叫中心系统的呼入呼出功能。呼叫中心系统不需要有短信功能,由第三方的短信平台提供短信服务。 ### ASSISTANT 非常关键的信息!您的架构思路非常清晰——**呼叫中心作为通信能力中台**,通过API为CRM/业务系统提供呼叫能力,这是目前主流且灵活的架构模式。 --- ## 第五轮:技术架构与接口规范 **15. 关于CRM/业务系统集成方式:** - 您的CRM系统是**Web端(B/S架构)**还是**桌面客户端(C/S架构)**? - 坐席使用的话机形态是什么? - [ ] **软电话(网页/PC软件接听)**——WebRTC或SIP软电话 - [ ] **IP话机(桌面硬件话机)** - [ ] **手机/手机App** - 如果是软电话,您倾向于: - [ ] 呼叫中心提供**独立软电话条**(小组件嵌入CRM) - [ ] CRM系统**自行开发通话UI**,只调用呼叫中心API **16. 关于API能力需求确认:** 请确认以下API能力是否需要: | 功能分类 | 具体能力 | 是否需要 | |---------|---------|---------| | **呼入控制** | 来电弹屏通知(推送来电信息到CRM) | ? | | | 接听/挂断/保持/取回 | ? | | | 转接(转坐席/转技能组/转外线) | ? | | | 三方通话/会议 | ? | | **呼出控制** | 点击外呼(坐席点击号码拨打) | ? | | | 预览式外呼(显示客户信息后手动拨打) | ? | | | 预测式外呼(系统自动拨打,接通转坐席) | ? | | **坐席状态** | 签入/签出/置忙/置闲/小休 | ? | | | 实时坐席状态查询 | ? | | **通话事件** | 通话状态回调(振铃、接通、挂断等) | ? | | | 通话结束后话单推送 | ? | | **录音** | 录音文件获取/下载 | ? | | | 实时录音流(用于实时质检/转写) | ? | **17. 关于来电分配策略(ACD):** - 同一技能组内多个坐席,来电如何分配? - [ ] 轮询(Round Robin) - [ ] 最长空闲优先 - [ ] 技能值优先(技能等级高的优先) - [ ] 历史服务优先(上次是谁服务的优先给谁) - 如果技能组全忙,是否需要**排队等待**功能? - [ ] 播放等待音乐/提示前面排队人数 - [ ] 等待超时转其他组/留言/回拨 --- ## 第六轮:机器人客服细化 **18. 关于呼入机器人(IVR+智能导航):** - IVR导航的复杂度如何? - [ ] **简单按键导航**:按1道路救援,按2家政服务,按3代驾... - [ ] **语音识别导航**:客户直接说"我要叫代驾"自动识别转接 - [ ] **多轮对话**:机器人先收集信息(位置、服务类型)再转人工 - 常见问题解答需要支持多少个问题?(10个以内 / 10-50个 / 50个以上) - 机器人**转人工的触发条件**是什么? - [ ] 客户说"转人工" - [ ] 客户按键选择 - [ ] 机器人识别不了时自动转 - [ ] 特定问题类型必须转人工(如投诉) **19. 关于呼出机器人:** - 回访/满意度调查的典型流程是什么? - 例如:播放"您好,请问您对本次服务满意吗?满意请按1,不满意请按2" - 是否需要**采集客户按键结果**并回传业务系统? - 如果客户不按键/无人接听,如何处理? **20. 关于机器人技术选型:** - 您是否有**指定的ASR/TTS供应商**?(如:阿里云、科大讯飞、百度等) - 还是希望呼叫中心系统**内置语音能力**? - 如果涉及智能对话(NLU),是否有指定的对话平台?还是需要系统提供? --- ## 第七轮:管理与运营功能 **21. 关于监控与管理:** - 是否需要**实时监控大屏**?(显示:当前排队数、各组坐席状态、接通率等) - 班长/主管需要哪些监控能力? - [ ] 实时监听坐席通话 - [ ] 强插(加入通话协助) - [ ] 强拆(强制结束通话) - [ ] 密语(对坐席耳语指导,客户听不到) **22. 关于统计报表:** - 需要哪些维度的报表? - [ ] 坐席维度:接通量、通话时长、工作状态时长占比 - [ ] 技能组维度:呼入量、接通率、排队放弃率 - [ ] 时段维度:每小时/每日话务量趋势 - [ ] 外呼任务维度:拨打量、接通率、意向客户数 - 报表是在**呼叫中心系统查看**,还是**数据推送到您的BI系统**? **23. 关于录音与质检:** - 所有通话都需要**全程录音**吗? - 是否需要**质检功能**? - [ ] 人工质检(抽检打分) - [ ] 智能质检(语音转文字+关键词检测+情绪分析) --- 请您对以上问题进行确认和补充,尤其是**第16题API能力表格**,这对系统设计至关重要!📋 ### USER 整套系统从零建设的话,大概需要多少预算?硬件和软件分别需要什么? ### ASSISTANT 非常好的问题!在进入详细预算前,我需要先确认几个影响成本的关键因素: --- ## 第八轮:预算与部署相关问题 **24. 关于部署方式:** - 您倾向于哪种部署方式? - [ ] **私有化部署**(服务器放在自己机房或自有云主机) - [ ] **公有云部署**(阿里云、腾讯云、华为云等) - [ ] **混合部署**(核心私有化,部分能力用云服务) **25. 关于线路资源:** - 您目前是否已有**运营商线路资源**? - [ ] 已有中继线路(E1/SIP中继) - [ ] 已有95/96/400/1010号码 - [ ] 没有,需要新申请 - 三种业务(道路救援、家政、代驾)计划用几个号码? **26. 关于并发需求:** - 150坐席同时工作,预估**最大并发通话数**是多少? - 通常按坐席数的 **70%-80%** 估算,即 **100-120路并发** - 您认为这个估算合理吗? **27. 关于建设方式:** - 您倾向于哪种方式? - [ ] **自研团队开发**(有FreeSWITCH开发能力) - [ ] **采购成熟产品**(购买商业呼叫中心系统) - [ ] **外包定制开发**(找供应商定制) --- ## 预算估算框架(先给您一个概览) 在您回答上述问题之前,我先给您一个**参考框架**,具体数字会根据您的回答调整: ### 一、硬件/基础设施预算 | 项目 | 私有化部署 | 云部署 | |------|-----------|--------| | **服务器** | 10-30万(3-5台服务器集群) | 云主机 3-8万/年 | | **存储(录音)** | 5-15万(NAS/SAN存储) | 对象存储 1-3万/年 | | **网络设备** | 2-5万(交换机、防火墙) | 含在云服务中 | | **语音网关** | 3-10万(若用传统E1线路) | SIP中继无需网关 | | **IP话机(可选)** | 300-800元/台 × 150 = 4.5-12万 | 同左 | | **机房/带宽** | 5-10万/年 | 含在云服务中 | | **小计** | **25-80万(一次性)+ 5-10万/年** | **5-15万/年** | ### 二、软件/系统预算 | 项目 | 采购商业产品 | 自研/定制开发 | |------|------------|--------------| | **呼叫中心平台** | 30-80万(按坐席授权) | 开发成本 50-150万 | | **智能IVR/机器人** | 10-30万 + 调用费用 | 需对接ASR/TTS厂商 | | **预测式外呼模块** | 10-20万 | 含在开发中 | | **录音/质检系统** | 5-15万 | 含在开发中 | | **API/集成开发** | 5-15万(定制费用) | 含在开发中 | | **小计** | **60-160万** | **50-150万 + 团队成本** | ### 三、语音资源/运营费用 | 项目 | 预估费用 | |------|---------| | **号码月租** | 95/96号码:5000-2万/月;400号码:1000-5000/月 | | **通话费** | 呼入:0.02-0.05元/分钟;呼出:0.08-0.15元/分钟 | | **ASR/TTS调用** | 约 0.01-0.03元/秒(按使用量) | | **线路并发费** | SIP中继:约 50-100元/路/月 | **按日呼量5万、平均时长3分钟估算:** - 月通话分钟数:5万 × 30天 × 3分钟 = **450万分钟/月** - 呼入成本(40%):180万分钟 × 0.03 = **5.4万/月** - 呼出成本(60%):270万分钟 × 0.10 = **27万/月** - **月通信成本约:30-35万/月** --- ### 四、总体预算汇总(参考) | 部署方式 | 首年投入 | 后续年度 | |---------|---------|---------| | **云部署 + 采购商业产品** | 70-180万 | 40-60万/年(含云资源+维保) | | **私有化 + 采购商业产品** | 100-250万 | 20-40万/年(维保+资源) | | **云部署 + 自研开发** | 80-200万 | 30-50万/年 + 研发团队 | > ⚠️ 以上**不含通信费用**(每年约 **360-420万**,这是大头) --- ## 追问几个成本优化相关问题: **28. 关于话机/通话终端:** - 坐席是否可以接受**纯软电话**(用耳麦+电脑接听)?可节省硬件话机成本 - 还是必须配备**IP硬话机**? **29. 关于录音存储:** - 录音需要保存多长时间?(3个月 / 6个月 / 1年 / 更长) - 日均5万通话 × 3分钟 ≈ 15万分钟/天,月存储需求约 **1-2TB** **30. 关于智能语音:** - 机器人对话的ASR/TTS,您倾向于: - [ ] 用云厂商API(按量付费,无需部署) - [ ] 私有化部署(一次性授权,成本较高但长期划算) --- 请您回答 **第24-30题**,我将结合您之前的回答,给出更精准的预算方案和硬件软件清单!💰 ### USER 关于CRM/业务系统集成方式:CRM系统是 **Web端(B/S架构),**坐席使用的话机形态是:IP话机,但是要支持软电话功能,软电话使用呼叫中心提供**独立软电话条**(小组件嵌入CRM),列出的API能力都需要,来电分配策略:可以支持多种分配策略,首先历史服务优先,最长空闲优先和轮训。如果技能组全忙,可以设置溢出到其他技能组。IVR导航要支持语音识别导航,常见问题解答需要支持50个以上。机器人**转人工的触发条件**是:客户说"转人工"、机器人识别不了时自动转、特定问题类型必须转人工(如投诉);机器人技术选型:指定的ASR/TTS供应商阿里云,如果涉及智能对话(NLU)由指定的对话平台。需要**实时监控大屏,**班长/主管需要监控能力:实时监听坐席通话、强插(加入通话协助)、强拆(强制结束通话);所有通话都要全程录音;统计报表和质检本次先不涉及。 ### ASSISTANT 非常详细!信息已经非常完整了。让我再确认最后几个关键问题,然后就可以为您生成完整的需求文档了。 --- ## 第九轮:最终确认 **31. 关于部署方式(回顾第24题):** - 您倾向于 **私有化部署** 还是 **公有云部署**? **32. 关于线路资源(回顾第25题):** - 是否已有运营商线路?还是需要新申请? - 三种业务(道路救援、家政、代驾)分别用独立号码,计划用什么类型的号码? - [ ] 95/96短号(品牌形象好,申请门槛高) - [ ] 400号码(全国统一,成本适中) - [ ] 普通固话/手机号(成本低) **33. 关于建设方式(回顾第27题):** - 您倾向于: - [ ] **采购成熟商业产品** - [ ] **自有团队基于FreeSWITCH自研** - [ ] **找供应商定制开发** **34. 关于录音存储(回顾第29题):** - 录音需要保存多长时间? **35. 关于外呼机器人补充:** - 您之前提到呼出机器人用于「标准化通知、自动回访、满意度调查」 - 请确认回访/满意度调查是否需要**采集客户按键反馈**并回传业务系统? - 例如:「满意请按1,不满意请按2」-> 结果写回CRM **36. 关于系统可用性要求:** - 道路救援是紧急服务,对系统稳定性要求高 - 是否需要**主备高可用部署**?(即一台故障,自动切换到备机) **37. 关于上线时间预期:** - 您期望系统在多长时间内上线? - [ ] 1-2个月(紧急) - [ ] 3-4个月(正常) - [ ] 6个月以上(充裕) --- 请您回答以上问题后,我将为您生成一份完整的 **《呼叫中心系统需求规格说明书》**,包含: 1. **项目概述**(背景、目标、范围) 2. **业务需求**(呼入、呼出、机器人场景) 3. **功能需求**(分模块详细描述) 4. **接口需求**(API清单) 5. **非功能需求**(性能、安全、可用性) 6. **系统架构建议** 7. **硬件软件清单** 8. **预算估算** 9. **项目里程碑建议** 期待您的最后回复!📝 ### USER 私有化部署,计划使用400号码和普通固话,建设方式:自有团队基于FreeSWITCH自研,录音需要长期保存,回访/满意度调查是需要**采集客户按键反馈**并回传业务系统;道路救援是紧急服务,对系统稳定性要求高且需要**主备高可用部署;**期望系统3-4个月上线。 ### ASSISTANT 感谢您的耐心回答!所有关键信息已收集完毕。现在我为您生成完整的需求规格说明书。 --- # 企业呼叫中心系统需求规格说明书 **文档版本**:V1.0 **编制日期**:2024年1月 **文档状态**:初稿 --- ## 第一章 项目概述 ### 1.1 项目背景 本企业主营道路救援、家政服务、代驾服务三大业务板块,面向个人消费者(ToC)提供服务。随着业务规模扩大,现需建设一套专业的呼叫中心系统,以提升客户服务体验、提高服务响应效率、降低人工成本。 ### 1.2 项目目标 | 目标维度 | 具体目标 | |---------|---------| | **服务能力** | 支持150坐席同时在线,日处理呼叫量50,000通 | | **响应效率** | 道路救援等紧急服务实现快速响应,7×24小时不间断服务 | | **人工效率** | 通过预测式外呼提升坐席利用率,减少空闲等待时间 | | **自动化水平** | 通过智能IVR和机器人实现常见问题自助解答、批量通知自动化 | | **系统稳定性** | 主备高可用架构,确保核心业务(道路救援)不中断 | ### 1.3 项目范围 **纳入范围:** - 基础呼叫能力(呼入/呼出) - 智能IVR与语音导航 - 呼入/呼出机器人 - 预测式外呼 - 无人化批量通知 - CRM系统集成(API对接) - 软电话条组件 - 实时监控大屏 - 班长监控功能 - 全程录音 - 主备高可用部署 **不纳入本期范围:** - 统计报表系统(二期规划) - 智能质检系统(二期规划) - 短信功能(由第三方平台提供) ### 1.4 术语定义 | 术语 | 定义 | |------|------| | **ACD** | 自动呼叫分配(Automatic Call Distribution) | | **IVR** | 交互式语音应答(Interactive Voice Response) | | **ASR** | 自动语音识别(Automatic Speech Recognition) | | **TTS** | 语音合成(Text To Speech) | | **NLU** | 自然语言理解(Natural Language Understanding) | | **CTI** | 计算机电话集成(Computer Telephony Integration) | | **SIP** | 会话发起协议(Session Initiation Protocol) | --- ## 第二章 业务需求 ### 2.1 业务架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 客户触点 │ │ 道路救援热线 家政服务热线 代驾服务热线 │ │ (400-XXX-XXXX) (400-XXX-XXXX) (固话/400) │ └───────────────┬─────────────────┬─────────────────┬─────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 呼叫中心系统 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 智能IVR │ │ 呼入机器人│ │ ACD分配 │ │ 外呼系统 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 坐席管理 │ │ 录音系统 │ │ 实时监控 │ │ API网关 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └───────────────────────────┬─────────────────────────────────────┘ │ API ▼ ┌─────────────────────────────────────────────────────────────────┐ │ CRM/业务系统 │ │ 客户管理 │ 工单管理 │ 派单系统 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.2 呼入业务场景 #### 2.2.1 道路救援来电场景 **业务流程:** ``` 客户来电 → 智能IVR(语音识别:"道路救援") → 机器人收集初步信息(可选) → 转救援技能组 → ACD分配(历史服务优先 > 最长空闲 > 轮询) → 技能组全忙时溢出到备用组 → 坐席接听 + CRM弹屏 → 坐席在CRM创建工单/派单 → 通话结束 → 录音保存 ``` **业务要求:** - 响应时效要求高,需优先保障接通率 - 支持7×24小时服务 - 来电需快速识别客户身份,展示历史服务记录 #### 2.2.2 家政服务来电场景 **业务流程:** ``` 客户来电 → 智能IVR(按键或语音选择"家政服务") → 常见问题解答(服务项目、价格咨询等) → 客户需要人工服务 → 转家政技能组 → 坐席接听 + CRM弹屏 → 预约服务/创建工单 → 通话结束 → 录音保存 ``` #### 2.2.3 代驾服务来电场景 **业务流程:** ``` 客户来电 → 智能IVR(语音识别:"叫代驾") → 转代驾技能组 → 坐席接听 + CRM弹屏 → 创建代驾订单/派单 → 通话结束 → 录音保存 ``` #### 2.2.4 投诉来电场景 **业务流程:** ``` 客户来电 → 智能IVR(识别"投诉"意图) → 直接转人工(投诉不经机器人处理) → 分配至对应业务技能组或投诉专席 → CRM弹屏 + 标记投诉标签 → 坐席记录投诉内容 ``` ### 2.3 呼出业务场景 #### 2.3.1 预测式外呼(营销/回访) **业务流程:** ``` 业务系统推送外呼任务(客户名单) → 呼叫中心批量拨打 → 客户接听 → 自动转接空闲坐席 → CRM弹屏展示客户信息 → 坐席进行营销/回访 → 通话结束 → 录音保存 → 话单回传业务系统 ``` **业务要求:** - 系统自动拨打,接通后才转坐席,减少坐席等待时间 - 支持接通率、坐席利用率统计 - 外呼任务支持暂停、恢复、停止 #### 2.3.2 机器人自动外呼(通知类) **场景一:服务人员派出/即将到达通知** ``` 业务系统触发通知(API调用) → 呼叫中心自动拨打客户电话 → 播放TTS语音:"您好,您预约的道路救援服务,师傅已出发,预计10分钟到达..." → 通话结束 → 结果回传业务系统 ``` **场景二:账单催缴提醒** ``` 业务系统推送催缴名单 → 批量自动拨打 → 播放TTS语音:"您好,您有一笔待支付账单..." → 纯语音播报,无需交互 → 拨打结果回传(接通/未接通/关机等) ``` #### 2.3.3 机器人自动外呼(回访/满意度调查) **业务流程:** ``` 业务系统推送回访任务 → 呼叫中心自动拨打 → 播放TTS语音:"您好,请问您对本次服务满意吗?满意请按1,不满意请按2" → 采集客户按键 → 按键结果回传业务系统 → 不满意客户可选择转人工跟进 ``` ### 2.4 机器人客服场景 #### 2.4.1 呼入机器人功能定位 | 功能 | 说明 | |------|------| | **语音导航** | 支持客户语音说出需求,自动识别并转接对应技能组 | | **常见问题解答** | 支持50个以上FAQ,自动语音解答 | | **转人工预处理** | 收集客户初步信息后转人工,提高坐席效率 | #### 2.4.2 转人工触发条件 | 触发条件 | 说明 | |---------|------| | 客户主动要求 | 客户说"转人工"或按键选择转人工 | | 识别失败 | 机器人连续2-3次无法识别客户意图 | | 特定问题类型 | 投诉类问题直接转人工,不经机器人处理 | --- ## 第三章 功能需求 ### 3.1 智能IVR模块 #### 3.1.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | IVR-001 | 多级语音菜单 | 支持配置多级IVR语音菜单,按键导航 | P0 | | IVR-002 | 语音识别导航 | 集成阿里云ASR,支持客户语音说出需求自动识别 | P0 | | IVR-003 | 多业务线路由 | 根据来电号码(400/固话)自动进入对应业务IVR流程 | P0 | | IVR-004 | 时间条件路由 | 支持按工作时间/节假日配置不同IVR流程 | P1 | | IVR-005 | 黑名单拦截 | 来电号码在黑名单中自动播放提示后挂断 | P1 | | IVR-006 | VIP客户识别 | VIP客户来电自动优先排队或转专属坐席 | P2 | | IVR-007 | IVR流程可视化配置 | 提供可视化拖拽界面配置IVR流程 | P1 | #### 3.1.2 语音识别要求 - **ASR供应商**:阿里云语音识别 - **识别准确率**:≥90% - **响应时间**:≤500ms - **支持方言**:普通话为主,可选支持粤语、四川话等 ### 3.2 ACD自动分配模块 #### 3.2.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | ACD-001 | 技能组管理 | 支持创建多个技能组(救援组、家政组、代驾组) | P0 | | ACD-002 | 坐席技能配置 | 坐席可属于一个或多个技能组,配置技能等级 | P0 | | ACD-003 | 历史服务优先 | 来电客户优先分配给上次服务的坐席 | P0 | | ACD-004 | 最长空闲优先 | 同技能组内优先分配给空闲时间最长的坐席 | P0 | | ACD-005 | 轮询分配 | 同技能组内按顺序轮流分配 | P0 | | ACD-006 | 分配策略优先级 | 支持配置分配策略优先级:历史服务 > 最长空闲 > 轮询 | P0 | | ACD-007 | 技能组溢出 | 技能组全忙时,来电溢出到配置的备用技能组 | P0 | | ACD-008 | 排队等待 | 所有坐席忙时,来电进入排队,播放等待音乐 | P0 | | ACD-009 | 排队超时处理 | 排队超时可配置:继续等待/溢出/留言/回拨 | P1 | | ACD-010 | 排队位置播报 | 向等待客户播报当前排队位置 | P2 | ### 3.3 坐席管理模块 #### 3.3.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | AGT-001 | 坐席签入/签出 | 坐席上下班签入签出系统 | P0 | | AGT-002 | 坐席状态管理 | 支持状态:空闲、通话中、振铃中、后处理、小休、忙碌 | P0 | | AGT-003 | 小休原因 | 小休时需选择原因(用餐、洗手间、培训等) | P1 | | AGT-004 | 状态自动切换 | 通话结束自动进入后处理状态,超时自动转空闲 | P0 | | AGT-005 | 后处理时长配置 | 可配置后处理时长(如30秒) | P1 | | AGT-006 | 强制签出 | 班长可强制签出坐席 | P1 | #### 3.3.2 坐席工作台要求 - 坐席不使用呼叫中心独立工作台 - 坐席在CRM系统中操作业务 - 呼叫中心提供**软电话条组件**嵌入CRM页面 - 软电话条通过WebSocket与呼叫中心通信 - 同时支持IP话机(SIP协议注册) ### 3.4 软电话条组件 #### 3.4.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | SFT-001 | 签入/签出 | 坐席登录CRM后通过软电话条签入呼叫中心 | P0 | | SFT-002 | 状态切换 | 置忙/置闲/小休状态切换按钮 | P0 | | SFT-003 | 来电弹屏触发 | 来电时向CRM发送弹屏事件,携带来电号码、客户ID等 | P0 | | SFT-004 | 接听/挂断 | 点击接听/挂断按钮控制通话 | P0 | | SFT-005 | 保持/取回 | 通话保持(播放等待音乐)和取回 | P0 | | SFT-006 | 静音 | 坐席侧静音 | P1 | | SFT-007 | 转接 | 转接至其他坐席/技能组/外线号码 | P0 | | SFT-008 | 咨询转 | 先与第三方通话咨询,再决定是否转接 | P1 | | SFT-009 | 三方通话 | 坐席、客户、第三方三方通话 | P1 | | SFT-010 | 点击拨号 | 在CRM中点击号码呼出 | P0 | | SFT-011 | 手动拨号 | 手动输入号码拨打 | P0 | | SFT-012 | 通话计时 | 显示当前通话时长 | P0 | | SFT-013 | 来电信息展示 | 显示来电号码、归属地、技能组等 | P0 | #### 3.4.2 软电话条技术要求 | 要求项 | 说明 | |-------|------| | **前端技术** | Web组件,支持iframe嵌入或JS SDK集成 | | **通信协议** | WebSocket长连接,实时接收呼叫事件 | | **语音方案** | WebRTC(浏览器软电话)或SIP话机(硬件话机) | | **浏览器支持** | Chrome 70+、Firefox 65+、Edge 79+ | ### 3.5 呼入通话控制 #### 3.5.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | IBC-001 | 来电接入 | 支持SIP中继接入来电 | P0 | | IBC-002 | 来电排队 | 坐席全忙时来电进入队列等待 | P0 | | IBC-003 | 等待音乐 | 排队等待时播放等待音乐 | P0 | | IBC-004 | 来电转接 | 支持盲转、咨询转、转技能组、转外线 | P0 | | IBC-005 | 三方通话 | 支持三方会议通话 | P1 | | IBC-006 | 通话保持 | 通话保持并播放等待音乐 | P0 | | IBC-007 | 通话录音 | 全程录音,支持双声道(坐席/客户分轨)| P0 | ### 3.6 呼出通话控制 #### 3.6.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | OBC-001 | 点击外呼 | 坐席点击号码发起外呼 | P0 | | OBC-002 | 外显号码设置 | 支持配置外呼显示的主叫号码 | P0 | | OBC-003 | 外呼转接 | 外呼通话中支持转接 | P0 | | OBC-004 | 通话录音 | 外呼通话全程录音 | P0 | ### 3.7 预测式外呼模块 #### 3.7.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | PDL-001 | 外呼任务创建 | 通过API创建外呼任务,导入号码名单 | P0 | | PDL-002 | 任务状态控制 | 支持任务启动、暂停、恢复、停止 | P0 | | PDL-003 | 预测式拨号算法 | 根据坐席空闲情况自动计算拨打频率 | P0 | | PDL-004 | 接通转坐席 | 客户接听后自动转接空闲坐席 | P0 | | PDL-005 | 无人接听处理 | 无人接听/忙音/关机等自动标记状态 | P0 | | PDL-006 | 外呼时段限制 | 可配置允许外呼的时间段(如9:00-21:00) | P1 | | PDL-007 | 号码去重 | 同一任务内号码自动去重 | P1 | | PDL-008 | 重试策略 | 未接通号码可配置自动重试次数和间隔 | P1 | | PDL-009 | 坐席利用率统计 | 统计坐席有效通话时长占比 | P0 | | PDL-010 | 接通率统计 | 统计任务接通率 | P0 | ### 3.8 机器人外呼模块 #### 3.8.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | RBT-001 | 批量通知任务 | 通过API创建批量语音通知任务 | P0 | | RBT-002 | TTS语音播放 | 集成阿里云TTS,文本转语音播放 | P0 | | RBT-003 | 变量替换 | 支持语音内容中插入变量(姓名、时间、金额等) | P0 | | RBT-004 | 纯播报模式 | 播放语音后直接挂断 | P0 | | RBT-005 | 按键采集模式 | 播放语音后等待客户按键,采集结果 | P0 | | RBT-006 | 按键结果回传 | 按键结果通过回调接口推送至业务系统 | P0 | | RBT-007 | 无应答处理 | 客户无应答/超时可配置重试或标记 | P1 | | RBT-008 | 并发控制 | 可配置任务最大并发拨打数 | P1 | | RBT-009 | 任务进度查询 | API查询任务进度和结果 | P0 | ### 3.9 呼入机器人模块 #### 3.9.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | IRB-001 | 语音识别 | 集成阿里云ASR识别客户语音 | P0 | | IRB-002 | 意图识别 | 对接NLU平台,识别客户意图 | P0 | | IRB-003 | FAQ问答 | 支持配置50+常见问题及答案 | P0 | | IRB-004 | 语音合成应答 | 集成阿里云TTS合成语音应答 | P0 | | IRB-005 | 多轮对话 | 支持多轮对话收集信息 | P1 | | IRB-006 | 转人工-主动请求 | 客户说"转人工"触发转接 | P0 | | IRB-007 | 转人工-识别失败 | 连续N次识别失败自动转人工 | P0 | | IRB-008 | 转人工-特定意图 | 识别到投诉等意图直接转人工 | P0 | | IRB-009 | 转接携带信息 | 转人工时携带已收集的信息至坐席 | P1 | | IRB-010 | 话术配置 | 支持配置机器人话术和回复内容 | P0 | ### 3.10 录音管理模块 #### 3.10.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | REC-001 | 全程录音 | 所有呼入呼出通话全程录音 | P0 | | REC-002 | 双声道录音 | 坐席和客户分轨录音(便于质检) | P1 | | REC-003 | 录音存储 | 录音文件存储至NAS/分布式存储 | P0 | | REC-004 | 长期保存 | 支持录音长期保存,定期归档 | P0 | | REC-005 | 录音检索 | 按通话ID、时间、号码等检索录音 | P0 | | REC-006 | 录音下载 | 通过API获取录音文件下载地址 | P0 | | REC-007 | 录音权限 | 录音访问权限控制 | P1 | ### 3.11 班长监控模块 #### 3.11.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | SUP-001 | 坐席状态监控 | 实时查看所有坐席当前状态 | P0 | | SUP-002 | 实时监听 | 监听坐席与客户通话,双方不知情 | P0 | | SUP-003 | 强插通话 | 加入通话,三方均可听到 | P0 | | SUP-004 | 强拆通话 | 强制结束坐席通话 | P0 | | SUP-005 | 密语(可选) | 对坐席单向说话,客户听不到(二期) | P2 | | SUP-006 | 强制签出 | 强制将坐席签出系统 | P1 | | SUP-007 | 强制置忙 | 强制将坐席状态设为忙碌 | P1 | ### 3.12 实时监控大屏 #### 3.12.1 功能清单 | 功能编号 | 功能名称 | 功能描述 | 优先级 | |---------|---------|---------|--------| | DSP-001 | 整体概览 | 展示当前呼入量、呼出量、接通率等 | P0 | | DSP-002 | 坐席状态分布 | 展示各状态坐席数量(空闲、通话、小休等) | P0 | | DSP-003 | 技能组监控 | 各技能组排队数、等待时长、可用坐席数 | P0 | | DSP-004 | 实时告警 | 排队超阈值、坐席空闲过少等告警提示 | P1 | | DSP-005 | 趋势图表 | 当日话务量趋势图(按小时) | P1 | | DSP-006 | 大屏适配 | 支持大屏显示器展示 | P0 | --- ## 第四章 接口需求 ### 4.1 接口概述 呼叫中心系统作为通信能力中台,通过API方式为CRM/业务系统提供呼叫能力。接口采用RESTful API + WebSocket事件推送模式。 ### 4.2 接口清单 #### 4.2.1 呼入控制接口 | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-IN-001 | 接听来电 | POST | 坐席接听当前来电 | | API-IN-002 | 挂断通话 | POST | 挂断当前通话 | | API-IN-003 | 保持通话 | POST | 将通话置于保持状态 | | API-IN-004 | 取回通话 | POST | 取回保持的通话 | | API-IN-005 | 转接通话 | POST | 转接至指定坐席/技能组/外线 | | API-IN-006 | 咨询通话 | POST | 发起咨询通话 | | API-IN-007 | 咨询转移 | POST | 咨询后转移 | | API-IN-008 | 三方通话 | POST | 建立三方通话 | #### 4.2.2 呼出控制接口 | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-OUT-001 | 点击外呼 | POST | 坐席发起外呼 | | API-OUT-002 | 取消外呼 | POST | 取消正在拨打的外呼 | | API-OUT-003 | 预览式外呼确认 | POST | 坐席确认拨打预览号码 | #### 4.2.3 坐席状态接口 | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-AGT-001 | 坐席签入 | POST | 坐席签入系统 | | API-AGT-002 | 坐席签出 | POST | 坐席签出系统 | | API-AGT-003 | 置忙 | POST | 坐席设置为忙碌状态 | | API-AGT-004 | 置闲 | POST | 坐席设置为空闲状态 | | API-AGT-005 | 小休 | POST | 坐席进入小休状态 | | API-AGT-006 | 查询坐席状态 | GET | 查询指定坐席当前状态 | | API-AGT-007 | 批量查询坐席状态 | GET | 批量查询坐席状态 | #### 4.2.4 外呼任务接口 | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-TASK-001 | 创建外呼任务 | POST | 创建预测式/机器人外呼任务 | | API-TASK-002 | 导入号码 | POST | 向任务导入外呼号码 | | API-TASK-003 | 启动任务 | POST | 启动外呼任务 | | API-TASK-004 | 暂停任务 | POST | 暂停外呼任务 | | API-TASK-005 | 恢复任务 | POST | 恢复暂停的任务 | | API-TASK-006 | 停止任务 | POST | 停止外呼任务 | | API-TASK-007 | 查询任务进度 | GET | 查询任务执行进度 | | API-TASK-008 | 查询任务结果 | GET | 查询任务拨打结果明细 | #### 4.2.5 单条外呼接口(通知类) | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-NOTIFY-001 | 发起语音通知 | POST | 触发单条语音通知外呼 | | API-NOTIFY-002 | 查询通知结果 | GET | 查询通知拨打结果 | #### 4.2.6 录音接口 | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-REC-001 | 查询录音列表 | GET | 按条件查询录音记录 | | API-REC-002 | 获取录音下载地址 | GET | 获取录音文件下载URL | #### 4.2.7 监控接口 | 接口编号 | 接口名称 | 请求方式 | 说明 | |---------|---------|---------|------| | API-MON-001 | 监听通话 | POST | 班长监听指定坐席通话 | | API-MON-002 | 强插通话 | POST | 班长强插指定坐席通话 | | API-MON-003 | 强拆通话 | POST | 班长强制挂断指定坐席通话 | | API-MON-004 | 实时统计数据 | GET | 获取实时监控统计数据 | | API-MON-005 | 技能组排队信息 | GET | 获取各技能组排队情况 | ### 4.3 事件推送(WebSocket) #### 4.3.1 通话事件 | 事件编号 | 事件名称 | 触发时机 | 推送内容 | |---------|---------|---------|---------| | EVT-001 | 来电振铃 | 来电分配到坐席时 | 来电号码、客户ID、技能组、IVR收集信息等 | | EVT-002 | 通话接通 | 坐席接听后 | 通话ID、接通时间 | | EVT-003 | 通话挂断 | 通话结束后 | 通话ID、挂断方、通话时长 | | EVT-004 | 转接振铃 | 转接到新坐席振铃时 | 转接来源、来电信息 | | EVT-005 | 外呼振铃 | 外呼拨打中 | 外呼号码、坐席ID | | EVT-006 | 外呼接通 | 客户接听 | 通话ID | | EVT-007 | 外呼失败 | 外呼未接通 | 失败原因(无人接听/忙/关机等) | #### 4.3.2 坐席状态事件 | 事件编号 | 事件名称 | 触发时机 | 推送内容 | |---------|---------|---------|---------| | EVT-101 | 状态变更 | 坐席状态变化时 | 坐席ID、新状态、旧状态、时间 | | EVT-102 | 签入成功 | 坐席签入成功 | 坐席ID、技能组 | | EVT-103 | 签出成功 | 坐席签出成功 | 坐席ID | #### 4.3.3 话单推送 | 事件编号 | 事件名称 | 触发时机 | 推送内容 | |---------|---------|---------|---------| | EVT-201 | 话单推送 | 通话结束后 | 通话ID、主被叫、开始时间、结束时间、时长、录音地址、坐席ID、技能组、呼叫类型等 | #### 4.3.4 机器人交互结果推送 | 事件编号 | 事件名称 | 触发时机 | 推送内容 | |---------|---------|---------|---------| | EVT-301 | 按键结果 | 客户按键后 | 任务ID、号码、按键值、时间 | | EVT-302 | 机器人通话结果 | 机器人外呼结束 | 任务ID、号码、接通状态、通话时长、按键结果 | --- ## 第五章 非功能需求 ### 5.1 性能需求 | 指标项 | 要求 | |-------|------| | **并发坐席数** | 支持150坐席同时在线 | | **并发通话数** | 支持120路并发通话(含呼入+呼出) | | **日处理量** | 支持日均50,000通呼叫 | | **呼叫建立时延** | 来电从接入到振铃坐席 ≤ 1秒 | | **API响应时间** | 95%请求响应 ≤ 200ms | | **事件推送时延** | 事件产生到推送至CRM ≤ 500ms | | **ASR识别延迟** | 语音识别延迟 ≤ 500ms | | **系统可用性** | ≥ 99.9%(全年停机不超过8.76小时) | ### 5.2 高可用需求 | 要求项 | 说明 | |-------|------| | **主备部署** | FreeSWITCH主备双机部署,主机故障自动切换至备机 | | **切换时间** | 故障切换时间 ≤ 30秒 | | **数据同步** | 主备配置实时同步 | | **心跳检测** | 主备心跳检测间隔 ≤ 5秒 | | **数据库高可用** | 数据库采用主从复制或集群部署 | | **录音存储** | 录音存储采用RAID或分布式存储,防止数据丢失 | ### 5.3 安全需求 | 要求项 | 说明 | |-------|------| | **网络安全** | SIP通信支持TLS加密;API支持HTTPS | | **API认证** | API调用需Token认证,支持IP白名单 | | **权限控制** | 支持角色权限管理(管理员、班长、坐席) | | **录音安全** | 录音文件访问需权限验证 | | **操作审计** | 关键操作记录审计日志 | ### 5.4 兼容性需求 | 要求项 | 说明 | |-------|------| | **SIP话机** | 兼容主流SIP话机品牌(如:Yealink、Grandstream、Fanvil等) | | **SIP协议** | 符合RFC 3261标准 | | **浏览器** | 软电话条支持Chrome 70+、Firefox 65+、Edge 79+ | | **编解码** | 语音编解码支持G.711a、G.711u、G.729(可选) | ### 5.5 可扩展性需求 | 要求项 | 说明 | |-------|------| | **坐席扩展** | 架构支持水平扩展至300+坐席 | | **业务线扩展** | 支持新增业务线/技能组,无需改动核心架构 | | **线路扩展** | 支持新增SIP中继线路 | --- ## 第六章 系统架构设计建议 ### 6.1 整体架构图 ``` ┌──────────────────┐ │ 运营商网络 │ │ (SIP Trunk/400) │ └────────┬─────────┘ │ ┌────────▼─────────┐ │ SBC/边界网关 │ │ (可选,安全防护) │ └────────┬─────────┘ │ ┌───────────────────────────┼───────────────────────────┐ │ │ │ │ ┌──────────────────────▼─────────────────────┐ │ │ │ FreeSWITCH │ │ │ │ (主备双机部署) │ │ │ │ ┌─────────┬─────────┬─────────┬─────────┐ │ │ │ │ │ IVR │ ACD │ 录音模块 │ 外呼引擎│ │ │ │ │ └─────────┴─────────┴─────────┴─────────┘ │ │ │ └───────────────────────┬────────────────────┘ │ │ │ │ │ ┌───────────────────────┼───────────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌──────────────┐ ┌────────┐│ │ │ CTI中间件 │ │ API Gateway │ │ WS推送 ││ │ │(ESL连接) │ │ (REST API) │ │ 服务 ││ │ └────┬─────┘ └──────┬───────┘ └───┬────┘│ │ │ │ │ │ │ └────────────────────┼────────────────────┘ │ │ │ │ │ ┌────────────────┼────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ │ │ MySQL │ │ Redis │ │ 文件存储 │ │ │ │ (话单/配置)│ │ (缓存/状态)│ │ (录音) │ │ │ └──────────┘ └──────────┘ └───────────┘ │ │ │ │ 呼叫中心系统边界 │ └───────────────────────────┬──────────────────────────┘ │ ┌───────────────────────┼───────────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ CRM系统 │ │ 业务系统 │ │ 阿里云语音 │ │(弹屏/工单) │ │(派单/订单)│ │(ASR/TTS) │ └──────────┘ └──────────┘ └────────────┘ ``` ### 6.2 核心组件说明 | 组件 | 技术选型 | 说明 | |------|---------|------| | **媒体服务器** | FreeSWITCH | 核心呼叫控制、媒体处理、IVR、录音 | | **CTI中间件** | 自研(基于ESL) | 封装FreeSWITCH ESL接口,提供业务逻辑 | | **API网关** | Nginx/Kong/自研 | RESTful API统一入口 | | **WebSocket服务** | Node.js/Golang | 实时事件推送 | | **数据库** | MySQL 8.0 | 话单、配置、坐席信息存储 | | **缓存** | Redis | 坐席状态、会话信息缓存 | | **录音存储** | NAS/MinIO/对象存储 | 录音文件长期存储 | | **ASR/TTS** | 阿里云智能语音 | 语音识别与合成 | | **NLU** | 外部指定平台 | 意图识别与对话管理 | ### 6.3 部署架构(主备高可用) ``` ┌─────────────────────────────────────┐ │ VIP(虚拟IP) │ └──────────────────┬──────────────────┘ │ ┌──────────────────────────┼──────────────────────────┐ │ │ │ ▼ │ ▼ ┌─────────────────┐ │ ┌─────────────────┐ │ FreeSWITCH │◄─────Keepalived心跳──────────────►│ FreeSWITCH │ │ (主机) │ │ │ (备机) │ │ CTI中间件 │ │ │ CTI中间件 │ │ API服务 │ │ │ API服务 │ └─────────────────┘ │ └─────────────────┘ │ │ │ └──────────────────────────┼──────────────────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌──────────────┐ ┌───────────────┐ │ MySQL主从集群 │ │ Redis Sentinel│ └──────────────┘ └───────────────┘ │ ▼ ┌───────────────┐ │ NAS/分布式存储 │ │ (录音文件) │ └───────────────┘ ``` --- ## 第七章 硬件与软件清单 ### 7.1 硬件清单 | 序号 | 设备类型 | 规格配置 | 数量 | 用途 | 参考单价(万元) | 合计(万元) | |-----|---------|---------|------|------|---------------|-------------| | 1 | 应用服务器(主) | 32核CPU/64GB内存/500GB SSD+2TB HDD | 1 | FreeSWITCH+CTI+API | 5-8 | 5-8 | | 2 | 应用服务器(备) | 32核CPU/64GB内存/500GB SSD+2TB HDD | 1 | FreeSWITCH备机 | 5-8 | 5-8 | | 3 | 数据库服务器 | 16核CPU/64GB内存/1TB SSD | 2 | MySQL主从 | 4-6 | 8-12 | | 4 | Redis服务器 | 8核CPU/32GB内存/500GB SSD | 3 | Redis Sentinel集群 | 2-3 | 6-9 | | 5 | NAS存储 | 96TB可用空间(RAID) | 1 | 录音长期存储 | 8-15 | 8-15 | | 6 | 网络交换机 | 千兆三层交换机 | 2 | 网络通信 | 1-2 | 2-4 | | 7 | 防火墙 | 企业级防火墙 | 1 | 安全防护 | 2-5 | 2-5 | | 8 | IP话机 | SIP话机(如Yealink T46U) | 150 | 坐席通话终端 | 0.05-0.08 | 7.5-12 | | 9 | 坐席耳麦 | 专业话务耳麦 | 150 | 坐席配件 | 0.02-0.05 | 3-7.5 | | **硬件合计** | | | | | | **47-80.5** | ### 7.2 软件清单 | 序号 | 软件/平台 | 说明 | 授权方式 | 预估费用(万元) | |-----|---------|------|---------|----------------| | 1 | FreeSWITCH | 开源软交换平台 | 开源免费 | 0 | | 2 | Linux操作系统 | CentOS/Rocky Linux | 开源免费 | 0 | | 3 | MySQL数据库 | 社区版 | 开源免费 | 0 | | 4 | Redis | 缓存 | 开源免费 | 0 | | 5 | Nginx | 反向代理/负载均衡 | 开源免费 | 0 | | 6 | 阿里云ASR | 语音识别 | 按量付费 | 详见运营费用 | | 7 | 阿里云TTS | 语音合成 | 按量付费 | 详见运营费用 | | 8 | NLU对话平台 | 意图识别 | 按需对接 | 视平台定价 | | 9 | 监控软件(Zabbix/Prometheus) | 系统监控 | 开源免费 | 0 | | **软件合计** | | | | **0(开源为主)** | ### 7.3 开发成本估算 | 模块 | 预估人天 | 说明 | |------|---------|------| | FreeSWITCH环境搭建与配置 | 15 | 主备部署、线路对接、编解码配置 | | IVR流程开发 | 20 | 多业务线IVR、语音导航 | | ACD分配模块 | 25 | 多种分配策略、溢出逻辑 | | CTI中间件开发 | 40 | ESL封装、业务逻辑 | | 坐席状态管理 | 15 | 状态机、持久化 | | API服务开发 | 30 | 全部API接口 | | WebSocket推送服务 | 15 | 事件推送 | | 软电话条组件 | 25 | 前端组件、WebRTC集成 | | 预测式外呼引擎 | 30 | 外呼算法、任务管理 | | 机器人外呼模块 | 20 | TTS集成、按键采集 | | 呼入机器人模块 | 25 | ASR/NLU集成、对话流程 | | 录音管理模块 | 10 | 录音存储、检索 | | 班长监控模块 | 15 | 监听、强插、强拆 | | 实时监控大屏 | 20 | 前端大屏、实时数据 | | 高可用部署 | 15 | 主备切换、Keepalived | | 联调测试 | 30 | 全流程测试 | | **合计** | **350人天** | | **按人天单价1500-2500元估算,开发成本约:52.5-87.5万元** --- ## 第八章 预算汇总 ### 8.1 一次性投入 | 项目 | 费用(万元) | |------|-------------| | 硬件采购 | 47-80.5 | | 软件(开源为主) | 0 | | 开发成本 | 52.5-87.5 | | 实施部署 | 5-10 | | **一次性投入合计** | **104.5-178** | ### 8.2 年度运营费用 | 项目 | 月费用(万元) | 年费用(万元) | 备注 | |------|--------------|---------------|------| | **线路月租** | | | | | 400号码(3个) | 0.3-1.5 | 3.6-18 | 3条业务线独立号码 | | SIP中继并发费(120路) | 0.6-1.2 | 7.2-14.4 | 按50-100元/路/月 | | **通信费** | | | | | 呼入通话费 | 5-9 | 60-108 | 180万分钟/月×0.03-0.05元 | | 呼出通话费 | 21.6-40.5 | 259-486 | 270万分钟/月×0.08-0.15元 | | **云服务费** | | | | | 阿里云ASR | 2-5 | 24-60 | 按使用量 | | 阿里云TTS | 1-3 | 12-36 | 按使用量 | | **其他** | | | | | 机房/带宽 | 1-2 | 12-24 | 如自有机房可降低 | | 系统维护 | 1-2 | 12-24 | 人员/备件 | | **年度运营合计** | | **390-770** | | ### 8.3 总预算汇总 | 项目 | 费用(万元) | |------|-------------| | **首年投入** | 494.5-948(一次性+首年运营) | | **后续每年** | 390-770(运营费用) | > ⚠️ **说明:** > - 通信费用是大头,约占年度成本的80%以上 > - 具体费用取决于运营商议价、实际话务量 > - 可通过接通率优化、IVR分流等降低通信成本 --- ## 第九章 项目里程碑建议 按照3-4个月上线周期规划: | 阶段 | 周期 | 主要工作 | 交付物 | |------|------|---------|--------| | **一、需求确认与设计** | 第1-2周 | 需求评审、技术方案设计、架构设计 | 技术方案文档、接口设计文档 | | **二、环境搭建** | 第3-4周 | 硬件采购到货、FreeSWITCH环境部署、线路对接 | 基础环境就绪 | | **三、核心功能开发** | 第5-10周 | CTI中间件、API服务、坐席管理、呼入呼出基础功能 | 核心功能可用 | | **四、高级功能开发** | 第11-14周 | 预测式外呼、机器人模块、IVR、监控大屏 | 全功能开发完成 | | **五、集成联调** | 第15-16周 | CRM对接、全流程联调、高可用测试 | 联调通过 | | **六、测试与优化** | 第17-18周 | 功能测试、性能测试、Bug修复 | 测试报告 | | **七、试运行** | 第19-20周 | 小范围上线、问题收集、优化调整 | 试运行报告 | | **八、正式上线** | 第21周 | 全面上线、监控保障 | 系统上线 | ``` 第1月 第2月 第3月 第4月 第5月 |----|----|----|----|----|----|----|----|----|----| [需求设计][环境搭建][====核心功能开发====][==高级功能==] [集成联调][测试] [试运行][上线] ``` --- ## 第十章 风险与建议 ### 10.1 项目风险 | 风险项 | 风险等级 | 影响 | 应对措施 | |-------|---------|------|---------| | FreeSWITCH技术门槛高 | 高 | 开发进度延迟 | 建议团队提前学习ESL开发,或引入有经验顾问 | | 线路对接周期不可控 | 中 | 影响联调测试 | 提前与运营商沟通,预留缓冲时间 | | ASR识别效果不及预期 | 中 | 影响机器人体验 | 提前做语料测试,必要时做热词优化 | | 高可用切换影响业务 | 高 | 道路救援中断 | 充分测试切换机制,演练故障场景 | | 录音存储空间不足 | 低 | 录音丢失 | 监控存储空间,提前扩容 | ### 10.2 建议事项 1. **团队能力准备**:建议团队提前熟悉FreeSWITCH、ESL、SIP协议相关知识 2. **分阶段上线**:可先上线基础呼入呼出功能,机器人功能迭代上线 3. **压力测试**:上线前务必进行150坐席并发的压力测试 4. **灰度发布**:建议先在一条业务线(如家政)试运行,再推广到道路救援 5. **监控告警**:完善监控体系,特别是对道路救援线路的可用性监控 --- ## 附录 ### 附录A:术语表 (见第一章1.4节) ### 附录B:接口详细设计 (需在技术设计阶段细化) ### 附录C:IVR流程图 (需在需求确认阶段补充) ### 附录D:机器人话术模板 (需在需求确认阶段补充) --- **文档结束** --- 以上就是完整的需求规格说明书。请您审阅,如有需要补充或调整的内容,请随时告知!🎯 ### USER 根据以上的文档,生成个项目立项书 ### ASSISTANT # 企业呼叫中心系统项目立项书 --- **项目名称**:企业呼叫中心系统建设项目 **项目编号**:CC-2024-001 **编制日期**:2024年1月 **编 制 人**:_________________ **审 核 人**:_________________ **批 准 人**:_________________ --- ## 文档修订记录 | 版本 | 日期 | 修订内容 | 修订人 | 审核人 | |------|------|---------|--------|--------| | V1.0 | 2024-01-XX | 初稿 | | | --- ## 目录 1. [项目概述](#第一章-项目概述) 2. [项目背景与必要性](#第二章-项目背景与必要性) 3. [项目目标](#第三章-项目目标) 4. [项目范围](#第四章-项目范围) 5. [项目建设方案](#第五章-项目建设方案) 6. [项目投资预算](#第六章-项目投资预算) 7. [项目效益分析](#第七章-项目效益分析) 8. [项目实施计划](#第八章-项目实施计划) 9. [项目组织架构](#第九章-项目组织架构) 10. [风险分析与应对](#第十章-风险分析与应对) 11. [结论与建议](#第十一章-结论与建议) --- ## 第一章 项目概述 ### 1.1 项目名称 企业呼叫中心系统建设项目 ### 1.2 项目建设单位 **单位名称**:_________________(企业名称) **负 责 人**:_________________ **联系方式**:_________________ ### 1.3 项目概要 本项目旨在建设一套集**呼入呼出、智能IVR、机器人客服、预测式外呼**于一体的企业级呼叫中心系统,支撑公司道路救援、家政服务、代驾服务三大业务板块的客户服务与营销运营工作。 | 项目要素 | 内容描述 | |---------|---------| | **服务对象** | 个人消费者(ToC) | | **业务范围** | 道路救援、家政服务、代驾服务 | | **坐席规模** | 150坐席 | | **日话务量** | 50,000通/日 | | **服务时间** | 7×24小时 | | **建设周期** | 3-4个月 | | **建设方式** | 自主研发(基于FreeSWITCH开源平台) | | **部署方式** | 私有化部署(主备高可用架构) | ### 1.4 项目总投资 | 投资类别 | 金额(万元) | |---------|-------------| | 一次性建设投资 | 104.5-178 | | 首年运营费用 | 390-770 | | **首年总投入** | **494.5-948** | --- ## 第二章 项目背景与必要性 ### 2.1 企业现状 本企业主营**道路救援、家政服务、代驾服务**三大业务,面向个人消费者提供上门服务。随着业务规模的持续扩大,现有的客户服务模式已无法满足业务发展需求: **现状问题:** | 问题领域 | 具体问题 | 影响 | |---------|---------|------| | **客服能力** | 无统一呼叫平台,多部门分散接听 | 客户体验差,管理困难 | | **响应效率** | 道路救援等紧急需求响应慢 | 客户流失,品牌受损 | | **服务时间** | 夜间/节假日服务能力不足 | 错失订单,客户投诉 | | **外呼效率** | 人工逐一拨打,效率低下 | 人力成本高,触达率低 | | **数据管理** | 通话无录音,无法追溯 | 纠纷处理困难,质量无法管控 | | **系统集成** | 与CRM系统无法打通 | 信息孤岛,坐席重复操作 | ### 2.2 建设必要性 #### 2.2.1 业务发展的迫切需要 ``` 业务增长趋势 话务量 ↑ 60000│ ╭──── 预测增长 │ ╭────╯ 50000│ ╭────╯ ←── 当前日均50000通 │ ╭────╯ 40000│ ╭────╯ │ ╭────╯ 30000│╭────╯ │ └────────────────────────────────────→ 时间 2022 2023 2024 2025 ``` 随着业务规模扩大,日话务量已达**50,000通**,预计未来2年将持续增长。迫切需要建设专业呼叫中心系统支撑业务发展。 #### 2.2.2 客户体验提升的需要 | 客户痛点 | 系统解决方案 | |---------|-------------| | 打电话难以接通 | 150坐席+智能排队+溢出机制 | | 等待时间长 | IVR自助分流+机器人预处理 | | 反复描述问题 | CRM弹屏,坐席即时了解客户信息 | | 投诉处理无反馈 | 录音追溯+工单闭环 | #### 2.2.3 运营效率提升的需要 | 运营痛点 | 系统解决方案 | |---------|-------------| | 外呼效率低 | 预测式外呼,坐席利用率提升50%+ | | 通知类工作繁重 | 机器人自动批量通知 | | 回访覆盖率低 | 机器人自动回访+满意度调查 | | 管理缺乏抓手 | 实时监控大屏+班长监控 | #### 2.2.4 合规与风控的需要 - 道路救援涉及紧急服务,需确保系统**高可用** - 服务过程需**全程录音**,便于纠纷处理 - 需与现有CRM系统**无缝集成**,确保数据安全 ### 2.3 建设可行性 #### 2.3.1 技术可行性 | 技术领域 | 可行性分析 | |---------|-----------| | **呼叫平台** | FreeSWITCH开源成熟,已广泛应用于企业呼叫中心 | | **智能语音** | 阿里云ASR/TTS技术成熟,识别准确率>95% | | **系统集成** | 标准API/WebSocket接口,与现有CRM可顺利对接 | | **高可用** | 主备架构+Keepalived方案成熟可靠 | #### 2.3.2 团队可行性 | 条件 | 评估 | |------|------| | 自有技术团队 | ✅ 具备自主开发能力 | | FreeSWITCH经验 | ⚠️ 需提前培训或引入顾问支持 | | 项目管理能力 | ✅ 具备中大型项目实施经验 | #### 2.3.3 经济可行性 - 自主研发相比采购商业产品**节省授权费用** - 私有化部署**长期成本可控** - 项目投资回报分析详见第七章 --- ## 第三章 项目目标 ### 3.1 总体目标 建设一套**安全、稳定、高效、智能**的企业级呼叫中心系统,实现客户服务统一接入、智能化分流、高效外呼,全面提升客户服务体验与运营效率。 ### 3.2 具体目标 #### 3.2.1 能力目标 | 目标项 | 指标要求 | |-------|---------| | 坐席并发能力 | 支持150坐席同时在线 | | 通话并发能力 | 支持120路并发通话 | | 日处理能力 | 支持50,000通/日 | | 系统可用性 | ≥99.9%(全年停机<8.76小时) | | 故障切换时间 | ≤30秒 | #### 3.2.2 效率目标 | 目标项 | 当前值 | 目标值 | 提升幅度 | |-------|-------|-------|---------| | 人工坐席利用率 | 约40% | ≥70% | +75% | | 外呼接通后转接时间 | N/A(人工拨号) | ≤3秒 | - | | 批量通知人工耗时 | 100%人工 | 0%(全自动) | 释放人力 | | 客户等待时长 | 无统计 | 平均<30秒 | - | #### 3.2.3 体验目标 | 目标项 | 指标要求 | |-------|---------| | 首次呼叫解决率 | ≥80% | | IVR自助分流率 | ≥30% | | 客户满意度 | ≥90% | ### 3.3 建设原则 | 原则 | 说明 | |------|------| | **统一规划** | 三大业务线统一平台,避免重复建设 | | **安全可靠** | 主备高可用,确保道路救援等紧急服务不中断 | | **开放集成** | 标准API接口,与CRM/业务系统无缝对接 | | **智能高效** | 引入AI能力,提升服务效率与体验 | | **弹性扩展** | 架构支持未来扩展至300+坐席 | | **自主可控** | 自主研发,核心技术自主掌握 | --- ## 第四章 项目范围 ### 4.1 业务范围 | 业务线 | 呼入场景 | 呼出场景 | |-------|---------|---------| | **道路救援** | 叫救援、咨询、投诉 | 回访、满意度调查、到达通知 | | **家政服务** | 预约服务、咨询、投诉 | 回访、营销推广、上门提醒 | | **代驾服务** | 叫代驾、咨询、投诉 | 回访、营销推广、到达通知 | ### 4.2 功能范围 #### 4.2.1 本期建设范围(一期) | 模块 | 功能项 | 优先级 | |------|-------|--------| | **基础通话** | 呼入接听、呼出拨打、转接、保持、三方通话 | P0 | | **智能IVR** | 语音导航、按键导航、语音识别、多业务分流 | P0 | | **ACD分配** | 技能组管理、多种分配策略、排队溢出 | P0 | | **坐席管理** | 签入签出、状态管理、软电话条 | P0 | | **预测式外呼** | 批量外呼任务、自动拨打、接通转坐席 | P0 | | **机器人外呼** | 批量通知、TTS播报、按键采集 | P0 | | **呼入机器人** | FAQ问答、意图识别、转人工 | P0 | | **录音管理** | 全程录音、录音存储、录音检索下载 | P0 | | **班长监控** | 监听、强插、强拆 | P0 | | **监控大屏** | 实时数据展示、坐席状态、排队监控 | P0 | | **系统集成** | CRM弹屏、API接口、事件推送 | P0 | | **高可用** | 主备部署、故障切换 | P0 | #### 4.2.2 二期规划范围(本期不含) | 模块 | 功能项 | 说明 | |------|-------|------| | **统计报表** | 话务报表、坐席绩效、外呼分析 | 二期建设 | | **智能质检** | 语音转写、关键词检测、情绪分析 | 二期建设 | | **密语功能** | 班长对坐席单向指导 | 二期建设 | | **短信功能** | 不涉及,由第三方平台提供 | 不纳入 | ### 4.3 集成范围 | 集成系统 | 集成方式 | 集成内容 | |---------|---------|---------| | **CRM系统** | API双向对接 | 来电弹屏、客户资料、工单创建 | | **业务系统** | API对接 | 外呼任务推送、通知触发、结果回传 | | **阿里云语音** | API调用 | ASR语音识别、TTS语音合成 | | **NLU平台** | API对接 | 意图识别、对话管理 | --- ## 第五章 项目建设方案 ### 5.1 总体架构 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 用户层 │ │ 道路救援客户 家政服务客户 代驾服务客户 │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ 400-XXX-XXX1 400-XXX-XXX2 固话/400 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 接入层 │ │ 运营商SIP中继线路 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 呼叫控制层 │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ FreeSWITCH (主备高可用) │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ 呼入控制│ │ 呼出控制│ │智能IVR │ │ ACD分配│ │ 录音模块│ │ │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 业务服务层 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │CTI中间件 │ │API Gateway│ │ WS推送 │ │ 外呼引擎 │ │ 机器人引擎│ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 数据层 │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ MySQL │ │ Redis │ │ NAS存储 │ │ │ │ (主从集群) │ │(Sentinel)│ │ (录音) │ │ │ └──────────┘ └──────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 集成层 │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ CRM系统 │ │ 业务系统 │ │ 阿里云ASR/TTS│ │ │ └──────────┘ └──────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 终端层 │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ IP话机 │ │ 软电话条 │ │ │ │ (150台) │ │ (嵌入CRM) │ │ │ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 5.2 部署架构 采用**私有化部署+主备高可用**架构: ``` ┌─────────────────┐ │ 虚拟IP(VIP) │ │ SIP线路接入 │ └────────┬────────┘ │ ┌─────────────────────┴─────────────────────┐ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ 主服务器 │◄──── Keepalived ────►│ 备服务器 │ ├─────────────────┤ 心跳检测 ├─────────────────┤ │ FreeSWITCH │ │ FreeSWITCH │ │ CTI中间件 │ │ CTI中间件 │ │ API服务 │ │ API服务 │ │ WebSocket服务 │ │ WebSocket服务 │ └────────┬────────┘ └────────┬────────┘ │ │ └─────────────────────┬─────────────────────┘ │ ┌─────────────────────────┼─────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ MySQL主从集群 │ │ Redis Sentinel │ │ NAS存储 │ │ (2节点) │ │ (3节点) │ │ (RAID冗余) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` ### 5.3 技术选型 | 组件类型 | 技术选型 | 选型理由 | |---------|---------|---------| | **媒体服务器** | FreeSWITCH 1.10+ | 开源成熟、性能优异、功能完善 | | **操作系统** | Rocky Linux 8/CentOS 8 | 稳定可靠、长期支持 | | **开发语言** | Java/Golang | CTI中间件、API服务 | | **前端框架** | Vue.js 3.x | 软电话条、监控大屏 | | **数据库** | MySQL 8.0 | 成熟稳定、主从复制 | | **缓存** | Redis 6.x | 高性能、Sentinel高可用 | | **消息队列** | RabbitMQ/Kafka | 事件异步处理(可选) | | **负载均衡** | Nginx | 反向代理、SSL卸载 | | **高可用** | Keepalived | VIP漂移、故障切换 | | **监控** | Prometheus + Grafana | 系统监控、告警 | | **ASR/TTS** | 阿里云智能语音 | 识别准确率高、服务稳定 | ### 5.4 网络规划 | 网络区域 | 网段规划 | 用途 | |---------|---------|------| | **业务网段** | 10.10.1.0/24 | 呼叫中心服务器 | | **数据网段** | 10.10.2.0/24 | 数据库、存储 | | **坐席网段** | 10.10.10.0/23 | 坐席终端、IP话机 | | **管理网段** | 10.10.100.0/24 | 运维管理 | | **SIP信令** | 独立VLAN | SIP中继对接 | ### 5.5 号码规划 | 业务线 | 号码类型 | 号码 | 用途 | |-------|---------|------|------| | 道路救援 | 400号码 | 400-XXX-XXX1 | 客户呼入 | | 家政服务 | 400号码 | 400-XXX-XXX2 | 客户呼入 | | 代驾服务 | 400/固话 | 400-XXX-XXX3 | 客户呼入 | | 外呼显号 | 固话 | 0XX-XXXXXXXX | 外呼主叫显示 | --- ## 第六章 项目投资预算 ### 6.1 投资概算汇总 | 投资类别 | 金额(万元) | 占比 | |---------|-------------|------| | 硬件设备 | 47-80.5 | 33%-45% | | 软件及开发 | 52.5-87.5 | 37%-49% | | 实施部署 | 5-10 | 4%-6% | | **一次性投资合计** | **104.5-178** | **100%** | ### 6.2 硬件投资明细 | 序号 | 设备名称 | 规格 | 数量 | 单价(万) | 合计(万) | |-----|---------|------|------|---------|---------| | 1 | 应用服务器 | 32核/64GB/500GB SSD+2TB | 2 | 5-8 | 10-16 | | 2 | 数据库服务器 | 16核/64GB/1TB SSD | 2 | 4-6 | 8-12 | | 3 | Redis服务器 | 8核/32GB/500GB SSD | 3 | 2-3 | 6-9 | | 4 | NAS存储 | 96TB可用(RAID) | 1 | 8-15 | 8-15 | | 5 | 网络交换机 | 千兆三层 | 2 | 1-2 | 2-4 | | 6 | 防火墙 | 企业级 | 1 | 2-5 | 2-5 | | 7 | IP话机 | SIP话机 | 150 | 0.05-0.08 | 7.5-12 | | 8 | 坐席耳麦 | 专业话务耳麦 | 150 | 0.02-0.05 | 3-7.5 | | | **硬件合计** | | | | **47-80.5** | ### 6.3 软件及开发投资明细 | 序号 | 项目 | 说明 | 费用(万) | |-----|------|------|---------| | 1 | FreeSWITCH | 开源免费 | 0 | | 2 | 操作系统 | Rocky Linux开源 | 0 | | 3 | MySQL/Redis | 社区版开源 | 0 | | 4 | 系统开发 | 350人天×1500-2500元 | 52.5-87.5 | | | **软件及开发合计** | | **52.5-87.5** | ### 6.4 实施部署投资 | 序号 | 项目 | 费用(万) | |-----|------|---------| | 1 | 硬件安装调试 | 1-2 | | 2 | 网络部署配置 | 1-2 | | 3 | 系统联调测试 | 2-4 | | 4 | 培训及上线支持 | 1-2 | | | **实施部署合计** | **5-10** | ### 6.5 年度运营费用 | 项目 | 月费用(万) | 年费用(万) | |------|-----------|-----------| | **线路费用** | | | | 400号码月租(3个) | 0.3-1.5 | 3.6-18 | | SIP并发费(120路) | 0.6-1.2 | 7.2-14.4 | | **通信费用** | | | | 呼入通话费 | 5-9 | 60-108 | | 呼出通话费 | 21.6-40.5 | 259-486 | | **云服务费用** | | | | 阿里云ASR | 2-5 | 24-60 | | 阿里云TTS | 1-3 | 12-36 | | **运维费用** | | | | 机房/带宽 | 1-2 | 12-24 | | 系统维护 | 1-2 | 12-24 | | **年度运营合计** | | **390-770** | ### 6.6 投资估算汇总表 | 项目 | 首年(万) | 第二年(万) | 第三年(万) | |------|---------|-----------|-----------| | 一次性建设投入 | 104.5-178 | - | - | | 年度运营费用 | 390-770 | 390-770 | 390-770 | | **年度总计** | **494.5-948** | **390-770** | **390-770** | | **三年累计** | | | **1274.5-2488** | --- ## 第七章 项目效益分析 ### 7.1 经济效益 #### 7.1.1 人力成本节约 | 节约项 | 计算依据 | 年节约(万) | |-------|---------|-----------| | **外呼效率提升** | 坐席利用率从40%→70%,相当于节约45个坐席人力 | 45×8=360 | | **批量通知自动化** | 原需5人专职,现全自动 | 5×8=40 | | **回访自动化** | 原需10人,现机器人承担80% | 8×8=64 | | **IVR自助分流** | 30%简单咨询自助解决,节约人力 | 15×8=120 | | **年人力成本节约** | | **约584万** | > 注:按坐席人均年成本8万元估算 #### 7.1.2 业务增收 | 增收项 | 计算依据 | 年增收(万) | |-------|---------|-----------| | **接通率提升** | 减少客户流失,预计增收5% | 约200 | | **外呼营销转化** | 新增营销触达能力 | 约150 | | **年业务增收** | | **约350万** | #### 7.1.3 投资回报分析 | 指标 | 数值 | |------|------| | 一次性投入 | 141万(取中值) | | 年运营成本 | 580万(取中值) | | 年成本节约 | 584万 | | 年业务增收 | 350万 | | **年净收益** | **354万** | | **投资回收期** | **约4.8个月** | ### 7.2 管理效益 | 效益项 | 说明 | |-------|------| | **服务可追溯** | 全程录音,服务质量可追溯,纠纷可举证 | | **管理可视化** | 实时监控大屏,管理者实时掌握运营状态 | | **决策有依据** | 话务数据积累,为排班、扩容提供数据支撑 | | **流程标准化** | IVR+机器人固化服务流程,服务质量稳定 | ### 7.3 客户效益 | 效益项 | 说明 | |-------|------| | **接通率提升** | 专业排队机制,减少占线挂断 | | **等待时间缩短** | IVR分流+智能分配,平均等待<30秒 | | **服务体验提升** | 来电自动识别,坐席了解客户历史,避免重复描述 | | **服务时间扩展** | 7×24小时服务,紧急需求随时响应 | --- ## 第八章 项目实施计划 ### 8.1 项目周期 **总工期:4个月(17周)** ### 8.2 阶段划分 ``` 1月 2月 3月 4月 5月 |----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----| W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 [需求设计 ] [环境搭建 ] [========= 核心功能开发 =========] [=== 高级功能开发 ===] [集成联调] [测试优化] [试运行][上线] ``` ### 8.3 里程碑计划 | 里程碑 | 计划时间 | 交付物 | 验收标准 | |-------|---------|-------|---------| | **M1: 需求确认** | 第2周末 | 需求规格说明书、技术方案 | 评审通过 | | **M2: 环境就绪** | 第4周末 | 硬件到位、基础环境部署完成 | FreeSWITCH可接打电话 | | **M3: 核心功能完成** | 第10周末 | 呼入呼出、ACD、坐席管理、API | 核心流程跑通 | | **M4: 全功能完成** | 第14周末 | 外呼、机器人、监控大屏 | 全部功能可用 | | **M5: 联调测试完成** | 第16周末 | CRM集成完成、测试报告 | 全流程联调通过 | | **M6: 试运行完成** | 第18周末 | 试运行报告 | 问题收敛,达到上线标准 | | **M7: 正式上线** | 第19周 | 上线报告 | 系统平稳运行 | ### 8.4 详细进度计划 | 阶段 | 时间 | 主要任务 | 交付物 | |------|------|---------|--------| | **一、需求设计** | W1-W2 | 需求评审、架构设计、接口设计、详细设计 | 技术方案、接口文档、设计文档 | | **二、环境搭建** | W3-W4 | 硬件采购到货、网络部署、FreeSWITCH安装、线路对接 | 基础环境就绪 | | **三、核心开发** | W5-W10 | CTI中间件、API服务、坐席管理、ACD、录音、软电话条 | 核心功能可用 | | **四、高级开发** | W11-W14 | 预测式外呼、机器人模块、IVR语音识别、监控大屏 | 高级功能可用 | | **五、集成联调** | W15-W16 | CRM对接、全流程联调、高可用测试 | 联调通过 | | **六、测试优化** | W16-W17 | 功能测试、性能测试、安全测试、Bug修复 | 测试报告 | | **七、试运行** | W18 | 小范围上线、问题收集 | 试运行报告 | | **八、正式上线** | W19 | 全面上线、监控保障 | 上线完成 | --- ## 第九章 项目组织架构 ### 9.1 组织架构图 ``` ┌─────────────────┐ │ 项目发起人 │ │ (公司高层) │ └────────┬────────┘ │ ┌────────▼────────┐ │ 项目经理 │ │ (总体负责) │ └────────┬────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ 技术开发组 │ │ 业务需求组 │ │ 测试实施组 │ ├───────────────┤ ├───────────────┤ ├───────────────┤ │ · 架构师 │ │ · 业务分析师 │ │ · 测试工程师 │ │ · 后端开发 │ │ · 产品经理 │ │ · 实施工程师 │ │ · 前端开发 │ │ · 业务对接人 │ │ · 运维工程师 │ │ · FreeSWITCH │ │ │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ ``` ### 9.2 角色职责 | 角色 | 人数 | 职责 | |------|------|------| | **项目发起人** | 1 | 项目决策、资源协调、重大问题决策 | | **项目经理** | 1 | 项目整体管理、进度控制、风险管理、对外沟通 | | **架构师** | 1 | 技术架构设计、技术选型、技术难点攻关 | | **后端开发** | 3-4 | CTI中间件、API服务、外呼引擎、机器人模块开发 | | **前端开发** | 1-2 | 软电话条、监控大屏开发 | | **FreeSWITCH工程师** | 1-2 | FreeSWITCH配置、IVR开发、ESL对接 | | **业务分析师** | 1 | 需求分析、业务流程梳理 | | **测试工程师** | 1-2 | 测试用例、功能测试、性能测试 | | **实施工程师** | 1 | 环境部署、系统上线 | | **运维工程师** | 1 | 系统运维、监控告警 | | **合计** | **12-16人** | | ### 9.3 沟通机制 | 会议类型 | 频率 | 参与人 | 内容 | |---------|------|-------|------| | **项目周会** | 每周一次 | 全体成员 | 进度汇报、问题协调、下周计划 | | **每日站会** | 每日 | 开发+测试 | 当日任务、阻塞问题 | | **技术评审会** | 按需 | 技术组 | 技术方案评审、代码评审 | | **里程碑汇报** | 每里程碑 | 项目经理+发起人 | 阶段成果、风险汇报 | | **上线评审会** | 上线前 | 全体+业务方 | 上线准备度检查 | --- ## 第十章 风险分析与应对 ### 10.1 风险清单 | 风险编号 | 风险描述 | 可能性 | 影响程度 | 风险等级 | |---------|---------|--------|---------|---------| | R01 | FreeSWITCH技术门槛高,团队经验不足 | 高 | 高 | **高** | | R02 | 运营商线路对接周期长 | 中 | 高 | **高** | | R03 | CRM系统对接复杂度超预期 | 中 | 中 | **中** | | R04 | ASR识别效果不及预期 | 中 | 中 | **中** | | R05 | 硬件采购/到货延迟 | 低 | 高 | **中** | | R06 | 并发性能不达标 | 低 | 高 | **中** | | R07 | 关键人员离职 | 低 | 高 | **中** | | R08 | 需求变更频繁 | 中 | 中 | **中** | ### 10.2 应对措施 | 风险编号 | 应对措施 | 责任人 | |---------|---------|--------| | **R01** | 1. 项目前期组织FreeSWITCH专项培训
2. 考虑引入外部FreeSWITCH顾问支持
3. 预留20%时间缓冲 | 项目经理 | | **R02** | 1. 提前2个月启动线路申请
2. 准备备选运营商
3. 先用测试线路开发,并行对接正式线路 | 项目经理 | | **R03** | 1. 需求阶段明确接口规范
2. 预留接口联调时间
3. CRM团队提前介入 | 架构师 | | **R04** | 1. 提前进行ASR效果测试
2. 准备热词优化方案
3. 设计降级策略(按键兜底) | 技术组 | | **R05** | 1. 提前下单采购
2. 准备备选供应商
3. 先用云主机开发 | 项目经理 | | **R06** | 1. 上线前进行压力测试
2. 准备扩容方案
3. 架构支持水平扩展 | 架构师 | | **R07** | 1. 知识文档化
2. 关键模块双人备份
3. 提前储备人员 | 项目经理 | | **R08** | 1. 需求基线管理
2. 变更走评审流程
3. 评估变更影响后决策 | 产品经理 | ### 10.3 风险监控 | 风险等级 | 监控频率 | 汇报层级 | |---------|---------|---------| | **高** | 每周跟踪 | 项目发起人 | | **中** | 双周跟踪 | 项目经理 | | **低** | 月度跟踪 | 项目组 | --- ## 第十一章 结论与建议 ### 11.1 结论 经过全面分析,本项目具备以下条件: | 维度 | 评估结论 | |------|---------| | **必要性** | ✅ 业务发展迫切需要,现有模式已无法支撑 | | **可行性** | ✅ 技术方案成熟,团队具备基本能力 | | **经济性** | ✅ 投资回报期短(约4.8个月),效益显著 | | **风险性** | ⚠️ 存在技术风险,但可通过措施控制 | **综合结论:项目具备立项条件,建议批准立项。** ### 11.2 建议 1. **尽快启动**:业务增长迅速,建议尽快批准立项,启动实施 2. **技术准备**:建议在项目正式启动前,组织FreeSWITCH技术培训 3. **线路先行**:建议立即启动运营商线路申请,避免成为关键路径 4. **分步上线**:建议优先上线家政业务,验证后再推广至道路救援 5. **预留缓冲**:建议预留10%-20%预算和时间缓冲,应对不确定性 6. **运维同步**:建议运维人员全程参与,确保上线后平稳运行 ### 11.3 审批意见 | 审批环节 | 审批人 | 日期 | 意见 | |---------|-------|------|------| | **技术评审** | | | | | **财务评审** | | | | | **业务评审** | | | | | **最终审批** | | | | --- ## 附件清单 | 附件编号 | 附件名称 | 说明 | |---------|---------|------| | 附件1 | 《呼叫中心系统需求规格说明书》 | 详细需求文档 | | 附件2 | 硬件选型参考清单 | 硬件详细配置及备选品牌 | | 附件3 | 运营商线路资费对比 | 各运营商资费方案对比 | | 附件4 | FreeSWITCH技术评估报告 | 技术可行性详细分析(如有) | --- **文档结束** --- 以上是完整的项目立项书,涵盖了项目背景、建设方案、投资预算、效益分析、实施计划、组织架构和风险应对等内容。请您审阅,如需调整或补充,请随时告知!📋 ### USER 能否提供FreeSWITCH的详细技术架构和ESL开发指南? ### ASSISTANT # FreeSWITCH技术架构与ESL开发指南 --- ## 目录 1. [FreeSWITCH技术架构](#第一部分-freeswitch技术架构) 2. [核心模块详解](#第二部分-核心模块详解) 3. [ESL开发指南](#第三部分-esl开发指南) 4. [呼叫中心场景开发实践](#第四部分-呼叫中心场景开发实践) 5. [高可用部署方案](#第五部分-高可用部署方案) 6. [性能优化指南](#第六部分-性能优化指南) --- ## 第一部分 FreeSWITCH技术架构 ### 1.1 整体架构概览 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 外部系统/应用层 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ CTI中间件 │ │ Web应用 │ │ 业务系统 │ │ 监控系统 │ │ 第三方App│ │ │ └─────┬────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ │ │ │ │ │ │ │ │ │ └─────────────┴─────────────┴──────┬──────┴─────────────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ ESL接口 │ │ │ │ (TCP:8021) │ │ │ └──────┬──────┘ │ └───────────────────────────────────────────┼─────────────────────────────────┘ │ ┌───────────────────────────────────────────▼─────────────────────────────────┐ │ FreeSWITCH 核心层 │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Core(核心引擎) │ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ │ │ 状态机管理 │ │ 内存管理 │ │ 线程池管理 │ │ │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ │ │ 事件系统 │ │ 数据库抽象 │ │ 日志系统 │ │ │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Module Interface(模块接口层) │ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Endpoint │ │ Application │ │ Codec │ │ Dialplan │ │ │ │ │ │ 终端模块 │ │ 应用模块 │ │ 编解码 │ │ 拨号计划 │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Event │ │ Format │ │ Logger │ │ XML │ │ │ │ │ │ 事件模块 │ │ 文件格式 │ │ 日志模块 │ │ XML处理 │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ┌───────────────────────────────────────────▼─────────────────────────────────┐ │ 协议/媒体层 │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ mod_sofia │ │ mod_verto │ │ mod_rtmp │ │ mod_skinny │ │ │ │ (SIP协议) │ │ (WebRTC) │ │ (RTMP) │ │ (Cisco) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ 媒体处理引擎 │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ │ │ RTP处理 │ │ 编解码转换 │ │ 录音处理 │ │ DTMF检测 │ │ │ │ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ┌───────────────────────────────────────────▼─────────────────────────────────┐ │ 网络层 │ │ SIP/UDP/TCP RTP/SRTP WebSocket HTTP │ │ :5060 :16384-32768 :8081 :8080 │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### 1.2 核心概念 #### 1.2.1 Channel(通道) Channel是FreeSWITCH中最核心的概念,代表一路通话的端点。 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 一通电话 │ │ │ │ ┌─────────────┐ Bridge ┌─────────────┐ │ │ │ Channel A │◄─────────────────────────►│ Channel B │ │ │ │ (主叫方) │ │ (被叫方) │ │ │ │ │ │ │ │ │ │ UUID: xxx-a │ │ UUID: xxx-b │ │ │ │ State: CS_ │ │ State: CS_ │ │ │ │ Variables │ │ Variables │ │ │ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` **Channel状态机:** ``` ┌──────────────────────────────────────────┐ │ │ ▼ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──┴──────┐ │ NEW │───►│ INIT │───►│ ROUTING │───►│ EXECUTE │───►│ HANGUP │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ ┌────▼────┐ ┌────▼────┐ │ CONSUME │ │ EXCHANGE│ │ MEDIA │ │ MEDIA │ └─────────┘ └─────────┘ Channel状态说明: ├── CS_NEW : 新建通道 ├── CS_INIT : 初始化 ├── CS_ROUTING : 路由中(执行dialplan) ├── CS_EXECUTE : 执行应用中 ├── CS_EXCHANGE_MEDIA : 媒体交换中(bridged) ├── CS_CONSUME_MEDIA : 媒体消费中 ├── CS_HIBERNATE : 休眠状态 ├── CS_HANGUP : 挂断中 └── CS_DESTROY : 销毁 ``` #### 1.2.2 Session(会话) Session包含Channel及其相关上下文信息: ```c // Session结构概念图 Session { ├── Channel // 通道信息 │ ├── UUID // 唯一标识 │ ├── State // 当前状态 │ ├── Direction // 呼叫方向(inbound/outbound) │ └── Variables // 通道变量 │ ├── Caller Profile // 主叫信息 │ ├── caller_id_name │ ├── caller_id_number │ ├── destination_number │ └── ... │ ├── Codec // 编解码信息 ├── RTP Session // RTP会话 └── Bridge Session // 桥接会话(如果有) } ``` #### 1.2.3 Event(事件) FreeSWITCH基于事件驱动架构,所有状态变化都产生事件: ``` ┌─────────────────────────────────────────────────────────────────┐ │ FreeSWITCH Event System │ │ │ │ ┌───────────────┐ │ │ │ Event Producer│ ──────┐ │ │ │ (模块/核心) │ │ │ │ └───────────────┘ │ ┌─────────────────────────┐ │ │ ├─────►│ Event Dispatcher │ │ │ ┌───────────────┐ │ │ (事件分发器) │ │ │ │ Event Producer│ ──────┤ └───────────┬─────────────┘ │ │ │ (模块/核心) │ │ │ │ │ └───────────────┘ │ ┌───────────┴─────────────┐ │ │ │ │ │ │ │ │ ┌───────────────┐ │ ▼ ▼ ▼ │ │ │ Event Producer│ ──────┘ ┌───────┐ ┌───────┐ ┌───────┐│ │ │ (模块/核心) │ │ ESL │ │Internal│ │ Lua/ ││ │ └───────────────┘ │Consumer│ │Handler │ │ JS ││ │ └───────┘ └───────┘ └───────┘│ └─────────────────────────────────────────────────────────────────┘ ``` **常用事件类型:** | 事件名称 | 触发时机 | 用途 | |---------|---------|------| | CHANNEL_CREATE | 通道创建 | 新呼叫进入 | | CHANNEL_ANSWER | 通话接听 | 通话建立 | | CHANNEL_BRIDGE | 通道桥接 | 双方接通 | | CHANNEL_UNBRIDGE | 取消桥接 | 桥接断开 | | CHANNEL_HANGUP | 挂断 | 通话结束 | | CHANNEL_HANGUP_COMPLETE | 挂断完成 | 通话完全结束 | | CHANNEL_STATE | 状态变化 | 状态监控 | | DTMF | 按键检测 | IVR交互 | | RECORD_START | 开始录音 | 录音控制 | | RECORD_STOP | 停止录音 | 录音控制 | | CUSTOM | 自定义事件 | 业务扩展 | ### 1.3 目录结构 ``` /usr/local/freeswitch/ ├── bin/ # 可执行文件 │ └── freeswitch # 主程序 │ ├── conf/ # 配置文件目录 │ ├── freeswitch.xml # 主配置文件 │ ├── vars.xml # 全局变量 │ ├── sip_profiles/ # SIP配置 │ │ ├── internal.xml # 内部Profile(坐席注册) │ │ └── external.xml # 外部Profile(中继对接) │ ├── dialplan/ # 拨号计划 │ │ ├── default.xml # 默认拨号计划 │ │ └── public.xml # 公共拨号计划(呼入) │ ├── directory/ # 用户目录(分机注册) │ │ └── default/ │ │ └── 1000.xml # 分机1000配置 │ ├── autoload_configs/ # 模块自动加载配置 │ │ ├── modules.conf.xml # 模块加载列表 │ │ ├── sofia.conf.xml # SIP模块配置 │ │ ├── event_socket.conf.xml# ESL配置 │ │ └── ... │ └── lang/ # 语音提示 │ ├── lib/ # 库文件 ├── mod/ # 模块文件(.so) ├── log/ # 日志目录 ├── db/ # 数据库文件 ├── recordings/ # 录音文件 ├── sounds/ # 语音文件 ├── scripts/ # 脚本目录 └── storage/ # 存储目录 ``` --- ## 第二部分 核心模块详解 ### 2.1 mod_sofia(SIP模块) mod_sofia是FreeSWITCH的SIP协议栈实现,基于Sofia-SIP库。 #### 2.1.1 架构 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ mod_sofia │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Sofia-SIP Stack │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ │ │ NUA │ │ NTA │ │ TPORT │ │ SDP │ │ │ │ │ │ User Agent│ │Transaction│ │ Transport │ │ Parser │ │ │ │ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Profile Management │ │ │ │ │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ │ │ Internal Profile │ │ External Profile │ │ │ │ │ │ (坐席/分机注册) │ │ (中继/网关) │ │ │ │ │ │ UDP:5060 │ │ UDP:5080 │ │ │ │ │ └─────────────────────┘ └─────────────────────┘ │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ Gateway Management │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ │ │Gateway A│ │Gateway B│ │Gateway C│ ... │ │ │ │ │ │ │(运营商1) │ │(运营商2) │ │(备用) │ │ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` #### 2.1.2 配置示例 **Internal Profile(坐席注册):** ```xml ``` **External Profile(中继对接):** ```xml ``` ### 2.2 Dialplan(拨号计划) 拨号计划定义了呼叫的路由和处理逻辑。 #### 2.2.1 执行流程 ``` ┌──────────────────────────────────────────────────────────────────┐ │ Dialplan 执行流程 │ │ │ │ 来电进入 │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ Context 选择 ││ │ │ (根据Profile配置的context) ││ │ │ 例如: public (呼入) / default (内部) ││ │ └──────────────────────────┬──────────────────────────────────┘│ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ Extension 匹配 ││ │ │ (按顺序匹配condition) ││ │ │ ││ │ │ Extension 1: ──┐ ││ │ │ Condition ───┼─► 不匹配 ───► 下一个Extension ││ │ │ │ ││ │ │ └─► 匹配 ───► 执行Actions ││ │ └──────────────────────────┬──────────────────────────────────┘│ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ Action 执行 ││ │ │ (按顺序执行应用) ││ │ │ ││ │ │ action: answer ││ │ │ action: playback ││ │ │ action: bridge ││ │ │ ... ││ │ └─────────────────────────────────────────────────────────────┘│ │ │ └──────────────────────────────────────────────────────────────────┘ ``` #### 2.2.2 配置示例 **呼入路由(public.xml):** ```xml ``` **IVR和呼叫处理(default.xml):** ```xml ``` ### 2.3 mod_event_socket(ESL模块) ESL是FreeSWITCH对外提供的控制接口,是开发呼叫中心的核心。 #### 2.3.1 工作模式 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ ESL 工作模式 │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ Inbound 模式 │ │ │ │ (客户端主动连接) │ │ │ │ │ │ │ │ ┌───────────┐ TCP:8021 ┌──────────────────┐ │ │ │ │ │ CTI中间件 │ ──────────────────────► │ FreeSWITCH │ │ │ │ │ │ (Client) │ ◄────────────────────── │ (Server) │ │ │ │ │ └───────────┘ Event Stream └──────────────────┘ │ │ │ │ │ │ │ │ 特点: │ │ │ │ - 客户端主动连接FreeSWITCH │ │ │ │ - 可以订阅事件、发送命令 │ │ │ │ - 适合集中控制、监控场景 │ │ │ │ - 推荐呼叫中心使用此模式 │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ Outbound 模式 │ │ │ │ (FreeSWITCH主动连接) │ │ │ │ │ │ │ │ ┌──────────────────┐ TCP ┌───────────────┐ │ │ │ │ │ FreeSWITCH │ ──────────────────►│ 应用服务器 │ │ │ │ │ │ (每个呼叫) │ ◄────────────────── │ (Server) │ │ │ │ │ └──────────────────┘ └───────────────┘ │ │ │ │ │ │ │ │ 特点: │ │ │ │ - 每个呼叫FreeSWITCH主动连接应用 │ │ │ │ - 在dialplan中使用socket应用触发 │ │ │ │ - 适合简单IVR场景 │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` #### 2.3.2 ESL配置 ```xml ``` --- ## 第三部分 ESL开发指南 ### 3.1 ESL协议详解 #### 3.1.1 协议格式 ESL使用类HTTP的文本协议: ``` ┌─────────────────────────────────────────────────────────────────────┐ │ ESL 消息格式 │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ 请求格式: │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ command [arguments] │ │ │ │ │ │ Header-Name: header-value │ │ │ │ │ │ Header-Name: header-value │ │ │ │ │ │ │ │ │ │ │ │ [body if Content-Length > 0] │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ 响应格式: │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ Content-Type: text/event-plain │ │ │ │ │ │ Content-Length: 1234 │ │ │ │ │ │ Reply-Text: +OK [message] │ │ │ │ │ │ │ │ │ │ │ │ [body] │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` #### 3.1.2 常用命令 | 命令类型 | 命令 | 说明 | |---------|------|------| | **认证** | `auth ` | 连接认证 | | **事件订阅** | `event plain ` | 订阅事件 | | | `event plain ALL` | 订阅所有事件 | | | `event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_HANGUP` | 订阅指定事件 | | | `nixevent ` | 取消订阅 | | | `noevents` | 取消所有订阅 | | **API命令** | `api [args]` | 同步执行API | | | `bgapi [args]` | 异步执行API | | **过滤器** | `filter
` | 过滤特定事件 | | | `filter Unique-ID ` | 只接收指定UUID事件 | | **呼叫控制** | `sendmsg ` | 向指定通道发送消息 | | | `execute` | 在通道上执行应用 | | **其他** | `log ` | 设置日志级别 | | | `linger` | 保持连接 | | | `nolinger` | 取消保持 | | | `exit` | 断开连接 | ### 3.2 Java ESL开发 #### 3.2.1 项目结构 ``` callcenter-cti/ ├── pom.xml ├── src/main/java/ │ └── com/callcenter/ │ ├── Application.java │ ├── esl/ │ │ ├── EslConnectionManager.java # ESL连接管理 │ │ ├── EslEventHandler.java # 事件处理 │ │ ├── EslCommandExecutor.java # 命令执行 │ │ └── InboundClient.java # Inbound客户端 │ ├── service/ │ │ ├── AgentService.java # 坐席服务 │ │ ├── CallService.java # 呼叫服务 │ │ ├── QueueService.java # 排队服务 │ │ └── AcdService.java # 话务分配服务 │ ├── model/ │ │ ├── Agent.java # 坐席模型 │ │ ├── Call.java # 呼叫模型 │ │ └── Queue.java # 队列模型 │ ├── api/ │ │ ├── AgentController.java # 坐席API │ │ └── CallController.java # 呼叫API │ └── websocket/ │ └── EventPushHandler.java # 事件推送 └── src/main/resources/ └── application.yml ``` #### 3.2.2 Maven依赖 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket org.freeswitch.esl.client org.freeswitch.esl.client 0.9.2 io.netty netty-all 4.1.86.Final org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 2.0.23 org.apache.commons commons-lang3 org.projectlombok lombok ``` #### 3.2.3 ESL连接管理 ```java package com.callcenter.esl; import lombok.extern.slf4j.Slf4j; import org.freeswitch.esl.client.inbound.Client; import org.freeswitch.esl.client.inbound.InboundConnectionFailure; import org.freeswitch.esl.client.transport.event.EslEvent; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; /** * ESL连接管理器 * 负责与FreeSWITCH建立和维护连接 */ @Slf4j @Component public class EslConnectionManager { @Value("${freeswitch.esl.host:127.0.0.1}") private String eslHost; @Value("${freeswitch.esl.port:8021}") private int eslPort; @Value("${freeswitch.esl.password:ClueCon}") private String eslPassword; @Value("${freeswitch.esl.timeout:30}") private int timeout; private Client eslClient; private final AtomicBoolean connected = new AtomicBoolean(false); private final AtomicBoolean reconnecting = new AtomicBoolean(false); private ScheduledExecutorService scheduler; private ExecutorService eventExecutor; private EslEventHandler eventHandler; @PostConstruct public void init() { // 初始化线程池 scheduler = Executors.newSingleThreadScheduledExecutor( r -> new Thread(r, "esl-reconnect")); eventExecutor = new ThreadPoolExecutor( 4, 16, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000), r -> new Thread(r, "esl-event-handler"), new ThreadPoolExecutor.CallerRunsPolicy() ); // 建立连接 connect(); // 启动心跳检测 startHeartbeat(); } /** * 建立ESL连接 */ public synchronized void connect() { if (connected.get()) { log.info("ESL已连接,无需重复连接"); return; } try { log.info("正在连接FreeSWITCH ESL: {}:{}", eslHost, eslPort); eslClient = new Client(); eslClient.connect(eslHost, eslPort, eslPassword, timeout); // 订阅事件 subscribeEvents(); // 设置事件监听器 eslClient.setEventSubscriptions("plain", "all"); eslClient.addEventListener((ctx, event) -> { handleEvent(event); }); connected.set(true); reconnecting.set(false); log.info("ESL连接成功"); } catch (InboundConnectionFailure e) { log.error("ESL连接失败: {}", e.getMessage()); connected.set(false); scheduleReconnect(); } } /** * 订阅所需事件 */ private void subscribeEvents() { String events = String.join(" ", "CHANNEL_CREATE", "CHANNEL_ANSWER", "CHANNEL_BRIDGE", "CHANNEL_UNBRIDGE", "CHANNEL_HANGUP", "CHANNEL_HANGUP_COMPLETE", "CHANNEL_STATE", "DTMF", "RECORD_START", "RECORD_STOP", "CUSTOM" ); eslClient.setEventSubscriptions("plain", events); eslClient.addEventFilter("Event-Name", "CHANNEL_CREATE"); log.info("已订阅事件: {}", events); } /** * 处理事件 */ private void handleEvent(EslEvent event) { eventExecutor.submit(() -> { try { if (eventHandler != null) { eventHandler.handle(event); } } catch (Exception e) { log.error("事件处理异常: {}", e.getMessage(), e); } }); } /** * 启动心跳检测 */ private void startHeartbeat() { scheduler.scheduleAtFixedRate(() -> { if (connected.get()) { try { // 发送status命令检测连接 eslClient.sendSyncApiCommand("status", ""); } catch (Exception e) { log.warn("心跳检测失败,连接可能已断开"); connected.set(false); scheduleReconnect(); } } }, 30, 30, TimeUnit.SECONDS); } /** * 计划重连 */ private void scheduleReconnect() { if (reconnecting.compareAndSet(false, true)) { scheduler.schedule(() -> { log.info("尝试重新连接ESL..."); connect(); }, 5, TimeUnit.SECONDS); } } /** * 同步执行API命令 */ public String sendSyncApiCommand(String command, String args) { checkConnection(); try { return eslClient.sendSyncApiCommand(command, args).getBodyLines().get(0); } catch (Exception e) { log.error("执行API命令失败: {} {}", command, args, e); throw new RuntimeException("ESL命令执行失败", e); } } /** * 异步执行API命令 */ public CompletableFuture sendAsyncApiCommand(String command, String args) { return CompletableFuture.supplyAsync(() -> sendSyncApiCommand(command, args), eventExecutor); } /** * 向指定UUID发送消息执行应用 */ public void sendExecute(String uuid, String application, String args) { checkConnection(); try { eslClient.sendSyncApiCommand("uuid_broadcast", uuid + " " + application + "::" + args + " both"); } catch (Exception e) { log.error("发送Execute失败: uuid={}, app={}", uuid, application, e); } } /** * 检查连接状态 */ private void checkConnection() { if (!connected.get()) { throw new RuntimeException("ESL未连接"); } } /** * 获取连接状态 */ public boolean isConnected() { return connected.get(); } /** * 设置事件处理器 */ public void setEventHandler(EslEventHandler handler) { this.eventHandler = handler; } @PreDestroy public void destroy() { try { connected.set(false); if (eslClient != null) { eslClient.close(); } if (scheduler != null) { scheduler.shutdown(); } if (eventExecutor != null) { eventExecutor.shutdown(); } log.info("ESL连接已关闭"); } catch (Exception e) { log.error("关闭ESL连接异常", e); } } } ``` #### 3.2.4 事件处理器 ```java package com.callcenter.esl; import com.callcenter.model.Call; import com.callcenter.model.CallDirection; import com.callcenter.model.CallState; import com.callcenter.service.AcdService; import com.callcenter.service.CallService; import com.callcenter.websocket.EventPushService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.freeswitch.esl.client.transport.event.EslEvent; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Map; /** * ESL事件处理器 */ @Slf4j @Component @RequiredArgsConstructor public class EslEventHandler { private final CallService callService; private final AcdService acdService; private final EventPushService eventPushService; /** * 处理ESL事件 */ public void handle(EslEvent event) { String eventName = event.getEventName(); Map headers = event.getEventHeaders(); log.debug("收到事件: {}, UUID: {}", eventName, headers.get("Unique-ID")); switch (eventName) { case "CHANNEL_CREATE": handleChannelCreate(headers); break; case "CHANNEL_ANSWER": handleChannelAnswer(headers); break; case "CHANNEL_BRIDGE": handleChannelBridge(headers); break; case "CHANNEL_UNBRIDGE": handleChannelUnbridge(headers); break; case "CHANNEL_HANGUP": handleChannelHangup(headers); break; case "CHANNEL_HANGUP_COMPLETE": handleChannelHangupComplete(headers); break; case "DTMF": handleDtmf(headers); break; case "RECORD_START": handleRecordStart(headers); break; case "RECORD_STOP": handleRecordStop(headers); break; case "CUSTOM": handleCustomEvent(headers); break; default: log.trace("未处理的事件类型: {}", eventName); } } /** * 处理通道创建事件(新呼叫) */ private void handleChannelCreate(Map headers) { String uuid = headers.get("Unique-ID"); String direction = headers.get("Call-Direction"); String callerNumber = headers.get("Caller-Caller-ID-Number"); String calleeNumber = headers.get("Caller-Destination-Number"); log.info("新呼叫: uuid={}, direction={}, caller={}, callee={}", uuid, direction, callerNumber, calleeNumber); // 创建呼叫记录 Call call = new Call(); call.setUuid(uuid); call.setDirection("inbound".equals(direction) ? CallDirection.INBOUND : CallDirection.OUTBOUND); call.setCallerNumber(callerNumber); call.setCalleeNumber(calleeNumber); call.setState(CallState.RINGING); call.setCreateTime(LocalDateTime.now()); // 获取业务标识(从通道变量) String businessType = headers.get("variable_business_type"); String skillGroup = headers.get("variable_skill_group"); call.setBusinessType(businessType); call.setSkillGroup(skillGroup); callService.saveCall(call); // 如果是呼入,触发ACD排队 if ("inbound".equals(direction)) { acdService.enqueue(call); } // 推送事件到前端 eventPushService.pushCallEvent("CALL_RINGING", call); } /** * 处理通话接听事件 */ private void handleChannelAnswer(Map headers) { String uuid = headers.get("Unique-ID"); log.info("通话接听: uuid={}", uuid); Call call = callService.getCallByUuid(uuid); if (call != null) { call.setState(CallState.ANSWERED); call.setAnswerTime(LocalDateTime.now()); callService.updateCall(call); // 推送事件 eventPushService.pushCallEvent("CALL_ANSWERED", call); } } /** * 处理通道桥接事件(双方接通) */ private void handleChannelBridge(Map headers) { String uuid = headers.get("Unique-ID"); String otherUuid = headers.get("Other-Leg-Unique-ID"); log.info("通道桥接: uuid={}, otherUuid={}", uuid, otherUuid); Call call = callService.getCallByUuid(uuid); if (call != null) { call.setState(CallState.BRIDGED); call.setBridgeUuid(otherUuid); call.setBridgeTime(LocalDateTime.now()); callService.updateCall(call); // 推送事件 eventPushService.pushCallEvent("CALL_BRIDGED", call); } } /** * 处理取消桥接事件 */ private void handleChannelUnbridge(Map headers) { String uuid = headers.get("Unique-ID"); log.info("取消桥接: uuid={}", uuid); Call call = callService.getCallByUuid(uuid); if (call != null) { call.setState(CallState.UNBRIDGED); callService.updateCall(call); eventPushService.pushCallEvent("CALL_UNBRIDGED", call); } } /** * 处理挂断事件 */ private void handleChannelHangup(Map headers) { String uuid = headers.get("Unique-ID"); String hangupCause = headers.get("Hangup-Cause"); log.info("通话挂断: uuid={}, cause={}", uuid, hangupCause); Call call = callService.getCallByUuid(uuid); if (call != null) { call.setState(CallState.HANGUP); call.setHangupCause(hangupCause); call.setHangupTime(LocalDateTime.now()); // 计算通话时长 if (call.getAnswerTime() != null) { long duration = java.time.Duration.between( call.getAnswerTime(), call.getHangupTime()).getSeconds(); call.setDuration(duration); } callService.updateCall(call); // 推送事件 eventPushService.pushCallEvent("CALL_HANGUP", call); // 更新坐席状态 if (call.getAgentId() != null) { acdService.onCallEnd(call.getAgentId(), call); } } } /** * 处理挂断完成事件(获取完整话单) */ private void handleChannelHangupComplete(Map headers) { String uuid = headers.get("Unique-ID"); String billsec = headers.get("variable_billsec"); String duration = headers.get("variable_duration"); String recordingPath = headers.get("variable_recording_path"); log.info("通话结束: uuid={}, billsec={}, recording={}", uuid, billsec, recordingPath); Call call = callService.getCallByUuid(uuid); if (call != null) { call.setBillsec(billsec != null ? Long.parseLong(billsec) : 0); call.setRecordingPath(recordingPath); callService.updateCall(call); // 推送话单 eventPushService.pushCdr(call); } } /** * 处理DTMF按键事件 */ private void handleDtmf(Map headers) { String uuid = headers.get("Unique-ID"); String digit = headers.get("DTMF-Digit"); log.info("DTMF按键: uuid={}, digit={}", uuid, digit); // 推送按键事件 eventPushService.pushDtmf(uuid, digit); } /** * 处理录音开始事件 */ private void handleRecordStart(Map headers) { String uuid = headers.get("Unique-ID"); String recordPath = headers.get("Record-File-Path"); log.info("开始录音: uuid={}, path={}", uuid, recordPath); Call call = callService.getCallByUuid(uuid); if (call != null) { call.setRecordingPath(recordPath); callService.updateCall(call); } } /** * 处理录音停止事件 */ private void handleRecordStop(Map headers) { String uuid = headers.get("Unique-ID"); String recordPath = headers.get("Record-File-Path"); log.info("停止录音: uuid={}, path={}", uuid, recordPath); } /** * 处理自定义事件 */ private void handleCustomEvent(Map headers) { String subclass = headers.get("Event-Subclass"); log.debug("自定义事件: subclass={}", subclass); // 处理特定的自定义事件 if ("callcenter::info".equals(subclass)) { // 处理呼叫中心相关事件 } } } ``` #### 3.2.5 呼叫服务 ```java package com.callcenter.service; import com.callcenter.esl.EslConnectionManager; import com.callcenter.model.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * 呼叫服务 * 提供呼叫控制相关功能 */ @Slf4j @Service @RequiredArgsConstructor public class CallService { private final EslConnectionManager eslManager; private final RedisTemplate redisTemplate; private static final String CALL_KEY_PREFIX = "call:"; private static final int CALL_EXPIRE_HOURS = 24; /** * 发起外呼 * @param agentId 坐席ID * @param callerNumber 主叫号码(外显号码) * @param calleeNumber 被叫号码 * @return 呼叫UUID */ public String originate(String agentId, String callerNumber, String calleeNumber) { log.info("发起外呼: agent={}, caller={}, callee={}", agentId, callerNumber, calleeNumber); // 获取坐席分机号 Agent agent = getAgent(agentId); if (agent == null || agent.getState() != AgentState.READY) { throw new RuntimeException("坐席不可用"); } // 构建originate命令 // originate {变量}呼叫字符串 &应用(参数) StringBuilder cmd = new StringBuilder(); cmd.append("{"); cmd.append("origination_caller_id_number=").append(callerNumber).append(","); cmd.append("origination_caller_id_name=公司名称").append(","); cmd.append("agent_id=").append(agentId).append(","); cmd.append("direction=outbound").append(","); cmd.append("RECORD_STEREO=true"); cmd.append("}"); // A-leg: 先呼叫坐席分机 cmd.append("user/").append(agent.getExtension()); // B-leg: 坐席接听后呼叫客户 cmd.append(" &bridge({origination_caller_id_number=") .append(callerNumber) .append("}sofia/gateway/carrier_primary/") .append(calleeNumber) .append(")"); String result = eslManager.sendSyncApiCommand("originate", cmd.toString()); if (result.startsWith("+OK")) { String uuid = result.substring(4).trim(); log.info("外呼成功: uuid={}", uuid); return uuid; } else { log.error("外呼失败: {}", result); throw new RuntimeException("外呼失败: " + result); } } /** * 接听来电 */ public void answer(String uuid) { log.info("接听来电: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_answer", uuid); } /** * 挂断通话 */ public void hangup(String uuid) { log.info("挂断通话: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_kill", uuid); } /** * 挂断通话(带原因) */ public void hangup(String uuid, String cause) { log.info("挂断通话: uuid={}, cause={}", uuid, cause); eslManager.sendSyncApiCommand("uuid_kill", uuid + " " + cause); } /** * 保持通话 */ public void hold(String uuid) { log.info("保持通话: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_hold", uuid); } /** * 取回保持 */ public void unhold(String uuid) { log.info("取回通话: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_hold", "off " + uuid); } /** * 静音 */ public void mute(String uuid) { log.info("静音: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_audio", uuid + " start mute"); } /** * 取消静音 */ public void unmute(String uuid) { log.info("取消静音: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_audio", uuid + " stop"); } /** * 盲转(直接转接) */ public void blindTransfer(String uuid, String destination) { log.info("盲转: uuid={}, dest={}", uuid, destination); eslManager.sendSyncApiCommand("uuid_transfer", uuid + " " + destination); } /** * 咨询转(先咨询再转接) */ public String consultTransfer(String uuid, String destination) { log.info("咨询转: uuid={}, dest={}", uuid, destination); // 先保持当前通话 hold(uuid); // 发起咨询呼叫 String consultCmd = "{origination_uuid=consult_" + uuid + "}user/" + destination; String result = eslManager.sendSyncApiCommand("originate", consultCmd + " &park()"); if (result.startsWith("+OK")) { return result.substring(4).trim(); } throw new RuntimeException("咨询转失败"); } /** * 完成咨询转接 */ public void completeTransfer(String originalUuid, String consultUuid) { log.info("完成咨询转: original={}, consult={}", originalUuid, consultUuid); eslManager.sendSyncApiCommand("uuid_bridge", originalUuid + " " + consultUuid); } /** * 三方通话 */ public void conference(String uuid1, String uuid2, String conferenceName) { log.info("三方通话: uuid1={}, uuid2={}, conf={}", uuid1, uuid2, conferenceName); // 将两个通道加入会议 eslManager.sendSyncApiCommand("uuid_transfer", uuid1 + " conference:" + conferenceName + " inline"); eslManager.sendSyncApiCommand("uuid_transfer", uuid2 + " conference:" + conferenceName + " inline"); } /** * 播放语音 */ public void playback(String uuid, String filePath) { log.info("播放语音: uuid={}, file={}", uuid, filePath); eslManager.sendSyncApiCommand("uuid_broadcast", uuid + " " + filePath + " both"); } /** * 停止播放 */ public void stopPlayback(String uuid) { log.info("停止播放: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_break", uuid); } /** * 开始录音 */ public void startRecording(String uuid, String filePath) { log.info("开始录音: uuid={}, file={}", uuid, filePath); eslManager.sendSyncApiCommand("uuid_record", uuid + " start " + filePath); } /** * 停止录音 */ public void stopRecording(String uuid, String filePath) { log.info("停止录音: uuid={}", uuid); eslManager.sendSyncApiCommand("uuid_record", uuid + " stop " + filePath); } /** * 桥接两个通道 */ public void bridge(String uuid1, String uuid2) { log.info("桥接通道: uuid1={}, uuid2={}", uuid1, uuid2); eslManager.sendSyncApiCommand("uuid_bridge", uuid1 + " " + uuid2); } /** * 设置通道变量 */ public void setVariable(String uuid, String name, String value) { eslManager.sendSyncApiCommand("uuid_setvar", uuid + " " + name + " " + value); } /** * 获取通道变量 */ public String getVariable(String uuid, String name) { return eslManager.sendSyncApiCommand("uuid_getvar", uuid + " " + name); } // ============ 呼叫记录管理 ============ /** * 保存呼叫记录 */ public void saveCall(Call call) { String key = CALL_KEY_PREFIX + call.getUuid(); redisTemplate.opsForValue().set(key, call, CALL_EXPIRE_HOURS, TimeUnit.HOURS); } /** * 获取呼叫记录 */ public Call getCallByUuid(String uuid) { String key = CALL_KEY_PREFIX + uuid; return (Call) redisTemplate.opsForValue().get(key); } /** * 更新呼叫记录 */ public void updateCall(Call call) { saveCall(call); } /** * 获取坐席信息 */ private Agent getAgent(String agentId) { // TODO: 从坐席服务获取 return null; } } ``` #### 3.2.6 ACD话务分配服务 ```java package com.callcenter.service; import com.callcenter.esl.EslConnectionManager; import com.callcenter.model.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * ACD话务分配服务 * 实现多种分配策略:历史服务优先、最长空闲优先、轮询 */ @Slf4j @Service @RequiredArgsConstructor public class AcdService { private final EslConnectionManager eslManager; private final RedisTemplate redisTemplate; private final AgentService agentService; private final CallService callService; // 队列Key前缀 private static final String QUEUE_KEY_PREFIX = "queue:"; // 坐席最后服务客户映射 private static final String AGENT_CUSTOMER_MAP_KEY = "agent:customer:map"; // 坐席最后空闲时间 private static final String AGENT_IDLE_TIME_KEY = "agent:idle:time:"; // 轮询索引 private final Map roundRobinIndex = new ConcurrentHashMap<>(); /** * 呼叫入队 */ public void enqueue(Call call) { String skillGroup = call.getSkillGroup(); String queueKey = QUEUE_KEY_PREFIX + skillGroup; QueueItem item = new QueueItem(); item.setCallUuid(call.getUuid()); item.setCallerNumber(call.getCallerNumber()); item.setSkillGroup(skillGroup); item.setEnqueueTime(LocalDateTime.now()); item.setPriority(calculatePriority(call)); // 使用SortedSet,按优先级和入队时间排序 double score = item.getPriority() * 1000000000L + System.currentTimeMillis() / 1000; redisTemplate.opsForZSet().add(queueKey, item, score); log.info("呼叫入队: uuid={}, queue={}, priority={}", call.getUuid(), skillGroup, item.getPriority()); // 尝试立即分配 tryAssign(skillGroup); } /** * 计算优先级(数字越小优先级越高) */ private int calculatePriority(Call call) { // VIP客户优先级高 if ("VIP".equals(call.getCustomerLevel())) { return 1; } // 投诉优先 if ("complaint".equals(call.getBusinessType())) { return 2; } // 普通客户 return 5; } /** * 尝试分配队列中的呼叫 */ public void tryAssign(String skillGroup) { String queueKey = QUEUE_KEY_PREFIX + skillGroup; // 获取可用坐席 List availableAgents = agentService.getAvailableAgents(skillGroup); if (availableAgents.isEmpty()) { log.debug("技能组{}无可用坐席", skillGroup); return; } // 获取队列中的呼叫 Set queueItems = redisTemplate.opsForZSet().range(queueKey, 0, 0); if (queueItems == null || queueItems.isEmpty()) { log.debug("技能组{}队列为空", skillGroup); return; } QueueItem item = (QueueItem) queueItems.iterator().next(); Call call = callService.getCallByUuid(item.getCallUuid()); if (call == null || call.getState() == CallState.HANGUP) { // 呼叫已结束,移除队列 redisTemplate.opsForZSet().remove(queueKey, item); tryAssign(skillGroup); // 继续处理下一个 return; } // 选择坐席 Agent selectedAgent = selectAgent(availableAgents, call); if (selectedAgent != null) { // 从队列移除 redisTemplate.opsForZSet().remove(queueKey, item); // 分配呼叫 assignCallToAgent(call, selectedAgent); } } /** * 选择坐席(多策略) */ private Agent selectAgent(List agents, Call call) { if (agents.isEmpty()) { return null; } // 策略1: 历史服务优先 Agent lastAgent = findLastServingAgent(call.getCallerNumber(), agents); if (lastAgent != null) { log.info("历史服务优先: 客户{}分配给上次服务坐席{}", call.getCallerNumber(), lastAgent.getId()); return lastAgent; } // 策略2: 最长空闲优先 Agent longestIdleAgent = findLongestIdleAgent(agents); if (longestIdleAgent != null) { log.info("最长空闲优先: 选择坐席{}", longestIdleAgent.getId()); return longestIdleAgent; } // 策略3: 轮询 Agent roundRobinAgent = selectByRoundRobin(agents, call.getSkillGroup()); log.info("轮询分配: 选择坐席{}", roundRobinAgent.getId()); return roundRobinAgent; } /** * 查找上次服务该客户的坐席 */ private Agent findLastServingAgent(String customerNumber, List agents) { String lastAgentId = (String) redisTemplate.opsForHash() .get(AGENT_CUSTOMER_MAP_KEY, customerNumber); if (lastAgentId != null) { return agents.stream() .filter(a -> a.getId().equals(lastAgentId)) .findFirst() .orElse(null); } return null; } /** * 查找空闲时间最长的坐席 */ private Agent findLongestIdleAgent(List agents) { return agents.stream() .max(Comparator.comparing(agent -> { String key = AGENT_IDLE_TIME_KEY + agent.getId(); Long idleTime = (Long) redisTemplate.opsForValue().get(key); return idleTime != null ? idleTime : 0L; })) .orElse(null); } /** * 轮询选择坐席 */ private Agent selectByRoundRobin(List agents, String skillGroup) { int currentIndex = roundRobinIndex.getOrDefault(skillGroup, 0); int nextIndex = (currentIndex + 1) % agents.size(); roundRobinIndex.put(skillGroup, nextIndex); return agents.get(currentIndex); } /** * 分配呼叫给坐席 */ private void assignCallToAgent(Call call, Agent agent) { log.info("分配呼叫: uuid={} -> agent={}", call.getUuid(), agent.getId()); // 更新坐席状态 agentService.updateAgentState(agent.getId(), AgentState.RINGING); // 更新呼叫信息 call.setAgentId(agent.getId()); call.setAgentExtension(agent.getExtension()); callService.updateCall(call); // 将呼叫转接到坐席分机 // 方式1: 使用uuid_transfer eslManager.sendSyncApiCommand("uuid_transfer", call.getUuid() + " " + agent.getExtension() + " XML default"); // 方式2: 使用originate + bridge(更灵活) // String bridgeCmd = String.format( // "{origination_uuid=%s,agent_id=%s}user/%s", // UUID.randomUUID(), agent.getId(), agent.getExtension()); // eslManager.sendSyncApiCommand("uuid_broadcast", // call.getUuid() + " bridge::" + bridgeCmd + " both"); // 记录坐席-客户关系(用于历史服务优先) redisTemplate.opsForHash().put(AGENT_CUSTOMER_MAP_KEY, call.getCallerNumber(), agent.getId()); } /** * 通话结束处理 */ public void onCallEnd(String agentId, Call call) { log.info("通话结束: agent={}, call={}", agentId, call.getUuid()); // 更新坐席状态为后处理 agentService.updateAgentState(agentId, AgentState.WRAP_UP); // 设置后处理超时(30秒后自动置闲) // 由定时任务处理 } /** * 坐席置闲时记录空闲时间 */ public void onAgentReady(String agentId, String skillGroup) { String key = AGENT_IDLE_TIME_KEY + agentId; redisTemplate.opsForValue().set(key, System.currentTimeMillis()); // 尝试分配队列中的呼叫 tryAssign(skillGroup); } /** * 定时处理队列 */ @Scheduled(fixedDelay = 1000) public void processQueues() { // 获取所有技能组 Set skillGroups = agentService.getAllSkillGroups(); for (String skillGroup : skillGroups) { tryAssign(skillGroup); } } /** * 获取队列状态 */ public QueueStatus getQueueStatus(String skillGroup) { String queueKey = QUEUE_KEY_PREFIX + skillGroup; QueueStatus status = new QueueStatus(); status.setSkillGroup(skillGroup); status.setWaitingCount(redisTemplate.opsForZSet().size(queueKey)); status.setAvailableAgents(agentService.getAvailableAgents(skillGroup).size()); // 计算最长等待时间 Set items = redisTemplate.opsForZSet().range(queueKey, 0, 0); if (items != null && !items.isEmpty()) { QueueItem firstItem = (QueueItem) items.iterator().next(); long waitSeconds = java.time.Duration.between( firstItem.getEnqueueTime(), LocalDateTime.now()).getSeconds(); status.setLongestWaitTime(waitSeconds); } return status; } /** * 溢出处理 * 当技能组无可用坐席且等待时间超过阈值时,溢出到其他技能组 */ @Scheduled(fixedDelay = 5000) public void handleOverflow() { // 获取溢出配置 Map overflowConfig = getOverflowConfig(); for (Map.Entry entry : overflowConfig.entrySet()) { String sourceGroup = entry.getKey(); String targetGroup = entry.getValue(); String queueKey = QUEUE_KEY_PREFIX + sourceGroup; Set items = redisTemplate.opsForZSet().range(queueKey, 0, -1); if (items == null || items.isEmpty()) { continue; } // 检查是否需要溢出 List sourceAgents = agentService.getAvailableAgents(sourceGroup); List targetAgents = agentService.getAvailableAgents(targetGroup); if (sourceAgents.isEmpty() && !targetAgents.isEmpty()) { for (Object obj : items) { QueueItem item = (QueueItem) obj; long waitSeconds = java.time.Duration.between( item.getEnqueueTime(), LocalDateTime.now()).getSeconds(); // 等待超过60秒,溢出 if (waitSeconds > 60) { log.info("溢出处理: uuid={}, {}->{}", item.getCallUuid(), sourceGroup, targetGroup); // 移出原队列 redisTemplate.opsForZSet().remove(queueKey, item); // 加入目标队列 item.setSkillGroup(targetGroup); String targetQueueKey = QUEUE_KEY_PREFIX + targetGroup; double score = item.getPriority() * 1000000000L + System.currentTimeMillis() / 1000; redisTemplate.opsForZSet().add(targetQueueKey, item, score); // 尝试分配 tryAssign(targetGroup); } } } } } private Map getOverflowConfig() { // 配置:源技能组 -> 目标技能组 Map config = new HashMap<>(); config.put("rescue_group", "default_group"); config.put("housekeeping_group", "default_group"); config.put("driver_group", "default_group"); return config; } } ``` #### 3.2.7 坐席服务 ```java package com.callcenter.service; import com.callcenter.esl.EslConnectionManager; import com.callcenter.model.*; import com.callcenter.websocket.EventPushService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; /** * 坐席服务 */ @Slf4j @Service @RequiredArgsConstructor public class AgentService { private final RedisTemplate redisTemplate; private final EslConnectionManager eslManager; private final EventPushService eventPushService; private final AcdService acdService; private static final String AGENT_KEY_PREFIX = "agent:"; private static final String AGENT_SET_KEY = "agents:all"; private static final String SKILL_GROUP_AGENTS_KEY = "skillgroup:agents:"; /** * 坐席签入 */ public void signIn(String agentId, String extension, List skillGroups) { log.info("坐席签入: agentId={}, extension={}, skills={}", agentId, extension, skillGroups); // 检查分机是否注册 String regStatus = eslManager.sendSyncApiCommand("sofia_contact", "internal/" + extension + "@${domain}"); if (regStatus.contains("error")) { throw new RuntimeException("分机未注册: " + extension); } Agent agent = new Agent(); agent.setId(agentId); agent.setExtension(extension); agent.setSkillGroups(skillGroups); agent.setState(AgentState.NOT_READY); agent.setSignInTime(LocalDateTime.now()); // 保存坐席信息 String key = AGENT_KEY_PREFIX + agentId; redisTemplate.opsForValue().set(key, agent); // 加入坐席集合 redisTemplate.opsForSet().add(AGENT_SET_KEY, agentId); // 加入技能组 for (String skillGroup : skillGroups) { redisTemplate.opsForSet().add(SKILL_GROUP_AGENTS_KEY + skillGroup, agentId); } // 推送状态变更 eventPushService.pushAgentState(agent); } /** * 坐席签出 */ public void signOut(String agentId) { log.info("坐席签出: agentId={}", agentId); Agent agent = getAgent(agentId); if (agent == null) { return; } // 检查是否在通话中 if (agent.getState() == AgentState.BUSY || agent.getState() == AgentState.RINGING) { throw new RuntimeException("坐席正在通话中,无法签出"); } // 从技能组移除 for (String skillGroup : agent.getSkillGroups()) { redisTemplate.opsForSet().remove(SKILL_GROUP_AGENTS_KEY + skillGroup, agentId); } // 从坐席集合移除 redisTemplate.opsForSet().remove(AGENT_SET_KEY, agentId); // 删除坐席信息 String key = AGENT_KEY_PREFIX + agentId; redisTemplate.delete(key); // 推送签出事件 agent.setState(AgentState.OFFLINE); eventPushService.pushAgentState(agent); } /** * 坐席置闲 */ public void ready(String agentId) { log.info("坐席置闲: agentId={}", agentId); Agent agent = getAgent(agentId); if (agent == null) { throw new RuntimeException("坐席不存在"); } if (agent.getState() == AgentState.BUSY) { throw new RuntimeException("坐席正在通话中"); } updateAgentState(agentId, AgentState.READY); // 通知ACD有坐席可用 for (String skillGroup : agent.getSkillGroups()) { acdService.onAgentReady(agentId, skillGroup); } } /** * 坐席置忙 */ public void notReady(String agentId, String reason) { log.info("坐席置忙: agentId={}, reason={}", agentId, reason); Agent agent = getAgent(agentId); if (agent == null) { throw new RuntimeException("坐席不存在"); } if (agent.getState() == AgentState.BUSY) { throw new RuntimeException("坐席正在通话中"); } agent.setNotReadyReason(reason); updateAgentState(agentId, AgentState.NOT_READY); } /** * 坐席小休 */ public void pause(String agentId, String reason) { log.info("坐席小休: agentId={}, reason={}", agentId, reason); Agent agent = getAgent(agentId); if (agent == null) { throw new RuntimeException("坐席不存在"); } if (agent.getState() == AgentState.BUSY) { throw new RuntimeException("坐席正在通话中"); } agent.setPauseReason(reason); updateAgentState(agentId, AgentState.PAUSED); } /** * 更新坐席状态 */ public void updateAgentState(String agentId, AgentState newState) { Agent agent = getAgent(agentId); if (agent == null) { return; } AgentState oldState = agent.getState(); agent.setState(newState); agent.setStateTime(LocalDateTime.now()); // 保存 String key = AGENT_KEY_PREFIX + agentId; redisTemplate.opsForValue().set(key, agent); log.info("坐席状态变更: agent={}, {} -> {}", agentId, oldState, newState); // 推送状态变更 eventPushService.pushAgentState(agent); } /** * 获取坐席信息 */ public Agent getAgent(String agentId) { String key = AGENT_KEY_PREFIX + agentId; return (Agent) redisTemplate.opsForValue().get(key); } /** * 获取技能组可用坐席 */ public List getAvailableAgents(String skillGroup) { Set agentIds = redisTemplate.opsForSet() .members(SKILL_GROUP_AGENTS_KEY + skillGroup); if (agentIds == null || agentIds.isEmpty()) { return Collections.emptyList(); } return agentIds.stream() .map(id -> getAgent((String) id)) .filter(agent -> agent != null && agent.getState() == AgentState.READY) .collect(Collectors.toList()); } /** * 获取所有技能组 */ public Set getAllSkillGroups() { Set keys = redisTemplate.keys(SKILL_GROUP_AGENTS_KEY + "*"); if (keys == null) { return Collections.emptySet(); } return keys.stream() .map(key -> key.replace(SKILL_GROUP_AGENTS_KEY, "")) .collect(Collectors.toSet()); } /** * 获取所有坐席状态(监控大屏) */ public List getAllAgents() { Set agentIds = redisTemplate.opsForSet().members(AGENT_SET_KEY); if (agentIds == null) { return Collections.emptyList(); } return agentIds.stream() .map(id -> getAgent((String) id)) .filter(Objects::nonNull) .collect(Collectors.toList()); } /** * 获取坐席状态统计 */ public Map getAgentStateStats() { return getAllAgents().stream() .collect(Collectors.groupingBy(Agent::getState, Collectors.counting())); } } ``` ### 3.3 Go ESL开发 如果您的团队更熟悉Go语言,以下是Go版本的ESL开发示例: #### 3.3.1 项目结构 ``` callcenter-cti-go/ ├── go.mod ├── go.sum ├── main.go ├── config/ │ └── config.go ├── esl/ │ ├── client.go # ESL客户端 │ ├── event.go # 事件定义 │ └── handler.go # 事件处理器 ├── service/ │ ├── agent.go # 坐席服务 │ ├── call.go # 呼叫服务 │ └── acd.go # ACD服务 ├── model/ │ └── models.go # 数据模型 ├── api/ │ ├── router.go # 路由 │ ├── agent_handler.go # 坐席API │ └── call_handler.go # 呼叫API └── websocket/ └── hub.go # WebSocket推送 ``` #### 3.3.2 ESL客户端 ```go // esl/client.go package esl import ( "bufio" "fmt" "log" "net" "net/textproto" "strconv" "strings" "sync" "time" ) // ESL客户端 type Client struct { conn net.Conn reader *textproto.Reader writer *bufio.Writer mu sync.Mutex connected bool password string eventChan chan *Event handlers []EventHandler reconnect bool } // 事件处理器接口 type EventHandler interface { Handle(event *Event) } // 创建ESL客户端 func NewClient(host string, port int, password string) (*Client, error) { client := &Client{ password: password, eventChan: make(chan *Event, 1000), handlers: make([]EventHandler, 0), reconnect: true, } err := client.connect(host, port) if err != nil { return nil, err } // 启动事件读取协程 go client.readLoop() // 启动事件分发协程 go client.dispatchLoop() return client, nil } // 连接FreeSWITCH func (c *Client) connect(host string, port int) error { addr := fmt.Sprintf("%s:%d", host, port) conn, err := net.DialTimeout("tcp", addr, 10*time.Second) if err != nil { return fmt.Errorf("连接失败: %v", err) } c.conn = conn c.reader = textproto.NewReader(bufio.NewReader(conn)) c.writer = bufio.NewWriter(conn) // 读取欢迎消息 _, err = c.readMessage() if err != nil { return err } // 认证 err = c.auth() if err != nil { return err } c.connected = true log.Println("ESL连接成功") return nil } // 认证 func (c *Client) auth() error { _, err := c.sendCommand(fmt.Sprintf("auth %s", c.password)) if err != nil { return fmt.Errorf("认证失败: %v", err) } return nil } // 发送命令 func (c *Client) sendCommand(command string) (*Message, error) { c.mu.Lock() defer c.mu.Unlock() _, err := c.writer.WriteString(command + "\n\n") if err != nil { return nil, err } c.writer.Flush() return c.readMessage() } // 发送API命令 func (c *Client) API(command string, args string) (string, error) { cmd := fmt.Sprintf("api %s %s", command, args) msg, err := c.sendCommand(cmd) if err != nil { return "", err } return msg.Body, nil } // 发送后台API命令 func (c *Client) BgAPI(command string, args string) (string, error) { cmd := fmt.Sprintf("bgapi %s %s", command, args) msg, err := c.sendCommand(cmd) if err != nil { return "", err } return msg.GetHeader("Job-UUID"), nil } // 订阅事件 func (c *Client) Subscribe(events ...string) error { eventStr := strings.Join(events, " ") _, err := c.sendCommand(fmt.Sprintf("event plain %s", eventStr)) return err } // 订阅所有事件 func (c *Client) SubscribeAll() error { _, err := c.sendCommand("event plain ALL") return err } // 读取消息 func (c *Client) readMessage() (*Message, error) { msg := &Message{ Headers: make(map[string]string), } // 读取headers for { line, err := c.reader.ReadLine() if err != nil { return nil, err } if line == "" { break } parts := strings.SplitN(line, ": ", 2) if len(parts) == 2 { msg.Headers[parts[0]] = parts[1] } } // 读取body if lenStr := msg.GetHeader("Content-Length"); lenStr != "" { length, _ := strconv.Atoi(lenStr) if length > 0 { body := make([]byte, length) _, err := c.reader.R.Read(body) if err != nil { return nil, err } msg.Body = string(body) } } return msg, nil } // 事件读取循环 func (c *Client) readLoop() { for c.connected { msg, err := c.readMessage() if err != nil { log.Printf("读取消息错误: %v", err) c.connected = false if c.reconnect { c.tryReconnect() } return } contentType := msg.GetHeader("Content-Type") if contentType == "text/event-plain" { event := parseEvent(msg) c.eventChan <- event } } } // 事件分发循环 func (c *Client) dispatchLoop() { for event := range c.eventChan { for _, handler := range c.handlers { go handler.Handle(event) } } } // 尝试重连 func (c *Client) tryReconnect() { for i := 0; i < 10; i++ { time.Sleep(5 * time.Second) log.Printf("尝试重连ESL (第%d次)...", i+1) // TODO: 重新连接 } } // 添加事件处理器 func (c *Client) AddHandler(handler EventHandler) { c.handlers = append(c.handlers, handler) } // 关闭连接 func (c *Client) Close() { c.connected = false c.reconnect = false if c.conn != nil { c.conn.Close() } close(c.eventChan) } // 消息结构 type Message struct { Headers map[string]string Body string } func (m *Message) GetHeader(name string) string { if v, ok := m.Headers[name]; ok { return v } return "" } // 解析事件 func parseEvent(msg *Message) *Event { event := &Event{ Headers: make(map[string]string), } // 解析body中的headers lines := strings.Split(msg.Body, "\n") for _, line := range lines { parts := strings.SplitN(line, ": ", 2) if len(parts) == 2 { // URL解码 value := strings.ReplaceAll(parts[1], "%20", " ") event.Headers[parts[0]] = value } } event.Name = event.GetHeader("Event-Name") event.UUID = event.GetHeader("Unique-ID") return event } ``` #### 3.3.3 事件定义 ```go // esl/event.go package esl // 事件结构 type Event struct { Name string UUID string Headers map[string]string } func (e *Event) GetHeader(name string) string { if v, ok := e.Headers[name]; ok { return v } return "" } // 事件名称常量 const ( EventChannelCreate = "CHANNEL_CREATE" EventChannelAnswer = "CHANNEL_ANSWER" EventChannelBridge = "CHANNEL_BRIDGE" EventChannelUnbridge = "CHANNEL_UNBRIDGE" EventChannelHangup = "CHANNEL_HANGUP" EventChannelHangupComplete = "CHANNEL_HANGUP_COMPLETE" EventChannelState = "CHANNEL_STATE" EventDTMF = "DTMF" EventRecordStart = "RECORD_START" EventRecordStop = "RECORD_STOP" EventCustom = "CUSTOM" ) ``` #### 3.3.4 事件处理器 ```go // esl/handler.go package esl import ( "callcenter/service" "log" ) // 呼叫中心事件处理器 type CallCenterHandler struct { callService *service.CallService agentService *service.AgentService acdService *service.AcdService } func NewCallCenterHandler( callService *service.CallService, agentService *service.AgentService, acdService *service.AcdService, ) *CallCenterHandler { return &CallCenterHandler{ callService: callService, agentService: agentService, acdService: acdService, } } func (h *CallCenterHandler) Handle(event *Event) { switch event.Name { case EventChannelCreate: h.handleChannelCreate(event) case EventChannelAnswer: h.handleChannelAnswer(event) case EventChannelBridge: h.handleChannelBridge(event) case EventChannelHangup: h.handleChannelHangup(event) case EventChannelHangupComplete: h.handleChannelHangupComplete(event) case EventDTMF: h.handleDTMF(event) } } func (h *CallCenterHandler) handleChannelCreate(event *Event) { uuid := event.UUID direction := event.GetHeader("Call-Direction") callerNumber := event.GetHeader("Caller-Caller-ID-Number") calleeNumber := event.GetHeader("Caller-Destination-Number") log.Printf("新呼叫: uuid=%s, direction=%s, caller=%s, callee=%s", uuid, direction, callerNumber, calleeNumber) // 创建呼叫记录 call := &service.Call{ UUID: uuid, Direction: direction, CallerNumber: callerNumber, CalleeNumber: calleeNumber, State: "ringing", } h.callService.SaveCall(call) // 呼入排队 if direction == "inbound" { skillGroup := event.GetHeader("variable_skill_group") call.SkillGroup = skillGroup h.acdService.Enqueue(call) } } func (h *CallCenterHandler) handleChannelAnswer(event *Event) { uuid := event.UUID log.Printf("通话接听: uuid=%s", uuid) call := h.callService.GetCall(uuid) if call != nil { call.State = "answered" h.callService.UpdateCall(call) } } func (h *CallCenterHandler) handleChannelBridge(event *Event) { uuid := event.UUID otherUUID := event.GetHeader("Other-Leg-Unique-ID") log.Printf("通道桥接: uuid=%s, other=%s", uuid, otherUUID) call := h.callService.GetCall(uuid) if call != nil { call.State = "bridged" call.BridgeUUID = otherUUID h.callService.UpdateCall(call) } } func (h *CallCenterHandler) handleChannelHangup(event *Event) { uuid := event.UUID hangupCause := event.GetHeader("Hangup-Cause") log.Printf("通话挂断: uuid=%s, cause=%s", uuid, hangupCause) call := h.callService.GetCall(uuid) if call != nil { call.State = "hangup" call.HangupCause = hangupCause h.callService.UpdateCall(call) // 更新坐席状态 if call.AgentID != "" { h.acdService.OnCallEnd(call.AgentID, call) } } } func (h *CallCenterHandler) handleChannelHangupComplete(event *Event) { uuid := event.UUID billsec := event.GetHeader("variable_billsec") recordingPath := event.GetHeader("variable_recording_path") log.Printf("通话结束: uuid=%s, billsec=%s, recording=%s", uuid, billsec, recordingPath) call := h.callService.GetCall(uuid) if call != nil { call.Billsec = billsec call.RecordingPath = recordingPath h.callService.UpdateCall(call) } } func (h *CallCenterHandler) handleDTMF(event *Event) { uuid := event.UUID digit := event.GetHeader("DTMF-Digit") log.Printf("DTMF按键: uuid=%s, digit=%s", uuid, digit) } ``` --- ## 第四部分 呼叫中心场景开发实践 ### 4.1 预测式外呼实现 ```java package com.callcenter.service; import com.callcenter.esl.EslConnectionManager; import com.callcenter.model.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** * 预测式外呼服务 */ @Slf4j @Service @RequiredArgsConstructor public class PredictiveDialerService { private final EslConnectionManager eslManager; private final AgentService agentService; private final CallService callService; // 外呼任务 private final Map activeTasks = new ConcurrentHashMap<>(); // 外呼线程池 private final ExecutorService dialerExecutor = Executors.newFixedThreadPool(10); /** * 创建外呼任务 */ public String createTask(DialTaskRequest request) { String taskId = UUID.randomUUID().toString(); DialTask task = new DialTask(); task.setId(taskId); task.setName(request.getName()); task.setSkillGroup(request.getSkillGroup()); task.setCallerIdNumber(request.getCallerIdNumber()); task.setNumbers(new LinkedBlockingQueue<>(request.getNumbers())); task.setMaxConcurrent(request.getMaxConcurrent()); task.setState(TaskState.CREATED); task.setStats(new DialStats()); activeTasks.put(taskId, task); log.info("创建外呼任务: taskId={}, name={}, numbers={}", taskId, request.getName(), request.getNumbers().size()); return taskId; } /** * 启动任务 */ public void startTask(String taskId) { DialTask task = activeTasks.get(taskId); if (task == null) { throw new RuntimeException("任务不存在"); } task.setState(TaskState.RUNNING); log.info("启动外呼任务: {}", taskId); // 启动外呼循环 dialerExecutor.submit(() -> dialLoop(task)); } /** * 暂停任务 */ public void pauseTask(String taskId) { DialTask task = activeTasks.get(taskId); if (task != null) { task.setState(TaskState.PAUSED); log.info("暂停外呼任务: {}", taskId); } } /** * 恢复任务 */ public void resumeTask(String taskId) { DialTask task = activeTasks.get(taskId); if (task != null && task.getState() == TaskState.PAUSED) { task.setState(TaskState.RUNNING); dialerExecutor.submit(() -> dialLoop(task)); log.info("恢复外呼任务: {}", taskId); } } /** * 停止任务 */ public void stopTask(String taskId) { DialTask task = activeTasks.get(taskId); if (task != null) { task.setState(TaskState.STOPPED); log.info("停止外呼任务: {}", taskId); } } /** * 外呼循环 */ private void dialLoop(DialTask task) { log.info("开始外呼循环: taskId={}", task.getId()); while (task.getState() == TaskState.RUNNING && !task.getNumbers().isEmpty()) { try { // 计算应该发起的呼叫数 int dialCount = calculateDialCount(task); if (dialCount <= 0) { // 没有空闲坐席,等待 Thread.sleep(1000); continue; } // 发起外呼 for (int i = 0; i < dialCount; i++) { String number = task.getNumbers().poll(); if (number == null) { break; } dial(task, number); } // 短暂等待,控制拨打速率 Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } catch (Exception e) { log.error("外呼异常: {}", e.getMessage(), e); } } // 任务完成 if (task.getNumbers().isEmpty()) { task.setState(TaskState.COMPLETED); } log.info("外呼循环结束: taskId={}, state={}", task.getId(), task.getState()); } /** * 计算应该发起的呼叫数(预测算法) */ private int calculateDialCount(DialTask task) { // 获取可用坐席数 List availableAgents = agentService.getAvailableAgents(task.getSkillGroup()); int availableAgentCount = availableAgents.size(); if (availableAgentCount == 0) { return 0; } // 当前正在拨打的数量 int currentDialing = task.getStats().getCurrentDialing().get(); // 历史接通率(动态计算) double connectRate = calculateConnectRate(task); // 预测式算法: // 目标:让坐席接到电话时,刚好有客户接通 // 预测需要拨打数 = 空闲坐席数 / 接通率 * 系数 double factor = 1.2; // 超拨系数,避免坐席空闲 int predictedDial = (int) Math.ceil(availableAgentCount / connectRate * factor); // 限制最大并发 int maxAllowed = task.getMaxConcurrent() - currentDialing; return Math.min(predictedDial, maxAllowed); } /** * 计算接通率 */ private double calculateConnectRate(DialTask task) { DialStats stats = task.getStats(); int total = stats.getTotalDialed().get(); int connected = stats.getConnected().get(); if (total < 10) { // 样本太少,使用默认值 return 0.3; } double rate = (double) connected / total; // 限制范围 return Math.max(0.1, Math.min(0.8, rate)); } /** * 发起单个外呼 */ private void dial(DialTask task, String number) { log.info("外呼: taskId={}, number={}", task.getId(), number); task.getStats().getTotalDialed().incrementAndGet(); task.getStats().getCurrentDialing().incrementAndGet(); try { // 构建originate命令 // 先拨打客户,客户接听后转到队列等待坐席 StringBuilder cmd = new StringBuilder(); cmd.append("{"); cmd.append("origination_caller_id_number=").append(task.getCallerIdNumber()).append(","); cmd.append("task_id=").append(task.getId()).append(","); cmd.append("dial_number=").append(number).append(","); cmd.append("dialer_mode=predictive"); cmd.append("}"); // 通过网关外呼 cmd.append("sofia/gateway/carrier_primary/").append(number); // 客户接听后的处理 cmd.append(" &lua(predictive_handler.lua)"); String result = eslManager.sendSyncApiCommand("originate", cmd.toString()); if (!result.startsWith("+OK")) { log.warn("外呼失败: number={}, result={}", number, result); task.getStats().getFailed().incrementAndGet(); } } catch (Exception e) { log.error("外呼异常: number={}", number, e); task.getStats().getFailed().incrementAndGet(); } finally { task.getStats().getCurrentDialing().decrementAndGet(); } } /** * 客户接听回调(由事件触发) */ public void onCustomerAnswer(String uuid, String taskId) { DialTask task = activeTasks.get(taskId); if (task == null) { eslManager.sendSyncApiCommand("uuid_kill", uuid + " NO_ROUTE_DESTINATION"); return; } task.getStats().getConnected().incrementAndGet(); // 查找可用坐席 List agents = agentService.getAvailableAgents(task.getSkillGroup()); if (agents.isEmpty()) { // 无可用坐席,播放等待音乐 eslManager.sendSyncApiCommand("uuid_broadcast", uuid + " ivr/please_wait.wav both"); // 放入等待队列 // TODO: 实现等待逻辑 // 超时处理 task.getStats().getAbandoned().incrementAndGet(); } else { // 转接坐席 Agent agent = agents.get(0); agentService.updateAgentState(agent.getId(), AgentState.RINGING); eslManager.sendSyncApiCommand("uuid_transfer", uuid + " " + agent.getExtension() + " XML default"); task.getStats().getTransferred().incrementAndGet(); } } /** * 获取任务统计 */ public DialStats getTaskStats(String taskId) { DialTask task = activeTasks.get(taskId); return task != null ? task.getStats() : null; } } /** * 外呼任务 */ @Data class DialTask { private String id; private String name; private String skillGroup; private String callerIdNumber; private BlockingQueue numbers; private int maxConcurrent; private TaskState state; private DialStats stats; } /** * 任务状态 */ enum TaskState { CREATED, RUNNING, PAUSED, STOPPED, COMPLETED } /** * 外呼统计 */ @Data class DialStats { private AtomicInteger totalDialed = new AtomicInteger(0); private AtomicInteger currentDialing = new AtomicInteger(0); private AtomicInteger connected = new AtomicInteger(0); private AtomicInteger transferred = new AtomicInteger(0); private AtomicInteger failed = new AtomicInteger(0); private AtomicInteger abandoned = new AtomicInteger(0); } ``` ### 4.2 机器人外呼实现 ```java package com.callcenter.service; import com.callcenter.esl.EslConnectionManager; import com.callcenter.model.*; import com.callcenter.tts.TtsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.*; /** * 机器人外呼服务 * 用于批量通知、回访、满意度调查 */ @Slf4j @Service @RequiredArgsConstructor public class RobotDialerService { private final EslConnectionManager eslManager; private final TtsService ttsService; private final Map activeTasks = new ConcurrentHashMap<>(); private final ExecutorService executor = Executors.newFixedThreadPool(20); /** * 创建机器人外呼任务 */ public String createTask(RobotTaskRequest request) { String taskId = UUID.randomUUID().toString(); RobotTask task = new RobotTask(); task.setId(taskId); task.setType(request.getType()); // NOTIFICATION, SURVEY, CALLBACK task.setNumbers(new LinkedBlockingQueue<>(request.getNumbers())); task.setTtsText(request.getTtsText()); task.setVariables(request.getVariables()); // 每个号码的变量 task.setMaxConcurrent(request.getMaxConcurrent()); task.setRetryTimes(request.getRetryTimes()); task.setCollectDtmf(request.isCollectDtmf()); task.setState(TaskState.CREATED); task.setResults(new ConcurrentHashMap<>()); activeTasks.put(taskId, task); return taskId; } /** * 启动任务 */ public void startTask(String taskId) { RobotTask task = activeTasks.get(taskId); if (task == null) { throw new RuntimeException("任务不存在"); } task.setState(TaskState.RUNNING); executor.submit(() -> robotDialLoop(task)); } /** * 机器人外呼循环 */ private void robotDialLoop(RobotTask task) { log.info("开始机器人外呼: taskId={}", task.getId()); Semaphore semaphore = new Semaphore(task.getMaxConcurrent()); while (task.getState() == TaskState.RUNNING) { String number = task.getNumbers().poll(); if (number == null) { break; } try { semaphore.acquire(); executor.submit(() -> { try { robotCall(task, number); } finally { semaphore.release(); } }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } // 等待所有呼叫完成 try { semaphore.acquire(task.getMaxConcurrent()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } task.setState(TaskState.COMPLETED); log.info("机器人外呼完成: taskId={}", task.getId()); } /** * 单个机器人呼叫 */ private void robotCall(RobotTask task, String number) { log.info("机器人呼叫: taskId={}, number={}", task.getId(), number); RobotCallResult result = new RobotCallResult(); result.setNumber(number); result.setStartTime(System.currentTimeMillis()); try { // 替换TTS文本中的变量 String ttsText = replaceTtsVariables(task.getTtsText(), task.getVariables().get(number)); // 生成语音文件 String audioFile = ttsService.synthesize(ttsText); // 构建外呼命令 StringBuilder cmd = new StringBuilder(); cmd.append("{"); cmd.append("task_id=").append(task.getId()).append(","); cmd.append("dial_number=").append(number).append(","); cmd.append("robot_mode=true").append(","); cmd.append("audio_file=").append(audioFile); cmd.append("}"); cmd.append("sofia/gateway/carrier_primary/").append(number); if (task.isCollectDtmf()) { // 需要收集按键 cmd.append(" &lua(robot_survey.lua)"); } else { // 纯播报 cmd.append(" &playback(").append(audioFile).append(")"); } String apiResult = eslManager.sendSyncApiCommand("originate", cmd.toString()); if (apiResult.startsWith("+OK")) { result.setStatus("SUCCESS"); result.setCallUuid(apiResult.substring(4).trim()); } else { result.setStatus("FAILED"); result.setError(apiResult); // 重试逻辑 handleRetry(task, number); } } catch (Exception e) { log.error("机器人呼叫异常: number={}", number, e); result.setStatus("ERROR"); result.setError(e.getMessage()); handleRetry(task, number); } result.setEndTime(System.currentTimeMillis()); task.getResults().put(number, result); } /** * 替换TTS变量 */ private String replaceTtsVariables(String template, Map vars) { if (vars == null || vars.isEmpty()) { return template; } String result = template; for (Map.Entry entry : vars.entrySet()) { result = result.replace("${" + entry.getKey() + "}", entry.getValue()); } return result; } /** * 处理重试 */ private void handleRetry(RobotTask task, String number) { int retryCount = task.getRetryCount().getOrDefault(number, 0); if (retryCount < task.getRetryTimes()) { task.getRetryCount().put(number, retryCount + 1); task.getNumbers().offer(number); log.info("加入重试队列: number={}, retry={}", number, retryCount + 1); } } /** * 处理DTMF按键结果(由事件触发) */ public void onDtmfResult(String uuid, String taskId, String number, String digit) { RobotTask task = activeTasks.get(taskId); if (task == null) { return; } RobotCallResult result = task.getResults().get(number); if (result != null) { result.setDtmfDigit(digit); log.info("收到按键: taskId={}, number={}, digit={}", taskId, number, digit); } } /** * 获取任务结果 */ public List getTaskResults(String taskId) { RobotTask task = activeTasks.get(taskId); if (task == null) { return Collections.emptyList(); } return new ArrayList<>(task.getResults().values()); } } ``` ### 4.3 班长监控实现 ```java package com.callcenter.service; import com.callcenter.esl.EslConnectionManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** * 班长监控服务 */ @Slf4j @Service @RequiredArgsConstructor public class SupervisorService { private final EslConnectionManager eslManager; /** * 监听坐席通话 * 班长可以听到双方说话,但双方听不到班长 */ public void spy(String supervisorExtension, String targetUuid) { log.info("开始监听: supervisor={}, target={}", supervisorExtension, targetUuid); // 使用eavesdrop应用 // originate user/supervisor &eavesdrop(target_uuid) String cmd = String.format( "{origination_caller_id_name=监听}user/%s &eavesdrop(%s)", supervisorExtension, targetUuid ); String result = eslManager.sendSyncApiCommand("originate", cmd); if (!result.startsWith("+OK")) { throw new RuntimeException("监听失败: " + result); } } /** * 强插通话 * 班长加入通话,三方都可以听到 */ public void barge(String supervisorExtension, String targetUuid) { log.info("强插通话: supervisor={}, target={}", supervisorExtension, targetUuid); // 使用三方会议实现 String confName = "barge_" + targetUuid; // 1. 将目标通话转入会议 eslManager.sendSyncApiCommand("uuid_transfer", targetUuid + " conference:" + confName + "+flags{mute} inline"); // 2. 呼叫班长加入会议 String cmd = String.format( "{origination_caller_id_name=强插}user/%s &conference(%s)", supervisorExtension, confName ); eslManager.sendSyncApiCommand("originate", cmd); } /** * 强拆通话 * 强制挂断坐席通话 */ public void hangup(String targetUuid, String reason) { log.info("强拆通话: target={}, reason={}", targetUuid, reason); eslManager.sendSyncApiCommand("uuid_kill", targetUuid + " " + reason); } /** * 密语(耳语) * 班长对坐席说话,客户听不到 */ public void whisper(String supervisorExtension, String agentUuid) { log.info("密语: supervisor={}, agent={}", supervisorExtension, agentUuid); // 使用eavesdrop的whisper模式 String cmd = String.format( "{origination_caller_id_name=密语}user/%s &eavesdrop(%s whisper)", supervisorExtension, agentUuid ); eslManager.sendSyncApiCommand("originate", cmd); } /** * 获取实时通话列表 */ public String getActiveCalls() { return eslManager.sendSyncApiCommand("show", "calls"); } /** * 获取坐席通道信息 */ public String getChannelInfo(String uuid) { return eslManager.sendSyncApiCommand("uuid_dump", uuid); } } ``` --- ## 第五部分 高可用部署方案 ### 5.1 主备架构 ``` ┌─────────────────┐ │ Keepalived │ │ VIP漂移管理 │ └────────┬────────┘ │ ┌───────────────────────────┴───────────────────────────┐ │ VIP: 10.10.1.100 │ │ (SIP/ESL统一入口) │ └───────────────────────────┬───────────────────────────┘ │ ┌─────────────────────┴─────────────────────┐ │ │ ▼ ▼ ┌────────────────────┐ ┌────────────────────┐ │ Master节点 │ 心跳检测 │ Backup节点 │ │ 10.10.1.10 │◄──────────────────►│ 10.10.1.11 │ ├────────────────────┤ ├────────────────────┤ │ FreeSWITCH │ │ FreeSWITCH │ │ CTI中间件 │ │ CTI中间件 │ │ API服务 │ │ API服务 │ └────────────────────┘ └────────────────────┘ │ │ │ 共享存储 / 数据同步 │ │ │ └─────────────────────┬─────────────────────┘ │ ┌──────────────────────────┼──────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ MySQL 主从 │ │ Redis Sentinel │ │ NAS存储 │ │ 10.10.2.10/11 │ │ 10.10.2.20-22 │ │ 10.10.2.30 │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` ### 5.2 Keepalived配置 **Master节点配置:** ```bash # /etc/keepalived/keepalived.conf (Master) global_defs { router_id FS_MASTER } # 检测FreeSWITCH状态的脚本 vrrp_script check_freeswitch { script "/etc/keepalived/check_freeswitch.sh" interval 2 weight -20 fall 3 rise 2 } vrrp_instance VI_FS { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass your_password } virtual_ipaddress { 10.10.1.100/24 dev eth0 } track_script { check_freeswitch } notify_master "/etc/keepalived/notify_master.sh" notify_backup "/etc/keepalived/notify_backup.sh" notify_fault "/etc/keepalived/notify_fault.sh" } ``` **检测脚本:** ```bash #!/bin/bash # /etc/keepalived/check_freeswitch.sh # 检测FreeSWITCH进程 if ! pgrep -x "freeswitch" > /dev/null; then exit 1 fi # 检测ESL端口 if ! nc -z localhost 8021; then exit 1 fi # 检测SIP端口 if ! nc -z localhost 5060; then exit 1 fi # 执行简单API命令测试 result=$(fs_cli -x "status" 2>/dev/null) if [[ $? -ne 0 ]]; then exit 1 fi exit 0 ``` **主节点切换通知脚本:** ```bash #!/bin/bash # /etc/keepalived/notify_master.sh # 记录日志 echo "$(date): Becoming MASTER" >> /var/log/keepalived-state.log # 启动FreeSWITCH(如果未运行) systemctl start freeswitch # 启动CTI服务 systemctl start callcenter-cti # 发送告警通知 curl -X POST "http://your-alert-api/notify" \ -d "message=FreeSWITCH切换到Master节点" ``` ### 5.3 配置同步方案 ```bash #!/bin/bash # sync_config.sh - 配置同步脚本 MASTER_HOST="10.10.1.10" BACKUP_HOST="10.10.1.11" FS_CONF_DIR="/usr/local/freeswitch/conf" # 使用rsync同步配置 sync_config() { rsync -avz --delete \ ${FS_CONF_DIR}/ \ ${BACKUP_HOST}:${FS_CONF_DIR}/ # 通知备机重载配置 ssh ${BACKUP_HOST} "fs_cli -x 'reloadxml'" } # 监控配置变化 inotifywait -m -r -e modify,create,delete ${FS_CONF_DIR} | while read path action file; do echo "配置变化: $path$file ($action)" sync_config done ``` ### 5.4 FreeSWITCH集群配置 对于需要更高并发的场景,可以部署FreeSWITCH集群: ```xml ``` --- ## 第六部分 性能优化指南 ### 6.1 FreeSWITCH优化 **系统级优化:** ```bash # /etc/security/limits.conf freeswitch soft nofile 65536 freeswitch hard nofile 65536 freeswitch soft core unlimited freeswitch hard core unlimited # /etc/sysctl.conf net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.core.rmem_default = 1048576 net.core.wmem_default = 1048576 net.core.netdev_max_backlog = 30000 net.ipv4.tcp_max_syn_backlog = 10240 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 ``` **FreeSWITCH配置优化:** ```xml ``` ### 6.2 CTI中间件优化 ```java // 连接池配置 @Configuration public class ConnectionPoolConfig { @Bean public ThreadPoolTaskExecutor eslEventExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(32); executor.setQueueCapacity(10000); executor.setThreadNamePrefix("esl-event-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useClusterServers() .addNodeAddress("redis://10.10.2.20:6379") .addNodeAddress("redis://10.10.2.21:6379") .addNodeAddress("redis://10.10.2.22:6379") .setMasterConnectionPoolSize(64) .setSlaveConnectionPoolSize(64); return Redisson.create(config); } } ``` ### 6.3 监控指标 ```yaml # prometheus配置 scrape_configs: - job_name: 'freeswitch' static_configs: - targets: ['10.10.1.10:9282', '10.10.1.11:9282'] - job_name: 'callcenter-cti' static_configs: - targets: ['10.10.1.10:8080', '10.10.1.11:8080'] metrics_path: '/actuator/prometheus' ``` **关键监控指标:** | 指标 | 说明 | 阈值 | |------|------|------| | freeswitch_sessions_active | 当前活跃会话数 | <80%最大值 | | freeswitch_sessions_per_second | 每秒新建会话数 | <100 | | freeswitch_cpu_usage | CPU使用率 | <70% | | freeswitch_memory_usage | 内存使用率 | <80% | | esl_event_queue_size | 事件队列大小 | <5000 | | acd_queue_wait_time | 平均等待时间 | <30s | | agent_utilization | 坐席利用率 | 目标>70% | --- ## 总结 本文档详细介绍了FreeSWITCH的技术架构和ESL开发实践,涵盖了: 1. **核心概念**:Channel、Session、Event等基础概念 2. **模块详解**:mod_sofia、Dialplan、ESL等核心模块 3. **ESL开发**:Java和Go两种语言的开发示例 4. **场景实践**:ACD、预测式外呼、机器人外呼、班长监控 5. **高可用部署**:主备架构、Keepalived配置 6. **性能优化**:系统优化、应用优化、监控指标 建议开发团队在正式开发前: - 搭建FreeSWITCH测试环境熟悉操作 - 仔细研读FreeSWITCH官方文档 - 从简单场景(如点击拨号)开始,逐步实现复杂功能 - 重视测试,特别是高可用切换测试和压力测试 如有任何问题,欢迎继续交流! ### USER 给出一个粗略的开发方案 ### ASSISTANT # 呼叫中心系统开发方案 --- **文档版本**:V1.0 **编制日期**:2024年1月 **项目周期**:4个月(17周) --- ## 一、开发方案概述 ### 1.1 项目背景回顾 | 项目要素 | 内容 | |---------|------| | 业务范围 | 道路救援、家政服务、代驾服务 | | 坐席规模 | 150坐席 | | 日话务量 | 50,000通 | | 技术路线 | 自研(基于FreeSWITCH) | | 部署方式 | 私有化 + 主备高可用 | ### 1.2 开发目标 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 系统能力目标 │ ├─────────────────────────────────────────────────────────────────────────┤ │ ✓ 150坐席并发 ✓ 120路并发通话 ✓ 99.9%可用性 │ │ ✓ 7×24小时服务 ✓ 故障30秒内切换 ✓ API响应<200ms │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 二、系统架构设计 ### 2.1 整体架构 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 外部系统 │ │ CRM系统 业务系统 阿里云语音 │ └──────────┬───────────────────────┬────────────────────┬─────────────────┘ │ │ │ │ REST API / WebSocket │ │ │ │ ┌──────────▼───────────────────────▼────────────────────▼─────────────────┐ │ CTI中间件层 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ API网关 │ │ 坐席服务 │ │ 呼叫服务 │ │ 外呼引擎 │ │ │ │ (Gateway) │ │ (Agent) │ │ (Call) │ │ (Dialer) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ ACD服务 │ │ 队列服务 │ │ 监控服务 │ │ 机器人服务 │ │ │ │ (ACD) │ │ (Queue) │ │ (Monitor) │ │ (Robot) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ ESL连接池 │ │ 事件分发器 │ │ │ │ (ESL Pool) │ │ (Dispatcher)│ │ │ └──────┬──────┘ └──────┬──────┘ │ └─────────┼────────────────┼──────────────────────────────────────────────┘ │ │ │ ESL协议 │ │ │ ┌─────────▼────────────────▼──────────────────────────────────────────────┐ │ FreeSWITCH 媒体层 │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │mod_sofia│ │mod_dptools│ │mod_record│ │mod_lua │ │mod_event│ │ │ │ │ │ (SIP) │ │(Dialplan)│ │ (录音) │ │ (IVR) │ │ (ESL) │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ FreeSWITCH Core │ └─────────────────────────────────┬───────────────────────────────────────┘ │ SIP Trunk │ ┌─────────────────────────────────▼───────────────────────────────────────┐ │ 运营商网络 │ │ 400号码 / SIP中继线路 │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 技术选型 | 层级 | 组件 | 技术选型 | 说明 | |------|------|---------|------| | **媒体层** | 软交换 | FreeSWITCH 1.10.x | 开源、稳定、功能完善 | | | SIP协议 | mod_sofia | FreeSWITCH内置 | | | IVR脚本 | Lua | 灵活、性能好 | | **中间件层** | 开发语言 | Java 17 + Spring Boot 3 | 团队熟悉、生态成熟 | | | ESL客户端 | 自研(基于Netty) | 更好的连接管理 | | | API框架 | Spring WebFlux | 响应式、高性能 | | | WebSocket | Spring WebSocket | 事件实时推送 | | **数据层** | 关系数据库 | MySQL 8.0 | 主从复制、读写分离 | | | 缓存 | Redis 7.0 | Sentinel高可用 | | | 消息队列 | RabbitMQ | 异步解耦 | | | 文件存储 | MinIO / NAS | 录音存储 | | **前端** | 软电话条 | Vue 3 + TypeScript | 组件化、类型安全 | | | 监控大屏 | Vue 3 + ECharts | 数据可视化 | | **运维** | 容器化 | Docker + Docker Compose | 简化部署 | | | 监控 | Prometheus + Grafana | 指标监控 | | | 日志 | ELK Stack | 日志收集分析 | | **外部服务** | ASR/TTS | 阿里云智能语音 | 按需调用 | | | NLU | 指定对话平台 | 意图识别 | ### 2.3 模块划分 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 系统模块划分 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ callcenter-platform │ │ │ │ (父工程) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────┼──────────────────────────┐ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ common │ │ esl │ │ core │ │ api │ │ web │ │ │ │ (公共) │ │(ESL封装)│ │(核心业务)│ │(REST API)│ │(前端) │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ │ └──────────────┴───────────┴───────────┴──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ gateway │ │ │ │ (启动入口) │ │ │ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **模块职责:** | 模块 | 职责 | 主要内容 | |------|------|---------| | **common** | 公共组件 | 工具类、常量、异常、通用模型 | | **esl** | ESL封装 | ESL连接管理、命令封装、事件解析 | | **core** | 核心业务 | 坐席管理、呼叫控制、ACD、队列、外呼、录音 | | **api** | REST接口 | 对外API、WebSocket推送 | | **web** | 前端工程 | 软电话条、监控大屏 | | **gateway** | 应用入口 | 配置、启动类 | --- ## 三、功能模块设计 ### 3.1 模块清单与优先级 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 功能模块优先级 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ P0 - 核心功能(必须完成) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ ☐ ESL连接管理 ☐ 坐席签入签出 ☐ 呼入呼出控制 │ │ │ │ ☐ ACD话务分配 ☐ 技能组管理 ☐ 排队等待 │ │ │ │ ☐ 全程录音 ☐ 软电话条 ☐ API接口 │ │ │ │ ☐ 事件推送 ☐ CRM集成 ☐ 基础IVR │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ P1 - 重要功能(应该完成) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ ☐ 预测式外呼 ☐ 机器人外呼 ☐ 语音识别IVR │ │ │ │ ☐ 呼入机器人 ☐ 班长监控 ☐ 实时监控大屏 │ │ │ │ ☐ 溢出策略 ☐ 主备高可用 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ P2 - 增强功能(尽量完成) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ ☐ VIP优先 ☐ 黑名单 ☐ 满意度调查 │ │ │ │ ☐ 多轮对话 ☐ 话务预测 ☐ 密语功能 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 3.2 核心模块设计 #### 3.2.1 ESL模块 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ ESL模块设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ EslConnectionManager │ │ │ │ (连接管理器) │ │ │ ├───────────────────────────────────────────────────────────────────┤ │ │ │ - connect() 建立ESL连接 │ │ │ │ - disconnect() 断开连接 │ │ │ │ - reconnect() 重连机制 │ │ │ │ - heartbeat() 心跳检测 │ │ │ │ - getConnection() 获取连接 │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ EslCommandExecutor │ │ │ │ (命令执行器) │ │ │ ├───────────────────────────────────────────────────────────────────┤ │ │ │ - api(cmd, args) 同步API命令 │ │ │ │ - bgapi(cmd, args) 异步API命令 │ │ │ │ - sendMsg(uuid, app) 发送消息 │ │ │ │ - originate(...) 发起呼叫 │ │ │ │ - bridge(uuid1, uuid2) 桥接通道 │ │ │ │ - hangup(uuid) 挂断通话 │ │ │ │ - transfer(uuid, dest) 转接 │ │ │ │ - hold/unhold(uuid) 保持/取回 │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ EslEventDispatcher │ │ │ │ (事件分发器) │ │ │ ├───────────────────────────────────────────────────────────────────┤ │ │ │ - subscribe(events) 订阅事件 │ │ │ │ - addHandler(handler) 注册处理器 │ │ │ │ - dispatch(event) 分发事件 │ │ │ │ - filter(uuid) 过滤事件 │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 关键事件处理: │ │ ┌────────────────┬────────────────────────────────────────────────┐ │ │ │ CHANNEL_CREATE │ → 创建呼叫记录,呼入触发ACD排队 │ │ │ │ CHANNEL_ANSWER │ → 更新状态为已接听,开始计时 │ │ │ │ CHANNEL_BRIDGE │ → 更新状态为桥接,记录桥接信息 │ │ │ │ CHANNEL_HANGUP │ → 更新坐席状态,生成话单 │ │ │ │ DTMF │ → IVR按键处理,满意度调查收集 │ │ │ └────────────────┴────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### 3.2.2 坐席管理模块 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 坐席管理模块设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 坐席状态机: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ OFFLINE │ ◄────────────────────────┐ │ │ │ │ └────┬────┘ │ │ │ │ │ │ signIn │ signOut │ │ │ │ ▼ │ │ │ │ │ ┌─────────┐ ready ┌────┴────┐ │ │ │ │ ┌───►│NOT_READY│─────────────────────►│ READY │◄───┐ │ │ │ │ │ └─────────┘ └────┬────┘ │ │ │ │ │ │ ▲ │ │ │ │ │ │ wrapUp │ │ notReady │ assign │ │ │ │ │ timeout│ │ ▼ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │ │ │ PAUSED │ │ RINGING │ │ │ │ │ │ │ │ (小休) │ └────┬────┘ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ │ answer │ │ │ │ │ │ ▼ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ │ │ BUSY │ │ │ │ │ │ │ │ (通话中) │ │ │ │ │ │ │ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ hangup │ │ │ │ │ │ ▼ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ └────────────────────────────────────│ WRAP_UP │────┘ │ │ │ │ │ (后处理) │ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 数据模型: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Agent { │ │ │ │ id: String // 坐席ID │ │ │ │ name: String // 坐席姓名 │ │ │ │ extension: String // 分机号 │ │ │ │ skillGroups: List // 技能组列表 │ │ │ │ state: AgentState // 当前状态 │ │ │ │ stateTime: DateTime // 状态开始时间 │ │ │ │ currentCallUuid: String // 当前通话UUID │ │ │ │ signInTime: DateTime // 签入时间 │ │ │ │ } │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 存储设计(Redis): │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ agent:{agentId} → Hash 坐席详细信息 │ │ │ │ agents:online → Set 在线坐席ID集合 │ │ │ │ skillgroup:{name}:agents → Set 技能组坐席集合 │ │ │ │ agent:{agentId}:idle_time → String 空闲开始时间 │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### 3.2.3 ACD模块 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ ACD模块设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 分配流程: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 来电 ──► IVR ──► 确定技能组 ──► 入队 ──► 分配 ──► 振铃 ──► 接听 │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌──────────┐ │ │ │ │ │ 排队等待 │ │ │ │ │ │ 播放音乐 │ │ │ │ │ └──────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 分配策略(按优先级): │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 1. 历史服务优先 │ │ │ │ └─► 查找该客户上次服务的坐席,若空闲则分配 │ │ │ │ │ │ │ │ 2. 最长空闲优先 │ │ │ │ └─► 选择空闲时间最长的坐席 │ │ │ │ │ │ │ │ 3. 轮询分配 │ │ │ │ └─► 按顺序轮流分配 │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 溢出策略: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 条件:技能组无空闲坐席 + 等待时间 > 阈值(如60秒) │ │ │ │ │ │ │ │ 溢出配置: │ │ │ │ ┌─────────────┬─────────────┬───────────┐ │ │ │ │ │ 源技能组 │ 目标技能组 │ 等待阈值 │ │ │ │ │ ├─────────────┼─────────────┼───────────┤ │ │ │ │ │ rescue │ default │ 60秒 │ │ │ │ │ │ housekeeping│ default │ 90秒 │ │ │ │ │ │ driver │ default │ 60秒 │ │ │ │ │ └─────────────┴─────────────┴───────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 队列数据结构(Redis Sorted Set): │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ queue:{skillGroup} │ │ │ │ ┌──────────────────────────────────────────────────────────────┐│ │ │ │ │ Score = 优先级 × 10^9 + 入队时间戳 ││ │ │ │ │ Member = QueueItem(callUuid, callerNumber, enqueueTime...) ││ │ │ │ └──────────────────────────────────────────────────────────────┘│ │ │ │ │ │ │ │ 优先级越小越优先,同优先级按入队时间排序 │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### 3.2.4 呼叫控制模块 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 呼叫控制模块设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 呼叫状态机: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ CREATED │────►│ RINGING │────►│ ANSWERED│────►│ BRIDGED │ │ │ │ │ └─────────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ │ │ HELD │ │ │ │ │ │ │ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ │ │ HANGUP │ │ │ │ │ └────────────────────────────────────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 核心操作: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 点击外呼: │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ 1. 检查坐席状态(必须READY) │ │ │ │ │ │ 2. 更新坐席状态为BUSY │ │ │ │ │ │ 3. originate呼叫坐席分机 │ │ │ │ │ │ 4. 坐席接听后bridge呼叫客户 │ │ │ │ │ │ 5. 开始录音 │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ 转接: │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ 盲转:uuid_transfer直接转接 │ │ │ │ │ │ 咨询转:先hold,originate咨询方,确认后bridge │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ 三方通话: │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ 1. 创建会议室 conference │ │ │ │ │ │ 2. 将原通话双方加入会议 │ │ │ │ │ │ 3. originate第三方加入会议 │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 数据模型: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Call { │ │ │ │ uuid: String // 呼叫唯一标识 │ │ │ │ direction: Enum // INBOUND/OUTBOUND │ │ │ │ callerNumber: String // 主叫号码 │ │ │ │ calleeNumber: String // 被叫号码 │ │ │ │ state: CallState // 呼叫状态 │ │ │ │ agentId: String // 服务坐席 │ │ │ │ skillGroup: String // 技能组 │ │ │ │ createTime: DateTime // 创建时间 │ │ │ │ answerTime: DateTime // 接听时间 │ │ │ │ hangupTime: DateTime // 挂断时间 │ │ │ │ duration: Long // 通话时长(秒) │ │ │ │ recordingPath: String // 录音路径 │ │ │ │ hangupCause: String // 挂断原因 │ │ │ │ } │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### 3.2.5 外呼模块 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 外呼模块设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 预测式外呼流程: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 业务系统 ──► 创建任务 ──► 导入号码 ──► 启动任务 │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌───────────────┐ │ │ │ │ │ 外呼循环 │ │ │ │ │ └───────┬───────┘ │ │ │ │ │ │ │ │ │ ┌───────────────────────────────────┴───────────────┐ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ 计算拨打数 │───►│ 批量拨打 │───►│ 接通转坐席 │ │ │ │ │ │ │(预测算法) │ │ │ │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ ▲ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ 循环 │ │ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 预测算法: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 目标:让坐席接听时刚好有客户接通,减少等待 │ │ │ │ │ │ │ │ 预测拨打数 = 空闲坐席数 / 历史接通率 × 超拨系数 │ │ │ │ │ │ │ │ 示例:空闲5人,接通率30%,超拨系数1.2 │ │ │ │ 拨打数 = 5 / 0.3 × 1.2 ≈ 20 │ │ │ │ │ │ │ │ 动态调整:根据实时接通率动态调整拨打速度 │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ 机器人外呼流程: │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ 创建任务 │───►│ 生成TTS │───►│ 批量拨打 │───►│ 播放语音 │ │ │ │ │ │ (含话术) │ │ 音频文件 │ │ │ │ │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ ┌───────────┴───────┐ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ ┌──────────┐ ┌──────────┐│ │ │ │ 纯播报 │ │ 收集按键 ││ │ │ │ 挂断 │ │ 回传结果 ││ │ │ └──────────┘ └──────────┘│ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 3.3 接口设计 #### 3.3.1 API接口清单 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ REST API 设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 坐席接口:/api/v1/agent │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ POST /signin 坐席签入 │ │ │ │ POST /signout 坐席签出 │ │ │ │ POST /ready 置闲 │ │ │ │ POST /not-ready 置忙 │ │ │ │ POST /pause 小休 │ │ │ │ GET /{agentId}/status 查询坐席状态 │ │ │ │ GET /list 查询坐席列表 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 呼叫接口:/api/v1/call │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ POST /originate 发起外呼 │ │ │ │ POST /{uuid}/answer 接听 │ │ │ │ POST /{uuid}/hangup 挂断 │ │ │ │ POST /{uuid}/hold 保持 │ │ │ │ POST /{uuid}/unhold 取回 │ │ │ │ POST /{uuid}/mute 静音 │ │ │ │ POST /{uuid}/unmute 取消静音 │ │ │ │ POST /{uuid}/transfer 转接 │ │ │ │ POST /{uuid}/conference 三方通话 │ │ │ │ GET /{uuid} 查询呼叫信息 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 外呼任务接口:/api/v1/task │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ POST /predictive 创建预测式外呼任务 │ │ │ │ POST /robot 创建机器人外呼任务 │ │ │ │ POST /{taskId}/start 启动任务 │ │ │ │ POST /{taskId}/pause 暂停任务 │ │ │ │ POST /{taskId}/resume 恢复任务 │ │ │ │ POST /{taskId}/stop 停止任务 │ │ │ │ GET /{taskId}/status 查询任务状态 │ │ │ │ GET /{taskId}/result 查询任务结果 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 监控接口:/api/v1/monitor │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ POST /spy 监听 │ │ │ │ POST /barge 强插 │ │ │ │ POST /hangup 强拆 │ │ │ │ GET /realtime 实时统计数据 │ │ │ │ GET /queue/{skillGroup} 队列状态 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 录音接口:/api/v1/recording │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ GET /list 录音列表查询 │ │ │ │ GET /{uuid}/url 获取录音下载地址 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### 3.3.2 WebSocket事件推送 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ WebSocket 事件设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 连接地址:ws://host:port/ws/events?token={token} │ │ │ │ 事件格式: │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ { │ │ │ │ "eventType": "CALL_RINGING", │ │ │ │ "timestamp": 1704067200000, │ │ │ │ "data": { │ │ │ │ "uuid": "xxx", │ │ │ │ "callerNumber": "13800138000", │ │ │ │ "calleeNumber": "4001234567", │ │ │ │ ... │ │ │ │ } │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 事件类型: │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 呼叫事件 │ │ │ │ ├── CALL_RINGING 来电振铃 │ │ │ │ ├── CALL_ANSWERED 通话接听 │ │ │ │ ├── CALL_BRIDGED 双方接通 │ │ │ │ ├── CALL_HANGUP 通话挂断 │ │ │ │ ├── CALL_TRANSFER 通话转接 │ │ │ │ └── CALL_HOLD 通话保持 │ │ │ │ │ │ │ │ 坐席事件 │ │ │ │ ├── AGENT_STATE_CHANGE 坐席状态变更 │ │ │ │ ├── AGENT_SIGNIN 坐席签入 │ │ │ │ └── AGENT_SIGNOUT 坐席签出 │ │ │ │ │ │ │ │ 话单事件 │ │ │ │ └── CDR_GENERATED 话单生成 │ │ │ │ │ │ │ │ DTMF事件 │ │ │ │ └── DTMF_RECEIVED 收到按键 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 3.4 数据库设计 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 数据库设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ cc_agent (坐席表) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ id BIGINT PRIMARY KEY │ │ │ │ agent_id VARCHAR(32) 坐席工号 │ │ │ │ agent_name VARCHAR(64) 坐席姓名 │ │ │ │ extension VARCHAR(16) 分机号 │ │ │ │ password VARCHAR(128) 密码 │ │ │ │ skill_groups VARCHAR(256) 技能组(逗号分隔) │ │ │ │ status TINYINT 状态(0禁用1启用) │ │ │ │ create_time DATETIME 创建时间 │ │ │ │ update_time DATETIME 更新时间 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ cc_skill_group (技能组表) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ id BIGINT PRIMARY KEY │ │ │ │ group_code VARCHAR(32) 技能组编码 │ │ │ │ group_name VARCHAR(64) 技能组名称 │ │ │ │ overflow_group VARCHAR(32) 溢出技能组 │ │ │ │ overflow_seconds INT 溢出等待秒数 │ │ │ │ status TINYINT 状态 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ cc_cdr (话单表) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ id BIGINT PRIMARY KEY │ │ │ │ uuid VARCHAR(64) 呼叫UUID │ │ │ │ direction VARCHAR(16) 呼叫方向 │ │ │ │ caller_number VARCHAR(32) 主叫号码 │ │ │ │ callee_number VARCHAR(32) 被叫号码 │ │ │ │ agent_id VARCHAR(32) 坐席ID │ │ │ │ skill_group VARCHAR(32) 技能组 │ │ │ │ queue_time INT 排队时长(秒) │ │ │ │ ring_time INT 振铃时长(秒) │ │ │ │ talk_time INT 通话时长(秒) │ │ │ │ start_time DATETIME 开始时间 │ │ │ │ answer_time DATETIME 接听时间 │ │ │ │ end_time DATETIME 结束时间 │ │ │ │ hangup_cause VARCHAR(32) 挂断原因 │ │ │ │ recording_path VARCHAR(256) 录音路径 │ │ │ │ business_type VARCHAR(32) 业务类型 │ │ │ │ create_time DATETIME 创建时间 │ │ │ │ INDEX idx_start_time (start_time) │ │ │ │ INDEX idx_agent (agent_id, start_time) │ │ │ │ INDEX idx_caller (caller_number, start_time) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ cc_dial_task (外呼任务表) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ id BIGINT PRIMARY KEY │ │ │ │ task_id VARCHAR(64) 任务ID │ │ │ │ task_name VARCHAR(128) 任务名称 │ │ │ │ task_type VARCHAR(16) 类型(PREDICTIVE/ROBOT) │ │ │ │ skill_group VARCHAR(32) 技能组 │ │ │ │ caller_id VARCHAR(32) 外显号码 │ │ │ │ total_count INT 总号码数 │ │ │ │ dialed_count INT 已拨打数 │ │ │ │ connected_count INT 接通数 │ │ │ │ state VARCHAR(16) 状态 │ │ │ │ start_time DATETIME 开始时间 │ │ │ │ end_time DATETIME 结束时间 │ │ │ │ create_time DATETIME 创建时间 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ cc_dial_detail (外呼明细表) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ id BIGINT PRIMARY KEY │ │ │ │ task_id VARCHAR(64) 任务ID │ │ │ │ phone VARCHAR(32) 号码 │ │ │ │ call_uuid VARCHAR(64) 呼叫UUID │ │ │ │ status VARCHAR(16) 状态 │ │ │ │ dial_time DATETIME 拨打时间 │ │ │ │ answer_time DATETIME 接听时间 │ │ │ │ hangup_time DATETIME 挂断时间 │ │ │ │ dtmf_result VARCHAR(16) 按键结果 │ │ │ │ retry_count INT 重试次数 │ │ │ │ INDEX idx_task (task_id) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 四、开发计划 ### 4.1 阶段划分 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 开发阶段划分(17周) │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 第一阶段:基础环境与核心框架(Week 1-4) │ │ │ │ │ │ │ │ Week 1-2: 环境搭建 + 技术预研 │ │ │ │ Week 3-4: ESL模块 + 基础框架 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 第二阶段:核心功能开发(Week 5-10) │ │ │ │ │ │ │ │ Week 5-6: 坐席管理 + 呼叫控制 │ │ │ │ Week 7-8: ACD分配 + 排队等待 │ │ │ │ Week 9-10: 录音 + 软电话条 + CRM集成 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 第三阶段:高级功能开发(Week 11-14) │ │ │ │ │ │ │ │ Week 11-12: 预测式外呼 + 机器人外呼 │ │ │ │ Week 13: IVR + 呼入机器人 │ │ │ │ Week 14: 监控大屏 + 班长监控 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 第四阶段:集成测试与上线(Week 15-17) │ │ │ │ │ │ │ │ Week 15: 集成联调 + 高可用部署 │ │ │ │ Week 16: 测试 + Bug修复 │ │ │ │ Week 17: 试运行 + 正式上线 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 4.2 详细开发计划 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 详细开发计划 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ═══════════════════════════════════════════════════════════════════ │ │ 第一阶段:基础环境与核心框架(Week 1-4) │ │ ═══════════════════════════════════════════════════════════════════ │ │ │ │ Week 1: 环境准备 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 服务器环境准备(开发、测试环境) │ │ │ │ □ FreeSWITCH安装配置 │ │ │ │ □ 开发环境搭建(IDE、Git、Maven) │ │ │ │ □ MySQL、Redis环境搭建 │ │ │ │ □ 测试SIP话机注册与基础通话 │ │ │ │ │ │ │ │ 交付物:环境部署文档、FreeSWITCH基础配置 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 2: 技术预研 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ ESL协议学习与Demo开发 │ │ │ │ □ FreeSWITCH Dialplan配置学习 │ │ │ │ □ Lua IVR脚本编写测试 │ │ │ │ □ 项目工程结构搭建 │ │ │ │ □ 技术方案评审 │ │ │ │ │ │ │ │ 交付物:技术预研报告、Demo代码 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 3-4: ESL模块开发 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ ESL连接管理(连接池、心跳、重连) │ │ │ │ □ ESL命令封装(API、SendMsg) │ │ │ │ □ 事件订阅与分发机制 │ │ │ │ □ 常用命令测试验证 │ │ │ │ □ 基础API框架搭建 │ │ │ │ □ WebSocket服务搭建 │ │ │ │ │ │ │ │ 交付物:ESL模块代码、单元测试 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ═══════════════════════════════════════════════════════════════════ │ │ 第二阶段:核心功能开发(Week 5-10) │ │ ═══════════════════════════════════════════════════════════════════ │ │ │ │ Week 5-6: 坐席管理 + 呼叫控制 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 坐席数据模型设计 │ │ │ │ □ 坐席签入/签出 │ │ │ │ □ 坐席状态管理(置闲、置忙、小休) │ │ │ │ □ 技能组管理 │ │ │ │ □ 点击外呼功能 │ │ │ │ □ 接听/挂断/保持/取回 │ │ │ │ □ 转接(盲转、咨询转) │ │ │ │ □ 坐席API接口 │ │ │ │ □ 呼叫API接口 │ │ │ │ │ │ │ │ 交付物:坐席模块、呼叫控制模块 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 7-8: ACD分配 + 排队等待 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 队列数据结构设计 │ │ │ │ □ 呼叫入队逻辑 │ │ │ │ □ 历史服务优先策略 │ │ │ │ □ 最长空闲优先策略 │ │ │ │ □ 轮询分配策略 │ │ │ │ □ 坐席选择与分配 │ │ │ │ □ 排队等待音乐 │ │ │ │ □ 溢出策略实现 │ │ │ │ □ 排队状态查询 │ │ │ │ │ │ │ │ 交付物:ACD模块、队列管理模块 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 9-10: 录音 + 软电话条 + CRM集成 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 录音启动/停止控制 │ │ │ │ □ 录音文件存储管理 │ │ │ │ □ 录音查询与下载API │ │ │ │ □ 软电话条前端开发(Vue组件) │ │ │ │ □ WebSocket事件接收与处理 │ │ │ │ □ 软电话条UI(状态、按钮、来电显示) │ │ │ │ □ CRM弹屏集成(事件推送) │ │ │ │ □ 话单推送至CRM │ │ │ │ □ 联调测试 │ │ │ │ │ │ │ │ 交付物:录音模块、软电话条组件、CRM集成文档 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ═══════════════════════════════════════════════════════════════════ │ │ 第三阶段:高级功能开发(Week 11-14) │ │ ═══════════════════════════════════════════════════════════════════ │ │ │ │ Week 11-12: 预测式外呼 + 机器人外呼 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 外呼任务管理(创建、启动、暂停、停止) │ │ │ │ □ 号码导入与管理 │ │ │ │ □ 预测式拨号算法实现 │ │ │ │ □ 接通后转坐席逻辑 │ │ │ │ □ 外呼统计(接通率、坐席利用率) │ │ │ │ □ 阿里云TTS集成 │ │ │ │ □ 机器人外呼流程(播报、按键收集) │ │ │ │ □ 外呼结果回传 │ │ │ │ □ 任务API接口 │ │ │ │ │ │ │ │ 交付物:外呼引擎、机器人外呼模块 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 13: IVR + 呼入机器人 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ IVR Dialplan配置(多业务线) │ │ │ │ □ 阿里云ASR集成 │ │ │ │ □ 语音识别导航实现 │ │ │ │ □ NLU对接(意图识别) │ │ │ │ □ FAQ问答流程 │ │ │ │ □ 转人工逻辑 │ │ │ │ □ Lua脚本开发 │ │ │ │ │ │ │ │ 交付物:IVR配置、呼入机器人模块 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 14: 监控大屏 + 班长监控 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 实时数据统计API │ │ │ │ □ 监控大屏前端开发 │ │ │ │ □ 坐席状态分布展示 │ │ │ │ □ 队列状态展示 │ │ │ │ □ 话务趋势图表 │ │ │ │ □ 监听功能实现 │ │ │ │ □ 强插功能实现 │ │ │ │ □ 强拆功能实现 │ │ │ │ │ │ │ │ 交付物:监控大屏、班长监控功能 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ═══════════════════════════════════════════════════════════════════ │ │ 第四阶段:集成测试与上线(Week 15-17) │ │ ═══════════════════════════════════════════════════════════════════ │ │ │ │ Week 15: 集成联调 + 高可用部署 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ CRM系统全流程联调 │ │ │ │ □ 业务系统对接联调 │ │ │ │ □ 运营商线路对接 │ │ │ │ □ 主备FreeSWITCH部署 │ │ │ │ □ Keepalived配置 │ │ │ │ □ 故障切换测试 │ │ │ │ □ 数据库主从配置 │ │ │ │ □ Redis Sentinel配置 │ │ │ │ │ │ │ │ 交付物:联调测试报告、高可用部署文档 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 16: 测试 + Bug修复 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 功能测试(全流程) │ │ │ │ □ 性能测试(150坐席并发) │ │ │ │ □ 压力测试(峰值话务) │ │ │ │ □ 故障恢复测试 │ │ │ │ □ Bug修复 │ │ │ │ □ 性能优化 │ │ │ │ □ 上线检查清单确认 │ │ │ │ │ │ │ │ 交付物:测试报告、Bug清单、优化记录 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ Week 17: 试运行 + 正式上线 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ □ 生产环境部署 │ │ │ │ □ 数据初始化 │ │ │ │ □ 小范围试运行(一条业务线) │ │ │ │ □ 问题收集与修复 │ │ │ │ □ 全量上线 │ │ │ │ □ 运维交接 │ │ │ │ □ 用户培训 │ │ │ │ │ │ │ │ 交付物:上线报告、运维手册、培训材料 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 4.3 甘特图 ``` ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ 项目甘特图 │ ├─────────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ 任务 │ W1 │ W2 │ W3 │ W4 │ W5 │ W6 │ W7 │ W8 │ W9 │W10│W11│W12│W13│W14│W15│W16│W17│ │ ─────────────────────────│────│────│────│────│────│────│────│────│────│───│───│───│───│───│───│───│───│ │ 第一阶段 │████│████│████│████│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 环境搭建 │████│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 技术预研 │ │████│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ESL模块开发 │ │ │████│████│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ─────────────────────────│────│────│────│────│────│────│────│────│────│───│───│───│───│───│───│───│───│ │ 第二阶段 │ │ │ │ │████│████│████│████│████│███│ │ │ │ │ │ │ │ │ 坐席管理 │ │ │ │ │████│████│ │ │ │ │ │ │ │ │ │ │ │ │ 呼叫控制 │ │ │ │ │████│████│ │ │ │ │ │ │ │ │ │ │ │ │ ACD分配 │ │ │ │ │ │ │████│████│ │ │ │ │ │ │ │ │ │ │ 排队等待 │ │ │ │ │ │ │████│████│ │ │ │ │ │ │ │ │ │ │ 录音功能 │ │ │ │ │ │ │ │ │████│ │ │ │ │ │ │ │ │ │ 软电话条 │ │ │ │ │ │ │ │ │████│███│ │ │ │ │ │ │ │ │ CRM集成 │ │ │ │ │ │ │ │ │ │███│ │ │ │ │ │ │ │ │ ─────────────────────────│────│────│────│────│────│────│────│────│────│───│───│───│───│───│───│───│───│ │ 第三阶段 │ │ │ │ │ │ │ │ │ │ │███│███│███│███│ │ │ │ │ 预测式外呼 │ │ │ │ │ │ │ │ │ │ │███│███│ │ │ │ │ │ │ 机器人外呼 │ │ │ │ │ │ │ │ │ │ │███│███│ │ │ │ │ │ │ IVR+呼入机器人 │ │ │ │ │ │ │ │ │ │ │ │ │███│ │ │ │ │ │ 监控大屏 │ │ │ │ │ │ │ │ │ │ │ │ │ │███│ │ │ │ │ 班长监控 │ │ │ │ │ │ │ │ │ │ │ │ │ │███│ │ │ │ │ ─────────────────────────│────│────│────│────│────│────│────│────│────│───│───│───│───│───│───│───│───│ │ 第四阶段 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │███│███│███│ │ 集成联调 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │███│ │ │ │ 高可用部署 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │███│ │ │ │ 测试+Bug修复 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │███│ │ │ 试运行 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │██ │ │ 正式上线 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ██│ │ ─────────────────────────│────│────│────│────│────│────│────│────│────│───│───│───│───│───│───│───│───│ │ 里程碑 │ │ M1 │ │ M2 │ │ │ │ │ │M3 │ │ │ │M4 │M5 │M6 │M7 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ M1: 需求确认 │ │ ◆ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ M2: 环境就绪 │ │ │ │ ◆ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ M3: 核心功能完成 │ │ │ │ │ │ │ │ │ │ ◆ │ │ │ │ │ │ │ │ │ M4: 全功能完成 │ │ │ │ │ │ │ │ │ │ │ │ │ │ ◆ │ │ │ │ │ M5: 联调测试完成 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ◆ │ │ │ │ M6: 试运行完成 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ◆ │ │ │ M7: 正式上线 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ◆ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 五、团队组织 ### 5.1 团队配置 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 团队组织架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ │ │ │ 项目经理 │ │ │ │ (1人) │ │ │ └──────┬──────┘ │ │ │ │ │ ┌─────────────────────┼─────────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 开发组 │ │ 测试组 │ │ 运维组 │ │ │ │ (8-10人) │ │ (1-2人) │ │ (1人) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ ├── 架构师 (1人) │ │ ├── 后端开发 (4-5人) │ │ ├── 前端开发 (2人) │ │ └── FreeSWITCH工程师 (1-2人) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 5.2 角色职责 | 角色 | 人数 | 职责 | 技能要求 | |------|------|------|---------| | **项目经理** | 1 | 项目管理、进度控制、风险管理 | 项目管理经验、沟通协调能力 | | **架构师** | 1 | 架构设计、技术选型、核心代码 | FreeSWITCH、分布式系统 | | **后端开发** | 4-5 | 业务模块开发、API开发 | Java、Spring Boot、Redis | | **前端开发** | 2 | 软电话条、监控大屏 | Vue3、TypeScript、WebSocket | | **FS工程师** | 1-2 | FreeSWITCH配置、Lua脚本 | FreeSWITCH、SIP协议、Lua | | **测试** | 1-2 | 功能测试、性能测试 | 测试用例设计、性能测试工具 | | **运维** | 1 | 环境部署、监控运维 | Linux、Docker、运维经验 | ### 5.3 人员分工 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 开发任务分工 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 架构师 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 整体架构设计 │ │ │ │ • ESL模块核心代码 │ │ │ │ • ACD核心算法 │ │ │ │ • 技术难点攻关 │ │ │ │ • 代码Review │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 后端开发A (资深) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 坐席管理模块 │ │ │ │ • 呼叫控制模块 │ │ │ │ • WebSocket推送服务 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 后端开发B │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • ACD分配模块 │ │ │ │ • 队列管理模块 │ │ │ │ • 溢出策略 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 后端开发C │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 预测式外呼模块 │ │ │ │ • 机器人外呼模块 │ │ │ │ • TTS集成 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 后端开发D │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 录音管理模块 │ │ │ │ • 监控统计模块 │ │ │ │ • 班长监控功能 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 后端开发E (可选) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • API接口开发 │ │ │ │ • CRM集成对接 │ │ │ │ • 单元测试 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 前端开发A │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 软电话条组件开发 │ │ │ │ • WebSocket客户端 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 前端开发B │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 实时监控大屏 │ │ │ │ • 数据可视化图表 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ FreeSWITCH工程师 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • FreeSWITCH配置优化 │ │ │ │ • Dialplan编写 │ │ │ │ • Lua IVR脚本开发 │ │ │ │ • 线路对接 │ │ │ │ • 高可用部署 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 六、风险控制 ### 6.1 风险清单 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 风险清单 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 风险1: FreeSWITCH技术门槛高 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 等级: 高 │ │ │ │ 影响: 开发进度延迟,功能实现困难 │ │ │ │ 应对: │ │ │ │ • Week 1-2 集中学习FreeSWITCH和ESL │ │ │ │ • 考虑引入外部FreeSWITCH顾问 │ │ │ │ • 预留20%时间缓冲 │ │ │ │ • 简化复杂功能,优先实现核心流程 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 风险2: 运营商线路对接周期长 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 等级: 高 │ │ │ │ 影响: 无法进行真实环境测试,上线延迟 │ │ │ │ 应对: │ │ │ │ • 项目启动即开始申请线路 │ │ │ │ • 开发阶段使用模拟线路/测试线路 │ │ │ │ • 准备备选运营商 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 风险3: CRM系统对接复杂 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 等级: 中 │ │ │ │ 影响: 集成联调时间延长 │ │ │ │ 应对: │ │ │ │ • 提前与CRM团队沟通,明确接口规范 │ │ │ │ • 提供详细API文档和示例代码 │ │ │ │ • 预留充足联调时间 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 风险4: 并发性能不达标 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 等级: 中 │ │ │ │ 影响: 系统无法支撑业务量 │ │ │ │ 应对: │ │ │ │ • Week 16 进行完整压力测试 │ │ │ │ • 预先设计水平扩展方案 │ │ │ │ • 关键代码Review,避免性能瓶颈 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 风险5: ASR/TTS识别效果不佳 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 等级: 中 │ │ │ │ 影响: 机器人/IVR体验差 │ │ │ │ 应对: │ │ │ │ • 提前进行ASR效果测试 │ │ │ │ • 准备热词优化方案 │ │ │ │ • 设计按键兜底方案 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 风险6: 核心人员离职 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 等级: 中 │ │ │ │ 影响: 知识断层,进度受阻 │ │ │ │ 应对: │ │ │ │ • 重要模块双人备份 │ │ │ │ • 代码规范、文档完善 │ │ │ │ • 定期代码Review和知识分享 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 6.2 应急预案 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 应急预案 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 场景1: 开发进度严重滞后 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 触发条件: 核心功能延迟超过2周 │ │ │ │ 应对措施: │ │ │ │ • 评估是否可以简化功能范围 │ │ │ │ • 考虑增加外部资源支持 │ │ │ │ • 与业务方协商分阶段上线 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 场景2: FreeSWITCH关键问题无法解决 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 触发条件: 技术问题阻塞超过1周 │ │ │ │ 应对措施: │ │ │ │ • 联系FreeSWITCH社区或付费支持 │ │ │ │ • 寻求外部FreeSWITCH专家支援 │ │ │ │ • 评估替代方案可行性 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 场景3: 上线后系统不稳定 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 触发条件: 故障频发或性能严重不足 │ │ │ │ 应对措施: │ │ │ │ • 启用回退方案(如有旧系统) │ │ │ │ • 降级部分功能,保障核心业务 │ │ │ │ • 紧急扩容或优化 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 七、交付物清单 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 项目交付物清单 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 阶段 │ 交付物 │ 负责人 │ │ ──────────────│───────────────────────────────────────│───────────│ │ │ │ │ │ 需求阶段 │ 需求规格说明书 │ 产品 │ │ │ 接口设计文档 │ 架构师 │ │ │ 数据库设计文档 │ 架构师 │ │ │ │ │ │ 开发阶段 │ 源代码(Git仓库) │ 开发团队 │ │ │ 单元测试代码 │ 开发团队 │ │ │ API接口文档 │ 后端开发 │ │ │ 前端组件文档 │ 前端开发 │ │ │ │ │ │ 测试阶段 │ 测试用例文档 │ 测试 │ │ │ 测试报告 │ 测试 │ │ │ 性能测试报告 │ 测试 │ │ │ │ │ │ 部署阶段 │ 部署手册 │ 运维 │ │ │ 运维手册 │ 运维 │ │ │ 高可用配置文档 │ 运维 │ │ │ │ │ │ 上线阶段 │ 上线检查清单 │ 项目经理 │ │ │ 培训材料 │ 产品 │ │ │ 用户操作手册 │ 产品 │ │ │ 项目总结报告 │ 项目经理 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 八、总结 ### 8.1 方案要点总结 | 维度 | 要点 | |------|------| | **技术架构** | FreeSWITCH + Java中间件 + Vue前端,ESL控制模式 | | **开发周期** | 4个月(17周),分4阶段 | | **团队配置** | 11-14人(含项目经理、开发、测试、运维) | | **核心功能** | 呼入呼出、ACD分配、预测式外呼、机器人、录音、监控 | | **风险控制** | FreeSWITCH技术预研、线路提前申请、预留缓冲时间 | ### 8.2 关键成功因素 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 关键成功因素 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 技术准备充分 │ │ └─► Week 1-2 完成FreeSWITCH学习和技术预研 │ │ │ │ 2. 核心功能优先 │ │ └─► 先完成P0功能,确保基础可用 │ │ │ │ 3. 持续集成测试 │ │ └─► 每完成一个模块立即测试,避免问题堆积 │ │ │ │ 4. CRM团队紧密配合 │ │ └─► 提前沟通接口,并行开发 │ │ │ │ 5. 线路提前就位 │ │ └─► 项目启动即申请运营商线路 │ │ │ │ 6. 分阶段上线 │ │ └─► 先上线一条业务线验证,再全量推广 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 8.3 下一步行动 | 序号 | 行动项 | 负责人 | 完成时间 | |------|-------|-------|---------| | 1 | 确认团队人员到位 | 项目经理 | 项目启动前 | | 2 | 采购/准备开发测试环境 | 运维 | Week 1 | | 3 | 启动运营商线路申请 | 项目经理 | Week 1 | | 4 | 组织FreeSWITCH技术培训 | 架构师 | Week 1-2 | | 5 | 与CRM团队召开接口对接会议 | 架构师 | Week 2 | | 6 | 完成技术方案评审 | 架构师 | Week 2 | --- **文档结束** 如需进一步细化某个模块的设计或有其他问题,请随时告知!