feat: add ads/sponsor/captcha modules and polish home & chat UI
This commit is contained in:
198
backend/service/sponsor.go
Normal file
198
backend/service/sponsor.go
Normal file
@@ -0,0 +1,198 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user