160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
// Copyright © 2023 OpenIM open source community. All rights reserved.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package sms
|
||
|
||
import (
|
||
"context"
|
||
"crypto/md5"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"github.com/openimsdk/tools/errs"
|
||
"github.com/openimsdk/tools/log"
|
||
)
|
||
|
||
func NewBao(endpoint, accessKeyId, accessKeySecret, signName, verificationCodeTemplateCode string) (SMS, error) {
|
||
return &bao{
|
||
endpoint: endpoint,
|
||
accessKeyId: accessKeyId,
|
||
accessKeySecret: accessKeySecret,
|
||
signName: signName,
|
||
verificationCodeTemplateCode: verificationCodeTemplateCode,
|
||
statusMap: map[string]string{
|
||
"0": "短信发送成功",
|
||
"-1": "参数不全",
|
||
"-2": "服务器空间不支持,请确认支持curl或者fsocket",
|
||
"30": "密码错误",
|
||
"40": "账号不存在",
|
||
"41": "余额不足",
|
||
"42": "帐户已过期",
|
||
"43": "IP地址限制",
|
||
"50": "内容含有敏感词",
|
||
"51": "手机号码格式错误",
|
||
"52": "短信内容为空",
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
type bao struct {
|
||
endpoint string
|
||
accessKeyId string
|
||
accessKeySecret string
|
||
signName string
|
||
verificationCodeTemplateCode string
|
||
statusMap map[string]string
|
||
}
|
||
|
||
func (b *bao) Name() string {
|
||
return "bao-sms"
|
||
}
|
||
|
||
func (b *bao) SendCode(ctx context.Context, areaCode string, phoneNumber string, verifyCode string) error {
|
||
// 去除国家码中的+号
|
||
areaCode = strings.TrimPrefix(areaCode, "+")
|
||
|
||
// 构建完整手机号
|
||
var fullPhoneNumber string
|
||
// 对于中国手机号,短信宝只需要11位数字
|
||
if areaCode == "86" || areaCode == "086" {
|
||
fullPhoneNumber = phoneNumber // 只发送手机号,不加国家码
|
||
} else {
|
||
fullPhoneNumber = areaCode + phoneNumber
|
||
}
|
||
|
||
log.ZInfo(ctx, "SMSBao Build Phone", "areaCode", areaCode, "phoneNumber", phoneNumber, "fullPhoneNumber", fullPhoneNumber)
|
||
|
||
// 密码处理:如果已经是MD5格式(32位十六进制),直接使用;否则进行MD5加密
|
||
password := b.accessKeySecret
|
||
// 检查是否是MD5格式(32位十六进制小写字符串)
|
||
if len(b.accessKeySecret) != 32 || !isHexString(b.accessKeySecret) {
|
||
password = fmt.Sprintf("%x", md5.Sum([]byte(b.accessKeySecret)))
|
||
}
|
||
|
||
// 构建短信内容
|
||
content := fmt.Sprintf("您的验证码是%s。如非本人操作,请忽略本短信", verifyCode)
|
||
if b.signName != "" {
|
||
content = fmt.Sprintf("【%s】%s", b.signName, content)
|
||
}
|
||
|
||
log.ZInfo(ctx, "SMSBao Content", "content", content)
|
||
|
||
// 构建请求URL - 短信宝API: https://api.smsbao.com/sms?u=用户名&p=MD5密码&m=手机号&c=内容
|
||
// 如果endpoint已包含完整路径,直接使用;否则追加/sms
|
||
smsURL := b.endpoint
|
||
if !strings.Contains(smsURL, "/sms") {
|
||
if !strings.HasSuffix(smsURL, "/") {
|
||
smsURL += "/"
|
||
}
|
||
smsURL += "sms"
|
||
}
|
||
|
||
apiURL := fmt.Sprintf("%s?u=%s&p=%s&m=%s&c=%s",
|
||
smsURL,
|
||
url.QueryEscape(b.accessKeyId),
|
||
url.QueryEscape(password),
|
||
url.QueryEscape(fullPhoneNumber),
|
||
url.QueryEscape(content),
|
||
)
|
||
|
||
log.ZInfo(ctx, "SMSBao Request", "url", apiURL, "phone", fullPhoneNumber, "areaCode", areaCode, "phoneNumber", phoneNumber)
|
||
|
||
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||
if err != nil {
|
||
return errs.Wrap(err)
|
||
}
|
||
|
||
client := &http.Client{}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
log.ZError(ctx, "SMSBao Request Failed", err, "phone", fullPhoneNumber)
|
||
return errs.Wrap(err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
log.ZError(ctx, "SMSBao Read Response Failed", err, "phone", fullPhoneNumber)
|
||
return errs.Wrap(err)
|
||
}
|
||
|
||
result := strings.TrimSpace(string(body))
|
||
log.ZInfo(ctx, "SMSBao Response", "phone", fullPhoneNumber, "status", resp.StatusCode, "result", result, "rawBody", string(body))
|
||
|
||
// 返回0表示成功
|
||
if result != "0" {
|
||
errMsg := b.statusMap[result]
|
||
if errMsg == "" {
|
||
errMsg = fmt.Sprintf("未知错误代码: %s", result)
|
||
}
|
||
log.ZError(ctx, "SMSBao Failed", fmt.Errorf("%s", errMsg), "code", result, "phone", fullPhoneNumber)
|
||
return errs.New(errMsg)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// isHexString 检查字符串是否是纯十六进制字符
|
||
func isHexString(s string) bool {
|
||
for _, c := range s {
|
||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|