feat(gateway): 优化gateway相关功能
Quality check / Web UI (push) Successful in 9m19s

This commit is contained in:
mei
2026-09-12 15:32:27 +08:00
parent 0d89b7fe8f
commit 9b656cebee
84 changed files with 2769 additions and 825 deletions
+8 -13
View File
@@ -1,33 +1,28 @@
package db
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/model"
)
// LoadAccountConfig 读取账号并把 Config 字段反序列化到 out。
// GetAccount 读取一个账号,返回数据库里的原始记录。
//
// 各个 driver 之前都各自实现了一遍这段逻辑,其中 github driver 甚至
// 直接把驱动内的结构体当成 gorm model 查询,这里统一收敛。
func LoadAccountConfig(accountID uint, out any) error {
// 规则执行需要的账号凭据由上层(handler/runner)在这里取好再传给 driver,
// driver 自己不碰数据库。accountID 为 0 表示该规则不需要账号,返回 (nil, nil)。
func GetAccount(accountID uint) (*model.Account, error) {
if accountID == 0 {
return fmt.Errorf("该规则没有关联账号")
return nil, nil
}
gormDB, err := GetGormDB()
if err != nil {
return err
return nil, err
}
var account model.Account
if err := gormDB.First(&account, accountID).Error; err != nil {
return fmt.Errorf("获取账号(ID: %d)失败: %w", accountID, err)
return nil, fmt.Errorf("获取账号(ID: %d)失败: %w", accountID, err)
}
if err := json.Unmarshal([]byte(account.Config), out); err != nil {
return fmt.Errorf("解析账号(ID: %d)配置失败: %w", accountID, err)
}
return nil
return &account, nil
}
+4
View File
@@ -43,6 +43,9 @@ func MustInit() *gorm.DB {
if err := ensureAdmin(gormDB); err != nil {
log.Fatalf("初始化管理员失败: %v", err)
}
if err := migrateGatewayRules(gormDB); err != nil {
log.Fatalf("迁移消息网关规则失败: %v", err)
}
return gormDB
}
@@ -136,6 +139,7 @@ func AutoMigrate(gormDB *gorm.DB) error {
&model.User{},
&model.Timer{},
&model.Rule{},
&model.GatewayRule{},
&model.Account{},
&model.ExecutionLog{},
&model.MessageGateway{},
+62
View File
@@ -0,0 +1,62 @@
package db
import (
"log"
"github.com/ssdomei232/goodBaby/model"
"gorm.io/gorm"
)
// legacyRule 是迁移前的 rules 表结构,只用来读出已经废弃的 gateway_id
type legacyRule struct {
ID uint
UID uint
GatewayID uint
Name string
AccountID uint
Type string
ConfigJson string
Enabled bool
CreateAt int64
}
// migrateGatewayRules 把历史数据里混在 rules 表中的消息网关规则搬到 gateway_rules 表。
//
// v2.1 之前消息网关规则和定时器规则共用 rules 表,靠 gateway_id 区分;
// 现在两者分表存储,这里做一次性搬迁,避免老用户升级后规则凭空消失。
func migrateGatewayRules(gormDB *gorm.DB) error {
if !gormDB.Migrator().HasColumn("rules", "gateway_id") {
return nil
}
var legacy []legacyRule
if err := gormDB.Table("rules").Where("gateway_id <> 0").Find(&legacy).Error; err != nil {
return err
}
if len(legacy) == 0 {
return nil
}
return gormDB.Transaction(func(tx *gorm.DB) error {
for _, rule := range legacy {
moved := model.GatewayRule{
UID: rule.UID,
GatewayID: rule.GatewayID,
Name: rule.Name,
AccountID: rule.AccountID,
Type: rule.Type,
ConfigJson: rule.ConfigJson,
Enabled: rule.Enabled,
CreateAt: rule.CreateAt,
}
if err := tx.Create(&moved).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM rules WHERE id = ?", rule.ID).Error; err != nil {
return err
}
}
log.Printf("已迁移 %d 条消息网关规则到 gateway_rules 表", len(legacy))
return nil
})
}
+9 -6
View File
@@ -9,10 +9,13 @@ import (
"github.com/ssdomei232/goodBaby/model"
)
// RuleExecutor 规则执行器接口
// RuleExecutor 规则执行器接口。
//
// 执行一条规则需要的一切(规则本体、账号凭据)都由上层组装成 model.RuleTask,
// driver 只负责把动作做出去:不查数据库、不写日志,因此每个实现都能单独测试。
type RuleExecutor interface {
// Execute 执行规则,ctx 决定了重试的最长时间
Execute(ctx context.Context, rule *model.Rule) error
Execute(ctx context.Context, task *model.RuleTask) error
// GetType 获取执行器支持的规则类型
GetType() string
}
@@ -35,13 +38,13 @@ func (er *ExecutorRegistry) Register(executor RuleExecutor) {
}
// Execute 根据规则类型执行规则
func (er *ExecutorRegistry) Execute(ctx context.Context, rule *model.Rule) error {
executor, exists := er.executors[rule.Type]
func (er *ExecutorRegistry) Execute(ctx context.Context, task *model.RuleTask) error {
executor, exists := er.executors[task.Rule.Type]
if !exists {
return fmt.Errorf("不支持的规则类型: %s", rule.Type)
return fmt.Errorf("不支持的规则类型: %s", task.Rule.Type)
}
return executor.Execute(ctx, rule)
return executor.Execute(ctx, task)
}
// GetSupportedTypes 获取所有支持的规则类型
+53 -12
View File
@@ -53,7 +53,18 @@ func Runner(timer *model.Timer) {
wg.Wait()
}
// ExecuteRule 执行单个规则并记录执行日志
// NewTask 组装执行一条规则所需的输入:规则本体 + 从数据库取出的关联账号。
//
// 数据库访问只发生在这一层,driver 拿到的 task 已经是自包含的。
func NewTask(rule *model.Rule) (*model.RuleTask, error) {
account, err := db.GetAccount(rule.AccountID)
if err != nil {
return nil, err
}
return &model.RuleTask{Rule: rule, Account: account}, nil
}
// ExecuteRule 执行单条规则并记录执行日志
func ExecuteRule(rule *model.Rule, trigger string) error {
ctx, cancel := retry.ExecutionContext()
defer cancel()
@@ -62,24 +73,54 @@ func ExecuteRule(rule *model.Rule, trigger string) error {
// ExecuteRuleWithContext 使用给定 context 执行规则,手动测试时可传入较短的超时
func ExecuteRuleWithContext(ctx context.Context, rule *model.Rule, trigger string) error {
err := GetGlobalExecutorRegistry().Execute(ctx, rule)
message := "执行成功"
if err != nil {
message = err.Error()
log.Printf("执行规则失败 [ID: %d, Type: %s]: %v", rule.ID, rule.Type, err)
}
logstore.Record(&model.ExecutionLog{
task, err := NewTask(rule)
return execTask(ctx, task, err, &model.ExecutionLog{
UID: rule.UID,
RuleID: rule.ID,
RuleName: rule.Name,
RuleType: rule.Type,
TimerID: rule.TimerID,
Trigger: trigger,
Success: err == nil,
Message: message,
})
}
// ExecuteGatewayRuleWithContext 执行一条消息网关规则,日志里带上网关信息。
//
// 与定时器规则走同一条执行链路,区别只有触发来源与日志字段。
func ExecuteGatewayRuleWithContext(ctx context.Context, rule *model.GatewayRule, trigger string) error {
task, err := NewTask(rule.AsRule())
return execTask(ctx, task, err, &model.ExecutionLog{
UID: rule.UID,
RuleName: rule.Name,
RuleType: rule.Type,
GatewayID: rule.GatewayID,
GatewayRuleID: rule.ID,
Trigger: trigger,
})
}
// execTask 执行规则并写入执行日志。
//
// taskErr 是组装输入时的错误(例如账号已被删除):这时不会走到 driver,
// 但同样要留下一条失败日志,方便用户在 WebUI 上看到原因。
func execTask(ctx context.Context, task *model.RuleTask, taskErr error, entry *model.ExecutionLog) error {
if taskErr != nil {
return record(entry, taskErr)
}
log.Printf("执行规则 [%s] %s (触发来源: %s)", entry.RuleType, entry.RuleName, entry.Trigger)
return record(entry, GetGlobalExecutorRegistry().Execute(ctx, task))
}
// record 写入执行日志并返回原始错误
func record(entry *model.ExecutionLog, err error) error {
entry.Success = err == nil
entry.Message = "执行成功"
if err != nil {
entry.Message = err.Error()
log.Printf("执行规则失败 [%s] %s: %v", entry.RuleType, entry.RuleName, err)
}
logstore.Record(entry)
return err
}