文档目录
glen
· 预计阅读 4 分钟

第 3 章 SubApp 契约:子应用必须实现的 10 个方法

本文导航
第 3 章 SubApp 契约:子应用必须实现的 10 个方法

第 3 章 SubApp 契约:子应用必须实现的 10 个方法

Owl 把每个业务系统都当作一个 SubApp。只要实现 owl.SubApp 接口,框架就能装配它。这一章逐项解释接口、注入规则和可选能力。

接口原文

type SubApp interface {
	Name() string
	RegisterRouters()
	ServiceProviders() []foundation.ServiceProvider
	Binds() []any
	Bootstrap()
	RegisterMenus() []*router.Menu
	RegisterCommands() []*cobra.Command
	RegisterMigrate() []any
	BeforeMigrate(db *gorm.DB) error
	AfterMigrate(db *gorm.DB) error
}

每个方法的职责与时机

方法 做什么 调用时机
Name() 子应用标识,如 admin、order;用于路由权限前缀和菜单归属 装配开始
RegisterRouters() 注册本子应用全部 HTTP 路由 仅 WebShell,菜单收集之前
ServiceProviders() 返回依赖的 Provider,如 db、redis、router 装配阶段
Binds() 返回 Repository / Service / Handle 的构造函数 装配阶段,最早
Bootstrap() Seeder、事件监听注册等 路由与菜单之后、迁移之前
RegisterMenus() 返回菜单树根节点 仅 WebShell
RegisterCommands() 返回 Cobra 命令 装配阶段,Console 模式挂到 rootCmd
RegisterMigrate() 返回需要 AutoMigrate 的 model 所有 Bootstrap 之后
BeforeMigrate() 迁移前钩子,如 DropColumn 每次启动,串行
AfterMigrate() 迁移后钩子,如种子数据、索引修补 每次启动,并行

必须有的 app 字段

框架用反射注入应用实例:

type SubAppOrder struct {
	app foundation.Application
}

injectAppInstance 通过 FieldByName("app") 找字段,找不到或类型不匹配会在启动时 panic。自定义 Provider 也一样,必须保留 app foundation.Application 字段。

这个字段让子应用可以在 RegisterRouters()、Bootstrap() 里用 i.app.Invoke(...) 取任意已注册的依赖。

最小骨架

package order

import (
	"bit-labs.cn/owl"
	"bit-labs.cn/owl/contract/foundation"
	"bit-labs.cn/owl/provider/router"
	"github.com/spf13/cobra"
	"gorm.io/gorm"
)

var _ owl.SubApp = (*SubAppOrder)(nil)

type SubAppOrder struct {
	app foundation.Application
}

func (i *SubAppOrder) Name() string { return "order" }

func (i *SubAppOrder) RegisterRouters() {
	// route.InitApi(i.app, i.Name())
}

func (i *SubAppOrder) ServiceProviders() []foundation.ServiceProvider {
	return []foundation.ServiceProvider{
		&router.RouterServiceProvider{},
	}
}

func (i *SubAppOrder) Binds() []any { return nil }

func (i *SubAppOrder) Bootstrap() {}

func (i *SubAppOrder) RegisterMenus() []*router.Menu { return nil }

func (i *SubAppOrder) RegisterCommands() []*cobra.Command { return nil }

func (i *SubAppOrder) RegisterMigrate() []any { return nil }

func (i *SubAppOrder) BeforeMigrate(db *gorm.DB) error { return nil }

func (i *SubAppOrder) AfterMigrate(db *gorm.DB) error { return nil }

Binds 怎么写

Binds() 里放构造函数,不放实例:

func (i *SubAppOrder) Binds() []any {
	return []any{
		v1.NewOrderHandle,          // HTTP 入口
		service.NewOrderService,    // 业务编排
		repository.NewOrderRepository, // 数据访问
	}
}

推荐先 Repository、再 Service、再 Handle。dig 会根据构造函数参数自动推导依赖,顺序本身不影响解析,但这样写和人脑的依赖方向一致,排查时更直观。

漏注册某个构造函数,启动时会报类似:

missing type: *repository.OrderRepository

这时候把对应的 NewXxx 加进 Binds() 即可。

按子系统类型选 Provider

场景 推荐 Provider
最小 HTTP 服务 router.RouterServiceProvider
需要数据库 再加 db.DBServiceProvider
需要分布式锁 / 缓存 再加 redis.RedisServiceProvider
需要 RBAC 权限 再加 permission.GuardProvider,并接入登录中间件
需要文件上传 再加 storage.StorageServiceProvider
纯命令行工具 可以不注册 router,使用 ConsoleShell

可选能力:登记系统配置

不改 SubApp 接口。如果一个子系统想让超管在后台「系统配置」页修改参数,额外实现 setting.Registrar:

import "bit-labs.cn/owl/provider/setting"

func (i *SubAppOrder) RegisterConfigs() setting.Catalog {
	return setting.Catalog{
		Title: "订单",
		Groups: []setting.Group{
			{
				Key:   "trade",
				Title: "交易",
				Items: []setting.Item{
					{
						Key:     "closeUnpaidMinutes",
						Title:   "未支付关闭时间",
						Hint:    "单位:分钟",
						Type:    setting.TypeInt,
						Default: "30",
						Min:     1,
						Max:     1440,
					},
				},
			},
		},
	}
}

框架在注册路由之前调用 RegisterConfigs(),Web 和命令行都会收集。几条约束:

  • 配置项的键在同一子系统内唯一;
  • 分组最多两级,同一组不能既放配置项又放子分组;
  • 类型支持 string、bool、int、image;
  • image 的值是 data:image/...;base64,...,解码后不能超过 200KB。

没实现该接口的子系统不会出现在配置页。

迁移相关方法

func (i *SubAppOrder) RegisterMigrate() []any {
	return []any{
		&model.Order{},
	}
}
  • RegisterMigrate() 返回 model 实例,不是表名;
  • 不在列表里的 model 永远不会自动迁移;
  • Bootstrap() 里不要再手动 AutoMigrate,统一交给框架;
  • 种子数据放 AfterMigrate,需要延迟执行可以自行 go,框架不会替你 fire-and-forget。

自检

  • [ ] 结构体有 app foundation.Application 字段;
  • [ ] 10 个方法全部实现(返回 nil 也算实现);
  • [ ] Binds() 里放的是构造函数;
  • [ ] 新增的 model 已加入 RegisterMigrate();
  • [ ] 若实现了 setting.Registrar,配置键在子系统内唯一。

评论

加载评论中…

上一篇 第 2 章 应用生命周期:从 NewApp 到 WebShell 下一篇 第 4 章 Service Provider 与依赖注入

BitLabs © 2026 · 论坛讨论 · 博客长文 · 文档手册