chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
125
platform/internal/smsstore/store.go
Normal file
125
platform/internal/smsstore/store.go
Normal file
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
23
platform/internal/smsstore/store_test.go
Normal file
23
platform/internal/smsstore/store_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package smsstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIssueAndConsume(t *testing.T) {
|
||||
s := New(time.Minute, time.Second, "654321")
|
||||
code, exp, retry, err := s.Issue(PurposeLogin, "13800138000")
|
||||
if err != nil || code != "654321" || exp <= 0 || retry != 0 {
|
||||
t.Fatalf("issue %q %d %d %v", code, exp, retry, err)
|
||||
}
|
||||
if err := s.Consume(PurposeLogin, "13800138000", "000000"); err == nil {
|
||||
t.Fatal("bad code should fail")
|
||||
}
|
||||
if err := s.Consume(PurposeLogin, "13800138000", "654321"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Consume(PurposeLogin, "13800138000", "654321"); err == nil {
|
||||
t.Fatal("consumed twice")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user