mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 12:52:27 +08:00
Compare commits
10 Commits
v2.1.5-rc.
...
revert-685
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b0966049 | ||
|
|
31f8e24994 | ||
|
|
ae1b056515 | ||
|
|
8bae546b77 | ||
|
|
9df943df69 | ||
|
|
0a37184c2f | ||
|
|
b4f243ab13 | ||
|
|
d92cd35533 | ||
|
|
58621cd79d | ||
|
|
8821a3856d |
@@ -12,9 +12,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// AccessTokenURL 获取access_token的接口
|
||||
// accessTokenURL 获取access_token的接口
|
||||
accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"
|
||||
// AccessTokenURL 企业微信获取access_token的接口
|
||||
// stableAccessTokenURL 获取稳定版access_token的接口
|
||||
stableAccessTokenURL = "https://api.weixin.qq.com/cgi-bin/stable_token"
|
||||
// workAccessTokenURL 企业微信获取access_token的接口
|
||||
workAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
|
||||
// CacheKeyOfficialAccountPrefix 微信公众号cache key前缀
|
||||
CacheKeyOfficialAccountPrefix = "gowechat_officialaccount_"
|
||||
@@ -79,17 +81,87 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access
|
||||
|
||||
// cache失效,从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = GetTokenFromServerContext(ctx, fmt.Sprintf(accessTokenURL, ak.appID, ak.appSecret))
|
||||
if resAccessToken, err = GetTokenFromServerContext(ctx, fmt.Sprintf(accessTokenURL, ak.appID, ak.appSecret)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(resAccessToken.ExpiresIn-1500)*time.Second); err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
// StableAccessToken 获取稳定版接口调用凭据(与getAccessToken获取的调用凭证完全隔离,互不影响)
|
||||
// 不强制更新access_token,可用于不同环境不同服务而不需要分布式锁以及公用缓存,避免access_token争抢
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
|
||||
type StableAccessToken struct {
|
||||
appID string
|
||||
appSecret string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
// NewStableAccessToken new StableAccessToken
|
||||
func NewStableAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle {
|
||||
if cache == nil {
|
||||
panic("cache is need")
|
||||
}
|
||||
return &StableAccessToken{
|
||||
appID: appID,
|
||||
appSecret: appSecret,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取
|
||||
func (ak *StableAccessToken) GetAccessToken() (accessToken string, err error) {
|
||||
return ak.GetAccessTokenContext(context.Background())
|
||||
}
|
||||
|
||||
// GetAccessTokenContext 获取access_token,先从cache中获取,没有则从服务端获取
|
||||
func (ak *StableAccessToken) GetAccessTokenContext(ctx context.Context) (accessToken string, err error) {
|
||||
// 先从cache中取
|
||||
accessTokenCacheKey := fmt.Sprintf("%s_stable_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
||||
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
// cache失效,从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = ak.GetAccessTokenDirectly(ctx, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expires := resAccessToken.ExpiresIn - 1500
|
||||
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
expires := resAccessToken.ExpiresIn - 300
|
||||
_ = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
// GetAccessTokenDirectly 从微信获取access_token
|
||||
func (ak *StableAccessToken) GetAccessTokenDirectly(ctx context.Context, forceRefresh bool) (resAccessToken ResAccessToken, err error) {
|
||||
b, err := util.PostJSONContext(ctx, stableAccessTokenURL, map[string]interface{}{
|
||||
"grant_type": "client_credential",
|
||||
"appid": ak.appID,
|
||||
"secret": ak.appSecret,
|
||||
"force_refresh": forceRefresh,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = resAccessToken.AccessToken
|
||||
|
||||
if err = json.Unmarshal(b, &resAccessToken); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resAccessToken.ErrCode != 0 {
|
||||
err = fmt.Errorf("get stable access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package user
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
@@ -10,9 +11,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN"
|
||||
updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s"
|
||||
userListURL = "https://api.weixin.qq.com/cgi-bin/user/get"
|
||||
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN"
|
||||
userInfoBatchURL = "https://api.weixin.qq.com/cgi-bin/user/info/batchget"
|
||||
updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s"
|
||||
userListURL = "https://api.weixin.qq.com/cgi-bin/user/get"
|
||||
)
|
||||
|
||||
// User 用户管理
|
||||
@@ -30,7 +32,11 @@ func NewUser(context *context.Context) *User {
|
||||
// Info 用户基本信息
|
||||
type Info struct {
|
||||
util.CommonError
|
||||
userInfo
|
||||
}
|
||||
|
||||
// 用户基本信息
|
||||
type userInfo struct {
|
||||
Subscribe int32 `json:"subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
@@ -88,6 +94,48 @@ func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// BatchGetUserInfoParams 批量获取用户基本信息参数
|
||||
type BatchGetUserInfoParams struct {
|
||||
UserList []BatchGetUserListItem `json:"user_list"` // 需要批量获取基本信息的用户列表
|
||||
}
|
||||
|
||||
// BatchGetUserListItem 需要获取基本信息的用户
|
||||
type BatchGetUserListItem struct {
|
||||
OpenID string `json:"openid"` // 用户的标识,对当前公众号唯一
|
||||
Lang string `json:"lang"` // 国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为zh-CN
|
||||
}
|
||||
|
||||
// InfoList 用户基本信息列表
|
||||
type InfoList struct {
|
||||
util.CommonError
|
||||
UserInfoList []userInfo `json:"user_info_list"`
|
||||
}
|
||||
|
||||
// BatchGetUserInfo 批量获取用户基本信息
|
||||
func (user *User) BatchGetUserInfo(params BatchGetUserInfoParams) (*InfoList, error) {
|
||||
if len(params.UserList) > 100 {
|
||||
return nil, errors.New("params length must be less than or equal to 100")
|
||||
}
|
||||
|
||||
ak, err := user.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", userInfoBatchURL, ak)
|
||||
res, err := util.PostJSON(uri, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data InfoList
|
||||
err = util.DecodeWithError(res, &data, "BatchGetUserInfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// UpdateRemark 设置用户备注名
|
||||
func (user *User) UpdateRemark(openID, remark string) (err error) {
|
||||
var accessToken string
|
||||
|
||||
@@ -36,7 +36,7 @@ type RegisterMiniProgramParam struct {
|
||||
func (component *Component) RegisterMiniProgram(param *RegisterMiniProgramParam) error {
|
||||
componentAK, err := component.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf(fastregisterweappURL+"?action=create&component_access_token=%s", componentAK)
|
||||
data, err := util.PostJSON(url, param)
|
||||
@@ -58,7 +58,7 @@ type GetRegistrationStatusParam struct {
|
||||
func (component *Component) GetRegistrationStatus(param *GetRegistrationStatusParam) error {
|
||||
componentAK, err := component.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf(fastregisterweappURL+"?action=search&component_access_token=%s", componentAK)
|
||||
data, err := util.PostJSON(url, param)
|
||||
|
||||
24
util/query.go
Normal file
24
util/query.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Query 将Map序列化为Query参数
|
||||
func Query(params map[string]interface{}) string {
|
||||
finalString := make([]string, 0)
|
||||
for key, value := range params {
|
||||
valueString := ""
|
||||
switch v := value.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
valueString = fmt.Sprintf("%d", v)
|
||||
case bool:
|
||||
valueString = fmt.Sprintf("%v", v)
|
||||
default:
|
||||
valueString = fmt.Sprintf("%s", v)
|
||||
}
|
||||
finalString = append(finalString, strings.Join([]string{key, valueString}, "="))
|
||||
}
|
||||
return strings.Join(finalString, "&")
|
||||
}
|
||||
19
util/query_test.go
Normal file
19
util/query_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestQuery query method test case
|
||||
func TestQuery(t *testing.T) {
|
||||
result := Query(map[string]interface{}{
|
||||
"age": 12,
|
||||
"name": "Alan",
|
||||
"cat": "Peter",
|
||||
})
|
||||
if result == "" {
|
||||
// 由于hash是乱序 所以没法很好的预测输出的字符串
|
||||
// 将会输出符合Query规则的字符串 "age=12&name=Alan&cat=Peter"
|
||||
t.Error("NOT PASS")
|
||||
}
|
||||
}
|
||||
24
util/template.go
Normal file
24
util/template.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Template 对字符串中的和map的key相同的字符串进行模板替换 仅支持 形如: {name}
|
||||
func Template(source string, data map[string]interface{}) string {
|
||||
sourceCopy := &source
|
||||
for k, val := range data {
|
||||
valStr := ""
|
||||
switch v := val.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
valStr = fmt.Sprintf("%d", v)
|
||||
case bool:
|
||||
valStr = fmt.Sprintf("%v", v)
|
||||
default:
|
||||
valStr = fmt.Sprintf("%s", v)
|
||||
}
|
||||
*sourceCopy = strings.Replace(*sourceCopy, strings.Join([]string{"{", k, "}"}, ""), valStr, 1)
|
||||
}
|
||||
return *sourceCopy
|
||||
}
|
||||
20
util/template_test.go
Normal file
20
util/template_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestTemplate testing case about Template method
|
||||
func TestTemplate(t *testing.T) {
|
||||
result := Template("{name}={age};{with}={another};any={any};boolean={boolean}", map[string]interface{}{
|
||||
"name": "Helan",
|
||||
"age": "33",
|
||||
"with": "Pep",
|
||||
"another": "C",
|
||||
"any": 33,
|
||||
"boolean": false,
|
||||
})
|
||||
if result != "Helan=33;Pep=C;any=33;boolean=false" {
|
||||
t.Error("NOT PSS testing")
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ const (
|
||||
updateTagURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/update?access_token=%s"
|
||||
// deleteTagURL 删除标签
|
||||
deleteTagURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?access_token=%s&tagid=%d"
|
||||
// getTagURL 获取标签成员
|
||||
getTagURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?access_token=%s&tagid=%d"
|
||||
// addTagUsersURL 增加标签成员
|
||||
addTagUsersURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers?access_token=%s"
|
||||
// delTagUsersURL 删除标签成员
|
||||
delTagUsersURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers?access_token=%s"
|
||||
// listTagURL 获取标签列表
|
||||
listTagURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/list?access_token=%s"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -90,3 +98,145 @@ func (r *Client) DeleteTag(tagID int) error {
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DeleteTag")
|
||||
}
|
||||
|
||||
type (
|
||||
// GetTagResponse 获取标签成员响应
|
||||
GetTagResponse struct {
|
||||
util.CommonError
|
||||
TagName string `json:"tagname"`
|
||||
UserList []GetTagUserList `json:"userlist"`
|
||||
PartyList []int `json:"partylist"`
|
||||
}
|
||||
// GetTagUserList 标签中包含的成员列表
|
||||
GetTagUserList struct {
|
||||
UserID string `json:"userid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetTag 获取标签成员
|
||||
// @see https://developer.work.weixin.qq.com/document/path/90213
|
||||
func (r *Client) GetTag(tagID int) (*GetTagResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getTagURL, accessToken, tagID)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetTagResponse{}
|
||||
if err = util.DecodeWithError(response, result, "GetTag"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// AddTagUsersRequest 增加标签成员请求
|
||||
AddTagUsersRequest struct {
|
||||
TagID int `json:"tagid"`
|
||||
UserList []string `json:"userlist"`
|
||||
PartyList []int `json:"partylist"`
|
||||
}
|
||||
// AddTagUsersResponse 增加标签成员响应
|
||||
AddTagUsersResponse struct {
|
||||
util.CommonError
|
||||
InvalidList string `json:"invalidlist"`
|
||||
InvalidParty []int `json:"invalidparty"`
|
||||
}
|
||||
)
|
||||
|
||||
// AddTagUsers 增加标签成员
|
||||
// see https://developer.work.weixin.qq.com/document/path/90214
|
||||
func (r *Client) AddTagUsers(req *AddTagUsersRequest) (*AddTagUsersResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(addTagUsersURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &AddTagUsersResponse{}
|
||||
if err = util.DecodeWithError(response, result, "AddTagUsers"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// DelTagUsersRequest 删除标签成员请求
|
||||
DelTagUsersRequest struct {
|
||||
TagID int `json:"tagid"`
|
||||
UserList []string `json:"userlist"`
|
||||
PartyList []int `json:"partylist"`
|
||||
}
|
||||
// DelTagUsersResponse 删除标签成员响应
|
||||
DelTagUsersResponse struct {
|
||||
util.CommonError
|
||||
InvalidList string `json:"invalidlist"`
|
||||
InvalidParty []int `json:"invalidparty"`
|
||||
}
|
||||
)
|
||||
|
||||
// DelTagUsers 删除标签成员
|
||||
// see https://developer.work.weixin.qq.com/document/path/90215
|
||||
func (r *Client) DelTagUsers(req *DelTagUsersRequest) (*DelTagUsersResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(delTagUsersURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &DelTagUsersResponse{}
|
||||
if err = util.DecodeWithError(response, result, "DelTagUsers"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// ListTagResponse 获取标签列表响应
|
||||
ListTagResponse struct {
|
||||
util.CommonError
|
||||
TagList []Tag `json:"taglist"`
|
||||
}
|
||||
// Tag 标签
|
||||
Tag struct {
|
||||
TagID int `json:"tagid"`
|
||||
TagName string `json:"tagname"`
|
||||
}
|
||||
)
|
||||
|
||||
// ListTag 获取标签列表
|
||||
// @see https://developer.work.weixin.qq.com/document/path/90216
|
||||
func (r *Client) ListTag() (*ListTagResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(listTagURL, accessToken)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ListTagResponse{}
|
||||
if err = util.DecodeWithError(response, result, "ListTag"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package addresslist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// userSimpleListURL 获取部门成员
|
||||
userSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%d"
|
||||
userSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist"
|
||||
// userGetURL 读取成员
|
||||
userGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s"
|
||||
userGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
||||
// userListIDURL 获取成员ID列表
|
||||
userListIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token=%s"
|
||||
userListIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id"
|
||||
// convertToOpenIDURL userID转openID
|
||||
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
||||
// convertToUserIDURL openID转userID
|
||||
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -41,7 +45,13 @@ func (r *Client) UserSimpleList(departmentID int) ([]*UserList, error) {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(userSimpleListURL, accessToken, departmentID)); err != nil {
|
||||
if response, err = util.HTTPGet(strings.Join([]string{
|
||||
userSimpleListURL,
|
||||
util.Query(map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"department_id": departmentID,
|
||||
}),
|
||||
}, "?")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &UserSimpleListResponse{}
|
||||
@@ -125,7 +135,15 @@ func (r *Client) UserGet(UserID string) (*UserGetResponse, error) {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(userGetURL, accessToken, UserID)); err != nil {
|
||||
|
||||
if response, err = util.HTTPGet(
|
||||
strings.Join([]string{
|
||||
userGetURL,
|
||||
util.Query(map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"department_id": UserID,
|
||||
}),
|
||||
}, "?")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &UserGetResponse{}
|
||||
@@ -166,7 +184,12 @@ func (r *Client) UserListID(req *UserListIDRequest) (*UserListIDResponse, error)
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(userListIDURL, accessToken), req); err != nil {
|
||||
if response, err = util.PostJSON(strings.Join([]string{
|
||||
userListIDURL,
|
||||
util.Query(map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
}),
|
||||
}, "?"), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &UserListIDResponse{}
|
||||
@@ -175,3 +198,87 @@ func (r *Client) UserListID(req *UserListIDRequest) (*UserListIDResponse, error)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// convertToOpenIDRequest userID转openID请求
|
||||
convertToOpenIDRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
}
|
||||
|
||||
// convertToOpenIDResponse userID转openID响应
|
||||
convertToOpenIDResponse struct {
|
||||
util.CommonError
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
)
|
||||
|
||||
// ConvertToOpenID userID转openID
|
||||
// see https://developer.work.weixin.qq.com/document/path/90202
|
||||
func (r *Client) ConvertToOpenID(userID string) (string, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var response []byte
|
||||
|
||||
if response, err = util.PostJSON(strings.Join([]string{
|
||||
convertToOpenIDURL,
|
||||
util.Query(map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
}),
|
||||
}, "?"), &convertToOpenIDRequest{
|
||||
UserID: userID,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := &convertToOpenIDResponse{}
|
||||
if err = util.DecodeWithError(response, result, "ConvertToOpenID"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.OpenID, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// convertToUserIDRequest openID转userID请求
|
||||
convertToUserIDRequest struct {
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
|
||||
// convertToUserIDResponse openID转userID响应
|
||||
convertToUserIDResponse struct {
|
||||
util.CommonError
|
||||
UserID string `json:"userid"`
|
||||
}
|
||||
)
|
||||
|
||||
// ConvertToUserID openID转userID
|
||||
// see https://developer.work.weixin.qq.com/document/path/90202
|
||||
func (r *Client) ConvertToUserID(openID string) (string, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var response []byte
|
||||
|
||||
if response, err = util.PostJSON(strings.Join([]string{
|
||||
convertToUserIDURL,
|
||||
util.Query(map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
}),
|
||||
}, "?"), &convertToUserIDRequest{
|
||||
OpenID: openID,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := &convertToUserIDResponse{}
|
||||
if err = util.DecodeWithError(response, result, "ConvertToUserID"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.UserID, nil
|
||||
}
|
||||
|
||||
115
work/appchat/appchat.go
Normal file
115
work/appchat/appchat.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package appchat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// 应用推送消息接口地址
|
||||
sendURL = "https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=%s"
|
||||
)
|
||||
|
||||
type (
|
||||
// SendRequestCommon 发送应用推送消息请求公共参数
|
||||
SendRequestCommon struct {
|
||||
// 群聊id
|
||||
ChatID string `json:"chatid"`
|
||||
// 消息类型
|
||||
MsgType string `json:"msgtype"`
|
||||
// 表示是否是保密消息,0表示否,1表示是,默认0
|
||||
Safe int `json:"safe"`
|
||||
}
|
||||
|
||||
// SendResponse 发送应用消息响应参数
|
||||
SendResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// SendTextRequest 发送文本消息的请求
|
||||
SendTextRequest struct {
|
||||
*SendRequestCommon
|
||||
Text TextField `json:"text"`
|
||||
}
|
||||
// TextField 文本消息参数
|
||||
TextField struct {
|
||||
// 消息内容,最长不超过2048个字节
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// SendImageRequest 发送图片消息的请求
|
||||
SendImageRequest struct {
|
||||
*SendRequestCommon
|
||||
Image ImageField `json:"image"`
|
||||
}
|
||||
// ImageField 图片消息参数
|
||||
ImageField struct {
|
||||
// 图片媒体文件id,可以调用上传临时素材接口获取
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
// SendVoiceRequest 发送语音消息的请求
|
||||
SendVoiceRequest struct {
|
||||
*SendRequestCommon
|
||||
Voice VoiceField `json:"voice"`
|
||||
}
|
||||
// VoiceField 语音消息参数
|
||||
VoiceField struct {
|
||||
// 语音文件id,可以调用上传临时素材接口获取
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
)
|
||||
|
||||
// Send 发送应用消息
|
||||
// @desc 实现企业微信发送应用消息接口:https://developer.work.weixin.qq.com/document/path/90248
|
||||
func (r *Client) Send(apiName string, request interface{}) (*SendResponse, error) {
|
||||
// 获取accessToken
|
||||
accessToken, err := r.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 请求参数转 JSON 格式
|
||||
jsonData, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 发起http请求
|
||||
response, err := util.HTTPPost(fmt.Sprintf(sendURL, accessToken), string(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 按照结构体解析返回值
|
||||
result := &SendResponse{}
|
||||
if err = util.DecodeWithError(response, result, apiName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 返回数据
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SendText 发送文本消息
|
||||
func (r *Client) SendText(request SendTextRequest) (*SendResponse, error) {
|
||||
// 发送文本消息MsgType参数固定为:text
|
||||
request.MsgType = "text"
|
||||
return r.Send("MessageSendText", request)
|
||||
}
|
||||
|
||||
// SendImage 发送图片消息
|
||||
func (r *Client) SendImage(request SendImageRequest) (*SendResponse, error) {
|
||||
// 发送图片消息MsgType参数固定为:image
|
||||
request.MsgType = "image"
|
||||
return r.Send("MessageSendImage", request)
|
||||
}
|
||||
|
||||
// SendVoice 发送语音消息
|
||||
func (r *Client) SendVoice(request SendVoiceRequest) (*SendResponse, error) {
|
||||
// 发送语音消息MsgType参数固定为:voice
|
||||
request.MsgType = "voice"
|
||||
return r.Send("MessageSendVoice", request)
|
||||
}
|
||||
|
||||
// 以上实现了部分常用消息推送:SendText 发送文本消息、SendImage 发送图片消息、SendVoice 发送语音消息,
|
||||
// 如需扩展其他消息类型,建议按照以上格式,扩展对应消息类型的参数即可
|
||||
// 也可以直接使用Send方法,按照企业微信消息推送的接口文档传对应消息类型的参数来使用
|
||||
16
work/appchat/client.go
Normal file
16
work/appchat/client.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Package appchat 应用发送消息到群聊会话,企业微信接口:https://developer.work.weixin.qq.com/document/path/90248
|
||||
package appchat
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/work/context"
|
||||
)
|
||||
|
||||
// Client 接口实例
|
||||
type Client struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewClient 初始化实例
|
||||
func NewClient(ctx *context.Context) *Client {
|
||||
return &Client{ctx}
|
||||
}
|
||||
@@ -52,12 +52,12 @@ type callbackOriginMessage struct {
|
||||
|
||||
// CallbackMessage 微信客服回调消息
|
||||
type CallbackMessage struct {
|
||||
ToUserName string `json:"to_user_name"` // 微信客服组件ID
|
||||
CreateTime int `json:"create_time"` // 消息创建时间,unix时间戳
|
||||
MsgType string `json:"msgtype"` // 消息的类型,此时固定为 event
|
||||
Event string `json:"event"` // 事件的类型,此时固定为 kf_msg_or_event
|
||||
Token string `json:"token"` // 调用拉取消息接口时,需要传此token,用于校验请求的合法性
|
||||
OpenKfID string `json:"open_kfid"` // 有新消息的客服帐号。可通过sync_msg接口指定open_kfid获取此客服帐号的消息
|
||||
ToUserName string `json:"to_user_name" xml:"ToUserName"` // 微信客服组件ID
|
||||
CreateTime int `json:"create_time" xml:"CreateTime"` // 消息创建时间,unix时间戳
|
||||
MsgType string `json:"msgtype" xml:"MsgType"` // 消息的类型,此时固定为 event
|
||||
Event string `json:"event" xml:"Event"` // 事件的类型,此时固定为 kf_msg_or_event
|
||||
Token string `json:"token" xml:"Token"` // 调用拉取消息接口时,需要传此token,用于校验请求的合法性
|
||||
OpenKfID string `json:"open_kfid" xml:"OpenKfId"` // 有新消息的客服帐号。可通过sync_msg接口指定open_kfid获取此客服帐号的消息
|
||||
}
|
||||
|
||||
// GetCallbackMessage 获取回调事件中的消息内容
|
||||
|
||||
@@ -16,11 +16,11 @@ const (
|
||||
|
||||
// SyncMsgOptions 获取消息查询参数
|
||||
type SyncMsgOptions struct {
|
||||
Cursor string `json:"cursor"` // 上一次调用时返回的next_cursor,第一次拉取可以不填, 不多于64字节
|
||||
Token string `json:"token"` // 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制, 不多于128字节
|
||||
Limit uint `json:"limit"` // 期望请求的数据量,默认值和最大值都为1000, 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。
|
||||
VoiceFormat uint `json:"voice_format"` // 语音消息类型,0-Amr 1-Silk,默认0。可通过该参数控制返回的语音格式,开发者可按需选择自己程序支持的一种格式
|
||||
OpenKfID string `json:"open_kfid"` // 指定拉取某个客服帐号的消息,否则默认返回有权限的客服帐号的消息。当客服帐号较多,建议按open_kfid来拉取以获取更好的性能。
|
||||
Cursor string `json:"cursor"` // 上一次调用时返回的next_cursor,第一次拉取可以不填, 不多于64字节
|
||||
Token string `json:"token"` // 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制, 不多于128字节
|
||||
Limit uint `json:"limit"` // 期望请求的数据量,默认值和最大值都为1000, 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。
|
||||
VoiceFormat uint `json:"voice_format,omitempty"` // 语音消息类型,0-Amr 1-Silk,默认0。可通过该参数控制返回的语音格式,开发者可按需选择自己程序支持的一种格式
|
||||
OpenKfID string `json:"open_kfid,omitempty"` // 指定拉取某个客服帐号的消息,否则默认返回有权限的客服帐号的消息。当客服帐号较多,建议按open_kfid来拉取以获取更好的性能。
|
||||
}
|
||||
|
||||
// SyncMsgSchema 获取消息查询响应内容
|
||||
|
||||
@@ -3,6 +3,7 @@ package work
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/work/addresslist"
|
||||
"github.com/silenceper/wechat/v2/work/appchat"
|
||||
"github.com/silenceper/wechat/v2/work/config"
|
||||
"github.com/silenceper/wechat/v2/work/context"
|
||||
"github.com/silenceper/wechat/v2/work/externalcontact"
|
||||
@@ -69,7 +70,12 @@ func (wk *Work) GetRobot() *robot.Client {
|
||||
return robot.NewClient(wk.ctx)
|
||||
}
|
||||
|
||||
// GetMessage get robot
|
||||
// GetMessage 获取发送应用消息接口实例
|
||||
func (wk *Work) GetMessage() *message.Client {
|
||||
return message.NewClient(wk.ctx)
|
||||
}
|
||||
|
||||
// GetAppChat 获取应用发送消息到群聊会话接口实例
|
||||
func (wk *Work) GetAppChat() *appchat.Client {
|
||||
return appchat.NewClient(wk.ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user