第 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,配置键在子系统内唯一。
评论
加载评论中…