1
0
mirror of https://github.com/silenceper/wechat.git synced 2026-02-15 10:12:28 +08:00

Compare commits

..

11 Commits

Author SHA1 Message Date
yangyl12345
e61b441be5 Merge a15ccac41e into 3fbe8634d9 2024-11-26 02:56:09 +00:00
w_yangyili
a15ccac41e 微信稳定性获取 access_token 接口,添加防止并发性获取 access_token 和多次微信服务获取代码 2024-11-26 10:55:56 +08:00
yangyl12345
b6762f11ef Update default_access_token.go
微信获取稳定版token,只有不等于空字符串的情况下才会返回access_token信息,未空的情况,继续调去微信服务
2024-11-25 22:18:24 +08:00
CyJaySong
3fbe8634d9 officeaccount 和 miniprogram 添加UseStableAK 配置项 (#788) 2024-11-23 18:25:24 +08:00
markwang
990ba6ede9 feat: 企业微信-接待人员管理增加部门ID及停止接待子类型 (#800) 2024-10-16 16:11:20 +08:00
曹晶
44b09c7c3b feat(media): add getTempFile api (#801)
add getTempFile api
2024-10-16 16:11:03 +08:00
Lien Li
2e0708845b add-show_splash_ad-for-qrcodeparam (#794) 2024-08-26 10:20:39 +08:00
oah8
c1770130a0 add stream_upload_source (#786)
Co-authored-by: _oah <57302072@qq.com>
2024-08-22 14:21:58 +08:00
一叶知秋
c22a036b7f fix: 增加一个用户信息接口 (#793) 2024-08-22 12:08:44 +08:00
Ji Luo Yang
05ac7148d4 feat: 公众号新增模板接口,支持模板参数传递 (#783)
* feat: 公众号新增模板接口,支持模板参数传递

* fix: 公众号新增模板接口,合并定义参数

* fix: 公众号新增模板接口,统一代码风格

---------

Co-authored-by: yangj23 <yangj23@mingyuanyun.com>
2024-07-16 21:02:59 +08:00
sam
6b3532cc2d feat: Material 增加 AddMaterialFromReader 与 AddVideoFromReader 方法 (#780)
* feat: Material 增加 AddMaterialFromReader 与 AddVideoFromReader 方法

* update: 调整 PostFileFromReader 参数。
2024-07-16 15:43:18 +08:00
13 changed files with 284 additions and 72 deletions

2
cache/redis.go vendored
View File

@@ -16,6 +16,7 @@ type Redis struct {
// RedisOpts redis 连接属性 // RedisOpts redis 连接属性
type RedisOpts struct { type RedisOpts struct {
Host string `yml:"host" json:"host"` Host string `yml:"host" json:"host"`
Username string `yaml:"username" json:"username"`
Password string `yml:"password" json:"password"` Password string `yml:"password" json:"password"`
Database int `yml:"database" json:"database"` Database int `yml:"database" json:"database"`
MaxIdle int `yml:"max_idle" json:"max_idle"` MaxIdle int `yml:"max_idle" json:"max_idle"`
@@ -28,6 +29,7 @@ func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
conn := redis.NewUniversalClient(&redis.UniversalOptions{ conn := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{opts.Host}, Addrs: []string{opts.Host},
DB: opts.Database, DB: opts.Database,
Username: opts.Username,
Password: opts.Password, Password: opts.Password,
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout), IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
MinIdleConns: opts.MaxIdle, MinIdleConns: opts.MaxIdle,

View File

@@ -101,10 +101,11 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access
// 不强制更新access_token,可用于不同环境不同服务而不需要分布式锁以及公用缓存避免access_token争抢 // 不强制更新access_token,可用于不同环境不同服务而不需要分布式锁以及公用缓存避免access_token争抢
// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html // https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
type StableAccessToken struct { type StableAccessToken struct {
appID string appID string
appSecret string appSecret string
cacheKeyPrefix string cacheKeyPrefix string
cache cache.Cache cache cache.Cache
accessTokenLock *sync.Mutex
} }
// NewStableAccessToken new StableAccessToken // NewStableAccessToken new StableAccessToken
@@ -113,10 +114,11 @@ func NewStableAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.C
panic("cache is need") panic("cache is need")
} }
return &StableAccessToken{ return &StableAccessToken{
appID: appID, appID: appID,
appSecret: appSecret, appSecret: appSecret,
cache: cache, cache: cache,
cacheKeyPrefix: cacheKeyPrefix, cacheKeyPrefix: cacheKeyPrefix,
accessTokenLock: new(sync.Mutex),
} }
} }
@@ -130,7 +132,20 @@ func (ak *StableAccessToken) GetAccessTokenContext(ctx context.Context) (accessT
// 先从cache中取 // 先从cache中取
accessTokenCacheKey := fmt.Sprintf("%s_stable_access_token_%s", ak.cacheKeyPrefix, ak.appID) accessTokenCacheKey := fmt.Sprintf("%s_stable_access_token_%s", ak.cacheKeyPrefix, ak.appID)
if val := ak.cache.Get(accessTokenCacheKey); val != nil { 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失效从微信服务器获取 // cache失效从微信服务器获取

View File

@@ -14,4 +14,5 @@ type Config struct {
Token string `json:"token"` // token Token string `json:"token"` // token
EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey
Cache cache.Cache Cache cache.Cache
UseStableAK bool // use the stable access_token
} }

View File

@@ -34,7 +34,13 @@ type MiniProgram struct {
// NewMiniProgram 实例化小程序 API // NewMiniProgram 实例化小程序 API
func NewMiniProgram(cfg *config.Config) *MiniProgram { 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{ ctx := &context.Context{
Config: cfg, Config: cfg,
AccessTokenHandle: defaultAkHandle, AccessTokenHandle: defaultAkHandle,

View File

@@ -54,6 +54,8 @@ type QRCoder struct {
IsHyaline bool `json:"is_hyaline,omitempty"` IsHyaline bool `json:"is_hyaline,omitempty"`
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop" // envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
EnvVersion string `json:"env_version,omitempty"` EnvVersion string `json:"env_version,omitempty"`
// ShowSplashAd 控制通过该小程序码进入小程序是否展示封面广告1、默认为true展示封面广告2、传入为false时不展示封面广告
ShowSplashAd bool `json:"show_splash_ad,omitempty"`
} }
// fetchCode 请求并返回二维码二进制数据 // fetchCode 请求并返回二维码二进制数据

View File

@@ -11,4 +11,5 @@ type Config struct {
Token string `json:"token"` // token Token string `json:"token"` // token
EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey
Cache cache.Cache Cache cache.Cache
UseStableAK bool // use the stable access_token
} }

View File

@@ -4,6 +4,9 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"os"
"path"
"github.com/silenceper/wechat/v2/officialaccount/context" "github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util" "github.com/silenceper/wechat/v2/util"
@@ -160,8 +163,8 @@ type resAddMaterial struct {
URL string `json:"url"` URL string `json:"url"`
} }
// AddMaterial 上传永久性素材(处理视频需要单独上传) // AddMaterialFromReader 上传永久性素材(处理视频需要单独上传),从 io.Reader 中读取
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) { func (material *Material) AddMaterialFromReader(mediaType MediaType, filePath string, reader io.Reader) (mediaID string, url string, err error) {
if mediaType == MediaTypeVideo { if mediaType == MediaTypeVideo {
err = errors.New("永久视频素材上传使用 AddVideo 方法") err = errors.New("永久视频素材上传使用 AddVideo 方法")
return return
@@ -173,8 +176,10 @@ func (material *Material) AddMaterial(mediaType MediaType, filename string) (med
} }
uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType) uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType)
// 获取文件名
filename := path.Base(filePath)
var response []byte var response []byte
response, err = util.PostFile("media", filename, uri) response, err = util.PostFileFromReader("media", filePath, filename, uri, reader)
if err != nil { if err != nil {
return return
} }
@@ -192,13 +197,24 @@ func (material *Material) AddMaterial(mediaType MediaType, filename string) (med
return return
} }
// AddMaterial 上传永久性素材(处理视频需要单独上传)
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
defer func() { _ = f.Close() }()
return material.AddMaterialFromReader(mediaType, filename, f)
}
type reqVideo struct { type reqVideo struct {
Title string `json:"title"` Title string `json:"title"`
Introduction string `json:"introduction"` Introduction string `json:"introduction"`
} }
// AddVideo 永久视频素材文件上传 // AddVideoFromReader 永久视频素材文件上传,从 io.Reader 中读取
func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) { func (material *Material) AddVideoFromReader(filePath, title, introduction string, reader io.Reader) (mediaID string, url string, err error) {
var accessToken string var accessToken string
accessToken, err = material.GetAccessToken() accessToken, err = material.GetAccessToken()
if err != nil { if err != nil {
@@ -216,16 +232,19 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
if err != nil { if err != nil {
return return
} }
fileName := path.Base(filePath)
fields := []util.MultipartFormField{ fields := []util.MultipartFormField{
{ {
IsFile: true, IsFile: true,
Fieldname: "media", Fieldname: "media",
Filename: filename, FilePath: filePath,
Filename: fileName,
FileReader: reader,
}, },
{ {
IsFile: false, IsFile: false,
Fieldname: "description", Fieldname: "description",
Filename: fileName,
Value: fieldValue, Value: fieldValue,
}, },
} }
@@ -250,6 +269,17 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
return return
} }
// AddVideo 永久视频素材文件上传
func (material *Material) AddVideo(directory, title, introduction string) (mediaID string, url string, err error) {
f, err := os.Open(directory)
if err != nil {
return "", "", err
}
defer func() { _ = f.Close() }()
return material.AddVideoFromReader(directory, title, introduction, f)
}
type reqDeleteMaterial struct { type reqDeleteMaterial struct {
MediaID string `json:"media_id"` MediaID string `json:"media_id"`
} }

View File

@@ -3,6 +3,7 @@ package material
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"github.com/silenceper/wechat/v2/util" "github.com/silenceper/wechat/v2/util"
) )
@@ -62,6 +63,38 @@ func (material *Material) MediaUpload(mediaType MediaType, filename string) (med
return return
} }
// MediaUploadFromReader 临时素材上传
func (material *Material) MediaUploadFromReader(mediaType MediaType, filename string, reader io.Reader) (media Media, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s&type=%s", mediaUploadURL, accessToken, mediaType)
var byteData []byte
byteData, err = io.ReadAll(reader)
if err != nil {
return
}
var response []byte
response, err = util.PostFileByStream("media", filename, uri, byteData)
if err != nil {
return
}
err = json.Unmarshal(response, &media)
if err != nil {
return
}
if media.ErrCode != 0 {
err = fmt.Errorf("MediaUpload error : errcode=%v , errmsg=%v", media.ErrCode, media.ErrMsg)
return
}
return
}
// GetMediaURL 返回临时素材的下载地址供用户自己处理 // GetMediaURL 返回临时素材的下载地址供用户自己处理
// NOTICE: URL 不可公开因为含access_token 需要立即另存文件 // NOTICE: URL 不可公开因为含access_token 需要立即另存文件
func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) { func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) {

View File

@@ -61,15 +61,15 @@ func (tpl *Template) Send(msg *TemplateMessage) (msgID int64, err error) {
if err != nil { if err != nil {
return return
} }
uri := fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken) var (
var response []byte uri = fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken)
response, err = util.PostJSON(uri, msg) response []byte
if err != nil { )
if response, err = util.PostJSON(uri, msg); err != nil {
return return
} }
var result resTemplateSend var result resTemplateSend
err = json.Unmarshal(response, &result) if err = json.Unmarshal(response, &result); err != nil {
if err != nil {
return return
} }
if result.ErrCode != 0 { if result.ErrCode != 0 {
@@ -103,10 +103,11 @@ func (tpl *Template) List() (templateList []*TemplateItem, err error) {
if err != nil { if err != nil {
return return
} }
uri := fmt.Sprintf("%s?access_token=%s", templateListURL, accessToken) var (
var response []byte uri = fmt.Sprintf("%s?access_token=%s", templateListURL, accessToken)
response, err = util.HTTPGet(uri) response []byte
if err != nil { )
if response, err = util.HTTPGet(uri); err != nil {
return return
} }
var res resTemplateList var res resTemplateList
@@ -121,22 +122,23 @@ type resTemplateAdd struct {
} }
// Add 添加模板. // Add 添加模板.
func (tpl *Template) Add(shortID string) (templateID string, err error) { func (tpl *Template) Add(shortID string, keyNameList []string) (templateID string, err error) {
var accessToken string var accessToken string
accessToken, err = tpl.GetAccessToken() accessToken, err = tpl.GetAccessToken()
if err != nil { if err != nil {
return return
} }
var msg = struct { var (
ShortID string `json:"template_id_short"` msg = struct {
}{ShortID: shortID} ShortID string `json:"template_id_short"`
uri := fmt.Sprintf("%s?access_token=%s", templateAddURL, accessToken) KeyNameList []string `json:"keyword_name_list"`
var response []byte }{ShortID: shortID, KeyNameList: keyNameList}
response, err = util.PostJSON(uri, msg) uri = fmt.Sprintf("%s?access_token=%s", templateAddURL, accessToken)
if err != nil { response []byte
)
if response, err = util.PostJSON(uri, msg); err != nil {
return return
} }
var result resTemplateAdd var result resTemplateAdd
err = util.DecodeWithError(response, &result, "AddTemplate") err = util.DecodeWithError(response, &result, "AddTemplate")
return result.TemplateID, err return result.TemplateID, err
@@ -149,14 +151,14 @@ func (tpl *Template) Delete(templateID string) (err error) {
if err != nil { if err != nil {
return return
} }
var msg = struct { var (
TemplateID string `json:"template_id"` msg = struct {
}{TemplateID: templateID} TemplateID string `json:"template_id"`
}{TemplateID: templateID}
uri := fmt.Sprintf("%s?access_token=%s", templateDelURL, accessToken) uri = fmt.Sprintf("%s?access_token=%s", templateDelURL, accessToken)
var response []byte response []byte
response, err = util.PostJSON(uri, msg) )
if err != nil { if response, err = util.PostJSON(uri, msg); err != nil {
return return
} }
return util.DecodeWithCommonError(response, "DeleteTemplate") return util.DecodeWithCommonError(response, "DeleteTemplate")

View File

@@ -49,7 +49,13 @@ type OfficialAccount struct {
// NewOfficialAccount 实例化公众号API // NewOfficialAccount 实例化公众号API
func NewOfficialAccount(cfg *config.Config) *OfficialAccount { 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{ ctx := &context.Context{
Config: cfg, Config: cfg,
AccessTokenHandle: defaultAkHandle, AccessTokenHandle: defaultAkHandle,

View File

@@ -146,13 +146,40 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e
return responseData, contentType, err return responseData, contentType, err
} }
// PostFileByStream 上传文件
func PostFileByStream(fieldName, fileName, uri string, byteData []byte) ([]byte, error) {
fields := []MultipartFormField{
{
IsFile: false,
Fieldname: fieldName,
Filename: fileName,
Value: byteData,
},
}
return PostMultipartForm(fields, uri)
}
// PostFile 上传文件 // PostFile 上传文件
func PostFile(fieldName, filename, uri string) ([]byte, error) { func PostFile(fieldName, filePath, uri string) ([]byte, error) {
fields := []MultipartFormField{ fields := []MultipartFormField{
{ {
IsFile: true, IsFile: true,
Fieldname: fieldName, Fieldname: fieldName,
Filename: filename, FilePath: filePath,
},
}
return PostMultipartForm(fields, uri)
}
// PostFileFromReader 上传文件,从 io.Reader 中读取
func PostFileFromReader(filedName, filePath, fileName, uri string, reader io.Reader) ([]byte, error) {
fields := []MultipartFormField{
{
IsFile: true,
Fieldname: filedName,
FilePath: filePath,
Filename: fileName,
FileReader: reader,
}, },
} }
return PostMultipartForm(fields, uri) return PostMultipartForm(fields, uri)
@@ -160,10 +187,12 @@ func PostFile(fieldName, filename, uri string) ([]byte, error) {
// MultipartFormField 保存文件或其他字段信息 // MultipartFormField 保存文件或其他字段信息
type MultipartFormField struct { type MultipartFormField struct {
IsFile bool IsFile bool
Fieldname string Fieldname string
Value []byte Value []byte
Filename string FilePath string
Filename string
FileReader io.Reader
} }
// PostMultipartForm 上传文件或其他多个字段 // PostMultipartForm 上传文件或其他多个字段
@@ -182,18 +211,24 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte
return return
} }
fh, e := os.Open(field.Filename) if field.FileReader == nil {
if e != nil { fh, e := os.Open(field.FilePath)
err = fmt.Errorf("error opening file , err=%v", e) if e != nil {
return err = fmt.Errorf("error opening file , err=%v", e)
} return
defer fh.Close() }
_, err = io.Copy(fileWriter, fh)
if _, err = io.Copy(fileWriter, fh); err != nil { _ = fh.Close()
return if err != nil {
return
}
} else {
if _, err = io.Copy(fileWriter, field.FileReader); err != nil {
return
}
} }
} else { } else {
partWriter, e := bodyWriter.CreateFormField(field.Fieldname) partWriter, e := bodyWriter.CreateFormFile(field.Fieldname, field.Filename)
if e != nil { if e != nil {
err = e err = e
return return
@@ -215,7 +250,7 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, err return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, resp.StatusCode)
} }
respBody, err = io.ReadAll(resp.Body) respBody, err = io.ReadAll(resp.Body)
return return

View File

@@ -18,20 +18,23 @@ const (
// ReceptionistOptions 添加接待人员请求参数 // ReceptionistOptions 添加接待人员请求参数
type ReceptionistOptions struct { type ReceptionistOptions struct {
OpenKFID string `json:"open_kfid"` // 客服帐号ID OpenKFID string `json:"open_kfid"` // 客服帐号ID
UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid即open_userid 可填充个数1 ~ 100。超过100个需分批调用。 UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid即open_userid 可填充个数1 ~ 100。超过100个需分批调用。
DepartmentIDList []int `json:"department_id_list"` // 接待人员部门id列表 可填充个数0 ~ 100。超过100个需分批调用。
} }
// ReceptionistSchema 添加接待人员响应内容 // ReceptionistSchema 添加接待人员响应内容
type ReceptionistSchema struct { type ReceptionistSchema struct {
util.CommonError util.CommonError
ResultList []struct { ResultList []struct {
UserID string `json:"userid"` UserID string `json:"userid"`
DepartmentID int `json:"department_id"`
util.CommonError util.CommonError
} `json:"result_list"` } `json:"result_list"`
} }
// ReceptionistAdd 添加接待人员 // ReceptionistAdd 添加接待人员
// @see https://developer.work.weixin.qq.com/document/path/94646
func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) { func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) {
var ( var (
accessToken string accessToken string
@@ -49,10 +52,11 @@ func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info Receptionist
if info.ErrCode != 0 { if info.ErrCode != 0 {
return info, NewSDKErr(info.ErrCode, info.ErrMsg) return info, NewSDKErr(info.ErrCode, info.ErrMsg)
} }
return info, nil return
} }
// ReceptionistDel 删除接待人员 // ReceptionistDel 删除接待人员
// @see https://developer.work.weixin.qq.com/document/path/94647
func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) { func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) {
var ( var (
accessToken string accessToken string
@@ -72,19 +76,22 @@ func (r *Client) ReceptionistDel(options ReceptionistOptions) (info Receptionist
if info.ErrCode != 0 { if info.ErrCode != 0 {
return info, NewSDKErr(info.ErrCode, info.ErrMsg) return info, NewSDKErr(info.ErrCode, info.ErrMsg)
} }
return info, nil return
} }
// ReceptionistListSchema 获取接待人员列表响应内容 // ReceptionistListSchema 获取接待人员列表响应内容
type ReceptionistListSchema struct { type ReceptionistListSchema struct {
util.CommonError util.CommonError
ReceptionistList []struct { ReceptionistList []struct {
UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid即open_userid UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid即open_userid
Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取 Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取
DepartmentID int `json:"department_id"` // 接待人员部门的id
StopType int `json:"stop_type"` // 接待人员的接待状态为「停止接待」的子类型。0:停止接待,1:暂时挂起
} `json:"servicer_list"` } `json:"servicer_list"`
} }
// ReceptionistList 获取接待人员列表 // ReceptionistList 获取接待人员列表
// @see https://developer.work.weixin.qq.com/document/path/94645
func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) { func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) {
var ( var (
accessToken string accessToken string
@@ -104,5 +111,5 @@ func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err
if info.ErrCode != 0 { if info.ErrCode != 0 {
return info, NewSDKErr(info.ErrCode, info.ErrMsg) return info, NewSDKErr(info.ErrCode, info.ErrMsg)
} }
return info, nil return
} }

View File

@@ -2,6 +2,7 @@ package material
import ( import (
"fmt" "fmt"
"io"
"github.com/silenceper/wechat/v2/util" "github.com/silenceper/wechat/v2/util"
) )
@@ -13,6 +14,8 @@ const (
uploadTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" uploadTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
// uploadAttachment 上传附件资源 // uploadAttachment 上传附件资源
uploadAttachment = "https://qyapi.weixin.qq.com/cgi-bin/media/upload_attachment?access_token=%s&media_type=%s&attachment_type=%d" 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 上传图片响应 // UploadImgResponse 上传图片响应
@@ -96,3 +99,72 @@ func (r *Client) UploadAttachment(filename string, mediaType string, attachmentT
err = util.DecodeWithError(response, result, "UploadAttachment") err = util.DecodeWithError(response, result, "UploadAttachment")
return result, err return result, err
} }
// UploadTempFileFromReader 上传临时素材
// @see https://developer.work.weixin.qq.com/document/path/90253
// @mediaType 媒体文件类型分别有图片image、语音voice、视频video普通文件file
func (r *Client) UploadTempFileFromReader(filename, mediaType string, reader io.Reader) (*UploadTempFileResponse, 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(uploadTempFile, accessToken, mediaType), byteData); err != nil {
return nil, err
}
result := &UploadTempFileResponse{}
err = util.DecodeWithError(response, result, "UploadTempFile")
return result, err
}
// UploadAttachmentFromReader 上传附件资源
// @see https://developer.work.weixin.qq.com/document/path/95098
// @mediaType 媒体文件类型分别有图片image、视频video、普通文件file
// @attachment_type 附件类型不同的附件类型用于不同的场景。1朋友圈2:商品图册
func (r *Client) UploadAttachmentFromReader(filename, mediaType string, reader io.Reader, attachmentType int) (*UploadAttachmentResponse, 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(uploadAttachment, accessToken, mediaType, attachmentType), byteData); err != nil {
return nil, err
}
result := &UploadAttachmentResponse{}
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
}
return response, nil
}