commit cf845c9cd39b85ec8748fcf726d1d14511a06f47 Author: liuxingran Date: Tue Jan 13 19:33:04 2026 +0800 微信公众号获取关注列表 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/development-kit-sdk.iml b/.idea/development-kit-sdk.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/development-kit-sdk.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..590545b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4b4367 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.valavala.com/vala/development-kit-sdk + +go 1.24.9 diff --git a/pre_commit.sh b/pre_commit.sh new file mode 100644 index 0000000..5e476bb --- /dev/null +++ b/pre_commit.sh @@ -0,0 +1 @@ +go build ./... && go mod tidy \ No newline at end of file diff --git a/utils/http.go b/utils/http.go new file mode 100644 index 0000000..c80b404 --- /dev/null +++ b/utils/http.go @@ -0,0 +1,42 @@ +package utils + +import ( + "bytes" + "fmt" + "io" + "net/http" + "time" +) + +// HttpRequest 发送 HTTP 请求 +func HttpRequest(method, url string, headers map[string]string, body []byte) ([]byte, error) { + client := &http.Client{ + Timeout: 30 * time.Second, // 设置超时时间 + } + + // 创建请求 + req, err := http.NewRequest(method, url, bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + + // 设置请求头 + for key, value := range headers { + req.Header.Set(key, value) + } + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + // 检查响应状态码 + rBody, err := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(rBody)) + } + + // 读取响应 + return rBody, err +} diff --git a/wechat-sdk/access-token/access_token.go b/wechat-sdk/access-token/access_token.go new file mode 100644 index 0000000..8403720 --- /dev/null +++ b/wechat-sdk/access-token/access_token.go @@ -0,0 +1,68 @@ +package access_token + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "git.valavala.com/vala/development-kit-sdk/utils" + "git.valavala.com/vala/development-kit-sdk/wechat-sdk/structs" +) + +var wechatStableAccessToken []*structs.WechatStableAccessToken + +type wechatStableAccessTokenMgr struct { +} + +func NewWechatStableAccessTokenMgr() *wechatStableAccessTokenMgr { + return &wechatStableAccessTokenMgr{} +} + +func (m *wechatStableAccessTokenMgr) GetStableAccessToken(appConf *structs.WechatConf) (string, error) { + if appConf == nil { + return "", errors.New("conf is empty") + } + + for i := range wechatStableAccessToken { + if wechatStableAccessToken[i].Appid == appConf.AppId { + if wechatStableAccessToken[i].AccessTokenExpiresAt > time.Now().Unix() && wechatStableAccessToken[i].AccessToken != "" { + return wechatStableAccessToken[i].AccessToken, nil + } + break + } + } + url := "https://api.weixin.qq.com/cgi-bin/stable_token" + params := map[string]string{ + "grant_type": "client_credential", + "appid": appConf.AppId, + "secret": appConf.AppSecret, + } + body, _ := json.Marshal(params) + header := map[string]string{ + "Content-Type": "application/json", + } + resp, err := utils.HttpRequest(http.MethodPost, url, header, body) + if err != nil { + return "", err + } + thisToken := &structs.WechatStableAccessToken{} + _ = json.Unmarshal(resp, thisToken) + thisToken.AccessTokenExpiresAt = time.Now().Unix() + int64(thisToken.ExpiresIn) + thisToken.Appid = appConf.AppId + if thisToken.AccessToken == "" { + return "", errors.New(appConf.AppId + " access_token is empty") + } + + var newTokens []*structs.WechatStableAccessToken + for i := range wechatStableAccessToken { + if wechatStableAccessToken[i].Appid == appConf.AppId { + newTokens = append(newTokens, thisToken) + continue + } + newTokens = append(newTokens, wechatStableAccessToken[i]) + } + wechatStableAccessToken = newTokens + + return thisToken.AccessToken, nil +} diff --git a/wechat-sdk/public-platform/fans.go b/wechat-sdk/public-platform/fans.go new file mode 100644 index 0000000..32af08b --- /dev/null +++ b/wechat-sdk/public-platform/fans.go @@ -0,0 +1,43 @@ +package public_platform + +import ( + "encoding/json" + "errors" + "net/http" + + "git.valavala.com/vala/development-kit-sdk/utils" + access_token "git.valavala.com/vala/development-kit-sdk/wechat-sdk/access-token" + "git.valavala.com/vala/development-kit-sdk/wechat-sdk/structs" +) + +type PublicPlatform struct { +} + +func NewWechatPublicPlatform() *PublicPlatform { + return &PublicPlatform{} +} + +// GetFans 获取公众号的关注列表 +// https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=OPENID +func (p *PublicPlatform) GetFans(wechatConf *structs.WechatConf, nextOpenid string) (*structs.WechatPublicPlatformFansResp, error) { + mgr := access_token.NewWechatStableAccessTokenMgr() + accessToken, err := mgr.GetStableAccessToken(wechatConf) + if err != nil { + return nil, err + } + url := "https://api.weixin.qq.com/cgi-bin/user/get?access_token=" + accessToken + "&next_openid=" + nextOpenid + + header := map[string]string{ + "Content-Type": "application/json", + } + resp, err := utils.HttpRequest(http.MethodGet, url, header, nil) + if err != nil { + return nil, err + } + wechatPublicPlatformFansResp := &structs.WechatPublicPlatformFansResp{} + _ = json.Unmarshal(resp, wechatPublicPlatformFansResp) + if wechatPublicPlatformFansResp.Errcode != 0 { + return nil, errors.New(wechatPublicPlatformFansResp.Errmsg) + } + return wechatPublicPlatformFansResp, nil +} diff --git a/wechat-sdk/structs/common.go b/wechat-sdk/structs/common.go new file mode 100644 index 0000000..02cc551 --- /dev/null +++ b/wechat-sdk/structs/common.go @@ -0,0 +1,21 @@ +package structs + +type WechatConf struct { + AppId string `json:"appId"` + AppName string `json:"appName"` + AppSecret string `json:"appSecret"` + MsgToken string `json:"msgToken"` + MsgAesKey string `json:"msgAesKey"` +} + +type WechatStableAccessToken struct { + Appid string `json:"appid"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + AccessTokenExpiresAt int64 `json:"access_token_expires_at"` +} + +type WechatCommonResp struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} diff --git a/wechat-sdk/structs/public_platform_fans.go b/wechat-sdk/structs/public_platform_fans.go new file mode 100644 index 0000000..2233244 --- /dev/null +++ b/wechat-sdk/structs/public_platform_fans.go @@ -0,0 +1,20 @@ +package structs + +/* + WechatPublicPlatformFansResp + +一次拉取调用最多拉取10000个关注者的OpenID,可以通过多次拉取的方式来满足需求。 +最后一次返回时next_openid可能为空表示列表结束。 + +接口信息:https://developers.weixin.qq.com/doc/subscription/api/usermanage/userinfo/api_getfans.html +*/ + +type WechatPublicPlatformFansResp struct { + WechatCommonResp + Total int `json:"total"` //关注该公众账号的总用户数 + Count int `json:"count"` //拉取的OPENID个数,最大值为10000 + Data struct { //列表数据,OPENID的列表 + Openid []string `json:"openid"` //用户唯一ID + } + NextOpenid string `json:"next_openid"` //拉取列表后一个用户的OPENID +}