package weixinapi import ( "bytes" "fmt" "math/rand" "mime/multipart" "net/http" "net/textproto" "strings" "sync" "gitrosy.jxc4.com/baseapi/platformapi" "gitrosy.jxc4.com/baseapi/utils" ) const ( prodURL = "https://api.weixin.qq.com" ) const ( ResponseCodeBusy = -1 ResponseCodeSuccess = 0 letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ) type TokenInfo struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } type ErrorInfo struct { ErrCode int `json:"errcode"` ErrMsg string `json:"errmsg"` } type API struct { token string appID string secret string client *http.Client config *platformapi.APIConfig locker sync.RWMutex msgToken string msgKey string } func New(appID, secret string, config ...*platformapi.APIConfig) *API { curConfig := platformapi.DefAPIConfig if len(config) > 0 { curConfig = *config[0] } return &API{ appID: appID, secret: secret, client: &http.Client{Timeout: curConfig.ClientTimeout}, config: &curConfig, } } func (a *API) GetAppID() string { return a.appID } func (a *API) GetSecret() string { return a.secret } func isSNSAction(action string) bool { return strings.Index(action, "sns/") == 0 } func (a *API) AccessAPI(action string, params map[string]interface{}, body string) (retVal map[string]interface{}, err error) { if params == nil && body == "" { panic("params and body can not all be empty") } params2 := make(map[string]interface{}) for k, v := range params { params2[k] = v } if params2["grant_type"] != nil { params2["appid"] = a.appID params2["secret"] = a.secret } else if !isSNSAction(action) { accessToken := a.CBGetToken() if accessToken == "" { panic("token is empty!") } params2["access_token"] = accessToken } fullURL := utils.GenerateGetURL(prodURL, action, params2) err = platformapi.AccessPlatformAPIWithRetry(a.client, func() *http.Request { var request *http.Request if body == "" { request, _ = http.NewRequest(http.MethodGet, fullURL, nil) } else { request, _ = http.NewRequest(http.MethodPost, fullURL, strings.NewReader(body)) if strings.Contains(fullURL, "wxa/generate_urllink") { request.Header.Set("Content-Type", "application/json") } } request.Close = true // todo try to fix EOF error when accessing weixin api. return request }, a.config, func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) { if jsonResult1 == nil { return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil") } var errInfo *ErrorInfo // 微信的返回值,在错误与正常情况下,结构是完全不一样的 if errCode, ok := jsonResult1["errcode"]; ok { errInfo = &ErrorInfo{ ErrCode: int(utils.MustInterface2Int64(errCode)), ErrMsg: jsonResult1["errmsg"].(string), } if errInfo.ErrCode == 0 { retVal = jsonResult1 } } else { retVal = jsonResult1 } if retVal != nil { return platformapi.ErrLevelSuccess, nil } newErr := utils.NewErrorIntCode(errInfo.ErrMsg, errInfo.ErrCode) if errInfo.ErrCode == ResponseCodeBusy { return platformapi.ErrLevelRecoverableErr, newErr } return platformapi.ErrLevelCodeIsNotOK, newErr }) return retVal, err } func RandStringBytes(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } func getBoundary() (boundary string) { boundary = "----WebKitFormBoundary" boundary += RandStringBytes(16) return boundary } func (a *API) AccessAPIUpload(url string, data []byte, fileName, contentType, materialType string) (retVal map[string]interface{}, err error) { params2 := make(map[string]interface{}) accessToken := a.CBGetToken() if accessToken == "" { panic("token is empty!") } params2["access_token"] = accessToken fullURL := utils.GenerateGetURL(prodURL, url, params2) // baseapi.SugarLogger.Debug(fullURL) // 实例化multipart body := &bytes.Buffer{} writer := multipart.NewWriter(body) writer.SetBoundary(getBoundary()) // 创建multipart 文件字段 h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "media", fileName)) h.Set("Content-Type", contentType) part, err := writer.CreatePart(h) // 写入文件数据到multipart,和读取本地文件方法的唯一区别 _, err = part.Write(data) //将额外参数也写入到multipart if materialType != "" { _ = writer.WriteField("type", materialType) } err = writer.Close() err = platformapi.AccessPlatformAPIWithRetry(a.client, func() *http.Request { request, _ := http.NewRequest(http.MethodPost, fullURL, body) request.Header.Set("Content-Type", writer.FormDataContentType()) return request }, a.config, func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) { if jsonResult1 == nil { return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil") } var errInfo *ErrorInfo // 微信的返回值,在错误与正常情况下,结构是完全不一样的 if errCode, ok := jsonResult1["errcode"]; ok { errInfo = &ErrorInfo{ ErrCode: int(utils.MustInterface2Int64(errCode)), ErrMsg: jsonResult1["errmsg"].(string), } if errInfo.ErrCode == 0 { retVal = jsonResult1 } } else { retVal = jsonResult1 } if retVal != nil { return platformapi.ErrLevelSuccess, nil } newErr := utils.NewErrorIntCode(errInfo.ErrMsg, errInfo.ErrCode) if errInfo.ErrCode == ResponseCodeBusy { return platformapi.ErrLevelRecoverableErr, newErr } return platformapi.ErrLevelCodeIsNotOK, newErr }) return retVal, err }