}
} else {
$this->stdout(
- "Нет данных для отправки второго сообщения в телеграм
+ "Нет данных для отправки сообщения в Whatsapp
на {$kogortDate} для целевой даты {$targetDate}.\n",
BaseConsole::FG_RED
);
}
$this->stdout(
- "Отправка второго сообщения в телеграм для корорты (ватсап)
+ "Отправка сообщения ватсап для корорты
на {$kogortDate} для целевой даты {$targetDate}.\n",
BaseConsole::FG_GREEN
);
return ExitCode::OK;
}
+ public function actionGetWhatsappMessageHistory()
+ {
+ $this->stdout("Получаем историю сообщений...\n", BaseConsole::FG_GREEN);
+ date_default_timezone_set('Europe/Moscow');
+ $currentDate = date('Y-m-d');
+
+ $params = [
+ "offset" => 0,
+ "limit" => 1000,
+ "channelTypes" => ["WHATSAPP"],
+ "direction" => "OUT",
+ "dateFrom" => $currentDate . "T00:00:00Z",
+ "dateTo" => $currentDate . "T23:59:59Z",
+ "sort" => [
+ [
+ "property" => "messageId",
+ "direction" => "DESC"
+ ]
+ ],
+ "subjectId" => 11374
+ ];
+
+ $done = WhatsAppService::processMessagesHistoryAndUpdateStatuses($params);
+
+ if ($done) {
+ $this->stdout(
+ "Сохранение статусов успешно завершено.\n",
+ BaseConsole::FG_GREEN
+ );
+ return ExitCode::OK;
+ } else {
+ $this->stdout(
+ "Сохранение статусов не удалось.\n",
+ BaseConsole::FG_RED
+ );
+ return ExitCode::DATAERR;
+ }
+ }
+
public function actionGenerateCallKogorts()
{
$messagesSettings = UsersMessageManagement::find()->one();
--- /dev/null
+<?php
+
+use yii\db\Migration;
+
+class m250305_121956_add_column_whatsapp_settings_to_users_message_management_table extends Migration
+{
+ const TABLE_NAME = 'erp24.users_message_management';
+ /**
+ * {@inheritdoc}
+ */
+ public function safeUp()
+ {
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME) === null) {
+ return;
+ }
+
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME)->getColumn('channel_name') === null) {
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'channel_name',
+ $this->string()->null()->comment('Имя канала')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'channel_id',
+ $this->string()->null()->comment('Идентификатор канала - подпись')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'cascade_name',
+ $this->string()->null()->comment('Имя каскада')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'channel_limit',
+ $this->integer()->null()->comment('Идентификатор шаблона')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'cascade_id',
+ $this->integer()->null()->comment('ID каскада')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'subject_id',
+ $this->integer()->null()->comment('Идентификатор подписи')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'template_name',
+ $this->string()->null()->comment('Имя шаблона')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'template_id',
+ $this->integer()->null()->comment('Идентификатор шаблона')
+ );
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'callback_status_url',
+ $this->string()->null()->comment('URL приема колбеков статусов сообщений')
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function safeDown()
+ {
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME) === null) {
+ return;
+ }
+
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME)->getColumn('channel_name') !== null) {
+ $this->dropColumn(self::TABLE_NAME, 'channel_name');
+ $this->dropColumn(self::TABLE_NAME, 'channel_id');
+ $this->dropColumn(self::TABLE_NAME, 'channel_limit');
+ $this->dropColumn(self::TABLE_NAME, 'cascade_name');
+ $this->dropColumn(self::TABLE_NAME, 'cascade_id');
+ $this->dropColumn(self::TABLE_NAME, 'subject_id');
+ $this->dropColumn(self::TABLE_NAME, 'template_name');
+ $this->dropColumn(self::TABLE_NAME, 'template_id');
+ $this->dropColumn(self::TABLE_NAME, 'callback_status_url');
+ }
+ }
+
+ /*
+ // Use up()/down() to run migration code without a transaction.
+ public function up()
+ {
+
+ }
+
+ public function down()
+ {
+ echo "m250305_121956_add_column_whatsapp_settings_to_users_message_management_table cannot be reverted.\n";
+
+ return false;
+ }
+ */
+}
* @property string|null $date_end Дата и время завершения события
* @property string $test_phones_list Список тестовых телефонов
* @property string $test_phones_active Активность тестового списка
+ * @property string $channel_name Имя канала
*/
class UsersMessageManagement extends \yii\db\ActiveRecord
{
[['bonus', 'day_before_step1', 'day_before_step2', 'day_before_step3', 'date_start', 'offer_1', 'offer_2', 'offer_whatsapp', 'offer_text', 'date_last_scenario', 'created_at', 'created_by', 'updated_at', 'updated_by', 'hold'], 'required'],
[['bonus'], 'number'],
[['day_before_step1', 'day_before_step2', 'day_before_step3', 'created_by', 'updated_by',
- 'hold', 'hold_active', 'day_before_step1_active', 'day_before_step2_active', 'day_before_step3_active', 'active'], 'default', 'value' => null],
- [['day_before_step1', 'day_before_step2', 'day_before_step3', 'created_by', 'updated_by',
- 'hold', 'hold_active', 'day_before_step1_active', 'day_before_step2_active', 'day_before_step3_active', 'active', 'test_phones_active'], 'integer'],
- [['date_start', 'date_last_scenario', 'created_at', 'updated_at', 'date_end', 'test_phones_list'], 'safe'],
- [['offer_1', 'offer_2', 'offer_3', 'offer_whatsapp', 'offer_text', 'date_end', 'test_phones_list'], 'string'],
+ 'hold', 'hold_active', 'day_before_step1_active', 'day_before_step2_active', 'day_before_step3_active', 'active',
+ 'cascade_id', 'subject_id', 'template_id', 'channel_limit', 'test_phones_active'], 'integer'],
+ [['date_start', 'date_last_scenario', 'created_at', 'updated_at', 'date_end'], 'safe'],
+ [['offer_1', 'offer_2', 'offer_3', 'offer_whatsapp', 'offer_text', 'test_phones_list',
+ 'channel_name', 'channel_id', 'cascade_name', 'template_name', 'callback_status_url'], 'string'],
[['offer_1', 'offer_2'], 'string', 'max' => 10000, 'tooLong' => '{attribute} должно содержать не более 10000 символов'],
[['offer_whatsapp', 'offer_text'], 'string', 'max' => 900, 'tooLong' => '{attribute} должно содержать не более 900 символов'],
];
}
+
public function attributes()
{
return array_merge(parent::attributes(), ['bonus_action']);
'bonus_action' => 'Срок действия бонуса',
'test_phones_list' => 'Список тестовых телефонов',
'test_phones_active' => 'Активность тестовой рассылки',
+ 'channel_name' => 'Имя канала',
+ 'channel_id' => 'Идентификатор канала - подпись',
+ 'channel_limit' => 'Суточный лимит сообщений',
+ 'cascade_name' => 'Имя каскада',
+ 'cascade_id' => 'ID каскада',
+ 'subject_id' => 'Идентификатор подписи',
+ 'template_name' => 'Имя шаблона',
+ 'template_id' => 'Идентификатор шаблона',
+ 'callback_status_url' => 'URL приема колбеков статусов сообщений',
];
}
{
$bonusActivity = $this->getBonusAction();
$step1 = $this->day_before_step1;
+ $step2 = $this->day_before_step2;
+
$startDate = date('d.m.Y', strtotime("-$step1 days", strtotime($targetDate)));
$validDate = date('d.m.Y', strtotime("+$bonusActivity days", strtotime($startDate)));
+
$message = str_replace('[NumberOfBonuses]', $this->bonus, $message);
- return str_replace('[ValidityOfBonuses]', $validDate, $message);
+ $message = str_replace('[ValidityOfBonuses]', $validDate, $message);
+ $message = str_replace('[StepTwoDaysBeforeTarget]', $step2, $message);
+
+ // Нормализуем переводы строк: заменяем \r\n на \n
+ $message = str_replace("\r\n", "\n", $message);
+
+ return $message;
}
}
namespace yii_app\services;
use GuzzleHttp\Client;
-use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Yii;
-use yii\base\Arrayable;
use yii\base\Exception;
use yii_app\helpers\DataHelper;
use yii_app\records\UsersMessageManagement;
+use yii_app\records\UsersWhatsappMessage;
/**
* Сервис для отправки сообщений WhatsApp через API.
*/
public function sendMessage($requestId, $phone, $message, $isTest = false, $startTime = null, $ttl = null)
{
-
if (!$message) {
Yii::error("Текст сообщения для WhatsApp не передан.");
return null;
$buttons = [
'rows' => [
- [
'buttons' => [
[
'text' => 'Наш сайт',
'url' => 'https://bazacvetov24.ru',
- 'urlPostfix' => '',
'type' => 'URL'
],
[
'text' => '1000 руб. забрать',
'url' => 'https://bazacvetov24.ru/akcii',
- 'urlPostfix' => '',
'type' => 'URL'
]
]
- ]
]
];
// Формируем содержимое WhatsApp-сообщения
];
if (!$isTest) {
- $whatsappContent['keyboard'] = $buttons;
- $whatsappContent['messageMatcherId'] = self::getMessageMatcherIdBySubjectId($subjectId, 'kogort_message') ?? 120669;
+ // $whatsappContent['keyboard'] = $buttons;
+ $whatsappContent['messageMatcherId'] = self::getMessageMatcherIdBySubjectId($subjectId, 'kogort_message_3') ?? 121254;
} else {
$whatsappContent['messageMatcherId'] = self::getMessageMatcherIdBySubjectId($subjectId, 'podrobnosti') ?? 117216;
}
return null;
} else {
self::handleErrorResponse($response);
- return null;
}
- } catch (RequestException $e) {
+ } catch (Exception $e) {
Yii::error("Ошибка при выполнении запроса: " . $e->getMessage());
return null;
}
return null;
} else {
self::handleErrorResponse($response);
- return null;
}
- } catch (RequestException $e) {
+ } catch (Exception $e) {
Yii::error("Ошибка при выполнении запроса: " . $e->getMessage());
return null;
}
return null;
} else {
self::handleErrorResponse($response);
- return null;
}
- } catch (RequestException $e) {
+ } catch (Exception $e) {
Yii::error("Ошибка при выполнении запроса: " . $e->getMessage());
return null;
}
$errorDetail = $data['detail'] ?? '';
$errorMessages = [
+ // Ошибки из других методов API:
'error-subject-unknown' => 'Указанное имя подписи отсутствует.',
- 'error-syntax' => 'Неверно указан тип канала.',
+ 'error-syntax' => 'Неверно указан тип канала.',
+ // Дополнительные ошибки для метода messages/history:
+ 'Api-key not found' => 'Указан неверный API-ключ.',
+ 'not-valid-request' => 'Указано пустое значение параметра address.',
+ 'limit-not-valid' => 'Указано значение больше 1000 в параметре limit.',
+ 'date-range-not-valid' => 'Диапазон значений между параметрами dateFrom и dateTo превышает 366 дней.',
+ 'message-matcher-subject-not-found' => 'Указан неверный идентификатор в значении параметра subjectId.',
];
$errorMessage = $errorMessages[$errorKey] ??
"Ошибка: " . $response->getStatusCode() . ". " . $errorKey . ". " . $errorDetail;
+
Yii::error($errorMessage);
+ throw new Exception($errorMessage);
+ }
+
+ /**
+ * Получает историю исходящих сообщений по каналу WHATSAPP.
+ *
+ * @param array $params Массив параметров запроса, например:
+ * [
+ * "offset" => 0,
+ * "limit" => 1000,
+ * "channelTypes" => ["WHATSAPP"],
+ * "direction" => "OUT",
+ * "dateFrom" => "2025-03-05T00:00:00Z",
+ * "dateTo" => "2025-03-05T23:59:59Z",
+ * "sort" => [
+ * [
+ * "property" => "messageId",
+ * "direction" => "DESC"
+ * ]
+ * ],
+ * "subjectId" => 11374
+ * ]
+ *
+ * @return array Массив сообщений, если запрос выполнен успешно.
+ *
+ * @throws \Exception При ошибке выполнения запроса.
+ */
+ public static function getMessagesHistoryByDate($params)
+ {
+ $apiKey = Yii::$app->params['WHATSAPP_API_KEY'];
+ $client = new Client();
+ $url = self::$apiBaseUrl . '/messages/history';
+
+ try {
+ $response = $client->request('POST', $url, [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'X-API-KEY' => $apiKey,
+ ],
+ 'json' => $params,
+ ]);
+
+ if ($response->getStatusCode() == 200) {
+ $data = json_decode($response->getBody(), true, 512);
+ Yii::error("Контент 200: " . json_encode($data, JSON_PRETTY_PRINT));
+ return $data['content'];
+ } else {
+ Yii::error("Контент: " . json_encode($response, JSON_PRETTY_PRINT));
+ self::handleErrorResponse($response);
+ }
+ } catch (Exception $e) {
+ Yii::error("Ошибка при выполнении запроса: " . $e->getMessage());
+ return [];
+ }
+ }
+
+ public static function processMessagesHistoryAndUpdateStatuses($params)
+ {
+ $messagesHistory = self::getMessagesHistoryByDate($params);
+ if (!is_array($messagesHistory)) {
+ Yii::error("Некорректный формат истории сообщений");
+ return false;
+ }
+
+ foreach ($messagesHistory as $message) {
+ if (
+ isset($message['address']) &&
+ isset($message['sentOrReceivedAt']) &&
+ isset($message['deliveryStatus'])
+ ) {
+ $phone = $message['address'];
+ try {
+ $dateTime = new \DateTime($message['sentOrReceivedAt']);
+ } catch (\Exception $e) {
+ Yii::error("Ошибка преобразования даты из сообщения: " . $e->getMessage());
+ continue;
+ }
+ $dateTime->modify('+3 hours');
+ $formattedTime = $dateTime->format('Y-m-d H:i');
+
+ $record = UsersWhatsappMessage::find()
+ ->where(['phone' => $phone])
+ ->andWhere("TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') = :sentTime", [':sentTime' => $formattedTime])
+ ->one();
+
+ if ($record) {
+ $record->status = $message['deliveryStatus'];
+ if (!$record->save(false)) {
+ Yii::error("Не удалось сохранить статус для сообщения с телефоном {$phone} и временем {$formattedTime}");
+ } else {
+ Yii::warning("Статус для сообщения с телефоном {$phone} успешно обновлён на {$message['deliveryStatus']}");
+ }
+ } else {
+ Yii::warning("Запись с телефоном {$phone} и временем {$formattedTime} не найдена.");
+ }
+ } else {
+ Yii::warning("Некорректный формат сообщения: " . json_encode($message));
+ }
+ }
+
+ return true;
}
-}
\ No newline at end of file
+}
?>
+<style>
+ [data-bs-toggle="collapse"][aria-expanded="true"] .collapse-arrow {
+ transform: rotate(90deg);
+ }
+ .collapse-arrow {
+ transition: transform 0.3s ease;
+ display: inline-block;
+ margin-right: 8px;
+ }
+</style>
+
<div class="usersMessageManagementIndex m-5">
<?php if (Yii::$app->session->hasFlash('error')): ?>
</div>
<div class="row">
<div class="col-4 mt-2">
- Второй этап
+ Второй этап [StepTwoDaysBeforeTarget]
</div>
<div class="col-8">
<div class="d-inline-block">
<?= $form->field($model, 'offer_whatsapp')
->textarea(['rows' => 7])
->hint('Используйте [NumberOfBonuses] для отображения количества бонусов.
- <br>Для даты окончания действия бонусов - [ValidityOfBonuses].') ?>
+ <br>Для даты окончания действия бонусов - [ValidityOfBonuses].
+ <br>Для дней до окончания акции [StepTwoDaysBeforeTarget]') ?>
+ <!-- Блок настроек WhatsApp -->
+ <div class="card mt-4">
+ <div class="" data-bs-toggle="collapse" data-bs-target="#whatsAppSettings" aria-expanded="false" aria-controls="whatsAppSettings" style="cursor:pointer;">
+
+ <p class="mb-0"> <span class="collapse-arrow">▶</span> Настройки вотсап</p>
+ </div>
+ <div id="whatsAppSettings" class="collapse">
+ <div class="card-body">
+ <?= $form->field($model, 'channel_name')->textInput()->label('Имя канала') ?>
+ <?= $form->field($model, 'channel_id')->textInput()->label('Идентификатор канала - подпись') ?>
+ <?= $form->field($model, 'channel_limit')->textInput(['type' => 'number'])->label('Суточный лимит сообщений') ?>
+ <?= $form->field($model, 'cascade_name')->textInput()->label('Имя каскада') ?>
+ <?= $form->field($model, 'cascade_id')->textInput(['type' => 'number'])->label('ID каскада') ?>
+ <?= $form->field($model, 'subject_id')->textInput(['type' => 'number'])->label('Идентификатор подписи') ?>
+ <?= $form->field($model, 'template_name')->textInput()->label('Имя шаблона') ?>
+ <?= $form->field($model, 'template_id')->textInput(['type' => 'number'])->label('Идентификатор шаблона') ?>
+ <?= $form->field($model, 'callback_status_url')->textInput()->label('URL приема колбеков статусов сообщений') ?>
+ </div>
+ </div>
+ </div>
</div>
<div class="col-6">
<?= $form->field($model, 'offer_text')