mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
feat(db): split migrations into dialect-specific sqlite and postgres packages
This commit is contained in:
+59
-6
@@ -7,7 +7,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pressly/goose/v3"
|
"github.com/pressly/goose/v3"
|
||||||
@@ -67,13 +69,13 @@ func newWaveletApp(profile core.Profile) *core.App {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Register all 8 domain business plugins
|
// 3. Register all 8 domain business plugins (admin first to ensure schema and base config tables exist)
|
||||||
app.Use(
|
app.Use(
|
||||||
auth.New(),
|
admin.New(),
|
||||||
user.New(),
|
user.New(),
|
||||||
|
auth.New(),
|
||||||
message_gateway.New(),
|
message_gateway.New(),
|
||||||
risk_control.New(),
|
risk_control.New(),
|
||||||
admin.New(),
|
|
||||||
upload.New(),
|
upload.New(),
|
||||||
cap.New(),
|
cap.New(),
|
||||||
system.New(),
|
system.New(),
|
||||||
@@ -179,8 +181,7 @@ func (s *sharedStore) ListMigrations(ctx context.Context, db goosedb.DBTxConn) (
|
|||||||
var results []*goosedb.ListMigrationsResult
|
var results []*goosedb.ListMigrationsResult
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r goosedb.ListMigrationsResult
|
var r goosedb.ListMigrationsResult
|
||||||
r.IsApplied = true
|
if err := rows.Scan(&r.Version, &r.IsApplied); err != nil {
|
||||||
if err := rows.Scan(&r.Version); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
results = append(results, &r)
|
results = append(results, &r)
|
||||||
@@ -240,7 +241,8 @@ func (e *gooseEngine) Migrate(ctx *core.Context, entries []core.MigrationEntry)
|
|||||||
dialect: dialectStr,
|
dialect: dialectStr,
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := goose.NewProvider(dialect, sqlDB, entry.FS, goose.WithStore(store))
|
migrationFS := findMigrationFS(entry.FS, dialect)
|
||||||
|
provider, err := goose.NewProvider(goose.DialectCustom, sqlDB, migrationFS, goose.WithStore(store))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("migration %s: create provider: %w", entry.PluginID, err)
|
return fmt.Errorf("migration %s: create provider: %w", entry.PluginID, err)
|
||||||
}
|
}
|
||||||
@@ -267,3 +269,54 @@ func gooseDialect() goose.Dialect {
|
|||||||
}
|
}
|
||||||
return goose.DialectPostgres
|
return goose.DialectPostgres
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findMigrationFS(rootFS fs.FS, dialect goose.Dialect) fs.FS {
|
||||||
|
dialectDir := "postgres"
|
||||||
|
if dialect == goose.DialectSQLite3 {
|
||||||
|
dialectDir = "sqlite"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Direct search for dialect folder (e.g., "sqlite", "migrations/sqlite", "logstore/migrations/sqlite")
|
||||||
|
for _, subDir := range []string{
|
||||||
|
dialectDir,
|
||||||
|
"migrations/" + dialectDir,
|
||||||
|
"logstore/migrations/" + dialectDir,
|
||||||
|
} {
|
||||||
|
if sub, err := fs.Sub(rootFS, subDir); err == nil {
|
||||||
|
if matches, err := fs.Glob(sub, "*.sql"); err == nil && len(matches) > 0 {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Recursive walk to find a directory named dialectDir with *.sql files
|
||||||
|
var foundDir string
|
||||||
|
_ = fs.WalkDir(rootFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err == nil && d.IsDir() && filepath.Base(path) == dialectDir {
|
||||||
|
if sub, subErr := fs.Sub(rootFS, path); subErr == nil {
|
||||||
|
if matches, globErr := fs.Glob(sub, "*.sql"); globErr == nil && len(matches) > 0 {
|
||||||
|
foundDir = path
|
||||||
|
return fs.SkipAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if foundDir != "" && foundDir != "." {
|
||||||
|
if sub, err := fs.Sub(rootFS, foundDir); err == nil {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to generic migrations / root if dialect specific is not present
|
||||||
|
for _, subDir := range []string{"migrations", "logstore/migrations"} {
|
||||||
|
if sub, err := fs.Sub(rootFS, subDir); err == nil {
|
||||||
|
if matches, err := fs.Glob(sub, "*.sql"); err == nil && len(matches) > 0 {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootFS
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"Wavelet/core"
|
||||||
|
"Wavelet/core/contracts"
|
||||||
|
"Wavelet/core/extpoints"
|
||||||
|
"Wavelet/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRedisPluggability_Simulation(t *testing.T) {
|
||||||
|
origRedisEnabled := config.Config.Redis.Enabled
|
||||||
|
defer func() { config.Config.Redis.Enabled = origRedisEnabled }()
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 场景 1: 拔出 Redis (Zero-Redis Monolith 模式)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
t.Run("Scenario_Unplugged_ZeroRedis_Mode", func(t *testing.T) {
|
||||||
|
config.Config.Redis.Enabled = false
|
||||||
|
|
||||||
|
app := newWaveletApp(core.ProfileAll)
|
||||||
|
require.NotNil(t, app)
|
||||||
|
|
||||||
|
// 1. 验证插件挂载形态
|
||||||
|
_, ok := app.Plugin("cache_memory")
|
||||||
|
assert.True(t, ok, "cache_memory 必须挂载")
|
||||||
|
_, ok = app.Plugin("driver_inproc_worker")
|
||||||
|
assert.True(t, ok, "driver_inproc_worker 必须挂载")
|
||||||
|
_, ok = app.Plugin("driver_inproc_cron")
|
||||||
|
assert.True(t, ok, "driver_inproc_cron 必须挂载")
|
||||||
|
|
||||||
|
_, ok = app.Plugin("cache")
|
||||||
|
assert.False(t, ok, "分布式 cache 不得挂载")
|
||||||
|
_, ok = app.Plugin("driver_asynq_worker")
|
||||||
|
assert.False(t, ok, "asynq_worker 不得挂载")
|
||||||
|
_, ok = app.Plugin("driver_asynq_cron")
|
||||||
|
assert.False(t, ok, "asynq_cron 不得挂载")
|
||||||
|
|
||||||
|
// 2. 注册测试任务与 Cron 定时
|
||||||
|
var taskExecuted atomic.Int32
|
||||||
|
var cronExecuted atomic.Int32
|
||||||
|
|
||||||
|
app.Context().Tasks().Register("test:inproc_task", func(ctx context.Context, payload []byte) error {
|
||||||
|
if string(payload) == "payload_unplugged" {
|
||||||
|
taskExecuted.Add(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, extpoints.WithTaskTimeout(3*time.Second))
|
||||||
|
|
||||||
|
app.Context().Schedules().RegisterCron("* * * * * *", "test:inproc_cron", []byte("cron_ping"))
|
||||||
|
app.Context().Tasks().Register("test:inproc_cron", func(ctx context.Context, payload []byte) error {
|
||||||
|
if string(payload) == "cron_ping" {
|
||||||
|
cronExecuted.Add(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 启动应用
|
||||||
|
bootCtx, bootCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer bootCancel()
|
||||||
|
require.NoError(t, app.Start(bootCtx))
|
||||||
|
|
||||||
|
// 4. 验证 CacheService 操作
|
||||||
|
cacheSvc, err := core.Inject[contracts.CacheService](app.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, cacheSvc)
|
||||||
|
|
||||||
|
reqCtx := context.Background()
|
||||||
|
require.NoError(t, cacheSvc.Set(reqCtx, "unplugged_key", "value_123", time.Minute))
|
||||||
|
var val string
|
||||||
|
require.NoError(t, cacheSvc.Get(reqCtx, "unplugged_key", &val))
|
||||||
|
assert.Equal(t, "value_123", val)
|
||||||
|
|
||||||
|
// 5. 验证异步 Worker 任务分发与执行
|
||||||
|
taskSvc, err := core.Inject[contracts.TaskService](app.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, taskSvc)
|
||||||
|
|
||||||
|
taskID, err := taskSvc.Dispatch(reqCtx, "test:inproc_task", []byte("payload_unplugged"), "unit_test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, taskID)
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return taskExecuted.Load() >= 1
|
||||||
|
}, 3*time.Second, 50*time.Millisecond, "内存 Worker 应在进程内顺利执行任务")
|
||||||
|
|
||||||
|
// 6. 验证 Cron 定时触发
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return cronExecuted.Load() >= 1
|
||||||
|
}, 3*time.Second, 100*time.Millisecond, "内存 Cron 驱动应成功触发定时任务")
|
||||||
|
|
||||||
|
// 7. 优雅关闭
|
||||||
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer stopCancel()
|
||||||
|
require.NoError(t, app.Stop(stopCtx))
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 场景 2: 插入 Redis (Distributed Cluster 模式)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
t.Run("Scenario_Plugged_Redis_Mode", func(t *testing.T) {
|
||||||
|
config.Config.Redis.Enabled = true
|
||||||
|
|
||||||
|
app := newWaveletApp(core.ProfileAll)
|
||||||
|
require.NotNil(t, app)
|
||||||
|
|
||||||
|
// 1. 验证插件挂载形态
|
||||||
|
_, ok := app.Plugin("cache")
|
||||||
|
assert.True(t, ok, "分布式 cache 必须挂载")
|
||||||
|
_, ok = app.Plugin("driver_asynq_worker")
|
||||||
|
assert.True(t, ok, "driver_asynq_worker 必须挂载")
|
||||||
|
_, ok = app.Plugin("driver_asynq_cron")
|
||||||
|
assert.True(t, ok, "driver_asynq_cron 必须挂载")
|
||||||
|
|
||||||
|
_, ok = app.Plugin("cache_memory")
|
||||||
|
assert.False(t, ok, "纯内存 cache 不得挂载")
|
||||||
|
_, ok = app.Plugin("driver_inproc_worker")
|
||||||
|
assert.False(t, ok, "inproc_worker 不得挂载")
|
||||||
|
_, ok = app.Plugin("driver_inproc_cron")
|
||||||
|
assert.False(t, ok, "inproc_cron 不得挂载")
|
||||||
|
|
||||||
|
// 2. 注册测试任务
|
||||||
|
var asynqTaskExecuted atomic.Int32
|
||||||
|
app.Context().Tasks().Register("test:asynq_task", func(ctx context.Context, payload []byte) error {
|
||||||
|
if string(payload) == "payload_plugged" {
|
||||||
|
asynqTaskExecuted.Add(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, extpoints.WithTaskTimeout(3*time.Second))
|
||||||
|
|
||||||
|
// 3. 启动应用 (连接真实运行中的 Redis 6379)
|
||||||
|
bootCtx, bootCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer bootCancel()
|
||||||
|
require.NoError(t, app.Start(bootCtx))
|
||||||
|
|
||||||
|
// 4. 验证 CacheService 操作 (L1 RAM + L2 Redis)
|
||||||
|
cacheSvc, err := core.Inject[contracts.CacheService](app.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, cacheSvc)
|
||||||
|
|
||||||
|
reqCtx := context.Background()
|
||||||
|
testKey := fmt.Sprintf("plugged_key_%d", time.Now().UnixNano())
|
||||||
|
require.NoError(t, cacheSvc.Set(reqCtx, testKey, "value_redis_cluster", time.Minute))
|
||||||
|
|
||||||
|
var val string
|
||||||
|
require.NoError(t, cacheSvc.Get(reqCtx, testKey, &val))
|
||||||
|
assert.Equal(t, "value_redis_cluster", val)
|
||||||
|
|
||||||
|
// 验证失效广播与删除
|
||||||
|
require.NoError(t, cacheSvc.Delete(reqCtx, testKey))
|
||||||
|
var valAfterDelete string
|
||||||
|
err = cacheSvc.Get(reqCtx, testKey, &valAfterDelete)
|
||||||
|
assert.ErrorIs(t, err, contracts.ErrCacheMiss)
|
||||||
|
|
||||||
|
// 5. 验证 Asynq Worker 任务分发与消费
|
||||||
|
taskSvc, err := core.Inject[contracts.TaskService](app.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, taskSvc)
|
||||||
|
|
||||||
|
taskID, err := taskSvc.Dispatch(reqCtx, "test:asynq_task", []byte("payload_plugged"), "unit_test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, taskID)
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return asynqTaskExecuted.Load() >= 1
|
||||||
|
}, 5*time.Second, 100*time.Millisecond, "Asynq Worker 应从 Redis 队列中成功消费并执行任务")
|
||||||
|
|
||||||
|
// 6. 优雅关闭
|
||||||
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer stopCancel()
|
||||||
|
require.NoError(t, app.Stop(stopCtx))
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 场景 3: 往复插拔连续切换 (拔出 → 插入 → 再拔出,验证时空可组合性与零残留)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
t.Run("Scenario_Dynamic_Plug_Unplug_Sequence", func(t *testing.T) {
|
||||||
|
for i := 1; i <= 2; i++ {
|
||||||
|
// 1. 拔出 Redis 运行
|
||||||
|
config.Config.Redis.Enabled = false
|
||||||
|
appUnplugged := newWaveletApp(core.ProfileAll)
|
||||||
|
require.NoError(t, appUnplugged.Start(context.Background()))
|
||||||
|
|
||||||
|
cacheSvc1, err := core.Inject[contracts.CacheService](appUnplugged.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cacheSvc1.Set(context.Background(), fmt.Sprintf("seq_key_%d", i), "seq_val_unplugged", time.Minute))
|
||||||
|
|
||||||
|
require.NoError(t, appUnplugged.Stop(context.Background()))
|
||||||
|
|
||||||
|
// 2. 插入 Redis 运行
|
||||||
|
config.Config.Redis.Enabled = true
|
||||||
|
appPlugged := newWaveletApp(core.ProfileAll)
|
||||||
|
require.NoError(t, appPlugged.Start(context.Background()))
|
||||||
|
|
||||||
|
cacheSvc2, err := core.Inject[contracts.CacheService](appPlugged.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cacheSvc2.Set(context.Background(), fmt.Sprintf("seq_key_%d", i), "seq_val_plugged", time.Minute))
|
||||||
|
|
||||||
|
require.NoError(t, appPlugged.Stop(context.Background()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+1
-1
@@ -84,4 +84,4 @@ DELETE FROM w_system_configs WHERE key IN (
|
|||||||
);
|
);
|
||||||
DROP TABLE IF EXISTS w_templates;
|
DROP TABLE IF EXISTS w_templates;
|
||||||
DROP TABLE IF EXISTS w_system_configs;
|
DROP TABLE IF EXISTS w_system_configs;
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_system_configs (
|
||||||
|
key VARCHAR(64) PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
type VARCHAR(32) NOT NULL DEFAULT 'system',
|
||||||
|
visibility INTEGER NOT NULL DEFAULT 0,
|
||||||
|
description VARCHAR(255),
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_templates (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
key VARCHAR(80) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL DEFAULT 'email',
|
||||||
|
subject VARCHAR(255),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
description VARCHAR(255),
|
||||||
|
is_system BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_templates_is_system ON w_templates (is_system);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_templates_created_at ON w_templates (created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_templates_updated_at ON w_templates (updated_at);
|
||||||
|
|
||||||
|
-- Seed system configs (all default platform configs)
|
||||||
|
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at) VALUES
|
||||||
|
('cap_login_enabled', 'false', 'system', 1, '是否启用登录人机验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('cap_auto_solve', 'true', 'system', 1, '打开页面后是否自动开始计算,关闭则需用户手动点击触发', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('cap_challenge_count', '1', 'system', 0, '客户端需求解的 PoW 难题总数,默认 1,推荐 1~5', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('cap_challenge_size', '32', 'system', 0, '人机验证盐值长度', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('cap_challenge_difficulty', '4', 'system', 0, '人机验证 PoW 难度(目标前缀长度)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('cap_challenge_ttl_seconds', '600', 'system', 0, '人机验证难题有效时间(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('cap_token_ttl_seconds', '1200', 'system', 0, '人机验证兑换凭证有效时间(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('server_address', '', 'system', 0, '服务器地址(用于跨域源控制,不设定则允许任意源)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('smtp_host', '', 'system', 0, 'SMTP 服务器地址(例如 smtp.example.com)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('smtp_port', '587', 'system', 0, 'SMTP 端口(例如 587 或 465)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('smtp_username', '', 'system', 0, 'SMTP 账户(如 sender@example.com)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('smtp_password', '', 'system', 0, 'SMTP 访问凭证(授权码/密码)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('upload_allowed_extensions', 'jpg,png,webp', 'system', 1, '允许上传的图片扩展名(逗号分隔)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('site_name', 'Wavelet', 'system', 1, '系统平台的展示名称', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('password_login_enabled', 'true', 'system', 1, '是否允许使用账号密码登录', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('registration_enabled', 'true', 'system', 1, '控制普通用户是否可以自主注册(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('password_register_enabled', 'true', 'system', 1, '是否允许通过密码创建本地账号', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('oidc_login_enabled', 'true', 'system', 1, '是否允许使用第三方 OIDC 认证源登录', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('max_api_keys_per_user', '5', 'business', 1, '限制每个普通用户可以创建的 API Key 最大数量', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('email_login_verification_enabled', 'false', 'system', 1, '是否开启邮箱登录验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('email_register_verification_enabled', 'false', 'system', 1, '是否开启邮箱注册验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('menu_display_config', '{}', 'system', 1, '目录显示配置(JSON 字符串,格式为 {url: enabled})', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('search_engine_indexing_enabled', 'false', 'system', 1, '是否允许搜索引擎爬取/检索该站点(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('update_upstream_repository', 'Rain-kl/Wavelet', 'system', 0, 'GitHub Actions Release 上游仓库(owner/repo 或 GitHub 仓库地址)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('storage_config', '{"driver":"local","local":{"root":"."},"s3":{"region":"us-east-1"},"r2":{"region":"auto"},"minio":{"region":"us-east-1","path_style":true},"oss":{},"webdav":{}}', 'system', 0, '文件存储驱动及连接配置(JSON)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('disk_cache_max_size_mb', '1024', 'system', 0, '磁盘缓存最大空间大小(MB)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('disk_cache_ttl_minutes', '1440', 'system', 0, '磁盘缓存默认有效期(分钟)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('disk_cache_lru_enabled', 'true', 'system', 0, '是否启用 LRU 淘汰机制', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('file_access_whitelist', '["avatar"]', 'system', 0, '免登录访问的文件业务类型白名单 (JSON 数组)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('login_session_ttl_hours', '168', 'system', 0, '登录会话过期时间(小时)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('log_database', '', 'system', 0, '当前日志主库(postgres/sqlite/clickhouse),由切换任务写入', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
('log_db_migration', '', 'system', 0, '日志库迁移冻结标记(空或 migrating)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO w_templates (id, key, name, type, subject, content, description, is_system, created_at, updated_at) VALUES
|
||||||
|
(1, 'login_email', '登录验证码邮件', 'email', 'Wavelet 登录验证码', '<h3>Wavelet 登录验证</h3><p>您的登录验证码为:<strong>{{.Code}}</strong>,5分钟内有效,请勿将验证码泄露给他人。</p>', '用户密码登录时发送的验证码邮件模板,支持变量:{{.Code}}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||||
|
(2, 'register_email', '注册验证码邮件', 'email', 'Wavelet 注册验证码', '<h3>Wavelet 注册验证</h3><p>您的注册验证码为:<strong>{{.Code}}</strong>,5分钟内有效,请勿泄露给他人。</p>', '用户注册时发送的验证码邮件模板,支持变量:{{.Code}}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DELETE FROM w_templates WHERE key IN ('login_email', 'register_email');
|
||||||
|
DELETE FROM w_system_configs WHERE key IN (
|
||||||
|
'cap_login_enabled', 'cap_auto_solve', 'cap_challenge_count', 'cap_challenge_size',
|
||||||
|
'cap_challenge_difficulty', 'cap_challenge_ttl_seconds', 'cap_token_ttl_seconds',
|
||||||
|
'server_address', 'smtp_host', 'smtp_port', 'smtp_username', 'smtp_password',
|
||||||
|
'upload_allowed_extensions', 'site_name', 'password_login_enabled', 'registration_enabled',
|
||||||
|
'password_register_enabled', 'oidc_login_enabled', 'max_api_keys_per_user',
|
||||||
|
'email_login_verification_enabled', 'email_register_verification_enabled',
|
||||||
|
'menu_display_config', 'search_engine_indexing_enabled', 'update_upstream_repository',
|
||||||
|
'storage_config', 'disk_cache_max_size_mb', 'disk_cache_ttl_minutes', 'disk_cache_lru_enabled',
|
||||||
|
'file_access_whitelist', 'login_session_ttl_hours', 'log_database', 'log_db_migration'
|
||||||
|
);
|
||||||
|
DROP TABLE IF EXISTS w_templates;
|
||||||
|
DROP TABLE IF EXISTS w_system_configs;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"Wavelet/core/extpoints"
|
"Wavelet/core/extpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var adminMigrations embed.FS
|
var adminMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the admin plugin.
|
// Option configures the admin plugin.
|
||||||
|
|||||||
+1
-7
@@ -42,17 +42,11 @@ CREATE TABLE IF NOT EXISTS w_access_tokens (
|
|||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_access_tokens_user_id ON w_access_tokens (user_id);
|
CREATE INDEX IF NOT EXISTS idx_w_access_tokens_user_id ON w_access_tokens (user_id);
|
||||||
|
|
||||||
-- Seed: login session TTL
|
|
||||||
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
|
||||||
VALUES ('login_session_ttl_hours', '0', 'system', 0, '登录会话过期时间 (小时,0表示浏览器关闭后自动退出,-1表示永不过期)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT (key) DO NOTHING;
|
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
DELETE FROM w_system_configs WHERE key = 'login_session_ttl_hours';
|
|
||||||
DROP TABLE IF EXISTS w_access_tokens;
|
DROP TABLE IF EXISTS w_access_tokens;
|
||||||
DROP TABLE IF EXISTS w_external_accounts;
|
DROP TABLE IF EXISTS w_external_accounts;
|
||||||
DROP TABLE IF EXISTS w_auth_sources;
|
DROP TABLE IF EXISTS w_auth_sources;
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_auth_sources (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
name VARCHAR(80) NOT NULL UNIQUE,
|
||||||
|
type VARCHAR(20) NOT NULL,
|
||||||
|
display_name VARCHAR(100),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
client_id VARCHAR(255),
|
||||||
|
client_secret VARCHAR(1024),
|
||||||
|
openid_discovery_url VARCHAR(1024),
|
||||||
|
scopes VARCHAR(255),
|
||||||
|
icon_url VARCHAR(1024),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_auth_sources_is_active ON w_auth_sources (is_active);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_external_accounts (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
auth_source_id BIGINT,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
external_id VARCHAR(255) NOT NULL,
|
||||||
|
external_username VARCHAR(255),
|
||||||
|
email VARCHAR(255),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_external_accounts_auth_source_id ON w_external_accounts (auth_source_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_external_accounts_user_id ON w_external_accounts (user_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_w_external_accounts_source_external ON w_external_accounts (auth_source_id, external_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_access_tokens (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
description VARCHAR(255),
|
||||||
|
is_admin BOOLEAN DEFAULT 0,
|
||||||
|
expires_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_access_tokens_user_id ON w_access_tokens (user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_access_tokens;
|
||||||
|
DROP TABLE IF EXISTS w_external_accounts;
|
||||||
|
DROP TABLE IF EXISTS w_auth_sources;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"Wavelet/core/extpoints"
|
"Wavelet/core/extpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var authMigrations embed.FS
|
var authMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the auth plugin.
|
// Option configures the auth plugin.
|
||||||
|
|||||||
+1
-1
@@ -90,4 +90,4 @@ DROP TABLE IF EXISTS w_push_events;
|
|||||||
DROP TABLE IF EXISTS w_message_pairing_codes;
|
DROP TABLE IF EXISTS w_message_pairing_codes;
|
||||||
DROP TABLE IF EXISTS w_message_bindings;
|
DROP TABLE IF EXISTS w_message_bindings;
|
||||||
DROP TABLE IF EXISTS w_message_channels;
|
DROP TABLE IF EXISTS w_message_channels;
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_message_channels (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
type VARCHAR(32) NOT NULL,
|
||||||
|
owner_scope VARCHAR(16) NOT NULL DEFAULT 'system',
|
||||||
|
owner_id BIGINT NULL,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
credentials TEXT NOT NULL DEFAULT '',
|
||||||
|
extra TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_message_channels_type ON w_message_channels (type);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_message_bindings (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
channel_id BIGINT NOT NULL,
|
||||||
|
platform_user_id VARCHAR(128) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_w_message_bindings_channel_platform
|
||||||
|
ON w_message_bindings (channel_id, platform_user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_message_bindings_user ON w_message_bindings (user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_message_pairing_codes (
|
||||||
|
code VARCHAR(16) PRIMARY KEY,
|
||||||
|
channel_id BIGINT NOT NULL,
|
||||||
|
platform_user_id VARCHAR(128) NOT NULL,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_message_pairing_lookup
|
||||||
|
ON w_message_pairing_codes (channel_id, platform_user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_push_events (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
event_key VARCHAR(80) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
task_type VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
channels TEXT NOT NULL DEFAULT '',
|
||||||
|
targets TEXT NOT NULL DEFAULT '',
|
||||||
|
template TEXT NOT NULL DEFAULT '',
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_w_push_events_key ON w_push_events(event_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_push_events_enabled ON w_push_events(enabled);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_push_events_task_type ON w_push_events(task_type);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_push_channels (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
name VARCHAR(80) NOT NULL,
|
||||||
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
type VARCHAR(50) NOT NULL DEFAULT 'custom',
|
||||||
|
token VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
url TEXT NOT NULL DEFAULT '',
|
||||||
|
other TEXT NOT NULL DEFAULT '',
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_w_push_channels_name ON w_push_channels(name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_push_channels_enabled ON w_push_channels(enabled);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_push_histories (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
event_key VARCHAR(80) NOT NULL,
|
||||||
|
channel VARCHAR(50) NOT NULL,
|
||||||
|
target VARCHAR(255) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
level VARCHAR(20) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
error_msg TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_push_histories_event ON w_push_histories(event_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_push_histories_created ON w_push_histories(created_at);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_push_histories;
|
||||||
|
DROP TABLE IF EXISTS w_push_channels;
|
||||||
|
DROP TABLE IF EXISTS w_push_events;
|
||||||
|
DROP TABLE IF EXISTS w_message_pairing_codes;
|
||||||
|
DROP TABLE IF EXISTS w_message_bindings;
|
||||||
|
DROP TABLE IF EXISTS w_message_channels;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"Wavelet/pkg/util"
|
"Wavelet/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var mgMigrations embed.FS
|
var mgMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the message_gateway plugin.
|
// Option configures the message_gateway plugin.
|
||||||
|
|||||||
+6
-2
@@ -1,4 +1,5 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
CREATE TABLE IF NOT EXISTS w_user_access_logs (
|
CREATE TABLE IF NOT EXISTS w_user_access_logs (
|
||||||
id BIGINT NOT NULL,
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL DEFAULT 0,
|
user_id BIGINT NOT NULL DEFAULT 0,
|
||||||
@@ -11,10 +12,13 @@ CREATE TABLE IF NOT EXISTS w_user_access_logs (
|
|||||||
latency BIGINT NOT NULL DEFAULT 0,
|
latency BIGINT NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id, created_at)
|
PRIMARY KEY (id, created_at)
|
||||||
) PARTITION BY RANGE (created_at);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_user_access_logs_user_id ON w_user_access_logs (user_id, created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_w_user_access_logs_user_id ON w_user_access_logs (user_id, created_at DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_user_access_logs_created_at ON w_user_access_logs (created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_w_user_access_logs_created_at ON w_user_access_logs (created_at DESC);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
DROP TABLE IF EXISTS w_user_access_logs;
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_user_access_logs;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_user_access_logs (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
path VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
method VARCHAR(16) NOT NULL DEFAULT '',
|
||||||
|
ip VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
user_agent TEXT NOT NULL DEFAULT '',
|
||||||
|
headers TEXT NOT NULL DEFAULT '',
|
||||||
|
status INTEGER NOT NULL DEFAULT 0,
|
||||||
|
latency BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id, created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_user_access_logs_user_id ON w_user_access_logs (user_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_user_access_logs_created_at ON w_user_access_logs (created_at DESC);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_user_access_logs;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"Wavelet/plugins/domain/risk_control/logstore"
|
"Wavelet/plugins/domain/risk_control/logstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed logstore/migrations/*.sql
|
//go:embed logstore/migrations/*/*.sql
|
||||||
var riskControlMigrations embed.FS
|
var riskControlMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the risk_control plugin.
|
// Option configures the risk_control plugin.
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
CREATE TABLE IF NOT EXISTS w_upload_stats (
|
|
||||||
dimension VARCHAR(32) NOT NULL,
|
|
||||||
stat_key VARCHAR(64) NOT NULL DEFAULT '',
|
|
||||||
file_count BIGINT NOT NULL DEFAULT 0,
|
|
||||||
file_size BIGINT NOT NULL DEFAULT 0,
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (dimension, stat_key)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_uploads_status_created_at ON w_uploads (status, created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_uploads_hash_file_size_status ON w_uploads (hash, file_size, status);
|
|
||||||
|
|
||||||
ALTER TABLE w_uploads ADD COLUMN IF NOT EXISTS access_mode INTEGER NOT NULL DEFAULT 0;
|
|
||||||
UPDATE w_uploads SET access_mode = 1 WHERE type = 'avatar';
|
|
||||||
|
|
||||||
-- Backfill upload stats
|
|
||||||
INSERT INTO w_upload_stats (dimension, stat_key, file_count, file_size)
|
|
||||||
SELECT 'total', '', COUNT(*), COALESCE(SUM(file_size), 0)
|
|
||||||
FROM w_uploads
|
|
||||||
WHERE status != 'deleted'
|
|
||||||
ON CONFLICT (dimension, stat_key) DO UPDATE SET
|
|
||||||
file_count = EXCLUDED.file_count,
|
|
||||||
file_size = EXCLUDED.file_size,
|
|
||||||
updated_at = CURRENT_TIMESTAMP;
|
|
||||||
|
|
||||||
INSERT INTO w_upload_stats (dimension, stat_key, file_count, file_size)
|
|
||||||
SELECT
|
|
||||||
'type',
|
|
||||||
COALESCE(NULLIF(type, ''), 'generic'),
|
|
||||||
COUNT(*),
|
|
||||||
COALESCE(SUM(file_size), 0)
|
|
||||||
FROM w_uploads
|
|
||||||
WHERE status != 'deleted'
|
|
||||||
GROUP BY COALESCE(NULLIF(type, ''), 'generic')
|
|
||||||
ON CONFLICT (dimension, stat_key) DO UPDATE SET
|
|
||||||
file_count = EXCLUDED.file_count,
|
|
||||||
file_size = EXCLUDED.file_size,
|
|
||||||
updated_at = CURRENT_TIMESTAMP;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP TABLE IF EXISTS w_upload_stats;
|
|
||||||
DROP INDEX IF EXISTS idx_w_uploads_hash_file_size_status;
|
|
||||||
DROP INDEX IF EXISTS idx_w_uploads_status_created_at;
|
|
||||||
ALTER TABLE w_uploads DROP COLUMN IF EXISTS access_mode;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_uploads (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
file_path VARCHAR(500) NOT NULL,
|
||||||
|
file_size BIGINT NOT NULL,
|
||||||
|
mime_type VARCHAR(100) NOT NULL,
|
||||||
|
extension VARCHAR(50) NOT NULL,
|
||||||
|
hash VARCHAR(64),
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
access_mode INTEGER NOT NULL DEFAULT 0,
|
||||||
|
metadata JSONB,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_user_id ON w_uploads (user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_file_path ON w_uploads (file_path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_hash ON w_uploads (hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_type ON w_uploads (type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_status_created_at ON w_uploads (status, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_hash_file_size_status ON w_uploads (hash, file_size, status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_upload_stats (
|
||||||
|
dimension VARCHAR(32) NOT NULL,
|
||||||
|
stat_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
file_count BIGINT NOT NULL DEFAULT 0,
|
||||||
|
file_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (dimension, stat_key)
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_upload_stats;
|
||||||
|
DROP TABLE IF EXISTS w_uploads;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_uploads (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
file_path VARCHAR(500) NOT NULL,
|
||||||
|
file_size BIGINT NOT NULL,
|
||||||
|
mime_type VARCHAR(100) NOT NULL,
|
||||||
|
extension VARCHAR(50) NOT NULL,
|
||||||
|
hash VARCHAR(64),
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
access_mode INTEGER NOT NULL DEFAULT 0,
|
||||||
|
metadata TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_user_id ON w_uploads (user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_file_path ON w_uploads (file_path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_hash ON w_uploads (hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_type ON w_uploads (type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_status_created_at ON w_uploads (status, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_hash_file_size_status ON w_uploads (hash, file_size, status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS w_upload_stats (
|
||||||
|
dimension VARCHAR(32) NOT NULL,
|
||||||
|
stat_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
file_count BIGINT NOT NULL DEFAULT 0,
|
||||||
|
file_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (dimension, stat_key)
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_upload_stats;
|
||||||
|
DROP TABLE IF EXISTS w_uploads;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"Wavelet/plugins/domain/upload/task"
|
"Wavelet/plugins/domain/upload/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var uploadMigrations embed.FS
|
var uploadMigrations embed.FS
|
||||||
|
|
||||||
// Plugin implements core.Plugin to provide file upload and media serving domain services.
|
// Plugin implements core.Plugin to provide file upload and media serving domain services.
|
||||||
|
|||||||
+1
-1
@@ -33,4 +33,4 @@ ON CONFLICT (username) DO NOTHING;
|
|||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
DELETE FROM w_users WHERE username = 'system';
|
DELETE FROM w_users WHERE username = 'system';
|
||||||
DROP TABLE IF EXISTS w_users;
|
DROP TABLE IF EXISTS w_users;
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_users (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
username VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255),
|
||||||
|
nickname VARCHAR(255),
|
||||||
|
email VARCHAR(255),
|
||||||
|
avatar_url VARCHAR(255),
|
||||||
|
is_active BOOLEAN DEFAULT 1,
|
||||||
|
is_admin BOOLEAN DEFAULT 0,
|
||||||
|
bio VARCHAR(500),
|
||||||
|
phone VARCHAR(32),
|
||||||
|
gender VARCHAR(16),
|
||||||
|
website VARCHAR(255),
|
||||||
|
location VARCHAR(255),
|
||||||
|
last_login_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_users_email ON w_users (email);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_users_is_active ON w_users (is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_users_last_login_at ON w_users (last_login_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_users_created_at ON w_users (created_at);
|
||||||
|
|
||||||
|
-- Seed system user
|
||||||
|
INSERT INTO w_users (id, username, password, nickname, avatar_url, is_active, is_admin, last_login_at, created_at, updated_at)
|
||||||
|
VALUES (999, 'system', '*', '系统', '', 1, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (username) DO NOTHING;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DELETE FROM w_users WHERE username = 'system';
|
||||||
|
DROP TABLE IF EXISTS w_users;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"Wavelet/core/extpoints"
|
"Wavelet/core/extpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var userMigrations embed.FS
|
var userMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the user plugin.
|
// Option configures the user plugin.
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,5 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
CREATE TABLE IF NOT EXISTS w_schedules (
|
CREATE TABLE IF NOT EXISTS w_schedules (
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
name VARCHAR(128) NOT NULL,
|
name VARCHAR(128) NOT NULL,
|
||||||
@@ -15,6 +16,9 @@ CREATE INDEX IF NOT EXISTS idx_w_schedules_is_active ON w_schedules (is_active);
|
|||||||
INSERT INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
|
INSERT INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
|
||||||
VALUES (1, '系统定期垃圾清理', 'system_cleanup', '0 3 * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (1, '系统定期垃圾清理', 'system_cleanup', '0 3 * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
DROP TABLE IF EXISTS w_schedules;
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_schedules;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_schedules (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
task_type VARCHAR(64) NOT NULL,
|
||||||
|
cron VARCHAR(64) NOT NULL,
|
||||||
|
payload TEXT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_schedules_is_active ON w_schedules (is_active);
|
||||||
|
|
||||||
|
-- Seed initial cleanup task
|
||||||
|
INSERT INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
|
||||||
|
VALUES (1, '系统定期垃圾清理', 'system_cleanup', '0 3 * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_schedules;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -16,9 +16,10 @@ import (
|
|||||||
|
|
||||||
"Wavelet/core"
|
"Wavelet/core"
|
||||||
"Wavelet/core/contracts"
|
"Wavelet/core/contracts"
|
||||||
|
"Wavelet/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var cronMigrations embed.FS
|
var cronMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the Asynq cron scheduler driver plugin.
|
// Option configures the Asynq cron scheduler driver plugin.
|
||||||
@@ -148,7 +149,25 @@ func (p *Plugin) Start(_ context.Context) error {
|
|||||||
opts.Location = p.location
|
opts.Location = p.location
|
||||||
}
|
}
|
||||||
|
|
||||||
p.scheduler = asynq.NewScheduler(p.redisOpt, opts)
|
opt := p.redisOpt
|
||||||
|
if opt == nil {
|
||||||
|
if RedisOpt != nil {
|
||||||
|
opt = RedisOpt
|
||||||
|
} else {
|
||||||
|
redisCfg := config.Config.Redis
|
||||||
|
addr := "127.0.0.1:6379"
|
||||||
|
if len(redisCfg.Addrs) > 0 && redisCfg.Addrs[0] != "" {
|
||||||
|
addr = redisCfg.Addrs[0]
|
||||||
|
}
|
||||||
|
opt = asynq.RedisClientOpt{
|
||||||
|
Addr: addr,
|
||||||
|
Username: redisCfg.Username,
|
||||||
|
Password: redisCfg.Password,
|
||||||
|
DB: redisCfg.DB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.scheduler = asynq.NewScheduler(opt, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.coreCtx != nil && p.coreCtx.Schedules() != nil {
|
if p.coreCtx != nil && p.coreCtx.Schedules() != nil {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package driver_asynq_worker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"Wavelet/core/extpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TaskParam 任务参数定义
|
// TaskParam 任务参数定义
|
||||||
@@ -61,30 +63,65 @@ func GetDispatchableTasks() []TaskMeta {
|
|||||||
return metas
|
return metas
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTaskMeta 根据任务类型获取元数据
|
var (
|
||||||
func GetTaskMeta(taskType string) *TaskMeta {
|
activeTaskRegMutex sync.RWMutex
|
||||||
dispatchableTasksMutex.RLock()
|
activeTaskReg extpoints.TaskExtension
|
||||||
defer dispatchableTasksMutex.RUnlock()
|
)
|
||||||
for _, t := range dispatchableTasks {
|
|
||||||
if t.Type == taskType {
|
// SetActiveTaskExtension sets the active task extension registry for task resolution.
|
||||||
copied := t
|
func SetActiveTaskExtension(reg extpoints.TaskExtension) {
|
||||||
return &copied
|
activeTaskRegMutex.Lock()
|
||||||
|
defer activeTaskRegMutex.Unlock()
|
||||||
|
activeTaskReg = reg
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFromActiveTaskExtension(taskType string) *TaskMeta {
|
||||||
|
activeTaskRegMutex.RLock()
|
||||||
|
defer activeTaskRegMutex.RUnlock()
|
||||||
|
if activeTaskReg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if td, ok := activeTaskReg.Get(taskType); ok {
|
||||||
|
return &TaskMeta{
|
||||||
|
Type: td.Pattern,
|
||||||
|
Name: td.Pattern,
|
||||||
|
AsynqTask: td.Pattern,
|
||||||
|
Queue: "default",
|
||||||
|
Retryable: td.Retry > 0,
|
||||||
|
MaxRetry: td.Retry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTaskMetaByAsynqTask 根据 Asynq 任务名称获取元数据
|
// GetTaskMeta 根据任务类型获取元数据
|
||||||
func GetTaskMetaByAsynqTask(asynqTask string) *TaskMeta {
|
func GetTaskMeta(taskType string) *TaskMeta {
|
||||||
dispatchableTasksMutex.RLock()
|
dispatchableTasksMutex.RLock()
|
||||||
defer dispatchableTasksMutex.RUnlock()
|
|
||||||
for _, t := range dispatchableTasks {
|
for _, t := range dispatchableTasks {
|
||||||
if t.AsynqTask == asynqTask {
|
if t.Type == taskType {
|
||||||
copied := t
|
copied := t
|
||||||
|
dispatchableTasksMutex.RUnlock()
|
||||||
return &copied
|
return &copied
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
dispatchableTasksMutex.RUnlock()
|
||||||
|
|
||||||
|
return getFromActiveTaskExtension(taskType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskMetaByAsynqTask 根据 Asynq 任务名称获取元数据
|
||||||
|
func GetTaskMetaByAsynqTask(asynqTask string) *TaskMeta {
|
||||||
|
dispatchableTasksMutex.RLock()
|
||||||
|
for _, t := range dispatchableTasks {
|
||||||
|
if t.AsynqTask == asynqTask {
|
||||||
|
copied := t
|
||||||
|
dispatchableTasksMutex.RUnlock()
|
||||||
|
return &copied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatchableTasksMutex.RUnlock()
|
||||||
|
|
||||||
|
return getFromActiveTaskExtension(asynqTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRegisteredAsynqTasks 返回所有已注册的 Asynq 任务名称,以便动态注册路由
|
// GetRegisteredAsynqTasks 返回所有已注册的 Asynq 任务名称,以便动态注册路由
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,5 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
CREATE TABLE IF NOT EXISTS w_task_executions (
|
CREATE TABLE IF NOT EXISTS w_task_executions (
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
task_id VARCHAR(128) NOT NULL UNIQUE,
|
task_id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
@@ -23,6 +24,9 @@ CREATE INDEX IF NOT EXISTS idx_w_task_executions_task_type ON w_task_executions
|
|||||||
CREATE INDEX IF NOT EXISTS idx_w_task_executions_status ON w_task_executions (status);
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_status ON w_task_executions (status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_task_executions_started_at ON w_task_executions (started_at);
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_started_at ON w_task_executions (started_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_w_task_executions_created_at ON w_task_executions (created_at);
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_created_at ON w_task_executions (created_at);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
DROP TABLE IF EXISTS w_task_executions;
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_task_executions;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS w_task_executions (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
task_id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
task_type VARCHAR(64) NOT NULL,
|
||||||
|
task_name VARCHAR(128),
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
retryable BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
max_retry INTEGER NOT NULL DEFAULT 0,
|
||||||
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
log TEXT,
|
||||||
|
error_message TEXT,
|
||||||
|
result TEXT,
|
||||||
|
started_at DATETIME,
|
||||||
|
finished_at DATETIME,
|
||||||
|
duration BIGINT,
|
||||||
|
payload TEXT,
|
||||||
|
triggered_by VARCHAR(32) NOT NULL DEFAULT 'system',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_task_type ON w_task_executions (task_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_status ON w_task_executions (status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_started_at ON w_task_executions (started_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_task_executions_created_at ON w_task_executions (created_at);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS w_task_executions;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"Wavelet/core"
|
"Wavelet/core"
|
||||||
"Wavelet/core/contracts"
|
"Wavelet/core/contracts"
|
||||||
|
"Wavelet/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,7 +24,7 @@ const (
|
|||||||
defaultShutdownTimeout = 10 * time.Second
|
defaultShutdownTimeout = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*/*.sql
|
||||||
var workerMigrations embed.FS
|
var workerMigrations embed.FS
|
||||||
|
|
||||||
// Option configures the Asynq worker driver plugin.
|
// Option configures the Asynq worker driver plugin.
|
||||||
@@ -131,11 +132,13 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
|||||||
// 1. Provide contracts.TaskService
|
// 1. Provide contracts.TaskService
|
||||||
p.taskSvc = &taskServiceImpl{}
|
p.taskSvc = &taskServiceImpl{}
|
||||||
core.Provide[contracts.TaskService](ctx, p.taskSvc)
|
core.Provide[contracts.TaskService](ctx, p.taskSvc)
|
||||||
|
SetActiveTaskExtension(ctx.Tasks())
|
||||||
|
|
||||||
// 2. Register migrations for w_task_executions table
|
// 2. Register migrations for w_task_executions table
|
||||||
ctx.Migrations().Register("driver_asynq_worker", workerMigrations)
|
ctx.Migrations().Register("driver_asynq_worker", workerMigrations)
|
||||||
|
|
||||||
ctx.OnDispose(func() error {
|
ctx.OnDispose(func() error {
|
||||||
|
SetActiveTaskExtension(nil)
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), p.shutdownTimeout)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), p.shutdownTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return p.Stop(shutdownCtx)
|
return p.Stop(shutdownCtx)
|
||||||
@@ -175,8 +178,26 @@ func (p *Plugin) Start(_ context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.server == nil {
|
if p.server == nil {
|
||||||
|
opt := p.redisOpt
|
||||||
|
if opt == nil {
|
||||||
|
if RedisOpt != nil {
|
||||||
|
opt = RedisOpt
|
||||||
|
} else {
|
||||||
|
redisCfg := config.Config.Redis
|
||||||
|
addr := "127.0.0.1:6379"
|
||||||
|
if len(redisCfg.Addrs) > 0 && redisCfg.Addrs[0] != "" {
|
||||||
|
addr = redisCfg.Addrs[0]
|
||||||
|
}
|
||||||
|
opt = asynq.RedisClientOpt{
|
||||||
|
Addr: addr,
|
||||||
|
Username: redisCfg.Username,
|
||||||
|
Password: redisCfg.Password,
|
||||||
|
DB: redisCfg.DB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
p.server = asynq.NewServer(
|
p.server = asynq.NewServer(
|
||||||
p.redisOpt,
|
opt,
|
||||||
asynq.Config{
|
asynq.Config{
|
||||||
Concurrency: p.concurrency,
|
Concurrency: p.concurrency,
|
||||||
Queues: p.queues,
|
Queues: p.queues,
|
||||||
|
|||||||
+10
-7
@@ -81,14 +81,16 @@ func (p *Plugin) Name() string {
|
|||||||
// Apply mounts the multi-layer cache service into the Context.
|
// Apply mounts the multi-layer cache service into the Context.
|
||||||
func (p *Plugin) Apply(ctx *core.Context) error {
|
func (p *Plugin) Apply(ctx *core.Context) error {
|
||||||
redisClient := p.redisClient
|
redisClient := p.redisClient
|
||||||
if redisClient == nil && Redis == nil {
|
if redisClient == nil {
|
||||||
var err error
|
if Redis == nil {
|
||||||
redisClient, err = InitRedis()
|
var err error
|
||||||
if err != nil {
|
redisClient, err = InitRedis()
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
redisClient = Redis
|
||||||
}
|
}
|
||||||
} else if redisClient == nil {
|
|
||||||
redisClient = Redis
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ramCache, err := ram.New[string, ramEntry](ram.Options{
|
ramCache, err := ram.New[string, ramEntry](ram.Options{
|
||||||
@@ -111,6 +113,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
|||||||
ctx.OnDispose(func() error {
|
ctx.OnDispose(func() error {
|
||||||
svc.stopPubSubListener()
|
svc.stopPubSubListener()
|
||||||
if p.redisClient == nil {
|
if p.redisClient == nil {
|
||||||
|
Redis = nil
|
||||||
if closeErr := redisClient.Close(); closeErr != nil && !errors.Is(closeErr, redis.ErrClosed) {
|
if closeErr := redisClient.Close(); closeErr != nil && !errors.Is(closeErr, redis.ErrClosed) {
|
||||||
return closeErr
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user