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

Compare commits

..

7 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
10 changed files with 392 additions and 13 deletions

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

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

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