diff --git a/cache/redis.go b/cache/redis.go index f51f7bf88..bcaa8b85a 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -16,6 +16,7 @@ type Redis struct { // RedisOpts redis 连接属性 type RedisOpts struct { Host string `yml:"host" json:"host"` + Username string `yaml:"username" json:"username"` Password string `yml:"password" json:"password"` Database int `yml:"database" json:"database"` 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{ Addrs: []string{opts.Host}, DB: opts.Database, + Username: opts.Username, Password: opts.Password, IdleTimeout: time.Second * time.Duration(opts.IdleTimeout), MinIdleConns: opts.MaxIdle, diff --git a/miniprogram/qrcode/qrcode.go b/miniprogram/qrcode/qrcode.go index 41e67e5bd..bb51b0399 100644 --- a/miniprogram/qrcode/qrcode.go +++ b/miniprogram/qrcode/qrcode.go @@ -54,6 +54,8 @@ type QRCoder struct { IsHyaline bool `json:"is_hyaline,omitempty"` // envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop" EnvVersion string `json:"env_version,omitempty"` + // ShowSplashAd 控制通过该小程序码进入小程序是否展示封面广告1、默认为true,展示封面广告2、传入为false时,不展示封面广告 + ShowSplashAd bool `json:"show_splash_ad,omitempty"` } // fetchCode 请求并返回二维码二进制数据 diff --git a/officialaccount/material/material.go b/officialaccount/material/material.go index d8a43b94a..18f5225eb 100644 --- a/officialaccount/material/material.go +++ b/officialaccount/material/material.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path" "github.com/silenceper/wechat/v2/officialaccount/context" "github.com/silenceper/wechat/v2/util" @@ -163,7 +164,7 @@ type resAddMaterial struct { } // AddMaterialFromReader 上传永久性素材(处理视频需要单独上传),从 io.Reader 中读取 -func (material *Material) AddMaterialFromReader(mediaType MediaType, filename string, reader io.Reader) (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 { err = errors.New("永久视频素材上传使用 AddVideo 方法") return @@ -175,8 +176,10 @@ func (material *Material) AddMaterialFromReader(mediaType MediaType, filename st } uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType) + // 获取文件名 + filename := path.Base(filePath) var response []byte - response, err = util.PostFileFromReader("media", filename, uri, reader) + response, err = util.PostFileFromReader("media", filePath, filename, uri, reader) if err != nil { return } @@ -211,7 +214,7 @@ type reqVideo struct { } // AddVideoFromReader 永久视频素材文件上传,从 io.Reader 中读取 -func (material *Material) AddVideoFromReader(filename, title, introduction string, reader io.Reader) (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 accessToken, err = material.GetAccessToken() if err != nil { @@ -229,17 +232,19 @@ func (material *Material) AddVideoFromReader(filename, title, introduction strin if err != nil { return } - + fileName := path.Base(filePath) fields := []util.MultipartFormField{ { IsFile: true, Fieldname: "media", - Filename: filename, + FilePath: filePath, + Filename: fileName, FileReader: reader, }, { IsFile: false, Fieldname: "description", + Filename: fileName, Value: fieldValue, }, } @@ -265,14 +270,14 @@ func (material *Material) AddVideoFromReader(filename, title, introduction strin } // AddVideo 永久视频素材文件上传 -func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) { - f, err := os.Open(filename) +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(filename, title, introduction, f) + return material.AddVideoFromReader(directory, title, introduction, f) } type reqDeleteMaterial struct { diff --git a/officialaccount/material/media.go b/officialaccount/material/media.go index 316758fb4..b7d6573aa 100644 --- a/officialaccount/material/media.go +++ b/officialaccount/material/media.go @@ -3,6 +3,7 @@ package material import ( "encoding/json" "fmt" + "io" "github.com/silenceper/wechat/v2/util" ) @@ -62,6 +63,38 @@ func (material *Material) MediaUpload(mediaType MediaType, filename string) (med 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 返回临时素材的下载地址供用户自己处理 // NOTICE: URL 不可公开,因为含access_token 需要立即另存文件 func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) { diff --git a/util/http.go b/util/http.go index b9b4b004b..948e38f31 100644 --- a/util/http.go +++ b/util/http.go @@ -146,24 +146,38 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e 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 上传文件 -func PostFile(fieldName, filename, uri string) ([]byte, error) { +func PostFile(fieldName, filePath, uri string) ([]byte, error) { fields := []MultipartFormField{ { IsFile: true, Fieldname: fieldName, - Filename: filename, + FilePath: filePath, }, } return PostMultipartForm(fields, uri) } // PostFileFromReader 上传文件,从 io.Reader 中读取 -func PostFileFromReader(filedName, fileName, uri string, reader io.Reader) ([]byte, error) { +func PostFileFromReader(filedName, filePath, fileName, uri string, reader io.Reader) ([]byte, error) { fields := []MultipartFormField{ { IsFile: true, Fieldname: filedName, + FilePath: filePath, Filename: fileName, FileReader: reader, }, @@ -176,6 +190,7 @@ type MultipartFormField struct { IsFile bool Fieldname string Value []byte + FilePath string Filename string FileReader io.Reader } @@ -197,7 +212,7 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte } if field.FileReader == nil { - fh, e := os.Open(field.Filename) + fh, e := os.Open(field.FilePath) if e != nil { err = fmt.Errorf("error opening file , err=%v", e) return @@ -213,7 +228,7 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte } } } else { - partWriter, e := bodyWriter.CreateFormField(field.Fieldname) + partWriter, e := bodyWriter.CreateFormFile(field.Fieldname, field.Filename) if e != nil { err = e return diff --git a/work/kf/servicer.go b/work/kf/servicer.go index 5c34efc99..8e64c3eaa 100644 --- a/work/kf/servicer.go +++ b/work/kf/servicer.go @@ -18,20 +18,23 @@ const ( // ReceptionistOptions 添加接待人员请求参数 type ReceptionistOptions struct { - OpenKFID string `json:"open_kfid"` // 客服帐号ID - UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 + OpenKFID string `json:"open_kfid"` // 客服帐号ID + UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 + DepartmentIDList []int `json:"department_id_list"` // 接待人员部门id列表 可填充个数:0 ~ 100。超过100个需分批调用。 } // ReceptionistSchema 添加接待人员响应内容 type ReceptionistSchema struct { util.CommonError ResultList []struct { - UserID string `json:"userid"` + UserID string `json:"userid"` + DepartmentID int `json:"department_id"` util.CommonError } `json:"result_list"` } // ReceptionistAdd 添加接待人员 +// @see https://developer.work.weixin.qq.com/document/path/94646 func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) { var ( accessToken string @@ -49,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 @@ -72,19 +76,22 @@ func (r *Client) ReceptionistDel(options ReceptionistOptions) (info Receptionist if info.ErrCode != 0 { return info, NewSDKErr(info.ErrCode, info.ErrMsg) } - return info, nil + return } // ReceptionistListSchema 获取接待人员列表响应内容 type ReceptionistListSchema struct { util.CommonError ReceptionistList []struct { - UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid,即open_userid - Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取 + 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 @@ -104,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 b32785a5c..c3c8e0c34 100644 --- a/work/material/media.go +++ b/work/material/media.go @@ -2,6 +2,7 @@ package material import ( "fmt" + "io" "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" // 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 上传图片响应 @@ -96,3 +99,72 @@ func (r *Client) UploadAttachment(filename string, mediaType string, attachmentT err = util.DecodeWithError(response, result, "UploadAttachment") 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 +}