diff --git a/.gitignore b/.gitignore index e2f1f53..a6b542a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ # vendor/ .idea .vscode +*_test.go diff --git a/go.mod b/go.mod index bd4f0b4..61d84f0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/buger/jsonparser v1.1.1 + github.com/ddliu/go-httpclient v0.6.9 github.com/gin-gonic/gin v1.7.2 github.com/go-redis/redis/v8 v8.3.4 github.com/go-redis/redis_rate/v9 v9.1.1 @@ -16,6 +17,8 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.8.1 go.uber.org/zap v1.17.0 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v2 v2.3.0 gorm.io/driver/mysql v1.0.0 gorm.io/gorm v1.21.12 diff --git a/go.sum b/go.sum index 49b3a00..bdd7efa 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL 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/ddliu/go-httpclient v0.6.9 h1:/3hsBVpcgCJwqm1dkVlnAJ9NWuYInbRc+i9FyUXX/LE= +github.com/ddliu/go-httpclient v0.6.9/go.mod h1:zM9P0OxV4OGGz1pt/ibuj0ooX2SWH9a6MvXZLbT0JMc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -142,9 +144,13 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/mail/code_tpl.html b/mail/code_tpl.html new file mode 100644 index 0000000..91549d0 --- /dev/null +++ b/mail/code_tpl.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+
+
+ 尊敬的用户:您好! + + 您正在进行{operate_description}操作,请在验证码输入框中输入:{operate_code},以完成操作。 + +
+
+ +

+ 注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全 +
(开发者不会向你索取此验证码,请勿泄漏!) +

+
+
+
+
+
+

此为系统邮件,请勿回复
+ 请保管好您的邮箱,避免账号被他人盗用 +

+

白茶清欢的个人网站

+
+
+
+ + \ No newline at end of file diff --git a/mail/mail.go b/mail/mail.go new file mode 100644 index 0000000..dbfb614 --- /dev/null +++ b/mail/mail.go @@ -0,0 +1,91 @@ +// Package mail 邮件发送配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 1:14 上午 2021/8/13 +package mail + +import ( + "fmt" + "strings" + + "git.zhangdeman.cn/zhangdeman/gopkg/util" + + "gopkg.in/gomail.v2" +) + +var ( + // Mail 邮件服务实例 + Mail *mail +) + +type mail struct { + conf map[string]string + port int + mailDialer *gomail.Dialer +} + +// NewMailMessage 获取邮件消息实例 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 1:32 上午 2021/8/13 +func NewMailMessage(fromMail string, fromMailName string, pass string, host string, port int) *mail { + if Mail == nil { + Mail = &mail{} + if len(fromMailName) == 0 { + fromMailName = "系统邮件" + } + Mail.conf = map[string]string{ + "user": fromMail, + "pass": pass, + "host": host, + "port": fmt.Sprintf("%v", port), + "from_mail_name": fromMailName, + } + + Mail.port = port + Mail.mailDialer = gomail.NewDialer(Mail.conf["host"], port, Mail.conf["user"], Mail.conf["pass"]) + } + return Mail +} + +// BaseSend 基础邮件发送 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 1:14 上午 2021/8/13 +func (m *mail) BaseSend(mailTo []string, subject string, body string) error { + message := gomail.NewMessage() + message.SetHeader("From", message.FormatAddress(m.conf["user"], m.conf["from_mail_name"])) //这种方式可以添加别名,即“XX官方” + //说明:如果是用网易邮箱账号发送,以下方法别名可以是中文,如果是qq企业邮箱,以下方法用中文别名,会报错,需要用上面此方法转码 + //m.SetHeader("From", "FB Sample"+"<"+mailConn["user"]+">") //这种方式可以添加别名,即“FB Sample”, 也可以直接用m.SetHeader("From",mailConn["user"]) 读者可以自行实验下效果 + //m.SetHeader("From", mailConn["user"]) + message.SetHeader("To", mailTo...) //发送给多个用户 + message.SetHeader("Subject", subject) //设置邮件主题 + message.SetBody("text/html", body) //设置邮件正文 + + d := gomail.NewDialer(m.conf["host"], m.port, m.conf["user"], m.conf["pass"]) + + return d.DialAndSend(message) +} + +// TplSend 使用模版发送邮件 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 1:05 下午 2021/8/14 +func (m *mail) TplSend(mailTo []string, subject string, tpl string, bindData map[string]string) error { + var ( + tplContent []byte + err error + ) + if tplContent, err = util.ReadFileContent(tpl); nil != err { + return err + } + mailBody := string(tplContent) + for k, v := range bindData { + mailBody = strings.ReplaceAll(mailBody, "{"+k+"}", v) + } + return m.BaseSend(mailTo, subject, mailBody) +} diff --git a/request/curl.go b/request/curl.go index 0fb0fc2..650ab23 100644 --- a/request/curl.go +++ b/request/curl.go @@ -7,6 +7,131 @@ // Date : 2021-08-01 8:47 下午 package request -func Send(apiMethod *APIMethod) { +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "time" + "github.com/tidwall/gjson" + + "github.com/ddliu/go-httpclient" +) + +// Send 发送HTTP请求 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 9:38 下午 2021/8/11 +func Send(apiMethod *APIMethod) *APIResponse { + if apiMethod.Parameter == nil { + apiMethod.Parameter = make(map[string]interface{}) + } + if apiMethod.Header == nil { + apiMethod.Header = make(map[string]string) + } + return nil +} + +// request 发送请求 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2:27 下午 2021/8/12 +func request(apiMethod *APIMethod, responseConfig *ResponseConfig) (*APIResponse, bool) { + apiResponse := &APIResponse{ + Code: "", + Message: "", + Data: "", + Success: false, + TraceID: "", + Cost: 0, + RealCost: 0, + TotalCost: 0, + TotalRealCost: 0, + ErrorList: make([]error, 0), + StartRequestTime: time.Now().Unix(), + FinishRequestTime: 0, + } + defer func(apiResponse *APIResponse) { + apiResponse.FinishRequestTime = time.Now().Unix() + apiResponse.RealCost = apiResponse.FinishRequestTime - apiResponse.StartRequestTime + }(apiResponse) + var ( + client *httpclient.HttpClient + response *httpclient.Response + err error + responseData []byte + ) + client = httpclient.NewHttpClient() + inputByteData, _ := json.Marshal(apiMethod.Parameter) + requestData := bytes.NewReader(inputByteData) + if response, err = client.Do(apiMethod.Method, apiMethod.ServiceDomain+apiMethod.URI, apiMethod.Header, requestData); nil != err { + apiResponse.Success = false + apiResponse.ErrorList = append(apiResponse.ErrorList, err) + return apiResponse, false + } + if responseData, err = ioutil.ReadAll(response.Body); nil != err { + apiResponse.Success = false + apiResponse.ErrorList = append(apiResponse.ErrorList, err) + return apiResponse, false + } + orgData := string(responseData) + apiResponse.Data = gjson.Get(orgData, responseConfig.DataKey).String() + apiResponse.Code = fmt.Sprintf("%v", gjson.Get(orgData, responseConfig.CodeKey).Value()) + apiResponse.Message = gjson.Get(orgData, responseConfig.MessageKey).String() + apiResponse.Cost = gjson.Get(orgData, responseConfig.CostKey).Int() + apiResponse.Success = isSuccessCode(apiResponse.Code, responseConfig.SuccessCode) + + if apiResponse.Success { + return apiResponse, false + } + + // 非成功, 判断是否需要重试 + return apiResponse, isNeedRetry(apiMethod, ResponseErrorTypeBusinessCode) +} + +// isNeedRetry 是否需要重试 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 3:01 下午 2021/8/12 +func isNeedRetry(apiMethod *APIMethod, errType string) bool { + return false +} + +// isSuccessCode 是否为业务处理成功状态码 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 3:43 下午 2021/8/13 +func isSuccessCode(responseCode string, successResponseCodeList []string) bool { + isSuccess := false + for _, itemCode := range successResponseCodeList { + if responseCode == itemCode { + isSuccess = true + break + } + } + return isSuccess +} + +// buildResponseData 构建响应结果 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2:57 下午 2021/8/12 +func buildResponseData(response *httpclient.Response) *APIResponse { + return nil +} + +// buildRequestHeader 构建请求header +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 9:41 下午 2021/8/11 +func buildRequestHeader(apiMethod *APIMethod) map[string]string { + header := make(map[string]string) + return header } diff --git a/request/define.go b/request/define.go index 0493c8d..54dc013 100644 --- a/request/define.go +++ b/request/define.go @@ -13,12 +13,12 @@ package request // // Date : 8:29 下午 2021/8/1 type APIMethod struct { - ServiceDomain string `json:"service_domain"` // 调用服务的域名 - URI string `json:"uri"` // 调用的URI - Method string `json:"method"` // 请求方法 - ISRestfulURI bool `json:"is_restful_uri"` // 是否为restful uri - Header map[string]string `json:"header"` // 请求header - + ServiceDomain string `json:"service_domain"` // 调用服务的域名 + URI string `json:"uri"` // 调用的URI + Method string `json:"method"` // 请求方法 + ISRestfulURI bool `json:"is_restful_uri"` // 是否为restful uri + Header map[string]string `json:"header"` // 请求header + Parameter map[string]interface{} `json:"parameter"` // 请求参数 } // ResponseConfig ... @@ -27,12 +27,18 @@ type APIMethod struct { // // Date : 8:32 下午 2021/8/1 type ResponseConfig struct { - ISJson bool `json:"is_json"` // 响应数据是否为json - CodeKey string `json:"code_key"` // 代表业务Code的Key - MessageKey string `json:"message_key"` // 描述业务code的key - DataKey string `json:"data_key"` // 返回数据的key - SuccessRule string `json:"success_rule"` // 请求成功的规则, http_code / business_code http状态码或者业务状态码, 如果是通过http code 判断是否为请求成功 code key / message key 配置无效 - SuccessCode []string `json:"success_code"` // 哪些状态码被认为是请求成功 + RetryRule int `json:"retry_rule"` // 重试规则 0 - http error 1 - business error 2 - both 0 && 1 + RetryCnt int `json:"retry_cnt"` // 重试次数(重试次数 1, 最多请求两次) + RetryInterval int `json:"retry_interval"` // 重试的时间间隔, 单位ms, 为 0 立即重试, > 0 休眠指定ms后重试 + ISJson bool `json:"is_json"` // 响应数据是否为json + CodeKey string `json:"code_key"` // 代表业务Code的Key + MessageKey string `json:"message_key"` // 描述业务code的key + TraceKey string `json:"trace_key"` // 接口返回的trace追踪字段key + DataKey string `json:"data_key"` // 返回数据的key + CostKey string `json:"cost_key"` // 代表接口耗时的key + SuccessRule string `json:"success_rule"` // 请求成功的规则, http_code / business_code http状态码或者业务状态码, 如果是通过http code 判断是否为请求成功 code key / message key 配置无效 + SuccessCode []string `json:"success_code"` // 哪些状态码被认为是请求成功 + SuccessHttpCode []string `json:"success_http_code"` // 被认为是成功的http code } // APIResponse API响应数据 @@ -41,8 +47,34 @@ type ResponseConfig struct { // // Date : 8:54 下午 2021/8/1 type APIResponse struct { - Data string `json:"data"` // 响应数据 - Cost int64 `json:"cost"` // 接口耗时 - StartRequestTime int64 `json:"start_request_time"` // 开始请求时间 - FinishRequestTime int64 `json:"finish_request_time"` // 完成请求时间 + Code string `json:"code"` // 响应状态码 + Message string `json:"message"` // 响应状态码的描述 + Data string `json:"data"` // 响应数据 + Success bool `json:"success"` // 请求是否成功 + TraceID string `json:"trace_id"` // 日志追踪traceID + Cost int64 `json:"cost"` // 接口耗时(第三方接口返回) + RealCost int64 `json:"real_cost"` // 内部从发起请求,到拿到结果的耗时 + TotalCost int64 `json:"total_cost"` // 累计请求加一起耗时 + TotalRealCost int64 `json:"total_real_cost"` // 累计真实耗时 + ErrorList []error `json:"error_list"` // 重试过程中累计出现过的异常 + StartRequestTime int64 `json:"start_request_time"` // 开始请求时间 + FinishRequestTime int64 `json:"finish_request_time"` // 完成请求时间 } + +const ( + // ResponseErrorTypeInternal 内部异常,请求没发出去 + ResponseErrorTypeInternal = "internal" + // ResponseErrorTypeHttpStatus http 状态码异常 + ResponseErrorTypeHttpStatus = "http" + // ResponseErrorTypeBusinessCode 业务状态码异常 + ResponseErrorTypeBusinessCode = "business" +) + +const ( + // RetryRuleOnlyHttpError 仅 http 异常重试 + RetryRuleOnlyHttpError = 0 + // RetryRuleOnlyBusinessError 仅业务状态码异常重试 + RetryRuleOnlyBusinessError = 1 + // RetryRuleMixed 任意一种异常出现,均重试 + RetryRuleMixed = 2 +)