package weixinapi import ( "crypto/aes" "crypto/cipher" "net/http" "strings" "sync" "git.rosy.net.cn/baseapi/platformapi" "git.rosy.net.cn/baseapi/utils" ) const ( prodURL = "https://api.weixin.qq.com" ) const ( actionGetToken = "token" ) const ( ResponseCodeBusy = -1 ResponseCodeSuccess = 0 ) 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 } type SNSTokenInfo struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` OpenID string `json:"openid"` Scope string `json:"scope"` } type SNSUserInfo struct { OpenID string `json:"openid"` NickName string `json:"nickname"` Sex int `json:"sex"` Province string `json:"province"` City string `json:"city"` Country string `json:"country"` HeadImgURL string `json:"headimgurl"` Privilege interface{} `json:"privilege"` UnionID string `json:"unionid"` } type SessionInfo struct { OpenID string `json:"openid"` SessionKey string `json:"session_key"` UnionID string `json:"unionid"` } 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 (a *API) SetToken(newToken string) bool { curToken := a.GetToken() if curToken != newToken { a.locker.Lock() defer a.locker.Unlock() a.token = newToken return true } return false } func (a *API) GetToken() string { a.locker.RLock() defer a.locker.RUnlock() return a.token } 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 non-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.GetToken() if accessToken == "" { panic("token is empty!") } params2["access_token"] = accessToken } fullURL := utils.GenerateGetURL(prodURL, action, params2) // baseapi.SugarLogger.Debug(fullURL) 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)) } request.Close = true // todo try to fix EOF error when accessing weixin api. return request }, a.config, func(jsonResult1 map[string]interface{}) (result string, err error) { 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 (a *API) RefreshToken() (tokenInfo *TokenInfo, err error) { result, err := a.AccessAPI("cgi-bin/token", utils.Params2Map("grant_type", "client_credential"), "") if err != nil { return nil, err } tokenInfo = &TokenInfo{ AccessToken: utils.Interface2String(result["access_token"]), ExpiresIn: int(utils.MustInterface2Int64(result["expires_in"])), } // update my token too. a.SetToken(tokenInfo.AccessToken) return tokenInfo, nil } func (a *API) MessageTemplateSend(userOpenID, templateID, downloadURL string, miniProgram, data interface{}) (err error) { bodyJson := map[string]interface{}{ "touser": userOpenID, "template_id": templateID, "url": downloadURL, "data": data, } if downloadURL != "" { bodyJson["url"] = downloadURL } if miniProgram != nil { bodyJson["miniprogram"] = miniProgram } _, err = a.AccessAPI("cgi-bin/message/template/send", nil, string(utils.MustMarshal(bodyJson))) return err } func mapData2SNSToken(result map[string]interface{}) *SNSTokenInfo { return &SNSTokenInfo{ AccessToken: utils.Interface2String(result["access_token"]), ExpiresIn: int(utils.MustInterface2Int64(result["expires_in"])), RefreshToken: utils.Interface2String(result["refresh_token"]), OpenID: utils.Interface2String(result["openid"]), Scope: utils.Interface2String(result["scope"]), } } func (a *API) SNSGetToken(code string) (tokenInfo *SNSTokenInfo, err error) { result, err := a.AccessAPI("sns/oauth2/access_token", utils.Params2Map("grant_type", "authorization_code", "code", code), "") if err != nil { return nil, err } return mapData2SNSToken(result), nil } func (a *API) SNSRefreshToken(refreshToken string) (tokenInfo *SNSTokenInfo, err error) { result, err := a.AccessAPI("sns/oauth2/refresh_token", utils.Params2Map("grant_type", "refresh_token", "refresh_token", refreshToken), "") if err != nil { return nil, err } return mapData2SNSToken(result), nil } func (a *API) SNSGetUserInfo(accessToken, openid string) (*SNSUserInfo, error) { result, err := a.AccessAPI("sns/userinfo", map[string]interface{}{ "access_token": accessToken, "openid": openid, }, "") if err == nil { retVal := &SNSUserInfo{ OpenID: utils.Interface2String(result["openid"]), NickName: utils.Interface2String(result["nickname"]), Sex: int(utils.MustInterface2Int64(result["sex"])), Province: utils.Interface2String(result["province"]), City: utils.Interface2String(result["city"]), Country: utils.Interface2String(result["country"]), HeadImgURL: utils.Interface2String(result["headimgurl"]), Privilege: result["privilege"], UnionID: utils.Interface2String(result["unionid"]), } return retVal, nil } return nil, err } func (a *API) SNSIsOpenIDValid(accessToken, openid string) (bool, error) { _, err := a.AccessAPI("sns/auth", map[string]interface{}{ "access_token": accessToken, "openid": openid, }, "") if err == nil { return true, nil } return false, err } func (a *API) SNSCode2Session(code string) (sessionInfo *SessionInfo, err error) { result, err := a.AccessAPI("sns/jscode2session", map[string]interface{}{ "js_code": code, "grant_type": "authorization_code", }, "") if err == nil { return &SessionInfo{ OpenID: utils.Interface2String(result["openid"]), SessionKey: utils.Interface2String(result["session_key"]), UnionID: utils.Interface2String(result["unionid"]), }, nil } return nil, err } func (a *API) SNSDecodeMiniProgramData(encryptedData, sessionKey, iv string) (decryptedData []byte, err error) { decodedDataList, err := utils.Base64DecodeMultiString(encryptedData, sessionKey, iv) if err != nil { return nil, err } c, err := aes.NewCipher(decodedDataList[1]) if err != nil { return nil, err } cfbdec := cipher.NewCBCDecrypter(c, decodedDataList[2][:c.BlockSize()]) decryptedData = make([]byte, len(decodedDataList[0])) cfbdec.CryptBlocks(decryptedData, decodedDataList[0]) decryptedData = PKCS7UnPadding(decryptedData) return decryptedData, nil } func PKCS7UnPadding(origData []byte) []byte { length := len(origData) unpadding := int(origData[length-1]) return origData[:(length - unpadding)] }