342 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			342 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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"          // 解绑手机号 - 解绑当前手机号
 | ||
| 	SMSSceneCertification  SMSScene = "certification"   // 企业认证 - 企业入驻认证
 | ||
| )
 | ||
| 
 | ||
| // 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,
 | ||
| 		SMSSceneCertification,
 | ||
| 	}
 | ||
| 
 | ||
| 	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:         "解绑手机号",
 | ||
| 		SMSSceneCertification:  "企业认证",
 | ||
| 	}
 | ||
| 
 | ||
| 	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,
 | ||
| 		SMSSceneCertification,
 | ||
| 	}
 | ||
| 
 | ||
| 	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:         "解绑手机号",
 | ||
| 		SMSSceneCertification:  "企业认证",
 | ||
| 	}
 | ||
| 
 | ||
| 	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"
 | ||
| }
 |