322 lines
11 KiB
Go
322 lines
11 KiB
Go
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,
|
||
}
|
||
}
|