From 93e0c9c01dbccb70aa83d7bcab6862ad03adabc3 Mon Sep 17 00:00:00 2001 From: John Mai Date: Sun, 14 May 2023 14:24:46 +0800 Subject: [PATCH] :recycle: Refactor code. --- .gitignore | 3 +- README.md | 81 ++++-- core/client.go | 135 --------- core/gateway.go | 14 - core/result.go | 19 -- core/strategy.go | 6 - core/errors.go => errors.go | 2 +- gateways/aliyun/aliyun.go | 27 +- gateways/aliyun/aliyun_test.go | 220 ++++++++++----- gateways/aliyun/{contract.go => common.go} | 20 -- gateways/yunpian/{contract.go => common.go} | 20 -- gateways/yunpian/yunpian.go | 29 +- gateways/yunpian/yunpian_test.go | 262 +++++++++++------- go.mod | 8 +- go.sum | 53 +++- gsms.go | 114 +++++--- gsms_test.go | 153 ++++++++++ interfaces.go | 44 +++ logger.go | 97 +++++++ {core => message}/message.go | 42 +-- options.go | 9 +- core/phone_number.go => phone_number.go | 14 +- ...one_number_test.go => phone_number_test.go | 2 +- strategies/order.go | 3 - strategies/order_test.go | 2 +- strategies/random.go | 7 +- strategies/random_test.go | 2 +- utils/dove/dove.go | 97 +++++++ utils/dove/options.go | 20 ++ utils/utils.go | 1 + 30 files changed, 981 insertions(+), 525 deletions(-) delete mode 100644 core/client.go delete mode 100644 core/gateway.go delete mode 100644 core/result.go delete mode 100644 core/strategy.go rename core/errors.go => errors.go (98%) rename gateways/aliyun/{contract.go => common.go} (60%) rename gateways/yunpian/{contract.go => common.go} (63%) create mode 100644 gsms_test.go create mode 100644 interfaces.go create mode 100644 logger.go rename {core => message}/message.go (54%) rename core/phone_number.go => phone_number.go (84%) rename core/phone_number_test.go => phone_number_test.go (99%) create mode 100644 utils/dove/dove.go create mode 100644 utils/dove/options.go create mode 100644 utils/utils.go diff --git a/.gitignore b/.gitignore index fdf2dd0..198ab26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -example \ No newline at end of file +example +gsms_mock.go \ No newline at end of file diff --git a/README.md b/README.md index 63e9d57..4e715e3 100644 --- a/README.md +++ b/README.md @@ -46,26 +46,32 @@ package main import ( "github.com/maiqingqiang/gsms" - "github.com/maiqingqiang/gsms/core" + "github.com/maiqingqiang/gsms/gateways/aliyun" "github.com/maiqingqiang/gsms/gateways/yunpian" + "github.com/maiqingqiang/gsms/message" "log" ) func main() { - g := gsms.New( - []core.Gateway{ + client := gsms.New( + []gsms.Gateway{ &yunpian.Gateway{ - ApiKey: "f4c1c41f48120eb311111111111097", - Signature: "【默认签名】", + ApiKey: "ApiKey", + Signature: "Signature", + }, + &aliyun.Gateway{ + AccessKeyId: "AccessKeyId", + AccessKeySecret: "AccessKeySecret", + SignName: "SignName", }, }, - gsms.WithDefaultGateway([]string{ - yunpian.NAME, + gsms.WithGateways([]string{ + yunpian.NAME, aliyun.NAME, }), ) - results, err := g.Send(18888888888, &core.Message{ - Template: "SMS_00000001", + results, err := client.Send(18888888888, &message.Message{ + Template: "5532044", Data: map[string]string{ "code": "521410", }, @@ -135,48 +141,73 @@ client.Send(18888888888, &core.Message{ ## 自定义网关 -只需要实现 `core.GatewayInterface` 接口即可,例如: +只需要实现 `gsms.Gateway` 接口即可,例如: ## 场景发送 ```go -var _ core.MessageInterface = (*OrderPaidMessage)(nil) +var _ gsms.Message = (*OrderPaidMessage)(nil) type OrderPaidMessage struct { - OrderNo string + OrderNo string } func (o *OrderPaidMessage) Gateways() ([]string, error) { - return []string{yunpian.NAME}, nil + return []string{yunpian.NAME}, nil } -func (o *OrderPaidMessage) Strategy() (core.StrategyInterface, error) { - return nil, nil +func (o *OrderPaidMessage) Strategy() (gsms.Strategy, error) { + return nil, nil } -func (o *OrderPaidMessage) GetContent(gateway core.GatewayInterface) (string, error) { - return fmt.Sprintf("您的订单:%s, 已经完成付款", o.OrderNo), nil +func (o *OrderPaidMessage) GetContent(gateway gsms.Gateway) (string, error) { + return fmt.Sprintf("您的订单:%s, 已经完成付款", o.OrderNo), nil } -func (o *OrderPaidMessage) GetTemplate(gateway core.GatewayInterface) (string, error) { - return "5532044", nil +func (o *OrderPaidMessage) GetTemplate(gateway gsms.Gateway) (string, error) { + return "5532044", nil } -func (o *OrderPaidMessage) GetData(gateway core.GatewayInterface) (map[string]string, error) { - return map[string]string{ - "code": "6379", - }, nil +func (o *OrderPaidMessage) GetData(gateway gsms.Gateway) (map[string]string, error) { + return map[string]string{ + "code": "6379", + }, nil } -func (o *OrderPaidMessage) GetType(gateway core.GatewayInterface) (string, error) { - return core.TextMessage, nil +func (o *OrderPaidMessage) GetType(gateway gsms.Gateway) (string, error) { + return message.TextMessage, nil } client.Send(18888888888, &OrderPaidMessage{OrderNo: "1234"}) ``` +## 各平台配置说明 + +### [阿里云](https://www.aliyun.com/) + +短信内容使用 `Template` + `Data` + +```go +&aliyun.Gateway{ + AccessKeyId: "AccessKeyId", + AccessKeySecret: "AccessKeySecret", + SignName: "【默认签名】", +} +``` + +### [云片](https://www.yunpian.com) + +短信内容使用 `Content` + +```go +&yunpian.Gateway{ + ApiKey: "ApiKey", + Signature: "【默认签名】", // 内容中无签名时使用 +} +``` + ## 版权说明 该项目签署了 MIT 授权许可,详情请参阅 [LICENSE](LICENSE) diff --git a/core/client.go b/core/client.go deleted file mode 100644 index 3c85a91..0000000 --- a/core/client.go +++ /dev/null @@ -1,135 +0,0 @@ -package core - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "strings" -) - -type ClientInterface interface { - GetWithUnmarshal(api string, data interface{}, v ResponseInterface) (string, error) - PostFormWithUnmarshal(api string, data string, v ResponseInterface) (string, error) -} - -type ResponseInterface interface { - Unmarshal(data []byte) error -} - -type Client struct { - HttpClient *http.Client - Debug bool -} - -func NewClient(httpClient *http.Client) *Client { - return &Client{HttpClient: httpClient} -} - -func (c *Client) Do(r *http.Request) (*http.Response, error) { - if r.Header.Get("Content-Type") == "" { - r.Header.Set("Content-Type", "application/json; charset=utf-8") - } - - if c.Debug { - log.Printf("Request: %s %s %s", r.Method, r.URL.String(), r.Body) - } - - return c.HttpClient.Do(r) -} - -func (c *Client) Post(api string, data map[string]interface{}) (*http.Response, error) { - d, err := json.Marshal(data) - if err != nil { - return nil, err - } - - r, err := http.NewRequest(http.MethodPost, api, bytes.NewReader(d)) - - return c.Do(r) -} - -func (c *Client) PostForm(api string, data string) (*http.Response, error) { - r, err := http.NewRequest(http.MethodPost, api, strings.NewReader(data)) - if err != nil { - return nil, err - } - - r.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - return c.Do(r) -} - -// PostFormWithUnmarshal is a helper function to post form data and unmarshal the response -func (c *Client) PostFormWithUnmarshal(api string, data string, v ResponseInterface) (string, error) { - resp, err := c.PostForm(api, data) - if err != nil { - return "", err - } - - body, err := c.Unmarshal(resp, v) - if err != nil { - return "", err - } - - return string(body), nil -} - -func (c *Client) Get(api string, data interface{}) (*http.Response, error) { - r, err := http.NewRequest(http.MethodGet, api, nil) - if err != nil { - return nil, err - } - - switch data.(type) { - case map[string]string: - q := r.URL.Query() - for k, v := range data.(map[string]string) { - q.Add(k, v) - } - r.URL.RawQuery = q.Encode() - case url.Values: - r.URL.RawQuery = data.(url.Values).Encode() - default: - return nil, ErrRequestDataTypeError - } - - return c.Do(r) -} - -func (c *Client) GetWithUnmarshal(api string, data interface{}, v ResponseInterface) (string, error) { - resp, err := c.Get(api, data) - if err != nil { - return "", err - } - - body, err := c.Unmarshal(resp, v) - if err != nil { - return "", err - } - - return string(body), nil -} - -func (c *Client) Unmarshal(resp *http.Response, v ResponseInterface) ([]byte, error) { - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - - if err != nil { - return nil, err - } - - err = v.Unmarshal(body) - - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { - if err != nil { - return nil, fmt.Errorf("http status code: %d, error: %s", resp.StatusCode, body) - } - return nil, err - } - - return body, nil -} diff --git a/core/gateway.go b/core/gateway.go deleted file mode 100644 index fdb3015..0000000 --- a/core/gateway.go +++ /dev/null @@ -1,14 +0,0 @@ -package core - -import "time" - -type GatewayInterface interface { - // Name Get gateway name - Name() string - // Send a short message - Send(to PhoneNumberInterface, message MessageInterface, request ClientInterface) (string, error) -} - -type GatewayBase struct { - Timeout time.Duration -} diff --git a/core/result.go b/core/result.go deleted file mode 100644 index e20fe8c..0000000 --- a/core/result.go +++ /dev/null @@ -1,19 +0,0 @@ -package core - -import "fmt" - -const StatusSuccess = "success" - -const StatusFailure = "failure" - -type Result struct { - Gateway string - Status string - Template string - Result string - Error error -} - -func (r *Result) String() string { - return fmt.Sprintf("gateway: %s, status: %s, template: %s, result: %s, error: %v", r.Gateway, r.Status, r.Template, r.Result, r.Error) -} diff --git a/core/strategy.go b/core/strategy.go deleted file mode 100644 index 478f0cc..0000000 --- a/core/strategy.go +++ /dev/null @@ -1,6 +0,0 @@ -package core - -type StrategyInterface interface { - // Apply the strategy and return result. - Apply(gateways []string) []string -} diff --git a/core/errors.go b/errors.go similarity index 98% rename from core/errors.go rename to errors.go index 8d39df8..79bead7 100644 --- a/core/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -package core +package gsms import ( "errors" diff --git a/gateways/aliyun/aliyun.go b/gateways/aliyun/aliyun.go index 780ed46..0eda1ad 100644 --- a/gateways/aliyun/aliyun.go +++ b/gateways/aliyun/aliyun.go @@ -7,14 +7,15 @@ import ( "encoding/json" "fmt" "github.com/google/uuid" - "github.com/maiqingqiang/gsms/core" + "github.com/maiqingqiang/gsms" + "github.com/maiqingqiang/gsms/utils/dove" "net/http" "net/url" "strings" "time" ) -var _ core.GatewayInterface = (*Gateway)(nil) +var _ gsms.Gateway = (*Gateway)(nil) type Gateway struct { AccessKeyId string @@ -26,7 +27,7 @@ func (g *Gateway) Name() string { return NAME } -func (g *Gateway) Send(to core.PhoneNumberInterface, message core.MessageInterface, client core.ClientInterface) (string, error) { +func (g *Gateway) Send(to *gsms.PhoneNumber, message gsms.Message, config *gsms.Config) error { query := url.Values{} query.Add("RegionId", EndpointRegionId) @@ -43,30 +44,36 @@ func (g *Gateway) Send(to core.PhoneNumberInterface, message core.MessageInterfa template, err := message.GetTemplate(g) if err != nil { - return "", err + return err } query.Add("TemplateCode", template) data, err := message.GetData(g) if err != nil { - return "", err + return err } marshal, err := json.Marshal(data) if err != nil { - return "", err + return err } query.Add("TemplateParam", string(marshal)) query.Add("Signature", generateSign(http.MethodGet, g.AccessKeySecret, query)) - response := &Response{} + var response Response - body, err := client.GetWithUnmarshal(EndpointUrl, query, response) + d := dove.New(dove.WithTimeout(config.Timeout), dove.WithLogger(config.Logger)) + + err = d.Get(EndpointUrl, strings.NewReader(query.Encode()), &response) if err != nil { - return "", err + return err + } + + if response.Code != OK { + return fmt.Errorf("send failed: %+v", response) } - return body, nil + return nil } // generateSign Generate sign. diff --git a/gateways/aliyun/aliyun_test.go b/gateways/aliyun/aliyun_test.go index 68a9aee..2744681 100644 --- a/gateways/aliyun/aliyun_test.go +++ b/gateways/aliyun/aliyun_test.go @@ -1,16 +1,15 @@ package aliyun import ( - "github.com/maiqingqiang/gsms/core" + "github.com/jarcoal/httpmock" + "github.com/maiqingqiang/gsms" + "github.com/maiqingqiang/gsms/message" "github.com/stretchr/testify/assert" "net/url" - netUrl "net/url" - "strings" "testing" + "time" ) -const Success = `{"Message":"只能向已回复授权信息的手机号发送","RequestId":"F69545AD-66DC-53BE-B5BD-0E4D2E147AF7","Code":"OK"}` - func Test_generateSign(t *testing.T) { type args struct { httpMethod string @@ -65,74 +64,169 @@ func Test_generateSign(t *testing.T) { } func TestGateway_Send(t *testing.T) { - type fields struct { - AccessKeyId string - AccessKeySecret string - SignName string + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", `http://dysmsapi.aliyuncs.com`, + httpmock.NewStringResponder(200, `{"Message":"发送成功","RequestId":"F69545AD-66DC-53BE-B5BD-0E4D2E147AF1","Code":"OK"}`)) + + g := &Gateway{ + AccessKeyId: "AccessKeyId", + AccessKeySecret: "AccessKeySecret", + SignName: "SignName", } - type args struct { - to int - message core.MessageInterface - request core.ClientInterface + + config := &gsms.Config{ + Timeout: 5 * time.Second, + Logger: gsms.NewLogger().LogMode(gsms.Info), } - tests := []struct { - name string - fields fields - args args - want string - wantErr string - }{ - { - name: "", - fields: fields{ - AccessKeyId: "AccessKeyId", - AccessKeySecret: "AccessKeySecret", - SignName: "SignName", + + phoneNumber := gsms.NewPhoneNumberWithoutIDDCode(188888888888) + + err := g.Send( + phoneNumber, + &message.Message{ + Template: "SMS_00000001", + Data: map[string]string{ + "code": "9527", }, - args: args{ - to: 18888888881, - message: &core.Message{ - Content: "祝您万事如意,财源广进。", - }, - request: &ClientTest{}, + }, + config, + ) + + assert.NoError(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Template: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "SMS_271311117" + } + return "5532011" + }, + Data: func(gateway gsms.Gateway) map[string]string { + if gateway.Name() == NAME { + return map[string]string{ + "code": "1111", + } + } + return map[string]string{ + "code": "6379", + } }, - want: Success, - wantErr: "", }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := &Gateway{ - AccessKeyId: tt.fields.AccessKeyId, - AccessKeySecret: tt.fields.AccessKeySecret, - SignName: tt.fields.SignName, - } - - phoneNumber := core.NewPhoneNumberWithoutIDDCode(tt.args.to) - - got, err := g.Send(phoneNumber, tt.args.message, tt.args.request) - if (err != nil) != (tt.wantErr != "") { - assert.ErrorContainsf(t, err, tt.wantErr, "Send(%d, %v)", tt.args.to, tt.args.message) - } - assert.Equalf(t, tt.want, got, "Send(%v, %v, %v)", tt.args.to, tt.args.message, tt.args.request) - }) - } -} + config, + ) + + assert.NoError(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Content: "【Gsms】您的验证码是521410", + }, + config, + ) + + assert.NoError(t, err) -var _ core.ClientInterface = (*ClientTest)(nil) + err = g.Send( + phoneNumber, + &message.Message{ + Content: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "【Gsms】您的验证码是521410" + } + return "【Gsms】活动验证码是111" + }, + }, + config, + ) -type ClientTest struct { + assert.NoError(t, err) } -func (c ClientTest) GetWithUnmarshal(api string, data interface{}, v core.ResponseInterface) (string, error) { - body := Success - if strings.Contains(api, "single_send") && data.(netUrl.Values).Get("mobile") == "18888888881" { - body = `{"Message":"OK","RequestId":"F69545AD-66DC-53BE-B5BD-0E4D2E147AF7","Code":"isv.SMS_TEST_NUMBER_LIMIT","BizId":"641921677331750484^0"}` +func TestGateway_Send_Failed(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", `http://dysmsapi.aliyuncs.com`, + httpmock.NewStringResponder(200, `{"Message":"发送失败","RequestId":"F69545AD-66DC-53BE-B5BD-0E4D2E147AF1","Code":"FAIL"}`)) + + g := &Gateway{ + AccessKeyId: "AccessKeyId", + AccessKeySecret: "AccessKeySecret", + SignName: "SignName", } - return body, v.Unmarshal([]byte(body)) -} + config := &gsms.Config{ + Timeout: 5 * time.Second, + Logger: gsms.NewLogger().LogMode(gsms.Info), + } + + phoneNumber := gsms.NewPhoneNumberWithoutIDDCode(188888888888) + + err := g.Send( + phoneNumber, + &message.Message{ + Template: "SMS_00000001", + Data: map[string]string{ + "code": "9527", + }, + }, + config, + ) + + assert.Error(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Template: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "SMS_271311117" + } + return "5532011" + }, + Data: func(gateway gsms.Gateway) map[string]string { + if gateway.Name() == NAME { + return map[string]string{ + "code": "1111", + } + } + return map[string]string{ + "code": "6379", + } + }, + }, + config, + ) + + assert.Error(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Content: "【Gsms】您的验证码是521410", + }, + config, + ) + + assert.Error(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Content: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "【Gsms】您的验证码是521410" + } + return "【Gsms】活动验证码是111" + }, + }, + config, + ) -func (c ClientTest) PostFormWithUnmarshal(api string, data string, v core.ResponseInterface) (string, error) { - panic("implement me") + assert.Error(t, err) } diff --git a/gateways/aliyun/contract.go b/gateways/aliyun/common.go similarity index 60% rename from gateways/aliyun/contract.go rename to gateways/aliyun/common.go index dc2adfa..bcece41 100644 --- a/gateways/aliyun/contract.go +++ b/gateways/aliyun/common.go @@ -1,11 +1,5 @@ package aliyun -import ( - "encoding/json" - "fmt" - "github.com/maiqingqiang/gsms/core" -) - const NAME = "aliyun" const EndpointUrl = "http://dysmsapi.aliyuncs.com" @@ -24,8 +18,6 @@ const EndpointSignatureVersion = "1.0" const OK = "OK" -var _ core.ResponseInterface = (*Response)(nil) - // Response // https://help.aliyun.com/document_detail/419273.htm?spm=a2c4g.11186623.0.0.4a0879bebUrJyq#api-detail-40 type Response struct { @@ -34,15 +26,3 @@ type Response struct { BizId string `json:"BizId"` RequestId string `json:"RequestId"` } - -func (r *Response) Unmarshal(data []byte) error { - if err := json.Unmarshal(data, r); err != nil { - return fmt.Errorf("unmarshal response failed: %w data: %s", err, data) - } - - if r.Code != OK { - return fmt.Errorf("send failed code:%s msg:%s", r.Code, r.Message) - } - - return nil -} diff --git a/gateways/yunpian/contract.go b/gateways/yunpian/common.go similarity index 63% rename from gateways/yunpian/contract.go rename to gateways/yunpian/common.go index 4113840..88f42e7 100644 --- a/gateways/yunpian/contract.go +++ b/gateways/yunpian/common.go @@ -1,11 +1,5 @@ package yunpian -import ( - "encoding/json" - "fmt" - "github.com/maiqingqiang/gsms/core" -) - const NAME = "yunpian" const EndpointTemplate = "https://%s.yunpian.com/%s/%s/%s.%s" const EndpointVersion = "v2" @@ -20,22 +14,8 @@ const MethodSingleSend = "single_send" // MethodTplSingleSend https://www.yunpian.com/official/document/sms/zh_CN/domestic_tpl_single_send const MethodTplSingleSend = "tpl_single_send" -var _ core.ResponseInterface = (*Response)(nil) - type Response struct { Code int `json:"code"` // 系统返回码 Msg string `json:"msg"` // 例如""发送成功"",或者相应错误信息 Detail string `json:"detail"` // 具体错误描述或解决方法 } - -func (r *Response) Unmarshal(data []byte) error { - if err := json.Unmarshal(data, r); err != nil { - return fmt.Errorf("unmarshal response failed: %w data: %s", err, data) - } - - if r.Code != SuccessCode { - return fmt.Errorf("send failed code:%d msg:%s detail:%s", r.Code, r.Msg, r.Detail) - } - - return nil -} diff --git a/gateways/yunpian/yunpian.go b/gateways/yunpian/yunpian.go index a99addb..84aebac 100644 --- a/gateways/yunpian/yunpian.go +++ b/gateways/yunpian/yunpian.go @@ -2,12 +2,13 @@ package yunpian import ( "fmt" - "github.com/maiqingqiang/gsms/core" + "github.com/maiqingqiang/gsms" + "github.com/maiqingqiang/gsms/utils/dove" "net/url" "strings" ) -var _ core.GatewayInterface = (*Gateway)(nil) +var _ gsms.Gateway = (*Gateway)(nil) type Gateway struct { ApiKey string @@ -15,8 +16,7 @@ type Gateway struct { } // Send message. -func (g *Gateway) Send(to core.PhoneNumberInterface, message core.MessageInterface, client core.ClientInterface) (string, error) { - +func (g *Gateway) Send(to *gsms.PhoneNumber, message gsms.Message, config *gsms.Config) (err error) { p := url.Values{} method := MethodSingleSend p.Add("apikey", g.ApiKey) @@ -24,17 +24,17 @@ func (g *Gateway) Send(to core.PhoneNumberInterface, message core.MessageInterfa template, err := message.GetTemplate(g) if err != nil { - return "", err + return } data, err := message.GetData(g) if err != nil { - return "", err + return } content, err := message.GetContent(g) if err != nil { - return "", err + return } if template != "" { @@ -51,13 +51,20 @@ func (g *Gateway) Send(to core.PhoneNumberInterface, message core.MessageInterfa endpoint := buildEndpoint(ProductSms, ResourceSms, method) - response := &Response{} + var response Response + + d := dove.New(dove.WithTimeout(config.Timeout), dove.WithLogger(config.Logger)) - body, err := client.PostFormWithUnmarshal(endpoint, p.Encode(), response) + err = d.PostForm(endpoint, strings.NewReader(p.Encode()), &response) if err != nil { - return "", err + return err } - return body, nil + + if response.Code != SuccessCode { + return fmt.Errorf("send failed code:%d msg:%s detail:%s", response.Code, response.Msg, response.Detail) + } + + return } // buildEndpoint Build endpoint url. diff --git a/gateways/yunpian/yunpian_test.go b/gateways/yunpian/yunpian_test.go index 079d84b..b9ccbf0 100644 --- a/gateways/yunpian/yunpian_test.go +++ b/gateways/yunpian/yunpian_test.go @@ -1,10 +1,12 @@ package yunpian import ( - "github.com/maiqingqiang/gsms/core" + "github.com/jarcoal/httpmock" + "github.com/maiqingqiang/gsms" + "github.com/maiqingqiang/gsms/message" "github.com/stretchr/testify/assert" - "strings" "testing" + "time" ) const NotSupportCountry = `{"http_status_code":400,"code":20,"msg":"暂不支持的国家地区","detail":"请确认号码归属地"}` @@ -39,104 +41,175 @@ func Test_buildEndpoint(t *testing.T) { } func TestGateway_Send(t *testing.T) { - type fields struct { - ApiKey string - Signature string + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", `https://sms.yunpian.com/v2/sms/single_send.json`, + httpmock.NewStringResponder(200, `{"code":0,"msg":"发送成功","count":1,"fee":0.05,"unit":"RMB","mobile":"18888888888","sid":74712264988}`)) + + httpmock.RegisterResponder("POST", `https://sms.yunpian.com/v2/sms/tpl_single_send.json`, + httpmock.NewStringResponder(200, `{"code":0,"msg":"发送成功","count":1,"fee":0.05,"unit":"RMB","mobile":"18888888888","sid":74712264988}`)) + + g := &Gateway{ + ApiKey: "ApiKey", + Signature: "Signature", } - type args struct { - to int - message core.MessageInterface + + config := &gsms.Config{ + Timeout: 5 * time.Second, + Logger: gsms.NewLogger().LogMode(gsms.Info), } - tests := []struct { - name string - fields fields - args args - want string - wantErr string - }{ - { - name: "single send 1", - fields: fields{ - ApiKey: "ApiKey", - Signature: "【云片】", - }, - args: args{ - to: 18888888881, - message: &core.Message{ - Content: "祝您万事如意,财源广进。", - }, + + phoneNumber := gsms.NewPhoneNumberWithoutIDDCode(188888888888) + + err := g.Send( + phoneNumber, + &message.Message{ + Template: "SMS_00000001", + Data: map[string]string{ + "code": "9527", }, - want: "", - wantErr: "暂不支持的国家地区", }, - { - name: "single send 2", - fields: fields{ - ApiKey: "ApiKey", - Signature: "【云片】", + config, + ) + + assert.NoError(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Template: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "SMS_271311117" + } + return "5532011" }, - args: args{ - to: 18888888882, - message: &core.Message{ - Content: "祝您万事如意,财源广进。", - }, + Data: func(gateway gsms.Gateway) map[string]string { + if gateway.Name() == NAME { + return map[string]string{ + "code": "1111", + } + } + return map[string]string{ + "code": "6379", + } }, - want: Success, - wantErr: "", }, - { - name: "tpl single send 1", - fields: fields{ - ApiKey: "ApiKey", - Signature: "【云片】", + config, + ) + + assert.NoError(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Content: "【Gsms】您的验证码是521410", + }, + config, + ) + + assert.NoError(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Content: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "【Gsms】您的验证码是521410" + } + return "【Gsms】活动验证码是111" }, - args: args{ - to: 18888888881, - message: &core.Message{ - Template: "15320323", - Data: map[string]string{ - "code": "6379", - }, - }, + }, + config, + ) + + assert.NoError(t, err) +} + +func TestGateway_Send_Failed(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", `https://sms.yunpian.com/v2/sms/single_send.json`, + httpmock.NewStringResponder(200, `{"http_status_code":400,"code":20,"msg":"暂不支持的国家地区","detail":"请确认号码归属地"}`)) + + httpmock.RegisterResponder("POST", `https://sms.yunpian.com/v2/sms/tpl_single_send.json`, + httpmock.NewStringResponder(200, `{"http_status_code":400,"code":20,"msg":"暂不支持的国家地区","detail":"请确认号码归属地"}`)) + + g := &Gateway{ + ApiKey: "ApiKey", + Signature: "Signature", + } + + config := &gsms.Config{ + Timeout: 5 * time.Second, + Logger: gsms.NewLogger().LogMode(gsms.Info), + } + + phoneNumber := gsms.NewPhoneNumberWithoutIDDCode(188888888888) + + err := g.Send( + phoneNumber, + &message.Message{ + Template: "SMS_00000001", + Data: map[string]string{ + "code": "9527", }, - want: "", - wantErr: "暂不支持的国家地区", - }, { - name: "tpl single send 2", - fields: fields{ - ApiKey: "ApiKey", - Signature: "【云片】", + }, + config, + ) + + assert.Error(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Template: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "SMS_271311117" + } + return "5532011" }, - args: args{ - to: 18888888882, - message: &core.Message{ - Template: "15320323", - Data: map[string]string{ - "code": "6379", - }, - }, + Data: func(gateway gsms.Gateway) map[string]string { + if gateway.Name() == NAME { + return map[string]string{ + "code": "1111", + } + } + return map[string]string{ + "code": "6379", + } }, - want: Success, - wantErr: "", }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gateway := &Gateway{ - ApiKey: tt.fields.ApiKey, - Signature: tt.fields.Signature, - } + config, + ) - phoneNumber := core.NewPhoneNumberWithoutIDDCode(tt.args.to) + assert.Error(t, err) - got, err := gateway.Send(phoneNumber, tt.args.message, &ClientTest{}) - if (err != nil) != (tt.wantErr != "") { - assert.ErrorContainsf(t, err, tt.wantErr, "Send(%d, %v)", tt.args.to, tt.args.message) - } + err = g.Send( + phoneNumber, + &message.Message{ + Content: "【Gsms】您的验证码是521410", + }, + config, + ) - assert.Equalf(t, tt.want, got, "Send(%v, %v)", tt.args.to, tt.args.message) - }) - } + assert.Error(t, err) + + err = g.Send( + phoneNumber, + &message.Message{ + Content: func(gateway gsms.Gateway) string { + if gateway.Name() == NAME { + return "【Gsms】您的验证码是521410" + } + return "【Gsms】活动验证码是111" + }, + }, + config, + ) + + assert.Error(t, err) } func Test_buildTplVal(t *testing.T) { @@ -164,22 +237,3 @@ func Test_buildTplVal(t *testing.T) { }) } } - -var _ core.ClientInterface = (*ClientTest)(nil) - -type ClientTest struct { -} - -func (c ClientTest) GetWithUnmarshal(api string, data interface{}, v core.ResponseInterface) (string, error) { - //TODO implement me - panic("implement me") -} - -func (c ClientTest) PostFormWithUnmarshal(api string, data string, v core.ResponseInterface) (string, error) { - body := Success - if strings.Contains(api, "single_send") && strings.Contains(data, "18888888881") { - body = NotSupportCountry - } - - return body, v.Unmarshal([]byte(body)) -} diff --git a/go.mod b/go.mod index 2fb0ccf..b9ddcf4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,11 @@ module github.com/maiqingqiang/gsms go 1.16 require ( + github.com/golang/mock v1.6.0 + github.com/google/uuid v1.3.0 + github.com/jarcoal/httpmock v1.3.0 github.com/stretchr/testify v1.8.1 + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 ) - -require github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 6db3d4e..85553ab 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,74 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gsms.go b/gsms.go index 6696396..585ecad 100644 --- a/gsms.go +++ b/gsms.go @@ -1,34 +1,58 @@ package gsms import ( - "github.com/maiqingqiang/gsms/core" + "fmt" "github.com/maiqingqiang/gsms/strategies" - "net/http" "strconv" "time" ) type Gsms struct { - DefaultGateways []string - Timeout time.Duration - Strategy core.StrategyInterface - Gateways map[string]core.GatewayInterface - client *core.Client + config *Config + defaultGateways []string + strategy Strategy + gateways map[string]Gateway } -func New(gateways []core.GatewayInterface, options ...Option) *Gsms { - gatewaysMap := make(map[string]core.GatewayInterface, len(gateways)) +// Config gsms config. +type Config struct { + Timeout time.Duration + Logger Logger +} + +// StatusSuccess send message success. +const StatusSuccess = "success" + +// StatusFailure send message failure. +const StatusFailure = "failure" + +// Result Gateway send message result. +type Result struct { + Gateway string + Status string + Template string + Error error +} + +func (r *Result) String() string { + return fmt.Sprintf("gateway: %s, status: %s, template: %s, error: %v", r.Gateway, r.Status, r.Template, r.Error) +} + +// New a gsms instance. +func New(gateways []Gateway, options ...Option) *Gsms { + gatewaysMap := make(map[string]Gateway, len(gateways)) for _, gateway := range gateways { gatewaysMap[gateway.Name()] = gateway } gsms := &Gsms{ - Gateways: gatewaysMap, - Strategy: &strategies.OrderStrategy{}, - client: core.NewClient(&http.Client{ - Timeout: time.Second * 5, - }), + config: &Config{ + Timeout: 5 * time.Second, + Logger: NewLogger(), + }, + gateways: gatewaysMap, + strategy: &strategies.OrderStrategy{}, } for _, option := range options { @@ -39,7 +63,7 @@ func New(gateways []core.GatewayInterface, options ...Option) *Gsms { } // Send a message. -func (g *Gsms) Send(to interface{}, message core.MessageInterface, gateways ...string) ([]*core.Result, error) { +func (g *Gsms) Send(to interface{}, message Message, gateways ...string) ([]*Result, error) { if len(gateways) == 0 { var err error @@ -50,96 +74,110 @@ func (g *Gsms) Send(to interface{}, message core.MessageInterface, gateways ...s } if len(gateways) == 0 { - gateways = g.DefaultGateways + gateways = g.defaultGateways } gateways = g.formatGateways(message, gateways) - var phoneNumber *core.PhoneNumber + var phoneNumber *PhoneNumber switch to.(type) { - case *core.PhoneNumber: - phoneNumber = to.(*core.PhoneNumber) + case *PhoneNumber: + phoneNumber = to.(*PhoneNumber) case int: - phoneNumber = core.NewPhoneNumberWithoutIDDCode(to.(int)) + phoneNumber = NewPhoneNumberWithoutIDDCode(to.(int)) case string: number, err := strconv.Atoi(to.(string)) if err != nil { return nil, err } - phoneNumber = core.NewPhoneNumberWithoutIDDCode(number) + phoneNumber = NewPhoneNumberWithoutIDDCode(number) default: - return nil, core.ErrInvalidPhoneNumber + return nil, ErrInvalidPhoneNumber } - var results []*core.Result + var results []*Result isSuccessful := false for _, gateway := range gateways { - result := &core.Result{ + result := &Result{ Gateway: gateway, - Status: core.StatusSuccess, + Status: StatusSuccess, } - var gw core.GatewayInterface + var gw Gateway gw, result.Error = g.Gateway(gateway) if result.Error != nil { - result.Status = core.StatusFailure + result.Status = StatusFailure results = append(results, result) continue } result.Template, result.Error = message.GetTemplate(gw) if result.Error != nil { - result.Status = core.StatusFailure + result.Status = StatusFailure results = append(results, result) continue } - result.Result, result.Error = gw.Send(phoneNumber, message, g.client) + g.config.Logger.Infof("[%s] start send [template: %s] message", gateway, result.Template) + + result.Error = gw.Send(phoneNumber, message, g.config) if result.Error != nil { - result.Status = core.StatusFailure + result.Status = StatusFailure + g.config.Logger.Warnf("[%s] send [template: %s] message failed: %+v", gateway, result.Template, result.Error) + } else { + g.config.Logger.Infof("[%s] send [template: %s] message success", gateway, result.Template) } + g.config.Logger.Infof("[%s] end send [template: %s] message\n", gateway, result.Template) + results = append(results, result) - if result.Status == core.StatusSuccess { + if result.Status == StatusSuccess { isSuccessful = true break } } if !isSuccessful { - return nil, core.NewErrGatewayFailed(results) + return nil, NewErrGatewayFailed(results) } return results, nil } // Gateway Get gateway by name -func (g *Gsms) Gateway(name string) (core.GatewayInterface, error) { - if gateway, ok := g.Gateways[name]; ok { +func (g *Gsms) Gateway(name string) (Gateway, error) { + if gateway, ok := g.gateways[name]; ok { return gateway, nil } - return nil, core.ErrGatewayNotFound + return nil, ErrGatewayNotFound } // formatGateways format gateways -func (g *Gsms) formatGateways(message core.MessageInterface, gateways []string) []string { +func (g *Gsms) formatGateways(message Message, gateways []string) []string { if strategy, err := message.Strategy(); err == nil && strategy != nil { return strategy.Apply(gateways) } - if g.Strategy != nil { - return g.Strategy.Apply(gateways) + if g.strategy != nil { + return g.strategy.Apply(gateways) } return gateways } + +// Debug set logger level to info +func (g *Gsms) Debug() *Gsms { + newGsms := *g + newGsms.config.Logger = newGsms.config.Logger.LogMode(Info) + return &newGsms +} diff --git a/gsms_test.go b/gsms_test.go new file mode 100644 index 0000000..c2de23e --- /dev/null +++ b/gsms_test.go @@ -0,0 +1,153 @@ +package gsms + +import ( + "fmt" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "testing" +) + +type Test1Gateway struct { +} + +func (t Test1Gateway) Name() string { + return "Test1" +} + +func (t Test1Gateway) Send(to *PhoneNumber, message Message, config *Config) error { + return nil +} + +var _ Gateway = (*Test1Gateway)(nil) + +type Test2Gateway struct { +} + +func (t Test2Gateway) Name() string { + return "Test2" +} + +func (t Test2Gateway) Send(to *PhoneNumber, message Message, config *Config) error { + return nil +} + +var _ Gateway = (*Test2Gateway)(nil) + +func TestGsms_Gateway(t *testing.T) { + type fields struct { + config *Config + DefaultGateways []string + Strategy Strategy + Gateways map[string]Gateway + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want Gateway + wantErr assert.ErrorAssertionFunc + }{ + { + name: "gsms.Gateway Return Test1", + fields: fields{}, + args: args{ + name: "Test1", + }, + want: &Test1Gateway{}, + wantErr: assert.NoError, + }, + { + name: "gsms.Gateway Return Test2", + fields: fields{}, + args: args{ + name: "Test2", + }, + want: &Test2Gateway{}, + wantErr: assert.NoError, + }, + { + name: "gsms.Gateway Return Test3", + fields: fields{}, + args: args{ + name: "Test3", + }, + want: nil, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := New([]Gateway{ + &Test1Gateway{}, + &Test2Gateway{}, + }) + + got, err := g.Gateway(tt.args.name) + if !tt.wantErr(t, err, fmt.Sprintf("Gateway(%v)", tt.args.name)) { + return + } + assert.Equalf(t, tt.want, got, "Gateway(%v)", tt.args.name) + }) + } +} + +func TestGsms_Send(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMessage := NewMockMessage(ctrl) + mockGateway := NewMockGateway(ctrl) + + mockMessage.EXPECT().Gateways().Return(nil, nil) + mockMessage.EXPECT().Strategy().Return(nil, nil) + mockMessage.EXPECT().GetTemplate(mockGateway).Return("SMS_00000001", nil) + + mockGateway.EXPECT().Name().Return("mockGateway") + mockGateway.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + g := New([]Gateway{ + mockGateway, + }, WithGateways([]string{ + "mockGateway", + })) + + result, err := g.Debug().Send(18888888888, mockMessage) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, result, []*Result{ + { + Gateway: "mockGateway", + Status: "success", + Template: "SMS_00000001", + Error: nil, + }, + }) +} + +func TestGsms_Send_Failed(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMessage := NewMockMessage(ctrl) + mockGateway := NewMockGateway(ctrl) + + mockMessage.EXPECT().Gateways().Return(nil, nil) + mockMessage.EXPECT().Strategy().Return(nil, nil) + mockMessage.EXPECT().GetTemplate(gomock.Any()).Return("SMS_00000001", nil) + + mockGateway.EXPECT().Name().Return("mockGateway") + mockGateway.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("send failed")) + + g := New([]Gateway{ + mockGateway, + }, WithGateways([]string{ + "mockGateway", + })) + _, err := g.Debug().Send(18888888888, mockMessage) + assert.Error(t, err) +} diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..f047c0e --- /dev/null +++ b/interfaces.go @@ -0,0 +1,44 @@ +//go:generate mockgen -source=interfaces.go -destination=gsms_mock.go -package=gsms + +package gsms + +type Gateway interface { + // Name Get gateway name + Name() string + // Send a short message + Send(to *PhoneNumber, message Message, config *Config) error +} + +// Message interface. +type Message interface { + // Gateways Supported gateways. + Gateways() ([]string, error) + // Strategy Message strategy. + Strategy() (Strategy, error) + // GetContent Get message content. + GetContent(gateway Gateway) (string, error) + // GetTemplate Get message template. + GetTemplate(gateway Gateway) (string, error) + // GetData Get message data. + GetData(gateway Gateway) (map[string]string, error) + // GetType Get message type. + GetType(gateway Gateway) (string, error) +} + +type Strategy interface { + // Apply the strategy and return result. + Apply(gateways []string) []string +} + +// LogLevel log level +type LogLevel int + +type Logger interface { + LogMode(LogLevel) Logger + Info(...interface{}) + Infof(string, ...interface{}) + Warn(...interface{}) + Warnf(string, ...interface{}) + Error(...interface{}) + Errorf(string, ...interface{}) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..352b30d --- /dev/null +++ b/logger.go @@ -0,0 +1,97 @@ +package gsms + +import ( + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +const ( + // Silent silent log level + Silent LogLevel = iota + 1 + // Error error log level + Error + // Warn warn log level + Warn + // Info info log level + Info +) + +type logger struct { + level LogLevel + writer *zap.Logger +} + +const callerSkipOffset = 2 + +func NewLogger(opts ...zap.Option) Logger { + opts = append(opts, zap.AddCallerSkip(callerSkipOffset)) + + config := zap.Config{ + Level: zap.NewAtomicLevelAt(zap.InfoLevel), + Development: false, + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + + writer, err := config.Build(opts...) + if err != nil { + panic(err) + } + + return &logger{ + level: Warn, + writer: writer, + } +} + +func (l *logger) LogMode(level LogLevel) Logger { + newLogger := *l + newLogger.level = level + return &newLogger +} + +func (l *logger) Info(v ...interface{}) { + if l.level >= Info { + l.writer.Info(fmt.Sprint(v...)) + } +} + +func (l *logger) Infof(format string, v ...interface{}) { + l.Info(fmt.Sprintf(format, v...)) +} + +func (l *logger) Warn(v ...interface{}) { + if l.level >= Warn { + l.writer.Warn(fmt.Sprint(v...)) + } +} + +func (l *logger) Warnf(format string, v ...interface{}) { + l.Warn(fmt.Sprintf(format, v...)) +} + +func (l *logger) Error(v ...interface{}) { + if l.level >= Error { + l.writer.Error(fmt.Sprint(v...)) + } +} + +func (l *logger) Errorf(format string, v ...interface{}) { + l.Error(fmt.Sprintf(format, v...)) +} diff --git a/core/message.go b/message/message.go similarity index 54% rename from core/message.go rename to message/message.go index ebcac68..65df290 100644 --- a/core/message.go +++ b/message/message.go @@ -1,4 +1,8 @@ -package core +package message + +import ( + "github.com/maiqingqiang/gsms" +) // TextMessage Text message type. const TextMessage = "text" @@ -6,22 +10,6 @@ const TextMessage = "text" // VoiceMessage Voice message type. const VoiceMessage = "voice" -// MessageInterface Message interface. -type MessageInterface interface { - // Gateways Supported gateways. - Gateways() ([]string, error) - // Strategy Message strategy. - Strategy() (StrategyInterface, error) - // GetContent Get message content. - GetContent(gateway GatewayInterface) (string, error) - // GetTemplate Get message template. - GetTemplate(gateway GatewayInterface) (string, error) - // GetData Get message data. - GetData(gateway GatewayInterface) (map[string]string, error) - // GetType Get message type. - GetType(gateway GatewayInterface) (string, error) -} - // ContentFunc Content function. type ContentFunc func(gatewayName string) string @@ -34,7 +22,7 @@ type DataFunc func(gatewayName string) map[string]string // TypeFunc Type function. type TypeFunc func(gatewayName string) string -var _ MessageInterface = (*Message)(nil) +//var _ gsms.Message = (*Message)(nil) type Message struct { Content interface{} @@ -49,27 +37,27 @@ func (m *Message) Gateways() ([]string, error) { } // Strategy Message strategy. -func (m *Message) Strategy() (StrategyInterface, error) { +func (m *Message) Strategy() (gsms.Strategy, error) { return nil, nil } // GetContent Get message content. -func (m *Message) GetContent(gateway GatewayInterface) (string, error) { +func (m *Message) GetContent(gateway gsms.Gateway) (string, error) { switch content := m.Content.(type) { case string: return content, nil - case func(gateway GatewayInterface) string: + case func(gateway gsms.Gateway) string: return content(gateway), nil } return "", nil } // GetTemplate Get message template. -func (m *Message) GetTemplate(gateway GatewayInterface) (string, error) { +func (m *Message) GetTemplate(gateway gsms.Gateway) (string, error) { switch template := m.Template.(type) { case string: return template, nil - case func(gateway GatewayInterface) string: + case func(gateway gsms.Gateway) string: return template(gateway), nil } @@ -77,11 +65,11 @@ func (m *Message) GetTemplate(gateway GatewayInterface) (string, error) { } // GetData Get message data. -func (m *Message) GetData(gateway GatewayInterface) (map[string]string, error) { +func (m *Message) GetData(gateway gsms.Gateway) (map[string]string, error) { switch data := m.Data.(type) { case map[string]string: return data, nil - case func(gateway GatewayInterface) map[string]string: + case func(gateway gsms.Gateway) map[string]string: return data(gateway), nil } @@ -89,11 +77,11 @@ func (m *Message) GetData(gateway GatewayInterface) (map[string]string, error) { } // GetType Get message type. -func (m *Message) GetType(gateway GatewayInterface) (string, error) { +func (m *Message) GetType(gateway gsms.Gateway) (string, error) { switch messageType := m.Type.(type) { case string: return messageType, nil - case func(gateway GatewayInterface) string: + case func(gateway gsms.Gateway) string: return messageType(gateway), nil } diff --git a/options.go b/options.go index c38fa8a..50903e9 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,6 @@ package gsms import ( - "github.com/maiqingqiang/gsms/core" "time" ) @@ -10,20 +9,20 @@ type Option func(*Gsms) // WithTimeout set the timeout. func WithTimeout(timeout time.Duration) func(*Gsms) { return func(gsms *Gsms) { - gsms.client.HttpClient.Timeout = timeout + gsms.config.Timeout = timeout } } // WithGateways set the gateways. func WithGateways(gateways []string) func(*Gsms) { return func(gsms *Gsms) { - gsms.DefaultGateways = gateways + gsms.defaultGateways = gateways } } // WithStrategy set the strategy. -func WithStrategy(strategy core.StrategyInterface) func(*Gsms) { +func WithStrategy(strategy Strategy) func(*Gsms) { return func(gsms *Gsms) { - gsms.Strategy = strategy + gsms.strategy = strategy } } diff --git a/core/phone_number.go b/phone_number.go similarity index 84% rename from core/phone_number.go rename to phone_number.go index 6220a3a..22d3853 100644 --- a/core/phone_number.go +++ b/phone_number.go @@ -1,4 +1,4 @@ -package core +package gsms import ( "fmt" @@ -6,16 +6,6 @@ import ( "strings" ) -type PhoneNumberInterface interface { - Number() int - IDDCode() int - UniversalNumber() string - ZeroPrefixedNumber() string - PrefixedIDDCode(prefix string) string - String() string - InChineseMainland() bool -} - type PhoneNumber struct { number int iddCode int @@ -50,7 +40,7 @@ func (p *PhoneNumber) IDDCode() int { return p.iddCode } -//UniversalNumber e.g. +8613800138000. +// UniversalNumber e.g. +8613800138000. func (p *PhoneNumber) UniversalNumber() string { return fmt.Sprintf("%s%d", p.PrefixedIDDCode("+"), p.number) } diff --git a/core/phone_number_test.go b/phone_number_test.go similarity index 99% rename from core/phone_number_test.go rename to phone_number_test.go index 7a16044..0db0413 100644 --- a/core/phone_number_test.go +++ b/phone_number_test.go @@ -1,4 +1,4 @@ -package core +package gsms import ( "github.com/stretchr/testify/assert" diff --git a/strategies/order.go b/strategies/order.go index 6c627af..29c7c06 100644 --- a/strategies/order.go +++ b/strategies/order.go @@ -1,12 +1,9 @@ package strategies import ( - "github.com/maiqingqiang/gsms/core" "sort" ) -var _ core.StrategyInterface = (*OrderStrategy)(nil) - type OrderStrategy struct { } diff --git a/strategies/order_test.go b/strategies/order_test.go index 4ee0699..53eabc5 100644 --- a/strategies/order_test.go +++ b/strategies/order_test.go @@ -15,7 +15,7 @@ func TestOrderStrategy_Apply(t *testing.T) { want []string }{ { - name: "Order Strategy", + name: "Order strategy", args: args{ gateways: []string{"yunpian", "aliyun", "aliyunrest", "aliyunintl", "submail", "huyi", "juhe"}, }, diff --git a/strategies/random.go b/strategies/random.go index cfd2be2..f6fe95a 100644 --- a/strategies/random.go +++ b/strategies/random.go @@ -1,19 +1,16 @@ package strategies import ( - "github.com/maiqingqiang/gsms/core" "math/rand" "time" ) -var _ core.StrategyInterface = (*RandomStrategy)(nil) - type RandomStrategy struct { } func (o *RandomStrategy) Apply(gateways []string) []string { - rand.Seed(time.Now().UnixNano()) - rand.Shuffle( + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle( len(gateways), func(i, j int) { gateways[i], gateways[j] = gateways[j], gateways[i] diff --git a/strategies/random_test.go b/strategies/random_test.go index f1deb62..d7661eb 100644 --- a/strategies/random_test.go +++ b/strategies/random_test.go @@ -15,7 +15,7 @@ func TestRandomStrategy_Apply(t *testing.T) { want []string }{ { - name: "Random Strategy", + name: "Random strategy", args: args{ gateways: []string{"yunpian", "aliyun", "aliyunrest", "aliyunintl", "submail", "huyi", "juhe"}, }, diff --git a/utils/dove/dove.go b/utils/dove/dove.go new file mode 100644 index 0000000..3242663 --- /dev/null +++ b/utils/dove/dove.go @@ -0,0 +1,97 @@ +package dove + +import ( + "encoding/json" + "fmt" + "github.com/maiqingqiang/gsms" + "io" + "net/http" +) + +type StatusCodeJudger func(statusCode int) error +type Unmarshal func(data []byte, v interface{}) error + +type Dove struct { + client *http.Client + statusCodeJudger StatusCodeJudger + unmarshal Unmarshal + logger gsms.Logger +} + +func New(opts ...Option) *Dove { + dove := &Dove{ + client: http.DefaultClient, + statusCodeJudger: func(statusCode int) error { + if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { + return fmt.Errorf("request status code: %d", statusCode) + } + + return nil + }, + unmarshal: json.Unmarshal, + } + + for _, opt := range opts { + opt(dove) + } + + return dove +} + +func (d *Dove) Request(method, url string, header http.Header, data io.Reader, response interface{}) error { + req, err := http.NewRequest(method, url, data) + if err != nil { + return err + } + + if header != nil { + req.Header = header + } + + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json; charset=utf-8") + } + + d.logger.Infof("request url: %s , method: %s , header: %+v , reqBody: %s", req.URL, req.Method, req.Header, req.Body) + + resp, err := d.client.Do(req) + if err != nil { + d.logger.Warnf("request failed: %v", err) + return err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + d.logger.Warnf("io.ReadAll failed: %v", err) + return err + } + + d.logger.Infof("response status code: %d , body: %s", resp.StatusCode, body) + + err = d.statusCodeJudger(resp.StatusCode) + if err != nil { + return err + } + + err = d.unmarshal(body, response) + if err != nil { + return err + } + + return nil +} + +func (d *Dove) Get(url string, data io.Reader, response interface{}) error { + return d.Request(http.MethodGet, url, nil, data, response) +} + +func (d *Dove) Post(url string, data io.Reader, response interface{}) error { + return d.Request(http.MethodPost, url, nil, data, response) +} + +func (d *Dove) PostForm(url string, data io.Reader, response interface{}) error { + header := http.Header{} + header.Set("Content-Type", "application/x-www-form-urlencoded") + + return d.Request(http.MethodPost, url, header, data, response) +} diff --git a/utils/dove/options.go b/utils/dove/options.go new file mode 100644 index 0000000..8b445b7 --- /dev/null +++ b/utils/dove/options.go @@ -0,0 +1,20 @@ +package dove + +import ( + "github.com/maiqingqiang/gsms" + "time" +) + +type Option func(dove *Dove) + +func WithTimeout(timeout time.Duration) Option { + return func(dove *Dove) { + dove.client.Timeout = timeout + } +} + +func WithLogger(logger gsms.Logger) Option { + return func(dove *Dove) { + dove.logger = logger + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..d4b585b --- /dev/null +++ b/utils/utils.go @@ -0,0 +1 @@ +package utils