// GET 微信资源, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + access_token; // 3. response 格式有要求, 要么是 *Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // Error // ... // } func (clt *Client) GetJSON(incompleteURL string, response interface{}) (err error) { token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) httpResp, err := clt.HttpClient.Get(finalURL) 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 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 } }
// 下载多媒体到 io.Writer. func (clt *Client) downloadMediaToWriter(mediaId string, writer io.Writer) (written int64, err error) { token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := "https://api.weixin.qq.com/cgi-bin/media/get?media_id=" + url.QueryEscape(mediaId) + "&access_token=" + url.QueryEscape(token) httpResp, err := clt.HttpClient.Get(finalURL) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { err = fmt.Errorf("http.Status: %s", httpResp.Status) return } ContentType, _, _ := mime.ParseMediaType(httpResp.Header.Get("Content-Type")) if ContentType != "text/plain" && ContentType != "application/json" { // 返回的是媒体流 return io.Copy(writer, httpResp.Body) } // 返回的是错误信息 var result mp.Error if err = json.NewDecoder(httpResp.Body).Decode(&result); err != nil { return } switch result.ErrCode { case mp.ErrCodeOK: return // 基本不会出现 case mp.ErrCodeInvalidCredential, mp.ErrCodeAccessTokenExpired: // 失效(过期)重试一次 mp.LogInfoln("[WECHAT_RETRY] err_code:", result.ErrCode, ", err_msg:", result.ErrMsg) mp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } mp.LogInfoln("[WECHAT_RETRY] new token:", token) result = mp.Error{} goto RETRY } mp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: err = &result return } }
func (clt *Client) getJSON(url string, response interface{}) (err error) { httpResp, err := clt.httpClient().Get(url) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } return json.NewDecoder(httpResp.Body).Decode(response) }
// 从微信服务器获取 access_token. // 同一时刻只能一个 goroutine 进入, 防止没必要的重复获取. func (srv *DefaultAccessTokenServer) getToken() (token accessTokenInfo, cached bool, err error) { srv.tokenGet.Lock() defer srv.tokenGet.Unlock() timeNowUnix := time.Now().Unix() // 在收敛周期内直接返回最近一次获取的 access_token, // 这里的收敛时间设定为2秒, 因为在同一个进程内, 收敛周期为2个http周期 if n := srv.tokenGet.LastTimestamp; n <= timeNowUnix && timeNowUnix < n+2 { // 因为只有成功获取后才会更新 srv.tokenGet.LastTimestamp, 所以这些都是有效数据 token = accessTokenInfo{ Token: srv.tokenGet.LastTokenInfo.Token, ExpiresIn: srv.tokenGet.LastTokenInfo.ExpiresIn - timeNowUnix + n, } cached = true return } _url := "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + url.QueryEscape(srv.appId) + "&secret=" + url.QueryEscape(srv.appSecret) httpResp, err := srv.httpClient.Get(_url) 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 { 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 != ErrCodeOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = &result.Error return } // 由于网络的延时, 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 }
// 用 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 = json.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 } }
// 从微信服务器获取 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 }