Files
OpenFlare/docs/WAVELET_DEVELOPER_GUIDE.md
T

44 KiB
Raw Blame History

Wavelet Cordis 插件化架构实战开发指南与标准规范

  • 文档类型: 下游开发者手册 / 架构实战指南 (Cookbook & Architecture Reference)
  • 目标受众: 官方插件开发者、下游业务二开工程师、架构师
  • 版本: v1.0.0 (2026-08-27)

目录


第一部分:下游项目实战开发指南与 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 解法:

  1. 接口均定义在 core/contracts,双方只依赖 core/contracts。
  2. 运行时采用 延迟注入 (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 模型规范是什么?

规范:

  1. 表名必须带有插件专有前缀(如 w_order_、w_auth_),避免跨插件表名冲突。
  2. 零值与数据库默认值严格对齐;禁止物理外键,显式建索引。
  3. 必须通过 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)

启动时,引擎遍历每个插件:

  1. 查询 w_schema_versions WHERE plugin_id = 'order' 获取当前最大版本号
  2. 扫描插件 migrations/ 目录下的 .sql 文件
  3. 如果存在未应用的版本号 → 执行迁移
  4. 如果全部已应用 → 跳过
-- 查看全局迁移状态
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:如果跨插件操作多张表,如何确保事务一致性?

在插件化和微服务就绪体系下,跨插件的强分布式事务是反模式。

  1. 同插件内多表操作:直接使用本地数据库事务:
    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
    })
    
  2. 跨插件操作(如创建订单 + 扣减库存 + 发送通知):
    • 采用 最终一致性 (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):

  1. core/:
    • 职责:纯抽象,提供 IoC、Context、EventBus 和 Lifecycle。
    • 严禁:严禁 import 任何具体业务包,严禁 import gin、gorm、asynq。
  2. core/contracts/:
    • 职责:仅定义公开的 Go Interface 和公共 DTO。
    • 严禁:严禁包含任何具体实现逻辑或 SQL 操作。
  3. 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 单向依赖。
    • 严禁:插件之间严禁跨包 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 默认值匹配           • 无任何上层包依赖             │
└──────────────────────────────────────────────────────────────────┘
  1. 表单一所有者原则 (Single Owner Principle):数据表有且仅由所属插件操作(表名统一前缀 w_<plugin>_*),跨插件一律通过公开契约 Interface 或 EventBus 协同。
  2. LIKE 查询安全防注入:所有涉及用户输入的模糊查询,必须经过 util.EscapeLike 转义通配符并显式声明 ESCAPE '\\' 语法。
  3. Goroutine 安全:并发任务统一使用 util.Go,杜绝直接使用裸 go func()。