// Client 通用的 json post 请求 func (c *Client) postJSON(url_ string, request interface{}, response interface{}) (err error) { buf := textBufferPool.Get().(*bytes.Buffer) // io.ReadWriter buf.Reset() // important defer textBufferPool.Put(buf) if err = wechatjson.NewEncoder(buf).Encode(request); err != nil { return } resp, err := c.httpClient.Post(url_, "application/json; charset=utf-8", buf) if err != nil { return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", resp.Status) } if err = json.NewDecoder(resp.Body).Decode(response); err != nil { return } return }
// 用 encoding/json 把 request marshal 为 JSON, 放入 http 请求的 body 中, // POST 到微信服务器, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + access_token; // 3. response 格式有要求, 要么是 *Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // Error // ... // } func (clt *Client) PostJSON(incompleteURL string, request interface{}, response interface{}) (err error) { buf := textBufferPool.Get().(*bytes.Buffer) buf.Reset() defer textBufferPool.Put(buf) if err = wechatjson.NewEncoder(buf).Encode(request); err != nil { return } requestBytes := buf.Bytes() token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) httpResp, err := clt.HttpClient.Post(finalURL, "application/json; charset=utf-8", bytes.NewReader(requestBytes)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } if err = json.NewDecoder(httpResp.Body).Decode(response); err != nil { return } var ErrorStructValue reflect.Value // Error // 下面的代码对 response 有特定要求, 见此函数 NOTE responseStructValue := reflect.ValueOf(response).Elem() if v := responseStructValue.Field(0); v.Kind() == reflect.Struct { ErrorStructValue = v } else { ErrorStructValue = responseStructValue } switch ErrCode := ErrorStructValue.Field(0).Int(); ErrCode { case ErrCodeOK: return case ErrCodeInvalidCredential, ErrCodeAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", ErrMsg) LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } LogInfoln("[WECHAT_RETRY] new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: return } }
// 用 encoding/json 把 request marshal 为 JSON, 放入 http 请求的 body 中, // POST 到微信服务器, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + access_token; // 3. response 要求是 struct 的指针, 并且该 struct 拥有属性: // ErrCode int `json:"errcode"` (可以是直接属性, 也可以是匿名属性里的属性) func (clt *CorpClient) PostJSON(incompleteURL string, request interface{}, response interface{}) (err error) { buf := textBufferPool.Get().(*bytes.Buffer) buf.Reset() defer textBufferPool.Put(buf) if err = wechatjson.NewEncoder(buf).Encode(request); err != nil { return } requestBytes := buf.Bytes() token, err := clt.Token() if err != nil { return } debugPrefix := "corp.CorpClient.PostJSON" if _, file, line, ok := runtime.Caller(1); ok { debugPrefix += fmt.Sprintf("(called at %s:%d)", file, line) } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) log.Println(debugPrefix, "request url:", finalURL) log.Println(debugPrefix, "request json:", string(requestBytes)) httpResp, err := clt.HttpClient.Post(finalURL, "application/json; charset=utf-8", bytes.NewReader(requestBytes)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } log.Println(debugPrefix, "response json:", string(respBody)) if err = json.Unmarshal(respBody, response); err != nil { return } // 请注意: // 下面获取 ErrCode 的代码不具备通用性!!! // // 因为本 SDK 的 response 都是 // struct { // Error // XXX // } // 的结构, 所以用下面简单的方法得到 ErrCode. // // 如果你是直接调用这个函数, 那么要根据你的 response 数据结构修改下面的代码. responseStructValue := reflect.ValueOf(response).Elem() ErrCode := responseStructValue.FieldByName("ErrCode").Int() switch ErrCode { case ErrCodeOK: return case ErrCodeTimeout, ErrCodeInvalidCredential: ErrMsg := responseStructValue.FieldByName("ErrMsg").String() log.Println("wechat/corp.PostJSON: RETRY, err_code:", ErrCode, ", err_msg:", ErrMsg) log.Println("wechat/corp.PostJSON: RETRY, current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } log.Println("wechat/corp.PostJSON: RETRY, new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } log.Println("wechat/corp.PostJSON: RETRY fallthrough, current token:", token) fallthrough default: return } }
// 用 encoding/json 把 request marshal 为 JSON, 放入 http 请求的 body 中, // POST 到微信服务器, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + access_token; // 3. response 要求是 struct 的指针, 并且该 struct 拥有属性: // ErrCode int `json:"errcode"` (可以是直接属性, 也可以是匿名属性里的属性) func (clt *CorpClient) PostJSON(incompleteURL string, request interface{}, response interface{}) (err error) { buf := textBufferPool.Get().(*bytes.Buffer) buf.Reset() defer textBufferPool.Put(buf) if err = wechatjson.NewEncoder(buf).Encode(request); err != nil { return } requestBytes := buf.Bytes() token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) httpResp, err := clt.HttpClient.Post(finalURL, "application/json; charset=utf-8", bytes.NewReader(requestBytes)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } if err = json.NewDecoder(httpResp.Body).Decode(response); err != nil { return } // 请注意: // 下面获取 ErrCode 的代码不具备通用性!!! // // 因为本 SDK 的 response 都是 // struct { // Error // XXX // } // 的结构, 所以用下面简单的方法得到 ErrCode. // // 如果你是直接调用这个函数, 那么要根据你的 response 数据结构修改下面的代码. ErrCode := reflect.ValueOf(response).Elem().FieldByName("ErrCode").Int() switch ErrCode { case ErrCodeOK: return case ErrCodeTimeout, ErrCodeInvalidCredential: if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } goto RETRY } fallthrough default: return } }
// 从微信服务器获取 suite_access_token. // 同一时刻只能一个 goroutine 进入, 防止没必要的重复获取. func (srv *DefaultAccessTokenServer) getToken() (token accessTokenInfo, cached bool, err error) { srv.tokenGet.Lock() defer srv.tokenGet.Unlock() timeNowUnix := time.Now().Unix() // 在收敛周期内直接返回最近一次获取的 suite_access_token, 这里的收敛时间设定为4秒. if n := srv.tokenGet.LastTimestamp; n <= timeNowUnix && timeNowUnix < n+4 { // 因为只有成功获取后才会更新 srv.tokenGet.LastTimestamp, 所以这些都是有效数据 token = accessTokenInfo{ Token: srv.tokenGet.LastTokenInfo.Token, ExpiresIn: srv.tokenGet.LastTokenInfo.ExpiresIn - timeNowUnix + n, } cached = true return } suiteTicket, err := srv.ticketGetter.GetSuiteTicket(srv.suiteId) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } request := struct { SuiteId string `json:"suite_id"` SuiteSecret string `json:"suite_secret"` SuiteTicket string `json:"suite_ticket"` }{ SuiteId: srv.suiteId, SuiteSecret: srv.suiteSecret, SuiteTicket: suiteTicket, } requestBuf := textBufferPool.Get().(*bytes.Buffer) requestBuf.Reset() defer textBufferPool.Put(requestBuf) if err = json.NewEncoder(requestBuf).Encode(&request); err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } requestBytes := requestBuf.Bytes() url := "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token" corp.LogInfoln("[WECHAT_DEBUG] request url:", url) corp.LogInfoln("[WECHAT_DEBUG] request json:", string(requestBytes)) httpResp, err := srv.httpClient.Post(url, "application/json; charset=utf-8", requestBuf) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = fmt.Errorf("http.Status: %s", httpResp.Status) return } var result struct { corp.Error accessTokenInfo } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } corp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, &result); err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } if result.ErrCode != corp.ErrCodeOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = &result.Error return } // 由于网络的延时, suite_access_token 过期时间留了一个缓冲区 switch { case result.ExpiresIn > 31556952: // 60*60*24*365.2425 srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = errors.New("expires_in too large: " + strconv.FormatInt(result.ExpiresIn, 10)) return case result.ExpiresIn > 60*60: result.ExpiresIn -= 60 * 10 case result.ExpiresIn > 60*30: result.ExpiresIn -= 60 * 5 case result.ExpiresIn > 60*5: result.ExpiresIn -= 60 case result.ExpiresIn > 60: result.ExpiresIn -= 10 default: srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = errors.New("expires_in too small: " + strconv.FormatInt(result.ExpiresIn, 10)) return } // 更新 tokenGet 信息 srv.tokenGet.LastTokenInfo = result.accessTokenInfo srv.tokenGet.LastTimestamp = timeNowUnix // 更新缓存 srv.tokenCache.Lock() srv.tokenCache.Token = result.accessTokenInfo.Token srv.tokenCache.Unlock() token = result.accessTokenInfo return }
// 从微信服务器获取 component_access_token. // 同一时刻只能一个 goroutine 进入, 防止没必要的重复获取. func (srv *DefaultAccessTokenServer) getToken() (token accessTokenInfo, cached bool, err error) { srv.tokenGet.Lock() defer srv.tokenGet.Unlock() timeNowUnix := time.Now().Unix() // 在收敛周期内直接返回最近一次获取的 component_access_token, 这里的收敛时间设定为4秒. if n := srv.tokenGet.LastTimestamp; n <= timeNowUnix && timeNowUnix < n+4 { // 因为只有成功获取后才会更新 srv.tokenGet.LastTimestamp, 所以这些都是有效数据 token = accessTokenInfo{ Token: srv.tokenGet.LastTokenInfo.Token, ExpiresIn: srv.tokenGet.LastTokenInfo.ExpiresIn - timeNowUnix + n, } cached = true return } verifyTicket, err := srv.verifyTicketGetter.GetComponentVerifyTicket(srv.appId) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } request := struct { AppId string `json:"component_appid"` AppSecret string `json:"component_appsecret"` VerifyTicket string `json:"component_verify_ticket"` }{ AppId: srv.appId, AppSecret: srv.appSecret, VerifyTicket: verifyTicket, } requestBuf := textBufferPool.Get().(*bytes.Buffer) requestBuf.Reset() defer textBufferPool.Put(requestBuf) if err = json.NewEncoder(requestBuf).Encode(&request); err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } url := "https://api.weixin.qq.com/cgi-bin/component/api_component_token" httpResp, err := srv.httpClient.Post(url, "application/json; charset=utf-8", requestBuf) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = fmt.Errorf("http.Status: %s", httpResp.Status) return } var result struct { mp.Error accessTokenInfo } if err = json.NewDecoder(httpResp.Body).Decode(&result); err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } if result.ErrCode != mp.ErrCodeOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = &result.Error return } // 由于网络的延时, component_access_token 过期时间留了一个缓冲区 switch { case result.ExpiresIn > 31556952: // 60*60*24*365.2425 srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = errors.New("expires_in too large: " + strconv.FormatInt(result.ExpiresIn, 10)) return case result.ExpiresIn > 60*60: result.ExpiresIn -= 60 * 10 case result.ExpiresIn > 60*30: result.ExpiresIn -= 60 * 5 case result.ExpiresIn > 60*5: result.ExpiresIn -= 60 case result.ExpiresIn > 60: result.ExpiresIn -= 10 default: srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = errors.New("expires_in too small: " + strconv.FormatInt(result.ExpiresIn, 10)) return } // 更新 tokenGet 信息 srv.tokenGet.LastTokenInfo = result.accessTokenInfo srv.tokenGet.LastTimestamp = timeNowUnix // 更新缓存 srv.tokenCache.Lock() srv.tokenCache.Token = result.accessTokenInfo.Token srv.tokenCache.Unlock() token = result.accessTokenInfo return }