跳转至

Remilia 最佳实践

最后更新: 2026-08-04

本文档总结了使用 Remilia 框架开发 QQ 机器人的最佳实践和常见模式。

📋 目录

  1. 项目结构
  2. 错误处理
  3. 并发控制
  4. 资源管理
  5. 日志规范
  6. 性能优化
  7. 安全实践
  8. 测试策略

1. 项目结构

推荐的目录结构

my-bot/
├── cmd/
│   └── bot/
│       └── main.go           # 入口文件
├── internal/
│   ├── handlers/             # 处理器
│   │   ├── command.go
│   │   ├── event.go
│   │   └── middleware.go
│   ├── plugins/              # 插件
│   │   ├── weather/
│   │   ├── admin/
│   │   └── utility/
│   ├── services/             # 业务逻辑
│   │   ├── weather.go
│   │   └── database.go
│   └── models/               # 数据模型
│       └── user.go
├── config/
│   ├── config.yaml           # 配置文件
│   └── config.example.yaml   # 配置示例
├── deployments/              # 部署配置
│   ├── docker/
│   └── kubernetes/
├── scripts/                  # 脚本
│   ├── build.sh
│   └── deploy.sh
├── docs/                     # 文档
├── go.mod
├── go.sum
└── README.md

模块化设计

✅ DO

// handlers/commands.go
package handlers

import (
    "github.com/KomeiDiSanXian/remilia/core/engine"
    "github.com/KomeiDiSanXian/remilia/platform"
)

func RegisterCommands(eng *engine.Engine) {
    eng.OnCommand(eventctx.EventGroup, "/help").Handle(HandleHelp)
    eng.OnCommand(eventctx.EventGroup, "/weather").Handle(HandleWeather)
}

// handlers/help.go
func HandleHelp(ctx *eventctx.Context) error {
    ctx.Reply(helpText)
        return nil
}

❌ DON'T

// main.go - 所有逻辑都在 main 中
func main() {
    eng := engine.NewEngine()

    // 100+ 行的处理器定义...
    eng.OnCommand(dto.GroupAtMessageCreate, "/help",
        func(ctx *eventctx.Context) error {
            // 大量逻辑...
            return nil
        })
}

2. 错误处理

统一错误处理

✅ DO

// 定义错误类型
var (
    ErrInvalidInput = errors.New("invalid input")
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

// 处理器中返回明确的错误
func HandleCommand(ctx *eventctx.Context) error {
    data := ctx.GetMessageContent()
    if data == "" {
        return ErrInvalidInput
    }

    result, err := service.Process(data)
    if err != nil {
        return fmt.Errorf("process failed: %w", err)
    }

    ctx.Reply(result)

        return nil
}

// 使用中间件统一处理错误
func ErrorHandler() eventctx.Middleware {
    return func(next eventctx.Handler) eventctx.Handler {
        return func(ctx *eventctx.Context) error {
            err := next(ctx)
            if err != nil {
                log.WithError(err).Error("Handler failed")

                // 根据错误类型返回不同消息
                switch {
                case errors.Is(err, ErrInvalidInput):
                    ctx.Reply("输入错误,请检查格式")
                case errors.Is(err, ErrNotFound):
                    ctx.Reply("未找到相关信息")
                case errors.Is(err, ErrUnauthorized):
                    ctx.Reply("权限不足")
                default:
                    ctx.Reply("处理失败,请稍后重试")
                }
            }
            return err
        }
    }
}

❌ DON'T

// 忽略错误
func HandleCommand(ctx *eventctx.Context) error {
    result, _ := service.Process(data)  // ❌ 忽略错误
    ctx.Reply(result)
    return nil
}

// 使用 panic
func HandleCommand(ctx *eventctx.Context) error {
    if data == "" {
        panic("invalid input")  // ❌ 不要使用 panic
    }
    return nil
}

3. 并发控制

使用中间件限流

✅ DO

// 简单全局限流(推荐默认选择)
eng.Use(middleware.SimpleRateLimit(10)) // 每秒最多 10 个事件

// 按用户/Group 限流
eng.Use(middleware.RateLimitTokenBucket(2, 4, func(ctx *context.Context) string {
    return ctx.GetSenderInfo().ID
}))

// 自适应限流(自动根据负载调整)
config := ratelimit.DefaultAdaptiveConfig()
config.MinConcurrency = 10
config.MaxConcurrency = 500

limiter := ratelimit.NewAdaptiveRateLimiter(config)
limiter.Start()
defer limiter.Stop()
eng.Use(limiter.Middleware())

选型建议: - 简单固定限流 → SimpleRateLimit(n) - 按 key(用户/群组)限流 → RateLimitTokenBucket - 根据 CPU/内存自动调整 → NewAdaptiveRateLimiter

使用 Context 控制超时

✅ DO

func HandleLongTask(ctx *eventctx.Context) error {
    // 使用 context 控制超时
    taskCtx, cancel := context.WithTimeout(ctx.Context(), 10*time.Second)
    defer cancel()

    result := make(chan string, 1)
    go func() {
        // 执行耗时任务
        data := performLongTask()
        result <- data
    }()

    select {
    case data := <-result:
        ctx.Reply(data)
            return nil
    case <-taskCtx.Done():
        ctx.Reply("处理超时,请稍后重试")
            return nil
    }
}

使用 sync.WaitGroup 等待任务

✅ DO

func HandleBatch(ctx *eventctx.Context) error {
    var wg sync.WaitGroup
    results := make(chan string, 10)

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            result := processItem(id)
            results <- result
        }(i)
    }

    // 等待所有任务完成
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集结果
    var allResults []string
    for r := range results {
        allResults = append(allResults, r)
    }

    ctx.Reply(strings.Join(allResults, "\n"))

        return nil
}

4. 资源管理

数据库连接

✅ DO

// 使用连接池
type Service struct {
    db *sql.DB
}

func NewService(dsn string) (*Service, error) {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, err
    }

    // 配置连接池
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)

    return &Service{db: db}, nil
}

func (s *Service) Close() error {
    return s.db.Close()
}

// 使用 context 控制查询
func (s *Service) Query(ctx context.Context, query string) ([]Row, error) {
    rows, err := s.db.QueryContext(ctx, query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var results []Row
    for rows.Next() {
        var row Row
        if err := rows.Scan(&row); err != nil {
            return nil, err
        }
        results = append(results, row)
    }

    return results, rows.Err()
}

HTTP 客户端

✅ DO

// 复用 HTTP 客户端
var httpClient = &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
}

func FetchData(url string) ([]byte, error) {
    resp, err := httpClient.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    return io.ReadAll(resp.Body)
}

❌ DON'T

// 每次创建新的客户端
func FetchData(url string) ([]byte, error) {
    client := &http.Client{Timeout: 10 * time.Second}  // ❌ 浪费资源
    resp, _ := client.Get(url)  // ❌ 忽略错误
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

5. 日志规范

结构化日志

✅ DO

import "github.com/KomeiDiSanXian/remilia/infra/logger"

// 使用结构化字段
func HandleCommand(ctx *eventctx.Context) error {
    log := logger.WithFields(logger.Fields{
        "command":  "/weather",
        "user_id":  ctx.GetSenderInfo().ID,
        "guild":    ctx.GetChatInfo().ParentID,
    })

    log.Info("Processing command")

    result, err := service.Process()
    if err != nil {
        log.WithError(err).Error("Command failed")
        return err
    }

    log.WithField("result_len", len(result)).Info("Command succeeded")
    ctx.Reply(result)
        return nil
}

日志级别

// 设置合适的日志级别
logger.SetLevel("info")

// Debug - 详细的调试信息
logger.WithField("key", key).Debug("Cache hit")

// Info - 一般信息
logger.WithField("user_id", userID).Info("User logged in")

// Warn - 警告信息
logger.WithField("usage", "90%").Warn("Rate limit approaching")

// Error - 错误信息
logger.WithError(err).Error("Database query failed")

// Fatal - 致命错误(会退出程序)
logger.Fatal("Failed to start server")

6. 性能优化

使用对象池

✅ DO

// 复用频繁创建的对象
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func ProcessData(data string) string {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)

    buf.Reset()
    buf.WriteString(data)
    // ... 处理

    return buf.String()
}

批量处理

✅ DO

// 批量处理消息
type MessageBatcher struct {
    messages chan *Message
    batch    []*Message
    mu       sync.Mutex
}

func (mb *MessageBatcher) Add(msg *Message) {
    mb.messages <- msg
}

func (mb *MessageBatcher) Start() {
    ticker := time.NewTicker(1 * time.Second)

    for {
        select {
        case msg := <-mb.messages:
            mb.batch = append(mb.batch, msg)

            if len(mb.batch) >= 100 {
                mb.flush()
            }

        case <-ticker.C:
            if len(mb.batch) > 0 {
                mb.flush()
            }
        }
    }
}

func (mb *MessageBatcher) flush() {
    // 批量处理
    processBatch(mb.batch)
    mb.batch = mb.batch[:0]
}

缓存策略

✅ DO

import "github.com/patrickmn/go-cache"

// 使用缓存减少重复计算
var dataCache = cache.New(5*time.Minute, 10*time.Minute)

func GetData(key string) (string, error) {
    // 尝试从缓存获取
    if cached, found := dataCache.Get(key); found {
        return cached.(string), nil
    }

    // 缓存未命中,执行计算
    data, err := expensiveOperation(key)
    if err != nil {
        return "", err
    }

    // 存入缓存
    dataCache.Set(key, data, cache.DefaultExpiration)

    return data, nil
}

7. 安全实践

输入验证

✅ DO

// 验证和清理用户输入
func HandleCommand(ctx *eventctx.Context) error {
    input := ctx.GetMessageContent()

    // 长度检查
    if len(input) > 1000 {
        ctx.Reply("输入过长")
            return nil
    }

    // 格式验证
    if !isValidFormat(input) {
        ctx.Reply("格式错误")
            return nil
    }

    // SQL 注入防护 - 使用参数化查询
    _, err := db.Exec("SELECT * FROM users WHERE name = ?", input)

    return err
}

// XSS 防护 - 转义输出
func SafeReply(ctx *eventctx.Context, text string) error {
    escaped := html.EscapeString(text)
    ctx.Reply(escaped)
        return nil
}

权限控制

✅ DO

// 定义权限检查中间件
func RequireAdmin() eventctx.Middleware {
    return func(next eventctx.Handler) eventctx.Handler {
        return func(ctx *eventctx.Context) error {
            userID := ctx.GetSenderInfo().ID

            if !isAdmin(userID) {
                ctx.Reply("权限不足")
                    return nil
            }

            return next(ctx)
        }
    }
}

// 使用
eng.Use(RequireAdmin())
eng.OnCommand("/admin", handleAdminCommand)

敏感信息保护

✅ DO

// 不要硬编码敏感信息
// ❌ token := "abc123"

// ✅ 使用环境变量
token := os.Getenv("BOT_TOKEN")

// ✅ 使用配置文件(不提交到版本控制)
config, _ := config.Load("config.yaml")
token := config.Bot.Token

// 日志中隐藏敏感信息
func logRequest(req *http.Request) {
    log.Info("Request",
        "url", req.URL.String(),
        "token", "***",  // ✅ 隐藏 token
    )
}

8. 测试策略

单元测试

✅ DO

package handlers

import (
    "testing"

    eventctx "github.com/KomeiDiSanXian/remilia/core/context"
    "github.com/KomeiDiSanXian/remilia/platform"
    "github.com/stretchr/testify/assert"
)

func TestHandleHelp(t *testing.T) {
    // 创建测试上下文
    event := &platform.Event{
        ID:   "test-id",
        Kind: platform.EventKindGroupMessage,
    }
    ctx := eventctx.NewContext(event, nil)

    // 执行处理器
    err := HandleHelp(ctx)

    // 断言
    assert.NoError(t, err)
    // 验证回复内容...
}

func TestServiceWithMock(t *testing.T) {
    // 使用 mock 隔离外部依赖
    mockDB := &MockDatabase{}
    service := NewService(mockDB)

    result, err := service.Query("test")

    assert.NoError(t, err)
    assert.Equal(t, "expected", result)
}

集成测试

func TestBotIntegration(t *testing.T) {
    // 创建测试 Bot
    eng := engine.NewEngine()
    RegisterHandlers(eng)

    adapter := qq.NewWebhookServerAdapter(":0", &dto.BotInfo{AppID: 123456})
    bot := remilia.NewBot(adapter, eng)

    // 启动
    err := bot.Start()
    assert.NoError(t, err)
    defer bot.Shutdown()

    // 发送测试消息
    // ... 测试逻辑
}

Benchmark 测试

func BenchmarkHandleCommand(b *testing.B) {
    ctx := createTestContext()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        HandleCommand(ctx)
    }
}

9. Context 数据操作

ctx.Set / ctx.Delete 行为差异

// ✅ 正确删除 key
ctx.Delete("session")

// ❌ 不会删除——ctx.Set(key, nil) 是 no-op,静默忽略
ctx.Set("session", nil)

ctx.Set(key, nil) 被设计为 no-op 是为了防止误删保留 key(框架内部 key 以 _remilia_internal_ 为前缀),需要显式删除时必须调用 ctx.Delete

跨 Handler 传递数据

// 在中间件中注入数据
func AuthMiddleware() eventctx.Middleware {
    return func(next eventctx.Handler) eventctx.Handler {
        return func(ctx *eventctx.Context) error {
            user := resolveUser(ctx)
            ctx.Set("current_user", user)  // 注入
            return next(ctx)
        }
    }
}

// 在 Handler 中读取
func HandleCommand(ctx *eventctx.Context) error {
    user, ok := ctx.Get("current_user")
    if !ok {
        ctx.Reply("未登录")
            return nil
    }
    u := user.(*User)
    ctx.Reply("你好, " + u.Name)
        return nil
}

10. 插件开发

使用 v2 Descriptor API,无需继承:

func New() *plugin.Descriptor {
    p := &MyPlugin{}
    return &plugin.Descriptor{

        Meta: &plugin.Metadata{
            Description: "示例插件",
            Category:    "工具",
        },
        Setup: func(ctx *plugin.SetupContext) (any, error) {
            ctx.Reg.RegisterCommand(dto.GroupAtMessageCreate, "/cmd").
                Handle(p.handle)
            return p, nil
        },
        Teardown: func(ctx *plugin.TeardownContext) error {
            ctx.Log.Info("stopped")
            return nil
        },
    }
}

详细指南:插件开发最佳实践


💡 其他建议

1. 使用依赖注入

type Bot struct {
    engine  *engine.Engine
    db      *Database
    cache   *Cache
    logger  *logger.Logger
}

func NewBot(eng *engine.Engine, db *Database, cache *Cache) *Bot {
    return &Bot{
        engine: eng,
        db:     db,
        cache:  cache,
        logger: logger.DefaultLogger(),
    }
}

2. 配置分离

// config/config.go
type Config struct {
    Bot        BotConfig
    Database   DatabaseConfig
    Redis      RedisConfig
    Middleware MiddlewareConfig
}

func Load(path string) (*Config, error) {
    // 加载配置...
}

3. 优雅关闭

func main() {
    bot := setupBot()

    // 捕获信号
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)

    // 启动
    bot.Start()

    // 等待信号
    <-sigCh

    // 优雅关闭
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := bot.ShutdownWithContext(ctx); err != nil {
        log.Error("Shutdown failed:", err)
    }
}

4. 监控和告警

// 暴露健康检查端点
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
    if bot.IsHealthy() {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    } else {
        w.WriteHeader(http.StatusServiceUnavailable)
    }
})

// 暴露 Prometheus 指标
http.Handle("/metrics", promhttp.Handler())

📚 相关文档


记住: 代码质量比功能数量更重要!