package entities import ( "time" "github.com/google/uuid" "gorm.io/gorm" ) // SMSCode 短信验证码记录实体 // 记录用户发送的所有短信验证码,支持多种使用场景 // 包含验证码的有效期管理、使用状态跟踪、安全审计等功能 // @Description 短信验证码记录实体 type SMSCode struct { // 基础标识 ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"短信验证码记录唯一标识" example:"123e4567-e89b-12d3-a456-426614174000"` Phone string `gorm:"type:varchar(20);not null;index" json:"phone" comment:"接收手机号" example:"13800138000"` Code string `gorm:"type:varchar(10);not null" json:"-" comment:"验证码内容(不返回给前端)"` Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene" comment:"使用场景" example:"register"` Used bool `gorm:"default:false" json:"used" comment:"是否已使用" example:"false"` ExpiresAt time.Time `gorm:"not null" json:"expires_at" comment:"过期时间" example:"2024-01-01T00:05:00Z"` UsedAt *time.Time `json:"used_at,omitempty" comment:"使用时间"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间" example:"2024-01-01T00:00:00Z"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间" example:"2024-01-01T00:00:00Z"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"` // 额外信息 - 安全审计相关数据 IP string `gorm:"type:varchar(45)" json:"ip" comment:"发送IP地址" example:"192.168.1.1"` UserAgent string `gorm:"type:varchar(500)" json:"user_agent" comment:"客户端信息" example:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"` } // SMSScene 短信验证码使用场景枚举 // 定义系统中所有需要使用短信验证码的业务场景 // @Description 短信验证码使用场景 type SMSScene string const ( SMSSceneRegister SMSScene = "register" // 注册 - 新用户注册验证 SMSSceneLogin SMSScene = "login" // 登录 - 手机号登录验证 SMSSceneChangePassword SMSScene = "change_password" // 修改密码 - 修改密码验证 SMSSceneResetPassword SMSScene = "reset_password" // 重置密码 - 忘记密码重置 SMSSceneBind SMSScene = "bind" // 绑定手机号 - 绑定新手机号 SMSSceneUnbind SMSScene = "unbind" // 解绑手机号 - 解绑当前手机号 ) // BeforeCreate GORM钩子:创建前自动生成UUID func (s *SMSCode) BeforeCreate(tx *gorm.DB) error { if s.ID == "" { s.ID = uuid.New().String() } return nil } // 实现 Entity 接口 - 提供统一的实体管理接口 // GetID 获取实体唯一标识 func (s *SMSCode) GetID() string { return s.ID } // GetCreatedAt 获取创建时间 func (s *SMSCode) GetCreatedAt() time.Time { return s.CreatedAt } // GetUpdatedAt 获取更新时间 func (s *SMSCode) GetUpdatedAt() time.Time { return s.UpdatedAt } // Validate 验证短信验证码 // 检查短信验证码记录的必填字段是否完整,确保数据的有效性 func (s *SMSCode) Validate() error { if s.Phone == "" { return &ValidationError{Message: "手机号不能为空"} } if s.Code == "" { return &ValidationError{Message: "验证码不能为空"} } if s.Scene == "" { return &ValidationError{Message: "使用场景不能为空"} } if s.ExpiresAt.IsZero() { return &ValidationError{Message: "过期时间不能为空"} } // 验证手机号格式 if !IsValidPhoneFormat(s.Phone) { return &ValidationError{Message: "手机号格式无效"} } // 验证验证码格式 if err := s.validateCodeFormat(); err != nil { return err } return nil } // ================ 业务方法 ================ // VerifyCode 验证验证码 // 检查输入的验证码是否匹配且有效 func (s *SMSCode) VerifyCode(inputCode string) error { // 1. 检查验证码是否已使用 if s.Used { return &ValidationError{Message: "验证码已被使用"} } // 2. 检查验证码是否已过期 if s.IsExpired() { return &ValidationError{Message: "验证码已过期"} } // 3. 检查验证码是否匹配 if s.Code != inputCode { return &ValidationError{Message: "验证码错误"} } // 4. 标记为已使用 s.MarkAsUsed() return nil } // IsExpired 检查验证码是否已过期 // 判断当前时间是否超过验证码的有效期 func (s *SMSCode) IsExpired() bool { return time.Now().After(s.ExpiresAt) || time.Now().Equal(s.ExpiresAt) } // IsValid 检查验证码是否有效 // 综合判断验证码是否可用,包括未使用和未过期两个条件 func (s *SMSCode) IsValid() bool { return !s.Used && !s.IsExpired() } // MarkAsUsed 标记验证码为已使用 // 在验证码被成功使用后调用,记录使用时间并标记状态 func (s *SMSCode) MarkAsUsed() { s.Used = true now := time.Now() s.UsedAt = &now } // CanResend 检查是否可以重新发送验证码 // 基于时间间隔和场景判断是否允许重新发送 func (s *SMSCode) CanResend(minInterval time.Duration) bool { // 如果验证码已使用或已过期,可以重新发送 if s.Used || s.IsExpired() { return true } // 检查距离上次发送的时间间隔 timeSinceCreated := time.Since(s.CreatedAt) return timeSinceCreated >= minInterval } // GetRemainingTime 获取验证码剩余有效时间 func (s *SMSCode) GetRemainingTime() time.Duration { if s.IsExpired() { return 0 } return s.ExpiresAt.Sub(time.Now()) } // IsRecentlySent 检查是否最近发送过验证码 func (s *SMSCode) IsRecentlySent(within time.Duration) bool { return time.Since(s.CreatedAt) < within } // GetMaskedCode 获取脱敏的验证码(用于日志记录) func (s *SMSCode) GetMaskedCode() string { if len(s.Code) < 3 { return "***" } return s.Code[:1] + "***" + s.Code[len(s.Code)-1:] } // GetMaskedPhone 获取脱敏的手机号 func (s *SMSCode) GetMaskedPhone() string { if len(s.Phone) < 7 { return s.Phone } return s.Phone[:3] + "****" + s.Phone[len(s.Phone)-4:] } // ================ 场景相关方法 ================ // IsSceneValid 检查场景是否有效 func (s *SMSCode) IsSceneValid() bool { validScenes := []SMSScene{ SMSSceneRegister, SMSSceneLogin, SMSSceneChangePassword, SMSSceneResetPassword, SMSSceneBind, SMSSceneUnbind, } for _, scene := range validScenes { if s.Scene == scene { return true } } return false } // GetSceneName 获取场景的中文名称 func (s *SMSCode) GetSceneName() string { sceneNames := map[SMSScene]string{ SMSSceneRegister: "用户注册", SMSSceneLogin: "用户登录", SMSSceneChangePassword: "修改密码", SMSSceneResetPassword: "重置密码", SMSSceneBind: "绑定手机号", SMSSceneUnbind: "解绑手机号", } if name, exists := sceneNames[s.Scene]; exists { return name } return string(s.Scene) } // ================ 安全相关方法 ================ // IsSuspicious 检查是否存在可疑行为 func (s *SMSCode) IsSuspicious() bool { // 检查IP地址是否为空(可能表示异常) if s.IP == "" { return true } // 检查UserAgent是否为空(可能表示异常) if s.UserAgent == "" { return true } // 可以添加更多安全检查逻辑 // 例如:检查IP是否来自异常地区、UserAgent是否异常等 return false } // GetSecurityInfo 获取安全信息摘要 func (s *SMSCode) GetSecurityInfo() map[string]interface{} { return map[string]interface{}{ "ip": s.IP, "user_agent": s.UserAgent, "suspicious": s.IsSuspicious(), "scene": s.GetSceneName(), "created_at": s.CreatedAt, } } // ================ 私有辅助方法 ================ // validateCodeFormat 验证验证码格式 func (s *SMSCode) validateCodeFormat() error { // 检查验证码长度 if len(s.Code) < 4 || len(s.Code) > 10 { return &ValidationError{Message: "验证码长度必须在4-10位之间"} } // 检查验证码是否只包含数字 for _, char := range s.Code { if char < '0' || char > '9' { return &ValidationError{Message: "验证码只能包含数字"} } } return nil } // ================ 静态工具方法 ================ // IsValidScene 检查场景是否有效(静态方法) func IsValidScene(scene SMSScene) bool { validScenes := []SMSScene{ SMSSceneRegister, SMSSceneLogin, SMSSceneChangePassword, SMSSceneResetPassword, SMSSceneBind, SMSSceneUnbind, } for _, validScene := range validScenes { if scene == validScene { return true } } return false } // GetSceneName 获取场景的中文名称(静态方法) func GetSceneName(scene SMSScene) string { sceneNames := map[SMSScene]string{ SMSSceneRegister: "用户注册", SMSSceneLogin: "用户登录", SMSSceneChangePassword: "修改密码", SMSSceneResetPassword: "重置密码", SMSSceneBind: "绑定手机号", SMSSceneUnbind: "解绑手机号", } if name, exists := sceneNames[scene]; exists { return name } return string(scene) } // NewSMSCode 创建新的短信验证码(工厂方法) func NewSMSCode(phone, code string, scene SMSScene, expireTime time.Duration, clientIP, userAgent string) (*SMSCode, error) { smsCode := &SMSCode{ Phone: phone, Code: code, Scene: scene, Used: false, ExpiresAt: time.Now().Add(expireTime), IP: clientIP, UserAgent: userAgent, } // 验证实体 if err := smsCode.Validate(); err != nil { return nil, err } return smsCode, nil } // TableName 指定表名 func (SMSCode) TableName() string { return "sms_codes" }