// 获取用户信息. // lang 可以为空值. func (clt *Client) GetUserInfo(userinfo interface{}, lang string) (err error) { if clt.Config == nil { err = errors.New("nil Config") return } tk, err := clt.getToken() if err != nil { return } // 过期自动刷新 Token if tk.AccessTokenExpired() { if tk, err = clt.tokenRefresh(tk); err != nil { return } } httpResp, err := clt.httpClient().Get(clt.Config.UserInfoURL(tk.AccessToken, tk.OpenId, lang)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } httpRespBytes, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } var errResult Error if err = json.Unmarshal(httpRespBytes, &errResult); err != nil { return } if errResult.ErrCode != ErrCodeOK { return &errResult } return json.Unmarshal(httpRespBytes, userinfo) }
// 通用上传接口. // // --BOUNDARY // Content-Disposition: form-data; name="FIELDNAME"; filename="FILENAME" // Content-Type: application/octet-stream // // FILE-CONTENT // --BOUNDARY // Content-Disposition: form-data; name="FIELDNAME" // // JSON-DESCRIPTION // --BOUNDARY-- // // // NOTE: // 1. 一般不需要调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + access_token; // 3. response 格式有要求, 要么是 *Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // Error // ... // } func (clt *Client) PostMultipartForm(incompleteURL string, fields []MultipartFormField, response interface{}) (err error) { bodyBuf := mediaBufferPool.Get().(*bytes.Buffer) bodyBuf.Reset() defer mediaBufferPool.Put(bodyBuf) multipartWriter := multipart.NewWriter(bodyBuf) for _, field := range fields { switch field.ContentType { case 0: // 文件 partWriter, err := multipartWriter.CreateFormFile(field.FieldName, field.FileName) if err != nil { return err } if _, err = io.Copy(partWriter, field.Value); err != nil { return err } case 1: // 文本 partWriter, err := multipartWriter.CreateFormField(field.FieldName) if err != nil { return err } if _, err = io.Copy(partWriter, field.Value); err != nil { return err } } } if err = multipartWriter.Close(); err != nil { return } bodyBytes := bodyBuf.Bytes() token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) httpResp, err := clt.HttpClient.Post(finalURL, multipartWriter.FormDataContentType(), bytes.NewReader(bodyBytes)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { err = fmt.Errorf("http.Status: %s", httpResp.Status) return } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } LogInfoln("[WECHAT_DEBUG] request url:", finalURL) LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, 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 + suite_access_token; // 3. response 格式有要求, 要么是 *corp.Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // corp.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) corp.LogInfoln("[WECHAT_DEBUG] request url:", finalURL) corp.LogInfoln("[WECHAT_DEBUG] 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 } corp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, response); err != nil { return } var ErrorStructValue reflect.Value // corp.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 corp.ErrCodeOK: return case corp.ErrCodeSuiteAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() corp.LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", ErrMsg) corp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } corp.LogInfoln("[WECHAT_RETRY] new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } corp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: return } }
// GET 微信资源, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + component_access_token; // 3. response 格式有要求, 要么是 *mp.Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // mp.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) } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } mp.LogInfoln("[WECHAT_DEBUG] request url:", finalURL) mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, response); err != nil { return } var ErrorStructValue reflect.Value // mp.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 mp.ErrCodeOK: return case mp.ErrCodeInvalidCredential, mp.ErrCodeAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() mp.LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", 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) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } mp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) 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 }