- 编辑页标题改 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,公开)与程序生成的示例图片
112 lines
3.1 KiB
Go
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
|
|
}
|