1
0
mirror of https://github.com/silenceper/wechat.git synced 2026-02-04 12:52:27 +08:00

Compare commits

..

10 Commits

Author SHA1 Message Date
silenceper
42b0966049 Revert "feat: 小程序/公众号增加 openApi 管理。 (#685)"
This reverts commit 31f8e24994.
2023-06-05 15:12:46 +08:00
sam
31f8e24994 feat: 小程序/公众号增加 openApi 管理。 (#685)
* feat: 小程序/公众号增加 openApi 管理。

* update: 修改字段名大小写。

* update: 修改字段名大小写。

* style: import 换行。

* update: openApi 管理放到内部包中。
2023-06-04 21:57:15 +08:00
sam
ae1b056515 feat: 公众号: 批量获取用户信息。 (#686)
* feat: 公众号: 批量获取用户信息。

* update: 公众号: 调整批量获取用户方法的输入参数。

* update: 公众号: 调整批量获取用户方法的输入参数。
2023-05-31 21:55:21 +08:00
jingyuexing
8bae546b77 Improve developer experience (#681)
* feat: 添加query 以及query单元测试
feat: 添加模板字符串解析以及模板字符串单元测试

* improve: 序列化请求参数,使得参数更易读

* delete: delete useless module

* format: format code

* docs: add function comment

* docs: comment method

* fix: fixed type convert error

* feat: support any type

* feat: support other type

* format: format code

* test: check logger

* format: format code

* test: udpate testing case

* del: remove useless code

* del: remove useless module

* test: update testing case

*  feat: support for unsigned integers

*  feat: template string support any type
2023-05-31 17:26:43 +08:00
markwang
9df943df69 企业微信userid与openid互换 (#676)
* 企业微信-客户联系-统计管理

* 企业微信-客户联系-统计管理

* 企业微信-客户联系-统计管理

* debug

* rollback

* debug

* debug

* 获取用户信息

* token

* json.Marshal错误输出

* debug

* bugfix

* 企业微信-通讯录管理相关接口

* 企业微信-通讯录管理

* 企业微信-通讯录管理

* 企业微信-通讯录管理

* 企业微信-[联系我]方式新增和查询

* 企业微信-[联系我]方式新增和获取

* 企业微信-[联系我]方式更新

* 企业微信-[联系我]方式列表、删除

* json.Marshal错误输出

* 已实现接口bug修改

* 历史接口bugfix

* 历史接口bugfix

* comment

* 企业微信:客户联系-消息推送;素材管理-上传图片

* fix

* 企业微信-获取群发记录列表

* 历史接口bugfix

* 1.企业微信-客户联系-消息推送-入群欢迎语素材管理
2.企业微信-通讯录管理-成员管理-获取成员ID列表

* golangci-lint

* gofmt

* 方法访问命名

* 企业微信-批量获取客户详情入参优化

* 企业微信-通讯录管理-标签管理-创建/更新/删除标签

* 请求地址常量无需导出

* 企业微信保持代码风格统一,接口URL不导出

* 企业微信-通讯录管理-标签管理-获取/增加/删除标签成员、获取标签列表

* feat:企业微信userid与openid互换

---------

Co-authored-by: wang.yu <wangyu@uniondrug.com>
Co-authored-by: markwang <www.wang61@qq.com>
2023-05-16 19:22:56 +08:00
just5325
0a37184c2f 新增应用推送消息模块,实现部分常用消息推送:SendText 发送文本消息、SendImage 发送图片消息、SendVoice 发送语音消息 企业微信应用推送消息接口:https://qyapi.weixin.qq.com/cgi-bin/appchat/send (#678)
* 修正work.GetMessage()方法的注释信息

(cherry picked from commit 3dfaf9222910ae4ad977e2852908692d04118abc)

* 新增应用推送消息模块,实现部分常用消息推送:SendText 发送文本消息、SendImage 发送图片消息、SendVoice 发送语音消息
企业微信应用推送消息接口:https://qyapi.weixin.qq.com/cgi-bin/appchat/send

(cherry picked from commit 654ea158d6590a006a8c78ac3c38eafe361fe303)
2023-05-16 19:22:18 +08:00
Sunny
b4f243ab13 Fix:err != nil return nil (#680) 2023-05-16 19:20:40 +08:00
markwang
d92cd35533 企业微信-通讯录管理-标签管理-获取/增加/删除标签成员、获取标签列表 (#674)
* 企业微信-客户联系-统计管理

* 企业微信-客户联系-统计管理

* 企业微信-客户联系-统计管理

* debug

* rollback

* debug

* debug

* 获取用户信息

* token

* json.Marshal错误输出

* debug

* bugfix

* 企业微信-通讯录管理相关接口

* 企业微信-通讯录管理

* 企业微信-通讯录管理

* 企业微信-通讯录管理

* 企业微信-[联系我]方式新增和查询

* 企业微信-[联系我]方式新增和获取

* 企业微信-[联系我]方式更新

* 企业微信-[联系我]方式列表、删除

* json.Marshal错误输出

* 已实现接口bug修改

* 历史接口bugfix

* 历史接口bugfix

* comment

* 企业微信:客户联系-消息推送;素材管理-上传图片

* fix

* 企业微信-获取群发记录列表

* 历史接口bugfix

* 1.企业微信-客户联系-消息推送-入群欢迎语素材管理
2.企业微信-通讯录管理-成员管理-获取成员ID列表

* golangci-lint

* gofmt

* 方法访问命名

* 企业微信-批量获取客户详情入参优化

* 企业微信-通讯录管理-标签管理-创建/更新/删除标签

* 请求地址常量无需导出

* 企业微信保持代码风格统一,接口URL不导出

* 企业微信-通讯录管理-标签管理-获取/增加/删除标签成员、获取标签列表

---------

Co-authored-by: wang.yu <wangyu@uniondrug.com>
Co-authored-by: markwang <www.wang61@qq.com>
2023-05-08 11:56:48 +08:00
Mark
58621cd79d fix CallbackMessage open_kfid and SyncMsg error (#672)
* 企业微信微信客服字段同步

* fix ci lint

* fix open_kfid xml Unmarshal error and  'syncMsg: field `open_kfid` unexpected empty string. invalid Request Parameter'

* add struct CallbackMessage xml tag

---------

Co-authored-by: liuyuezhong <liuyuezhong@inke.cn>
2023-04-19 16:21:04 +08:00
welllog
8821a3856d feat: 获取稳定版接口调用凭据 (#669)
* feat: 获取稳定版接口调用凭据

* Update default_access_token.go

* fix: format code

---------

Co-authored-by: ctr <orinfy@foxmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2023-04-18 11:54:34 +08:00
14 changed files with 631 additions and 30 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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")
}
}

View File

@@ -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
}

View File

@@ -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
View 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
View 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}
}

View File

@@ -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 获取回调事件中的消息内容

View File

@@ -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 获取消息查询响应内容

View File

@@ -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)
}