fix: error handling, defer cleanup, graceful shutdown, golangci-lint setup

This commit is contained in:
2026-04-18 17:32:34 +03:00
parent 44cb3c6576
commit ebb8cded41
16 changed files with 174 additions and 47 deletions

31
.github/workflows/ci.yml vendored Normal file
View 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
View File

@@ -0,0 +1,12 @@
version: "2"
linters:
default: none
enable:
- govet
- staticcheck
- unused
exclusions:
rules:
- linters: [staticcheck]
text: "SA1029"

View File

@@ -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("Приложение остановлено")
} }

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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()))
}
} }
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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()))
}
} }
} }
}) })

View File

@@ -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))
}) })
} }

View File

@@ -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)

View File

@@ -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"`
} }

View File

@@ -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))

View File

@@ -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"`

View File

@@ -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)
} }