Files
jiang13-bbs/backend/service/sponsor.go

199 lines
5.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}