Files
pure-note/internal/store/settings.go
T
wangairnan 5457aeaa33 feat: 新增发布日期自选、站点 Logo、回收站清空等十项改进
- 编辑页标题改 filled 变体;标签改 tags 选择器(可勾选既有/输入新建)
- 笔记发布日期可自选(schema v2 新增 published_at 并回填),前台展示发布日期
- 登录页移除「请输入管理密码以继续」「连续失败将被暂时锁定」文案
- 站点设置每页条数收敛为 10/20/30/50 选择器,后端白名单同源校验
- 站点设置两卡片宽屏左右/窄屏上下;修改密码按钮改常规大小
- 后台各页顶栏固定高度,切换页面不再抖动
- 站点 Logo:settings 新增 site_logo(站内路径),gc/孤儿清单豁免 Logo 图片,
  博客 header 站点名前展示,设置页支持上传/更换/清除
- 回收站右上角一键清空(DELETE /api/admin/trash,含确认弹窗)
- pn init 写入 Markdown 语法示例文档(slug welcome,公开)与程序生成的示例图片
2026-09-09 11:10:30 +08:00

112 lines
3.1 KiB
Go

package store
import (
"database/sql"
"errors"
"strconv"
"strings"
)
// Settings 键白名单(§7.1 SettingsDTO 同源;admin_password_hash 永不进入 API 响应)。
const (
KeyAdminPasswordHash = "admin_password_hash"
KeySiteTitle = "site_title"
KeySiteDesc = "site_desc"
KeyPageSize = "page_size"
KeyBeianNo = "beian_no"
KeySiteLogo = "site_logo"
)
// GetSetting 读取单个设置。
func (s *Store) GetSetting(key string) (string, bool, error) {
var v string
err := s.db.QueryRow(`SELECT value FROM settings WHERE key=?`, key).Scan(&v)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
}
return v, err == nil, err
}
// SetSetting 写入单个设置(INSERT OR REPLACE)。
func (s *Store) SetSetting(key, value string) error {
_, err := s.db.Exec(`INSERT INTO settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value`, key, value)
return err
}
// SiteSettings 对外的站点设置白名单视图。
type SiteSettings struct {
SiteTitle string `json:"site_title"`
SiteDesc string `json:"site_desc"`
PageSize int `json:"page_size"`
BeianNo string `json:"beian_no"`
SiteLogo string `json:"site_logo"`
}
// Defaults,未初始化时兜底。
const (
DefaultSiteTitle = "Pure Note"
DefaultSiteDesc = ""
DefaultPageSize = 10
DefaultBeianNo = ""
)
// ValidPageSize 每页条数可选项(前端选择器同源:10/20/30/50)。
func ValidPageSize(n int) bool {
switch n {
case 10, 20, 30, 50:
return true
}
return false
}
// GetSiteSettings 读取站点设置(白名单键,带默认值)。
func (s *Store) GetSiteSettings() (*SiteSettings, error) {
ss := &SiteSettings{SiteTitle: DefaultSiteTitle, SiteDesc: DefaultSiteDesc, PageSize: DefaultPageSize, BeianNo: DefaultBeianNo}
if v, ok, err := s.GetSetting(KeySiteTitle); err != nil {
return nil, err
} else if ok && v != "" {
ss.SiteTitle = v
}
if v, ok, err := s.GetSetting(KeySiteDesc); err != nil {
return nil, err
} else if ok {
ss.SiteDesc = v
}
if v, ok, err := s.GetSetting(KeyPageSize); err != nil {
return nil, err
} else if ok {
// strconv.Atoi 全文解析:拒绝 "10abc" 类部分解析的脏值(评审 round2 P2-13);
// 脏值/越界值回退默认,而非落入任意 [1,100]
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && ValidPageSize(n) {
ss.PageSize = n
}
}
if v, ok, err := s.GetSetting(KeyBeianNo); err != nil {
return nil, err
} else if ok {
ss.BeianNo = strings.TrimSpace(v)
}
if v, ok, err := s.GetSetting(KeySiteLogo); err != nil {
return nil, err
} else if ok {
ss.SiteLogo = strings.TrimSpace(v)
}
return ss, nil
}
// logoImageID 从 site_logo 设置解析受 GC 保护的图片 id(无 logo 或非本站图片路径返回 0)。
func (s *Store) logoImageID() int64 {
v, ok, err := s.GetSetting(KeySiteLogo)
if err != nil || !ok || v == "" {
return 0
}
if m := imageRefRe.FindStringSubmatch(v); m != nil {
var id int64
if _, err := fmtSscanInt(m[1], &id); err == nil && id > 0 {
return id
}
}
return 0
}