spark-server/internal/module/sms/logic/service.go
2026-09-22 17:07:26 +08:00

70 lines
1.7 KiB
Go

package logic
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
smsentity "spark-server/internal/module/sms/entity"
userentity "spark-server/internal/module/user/entity"
)
const (
defaultCodeTTLSeconds = 300
defaultCodeCooldownSeconds = 60
)
// CodeTTLSeconds is the OTP lifetime returned to clients.
const CodeTTLSeconds = defaultCodeTTLSeconds
// AccountEmail resolves the bound mailbox for an authenticated user (account delete OTP).
type AccountEmail interface {
EmailByUserID(ctx context.Context, userID int64) (string, error)
}
// Service issues and checks OTPs for email and phone.
type Service struct {
store CodeStore
email EmailSender
phone []PhoneCarrierSender
accounts AccountEmail
}
func NewService(store CodeStore, email EmailSender, phone []PhoneCarrierSender, accounts AccountEmail) *Service {
return &Service{store: store, email: email, phone: phone, accounts: accounts}
}
func storeKey(ch smsentity.Channel, purpose smsentity.Purpose, target string) string {
return fmt.Sprintf("%d:%d:%s", ch, purpose, target)
}
func randomDigits(n int) (string, error) {
if n <= 0 {
return "", fmt.Errorf("digits")
}
var buf [8]byte
out := make([]byte, n)
for i := 0; i < n; i++ {
if _, err := rand.Read(buf[:4]); err != nil {
return "", err
}
out[i] = '0' + byte(binary.BigEndian.Uint32(buf[:4])%10)
}
return string(out), nil
}
func skipSendEmail(email string) bool {
return userentity.IsTestEmail(email)
}
func skipSendPhone(phone string) bool {
// Dev-friendly test MSISDNs matching spark test convention.
switch phone {
case "+8613800138000", "+8613800138001", "+8613800138002":
return true
default:
return false
}
}