// Package model 定义 GORM 模型与数据库接入。 // // 兼容性约束(见 PLAN §6): // - 避免平台特有类型,时间统一 UTC,字段尽量用通用类型; // - detail 等 JSON 内容以 string 存储(入库前序列化),避免依赖 // datatypes.JSON 的平台差异; // - 由本包 AutoMigrate 保证 SQLite / MySQL 均可平滑迁移。 package model import ( "fmt" "os" "path/filepath" "time" "github.com/glebarez/sqlite" "gorm.io/driver/mysql" "gorm.io/gorm" gormlogger "gorm.io/gorm/logger" ) // 外部用户账号状态。 const ( UserStatusActive = "active" // 正常可用 UserStatusDisabled = "disabled" // 管理员禁用 UserStatusExpired = "expired" // 已到期,等待回收或延期 ) // 密钥 / 申请 / 审计结果等通用状态。 const ( StatusActive = "active" StatusRevoked = "revoked" StatusPending = "pending" StatusApproved = "approved" StatusRejected = "rejected" ResultSuccess = "success" ResultFailed = "failed" ) // AdminUser 管理端账号。 type AdminUser struct { ID uint `gorm:"primaryKey" json:"id"` Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` PasswordHash string `gorm:"size:255;not null" json:"-"` Email string `gorm:"size:255" json:"email"` Role string `gorm:"size:32;not null;default:admin" json:"role"` Status string `gorm:"size:16;not null;default:active" json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // User 外部用户(1:1 对应系统账号 ext_)。 type User struct { ID uint `gorm:"primaryKey" json:"id"` Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` // 含 ext_ 前缀 Email string `gorm:"size:255;not null" json:"email"` Supervisor string `gorm:"size:128" json:"supervisor"` // 挂靠老师 Purpose string `gorm:"size:512" json:"purpose"` // 用途 Status string `gorm:"size:16;not null;default:active;index" json:"status"` ExpireAt *time.Time `gorm:"index" json:"expire_at"` Shell string `gorm:"size:64;not null" json:"shell"` CreatedBy uint `json:"created_by"` LastLoginAt *time.Time `json:"last_login_at"` RecycledAt *time.Time `json:"recycled_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // SSHKey 用户上传的 SSH 公钥。仅用户自行上传(source=user_uploaded),管理员不代签。 type SSHKey struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"not null;index" json:"user_id"` Name string `gorm:"size:64;not null" json:"name"` KeyType string `gorm:"size:32;not null" json:"key_type"` // ssh-ed25519 / ssh-rsa ... PublicKey string `gorm:"type:text;not null" json:"public_key"` Fingerprint string `gorm:"size:64;index" json:"fingerprint"` Status string `gorm:"size:16;not null;default:active;index" json:"status"` Source string `gorm:"size:32;not null;default:user_uploaded" json:"source"` CreatedBy uint `json:"created_by"` RevokedAt *time.Time `json:"revoked_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Approval 新账号申请单(仅新账号申请走审批流,延期由管理员直接操作)。 type Approval struct { ID uint `gorm:"primaryKey" json:"id"` UsernameRequested string `gorm:"size:64;index;not null" json:"username_requested"` // 不含 ext_ 前缀 Email string `gorm:"size:255;not null" json:"email"` Supervisor string `gorm:"size:128" json:"supervisor"` Purpose string `gorm:"size:512" json:"purpose"` Status string `gorm:"size:16;not null;default:pending;index" json:"status"` ReviewerID *uint `json:"reviewer_id"` ReviewedAt *time.Time `json:"reviewed_at"` Reason string `gorm:"size:512" json:"reason"` // 拒绝理由 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // AuditLog 审计日志,append-only:业务代码只允许 Create,禁止 Update/Delete。 type AuditLog struct { ID uint `gorm:"primaryKey" json:"id"` ActorID uint `gorm:"index" json:"actor_id"` ActorName string `gorm:"size:64" json:"actor_name"` Action string `gorm:"size:64;index;not null" json:"action"` ResourceType string `gorm:"size:32;index" json:"resource_type"` ResourceID string `gorm:"size:64;index" json:"resource_id"` Detail string `gorm:"type:text" json:"detail"` // JSON 序列化后的详情 IP string `gorm:"size:64" json:"ip"` Result string `gorm:"size:16;not null" json:"result"` CreatedAt time.Time `gorm:"index" json:"created_at"` } // Session 服务端会话(cookie 会话,DB 存储,兼容多实例)。 type Session struct { ID string `gorm:"primaryKey;size:64" json:"session_id"` UserType string `gorm:"size:16;not null" json:"user_type"` // admin / user RefID uint `gorm:"index;not null" json:"ref_id"` ExpireAt time.Time `gorm:"index;not null" json:"expire_at"` IP string `gorm:"size:64" json:"ip"` UserAgent string `gorm:"size:255" json:"user_agent"` CreatedAt time.Time `json:"created_at"` } // Setting 系统设置(SMTP、默认有效期等,config 提供默认值,settings 表可覆盖)。 type Setting struct { Key string `gorm:"primaryKey;size:128" json:"key"` Value string `gorm:"type:text" json:"value"` UpdatedAt time.Time `json:"updated_at"` } // MailLog 邮件发送记录(队列 + 重试 + 失败记录)。 type MailLog struct { ID uint `gorm:"primaryKey" json:"id"` To string `gorm:"size:255;index;not null" json:"to"` Subject string `gorm:"size:255" json:"subject"` Status string `gorm:"size:16;not null;default:pending;index" json:"status"` Error string `gorm:"type:text" json:"error"` RetryCount int `gorm:"not null;default:0" json:"retry_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // OTPCode 外部用户 OTP 验证码(DB 存储,邮件与 CLI 双通道共享)。 // // 每用户一行(username 唯一):邮件通道经 Send 生成,CLI 通道经 Current // 读取同一验证码,保证"同一验证码、同一有效期、同一冷却与失败限速"(PLAN §2.2)。 // 验证码为 6 位数字,生命周期短(10 分钟)且一次性消费,存储明文以便 CLI // 复用返回;并发写由单实例部署的串行事务保证(多实例需改 DB 行锁/Redis,PLAN §6)。 type OTPCode struct { ID uint `gorm:"primaryKey" json:"id"` Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` Code string `gorm:"size:16;not null" json:"-"` ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` CooldownUntil time.Time `json:"cooldown_until"` // Send 冷却截止 Failures int `json:"failures"` FailedAt *time.Time `json:"failed_at"` // 失败计数窗口起点(窗口内达阈值限速) ConsumedAt *time.Time `json:"consumed_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // PasswordResetToken 管理员密码重置令牌(邮件重置)。DB 只存哈希, // 明文令牌仅经邮件/日志发送给管理员,一次有效。 type PasswordResetToken struct { ID uint `gorm:"primaryKey" json:"id"` AdminID uint `gorm:"index;not null" json:"admin_id"` TokenHash string `gorm:"size:64;not null" json:"-"` ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` UsedAt *time.Time `json:"used_at"` IP string `gorm:"size:64" json:"ip"` CreatedAt time.Time `json:"created_at"` } // AllModels 供 AutoMigrate 使用的全部模型。 func AllModels() []any { return []any{ &AdminUser{}, &User{}, &SSHKey{}, &Approval{}, &AuditLog{}, &Session{}, &Setting{}, &MailLog{}, &OTPCode{}, &PasswordResetToken{}, } } // Open 按 driver 打开 GORM 连接。 // // driver: sqlite | mysql // dsn: sqlite 为文件路径(自动创建父目录);mysql 为 DSN 字符串 func Open(driver, dsn string, debug bool) (*gorm.DB, error) { logLevel := gormlogger.Warn if debug { logLevel = gormlogger.Info } cfg := &gorm.Config{ Logger: gormlogger.Default.LogMode(logLevel), // 表名默认复数(users / ssh_keys / approvals / ...),与 PLAN §6 一致。 } var dialector gorm.Dialector switch driver { case "sqlite": if dsn != ":memory:" { if err := os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil { return nil, fmt.Errorf("model: create sqlite dir: %w", err) } } dialector = sqlite.Open(dsn) case "mysql": dialector = mysql.Open(dsn) default: return nil, fmt.Errorf("model: unsupported driver %q", driver) } return gorm.Open(dialector, cfg) } // Migrate 执行 AutoMigrate(CLI migrate 子命令入口)。 func Migrate(db *gorm.DB) error { return db.AutoMigrate(AllModels()...) }