Files
lijiaoqiao/supply-api/internal/sms/tencent_sms.go
Your Name ab0b0cc031 fix(supply-api): close sms verify and audit flush gaps
Use a shared in-memory code store across mock, Tencent, and Aliyun SMS services so send and verify follow the same contract. Also surface batch flush failures through FlushNow and explicit error tracking hooks for audit buffering.
2026-04-14 12:22:10 +08:00

152 lines
4.5 KiB
Go

package sms
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// TencentSMSService implements SMSService for Tencent Cloud SMS.
type TencentSMSService struct {
config *Config
httpClient *http.Client
store *InMemoryCodeStore
}
// NewTencentSMSService creates a new Tencent Cloud SMS service.
func NewTencentSMSService(config *Config) *TencentSMSService {
return NewTencentSMSServiceWithCodeStore(config, NewInMemoryCodeStore())
}
// NewTencentSMSServiceWithCodeStore creates a new Tencent Cloud SMS service with an explicit code store.
func NewTencentSMSServiceWithCodeStore(config *Config, store *InMemoryCodeStore) *TencentSMSService {
if config == nil {
config = DefaultConfig()
}
if store == nil {
store = NewInMemoryCodeStore()
}
return &TencentSMSService{
config: config,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
store: store,
}
}
// IsEnabled returns whether SMS service is enabled.
func (t *TencentSMSService) IsEnabled() bool {
return t.config.Enabled
}
// SendVerificationCode sends an SMS verification code via Tencent Cloud.
func (t *TencentSMSService) SendVerificationCode(ctx context.Context, phoneNumber string) (string, error) {
if !t.config.Enabled {
return "", ErrSMSServiceDisabled
}
code, err := GenerateCode(t.config.CodeLength)
if err != nil {
return "", err
}
// Tencent Cloud SMS API request
// Sign and send request
err = t.sendSMS(ctx, phoneNumber, code)
if err != nil {
return "", fmt.Errorf("failed to send SMS via Tencent Cloud: %w", err)
}
codeID, err := t.store.Save(phoneNumber, code, time.Duration(t.config.CodeExpireMins)*time.Minute, "tencent")
if err != nil {
return "", err
}
fmt.Printf("[TencentSMS] Code '%s' sent to %s\n", code, phoneNumber)
return codeID, nil
}
// sendSMS sends SMS via Tencent Cloud API
func (t *TencentSMSService) sendSMS(ctx context.Context, phoneNumber, code string) error {
// Tencent Cloud SMS API endpoint
endpoint := fmt.Sprintf("https://sms.tencentcloudapi.com/?Action=SendSms&Region=%s", t.config.Region)
// Build request body
params := url.Values{}
params.Set("PhoneNumberSet.0", phoneNumber)
params.Set("SmsSdkAppId", t.config.AppID)
params.Set("TemplateId", t.config.TemplateCode)
params.Set("TemplateParamSet.0", code)
params.Set("SignName", t.config.SignName)
// Sign request (simplified - in production use proper Tencent Auth)
body := strings.NewReader(params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("X-TC-Action", "SendSms")
req.Header.Set("X-TC-Version", "2021-01-11")
req.Header.Set("X-TC-Timestamp", fmt.Sprintf("%d", time.Now().Unix()))
req.Header.Set("X-TC-Region", t.config.Region)
// In production, add proper authentication headers
// Use Tencent Cloud SDK for proper signing:
// https://github.com/TencentCloud/tencentcloud-sdk-go-intl
resp, err := t.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("Tencent SMS API error: status=%d, body=%s", resp.StatusCode, string(bodyBytes))
}
return nil
}
// VerifyCode is a no-op for Tencent - verification is handled by the code store
// In production, you would verify against your own code store or use Tencent's verification API
func (t *TencentSMSService) VerifyCode(ctx context.Context, codeID string, phoneNumber string, code string) (bool, error) {
return t.store.Verify(codeID, phoneNumber, code)
}
// TencentSMSResponse represents the Tencent Cloud SMS API response.
type TencentSMSResponse struct {
Response struct {
RequestID string `json:"RequestId"`
SendStatusSet []struct {
SerialNo string `json:"SerialNo"`
PhoneNumber string `json:"PhoneNumber"`
CountryCode string `json:"CountryCode"`
InvokeID string `json:"InvokeId"`
Fee int `json:"Fee"`
StatusCode string `json:"StatusCode"`
Code string `json:"Code"` // 腾讯云实际字段名
StatusMessage string `json:"StatusMessage"`
Message string `json:"Message"` // 腾讯云实际字段名
} `json:"SendStatusSet"`
} `json:"Response"`
}
// parseResponse parses Tencent Cloud SMS API response.
func parseTencentResponse(body []byte) (*TencentSMSResponse, error) {
var resp TencentSMSResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, err
}
return &resp, nil
}