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/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
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/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=
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user