fix: error handling, defer cleanup, graceful shutdown, golangci-lint setup
This commit is contained in:
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.25"
|
||||||
|
- uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.25"
|
||||||
|
- name: Test
|
||||||
|
run: go test ./tests/...
|
||||||
12
.golangci.yml
Normal file
12
.golangci.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- govet
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
- linters: [staticcheck]
|
||||||
|
text: "SA1029"
|
||||||
12
cmd/main.go
12
cmd/main.go
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -54,7 +56,11 @@ func main() {
|
|||||||
slog.ErrorContext(ctx, "Ошибка подключения к redis", slog.String("error", err.Error()))
|
slog.ErrorContext(ctx, "Ошибка подключения к redis", slog.String("error", err.Error()))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer redis.Close()
|
defer func() {
|
||||||
|
if err := redis.Close(); err != nil {
|
||||||
|
slog.Warn("redis close error", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := database.RunMigrations(ctx, cfg.DatabaseURL()); err != nil {
|
if err := database.RunMigrations(ctx, cfg.DatabaseURL()); err != nil {
|
||||||
slog.ErrorContext(ctx, "Ошибка миграций", slog.String("error", err.Error()))
|
slog.ErrorContext(ctx, "Ошибка миграций", slog.String("error", err.Error()))
|
||||||
@@ -78,5 +84,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
if err := <-serverErr; err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
slog.Error("Ошибка при остановке сервера", "error", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("Приложение остановлено")
|
slog.Info("Приложение остановлено")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ func RunMigrations(ctx context.Context, dbURL string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("не удалось открыть соединение для миграций: %w", err)
|
return fmt.Errorf("не удалось открыть соединение для миграций: %w", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close() //nolint:errcheck
|
||||||
|
|
||||||
goose.SetDialect(gooseDriverName)
|
if err := goose.SetDialect(gooseDriverName); err != nil {
|
||||||
|
return fmt.Errorf("goose dialect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
statusErr := goose.Status(db, migrationsDir)
|
statusErr := goose.Status(db, migrationsDir)
|
||||||
if statusErr != nil {
|
if statusErr != nil {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"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"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +35,12 @@ func (s *Server) ListDrivers(w http.ResponseWriter, r *http.Request, params api.
|
|||||||
|
|
||||||
func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
|
if !ok {
|
||||||
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
|
return
|
||||||
|
}
|
||||||
if claims.Role != "admin" {
|
if claims.Role != "admin" {
|
||||||
slog.WarnContext(ctx, "unusual try from not allowed role", slog.String("Role", claims.Role), slog.String("id", claims.ID.String()))
|
slog.WarnContext(ctx, "unusual try from not allowed role", slog.String("Role", claims.Role), slog.String("id", claims.ID.String()))
|
||||||
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
|
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
|
||||||
@@ -72,7 +78,11 @@ func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer func() {
|
||||||
|
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||||
|
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := storage.Create(ctx, "users", user, tx); err != nil {
|
if err := storage.Create(ctx, "users", user, tx); err != nil {
|
||||||
slog.ErrorContext(ctx, "Unable to create user", slog.String("error", err.Error()), slog.Any("user", user))
|
slog.ErrorContext(ctx, "Unable to create user", slog.String("error", err.Error()), slog.Any("user", user))
|
||||||
@@ -111,9 +121,9 @@ func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) UpdateMyDriverStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) UpdateMyDriverStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok || claims == nil {
|
if !ok || claims == nil {
|
||||||
slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value("user")))
|
slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value(UserKey)))
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -43,7 +44,9 @@ func (h *Hub) Broadcast(orderID uuid.UUID, msg any) {
|
|||||||
h.mu.RLock()
|
h.mu.RLock()
|
||||||
defer h.mu.RUnlock()
|
defer h.mu.RUnlock()
|
||||||
for _, conn := range h.connections[orderID] {
|
for _, conn := range h.connections[orderID] {
|
||||||
conn.WriteJSON(msg)
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
slog.Error("error while writing json to clients", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
"github.com/huandu/go-sqlbuilder"
|
"github.com/huandu/go-sqlbuilder"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,12 +66,18 @@ func (s *Server) CreateManager(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer func() {
|
||||||
|
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||||
|
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := storage.Create(ctx, "users", userModel, tx); err != nil {
|
if err := storage.Create(ctx, "users", userModel, tx); err != nil {
|
||||||
slog.ErrorContext(ctx, "Error while creating user", slog.String("error", err.Error()))
|
slog.ErrorContext(ctx, "Error while creating user", slog.String("error", err.Error()))
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
tx.Rollback(ctx)
|
if err := tx.Rollback(ctx); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +91,9 @@ func (s *Server) CreateManager(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := storage.Create(ctx, "managers", managerModel, tx); err != nil {
|
if err := storage.Create(ctx, "managers", managerModel, tx); err != nil {
|
||||||
slog.ErrorContext(ctx, "Error while creating manager", slog.String("error", err.Error()))
|
slog.ErrorContext(ctx, "Error while creating manager", slog.String("error", err.Error()))
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
tx.Rollback(ctx)
|
if err := tx.Rollback(ctx); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, params api.ListNotificationsParams) {
|
func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, params api.ListNotificationsParams) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -36,7 +36,7 @@ func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, param
|
|||||||
|
|
||||||
func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -51,6 +51,10 @@ func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id
|
|||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if notification == nil {
|
||||||
|
s.JSON(w, r, http.StatusNotFound, MsgNotFound, RespNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
if notification.UserID != claims.ID {
|
if notification.UserID != claims.ID {
|
||||||
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
|
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
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) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -32,7 +32,7 @@ func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.L
|
|||||||
|
|
||||||
func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "error while casting claims")
|
slog.ErrorContext(ctx, "error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -60,7 +60,7 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -86,7 +86,7 @@ func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_typ
|
|||||||
|
|
||||||
func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -111,7 +111,7 @@ func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_
|
|||||||
|
|
||||||
func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -123,6 +123,7 @@ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id op
|
|||||||
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
|
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
order, err := s.OrderSerice.UpdateOrderStatus(ctx, id, claims.ID, claims.Role, req)
|
order, err := s.OrderSerice.UpdateOrderStatus(ctx, id, claims.ID, claims.Role, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
@@ -136,15 +137,17 @@ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id op
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Status == api.OrderStatusUpdateStatusInTransit {
|
if req.Status == api.OrderStatusUpdateStatusInTransit {
|
||||||
go s.startRouteTracker(id)
|
go s.startRouteTracker(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.JSON(w, r, http.StatusOK, order, RespSuccess)
|
s.JSON(w, r, http.StatusOK, order, RespSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params api.GetOrdersReportParams) {
|
func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params api.GetOrdersReportParams) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -164,12 +167,12 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params
|
|||||||
|
|
||||||
f := excelize.NewFile()
|
f := excelize.NewFile()
|
||||||
sheet := "Orders"
|
sheet := "Orders"
|
||||||
f.SetSheetName("Sheet1", sheet)
|
f.SetSheetName("Sheet1", sheet) //nolint:errcheck
|
||||||
|
|
||||||
headers := []string{"ID", "Status", "Origin", "Destination", "Weight", "Volume", "Price", "Created At"}
|
headers := []string{"ID", "Status", "Origin", "Destination", "Weight", "Volume", "Price", "Created At"}
|
||||||
for i, h := range headers {
|
for i, h := range headers {
|
||||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||||
f.SetCellValue(sheet, cell, h)
|
f.SetCellValue(sheet, cell, h) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
for row, o := range orders {
|
for row, o := range orders {
|
||||||
@@ -185,7 +188,7 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params
|
|||||||
}
|
}
|
||||||
for col, v := range values {
|
for col, v := range values {
|
||||||
cell, _ := excelize.CoordinatesToCellName(col+1, row+2)
|
cell, _ := excelize.CoordinatesToCellName(col+1, row+2)
|
||||||
f.SetCellValue(sheet, cell, v)
|
f.SetCellValue(sheet, cell, v) //nolint:errcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf, err := f.WriteToBuffer()
|
buf, err := f.WriteToBuffer()
|
||||||
@@ -198,12 +201,12 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params
|
|||||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename=orders_report.xlsx")
|
w.Header().Set("Content-Disposition", "attachment; filename=orders_report.xlsx")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(buf.Bytes())
|
w.Write(buf.Bytes()) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) GetDashboard(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) GetDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "error while casting claims")
|
slog.ErrorContext(ctx, "error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ func (s *Server) RouteWebSocket(w http.ResponseWriter, r *http.Request, id opena
|
|||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
slog.Warn("ws conn close error", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
orderID := id
|
orderID := id
|
||||||
s.Hub.Register(id, conn)
|
s.Hub.Register(id, conn)
|
||||||
@@ -58,10 +62,12 @@ func (s *Server) RouteWebSocket(w http.ResponseWriter, r *http.Request, id opena
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
coords, err := route.ParseCoordinates()
|
coords, err := route.ParseCoordinates()
|
||||||
if err == nil && route.CurrentIndex < len(coords) {
|
if err == nil && route.CurrentIndex < len(coords) {
|
||||||
conn.WriteJSON(map[string]any{
|
if err := conn.WriteJSON(map[string]any{
|
||||||
"current_index": route.CurrentIndex,
|
"current_index": route.CurrentIndex,
|
||||||
"coordinate": coords[route.CurrentIndex],
|
"coordinate": coords[route.CurrentIndex],
|
||||||
})
|
}); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "error while writing date to clients", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +108,11 @@ func (s *Server) startRouteTracker(orderID uuid.UUID) {
|
|||||||
"coordinate": coords[route.CurrentIndex],
|
"coordinate": coords[route.CurrentIndex],
|
||||||
})
|
})
|
||||||
route.CurrentIndex++
|
route.CurrentIndex++
|
||||||
storage.Update(ctx, "routes", *route, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
|
if err := storage.Update(ctx, "routes", *route, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
|
||||||
sb.Where(sb.EQ("order_id", orderID))
|
sb.Where(sb.EQ("order_id", orderID))
|
||||||
})
|
}); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "error while updating route", slog.String("id", route.ID.String()), slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type ctxKey string
|
|||||||
const (
|
const (
|
||||||
requestIDKey ctxKey = "X-Request-ID"
|
requestIDKey ctxKey = "X-Request-ID"
|
||||||
tokenKey ctxKey = "Authorization"
|
tokenKey ctxKey = "Authorization"
|
||||||
|
UserKey ctxKey = "user"
|
||||||
|
|
||||||
// response messages
|
// response messages
|
||||||
MsgInternalError = "Internal server error"
|
MsgInternalError = "Internal server error"
|
||||||
@@ -145,7 +146,7 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), "user", claims)
|
ctx := context.WithValue(r.Context(), UserKey, claims)
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"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"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -54,9 +55,13 @@ func (s *Server) AuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
user.LastLoginAt = &now
|
user.LastLoginAt = &now
|
||||||
user.UpdatedAt = now
|
user.UpdatedAt = now
|
||||||
err = storage.Update(ctx, "users", user, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
|
if err := storage.Update(ctx, "users", user, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
|
||||||
sb.Where(sb.Equal("id", user.ID))
|
sb.Where(sb.Equal("id", user.ID))
|
||||||
})
|
}); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "Failed to update user", slog.String("error", err.Error()))
|
||||||
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
|
return
|
||||||
|
}
|
||||||
s.issueTokens(w, r, user)
|
s.issueTokens(w, r, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +98,8 @@ func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.deleteRefreshCookie(w)
|
||||||
|
s.JSON(w, r, http.StatusOK, "Logged out", RespSuccess)
|
||||||
}
|
}
|
||||||
func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -113,7 +120,7 @@ func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claimsValue := ctx.Value("user")
|
claimsValue := ctx.Value(UserKey)
|
||||||
|
|
||||||
claims, ok := claimsValue.(*Claims)
|
claims, ok := claimsValue.(*Claims)
|
||||||
|
|
||||||
@@ -181,7 +188,7 @@ func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
claimsValue := ctx.Value("user")
|
claimsValue := ctx.Value(UserKey)
|
||||||
claims, ok := claimsValue.(*Claims)
|
claims, ok := claimsValue.(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
|
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
|
||||||
@@ -195,7 +202,11 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer func() {
|
||||||
|
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||||
|
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
err = storage.Delete[models.User](ctx, "users", tx, func(sb *sqlbuilder.DeleteBuilder) {
|
err = storage.Delete[models.User](ctx, "users", tx, func(sb *sqlbuilder.DeleteBuilder) {
|
||||||
sb.Where(sb.Equal("id", userID))
|
sb.Where(sb.Equal("id", userID))
|
||||||
@@ -242,7 +253,7 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
claimsValue := ctx.Value("user")
|
claimsValue := ctx.Value(UserKey)
|
||||||
claims, ok := claimsValue.(*Claims)
|
claims, ok := claimsValue.(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
|
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
|
||||||
@@ -265,7 +276,7 @@ func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
@@ -325,7 +336,7 @@ func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) GetMyTrips(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) GetMyTrips(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value("user").(*Claims)
|
claims, ok := ctx.Value(UserKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.ErrorContext(ctx, "Error while casting claims")
|
slog.ErrorContext(ctx, "Error while casting claims")
|
||||||
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ type DashboardRevenue struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardOrderStatus struct {
|
type DashboardOrderStatus struct {
|
||||||
Total int `json:"total`
|
Total int `json:"total"`
|
||||||
Delivered int `json:"delivered"`
|
Delivered int `json:"delivered"`
|
||||||
InTransit int `json:"inTransit`
|
InTransit int `json:"inTransit"`
|
||||||
Pending int `json:"pending"`
|
Pending int `json:"pending"`
|
||||||
Cancelled int `json:"cancelled"`
|
Cancelled int `json:"cancelled"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/anxi0uz/logiflow/pkg/geocode"
|
"github.com/anxi0uz/logiflow/pkg/geocode"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/huandu/go-sqlbuilder"
|
"github.com/huandu/go-sqlbuilder"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
@@ -102,7 +103,11 @@ func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, use
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("osrm request: %w", err)
|
return nil, fmt.Errorf("osrm request: %w", err)
|
||||||
}
|
}
|
||||||
defer osrmResp.Body.Close()
|
defer func() {
|
||||||
|
if err := osrmResp.Body.Close(); err != nil {
|
||||||
|
slog.Warn("failed to close response body", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
var osrmResult geocode.OsrmResult
|
var osrmResult geocode.OsrmResult
|
||||||
if err := json.NewDecoder(osrmResp.Body).Decode(&osrmResult); err != nil || len(osrmResult.Routes) == 0 {
|
if err := json.NewDecoder(osrmResp.Body).Decode(&osrmResult); err != nil || len(osrmResult.Routes) == 0 {
|
||||||
@@ -154,6 +159,7 @@ func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, use
|
|||||||
OrderID: orderID,
|
OrderID: orderID,
|
||||||
Coordinates: coordsJSON,
|
Coordinates: coordsJSON,
|
||||||
DurationSec: int(route.Duration),
|
DurationSec: int(route.Duration),
|
||||||
|
DistanceKm: distanceKm,
|
||||||
Status: "pending",
|
Status: "pending",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +167,11 @@ func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, use
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("begin tx: %w", err)
|
return nil, fmt.Errorf("begin tx: %w", err)
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer func() {
|
||||||
|
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||||
|
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := storage.Create(ctx, "orders", order, tx); err != nil {
|
if err := storage.Create(ctx, "orders", order, tx); err != nil {
|
||||||
return nil, fmt.Errorf("create order: %w", err)
|
return nil, fmt.Errorf("create order: %w", err)
|
||||||
@@ -284,20 +294,28 @@ func (s *OrderService) UpdateOrderStatus(ctx context.Context, id uuid.UUID, user
|
|||||||
}
|
}
|
||||||
order.DriverID = req.DriverId
|
order.DriverID = req.DriverId
|
||||||
order.AssignedAt = &now
|
order.AssignedAt = &now
|
||||||
driver, _ := storage.GetOne[models.Driver](ctx, s.db, "drivers", func(sb *sqlbuilder.SelectBuilder) {
|
driver, err := storage.GetOne[models.Driver](ctx, s.db, "drivers", func(sb *sqlbuilder.SelectBuilder) {
|
||||||
sb.Where(sb.EQ("id", order.DriverID))
|
sb.Where(sb.EQ("id", order.DriverID))
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "error while getting driver", slog.String("error", err.Error()))
|
||||||
|
return nil, fmt.Errorf("update order status: %w", err)
|
||||||
|
}
|
||||||
s.createNotification(ctx, driver.UserID, "Новый заказ", "Вам назначен новый заказ")
|
s.createNotification(ctx, driver.UserID, "Новый заказ", "Вам назначен новый заказ")
|
||||||
}
|
}
|
||||||
if req.Status == api.OrderStatusUpdateStatusInTransit {
|
if req.Status == api.OrderStatusUpdateStatusInTransit {
|
||||||
if order.CreatedByID == nil {
|
if order.CreatedByID == nil {
|
||||||
return nil, fmt.Errorf("created by id needed")
|
return nil, fmt.Errorf("created by id needed")
|
||||||
}
|
}
|
||||||
s.createNotification(ctx, *order.CreatedByID, "Заказ в пути", "Ваш заказ передан водителю")
|
if order.CreatedByID != nil {
|
||||||
|
s.createNotification(ctx, *order.CreatedByID, "Заказ в пути", "Ваш заказ передан водителю")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if req.Status == api.OrderStatusUpdateStatusDelivered {
|
if req.Status == api.OrderStatusUpdateStatusDelivered {
|
||||||
order.DeliveredAt = &now
|
order.DeliveredAt = &now
|
||||||
s.createNotification(ctx, *order.CreatedByID, "Заказ доставлен", "Ваш заказ успешно доставлен")
|
if order.CreatedByID != nil {
|
||||||
|
s.createNotification(ctx, *order.CreatedByID, "Заказ доставлен", "Ваш заказ успешно доставлен")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := storage.Update(ctx, "orders", *order, s.db, func(sb *sqlbuilder.UpdateBuilder) {
|
if err := storage.Update(ctx, "orders", *order, s.db, func(sb *sqlbuilder.UpdateBuilder) {
|
||||||
sb.Where(sb.EQ("id", id))
|
sb.Where(sb.EQ("id", id))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
neturl "net/url"
|
neturl "net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -35,7 +36,11 @@ func Geocode(ctx context.Context, address string) (lat, lon float64, err error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
slog.Warn("failed to close response body", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
var results []struct {
|
var results []struct {
|
||||||
Lat string `json:"lat"`
|
Lat string `json:"lat"`
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func newTestServer(svc services.OrderServicer) *handler.Server {
|
|||||||
|
|
||||||
func withClaims(r *http.Request, id uuid.UUID, role string) *http.Request {
|
func withClaims(r *http.Request, id uuid.UUID, role string) *http.Request {
|
||||||
claims := &handler.Claims{ID: id, Role: role}
|
claims := &handler.Claims{ID: id, Role: role}
|
||||||
ctx := context.WithValue(r.Context(), "user", claims)
|
ctx := context.WithValue(r.Context(), handler.UserKey, claims)
|
||||||
return r.WithContext(ctx)
|
return r.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user