package service import ( "encoding/json" "errors" "fmt" "net/url" "strings" "time" "unicode/utf8" "github.com/freefire/jiang13-bbs/model" "gorm.io/gorm" ) const ( SettingKeySponsorsConfig = "sponsors_config" SponsorDefaultTitle = "赞助商" SponsorMaxItems = 24 SponsorMaxNameRunes = 32 SponsorMaxHoverRunes = 120 ) // SponsorItem 侧栏赞助商条目 type SponsorItem struct { ID string `json:"id"` Name string `json:"name"` LogoURL string `json:"logo_url"` LinkURL string `json:"link_url"` HoverText string `json:"hover_text"` Enabled bool `json:"enabled"` SortOrder int `json:"sort_order"` } // SponsorsConfig 赞助商板块配置 type SponsorsConfig struct { Enabled bool `json:"enabled"` PanelTitle string `json:"panel_title"` Items []SponsorItem `json:"items"` } func DefaultSponsorsConfig() SponsorsConfig { return SponsorsConfig{ Enabled: true, PanelTitle: SponsorDefaultTitle, Items: []SponsorItem{}, } } func (s *AdService) loadSponsorsConfig() (SponsorsConfig, error) { var row model.SiteSetting err := s.db.Where("key = ?", SettingKeySponsorsConfig).First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return DefaultSponsorsConfig(), nil } if err != nil { return SponsorsConfig{}, err } cfg := DefaultSponsorsConfig() if strings.TrimSpace(row.Value) == "" { return cfg, nil } if err := json.Unmarshal([]byte(row.Value), &cfg); err != nil { return DefaultSponsorsConfig(), nil } if strings.TrimSpace(cfg.PanelTitle) == "" { cfg.PanelTitle = SponsorDefaultTitle } return cfg, nil } func (s *AdService) GetSponsorsConfig() (SponsorsConfig, error) { return s.loadSponsorsConfig() } // PublicSponsors 侧栏展示用(仅启用且有 logo 的条目) func (s *AdService) PublicSponsors() (SponsorsConfig, []SponsorItem, error) { cfg, err := s.loadSponsorsConfig() if err != nil { return cfg, nil, err } if !cfg.Enabled { return cfg, []SponsorItem{}, nil } out := make([]SponsorItem, 0, len(cfg.Items)) for _, it := range cfg.Items { if !it.Enabled { continue } if strings.TrimSpace(it.LogoURL) == "" { continue } out = append(out, it) } return cfg, out, nil } func (s *AdService) SaveSponsorsConfig(in SponsorsConfig) (SponsorsConfig, error) { out, err := normalizeSponsorsConfig(in) if err != nil { return SponsorsConfig{}, err } b, err := json.Marshal(out) if err != nil { return SponsorsConfig{}, err } row := model.SiteSetting{Key: SettingKeySponsorsConfig, Value: string(b), UpdatedAt: time.Now()} if err := s.db.Save(&row).Error; err != nil { return SponsorsConfig{}, err } return out, nil } func normalizeSponsorsConfig(in SponsorsConfig) (SponsorsConfig, error) { out := DefaultSponsorsConfig() out.Enabled = in.Enabled title := strings.TrimSpace(in.PanelTitle) if title == "" { title = SponsorDefaultTitle } if utf8.RuneCountInString(title) > 16 { return SponsorsConfig{}, errors.New("侧栏标题最多 16 字") } out.PanelTitle = title if len(in.Items) > SponsorMaxItems { return SponsorsConfig{}, fmt.Errorf("赞助商最多 %d 个", SponsorMaxItems) } seen := map[string]struct{}{} items := make([]SponsorItem, 0, len(in.Items)) for i, raw := range in.Items { id := strings.TrimSpace(raw.ID) if id == "" { id = fmt.Sprintf("sp_%d", i+1) } if _, ok := seen[id]; ok { return SponsorsConfig{}, errors.New("赞助商 id 重复") } seen[id] = struct{}{} name := strings.TrimSpace(raw.Name) if name == "" { return SponsorsConfig{}, errors.New("请填写赞助商名称") } if utf8.RuneCountInString(name) > SponsorMaxNameRunes { return SponsorsConfig{}, fmt.Errorf("赞助商名称最多 %d 字", SponsorMaxNameRunes) } logo := strings.TrimSpace(raw.LogoURL) if logo == "" { return SponsorsConfig{}, errors.New("请上传或填写赞助商 logo") } if !validSponsorLogoURL(logo) { return SponsorsConfig{}, errors.New("logo 须为 https 或本站 /uploads/ 图片") } link := strings.TrimSpace(raw.LinkURL) if link != "" { u, err := url.Parse(link) if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" { return SponsorsConfig{}, errors.New("赞助商链接须为 http(s) 地址") } link = u.String() } hover := strings.TrimSpace(raw.HoverText) if utf8.RuneCountInString(hover) > SponsorMaxHoverRunes { return SponsorsConfig{}, fmt.Errorf("赞助说明最多 %d 字", SponsorMaxHoverRunes) } items = append(items, SponsorItem{ ID: id, Name: name, LogoURL: logo, LinkURL: link, HoverText: hover, Enabled: raw.Enabled, SortOrder: i, }) } out.Items = items return out, nil } func validSponsorLogoURL(u string) bool { if adImageUploadRe.MatchString(u) { return true } if strings.HasPrefix(strings.ToLower(u), "https://") && adImageHTTPSRe.MatchString(u) { return true } // 允许常见本站 uploads 路径(赞助 logo) low := strings.ToLower(u) if strings.HasPrefix(low, "/uploads/") && len(u) <= 500 { return true } return false }