package storage import ( "database/sql/driver" "encoding/json" "fmt" "sproutgate-backend/internal/models" ) // ─── JSON 序列化辅助类型 ──────────────────────────────────────────────────────── // StringSlice 将 []string 序列化为 JSON 字符串存入 TEXT 列。 type StringSlice []string func (s StringSlice) Value() (driver.Value, error) { if s == nil { return "[]", nil } b, err := json.Marshal([]string(s)) return string(b), err } func (s *StringSlice) Scan(value interface{}) error { if value == nil { *s = StringSlice{} return nil } var raw []byte switch v := value.(type) { case []byte: raw = v case string: raw = []byte(v) default: return fmt.Errorf("StringSlice.Scan: unsupported type %T", value) } if len(raw) == 0 || string(raw) == "null" { *s = StringSlice{} return nil } return json.Unmarshal(raw, (*[]string)(s)) } // AuthClientSlice 将 []models.AuthClientEntry 序列化为 JSON 存入 TEXT 列。 type AuthClientSlice []models.AuthClientEntry func (a AuthClientSlice) Value() (driver.Value, error) { if a == nil { return "[]", nil } b, err := json.Marshal([]models.AuthClientEntry(a)) return string(b), err } func (a *AuthClientSlice) Scan(value interface{}) error { if value == nil { *a = AuthClientSlice{} return nil } var raw []byte switch v := value.(type) { case []byte: raw = v case string: raw = []byte(v) default: return fmt.Errorf("AuthClientSlice.Scan: unsupported type %T", value) } if len(raw) == 0 || string(raw) == "null" { *a = AuthClientSlice{} return nil } return json.Unmarshal(raw, (*[]models.AuthClientEntry)(a)) } // ─── GORM DB 模型 ───────────────────────────────────────────────────────────── // DBUser 对应数据库 users 表。 type DBUser struct { Account string `gorm:"primaryKey;column:account;size:255"` PasswordHash string `gorm:"column:password_hash;not null"` Username string `gorm:"column:username;not null;size:255"` Email string `gorm:"column:email;not null;size:255;index"` Level int `gorm:"column:level;default:0"` SproutCoins int `gorm:"column:sprout_coins;default:0"` LastCheckInDate string `gorm:"column:last_check_in_date;size:20"` LastCheckInAt string `gorm:"column:last_check_in_at;size:128"` LastVisitDate string `gorm:"column:last_visit_date;size:20"` LastVisitAt string `gorm:"column:last_visit_at;size:128"` LastVisitIP string `gorm:"column:last_visit_ip;size:45"` LastVisitDisplayLocation string `gorm:"column:last_visit_display_location;size:512"` CheckInTimes StringSlice `gorm:"column:check_in_times;type:mediumtext"` VisitTimes StringSlice `gorm:"column:visit_times;type:mediumtext"` SecondaryEmails StringSlice `gorm:"column:secondary_emails;type:text"` Phone string `gorm:"column:phone;size:50"` AvatarURL string `gorm:"column:avatar_url;size:1024"` WebsiteURL string `gorm:"column:website_url;size:1024"` Bio string `gorm:"column:bio;type:text"` CreatedAt string `gorm:"column:created_at;size:50;not null;<-:create"` UpdatedAt string `gorm:"column:updated_at;size:50;not null"` Banned bool `gorm:"column:banned;default:false"` BanReason string `gorm:"column:ban_reason;type:text"` BannedAt string `gorm:"column:banned_at;size:50"` TokenEpoch int64 `gorm:"column:token_epoch;default:0"` AuthClients AuthClientSlice `gorm:"column:auth_clients;type:mediumtext"` } func (DBUser) TableName() string { return "users" } // DBUserFromRecord 将领域模型转换为 GORM 模型(导出供 migrate 工具使用)。 func DBUserFromRecord(r models.UserRecord) DBUser { return dbUserFromRecord(r) } func dbUserFromRecord(r models.UserRecord) DBUser { return DBUser{ Account: r.Account, PasswordHash: r.PasswordHash, Username: r.Username, Email: r.Email, Level: r.Level, SproutCoins: r.SproutCoins, LastCheckInDate: r.LastCheckInDate, LastCheckInAt: r.LastCheckInAt, LastVisitDate: r.LastVisitDate, LastVisitAt: r.LastVisitAt, LastVisitIP: r.LastVisitIP, LastVisitDisplayLocation: r.LastVisitDisplayLocation, CheckInTimes: StringSlice(r.CheckInTimes), VisitTimes: StringSlice(r.VisitTimes), SecondaryEmails: StringSlice(r.SecondaryEmails), Phone: r.Phone, AvatarURL: r.AvatarURL, WebsiteURL: r.WebsiteURL, Bio: r.Bio, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, Banned: r.Banned, BanReason: r.BanReason, BannedAt: r.BannedAt, TokenEpoch: r.TokenEpoch, AuthClients: AuthClientSlice(r.AuthClients), } } func (d DBUser) toRecord() models.UserRecord { return models.UserRecord{ Account: d.Account, PasswordHash: d.PasswordHash, Username: d.Username, Email: d.Email, Level: d.Level, SproutCoins: d.SproutCoins, LastCheckInDate: d.LastCheckInDate, LastCheckInAt: d.LastCheckInAt, LastVisitDate: d.LastVisitDate, LastVisitAt: d.LastVisitAt, LastVisitIP: d.LastVisitIP, LastVisitDisplayLocation: d.LastVisitDisplayLocation, CheckInTimes: []string(d.CheckInTimes), VisitTimes: []string(d.VisitTimes), SecondaryEmails: []string(d.SecondaryEmails), Phone: d.Phone, AvatarURL: d.AvatarURL, WebsiteURL: d.WebsiteURL, Bio: d.Bio, CreatedAt: d.CreatedAt, UpdatedAt: d.UpdatedAt, Banned: d.Banned, BanReason: d.BanReason, BannedAt: d.BannedAt, TokenEpoch: d.TokenEpoch, AuthClients: []models.AuthClientEntry(d.AuthClients), } } // DBPendingUser 对应 pending_users 表。 type DBPendingUser struct { Account string `gorm:"primaryKey;column:account;size:255"` PasswordHash string `gorm:"column:password_hash;not null"` Username string `gorm:"column:username;not null;size:255"` Email string `gorm:"column:email;not null;size:255;index"` CodeHash string `gorm:"column:code_hash;not null"` ExpiresAt string `gorm:"column:expires_at;size:50;not null"` CreatedAt string `gorm:"column:created_at;size:50;not null;<-:create"` InviteCode string `gorm:"column:invite_code;size:32"` } func (DBPendingUser) TableName() string { return "pending_users" } func dbPendingFromModel(p models.PendingUser) DBPendingUser { return DBPendingUser{ Account: p.Account, PasswordHash: p.PasswordHash, Username: p.Username, Email: p.Email, CodeHash: p.CodeHash, ExpiresAt: p.ExpiresAt, CreatedAt: p.CreatedAt, InviteCode: p.InviteCode, } } func (d DBPendingUser) toModel() models.PendingUser { return models.PendingUser{ Account: d.Account, PasswordHash: d.PasswordHash, Username: d.Username, Email: d.Email, CodeHash: d.CodeHash, ExpiresAt: d.ExpiresAt, CreatedAt: d.CreatedAt, InviteCode: d.InviteCode, } } // DBResetPassword 对应 password_resets 表。 type DBResetPassword struct { Account string `gorm:"primaryKey;column:account;size:255"` Email string `gorm:"column:email;not null;size:255"` CodeHash string `gorm:"column:code_hash;not null"` ExpiresAt string `gorm:"column:expires_at;size:50;not null"` CreatedAt string `gorm:"column:created_at;size:50;not null;<-:create"` } func (DBResetPassword) TableName() string { return "password_resets" } func dbResetFromModel(r models.ResetPassword) DBResetPassword { return DBResetPassword{ Account: r.Account, Email: r.Email, CodeHash: r.CodeHash, ExpiresAt: r.ExpiresAt, CreatedAt: r.CreatedAt, } } func (d DBResetPassword) toModel() models.ResetPassword { return models.ResetPassword{ Account: d.Account, Email: d.Email, CodeHash: d.CodeHash, ExpiresAt: d.ExpiresAt, CreatedAt: d.CreatedAt, } } // DBSecondaryEmailVerification 对应 secondary_email_verifications 表。 // 联合主键 (account, email)。 type DBSecondaryEmailVerification struct { Account string `gorm:"primaryKey;column:account;size:255"` Email string `gorm:"primaryKey;column:email;size:255"` CodeHash string `gorm:"column:code_hash;not null"` ExpiresAt string `gorm:"column:expires_at;size:50;not null"` CreatedAt string `gorm:"column:created_at;size:50;not null;<-:create"` } func (DBSecondaryEmailVerification) TableName() string { return "secondary_email_verifications" } func dbSecondaryFromModel(s models.SecondaryEmailVerification) DBSecondaryEmailVerification { return DBSecondaryEmailVerification{ Account: s.Account, Email: s.Email, CodeHash: s.CodeHash, ExpiresAt: s.ExpiresAt, CreatedAt: s.CreatedAt, } } func (d DBSecondaryEmailVerification) toModel() models.SecondaryEmailVerification { return models.SecondaryEmailVerification{ Account: d.Account, Email: d.Email, CodeHash: d.CodeHash, ExpiresAt: d.ExpiresAt, CreatedAt: d.CreatedAt, } } // DBAppConfig 对应 app_configs 表,存储应用配置键值对(value 为 JSON 文本)。 type DBAppConfig struct { ConfigKey string `gorm:"primaryKey;column:config_key;size:128"` ConfigValue string `gorm:"column:config_value;type:text;not null"` } func (DBAppConfig) TableName() string { return "app_configs" } // DBInviteCode 对应 invite_codes 表。 type DBInviteCode struct { Code string `gorm:"primaryKey;column:code;size:32"` Note string `gorm:"column:note;size:512"` MaxUses int `gorm:"column:max_uses;default:0"` Uses int `gorm:"column:uses;default:0"` ExpiresAt string `gorm:"column:expires_at;size:50"` CreatedAt string `gorm:"column:created_at;size:50;not null;<-:create"` } func (DBInviteCode) TableName() string { return "invite_codes" } func dbInviteFromEntry(e InviteEntry) DBInviteCode { return DBInviteCode{ Code: e.Code, Note: e.Note, MaxUses: e.MaxUses, Uses: e.Uses, ExpiresAt: e.ExpiresAt, CreatedAt: e.CreatedAt, } } func (d DBInviteCode) toEntry() InviteEntry { return InviteEntry{ Code: d.Code, Note: d.Note, MaxUses: d.MaxUses, Uses: d.Uses, ExpiresAt: d.ExpiresAt, CreatedAt: d.CreatedAt, } }