Files
base/internal/application/landing/service.go
2026-04-10 18:25:21 +03:30

239 lines
5.3 KiB
Go

package landing
import (
"context"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.uber.org/fx"
"golang.org/x/sync/errgroup"
domainAsset "base/internal/domain/asset"
domainProfile "base/internal/domain/profile"
"base/internal/dto"
"base/pkg/cache"
)
const (
defaultAssetsPerCategory = 6
defaultSpecialistsLimit = 6
defaultRolesLimit = 20
landingCacheKey = "landing:page"
landingCacheTTL = 5 * time.Minute
)
type Service interface {
GetLanding(ctx context.Context) (*dto.Landing, error)
}
type service struct {
logger zerolog.Logger
cache cache.Cache[dto.Landing]
categoryRepo domainAsset.CategoryRepository
assetRepo domainAsset.AssetRepository
profileRepo domainProfile.Repository
roleRepo domainProfile.RoleRepository
}
type Param struct {
Logger zerolog.Logger
Cache cache.Cache[dto.Landing]
CategoryRepo domainAsset.CategoryRepository
AssetRepo domainAsset.AssetRepository
ProfileRepo domainProfile.Repository
RoleRepo domainProfile.RoleRepository
fx.In
}
func New(param Param) Service {
return &service{
logger: param.Logger,
cache: param.Cache,
categoryRepo: param.CategoryRepo,
assetRepo: param.AssetRepo,
profileRepo: param.ProfileRepo,
roleRepo: param.RoleRepo,
}
}
func (s *service) GetLanding(ctx context.Context) (*dto.Landing, error) {
result, err := s.cache.WithCache(ctx, landingCacheKey, s.fetchLanding, landingCacheTTL)
if err != nil {
return nil, err
}
return &result, nil
}
func (s *service) fetchLanding(ctx context.Context) (dto.Landing, error) {
data := &dto.LandingPageData{
Categories: []dto.CategoryDTO{},
SpecialistRoles: []dto.ProfileRole{},
Assets: []dto.LandingAssetData{},
Specialists: []dto.Specialist{},
Blogs: []dto.Blog{},
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
categories, err := s.categoryRepo.FindAll(gCtx)
if err != nil {
return err
}
data.Categories = make([]dto.CategoryDTO, len(categories))
for i, c := range categories {
data.Categories[i] = dto.CategoryDTO{
ID: c.ID,
Name: c.Name,
Icon: c.Icon,
Color: c.Color,
CardType: c.CardType,
Featured: c.Featured,
Description: c.Description,
}
}
return nil
})
g.Go(func() error {
domainRoles, err := s.roleRepo.List(gCtx, defaultRolesLimit, 0)
if err != nil {
return err
}
data.SpecialistRoles = make([]dto.ProfileRole, len(domainRoles))
for i, r := range domainRoles {
data.SpecialistRoles[i] = dto.ProfileRole{
Id: r.ID.String(),
Title: r.Title,
}
}
return nil
})
g.Go(func() error {
profiles, _, err := s.profileRepo.FindAll(
gCtx,
domainProfile.Filter{
Page: 1,
PageSize: defaultSpecialistsLimit,
SortedBy: "created_at",
Ascending: false,
})
if err != nil {
return err
}
data.Specialists = make([]dto.Specialist, len(profiles))
for i, p := range profiles {
data.Specialists[i] = dto.Specialist{
Id: p.ID.String(),
Handle: p.Handle,
Avatar: p.About.ProfilePicture,
}
}
return nil
})
g.Go(func() error {
categories, err := s.categoryRepo.FindAll(gCtx)
if err != nil {
return err
}
assetsByCat := make([]dto.LandingAssetData, len(categories))
mu := &sync.Mutex{}
eg, egCtx := errgroup.WithContext(gCtx)
for index, category := range categories {
i, cat := index, category
eg.Go(func() error {
assets, findLatestAssetErr := s.assetRepo.FindLatestByCategory(egCtx, cat.ID, defaultAssetsPerCategory)
if findLatestAssetErr != nil {
return findLatestAssetErr
}
assetResp := make([]dto.AssetResponse, len(assets))
for j, a := range assets {
assetResp[j] = *s.toAssetResponse(a)
}
mu.Lock()
assetsByCat[i] = dto.LandingAssetData{
AssetCategory: dto.AssetCategory{
Id: cat.ID.String(),
Title: cat.Name,
Icon: cat.Icon,
},
Assets: assetResp,
}
mu.Unlock()
return nil
})
}
if err = eg.Wait(); err != nil {
return err
}
data.Assets = assetsByCat
return nil
})
if err := g.Wait(); err != nil {
return dto.Landing{}, err
}
return dto.Landing{
Message: "Landing page fetched successfully",
Data: *data,
}, nil
}
func (s *service) toAssetResponse(a *domainAsset.Asset) *dto.AssetResponse {
coverImage := ""
for _, art := range a.AssetArtifacts {
if strings.Contains(strings.ToLower(art.Type), "image") {
coverImage = art.DownloadURL
break
}
}
resp := &dto.AssetResponse{
ID: a.ID,
ProfileID: a.ProfileID,
AssetCategoryID: a.AssetCategoryID,
Title: a.Title,
Description: a.Description,
Link: a.Link,
CoverImage: coverImage,
Status: int(a.Status),
CreatedAt: formatTime(a.CreatedAt),
UpdatedAt: formatTime(a.UpdatedAt),
}
if a.AssetCategory.ID != uuid.Nil {
resp.Category = dto.CategoryDTO{
ID: a.AssetCategory.ID,
Name: a.AssetCategory.Name,
Icon: a.AssetCategory.Icon,
Color: a.AssetCategory.Color,
CardType: a.AssetCategory.CardType,
Featured: a.AssetCategory.Featured,
Description: a.AssetCategory.Description,
}
}
return resp
}
func formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.RFC3339)
}