Сегодня мы опишем создание простого сервиса для отправки SMS с вопросами через веб-интерфейс своим клиентам и просмотра всех полученных ответов. На базе этого решения можно построить систему автоматической обработки обратной связи, поэтому вашему маркетологу должно это понравиться.

Мы используем SMS API от Exolve, фреймворк Next.js без APP роутера и базу данных PostgreSQL. Для тестирования в локальной среде и приёма сообщений из интернета работаем с Ngrok.

Установка Next.js

Первым делом устанавливаем приложение Next.js с помощью команды:

npx create-next-app sms-feedback

Для оформления графического интерфейса менеджера, который будет делать рассылки и анализировать полученную обратную связь, при установке Next.js выбираем фреймворк для работы со стилями Tailwind CSS.

Создание интерфейса

Переходим в корень проекта, если ещё находимся не там, и создаём папку components, а в ней — компонент SmsForm.js.

import React, { useState } from 'react';
​
const SmsForm = () => {
 // Используем хук useState для управления состоянием полей ввода
 const [phoneNumbers, setPhoneNumbers] = useState(''); // хранение номеров телефонов
 const [message, setMessage] = useState(''); // хранение текста сообщения
​
 // Функция для обработки отправки формы
 const handleSubmit = async (event) => {
   event.preventDefault(); // Предотвращаем перезагрузку страницы при отправке формы
   // Делим строку, переводя в массив номеров телефонов
   const destinationNumbers = phoneNumbers.split(',').map(number => number.trim());
   
   try {
     // Отправляем POST-запрос на серверный API для отправки SMS
     const response = await fetch('/api/sendSms', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json', // Указываем тип содержимого запроса
       },
       body: JSON.stringify({
         senderNumber: 'ВАШ НОМЕР ОТПРАВИТЕЛЯ EXOLVE', // Номер отправителя
         destinationNumbers, // Массив номеров получателей
         text: message, // Текст сообщения
       })
     });
​
     // Разбираем ответ API
     const data = await response.json();
     if (response.ok) {
       alert('Сообщение успешно отправлено!'); // Показываем уведомление об успешной отправке
       setPhoneNumbers(''); // Очищаем поле ввода номеров телефонов
       setMessage(''); // Очищаем поле ввода сообщения
     } else {
       throw new Error(data.message || 'Что-то пошло не так'); // Обрабатываем ошибку, если запрос не был успешным
     }
   } catch (error) {
     // Обрабатываем ошибку при выполнении запроса
     console.error('Ошибка при отправке SMS:', error);
     alert(`Ошибка: ${error.message}`);
   }
 };
​
 return (
   <form onSubmit={handleSubmit} className="max-w-lg mx-auto my-10 p-4">
     <div className="mb-6">
       <label htmlFor="phoneNumbers" className="block mb-2 text-sm font-medium text-gray-900">
         Телефонные номера (разделите запятой):
       </label>
       <input
         type="text"
         id="phoneNumbers"
         name="phoneNumbers"
         value={phoneNumbers}
         onChange={e => setPhoneNumbers(e.target.value)}
         className="border border-blue-400 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
         placeholder="79031234567, 79031234568"
         required
       />
     </div>
     <div className="mb-6">
       <label htmlFor="message" className="block mb-2 text-sm font-medium text-gray-900">
         Сообщение:
       </label>
       <textarea
         id="message"
         name="message"
         value={message}
         onChange={e => setMessage(e.target.value)}
         className="border border-blue-400 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
         rows="4"
         required
       />
     </div>
     <button type="submit" className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center">
       Отправить сообщения
     </button>
   </form>
 );
};
​
export default SmsForm;

В результате выполнения кода будет создан компонент React SmsForm, который позволит маркетологу вводить списки из номеров телефонов и текст SMS для отправки. Этот компонент управляет состоянием полей ввода с хуками useState.

Когда пользователь отправляет форму, вызывается функция-обработчик handleSubmit, которая не даёт странице обновиться, делит строку со списком номеров и переводит в массив, отправляя POST-запрос на серверный API /api/sendSms.

Если запрос успешен, показывается уведомление, и поля ввода очищаются. В случае ошибки появится запись об этом в консоли, и пользователь увидит уведомление.

Так выглядит интерфейс маркетолога для отправки SMS
Так выглядит интерфейс маркетолога для отправки SMS

Настройка базы данных PostgreSQL

Установка PostgreSQL

Скачиваем и устанавливаем PostgreSQL. После установки создаём новую базу данных с именем sms-feedback.

Создание таблицы сообщений

Через командную строку PostgreSQL входим в эту базу данных. Для создания таблицы, которая будет хранить информацию о сообщениях, выполняем следующую команду:

CREATE TABLE messages (
    id SERIAL PRIMARY KEY,
    message_id VARCHAR(255),
    sender_number VARCHAR(50),
    recipient_numbers TEXT,
    text TEXT,
    direction VARCHAR(20),
    sent_at TIMESTAMPTZ,
    received_at TIMESTAMPTZ
);

Такая таблица будет хранить отправленные исходящие и полученные входящие сообщения.

Настройка подключения к базе данных

Переходим в корневую директорию проекта и создаём там файл db.js со следующими данными:

// db.js
import { Pool } from 'pg';

const pool = new Pool({
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_DATABASE,
  password: process.env.DB_PASSWORD,
  port: process.env.DB_PORT,
});

export default pool;

Конфигурация переменных окружения

Там же создаём файл .env и добавляем в него переменные с данными для подключения к БД и SMS API МТС Exolve:

EXOLVE_API_TOKEN=ВАШ_КЛЮЧ_API
EXOLVE_API_URL=https://api.exolve.ru/messaging/v1/SendSMS
DB_USER=postgres
DB_HOST=localhost
DB_DATABASE=sms-feedback
DB_PASSWORD=ВАШ_ПАРОЛЬ
DB_PORT=5432

База данных теперь настроена и готова к использованию в вашем проекте.

Создание API для отправки SMS

В папке api создаём файл sendSms.js со следующим кодом:

// pages/api/sendSms.js
import pool from '../../../db';
​
export default async function handler(req, res) {
 if (req.method === 'POST') {
   try {
     // Разбор тела запроса
     const { senderNumber, destinationNumbers, text } = req.body;
​
     // Проверяем, что все необходимые данные предоставлены
     if (!senderNumber || !destinationNumbers || !text) {
       return res.status(400).json({ message: 'Необходимо предоставить senderNumber, destinationNumbers и text.' });
     }
​
     // Токен аутентификации
     const token = process.env.EXOLVE_API_TOKEN;
     if (!token) {
       return res.status(500).json({ message: 'Токен аутентификации не настроен.' });
     }
​
     const sentAt = new Date(); // Время отправки сообщения
​
     // Проходим по каждому номеру телефона и шлем SMS
     for (const destination of destinationNumbers) {
       const response = await fetch(process.env.EXOLVE_API_URL, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
           'Authorization': `Bearer ${token}`
         },
         body: JSON.stringify({
           number: senderNumber,
           destination,
           text
         })
       });
​
       // Обрабатываем ответ API
       const data = await response.json();
       if (!response.ok) {
         throw new Error(data.message || 'Ошибка отправки SMS');
       }
​
       // Сохраняем отправленное сообщение в базу данных
       try {
         const insertQuery = `
           INSERT INTO messages (message_id, sender_number, recipient_numbers, text, direction, sent_at)
           VALUES ($1, $2, $3, $4, 'DIRECTION_OUTGOING', $5)
           ON CONFLICT (message_id) DO NOTHING
           RETURNING *;
         `;
         const values = [data.message_id, senderNumber, destination, text, sentAt];
         await pool.query(insertQuery, values);
       } catch (dbError) {
         console.error('Ошибка при сохранении сообщения в базу данных:', dbError);
       }
     }
​
     // Возвращаем ответ, если все SMS были успешно отправлены и сохранены
     return res.status(200).json({ message: 'Все сообщения успешно отправлены и сохранены' });
   } catch (error) {
     // Обработка ошибок при запросе к внешнему API
     console.error('Ошибка при отправке SMS:', error);
     return res.status(500).json({ message: `Ошибка сервера: ${error.message}` });
   }
 } else {
   // Отклоняем все не-POST запросы
   res.setHeader('Allow', ['POST']);
   res.status(405).end(`Метод ${req.method} не разрешен`);
 }
}

Этот код обрабатывает POST-запросы для отправки SMS и записи информации о них в базу данных. Он извлекает данные из запроса, проверяет их наличие и отправляет сообщения через внешний API. Если сообщение успешно отправлено, оно сохраняется в базе данных. В случае ошибок возвращается соответствующий статус и сообщение. Запросы с методами, отличными от POST, отклоняются.

API для приёма сообщений от пользователей через вебхук

Для мониторинга входящих сообщений мы разработаем код, который будет использовать вебхук МТС Exolve для перехвата данных, поступающих на сервер. Для этого в папке api создаём файл receiveMessage.js.

// pages/api/receiveMessage.js
import pool from '../../../db'; // Импортируем пул соединений с базой данных
​
export default async function handler(req, res) {
 if (req.method === 'POST') {
   // Достаем данные из тела запроса
   const {
     message_id,
     sender,
     receiver,
     text,
     direction
   } = req.body;
​
   // Проверяем, что все необходимые поля переданы и не содержат null
   if (!message_id || !sender || !receiver || !text || !direction) {
     return res.status(400).json({
       error: "Все поля должны быть предоставлены и не могут быть null."
     });
   }
​
   // Время получения сообщения
   const receivedAt = new Date();
​
   try {
     // Формируем SQL-запрос для вставки данных сообщения в таблицу
     const insertQuery = `
       INSERT INTO messages (message_id, sender_number, recipient_numbers, text, direction, received_at)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (message_id) DO NOTHING
       RETURNING *;
     `;
     const values = [message_id, sender, receiver, text, direction, receivedAt];
     const result = await pool.query(insertQuery, values);
​
     // Если сообщение уже существует, возвращаем ошибку 409
     if (result.rows.length === 0) {
       return res.status(409).json({
         error: "Сообщение уже существует"
       });
     }
​
     // Возвращаем вставленное сообщение с кодом 201
     res.status(201).json(result.rows[0]);
   } catch (error) {
     // Обрабатываем ошибки базы данных
     console.error('Ошибка базы данных:', error);
     res.status(500).json({
       error: 'Внутренняя ошибка сервера',
       details: error.message
     });
   }
 } else {
   // Отклоняем запросы с методами, отличными от POST
   res.status(405).end('Метод не разрешен');
 }
}

Этот код обрабатывает POST-запросы для получения сообщений и их записи в базу данных PostgreSQL. Он извлекает данные из тела запроса, проверяет их наличие и записывает их в базу данных с текущим временем получения сообщения. Если сообщение с таким message_id уже существует, оно не вставляется. Если данные успешно сохранены, возвращается ответ с кодом 201. В случае ошибки базы данных возвращается код 500. Запросы с методами, отличными от POST, отклоняются с кодом 405.

Получение сообщений из базы данных

Мы разработаем API, который позволит извлекать все сообщения из базы данных. Для этого создаём файл messages.js в папке api:

// pages/api/messages/index.js
import pool from '../../../db'; // Убедитесь, что путь к файлу конфигурации базы данных верный
​
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
// Выполняем SQL-запрос для получения всех сообщений из базы данных, отсортированных по убыванию id
const { rows } = await pool.query('SELECT * FROM messages ORDER BY id DESC');
// Возвращаем полученные строки с кодом 200
res.status(200).json(rows);
} catch (error) {
// В случае ошибки выводим сообщение об ошибке в консоль и возвращаем ошибку 500
console.error('Ошибка базы данных:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
} else {
// Если метод запроса не GET, устанавливаем заголовок Allow и возвращаем ошибку 405
res.setHeader('Allow', ['GET']);
res.status(405).end('Метод не разрешен');
}
}

Код представляет собой API-эндпоинт, который обрабатывает HTTP-запросы для получения сообщений из базы данных PostgreSQL. Он проверяет, что метод запроса — GET. Если это так, эндпоинт выполняет SQL-запрос для получения всех сообщений, отсортированных по убыванию ID, и возвращает полученные данные в ответе с кодом 200. Ошибка при выполнении SQL-запроса логируется в консоль, возвращается ответ с кодом 500 и сообщением о внутренней ошибке сервера. Если метод запроса не GET, устанавливается заголовок Allow и возвращается ошибка 405 с сообщением «Метод не разрешён».

Отрисовка сообщений

В папке components создаём компонент MessagesList.js, который будет извлекать сообщения из базы данных и отображать их на сайте. Вот основной код компонента:

// components/MessagesList.js
import React, { useEffect, useState } from 'react';
​
const MessagesList = () => {
 const [messages, setMessages] = useState([]);
 const [isLoading, setIsLoading] = useState(false);
 const [error, setError] = useState(null);
​
 useEffect(() => {
   const fetchMessages = async () => {
     setIsLoading(true);
     try {
       const res = await fetch('/api/messages');
       if (!res.ok) throw new Error('Data fetching failed');
       const data = await res.json();
       setMessages(data);
     } catch (error) {
       setError(error.message);
     }
     setIsLoading(false);
   };
​
   fetchMessages();
 }, []);
​
 if (isLoading) return <p className="text-center text-blue-500">Loading...</p>;
 if (error) return <p className="text-center text-red-500">Error: {error}</p>;
​
 return (
   <div className="max-w-4xl mx-auto mt-10">
     <h1 className="text-2xl font-bold text-center text-gray-800 mb-4">Полученные сообщения!!!!</h1>
     <ul className="divide-y divide-gray-300">
       {messages.map((message) => (
         <li key={message.id} className="p-4 hover:bg-gray-50 transition duration-150 ease-in-out">
           <p className="text-gray-600"><strong>От:</strong> {message.sender_number}</p>
           <p className="text-gray-900"><strong>Сообщение:</strong> {message.text}</p>
           <p className="text-gray-500 text-sm">
             <strong>Время:</strong> {new Date(message.direction === 'DIRECTION_OUTGOING' ? message.sent_at : message.received_at).toLocaleString()}
           </p>
         </li>
       ))}
     </ul>
   </div>
 );
};
​
export default MessagesList;

Этот компонент MessagesList загружает и выводит список сообщений, полученных с сервера. При монтировании компонента выполняется запрос к API для получения всех сообщений, и они сохраняются в состоянии messages. В случае загрузки или ошибки отображаются соответствующие сообщения. В основной части компонента рендерится список SMS с информацией о номере отправителя, тексте сообщения и времени отправки/получения.

Так выглядит полный интерфейс сервиса
Так выглядит полный интерфейс сервиса

Импорт компонентов в index.js

Важно правильно импортировать клиентские компоненты в главный файл приложения. Для этого в файле index.js добавляем следующие строки:

import SmsForm from "@/components/SmsForm";
import MessagesList from "@/components/MessagesList";
​
export default function Home() {
 return (
   <>
   <SmsForm/>
   <MessagesList/>
   </>
 );
}

Тестирование на локальном хосте

Перед тем как начать тестирование функциональности приложения на локальном сервере, устанавливаем и настраиваем ngrok для приёма внешних сообщений. Ngrok позволяет принимать запросы из интернета, направляя их на ваш локальный сервер.

Запускаем ngrok в командной строке:

ngrok http 3000

Команда активирует ngrok на порту 3000, который используется Next.js по умолчанию. Ngrok предоставит URL для перенаправления запросов на ваш локальный сервер. Примерно это будет выглядеть так:

Forwarding                    https://183d-178-140-235-215.ngrok-free.app -> http://localhost:3000

Копируем полученный URL и добавляем к нему путь к вашему API для приёма сообщений, например:

https://183d-178-140-235-215.ngrok-free.app/api/receiveSms

Этот URL нужно будет указать в настройках вебхуков для SMS в личном кабинете МТС Exolve, чтобы приложение могло принимать сообщения. После настройки можно проводить тесты: отправляйте сообщения через ваш интерфейс и проверяйте их приём и отображение в приложении.

Заключение

Мы рассмотрели, как использовать SMS API от MTС Exolve и Next.js для создания надёжных fullstack-сервисов с обработкой SMS, и работу с базами данных. Теперь вы знаете, как можно быстро создать инструмент для маркетолога, чтобы он самостоятельно не только рассылал сообщения, но и выгружал ответы.

Чтобы добиться лучших результатов, придерживайтесь передовых принципов проектирования интерфейсов. Это поможет избежать распространённой ошибки превращения кода в «спагетти-код».

Комментарии (2)


  1. CitizenOfDreams
    15.10.2024 07:07

    вашему маркетологу должно это понравиться

    А мне вот почему-то ни фига не нравится получать СМС "оцените нас по шкале от 1 до 5". Видимо, я не маркетолог.


    1. systembro
      15.10.2024 07:07

      Есть другая сторона, когда сервисы логистики пишут ВК с предложением оценить их или кидают код подтверждения только ВК, хотя очевидно, что я там постоянно не сижу, вот это зашквар