65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用 C#/XML/HTTP/PHP 通过 DTMF 认证创建基于 IVR 的电话客户端网关系统

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (20投票s)

2014年7月9日

CPOL

19分钟阅读

viewsIcon

50376

downloadIcon

2170

本文介绍了如何使用 XML/HTTP/PHP 和 DTMF 信号开发一个 C# 应用程序,该程序允许您通过电话呼叫,借助客户的用户 ID 和 PIN 码来验证客户身份。

目录

引言

为了使公司与客户的互动更加高效和安全,公司通常会为每位客户分配一个唯一的客户 ID 码。这种客户识别通常由一个用户 ID(如用户名)和一个密码(或者说是 PIN 码)组成。通过这种方式,客户事务可以被识别和跟踪。公司可以在个人管理或其网页上要求输入此客户 ID,以便让客户进入其特定页面并访问自己的数据和设置。

然而,客户识别也可以通过电话呼叫实现,无需任何人工干预。您唯一需要的是一个高效的 IVR 菜单系统。(IVR 是 Interactive Voice Response 的缩写,即交互式语音应答,它是一种允许计算机通过语音和键盘输入的 DTMF 音频与人互动技术。)它可以是任何 CRM(客户关系管理)系统的有效组成部分。下图展示了我的解决方案是如何工作的。首先,您的客户(也就是呼叫者)拨打您的电话号码。呼叫将由您的 IVR 系统自动接听。您可以指定一条问候语,它会自动播放。之后,您的客户(即呼叫者)需要输入他/她的 ID(一个 PIN 码)。在听完可选的菜单项后,您的客户就可以访问他/她的特定数据和设置了。

整个项目基于一个 IVR 菜单系统。在这个项目中,我的 IVR 是用 C# 编写的,并使用了 XML 代码。XML 编码有助于设计菜单结构,因为它使菜单和子菜单的体系更加清晰。我将展示一个非常简单的例子,说明如何构建一个通用的电话客户端网关系统,但经过一些修改后,这个项目可以用来创建一个完整的银行或电话余额查询系统,或任何其他需要用户认证的系统。

背景

为了实现 VoIP IVR 功能,我使用了 Ozeki VoIP SIP SDKMicrosoft Visual Studio。(请注意,Visual Studio 至少需要 .NET Framework 3.5 SP1 或任何更新版本。)IVR 的默认行为是用 C# 实现的,但我也使用了 XML 来设计菜单结构。

为了能够接听客户的来电,一个简单的软电话应用程序也是必不可少的。鉴于我的文章侧重于 DTMF 认证和电话客户端网关系统的实现,我不想描述软电话的开发过程。因此,我的文章假设您已经有了一个软电话。如果没有,您可以按照下面的视频指南轻松构建自己的软电话。

在我的项目中,使用了 7 个类:Softphone.cs, ICommand.cs, Program.cs, Menu.cs, MultipleCommandHandler.cs, SpeakCommand.csPlayCommand.cs

为了更好地理解,我将项目分成了两个主要部分

  1. 首先使用 XML 代码在 C# 中实现 IVR
  2. 在 C# 中使用 HTTP 和 PHP 实现 DTMF 认证

1. 首先使用 XML 代码在 C# 中实现 IVR

创建 Softphone.cs 类

作为第一步,您需要创建 Softphone.cs 类,因为为了能够接听客户的来电,一个简单的软电话应用程序是必不可少的。要完成此步骤,请查看前面提到的视频教程。(如果您已经有一个软电话应用程序,当然也可以使用它。)

创建 ICommand.cs 类

由于有了 ICommand 接口,系统将能够处理在使用 IVR 菜单系统期间可能发生的所有可能命令(文本转语音、新菜单层级等)。正如您在以下代码片段中看到的,该接口实现了 Start()Cancel() 方法(它们可以启动和取消命令)以及 Completed 事件(以便能够检查命令是否完成)。

interface ICommand
{  
    void Start(ICall call);  
    void Cancel();  
    event EventHandler Completed;  
}

代码示例 1:创建 ICommand.cs 类

创建 Program.cs 类

Program.cs 类负责从用户处获取 SIP 账户详情以创建软电话、管理来电以及处理 XML 代码的所有组件。

如下所示,为了实现 Program.cs 类,首先您需要在 Main() 方法中调用 ShowHelp() 方法,为用户提供关于程序操作的简要介绍。为了让应用程序从用户处获取创建软电话和电话线路所需的 SIP 账户详情,您需要从 Softphone 类创建一个软电话对象,然后使用该软电话参数调用 sipAccountInitialization() 方法。现在,您需要订阅软电话的 IncomingCall 事件,以检查系统是否有呼入电话。

static void Main(string[] args)  
{  
     ShowHelp();  
 
     Softphone softphone = new Softphone();  
 
     sipAccountInitialization(softphone);  
 
     softphone.IncomigCall += softphone_IncomigCall;  
 
     Console.ReadLine();  
}

代码示例 2:Program.cs 的 Main() 方法

当发生呼入事件时,softphone_IncomingCall() 方法将被调用(代码示例 3)。以下代码片段显示,您需要创建一个 menu 变量,该变量将等于 Read_XML() 方法的返回值。

  • 如果它不为 null,将调用此菜单的 Start() 方法
  • 如果它为 null,来电将被拒绝
static void softphone_IncomigCall(object sender, Ozeki.VoIP.VoIPEventArgs<Ozeki.VoIP.IPhoneCall> e)  
{  
    var menu = ReadXML();  
 
    if (menu != null)  
         menu.Start(e.Item);  
    else  
         e.Item.Reject();  
}

代码示例 3:softphone_IncomingCall() 方法

代码示例 4 演示了 ReadXML() 方法,这是一个菜单类型的方法。它包含了 IVR 的 XML 代码。ReadXML() 方法以一个 menu 标签开始,因此它将是主菜单。当系统接听电话时,会为呼叫者播放一条问候消息(可以在下面的 <init> 部分看到)。将使用以下两个命令:

  • Speak 命令:它允许 IVR 朗读一段可选的文本消息(文本转语音)。
  • Play 命令:它使 IVR 能够播放预先录制的 mp3 消息。

下面插入的代码片段展示了一个简单的菜单示例。如果呼叫者按 1,他/她将听到一段简短的公司信息和一个预录的 mp3 文件。如果他/她按 2,系统将通知呼叫者按下了按钮 2。

现在让我们创建一个新的 Menu 对象,并使用包含 XML 代码字符串的 ivrXML 参数调用 MenuLevel() 方法,然后返回该菜单。

private static Menu ReadXML()  
{  
 
            string ivrXml = @"<ivr>  
                                <menu>  
                                    <init>  
                                        <speak>  
                                               Welcome to our Interactive Voice Menu System.
                                               To get more information about our company and hear a sample mp3 song, please press button one.  
                                               By pressing button two, you can listen an inform message  
                                        </speak>  
                                        <play>../../test.mp3</play>  
                                    </init>  
                                    <keys>  
                                        <key pressed='1'>  
                                               <speak>  
                                                      Our company is a well-known corporation. Etc.
                                               </speak>  
                                               <play>../../test.mp3</play>  
                                        </key>  
                                        <key pressed='2'>  
                                               <speak>  
                                                      You pressed button two. You did nothing.  
                                               </speak>  
                                        </key>  
                                    </keys>  
                                </menu>  
                           </ivr>";  
 
   var menu = new Menu();  
   MenuLevel(ivrXml, menu);  
   return menu;  
}

代码示例 4:ReadXML() 方法

代码示例 5 演示了 MenuLevel() 方法的实现。该方法有两个参数:

  • 字符串类型 ivrXML (它包含 XML 代码的当前菜单部分)
  • Menu 类型的 menu 对象

如下所示,整个过程都在一个 try-catch 块中——如果系统捕获到意外的异常,屏幕上将显示一条错误消息。

首先,您需要使用带 ivrXml 参数的 Parse() 方法从您的 XML 字符串加载一个 XElement。然后,选择代码的 menu 元素。如果 menuElement 变量为 null,则表示 XML 代码中没有任何 menu 标签。在这种情况下,XML 代码是不充分的,停止运行。如果没有问题,选择 menuElement 的 init 元素。需要获取 init 部分中使用的所有命令(speak、play),所以您需要用 foreach 语句迭代它们。对于每个命令,您需要调用 menu 类的 AddInitCommand() 方法。如果当前命令是 speak,则使用 SpeakCommand() 参数添加命令;如果当前命令是 play,则使用 PlayCommand() 参数。

在获取了所有命令之后,您需要选择菜单的 keys 元素。它包含了呼叫者可以按下的按键。如果没有 pressedKeyAttribute,XML 代码就是无效的。之后,获取按下的键,将其存储在 pressedKey 整数变量中,并添加属于当前键的所有命令(代码示例 5)。

private static void MenuLevel(string ivrXml, Menu menu)  
{  
        try  
        {  
                var xelement = XElement.Parse(ivrXml);  
 
                var menuElement = xelement.Element("menu");  
 
                if (menuElement == null)  
                {  
                    Console.WriteLine("Wrong XML code!");  
                    return;  
                }  
 
                var menuInit = menuElement.Element("init");  
 
                foreach (var initElements in menuInit.Elements())  
                {  
                    switch (initElements.Name.ToString())  
                    {  
                        case "play":  
                            menu.AddInitCommand(new PlayCommand(initElements.Value.ToString()));  
                            break;  
                        case "speak":  
                            menu.AddInitCommand(new SpeakCommand(initElements.Value.ToString()));  
                            break;  
                    }  
                }  
 
                var menuKeys = menuElement.Element("keys");  
 
                foreach (var key in menuKeys.Elements("key"))  
                {  
                    var pressedKeyAttribute = key.Attribute("pressed");  
 
                    if (pressedKeyAttribute == null)  
                    {  
                        Console.WriteLine("Invalid ivr xml, keypress has no value!");  
                        return;  
                    }  
 
                    int pressedKey;  
 
                    if (!Int32.TryParse(pressedKeyAttribute.Value, out pressedKey))  
                    {  
                        Console.WriteLine("You did not add any number!");  
                    }  
 
                    foreach (var element in key.Elements())  
                    {  
                        switch (element.Name.ToString())  
                        {  
                            case "play":  
                                menu.AddKeypressCommand(pressedKey, new PlayCommand(element.Value.ToString()));  
                                break;  
                            case "speak":  
                                menu.AddKeypressCommand(pressedKey, new SpeakCommand(element.Value.ToString()));  
                                break;  
                            default:  
                                return;  
                        }  
                    }  
 
                }  
            }  
        catch (Exception ex)  
        {  
                Console.WriteLine(ex.Message);  
                Console.WriteLine("Invalid ivr xml");  
        }  
}

代码示例 5:MenuLevel() 方法

创建 Menu.cs 类

Menu.cs 类负责管理 IVR 系统的菜单部分(包括主菜单和子菜单部分)。因此,这个类将管理代码的 initkeys 部分。

首先,您需要实现 ICommand 接口的所有方法和事件,并创建以下对象(代码示例 6):

Dictionary<int, MultipleCommandHandler> keys; // to store the commands with the keys
MultipleCommandHandler init; // to handle the process of the init section
ICall call; // to manage the calls
Timer greetingMessageTimer; // to repeat the greeting message
MultipleCommandHandler handler; // to handle the actual commands

代码示例 6:为实现 Menu.cs 类创建一些对象

代码示例 7 展示了 Main.cs 类的构造函数。在创建一个 Menu 对象后,您需要设置 keysinitgreetingMessageTimer 实例,然后将 greetingMessageTimer 设置为自动重置。这样,问候语将自动重复播放。

public Menu()  
{  
      keys = new Dictionary<int, MultipleCommandHandler>();  
      init = new MultipleCommandHandler();  
      greetingMessageTimer = new Timer();  
      greetingMessageTimer.AutoReset = true;  
}

代码示例 7:Main.cs 类的构造函数

启动菜单后,您需要用参数设置本地呼叫对象,订阅必要的事件(呼叫的 CallStateChangedDtmfReceived 事件;greetingMessageTimerElapsed 事件)。如代码示例 8 所示,您需要设置 greetingMessageTimer 的 Interval,接听电话,启动 greetingMessageTimer 并调用 initStart() 方法,以启动包含的命令。

public void Start(ICall call)  
{  
      this.call = call;  
      Onsubscribe();  
      greetingMessageTimer.Interval = 20000;  
      call.Accept();  
      greetingMessageTimer.Start();  
      init.Start(call);  
}

代码示例 8:Main.cs 类的 Start() 方法

call_DtmfReceived() 方法用于监听 DTMF 信号,这些信号会告知您呼叫者按下了哪个按钮。通过这种方式,您可以找出呼叫者选择了哪个菜单。当呼叫者按下按钮时,应该检查该键是否有对应的命令列表。如果为真,您需要取消订阅所有事件,并且必须订阅处理程序的 completed 事件,并使用 call 参数调用其 Start() 方法(代码示例 9)。

void call_DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)  
{  
      if (keys.TryGetValue(e.Item.Signal.Signal, out handler))  
      {  
            Unsubscribe();  
            handler.Completed += handler_Completed;  
            handler.Start(call);  
      }  
}

代码示例 9:call_DtmfReceived() 方法

代码示例 10 演示了以下三种方法:

  • Unsubscribe(): 它用于取消 init 并停止 greetingMessageTimer,因为按下的键有对应的命令。如果处理程序不为 null,则应调用其 Cancel() 方法,并且您需要取消订阅所有事件。
  • Onsubscribe(): 它用于重新订阅所有事件并启动 greetingMessageTimer
  • handler_Completed(): 当当前句柄完成后,调用 Onsubscribe 方法并启动菜单的问候语。
public void Unsubscribe()  
{  
            init.Cancel();  
            greetingMessageTimer.Stop();  
 
            if (handler != null)  
                handler.Cancel();  
 
            call.CallStateChanged -= call_CallStateChanged;  
            call.DtmfReceived -= call_DtmfReceived;  
            greetingMessageTimer.Elapsed -= greetingMessageTimer_Elapsed;  
}  
 
private void Onsubscribe()  
{  
            Unsubscribe();  
 
            call.CallStateChanged += call_CallStateChanged;  
            call.DtmfReceived += call_DtmfReceived;  
            greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;  
            greetingMessageTimer.Start();  
}  
 
void handler_Completed(object sender, EventArgs e)  
{  
            Onsubscribe();  
 
            init.Start(call);  
}

代码示例 10:Onsubscribe()、Unsubscribe() 和 handler_Completed() 方法

代码示例 11 中,您可以看到那些可用于将命令从 Program.cs 传递到 MultipleCommandHandler.cs 的方法(其实现将在下一个代码片段后描述)。

public void AddInitCommand(ICommand command)  
{  
            init.AddCommand(command);  
}  
 
public void AddKeypressCommand(int digit, ICommand command)  
{  
            if (!keys.ContainsKey(digit))  
                keys[digit] = new MultipleCommandHandler();  
 
            keys[digit].AddCommand(command);  
}

代码示例 11:AddInitCommand 和 AddKeyPressCommand

创建 MultipleCommandHandler.cs 类

如果呼叫者按下的键对应多个命令,IVR 系统需要启动所有具有相同键的命令。在这种情况下,可以使用 MultipleCommandHandler.cs 类。

首先,您需要创建以下对象。然后,您需要在类的构造函数中设置 commandList 实例(代码示例 12)。

Queue<ICommand> commandQueue; // to get tge commands in sequence
List<ICommand> commandList; // to store the commands in it
ICall call; // to manage the call
ICommand currentCommand; // to handle the current command in the list
 
public MultipleCommandHandler()  
{  
       commandList = new List<ICommand>();  
}

代码示例 12:MultipleCommandHandler.cs 所需的对象和构造函数

如下所示,如果您调用 Start() 方法,您需要使用 call 参数设置呼叫,使用 ICommand 元素设置 commandQueue 对象,并且还需要调用 StartNextCommand() 方法。

public void Start(ICall call)  
{  
    this.call = call;  
    commandQueue = new Queue<ICommand>(commandList);  
    StartNextCommand();  
}

代码示例 13:Start() 方法

代码示例 14 演示了 StartNextCommand() 方法,该方法用于检查 commandQueue。如果计数大于 0,程序会从 commandQueue 中获取第一个命令,订阅 currentCommand 的 completed 事件,取消它,最后使用 call 参数调用 Start() 方法。

OnCompleted 方法中,您需要调用 Completed 事件,然后在 currentCommand_Complete 方法中——如果当前命令已完成——启动下一个命令。

void StartNextCommand()  
{  
       if (commandQueue.Count > 0)  
       {  
             currentCommand = commandQueue.Dequeue();  
             currentCommand.Completed += currentCommand_Complete;  
             currentCommand.Cancel();  
             currentCommand.Start(call);  
       }  
 
       else  
            OnCompleted();  
}  
 
private void OnCompleted()  
{  
      var Handler = Completed;  
      if (Handler != null)   
            Handler(this, EventArgs.Empty);  
}  
 
void currentCommand_Complete(object sender, EventArgs e)  
{  
     StartNextCommand();  
}

代码示例 14:StartNextCommand() 方法

通过调用 MultipleCommandHandler.cs 类的 Cancel() 方法,您可以取消订阅 currentCommand 的 completed 事件并取消该命令。

public void Cancel()  
{  
      if (currentCommand != null)  
      {  
           currentCommand.Completed -= currentCommand_Complete;  
           currentCommand.Cancel();  
      }  
}

代码示例 15:Cancel() 方法

创建 SpeakCommand.cs 类

speak 命令(即上面提到的两个特定命令之一)允许 IVR 朗读一段首选的文本消息(文本转语音)。您的 IVR 系统将通过使用 SpeakCommand.cs 类来实现此功能。

为了能够管理 speak 命令,您需要实现以下对象。然后,您需要在构造函数中创建一个实例,通过使用 text 参数设置本地 text 变量(代码示例 16)。

ICall call;  
PhoneCallAudioSender phoneCallAudioSender;  
AudioHandler audioHandler;  
MediaConnector mediaConnector;  
string text;  
bool isStarted;  
 
public SpeakCommand(string text)  
{  
    this.text = text;  
}

代码示例 16:SpeakCommand.cs 所需的对象和构造函数

如下所示,在该类的 Start() 方法中,您需要设置对象并使用文本调用 TextToSpeech() 方法,以便将文本转换为语音。

public void Start(Ozeki.VoIP.ICall call)  
{  
     isStarted = true;  
     this.call = call;  
     phoneCallAudioSender = new PhoneCallAudioSender();  
     phoneCallAudioSender.AttachToCall(call);  
     mediaConnector = new MediaConnector();  
     TextToSpeech(text);  
}

代码示例 17:SpeakCommand.cs 类的 Start() 方法

TextToSpeech() 方法可用于将提供的文本转换为语音。为此,您需要创建一个 TextToSpeech 对象,然后将 audioHandler 设置为等于该对象。之后,您需要订阅 ttsStopped 事件,将 audioHandler 连接到 PhoneCallAudioHandler,并且还需要使用 text 参数调用 tts 的 AddAndStartText代码示例 18)。

private void TextToSpeech(string text)  
{  
    var tts = new TextToSpeech();  
    audioHandler = tts;  
    tts.Stopped += tts_Stopped;  
    mediaConnector.Connect(audioHandler, phoneCallAudioSender);  
    tts.AddAndStartText(text);  
}

代码示例 18:SpeakCommand.cs 类的 TextToSpeech() 方法

代码示例 19 所示,在 tts_Stopped() 方法中,您需要调用 SpeakCommand.cs 类的 Completed 事件,以便能够知道命令何时停止。要取消当前的 Speak 命令,您需要调用该类的 Cancel 方法。之后,您需要检查 audioHandler 是否为 null。如果不为 null,您需要将 audioHandlerphoneCallAudioHandler 断开连接,并使用 Dispose() 方法处理 audioHandler

void tts_Stopped(object sender, EventArgs e)  
{  
    if (!isStarted)  
        return;  
    isStarted = false;  
              
    var handler = Completed;  
    if (handler != null)  
        handler(this, EventArgs.Empty);  
}  
 
public void Cancel()  
{  
   if (audioHandler != null)  
   {  
       mediaConnector.Disconnect(audioHandler, phoneCallAudioSender);  
       audioHandler.Dispose();  
   }  
}

代码示例 19:tts_Stopped() 和 Cancel() 方法

创建 PlayCommand.cs 类

play 命令使 IVR 能够播放预先录制的 mp3 消息。您的 IVR 系统将通过使用 PlayCommand.cs 类来实现此功能。

为了能够管理 play 命令,您需要创建一个 MP3ToSpeech() 方法。为此,您需要使用 path 参数创建一个 MP3StreamPlayback 对象,该参数定义了您想要播放的文件的路径。之后,还需要将 audioHandler 设置为等于该对象。然后订阅 tts 的 Stopped 事件,将 audioHandler 连接到 PhoneCallAudioHandler,最后使用 text 参数调用 ttsAddAndStartText代码示例 20)。

private void MP3ToSpeaker(string path)  
{  
    DisposeMediaConnection();  
    mediaConnector = new MediaConnector();  
    var mp3Player = new MP3StreamPlayback(path);  
    audioHandler = mp3Player;  
    mp3Player.Stopped += mp3Player_Stopped;  
            
    mediaConnector.Connect(audioHandler, phoneCallAudioSender);  
    mp3Player.StartStreaming();  
}

代码示例 20:PlayCommand.cs 类的 MP3ToSpeaker() 方法

现在您有了一个单级 IVR 菜单系统。但对于 DTMF 认证来说,拥有一个多级 IVR 是必不可少的(代码示例 21)。

<ivr>  
    <menu>  
         <init>  
              <speak>  
Welcome to our Interactive Voice Menu System.
                                               To get more information about our company and hear a sample mp3 song, please press button one.  
                                               By pressing button two, you can listen an inform message  
              </speak>  
              <play>../../test.mp3</play>  
         </init>  
         <keys>  
                     <key pressed="1">  
                                   <speak>  
Our company is a well-known corporation. Etc.
                                   </speak>  
                                   <play>../../test.mp3</play>  
                    </key>  
                    <key pressed="2">  
                                   <speak>  
                                         You pressed button two. You did nothing.  
                                   </speak>  
                    </key>  
                    <key pressed="3">  
                                   <menu>  
                                         <init>  
                                               <speak>You reached the lower menu.</speak>  
                                         </init>  
                                         <keys>  
                                               <key pressed="1">  
                                                              <speak>  
                                                                    You pressed button one at the lower menu level.  
                                                              </speak>  
                                               </key>  
                                         </keys>  
                                  </menu>  
                    </key>  
          </keys>  
    </menu>  
</ivr>

代码示例 21:多级 IVR

最后,您需要在 MenuLevel() 方法中做一些修改——在您检查哪个命令属于被按下按键的部分(因为现在命令也可以是一个新菜单)。所以,让我们创建一个新的 Menu 对象,并用 AddKeypressCommand 将菜单命令添加到命令列表中。使用 innerMenu 对象和属于被按下按键的 XML 代码部分递归调用这个 MenuLevel() 方法(代码示例 22)。

foreach (var element in key.Elements())  
{  
     switch (element.Name.ToString())  
     {  
           case "play":  
                 menu.AddKeypressCommand(pressedKey, new PlayCommand(element.Value.ToString()));  
                 break;  
           case "speak":  
                 menu.AddKeypressCommand(pressedKey, new SpeakCommand(element.Value.ToString()));  
                 break;  
           case "menu":  
                 Menu innerMenu = new Menu();  
                 menu.AddKeypressCommand(pressedKey, innerMenu);  
                 MenuLevel(key.ToString(), innerMenu);  
                 break;  
           default:  
                 return;  
     }  
}

代码示例 22:MenuLevel() 方法中的修改

恭喜您完成了多级 IVR 系统!由于采用了 XML 编码,如果您想添加一些新的菜单项,就不需要修改源代码。只需在 XML 代码中写入一些新的菜单级别即可轻松实现。

现在,您已准备好实现 DTMF 认证了!

2. 实现 DTMF 认证

在这个项目中,我使用 HTTP 请求来传输 DTMF 信号(以及用户 ID)。原因很简单:这是一个平台无关的解决方案,可以用任何编程语言接收。这使得 IVR 可以与任何应用程序集成。

为了存储与用户 ID 对应的 PIN 码,我在这个项目中使用了 PHP。(当然,为此也可以使用任何数据库。)PHP 还允许您检查提供的用户 ID-PIN 码组合是否正确。

为了能够正确解释 PIN 码,您的应用程序需要能够将连续提供的多个 DTMF 信号作为一个连贯整体来管理。

创建 IVRFactory.cs 类

与 IVR XML 相关的方法已从 Program.cs 类中移出,并放入一个名为 IVRFactory.cs 的新类中。它包含 CreateIVR 方法,该方法负责解析默认的 IVR 和响应的 IVR XML(代码示例 23)。

public static ICommand CreateIVR(string ivrXml, Menu menu = null)
        {
            if (menu == null)
            {
                var responseMenu = new Menu();
 
                try
                {
                    var xelement = XElement.Parse(ivrXml);
                    var menuElement = xelement.Element("menu");
 
                    if (menuElement != null)
                    {
                        InitElement(menuElement, responseMenu);
                        KeyElement(menuElement, responseMenu, false);
                     
                        return responseMenu;
                    }
                    else
                    {
                        return PlaySpeakElement(xelement);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    Console.WriteLine("Invalid ivr xml");
                }
            }
            else
            {
                try
                {
                    var xelement = XElement.Parse(ivrXml);
                    var menuElement = xelement.Element("menu");
 
                    if (menuElement == null)
                    {
                        PlaySpeakElement(xelement, menu);
                        return menu;
                    }
 
                    ForwardToUrl(menuElement, menu);
                    InitElement(menuElement, menu);
                    KeyElement(menuElement, menu, true);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    Console.WriteLine("Invalid ivr xml");
                }
 
                return menu;
            }
            return null;
        }

代码示例 23:CreateIVR 方法

如果 CreateIVR 方法没有接收到任何菜单参数,它将检查接收到的 XML 是否包含菜单节点。

  • 如果是CreateIVR 方法将调用 Initelement()KeyElement() 方法进行解析。它返回新的菜单。(这种情况会发生在提供的 PIN 码正确,且呼叫者需要进入新的菜单级别时。)
  • 如果不是CreateIVR 方法将调用 PlaySpeakElement() 方法。(这种情况会发生在提供的 PIN 码不正确,且应通知呼叫者错误时。)

如果 CreateIVR 方法在调用时接收到一个 menu 参数,那么将调用那些生成默认 IVR 所需的方法。(只有当 CreateIVR 方法接收到用于处理的内置 ivrXml,其中包含默认菜单时,它才会接收 menu 参数。)在这种情况下,还需要调用 ForwardToUrl() 方法,该方法将 forwardToUrl 的参数转换为 ForwardToUrl 变量。这个变量包含了将要发送 HTTP 请求的 URL(代码示例 24)。

static void ForwardToUrl(XElement element, Menu menu)
{
    var forwardToUrl = element.Attribute("forwardToUrl");
 
    if (forwardToUrl != null)
    {
        string[] strings = forwardToUrl.ToString().Split('"');
        menu.ForwardToUrl = strings[1];
    }
}

代码示例 24:ForwardToUrl 方法

Menu.cs 类中的修改

现在让我们看看 Menu.cs 类,因为您需要在那做一些修改。CreateHttpRequest() 方法可以在这里找到。该方法负责发送 HTTP 请求并处理响应。如下所示CreateHttpRequest() 方法接收 URL 作为参数(即需要发送请求的路径)、电话号码以及呼叫者提供的 DTMF 信号。

string CreateHttpRequest(string url, int dtmf, string phoneNumber)
{
    // Create a request using a URL that can receive a post.
    WebRequest request = WebRequest.Create(url);
    // Set the Method property of the request to POST.
    request.Method = "POST";
     
    // Create POST data and convert it to a byte array.
    string postDtmf = dtmf.ToString();
    string callerInfo = phoneNumber;
    string postData = postDtmf + " " + callerInfo;
    byte[] byteArray = Encoding.UTF8.GetBytes(postData);
 
    // Set the ContentType and ContentLength property of the WebRequest.
    request.ContentType = "application/x-www-form-urlencoded";
    request.ContentLength = byteArray.Length;
 
    // Get the request stream and write the data in it.
    Stream dataStream = request.GetRequestStream();
    dataStream.Write(byteArray, 0, byteArray.Length);
 
    // Close the Stream object.
    dataStream.Close();
 
    // Get the response.
    WebResponse response = request.GetResponse();
     
    // Get the stream containing content returned by the server.
    dataStream = response.GetResponseStream();
    StreamReader reader = new StreamReader(dataStream);
    // Read the content.
    string responseFromServer = reader.ReadToEnd();
 
    // Clean up the streams.
    reader.Close();
    dataStream.Close();
    response.Close();
 
    return responseFromServer;
}

代码示例 25: CreateHttpRequest() 方法

上面描述的 CreateHttpRequest() 方法使用提供的 URL 创建一个 Webrequest 对象。之后,它将以 byteArray 的形式通过 POST 消息发送 DTMF 信号和电话号码。然后,它还将为响应创建一个对象。它将使用 StreamReader 将作为响应接收到的数据保存在一个字符串中。在关闭流之后,它将返回这个字符串。

如上所述,能够将连续提供的多个 DTMF 信号作为一个连贯整体来管理非常重要。如果程序连续接收到多个 DTMF 信号,它会将它们添加到一个链中。这个链本身就是 PIN 码。为此,可以使用 InitKeyPressTimeoutTimer() 方法。它在 Menu.cs 类的构造函数中被调用(代码示例 26)。

void InitKeypressTimeoutTimer()
{
    keypressTimeoutTimer = new Timer(1000);
    keypressTimeoutTimer.AutoReset = true;
    keypressTimeoutTimer.Elapsed += KeypressTimeoutElapsed;
    keypressTimeoutTimer.Start();
}
 
void KeypressTimeoutElapsed(object sender, ElapsedEventArgs e)
{
    if (!dtmfPressed)
    {
        call_DtmfReceived(sender, dtmfChain);
        dtmfChain = null;
    }
 
    dtmfPressed = false;
}

代码示例 26:InitKeyPressTimeoutTimer() 方法

如果呼叫者为了提供他/她的 PIN 码而连续按下电话按钮(即 DTMF 信号被连续接收),应用程序将把 DTMF 的值添加到链中。代码示例 27 展示了在每次 DtmfReceived 事件中使用的 DtmfReceived() 方法。

void DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)
{
    dtmfPressed = true;
    dtmfChain += DtmfNamedEventConverter.DtmfNamedEventsToString(e.Item.Signal.Signal);
}

代码示例 27:DtmfReceived() 方法

如果在一秒内没有更多的 DTMF 信号,KeypressTimeoutElapsed() 方法将通过调用 call_DtmfReceived() 方法发送该链(代码示例 28

void call_DtmfReceived(object sender, string dtmfChain)
{
    if (dtmfChain != null)
    {
        int pressedKey;
        if (!Int32.TryParse(dtmfChain, out pressedKey))
        {
            Console.WriteLine("You did not add a valid number!");
        }
 
        MultipleCommandHandler command;
        if (keys.TryGetValue(pressedKey, out command))
        {
            StartCommand(command);
        }
        else
        {
            if (ForwardToUrl != null)
            {
                ResponseXml = CreateHttpRequest(ForwardToUrl, pressedKey, call.DialInfo.UserName);
 
                var responseIvr = IVRFactory.CreateIVR(ResponseXml);
 
                StartCommand(responseIvr);
            }
            else
            {
                Console.WriteLine("This is a not used option! Please try again!");
            }
        }
    }
}

代码示例 28:call_DtmfReceived() 方法

实现必要的 PHP 部分

以下几个代码片段展示了我项目中的 PHP 部分。PHP 接收 HTTP 请求并确认提供的 PIN 码是否有效。(如果输入的 PIN 码有效,PHP 将在响应中发送 IVR 菜单。如果无效,PHP 将发送一个只包含 speak 节点的 IVR 响应。)

由于用户数据存储在多维数组中,PHP 将在遍历该数组后查找必要的信息(代码示例 29)。

$userdb=Array
(
    (0) => Array
        (
            ('uid') => '1001',
            ('balance') => '23012312',
            ('pin') => '6544'
             
        ),
 
    (1) => Array
        (
            ('uid') => '1002',
            ('balance') => '11021021',
            ('pin') => '1234'
        ),
 
    (2) => Array
        (
            ('uid') => '1003',
            ('balance') => '1012',
            ('pin') => '7658'
        )
);

代码示例 29:多维数组中的用户数据

代码示例 30 展示了 file_get_contents('php://input') 函数,它用于将 HTTP 请求存储在一个变量中。之后,您可以对 HTTP 请求进行分词,从而获取 DTML 值和电话号码。

$rawdata = file_get_contents('php://input');
     
$dtmf = strtok($rawdata, ' ');
$phoneNumber = strtok(' ');

代码示例 30:file_get_contents('php://input') 函数

如下所示,首先,searchForId() 函数检查属于接收到的电话号码的数据可以在哪个块中找到。

function searchForId($id, $array) {
   foreach ($array as $key => $val) {
       if ($val['uid'] === $id) {
           return $key;
       }
   }
   return null;
}
$id = searchForId($phoneNumber, $userdb);

代码示例 31:searchForId() 函数

最后,array_walk_recursive() 函数将在该块中检查提供的 PIN 码是否有效(代码示例 32)。

array_walk_recursive($userdb[$id], function ($item, $key) {
    global $dtmf;
    global $money;
 
    if($key == 'balance'){
        $money = $item;
    }
    if($key == 'pin'){
        if($item == $dtmf){
            echo "<response>
                    <menu>
                    <init>
                        <speak>
                            Access Granted to your bank account. If you want to know your balance, please press one.
                        </speak>
                    </init>
                    <keys>
                        <key pressed='1'>
                            <speak>
                                    You pressed button one. Your balance is $" . $money .
                            "</speak>
                        </key>
                    </keys>
                </menu>
            </response>";
        }
        else {
            echo "<response>
                    <speak>
                            Access denied to your bank account.
                    </speak>
              </response>";
        }
    }
});

代码示例 32:array_walk_recursive() 函数

摘要

总而言之,我创建了一个应用程序,可用于通过电话呼叫并使用 DTMF 信号来验证您的客户。该解决方案可以有效地用于各种 CRM(客户关系管理)系统,例如银行/电话余额查询或客户端网关入口。借助 IVR 系统和 DTMF 信号,您的客户可以通过使用他们的按键式电话键盘自动访问他们的数据(在提供他们唯一的 PIN 码之后)。为了使我的项目更易于理解,我将其分为两个主要部分:在第一部分中,我解释了如何在必要的 VoIP 组件的背景支持下,使用 C# 和 XML 代码创建一个 IVR;在第二部分中,我演示了如何使用 C#、HTTP 和 PHP 实现 DTMF 认证。

参考文献

© . All rights reserved.