diff --git a/api/http/cafe_handler.go b/api/http/cafe_handler.go index 97cfd2c..f30abec 100644 --- a/api/http/cafe_handler.go +++ b/api/http/cafe_handler.go @@ -653,3 +653,56 @@ func (h Cafe) ReserveCafe(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) return } + +type RequestNearestCafes struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Radius float64 `json:"radius"` +} + +func (h Cafe) GetNearestCafes(c *gin.Context) { + ctx, cancel := context.WithTimeout(c, TimeOut) + defer cancel() + + var req RequestNearestCafes + + err := c.ShouldBindJSON(&req) + if err != nil { + log.GetLog().WithError(err).Error("Unable to bind JSON") + c.JSON(http.StatusBadRequest, gin.H{"error": errors.ErrBadRequest.Error().Error()}) + return + } + + cafes, err := h.Handler.GetNearestCafes(ctx, req.Latitude, req.Longitude, req.Radius) + if err != nil { + log.GetLog().Errorf("Unable to get nearest cafes. error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"cafes": cafes}) + +} + +func (h Cafe) SetCafeLocation(c *gin.Context) { + ctx, cancel := context.WithTimeout(c, TimeOut) + defer cancel() + + var req models.Location + + err := c.ShouldBindJSON(&req) + if err != nil { + log.GetLog().WithError(err).Error("Unable to bind JSON") + c.JSON(http.StatusBadRequest, gin.H{"error": errors.ErrBadRequest.Error().Error()}) + return + } + + err = h.Handler.SetCafeLocation(ctx, &req) + if err != nil { + log.GetLog().Errorf("Unable to set cafe location. error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml index 6212855..07d1ef6 100644 --- a/deploy/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -26,6 +26,20 @@ services: resources: limits: memory: 200M + redis: + image: redis + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - 6379:6379 + deploy: + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 5 + resources: + limits: + memory: 400M postgres_backend: image: postgres:alpine3.19 volumes: diff --git a/go.mod b/go.mod index e7b6d5c..79b4cc6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.5.5 + github.com/redis/go-redis/v9 v9.5.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cast v1.6.0 github.com/stretchr/testify v1.9.0 @@ -17,9 +18,11 @@ require ( require ( github.com/bytedance/sonic v1.11.3 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -29,7 +32,6 @@ require ( github.com/golang/snappy v0.0.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx v3.6.2+incompatible // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect @@ -40,7 +42,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect diff --git a/go.sum b/go.sum index 2b69b58..6a1ad2d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -12,6 +18,8 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -43,8 +51,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= @@ -74,10 +80,10 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6f github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= diff --git a/internal/modules/cafe.go b/internal/modules/cafe.go index 91fe66d..470a7c3 100644 --- a/internal/modules/cafe.go +++ b/internal/modules/cafe.go @@ -8,6 +8,7 @@ import ( "barista/pkg/utils" "context" "fmt" + "github.com/redis/go-redis/v9" "math/rand" "strconv" "time" @@ -27,6 +28,8 @@ type CafeHandler struct { ReservationRepo repo.ReservationRepo MenuItemRepo repo.MenuItemsRepo PaymentRepo repo.Transaction + LocationsRepo repo.LocationsRepo + Redis *redis.Client } func (c CafeHandler) Create(ctx context.Context, cafe *models.Cafe) error { @@ -945,3 +948,21 @@ func (c CafeHandler) ReserveCafe(ctx context.Context, reservation *models.Reserv return nil } + +func (c CafeHandler) GetNearestCafes(ctx context.Context, lat float64, long float64, radius float64) ([]redis.GeoLocation, error) { + + return c.Redis.GeoRadius(ctx, "locations", lat, long, &redis.GeoRadiusQuery{ + Radius: radius, + Unit: "km", + WithCoord: true, + WithDist: true, + WithGeoHash: true, + Count: 5, + Sort: "ASC", + }).Result() + +} + +func (c CafeHandler) SetCafeLocation(ctx context.Context, m *models.Location) error { + return c.LocationsRepo.SetLocation(ctx, m) +} diff --git a/internal/service.go b/internal/service.go index d7acdb9..7fa8692 100644 --- a/internal/service.go +++ b/internal/service.go @@ -3,13 +3,17 @@ package internal import ( "barista/api/http" "barista/internal/modules" + "barista/pkg/log" "barista/pkg/middlewares" "barista/pkg/models" "barista/pkg/repo" "barista/pkg/utils" + "context" + "github.com/redis/go-redis/v9" "github.com/spf13/cast" "go.mongodb.org/mongo-driver/mongo/options" "os" + "time" ) func getPostgres() (string, int) { @@ -40,6 +44,20 @@ func getMongo() (string, int) { return address, cast.ToInt(port) } +func getRedis() (string, int) { + address, ok := os.LookupEnv("redis_address") + if !ok { + return "localhost", 6379 + } + + port, ok := os.LookupEnv("redis_port") + if !ok { + return "localhost", 6379 + } + + return address, cast.ToInt(port) +} + func Run() { address, port := getPostgres() postgres := utils.NewPostgres( @@ -63,6 +81,13 @@ func Run() { ) mongoDbOpt := options.GridFSBucket().SetName("image-server") + address, port = getRedis() + rdb := redis.NewClient(&redis.Options{ + Addr: address + ":" + cast.ToString(port), + Password: "", // no password set + DB: 0, // use default DB + }) + authMiddleware := middlewares.AuthMiddleware{Postgres: postgres} service := StartService() @@ -93,10 +118,48 @@ func Run() { eventRepo := repo.NewEventRepoImp(postgres) reservationRepo := repo.NewReservationRepoImp(postgres) menuItemRepo := repo.NewMenuItemRepoImp(postgres) - - cafeHandler := modules.CafeHandler{CafeRepo: cafeRepo, Rating: ratingRepo, CommentRepo: commentRepo, ImageRepo: imageRepo, EventRepo: eventRepo, UserRepo: userRepo, ReservationRepo: reservationRepo, MenuItemRepo: menuItemRepo, PaymentRepo: paymentRepo} + locationRepo := repo.NewLocationsRepoImp(postgres) + + cafeHandler := modules.CafeHandler{ + CafeRepo: cafeRepo, + Rating: ratingRepo, + CommentRepo: commentRepo, + ImageRepo: imageRepo, + EventRepo: eventRepo, + UserRepo: userRepo, + ReservationRepo: reservationRepo, + MenuItemRepo: menuItemRepo, + PaymentRepo: paymentRepo, + Redis: rdb, + } cafeHttpHandler := http.Cafe{Handler: &cafeHandler} + newTicker := time.NewTicker(1 * time.Minute) + go func() { + for { + select { + case <-newTicker.C: + if err := rdb.FlushDB(context.Background()).Err(); err != nil { + log.GetLog().Errorf("Unable to flush redis db. error: %v", err) + } + locations, err := locationRepo.FindAll(context.Background()) + if err != nil { + log.GetLog().Errorf("Unable to get locations. error: %v", err) + } + + for _, location := range locations { + if err := rdb.GeoAdd(context.Background(), "locations", &redis.GeoLocation{ + Name: cast.ToString(location.CafeID), + Longitude: location.Lng, + Latitude: location.Lat, + }).Err(); err != nil { + log.GetLog().Errorf("Unable to add location to redis. error: %v", err) + } + } + } + } + }() + cafe := apiV1.Group("/cafe") cafe.Handle(string(models.POST), "create", authMiddleware.IsAuthorized, cafeHttpHandler.Create) cafe.Handle(string(models.POST), "search-cafe", cafeHttpHandler.SearchCafe) @@ -117,6 +180,8 @@ func Run() { cafe.Handle(string(models.GET), "fully-booked-days", cafeHttpHandler.GetFullyBookedDays) cafe.Handle(string(models.GET), "time-slots", cafeHttpHandler.GetTimeSlots) cafe.Handle(string(models.POST), "reserve-cafe", authMiddleware.IsAuthorized, cafeHttpHandler.ReserveCafe) + cafe.Handle(string(models.GET), "get-nearest-cafes", cafeHttpHandler.GetNearestCafes) + cafe.Handle(string(models.GET), "set-location", cafeHttpHandler.SetCafeLocation) imageHandler := http.ImageHandler{MongoDb: mongoDb, MongoOpt: mongoDbOpt, ImageRepo: imageRepo} image := apiV1.Group("/image") diff --git a/pkg/models/locations.go b/pkg/models/locations.go new file mode 100644 index 0000000..42bef93 --- /dev/null +++ b/pkg/models/locations.go @@ -0,0 +1,7 @@ +package models + +type Location struct { + CafeID int32 `json:"cafe_id"` + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` +} diff --git a/pkg/repo/lcoations.go b/pkg/repo/lcoations.go new file mode 100644 index 0000000..88832dd --- /dev/null +++ b/pkg/repo/lcoations.go @@ -0,0 +1,56 @@ +package repo + +import ( + "barista/pkg/log" + "barista/pkg/models" + "context" + "github.com/jackc/pgx/v5/pgxpool" +) + +type LocationsRepo interface { + SetLocation(ctx context.Context, location *models.Location) error + FindAll(ctx context.Context) ([]*models.Location, error) +} + +type LocationsRepoImp struct { + postgres *pgxpool.Pool +} + +func NewLocationsRepoImp(postgres *pgxpool.Pool) *LocationsRepoImp { + _, err := postgres.Exec(context.Background(), + `CREATE TABLE IF NOT EXISTS locations ( + id BIGINT PRIMARY KEY, + latitude FLOAT, + longitude FLOAT + );`) + if err != nil { + log.GetLog().WithError(err).WithField("table", "locations").Fatal("Unable to create table") + } + return &LocationsRepoImp{postgres: postgres} +} + +func (r *LocationsRepoImp) SetLocation(ctx context.Context, location *models.Location) error { + _, err := r.postgres.Exec(ctx, "INSERT INTO locations (id, latitude, longitude) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET latitude = $2, longitude = $3", location.CafeID, location.Lat, location.Lng) + if err != nil { + log.GetLog().Errorf("Unable to insert location. error: %v", err) + } + return err +} + +func (r *LocationsRepoImp) FindAll(ctx context.Context) ([]*models.Location, error) { + var locations []*models.Location + rows, err := r.postgres.Query(ctx, "SELECT id, latitude, longitude FROM locations") + if err != nil { + log.GetLog().Errorf("Unable to get locations. error: %v", err) + } + for rows.Next() { + var location models.Location + err := rows.Scan(&location.CafeID, &location.Lat, &location.Lng) + if err != nil { + log.GetLog().Errorf("Unable to scan location. error: %v", err) + continue + } + locations = append(locations, &location) + } + return locations, err +}