Утро, аромат свежесваренного кофе, и телефон тихонько напоминает вам о приеме важного лекарства. «Привет! Не забудь принять лекарство!» Такую систему можно реализовать самостоятельно с помощью Golang и Exolve API.
SMS остается простым, универсальным и проверенным временем методом коммуникации, работающим на любом телефоне. В этой статье я покажу, как клиники и медцентры могут построить надежную систему SMS-напоминаний.
Что понадобится:
- Golang: язык программирования, идеально подходящий для создания серверных приложений. 
- Exolve SMS API: инструмент для отправки и управления SMS-сообщениями. 
- Docker: для контейнеризации приложения. 
- PostgreSQL: надежная БД для хранения информации о пациентах и напоминаниях. 
- ngrok: для тестирования вебхуков и локального сервера (опционально). 
Предлагаю следующую структуру проекта:
sms-reminder/
├── main.go
├── handlers/
│   └── reminder.go
├── models/
│   └── patient.go
├── utils/
│   └── exolve.go
├── config/
│   └── config.go
├── Dockerfile
├── .env
└── go.modТеперь перейдем к реализации.
Реализация системы
Конфигурация
Сначала создадим файл config/config.go для хранения конфигурационных параметров, включая API-ключ Exolve:
package config
import (
    "log"
    "os"
    "github.com/joho/godotenv"
)
// Config структура для хранения конфигурации приложения
type Config struct {
    ExolveAPIKey string
    Port         string
    DBConnString string
}
// LoadConfig загружает конфигурацию из .env файла
func LoadConfig() Config {
    // Загружаем переменные окружения из .env файла
    err := godotenv.Load()
    if err != nil {
        log.Fatalf("Ошибка загрузки .env файла: %v", err)
    }
    // Возвращаем конфигурацию
    return Config{
        ExolveAPIKey: os.Getenv("EXOLVE_API_KEY"),
        Port:         os.Getenv("PORT"),
        DBConnString: os.Getenv("DB_CONN_STRING"),
    }
}Не забываем создать .env файл с необходимыми переменными:
EXOLVE_API_KEY=Bearer your_exolve_api_key
PORT=8080
DB_CONN_STRING=postgres://user:password@localhost:5432/sms_reminder?sslmode=disableНикогда не храните конфиденциальные данные в коде. Используйте переменные окружения для защиты ключей и паролей. Добавляем .env файл в .gitignore, чтобы он не попал в систему контроля версий.
Модели
В models/patient.go опишем структуру пациента:
package models
import "time"
// Patient структура для хранения информации о пациенте
type Patient struct {
    ID           int       `json:"id"`            // Уникальный идентификатор пациента
    Name         string    `json:"name"`          // Имя пациента
    PhoneNumber  string    `json:"phone_number"`  // Номер телефона пациента
    Email        string    `json:"email"`         // Электронная почта пациента
    DrugName     string    `json:"drug_name"`     // Название лекарства
    Dosage       string    `json:"dosage"`        // Дозировка лекарства
    ReminderTime time.Time `json:"reminder_time"` // Время напоминания (в формате HH:MM:SS)
    CreatedAt    time.Time `json:"created_at"`    // Время создания записи
    UpdatedAt    time.Time `json:"updated_at"`    // Время последнего обновления записи
}Интегрируемся с Exolve API
В utils/exolve.go реализуем функции для взаимодействия с Exolve SMS API:
package utils
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)
// ExolveClient структура для взаимодействия с Exolve API
type ExolveClient struct {
    APIKey string // API-ключ для авторизации
}
// NewExolveClient создает новый экземпляр ExolveClient
func NewExolveClient(apiKey string) *ExolveClient {
    return &ExolveClient{APIKey: apiKey}
}
// SendSMSRequest структура запроса для отправки SMS
type SendSMSRequest struct {
    Number              string  `json:"number"`                        // Номер отправителя или альфа-имя
    Destination         string  `json:"destination"`                   // Номер получателя
    Text                string  `json:"text"`                          // Текст сообщения
    TemplateResourceID  *uint64 `json:"template_resource_id,omitempty"` // Идентификатор шаблона (опционально)
}
// SendSMSResponse структура ответа от Exolve API при отправке SMS
type SendSMSResponse struct {
    MessageID string `json:"message_id"` // Уникальный идентификатор сообщения
}
// SendSMS отправляет SMS-сообщение через Exolve API
func (c *ExolveClient) SendSMS(req SendSMSRequest) (SendSMSResponse, error) {
    url := "https://api.exolve.ru/messaging/v1/SendSMS" // Точка подключения API
    jsonData, err := json.Marshal(req) // Преобразуем запрос в JSON
    if err != nil {
        return SendSMSResponse{}, fmt.Errorf("ошибка маршалинга запроса: %v", err)
    }
    // Создаем новый HTTP-запрос
    httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        return SendSMSResponse{}, fmt.Errorf("ошибка создания HTTP-запроса: %v", err)
    }
    // Устанавливаем заголовки
    httpReq.Header.Set("Authorization", c.APIKey)
    httpReq.Header.Set("Content-Type", "application/json")
    client := &http.Client{}
    resp, err := client.Do(httpReq) // Отправляем запрос
    if err != nil {
        return SendSMSResponse{}, fmt.Errorf("ошибка отправки HTTP-запроса: %v", err)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body) // Читаем тело ответа
    if err != nil {
        return SendSMSResponse{}, fmt.Errorf("ошибка чтения ответа: %v", err)
    }
    if resp.StatusCode != http.StatusOK { // Проверяем статус ответа
        return SendSMSResponse{}, fmt.Errorf("Exolve API ошибка: %s", string(body))
    }
    var sendResp SendSMSResponse
    err = json.Unmarshal(body, &sendResp) // Парсим ответ
    if err != nil {
        return SendSMSResponse{}, fmt.Errorf("ошибка парсинга ответа: %v", err)
    }
    return sendResp, nil // Возвращаем результат
}ExolveClient — структура для работы с Exolve API, включая отправку SMS через метод SendSMS. Метод формирует JSON-запрос, отправляет его с нужными заголовками, обрабатывает ответ и извлекает MessageID.
Обработчики
В handlers/reminder.go создадим обработчики для добавления пациента и отправки напоминаний:
package handlers
import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "time"
    "sms-reminder/models"
    "sms-reminder/utils"
    _ "github.com/lib/pq" // Драйвер PostgreSQL
)
// ReminderHandler структура для обработки HTTP-запросов
type ReminderHandler struct {
    DB           *sql.DB            // Подключение к базе данных
    ExolveClient *utils.ExolveClient // Клиент для взаимодействия с Exolve API
}
// NewReminderHandler создает новый экземпляр ReminderHandler
func NewReminderHandler(db *sql.DB, exolveClient *utils.ExolveClient) *ReminderHandler {
    return &ReminderHandler{
        DB:           db,
        ExolveClient: exolveClient,
    }
}
// AddPatient обрабатывает POST-запрос для добавления нового пациента
func (h *ReminderHandler) AddPatient(w http.ResponseWriter, r *http.Request) {
    var patient models.Patient
    // Декодируем JSON-запрос в структуру Patient
    err := json.NewDecoder(r.Body).Decode(&patient)
    if err != nil {
        http.Error(w, "Неверный формат запроса", http.StatusBadRequest)
        return
    }
    // Валидация входных данных
    if patient.Name == "" || patient.PhoneNumber == "" || patient.DrugName == "" || patient.Dosage == "" || patient.ReminderTime.IsZero() {
        http.Error(w, "Не все обязательные поля заполнены", http.StatusBadRequest)
        return
    }
    // SQL-запрос для вставки нового пациента в базу данных
    query := `INSERT INTO patients (name, phone_number, email, drug_name, dosage, reminder_time, created_at, updated_at)
              VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING id`
    
    // Выполняем запрос и получаем ID нового пациента
    err = h.DB.QueryRow(query, patient.Name, patient.PhoneNumber, patient.Email, patient.DrugName, patient.Dosage, patient.ReminderTime.Format("15:04:05")).Scan(&patient.ID)
    if err != nil {
        log.Printf("Ошибка вставки в базу данных: %v", err)
        http.Error(w, "Ошибка базы данных", http.StatusInternalServerError)
        return
    }
    // Устанавливаем статус ответа и отправляем информацию о пациенте
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(patient)
}
// SendReminders обрабатывает GET-запрос для отправки напоминаний
func (h *ReminderHandler) SendReminders(w http.ResponseWriter, r *http.Request) {
    currentTime := time.Now().Format("15:04:05") // Получаем текущее время в формате HH:MM:SS
    // SQL-запрос для выборки пациентов, у которых время напоминания совпадает с текущим временем
    query := `SELECT id, name, phone_number, drug_name, dosage FROM patients WHERE reminder_time = $1`
    rows, err := h.DB.Query(query, currentTime)
    if err != nil {
        log.Printf("Ошибка выборки из базы данных: %v", err)
        http.Error(w, "Ошибка базы данных", http.StatusInternalServerError)
        return
    }
    defer rows.Close()
    // Обрабатываем каждого пациента и отправляем ему SMS-напоминание
    for rows.Next() {
        var p models.Patient
        err := rows.Scan(&p.ID, &p.Name, &p.PhoneNumber, &p.DrugName, &p.Dosage)
        if err != nil {
            log.Printf("Ошибка сканирования строки: %v", err)
            continue
        }
        // Формируем текст напоминания
        message := "Привет " + p.Name + "! Это напоминание принять ваше лекарство: " + p.DrugName + ". Дозировка: " + p.Dosage + "."
        // Создаем запрос для отправки SMS
        sendReq := utils.SendSMSRequest{
            Number:      "YourSenderNumberOrAlphaName", // Замените на ваш номер или альфа-имя
            Destination: p.PhoneNumber,
            Text:        message,
        }
        // Отправляем SMS через Exolve API
        sendResp, err := h.ExolveClient.SendSMS(sendReq)
        if err != nil {
            log.Printf("Не удалось отправить SMS на %s: %v", p.PhoneNumber, err)
            continue
        }
        // Логируем успешную отправку
        log.Printf("Напоминание отправлено пациенту %s, MessageID: %s", p.PhoneNumber, sendResp.MessageID)
    }
    // Отправляем ответ о завершении процесса
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Напоминания отправлены"))
}ReminderHandler управляет подключением к базе данных и клиентом Exolve для отправки SMS. Конструктор NewReminderHandler создает экземпляр обработчика. Метод AddPatient принимает POST-запрос, валидирует данные, сохраняет пациента в базу данных и возвращает его информацию. Метод SendReminders обрабатывает GET-запрос, выбирает пациентов с актуальным временем напоминания, отправляет SMS через Exolve API и логирует результаты.
Основной файл
В main.go настроим маршрутизацию и запуск сервера:
package main
import (
    "database/sql"
    "log"
    "net/http"
    "sms-reminder/config"
    "sms-reminder/handlers"
    "sms-reminder/utils"
    "github.com/gorilla/mux"
    _ "github.com/lib/pq" // Драйвер PostgreSQL
)
func main() {
    // Загружаем конфигурацию из .env файла
    cfg := config.LoadConfig()
    // Подключаемся к базе данных
    db, err := sql.Open("postgres", cfg.DBConnString)
    if err != nil {
        log.Fatalf("Не удалось подключиться к базе данных: %v", err)
    }
    defer db.Close()
    // Проверяем подключение к базе данных
    err = db.Ping()
    if err != nil {
        log.Fatalf("Не удалось установить соединение с базой данных: %v", err)
    }
    log.Println("Успешно подключились к базе данных")
    // Создаем клиента для Exolve API
    exolveClient := utils.NewExolveClient(cfg.ExolveAPIKey)
    // Создаем обработчик напоминаний
    reminderHandler := handlers.NewReminderHandler(db, exolveClient)
    // Настраиваем маршруты с использованием Gorilla Mux
    r := mux.NewRouter()
    r.HandleFunc("/add-patient", reminderHandler.AddPatient).Methods("POST")         // Маршрут для добавления пациента
    r.HandleFunc("/send-reminders", reminderHandler.SendReminders).Methods("GET")     // Маршрут для отправки напоминаний
    // Запускаем сервер
    log.Printf("Сервер запускается на порту %s", cfg.Port)
    if err := http.ListenAndServe(":"+cfg.Port, r); err != nil {
        log.Fatalf("Не удалось запустить сервер: %v", err)
    }
}Загружаем настройки из .env с помощью LoadConfig, подключаемся к PostgreSQL и инициализируем Exolve клиент. Создаем ReminderHandler для работы с базой данных и API, настраиваем маршруты для добавления пациентов и отправки напоминаний через gorilla/mux. Запускаем HTTP-сервер и слушаем запросы.
Dockerfile
Для удобства развертывания добавим Dockerfile:
# Используем официальный образ Golang в качестве базового
FROM golang:1.20-alpine
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем файлы зависимостей и устанавливаем их
COPY go.mod .
COPY go.sum .
RUN go mod download
# Копируем весь исходный код в контейнер
COPY . .
# Собираем приложение
RUN go build -o main .
# Указываем порт, который будет использоваться
EXPOSE 8080
# Определяем команду для запуска приложения
CMD ["./main"]Используем легковесный Alpine образ с Golang, настраиваем рабочую директорию, копируем go.mod и go.sum для установки зависимостей, переносим исходный код, компилируем приложение, открываем порт 8080 и задаем команду запуска контейнера.
Создадим базу данных
Перед запуском приложения создадим базу данных и таблицу для хранения информации о пациентах. Ниже приведен пример SQL-скрипта для создания таблицы patients:
CREATE DATABASE sms_reminder;
\c sms_reminder
CREATE TABLE patients (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    phone_number VARCHAR(20) NOT NULL,
    email VARCHAR(255),
    drug_name VARCHAR(255) NOT NULL,
    dosage VARCHAR(255) NOT NULL,
    reminder_time TIME NOT NULL,
    created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() AT TIME ZONE 'UTC'),
    updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() AT TIME ZONE 'UTC')
);
-- Добавляем индекс для быстрого поиска по времени напоминания
CREATE INDEX idx_reminder_time ON patients(reminder_time);Создаем базу данных sms_reminder и таблицу patients, которая хранит информацию о пациентах и времени напоминаний. Добавляем индекс на поле reminder_time для ускорения запросов по времени напоминания.
Тестирование
Соберем и запустим Docker-контейнер:
docker build -t sms-reminder .
docker run -d -p 8080:8080 --env-file .env sms-reminderОтправим POST-запрос для добавления нового пациента:
curl -X POST http://localhost:8080/add-patient \
-H "Content-Type: application/json" \
-d '{
    "name": "Иван Иванов",
    "phone_number": "79991234567",
    "email": "ivan@example.com",
    "drug_name": "Парацетамол",
    "dosage": "500 мг",
    "reminder_time": "08:00:00"
}'Ожидаемый ответ:
{
    "id": 1,
    "name": "Иван Иванов",
    "phone_number": "79991234567",
    "email": "ivan@example.com",
    "drug_name": "Парацетамол",
    "dosage": "500 мг",
    "reminder_time": "08:00:00",
    "created_at": "2024-12-18T07:59:59Z",
    "updated_at": "2024-12-18T07:59:59Z"
}Отправка напоминаний
Отправим GET-запрос для отправки напоминаний:
curl http://localhost:8080/send-remindersОжидаемый ответ:
Напоминания отправлены
В логах контейнера можно увидеть что-то вроде:
Напоминание отправлено пациенту 79991234567, MessageID: 439166538239448536После отправки напоминаний, можно будет проверить отправленные SMS-сообщения в панели управления Exolve.
Заключение
Не останавливайтесь на достигнутом! Вы можете расширить функции системы, добавив, например, отправку напоминаний по электронной почте или интеграцию с мобильными приложениями. Возможности безграничны, и только ваше воображение задает рамки.
Удачи в ваших начинаниях!
Подписывайтесь на наш Хаб, следите за новыми гайдами и получайте приз
Каждый понедельник мы случайным образом выбираем победителей среди новых подписчиков нашего Хабр-канала и дарим крутые призы от МТС Exolve: стильные рюкзаки, лонгсливы и мощные беспроводные зарядки. Победители прошлых розыгрышей и правила.
Комментарии (7)
 - verax_mendax26.12.2024 09:27- У меня сразу возник вопрос. Будет ли оповещение с текстом "Дед, пей таблетки..." и соответствующей картинкой? 
 - kuzzdra26.12.2024 09:27- Сикхи (или ситхи? путаю их) все доводят до абсолюта ;) - Курс лечения (обычно) - недлинный. Число напоминаний - ограничено. Их можно сформировать в виде ICF файла и импортировать в гуглокалендарь. А ссылку на icf напечатать в виде qr кода на назначении врача. 
 - aleks-th26.12.2024 09:27- Осталось сделать систему напоминаний о напоминаниях, и все будет зашибись )))) 
 Интересно, а чем миллион различных календарей хуже ?
 Там так же настраиваются всплывающие напоминания ...
 Как только это дойдет до реализации в итоге будет система отсылки спама под видом благого дела. Будет повод ежедневно цеплять спам к напоминанию. В итоге это превратится в спамкошмар и нормальные люди будут эту хрень отключать по умолчанию....
 А потом доставка СМС вещь вообще не гарантированная, и кто будет отвечать если сообщение не дойдет и пациент пропустит прием и получит ущерб здоровью.
 ----- Делюсь бесплатным аналоговым лайфхаком которым сам пользуюсь. 
 На блистере с таблеткам пишешь дату, на каждой таблетке когда ее нужно принять.
 Блистеры кладешь в одно место, надписями вверх.
 Принимая лекарства ты всегда видишь что принял, а что нет, и что сегодня еще принять нужно.
 И все. - yarkov26.12.2024 09:27- Целиком поддерживаю. А ещё таблетницы, даже с напоминалкой, придумали. Вообще не понял смысла статьи. 
 
 
           
 
ilyaberdysh
о уау, ко мне недавно на консультацию приходил парень, который такое в телеграмм-боте хотел реализовать, закину ему!
michabramov Автор
когда-нибудь все клиники дойдут до такой степени автоматизации :)