2022.04.20
Twilioを使ってエスカレ自動架電ツールを作ってみた(第二回)
皆様こんにちは!KDDIウェブコミュニケーションズ 矢田と申します。
弊社で内製したTwilioを使った運用ツールについてご紹介したく、三回に分けて記事の執筆をしております。
各回のトピックは以下となります。
- 第1回 自己紹介、開発経緯〜使用技術検討
- 第2回 ツール開発の準備〜実装内容の紹介(ブラウザ架電〜通知〜自動連続架電)
- 第3回 実装内容の紹介(エスカレリスト更新)〜番外
今回は第2回「ツール開発の準備〜実装内容の紹介(ブラウザ架電〜通知〜自動連続架電)」です。ソースコードも一部お見せするため、開発の参考になれば幸いです。
自己紹介
改めてこんにちは!KDDIウェブコミュニケーションズ 矢田と申します。技術本部 運用部でインフラエンジニアとして働いています。
主な仕事は、各サービスで使用しているサーバーやネットワーク機器の運用保守です。サービスを安定稼働させるためのメンテナンス、障害対応が日常業務ですが、業務効率化を目的としたツールの開発もおこなっています。
その中で今回は、Twilioを使った運用ツールの具体的な開発部分をご紹介したいと思います。
ツール開発の準備
開発にあたり、まず以下の準備をおこないました。
- Webサーバの用意
- ネットワーク要件の整理
- セキュリティ要件の整理
- UIの設計
- 画面遷移図の設計
上から順に詳細をご説明します。
①Webサーバの用意
Webツール(Webアプリ)を作る場合、何らかの方法でWebサーバを用意する必要があります。
AWSなどのクラウドで管理する方法もありますが、今回は弊社内で使用している仮想化基盤があったため、そちらにVMを立ち上げてWebサーバーを構築しました。
使用したOSはサポート期限が公開される前のタイミングであったことから、CentOS8を採用しました。(CentOS8は2021年12月末でEOLを迎えているため、これから構築する際は別OSをご検討ください)
また使用したミドルウェアはapache2.4となります。SSLについては社内で使っているドメインのワイルドカード証明書を適用しました。
②ネットワーク要件の整理
今回、必要な要件として以下が挙がっていました。
- WebUIへの社外からのアクセスは禁止。社内からのアクセスのみ許可。
- Twilio,TeamsのセグメントからVMへのアクセスのみ対外は許可する。
以上からローカルIPを1つ、グローバルIPを1つ取得。また上位FWで対外からのアクセスを制限しました。
③セキュリティ要件の整理
先ほど述べたネットワーク要件に沿ってセキュリティ要件を整理しました。以下の内容に沿って、上位FWおよびVM内のFWを設定しています。
- 社内NWからインターネットへの通信は全て許可(httpsなど)
- Twilio,TeamsからVMへ向けての通信はhttp,httpsのみ許可
④UIの設計・画面遷移図の設計
設計当時、UIについては運用部内で作成をおこないました。以下にUI・画面遷移図を掲載します(計6枚)
初期画面
エスカレ前の画面
エスカレ開始時の画面
エスカレ呼出成功時の画面
エスカレを継続しておこなう際の画面
ヘルプを依頼する際の画面
当時はこれらの画面遷移図を用意していましたが、こちらはあくまでプロトタイプでの遷移図となります。実際に出来上がったものについては、ヘルプ機能を削除したり色の変化をなくしたりと、いくつか省かれている部分が存在します。
以上が事前に準備した内容です。これらを整えた後にWebUIの構築を進めていきました。
ブラウザから指定した番号へ架電を実施
ここからは実装内容の紹介となります。本記事では
- ブラウザからの架電方法
- チャットツール(Teams)への通知
- 架電後、エスカレリストに従って連続架電するアルゴリズム
に絞って紹介します。
前提条件
本システムではブラウザから架電を実施しております。イメージとしては、
- 発信者:ブラウザからボタンを押して架電を実施。ブラウザからPCのマイクを介して通話
- 受信者:携帯電話に着信があり、電話に出る
となります。
ブラウザ架電はTwilioを用いて実施しており、javascriptを使うことで実装できます。ブラウザ上の架電ボタンをクリックすることでjavascriptが動作し、ブラウザ架電を可能にします。
なおこのコードを使用するには、別途TwilioのJavaScript SDを参照する必要があります。参照方法は以下の記事をご参考ください。
ブラウザを電話機に!Twilio Client ブラウザフォンの作り方
実装したコード
実際に実装したコードを以下に記載いたします。(※電話番号やドメインなどは伏せています)
※Twilio Clientはv1.11.0を利用しています。
brouser_phone.js
$(function () {
document.getElementById('button-call').style.display = 'inline';
document.getElementById('button-hangup').style.display = 'none';
var device;
//ログ出力を非表示に設定
$('#log').css('display','none');
$.getJSON('https://your-domain/capability-token')
.then(function (data) {
//console.log('Token: ' + data.token);
device = new Twilio.Device(data.token, {
region:'jp1',
codecPreferences: ['opus', 'pcmu'],
fakeLocalDTMF: true,
});
device.on('ready',function (device) {
});
device.on('error', function (error) {
});
device.on('connect', function (conn) {
document.getElementById('button-call').style.display = 'none';
document.getElementById('button-hangup').style.display = 'inline';
});
device.on('disconnect', function (conn) {
document.getElementById('button-call').style.display = 'inline';
document.getElementById('button-hangup').style.display = 'none';
});
})
.catch(function (err) {
console.log(err);
});
document.getElementById('button-call').onclick = function (){
let callFlag = confirm('エスカレ架電を行いますか?');
if(callFlag == true){
var params = {
To: '+xxxxxxxxxxx' <-Twilioユーザポータルで購入した電話番号を記載
};
console.log('Calling ' + params.To + '...');
if (device) {
device.connect(params);
}
}
};
document.getElementById('button-hangup').onclick = function () {
if (device) {
device.disconnectAll();
}
};
パーツに分けて解説いたします。
最初に以下の部分で、Twilioでの架電に必要なオブジェクトを作成します。
var device;
//ログ出力を非表示に設定
$('#log').css('display','none');
$.getJSON('https://your-domain/capability-token')
.then(function (data) {
//console.log('Token: ' + data.token);
//Twilion架電に必要なオブジェクトの作成
device = new Twilio.Device(data.token, {
region:'jp1',
codecPreferences: ['opus', 'pcmu'],
fakeLocalDTMF: true,
});
device.on('ready',function (device) {
});
device.on('error', function (error) {
});
//架電ボタンの表示切り替え
device.on('connect', function (conn) {
document.getElementById('button-call').style.display = 'none';
document.getElementById('button-hangup').style.display = 'inline';
});
device.on('disconnect', function (conn) {
document.getElementById('button-call').style.display = 'inline';
document.getElementById('button-hangup').style.display = 'none';
});
})
.catch(function (err) {
console.log(err);
});
上記のコードでオブジェクトを作成後、ボタンが押されたか判定をおこない、本当に架電するか確認します。
OKが押されたら変数に架電したい電話番号を格納、その電話番号を引数にしてconnect関数を実施することで架電ができます。またdisconnectAll関数を使ってボタンを押せば電話を切断できます。
コードとしては以下の部分となります。
document.getElementById('button-call').onclick = function (){
//電話をするかポップアップで確認
let callFlag = confirm('エスカレ架電を行いますか?');
if(callFlag == true){
var params = {
//架電先の電話番号を変数に格納
To: '+xxxxxxxxxxx' <-Twilioユーザポータルで購入した電話番号を記載
};
console.log('Calling ' + params.To + '...');
if (device) {
//電話番号を引数に架電を実施
device.connect(params);
}
}
};
document.getElementById('button-hangup').onclick = function () {
if (device) {
//実施している架電を切断
device.disconnectAll();
}
};
基本はこの流れとなります。架電先の電話番号を編集することで、固定電話や携帯電話などさまざまな番号への架電が可能です。
チャットツール(Teams)への通知
エスカレツールということもあり、架電ではなく社内チャットへのエスカレ内容の通知も必要となります。そのため本システムでは、
- Web上でエスカレ内容を記入
- ボタン押下で通知を実施
- 押下後にエスカレ架電ボタンが押下可能になり架電を実施する
という流れで構築を進めました。
その中で2番のチャットツール(Teams)への通知について、実際に実装したコードを紹介いたします。
実装したコード~事前準備~
teamsSend.php
<?php
$json_string = file_get_contents('php://input');
$data = json_decode($json_string,true);
//テスト用チャンネル
$webhook_url='TemesのWebHook URL';
//エスカレ用チャンネル
$webhook_url='TemesのWebHook URL';
if($data['time'] == "営業時間内"){
require_once '../twilio_phone/inside_access.php';
$actionUrl = "https://example.com/teams/teamsInsideUpdate.php";
}else if($data['time'] == "営業時間外"){
require_once '../twilio_phone/outside_access.php';
$actionUrl = "https://example.com/teams/teamsOutsideUpdate.php";
}
date_default_timezone_set('Asia/Tokyo');
$now = date("Ymd");
$dsn = 'mysql:dbname=hogehoge_db;host=x.x.x.x;charset=hoge;port=xxxx';
$user = 'hoge';
$password = 'hogehoge;
try {
$pdo = new PDO($dsn, $user, $password);
$pdo->beginTransaction();
$stmt = $pdo->query('update count set count = count + 1');
$stmt = $pdo->query('select lpad(count, 3, 0) from count');
$count = $stmt->fetchColumn();
$pdo->commit();
} catch (PDOException $e) {
echo "接続出来ませんでした: " . $e->getMessage() . "\n";
$pdo->rollBack();
}
$users=array();
for($i = 0 ; $i < count($names); $i++){
$add_user[$i] = ['display'=>"$names[$i]",'value'=>"$ids[$i]"];
$users[$i] = $add_user[$i];
}
$esca_num = "EC" . $now . $count;
$options = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode([
"@type"=> "MessageCard",
"@context"=> "http://schema.org/extensions",
"themeColor"=> "f54242",
"summary"=> $data['middle'].":".$data['host'],
"sections"=> [[
"activityTitle"=> "エスカレが発生しました",
"activitySubtitle"=>"エスカレリスト → https://example.com /escalation_list_edit",
"activityImage"=> "https://teamsnodesample.azurewebsites.net/static/img/image1.png",
"facts"=> [[
"name"=> "障害機器名",
"value"=> $data['host']
],[
"name"=> "エスカレ番号",
"value"=> $esca_num
],[
"name"=> "障害の内容",
"value"=> $data['prob']
],[
"name"=> "大カテゴリ",
"value"=> $data['big']
],[
"name"=> "中カテゴリ",
"value"=> $data['middle']
],[
"name"=> "時間帯",
"value"=> $data['time']
]],
"markdown"=> true
]],
"potentialAction"=> [[
"@type"=> "ActionCard",
"name"=> "エスカレ対応者を選択",
"inputs"=> [[
"@type"=> "MultichoiceInput",
"id"=> "list",
"title"=> "エスカレ対応者を選択",
"isMultiSelect"=> false,
"choices"=> $users
]],
"actions"=> [[
"@type"=> "HttpPOST",
"name"=> "エスカレリストを更新",
"target"=> $actionUrl,
"body"=> "list=",
"bodyContentType"=> "application/x-www-form-urlencoded"
]]
],
]
]),
]
];
file_get_contents($webhook_url, false, stream_context_create($options));
パーツに分けて解説いたします。
以下でチャットに通知するために必要になるWebhook URLを変数に入れます。
//テスト用チャンネル
$webhook_url='TeamsのWebHook URL';
//エスカレ用チャンネル
$webhook_url='TeamsのWebHook URL';
通知時の時間をチェックし、営業時間内か営業時間外かを判定します。この判定内容に伴い、通知後におこなうTeamsチャットからのエスカレリストの更新に用いるPHPのファイルパスを設定します。(エスカレリストの更新については第3回でご紹介いたします)
if($data['time'] == "営業時間内"){
require_once '../twilio_phone/inside_access.php';
$actionUrl = "https://hogetest.com/teams/teamsInsideUpdate.php";
}else if($data['time'] == "営業時間外"){
require_once '../twilio_phone/outside_access.php';
$actionUrl = "https://hogetest.com/teams/teamsOutsideUpdate.php";
}
併せて通知したチャットから、エスカレリストを更新する際に選択する対応者一覧をDBにアクセスして取得します。以下はmysqlのDBにアクセスする際のソースとなります。
$dsn = 'mysql:dbname=hogehoge_db;host=x.x.x.x;charset=hoge;port=xxxx';
$user = 'hoge';
$password = 'hogehoge;
try {
$pdo = new PDO($dsn, $user, $password);
$pdo->beginTransaction();
$stmt = $pdo->query('update count set count = count + 1');
$stmt = $pdo->query('select lpad(count, 3, 0) from count');
$count = $stmt->fetchColumn();
$pdo->commit();
} catch (PDOException $e) {
echo "接続出来ませんでした: " . $e->getMessage() . "\n";
$pdo->rollBack();
}
$users=array();
for($i = 0 ; $i < count($names); $i++){
$add_user[$i] = ['display'=>"$names[$i]",'value'=>"$ids[$i]"];
$users[$i] = $add_user[$i];
}
またエスカレ通知の際に、独自のエスカレ番号を発行しています。
$esca_num = "EC" . $now . $count;>
Teamsに通知するフォーマットと内容
以下はTeamsに通知する際のフォーマットおよび内容の指定となります。フォーマットの詳細についてはTeams APIのドキュメントをご参考ください。
$options = [
'http' => [
'method' => 'POST', <- 通知の際はPOSTを指定
'header' => 'Content-Type: application/json',
'content' => json_encode([
"@type"=> "MessageCard" <- Card形式を指定,
"@context"=> "http://schema.org/extensions",
"themeColor"=> "f54242", <- 通知の際の枠の色を指定
"summary"=> $data['middle'].":".$data['host'],
"sections"=> [[
"activityTitle"=> "エスカレが発生しました",
"activitySubtitle"=>"エスカレリスト → https://hogetest.com/escalation_list_edit", <-サブタイトルに
"activityImage"=> "https://teamsnodesample.azurewebsites.net/static/img/image1.png", <-通知時のアイコンを指定
//以下通知時に指定したエスカレ内容を本文に記載
"facts"=> [[
"name"=> "障害機器名",
"value"=> $data['host']
],[
"name"=> "エスカレ番号",
"value"=> $esca_num
],[
"name"=> "障害の内容",
"value"=> $data['prob']
],[
"name"=> "大カテゴリ",
"value"=> $data['big']
],[
"name"=> "中カテゴリ",
"value"=> $data['middle']
],[
"name"=> "時間帯",
"value"=> $data['time']
]],
"markdown"=> true
]],
//エスカレリスト更新に使用するパラメータ
"potentialAction"=> [[
"@type"=> "ActionCard",
"name"=> "エスカレ対応者を選択",
"inputs"=> [[
"@type"=> "MultichoiceInput",
"id"=> "list",
"title"=> "エスカレ対応者を選択",
"isMultiSelect"=> false,
"choices"=> $users
]],
"actions"=> [[
"@type"=> "HttpPOST",
"name"=> "エスカレリストを更新",
"target"=> $actionUrl,
"body"=> "list=",
"bodyContentType"=> "application/x-www-form-urlencoded"
]]
],
]
]),
]
];
最後に先ほど指定したTeamsへの通知用URL、本文内容とフォーマットを指定して通知を実施します。
file_get_contents($webhook_url, false, stream_context_create($options));
結果として、以下のような表示でTeamsに通知できます。
ブラウザからリストに沿って連続架電
さて、ここまででブラウザからの架電方法とチャットツールへの通知を実施しました。 ここからは「架電時にエスカレリストの順番に沿って自動連続架電するアルゴリズム」を作成します。
自動連続架電の仕組みについて、以下の順に説明していきます。
- エスカレリストを担当するDB部分(使用するミドルウェア、カラムの紹介)
- 自動連続架電アルゴリズムの紹介
- 実装コードの共有(本記事ではPHP版を記載)
- incoming_sequence.php(架電時に最初に動くプログラム)
- inside_access.php , outside_access.php(リストを読み込むプログラム)
①エスカレリストを担当するDB部分
今回エスカレリストを管理するにあたって、DBを使用することにしました。DBについてはMySQLを採用しています。
またエスカレリストテーブルに必要となるカラムを整理いたしました。
以下で実際に使用しているエスカレリストテーブルのカラムを紹介いたします。
Field | Type | Null | Key | Default | |
id | int(10) unsigned | NO | PRI | NULL | auto_increment |
order_number | int(11) | YES | NULL | ||
name | varchar(255) | YES | UNI | NULL | |
rest_period_start | datetime | YES | NULL | ||
rest_period_end | datetime | YES | NULL | ||
rest_flag | tinyint(1) | NO | 0 | ||
tel | text | YES | NULL | ||
fixed_flag | tinyint(1) | NO | 0 | ||
notes | text | YES | NULL | ||
change_flag | tinyint(1) | NO | 0 |
- id:DBで管理する際に一意なカラムが必要なため追加
- order_number:エスカレリストの順番(架電をおこなう順番)
- name:担当者の名前
- rest_period_start:エスカレ休止期間の開始日時
- rest_period_end:エスカレ休止期間の終了日時
- rest_flag:エスカレ休止中か判定するフラグ
- tel:担当者の携帯電話番号
- fixed_flag:エスカレリストで順番を変えず固定の順番にするかどうかのフラグ
- notes:備考欄
- change_flag:エスカレリストの順番更新時に使用するフラグ
こちらのカラムを踏まえつつ、連続架電のアルゴリズムと実装したコードを紹介いたします。
②自動連続架電アルゴリズムの紹介
自動連続架電のアルゴリズムについてフローチャートを作成いたしました。
流れとしては、架電を開始したら営業時間内・外を判定。その後該当のエスカレリストを読み込み、若番から架電を開始。繋がったら通話して架電を終了します。
もし繋がらなければ再度次の順番の担当者へ架電し、最後まで繋がらなかった場合は再度若番から架電します。 2周目でも繋がらなかった場合は架電終了となります。
③実装コードの共有(本記事ではPHP版を記載)
先述したフローチャートを基に、実際にPHPで実装を進めました。プログラムとしては
- incoming_sequence.php(架電時に最初に動くプログラム)
- inside_access.php(営業時間内にリストを読み込むプログラム)
この2つを実装いたしました。
Incoming_sequence.php
こちらは架電時に最初に動くプログラムで、連続架電部分を担っています。コメントも併せて記載していますので、ぜひご参考ください。
<?php
header('Content-Type: application/xml');
require_once '../../vendor/autoload.php';
date_default_timezone_set('Asia/Tokyo');
//架電した日が休日かどうかを判定(営業時間内・外の判定のため)
require_once 'holiday_datetime.php';
$datetime = new HolidayDateTime();
$holiday = $datetime->holiday();
$now_list = date("H:i");
$day_of_week = date("w");
if($day_of_week == 6 || $day_of_week == 0 || $holiday != false){
require_once 'outside_access.php';
}else{
if("10:00" <= $now_list && $now_list <= "19:00"){
require_once 'inside_access.php';
}else{
require_once 'outside_access.php';
}
}
//ここまで営業時間内・外判定部分
//Twilio架電に必要なライブラリの読み込み
use Twilio\TwiML\VoiceResponse;
$webhook_url = 'つながった際にチャットに担当者を通知するためのWebhook URL';
//該当エスカレリストの休止中担当者をDBから抽出
$dsn = 'mysql:dbname=hogehoge_db;host=x.x.x.x;charset=utf8;port=xxxx';
$user = 'xxx';
$password = 'xxx';
try {
$pdo = new PDO($dsn, $user, $password);
$pdo->beginTransaction();
$stmt = $pdo->query('select lpad(count, 3, 0) from count');
$count = $stmt->fetchColumn();
$pdo->commit();
} catch (PDOException $e) {
echo "接続出来ませんでした: " . $e->getMessage() . "\n";
$pdo->rollBack();
}
$response = new VoiceResponse();
$start = $_GET['startFlag'];
//1番から架電を開始
if($start == 1){
if($con_user_order_num != null){
$number_index = $con_user_order_num;
//リスト最後尾まで到達しているか判定
if($number_index == 9){
$number_index = 0;
}
//架電時のアナウンス文を作成
$response->say("エスカレを継続して、$work_names[$number_index]さんから続けて電話します" ,['voice' => 'Polly.Mizuki','language' => 'ja-JP']);
}else{
$response->say('しょうがいたいおうたんとうしゃに、じゅんばんにでんわいたします。担当者が電話に出た際、無音の時間がありますのでしばらく待機してください。' ,['voice' => 'Polly.Mizuki','language' => 'ja-JP']);
$number_index = 0;
$loop_count = 0;
}
}else{
$number_index = $_REQUEST['number_index'];
$loop_count = $_REQUEST['loop_count'];
if($loop_count == null){
$loop_count = 0;
}
if($number_index == null){
$number_index = 0;
}else{
if($con_user_order_num - 1 == $number_index){
if($con_user_order_num == 9){
$con_flag = 1;
$loop_count = 2;
}
$number_index = $number_index + 1;
}
}
}
if(isset($_POST['DialCallStatus'])){
$DialCallStatus = $_POST['DialCallStatus'];
}else{
$DialCallStatus = "";
}
$my_number = "+xxxxxxxxxxxx";
$now = date("Ymd");
if($loop_count != 0 && $number_index == 0){
$url_name = $_REQUEST['name'];
$name = urldecode($url_name);
}else{
$name = $work_names[$number_index - 1];
}
//Twilio電話番号への架電が成功したか判定
if($DialCallStatus != "completed"){
if($start == 1){
if($con_user_order_num == null){
$response->say("はじめに、$work_names[$number_index]さんにでんわします。" ,['voice' => 'Polly.Mizuki','language' => 'ja-JP']);
}
}else{
//1周目か判定
if($number_index == 0 && $loop_count == 1){
$response->say("1じゅんしたため、あらためて$work_names[$number_index]さんから架電します。" ,['voice' => 'Polly.Mizuki','language' => 'ja-JP']);
//2周目か判定
}else if($loop_count < 2 && $con_flag != 1){
$response->say("電話に応答がないため、続けて$work_names[$number_index]さんにでんわします。" ,['voice' => 'Polly.Mizuki','language' => 'ja-JP']);
}
}else if($loop_count == 2){
//3周目に到達したら架電終了
$response->say('2周して電話が繋がらないため、エスカレ架電を終了します。再度架電をお願いいたします。', ['voice' => 'Polly.Mizuki', 'language' => 'ja-JP']);
$response->hangup();
}else if($loop_count < 2){
if($number_index < $tel_count - 1){
$tmp = $number_index + 1;
$dial = $response->dial('',['callerId' => "$my_number",'timeout' => "40",'action' => "incoming_sequence.php?number_index=$tmp&loop_count=$loop_count"]);
$dial->number("$tels[$number_index]",['url' => 'calling.php']);
}else{
$tmp = $loop_count + 1;
$get_name = urlencode($work_names[$number_index]);
$dial = $response->dial('',['callerId' => "$my_number",'timeout' => "40",'action' => "incoming_sequence.php?loop_count=$tmp&name=${get_name}"]);
$dial->number("$tels[$number_index]",['url' => 'calling.php']);
}
}
}else{
$response->hangup();
$now_time = date("H:i");
if($now_time < "10:00" || "19:00" < $now_time){
try {
$pdo = new PDO($dsn, $user, $password);
$pdo->beginTransaction();
$stmt = $pdo->query("update outside_hours set continue_count = 1 where name = '${name}' and continue_flag = 1");
$pdo->commit();
} catch (PDOException $e) {
echo "接続出来ませんでした: " . $e->getMessage() . "\n";
$pdo->rollBack();
}
}
$options = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode([
"summary"=> '「'.$name.'」さんが対応します',
"text" => $esca_str
])
]
];
//電話に出た担当者をチャットに通知
file_get_contents($webhook_url, false, stream_context_create($options));
}
echo $response;
if文の判定パラメータを変えることで何周させるかを制御できます。 構築時の要件に沿って変更してください。
inside_access.php
こちらは先ほどのincoming_sequence.php内で参照する、エスカレリストを読み込むプログラムとなります。各処理部分にコメントを記載していますので、そちらもご参照ください。
<?php
$dsn = 'mysql:dbname=hogehoge_db;host=x.x.x.x;charset=utf8;port=xxxx';
$user = 'xxx';
$password = 'xxx';
$ex_user = "xx";
try {
$pdo = new PDO($dsn, $user, $password);
$pdo->beginTransaction();
$stmt = $pdo->query("select now()");
$now = $stmt->fetchColumn();
//エスカレ休止期間ではない担当者一覧を抽出
$stmt = $pdo->query("select id from inside_hours where (rest_period_start <=> null and name != '${ex_user}') or ('${now}' < rest_period_start or rest_period_end < '{$now}' and name != '${ex_user}') order by order_number");
$work_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
//エスカレ休止期間に入っている担当者を取得
$stmt = $pdo->query("select id from inside_hours where rest_flag = true and rest_period_start <= '${now}' and '${now}' <= rest_period_end and name != '${ex_user}'order by order_number");
$lest_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$ids = array_merge($work_ids,$lest_ids);
//各担当者の電話番号を取得
$stmt = $pdo->query("select tel from inside_hours where (rest_period_start <=> null and name != '${ex_user}') or ('${now}' < rest_period_start or rest_period_end < '{$now}' and name != '${ex_user}') order by order_number");
$tels = $stmt->fetchAll(PDO::FETCH_COLUMN);
//各担当者の名前を取得
$stmt = $pdo->query("select name from inside_hours where (rest_period_start <=> null and name != '${ex_user}') or ('${now}' < rest_period_start or rest_period_end < '{$now}' and name != '${ex_user}') order by order_number");
$work_names = $stmt->fetchAll(PDO::FETCH_COLUMN);
//エスカレ休止中の担当者の名前を抽出
$stmt = $pdo->query("select concat(name, '(休止中)') from inside_hours where rest_flag = true and rest_period_start <= '${now}' and '${now}' <= rest_period_end and name != '${ex_user}'order by order_number");
$lest_names = $stmt->fetchAll(PDO::FETCH_COLUMN);
$names = array_merge($work_names,$lest_names);
//かける予定の電話番号がいくつあるか確認中
$stmt = $pdo->query("select count(tel) from inside_hours where (rest_period_start <=> null and name != '${ex_user}') or ('${now}' < rest_period_start or rest_period_end < '{$now}' and name != '${ex_user}') order by order_number");
$tel_count = $stmt->fetchColumn();
$pdo->commit();
} catch (PDOException $e) {
echo "接続出来ませんでした: " . $e->getMessage() . "\n";
$pdo->rollBack();
}
以上、紹介した2つのコードを用いて連続架電をおこないます。実際は状況に併せてパラメーターを変更するなど、細かい調整が必要です。
まとめ
以上、ツール開発の準備〜実装内容までをご紹介いたしました。コードの詳細に関する紹介がやや足りないかもしれませんが、多少でも実装の参考になると幸いです。
次回は実装内容の紹介(エスカレリスト更新)〜番外となります。エスカレリスト(DB)の更新プログラムの紹介や、ローカル検証環境準備方法をご紹介できればと思います。
ここまでお読みいただきありがとうございました!次回の記事もお読みいただけると幸いです。

技術本部の運用部に所属しています。普段は、弊社提供のホスティングサービスである「CPI」のサーバ・ネットワーク運用保守を主に担当。 それ以外にも、運用を効率化するWebツールの開発および運用保守を行っています。
この記事を読んだ人へのオススメ
-
2022.05.18
-
2022.04.18
-
2022.07.01
-
2022.10.11