From 213d37033230cb02754f2a9b0d1fd51077297daa Mon Sep 17 00:00:00 2001 From: ericprd Date: Sun, 9 Mar 2025 00:47:24 +0800 Subject: [PATCH] feat: create ads --- database/ads_model.go | 11 ++++ database/new_db.go | 1 + internal/accessor/ads/create.go | 14 +++++ internal/accessor/ads/impl.go | 20 ++++++ internal/accessor/module.go | 2 + internal/accessor/oss/oss.go | 16 +++++ internal/accessor/oss/upload.go | 1 + internal/api/http/ads/create.go | 106 ++++++++++++++++++++++++++++++++ internal/api/http/ads/module.go | 7 +++ internal/api/http/router.go | 2 + internal/domain/ads/spec.go | 19 ++++++ internal/services/ads/create.go | 56 +++++++++++++++++ internal/services/ads/impl.go | 32 ++++++++++ internal/services/module.go | 2 + 14 files changed, 289 insertions(+) create mode 100644 database/ads_model.go create mode 100644 internal/accessor/ads/create.go create mode 100644 internal/accessor/ads/impl.go create mode 100644 internal/api/http/ads/create.go create mode 100644 internal/api/http/ads/module.go create mode 100644 internal/domain/ads/spec.go create mode 100644 internal/services/ads/create.go create mode 100644 internal/services/ads/impl.go diff --git a/database/ads_model.go b/database/ads_model.go new file mode 100644 index 0000000..7177d40 --- /dev/null +++ b/database/ads_model.go @@ -0,0 +1,11 @@ +package database + +import "time" + +type Ads struct { + ID string `gorm:"primaryKey;not null" json:"id"` + ImageUrl string `gorm:"not null" json:"image_url"` + Url string `gorm:"not null" json:"url"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` +} diff --git a/database/new_db.go b/database/new_db.go index 2bd0f7c..bd3c56f 100644 --- a/database/new_db.go +++ b/database/new_db.go @@ -54,5 +54,6 @@ func (db *DB) Migrate() error { &News{}, &Tag{}, &Category{}, + &Ads{}, ) } diff --git a/internal/accessor/ads/create.go b/internal/accessor/ads/create.go new file mode 100644 index 0000000..309f536 --- /dev/null +++ b/internal/accessor/ads/create.go @@ -0,0 +1,14 @@ +package adsrepository + +import ( + "fmt" + adsdomain "legalgo-BE-go/internal/domain/ads" +) + +func (a *accessor) Create(spec adsdomain.Ads) error { + if err := a.db.Create(&spec).Error; err != nil { + return fmt.Errorf("failed to create ads: %v", err) + } + + return nil +} diff --git a/internal/accessor/ads/impl.go b/internal/accessor/ads/impl.go new file mode 100644 index 0000000..5f3689d --- /dev/null +++ b/internal/accessor/ads/impl.go @@ -0,0 +1,20 @@ +package adsrepository + +import ( + "legalgo-BE-go/database" + adsdomain "legalgo-BE-go/internal/domain/ads" +) + +type accessor struct { + db *database.DB +} + +type Ads interface { + Create(adsdomain.Ads) error +} + +func New( + db *database.DB, +) Ads { + return &accessor{db} +} diff --git a/internal/accessor/module.go b/internal/accessor/module.go index 82de0de..5813846 100644 --- a/internal/accessor/module.go +++ b/internal/accessor/module.go @@ -1,6 +1,7 @@ package repository import ( + adsrepository "legalgo-BE-go/internal/accessor/ads" categoryrepository "legalgo-BE-go/internal/accessor/category" newsrepository "legalgo-BE-go/internal/accessor/news" "legalgo-BE-go/internal/accessor/oss" @@ -24,4 +25,5 @@ var Module = fx.Module("repository", fx.Provide( categoryrepository.New, newsrepository.New, oss.New, + adsrepository.New, )) diff --git a/internal/accessor/oss/oss.go b/internal/accessor/oss/oss.go index c5af327..b005c05 100644 --- a/internal/accessor/oss/oss.go +++ b/internal/accessor/oss/oss.go @@ -68,3 +68,19 @@ func (r *OssRepositoryImpl) GetPublicURL(fileName string) string { } return fmt.Sprintf("%s:%s%s", r.cfg.GetPublicURL(), r.cfg.GetBucketName(), fileName) } + +func (r *OssRepositoryImpl) DeleteObject(ctx context.Context, fileName string) error { + if fileName == "" { + return fmt.Errorf("file name is not provided") + } + + _, err := r.s3.DeleteObject(&s3.DeleteObjectInput{ + Key: aws.String(fileName), + }) + + if err != nil { + return fmt.Errorf("failed to delete object %s from bucket: %v", fileName, err) + } + + return nil +} diff --git a/internal/accessor/oss/upload.go b/internal/accessor/oss/upload.go index 9bb518b..ab8e204 100644 --- a/internal/accessor/oss/upload.go +++ b/internal/accessor/oss/upload.go @@ -7,4 +7,5 @@ import ( type OSSRepository interface { UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) GetPublicURL(fileName string) string + DeleteObject(ctx context.Context, fileName string) error } diff --git a/internal/api/http/ads/create.go b/internal/api/http/ads/create.go new file mode 100644 index 0000000..8df90fd --- /dev/null +++ b/internal/api/http/ads/create.go @@ -0,0 +1,106 @@ +package adshttp + +import ( + "fmt" + authmiddleware "legalgo-BE-go/internal/api/http/middleware/auth" + adsdomain "legalgo-BE-go/internal/domain/ads" + "legalgo-BE-go/internal/domain/oss" + adssvc "legalgo-BE-go/internal/services/ads" + "legalgo-BE-go/internal/utilities/response" + "net/http" + + "github.com/go-chi/chi/v5" +) + +const _oneMB = 1 << 20 +const _maxUploadSize = 2 * _oneMB +const _folderName = "/file" + +func Create( + router chi.Router, + adsSvc adssvc.Ads, +) { + router.With(authmiddleware.Authorize()).Post("/ads/create", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + err := r.ParseMultipartForm(10 << 20) // 10 MB + if err != nil { + response.RespondJsonErrorWithCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "unable to parse form", + ) + return + } + + url := r.FormValue("url") + if url == "" { + response.RespondJsonErrorWithCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "URL is missing", + ) + return + } + + file, header, err := r.FormFile("image") + if err != nil { + response.RespondJsonErrorWithCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "unable to retrieve image", + ) + return + } + defer file.Close() + + if header.Size > int64(_maxUploadSize) { + response.ResponseWithErrorCode( + ctx, + w, + fmt.Errorf("file too big"), + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + fmt.Sprintf("The file is too big. The maximum size is %d MB", _maxUploadSize/_oneMB), + ) + return + } + + imageRequest := &oss.UploadFileRequest{ + FileHeader: header, + FolderName: _folderName, + } + + adsReq := adsdomain.AdsReq{ + Image: imageRequest, + Url: url, + } + + if err := adsSvc.Create(ctx, adsReq); err != nil { + response.RespondJsonErrorWithCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.Error(), + ) + return + } + + response.RespondJsonSuccess(ctx, w, struct { + Message string + }{ + Message: "ads created successfully", + }) + }) +} diff --git a/internal/api/http/ads/module.go b/internal/api/http/ads/module.go new file mode 100644 index 0000000..bcd4d52 --- /dev/null +++ b/internal/api/http/ads/module.go @@ -0,0 +1,7 @@ +package adshttp + +import "go.uber.org/fx" + +var Module = fx.Module("ads-http", fx.Invoke( + Create, +)) diff --git a/internal/api/http/router.go b/internal/api/http/router.go index a243659..4c440c7 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -1,6 +1,7 @@ package internalhttp import ( + adshttp "legalgo-BE-go/internal/api/http/ads" categoryhttp "legalgo-BE-go/internal/api/http/category" newshttp "legalgo-BE-go/internal/api/http/news" osshttp "legalgo-BE-go/internal/api/http/oss" @@ -31,6 +32,7 @@ var Module = fx.Module("router", osshttp.Module, userhttp.Module, subscribehttp.Module, + adshttp.Module, ) func initRouter() chi.Router { diff --git a/internal/domain/ads/spec.go b/internal/domain/ads/spec.go new file mode 100644 index 0000000..7ca2561 --- /dev/null +++ b/internal/domain/ads/spec.go @@ -0,0 +1,19 @@ +package adsdomain + +import ( + "legalgo-BE-go/internal/domain/oss" + "time" +) + +type Ads struct { + ID string `gorm:"primaryKey;not null" json:"id"` + ImageUrl string `gorm:"not null" json:"image_url"` + Url string `gorm:"not null" json:"url"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` +} + +type AdsReq struct { + Image *oss.UploadFileRequest `json:"image"` + Url string `json:"url"` +} diff --git a/internal/services/ads/create.go b/internal/services/ads/create.go new file mode 100644 index 0000000..5d68cf9 --- /dev/null +++ b/internal/services/ads/create.go @@ -0,0 +1,56 @@ +package adssvc + +import ( + "context" + "fmt" + adsdomain "legalgo-BE-go/internal/domain/ads" + "path" + + "github.com/google/uuid" +) + +func (i *impl) Create(ctx context.Context, spec adsdomain.AdsReq) error { + id := uuid.NewString() + + file := spec.Image.FileHeader + spec.Image.FileSize = file.Size + spec.Image.Ext = path.Ext(file.Filename) + + if err := i.validate.Struct(spec); err != nil { + return err + } + + // Open the file and read its content + srcFile, err := file.Open() + if err != nil { + return err + } + defer srcFile.Close() + + fileContent := make([]byte, file.Size) + _, err = srcFile.Read(fileContent) + if err != nil { + return err + } + + filePath := fmt.Sprintf("%v/%v%v", spec.Image.FolderName, id, spec.Image.Ext) + fileUrl, err := i.ossRepo.UploadFile(ctx, filePath, fileContent) + if err != nil { + return err + } + + newSpec := adsdomain.Ads{ + ID: id, + ImageUrl: fileUrl, + Url: spec.Url, + } + + if err := i.ads.Create(newSpec); err != nil { + if err := i.ossRepo.DeleteObject(ctx, id); err != nil { + return err + } + return err + } + + return nil +} diff --git a/internal/services/ads/impl.go b/internal/services/ads/impl.go new file mode 100644 index 0000000..4222ada --- /dev/null +++ b/internal/services/ads/impl.go @@ -0,0 +1,32 @@ +package adssvc + +import ( + "context" + adsrepository "legalgo-BE-go/internal/accessor/ads" + "legalgo-BE-go/internal/accessor/oss" + adsdomain "legalgo-BE-go/internal/domain/ads" + + "github.com/go-playground/validator/v10" +) + +type impl struct { + ossRepo oss.OSSRepository + ads adsrepository.Ads + validate *validator.Validate +} + +type Ads interface { + Create(context.Context, adsdomain.AdsReq) error +} + +func New( + ossRepo oss.OSSRepository, + ads adsrepository.Ads, + validate *validator.Validate, +) Ads { + return &impl{ + ossRepo, + ads, + validate, + } +} diff --git a/internal/services/module.go b/internal/services/module.go index 06866c2..d32ddf5 100644 --- a/internal/services/module.go +++ b/internal/services/module.go @@ -1,6 +1,7 @@ package services import ( + adssvc "legalgo-BE-go/internal/services/ads" serviceauth "legalgo-BE-go/internal/services/auth" categorysvc "legalgo-BE-go/internal/services/category" newssvc "legalgo-BE-go/internal/services/news" @@ -23,5 +24,6 @@ var Module = fx.Module("services", newssvc.New, oss.NewOSSService, usersvc.New, + adssvc.New, ), )