README.md добавил, заменил последовательные запросы к nominantim на параллельные с errgroup
This commit is contained in:
113
README.md
Normal file
113
README.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Logiflow
|
||||||
|
|
||||||
|
Бэкенд для логистической платформы. Управление заказами на перевозку, водителями, транспортом и складами. При создании заказа строится реальный маршрут через OSRM, считается стоимость перевозки, трекинг водителя в реальном времени через WebSocket.
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
|
||||||
|
- **Go 1.25** — Chi v5, oapi-codegen, pgx/v5, go-redis
|
||||||
|
- **PostgreSQL** — миграции через Goose
|
||||||
|
- **Redis** — хранение JWT access/refresh токенов
|
||||||
|
- **Nominatim** — геокодинг адресов (OpenStreetMap, без ключа)
|
||||||
|
- **OSRM** — построение маршрутов и расчёт дистанции
|
||||||
|
- **Prometheus + Grafana** — мониторинг
|
||||||
|
- **Podman** — контейнеризация
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# поднять все сервисы
|
||||||
|
make up
|
||||||
|
|
||||||
|
# пересобрать и поднять
|
||||||
|
make build-up
|
||||||
|
|
||||||
|
# остановить
|
||||||
|
make down
|
||||||
|
|
||||||
|
# для деплоя (pull + build + up)
|
||||||
|
make deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Перед запуском создать сеть:
|
||||||
|
```bash
|
||||||
|
podman network create LogiflowNetwork
|
||||||
|
```
|
||||||
|
|
||||||
|
Скопировать `.env` и заполнить секреты:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Обязательные переменные:
|
||||||
|
```
|
||||||
|
LOGIFLOW_DATABASE_USER=
|
||||||
|
LOGIFLOW_DATABASE_PASSWORD=
|
||||||
|
LOGIFLOW_DATABASE_NAME=
|
||||||
|
LOGIFLOW_REDIS_PASSWORD=
|
||||||
|
LOGIFLOW_JWT_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Конфиг читается из `configs/config.toml`, переменные окружения с префиксом `LOGIFLOW_` перекрывают файл.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[pricing]
|
||||||
|
baseFee = 5000.0 # базовая ставка, руб
|
||||||
|
perKm = 100.0 # руб/км
|
||||||
|
perKg = 15.0 # руб/кг
|
||||||
|
perM3 = 600.0 # руб/м³
|
||||||
|
```
|
||||||
|
|
||||||
|
Цена заказа считается по формуле:
|
||||||
|
```
|
||||||
|
total = baseFee + distance_km * perKm + weight_kg * perKg + volume_m3 * perM3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Роли
|
||||||
|
|
||||||
|
| Роль | Возможности |
|
||||||
|
|---|---|
|
||||||
|
| `client` | Создаёт заказы, следит за своими заявками |
|
||||||
|
| `driver` | Меняет свой статус, видит назначенные маршруты |
|
||||||
|
| `manager` | Назначает водителей на заказы |
|
||||||
|
| `admin` | Создаёт профили водителей и менеджеров, видит всё |
|
||||||
|
|
||||||
|
Клиенты регистрируются сами через `POST /auth/register`. Водителей и менеджеров создаёт только администратор.
|
||||||
|
|
||||||
|
## Авторизация
|
||||||
|
|
||||||
|
JWT (HS256) + refresh токены. Access токен живёт 24 часа, refresh — 7 дней в HTTP-only cookie. Оба хранятся в Redis — при логауте удаляются.
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Флоу заказа
|
||||||
|
|
||||||
|
```
|
||||||
|
Клиент создаёт заказ (адреса → Nominatim → координаты → OSRM → маршрут)
|
||||||
|
↓
|
||||||
|
Менеджер назначает водителя (pending → assigned)
|
||||||
|
↓
|
||||||
|
Водитель начинает поездку (assigned → in_transit)
|
||||||
|
↓
|
||||||
|
Трекинг по WebSocket — current_index двигается по массиву координат
|
||||||
|
↓
|
||||||
|
Водитель завершает (in_transit → delivered)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура БД
|
||||||
|
|
||||||
|
```
|
||||||
|
users
|
||||||
|
├── drivers (user_id) → vehicles
|
||||||
|
└── managers (user_id) → warehouses
|
||||||
|
|
||||||
|
orders (created_by_id → users, driver_id → drivers, manager_id → managers)
|
||||||
|
└── routes (order_id) — JSONB координаты маршрута, current_index
|
||||||
|
|
||||||
|
notifications (user_id → users)
|
||||||
|
```
|
||||||
|
|
||||||
|
Сервер поднимается на `localhost:3001`, Grafana на `localhost:3000`, Prometheus на `localhost:9090`.
|
||||||
3
go.mod
3
go.mod
@@ -22,13 +22,13 @@ require (
|
|||||||
github.com/rs/xid v1.6.0
|
github.com/rs/xid v1.6.0
|
||||||
github.com/samber/slog-chi v1.19.0
|
github.com/samber/slog-chi v1.19.0
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
|
golang.org/x/sync v0.19.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/doppiogancio/go-nominatim v2.0.1+incompatible // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||||
@@ -47,7 +47,6 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -13,8 +13,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/doppiogancio/go-nominatim v2.0.1+incompatible h1:S0PYXIVKwV6vF+JNBSO6T7BdvH4Ktuaf5HVL51i0lyQ=
|
|
||||||
github.com/doppiogancio/go-nominatim v2.0.1+incompatible/go.mod h1:lePiHgediF5zQ6qRyynsDoilUOBVFSpOpsWiDd21Dic=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
storage "github.com/anxi0uz/logiflow/pkg"
|
storage "github.com/anxi0uz/logiflow/pkg"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.ListOrdersParams) {}
|
func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.ListOrdersParams) {}
|
||||||
@@ -31,16 +32,26 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originLat, originLon, err := s.geocode(ctx, *req.OriginAddress)
|
var (
|
||||||
if err != nil {
|
originLat, originLon float64
|
||||||
slog.ErrorContext(ctx, "Failed to geocode origin", slog.String("error", err.Error()))
|
destLat, destLon float64
|
||||||
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты адреса отправки", RespError)
|
)
|
||||||
return
|
g, gctx := errgroup.WithContext(ctx)
|
||||||
}
|
|
||||||
destLat, destLon, err := s.geocode(ctx, req.DestinationAddress)
|
g.Go(func() error {
|
||||||
if err != nil {
|
var err error
|
||||||
slog.ErrorContext(ctx, "Failed to geocode destination", slog.String("error", err.Error()))
|
originLat, originLon, err = s.geocode(gctx, *req.OriginAddress)
|
||||||
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты адреса назначения", RespError)
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
var err error
|
||||||
|
destLat, destLon, err = s.geocode(gctx, req.DestinationAddress)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты", RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +86,14 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|||||||
route := osrmResult.Routes[0]
|
route := osrmResult.Routes[0]
|
||||||
distanceKm := route.Distance / 1000
|
distanceKm := route.Distance / 1000
|
||||||
|
|
||||||
var weightKm, volumeM3 float64
|
var weightKg, volumeM3 float64
|
||||||
if req.WeightKg != nil {
|
if req.WeightKg != nil {
|
||||||
weightKm = float64(*req.WeightKg)
|
weightKg = float64(*req.WeightKg)
|
||||||
}
|
}
|
||||||
if req.VolumeM3 != nil {
|
if req.VolumeM3 != nil {
|
||||||
volumeM3 = float64(*req.VolumeM3)
|
volumeM3 = float64(*req.VolumeM3)
|
||||||
}
|
}
|
||||||
price := s.Config.Pricing.BaseFee + distanceKm*s.Config.Pricing.PerKg + weightKm*s.Config.Pricing.PerKg + volumeM3*s.Config.Pricing.PerM3
|
price := s.Config.Pricing.BaseFee + distanceKm*s.Config.Pricing.PerKm + weightKg*s.Config.Pricing.PerKg + volumeM3*s.Config.Pricing.PerM3
|
||||||
|
|
||||||
now := time.Now
|
now := time.Now
|
||||||
orderID := uuid.New()
|
orderID := uuid.New()
|
||||||
|
|||||||
Reference in New Issue
Block a user