README.md добавил, заменил последовательные запросы к nominantim на параллельные с errgroup

This commit is contained in:
2026-03-29 21:33:43 +05:00
parent 934703be29
commit 21f9703f7b
4 changed files with 138 additions and 17 deletions

113
README.md Normal file
View 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
View File

@@ -22,13 +22,13 @@ require (
github.com/rs/xid v1.6.0
github.com/samber/slog-chi v1.19.0
golang.org/x/crypto v0.48.0
golang.org/x/sync v0.19.0
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // 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/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
@@ -47,7 +47,6 @@ require (
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/atomic 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/text v0.34.0 // indirect
)

2
go.sum
View File

@@ -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/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/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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=

View File

@@ -12,6 +12,7 @@ import (
storage "github.com/anxi0uz/logiflow/pkg"
"github.com/google/uuid"
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) {}
@@ -31,16 +32,26 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
return
}
originLat, originLon, err := s.geocode(ctx, *req.OriginAddress)
if err != nil {
slog.ErrorContext(ctx, "Failed to geocode origin", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты адреса отправки", RespError)
return
}
destLat, destLon, err := s.geocode(ctx, req.DestinationAddress)
if err != nil {
slog.ErrorContext(ctx, "Failed to geocode destination", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты адреса назначения", RespError)
var (
originLat, originLon float64
destLat, destLon float64
)
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
originLat, originLon, err = s.geocode(gctx, *req.OriginAddress)
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
}
@@ -75,14 +86,14 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
route := osrmResult.Routes[0]
distanceKm := route.Distance / 1000
var weightKm, volumeM3 float64
var weightKg, volumeM3 float64
if req.WeightKg != nil {
weightKm = float64(*req.WeightKg)
weightKg = float64(*req.WeightKg)
}
if req.VolumeM3 != nil {
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
orderID := uuid.New()