44 KiB
Wavelet Cordis 插件化架构实战开发指南与标准规范
- 文档类型: 下游开发者手册 / 架构实战指南 (Cookbook & Architecture Reference)
- 目标受众: 官方插件开发者、下游业务二开工程师、架构师
- 版本: v1.0.0 (2026-08-27)
目录
- 第一部分:下游项目实战开发指南与 22 个高频开发场景解答
- 场景 1:插件必须要实现哪些方法与契约?
- 场景 2:插件间如何进行单向服务调用?
- 场景 3:插件间存在双向/循环调用时如何解决(杜绝 import cycle)?
- 场景 4:如何开发并注册一个 HTTP API 接口?如何添加路由中间件?
- 场景 5:如何获取当前登录用户信息?
- 场景 6:如何开发并注册一个 Asynq 异步 Worker 任务?
- 场景 7:如何开发并注册一个 Cron 定时任务?
- 场景 8:数据库表结构如何声明?ORM 模型规范是什么?
- 场景 9:数据库如何做独立迁移?Goose SQL 怎么组织?
- 场景 10:如果有多个业务插件需要读写同一张表怎么办?
- 场景 11:如果跨插件操作多张表,如何确保事务一致性?
- 场景 12:如何发布和订阅领域事件 (EventBus)?
- 场景 13:如何向系统注册插件自定义配置(config.yaml 与管理台热加载设置)?
- 场景 14:如何使用多层缓存(RAM L1 + Redis L2 + PubSub 同步)?
- 场景 15:如何使用分布式锁 (DistLock) 防止并发超卖与重复消费?
- 场景 16:如何向管理后台动态注册监控数据与管理控制台?
- 场景 17:插件如何实现健康检查探针与就绪检查 (Health Check)?
- 场景 18:插件如何扩展其他插件的能力(如新增一种 OAuth 登录提供商 / 新增消息推送渠道)?
- 场景 19:插件如何编写单元测试与集成测试(Mock 上下文与依赖打桩)?
- 场景 20:以不同角色(api / worker / schedule / all)启动时,插件代码如何适配?
- 场景 21:当某个插件流量暴增需要独立拆分为微服务时,如何零成本平滑改造?
- 场景 22:插件如何安全处理文件上传与大文件摄取 (upload.Ingest)?
- 第二部分:整个项目的目录结构划分与包职责定义
- 第三部分:框架核心提供给插件调用的公用能力矩阵 (Context Capability Matrix)
- 第四部分:Cordis 插件分层开发规范与代码模板 (Plugin Layered Architecture & Code Templates)
第一部分:下游项目实战开发指南与 22 个高频开发场景解答
场景 1:插件必须要实现哪些方法与契约?
每个插件必须实现 core.Plugin 接口,仅需提供两个核心方法:Name() 与 Apply(ctx *core.Context)。
package myplugin
import "github.com/Rain-kl/Wavelet/core"
type Plugin struct{}
// 1. Name: 返回全局唯一的插件标识符(建议遵循命名空间规范,如 "biz.order")
func (p *Plugin) Name() string {
return "biz.order"
}
// 2. Apply: 核心装载入口,所有的路由注册、任务注册、服务提供与依赖消费均在此完成
func (p *Plugin) Apply(ctx *core.Context) error {
// 在此编写装载逻辑
return nil
}
场景 2:插件间如何进行单向服务调用?
规则:插件之间禁止直接相互 import 具体实现包。调用方仅面向 core/contracts 中的纯 Interface 编程,运行时通过 Context 解析。
// 1. 插件 A (提供者 plugins/user) 将服务注入 Context
func (p *UserPlugin) Apply(ctx *core.Context) error {
dbSvc, _ := core.Inject[contracts.DBService](ctx)
userSvc := NewUserServiceImpl(dbSvc)
core.Provide[contracts.UserService](ctx, userSvc)
return nil
}
// 2. 插件 B (消费者 plugins/order) 声明依赖并调用
func (p *OrderPlugin) Apply(ctx *core.Context) error {
return ctx.Using(func(userSvc contracts.UserService) {
// userSvc 已由容器自动注入就绪
v1 := ctx.Router().Group("/api/v1/orders")
v1.POST("", func(c *gin.Context) {
userInfo, err := userSvc.GetUserProfile(c.Request.Context(), "user_123")
// 处理订单逻辑...
})
})
}
场景 3:插件间存在双向/循环调用时如何解决(杜绝 import cycle)?
问题场景:auth 登录成功后需要查 user 资料;user 重置密码后需要调 auth 吊销 session。若两个 package 互相 import,Go 编译器会报 import cycle not allowed。
Cordis 解法:
- 接口均定义在
core/contracts,双方只依赖core/contracts。 - 运行时采用 延迟注入 (Lazy Resolution / Inject) 或 事件解耦 (EventBus):
// plugins/auth/service.go
func (s *AuthServiceImpl) OnLoginSuccess(c context.Context, uid string) {
// 延迟注入 UserService,不发生 package 级循环导入
userSvc, err := core.Inject[contracts.UserService](s.ctx)
if err == nil {
userSvc.UpdateLastLoginTime(c, uid)
}
}
更加推荐的方式是发射领域事件(见场景 12),由 user 插件自愿监听,彻底消除相互调用的硬依赖。
场景 4:如何开发并注册一个 HTTP API 接口?如何添加路由中间件?
插件通过 ctx.Router() 声明路由。微内核支持标准 Gin 路由组与中间件挂载:
func (p *OrderPlugin) Apply(ctx *core.Context) error {
// 获取全局或 auth 插件提供的中间件
authSvc, _ := core.Inject[contracts.AuthService](ctx)
// 创建带版本前缀和鉴权中间件的路由组
group := ctx.Router().Group("/api/v1/orders", authSvc.RequireAuthMiddleware())
// 注册 Handler
group.GET("", p.handleListOrders)
group.POST("", p.handleCreateOrder)
group.GET("/:id", p.handleGetOrderDetail)
return nil
}
场景 5:如何获取当前登录用户信息?
auth 插件会在上下文中注入当前用户 Session。业务 Handler 可直接调用统一 Helper:
func (p *OrderPlugin) handleCreateOrder(c *gin.Context) {
// 1. 从当前 Gin 请求上下文中提取认证用户信息
currentUser, ok := oauth.GetCurrentUser(c)
if !ok {
response.AbortUnauthorized(c, errs.ErrUnauthorized)
return
}
log.Printf("当前下单用户 ID: %s, 权限角色: %s", currentUser.ID, currentUser.Role)
// 2. 正常业务处理...
}
场景 6:如何开发并注册一个 Asynq 异步 Worker 任务?
func (p *OrderPlugin) Apply(ctx *core.Context) error {
// 1. 注册 Asynq 任务类型与消费处理器
ctx.Task().Register("order:cancel_timeout", p.handleTimeoutCancelTask)
return nil
}
// 2. 任务执行函数
func (p *OrderPlugin) handleTimeoutCancelTask(ctx context.Context, t *asynq.Task) error {
var payload OrderTimeoutPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return err
}
// 执行超时关单业务逻辑...
return nil
}
// 3. 业务中异步投递任务
func (p *OrderPlugin) EnqueueTimeoutCheck(ctx context.Context, orderID string) {
p.ctx.TaskClient().EnqueueContext(ctx, asynq.NewTask("order:cancel_timeout", payloadBytes), asynq.ProcessIn(15*time.Minute))
}
场景 7:如何开发并注册一个 Cron 定时任务?
func (p *ReportPlugin) Apply(ctx *core.Context) error {
// 每天凌晨 2 点执行日报汇总任务
ctx.Schedule().RegisterCron("0 2 * * *", "report:daily_summary", DailyReportPayload{Type: "all"})
return nil
}
场景 8:数据库表结构如何声明?ORM 模型规范是什么?
规范:
- 表名必须带有插件专有前缀(如
w_order_、w_auth_),避免跨插件表名冲突。 - 零值与数据库默认值严格对齐;禁止物理外键,显式建索引。
- 必须通过 GORM 结构体清晰声明
gorm:"..."标签与json:"..."。
package models
import "time"
type Order struct {
ID string `gorm:"column:id;primaryKey;size:64" json:"id"`
UserID string `gorm:"column:user_id;index;size:64;not null" json:"user_id"`
Amount int64 `gorm:"column:amount;not null" json:"amount"`
Status string `gorm:"column:status;size:32;index;not null;default:'pending'" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
}
func (Order) TableName() string {
return "w_orders"
}
场景 9:数据库如何做独立迁移?Goose SQL 怎么组织?
彻底告别集中大迁移目录。每个插件在内部目录建立 migrations/,并通过 //go:embed 打包注入:
// plugins/order/plugin.go
package order
import (
"embed"
"github.com/Rain-kl/Wavelet/core"
)
//go:embed migrations/*.sql
var orderMigrations embed.FS
func (p *Plugin) Apply(ctx *core.Context) error {
// 注册本插件的专属迁移(系统启动时自动按版本号执行)
ctx.Migrations().Register("order", orderMigrations)
return nil
}
SQL 迁移脚本规范 (plugins/order/migrations/00001_initial.sql):
每个插件只需维护一个 00001_initial.sql,包含该插件的全部建表语句与种子数据。
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS w_orders (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
amount BIGINT NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_w_orders_user_id ON w_orders(user_id);
-- 种子数据(使用 ON CONFLICT DO NOTHING 保证幂等)
INSERT INTO w_orders (id, user_id, amount, status, created_at, updated_at)
VALUES ('init_001', 'system', 0, 'completed', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS w_orders;
-- +goose StatementEnd
版本管理机制
所有插件共享一张 w_schema_versions 表,以 plugin_id 区分:
w_schema_versions (plugin_id, version_id, applied_at)
启动时,引擎遍历每个插件:
- 查询
w_schema_versions WHERE plugin_id = 'order'获取当前最大版本号 - 扫描插件
migrations/目录下的.sql文件 - 如果存在未应用的版本号 → 执行迁移
- 如果全部已应用 → 跳过
-- 查看全局迁移状态
SELECT * FROM w_schema_versions ORDER BY plugin_id, version_id;
场景 10:如果有多个业务插件需要读写同一张表怎么办?
黄金准则:表有且仅有一个所有者插件 (Single Owner Principle)。
- 严禁插件 B 直接通过 SQL 修改插件 A 拥有的核心表(如订单插件直接修改用户表)。
- 合法模式 1(服务调用):插件 A 提供
UserService.DeductBalance(uid, amount),插件 B 调用该接口。 - 合法模式 2(只读视图 / 共享查询 DTO):如果仅仅是高频联合查询(报表),插件 A 暴露只读查询接口,或通过数据库只读从库直接投影。
场景 11:如果跨插件操作多张表,如何确保事务一致性?
在插件化和微服务就绪体系下,跨插件的强分布式事务是反模式。
- 同插件内多表操作:直接使用本地数据库事务:
err := ctx.DB().Transaction(func(tx *gorm.DB) error { if err := tx.Create(&order).Error; err != nil { return err } if err := tx.Create(&orderItem).Error; err != nil { return err } return nil }) - 跨插件操作(如创建订单 + 扣减库存 + 发送通知):
- 采用 最终一致性 (Eventual Consistency / Saga 模式)。
- 本地事务成功后,发射
OrderCreatedEvent到 EventBus; - 库存插件监听到事件后扣减库存,若失败则发布补偿事件触发订单取消。
场景 12:如何发布和订阅领域事件 (EventBus)?
Wavelet 完整实现了 Cordis 架构的 4 种类型化事件分发语义,支持同步管道、并发聚合与异步广播:
| 分发方法 | 分发语义 | 返回值/错误处理 | 适用场景 |
|---|---|---|---|
ctx.Events().Emit(ctx, topic, payload) |
异步广播 (Broadcast) | 不阻塞主流程,静默恢复 handler panic | 状态变更广播、审计日志记录、跨插件解耦通知 |
ctx.Events().Waterfall(ctx, topic, initial) |
流式管道 (Waterfall) | 依次将前一个 handler 返回值传给下一个,遇错立即短路退出 | 参数过滤拦截链、内容审查、数据清洗与加工 |
ctx.Events().Parallel(ctx, topic, payload) |
并发聚合 (Parallel) | 并发启动 goroutine 执行所有 handler,用 errors.Join 聚合所有错误 |
并发外部系统推送、多渠道并行通知校验 |
ctx.Events().Serial(ctx, topic, payload) |
串行执行 (Serial) | 按注册顺序依次同步执行,遇到首个非 nil 错误立即短路中断 | 敏感操作前置拦截(如权限/风控准入校验) |
1. 异步广播 (Emit) 与订阅 (On)
// 1. 定义强类型事件结构
type OrderPaidEvent struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
PayAmount int64 `json:"pay_amount"`
}
// 2. 插件 A 发布事件(广播)
ctx.Events().Emit(ctx, "order:paid", OrderPaidEvent{OrderID: "ord_1", UserID: "u_1", PayAmount: 9900})
// 3. 插件 B 订阅事件
ctx.Events().On("order:paid", func(c context.Context, e OrderPaidEvent) error {
log.Printf("收到支付成功事件,开始为用户 %s 发放权益", e.UserID)
return nil
})
2. 流式管道变换 (Waterfall)
Handler 支持返回 (T, error) 或 T,后续 Handler 接收上一 Handler 的返回值:
// 插件注册拦截处理
ctx.Events().On("content:filter", func(c context.Context, text string) (string, error) {
return strings.ReplaceAll(text, "敏感词", "***"), nil
})
// 调用方通过 Waterfall 获得流式处理后的结果
cleaned, err := ctx.Events().Waterfall(ctx, "content:filter", "原始文本包含敏感词")
// cleaned == "原始文本包含***"
3. 串行准入拦截 (Serial)
// 风控插件注册校验
ctx.Events().On("order:pre_create", func(c context.Context, req CreateOrderRequest) error {
if isBlacklisted(req.UserID) {
return errors.New("用户处于风控黑名单,禁止下单")
}
return nil
})
// 订单插件执行准入链,遇到首个错误立即中断并返回
if err := ctx.Events().Serial(ctx, "order:pre_create", req); err != nil {
return err // 拦截创建
}
场景 13:如何向系统注册插件自定义配置(config.yaml 与管理台热加载设置)?
type OrderConfig struct {
MaxItemsPerOrder int `yaml:"max_items" json:"max_items"`
AutoCancelMins int `yaml:"auto_cancel_mins" json:"auto_cancel_mins"`
}
func (p *OrderPlugin) Apply(ctx *core.Context) error {
var cfg OrderConfig
// 1. 自动从 config.yaml 中的 plugins.order 节点绑定配置
ctx.Config().Bind("plugins.order", &cfg)
// 2. 注册为管理台可动态修改的系统参数
ctx.Settings().Register(core.SettingSchema{
Key: "order.auto_cancel_mins",
Default: 15,
Description: "未支付订单自动取消时间 (分钟)",
})
return nil
}
场景 14:如何使用多层缓存(RAM L1 + Redis L2 + PubSub 同步)?
框架提供三层穿透缓存能力,防止缓存击穿与雪崩:
func (s *OrderService) GetOrderWithCache(ctx context.Context, orderID string) (*Order, error) {
var order Order
err := s.ctx.Cache().GetOrSet(ctx, "order:"+orderID, &order, 10*time.Minute, func() (any, error) {
// Cache Miss 回源查 DB
var dbOrder Order
if err := s.db.WithContext(ctx).First(&dbOrder, "id = ?", orderID).Error; err != nil {
return nil, err
}
return &dbOrder, nil
})
return &order, err
}
// 当订单更新时,广播失效所有节点的 L1 内存缓存与 L2 Redis 缓存
func (s *OrderService) InvalidateCache(ctx context.Context, orderID string) {
s.ctx.Cache().Delete(ctx, "order:"+orderID)
}
场景 15:如何使用分布式锁 (DistLock) 防止并发超卖与重复消费?
func (s *OrderService) ProcessPayment(ctx context.Context, orderID string) error {
// 获取分布式锁,租期 5 秒
unlock, err := s.ctx.DistLock().Lock(ctx, "lock:order:pay:"+orderID, 5*time.Second)
if err != nil {
return fmt.Errorf("当前订单正在处理中,请勿重复提交")
}
defer unlock() // 确保释放
// 执行扣款操作...
return nil
}
场景 16:如何向管理后台动态注册监控数据与管理控制台?
插件可以向管理后台扩展点注入自己的仪表盘指标和诊断探针:
func (p *OrderPlugin) Apply(ctx *core.Context) error {
ctx.Admin().RegisterMetric("order_count_today", func(c context.Context) any {
var count int64
ctx.DB().Model(&models.Order{}).Where("created_at >= ?", todayStart()).Count(&count)
return count
})
return nil
}
场景 17:插件如何实现健康检查探针与就绪检查 (Health Check)?
func (p *PaymentPlugin) Apply(ctx *core.Context) error {
ctx.Health().RegisterProbe("payment_gateway", func(ctx context.Context) error {
// 测试第三方支付网关网络连通性
return pingPaymentGateway(ctx)
})
return nil
}
场景 18:插件如何扩展其他插件的能力(如新增一种 OAuth 登录提供商 / 新增消息推送渠道)?
采用 注册表扩展点模式 (Registry Pattern):
// 1. 下游编写微信登录插件 plugins/oauth_wechat
func (p *WeChatOAuthPlugin) Apply(ctx *core.Context) error {
return ctx.Using(func(authRegistry contracts.AuthRegistry) {
// 向核心 auth 插件注入微信 OAuth 实现
authRegistry.RegisterOAuthProvider("wechat", &WeChatProvider{...})
})
}
场景 19:插件如何编写单元测试与集成测试(Mock 上下文与依赖打桩)?
微内核提供轻量测试脚手架 coretest:
func TestOrderCreate(t *testing.T) {
// 1. 创建内存测试专用 Context
ctx := coretest.NewMockContext(t)
// 2. Mock 依赖的 UserService
mockUserSvc := &MockUserService{ReturnUser: &contracts.UserDTO{ID: "u_1", Balance: 1000}}
ctx.Provide[contracts.UserService](mockUserSvc)
// 3. 装载插件
plugin := &OrderPlugin{}
require.NoError(t, plugin.Apply(ctx))
// 4. 发起 HTTP 接口测试
w := ctx.PerformRequest("POST", "/api/v1/orders", `{"item_id":"item_1"}`)
assert.Equal(t, 200, w.Code)
}
场景 20:以不同角色(api / worker / schedule / all)启动时,插件代码如何适配?
开发者无需做任何特殊处理!
插件只需在一个 Apply 方法中把自己的路由、任务、调度全部注册进 Context。微内核调度器会根据运行命令自动按需激活对应的运行时驱动,不匹配的能力保持休眠。
场景 21:当某个插件流量暴增需要独立拆分为微服务时,如何零成本平滑改造?
// 1. 之前单体模式:在 main.go 中加载本地实现
app.Use(&auth.Plugin{}) // 进程内直接运行
// 2. 拆分为微服务后:只需将 main.go 替换为 gRPC 客户端代理插件!
app.Use(&auth_grpc_client.Plugin{RemoteAddr: "auth-service.prod:9000"})
// 3. 所有依赖 auth 的业务插件(如 order, user)业务代码 0 处修改!
场景 22:插件如何安全处理文件上传与大文件摄取 (upload.Ingest)?
严格规则:禁止插件自行直接写入对象存储底层 Bucket 或直连底层文件系统。统一走平台摄取服务:
func (p *OrderPlugin) handleUploadInvoice(c *gin.Context) {
fileHeader, _ := c.FormFile("file")
// 使用平台统一摄取引擎(自动计算哈希、防重传、生成签名 URL 与入库追踪)
ingestResult, err := upload.IngestFormFile(c.Request.Context(), fileHeader, upload.IngestPolicy{
AllowedTypes: []string{"image/png", "application/pdf"},
MaxSizeBytes: 10 * 1024 * 1024,
})
if err != nil {
response.AbortBadRequest(c, errs.ErrUploadFailed)
return
}
c.JSON(200, response.OK(gin.H{"file_url": ingestResult.URL}))
}
第二部分:整个项目的目录结构划分与包职责定义
Wavelet/
├── cmd/ # CLI 命令分发与装配入口
│ ├── root.go # Cobra 根命令
│ ├── server.go # 综合启动器(支持 api/worker/schedule/all profile)
│ └── migrate.go # 数据库独立迁移命令行工具
│
├── core/ # 【微内核引擎 (Zero Business Logic)】
│ ├── context.go # Context 上下文总线与 Fork 树
│ ├── container.go # 基于泛型的 IoC 服务注册与解析器
│ ├── events.go # 强类型领域事件总线 (EventBus)
│ ├── lifecycle.go # 启动/停止生命周期编排状态机
│ ├── contracts/ # 【跨插件标准服务契约 (纯 Interface)】
│ │ ├── auth.go # AuthService 契约
│ │ ├── user.go # UserService 契约
│ │ ├── cache.go # CacheService 契约
│ │ └── database.go # DBService 契约
│ └── extpoints/ # 扩展点定义 (Router, Task, Migration, Setting)
│
├── plugins/ # 【官方标准插件库 (完全高内聚闭包)】
│ ├── drivers/ # 运行时驱动插件
│ │ ├── driver_http/ # Gin Web HTTP 驱动
│ │ ├── driver_asynq_worker/ # Asynq Worker 并发消费驱动
│ │ └── driver_asynq_cron/ # Asynq Cron 调度器驱动
│ │
│ ├── infra/ # 基础设施服务插件
│ │ ├── database/ # GORM 多数据源与读写分离插件
│ │ ├── cache/ # RAM + Redis + PubSub 缓存插件
│ │ ├── logger/ # Zap + Otel 分布式链路追踪日志插件
│ │ └── storage/ # S3 / OSS / Local 对象存储插件
│ │
│ └── domain/ # 业务领域能力插件
│ ├── auth/ # OAuth / Session / Passkey 认证插件
│ ├── user/ # 用户资料 / 权限 / 角色插件
│ ├── message_gateway/ # Bot 网关 / 渠道推送插件
│ ├── risk_control/ # 访问控制 / IP 限流 / 安全风控插件
│ └── admin/ # 系统管理台与监控面板插件
│
└── downstream/ # 【下游二开项目模板与脚手架】
├── custom_plugins/ # 下游自定义业务插件
├── config.yaml # 声明启用的插件与配置文件
└── main.go # 下游项目组合启动入口
各层职责与禁止规则 (Guardrails):
core/:- 职责:纯抽象,提供 IoC、Context、EventBus 和 Lifecycle。
- 严禁:严禁 import 任何具体业务包,严禁 import
gin、gorm、asynq。
core/contracts/:- 职责:仅定义公开的 Go Interface 和公共 DTO。
- 严禁:严禁包含任何具体实现逻辑或 SQL 操作。
plugins/:- 职责:所有业务逻辑和驱动实现的归宿。遵循标准分层架构(Layered Architecture / MVC 变体)。
- 分层模式选型:
- 模式 1(极简单文件分层,极简微型插件专用):单 package 内部仅各保留 1 个对应文件(
plugin.go,handlers.go,service.go,repository.go,models.go,errs.go,migrations/)。仅适用于单一实体、极小代码量 (<500行) 的微型插件。 - 模式 2(标准独立子包分层架构,官方推荐标准):按职责严格物理分包(
plugin.go,handler/,service/,repository/,model/,errs/,migrations/)。子包内文件以纯业务实体命名(如user.go、config.go),严禁在根包平铺handlers_*、service_*、repository_*等前缀文件。编译器级强约束handler -> service -> repository -> model单向依赖。
- 模式 1(极简单文件分层,极简微型插件专用):单 package 内部仅各保留 1 个对应文件(
- 严禁:插件之间严禁跨包 import 内部私有代码,跨插件调用一律走
contracts接口或EventBus。
第三部分:框架核心提供给插件调用的公用能力矩阵 (Context Capability Matrix)
每个插件在 Apply(ctx *core.Context) 时,都可以无缝调用微内核暴露的以下标准能力:
| 扩展点/能力方法 | 返回类型 | 功能说明 | 适用场景 |
|---|---|---|---|
ctx.Router() |
RouterExtension |
声明 HTTP 路由、前缀分组与挂载中间件,支持 Unregister / UnregisterByID |
暴露 API 接口、Web 控制台 |
ctx.Task() |
TaskExtension |
注册 Asynq 异步任务消费处理器,支持 Unregister |
耗时后台任务、异步消息发送 |
ctx.Schedule() |
ScheduleExtension |
注册 Cron 定时调度任务,支持 Unregister |
定时报表统计、周期性清理 |
ctx.Migrations() |
MigrationExtension |
注册插件专属的 Goose SQL 迁移嵌入系统,支持 Unregister |
自建数据表、版本升级 |
ctx.Events() |
EventBus |
强类型领域事件总线(支持 Emit, Waterfall, Parallel, Serial) |
跨插件完全解耦通知与状态同步 |
ctx.Settings() |
SettingExtension |
声明动态可配置项(支持热更新),支持 Unregister |
业务参数配置、管理台可调节参数 |
ctx.Fork() |
*Context |
创建继承父级容器并隔离局部副作用的子上下文 | 局部 Fiber、请求域隔离 |
core.Provide[T] |
void |
向全局 IoC 容器注册强类型服务(自动挂载 OnDispose 逆操作) |
暴露自身能力给其他插件消费 |
core.Inject[T] |
(T, error) |
从全局 IoC 容器中按类型获取服务实例 | 消费其他插件暴露的服务 |
core.When[T] |
void |
响应式监听服务注入(当服务一旦就绪立即触发回调) | 解决插件装载时序竞争与延迟初始化 |
core.Has[T] |
bool |
判断指定服务类型当前是否已在容器中注册 | 探测环境能力与条件装载 |
ctx.Using(func(T)) |
error |
响应式声明依赖,当服务就绪时执行回调 | 声明前置依赖关系 |
core.Inject[contracts.DBService] |
(DBService, error) |
获取受事务与 Trace 保护的数据库连接与 GORM 实例 | 数据持久化 CRUD |
core.Inject[contracts.CacheService] |
(CacheService, error) |
三层穿透缓存(RAM L1 + Redis L2 + PubSub 广播) | 高频读数据性能加速 |
core.Inject[contracts.StorageService] |
(StorageService, error) |
统一对象存储读写引擎 | 文件摄取、图片持久化 |
core.Inject[contracts.TaskService] |
(TaskService, error) |
后台任务下发、重试与调度管理契约 | 任务下发与定时调度管理 |
core.Inject[contracts.RiskControlService] |
(RiskControlService, error) |
访问日志查询、聚合分析与存储引擎管理契约 | 审计日志与安全分析 |
第四部分:Cordis 插件分层开发规范与代码模板 (Plugin Layered Architecture & Code Templates)
为了统一规范 Wavelet 所有官方插件与下游业务二开插件的研发质量,每个插件在内部遵循 标准分层架构(Layered Architecture / MVC 变体)。
1. 分型与选型策略 (模式 1 vs 模式 2)
根据业务复杂度和规模采用不同的物理包组织方式:
┌────────────────────────┐
│ 插件分层模式选型策略 │
└───────────┬────────────┘
│
┌───────────────────────┴───────────────────────┐
▼ ▼
【模式 1:极简单文件分层】 【模式 2:标准独立子包分层】
适合:极简微型/Demo插件 (<500行) 适合:标准/中大型业务插件 (推荐标准)
结构:单 Package,每层仅对应 1 个同名文件 结构:严格分包 handler/, service/, repository/, model/
禁令:严禁根目录平铺 handlers_* 等前缀文件 规范:子包内以业务实体命名 (如 user.go, order.go)
| 维度 | 模式 1:极简单文件分层 (Single-File Flat) | 模式 2:标准独立子包分层 (Standard Sub-packages) |
|---|---|---|
| 适用场景 | 极简微型插件、单一实体(仅用于小型工具/示例) | 标准业务插件、包含多实体/多接口(官方推荐标准) |
| 代码量规模 | 通常 < 500 行 | 通常 ≥ 500 行(如 upload, auth, admin, order) |
| Go 包形态 | 单一 Go Package,各层级仅各 1 个同名文件 | 按职责严格物理子目录分包,编译级强约束单向依赖 |
| 命名禁令 | 严禁在根目录平铺 handlers_*、service_* 文件 |
子包内文件直接以业务命名(如 user.go),禁止带 handler_* 前缀 |
2. 模式 1:极简单文件分层规范与完整代码模板
2.1 目录结构
backend/plugins/domain/<plugin_name>/
├── plugin.go # [Cordis 接入层] 实现 core.Plugin,负责 Apply 组装与扩展点注册
├── handlers.go # [Handler 层] 单一文件:Gin API Handler
├── service.go # [Service 层] 单一文件:核心业务用例
├── repository.go # [Repository 层] 单一文件:GORM / DB 操作
├── models.go # [Model 层] 单一文件:实体与 DTO
├── errs.go # [Error 层] 单一文件:错误常量
├── plugin_test.go # 插件级单元与集成测试
└── migrations/ # Goose SQL 嵌入文件 (//go:embed)
└── 20260828000001_init_<plugin_name>.sql
⚠️ 严禁规则:当单一文件膨胀或需要拆分多个业务实体时,严禁在根目录创建
handlers_user.go,handlers_admin.go,service_user.go等前缀文件,必须立即重构并迁移为 模式 2(标准独立子包分层架构)!
2.2 核心代码模板 (模式 1)
(1) plugin.go (插件入口与装配)
package order
import (
"embed"
"reflect"
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/gin-gonic/gin"
)
//go:embed migrations/*.sql
var orderMigrations embed.FS
const PluginName = "domain.order"
type Plugin struct {
svc *OrderService
}
func New() *Plugin {
return &Plugin{}
}
func (p *Plugin) Name() string {
return PluginName
}
func (p *Plugin) Inject() []reflect.Type {
return []reflect.Type{
reflect.TypeFor[contracts.DBService](),
}
}
func (p *Plugin) Apply(ctx *core.Context) error {
// 1. 注册专属数据库迁移
ctx.Migrations().Register("order", orderMigrations)
// 2. 初始化持久层与服务层
repo := newOrderRepository(ctx)
p.svc = newOrderService(ctx, repo)
// 3. 注册 HTTP 路由组
authSvc, _ := core.Inject[contracts.AuthService](ctx)
group := ctx.Router().Group("/api/v1/orders")
if authSvc != nil {
group.Use(authSvc.RequireAuthMiddleware())
}
{
group.POST("", p.handleCreateOrder)
group.GET("/:id", p.handleGetOrderDetail)
}
return nil
}
(2) handlers.go (Controller 层)
package order
import (
"net/http"
"github.com/Rain-kl/Wavelet/pkg/oauth"
"github.com/Rain-kl/Wavelet/pkg/response"
"github.com/gin-gonic/gin"
)
// @Summary 创建订单
// @Description 创建新的用户订单
// @Tags Order
// @Accept json
// @Produce json
// @Param request body CreateOrderRequest true "创建参数"
// @Success 200 {object} response.Envelope{data=OrderDTO} "成功"
// @Failure 400 {object} response.Envelope "参数错误"
// @Router /api/v1/orders [post]
func (p *Plugin) handleCreateOrder(c *gin.Context) {
var req CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, errBindParamsFailed)
return
}
user, ok := oauth.GetCurrentUser(c)
if !ok {
response.AbortUnauthorized(c, errUnauthorized)
return
}
order, err := p.svc.CreateOrder(c.Request.Context(), user.ID, req)
if err != nil {
response.AbortInternal(c, errCreateOrderFailed)
return
}
c.JSON(http.StatusOK, response.OK(order))
}
(3) service.go (Service 业务逻辑层)
package order
import (
"context"
"github.com/Rain-kl/Wavelet/core"
)
type OrderService struct {
ctx *core.Context
repo *orderRepository
}
func newOrderService(ctx *core.Context, repo *orderRepository) *OrderService {
return &OrderService{ctx: ctx, repo: repo}
}
func (s *OrderService) CreateOrder(ctx context.Context, userID string, req CreateOrderRequest) (*OrderDTO, error) {
order := &OrderModel{
UserID: userID,
Amount: req.Amount,
Status: "pending",
}
if err := s.repo.Create(ctx, order); err != nil {
return nil, err
}
// 发射领域事件
s.ctx.Events().Emit(ctx, "order:created", OrderCreatedEvent{
OrderID: order.ID,
UserID: order.UserID,
Amount: order.Amount,
})
return &OrderDTO{
ID: order.ID,
Amount: order.Amount,
Status: order.Status,
}, nil
}
(4) repository.go (Repository 数据访问层)
package order
import (
"context"
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/pkg/util"
"gorm.io/gorm"
)
type orderRepository struct {
ctx *core.Context
}
func newOrderRepository(ctx *core.Context) *orderRepository {
return &orderRepository{ctx: ctx}
}
func (r *orderRepository) getDB(ctx context.Context) *gorm.DB {
if dbSvc, err := core.Inject[contracts.DBService](r.ctx); err == nil && dbSvc != nil {
return dbSvc.GetDB().WithContext(ctx)
}
return nil
}
func (r *orderRepository) Create(ctx context.Context, order *OrderModel) error {
return r.getDB(ctx).Create(order).Error
}
func (r *orderRepository) SearchByKeyword(ctx context.Context, keyword string) ([]OrderModel, error) {
var list []OrderModel
// SQL LIKE 防注入与通配符转义规范
safeKeyword := util.EscapeLike(keyword) + "%"
err := r.getDB(ctx).Where("status LIKE ? ESCAPE '\\'", safeKeyword).Find(&list).Error
return list, err
}
(5) models.go 与 errs.go
// models.go
package order
import "time"
type OrderModel struct {
ID string `gorm:"column:id;primaryKey;size:64" json:"id"`
UserID string `gorm:"column:user_id;index;size:64;not null" json:"user_id"`
Amount int64 `gorm:"column:amount;not null" json:"amount"`
Status string `gorm:"column:status;size:32;index;not null;default:'pending'" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
}
func (OrderModel) TableName() string {
return "w_orders"
}
type CreateOrderRequest struct {
Amount int64 `json:"amount" binding:"required,gt=0"`
}
type OrderDTO struct {
ID string `json:"id"`
Amount int64 `json:"amount"`
Status string `json:"status"`
}
type OrderCreatedEvent struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
Amount int64 `json:"amount"`
}
// errs.go
package order
const (
errBindParamsFailed = "errBindParamsFailed"
errUnauthorized = "errUnauthorized"
errCreateOrderFailed = "errCreateOrderFailed"
)
3. 模式 2:标准独立子包物理分层规范与完整代码模板 (推荐标准)
用于标准与中大型业务插件,各层使用独立的 Go package 物理隔离。
3.1 目录结构与文件命名规约
backend/plugins/domain/order/
├── plugin.go # [插件根入口] 实现 core.Plugin,装配各子包并向 Cordis 注册
│
├── handler/ # package handler:HTTP API 接入层(或 controller/)
│ ├── router.go # 路由组挂载与中间件绑定
│ └── order.go # 订单相关 Handler(以业务直接命名,禁止 handlers_order.go)
│
├── service/ # package service:核心业务逻辑层
│ ├── service.go # 业务用例接口定义 (Service Interface)
│ └── order.go # 订单业务用例实现(以业务直接命名,禁止 service_order.go)
│
├── repository/ # package repository:数据访问持久化层 (DAL)
│ ├── repository.go # 仓储通用方法与工厂
│ └── order.go # 订单仓储持久化实现(以业务直接命名,禁止 repository_order.go)
│
├── model/ # package model (或 models/):纯领域实体与传输对象(零外部框架依赖)
│ ├── entity.go # 数据库映射实体 (TableName() 必须带 w_<plugin>_ 前缀)
│ ├── dto.go # 请求与响应 DTO
│ └── events.go # 领域事件结构体
│
├── errs/ # package errs:错误常量与错误码定义 (或根目录 errs.go)
│ └── errs.go
│
└── migrations/ # Goose SQL 独立迁移嵌入文件 (//go:embed)
└── 20260828000001_init_order.sql
3.2 模式 2 核心装配代码范例 (plugin.go)
package order
import (
"embed"
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/plugins/domain/order/handler"
"github.com/Rain-kl/Wavelet/plugins/domain/order/repository"
"github.com/Rain-kl/Wavelet/plugins/domain/order/service"
)
//go:embed migrations/*.sql
var orderMigrations embed.FS
type Plugin struct{}
func (p *Plugin) Name() string {
return "domain.order"
}
func (p *Plugin) Apply(ctx *core.Context) error {
// 1. 注册迁移
ctx.Migrations().Register("order", orderMigrations)
// 2. 构造数据层与服务层
repo := repository.NewOrderRepository(ctx)
svc := service.NewOrderService(ctx, repo)
// 3. 构造 Handler 并挂载路由
h := handler.NewOrderHandler(svc)
authSvc, _ := core.Inject[contracts.AuthService](ctx)
handler.RegisterRoutes(ctx.Router(), h, authSvc)
return nil
}
4. 各层核心职责边界与严格禁止防线 (Guardrails)
┌──────────────────────────────────────────────────────────────────┐
│ Controller / Handler 层 (HTTP 接入) │
│ • 参数绑定 ShouldBindJSON • 用户会话 oauth.GetCurrentUser │
│ • 统一信封 response.OK/Abort* • 严禁 SQL 操作 / 严禁重度业务 │
└─────────────────────────────────┬────────────────────────────────┘
│ 调用 Service (入参 context.Context)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Service 层 (业务用例 & 领域逻辑) │
│ • 纯 Go 逻辑 (零 Web 依赖) • 事务编排 ctx.DB().Transaction │
│ • 领域事件 ctx.Events().Emit • 严禁 import gin / c.JSON │
└─────────────────────────────────┬────────────────────────────────┘
│ 调用 Repository 接口
▼
┌──────────────────────────────────────────────────────────────────┐
│ Repository 层 (数据持久化 DAL) │
│ • GORM CRUD 与查询 • EscapeLike 通配符安全转义 │
│ • 严禁反向依赖 Service/Controller • 严禁越权读写其他插件数据表 │
└─────────────────────────────────┬────────────────────────────────┘
│ 映射
▼
┌──────────────────────────────────────────────────────────────────┐
│ Model 层 (纯实体 & DTO) │
│ • TableName() 带专属表前缀 • 请求/响应结构体 │
│ • 零值与 DB 默认值匹配 • 无任何上层包依赖 │
└──────────────────────────────────────────────────────────────────┘
- 表单一所有者原则 (Single Owner Principle):数据表有且仅由所属插件操作(表名统一前缀
w_<plugin>_*),跨插件一律通过公开契约 Interface 或 EventBus 协同。 - LIKE 查询安全防注入:所有涉及用户输入的模糊查询,必须经过
util.EscapeLike转义通配符并显式声明ESCAPE '\\'语法。 - Goroutine 安全:并发任务统一使用
util.Go,杜绝直接使用裸go func()。