Привет, Хабр! Недавно мне пришла задача: провести голосование среди пользователей, но без сложных и дорогостоящих решений. Когда я пришёл к выбору системы SMS-голосования, осознал, что многие решения на рынке либо слишком сложны для интеграции, либо слишком дороги для решения простых задач.
Я хотел создать что-то, что могло бы работать везде, где есть мобильная сеть. Вооружившись Golang, подключив Exolve SMS API и настроив Supabase, я приступил к работе.
Для начала нужно установить сам Go и подключить следующие библиотеки:
-
Supabase: прекрасный инструмент для работы с базами данных, который отлично интегрируется с Golang через GORM. В нашем случае он будет хранить данные о голосах.
-
Exolve SMS API: основной инструмент для работы с SMS. Ознакомиться с API можно здесь.
-
Gin: легковесный и быстрый фреймворк для создания веб-приложений.
-
GORM: одна из лучших ORM для Golang, позволяющая легко работать с базами данных.
Инициализация проекта
Создадим новый проект и инициализируем его с помощью команды go mod init:
$ mkdir sms-voting $ cd sms-voting $ go mod init sms-voting
Для хранения данных о голосах будем использовать Supabase.
Установим необходимые библиотеки:
$ go get -u github.com/gin-gonic/gin $ go get -u gorm.io/gorm $ go get -u gorm.io/driver/postgres
Регистрируемся на самом Supabase, создадим новый проект и добавим таблицу для голосов:
CREATE TABLE votes ( id SERIAL PRIMARY KEY, candidate_name VARCHAR(255) NOT NULL, vote_count INT DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
Это позволит хранить информацию о каждом голосе и отслеживать количество голосов за каждого кандидата.
Структура проекта
Структура будет выглядеть следующим образом:
sms-voting/ ├── config/ │ └── config.go ├── db/ │ └── db.go ├── sms/ │ └── sms.go ├── handlers/ │ └── sms_handler.go ├── models/ │ └── vote.go ├── main.go └── go.mod
Конфигурация проекта
Создадим файл config/config.go для хранения конфигурационных параметров:
// config/config.go package config import ( "log" "os" ) type Config struct { DBHost string DBUser string DBPassword string DBName string ExolveAPIKey string SenderNumber string ServerPort string } func LoadConfig() *Config { config := &Config{ DBHost: getEnv("DB_HOST", "localhost"), DBUser: getEnv("DB_USER", "postgres"), DBPassword: getEnv("DB_PASSWORD", "password"), DBName: getEnv("DB_NAME", "votes_db"), ExolveAPIKey: getEnv("EXOLVE_API_KEY", ""), SenderNumber: getEnv("SENDER_NUMBER", ""), ServerPort: getEnv("SERVER_PORT", "8080"), } if config.ExolveAPIKey == "" || config.SenderNumber == "" { log.Fatal("EXOLVE_API_KEY and SENDER_NUMBER must be set") } return config } func getEnv(key, fallback string) string { if value, exists := os.LookupEnv(key); exists { return value } return fallback }
Не забываем установить переменные окружения перед запуском приложения:
export DB_HOST=your_db_host export DB_USER=your_db_user export DB_PASSWORD=your_db_password export DB_NAME=your_db_name export EXOLVE_API_KEY=your_exolve_api_key export SENDER_NUMBER=your_sender_number export SERVER_PORT=8080
Модель данных
Создадим файл models/vote.go для описания модели голосования:
// models/vote.go package models import ( "time" "gorm.io/gorm" ) type Vote struct { ID uint `gorm:"primaryKey" json:"id"` CandidateName string `gorm:"not null" json:"candidate_name"` VoteCount int `gorm:"default:0" json:"vote_count"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` }
Работа с БД
Создадим файл db/db.go для настройки подключения к базе данных:
// db/db.go package db import ( "fmt" "log" "gorm.io/driver/postgres" "gorm.io/gorm" "sms-voting/config" "sms-voting/models" ) type Database struct { Conn *gorm.DB } func NewDatabase(cfg *config.Config) *Database { dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=5432 sslmode=require", cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal("failed to connect database:", err) } // Миграция схемы if err := db.AutoMigrate(&models.Vote{}); err != nil { log.Fatal("failed to migrate database:", err) } return &Database{Conn: db} }
Интерфейсы
Для того, чтобы все было гибко и удобно, при том же тестировании, введем интерфейсы для работы с базой данных и отправки SMS.
Интерфейс для базы данных
Создадим файл models/vote_repository.go:
// models/vote_repository.go package models import ( "errors" "gorm.io/gorm" ) type VoteRepository interface { GetOrCreateVote(candidateName string) (*Vote, error) IncrementVote(vote *Vote) error GetAllVotes() ([]Vote, error) } type voteRepository struct { db *gorm.DB } func NewVoteRepository(db *gorm.DB) VoteRepository { return &voteRepository{db} } func (r *voteRepository) GetOrCreateVote(candidateName string) (*Vote, error) { var vote Vote if err := r.db.Where("candidate_name = ?", candidateName).First(&vote).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { vote = Vote{CandidateName: candidateName} if err := r.db.Create(&vote).Error; err != nil { return nil, err } } else { return nil, err } } return &vote, nil } func (r *voteRepository) IncrementVote(vote *Vote) error { vote.VoteCount += 1 return r.db.Save(vote).Error } func (r *voteRepository) GetAllVotes() ([]Vote, error) { var votes []Vote if err := r.db.Find(&votes).Error; err != nil { return nil, err } return votes, nil }
Интерфейс для отправки SMS
// sms/sms_service.go package sms import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" ) type SMSService interface { SendSMS(to string, message string) error } type exolveSMSService struct { apiKey string senderNumber string apiURL string } func NewExolveSMSService(apiKey, senderNumber string) SMSService { return &exolveSMSService{ apiKey: apiKey, senderNumber: senderNumber, apiURL: "https://api.exolve.ru/sms/send", } } type SMSSendRequest struct { To string `json:"to"` From string `json:"from"` Message string `json:"message"` } func (s *exolveSMSService) SendSMS(to string, message string) error { smsSendReq := SMSSendRequest{ To: to, From: s.senderNumber, Message: message, } body, err := json.Marshal(smsSendReq) if err != nil { return fmt.Errorf("error marshalling SMS request: %w", err) } req, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(body)) if err != nil { return fmt.Errorf("error creating SMS request: %w", err) } req.Header.Set("Authorization", "Bearer "+s.apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return fmt.Errorf("error sending SMS: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := ioutil.ReadAll(resp.Body) return fmt.Errorf("failed to send SMS, status code: %d, response: %s", resp.StatusCode, string(respBody)) } return nil }
Обработчики HTTP-запросов
Создадим файл handlers/sms_handler.go для обработки входящих SMS:
// handlers/sms_handler.go package handlers import ( "fmt" "log" "net/http" "github.com/gin-gonic/gin" "sms-voting/models" "sms-voting/sms" ) type SMSRequest struct { From string `json:"from"` Body string `json:"body"` } type SMSHandler struct { VoteRepo models.VoteRepository SMSService sms.SMSService } func NewSMSHandler(voteRepo models.VoteRepository, smsService sms.SMSService) *SMSHandler { return &SMSHandler{ VoteRepo: voteRepo, SMSService: smsService, } } func (h *SMSHandler) HandleSMS(c *gin.Context) { var smsReq SMSRequest if err := c.ShouldBindJSON(&smsReq); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } candidateName := smsReq.Body vote, err := h.VoteRepo.GetOrCreateVote(candidateName) if err != nil { log.Printf("Error fetching/creating vote for candidate %s: %v", candidateName, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) return } if err := h.VoteRepo.IncrementVote(vote); err != nil { log.Printf("Error incrementing vote for candidate %s: %v", candidateName, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) return } responseMessage := fmt.Sprintf("Ваш голос за %s принят. Спасибо за участие!", vote.CandidateName) if err := h.SMSService.SendSMS(smsReq.From, responseMessage); err != nil { log.Printf("Error sending SMS to %s: %v", smsReq.From, err) // Можно решить, возвращать ли ошибку пользователю или нет } c.JSON(http.StatusOK, gin.H{"message": responseMessage}) }
Основной файл приложения
Создадим файл main.go, который будет запускать сервер:
// main.go package main import ( "log" "github.com/gin-gonic/gin" "sms-voting/config" "sms-voting/db" "sms-voting/handlers" "sms-voting/models" "sms-voting/sms" ) func main() { // Загрузка конфигурации cfg := config.LoadConfig() // Инициализация базы данных database := db.NewDatabase(cfg) voteRepo := models.NewVoteRepository(database.Conn) // Инициализация SMS-сервиса smsService := sms.NewExolveSMSService(cfg.ExolveAPIKey, cfg.SenderNumber) // Инициализация обработчиков smsHandler := handlers.NewSMSHandler(voteRepo, smsService) // Настройка роутера Gin router := gin.Default() router.POST("/sms", smsHandler.HandleSMS) // Запуск сервера log.Printf("Сервер запущен на порту %s", cfg.ServerPort) if err := router.Run(":" + cfg.ServerPort); err != nil { log.Fatalf("Не удалось запустить сервер: %v", err) } }
Теперь можно запустить сервер:
$ go run main.go
Пример тела запроса для тестирования:
{ "from": "+79678880033", "body": "Кандидат" }
После обработки запроса система увеличит счетчик голосов за указанного кандидата и отправит подтверждающее SMS пользователю.
Что дальше
В эту систему можно добавить дополнительные функции: отчеты, управление кампаниями, интеграцию с другими сервисами или то же масштабирование. Exolve SMS API также имеет множество возможностей для улучшения сценариев.
Делитесь своими идеями и улучшениями в комментариях. До новых встреч!
ссылка на оригинал статьи https://habr.com/ru/articles/845882/
Добавить комментарий