// 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 }