Files
2026-07-31 10:31:17 +08:00

126 lines
2.6 KiB
Go

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
}