Логика из CreateOrder ручки была вынесена в OrderService.CreateOrder, так же Geocode вынесен в pkg, а ещё слегка переработана логика поиска координат складов
This commit is contained in:
@@ -474,6 +474,10 @@ components:
|
|||||||
volumeM3:
|
volumeM3:
|
||||||
type: number
|
type: number
|
||||||
nullable: true
|
nullable: true
|
||||||
|
destinationWarehouseId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
|
||||||
OrderStatusUpdate:
|
OrderStatusUpdate:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -252,12 +252,13 @@ type ManagerCreate struct {
|
|||||||
|
|
||||||
// OrderCreate defines model for OrderCreate.
|
// OrderCreate defines model for OrderCreate.
|
||||||
type OrderCreate struct {
|
type OrderCreate struct {
|
||||||
CargoDescription *string `json:"cargoDescription,omitempty"`
|
CargoDescription *string `json:"cargoDescription,omitempty"`
|
||||||
DestinationAddress string `json:"destinationAddress"`
|
DestinationAddress string `json:"destinationAddress"`
|
||||||
OriginAddress *string `json:"originAddress,omitempty"`
|
DestinationWarehouseId *openapi_types.UUID `json:"destinationWarehouseId,omitempty"`
|
||||||
OriginWarehouseId *openapi_types.UUID `json:"originWarehouseId,omitempty"`
|
OriginAddress *string `json:"originAddress,omitempty"`
|
||||||
VolumeM3 *float32 `json:"volumeM3,omitempty"`
|
OriginWarehouseId *openapi_types.UUID `json:"originWarehouseId,omitempty"`
|
||||||
WeightKg *float32 `json:"weightKg,omitempty"`
|
VolumeM3 *float32 `json:"volumeM3,omitempty"`
|
||||||
|
WeightKg *float32 `json:"weightKg,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrderStatusUpdate defines model for OrderStatusUpdate.
|
// OrderStatusUpdate defines model for OrderStatusUpdate.
|
||||||
|
|||||||
@@ -2,17 +2,11 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/anxi0uz/logiflow/internal/api"
|
"github.com/anxi0uz/logiflow/internal/api"
|
||||||
"github.com/anxi0uz/logiflow/internal/models"
|
|
||||||
storage "github.com/anxi0uz/logiflow/pkg"
|
|
||||||
"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) {}
|
||||||
@@ -32,130 +26,16 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
result, err := s.OrderSerice.CreateOrder(ctx, req, claims.ID)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
osrmURL := fmt.Sprintf(
|
|
||||||
"http://router.project-osrm.org/route/v1/driving/%f,%f;%f,%f?overview=full&geometries=geojson",
|
|
||||||
originLon, originLat, destLon, destLat,
|
|
||||||
)
|
|
||||||
osrmReq, _ := http.NewRequestWithContext(ctx, http.MethodGet, osrmURL, nil)
|
|
||||||
osrmReq.Header.Set("User-Agent", "logiflow/1.0")
|
|
||||||
|
|
||||||
osrmResp, err := http.DefaultClient.Do(osrmReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "OSRM request failed", slog.String("error", err.Error()))
|
slog.ErrorContext(ctx, "create order failed", slog.String("error", err.Error()))
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer osrmResp.Body.Close()
|
|
||||||
|
|
||||||
var osrmResult struct {
|
|
||||||
Routes []struct {
|
|
||||||
Geometry struct {
|
|
||||||
Coordinates [][]float64 `json:"coordinates"`
|
|
||||||
} `json:"geometry"`
|
|
||||||
Distance float64 `json:"distance"`
|
|
||||||
Duration float64 `json:"duration"`
|
|
||||||
} `json:"routes"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(osrmResp.Body).Decode(&osrmResult); err != nil || len(osrmResult.Routes) == 0 {
|
|
||||||
s.JSON(w, r, http.StatusBadRequest, "Не удалось построить маршрут", RespError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
route := osrmResult.Routes[0]
|
|
||||||
distanceKm := route.Distance / 1000
|
|
||||||
|
|
||||||
var weightKg, volumeM3 float64
|
|
||||||
if req.WeightKg != nil {
|
|
||||||
weightKg = float64(*req.WeightKg)
|
|
||||||
}
|
|
||||||
if req.VolumeM3 != nil {
|
|
||||||
volumeM3 = float64(*req.VolumeM3)
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
order := models.Order{
|
|
||||||
ID: orderID,
|
|
||||||
CreatedByID: &claims.ID,
|
|
||||||
DestinationAddress: req.DestinationAddress,
|
|
||||||
Status: "pending",
|
|
||||||
TotalPrice: price,
|
|
||||||
CreatedAt: now(),
|
|
||||||
}
|
|
||||||
if *req.OriginAddress != "" {
|
|
||||||
order.OriginAddress = *req.OriginAddress
|
|
||||||
}
|
|
||||||
if req.CargoDescription != nil {
|
|
||||||
order.CargoDescription = *req.CargoDescription
|
|
||||||
}
|
|
||||||
if req.WeightKg != nil {
|
|
||||||
order.WeightKg = float64(*req.WeightKg)
|
|
||||||
}
|
|
||||||
if req.VolumeM3 != nil {
|
|
||||||
order.VolumeM3 = volumeM3
|
|
||||||
}
|
|
||||||
coordsJSON, err := json.Marshal(route.Geometry.Coordinates)
|
|
||||||
if err != nil {
|
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
routeModel := models.Route{
|
|
||||||
ID: uuid.New(),
|
|
||||||
OrderID: orderID,
|
|
||||||
Coordinates: coordsJSON,
|
|
||||||
DurationSec: int(route.Duration),
|
|
||||||
Status: "pending",
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := s.DB.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
if err := storage.Create(ctx, "orders", order, tx); err != nil {
|
|
||||||
slog.ErrorContext(ctx, "Failed to create order", slog.String("error", err.Error()))
|
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := storage.Create(ctx, "routes", routeModel, tx); err != nil {
|
|
||||||
slog.ErrorContext(ctx, "Failed to create route", slog.String("error", err.Error()))
|
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.JSON(w, r, http.StatusCreated, map[string]any{
|
s.JSON(w, r, http.StatusCreated, map[string]any{
|
||||||
"order": order,
|
"order": result.Order,
|
||||||
"route": routeModel,
|
"route": result.Route,
|
||||||
}, "order")
|
}, "order")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
neturl "net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/anxi0uz/logiflow/internal/api"
|
"github.com/anxi0uz/logiflow/internal/api"
|
||||||
"github.com/anxi0uz/logiflow/internal/config"
|
"github.com/anxi0uz/logiflow/internal/config"
|
||||||
|
"github.com/anxi0uz/logiflow/internal/services"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -49,20 +48,22 @@ type responseOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
DB *pgxpool.Pool
|
DB *pgxpool.Pool
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
JwtKey []byte
|
JwtKey []byte
|
||||||
|
OrderSerice services.OrderService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(db *pgxpool.Pool, redis *redis.Client, cfg *config.Config) *Server {
|
func NewServer(db *pgxpool.Pool, redis *redis.Client, cfg *config.Config) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
DB: db,
|
DB: db,
|
||||||
Redis: redis,
|
Redis: redis,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
JwtKey: []byte(cfg.JwtOpt.Key),
|
JwtKey: []byte(cfg.JwtOpt.Key),
|
||||||
|
OrderSerice: *services.NewOrderService(db, *cfg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,36 +237,3 @@ func (s *Server) validateAccessToken(ctx context.Context, tokenStr string) (*Cla
|
|||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
func (s *Server) geocode(ctx context.Context, address string) (lat, lon float64, err error) {
|
|
||||||
url := fmt.Sprintf(
|
|
||||||
"https://nominatim.openstreetmap.org/search?q=%s&format=json&limit=1",
|
|
||||||
neturl.QueryEscape(address),
|
|
||||||
)
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", "logiflow/1.0")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var results []struct {
|
|
||||||
Lat string `json:"lat"`
|
|
||||||
Lon string `json:"lon"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
if len(results) == 0 {
|
|
||||||
return 0, 0, fmt.Errorf("address not found: %s", address)
|
|
||||||
}
|
|
||||||
|
|
||||||
lat, _ = strconv.ParseFloat(results[0].Lat, 64)
|
|
||||||
lon, _ = strconv.ParseFloat(results[0].Lon, 64)
|
|
||||||
return lat, lon, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/anxi0uz/logiflow/internal/api"
|
"github.com/anxi0uz/logiflow/internal/api"
|
||||||
"github.com/anxi0uz/logiflow/internal/models"
|
"github.com/anxi0uz/logiflow/internal/models"
|
||||||
storage "github.com/anxi0uz/logiflow/pkg"
|
storage "github.com/anxi0uz/logiflow/pkg"
|
||||||
|
"github.com/anxi0uz/logiflow/pkg/geocode"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gosimple/slug"
|
"github.com/gosimple/slug"
|
||||||
"github.com/huandu/go-sqlbuilder"
|
"github.com/huandu/go-sqlbuilder"
|
||||||
@@ -38,7 +39,7 @@ func (s *Server) CreateWarehouse(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullAddress := fmt.Sprintf("%s, %s", req.City, req.Address)
|
fullAddress := fmt.Sprintf("%s, %s", req.City, req.Address)
|
||||||
lat, lon, err := s.geocode(ctx, fullAddress)
|
lat, lon, err := geocode.Geocode(ctx, fullAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Failed to geocode address", slog.String("address", req.Address), slog.String("error", err.Error()))
|
slog.ErrorContext(ctx, "Failed to geocode address", slog.String("address", req.Address), slog.String("error", err.Error()))
|
||||||
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты по адресу", RespError)
|
s.JSON(w, r, http.StatusBadRequest, "Не удалось определить координаты по адресу", RespError)
|
||||||
|
|||||||
164
internal/services/order.go
Normal file
164
internal/services/order.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anxi0uz/logiflow/internal/api"
|
||||||
|
"github.com/anxi0uz/logiflow/internal/config"
|
||||||
|
"github.com/anxi0uz/logiflow/internal/models"
|
||||||
|
storage "github.com/anxi0uz/logiflow/pkg"
|
||||||
|
"github.com/anxi0uz/logiflow/pkg/geocode"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/huandu/go-sqlbuilder"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderService struct {
|
||||||
|
db *pgxpool.Pool
|
||||||
|
config config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateOrderResult struct {
|
||||||
|
Order models.Order
|
||||||
|
Route models.Route
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrderService(db *pgxpool.Pool, cfg config.Config) *OrderService {
|
||||||
|
return &OrderService{db: db, config: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, userID uuid.UUID) (*CreateOrderResult, error) {
|
||||||
|
var (
|
||||||
|
originLat, originLon float64
|
||||||
|
destLat, destLon float64
|
||||||
|
)
|
||||||
|
g, gctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
if req.OriginWarehouseId != nil {
|
||||||
|
wh, err := storage.GetOne[models.Warehouse](gctx, s.db, "warehouses", func(sb *sqlbuilder.SelectBuilder) {
|
||||||
|
sb.Where(sb.Equal("id", req.OriginWarehouseId))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
originLat = wh.Latitude
|
||||||
|
originLon = wh.Longitude
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
originLat, originLon, err = geocode.Geocode(gctx, *req.OriginAddress)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
g.Go(func() error {
|
||||||
|
if req.DestinationWarehouseId != nil {
|
||||||
|
wh, err := storage.GetOne[models.Warehouse](gctx, s.db, "warehouses", func(sb *sqlbuilder.SelectBuilder) {
|
||||||
|
sb.Where(sb.Equal("id", req.DestinationWarehouseId))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destLat = wh.Latitude
|
||||||
|
destLon = wh.Longitude
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
destLat, originLon, err = geocode.Geocode(ctx, req.DestinationAddress)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "error while getting coordinates", slog.String("error", err.Error()))
|
||||||
|
return nil, fmt.Errorf("error while getting coordinates: %w", err)
|
||||||
|
}
|
||||||
|
osrmURL := fmt.Sprintf(
|
||||||
|
"http://router.project-osrm.org/route/v1/driving/%f,%f;%f,%f?overview=full&geometries=geojson",
|
||||||
|
originLon, originLat, destLon, destLat,
|
||||||
|
)
|
||||||
|
osrmReq, _ := http.NewRequestWithContext(ctx, http.MethodGet, osrmURL, nil)
|
||||||
|
osrmReq.Header.Set("User-Agent", "logiflow/1.0")
|
||||||
|
|
||||||
|
osrmResp, err := http.DefaultClient.Do(osrmReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("osrm request: %w", err)
|
||||||
|
}
|
||||||
|
defer osrmResp.Body.Close()
|
||||||
|
|
||||||
|
var osrmResult geocode.OsrmResult
|
||||||
|
if err := json.NewDecoder(osrmResp.Body).Decode(&osrmResult); err != nil || len(osrmResult.Routes) == 0 {
|
||||||
|
return nil, fmt.Errorf("osrm: no route found")
|
||||||
|
}
|
||||||
|
route := osrmResult.Routes[0]
|
||||||
|
distanceKm := route.Distance / 1000
|
||||||
|
|
||||||
|
var weightKg, volumeM3 float64
|
||||||
|
if req.WeightKg != nil {
|
||||||
|
weightKg = float64(*req.WeightKg)
|
||||||
|
}
|
||||||
|
if req.VolumeM3 != nil {
|
||||||
|
volumeM3 = float64(*req.VolumeM3)
|
||||||
|
}
|
||||||
|
price := s.config.Pricing.BaseFee +
|
||||||
|
distanceKm*s.config.Pricing.PerKm +
|
||||||
|
weightKg*s.config.Pricing.PerKg +
|
||||||
|
volumeM3*s.config.Pricing.PerM3
|
||||||
|
|
||||||
|
orderID := uuid.New()
|
||||||
|
order := models.Order{
|
||||||
|
ID: orderID,
|
||||||
|
CreatedByID: &userID,
|
||||||
|
DestinationAddress: req.DestinationAddress,
|
||||||
|
Status: "pending",
|
||||||
|
TotalPrice: price,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if req.OriginAddress != nil && *req.OriginAddress != "" {
|
||||||
|
order.OriginAddress = *req.OriginAddress
|
||||||
|
}
|
||||||
|
if req.CargoDescription != nil {
|
||||||
|
order.CargoDescription = *req.CargoDescription
|
||||||
|
}
|
||||||
|
if req.WeightKg != nil {
|
||||||
|
order.WeightKg = weightKg
|
||||||
|
}
|
||||||
|
if req.VolumeM3 != nil {
|
||||||
|
order.VolumeM3 = volumeM3
|
||||||
|
}
|
||||||
|
|
||||||
|
coordsJSON, err := json.Marshal(route.Geometry.Coordinates)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal coordinates: %w", err)
|
||||||
|
}
|
||||||
|
routeModel := models.Route{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrderID: orderID,
|
||||||
|
Coordinates: coordsJSON,
|
||||||
|
DurationSec: int(route.Duration),
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
if err := storage.Create(ctx, "orders", order, tx); err != nil {
|
||||||
|
return nil, fmt.Errorf("create order: %w", err)
|
||||||
|
}
|
||||||
|
if err := storage.Create(ctx, "routes", routeModel, tx); err != nil {
|
||||||
|
return nil, fmt.Errorf("create route: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CreateOrderResult{Order: order, Route: routeModel}, nil
|
||||||
|
}
|
||||||
54
pkg/geocode/geocode.go
Normal file
54
pkg/geocode/geocode.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package geocode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
neturl "net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OsrmResult struct {
|
||||||
|
Routes []struct {
|
||||||
|
Geometry struct {
|
||||||
|
Coordinates [][]float64 `json:"coordinates"`
|
||||||
|
} `json:"geometry"`
|
||||||
|
Distance float64 `json:"distance"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
} `json:"routes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Geocode(ctx context.Context, address string) (lat, lon float64, err error) {
|
||||||
|
url := fmt.Sprintf(
|
||||||
|
"https://nominatim.openstreetmap.org/search?q=%s&format=json&limit=1",
|
||||||
|
neturl.QueryEscape(address),
|
||||||
|
)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "logiflow/1.0")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var results []struct {
|
||||||
|
Lat string `json:"lat"`
|
||||||
|
Lon string `json:"lon"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if len(results) == 0 {
|
||||||
|
return 0, 0, fmt.Errorf("address not found: %s", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
lat, _ = strconv.ParseFloat(results[0].Lat, 64)
|
||||||
|
lon, _ = strconv.ParseFloat(results[0].Lon, 64)
|
||||||
|
return lat, lon, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user