Help us shape the future of Go! Take the 2026 Go Developer Survey

settings

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 26, 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