feat: 更新SproutGate前后端代码

This commit is contained in:
2026-04-01 22:04:01 +08:00
parent 90590c7cb0
commit 650e1c7707
49 changed files with 3609 additions and 768 deletions

View File

@@ -0,0 +1,321 @@
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,
}
}