menu

package
v0.1.3 Latest Latest
Warning

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

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

Documentation

Overview

Package menu 提供导航菜单。

菜单分两层:menus 是菜单本身(主题按 slug 引用),menu_items 是它的条目树。 条目分两类——自定义链接手填地址,站内条目(文章 / 页面 / 分类 / 标签)指向记录 ID, 地址在读取时解析,记录改名或改 slug 后菜单自动跟随。

Index

Constants

View Source
const Name = "menu"

Name 是模块名,也是迁移版本表的后缀(goose_db_version_menu)。

Variables

View Source
var (
	// ErrNotFound 表示菜单不存在。
	ErrNotFound = errors.New("菜单不存在")
	// ErrSlugTaken 表示菜单 slug 已被占用。
	ErrSlugTaken = errors.New("菜单标识已被占用")
	// ErrInvalid 表示字段违反了数据库约束。
	ErrInvalid = errors.New("字段不合法")
	// ErrCycle 表示条目树的父子关系成环。
	ErrCycle = errors.New("父条目不能是自身或其后代")
	// ErrTargetNotFound 表示条目指向的站内记录不存在。
	ErrTargetNotFound = errors.New("条目指向的内容不存在")
)

错误哨兵。处理器据此映射为 400 / 404 / 409。

Functions

func CountItems

func CountItems(nodes []*ItemNode) int

CountItems 统计树中的条目数。

Types

type Handler

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

Handler 提供菜单的 Console 与 Public 接口。

func NewHandler

func NewHandler(store *Store, resolver *resolver, log logger) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

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

Register 挂载接口。

读写都要求 menus:manage:菜单是站点级配置,不存在「自己的菜单」这种所有权概念。

type Item

type Item struct {
	bun.BaseModel `bun:"table:menu_items,alias:mi"`

	ID       int64    `bun:"id,pk,autoincrement" json:"id"`
	MenuID   int64    `bun:"menu_id,notnull"     json:"menuId"`
	ParentID *int64   `bun:"parent_id"           json:"parentId" doc:"父条目 ID,一级菜单为 null"`
	Position int      `bun:"position"            json:"position" doc:"同级排序,越小越靠前"`
	Label    string   `bun:"label,notnull"       json:"label"`
	Type     ItemType `bun:"type,notnull"        json:"type" enum:"custom,post,page,category,tag"`
	// TargetID 是站内记录的 ID,仅 post / page / category / tag 类型有值。
	TargetID *int64 `bun:"target_id" json:"targetId" doc:"站内记录 ID,自定义链接为 null"`
	// URL 对自定义链接是手填值;对站内条目是由记录解析出的地址。
	URL    string `bun:"url"     json:"url"`
	Target string `bun:"target"  json:"target" enum:",_blank" doc:"空串为当前窗口,_blank 为新窗口"`
	Rel    string `bun:"rel"     json:"rel" doc:"额外的 rel 属性,如 nofollow"`
	// Visible 为假时前台不渲染该条目,但后台仍保留。
	Visible   bool      `bun:"visible"            json:"visible"`
	CreatedAt time.Time `bun:"created_at,nullzero" json:"createdAt"`
	UpdatedAt time.Time `bun:"updated_at,nullzero" json:"updatedAt"`
}

Item 是菜单里的一个条目。

type ItemNode

type ItemNode struct {
	Item
	Children []*ItemNode `json:"children" doc:"子条目,按 position 排序"`
}

ItemNode 是菜单树的节点:条目本身加上按 position 排序的子条目。

func BuildTree

func BuildTree(items []Item) []*ItemNode

BuildTree 把平铺的条目组装成树。

只保留能追溯到根节点的条目:父项被删时子项已由外键级联删除, 但脏数据仍可能存在,这里再兜一层。

func VisibleTree

func VisibleTree(nodes []*ItemNode) []*ItemNode

VisibleTree 返回只含可见条目的树,供前台渲染。

父项隐藏时整支不渲染:让一个隐藏的父项还留着可见的子项, 会在前台产生一串没有归属的链接。

type ItemType

type ItemType string

ItemType 是菜单条目的类型,决定 URL 从哪来。

const (
	// TypeCustom 是手填地址的条目。
	TypeCustom ItemType = "custom"
	// 以下四种指向站内已有记录,URL 由服务端解析,记录改名或改 slug 后菜单自动跟随。
	TypePost     ItemType = "post"
	TypePage     ItemType = "page"
	TypeCategory ItemType = "category"
	TypeTag      ItemType = "tag"
)

条目类型。

type Menu struct {
	bun.BaseModel `bun:"table:menus,alias:mn"`

	ID          int64     `bun:"id,pk,autoincrement" json:"id"`
	Name        string    `bun:"name,notnull"        json:"name"`
	Slug        string    `bun:"slug,notnull"        json:"slug" doc:"主题按此名取菜单,全局唯一"`
	Description string    `bun:"description"         json:"description"`
	CreatedAt   time.Time `bun:"created_at,nullzero" json:"createdAt"`
	UpdatedAt   time.Time `bun:"updated_at,nullzero" json:"updatedAt"`

	// Items 仅在按 slug 取整棵树时填充,不是数据库列。
	Items []*ItemNode `bun:"-" json:"items,omitempty"`
	// ItemCount 是条目数,供后台列表显示。
	ItemCount int `bun:"-" json:"itemCount"`
}

Menu 是一组菜单。

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。

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。

type Store

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

Store 提供菜单与条目的持久化操作。

func NewStore

func NewStore(db *bun.DB) *Store

NewStore 构造 Store。

func (*Store) Create

func (s *Store) Create(ctx context.Context, m *Menu) error

Create 创建菜单;成功后回填 ID 与时间戳。

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, id int64) error

Delete 删除菜单;其条目由外键级联删除。

func (*Store) Get

func (s *Store) Get(ctx context.Context, id int64) (*Menu, error)

Get 按 ID 查菜单。

func (*Store) GetBySlug

func (s *Store) GetBySlug(ctx context.Context, slug string) (*Menu, error)

GetBySlug 按 slug 查菜单。

func (*Store) Items

func (s *Store) Items(ctx context.Context, menuID int64) ([]Item, error)

Items 返回某菜单的全部条目,按父项与 position 排序。

func (*Store) List

func (s *Store) List(ctx context.Context) ([]Menu, error)

List 返回全部菜单,按名称排序,并带上条目数。

func (*Store) ReplaceItems

func (s *Store) ReplaceItems(ctx context.Context, menuID int64, items []Item, parents []int) error

ReplaceItems 用给定条目整体替换菜单的条目树。

整体替换而不是逐条 diff:菜单是「一次性编辑、一次性保存」的表单, 逐条同步要处理移动、重排、删除与重建的交叉情形,出错的概率远大于收益。 代价是条目 ID 每次保存都会变——主题按 label 与 url 渲染,不依赖 ID。

items 须为深度优先序(父项一定排在其子项之前),parents[i] 给出第 i 项在 items 中的父项下标,-1 表示一级条目。父项的行号只有在插入后才能拿到, 所以按下标而不是按客户端给的 ID 建立父子关系。

事务开头先对菜单父行加行锁(SELECT ... FOR UPDATE),把同一菜单的整树替换串行化: 两个并发事务若都先 DELETE 再各自 INSERT,最终表里会同时留下两棵树 (DELETE 看不见对方尚未提交的插入),而两个请求都返回成功。

func (*Store) Update

func (s *Store) Update(ctx context.Context, m *Menu) error

Update 更新菜单的基本信息。

Jump to

Keyboard shortcuts

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