package smsstore import ( "crypto/rand" "fmt" "sync" "time" ) type Purpose string const ( PurposeLogin Purpose = "login" PurposeBind Purpose = "bind" ) type Record struct { Code string ExpiresAt time.Time SentAt time.Time Attempts int } type Store struct { mu sync.Mutex byKey map[string]*Record ttl time.Duration resendGap time.Duration maxAttempts int fixedCode string // 开发固定码;空则随机 6 位 } func New(ttl, resendGap time.Duration, fixedCode string) *Store { if ttl <= 0 { ttl = 5 * time.Minute } if resendGap <= 0 { resendGap = 60 * time.Second } return &Store{ byKey: map[string]*Record{}, ttl: ttl, resendGap: resendGap, maxAttempts: 5, fixedCode: fixedCode, } } func key(purpose Purpose, phone string) string { return string(purpose) + ":" + phone } // Issue 生成并保存验证码;若未到重发间隔返回 retryAfter>0。 func (s *Store) Issue(purpose Purpose, phone string) (code string, expiresIn, retryAfter int, err error) { s.mu.Lock() defer s.mu.Unlock() s.gcLocked() k := key(purpose, phone) now := time.Now() if old, ok := s.byKey[k]; ok && now.Sub(old.SentAt) < s.resendGap { left := int((s.resendGap - now.Sub(old.SentAt)).Seconds()) if left < 1 { left = 1 } return "", 0, left, fmt.Errorf("发送过于频繁,请 %d 秒后再试", left) } code = s.fixedCode if code == "" { var e error code, e = randomDigits(6) if e != nil { return "", 0, 0, e } } s.byKey[k] = &Record{ Code: code, ExpiresAt: now.Add(s.ttl), SentAt: now, } return code, int(s.ttl.Seconds()), 0, nil } // Consume 校验并消费验证码(成功后删除)。 func (s *Store) Consume(purpose Purpose, phone, code string) error { s.mu.Lock() defer s.mu.Unlock() s.gcLocked() k := key(purpose, phone) rec, ok := s.byKey[k] if !ok || time.Now().After(rec.ExpiresAt) { delete(s.byKey, k) return fmt.Errorf("验证码无效或已过期") } if rec.Code != code { rec.Attempts++ if rec.Attempts >= s.maxAttempts { delete(s.byKey, k) return fmt.Errorf("验证码错误次数过多,请重新获取") } return fmt.Errorf("验证码错误") } delete(s.byKey, k) return nil } func (s *Store) gcLocked() { now := time.Now() for k, r := range s.byKey { if now.After(r.ExpiresAt.Add(time.Minute)) { delete(s.byKey, k) } } } func randomDigits(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } out := make([]byte, n) for i := range b { out[i] = '0' + b[i]%10 } return string(out), nil }