Files
jiang13-bbs/backend/service/setting.go
freefire dd7bd010b8 feat(admin): 完善站点设置、内容回收站与后台菜单分组
新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 05:56:27 +08:00

254 lines
6.9 KiB
Go
Raw 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 (
"errors"
"regexp"
"strconv"
"strings"
"unicode/utf8"
"github.com/freefire/jiang13-bbs/model"
"gorm.io/gorm"
)
// 站点设置键
const (
// SettingKeyAccent 站点主题色(浅色基准,#rrggbb);为空表示使用内置默认色
SettingKeyAccent = "theme_accent"
// SettingKeyTrustReviewedPublish 已过审用户后续发帖/评论免审;缺行视为开启(默认开)
SettingKeyTrustReviewedPublish = "trust_reviewed_publish"
SettingKeySiteName = "site_name"
SettingKeySiteDescription = "site_description"
SettingKeyAllowRegister = "allow_register"
SettingKeyPostCooldownHours = "post_cooldown_hours"
)
const (
DefaultSiteName = "姜十三论坛"
DefaultSiteDescription = "姜十三论坛 - 技术分享与讨论社区"
DefaultCooldownHours = 24
MaxSiteNameRunes = 32
MaxSiteDescRunes = 160
MaxCooldownHours = 168
)
// ErrInvalidAccent 主题色格式非法
var ErrInvalidAccent = errors.New("主题色格式不正确,应为 #RRGGBB")
// ErrInvalidSiteSetting 站点设置字段校验失败
var ErrInvalidSiteSetting = errors.New("站点设置参数无效")
var accentHexRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
// PublicSiteSettings 对前台公开的站点配置(SEO / 注册入口 / 发帖冷静期提示)
type PublicSiteSettings struct {
Accent string `json:"accent"`
TrustReviewedPublish bool `json:"trust_reviewed_publish"`
SiteName string `json:"site_name"`
SiteDescription string `json:"site_description"`
AllowRegister bool `json:"allow_register"`
PostCooldownHours int `json:"post_cooldown_hours"`
}
// SettingService 站点级键值设置
type SettingService struct {
db *gorm.DB
}
func NewSettingService(db *gorm.DB) *SettingService {
return &SettingService{db: db}
}
func (s *SettingService) getValue(key string) (string, bool, error) {
var st model.SiteSetting
err := s.db.Where("key = ?", key).First(&st).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", false, nil
}
if err != nil {
return "", false, err
}
return st.Value, true, nil
}
func (s *SettingService) putValue(key, value string) error {
st := model.SiteSetting{Key: key, Value: value}
return s.db.Save(&st).Error
}
func (s *SettingService) deleteKey(key string) error {
return s.db.Where("key = ?", key).Delete(&model.SiteSetting{}).Error
}
// 缺行或空值视为 on;仅显式 false/0/off/no 为关。不用 GORM bool default:true,避免零值省略。
func parseBoolDefaultTrue(v string, found bool) bool {
if !found {
return true
}
switch strings.TrimSpace(strings.ToLower(v)) {
case "false", "0", "off", "no":
return false
default:
return true
}
}
func (s *SettingService) setBoolDefaultTrue(key string, on bool) error {
if on {
return s.deleteKey(key)
}
return s.putValue(key, "false")
}
// Public 返回解析后的公开站点设置(缺省已填默认值)
func (s *SettingService) Public() (PublicSiteSettings, error) {
out := PublicSiteSettings{
SiteName: DefaultSiteName,
SiteDescription: DefaultSiteDescription,
TrustReviewedPublish: true,
AllowRegister: true,
PostCooldownHours: DefaultCooldownHours,
}
accent, err := s.AccentColor()
if err != nil {
return out, err
}
out.Accent = accent
trust, err := s.TrustReviewedPublish()
if err != nil {
return out, err
}
out.TrustReviewedPublish = trust
name, found, err := s.getValue(SettingKeySiteName)
if err != nil {
return out, err
}
if found {
name = strings.TrimSpace(name)
if name != "" {
out.SiteName = name
}
}
desc, found, err := s.getValue(SettingKeySiteDescription)
if err != nil {
return out, err
}
if found {
out.SiteDescription = strings.TrimSpace(desc)
}
reg, found, err := s.getValue(SettingKeyAllowRegister)
if err != nil {
return out, err
}
out.AllowRegister = parseBoolDefaultTrue(reg, found)
hours, err := s.PostCooldownHours()
if err != nil {
return out, err
}
out.PostCooldownHours = hours
return out, nil
}
// AccentColor 返回配置的主题色(小写 #rrggbb);未配置时返回空串,表示走前端默认色
func (s *SettingService) AccentColor() (string, error) {
v, found, err := s.getValue(SettingKeyAccent)
if err != nil || !found {
return "", err
}
return strings.ToLower(v), nil
}
// SetAccent 校验并保存主题色;hex 传空串表示恢复默认(删除该键)
func (s *SettingService) SetAccent(hex string) error {
hex = strings.TrimSpace(strings.ToLower(hex))
if hex == "" {
return s.deleteKey(SettingKeyAccent)
}
if !accentHexRe.MatchString(hex) {
return ErrInvalidAccent
}
return s.putValue(SettingKeyAccent, hex)
}
// TrustReviewedPublish 是否对「已有过审内容」的用户免审。
func (s *SettingService) TrustReviewedPublish() (bool, error) {
v, found, err := s.getValue(SettingKeyTrustReviewedPublish)
if err != nil {
return true, err
}
return parseBoolDefaultTrue(v, found), nil
}
// SetTrustReviewedPublish 持久化免审开关;开启时删除键以保持「缺行=默认开」语义
func (s *SettingService) SetTrustReviewedPublish(on bool) error {
return s.setBoolDefaultTrue(SettingKeyTrustReviewedPublish, on)
}
func (s *SettingService) SetSiteName(name string) error {
name = strings.TrimSpace(name)
if name == "" || name == DefaultSiteName {
return s.deleteKey(SettingKeySiteName)
}
if utf8.RuneCountInString(name) > MaxSiteNameRunes {
return ErrInvalidSiteSetting
}
return s.putValue(SettingKeySiteName, name)
}
func (s *SettingService) SetSiteDescription(desc string) error {
desc = strings.TrimSpace(desc)
if utf8.RuneCountInString(desc) > MaxSiteDescRunes {
return ErrInvalidSiteSetting
}
if desc == "" || desc == DefaultSiteDescription {
return s.deleteKey(SettingKeySiteDescription)
}
return s.putValue(SettingKeySiteDescription, desc)
}
func (s *SettingService) AllowRegister() (bool, error) {
v, found, err := s.getValue(SettingKeyAllowRegister)
if err != nil {
return true, err
}
return parseBoolDefaultTrue(v, found), nil
}
func (s *SettingService) SetAllowRegister(on bool) error {
return s.setBoolDefaultTrue(SettingKeyAllowRegister, on)
}
// PostCooldownHours 新用户发帖冷静期(小时)。缺行=24;0 表示关闭。
func (s *SettingService) PostCooldownHours() (int, error) {
v, found, err := s.getValue(SettingKeyPostCooldownHours)
if err != nil {
return DefaultCooldownHours, err
}
if !found {
return DefaultCooldownHours, nil
}
n, convErr := strconv.Atoi(strings.TrimSpace(v))
if convErr != nil || n < 0 {
return DefaultCooldownHours, nil
}
if n > MaxCooldownHours {
return MaxCooldownHours, nil
}
return n, nil
}
func (s *SettingService) SetPostCooldownHours(hours int) error {
if hours < 0 || hours > MaxCooldownHours {
return ErrInvalidSiteSetting
}
if hours == DefaultCooldownHours {
return s.deleteKey(SettingKeyPostCooldownHours)
}
return s.putValue(SettingKeyPostCooldownHours, strconv.Itoa(hours))
}