commit ad649555c4937ed5cacb401c3c4ef3ad614fab34 Author: gazebo Date: Tue Jun 5 16:57:27 2018 +0800 - first edition. diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d3bffdfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +.DS_Store +*.test diff --git a/platform/jdapi/jdapi.go b/platform/jdapi/jdapi.go new file mode 100644 index 00000000..0d3f1ffb --- /dev/null +++ b/platform/jdapi/jdapi.go @@ -0,0 +1,346 @@ +package jdapi + +import ( + "crypto/md5" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "git.rosy.net.cn/baseapi/utils" + "github.com/satori/go.uuid" + "go.uber.org/zap" +) + +type JDAPI struct { + token string + appKey string + appSecret string + logger *zap.Logger + sugarLogger *zap.SugaredLogger + client http.Client +} + +const ( + sleepSecondWhenLimited = 6 * time.Second + maxRetryCountWhenNetworkException = 3 + maxRetryCountWhenReachLimited = 10 + jdAPIURL = "https://openo2o.jd.com/djapi/%s" + AllPage = 0 + DefaultPageSize = 50 +) + +var ( + ErrJdParam = errors.New("can not marshal jd param") + 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") + ErrFormatWrong = errors.New("JD Result format is strange!") + ErrJDCode = errors.New("JD code is not 0") + ErrInnerCodeIsNotOk = errors.New("JD result inner code is not ok") + + exceedLimitCodes = map[string]int{ + "10032": 1, + } + canRetryCodes = map[string]int{ + "-10000": 1, + "-1": 1, + "10000": 1, + "100022": 1, + "100023": 1, + "100024": 1, + } + + jdResultInnerCodeKeys = []string{"code", "retCode", "errorCode"} + + jdResultInnerCodeOK = map[string]int{ + "None": 1, + "0": 1, + "1": 1, + "190005": 1, // 部分失败 + } + + jdResultNoPageInnerDataKeys = []string{"result", "data"} + jdResultPageInner2DataKeys = []string{"result", "resultList"} + jdResultPageTotalCountKeys = []string{"totalCount", "count"} +) + +type PageResultParser func(map[string]interface{}, int) ([]interface{}, int) + +func getUUID() string { + return strings.ToUpper(strings.Replace(uuid.Must(uuid.NewV1()).String(), "-", "", -1)) +} + +func getJDOperator() string { + return time.Now().Format("2006-01-02_15:04:05") +} + +func getCurTimeStr() string { + // return "2018-06-04 22:35:25" + return time.Now().Format("2006-01-02 15:04:05") +} + +func SignParams(jdParams map[string]string) string { + var keys []string + for k := range jdParams { + if k != "app_secret" { + keys = append(keys, k) + } + } + + sort.Strings(keys) + allStr := "" + for _, k := range keys { + allStr += k + jdParams[k] + } + allStr = jdParams["app_secret"] + allStr + jdParams["app_secret"] + + return fmt.Sprintf("%X", md5.Sum([]byte(allStr))) +} + +func genGetURL(baseURL, apiStr string, params map[string]string) string { + fullURL := "" + + if params != nil { + for k, v := range params { + if fullURL == "" { + fullURL = "?" + } else { + fullURL += "&" + } + fullURL += k + "=" + url.QueryEscape(v) + } + } + + return fmt.Sprintf(baseURL, apiStr) + fullURL +} + +func NewJDAPI(token, appKey, appSecret string, logger *zap.Logger) *JDAPI { + return &JDAPI{token, appKey, appSecret, logger, logger.Sugar(), http.Client{Timeout: time.Second * 10}} +} + +func (j JDAPI) accessJDQuery(apiStr string, jdParams map[string]string) (result map[string]interface{}, err error) { + params := make(map[string]string) + params["v"] = "1.0" + params["format"] = "json" + params["app_key"] = j.appKey + params["app_secret"] = j.appSecret + params["token"] = j.token + + if jdParams == nil { + jdParams = make(map[string]string, 0) + } + jdParamStr, err := json.Marshal(jdParams) + if err == nil { + params["jd_param_json"] = string(jdParamStr) + } else { + return nil, ErrJdParam + } + + params["timestamp"] = getCurTimeStr() + 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() + + if response.StatusCode != 200 { + j.sugarLogger.Debugf("http code is:%d", response.StatusCode) + systemErrorRetryCount++ + if systemErrorRetryCount <= maxRetryCountWhenNetworkException { + continue + } + + return nil, ErrHttpCode + } + + var jsonResult map[string]interface{} + bodyData, err := ioutil.ReadAll(response.Body) + if err != nil { + j.sugarLogger.Debugf("ioutil.ReadAll return:%v", err) + return nil, err + } + + err = utils.UnmarshalUseNumber(bodyData, &jsonResult) + if err != nil { + j.sugarLogger.Debugf("json.Unmarshal return:%v", err) + return nil, err + } + + code := jsonResult["code"].(string) + if code == "0" { + return jsonResult, 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 + } + } else if _, ok := canRetryCodes[code]; ok { + systemErrorRetryCount++ + if systemErrorRetryCount > maxRetryCountWhenNetworkException { + return jsonResult, ErrSystemErrMaxRetry + } + } else { + return jsonResult, ErrJDCode + } + } +} + +func (j JDAPI) accessJDQueryNoPage(apiStr string, jdParams map[string]string, keyToRemove, keyToKeep []string) (interface{}, error) { + jsonResult, err := j.accessJDQuery(apiStr, jdParams) + if err != nil { + return jsonResult, err + } + + var data map[string]interface{} + err = utils.UnmarshalUseNumber([]byte(jsonResult["data"].(string)), &data) + + if err != nil { + return jsonResult, err + } + + innerCode := "" + for _, innerCodeKey := range jdResultInnerCodeKeys { + if innerCode2, ok := data[innerCodeKey]; ok { + innerCode = innerCode2.(string) + break + } + } + + if _, ok := jdResultInnerCodeOK[innerCode]; ok { + for _, innerDataKey := range jdResultNoPageInnerDataKeys { + if innerData, ok := data[innerDataKey]; ok { + return innerData, nil + } + } + panic("Can not find inner data") + } else { + return utils.DictKeysMan(jsonResult, keyToRemove, keyToKeep), ErrInnerCodeIsNotOk + } +} + +func (j JDAPI) accessJDQueryHavePage(apiStr string, jdParams map[string]string, keyToRemove, keyToKeep []string, pageNo, pageSize int, pageResultParser PageResultParser) (interface{}, error) { + normalJDQueryHavePageResultParser := func(data map[string]interface{}, totalCount int) ([]interface{}, int) { + var result map[string]interface{} + var retVal []interface{} + + tempResult := data["result"] + if resultStr, ok := tempResult.(string); ok { + if err := utils.UnmarshalUseNumber([]byte(resultStr), &tempResult); err != nil { + panic("Wrong format") + } + } + + result = tempResult.(map[string]interface{}) + + if totalCount == 0 { + for _, totalCountKey := range jdResultPageTotalCountKeys { + if totalCount2, ok := result[totalCountKey]; ok { + totalCountInt64, _ := totalCount2.(json.Number).Int64() + totalCount = int(totalCountInt64) + if totalCount == 0 { + return make([]interface{}, 0), 0 + } + break + } + } + if totalCount == 0 { + panic("can not find totalCount key") + } + } + + for _, inner2ResultKey := range jdResultPageInner2DataKeys { + if inner2Result, ok := result[inner2ResultKey]; ok { + + if inner2ResultStr, ok := inner2Result.(string); ok { + err := utils.UnmarshalUseNumber([]byte(inner2ResultStr), &retVal) + if err != nil { + panic("can not unmarshal inner2result") + } + } else { + retVal = inner2Result.([]interface{}) + } + + return retVal, totalCount + } + } + + panic("wrong format") + } + + if pageResultParser == nil { + pageResultParser = normalJDQueryHavePageResultParser + } + + totalCount := 0 + curPage := pageNo + if curPage == AllPage { + curPage = 1 + } + + retVal := make([]interface{}, 0) + localJdParams := make(map[string]string) + if jdParams != nil && len(jdParams) > 0 { + for k, v := range jdParams { + localJdParams[k] = v + } + } + for { + localJdParams["pageNo"] = strconv.FormatInt(int64(curPage), 10) + localJdParams["pageSize"] = strconv.FormatInt(int64(pageSize), 10) + jsonResult, err := j.accessJDQuery(apiStr, localJdParams) + if err != nil { + return nil, err + } + var data map[string]interface{} + err = utils.UnmarshalUseNumber([]byte(jsonResult["data"].(string)), &data) + + if err != nil { + return nil, err + } + + innerCode := data["code"].(string) + if innerCode != "0" && innerCode != "200" { + return nil, ErrInnerCodeIsNotOk + } + + inResult, totalCount2 := pageResultParser(data, totalCount) + totalCount = totalCount2 + + inResult = utils.DictKeysMan(inResult, keyToRemove, keyToKeep).([]interface{}) + retVal = append(retVal, inResult...) + + if curPage == pageNo || curPage*pageSize >= totalCount { + break + } + curPage++ + } + + return retVal, nil +} diff --git a/platform/jdapi/jdapi_test.go b/platform/jdapi/jdapi_test.go new file mode 100644 index 00000000..ac2c0fad --- /dev/null +++ b/platform/jdapi/jdapi_test.go @@ -0,0 +1,79 @@ +package jdapi + +import ( + "net/http" + "testing" + + "go.uber.org/zap" +) + +var ( + jdapi *JDAPI + sugarLogger *zap.SugaredLogger +) + +func init() { + logger, _ := zap.NewDevelopment() + sugarLogger = logger.Sugar() + jdapi = NewJDAPI("91633f2a-c5f5-4982-a925-a220d19095c3", "1dba76d40cac446ca500c0391a0b6c9d", "a88d031a1e7b462cb1579f12e97fe7f4", logger) +} + +func TestTest(t *testing.T) { + sugarLogger.Debug(getCurTimeStr()) +} + +func TestAccessJDQuery(t *testing.T) { + result, err := jdapi.accessJDQuery("address/allcities", nil) + if err != nil { + t.Fatalf("Error when accessing accessJDQuery: %v", err) + } else { + code := result["code"].(string) + if code != "0" { + t.Fatalf("code is not 0, %v", code) + } + } +} + +func TestAccessJDQueryNoPage(t *testing.T) { + result, err := jdapi.accessJDQueryNoPage("address/allcities", nil, nil, nil) + if err != nil { + t.Fatalf("accessJDQueryNoPage return error:%v", err) + } + cityInfo := result.([]interface{}) + if len(cityInfo) == 0 { + t.Fatal("city info is empty") + } + oneCity := cityInfo[0].(map[string]interface{}) + if _, ok := oneCity["areaName"]; !ok { + t.Fatal("no areaName key") + } +} + +func TestAccessJDQueryHavePage(t *testing.T) { + result, err := jdapi.accessJDQueryHavePage("pms/querySkuInfos", nil, nil, nil, 1, 50, nil) + if err != nil { + t.Fatalf("accessJDQueryHavePage return error:%v", err) + } + skuInfo := result.([]interface{}) + if len(skuInfo) == 0 { + t.Fatal("city info is empty") + } + oneSku := skuInfo[0].(map[string]interface{}) + if _, ok := oneSku["skuName"]; !ok { + t.Fatal("no skuName key") + } +} + +func TestGenGetURL(t *testing.T) { + params := make(map[string]string) + params["key"] = "v" + params["key2"] = "v2" + + fullURL := genGetURL(jdAPIURL, "address/allcities", params) + + response, err := http.Get(fullURL) + if err != nil { + t.Fatalf("Get return error:%v", err) + } + defer response.Body.Close() +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..faeabcf8 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,78 @@ +package utils + +import ( + "bytes" + "encoding/json" + "reflect" +) + +func DictKeysMan(data interface{}, keysToRemove []string, keysToKeep []string) interface{} { + if data == nil || (keysToKeep == nil && keysToRemove == nil) { + return data + } + + var keysToRemoveMap map[string]int + var keysToKeepMap map[string]int + if keysToKeep != nil { + keysToKeepMap = make(map[string]int) + for _, v := range keysToKeep { + keysToKeepMap[v] = 1 + } + } else if keysToRemove != nil { + keysToRemoveMap = make(map[string]int) + for _, v := range keysToRemove { + keysToRemoveMap[v] = 1 + } + } + + dataIsSlice := true + valueOfData := reflect.ValueOf(data) + if valueOfData.Kind() != reflect.Slice { + dataIsSlice = false + valueOfData = reflect.ValueOf([]interface{}{data}) + } else if valueOfData.Len() == 0 { + return data + } + + retVal := make([]interface{}, valueOfData.Len()) + + for index := 0; index < valueOfData.Len(); index++ { + realV := valueOfData.Index(index) + if realV.Kind() == reflect.Interface || realV.Kind() == reflect.Ptr { + realV = realV.Elem() + } + if keysToRemoveMap != nil || keysToKeepMap != nil { + mapV := make(map[string]interface{}) + for _, key := range realV.MapKeys() { + fieldName := key.String() + wantThisField := true + if keysToKeepMap != nil { + if _, ok := keysToKeepMap[fieldName]; !ok { + wantThisField = false + } + } else if keysToRemoveMap != nil { + if _, ok := keysToRemoveMap[fieldName]; ok { + wantThisField = false + } + } + if wantThisField { + mapV[fieldName] = realV.MapIndex(key) + } + } + retVal[index] = mapV + } else { + retVal[index] = realV.Interface() + } + } + + if !dataIsSlice { + return retVal[0] + } + return retVal +} + +func UnmarshalUseNumber(data []byte, result interface{}) error { + d := json.NewDecoder(bytes.NewReader(data)) + d.UseNumber() + return d.Decode(result) +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 00000000..87e096d6 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,35 @@ +package utils + +import ( + "testing" +) + +type II interface { + foo() +} + +type TT struct { +} + +func (t TT) foo() { + +} +func TestDictKeysMan(t *testing.T) { + testData := map[string]string{"k1": "v1", "k2": "v2"} + result1 := DictKeysMan(testData, []string{"k1"}, nil).(map[string]interface{}) + if _, ok := result1["k1"]; ok { + t.Error("Params keysToRemove can not remove key!") + } + if _, ok := result1["k2"]; !ok { + t.Error("Params keysToRemove removed wrong key!") + } + + result2 := DictKeysMan([]interface{}{testData}, nil, []string{"k1"}).([]interface{}) + result20 := result2[0].(map[string]interface{}) + if _, ok := result20["k1"]; !ok { + t.Error("Params keysToKeep can not keep key!") + } + if _, ok := result20["k2"]; ok { + t.Error("Params keysToKeep keep wrong key!") + } +}