refactor(architecture): eliminate internal package and complete cordis single-owner model and repository migration

- Physically purged all legacy internal/ packages, centralized pkg/model/ and pkg/repository/
- Migrated domain models and database repositories into self-contained owner plugins (user, auth, message_gateway, admin, upload, risk_control)
- Decoupled cross-plugin interactions via pure core/contracts and typed EventBus
- Ensured 100% test coverage pass, zero data races (-race clean), and 0 lint issues in make code-check
This commit is contained in:
ryan
2026-08-28 08:40:43 +08:00
parent 1f348fd425
commit fb6a3edb89
323 changed files with 8222 additions and 17693 deletions
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package testhelper
// RegisterCleanup registers an extra cleanup hook invoked by SetupTestEnvironment.
func RegisterCleanup(fn func()) {
extraCleanups = append(extraCleanups, fn)
}
var extraCleanups []func()
func runExtraCleanups() {
for _, fn := range extraCleanups {
fn()
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package testhelper
import (
"github.com/Rain-kl/Wavelet/pkg/response"
"github.com/gin-gonic/gin"
)
// NewTestGinEngine 创建带 ErrorHandlerMiddleware 的 Gin 引擎,与生产环境错误响应行为一致。
func NewTestGinEngine(middlewares ...gin.HandlerFunc) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(response.ErrorHandlerMiddleware())
for _, middleware := range middlewares {
r.Use(middleware)
}
return r
}
+332
View File
@@ -0,0 +1,332 @@
// Copyright 2025 linux.do
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package testhelper 提供测试辅助工具
package testhelper
import (
"context"
"testing"
"time"
db "github.com/Rain-kl/Wavelet/pkg/persistence"
"github.com/alicebob/miniredis/v2"
"github.com/glebarez/sqlite"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/maintnotifications"
"gorm.io/gorm"
)
// SystemConfig 测试用系统配置表
type SystemConfig struct {
Key string `gorm:"primaryKey;size:64;not null"`
Value string `gorm:"type:text;not null"`
Type string `gorm:"size:32;not null"`
Visibility string `gorm:"size:32;not null;default:'hidden'"`
Description string `gorm:"size:255"`
}
// TableName 返回测试配置表表名
func (SystemConfig) TableName() string {
return "w_system_configs"
}
type userHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
Username string `gorm:"size:64;uniqueIndex;not null"`
Nickname string `gorm:"size:64;not null;default:''"`
Password string `gorm:"size:255;not null;default:''"`
Email string `gorm:"size:128;index;default:''"`
AvatarURL string `gorm:"size:255;default:''"`
IsAdmin bool `gorm:"default:false;not null"`
IsActive bool `gorm:"default:true;not null"`
NeedChangePassword bool `gorm:"default:false;not null"`
Bio string `gorm:"size:500;default:''"`
Phone string `gorm:"size:32;default:''"`
Gender string `gorm:"size:16;default:''"`
Website string `gorm:"size:255;default:''"`
Location string `gorm:"size:255;default:''"`
LastLoginAt time.Time
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (userHelper) TableName() string { return "w_users" }
type accessTokenHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
UserID uint64 `gorm:"not null;index"`
Name string `gorm:"size:64;not null"`
TokenHash string `gorm:"size:64;uniqueIndex;not null"`
MaskedToken string `gorm:"size:32;not null"`
IsAdmin bool `gorm:"default:false;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (accessTokenHelper) TableName() string { return "w_access_tokens" }
type authSourceHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
Type string `gorm:"size:32;not null"`
Name string `gorm:"size:64;not null"`
Enabled bool `gorm:"default:false;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (authSourceHelper) TableName() string { return "w_auth_sources" }
type externalAccountHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
UserID uint64 `gorm:"not null;index"`
AuthSourceType string `gorm:"size:32;not null;index"`
ExternalID string `gorm:"size:128;not null;index"`
Username string `gorm:"size:128;default:''"`
Email string `gorm:"size:128;default:''"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (externalAccountHelper) TableName() string { return "w_external_accounts" }
type uploadHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
UserID uint64 `gorm:"not null;index"`
FileName string `gorm:"size:255;not null"`
FilePath string `gorm:"size:500;not null"`
FileSize int64 `gorm:"not null"`
MimeType string `gorm:"size:128;not null"`
Extension string `gorm:"size:32;not null"`
Hash string `gorm:"size:64;index;not null;default:''"`
Type string `gorm:"size:50;not null;index"`
Status string `gorm:"size:20;not null;default:'pending'"`
AccessMode int `gorm:"not null;default:0"`
Metadata string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime;index"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (uploadHelper) TableName() string { return "w_uploads" }
type uploadStatHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
Dimension string `gorm:"size:32;not null;uniqueIndex:idx_stat_dimension_key"`
StatKey string `gorm:"size:100;not null;uniqueIndex:idx_stat_dimension_key"`
FileCount int64 `gorm:"not null;default:0"`
FileSize int64 `gorm:"not null;default:0"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (uploadStatHelper) TableName() string { return "w_upload_stats" }
type messageChannelHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
Type string `gorm:"size:32;not null"`
Name string `gorm:"size:64;not null"`
OwnerScope string `gorm:"size:32;not null;default:'system'"`
OwnerID *uint64
Credentials string `gorm:"type:text;not null"`
Extra string `gorm:"type:text"`
Enabled bool `gorm:"default:false;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (messageChannelHelper) TableName() string { return "w_message_channels" }
type messageBindingHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
ChannelID uint64 `gorm:"not null;index"`
PlatformUserID string `gorm:"size:128;not null;index"`
UserID uint64 `gorm:"not null;index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
func (messageBindingHelper) TableName() string { return "w_message_bindings" }
type messagePairingHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
Code string `gorm:"size:32;uniqueIndex;not null"`
ChannelID uint64 `gorm:"not null;index"`
PlatformUserID string `gorm:"size:128;not null;index"`
UserID uint64 `gorm:"not null;index"`
ExpiresAt time.Time `gorm:"not null;index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
func (messagePairingHelper) TableName() string { return "w_message_pairing_codes" }
type taskExecutionHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
TaskID string `gorm:"size:128;uniqueIndex;not null"`
TaskType string `gorm:"size:64;index;not null"`
TaskName string `gorm:"size:128"`
Status string `gorm:"size:32;index;not null"`
Retryable bool `gorm:"not null;default:false"`
MaxRetry int `gorm:"not null;default:0"`
RetryCount int `gorm:"not null;default:0"`
Log string `gorm:"type:text"`
ErrorMessage string `gorm:"type:text"`
Result string `gorm:"type:text"`
StartedAt *time.Time `gorm:"index"`
FinishedAt *time.Time
Duration int64
Payload string `gorm:"type:text"`
TriggeredBy string `gorm:"size:32;not null;default:system"`
CreatedAt time.Time `gorm:"autoCreateTime;index"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (taskExecutionHelper) TableName() string {
return "w_task_executions"
}
type scheduleHelper struct {
ID uint64 `gorm:"primaryKey;autoIncrement"`
TaskType string `gorm:"size:64;uniqueIndex;not null"`
TaskName string `gorm:"size:128;not null"`
CronExpr string `gorm:"size:64;not null"`
Payload string `gorm:"type:text"`
Enabled bool `gorm:"default:true;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (scheduleHelper) TableName() string { return "w_schedules" }
const (
configTypeSystem = "system"
configTypeBusiness = "business"
configValueTrue = "true"
configValueFalse = "false"
)
// SetupTestEnvironment initializes an in-memory SQLite DB, seeds default configurations,
// starts miniredis, and overrides the global db/Redis clients. It returns a cleanup function.
func SetupTestEnvironment(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) {
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
t.Fatalf("failed to open in-memory SQLite db: %v", err)
}
if sqlDB, err := sqliteDB.DB(); err == nil {
sqlDB.SetMaxOpenConns(1)
}
// AutoMigrate all tables via internal test helpers to completely decouple testhelper from domain plugins
err = sqliteDB.AutoMigrate(
&userHelper{},
&accessTokenHelper{},
&authSourceHelper{},
&externalAccountHelper{},
&SystemConfig{},
&uploadHelper{},
&uploadStatHelper{},
&taskExecutionHelper{},
&scheduleHelper{},
&messageChannelHelper{},
&messageBindingHelper{},
&messagePairingHelper{},
)
if err != nil {
t.Fatalf("failed to auto migrate tables: %v", err)
}
mr, err := miniredis.Run()
if err != nil {
t.Fatalf("failed to start miniredis: %v", err)
}
redisClient := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
},
})
db.SetDB(sqliteDB)
db.Redis = redisClient
seedDefaultConfigs(t, sqliteDB)
cleanup := func() {
runExtraCleanups()
_ = redisClient.Close()
mr.Close()
db.SetDB(nil)
db.Redis = nil
}
return sqliteDB, mr, cleanup
}
func getSeedConfigsPart1() []SystemConfig {
return []SystemConfig{
{Key: "upload_allowed_extensions", Value: `["jpg", "jpeg", "png", "gif", "webp", "txt", "pdf", "zip"]`, Type: configTypeSystem, Description: "允许上传的文件扩展名列表(JSON 字符串数组)"},
{Key: "site_name", Value: "Wavelet", Type: configTypeSystem, Description: "站点名称"},
{Key: "site_description", Value: "Lightweight and Modular Web Application Platform", Type: configTypeSystem, Description: "站点描述"},
{Key: "password_login_enabled", Value: configValueTrue, Type: configTypeSystem, Description: "是否开启账号密码登录(true/false)"},
{Key: "registration_enabled", Value: configValueTrue, Type: configTypeSystem, Description: "是否允许新用户注册(全局总开关,true/false)"},
{Key: "password_register_enabled", Value: configValueTrue, Type: configTypeSystem, Description: "是否允许账号密码注册(true/false)"},
{Key: "oidc_login_enabled", Value: configValueFalse, Type: configTypeSystem, Description: "是否开启 OIDC 登录(true/false)"},
{Key: "server_address", Value: "http://localhost:8000", Type: configTypeSystem, Description: "服务端访问地址(用于生成绝对路径链接,多个地址用英文逗号分隔)"},
{Key: "smtp_host", Value: "", Type: configTypeSystem, Description: "SMTP 服务器主机名或 IP"},
{Key: "smtp_port", Value: "587", Type: configTypeSystem, Description: "SMTP 服务器端口(标准 STARTTLS 为 587,SMTPS 为 465)"},
{Key: "smtp_username", Value: "", Type: configTypeSystem, Description: "SMTP 账户(如 sender@example.com)"},
{Key: "smtp_password", Value: "", Type: configTypeSystem, Description: "SMTP 访问凭证(授权码/密码)"},
{Key: "email_login_verification_enabled", Value: configValueFalse, Type: configTypeSystem, Description: "是否开启邮箱登录验证(true/false)"},
{Key: "email_register_verification_enabled", Value: configValueFalse, Type: configTypeSystem, Description: "是否开启邮箱注册验证(true/false)"},
{Key: "menu_display_config", Value: "{}", Type: configTypeSystem, Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})"},
{Key: "search_engine_indexing_enabled", Value: configValueFalse, Type: configTypeSystem, Description: "是否允许搜索引擎检索"},
{Key: "file_access_whitelist", Value: `["avatar"]`, Type: configTypeSystem, Description: "免登录访问的文件业务类型白名单"},
{Key: "disk_cache_max_size_mb", Value: "100", Type: configTypeSystem, Description: "磁盘缓存最大空间大小 (MB)"},
{Key: "disk_cache_ttl_minutes", Value: "60", Type: configTypeSystem, Description: "磁盘缓存默认有效期 (分钟)"},
{Key: "disk_cache_lru_enabled", Value: configValueTrue, Type: configTypeSystem, Description: "是否启用 LRU 淘汰机制"},
{Key: "login_session_ttl_hours", Value: "0", Type: configTypeSystem, Description: "登录会话过期时间 (小时,0表示浏览器关闭后自动退出,-1表示永不过期)"},
{Key: "update_upstream_repository", Value: "Rain-kl/Wavelet", Type: configTypeSystem, Description: "GitHub Actions Release 上游仓库"},
{Key: "storage_config", Value: `{"driver":"local","local":{"root":"."},"s3":{"region":"us-east-1"},"r2":{"region":"auto"},"minio":{"region":"us-east-1","path_style":true},"oss":{},"webdav":{}}`, Type: configTypeSystem, Description: "文件存储驱动及连接配置(JSON)"},
{Key: "log_database", Value: "sqlite", Type: configTypeSystem, Description: "当前日志主库"},
{Key: "log_db_migration", Value: "", Type: configTypeSystem, Description: "日志库迁移冻结标记"},
{Key: "log_retention_days_postgres", Value: "30", Type: configTypeBusiness, Description: "PostgreSQL 用户访问日志保留天数"},
{Key: "log_retention_days_sqlite", Value: "30", Type: configTypeBusiness, Description: "SQLite 用户访问日志保留天数"},
{Key: "log_retention_days_clickhouse", Value: "30", Type: configTypeBusiness, Description: "ClickHouse 用户访问日志保留天数"},
}
}
func seedDefaultConfigs(t *testing.T, tx *gorm.DB) {
defaultConfigs := getSeedConfigsPart1()
if err := tx.Create(&defaultConfigs).Error; err != nil {
t.Fatalf("failed to seed default system configs: %v", err)
}
publicKeys := map[string]struct{}{
"upload_allowed_extensions": {},
"site_name": {},
"password_login_enabled": {},
"registration_enabled": {},
"password_register_enabled": {},
"oidc_login_enabled": {},
}
keys := make([]string, 0, len(publicKeys))
for key := range publicKeys {
keys = append(keys, key)
}
if err := tx.Model(&SystemConfig{}).
Where("key IN ?", keys).
Update("visibility", "visible").Error; err != nil {
t.Fatalf("failed to seed public system config visibility: %v", err)
}
for _, config := range defaultConfigs {
if _, ok := publicKeys[config.Key]; ok {
config.Visibility = "visible"
}
_ = db.HSetJSON(context.Background(), "system_configs", config.Key, &config)
}
}