Меня зовут Антон Малыгин, я пишу мини-серию статей о своих пет-проектах или просто нерабочих проектах, в которых участвовал, с техническими подробностями. В прошлой статье я рассказал, как мы «озвучивали» интернет.
Сегодня расскажу о проекте из того же 2018 года, в котором мне предложили поучаствовать, также, в качестве backend-разработчика.
Идея
К моему коллеге пришел один знакомый и предложил идею — система учета речных перевозок с приложением для капитанов, в которой можно трекать события: подход, рейд, погрузка, переход, рейд, выгрузка, простой и его причина.
Суть работы сервиса была следующая: диспетчер/оператор через web-версию создает черновик маршрута для судна, с различным набором событий для учета. Капитан или кто-то из команды, с помощью мобильной версии приложения, подтверждает события из маршрута. Например, по плану должна быть погрузка, капитан подтверждает, потом начинается рейд, капитан отмечает, что рейд начался и т.д. Естественно, все это фиксируется на временных отрезках, т.е. диспетчер видит сколько фактически времени занял каждый этап.
Нужно было сделать MVP проекта, которая включала в себя web-версию, android и backend.
Техническая часть
Я решил писать backend на Go, так как к тому моменту имел небольшой опыт реализации сервиса для отправки Push-уведомлений для iOS, используя Go.
К тому уже имел опыт реализации бэка на MVC на языке Scala, и именно поэтому решил использовать MVC здесь.
После небольших поисков и сравнений решено было использовать web framework iris.
По-моему скромному мнению написан понятно и хорошо, также очень хорошо поддерживается, плюс у него есть модуль для MVC, ну и, конечно, большое количество звездочек на гитхаб =)
Вот так происходит инициализация сервиса:
func main() {
app := NewAppDB()
app.Run(
iris.Addr(":8080"),
)
}
func NewAppDB() *iris.Application {
app := iris.New()
app.Logger().SetLevel("debug")
config := PsqlInfo()
fmt.Printf("%s", config)
database, err := db.InitWithOptions(config, true)
if err != nil {
fmt.Printf("%s", err.Error())
panic(err)
}
service := services.NewEntityService(database)
userService := services.NewUserService(database)
taskService := services.NewTaskService(database)
planService := services.NewPlanService(database)
task := mvc.New(app.Party("/api/v1/tasks"))
task.Register(taskService, userService)
task.Handle(new(controllers.TaskController))
plan := mvc.New(app.Party("/api/v1/plans"))
plan.Register(planService, userService)
plan.Handle(new(controllers.PlanController))
entity := mvc.New(app.Party("/api/v1/entities"))
entity.Register(service, userService)
entity.Handle(new(controllers.EntityController))
login := mvc.New(app.Party("/api/v1/login"))
login.Register(userService)
login.Handle(new(controllers.AuthController))
users := mvc.New(app.Party("/api/v1/users"))
users.Register(userService)
users.Handle(new(controllers.UserController))
return app
}
Web-приложение случает 8080 порт, на который все запросы перенаправляет Nginx.
Сервис работает по принципу CRUD, т.е. по сути является прослойкой между клиентом и базой данных, выполняя простые операции.
Все приложение состоит из 3 основных слоев:
- Контроллеры
- Модели данных
- Сервисы
Рассмотрим простой контроллер аутентификации:
type AuthController struct {
Ctx iris.Context
Service services.UserService
}
func (c *AuthController) Post() interface{} {
var request AuthRequest
err := c.Ctx.ReadJSON(&request)
if err != nil {
return iris.StatusBadRequest
}
user, err := c.Service.Auth(request.Login, request.Password)
if err != nil {
return iris.StatusUnauthorized
}
return UserResult{Id:user.Id, Token:user.Token}
}
type AuthRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type UserResult struct {
Id int64 `json:"id"`
Token string `json:"token"`
}
Контроллер получает POST запрос с логином и паролем, парсит json в AuthRequest, далее пытается получить User на основе пары логин/пароль из UserService. В случае успеха, возвращается структура UserResult c токеном и ID пользователя.
Преобразование в json и обратно происходит автоматически, с помощью метки «json» у каждого поля структуры.
Сервисы реализуют бизнес-логику, работу с хранилищем и т.д. На примере EntityService:
type EntityService interface {
Get(companyId int64) (*datamodels.EntitiesResult, error)
GetEntities(entityType EntityType, companyId int64) (*[]datamodels.Entity, error)
GetVessels(companyId int64) (*[]datamodels.VesselEntity, error)
GetHistory(historyId int64, companyId int64) (*datamodels.EntitiesHistoryResult, error)
GetBarges(companyId int64) (*[]datamodels.BargeEntity, error)
}
func NewEntityService(database *db2.Database) EntityService {
return &EntityServiceImpl{Database: database}
}
type EntityServiceImpl struct {
Database *db2.Database
}
func (es *EntityServiceImpl) GetVessels(companyId int64) (*[]datamodels.VesselEntity, error) {
vessels, err := db2.GetVessels(es.Database.DB, companyId)
if err != nil {
return nil, err
}
return mappingVesselsToVesselEntity(vessels), nil
}
Интерфейс EntityService отвечает за работу с разными объектами, например, баржи, суда и т.д.
Реализация GetVessels возвращает все суда для companyId.
Для работы с БД используется расширение стандартного пакета database/sql:
type EntityObject struct {
Id int64
Name string
CompanyId int64 `db:"company_id"`
}
type Vessel struct {
EntityObject
InRent bool `db:"in_rent"`
}
func GetVessels(db *sqlx.DB, companyId int64) (*[]Vessel, error) {
var vessels []Vessel
err := GetObjects(db, Vessels, &vessels, companyId)
return &vessels, err
}
func GetObjects(db *sqlx.DB, tableName EntityTableName, result interface{}, companyId int64) error {
err := db.Select(result, "SELECT * FROM " + string(tableName) + " WHERE company_id = $1", companyId)
return err
}
Вместо наследования в Go используется композиция, например, здесь, есть объект EntityObject, с общим набором полей и специфичный Vessel, добавляющий поле InRent.
Заключение
Проект получился неплох для MVP, отработал нужный период времени, но дальнейшего развития не получил. Как оказалось, заказчик смотрел не только наш MVP, но и от «конкурентов». Другое решение выигрывало у нашего, потому что, по сути, уже не являлось MVP.
Но я выполнил программу минимум для себя — получил интересный и полезный опыт разработки на Go, поэтому потратил время не напрасно.
В следующей статье расскажу про, можно сказать, успешный пет-проект, который работает по сей день, и на котором даже удалось заработать.