一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

当然,你们中的许多人,像我一样,都有做一些独特的事情的想法。 在这篇文章中,我将描述我在开发PBX时必须面对的技术问题和解决方案。 也许这会帮助某人决定自己的想法,并帮助某人走上人们走过的路,因为我也从先驱者的经验中受益。

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

理念及关键要求

这一切都始于对的爱 星号 (构建通信应用程序的框架)、电话和安装自动化 FreePBX的 (网络界面 星号)。 如果公司的需求没有具体细节并且在能力范围内 FreePBX的 - 一切都很好。 整个安装在 XNUMX 小时内完成,该公司收到了配置好的 PBX、用户友好的界面以及短期培训和支持(如果需要)。

但最有趣的任务是非标准的,然后它就不那么美妙了。 星号 可以做很多事情,但要保持 Web 界面正常工作,就需要花费数倍的时间。 因此,一个小细节可能会比安装 PBX 的其余部分花费更长的时间。 重点不在于编写一个 Web 界面需要很长时间,而在于架构特征 FreePBX的。 架构途径和方法 FreePBX的 php4的时候就布局了,那时已经有了php5.6,一切都可以变得更简单、更方便。

最后一根稻草是图表形式的图形拨号计划。 当我尝试构建这样的东西时 FreePBX的,我意识到我必须大幅重写它,并且构建新的东西会更容易。

关键要求是:

  • 设置简单,即使是新手管理员也可以直观地访问。 这样,企业不需要我们这边维护PBX,
  • 轻松修改,以便在足够的时间内解决任务,
  • 易于与 PBX 集成。 U FreePBX的 没有用于更改设置的 API,即例如,您不能从第三方应用程序创建组或语音菜单,只能从 API 本身创建 星号,
  • 开源——对于程序员来说,这对于客户端的修改非常重要。

更快开发的想法是让所有功能都由对象形式的模块组成。 所有对象都必须有一个共同的父类,这意味着所有主要函数的名称都是已知的,因此已经有默认实现。 对象将允许您以带有字符串键的关联数组的形式显着减少参数的数量,您可以在 FreePBX的 通过检查整个函数和嵌套函数可以实现这一点。 对于对象,平庸的自动完成将显示所有属性,并且通常会多次简化生活。 另外,继承和重新定义已经解决了许多修改问题。

接下来减慢返工时间并值得避免的事情是重复。 如果有一个模块负责拨打员工电话,那么所有其他需要向员工发送呼叫的模块都应该使用它,而不是创建自己的副本。 因此,如果您需要更改某些内容,那么您只需在一个地方进行更改,并且对“它是如何工作的”的搜索应该在一个地方进行,而不是在整个项目中进行搜索。

第一个版本和第一个错误

第一个原型在一年内就准备好了。 按照计划,整个 PBX 是模块化的,这些模块不仅可以添加处理呼叫的新功能,还可以更改 Web 界面本身。

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX
是的,以这种方案的形式构建拨号计划的想法不是我的,但它非常方便,我也做了同样的事情 星号.

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

通过编写模块,程序员已经可以:

  • 创建您自己的呼叫处理功能,可以将其放置在图表上以及左侧的元素菜单中,
  • 为 Web 界面创建您自己的页面并将模板添加到现有页面(如果页面开发人员已提供此功能),
  • 将您的设置添加到主设置选项卡或创建您自己的设置选项卡,
  • 程序员可以继承现有模块,更改部分功能并以新名称注册或替换原始模块。

例如,您可以通过以下方式创建自己的语音菜单:

......
class CPBX_MYIVR extends CPBX_IVR
{
 function __construct()
 {
 parent::__construct();
 $this->_module = "myivr";
 }
}
.....
$myIvrModule = new CPBX_MYIVR();
CPBXEngine::getInstance()->registerModule($myIvrModule,__DIR__); //Зарегистрировать новый модуль
CPBXEngine::getInstance()->registerModuleExtension($myIvrModule,'ivr',__DIR__); //Подменить существующий модуль

第一个复杂的实现带来了最初的自豪和最初的失望。 我很高兴它有效,我已经能够重现主要功能 FreePBX的。 我很高兴人们喜欢这个计划的想法。 简化开发的选择仍然有很多,但即使在那时,一些任务已经变得更容易了。

用于更改 PBX 配置的 API 令人失望 - 结果根本不是我们想要的。 我采取了与中相同的原则 FreePBX的,通过单击“应用”按钮,将重新创建整个配置并重新启动模块。

它看起来像这样:

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX
*拨号方案是处理呼叫的规则(算法)。

但使用此选项,不可能编写普通的 API 来更改 PBX 设置。 一、应用变更的操作 星号 太长且占用资源。
其次,你不能同时调用两个函数,因为两者都会创建配置。
第三,它应用所有设置,包括管理员所做的设置。

在此版本中,如 阿斯科齐亚,可以仅生成已更改模块的配置并仅重新启动必要的模块,但这些都是半措施。 有必要改变方法。

第二个版本。 鼻子被拉出来尾巴被卡住

解决问题的想法不是重新创建配置和拨号方案 星号,但将信息保存到数据库并在处理调用时直接从数据库读取。 星号 我已经知道如何从数据库读取配置,只需更改数据库中的值,下一次调用将考虑更改进行处理,并且该功能非常适合读取 dialplan 参数 REALTIME_HASH.

最后甚至不需要重启 星号 更改设置时,所有设置立即开始应用到 星号.

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

对拨号方案的唯一更改是添加分机号码和 提示。 但这些都是小改动

exten=>101,1,GoSub(‘sub-callusers’,s,1(1)); - точечное изменение, добавляется/изменяется через ami

; sub-callusers – универсальная функция генерится при установке модуля.
[sub-callusers]
exten =>s,1,Noop()
exten =>s,n,Set(LOCAL(TOUSERID)=${ARG1})
exten =>s,n,ClearHash(TOUSERPARAM)
exten =>s,n,Set(HASH(TOUSERPARAM)=${REALTIME_HASH(rl_users,id,${LOCAL(TOUSERID)})})
exten =>s,n,GotoIf($["${HASH(TOUSERPARAM,id)}"=""]?return)
...

您可以使用以下命令轻松添加或更改拨号方案中的线路 朋友 (控制接口 星号)并且不需要重新启动整个拨号计划。

这解决了配置 API 的问题。 您甚至可以直接进入数据库并添加新的组或更改,例如,该组的“dialtime”字段中的拨号时间,并且下一次呼叫将持续指定的时间(这不是建议操作,因为某些 API 操作需要 朋友 来电)。

第一次艰难的实施再次带来了最初的自豪和失望。 我很高兴它起作用了。 数据库成为了关键环节,对磁盘的依赖增加了,风险也多了,但一切都运行稳定,没有出现问题。 最重要的是,现在可以通过 Web 界面完成的所有操作都可以通过 API 完成,并且使用相同的方法。 此外,网络界面取消了管理员经常忘记的“将设置应用于 PBX”按钮。

令人失望的是,开发变得更加复杂。 从第一个版本开始,PHP 语言就生成了该语言的 dialplan 星号 而且它看起来完全不可读,加上语言本身 星号 对于编写拨号计划来说,这是非常原始的。

它看起来像什么:

$usersInitSection = $dialplan->createExtSection('usersinit-sub','s');
$usersInitSection
 ->add('',new Dialplanext_gotoif('$["${G_USERINIT}"="1"]','exit'))
 ->add('',new Dialplanext_set('G_USERINIT','1'))
 ->add('',new Dialplanext_gosub('1','s','sub-AddOnAnswerSub','usersconnected-sub'))
 ->add('',new Dialplanext_gosub('1','s','sub-AddOnPredoDialSub','usersinitondial-sub'))
 ->add('',new Dialplanext_set('LOCAL(TECH)','${CUT(CHANNEL(name),/,1)}'))
 ->add('',new Dialplanext_gotoif('$["${LOCAL(TECH)}"="SIP"]','sipdev'))
 ->add('',new Dialplanext_gotoif('$["${LOCAL(TECH)}"="PJSIP"]','pjsipdev'))

在第二个版本中,拨号方案变得通用,它包括所有可能的处理选项,具体取决于参数,并且其大小显着增加。 所有这些都大大减慢了开发时间,一想到有必要再次干扰拨号计划就让我感到难过。

第三版

解决问题的想法不是产生 星号 来自 php 的 dialplan 并使用 快速AGI 并用PHP本身编写所有处理规则。 快速AGI 它允许 星号,要处理调用,请连接到套接字。 从那里接收命令并发送结果。 因此,拨号方案的逻辑已经超出了界限 星号 可以用任何语言编写,在我的例子中是 PHP。

有很多尝试和错误。 主要问题是我已经有很多类/文件。 创建对象、初始化对象、相互注册大约需要 1,5 秒,而且每次调用的这种延迟是不可忽略的。

初始化应该只发生一次,因此寻找解决方案从使用 php 编写服务开始 线程。 经过一周的实验后,由于该扩展的工作方式错综复杂,该选项被搁置。 经过一个月的测试,我还不得不放弃 PHP 中的异步编程;我需要一些简单的、任何 PHP 初学者都熟悉的东西,而且 PHP 的许多扩展都是同步的。

解决方案是我们自己的 C 多线程服务,它是用 PHP库。 它加载所有 ATS php 文件,等待所有模块初始化,相互添加回调,当一切准备就绪时,将其缓存。 查询时通过 快速AGI 创建一个流,在其中复制所有类和数据的缓存副本,并将请求传递给 php 函数。

通过此解决方案,从向我们的服务发送调用到第一个命令的时间 星号 从 1,5s 减少到 0,05s,这个时间稍微取决于项目的大小。

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

结果,拨号计划开发的时间显着减少,我很欣赏这一点,因为我必须用 PHP 重写所有模块的整个拨号计划。 首先,php中应该已经编写了从数据库获取对象的方法;需要它们在Web界面中显示,其次,这是最重要的,终于可以方便地使用带有数字和数组的字符串了带有数据库以及许多 PHP 扩展。

要在模块类中处理拨号方案,您需要实现该函数 dialplan动态呼叫 和论证 pbx呼叫请求 将包含一个与之交互的对象 星号.

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

此外,还可以调试 dialplan(php 有 xdebug,它适用于我们的服务),您可以通过查看变量的值来逐步移动。

通话数据

任何分析和报告都需要正确收集的数据,并且这个 PBX 块从第一个版本到第三个版本也经历了大量的试验和错误。 通常,通话数据是一个标志。 一次通话 = 一次录音:谁打电话、谁接听、通话时长。 在更有趣的选项中,还有一个附加标志,指示在通话过程中呼叫了哪个 PBX 员工。 但这一切仅满足了部分需求。

最初的要求是:

  • 不仅保存 PBX 呼叫的人,还保存接听的人,因为存在拦截,在分析呼叫时需要考虑到这一点,
  • 与员工联系之前的时间。 在 FreePBX的 和其他一些 PBX,一旦 PBX 拿起电话,呼叫就被视为已应答。 但对于语音菜单,您已经需要拿起电话,因此所有呼叫都会被接听,并且接听的等待时间变为0-1秒。 因此,决定不仅节省响应前的时间,还节省与关键模块连接前的时间(模块本身设置此标志,目前为“员工”、“外线”),
  • 对于更复杂的拨号方案,当呼叫在不同组之间传输时,必须能够单独检查每个元素。

事实证明,最好的选择是 PBX 模块在呼叫时发送有关自身的信息,并最终以树的形式保存信息。

看起来像这样:

首先,有关通话的一般信息(与其他人一样 - 没什么特别的)。

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

  1. 接到外线电话”对于面团“05:55:52从89295671458到89999999999,最后有员工接听”秘书2» 号码为 104。客户等待了 60 秒,发言了 36 秒。
  2. 员工 ”秘书2“拨打 112,一名员工接听”经理1» 8 秒后。 他们聊了 14 秒。
  3. 客户转移给员工“经理1“他们又继续聊了 13 秒

但这只是冰山一角;对于每条记录,您都可以通过 PBX 获取详细的通话历史记录。

一个项目的故事,或者我如何花费 7 年时间创建基于 Asterisk 和 Php 的 PBX

所有信息都以调用嵌套的形式呈现:

  1. 接到外线电话”对于面团» 05:55:52 从号码 89295671458 转到号码 89999999999。
  2. 05:55:53 外线向呼入电路发送呼叫”test»
  3. 根据该方案处理呼叫时,模块“经理电话”,其中通话时长为16秒。 这是为客户开发的模块。
  4. 模块 ”经理电话“ 向负责该号码的员工(客户)发送电话”经理1”并等待 5 秒以获得响应。 经理没有回答。
  5. 模块 ”经理电话“向群组发送呼叫”公司经理” 这些是同一方向的其他经理(坐在同一个房间),等待 11 秒才得到答复。
  6. 团体 ”公司经理“给员工打电话”经理1, 经理2, 经理3“同时持续 11 秒。 没有答案。
  7. 经理的通话结束。 并且电路向模块发送调用“从1c中选择一条路线” 也是为客户端编写的模块。 这里调用被处理了 0 秒。
  8. 该电路向语音菜单发送呼叫“基本功能,带附加拨号功能” 客户在那里等待了31秒,没有额外的拨号。
  9. 该计划向集团发出呼吁“秘书”,客户等待了 12 秒。
  10. 一个群组中,同时呼叫 2 名员工“秘书1“和”秘书2“12 秒后,员工回答”秘书2” 呼叫的应答将复制到父呼叫中。 原来他在群里回答“秘书2“,当呼叫电路应答时”秘书2”并接听外线电话“秘书2“。

保存有关每个操作及其嵌套的信息将使简单地生成报告成为可能。 语音菜单上的报告将帮助您了解它有多少帮助或阻碍。 构建有关员工未接电话的报告,考虑到该呼叫被拦截,因此不被视为未接,并考虑到这是一个群组呼叫,并且其他人较早应答,这意味着该呼叫也未被视为未接。

此类信息存储将允许您单独获取每个组并确定其工作效率,并按小时构建已回答和错过的组的图表。 您还可以通过在连接到经理后分析转接来检查与负责经理的连接的准确性。

您还可以进行非常非典型的研究,例如,不在数据库中的号码拨打正确分机的频率,或者转接至移动电话的拨出呼叫的百分比。

结果如何呢?

不需要专家来维护 PBX;最普通的管理员就可以做到 - 经过实践检验。

对于修改,不需要具有严格资格的专家;PHP 知识就足够了,因为已经为 SIP 协议、队列、呼叫员工等编写了模块。 有一个包装类 星号。 为了开发模块,程序员可以(并且应该以一种好的方式)调用现成的模块。 和知识 星号 如果客户要求添加包含某些新报告的页面,则完全没有必要。 但实践表明,虽然第三方程序员可以应对,但如果没有文档和正常的注释覆盖,他们会感到不安全,因此仍有改进的空间。

模块可以:

  • 创建新的呼叫处理能力,
  • 向网络界面添加新块,
  • 从任何现有模块继承,重新定义功能并替换它,或者只是稍微修改一下副本,
  • 将您的设置添加到其他模块的设置模板等等。

通过 API 进行 PBX 设置。 如上所述,所有设置都存储在数据库中并在呼叫时读取,因此您可以通过 API 更改所有 PBX 设置。 调用 API 时,不会重新创建配置,也不会重新启动模块,因此,无论您有多少设置和员工,都没有关系。 API 请求执行速度快且不会相互阻塞。

PBX 存储所有呼叫的关键操作,包括持续时间(等待/通话)、嵌套以及 PBX 术语(员工、组、外部线路,而不是频道、号码)。 这允许您为特定客户构建各种报告,并且大部分工作是创建用户友好的界面。

时间会告诉我们接下来会发生什么。 仍然有许多细微差别需要重做,仍然有许多计划,但自第三版创建以来已经过去了一年,我们已经可以说这个想法正在发挥作用。 版本 3 的主要缺点是硬件资源,但这通常是您为了易于开发而必须付出的代价。

来源: habr.com

添加评论