package distribution import ( "context" "errors" "strings" "time" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) // 分站公告中心:站长在分销后台自助维护本站公告。 // // 站点隔离口径:所有写操作与列表查询都强制 distribution_site_id = siteID, // 且先通过 ensureSiteAccess 校验当前管理员对该站点的访问权,避免跨站读写。 // 前台展示范围由站点 announcement_mode 决定(见 announcement 模块 ResolvePublicSiteScope)。 // normalizeAnnouncementMode 校验公告中心来源模式,空值按默认 inherit 处理。 func normalizeAnnouncementMode(value string) (string, error) { switch strings.TrimSpace(value) { case "", model.AnnouncementModeInherit: return model.AnnouncementModeInherit, nil case model.AnnouncementModeOwnOnly: return model.AnnouncementModeOwnOnly, nil default: return "", ErrInvalidAnnouncementMode } } // ListSiteAnnouncements 分页查询本站公告(含草稿/归档,供站长管理)。 func (r *Repository) ListSiteAnnouncements(ctx context.Context, adminID, siteID uint64, status string, page, pageSize int) (*SiteAnnouncementPage, error) { if siteID == 0 { return nil, ErrSiteNotFound } if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil { return nil, err } if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } base := r.db.WithContext(ctx).Model(&model.Announcement{}).Where("distribution_site_id = ?", siteID) if s := strings.TrimSpace(status); s != "" { base = base.Where("status = ?", s) } var total int64 if err := base.Session(&gorm.Session{}).Count(&total).Error; err != nil { return nil, err } var rows []model.Announcement if err := base.Session(&gorm.Session{}). Order("is_pinned DESC, priority DESC, id DESC"). Offset((page - 1) * pageSize). Limit(pageSize). Find(&rows).Error; err != nil { return nil, err } items := make([]SiteAnnouncementDTO, 0, len(rows)) for _, row := range rows { items = append(items, toSiteAnnouncementDTO(row)) } return &SiteAnnouncementPage{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } // CreateSiteAnnouncement 在指定站点下新建公告(草稿)。 func (r *Repository) CreateSiteAnnouncement(ctx context.Context, adminID, siteID uint64, req CreateSiteAnnouncementRequest) (*SiteAnnouncementDTO, error) { if siteID == 0 { return nil, ErrSiteNotFound } if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil { return nil, err } createdBy := adminID row := model.Announcement{ DistributionSiteID: siteID, Title: strings.TrimSpace(req.Title), Content: req.Content, Category: strings.TrimSpace(req.Category), Priority: req.Priority, IsPinned: req.IsPinned, IsImportant: req.IsImportant, IsPopup: req.IsPopup, Status: "draft", CreatedBy: &createdBy, } if row.Category == "" { row.Category = "notice" } if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { return nil, err } dto := toSiteAnnouncementDTO(row) return &dto, nil } // UpdateSiteAnnouncement 更新本站公告内容。 func (r *Repository) UpdateSiteAnnouncement(ctx context.Context, adminID, siteID, id uint64, req UpdateSiteAnnouncementRequest) (*SiteAnnouncementDTO, error) { if siteID == 0 { return nil, ErrSiteNotFound } if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil { return nil, err } row, err := r.findSiteAnnouncement(ctx, siteID, id) if err != nil { return nil, err } updates := map[string]any{ "priority": req.Priority, "is_pinned": req.IsPinned, "is_important": req.IsImportant, "is_popup": req.IsPopup, } if title := strings.TrimSpace(req.Title); title != "" { updates["title"] = title } if req.Content != "" { updates["content"] = req.Content } if category := strings.TrimSpace(req.Category); category != "" { updates["category"] = category } if err := r.db.WithContext(ctx).Model(&model.Announcement{}).Where("id = ?", row.ID).Updates(updates).Error; err != nil { return nil, err } updated, err := r.findSiteAnnouncement(ctx, siteID, id) if err != nil { return nil, err } dto := toSiteAnnouncementDTO(*updated) return &dto, nil } // UpdateSiteAnnouncementStatus 站点公告上下架:publish=发布,archive=归档,unarchive=取消归档。 func (r *Repository) UpdateSiteAnnouncementStatus(ctx context.Context, adminID, siteID, id uint64, action string) (*SiteAnnouncementDTO, error) { if siteID == 0 { return nil, ErrSiteNotFound } if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil { return nil, err } row, err := r.findSiteAnnouncement(ctx, siteID, id) if err != nil { return nil, err } now := time.Now() updates := map[string]any{} switch strings.TrimSpace(action) { case "publish": updates["status"] = "published" updates["published_at"] = now case "archive": updates["status"] = "archived" case "unarchive": updates["status"] = "published" updates["published_at"] = gorm.Expr("COALESCE(published_at, ?)", now) default: return nil, ErrInvalidListingStatus } if err := r.db.WithContext(ctx).Model(&model.Announcement{}).Where("id = ?", row.ID).Updates(updates).Error; err != nil { return nil, err } updated, err := r.findSiteAnnouncement(ctx, siteID, id) if err != nil { return nil, err } dto := toSiteAnnouncementDTO(*updated) return &dto, nil } // DeleteSiteAnnouncement 删除本站公告。 func (r *Repository) DeleteSiteAnnouncement(ctx context.Context, adminID, siteID, id uint64) error { if siteID == 0 { return ErrSiteNotFound } if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil { return err } row, err := r.findSiteAnnouncement(ctx, siteID, id) if err != nil { return err } return r.db.WithContext(ctx).Where("id = ?", row.ID).Delete(&model.Announcement{}).Error } // findSiteAnnouncement 按站点归属读取公告,跨站 ID 一律视为不存在。 func (r *Repository) findSiteAnnouncement(ctx context.Context, siteID, id uint64) (*model.Announcement, error) { var row model.Announcement err := r.db.WithContext(ctx).Where("id = ? AND distribution_site_id = ?", id, siteID).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAnnouncementNotFound } return nil, err } return &row, nil } func toSiteAnnouncementDTO(a model.Announcement) SiteAnnouncementDTO { return SiteAnnouncementDTO{ 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, } } // normalizeAnnouncementModeOrInherit 读侧容错:非法/历史空值一律按默认继承口径返回。 func normalizeAnnouncementModeOrInherit(value string) string { mode, err := normalizeAnnouncementMode(value) if err != nil { return model.AnnouncementModeInherit } return mode }