Files
lijiaoqiao/supply-api/internal/sms/sms.go

172 lines
5.2 KiB
Go
Raw Normal View History

// Package sms provides SMS verification code services for Tencent Cloud and Aliyun.
package sms
import (
"context"
"crypto/rand"
"errors"
"fmt"
"math/big"
"time"
)
// ProviderType represents the SMS service provider type.
type ProviderType string
const (
ProviderTencent ProviderType = "tencent" // 腾讯云
ProviderAliyun ProviderType = "aliyun" // 阿里云
)
// Config holds SMS service configuration.
type Config struct {
Provider ProviderType `json:"provider" yaml:"provider"`
Enabled bool `json:"enabled" yaml:"enabled"`
AppID string `json:"app_id" yaml:"app_id"` // 腾讯云: SDK AppID; 阿里云: AccessKey ID
AppSecret string `json:"app_secret" yaml:"app_secret"` // 腾讯云: AppKey; 阿里云: AccessKey Secret
SignName string `json:"sign_name" yaml:"sign_name"` // 短信签名
TemplateCode string `json:"template_code" yaml:"template_code"` // 短信模板CODE
Region string `json:"region" yaml:"region"` // 区域 (腾讯云用)
Endpoint string `json:"endpoint" yaml:"endpoint"` // 自定义 endpoint (可选)
CodeLength int `json:"code_length" yaml:"code_length"` // 验证码长度默认6位
CodeExpireMins int `json:"code_expire_mins" yaml:"code_expire_mins"` // 验证码有效期(分钟)默认5分钟
}
// DefaultConfig returns a default SMS configuration.
func DefaultConfig() *Config {
return &Config{
Enabled: false, // 默认关闭,使用硬编码验证码
Provider: ProviderTencent,
CodeLength: 6,
CodeExpireMins: 5,
Region: "ap-guangzhou",
}
}
// SMSService defines the interface for SMS verification code services.
type SMSService interface {
// SendVerificationCode sends an SMS verification code to the given phone number.
// Returns the verification code ID (used for later verification) and error.
SendVerificationCode(ctx context.Context, phoneNumber string) (string, error)
// VerifyCode verifies if the provided code matches the one sent to the phone.
// Returns true if valid, false otherwise.
VerifyCode(ctx context.Context, codeID string, phoneNumber string, code string) (bool, error)
// IsEnabled returns whether SMS service is enabled.
IsEnabled() bool
}
// InMemoryCodeStore stores verification codes in memory (for development/testing).
type InMemoryCodeStore struct {
codes map[string]*codeEntry
}
type codeEntry struct {
Code string
Phone string
ExpiresAt time.Time
}
// NewInMemoryCodeStore creates a new in-memory code store.
func NewInMemoryCodeStore() *InMemoryCodeStore {
return &InMemoryCodeStore{
codes: make(map[string]*codeEntry),
}
}
// GenerateCode generates a random numeric code of specified length.
func GenerateCode(length int) (string, error) {
if length <= 0 {
length = 6
}
code := ""
for i := 0; i < length; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", fmt.Errorf("failed to generate random number: %w", err)
}
code += fmt.Sprintf("%d", n.Int64())
}
return code, nil
}
// MockSMSService is a mock SMS service for development/testing.
type MockSMSService struct {
store *InMemoryCodeStore
config *Config
}
// NewMockSMSService creates a new mock SMS service.
func NewMockSMSService(config *Config) *MockSMSService {
if config == nil {
config = DefaultConfig()
}
return &MockSMSService{
store: NewInMemoryCodeStore(),
config: config,
}
}
// IsEnabled returns true if the SMS service is enabled.
func (m *MockSMSService) IsEnabled() bool {
return m.config.Enabled
}
// ErrSMSServiceDisabled indicates SMS service is disabled
var ErrSMSServiceDisabled = errors.New("SMS service is disabled")
// SendVerificationCode generates and "sends" a verification code.
func (m *MockSMSService) SendVerificationCode(ctx context.Context, phoneNumber string) (string, error) {
if !m.config.Enabled {
// 安全修复: 禁用时返回错误而非返回mock code
return "", ErrSMSServiceDisabled
}
code, err := GenerateCode(m.config.CodeLength)
if err != nil {
return "", err
}
codeID := fmt.Sprintf("mock-%d", time.Now().UnixNano())
expiresAt := time.Now().Add(time.Duration(m.config.CodeExpireMins) * time.Minute)
m.store.codes[codeID] = &codeEntry{
Code: code,
Phone: phoneNumber,
ExpiresAt: expiresAt,
}
// In a real implementation, this would send the SMS via HTTP API
fmt.Printf("[SMS Mock] Sending code '%s' to %s (codeID: %s)\n", code, phoneNumber, codeID)
return codeID, nil
}
// VerifyCode verifies the provided code.
func (m *MockSMSService) VerifyCode(ctx context.Context, codeID string, phoneNumber string, code string) (bool, error) {
if !m.config.Enabled {
// 安全修复: 禁用时拒绝验证,返回错误
return false, ErrSMSServiceDisabled
}
entry, ok := m.store.codes[codeID]
if !ok {
return false, nil
}
if time.Now().After(entry.ExpiresAt) {
delete(m.store.codes, codeID)
return false, nil
}
if entry.Phone != phoneNumber {
return false, nil
}
valid := entry.Code == code
if valid {
delete(m.store.codes, codeID) // Code can only be used once
}
return valid, nil
}