Утро, аромат свежесваренного кофе, и телефон тихонько напоминает вам о приеме важного лекарства. «Привет! Не забудь принять лекарство!» Такую систему можно реализовать самостоятельно с помощью 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: стильные рюкзаки, лонгсливы и мощные беспроводные зарядки. Победители прошлых розыгрышей и правила.

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


  1. ilyaberdysh
    26.12.2024 09:27

    о уау, ко мне недавно на консультацию приходил парень, который такое в телеграмм-боте хотел реализовать, закину ему!


    1. michabramov Автор
      26.12.2024 09:27

      когда-нибудь все клиники дойдут до такой степени автоматизации :)


  1. verax_mendax
    26.12.2024 09:27

    У меня сразу возник вопрос. Будет ли оповещение с текстом "Дед, пей таблетки..." и соответствующей картинкой?


  1. kuzzdra
    26.12.2024 09:27

    Сикхи (или ситхи? путаю их) все доводят до абсолюта ;)

    Курс лечения (обычно) - недлинный. Число напоминаний - ограничено. Их можно сформировать в виде ICF файла и импортировать в гуглокалендарь. А ссылку на icf напечатать в виде qr кода на назначении врача.


  1. aleks-th
    26.12.2024 09:27

    Осталось сделать систему напоминаний о напоминаниях, и все будет зашибись ))))

    Интересно, а чем миллион различных календарей хуже ?
    Там так же настраиваются всплывающие напоминания ...

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

    А потом доставка СМС вещь вообще не гарантированная, и кто будет отвечать если сообщение не дойдет и пациент пропустит прием и получит ущерб здоровью.
    ----

    Делюсь бесплатным аналоговым лайфхаком которым сам пользуюсь.
    На блистере с таблеткам пишешь дату, на каждой таблетке когда ее нужно принять.
    Блистеры кладешь в одно место, надписями вверх.
    Принимая лекарства ты всегда видишь что принял, а что нет, и что сегодня еще принять нужно.
    И все.


    1. yarkov
      26.12.2024 09:27

      Целиком поддерживаю. А ещё таблетницы, даже с напоминалкой, придумали. Вообще не понял смысла статьи.