package announcement import ( "context" "errors" "strings" "time" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) type Repository struct { db *gorm.DB } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } // ResolvePublicSiteScope 解析前台请求应看到的公告归属站点集合。 // // - 主站(无站点编码):只看主站公告 [0] // - 分站 inherit:主站公告 + 本站公告 [0, siteID] // - 分站 own_only:仅本站公告 [siteID] // - 站点编码无效/已停用:回落主站,与前台站点探测失败的兜底一致 func (r *Repository) ResolvePublicSiteScope(ctx context.Context, siteCode string) ([]uint64, error) { code := strings.TrimSpace(siteCode) if code == "" { return []uint64{0}, nil } var site model.DistributionSite err := r.db.WithContext(ctx).Select("id", "announcement_mode"). Where("site_code = ? AND status = ?", code, "active").First(&site).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return []uint64{0}, nil } return nil, err } if strings.TrimSpace(site.AnnouncementMode) == model.AnnouncementModeOwnOnly { return []uint64{site.ID}, nil } return []uint64{0, site.ID}, nil } // List 获取公告列表(前台用户) func (r *Repository) List(ctx context.Context, query AnnouncementListQuery) (*PaginatedResult, error) { var total int64 tx := r.db.WithContext(ctx).Model(&model.Announcement{}).Where("status = ?", "published") if len(query.AllowedSiteIDs) > 0 { tx = tx.Where("distribution_site_id IN ?", query.AllowedSiteIDs) } if query.Category != "" { tx = tx.Where("category = ?", query.Category) } if err := tx.Count(&total).Error; err != nil { return nil, err } offset := (query.Page - 1) * query.PageSize var rows []model.Announcement if err := tx.Order("is_pinned DESC, priority DESC, published_at DESC"). Offset(offset). Limit(query.PageSize). Find(&rows).Error; err != nil { return nil, err } items := make([]AnnouncementDTO, 0, len(rows)) for _, row := range rows { items = append(items, toAnnouncementDTO(row)) } return &PaginatedResult{ Items: items, Total: total, Page: query.Page, PageSize: query.PageSize, }, nil } // PublicPopup 取当前站点范围内优先级最高的弹窗公告;没有可弹公告时返回 nil, nil。 // // 只取 published 且 is_popup = 1 的公告,按 priority DESC(其次发布时间/ID)排序取第一条, // 因此「多条同时生效只弹优先级最高的一条」由这里保证,前台无需再筛选。 func (r *Repository) PublicPopup(ctx context.Context, allowedSiteIDs []uint64) (*AnnouncementDTO, error) { query := r.db.WithContext(ctx). Where("status = ? AND is_popup = ?", "published", true) if len(allowedSiteIDs) > 0 { query = query.Where("distribution_site_id IN ?", allowedSiteIDs) } var announcement model.Announcement if err := query. Order("priority DESC, published_at DESC, id DESC"). First(&announcement).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } dto := toAnnouncementDTO(announcement) return &dto, nil } // GetByID 获取公告详情(限前台可见的站点范围,避免跨站按 ID 读取) func (r *Repository) GetByID(ctx context.Context, id uint64, allowedSiteIDs []uint64) (*AnnouncementDTO, error) { var announcement model.Announcement db := r.db.WithContext(ctx) query := db.Where("id = ? AND status = ?", id, "published") if len(allowedSiteIDs) > 0 { query = query.Where("distribution_site_id IN ?", allowedSiteIDs) } if err := query.First(&announcement).Error; err != nil { return nil, err } // 增加查看次数 db.Model(&model.Announcement{}).Where("id = ?", id).UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)) dto := toAnnouncementDTO(announcement) return &dto, nil } // AdminList 管理员获取公告列表(可按站点筛选,含草稿/归档) func (r *Repository) AdminList(ctx context.Context, query AnnouncementListQuery) (*PaginatedResult, error) { base := r.adminListBase(ctx, query) var total int64 if err := base.Session(&gorm.Session{}).Count(&total).Error; err != nil { return nil, err } offset := (query.Page - 1) * query.PageSize var items []AnnouncementDTO if err := base.Session(&gorm.Session{}). Order("a.is_pinned DESC, a.priority DESC, a.id DESC"). Offset(offset). Limit(query.PageSize). Scan(&items).Error; err != nil { return nil, err } if items == nil { items = []AnnouncementDTO{} } return &PaginatedResult{ Items: items, Total: total, Page: query.Page, PageSize: query.PageSize, }, nil } // adminListBase 管理端公告列表基础查询:带站点名,支持状态/分类/站点筛选。 func (r *Repository) adminListBase(ctx context.Context, query AnnouncementListQuery) *gorm.DB { base := r.db.WithContext(ctx).Table("announcements AS a"). Select("a.id, a.distribution_site_id, COALESCE(s.site_name, '') AS distribution_site_name, a.title, a.content, a.category, a.priority, a.is_pinned, a.is_important, a.is_popup, a.view_count, a.status, a.published_at, a.created_at, a.updated_at"). Joins("LEFT JOIN distribution_sites AS s ON s.id = a.distribution_site_id") if query.Status != "" { base = base.Where("a.status = ?", query.Status) } if query.Category != "" { base = base.Where("a.category = ?", query.Category) } if query.AdminSiteID != nil { base = base.Where("a.distribution_site_id = ?", *query.AdminSiteID) } return base } // AdminGetByID 管理员获取公告详情(带站点名) func (r *Repository) AdminGetByID(ctx context.Context, id uint64) (*AnnouncementDTO, error) { var dto AnnouncementDTO if err := r.adminListBase(ctx, AnnouncementListQuery{}). Where("a.id = ?", id). Scan(&dto).Error; err != nil { return nil, err } if dto.ID == 0 { return nil, gorm.ErrRecordNotFound } return &dto, nil } // Create 创建公告 func (r *Repository) Create(ctx context.Context, req CreateAnnouncementRequest, createdBy uint64) (*AnnouncementDTO, error) { announcement := model.Announcement{ DistributionSiteID: req.DistributionSiteID, Title: req.Title, Content: req.Content, Category: req.Category, Priority: req.Priority, IsPinned: req.IsPinned, IsImportant: req.IsImportant, IsPopup: req.IsPopup, Status: "draft", CreatedBy: &createdBy, } if err := r.db.WithContext(ctx).Create(&announcement).Error; err != nil { return nil, err } dto := toAnnouncementDTO(announcement) return &dto, nil } // Update 更新公告 func (r *Repository) Update(ctx context.Context, id uint64, req UpdateAnnouncementRequest) (*AnnouncementDTO, error) { updates := make(map[string]interface{}) if req.Title != "" { updates["title"] = req.Title } if req.Content != "" { updates["content"] = req.Content } if req.Category != "" { updates["category"] = req.Category } updates["priority"] = req.Priority updates["is_pinned"] = req.IsPinned updates["is_important"] = req.IsImportant updates["is_popup"] = req.IsPopup if err := r.db.WithContext(ctx).Model(&model.Announcement{}).Where("id = ?", id).Updates(updates).Error; err != nil { return nil, err } return r.AdminGetByID(ctx, id) } // Publish 发布公告 func (r *Repository) Publish(ctx context.Context, id uint64) error { now := time.Now() return r.db.WithContext(ctx).Model(&model.Announcement{}). Where("id = ?", id). Updates(map[string]interface{}{ "status": "published", "published_at": now, }).Error } // Archive 归档公告 func (r *Repository) Archive(ctx context.Context, id uint64) error { return r.db.WithContext(ctx).Model(&model.Announcement{}). Where("id = ?", id). Update("status", "archived").Error } // Unarchive 取消归档公告,恢复为已发布状态 func (r *Repository) Unarchive(ctx context.Context, id uint64) error { now := time.Now() return r.db.WithContext(ctx).Model(&model.Announcement{}). Where("id = ?", id). Updates(map[string]interface{}{ "status": "published", "published_at": gorm.Expr("COALESCE(published_at, ?)", now), }).Error } // Delete 删除公告 func (r *Repository) Delete(ctx context.Context, id uint64) error { return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Announcement{}).Error } func toAnnouncementDTO(a model.Announcement) AnnouncementDTO { return AnnouncementDTO{ ID: a.ID, DistributionSiteID: a.DistributionSiteID, Title: a.Title, Content: a.Content, Category: a.Category, Priority: a.Priority, IsPinned: a.IsPinned, IsImportant: a.IsImportant, IsPopup: a.IsPopup, ViewCount: a.ViewCount, Status: a.Status, PublishedAt: a.PublishedAt, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, } }