package services import ( "encoding/json" "fmt" "log" "mime/multipart" "os" "path/filepath" "time" "rijig/dto" "rijig/internal/repositories" "rijig/model" "rijig/utils" "github.com/google/uuid" ) type ArticleService interface { CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) DeleteArticle(id string) error } type articleService struct { ArticleRepo repositories.ArticleRepository } func NewArticleService(articleRepo repositories.ArticleRepository) ArticleService { return &articleService{ArticleRepo: articleRepo} } func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) { pathImage := "/uploads/articles/" coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage if _, err := os.Stat(coverArticleDir); os.IsNotExist(err) { if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil { return "", fmt.Errorf("failed to create directory for cover article: %v", err) } } allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} extension := filepath.Ext(coverArticle.Filename) if !allowedExtensions[extension] { return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") } coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension) coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName) src, err := coverArticle.Open() if err != nil { return "", fmt.Errorf("failed to open uploaded file: %v", err) } defer src.Close() dst, err := os.Create(coverArticlePath) if err != nil { return "", fmt.Errorf("failed to create cover article file: %v", err) } defer dst.Close() if _, err := dst.ReadFrom(src); err != nil { return "", fmt.Errorf("failed to save cover article: %v", err) } iconTrashUrl := fmt.Sprintf("%s%s", pathImage, coverArticleFileName) return iconTrashUrl, nil } func deleteCoverArticle(imagePath string) error { if imagePath == "" { return nil } baseDir := "./public/" + os.Getenv("BASE_URL") absolutePath := baseDir + imagePath if _, err := os.Stat(absolutePath); os.IsNotExist(err) { return fmt.Errorf("image file not found: %v", err) } err := os.Remove(absolutePath) if err != nil { return fmt.Errorf("failed to delete image: %v", err) } log.Printf("Image deleted successfully: %s", absolutePath) return nil } func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { coverArticlePath, err := s.saveCoverArticle(coverImage) if err != nil { return nil, fmt.Errorf("gagal menyimpan ikon sampah: %v", err) } article := model.Article{ Title: request.Title, CoverImage: coverArticlePath, Author: request.Author, Heading: request.Heading, Content: request.Content, } if err := s.ArticleRepo.CreateArticle(&article); err != nil { return nil, fmt.Errorf("failed to create article: %v", err) } createdAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) articleResponseDTO := &dto.ArticleResponseDTO{ ID: article.ID, Title: article.Title, CoverImage: article.CoverImage, Author: article.Author, Heading: article.Heading, Content: article.Content, PublishedAt: createdAt, UpdatedAt: updatedAt, } cacheKey := fmt.Sprintf("article:%s", article.ID) cacheData := map[string]interface{}{ "data": articleResponseDTO, } if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { fmt.Printf("Error caching article to Redis: %v\n", err) } articles, total, err := s.ArticleRepo.FindAllArticles(0, 0) if err != nil { fmt.Printf("Error fetching all articles: %v\n", err) } var articleDTOs []dto.ArticleResponseDTO for _, a := range articles { createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ ID: a.ID, Title: a.Title, CoverImage: a.CoverImage, Author: a.Author, Heading: a.Heading, Content: a.Content, PublishedAt: createdAt, UpdatedAt: updatedAt, }) } articlesCacheKey := "articles:all" cacheData = map[string]interface{}{ "data": articleDTOs, "total": total, } if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { fmt.Printf("Error caching all articles to Redis: %v\n", err) } return articleResponseDTO, nil } func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) { var cacheKey string if page == 0 && limit == 0 { cacheKey = "articles:all" cachedData, err := utils.GetJSONData(cacheKey) if err == nil && cachedData != nil { if data, ok := cachedData["data"].([]interface{}); ok { var articles []dto.ArticleResponseDTO for _, item := range data { articleData, ok := item.(map[string]interface{}) if ok { articles = append(articles, dto.ArticleResponseDTO{ ID: articleData["article_id"].(string), Title: articleData["title"].(string), CoverImage: articleData["coverImage"].(string), Author: articleData["author"].(string), Heading: articleData["heading"].(string), Content: articleData["content"].(string), PublishedAt: articleData["publishedAt"].(string), UpdatedAt: articleData["updatedAt"].(string), }) } } if total, ok := cachedData["total"].(float64); ok { fmt.Printf("Cached Total Articles: %f\n", total) return articles, int(total), nil } else { fmt.Println("Total articles not found in cache, using 0 as fallback.") return articles, 0, nil } } } } articles, total, err := s.ArticleRepo.FindAllArticles(page, limit) if err != nil { return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) } fmt.Printf("Total Articles from Database: %d\n", total) var articleDTOs []dto.ArticleResponseDTO for _, article := range articles { publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ ID: article.ID, Title: article.Title, CoverImage: article.CoverImage, Author: article.Author, Heading: article.Heading, Content: article.Content, PublishedAt: publishedAt, UpdatedAt: updatedAt, }) } cacheKey = fmt.Sprintf("articles_page:%d_limit:%d", page, limit) cacheData := map[string]interface{}{ "data": articleDTOs, "total": total, } fmt.Printf("Setting cache with total: %d\n", total) if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { fmt.Printf("Error caching articles to Redis: %v\n", err) } return articleDTOs, total, nil } func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) { cacheKey := fmt.Sprintf("article:%s", id) cachedData, err := utils.GetJSONData(cacheKey) if err == nil && cachedData != nil { articleResponse := &dto.ArticleResponseDTO{} if data, ok := cachedData["data"].(string); ok { if err := json.Unmarshal([]byte(data), articleResponse); err == nil { return articleResponse, nil } } } article, err := s.ArticleRepo.FindArticleByID(id) if err != nil { return nil, fmt.Errorf("failed to fetch article by ID: %v", err) } createdAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) articleResponseDTO := &dto.ArticleResponseDTO{ ID: article.ID, Title: article.Title, CoverImage: article.CoverImage, Author: article.Author, Heading: article.Heading, Content: article.Content, PublishedAt: createdAt, UpdatedAt: updatedAt, } cacheData := map[string]interface{}{ "data": articleResponseDTO, } if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { fmt.Printf("Error caching article to Redis: %v\n", err) } return articleResponseDTO, nil } func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { article, err := s.ArticleRepo.FindArticleByID(id) if err != nil { return nil, fmt.Errorf("article not found: %v", id) } if article.CoverImage != "" { err := deleteCoverArticle(article.CoverImage) if err != nil { return nil, fmt.Errorf("failed to delete old image: %v", err) } } var coverArticlePath string if coverImage != nil { coverArticlePath, err = s.saveCoverArticle(coverImage) if err != nil { return nil, fmt.Errorf("failed to save card photo: %v", err) } } if coverArticlePath != "" { article.CoverImage = coverArticlePath } article.Title = request.Title article.Heading = request.Heading article.Content = request.Content article.Author = request.Author err = s.ArticleRepo.UpdateArticle(id, article) if err != nil { return nil, fmt.Errorf("failed to update article: %v", err) } updatedArticle, err := s.ArticleRepo.FindArticleByID(id) if err != nil { return nil, fmt.Errorf("failed to fetch updated article: %v", err) } createdAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.PublishedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.UpdatedAt) articleResponseDTO := &dto.ArticleResponseDTO{ ID: updatedArticle.ID, Title: updatedArticle.Title, CoverImage: updatedArticle.CoverImage, Author: updatedArticle.Author, Heading: updatedArticle.Heading, Content: updatedArticle.Content, PublishedAt: createdAt, UpdatedAt: updatedAt, } articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID) err = utils.SetJSONData(articleCacheKey, map[string]interface{}{"data": articleResponseDTO}, time.Hour*24) if err != nil { fmt.Printf("Error caching updated article to Redis: %v\n", err) } articlesCacheKey := "articles:all" err = utils.DeleteData(articlesCacheKey) if err != nil { fmt.Printf("Error deleting articles cache: %v\n", err) } articles, _, err := s.ArticleRepo.FindAllArticles(0, 0) if err != nil { fmt.Printf("Error fetching all articles: %v\n", err) } else { var articleDTOs []dto.ArticleResponseDTO for _, a := range articles { createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ ID: a.ID, Title: a.Title, CoverImage: a.CoverImage, Author: a.Author, Heading: a.Heading, Content: a.Content, PublishedAt: createdAt, UpdatedAt: updatedAt, }) } cacheData := map[string]interface{}{ "data": articleDTOs, } err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24) if err != nil { fmt.Printf("Error caching updated articles to Redis: %v\n", err) } } return articleResponseDTO, nil } func (s *articleService) DeleteArticle(id string) error { article, err := s.ArticleRepo.FindArticleByID(id) if err != nil { return fmt.Errorf("failed to find article: %v", id) } if err := deleteCoverArticle(article.CoverImage); err != nil { return fmt.Errorf("error waktu menghapus cover image article %s: %v", id, err) } err = s.ArticleRepo.DeleteArticle(id) if err != nil { return fmt.Errorf("failed to delete article: %v", err) } articleCacheKey := fmt.Sprintf("article:%s", id) err = utils.DeleteData(articleCacheKey) if err != nil { fmt.Printf("Error deleting cache for article: %v\n", err) } articlesCacheKey := "articles:all" err = utils.DeleteData(articlesCacheKey) if err != nil { fmt.Printf("Error deleting cache for all articles: %v\n", err) } return nil }