feat(admin): 完善站点设置、内容回收站与后台菜单分组
新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,9 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
@@ -15,14 +17,40 @@ const (
|
||||
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}$`)
|
||||
|
||||
// SettingService 站点级键值设置(主题色等外观配置)
|
||||
// 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
|
||||
}
|
||||
@@ -31,59 +59,195 @@ func NewSettingService(db *gorm.DB) *SettingService {
|
||||
return &SettingService{db: db}
|
||||
}
|
||||
|
||||
// AccentColor 返回配置的主题色(小写 #rrggbb);未配置时返回空串,表示走前端默认色
|
||||
func (s *SettingService) AccentColor() (string, error) {
|
||||
func (s *SettingService) getValue(key string) (string, bool, error) {
|
||||
var st model.SiteSetting
|
||||
err := s.db.Where("key = ?", SettingKeyAccent).First(&st).Error
|
||||
err := s.db.Where("key = ?", key).First(&st).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
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(st.Value), nil
|
||||
return strings.ToLower(v), nil
|
||||
}
|
||||
|
||||
// SetAccent 校验并保存主题色;hex 传空串表示恢复默认(删除该键)
|
||||
func (s *SettingService) SetAccent(hex string) error {
|
||||
hex = strings.TrimSpace(strings.ToLower(hex))
|
||||
if hex == "" {
|
||||
return s.db.Where("key = ?", SettingKeyAccent).Delete(&model.SiteSetting{}).Error
|
||||
return s.deleteKey(SettingKeyAccent)
|
||||
}
|
||||
if !accentHexRe.MatchString(hex) {
|
||||
return ErrInvalidAccent
|
||||
}
|
||||
// UPSERT:键冲突时更新值与时间戳
|
||||
st := model.SiteSetting{Key: SettingKeyAccent, Value: hex}
|
||||
return s.db.Save(&st).Error
|
||||
return s.putValue(SettingKeyAccent, hex)
|
||||
}
|
||||
|
||||
// TrustReviewedPublish 是否对「已有过审内容」的用户免审。
|
||||
// KV 缺行 / 空值视为开启(默认开);仅显式写入 false/0/off/no 为关闭。
|
||||
// 不用 GORM bool default:true,避免零值省略陷阱。
|
||||
func (s *SettingService) TrustReviewedPublish() (bool, error) {
|
||||
var st model.SiteSetting
|
||||
err := s.db.Where("key = ?", SettingKeyTrustReviewedPublish).First(&st).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return true, nil
|
||||
}
|
||||
v, found, err := s.getValue(SettingKeyTrustReviewedPublish)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
v := strings.TrimSpace(strings.ToLower(st.Value))
|
||||
switch v {
|
||||
case "false", "0", "off", "no":
|
||||
return false, nil
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
return parseBoolDefaultTrue(v, found), nil
|
||||
}
|
||||
|
||||
// SetTrustReviewedPublish 持久化免审开关;开启时删除键以保持「缺行=默认开」语义
|
||||
func (s *SettingService) SetTrustReviewedPublish(on bool) error {
|
||||
if on {
|
||||
return s.db.Where("key = ?", SettingKeyTrustReviewedPublish).Delete(&model.SiteSetting{}).Error
|
||||
}
|
||||
st := model.SiteSetting{Key: SettingKeyTrustReviewedPublish, Value: "false"}
|
||||
return s.db.Save(&st).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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user