199 lines
5.0 KiB
Go
199 lines
5.0 KiB
Go
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
|
||
}
|