package persistence import ( domain "RenewCMS/internal/domain/site" "RenewCMS/internal/infrastructure/persistence/mappers" entity "RenewCMS/internal/infrastructure/persistence/models" "strings" "gorm.io/gorm" "gorm.io/gorm/clause" ) type SiteRepository struct { db *gorm.DB } func NewSiteRepository(db *gorm.DB) *SiteRepository { return &SiteRepository{db} } type articleTotal struct { SiteID uint32 Total int } func (s *SiteRepository) articleCounts(db *gorm.DB) (map[uint32]int, error) { var totals []articleTotal err := db.Model(&entity.Article{}).Select("site_id, COUNT(*) AS total").Group("site_id").Scan(&totals).Error if err != nil { return nil, err } counts := make(map[uint32]int, len(totals)) for _, total := range totals { counts[total.SiteID] = total.Total } return counts, nil } func (s *SiteRepository) articleCount(db *gorm.DB, id uint32) (int, error) { var total int64 if err := db.Model(&entity.Article{}).Where("site_id = ?", id).Count(&total).Error; err != nil { return 0, err } return int(total), nil } func (s *SiteRepository) List() ([]domain.Site, error) { var found []entity.Site if err := s.db.Order("is_default DESC, LOWER(name) ASC, id ASC").Find(&found).Error; err != nil { return nil, err } counts, err := s.articleCounts(s.db) if err != nil { return nil, err } sites := make([]domain.Site, 0, len(found)) for _, model := range found { sites = append(sites, mappers.SiteToDomain(model, counts[model.ID])) } return sites, nil } func (s *SiteRepository) Find(query domain.ListQuery) (domain.Page, error) { all, err := s.List() if err != nil { return domain.Page{}, err } matching := all if needle := foldForSearch(strings.TrimSpace(query.Q)); needle != "" { matching = make([]domain.Site, 0, len(all)) for _, candidate := range all { if siteMatches(candidate, needle) { matching = append(matching, candidate) } } } page := domain.Page{Items: []domain.Site{}, Total: len(matching)} if query.Size <= 0 { page.Items = matching return page, nil } start := pageStart(max(query.Page, 1), query.Size, len(matching)) end := min(start+query.Size, len(matching)) page.Items = matching[start:end] return page, nil } func siteMatches(candidate domain.Site, needle string) bool { for _, field := range []string{candidate.Name, candidate.Key, candidate.BaseURL} { if strings.Contains(foldForSearch(field), needle) { return true } } return false } func (s *SiteRepository) load(db *gorm.DB, model entity.Site) (domain.Site, error) { total, err := s.articleCount(db, model.ID) if err != nil { return domain.Site{}, err } return mappers.SiteToDomain(model, total), nil } func (s *SiteRepository) Get(id uint32) (domain.Site, error) { var model entity.Site if err := s.db.First(&model, id).Error; err != nil { return domain.Site{}, mapNotFoundErr(err, domain.ErrSiteNotFound) } return s.load(s.db, model) } func (s *SiteRepository) GetByKey(key string) (domain.Site, error) { var model entity.Site if err := s.db.Where("site_key = ?", key).First(&model).Error; err != nil { return domain.Site{}, mapNotFoundErr(err, domain.ErrSiteNotFound) } return s.load(s.db, model) } func (s *SiteRepository) GetDefault() (domain.Site, error) { var model entity.Site if err := s.db.Where("is_default = ?", true).Order("id ASC").First(&model).Error; err != nil { return domain.Site{}, mapNotFoundErr(err, domain.ErrSiteNotFound) } return s.load(s.db, model) } func (s *SiteRepository) Create(site domain.Site) (domain.Site, error) { model := mappers.SiteToModel(site) model.ID = 0 err := s.db.Transaction(func(tx *gorm.DB) error { if model.IsDefault { if err := lockAllSites(tx); err != nil { return err } if err := unflagDefaults(tx, 0); err != nil { return err } } return tx.Create(&model).Error }) if err != nil { return domain.Site{}, s.explainKeyFailure(model.Key, err) } return mappers.SiteToDomain(model, 0), nil } func (s *SiteRepository) explainKeyFailure(key string, cause error) error { if _, err := s.GetByKey(key); err == nil { return domain.ErrSiteKeyTaken } return cause } func lockAllSites(tx *gorm.DB) error { var locked []entity.Site return tx.Clauses(clause.Locking{Strength: "UPDATE"}).Order("id ASC").Find(&locked).Error } func lockSite(tx *gorm.DB, id uint32) (entity.Site, error) { var locked entity.Site if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&locked, id).Error; err != nil { return entity.Site{}, mapNotFoundErr(err, domain.ErrSiteNotFound) } return locked, nil } func unflagDefaults(tx *gorm.DB, exceptID uint32) error { return tx.Model(&entity.Site{}).Where("is_default = ? AND id <> ?", true, exceptID).UpdateColumn("is_default", false).Error } func (s *SiteRepository) Update(site domain.Site) (domain.Site, error) { changes := map[string]any{ "name": site.Name, "base_url": site.BaseURL, "default_social_image_id": site.DefaultSocialImageID, "default_robots": site.DefaultRobots, "locale": site.Locale, "site_name": site.SiteName, "title_template": site.TitleTemplate, "default_description": site.DefaultDescription, } if _, err := s.Get(site.ID); err != nil { return domain.Site{}, err } if err := s.db.Model(&entity.Site{}).Where("id = ?", site.ID).Updates(changes).Error; err != nil { return domain.Site{}, err } return s.Get(site.ID) } func (s *SiteRepository) SetDefault(id uint32) (domain.Site, error) { err := s.db.Transaction(func(tx *gorm.DB) error { if err := lockAllSites(tx); err != nil { return err } if _, err := lockSite(tx, id); err != nil { return err } if err := unflagDefaults(tx, id); err != nil { return err } return tx.Model(&entity.Site{}).Where("id = ?", id).UpdateColumn("is_default", true).Error }) if err != nil { return domain.Site{}, err } return s.Get(id) } func (s *SiteRepository) DeleteMovingArticles(id uint32, reassignTo *uint32) error { return s.db.Transaction(func(tx *gorm.DB) error { target, err := lockSite(tx, id) if err != nil { return err } if target.IsDefault { return domain.ErrDefaultSite } remaining, err := s.articleCount(tx, id) if err != nil { return err } if remaining > 0 && reassignTo == nil { return domain.ErrSiteHasArticles } if reassignTo != nil { if err := moveArticles(tx, id, *reassignTo); err != nil { return err } } return deleteOrNotFound(tx.Delete(&entity.Site{}, id), domain.ErrSiteNotFound) }) } func moveArticles(tx *gorm.DB, from uint32, to uint32) error { if _, err := lockSite(tx, to); err != nil { return err } return tx.Unscoped().Model(&entity.Article{}).Where("site_id = ?", from).UpdateColumn("site_id", to).Error } func (s *SiteRepository) ClearImage(imageID uint32) ([]uint32, error) { ids := []uint32{} err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&entity.Site{}).Where("default_social_image_id = ?", imageID).Order("id ASC").Pluck("id", &ids).Error; err != nil { return err } if len(ids) == 0 { return nil } return tx.Model(&entity.Site{}).Where("id IN ?", ids).UpdateColumn("default_social_image_id", nil).Error }) if err != nil { return nil, err } return ids, nil } func (s *SiteRepository) AssignImage(siteIDs []uint32, imageID uint32) error { if len(siteIDs) == 0 { return nil } return s.db.Model(&entity.Site{}).Where("id IN ?", siteIDs).UpdateColumn("default_social_image_id", imageID).Error } func (s *SiteRepository) RepointImage(fromImageIDs []uint32, toImageID uint32) (int, error) { if len(fromImageIDs) == 0 { return 0, nil } result := s.db.Model(&entity.Site{}).Where("default_social_image_id IN ?", fromImageIDs).UpdateColumn("default_social_image_id", toImageID) if result.Error != nil { return 0, result.Error } return int(result.RowsAffected), nil } var _ domain.Repository = &SiteRepository{}