settings

package
v0.1.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: AGPL-3.0 Imports: 27 Imported by: 0

Documentation

Overview

Package settings 提供统一的声明式设置:分组 Schema、值存储、校验与读取。

每个模块经 app.SettingsProvider 声明自己的分组;本模块在 Start 时汇总全部分组并编译 Schema, 其他模块经 From(app) 取得 Service 读取有效值。装配顺序上本模块应排在最前。

Index

Constants

View Source
const (
	SlugUnicode = "unicode"
	SlugPinyin  = "pinyin"
)

slug 生成策略。

View Source
const GroupSite = "site"

GroupSite 是站点基本信息分组。

View Source
const Icon = "settings"

Icon 是设置在侧边栏与页内的图标名(见 console/src/lib/icons.ts)。

View Source
const Name = "settings"

Name 是模块名,也是迁移版本表后缀与 App.Provide 的键。

View Source
const Path = "/" + Name

Path 是设置页在 Console 内的路径,也是本模块接口在 Console 平面下的前缀。

页面与接口共用这一段是刻意的:`/settings/<分组>` 既是接口地址,也是 「展开那一块」的页面地址,两者写成两个常量迟早会漂开。

Variables

View Source
var (
	// ErrUnknownGroup 表示分组未注册。
	ErrUnknownGroup = errors.New("设置分组不存在")
)

错误哨兵。

Functions

func Merge

func Merge(defaults, overrides map[string]any) map[string]any

Merge 返回 defaults 被 overrides 按顶层键覆盖后的新对象,与设置分组的合并语义一致。

func RejectSecrets

func RejectSecrets(scope string, f *form.Form) error

RejectSecrets 拒绝在作用域设置(主题、插件)里声明的口令字段。

那些设置的存法与站点设置不同,加解密没有接进去;放任它们声明 Secret 会得到一个「界面上写着不回传、库里其实躺着明文」的假象,比直接不支持更糟。

Types

type Group

type Group struct {
	app.SettingGroup
	// contains filtered or unexported fields
}

Group 是已编译的设置分组。

func (*Group) DefaultValues

func (g *Group) DefaultValues() map[string]any

Defaults 返回缺省值的副本。

func (*Group) Mask

func (g *Group) Mask(values map[string]any) (masked map[string]any, set []string)

Mask 抹掉口令字段的值,返回抹过的副本与其中确有值的字段名。

「确有值」这件事必须单独告诉界面:抹完之后每个口令字段都是空串, 而站长需要知道库里到底存没存过——否则他无从判断要不要重填。

func (*Group) SchemaDoc

func (g *Group) SchemaDoc() map[string]any

SchemaDoc 返回解析后的 Schema 文档,供接口输出。

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler 提供设置的 Console 与 Public 接口。

func NewHandler

func NewHandler(service *Service) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

func (h *Handler) Register(console, public huma.API)

Register 挂载接口:Console 全部需要 settings:manage,Public 只暴露公开字段。

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module 是设置模块。

func New

func New() *Module

New 构造模块。

func (*Module) Migrations

func (m *Module) Migrations() fs.FS

Migrations 实现 app.Migrator。

func (*Module) Name

func (m *Module) Name() string

Name 实现 app.Module。

func (*Module) Navigation

func (m *Module) Navigation() app.Navigation

Navigation 实现 app.NavigationProvider:侧边栏一个入口,分组做命令面板的直达项。

侧边栏只有「站点设置」一条:所有分组现在铺在同一页上(见 console 的 settings 页), 再给每个分组画一条侧栏菜单,等于让站长先选一个分组才能开始配置, 而他要改的两项很可能分属两组。

分组仍各自下发一条 Hidden 的菜单项。Hidden 的项不进侧边栏,但仍进命令面板与标题映射 (见 app.NavItem.Hidden):Ctrl+K 里搜「邮件」应当能直接跳到邮件那一块, 而这正是把入口收成一个之后唯一会丢掉的东西。

用推导而不是逐个手写:设置分组可能来自模块,也可能来自主题或插件, 手写一份清单就必然有漏。此前 comment 分组在后端存在而在侧边栏没有入口, 正是因为那份清单是手写的。

func (*Module) Permissions

func (m *Module) Permissions() []app.Permission

Permissions 实现 app.PermissionProvider。

func (*Module) Register

func (m *Module) Register(a *app.App) error

Register 实现 app.Module:装配存储与服务,并登记为共享服务供后续模块取用。

func (*Module) Routes

func (m *Module) Routes(r app.Router)

Routes 实现 app.RouteProvider。

func (*Module) Settings

func (m *Module) Settings() []app.SettingGroup

Settings 实现 app.SettingsProvider:本模块自带 site 分组。

func (*Module) Start

func (m *Module) Start(context.Context) error

Start 实现 app.Starter:此时全部模块已注册,汇总它们声明的分组并编译。

主密钥在编译之后才加载:只有真有分组声明了口令字段时才需要碰密钥文件。 反过来的话,每个只装了本站的目录里都会多出一个 secret.key——多一个「这文件要不要 跟着备份」的问题,而多数站点根本没有任何需要加密的口令。

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service 是设置的读写入口:持有已注册分组、校验器与缓存。

func From

func From(a *app.App) *Service

From 取回设置服务;settings 模块未装配(或尚未注册)时返回 nil。

调用方须能在 nil 时退化(如 slug 策略退回 unicode),以便单独测试其他模块。

func NewService

func NewService(store ValueStore) *Service

NewService 构造 Service;store 可为 nil(仅做 Schema 校验的场景)。

func (*Service) Effective

func (s *Service) Effective(ctx context.Context, name string) (map[string]any, error)

Effective 返回分组的有效值(缺省值被已保存值按顶层键覆盖)的副本。

func (*Service) Get

func (s *Service) Get(ctx context.Context, name string, out any) error

Get 把分组的有效值解码到 out(通常是带 json 标签的结构体)。

func (*Service) Group

func (s *Service) Group(name string) (*Group, bool)

Group 按名称取分组。

func (*Service) Groups

func (s *Service) Groups() []*Group

Groups 返回全部分组,按 Order、Name 排序。

func (*Service) HasSecrets

func (s *Service) HasSecrets() bool

HasSecrets 报告已注册分组里有没有口令字段。

模块据此决定要不要去加载主密钥:没有口令字段就不碰密钥文件, 免得每个只装了本站的目录里都躺着一个没人知道该不该备份的 secret.key。

func (*Service) Public

func (s *Service) Public(ctx context.Context) (map[string]map[string]any, error)

Public 返回各分组中标记为公开的字段,供前台与登录页使用。

func (*Service) RegisterGroups

func (s *Service) RegisterGroups(groups []app.SettingGroup) error

RegisterGroups 编译并登记分组。任一分组的 Schema 或 Defaults 不合法即整体失败: 那是模块作者的错误,必须在启动时暴露。

func (*Service) SetKeyring

func (s *Service) SetKeyring(k *secret.Keyring)

SetKeyring 注入主密钥,供口令字段加解密。须在开始服务之前调用。

func (*Service) SetLogger

func (s *Service) SetLogger(l *slog.Logger)

SetLogger 注入日志器;口令解不开时靠它留一条线索。

func (*Service) Slug

func (s *Service) Slug(ctx context.Context, text string) string

Slug 按站点设置的策略生成 slug:unicode 保留中文,pinyin 转为拼音。

分组尚未注册或读取失败时退回 unicode 策略,不让一次设置读取失败拖垮内容创建。

func (*Service) Update

func (s *Service) Update(ctx context.Context, name string, values map[string]any) error

Update 用给定值覆盖分组:先与缺省值合并成完整对象,校验通过后整体保存。

**不返回有效值**:有效值里含解密后的口令,返回它等于把明文口令递给每一个调用方, 而接口要的是抹掉口令的视图(见 Group.Mask)。少一个返回值就少一条泄漏路径。

type Site

type Site struct {
	Title        string `json:"title"`
	Subtitle     string `json:"subtitle"`
	Description  string `json:"description"`
	URL          string `json:"url"`
	Language     string `json:"language"`
	Timezone     string `json:"timezone"`
	LogoURL      string `json:"logoUrl"`
	FaviconURL   string `json:"faviconUrl"`
	SlugStrategy string `json:"slugStrategy"`
	PageSize     int    `json:"pageSize"`
}

Site 是 site 分组的有效值。

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store 提供设置值的持久化操作。

func NewStore

func NewStore(db *bun.DB) *Store

NewStore 构造 Store。

func (*Store) Load

func (s *Store) Load(ctx context.Context, name string) (map[string]any, error)

Load 读取分组的已保存值;从未保存过时返回空对象。

func (*Store) Save

func (s *Store) Save(ctx context.Context, name string, values map[string]any) error

Save 覆盖式写入分组的值。

type ValidationError

type ValidationError struct {
	Details []httpx.ErrorDetail
}

ValidationError 表示值未通过 Schema 或 Go 侧校验,Details 逐条定位到字段。

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error 实现 error。

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator 是一份编译好的表单校验器,供设置分组之外的场景复用。

主题设置的声明由主题包的 settings.yaml 给出、随主题安装而变, 不能走 RegisterGroups 那条「启动期固定登记」的路径;但校验语义必须与站点设置完全一致, 否则主题作者要面对两套规则。故把编译与校验单独暴露出来。

校验分两步,缺一不可:JSON Schema 判「值是否合法」,Form.Missing 判 「此刻该显示的必填项是否都填了」。后者只能由 Form 回答——条件依赖的可见性是 它的知识,Schema 里只留了一句 x-show-if 的提示。

func NewValidator

func NewValidator(name string, f *form.Form) (*Validator, error)

NewValidator 编译一份表单声明。

name 只用于错误信息与内部资源定位,不要求全局唯一。

func (*Validator) DefaultValues

func (v *Validator) DefaultValues() map[string]any

DefaultValues 返回缺省值的副本。

func (*Validator) Doc

func (v *Validator) Doc() map[string]any

Doc 返回解析后的 Schema 文档,供接口原样输出给表单引擎。

func (*Validator) Missing

func (v *Validator) Missing(values map[string]any) []string

Missing 返回此刻应当显示却没填的必填字段,供调用方在保存前判定。

func (*Validator) Validate

func (v *Validator) Validate(values map[string]any) error

Validate 校验一个值对象,失败时返回 *ValidationError(明细逐条定位到字段)。

type ValueStore

type ValueStore interface {
	// Load 读取分组的已保存值;从未保存过时返回空对象。
	Load(ctx context.Context, name string) (map[string]any, error)
	// Save 覆盖式写入分组的值。
	Save(ctx context.Context, name string, values map[string]any) error
}

ValueStore 是设置值的持久化接口。

抽成接口不是为了将来换数据库——它的实现只有 Store 一个——而是为了让 「口令进库是密文」这类断言能在纯内存的测试里验:真去连一个 PG 才能验加密, 代价是这条最要紧的性质在最常见的开发环境里跑不起来。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL