diff --git a/credential/default_access_token.go b/credential/default_access_token.go index 00ad481cf..ec88a89dc 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -101,10 +101,11 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access // 不强制更新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 + appID string + appSecret string + cacheKeyPrefix string + cache cache.Cache + accessTokenLock *sync.Mutex } // NewStableAccessToken new StableAccessToken @@ -113,10 +114,11 @@ func NewStableAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.C panic("cache is need") } return &StableAccessToken{ - appID: appID, - appSecret: appSecret, - cache: cache, - cacheKeyPrefix: cacheKeyPrefix, + appID: appID, + appSecret: appSecret, + cache: cache, + cacheKeyPrefix: cacheKeyPrefix, + accessTokenLock: new(sync.Mutex), } } @@ -130,7 +132,20 @@ func (ak *StableAccessToken) GetAccessTokenContext(ctx context.Context) (accessT // 先从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 + if accessToken = val.(string); accessToken != "" { + return + } + } + + // 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token + ak.accessTokenLock.Lock() + defer ak.accessTokenLock.Unlock() + + // 双检,防止重复从微信服务器获取 + if val := ak.cache.Get(accessTokenCacheKey); val != nil { + if accessToken = val.(string); accessToken != "" { + return + } } // cache失效,从微信服务器获取 diff --git a/miniprogram/config/config.go b/miniprogram/config/config.go index fb3e1518b..38cbd0365 100644 --- a/miniprogram/config/config.go +++ b/miniprogram/config/config.go @@ -14,4 +14,5 @@ type Config struct { Token string `json:"token"` // token EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey Cache cache.Cache + UseStableAK bool // use the stable access_token } diff --git a/miniprogram/miniprogram.go b/miniprogram/miniprogram.go index 8adbb00d7..c50273827 100644 --- a/miniprogram/miniprogram.go +++ b/miniprogram/miniprogram.go @@ -34,7 +34,13 @@ type MiniProgram struct { // NewMiniProgram 实例化小程序 API func NewMiniProgram(cfg *config.Config) *MiniProgram { - defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyMiniProgramPrefix, cfg.Cache) + var defaultAkHandle credential.AccessTokenContextHandle + const cacheKeyPrefix = credential.CacheKeyMiniProgramPrefix + if cfg.UseStableAK { + defaultAkHandle = credential.NewStableAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache) + } else { + defaultAkHandle = credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache) + } ctx := &context.Context{ Config: cfg, AccessTokenHandle: defaultAkHandle, diff --git a/miniprogram/subscribe/subscribe.go b/miniprogram/subscribe/subscribe.go index 9099044a1..5be3bcfcb 100644 --- a/miniprogram/subscribe/subscribe.go +++ b/miniprogram/subscribe/subscribe.go @@ -1,6 +1,7 @@ package subscribe import ( + "encoding/json" "fmt" "github.com/silenceper/wechat/v2/miniprogram/context" @@ -70,6 +71,13 @@ type TemplateList struct { Data []TemplateItem `json:"data"` } +// resTemplateSend 发送获取 msg id +type resTemplateSend struct { + util.CommonError + + MsgID int64 `json:"msgid"` +} + // Send 发送订阅消息 func (s *Subscribe) Send(msg *Message) (err error) { var accessToken string @@ -85,6 +93,33 @@ func (s *Subscribe) Send(msg *Message) (err error) { return util.DecodeWithCommonError(response, "Send") } +// SendGetMsgID 发送订阅消息返回 msgid +func (s *Subscribe) SendGetMsgID(msg *Message) (msgID int64, err error) { + var accessToken string + accessToken, err = s.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", subscribeSendURL, accessToken) + response, err := util.PostJSON(uri, msg) + if err != nil { + return + } + + var result resTemplateSend + if err = json.Unmarshal(response, &result); err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + + msgID = result.MsgID + + return +} + // ListTemplates 获取当前帐号下的个人模板列表 // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html func (s *Subscribe) ListTemplates() (*TemplateList, error) { diff --git a/officialaccount/config/config.go b/officialaccount/config/config.go index b38fccec9..409b2bd22 100644 --- a/officialaccount/config/config.go +++ b/officialaccount/config/config.go @@ -11,4 +11,5 @@ type Config struct { Token string `json:"token"` // token EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey Cache cache.Cache + UseStableAK bool // use the stable access_token } diff --git a/officialaccount/officialaccount.go b/officialaccount/officialaccount.go index 0c00817ba..fcad7bd1e 100644 --- a/officialaccount/officialaccount.go +++ b/officialaccount/officialaccount.go @@ -49,7 +49,13 @@ type OfficialAccount struct { // NewOfficialAccount 实例化公众号API func NewOfficialAccount(cfg *config.Config) *OfficialAccount { - defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyOfficialAccountPrefix, cfg.Cache) + var defaultAkHandle credential.AccessTokenContextHandle + const cacheKeyPrefix = credential.CacheKeyOfficialAccountPrefix + if cfg.UseStableAK { + defaultAkHandle = credential.NewStableAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache) + } else { + defaultAkHandle = credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache) + } ctx := &context.Context{ Config: cfg, AccessTokenHandle: defaultAkHandle, diff --git a/work/addresslist/department.go b/work/addresslist/department.go index b2feca0bf..5f65d660b 100644 --- a/work/addresslist/department.go +++ b/work/addresslist/department.go @@ -9,12 +9,16 @@ import ( const ( // departmentCreateURL 创建部门 departmentCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s" + // departmentUpdateURL 更新部门 + departmentUpdateURL = "https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=%s" + // departmentDeleteURL 删除部门 + departmentDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/department/delete?access_token=%s&id=%d" // departmentSimpleListURL 获取子部门ID列表 departmentSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=%s&id=%d" // departmentListURL 获取部门列表 departmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s" departmentListByIDURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%d" - // departmentGetURL 获取单个部门详情 https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=ACCESS_TOKEN&id=ID + // departmentGetURL 获取单个部门详情 departmentGetURL = "https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=%s&id=%d" ) @@ -85,6 +89,49 @@ func (r *Client) DepartmentCreate(req *DepartmentCreateRequest) (*DepartmentCrea return result, err } +// DepartmentUpdateRequest 更新部门请求 +type DepartmentUpdateRequest struct { + ID int `json:"id"` + Name string `json:"name,omitempty"` + NameEn string `json:"name_en,omitempty"` + ParentID int `json:"parentid,omitempty"` + Order int `json:"order,omitempty"` +} + +// DepartmentUpdate 更新部门 +// see https://developer.work.weixin.qq.com/document/path/90206 +func (r *Client) DepartmentUpdate(req *DepartmentUpdateRequest) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(departmentUpdateURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DepartmentUpdate") +} + +// DepartmentDelete 删除部门 +// @see https://developer.work.weixin.qq.com/document/path/90207 +func (r *Client) DepartmentDelete(departmentID int) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.HTTPGet(fmt.Sprintf(departmentDeleteURL, accessToken, departmentID)); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DepartmentDelete") +} + // DepartmentSimpleList 获取子部门ID列表 // see https://developer.work.weixin.qq.com/document/path/95350 func (r *Client) DepartmentSimpleList(departmentID int) ([]*DepartmentID, error) { diff --git a/work/addresslist/user.go b/work/addresslist/user.go index 61bb14612..b8092f7f7 100644 --- a/work/addresslist/user.go +++ b/work/addresslist/user.go @@ -12,6 +12,8 @@ const ( userSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist" // userCreateURL 创建成员 userCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s" + // userUpdateURL 更新成员 + userUpdateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/update?access_token=%s" // userGetURL 读取成员 userGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get" // userDeleteURL 删除成员 @@ -154,6 +156,51 @@ func (r *Client) UserCreate(req *UserCreateRequest) (*UserCreateResponse, error) return result, err } +// UserUpdateRequest 更新成员请求 +type UserUpdateRequest struct { + UserID string `json:"userid"` + NewUserID string `json:"new_userid"` + Name string `json:"name"` + Alias string `json:"alias"` + Mobile string `json:"mobile"` + Department []int `json:"department"` + Order []int `json:"order"` + Position string `json:"position"` + Gender int `json:"gender"` + Email string `json:"email"` + BizMail string `json:"biz_mail"` + IsLeaderInDept []int `json:"is_leader_in_dept"` + DirectLeader []string `json:"direct_leader"` + Enable int `json:"enable"` + AvatarMediaid string `json:"avatar_mediaid"` + Telephone string `json:"telephone"` + Address string `json:"address"` + MainDepartment int `json:"main_department"` + Extattr struct { + Attrs []ExtraAttr `json:"attrs"` + } `json:"extattr"` + ToInvite bool `json:"to_invite"` + ExternalPosition string `json:"external_position"` + ExternalProfile ExternalProfile `json:"external_profile"` +} + +// UserUpdate 更新成员 +// see https://developer.work.weixin.qq.com/document/path/90197 +func (r *Client) UserUpdate(req *UserUpdateRequest) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(userUpdateURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "UserUpdate") +} + // UserGetResponse 获取部门成员响应 type UserGetResponse struct { util.CommonError diff --git a/work/kf/servicer.go b/work/kf/servicer.go index a620f1417..8e64c3eaa 100644 --- a/work/kf/servicer.go +++ b/work/kf/servicer.go @@ -34,6 +34,7 @@ type ReceptionistSchema struct { } // ReceptionistAdd 添加接待人员 +// @see https://developer.work.weixin.qq.com/document/path/94646 func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) { var ( accessToken string @@ -51,10 +52,11 @@ func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info Receptionist if info.ErrCode != 0 { return info, NewSDKErr(info.ErrCode, info.ErrMsg) } - return info, nil + return } // ReceptionistDel 删除接待人员 +// @see https://developer.work.weixin.qq.com/document/path/94647 func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) { var ( accessToken string @@ -74,7 +76,7 @@ func (r *Client) ReceptionistDel(options ReceptionistOptions) (info Receptionist if info.ErrCode != 0 { return info, NewSDKErr(info.ErrCode, info.ErrMsg) } - return info, nil + return } // ReceptionistListSchema 获取接待人员列表响应内容 @@ -84,10 +86,12 @@ type ReceptionistListSchema struct { UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid,即open_userid Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取 DepartmentID int `json:"department_id"` // 接待人员部门的id + StopType int `json:"stop_type"` // 接待人员的接待状态为「停止接待」的子类型。0:停止接待,1:暂时挂起 } `json:"servicer_list"` } // ReceptionistList 获取接待人员列表 +// @see https://developer.work.weixin.qq.com/document/path/94645 func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) { var ( accessToken string @@ -107,5 +111,5 @@ func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err if info.ErrCode != 0 { return info, NewSDKErr(info.ErrCode, info.ErrMsg) } - return info, nil + return } diff --git a/work/material/media.go b/work/material/media.go index b9647adc3..551eeee23 100644 --- a/work/material/media.go +++ b/work/material/media.go @@ -14,6 +14,8 @@ const ( uploadTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" // uploadAttachment 上传附件资源 uploadAttachment = "https://qyapi.weixin.qq.com/cgi-bin/media/upload_attachment?access_token=%s&media_type=%s&attachment_type=%d" + // getTempFile 获取临时素材 + getTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s" ) // UploadImgResponse 上传图片响应 @@ -57,6 +59,30 @@ func (r *Client) UploadImg(filename string) (*UploadImgResponse, error) { return result, err } +// UploadImgFromReader 从 io.Reader 上传图片 +// @see https://developer.work.weixin.qq.com/document/path/90256 +func (r *Client) UploadImgFromReader(filename string, reader io.Reader) (*UploadImgResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var byteData []byte + byteData, err = io.ReadAll(reader) + if err != nil { + return nil, err + } + var response []byte + if response, err = util.PostFileByStream("media", filename, fmt.Sprintf(uploadImgURL, accessToken), byteData); err != nil { + return nil, err + } + result := &UploadImgResponse{} + err = util.DecodeWithError(response, result, "UploadImg") + return result, err +} + // UploadTempFile 上传临时素材 // @see https://developer.work.weixin.qq.com/document/path/90253 // @mediaType 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file) @@ -148,3 +174,29 @@ func (r *Client) UploadAttachmentFromReader(filename, mediaType string, reader i err = util.DecodeWithError(response, result, "UploadAttachment") return result, err } + +// GetTempFile 获取临时素材 +// @see https://developer.work.weixin.qq.com/document/path/90254 +func (r *Client) GetTempFile(mediaID string) ([]byte, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + url := fmt.Sprintf(getTempFile, accessToken, mediaID) + response, err := util.HTTPGet(url) + if err != nil { + return nil, err + } + + // 检查响应是否为错误信息 + err = util.DecodeWithCommonError(response, "GetTempFile") + if err != nil { + return nil, err + } + + // 如果不是错误响应,则返回原始数据 + return response, nil +}