From 4f1a2bc950fc79853f442fe5efdb4eb7fea0a8ab Mon Sep 17 00:00:00 2001 From: gazebo Date: Thu, 14 Jun 2018 14:45:19 +0800 Subject: [PATCH] - basic mtps api added. --- platform/common/common.go | 83 ++++++++++++++++++ platform/elmapi/elmapi.go | 99 +++++++-------------- platform/elmapi/elmapi_test.go | 6 +- platform/elmapi/order.go | 6 +- platform/jdapi/jdapi.go | 116 ++++++++++++------------- platform/mtpsapi/mtpsapi.go | 142 +++++++++++++++++++++++++++++++ platform/mtpsapi/mtpsapi_test.go | 40 +++++++++ 7 files changed, 355 insertions(+), 137 deletions(-) create mode 100644 platform/common/common.go create mode 100644 platform/mtpsapi/mtpsapi.go create mode 100644 platform/mtpsapi/mtpsapi_test.go diff --git a/platform/common/common.go b/platform/common/common.go new file mode 100644 index 00000000..7d9126cc --- /dev/null +++ b/platform/common/common.go @@ -0,0 +1,83 @@ +package common + +import ( + "errors" + "net" + "net/http" + "time" + + "go.uber.org/zap" +) + +type AccessPlatformAPIWithRetryParams struct { + MaxExceedLimitRetryCount int + MaxRecoverableRetryCount int + SleepSecondWhenExceedLimit time.Duration + Client *http.Client + Request *http.Request + SugarLogger *zap.SugaredLogger +} + +const ( + PAErrorLevelSuccess = 0 + PAErrorLevelExceedLimit = 1 + PAErrorLevelRecoverable = 2 + PAErrorLevelFailed = 3 +) + +var ( + ErrRecoverableErrMaxRetry = errors.New("recoverable error reach max retry count!") + ErrLimitReachMaxRetry = errors.New("Reach max retry count!") + ErrHttpCode = errors.New("HTTP Code is not 200") + ErrBusinessCode = errors.New("Business code is not ok") +) + +func AccessPlatformAPIWithRetry(params *AccessPlatformAPIWithRetryParams, handleResponse func(response *http.Response) (int, error)) error { + exceedLimitRetryCount := 0 + recoverableErrorRetryCount := 0 + for { + response, err := params.Client.Do(params.Request) + if err != nil { + params.SugarLogger.Debugf("client.Get return err:%v", err) + err, ok := err.(net.Error) + recoverableErrorRetryCount++ + if ok && err.Timeout() && recoverableErrorRetryCount <= params.MaxRecoverableRetryCount { + continue + } else { + return err + } + } + defer response.Body.Close() + if response.StatusCode != 200 { + params.SugarLogger.Debugf("http code is:%d", response.StatusCode) + recoverableErrorRetryCount++ + if recoverableErrorRetryCount <= params.MaxRecoverableRetryCount { + continue + } + + return ErrHttpCode + } + + errLevel, err := handleResponse(response) + if err != nil { + return err + } + if errLevel == PAErrorLevelSuccess { + return nil + } else if errLevel == PAErrorLevelExceedLimit { + exceedLimitRetryCount++ + if exceedLimitRetryCount <= params.MaxExceedLimitRetryCount { + time.Sleep(params.SleepSecondWhenExceedLimit) + } else { + return ErrLimitReachMaxRetry + } + } else if errLevel == PAErrorLevelRecoverable { + if recoverableErrorRetryCount <= params.MaxRecoverableRetryCount { + continue + } + return ErrRecoverableErrMaxRetry + } else { + return ErrBusinessCode + } + } +} diff --git a/platform/elmapi/elmapi.go b/platform/elmapi/elmapi.go index 4bb630e2..47059502 100644 --- a/platform/elmapi/elmapi.go +++ b/platform/elmapi/elmapi.go @@ -2,36 +2,29 @@ package elmapi import ( "crypto/md5" - "errors" "fmt" "io/ioutil" - "net" "net/http" "net/url" "sort" "strings" "time" + "git.rosy.net.cn/baseapi/platform/common" "git.rosy.net.cn/baseapi/utils" "go.uber.org/zap" ) const ( - ELM_API_URL_SANDBOX = "https://open-api-sandbox.shop.ele.me/api/v1/" - ELM_API_URL_PROD = "https://open-api.shop.ele.me/api/v1/" -) - -const ( + clientTimeout = time.Second * 10 sleepSecondWhenLimited = 6 * time.Second maxRetryCountWhenNetworkException = 3 maxRetryCountWhenReachLimited = 10 ) -var ( - ErrSystemErrMaxRetry = errors.New("ELM System error reach max retry count!") - ErrLimitReachMaxRetry = errors.New("ELM Reach max retry count!") - ErrHttpCode = errors.New("ELM HTTP Code is not 200") - ErrELMCode = errors.New("ELM code is not 0") +const ( + ELM_API_URL_SANDBOX = "https://open-api-sandbox.shop.ele.me/api/v1/" + ELM_API_URL_PROD = "https://open-api.shop.ele.me/api/v1/" ) type ELMResult struct { @@ -46,7 +39,7 @@ type ELMAPI struct { secret string sugarLogger *zap.SugaredLogger url *url.URL - client http.Client + client *http.Client } type ELMPayload struct { @@ -65,7 +58,7 @@ func NewELMAPI(token, appKey, secret string, sugarLogger *zap.SugaredLogger, isP appKey: appKey, secret: secret, sugarLogger: sugarLogger, - client: http.Client{Timeout: time.Second * 10}, + client: &http.Client{Timeout: clientTimeout}, } if isProd { @@ -95,7 +88,7 @@ func (e *ELMAPI) signParams(action string, payload *ELMPayload) string { return fmt.Sprintf("%X", md5.Sum([]byte(finalStr))) } -func (e *ELMAPI) AccessELM(action string, params map[string]interface{}) (*ELMResult, error) { +func (e *ELMAPI) AccessELM(action string, params map[string]interface{}) (retVal *ELMResult, err error) { if params == nil { params = make(map[string]interface{}, 0) } @@ -114,14 +107,13 @@ func (e *ELMAPI) AccessELM(action string, params map[string]interface{}) (*ELMRe } payload.Signature = e.signParams(action, payload) - dataBytes := utils.MustMarshal(payload) - dataStr := string(dataBytes) - exceedLimitRetryCount := 0 - systemErrorRetryCount := 0 - - for { - request := &http.Request{ + apiAccess := &common.AccessPlatformAPIWithRetryParams{ + MaxExceedLimitRetryCount: maxRetryCountWhenReachLimited, + MaxRecoverableRetryCount: maxRetryCountWhenNetworkException, + SleepSecondWhenExceedLimit: sleepSecondWhenLimited, + Client: e.client, + Request: &http.Request{ Method: "POST", URL: e.url, Header: http.Header{ @@ -131,64 +123,37 @@ func (e *ELMAPI) AccessELM(action string, params map[string]interface{}) (*ELMRe // "x-eleme-requestid": []string{payload.Id}, }, - Body: ioutil.NopCloser(strings.NewReader(dataStr)), - } - response, err := e.client.Do(request) - if err != nil { - e.sugarLogger.Debugf("client.Get return err:%v", err) - err, ok := err.(net.Error) - systemErrorRetryCount++ - if ok && err.Timeout() && systemErrorRetryCount <= maxRetryCountWhenNetworkException { - continue - } else { - return nil, err - } - } - defer response.Body.Close() - - if response.StatusCode != 200 { - e.sugarLogger.Debugf("http code is:%d", response.StatusCode) - systemErrorRetryCount++ - if systemErrorRetryCount <= maxRetryCountWhenNetworkException { - continue - } - - return nil, ErrHttpCode - } + Body: ioutil.NopCloser(strings.NewReader(string(utils.MustMarshal(payload)))), + }, + SugarLogger: e.sugarLogger, + } + err = common.AccessPlatformAPIWithRetry(apiAccess, func(response *http.Response) (result int, err error) { jsonResult1, err := utils.HttpResponse2Json(response) + if err != nil { + e.sugarLogger.Warnf("HttpResponse2Json return:%v", err) + return 0, err + } resultError, _ := jsonResult1["error"].(map[string]interface{}) - jsonResult := &ELMResult{ + retVal = &ELMResult{ Id: jsonResult1["id"].(string), Error: resultError, Result: jsonResult1["result"], } - if err != nil { - e.sugarLogger.Warnf("HttpResponse2Json return:%v", err) - return nil, err - } - - errinfoMap := jsonResult.Error + errinfoMap := retVal.Error if errinfoMap == nil { - return jsonResult, nil + return common.PAErrorLevelSuccess, nil } errCode := errinfoMap["code"].(string) if errCode == "EXCEED_LIMIT" { - exceedLimitRetryCount++ - if exceedLimitRetryCount <= maxRetryCountWhenReachLimited { - time.Sleep(sleepSecondWhenLimited) - } else { - return jsonResult, ErrLimitReachMaxRetry - } + return common.PAErrorLevelExceedLimit, nil } else if errCode == "SERVER_ERROR" || errCode == "BIZ_SYSTEM_ERROR" || errCode == "BIZ_1006" || errCode == "BUSINESS_ERROR" { - if systemErrorRetryCount <= maxRetryCountWhenNetworkException { - continue - } - return jsonResult, ErrSystemErrMaxRetry + return common.PAErrorLevelRecoverable, nil } else { - e.sugarLogger.Debug(jsonResult) - return jsonResult, ErrELMCode + return common.PAErrorLevelFailed, nil } - } + }) + + return retVal, err } diff --git a/platform/elmapi/elmapi_test.go b/platform/elmapi/elmapi_test.go index 814410c2..1c722199 100644 --- a/platform/elmapi/elmapi_test.go +++ b/platform/elmapi/elmapi_test.go @@ -1,7 +1,6 @@ package elmapi import ( - "encoding/json" "testing" "git.rosy.net.cn/baseapi/utils" @@ -29,10 +28,9 @@ func TestAccessELM(t *testing.T) { t.Fatalf("Error when accessing AccessJDQuery result:%v, error:%v", result, err) } else { mapResult := result.Result.(map[string]interface{}) - userIdNumber := mapResult["userId"].(json.Number) - userId, err := userIdNumber.Int64() + userId := utils.MustInterface2Int64(mapResult["userId"]) if userId != 336072266326420104 || err != nil { - t.Fatalf("userId is not correct:%v", userIdNumber) + t.Fatalf("userId is not correct:%v", mapResult["userId"]) } } } diff --git a/platform/elmapi/order.go b/platform/elmapi/order.go index cf8921fa..021c8659 100644 --- a/platform/elmapi/order.go +++ b/platform/elmapi/order.go @@ -1,14 +1,12 @@ package elmapi -type ELMOrderInfo map[string]interface{} - -func (e *ELMAPI) GetOrder(orderId string) (ELMOrderInfo, error) { +func (e *ELMAPI) GetOrder(orderId string) (map[string]interface{}, error) { result, err := e.AccessELM("eleme.order.getOrder", map[string]interface{}{ "orderId": orderId, }) if err == nil { - innerResult := ELMOrderInfo(result.Result.(map[string]interface{})) + innerResult := map[string]interface{}(result.Result.(map[string]interface{})) return innerResult, nil } return nil, err diff --git a/platform/jdapi/jdapi.go b/platform/jdapi/jdapi.go index 0687d9e3..f9420097 100644 --- a/platform/jdapi/jdapi.go +++ b/platform/jdapi/jdapi.go @@ -5,17 +5,25 @@ import ( "encoding/json" "errors" "fmt" - "net" "net/http" "net/url" "sort" "strconv" "time" + "git.rosy.net.cn/baseapi/platform/common" + "git.rosy.net.cn/baseapi/utils" "go.uber.org/zap" ) +const ( + clientTimeout = time.Second * 10 + sleepSecondWhenLimited = 6 * time.Second + maxRetryCountWhenNetworkException = 3 + maxRetryCountWhenReachLimited = 10 +) + const ( // JDErrorCodeSuccess 操作成功 JDErrorCodeSuccess = "0" @@ -37,30 +45,23 @@ const ( JDErrorCodeLoadUnexpected = "100024" ) +const ( + jdAPIURL = "https://openo2o.jd.com/djapi/%s" + AllPage = 0 + DefaultPageSize = 50 +) + type JDAPI struct { token string appKey string appSecret string logger *zap.Logger sugarLogger *zap.SugaredLogger - client http.Client + client *http.Client } -const ( - sleepSecondWhenLimited = 6 * time.Second - maxRetryCountWhenNetworkException = 3 - maxRetryCountWhenReachLimited = 10 - jdAPIURL = "https://openo2o.jd.com/djapi/%s" - AllPage = 0 - DefaultPageSize = 50 -) - var ( - ErrSystemErrMaxRetry = errors.New("JD System error reach max retry count!") - ErrLimitReachMaxRetry = errors.New("JD Reach max retry count!") - ErrHttpCode = errors.New("JD HTTP Code is not 200") - ErrJDCode = errors.New("JD code is not 0") - ErrInnerCodeIsNotOk = errors.New("JD result inner code is not ok") + ErrInnerCodeIsNotOk = errors.New("JD result inner code is not ok") exceedLimitCodes = map[string]int{ JDErrorCodeExceedLimit: 1, @@ -82,6 +83,7 @@ var ( "None": 1, "0": 1, "1": 1, + "200": 1, "190005": 1, // 部分失败 } @@ -128,10 +130,10 @@ func genGetURL(baseURL, apiStr string, params map[string]string) string { } func NewJDAPI(token, appKey, appSecret string, logger *zap.Logger) *JDAPI { - return &JDAPI{token, appKey, appSecret, logger, logger.Sugar(), http.Client{Timeout: time.Second * 10}} + return &JDAPI{token, appKey, appSecret, logger, logger.Sugar(), &http.Client{Timeout: clientTimeout}} } -func (j *JDAPI) AccessJDQuery(apiStr string, jdParams map[string]string) (map[string]interface{}, error) { +func (j *JDAPI) AccessJDQuery(apiStr string, jdParams map[string]string) (retVal map[string]interface{}, err error) { params := make(map[string]string) params["v"] = "1.0" params["format"] = "json" @@ -152,62 +154,45 @@ func (j *JDAPI) AccessJDQuery(apiStr string, jdParams map[string]string) (map[st sign := signParams(params) params["sign"] = sign - exceedLimitRetryCount := 0 - systemErrorRetryCount := 0 - for { - fullURL := genGetURL(jdAPIURL, apiStr, params) - j.sugarLogger.Debugf("fullURL:%v", fullURL) - response, err := j.client.Get(fullURL) - if err != nil { - j.sugarLogger.Debugf("client.Get return err:%v", err) - err, ok := err.(net.Error) - systemErrorRetryCount++ - if ok && err.Timeout() && systemErrorRetryCount <= maxRetryCountWhenNetworkException { - continue - } else { - return nil, err - } - } - defer response.Body.Close() + url, _ := url.Parse(genGetURL(jdAPIURL, apiStr, params)) - if response.StatusCode != 200 { - j.sugarLogger.Debugf("http code is:%d", response.StatusCode) - systemErrorRetryCount++ - if systemErrorRetryCount <= maxRetryCountWhenNetworkException { - continue - } + apiAccess := &common.AccessPlatformAPIWithRetryParams{ + MaxExceedLimitRetryCount: maxRetryCountWhenReachLimited, + MaxRecoverableRetryCount: maxRetryCountWhenNetworkException, + SleepSecondWhenExceedLimit: sleepSecondWhenLimited, + Client: j.client, + Request: &http.Request{ + Method: "GET", + URL: url, + }, + SugarLogger: j.sugarLogger, + } - return nil, ErrHttpCode - } - - jsonResult, err := utils.HttpResponse2Json(response) + err = common.AccessPlatformAPIWithRetry(apiAccess, func(response *http.Response) (errLevel int, err error) { + jsonResult1, err := utils.HttpResponse2Json(response) if err != nil { j.sugarLogger.Warnf("HttpResponse2Json return:%v", err) - return nil, err + return 0, err } - code := jsonResult["code"].(string) + code := jsonResult1["code"].(string) if code == "0" { - return jsonResult, nil + retVal = jsonResult1 + return common.PAErrorLevelSuccess, nil } j.sugarLogger.Debugf("jd code is:%s", code) if _, ok := exceedLimitCodes[code]; ok { - exceedLimitRetryCount++ - if exceedLimitRetryCount <= maxRetryCountWhenReachLimited { - time.Sleep(sleepSecondWhenLimited) - } else { - return jsonResult, ErrLimitReachMaxRetry - } + return common.PAErrorLevelExceedLimit, nil } else if _, ok := canRetryCodes[code]; ok { - systemErrorRetryCount++ - if systemErrorRetryCount > maxRetryCountWhenNetworkException { - return jsonResult, ErrSystemErrMaxRetry - } + return common.PAErrorLevelRecoverable, nil } else { - return jsonResult, ErrJDCode + return common.PAErrorLevelFailed, nil } - } + + }) + + return retVal, err } func (j *JDAPI) AccessJDQueryNoPage(apiStr string, jdParams map[string]string, keyToRemove, keyToKeep []string) (interface{}, error) { @@ -226,7 +211,7 @@ func (j *JDAPI) AccessJDQueryNoPage(apiStr string, jdParams map[string]string, k innerCode := "" for _, innerCodeKey := range jdResultInnerCodeKeys { if innerCode2, ok := data[innerCodeKey]; ok { - innerCode = innerCode2.(string) + innerCode = forceInnerCode2Str(innerCode2) break } } @@ -333,7 +318,7 @@ func (j *JDAPI) AccessJDQueryHavePage(apiStr string, jdParams map[string]string, return nil, err } - innerCode := data["code"].(string) + innerCode := forceInnerCode2Str(data["code"]) if innerCode != "0" && innerCode != "200" { return nil, ErrInnerCodeIsNotOk } @@ -352,3 +337,10 @@ func (j *JDAPI) AccessJDQueryHavePage(apiStr string, jdParams map[string]string, return retVal, nil } + +func forceInnerCode2Str(innerCode interface{}) string { + if innerCodeStr, ok := innerCode.(string); ok { + return innerCodeStr + } + return string(utils.MustInterface2Int64(innerCode)) +} diff --git a/platform/mtpsapi/mtpsapi.go b/platform/mtpsapi/mtpsapi.go new file mode 100644 index 00000000..b8597ed1 --- /dev/null +++ b/platform/mtpsapi/mtpsapi.go @@ -0,0 +1,142 @@ +package mtpsapi + +import ( + "crypto/sha1" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "git.rosy.net.cn/baseapi/platform/common" + "git.rosy.net.cn/baseapi/utils" + "go.uber.org/zap" +) + +const ( + clientTimeout = time.Second * 10 + sleepSecondWhenLimited = 6 * time.Second + maxRetryCountWhenNetworkException = 3 + maxRetryCountWhenReachLimited = 10 +) + +const ( + mtpsAPIURL = "https://peisongopen.meituan.com/api" +) + +const ( + mtpsStatusSuccess = 0 + mtpsStatusSystemError = 1 + mtpsStatusMissingSystemParams = 2 + mtpsStatusMissingBusinessParams = 3 +) + +type MTPSResult struct { + Code int `json:"code"` + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +type MTPSAPI struct { + appKey string + secret string + sugarLogger *zap.SugaredLogger + client *http.Client +} + +// type MTPSOderStatus struct { +// DeliveryId int64 `json:"delivery_id"` +// MtPeiSongId string `json:"mt_peisong_id"` +// OrderId string `json:"order_id"` +// Status int `json:"status"` +// OperateTime int `json:"operate_time"` +// CourierName string `json:"courier_name"` +// CourierPhone string `json:"courier_phone"` +// CancelReasonId int `json:"cancel_reason_id"` +// CancelReason string `json:"cancel_reason"` +// } + +func NewMTPSAPI(appKey, secret string, sugarLogger *zap.SugaredLogger) *MTPSAPI { + api := &MTPSAPI{ + appKey: appKey, + secret: secret, + sugarLogger: sugarLogger, + client: &http.Client{Timeout: clientTimeout}, + } + + return api +} + +func (m *MTPSAPI) signParams(params url.Values) string { + keyValues := make([]string, 0) + for k, v := range params { + valStr := strings.Join(v, "") + if valStr != "" { + keyValues = append(keyValues, k+valStr) + } + } + + sort.Strings(keyValues) + finalStr := m.secret + strings.Join(keyValues, "") + // e.sugarLogger.Debugf("sign str:%v", finalStr) + return fmt.Sprintf("%x", sha1.Sum([]byte(finalStr))) +} + +func (m *MTPSAPI) AccessMTPS(action string, params map[string]interface{}) (retVal *MTPSResult, err error) { + if params == nil { + panic("params is nil!") + } + params["appkey"] = m.appKey + params["timestamp"] = utils.GetCurTimestamp() + params["version"] = "1.0" + + params2 := make(url.Values) + for k, v := range params { + params2[k] = []string{fmt.Sprint(v)} + } + params2["sign"] = []string{m.signParams(params2)} + + request, _ := http.NewRequest("POST", mtpsAPIURL+"/"+action, strings.NewReader(string(params2.Encode()))) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + apiAccess := &common.AccessPlatformAPIWithRetryParams{ + MaxExceedLimitRetryCount: maxRetryCountWhenReachLimited, + MaxRecoverableRetryCount: maxRetryCountWhenNetworkException, + SleepSecondWhenExceedLimit: sleepSecondWhenLimited, + Client: m.client, + Request: request, + SugarLogger: m.sugarLogger, + } + + err = common.AccessPlatformAPIWithRetry(apiAccess, func(response *http.Response) (result int, err error) { + jsonResult1, err := utils.HttpResponse2Json(response) + if err != nil { + m.sugarLogger.Warnf("HttpResponse2Json return:%v", err) + return 0, err + } + code := int(utils.MustInterface2Int64(jsonResult1["code"])) + retVal = &MTPSResult{ + Code: code, + } + if code == mtpsStatusSuccess { + retVal.Data = jsonResult1["data"].(map[string]interface{}) + return common.PAErrorLevelSuccess, nil + } + retVal.Message = jsonResult1["message"].(string) + return common.PAErrorLevelFailed, nil + }) + + return retVal, err +} + +func (m *MTPSAPI) QueryOrderStatus(deliveryId int64, MtPeiSongId string) (retVal map[string]interface{}, err error) { + params := map[string]interface{}{ + "delivery_id": deliveryId, + "mt_peisong_id": MtPeiSongId, + } + if result, err := m.AccessMTPS("order/status/query", params); err != nil { + return nil, err + } else { + return result.Data, nil + } +} diff --git a/platform/mtpsapi/mtpsapi_test.go b/platform/mtpsapi/mtpsapi_test.go new file mode 100644 index 00000000..c0e5698c --- /dev/null +++ b/platform/mtpsapi/mtpsapi_test.go @@ -0,0 +1,40 @@ +package mtpsapi + +import ( + "testing" + + "git.rosy.net.cn/baseapi/utils" + "go.uber.org/zap" +) + +var ( + mtpsapi *MTPSAPI + sugarLogger *zap.SugaredLogger +) + +func init() { + logger, _ := zap.NewDevelopment() + sugarLogger = logger.Sugar() + mtpsapi = NewMTPSAPI("3c0a05d464c247c19d7ec13accc78605", "b1M}9?:sTbsB[OF2gNORnN(|(iy9rB8(`7]|[wGLnbmt`evfM>E:A90DjHAW:UPE", sugarLogger) +} + +func TestTest(t *testing.T) { + sugarLogger.Debug(utils.GetCurTimeStr()) +} + +func TestAccessMTPS(t *testing.T) { + mtPeiSongId := "1528955769086020" + params := map[string]interface{}{ + "delivery_id": 378194, + "mt_peisong_id": mtPeiSongId, + } + result, err := mtpsapi.AccessMTPS("order/status/query", params) + if err != nil { + t.Fatalf("Error when accessing AccessMTPS result:%v, error:%v", result, err) + } else { + getMtPsId := result.Data["mt_peisong_id"].(string) + if getMtPsId != mtPeiSongId { + t.Fatalf("mt_peisong_id is not same, %v vs %v", mtPeiSongId, getMtPsId) + } + } +}