LINUX.ORG.RU

Как организовать работу с MySQL и обработку правильно?

 , ,


0

1

Всем привет!

Такое дело - перешли на IP-телефонию, используем Asterisk (IP АТС - Yeastar S20). Всё это сделали для соединения телефонии с 1С «АльфаАвто» через СофтФон. Всё круто, входящие звонки обрабатываются, исходящие тоже, если номер есть в базе - дёргается информация по контрагенту, все счастливы. Кроме одного - пропущенные звонки (на которые никто не ответил) не попадают в 1С вообще.

Поскольку на АТС есть доступ к MySQL - решил обрабатывать пропущенные звонки отдельно. В общем, поковырялся с MySQL, разобрался с PREPARE STATEMENT, сделал запрос (кому интересно - вот). Пока черновой, его ещё допилить надо (убрать дубликаты и всё такое).

Основной вопрос - как правильно сделать обработку? Моя идея была такая: делаем бота в Telegram, там же в коде каждые 5 минут дёргаем БД этим запросом. Гонять всё это буду на отдельном VPS. Наверняка тут есть люди, которым приходится обрабатывать данные в MySQL, поэтому хочется получить советов, как это лучше сделать.

Конечное решение должно каждые 5 минут проверять наличие пропущенных звонков с номеров, на которые НЕ перезвонили в течении 5 минут. Отбор этих номеров идёт через запрос в MySQL.

Как лучше и правильнее делать? Гонять промежуточную базу? А как-то можно создавать дубликат текущей базы MySQL на отдельном VPS, чтобы иметь постоянный живой бэкап в текущем времени, и его обрабатывать? Как нормализовать запросы и траффик, ведь в рабочее время эта фигня должна постоянно работать?

Кратко суть:

  • Поскольку на АТС консоль абсолютно урезана и никаких прав - обработку могу делать только на отдельном VPS (да, я в кои-то веки обзавёлся VPS).
  • Как правильнее - делать запрос с VPS? Или как-то создать на VPS клон БД с АТС?
  • В виду практически полного отсутствия опыта работы с SQL я не представляю себе, как сделать технически грамотное решение.
  • Как бы вы решили такую проблему? Повторюсь - задача: получать каждые 5 минут список пропущенных звонков, на которые операторы не перезвонили, и куда-то слать уведомление, чтобы операторы могли эту информацию посмотреть и перезвонить. Я пока остановился на варианте с ботом в Telegram.

Помогите, пожалуйста.

В свою очередь, если кого-то интересует информация по IP телефонии и интеграции с 1С СофтФон (по крайней мере, в «АльфаАвто») - могу рассказать, как это сделали мы.

★★★★

Ой, не стоит путать личный VPS с базой данных предприятия! Это очень неправильно. Только рабочий сервер. Личный VPS - для личных нужд.

Я бы это сделал простым скриптом, засунутым в крон, с пометкой уже обработанных записей. Логика такова:

1. Готовимся ловить исключения
    2. Подключаемся к базе данных и убеждаемся, что при малейшей ошибке будет сыпать исключениями.
    3. Забираем идентификатор последней записи, обработанной в предыдущем запуске скрипта и бережно сохранённой в файл/базу данных. Например, это дата звонка и последний ID.
    4. Если такого ещё нет (первый запуск), назначаем: например, дата звонка  - начало месяца, ID  - 0
    5. Если старая дата слишком стара, тебе может вывалится куча старых записей. Так что посчитай минимальную актуальную дату звонка, старше которой звонки тебе определённо не интересны. Например, трое суток. Если старая дата меньше актуальной даты, увеличь её до актуальной.
    6. Забираем ID и дату последнего звонка, до которых мы будем обрабатывать записи. Учти свои пять минут, которые нужны для определения того, что на звонок не отвечали! То есть, тебе нужен последний, но достаточно старый, звонок.
    7. Находим список проблемных звонков между старой датой/ID и новой датой/ID.
    8. Сохраняем новую дату/ID как старую дату/ID в файл/базу данных (см. пункт 3). Убеждаемся, что успешно сохранилось, если нет -  бросаем исключение.
    9. Отсылаем список звонков куда тебе надо (если в нём что-нибудь есть).
10. Если возникло исключение, отсылаем письмо себе.
11. В крон добавляем запись, чтоб наш новый скрипт выполнялся только тогда, когда надо. Например, каждые 15 минут в рабочие часы.

anonymous
()

К стати, у тебя там, наверное, в запросе ошибка - не проверяешь дат в таблицах c и p. Вдруг клиент несколько раз звонил, да ещё в разные дни? Например, звонил первого числа, а ты к нему привязал звонок двадцатого?

anonymous
()
Ответ на: комментарий от anonymous

А, и ещё, если на 6 пункте такого звонка не нашёл или его дата/ID не большее старой даты/ID, значит, новых данных нет, просто выходим из скрипта.

anonymous
()

Так как делать мне больше нечего, написал примерный скрипт, как я бы делал. Ради выразительности слегка пожертвовал «best coding practices». Да и толком не тестировал, но тебе же больше сам принцип нужен, не так ли? Скрипт на PHP, но, надеюсь, его знания тебе не понадобится - там всё очень просто и прямолинейно. Ещё и комментарии поставил, чтоб знал, на фига я эти части кода писал. В следующем комментарий - простенькие функции, которые используется в этом скрипте, ибо мне лор ругается, что слишком много питаюсь отослать. Работаю с вымышленной таблицей, так как не знаю, что там у тебя и как заполняется. Смотри под катом.

<?php

define('OLD_DATA_FILE', '/tmp/script.old-data.txt');
define('OLD_DATA_DELIMITER', "\n"); #no digits, "-", ":" or spaces
define('LAST_ERROR_FILE', '/tmp/script.is-error.txt');
define('LOCK_FILE', '/tmp/script.lock');
define('ADMIN_EMAIL', 'admin@example.com');
define('WORKERS_EMAIL', 'workers@example.com');

try {

    #trying to lock file - what if another script is already running?
    $lockFile = fopen(LOCK_FILE, 'w+');
    if (!$lockFile) {
        throw new Exception('Lock file is already locked. Heavy queries, bad file?');
    }

    #if there is no old data file, trying to init one
    if (!is_file(OLD_DATA_FILE)) {
        writeFile(OLD_DATA_FILE, OLD_DATA_DELIMITER);
    }

    #fetching old data
    list($oldDate, $oldId) = explode(OLD_DATA_DELIMITER, file_get_contents(OLD_DATA_FILE));
    $oldId = (int) $oldId;
    $oldDate = parseDate($oldDate, 'last month');

    #old date can not be too small
    $actualDate = parseDate('-3 days');
    if ($oldDate < $actualDate) {
        $oldDate = $actualDate;
    }

    #is old date in a future?
    failIfDateInAFuture($oldDate, 'Somebody is messing with us? Please fix date in file.');

    #initializing DB connection, throwing exceptions on any error
    $dbh = new PDO('mysql:host=calls_db_host;dbname=calls_db_name', 'calls_db_user', 'calls_db_password', array(
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ));

    #getting new data
    $query = 'select max(date) as date, max(id) as id from calls where date < adddate(NOW(), interval -5 minute)';
    $newDataRaw =  $dbh->query($query)->fetch();
    $newId = (int) $newDataRaw['id'];
    $newDate = parseDate($newDataRaw['date'], $oldDate);

    #no new data - nothing left to do, just close lock file and report success before exiting
    if (($newDate <= $oldDate) && ($newId <= $oldId)) {
        fclose($lockFile);
        reportSuccess();
        return;
    }

    #is new date in a future?
    failIfDateInAFuture($newDate, 'DB records corrupted?');

    #main part - fetching new records
    $callsToReport = array();
    $query = 'select * from calls where (date > ? or date = ? and id > ?) and (date < ? or date = ? and id <= ?)';
    $sth = $dbh->prepare($query);
    $sth->execute(array($oldDate, $oldDate, $oldId, $newDate, $newDate, $newId));
    while ($rw = $sth->fetch()) {
        $callsToReport[] = $rw;
    }

    #trying to save new data into file for next use
    writeFile(OLD_DATA_FILE,  $newDate . OLD_DATA_DELIMITER . $newId);

    #sending report, if there is something to report
    if ($callsToReport) {
        doMail(WORKERS_EMAIL, 'new calls', print_r($callsToReport, true));
    }

    #removing admin status file, if there were error previuosly
    reportSuccess();

    #releasing a lock
    fclose($lockFile);
    $lockFile = null;

} catch (Exception $ex) {

    #reporting error, if there were no errors previously
    if (!is_file(LAST_ERROR_FILE)) {
        $msg = $ex->getMessage();
        doMail(ADMIN_EMAIL, 'script error', $msg);
        file_put_contents(LAST_ERROR_FILE, $msg);
    }

    #releasing a lock if we have locked file
    if ($lockFile) {
        fclose($lockFile);
    }
}
anonymous
()
Ответ на: комментарий от anonymous

Функции:

### functions

function writeFile($fileName, $text)
{
    $bytesWritten = file_put_contents($fileName, $text);
    if ($bytesWritten !== strlen($text)) {
        throw new Exception("Could not write text '$text' to file '$fileName'");
    }
}

function failIfDateInAFuture($date, $explaination)
{
    $futureDate = parseDate('+1 hour'); //Possible daylight changes
    if ($date > $futureDate) {
        throw new Exception("Date '$date' is in a future. $explaination");
    }
}
function reportSuccess()
{
    if (is_file(LAST_ERROR_FILE)) {
        $deleted = unlink(LAST_ERROR_FILE);
        if ($deleted) {
            doMail(ADMIN_EMAIL, 'script is ok', 'Script is working again');
        } else {
            doMail(ADMIN_EMAIL, 'script error', 'Script is working again, but I can not delete last error file');
        }
    }
}
function parseDate($currentDate, $defaultDate = 'now')
{
    $date = ($currentDate ? $currentDate : $defaultDate);
    return date('Y-m-d H:i:s', strtotime($date));
}

function doMail($email, $subject, $body)
{
    #mail($email, $subject, $body);
    echo "TO: $email\nSUBJECT: $subject\nBODY: \n---\n$body\n---\n";
}
anonymous
()

Куча таблиц по месяцу? Это делается партициями. Их можно добавлять/удалять. Запросы пишутся как к одной таблице, а работают как с разными (там реально на каждую партицию по файлу создаётся).

disposition сделай enum

anonymous
()
Ответ на: комментарий от anonymous

Ох.

Увы, тут в локалке все компы на винде, в том числе сервер 1С. Поэтому тут ничего сделать не могу. Можно купить какой-нибудь Raspberry Pi и его гонять.

Метода обработки звонков такая: если звонок пропущен с 8:30 до 20:00 (в рабочее время, в этом диапазоне каждые 5 минут дёргаем БД запросом) - шлём уведомление о пропущенном звонке с такого-то номера, на него сразу перезванивают. Если пропущено 2 или более звонков с одного и того же номера - просто обрабатываем 1 событие (типа исключаем повторяющиеся значения из выборки). Далее идёт усложнение - если на этот номер уже поступал исходящий звонок ПОСЛЕ события пропущенного звонка - то уведомление об этом пропущенном не отсылаем.

Ну, я так вижу решение этой проблемы.

А если звонок пропущен в нерабочее время - просто утром к 8:30 сваливаем все пропущенные звонки.

ekzotech ★★★★
() автор топика
Ответ на: комментарий от anonymous

Там задача обработать пропущенный звонок в течение 5 минут.

ekzotech ★★★★
() автор топика
Ответ на: комментарий от anonymous

Ого! Спасибо, я скрипт немного по-другому представлял, в более упрощённом варианте.

ekzotech ★★★★
() автор топика
Ответ на: комментарий от anonymous

Если смотреть таблицы - то там есть cdr (пустая), cdrtemp (сколько ни смотрел - всё время пустая, возможно надо просто успеть поймать транзакцию), cdr_201707, cdr_201708 (за июль и август соответственно).

ekzotech ★★★★
() автор топика
Ответ на: комментарий от ekzotech

.. Ну, я так вижу решение этой проблемы.

Допустим, формат таблицы звонков:

calls(id, number, date, status=[ANSWERED, MISSED])

Тогда часть с «#main part - fetching new records» будет выглядеть так:

#main part - fetching new records
$queryMissed = <<<EOT
select
    number,
    max(date) as last_call_date
from calls
where
    (date > ? or date = ? and id > ?)
    and (date < ? or date = ? and id <= ?)
    and status='MISSED'
group by number
order by 2
EOT;

$sth = $dbh->prepare($queryMissed);
$sth->execute(array($oldDate, $oldDate, $oldId, $newDate, $newDate, $newId));
$callsToReport = array();
while ($rw = $sth->fetch()) {
    if ($rw['number'] && $rw['last_call_date']) {
        $callsToReport[$rw['number']] = $rw['last_call_date'];
    }
}

#removing latter answered calls
$queryAnswered = <<<EOT
select
    number,
    max(date) as last_call_date
from calls
where
    (date > ? or date = ? and id > ?)
    and status='ANSWERED'
group by number
order by 2
EOT;

$sth = $dbh->prepare($queryAnswered);
$sth->execute(array($oldDate, $oldDate, $oldId));
while ($rw = $sth->fetch()) {
    if (
        $rw['number']
        && isset($callsToReport[$rw['number']])
        && ($callsToReport[$rw['number']] <= $rw['last_call_date']) #last miss date <= last answer date
    ) {
        unset($callsToReport[$rw['number']]);
    }
}

#trying to save new data into file for next use
...
anonymous
()
Ответ на: комментарий от anonymous

Кстати, если у тебя в базе данных есть индекс по дате, запрос типа

...
(date  > ? or date = ? and id > ?)
...
(date < ? or date = ? and id <= ?)
....

лучше писать так:

...
(date >= ?) and (date > ? or date = ? and id > ?)
...
(date <= ?) and (date < ? or date = ? and id <= ?)
....

Конечно, надо не забывать про дополнительные параметры. :)

anonymous
()

По-хорошему это внутри 1С надо обрабатывать

pekmop1024 ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.