Как отследить сбой обмена заказами между Битрикс и 1С и получить письмо
Зачем отдельный мониторинг
Статус обмена в административной части не всегда заметен до звонка из отдела продаж. Проще один раз вставить в точку, где обработчик заказов уже сообщил «я отработал», сохранение своей метки времени и запускать короткую CLI‑проверку по расписанию. Если давно не было живого mode=query или у свежих заказов нет отметки импорта в учётную систему, скрипт отправляет письмо с понятной темой.
Метка в сценарии обмена
В том месте штатного кода, где уже фиксируют последний экспорт (после строки с COption::SetOptionString('sale', 'last_export_time_'.$curPage, time())), добавьте фиксацию пользовательского счётчика через API настроек. Так вы отвязываете контроль от внутренних ключей и можете переименовать параметр, если в проекте заняты короткие имена.
use Bitrix\Main\Config\Option;
Option::set('sale', 'pulse/exchange/sale/query_at', (string) time());Значение читается строкой; для сравнения ниже приводится к целому временем Unix.
Почтовое событие
Создайте тип события, например MONITOR_REPORT, и шаблон с полями отправителя из системы, произвольным текстом письма и заголовком через плейсхолдеры #SUBJECT#, #MESSAGE#, #USER_MAIL#, #DEFAULT_EMAIL_FROM#. Тип тела — HTML, чтобы в уведомление можно было добавить короткий постскриптум курсивом.
Скрипт проверки на D7
Скрипт допускают только из CLI; через браузер его закрывают первой строкой. Подключается ядро через prolog_before.php, подгружаются модуль sale и основные пространства имён. Первая проверка сравнивает сохранённую метку запроса заказов с допустимым отставанием в минутах. Вторая выбирает активные заказы интернет‑магазина за последние сутки, у которых давно не было обновления статуса, и ищет запись изменения с типом импорта из 1С — при отсутствии такой строки заказ попадает в отчёт.
Уведомление не спамит: отдельная опция хранит время последней отправки и не пускает следующую раньше заданного интервала в секундах.
Автоматическое уведомление portal.example';
private string $optQuery = 'pulse/exchange/sale/query_at';
private string $optCooldown = 'pulse/exchange/sale/notify_at';
public function __construct()
{
Loader::includeModule('sale');
$this->clock = new \DateTimeImmutable('now');
}
public function execute(): void
{
try {
$this->assertFreshQueryMarker();
$this->assertImportsRegistered();
} catch (\Throwable $e) {
if ($this->cooldownExpired()) {
$this->persistCooldown();
$this->dispatchMail($e->getMessage());
}
fwrite(STDOUT, $e->getMessage() . PHP_EOL);
}
}
private function assertFreshQueryMarker(): void
{
$stored = Option::get('sale', $this->optQuery, '0');
if ($stored === '0') {
return;
}
$lastTs = (int) $stored;
$deadline = $this->clock
->modify('-' . max(1, $this->queryLagMinutes) . ' minutes')
->getTimestamp();
if ($deadline - $lastTs > 0) {
throw new \RuntimeException(
'Давно не было получения заказов: с ' . date('Y-m-d H:i:s', $lastTs)
);
}
}
private function assertImportsRegistered(): void
{
$dayBoundary = $this->clock->modify('-24 hours')->format('d.m.Y H:i:s');
$stalePivot = $this->clock
->modify('-' . max(1, $this->confirmLagMinutes) . ' minutes')
->format('d.m.Y H:i:s');
$cursor = OrderTable::getList([
'filter' => [
'=CANCELED' => 'N',
'>DATE_INSERT' => $dayBoundary,
'<DATE_UPDATE' => $stalePivot,
'=LID' => LANGUAGE_ID,
],
'select' => ['ID', 'DATE_INSERT'],
'order' => ['ID' => 'DESC'],
]);
$pending = [];
while ($row = $cursor->fetch()) {
$hasImport = OrderChangeTable::getList([
'filter' => [
'=ORDER_ID' => $row['ID'],
'=TYPE' => 'ORDER_1C_IMPORT',
],
'select' => ['ID'],
'limit' => 1,
])->fetch();
if (!$hasImport) {
$pending[] = $row['ID'] . ' [' . $row['DATE_INSERT'] . ']';
}
}
if ($pending !== []) {
throw new \RuntimeException(
'Не были импортированы в 1С:
' . implode('
', $pending)
);
}
}
private function cooldownExpired(): bool
{
$last = (int) Option::get('sale', $this->optCooldown, '0');
if ($last === 0) {
return true;
}
return $this->clock->getTimestamp() - $last > $this->repeatCooldownSec;
}
private function persistCooldown(): void
{
Option::set('sale', $this->optCooldown, (string) $this->clock->getTimestamp());
}
private function dispatchMail(string $bodyHtml): void
{
Event::sendImmediate([
'EVENT_NAME' => 'MONITOR_REPORT',
'LID' => LANGUAGE_ID,
'C_FIELDS' => [
'USER_MAIL' => $this->recipientCsv,
'SUBJECT' => $this->mailSubject,
'MESSAGE' => '' . $bodyHtml . '
' . $this->footerHtml,
],
]);
}
}
(new SaleBridgePulseMonitor())->execute();Расписание cron
Запускайте файл каждые десять минут в рабочее окно, чтобы не дергать сайт ночью без необходимости. Путь к PHP и к каталогу проекта замените на свои.
*/10 8-20 * * 1-5 /usr/bin/php /var/www/site/cron/monitor-sale-bridge.phpНа что смотреть после внедрения
Убедитесь, что служебный тип события привязан к активному шаблону и адреса в строке получателей не режет корпоративный антиспам. При смене идентификатора сайта обновите константу и фильтр по полю LID. Если в вашей версии торгового модуля тип следов обмена называется иначе, скорректируйте строку фильтра к фактическому коду записи журнала заказов.
Не хотите копаться сами?
Починю за 1-3 дня. Без предоплаты — оплата по результату.
15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии