diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index bd97662..0eb0c0f 100755
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -2,12 +2,33 @@
## Обзор
-API Sync — однопроходная утилита (не демон), запускаемая по cron. При каждом запуске выполняет три последовательных этапа: синхронизацию данных из API в PostgreSQL, очистку устаревших записей и обновление whitelist-файла в Git-репозитории.
+API Sync — однопроходная утилита (не демон), запускаемая по cron. Поддерживает два режима запуска: основной и проверка дублей по ключу `--cd`.
+
+---
+
+## Режимы запуска
+
+```bash
+./API_sync # основной запуск
+./API_sync --cd # только проверка дублирующихся доменов/алиасов
+```
+
+Пример cron-конфигурации:
+
+```
+# Основной запуск каждые 15 минут
+*/15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1
+
+# Проверка дублей каждый час
+0 * * * * install cd /home/install && /home/install/API_sync --cd >> /var/log/API_sync.log 2>&1
+```
---
## Поток выполнения
+### Основной режим
+
```
main()
│
@@ -19,33 +40,49 @@ main()
│
├─ 4. Параллельная обработка SID (worker pool)
│ └─ processSID() для каждого SID
- │ ├─ GET /l7/resource/{sid}/global → whois
- │ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins
- │ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases
+ │ ├─ GET /l7/resource/{sid}/global → whois
+ │ │ └─ ошибка → алерт 🔴 + следующий SID
+ │ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins
+ │ │ └─ ошибка → алерт 🔴 + следующий SID
+ │ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases
+ │ │ └─ ошибка → алерт 🔴 + следующий SID
│ ├─ DNS резолв домена → проверка AntiDDOS
│ └─ upsertAPIInfo() → сравнение + запись в API_info + алерты
│
├─ 5. Очистка устаревших SID из API_info (cleanupRemovedSIDs)
│
- └─ 6. Обновление WAF whitelist (updateWAFWhitelist)
+ ├─ 6. Проверка дублирующихся доменов/алиасов (если CHECK_DUPLICATES=true)
+ │ └─ checkDuplicateDomains() → алерт 🔴 если найдены дубли
+ │
+ └─ 7. Обновление WAF whitelist (updateWAFWhitelist)
├─ git pull
- ├─ Загрузка IP из API_info (auto)
- ├─ Загрузка IP из manual_info (ручные)
+ ├─ Загрузка IP из API_info (auto-ресурсы)
+ ├─ Загрузка IP из manual_info (ручные ресурсы)
+ ├─ Объединение и дедупликация IP
├─ Фильтрация WAF-сетей (таблица ips)
├─ Сравнение с текущим whitelist-файлом
├─ Запись файла + git commit + git push
└─ Отправка алерта об изменениях
```
+### Режим --cd
+
+```
+main() --cd
+ │
+ ├─ 1. Подключение к PostgreSQL
+ └─ 2. checkDuplicateDomains() → алерт 🔴 если найдены дубли
+```
+
---
## Модули
### main.go — точка входа и worker pool
-Инициализирует подключение к БД, собирает все SID для обработки и запускает пул горутин. Количество воркеров задаётся через `MAX_CONCURRENT_WORKERS` (по умолчанию 5). SID передаются воркерам через буферизированный канал.
+При запуске с ключом `--cd` выполняет только проверку дублей и завершается. В основном режиме инициализирует подключение к БД, собирает все SID и запускает пул горутин. Количество воркеров задаётся через `MAX_CONCURRENT_WORKERS` (по умолчанию 5). SID передаются воркерам через буферизированный канал. Все воркеры работают параллельно — DNS-резолв, запросы к API и запись в БД выполняются одновременно для разных SID.
-После завершения всех воркеров последовательно запускаются очистка устаревших записей и обновление whitelist.
+После завершения всех воркеров последовательно запускаются: очистка устаревших записей, проверка дублей (если не отключена через `CHECK_DUPLICATES=false`) и обновление whitelist.
### config.go — конфигурация
@@ -53,7 +90,7 @@ main()
### api.go — HTTP-клиент и структуры API
-Содержит единственный HTTP-клиент с таймаутом 20 секунд и функцию `apiGet()`, которая выполняет GET-запрос к API API с Bearer-авторизацией и десериализует JSON-ответ в переданную структуру.
+Содержит единственный HTTP-клиент с таймаутом 20 секунд и функцию `apiGet()`, которая выполняет GET-запрос к API ServicePipe с Bearer-авторизацией и десериализует JSON-ответ в переданную структуру.
Определены структуры ответов API: `originItem`, `aliasItem`, `whoisReAPIonse`, `apiList`.
@@ -63,25 +100,31 @@ main()
**`loadL7IDs()`** — загружает SID из `apps_settings` для конкретного клиента с `mode = 'auto'`.
-**`processSID()`** — оркестрирует обработку одного SID: делает три запроса к API, резолвит домен для проверки AntiDDOS, вызывает `upsertAPIInfo()`.
+**`processSID()`** — оркестрирует обработку одного SID: делает три запроса к API, резолвит домен для проверки AntiDDOS, вызывает `upsertAPIInfo()`. При ошибке любого API-запроса отправляет алерт через `sendAPIErrorAlert()` с указанием SID, endpoint и текста ошибки.
-**`upsertAPIInfo()`** — сравнивает новые данные со старыми из БД и при наличии изменений формирует алерты:
-- изменение домена → алерт + флаг необходимости обновления PT AF
-- изменение origins (IP, mode, weight) — через `compareOrigins()`
-- изменение aliases — через `compareAliases()`
+**`sendAPIErrorAlert()`** — формирует и отправляет алерт 🔴 при ошибке запроса к API ServicePipe. Указывает SID, endpoint (`whois`, `origins`, `aliases`) и текст ошибки.
+
+**`upsertAPIInfo()`** — в начале подтягивает из БД `client_title` (из `apps_settings`) и `waf_provider` (из `client_info`) для использования в алертах. Затем сравнивает новые данные со старыми и при наличии изменений формирует алерты:
+- изменение домена → строка `Домен изменён: old -> new` + флаг необходимости обновления WAF
+- изменение origins — через `compareOrigins()`
+- изменение aliases — через `compareAliases()` + флаг обновления WAF
- изменение WAF-настроек (vendor, instance, enabled) — отдельный алерт
Сохранение выполняется через `INSERT ... ON CONFLICT DO UPDATE` (upsert по `sid`).
**`cleanupRemovedSIDs()`** — сравнивает все SID в `API_info` с актуальным списком активных SID. Записи, которых нет в активном списке (ресурс удалён из `apps_settings` или сменил `mode`), удаляются из `API_info`. По каждому удалённому ресурсу отправляется алерт с указанием WAF-вендора (берётся из `apps_settings` через LEFT JOIN).
-**`wafVendorWarning()`** — формирует строку предупреждения в зависимости от вендора: `ptaf` → PT AF, `sw` → SW, `wmx` → WMX.
+**`checkDuplicateDomains()`** — загружает все `domain_name` и `aliases` из `API_info` в единую карту. Если одно и то же значение встречается у двух и более разных SID в любой комбинации полей — считается дублем. При наличии дублей отправляется алерт 🔴 с перечислением всех совпадений и их SID. В основном режиме управляется флагом `CHECK_DUPLICATES`, при запуске с `--cd` выполняется всегда.
+
+**`wafVendorName()`** — возвращает читаемое название вендора: `ptaf` → `PT AF`, `sw` → `SW`, `wmx` → `WMX`.
+
+**`wafVendorWarning()`** — возвращает строку предупреждения для алерта об удалении ресурса с указанием конкретного WAF-вендора.
### compare.go — функции сравнения
-**`compareOrigins()`** — сравнивает два списка origins по IP-адресу. Определяет добавленные, удалённые и изменённые (mode/weight) записи. Возвращает отформатированную строку для алерта с текущим состоянием origins.
+**`compareOrigins()`** — сравнивает два списка origins по IP-адресу. Определяет добавленные, удалённые и изменённые (mode/weight) записи. Возвращает отформатированную строку с изменениями и текущим состоянием origins.
-**`compareAliases()`** — сравнивает два отсортированных списка доменов. Возвращает строку с изменениями и булев флаг наличия изменений.
+**`compareAliases()`** — сравнивает два отсортированных списка доменов. Добавленные aliases оформляются как HTML-ссылки. Возвращает строку с изменениями и булев флаг наличия изменений.
### dns.go — DNS-резолвер
@@ -100,7 +143,7 @@ main()
**Mattermost** — POST на `/api/v4/posts` с Bearer-токеном. HTML-теги Telegram конвертируются в Markdown через `htmlToMarkdown()`. Отправляется только если заполнены все три переменные: `MATTERMOST_URL`, `MATTERMOST_BOT_TOKEN`, `MATTERMOST_CHANNEL_ID`.
-**Email** — через стандартный `net/smtp`. Поддерживает несколько адресов получателей через запятую в `EMAIL_TO`. Авторизация опциональна — если `EMAIL_SMTP_USER` пуст, отправка идёт без аутентификации (relay). HTML-теги Telegram удаляются через `stripHTMLTags()`. Отправляется только если заполнены `EMAIL_SMTP_HOST`, `EMAIL_FROM`, `EMAIL_TO`.
+**Email** — через стандартный `net/smtp`. Поддерживает несколько адресов получателей через запятую в `EMAIL_TO`. При `EMAIL_SKIP_TLS_VERIFY=true` использует STARTTLS с отключённой проверкой сертификата. Механизм аутентификации выбирается автоматически по возможностям сервера: если сервер поддерживает `LOGIN` — используется он, иначе `PLAIN`. Реализован собственный `loginAuth` для совместимости с корпоративными SMTP (Exchange, Outlook). Отправляется только если заполнены `EMAIL_SMTP_HOST`, `EMAIL_FROM`, `EMAIL_TO`.
### git.go — Git-операции
@@ -118,6 +161,8 @@ main()
5. Сравнивает с текущим содержимым файла (`compareWhitelists`)
6. При наличии изменений — перезаписывает файл, коммитит и пушит, отправляет алерт
+**`loadAllOrigins()`** — загружает все origin IP из `API_info` (auto-ресурсы).
+
**`loadManualOrigins()`** — загружает все записи из `manual_info` как есть, без дополнительных условий. Таблица заполняется администратором вручную и программой не изменяется.
**`filterNonWAFOrigins()`** — исключает из списка IP-адреса, которые принадлежат WAF-сетям из таблицы `ips`. Такие адреса являются узлами самого WAF и не должны попадать в whitelist.
@@ -128,14 +173,50 @@ main()
Все алерты отправляются через единую функцию `sendAlert()`. Типы алертов:
-| Событие | Содержание |
-|---|---|
-| Изменение origins | Добавленные/удалённые/изменённые backend IP + текущее состояние |
-| Изменение aliases | Добавленные/удалённые домены |
-| Изменение домена | Старое и новое значение + предупреждение об обновлении WAF |
-| Изменение WAF-настроек | Изменения waf_enabled, waf_vendor, waf_instance |
-| Удаление ресурса | SID, домен, предупреждение с указанием конкретного WAF-вендора |
-| Обновление whitelist | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество |
+| Событие | Эмодзи | Содержание |
+|---|---|---|
+| Изменение origins/aliases/домена | 🟡 | SID, домен, TENANT, WAF provider, детали изменений, предупреждение об обновлении WAF, время |
+| Изменение WAF-настроек инстанса | ⚪ | SID, домен, TENANT, изменения waf_enabled/vendor/instance в формате `было -> стало`, время |
+| Удаление ресурса из мониторинга | 🗑 | SID, домен, предупреждение с указанием конкретного WAF-вендора |
+| Ошибка запроса к API ServicePipe | 🔴 | SID, endpoint, текст ошибки, время |
+| Дублирующиеся домены/алиасы | 🔴 | Список дублей с указанием SID, время проверки |
+| Обновление whitelist | 🔄 | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество |
+
+Предупреждение об обновлении WAF формируется динамически в зависимости от вендора: `ptaf` → PT AF, `sw` → SW, `wmx` → WMX.
+
+---
+
+## Переменные окружения
+
+| Переменная | По умолчанию | Описание |
+|---|---|---|
+| `BEARER_TOKEN` | — | Токен авторизации API ServicePipe |
+| `DB_HOST` | localhost | Хост PostgreSQL |
+| `DB_PORT` | 5432 | Порт PostgreSQL |
+| `DB_USER` | — | Пользователь БД |
+| `DB_PASSWORD` | — | Пароль БД |
+| `DB_NAME` | waf_info | Имя БД |
+| `TELEGRAM_BOT_TOKEN` | — | Токен Telegram-бота |
+| `TELEGRAM_CHAT_ID` | — | ID чата для алертов |
+| `TELEGRAM_HTTP_PROXY` | — | HTTP-прокси для Telegram |
+| `TELEGRAM_SOCKS5_PROXY` | — | SOCKS5-прокси для Telegram |
+| `TELEGRAM_SOCKS5_USER` | — | Логин SOCKS5-прокси |
+| `TELEGRAM_SOCKS5_PASSWORD` | — | Пароль SOCKS5-прокси |
+| `MATTERMOST_URL` | — | URL Mattermost-сервера |
+| `MATTERMOST_BOT_TOKEN` | — | Токен Mattermost-бота |
+| `MATTERMOST_CHANNEL_ID` | — | ID канала Mattermost |
+| `EMAIL_SMTP_HOST` | — | SMTP-сервер |
+| `EMAIL_SMTP_PORT` | 587 | SMTP-порт |
+| `EMAIL_SMTP_USER` | — | Логин SMTP |
+| `EMAIL_SMTP_PASSWORD` | — | Пароль SMTP |
+| `EMAIL_FROM` | — | Адрес отправителя |
+| `EMAIL_TO` | — | Адреса получателей (через запятую) |
+| `EMAIL_SKIP_TLS_VERIFY` | false | Отключить проверку TLS-сертификата SMTP |
+| `GIT_REPO_URL` | — | URL Git-репозитория whitelist |
+| `GIT_REPO_PATH` | — | Локальный путь к репозиторию |
+| `WHITELIST_FILE` | whitelist_ptaf.txt | Имя файла whitelist |
+| `MAX_CONCURRENT_WORKERS` | 5 | Количество параллельных воркеров |
+| `CHECK_DUPLICATES` | true | Включить проверку дублей в основном режиме |
---
@@ -147,3 +228,4 @@ main()
| `github.com/joho/godotenv` | Загрузка `.env` файла |
| `golang.org/x/net` | SOCKS5 прокси для Telegram |
| `net/smtp` | Отправка email (стандартная библиотека) |
+| `crypto/tls` | TLS для SMTP с отключённой проверкой сертификата |
\ No newline at end of file
diff --git a/README.md b/README.md
index f0a039a..87f65c4 100755
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
## Структура проекта
```
-├── main.go # Точка входа, worker pool
+├── main.go # Точка входа, worker pool, режим --cd
├── config.go # Конфигурация и переменные окружения
├── api.go # HTTP клиент и API структуры
├── database.go # Работа с PostgreSQL
@@ -37,8 +37,6 @@ go build -o API_sync
go build -ldflags="-s -w" -o API_sync
```
-Go автоматически найдёт все `.go` файлы в директории и скомпилирует их в один бинарник.
-
## Конфигурация
### Шаг 1: Создать файл конфигурации
@@ -83,14 +81,16 @@ EMAIL_SMTP_USER=user@example.com
EMAIL_SMTP_PASSWORD=your_password
EMAIL_FROM=API_sync@example.com
EMAIL_TO=admin@example.com,team@example.com
+EMAIL_SKIP_TLS_VERIFY=false
# Git репозиторий whitelist
-GIT_REPO_URL=https://token@svc-git.test.ru/wmx/waf_whitelist.git
+GIT_REPO_URL=https://token@svc-git.cirex.ru/wmx/waf_whitelist.git
GIT_REPO_PATH=/home/install/waf_whitelist
WHITELIST_FILE=whitelist_ptaf.txt
-# Параллельность
+# Параллельность и функции
MAX_CONCURRENT_WORKERS=5
+CHECK_DUPLICATES=true
```
### Шаг 3: Защитить файл конфигурации
@@ -104,14 +104,23 @@ chmod 600 API_sync.env
### Запуск вручную
```bash
+# Основной запуск
./API_sync
+
+# Только проверка дублирующихся доменов/алиасов
+./API_sync --cd
```
### Запуск через cron
```bash
# /etc/cron.d/API_sync
+
+# Основной запуск каждые 15 минут
*/15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1
+
+# Проверка дублей каждый час
+0 * * * * install cd /home/install && /home/install/API_sync --cd >> /var/log/API_sync.log 2>&1
```
## Возможности
@@ -132,6 +141,10 @@ chmod 600 API_sync.env
✅ **Очистка устаревших данных** — автоматическое удаление из API_info ресурсов, пропавших из apps_settings
+✅ **Алерты об ошибках API** — уведомление при недоступности API API для конкретного SID
+
+✅ **Проверка дублей** — поиск совпадающих доменов/алиасов между разными SID, запускается в основном режиме (управляется `CHECK_DUPLICATES`) или отдельно через `--cd`
+
✅ **Режим auto/manual** — только ресурсы с `mode = 'auto'` синхронизируются через API; ресурсы из manual_info используются как дополнительный источник IP для whitelist
## База данных
@@ -173,32 +186,34 @@ CREATE TABLE manual_info (
origins JSONB
);
--- Добавить столбец mode в apps_settings (если ещё не добавлен)
+-- Добавить столбцы в apps_settings (если ещё не добавлены)
ALTER TABLE apps_settings ADD COLUMN mode VARCHAR(10) DEFAULT 'auto';
-
--- Добавить столбец waf_vendor в apps_settings (если ещё не добавлен)
ALTER TABLE apps_settings ADD COLUMN waf_vendor VARCHAR(50);
+
+-- Добавить столбец в client_info (если ещё не добавлен)
+ALTER TABLE client_info ADD COLUMN waf_provider VARCHAR(50);
```
## Логи
-Программа выводит детальные логи:
-
```
-2025/12/26 10:00:00 Start sync API_info
-2025/12/26 10:00:00 Total SIDs to process: 25
-2025/12/26 10:00:00 Using 5 concurrent workers
-2025/12/26 10:00:01 [Worker 0] Processing SID 10307
-2025/12/26 10:00:01 [WAF Info] SID: 10307, WAF Enabled: 1, Vendor: ptaf, Instance: PTAFd_02_03
-2025/12/26 10:00:01 [AntiDDOS Check] ✅ AntiDDOS is ENABLED for nationallottery.ru
-2025/12/26 10:00:01 [Telegram] Using SOCKS5 proxy: proxy.example.com:1080
-2025/12/26 10:00:15 Finish sync API_info
-2025/12/26 10:00:15 Start cleanup of removed SIDs
-2025/12/26 10:00:15 [Cleanup] No stale SIDs found in API_info
-2025/12/26 10:00:15 Finish cleanup of removed SIDs
-2025/12/26 10:00:15 Start WAF whitelist update
-2025/12/26 10:00:16 [WAF Whitelist] No changes needed
-2025/12/26 10:00:16 Finish WAF whitelist update
+2026/06/10 10:00:00 Start sync API_info
+2026/06/10 10:00:00 Total SIDs to process: 25
+2026/06/10 10:00:00 Using 5 concurrent workers
+2026/06/10 10:00:01 [Worker 0] Processing SID 10307
+2026/06/10 10:00:01 [WAF Info] SID: 10307, WAF Enabled: 1, Vendor: ptaf, Instance: PTAFd_02_03
+2026/06/10 10:00:01 [AntiDDOS Check] ✅ AntiDDOS is ENABLED for nationallottery.ru
+2026/06/10 10:00:01 [Telegram] Using SOCKS5 proxy: proxy.example.com:1080
+2026/06/10 10:00:15 Finish sync API_info
+2026/06/10 10:00:15 Start cleanup of removed SIDs
+2026/06/10 10:00:15 [Cleanup] No stale SIDs found in API_info
+2026/06/10 10:00:15 Finish cleanup of removed SIDs
+2026/06/10 10:00:15 Start duplicate domain/alias check
+2026/06/10 10:00:15 [Duplicate Check] No duplicates found
+2026/06/10 10:00:15 Finish duplicate domain/alias check
+2026/06/10 10:00:15 Start WAF whitelist update
+2026/06/10 10:00:16 [WAF Whitelist] No changes needed
+2026/06/10 10:00:16 Finish WAF whitelist update
```
## Troubleshooting
@@ -228,3 +243,11 @@ MAX_CONCURRENT_WORKERS=10
### Telegram не доступен напрямую
Заполните переменные прокси в конфигурации. Утилита автоматически попробует HTTP-прокси, затем SOCKS5, и только если оба недоступны — прямое подключение.
+
+### Ошибка TLS при отправке email
+
+Если сертификат SMTP-сервера просрочен или невалиден:
+
+```env
+EMAIL_SKIP_TLS_VERIFY=true
+```
\ No newline at end of file
diff --git a/database.go b/database.go
index 6646fd9..ab276f0 100755
--- a/database.go
+++ b/database.go
@@ -63,18 +63,21 @@ func processSID(db *sql.DB, sid int64) error {
// WHOIS
var whois whoisResponse
if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil {
+ sendAPIErrorAlert(sid, "whois", err)
return err
}
// ORIGINS
var originsResp apiList[originItem]
if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil {
+ sendAPIErrorAlert(sid, "origins", err)
return err
}
// ALIASES
var aliasResp apiList[aliasItem]
if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil {
+ sendAPIErrorAlert(sid, "aliases", err)
return err
}
@@ -331,7 +334,7 @@ func upsertSPInfo(
wafProviderClient,
strings.Join(changeDetails, "\n"),
ptafWarning,
- now.Format("2006-01-02 15:04:05 UTC+3"),
+ now.Format("2006-01-02 15:04:05")+" UTC+3",
)
if err := sendAlert(message); err != nil {
@@ -353,7 +356,7 @@ func upsertSPInfo(
domain,
clientTitle,
strings.Join(wafChangeDetails, "\n"),
- now.Format("2006-01-02 15:04:05 UTC+3"),
+ now.Format("2006-01-02 15:04:05")+" UTC+3",
)
if err := sendAlert(wafMessage); err != nil {
@@ -366,6 +369,24 @@ func upsertSPInfo(
return nil
}
+func sendAPIErrorAlert(sid int64, endpoint string, err error) {
+ now := time.Now().In(time.FixedZone("UTC+3", 3*60*60))
+ message := fmt.Sprintf(
+ "🔴 Ошибка запроса к API ServicePipe\n\n"+
+ "SID: %d\n"+
+ "Endpoint: %s\n"+
+ "Ошибка: %s\n\n"+
+ "Время: %s",
+ sid,
+ endpoint,
+ err.Error(),
+ now.Format("2006-01-02 15:04:05")+" UTC+3",
+ )
+ if alertErr := sendAlert(message); alertErr != nil {
+ log.Printf("[API Error Alert] Failed to send alert for SID %d: %v", sid, alertErr)
+ }
+}
+
func wafVendorName(vendor string) string {
switch strings.ToLower(vendor) {
case "ptaf":
@@ -559,7 +580,7 @@ func checkDuplicateDomains(db *sql.DB) error {
}
now := time.Now().In(time.FixedZone("UTC+3", 3*60*60))
- msg.WriteString(fmt.Sprintf("\nВремя проверки: %s", now.Format("2006-01-02 15:04:05 UTC+3")))
+ msg.WriteString(fmt.Sprintf("\nВремя проверки: %s", now.Format("2006-01-02 15:04:05")+" UTC+3"))
if err := sendAlert(msg.String()); err != nil {
log.Printf("[Duplicate Check] Failed to send alert: %v", err)
diff --git a/main.go b/main.go
index 12720ca..a0ad793 100755
--- a/main.go
+++ b/main.go
@@ -2,12 +2,28 @@ package main
import (
"log"
+ "os"
"sync"
_ "github.com/lib/pq" // PostgreSQL driver
)
func main() {
+ // Режим --cd: только проверка дублей
+ if len(os.Args) > 1 && os.Args[1] == "--cd" {
+ log.Println("Running duplicate domain/alias check only")
+
+ db, err := dbConnect()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer db.Close()
+
+ if err := checkDuplicateDomains(db); err != nil {
+ log.Printf("Duplicate check error: %v", err)
+ }
+ return
+ }
log.Println("Start sync sp_info")
db, err := dbConnect()