Documentation
¶
Overview ¶
Package menu 提供导航菜单。
菜单分两层:menus 是菜单本身(主题按 slug 引用),menu_items 是它的条目树。 条目分两类——自定义链接手填地址,站内条目(文章 / 页面 / 分类 / 标签)指向记录 ID, 地址在读取时解析,记录改名或改 slug 后菜单自动跟随。
Index ¶
- Constants
- Variables
- func CountItems(nodes []*ItemNode) int
- type Handler
- type Item
- type ItemNode
- type ItemType
- type Menu
- type Module
- type Store
- func (s *Store) Create(ctx context.Context, m *Menu) error
- func (s *Store) Delete(ctx context.Context, id int64) error
- func (s *Store) Get(ctx context.Context, id int64) (*Menu, error)
- func (s *Store) GetBySlug(ctx context.Context, slug string) (*Menu, error)
- func (s *Store) Items(ctx context.Context, menuID int64) ([]Item, error)
- func (s *Store) List(ctx context.Context) ([]Menu, error)
- func (s *Store) ReplaceItems(ctx context.Context, menuID int64, items []Item, parents []int) error
- func (s *Store) Update(ctx context.Context, m *Menu) error
Constants ¶
const Name = "menu"
Name 是模块名,也是迁移版本表的后缀(goose_db_version_menu)。
Variables ¶
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 ¶
Types ¶
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler 提供菜单的 Console 与 Public 接口。
func NewHandler ¶
NewHandler 构造 Handler。
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 ¶
ItemNode 是菜单树的节点:条目本身加上按 position 排序的子条目。
type Menu ¶
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 (*Module) Navigation ¶
func (m *Module) Navigation() app.Navigation
Navigation 实现 app.NavigationProvider。
func (*Module) Permissions ¶
func (m *Module) Permissions() []app.Permission
Permissions 实现 app.PermissionProvider。
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store 提供菜单与条目的持久化操作。
func (*Store) ReplaceItems ¶
ReplaceItems 用给定条目整体替换菜单的条目树。
整体替换而不是逐条 diff:菜单是「一次性编辑、一次性保存」的表单, 逐条同步要处理移动、重排、删除与重建的交叉情形,出错的概率远大于收益。 代价是条目 ID 每次保存都会变——主题按 label 与 url 渲染,不依赖 ID。
items 须为深度优先序(父项一定排在其子项之前),parents[i] 给出第 i 项在 items 中的父项下标,-1 表示一级条目。父项的行号只有在插入后才能拿到, 所以按下标而不是按客户端给的 ID 建立父子关系。
事务开头先对菜单父行加行锁(SELECT ... FOR UPDATE),把同一菜单的整树替换串行化: 两个并发事务若都先 DELETE 再各自 INSERT,最终表里会同时留下两棵树 (DELETE 看不见对方尚未提交的插入),而两个请求都返回成功。