diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 5d6e38c..f34eaa6 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,3 @@ ## 问题及现象 + +**你想要实现的模块或API** + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..db13f82 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,15 @@ +--- +name: 使用咨询 +about: 关于SDK使用相关的咨询,在使用前请先阅读官方微信文档 +title: "[咨询]" +labels: question +assignees: '' + +--- + + +**请描述您的问题** diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 68e3ed2..b8d591f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,24 +2,29 @@ name: Go on: push: - branches: [ master,release-* ] + branches: [ master,release-*,v2 ] pull_request: - branches: [ master,release-* ] + branches: [ master,release-*,v2 ] jobs: golangci: strategy: matrix: - go-version: [1.15.x] + go-version: [ '1.16','1.17','1.18','1.19','1.20' ] name: golangci-lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Setup Golang ${{ matrix.go-version }} + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout + uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.31 + version: v1.52.2 build: name: Test runs-on: ubuntu-latest @@ -27,18 +32,24 @@ jobs: redis: image: redis ports: - - 6379:6379 + - 6379:6379 options: --entrypoint redis-server memcached: image: memcached ports: - 11211:11211 + + # strategy set + strategy: + matrix: + go: [ '1.16','1.17','1.18','1.19','1.20' ] + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: ^1.13 + go-version: ${{ matrix.go }} id: go - name: Test - run: go test -v -race ./... + run: go test -v -race ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 63a4889..be1b717 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ _testmain.go .vscode/ vendor .idea/ -example/* \ No newline at end of file +example/* +/test diff --git a/.golangci.yml b/.golangci.yml index 933f6ea..bf68478 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,8 +36,13 @@ linters: - whitespace issues: - # Excluding configuration per-path, per-linter, per-text and per-source + include: + - EXC0002 # disable excluding of issues about comments from golint exclude-rules: + - linters: + - stylecheck + text: "ST1000:" + # Excluding configuration per-path, per-linter, per-text and per-source - path: _test\.go linters: - gomnd @@ -50,12 +55,12 @@ issues: linters-settings: funlen: lines: 66 - statements: 40 + statements: 50 -issues: - include: - - EXC0002 # disable excluding of issues about comments from golint - exclude-rules: - - linters: - - stylecheck - text: "ST1000:" +#issues: +# include: +# - EXC0002 # disable excluding of issues about comments from golint +# exclude-rules: +# - linters: +# - stylecheck +# text: "ST1000:" diff --git a/README.md b/README.md index a03c9d0..c3b3a4c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ # WeChat SDK for Go + ![Go](https://github.com/silenceper/wechat/workflows/Go/badge.svg?branch=release-2.0) [![Go Report Card](https://goreportcard.com/badge/github.com/silenceper/wechat)](https://goreportcard.com/report/github.com/silenceper/wechat) [![pkg](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc) +![version](https://img.shields.io/badge/version-v2-green) 使用Golang开发的微信SDK,简单、易用。 ->当前版本为v2版本 - +> 注意:当前版本为v2版本,v1版本已废弃 ## 文档 && 例子 + +[API列表](https://github.com/silenceper/wechat/tree/v2/doc/api) + [Wechat SDK 2.0 文档](https://silenceper.com/wechat) [Wechat SDK 2.0 例子](https://github.com/gowechat/example) ## 快速开始 + ``` import "github.com/silenceper/wechat/v2" ``` @@ -20,40 +25,41 @@ import "github.com/silenceper/wechat/v2" 以下是一个微信公众号处理消息接收以及回复的例子: ```go -//使用memcache保存access_token,也可选择redis或自定义cache +// 使用memcache保存access_token,也可选择redis或自定义cache wc := wechat.NewWechat() memory := cache.NewMemory() cfg := &offConfig.Config{ AppID: "xxx", AppSecret: "xxx", Token: "xxx", - //EncodingAESKey: "xxxx", + // EncodingAESKey: "xxxx", Cache: memory, } officialAccount := wc.GetOfficialAccount(cfg) // 传入request和responseWriter server := officialAccount.GetServer(req, rw) -//设置接收消息的处理方法 +// 设置接收消息的处理方法 server.SetMessageHandler(func(msg *message.MixMessage) *message.Reply { - //回复消息:演示回复用户发送的消息 + // 回复消息:演示回复用户发送的消息 text := message.NewText(msg.Content) return &message.Reply{MsgType: message.MsgTypeText, MsgData: text} }) -//处理消息接收以及回复 +// 处理消息接收以及回复 err := server.Serve() if err != nil { fmt.Println(err) return } -//发送回复的消息 +// 发送回复的消息 server.Send() ``` ## 目录说明 + - officialaccount: 微信公众号API - miniprogram: 小程序API - minigame:小游戏API @@ -61,12 +67,16 @@ server.Send() - openplatform:开放平台API - work:企业微信 - aispeech:智能对话 +- doc: api文档 ## 贡献 + +- 在[API列表](https://github.com/silenceper/wechat/tree/v2/doc/api)中查看哪些API未实现 - 提交issue,描述需要贡献的内容 - 完成更改后,提交PR ## 公众号 + ![img](https://silenceper.oss-cn-beijing.aliyuncs.com/qrcode/search_study_program.png) ## License diff --git a/cache/cache.go b/cache/cache.go index f3feb84..077597d 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -1,6 +1,9 @@ package cache -import "time" +import ( + "context" + "time" +) // Cache interface type Cache interface { @@ -9,3 +12,44 @@ type Cache interface { IsExist(key string) bool Delete(key string) error } + +// ContextCache interface +type ContextCache interface { + Cache + GetContext(ctx context.Context, key string) interface{} + SetContext(ctx context.Context, key string, val interface{}, timeout time.Duration) error + IsExistContext(ctx context.Context, key string) bool + DeleteContext(ctx context.Context, key string) error +} + +// GetContext get value from cache +func GetContext(ctx context.Context, cache Cache, key string) interface{} { + if cache, ok := cache.(ContextCache); ok { + return cache.GetContext(ctx, key) + } + return cache.Get(key) +} + +// SetContext set value to cache +func SetContext(ctx context.Context, cache Cache, key string, val interface{}, timeout time.Duration) error { + if cache, ok := cache.(ContextCache); ok { + return cache.SetContext(ctx, key, val, timeout) + } + return cache.Set(key, val, timeout) +} + +// IsExistContext check value exists in cache. +func IsExistContext(ctx context.Context, cache Cache, key string) bool { + if cache, ok := cache.(ContextCache); ok { + return cache.IsExistContext(ctx, key) + } + return cache.IsExist(key) +} + +// DeleteContext delete value in cache. +func DeleteContext(ctx context.Context, cache Cache, key string) error { + if cache, ok := cache.(ContextCache); ok { + return cache.DeleteContext(ctx, key) + } + return cache.Delete(key) +} diff --git a/cache/memory.go b/cache/memory.go index 7206637..135d9d9 100644 --- a/cache/memory.go +++ b/cache/memory.go @@ -5,7 +5,7 @@ import ( "time" ) -//Memory struct contains *memcache.Client +// Memory struct contains *memcache.Client type Memory struct { sync.Mutex @@ -17,14 +17,14 @@ type data struct { Expired time.Time } -//NewMemory create new memcache +// NewMemory create new memcache func NewMemory() *Memory { return &Memory{ data: map[string]*data{}, } } -//Get return cached value +// Get return cached value func (mem *Memory) Get(key string) interface{} { if ret, ok := mem.data[key]; ok { if ret.Expired.Before(time.Now()) { @@ -48,7 +48,7 @@ func (mem *Memory) IsExist(key string) bool { return false } -//Set cached value with key and expire time. +// Set cached value with key and expire time. func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err error) { mem.Lock() defer mem.Unlock() @@ -60,7 +60,7 @@ func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err return nil } -//Delete delete value in memcache. +// Delete delete value in memcache. func (mem *Memory) Delete(key string) error { mem.deleteKey(key) return nil diff --git a/cache/redis.go b/cache/redis.go index e314739..f51f7bf 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -1,111 +1,92 @@ package cache import ( - "encoding/json" + "context" "time" - "github.com/gomodule/redigo/redis" + "github.com/go-redis/redis/v8" ) -//Redis redis cache +// Redis .redis cache type Redis struct { - conn *redis.Pool + ctx context.Context + conn redis.UniversalClient } -//RedisOpts redis 连接属性 +// RedisOpts redis 连接属性 type RedisOpts struct { Host string `yml:"host" json:"host"` Password string `yml:"password" json:"password"` Database int `yml:"database" json:"database"` MaxIdle int `yml:"max_idle" json:"max_idle"` MaxActive int `yml:"max_active" json:"max_active"` - IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` //second + IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` // second } -//NewRedis 实例化 -func NewRedis(opts *RedisOpts) *Redis { - pool := &redis.Pool{ - MaxActive: opts.MaxActive, - MaxIdle: opts.MaxIdle, - IdleTimeout: time.Second * time.Duration(opts.IdleTimeout), - Dial: func() (redis.Conn, error) { - return redis.Dial("tcp", opts.Host, - redis.DialDatabase(opts.Database), - redis.DialPassword(opts.Password), - ) - }, - TestOnBorrow: func(conn redis.Conn, t time.Time) error { - if time.Since(t) < time.Minute { - return nil - } - _, err := conn.Do("PING") - return err - }, - } - return &Redis{pool} +// NewRedis 实例化 +func NewRedis(ctx context.Context, opts *RedisOpts) *Redis { + conn := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{opts.Host}, + DB: opts.Database, + Password: opts.Password, + IdleTimeout: time.Second * time.Duration(opts.IdleTimeout), + MinIdleConns: opts.MaxIdle, + }) + return &Redis{ctx: ctx, conn: conn} } -//SetRedisPool 设置redis连接池 -func (r *Redis) SetRedisPool(pool *redis.Pool) { - r.conn = pool -} - -//SetConn 设置conn -func (r *Redis) SetConn(conn *redis.Pool) { +// SetConn 设置conn +func (r *Redis) SetConn(conn redis.UniversalClient) { r.conn = conn } -//Get 获取一个值 +// SetRedisCtx 设置redis ctx 参数 +func (r *Redis) SetRedisCtx(ctx context.Context) { + r.ctx = ctx +} + +// Get 获取一个值 func (r *Redis) Get(key string) interface{} { - conn := r.conn.Get() - defer conn.Close() - - var data []byte - var err error - if data, err = redis.Bytes(conn.Do("GET", key)); err != nil { - return nil - } - var reply interface{} - if err = json.Unmarshal(data, &reply); err != nil { - return nil - } - - return reply + return r.GetContext(r.ctx, key) } -//Set 设置一个值 -func (r *Redis) Set(key string, val interface{}, timeout time.Duration) (err error) { - conn := r.conn.Get() - defer conn.Close() - - var data []byte - if data, err = json.Marshal(val); err != nil { - return +// GetContext 获取一个值 +func (r *Redis) GetContext(ctx context.Context, key string) interface{} { + result, err := r.conn.Do(ctx, "GET", key).Result() + if err != nil { + return nil } - - _, err = conn.Do("SETEX", key, int64(timeout/time.Second), data) - - return + return result } -//IsExist 判断key是否存在 +// Set 设置一个值 +func (r *Redis) Set(key string, val interface{}, timeout time.Duration) error { + return r.SetContext(r.ctx, key, val, timeout) +} + +// SetContext 设置一个值 +func (r *Redis) SetContext(ctx context.Context, key string, val interface{}, timeout time.Duration) error { + return r.conn.SetEX(ctx, key, val, timeout).Err() +} + +// IsExist 判断key是否存在 func (r *Redis) IsExist(key string) bool { - conn := r.conn.Get() - defer conn.Close() - - a, _ := conn.Do("EXISTS", key) - i := a.(int64) - return i > 0 + return r.IsExistContext(r.ctx, key) } -//Delete 删除 +// IsExistContext 判断key是否存在 +func (r *Redis) IsExistContext(ctx context.Context, key string) bool { + result, _ := r.conn.Exists(ctx, key).Result() + + return result > 0 +} + +// Delete 删除 func (r *Redis) Delete(key string) error { - conn := r.conn.Get() - defer conn.Close() - - if _, err := conn.Do("DEL", key); err != nil { - return err - } - - return nil + return r.DeleteContext(r.ctx, key) +} + +// DeleteContext 删除 +func (r *Redis) DeleteContext(ctx context.Context, key string) error { + return r.conn.Del(ctx, key).Err() } diff --git a/cache/redis_test.go b/cache/redis_test.go index 80e3d72..a41a2f1 100644 --- a/cache/redis_test.go +++ b/cache/redis_test.go @@ -1,33 +1,46 @@ package cache import ( + "context" "testing" "time" + + "github.com/alicebob/miniredis/v2" ) func TestRedis(t *testing.T) { - opts := &RedisOpts{ - Host: "127.0.0.1:6379", + server, err := miniredis.Run() + if err != nil { + t.Error("miniredis.Run Error", err) } - redis := NewRedis(opts) + t.Cleanup(server.Close) + var ( + timeoutDuration = time.Second + ctx = context.Background() + opts = &RedisOpts{ + Host: server.Addr(), + } + redis = NewRedis(ctx, opts) + val = "silenceper" + key = "username" + ) redis.SetConn(redis.conn) - var err error - timeoutDuration := 1 * time.Second + redis.SetRedisCtx(ctx) - if err = redis.Set("username", "silenceper", timeoutDuration); err != nil { + if err = redis.Set(key, val, timeoutDuration); err != nil { t.Error("set Error", err) } - if !redis.IsExist("username") { + if !redis.IsExist(key) { t.Error("IsExist Error") } - name := redis.Get("username").(string) - if name != "silenceper" { + name := redis.Get(key).(string) + if name != val { t.Error("get Error") } - if err = redis.Delete("username"); err != nil { + if err = redis.Delete(key); err != nil { t.Errorf("delete Error , err=%v", err) } } diff --git a/credential/access_token.go b/credential/access_token.go index 362e705..6094a02 100644 --- a/credential/access_token.go +++ b/credential/access_token.go @@ -1,6 +1,14 @@ package credential -//AccessTokenHandle AccessToken 接口 +import "context" + +// AccessTokenHandle AccessToken 接口 type AccessTokenHandle interface { GetAccessToken() (accessToken string, err error) } + +// AccessTokenContextHandle AccessToken 接口 +type AccessTokenContextHandle interface { + AccessTokenHandle + GetAccessTokenContext(ctx context.Context) (accessToken string, err error) +} diff --git a/credential/default_access_token.go b/credential/default_access_token.go index 1157d2e..90dffd1 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -1,6 +1,7 @@ package credential import ( + "context" "encoding/json" "fmt" "sync" @@ -11,15 +12,21 @@ import ( ) const ( - //AccessTokenURL 获取access_token的接口 - accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token" - //CacheKeyOfficialAccountPrefix 微信公众号cache key前缀 + // accessTokenURL 获取access_token的接口 + accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" + // 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_" - //CacheKeyMiniProgramPrefix 小程序cache key前缀 + // CacheKeyMiniProgramPrefix 小程序cache key前缀 CacheKeyMiniProgramPrefix = "gowechat_miniprogram_" + // CacheKeyWorkPrefix 企业微信cache key前缀 + CacheKeyWorkPrefix = "gowechat_work_" ) -//DefaultAccessToken 默认AccessToken 获取 +// DefaultAccessToken 默认AccessToken 获取 type DefaultAccessToken struct { appID string appSecret string @@ -28,8 +35,8 @@ type DefaultAccessToken struct { accessTokenLock *sync.Mutex } -//NewDefaultAccessToken new DefaultAccessToken -func NewDefaultAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenHandle { +// NewDefaultAccessToken new DefaultAccessToken +func NewDefaultAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle { if cache == nil { panic("cache is ineed") } @@ -42,7 +49,7 @@ func NewDefaultAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache. } } -//ResAccessToken struct +// ResAccessToken struct type ResAccessToken struct { util.CommonError @@ -50,22 +57,159 @@ type ResAccessToken struct { ExpiresIn int64 `json:"expires_in"` } -//GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取 +// GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取 func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) { - //加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token + return ak.GetAccessTokenContext(context.Background()) +} + +// GetAccessTokenContext 获取access_token,先从cache中获取,没有则从服务端获取 +func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (accessToken string, err error) { + // 先从cache中取 + accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID) + val := ak.cache.Get(accessTokenCacheKey) + if accessToken = val.(string); accessToken != "" { + return + } + + // 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token ak.accessTokenLock.Lock() defer ak.accessTokenLock.Unlock() - accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID) + // 双检,防止重复从微信服务器获取 + val = ak.cache.Get(accessTokenCacheKey) + if accessToken = val.(string); accessToken != "" { + return + } + + // cache失效,从微信服务器获取 + var resAccessToken ResAccessToken + 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 - 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 + } + + 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 +} + +// WorkAccessToken 企业微信AccessToken 获取 +type WorkAccessToken struct { + CorpID string + CorpSecret string + cacheKeyPrefix string + cache cache.Cache + accessTokenLock *sync.Mutex +} + +// NewWorkAccessToken new WorkAccessToken +func NewWorkAccessToken(corpID, corpSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle { + if cache == nil { + panic("cache the not exist") + } + return &WorkAccessToken{ + CorpID: corpID, + CorpSecret: corpSecret, + cache: cache, + cacheKeyPrefix: cacheKeyPrefix, + accessTokenLock: new(sync.Mutex), + } +} + +// GetAccessToken 企业微信获取access_token,先从cache中获取,没有则从服务端获取 +func (ak *WorkAccessToken) GetAccessToken() (accessToken string, err error) { + return ak.GetAccessTokenContext(context.Background()) +} + +// GetAccessTokenContext 企业微信获取access_token,先从cache中获取,没有则从服务端获取 +func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessToken string, err error) { + // 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token + ak.accessTokenLock.Lock() + defer ak.accessTokenLock.Unlock() + accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID) val := ak.cache.Get(accessTokenCacheKey) if val != nil { accessToken = val.(string) return } - //cache失效,从微信服务器获取 + // cache失效,从微信服务器获取 var resAccessToken ResAccessToken - resAccessToken, err = GetTokenFromServer(ak.appID, ak.appSecret) + resAccessToken, err = GetTokenFromServerContext(ctx, fmt.Sprintf(workAccessTokenURL, ak.CorpID, ak.CorpSecret)) if err != nil { return } @@ -79,11 +223,15 @@ func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) { return } -//GetTokenFromServer 强制从微信服务器获取token -func GetTokenFromServer(appID, appSecret string) (resAccessToken ResAccessToken, err error) { - url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenURL, appID, appSecret) +// GetTokenFromServer 强制从微信服务器获取token +func GetTokenFromServer(url string) (resAccessToken ResAccessToken, err error) { + return GetTokenFromServerContext(context.Background(), url) +} + +// GetTokenFromServerContext 强制从微信服务器获取token +func GetTokenFromServerContext(ctx context.Context, url string) (resAccessToken ResAccessToken, err error) { var body []byte - body, err = util.HTTPGet(url) + body, err = util.HTTPGetContext(ctx, url) if err != nil { return } @@ -91,7 +239,7 @@ func GetTokenFromServer(appID, appSecret string) (resAccessToken ResAccessToken, if err != nil { return } - if resAccessToken.ErrMsg != "" { + if resAccessToken.ErrCode != 0 { err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg) return } diff --git a/credential/default_access_token_test.go b/credential/default_access_token_test.go index feafe39..93e4ee6 100644 --- a/credential/default_access_token_test.go +++ b/credential/default_access_token_test.go @@ -7,6 +7,7 @@ import ( "gopkg.in/h2non/gock.v1" ) +// TestGetTicketFromServer . func TestGetTicketFromServer(t *testing.T) { defer gock.Off() gock.New(getTicketURL).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10}) diff --git a/credential/default_js_ticket.go b/credential/default_js_ticket.go index 1d4d7a0..5ba55c3 100644 --- a/credential/default_js_ticket.go +++ b/credential/default_js_ticket.go @@ -10,19 +10,19 @@ import ( "github.com/silenceper/wechat/v2/util" ) -//获取ticket的url +// getTicketURL 获取ticket的url const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi" -//DefaultJsTicket 默认获取js ticket方法 +// DefaultJsTicket 默认获取js ticket方法 type DefaultJsTicket struct { appID string cacheKeyPrefix string cache cache.Cache - //jsAPITicket 读写锁 同一个AppID一个 + // jsAPITicket 读写锁 同一个AppID一个 jsAPITicketLock *sync.Mutex } -//NewDefaultJsTicket new +// NewDefaultJsTicket new func NewDefaultJsTicket(appID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle { return &DefaultJsTicket{ appID: appID, @@ -40,18 +40,22 @@ type ResTicket struct { ExpiresIn int64 `json:"expires_in"` } -//GetTicket 获取jsapi_ticket +// GetTicket 获取jsapi_ticket func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) { + // 先从cache中取 + jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID) + if val := js.cache.Get(jsAPITicketCacheKey); val != nil { + return val.(string), nil + } + js.jsAPITicketLock.Lock() defer js.jsAPITicketLock.Unlock() - //先从cache中取 - jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID) - val := js.cache.Get(jsAPITicketCacheKey) - if val != nil { - ticketStr = val.(string) - return + // 双检,防止重复从微信服务器获取 + if val := js.cache.Get(jsAPITicketCacheKey); val != nil { + return val.(string), nil } + var ticket ResTicket ticket, err = GetTicketFromServer(accessToken) if err != nil { @@ -63,7 +67,7 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err return } -//GetTicketFromServer 从服务器中获取ticket +// GetTicketFromServer 从服务器中获取ticket func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) { var response []byte url := fmt.Sprintf(getTicketURL, accessToken) diff --git a/credential/js_ticket.go b/credential/js_ticket.go index e6f4ebc..d2f3c0c 100644 --- a/credential/js_ticket.go +++ b/credential/js_ticket.go @@ -1,7 +1,7 @@ package credential -//JsTicketHandle js ticket获取 +// JsTicketHandle js ticket获取 type JsTicketHandle interface { - //GetTicket 获取ticket + // GetTicket 获取ticket GetTicket(accessToken string) (ticket string, err error) } diff --git a/doc.go b/doc.go index c8ce6c9..5047312 100644 --- a/doc.go +++ b/doc.go @@ -7,4 +7,6 @@ Package wechat provide wechat sdk for go 更多信息:https://github.com/silenceper/wechat */ + +// Package wechat provide wechat sdk for go package wechat diff --git a/doc/api/README.md b/doc/api/README.md new file mode 100644 index 0000000..f38c388 --- /dev/null +++ b/doc/api/README.md @@ -0,0 +1,17 @@ +# API 文档 + +已完成以及未完成API列表汇总 + +如果有兴趣参与贡献,可以在具体的API表格后面标识自己为贡献者以及完成时间,例如: + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者|完成时间| +| :---------------------: | -------- | :------------------------- | ---------- | -------- |-------- |-------- | +| 获取公众号类目 | GET | /wxaapi/newtmpl/getcategory | NO | |silenceper| 2021-12-20| + +- [微信公众号](./officialaccount.md) +- [小程序](./miniprogram.md) +- [小游戏](./minigame.md) +- [开放平台](./oplatform.md) +- [微信支付](./wxpay.md) +- [企业微信](./work.md) +- [智能对话](./aispeech.md) diff --git a/doc/api/aispeech.md b/doc/api/aispeech.md new file mode 100644 index 0000000..6d6120b --- /dev/null +++ b/doc/api/aispeech.md @@ -0,0 +1,3 @@ +# 智能对话 + +TODO diff --git a/doc/api/minigame.md b/doc/api/minigame.md new file mode 100644 index 0000000..f19ede7 --- /dev/null +++ b/doc/api/minigame.md @@ -0,0 +1,3 @@ +# 小游戏 + +TODO \ No newline at end of file diff --git a/doc/api/miniprogram.md b/doc/api/miniprogram.md new file mode 100644 index 0000000..75d2a24 --- /dev/null +++ b/doc/api/miniprogram.md @@ -0,0 +1,50 @@ +# 小程序 + +## 基础接口 + +TODO + +## 内容安全 + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.mediaCheckAsync.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :-------------------------------: | -------- | :------------------------------- | ---------- | -------------------------------------- | +| 异步校验图片/音频 v1.0 | POST | /wxa/media_check_async | YES | (security *Security) MediaCheckAsyncV1 | +| 同步校验一张图片 v1.0 | POST | /wxa/img_sec_check | YES | (security *Security) ImageCheckV1 | +| 异步校验图片/音频 | POST | /wxa/media_check_async?version=2 | YES | (security *Security) MediaCheckAsync | +| 同步检查一段文本 v1.0 | POST | /wxa/msg_sec_check | YES | (security *Security) MsgCheckV1 | +| 同步检查一段文本 | POST | /wxa/msg_sec_check?version=2 | YES | (security *Security) MsgCheck | + + +## OCR + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.bankcard.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :------------: | -------- | :--------------------- | ---------- | -------- | +| 银行卡识别 | POST | /cv/ocr/bankcard | | | +| 营业执照识别 | POST | /cv/ocr/bizlicense | | | +| 驾驶证识别 | POST | /cv/ocr/drivinglicense | | | +| 身份证识别 | POST | /cv/ocr/idcard | | | +| 通用印刷体识别 | POST | /cv/ocr/comm | | | +| 行驶证识别 | POST | /cv/ocr/driving | | | + + +## 手机号 + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :----------------: | -------- | :------------------------------- | ---------- | ----------------------------------- | +| code换取用户手机号 | POST | /wxa/business/getuserphonenumber | YES | (business *Business) GetPhoneNumber | + + +## 安全风控 + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/safety-control-capability/riskControl.getUserRiskRank.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :----------------: | -------- | :------------------- | ---------- | ------------------------------------------ | +| 获取用户的安全等级 | POST | /wxa/getuserriskrank | YES | (riskControl *RiskControl) GetUserRiskRank | + diff --git a/doc/api/officialaccount.md b/doc/api/officialaccount.md new file mode 100644 index 0000000..5b81b4e --- /dev/null +++ b/doc/api/officialaccount.md @@ -0,0 +1,238 @@ +# 微信公众号 API 列表 + +## 基础接口 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :-----------------------: | -------- | :------------------------- | ---------- | -------- | +| 获取 Access token | GET | /cgi-bin/token | YES | | +| 获取微信服务器 IP 地址 | GET | /cgi-bin/get_api_domain_ip | YES | | +| 获取微信 callback IP 地址 | GET | /cgi-bin/getcallbackip | YES | | +| 清理接口调用次数 | POST | /cgi-bin/clear_quota | YES | | + +## 订阅通知 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Subscription_Messages/api.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| -------------------- | -------- | -------------------------------------- | ---------- | -------------------------------------------- | +| 选用模板 | POST | /wxaapi/newtmpl/addtemplate | YES | (tpl \*Subscribe) Add | +| 删除模板 | POST | /wxaapi/newtmpl/deltemplate | YES | (tpl \*Subscribe) Delete | +| 获取公众号类目 | GET | /wxaapi/newtmpl/getcategory | YES | (tpl \*Subscribe) GetCategory | +| 获取模板中的关键词 | GET | /wxaapi/newtmpl/getpubtemplatekeywords | YES | (tpl \*Subscribe) GetPubTplKeyWordsByID | +| 获取类目下的公共模板 | GET | /wxaapi/newtmpl/getpubtemplatetitles | YES | (tpl \*Subscribe) GetPublicTemplateTitleList | +| 获取私有模板列表 | GET | /wxaapi/newtmpl/gettemplate | YES | (tpl \*Subscribe) List() | +| 发送订阅通知 | POST | /cgi-bin/message/subscribe/bizsend | YES | (tpl \*Subscribe) Send | + +## 客服消息 + +### PC 客服能力 + +#### 客服管理 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Customer_Service_Management.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ---------------- | --------- | -------------------------------------- | ---------- | -------------------------------- | +| 获取客服基本信息 | GET | /cgi-bin/customservice/getkflist | YES | (csm \*Manager) List | +| 添加客服帐号 | POST | /customservice/kfaccount/add | YES | (csm \*Manager) Add | +| 邀请绑定客服帐号 | POST | /customservice/kfaccount/inviteworker | YES | (csm \*Manager) InviteBind | +| 设置客服信息 | POST | /customservice/kfaccount/update | YES | (csm \*Manager) Update | +| 上传客服头像 | POST/FORM | /customservice/kfaccount/uploadheadimg | YES | (csm \*Manager) UploadHeadImg | +| 删除客服帐号 | POST | /customservice/kfaccount/del | YES | (csm \*Manager) Delete | +| 获取在线客服 | POST | /cgi-bin/customservice/getonlinekflist | YES | (csm \*Manager) OnlineList | +| 下发客服输入状态 | POST | /cgi-bin/message/custom/typing | YES | (csm \*Manager) SendTypingStatus | + +#### 会话控制 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Session_control.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------ | -------- | --------------------------------------- | ---------- | -------- | +| 创建会话 | POST | /customservice/kfsession/create | NO | | +| 获取客户会话状态 | GET | /customservice/kfsession/getsession | NO | | +| 获取客服会话列表 | GET | /customservice/kfsession/getsessionlist | NO | | +| 获取未接入会话列表 | POST | /customservice/kfsession/getwaitcase | NO | | + +#### 获取聊天记录 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Obtain_chat_transcript.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------ | -------- | ----------------------------------- | ---------- | -------- | +| 获取聊天记录 | POST | /customservice/msgrecord/getmsglist | NO | | + +### 对话能力 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Shopping_Guide/guide.html) + +#### 顾问管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------------------ | -------- | -------------------------------------- | ---------- | -------- | +| 添加顾问 | POST | /cgi-bin/guide/addguideacct | NO | | +| 获取顾问信息 | POST | /cgi-bin/guide/getguideacct | NO | | +| 修改顾问信息 | POST | /cgi-bin/guide/updateguideacct | NO | | +| 删除顾问 | POST | /cgi-bin/guide/delguideacct | NO | | +| 获取服务号顾问列表 | POST | /cgi-bin/guide/getguideacctlist | NO | | +| 生成顾问二维码 | POST | /cgi-bin/guide/guidecreateqrcode | NO | | +| 获取顾问聊天记录 | POST | /cgi-bin/guide/getguidebuyerchatrecord | NO | | +| 设置快捷回复与关注自动回复 | POST | /cgi-bin/guide/setguideconfig | NO | | +| 获取快捷回复与关注自动回复 | POST | /cgi-bin/guide/getguideconfig | NO | | +| 设置敏感词与离线自动回复 | POST | /cgi-bin/guide/setguideacctconfig | NO | | +| 获取离线自动回复与敏感词 | POST | /cgi-bin/guide/getguideacctconfig | NO | | +| 允许微信用户复制小程序页面路径 | POST | /cgi-bin/guide/pushshowwxapathmenu | NO | | +| 新建顾问分组 | POST | /cgi-bin/guide/newguidegroup | NO | | +| 获取顾问分组列表 | POST | /cgi-bin/guide/getguidegrouplist | NO | | +| 获取顾问分组信息 | POST | /cgi-bin/guide/getgroupinfo | NO | | +| 分组内添加顾问 | POST | /cgi-bin/guide/addguide2guidegroup | NO | | +| 分组内删除顾问 | POST | /cgi-bin/guide/delguide2guidegroup | NO | | +| 获取顾问所在分组 | POST | /cgi-bin/guide/getgroupbyguide | NO | | +| 删除指定顾问分组 | POST | /cgi-bin/guide/delguidegroup | NO | | + +#### 客户管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------------ | -------- | ------------------------------------------- | ---------- | -------- | +| 为顾问分配客户 | POST | /cgi-bin/guide/addguidebuyerrelation | NO | | +| 为顾问移除客户 | POST | /cgi-bin/guide/delguidebuyerrelation | NO | | +| 获取顾问的客户列表 | POST | /cgi-bin/guide/getguidebuyerrelationlist | NO | | +| 为客户更换顾问 | POST | /cgi-bin/guide/rebindguideacctforbuyer | NO | | +| 修改客户昵称 | POST | /cgi-bin/guide/updateguidebuyerrelation | NO | | +| 查询客户所属顾问 | POST | /cgi-bin/guide/getguidebuyerrelationbybuyer | NO | | +| 查询指定顾问和客户的关系 | POST | /cgi-bin/guide/getguidebuyerrelation | NO | | + +#### 标签管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------ | -------- | -------------------------------------- | ---------- | -------- | +| 新建标签类型 | POST | /cgi-bin/guide/newguidetagoption | NO | | +| 删除标签类型 | POST | /cgi-bin/guide/delguidetagoption | NO | | +| 为标签添加可选值 | POST | /cgi-bin/guide/addguidetagoption | NO | | +| 获取标签和可选值 | POST | /cgi-bin/guide/getguidetagoption | NO | | +| 为客户设置标签 | POST | /cgi-bin/guide/addguidebuyertag | NO | | +| 查询客户标签 | POST | /cgi-bin/guide/getguidebuyertag | NO | | +| 根据标签值筛选客户 | POST | /cgi-bin/guide/queryguidebuyerbytag | NO | | +| 删除客户标签 | POST | /cgi-bin/guide/delguidebuyertag | NO | | +| 设置自定义客户信息 | POST | /cgi-bin/guide/addguidebuyerdisplaytag | NO | | +| 获取自定义客户信息 | POST | /cgi-bin/guide/getguidebuyerdisplaytag | NO | | + +#### 素材管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------ | -------- | ------------------------------------ | ---------- | -------- | +| 添加小程序卡片素材 | POST | /cgi-bin/guide/setguidecardmaterial | NO | | +| 查询小程序卡片素材 | POST | /cgi-bin/guide/getguidecardmaterial | NO | | +| 删除小程序卡片素材 | POST | /cgi-bin/guide/delguidecardmaterial | NO | | +| 添加图片素材 | POST | /cgi-bin/guide/setguideimagematerial | NO | | +| 查询图片素材 | POST | /cgi-bin/guide/getguideimagematerial | NO | | +| 删除图片素材 | POST | /cgi-bin/guide/delguideimagematerial | NO | | +| 添加文字素材 | POST | /cgi-bin/guide/setguidewordmaterial | NO | | +| 查询文字素材 | POST | /cgi-bin/guide/getguidewordmaterial | NO | | +| 删除文字素材 | POST | /cgi-bin/guide/delguidewordmaterial | NO | | + +#### 群发任务管理 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Shopping_Guide/task-account/shopping-guide.addGuideMassendJob.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| -------------------- | -------- | ------------------------------------- | ---------- | -------- | +| 添加群发任务 | POST | /cgi-bin/guide/addguidemassendjob | NO | | +| 获取群发任务列表 | POST | /cgi-bin/guide/getguidemassendjoblist | NO | | +| 获取指定群发任务信息 | POST | /cgi-bin/guide/getguidemassendjob | NO | | +| 修改群发任务 | POST | /cgi-bin/guide/updateguidemassendjob | NO | | +| 取消群发任务 | POST | /cgi-bin/guide/cancelguidemassendjob | NO | | + +## 微信网页开发 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ----------------------------------------------------------------------- | -------- | --------------------------------------------------- | ---------- | ------------------------------------ | +| 获取跳转的 url 地址 | GET | https://open.weixin.qq.com/connect/oauth2/authorize | YES | (oauth \*Oauth) GetRedirectURL | +| 获取网页应用跳转的 url 地址 | GET | https://open.weixin.qq.com/connect/qrconnect | YES | (oauth \*Oauth) GetWebAppRedirectURL | +| 通过网页授权的 code 换取 access_token(区别于 context 中的 access_token) | GET | /sns/oauth2/access_token | YES | (oauth \*Oauth) GetUserAccessToken | +| 刷新 access_token | GET | /sns/oauth2/refresh_token? | YES | (oauth \*Oauth) RefreshAccessToken | +| 检验 access_token 是否有效 | GET | /sns/auth | YES | (oauth \*Oauth) CheckAccessToken( | +| 拉取用户信息(需 scope 为 snsapi_userinfo) | GET | /sns/userinfo | YES | (oauth \*Oauth) GetUserInfo | +| 获取 jssdk 需要的配置参数 | GET | /cgi-bin/ticket/getticket | YES | (js \*Js) GetConfig | + +## 素材管理 + +## 草稿箱 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Draft_Box/Add_draft.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| --------------------------- | -------- | ------------------------------------------------------------ | ---------- | ----------------------------- | +| 新建草稿 | POST | /cgi-bin/draft/add | YES | (draft \*Draft) AddDraft | +| 获取草稿 | POST | /cgi-bin/draft/get | YES | (draft \*Draft) GetDraft | +| 删除草稿 | POST | /cgi-bin/draft/delete | YES | (draft \*Draft) DeleteDraft | +| 修改草稿 | POST | /cgi-bin/draft/update | YES | (draft \*Draft) UpdateDraft | +| 获取草稿总数 | GET | /cgi-bin/draft/count | YES | (draft \*Draft) CountDraft | +| 获取草稿列表 | POST | /cgi-bin/draft/batchget | YES | (draft \*Draft) PaginateDraft | +| MP 端开关(仅内测期间使用) | POST | /cgi-bin/draft/switch
/cgi-bin/draft/switch?checkonly=1 | NO | | + +## 发布能力 + +[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Publish/Publish.html) + +说明:「发表记录」包括群发和发布。 + +注意:该接口,只能处理 "发布" 相关的信息,无法操作和获取 "群发" 相关内容!![官方回复](https://developers.weixin.qq.com/community/develop/doc/0002a4fb2109d8f7a91d421c556c00) + +- 群发:主动推送给粉丝,历史消息可看,被搜一搜收录,可以限定部分的粉丝接收到。 +- 发布:不会主动推给粉丝,历史消息列表看不到,但是是公开给所有人的文章。也不会占用群发的次数。每天可以发布多篇内容。可以用于自动回复、自定义菜单、页面模板和话题中,发布成功时会生成一个永久链接。 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------------------ | -------- | ------------------------------- | ---------- | ---------------------------------------- | +| 发布接口 | POST | /cgi-bin/freepublish/submit | YES | (freePublish \*FreePublish) Publish | +| 发布状态轮询接口 | POST | /cgi-bin/freepublish/get | YES | (freePublish \*FreePublish) SelectStatus | +| 事件推送发布结果 | | | YES | EventPublishJobFinish | +| 删除发布 | POST | /cgi-bin/freepublish/delete | YES | (freePublish \*FreePublish) Delete | +| 通过 article_id 获取已发布文章 | POST | /cgi-bin/freepublish/getarticle | YES | (freePublish \*FreePublish) First | +| 获取成功发布列表 | POST | /cgi-bin/freepublish/batchget | YES | (freePublish \*FreePublish) Paginate | + +## 图文消息留言管理 + +## 用户管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| ------------------------------------------ | -------- | -------------------------------------- | ---------- | ---------------------------------- | +| 获取指定 OpenID 变化列表(公众号账号迁移) | POST | /cgi-bin/changeopenid | YES | (user \*User) ListChangeOpenIDs | +| 获取所有用户 OpenID 列表(公众号账号迁移) | | | YES | (user \*User) ListAllChangeOpenIDs | +| 获取用户基本信息 | GET | /cgi-bin/user/info | YES | (user \*User) GetUserInfo | +| 设置用户备注名 | POST | /cgi-bin/user/info/updateremark | YES | (user \*User) UpdateRemark | +| 获取用户列表 | GET | /cgi-bin/user/get | YES | (user \*User) ListUserOpenIDs | +| 获取所有用户 OpenID 列表 | | | YES | (user \*User) ListAllUserOpenIDs | +| 获取公众号的黑名单列表 | POST | /cgi-bin/tags/members/getblacklist | YES | (user \*User) GetBlackList | +| 获取公众号的所有黑名单列表 | | | YES | (user \*User) GetAllBlackList | +| 拉黑用户 | POST | /cgi-bin/tags/members/batchblacklist | YES | (user \*User) BatchBlackList | +| 取消拉黑用户 | POST | /cgi-bin/tags/members/batchunblacklist | YES | (user \*User) BatchUnBlackList | +| 创建标签 | POST | /cgi-bin/tags/create | YES | (user \*User) CreateTag | +| 删除标签 | POST | /cgi-bin/tags/delete | YES | (user \*User) DeleteTag | +| 编辑标签 | POST | /cgi-bin/tags/update | YES | (user \*User) UpdateTag | +| 获取公众号已创建的标签 | GET | /cgi-bin/tags/get | YES | (user \*User) GetTag | +| 获取标签下粉丝列表 | POST | /cgi-bin/user/tag/get | YES | (user \*User) OpenIDListByTag | +| 批量为用户打标签 | POST | /cgi-bin/tags/members/batchtagging | YES | (user \*User) BatchTag | +| 批量为用户取消标签 | POST | /cgi-bin/tags/members/batchuntagging | YES | (user \*User) BatchUntag | +| 获取用户身上的标签列表 | POST | /cgi-bin/tags/getidlist | YES | (user \*User) UserTidList | + +## 账号管理 + +## 数据统计 + +## 微信卡券 + +## 微信门店 + +## 智能接口 + +## 微信设备功能 + +## 微信“一物一码” + +## 微信发票 + +## 微信非税缴费 diff --git a/doc/api/oplatform.md b/doc/api/oplatform.md new file mode 100644 index 0000000..69f56ae --- /dev/null +++ b/doc/api/oplatform.md @@ -0,0 +1 @@ +# 开放平台 diff --git a/doc/api/work.md b/doc/api/work.md new file mode 100644 index 0000000..3be65c7 --- /dev/null +++ b/doc/api/work.md @@ -0,0 +1,129 @@ +# 企业微信 + +host: https://qyapi.weixin.qq.com/ + +## 微信客服 + +[官方文档](https://work.weixin.qq.com/api/doc/90000/90135/94638) + +### 客服账号管理 + +[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94684) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 | +| :--------------: | -------- | :-------------------------- | ---------- | -------------------------------|------------| +| 添加客服帐号 | POST | /cgi-bin/kf/account/add | YES | (r *Client) AccountAdd | NICEXAI | +| 删除客服帐号 | POST | /cgi-bin/kf/account/del | YES | (r *Client) AccountDel | NICEXAI | +| 修改客服帐号 | POST | /cgi-bin/kf/account/update | YES | (r *Client) AccountUpdate | NICEXAI | +| 获取客服帐号列表 | GET | /cgi-bin/kf/account/list | YES | (r *Client) AccountList | NICEXAI | +| 获取客服帐号链接 | GET | /cgi-bin/kf/add_contact_way | YES | (r *Client) AddContactWay | NICEXAI | + +### 接待人员列表 + +[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94693) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 | +| :--------------: | -------- | :-------------------------- | ---------- | -------------------------------|------------| +| 添加接待人员 | POST | /cgi-bin/kf/servicer/add | YES | (r *Client) ReceptionistAdd | NICEXAI | +| 删除接待人员 | POST | /cgi-bin/kf/servicer/del | YES | (r *Client) ReceptionistDel | NICEXAI | +| 获取接待人员列表 | GET | /cgi-bin/kf/servicer/list | YES | (r *Client) ReceptionistList | NICEXAI | + +### 会话分配与消息收发 + +[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94694) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 | +| :--------------: | -------- | :-------------------------------| ---------- | ------------------------------- |------------| +| 获取会话状态 | POST | /cgi-bin/kf/service_state/get | YES | (r *Client) ServiceStateGet | NICEXAI | +| 变更会话状态 | POST | /cgi-bin/kf/service_state/trans | YES | (r *Client) ServiceStateTrans | NICEXAI | +| 读取消息 | POST | /cgi-bin/kf/sync_msg | YES | (r *Client) SyncMsg | NICEXAI | +| 发送消息 | POST | /cgi-bin/kf/send_msg | YES | (r *Client) SendMsg | NICEXAI | +| 发送事件响应消息 | POST | /cgi-bin/kf/send_msg_on_event | YES | (r *Client) SendMsgOnEvent | NICEXAI | + +### 升级服务配置 + +[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94702) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 | +| :--------------: | -------- | :-------------------------------------------------| ---------- | ------------------------------- |------------| +| 获取配置的专员与客户群 | POST | /cgi-bin/kf/customer/get_upgrade_service_config | YES | (r *Client) UpgradeServiceConfig | NICEXAI | +| 为客户升级为专员或客户群服务 | POST | /cgi-bin/kf/customer/upgrade_service | YES | (r *Client) UpgradeService | NICEXAI | +| 为客户取消推荐 | POST | /cgi-bin/kf/customer/cancel_upgrade_service | YES | (r *Client) UpgradeServiceCancel | NICEXAI | + +### 其他基础信息获取 + +[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/95148) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +| :--------------: | -------- | :---------------------------------------| ---------- | ------------------------------- |------------| +| 获取客户基础信息 | POST | /cgi-bin/kf/customer/batchget | YES | (r *Client) CustomerBatchGet | NICEXAI | +| 获取视频号绑定状态 | GET | /cgi-bin/kf/get_corp_qualification | YES | (r *Client) GetCorpQualification | NICEXAI | + +### 客户联系 +[官方文档](https://developer.work.weixin.qq.com/document/path/92132/92133/92228) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +|:------------------------:| -------- |:-------------------------------------------------------------| ---------- | ------------------------------- |----------| +| 获取「联系客户统计」数据 | POST | /cgi-bin/externalcontact/get_user_behavior_data | YES | (r *Client) GetUserBehaviorData | MARKWANG | +| 获取「群聊数据统计」数据 (按群主聚合的方式) | POST | /cgi-bin/externalcontact/groupchat/statistic | YES | (r *Client) GetGroupChatStat | MARKWANG | +| 获取「群聊数据统计」数据 (按自然日聚合的方式) | POST | /cgi-bin/externalcontact/groupchat/statistic_group_by_day | YES | (r *Client) GetGroupChatStatByDay | MARKWANG | +| 配置客户联系「联系我」方式 | POST | /cgi-bin/externalcontact/add_contact_way | YES | (r *Client) AddContactWay | MARKWANG | +| 获取企业已配置的「联系我」方式 | POST | /cgi-bin/externalcontact/get_contact_way | YES | (r *Client) GetContactWay | MARKWANG | +| 更新企业已配置的「联系我」方式 | POST | /cgi-bin/externalcontact/update_contact_way | YES | (r *Client) UpdateContactWay | MARKWANG | +| 获取企业已配置的「联系我」列表 | POST | /cgi-bin/externalcontact/list_contact_way | YES | (r *Client) ListContactWay | MARKWANG | +| 删除企业已配置的「联系我」方式 | POST | /cgi-bin/externalcontact/del_contact_way | YES | (r *Client) DelContactWay | MARKWANG | +| 创建企业群发 | POST | /cgi-bin/externalcontact/add_msg_template | YES | (r *Client) AddMsgTemplate | MARKWANG | +| 获取群发记录列表 | POST | /cgi-bin/externalcontact/get_groupmsg_list_v2 | YES | (r *Client) GetGroupMsgListV2 | MARKWANG | +| 获取群发成员发送任务列表 | POST | /cgi-bin/externalcontact/get_groupmsg_task | YES | (r *Client) GetGroupMsgTask | MARKWANG | +| 获取企业群发成员执行结果 | POST | /cgi-bin/externalcontact/get_groupmsg_send_result | YES | (r *Client) GetGroupMsgSendResult | MARKWANG | +| 发送新客户欢迎语 | POST | /cgi-bin/externalcontact/send_welcome_msg | YES | (r *Client) SendWelcomeMsg | MARKWANG | +| 添加入群欢迎语素材 | POST | /cgi-bin/externalcontact/group_welcome_template/add | YES | (r *Client) AddGroupWelcomeTemplate | MARKWANG | +| 编辑入群欢迎语素材 | POST | /cgi-bin/externalcontact/group_welcome_template/edit | YES | (r *Client) EditGroupWelcomeTemplate | MARKWANG | +| 获取入群欢迎语素材 | POST | /cgi-bin/externalcontact/group_welcome_template/get | YES | (r *Client) GetGroupWelcomeTemplate | MARKWANG | +| 删除入群欢迎语素材 | POST | /cgi-bin/externalcontact/group_welcome_template/del | YES | (r *Client) DelGroupWelcomeTemplate | MARKWANG | + +## 通讯录管理 +[官方文档](https://developer.work.weixin.qq.com/document/path/90193) + +### 部门管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +|:---------:|------|:----------------------------------------| ---------- | ------------------------------- |----------| +| 获取子部门ID列表 | GET | /cgi-bin/department/simplelist | YES | (r *Client) DepartmentSimpleList| MARKWANG | +| 获取部门成员 | GET | /cgi-bin/user/simplelist | YES | (r *Client) UserSimpleList | MARKWANG | +| 获取成员ID列表 | Post | /cgi-bin/user/list_id | YES | (r *Client) UserListId | MARKWANG | + + +## 素材管理 +[官方文档](https://developer.work.weixin.qq.com/document/path/91054) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +|:---------:|------|:----------------------------------------| ---------- | ------------------------------- |----------| +| 上传图片 | POST | /cgi-bin/media/uploadimg | YES | (r *Client) UploadImg| MARKWANG | + +### 成员管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +| -------- | -------- | ----------------- | ---------- | ------------------- | -------- | +| 读取成员 | GET | /cgi-bin/user/get | YES | (r *Client) UserGet | chcthink | + + +## 群机器人 + +[官方文档](https://developer.work.weixin.qq.com/document/path/91770) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +| ---------------- | -------- | --------------------- | ---------- | -------------------------- | -------- | +| 群机器人发送消息 | POST | /cgi-bin/webhook/send | YES | (r *Client) RobotBroadcast | chcthink | + +## 打卡 + +[官方文档](https://developer.work.weixin.qq.com/document/path/96497) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +|----------| -------- | --------------------- | ---------- | -------------------------- |---------| +| 获取打卡日报数据 | POST | /cgi-bin/checkin/getcheckin_daydata | YES | (r *Client) GetDayData | Thinker | +| 获取打卡月报数据 | POST | /cgi-bin/checkin/getcheckin_monthdata | YES | (r *Client) GetMonthData | Thinker | + +## 应用管理 +TODO diff --git a/doc/api/wxpay.md b/doc/api/wxpay.md new file mode 100644 index 0000000..f686b51 --- /dev/null +++ b/doc/api/wxpay.md @@ -0,0 +1,3 @@ +# 微信支付 + +TODO diff --git a/domain/openapi/mgnt.go b/domain/openapi/mgnt.go new file mode 100644 index 0000000..02621d4 --- /dev/null +++ b/domain/openapi/mgnt.go @@ -0,0 +1,36 @@ +package openapi + +import "github.com/silenceper/wechat/v2/util" + +// GetAPIQuotaParams 查询API调用额度参数 +type GetAPIQuotaParams struct { + CgiPath string `json:"cgi_path"` // api的请求地址,例如"/cgi-bin/message/custom/send";不要前缀“https://api.weixin.qq.com” ,也不要漏了"/",否则都会76003的报错 +} + +// APIQuota API调用额度 +type APIQuota struct { + util.CommonError + Quota struct { + DailyLimit int64 `json:"daily_limit"` // 当天该账号可调用该接口的次数 + Used int64 `json:"used"` // 当天已经调用的次数 + Remain int64 `json:"remain"` // 当天剩余调用次数 + } `json:"quota"` // 详情 +} + +// GetRidInfoParams 查询rid信息参数 +type GetRidInfoParams struct { + Rid string `json:"rid"` // 调用接口报错返回的rid +} + +// RidInfo rid信息 +type RidInfo struct { + util.CommonError + Request struct { + InvokeTime int64 `json:"invoke_time"` // 发起请求的时间戳 + CostInMs int64 `json:"cost_in_ms"` // 请求毫秒级耗时 + RequestURL string `json:"request_url"` // 请求的URL参数 + RequestBody string `json:"request_body"` // post请求的请求参数 + ResponseBody string `json:"response_body"` // 接口请求返回参数 + ClientIP string `json:"client_ip"` // 接口请求的客户端ip + } `json:"request"` // 该rid对应的请求详情 +} diff --git a/go.mod b/go.mod index a019377..0180599 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,16 @@ module github.com/silenceper/wechat/v2 -go 1.14 +go 1.16 require ( - github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b + github.com/alicebob/miniredis/v2 v2.30.0 + github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d github.com/fatih/structs v1.1.0 - github.com/gomodule/redigo v1.8.4 - github.com/kr/text v0.2.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cast v1.3.1 - github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 - golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/h2non/gock.v1 v1.0.15 + github.com/go-redis/redis/v8 v8.11.5 + github.com/sirupsen/logrus v1.9.0 + github.com/spf13/cast v1.4.1 + github.com/stretchr/testify v1.7.1 + github.com/tidwall/gjson v1.14.1 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d + gopkg.in/h2non/gock.v1 v1.1.2 ) diff --git a/go.sum b/go.sum index 05a74f6..64deda9 100644 --- a/go.sum +++ b/go.sum @@ -1,50 +1,147 @@ -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M= +github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg= -github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= +github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= +github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= -gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/openapi/mgnt.go b/internal/openapi/mgnt.go new file mode 100644 index 0000000..eb5c438 --- /dev/null +++ b/internal/openapi/mgnt.go @@ -0,0 +1,127 @@ +package openapi + +import ( + "errors" + "fmt" + + "github.com/silenceper/wechat/v2/domain/openapi" + mpContext "github.com/silenceper/wechat/v2/miniprogram/context" + ocContext "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + clearQuotaURL = "https://api.weixin.qq.com/cgi-bin/clear_quota" // 重置API调用次数 + getAPIQuotaURL = "https://api.weixin.qq.com/cgi-bin/openapi/quota/get" // 查询API调用额度 + getRidInfoURL = "https://api.weixin.qq.com/cgi-bin/openapi/rid/get" // 查询rid信息 + clearQuotaByAppSecretURL = "https://api.weixin.qq.com/cgi-bin/clear_quota/v2" // 使用AppSecret重置 API 调用次数 +) + +// OpenAPI openApi管理 +type OpenAPI struct { + ctx interface{} +} + +// NewOpenAPI 实例化 +func NewOpenAPI(ctx interface{}) *OpenAPI { + return &OpenAPI{ctx: ctx} +} + +// ClearQuota 重置API调用次数 +// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/openApi-mgnt/clearQuota.html +func (o *OpenAPI) ClearQuota() error { + appID, _, err := o.getAppIDAndSecret() + if err != nil { + return err + } + + var payload = struct { + AppID string `json:"appid"` + }{ + AppID: appID, + } + res, err := o.doPostRequest(clearQuotaURL, payload) + if err != nil { + return err + } + + return util.DecodeWithCommonError(res, "ClearQuota") +} + +// GetAPIQuota 查询API调用额度 +// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/openApi-mgnt/getApiQuota.html +func (o *OpenAPI) GetAPIQuota(params openapi.GetAPIQuotaParams) (quota openapi.APIQuota, err error) { + res, err := o.doPostRequest(getAPIQuotaURL, params) + if err != nil { + return + } + + err = util.DecodeWithError(res, "a, "GetAPIQuota") + return +} + +// GetRidInfo 查询rid信息 +// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/openApi-mgnt/getRidInfo.html +func (o *OpenAPI) GetRidInfo(params openapi.GetRidInfoParams) (r openapi.RidInfo, err error) { + res, err := o.doPostRequest(getRidInfoURL, params) + if err != nil { + return + } + + err = util.DecodeWithError(res, &r, "GetRidInfo") + return +} + +// ClearQuotaByAppSecret 使用AppSecret重置 API 调用次数 +// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/openApi-mgnt/clearQuotaByAppSecret.html +func (o *OpenAPI) ClearQuotaByAppSecret() error { + id, secret, err := o.getAppIDAndSecret() + if err != nil { + return err + } + + uri := fmt.Sprintf("%s?appid=%s&appsecret=%s", clearQuotaByAppSecretURL, id, secret) + res, err := util.HTTPPost(uri, "") + if err != nil { + return err + } + + return util.DecodeWithCommonError(res, "ClearQuotaByAppSecret") +} + +// 获取 AppID 和 AppSecret +func (o *OpenAPI) getAppIDAndSecret() (string, string, error) { + switch o.ctx.(type) { + case *mpContext.Context: + c := o.ctx.(*mpContext.Context) + return c.AppID, c.AppSecret, nil + case *ocContext.Context: + c := o.ctx.(*ocContext.Context) + return c.AppID, c.AppSecret, nil + default: + return "", "", errors.New("invalid context type") + } +} + +// 获取 AccessToken +func (o *OpenAPI) getAccessToken() (string, error) { + switch o.ctx.(type) { + case *mpContext.Context: + return o.ctx.(*mpContext.Context).GetAccessToken() + case *ocContext.Context: + return o.ctx.(*ocContext.Context).GetAccessToken() + default: + return "", errors.New("invalid context type") + } +} + +// 创建 POST 请求 +func (o *OpenAPI) doPostRequest(uri string, payload interface{}) ([]byte, error) { + ak, err := o.getAccessToken() + if err != nil { + return nil, err + } + + uri = fmt.Sprintf("%s?access_token=%s", uri, ak) + return util.PostJSON(uri, payload) +} diff --git a/minigame/README.md b/minigame/README.md index b8efbe8..201beb8 100644 --- a/minigame/README.md +++ b/minigame/README.md @@ -3,6 +3,7 @@ [官方文档](https://developers.weixin.qq.com/minigame/dev/api-backend/) ## 快速入门 + ```go ``` \ No newline at end of file diff --git a/miniprogram/README.md b/miniprogram/README.md index f5424a5..925be2e 100644 --- a/miniprogram/README.md +++ b/miniprogram/README.md @@ -2,11 +2,12 @@ [官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/) - ## 包说明 -- analysis 数据分析相关API + +- analysis 数据分析相关 API ## 快速入门 + ```go wc := wechat.NewWechat() memory := cache.NewMemory() @@ -17,4 +18,36 @@ cfg := &miniConfig.Config{ } miniprogram := wc.GetMiniProgram(cfg) miniprogram.GetAnalysis().GetAnalysisDailyRetain() +``` + +### 小程序虚拟支付 +#### `注意:需要传入 Appkey、OfferID 的值` +相关文档:[小程序虚拟支付](https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html) +```go +wc := wechat.NewWechat() +miniprogram := wc.GetMiniProgram(&miniConfig.Config{ + AppID: "xxx", + AppSecret: "xxx", + AppKey: "xxx", + OfferID: "xxx", + Cache: cache.NewRedis(&redis.Options{ + Addr: "", + }), +}) +virtualPayment := miniprogram.GetVirtualPayment() +virtualPayment.SetSessionKey("xxx") +// 查询用户余额 +var ( + res *virtualPayment.QueryUserBalanceResponse + err error +) + +if res, err = virtualPayment.QueryUserBalance(context.TODO(), &virtualPayment.QueryUserBalanceRequest{ + OpenID: "xxx", + Env: virtualPayment.EnvProduction, + UserIP: "xxx", +}); err != nil { + panic(err) +} + ``` \ No newline at end of file diff --git a/miniprogram/analysis/analysis.go b/miniprogram/analysis/analysis.go index d3ca02c..55cfdec 100644 --- a/miniprogram/analysis/analysis.go +++ b/miniprogram/analysis/analysis.go @@ -32,12 +32,12 @@ const ( getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s" ) -//Analysis analyis 数据分析 +// Analysis analyis 数据分析 type Analysis struct { *context.Context } -//NewAnalysis new +// NewAnalysis new func NewAnalysis(ctx *context.Context) *Analysis { return &Analysis{ctx} } @@ -190,9 +190,9 @@ func (analysis *Analysis) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) // UserPortraitItem 用户画像项目 type UserPortraitItem struct { - ID int `json:"id"` // 属性值id - Name string `json:"name"` // 属性值名称 - AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景访问uv + ID int `json:"id"` // 属性值id + Name string `json:"name"` // 属性值名称 + Value int `json:"value"` // 该场景访问uv } // UserPortrait 用户画像 diff --git a/miniprogram/auth/auth.go b/miniprogram/auth/auth.go index 377e2b7..32c1493 100644 --- a/miniprogram/auth/auth.go +++ b/miniprogram/auth/auth.go @@ -1,6 +1,7 @@ package auth import ( + context2 "context" "encoding/json" "fmt" @@ -10,14 +11,18 @@ import ( const ( code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code" + + checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s" + + getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s" ) -//Auth 登录/用户信息 +// Auth 登录/用户信息 type Auth struct { *context.Context } -//NewAuth new auth +// NewAuth new auth func NewAuth(ctx *context.Context) *Auth { return &Auth{ctx} } @@ -31,16 +36,26 @@ type ResCode2Session struct { UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回 } -//Code2Session 登录凭证校验。 +// RspCheckEncryptedData . +type RspCheckEncryptedData struct { + util.CommonError + + Vaild bool `json:"vaild"` // 是否是合法的数据 + CreateTime uint64 `json:"create_time"` // 加密数据生成的时间戳 +} + +// Code2Session 登录凭证校验。 func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) { - urlStr := fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode) + return auth.Code2SessionContext(context2.Background(), jsCode) +} + +// Code2SessionContext 登录凭证校验。 +func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (result ResCode2Session, err error) { var response []byte - response, err = util.HTTPGet(urlStr) - if err != nil { + if response, err = util.HTTPGetContext(ctx, fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)); err != nil { return } - err = json.Unmarshal(response, &result) - if err != nil { + if err = json.Unmarshal(response, &result); err != nil { return } if result.ErrCode != 0 { @@ -50,7 +65,86 @@ func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error return } -//GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权 +// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权 func (auth *Auth) GetPaidUnionID() { - //TODO + // TODO +} + +// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据 +func (auth *Auth) CheckEncryptedData(encryptedMsgHash string) (result RspCheckEncryptedData, err error) { + return auth.CheckEncryptedDataContext(context2.Background(), encryptedMsgHash) +} + +// CheckEncryptedDataContext .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据 +func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHash string) (result RspCheckEncryptedData, err error) { + var response []byte + var ( + at string + ) + if at, err = auth.GetAccessToken(); err != nil { + return + } + + // 由于GetPhoneNumberContext需要传入JSON,所以HTTPPostContext入参改为[]byte + if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(checkEncryptedDataURL, at), []byte("encrypted_msg_hash="+encryptedMsgHash), nil); err != nil { + return + } + if err = util.DecodeWithError(response, &result, "CheckEncryptedDataAuth"); err != nil { + return + } + return +} + +// GetPhoneNumberResponse 新版获取用户手机号响应结构体 +type GetPhoneNumberResponse struct { + util.CommonError + + PhoneInfo PhoneInfo `json:"phone_info"` +} + +// PhoneInfo 获取用户手机号内容 +type PhoneInfo struct { + PhoneNumber string `json:"phoneNumber"` // 用户绑定的手机号 + PurePhoneNumber string `json:"purePhoneNumber"` // 没有区号的手机号 + CountryCode string `json:"countryCode"` // 区号 + WaterMark struct { + Timestamp int64 `json:"timestamp"` + AppID string `json:"appid"` + } `json:"watermark"` // 数据水印 +} + +// GetPhoneNumberContext 小程序通过code获取用户手机号 +func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*GetPhoneNumberResponse, error) { + var response []byte + var ( + at string + err error + ) + if at, err = auth.GetAccessToken(); err != nil { + return nil, err + } + body := map[string]interface{}{ + "code": code, + } + + bodyBytes, err := json.Marshal(body) + if err != nil { + return nil, err + } + + header := map[string]string{"Content-Type": "application/json;charset=utf-8"} + if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(getPhoneNumber, at), bodyBytes, header); err != nil { + return nil, err + } + + var result GetPhoneNumberResponse + if err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber"); err != nil { + return nil, err + } + return &result, nil +} + +// GetPhoneNumber 小程序通过code获取用户手机号 +func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) { + return auth.GetPhoneNumberContext(context2.Background(), code) } diff --git a/miniprogram/business/business.go b/miniprogram/business/business.go new file mode 100644 index 0000000..1b0d826 --- /dev/null +++ b/miniprogram/business/business.go @@ -0,0 +1,13 @@ +package business + +import "github.com/silenceper/wechat/v2/miniprogram/context" + +// Business 业务 +type Business struct { + *context.Context +} + +// NewBusiness init +func NewBusiness(ctx *context.Context) *Business { + return &Business{ctx} +} diff --git a/miniprogram/business/phone_number.go b/miniprogram/business/phone_number.go new file mode 100644 index 0000000..bf99057 --- /dev/null +++ b/miniprogram/business/phone_number.go @@ -0,0 +1,54 @@ +package business + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + getPhoneNumberURL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s" +) + +// GetPhoneNumberRequest 获取手机号请求 +type GetPhoneNumberRequest struct { + Code string `json:"code"` // 手机号获取凭证 +} + +// PhoneInfo 手机号信息 +type PhoneInfo struct { + PhoneNumber string `json:"phoneNumber"` // 用户绑定的手机号(国外手机号会有区号) + PurePhoneNumber string `json:"purePhoneNumber"` // 没有区号的手机号 + CountryCode string `json:"countryCode"` // 区号 + Watermark struct { + AppID string `json:"appid"` // 小程序appid + Timestamp int64 `json:"timestamp"` // 用户获取手机号操作的时间戳 + } `json:"watermark"` +} + +// GetPhoneNumber code换取用户手机号。 每个code只能使用一次,code的有效期为5min +func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneInfo, err error) { + accessToken, err := business.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getPhoneNumberURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + var resp struct { + util.CommonError + PhoneInfo PhoneInfo `json:"phone_info"` + } + err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber") + if nil != err { + return + } + + info = resp.PhoneInfo + return +} diff --git a/miniprogram/config/config.go b/miniprogram/config/config.go index 455d3e1..fb3e151 100644 --- a/miniprogram/config/config.go +++ b/miniprogram/config/config.go @@ -1,15 +1,17 @@ -// Package config 小程序config配置 +// Package config 小程序 config 配置 package config import ( "github.com/silenceper/wechat/v2/cache" ) -// Config config for 小程序 +// Config .config for 小程序 type Config struct { - AppID string `json:"app_id"` // app_id - AppSecret string `json:"app_secret"` // app_secret + AppID string `json:"app_id"` // appid + AppSecret string `json:"app_secret"` // appSecret + AppKey string `json:"app_key"` // appKey + OfferID string `json:"offer_id"` // offerId Token string `json:"token"` // token - EncodingAESKey string `json:"encoding_aes_key"` // encoding_aes_key + EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey Cache cache.Cache } diff --git a/miniprogram/content/content.go b/miniprogram/content/content.go new file mode 100644 index 0000000..d53f2db --- /dev/null +++ b/miniprogram/content/content.go @@ -0,0 +1,65 @@ +package content + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + checkTextURL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=%s" + checkImageURL = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=%s" +) + +// Content 内容安全 +type Content struct { + *context.Context +} + +// NewContent 内容安全接口 +func NewContent(ctx *context.Context) *Content { + return &Content{ctx} +} + +// CheckText 检测文字 +// @text 需要检测的文字 +// Deprecated +// 采用 security.MsgCheckV1 替代,返回值更加丰富 +func (content *Content) CheckText(text string) error { + accessToken, err := content.GetAccessToken() + if err != nil { + return err + } + response, err := util.PostJSON( + fmt.Sprintf(checkTextURL, accessToken), + map[string]string{ + "content": text, + }, + ) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "ContentCheckText") +} + +// CheckImage 检测图片 +// 所传参数为要检测的图片文件的绝对路径,图片格式支持PNG、JPEG、JPG、GIF, 像素不超过 750 x 1334,同时文件大小以不超过 300K 为宜,否则可能报错 +// @media 图片文件的绝对路径 +// Deprecated +// 采用 security.ImageCheckV1 替代,返回值更加丰富 +func (content *Content) CheckImage(media string) error { + accessToken, err := content.GetAccessToken() + if err != nil { + return err + } + response, err := util.PostFile( + "media", + media, + fmt.Sprintf(checkImageURL, accessToken), + ) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "ContentCheckImage") +} diff --git a/miniprogram/encryptor/encryptor.go b/miniprogram/encryptor/encryptor.go index bd8613f..af3c4c2 100644 --- a/miniprogram/encryptor/encryptor.go +++ b/miniprogram/encryptor/encryptor.go @@ -6,16 +6,17 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "github.com/silenceper/wechat/v2/miniprogram/context" ) -//Encryptor struct +// Encryptor struct type Encryptor struct { *context.Context } -//NewEncryptor 实例 +// NewEncryptor 实例 func NewEncryptor(context *context.Context) *Encryptor { basic := new(Encryptor) basic.Context = context @@ -90,6 +91,9 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) { if err != nil { return nil, err } + if len(ivBytes) != aes.BlockSize { + return nil, fmt.Errorf("bad iv length %d", len(ivBytes)) + } block, err := aes.NewCipher(aesKey) if err != nil { return nil, err diff --git a/miniprogram/encryptor/encryptor_test.go b/miniprogram/encryptor/encryptor_test.go new file mode 100644 index 0000000..38b518d --- /dev/null +++ b/miniprogram/encryptor/encryptor_test.go @@ -0,0 +1,15 @@ +package encryptor + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetCipherText_BadIV(t *testing.T) { + keyData := base64.StdEncoding.EncodeToString([]byte("1234567890123456")) + badData := base64.StdEncoding.EncodeToString([]byte("1")) + _, err := GetCipherText(keyData, badData, badData) + assert.Error(t, err) +} diff --git a/miniprogram/message/consts.go b/miniprogram/message/consts.go index 15f83ad..c80976c 100644 --- a/miniprogram/message/consts.go +++ b/miniprogram/message/consts.go @@ -12,16 +12,20 @@ type EventType string type InfoType string const ( - //MsgTypeText 文本消息 + // MsgTypeText 文本消息 MsgTypeText MsgType = "text" - //MsgTypeImage 图片消息 + // MsgTypeImage 图片消息 MsgTypeImage = "image" - //MsgTypeLink 图文链接 + // MsgTypeLink 图文链接 MsgTypeLink = "link" - //MsgTypeMiniProgramPage 小程序卡片 + // MsgTypeMiniProgramPage 小程序卡片 MsgTypeMiniProgramPage = "miniprogrampage" - //MsgTypeEvent 表示事件推送消息 - MsgTypeEvent = "event" + // MsgTypeEvent 事件 + MsgTypeEvent MsgType = "event" + // DataTypeXML XML格式数据 + DataTypeXML = "xml" + // DataTypeJSON JSON格式数据 + DataTypeJSON = "json" ) const ( @@ -87,6 +91,7 @@ type MiniProgramMixMessage struct { } `xml:"SubscribeMsgSentEvent"` } +// SubscribeMessageList 订阅消息事件列表 type SubscribeMessageList struct { TemplateId string `xml:"TemplateId"` SubscribeStatusString string `xml:"SubscribeStatusString"` diff --git a/miniprogram/message/customer_message.go b/miniprogram/message/customer_message.go index fe650d0..ce3830f 100644 --- a/miniprogram/message/customer_message.go +++ b/miniprogram/message/customer_message.go @@ -11,29 +11,29 @@ const ( customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send" ) -//Manager 消息管理者,可以发送消息 +// Manager 消息管理者,可以发送消息 type Manager struct { *context.Context } -//NewCustomerMessageManager 实例化消息管理者 +// NewCustomerMessageManager 实例化消息管理者 func NewCustomerMessageManager(context *context.Context) *Manager { return &Manager{ context, } } -//MediaText 文本消息的文字 +// MediaText 文本消息的文字 type MediaText struct { Content string `json:"content"` } -//MediaResource 消息使用的临时素材id +// MediaResource 消息使用的临时素材id type MediaResource struct { MediaID string `json:"media_id"` } -//MediaMiniprogrampage 小程序卡片 +// MediaMiniprogrampage 小程序卡片 type MediaMiniprogrampage struct { Title string `json:"title"` Appid string `json:"appid"` @@ -49,17 +49,17 @@ type MediaLink struct { ThumbURL string `json:"thumb_url"` } -//CustomerMessage 客服消息 +// CustomerMessage 客服消息 type CustomerMessage struct { - ToUser string `json:"touser"` //接受者OpenID - Msgtype MsgType `json:"msgtype"` //客服消息类型 - Text *MediaText `json:"text,omitempty"` //可选 - Image *MediaResource `json:"image,omitempty"` //可选 - Link *MediaLink `json:"link,omitempty"` //可选 - Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` //可选 + ToUser string `json:"touser"` // 接受者OpenID + Msgtype MsgType `json:"msgtype"` // 客服消息类型 + Text *MediaText `json:"text,omitempty"` // 可选 + Image *MediaResource `json:"image,omitempty"` // 可选 + Link *MediaLink `json:"link,omitempty"` // 可选 + Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` // 可选 } -//NewCustomerTextMessage 文本消息结构体构造方法 +// NewCustomerTextMessage 文本消息结构体构造方法 func NewCustomerTextMessage(toUser, text string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -70,7 +70,7 @@ func NewCustomerTextMessage(toUser, text string) *CustomerMessage { } } -//NewCustomerImgMessage 图片消息的构造方法 +// NewCustomerImgMessage 图片消息的构造方法 func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -81,7 +81,7 @@ func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage { } } -//NewCustomerLinkMessage 图文链接消息的构造方法 +// NewCustomerLinkMessage 图文链接消息的构造方法 func NewCustomerLinkMessage(toUser, title, description, url, thumbURL string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -95,7 +95,7 @@ func NewCustomerLinkMessage(toUser, title, description, url, thumbURL string) *C } } -//NewCustomerMiniprogrampageMessage 小程序卡片消息的构造方法 +// NewCustomerMiniprogrampageMessage 小程序卡片消息的构造方法 func NewCustomerMiniprogrampageMessage(toUser, title, pagepath, thumbMediaID string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -108,7 +108,7 @@ func NewCustomerMiniprogrampageMessage(toUser, title, pagepath, thumbMediaID str } } -//Send 发送客服消息 +// Send 发送客服消息 func (manager *Manager) Send(msg *CustomerMessage) error { accessToken, err := manager.Context.GetAccessToken() if err != nil { diff --git a/miniprogram/message/message.go b/miniprogram/message/message.go new file mode 100644 index 0000000..89e4b8d --- /dev/null +++ b/miniprogram/message/message.go @@ -0,0 +1,375 @@ +package message + +import ( + "encoding/json" + "encoding/xml" + "errors" + "io" + "net/http" + "sort" + "strings" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/miniprogram/security" + "github.com/silenceper/wechat/v2/util" +) + +// ConfirmReceiveMethod 确认收货方式 +type ConfirmReceiveMethod int8 + +const ( + // EventTypeTradeManageRemindAccessAPI 提醒接入发货信息管理服务API + // 小程序完成账期授权时/小程序产生第一笔交易时/已产生交易但从未发货的小程序,每天一次 + EventTypeTradeManageRemindAccessAPI EventType = "trade_manage_remind_access_api" + // EventTypeTradeManageRemindShipping 提醒需要上传发货信息 + // 曾经发过货的小程序,订单超过48小时未发货时 + EventTypeTradeManageRemindShipping EventType = "trade_manage_remind_shipping" + // EventTypeTradeManageOrderSettlement 订单将要结算或已经结算 + // 订单完成发货时/订单结算时 + EventTypeTradeManageOrderSettlement EventType = "trade_manage_order_settlement" + // EventTypeAddExpressPath 运单轨迹更新事件 + EventTypeAddExpressPath EventType = "add_express_path" + // EventTypeSecvodUpload 短剧媒资上传完成事件 + EventTypeSecvodUpload EventType = "secvod_upload_event" + // EventTypeSecvodAudit 短剧媒资审核状态事件 + EventTypeSecvodAudit EventType = "secvod_audit_event" + // EventTypeWxaMediaCheck 媒体内容安全异步审查结果通知 + EventTypeWxaMediaCheck EventType = "wxa_media_check" + // EventTypeXpayGoodsDeliverNotify 道具发货推送事件 + EventTypeXpayGoodsDeliverNotify EventType = "xpay_goods_deliver_notify" + // EventTypeXpayCoinPayNotify 代币支付推送事件 + EventTypeXpayCoinPayNotify EventType = "xpay_coin_pay_notify" + // ConfirmReceiveMethodAuto 自动确认收货 + ConfirmReceiveMethodAuto ConfirmReceiveMethod = 1 + // ConfirmReceiveMethodManual 手动确认收货 + ConfirmReceiveMethodManual ConfirmReceiveMethod = 2 +) + +// PushReceiver 接收消息推送 +// 暂仅支付Aes加密方式 +type PushReceiver struct { + *context.Context +} + +// NewPushReceiver 实例化 +func NewPushReceiver(ctx *context.Context) *PushReceiver { + return &PushReceiver{ + Context: ctx, + } +} + +// GetMsg 获取接收到的消息(如果是加密的返回解密数据) +func (receiver *PushReceiver) GetMsg(r *http.Request) (string, []byte, error) { + // 判断请求格式 + var dataType string + contentType := r.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "text/xml") { + // xml格式 + dataType = DataTypeXML + } else { + // json格式 + dataType = DataTypeJSON + } + + // 读取参数,验证签名 + signature := r.FormValue("signature") + timestamp := r.FormValue("timestamp") + nonce := r.FormValue("nonce") + encryptType := r.FormValue("encrypt_type") + // 验证签名 + tmpArr := []string{ + receiver.Token, + timestamp, + nonce, + } + sort.Strings(tmpArr) + tmpSignature := util.Signature(tmpArr...) + if tmpSignature != signature { + return dataType, nil, errors.New("signature error") + } + + if encryptType == "aes" { + // 解密 + var reqData DataReceived + if dataType == DataTypeXML { + if err := xml.NewDecoder(r.Body).Decode(&reqData); err != nil { + return dataType, nil, err + } + } else { + if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil { + return dataType, nil, err + } + } + _, rawMsgBytes, err := util.DecryptMsg(receiver.AppID, reqData.Encrypt, receiver.EncodingAESKey) + return dataType, rawMsgBytes, err + } + // 不加密 + byteData, err := io.ReadAll(r.Body) + return dataType, byteData, err +} + +// GetMsgData 获取接收到的消息(解密数据) +func (receiver *PushReceiver) GetMsgData(r *http.Request) (MsgType, EventType, PushData, error) { + dataType, decryptMsg, err := receiver.GetMsg(r) + if err != nil { + return "", "", nil, err + } + var ( + msgType MsgType + eventType EventType + ) + if dataType == DataTypeXML { + var commonToken CommonPushData + if err := xml.Unmarshal(decryptMsg, &commonToken); err != nil { + return "", "", nil, err + } + msgType, eventType = commonToken.MsgType, commonToken.Event + } else { + var commonToken CommonPushData + if err := json.Unmarshal(decryptMsg, &commonToken); err != nil { + return "", "", nil, err + } + msgType, eventType = commonToken.MsgType, commonToken.Event + } + if msgType == MsgTypeEvent { + pushData, err := receiver.getEvent(dataType, eventType, decryptMsg) + // 暂不支持其他事件类型 + return msgType, eventType, pushData, err + } + // 暂不支持其他消息类型 + return msgType, eventType, decryptMsg, nil +} + +// getEvent 获取事件推送的数据 +func (receiver *PushReceiver) getEvent(dataType string, eventType EventType, decryptMsg []byte) (PushData, error) { + switch eventType { + case EventTypeTradeManageRemindAccessAPI: + // 提醒接入发货信息管理服务API + var pushData PushDataRemindAccessAPI + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeTradeManageRemindShipping: + // 提醒需要上传发货信息 + var pushData PushDataRemindShipping + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeTradeManageOrderSettlement: + // 订单将要结算或已经结算 + var pushData PushDataOrderSettlement + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeWxaMediaCheck: + // 媒体内容安全异步审查结果通知 + var pushData MediaCheckAsyncData + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeAddExpressPath: + // 运单轨迹更新 + var pushData PushDataAddExpressPath + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeSecvodUpload: + // 短剧媒资上传完成 + var pushData PushDataSecVodUpload + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeSecvodAudit: + // 短剧媒资审核状态 + var pushData PushDataSecVodAudit + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeXpayGoodsDeliverNotify: + // 道具发货推送事件 + var pushData PushDataXpayGoodsDeliverNotify + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + case EventTypeXpayCoinPayNotify: + // 代币支付推送事件 + var pushData PushDataXpayCoinPayNotify + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + return &pushData, err + } + // 暂不支持其他事件类型,直接返回解密后的数据,由调用方处理 + return decryptMsg, nil +} + +// unmarshal 解析推送的数据 +func (receiver *PushReceiver) unmarshal(dateType string, decryptMsg []byte, pushData interface{}) error { + if dateType == DataTypeXML { + return xml.Unmarshal(decryptMsg, pushData) + } + return json.Unmarshal(decryptMsg, pushData) +} + +// DataReceived 接收到的数据 +type DataReceived struct { + Encrypt string `json:"Encrypt" xml:"Encrypt"` // 加密的消息体 +} + +// PushData 推送的数据(已转对应的结构体) +type PushData interface{} + +// CommonPushData 推送数据通用部分 +type CommonPushData struct { + XMLName xml.Name `json:"-" xml:"xml"` + MsgType MsgType `json:"MsgType" xml:"MsgType"` // 消息类型,为固定值 "event" + Event EventType `json:"Event" xml:"Event"` // 事件类型 + ToUserName string `json:"ToUserName" xml:"ToUserName"` // 小程序的原始 ID + FromUserName string `json:"FromUserName" xml:"FromUserName"` // 发送方账号(一个 OpenID,此时发送方是系统账号) + CreateTime int64 `json:"CreateTime" xml:"CreateTime"` // 消息创建时间 (整型),时间戳 +} + +// MediaCheckAsyncData 媒体内容安全异步审查结果通知 +type MediaCheckAsyncData struct { + CommonPushData + Appid string `json:"appid" xml:"appid"` + TraceID string `json:"trace_id" xml:"trace_id"` + Version int `json:"version" xml:"version"` + Detail []*MediaCheckDetail `json:"detail" xml:"detail"` + Errcode int `json:"errcode" xml:"errcode"` + Errmsg string `json:"errmsg" xml:"errmsg"` + Result MediaCheckAsyncResult `json:"result" xml:"result"` +} + +// MediaCheckDetail 检测结果详情 +type MediaCheckDetail struct { + Strategy string `json:"strategy" xml:"strategy"` + Errcode int `json:"errcode" xml:"errcode"` + Suggest security.CheckSuggest `json:"suggest" xml:"suggest"` + Label int `json:"label" xml:"label"` + Prob int `json:"prob" xml:"prob"` +} + +// MediaCheckAsyncResult 检测结果 +type MediaCheckAsyncResult struct { + Suggest security.CheckSuggest `json:"suggest" xml:"suggest"` + Label security.CheckLabel `json:"label" xml:"label"` +} + +// PushDataOrderSettlement 订单将要结算或已经结算通知 +type PushDataOrderSettlement struct { + CommonPushData + TransactionID string `json:"transaction_id" xml:"transaction_id"` // 支付订单号 + MerchantID string `json:"merchant_id" xml:"merchant_id"` // 商户号 + SubMerchantID string `json:"sub_merchant_id" xml:"sub_merchant_id"` // 子商户号 + MerchantTradeNo string `json:"merchant_trade_no" xml:"merchant_trade_no"` // 商户订单号 + PayTime int64 `json:"pay_time" xml:"pay_time"` // 支付成功时间,秒级时间戳 + ShippedTime int64 `json:"shipped_time" xml:"shipped_time"` // 发货时间,秒级时间戳 + EstimatedSettlementTime int64 `json:"estimated_settlement_time" xml:"estimated_settlement_time"` // 预计结算时间,秒级时间戳。发货时推送才有该字段 + ConfirmReceiveMethod ConfirmReceiveMethod `json:"confirm_receive_method" xml:"confirm_receive_method"` // 确认收货方式:1. 自动确认收货;2. 手动确认收货。结算时推送才有该字段 + ConfirmReceiveTime int64 `json:"confirm_receive_time" xml:"confirm_receive_time"` // 确认收货时间,秒级时间戳。结算时推送才有该字段 + SettlementTime int64 `json:"settlement_time" xml:"settlement_time"` // 订单结算时间,秒级时间戳。结算时推送才有该字段 +} + +// PushDataRemindShipping 提醒需要上传发货信息 +type PushDataRemindShipping struct { + CommonPushData + TransactionID string `json:"transaction_id" xml:"transaction_id"` // 微信支付订单号 + MerchantID string `json:"merchant_id" xml:"merchant_id"` // 商户号 + SubMerchantID string `json:"sub_merchant_id" xml:"sub_merchant_id"` // 子商户号 + MerchantTradeNo string `json:"merchant_trade_no" xml:"merchant_trade_no"` // 商户订单号 + PayTime int64 `json:"pay_time" xml:"pay_time"` // 支付成功时间,秒级时间戳 + Msg string `json:"msg" xml:"msg"` // 消息文本内容 +} + +// PushDataRemindAccessAPI 提醒接入发货信息管理服务API信息 +type PushDataRemindAccessAPI struct { + CommonPushData + Msg string `json:"msg" xml:"msg"` // 消息文本内容 +} + +// PushDataAddExpressPath 运单轨迹更新信息 +type PushDataAddExpressPath struct { + CommonPushData + DeliveryID string `json:"DeliveryID" xml:"DeliveryID"` // 快递公司ID + WayBillID string `json:"WaybillId" xml:"WaybillId"` // 运单ID + OrderID string `json:"OrderId" xml:"OrderId"` // 订单ID + Version int `json:"Version" xml:"Version"` // 轨迹版本号(整型) + Count int `json:"Count" xml:"Count"` // 轨迹节点数(整型) + Actions []*PushDataAddExpressPathAction `json:"Actions" xml:"Actions"` // 轨迹节点列表 +} + +// PushDataAddExpressPathAction 轨迹节点 +type PushDataAddExpressPathAction struct { + ActionTime int64 `json:"ActionTime" xml:"ActionTime"` // 轨迹节点 Unix 时间戳 + ActionType int `json:"ActionType" xml:"ActionType"` // 轨迹节点类型 + ActionMsg string `json:"ActionMsg" xml:"ActionMsg"` // 轨迹节点详情 +} + +// PushDataSecVodUpload 短剧媒资上传完成 +type PushDataSecVodUpload struct { + CommonPushData + UploadEvent SecVodUploadEvent `json:"upload_event" xml:"upload_event"` // 上传完成事件 +} + +// SecVodUploadEvent 短剧媒资上传完成事件 +type SecVodUploadEvent struct { + MediaID string `json:"media_id" xml:"media_id"` // 媒资id + SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值。 + Errcode int `json:"errcode" xml:"errcode"` // 错误码,上传失败时该值非 + Errmsg string `json:"errmsg" xml:"errmsg"` // 错误提示 +} + +// PushDataSecVodAudit 短剧媒资审核状态 +type PushDataSecVodAudit struct { + CommonPushData + AuditEvent SecVodAuditEvent `json:"audit_event" xml:"audit_event"` // 审核状态事件 +} + +// SecVodAuditEvent 短剧媒资审核状态事件 +type SecVodAuditEvent struct { + DramaID string `json:"drama_id" xml:"drama_id"` // 剧目id + SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值 + AuditDetail DramaAuditDetail `json:"audit_detail" xml:"audit_detail"` // 剧目审核结果,单独每一集的审核结果可以根据drama_id查询剧集详情得到 +} + +// DramaAuditDetail 剧目审核结果 +type DramaAuditDetail struct { + Status int `json:"status" xml:"status"` // 审核状态,0为无效值;1为审核中;2为最终失败;3为审核通过;4为驳回重填 + CreateTime int64 `json:"create_time" xml:"create_time"` // 提审时间戳 + AuditTime int64 `json:"audit_time" xml:"audit_time"` // 审核时间戳 +} + +// PushDataXpayGoodsDeliverNotify 道具发货推送 +type PushDataXpayGoodsDeliverNotify struct { + CommonPushData + OpenID string `json:"OpenId" xml:"OpenId"` // 用户openid + OutTradeNo string `json:"OutTradeNo" xml:"OutTradeNo"` // 业务订单号 + Env int `json:"Env" xml:"Env"` //,环境配置 0:现网环境(也叫正式环境)1:沙箱环境 + WeChatPayInfo WeChatPayInfo `json:"WeChatPayInfo" xml:"WeChatPayInfo"` // 微信支付信息 非微信支付渠道可能没有 + GoodsInfo GoodsInfo `json:"GoodsInfo" xml:"GoodsInfo"` // 道具参数信息 +} + +// WeChatPayInfo 微信支付信息 +type WeChatPayInfo struct { + MchOrderNo string `json:"MchOrderNo" xml:"MchOrderNo"` // 微信支付商户单号 + TransactionID string `json:"TransactionId" xml:"TransactionId"` // 交易单号(微信支付订单号) + PaidTime int64 `json:"PaidTime" xml:"PaidTime"` // 用户支付时间,Linux秒级时间戳 +} + +// GoodsInfo 道具参数信息 +type GoodsInfo struct { + ProductID string `json:"ProductId" xml:"ProductId"` // 道具ID + Quantity int `json:"Quantity" xml:"Quantity"` // 数量 + OrigPrice int64 `json:"OrigPrice" xml:"OrigPrice"` // 物品原始价格 (单位:分) + ActualPrice int64 `json:"ActualPrice" xml:"ActualPrice"` // 物品实际支付价格(单位:分) + Attach string `json:"Attach" xml:"Attach"` // 透传信息 +} + +// PushDataXpayCoinPayNotify 代币支付推送 +type PushDataXpayCoinPayNotify struct { + CommonPushData + OpenID string `json:"OpenId" xml:"OpenId"` // 用户openid + OutTradeNo string `json:"OutTradeNo" xml:"OutTradeNo"` // 业务订单号 + Env int `json:"Env" xml:"Env"` //,环境配置 0:现网环境(也叫正式环境)1:沙箱环境 + WeChatPayInfo WeChatPayInfo `json:"WeChatPayInfo" xml:"WeChatPayInfo"` // 微信支付信息 非微信支付渠道可能没有 + CoinInfo CoinInfo `json:"CoinInfo" xml:"CoinInfo"` // 代币参数信息 +} + +// CoinInfo 代币参数信息 +type CoinInfo struct { + Quantity int `json:"Quantity" xml:"Quantity"` // 数量 + OrigPrice int64 `json:"OrigPrice" xml:"OrigPrice"` // 物品原始价格 (单位:分) + ActualPrice int64 `json:"ActualPrice" xml:"ActualPrice"` // 物品实际支付价格(单位:分) + Attach string `json:"Attach" xml:"Attach"` // 透传信息 +} diff --git a/miniprogram/minidrama/constant.go b/miniprogram/minidrama/constant.go new file mode 100644 index 0000000..01198fd --- /dev/null +++ b/miniprogram/minidrama/constant.go @@ -0,0 +1,100 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +package minidrama + +const ( + // Success 错误码 0、成功 + Success ErrCode = 0 + // SystemError 错误码 -1、系统错误 + SystemError ErrCode = -1 + // InitError 错误码 -2 初始化未完成,请稍后再试 + InitError ErrCode = -2 + // FormatError 错误码 47001 输入格式错误 + FormatError ErrCode = 47001 + // ParamError 错误码 47003 参数不符合要求 + ParamError ErrCode = 47003 + // PostError 错误码 44002 POST 内容为空 + PostError ErrCode = 44002 + // MethodError 错误码 43002 HTTP 请求必须使用 POST 方法 + MethodError ErrCode = 43002 + // VideoTypeError 错误码 10090001 视频类型不支持 + VideoTypeError ErrCode = 10090001 + // ImageTypeError 错误码 10090002 图片类型不支持 + ImageTypeError ErrCode = 10090002 + // ImageURLError 错误码 10090003 图片 URL 无效 + ImageURLError ErrCode = 10090003 + // ResourceType 错误码 10090005 resource_type 无效 + ResourceType ErrCode = 10090005 + // OperationError 错误码 10093011 操作失败 + OperationError ErrCode = 10093011 + // ParamError2 错误码 10093014 参数错误(包括参数格式、类型等错误) + ParamError2 ErrCode = 10093014 + // OperationFrequentError 错误码 10093023 操作过于频繁 + OperationFrequentError ErrCode = 10093023 + // ResourceNotExistError 错误码 10093030 资源不存在 + ResourceNotExistError ErrCode = 10093030 +) + +const ( + // singleFileUpload 单个文件上传,上传媒体(和封面)文件,上传小文件(小于 10MB)时使用。上传大文件请使用分片上传接口。 + singleFileUpload = "https://api.weixin.qq.com/wxa/sec/vod/singlefileupload?access_token=" + + // pullUpload 拉取上传,该接口用于将一个网络上的视频拉取上传到平台。 + pullUpload = "https://api.weixin.qq.com/wxa/sec/vod/pullupload?access_token=" + + // getTask 查询任务,该接口用于查询拉取上传的任务状态。 + getTask = "https://api.weixin.qq.com/wxa/sec/vod/gettask?access_token=" + + // applyUpload 申请分片上传 + applyUpload = "https://api.weixin.qq.com/wxa/sec/vod/applyupload?access_token=" + + // uploadPart 上传分片 + uploadPart = "https://api.weixin.qq.com/wxa/sec/vod/uploadpart?access_token=" + + // commitUpload 确认上传,该接口用于完成整个分片上传流程,合并所有文件分片,确认媒体文件(和封面图片文件)上传到平台的结果,返回文件的 ID。请求中需要给出每一个分片的 part_number 和 etag,用来校验分片的准确性。 + commitUpload = "https://api.weixin.qq.com/wxa/sec/vod/commitupload?access_token=" + + // listMedia 获取媒体列表 + listMedia = "https://api.weixin.qq.com/wxa/sec/vod/listmedia?access_token=" + + // getMedia 获取媒资详细信息,该接口用于获取已上传到平台的指定媒资信息,用于开发者后台管理使用。用于给用户客户端播放的链接应该使用 getmedialink 接口获取。 + getMedia = "https://api.weixin.qq.com/wxa/sec/vod/getmedia?access_token=" + + // getMediaLink 获取媒资播放链接,该接口用于获取视频临时播放链接,用于给用户的播放使用。只有审核通过的视频才能通过该接口获取播放链接。 + getMediaLink = "https://api.weixin.qq.com/wxa/sec/vod/getmedialink?access_token=" + + // deleteMedia 删除媒体,该接口用于删除指定媒资。 + deleteMedia = "https://api.weixin.qq.com/wxa/sec/vod/deletemedia?access_token=" + + // auditDrama 审核剧本 + auditDrama = "https://api.weixin.qq.com/wxa/sec/vod/auditdrama?access_token=" + + // listDramas 获取剧目列表 + listDramas = "https://api.weixin.qq.com/wxa/sec/vod/listdramas?access_token=" + + // getDrama 获取剧目信息,该接口用于查询已提交的剧目。 + getDrama = "https://api.weixin.qq.com/wxa/sec/vod/getdrama?access_token=" + + // getCdnUsageData 查询 CDN 用量数据,该接口用于查询点播 CDN 的流量数据。 + getCdnUsageData = "https://api.weixin.qq.com/wxa/sec/vod/getcdnusagedata?access_token=" + + // getCdnLogs 查询 CDN 日志,该接口用于查询点播 CDN 的日志。 + getCdnLogs = "https://api.weixin.qq.com/wxa/sec/vod/getcdnlogs?access_token=" +) diff --git a/miniprogram/minidrama/doc.go b/miniprogram/minidrama/doc.go new file mode 100644 index 0000000..a74862c --- /dev/null +++ b/miniprogram/minidrama/doc.go @@ -0,0 +1,32 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +// Package minidrama Mini Program entertainment mini-drama related interface +package minidrama + +import ( + "github.com/silenceper/wechat/v2/miniprogram/context" +) + +// NewMiniDrama 实例化小程序娱乐直播 API +func NewMiniDrama(ctx *context.Context) *MiniDrama { + return &MiniDrama{ + ctx: ctx, + } +} diff --git a/miniprogram/minidrama/domain.go b/miniprogram/minidrama/domain.go new file mode 100644 index 0000000..eb63095 --- /dev/null +++ b/miniprogram/minidrama/domain.go @@ -0,0 +1,440 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +package minidrama + +import ( + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +// MiniDrama mini program entertainment live broadcast related +type MiniDrama struct { + ctx *context.Context +} + +// ErrCode error code +type ErrCode int + +// SingleFileUploadRequest 单文件上传请求 +// Content-Type 需要指定为 multipart/form-data; boundary= +// <箭头括号> 表示必须替换为有效值的变量。 +// 不填写 cover_type,cover_data 字段时默认截取视频首帧作为视频封面。 +type SingleFileUploadRequest struct { + MediaName string `json:"media_name"` // 媒体文件名称 文件名,需按照“剧目名 - 对应剧集数”格式命名文件,示例值:"我的演艺 - 第 1 集"。 + MediaType string `json:"media_type"` // 媒体文件类型 视频格式,支持:MP4,TS,MOV,MXF,MPG,FLV,WMV,AVI,M4V,F4V,MPEG,3GP,ASF,MKV,示例值:"MP4"。 + MediaData []byte `json:"media_data"` // 媒体文件数据 视频文件内容,二进制。 + CoverType string `json:"cover_type,omitempty"` // 视频封面图片格式,支持:JPG、JPEG、PNG、BMP、TIFF、AI、CDR、EPS、TIF,示例值:"JPG"。 + CoverData []byte `json:"cover_data,omitempty"` // 视频封面图片内容,二进制。 + SourceContext string `json:"source_context,omitempty"` // 来源上下文,会在上传完成事件中透传给开发者。 +} + +// SingleFileUploadResponse 单文件上传响应 +type SingleFileUploadResponse struct { + util.CommonError + MediaID int64 `json:"media_id"` // 媒体文件唯一标识,用于发布视频。 +} + +// PullUploadRequest 拉取上传请求 +// 不填写 cover_url 字段时默认截取视频首帧作为封面。 +// Content-Type 需要指定为 application/json +// 该接口为异步接口,上传完成会推送上传完成事件到开发者服务器,开发者也可以调用"查询任务"接口来轮询上传结果。 +type PullUploadRequest struct { + MediaName string `json:"media_name"` // 媒体文件名称 文件名,需按照“剧目名 - 对应剧集数”格式命名文件,示例值:"我的演艺 - 第 1 集"。 + MediaURL string `json:"media_url"` // 视频 URL,示例值:"https://developers.weixin.qq.com/test.mp4"。 + CoverURL string `json:"cover_url,omitempty"` // 视频封面 URL,示例值:"https://developers.weixin.qq.com/test.jpg"。 + SourceContext string `json:"source_context,omitempty"` // 来源上下文,会在上传完成事件中透传给开发者。 +} + +// PullUploadResponse 拉取上传响应 +type PullUploadResponse struct { + util.CommonError + TaskID int64 `json:"task_id"` // 任务 ID,用于查询拉取上传任务的结果。 +} + +// GetTaskRequest 查询任务请求 +// 该接口用于查询拉取上传的任务状态。 +// Content-Type 需要指定为 application/json。 +type GetTaskRequest struct { + TaskID int64 `json:"task_id"` // 任务 ID,用于查询拉取上传任务的结果。 +} + +// GetTaskResponse 查询任务响应 +type GetTaskResponse struct { + util.CommonError + TaskInfo TaskInfo `json:"task_info"` // 任务信息。 +} + +// TaskInfo 任务信息 +type TaskInfo struct { + ID int64 `json:"id"` // 任务 ID。 + TaskType int `json:"task_type"` // 任务类型,1:拉取上传任务。 + Status int `json:"status"` // 任务状态枚举值:1. 等待中;2. 正在处理;3. 已完成;4. 失败。 + ErrCode int `json:"errcode"` // 任务错误码,0 表示成功,其它表示失败。 + ErrMsg string `json:"errmsg"` // 任务错误原因。 + CreateTime int64 `json:"create_time"` // 任务创建时间,时间戳,单位:秒。 + FinishTime int64 `json:"finish_time"` // 任务完成时间,时间戳,单位:秒。 + MediaID int64 `json:"media_id"` // 媒体文件唯一标识,用于发布视频。 +} + +// ApplyUploadRequest 申请上传请求 +// 上传大文件时需使用分片上传方式,分为 3 个步骤: +// +// 申请分片上传,确定文件名、格式类型,返回 upload_id,唯一标识本次分片上传。 +// 上传分片,多次调用上传文件分片,需要携带 part_number 和 upload_id,其中 part_number 为分片的编号,支持乱序上传。当传入 part_number 和 upload_id 都相同的时候,后发起上传请求的分片将覆盖之前的分片。 +// 确认分片上传,当上传完所有分片后,需要完成整个文件的合并。请求体中需要给出每一个分片的 part_number 和 etag,用来校验分片的准确性,最后返回文件的 media_id。 +// 如果填写了 cover_type,表明本次分片上传除上传媒体文件外还需要上传封面图片,不填写 cover_type 则默认截取视频首帧作为封面。 +// Content-Type 需要指定为 application/json。 +type ApplyUploadRequest struct { + MediaName string `json:"media_name"` // 媒体文件名称 文件名,需按照“剧目名 - 对应剧集数”格式命名文件,示例值:"我的演艺 - 第 1 集"。 + MediaType string `json:"media_type"` // 媒体文件类型 视频格式,支持:MP4,TS,MOV,MXF,MPG,FLV,WMV,AVI,M4V,F4V,MPEG,3GP,ASF,MKV,示例值:"MP4"。 + CoverType string `json:"cover_type,omitempty"` // 视频封面图片格式,支持:JPG、JPEG、PNG、BMP、TIFF、AI、CDR、EPS、TIF,示例值:"JPG"。 + SourceContext string `json:"source_context,omitempty"` // 来源上下文,会在上传完成事件中透传给开发者。 +} + +// ApplyUploadResponse 申请上传响应 +type ApplyUploadResponse struct { + util.CommonError + UploadID string `json:"upload_id"` // 本次分片上传的唯一标识。 +} + +// UploadPartRequest 上传分片请求 +// 将文件的其中一个分片上传到平台,最多支持 100 个分片,每个分片大小为 5MB,最后一个分片可以小于 5MB。该接口适用于视频和封面图片。视频最大支持 500MB,封面图片最大支持 10MB。 +// 调用该接口之前必须先调用申请分片上传接口。 +// 在申请分片上传时,如果不填写 cover_type,则默认截取视频首帧作为封面。 +// Content-Type 需要指定为 multipart/form-data; boundary=,<箭头括号>表示必须替换为有效值的变量。 +// part_number 从 1 开始。如除了上传视频外还需要上传封面图片,则封面图片的 part_number 需重新从 1 开始编号。 +type UploadPartRequest struct { + UploadID string `json:"upload_id"` // 一次分片上传的唯一标识,由申请分片上传接口返回。 + PartNumber int `json:"part_number"` // 本次上传的分片的编号,范围在 1 - 100。 + ResourceType int `json:"resource_type"` // 指定该分片属于视频还是封面图片的枚举值:1. 视频,2. 封面图片。 + Data []byte `json:"data"` // 分片内容,二进制。 +} + +// UploadPartResponse 上传分片响应 +type UploadPartResponse struct { + util.CommonError + ETag string `json:"etag"` // 上传分片成功后返回的分片标识,用于后续确认分片上传接口。 +} + +// CommitUploadRequest 确认分片上传请求 +// 该接口用于完成整个分片上传流程,合并所有文件分片,确认媒体文件(和封面图片文件)上传到平台的结果,返回文件的 ID。请求中需要给出每一个分片的 part_number 和 etag,用来校验分片的准确性。 +// 注意事项 +// Content-Type 需要指定为 application/json。 +// 调用该接口之前必须先调用申请分片上传接口以及上传分片接口。 +// 如本次分片上传除上传媒体文件外还需要上传封面图片,则请求中还需提供 cover_part_infos 字段以用于合并封面图片文件分片。 +// 请求中 media_part_infos 和 cover_part_infos 字段必须按 part_number 从小到大排序,part_number 必须从 1 开始,连续且不重复。 +type CommitUploadRequest struct { + UploadID string `json:"upload_id"` + MediaPartInfos []*PartInfo `json:"media_part_infos"` + CoverPartInfos []*PartInfo `json:"cover_part_infos,omitempty"` +} + +// PartInfo 分片信息 +type PartInfo struct { + PartNumber int `json:"part_number"` // 分片编号。 + Etag string `json:"etag"` // 使用上传分片接口上传成功后返回的 etag 的值 +} + +// CommitUploadResponse 确认分片上传响应 +type CommitUploadResponse struct { + util.CommonError + MediaID int64 `json:"media_id"` // 媒体文件唯一标识,用于发布视频。 +} + +// ListMediaRequest 查询媒体列表请求 +// 该接口用于查询已经上传到平台的媒体文件列表。 +// 注意事项 +// Content-Type 需要指定为 application/json。 +// 本接口返回的视频或图片链接均为临时链接,不应将其保存下来。 +// media_name 参数支持模糊匹配,当需要模糊匹配时可以在前面或后面加上 %,否则为精确匹配。例如 "test%" 可以匹配到 "test123", "testxxx", "test"。 +// 调用方式 +type ListMediaRequest struct { + DramaID int64 `json:"drama_id,omitempty"` // 剧目 ID,可通过查询剧目列表接口获取。 + MediaName string `json:"media_name,omitempty"` // 媒体文件名称,可通过查询媒体列表接口获取,模糊匹配。 + StartTime int64 `json:"start_time,omitempty"` // 媒资上传时间>=start_time,Unix 时间戳,单位:秒。 + EndTime int64 `json:"end_time,omitempty"` // 媒资上传时间,<箭头括号>表示必须替换为有效值的变量。 +func (s *MiniDrama) UploadPart(ctx context.Context, in *UploadPartRequest) (out UploadPartResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, uploadPart); err != nil { + return + } + + var ( + fields = []util.MultipartFormField{ + { + IsFile: true, + Fieldname: "data", + Filename: string(in.Data), + }, { + IsFile: false, + Fieldname: "upload_id", + Value: []byte(in.UploadID), + }, { + IsFile: false, + Fieldname: "part_number", + Value: []byte(strconv.Itoa(in.PartNumber)), + }, { + IsFile: false, + Fieldname: "resource_type", + Value: []byte(strconv.Itoa(in.PartNumber)), + }, + } + response []byte + ) + if response, err = util.PostMultipartForm(fields, address); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "UploadPart") + return +} + +// CommitUpload 确认上传 +func (s *MiniDrama) CommitUpload(ctx context.Context, in *CommitUploadRequest) (out CommitUploadResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, commitUpload); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "CommitUpload") + return +} + +// ListMedia 获取媒体列表 +func (s *MiniDrama) ListMedia(ctx context.Context, in *ListMediaRequest) (out ListMediaResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, listMedia); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "ListMedia") + return +} + +// GetMedia 获取媒资详细信息 +func (s *MiniDrama) GetMedia(ctx context.Context, in *GetMediaRequest) (out GetMediaResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, getMedia); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "GetMedia") + return +} + +// GetMediaLink 获取媒资播放链接 +func (s *MiniDrama) GetMediaLink(ctx context.Context, in *GetMediaLinkRequest) (out GetMediaLinkResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, getMediaLink); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "GetMediaLink") + return +} + +// DeleteMedia 删除媒体 +func (s *MiniDrama) DeleteMedia(ctx context.Context, in *DeleteMediaRequest) (out DeleteMediaResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, deleteMedia); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "DeleteMedia") + return +} + +// AuditDrama 审核剧本 +func (s *MiniDrama) AuditDrama(ctx context.Context, in *AuditDramaRequest) (out AuditDramaResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, auditDrama); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "AuditDrama") + return +} + +// ListDramas 获取剧目列表 +func (s *MiniDrama) ListDramas(ctx context.Context, in *ListDramasRequest) (out ListDramasResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, listDramas); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "ListDramas") + return +} + +// GetDrama 获取剧目信息 +func (s *MiniDrama) GetDrama(ctx context.Context, in *GetDramaRequest) (out GetDramaResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, getDrama); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "GetDrama") + return +} + +// GetCdnUsageData 查询 CDN 用量数据 +func (s *MiniDrama) GetCdnUsageData(ctx context.Context, in *GetCdnUsageDataRequest) (out GetCdnUsageDataResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, getCdnUsageData); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "GetCdnUsageData") + return +} + +// GetCdnLogs 查询 CDN 日志 +func (s *MiniDrama) GetCdnLogs(ctx context.Context, in *GetCdnLogsRequest) (out GetCdnLogsResponse, err error) { + var address string + if address, err = s.requestAddress(ctx, getCdnLogs); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "GetCdnLogs") + return +} + +// requestAddress 请求地址 +func (s *MiniDrama) requestAddress(_ context.Context, url string) (string, error) { + accessToken, err := s.ctx.GetAccessToken() + if err != nil { + return "", err + } + return url + accessToken, nil +} diff --git a/miniprogram/miniprogram.go b/miniprogram/miniprogram.go index f3c69be..15d0ed2 100644 --- a/miniprogram/miniprogram.go +++ b/miniprogram/miniprogram.go @@ -2,26 +2,38 @@ package miniprogram import ( "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/internal/openapi" "github.com/silenceper/wechat/v2/miniprogram/analysis" "github.com/silenceper/wechat/v2/miniprogram/auth" + "github.com/silenceper/wechat/v2/miniprogram/business" "github.com/silenceper/wechat/v2/miniprogram/config" + "github.com/silenceper/wechat/v2/miniprogram/content" "github.com/silenceper/wechat/v2/miniprogram/context" "github.com/silenceper/wechat/v2/miniprogram/encryptor" "github.com/silenceper/wechat/v2/miniprogram/message" + "github.com/silenceper/wechat/v2/miniprogram/minidrama" + "github.com/silenceper/wechat/v2/miniprogram/order" + "github.com/silenceper/wechat/v2/miniprogram/privacy" "github.com/silenceper/wechat/v2/miniprogram/qrcode" "github.com/silenceper/wechat/v2/miniprogram/server" + "github.com/silenceper/wechat/v2/miniprogram/riskcontrol" + "github.com/silenceper/wechat/v2/miniprogram/security" + "github.com/silenceper/wechat/v2/miniprogram/shortlink" "github.com/silenceper/wechat/v2/miniprogram/subscribe" "github.com/silenceper/wechat/v2/miniprogram/tcb" + "github.com/silenceper/wechat/v2/miniprogram/urllink" + "github.com/silenceper/wechat/v2/miniprogram/urlscheme" + "github.com/silenceper/wechat/v2/miniprogram/virtualpayment" "github.com/silenceper/wechat/v2/miniprogram/werun" "net/http" ) -//MiniProgram 微信小程序相关API +// MiniProgram 微信小程序相关 API type MiniProgram struct { ctx *context.Context } -//NewMiniProgram 实例化小程序API +// NewMiniProgram 实例化小程序 API func NewMiniProgram(cfg *config.Config) *MiniProgram { defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyMiniProgramPrefix, cfg.Cache) ctx := &context.Context{ @@ -31,7 +43,7 @@ func NewMiniProgram(cfg *config.Config) *MiniProgram { return &MiniProgram{ctx} } -//SetAccessTokenHandle 自定义access_token获取方式 +// SetAccessTokenHandle 自定义 access_token 获取方式 func (miniProgram *MiniProgram) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) { miniProgram.ctx.AccessTokenHandle = accessTokenHandle } @@ -46,27 +58,37 @@ func (miniProgram *MiniProgram) GetEncryptor() *encryptor.Encryptor { return encryptor.NewEncryptor(miniProgram.ctx) } -//GetAuth 登录/用户信息相关接口 +// GetAuth 登录/用户信息相关接口 func (miniProgram *MiniProgram) GetAuth() *auth.Auth { return auth.NewAuth(miniProgram.ctx) } -//GetAnalysis 数据分析 +// GetAnalysis 数据分析 func (miniProgram *MiniProgram) GetAnalysis() *analysis.Analysis { return analysis.NewAnalysis(miniProgram.ctx) } -//GetQRCode 小程序码相关API +// GetBusiness 业务接口 +func (miniProgram *MiniProgram) GetBusiness() *business.Business { + return business.NewBusiness(miniProgram.ctx) +} + +// GetPrivacy 小程序隐私协议相关 API +func (miniProgram *MiniProgram) GetPrivacy() *privacy.Privacy { + return privacy.NewPrivacy(miniProgram.ctx) +} + +// GetQRCode 小程序码相关 API func (miniProgram *MiniProgram) GetQRCode() *qrcode.QRCode { return qrcode.NewQRCode(miniProgram.ctx) } -//GetTcb 小程序云开发API +// GetTcb 小程序云开发 API func (miniProgram *MiniProgram) GetTcb() *tcb.Tcb { return tcb.NewTcb(miniProgram.ctx) } -//GetSubscribe 小程序订阅消息 +// GetSubscribe 小程序订阅消息 func (miniProgram *MiniProgram) GetSubscribe() *subscribe.Subscribe { return subscribe.NewSubscribe(miniProgram.ctx) } @@ -88,3 +110,58 @@ func (miniProgram *MiniProgram) GetServer(req *http.Request, write http.Response srv.Write = write return srv } + +// GetContentSecurity 内容安全接口 +func (miniProgram *MiniProgram) GetContentSecurity() *content.Content { + return content.NewContent(miniProgram.ctx) +} + +// GetURLLink 小程序 URL Link 接口 +func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink { + return urllink.NewURLLink(miniProgram.ctx) +} + +// GetRiskControl 安全风控接口 +func (miniProgram *MiniProgram) GetRiskControl() *riskcontrol.RiskControl { + return riskcontrol.NewRiskControl(miniProgram.ctx) +} + +// GetSecurity 内容安全接口 +func (miniProgram *MiniProgram) GetSecurity() *security.Security { + return security.NewSecurity(miniProgram.ctx) +} + +// GetShortLink 小程序短链接口 +func (miniProgram *MiniProgram) GetShortLink() *shortlink.ShortLink { + return shortlink.NewShortLink(miniProgram.ctx) +} + +// GetSURLScheme 小程序 URL Scheme 接口 +func (miniProgram *MiniProgram) GetSURLScheme() *urlscheme.URLScheme { + return urlscheme.NewURLScheme(miniProgram.ctx) +} + +// GetOpenAPI openApi 管理接口 +func (miniProgram *MiniProgram) GetOpenAPI() *openapi.OpenAPI { + return openapi.NewOpenAPI(miniProgram.ctx) +} + +// GetVirtualPayment 小程序虚拟支付 +func (miniProgram *MiniProgram) GetVirtualPayment() *virtualpayment.VirtualPayment { + return virtualpayment.NewVirtualPayment(miniProgram.ctx) +} + +// GetMessageReceiver 获取消息推送接收器 +func (miniProgram *MiniProgram) GetMessageReceiver() *message.PushReceiver { + return message.NewPushReceiver(miniProgram.ctx) +} + +// GetShipping 小程序发货信息管理服务 +func (miniProgram *MiniProgram) GetShipping() *order.Shipping { + return order.NewShipping(miniProgram.ctx) +} + +// GetMiniDrama 小程序娱乐微短剧 +func (miniProgram *MiniProgram) GetMiniDrama() *minidrama.MiniDrama { + return minidrama.NewMiniDrama(miniProgram.ctx) +} diff --git a/miniprogram/order/shipping.go b/miniprogram/order/shipping.go new file mode 100644 index 0000000..b47f9d4 --- /dev/null +++ b/miniprogram/order/shipping.go @@ -0,0 +1,269 @@ +package order + +import ( + "fmt" + "time" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 发货信息录入 + uploadShippingInfoURL = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=%s" + + // 查询订单发货状态 + getShippingOrderURL = "https://api.weixin.qq.com/wxa/sec/order/get_order?access_token=%s" + + // 查询订单列表 + getShippingOrderListURL = "https://api.weixin.qq.com/wxa/sec/order/get_order_list?access_token=%s" + + // 确认收货提醒接口 + notifyConfirmReceiveURL = "https://api.weixin.qq.com/wxa/sec/order/notify_confirm_receive?access_token=%s" +) + +// Shipping 发货信息管理 +type Shipping struct { + *context.Context +} + +// NewShipping init +func NewShipping(ctx *context.Context) *Shipping { + return &Shipping{ctx} +} + +// UploadShippingInfo 发货信息录入 +// see https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html +func (shipping *Shipping) UploadShippingInfo(in *UploadShippingInfoRequest) (err error) { + accessToken, err := shipping.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(uploadShippingInfoURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + return util.DecodeWithCommonError(response, "UploadShippingInfo") +} + +// GetShippingOrder 查询订单发货状态 +func (shipping *Shipping) GetShippingOrder(in *GetShippingOrderRequest) (res ShippingOrderResponse, err error) { + accessToken, err := shipping.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getShippingOrderURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + err = util.DecodeWithError(response, &res, "GetShippingOrder") + return +} + +// GetShippingOrderList 查询订单列表 +func (shipping *Shipping) GetShippingOrderList(in *GetShippingOrderListRequest) (res GetShippingOrderListResponse, err error) { + accessToken, err := shipping.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getShippingOrderListURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + err = util.DecodeWithError(response, &res, "GetShippingOrderList") + return +} + +// NotifyConfirmReceive 确认收货提醒接口 +func (shipping *Shipping) NotifyConfirmReceive(in *NotifyConfirmReceiveRequest) (err error) { + accessToken, err := shipping.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(notifyConfirmReceiveURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + return util.DecodeWithCommonError(response, "NotifyConfirmReceive") +} + +// UploadShippingInfoRequest 发货信息录入请求参数 +type UploadShippingInfoRequest struct { + OrderKey *ShippingOrderKey `json:"order_key"` // 订单,需要上传物流信息的订单 + LogisticsType LogisticsType `json:"logistics_type"` // 物流模式 + DeliveryMode DeliveryMode `json:"delivery_mode"` // 发货模式 + IsAllDelivered bool `json:"is_all_delivered"` // 分拆发货模式时必填,用于标识分拆发货模式下是否已全部发货完成 + ShippingList []*ShippingInfo `json:"shipping_list"` // 物流信息列表,发货物流单列表,支持统一发货(单个物流单)和分拆发货(多个物流单)两种模式 + UploadTime *time.Time `json:"upload_time"` // 上传时间,用于标识请求的先后顺序 + Payer *ShippingPayer `json:"payer"` // 支付人信息 +} + +// ShippingOrderKey 订单 +type ShippingOrderKey struct { + OrderNumberType NumberType `json:"order_number_type"` // 订单单号类型,用于确认需要上传详情的订单。枚举值1,使用下单商户号和商户侧单号;枚举值2,使用微信支付单号。 + TransactionID string `json:"transaction_id"` // 原支付交易对应的微信订单号 + Mchid string `json:"mchid"` // 支付下单商户的商户号,由微信支付生成并下发 + OutTradeNo string `json:"out_trade_no"` // 商户系统内部订单号,只能是数字、大小写字母`_-*`且在同一个商户号下唯一 +} + +// ShippingPayer 支付者信息 +type ShippingPayer struct { + Openid string `json:"openid"` // 用户标识,用户在小程序appid下的唯一标识 +} + +// ShippingInfo 物流信息 +type ShippingInfo struct { + TrackingNo string `json:"tracking_no"` // 物流单号,物流快递发货时必填 + ExpressCompany string `json:"express_company"` // 物流公司编码,快递公司ID,物流快递发货时必填;参见「查询物流公司编码列表」 + ItemDesc string `json:"item_desc"` // 商品信息,例如:微信红包抱枕*1个,限120个字以内 + Contact ShippingContact `json:"contact"` // 联系方式,当发货的物流公司为顺丰时,联系方式为必填,收件人或寄件人联系方式二选一 +} + +// ShippingContact 联系方式 +type ShippingContact struct { + ConsignorContact string `json:"consignor_contact"` // 寄件人联系方式,寄件人联系方式,采用掩码传输,最后4位数字不能打掩码 + ReceiverContact string `json:"receiver_contact"` // 收件人联系方式,收件人联系方式,采用掩码传输,最后4位数字不能打掩码 +} + +// DeliveryMode 发货模式 +type DeliveryMode uint8 + +const ( + // DeliveryModeUnifiedDelivery 统一发货 + DeliveryModeUnifiedDelivery DeliveryMode = 1 + // DeliveryModeSplitDelivery 分拆发货 + DeliveryModeSplitDelivery DeliveryMode = 2 +) + +// LogisticsType 物流模式 +type LogisticsType uint8 + +const ( + // LogisticsTypeExpress 实体物流配送采用快递公司进行实体物流配送形式 + LogisticsTypeExpress LogisticsType = 1 + // LogisticsTypeSameCity 同城配送 + LogisticsTypeSameCity LogisticsType = 2 + // LogisticsTypeVirtual 虚拟商品,虚拟商品,例如话费充值,点卡等,无实体配送形式 + LogisticsTypeVirtual LogisticsType = 3 + // LogisticsTypeSelfPickup 用户自提 + LogisticsTypeSelfPickup LogisticsType = 4 +) + +// NumberType 订单单号类型 +type NumberType uint8 + +const ( + // NumberTypeOutTradeNo 使用下单商户号和商户侧单号 + NumberTypeOutTradeNo NumberType = 1 + // NumberTypeTransactionID 使用微信支付单号 + NumberTypeTransactionID NumberType = 2 +) + +// GetShippingOrderRequest 查询订单发货状态参数 +type GetShippingOrderRequest struct { + TransactionID string `json:"transaction_id"` // 原支付交易对应的微信订单号 + MerchantID string `json:"merchant_id"` // 支付下单商户的商户号,由微信支付生成并下发 + SubMerchantID string `json:"sub_merchant_id"` //二级商户号 + MerchantTradeNo string `json:"merchant_trade_no"` //商户系统内部订单号,只能是数字、大小写字母`_-*`且在同一个商户号下唯一。 +} + +// ShippingItem 物流信息 +type ShippingItem struct { + TrackingNo string `json:"tracking_no"` // 物流单号,示例值: "323244567777 + ExpressCompany string `json:"express_company"` // 物流公司编码,快递公司ID,物流快递发货时必填;参见「查询物流公司编码列表」 + UploadTime int64 `json:"upload_time"` // 上传物流信息时间,时间戳形式 +} + +// ShippingDetail 发货信息 +type ShippingDetail struct { + DeliveryMode DeliveryMode `json:"delivery_mode"` // 发货模式 + LogisticsType LogisticsType `json:"logistics_type"` // 物流模式 + FinishShipping bool `json:"finish_shipping"` // 是否已全部发货 + FinishShippingCount int `json:"finish_shipping_count"` // 已完成全部发货的次数 + GoodsDesc string `json:"goods_desc"` // 在小程序后台发货信息录入页录入的商品描述 + ShippingList []*ShippingItem `json:"shipping_list"` // 物流信息列表 +} + +// ShippingOrder 订单发货状态 +type ShippingOrder struct { + TransactionID string `json:"transaction_id"` // 原支付交易对应的微信订单号 + MerchantTradeNo string `json:"merchant_trade_no"` // 商户系统内部订单号,只能是数字、大小写字母`_-*`且在同一个商户号下唯一 + MerchantID string `json:"merchant_id"` // 支付下单商户的商户号,由微信支付生成并下发 + SubMerchantID string `json:"sub_merchant_id"` // 二级商户号 + Description string `json:"description"` // 以分号连接的该支付单的所有商品描述,当超过120字时自动截断并以 “...” 结尾 + PaidAmount int64 `json:"paid_amount"` // 支付单实际支付金额,整型,单位:分钱 + Openid string `json:"openid"` // 支付者openid + TradeCreateTime int64 `json:"trade_create_time"` // 交易创建时间,时间戳形式 + PayTime int64 `json:"pay_time"` // 支付时间,时间戳形式 + InComplaint bool `json:"in_complaint"` // 是否处在交易纠纷中 + OrderState State `json:"order_state"` // 订单状态枚举:(1) 待发货;(2) 已发货;(3) 确认收货;(4) 交易完成;(5) 已退款 + Shipping *ShippingDetail `json:"shipping"` // 订单发货信息 +} + +// ShippingOrderResponse 查询订单发货状态返回参数 +type ShippingOrderResponse struct { + util.CommonError + Order ShippingOrder `json:"order"` // 订单发货信息 +} + +// State 订单状态 +type State uint8 + +const ( + // StateWaitShipment 待发货 + StateWaitShipment State = 1 + // StateShipped 已发货 + StateShipped State = 2 + // StateConfirm 确认收货 + StateConfirm State = 3 + // StateComplete 交易完成 + StateComplete State = 4 + // StateRefund 已退款 + StateRefund State = 5 +) + +// GetShippingOrderListRequest 查询订单列表请求参数 +type GetShippingOrderListRequest struct { + PayTimeRange *TimeRange `json:"pay_time_range"` // 支付时间范围 + OrderState State `json:"order_state,omitempty"` // 订单状态 + Openid string `json:"openid,omitempty"` // 支付者openid + LastIndex string `json:"last_index,omitempty"` // 翻页时使用,获取第一页时不用传入,如果查询结果中 has_more 字段为 true,则传入该次查询结果中返回的 last_index 字段可获取下一页 + PageSize int64 `json:"page_size"` // 每页数量,最多50条 +} + +// TimeRange 时间范围 +type TimeRange struct { + BeginTime int64 `json:"begin_time,omitempty"` // 查询开始时间,时间戳形式 + EndTime int64 `json:"end_time,omitempty"` // 查询结束时间,时间戳形式 +} + +// GetShippingOrderListResponse 查询订单列表返回参数 +type GetShippingOrderListResponse struct { + util.CommonError + OrderList []*ShippingOrder `json:"order_list"` + LastIndex string `json:"last_index"` + HasMore bool `json:"has_more"` +} + +// NotifyConfirmReceiveRequest 确认收货提醒接口请求参数 +type NotifyConfirmReceiveRequest struct { + TransactionID string `json:"transaction_id"` // 原支付交易对应的微信订单号 + MerchantID string `json:"merchant_id"` // 支付下单商户的商户号,由微信支付生成并下发 + SubMerchantID string `json:"sub_merchant_id"` // 二级商户号 + MerchantTradeNo string `json:"merchant_trade_no"` // 商户系统内部订单号,只能是数字、大小写字母`_-*`且在同一个商户号下唯一 + ReceivedTime int64 `json:"received_time"` // 收货时间,时间戳形式 +} diff --git a/miniprogram/privacy/privacy.go b/miniprogram/privacy/privacy.go new file mode 100644 index 0000000..024e379 --- /dev/null +++ b/miniprogram/privacy/privacy.go @@ -0,0 +1,167 @@ +package privacy + +import ( + "errors" + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +// Privacy 小程序授权隐私设置 +type Privacy struct { + *context.Context +} + +// NewPrivacy 实例化小程序隐私接口 +// 文档地址 https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/privacy_config/set_privacy_setting.html +func NewPrivacy(context *context.Context) *Privacy { + if context == nil { + panic("NewPrivacy got a nil context") + } + return &Privacy{ + context, + } +} + +// OwnerSetting 收集方(开发者)信息配置 +type OwnerSetting struct { + ContactEmail string `json:"contact_email"` + ContactPhone string `json:"contact_phone"` + ContactQQ string `json:"contact_qq"` + ContactWeixin string `json:"contact_weixin"` + ExtFileMediaID string `json:"ext_file_media_id"` + NoticeMethod string `json:"notice_method"` + StoreExpireTimestamp string `json:"store_expire_timestamp"` +} + +// SettingItem 收集权限的配置 +type SettingItem struct { + PrivacyKey string `json:"privacy_key"` + PrivacyText string `json:"privacy_text"` +} + +// SetPrivacySettingRequest 设置权限的请求参数 +type SetPrivacySettingRequest struct { + PrivacyVer int `json:"privacy_ver"` + OwnerSetting OwnerSetting `json:"owner_setting"` + SettingList []SettingItem `json:"setting_list"` +} + +const ( + setPrivacySettingURL = "https://api.weixin.qq.com/cgi-bin/component/setprivacysetting" + getPrivacySettingURL = "https://api.weixin.qq.com/cgi-bin/component/getprivacysetting" + uploadPrivacyExtFileURL = "https://api.weixin.qq.com/cgi-bin/component/uploadprivacyextfile" + + // PrivacyV1 用户隐私保护指引的版本,1表示现网版本。 + PrivacyV1 = 1 + // PrivacyV2 2表示开发版。默认是2开发版。 + PrivacyV2 = 2 +) + +// GetPrivacySettingResponse 获取权限配置的响应结果 +type GetPrivacySettingResponse struct { + util.CommonError + CodeExist int `json:"code_exist"` + PrivacyList []string `json:"privacy_list"` + SettingList []SettingResponseItem `json:"setting_list"` + UpdateTime int64 `json:"update_time"` + OwnerSetting OwnerSetting `json:"owner_setting"` + PrivacyDesc DescList `json:"privacy_desc"` +} + +// SettingResponseItem 获取权限设置的响应明细 +type SettingResponseItem struct { + PrivacyKey string `json:"privacy_key"` + PrivacyText string `json:"privacy_text"` + PrivacyLabel string `json:"privacy_label"` +} + +// DescList 权限列表(保持与官方一致) +type DescList struct { + PrivacyDescList []Desc `json:"privacy_desc_list"` +} + +// Desc 权限列表明细(保持与官方一致) +type Desc struct { + PrivacyDesc string `json:"privacy_desc"` + PrivacyKey string `json:"privacy_key"` +} + +// GetPrivacySetting 获取小程序权限配置 +func (s *Privacy) GetPrivacySetting(privacyVer int) (GetPrivacySettingResponse, error) { + accessToken, err := s.GetAccessToken() + if err != nil { + return GetPrivacySettingResponse{}, err + } + + response, err := util.PostJSON(fmt.Sprintf("%s?access_token=%s", getPrivacySettingURL, accessToken), map[string]int{ + "privacy_ver": privacyVer, + }) + if err != nil { + return GetPrivacySettingResponse{}, err + } + // 返回错误信息 + var result GetPrivacySettingResponse + if err = util.DecodeWithError(response, &result, "getprivacysetting"); err != nil { + return GetPrivacySettingResponse{}, err + } + + return result, nil +} + +// SetPrivacySetting 更新小程序权限配置 +func (s *Privacy) SetPrivacySetting(privacyVer int, ownerSetting OwnerSetting, settingList []SettingItem) error { + if privacyVer == PrivacyV1 && len(settingList) > 0 { + return errors.New("当privacy_ver传2或者不传时,setting_list是必填;当privacy_ver传1时,该参数不可传") + } + accessToken, err := s.GetAccessToken() + if err != nil { + return err + } + + response, err := util.PostJSON(fmt.Sprintf("%s?access_token=%s", setPrivacySettingURL, accessToken), SetPrivacySettingRequest{ + PrivacyVer: privacyVer, + OwnerSetting: ownerSetting, + SettingList: settingList, + }) + if err != nil { + return err + } + + // 返回错误信息 + if err = util.DecodeWithCommonError(response, "setprivacysetting"); err != nil { + return err + } + + return err +} + +// UploadPrivacyExtFileResponse 上传权限定义模板响应参数 +type UploadPrivacyExtFileResponse struct { + util.CommonError + ExtFileMediaID string `json:"ext_file_media_id"` +} + +// UploadPrivacyExtFile 上传权限定义模板 +func (s *Privacy) UploadPrivacyExtFile(fileData []byte) (UploadPrivacyExtFileResponse, error) { + accessToken, err := s.GetAccessToken() + if err != nil { + return UploadPrivacyExtFileResponse{}, err + } + + response, err := util.PostJSON(fmt.Sprintf("%s?access_token=%s", uploadPrivacyExtFileURL, accessToken), map[string][]byte{ + "file": fileData, + }) + if err != nil { + return UploadPrivacyExtFileResponse{}, err + } + + // 返回错误信息 + var result UploadPrivacyExtFileResponse + if err = util.DecodeWithError(response, &result, "setprivacysetting"); err != nil { + return UploadPrivacyExtFileResponse{}, err + } + + return result, err +} diff --git a/miniprogram/qrcode/qrcode.go b/miniprogram/qrcode/qrcode.go index 26e3db7..41e67e5 100644 --- a/miniprogram/qrcode/qrcode.go +++ b/miniprogram/qrcode/qrcode.go @@ -15,12 +15,12 @@ const ( getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s" ) -//QRCode struct +// QRCode struct type QRCode struct { *context.Context } -//NewQRCode 实例 +// NewQRCode 实例 func NewQRCode(context *context.Context) *QRCode { qrCode := new(QRCode) qrCode.Context = context @@ -40,16 +40,20 @@ type QRCoder struct { Page string `json:"page,omitempty"` // path 扫码进入的小程序页面路径 Path string `json:"path,omitempty"` + // checkPath 检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用,默认true + CheckPath *bool `json:"check_path,omitempty"` // width 图片宽度 Width int `json:"width,omitempty"` // scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) Scene string `json:"scene,omitempty"` - // autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 + // autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认false AutoColor bool `json:"auto_color,omitempty"` // lineColor AutoColor 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"},十进制表示 - LineColor Color `json:"line_color,omitempty"` - // isHyaline 是否需要透明底色 + LineColor *Color `json:"line_color,omitempty"` + // isHyaline 是否需要透明底色,默认false IsHyaline bool `json:"is_hyaline,omitempty"` + // envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop" + EnvVersion string `json:"env_version,omitempty"` } // fetchCode 请求并返回二维码二进制数据 diff --git a/miniprogram/riskcontrol/riskcontrol.go b/miniprogram/riskcontrol/riskcontrol.go new file mode 100644 index 0000000..2618772 --- /dev/null +++ b/miniprogram/riskcontrol/riskcontrol.go @@ -0,0 +1,60 @@ +package riskcontrol + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + getUserRiskRankURL = "https://api.weixin.qq.com/wxa/getuserriskrank?access_token=%s" +) + +// RiskControl 安全风控 +type RiskControl struct { + *context.Context +} + +// NewRiskControl init +func NewRiskControl(ctx *context.Context) *RiskControl { + return &RiskControl{ctx} +} + +// UserRiskRankRequest 获取用户安全等级请求 +type UserRiskRankRequest struct { + AppID string `json:"appid"` // 小程序 app id + OpenID string `json:"openid"` // 用户的 openid + Scene uint8 `json:"scene"` // 场景值,0:注册,1:营销作弊 + ClientIP string `json:"client_ip"` // 用户访问源ip + + Mobile string `json:"mobile_no"` // 用户手机号 + Email string `json:"email_address"` // 用户邮箱地址 + ExtendedInfo string `json:"extended_info"` // 额外补充信息 + IsTest bool `json:"is_test"` // false:正式调用,true:测试调用 +} + +// UserRiskRank 用户安全等级 +type UserRiskRank struct { + util.CommonError + UnionID int64 `json:"union_id"` // 唯一请求标识 + RiskRank uint8 `json:"risk_rank"` // 用户风险等级 +} + +// GetUserRiskRank 根据提交的用户信息数据获取用户的安全等级 risk_rank,无需用户授权。 +func (riskControl *RiskControl) GetUserRiskRank(in *UserRiskRankRequest) (res UserRiskRank, err error) { + accessToken, err := riskControl.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getUserRiskRankURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "GetUserRiskRank") + return +} diff --git a/miniprogram/security/security.go b/miniprogram/security/security.go new file mode 100644 index 0000000..7e4d5dc --- /dev/null +++ b/miniprogram/security/security.go @@ -0,0 +1,256 @@ +package security + +import ( + "fmt" + "strconv" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + mediaCheckAsyncURL = "https://api.weixin.qq.com/wxa/media_check_async?access_token=%s" + imageCheckURL = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=%s" + msgCheckURL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=%s" +) + +// Security 内容安全 +type Security struct { + *context.Context +} + +// NewSecurity init +func NewSecurity(ctx *context.Context) *Security { + return &Security{ctx} +} + +// MediaCheckAsyncV1Request 图片/音频异步校验请求参数 +type MediaCheckAsyncV1Request struct { + MediaURL string `json:"media_url"` // 要检测的图片或音频的url,支持图片格式包括jpg, jepg, png, bmp, gif(取首帧),支持的音频格式包括mp3, aac, ac3, wma, flac, vorbis, opus, wav + MediaType uint8 `json:"media_type"` // 1:音频;2:图片 +} + +// MediaCheckAsyncV1 异步校验图片/音频是否含有违法违规内容 +// Deprecated +// 在2021年9月1日停止更新,请尽快更新至 2.0 接口。建议使用 MediaCheckAsync +func (security *Security) MediaCheckAsyncV1(in *MediaCheckAsyncV1Request) (traceID string, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + var res struct { + util.CommonError + TraceID string `json:"trace_id"` + } + err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1") + if err != nil { + return + } + + traceID = res.TraceID + return +} + +// MediaCheckAsyncRequest 图片/音频异步校验请求参数 +type MediaCheckAsyncRequest struct { + MediaURL string `json:"media_url"` // 要检测的图片或音频的url,支持图片格式包括jpg, jepg, png, bmp, gif(取首帧),支持的音频格式包括mp3, aac, ac3, wma, flac, vorbis, opus, wav + MediaType uint8 `json:"media_type"` // 1:音频;2:图片 + OpenID string `json:"openid"` // 用户的openid(用户需在近两小时访问过小程序) + Scene uint8 `json:"scene"` // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志) +} + +// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容 +func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + var req struct { + MediaCheckAsyncRequest + Version uint `json:"version"` // 接口版本号,2.0版本为固定值2 + } + req.MediaCheckAsyncRequest = *in + req.Version = 2 + + uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + // 使用通用方法返回错误 + var res struct { + util.CommonError + TraceID string `json:"trace_id"` + } + err = util.DecodeWithError(response, &res, "MediaCheckAsync") + if err != nil { + return + } + + traceID = res.TraceID + return +} + +// ImageCheckV1 校验一张图片是否含有违法违规内容(同步) +// https://developers.weixin.qq.com/miniprogram/dev/framework/security.imgSecCheck.html +// Deprecated +// 在2021年9月1日停止更新。建议使用 MediaCheckAsync +func (security *Security) ImageCheckV1(filename string) (err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(imageCheckURL, accessToken) + response, err := util.PostFile("media", filename, uri) + if err != nil { + return + } + + // 使用通用方法返回错误 + return util.DecodeWithCommonError(response, "ImageCheckV1") +} + +// CheckSuggest 检查建议 +type CheckSuggest string + +const ( + // CheckSuggestRisky 违规风险建议 + CheckSuggestRisky CheckSuggest = "risky" + // CheckSuggestPass 安全 + CheckSuggestPass CheckSuggest = "pass" + // CheckSuggestReview 需要审查 + CheckSuggestReview CheckSuggest = "review" +) + +// MsgScene 文本场景 +type MsgScene uint8 + +const ( + // MsgSceneMaterial 资料文件检查场景 + MsgSceneMaterial MsgScene = iota + 1 + // MsgSceneComment 评论 + MsgSceneComment + // MsgSceneForum 论坛 + MsgSceneForum + // MsgSceneSocialLog 社交日志 + MsgSceneSocialLog +) + +// CheckLabel 检查命中标签 +type CheckLabel int + +func (cl CheckLabel) String() string { + switch cl { + case 100: + return "正常" + case 10001: + return "广告" + case 20001: + return "时政" + case 20002: + return "色情" + case 20003: + return "辱骂" + case 20006: + return "违法犯罪" + case 20008: + return "欺诈" + case 20012: + return "低俗" + case 20013: + return "版权" + case 21000: + return "其他" + default: + return strconv.Itoa(int(cl)) + } +} + +// MsgCheckRequest 文本检查请求 +type MsgCheckRequest struct { + OpenID string `json:"openid"` // 用户的openid(用户需在近两小时访问过小程序) + Scene MsgScene `json:"scene"` // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志) + Content string `json:"content"` // 需检测的文本内容,文本字数的上限为 2500 字,需使用 UTF-8 编码 + Nickname string `json:"nickname"` // (非必填)用户昵称,需使用UTF-8编码 + Title string `json:"title"` // (非必填)文本标题,需使用UTF-8编码 + Signature string `json:"signature"` // (非必填)个性签名,该参数仅在资料类场景有效(scene=1),需使用UTF-8编码 +} + +// MsgCheckResponse 文本检查响应 +type MsgCheckResponse struct { + util.CommonError + TraceID string `json:"trace_id"` // 唯一请求标识 + Result struct { + Suggest CheckSuggest `json:"suggest"` // 建议 + Label CheckLabel `json:"label"` // 命中标签 + } `json:"result"` // 综合结果 + Detail []struct { + ErrCode int64 `json:"errcode"` // 错误码,仅当该值为0时,该项结果有效 + Strategy string `json:"strategy"` // 策略类型 + Suggest string `json:"suggest"` // 建议 + Label CheckLabel `json:"label"` // 命中标签 + Prob uint `json:"prob"` // 置信度。0-100,越高代表越有可能属于当前返回的标签(label) + Keyword string `json:"keyword"` // 命中的自定义关键词 + } `json:"detail"` // 详细检测结果 +} + +// MsgCheckV1 检查一段文本是否含有违法违规内容 +// Deprecated +// 在2021年9月1日停止更新,请尽快更新至 2.0 接口。建议使用 MsgCheck +func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + var req struct { + Content string `json:"content"` + } + req.Content = content + + uri := fmt.Sprintf(msgCheckURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "security.MsgCheckV1") + return +} + +// MsgCheck 检查一段文本是否含有违法违规内容 +func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + var req struct { + MsgCheckRequest + Version uint `json:"version"` + } + req.MsgCheckRequest = *in + req.Version = 2 + + uri := fmt.Sprintf(msgCheckURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "security.MsgCheck") + return +} diff --git a/miniprogram/shortlink/shortlink.go b/miniprogram/shortlink/shortlink.go new file mode 100644 index 0000000..c386c21 --- /dev/null +++ b/miniprogram/shortlink/shortlink.go @@ -0,0 +1,86 @@ +package shortlink + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + generateShortLinkURL = "https://api.weixin.qq.com/wxa/genwxashortlink?access_token=%s" +) + +// ShortLink 短链接 +type ShortLink struct { + *context.Context +} + +// NewShortLink 实例 +func NewShortLink(ctx *context.Context) *ShortLink { + return &ShortLink{ctx} +} + +// ShortLinker 请求结构体 +type ShortLinker struct { + + // pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大 1024 个字符 + PageURL string `json:"page_url"` + + // pageTitle 页面标题,不能包含违法信息,超过 20 字符会用... 截断代替 + PageTitle string `json:"page_title"` + + // isPermanent 生成的 Short Link 类型,短期有效:false,永久有效:true + IsPermanent bool `json:"is_permanent,omitempty"` +} + +// resShortLinker 返回结构体 +type resShortLinker struct { + // 通用错误 + util.CommonError + + // 返回的 shortLink + Link string `json:"link"` +} + +// Generate 生成 shortLink +func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error) { + var accessToken string + accessToken, err := shortLink.GetAccessToken() + if err != nil { + return "", err + } + + urlStr := fmt.Sprintf(generateShortLinkURL, accessToken) + response, err := util.PostJSON(urlStr, shortLinkParams) + if err != nil { + return "", err + } + + // 使用通用方法返回错误 + var res resShortLinker + err = util.DecodeWithError(response, &res, "GenerateShortLink") + if err != nil { + return "", err + } + + return res.Link, nil +} + +// GenerateShortLinkPermanent 生成永久 shortLink +func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string) (string, error) { + return shortLink.generate(ShortLinker{ + PageURL: PageURL, + PageTitle: pageTitle, + IsPermanent: true, + }) +} + +// GenerateShortLinkTemp 生成临时 shortLink +func (shortLink *ShortLink) GenerateShortLinkTemp(PageURL, pageTitle string) (string, error) { + return shortLink.generate(ShortLinker{ + PageURL: PageURL, + PageTitle: pageTitle, + IsPermanent: false, + }) +} diff --git a/miniprogram/subscribe/subscribe.go b/miniprogram/subscribe/subscribe.go index c260fca..0015a43 100644 --- a/miniprogram/subscribe/subscribe.go +++ b/miniprogram/subscribe/subscribe.go @@ -8,14 +8,22 @@ import ( ) const ( - //发送订阅消息 - //https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html + // 发送订阅消息 + // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send" // 获取当前帐号下的个人模板列表 // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate" + // 添加订阅模板 + // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html + addTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate" + + // 删除私有模板 + // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html + delTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate" + // 统一服务消息 // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send" @@ -33,21 +41,21 @@ func NewSubscribe(ctx *context.Context) *Subscribe { // Message 订阅消息请求参数 type Message struct { - ToUser string `json:"touser"` //必选,接收者(用户)的 openid - TemplateID string `json:"template_id"` //必选,所需下发的订阅模板id - Page string `json:"page"` //可选,点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 - Data map[string]*DataItem `json:"data"` //必选, 模板内容 - MiniprogramState string `json:"miniprogram_state"` //可选,跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 - Lang string `json:"lang"` //入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN + ToUser string `json:"touser"` // 必选,接收者(用户)的 openid + TemplateID string `json:"template_id"` // 必选,所需下发的订阅模板id + Page string `json:"page"` // 可选,点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 + Data map[string]*DataItem `json:"data"` // 必选, 模板内容 + MiniprogramState string `json:"miniprogram_state"` // 可选,跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 + Lang string `json:"lang"` // 入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN } -//DataItem 模版内某个 .DATA 的值 +// DataItem 模版内某个 .DATA 的值 type DataItem struct { Value interface{} `json:"value"` Color string `json:"color"` } -//TemplateItem template item +// TemplateItem template item type TemplateItem struct { PriTmplID string `json:"priTmplId"` Title string `json:"title"` @@ -56,7 +64,7 @@ type TemplateItem struct { Type int64 `json:"type"` } -//TemplateList template list +// TemplateList template list type TemplateList struct { util.CommonError Data []TemplateItem `json:"data"` @@ -77,7 +85,7 @@ func (s *Subscribe) Send(msg *Message) (err error) { return util.DecodeWithCommonError(response, "Send") } -//ListTemplates 获取当前帐号下的个人模板列表 +// ListTemplates 获取当前帐号下的个人模板列表 // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html func (s *Subscribe) ListTemplates() (*TemplateList, error) { accessToken, err := s.GetAccessToken() @@ -133,3 +141,55 @@ func (s *Subscribe) UniformSend(msg *UniformMessage) (err error) { } return util.DecodeWithCommonError(response, "UniformSend") } + +type resSubscribeAdd struct { + util.CommonError + + TemplateID string `json:"priTmplId"` +} + +// Add 添加订阅消息模板 +func (s *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templateID string, err error) { + var accessToken string + accessToken, err = s.GetAccessToken() + if err != nil { + return + } + var msg = struct { + TemplateIDShort string `json:"tid"` + SceneDesc string `json:"sceneDesc"` + KidList []int `json:"kidList"` + }{TemplateIDShort: ShortID, SceneDesc: sceneDesc, KidList: kidList} + uri := fmt.Sprintf("%s?access_token=%s", addTemplateURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + var result resSubscribeAdd + err = util.DecodeWithError(response, &result, "AddSubscribe") + if err != nil { + return + } + templateID = result.TemplateID + return +} + +// Delete 删除私有模板 +func (s *Subscribe) Delete(templateID string) (err error) { + var accessToken string + accessToken, err = s.GetAccessToken() + if err != nil { + return + } + var msg = struct { + TemplateID string `json:"priTmplId"` + }{TemplateID: templateID} + uri := fmt.Sprintf("%s?access_token=%s", delTemplateURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + return util.DecodeWithCommonError(response, "DeleteSubscribe") +} diff --git a/miniprogram/tcb/README.md b/miniprogram/tcb/README.md index eb27fb7..3ef3afa 100644 --- a/miniprogram/tcb/README.md +++ b/miniprogram/tcb/README.md @@ -21,7 +21,9 @@ wcTcb := wc.GetTcb() ``` ### 举例 + #### 触发云函数 + ```golang res, err := wcTcb.InvokeCloudFunction("test-xxxx", "add", `{"a":1,"b":2}`) if err != nil { diff --git a/miniprogram/tcb/cloudfunction.go b/miniprogram/tcb/cloudfunction.go index 87b11ab..bed0332 100644 --- a/miniprogram/tcb/cloudfunction.go +++ b/miniprogram/tcb/cloudfunction.go @@ -7,17 +7,18 @@ import ( ) const ( - //触发云函数 + // 触发云函数 invokeCloudFunctionURL = "https://api.weixin.qq.com/tcb/invokecloudfunction" ) -//InvokeCloudFunctionRes 云函数调用返回结果 +// InvokeCloudFunctionRes 云函数调用返回结果 type InvokeCloudFunctionRes struct { util.CommonError - RespData string `json:"resp_data"` //云函数返回的buffer + RespData string `json:"resp_data"` // 云函数返回的buffer } -//InvokeCloudFunction 云函数调用 +// InvokeCloudFunction 云函数调用 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/functions/invokeCloudFunction.html func (tcb *Tcb) InvokeCloudFunction(env, name, args string) (*InvokeCloudFunctionRes, error) { accessToken, err := tcb.GetAccessToken() diff --git a/miniprogram/tcb/database.go b/miniprogram/tcb/database.go index 9d473dd..635ec10 100644 --- a/miniprogram/tcb/database.go +++ b/miniprogram/tcb/database.go @@ -7,191 +7,192 @@ import ( ) const ( - //数据库导入 + // 数据库导入 databaseMigrateImportURL = "https://api.weixin.qq.com/tcb/databasemigrateimport" - //数据库导出 + // 数据库导出 databaseMigrateExportURL = "https://api.weixin.qq.com/tcb/databasemigrateexport" - //数据库迁移状态查询 + // 数据库迁移状态查询 databaseMigrateQueryInfoURL = "https://api.weixin.qq.com/tcb/databasemigratequeryinfo" - //变更数据库索引 + // 变更数据库索引 updateIndexURL = "https://api.weixin.qq.com/tcb/updateindex" - //新增集合 + // 新增集合 databaseCollectionAddURL = "https://api.weixin.qq.com/tcb/databasecollectionadd" - //删除集合 + // 删除集合 databaseCollectionDeleteURL = "https://api.weixin.qq.com/tcb/databasecollectiondelete" - //获取特定云环境下集合信息 + // 获取特定云环境下集合信息 databaseCollectionGetURL = "https://api.weixin.qq.com/tcb/databasecollectionget" - //数据库插入记录 + // 数据库插入记录 databaseAddURL = "https://api.weixin.qq.com/tcb/databaseadd" - //数据库删除记录 + // 数据库删除记录 databaseDeleteURL = "https://api.weixin.qq.com/tcb/databasedelete" - //数据库更新记录 + // 数据库更新记录 databaseUpdateURL = "https://api.weixin.qq.com/tcb/databaseupdate" - //数据库查询记录 + // 数据库查询记录 databaseQueryURL = "https://api.weixin.qq.com/tcb/databasequery" - //统计集合记录数或统计查询语句对应的结果记录数 + // 统计集合记录数或统计查询语句对应的结果记录数 databaseCountURL = "https://api.weixin.qq.com/tcb/databasecount" - //ConflictModeInster 冲突处理模式 插入 + // ConflictModeInster 冲突处理模式 插入 ConflictModeInster ConflictMode = 1 - //ConflictModeUpsert 冲突处理模式 更新 + // ConflictModeUpsert 冲突处理模式 更新 ConflictModeUpsert ConflictMode = 2 - //FileTypeJSON 的合法值 json + // FileTypeJSON 的合法值 json FileTypeJSON FileType = 1 - //FileTypeCsv 的合法值 csv + // FileTypeCsv 的合法值 csv FileTypeCsv FileType = 2 ) -//ConflictMode 冲突处理模式 +// ConflictMode 冲突处理模式 type ConflictMode int -//FileType 文件上传和导出的允许文件类型 +// FileType 文件上传和导出的允许文件类型 type FileType int -//ValidDirections 合法的direction值 +// ValidDirections 合法的direction值 var ValidDirections = []string{"1", "-1", "2dsphere"} -//DatabaseMigrateExportReq 数据库出 请求参数 +// DatabaseMigrateExportReq 数据库出 请求参数 type DatabaseMigrateExportReq struct { - Env string `json:"env,omitempty"` //云环境ID - FilePath string `json:"file_path,omitempty"` //导出文件路径(导入文件需先上传到同环境的存储中,可使用开发者工具或 HTTP API的上传文件 API上传) - FileType FileType `json:"file_type,omitempty"` //导出文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv - Query string `json:"query,omitempty"` //导出条件 + Env string `json:"env,omitempty"` // 云环境ID + FilePath string `json:"file_path,omitempty"` // 导出文件路径(导入文件需先上传到同环境的存储中,可使用开发者工具或 HTTP API的上传文件 API上传) + FileType FileType `json:"file_type,omitempty"` // 导出文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv + Query string `json:"query,omitempty"` // 导出条件 } -//DatabaseMigrateExportRes 数据库导出 返回结果 +// DatabaseMigrateExportRes 数据库导出 返回结果 type DatabaseMigrateExportRes struct { util.CommonError - JobID int64 `json:"job_id"` //导出任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果 + JobID int64 `json:"job_id"` // 导出任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果 } -//DatabaseMigrateImportReq 数据库导入 请求参数 +// DatabaseMigrateImportReq 数据库导入 请求参数 type DatabaseMigrateImportReq struct { - Env string `json:"env,omitempty"` //云环境ID - CollectionName string `json:"collection_name,omitempty"` //集合名称 - FilePath string `json:"file_path,omitempty"` //导出文件路径(文件会导出到同环境的云存储中,可使用获取下载链接 API 获取下载链接) - FileType FileType `json:"file_type,omitempty"` //导入文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv - StopOnError bool `json:"stop_on_error,omitempty"` //是否在遇到错误时停止导入 - ConflictMode ConflictMode `json:"conflict_mode,omitempty"` //冲突处理模式 1:inster 2:UPSERT + Env string `json:"env,omitempty"` // 云环境ID + CollectionName string `json:"collection_name,omitempty"` // 集合名称 + FilePath string `json:"file_path,omitempty"` // 导出文件路径(文件会导出到同环境的云存储中,可使用获取下载链接 API 获取下载链接) + FileType FileType `json:"file_type,omitempty"` // 导入文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv + StopOnError bool `json:"stop_on_error,omitempty"` // 是否在遇到错误时停止导入 + ConflictMode ConflictMode `json:"conflict_mode,omitempty"` // 冲突处理模式 1:inster 2:UPSERT } -//DatabaseMigrateImportRes 数据库导入 返回结果 +// DatabaseMigrateImportRes 数据库导入 返回结果 type DatabaseMigrateImportRes struct { util.CommonError - JobID int64 `json:"job_id"` //导入任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果 + JobID int64 `json:"job_id"` // 导入任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果 } -//DatabaseMigrateQueryInfoRes 数据库迁移状态查询 +// DatabaseMigrateQueryInfoRes 数据库迁移状态查询 type DatabaseMigrateQueryInfoRes struct { util.CommonError - Status string `json:"status"` //导出状态 - RecordSuccess int64 `json:"record_success"` //导出成功记录数 - RecordFail int64 `json:"record_fail"` //导出失败记录数 - ErrMsg string `json:"err_msg"` //导出错误信息 - FileURL string `json:"file_url"` //导出文件下载地址 + Status string `json:"status"` // 导出状态 + RecordSuccess int64 `json:"record_success"` // 导出成功记录数 + RecordFail int64 `json:"record_fail"` // 导出失败记录数 + ErrMsg string `json:"err_msg"` // 导出错误信息 + FileURL string `json:"file_url"` // 导出文件下载地址 } -//UpdateIndexReq 变更数据库索引 请求参数 +// UpdateIndexReq 变更数据库索引 请求参数 type UpdateIndexReq struct { - Env string `json:"env,omitempty"` //云环境ID - CollectionName string `json:"collection_name,omitempty"` //集合名称 - CreateIndexes []CreateIndex `json:"create_indexes,omitempty"` //新增索引 - DropIndexes []DropIndex `json:"drop_indexes,omitempty"` //删除索引 + Env string `json:"env,omitempty"` // 云环境ID + CollectionName string `json:"collection_name,omitempty"` // 集合名称 + CreateIndexes []CreateIndex `json:"create_indexes,omitempty"` // 新增索引 + DropIndexes []DropIndex `json:"drop_indexes,omitempty"` // 删除索引 } -//CreateIndex 新增索引 +// CreateIndex 新增索引 type CreateIndex struct { - Name string `json:"name,omitempty"` //索引名 - Unique bool `json:"unique,omitempty"` //是否唯一 - Keys []CreateIndexKey `json:"keys,omitempty"` //索引字段 + Name string `json:"name,omitempty"` // 索引名 + Unique bool `json:"unique,omitempty"` // 是否唯一 + Keys []CreateIndexKey `json:"keys,omitempty"` // 索引字段 } -//CreateIndexKey create index key +// CreateIndexKey create index key type CreateIndexKey struct { - Name string `json:"name,omitempty"` //字段名 - Direction string `json:"direction,omitempty"` //字段排序 + Name string `json:"name,omitempty"` // 字段名 + Direction string `json:"direction,omitempty"` // 字段排序 } -//DropIndex 删除索引 +// DropIndex 删除索引 type DropIndex struct { Name string `json:"name,omitempty"` } -//DatabaseCollectionReq 新增/删除集合请求参数 +// DatabaseCollectionReq 新增/删除集合请求参数 type DatabaseCollectionReq struct { - Env string `json:"env,omitempty"` //云环境ID - CollectionName string `json:"collection_name,omitempty"` //集合名称 + Env string `json:"env,omitempty"` // 云环境ID + CollectionName string `json:"collection_name,omitempty"` // 集合名称 } -//DatabaseCollectionGetReq 获取特定云环境下集合信息请求 +// DatabaseCollectionGetReq 获取特定云环境下集合信息请求 type DatabaseCollectionGetReq struct { - Env string `json:"env,omitempty"` //云环境ID - Limit int64 `json:"limit,omitempty"` //获取数量限制 - Offset int64 `json:"offset,omitempty"` //偏移量 + Env string `json:"env,omitempty"` // 云环境ID + Limit int64 `json:"limit,omitempty"` // 获取数量限制 + Offset int64 `json:"offset,omitempty"` // 偏移量 } -//DatabaseCollectionGetRes 获取特定云环境下集合信息结果 +// DatabaseCollectionGetRes 获取特定云环境下集合信息结果 type DatabaseCollectionGetRes struct { util.CommonError Pager struct { - Limit int64 `json:"limit"` //单次查询限制 - Offset int64 `json:"offset"` //偏移量 - Total int64 `json:"total"` //符合查询条件的记录总数 + Limit int64 `json:"limit"` // 单次查询限制 + Offset int64 `json:"offset"` // 偏移量 + Total int64 `json:"total"` // 符合查询条件的记录总数 } `json:"pager"` Collections []struct { - Name string `json:"name"` //集合名 - Count int64 `json:"count"` //表中文档数量 - Size int64 `json:"size"` //表的大小(即表中文档总大小),单位:字节 - IndexCount int64 `json:"index_count"` //索引数量 - IndexSize int64 `json:"index_size"` //索引占用大小,单位:字节 + Name string `json:"name"` // 集合名 + Count int64 `json:"count"` // 表中文档数量 + Size int64 `json:"size"` // 表的大小(即表中文档总大小),单位:字节 + IndexCount int64 `json:"index_count"` // 索引数量 + IndexSize int64 `json:"index_size"` // 索引占用大小,单位:字节 } `json:"collections"` } -//DatabaseReq 数据库插入/删除/更新/查询/统计记录请求参数 +// DatabaseReq 数据库插入/删除/更新/查询/统计记录请求参数 type DatabaseReq struct { - Env string `json:"env,omitempty"` //云环境ID - Query string `json:"query,omitempty"` //数据库操作语句 + Env string `json:"env,omitempty"` // 云环境ID + Query string `json:"query,omitempty"` // 数据库操作语句 } -//DatabaseAddRes 数据库插入记录返回结果 +// DatabaseAddRes 数据库插入记录返回结果 type DatabaseAddRes struct { util.CommonError - IDList []string `json:"id_list"` //插入成功的数据集合主键_id。 + IDList []string `json:"id_list"` // 插入成功的数据集合主键_id。 } -//DatabaseDeleteRes 数据库删除记录返回结果 +// DatabaseDeleteRes 数据库删除记录返回结果 type DatabaseDeleteRes struct { util.CommonError - Deleted int64 `json:"deleted"` //删除记录数量 + Deleted int64 `json:"deleted"` // 删除记录数量 } -//DatabaseUpdateRes 数据库更新记录返回结果 +// DatabaseUpdateRes 数据库更新记录返回结果 type DatabaseUpdateRes struct { util.CommonError - Matched int64 `json:"matched"` //更新条件匹配到的结果数 - Modified int64 `json:"modified"` //修改的记录数,注意:使用set操作新插入的数据不计入修改数目 + Matched int64 `json:"matched"` // 更新条件匹配到的结果数 + Modified int64 `json:"modified"` // 修改的记录数,注意:使用set操作新插入的数据不计入修改数目 ID string `json:"id"` } -//DatabaseQueryRes 数据库查询记录 返回结果 +// DatabaseQueryRes 数据库查询记录 返回结果 type DatabaseQueryRes struct { util.CommonError Pager struct { - Limit int64 `json:"limit"` //单次查询限制 - Offset int64 `json:"offset"` //偏移量 - Total int64 `json:"total"` //符合查询条件的记录总数 + Limit int64 `json:"limit"` // 单次查询限制 + Offset int64 `json:"offset"` // 偏移量 + Total int64 `json:"total"` // 符合查询条件的记录总数 } `json:"pager"` Data []string `json:"data"` } -//DatabaseCountRes 统计集合记录数或统计查询语句对应的结果记录数 返回结果 +// DatabaseCountRes 统计集合记录数或统计查询语句对应的结果记录数 返回结果 type DatabaseCountRes struct { util.CommonError - Count int64 `json:"count"` //记录数量 + Count int64 `json:"count"` // 记录数量 } -//DatabaseMigrateImport 数据库导入 +// DatabaseMigrateImport 数据库导入 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateImport.html func (tcb *Tcb) DatabaseMigrateImport(req *DatabaseMigrateImportReq) (*DatabaseMigrateImportRes, error) { accessToken, err := tcb.GetAccessToken() @@ -208,7 +209,8 @@ func (tcb *Tcb) DatabaseMigrateImport(req *DatabaseMigrateImportReq) (*DatabaseM return databaseMigrateImportRes, err } -//DatabaseMigrateExport 数据库导出 +// DatabaseMigrateExport 数据库导出 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateExport.html func (tcb *Tcb) DatabaseMigrateExport(req *DatabaseMigrateExportReq) (*DatabaseMigrateExportRes, error) { accessToken, err := tcb.GetAccessToken() @@ -225,7 +227,8 @@ func (tcb *Tcb) DatabaseMigrateExport(req *DatabaseMigrateExportReq) (*DatabaseM return databaseMigrateExportRes, err } -//DatabaseMigrateQueryInfo 数据库迁移状态查询 +// DatabaseMigrateQueryInfo 数据库迁移状态查询 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateQueryInfo.html func (tcb *Tcb) DatabaseMigrateQueryInfo(env string, jobID int64) (*DatabaseMigrateQueryInfoRes, error) { accessToken, err := tcb.GetAccessToken() @@ -245,8 +248,8 @@ func (tcb *Tcb) DatabaseMigrateQueryInfo(env string, jobID int64) (*DatabaseMigr return databaseMigrateQueryInfoRes, err } -//UpdateIndex 变更数据库索引 -//https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/updateIndex.html +// UpdateIndex 变更数据库索引 +// https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/updateIndex.html func (tcb *Tcb) UpdateIndex(req *UpdateIndexReq) error { accessToken, err := tcb.GetAccessToken() if err != nil { @@ -260,7 +263,8 @@ func (tcb *Tcb) UpdateIndex(req *UpdateIndexReq) error { return util.DecodeWithCommonError(response, "UpdateIndex") } -//DatabaseCollectionAdd 新增集合 +// DatabaseCollectionAdd 新增集合 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionAdd.html func (tcb *Tcb) DatabaseCollectionAdd(env, collectionName string) error { accessToken, err := tcb.GetAccessToken() @@ -278,7 +282,8 @@ func (tcb *Tcb) DatabaseCollectionAdd(env, collectionName string) error { return util.DecodeWithCommonError(response, "DatabaseCollectionAdd") } -//DatabaseCollectionDelete 删除集合 +// DatabaseCollectionDelete 删除集合 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionDelete.html func (tcb *Tcb) DatabaseCollectionDelete(env, collectionName string) error { accessToken, err := tcb.GetAccessToken() @@ -296,7 +301,8 @@ func (tcb *Tcb) DatabaseCollectionDelete(env, collectionName string) error { return util.DecodeWithCommonError(response, "DatabaseCollectionDelete") } -//DatabaseCollectionGet 获取特定云环境下集合信息 +// DatabaseCollectionGet 获取特定云环境下集合信息 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionGet.html func (tcb *Tcb) DatabaseCollectionGet(env string, limit, offset int64) (*DatabaseCollectionGetRes, error) { accessToken, err := tcb.GetAccessToken() @@ -317,7 +323,8 @@ func (tcb *Tcb) DatabaseCollectionGet(env string, limit, offset int64) (*Databas return databaseCollectionGetRes, err } -//DatabaseAdd 数据库插入记录 +// DatabaseAdd 数据库插入记录 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseAdd.html func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) { accessToken, err := tcb.GetAccessToken() @@ -337,7 +344,8 @@ func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) { return databaseAddRes, err } -//DatabaseDelete 数据库插入记录 +// DatabaseDelete 数据库插入记录 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseDelete.html func (tcb *Tcb) DatabaseDelete(env, query string) (*DatabaseDeleteRes, error) { accessToken, err := tcb.GetAccessToken() @@ -357,7 +365,8 @@ func (tcb *Tcb) DatabaseDelete(env, query string) (*DatabaseDeleteRes, error) { return databaseDeleteRes, err } -//DatabaseUpdate 数据库插入记录 +// DatabaseUpdate 数据库插入记录 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseUpdate.html func (tcb *Tcb) DatabaseUpdate(env, query string) (*DatabaseUpdateRes, error) { accessToken, err := tcb.GetAccessToken() @@ -377,7 +386,8 @@ func (tcb *Tcb) DatabaseUpdate(env, query string) (*DatabaseUpdateRes, error) { return databaseUpdateRes, err } -//DatabaseQuery 数据库查询记录 +// DatabaseQuery 数据库查询记录 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseQuery.html func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error) { accessToken, err := tcb.GetAccessToken() @@ -397,7 +407,8 @@ func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error) { return databaseQueryRes, err } -//DatabaseCount 统计集合记录数或统计查询语句对应的结果记录数 +// DatabaseCount 统计集合记录数或统计查询语句对应的结果记录数 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCount.html func (tcb *Tcb) DatabaseCount(env, query string) (*DatabaseCountRes, error) { accessToken, err := tcb.GetAccessToken() diff --git a/miniprogram/tcb/file.go b/miniprogram/tcb/file.go index 89c8c3e..7ddc4f7 100644 --- a/miniprogram/tcb/file.go +++ b/miniprogram/tcb/file.go @@ -7,60 +7,60 @@ import ( ) const ( - //获取文件上传链接 + // 获取文件上传链接 uploadFilePathURL = "https://api.weixin.qq.com/tcb/uploadfile" - //获取文件下载链接 + // 获取文件下载链接 batchDownloadFileURL = "https://api.weixin.qq.com/tcb/batchdownloadfile" - //删除文件链接 + // 删除文件链接 batchDeleteFileURL = "https://api.weixin.qq.com/tcb/batchdeletefile" ) -//UploadFileReq 上传文件请求值 +// UploadFileReq 上传文件请求值 type UploadFileReq struct { Env string `json:"env,omitempty"` Path string `json:"path,omitempty"` } -//UploadFileRes 上传文件返回结果 +// UploadFileRes 上传文件返回结果 type UploadFileRes struct { util.CommonError - URL string `json:"url"` //上传url - Token string `json:"token"` //token - Authorization string `json:"authorization"` //authorization - FileID string `json:"file_id"` //文件ID - CosFileID string `json:"cos_file_id"` //cos文件ID + URL string `json:"url"` // 上传url + Token string `json:"token"` // token + Authorization string `json:"authorization"` // authorization + FileID string `json:"file_id"` // 文件ID + CosFileID string `json:"cos_file_id"` // cos文件ID } -//BatchDownloadFileReq 上传文件请求值 +// BatchDownloadFileReq 上传文件请求值 type BatchDownloadFileReq struct { Env string `json:"env,omitempty"` FileList []*DownloadFile `json:"file_list,omitempty"` } -//DownloadFile 文件信息 +// DownloadFile 文件信息 type DownloadFile struct { - FileID string `json:"fileid"` //文件ID - MaxAge int64 `json:"max_age"` //下载链接有效期 + FileID string `json:"fileid"` // 文件ID + MaxAge int64 `json:"max_age"` // 下载链接有效期 } -//BatchDownloadFileRes 上传文件返回结果 +// BatchDownloadFileRes 上传文件返回结果 type BatchDownloadFileRes struct { util.CommonError FileList []struct { - FileID string `json:"file_id"` //文件ID - DownloadURL string `json:"download_url"` //下载链接 - Status int64 `json:"status"` //状态码 - ErrMsg string `json:"errmsg"` //该文件错误信息 + FileID string `json:"file_id"` // 文件ID + DownloadURL string `json:"download_url"` // 下载链接 + Status int64 `json:"status"` // 状态码 + ErrMsg string `json:"errmsg"` // 该文件错误信息 } `json:"file_list"` } -//BatchDeleteFileReq 批量删除文件请求参数 +// BatchDeleteFileReq 批量删除文件请求参数 type BatchDeleteFileReq struct { Env string `json:"env,omitempty"` FileIDList []string `json:"fileid_list,omitempty"` } -//BatchDeleteFileRes 批量删除文件返回结果 +// BatchDeleteFileRes 批量删除文件返回结果 type BatchDeleteFileRes struct { util.CommonError DeleteList []struct { @@ -70,7 +70,8 @@ type BatchDeleteFileRes struct { } `json:"delete_list"` } -//UploadFile 上传文件 +// UploadFile 上传文件 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/uploadFile.html func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) { accessToken, err := tcb.GetAccessToken() @@ -91,7 +92,8 @@ func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) { return uploadFileRes, err } -//BatchDownloadFile 获取文件下载链接 +// BatchDownloadFile 获取文件下载链接 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDownloadFile.html func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchDownloadFileRes, error) { accessToken, err := tcb.GetAccessToken() @@ -112,7 +114,8 @@ func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchD return batchDownloadFileRes, err } -//BatchDeleteFile 批量删除文件 +// BatchDeleteFile 批量删除文件 +// //reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDeleteFile.html func (tcb *Tcb) BatchDeleteFile(env string, fileIDList []string) (*BatchDeleteFileRes, error) { accessToken, err := tcb.GetAccessToken() diff --git a/miniprogram/tcb/tcb.go b/miniprogram/tcb/tcb.go index 35522ee..5f6cfd4 100644 --- a/miniprogram/tcb/tcb.go +++ b/miniprogram/tcb/tcb.go @@ -2,12 +2,12 @@ package tcb import "github.com/silenceper/wechat/v2/miniprogram/context" -//Tcb Tencent Cloud Base +// Tcb Tencent Cloud Base type Tcb struct { *context.Context } -//NewTcb new Tencent Cloud Base +// NewTcb new Tencent Cloud Base func NewTcb(context *context.Context) *Tcb { return &Tcb{ context, diff --git a/miniprogram/urllink/query.go b/miniprogram/urllink/query.go new file mode 100644 index 0000000..72fc57a --- /dev/null +++ b/miniprogram/urllink/query.go @@ -0,0 +1,52 @@ +package urllink + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const queryURL = "https://api.weixin.qq.com/wxa/query_urllink" + +// ULQueryResult 返回的结果 +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值 +type ULQueryResult struct { + util.CommonError + + URLLinkInfo struct { + Appid string `json:"appid"` + Path string `json:"path"` + Query string `json:"query"` + CreateTime int64 `json:"create_time"` + ExpireTime int64 `json:"expire_time"` + EnvVersion string `json:"env_version"` + CloudBase struct { + Env string `json:"env"` + Domain string `json:"domain"` + Path string `json:"path"` + Query string `json:"query"` + ResourceAppid string `json:"resource_appid"` + } `json:"cloud_base"` + } `json:"url_link_info"` + VisitOpenid string `json:"visit_openid"` +} + +// Query 查询小程序 url_link 配置。 +func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) { + accessToken, err := u.GetAccessToken() + if err != nil { + return nil, err + } + + uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken) + response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink}) + if err != nil { + return nil, err + } + var resp ULQueryResult + err = util.DecodeWithError(response, &resp, "URLLink.Query") + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/miniprogram/urllink/urllink.go b/miniprogram/urllink/urllink.go new file mode 100644 index 0000000..378d58a --- /dev/null +++ b/miniprogram/urllink/urllink.go @@ -0,0 +1,72 @@ +package urllink + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +// URLLink 小程序 URL Link +type URLLink struct { + *context.Context +} + +// NewURLLink 实例化 +func NewURLLink(ctx *context.Context) *URLLink { + return &URLLink{Context: ctx} +} + +const generateURL = "https://api.weixin.qq.com/wxa/generate_urllink" + +// TExpireType 失效类型 (指定时间戳/指定间隔) +type TExpireType int + +const ( + // ExpireTypeTime 指定时间戳后失效 + ExpireTypeTime TExpireType = 0 + + // ExpireTypeInterval 间隔指定天数后失效 + ExpireTypeInterval TExpireType = 1 +) + +// ULParams 请求参数 +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html#请求参数 +type ULParams struct { + Path string `json:"path"` + Query string `json:"query"` + // envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop" + EnvVersion string `json:"env_version,omitempty"` + IsExpire bool `json:"is_expire"` + ExpireType TExpireType `json:"expire_type"` + ExpireTime int64 `json:"expire_time"` + ExpireInterval int `json:"expire_interval"` +} + +// ULResult 返回的结果 +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html#返回值 +type ULResult struct { + util.CommonError + + URLLink string `json:"url_link"` +} + +// Generate 生成url link +func (u *URLLink) Generate(params *ULParams) (string, error) { + accessToken, err := u.GetAccessToken() + if err != nil { + return "", err + } + + uri := fmt.Sprintf("%s?access_token=%s", generateURL, accessToken) + response, err := util.PostJSON(uri, params) + if err != nil { + return "", err + } + var resp ULResult + err = util.DecodeWithError(response, &resp, "URLLink.Generate") + if err != nil { + return "", err + } + return resp.URLLink, nil +} diff --git a/miniprogram/urlscheme/query.go b/miniprogram/urlscheme/query.go new file mode 100644 index 0000000..d9ff481 --- /dev/null +++ b/miniprogram/urlscheme/query.go @@ -0,0 +1,70 @@ +package urlscheme + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + querySchemeURL = "https://api.weixin.qq.com/wxa/queryscheme?access_token=%s" +) + +// QueryScheme 获取小程序访问scheme +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数 +type QueryScheme struct { + // 小程序 scheme 码 + Scheme string `json:"scheme"` +} + +// SchemeInfo scheme 配置 +type SchemeInfo struct { + // 小程序 appid。 + AppID string `json:"appid"` + // 小程序页面路径。 + Path string `json:"path"` + // 小程序页面query。 + Query string `json:"query"` + // 创建时间,为 Unix 时间戳。 + CreateTime int64 `json:"create_time"` + // 到期失效时间,为 Unix 时间戳,0 表示永久生效 + ExpireTime int64 `json:"expire_time"` + // 要打开的小程序版本。正式版为"release",体验版为"trial",开发版为"develop"。 + EnvVersion EnvVersion `json:"env_version"` +} + +// resQueryScheme 返回结构体 +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数 +type resQueryScheme struct { + // 通用错误 + *util.CommonError + // scheme 配置 + SchemeInfo SchemeInfo `json:"scheme_info"` + // 访问该链接的openid,没有用户访问过则为空字符串 + VisitOpenid string `json:"visit_openid"` +} + +// QueryScheme 查询小程序 scheme 码 +func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) { + var accessToken string + accessToken, err = u.GetAccessToken() + if err != nil { + return + } + + urlStr := fmt.Sprintf(querySchemeURL, accessToken) + var response []byte + response, err = util.PostJSON(urlStr, querySchemeParams) + if err != nil { + return + } + + // 使用通用方法返回错误 + var res resQueryScheme + err = util.DecodeWithError(response, &res, "QueryScheme") + if err != nil { + return + } + + return res.SchemeInfo, res.VisitOpenid, nil +} diff --git a/miniprogram/urlscheme/urlscheme.go b/miniprogram/urlscheme/urlscheme.go new file mode 100644 index 0000000..37bcd75 --- /dev/null +++ b/miniprogram/urlscheme/urlscheme.go @@ -0,0 +1,85 @@ +package urlscheme + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +// URLScheme 小程序 URL Scheme +type URLScheme struct { + *context.Context +} + +// NewURLScheme 实例化 +func NewURLScheme(ctx *context.Context) *URLScheme { + return &URLScheme{Context: ctx} +} + +const generateURL = "https://api.weixin.qq.com/wxa/generatescheme" + +// TExpireType 失效类型 (指定时间戳/指定间隔) +type TExpireType int + +// EnvVersion 要打开的小程序版本 +type EnvVersion string + +const ( + // ExpireTypeTime 指定时间戳后失效 + ExpireTypeTime TExpireType = 0 + // ExpireTypeInterval 间隔指定天数后失效 + ExpireTypeInterval TExpireType = 1 + + // EnvVersionRelease 正式版为"release" + EnvVersionRelease EnvVersion = "release" + // EnvVersionTrial 体验版为"trial" + EnvVersionTrial EnvVersion = "trial" + // EnvVersionDevelop 开发版为"develop" + EnvVersionDevelop EnvVersion = "develop" +) + +// JumpWxa 跳转到的目标小程序信息 +type JumpWxa struct { + Path string `json:"path"` + Query string `json:"query"` + // envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop" + EnvVersion EnvVersion `json:"env_version,omitempty"` +} + +// USParams 请求参数 +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数 +type USParams struct { + JumpWxa *JumpWxa `json:"jump_wxa"` + ExpireType TExpireType `json:"expire_type"` + ExpireTime int64 `json:"expire_time"` + ExpireInterval int `json:"expire_interval"` +} + +// USResult 返回的结果 +// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#返回值 +type USResult struct { + util.CommonError + + OpenLink string `json:"openlink"` +} + +// Generate 生成url link +func (u *URLScheme) Generate(params *USParams) (string, error) { + accessToken, err := u.GetAccessToken() + if err != nil { + return "", err + } + + uri := fmt.Sprintf("%s?access_token=%s", generateURL, accessToken) + response, err := util.PostJSON(uri, params) + if err != nil { + return "", err + } + var resp USResult + err = util.DecodeWithError(response, &resp, "URLScheme.Generate") + if err != nil { + return "", err + } + return resp.OpenLink, nil +} diff --git a/miniprogram/virtualpayment/constant.go b/miniprogram/virtualpayment/constant.go new file mode 100644 index 0000000..05c78d6 --- /dev/null +++ b/miniprogram/virtualpayment/constant.go @@ -0,0 +1,134 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +package virtualpayment + +const ( + // EnvProduction 环境 0-正式环境 1-沙箱环境 + EnvProduction Env = 0 + // EnvSandbox 环境 0-正式环境 1-沙箱环境 + EnvSandbox Env = 1 +) + +const ( + // Success 错误码 0、成功 + Success ErrCode = 0 + // SystemError 错误码 -1、系统错误 + SystemError ErrCode = -1 + // OpenIDError 错误码 268490001、openid 错误 + OpenIDError ErrCode = 268490001 + // RequestParamError 错误码 268490002、请求参数字段错误,具体看 errmsg + RequestParamError ErrCode = 268490002 + // SignError 错误码 268490003、签名错误 + SignError ErrCode = 268490003 + // RepeatOperationError 错误码 268490004、重复操作(赠送和代币支付相关接口会返回,表示之前的操作已经成功) + RepeatOperationError ErrCode = 268490004 + // OrderRefundedError 错误码 268490005、订单已经通过 cancel_currency_pay 接口退款,不支持再退款 + OrderRefundedError ErrCode = 268490005 + // InsufficientBalanceError 错误码 268490006、代币的退款/支付操作金额不足 + InsufficientBalanceError ErrCode = 268490006 + // SensitiveContentError 错误码 268490007、图片或文字存在敏感内容,禁止使用 + SensitiveContentError ErrCode = 268490007 + // TokenNotPublishedError 错误码 268490008、代币未发布,不允许进行代币操作 + TokenNotPublishedError ErrCode = 268490008 + // SessionKeyExpiredError 错误码 268490009、用户 session_key 不存在或已过期,请重新登录 + SessionKeyExpiredError ErrCode = 268490009 + // BillGeneratingError 错误码 268490011、账单数据生成中,请稍后调用本接口获取 + BillGeneratingError ErrCode = 268490011 +) + +const ( + // OrderStatusInit 订单状态 当前状态 0-订单初始化(未创建成功,不可用于支付) + OrderStatusInit OrderStatus = 0 + // OrderStatusCreated 订单状态 当前状态 1-订单创建成功 + OrderStatusCreated OrderStatus = 1 + // OrderStatusPaid 订单状态 当前状态 2-订单已经支付,待发货 + OrderStatusPaid OrderStatus = 2 + // OrderStatusDelivering 订单状态 当前状态 3-订单发货中 + OrderStatusDelivering OrderStatus = 3 + // OrderStatusDelivered 订单状态 当前状态 4-订单已发货 + OrderStatusDelivered OrderStatus = 4 + // OrderStatusRefunded 订单状态 当前状态 5-订单已经退款 + OrderStatusRefunded OrderStatus = 5 + // OrderStatusClosed 订单状态 当前状态 6-订单已经关闭(不可再使用) + OrderStatusClosed OrderStatus = 6 + // OrderStatusRefundFailed 订单状态 当前状态 7-订单退款失败 + OrderStatusRefundFailed OrderStatus = 7 +) + +const ( + // baseSite 基础网址 + baseSite = "https://api.weixin.qq.com" + + // queryUserBalance 查询虚拟支付余额 + queryUserBalance = "/xpay/query_user_balance" + + // currencyPay 扣减代币(一般用于代币支付) + currencyPay = "/xpay/currency_pay" + + // queryOrder 查询创建的订单(现金单,非代币单) + queryOrder = "/xpay/query_order" + + // cancelCurrencyPay 代币支付退款 (currency_pay 接口的逆操作) + cancelCurrencyPay = "/xpay/cancel_currency_pay" + + // notifyProvideGoods 通知已经发货完成(只能通知现金单),正常通过 xpay_goods_deliver_notify 消息推送返回成功就不需要调用这个 api 接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态 + notifyProvideGoods = "/xpay/notify_provide_goods" + + // presentCurrency 代币赠送接口,由于目前不支付按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回 0 或者返回 268490004(重复操作)为止 + presentCurrency = "/xpay/present_currency" + + // downloadBill 下载账单 + downloadBill = "/xpay/download_bill" + + // refundOrder 退款 对使用 jsapi 接口下的单进行退款 + refundOrder = "/xpay/refund_order" + + // createWithdrawOrder 创建提现单 + createWithdrawOrder = "/xpay/create_withdraw_order" + + // queryWithdrawOrder 查询提现单 + queryWithdrawOrder = "/xpay/query_withdraw_order" + + // startUploadGoods 启动批量上传道具任务 + startUploadGoods = "/xpay/start_upload_goods" + + // queryUploadGoods 查询批量上传道具任务状态 + queryUploadGoods = "/xpay/query_upload_goods" + + // startPublishGoods 启动批量发布道具任务 + startPublishGoods = "/xpay/start_publish_goods" + + // queryPublishGoods 查询批量发布道具任务状态 + queryPublishGoods = "/xpay/query_publish_goods" +) + +const ( + // signature user mode signature + signature = "signature" + + // paySignature payment signature + paySignature = "pay_sig" + + // accessToken access_token authorization tokens + accessToken = "access_token" + + // EmptyString empty string + EmptyString = "" +) diff --git a/miniprogram/virtualpayment/doc.go b/miniprogram/virtualpayment/doc.go new file mode 100644 index 0000000..e3501d1 --- /dev/null +++ b/miniprogram/virtualpayment/doc.go @@ -0,0 +1,32 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +// Package virtualpayment mini program virtual payment +package virtualpayment + +import ( + "github.com/silenceper/wechat/v2/miniprogram/context" +) + +// NewVirtualPayment 实例化小程序虚拟支付 API +func NewVirtualPayment(ctx *context.Context) *VirtualPayment { + return &VirtualPayment{ + ctx: ctx, + } +} diff --git a/miniprogram/virtualpayment/domain.go b/miniprogram/virtualpayment/domain.go new file mode 100644 index 0000000..387e7f0 --- /dev/null +++ b/miniprogram/virtualpayment/domain.go @@ -0,0 +1,427 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +package virtualpayment + +import ( + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +// VirtualPayment mini program virtual payment +// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html#_2-3-%E6%9C%8D%E5%8A%A1%E5%99%A8API +type VirtualPayment struct { + ctx *context.Context + sessionKey string +} + +// Env Environment 0 - Production environment 1 - Sandbox environment +type Env int + +// ErrCode error code +type ErrCode int + +// OrderStatus 订单状态 +type OrderStatus int + +// CommonRequest common request parameters +type CommonRequest struct { + OpenID string `json:"openid"` // The user's openID + Env Env `json:"env"` // Environment 0 - Production environment 1 - Sandbox environment +} + +// PaymentRequest payment request parameters +type PaymentRequest struct { + SignData string `json:"sign_data"` // 具体支付参数见 signData, 该参数需以 string 形式传递,例如 signData: '{"offerId":"123","buyQuantity":1,"env":0,"currencyType":"CNY","platform":"android","productId":"testproductId","goodsPrice":10,"outTradeNo":"xxxxxx","attach":"testdata"}' + Mode string `json:"mode"` // 支付模式,枚举值:short_series_goods: 道具直购,short_series_coin: 代币充值 + PaySig string `json:"pay_sig"` // 支付签名,具体生成方式见下方说明 + Signature string `json:"signature"` // 用户态签名,具体生成方式见下方说明 +} + +// SignData 签名数据 +type SignData struct { + OfferID string `json:"offerId"` // 在米大师侧申请的应用 id, mp-支付基础配置中的 offerid + BuyQuantity int `json:"buyQuantity"` // 购买数量 + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 + CurrencyType string `json:"currencyType"` // 币种 默认值:CNY 人民币 + Platform string `json:"platform,omitempty"` // 申请接入时的平台,platform 与应用 id 有关 默认值:android 安卓平台 + ProductID string `json:"productId,omitempty"` // 道具 ID, **该字段仅 mode=short_series_goods 时可用** + GoodsPrice int `json:"goodsPrice"` // 道具单价 (分), **该字段仅 mode=short_series_goods 时可用**, 用来校验价格与后台道具价格是否一致,避免用户在业务商城页看到的价格与实际价格不一致导致投诉 + OutTradeNo string `json:"outTradeNo"` // 业务订单号,每个订单号只能使用一次,重复使用会失败 (极端情况不保证唯一,不建议业务强依赖唯一性). 要求 8-32 个字符内,只能是数字、大小写字母、符号 _-|*@组成,不能以下划线 (_) 开头 + Attach string `json:"attach"` // 透传数据,发货通知时会透传给开发者 +} + +// QueryUserBalanceRequest 查询用户代币余额,请求参数 +// 1. 需要用户态签名与支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type QueryUserBalanceRequest struct { + CommonRequest + UserIP string `json:"user_ip"` // 用户 ip,例如:1.1.1.1 +} + +// QueryUserBalanceResponse 查询虚拟支付余额 响应参数 +type QueryUserBalanceResponse struct { + util.CommonError + Balance int `json:"balance"` // 代币总余额,包括有价和赠送部分 + PresentBalance int `json:"present_balance"` // 赠送账户的代币余额 + SumSave int `json:"sum_save"` // 累计有价货币充值数量 + SumPresent int `json:"sum_present"` // 累计赠送无价货币数量 + SumBalance int `json:"sum_balance"` // 历史总增加的代币金额 + SumCost int `json:"sum_cost"` // 历史总消耗代币金额 + FirstSaveFlag int `json:"first_save_flag"` // 是否满足首充活动标记。0:不满足。1:满足 +} + +// CurrencyPayRequest 扣减代币(一般用于代币支付) +// 1. 需要用户态签名与支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type CurrencyPayRequest struct { + CommonRequest + UserIP string `json:"user_ip"` // 用户 ip,例如:1.1.1.1 + Amount int `json:"amount"` // 支付的代币数量 + OrderID string `json:"order_id"` // 商户订单号,需要保证唯一性 + PayItem string `json:"payitem"` // 物品信息。记录到账户流水中。如:[{"productid":"物品 id", "unit_price": 单价,"quantity": 数量}] + Remark string `json:"remark"` // 备注信息。需要在账单中展示 + DeviceType string `json:"device_type"` // 平台类型 1-安卓 2-苹果 +} + +// PayItem 物品信息 +type PayItem struct { + ProductID string `json:"productid"` // 物品 id + UnitPrice int `json:"unit_price"` // 单价 + Quantity int `json:"quantity"` // 数量 +} + +// CurrencyPayResponse 扣减代币(一般用于代币支付)响应参数 +type CurrencyPayResponse struct { + util.CommonError + OrderID string `json:"order_id"` // 商户订单号 + Balance int `json:"balance"` // 总余额,包括有价和赠送部分 + UsedPresentAmount int `json:"used_present_amount"` // 使用赠送部分的代币数量 +} + +// QueryOrderRequest 查询创建的订单(现金单,非代币单),请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type QueryOrderRequest struct { + CommonRequest + OrderID string `json:"order_id,omitempty"` // 商户订单号 创建的订单号 + WxOrderID string `json:"wx_order_id,omitempty"` // 微信内部单号 (与 order_id 二选一) +} + +// OrderItem 订单信息 +type OrderItem struct { + OrderID string `json:"order_id"` // 商户订单号 + CreateTime int64 `json:"create_time"` // 订单创建时间 + UpdateTime int64 `json:"update_time"` // 订单更新时间 + Status OrderStatus `json:"status"` // 订单状态 当前状态 0-订单初始化(未创建成功,不可用于支付)1-订单创建成功 2-订单已经支付,待发货 3-订单发货中 4-订单已发货 5-订单已经退款 6-订单已经关闭(不可再使用)7-订单退款失败 + BizType int `json:"biz_type"` // 业务类型 0-短剧 + OrderFee int `json:"order_fee"` // 订单金额,单位:分 + CouponFee int `json:"coupon_fee"` // 优惠金额,单位:分 + PaidFee int `json:"paid_fee"` // 用户支付金额,单位:分 + OrderType int `json:"order_type"` // 订单类型 0-支付单 1-退款单 + RefundFee int `json:"refund_fee"` // 当类型为退款单时表示退款金额,单位分 + PaidTime int64 `json:"paid_time"` // 支付/退款时间,unix秒级时间戳 + ProvideTime int64 `json:"provide_time"` // 发货时间,unix 秒级时间戳 + BizMeta string `json:"biz_meta"` // 业务自定义数据 订单创建时传的信息 + EnvType int `json:"env_type"` // 环境类型 1-现网 2-沙箱 + Token string `json:"token"` // 下单时米大师返回的 token + LeftFee int `json:"left_fee"` // 支付单类型时表示此单经过退款还剩余的金额,单位:分 + WxOrderID string `json:"wx_order_id"` // 微信内部单号 + ChannelOrderID string `json:"channel_order_id"` // 渠道订单号,为用户微信支付详情页面上的商户单号 + WxPayOrderID string `json:"wxpay_order_id"` // 微信支付交易单号,为用户微信支付详情页面上的交易单号 +} + +// QueryOrderResponse 查询创建的订单(现金单,非代币单)响应参数 +type QueryOrderResponse struct { + util.CommonError + Order *OrderItem `json:"order"` // 订单信息 +} + +// CancelCurrencyPayRequest 取消订单(现金单,非代币单),请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type CancelCurrencyPayRequest struct { + CommonRequest + UserIP string `json:"user_ip"` // 用户 ip,例如:1.1.1.1 + PayOrderID string `json:"pay_order_id"` // 支付单号 代币支付 (调用 currency_pay 接口时) 时传的 order_id + OrderID string `json:"order_id"` // 本次退款单的单号 + Amount int `json:"amount"` // 退款金额 + DeviceType int `json:"device_type"` // 平台类型 1-安卓 2-苹果 +} + +// CancelCurrencyPayResponse 取消订单(现金单,非代币单)响应参数 +type CancelCurrencyPayResponse struct { + util.CommonError + OrderID string `json:"order_id"` // 退款订单号 +} + +// NotifyProvideGoodsRequest 通知发货,请求参数 +// 通知已经发货完成(只能通知现金单),正常通过 xpay_goods_deliver_notify 消息推送返回成功就不需要调用这个 api 接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type NotifyProvideGoodsRequest struct { + OrderID string `json:"order_id,omitempty"` // 商户订单号 下单时传的单号 + WxOrderID string `json:"wx_order_id,omitempty"` // 微信内部单号 (与 order_id 二选一) + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 +} + +// NotifyProvideGoodsResponse 通知发货响应参数 +type NotifyProvideGoodsResponse struct { + util.CommonError +} + +// PresentCurrencyRequest 赠送代币,请求参数 +// 代币赠送接口,由于目前不支付按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回 0 或者返回 268490004(重复操作)为止 +// 1. 需要用户态签名与支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type PresentCurrencyRequest struct { + CommonRequest + OrderID string `json:"order_id"` // 赠送单号,商户订单号,需要保证唯一性 + Amount int `json:"amount"` // 赠送的代币数量 + DeviceType string `json:"device_type"` // 平台类型 1-安卓 2-苹果 +} + +// PresentCurrencyResponse 赠送代币响应参数 +type PresentCurrencyResponse struct { + util.CommonError + Balance int `json:"balance"` // 赠送后用户的代币余额 + OrderID string `json:"order_id"` // 赠送单号 + PresentBalance int `json:"present_balance"` // 用户收到的总赠送金额 +} + +// DownloadBillRequest 下载账单,请求参数 +// 用于下载小程序账单,第一次调用触发生成下载 url,可以间隔轮训来获取最终生成的下载 url。账单中金额相关字段是以分为单位。 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type DownloadBillRequest struct { + BeginDs string `json:"begin_ds"` // 账单开始日期,格式为 yyyymmdd 起始时间(如 20230801) + EndDs string `json:"end_ds"` // 账单结束日期,格式为 yyyymmdd 结束时间(如 20230801) +} + +// DownloadBillResponse 下载账单响应参数 +type DownloadBillResponse struct { + util.CommonError + URL string `json:"url"` // 账单下载地址 +} + +// RefundOrderRequest 退款,请求参数 +// 对使用 jsapi 接口下的单进行退款 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type RefundOrderRequest struct { + CommonRequest + OrderID string `json:"order_id"` // 商户订单号,需要保证唯一性 + WxOrderID string `json:"wx_order_id"` // 微信内部单号 (与 order_id 二选一) + RefundOrderID string `json:"refund_order_id"` // 退款单号,本次退款时需要传的单号,长度为 [8,32],字符只允许使用字母、数字、'_'、'-' + LeftFee int `json:"left_fee"` // 退款金额,单位:分 当前单剩余可退金额,单位分,可以通过调用 query_order 接口查到 + RefundFee int `json:"refund_fee"` // 退款金额,单位:分 需要 (0,left_fee] 之间 + BizMeta string `json:"biz_meta"` // 商家自定义数据,传入后可在 query_order 接口查询时原样返回,长度需要 [0,1024] + RefundReason string `json:"refund_reason"` // 退款原因,当前仅支持以下值 0-暂无描述 1-产品问题,影响使用或效果不佳 2-售后问题,无法满足需求 3-意愿问题,用户主动退款 4-价格问题 5:其他原因 + ReqFrom string `json:"req_from"` // 退款来源,当前仅支持以下值 1-人工客服退款,即用户电话给客服,由客服发起退款流程 2-用户自己发起退款流程 3-其它 +} + +// RefundOrderResponse 退款响应参数 +type RefundOrderResponse struct { + util.CommonError + RefundOrderID string `json:"refund_order_id"` // 退款单号 + RefundWxOrderID string `json:"refund_wx_order_id"` // 退款单的微信侧单号 + PayOrderID string `json:"pay_order_id"` // 该退款单对应的支付单单号 + PayWxOrderID string `json:"pay_wx_order_id"` // 该退款单对应的支付单微信侧单号 +} + +// CreateWithdrawOrderRequest 创建提现单,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type CreateWithdrawOrderRequest struct { + WithdrawNO string `json:"withdraw_no"` // 提现单单号,长度为 [8,32],字符只允许使用字母、数字、'_'、'-' + WithdrawAmount string `json:"withdraw_amount"` // 提现的金额,单位元,例如提现 1 分钱请使用 0.01 + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 +} + +// CreateWithdrawOrderResponse 创建提现单响应参数 +type CreateWithdrawOrderResponse struct { + util.CommonError + WithdrawNO string `json:"withdraw_no"` // 提现单单号 + WxWithdrawNO string `json:"wx_withdraw_no"` // 提现单的微信侧单号 +} + +// QueryWithdrawOrderRequest 查询提现单,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type QueryWithdrawOrderRequest struct { + WithdrawNO string `json:"withdraw_no"` // 提现单单号,长度为 [8,32],字符只允许使用字母、数字、'_'、'-' (与 wx_withdraw_no 二选一) + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 +} + +// QueryWithdrawOrderResponse 查询提现单响应参数 +type QueryWithdrawOrderResponse struct { + util.CommonError + WithdrawNO string `json:"withdraw_no"` // 提现单单号 + Status int `json:"status"` // 提现单的微信侧单号 1-创建成功,提现中 2-提现成功 3-提现失败 + WithdrawAmount string `json:"withdraw_amount"` // 提现的金额,单位元,例如提现 1 分钱请使用 0.01 + WxWithdrawNo string `json:"wx_withdraw_no"` // 提现单的微信侧单号 + WithdrawSuccessTimestamp int64 `json:"withdraw_success_timestamp"` // 提现单成功的秒级时间戳,unix 秒级时间戳 + CreateTime string `json:"create_time"` // 提现单创建时间 + FailReason string `json:"failReason"` // 提现失败的原因 +} + +// StartUploadGoodsRequest 启动批量上传道具任务,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type StartUploadGoodsRequest struct { + UploadItem []*UploadItem `json:"upload_item"` // 道具信息 + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 +} + +// UploadItem 道具信息 +type UploadItem struct { + ID string `json:"id"` // 道具 id,长度 (0,64],字符只允许使用字母、数字、'_'、'-' + Name string `json:"name"` // 道具名称,长度 (0,1024] + Price int `json:"price"` // 道具单价,单位分,需要大于 0 + Remark string `json:"remark"` // 道具备注,长度 (0,1024] + ItemURL string `json:"item_url"` // 道具图片的 url 地址,当前仅支持 jpg,png 等格式 + UploadStatus int `json:"upload_status,omitempty"` // 上传状态 0-上传中 1-id 已经存在 2-上传成功 3-上传失败 + ErrMsg string `json:"errmsg,omitempty"` // 上传失败的原因 +} + +// StartUploadGoodsResponse 启动批量上传道具任务响应参数 +type StartUploadGoodsResponse struct { + util.CommonError +} + +// QueryUploadGoodsRequest 查询批量上传道具任务,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type QueryUploadGoodsRequest struct { + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 +} + +// QueryUploadGoodsResponse 查询批量上传道具任务响应参数 +type QueryUploadGoodsResponse struct { + util.CommonError + UploadItem []*UploadItem `json:"upload_item"` // 道具信息列表 + Status int `json:"status"` // 任务状态 0-无任务在运行 1-任务运行中 2-上传失败或部分失败(上传任务已经完成)3-上传成功 +} + +// StartPublishGoodsRequest 启动批量发布道具任务,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type StartPublishGoodsRequest struct { + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 + PublishItem []*PublishItem `json:"publish_item"` // 道具信息 发布的商品列表 +} + +// PublishItem 道具信息 +type PublishItem struct { + ID string `json:"id"` // 道具 id,添加到开发环境时传的道具 id,长度 (0,64],字符只允许使用字母、数字、'_'、'-' + PublishStatus int `json:"publish_status,omitempty"` // 发布状态 0-上传中 1-id 已经存在 2-发布成功 3-发布失败 + ErrMsg string `json:"errmsg,omitempty"` // 发布失败的原因 +} + +// StartPublishGoodsResponse 启动批量发布道具任务响应参数 +type StartPublishGoodsResponse struct { + util.CommonError +} + +// QueryPublishGoodsRequest 查询批量发布道具任务,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type QueryPublishGoodsRequest struct { + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 +} + +// QueryPublishGoodsResponse 查询批量发布道具任务响应参数 +type QueryPublishGoodsResponse struct { + util.CommonError + PublishItem []*PublishItem `json:"publish_item"` // 道具信息列表 + Status int `json:"status"` // 任务状态 0-无任务在运行 1-任务运行中 2-上传失败或部分失败(上传任务已经完成)3-上传成功 + +} + +// AsyncXPayGoodsDeliverNotifyRequest 异步通知发货,请求参数 +// 1. 使用支付签名 +// POST,请求参数为 json 字符串,Content-Type 为 application/json +type AsyncXPayGoodsDeliverNotifyRequest struct { + ToUserName string `json:"ToUserName"` // 小程序的原始 ID + FromUserName string `json:"FromUserName"` // 发送方帐号(一个 OpenID)该事件消息的 openid,道具发货场景固定为微信官方的 openid + CreateTime int `json:"CreateTime"` // 消息发送时间(整型) + MsgType string `json:"MsgType"` // 消息类型,此时固定为:event + Event string `json:"Event"` // 事件类型,此时固定为:xpay_goods_deliver_notify + Openid string `json:"openid"` // 用户 openid + OutTradeNo string `json:"OutTradeNo"` // 业务订单号 + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 + WechatPayInfo *WeChatPayInfo `json:"WechatPayInfo"` // 微信支付订单信息 + GoodsInfo *GoodsInfo `json:"GoodsInfo"` // 道具信息 +} + +// WeChatPayInfo 微信支付信息 非微信支付渠道可能没有 +type WeChatPayInfo struct { + MchOrderNo string `json:"MchOrderNo"` // 商户订单号 + TransactionID string `json:"TransactionId"` // 微信支付订单号 +} + +// GoodsInfo 道具参数信息 +type GoodsInfo struct { + ProductID string `json:"ProductId"` // 道具 ID + Quantity int `json:"Quantity"` // 数量 + OrigPrice int `json:"OrigPrice"` // 物品原始价格(单位:分) + ActualPrice int `json:"ActualPrice"` // 物品实际支付价格(单位:分) + Attach string `json:"Attach"` // 透传信息 +} + +// AsyncXPayGoodsDeliverNotifyResponse 异步通知发货响应参数 +type AsyncXPayGoodsDeliverNotifyResponse struct { + util.CommonError +} + +// AsyncXPayCoinPayNotifyRequest 异步通知代币支付推送,请求参数 +type AsyncXPayCoinPayNotifyRequest struct { + ToUserName string `json:"ToUserName"` // 小程序的原始 ID + FromUserName string `json:"FromUserName"` // 发送方帐号(一个 OpenID)该事件消息的 openid,道具发货场景固定为微信官方的 openid + CreateTime int `json:"CreateTime"` // 消息发送时间(整型) + MsgType string `json:"MsgType"` // 消息类型,此时固定为:event + Event string `json:"Event"` // 事件类型,此时固定为:xpay_goods_deliver_notify + Openid string `json:"openid"` // 用户 openid + OutTradeNo string `json:"OutTradeNo"` // 业务订单号 + Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境 + WechatPayInfo *WeChatPayInfo `json:"WechatPayInfo"` // 微信支付订单信息 + CoinInfo *CoinInfo `json:"GoodsInfo"` // 道具信息 +} + +// CoinInfo 代币信息 +type CoinInfo struct { + Quantity int `json:"Quantity"` // 数量 + OrigPrice int `json:"OrigPrice"` // 物品原始价格(单位:分) + ActualPrice int `json:"ActualPrice"` // 物品实际支付价格(单位:分) + Attach string `json:"Attach"` // 透传信息 +} + +// AsyncXPayCoinPayNotifyResponse 异步通知代币支付推送响应参数 +type AsyncXPayCoinPayNotifyResponse struct { + util.CommonError +} + +// URLParams url parameter +type URLParams struct { + Path string `json:"path"` + AccessToken string `json:"access_token"` + PaySign string `json:"paySign"` + Signature string `json:"signature"` + Content string `json:"content"` +} diff --git a/miniprogram/virtualpayment/virtualpayment.go b/miniprogram/virtualpayment/virtualpayment.go new file mode 100644 index 0000000..1da90c3 --- /dev/null +++ b/miniprogram/virtualpayment/virtualpayment.go @@ -0,0 +1,516 @@ +/* + * Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You can obtain one at https://github.com/silenceper/wechat. + * + */ + +package virtualpayment + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "strings" + + "github.com/silenceper/wechat/v2/util" +) + +// SetSessionKey 设置 sessionKey +func (s *VirtualPayment) SetSessionKey(sessionKey string) { + s.sessionKey = sessionKey +} + +// QueryUserBalance 查询虚拟支付余额 +func (s *VirtualPayment) QueryUserBalance(ctx context.Context, in *QueryUserBalanceRequest) (out QueryUserBalanceResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: queryUserBalance, + Content: string(jsonByte), + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "QueryUserBalance") + return +} + +// CurrencyPay currency pay 扣减代币(一般用于代币支付) +func (s *VirtualPayment) CurrencyPay(ctx context.Context, in *CurrencyPayRequest) (out CurrencyPayResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: currencyPay, + Content: string(jsonByte), + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "CurrencyPay") + return +} + +// QueryOrder 查询创建的订单(现金单,非代币单) +func (s *VirtualPayment) QueryOrder(ctx context.Context, in *QueryOrderRequest) (out QueryOrderResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: queryOrder, + Signature: EmptyString, + Content: string(jsonByte), + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "QueryOrder") + return +} + +// CancelCurrencyPay 取消订单 代币支付退款 (currency_pay 接口的逆操作) +func (s *VirtualPayment) CancelCurrencyPay(ctx context.Context, in *CancelCurrencyPayRequest) (out CancelCurrencyPayResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: cancelCurrencyPay, + Content: string(jsonByte), + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "CancelCurrencyPay") + return +} + +// NotifyProvideGoods 通知发货 +// 通知已经发货完成(只能通知现金单),正常通过 xpay_goods_deliver_notify 消息推送返回成功就不需要调用这个 api 接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态 +func (s *VirtualPayment) NotifyProvideGoods(ctx context.Context, in *NotifyProvideGoodsRequest) (out NotifyProvideGoodsResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: notifyProvideGoods, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "NotifyProvideGoods") + return +} + +// PresentCurrency 代币赠送接口,由于目前不支付按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回 0 或者返回 268490004(重复操作)为止 +func (s *VirtualPayment) PresentCurrency(ctx context.Context, in *PresentCurrencyRequest) (out PresentCurrencyResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: presentCurrency, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "PresentCurrency") + return +} + +// DownloadBill 下载订单交易账单 +func (s *VirtualPayment) DownloadBill(ctx context.Context, in *DownloadBillRequest) (out DownloadBillResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: downloadBill, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "DownloadBill") + return +} + +// RefundOrder 退款 对使用 jsapi 接口下的单进行退款 +func (s *VirtualPayment) RefundOrder(ctx context.Context, in *RefundOrderRequest) (out RefundOrderResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: refundOrder, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "RefundOrder") + return +} + +// CreateWithdrawOrder 创建提现单 +func (s *VirtualPayment) CreateWithdrawOrder(ctx context.Context, in *CreateWithdrawOrderRequest) (out CreateWithdrawOrderResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: createWithdrawOrder, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "CreateWithdrawOrder") + return +} + +// QueryWithdrawOrder 查询提现单 +func (s *VirtualPayment) QueryWithdrawOrder(ctx context.Context, in *QueryWithdrawOrderRequest) (out QueryWithdrawOrderResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: queryWithdrawOrder, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "QueryWithdrawOrder") + return +} + +// StartUploadGoods 开始上传商品 +func (s *VirtualPayment) StartUploadGoods(ctx context.Context, in *StartUploadGoodsRequest) (out StartUploadGoodsResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: startUploadGoods, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "StartUploadGoods") + return +} + +// QueryUploadGoods 查询上传商品 +func (s *VirtualPayment) QueryUploadGoods(ctx context.Context, in *QueryUploadGoodsRequest) (out QueryUploadGoodsResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: queryUploadGoods, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "QueryUploadGoods") + return +} + +// StartPublishGoods 开始发布商品 +func (s *VirtualPayment) StartPublishGoods(ctx context.Context, in *StartPublishGoodsRequest) (out StartPublishGoodsResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: startPublishGoods, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "StartPublishGoods") + return +} + +// QueryPublishGoods 查询发布商品 +func (s *VirtualPayment) QueryPublishGoods(ctx context.Context, in *QueryPublishGoodsRequest) (out QueryPublishGoodsResponse, err error) { + var jsonByte []byte + if jsonByte, err = json.Marshal(in); err != nil { + return + } + + var ( + params = URLParams{ + Path: queryPublishGoods, + Content: string(jsonByte), + Signature: EmptyString, + } + address string + ) + if address, err = s.requestAddress(params); err != nil { + return + } + + var response []byte + if response, err = util.PostJSONContext(ctx, address, in); err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &out, "QueryPublishGoods") + return +} + +// hmacSha256 hmac sha256 +func (s *VirtualPayment) hmacSha256(key, data string) string { + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +// PaySign pay sign +func (s *VirtualPayment) PaySign(url, data string) (string, error) { + if strings.TrimSpace(s.ctx.Config.AppKey) == "" { + return "", errors.New("appKey is empty") + } + return s.hmacSha256(s.ctx.Config.AppKey, url+"&"+data), nil +} + +// Signature user signature +func (s *VirtualPayment) Signature(data string) (string, error) { + if strings.TrimSpace(s.sessionKey) == "" { + return "", errors.New("sessionKey is empty") + } + return s.hmacSha256(s.sessionKey, data), nil +} + +// PaySignature pay sign and signature +func (s *VirtualPayment) PaySignature(url, data string) (paySign, signature string, err error) { + if paySign, err = s.PaySign(url, data); err != nil { + return + } + if signature, err = s.Signature(data); err != nil { + return + } + return +} + +// requestURL .组合 URL +func (s *VirtualPayment) requestAddress(params URLParams) (url string, err error) { + switch params.Path { + case queryUserBalance: + case currencyPay: + case cancelCurrencyPay: + if params.PaySign, params.Signature, err = s.PaySignature(params.Path, params.Content); err != nil { + return + } + case queryOrder: + case notifyProvideGoods: + case presentCurrency: + case downloadBill: + case refundOrder: + case createWithdrawOrder: + case queryWithdrawOrder: + case startUploadGoods: + case queryUploadGoods: + case startPublishGoods: + case queryPublishGoods: + if params.PaySign, err = s.PaySign(params.Path, params.Content); err != nil { + return + } + default: + err = errors.New("path is not exist") + return + } + + if params.AccessToken, err = s.ctx.GetAccessToken(); err != nil { + return + } + + url = baseSite + params.Path + "?" + accessToken + "=" + params.AccessToken + if params.PaySign != EmptyString { + url += "&" + paySignature + "=" + params.PaySign + } + if params.Signature != EmptyString { + url += "&" + signature + "=" + params.Signature + } + return +} diff --git a/officialaccount/basic/basic.go b/officialaccount/basic/basic.go index c0f02ba..756db97 100644 --- a/officialaccount/basic/basic.go +++ b/officialaccount/basic/basic.go @@ -8,34 +8,34 @@ import ( ) var ( - //获取微信服务器IP地址 - //文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_the_WeChat_server_IP_address.html + // 获取微信服务器IP地址 + // 文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_the_WeChat_server_IP_address.html getCallbackIPURL = "https://api.weixin.qq.com/cgi-bin/getcallbackip" getAPIDomainIPURL = "https://api.weixin.qq.com/cgi-bin/get_api_domain_ip" - //清理接口调用次数 + // 清理接口调用次数 clearQuotaURL = "https://api.weixin.qq.com/cgi-bin/clear_quota" ) -//Basic struct +// Basic struct type Basic struct { *context.Context } -//NewBasic 实例 +// NewBasic 实例 func NewBasic(context *context.Context) *Basic { basic := new(Basic) basic.Context = context return basic } -//IPListRes 获取微信服务器IP地址 返回结果 +// IPListRes 获取微信服务器IP地址 返回结果 type IPListRes struct { util.CommonError IPList []string `json:"ip_list"` } -//GetCallbackIP 获取微信callback IP地址 +// GetCallbackIP 获取微信callback IP地址 func (basic *Basic) GetCallbackIP() ([]string, error) { ak, err := basic.GetAccessToken() if err != nil { @@ -51,7 +51,7 @@ func (basic *Basic) GetCallbackIP() ([]string, error) { return ipListRes.IPList, err } -//GetAPIDomainIP 获取微信API接口 IP地址 +// GetAPIDomainIP 获取微信API接口 IP地址 func (basic *Basic) GetAPIDomainIP() ([]string, error) { ak, err := basic.GetAccessToken() if err != nil { @@ -67,7 +67,7 @@ func (basic *Basic) GetAPIDomainIP() ([]string, error) { return ipListRes.IPList, err } -//ClearQuota 清理接口调用次数 +// ClearQuota 清理接口调用次数 func (basic *Basic) ClearQuota() error { ak, err := basic.GetAccessToken() if err != nil { diff --git a/officialaccount/broadcast/broadcast.go b/officialaccount/broadcast/broadcast.go index 3093afa..885f683 100644 --- a/officialaccount/broadcast/broadcast.go +++ b/officialaccount/broadcast/broadcast.go @@ -17,42 +17,42 @@ const ( setSpeedSendURL = "https://api.weixin.qq.com/cgi-bin/message/mass/speed/set" ) -//MsgType 发送消息类型 +// MsgType 发送消息类型 type MsgType string const ( - //MsgTypeNews 图文消息 + // MsgTypeNews 图文消息 MsgTypeNews MsgType = "mpnews" - //MsgTypeText 文本 + // MsgTypeText 文本 MsgTypeText MsgType = "text" - //MsgTypeVoice 语音/音频 + // MsgTypeVoice 语音/音频 MsgTypeVoice MsgType = "voice" - //MsgTypeImage 图片 + // MsgTypeImage 图片 MsgTypeImage MsgType = "image" - //MsgTypeVideo 视频 + // MsgTypeVideo 视频 MsgTypeVideo MsgType = "mpvideo" - //MsgTypeWxCard 卡券 + // MsgTypeWxCard 卡券 MsgTypeWxCard MsgType = "wxcard" ) -//Broadcast 群发消息 +// Broadcast 群发消息 type Broadcast struct { *context.Context preview bool } -//NewBroadcast new +// NewBroadcast new func NewBroadcast(ctx *context.Context) *Broadcast { return &Broadcast{ctx, false} } -//User 发送的用户 +// User 发送的用户 type User struct { TagID int64 OpenID []string } -//Result 群发返回结果 +// Result 群发返回结果 type Result struct { util.CommonError MsgID int64 `json:"msg_id"` @@ -60,34 +60,34 @@ type Result struct { MsgStatus string `json:"msg_status"` } -//SpeedResult 群发速度返回结果 +// SpeedResult 群发速度返回结果 type SpeedResult struct { util.CommonError Speed int64 `json:"speed"` RealSpeed int64 `json:"realspeed"` } -//sendRequest 发送请求的数据 +// sendRequest 发送请求的数据 type sendRequest struct { - //根据tag获全部发送 + // 根据tag获全部发送 Filter map[string]interface{} `json:"filter,omitempty"` - //根据OpenID发送 + // 根据OpenID发送 ToUser interface{} `json:"touser,omitempty"` - //发送文本 + // 发送文本 Text map[string]interface{} `json:"text,omitempty"` - //发送图文消息 + // 发送图文消息 Mpnews map[string]interface{} `json:"mpnews,omitempty"` - //发送语音 + // 发送语音 Voice map[string]interface{} `json:"voice,omitempty"` - //发送图片 + // 发送图片 Images *Image `json:"images,omitempty"` - //发送卡券 + // 发送卡券 WxCard map[string]interface{} `json:"wxcard,omitempty"` MsgType MsgType `json:"msgtype"` SendIgnoreReprint int32 `json:"send_ignore_reprint,omitempty"` } -//Image 发送图片 +// Image 发送图片 type Image struct { MediaIDs []string `json:"media_ids"` Recommend string `json:"recommend"` @@ -95,10 +95,10 @@ type Image struct { OnlyFansCanComment int32 `json:"only_fans_can_comment"` } -//SendText 群发文本 -//user 为nil,表示全员发送 -//&User{TagID:2} 根据tag发送 -//&User{OpenID:[]string("xxx","xxx")} 根据openid发送 +// SendText 群发文本 +// user 为nil,表示全员发送 +// &User{TagID:2} 根据tag发送 +// &User{OpenID:[]string("xxx","xxx")} 根据openid发送 func (broadcast *Broadcast) SendText(user *User, content string) (*Result, error) { ak, err := broadcast.GetAccessToken() if err != nil { @@ -122,7 +122,7 @@ func (broadcast *Broadcast) SendText(user *User, content string) (*Result, error return res, err } -//SendNews 发送图文 +// SendNews 发送图文 func (broadcast *Broadcast) SendNews(user *User, mediaID string, ignoreReprint bool) (*Result, error) { ak, err := broadcast.GetAccessToken() if err != nil { @@ -149,7 +149,7 @@ func (broadcast *Broadcast) SendNews(user *User, mediaID string, ignoreReprint b return res, err } -//SendVoice 发送语音 +// SendVoice 发送语音 func (broadcast *Broadcast) SendVoice(user *User, mediaID string) (*Result, error) { ak, err := broadcast.GetAccessToken() if err != nil { @@ -173,7 +173,7 @@ func (broadcast *Broadcast) SendVoice(user *User, mediaID string) (*Result, erro return res, err } -//SendImage 发送图片 +// SendImage 发送图片 func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error) { ak, err := broadcast.GetAccessToken() if err != nil { @@ -195,7 +195,7 @@ func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error return res, err } -//SendVideo 发送视频 +// SendVideo 发送视频 func (broadcast *Broadcast) SendVideo(user *User, mediaID string, title, description string) (*Result, error) { ak, err := broadcast.GetAccessToken() if err != nil { @@ -221,7 +221,7 @@ func (broadcast *Broadcast) SendVideo(user *User, mediaID string, title, descrip return res, err } -//SendWxCard 发送卡券 +// SendWxCard 发送卡券 func (broadcast *Broadcast) SendWxCard(user *User, cardID string) (*Result, error) { ak, err := broadcast.GetAccessToken() if err != nil { @@ -245,7 +245,7 @@ func (broadcast *Broadcast) SendWxCard(user *User, cardID string) (*Result, erro return res, err } -//Delete 删除群发消息 +// Delete 删除群发消息 func (broadcast *Broadcast) Delete(msgID int64, articleIDx int64) error { ak, err := broadcast.GetAccessToken() if err != nil { diff --git a/officialaccount/config/config.go b/officialaccount/config/config.go index 7846801..b38fcce 100644 --- a/officialaccount/config/config.go +++ b/officialaccount/config/config.go @@ -4,11 +4,11 @@ import ( "github.com/silenceper/wechat/v2/cache" ) -// Config config for 微信公众号 +// Config .config for 微信公众号 type Config struct { - AppID string `json:"app_id"` //appid - AppSecret string `json:"app_secret"` //appsecret - Token string `json:"token"` //token - EncodingAESKey string `json:"encoding_aes_key"` //EncodingAESKey + AppID string `json:"app_id"` // appid + AppSecret string `json:"app_secret"` // appsecret + Token string `json:"token"` // token + EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey Cache cache.Cache } diff --git a/officialaccount/customerservice/manager.go b/officialaccount/customerservice/manager.go new file mode 100644 index 0000000..6422fb7 --- /dev/null +++ b/officialaccount/customerservice/manager.go @@ -0,0 +1,253 @@ +package customerservice + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/util" +) + +// TypingStatus 输入状态类型 +type TypingStatus string + +const ( + customerServiceListURL = "https://api.weixin.qq.com/cgi-bin/customservice/getkflist" + customerServiceOnlineListURL = "https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist" + customerServiceAddURL = "https://api.weixin.qq.com/customservice/kfaccount/add" + customerServiceUpdateURL = "https://api.weixin.qq.com/customservice/kfaccount/update" + customerServiceDeleteURL = "https://api.weixin.qq.com/customservice/kfaccount/del" + customerServiceInviteURL = "https://api.weixin.qq.com/customservice/kfaccount/inviteworker" + customerServiceUploadHeadImg = "https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg" + customerServiceTypingURL = "https://api.weixin.qq.com/cgi-bin/message/custom/typing" +) + +const ( + // Typing 表示正在输入状态 + Typing TypingStatus = "Typing" + // CancelTyping 表示取消正在输入状态 + CancelTyping TypingStatus = "CancelTyping" +) + +// Manager 客服管理者,可以管理客服 +type Manager struct { + *context.Context +} + +// NewCustomerServiceManager 实例化客服管理 +func NewCustomerServiceManager(ctx *context.Context) *Manager { + csm := new(Manager) + csm.Context = ctx + return csm +} + +// KeFuInfo 客服基本信息 +type KeFuInfo struct { + KfAccount string `json:"kf_account"` // 完整客服帐号,格式为:帐号前缀@公众号微信号 + KfNick string `json:"kf_nick"` // 客服昵称 + KfID int `json:"kf_id"` // 客服编号 + KfHeadImgURL string `json:"kf_headimgurl"` // 客服头像 + KfWX string `json:"kf_wx"` // 如果客服帐号已绑定了客服人员微信号, 则此处显示微信号 + InviteWX string `json:"invite_wx"` // 如果客服帐号尚未绑定微信号,但是已经发起了一个绑定邀请, 则此处显示绑定邀请的微信号 + InviteExpTime int `json:"invite_expire_time"` // 如果客服帐号尚未绑定微信号,但是已经发起过一个绑定邀请, 邀请的过期时间,为unix 时间戳 + InviteStatus string `json:"invite_status"` // 邀请的状态,有等待确认“waiting”,被拒绝“rejected”, 过期“expired” +} + +type resKeFuList struct { + util.CommonError + KfList []*KeFuInfo `json:"kf_list"` +} + +// List 获取所有客服基本信息 +func (csm *Manager) List() (customerServiceList []*KeFuInfo, err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceListURL, accessToken) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var res resKeFuList + err = util.DecodeWithError(response, &res, "ListCustomerService") + if err != nil { + return + } + customerServiceList = res.KfList + return +} + +// KeFuOnlineInfo 客服在线信息 +type KeFuOnlineInfo struct { + KfAccount string `json:"kf_account"` + Status int `json:"status"` + KfID int `json:"kf_id"` + AcceptedCase int `json:"accepted_case"` +} + +type resKeFuOnlineList struct { + util.CommonError + KfOnlineList []*KeFuOnlineInfo `json:"kf_online_list"` +} + +// OnlineList 获取在线客服列表 +func (csm *Manager) OnlineList() (customerServiceOnlineList []*KeFuOnlineInfo, err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceOnlineListURL, accessToken) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var res resKeFuOnlineList + err = util.DecodeWithError(response, &res, "ListOnlineCustomerService") + if err != nil { + return + } + customerServiceOnlineList = res.KfOnlineList + return +} + +// Add 添加客服账号 +func (csm *Manager) Add(kfAccount, nickName string) (err error) { + // kfAccount:完整客服帐号,格式为:帐号前缀@公众号微信号,帐号前缀最多10个字符,必须是英文、数字字符或者下划线,后缀为公众号微信号,长度不超过30个字符 + // nickName:客服昵称,最长16个字 + // 参数此处均不做校验 + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceAddURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + NickName string `json:"nickname"` + }{ + KfAccount: kfAccount, + NickName: nickName, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "AddCustomerService") + return +} + +// Update 修改客服账号 +func (csm *Manager) Update(kfAccount, nickName string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceUpdateURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + NickName string `json:"nickname"` + }{ + KfAccount: kfAccount, + NickName: nickName, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "UpdateCustomerService") + return +} + +// Delete 删除客服帐号 +func (csm *Manager) Delete(kfAccount string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceDeleteURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + }{ + KfAccount: kfAccount, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "DeleteCustomerService") + return +} + +// InviteBind 邀请绑定客服帐号和微信号 +func (csm *Manager) InviteBind(kfAccount, inviteWX string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceInviteURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + InviteWX string `json:"invite_wx"` + }{ + KfAccount: kfAccount, + InviteWX: inviteWX, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "InviteBindCustomerService") + return +} + +// UploadHeadImg 上传客服头像 +func (csm *Manager) UploadHeadImg(kfAccount, fileName string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s&kf_account=%s", customerServiceUploadHeadImg, accessToken, kfAccount) + var response []byte + response, err = util.PostFile("media", fileName, uri) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "UploadCustomerServiceHeadImg") + return +} + +// SendTypingStatus 下发客服输入状态给用户 +func (csm *Manager) SendTypingStatus(openid string, cmd TypingStatus) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceTypingURL, accessToken) + data := struct { + ToUser string `json:"touser"` + Command string `json:"command"` + }{ + ToUser: openid, + Command: string(cmd), + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "SendTypingStatus") + return +} diff --git a/officialaccount/datacube/broadcast.go b/officialaccount/datacube/broadcast.go index 4a3e1a4..7c11858 100644 --- a/officialaccount/datacube/broadcast.go +++ b/officialaccount/datacube/broadcast.go @@ -15,7 +15,7 @@ const ( getUserShareHour = "https://api.weixin.qq.com/datacube/getusersharehour" ) -//ResArticleSummary 获取图文群发每日数据响应 +// ResArticleSummary 获取图文群发每日数据响应 type ResArticleSummary struct { util.CommonError @@ -34,7 +34,7 @@ type ResArticleSummary struct { } `json:"list"` } -//ResArticleTotal 获取图文群发总数据响应 +// ResArticleTotal 获取图文群发总数据响应 type ResArticleTotal struct { util.CommonError @@ -46,7 +46,7 @@ type ResArticleTotal struct { } `json:"list"` } -//ArticleTotalDetails 获取图文群发总数据响应文字详情 +// ArticleTotalDetails 获取图文群发总数据响应文字详情 type ArticleTotalDetails struct { StatDate string `json:"stat_date"` TargetUser int `json:"target_user"` @@ -76,7 +76,7 @@ type ArticleTotalDetails struct { FeedShareFromOtherCnt int `json:"feed_share_from_other_cnt"` } -//ResUserRead 获取图文统计数据响应 +// ResUserRead 获取图文统计数据响应 type ResUserRead struct { util.CommonError @@ -94,7 +94,7 @@ type ResUserRead struct { } `json:"list"` } -//ResUserReadHour 获取图文统计分时数据 +// ResUserReadHour 获取图文统计分时数据 type ResUserReadHour struct { util.CommonError @@ -113,7 +113,7 @@ type ResUserReadHour struct { } `json:"list"` } -//ResUserShare 获取图文分享转发数据 +// ResUserShare 获取图文分享转发数据 type ResUserShare struct { util.CommonError @@ -125,7 +125,7 @@ type ResUserShare struct { } `json:"list"` } -//ResUserShareHour 获取图文分享转发分时数据 +// ResUserShareHour 获取图文分享转发分时数据 type ResUserShareHour struct { util.CommonError @@ -138,7 +138,7 @@ type ResUserShareHour struct { } `json:"list"` } -//GetArticleSummary 获取图文群发每日数据 +// GetArticleSummary 获取图文群发每日数据 func (cube *DataCube) GetArticleSummary(s string, e string) (resArticleSummary ResArticleSummary, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -160,7 +160,7 @@ func (cube *DataCube) GetArticleSummary(s string, e string) (resArticleSummary R return } -//GetArticleTotal 获取图文群发总数据 +// GetArticleTotal 获取图文群发总数据 func (cube *DataCube) GetArticleTotal(s string, e string) (resArticleTotal ResArticleTotal, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -182,7 +182,7 @@ func (cube *DataCube) GetArticleTotal(s string, e string) (resArticleTotal ResAr return } -//GetUserRead 获取图文统计数据 +// GetUserRead 获取图文统计数据 func (cube *DataCube) GetUserRead(s string, e string) (resUserRead ResUserRead, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -204,7 +204,7 @@ func (cube *DataCube) GetUserRead(s string, e string) (resUserRead ResUserRead, return } -//GetUserReadHour 获取图文统计分时数据 +// GetUserReadHour 获取图文统计分时数据 func (cube *DataCube) GetUserReadHour(s string, e string) (resUserReadHour ResUserReadHour, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -226,7 +226,7 @@ func (cube *DataCube) GetUserReadHour(s string, e string) (resUserReadHour ResUs return } -//GetUserShare 获取图文分享转发数据 +// GetUserShare 获取图文分享转发数据 func (cube *DataCube) GetUserShare(s string, e string) (resUserShare ResUserShare, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -248,7 +248,7 @@ func (cube *DataCube) GetUserShare(s string, e string) (resUserShare ResUserShar return } -//GetUserShareHour 获取图文分享转发分时数据 +// GetUserShareHour 获取图文分享转发分时数据 func (cube *DataCube) GetUserShareHour(s string, e string) (resUserShareHour ResUserShareHour, err error) { accessToken, err := cube.GetAccessToken() if err != nil { diff --git a/officialaccount/datacube/datacube.go b/officialaccount/datacube/datacube.go index 22e593d..3dd3af7 100644 --- a/officialaccount/datacube/datacube.go +++ b/officialaccount/datacube/datacube.go @@ -9,12 +9,12 @@ type reqDate struct { EndDate string `json:"end_date"` } -//DataCube 数据统计 +// DataCube 数据统计 type DataCube struct { *context.Context } -//NewCube 数据统计 +// NewCube 数据统计 func NewCube(context *context.Context) *DataCube { dataCube := new(DataCube) dataCube.Context = context diff --git a/officialaccount/datacube/interface.go b/officialaccount/datacube/interface.go index ea6f839..339308d 100644 --- a/officialaccount/datacube/interface.go +++ b/officialaccount/datacube/interface.go @@ -11,7 +11,7 @@ const ( getInterfaceSummaryHour = "https://api.weixin.qq.com/datacube/getinterfacesummaryhour" ) -//ResInterfaceSummary 接口分析数据响应 +// ResInterfaceSummary 接口分析数据响应 type ResInterfaceSummary struct { util.CommonError @@ -24,7 +24,7 @@ type ResInterfaceSummary struct { } `json:"list"` } -//ResInterfaceSummaryHour 接口分析分时数据响应 +// ResInterfaceSummaryHour 接口分析分时数据响应 type ResInterfaceSummaryHour struct { util.CommonError @@ -38,7 +38,7 @@ type ResInterfaceSummaryHour struct { } `json:"list"` } -//GetInterfaceSummary 获取接口分析数据 +// GetInterfaceSummary 获取接口分析数据 func (cube *DataCube) GetInterfaceSummary(s string, e string) (resInterfaceSummary ResInterfaceSummary, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -60,7 +60,7 @@ func (cube *DataCube) GetInterfaceSummary(s string, e string) (resInterfaceSumma return } -//GetInterfaceSummaryHour 获取接口分析分时数据 +// GetInterfaceSummaryHour 获取接口分析分时数据 func (cube *DataCube) GetInterfaceSummaryHour(s string, e string) (resInterfaceSummaryHour ResInterfaceSummaryHour, err error) { accessToken, err := cube.GetAccessToken() if err != nil { diff --git a/officialaccount/datacube/message.go b/officialaccount/datacube/message.go index 4ec3fd7..28f162b 100644 --- a/officialaccount/datacube/message.go +++ b/officialaccount/datacube/message.go @@ -16,7 +16,7 @@ const ( getUpstreamMsgDistMonth = "https://api.weixin.qq.com/datacube/getupstreammsgdistmonth" ) -//ResUpstreamMsg 获取消息发送概况数据响应 +// ResUpstreamMsg 获取消息发送概况数据响应 type ResUpstreamMsg struct { util.CommonError @@ -28,7 +28,7 @@ type ResUpstreamMsg struct { } `json:"list"` } -//ResUpstreamMsgHour 获取消息分送分时数据响应 +// ResUpstreamMsgHour 获取消息分送分时数据响应 type ResUpstreamMsgHour struct { util.CommonError @@ -41,7 +41,7 @@ type ResUpstreamMsgHour struct { } `json:"list"` } -//ResUpstreamMsgWeek 获取消息发送周数据响应 +// ResUpstreamMsgWeek 获取消息发送周数据响应 type ResUpstreamMsgWeek struct { util.CommonError @@ -53,7 +53,7 @@ type ResUpstreamMsgWeek struct { } `json:"list"` } -//ResUpstreamMsgMonth 获取消息发送月数据响应 +// ResUpstreamMsgMonth 获取消息发送月数据响应 type ResUpstreamMsgMonth struct { util.CommonError @@ -65,7 +65,7 @@ type ResUpstreamMsgMonth struct { } `json:"list"` } -//ResUpstreamMsgDist 获取消息发送分布数据响应 +// ResUpstreamMsgDist 获取消息发送分布数据响应 type ResUpstreamMsgDist struct { util.CommonError @@ -76,7 +76,7 @@ type ResUpstreamMsgDist struct { } `json:"list"` } -//ResUpstreamMsgDistWeek 获取消息发送分布周数据响应 +// ResUpstreamMsgDistWeek 获取消息发送分布周数据响应 type ResUpstreamMsgDistWeek struct { util.CommonError @@ -87,7 +87,7 @@ type ResUpstreamMsgDistWeek struct { } `json:"list"` } -//ResUpstreamMsgDistMonth 获取消息发送分布月数据响应 +// ResUpstreamMsgDistMonth 获取消息发送分布月数据响应 type ResUpstreamMsgDistMonth struct { util.CommonError @@ -98,7 +98,7 @@ type ResUpstreamMsgDistMonth struct { } `json:"list"` } -//GetUpstreamMsg 获取消息发送概况数据 +// GetUpstreamMsg 获取消息发送概况数据 func (cube *DataCube) GetUpstreamMsg(s string, e string) (resUpstreamMsg ResUpstreamMsg, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -120,7 +120,7 @@ func (cube *DataCube) GetUpstreamMsg(s string, e string) (resUpstreamMsg ResUpst return } -//GetUpstreamMsgHour 获取消息分送分时数据 +// GetUpstreamMsgHour 获取消息分送分时数据 func (cube *DataCube) GetUpstreamMsgHour(s string, e string) (resUpstreamMsgHour ResUpstreamMsgHour, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -142,7 +142,7 @@ func (cube *DataCube) GetUpstreamMsgHour(s string, e string) (resUpstreamMsgHour return } -//GetUpstreamMsgWeek 获取消息发送周数据 +// GetUpstreamMsgWeek 获取消息发送周数据 func (cube *DataCube) GetUpstreamMsgWeek(s string, e string) (resUpstreamMsgWeek ResUpstreamMsgWeek, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -164,7 +164,7 @@ func (cube *DataCube) GetUpstreamMsgWeek(s string, e string) (resUpstreamMsgWeek return } -//GetUpstreamMsgMonth 获取消息发送月数据 +// GetUpstreamMsgMonth 获取消息发送月数据 func (cube *DataCube) GetUpstreamMsgMonth(s string, e string) (resUpstreamMsgMonth ResUpstreamMsgMonth, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -186,7 +186,7 @@ func (cube *DataCube) GetUpstreamMsgMonth(s string, e string) (resUpstreamMsgMon return } -//GetUpstreamMsgDist 获取消息发送分布数据 +// GetUpstreamMsgDist 获取消息发送分布数据 func (cube *DataCube) GetUpstreamMsgDist(s string, e string) (resUpstreamMsgDist ResUpstreamMsgDist, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -208,7 +208,7 @@ func (cube *DataCube) GetUpstreamMsgDist(s string, e string) (resUpstreamMsgDist return } -//GetUpstreamMsgDistWeek 获取消息发送分布周数据 +// GetUpstreamMsgDistWeek 获取消息发送分布周数据 func (cube *DataCube) GetUpstreamMsgDistWeek(s string, e string) (resUpstreamMsgDistWeek ResUpstreamMsgDistWeek, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -230,7 +230,7 @@ func (cube *DataCube) GetUpstreamMsgDistWeek(s string, e string) (resUpstreamMsg return } -//GetUpstreamMsgDistMonth 获取消息发送分布月数据 +// GetUpstreamMsgDistMonth 获取消息发送分布月数据 func (cube *DataCube) GetUpstreamMsgDistMonth(s string, e string) (resUpstreamMsgDistMonth ResUpstreamMsgDistMonth, err error) { accessToken, err := cube.GetAccessToken() if err != nil { diff --git a/officialaccount/datacube/publisher.go b/officialaccount/datacube/publisher.go index 267a550..bd640d0 100644 --- a/officialaccount/datacube/publisher.go +++ b/officialaccount/datacube/publisher.go @@ -8,31 +8,31 @@ import ( "github.com/silenceper/wechat/v2/util" ) -//AdSlot 广告位类型 +// AdSlot 广告位类型 type AdSlot string const ( - //SlotIDBizBottom 公众号底部广告 + // SlotIDBizBottom 公众号底部广告 SlotIDBizBottom AdSlot = "SLOT_ID_BIZ_BOTTOM" - //SlotIDBizMidContext 公众号文中广告 + // SlotIDBizMidContext 公众号文中广告 SlotIDBizMidContext AdSlot = "SLOT_ID_BIZ_MID_CONTEXT" - //SlotIDBizVideoEnd 公众号视频后贴 + // SlotIDBizVideoEnd 公众号视频后贴 SlotIDBizVideoEnd AdSlot = "SLOT_ID_BIZ_VIDEO_END" - //SlotIDBizSponsor 公众号互选广告 + // SlotIDBizSponsor 公众号互选广告 SlotIDBizSponsor AdSlot = "SLOT_ID_BIZ_SPONSOR" - //SlotIDBizCps 公众号返佣商品 + // SlotIDBizCps 公众号返佣商品 SlotIDBizCps AdSlot = "SLOT_ID_BIZ_CPS" - //SlotIDWeappBanner 小程序banner + // SlotIDWeappBanner 小程序banner SlotIDWeappBanner AdSlot = "SLOT_ID_WEAPP_BANNER" - //SlotIDWeappRewardVideo 小程序激励视频 + // SlotIDWeappRewardVideo 小程序激励视频 SlotIDWeappRewardVideo AdSlot = "SLOT_ID_WEAPP_REWARD_VIDEO" - //SlotIDWeappInterstitial 小程序插屏广告 + // SlotIDWeappInterstitial 小程序插屏广告 SlotIDWeappInterstitial AdSlot = "SLOT_ID_WEAPP_INTERSTITIAL" - //SlotIDWeappVideoFeeds 小程序视频广告 + // SlotIDWeappVideoFeeds 小程序视频广告 SlotIDWeappVideoFeeds AdSlot = "SLOT_ID_WEAPP_VIDEO_FEEDS" - //SlotIDWeappVideoBegin 小程序视频前贴 + // SlotIDWeappVideoBegin 小程序视频前贴 SlotIDWeappVideoBegin AdSlot = "SLOT_ID_WEAPP_VIDEO_BEGIN" - //SlotIDWeappBox 小程序格子广告 + // SlotIDWeappBox 小程序格子广告 SlotIDWeappBox AdSlot = "SLOT_ID_WEAPP_BOX" ) @@ -46,13 +46,13 @@ const ( actionPublisherSettlement = "publisher_settlement" ) -//BaseResp 错误信息 +// BaseResp 错误信息 type BaseResp struct { ErrMsg string `json:"err_msg"` Ret int `json:"ret"` } -//ResPublisherAdPos 公众号分广告位数据响应 +// ResPublisherAdPos 公众号分广告位数据响应 type ResPublisherAdPos struct { util.CommonError @@ -62,7 +62,7 @@ type ResPublisherAdPos struct { TotalNum int `json:"total_num"` } -//ResAdPosList 公众号分广告位列表 +// ResAdPosList 公众号分广告位列表 type ResAdPosList struct { SlotID int64 `json:"slot_id"` AdSlot string `json:"ad_slot"` @@ -76,7 +76,7 @@ type ResAdPosList struct { Ecpm float64 `json:"ecpm"` } -//ResAdPosSummary 公众号分广告位概览 +// ResAdPosSummary 公众号分广告位概览 type ResAdPosSummary struct { ReqSuccCount int `json:"req_succ_count"` ExposureCount int `json:"exposure_count"` @@ -87,7 +87,7 @@ type ResAdPosSummary struct { Ecpm float64 `json:"ecpm"` } -//ResPublisherCps 公众号返佣商品数据响应 +// ResPublisherCps 公众号返佣商品数据响应 type ResPublisherCps struct { util.CommonError @@ -97,7 +97,7 @@ type ResPublisherCps struct { TotalNum int `json:"total_num"` } -//ResCpsList 公众号返佣商品列表 +// ResCpsList 公众号返佣商品列表 type ResCpsList struct { Date string `json:"date"` ExposureCount int `json:"exposure_count"` @@ -109,7 +109,7 @@ type ResCpsList struct { TotalCommission int `json:"total_commission"` } -//ResCpsSummary 公众号返佣概览 +// ResCpsSummary 公众号返佣概览 type ResCpsSummary struct { ExposureCount int `json:"exposure_count"` ClickCount int `json:"click_count"` @@ -120,7 +120,7 @@ type ResCpsSummary struct { TotalCommission int `json:"total_commission"` } -//ResPublisherSettlement 公众号结算收入数据及结算主体信息响应 +// ResPublisherSettlement 公众号结算收入数据及结算主体信息响应 type ResPublisherSettlement struct { util.CommonError @@ -133,7 +133,7 @@ type ResPublisherSettlement struct { TotalNum int `json:"total_num"` } -//SettlementList 结算单列表 +// SettlementList 结算单列表 type SettlementList struct { Date string `json:"date"` Zone string `json:"zone"` @@ -146,13 +146,13 @@ type SettlementList struct { SlotRevenue []SlotRevenue `json:"slot_revenue"` } -//SlotRevenue 产生收入的广告 +// SlotRevenue 产生收入的广告 type SlotRevenue struct { SlotID string `json:"slot_id"` SlotSettledRevenue int `json:"slot_settled_revenue"` } -//ParamsPublisher 拉取数据参数 +// ParamsPublisher 拉取数据参数 type ParamsPublisher struct { Action string `json:"action"` StartDate string `json:"start_date"` @@ -189,7 +189,7 @@ func (cube *DataCube) fetchData(params ParamsPublisher) (response []byte, err er return } -//GetPublisherAdPosGeneral 获取公众号分广告位数据 +// GetPublisherAdPosGeneral 获取公众号分广告位数据 func (cube *DataCube) GetPublisherAdPosGeneral(startDate, endDate string, page, pageSize int, adSlot AdSlot) (resPublisherAdPos ResPublisherAdPos, err error) { params := ParamsPublisher{ Action: actionPublisherAdPosGeneral, @@ -217,7 +217,7 @@ func (cube *DataCube) GetPublisherAdPosGeneral(startDate, endDate string, page, return } -//GetPublisherCpsGeneral 获取公众号返佣商品数据 +// GetPublisherCpsGeneral 获取公众号返佣商品数据 func (cube *DataCube) GetPublisherCpsGeneral(startDate, endDate string, page, pageSize int) (resPublisherCps ResPublisherCps, err error) { params := ParamsPublisher{ Action: actionPublisherCpsGeneral, @@ -244,7 +244,7 @@ func (cube *DataCube) GetPublisherCpsGeneral(startDate, endDate string, page, pa return } -//GetPublisherSettlement 获取公众号结算收入数据及结算主体信息 +// GetPublisherSettlement 获取公众号结算收入数据及结算主体信息 func (cube *DataCube) GetPublisherSettlement(startDate, endDate string, page, pageSize int) (resPublisherSettlement ResPublisherSettlement, err error) { params := ParamsPublisher{ Action: actionPublisherSettlement, diff --git a/officialaccount/datacube/user.go b/officialaccount/datacube/user.go index e4242d0..a24fbc4 100644 --- a/officialaccount/datacube/user.go +++ b/officialaccount/datacube/user.go @@ -11,7 +11,7 @@ const ( getUserAccumulate = "https://api.weixin.qq.com/datacube/getusercumulate" ) -//ResUserSummary 获取用户增减数据响应 +// ResUserSummary 获取用户增减数据响应 type ResUserSummary struct { util.CommonError @@ -23,7 +23,7 @@ type ResUserSummary struct { } `json:"list"` } -//ResUserAccumulate 获取累计用户数据响应 +// ResUserAccumulate 获取累计用户数据响应 type ResUserAccumulate struct { util.CommonError @@ -33,7 +33,7 @@ type ResUserAccumulate struct { } `json:"list"` } -//GetUserSummary 获取用户增减数据 +// GetUserSummary 获取用户增减数据 func (cube *DataCube) GetUserSummary(s string, e string) (resUserSummary ResUserSummary, err error) { accessToken, err := cube.GetAccessToken() if err != nil { @@ -55,7 +55,7 @@ func (cube *DataCube) GetUserSummary(s string, e string) (resUserSummary ResUser return } -//GetUserAccumulate 获取累计用户数据 +// GetUserAccumulate 获取累计用户数据 func (cube *DataCube) GetUserAccumulate(s string, e string) (resUserAccumulate ResUserAccumulate, err error) { accessToken, err := cube.GetAccessToken() if err != nil { diff --git a/officialaccount/device/authorize.go b/officialaccount/device/authorize.go index 1368356..4f1b6bf 100644 --- a/officialaccount/device/authorize.go +++ b/officialaccount/device/authorize.go @@ -1,4 +1,4 @@ -//Package device 设备相关接口 +// Package device 设备相关接口 package device import ( @@ -23,13 +23,13 @@ type reqDeviceAuthorize struct { // 请求操作的类型,限定取值为:0:设备授权(缺省值为0) 1:设备更新(更新已授权设备的各属性值) OpType string `json:"op_type,omitempty"` // 设备的产品编号(由微信硬件平台分配)。可在公众号设备功能管理页面查询。 - //当 op_type 为‘0’,product_id 为‘1’时,不要填写 product_id 字段(会引起不必要错误); - //当 op_typy 为‘0’,product_id 不为‘1’时,必须填写 product_id 字段; - //当 op_type 为 1 时,不要填写 product_id 字段。 + // 当 op_type 为‘0’,product_id 为‘1’时,不要填写 product_id 字段(会引起不必要错误); + // 当 op_typy 为‘0’,product_id 不为‘1’时,必须填写 product_id 字段; + // 当 op_type 为 1 时,不要填写 product_id 字段。 ProductID string `json:"product_id,omitempty"` } -//ReqDevice 设备授权实体 +// ReqDevice 设备授权实体 type ReqDevice struct { // 设备的 device id ID string `json:"id"` @@ -45,13 +45,13 @@ type ReqDevice struct { // 1:表示设备仅支持andiod classic bluetooth 1|2:表示设备支持android 和ios 两种classic bluetooth,但是客户端优先选择android classic bluetooth 协议,如果android classic bluetooth协议连接失败,再选择ios classic bluetooth协议进行连接 // (注:安卓平台不同时支持BLE和classic类型) ConnectProtocol string `json:"connect_protocol"` - //auth及通信的加密key,第三方需要将key烧制在设备上(128bit),格式采用16进制串的方式(长度为32字节),不需要0X前缀,如: 1234567890ABCDEF1234567890ABCDEF + // auth及通信的加密key,第三方需要将key烧制在设备上(128bit),格式采用16进制串的方式(长度为32字节),不需要0X前缀,如: 1234567890ABCDEF1234567890ABCDEF AuthKey string `json:"auth_key"` // 断开策略,目前支持: 1:退出公众号页面时即断开连接 2:退出公众号之后保持连接不断开 CloseStrategy string `json:"close_strategy"` - //连接策略,32位整型,按bit位置位,目前仅第1bit和第3bit位有效(bit置0为无效,1为有效;第2bit已被废弃),且bit位可以按或置位(如1|4=5),各bit置位含义说明如下: - //1:(第1bit置位)在公众号对话页面,不停的尝试连接设备 - //4:(第3bit置位)处于非公众号页面(如主界面等),微信自动连接。当用户切换微信到前台时,可能尝试去连接设备,连上后一定时间会断开 + // 连接策略,32位整型,按bit位置位,目前仅第1bit和第3bit位有效(bit置0为无效,1为有效;第2bit已被废弃),且bit位可以按或置位(如1|4=5),各bit置位含义说明如下: + // 1:(第1bit置位)在公众号对话页面,不停的尝试连接设备 + // 4:(第3bit置位)处于非公众号页面(如主界面等),微信自动连接。当用户切换微信到前台时,可能尝试去连接设备,连上后一定时间会断开 ConnStrategy string `json:"conn_strategy"` // auth version,设备和微信进行auth时,会根据该版本号来确认auth buf和auth key的格式(各version对应的auth buf及key的具体格式可以参看“客户端蓝牙外设协议”),该字段目前支持取值: // 0:不加密的version @@ -69,7 +69,7 @@ type ReqDevice struct { BleSimpleProtocol string `json:"ble_simple_protocol,omitempty"` } -//ResBaseInfo 授权回调实体 +// ResBaseInfo 授权回调实体 type ResBaseInfo struct { BaseInfo struct { DeviceType string `json:"device_type"` diff --git a/officialaccount/device/device.go b/officialaccount/device/device.go index 327add4..75ef235 100644 --- a/officialaccount/device/device.go +++ b/officialaccount/device/device.go @@ -19,12 +19,12 @@ const ( uriState = "https://api.weixin.qq.com/device/get_stat" ) -//Device struct +// Device struct type Device struct { *context.Context } -//NewDevice 实例 +// NewDevice 实例 func NewDevice(context *context.Context) *Device { device := new(Device) device.Context = context diff --git a/officialaccount/device/message.go b/officialaccount/device/message.go index 7efcbbb..4e79097 100644 --- a/officialaccount/device/message.go +++ b/officialaccount/device/message.go @@ -1,6 +1,6 @@ package device -//MsgDevice 设备消息响应 +// MsgDevice 设备消息响应 type MsgDevice struct { DeviceType string DeviceID string diff --git a/officialaccount/device/qrcode.go b/officialaccount/device/qrcode.go index c77d659..89c21f3 100644 --- a/officialaccount/device/qrcode.go +++ b/officialaccount/device/qrcode.go @@ -7,7 +7,7 @@ import ( "github.com/silenceper/wechat/v2/util" ) -//ResCreateQRCode 获取二维码的返回实体 +// ResCreateQRCode 获取二维码的返回实体 type ResCreateQRCode struct { util.CommonError DeviceNum int `json:"device_num"` @@ -42,7 +42,7 @@ func (d *Device) CreateQRCode(devices []string) (res ResCreateQRCode, err error) return } -//ResVerifyQRCode 验证授权结果实体 +// ResVerifyQRCode 验证授权结果实体 type ResVerifyQRCode struct { util.CommonError DeviceType string `json:"device_type"` diff --git a/officialaccount/draft/draft.go b/officialaccount/draft/draft.go new file mode 100644 index 0000000..e7f15d3 --- /dev/null +++ b/officialaccount/draft/draft.go @@ -0,0 +1,228 @@ +package draft + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + addURL = "https://api.weixin.qq.com/cgi-bin/draft/add" // 新建草稿 + getURL = "https://api.weixin.qq.com/cgi-bin/draft/get" // 获取草稿 + deleteURL = "https://api.weixin.qq.com/cgi-bin/draft/delete" // 删除草稿 + updateURL = "https://api.weixin.qq.com/cgi-bin/draft/update" // 修改草稿 + countURL = "https://api.weixin.qq.com/cgi-bin/draft/count" // 获取草稿总数 + paginateURL = "https://api.weixin.qq.com/cgi-bin/draft/batchget" // 获取草稿列表 +) + +// Draft 草稿箱 +type Draft struct { + *context.Context +} + +// NewDraft init +func NewDraft(ctx *context.Context) *Draft { + return &Draft{ + Context: ctx, + } +} + +// Article 草稿 +type Article struct { + Title string `json:"title"` // 标题 + Author string `json:"author"` // 作者 + Digest string `json:"digest"` // 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空。 + Content string `json:"content"` // 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且去除JS + ContentSourceURL string `json:"content_source_url"` // 图文消息的原文地址,即点击“阅读原文”后的URL + ThumbMediaID string `json:"thumb_media_id"` // 图文消息的封面图片素材id(必须是永久MediaID) + ShowCoverPic uint `json:"show_cover_pic"` // 是否显示封面,0为false,即不显示,1为true,即显示(默认) + NeedOpenComment uint `json:"need_open_comment"` // 是否打开评论,0不打开(默认),1打开 + OnlyFansCanComment uint `json:"only_fans_can_comment"` // 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论 +} + +// AddDraft 新建草稿 +func (draft *Draft) AddDraft(articles []*Article) (mediaID string, err error) { + accessToken, err := draft.GetAccessToken() + if err != nil { + return + } + + var req struct { + Articles []*Article `json:"articles"` + } + req.Articles = articles + + uri := fmt.Sprintf("%s?access_token=%s", addURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + var res struct { + util.CommonError + MediaID string `json:"media_id"` + } + err = util.DecodeWithError(response, &res, "AddDraft") + if err != nil { + return + } + mediaID = res.MediaID + return +} + +// GetDraft 获取草稿 +func (draft *Draft) GetDraft(mediaID string) (articles []*Article, err error) { + accessToken, err := draft.GetAccessToken() + if err != nil { + return + } + + var req struct { + MediaID string `json:"media_id"` + } + req.MediaID = mediaID + + uri := fmt.Sprintf("%s?access_token=%s", getURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + var res struct { + util.CommonError + NewsItem []*Article `json:"news_item"` + } + err = util.DecodeWithError(response, &res, "GetDraft") + if err != nil { + return + } + + articles = res.NewsItem + return +} + +// DeleteDraft 删除草稿 +func (draft *Draft) DeleteDraft(mediaID string) (err error) { + accessToken, err := draft.GetAccessToken() + if err != nil { + return + } + + var req struct { + MediaID string `json:"media_id"` + } + req.MediaID = mediaID + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", deleteURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + err = util.DecodeWithCommonError(response, "DeleteDraft") + return +} + +// UpdateDraft 修改草稿 +// index 要更新的文章在图文消息中的位置(多图文消息时,此字段才有意义),第一篇为0 +func (draft *Draft) UpdateDraft(article *Article, mediaID string, index uint) (err error) { + accessToken, err := draft.GetAccessToken() + if err != nil { + return + } + + var req struct { + MediaID string `json:"media_id"` + Index uint `json:"index"` + Article *Article `json:"articles"` + } + req.MediaID = mediaID + req.Index = index + req.Article = article + + uri := fmt.Sprintf("%s?access_token=%s", updateURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + err = util.DecodeWithCommonError(response, "UpdateDraft") + return +} + +// CountDraft 获取草稿总数 +func (draft *Draft) CountDraft() (total uint, err error) { + accessToken, err := draft.GetAccessToken() + if err != nil { + return + } + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", countURL, accessToken) + response, err = util.HTTPGet(uri) + if err != nil { + return + } + + var res struct { + util.CommonError + Total uint `json:"total_count"` + } + err = util.DecodeWithError(response, &res, "CountDraft") + if nil != err { + return + } + + total = res.Total + return +} + +// ArticleList 草稿列表 +type ArticleList struct { + util.CommonError + TotalCount int64 `json:"total_count"` // 草稿素材的总数 + ItemCount int64 `json:"item_count"` // 本次调用获取的素材的数量 + Item []ArticleListItem `json:"item"` +} + +// ArticleListItem 用于 ArticleList 的 item 节点 +type ArticleListItem struct { + MediaID string `json:"media_id"` // 图文消息的id + Content ArticleListContent `json:"content"` // 内容 + UpdateTime int64 `json:"update_time"` // 这篇图文消息素材的最后更新时间 +} + +// ArticleListContent 用于 ArticleListItem 的 content 节点 +type ArticleListContent struct { + NewsItem []Article `json:"news_item"` // 这篇图文消息素材的内容 +} + +// PaginateDraft 获取草稿列表 +func (draft *Draft) PaginateDraft(offset, count int64, noReturnContent bool) (list ArticleList, err error) { + accessToken, err := draft.GetAccessToken() + if err != nil { + return + } + + var req struct { + Count int64 `json:"count"` + Offset int64 `json:"offset"` + NoReturnContent bool `json:"no_content"` + } + req.Count = count + req.Offset = offset + req.NoReturnContent = noReturnContent + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", paginateURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + err = util.DecodeWithError(response, &list, "PaginateDraft") + return +} diff --git a/officialaccount/freepublish/freepublish.go b/officialaccount/freepublish/freepublish.go new file mode 100644 index 0000000..041bf66 --- /dev/null +++ b/officialaccount/freepublish/freepublish.go @@ -0,0 +1,248 @@ +package freepublish + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + publishURL = "https://api.weixin.qq.com/cgi-bin/freepublish/submit" // 发布接口 + selectStateURL = "https://api.weixin.qq.com/cgi-bin/freepublish/get" // 发布状态轮询接口 + deleteURL = "https://api.weixin.qq.com/cgi-bin/freepublish/delete" // 删除发布 + firstArticleURL = "https://api.weixin.qq.com/cgi-bin/freepublish/getarticle" // 通过 article_id 获取已发布文章 + paginateURL = "https://api.weixin.qq.com/cgi-bin/freepublish/batchget" // 获取成功发布列表 +) + +// PublishStatus 发布状态 +type PublishStatus uint + +const ( + // PublishStatusSuccess 0:成功 + PublishStatusSuccess PublishStatus = iota + // PublishStatusPublishing 1:发布中 + PublishStatusPublishing + // PublishStatusOriginalFail 2:原创失败 + PublishStatusOriginalFail + // PublishStatusFail 3:常规失败 + PublishStatusFail + // PublishStatusAuditRefused 4:平台审核不通过 + PublishStatusAuditRefused + // PublishStatusUserDeleted 5:成功后用户删除所有文章 + PublishStatusUserDeleted + // PublishStatusSystemBanned 6:成功后系统封禁所有文章 + PublishStatusSystemBanned +) + +// FreePublish 发布能力 +type FreePublish struct { + *context.Context +} + +// NewFreePublish init +func NewFreePublish(ctx *context.Context) *FreePublish { + return &FreePublish{ + Context: ctx, + } +} + +// Publish 发布接口。需要先将图文素材以草稿的形式保存(见“草稿箱/新建草稿”, +// 如需从已保存的草稿中选择,见“草稿箱/获取草稿列表”),选择要发布的草稿 media_id 进行发布 +func (freePublish *FreePublish) Publish(mediaID string) (publishID int64, err error) { + var accessToken string + accessToken, err = freePublish.GetAccessToken() + if err != nil { + return + } + + var req struct { + MediaID string `json:"media_id"` + } + req.MediaID = mediaID + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", publishURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + var res struct { + util.CommonError + PublishID int64 `json:"publish_id"` + } + err = util.DecodeWithError(response, &res, "SubmitFreePublish") + if err != nil { + return + } + + publishID = res.PublishID + return +} + +// PublishStatusList 发布任务状态列表 +type PublishStatusList struct { + util.CommonError + PublishID int64 `json:"publish_id"` // 发布任务id + PublishStatus PublishStatus `json:"publish_status"` // 发布状态 + ArticleID string `json:"article_id"` // 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景 + ArticleDetail PublishArticleDetail `json:"article_detail"` // 发布任务文章成功状态详情 + FailIndex []uint `json:"fail_idx"` // 当发布状态为2或4时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空 +} + +// PublishArticleDetail 发布任务成功详情 +type PublishArticleDetail struct { + Count uint `json:"count"` // 当发布状态为0时(即成功)时,返回文章数量 + Items []PublishArticleItem `json:"item"` +} + +// PublishArticleItem 发布任务成功的文章内容 +type PublishArticleItem struct { + Index uint `json:"idx"` // 当发布状态为0时(即成功)时,返回文章对应的编号 + ArticleURL string `json:"article_url"` // 当发布状态为0时(即成功)时,返回图文的永久链接 +} + +// SelectStatus 发布状态轮询接口 +func (freePublish *FreePublish) SelectStatus(publishID int64) (list PublishStatusList, err error) { + accessToken, err := freePublish.GetAccessToken() + if err != nil { + return + } + + var req struct { + PublishID int64 `json:"publish_id"` + } + req.PublishID = publishID + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", selectStateURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + err = util.DecodeWithError(response, &list, "SelectStatusFreePublish") + return +} + +// Delete 删除发布。 +// index 要删除的文章在图文消息中的位置,第一篇编号为1,该字段不填或填0会删除全部文章 +// !!!此操作不可逆,请谨慎操作!!!删除后微信公众号后台仍然会有记录!!! +func (freePublish *FreePublish) Delete(articleID string, index uint) (err error) { + accessToken, err := freePublish.GetAccessToken() + if err != nil { + return err + } + + var req struct { + ArticleID string `json:"article_id"` + Index uint `json:"index"` + } + req.ArticleID = articleID + req.Index = index + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", deleteURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return err + } + + return util.DecodeWithCommonError(response, "DeleteFreePublish") +} + +// Article 图文信息内容 +type Article struct { + Title string `json:"title"` // 标题 + Author string `json:"author"` // 作者 + Digest string `json:"digest"` // 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空 + Content string `json:"content"` // 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS + ContentSourceURL string `json:"content_source_url"` // 图文消息的原文地址,即点击“阅读原文”后的URL + ThumbMediaID string `json:"thumb_media_id"` // 图文消息的封面图片素材id(一定是永久MediaID) + ShowCoverPic uint `json:"show_cover_pic"` // 是否显示封面,0为false,即不显示,1为true,即显示(默认) + NeedOpenComment uint `json:"need_open_comment"` // 是否打开评论,0不打开(默认),1打开 + OnlyFansCanComment uint `json:"only_fans_can_comment"` // 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论 + URL string `json:"url"` // 图文消息的URL + IsDeleted bool `json:"is_deleted"` // 该图文是否被删除 +} + +// First 通过 article_id 获取已发布文章 +func (freePublish *FreePublish) First(articleID string) (list []Article, err error) { + accessToken, err := freePublish.GetAccessToken() + if err != nil { + return + } + + var req struct { + ArticleID string `json:"article_id"` + } + req.ArticleID = articleID + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", firstArticleURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + var res struct { + util.CommonError + NewsItem []Article `json:"news_item"` + } + err = util.DecodeWithError(response, &res, "FirstFreePublish") + if err != nil { + return + } + + list = res.NewsItem + return +} + +// ArticleList 发布列表 +type ArticleList struct { + util.CommonError + TotalCount int64 `json:"total_count"` // 成功发布素材的总数 + ItemCount int64 `json:"item_count"` // 本次调用获取的素材的数量 + Item []ArticleListItem `json:"item"` +} + +// ArticleListItem 用于 ArticleList 的 item 节点 +type ArticleListItem struct { + ArticleID string `json:"article_id"` // 成功发布的图文消息id + Content ArticleListContent `json:"content"` // 内容 + UpdateTime int64 `json:"update_time"` // 这篇图文消息素材的最后更新时间 +} + +// ArticleListContent 用于 ArticleListItem 的 content 节点 +type ArticleListContent struct { + NewsItem []Article `json:"news_item"` // 这篇图文消息素材的内容 +} + +// Paginate 获取成功发布列表 +func (freePublish *FreePublish) Paginate(offset, count int64, noReturnContent bool) (list ArticleList, err error) { + var accessToken string + accessToken, err = freePublish.GetAccessToken() + if err != nil { + return + } + + var req struct { + Count int64 `json:"count"` + Offset int64 `json:"offset"` + NoReturnContent bool `json:"no_content"` + } + req.Count = count + req.Offset = offset + req.NoReturnContent = noReturnContent + + var response []byte + uri := fmt.Sprintf("%s?access_token=%s", paginateURL, accessToken) + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + + err = util.DecodeWithError(response, &list, "PaginateFreePublish") + return +} diff --git a/officialaccount/js/js.go b/officialaccount/js/js.go index a2a0683..e1c749f 100644 --- a/officialaccount/js/js.go +++ b/officialaccount/js/js.go @@ -22,7 +22,7 @@ type Config struct { Signature string `json:"signature"` } -//NewJs init +// NewJs init func NewJs(context *context.Context) *Js { js := new(Js) js.Context = context @@ -31,13 +31,13 @@ func NewJs(context *context.Context) *Js { return js } -//SetJsTicketHandle 自定义js ticket取值方式 +// SetJsTicketHandle 自定义js ticket取值方式 func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) { js.JsTicketHandle = ticketHandle } -//GetConfig 获取jssdk需要的配置参数 -//uri 为当前网页地址 +// GetConfig 获取jssdk需要的配置参数 +// uri 为当前网页地址 func (js *Js) GetConfig(uri string) (config *Config, err error) { config = new(Config) var accessToken string diff --git a/officialaccount/material/material.go b/officialaccount/material/material.go index eaaad7e..5b5f1c6 100644 --- a/officialaccount/material/material.go +++ b/officialaccount/material/material.go @@ -19,33 +19,33 @@ const ( batchGetMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/batchget_material" ) -//PermanentMaterialType 永久素材类型 +// PermanentMaterialType 永久素材类型 type PermanentMaterialType string const ( - //PermanentMaterialTypeImage 永久素材图片类型(image) + // PermanentMaterialTypeImage 永久素材图片类型(image) PermanentMaterialTypeImage PermanentMaterialType = "image" - //PermanentMaterialTypeVideo 永久素材视频类型(video) + // PermanentMaterialTypeVideo 永久素材视频类型(video) PermanentMaterialTypeVideo PermanentMaterialType = "video" - //PermanentMaterialTypeVoice 永久素材语音类型 (voice) + // PermanentMaterialTypeVoice 永久素材语音类型(voice) PermanentMaterialTypeVoice PermanentMaterialType = "voice" - //PermanentMaterialTypeNews 永久素材图文类型(news) + // PermanentMaterialTypeNews 永久素材图文类型(news) PermanentMaterialTypeNews PermanentMaterialType = "news" ) -//Material 素材管理 +// Material 素材管理 type Material struct { *context.Context } -//NewMaterial init +// NewMaterial init func NewMaterial(context *context.Context) *Material { material := new(Material) material.Context = context return material } -//Article 永久图文素材 +// Article 永久图文素材 type Article struct { Title string `json:"title"` ThumbMediaID string `json:"thumb_media_id"` @@ -87,19 +87,19 @@ func (material *Material) GetNews(id string) ([]*Article, error) { return res.NewsItem, nil } -//reqArticles 永久性图文素材请求信息 +// reqArticles 永久性图文素材请求信息 type reqArticles struct { Articles []*Article `json:"articles"` } -//resArticles 永久性图文素材返回结果 +// resArticles 永久性图文素材返回结果 type resArticles struct { util.CommonError MediaID string `json:"media_id"` } -//AddNews 新增永久图文素材 +// AddNews 新增永久图文素材 func (material *Material) AddNews(articles []*Article) (mediaID string, err error) { req := &reqArticles{articles} @@ -126,7 +126,7 @@ func (material *Material) AddNews(articles []*Article) (mediaID string, err erro return } -//reqUpdateArticle 更新永久性图文素材请求信息 +// reqUpdateArticle 更新永久性图文素材请求信息 type reqUpdateArticle struct { MediaID string `json:"media_id"` Index int64 `json:"index"` @@ -152,7 +152,7 @@ func (material *Material) UpdateNews(article *Article, mediaID string, index int return util.DecodeWithCommonError(response, "UpdateNews") } -//resAddMaterial 永久性素材上传返回的结果 +// resAddMaterial 永久性素材上传返回的结果 type resAddMaterial struct { util.CommonError @@ -160,7 +160,7 @@ type resAddMaterial struct { URL string `json:"url"` } -//AddMaterial 上传永久性素材(处理视频需要单独上传) +// AddMaterial 上传永久性素材(处理视频需要单独上传) func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) { if mediaType == MediaTypeVideo { err = errors.New("永久视频素材上传使用 AddVideo 方法") @@ -197,7 +197,7 @@ type reqVideo struct { Introduction string `json:"introduction"` } -//AddVideo 永久视频素材文件上传 +// AddVideo 永久视频素材文件上传 func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) { var accessToken string accessToken, err = material.GetAccessToken() @@ -254,7 +254,7 @@ type reqDeleteMaterial struct { MediaID string `json:"media_id"` } -//DeleteMaterial 删除永久素材 +// DeleteMaterial 删除永久素材 func (material *Material) DeleteMaterial(mediaID string) error { accessToken, err := material.GetAccessToken() if err != nil { @@ -270,7 +270,7 @@ func (material *Material) DeleteMaterial(mediaID string) error { return util.DecodeWithCommonError(response, "DeleteMaterial") } -//ArticleList 永久素材列表 +// ArticleList 永久素材列表 type ArticleList struct { util.CommonError TotalCount int64 `json:"total_count"` @@ -278,7 +278,7 @@ type ArticleList struct { Item []ArticleListItem `json:"item"` } -//ArticleListItem 用于ArticleList的item节点 +// ArticleListItem 用于 ArticleList 的 item 节点 type ArticleListItem struct { MediaID string `json:"media_id"` Content ArticleListContent `json:"content"` @@ -287,14 +287,14 @@ type ArticleListItem struct { UpdateTime int64 `json:"update_time"` } -//ArticleListContent 用于ArticleListItem的content节点 +// ArticleListContent 用于 ArticleListItem 的 content 节点 type ArticleListContent struct { NewsItem []Article `json:"news_item"` UpdateTime int64 `json:"update_time"` CreateTime int64 `json:"create_time"` } -//reqBatchGetMaterial BatchGetMaterial请求参数 +// reqBatchGetMaterial BatchGetMaterial 请求参数 type reqBatchGetMaterial struct { Type PermanentMaterialType `json:"type"` Count int64 `json:"count"` @@ -302,6 +302,7 @@ type reqBatchGetMaterial struct { } // BatchGetMaterial 批量获取永久素材 +// //reference:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html func (material *Material) BatchGetMaterial(permanentMaterialType PermanentMaterialType, offset, count int64) (list ArticleList, err error) { var accessToken string @@ -336,7 +337,7 @@ type ResMaterialCount struct { NewsCount int64 `json:"news_count"` // 图文总数量 } -// GetMaterialCount 获取素材总数. +// GetMaterialCount 获取素材总数。 func (material *Material) GetMaterialCount() (res ResMaterialCount, err error) { var accessToken string accessToken, err = material.GetAccessToken() diff --git a/officialaccount/material/media.go b/officialaccount/material/media.go index f5827cc..316758f 100644 --- a/officialaccount/material/media.go +++ b/officialaccount/material/media.go @@ -7,17 +7,17 @@ import ( "github.com/silenceper/wechat/v2/util" ) -//MediaType 媒体文件类型 +// MediaType 媒体文件类型 type MediaType string const ( - //MediaTypeImage 媒体文件:图片 + // MediaTypeImage 媒体文件:图片 MediaTypeImage MediaType = "image" - //MediaTypeVoice 媒体文件:声音 + // MediaTypeVoice 媒体文件:声音 MediaTypeVoice MediaType = "voice" - //MediaTypeVideo 媒体文件:视频 + // MediaTypeVideo 媒体文件:视频 MediaTypeVideo MediaType = "video" - //MediaTypeThumb 媒体文件:缩略图 + // MediaTypeThumb 媒体文件:缩略图 MediaTypeThumb MediaType = "thumb" ) @@ -27,7 +27,7 @@ const ( mediaGetURL = "https://api.weixin.qq.com/cgi-bin/media/get" ) -//Media 临时素材上传返回信息 +// Media 临时素材上传返回信息 type Media struct { util.CommonError @@ -37,7 +37,7 @@ type Media struct { CreatedAt int64 `json:"created_at"` } -//MediaUpload 临时素材上传 +// MediaUpload 临时素材上传 func (material *Material) MediaUpload(mediaType MediaType, filename string) (media Media, err error) { var accessToken string accessToken, err = material.GetAccessToken() @@ -62,8 +62,8 @@ func (material *Material) MediaUpload(mediaType MediaType, filename string) (med return } -//GetMediaURL 返回临时素材的下载地址供用户自己处理 -//NOTICE: URL 不可公开,因为含access_token 需要立即另存文件 +// GetMediaURL 返回临时素材的下载地址供用户自己处理 +// NOTICE: URL 不可公开,因为含access_token 需要立即另存文件 func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) { var accessToken string accessToken, err = material.GetAccessToken() @@ -74,14 +74,14 @@ func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err erro return } -//resMediaImage 图片上传返回结果 +// resMediaImage 图片上传返回结果 type resMediaImage struct { util.CommonError URL string `json:"url"` } -//ImageUpload 图片上传 +// ImageUpload 图片上传 func (material *Material) ImageUpload(filename string) (url string, err error) { var accessToken string accessToken, err = material.GetAccessToken() diff --git a/officialaccount/menu/button.go b/officialaccount/menu/button.go index 9956308..6b702db 100644 --- a/officialaccount/menu/button.go +++ b/officialaccount/menu/button.go @@ -1,6 +1,6 @@ package menu -//Button 菜单按钮 +// Button 菜单按钮 type Button struct { Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` @@ -12,7 +12,7 @@ type Button struct { SubButtons []*Button `json:"sub_button,omitempty"` } -//SetSubButton 设置二级菜单 +// SetSubButton 设置二级菜单 func (btn *Button) SetSubButton(name string, subButtons []*Button) *Button { btn.Name = name btn.SubButtons = subButtons @@ -23,7 +23,7 @@ func (btn *Button) SetSubButton(name string, subButtons []*Button) *Button { return btn } -//SetClickButton btn 为click类型 +// SetClickButton btn 为click类型 func (btn *Button) SetClickButton(name, key string) *Button { btn.Type = "click" btn.Name = name @@ -34,7 +34,7 @@ func (btn *Button) SetClickButton(name, key string) *Button { return btn } -//SetViewButton view类型 +// SetViewButton view类型 func (btn *Button) SetViewButton(name, url string) *Button { btn.Type = "view" btn.Name = name @@ -45,7 +45,7 @@ func (btn *Button) SetViewButton(name, url string) *Button { return btn } -//SetScanCodePushButton 扫码推事件 +// SetScanCodePushButton 扫码推事件 func (btn *Button) SetScanCodePushButton(name, key string) *Button { btn.Type = "scancode_push" btn.Name = name @@ -56,7 +56,7 @@ func (btn *Button) SetScanCodePushButton(name, key string) *Button { return btn } -//SetScanCodeWaitMsgButton 设置 扫码推事件且弹出"消息接收中"提示框 +// SetScanCodeWaitMsgButton 设置 扫码推事件且弹出"消息接收中"提示框 func (btn *Button) SetScanCodeWaitMsgButton(name, key string) *Button { btn.Type = "scancode_waitmsg" btn.Name = name @@ -68,7 +68,7 @@ func (btn *Button) SetScanCodeWaitMsgButton(name, key string) *Button { return btn } -//SetPicSysPhotoButton 设置弹出系统拍照发图按钮 +// SetPicSysPhotoButton 设置弹出系统拍照发图按钮 func (btn *Button) SetPicSysPhotoButton(name, key string) *Button { btn.Type = "pic_sysphoto" btn.Name = name @@ -80,7 +80,7 @@ func (btn *Button) SetPicSysPhotoButton(name, key string) *Button { return btn } -//SetPicPhotoOrAlbumButton 设置弹出拍照或者相册发图类型按钮 +// SetPicPhotoOrAlbumButton 设置弹出拍照或者相册发图类型按钮 func (btn *Button) SetPicPhotoOrAlbumButton(name, key string) *Button { btn.Type = "pic_photo_or_album" btn.Name = name @@ -92,7 +92,7 @@ func (btn *Button) SetPicPhotoOrAlbumButton(name, key string) *Button { return btn } -//SetPicWeixinButton 设置弹出微信相册发图器类型按钮 +// SetPicWeixinButton 设置弹出微信相册发图器类型按钮 func (btn *Button) SetPicWeixinButton(name, key string) *Button { btn.Type = "pic_weixin" btn.Name = name @@ -104,7 +104,7 @@ func (btn *Button) SetPicWeixinButton(name, key string) *Button { return btn } -//SetLocationSelectButton 设置 弹出地理位置选择器 类型按钮 +// SetLocationSelectButton 设置 弹出地理位置选择器 类型按钮 func (btn *Button) SetLocationSelectButton(name, key string) *Button { btn.Type = "location_select" btn.Name = name @@ -116,7 +116,7 @@ func (btn *Button) SetLocationSelectButton(name, key string) *Button { return btn } -//SetMediaIDButton 设置 下发消息(除文本消息) 类型按钮 +// SetMediaIDButton 设置 下发消息(除文本消息) 类型按钮 func (btn *Button) SetMediaIDButton(name, mediaID string) *Button { btn.Type = "media_id" btn.Name = name @@ -128,7 +128,7 @@ func (btn *Button) SetMediaIDButton(name, mediaID string) *Button { return btn } -//SetViewLimitedButton 设置 跳转图文消息URL 类型按钮 +// SetViewLimitedButton 设置 跳转图文消息URL 类型按钮 func (btn *Button) SetViewLimitedButton(name, mediaID string) *Button { btn.Type = "view_limited" btn.Name = name @@ -140,7 +140,7 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) *Button { return btn } -//SetMiniprogramButton 设置 跳转小程序 类型按钮 (公众号后台必须已经关联小程序) +// SetMiniprogramButton 设置 跳转小程序 类型按钮 (公众号后台必须已经关联小程序) func (btn *Button) SetMiniprogramButton(name, url, appID, pagePath string) *Button { btn.Type = "miniprogram" btn.Name = name @@ -154,62 +154,62 @@ func (btn *Button) SetMiniprogramButton(name, url, appID, pagePath string) *Butt return btn } -//NewSubButton 二级菜单 +// NewSubButton 二级菜单 func NewSubButton(name string, subButtons []*Button) *Button { return (&Button{}).SetSubButton(name, subButtons) } -//NewClickButton btn 为click类型 +// NewClickButton btn 为click类型 func NewClickButton(name, key string) *Button { return (&Button{}).SetClickButton(name, key) } -//NewViewButton view类型 +// NewViewButton view类型 func NewViewButton(name, url string) *Button { return (&Button{}).SetViewButton(name, url) } -//NewScanCodePushButton 扫码推事件 +// NewScanCodePushButton 扫码推事件 func NewScanCodePushButton(name, key string) *Button { return (&Button{}).SetScanCodePushButton(name, key) } -//NewScanCodeWaitMsgButton 扫码推事件且弹出"消息接收中"提示框 +// NewScanCodeWaitMsgButton 扫码推事件且弹出"消息接收中"提示框 func NewScanCodeWaitMsgButton(name, key string) *Button { return (&Button{}).SetScanCodeWaitMsgButton(name, key) } -//NewPicSysPhotoButton 弹出系统拍照发图按钮 +// NewPicSysPhotoButton 弹出系统拍照发图按钮 func NewPicSysPhotoButton(name, key string) *Button { return (&Button{}).SetPicSysPhotoButton(name, key) } -//NewPicPhotoOrAlbumButton 弹出拍照或者相册发图类型按钮 +// NewPicPhotoOrAlbumButton 弹出拍照或者相册发图类型按钮 func NewPicPhotoOrAlbumButton(name, key string) *Button { return (&Button{}).SetPicPhotoOrAlbumButton(name, key) } -//NewPicWeixinButton 弹出微信相册发图器类型按钮 +// NewPicWeixinButton 弹出微信相册发图器类型按钮 func NewPicWeixinButton(name, key string) *Button { return (&Button{}).SetPicWeixinButton(name, key) } -//NewLocationSelectButton 弹出地理位置选择器 类型按钮 +// NewLocationSelectButton 弹出地理位置选择器 类型按钮 func NewLocationSelectButton(name, key string) *Button { return (&Button{}).SetLocationSelectButton(name, key) } -//NewMediaIDButton 下发消息(除文本消息) 类型按钮 +// NewMediaIDButton 下发消息(除文本消息) 类型按钮 func NewMediaIDButton(name, mediaID string) *Button { return (&Button{}).SetMediaIDButton(name, mediaID) } -//NewViewLimitedButton 跳转图文消息URL 类型按钮 +// NewViewLimitedButton 跳转图文消息URL 类型按钮 func NewViewLimitedButton(name, mediaID string) *Button { return (&Button{}).SetViewLimitedButton(name, mediaID) } -//NewMiniprogramButton 跳转小程序 类型按钮 (公众号后台必须已经关联小程序) +// NewMiniprogramButton 跳转小程序 类型按钮 (公众号后台必须已经关联小程序) func NewMiniprogramButton(name, url, appID, pagePath string) *Button { return (&Button{}).SetMiniprogramButton(name, url, appID, pagePath) } diff --git a/officialaccount/menu/menu.go b/officialaccount/menu/menu.go index 7454072..4739723 100644 --- a/officialaccount/menu/menu.go +++ b/officialaccount/menu/menu.go @@ -18,42 +18,42 @@ const ( menuSelfMenuInfoURL = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info" ) -//Menu struct +// Menu struct type Menu struct { *context.Context } -//reqMenu 设置菜单请求数据 +// reqMenu 设置菜单请求数据 type reqMenu struct { Button []*Button `json:"button,omitempty"` MatchRule *MatchRule `json:"matchrule,omitempty"` } -//reqDeleteConditional 删除个性化菜单请求数据 +// reqDeleteConditional 删除个性化菜单请求数据 type reqDeleteConditional struct { MenuID int64 `json:"menuid"` } -//reqMenuTryMatch 菜单匹配请求 +// reqMenuTryMatch 菜单匹配请求 type reqMenuTryMatch struct { UserID string `json:"user_id"` } -//resConditionalMenu 个性化菜单返回结果 +// resConditionalMenu 个性化菜单返回结果 type resConditionalMenu struct { Button []Button `json:"button"` MatchRule MatchRule `json:"matchrule"` MenuID int64 `json:"menuid"` } -//resMenuTryMatch 菜单匹配请求结果 +// resMenuTryMatch 菜单匹配请求结果 type resMenuTryMatch struct { util.CommonError Button []Button `json:"button"` } -//ResMenu 查询菜单的返回数据 +// ResMenu 查询菜单的返回数据 type ResMenu struct { util.CommonError @@ -64,7 +64,7 @@ type ResMenu struct { Conditionalmenu []resConditionalMenu `json:"conditionalmenu"` } -//ResSelfMenuInfo 自定义菜单配置返回结果 +// ResSelfMenuInfo 自定义菜单配置返回结果 type ResSelfMenuInfo struct { util.CommonError @@ -74,7 +74,7 @@ type ResSelfMenuInfo struct { } `json:"selfmenu_info"` } -//SelfMenuButton 自定义菜单配置详情 +// SelfMenuButton 自定义菜单配置详情 type SelfMenuButton struct { Type string `json:"type"` Name string `json:"name"` @@ -89,7 +89,7 @@ type SelfMenuButton struct { } `json:"news_info,omitempty"` } -//ButtonNew 图文消息菜单 +// ButtonNew 图文消息菜单 type ButtonNew struct { Title string `json:"title"` Author string `json:"author"` @@ -100,7 +100,7 @@ type ButtonNew struct { SourceURL string `json:"source_url"` } -//MatchRule 个性化菜单规则 +// MatchRule 个性化菜单规则 type MatchRule struct { GroupID string `json:"group_id,omitempty"` Sex string `json:"sex,omitempty"` @@ -111,14 +111,14 @@ type MatchRule struct { Language string `json:"language,omitempty"` } -//NewMenu 实例 +// NewMenu 实例 func NewMenu(context *context.Context) *Menu { menu := new(Menu) menu.Context = context return menu } -//SetMenu 设置按钮 +// SetMenu 设置按钮 func (menu *Menu) SetMenu(buttons []*Button) error { accessToken, err := menu.GetAccessToken() if err != nil { @@ -138,7 +138,7 @@ func (menu *Menu) SetMenu(buttons []*Button) error { return util.DecodeWithCommonError(response, "SetMenu") } -//SetMenuByJSON 设置按钮 +// SetMenuByJSON 设置按钮 func (menu *Menu) SetMenuByJSON(jsonInfo string) error { accessToken, err := menu.GetAccessToken() if err != nil { @@ -155,7 +155,7 @@ func (menu *Menu) SetMenuByJSON(jsonInfo string) error { return util.DecodeWithCommonError(response, "SetMenuByJSON") } -//GetMenu 获取菜单配置 +// GetMenu 获取菜单配置 func (menu *Menu) GetMenu() (resMenu ResMenu, err error) { var accessToken string accessToken, err = menu.GetAccessToken() @@ -179,7 +179,7 @@ func (menu *Menu) GetMenu() (resMenu ResMenu, err error) { return } -//DeleteMenu 删除菜单 +// DeleteMenu 删除菜单 func (menu *Menu) DeleteMenu() error { accessToken, err := menu.GetAccessToken() if err != nil { @@ -194,7 +194,7 @@ func (menu *Menu) DeleteMenu() error { return util.DecodeWithCommonError(response, "GetMenu") } -//AddConditional 添加个性化菜单 +// AddConditional 添加个性化菜单 func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error { accessToken, err := menu.GetAccessToken() if err != nil { @@ -215,7 +215,7 @@ func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error return util.DecodeWithCommonError(response, "AddConditional") } -//AddConditionalByJSON 添加个性化菜单 +// AddConditionalByJSON 添加个性化菜单 func (menu *Menu) AddConditionalByJSON(jsonInfo string) error { accessToken, err := menu.GetAccessToken() if err != nil { @@ -231,7 +231,7 @@ func (menu *Menu) AddConditionalByJSON(jsonInfo string) error { return util.DecodeWithCommonError(response, "AddConditional") } -//DeleteConditional 删除个性化菜单 +// DeleteConditional 删除个性化菜单 func (menu *Menu) DeleteConditional(menuID int64) error { accessToken, err := menu.GetAccessToken() if err != nil { @@ -251,7 +251,7 @@ func (menu *Menu) DeleteConditional(menuID int64) error { return util.DecodeWithCommonError(response, "DeleteConditional") } -//MenuTryMatch 菜单匹配 +// MenuTryMatch 菜单匹配 func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) { var accessToken string accessToken, err = menu.GetAccessToken() @@ -278,7 +278,7 @@ func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) { return } -//GetCurrentSelfMenuInfo 获取自定义菜单配置接口 +// GetCurrentSelfMenuInfo 获取自定义菜单配置接口 func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err error) { var accessToken string accessToken, err = menu.GetAccessToken() diff --git a/officialaccount/message/customer_message.go b/officialaccount/message/customer_message.go index 9c64cb0..6742a61 100644 --- a/officialaccount/message/customer_message.go +++ b/officialaccount/message/customer_message.go @@ -12,35 +12,36 @@ const ( customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send" ) -//Manager 消息管理者,可以发送消息 +// Manager 消息管理者,可以发送消息 type Manager struct { *context.Context } -//NewMessageManager 实例化消息管理者 +// NewMessageManager 实例化消息管理者 func NewMessageManager(context *context.Context) *Manager { return &Manager{ context, } } -//CustomerMessage 客服消息 +// CustomerMessage 客服消息 type CustomerMessage struct { - ToUser string `json:"touser"` //接受者OpenID - Msgtype MsgType `json:"msgtype"` //客服消息类型 - Text *MediaText `json:"text,omitempty"` //可选 - Image *MediaResource `json:"image,omitempty"` //可选 - Voice *MediaResource `json:"voice,omitempty"` //可选 - Video *MediaVideo `json:"video,omitempty"` //可选 - Music *MediaMusic `json:"music,omitempty"` //可选 - News *MediaNews `json:"news,omitempty"` //可选 - Mpnews *MediaResource `json:"mpnews,omitempty"` //可选 - Wxcard *MediaWxcard `json:"wxcard,omitempty"` //可选 - Msgmenu *MediaMsgmenu `json:"msgmenu,omitempty"` //可选 - Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` //可选 + ToUser string `json:"touser"` // 接受者OpenID + Msgtype MsgType `json:"msgtype"` // 客服消息类型 + Text *MediaText `json:"text,omitempty"` // 可选 + Image *MediaResource `json:"image,omitempty"` // 可选 + Voice *MediaResource `json:"voice,omitempty"` // 可选 + Video *MediaVideo `json:"video,omitempty"` // 可选 + Music *MediaMusic `json:"music,omitempty"` // 可选 + News *MediaNews `json:"news,omitempty"` // 可选 + Mpnews *MediaResource `json:"mpnews,omitempty"` // 可选 + Wxcard *MediaWxcard `json:"wxcard,omitempty"` // 可选 + Msgmenu *MediaMsgmenu `json:"msgmenu,omitempty"` // 可选 + Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` // 可选 + Mpnewsarticle *MediaArticle `json:"mpnewsarticle,omitempty"` // 可选 } -//NewCustomerTextMessage 文本消息结构体构造方法 +// NewCustomerTextMessage 文本消息结构体构造方法 func NewCustomerTextMessage(toUser, text string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -51,7 +52,7 @@ func NewCustomerTextMessage(toUser, text string) *CustomerMessage { } } -//NewCustomerImgMessage 图片消息的构造方法 +// NewCustomerImgMessage 图片消息的构造方法 func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -62,7 +63,7 @@ func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage { } } -//NewCustomerVoiceMessage 语音消息的构造方法 +// NewCustomerVoiceMessage 语音消息的构造方法 func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -73,7 +74,7 @@ func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage { } } -//NewCustomerMiniprogrampageMessage 小程序卡片消息的构造方法 +// NewCustomerMiniprogrampageMessage 小程序卡片消息的构造方法 func NewCustomerMiniprogrampageMessage(toUser, title, appID, pagePath, thumbMediaID string) *CustomerMessage { return &CustomerMessage{ ToUser: toUser, @@ -87,17 +88,22 @@ func NewCustomerMiniprogrampageMessage(toUser, title, appID, pagePath, thumbMedi } } -//MediaText 文本消息的文字 +// MediaText 文本消息的文字 type MediaText struct { Content string `json:"content"` } -//MediaResource 消息使用的永久素材id +// MediaResource 消息使用的永久素材id type MediaResource struct { MediaID string `json:"media_id"` } -//MediaVideo 视频消息包含的内容 +// MediaArticle 消息使用的已发布文章id +type MediaArticle struct { + ArticleID string `json:"article_id"` +} + +// MediaVideo 视频消息包含的内容 type MediaVideo struct { MediaID string `json:"media_id"` ThumbMediaID string `json:"thumb_media_id"` @@ -105,7 +111,7 @@ type MediaVideo struct { Description string `json:"description"` } -//MediaMusic 音乐消息包括的内容 +// MediaMusic 音乐消息包括的内容 type MediaMusic struct { Title string `json:"title"` Description string `json:"description"` @@ -114,12 +120,12 @@ type MediaMusic struct { ThumbMediaID string `json:"thumb_media_id"` } -//MediaNews 图文消息的内容 +// MediaNews 图文消息的内容 type MediaNews struct { Articles []MediaArticles `json:"articles"` } -//MediaArticles 图文消息的内容的文章列表中的单独一条 +// MediaArticles 图文消息的内容的文章列表中的单独一条 type MediaArticles struct { Title string `json:"title"` Description string `json:"description"` @@ -127,25 +133,25 @@ type MediaArticles struct { Picurl string `json:"picurl"` } -//MediaMsgmenu 菜单消息的内容 +// MediaMsgmenu 菜单消息的内容 type MediaMsgmenu struct { HeadContent string `json:"head_content"` List []MsgmenuItem `json:"list"` TailContent string `json:"tail_content"` } -//MsgmenuItem 菜单消息的菜单按钮 +// MsgmenuItem 菜单消息的菜单按钮 type MsgmenuItem struct { ID string `json:"id"` Content string `json:"content"` } -//MediaWxcard 卡券的id +// MediaWxcard 卡券的id type MediaWxcard struct { CardID string `json:"card_id"` } -//MediaMiniprogrampage 小程序消息 +// MediaMiniprogrampage 小程序消息 type MediaMiniprogrampage struct { Title string `json:"title"` AppID string `json:"appid"` @@ -153,7 +159,7 @@ type MediaMiniprogrampage struct { ThumbMediaID string `json:"thumb_media_id"` } -//Send 发送客服消息 +// Send 发送客服消息 func (manager *Manager) Send(msg *CustomerMessage) error { accessToken, err := manager.Context.GetAccessToken() if err != nil { diff --git a/officialaccount/message/image.go b/officialaccount/message/image.go index 93e6bc0..b79f9a4 100644 --- a/officialaccount/message/image.go +++ b/officialaccount/message/image.go @@ -1,6 +1,6 @@ package message -//Image 图片消息 +// Image 图片消息 type Image struct { CommonToken @@ -9,7 +9,7 @@ type Image struct { } `xml:"Image"` } -//NewImage 回复图片消息 +// NewImage 回复图片消息 func NewImage(mediaID string) *Image { image := new(Image) image.Image.MediaID = mediaID diff --git a/officialaccount/message/message.go b/officialaccount/message/message.go index ecbe12a..41ffedc 100644 --- a/officialaccount/message/message.go +++ b/officialaccount/message/message.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "github.com/silenceper/wechat/v2/officialaccount/device" + "github.com/silenceper/wechat/v2/officialaccount/freepublish" ) // MsgType 基本消息类型 @@ -16,85 +17,99 @@ type EventType string type InfoType string const ( - //MsgTypeText 表示文本消息 + // MsgTypeText 表示文本消息 MsgTypeText MsgType = "text" - //MsgTypeImage 表示图片消息 - MsgTypeImage = "image" - //MsgTypeVoice 表示语音消息 - MsgTypeVoice = "voice" - //MsgTypeVideo 表示视频消息 - MsgTypeVideo = "video" - //MsgTypeMiniprogrampage 表示小程序卡片消息 - MsgTypeMiniprogrampage = "miniprogrampage" - //MsgTypeShortVideo 表示短视频消息[限接收] - MsgTypeShortVideo = "shortvideo" - //MsgTypeLocation 表示坐标消息[限接收] - MsgTypeLocation = "location" - //MsgTypeLink 表示链接消息[限接收] - MsgTypeLink = "link" - //MsgTypeMusic 表示音乐消息[限回复] - MsgTypeMusic = "music" - //MsgTypeNews 表示图文消息[限回复] - MsgTypeNews = "news" - //MsgTypeTransfer 表示消息消息转发到客服 - MsgTypeTransfer = "transfer_customer_service" - //MsgTypeEvent 表示事件推送消息 - MsgTypeEvent = "event" + // MsgTypeImage 表示图片消息 + MsgTypeImage MsgType = "image" + // MsgTypeVoice 表示语音消息 + MsgTypeVoice MsgType = "voice" + // MsgTypeVideo 表示视频消息 + MsgTypeVideo MsgType = "video" + // MsgTypeMiniprogrampage 表示小程序卡片消息 + MsgTypeMiniprogrampage MsgType = "miniprogrampage" + // MsgTypeShortVideo 表示短视频消息[限接收] + MsgTypeShortVideo MsgType = "shortvideo" + // MsgTypeLocation 表示坐标消息[限接收] + MsgTypeLocation MsgType = "location" + // MsgTypeLink 表示链接消息[限接收] + MsgTypeLink MsgType = "link" + // MsgTypeMusic 表示音乐消息[限回复] + MsgTypeMusic MsgType = "music" + // MsgTypeNews 表示图文消息[限回复] + MsgTypeNews MsgType = "news" + // MsgTypeTransfer 表示消息消息转发到客服 + MsgTypeTransfer MsgType = "transfer_customer_service" + // MsgTypeEvent 表示事件推送消息 + MsgTypeEvent MsgType = "event" ) const ( - //EventSubscribe 订阅 + // EventSubscribe 订阅 EventSubscribe EventType = "subscribe" - //EventUnsubscribe 取消订阅 - EventUnsubscribe = "unsubscribe" - //EventScan 用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者 - EventScan = "SCAN" - //EventLocation 上报地理位置事件 - EventLocation = "LOCATION" - //EventClick 点击菜单拉取消息时的事件推送 - EventClick = "CLICK" - //EventView 点击菜单跳转链接时的事件推送 - EventView = "VIEW" - //EventScancodePush 扫码推事件的事件推送 - EventScancodePush = "scancode_push" - //EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送 - EventScancodeWaitmsg = "scancode_waitmsg" - //EventPicSysphoto 弹出系统拍照发图的事件推送 - EventPicSysphoto = "pic_sysphoto" - //EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送 - EventPicPhotoOrAlbum = "pic_photo_or_album" - //EventPicWeixin 弹出微信相册发图器的事件推送 - EventPicWeixin = "pic_weixin" - //EventLocationSelect 弹出地理位置选择器的事件推送 - EventLocationSelect = "location_select" - //EventTemplateSendJobFinish 发送模板消息推送通知 - EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH" - //EventMassSendJobFinish 群发消息推送通知 - EventMassSendJobFinish = "MASSSENDJOBFINISH" - //EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件 - EventWxaMediaCheck = "wxa_media_check" + // EventUnsubscribe 取消订阅 + EventUnsubscribe EventType = "unsubscribe" + // EventScan 用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者 + EventScan EventType = "SCAN" + // EventLocation 上报地理位置事件 + EventLocation EventType = "LOCATION" + // EventClick 点击菜单拉取消息时的事件推送 + EventClick EventType = "CLICK" + // EventView 点击菜单跳转链接时的事件推送 + EventView EventType = "VIEW" + // EventScancodePush 扫码推事件的事件推送 + EventScancodePush EventType = "scancode_push" + // EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送 + EventScancodeWaitmsg EventType = "scancode_waitmsg" + // EventPicSysphoto 弹出系统拍照发图的事件推送 + EventPicSysphoto EventType = "pic_sysphoto" + // EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送 + EventPicPhotoOrAlbum EventType = "pic_photo_or_album" + // EventPicWeixin 弹出微信相册发图器的事件推送 + EventPicWeixin EventType = "pic_weixin" + // EventLocationSelect 弹出地理位置选择器的事件推送 + EventLocationSelect EventType = "location_select" + // EventViewMiniprogram 点击菜单跳转小程序的事件推送 + EventViewMiniprogram EventType = "view_miniprogram" + // EventTemplateSendJobFinish 发送模板消息推送通知 + EventTemplateSendJobFinish EventType = "TEMPLATESENDJOBFINISH" + // EventMassSendJobFinish 群发消息推送通知 + EventMassSendJobFinish EventType = "MASSSENDJOBFINISH" + // EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件 + EventWxaMediaCheck EventType = "wxa_media_check" + // EventSubscribeMsgPopupEvent 订阅通知事件推送 + EventSubscribeMsgPopupEvent EventType = "subscribe_msg_popup_event" + // EventPublishJobFinish 发布任务完成 + EventPublishJobFinish EventType = "PUBLISHJOBFINISH" + // EventWeappAuditSuccess 审核通过 + EventWeappAuditSuccess EventType = "weapp_audit_success" + // EventWeappAuditFail 审核不通过 + EventWeappAuditFail EventType = "weapp_audit_fail" + // EventWeappAuditDelay 审核延后 + EventWeappAuditDelay EventType = "weapp_audit_delay" ) const ( - //微信开放平台需要用到 + // 微信开放平台需要用到 // InfoTypeVerifyTicket 返回ticket InfoTypeVerifyTicket InfoType = "component_verify_ticket" // InfoTypeAuthorized 授权 - InfoTypeAuthorized = "authorized" + InfoTypeAuthorized InfoType = "authorized" // InfoTypeUnauthorized 取消授权 - InfoTypeUnauthorized = "unauthorized" + InfoTypeUnauthorized InfoType = "unauthorized" // InfoTypeUpdateAuthorized 更新授权 - InfoTypeUpdateAuthorized = "updateauthorized" + InfoTypeUpdateAuthorized InfoType = "updateauthorized" + // InfoTypeNotifyThirdFasterRegister 注册审核事件推送 + InfoTypeNotifyThirdFasterRegister InfoType = "notify_third_fasteregister" ) -//MixMessage 存放所有微信发送过来的消息和事件 +// MixMessage 存放所有微信发送过来的消息和事件 type MixMessage struct { CommonToken - //基本消息 - MsgID int64 `xml:"MsgId"` //其他消息推送过来是MsgId - TemplateMsgID int64 `xml:"MsgID"` //模板消息推送成功的消息是MsgID + // 基本消息 + MsgID int64 `xml:"MsgId"` // 其他消息推送过来是MsgId + TemplateMsgID int64 `xml:"MsgID"` // 模板消息推送成功的消息是MsgID Content string `xml:"Content"` Recognition string `xml:"Recognition"` PicURL string `xml:"PicUrl"` @@ -108,9 +123,10 @@ type MixMessage struct { Title string `xml:"Title"` Description string `xml:"Description"` URL string `xml:"Url"` + BizMsgMenuID int64 `xml:"bizmsgmenuid"` - //事件相关 - Event EventType `xml:"Event"` + // 事件相关 + Event EventType `xml:"Event" json:"Event"` EventKey string `xml:"EventKey"` Ticket string `xml:"Ticket"` Latitude string `xml:"Latitude"` @@ -142,6 +158,27 @@ type MixMessage struct { Poiname string `xml:"Poiname"` } + subscribeMsgPopupEventList []SubscribeMsgPopupEvent `json:"-"` + + SubscribeMsgPopupEvent []struct { + List SubscribeMsgPopupEvent `xml:"List"` + } `xml:"SubscribeMsgPopupEvent"` + + // 事件相关:发布能力 + PublishEventInfo struct { + PublishID int64 `xml:"publish_id"` // 发布任务id + PublishStatus freepublish.PublishStatus `xml:"publish_status"` // 发布状态 + ArticleID string `xml:"article_id"` // 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景 + ArticleDetail struct { + Count uint `xml:"count"` // 文章数量 + Item []struct { + Index uint `xml:"idx"` // 文章对应的编号 + ArticleURL string `xml:"article_url"` // 图文的永久链接 + } `xml:"item"` + } `xml:"article_detail"` // 当发布状态为0时(即成功)时,返回内容 + FailIndex []uint `xml:"fail_idx"` // 当发布状态为2或4时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空 + } `xml:"PublishEventInfo"` + // 第三方平台相关 InfoType InfoType `xml:"InfoType"` AppID string `xml:"AppId"` @@ -150,6 +187,23 @@ type MixMessage struct { AuthorizationCode string `xml:"AuthorizationCode"` AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"` PreAuthCode string `xml:"PreAuthCode"` + AuthCode string `xml:"auth_code"` + Info struct { + Name string `xml:"name"` + Code string `xml:"code"` + CodeType int `xml:"code_type"` + LegalPersonaWechat string `xml:"legal_persona_wechat"` + LegalPersonaName string `xml:"legal_persona_name"` + ComponentPhone string `xml:"component_phone"` + } `xml:"info"` + ResultInfo struct { + APIName string `xml:"api_name"` + ApplyTime string `xml:"apply_time"` + AuditID string `xml:"audit_id"` + AuditTime string `xml:"audit_time"` + Reason string `xml:"reason"` + Status string `xml:"status"` + } `xml:"result_info"` // 卡券相关 CardID string `xml:"CardId"` @@ -168,23 +222,58 @@ type MixMessage struct { TraceID string `xml:"trace_id"` StatusCode int `xml:"status_code"` - //设备相关 + //小程序名称审核结果事件推送 + Ret int32 `xml:"ret"` //审核结果 2:失败,3:成功 + NickName string `xml:"nickname"` //小程序昵称 + + // 设备相关 device.MsgDevice + + //小程序审核通知 + SuccTime int `xml:"SuccTime"` //审核成功时的时间戳 + FailTime int `xml:"FailTime"` //审核不通过的时间戳 + DelayTime int `xml:"DelayTime"` //审核延后时的时间戳 + Reason string `xml:"Reason"` //审核不通过的原因 + ScreenShot string `xml:"ScreenShot"` //审核不通过的截图示例。用 | 分隔的 media_id 的列表,可通过获取永久素材接口拉取截图内容 } -//EventPic 发图事件推送 +// SubscribeMsgPopupEvent 订阅通知事件推送的消息体 +type SubscribeMsgPopupEvent struct { + TemplateID string `xml:"TemplateId" json:"TemplateId"` + SubscribeStatusString string `xml:"SubscribeStatusString" json:"SubscribeStatusString"` + PopupScene int `xml:"PopupScene" json:"PopupScene,string"` +} + +// SetSubscribeMsgPopupEvents 设置订阅消息事件 +func (s *MixMessage) SetSubscribeMsgPopupEvents(list []SubscribeMsgPopupEvent) { + s.subscribeMsgPopupEventList = list +} + +// GetSubscribeMsgPopupEvents 获取订阅消息事件数据 +func (s *MixMessage) GetSubscribeMsgPopupEvents() []SubscribeMsgPopupEvent { + if s.subscribeMsgPopupEventList != nil { + return s.subscribeMsgPopupEventList + } + list := make([]SubscribeMsgPopupEvent, len(s.SubscribeMsgPopupEvent)) + for i, item := range s.SubscribeMsgPopupEvent { + list[i] = item.List + } + return list +} + +// EventPic 发图事件推送 type EventPic struct { PicMd5Sum string `xml:"PicMd5Sum"` } -//EncryptedXMLMsg 安全模式下的消息体 +// EncryptedXMLMsg 安全模式下的消息体 type EncryptedXMLMsg struct { XMLName struct{} `xml:"xml" json:"-"` ToUserName string `xml:"ToUserName" json:"ToUserName"` EncryptedMsg string `xml:"Encrypt" json:"Encrypt"` } -//ResponseEncryptedXMLMsg 需要返回的消息体 +// ResponseEncryptedXMLMsg 需要返回的消息体 type ResponseEncryptedXMLMsg struct { XMLName struct{} `xml:"xml" json:"-"` EncryptedMsg string `xml:"Encrypt" json:"Encrypt"` @@ -206,33 +295,33 @@ func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error { // CommonToken 消息中通用的结构 type CommonToken struct { XMLName xml.Name `xml:"xml"` - ToUserName CDATA `xml:"ToUserName"` - FromUserName CDATA `xml:"FromUserName"` - CreateTime int64 `xml:"CreateTime"` - MsgType MsgType `xml:"MsgType"` + ToUserName CDATA `xml:"ToUserName" json:"ToUserName"` + FromUserName CDATA `xml:"FromUserName" json:"FromUserName"` + CreateTime int64 `xml:"CreateTime" json:"CreateTime"` + MsgType MsgType `xml:"MsgType" json:"MsgType"` } -//SetToUserName set ToUserName +// SetToUserName set ToUserName func (msg *CommonToken) SetToUserName(toUserName CDATA) { msg.ToUserName = toUserName } -//SetFromUserName set FromUserName +// SetFromUserName set FromUserName func (msg *CommonToken) SetFromUserName(fromUserName CDATA) { msg.FromUserName = fromUserName } -//SetCreateTime set createTime +// SetCreateTime set createTime func (msg *CommonToken) SetCreateTime(createTime int64) { msg.CreateTime = createTime } -//SetMsgType set MsgType +// SetMsgType set MsgType func (msg *CommonToken) SetMsgType(msgType MsgType) { msg.MsgType = msgType } -//GetOpenID get the FromUserName value +// GetOpenID get the FromUserName value func (msg *CommonToken) GetOpenID() string { return string(msg.FromUserName) } diff --git a/officialaccount/message/music.go b/officialaccount/message/music.go index 3e010ed..5d71f5a 100644 --- a/officialaccount/message/music.go +++ b/officialaccount/message/music.go @@ -1,6 +1,6 @@ package message -//Music 音乐消息 +// Music 音乐消息 type Music struct { CommonToken @@ -13,7 +13,7 @@ type Music struct { } `xml:"Music"` } -//NewMusic 回复音乐消息 +// NewMusic 回复音乐消息 func NewMusic(title, description, musicURL, hQMusicURL, thumbMediaID string) *Music { music := new(Music) music.Music.Title = title diff --git a/officialaccount/message/news.go b/officialaccount/message/news.go index ee28b0c..fa00249 100644 --- a/officialaccount/message/news.go +++ b/officialaccount/message/news.go @@ -1,6 +1,6 @@ package message -//News 图文消息 +// News 图文消息 type News struct { CommonToken @@ -8,7 +8,7 @@ type News struct { Articles []*Article `xml:"Articles>item,omitempty"` } -//NewNews 初始化图文消息 +// NewNews 初始化图文消息 func NewNews(articles []*Article) *News { news := new(News) news.ArticleCount = len(articles) @@ -16,7 +16,7 @@ func NewNews(articles []*Article) *News { return news } -//Article 单篇文章 +// Article 单篇文章 type Article struct { Title string `xml:"Title,omitempty"` Description string `xml:"Description,omitempty"` @@ -24,7 +24,7 @@ type Article struct { URL string `xml:"Url,omitempty"` } -//NewArticle 初始化文章 +// NewArticle 初始化文章 func NewArticle(title, description, picURL, url string) *Article { article := new(Article) article.Title = title diff --git a/officialaccount/message/reply.go b/officialaccount/message/reply.go index 53592f0..5488fb8 100644 --- a/officialaccount/message/reply.go +++ b/officialaccount/message/reply.go @@ -2,13 +2,13 @@ package message import "errors" -//ErrInvalidReply 无效的回复 +// ErrInvalidReply 无效的回复 var ErrInvalidReply = errors.New("无效的回复消息") -//ErrUnsupportReply 不支持的回复类型 +// ErrUnsupportReply 不支持的回复类型 var ErrUnsupportReply = errors.New("不支持的回复消息") -//Reply 消息回复 +// Reply 消息回复 type Reply struct { MsgType MsgType MsgData interface{} diff --git a/officialaccount/message/subscribe.go b/officialaccount/message/subscribe.go index 0cf5172..9b6e18a 100644 --- a/officialaccount/message/subscribe.go +++ b/officialaccount/message/subscribe.go @@ -8,41 +8,46 @@ import ( ) const ( - subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend" - subscribeTemplateListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate" + subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend" + subscribeTemplateListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate" + subscribeTemplateAddURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate" + subscribeTemplateDelURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate" + subscribeTemplateGetCategoryURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getcategory" + subscribeTemplateGetPubTplKeyWorksURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords" + subscribeTemplateGetPubTplTitles = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles" ) -//Subscrib 订阅消息 -type Subscrib struct { +// Subscribe 订阅消息 +type Subscribe struct { *context.Context } -//NewSubscrib 实例化 -func NewSubscrib(context *context.Context) *Subscrib { - tpl := new(Subscrib) +// NewSubscribe 实例化 +func NewSubscribe(context *context.Context) *Subscribe { + tpl := new(Subscribe) tpl.Context = context return tpl } -//SubscribeMessage 发送的订阅消息内容 +// SubscribeMessage 发送的订阅消息内容 type SubscribeMessage struct { ToUser string `json:"touser"` // 必须, 接受者OpenID TemplateID string `json:"template_id"` // 必须, 模版ID Page string `json:"page,omitempty"` // 可选, 跳转网页时填写 Data map[string]*SubscribeDataItem `json:"data"` // 必须, 模板数据 MiniProgram struct { - AppID string `json:"appid"` //所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) - PagePath string `json:"pagepath"` //所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar) - } `json:"miniprogram"` //可选,跳转至小程序地址 + AppID string `json:"appid"` // 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) + PagePath string `json:"pagepath"` // 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar) + } `json:"miniprogram"` // 可选,跳转至小程序地址 } -//SubscribeDataItem 模版内某个 .DATA 的值 +// SubscribeDataItem 模版内某个 .DATA 的值 type SubscribeDataItem struct { Value string `json:"value"` } -//Send 发送订阅消息 -func (tpl *Subscrib) Send(msg *SubscribeMessage) (err error) { +// Send 发送订阅消息 +func (tpl *Subscribe) Send(msg *SubscribeMessage) (err error) { var accessToken string accessToken, err = tpl.GetAccessToken() if err != nil { @@ -53,25 +58,25 @@ func (tpl *Subscrib) Send(msg *SubscribeMessage) (err error) { if err != nil { return } - return util.DecodeWithCommonError(response, "SendSubscribMessage") + return util.DecodeWithCommonError(response, "SendSubscribeMessage") } -// PrivateSubscribItem 私有订阅消息模板 -type PrivateSubscribItem struct { +// PrivateSubscribeItem 私有订阅消息模板 +type PrivateSubscribeItem struct { PriTmplID string `json:"priTmplId"` // 添加至帐号下的模板 id,发送订阅通知时所需 - Title string `json:"title"` //模版标题 - Content string `json:"content"` //模版内容 - Example string `json:"example"` //模板内容示例 - SubType int `json:"type"` //模版类型,2 为一次性订阅,3 为长期订阅 + Title string `json:"title"` // 模版标题 + Content string `json:"content"` // 模版内容 + Example string `json:"example"` // 模板内容示例 + SubType int `json:"type"` // 模版类型,2 为一次性订阅,3 为长期订阅 } -type resPrivateSubscribList struct { +type resPrivateSubscribeList struct { util.CommonError - SubscriptionList []*PrivateSubscribItem `json:"data"` + SubscriptionList []*PrivateSubscribeItem `json:"data"` } -//List 获取私有订阅消息模板列表 -func (tpl *Subscrib) List() (templateList []*PrivateSubscribItem, err error) { +// List 获取私有订阅消息模板列表 +func (tpl *Subscribe) List() (templateList []*PrivateSubscribeItem, err error) { var accessToken string accessToken, err = tpl.GetAccessToken() if err != nil { @@ -83,11 +88,168 @@ func (tpl *Subscrib) List() (templateList []*PrivateSubscribItem, err error) { if err != nil { return } - var res resPrivateSubscribList - err = util.DecodeWithError(response, &res, "ListSubscription") + var res resPrivateSubscribeList + err = util.DecodeWithError(response, &res, "ListSubscribe") if err != nil { return } templateList = res.SubscriptionList return } + +type resSubscribeAdd struct { + util.CommonError + + TemplateID string `json:"priTmplId"` +} + +// Add 添加订阅消息模板 +func (tpl *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templateID string, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + var msg = struct { + TemplateIDShort string `json:"tid"` + SceneDesc string `json:"sceneDesc"` + KidList []int `json:"kidList"` + }{TemplateIDShort: ShortID, SceneDesc: sceneDesc, KidList: kidList} + uri := fmt.Sprintf("%s?access_token=%s", subscribeTemplateAddURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + var result resSubscribeAdd + err = util.DecodeWithError(response, &result, "AddSubscribe") + if err != nil { + return + } + templateID = result.TemplateID + return +} + +// Delete 删除私有模板 +func (tpl *Subscribe) Delete(templateID string) (err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + var msg = struct { + TemplateID string `json:"priTmplId"` + }{TemplateID: templateID} + uri := fmt.Sprintf("%s?access_token=%s", subscribeTemplateDelURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + return util.DecodeWithCommonError(response, "DeleteSubscribe") +} + +// PublicTemplateCategory 公众号类目 +type PublicTemplateCategory struct { + ID int `json:"id"` // 类目ID + Name string `json:"name"` // 类目的中文名 +} + +type resSubscribeCategoryList struct { + util.CommonError + CategoryList []*PublicTemplateCategory `json:"data"` +} + +// GetCategory 获取公众号类目 +func (tpl *Subscribe) GetCategory() (categoryList []*PublicTemplateCategory, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", subscribeTemplateGetCategoryURL, accessToken) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var result resSubscribeCategoryList + err = util.DecodeWithError(response, &result, "GetCategory") + if err != nil { + return + } + categoryList = result.CategoryList + return +} + +// PublicTemplateKeyWords 模板中的关键词 +type PublicTemplateKeyWords struct { + KeyWordsID int `json:"kid"` // 关键词 id + Name string `json:"name"` // 关键词内容 + Example string `json:"example"` // 关键词内容对应的示例 + Rule string `json:"rule"` // 参数类型 +} + +type resPublicTemplateKeyWordsList struct { + util.CommonError + KeyWordsList []*PublicTemplateKeyWords `json:"data"` // 关键词列表 +} + +// GetPubTplKeyWordsByID 获取模板中的关键词 +func (tpl *Subscribe) GetPubTplKeyWordsByID(titleID string) (keyWordsList []*PublicTemplateKeyWords, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s&tid=%s", subscribeTemplateGetPubTplKeyWorksURL, accessToken, titleID) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var result resPublicTemplateKeyWordsList + err = util.DecodeWithError(response, &result, "GetPublicTemplateKeyWords") + if err != nil { + return + } + keyWordsList = result.KeyWordsList + return +} + +// PublicTemplateTitle 类目下的公共模板 +type PublicTemplateTitle struct { + TitleID int `json:"tid"` // 模版标题 id + Title string `json:"title"` // 模版标题 + Type int `json:"type"` // 模版类型,2 为一次性订阅,3 为长期订阅 + CategoryID string `json:"categoryId"` // 模版所属类目 id +} + +type resPublicTemplateTitleList struct { + util.CommonError + Count int `json:"count"` // 公共模板列表总数 + TemplateTitleList []*PublicTemplateTitle `json:"data"` // 模板标题列表 +} + +// GetPublicTemplateTitleList 获取类目下的公共模板 +func (tpl *Subscribe) GetPublicTemplateTitleList(ids string, start int, limit int) (count int, templateTitleList []*PublicTemplateTitle, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s&ids=%s&start=%d&limit=%d", subscribeTemplateGetPubTplTitles, accessToken, ids, start, limit) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var result resPublicTemplateTitleList + err = util.DecodeWithError(response, &result, "GetPublicTemplateTitle") + if err != nil { + return + } + count = result.Count + templateTitleList = result.TemplateTitleList + return +} diff --git a/officialaccount/message/template.go b/officialaccount/message/template.go index 2964d5c..24a47ba 100644 --- a/officialaccount/message/template.go +++ b/officialaccount/message/template.go @@ -11,35 +11,38 @@ import ( const ( templateSendURL = "https://api.weixin.qq.com/cgi-bin/message/template/send" templateListURL = "https://api.weixin.qq.com/cgi-bin/template/get_all_private_template" + templateAddURL = "https://api.weixin.qq.com/cgi-bin/template/api_add_template" + templateDelURL = "https://api.weixin.qq.com/cgi-bin/template/del_private_template" ) -//Template 模板消息 +// Template 模板消息 type Template struct { *context.Context } -//NewTemplate 实例化 +// NewTemplate 实例化 func NewTemplate(context *context.Context) *Template { tpl := new(Template) tpl.Context = context return tpl } -//TemplateMessage 发送的模板消息内容 +// TemplateMessage 发送的模板消息内容 type TemplateMessage struct { - ToUser string `json:"touser"` // 必须, 接受者OpenID - TemplateID string `json:"template_id"` // 必须, 模版ID - URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中 - Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置 - Data map[string]*TemplateDataItem `json:"data"` // 必须, 模板数据 + ToUser string `json:"touser"` // 必须, 接受者OpenID + TemplateID string `json:"template_id"` // 必须, 模版ID + URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中 + Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置 + Data map[string]*TemplateDataItem `json:"data"` // 必须, 模板数据 + ClientMsgID string `json:"client_msg_id,omitempty"` // 可选, 防重入ID MiniProgram struct { - AppID string `json:"appid"` //所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) - PagePath string `json:"pagepath"` //所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar) - } `json:"miniprogram"` //可选,跳转至小程序地址 + AppID string `json:"appid"` // 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) + PagePath string `json:"pagepath"` // 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar) + } `json:"miniprogram"` // 可选,跳转至小程序地址 } -//TemplateDataItem 模版内某个 .DATA 的值 +// TemplateDataItem 模版内某个 .DATA 的值 type TemplateDataItem struct { Value string `json:"value"` Color string `json:"color,omitempty"` @@ -51,7 +54,7 @@ type resTemplateSend struct { MsgID int64 `json:"msgid"` } -//Send 发送模板消息 +// Send 发送模板消息 func (tpl *Template) Send(msg *TemplateMessage) (msgID int64, err error) { var accessToken string accessToken, err = tpl.GetAccessToken() @@ -59,7 +62,8 @@ func (tpl *Template) Send(msg *TemplateMessage) (msgID int64, err error) { return } uri := fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken) - response, err := util.PostJSON(uri, msg) + var response []byte + response, err = util.PostJSON(uri, msg) if err != nil { return } @@ -92,7 +96,7 @@ type resTemplateList struct { TemplateList []*TemplateItem `json:"template_list"` } -//List 获取模板列表 +// List 获取模板列表 func (tpl *Template) List() (templateList []*TemplateItem, err error) { var accessToken string accessToken, err = tpl.GetAccessToken() @@ -113,3 +117,55 @@ func (tpl *Template) List() (templateList []*TemplateItem, err error) { templateList = res.TemplateList return } + +type resTemplateAdd struct { + util.CommonError + + TemplateID string `json:"template_id"` +} + +// Add 添加模板. +func (tpl *Template) Add(shortID string) (templateID string, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + var msg = struct { + ShortID string `json:"template_id_short"` + }{ShortID: shortID} + uri := fmt.Sprintf("%s?access_token=%s", templateAddURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + + var result resTemplateAdd + err = util.DecodeWithError(response, &result, "AddTemplate") + if err != nil { + return + } + templateID = result.TemplateID + return +} + +// Delete 删除私有模板. +func (tpl *Template) Delete(templateID string) (err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + var msg = struct { + TemplateID string `json:"template_id"` + }{TemplateID: templateID} + + uri := fmt.Sprintf("%s?access_token=%s", templateDelURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + return util.DecodeWithCommonError(response, "DeleteTemplate") +} diff --git a/officialaccount/message/text.go b/officialaccount/message/text.go index 88ac19a..a6819d9 100644 --- a/officialaccount/message/text.go +++ b/officialaccount/message/text.go @@ -1,12 +1,12 @@ package message -//Text 文本消息 +// Text 文本消息 type Text struct { CommonToken Content CDATA `xml:"Content"` } -//NewText 初始化文本消息 +// NewText 初始化文本消息 func NewText(content string) *Text { text := new(Text) text.Content = CDATA(content) diff --git a/officialaccount/message/transfer_customer.go b/officialaccount/message/transfer_customer.go index af336e8..7dbf9b7 100644 --- a/officialaccount/message/transfer_customer.go +++ b/officialaccount/message/transfer_customer.go @@ -1,18 +1,18 @@ package message -//TransferCustomer 转发客服消息 +// TransferCustomer 转发客服消息 type TransferCustomer struct { CommonToken TransInfo *TransInfo `xml:"TransInfo,omitempty"` } -//TransInfo 转发到指定客服 +// TransInfo 转发到指定客服 type TransInfo struct { KfAccount string `xml:"KfAccount"` } -//NewTransferCustomer 实例化 +// NewTransferCustomer 实例化 func NewTransferCustomer(kfAccount string) *TransferCustomer { tc := new(TransferCustomer) if kfAccount != "" { diff --git a/officialaccount/message/video.go b/officialaccount/message/video.go index a082065..6f64875 100644 --- a/officialaccount/message/video.go +++ b/officialaccount/message/video.go @@ -1,6 +1,6 @@ package message -//Video 视频消息 +// Video 视频消息 type Video struct { CommonToken @@ -11,7 +11,7 @@ type Video struct { } `xml:"Video"` } -//NewVideo 回复图片消息 +// NewVideo 回复图片消息 func NewVideo(mediaID, title, description string) *Video { video := new(Video) video.Video.MediaID = mediaID diff --git a/officialaccount/message/voice.go b/officialaccount/message/voice.go index d76985c..a9cb662 100644 --- a/officialaccount/message/voice.go +++ b/officialaccount/message/voice.go @@ -1,6 +1,6 @@ package message -//Voice 语音消息 +// Voice 语音消息 type Voice struct { CommonToken @@ -9,7 +9,7 @@ type Voice struct { } `xml:"Voice"` } -//NewVoice 回复语音消息 +// NewVoice 回复语音消息 func NewVoice(mediaID string) *Voice { voice := new(Voice) voice.Voice.MediaID = mediaID diff --git a/officialaccount/oauth/oauth.go b/officialaccount/oauth/oauth.go index 27dde91..c7c647a 100644 --- a/officialaccount/oauth/oauth.go +++ b/officialaccount/oauth/oauth.go @@ -19,32 +19,32 @@ const ( checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s" ) -//Oauth 保存用户授权信息 +// Oauth 保存用户授权信息 type Oauth struct { *context.Context } -//NewOauth 实例化授权信息 +// NewOauth 实例化授权信息 func NewOauth(context *context.Context) *Oauth { auth := new(Oauth) auth.Context = context return auth } -//GetRedirectURL 获取跳转的url地址 +// GetRedirectURL 获取跳转的url地址 func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, error) { - //url encode + // url encode urlStr := url.QueryEscape(redirectURI) return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil } -//GetWebAppRedirectURL 获取网页应用跳转的url地址 +// GetWebAppRedirectURL 获取网页应用跳转的url地址 func (oauth *Oauth) GetWebAppRedirectURL(redirectURI, scope, state string) (string, error) { urlStr := url.QueryEscape(redirectURI) return fmt.Sprintf(webAppRedirectOauthURL, oauth.AppID, urlStr, scope, state), nil } -//Redirect 跳转到网页授权 +// Redirect 跳转到网页授权 func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error { location, err := oauth.GetRedirectURL(redirectURI, scope, state) if err != nil { @@ -64,6 +64,10 @@ type ResAccessToken struct { OpenID string `json:"openid"` Scope string `json:"scope"` + // IsSnapShotUser 是否为快照页模式虚拟账号,只有当用户是快照页模式虚拟账号时返回,值为1 + // 公众号文档 https://developers.weixin.qq.com/community/minihome/doc/000c2c34068880629ced91a2f56001 + IsSnapShotUser int `json:"is_snapshotuser"` + // UnionID 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 // 公众号文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 UnionID string `json:"unionid"` @@ -88,7 +92,7 @@ func (oauth *Oauth) GetUserAccessToken(code string) (result ResAccessToken, err return } -//RefreshAccessToken 刷新access_token +// RefreshAccessToken 刷新access_token func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessToken, err error) { urlStr := fmt.Sprintf(refreshAccessTokenURL, oauth.AppID, refreshToken) var response []byte @@ -107,7 +111,7 @@ func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessTok return } -//CheckAccessToken 检验access_token是否有效 +// CheckAccessToken 检验access_token是否有效 func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err error) { urlStr := fmt.Sprintf(checkAccessTokenURL, accessToken, openID) var response []byte @@ -128,7 +132,7 @@ func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err er return } -//UserInfo 用户授权获取到用户信息 +// UserInfo 用户授权获取到用户信息 type UserInfo struct { util.CommonError @@ -143,7 +147,7 @@ type UserInfo struct { Unionid string `json:"unionid"` } -//GetUserInfo 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息 +// GetUserInfo 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息 func (oauth *Oauth) GetUserInfo(accessToken, openID, lang string) (result UserInfo, err error) { if lang == "" { lang = "zh_CN" diff --git a/officialaccount/ocr/ocr.go b/officialaccount/ocr/ocr.go index c1521f8..2420a80 100644 --- a/officialaccount/ocr/ocr.go +++ b/officialaccount/ocr/ocr.go @@ -18,18 +18,18 @@ const ( ocrPlateNumberURL = "https://api.weixin.qq.com/cv/ocr/platenum" ) -//OCR struct +// OCR struct type OCR struct { *context.Context } -//coordinate 坐标 +// coordinate 坐标 type coordinate struct { X int64 `json:"x,omitempty"` Y int64 `json:"y,omitempty"` } -//position 位置 +// position 位置 type position struct { LeftTop coordinate `json:"left_top"` RightTop coordinate `json:"right_top"` @@ -37,13 +37,13 @@ type position struct { LeftBottom coordinate `json:"left_bottom"` } -//imageSize 图片尺寸 +// imageSize 图片尺寸 type imageSize struct { Width int64 `json:"w,omitempty"` Height int64 `json:"h,omitempty"` } -//ResDriving 行驶证返回结果 +// ResDriving 行驶证返回结果 type ResDriving struct { util.CommonError @@ -68,7 +68,7 @@ type ResDriving struct { ImageSize imageSize `json:"img_size,omitempty"` } -//ResIDCard 身份证返回结果 +// ResIDCard 身份证返回结果 type ResIDCard struct { util.CommonError @@ -81,14 +81,14 @@ type ResIDCard struct { ValidDate string `json:"valid_date,omitempty"` } -//ResBankCard 银行卡返回结果 +// ResBankCard 银行卡返回结果 type ResBankCard struct { util.CommonError Number string `json:"number,omitempty"` } -//ResDrivingLicense 驾驶证返回结果 +// ResDrivingLicense 驾驶证返回结果 type ResDrivingLicense struct { util.CommonError @@ -105,7 +105,7 @@ type ResDrivingLicense struct { OfficialSeal string `json:"official_seal,omitempty"` } -//ResBizLicense 营业执照返回结果 +// ResBizLicense 营业执照返回结果 type ResBizLicense struct { util.CommonError @@ -125,7 +125,7 @@ type ResBizLicense struct { ImageSize imageSize `json:"img_size,omitempty"` } -//ResCommon 公共印刷品返回结果 +// ResCommon 公共印刷品返回结果 type ResCommon struct { util.CommonError @@ -133,155 +133,141 @@ type ResCommon struct { ImageSize imageSize `json:"img_size,omitempty"` } -//commonItem 公共元素 +// commonItem 公共元素 type commonItem struct { Position position `json:"pos"` Text string `json:"text"` } -//ResPlateNumber 车牌号返回结果 +// ResPlateNumber 车牌号返回结果 type ResPlateNumber struct { util.CommonError Number string `json:"number"` } -//NewOCR 实例 +// NewOCR 实例 func NewOCR(c *context.Context) *OCR { ocr := new(OCR) ocr.Context = c return ocr } -//IDCard 身份证OCR识别接口 -func (ocr *OCR) IDCard(path string) (ResIDCard ResIDCard, err error) { +// IDCard 身份证OCR识别接口 +func (ocr *OCR) IDCard(path string) (resIDCard ResIDCard, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrIDCardURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrIDCardURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResIDCard, "OCRIDCard") + err = util.DecodeWithError(response, &resIDCard, "OCRIDCard") return } -//BankCard 银行卡OCR识别接口 -func (ocr *OCR) BankCard(path string) (ResBankCard ResBankCard, err error) { +// BankCard 银行卡OCR识别接口 +func (ocr *OCR) BankCard(path string) (resBankCard ResBankCard, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBankCardURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBankCardURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResBankCard, "OCRBankCard") + err = util.DecodeWithError(response, &resBankCard, "OCRBankCard") return } -//Driving 行驶证OCR识别接口 -func (ocr *OCR) Driving(path string) (ResDriving ResDriving, err error) { +// Driving 行驶证OCR识别接口 +func (ocr *OCR) Driving(path string) (resDriving ResDriving, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResDriving, "OCRDriving") + err = util.DecodeWithError(response, &resDriving, "OCRDriving") return } -//DrivingLicense 驾驶证OCR识别接口 -func (ocr *OCR) DrivingLicense(path string) (ResDrivingLicense ResDrivingLicense, err error) { +// DrivingLicense 驾驶证OCR识别接口 +func (ocr *OCR) DrivingLicense(path string) (resDrivingLicense ResDrivingLicense, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingLicenseURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingLicenseURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResDrivingLicense, "OCRDrivingLicense") + err = util.DecodeWithError(response, &resDrivingLicense, "OCRDrivingLicense") return } -//BizLicense 营业执照OCR识别接口 -func (ocr *OCR) BizLicense(path string) (ResBizLicense ResBizLicense, err error) { +// BizLicense 营业执照OCR识别接口 +func (ocr *OCR) BizLicense(path string) (resBizLicense ResBizLicense, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBizLicenseURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBizLicenseURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResBizLicense, "OCRBizLicense") + err = util.DecodeWithError(response, &resBizLicense, "OCRBizLicense") return } -//Common 通用印刷体OCR识别接口 -func (ocr *OCR) Common(path string) (ResCommon ResCommon, err error) { +// Common 通用印刷体OCR识别接口 +func (ocr *OCR) Common(path string) (resCommon ResCommon, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrCommonURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrCommonURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResCommon, "OCRCommon") + err = util.DecodeWithError(response, &resCommon, "OCRCommon") return } -//PlateNumber 车牌OCR识别接口 -func (ocr *OCR) PlateNumber(path string) (ResPlateNumber ResPlateNumber, err error) { +// PlateNumber 车牌OCR识别接口 +func (ocr *OCR) PlateNumber(path string) (resPlateNumber ResPlateNumber, err error) { accessToken, err := ocr.GetAccessToken() if err != nil { return } - uri := fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrPlateNumberURL, url.QueryEscape(path), accessToken) - - response, err := util.HTTPPost(uri, "") + response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrPlateNumberURL, url.QueryEscape(path), accessToken), "") if err != nil { return } - err = util.DecodeWithError(response, &ResPlateNumber, "OCRPlateNumber") + err = util.DecodeWithError(response, &resPlateNumber, "OCRPlateNumber") return } diff --git a/officialaccount/officialaccount.go b/officialaccount/officialaccount.go index 5c48c75..0c00817 100644 --- a/officialaccount/officialaccount.go +++ b/officialaccount/officialaccount.go @@ -1,8 +1,12 @@ package officialaccount import ( + stdcontext "context" "net/http" + "github.com/silenceper/wechat/v2/internal/openapi" + "github.com/silenceper/wechat/v2/officialaccount/draft" + "github.com/silenceper/wechat/v2/officialaccount/freepublish" "github.com/silenceper/wechat/v2/officialaccount/ocr" "github.com/silenceper/wechat/v2/officialaccount/datacube" @@ -12,6 +16,7 @@ import ( "github.com/silenceper/wechat/v2/officialaccount/broadcast" "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/officialaccount/customerservice" "github.com/silenceper/wechat/v2/officialaccount/device" "github.com/silenceper/wechat/v2/officialaccount/js" "github.com/silenceper/wechat/v2/officialaccount/material" @@ -22,12 +27,27 @@ import ( "github.com/silenceper/wechat/v2/officialaccount/user" ) -//OfficialAccount 微信公众号相关API +// OfficialAccount 微信公众号相关API type OfficialAccount struct { - ctx *context.Context + ctx *context.Context + basic *basic.Basic + menu *menu.Menu + oauth *oauth.Oauth + material *material.Material + draft *draft.Draft + freepublish *freepublish.FreePublish + js *js.Js + user *user.User + templateMsg *message.Template + managerMsg *message.Manager + device *device.Device + broadcast *broadcast.Broadcast + datacube *datacube.DataCube + ocr *ocr.OCR + subscribeMsg *message.Subscribe } -//NewOfficialAccount 实例化公众号API +// NewOfficialAccount 实例化公众号API func NewOfficialAccount(cfg *config.Config) *OfficialAccount { defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyOfficialAccountPrefix, cfg.Cache) ctx := &context.Context{ @@ -37,7 +57,7 @@ func NewOfficialAccount(cfg *config.Config) *OfficialAccount { return &OfficialAccount{ctx: ctx} } -//SetAccessTokenHandle 自定义access_token获取方式 +// SetAccessTokenHandle 自定义access_token获取方式 func (officialAccount *OfficialAccount) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) { officialAccount.ctx.AccessTokenHandle = accessTokenHandle } @@ -49,12 +69,18 @@ func (officialAccount *OfficialAccount) GetContext() *context.Context { // GetBasic qr/url 相关配置 func (officialAccount *OfficialAccount) GetBasic() *basic.Basic { - return basic.NewBasic(officialAccount.ctx) + if officialAccount.basic == nil { + officialAccount.basic = basic.NewBasic(officialAccount.ctx) + } + return officialAccount.basic } // GetMenu 菜单管理接口 func (officialAccount *OfficialAccount) GetMenu() *menu.Menu { - return menu.NewMenu(officialAccount.ctx) + if officialAccount.menu == nil { + officialAccount.menu = menu.NewMenu(officialAccount.ctx) + } + return officialAccount.menu } // GetServer 消息管理:接收事件,被动回复消息管理 @@ -65,58 +91,130 @@ func (officialAccount *OfficialAccount) GetServer(req *http.Request, writer http return srv } -//GetAccessToken 获取access_token +// GetAccessToken 获取access_token func (officialAccount *OfficialAccount) GetAccessToken() (string, error) { return officialAccount.ctx.GetAccessToken() } +// GetAccessTokenContext 获取access_token +func (officialAccount *OfficialAccount) GetAccessTokenContext(ctx stdcontext.Context) (string, error) { + if c, ok := officialAccount.ctx.AccessTokenHandle.(credential.AccessTokenContextHandle); ok { + return c.GetAccessTokenContext(ctx) + } + return officialAccount.ctx.GetAccessToken() +} + // GetOauth oauth2网页授权 func (officialAccount *OfficialAccount) GetOauth() *oauth.Oauth { - return oauth.NewOauth(officialAccount.ctx) + if officialAccount.oauth == nil { + officialAccount.oauth = oauth.NewOauth(officialAccount.ctx) + } + return officialAccount.oauth } // GetMaterial 素材管理 func (officialAccount *OfficialAccount) GetMaterial() *material.Material { - return material.NewMaterial(officialAccount.ctx) + if officialAccount.material == nil { + officialAccount.material = material.NewMaterial(officialAccount.ctx) + } + return officialAccount.material +} + +// GetDraft 草稿箱 +func (officialAccount *OfficialAccount) GetDraft() *draft.Draft { + if officialAccount.draft == nil { + officialAccount.draft = draft.NewDraft(officialAccount.ctx) + } + return officialAccount.draft +} + +// GetFreePublish 发布能力 +func (officialAccount *OfficialAccount) GetFreePublish() *freepublish.FreePublish { + if officialAccount.freepublish == nil { + officialAccount.freepublish = freepublish.NewFreePublish(officialAccount.ctx) + } + return officialAccount.freepublish } // GetJs js-sdk配置 func (officialAccount *OfficialAccount) GetJs() *js.Js { - return js.NewJs(officialAccount.ctx) + if officialAccount.js == nil { + officialAccount.js = js.NewJs(officialAccount.ctx) + } + return officialAccount.js } // GetUser 用户管理接口 func (officialAccount *OfficialAccount) GetUser() *user.User { - return user.NewUser(officialAccount.ctx) + if officialAccount.user == nil { + officialAccount.user = user.NewUser(officialAccount.ctx) + } + return officialAccount.user } // GetTemplate 模板消息接口 func (officialAccount *OfficialAccount) GetTemplate() *message.Template { - return message.NewTemplate(officialAccount.ctx) + if officialAccount.templateMsg == nil { + officialAccount.templateMsg = message.NewTemplate(officialAccount.ctx) + } + return officialAccount.templateMsg } // GetCustomerMessageManager 客服消息接口 func (officialAccount *OfficialAccount) GetCustomerMessageManager() *message.Manager { - return message.NewMessageManager(officialAccount.ctx) + if officialAccount.managerMsg == nil { + officialAccount.managerMsg = message.NewMessageManager(officialAccount.ctx) + } + return officialAccount.managerMsg } // GetDevice 获取智能设备的实例 func (officialAccount *OfficialAccount) GetDevice() *device.Device { - return device.NewDevice(officialAccount.ctx) + if officialAccount.device == nil { + officialAccount.device = device.NewDevice(officialAccount.ctx) + } + return officialAccount.device } -//GetBroadcast 群发消息 -//TODO 待完善 +// GetBroadcast 群发消息 +// TODO 待完善 func (officialAccount *OfficialAccount) GetBroadcast() *broadcast.Broadcast { - return broadcast.NewBroadcast(officialAccount.ctx) + if officialAccount.broadcast == nil { + officialAccount.broadcast = broadcast.NewBroadcast(officialAccount.ctx) + } + return officialAccount.broadcast } -//GetDataCube 数据统计 +// GetDataCube 数据统计 func (officialAccount *OfficialAccount) GetDataCube() *datacube.DataCube { - return datacube.NewCube(officialAccount.ctx) + if officialAccount.datacube == nil { + officialAccount.datacube = datacube.NewCube(officialAccount.ctx) + } + return officialAccount.datacube } -//GetOCR OCR接口 +// GetOCR OCR接口 func (officialAccount *OfficialAccount) GetOCR() *ocr.OCR { - return ocr.NewOCR(officialAccount.ctx) + if officialAccount.ocr == nil { + officialAccount.ocr = ocr.NewOCR(officialAccount.ctx) + } + return officialAccount.ocr +} + +// GetSubscribe 公众号订阅消息 +func (officialAccount *OfficialAccount) GetSubscribe() *message.Subscribe { + if officialAccount.subscribeMsg == nil { + officialAccount.subscribeMsg = message.NewSubscribe(officialAccount.ctx) + } + return officialAccount.subscribeMsg +} + +// GetCustomerServiceManager 客服管理 +func (officialAccount *OfficialAccount) GetCustomerServiceManager() *customerservice.Manager { + return customerservice.NewCustomerServiceManager(officialAccount.ctx) +} + +// GetOpenAPI openApi管理接口 +func (officialAccount *OfficialAccount) GetOpenAPI() *openapi.OpenAPI { + return openapi.NewOpenAPI(officialAccount.ctx) } diff --git a/officialaccount/server/server.go b/officialaccount/server/server.go index 9436b7e..c741ee6 100644 --- a/officialaccount/server/server.go +++ b/officialaccount/server/server.go @@ -1,23 +1,26 @@ package server import ( + "encoding/json" "encoding/xml" "errors" "fmt" - "io/ioutil" + "io" "net/http" "reflect" "runtime/debug" "strconv" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" "github.com/silenceper/wechat/v2/officialaccount/context" "github.com/silenceper/wechat/v2/officialaccount/message" - log "github.com/sirupsen/logrus" - "github.com/silenceper/wechat/v2/util" ) -//Server struct +// Server struct type Server struct { *context.Context Writer http.ResponseWriter @@ -34,13 +37,14 @@ type Server struct { ResponseRawXMLMsg []byte ResponseMsg interface{} - isSafeMode bool - random []byte - nonce string - timestamp int64 + isSafeMode bool + isJSONContent bool + random []byte + nonce string + timestamp int64 } -//NewServer init +// NewServer init func NewServer(context *context.Context) *Server { srv := new(Server) srv.Context = context @@ -52,7 +56,7 @@ func (srv *Server) SkipValidate(skip bool) { srv.skipValidate = skip } -//Serve 处理微信的请求消息 +// Serve 处理微信的请求消息 func (srv *Server) Serve() error { if !srv.Validate() { log.Error("Validate Signature Failed.") @@ -69,14 +73,19 @@ func (srv *Server) Serve() error { if err != nil { return err } + // 非安全模式下,请求处理方法返回为nil则直接回复success给微信服务器 + if response == nil && !srv.isSafeMode { + srv.String("success") + return nil + } - //debug print request msg + // debug print request msg log.Debugf("request msg =%s", string(srv.RequestRawXMLMsg)) return srv.buildResponse(response) } -//Validate 校验请求是否合法 +// Validate 校验请求是否合法 func (srv *Server) Validate() bool { if srv.skipValidate { return true @@ -88,16 +97,20 @@ func (srv *Server) Validate() bool { return signature == util.Signature(srv.Token, timestamp, nonce) } -//HandleRequest 处理微信的请求 +// HandleRequest 处理微信的请求 func (srv *Server) handleRequest() (reply *message.Reply, err error) { - //set isSafeMode + // set isSafeMode srv.isSafeMode = false encryptType := srv.Query("encrypt_type") if encryptType == "aes" { srv.isSafeMode = true } - //set openID + // set request contentType + contentType := srv.Request.Header.Get("Content-Type") + srv.isJSONContent = strings.Contains(contentType, "application/json") + + // set openID srv.openID = srv.Query("openid") var msg interface{} @@ -114,22 +127,22 @@ func (srv *Server) handleRequest() (reply *message.Reply, err error) { return } -//GetOpenID return openID +// GetOpenID return openID func (srv *Server) GetOpenID() string { return srv.openID } -//getMessage 解析微信返回的消息 +// getMessage 解析微信返回的消息 func (srv *Server) getMessage() (interface{}, error) { var rawXMLMsgBytes []byte var err error if srv.isSafeMode { - var encryptedXMLMsg message.EncryptedXMLMsg - if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil { - return nil, fmt.Errorf("从body中解析xml失败,err=%v", err) + encryptedXMLMsg, dataErr := srv.getEncryptBody() + if dataErr != nil { + return nil, dataErr } - //验证消息签名 + // 验证消息签名 timestamp := srv.Query("timestamp") srv.timestamp, err = strconv.ParseInt(timestamp, 10, 32) if err != nil { @@ -143,13 +156,13 @@ func (srv *Server) getMessage() (interface{}, error) { return nil, fmt.Errorf("消息不合法,验证签名失败") } - //解密 + // 解密 srv.random, rawXMLMsgBytes, err = util.DecryptMsg(srv.AppID, encryptedXMLMsg.EncryptedMsg, srv.EncodingAESKey) if err != nil { return nil, fmt.Errorf("消息解密失败, err=%v", err) } } else { - rawXMLMsgBytes, err = ioutil.ReadAll(srv.Request.Body) + rawXMLMsgBytes, err = io.ReadAll(srv.Request.Body) if err != nil { return nil, fmt.Errorf("从body中解析xml失败, err=%v", err) } @@ -160,13 +173,52 @@ func (srv *Server) getMessage() (interface{}, error) { return srv.parseRequestMessage(rawXMLMsgBytes) } +func (srv *Server) getEncryptBody() (*message.EncryptedXMLMsg, error) { + var encryptedXMLMsg = &message.EncryptedXMLMsg{} + if srv.isJSONContent { + if err := json.NewDecoder(srv.Request.Body).Decode(encryptedXMLMsg); err != nil { + return nil, fmt.Errorf("从body中解析json失败,err=%v", err) + } + } else { + if err := xml.NewDecoder(srv.Request.Body).Decode(encryptedXMLMsg); err != nil { + return nil, fmt.Errorf("从body中解析xml失败,err=%v", err) + } + } + return encryptedXMLMsg, nil +} + func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixMessage, err error) { msg = &message.MixMessage{} - err = xml.Unmarshal(rawXMLMsgBytes, msg) + if !srv.isJSONContent { + err = xml.Unmarshal(rawXMLMsgBytes, msg) + return + } + // parse json + err = json.Unmarshal(rawXMLMsgBytes, msg) + if err != nil { + return + } + // nonstandard json, 目前小程序订阅消息返回数据格式不标准,订阅消息模板单个List返回是对象,多个List返回是数组。 + if msg.MsgType == message.MsgTypeEvent { + listData := gjson.Get(string(rawXMLMsgBytes), "List") + if listData.IsObject() { + listItem := message.SubscribeMsgPopupEvent{} + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil { + return msg, parseErr + } + msg.SetSubscribeMsgPopupEvents([]message.SubscribeMsgPopupEvent{listItem}) + } else if listData.IsArray() { + listItems := make([]message.SubscribeMsgPopupEvent, 0) + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil { + return msg, parseErr + } + msg.SetSubscribeMsgPopupEvents(listItems) + } + } return } -//SetMessageHandler 设置用户自定义的回调方法 +// SetMessageHandler 设置用户自定义的回调方法 func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) { srv.messageHandler = handler } @@ -178,7 +230,7 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) { } }() if reply == nil { - //do nothing + // do nothing return nil } msgType := reply.MsgType @@ -197,7 +249,7 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) { msgData := reply.MsgData value := reflect.ValueOf(msgData) - //msgData must be a ptr + // msgData must be a ptr kind := value.Kind().String() if kind != "ptr" { return message.ErrUnsupportReply @@ -221,18 +273,18 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) { return } -//Send 将自定义的消息发送 +// Send 将自定义的消息发送 func (srv *Server) Send() (err error) { replyMsg := srv.ResponseMsg log.Debugf("response msg =%+v", replyMsg) if srv.isSafeMode { - //安全模式下对消息进行加密 + // 安全模式下对消息进行加密 var encryptedMsg []byte encryptedMsg, err = util.EncryptMsg(srv.random, srv.ResponseRawXMLMsg, srv.AppID, srv.EncodingAESKey) if err != nil { return } - //TODO 如果获取不到timestamp nonce 则自己生成 + // TODO 如果获取不到timestamp nonce 则自己生成 timestamp := srv.timestamp timestampStr := strconv.FormatInt(timestamp, 10) msgSignature := util.Signature(srv.Token, timestampStr, srv.nonce, string(encryptedMsg)) diff --git a/officialaccount/server/util.go b/officialaccount/server/util.go index 6c108fa..5c2ceb2 100644 --- a/officialaccount/server/util.go +++ b/officialaccount/server/util.go @@ -15,10 +15,10 @@ func writeContextType(w http.ResponseWriter, value []string) { } } -//Render render from bytes +// Render render from bytes func (srv *Server) Render(bytes []byte) { - //debug - //fmt.Println("response msg = ", string(bytes)) + // debug + // fmt.Println("response msg = ", string(bytes)) srv.Writer.WriteHeader(200) _, err := srv.Writer.Write(bytes) if err != nil { @@ -26,13 +26,13 @@ func (srv *Server) Render(bytes []byte) { } } -//String render from string +// String render from string func (srv *Server) String(str string) { writeContextType(srv.Writer, plainContentType) srv.Render([]byte(str)) } -//XML render to xml +// XML render to xml func (srv *Server) XML(obj interface{}) { writeContextType(srv.Writer, xmlContentType) bytes, err := xml.Marshal(obj) diff --git a/officialaccount/user/blacklist.go b/officialaccount/user/blacklist.go new file mode 100644 index 0000000..b957b09 --- /dev/null +++ b/officialaccount/user/blacklist.go @@ -0,0 +1,116 @@ +// Package user blacklist 公众号用户黑名单管理 +// 参考文档:https://developers.weixin.qq.com/doc/offiaccount/User_Management/Manage_blacklist.html +package user + +import ( + "errors" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 获取公众号的黑名单列表 + getblacklistURL = "https://api.weixin.qq.com/cgi-bin/tags/members/getblacklist?access_token=%s" + // 拉黑用户 + batchblacklistURL = "https://api.weixin.qq.com/cgi-bin/tags/members/batchblacklist?access_token=%s" + // 取消拉黑用户 + batchunblacklistURL = "https://api.weixin.qq.com/cgi-bin/tags/members/batchunblacklist?access_token=%s" +) + +// GetBlackList 获取公众号的黑名单列表 +// 该接口每次调用最多可拉取 1000 个OpenID,当列表数较多时,可以通过多次拉取的方式来满足需求。 +// 参数 beginOpenid:当 begin_openid 为空时,默认从开头拉取。 +func (user *User) GetBlackList(beginOpenid ...string) (userlist *OpenidList, err error) { + if len(beginOpenid) > 1 { + return nil, errors.New("参数 beginOpenid 错误:请传递 1 个openID,若需要从头开始拉取列表请留空。") + } + // 获取 AccessToken + var accessToken string + if accessToken, err = user.GetAccessToken(); err != nil { + return + } + + // 处理 request 内容 + request := map[string]string{"begin_openid": ""} + if len(beginOpenid) == 1 { + request["begin_openid"] = beginOpenid[0] + } + + // 调用接口 + var resp []byte + url := fmt.Sprintf(getblacklistURL, accessToken) + if resp, err = util.PostJSON(url, &request); err != nil { + return nil, err + } + + // 处理返回 + userlist = &OpenidList{} + if err = util.DecodeWithError(resp, userlist, "GetBlackList"); err != nil { + return nil, err + } + + return +} + +// GetAllBlackList 获取公众号的所有黑名单列表 +func (user *User) GetAllBlackList() (openIDList []string, err error) { + var ( + beginOpenid string + count int + userlist *OpenidList + ) + + for { + // 获取列表(每次1k条) + if userlist, err = user.GetBlackList(beginOpenid); err != nil { + return nil, err + } + openIDList = append(openIDList, userlist.Data.OpenIDs...) // 存储本次获得的OpenIDs + count += userlist.Count // 记录获得的总数量 + beginOpenid = userlist.NextOpenID // 记录下次循环的起始openID + if count >= userlist.Total { + break // 获得的数量=total,结束循环 + } + } + + return +} + +// BatchBlackList 拉黑用户 +// 参数 openidList:需要拉入黑名单的用户的openid,每次拉黑最多允许20个 +func (user *User) BatchBlackList(openidList ...string) (err error) { + return user.batch(batchblacklistURL, "BatchBlackList", openidList...) +} + +// BatchUnBlackList 取消拉黑用户 +// 参数 openidList:需要取消拉入黑名单的用户的openid,每次拉黑最多允许20个 +func (user *User) BatchUnBlackList(openidList ...string) (err error) { + return user.batch(batchunblacklistURL, "BatchUnBlackList", openidList...) +} + +// batch 公共方法 +func (user *User) batch(url, apiName string, openidList ...string) (err error) { + // 检查参数 + if len(openidList) == 0 || len(openidList) > 20 { + return errors.New("参数 openidList 错误:每次操作黑名单用户数量为1-20个。") + } + + // 获取 AccessToken + var accessToken string + if accessToken, err = user.GetAccessToken(); err != nil { + return + } + + // 处理 request 内容 + request := map[string][]string{"openid_list": openidList} + + // 调用接口 + var resp []byte + url = fmt.Sprintf(url, accessToken) + if resp, err = util.PostJSON(url, &request); err != nil { + return + } + + return util.DecodeWithCommonError(resp, apiName) +} diff --git a/officialaccount/user/migrate.go b/officialaccount/user/migrate.go index 97e63f0..44afe05 100644 --- a/officialaccount/user/migrate.go +++ b/officialaccount/user/migrate.go @@ -1,5 +1,5 @@ -//Package user migrate 用于微信公众号账号迁移,获取openID变化 -//参考文档:https://kf.qq.com/faq/1901177NrqMr190117nqYJze.html +// Package user migrate 用于微信公众号账号迁移,获取openID变化 +// 参考文档:https://kf.qq.com/faq/1901177NrqMr190117nqYJze.html package user import ( @@ -32,7 +32,7 @@ type ChangeOpenIDResultList struct { // AccessToken 为新账号的AccessToken func (user *User) ListChangeOpenIDs(fromAppID string, openIDs ...string) (list *ChangeOpenIDResultList, err error) { list = &ChangeOpenIDResultList{} - //list.List = make([]ChangeOpenIDResult, 0) + // list.List = make([]ChangeOpenIDResult, 0) if len(openIDs) > 100 { err = errors.New("openIDs length must be lt 100") return diff --git a/officialaccount/user/tag.go b/officialaccount/user/tag.go index d2f07a1..3a61c5d 100644 --- a/officialaccount/user/tag.go +++ b/officialaccount/user/tag.go @@ -18,7 +18,7 @@ const ( tagUserTidListURL = "https://api.weixin.qq.com/cgi-bin/tags/getidlist?access_token=%s" ) -//TagInfo 标签信息 +// TagInfo 标签信息 type TagInfo struct { ID int32 `json:"id"` Name string `json:"name"` @@ -34,7 +34,7 @@ type TagOpenIDList struct { NextOpenID string `json:"next_openid"` } -//CreateTag 创建标签 +// CreateTag 创建标签 func (user *User) CreateTag(tagName string) (tagInfo *TagInfo, err error) { var accessToken string accessToken, err = user.GetAccessToken() @@ -68,7 +68,7 @@ func (user *User) CreateTag(tagName string) (tagInfo *TagInfo, err error) { return result.Tag, nil } -//DeleteTag 删除标签 +// DeleteTag 删除标签 func (user *User) DeleteTag(tagID int32) (err error) { accessToken, err := user.GetAccessToken() if err != nil { @@ -88,7 +88,7 @@ func (user *User) DeleteTag(tagID int32) (err error) { return util.DecodeWithCommonError(resp, "DeleteTag") } -//UpdateTag 编辑标签 +// UpdateTag 编辑标签 func (user *User) UpdateTag(tagID int32, tagName string) (err error) { accessToken, err := user.GetAccessToken() if err != nil { @@ -110,7 +110,7 @@ func (user *User) UpdateTag(tagID int32, tagName string) (err error) { return util.DecodeWithCommonError(resp, "UpdateTag") } -//GetTag 获取公众号已创建的标签 +// GetTag 获取公众号已创建的标签 func (user *User) GetTag() (tags []*TagInfo, err error) { accessToken, err := user.GetAccessToken() if err != nil { @@ -132,7 +132,7 @@ func (user *User) GetTag() (tags []*TagInfo, err error) { return result.Tags, nil } -//OpenIDListByTag 获取标签下粉丝列表 +// OpenIDListByTag 获取标签下粉丝列表 func (user *User) OpenIDListByTag(tagID int32, nextOpenID ...string) (userList *TagOpenIDList, err error) { accessToken, err := user.GetAccessToken() if err != nil { @@ -160,7 +160,7 @@ func (user *User) OpenIDListByTag(tagID int32, nextOpenID ...string) (userList * return } -//BatchTag 批量为用户打标签 +// BatchTag 批量为用户打标签 func (user *User) BatchTag(openIDList []string, tagID int32) (err error) { accessToken, err := user.GetAccessToken() if err != nil { @@ -184,7 +184,7 @@ func (user *User) BatchTag(openIDList []string, tagID int32) (err error) { return util.DecodeWithCommonError(resp, "BatchTag") } -//BatchUntag 批量为用户取消标签 +// BatchUntag 批量为用户取消标签 func (user *User) BatchUntag(openIDList []string, tagID int32) (err error) { if len(openIDList) == 0 { return @@ -208,7 +208,7 @@ func (user *User) BatchUntag(openIDList []string, tagID int32) (err error) { return util.DecodeWithCommonError(resp, "BatchUntag") } -//UserTidList 获取用户身上的标签列表 +// UserTidList 获取用户身上的标签列表 func (user *User) UserTidList(openID string) (tagIDList []int32, err error) { accessToken, err := user.GetAccessToken() if err != nil { diff --git a/officialaccount/user/user.go b/officialaccount/user/user.go index 8cffa36..fedfcbc 100644 --- a/officialaccount/user/user.go +++ b/officialaccount/user/user.go @@ -2,6 +2,7 @@ package user import ( "encoding/json" + "errors" "fmt" "net/url" @@ -10,27 +11,32 @@ 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 用户管理 +// User 用户管理 type User struct { *context.Context } -//NewUser 实例化 +// NewUser 实例化 func NewUser(context *context.Context) *User { user := new(User) user.Context = context return user } -//Info 用户基本信息 +// Info 用户基本信息 type Info struct { util.CommonError + userInfo +} +// 用户基本信息 +type userInfo struct { Subscribe int32 `json:"subscribe"` OpenID string `json:"openid"` Nickname string `json:"nickname"` @@ -62,7 +68,7 @@ type OpenidList struct { NextOpenID string `json:"next_openid"` } -//GetUserInfo 获取用户基本信息 +// GetUserInfo 获取用户基本信息 func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) { var accessToken string accessToken, err = user.GetAccessToken() @@ -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 diff --git a/openplatform/README.md b/openplatform/README.md index f97340b..a206454 100644 --- a/openplatform/README.md +++ b/openplatform/README.md @@ -1,11 +1,11 @@ # 微信开放平台 - [官方文档](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Third_party_platform_appid.html) ## 快速入门 ### 服务端处理 + ```go wc := wechat.NewWechat() memory := cache.NewMemory() @@ -52,7 +52,9 @@ server.Send() ``` + ### 待授权处理消息 + ```go //授权的第三方公众号的appID diff --git a/openplatform/account/account.go b/openplatform/account/account.go index c91ed9e..29b231c 100644 --- a/openplatform/account/account.go +++ b/openplatform/account/account.go @@ -2,33 +2,33 @@ package account import "github.com/silenceper/wechat/v2/openplatform/context" -//Account 开放平台张哈管理 -//TODO 实现方法 +// Account 开放平台张哈管理 +// TODO 实现方法 type Account struct { *context.Context } -//NewAccount new +// NewAccount new func NewAccount(ctx *context.Context) *Account { return &Account{ctx} } -//Create 创建开放平台帐号并绑定公众号/小程序 +// Create 创建开放平台帐号并绑定公众号/小程序 func (account *Account) Create(appID string) (string, error) { return "", nil } -//Bind 将公众号/小程序绑定到开放平台帐号下 +// Bind 将公众号/小程序绑定到开放平台帐号下 func (account *Account) Bind(appID string) error { return nil } -//Unbind 将公众号/小程序从开放平台帐号下解绑 +// Unbind 将公众号/小程序从开放平台帐号下解绑 func (account *Account) Unbind(appID string, openAppID string) error { return nil } -//Get 获取公众号/小程序所绑定的开放平台帐号 +// Get 获取公众号/小程序所绑定的开放平台帐号 func (account *Account) Get(appID string) (string, error) { return "", nil } diff --git a/openplatform/config/config.go b/openplatform/config/config.go index 1c20d38..98e7c4e 100644 --- a/openplatform/config/config.go +++ b/openplatform/config/config.go @@ -4,11 +4,11 @@ import ( "github.com/silenceper/wechat/v2/cache" ) -//Config config for 微信开放平台 +// Config .config for 微信开放平台 type Config struct { - AppID string `json:"app_id"` //appid - AppSecret string `json:"app_secret"` //appsecret - Token string `json:"token"` //token - EncodingAESKey string `json:"encoding_aes_key"` //EncodingAESKey + AppID string `json:"app_id"` // appid + AppSecret string `json:"app_secret"` // appsecret + Token string `json:"token"` // token + EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey Cache cache.Cache } diff --git a/openplatform/context/accessToken.go b/openplatform/context/accessToken.go index ed41e42..6886129 100644 --- a/openplatform/context/accessToken.go +++ b/openplatform/context/accessToken.go @@ -1,12 +1,14 @@ -//Package context 开放平台相关context +// Package context 开放平台相关context package context import ( + "context" "encoding/json" "fmt" "net/url" "time" + "github.com/silenceper/wechat/v2/cache" "github.com/silenceper/wechat/v2/util" ) @@ -18,36 +20,42 @@ const ( getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s" componentLoginURL = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=%d&biz_appid=%s" bindComponentURL = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect" - //TODO 获取授权方选项信息 - //getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s" - //TODO 获取已授权的账号信息 - //getuthorizerListURL = "POST https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token=%s" + // TODO 获取授权方选项信息 + // getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s" + // TODO 获取已授权的账号信息 + // getuthorizerListURL = "POST https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token=%s" ) // ComponentAccessToken 第三方平台 type ComponentAccessToken struct { + util.CommonError AccessToken string `json:"component_access_token"` ExpiresIn int64 `json:"expires_in"` } -// GetComponentAccessToken 获取 ComponentAccessToken -func (ctx *Context) GetComponentAccessToken() (string, error) { +// GetComponentAccessTokenContext 获取 ComponentAccessToken +func (ctx *Context) GetComponentAccessTokenContext(stdCtx context.Context) (string, error) { accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID) - val := ctx.Cache.Get(accessTokenCacheKey) + val := cache.GetContext(stdCtx, ctx.Cache, accessTokenCacheKey) if val == nil { return "", fmt.Errorf("cann't get component access token") } return val.(string), nil } -// SetComponentAccessToken 通过component_verify_ticket 获取 ComponentAccessToken -func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAccessToken, error) { +// GetComponentAccessToken 获取 ComponentAccessToken +func (ctx *Context) GetComponentAccessToken() (string, error) { + return ctx.GetComponentAccessTokenContext(context.Background()) +} + +// SetComponentAccessTokenContext 通过component_verify_ticket 获取 ComponentAccessToken +func (ctx *Context) SetComponentAccessTokenContext(stdCtx context.Context, verifyTicket string) (*ComponentAccessToken, error) { body := map[string]string{ "component_appid": ctx.AppID, "component_appsecret": ctx.AppSecret, "component_verify_ticket": verifyTicket, } - respBody, err := util.PostJSON(componentAccessTokenURL, body) + respBody, err := util.PostJSONContext(stdCtx, componentAccessTokenURL, body) if err != nil { return nil, err } @@ -57,17 +65,26 @@ func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAcce return nil, err } + if at.ErrCode != 0 { + return nil, fmt.Errorf("SetComponentAccessToken Error , errcode=%d , errmsg=%s", at.ErrCode, at.ErrMsg) + } + accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID) expires := at.ExpiresIn - 1500 - if err := ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second); err != nil { + if err := cache.SetContext(stdCtx, ctx.Cache, accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second); err != nil { return nil, nil } return at, nil } -// GetPreCode 获取预授权码 -func (ctx *Context) GetPreCode() (string, error) { - cat, err := ctx.GetComponentAccessToken() +// SetComponentAccessToken 通过component_verify_ticket 获取 ComponentAccessToken +func (ctx *Context) SetComponentAccessToken(stdCtx context.Context, verifyTicket string) (*ComponentAccessToken, error) { + return ctx.SetComponentAccessTokenContext(stdCtx, verifyTicket) +} + +// GetPreCodeContext 获取预授权码 +func (ctx *Context) GetPreCodeContext(stdCtx context.Context) (string, error) { + cat, err := ctx.GetComponentAccessTokenContext(stdCtx) if err != nil { return "", err } @@ -75,7 +92,7 @@ func (ctx *Context) GetPreCode() (string, error) { "component_appid": ctx.AppID, } uri := fmt.Sprintf(getPreCodeURL, cat) - body, err := util.PostJSON(uri, req) + body, err := util.PostJSONContext(stdCtx, uri, req) if err != nil { return "", err } @@ -90,24 +107,39 @@ func (ctx *Context) GetPreCode() (string, error) { return ret.PreCode, nil } -// GetComponentLoginPage 获取第三方公众号授权链接(扫码授权) -func (ctx *Context) GetComponentLoginPage(redirectURI string, authType int, bizAppID string) (string, error) { - code, err := ctx.GetPreCode() +// GetPreCode 获取预授权码 +func (ctx *Context) GetPreCode() (string, error) { + return ctx.GetPreCodeContext(context.Background()) +} + +// GetComponentLoginPageContext 获取第三方公众号授权链接(扫码授权) +func (ctx *Context) GetComponentLoginPageContext(stdCtx context.Context, redirectURI string, authType int, bizAppID string) (string, error) { + code, err := ctx.GetPreCodeContext(stdCtx) if err != nil { return "", err } return fmt.Sprintf(componentLoginURL, ctx.AppID, code, url.QueryEscape(redirectURI), authType, bizAppID), nil } -// GetBindComponentURL 获取第三方公众号授权链接(链接跳转,适用移动端) -func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizAppID string) (string, error) { - code, err := ctx.GetPreCode() +// GetComponentLoginPage 获取第三方公众号授权链接(扫码授权) +func (ctx *Context) GetComponentLoginPage(redirectURI string, authType int, bizAppID string) (string, error) { + return ctx.GetComponentLoginPageContext(context.Background(), redirectURI, authType, bizAppID) +} + +// GetBindComponentURLContext 获取第三方公众号授权链接(链接跳转,适用移动端) +func (ctx *Context) GetBindComponentURLContext(stdCtx context.Context, redirectURI string, authType int, bizAppID string) (string, error) { + code, err := ctx.GetPreCodeContext(stdCtx) if err != nil { return "", err } return fmt.Sprintf(bindComponentURL, authType, ctx.AppID, code, url.QueryEscape(redirectURI), bizAppID), nil } +// GetBindComponentURL 获取第三方公众号授权链接(链接跳转,适用移动端) +func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizAppID string) (string, error) { + return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID) +} + // ID 微信返回接口中各种类型字段 type ID struct { ID int `json:"id"` @@ -132,9 +164,9 @@ type AuthrAccessToken struct { RefreshToken string `json:"authorizer_refresh_token"` } -// QueryAuthCode 使用授权码换取公众号或小程序的接口调用凭据和授权信息 -func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) { - cat, err := ctx.GetComponentAccessToken() +// QueryAuthCodeContext 使用授权码换取公众号或小程序的接口调用凭据和授权信息 +func (ctx *Context) QueryAuthCodeContext(stdCtx context.Context, authCode string) (*AuthBaseInfo, error) { + cat, err := ctx.GetComponentAccessTokenContext(stdCtx) if err != nil { return nil, err } @@ -144,7 +176,7 @@ func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) { "authorization_code": authCode, } uri := fmt.Sprintf(queryAuthURL, cat) - body, err := util.PostJSON(uri, req) + body, err := util.PostJSONContext(stdCtx, uri, req) if err != nil { return nil, err } @@ -164,9 +196,14 @@ func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) { return ret.Info, nil } -// RefreshAuthrToken 获取(刷新)授权公众号或小程序的接口调用凭据(令牌) -func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessToken, error) { - cat, err := ctx.GetComponentAccessToken() +// QueryAuthCode 使用授权码换取公众号或小程序的接口调用凭据和授权信息 +func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) { + return ctx.QueryAuthCodeContext(context.Background(), authCode) +} + +// RefreshAuthrTokenContext 获取(刷新)授权公众号或小程序的接口调用凭据(令牌) +func (ctx *Context) RefreshAuthrTokenContext(stdCtx context.Context, appid, refreshToken string) (*AuthrAccessToken, error) { + cat, err := ctx.GetComponentAccessTokenContext(stdCtx) if err != nil { return nil, err } @@ -177,7 +214,7 @@ func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessT "authorizer_refresh_token": refreshToken, } uri := fmt.Sprintf(refreshTokenURL, cat) - body, err := util.PostJSON(uri, req) + body, err := util.PostJSONContext(stdCtx, uri, req) if err != nil { return nil, err } @@ -188,22 +225,32 @@ func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessT } authrTokenKey := "authorizer_access_token_" + appid - if err := ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80); err != nil { + if err := cache.SetContext(stdCtx, ctx.Cache, authrTokenKey, ret.AccessToken, time.Second*time.Duration(ret.ExpiresIn-30)); err != nil { return nil, err } return ret, nil } -// GetAuthrAccessToken 获取授权方AccessToken -func (ctx *Context) GetAuthrAccessToken(appid string) (string, error) { +// RefreshAuthrToken 获取(刷新)授权公众号或小程序的接口调用凭据(令牌) +func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessToken, error) { + return ctx.RefreshAuthrTokenContext(context.Background(), appid, refreshToken) +} + +// GetAuthrAccessTokenContext 获取授权方AccessToken +func (ctx *Context) GetAuthrAccessTokenContext(stdCtx context.Context, appid string) (string, error) { authrTokenKey := "authorizer_access_token_" + appid - val := ctx.Cache.Get(authrTokenKey) + val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey) if val == nil { return "", fmt.Errorf("cannot get authorizer %s access token", appid) } return val.(string), nil } +// GetAuthrAccessToken 获取授权方AccessToken +func (ctx *Context) GetAuthrAccessToken(appid string) (string, error) { + return ctx.GetAuthrAccessTokenContext(context.Background(), appid) +} + // AuthorizerInfo 授权方详细信息 type AuthorizerInfo struct { NickName string `json:"nick_name"` @@ -221,11 +268,41 @@ type AuthorizerInfo struct { } Alias string `json:"alias"` QrcodeURL string `json:"qrcode_url"` + + MiniProgramInfo *MiniProgramInfo `json:"MiniProgramInfo"` + RegisterType int `json:"register_type"` + AccountStatus int `json:"account_status"` + BasicConfig *AuthorizerBasicConfig `json:"basic_config"` } -// GetAuthrInfo 获取授权方的帐号基本信息 -func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) { - cat, err := ctx.GetComponentAccessToken() +// AuthorizerBasicConfig 授权账号的基础配置结构体 +type AuthorizerBasicConfig struct { + IsPhoneConfigured bool `json:"isPhoneConfigured"` + IsEmailConfigured bool `json:"isEmailConfigured"` +} + +// MiniProgramInfo 授权账号小程序配置 授权账号为小程序时存在 +type MiniProgramInfo struct { + Network struct { + RequestDomain []string `json:"RequestDomain"` + WsRequestDomain []string `json:"WsRequestDomain"` + UploadDomain []string `json:"UploadDomain"` + DownloadDomain []string `json:"DownloadDomain"` + BizDomain []string `json:"BizDomain"` + UDPDomain []string `json:"UDPDomain"` + } `json:"network"` + Categories []CategoriesInfo `json:"categories"` +} + +// CategoriesInfo 授权账号小程序配置的类目信息 +type CategoriesInfo struct { + First string `wx:"first"` + Second string `wx:"second"` +} + +// GetAuthrInfoContext 获取授权方的帐号基本信息 +func (ctx *Context) GetAuthrInfoContext(stdCtx context.Context, appid string) (*AuthorizerInfo, *AuthBaseInfo, error) { + cat, err := ctx.GetComponentAccessTokenContext(stdCtx) if err != nil { return nil, nil, err } @@ -236,7 +313,7 @@ func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, } uri := fmt.Sprintf(getComponentInfoURL, cat) - body, err := util.PostJSON(uri, req) + body, err := util.PostJSONContext(stdCtx, uri, req) if err != nil { return nil, nil, err } @@ -251,3 +328,8 @@ func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, return ret.AuthorizerInfo, ret.AuthorizationInfo, nil } + +// GetAuthrInfo 获取授权方的帐号基本信息 +func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) { + return ctx.GetAuthrInfoContext(context.Background(), appid) +} diff --git a/openplatform/miniprogram/basic/basic.go b/openplatform/miniprogram/basic/basic.go index 41389f0..58890ab 100644 --- a/openplatform/miniprogram/basic/basic.go +++ b/openplatform/miniprogram/basic/basic.go @@ -11,23 +11,24 @@ const ( getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo" ) -//Basic 基础信息设置 +// Basic 基础信息设置 type Basic struct { *openContext.Context appID string } -//NewBasic new +// NewBasic new func NewBasic(opContext *openContext.Context, appID string) *Basic { return &Basic{Context: opContext, appID: appID} } -//AccountBasicInfo 基础信息 +// AccountBasicInfo 基础信息 type AccountBasicInfo struct { util.CommonError } -//GetAccountBasicInfo 获取小程序基础信息 +// GetAccountBasicInfo 获取小程序基础信息 +// //reference:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Mini_Program_Information_Settings.html func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) { ak, err := basic.GetAuthrAccessToken(basic.AppID) @@ -46,7 +47,7 @@ func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) { return result, nil } -//modify_domain设置服务器域名 -//TODO -//func (encryptor *Basic) modifyDomain() { -//} +// modify_domain设置服务器域名 +// TODO +// func (encryptor *Basic) modifyDomain() { +// } diff --git a/openplatform/miniprogram/component/component.go b/openplatform/miniprogram/component/component.go index 8a5ff22..8fff007 100644 --- a/openplatform/miniprogram/component/component.go +++ b/openplatform/miniprogram/component/component.go @@ -11,32 +11,32 @@ const ( fastregisterweappURL = "https://api.weixin.qq.com/cgi-bin/component/fastregisterweapp" ) -//Component 快速创建小程序 +// Component 快速创建小程序 type Component struct { *openContext.Context } -//NewComponent new +// NewComponent new func NewComponent(opContext *openContext.Context) *Component { return &Component{opContext} } -//RegisterMiniProgramParam 快速注册小程序参数 +// RegisterMiniProgramParam 快速注册小程序参数 type RegisterMiniProgramParam struct { - Name string `json:"name"` //企业名 - Code string `json:"code"` //企业代码 - CodeType string `json:"code_type"` //企业代码类型 1:统一社会信用代码(18 位) 2:组织机构代码(9 位 xxxxxxxx-x) 3:营业执照注册号(15 位) - LegalPersonaWechat string `json:"legal_persona_wechat"` //法人微信号 - LegalPersonaName string `json:"legal_persona_name"` //法人姓名(绑定银行卡) - ComponentPhone string `json:"component_phone"` //第三方联系电话(方便法人与第三方联系) + Name string `json:"name"` // 企业名 + Code string `json:"code"` // 企业代码 + CodeType string `json:"code_type"` // 企业代码类型 1:统一社会信用代码(18 位) 2:组织机构代码(9 位 xxxxxxxx-x) 3:营业执照注册号(15 位) + LegalPersonaWechat string `json:"legal_persona_wechat"` // 法人微信号 + LegalPersonaName string `json:"legal_persona_name"` // 法人姓名(绑定银行卡) + ComponentPhone string `json:"component_phone"` // 第三方联系电话(方便法人与第三方联系) } -//RegisterMiniProgram 快速创建小程 -//reference: https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Fast_Registration_Interface_document.html +// RegisterMiniProgram 快速创建小程 +// reference: https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Fast_Registration_Interface_document.html 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) @@ -46,19 +46,19 @@ func (component *Component) RegisterMiniProgram(param *RegisterMiniProgramParam) return util.DecodeWithCommonError(data, "component/fastregisterweapp?action=create") } -//GetRegistrationStatusParam 查询任务创建状态 +// GetRegistrationStatusParam 查询任务创建状态 type GetRegistrationStatusParam struct { - Name string `json:"name"` //企业名 - LegalPersonaWechat string `json:"legal_persona_wechat"` //法人微信号 - LegalPersonaName string `json:"legal_persona_name"` //法人姓名(绑定银行卡) + Name string `json:"name"` // 企业名 + LegalPersonaWechat string `json:"legal_persona_wechat"` // 法人微信号 + LegalPersonaName string `json:"legal_persona_name"` // 法人姓名(绑定银行卡) } -//GetRegistrationStatus 查询创建任务状态. +// GetRegistrationStatus 查询创建任务状态. 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) diff --git a/openplatform/miniprogram/miniprogram.go b/openplatform/miniprogram/miniprogram.go index 1f02a51..8bececb 100644 --- a/openplatform/miniprogram/miniprogram.go +++ b/openplatform/miniprogram/miniprogram.go @@ -1,32 +1,92 @@ package miniprogram import ( + "fmt" + + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/miniprogram" + miniConfig "github.com/silenceper/wechat/v2/miniprogram/config" + miniContext "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/miniprogram/urllink" openContext "github.com/silenceper/wechat/v2/openplatform/context" "github.com/silenceper/wechat/v2/openplatform/miniprogram/basic" "github.com/silenceper/wechat/v2/openplatform/miniprogram/component" ) -//MiniProgram 代小程序实现业务 +// MiniProgram 代小程序实现业务 type MiniProgram struct { AppID string openContext *openContext.Context + *miniprogram.MiniProgram + authorizerRefreshToken string } -//NewMiniProgram 实例化 -func NewMiniProgram(opCtx *openContext.Context, appID string) *MiniProgram { - return &MiniProgram{ - openContext: opCtx, - AppID: appID, +// GetAccessToken 获取ak +func (miniProgram *MiniProgram) GetAccessToken() (string, error) { + ak, akErr := miniProgram.openContext.GetAuthrAccessToken(miniProgram.AppID) + if akErr == nil { + return ak, nil } + if miniProgram.authorizerRefreshToken == "" { + return "", fmt.Errorf("please set the authorizer_refresh_token first") + } + akRes, akResErr := miniProgram.GetComponent().RefreshAuthrToken(miniProgram.AppID, miniProgram.authorizerRefreshToken) + if akResErr != nil { + return "", akResErr + } + return akRes.AccessToken, nil } -//GetComponent get component -//快速注册小程序相关 +// SetAuthorizerRefreshToken 设置代执操作业务授权账号authorizer_refresh_token +func (miniProgram *MiniProgram) SetAuthorizerRefreshToken(authorizerRefreshToken string) *MiniProgram { + miniProgram.authorizerRefreshToken = authorizerRefreshToken + return miniProgram +} + +// NewMiniProgram 实例化 +func NewMiniProgram(opCtx *openContext.Context, appID string) *MiniProgram { + miniProgram := miniprogram.NewMiniProgram(&miniConfig.Config{ + AppID: opCtx.AppID, + Cache: opCtx.Cache, + }) + // 设置获取access_token的函数 + miniProgram.SetAccessTokenHandle(NewDefaultAuthrAccessToken(opCtx, appID)) + return &MiniProgram{AppID: appID, MiniProgram: miniProgram, openContext: opCtx} +} + +// GetComponent get component +// 快速注册小程序相关 func (miniProgram *MiniProgram) GetComponent() *component.Component { return component.NewComponent(miniProgram.openContext) } -//GetBasic 基础信息设置 +// GetBasic 基础信息设置 func (miniProgram *MiniProgram) GetBasic() *basic.Basic { return basic.NewBasic(miniProgram.openContext, miniProgram.AppID) } + +// GetURLLink 小程序URL Link接口 调用前需确认已调用 SetAuthorizerRefreshToken 避免由于缓存中 authorizer_access_token 过期执行中断 +func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink { + return urllink.NewURLLink(&miniContext.Context{ + AccessTokenHandle: miniProgram, + }) +} + +// DefaultAuthrAccessToken 默认获取授权ak的方法 +type DefaultAuthrAccessToken struct { + opCtx *openContext.Context + appID string +} + +// NewDefaultAuthrAccessToken 设置access_token +func NewDefaultAuthrAccessToken(opCtx *openContext.Context, appID string) credential.AccessTokenHandle { + return &DefaultAuthrAccessToken{ + opCtx: opCtx, + appID: appID, + } +} + +// GetAccessToken 获取ak +func (ak *DefaultAuthrAccessToken) GetAccessToken() (string, error) { + return ak.opCtx.GetAuthrAccessToken(ak.appID) +} diff --git a/openplatform/officialaccount/js/js.go b/openplatform/officialaccount/js/js.go index 160d52f..aca85f5 100644 --- a/openplatform/officialaccount/js/js.go +++ b/openplatform/officialaccount/js/js.go @@ -15,7 +15,7 @@ type Js struct { credential.JsTicketHandle } -//NewJs init +// NewJs init func NewJs(context *context.Context, appID string) *Js { js := new(Js) js.Context = context @@ -24,13 +24,13 @@ func NewJs(context *context.Context, appID string) *Js { return js } -//SetJsTicketHandle 自定义js ticket取值方式 +// SetJsTicketHandle 自定义js ticket取值方式 func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) { js.JsTicketHandle = ticketHandle } -//GetConfig 第三方平台 - 获取jssdk需要的配置参数 -//uri 为当前网页地址 +// GetConfig 第三方平台 - 获取jssdk需要的配置参数 +// uri 为当前网页地址 func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) { config = new(officialJs.Config) var accessToken string diff --git a/openplatform/officialaccount/oauth/oauth.go b/openplatform/officialaccount/oauth/oauth.go index e8cf1fb..f83dcc0 100644 --- a/openplatform/officialaccount/oauth/oauth.go +++ b/openplatform/officialaccount/oauth/oauth.go @@ -28,14 +28,14 @@ func NewOauth(context *context.Context) *Oauth { return auth } -//GetRedirectURL 第三方平台 - 获取跳转的url地址 +// GetRedirectURL 第三方平台 - 获取跳转的url地址 func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state, appID string) (string, error) { - //url encode + // url encode urlStr := url.QueryEscape(redirectURI) return fmt.Sprintf(platformRedirectOauthURL, appID, urlStr, scope, state, oauth.AppID), nil } -//Redirect 第三方平台 - 跳转到网页授权 +// Redirect 第三方平台 - 跳转到网页授权 func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state, appID string) error { location, err := oauth.GetRedirectURL(redirectURI, scope, state, appID) if err != nil { diff --git a/openplatform/officialaccount/officialaccount.go b/openplatform/officialaccount/officialaccount.go index 7729e90..c5fd09b 100644 --- a/openplatform/officialaccount/officialaccount.go +++ b/openplatform/officialaccount/officialaccount.go @@ -9,15 +9,15 @@ import ( "github.com/silenceper/wechat/v2/openplatform/officialaccount/oauth" ) -//OfficialAccount 代公众号实现业务 +// OfficialAccount 代公众号实现业务 type OfficialAccount struct { - //授权的公众号的appID + // 授权的公众号的appID appID string *officialaccount.OfficialAccount } -//NewOfficialAccount 实例化 -//appID :为授权方公众号 APPID,非开放平台第三方平台 APPID +// NewOfficialAccount 实例化 +// appID :为授权方公众号 APPID,非开放平台第三方平台 APPID func NewOfficialAccount(opCtx *opContext.Context, appID string) *OfficialAccount { officialAccount := officialaccount.NewOfficialAccount(&offConfig.Config{ AppID: opCtx.AppID, @@ -25,7 +25,7 @@ func NewOfficialAccount(opCtx *opContext.Context, appID string) *OfficialAccount Token: opCtx.Token, Cache: opCtx.Cache, }) - //设置获取access_token的函数 + // 设置获取access_token的函数 officialAccount.SetAccessTokenHandle(NewDefaultAuthrAccessToken(opCtx, appID)) return &OfficialAccount{appID: appID, OfficialAccount: officialAccount} } @@ -40,13 +40,13 @@ func (officialAccount *OfficialAccount) PlatformJs() *js.Js { return js.NewJs(officialAccount.GetContext(), officialAccount.appID) } -//DefaultAuthrAccessToken 默认获取授权ak的方法 +// DefaultAuthrAccessToken 默认获取授权ak的方法 type DefaultAuthrAccessToken struct { opCtx *opContext.Context appID string } -//NewDefaultAuthrAccessToken New +// NewDefaultAuthrAccessToken New func NewDefaultAuthrAccessToken(opCtx *opContext.Context, appID string) credential.AccessTokenHandle { return &DefaultAuthrAccessToken{ opCtx: opCtx, @@ -54,7 +54,7 @@ func NewDefaultAuthrAccessToken(opCtx *opContext.Context, appID string) credenti } } -//GetAccessToken 获取ak +// GetAccessToken 获取ak func (ak *DefaultAuthrAccessToken) GetAccessToken() (string, error) { return ak.opCtx.GetAuthrAccessToken(ak.appID) } diff --git a/openplatform/openplatform.go b/openplatform/openplatform.go index b0497f7..5c509b9 100644 --- a/openplatform/openplatform.go +++ b/openplatform/openplatform.go @@ -11,40 +11,37 @@ import ( "github.com/silenceper/wechat/v2/openplatform/officialaccount" ) -//OpenPlatform 微信开放平台相关api +// OpenPlatform 微信开放平台相关api type OpenPlatform struct { *context.Context } -//NewOpenPlatform new openplatform +// NewOpenPlatform new openplatform func NewOpenPlatform(cfg *config.Config) *OpenPlatform { - if cfg.Cache == nil { - panic("cache 未设置") - } ctx := &context.Context{ Config: cfg, } return &OpenPlatform{ctx} } -//GetServer get server +// GetServer get server func (openPlatform *OpenPlatform) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server { off := officialaccount.NewOfficialAccount(openPlatform.Context, "") return off.GetServer(req, writer) } -//GetOfficialAccount 公众号代处理 +// GetOfficialAccount 公众号代处理 func (openPlatform *OpenPlatform) GetOfficialAccount(appID string) *officialaccount.OfficialAccount { return officialaccount.NewOfficialAccount(openPlatform.Context, appID) } -//GetMiniProgram 小程序代理 +// GetMiniProgram 小程序代理 func (openPlatform *OpenPlatform) GetMiniProgram(appID string) *miniprogram.MiniProgram { return miniprogram.NewMiniProgram(openPlatform.Context, appID) } -//GetAccountManager 账号管理 -//TODO +// GetAccountManager 账号管理 +// TODO func (openPlatform *OpenPlatform) GetAccountManager() *account.Account { return account.NewAccount(openPlatform.Context) } diff --git a/pay/config/config.go b/pay/config/config.go index f5a31e4..b3603cc 100644 --- a/pay/config/config.go +++ b/pay/config/config.go @@ -1,6 +1,6 @@ package config -// Config config for pay +// Config .config for pay type Config struct { AppID string `json:"app_id"` MchID string `json:"mch_id"` diff --git a/pay/notify/notify.go b/pay/notify/notify.go index 4630996..81b1520 100644 --- a/pay/notify/notify.go +++ b/pay/notify/notify.go @@ -4,12 +4,12 @@ import ( "github.com/silenceper/wechat/v2/pay/config" ) -//Notify 回调 +// Notify 回调 type Notify struct { *config.Config } -//NewNotify new +// NewNotify new func NewNotify(cfg *config.Config) *Notify { return &Notify{cfg} } diff --git a/pay/notify/paid.go b/pay/notify/paid.go index 63755fc..451e2e1 100644 --- a/pay/notify/paid.go +++ b/pay/notify/paid.go @@ -7,8 +7,9 @@ import ( "strings" "github.com/fatih/structs" - "github.com/silenceper/wechat/v2/util" "github.com/spf13/cast" + + "github.com/silenceper/wechat/v2/util" ) // doc: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8 @@ -30,6 +31,7 @@ type PaidResult struct { OpenID *string `xml:"openid"` IsSubscribe *string `xml:"is_subscribe"` TradeType *string `xml:"trade_type"` + TradeState *string `xml:"trade_state"` BankType *string `xml:"bank_type"` TotalFee *int `xml:"total_fee"` SettlementTotalFee *int `xml:"settlement_total_fee"` @@ -46,9 +48,9 @@ type PaidResult struct { CouponID0 *string `xml:"coupon_id_0"` CouponID1 *string `xml:"coupon_id_1"` CouponID2 *string `xml:"coupon_id_2"` - CouponFeed0 *string `xml:"coupon_fee_0"` - CouponFeed1 *string `xml:"coupon_fee_1"` - CouponFeed2 *string `xml:"coupon_fee_2"` + CouponFee0 *string `xml:"coupon_fee_0"` + CouponFee1 *string `xml:"coupon_fee_1"` + CouponFee2 *string `xml:"coupon_fee_2"` TransactionID *string `xml:"transaction_id"` OutTradeNo *string `xml:"out_trade_no"` diff --git a/pay/notify/refund.go b/pay/notify/refund.go index af04ca4..be550d5 100644 --- a/pay/notify/refund.go +++ b/pay/notify/refund.go @@ -35,7 +35,7 @@ type RefundedReqInfo struct { SettlementRefundFee *int `xml:"settlement_refund_fee"` RefundStatus *string `xml:"refund_status"` SuccessTime *string `xml:"success_time"` - RefundRecvAccount *string `xml:"refund_recv_account"` + RefundRecvAccount *string `xml:"refund_recv_accout"` RefundAccount *string `xml:"refund_account"` RefundRequestSource *string `xml:"refund_request_source"` } @@ -48,7 +48,6 @@ type RefundedResp struct { // DecryptReqInfo 对退款结果进行解密 func (notify *Notify) DecryptReqInfo(result *RefundedResult) (*RefundedReqInfo, error) { - var err error if result == nil || result.ReqInfo == nil { return nil, errors.New("empty refunded_result or req_info") } diff --git a/pay/order/close.go b/pay/order/close.go new file mode 100644 index 0000000..b8976a5 --- /dev/null +++ b/pay/order/close.go @@ -0,0 +1,98 @@ +package order + +import ( + "encoding/xml" + "errors" + + "github.com/silenceper/wechat/v2/util" +) + +// https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3 +var closeGateway = "https://api.mch.weixin.qq.com/pay/closeorder" + +// CloseParams 传入的参数 +type CloseParams struct { + OutTradeNo string // 商户订单号 + SignType string // 签名类型 +} + +// closeRequest 接口请求参数 +type closeRequest struct { + AppID string `xml:"appid"` // 公众账号ID + MchID string `xml:"mch_id"` // 商户号 + NonceStr string `xml:"nonce_str"` // 随机字符串 + Sign string `xml:"sign"` // 签名 + SignType string `xml:"sign_type,omitempty"` // 签名类型 + OutTradeNo string `xml:"out_trade_no"` // 商户订单号 +} + +// CloseResult 关闭订单返回结果 +type CloseResult struct { + ReturnCode *string `xml:"return_code"` + ReturnMsg *string `xml:"return_msg"` + + AppID *string `xml:"appid" json:"appid"` + MchID *string `xml:"mch_id"` + NonceStr *string `xml:"nonce_str"` + Sign *string `xml:"sign"` + ResultCode *string `xml:"result_code"` + ResultMsg *string `xml:"result_msg"` + ErrCode *string `xml:"err_code"` + ErrCodeDes *string `xml:"err_code_des"` +} + +// CloseOrder 关闭订单 +func (o *Order) CloseOrder(p *CloseParams) (closeResult CloseResult, err error) { + nonceStr := util.RandomStr(32) + // 签名类型 + if p.SignType == "" { + p.SignType = "MD5" + } + + params := make(map[string]string) + params["appid"] = o.AppID + params["mch_id"] = o.MchID + params["nonce_str"] = nonceStr + params["out_trade_no"] = p.OutTradeNo + params["sign_type"] = p.SignType + + var ( + sign string + rawRet []byte + ) + + sign, err = util.ParamSign(params, o.Key) + if err != nil { + return + } + request := closeRequest{ + AppID: o.AppID, + MchID: o.MchID, + NonceStr: nonceStr, + Sign: sign, + OutTradeNo: p.OutTradeNo, + SignType: p.SignType, + } + + rawRet, err = util.PostXML(closeGateway, request) + if err != nil { + return + } + + err = xml.Unmarshal(rawRet, &closeResult) + if err != nil { + return + } + + if *closeResult.ReturnCode == SUCCESS { + // close success + if *closeResult.ResultCode == SUCCESS { + err = nil + return + } + err = errors.New(*closeResult.ErrCode + *closeResult.ErrCodeDes) + return + } + err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [sign : " + sign + "]") + return +} diff --git a/pay/order/pay.go b/pay/order/pay.go index f8fcfc0..5cf896a 100644 --- a/pay/order/pay.go +++ b/pay/order/pay.go @@ -35,6 +35,7 @@ type Params struct { CreateIP string Body string OutTradeNo string + TimeExpire string // 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。 OpenID string TradeType string SignType string @@ -54,6 +55,17 @@ type Config struct { PaySign string `json:"paySign"` } +// ConfigForApp 是传出用于 app sdk 用的参数 +type ConfigForApp struct { + AppID string `json:"appid"` + MchID string `json:"partnerid"` // 微信支付分配的商户号 + PrePayID string `json:"prepayid"` + Package string `json:"package"` + NonceStr string `json:"nonceStr"` + Timestamp string `json:"timestamp"` + Sign string `json:"sign"` +} + // PreOrder 是 Unified order 接口的返回 type PreOrder struct { ReturnCode string `xml:"return_code"` @@ -99,6 +111,27 @@ type payRequest struct { XMLName struct{} `xml:"xml"` } +func (req *payRequest) BridgePayRequest(p *Params, AppID, MchID, nonceStr, sign string) *payRequest { + request := payRequest{ + AppID: AppID, + MchID: MchID, + NonceStr: nonceStr, + Sign: sign, + Body: p.Body, + OutTradeNo: p.OutTradeNo, + TotalFee: p.TotalFee, + SpbillCreateIP: p.CreateIP, + NotifyURL: p.NotifyURL, + TradeType: p.TradeType, + OpenID: p.OpenID, + SignType: p.SignType, + Detail: p.Detail, + Attach: p.Attach, + GoodsTag: p.GoodsTag, + } + return &request +} + // BridgeConfig get js bridge config func (o *Order) BridgeConfig(p *Params) (cfg Config, err error) { var ( @@ -136,54 +169,88 @@ func (o *Order) BridgeConfig(p *Params) (cfg Config, err error) { return } +// BridgeAppConfig get app bridge config +func (o *Order) BridgeAppConfig(p *Params) (cfg ConfigForApp, err error) { + var ( + timestamp = strconv.FormatInt(util.GetCurrTS(), 10) + noncestr = util.RandomStr(32) + _package = "Sign=WXPay" + ) + order, err := o.PrePayOrder(p) + if err != nil { + return + } + + result := map[string]string{ + "appid": order.AppID, + "partnerid": order.MchID, + "prepayid": order.PrePayID, + "package": _package, + "noncestr": noncestr, + "timestamp": timestamp, + } + // 签名 + sign, err := util.ParamSign(result, o.Key) + if err != nil { + return + } + result["sign"] = sign + cfg = ConfigForApp{ + AppID: result["appid"], + MchID: result["partnerid"], + PrePayID: result["prepayid"], + Package: result["package"], + NonceStr: result["noncestr"], + Timestamp: result["timestamp"], + Sign: result["sign"], + } + return +} + // PrePayOrder return data for invoke wechat payment func (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) { nonceStr := util.RandomStr(32) - notifyURL := o.NotifyURL + + // 通知地址 + if len(p.NotifyURL) == 0 { + p.NotifyURL = o.NotifyURL // 默认使用order.NotifyURL + } + // 签名类型 if p.SignType == "" { p.SignType = util.SignTypeMD5 } - // 通知地址 - if p.NotifyURL != "" { - notifyURL = p.NotifyURL + + param := map[string]string{ + "appid": o.AppID, + "body": p.Body, + "mch_id": o.MchID, + "nonce_str": nonceStr, + "out_trade_no": p.OutTradeNo, + "spbill_create_ip": p.CreateIP, + "total_fee": p.TotalFee, + "trade_type": p.TradeType, + "openid": p.OpenID, + "sign_type": p.SignType, + "detail": p.Detail, + "attach": p.Attach, + "goods_tag": p.GoodsTag, + "notify_url": p.NotifyURL, + } + + if p.TimeExpire != "" { + // 如果有传入交易结束时间 + param["time_expire"] = p.TimeExpire } - param := make(map[string]string) - param["appid"] = o.AppID - param["body"] = p.Body - param["mch_id"] = o.MchID - param["nonce_str"] = nonceStr - param["out_trade_no"] = p.OutTradeNo - param["spbill_create_ip"] = p.CreateIP - param["total_fee"] = p.TotalFee - param["trade_type"] = p.TradeType - param["openid"] = p.OpenID - param["sign_type"] = p.SignType - param["detail"] = p.Detail - param["attach"] = p.Attach - param["goods_tag"] = p.GoodsTag - param["notify_url"] = notifyURL sign, err := util.ParamSign(param, o.Key) if err != nil { return } - request := payRequest{ - AppID: o.AppID, - MchID: o.MchID, - NonceStr: nonceStr, - Sign: sign, - Body: p.Body, - OutTradeNo: p.OutTradeNo, - TotalFee: p.TotalFee, - SpbillCreateIP: p.CreateIP, - NotifyURL: notifyURL, - TradeType: p.TradeType, - OpenID: p.OpenID, - SignType: p.SignType, - Detail: p.Detail, - Attach: p.Attach, - GoodsTag: p.GoodsTag, + request := new(payRequest).BridgePayRequest(p, o.AppID, o.MchID, nonceStr, sign) + if len(p.TimeExpire) > 0 { + // 如果有传入交易结束时间 + request.TimeExpire = p.TimeExpire } rawRet, err := util.PostXML(payGateway, request) if err != nil { @@ -206,7 +273,7 @@ func (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) { return } -// PrePayID will request wechat merchant api and request for a pre payment order id +// PrePayID will request wechat merchant api and request for a pre-payment order id func (o *Order) PrePayID(p *Params) (prePayID string, err error) { order, err := o.PrePayOrder(p) if err != nil { diff --git a/pay/pay.go b/pay/pay.go index bb43557..95416c2 100644 --- a/pay/pay.go +++ b/pay/pay.go @@ -4,16 +4,17 @@ import ( "github.com/silenceper/wechat/v2/pay/config" "github.com/silenceper/wechat/v2/pay/notify" "github.com/silenceper/wechat/v2/pay/order" + "github.com/silenceper/wechat/v2/pay/redpacket" "github.com/silenceper/wechat/v2/pay/refund" "github.com/silenceper/wechat/v2/pay/transfer" ) -//Pay 微信支付相关API +// Pay 微信支付相关API type Pay struct { cfg *config.Config } -//NewPay 实例化微信支付相关API +// NewPay 实例化微信支付相关API func NewPay(cfg *config.Config) *Pay { return &Pay{cfg} } @@ -23,12 +24,12 @@ func (pay *Pay) GetOrder() *order.Order { return order.NewOrder(pay.cfg) } -// GetNotify 通知 +// GetNotify 通知 func (pay *Pay) GetNotify() *notify.Notify { return notify.NewNotify(pay.cfg) } -// GetRefund 退款 +// GetRefund 退款 func (pay *Pay) GetRefund() *refund.Refund { return refund.NewRefund(pay.cfg) } @@ -37,3 +38,8 @@ func (pay *Pay) GetRefund() *refund.Refund { func (pay *Pay) GetTransfer() *transfer.Transfer { return transfer.NewTransfer(pay.cfg) } + +// GetRedpacket 红包 +func (pay *Pay) GetRedpacket() *redpacket.Redpacket { + return redpacket.NewRedpacket(pay.cfg) +} diff --git a/pay/redpacket/redpacket.go b/pay/redpacket/redpacket.go new file mode 100644 index 0000000..ff1a397 --- /dev/null +++ b/pay/redpacket/redpacket.go @@ -0,0 +1,131 @@ +package redpacket + +import ( + "encoding/xml" + "fmt" + "strconv" + + "github.com/silenceper/wechat/v2/pay/config" + "github.com/silenceper/wechat/v2/util" +) + +// redpacketGateway 发放红包接口 +// https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3 +var redpacketGateway = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack" + +// Redpacket struct extends context +type Redpacket struct { + *config.Config +} + +// NewRedpacket return an instance of Redpacket package +func NewRedpacket(cfg *config.Config) *Redpacket { + return &Redpacket{cfg} +} + +// Params 调用参数 +type Params struct { + MchBillno string // 商户订单号 + SendName string // 商户名称 + ReOpenID string + TotalAmount int + TotalNum int + Wishing string + ClientIP string + ActName string + Remark string + + RootCa string // ca证书 +} + +// request 接口请求参数 +type request struct { + NonceStr string `xml:"nonce_str"` + Sign string `xml:"sign"` + MchID string `xml:"mch_id"` + MchBillno string `xml:"mch_billno"` + Wxappid string `xml:"wxappid"` + SendName string `xml:"send_name"` + ReOpenID string `xml:"re_openid"` + TotalAmount int `xml:"total_amount"` + TotalNum int `xml:"total_num"` + Wishing string `xml:"wishing"` + ClientIP string `xml:"client_ip"` + ActName string `xml:"act_name"` + Remark string `xml:"remark"` +} + +// Response 接口返回 +type Response struct { + ReturnCode string `xml:"return_code"` + ReturnMsg string `xml:"return_msg"` + ResultCode string `xml:"result_code,omitempty"` + ErrCode string `xml:"err_code,omitempty"` + ErrCodeDes string `xml:"err_code_des,omitempty"` + MchBillno string `xml:"mch_billno,omitempty"` + MchID string `xml:"mch_id,omitempty"` + Wxappid string `xml:"wxappid"` + ReOpenID string `xml:"re_openid"` + TotalAmount int `xml:"total_amount"` + SendListid string `xml:"send_listid"` +} + +// SendRedpacket 发放红包 +func (redpacket *Redpacket) SendRedpacket(p *Params) (rsp *Response, err error) { + nonceStr := util.RandomStr(32) + param := make(map[string]string) + + param["nonce_str"] = nonceStr + param["mch_id"] = redpacket.MchID + param["wxappid"] = redpacket.AppID + param["mch_billno"] = p.MchBillno + param["send_name"] = p.SendName + param["re_openid"] = p.ReOpenID + param["total_amount"] = strconv.Itoa(p.TotalAmount) + param["total_num"] = strconv.Itoa(p.TotalNum) + param["wishing"] = p.Wishing + param["client_ip"] = p.ClientIP + param["act_name"] = p.ActName + param["remark"] = p.Remark + //param["scene_id"] = "PRODUCT_2" + + sign, err := util.ParamSign(param, redpacket.Key) + if err != nil { + return + } + + req := request{ + NonceStr: nonceStr, + Sign: sign, + MchID: redpacket.MchID, + Wxappid: redpacket.AppID, + MchBillno: p.MchBillno, + SendName: p.SendName, + ReOpenID: p.ReOpenID, + TotalAmount: p.TotalAmount, + TotalNum: p.TotalNum, + Wishing: p.Wishing, + ClientIP: p.ClientIP, + ActName: p.ActName, + Remark: p.Remark, + } + + rawRet, err := util.PostXMLWithTLS(redpacketGateway, req, p.RootCa, redpacket.MchID) + if err != nil { + return + } + err = xml.Unmarshal(rawRet, &rsp) + if err != nil { + return + } + if rsp.ReturnCode == "SUCCESS" { + if rsp.ResultCode == "SUCCESS" { + err = nil + return + } + err = fmt.Errorf("send redpacket error, errcode=%s,errmsg=%s", rsp.ErrCode, rsp.ErrCodeDes) + return + } + err = fmt.Errorf("[msg : xmlUnmarshalError] [rawReturn : %s] [sign : %s]", string(rawRet), sign) + return +} diff --git a/pay/transfer/transfer_wallet.go b/pay/transfer/transfer_wallet.go index b6dcc91..73fe311 100644 --- a/pay/transfer/transfer_wallet.go +++ b/pay/transfer/transfer_wallet.go @@ -9,7 +9,7 @@ import ( "github.com/silenceper/wechat/v2/util" ) -// 付款到零钱 +// walletTransferGateway 付款到零钱 // https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 var walletTransferGateway = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers" @@ -20,11 +20,10 @@ type Transfer struct { // NewTransfer return an instance of Transfer package func NewTransfer(cfg *config.Config) *Transfer { - transfer := Transfer{cfg} - return &transfer + return &Transfer{cfg} } -//Params 调用参数 +// Params 调用参数 type Params struct { DeviceInfo string PartnerTradeNo string @@ -34,10 +33,10 @@ type Params struct { Amount int Desc string SpbillCreateIP string - RootCa string //ca证书 + RootCa string // ca证书 } -//request 接口请求参数 +// request 接口请求参数 type request struct { AppID string `xml:"mch_appid"` MchID string `xml:"mchid"` @@ -53,7 +52,7 @@ type request struct { SpbillCreateIP string `xml:"spbill_create_ip,omitempty"` } -//Response 接口返回 +// Response 接口返回 type Response struct { ReturnCode string `xml:"return_code"` ReturnMsg string `xml:"return_msg"` @@ -69,8 +68,8 @@ type Response struct { PaymentTime string `xml:"payment_time"` } -//WalletTransfer 付款到零钱 -func (transfer *Transfer) WalletTransfer(p *Params) (rsp Response, err error) { +// WalletTransfer 付款到零钱 +func (transfer *Transfer) WalletTransfer(p *Params) (rsp *Response, err error) { nonceStr := util.RandomStr(32) param := make(map[string]string) param["mch_appid"] = transfer.AppID @@ -83,11 +82,10 @@ func (transfer *Transfer) WalletTransfer(p *Params) (rsp Response, err error) { if p.DeviceInfo != "" { param["device_info"] = p.DeviceInfo } + param["check_name"] = "NO_CHECK" if p.CheckName { param["check_name"] = "FORCE_CHECK" param["re_user_name"] = p.ReUserName - } else { - param["check_name"] = "NO_CHECK" } if p.SpbillCreateIP != "" { param["spbill_create_ip"] = p.SpbillCreateIP @@ -110,13 +108,11 @@ func (transfer *Transfer) WalletTransfer(p *Params) (rsp Response, err error) { Desc: p.Desc, SpbillCreateIP: p.SpbillCreateIP, } + req.CheckName = "NO_CHECK" if p.CheckName { req.CheckName = "FORCE_CHECK" req.ReUserName = p.ReUserName - } else { - req.CheckName = "NO_CHECK" } - rawRet, err := util.PostXMLWithTLS(walletTransferGateway, req, p.RootCa, transfer.MchID) if err != nil { return diff --git a/util/crypto.go b/util/crypto.go index 1c98d73..815b705 100644 --- a/util/crypto.go +++ b/util/crypto.go @@ -40,7 +40,7 @@ func EncryptMsg(random, rawXMLMsg []byte, appID, aesKey string) (encrtptMsg []by } // AESEncryptMsg ciphertext = AES_Encrypt[random(16B) + msg_len(4B) + rawXMLMsg + appId] -//参考:github.com/chanxuehong/wechat.v2 +// 参考:github.com/chanxuehong/wechat.v2 func AESEncryptMsg(random, rawXMLMsg []byte, appID string, aesKey []byte) (ciphertext []byte) { const ( BlockSize = 32 // PKCS#7 @@ -123,7 +123,7 @@ func aesKeyDecode(encodedAESKey string) (key []byte, err error) { } // AESDecryptMsg ciphertext = AES_Encrypt[random(16B) + msg_len(4B) + rawXMLMsg + appId] -//参考:github.com/chanxuehong/wechat.v2 +// 参考:github.com/chanxuehong/wechat.v2 func AESDecryptMsg(ciphertext []byte, aesKey []byte) (random, rawXMLMsg, appID []byte, err error) { const ( BlockSize = 32 // PKCS#7 diff --git a/util/error.go b/util/error.go index b971c47..72b772f 100644 --- a/util/error.go +++ b/util/error.go @@ -6,21 +6,36 @@ import ( "reflect" ) -// CommonError 微信返回的通用错误json +// CommonError 微信返回的通用错误 json type CommonError struct { + apiName string ErrCode int64 `json:"errcode"` ErrMsg string `json:"errmsg"` } -// DecodeWithCommonError 将返回值按照CommonError解析 +func (c *CommonError) Error() string { + return fmt.Sprintf("%s Error , errcode=%d , errmsg=%s", c.apiName, c.ErrCode, c.ErrMsg) +} + +// NewCommonError 新建 CommonError 错误,对于无 errcode 和 errmsg 的返回也可以返回该通用错误 +func NewCommonError(apiName string, code int64, msg string) *CommonError { + return &CommonError{ + apiName: apiName, + ErrCode: code, + ErrMsg: msg, + } +} + +// DecodeWithCommonError 将返回值按照 CommonError 解析 func DecodeWithCommonError(response []byte, apiName string) (err error) { var commError CommonError err = json.Unmarshal(response, &commError) if err != nil { return } + commError.apiName = apiName if commError.ErrCode != 0 { - return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, commError.ErrCode, commError.ErrMsg) + return &commError } return nil } @@ -45,7 +60,11 @@ func DecodeWithError(response []byte, obj interface{}, apiName string) error { return fmt.Errorf("errcode or errmsg is invalid") } if errCode.Int() != 0 { - return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, errCode.Int(), errMsg.String()) + return &CommonError{ + apiName: apiName, + ErrCode: errCode.Int(), + ErrMsg: errMsg.String(), + } } return nil } diff --git a/util/error_test.go b/util/error_test.go new file mode 100644 index 0000000..50fc996 --- /dev/null +++ b/util/error_test.go @@ -0,0 +1,55 @@ +package util + +import "testing" + +var okErrData string = `{"errcode": 0}` +var errData string = `{"errcode": 43101, "errmsg": "user refuse to accept the msg"}` +var expectError string = "Send Error , errcode=43101 , errmsg=user refuse to accept the msg" + +func TestDecodeWithCommonErrorNoError(t *testing.T) { + err := DecodeWithCommonError([]byte(okErrData), "Send") + if err != nil { + t.Error("DecodeWithCommonError should not return error") + return + } +} + +func TestDecodeWithCommonError(t *testing.T) { + err := DecodeWithCommonError([]byte(errData), "Send") + if err == nil { + t.Error("DecodeWithCommonError should return error") + return + } + + cErr, ok := err.(*CommonError) + if !ok { + t.Errorf("DecodeWithCommonError should return *CommonError but %T", err) + return + } + if !(cErr.ErrCode == 43101 && cErr.ErrMsg == "user refuse to accept the msg" && cErr.Error() == expectError) { + t.Error("DecodeWithCommonError return bad *CommonError") + return + } +} + +func TestDecodeWithError(t *testing.T) { + type DE struct { + CommonError + } + var obj DE + err := DecodeWithError([]byte(errData), &obj, "Send") + if err == nil { + t.Error("DecodeWithError should return error") + return + } + + cErr, ok := err.(*CommonError) + if !ok { + t.Errorf("DecodeWithError should return *CommonError but %T", err) + return + } + if !(cErr.ErrCode == 43101 && cErr.ErrMsg == "user refuse to accept the msg" && cErr.Error() == expectError) { + t.Error("DecodeWithError return bad *CommonError") + return + } +} diff --git a/util/http.go b/util/http.go index dd2788e..46dcb8f 100644 --- a/util/http.go +++ b/util/http.go @@ -2,13 +2,13 @@ package util import ( "bytes" + "context" "crypto/tls" "encoding/json" "encoding/pem" "encoding/xml" "fmt" "io" - "io/ioutil" "log" "mime/multipart" "net/http" @@ -17,9 +17,18 @@ import ( "golang.org/x/crypto/pkcs12" ) -//HTTPGet get 请求 +// HTTPGet get 请求 func HTTPGet(uri string) ([]byte, error) { - response, err := http.Get(uri) + return HTTPGetContext(context.Background(), uri) +} + +// HTTPGetContext get 请求 +func HTTPGetContext(ctx context.Context, uri string) ([]byte, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + response, err := http.DefaultClient.Do(request) if err != nil { return nil, err } @@ -28,26 +37,40 @@ func HTTPGet(uri string) ([]byte, error) { if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) } - return ioutil.ReadAll(response.Body) + return io.ReadAll(response.Body) } -//HTTPPost post 请求 +// HTTPPost post 请求 func HTTPPost(uri string, data string) ([]byte, error) { - body := bytes.NewBuffer([]byte(data)) - response, err := http.Post(uri, "", body) + return HTTPPostContext(context.Background(), uri, []byte(data), nil) +} + +// HTTPPostContext post 请求 +func HTTPPostContext(ctx context.Context, uri string, data []byte, header map[string]string) ([]byte, error) { + body := bytes.NewBuffer(data) + request, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, body) + if err != nil { + return nil, err + } + + for key, value := range header { + request.Header.Set(key, value) + } + + response, err := http.DefaultClient.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { - return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) + return nil, fmt.Errorf("http post error : uri=%v , statusCode=%v", uri, response.StatusCode) } - return ioutil.ReadAll(response.Body) + return io.ReadAll(response.Body) } -//PostJSON post json 数据请求 -func PostJSON(uri string, obj interface{}) ([]byte, error) { +// PostJSONContext post json 数据请求 +func PostJSONContext(ctx context.Context, uri string, obj interface{}) ([]byte, error) { jsonBuf := new(bytes.Buffer) enc := json.NewEncoder(jsonBuf) enc.SetEscapeHTML(false) @@ -55,7 +78,12 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) { if err != nil { return nil, err } - response, err := http.Post(uri, "application/json;charset=utf-8", jsonBuf) + req, err := http.NewRequestWithContext(ctx, "POST", uri, jsonBuf) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json;charset=utf-8") + response, err := http.DefaultClient.Do(req) if err != nil { return nil, err } @@ -64,10 +92,15 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) { if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) } - return ioutil.ReadAll(response.Body) + return io.ReadAll(response.Body) } -// PostJSONWithRespContentType post json数据请求,且返回数据类型 +// PostJSON post json 数据请求 +func PostJSON(uri string, obj interface{}) ([]byte, error) { + return PostJSONContext(context.Background(), uri, obj) +} + +// PostJSONWithRespContentType post json 数据请求,且返回数据类型 func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, error) { jsonBuf := new(bytes.Buffer) enc := json.NewEncoder(jsonBuf) @@ -86,24 +119,24 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e if response.StatusCode != http.StatusOK { return nil, "", fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) } - responseData, err := ioutil.ReadAll(response.Body) + responseData, err := io.ReadAll(response.Body) contentType := response.Header.Get("Content-Type") return responseData, contentType, err } -//PostFile 上传文件 -func PostFile(fieldname, filename, uri string) ([]byte, error) { +// PostFile 上传文件 +func PostFile(fieldName, filename, uri string) ([]byte, error) { fields := []MultipartFormField{ { IsFile: true, - Fieldname: fieldname, + Fieldname: fieldName, Filename: filename, }, } return PostMultipartForm(fields, uri) } -//MultipartFormField 保存文件或其他字段信息 +// MultipartFormField 保存文件或其他字段信息 type MultipartFormField struct { IsFile bool Fieldname string @@ -111,7 +144,7 @@ type MultipartFormField struct { Filename string } -//PostMultipartForm 上传文件或其他多个字段 +// PostMultipartForm 上传文件或其他多个字段 func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte, err error) { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) @@ -159,11 +192,11 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte if resp.StatusCode != http.StatusOK { return nil, err } - respBody, err = ioutil.ReadAll(resp.Body) + respBody, err = io.ReadAll(resp.Body) return } -//PostXML perform a HTTP/POST request with XML body +// PostXML perform a HTTP/POST request with XML body func PostXML(uri string, obj interface{}) ([]byte, error) { xmlData, err := xml.Marshal(obj) if err != nil { @@ -180,13 +213,13 @@ func PostXML(uri string, obj interface{}) ([]byte, error) { if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode) } - return ioutil.ReadAll(response.Body) + return io.ReadAll(response.Body) } -//httpWithTLS CA证书 +// httpWithTLS CA 证书 func httpWithTLS(rootCa, key string) (*http.Client, error) { var client *http.Client - certData, err := ioutil.ReadFile(rootCa) + certData, err := os.ReadFile(rootCa) if err != nil { return nil, fmt.Errorf("unable to find cert path=%s, error=%v", rootCa, err) } @@ -202,7 +235,7 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) { return client, nil } -//pkcs12ToPem 将Pkcs12转成Pem +// pkcs12ToPem 将 Pkcs12 转成 Pem func pkcs12ToPem(p12 []byte, password string) tls.Certificate { blocks, err := pkcs12.ToPEM(p12, password) defer func() { @@ -224,7 +257,7 @@ func pkcs12ToPem(p12 []byte, password string) tls.Certificate { return cert } -//PostXMLWithTLS perform a HTTP/POST request with XML body and TLS +// PostXMLWithTLS perform a HTTP/POST request with XML body and TLS func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { xmlData, err := xml.Marshal(obj) if err != nil { @@ -245,5 +278,5 @@ func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode) } - return ioutil.ReadAll(response.Body) + return io.ReadAll(response.Body) } diff --git a/util/query.go b/util/query.go new file mode 100644 index 0000000..008c0cf --- /dev/null +++ b/util/query.go @@ -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, "&") +} diff --git a/util/query_test.go b/util/query_test.go new file mode 100644 index 0000000..8d4841a --- /dev/null +++ b/util/query_test.go @@ -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") + } +} diff --git a/util/rsa.go b/util/rsa.go new file mode 100644 index 0000000..528ed14 --- /dev/null +++ b/util/rsa.go @@ -0,0 +1,43 @@ +package util + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" +) + +// RSADecrypt 数据解密 +func RSADecrypt(privateKey string, ciphertext []byte) ([]byte, error) { + block, _ := pem.Decode([]byte(privateKey)) + if block == nil { + return nil, errors.New("PrivateKey format error") + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + oldErr := err + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("ParsePKCS1PrivateKey error: %s, ParsePKCS8PrivateKey error: %s", oldErr.Error(), err.Error()) + } + switch t := key.(type) { + case *rsa.PrivateKey: + priv = key.(*rsa.PrivateKey) + default: + return nil, fmt.Errorf("ParsePKCS1PrivateKey error: %s, ParsePKCS8PrivateKey error: Not supported privatekey format, should be *rsa.PrivateKey, got %T", oldErr.Error(), t) + } + } + return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext) +} + +// RSADecryptBase64 Base64解码后再次进行RSA解密 +func RSADecryptBase64(privateKey string, cryptoText string) ([]byte, error) { + encryptedData, err := base64.StdEncoding.DecodeString(cryptoText) + if err != nil { + return nil, err + } + return RSADecrypt(privateKey, encryptedData) +} diff --git a/util/signature.go b/util/signature.go index 22a9cc5..2deb8e2 100644 --- a/util/signature.go +++ b/util/signature.go @@ -7,7 +7,7 @@ import ( "sort" ) -//Signature sha1签名 +// Signature sha1签名 func Signature(params ...string) string { sort.Strings(params) h := sha1.New() diff --git a/util/signature_test.go b/util/signature_test.go index 9aa2f7f..b1e8216 100644 --- a/util/signature_test.go +++ b/util/signature_test.go @@ -3,7 +3,7 @@ package util import "testing" func TestSignature(t *testing.T) { - //abc sig + // abc sig abc := "a9993e364706816aba3e25717850c26c9cd0d89d" if abc != Signature("a", "b", "c") { t.Error("test Signature Error") diff --git a/util/string.go b/util/string.go index 62b5c13..8179b70 100644 --- a/util/string.go +++ b/util/string.go @@ -5,7 +5,7 @@ import ( "time" ) -//RandomStr 随机生成字符串 +// RandomStr 随机生成字符串 func RandomStr(length int) string { str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" bytes := []byte(str) diff --git a/util/template.go b/util/template.go new file mode 100644 index 0000000..6d43dee --- /dev/null +++ b/util/template.go @@ -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 +} diff --git a/util/template_test.go b/util/template_test.go new file mode 100644 index 0000000..5a59b3e --- /dev/null +++ b/util/template_test.go @@ -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") + } +} diff --git a/util/time.go b/util/time.go index 91d31ba..b124b93 100644 --- a/util/time.go +++ b/util/time.go @@ -2,7 +2,7 @@ package util import "time" -//GetCurrTS return current timestamps +// GetCurrTS return current timestamps func GetCurrTS() int64 { return time.Now().Unix() } diff --git a/util/util.go b/util/util.go index f18d6fb..a043e0f 100644 --- a/util/util.go +++ b/util/util.go @@ -1,6 +1,6 @@ package util -//SliceChunk 用于将字符串切片分块 +// SliceChunk 用于将字符串切片分块 func SliceChunk(src []string, chunkSize int) (chunks [][]string) { total := len(src) chunks = make([][]string, 0) diff --git a/wechat.go b/wechat.go index a7023ff..29b86e0 100644 --- a/wechat.go +++ b/wechat.go @@ -3,6 +3,8 @@ package wechat import ( "os" + log "github.com/sirupsen/logrus" + "github.com/silenceper/wechat/v2/cache" "github.com/silenceper/wechat/v2/miniprogram" miniConfig "github.com/silenceper/wechat/v2/miniprogram/config" @@ -12,7 +14,8 @@ import ( openConfig "github.com/silenceper/wechat/v2/openplatform/config" "github.com/silenceper/wechat/v2/pay" payConfig "github.com/silenceper/wechat/v2/pay/config" - log "github.com/sirupsen/logrus" + "github.com/silenceper/wechat/v2/work" + workConfig "github.com/silenceper/wechat/v2/work/config" ) func init() { @@ -37,12 +40,12 @@ func NewWechat() *Wechat { return &Wechat{} } -//SetCache 设置cache -func (wc *Wechat) SetCache(cahce cache.Cache) { - wc.cache = cahce +// SetCache 设置 cache +func (wc *Wechat) SetCache(cache cache.Cache) { + wc.cache = cache } -//GetOfficialAccount 获取微信公众号实例 +// GetOfficialAccount 获取微信公众号实例 func (wc *Wechat) GetOfficialAccount(cfg *offConfig.Config) *officialaccount.OfficialAccount { if cfg.Cache == nil { cfg.Cache = wc.cache @@ -65,5 +68,16 @@ func (wc *Wechat) GetPay(cfg *payConfig.Config) *pay.Pay { // GetOpenPlatform 获取微信开放平台的实例 func (wc *Wechat) GetOpenPlatform(cfg *openConfig.Config) *openplatform.OpenPlatform { + if cfg.Cache == nil { + cfg.Cache = wc.cache + } return openplatform.NewOpenPlatform(cfg) } + +// GetWork 获取企业微信的实例 +func (wc *Wechat) GetWork(cfg *workConfig.Config) *work.Work { + if cfg.Cache == nil { + cfg.Cache = wc.cache + } + return work.NewWork(cfg) +} diff --git a/work/addresslist/client.go b/work/addresslist/client.go new file mode 100644 index 0000000..7b8cf4c --- /dev/null +++ b/work/addresslist/client.go @@ -0,0 +1,17 @@ +package addresslist + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 通讯录管理接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/addresslist/department.go b/work/addresslist/department.go new file mode 100644 index 0000000..3b41c38 --- /dev/null +++ b/work/addresslist/department.go @@ -0,0 +1,143 @@ +package addresslist + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // departmentCreateURL 创建部门 + departmentCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s" + // 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" + // departmentGetURL 获取单个部门详情 https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=ACCESS_TOKEN&id=ID + departmentGetURL = "https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=%s&id=%d" +) + +type ( + // DepartmentCreateRequest 创建部门数据请求 + DepartmentCreateRequest struct { + Name string `json:"name"` + NameEn string `json:"name_en,omitempty"` + ParentID int `json:"parentid"` + Order int `json:"order,omitempty"` + ID int `json:"id,omitempty"` + } + // DepartmentCreateResponse 创建部门数据响应 + DepartmentCreateResponse struct { + util.CommonError + ID int `json:"id"` + } + + // DepartmentSimpleListResponse 获取子部门ID列表响应 + DepartmentSimpleListResponse struct { + util.CommonError + DepartmentID []*DepartmentID `json:"department_id"` + } + // DepartmentID 子部门ID + DepartmentID struct { + ID int `json:"id"` + ParentID int `json:"parentid"` + Order int `json:"order"` + } + + // DepartmentListResponse 获取部门列表响应 + DepartmentListResponse struct { + util.CommonError + Department []*Department `json:"department"` + } + // Department 部门列表数据 + Department struct { + ID int `json:"id"` // 创建的部门id + Name string `json:"name"` // 部门名称 + NameEn string `json:"name_en"` // 英文名称 + DepartmentLeader []string `json:"department_leader"` // 部门负责人的UserID + ParentID int `json:"parentid"` // 父部门id。根部门为1 + Order int `json:"order"` // 在父部门中的次序值。order值大的排序靠前 + } + // DepartmentGetResponse 获取单个部门详情 + DepartmentGetResponse struct { + util.CommonError + Department Department `json:"department"` + } +) + +// DepartmentCreate 创建部门 +// see https://developer.work.weixin.qq.com/document/path/90205 +func (r *Client) DepartmentCreate(req *DepartmentCreateRequest) (*DepartmentCreateResponse, 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(departmentCreateURL, accessToken), req); err != nil { + return nil, err + } + result := &DepartmentCreateResponse{} + err = util.DecodeWithError(response, result, "DepartmentCreate") + return result, err +} + +// DepartmentSimpleList 获取子部门ID列表 +// see https://developer.work.weixin.qq.com/document/path/95350 +func (r *Client) DepartmentSimpleList(departmentID int) ([]*DepartmentID, 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(departmentSimpleListURL, accessToken, departmentID)); err != nil { + return nil, err + } + result := &DepartmentSimpleListResponse{} + err = util.DecodeWithError(response, result, "DepartmentSimpleList") + return result.DepartmentID, err +} + +// DepartmentList 获取部门列表 +// @desc https://developer.work.weixin.qq.com/document/path/90208 +func (r *Client) DepartmentList() ([]*Department, error) { + // 获取accessToken + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + // 发起http请求 + response, err := util.HTTPGet(fmt.Sprintf(departmentListURL, accessToken)) + if err != nil { + return nil, err + } + // 按照结构体解析返回值 + result := &DepartmentListResponse{} + err = util.DecodeWithError(response, result, "DepartmentList") + // 返回数据 + return result.Department, err +} + +// DepartmentGet 获取单个部门详情 +// see https://developer.work.weixin.qq.com/document/path/95351 +func (r *Client) DepartmentGet(departmentID int) (*Department, 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(departmentGetURL, accessToken, departmentID)); err != nil { + return nil, err + } + result := &DepartmentGetResponse{} + err = util.DecodeWithError(response, result, "DepartmentGet") + return &result.Department, err +} diff --git a/work/addresslist/linkedcorp.go b/work/addresslist/linkedcorp.go new file mode 100644 index 0000000..3d85e67 --- /dev/null +++ b/work/addresslist/linkedcorp.go @@ -0,0 +1,220 @@ +package addresslist + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // getPermListURL 获取应用的可见范围 + getPermListURL = "https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/agent/get_perm_list?access_token=%s" + // getLinkedCorpUserURL 获取互联企业成员详细信息 + getLinkedCorpUserURL = "https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/user/get?access_token=%s" + // linkedCorpSimpleListURL 获取互联企业部门成员 + linkedCorpSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/user/simplelist?access_token=%s" + // linkedCorpUserListURL 获取互联企业部门成员详情 + linkedCorpUserListURL = "https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/user/list?access_token=%s" + // linkedCorpDepartmentListURL 获取互联企业部门列表 + linkedCorpDepartmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/department/list?access_token=%s" +) + +// GetPermListResponse 获取应用的可见范围响应 +type GetPermListResponse struct { + util.CommonError + UserIDs []string `json:"userids"` + DepartmentIDs []string `json:"department_ids"` +} + +// GetPermList 获取应用的可见范围 +// see https://developer.work.weixin.qq.com/document/path/93172 +func (r *Client) GetPermList() (*GetPermListResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.HTTPPost(fmt.Sprintf(getPermListURL, accessToken), ""); err != nil { + return nil, err + } + result := &GetPermListResponse{} + err = util.DecodeWithError(response, result, "GetPermList") + return result, err +} + +// GetLinkedCorpUserRequest 获取互联企业成员详细信息请求 +type GetLinkedCorpUserRequest struct { + UserID string `json:"userid"` +} + +// GetLinkedCorpUserResponse 获取互联企业成员详细信息响应 +type GetLinkedCorpUserResponse struct { + util.CommonError + UserInfo LinkedCorpUserInfo `json:"user_info"` +} + +// LinkedCorpUserInfo 互联企业成员详细信息 +type LinkedCorpUserInfo struct { + UserID string `json:"userid"` + Name string `json:"name"` + Department []string `json:"department"` + Mobile string `json:"mobile"` + Telephone string `json:"telephone"` + Email string `json:"email"` + Position string `json:"position"` + CorpID string `json:"corpid"` + Extattr Extattr `json:"extattr"` +} + +// Extattr 互联企业成员详细信息扩展属性 +type Extattr struct { + Attrs []ExtattrItem `json:"attrs"` +} + +// ExtattrItem 互联企业成员详细信息扩展属性条目 +type ExtattrItem struct { + Name string `json:"name"` + Value string `json:"value,omitempty"` + Type int `json:"type"` + Text ExtattrItemText `json:"text,omitempty"` + Web ExtattrItemWeb `json:"web,omitempty"` +} + +// ExtattrItemText 互联企业成员详细信息自定义属性(文本) +type ExtattrItemText struct { + Value string `json:"value"` +} + +// ExtattrItemWeb 互联企业成员详细信息自定义属性(网页) +type ExtattrItemWeb struct { + URL string `json:"url"` + Title string `json:"title"` +} + +// GetLinkedCorpUser 获取互联企业成员详细信息 +// see https://developer.work.weixin.qq.com/document/path/93171 +func (r *Client) GetLinkedCorpUser(req *GetLinkedCorpUserRequest) (*GetLinkedCorpUserResponse, 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(getLinkedCorpUserURL, accessToken), req); err != nil { + return nil, err + } + result := &GetLinkedCorpUserResponse{} + err = util.DecodeWithError(response, result, "GetLinkedCorpUser") + return result, err +} + +// LinkedCorpSimpleListRequest 获取互联企业部门成员请求 +type LinkedCorpSimpleListRequest struct { + DepartmentID string `json:"department_id"` +} + +// LinkedCorpSimpleListResponse 获取互联企业部门成员响应 +type LinkedCorpSimpleListResponse struct { + util.CommonError + Userlist []LinkedCorpUser `json:"userlist"` +} + +// LinkedCorpUser 企业部门成员 +type LinkedCorpUser struct { + UserID string `json:"userid"` + Name string `json:"name"` + Department []string `json:"department"` + CorpID string `json:"corpid"` +} + +// LinkedCorpSimpleList 获取互联企业部门成员 +// see https://developer.work.weixin.qq.com/document/path/93168 +func (r *Client) LinkedCorpSimpleList(req *LinkedCorpSimpleListRequest) (*LinkedCorpSimpleListResponse, 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(linkedCorpSimpleListURL, accessToken), req); err != nil { + return nil, err + } + result := &LinkedCorpSimpleListResponse{} + err = util.DecodeWithError(response, result, "LinkedCorpSimpleList") + return result, err +} + +// LinkedCorpUserListRequest 获取互联企业部门成员详情请求 +type LinkedCorpUserListRequest struct { + DepartmentID string `json:"department_id"` +} + +// LinkedCorpUserListResponse 获取互联企业部门成员详情响应 +type LinkedCorpUserListResponse struct { + util.CommonError + UserList []LinkedCorpUserInfo `json:"userlist"` +} + +// LinkedCorpUserList 获取互联企业部门成员详情 +// see https://developer.work.weixin.qq.com/document/path/93169 +func (r *Client) LinkedCorpUserList(req *LinkedCorpUserListRequest) (*LinkedCorpUserListResponse, 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(linkedCorpUserListURL, accessToken), req); err != nil { + return nil, err + } + result := &LinkedCorpUserListResponse{} + err = util.DecodeWithError(response, result, "LinkedCorpUserList") + return result, err +} + +// LinkedCorpDepartmentListRequest 获取互联企业部门列表请求 +type LinkedCorpDepartmentListRequest struct { + DepartmentID string `json:"department_id"` +} + +// LinkedCorpDepartmentListResponse 获取互联企业部门列表响应 +type LinkedCorpDepartmentListResponse struct { + util.CommonError + DepartmentList []LinkedCorpDepartment `json:"department_list"` +} + +// LinkedCorpDepartment 互联企业部门 +type LinkedCorpDepartment struct { + DepartmentID string `json:"department_id"` + DepartmentName string `json:"department_name"` + ParentID string `json:"parentid"` + Order int `json:"order"` +} + +// LinkedCorpDepartmentList 获取互联企业部门列表 +// see https://developer.work.weixin.qq.com/document/path/93170 +func (r *Client) LinkedCorpDepartmentList(req *LinkedCorpDepartmentListRequest) (*LinkedCorpDepartmentListResponse, 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(linkedCorpDepartmentListURL, accessToken), req); err != nil { + return nil, err + } + result := &LinkedCorpDepartmentListResponse{} + err = util.DecodeWithError(response, result, "LinkedCorpDepartmentList") + return result, err +} diff --git a/work/addresslist/tag.go b/work/addresslist/tag.go new file mode 100644 index 0000000..08a5c00 --- /dev/null +++ b/work/addresslist/tag.go @@ -0,0 +1,232 @@ +package addresslist + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // createTagURL 创建标签 + createTagURL = "https://qyapi.weixin.qq.com/cgi-bin/tag/create?access_token=%s" + // updateTagURL 更新标签名字 + 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 ( + // CreateTagRequest 创建标签请求 + CreateTagRequest struct { + TagName string `json:"tagname"` + TagID int `json:"tagid,omitempty"` + } + // CreateTagResponse 创建标签响应 + CreateTagResponse struct { + util.CommonError + TagID int `json:"tagid"` + } +) + +// CreateTag 创建标签 +// see https://developer.work.weixin.qq.com/document/path/90210 +func (r *Client) CreateTag(req *CreateTagRequest) (*CreateTagResponse, 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(createTagURL, accessToken), req); err != nil { + return nil, err + } + result := &CreateTagResponse{} + err = util.DecodeWithError(response, result, "CreateTag") + return result, err +} + +type ( + // UpdateTagRequest 更新标签名字请求 + UpdateTagRequest struct { + TagID int `json:"tagid"` + TagName string `json:"tagname"` + } +) + +// UpdateTag 更新标签名字 +// see https://developer.work.weixin.qq.com/document/path/90211 +func (r *Client) UpdateTag(req *UpdateTagRequest) 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(updateTagURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "UpdateTag") +} + +// DeleteTag 删除标签 +// @see https://developer.work.weixin.qq.com/document/path/90212 +func (r *Client) DeleteTag(tagID 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(deleteTagURL, accessToken, tagID)); err != nil { + return err + } + 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{} + err = util.DecodeWithError(response, result, "GetTag") + return result, err +} + +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{} + err = util.DecodeWithError(response, result, "AddTagUsers") + return result, err +} + +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{} + err = util.DecodeWithError(response, result, "DelTagUsers") + return result, err +} + +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{} + err = util.DecodeWithError(response, result, "ListTag") + return result, err +} diff --git a/work/addresslist/user.go b/work/addresslist/user.go new file mode 100644 index 0000000..61bb146 --- /dev/null +++ b/work/addresslist/user.go @@ -0,0 +1,399 @@ +package addresslist + +import ( + "fmt" + "strings" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // userSimpleListURL 获取部门成员 + userSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist" + // userCreateURL 创建成员 + userCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s" + // userGetURL 读取成员 + userGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get" + // userDeleteURL 删除成员 + userDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/user/delete" + // userListIDURL 获取成员ID列表 + 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 ( + // UserSimpleListResponse 获取部门成员响应 + UserSimpleListResponse struct { + util.CommonError + UserList []*UserList + } + // UserList 部门成员 + UserList struct { + UserID string `json:"userid"` + Name string `json:"name"` + Department []int `json:"department"` + OpenUserID string `json:"open_userid"` + } +) + +// UserSimpleList 获取部门成员 +// @see https://developer.work.weixin.qq.com/document/path/90200 +func (r *Client) UserSimpleList(departmentID int) ([]*UserList, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + 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{} + err = util.DecodeWithError(response, result, "UserSimpleList") + return result.UserList, err +} + +type ( + // UserCreateRequest 创建成员数据请求 + UserCreateRequest struct { + UserID string `json:"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"` + } + // ExtraAttr 扩展属性 + ExtraAttr struct { + Type int `json:"type"` + Name string `json:"name"` + Text struct { + Value string `json:"value"` + } `json:"text,omitempty"` + Web struct { + URL string `json:"url"` + Title string `json:"title"` + } `json:"web,omitempty"` + } + // ExternalProfile 成员对外信息 + ExternalProfile struct { + ExternalCorpName string `json:"external_corp_name"` + WechatChannels struct { + Nickname string `json:"nickname"` + Status int `json:"status"` + } `json:"wechat_channels"` + ExternalAttr []ExternalProfileAttr `json:"external_attr"` + } + // ExternalProfileAttr 成员对外信息属性 + ExternalProfileAttr struct { + Type int `json:"type"` + Name string `json:"name"` + Text struct { + Value string `json:"value"` + } `json:"text,omitempty"` + Web struct { + URL string `json:"url"` + Title string `json:"title"` + } `json:"web,omitempty"` + Miniprogram struct { + Appid string `json:"appid"` + Pagepath string `json:"pagepath"` + Title string `json:"title"` + } `json:"miniprogram,omitempty"` + } + // UserCreateResponse 创建成员数据响应 + UserCreateResponse struct { + util.CommonError + } +) + +// UserCreate 创建成员 +// @see https://developer.work.weixin.qq.com/document/path/90195 +func (r *Client) UserCreate(req *UserCreateRequest) (*UserCreateResponse, 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(userCreateURL, accessToken), req); err != nil { + return nil, err + } + result := &UserCreateResponse{} + err = util.DecodeWithError(response, result, "UserCreate") + return result, err +} + +// UserGetResponse 获取部门成员响应 +type UserGetResponse struct { + util.CommonError + UserID string `json:"userid"` // 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节;第三方应用返回的值为open_userid + Name string `json:"name"` // 成员名称;第三方不可获取,调用时返回userid以代替name;代开发自建应用需要管理员授权才返回;对于非第三方创建的成员,第三方通讯录应用也不可获取;未返回name的情况需要通过通讯录展示组件来展示名字 + Department []int `json:"department"` // 成员所属部门id列表,仅返回该应用有查看权限的部门id;成员授权模式下,固定返回根部门id,即固定为1。对授权了“组织架构信息”权限的第三方应用,返回成员所属的全部部门id + Order []int `json:"order"` // 部门内的排序值,默认为0。数量必须和department一致,数值越大排序越前面。值范围是[0, 2^32)。成员授权模式下不返回该字段 + Position string `json:"position"` // 职务信息;代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Mobile string `json:"mobile"` // 手机号码,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Gender string `json:"gender"` // 性别。0表示未定义,1表示男性,2表示女性。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段。注:不可获取指返回值0 + Email string `json:"email"` // 邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + BizMail string `json:"biz_mail"` // 企业邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为部门负责人,数量与department一致;第三方通讯录应用或者授权了“组织架构信息-应用可获取企业的部门组织架构信息-部门负责人”权限的第三方应用可获取;对于非第三方创建的成员,第三方通讯录应用不可获取;上游企业不可获取下游企业成员该字段 + DirectLeader []string `json:"direct_leader"` // 直属上级UserID,返回在应用可见范围内的直属上级列表,最多有五个直属上级;第三方通讯录应用或者授权了“组织架构信息-应用可获取可见范围内成员组织架构信息-直属上级”权限的第三方应用可获取;对于非第三方创建的成员,第三方通讯录应用不可获取;上游企业不可获取下游企业成员该字段;代开发自建应用不可获取该字段 + Avatar string `json:"avatar"` // 头像url。 代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + ThumbAvatar string `json:"thumb_avatar"` // 头像缩略图url。第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Telephone string `json:"telephone"` // 座机。代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Alias string `json:"alias"` // 别名;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Address string `json:"address"` // 地址。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + OpenUserid string `json:"open_userid"` // 全局唯一。对于同一个服务商,不同应用获取到企业内同一个成员的open_userid是相同的,最多64个字节。仅第三方应用可获取 + MainDepartment int `json:"main_department"` // 主部门,仅当应用对主部门有查看权限时返回。 + Extattr struct { + Attrs []struct { + Type int `json:"type"` + Name string `json:"name"` + Text struct { + Value string `json:"value"` + } `json:"text,omitempty"` + Web struct { + URL string `json:"url"` + Title string `json:"title"` + } `json:"web,omitempty"` + } `json:"attrs"` + } `json:"extattr"` // 扩展属性,代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。 已激活代表已激活企业微信或已关注微信插件(原企业号)。未激活代表既未激活企业微信又未关注微信插件(原企业号)。 + QrCode string `json:"qr_code"` // 员工个人二维码,扫描可添加为外部联系人(注意返回的是一个url,可在浏览器上打开该url以展示二维码);代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + ExternalPosition string `json:"external_position"` // 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以position来展示。代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + ExternalProfile struct { + ExternalCorpName string `json:"external_corp_name"` + WechatChannels struct { + Nickname string `json:"nickname"` + Status int `json:"status"` + } `json:"wechat_channels"` + ExternalAttr []struct { + Type int `json:"type"` + Name string `json:"name"` + Text struct { + Value string `json:"value"` + } `json:"text,omitempty"` + Web struct { + URL string `json:"url"` + Title string `json:"title"` + } `json:"web,omitempty"` + Miniprogram struct { + Appid string `json:"appid"` + Pagepath string `json:"pagepath"` + Title string `json:"title"` + } `json:"miniprogram,omitempty"` + } `json:"external_attr"` + } `json:"external_profile"` // 成员对外属性,字段详情见对外属性;代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 +} + +// UserGet 读取成员 +// @see https://developer.work.weixin.qq.com/document/path/90196 +func (r *Client) UserGet(UserID string) (*UserGetResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + + if response, err = util.HTTPGet( + strings.Join([]string{ + userGetURL, + util.Query(map[string]interface{}{ + "access_token": accessToken, + "userid": UserID, + }), + }, "?")); err != nil { + return nil, err + } + result := &UserGetResponse{} + err = util.DecodeWithError(response, result, "UserGet") + return result, err +} + +type ( + // UserDeleteResponse 删除成员数据响应 + UserDeleteResponse struct { + util.CommonError + } +) + +// UserDelete 删除成员 +// @see https://developer.work.weixin.qq.com/document/path/90334 +func (r *Client) UserDelete(userID string) (*UserDeleteResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.HTTPGet(strings.Join([]string{ + userDeleteURL, + util.Query(map[string]interface{}{ + "access_token": accessToken, + "userid": userID, + }), + }, "?")); err != nil { + return nil, err + } + result := &UserDeleteResponse{} + err = util.DecodeWithError(response, result, "UserDelete") + return result, err +} + +// UserListIDRequest 获取成员ID列表请求 +type UserListIDRequest struct { + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// UserListIDResponse 获取成员ID列表响应 +type UserListIDResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + DeptUser []*DeptUser `json:"dept_user"` +} + +// DeptUser 用户-部门关系 +type DeptUser struct { + UserID string `json:"userid"` + Department int `json:"department"` +} + +// UserListID 获取成员ID列表 +// see https://developer.work.weixin.qq.com/document/path/96067 +func (r *Client) UserListID(req *UserListIDRequest) (*UserListIDResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + 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{} + err = util.DecodeWithError(response, result, "UserListID") + return result, err +} + +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{} + err = util.DecodeWithError(response, result, "ConvertToOpenID") + return result.OpenID, err +} + +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{} + err = util.DecodeWithError(response, result, "ConvertToUserID") + return result.UserID, err +} diff --git a/work/appchat/appchat.go b/work/appchat/appchat.go new file mode 100644 index 0000000..076680d --- /dev/null +++ b/work/appchat/appchat.go @@ -0,0 +1,113 @@ +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{} + err = util.DecodeWithError(response, result, apiName) + // 返回数据 + return result, err +} + +// 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方法,按照企业微信消息推送的接口文档传对应消息类型的参数来使用 diff --git a/work/appchat/client.go b/work/appchat/client.go new file mode 100644 index 0000000..246ad35 --- /dev/null +++ b/work/appchat/client.go @@ -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} +} diff --git a/work/checkin/client.go b/work/checkin/client.go new file mode 100644 index 0000000..95b9374 --- /dev/null +++ b/work/checkin/client.go @@ -0,0 +1,17 @@ +package checkin + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 打卡接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/checkin/record.go b/work/checkin/record.go new file mode 100644 index 0000000..8136ce0 --- /dev/null +++ b/work/checkin/record.go @@ -0,0 +1,259 @@ +package checkin + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // getCheckinDataURL 获取打卡记录数据 + getCheckinDataURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckindata?access_token=%s" + // getDayDataURL 获取打卡日报数据 + getDayDataURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckin_daydata?access_token=%s" + // getMonthDataURL 获取打卡月报数据 + getMonthDataURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckin_monthdata?access_token=%s" +) + +type ( + // GetCheckinDataRequest 获取打卡记录数据请求 + GetCheckinDataRequest struct { + OpenCheckinDataType int64 `json:"opencheckindatatype"` + StartTime int64 `json:"starttime"` + EndTime int64 `json:"endtime"` + UserIDList []string `json:"useridlist"` + } + // GetCheckinDataResponse 获取打卡记录数据响应 + GetCheckinDataResponse struct { + util.CommonError + CheckinData []*GetCheckinDataItem `json:"checkindata"` + } + // GetCheckinDataItem 打卡记录数据 + GetCheckinDataItem struct { + UserID string `json:"userid"` + GroupName string `json:"groupname"` + CheckinType string `json:"checkin_type"` + ExceptionType string `json:"exception_type"` + CheckinTime int64 `json:"checkin_time"` + LocationTitle string `json:"location_title"` + LocationDetail string `json:"location_detail"` + WifiName string `json:"wifiname"` + Notes string `json:"notes"` + WifiMac string `json:"wifimac"` + MediaIDs []string `json:"mediaids"` + SchCheckinTime int64 `json:"sch_checkin_time"` + GroupID int64 `json:"groupid"` + ScheduleID int64 `json:"schedule_id"` + TimelineID int64 `json:"timeline_id"` + Lat int64 `json:"lat,omitempty"` + Lng int64 `json:"lng,omitempty"` + DeviceID string `json:"deviceid,omitempty"` + } +) + +// GetCheckinData 获取打卡记录数据 +// @see https://developer.work.weixin.qq.com/document/path/90262 +func (r *Client) GetCheckinData(req *GetCheckinDataRequest) (*GetCheckinDataResponse, 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(getCheckinDataURL, accessToken), req); err != nil { + return nil, err + } + result := &GetCheckinDataResponse{} + err = util.DecodeWithError(response, result, "GetCheckinData") + return result, err +} + +type ( + // GetDayDataResponse 获取打卡日报数据 + GetDayDataResponse struct { + util.CommonError + Datas []DayDataItem `json:"datas"` + } + + // DayDataItem 日报 + DayDataItem struct { + BaseInfo DayBaseInfo `json:"base_info"` + SummaryInfo DaySummaryInfo `json:"summary_info"` + HolidayInfos []HolidayInfo `json:"holiday_infos"` + ExceptionInfos []ExceptionInfo `json:"exception_infos"` + OtInfo OtInfo `json:"ot_info"` + SpItems []SpItem `json:"sp_items"` + } + + // DayBaseInfo 基础信息 + DayBaseInfo struct { + Date int64 `json:"date"` + RecordType int64 `json:"record_type"` + Name string `json:"name"` + NameEx string `json:"name_ex"` + DepartsName string `json:"departs_name"` + AcctID string `json:"acctid"` + DayType int64 `json:"day_type"` + RuleInfo DayRuleInfo `json:"rule_info"` + } + + // DayCheckInTime 当日打卡时间 + DayCheckInTime struct { + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + } + + // DayRuleInfo 打卡人员所属规则信息 + DayRuleInfo struct { + GroupID int64 `json:"groupid"` + GroupName string `json:"groupname"` + ScheduleID int64 `json:"scheduleid"` + ScheduleName string `json:"schedulename"` + CheckInTimes []DayCheckInTime `json:"checkintime"` + } + + // DaySummaryInfo 汇总信息 + DaySummaryInfo struct { + CheckinCount int64 `json:"checkin_count"` + RegularWorkSec int64 `json:"regular_work_sec"` + StandardWorkSec int64 `json:"standard_work_sec"` + EarliestTime int64 `json:"earliest_time"` + LastestTime int64 `json:"lastest_time"` + } + + // HolidayInfo 假勤相关信息 + HolidayInfo struct { + SpNumber string `json:"sp_number"` + SpTitle SpTitle `json:"sp_title"` + SpDescription SpDescription `json:"sp_description"` + } + + // SpTitle 假勤信息摘要-标题信息 + SpTitle struct { + Data []SpData `json:"data"` + } + + // SpDescription 假勤信息摘要-描述信息 + SpDescription struct { + Data []SpData `json:"data"` + } + + // SpData 假勤信息(多种语言描述,目前只有中文一种) + SpData struct { + Lang string `json:"lang"` + Text string `json:"text"` + } + + // SpItem 假勤统计信息 + SpItem struct { + Count int64 `json:"count"` + Duration int64 `json:"duration"` + TimeType int64 `json:"time_type"` + Type int64 `json:"type"` + VacationID int64 `json:"vacation_id"` + Name string `json:"name"` + } + + // ExceptionInfo 校准状态信息 + ExceptionInfo struct { + Count int64 `json:"count"` + Duration int64 `json:"duration"` + Exception int64 `json:"exception"` + } + + // OtInfo 加班信息 + OtInfo struct { + OtStatus int64 `json:"ot_status"` + OtDuration int64 `json:"ot_duration"` + ExceptionDuration []uint64 `json:"exception_duration"` + } +) + +// GetDayData 获取打卡日报数据 +// @see https://developer.work.weixin.qq.com/document/path/96498 +func (r *Client) GetDayData(req *GetCheckinDataRequest) (result *GetDayDataResponse, err error) { + var ( + response []byte + accessToken string + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return + } + if response, err = util.PostJSON(fmt.Sprintf(getDayDataURL, accessToken), req); err != nil { + return + } + + result = new(GetDayDataResponse) + err = util.DecodeWithError(response, result, "GetDayData") + return +} + +type ( + // GetMonthDataResponse 获取打卡月报数据 + GetMonthDataResponse struct { + util.CommonError + Datas []MonthDataItem `json:"datas"` + } + + // MonthDataItem 月报数据 + MonthDataItem struct { + BaseInfo MonthBaseInfo `json:"base_info"` + SummaryInfo MonthSummaryInfo `json:"summary_info"` + ExceptionInfos []ExceptionInfo `json:"exception_infos"` + SpItems []SpItem `json:"sp_items"` + OverWorkInfo OverWorkInfo `json:"overwork_info"` + } + + // MonthBaseInfo 基础信息 + MonthBaseInfo struct { + RecordType int64 `json:"record_type"` + Name string `json:"name"` + NameEx string `json:"name_ex"` + DepartsName string `json:"departs_name"` + AcctID string `json:"acctid"` + RuleInfo MonthRuleInfo `json:"rule_info"` + } + + // MonthRuleInfo 打卡人员所属规则信息 + MonthRuleInfo struct { + GroupID int64 `json:"groupid"` + GroupName string `json:"groupname"` + } + + // MonthSummaryInfo 汇总信息 + MonthSummaryInfo struct { + WorkDays int64 `json:"work_days"` + ExceptDays int64 `json:"except_days"` + RegularDays int64 `json:"regular_days"` + RegularWorkSec int64 `json:"regular_work_sec"` + StandardWorkSec int64 `json:"standard_work_sec"` + } + + // OverWorkInfo 加班情况 + OverWorkInfo struct { + WorkdayOverSec int64 `json:"workday_over_sec"` + HolidayOverSec int64 `json:"holidays_over_sec"` + RestDayOverSec int64 `json:"restdays_over_sec"` + } +) + +// GetMonthData 获取打卡月报数据 +// @see https://developer.work.weixin.qq.com/document/path/96499 +func (r *Client) GetMonthData(req *GetCheckinDataRequest) (result *GetMonthDataResponse, err error) { + var ( + response []byte + accessToken string + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return + } + if response, err = util.PostJSON(fmt.Sprintf(getMonthDataURL, accessToken), req); err != nil { + return + } + + result = new(GetMonthDataResponse) + err = util.DecodeWithError(response, result, "GetMonthData") + return +} diff --git a/work/config/config.go b/work/config/config.go new file mode 100644 index 0000000..84aef7c --- /dev/null +++ b/work/config/config.go @@ -0,0 +1,18 @@ +// Package config 企业微信config配置 +package config + +import ( + "github.com/silenceper/wechat/v2/cache" +) + +// Config for 企业微信 +type Config struct { + CorpID string `json:"corp_id"` // corp_id + CorpSecret string `json:"corp_secret"` // corp_secret,如果需要获取会话存档实例,当前参数请填写聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 + AgentID string `json:"agent_id"` // agent_id + Cache cache.Cache + RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存 + + Token string `json:"token"` // 微信客服回调配置,用于生成签名校验回调请求的合法性 + EncodingAESKey string `json:"encoding_aes_key"` // 微信客服回调p配置,用于解密回调消息内容对应的密文 +} diff --git a/work/context/context.go b/work/context/context.go new file mode 100644 index 0000000..2927d85 --- /dev/null +++ b/work/context/context.go @@ -0,0 +1,12 @@ +package context + +import ( + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/work/config" +) + +// Context struct +type Context struct { + *config.Config + credential.AccessTokenHandle +} diff --git a/work/externalcontact/README.md b/work/externalcontact/README.md new file mode 100644 index 0000000..f0a1e64 --- /dev/null +++ b/work/externalcontact/README.md @@ -0,0 +1,3 @@ +### 企业微信 客户联系部分 + +相关文档正在梳理中... \ No newline at end of file diff --git a/work/externalcontact/callback.go b/work/externalcontact/callback.go new file mode 100644 index 0000000..14aa1c9 --- /dev/null +++ b/work/externalcontact/callback.go @@ -0,0 +1,43 @@ +package externalcontact + +import ( + "encoding/xml" + + "github.com/silenceper/wechat/v2/util" +) + +// 原始回调消息内容 +type callbackOriginMessage struct { + ToUserName string // 企业微信的CorpID,当为第三方套件回调事件时,CorpID的内容为suiteid + AgentID string // 接收的应用id,可在应用的设置页面获取 + Encrypt string // 消息结构体加密后的字符串 +} + +// EventCallbackMessage 微信客户联系回调消息 +// https://developer.work.weixin.qq.com/document/path/92130 +type EventCallbackMessage struct { + ToUserName string `json:"to_user_name"` + FromUserName string `json:"from_user_name"` + CreateTime int64 `json:"create_time"` + MsgType string `json:"msg_type"` + Event string `json:"event"` + ChangeType string `json:"change_type"` + UserID string `json:"user_id"` + ExternalUserID string `json:"external_user_id"` + State string `json:"state"` + WelcomeCode string `json:"welcome_code"` +} + +// GetCallbackMessage 获取联系客户回调事件中的消息内容 +func (r *Client) GetCallbackMessage(encryptedMsg []byte) (msg EventCallbackMessage, err error) { + var origin callbackOriginMessage + if err = xml.Unmarshal(encryptedMsg, &origin); err != nil { + return + } + _, bData, err := util.DecryptMsg(r.CorpID, origin.Encrypt, r.EncodingAESKey) + if err != nil { + return + } + err = xml.Unmarshal(bData, &msg) + return +} diff --git a/work/externalcontact/client.go b/work/externalcontact/client.go new file mode 100644 index 0000000..6399f4d --- /dev/null +++ b/work/externalcontact/client.go @@ -0,0 +1,17 @@ +package externalcontact + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 外部联系接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/externalcontact/contact_way.go b/work/externalcontact/contact_way.go new file mode 100644 index 0000000..6420006 --- /dev/null +++ b/work/externalcontact/contact_way.go @@ -0,0 +1,265 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // addContactWayURL 配置客户联系「联系我」方式 + addContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_contact_way?access_token=%s" + // getContactWayURL 获取企业已配置的「联系我」方式 + getContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_contact_way?access_token=%s" + // updateContactWayURL 更新企业已配置的「联系我」方式 + updateContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/update_contact_way?access_token=%s" + // listContactWayURL 获取企业已配置的「联系我」列表 + listContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_contact_way?access_token=%s" + // delContactWayURL 删除企业已配置的「联系我」方式 + delContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_contact_way?access_token=%s" +) + +type ( + // ConclusionsRequest 结束语请求 + ConclusionsRequest struct { + Text ConclusionsText `json:"text"` + Image ConclusionsImageRequest `json:"image"` + Link ConclusionsLink `json:"link"` + MiniProgram ConclusionsMiniProgram `json:"miniprogram"` + } + // ConclusionsText 文本格式结束语 + ConclusionsText struct { + Content string `json:"content"` + } + // ConclusionsImageRequest 图片格式结束语请求 + ConclusionsImageRequest struct { + MediaID string `json:"media_id"` + } + // ConclusionsLink 链接格式结束语 + ConclusionsLink struct { + Title string `json:"title"` + PicURL string `json:"picurl"` + Desc string `json:"desc"` + URL string `json:"url"` + } + // ConclusionsMiniProgram 小程序格式结束语 + ConclusionsMiniProgram struct { + Title string `json:"title"` + PicMediaID string `json:"pic_media_id"` + AppID string `json:"appid"` + Page string `json:"page"` + } + // ConclusionsResponse 结束语响应 + ConclusionsResponse struct { + Text ConclusionsText `json:"text"` + Image ConclusionsImageResponse `json:"image"` + Link ConclusionsLink `json:"link"` + MiniProgram ConclusionsMiniProgram `json:"miniprogram"` + } + // ConclusionsImageResponse 图片格式结束语响应 + ConclusionsImageResponse struct { + PicURL string `json:"pic_url"` + } +) + +type ( + // AddContactWayRequest 配置客户联系「联系我」方式请求 + AddContactWayRequest struct { + Type int `json:"type"` + Scene int `json:"scene"` + Style int `json:"style"` + Remark string `json:"remark"` + SkipVerify bool `json:"skip_verify"` + State string `json:"state"` + User []string `json:"user"` + Party []int `json:"party"` + IsTemp bool `json:"is_temp"` + ExpiresIn int `json:"expires_in"` + ChatExpiresIn int `json:"chat_expires_in"` + UnionID string `json:"unionid"` + Conclusions ConclusionsRequest `json:"conclusions"` + } + // AddContactWayResponse 配置客户联系「联系我」方式响应 + AddContactWayResponse struct { + util.CommonError + ConfigID string `json:"config_id"` + QrCode string `json:"qr_code"` + } +) + +// AddContactWay 配置客户联系「联系我」方式 +// see https://developer.work.weixin.qq.com/document/path/92228 +func (r *Client) AddContactWay(req *AddContactWayRequest) (*AddContactWayResponse, 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(addContactWayURL, accessToken), req); err != nil { + return nil, err + } + result := &AddContactWayResponse{} + err = util.DecodeWithError(response, result, "AddContactWay") + return result, err +} + +type ( + // GetContactWayRequest 获取企业已配置的「联系我」方式请求 + GetContactWayRequest struct { + ConfigID string `json:"config_id"` + } + // GetContactWayResponse 获取企业已配置的「联系我」方式响应 + GetContactWayResponse struct { + util.CommonError + ContactWay ContactWay `json:"contact_way"` + } + // ContactWay 「联系我」配置 + ContactWay struct { + ConfigID string `json:"config_id"` + Type int `json:"type"` + Scene int `json:"scene"` + Style int `json:"style"` + Remark string `json:"remark"` + SkipVerify bool `json:"skip_verify"` + State string `json:"state"` + QrCode string `json:"qr_code"` + User []string `json:"user"` + Party []int `json:"party"` + IsTemp bool `json:"is_temp"` + ExpiresIn int `json:"expires_in"` + ChatExpiresIn int `json:"chat_expires_in"` + UnionID string `json:"unionid"` + Conclusions ConclusionsResponse `json:"conclusions"` + } +) + +// GetContactWay 获取企业已配置的「联系我」方式 +// see https://developer.work.weixin.qq.com/document/path/92228 +func (r *Client) GetContactWay(req *GetContactWayRequest) (*GetContactWayResponse, 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(getContactWayURL, accessToken), req); err != nil { + return nil, err + } + result := &GetContactWayResponse{} + err = util.DecodeWithError(response, result, "GetContactWay") + return result, err +} + +type ( + // UpdateContactWayRequest 更新企业已配置的「联系我」方式请求 + UpdateContactWayRequest struct { + ConfigID string `json:"config_id"` + Remark string `json:"remark"` + SkipVerify bool `json:"skip_verify"` + Style int `json:"style"` + State string `json:"state"` + User []string `json:"user"` + Party []int `json:"party"` + ExpiresIn int `json:"expires_in"` + ChatExpiresIn int `json:"chat_expires_in"` + UnionID string `json:"unionid"` + Conclusions ConclusionsRequest `json:"conclusions"` + } + // UpdateContactWayResponse 更新企业已配置的「联系我」方式响应 + UpdateContactWayResponse struct { + util.CommonError + } +) + +// UpdateContactWay 更新企业已配置的「联系我」方式 +// see https://developer.work.weixin.qq.com/document/path/92228 +func (r *Client) UpdateContactWay(req *UpdateContactWayRequest) (*UpdateContactWayResponse, 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(updateContactWayURL, accessToken), req); err != nil { + return nil, err + } + result := &UpdateContactWayResponse{} + err = util.DecodeWithError(response, result, "UpdateContactWay") + return result, err +} + +type ( + //ListContactWayRequest 获取企业已配置的「联系我」列表请求 + ListContactWayRequest struct { + StartTime int `json:"start_time"` + EndTime int `json:"end_time"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` + } + //ListContactWayResponse 获取企业已配置的「联系我」列表响应 + ListContactWayResponse struct { + util.CommonError + ContactWay []*ContactWayForList `json:"contact_way"` + NextCursor string `json:"next_cursor"` + } + // ContactWayForList 「联系我」配置 + ContactWayForList struct { + ConfigID string `json:"config_id"` + } +) + +// ListContactWay 获取企业已配置的「联系我」列表 +// see https://developer.work.weixin.qq.com/document/path/92228 +func (r *Client) ListContactWay(req *ListContactWayRequest) (*ListContactWayResponse, 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(listContactWayURL, accessToken), req); err != nil { + return nil, err + } + result := &ListContactWayResponse{} + err = util.DecodeWithError(response, result, "ListContactWay") + return result, err +} + +type ( + // DelContactWayRequest 删除企业已配置的「联系我」方式请求 + DelContactWayRequest struct { + ConfigID string `json:"config_id"` + } + // DelContactWayResponse 删除企业已配置的「联系我」方式响应 + DelContactWayResponse struct { + util.CommonError + } +) + +// DelContactWay 删除企业已配置的「联系我」方式 +// see https://developer.work.weixin.qq.com/document/path/92228 +func (r *Client) DelContactWay(req *DelContactWayRequest) (*DelContactWayResponse, 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(delContactWayURL, accessToken), req); err != nil { + return nil, err + } + result := &DelContactWayResponse{} + err = util.DecodeWithError(response, result, "DelContactWay") + return result, err +} diff --git a/work/externalcontact/customer_acquisition.go b/work/externalcontact/customer_acquisition.go new file mode 100644 index 0000000..e2d8ea1 --- /dev/null +++ b/work/externalcontact/customer_acquisition.go @@ -0,0 +1,310 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // listLinkUrl 获取获客链接列表 + listLinkURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/list_link?access_token=%s" + // getCustomerAcquisition 获取获客链接详情 + getCustomerAcquisitionURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get?access_token=%s" + // createCustomerAcquisitionLink 创建获客链接 + createCustomerAcquisitionLinkURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/create_link?access_token=%s" + // updateCustomerAcquisitionLink 编辑获客链接 + updateCustomerAcquisitionLinkURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/update_link?access_token=%s" + // deleteCustomerAcquisitionLink 删除获客链接 + deleteCustomerAcquisitionLinkURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/delete_link?access_token=%s" + // getCustomerInfoWithCustomerAcquisitionLinkURL 获取由获客链接添加的客户信息 + getCustomerInfoWithCustomerAcquisitionLinkURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/customer?access_token=%s" + // customerAcquisitionQuota 查询剩余使用量 + customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s" + // customerAcquisitionStatistic 查询链接使用详情 + customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s" +) + +type ( + // ListLinkRequest 获取获客链接列表请求 + ListLinkRequest struct { + Limit int `json:"limit"` + Cursor string `json:"cursor"` + } + // ListLinkResponse 获取获客链接列表响应 + ListLinkResponse struct { + util.CommonError + LinkIDList []string `json:"link_id_list"` + NextCursor string `json:"next_cursor"` + } +) + +// ListLink 获客助手--获取获客链接列表 +// see https://developer.work.weixin.qq.com/document/path/97297 +func (r *Client) ListLink(req *ListLinkRequest) (*ListLinkResponse, 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(listLinkURL, accessToken), req); err != nil { + return nil, err + } + result := &ListLinkResponse{} + err = util.DecodeWithError(response, result, "ListLink") + return result, err +} + +type ( + // GetCustomerAcquisitionRequest 获取获客链接详情请求 + GetCustomerAcquisitionRequest struct { + LinkID string `json:"link_id"` + } + // GetCustomerAcquisitionResponse 获取获客链接详情响应 + GetCustomerAcquisitionResponse struct { + util.CommonError + Link Link `json:"link"` + Range CustomerAcquisitionRange `json:"range"` + SkipVerify bool `json:"skip_verify"` + } + // Link 获客链接 + Link struct { + LinkID string `json:"link_id"` + LinkName string `json:"link_name"` + URL string `json:"url"` + CreateTime int64 `json:"create_time"` + } + + // CustomerAcquisitionRange 该获客链接使用范围 + CustomerAcquisitionRange struct { + UserList []string `json:"user_list"` + DepartmentList []int64 `json:"department_list"` + } +) + +// GetCustomerAcquisition 获客助手--获取获客链接详情 +// see https://developer.work.weixin.qq.com/document/path/97297 +func (r *Client) GetCustomerAcquisition(req *GetCustomerAcquisitionRequest) (*GetCustomerAcquisitionResponse, 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(getCustomerAcquisitionURL, accessToken), req); err != nil { + return nil, err + } + result := &GetCustomerAcquisitionResponse{} + err = util.DecodeWithError(response, result, "GetCustomerAcquisition") + return result, err +} + +type ( + // CreateCustomerAcquisitionLinkRequest 创建获客链接请求 + CreateCustomerAcquisitionLinkRequest struct { + LinkName string `json:"link_name"` + Range CustomerAcquisitionRange `json:"range"` + SkipVerify bool `json:"skip_verify"` + } + // CreateCustomerAcquisitionLinkResponse 创建获客链接响应 + CreateCustomerAcquisitionLinkResponse struct { + util.CommonError + Link Link `json:"link"` + } +) + +// CreateCustomerAcquisitionLink 获客助手--创建获客链接 +// see https://developer.work.weixin.qq.com/document/path/97297 +func (r *Client) CreateCustomerAcquisitionLink(req *CreateCustomerAcquisitionLinkRequest) (*CreateCustomerAcquisitionLinkResponse, 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(createCustomerAcquisitionLinkURL, accessToken), req); err != nil { + return nil, err + } + result := &CreateCustomerAcquisitionLinkResponse{} + err = util.DecodeWithError(response, result, "CreateCustomerAcquisitionLink") + return result, err +} + +type ( + // UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求 + UpdateCustomerAcquisitionLinkRequest struct { + LinkID string `json:"link_id"` + LinkName string `json:"link_name"` + Range CustomerAcquisitionRange `json:"range"` + SkipVerify bool `json:"skip_verify"` + } + // UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应 + UpdateCustomerAcquisitionLinkResponse struct { + util.CommonError + } +) + +// UpdateCustomerAcquisitionLink 获客助手--编辑获客链接 +// see https://developer.work.weixin.qq.com/document/path/97297 +func (r *Client) UpdateCustomerAcquisitionLink(req *UpdateCustomerAcquisitionLinkRequest) (*UpdateCustomerAcquisitionLinkResponse, 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(updateCustomerAcquisitionLinkURL, accessToken), req); err != nil { + return nil, err + } + result := &UpdateCustomerAcquisitionLinkResponse{} + err = util.DecodeWithError(response, result, "UpdateCustomerAcquisitionLink") + return result, err +} + +type ( + // DeleteCustomerAcquisitionLinkRequest 删除获客链接请求 + DeleteCustomerAcquisitionLinkRequest struct { + LinkID string `json:"link_id"` + } + // DeleteCustomerAcquisitionLinkResponse 删除获客链接响应 + DeleteCustomerAcquisitionLinkResponse struct { + util.CommonError + } +) + +// DeleteCustomerAcquisitionLink 获客助手--删除获客链接 +// see https://developer.work.weixin.qq.com/document/path/97297 +func (r *Client) DeleteCustomerAcquisitionLink(req *DeleteCustomerAcquisitionLinkRequest) (*DeleteCustomerAcquisitionLinkResponse, 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(deleteCustomerAcquisitionLinkURL, accessToken), req); err != nil { + return nil, err + } + result := &DeleteCustomerAcquisitionLinkResponse{} + err = util.DecodeWithError(response, result, "DeleteCustomerAcquisitionLink") + return result, err +} + +type ( + // GetCustomerInfoWithCustomerAcquisitionLinkRequest 获取由获客链接添加的客户信息请求 + GetCustomerInfoWithCustomerAcquisitionLinkRequest struct { + LinkID string `json:"link_id"` + Limit int64 `json:"limit"` + Cursor string `json:"cursor"` + } + // GetCustomerInfoWithCustomerAcquisitionLinkResponse 获取由获客链接添加的客户信息响应 + GetCustomerInfoWithCustomerAcquisitionLinkResponse struct { + util.CommonError + CustomerList []CustomerList `json:"customer_list"` + NextCursor string `json:"next_cursor"` + } + // CustomerList 客户列表 + CustomerList struct { + ExternalUserid string `json:"external_userid"` + Userid string `json:"userid"` + ChatStatus int64 `json:"chat_status"` + State string `json:"state"` + } +) + +// GetCustomerInfoWithCustomerAcquisitionLink 获客助手--获取由获客链接添加的客户信息 +// see https://developer.work.weixin.qq.com/document/path/97298 +func (r *Client) GetCustomerInfoWithCustomerAcquisitionLink(req *GetCustomerInfoWithCustomerAcquisitionLinkRequest) (*GetCustomerInfoWithCustomerAcquisitionLinkResponse, 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(getCustomerInfoWithCustomerAcquisitionLinkURL, accessToken), req); err != nil { + return nil, err + } + result := &GetCustomerInfoWithCustomerAcquisitionLinkResponse{} + err = util.DecodeWithError(response, result, "GetCustomerInfoWithCustomerAcquisitionLink") + return result, err +} + +type ( + // CustomerAcquisitionQuotaResponse 查询剩余使用量响应 + CustomerAcquisitionQuotaResponse struct { + util.CommonError + Total int64 `json:"total"` + Balance int64 `json:"balance"` + QuotaList []QuotaList `json:"quota_list"` + } + // QuotaList 额度 + QuotaList struct { + ExpireDate int64 `json:"expire_date"` + Balance int64 `json:"balance"` + } +) + +// CustomerAcquisitionQuota 获客助手额度管理与使用统计--查询剩余使用量 +// see https://developer.work.weixin.qq.com/document/path/97375 +func (r *Client) CustomerAcquisitionQuota() (*CustomerAcquisitionQuotaResponse, 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(customerAcquisitionQuotaURL, accessToken)); err != nil { + return nil, err + } + result := &CustomerAcquisitionQuotaResponse{} + err = util.DecodeWithError(response, result, "CustomerAcquisitionQuota") + return result, err +} + +type ( + // CustomerAcquisitionStatisticRequest 查询链接使用详情请求 + CustomerAcquisitionStatisticRequest struct { + LinkID string `json:"link_id"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + } + // CustomerAcquisitionStatisticResponse 查询链接使用详情响应 + CustomerAcquisitionStatisticResponse struct { + util.CommonError + ClickLinkCustomerCnt int64 `json:"click_link_customer_cnt"` + NewCustomerCnt int64 `json:"new_customer_cnt"` + } +) + +// CustomerAcquisitionStatistic 获客助手额度管理与使用统计--查询链接使用详情 +// see https://developer.work.weixin.qq.com/document/path/97375 +func (r *Client) CustomerAcquisitionStatistic(req *CustomerAcquisitionStatisticRequest) (*CustomerAcquisitionStatisticResponse, 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(customerAcquisitionStatisticURL, accessToken), req); err != nil { + return nil, err + } + result := &CustomerAcquisitionStatisticResponse{} + err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic") + return result, err +} diff --git a/work/externalcontact/external_user.go b/work/externalcontact/external_user.go new file mode 100644 index 0000000..2c58d73 --- /dev/null +++ b/work/externalcontact/external_user.go @@ -0,0 +1,449 @@ +package externalcontact + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // fetchExternalContactUserListURL 获取客户列表 + fetchExternalContactUserListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list" + // fetchExternalContactUserDetailURL 获取客户详情 + fetchExternalContactUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get" + // fetchBatchExternalContactUserDetailURL 批量获取客户详情 + fetchBatchExternalContactUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user" + // updateUserRemarkURL 更新客户备注信息 + updateUserRemarkURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remark" + // listCustomerStrategyURL 获取规则组列表 + listCustomerStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_strategy/list?access_token=%s" + // getCustomerStrategyURL 获取规则组详情 + getCustomerStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_strategy/get?access_token=%s" + // getRangeCustomerStrategyURL 获取规则组管理范围 + getRangeCustomerStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_strategy/get_range?access_token=%s" + // createCustomerStrategyURL 创建新的规则组 + createCustomerStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_strategy/create?access_token=%s" + // editCustomerStrategyURL 编辑规则组及其管理范围 + editCustomerStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_strategy/edit?access_token=%s" + // delCustomerStrategyURL 删除规则组 + delCustomerStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_strategy/del?access_token=%s" +) + +// ExternalUserListResponse 外部联系人列表响应 +type ExternalUserListResponse struct { + util.CommonError + ExternalUserID []string `json:"external_userid"` +} + +// GetExternalUserList 获取客户列表 +// @see https://developer.work.weixin.qq.com/document/path/92113 +func (r *Client) GetExternalUserList(userID string) ([]string, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + response, err = util.HTTPGet(fmt.Sprintf("%s?access_token=%v&userid=%v", fetchExternalContactUserListURL, accessToken, userID)) + if err != nil { + return nil, err + } + var result ExternalUserListResponse + err = util.DecodeWithError(response, &result, "GetExternalUserList") + return result.ExternalUserID, err +} + +// ExternalUserDetailResponse 外部联系人详情响应 +type ExternalUserDetailResponse struct { + util.CommonError + ExternalContact ExternalUser `json:"external_contact"` + FollowUser []FollowUser `json:"follow_user"` + NextCursor string `json:"next_cursor"` +} + +// ExternalUser 外部联系人 +type ExternalUser struct { + ExternalUserID string `json:"external_userid"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Type int64 `json:"type"` + Gender int64 `json:"gender"` + UnionID string `json:"unionid"` + Position string `json:"position"` + CorpName string `json:"corp_name"` + CorpFullName string `json:"corp_full_name"` + ExternalProfile string `json:"external_profile"` +} + +// FollowUser 跟进用户(指企业内部用户) +type FollowUser struct { + UserID string `json:"userid"` + Remark string `json:"remark"` + Description string `json:"description"` + CreateTime int64 `json:"createtime"` + Tags []Tag `json:"tags"` + RemarkCorpName string `json:"remark_corp_name"` + RemarkMobiles []string `json:"remark_mobiles"` + OperUserID string `json:"oper_userid"` + AddWay int64 `json:"add_way"` + WeChatChannels WechatChannel `json:"wechat_channels"` + State string `json:"state"` +} + +// Tag 已绑定在外部联系人的标签 +type Tag struct { + GroupName string `json:"group_name"` + TagName string `json:"tag_name"` + Type int64 `json:"type"` + TagID string `json:"tag_id"` +} + +// WechatChannel 视频号添加的场景 +type WechatChannel struct { + NickName string `json:"nickname"` + Source int `json:"source"` +} + +// GetExternalUserDetail 获取外部联系人详情 +// @see https://developer.work.weixin.qq.com/document/path/92114 +func (r *Client) GetExternalUserDetail(externalUserID string, nextCursor ...string) (*ExternalUserDetailResponse, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + var cursor string + if len(nextCursor) > 0 { + cursor = nextCursor[0] + } + response, err = util.HTTPGet(fmt.Sprintf("%s?access_token=%v&external_userid=%v&cursor=%v", fetchExternalContactUserDetailURL, accessToken, externalUserID, cursor)) + if err != nil { + return nil, err + } + result := &ExternalUserDetailResponse{} + err = util.DecodeWithError(response, result, "get_external_user_detail") + return result, err +} + +// BatchGetExternalUserDetailsRequest 批量获取外部联系人详情请求 +type BatchGetExternalUserDetailsRequest struct { + UserIDList []string `json:"userid_list"` + Cursor string `json:"cursor"` + Limit int `json:"limit,omitempty"` +} + +// ExternalUserDetailListResponse 批量获取外部联系人详情响应 +type ExternalUserDetailListResponse struct { + util.CommonError + ExternalContactList []ExternalUserForBatch `json:"external_contact_list"` +} + +// ExternalUserForBatch 批量获取外部联系人客户列表 +type ExternalUserForBatch struct { + ExternalContact ExternalContact `json:"external_contact"` + FollowInfo FollowInfo `json:"follow_info"` +} + +// ExternalContact 批量获取外部联系人用户信息 +type ExternalContact struct { + ExternalUserID string `json:"external_userid"` + Name string `json:"name"` + Position string `json:"position"` + Avatar string `json:"avatar"` + CorpName string `json:"corp_name"` + CorpFullName string `json:"corp_full_name"` + Type int64 `json:"type"` + Gender int64 `json:"gender"` + UnionID string `json:"unionid"` + ExternalProfile string `json:"external_profile"` +} + +// FollowInfo 批量获取外部联系人跟进人信息 +type FollowInfo struct { + UserID string `json:"userid"` + Remark string `json:"remark"` + Description string `json:"description"` + CreateTime int64 `json:"createtime"` + TagID []string `json:"tag_id"` + RemarkCorpName string `json:"remark_corp_name"` + RemarkMobiles []string `json:"remark_mobiles"` + OperUserID string `json:"oper_userid"` + AddWay int64 `json:"add_way"` + WeChatChannels WechatChannel `json:"wechat_channels"` +} + +// BatchGetExternalUserDetails 批量获取外部联系人详情 +// @see https://developer.work.weixin.qq.com/document/path/92994 +func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + jsonData, err := json.Marshal(request) + if err != nil { + return nil, err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", fetchBatchExternalContactUserDetailURL, accessToken), string(jsonData)) + if err != nil { + return nil, err + } + var result ExternalUserDetailListResponse + err = util.DecodeWithError(response, &result, "BatchGetExternalUserDetails") + return result.ExternalContactList, err +} + +// UpdateUserRemarkRequest 修改客户备注信息请求体 +type UpdateUserRemarkRequest struct { + UserID string `json:"userid"` + ExternalUserID string `json:"external_userid"` + Remark string `json:"remark"` + Description string `json:"description"` + RemarkCompany string `json:"remark_company"` + RemarkMobiles []string `json:"remark_mobiles"` + RemarkPicMediaID string `json:"remark_pic_mediaid"` +} + +// UpdateUserRemark 修改客户备注信息 +// @see https://developer.work.weixin.qq.com/document/path/92115 +func (r *Client) UpdateUserRemark(request UpdateUserRemarkRequest) error { + accessToken, err := r.GetAccessToken() + if err != nil { + return err + } + var response []byte + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", updateUserRemarkURL, accessToken), string(jsonData)) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "UpdateUserRemark") +} + +// ListCustomerStrategyRequest 获取规则组列表请求 +type ListCustomerStrategyRequest struct { + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// ListCustomerStrategyResponse 获取规则组列表响应 +type ListCustomerStrategyResponse struct { + util.CommonError + Strategy []StrategyID `json:"strategy"` + NextCursor string `json:"next_cursor"` +} + +// StrategyID 规则组ID +type StrategyID struct { + StrategyID int `json:"strategy_id"` +} + +// ListCustomerStrategy 获取规则组列表 +// @see https://developer.work.weixin.qq.com/document/path/94883#%E8%8E%B7%E5%8F%96%E8%A7%84%E5%88%99%E7%BB%84%E5%88%97%E8%A1%A8 +func (r *Client) ListCustomerStrategy(req *ListCustomerStrategyRequest) (*ListCustomerStrategyResponse, 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(listCustomerStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &ListCustomerStrategyResponse{} + err = util.DecodeWithError(response, result, "ListCustomerStrategy") + return result, err +} + +// GetCustomerStrategyRequest 获取规则组详情请求 +type GetCustomerStrategyRequest struct { + StrategyID int `json:"strategy_id"` +} + +// GetCustomerStrategyResponse 获取规则组详情响应 +type GetCustomerStrategyResponse struct { + util.CommonError + Strategy Strategy `json:"strategy"` +} + +// Strategy 规则组 +type Strategy struct { + StrategyID int `json:"strategy_id"` + ParentID int `json:"parent_id"` + StrategyName string `json:"strategy_name"` + CreateTime int64 `json:"create_time"` + AdminList []string `json:"admin_list"` + Privilege Privilege `json:"privilege"` +} + +// Privilege 权限 +type Privilege struct { + ViewCustomerList bool `json:"view_customer_list"` + ViewCustomerData bool `json:"view_customer_data"` + ViewRoomList bool `json:"view_room_list"` + ContactMe bool `json:"contact_me"` + JoinRoom bool `json:"join_room"` + ShareCustomer bool `json:"share_customer"` + OperResignCustomer bool `json:"oper_resign_customer"` + OperResignGroup bool `json:"oper_resign_group"` + SendCustomerMsg bool `json:"send_customer_msg"` + EditWelcomeMsg bool `json:"edit_welcome_msg"` + ViewBehaviorData bool `json:"view_behavior_data"` + ViewRoomData bool `json:"view_room_data"` + SendGroupMsg bool `json:"send_group_msg"` + RoomDeduplication bool `json:"room_deduplication"` + RapidReply bool `json:"rapid_reply"` + OnjobCustomerTransfer bool `json:"onjob_customer_transfer"` + EditAntiSpamRule bool `json:"edit_anti_spam_rule"` + ExportCustomerList bool `json:"export_customer_list"` + ExportCustomerData bool `json:"export_customer_data"` + ExportCustomerGroupList bool `json:"export_customer_group_list"` + ManageCustomerTag bool `json:"manage_customer_tag"` +} + +// GetCustomerStrategy 获取规则组详情 +// @see https://developer.work.weixin.qq.com/document/path/94883#%E8%8E%B7%E5%8F%96%E8%A7%84%E5%88%99%E7%BB%84%E8%AF%A6%E6%83%85 +func (r *Client) GetCustomerStrategy(req *GetCustomerStrategyRequest) (*GetCustomerStrategyResponse, 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(getCustomerStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &GetCustomerStrategyResponse{} + err = util.DecodeWithError(response, result, "GetCustomerStrategy") + return result, err +} + +// GetRangeCustomerStrategyRequest 获取规则组管理范围请求 +type GetRangeCustomerStrategyRequest struct { + StrategyID int `json:"strategy_id"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// GetRangeCustomerStrategyResponse 获取规则组管理范围响应 +type GetRangeCustomerStrategyResponse struct { + util.CommonError + Range []Range `json:"range"` + NextCursor string `json:"next_cursor"` +} + +// Range 管理范围节点 +type Range struct { + Type int `json:"type"` + UserID string `json:"userid,omitempty"` + PartyID int `json:"partyid,omitempty"` +} + +// GetRangeCustomerStrategy 获取规则组管理范围 +// @see https://developer.work.weixin.qq.com/document/path/94883#%E8%8E%B7%E5%8F%96%E8%A7%84%E5%88%99%E7%BB%84%E7%AE%A1%E7%90%86%E8%8C%83%E5%9B%B4 +func (r *Client) GetRangeCustomerStrategy(req *GetRangeCustomerStrategyRequest) (*GetRangeCustomerStrategyResponse, 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(getRangeCustomerStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &GetRangeCustomerStrategyResponse{} + err = util.DecodeWithError(response, result, "GetRangeCustomerStrategy") + return result, err +} + +// CreateCustomerStrategyRequest 创建新的规则组请求 +type CreateCustomerStrategyRequest struct { + ParentID int `json:"parent_id"` + StrategyName string `json:"strategy_name"` + AdminList []string `json:"admin_list"` + Privilege Privilege `json:"privilege"` + Range []Range `json:"range"` +} + +// CreateCustomerStrategyResponse 创建新的规则组响应 +type CreateCustomerStrategyResponse struct { + util.CommonError + StrategyID int `json:"strategy_id"` +} + +// CreateCustomerStrategy 创建新的规则组 +// @see https://developer.work.weixin.qq.com/document/path/94883#%E5%88%9B%E5%BB%BA%E6%96%B0%E7%9A%84%E8%A7%84%E5%88%99%E7%BB%84 +func (r *Client) CreateCustomerStrategy(req *CreateCustomerStrategyRequest) (*CreateCustomerStrategyResponse, 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(createCustomerStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &CreateCustomerStrategyResponse{} + err = util.DecodeWithError(response, result, "CreateCustomerStrategy") + return result, err +} + +// EditCustomerStrategyRequest 编辑规则组及其管理范围请求 +type EditCustomerStrategyRequest struct { + StrategyID int `json:"strategy_id"` + StrategyName string `json:"strategy_name"` + AdminList []string `json:"admin_list"` + Privilege Privilege `json:"privilege"` + RangeAdd []Range `json:"range_add"` + RangeDel []Range `json:"range_del"` +} + +// EditCustomerStrategy 编辑规则组及其管理范围 +// see https://developer.work.weixin.qq.com/document/path/94883#%E7%BC%96%E8%BE%91%E8%A7%84%E5%88%99%E7%BB%84%E5%8F%8A%E5%85%B6%E7%AE%A1%E7%90%86%E8%8C%83%E5%9B%B4 +func (r *Client) EditCustomerStrategy(req *EditCustomerStrategyRequest) 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(editCustomerStrategyURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "EditCustomerStrategy") +} + +// DelCustomerStrategyRequest 删除规则组请求 +type DelCustomerStrategyRequest struct { + StrategyID int `json:"strategy_id"` +} + +// DelCustomerStrategy 删除规则组 +// see https://developer.work.weixin.qq.com/document/path/94883#%E5%88%A0%E9%99%A4%E8%A7%84%E5%88%99%E7%BB%84 +func (r *Client) DelCustomerStrategy(req *DelCustomerStrategyRequest) 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(delCustomerStrategyURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelCustomerStrategy") +} diff --git a/work/externalcontact/follow_user.go b/work/externalcontact/follow_user.go new file mode 100644 index 0000000..43d9c49 --- /dev/null +++ b/work/externalcontact/follow_user.go @@ -0,0 +1,35 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // fetchFollowUserListURL 获取配置了客户联系功能的成员列表 + fetchFollowUserListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list" +) + +// followerUserResponse 客户联系功能的成员列表响应 +type followerUserResponse struct { + util.CommonError + FollowUser []string `json:"follow_user"` +} + +// GetFollowUserList 获取配置了客户联系功能的成员列表 +// @see https://developer.work.weixin.qq.com/document/path/92571 +func (r *Client) GetFollowUserList() ([]string, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + response, err = util.HTTPGet(fmt.Sprintf("%s?access_token=%s", fetchFollowUserListURL, accessToken)) + if err != nil { + return nil, err + } + var result followerUserResponse + err = util.DecodeWithError(response, &result, "GetFollowUserList") + return result.FollowUser, err +} diff --git a/work/externalcontact/groupchat.go b/work/externalcontact/groupchat.go new file mode 100644 index 0000000..018c397 --- /dev/null +++ b/work/externalcontact/groupchat.go @@ -0,0 +1,138 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +// opengIDToChatIDURL 客户群opengid转换URL +const opengIDToChatIDURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/opengid_to_chatid" + +type ( + //GroupChatListRequest 获取客户群列表的请求参数 + GroupChatListRequest struct { + StatusFilter int `json:"status_filter"` // 非必填 客户群跟进状态过滤。0 - 所有列表(即不过滤) 1 - 离职待继承 2 - 离职继承中 3 - 离职继承完成 + OwnerFilter OwnerFilter `json:"owner_filter"` //非必填 群主过滤。如果不填,表示获取应用可见范围内全部群主的数据(但是不建议这么用,如果可见范围人数超过1000人,为了防止数据包过大,会报错 81017) + Cursor string `json:"cursor"` //非必填 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用不填 + Limit int `json:"limit"` //必填 分页,预期请求的数据量,取值范围 1 ~ 1000 + } + + //GroupChatList 客户群列表 + GroupChatList struct { + ChatID string `json:"chat_id"` + Status int `json:"status"` + } + //GroupChatListResponse 获取客户群列表的返回值 + GroupChatListResponse struct { + util.CommonError + GroupChatList []GroupChatList `json:"group_chat_list"` + NextCursor string `json:"next_cursor"` //游标 + } +) + +// GetGroupChatList 获取客户群列表 +// @see https://developer.work.weixin.qq.com/document/path/92120 +func (r *Client) GetGroupChatList(req *GroupChatListRequest) (*GroupChatListResponse, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + response, err = util.PostJSON(fmt.Sprintf("%s/list?access_token=%s", groupChatURL, accessToken), req) + if err != nil { + return nil, err + } + result := &GroupChatListResponse{} + err = util.DecodeWithError(response, result, "GetGroupChatList") + return result, err +} + +type ( + //GroupChatDetailRequest 客户群详情 请求参数 + GroupChatDetailRequest struct { + ChatID string `json:"chat_id"` + NeedName int `json:"need_name"` + } + //Invitor 邀请者 + Invitor struct { + UserID string `json:"userid"` //邀请者的userid + } + //GroupChatMember 群成员 + GroupChatMember struct { + UserID string `json:"userid"` //群成员id + Type int `json:"type"` //成员类型。 1 - 企业成员 2 - 外部联系人 + JoinTime int `json:"join_time"` //入群时间 + JoinScene int `json:"join_scene"` //入群方式 1 - 由群成员邀请入群(直接邀请入群) 2 - 由群成员邀请入群(通过邀请链接入群) 3 - 通过扫描群二维码入群 + Invitor Invitor `json:"invitor,omitempty"` //邀请者。目前仅当是由本企业内部成员邀请入群时会返回该值 + GroupNickname string `json:"group_nickname"` //在群里的昵称 + Name string `json:"name"` //名字。仅当 need_name = 1 时返回 如果是微信用户,则返回其在微信中设置的名字 如果是企业微信联系人,则返回其设置对外展示的别名或实名 + UnionID string `json:"unionid,omitempty"` //外部联系人在微信开放平台的唯一身份标识(微信unionid),通过此字段企业可将外部联系人与公众号/小程序用户关联起来。仅当群成员类型是微信用户(包括企业成员未添加好友),且企业绑定了微信开发者ID有此字段(查看绑定方法)。第三方不可获取,上游企业不可获取下游企业客户的unionid字段 + State string `json:"state,omitempty"` //如果在配置入群方式时,配置了state参数,那么在获取客户群详情时,通过该方式入群的成员,会额外获取到相应的state参数 + } + //GroupChatAdmin 群管理员 + GroupChatAdmin struct { + UserID string `json:"userid"` //群管理员userid + } + //GroupChat 客户群详情 + GroupChat struct { + ChatID string `json:"chat_id"` //客户群ID + Name string `json:"name"` //群名 + Owner string `json:"owner"` //群主ID + CreateTime int64 `json:"create_time"` //群的创建时间 + Notice string `json:"notice"` //群公告 + MemberList []GroupChatMember `json:"member_list"` //群成员列表 + AdminList []GroupChatAdmin `json:"admin_list"` //群管理员列表 + } + //GroupChatDetailResponse 客户群详情 返回值 + GroupChatDetailResponse struct { + util.CommonError + GroupChat GroupChat `json:"group_chat"` //客户群详情 + } +) + +// GetGroupChatDetail 获取客户群详情 +// @see https://developer.work.weixin.qq.com/document/path/92122 +func (r *Client) GetGroupChatDetail(req *GroupChatDetailRequest) (*GroupChatDetailResponse, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + response, err = util.PostJSON(fmt.Sprintf("%s/get?access_token=%s", groupChatURL, accessToken), req) + if err != nil { + return nil, err + } + result := &GroupChatDetailResponse{} + err = util.DecodeWithError(response, result, "GetGroupChatDetail") + return result, err +} + +type ( + //OpengIDToChatIDRequest 客户群opengid转换 请求参数 + OpengIDToChatIDRequest struct { + OpengID string `json:"opengid"` + } + //OpengIDToChatIDResponse 客户群opengid转换 返回值 + OpengIDToChatIDResponse struct { + util.CommonError + ChatID string `json:"chat_id"` //客户群ID + } +) + +// OpengIDToChatID 客户群opengid转换 +// @see https://developer.work.weixin.qq.com/document/path/94828 +func (r *Client) OpengIDToChatID(req *OpengIDToChatIDRequest) (*OpengIDToChatIDResponse, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + response, err = util.PostJSON(fmt.Sprintf("%s?access_token=%s", opengIDToChatIDURL, accessToken), req) + if err != nil { + return nil, err + } + result := &OpengIDToChatIDResponse{} + err = util.DecodeWithError(response, result, "GetGroupChatDetail") + return result, err +} diff --git a/work/externalcontact/join_way.go b/work/externalcontact/join_way.go new file mode 100644 index 0000000..b9c1140 --- /dev/null +++ b/work/externalcontact/join_way.go @@ -0,0 +1,142 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +// groupChatURL 客户群 +const groupChatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat" + +type ( + // AddJoinWayRequest 添加群配置请求参数 + AddJoinWayRequest struct { + Scene int `json:"scene"` // 必填 1 - 群的小程序插件,2 - 群的二维码插件 + Remark string `json:"remark"` //非必填 联系方式的备注信息,用于助记,超过30个字符将被截断 + AutoCreateRoom int `json:"auto_create_room"` //非必填 当群满了后,是否自动新建群。0-否;1-是。 默认为1 + RoomBaseName string `json:"room_base_name"` //非必填 自动建群的群名前缀,当auto_create_room为1时有效。最长40个utf8字符 + RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效 + ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法 + State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符 + } + + // AddJoinWayResponse 添加群配置返回值 + AddJoinWayResponse struct { + util.CommonError + ConfigID string `json:"config_id"` + } +) + +// AddJoinWay 加入群聊 +// @see https://developer.work.weixin.qq.com/document/path/92229 +func (r *Client) AddJoinWay(req *AddJoinWayRequest) (*AddJoinWayResponse, error) { + var ( + accessToken string + err error + response []byte + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + response, err = util.PostJSON(fmt.Sprintf("%s/add_join_way?access_token=%s", groupChatURL, accessToken), req) + if err != nil { + return nil, err + } + result := &AddJoinWayResponse{} + err = util.DecodeWithError(response, result, "AddJoinWay") + return result, err +} + +type ( + //JoinWayConfigRequest 获取或删除群配置的请求参数 + JoinWayConfigRequest struct { + ConfigID string `json:"config_id"` + } + + //JoinWay 群配置 + JoinWay struct { + ConfigID string `json:"config_id"` + Scene int `json:"scene"` + Remark string `json:"remark"` + AutoCreateRoom int `json:"auto_create_room"` + RoomBaseName string `json:"room_base_name"` + RoomBaseID int `json:"room_base_id"` + ChatIDList []string `json:"chat_id_list"` + QrCode string `json:"qr_code"` + State string `json:"state"` + } + //GetJoinWayResponse 获取群配置的返回值 + GetJoinWayResponse struct { + util.CommonError + JoinWay JoinWay `json:"join_way"` + } +) + +// GetJoinWay 获取客户群进群方式配置 +// @see https://developer.work.weixin.qq.com/document/path/92229 +func (r *Client) GetJoinWay(req *JoinWayConfigRequest) (*GetJoinWayResponse, error) { + var ( + accessToken string + err error + response []byte + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + response, err = util.PostJSON(fmt.Sprintf("%s/get_join_way?access_token=%s", groupChatURL, accessToken), req) + if err != nil { + return nil, err + } + result := &GetJoinWayResponse{} + err = util.DecodeWithError(response, result, "GetJoinWay") + return result, err +} + +// UpdateJoinWayRequest 更新群配置的请求参数 +type UpdateJoinWayRequest struct { + ConfigID string `json:"config_id"` + Scene int `json:"scene"` // 必填 1 - 群的小程序插件,2 - 群的二维码插件 + Remark string `json:"remark"` //非必填 联系方式的备注信息,用于助记,超过30个字符将被截断 + AutoCreateRoom int `json:"auto_create_room"` //非必填 当群满了后,是否自动新建群。0-否;1-是。 默认为1 + RoomBaseName string `json:"room_base_name"` //非必填 自动建群的群名前缀,当auto_create_room为1时有效。最长40个utf8字符 + RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效 + ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法 + State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符 +} + +// UpdateJoinWay 更新客户群进群方式配置 +// @see https://developer.work.weixin.qq.com/document/path/92229 +func (r *Client) UpdateJoinWay(req *UpdateJoinWayRequest) error { + var ( + accessToken string + err error + response []byte + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return err + } + response, err = util.PostJSON(fmt.Sprintf("%s/update_join_way?access_token=%s", groupChatURL, accessToken), req) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "UpdateJoinWay") +} + +// DelJoinWay 删除客户群进群方式配置 +// @see https://developer.work.weixin.qq.com/document/path/92229 +func (r *Client) DelJoinWay(req *JoinWayConfigRequest) error { + var ( + accessToken string + err error + response []byte + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return err + } + response, err = util.PostJSON(fmt.Sprintf("%s/del_join_way?access_token=%s", groupChatURL, accessToken), req) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelJoinWay") +} diff --git a/work/externalcontact/moment.go b/work/externalcontact/moment.go new file mode 100644 index 0000000..8086fa6 --- /dev/null +++ b/work/externalcontact/moment.go @@ -0,0 +1,636 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // addMomentTaskURL 创建发表任务 + addMomentTaskURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_moment_task?access_token=%s" + // getMomentTaskResultURL 获取任务创建结果 + getMomentTaskResultURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_moment_task_result?access_token=%s&jobid=%s" + // cancelMomentTaskURL 停止发表企业朋友圈 + cancelMomentTaskURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/cancel_moment_task?access_token=%s" + // getMomentListURL 获取企业全部的发表列表 + getMomentListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_moment_list?access_token=%s" + // getMomentTaskURL 获取客户朋友圈企业发表的列表 + getMomentTaskURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_moment_task?access_token=%s" + // getMomentCustomerListURL 获取客户朋友圈发表时选择的可见范围 + getMomentCustomerListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_moment_customer_list?access_token=%s" + // getMomentSendResultURL 获取客户朋友圈发表后的可见客户列表 + getMomentSendResultURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_moment_send_result?access_token=%s" + // getMomentCommentsURL 获取客户朋友圈的互动数据 + getMomentCommentsURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_moment_comments?access_token=%s" + // listMomentStrategyURL 获取规则组列表 + listMomentStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/moment_strategy/list?access_token=%s" + // getMomentStrategyURL 获取规则组详情 + getMomentStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/moment_strategy/get?access_token=%s" + // getRangeMomentStrategyURL 获取规则组管理范围 + getRangeMomentStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/moment_strategy/get_range?access_token=%s" + // createMomentStrategyURL 创建新的规则组 + createMomentStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/moment_strategy/create?access_token=%s" + // editMomentStrategyURL 编辑规则组及其管理范围 + editMomentStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/moment_strategy/edit?access_token=%s" + // delMomentStrategyURL 删除规则组 + delMomentStrategyURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/moment_strategy/del?access_token=%s" +) + +// AddMomentTaskRequest 创建发表任务请求 +type AddMomentTaskRequest struct { + Text MomentTaskText `json:"text"` + Attachments []MomentTaskAttachment `json:"attachments"` + VisibleRange MomentVisibleRange `json:"visible_range"` +} + +// MomentTaskText 发表任务文本消息 +type MomentTaskText struct { + Content string `json:"content"` +} + +// MomentTaskImage 发表任务图片消息 +type MomentTaskImage struct { + MediaID string `json:"media_id"` +} + +// MomentTaskVideo 发表任务视频消息 +type MomentTaskVideo struct { + MediaID string `json:"media_id"` +} + +// MomentTaskLink 发表任务图文消息 +type MomentTaskLink struct { + Title string `json:"title"` + URL string `json:"url"` + MediaID string `json:"media_id"` +} + +// MomentTaskAttachment 发表任务附件 +type MomentTaskAttachment struct { + MsgType string `json:"msgtype"` + Image MomentTaskImage `json:"image,omitempty"` + Video MomentTaskVideo `json:"video,omitempty"` + Link MomentTaskLink `json:"link,omitempty"` +} + +// MomentVisibleRange 朋友圈指定的发表范围 +type MomentVisibleRange struct { + SenderList MomentSenderList `json:"sender_list"` + ExternalContactList MomentExternalContactList `json:"external_contact_list"` +} + +// MomentSenderList 发表任务的执行者列表 +type MomentSenderList struct { + UserList []string `json:"user_list"` + DepartmentList []int `json:"department_list"` +} + +// MomentExternalContactList 可见到该朋友圈的客户列表 +type MomentExternalContactList struct { + TagList []string `json:"tag_list"` +} + +// AddMomentTaskResponse 创建发表任务响应 +type AddMomentTaskResponse struct { + util.CommonError + JobID string `json:"jobid"` +} + +// AddMomentTask 创建发表任务 +// see https://developer.work.weixin.qq.com/document/path/95094#%E5%88%9B%E5%BB%BA%E5%8F%91%E8%A1%A8%E4%BB%BB%E5%8A%A1 +func (r *Client) AddMomentTask(req *AddMomentTaskRequest) (*AddMomentTaskResponse, 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(addMomentTaskURL, accessToken), req); err != nil { + return nil, err + } + result := &AddMomentTaskResponse{} + err = util.DecodeWithError(response, result, "AddMomentTask") + return result, err +} + +// GetMomentTaskResultResponse 获取任务创建结果响应 +type GetMomentTaskResultResponse struct { + util.CommonError + Status int `json:"status"` + Type string `json:"type"` + Result MomentTaskResult `json:"result"` +} + +// MomentTaskResult 任务创建结果 +type MomentTaskResult struct { + ErrCode int64 `json:"errcode"` + ErrMsg string `json:"errmsg"` + MomentID string `json:"moment_id"` + InvalidSenderList MomentInvalidSenderList `json:"invalid_sender_list"` + InvalidExternalContactList MomentInvalidExternalContactList `json:"invalid_external_contact_list"` +} + +// MomentInvalidSenderList 不合法的执行者列表 +type MomentInvalidSenderList struct { + UserList []string `json:"user_list"` + DepartmentList []int `json:"department_list"` +} + +// MomentInvalidExternalContactList 不合法的可见到该朋友圈的客户列表 +type MomentInvalidExternalContactList struct { + TagList []string `json:"tag_list"` +} + +// GetMomentTaskResult 获取任务创建结果 +// see https://developer.work.weixin.qq.com/document/path/95094#%E8%8E%B7%E5%8F%96%E4%BB%BB%E5%8A%A1%E5%88%9B%E5%BB%BA%E7%BB%93%E6%9E%9C +func (r *Client) GetMomentTaskResult(jobID string) (*GetMomentTaskResultResponse, 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(getMomentTaskResultURL, accessToken, jobID)); err != nil { + return nil, err + } + result := &GetMomentTaskResultResponse{} + err = util.DecodeWithError(response, result, "GetMomentTaskResult") + return result, err +} + +// CancelMomentTaskRequest 停止发表企业朋友圈请求 +type CancelMomentTaskRequest struct { + MomentID string `json:"moment_id"` +} + +// CancelMomentTask 停止发表企业朋友圈 +// see https://developer.work.weixin.qq.com/document/path/97612 +func (r *Client) CancelMomentTask(req *CancelMomentTaskRequest) 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(cancelMomentTaskURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "CancelMomentTask") +} + +// GetMomentListRequest 获取企业全部的发表列表请求 +type GetMomentListRequest struct { + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Creator string `json:"creator"` + FilterType int `json:"filter_type"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// GetMomentListResponse 获取企业全部的发表列表响应 +type GetMomentListResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + MomentList []MomentItem `json:"moment_list"` +} + +// MomentItem 朋友圈 +type MomentItem struct { + MomentID string `json:"moment_id"` + Creator string `json:"creator"` + CreateTime int64 `json:"create_time"` + CreateType int `json:"create_type"` + VisibleType int `json:"visible_type"` + Text MomentText `json:"text"` + Image []MomentImage `json:"image"` + Video MomentVideo `json:"video"` + Link MomentLink `json:"link"` + Location MomentLocation `json:"location"` +} + +// MomentText 朋友圈文本消息 +type MomentText struct { + Content string `json:"content"` +} + +// MomentImage 朋友圈图片 +type MomentImage struct { + MediaID string `json:"media_id"` +} + +// MomentVideo 朋友圈视频 +type MomentVideo struct { + MediaID string `json:"media_id"` + ThumbMediaID string `json:"thumb_media_id"` +} + +// MomentLink 朋友圈网页链接 +type MomentLink struct { + Title string `json:"title"` + URL string `json:"url"` +} + +// MomentLocation 朋友圈地理位置 +type MomentLocation struct { + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + Name string `json:"name"` +} + +// GetMomentList 获取企业全部的发表列表 +// see https://developer.work.weixin.qq.com/document/path/93333#%E8%8E%B7%E5%8F%96%E4%BC%81%E4%B8%9A%E5%85%A8%E9%83%A8%E7%9A%84%E5%8F%91%E8%A1%A8%E5%88%97%E8%A1%A8 +func (r *Client) GetMomentList(req *GetMomentListRequest) (*GetMomentListResponse, 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(getMomentListURL, accessToken), req); err != nil { + return nil, err + } + result := &GetMomentListResponse{} + err = util.DecodeWithError(response, result, "GetMomentList") + return result, err +} + +// GetMomentTaskRequest 获取客户朋友圈企业发表的列表请求 +type GetMomentTaskRequest struct { + MomentID string `json:"moment_id"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// GetMomentTaskResponse 获取客户朋友圈企业发表的列表响应 +type GetMomentTaskResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + TaskList []MomentTask `json:"task_list"` +} + +// MomentTask 发表任务 +type MomentTask struct { + UserID string `json:"userid"` + PublishStatus int `json:"publish_status"` +} + +// GetMomentTask 获取客户朋友圈企业发表的列表 +// see https://developer.work.weixin.qq.com/document/path/93333#%E8%8E%B7%E5%8F%96%E5%AE%A2%E6%88%B7%E6%9C%8B%E5%8F%8B%E5%9C%88%E4%BC%81%E4%B8%9A%E5%8F%91%E8%A1%A8%E7%9A%84%E5%88%97%E8%A1%A8 +func (r *Client) GetMomentTask(req *GetMomentTaskRequest) (*GetMomentTaskResponse, 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(getMomentTaskURL, accessToken), req); err != nil { + return nil, err + } + result := &GetMomentTaskResponse{} + err = util.DecodeWithError(response, result, "GetMomentTask") + return result, err +} + +// GetMomentCustomerListRequest 获取客户朋友圈发表时选择的可见范围请求 +type GetMomentCustomerListRequest struct { + MomentID string `json:"moment_id"` + UserID string `json:"userid"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// GetMomentCustomerListResponse 获取客户朋友圈发表时选择的可见范围响应 +type GetMomentCustomerListResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + CustomerList []MomentCustomer `json:"customer_list"` +} + +// MomentCustomer 成员可见客户列表 +type MomentCustomer struct { + UserID string `json:"userid"` + ExternalUserID string `json:"external_userid"` +} + +// GetMomentCustomerList 获取客户朋友圈发表时选择的可见范围 +// see https://developer.work.weixin.qq.com/document/path/93333#%E8%8E%B7%E5%8F%96%E5%AE%A2%E6%88%B7%E6%9C%8B%E5%8F%8B%E5%9C%88%E5%8F%91%E8%A1%A8%E6%97%B6%E9%80%89%E6%8B%A9%E7%9A%84%E5%8F%AF%E8%A7%81%E8%8C%83%E5%9B%B4 +func (r *Client) GetMomentCustomerList(req *GetMomentCustomerListRequest) (*GetMomentCustomerListResponse, 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(getMomentCustomerListURL, accessToken), req); err != nil { + return nil, err + } + result := &GetMomentCustomerListResponse{} + err = util.DecodeWithError(response, result, "GetMomentCustomerList") + return result, err +} + +// GetMomentSendResultRequest 获取客户朋友圈发表后的可见客户列表请求 +type GetMomentSendResultRequest struct { + MomentID string `json:"moment_id"` + UserID string `json:"userid"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// GetMomentSendResultResponse 获取客户朋友圈发表后的可见客户列表响应 +type GetMomentSendResultResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + CustomerList []MomentSendCustomer `json:"customer_list"` +} + +// MomentSendCustomer 成员发送成功客户 +type MomentSendCustomer struct { + ExternalUserID string `json:"external_userid"` +} + +// GetMomentSendResult 获取客户朋友圈发表后的可见客户列表 +// see https://developer.work.weixin.qq.com/document/path/93333#%E8%8E%B7%E5%8F%96%E5%AE%A2%E6%88%B7%E6%9C%8B%E5%8F%8B%E5%9C%88%E5%8F%91%E8%A1%A8%E5%90%8E%E7%9A%84%E5%8F%AF%E8%A7%81%E5%AE%A2%E6%88%B7%E5%88%97%E8%A1%A8 +func (r *Client) GetMomentSendResult(req *GetMomentSendResultRequest) (*GetMomentSendResultResponse, 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(getMomentSendResultURL, accessToken), req); err != nil { + return nil, err + } + result := &GetMomentSendResultResponse{} + err = util.DecodeWithError(response, result, "GetMomentSendResult") + return result, err +} + +// GetMomentCommentsRequest 获取客户朋友圈的互动数据请求 +type GetMomentCommentsRequest struct { + MomentID string `json:"moment_id"` + UserID string `json:"userid"` +} + +// GetMomentCommentsResponse 获取客户朋友圈的互动数据响应 +type GetMomentCommentsResponse struct { + util.CommonError + CommentList []MomentComment `json:"comment_list"` + LikeList []MomentLike `json:"like_list"` +} + +// MomentComment 朋友圈评论 +type MomentComment struct { + ExternalUserID string `json:"external_userid,omitempty"` + UserID string `json:"userid,omitempty"` + CreateTime int64 `json:"create_time"` +} + +// MomentLike 朋友圈点赞 +type MomentLike struct { + ExternalUserID string `json:"external_userid,omitempty"` + UserID string `json:"userid,omitempty"` + CreateTime int64 `json:"create_time"` +} + +// GetMomentComments 获取客户朋友圈的互动数据 +// see https://developer.work.weixin.qq.com/document/path/93333#%E8%8E%B7%E5%8F%96%E5%AE%A2%E6%88%B7%E6%9C%8B%E5%8F%8B%E5%9C%88%E7%9A%84%E4%BA%92%E5%8A%A8%E6%95%B0%E6%8D%AE +func (r *Client) GetMomentComments(req *GetMomentCommentsRequest) (*GetMomentCommentsResponse, 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(getMomentCommentsURL, accessToken), req); err != nil { + return nil, err + } + result := &GetMomentCommentsResponse{} + err = util.DecodeWithError(response, result, "GetMomentComments") + return result, err +} + +// ListMomentStrategyRequest 获取规则组列表请求 +type ListMomentStrategyRequest struct { + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// ListMomentStrategyResponse 获取规则组列表响应 +type ListMomentStrategyResponse struct { + util.CommonError + Strategy []MomentStrategyID `json:"strategy"` + NextCursor string `json:"next_cursor"` +} + +// MomentStrategyID 规则组ID +type MomentStrategyID struct { + StrategyID int `json:"strategy_id"` +} + +// ListMomentStrategy 获取规则组列表 +// see https://developer.work.weixin.qq.com/document/path/94890#%E8%8E%B7%E5%8F%96%E8%A7%84%E5%88%99%E7%BB%84%E5%88%97%E8%A1%A8 +func (r *Client) ListMomentStrategy(req *ListMomentStrategyRequest) (*ListMomentStrategyResponse, 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(listMomentStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &ListMomentStrategyResponse{} + err = util.DecodeWithError(response, result, "ListMomentStrategy") + return result, err +} + +// GetMomentStrategyRequest 获取规则组详情请求 +type GetMomentStrategyRequest struct { + StrategyID int `json:"strategy_id"` +} + +// GetMomentStrategyResponse 获取规则组详情响应 +type GetMomentStrategyResponse struct { + util.CommonError + Strategy MomentStrategy `json:"strategy"` +} + +// MomentStrategy 规则组 +type MomentStrategy struct { + StrategyID int `json:"strategy_id"` + ParentID int `json:"parent_id"` + StrategyName string `json:"strategy_name"` + CreateTime int64 `json:"create_time"` + AdminList []string `json:"admin_list"` + Privilege MomentPrivilege `json:"privilege"` +} + +// MomentPrivilege 规则组权限 +type MomentPrivilege struct { + ViewMomentList bool `json:"view_moment_list"` + SendMoment bool `json:"send_moment"` + ManageMomentCoverAndSign bool `json:"manage_moment_cover_and_sign"` +} + +// GetMomentStrategy 获取规则组详情 +// see https://developer.work.weixin.qq.com/document/path/94890#%E8%8E%B7%E5%8F%96%E8%A7%84%E5%88%99%E7%BB%84%E8%AF%A6%E6%83%85 +func (r *Client) GetMomentStrategy(req *GetMomentStrategyRequest) (*GetMomentStrategyResponse, 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(getMomentStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &GetMomentStrategyResponse{} + err = util.DecodeWithError(response, result, "GetMomentStrategy") + return result, err +} + +// GetRangeMomentStrategyRequest 获取规则组管理范围请求 +type GetRangeMomentStrategyRequest struct { + StrategyID int `json:"strategy_id"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} + +// GetRangeMomentStrategyResponse 获取规则组管理范围响应 +type GetRangeMomentStrategyResponse struct { + util.CommonError + Range []RangeMomentStrategy `json:"range"` + NextCursor string `json:"next_cursor"` +} + +// RangeMomentStrategy 管理范围内配置的成员或部门 +type RangeMomentStrategy struct { + Type int `json:"type"` + UserID string `json:"userid,omitempty"` + PartyID int `json:"partyid,omitempty"` +} + +// GetRangeMomentStrategy 获取规则组管理范围 +// see https://developer.work.weixin.qq.com/document/path/94890#%E8%8E%B7%E5%8F%96%E8%A7%84%E5%88%99%E7%BB%84%E7%AE%A1%E7%90%86%E8%8C%83%E5%9B%B4 +func (r *Client) GetRangeMomentStrategy(req *GetRangeMomentStrategyRequest) (*GetRangeMomentStrategyResponse, 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(getRangeMomentStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &GetRangeMomentStrategyResponse{} + err = util.DecodeWithError(response, result, "GetRangeMomentStrategy") + return result, err +} + +// CreateMomentStrategyRequest 创建新的规则组请求 +type CreateMomentStrategyRequest struct { + ParentID int `json:"parent_id"` + StrategyName string `json:"strategy_name"` + AdminList []string `json:"admin_list"` + Privilege MomentPrivilege `json:"privilege"` + Range []RangeMomentStrategy `json:"range"` +} + +// CreateMomentStrategyResponse 创建新的规则组响应 +type CreateMomentStrategyResponse struct { + util.CommonError + StrategyID int `json:"strategy_id"` +} + +// CreateMomentStrategy 创建新的规则组 +// see https://developer.work.weixin.qq.com/document/path/94890#%E5%88%9B%E5%BB%BA%E6%96%B0%E7%9A%84%E8%A7%84%E5%88%99%E7%BB%84 +func (r *Client) CreateMomentStrategy(req *CreateMomentStrategyRequest) (*CreateMomentStrategyResponse, 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(createMomentStrategyURL, accessToken), req); err != nil { + return nil, err + } + result := &CreateMomentStrategyResponse{} + err = util.DecodeWithError(response, result, "CreateMomentStrategy") + return result, err +} + +// EditMomentStrategyRequest 编辑规则组及其管理范围请求 +type EditMomentStrategyRequest struct { + StrategyID int `json:"strategy_id"` + StrategyName string `json:"strategy_name"` + AdminList []string `json:"admin_list"` + Privilege MomentPrivilege `json:"privilege"` + RangeAdd []RangeMomentStrategy `json:"range_add"` + RangeDel []RangeMomentStrategy `json:"range_del"` +} + +// EditMomentStrategy 编辑规则组及其管理范围 +// see https://developer.work.weixin.qq.com/document/path/94890#%E7%BC%96%E8%BE%91%E8%A7%84%E5%88%99%E7%BB%84%E5%8F%8A%E5%85%B6%E7%AE%A1%E7%90%86%E8%8C%83%E5%9B%B4 +func (r *Client) EditMomentStrategy(req *EditMomentStrategyRequest) 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(editMomentStrategyURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "EditMomentStrategy") +} + +// DelMomentStrategyRequest 删除规则组请求 +type DelMomentStrategyRequest struct { + StrategyID int `json:"strategy_id"` +} + +// DelMomentStrategy 删除规则组 +// see https://developer.work.weixin.qq.com/document/path/94890#%E5%88%A0%E9%99%A4%E8%A7%84%E5%88%99%E7%BB%84 +func (r *Client) DelMomentStrategy(req *DelMomentStrategyRequest) 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(delMomentStrategyURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelMomentStrategy") +} diff --git a/work/externalcontact/msg.go b/work/externalcontact/msg.go new file mode 100644 index 0000000..ab28370 --- /dev/null +++ b/work/externalcontact/msg.go @@ -0,0 +1,466 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // addMsgTemplateURL 创建企业群发 + addMsgTemplateURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template?access_token=%s" + // getGroupMsgListV2URL 获取群发记录列表 + getGroupMsgListV2URL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_groupmsg_list_v2?access_token=%s" + // getGroupMsgTaskURL 获取群发成员发送任务列表 + getGroupMsgTaskURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_groupmsg_task?access_token=%s" + // getGroupMsgSendResultURL 获取企业群发成员执行结果 + getGroupMsgSendResultURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_groupmsg_send_result?access_token=%s" + // sendWelcomeMsgURL 发送新客户欢迎语 + sendWelcomeMsgURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=%s" + // addGroupWelcomeTemplateURL 添加入群欢迎语素材 + addGroupWelcomeTemplateURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/group_welcome_template/add?access_token=%s" + // editGroupWelcomeTemplateURL 编辑入群欢迎语素材 + editGroupWelcomeTemplateURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/group_welcome_template/edit?access_token=%s" + // getGroupWelcomeTemplateURL 获取入群欢迎语素材 + getGroupWelcomeTemplateURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/group_welcome_template/get?access_token=%s" + // delGroupWelcomeTemplateURL 删除入群欢迎语素材 + delGroupWelcomeTemplateURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/group_welcome_template/del?access_token=%s" + // remindGroupMsgSendURL 提醒成员群发 + remindGroupMsgSendURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remind_groupmsg_send?access_token=%s" + // cancelGroupMsgSendURL 停止企业群发 + cancelGroupMsgSendURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/cancel_groupmsg_send?access_token=%s" +) + +// AddMsgTemplateRequest 创建企业群发请求 +type AddMsgTemplateRequest struct { + ChatType string `json:"chat_type"` + ExternalUserID []string `json:"external_userid"` + Sender string `json:"sender,omitempty"` + Text MsgText `json:"text"` + Attachments []*Attachment `json:"attachments"` + AllowSelect bool `json:"allow_select,omitempty"` + ChatIDList []string `json:"chat_id_list,omitempty"` + TagFilter TagFilter `json:"tag_filter,omitempty"` +} + +type ( + // TagFilter 标签过滤 + TagFilter struct { + GroupList []TagGroupList `json:"group_list"` + } + + // TagGroupList 标签组 + TagGroupList struct { + TagList []string `json:"tag_list"` + } +) + +// MsgText 文本消息 +type MsgText struct { + Content string `json:"content"` +} + +type ( + // Attachment 附件 + Attachment struct { + MsgType string `json:"msgtype"` + Image AttachmentImg `json:"image,omitempty"` + Link AttachmentLink `json:"link,omitempty"` + MiniProgram AttachmentMiniProgram `json:"miniprogram,omitempty"` + Video AttachmentVideo `json:"video,omitempty"` + File AttachmentFile `json:"file,omitempty"` + } + // AttachmentImg 图片消息 + AttachmentImg struct { + MediaID string `json:"media_id"` + PicURL string `json:"pic_url"` + } + // AttachmentLink 图文消息 + AttachmentLink struct { + Title string `json:"title"` + PicURL string `json:"picurl"` + Desc string `json:"desc"` + URL string `json:"url"` + } + // AttachmentMiniProgram 小程序消息 + AttachmentMiniProgram struct { + Title string `json:"title"` + PicMediaID string `json:"pic_media_id"` + AppID string `json:"appid"` + Page string `json:"page"` + } + // AttachmentVideo 视频消息 + AttachmentVideo struct { + MediaID string `json:"media_id"` + } + // AttachmentFile 文件消息 + AttachmentFile struct { + MediaID string `json:"media_id"` + } +) + +// AddMsgTemplateResponse 创建企业群发响应 +type AddMsgTemplateResponse struct { + util.CommonError + FailList []string `json:"fail_list"` + MsgID string `json:"msgid"` +} + +// AddMsgTemplate 创建企业群发 +// see https://developer.work.weixin.qq.com/document/path/92135 +func (r *Client) AddMsgTemplate(req *AddMsgTemplateRequest) (*AddMsgTemplateResponse, 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(addMsgTemplateURL, accessToken), req); err != nil { + return nil, err + } + result := &AddMsgTemplateResponse{} + err = util.DecodeWithError(response, result, "AddMsgTemplate") + return result, err +} + +// GetGroupMsgListV2Request 获取群发记录列表请求 +type GetGroupMsgListV2Request struct { + ChatType string `json:"chat_type"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Creator string `json:"creator,omitempty"` + FilterType int `json:"filter_type"` + Limit int `json:"limit"` + Cursor string `json:"cursor"` +} + +// GetGroupMsgListV2Response 获取群发记录列表响应 +type GetGroupMsgListV2Response struct { + util.CommonError + NextCursor string `json:"next_cursor"` + GroupMsgList []*GroupMsg `json:"group_msg_list"` +} + +// GroupMsg 群发消息 +type GroupMsg struct { + MsgID string `json:"msgid"` + Creator string `json:"creator"` + CreateTime int64 `json:"create_time"` + CreateType int `json:"create_type"` + Text MsgText `json:"text"` + Attachments []*Attachment `json:"attachments"` +} + +// GetGroupMsgListV2 获取群发记录列表 +// see https://developer.work.weixin.qq.com/document/path/93338#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%8F%91%E8%AE%B0%E5%BD%95%E5%88%97%E8%A1%A8 +func (r *Client) GetGroupMsgListV2(req *GetGroupMsgListV2Request) (*GetGroupMsgListV2Response, 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(getGroupMsgListV2URL, accessToken), req); err != nil { + return nil, err + } + result := &GetGroupMsgListV2Response{} + err = util.DecodeWithError(response, result, "GetGroupMsgListV2") + return result, err +} + +// GetGroupMsgTaskRequest 获取群发成员发送任务列表请求 +type GetGroupMsgTaskRequest struct { + MsgID string `json:"msgid"` + Limit int `json:"limit"` + Cursor string `json:"cursor"` +} + +// GetGroupMsgTaskResponse 获取群发成员发送任务列表响应 +type GetGroupMsgTaskResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + TaskList []*Task `json:"task_list"` +} + +// Task 获取群发成员发送任务列表任务 +type Task struct { + UserID string `json:"userid"` + Status int `json:"status"` + SendTime int `json:"send_time"` +} + +// GetGroupMsgTask 获取群发成员发送任务列表 +// see https://developer.work.weixin.qq.com/document/path/93338#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%8F%91%E6%88%90%E5%91%98%E5%8F%91%E9%80%81%E4%BB%BB%E5%8A%A1%E5%88%97%E8%A1%A8 +func (r *Client) GetGroupMsgTask(req *GetGroupMsgTaskRequest) (*GetGroupMsgTaskResponse, 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(getGroupMsgTaskURL, accessToken), req); err != nil { + return nil, err + } + result := &GetGroupMsgTaskResponse{} + err = util.DecodeWithError(response, result, "GetGroupMsgTask") + return result, err +} + +// GetGroupMsgSendResultRequest 获取企业群发成员执行结果请求 +type GetGroupMsgSendResultRequest struct { + MsgID string `json:"msgid"` + UserID string `json:"userid"` + Limit int `json:"limit"` + Cursor string `json:"cursor"` +} + +// GetGroupMsgSendResultResponse 获取企业群发成员执行结果响应 +type GetGroupMsgSendResultResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + SendList []*Send `json:"send_list"` +} + +// Send 企业群发成员执行结果 +type Send struct { + ExternalUserID string `json:"external_userid"` + ChatID string `json:"chat_id"` + UserID string `json:"userid"` + Status int `json:"status"` + SendTime int `json:"send_time"` +} + +// GetGroupMsgSendResult 获取企业群发成员执行结果 +// see https://developer.work.weixin.qq.com/document/path/93338#%E8%8E%B7%E5%8F%96%E4%BC%81%E4%B8%9A%E7%BE%A4%E5%8F%91%E6%88%90%E5%91%98%E6%89%A7%E8%A1%8C%E7%BB%93%E6%9E%9C +func (r *Client) GetGroupMsgSendResult(req *GetGroupMsgSendResultRequest) (*GetGroupMsgSendResultResponse, 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(getGroupMsgSendResultURL, accessToken), req); err != nil { + return nil, err + } + result := &GetGroupMsgSendResultResponse{} + err = util.DecodeWithError(response, result, "GetGroupMsgSendResult") + return result, err +} + +// SendWelcomeMsgRequest 发送新客户欢迎语请求 +type SendWelcomeMsgRequest struct { + WelcomeCode string `json:"welcome_code"` + Text MsgText `json:"text"` + Attachments []*Attachment `json:"attachments"` +} + +// SendWelcomeMsgResponse 发送新客户欢迎语响应 +type SendWelcomeMsgResponse struct { + util.CommonError +} + +// SendWelcomeMsg 发送新客户欢迎语 +// see https://developer.work.weixin.qq.com/document/path/92137 +func (r *Client) SendWelcomeMsg(req *SendWelcomeMsgRequest) 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(sendWelcomeMsgURL, accessToken), req); err != nil { + return err + } + result := &SendWelcomeMsgResponse{} + return util.DecodeWithError(response, result, "SendWelcomeMsg") +} + +// AddGroupWelcomeTemplateRequest 添加入群欢迎语素材请求 +type AddGroupWelcomeTemplateRequest struct { + Text MsgText `json:"text"` + Image AttachmentImg `json:"image"` + Link AttachmentLink `json:"link"` + MiniProgram AttachmentMiniProgram `json:"miniprogram"` + File AttachmentFile `json:"file"` + Video AttachmentVideo `json:"video"` + AgentID int `json:"agentid"` + Notify int `json:"notify"` +} + +// AddGroupWelcomeTemplateResponse 添加入群欢迎语素材响应 +type AddGroupWelcomeTemplateResponse struct { + util.CommonError + TemplateID string `json:"template_id"` +} + +// AddGroupWelcomeTemplate 添加入群欢迎语素材 +// see https://developer.work.weixin.qq.com/document/path/92366#%E6%B7%BB%E5%8A%A0%E5%85%A5%E7%BE%A4%E6%AC%A2%E8%BF%8E%E8%AF%AD%E7%B4%A0%E6%9D%90 +func (r *Client) AddGroupWelcomeTemplate(req *AddGroupWelcomeTemplateRequest) (*AddGroupWelcomeTemplateResponse, 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(addGroupWelcomeTemplateURL, accessToken), req); err != nil { + return nil, err + } + result := &AddGroupWelcomeTemplateResponse{} + err = util.DecodeWithError(response, result, "AddGroupWelcomeTemplate") + return result, err +} + +// EditGroupWelcomeTemplateRequest 编辑入群欢迎语素材请求 +type EditGroupWelcomeTemplateRequest struct { + TemplateID string `json:"template_id"` + Text MsgText `json:"text"` + Image AttachmentImg `json:"image"` + Link AttachmentLink `json:"link"` + MiniProgram AttachmentMiniProgram `json:"miniprogram"` + File AttachmentFile `json:"file"` + Video AttachmentVideo `json:"video"` + AgentID int `json:"agentid"` +} + +// EditGroupWelcomeTemplateResponse 编辑入群欢迎语素材响应 +type EditGroupWelcomeTemplateResponse struct { + util.CommonError +} + +// EditGroupWelcomeTemplate 编辑入群欢迎语素材 +// see https://developer.work.weixin.qq.com/document/path/92366#%E7%BC%96%E8%BE%91%E5%85%A5%E7%BE%A4%E6%AC%A2%E8%BF%8E%E8%AF%AD%E7%B4%A0%E6%9D%90 +func (r *Client) EditGroupWelcomeTemplate(req *EditGroupWelcomeTemplateRequest) 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(editGroupWelcomeTemplateURL, accessToken), req); err != nil { + return err + } + result := &EditGroupWelcomeTemplateResponse{} + return util.DecodeWithError(response, result, "EditGroupWelcomeTemplate") +} + +// GetGroupWelcomeTemplateRequest 获取入群欢迎语素材请求 +type GetGroupWelcomeTemplateRequest struct { + TemplateID string `json:"template_id"` +} + +// GetGroupWelcomeTemplateResponse 获取入群欢迎语素材响应 +type GetGroupWelcomeTemplateResponse struct { + util.CommonError + Text MsgText `json:"text"` + Image AttachmentImg `json:"image"` + Link AttachmentLink `json:"link"` + MiniProgram AttachmentMiniProgram `json:"miniprogram"` + File AttachmentFile `json:"file"` + Video AttachmentVideo `json:"video"` +} + +// GetGroupWelcomeTemplate 获取入群欢迎语素材 +// see https://developer.work.weixin.qq.com/document/path/92366#%E8%8E%B7%E5%8F%96%E5%85%A5%E7%BE%A4%E6%AC%A2%E8%BF%8E%E8%AF%AD%E7%B4%A0%E6%9D%90 +func (r *Client) GetGroupWelcomeTemplate(req *GetGroupWelcomeTemplateRequest) (*GetGroupWelcomeTemplateResponse, 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(getGroupWelcomeTemplateURL, accessToken), req); err != nil { + return nil, err + } + result := &GetGroupWelcomeTemplateResponse{} + err = util.DecodeWithError(response, result, "GetGroupWelcomeTemplate") + return result, err +} + +// DelGroupWelcomeTemplateRequest 删除入群欢迎语素材请求 +type DelGroupWelcomeTemplateRequest struct { + TemplateID string `json:"template_id"` + AgentID int `json:"agentid"` +} + +// DelGroupWelcomeTemplateResponse 删除入群欢迎语素材响应 +type DelGroupWelcomeTemplateResponse struct { + util.CommonError +} + +// DelGroupWelcomeTemplate 删除入群欢迎语素材 +// see https://developer.work.weixin.qq.com/document/path/92366#%E5%88%A0%E9%99%A4%E5%85%A5%E7%BE%A4%E6%AC%A2%E8%BF%8E%E8%AF%AD%E7%B4%A0%E6%9D%90 +func (r *Client) DelGroupWelcomeTemplate(req *DelGroupWelcomeTemplateRequest) 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(delGroupWelcomeTemplateURL, accessToken), req); err != nil { + return err + } + result := &DelGroupWelcomeTemplateResponse{} + return util.DecodeWithError(response, result, "DelGroupWelcomeTemplate") +} + +// RemindGroupMsgSendRequest 提醒成员群发请求 +type RemindGroupMsgSendRequest struct { + MsgID string `json:"msgid"` +} + +// RemindGroupMsgSend 提醒成员群发 +// see https://developer.work.weixin.qq.com/document/path/97610 +func (r *Client) RemindGroupMsgSend(req *RemindGroupMsgSendRequest) 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(remindGroupMsgSendURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "RemindGroupMsgSend") +} + +// CancelGroupMsgSendRequest 停止企业群发请求 +type CancelGroupMsgSendRequest struct { + MsgID string `json:"msgid"` +} + +// CancelGroupMsgSend 提醒成员群发 +// see https://developer.work.weixin.qq.com/document/path/97611 +func (r *Client) CancelGroupMsgSend(req *CancelGroupMsgSendRequest) 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(cancelGroupMsgSendURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "CancelGroupMsgSend") +} diff --git a/work/externalcontact/statistic.go b/work/externalcontact/statistic.go new file mode 100644 index 0000000..e639062 --- /dev/null +++ b/work/externalcontact/statistic.go @@ -0,0 +1,167 @@ +package externalcontact + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // getUserBehaviorDataURL 获取「联系客户统计」数据 + getUserBehaviorDataURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_user_behavior_data" + // getGroupChatStatURL 获取「群聊数据统计」数据 按群主聚合的方式 + getGroupChatStatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/statistic" + // getGroupChatStatByDayURL 获取「群聊数据统计」数据 按自然日聚合的方式 + getGroupChatStatByDayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/statistic_group_by_day" +) + +type ( + // GetUserBehaviorRequest 获取「联系客户统计」数据请求 + GetUserBehaviorRequest struct { + UserID []string `json:"userid"` + PartyID []int `json:"partyid"` + StartTime int `json:"start_time"` + EndTime int `json:"end_time"` + } + // GetUserBehaviorResponse 获取「联系客户统计」数据响应 + GetUserBehaviorResponse struct { + util.CommonError + BehaviorData []BehaviorData `json:"behavior_data"` + } + // BehaviorData 联系客户统计数据 + BehaviorData struct { + StatTime int `json:"stat_time"` + ChatCnt int `json:"chat_cnt"` + MessageCnt int `json:"message_cnt"` + ReplyPercentage float64 `json:"reply_percentage"` + AvgReplyTime int `json:"avg_reply_time"` + NegativeFeedbackCnt int `json:"negative_feedback_cnt"` + NewApplyCnt int `json:"new_apply_cnt"` + NewContactCnt int `json:"new_contact_cnt"` + } +) + +// GetUserBehaviorData 获取「联系客户统计」数据 +// @see https://developer.work.weixin.qq.com/document/path/92132 +func (r *Client) GetUserBehaviorData(req *GetUserBehaviorRequest) ([]BehaviorData, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return nil, err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", getUserBehaviorDataURL, accessToken), string(jsonData)) + if err != nil { + return nil, err + } + var result GetUserBehaviorResponse + err = util.DecodeWithError(response, &result, "GetUserBehaviorData") + return result.BehaviorData, err +} + +type ( + // GetGroupChatStatRequest 获取「群聊数据统计」数据 按群主聚合的方式 请求 + GetGroupChatStatRequest struct { + DayBeginTime int `json:"day_begin_time"` + DayEndTime int `json:"day_end_time"` + OwnerFilter OwnerFilter `json:"owner_filter"` + OrderBy int `json:"order_by"` + OrderAsc int `json:"order_asc"` + Offset int `json:"offset"` + Limit int `json:"limit"` + } + // GetGroupChatStatResponse 获取「群聊数据统计」数据 按群主聚合的方式 响应 + GetGroupChatStatResponse struct { + util.CommonError + Total int `json:"total"` + NextOffset int `json:"next_offset"` + Items []GroupChatStatItem `json:"items"` + } + // GroupChatStatItem 群聊数据统计(按群主聚合)条目 + GroupChatStatItem struct { + Owner string `json:"owner"` + Data GroupChatStatItemData `json:"data"` + } +) + +// OwnerFilter 群主过滤 +type OwnerFilter struct { + UseridList []string `json:"userid_list"` +} + +// GroupChatStatItemData 群聊数据统计条目数据 +type GroupChatStatItemData struct { + NewChatCnt int `json:"new_chat_cnt"` + ChatTotal int `json:"chat_total"` + ChatHasMsg int `json:"chat_has_msg"` + NewMemberCnt int `json:"new_member_cnt"` + MemberTotal int `json:"member_total"` + MemberHasMsg int `json:"member_has_msg"` + MsgTotal int `json:"msg_total"` + MigrateTraineeChatCnt int `json:"migrate_trainee_chat_cnt"` +} + +// GetGroupChatStat 获取「群聊数据统计」数据 按群主聚合的方式 +// @see https://developer.work.weixin.qq.com/document/path/92133 +func (r *Client) GetGroupChatStat(req *GetGroupChatStatRequest) (*GetGroupChatStatResponse, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return nil, err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", getGroupChatStatURL, accessToken), string(jsonData)) + if err != nil { + return nil, err + } + result := &GetGroupChatStatResponse{} + err = util.DecodeWithError(response, result, "GetGroupChatStat") + return result, err +} + +type ( + // GetGroupChatStatByDayRequest 获取「群聊数据统计」数据 按自然日聚合的方式 请求 + GetGroupChatStatByDayRequest struct { + DayBeginTime int `json:"day_begin_time"` + DayEndTime int `json:"day_end_time"` + OwnerFilter OwnerFilter `json:"owner_filter"` + } + // GetGroupChatStatByDayResponse 获取「群聊数据统计」数据 按自然日聚合的方式 响应 + GetGroupChatStatByDayResponse struct { + util.CommonError + Items []GetGroupChatStatByDayItem `json:"items"` + } + // GetGroupChatStatByDayItem 群聊数据统计(按自然日聚合)条目 + GetGroupChatStatByDayItem struct { + StatTime int `json:"stat_time"` + Data GroupChatStatItemData `json:"data"` + } +) + +// GetGroupChatStatByDay 获取「群聊数据统计」数据 按自然日聚合的方式 +// @see https://developer.work.weixin.qq.com/document/path/92133 +func (r *Client) GetGroupChatStatByDay(req *GetGroupChatStatByDayRequest) ([]GetGroupChatStatByDayItem, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return nil, err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", getGroupChatStatByDayURL, accessToken), string(jsonData)) + if err != nil { + return nil, err + } + var result GetGroupChatStatByDayResponse + err = util.DecodeWithError(response, &result, "GetGroupChatStatByDay") + return result.Items, err +} diff --git a/work/externalcontact/tag.go b/work/externalcontact/tag.go new file mode 100644 index 0000000..00c2680 --- /dev/null +++ b/work/externalcontact/tag.go @@ -0,0 +1,359 @@ +package externalcontact + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // getCropTagURL 获取标签列表 + getCropTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_corp_tag_list" + // addCropTagURL 添加标签 + addCropTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_corp_tag" + // editCropTagURL 修改标签 + editCropTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/edit_corp_tag" + // delCropTagURL 删除标签 + delCropTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_corp_tag" + // markCropTagURL 为客户打上、删除标签 + markCropTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/mark_tag" + // getStrategyTagListURL 获取指定规则组下的企业客户标签 + getStrategyTagListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_strategy_tag_list?access_token=%s" + // addStrategyTagURL 为指定规则组创建企业客户标签 + addStrategyTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_strategy_tag?access_token=%s" + // editStrategyTagURL 编辑指定规则组下的企业客户标签 + editStrategyTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/edit_strategy_tag?access_token=%s" + // delStrategyTagURL 删除指定规则组下的企业客户标签 + delStrategyTagURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_strategy_tag?access_token=%s" +) + +// GetCropTagRequest 获取企业标签请求 +type GetCropTagRequest struct { + TagID []string `json:"tag_id"` + GroupID []string `json:"group_id"` +} + +// GetCropTagListResponse 获取企业标签列表响应 +type GetCropTagListResponse struct { + util.CommonError + TagGroup []TagGroup `json:"tag_group"` +} + +// TagGroup 企业标签组 +type TagGroup struct { + GroupID string `json:"group_id"` + GroupName string `json:"group_name"` + CreateTime int64 `json:"create_time"` + GroupOrder int `json:"group_order"` + Deleted bool `json:"deleted"` + Tag []TagGroupTagItem `json:"tag"` +} + +// TagGroupTagItem 企业标签内的子项 +type TagGroupTagItem struct { + ID string `json:"id"` + Name string `json:"name"` + CreateTime int64 `json:"create_time"` + Order int `json:"order"` + Deleted bool `json:"deleted"` +} + +// GetCropTagList 获取企业标签库 +// @see https://developer.work.weixin.qq.com/document/path/92117 +func (r *Client) GetCropTagList(req GetCropTagRequest) ([]TagGroup, error) { + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return nil, err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", getCropTagURL, accessToken), string(jsonData)) + if err != nil { + return nil, err + } + var result GetCropTagListResponse + err = util.DecodeWithError(response, &result, "GetCropTagList") + return result.TagGroup, err +} + +// AddCropTagRequest 添加企业标签请求 +type AddCropTagRequest struct { + GroupID string `json:"group_id,omitempty"` + GroupName string `json:"group_name"` + Order int `json:"order"` + Tag []AddCropTagItem `json:"tag"` + AgentID int `json:"agentid"` +} + +// AddCropTagItem 添加企业标签子项 +type AddCropTagItem struct { + Name string `json:"name"` + Order int `json:"order"` +} + +// AddCropTagResponse 添加企业标签响应 +type AddCropTagResponse struct { + util.CommonError + TagGroup TagGroup `json:"tag_group"` +} + +// AddCropTag 添加企业客户标签 +// @see https://developer.work.weixin.qq.com/document/path/92117 +func (r *Client) AddCropTag(req AddCropTagRequest) (*TagGroup, error) { + var accessToken string + accessToken, err := r.GetAccessToken() + if err != nil { + return nil, err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return nil, err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", addCropTagURL, accessToken), string(jsonData)) + if err != nil { + return nil, err + } + var result AddCropTagResponse + err = util.DecodeWithError(response, &result, "AddCropTag") + return &result.TagGroup, err +} + +// EditCropTagRequest 编辑客户企业标签请求 +type EditCropTagRequest struct { + ID string `json:"id"` + Name string `json:"name"` + Order int `json:"order"` + AgentID string `json:"agent_id"` +} + +// EditCropTag 修改企业客户标签 +// @see https://developer.work.weixin.qq.com/document/path/92117 +func (r *Client) EditCropTag(req EditCropTagRequest) error { + accessToken, err := r.GetAccessToken() + if err != nil { + return err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", editCropTagURL, accessToken), string(jsonData)) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "EditCropTag") +} + +// DeleteCropTagRequest 删除企业标签请求 +type DeleteCropTagRequest struct { + TagID []string `json:"tag_id"` + GroupID []string `json:"group_id"` + AgentID string `json:"agent_id"` +} + +// DeleteCropTag 删除企业客户标签 +// @see https://developer.work.weixin.qq.com/document/path/92117 +func (r *Client) DeleteCropTag(req DeleteCropTagRequest) error { + accessToken, err := r.GetAccessToken() + if err != nil { + return err + } + var response []byte + jsonData, err := json.Marshal(req) + if err != nil { + return err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", delCropTagURL, accessToken), string(jsonData)) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "DeleteCropTag") +} + +// MarkTagRequest 给客户打标签请求 +// 相关文档地址:https://developer.work.weixin.qq.com/document/path/92118 +type MarkTagRequest struct { + UserID string `json:"userid"` + ExternalUserID string `json:"external_userid"` + AddTag []string `json:"add_tag"` + RemoveTag []string `json:"remove_tag"` +} + +// MarkTag 为客户打上标签 +// @see https://developer.work.weixin.qq.com/document/path/92118 +func (r *Client) MarkTag(request MarkTagRequest) error { + accessToken, err := r.GetAccessToken() + if err != nil { + return err + } + var response []byte + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", markCropTagURL, accessToken), string(jsonData)) + if err != nil { + return err + } + return util.DecodeWithCommonError(response, "MarkTag") +} + +// GetStrategyTagListRequest 获取指定规则组下的企业客户标签请求 +type GetStrategyTagListRequest struct { + StrategyID int `json:"strategy_id"` + TagID []string `json:"tag_id"` + GroupID []string `json:"group_id"` +} + +// GetStrategyTagListResponse 获取指定规则组下的企业客户标签响应 +type GetStrategyTagListResponse struct { + util.CommonError + TagGroup []StrategyTagGroup `json:"tag_group"` +} + +// StrategyTagGroup 规则组下的企业标签组 +type StrategyTagGroup struct { + GroupID string `json:"group_id"` + GroupName string `json:"group_name"` + CreateTime int64 `json:"create_time"` + Order int `json:"order"` + StrategyID int `json:"strategy_id"` + Tag []StrategyTag `json:"tag"` +} + +// StrategyTag 规则组下的企业标签 +type StrategyTag struct { + ID string `json:"id"` + Name string `json:"name"` + CreateTime int64 `json:"create_time"` + Order int `json:"order"` +} + +// GetStrategyTagList 获取指定规则组下的企业客户标签 +// @see https://developer.work.weixin.qq.com/document/path/94882#%E8%8E%B7%E5%8F%96%E6%8C%87%E5%AE%9A%E8%A7%84%E5%88%99%E7%BB%84%E4%B8%8B%E7%9A%84%E4%BC%81%E4%B8%9A%E5%AE%A2%E6%88%B7%E6%A0%87%E7%AD%BE +func (r *Client) GetStrategyTagList(req *GetStrategyTagListRequest) (*GetStrategyTagListResponse, 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(getStrategyTagListURL, accessToken), req); err != nil { + return nil, err + } + result := &GetStrategyTagListResponse{} + err = util.DecodeWithError(response, result, "GetStrategyTagList") + return result, err +} + +// AddStrategyTagRequest 为指定规则组创建企业客户标签请求 +type AddStrategyTagRequest struct { + StrategyID int `json:"strategy_id"` + GroupID string `json:"group_id"` + GroupName string `json:"group_name"` + Order int `json:"order"` + Tag []AddStrategyTagRequestItem `json:"tag"` +} + +// AddStrategyTagRequestItem 为指定规则组创建企业客户标签请求条目 +type AddStrategyTagRequestItem struct { + Name string `json:"name"` + Order int `json:"order"` +} + +// AddStrategyTagResponse 为指定规则组创建企业客户标签响应 +type AddStrategyTagResponse struct { + util.CommonError + TagGroup AddStrategyTagResponseTagGroup `json:"tag_group"` +} + +// AddStrategyTagResponseTagGroup 为指定规则组创建企业客户标签响应标签组 +type AddStrategyTagResponseTagGroup struct { + GroupID string `json:"group_id"` + GroupName string `json:"group_name"` + CreateTime int64 `json:"create_time"` + Order int `json:"order"` + Tag []AddStrategyTagResponseItem `json:"tag"` +} + +// AddStrategyTagResponseItem 标签组内的标签列表 +type AddStrategyTagResponseItem struct { + ID string `json:"id"` + Name string `json:"name"` + CreateTime int64 `json:"create_time"` + Order int `json:"order"` +} + +// AddStrategyTag 为指定规则组创建企业客户标签 +// @see https://developer.work.weixin.qq.com/document/path/94882#%E4%B8%BA%E6%8C%87%E5%AE%9A%E8%A7%84%E5%88%99%E7%BB%84%E5%88%9B%E5%BB%BA%E4%BC%81%E4%B8%9A%E5%AE%A2%E6%88%B7%E6%A0%87%E7%AD%BE +func (r *Client) AddStrategyTag(req *AddStrategyTagRequest) (*AddStrategyTagResponse, 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(addStrategyTagURL, accessToken), req); err != nil { + return nil, err + } + result := &AddStrategyTagResponse{} + err = util.DecodeWithError(response, result, "AddStrategyTag") + return result, err +} + +// EditStrategyTagRequest 编辑指定规则组下的企业客户标签请求 +type EditStrategyTagRequest struct { + ID string `json:"id"` + Name string `json:"name"` + Order int `json:"order"` +} + +// EditStrategyTag 编辑指定规则组下的企业客户标签 +// see https://developer.work.weixin.qq.com/document/path/94882#%E7%BC%96%E8%BE%91%E6%8C%87%E5%AE%9A%E8%A7%84%E5%88%99%E7%BB%84%E4%B8%8B%E7%9A%84%E4%BC%81%E4%B8%9A%E5%AE%A2%E6%88%B7%E6%A0%87%E7%AD%BE +func (r *Client) EditStrategyTag(req *EditStrategyTagRequest) 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(editStrategyTagURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "EditStrategyTag") +} + +// DelStrategyTagRequest 删除指定规则组下的企业客户标签请求 +type DelStrategyTagRequest struct { + TagID []string `json:"tag_id"` + GroupID []string `json:"group_id"` +} + +// DelStrategyTag 删除指定规则组下的企业客户标签 +// see https://developer.work.weixin.qq.com/document/path/94882#%E5%88%A0%E9%99%A4%E6%8C%87%E5%AE%9A%E8%A7%84%E5%88%99%E7%BB%84%E4%B8%8B%E7%9A%84%E4%BC%81%E4%B8%9A%E5%AE%A2%E6%88%B7%E6%A0%87%E7%AD%BE +func (r *Client) DelStrategyTag(req *DelStrategyTagRequest) 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(delStrategyTagURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelStrategyTag") +} diff --git a/work/externalcontact/transfer.go b/work/externalcontact/transfer.go new file mode 100644 index 0000000..4b74a0f --- /dev/null +++ b/work/externalcontact/transfer.go @@ -0,0 +1,277 @@ +package externalcontact + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // transferCustomerURL 分配在职成员的客户 + transferCustomerURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/transfer_customer?access_token=%s" + // transferResultURL 查询客户接替状态 + transferResultURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/transfer_result?access_token=%s" + // groupChatOnJobTransferURL 分配在职成员的客户群 + groupChatOnJobTransferURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/onjob_transfer?access_token=%s" + // getUnassignedListURL 获取待分配的离职成员列表 + getUnassignedListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_unassigned_list?access_token=%s" + // resignedTransferCustomerURL 分配离职成员的客户 + resignedTransferCustomerURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/resigned/transfer_customer?access_token=%s" + // resignedTransferResultURL 查询离职客户接替状态 + resignedTransferResultURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/resigned/transfer_result?access_token=%s" + // groupChatTransferURL 分配离职成员的客户群 + groupChatTransferURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/transfer?access_token=%s" +) + +// TransferCustomerRequest 分配在职成员的客户请求 +type TransferCustomerRequest struct { + HandoverUserID string `json:"handover_userid"` + TakeoverUserID string `json:"takeover_userid"` + ExternalUserID []string `json:"external_userid"` + TransferSuccessMsg string `json:"transfer_success_msg"` +} + +// TransferCustomerResponse 分配在职成员的客户请求响应 +type TransferCustomerResponse struct { + util.CommonError + Customer []TransferCustomerItem `json:"customer"` +} + +// TransferCustomerItem 客户分配结果 +type TransferCustomerItem struct { + ExternalUserID string `json:"external_userid"` + ErrCode int `json:"errcode"` +} + +// TransferCustomer 分配在职成员的客户 +// see https://developer.work.weixin.qq.com/document/path/92125 +func (r *Client) TransferCustomer(req *TransferCustomerRequest) (*TransferCustomerResponse, 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(transferCustomerURL, accessToken), req); err != nil { + return nil, err + } + result := &TransferCustomerResponse{} + err = util.DecodeWithError(response, result, "TransferCustomer") + return result, err +} + +// TransferResultRequest 查询客户接替状态请求 +type TransferResultRequest struct { + HandoverUserID string `json:"handover_userid"` + TakeoverUserID string `json:"takeover_userid"` + Cursor string `json:"cursor"` +} + +// TransferResultResponse 查询客户接替状态响应 +type TransferResultResponse struct { + util.CommonError + Customer []TransferResultItem `json:"customer"` + NextCursor string `json:"next_cursor"` +} + +// TransferResultItem 客户接替状态 +type TransferResultItem struct { + ExternalUserID string `json:"external_userid"` + Status int `json:"status"` + TakeoverTime int64 `json:"takeover_time"` +} + +// TransferResult 查询客户接替状态 +// see https://developer.work.weixin.qq.com/document/path/94088 +func (r *Client) TransferResult(req *TransferResultRequest) (*TransferResultResponse, 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(transferResultURL, accessToken), req); err != nil { + return nil, err + } + result := &TransferResultResponse{} + err = util.DecodeWithError(response, result, "TransferResult") + return result, err +} + +// GroupChatOnJobTransferRequest 分配在职成员的客户群请求 +type GroupChatOnJobTransferRequest struct { + ChatIDList []string `json:"chat_id_list"` + NewOwner string `json:"new_owner"` +} + +// GroupChatOnJobTransferResponse 分配在职成员的客户群响应 +type GroupChatOnJobTransferResponse struct { + util.CommonError + FailedChatList []FailedChat `json:"failed_chat_list"` +} + +// FailedChat 没能成功继承的群 +type FailedChat struct { + ChatID string `json:"chat_id"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +// GroupChatOnJobTransfer 分配在职成员的客户群 +// see https://developer.work.weixin.qq.com/document/path/95703 +func (r *Client) GroupChatOnJobTransfer(req *GroupChatOnJobTransferRequest) (*GroupChatOnJobTransferResponse, 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(groupChatOnJobTransferURL, accessToken), req); err != nil { + return nil, err + } + result := &GroupChatOnJobTransferResponse{} + err = util.DecodeWithError(response, result, "GroupChatOnJobTransfer") + return result, err +} + +// GetUnassignedListRequest 获取待分配的离职成员列表请求 +type GetUnassignedListRequest struct { + Cursor string `json:"cursor"` + PageSize int `json:"page_size"` +} + +// GetUnassignedListResponse 获取待分配的离职成员列表响应 +type GetUnassignedListResponse struct { + util.CommonError + Info []UnassignedListInfo `json:"info"` + IsLast bool `json:"is_last"` + NextCursor string `json:"next_cursor"` +} + +// UnassignedListInfo 离职成员信息 +type UnassignedListInfo struct { + HandoverUserID string `json:"handover_userid"` + ExternalUserID string `json:"external_userid"` + DimissionTime int64 `json:"dimission_time"` +} + +// GetUnassignedList 获取待分配的离职成员列表 +// see https://developer.work.weixin.qq.com/document/path/92124 +func (r *Client) GetUnassignedList(req *GetUnassignedListRequest) (*GetUnassignedListResponse, 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(getUnassignedListURL, accessToken), req); err != nil { + return nil, err + } + result := &GetUnassignedListResponse{} + err = util.DecodeWithError(response, result, "GetUnassignedList") + return result, err +} + +// ResignedTransferCustomerRequest 分配离职成员的客户请求 +type ResignedTransferCustomerRequest struct { + HandoverUserID string `json:"handover_userid"` + TakeoverUserID string `json:"takeover_userid"` + ExternalUserID []string `json:"external_userid"` +} + +// ResignedTransferCustomerResponse 分配离职成员的客户响应 +type ResignedTransferCustomerResponse struct { + util.CommonError + Customer []TransferCustomerItem `json:"customer"` +} + +// ResignedTransferCustomer 分配离职成员的客户 +// see https://developer.work.weixin.qq.com/document/path/94081 +func (r *Client) ResignedTransferCustomer(req *ResignedTransferCustomerRequest) (*ResignedTransferCustomerResponse, 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(resignedTransferCustomerURL, accessToken), req); err != nil { + return nil, err + } + result := &ResignedTransferCustomerResponse{} + err = util.DecodeWithError(response, result, "ResignedTransferCustomer") + return result, err +} + +// ResignedTransferResultRequest 查询离职客户接替状态请求 +type ResignedTransferResultRequest struct { + HandoverUserID string `json:"handover_userid"` + TakeoverUserID string `json:"takeover_userid"` + Cursor string `json:"cursor"` +} + +// ResignedTransferResultResponse 查询离职客户接替状态响应 +type ResignedTransferResultResponse struct { + util.CommonError + Customer []TransferResultItem `json:"customer"` + NextCursor string `json:"next_cursor"` +} + +// ResignedTransferResult 查询离职客户接替状态 +// see https://developer.work.weixin.qq.com/document/path/94082 +func (r *Client) ResignedTransferResult(req *ResignedTransferResultRequest) (*ResignedTransferResultResponse, 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(resignedTransferResultURL, accessToken), req); err != nil { + return nil, err + } + result := &ResignedTransferResultResponse{} + err = util.DecodeWithError(response, result, "ResignedTransferResult") + return result, err +} + +// GroupChatTransferRequest 分配离职成员的客户群请求 +type GroupChatTransferRequest struct { + ChatIDList []string `json:"chat_id_list"` + NewOwner string `json:"new_owner"` +} + +// GroupChatTransferResponse 分配离职成员的客户群响应 +type GroupChatTransferResponse struct { + util.CommonError + FailedChatList []FailedChat `json:"failed_chat_list"` +} + +// GroupChatTransfer 分配离职成员的客户群 +// see https://developer.work.weixin.qq.com/document/path/92127 +func (r *Client) GroupChatTransfer(req *GroupChatTransferRequest) (*GroupChatTransferResponse, 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(groupChatTransferURL, accessToken), req); err != nil { + return nil, err + } + result := &GroupChatTransferResponse{} + err = util.DecodeWithError(response, result, "GroupChatTransfer") + return result, err +} diff --git a/work/invoice/client.go b/work/invoice/client.go new file mode 100644 index 0000000..c5dde0d --- /dev/null +++ b/work/invoice/client.go @@ -0,0 +1,17 @@ +package invoice + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 电子发票接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/invoice/invoice.go b/work/invoice/invoice.go new file mode 100644 index 0000000..d145b07 --- /dev/null +++ b/work/invoice/invoice.go @@ -0,0 +1,187 @@ +package invoice + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // getInvoiceInfoURL 查询电子发票 + getInvoiceInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=%s" + // updateInvoiceStatusURL 更新发票状态 + updateInvoiceStatusURL = "https://qyapi.weixin.qq.com/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=%s" + // updateStatusBatchURL 批量更新发票状态 + updateStatusBatchURL = "https://qyapi.weixin.qq.com/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=%s" + // getInvoiceInfoBatchURL 批量查询电子发票 + getInvoiceInfoBatchURL = "https://qyapi.weixin.qq.com/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=%s" +) + +// GetInvoiceInfoRequest 查询电子发票请求 +type GetInvoiceInfoRequest struct { + CardID string `json:"card_id"` + EncryptCode string `json:"encrypt_code"` +} + +// GetInvoiceInfoResponse 查询电子发票响应 +type GetInvoiceInfoResponse struct { + util.CommonError + CardID string `json:"card_id"` + BeginTime int64 `json:"begin_time"` + EndTime int64 `json:"end_time"` + OpenID string `json:"openid"` + Type string `json:"type"` + Payee string `json:"payee"` + Detail string `json:"detail"` + UserInfo UserInfo `json:"user_info"` +} + +// UserInfo 发票的用户信息 +type UserInfo struct { + Fee int64 `json:"fee"` + Title string `json:"title"` + BillingTime int64 `json:"billing_time"` + BillingNo string `json:"billing_no"` + BillingCode string `json:"billing_code"` + Info []Info `json:"info"` + FeeWithoutTax int64 `json:"fee_without_tax"` + Tax int64 `json:"tax"` + Detail string `json:"detail"` + PdfURL string `json:"pdf_url"` + TripPdfURL string `json:"trip_pdf_url"` + ReimburseStatus string `json:"reimburse_status"` + CheckCode string `json:"check_code"` + BuyerNumber string `json:"buyer_number"` + BuyerAddressAndPhone string `json:"buyer_address_and_phone"` + BuyerBankAccount string `json:"buyer_bank_account"` + SellerNumber string `json:"seller_number"` + SellerAddressAndPhone string `json:"seller_address_and_phone"` + SellerBankAccount string `json:"seller_bank_account"` + Remarks string `json:"remarks"` + Cashier string `json:"cashier"` + Maker string `json:"maker"` +} + +// Info 商品信息结构 +type Info struct { + Name string `json:"name"` + Num int64 `json:"num"` + Unit string `json:"unit"` + Fee int64 `json:"fee"` + Price int64 `json:"price"` +} + +// GetInvoiceInfo 查询电子发票 +// see https://developer.work.weixin.qq.com/document/path/90284 +func (r *Client) GetInvoiceInfo(req *GetInvoiceInfoRequest) (*GetInvoiceInfoResponse, 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(getInvoiceInfoURL, accessToken), req); err != nil { + return nil, err + } + result := &GetInvoiceInfoResponse{} + err = util.DecodeWithError(response, result, "GetInvoiceInfo") + return result, err +} + +// UpdateInvoiceStatusRequest 更新发票状态请求 +type UpdateInvoiceStatusRequest struct { + CardID string `json:"card_id"` + EncryptCode string `json:"encrypt_code"` + ReimburseStatus string `json:"reimburse_status"` +} + +// UpdateInvoiceStatus 更新发票状态 +// see https://developer.work.weixin.qq.com/document/path/90285 +func (r *Client) UpdateInvoiceStatus(req *UpdateInvoiceStatusRequest) 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(updateInvoiceStatusURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "UpdateInvoiceStatus") +} + +// UpdateStatusBatchRequest 批量更新发票状态 +type UpdateStatusBatchRequest struct { + OpenID string `json:"openid"` + ReimburseStatus string `json:"reimburse_status"` + InvoiceList []Invoice `json:"invoice_list"` +} + +// Invoice 发票卡券 +type Invoice struct { + CardID string `json:"card_id"` + EncryptCode string `json:"encrypt_code"` +} + +// UpdateStatusBatch 批量更新发票状态 +// see https://developer.work.weixin.qq.com/document/path/90286 +func (r *Client) UpdateStatusBatch(req *UpdateStatusBatchRequest) 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(updateStatusBatchURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "UpdateStatusBatch") +} + +// GetInvoiceInfoBatchRequest 批量查询电子发票请求 +type GetInvoiceInfoBatchRequest struct { + ItemList []Invoice `json:"item_list"` +} + +// GetInvoiceInfoBatchResponse 批量查询电子发票响应 +type GetInvoiceInfoBatchResponse struct { + util.CommonError + ItemList []Item `json:"item_list"` +} + +// Item 电子发票的结构化信息 +type Item struct { + CardID string `json:"card_id"` + BeginTime int64 `json:"begin_time"` + EndTime int64 `json:"end_time"` + OpenID string `json:"openid"` + Type string `json:"type"` + Payee string `json:"payee"` + Detail string `json:"detail"` + UserInfo UserInfo `json:"user_info"` +} + +// GetInvoiceInfoBatch 批量查询电子发票 +// see https://developer.work.weixin.qq.com/document/path/90287 +func (r *Client) GetInvoiceInfoBatch(req *GetInvoiceInfoBatchRequest) (*GetInvoiceInfoBatchResponse, 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(getInvoiceInfoBatchURL, accessToken), req); err != nil { + return nil, err + } + result := &GetInvoiceInfoBatchResponse{} + err = util.DecodeWithError(response, result, "GetInvoiceInfoBatch") + return result, err +} diff --git a/work/kf/README.md b/work/kf/README.md new file mode 100644 index 0000000..f736183 --- /dev/null +++ b/work/kf/README.md @@ -0,0 +1,3 @@ +### 微信客服SDK + +相关文档正在梳理中... \ No newline at end of file diff --git a/work/kf/account.go b/work/kf/account.go new file mode 100644 index 0000000..6153faa --- /dev/null +++ b/work/kf/account.go @@ -0,0 +1,179 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 添加客服账号 + accountAddAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/add?access_token=%s" + // 删除客服账号 + accountDelAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/del?access_token=%s" + // 修改客服账号 + accountUpdateAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/update?access_token=%s" + // 获取客服账号列表 + accountListAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/list?access_token=%s" + // 获取客服账号链接 + addContactWayAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/add_contact_way?access_token=%s" +) + +// AccountAddOptions 添加客服账号请求参数 +type AccountAddOptions struct { + Name string `json:"name"` // 客服帐号名称, 不多于16个字符 + MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节 +} + +// AccountAddSchema 添加客服账号响应内容 +type AccountAddSchema struct { + util.CommonError + OpenKFID string `json:"open_kfid"` // 新创建的客服张号ID +} + +// AccountAdd 添加客服账号 +func (r *Client) AccountAdd(options AccountAddOptions) (info AccountAddSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(accountAddAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// AccountDelOptions 删除客服账号请求参数 +type AccountDelOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节 +} + +// AccountDel 删除客服账号 +func (r *Client) AccountDel(options AccountDelOptions) (info util.CommonError, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(accountDelAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// AccountUpdateOptions 修改客服账号请求参数 +type AccountUpdateOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节 + Name string `json:"name"` // 客服帐号名称, 不多于16个字符 + MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节 +} + +// AccountUpdate 修复客服账号 +func (r *Client) AccountUpdate(options AccountUpdateOptions) (info util.CommonError, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(accountUpdateAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// AccountInfoSchema 客服详情 +type AccountInfoSchema struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + Name string `json:"name"` // 客服帐号名称 + Avatar string `json:"avatar"` // 客服头像URL +} + +// AccountListSchema 获取客服账号列表响应内容 +type AccountListSchema struct { + util.CommonError + AccountList []AccountInfoSchema `json:"account_list"` // 客服账号列表 +} + +// AccountList 获取客服账号列表 +func (r *Client) AccountList() (info AccountListSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.HTTPGet(fmt.Sprintf(accountListAddr, accessToken)); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// AddContactWayOptions 获取客服账号链接 +// 1.若scene非空,返回的客服链接开发者可拼接scene_param=SCENE_PARAM参数使用,用户进入会话事件会将SCENE_PARAM原样返回。其中SCENE_PARAM需要urlencode,且长度不能超过128字节。 +// 如 https://work.weixin.qq.com/kf/kfcbf8f8d07ac7215f?enc_scene=ENCGFSDF567DF&scene_param=a%3D1%26b%3D2 +// 2.历史调用接口返回的客服链接(包含encScene=XXX参数),不支持scene_param参数。 +// 3.返回的客服链接,不能修改或复制参数到其他链接使用。否则进入会话事件参数校验不通过,导致无法回调。 +type AddContactWayOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节 + Scene string `json:"scene"` // 场景值,字符串类型,由开发者自定义, 不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]* +} + +// AddContactWaySchema 获取客服账号链接响应内容 +type AddContactWaySchema struct { + util.CommonError + URL string `json:"url"` // 客服链接,开发者可将该链接嵌入到H5页面中,用户点击链接即可向对应的微信客服帐号发起咨询。开发者也可根据该url自行生成需要的二维码图片 +} + +// AddContactWay 获取客服账号链接 +func (r *Client) AddContactWay(options AddContactWayOptions) (info AddContactWaySchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(addContactWayAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/callback.go b/work/kf/callback.go new file mode 100644 index 0000000..a62cb1a --- /dev/null +++ b/work/kf/callback.go @@ -0,0 +1,97 @@ +package kf + +import ( + "encoding/xml" + + "github.com/silenceper/wechat/v2/util" +) + +// SignatureOptions 微信服务器验证参数 +type SignatureOptions struct { + Signature string `form:"msg_signature"` + TimeStamp string `form:"timestamp"` + Nonce string `form:"nonce"` + EchoStr string `form:"echostr"` +} + +// VerifyURL 验证请求参数是否合法并返回解密后的消息内容 +// +// //Gin框架的使用示例 +// r.GET("/v1/event/callback", func(c *gin.Context) { +// options := kf.SignatureOptions{} +// //获取回调的的校验参数 +// if = c.ShouldBindQuery(&options); err != nil { +// c.String(http.StatusUnauthorized, "参数解析失败") +// } +// // 调用VerifyURL方法校验当前请求,如果合法则把解密后的内容作为响应返回给微信服务器 +// echo, err := kfClient.VerifyURL(options) +// if err == nil { +// c.String(http.StatusOK, echo) +// } else { +// c.String(http.StatusUnauthorized, "非法请求来源") +// } +// }) +func (r *Client) VerifyURL(options SignatureOptions) (string, error) { + if options.Signature != util.Signature(r.ctx.Token, options.TimeStamp, options.Nonce, options.EchoStr) { + return "", NewSDKErr(40015) + } + _, bData, err := util.DecryptMsg(r.corpID, options.EchoStr, r.encodingAESKey) + if err != nil { + return "", NewSDKErr(40016) + } + + return string(bData), nil +} + +// 原始回调消息内容 +type callbackOriginMessage struct { + ToUserName string // 企业微信的CorpID,当为第三方套件回调事件时,CorpID的内容为suiteid + AgentID string // 接收的应用id,可在应用的设置页面获取 + Encrypt string // 消息结构体加密后的字符串 +} + +// CallbackMessage 微信客服回调消息 +type CallbackMessage struct { + ToUserName string `json:"to_user_name" xml:"ToUserName"` // 微信客服组件ID + CreateTime int64 `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 获取回调事件中的消息内容 +// +// //Gin框架的使用示例 +// r.POST("/v1/event/callback", func(c *gin.Context) { +// var ( +// message kf.CallbackMessage +// body []byte +// ) +// // 读取原始消息内容 +// body, err = c.GetRawData() +// if err != nil { +// c.String(http.StatusInternalServerError, err.Error()) +// return +// } +// // 解析原始数据 +// message, err = kfClient.GetCallbackMessage(body) +// if err != nil { +// c.String(http.StatusInternalServerError, "消息获取失败") +// return +// } +// fmt.Println(message) +// c.String(200, "ok") +// }) +func (r *Client) GetCallbackMessage(encryptedMsg []byte) (msg CallbackMessage, err error) { + var origin callbackOriginMessage + if err = xml.Unmarshal(encryptedMsg, &origin); err != nil { + return msg, err + } + _, bData, err := util.DecryptMsg(r.corpID, origin.Encrypt, r.encodingAESKey) + if err != nil { + return msg, NewSDKErr(40016) + } + err = xml.Unmarshal(bData, &msg) + return msg, err +} diff --git a/work/kf/client.go b/work/kf/client.go new file mode 100644 index 0000000..d83a5fc --- /dev/null +++ b/work/kf/client.go @@ -0,0 +1,43 @@ +package kf + +import ( + "github.com/silenceper/wechat/v2/cache" + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/work/config" + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 微信客服实例 +type Client struct { + corpID string // 企业ID:企业开通的每个微信客服,都对应唯一的企业ID,企业可在微信客服管理后台的企业信息处查看 + secret string // Secret是微信客服用于校验开发者身份的访问密钥,企业成功注册微信客服后,可在「微信客服管理后台-开发配置」处获取 + token string // 用于生成签名校验回调请求的合法性 + encodingAESKey string // 回调消息加解密参数是AES密钥的Base64编码,用于解密回调消息内容对应的密文 + cache cache.Cache + ctx *context.Context +} + +// NewClient 初始化微信客服实例 +func NewClient(cfg *config.Config) (client *Client, err error) { + if cfg.Cache == nil { + return nil, NewSDKErr(50001) + } + + // 初始化 AccessToken Handle + defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache) + ctx := &context.Context{ + Config: cfg, + AccessTokenHandle: defaultAkHandle, + } + + client = &Client{ + corpID: cfg.CorpID, + secret: cfg.CorpSecret, + token: cfg.Token, + encodingAESKey: cfg.EncodingAESKey, + cache: cfg.Cache, + ctx: ctx, + } + + return client, nil +} diff --git a/work/kf/customer.go b/work/kf/customer.go new file mode 100644 index 0000000..1ded143 --- /dev/null +++ b/work/kf/customer.go @@ -0,0 +1,54 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + customerBatchGetAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=%s" +) + +// CustomerBatchGetOptions 客户基本信息获取请求参数 +type CustomerBatchGetOptions struct { + ExternalUserIDList []string `json:"external_userid_list"` // external_userid列表 +} + +// CustomerSchema 微信客户基本资料 +type CustomerSchema struct { + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid + NickName string `json:"nickname"` // 微信昵称 + Avatar string `json:"avatar"` // 微信头像。第三方不可获取 + Gender int `json:"gender"` // 性别 + UnionID string `json:"unionid"` // unionid,需要绑定微信开发者帐号才能获取到,查看绑定方法: https://open.work.weixin.qq.com/kf/doc/92512/93143/94769#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E5%AE%A2%E6%88%B7%E7%9A%84unionid +} + +// CustomerBatchGetSchema 获取客户基本信息响应内容 +type CustomerBatchGetSchema struct { + util.CommonError + CustomerList []CustomerSchema `json:"customer_list"` // 微信客户信息列表 + InvalidExternalUserID []string `json:"invalid_external_userid"` // 无效的微信客户ID +} + +// CustomerBatchGet 客户基本信息获取 +func (r *Client) CustomerBatchGet(options CustomerBatchGetOptions) (info CustomerBatchGetSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(customerBatchGetAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/error.go b/work/kf/error.go new file mode 100644 index 0000000..57633de --- /dev/null +++ b/work/kf/error.go @@ -0,0 +1,93 @@ +package kf + +import ( + "reflect" + "strings" +) + +// Error 错误 +type Error string + +const ( + // SDKInitFailed 错误码:50001 + SDKInitFailed Error = "SDK初始化失败" + // SDKCacheUnavailable 错误码:50002 + SDKCacheUnavailable Error = "缓存无效" + // SDKUnknownError 错误码:50003 + SDKUnknownError Error = "未知错误" + // SDKInvalidCredential 错误码:40001 + SDKInvalidCredential Error = "不合法的secret参数" + // SDKInvalidImageSize 错误码:40009 + SDKInvalidImageSize Error = "无效的图片大小" + // SDKInvalidCorpID 错误码:40013 + SDKInvalidCorpID Error = "无效的 CorpID" + // SDKAccessTokenInvalid 错误码:40014 + SDKAccessTokenInvalid Error = "AccessToken 无效" + // SDKValidateSignatureFailed 错误码:40015 + SDKValidateSignatureFailed Error = "校验签名错误" + // SDKDecryptMSGFailed 错误码:40016 + SDKDecryptMSGFailed Error = "消息解密失败" + // SDKMediaIDExceedMinLength 错误码:40058 + SDKMediaIDExceedMinLength Error = "不合法的参数, 请参照具体 API 接口说明进行传参" + // SDKContentContainsSensitiveInformation 错误码:40201 + SDKContentContainsSensitiveInformation Error = "当前客服账号由于涉及敏感信息,已被封禁,请联系企业微信客服处理" + // SDKAccessTokenMissing 错误码:41001 + SDKAccessTokenMissing Error = "缺少AccessToken参数" + // SDKAccessTokenExpired 错误码:42001 + SDKAccessTokenExpired Error = "AccessToken 已过期" + // SDKApiFreqOutOfLimit 错误码:45009 + SDKApiFreqOutOfLimit Error = "接口请求次数超频" + // SDKApiForbidden 错误码:48002 + SDKApiForbidden Error = "API 禁止调用" + // SDKInvalidOpenKFID 错误码:95000 + SDKInvalidOpenKFID Error = "无效的 open_kfid" + // SDKOpenKFIDNotExist 错误码:95004 + SDKOpenKFIDNotExist Error = "open_kfid 不存在" + // SDKWeWorkAlready 错误码:95011 + SDKWeWorkAlready Error = "已在企业微信使用微信客服" + // SDKNotUseInWeCom 错误码:95012 + SDKNotUseInWeCom Error = "未在企业微信使用微信客服" + // SDKApiNotOpen 错误码:95017 + SDKApiNotOpen Error = "API 功能没有被开启" +) + +// Error 输出错误信息 +func (r Error) Error() string { + return reflect.ValueOf(r).String() +} + +var codeDic = map[int64]error{ + 50001: SDKInitFailed, + 50002: SDKCacheUnavailable, + 50003: SDKUnknownError, + 40001: SDKInvalidCredential, + 40009: SDKInvalidImageSize, + 40013: SDKInvalidCorpID, + 40014: SDKAccessTokenInvalid, + 40015: SDKValidateSignatureFailed, + 40016: SDKDecryptMSGFailed, + 40058: SDKMediaIDExceedMinLength, + 40201: SDKContentContainsSensitiveInformation, + 41001: SDKAccessTokenMissing, + 42001: SDKAccessTokenExpired, + 45009: SDKApiFreqOutOfLimit, + 48002: SDKApiForbidden, + 95000: SDKInvalidOpenKFID, + 95004: SDKOpenKFIDNotExist, + 95011: SDKWeWorkAlready, + 95012: SDKNotUseInWeCom, + 95017: SDKApiNotOpen, +} + +// NewSDKErr 初始化SDK实例错误信息 +func NewSDKErr(code int64, msgList ...string) error { + if err := codeDic[code]; err != nil { + return err + } + + // 返回未知的自定义错误 + if len(msgList) > 0 { + return Error(strings.Join(msgList, ",")) + } + return SDKUnknownError +} diff --git a/work/kf/knowledge.go b/work/kf/knowledge.go new file mode 100644 index 0000000..1d196c1 --- /dev/null +++ b/work/kf/knowledge.go @@ -0,0 +1,359 @@ +package kf + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // addKnowledgeGroupURL 知识库分组添加 + addKnowledgeGroupURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/add_group?access_token=%s" + // delKnowledgeGroupURL 知识库分组删除 + delKnowledgeGroupURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/del_group?access_token=%s" + // modKnowledgeGroupURL 知识库分组修改 + modKnowledgeGroupURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/mod_group?access_token=%s" + // listKnowledgeGroupURL 知识库分组列表 + listKnowledgeGroupURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/list_group?access_token=%s" + // addKnowledgeIntentURL 知识库问答添加 + addKnowledgeIntentURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/add_intent?access_token=%s" + // delKnowledgeIntentURL 知识库问答删除 + delKnowledgeIntentURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/del_intent?access_token=%s" + // modKnowledgeIntentURL 知识库问答修改 + modKnowledgeIntentURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/mod_intent?access_token=%s" + // listKnowledgeIntentURL 知识库问答列表 + listKnowledgeIntentURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/knowledge/list_intent?access_token=%s" +) + +// AddKnowledgeGroupRequest 知识库分组添加请求 +type AddKnowledgeGroupRequest struct { + Name string `json:"name"` +} + +// AddKnowledgeGroupResponse 知识库分组添加响应 +type AddKnowledgeGroupResponse struct { + util.CommonError + GroupID string `json:"group_id"` +} + +// AddKnowledgeGroup 知识库分组添加 +// see https://developer.work.weixin.qq.com/document/path/95971#%E6%B7%BB%E5%8A%A0%E5%88%86%E7%BB%84 +func (r *Client) AddKnowledgeGroup(req *AddKnowledgeGroupRequest) (*AddKnowledgeGroupResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(addKnowledgeGroupURL, accessToken), req); err != nil { + return nil, err + } + result := &AddKnowledgeGroupResponse{} + err = util.DecodeWithError(response, result, "AddKnowledgeGroup") + return result, err +} + +// DelKnowledgeGroupRequest 知识库分组删除请求 +type DelKnowledgeGroupRequest struct { + GroupID string `json:"group_id"` +} + +// DelKnowledgeGroup 知识库分组删除 +// see https://developer.work.weixin.qq.com/document/path/95971#%E5%88%A0%E9%99%A4%E5%88%86%E7%BB%84 +func (r *Client) DelKnowledgeGroup(req *DelKnowledgeGroupRequest) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(delKnowledgeGroupURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelKnowledgeGroup") +} + +// ModKnowledgeGroupRequest 知识库分组修改请求 +type ModKnowledgeGroupRequest struct { + GroupID string `json:"group_id"` + Name string `json:"name"` +} + +// ModKnowledgeGroup 知识库分组修改 +// see https://developer.work.weixin.qq.com/document/path/95971#%E4%BF%AE%E6%94%B9%E5%88%86%E7%BB%84 +func (r *Client) ModKnowledgeGroup(req *ModKnowledgeGroupRequest) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(modKnowledgeGroupURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "ModKnowledgeGroup") +} + +// ListKnowledgeGroupRequest 知识库分组列表请求 +type ListKnowledgeGroupRequest struct { + Cursor string `json:"cursor"` + Limit int `json:"limit"` + GroupID string `json:"group_id"` +} + +// ListKnowledgeGroupResponse 知识库分组列表响应 +type ListKnowledgeGroupResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + HasMore int `json:"has_more"` + GroupList []KnowledgeGroup `json:"group_list"` +} + +// KnowledgeGroup 知识库分组 +type KnowledgeGroup struct { + GroupID string `json:"group_id"` + Name string `json:"name"` + IsDefault int `json:"is_default"` +} + +// ListKnowledgeGroup 知识库分组列表 +// see https://developer.work.weixin.qq.com/document/path/95971#%E8%8E%B7%E5%8F%96%E5%88%86%E7%BB%84%E5%88%97%E8%A1%A8 +func (r *Client) ListKnowledgeGroup(req *ListKnowledgeGroupRequest) (*ListKnowledgeGroupResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(listKnowledgeGroupURL, accessToken), req); err != nil { + return nil, err + } + result := &ListKnowledgeGroupResponse{} + err = util.DecodeWithError(response, result, "ListKnowledgeGroup") + return result, err +} + +// AddKnowledgeIntentRequest 知识库问答添加请求 +type AddKnowledgeIntentRequest struct { + GroupID string `json:"group_id"` + Question IntentQuestion `json:"question"` + SimilarQuestions IntentSimilarQuestions `json:"similar_questions"` + Answers []IntentAnswerReq `json:"answers"` +} + +// IntentQuestion 主问题 +type IntentQuestion struct { + Text IntentQuestionText `json:"text"` +} + +// IntentQuestionText 问题文本 +type IntentQuestionText struct { + Content string `json:"content"` +} + +// IntentSimilarQuestions 相似问题 +type IntentSimilarQuestions struct { + Items []IntentQuestion `json:"items"` +} + +// IntentAnswerReq 回答请求 +type IntentAnswerReq struct { + Text IntentAnswerText `json:"text"` + Attachments []IntentAnswerAttachmentReq `json:"attachments"` +} + +// IntentAnswerText 回答文本 +type IntentAnswerText struct { + Content string `json:"content"` +} + +// IntentAnswerAttachmentReq 回答附件请求 +type IntentAnswerAttachmentReq struct { + MsgType string `json:"msgtype"` + Image IntentAnswerAttachmentImgReq `json:"image,omitempty"` + Video IntentAnswerAttachmentVideoReq `json:"video,omitempty"` + Link IntentAnswerAttachmentLink `json:"link,omitempty"` + MiniProgram IntentAnswerAttachmentMiniProgramReq `json:"miniprogram,omitempty"` +} + +// IntentAnswerAttachmentImgReq 图片类型回答附件请求 +type IntentAnswerAttachmentImgReq struct { + MediaID string `json:"media_id"` +} + +// IntentAnswerAttachmentVideoReq 视频类型回答附件请求 +type IntentAnswerAttachmentVideoReq struct { + MediaID string `json:"media_id"` +} + +// IntentAnswerAttachmentLink 链接类型回答附件 +type IntentAnswerAttachmentLink struct { + Title string `json:"title"` + PicURL string `json:"picurl"` + Desc string `json:"desc"` + URL string `json:"url"` +} + +// IntentAnswerAttachmentMiniProgramReq 小程序类型回答附件请求 +type IntentAnswerAttachmentMiniProgramReq struct { + Title string `json:"title"` + ThumbMediaID string `json:"thumb_media_id"` + AppID string `json:"appid"` + PagePath string `json:"pagepath"` +} + +// AddKnowledgeIntentResponse 知识库问答添加响应 +type AddKnowledgeIntentResponse struct { + util.CommonError + IntentID string `json:"intent_id"` +} + +// AddKnowledgeIntent 知识库问答添加 +// see https://developer.work.weixin.qq.com/document/path/95972#%E6%B7%BB%E5%8A%A0%E9%97%AE%E7%AD%94 +func (r *Client) AddKnowledgeIntent(req *AddKnowledgeIntentRequest) (*AddKnowledgeIntentResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(addKnowledgeIntentURL, accessToken), req); err != nil { + return nil, err + } + result := &AddKnowledgeIntentResponse{} + err = util.DecodeWithError(response, result, "AddKnowledgeIntent") + return result, err +} + +// DelKnowledgeIntentRequest 知识库问答删除请求 +type DelKnowledgeIntentRequest struct { + IntentID string `json:"intent_id"` +} + +// DelKnowledgeIntent 知识库问答删除 +// see https://developer.work.weixin.qq.com/document/path/95972#%E5%88%A0%E9%99%A4%E9%97%AE%E7%AD%94 +func (r *Client) DelKnowledgeIntent(req *DelKnowledgeIntentRequest) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(delKnowledgeIntentURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelKnowledgeIntent") +} + +// ModKnowledgeIntentRequest 知识库问答修改请求 +type ModKnowledgeIntentRequest struct { + IntentID string `json:"intent_id"` + Question IntentQuestion `json:"question"` + SimilarQuestions IntentSimilarQuestions `json:"similar_questions"` + Answers []IntentAnswerReq `json:"answers"` +} + +// ModKnowledgeIntent 知识库问答修改 +// see https://developer.work.weixin.qq.com/document/path/95972#%E4%BF%AE%E6%94%B9%E9%97%AE%E7%AD%94 +func (r *Client) ModKnowledgeIntent(req *ModKnowledgeIntentRequest) error { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(modKnowledgeIntentURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "ModKnowledgeIntent") +} + +// ListKnowledgeIntentRequest 知识库问答列表请求 +type ListKnowledgeIntentRequest struct { + Cursor string `json:"cursor"` + Limit int `json:"limit"` + GroupID string `json:"group_id"` + IntentID string `json:"intent_id"` +} + +// ListKnowledgeIntentResponse 知识库问答列表响应 +type ListKnowledgeIntentResponse struct { + util.CommonError + NextCursor string `json:"next_cursor"` + HasMore int `json:"has_more"` + IntentList []KnowledgeIntent `json:"intent_list"` +} + +// KnowledgeIntent 问答摘要 +type KnowledgeIntent struct { + GroupID string `json:"group_id"` + IntentID string `json:"intent_id"` + Question IntentQuestion `json:"question"` + SimilarQuestions IntentSimilarQuestions `json:"similar_questions"` + Answers []IntentAnswerRes `json:"answers"` +} + +// IntentAnswerRes 回答返回 +type IntentAnswerRes struct { + Text IntentAnswerText `json:"text"` + Attachments []IntentAnswerAttachmentRes `json:"attachments"` +} + +// IntentAnswerAttachmentRes 回答附件返回 +type IntentAnswerAttachmentRes struct { + MsgType string `json:"msgtype"` + Image IntentAnswerAttachmentImgRes `json:"image,omitempty"` + Video IntentAnswerAttachmentVideoRes `json:"video,omitempty"` + Link IntentAnswerAttachmentLink `json:"link,omitempty"` + MiniProgram IntentAnswerAttachmentMiniProgramRes `json:"miniprogram,omitempty"` +} + +// IntentAnswerAttachmentImgRes 图片类型回答附件返回 +type IntentAnswerAttachmentImgRes struct { + Name string `json:"name"` +} + +// IntentAnswerAttachmentVideoRes 视频类型回答附件返回 +type IntentAnswerAttachmentVideoRes struct { + Name string `json:"name"` +} + +// IntentAnswerAttachmentMiniProgramRes 小程序类型回答附件返回 +type IntentAnswerAttachmentMiniProgramRes struct { + Title string `json:"title"` + AppID string `json:"appid"` + PagePath string `json:"pagepath"` +} + +// ListKnowledgeIntent 知识库问答列表 +// see https://developer.work.weixin.qq.com/document/path/95972#%E8%8E%B7%E5%8F%96%E9%97%AE%E7%AD%94%E5%88%97%E8%A1%A8 +func (r *Client) ListKnowledgeIntent(req *ListKnowledgeIntentRequest) (*ListKnowledgeIntentResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(listKnowledgeIntentURL, accessToken), req); err != nil { + return nil, err + } + result := &ListKnowledgeIntentResponse{} + err = util.DecodeWithError(response, result, "ListKnowledgeIntent") + return result, err +} diff --git a/work/kf/other.go b/work/kf/other.go new file mode 100644 index 0000000..355f614 --- /dev/null +++ b/work/kf/other.go @@ -0,0 +1,47 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 获取视频号绑定状态 + corpQualification = "https://qyapi.weixin.qq.com/cgi-bin/kf/get_corp_qualification?access_token=%s" +) + +// CorpQualificationSchema 获取视频号绑定状态响应内容 +type CorpQualificationSchema struct { + util.CommonError + WechatChannelsBinding bool `json:"wechat_channels_binding"` // 当企业具有绑定成功的视频号时,返回true,否则返回false。 1. 企业申请绑定视频号且由视频号管理员确认后,才为绑定成功状态 2. 至少有一个绑定成功的视频号就会返回true +} + +// GetCorpQualification 获取视频号绑定状态 +// 微信客服可接待的客户数,和企业是否已完成主体验证、是否绑定视频号相关。 +// +// 企业未完成主体验证时,微信客服仅可累计接待100位客户 +// 企业已验证但未绑定视频号时,微信客服仅可累计接待10000位客户 +// 企业已验证且已绑定视频号时,微信客服可接待的客户数不受限制 +// +// 开发者可获取状态后,在应用等地方提示企业去完成主体验证或绑定视频号。 +func (r *Client) GetCorpQualification() (info CorpQualificationSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.HTTPGet(fmt.Sprintf(corpQualification, accessToken)); err != nil { + return info, err + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/sendmsg.go b/work/kf/sendmsg.go new file mode 100644 index 0000000..6eda661 --- /dev/null +++ b/work/kf/sendmsg.go @@ -0,0 +1,47 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 发送消息 + sendMsgAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token=%s" +) + +// SendMsgSchema 发送消息响应内容 +type SendMsgSchema struct { + util.CommonError + MsgID string `json:"msgid"` // 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]* +} + +// SendMsg 发送消息 +// 当微信客户处于“新接入待处理”或“由智能助手接待”状态下,可调用该接口给用户发送消息。 +// 注意仅当微信客户在主动发送消息给客服后的48小时内,企业可发送消息给客户,最多可发送5条消息;若用户继续发送消息,企业可再次下发消息。 +// 支持发送消息类型:文本、图片、语音、视频、文件、图文、小程序、菜单消息、地理位置。 +// 目前该接口允许下发消息条数和下发时限如下: +// +// 用户动作 允许下发条数限制 下发时限 +// 用户发送消息 5条 48 小时 +func (r *Client) SendMsg(options interface{}) (info SendMsgSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(sendMsgAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/sendmsg/message.go b/work/kf/sendmsg/message.go new file mode 100644 index 0000000..e6a6966 --- /dev/null +++ b/work/kf/sendmsg/message.go @@ -0,0 +1,128 @@ +package sendmsg + +// Message 发送消息 +type Message struct { + ToUser string `json:"touser"` // 指定接收消息的客户UserID + OpenKFID string `json:"open_kfid"` // 指定发送消息的客服帐号ID + MsgID string `json:"msgid,omitempty"` // 指定消息ID +} + +// Text 发送文本消息 +type Text struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:text + Text struct { + Content string `json:"content"` // 消息内容,最长不超过2048个字节 + } `json:"text"` // 文本消息 +} + +// Image 发送图片消息 +type Image struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:image + Image struct { + MediaID string `json:"media_id"` // 图片文件id,可以调用上传临时素材接口获取 + } `json:"image"` // 图片消息 +} + +// Voice 发送语音消息 +type Voice struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:voice + Voice struct { + MediaID string `json:"media_id"` // 语音文件id,可以调用上传临时素材接口获取 + } `json:"voice"` // 语音消息 +} + +// Video 发送视频消息 +type Video struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:video + Video struct { + MediaID string `json:"media_id"` // 视频文件id,可以调用上传临时素材接口获取 + } `json:"video"` // 视频消息 +} + +// File 发送文件消息 +type File struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:file + File struct { + MediaID string `json:"media_id"` // 文件id,可以调用上传临时素材接口获取 + } `json:"file"` // 文件消息 +} + +// Link 图文链接消息 +type Link struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:link + Link struct { + Title string `json:"title"` // 标题,不超过128个字节,超过会自动截断 + Desc string `json:"desc"` // 描述,不超过512个字节,超过会自动截断 + URL string `json:"url"` // 点击后跳转的链接。 最长2048字节,请确保包含了协议头(http/https) + ThumbMediaID string `json:"thumb_media_id"` // 缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id + } `json:"link"` // 链接消息 +} + +// MiniProgram 小程序消息 +type MiniProgram struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:miniprogram + MiniProgram struct { + AppID string `json:"appid"` // 小程序appid,必须是关联到企业的小程序应用 + Title string `json:"title"` // 小程序消息标题,最多64个字节,超过会自动截断 + ThumbMediaID string `json:"thumb_media_id"` // 小程序消息封面的mediaid,封面图建议尺寸为520*416 + PagePath string `json:"pagepath"` // 点击消息卡片后进入的小程序页面路径 + } `json:"miniprogram"` // 小程序消息 +} + +// Menu 发送菜单消息 +type Menu struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:msgmenu + MsgMenu struct { + HeadContent string `json:"head_content"` // 消息内容,不多于1024字节 + List []interface{} `json:"list"` // 菜单项配置,不能多余10个 + TailContent string `json:"tail_content"` // 结束文本, 不多于1024字 + } `json:"msgmenu"` +} + +// MenuClick 回复菜单 +type MenuClick struct { + Type string `json:"type"` // 菜单类型: click 回复菜单 + Click struct { + ID string `json:"id"` // 菜单ID, 不少于1字节, 不多于64字节 + Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于128字节 + } `json:"click"` +} + +// MenuView 超链接菜单 +type MenuView struct { + Type string `json:"type"` // 菜单类型: view 超链接菜单 + View struct { + URL string `json:"url"` // 点击后跳转的链接, 不少于1字节, 不多于2048字节 + Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节 + } `json:"view"` +} + +// MenuMiniProgram 小程序菜单 +type MenuMiniProgram struct { + Type string `json:"type"` // 菜单类型: miniprogram 小程序菜单 + MiniProgram struct { + AppID string `json:"appid"` // 小程序appid, 不少于1字节, 不多于32字节 + PagePath string `json:"pagepath"` // 点击后进入的小程序页面, 不少于1字节, 不多于1024字节 + Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节 + } `json:"miniprogram"` +} + +// Location 地理位置消息 +type Location struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:location + Location struct { + Latitude float32 `json:"latitude"` // 纬度, 浮点数,范围为90 ~ -90 + Longitude float32 `json:"longitude"` // 经度, 浮点数,范围为180 ~ -180 + Name string `json:"name"` // 位置名 + Address string `json:"address"` // 地址详情说明 + } `json:"location"` +} diff --git a/work/kf/sendmsg/sendmsg.go b/work/kf/sendmsg/sendmsg.go new file mode 100644 index 0000000..58e7e58 --- /dev/null +++ b/work/kf/sendmsg/sendmsg.go @@ -0,0 +1 @@ +package sendmsg diff --git a/work/kf/sendmsgonevent.go b/work/kf/sendmsgonevent.go new file mode 100644 index 0000000..ea82293 --- /dev/null +++ b/work/kf/sendmsgonevent.go @@ -0,0 +1,54 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 发送事件响应消息 + sendMsgOnEventAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg_on_event?access_token=%s" +) + +// SendMsgOnEventSchema 发送事件响应消息 +type SendMsgOnEventSchema struct { + util.CommonError + MsgID string `json:"msgid"` // 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]* +} + +// SendMsgOnEvent 发送事件响应消息 +// 当特定的事件回调消息包含code字段,或通过接口变更到特定的会话状态,会返回code字段。 +// 开发者可以此code为凭证,调用该接口给用户发送相应事件场景下的消息,如客服欢迎语、客服提示语和会话结束语等。 +// 除”用户进入会话事件”以外,响应消息仅支持会话处于获取该code的会话状态时发送,如将会话转入待接入池时获得的code仅能在会话状态为”待接入池排队中“时发送。 +// +// 目前支持的事件场景和相关约束如下: +// +// 事件场景 允许下发条数 code有效期 支持的消息类型 获取code途径 +// 用户进入会话,用于发送客服欢迎语 1条 20秒 文本、菜单 事件回调 +// 进入接待池,用于发送排队提示语等 1条 48小时 文本 转接会话接口 +// 从接待池接入会话,用于发送非工作时间的提示语或超时未回复的提示语等 1条 48小时 文本 事件回调、转接会话接口 +// 结束会话,用于发送结束会话提示语或满意度评价等 1条 20秒 文本、菜单 事件回调、转接会话接口 +// +// 「进入会话事件」响应消息: +// 如果满足通过API下发欢迎语条件(条件为:1. 企业没有在管理端配置了原生欢迎语;2. 用户在过去48小时里未收过欢迎语,且未向该用户发过消息),则用户进入会话事件会额外返回一个welcome_code,开发者以此为凭据调用接口(填到该接口code参数),即可向客户发送客服欢迎语。 +func (r *Client) SendMsgOnEvent(options interface{}) (info SendMsgOnEventSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(sendMsgOnEventAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/sendmsgonevent/message.go b/work/kf/sendmsgonevent/message.go new file mode 100644 index 0000000..f822e3c --- /dev/null +++ b/work/kf/sendmsgonevent/message.go @@ -0,0 +1,55 @@ +package sendmsgonevent + +// Message 发送事件响应消息 +type Message struct { + Code string `json:"code"` // 事件响应消息对应的code。通过事件回调下发,仅可使用一次。 + MsgID string `json:"msgid"` // 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节,不多于32字节 +} + +// Text 文本消息 +type Text struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:text + Text struct { + Content string `json:"content"` // 消息内容,最长不超过2048个字节 + } `json:"text"` // 文本消息 +} + +// Menu 发送菜单消息 +type Menu struct { + Message + MsgType string `json:"msgtype"` // 消息类型,此时固定为:msgmenu + MsgMenu struct { + HeadContent string `json:"head_content"` // 消息内容,不多于1024字节 + List []interface{} `json:"list"` // 菜单项配置,不能多余10个 + TailContent string `json:"tail_content"` // 结束文本, 不多于1024字 + } `json:"msgmenu"` +} + +// MenuClick 回复菜单 +type MenuClick struct { + Type string `json:"type"` // 菜单类型: click 回复菜单 + Click struct { + ID string `json:"id"` // 菜单ID, 不少于1字节, 不多于64字节 + Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于128字节 + } `json:"click"` +} + +// MenuView 超链接菜单 +type MenuView struct { + Type string `json:"type"` // 菜单类型: view 超链接菜单 + View struct { + URL string `json:"url"` // 点击后跳转的链接, 不少于1字节, 不多于2048字节 + Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节 + } `json:"view"` +} + +// MenuMiniProgram 小程序菜单 +type MenuMiniProgram struct { + Type string `json:"type"` // 菜单类型: miniprogram 小程序菜单 + MiniProgram struct { + AppID string `json:"appid"` // 小程序appid, 不少于1字节, 不多于32字节 + PagePath string `json:"pagepath"` // 点击后进入的小程序页面, 不少于1字节, 不多于1024字节 + Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节 + } `json:"miniprogram"` +} diff --git a/work/kf/servicer.go b/work/kf/servicer.go new file mode 100644 index 0000000..5c34efc --- /dev/null +++ b/work/kf/servicer.go @@ -0,0 +1,108 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 添加接待人员 + receptionistAddAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/add?access_token=%s" + // 删除接待人员 + receptionistDelAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/del?access_token=%s" + // 获取接待人员列表 + receptionistListAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/list?access_token=%s&open_kfid=%s" +) + +// ReceptionistOptions 添加接待人员请求参数 +type ReceptionistOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 +} + +// ReceptionistSchema 添加接待人员响应内容 +type ReceptionistSchema struct { + util.CommonError + ResultList []struct { + UserID string `json:"userid"` + util.CommonError + } `json:"result_list"` +} + +// ReceptionistAdd 添加接待人员 +func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(receptionistAddAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// ReceptionistDel 删除接待人员 +func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) { + var ( + accessToken string + data []byte + ) + accessToken, err = r.ctx.GetAccessToken() + if err != nil { + return + } + data, err = util.PostJSON(fmt.Sprintf(receptionistDelAddr, accessToken), options) + if err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// ReceptionistListSchema 获取接待人员列表响应内容 +type ReceptionistListSchema struct { + util.CommonError + ReceptionistList []struct { + UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid,即open_userid + Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取 + } `json:"servicer_list"` +} + +// ReceptionistList 获取接待人员列表 +func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) { + var ( + accessToken string + data []byte + ) + accessToken, err = r.ctx.GetAccessToken() + if err != nil { + return + } + data, err = util.HTTPGet(fmt.Sprintf(receptionistListAddr, accessToken, kfID)) + if err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/servicestate.go b/work/kf/servicestate.go new file mode 100644 index 0000000..ac20e46 --- /dev/null +++ b/work/kf/servicestate.go @@ -0,0 +1,90 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 获取会话状态 + serviceStateGetAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/get?access_token=%s" + // 变更会话状态 + serviceStateTransAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/trans?access_token=%s" +) + +// ServiceStateGetOptions 获取会话状态请求参数 +type ServiceStateGetOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid +} + +// ServiceStateGetSchema 获取会话状态响应内容 +type ServiceStateGetSchema struct { + util.CommonError + ServiceState int `json:"service_state"` // 当前的会话状态,状态定义参考概述中的表格 + ServiceUserID string `json:"service_userid"` // 接待人员的userid,仅当state=3时有效 +} + +// ServiceStateGet 获取会话状态 +// 0 未处理 新会话接入(客户发信咨询)。可选择:1.直接用API自动回复消息。2.放进待接入池等待接待人员接待。3.指定接待人员(接待人员须处于“正在接待”中,下同)进行接待 +// 1 由智能助手接待 可使用API回复消息。可选择转入待接入池或者指定接待人员处理 +// 2 待接入池排队中 在待接入池中排队等待接待人员接入。可选择转为指定人员接待 +// 3 由人工接待 人工接待中。可选择转接给其他接待人员处理或者结束会话 +// 4 已结束 会话已经结束或未开始。不允许变更会话状态,客户重新发信咨询后会话状态变为“未处理” +// 注:一个微信用户向一个客服帐号发起咨询后,在48h内,或主动结束会话前(包括接待人员手动结束,或企业通过API结束会话),都算是一次会话 +func (r *Client) ServiceStateGet(options ServiceStateGetOptions) (info ServiceStateGetSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(serviceStateGetAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// ServiceStateTransOptions 变更会话状态请求参数 +type ServiceStateTransOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid + ServiceState int `json:"service_state"` // 变更的目标状态,状态定义和所允许的变更可参考概述中的流程图和表格 + ServicerUserID string `json:"servicer_userid"` // 接待人员的userid,当state=3时要求必填,接待人员须处于“正在接待”中 +} + +// ServiceStateTransSchema 变更会话状态响应内容 +type ServiceStateTransSchema struct { + util.CommonError + MsgCode string `json:"msg_code"` // 用于发送响应事件消息的code,将会话初次变更为service_state为2和3时,返回回复语code,service_state为4时,返回结束语code。可用该code调用发送事件响应消息接口给客户发送事件响应消息 +} + +// ServiceStateTrans 变更会话状态 +func (r *Client) ServiceStateTrans(options ServiceStateTransOptions) (info ServiceStateTransSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(serviceStateTransAddr, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/kf/statistic.go b/work/kf/statistic.go new file mode 100644 index 0000000..f4f7416 --- /dev/null +++ b/work/kf/statistic.go @@ -0,0 +1,123 @@ +package kf + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // getCorpStatisticURL 获取「客户数据统计」企业汇总数据 + getCorpStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/get_corp_statistic?access_token=%s" + // getServicerStatisticURL 获取「客户数据统计」接待人员明细数据 + getServicerStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/kf/get_servicer_statistic?access_token=%s" +) + +// GetCorpStatisticRequest 获取「客户数据统计」企业汇总数据请求 +type GetCorpStatisticRequest struct { + OpenKfID string `json:"open_kfid"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` +} + +// GetCorpStatisticResponse 获取「客户数据统计」企业汇总数据响应 +type GetCorpStatisticResponse struct { + util.CommonError + StatisticList []CorpStatisticList `json:"statistic_list"` +} + +// CorpStatisticList 企业汇总统计数据列表 +type CorpStatisticList struct { + StatTime int64 `json:"stat_time"` + Statistic CorpStatistic `json:"statistic"` +} + +// CorpStatistic 企业汇总统计一天的统计数据 +type CorpStatistic struct { + SessionCnt int64 `json:"session_cnt"` + CustomerCnt int64 `json:"customer_cnt"` + CustomerMsgCnt int64 `json:"customer_msg_cnt"` + UpgradeServiceCustomerCnt int64 `json:"upgrade_service_customer_cnt"` + AiSessionReplyCnt int64 `json:"ai_session_reply_cnt"` + AiTransferRate float64 `json:"ai_transfer_rate"` + AiKnowledgeHitRate float64 `json:"ai_knowledge_hit_rate"` + MsgRejectedCustomerCnt int64 `json:"msg_rejected_customer_cnt"` +} + +// GetCorpStatistic 获取「客户数据统计」企业汇总数据 +// see https://developer.work.weixin.qq.com/document/path/95489 +func (r *Client) GetCorpStatistic(req *GetCorpStatisticRequest) (*GetCorpStatisticResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(getCorpStatisticURL, accessToken), req); err != nil { + return nil, err + } + result := &GetCorpStatisticResponse{} + err = util.DecodeWithError(response, result, "GetCorpStatistic") + return result, err +} + +// GetServicerStatisticRequest 获取「客户数据统计」接待人员明细数据请求 +type GetServicerStatisticRequest struct { + OpenKfID string `json:"open_kfid"` + ServicerUserID string `json:"servicer_userid"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` +} + +// GetServicerStatisticResponse 获取「客户数据统计」接待人员明细数据响应 +type GetServicerStatisticResponse struct { + util.CommonError + StatisticList []ServicerStatisticList `json:"statistic_list"` +} + +// ServicerStatisticList 接待人员明细统计数据列表 +type ServicerStatisticList struct { + StatTime int64 `json:"stat_time"` + Statistic ServicerStatistic `json:"statistic"` +} + +// ServicerStatistic 接待人员明细统计一天的统计数据 +type ServicerStatistic struct { + SessionCnt int64 `json:"session_cnt"` + CustomerCnt int64 `json:"customer_cnt"` + CustomerMsgCnt int64 `json:"customer_msg_cnt"` + ReplyRate float64 `json:"reply_rate"` + FirstReplyAverageSec float64 `json:"first_reply_average_sec"` + SatisfactionInvestgateCnt int64 `json:"satisfaction_investgate_cnt"` + SatisfactionParticipationRate float64 `json:"satisfaction_participation_rate"` + SatisfiedRate float64 `json:"satisfied_rate"` + MiddlingRate float64 `json:"middling_rate"` + DissatisfiedRate float64 `json:"dissatisfied_rate"` + UpgradeServiceCustomerCnt int64 `json:"upgrade_service_customer_cnt"` + UpgradeServiceMemberInviteCnt int64 `json:"upgrade_service_member_invite_cnt"` + UpgradeServiceMemberCustomerCnt int64 `json:"upgrade_service_member_customer_cnt"` + UpgradeServiceGroupChatInviteCnt int64 `json:"upgrade_service_groupchat_invite_cnt"` + UpgradeServiceGroupChatCustomerCnt int64 `json:"upgrade_service_groupchat_customer_cnt"` + MsgRejectedCustomerCnt int64 `json:"msg_rejected_customer_cnt"` +} + +// GetServicerStatistic 获取「客户数据统计」接待人员明细数据 +// see https://developer.work.weixin.qq.com/document/path/95490 +func (r *Client) GetServicerStatistic(req *GetServicerStatisticRequest) (*GetServicerStatisticResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(getServicerStatisticURL, accessToken), req); err != nil { + return nil, err + } + result := &GetServicerStatisticResponse{} + err = util.DecodeWithError(response, result, "GetServicerStatistic") + return result, err +} diff --git a/work/kf/syncmsg.go b/work/kf/syncmsg.go new file mode 100644 index 0000000..55ac079 --- /dev/null +++ b/work/kf/syncmsg.go @@ -0,0 +1,108 @@ +package kf + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/kf/syncmsg" +) + +const ( + // 获取消息 + syncMsgAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=%s" +) + +// 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,omitempty"` // 语音消息类型,0-Amr 1-Silk,默认0。可通过该参数控制返回的语音格式,开发者可按需选择自己程序支持的一种格式 + OpenKfID string `json:"open_kfid,omitempty"` // 指定拉取某个客服帐号的消息,否则默认返回有权限的客服帐号的消息。当客服帐号较多,建议按open_kfid来拉取以获取更好的性能。 +} + +// SyncMsgSchema 获取消息查询响应内容 +type syncMsgSchema struct { + ErrCode int32 `json:"errcode"` // 返回码 + ErrMsg string `json:"errmsg"` // 错误码描述 + NextCursor string `json:"next_cursor"` // 下次调用带上该值,则从当前的位置继续往后拉,以实现增量拉取。强烈建议对改该字段入库保存,每次请求读取带上,请求结束后更新。避免因意外丢,导致必须从头开始拉取,引起消息延迟。 + HasMore uint32 `json:"has_more"` // 是否还有更多数据。0-否;1-是。不能通过判断msg_list是否空来停止拉取,可能会出现has_more为1,而msg_list为空的情况 + MsgList []map[string]interface{} `json:"msg_list"` // 消息列表 +} + +// SyncMsgSchema 获取消息查询响应内容 +type SyncMsgSchema struct { + ErrCode int32 `json:"errcode"` // 返回码 + ErrMsg string `json:"errmsg"` // 错误码描述 + NextCursor string `json:"next_cursor"` // 下次调用带上该值则从该key值往后拉,用于增量拉取 + HasMore uint32 `json:"has_more"` // 是否还有更多数据。0-否;1-是。不能通过判断msg_list是否空来停止拉取,可能会出现has_more为1,而msg_list为空的情况 + MsgList []syncmsg.Message `json:"msg_list"` // 消息列表 +} + +// SyncMsg 获取消息 +func (r *Client) SyncMsg(options SyncMsgOptions) (info SyncMsgSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(syncMsgAddr, accessToken), options); err != nil { + return + } + originInfo := syncMsgSchema{} + if err = json.Unmarshal(data, &originInfo); err != nil { + return + } + if originInfo.ErrCode != 0 { + return info, errors.New(originInfo.ErrMsg) + } + msgList := make([]syncmsg.Message, 0) + if len(originInfo.MsgList) > 0 { + for _, msg := range originInfo.MsgList { + newMsg := syncmsg.Message{} + if val, ok := msg["msgid"].(string); ok { + newMsg.MsgID = val + } + if val, ok := msg["open_kfid"].(string); ok { + newMsg.OpenKFID = val + } + if val, ok := msg["external_userid"].(string); ok { + newMsg.ExternalUserID = val + } + if val, ok := msg["send_time"].(float64); ok { + newMsg.SendTime = uint64(val) + } + if val, ok := msg["origin"].(float64); ok { + newMsg.Origin = uint32(val) + } + + if val, ok := msg["msgtype"].(string); ok { + newMsg.MsgType = val + } + if newMsg.MsgType == "event" { + if event, ok := msg["event"].(map[string]interface{}); ok { + if eType, ok := event["event_type"].(string); ok { + newMsg.EventType = eType + } + } + } + originData, err := json.Marshal(msg) + if err != nil { + return info, err + } + newMsg.OriginData = originData + msgList = append(msgList, newMsg) + } + } + return SyncMsgSchema{ + ErrCode: originInfo.ErrCode, + ErrMsg: originInfo.ErrMsg, + NextCursor: originInfo.NextCursor, + HasMore: originInfo.HasMore, + MsgList: msgList, + }, nil +} diff --git a/work/kf/syncmsg/message.go b/work/kf/syncmsg/message.go new file mode 100644 index 0000000..91aaffc --- /dev/null +++ b/work/kf/syncmsg/message.go @@ -0,0 +1,165 @@ +package syncmsg + +// BaseMessage 接收消息 +type BaseMessage struct { + MsgID string `json:"msgid"` // 消息ID + OpenKFID string `json:"open_kfid"` // 客服帐号ID(msgtype为event,该字段不返回) + ExternalUserID string `json:"external_userid"` // 客户UserID(msgtype为event,该字段不返回) + ReceptionistUserID string `json:"servicer_userid"` // 接待客服userID + SendTime uint64 `json:"send_time"` // 消息发送时间 + Origin uint32 `json:"origin"` // 消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息 +} + +// Text 文本消息 +type Text struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:text + Text struct { + Content string `json:"content"` // 文本内容 + MenuID string `json:"menu_id"` // 客户点击菜单消息,触发的回复消息中附带的菜单ID + } `json:"text"` // 文本消息 +} + +// Image 图片消息 +type Image struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:image + Image struct { + MediaID string `json:"media_id"` // 图片文件ID + } `json:"image"` // 图片消息 +} + +// Voice 语音消息 +type Voice struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:voice + Voice struct { + MediaID string `json:"media_id"` // 语音文件ID + } `json:"voice"` // 语音消息 +} + +// Video 视频消息 +type Video struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:video + Video struct { + MediaID string `json:"media_id"` // 文件ID + } `json:"video"` // 视频消息 +} + +// File 文件消息 +type File struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:file + File struct { + MediaID string `json:"media_id"` // 文件ID + } `json:"file"` // 文件消息 +} + +// Location 地理位置消息 +type Location struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:location + Location struct { + Latitude float32 `json:"latitude"` // 纬度 + Longitude float32 `json:"longitude"` // 经度 + Name string `json:"name"` // 位置名 + Address string `json:"address"` // 地址详情说明 + } `json:"location"` // 地理位置消息 +} + +// Link 链接消息 +type Link struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:link + Link struct { + Title string `json:"title"` // 标题 + Desc string `json:"desc"` // 描述 + URL string `json:"url"` // 点击后跳转的链接 + PicURL string `json:"pic_url"` // 缩略图链接 + } `json:"link"` // 链接消息 +} + +// BusinessCard 名片消息 +type BusinessCard struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:business_card + BusinessCard struct { + UserID string `json:"userid"` // 名片 userid + } `json:"business_card"` // 名片消息 +} + +// MiniProgram 小程序消息 +type MiniProgram struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:miniprogram + MiniProgram struct { + AppID string `json:"appid"` // 小程序appid,必须是关联到企业的小程序应用 + Title string `json:"title"` // 小程序消息标题,最多64个字节,超过会自动截断 + ThumbMediaID string `json:"thumb_media_id"` // 小程序消息封面的mediaid,封面图建议尺寸为520*416 + PagePath string `json:"pagepath"` // 点击消息卡片后进入的小程序页面路径 + } `json:"miniprogram"` // 小程序消息 +} + +// EventMessage 事件消息 +type EventMessage struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:event + Event struct { + EventType string `json:"event_type"` // 事件类型 + } `json:"event"` // 事件消息 +} + +// EnterSessionEvent 用户进入会话事件 +type EnterSessionEvent struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:event + Event struct { + EventType string `json:"event_type"` // 事件类型。此处固定为:enter_session + OpenKFID string `json:"open_kfid"` // 客服账号ID + ExternalUserID string `json:"external_userid"` // 客户UserID + Scene string `json:"scene"` // 进入会话的场景值,获取客服帐号链接开发者自定义的场景值 + SceneParam string `json:"scene_param"` // 进入会话的自定义参数,获取客服帐号链接返回的url,开发者按规范拼接的scene_param参数 + WelcomeCode string `json:"welcome_code"` // 如果满足发送欢迎语条件(条件为:1. 企业没有在管理端配置了原生欢迎语;2. 用户在过去48小时里未收过欢迎语,且未向该用户发过消息),会返回该字段。可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。 + } `json:"event"` // 事件消息 +} + +// MsgSendFailEvent 消息发送失败事件 +type MsgSendFailEvent struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:event + Event struct { + EventType string `json:"event_type"` // 事件类型。此处固定为:msg_send_fail + OpenKFID string `json:"open_kfid"` // 客服账号ID + ExternalUserID string `json:"external_userid"` // 客户UserID + FailMsgID string `json:"fail_msgid"` // 发送失败的消息msgid + FailType uint32 `json:"fail_type"` // 失败类型。0-未知原因 1-客服账号已删除 2-应用已关闭 4-会话已过期,超过48小时 5-会话已关闭 6-超过5条限制 7-未绑定视频号 8-主体未验证 9-未绑定视频号且主体未验证 10-用户拒收 + } `json:"event"` // 事件消息 +} + +// ReceptionistStatusChangeEvent 客服人员接待状态变更事件 +type ReceptionistStatusChangeEvent struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:event + Event struct { + EventType string `json:"event_type"` // 事件类型。此处固定为:servicer_status_change + ReceptionistUserID string `json:"servicer_userid"` // 客服人员userid + OpenKFID string `json:"open_kfid"` // 客服帐号ID + Status uint32 `json:"status"` // 状态类型。1-接待中 2-停止接待 + } `json:"event"` +} + +// SessionStatusChangeEvent 会话状态变更事件 +type SessionStatusChangeEvent struct { + BaseMessage + MsgType string `json:"msgtype"` // 消息类型,此时固定为:event + Event struct { + EventType string `json:"event_type"` // 事件类型。此处固定为:session_status_change + OpenKFID string `json:"open_kfid"` // 客服账号ID + ExternalUserID string `json:"external_userid"` // 客户UserID + ChangeType uint32 `json:"change_type"` // 变更类型。1-从接待池接入会话 2-转接会话 3-结束会话 + OldReceptionistUserID string `json:"old_servicer_userid"` // 老的客服人员userid。仅change_type为2和3有值 + NewReceptionistUserID string `json:"new_servicer_userid"` // 新的客服人员userid。仅change_type为1和2有值 + MsgCode string `json:"msg_code"` // 用于发送事件响应消息的code,仅change_type为1和3时,会返回该字段。可用该msg_code调用发送事件响应消息接口给客户发送回复语或结束语。 + } `json:"event"` // 事件消息 +} diff --git a/work/kf/syncmsg/syncmsg.go b/work/kf/syncmsg/syncmsg.go new file mode 100644 index 0000000..43b5e67 --- /dev/null +++ b/work/kf/syncmsg/syncmsg.go @@ -0,0 +1,103 @@ +package syncmsg + +import "encoding/json" + +// Message 同步的消息内容 +type Message struct { + MsgID string `json:"msgid"` // 消息ID + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 客户UserID + ReceptionistUserID string `json:"servicer_userid"` // 接待客服userID + SendTime uint64 `json:"send_time"` // 消息发送时间 + Origin uint32 `json:"origin"` // 消息来源。3-客户回复的消息 4-系统推送的消 息 + MsgType string `json:"msgtype"` // 消息类型 + EventType string `json:"event_type"` // 事件类型 + OriginData []byte `json:"origin_data"` // 原始数据内容 +} + +// GetOriginMessage 获取原始消息 +func (r Message) GetOriginMessage() (info []byte) { + return r.OriginData +} + +// GetTextMessage 获取文本消息 +func (r Message) GetTextMessage() (info Text, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetImageMessage 获取图片消息 +func (r Message) GetImageMessage() (info Image, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetVoiceMessage 获取语音消息 +func (r Message) GetVoiceMessage() (info Voice, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetVideoMessage 获取视频消息 +func (r Message) GetVideoMessage() (info Video, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetFileMessage 获取文件消息 +func (r Message) GetFileMessage() (info File, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetLocationMessage 获取文件消息 +func (r Message) GetLocationMessage() (info Location, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetLinkMessage 获取链接消息 +func (r Message) GetLinkMessage() (info Link, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetBusinessCardMessage 获取名片消息 +func (r Message) GetBusinessCardMessage() (info BusinessCard, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetMiniProgramMessage 获取小程序消息 +func (r Message) GetMiniProgramMessage() (info MiniProgram, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetEnterSessionEvent 用户进入会话事件 +func (r Message) GetEnterSessionEvent() (info EnterSessionEvent, err error) { + err = json.Unmarshal(r.OriginData, &info) + info.OpenKFID = info.Event.OpenKFID + info.ExternalUserID = info.Event.ExternalUserID + return info, err +} + +// GetMsgSendFailEvent 消息发送失败事件 +func (r Message) GetMsgSendFailEvent() (info MsgSendFailEvent, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetReceptionistStatusChangeEvent 客服人员接待状态变更事件 +func (r Message) GetReceptionistStatusChangeEvent() (info ReceptionistStatusChangeEvent, err error) { + err = json.Unmarshal(r.OriginData, &info) + return info, err +} + +// GetSessionStatusChangeEvent 会话状态变更事件 +func (r Message) GetSessionStatusChangeEvent() (info SessionStatusChangeEvent, err error) { + err = json.Unmarshal(r.OriginData, &info) + info.OpenKFID = info.Event.OpenKFID + info.ExternalUserID = info.Event.ExternalUserID + return info, err +} diff --git a/work/kf/upgrade.go b/work/kf/upgrade.go new file mode 100644 index 0000000..73f2da6 --- /dev/null +++ b/work/kf/upgrade.go @@ -0,0 +1,179 @@ +package kf + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 获取配置的专员与客户群 + upgradeServiceConfigAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/get_upgrade_service_config?access_token=%s" + // 为客户升级为专员或客户群服务 + upgradeService = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/upgrade_service?access_token=%s" + // 为客户取消推荐 + upgradeServiceCancel = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/cancel_upgrade_service?access_token=%s" +) + +// UpgradeServiceConfigSchema 获取配置的专员与客户群 +type UpgradeServiceConfigSchema struct { + util.CommonError + MemberRange struct { + UserIDList []string `json:"userid_list"` // 专员userid列表 + DepartmentIDList []string `json:"department_id_list"` // 专员部门列表 + } `json:"member_range"` // 专员服务配置范围 + GroupChatRange struct { + ChatIDList []string `json:"chat_id_list"` // 客户群列表 + } `json:"groupchat_range"` // 客户群配置范围 +} + +// UpgradeServiceConfig 获取配置的专员与客户群 +func (r *Client) UpgradeServiceConfig() (info UpgradeServiceConfigSchema, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.HTTPGet(fmt.Sprintf(upgradeServiceConfigAddr, accessToken)); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// UpgradeServiceOptions 为客户升级为专员或客户群服务请求参数 +type UpgradeServiceOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid + Type int `json:"type"` // 表示是升级到专员服务还是客户群服务。1:专员服务。2:客户群服务 + Member struct { + UserID string `json:"userid"` // 服务专员的userid + Wording string `json:"wording"` // 推荐语 + } `json:"member"` // 推荐的服务专员,type等于1时有效 + GroupChat struct { + ChatID string `json:"chat_id"` // 客户群id + Wording string `json:"wording"` // 推荐语 + } `json:"groupchat"` // 推荐的客户群,type等于2时有效 +} + +// UpgradeService 为客户升级为专员或客户群服务 +func (r *Client) UpgradeService(options UpgradeServiceOptions) (info util.CommonError, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(upgradeService, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// UpgradeMemberServiceOptions 为客户升级为专员服务请求参数 +type UpgradeMemberServiceOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid + Type int `json:"type"` // 表示是升级到专员服务还是客户群服务。1:专员服务 + Member struct { + UserID string `json:"userid"` // 服务专员的userid + Wording string `json:"wording"` // 推荐语 + } `json:"member"` // 推荐的服务专员,type等于1时有效 +} + +// UpgradeMemberService 为客户升级为专员服务 +func (r *Client) UpgradeMemberService(options UpgradeMemberServiceOptions) (info util.CommonError, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(upgradeService, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// UpgradeServiceGroupChatOptions 为客户升级为客户群服务请求参数 +type UpgradeServiceGroupChatOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid + Type int `json:"type"` // 表示是升级到专员服务还是客户群服务。2:客户群服务 + GroupChat struct { + ChatID string `json:"chat_id"` // 客户群id + Wording string `json:"wording"` // 推荐语 + } `json:"groupchat"` // 推荐的客户群,type等于2时有效 +} + +// UpgradeGroupChatService 为客户升级为客户群服务 +func (r *Client) UpgradeGroupChatService(options UpgradeServiceGroupChatOptions) (info util.CommonError, err error) { + var ( + accessToken string + data []byte + ) + accessToken, err = r.ctx.GetAccessToken() + if err != nil { + return + } + data, err = util.PostJSON(fmt.Sprintf(upgradeService, accessToken), options) + if err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} + +// UpgradeServiceCancelOptions 为客户取消推荐 +type UpgradeServiceCancelOptions struct { + OpenKFID string `json:"open_kfid"` // 客服帐号ID + ExternalUserID string `json:"external_userid"` // 微信客户的external_userid +} + +// UpgradeServiceCancel 为客户取消推荐 +func (r *Client) UpgradeServiceCancel(options UpgradeServiceCancelOptions) (info util.CommonError, err error) { + var ( + accessToken string + data []byte + ) + if accessToken, err = r.ctx.GetAccessToken(); err != nil { + return + } + if data, err = util.PostJSON(fmt.Sprintf(upgradeServiceCancel, accessToken), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, NewSDKErr(info.ErrCode, info.ErrMsg) + } + return info, nil +} diff --git a/work/material/client.go b/work/material/client.go new file mode 100644 index 0000000..d10f980 --- /dev/null +++ b/work/material/client.go @@ -0,0 +1,17 @@ +package material + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 素材管理接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/material/media.go b/work/material/media.go new file mode 100644 index 0000000..b32785a --- /dev/null +++ b/work/material/media.go @@ -0,0 +1,98 @@ +package material + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // uploadImgURL 上传图片 + uploadImgURL = "https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s" + // uploadTempFile 上传临时素材 + 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" +) + +// UploadImgResponse 上传图片响应 +type UploadImgResponse struct { + util.CommonError + URL string `json:"url"` +} + +// UploadTempFileResponse 上传临时素材响应 +type UploadTempFileResponse struct { + util.CommonError + MediaID string `json:"media_id"` + CreateAt string `json:"created_at"` + Type string `json:"type"` +} + +// UploadAttachmentResponse 上传资源附件响应 +type UploadAttachmentResponse struct { + util.CommonError + MediaID string `json:"media_id"` + CreateAt int64 `json:"created_at"` + Type string `json:"type"` +} + +// UploadImg 上传图片 +// @see https://developer.work.weixin.qq.com/document/path/90256 +func (r *Client) UploadImg(filename string) (*UploadImgResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostFile("media", filename, fmt.Sprintf(uploadImgURL, accessToken)); 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) +func (r *Client) UploadTempFile(filename string, mediaType string) (*UploadTempFileResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostFile("media", filename, fmt.Sprintf(uploadTempFile, accessToken, mediaType)); err != nil { + return nil, err + } + result := &UploadTempFileResponse{} + err = util.DecodeWithError(response, result, "UploadTempFile") + return result, err +} + +// UploadAttachment 上传附件资源 +// @see https://developer.work.weixin.qq.com/document/path/95098 +// @mediaType 媒体文件类型,分别有图片(image)、视频(video)、普通文件(file) +// @attachment_type 附件类型,不同的附件类型用于不同的场景。1:朋友圈;2:商品图册 +func (r *Client) UploadAttachment(filename string, mediaType string, attachmentType int) (*UploadAttachmentResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostFile("media", filename, fmt.Sprintf(uploadAttachment, accessToken, mediaType, attachmentType)); err != nil { + return nil, err + } + result := &UploadAttachmentResponse{} + err = util.DecodeWithError(response, result, "UploadAttachment") + return result, err +} diff --git a/work/message/client.go b/work/message/client.go new file mode 100644 index 0000000..b50b37e --- /dev/null +++ b/work/message/client.go @@ -0,0 +1,16 @@ +// Package message 消息推送,实现企业微信消息推送相关接口:https://developer.work.weixin.qq.com/document/path/90235 +package message + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 消息推送接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ctx} +} diff --git a/work/message/message.go b/work/message/message.go new file mode 100644 index 0000000..ef6d24d --- /dev/null +++ b/work/message/message.go @@ -0,0 +1,130 @@ +package message + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // 发送应用消息的接口地址 + sendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" +) + +type ( + // SendRequestCommon 发送应用消息请求公共参数 + SendRequestCommon struct { + // 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。 特殊情况:指定为"@all",则向该企业应用的全部成员发送 + ToUser string `json:"touser"` + // 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。 当touser为"@all"时忽略本参数 + ToParty string `json:"toparty"` + // 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。 当touser为"@all"时忽略本参数 + ToTag string `json:"totag"` + // 消息类型,此时固定为:text + MsgType string `json:"msgtype"` + // 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 + AgentID string `json:"agentid"` + // 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 + Safe int `json:"safe"` + // 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。 + EnableIDTrans int `json:"enable_id_trans"` + // 表示是否开启重复消息检查,0表示否,1表示是,默认0 + EnableDuplicateCheck int `json:"enable_duplicate_check"` + // 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时 + DuplicateCheckInterval int `json:"duplicate_check_interval"` + } + // SendResponse 发送应用消息响应参数 + SendResponse struct { + util.CommonError + InvalidUser string `json:"invaliduser"` // 不合法的userid,不区分大小写,统一转为小写 + InvalidParty string `json:"invalidparty"` // 不合法的partyid + InvalidTag string `json:"invalidtag"` // 不合法的标签id + UnlicensedUser string `json:"unlicenseduser"` // 没有基础接口许可(包含已过期)的userid + MsgID string `json:"msgid"` // 消息id + ResponseCode string `json:"response_code"` + } + + // SendTextRequest 发送文本消息的请求 + SendTextRequest struct { + *SendRequestCommon + Text TextField `json:"text"` + } + // TextField 文本消息参数 + TextField struct { + // 消息内容,最长不超过2048个字节,超过将截断(支持id转译) + 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/90236 +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{} + err = util.DecodeWithError(response, result, apiName) + // 返回数据 + return result, err +} + +// 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方法,按照企业微信消息推送的接口文档传对应消息类型的参数来使用 diff --git a/work/msgaudit/README.md b/work/msgaudit/README.md new file mode 100644 index 0000000..3595500 --- /dev/null +++ b/work/msgaudit/README.md @@ -0,0 +1,102 @@ +企业微信会话存档SDK(基于企业微信C版官方SDK封装),暂时只支持在`linux`环境下使用当前SDK。 + + +### 官方文档地址 +https://open.work.weixin.qq.com/api/doc/90000/90135/91774 + +### 使用方式 + +1、安装 go module +> go get -u github.com/silenceper/wechat/v2 + +2、从 `github.com/silenceper/wechat/v2/work/msgaudit/lib` 文件夹下复制 `libWeWorkFinanceSdk_C.so` 动态库文件到系统动态链接库默认文件夹下,或者复制到任意文件夹并在当前文件夹下执行 `export LD_LIBRARY_PATH=$(pwd)`命令设置动态链接库检索地址后即可正常使用 + +3、编译要求 +- 开启CGO: `CGO_ENABLED=1` +- 增加tags参数`msgaudit`: `go build -tags msgaudit`或者`go run -tags msgaudit main.go` + +### Example + +```go +package main + +import ( + "bytes" + "fmt" + "github.com/silenceper/wechat/v2" + "github.com/silenceper/wechat/v2/work/msgaudit" + "github.com/silenceper/wechat/v2/work/config" + "io/ioutil" + "os" + "path" +) + +func main() { + //初始化客户端 + wechatClient := wechat.NewWechat() + + workClient := wechatClient.GetWork(&config.Config{ + CorpID: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + CorpSecret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + RasPrivateKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + }) + + client, err := workClient.GetMsgAudit() + if err != nil { + fmt.Printf("SDK 初始化失败:%v \n", err) + return + } + + //同步消息 + chatDataList, err := client.GetChatData(0, 100, "", "", 3) + if err != nil { + fmt.Printf("消息同步失败:%v \n", err) + return + } + + for _, chatData := range chatDataList { + //消息解密 + chatInfo, err := client.DecryptData(chatData.EncryptRandomKey, chatData.EncryptChatMsg) + if err != nil { + fmt.Printf("消息解密失败:%v \n", err) + return + } + + if chatInfo.Type == "image" { + image, _ := chatInfo.GetImageMessage() + sdkFileID := image.Image.SdkFileID + + isFinish := false + buffer := bytes.Buffer{} + indexBuf := "" + for !isFinish { + //获取媒体数据 + mediaData, err := client.GetMediaData(indexBuf, sdkFileID, "", "", 5) + if err != nil { + fmt.Printf("媒体数据拉取失败:%v \n", err) + return + } + buffer.Write(mediaData.Data) + if mediaData.IsFinish { + isFinish = mediaData.IsFinish + } + indexBuf = mediaData.OutIndexBuf + } + filePath, _ := os.Getwd() + filePath = path.Join(filePath, "test.png") + err := ioutil.WriteFile(filePath, buffer.Bytes(), 0666) + if err != nil { + fmt.Printf("文件存储失败:%v \n", err) + return + } + break + } + } + + //释放SDK实例 + client.Free() +} + + + +``` \ No newline at end of file diff --git a/work/msgaudit/chat.go b/work/msgaudit/chat.go new file mode 100644 index 0000000..229282f --- /dev/null +++ b/work/msgaudit/chat.go @@ -0,0 +1,207 @@ +package msgaudit + +import "encoding/json" + +// ChatDataResponse 会话存档消息响应数据 +type ChatDataResponse struct { + Error + ChatDataList []ChatData `json:"chatdata,omitempty"` +} + +// IsError 判断是否正确响应 +func (c ChatDataResponse) IsError() bool { + return c.ErrCode != 0 +} + +// ChatData 会话存档原始数据 +type ChatData struct { + Seq uint64 `json:"seq,omitempty"` // 消息的seq值,标识消息的序号。再次拉取需要带上上次回包中最大的seq。Uint64类型,范围0-pow(2,64)-1 + MsgID string `json:"msgid,omitempty"` // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。 + PublickeyVer uint32 `json:"publickey_ver,omitempty"` // 加密此条消息使用的公钥版本号。 + EncryptRandomKey string `json:"encrypt_random_key,omitempty"` // 使用publickey_ver指定版本的公钥进行非对称加密后base64加密的内容,需要业务方先base64 decode处理后,再使用指定版本的私钥进行解密,得出内容。 + EncryptChatMsg string `json:"encrypt_chat_msg,omitempty"` // 消息密文。需要业务方使用将encrypt_random_key解密得到的内容,与encrypt_chat_msg,传入sdk接口DecryptData,得到消息明文。 +} + +// ChatMessage 会话存档消息 +type ChatMessage struct { + ID string // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。 + From string // 消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid。 + ToList []string // 消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。 + Action string // 消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。 + Type string // 消息类型 + originData []byte // 原始消息对象 +} + +// GetOriginMessage 获取消息原始数据 +func (c ChatMessage) GetOriginMessage() (msg map[string]interface{}, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetTextMessage 获取文本消息 +func (c ChatMessage) GetTextMessage() (msg TextMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetImageMessage 获取图片消息 +func (c ChatMessage) GetImageMessage() (msg ImageMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetRevokeMessage 获取撤回消息 +func (c ChatMessage) GetRevokeMessage() (msg RevokeMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetAgreeMessage 获取同意会话聊天内容 +func (c ChatMessage) GetAgreeMessage() (msg AgreeMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVoiceMessage 获取语音消息 +func (c ChatMessage) GetVoiceMessage() (msg VoiceMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVideoMessage 获取视频消息 +func (c ChatMessage) GetVideoMessage() (msg VideoMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetCardMessage 获取名片消息 +func (c ChatMessage) GetCardMessage() (msg CardMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetLocationMessage 获取位置消息 +func (c ChatMessage) GetLocationMessage() (msg LocationMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetEmotionMessage 获取表情消息 +func (c ChatMessage) GetEmotionMessage() (msg EmotionMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetFileMessage 获取文件消息 +func (c ChatMessage) GetFileMessage() (msg FileMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetLinkMessage 获取链接消息 +func (c ChatMessage) GetLinkMessage() (msg LinkMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetWeappMessage 获取小程序消息 +func (c ChatMessage) GetWeappMessage() (msg WeappMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetChatRecordMessage 获取会话记录消息 +func (c ChatMessage) GetChatRecordMessage() (msg ChatRecordMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetTodoMessage 获取待办消息 +func (c ChatMessage) GetTodoMessage() (msg TodoMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVoteMessage 获取投票消息 +func (c ChatMessage) GetVoteMessage() (msg VoteMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetCollectMessage 获取填表消息 +func (c ChatMessage) GetCollectMessage() (msg CollectMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetRedpacketMessage 获取红包消息 +func (c ChatMessage) GetRedpacketMessage() (msg RedpacketMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMeetingMessage 获取会议邀请消息 +func (c ChatMessage) GetMeetingMessage() (msg MeetingMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetDocMessage 获取在线文档消息 +func (c ChatMessage) GetDocMessage() (msg DocMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMarkdownMessage 获取MarkDown格式消息 +func (c ChatMessage) GetMarkdownMessage() (msg MarkdownMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetNewsMessage 获取图文消息 +func (c ChatMessage) GetNewsMessage() (msg NewsMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetCalendarMessage 获取日程消息 +func (c ChatMessage) GetCalendarMessage() (msg CalendarMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMixedMessage 获取混合消息 +func (c ChatMessage) GetMixedMessage() (msg MixedMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMeetingVoiceCallMessage 获取音频存档消息 +func (c ChatMessage) GetMeetingVoiceCallMessage() (msg MeetingVoiceCallMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVoipDocShareMessage 获取音频共享消息 +func (c ChatMessage) GetVoipDocShareMessage() (msg VoipDocShareMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetExternalRedPacketMessage 获取互通红包消息 +func (c ChatMessage) GetExternalRedPacketMessage() (msg ExternalRedPacketMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetSphFeedMessage 获取视频号消息 +func (c ChatMessage) GetSphFeedMessage() (msg SphFeedMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetSwitchMessage 获取切换企业日志 +func (c ChatMessage) GetSwitchMessage() (msg SwitchMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} diff --git a/work/msgaudit/client_linux.go b/work/msgaudit/client_linux.go new file mode 100644 index 0000000..80d21a2 --- /dev/null +++ b/work/msgaudit/client_linux.go @@ -0,0 +1,262 @@ +//go:build linux && cgo && msgaudit +// +build linux,cgo,msgaudit + +// Package msgaudit only for linux +package msgaudit + +// #cgo LDFLAGS: -L${SRCDIR}/lib -lWeWorkFinanceSdk_C +// #cgo CFLAGS: -I ./lib/ +// #include +// #include "WeWorkFinanceSdk_C.h" +import "C" +import ( + "encoding/json" + "unsafe" + + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/config" +) + +// Client 会话存档 +type Client struct { + ptr *C.WeWorkFinanceSdk_t + privateKey string +} + +// NewClient 初始会话会话存档实例 +/** +* 初始化函数 +* Return值=0表示该API调用成功 +* +* @param [in] sdk NewSdk返回的sdk指针 +* @param [in] corpid 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 +* @param [in] secret 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 +* @param [in] privateKey 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存 +* +* +* @return 返回是否初始化成功 +* 0 - 成功 +* !=0 - 失败 + */ +func NewClient(cfg *config.Config) (*Client, error) { + ptr := C.NewSdk() + corpIDC := C.CString(cfg.CorpID) + corpSecretC := C.CString(cfg.CorpSecret) + defer func() { + C.free(unsafe.Pointer(corpIDC)) + C.free(unsafe.Pointer(corpSecretC)) + }() + retC := C.Init(ptr, corpIDC, corpSecretC) + ret := int(retC) + if ret != 0 { + return nil, NewSDKErr(ret) + } + return &Client{ + ptr: ptr, + privateKey: cfg.RasPrivateKey, + }, nil +} + +// Free 释放SDK实例是可调用该方法释放内存 +func (s *Client) Free() { + if s.ptr == nil { + return + } + C.DestroySdk(s.ptr) + s.ptr = nil +} + +// GetChatData 拉取聊天记录函数 +/** +* 拉取聊天记录函数 +* +* +* @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 +* @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 +* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 +* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 +* @param [in] timeout 超时时间,单位秒 +* @return chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。示例如下: + +{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]} +*/ +func (s *Client) GetChatData(seq uint64, limit uint64, proxy string, passwd string, timeout int) ([]ChatData, error) { + proxyC := C.CString(proxy) + passwdC := C.CString(passwd) + chatSlice := C.NewSlice() + defer func() { + C.free(unsafe.Pointer(proxyC)) + C.free(unsafe.Pointer(passwdC)) + C.FreeSlice(chatSlice) + }() + + if s.ptr == nil { + return nil, NewSDKErr(10002) + } + + retC := C.GetChatData(s.ptr, C.ulonglong(seq), C.uint(limit), proxyC, passwdC, C.int(timeout), chatSlice) + ret := int(retC) + if ret != 0 { + return nil, NewSDKErr(ret) + } + buf := s.GetContentFromSlice(chatSlice) + + var data ChatDataResponse + err := json.Unmarshal(buf, &data) + if err != nil { + return nil, err + } + if data.ErrCode != 0 { + return nil, data.Error + } + return data.ChatDataList, nil +} + +// GetRawChatData 拉取聊天记录函数 +/** +* 拉取聊天记录函数 +* +* +* @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 +* @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 +* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 +* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 +* @param [in] timeout 超时时间,单位秒 +* @return chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。示例如下: + +{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]} +*/ +func (s *Client) GetRawChatData(seq uint64, limit uint64, proxy string, passwd string, timeout int) (ChatDataResponse, error) { + proxyC := C.CString(proxy) + passwdC := C.CString(passwd) + chatSlice := C.NewSlice() + defer func() { + C.free(unsafe.Pointer(proxyC)) + C.free(unsafe.Pointer(passwdC)) + C.FreeSlice(chatSlice) + }() + + if s.ptr == nil { + return ChatDataResponse{}, NewSDKErr(10002) + } + + retC := C.GetChatData(s.ptr, C.ulonglong(seq), C.uint(limit), proxyC, passwdC, C.int(timeout), chatSlice) + ret := int(retC) + if ret != 0 { + return ChatDataResponse{}, NewSDKErr(ret) + } + buf := s.GetContentFromSlice(chatSlice) + + var data ChatDataResponse + err := json.Unmarshal(buf, &data) + return data, err +} + +// DecryptData 解析密文.企业微信自有解密内容 +/** +* @brief 解析密文.企业微信自有解密内容 +* @param [in] encrypt_key, getchatdata返回的encrypt_random_key,使用企业自持对应版本秘钥RSA解密后的内容 +* @param [in] encrypt_msg, getchatdata返回的encrypt_chat_msg +* @param [out] msg, 解密的消息明文 +* @return 返回是否调用成功 +* 0 - 成功 +* !=0 - 失败 + */ +func (s *Client) DecryptData(encryptRandomKey string, encryptMsg string) (msg ChatMessage, err error) { + encryptKey, err := util.RSADecryptBase64(s.privateKey, encryptRandomKey) + if err != nil { + return msg, err + } + encryptKeyC := C.CString(string(encryptKey)) + encryptMsgC := C.CString(encryptMsg) + msgSlice := C.NewSlice() + defer func() { + C.free(unsafe.Pointer(encryptKeyC)) + C.free(unsafe.Pointer(encryptMsgC)) + C.FreeSlice(msgSlice) + }() + + retC := C.DecryptData(encryptKeyC, encryptMsgC, msgSlice) + ret := int(retC) + if ret != 0 { + return msg, NewSDKErr(ret) + } + buf := s.GetContentFromSlice(msgSlice) + + // handle illegal escape character in text + for i := 0; i < len(buf); { + if buf[i] < 0x20 { + buf = append(buf[:i], buf[i+1:]...) + continue + } + i++ + } + + var baseMessage BaseMessage + err = json.Unmarshal(buf, &baseMessage) + if err != nil { + return msg, err + } + + msg.ID = baseMessage.MsgID + msg.From = baseMessage.From + msg.ToList = baseMessage.ToList + msg.Action = baseMessage.Action + msg.Type = baseMessage.MsgType + msg.originData = buf + return msg, err +} + +// GetMediaData 拉取媒体消息函数 +/** + * 拉取媒体消息函数 + * Return值=0表示该API调用成功 + * + * + * @param [in] sdk NewSdk返回的sdk指针 + * @param [in] sdkFileid 从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid + * @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 + * @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 + * @param [in] indexbuf 媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。 + * @param [in] timeout 超时时间,单位秒 + * @param [out] media_data 返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记) + + * + * @return 返回是否调用成功 + * 0 - 成功 + * !=0 - 失败 + */ +func (s *Client) GetMediaData(indexBuf string, sdkFileID string, proxy string, passwd string, timeout int) (*MediaData, error) { + indexBufC := C.CString(indexBuf) + sdkFileIDC := C.CString(sdkFileID) + proxyC := C.CString(proxy) + passwdC := C.CString(passwd) + mediaDataC := C.NewMediaData() + defer func() { + C.free(unsafe.Pointer(indexBufC)) + C.free(unsafe.Pointer(sdkFileIDC)) + C.free(unsafe.Pointer(proxyC)) + C.free(unsafe.Pointer(passwdC)) + C.FreeMediaData(mediaDataC) + }() + + if s.ptr == nil { + return nil, NewSDKErr(10002) + } + + retC := C.GetMediaData(s.ptr, indexBufC, sdkFileIDC, proxyC, passwdC, C.int(timeout), mediaDataC) + ret := int(retC) + if ret != 0 { + return nil, NewSDKErr(ret) + } + return &MediaData{ + OutIndexBuf: C.GoString(C.GetOutIndexBuf(mediaDataC)), + Data: C.GoBytes(unsafe.Pointer(C.GetData(mediaDataC)), C.GetDataLen(mediaDataC)), + IsFinish: int(C.IsMediaDataFinish(mediaDataC)) == 1, + }, nil +} + +// GetContentFromSlice 从切片内获取内容 +func (s *Client) GetContentFromSlice(slice *C.struct_Slice_t) []byte { + return C.GoBytes(unsafe.Pointer(C.GetContentFromSlice(slice)), C.GetSliceLen(slice)) +} diff --git a/work/msgaudit/client_unsupport.go b/work/msgaudit/client_unsupport.go new file mode 100644 index 0000000..60f1151 --- /dev/null +++ b/work/msgaudit/client_unsupport.go @@ -0,0 +1,20 @@ +//go:build !linux || !cgo || !msgaudit +// +build !linux !cgo !msgaudit + +// Package msgaudit for unsupport platform +package msgaudit + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/work/config" +) + +// Client 会话存档 +type Client struct { +} + +// NewClient new +func NewClient(cfg *config.Config) (*Client, error) { + return nil, fmt.Errorf("会话存档功能目前只支持Linux平台运行,并且打开设置CGO_ENABLED=1") +} diff --git a/work/msgaudit/config.go b/work/msgaudit/config.go new file mode 100644 index 0000000..6130b28 --- /dev/null +++ b/work/msgaudit/config.go @@ -0,0 +1,8 @@ +package msgaudit + +// Config 会话存档初始化参数 +type Config struct { + CorpID string // 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 + CorpSecret string // 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 + RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存 +} diff --git a/work/msgaudit/error.go b/work/msgaudit/error.go new file mode 100644 index 0000000..c3e191d --- /dev/null +++ b/work/msgaudit/error.go @@ -0,0 +1,76 @@ +package msgaudit + +import ( + "fmt" +) + +// 返回码 错误说明 +// 10000 参数错误,请求参数错误 +// 10001 网络错误,网络请求错误 +// 10002 数据解析失败 +// 10003 系统失败 +// 10004 密钥错误导致加密失败 +// 10005 fileid错误 +// 10006 解密失败 +// 10007 找不到消息加密版本的私钥,需要重新传入私钥对 +// 10008 解析encrypt_key出错 +// 10009 ip非法 +// 10010 数据过期 +// 10011 证书错误 +const ( + SDKErrMsg = "sdk failed" + SDKParamsErrMsg = "参数错误,请求参数错误" + SDKNetworkErrMsg = "网络错误,网络请求错误" + SDKParseErrMsg = "数据解析失败" + SDKSystemErrMsg = "系统失败" + SDKSecretErrMsg = "密钥错误导致加密失败" + SDKFileIDErrMsg = "fileid错误" + SDKDecryptErrMsg = "解密失败" + SDKSecretMissErrMsg = "找不到消息加密版本的私钥,需要重新传入私钥对" + SDKEncryptKeyErrMsg = "解析encrypt_key出错" + SDKIPNotWhiteListErrMsg = "ip非法" + SDKDataExpiredErrMsg = "数据过期" + SDKTokenExpiredErrMsg = "证书过期" +) + +// Error 错误 +type Error struct { + ErrCode int `json:"errcode,omitempty"` + ErrMsg string `json:"errmsg,omitempty"` +} + +func (e Error) Error() string { + return fmt.Sprintf("%d:%s", e.ErrCode, e.ErrMsg) +} + +// NewSDKErr 初始化新的SDK错误 +func NewSDKErr(code int) Error { + msg := "" + switch code { + case 10000: + msg = SDKParamsErrMsg + case 10001: + msg = SDKNetworkErrMsg + case 10002: + msg = SDKParseErrMsg + case 10003: + msg = SDKSystemErrMsg + case 10004: + msg = SDKSecretErrMsg + case 10005: + msg = SDKFileIDErrMsg + case 10006: + msg = SDKDecryptErrMsg + case 10007: + msg = SDKSecretMissErrMsg + case 10008: + msg = SDKEncryptKeyErrMsg + case 10009: + msg = SDKIPNotWhiteListErrMsg + case 10010: + msg = SDKDataExpiredErrMsg + case 10011: + msg = SDKTokenExpiredErrMsg + } + return Error{ErrCode: code, ErrMsg: msg} +} diff --git a/work/msgaudit/lib/WeWorkFinanceSdk_C.h b/work/msgaudit/lib/WeWorkFinanceSdk_C.h new file mode 100644 index 0000000..27c4bc7 --- /dev/null +++ b/work/msgaudit/lib/WeWorkFinanceSdk_C.h @@ -0,0 +1,152 @@ +// All Rights Reserved. +// *File WeWorkFinanceSdk_C.h +// @Briefȡҵ¼ýϢsdkͷļ + +#pragma once +// ˵ +//10000 +//10001 +//10002 ݽʧ +//10003 ϵͳʧ +//10004 Կ¼ʧ +//10005 fileid +//10006 ʧ +//10007 ҲϢܰ汾˽ԿҪ´˽Կ +//10008 encrypt_key +//10009 ipǷ +//10010 ݹ +//10011 ֤ + +typedef struct WeWorkFinanceSdk_t WeWorkFinanceSdk_t; + +// +typedef struct Slice_t { + char* buf; + int len; +} Slice_t; + +typedef struct MediaData { + char* outindexbuf; + int out_len; + char* data; + int data_len; + int is_finish; +} MediaData_t; + + +#ifdef __cplusplus +extern "C" { +#endif + + WeWorkFinanceSdk_t* NewSdk(); + + + /** + * ʼ + * Returnֵ=0ʾAPIóɹ + * + * @param [in] sdk NewSdkصsdkָ + * @param [in] corpid ҵҵid磺wwd08c8exxxx5ab44dҵ΢Ź--ҵҵ--ҵϢ鿴 + * @param [in] secret ݴ浵Secretҵ΢Ź----ݴ浵鿴 + * + * + * @return Ƿʼɹ + * 0 - ɹ + * !=0 - ʧ + */ + int Init(WeWorkFinanceSdk_t* sdk, const char* corpid, const char* secret); + + /** + * ȡ¼ + * Returnֵ=0ʾAPIóɹ + * + * + * @param [in] sdk NewSdkصsdkָ + * @param [in] seq ָseqʼȡϢעǷصϢseq+1ʼأseqΪ֮ǰӿڷصseqֵ״ʹʹseq:0 + * @param [in] limit һȡϢֵ10001000᷵ش + * @param [in] proxy ʹôҪӡ磺socks5://10.0.0.1:8081 http://10.0.0.1:8081 + * @param [in] passwd ˺룬Ҫ˺롣 user_name:passwd_123 + * @param [in] timeout ʱʱ䣬λ + * @param [out] chatDatas رȡϢݣsliceṹ.ݰerrcode/errmsgԼÿϢݡʾ£ + + {"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]} + + * + * @return Ƿóɹ + * 0 - ɹ + * !=0 - ʧ + */ + int GetChatData(WeWorkFinanceSdk_t* sdk, unsigned long long seq, unsigned int limit, const char *proxy,const char* passwd, int timeout,Slice_t* chatDatas); + + /** + * @brief .ҵ΢н + * @param [in] encrypt_key, getchatdataصencrypt_random_key,ʹҵԳֶӦ汾ԿRSAܺ + * @param [in] encrypt_msg, getchatdataصencrypt_chat_msg + * @param [out] msg, ܵϢ + * @return Ƿóɹ + * 0 - ɹ + * !=0 - ʧ + */ + int DecryptData(const char* encrypt_key, const char* encrypt_msg, Slice_t* msg); + + /** + * ȡýϢ + * Returnֵ=0ʾAPIóɹ + * + * + * @param [in] sdk NewSdkصsdkָ + * @param [in] sdkFileid GetChatDataصϢУýϢsdkfileid + * @param [in] proxy ʹôҪӡ磺socks5://10.0.0.1:8081 http://10.0.0.1:8081 + * @param [in] passwd ˺룬Ҫ˺롣 user_name:passwd_123 + * @param [in] indexbuf ýϢƬȡҪÿȡϢ״βҪдĬȡ512kÿεֻҪϴε÷صoutindexbuf뼴ɡ + * @param [in] timeout ʱʱ䣬λ + * @param [out] media_data رȡý.MediaDataṹ.ݰdata()/outindexbuf(´)/is_finish(ȡɱ) + + * + * @return Ƿóɹ + * 0 - ɹ + * !=0 - ʧ + */ + int GetMediaData(WeWorkFinanceSdk_t* sdk, const char* indexbuf, + const char* sdkFileid,const char *proxy,const char* passwd, int timeout, MediaData_t* media_data); + + /** + * @brief ͷsdkNewSdkɶʹ + * @return + */ + void DestroySdk(WeWorkFinanceSdk_t* sdk); + + + //--------------ӿΪpythonȵcӿڣʹ-------------- + Slice_t* NewSlice(); + + /** + * @brief ͷsliceNewSliceɶʹ + * @return + */ + void FreeSlice(Slice_t* slice); + + /** + * @brief Ϊṩȡӿ + * @return bufָ + * !=NULL - ɹ + * NULL - ʧ + */ + char* GetContentFromSlice(Slice_t* slice); + int GetSliceLen(Slice_t* slice); + + // ý¼ع + + MediaData_t* NewMediaData(); + + void FreeMediaData(MediaData_t* media_data); + + char* GetOutIndexBuf(MediaData_t* media_data); + char* GetData(MediaData_t* media_data); + int GetIndexLen(MediaData_t* media_data); + int GetDataLen(MediaData_t* media_data); + int IsMediaDataFinish(MediaData_t* media_data); + +#ifdef __cplusplus +} +#endif diff --git a/work/msgaudit/lib/libWeWorkFinanceSdk_C.so b/work/msgaudit/lib/libWeWorkFinanceSdk_C.so new file mode 100644 index 0000000..6bc84a4 Binary files /dev/null and b/work/msgaudit/lib/libWeWorkFinanceSdk_C.so differ diff --git a/work/msgaudit/lib/md5.txt b/work/msgaudit/lib/md5.txt new file mode 100644 index 0000000..313cc61 --- /dev/null +++ b/work/msgaudit/lib/md5.txt @@ -0,0 +1 @@ +781ec3cbad904b1527023cc9df0f279b diff --git a/work/msgaudit/lib/tool_testSdk.cpp b/work/msgaudit/lib/tool_testSdk.cpp new file mode 100644 index 0000000..e5e6ddf --- /dev/null +++ b/work/msgaudit/lib/tool_testSdk.cpp @@ -0,0 +1,148 @@ +#include "WeWorkFinanceSdk_C.h" +#include +#include +#include +#include +#include +using std::string; + +typedef WeWorkFinanceSdk_t* newsdk_t(); +typedef int Init_t(WeWorkFinanceSdk_t*, const char*, const char*); +typedef void DestroySdk_t(WeWorkFinanceSdk_t*); + +typedef int GetChatData_t(WeWorkFinanceSdk_t*, unsigned long long, unsigned int, const char*, const char*, int, Slice_t*); +typedef Slice_t* NewSlice_t(); +typedef void FreeSlice_t(Slice_t*); + +typedef int GetMediaData_t(WeWorkFinanceSdk_t*, const char*, const char*, const char*, const char*, int, MediaData_t*); +typedef int DecryptData_t(const char*, const char*, Slice_t*); +typedef MediaData_t* NewMediaData_t(); +typedef void FreeMediaData_t(MediaData_t*); + +int main(int argc, char* argv[]) +{ + int ret = 0; + //seq 表示该企业存档消息序号,该序号单调递增,拉取序号建议设置为上次拉取返回结果中最大序号。首次拉取时seq传0,sdk会返回有效期内最早的消息。 + //limit 表示本次拉取的最大消息条数,取值范围为1~1000 + //proxy与passwd为代理参数,如果运行sdk的环境不能直接访问外网,需要配置代理参数。sdk访问的域名是"https://qyapi.weixin.qq.com"。 + //建议先通过curl访问"https://qyapi.weixin.qq.com",验证代理配置正确后,再传入sdk。 + //timeout 为拉取会话存档的超时时间,单位为秒,建议超时时间设置为5s。 + //sdkfileid 媒体文件id,从解密后的会话存档中得到 + //savefile 媒体文件保存路径 + //encrypt_key 拉取会话存档返回的encrypt_random_key,使用配置在企业微信管理台的rsa公钥对应的私钥解密后得到encrypt_key。 + //encrypt_chat_msg 拉取会话存档返回的encrypt_chat_msg + if (argc < 2) { + printf("./sdktools 1(chatmsg) 2(mediadata) 3(decryptdata)\n"); + printf("./sdktools 1 seq limit proxy passwd timeout\n"); + printf("./sdktools 2 fileid proxy passwd timeout savefile\n"); + printf("./sdktools 3 encrypt_key encrypt_chat_msg\n"); + return -1; + } + + void* so_handle = dlopen("./libWeWorkFinanceSdk_C.so", RTLD_LAZY); + if (!so_handle) { + printf("load sdk so fail:%s\n", dlerror()); + return -1; + } + newsdk_t* newsdk_fn = (newsdk_t*)dlsym(so_handle, "NewSdk"); + WeWorkFinanceSdk_t* sdk = newsdk_fn(); + + //使用sdk前需要初始化,初始化成功后的sdk可以一直使用。 + //如需并发调用sdk,建议每个线程持有一个sdk实例。 + //初始化时请填入自己企业的corpid与secrectkey。 + Init_t* init_fn = (Init_t*)dlsym(so_handle, "Init"); + DestroySdk_t* destroysdk_fn = (DestroySdk_t*)dlsym(so_handle, "DestroySdk"); + ret = init_fn(sdk, "wwd08c8e7c775ab44d", "zJ6k0naVVQ--gt9PUSSEvs03zW_nlDVmjLCTOTAfrew"); + if (ret != 0) { + //sdk需要主动释放 + destroysdk_fn(sdk); + printf("init sdk err ret:%d\n", ret); + return -1; + } + + int type = strtoul(argv[1], NULL, 10); + if (type == 1) { + //拉取会话存档 + uint64_t iSeq = strtoul(argv[2], NULL, 10); + uint64_t iLimit = strtoul(argv[3], NULL, 10); + uint64_t timeout = strtoul(argv[6], NULL, 10); + + NewSlice_t* newslice_fn = (NewSlice_t*)dlsym(so_handle, "NewSlice"); + FreeSlice_t* freeslice_fn = (FreeSlice_t*)dlsym(so_handle, "FreeSlice"); + + //每次使用GetChatData拉取存档前需要调用NewSlice获取一个chatDatas,在使用完chatDatas中数据后,还需要调用FreeSlice释放。 + Slice_t* chatDatas = newslice_fn(); + GetChatData_t* getchatdata_fn = (GetChatData_t*)dlsym(so_handle, "GetChatData"); + ret = getchatdata_fn(sdk, iSeq, iLimit, argv[4], argv[5], timeout, chatDatas); + if (ret != 0) { + freeslice_fn(chatDatas); + printf("GetChatData err ret:%d\n", ret); + return -1; + } + printf("GetChatData len:%d data:%s\n", chatDatas->len, chatDatas->buf); + freeslice_fn(chatDatas); + } + else if (type == 2) { + //拉取媒体文件 + std::string index; + uint64_t timeout = strtoul(argv[5], NULL, 10); + int isfinish = 0; + + GetMediaData_t* getmediadata_fn = (GetMediaData_t*)dlsym(so_handle, "GetMediaData"); + NewMediaData_t* newmediadata_fn = (NewMediaData_t*)dlsym(so_handle, "NewMediaData"); + FreeMediaData_t* freemediadata_fn = (FreeMediaData_t*)dlsym(so_handle, "FreeMediaData"); + + //媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。若该文件未拉取完整,mediaData中的is_finish会返回0,同时mediaData中的outindexbuf会返回下次拉取需要传入GetMediaData的indexbuf。 + //indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“,表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。 + while (isfinish == 0) { + //每次使用GetMediaData拉取存档前需要调用NewMediaData获取一个mediaData,在使用完mediaData中数据后,还需要调用FreeMediaData释放。 + printf("index:%s\n", index.c_str()); + MediaData_t* mediaData = newmediadata_fn(); + ret = getmediadata_fn(sdk, index.c_str(), argv[2], argv[3], argv[4], timeout, mediaData); + if (ret != 0) { + //单个分片拉取失败建议重试拉取该分片,避免从头开始拉取。 + freemediadata_fn(mediaData); + printf("GetMediaData err ret:%d\n", ret); + return -1; + } + printf("content size:%d isfin:%d outindex:%s\n", mediaData->data_len, mediaData->is_finish, mediaData->outindexbuf); + + //大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + char file[200]; + snprintf(file, sizeof(file), "%s", argv[6]); + FILE* fp = fopen(file, "ab+"); + printf("filename:%s \n", file); + if (NULL == fp) { + freemediadata_fn(mediaData); + printf("open file err\n"); + return -1; + } + + fwrite(mediaData->data, mediaData->data_len, 1, fp); + fclose(fp); + + //获取下次拉取需要使用的indexbuf + index.assign(string(mediaData->outindexbuf)); + isfinish = mediaData->is_finish; + freemediadata_fn(mediaData); + } + } + else if (type == 3) { + //解密会话存档内容 + //sdk不会要求用户传入rsa私钥,保证用户会话存档数据只有自己能够解密。 + //此处需要用户先用rsa私钥解密encrypt_random_key后,作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。 + //每次使用DecryptData解密会话存档前需要调用NewSlice获取一个Msgs,在使用完Msgs中数据后,还需要调用FreeSlice释放。 + NewSlice_t* newslice_fn = (NewSlice_t*)dlsym(so_handle, "NewSlice"); + FreeSlice_t* freeslice_fn = (FreeSlice_t*)dlsym(so_handle, "FreeSlice"); + + Slice_t* Msgs = newslice_fn(); + // decryptdata api + DecryptData_t* decryptdata_fn = (DecryptData_t*)dlsym(so_handle, "DecryptData"); + ret = decryptdata_fn(argv[2], argv[3], Msgs); + printf("chatdata :%s ret :%d\n", Msgs->buf, ret); + + freeslice_fn(Msgs); + } + + return ret; +} diff --git a/work/msgaudit/lib/version.txt b/work/msgaudit/lib/version.txt new file mode 100644 index 0000000..1438950 --- /dev/null +++ b/work/msgaudit/lib/version.txt @@ -0,0 +1 @@ +200215 diff --git a/work/msgaudit/media.go b/work/msgaudit/media.go new file mode 100644 index 0000000..4375f8b --- /dev/null +++ b/work/msgaudit/media.go @@ -0,0 +1,8 @@ +package msgaudit + +// MediaData 媒体文件数据 +type MediaData struct { + OutIndexBuf string `json:"outindexbuf,omitempty"` + IsFinish bool `json:"is_finish,omitempty"` + Data []byte `json:"data,omitempty"` +} diff --git a/work/msgaudit/message.go b/work/msgaudit/message.go new file mode 100644 index 0000000..0d8e990 --- /dev/null +++ b/work/msgaudit/message.go @@ -0,0 +1,352 @@ +package msgaudit + +// BaseMessage 基础消息 +type BaseMessage struct { + MsgID string `json:"msgid,omitempty"` // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。 + Action string `json:"action,omitempty"` // 消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。 + From string `json:"from,omitempty"` // 消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid。 + ToList []string `json:"tolist,omitempty"` // 消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。 + RoomID string `json:"roomid,omitempty"` // 群聊消息的群id。如果是单聊则为空。 + MsgTime int64 `json:"msgtime,omitempty"` // 消息发送时间戳,utc时间,ms单位。 + MsgType string `json:"msgtype,omitempty"` // 文本消息为:text。 +} + +// TextMessage 文本消息 +type TextMessage struct { + BaseMessage + Text struct { + Content string `json:"content,omitempty"` // 消息内容。 + } `json:"text,omitempty"` +} + +// ImageMessage 图片消息 +type ImageMessage struct { + BaseMessage + Image struct { + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + FileSize uint32 `json:"filesize,omitempty"` // 图片资源的文件大小。 + } `json:"image,omitempty"` +} + +// RevokeMessage 撤回消息 +type RevokeMessage struct { + BaseMessage + Revoke struct { + PreMsgID string `json:"pre_msgid,omitempty"` // 标识撤回的原消息的msgid + } `json:"revoke,omitempty"` +} + +// AgreeMessage 同意会话聊天内容 +type AgreeMessage struct { + BaseMessage + Agree struct { + UserID string `json:"userid,omitempty"` // 同意/不同意协议者的userid,外部企业默认为external_userid。 + AgreeTime int64 `json:"agree_time,omitempty"` // 同意/不同意协议的时间,utc时间,ms单位。 + } `json:"agree,omitempty"` +} + +// VoiceMessage 语音消息 +type VoiceMessage struct { + BaseMessage + Voice struct { + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + VoiceSize uint32 `json:"voice_size,omitempty"` // 语音消息大小。 + PlayLength uint32 `json:"play_length,omitempty"` // 播放长度。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + } `json:"voice,omitempty"` +} + +// VideoMessage 视频消息 +type VideoMessage struct { + BaseMessage + Video struct { + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + FileSize uint32 `json:"filesize,omitempty"` // 图片资源的文件大小。 + PlayLength uint32 `json:"play_length,omitempty"` // 播放长度。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + } `json:"video,omitempty"` +} + +// CardMessage 名片消息 +type CardMessage struct { + BaseMessage + Card struct { + CorpName string `json:"corpname,omitempty"` // 名片所有者所在的公司名称。 + UserID string `json:"userid,omitempty"` // 名片所有者的id,同一公司是userid,不同公司是external_userid + } `json:"card,omitempty"` +} + +// LocationMessage 位置消息 +type LocationMessage struct { + BaseMessage + Location struct { + Lng float64 `json:"longitude,omitempty"` // 经度,单位double + Lat float64 `json:"latitude,omitempty"` // 纬度,单位double + Address string `json:"address,omitempty"` // 地址信息 + Title string `json:"title,omitempty"` // 位置信息的title。 + Zoom uint32 `json:"zoom,omitempty"` // 缩放比例。 + } `json:"location,omitempty"` +} + +// EmotionMessage 表情消息 +type EmotionMessage struct { + BaseMessage + Emotion struct { + Type uint32 `json:"type,omitempty"` // 表情类型,png或者gif.1表示gif 2表示png。 + Width uint32 `json:"width,omitempty"` // 表情图片宽度。 + Height uint32 `json:"height,omitempty"` // 表情图片高度。 + ImageSize uint32 `json:"imagesize,omitempty"` // 资源的文件大小。 + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + } `json:"emotion,omitempty"` +} + +// FileMessage 文件消息 +type FileMessage struct { + BaseMessage + File struct { + FileName string `json:"filename,omitempty"` // 文件名称。 + FileExt string `json:"fileext,omitempty"` // 文件类型后缀。 + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + FileSize uint32 `json:"filesize,omitempty"` // 文件大小。 + Md5Sum string `json:"md5sum,omitempty"` // 资源的md5值,供进行校验。 + } `json:"file,omitempty"` +} + +// LinkMessage 链接消息 +type LinkMessage struct { + BaseMessage + Link struct { + Title string `json:"title,omitempty"` // 消息标题。 + Desc string `json:"description,omitempty"` // 消息描述。 + LinkURL string `json:"link_url,omitempty"` // 链接url地址 + ImageURL string `json:"image_url,omitempty"` // 链接图片url。 + } `json:"link,omitempty"` +} + +// WeappMessage 小程序消息 +type WeappMessage struct { + BaseMessage + WeApp struct { + Title string `json:"title,omitempty"` // 消息标题。 + Desc string `json:"description,omitempty"` // 消息描述。 + Username string `json:"username,omitempty"` // 用户名称。 + DisplayName string `json:"displayname,omitempty"` // 小程序名称 + } `json:"weapp,omitempty"` +} + +// ChatRecordMessage 会话记录消息 +type ChatRecordMessage struct { + BaseMessage + ChatRecord struct { + Title string `json:"title,omitempty"` // 聊天记录标题 + Item []ChatRecord `json:"item,omitempty"` // 消息记录内的消息内容,批量数据 + } `json:"chatrecord,omitempty"` +} + +// TodoMessage 待办消息 +type TodoMessage struct { + BaseMessage + Todo struct { + Title string `json:"title,omitempty"` // 代办的来源文本 + Content string `json:"content,omitempty"` // 代办的具体内容 + } `json:"todo,omitempty"` +} + +// VoteMessage 投票消息 +type VoteMessage struct { + BaseMessage + VoteTitle string `json:"votetitle,omitempty"` // 投票主题。 + VoteItem []string `json:"voteitem,omitempty"` // 投票选项,可能多个内容。 + VoteType uint32 `json:"votetype,omitempty"` // 投票类型.101发起投票、102参与投票。 + VoteID string `json:"voteid,omitempty"` // 投票id,方便将参与投票消息与发起投票消息进行前后对照。 +} + +// CollectMessage 填表消息 +type CollectMessage struct { + BaseMessage + Collect struct { + RoomName string `json:"room_name,omitempty"` // 填表消息所在的群名称。 + Creator string `json:"creator,omitempty"` // 创建者在群中的名字 + CreateTime int64 `json:"create_time,omitempty"` // 创建的时间 + Details []CollectDetails `json:"details,omitempty"` // 表内容 + } `json:"collect,omitempty"` +} + +// RedpacketMessage 红包消息 +type RedpacketMessage struct { + BaseMessage + RedPacket struct { + Type uint32 `json:"type,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包、3 激励群红包。 + Wish string `json:"wish,omitempty"` // 红包祝福语 + TotalCnt uint32 `json:"totalcnt,omitempty"` // 红包总个数 + TotalAmount uint32 `json:"totalamount,omitempty"` // 红包总金额。单位为分。 + } `json:"redpacket,omitempty"` +} + +// MeetingMessage 会议邀请消息 +type MeetingMessage struct { + BaseMessage + Meeting struct { + Topic string `json:"topic,omitempty"` // 会议主题 + StartTime int64 `json:"starttime,omitempty"` // 会议开始时间。Utc时间 + EndTime int64 `json:"endtime,omitempty"` // 会议结束时间。Utc时间 + Address string `json:"address,omitempty"` // 会议地址 + Remarks string `json:"remarks,omitempty"` // 会议备注 + MeetingType uint32 `json:"meetingtype,omitempty"` // 会议消息类型。101发起会议邀请消息、102处理会议邀请消息 + MeetingID uint64 `json:"meetingid,omitempty"` // 会议id。方便将发起、处理消息进行对照 + Status uint32 `json:"status,omitempty"` // 会议邀请处理状态。1 参加会议、2 拒绝会议、3 待定、4 未被邀请、5 会议已取消、6 会议已过期、7 不在房间内。 + } `json:"meeting,omitempty"` +} + +// DocMessage 在线文档消息 +type DocMessage struct { + BaseMessage + Doc struct { + Title string `json:"title,omitempty"` // 在线文档名称 + LinkURL string `json:"link_url,omitempty"` // 在线文档链接 + DocCreator string `json:"doc_creator,omitempty"` // 在线文档创建者。本企业成员创建为userid;外部企业成员创建为external_userid + } `json:"doc,omitempty"` +} + +// MarkdownMessage MarkDown消息 +type MarkdownMessage struct { + BaseMessage + Info struct { + Content string `json:"content,omitempty"` // markdown消息内容,目前为机器人发出的消息 + } `json:"info,omitempty"` +} + +// NewsMessage 图文消息 +type NewsMessage struct { + BaseMessage + Info struct { + Item []News `json:"item,omitempty"` // 图文消息数组 + } `json:"info,omitempty"` // 图文消息的内容 +} + +// CalendarMessage 日程消息 +type CalendarMessage struct { + BaseMessage + Calendar struct { + Title string `json:"title,omitempty"` // 日程主题 + CreatorName string `json:"creatorname,omitempty"` // 日程组织者 + AttendeeName []string `json:"attendeename,omitempty"` // 日程参与人。数组,内容为String类型 + StartTime int64 `json:"starttime,omitempty"` // 日程开始时间。Utc时间,单位秒 + EndTime int64 `json:"endtime,omitempty"` // 日程结束时间。Utc时间,单位秒 + Place string `json:"place,omitempty"` // 日程地点 + Remarks string `json:"remarks,omitempty"` // 日程备注 + } `json:"calendar,omitempty"` +} + +// MixedMessage 混合消息 +type MixedMessage struct { + BaseMessage + Mixed struct { + Item []MixedMsg `json:"item,omitempty"` + } `json:"mixed,omitempty"` // 消息内容。可包含图片、文字、表情等多种消息。Object类型 +} + +// MeetingVoiceCallMessage 音频存档消息 +type MeetingVoiceCallMessage struct { + BaseMessage + VoiceID string `json:"voiceid,omitempty"` // 音频id + MeetingVoiceCall *MeetingVoiceCall `json:"meeting_voice_call,omitempty"` // 音频消息内容。包括结束时间、fileid,可能包括多个demofiledata、sharescreendata消息,demofiledata表示文档共享信息,sharescreendata表示屏幕共享信息。Object类型 +} + +// VoipDocShareMessage 音频共享消息 +type VoipDocShareMessage struct { + BaseMessage + VoipID string `json:"voipid,omitempty"` // 音频id + VoipDocShare *VoipDocShare `json:"voip_doc_share,omitempty"` // 共享文档消息内容。包括filename、md5sum、filesize、sdkfileid字段。Object类型 +} + +// ExternalRedPacketMessage 互通小红包消息 +type ExternalRedPacketMessage struct { + BaseMessage + RedPacket struct { + Type uint32 `json:"type,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包。Uint32类型 + Wish string `json:"wish,omitempty"` // 红包祝福语。String类型 + TotalCnt uint32 `json:"totalcnt,omitempty"` // 红包总个数。Uint32类型 + TotalAmount uint32 `json:"totalamount,omitempty"` // 红包总金额。Uint32类型,单位为分。 + } `json:"redpacket,omitempty"` +} + +// SphFeedMessage 视频号消息 +type SphFeedMessage struct { + BaseMessage + SphFeed struct { + FeedType uint32 `json:"feed_type,omitempty"` // 视频号消息类型。2 图片、4 视频、9 直播。Uint32类型 + SphName string `json:"sph_name,omitempty"` // 视频号账号名称。String类型 + FeedDesc string `json:"feed_desc,omitempty"` // 视频号消息描述。String类型 + } +} + +// SwitchMessage 企业切换日志 +type SwitchMessage struct { + MsgID string `json:"msgid,omitempty"` // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重 + Action string `json:"action,omitempty"` // 消息动作,切换企业为switch + Time int64 `json:"time,omitempty"` // 消息发送时间戳,utc时间,ms单位。 + User string `json:"user,omitempty"` // 具体为切换企业的成员的userid。 +} + +// ChatRecord 会话记录消息 +type ChatRecord struct { + Type string `json:"type,omitempty"` // 每条聊天记录的具体消息类型:ChatRecordText/ ChatRecordFile/ ChatRecordImage/ ChatRecordVideo/ ChatRecordLink/ ChatRecordLocation/ ChatRecordMixed …. + Content string `json:"content,omitempty"` // 消息内容。Json串,内容为对应类型的json + MsgTime int64 `json:"msgtime,omitempty"` // 消息时间,utc时间,ms单位。 + FromChatroom bool `json:"from_chatroom,omitempty"` // 是否来自群会话。 +} + +// CollectDetails 填表消息 +type CollectDetails struct { + ID uint64 `json:"id,omitempty"` // 表项id + Ques string `json:"ques,omitempty"` // 表项名称 + Type string `json:"type,omitempty"` // 表项类型,有Text(文本),Number(数字),Date(日期),Time(时间) +} + +// News 图文消息 +type News struct { + Title string `json:"title,omitempty"` // 图文消息标题 + Desc string `json:"description,omitempty"` // 图文消息描述 + URL string `json:"url,omitempty"` // 图文消息点击跳转地址 + PicURL string `json:"picurl,omitempty"` // 图文消息配图的url +} + +// MixedMsg 混合消息 +type MixedMsg struct { + Type string `json:"type,omitempty"` + Content string `json:"content,omitempty"` +} + +// MeetingVoiceCall 音频存档消息 +type MeetingVoiceCall struct { + EndTime int64 `json:"endtime,omitempty"` // 音频结束时间 + SdkFileID string `json:"sdkfileid,omitempty"` // 音频媒体下载的id + DemoFileData []DemoFileData `json:"demofiledata,omitempty"` // 文档分享对象,Object类型 + ShareScreenData []ShareScreenData `json:"sharescreendata,omitempty"` // 屏幕共享对象,Object类型 +} + +// DemoFileData 文档共享消息 +type DemoFileData struct { + FileName string `json:"filename,omitempty"` // 文档共享名称 + DemoOperator string `json:"demooperator,omitempty"` // 文档共享操作用户的id + StartTime int64 `json:"starttime,omitempty"` // 文档共享开始时间 + EndTime int64 `json:"endtime,omitempty"` // 文档共享结束时间 +} + +// ShareScreenData 屏幕共享信息 +type ShareScreenData struct { + Share string `json:"share,omitempty"` // 屏幕共享用户的id + StartTime int64 `json:"starttime,omitempty"` // 屏幕共享开始时间 + EndTime int64 `json:"endtime,omitempty"` // 屏幕共享结束时间 +} + +// VoipDocShare 音频共享文档消息 +type VoipDocShare struct { + FileName string `json:"filename,omitempty"` // 文档共享文件名称 + Md5Sum string `json:"md5sum,omitempty"` // 共享文件的md5值 + FileSize uint64 `json:"filesize,omitempty"` // 共享文件的大小 + SdkFileID string `json:"sdkfileid,omitempty"` // 共享文件的sdkfile,通过此字段进行媒体数据下载 +} diff --git a/work/oauth/oauth.go b/work/oauth/oauth.go new file mode 100644 index 0000000..add4b02 --- /dev/null +++ b/work/oauth/oauth.go @@ -0,0 +1,165 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/context" +) + +// Oauth auth +type Oauth struct { + *context.Context +} + +var ( + // oauthTargetURL 企业微信内跳转地址 + oauthTargetURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect" + // oauthTargetURL 企业微信内跳转地址(获取成员的详细信息) + oauthTargetPrivateURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&agentid=%s&state=STATE#wechat_redirect" + // oauthUserInfoURL 获取用户信息地址 + oauthUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s" + // oauthQrContentTargetURL 构造独立窗口登录二维码 + oauthQrContentTargetURL = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s" + // getUserInfoURL 获取访问用户身份&获取用户登录身份 + getUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s" + // getUserDetailURL 获取访问用户敏感信息 + getUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s" +) + +// NewOauth new init oauth +func NewOauth(ctx *context.Context) *Oauth { + return &Oauth{ + ctx, + } +} + +// GetTargetURL 获取授权地址 +func (ctr *Oauth) GetTargetURL(callbackURL string) string { + // url encode + return fmt.Sprintf( + oauthTargetURL, + ctr.CorpID, + url.QueryEscape(callbackURL), + ) +} + +// GetTargetPrivateURL 获取个人信息授权地址 +func (ctr *Oauth) GetTargetPrivateURL(callbackURL string, agentID string) string { + // url encode + return fmt.Sprintf( + oauthTargetPrivateURL, + ctr.CorpID, + url.QueryEscape(callbackURL), + agentID, + ) +} + +// GetQrContentTargetURL 构造独立窗口登录二维码 +func (ctr *Oauth) GetQrContentTargetURL(callbackURL string) string { + // url encode + return fmt.Sprintf( + oauthQrContentTargetURL, + ctr.CorpID, + ctr.AgentID, + url.QueryEscape(callbackURL), + util.RandomStr(16), + ) +} + +// ResUserInfo 返回得用户信息 +type ResUserInfo struct { + util.CommonError + // 当用户为企业成员时返回 + UserID string `json:"UserId"` + DeviceID string `json:"DeviceId"` + // 非企业成员授权时返回 + OpenID string `json:"OpenId"` + ExternalUserID string `json:"external_userid"` +} + +// UserFromCode 根据code获取用户信息 +func (ctr *Oauth) UserFromCode(code string) (result ResUserInfo, err error) { + var accessToken string + if accessToken, err = ctr.GetAccessToken(); err != nil { + return + } + var response []byte + if response, err = util.HTTPGet(fmt.Sprintf(oauthUserInfoURL, accessToken, code)); err != nil { + return + } + err = json.Unmarshal(response, &result) + if result.ErrCode != 0 { + err = fmt.Errorf("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// GetUserInfoResponse 获取访问用户身份&获取用户登录身份响应 +type GetUserInfoResponse struct { + util.CommonError + UserID string `json:"userid"` + UserTicket string `json:"user_ticket"` + OpenID string `json:"openid"` + ExternalUserID string `json:"external_userid"` +} + +// GetUserInfo 获取访问用户身份&获取用户登录身份 +// @see https://developer.work.weixin.qq.com/document/path/90213 获取访问用户身份 +// @see https://developer.work.weixin.qq.com/document/path/98176 获取用户登录身份 +func (ctr *Oauth) GetUserInfo(code string) (*GetUserInfoResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = ctr.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.HTTPGet(fmt.Sprintf(getUserInfoURL, accessToken, code)); err != nil { + return nil, err + } + result := &GetUserInfoResponse{} + err = util.DecodeWithError(response, result, "GetUserInfo") + return result, err +} + +// GetUserDetailRequest 获取访问用户敏感信息请求 +type GetUserDetailRequest struct { + UserTicket string `json:"user_ticket"` +} + +// GetUserDetailResponse 获取访问用户敏感信息响应 +type GetUserDetailResponse struct { + util.CommonError + UserID string `json:"userid"` + Gender string `json:"gender"` + Avatar string `json:"avatar"` + QrCode string `json:"qr_code"` + Mobile string `json:"mobile"` + Email string `json:"email"` + BizMail string `json:"biz_mail"` + Address string `json:"address"` +} + +// GetUserDetail 获取访问用户敏感信息 +// @see https://developer.work.weixin.qq.com/document/path/95833 +func (ctr *Oauth) GetUserDetail(req *GetUserDetailRequest) (*GetUserDetailResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = ctr.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.PostJSON(fmt.Sprintf(getUserDetailURL, accessToken), req); err != nil { + return nil, err + } + result := &GetUserDetailResponse{} + err = util.DecodeWithError(response, result, "GetUserDetail") + return result, err +} diff --git a/work/robot/client.go b/work/robot/client.go new file mode 100644 index 0000000..faaaa3e --- /dev/null +++ b/work/robot/client.go @@ -0,0 +1,17 @@ +package robot + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 群聊机器人接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/robot/robot.go b/work/robot/robot.go new file mode 100644 index 0000000..eee3837 --- /dev/null +++ b/work/robot/robot.go @@ -0,0 +1,29 @@ +package robot + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // webhookSendURL 机器人发送群组消息 + webhookSendURL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s" +) + +// RobotBroadcast 群机器人消息发送 +// @see https://developer.work.weixin.qq.com/document/path/91770 +func (r *Client) RobotBroadcast(webhookKey string, options interface{}) (info util.CommonError, err error) { + var data []byte + if data, err = util.PostJSON(fmt.Sprintf(webhookSendURL, webhookKey), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, err + } + return info, nil +} diff --git a/work/robot/send_option.go b/work/robot/send_option.go new file mode 100644 index 0000000..39efebe --- /dev/null +++ b/work/robot/send_option.go @@ -0,0 +1,126 @@ +package robot + +import "github.com/silenceper/wechat/v2/util" + +// WebhookSendResponse 机器人发送群组消息响应 +type WebhookSendResponse struct { + util.CommonError +} + +// WebhookSendTextOption 机器人发送文本消息请求参数 +type WebhookSendTextOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为text + Text struct { + Content string `json:"content"` // 文本内容,最长不超过2048个字节,必须是utf8编码 + MentionedList []string `json:"mentioned_list"` // userid的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userid,可以使用mentioned_mobile_list + MentionedMobileList []string `json:"mentioned_mobile_list"` // 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 + } `json:"text"` // 文本消息内容 +} + +// WebhookSendMarkdownOption 机器人发送markdown消息请求参数 +// 支持语法参考 https://developer.work.weixin.qq.com/document/path/91770 +type WebhookSendMarkdownOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为markdown + Markdown struct { + Content string `json:"content"` // markdown内容,最长不超过4096个字节,必须是utf8编码 + } `json:"markdown"` // markdown消息内容 +} + +// WebhookSendImageOption 机器人发送图片消息请求参数 +type WebhookSendImageOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为image + Image struct { + Base64 string `json:"base64"` // 图片内容的base64编码 + MD5 string `json:"md5"` // 图片内容(base64编码前)的md5值 + } `json:"image"` // 图片消息内容 +} + +// WebhookSendNewsOption 机器人发送图文消息请求参数 +type WebhookSendNewsOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为news + News struct { + Articles []struct { + Title string `json:"title"` // 标题,不超过128个字节,超过会自动截断 + Description string `json:"description"` // 描述,不超过512个字节,超过会自动截断 + URL string `json:"url"` // 点击后跳转的链接 + PicURL string `json:"picurl"` // 图文消息的图片链接,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150 + } `json:"articles"` // 图文消息列表 一个图文消息支持1到8条图文 + } `json:"news"` // 图文消息内容 +} + +// WebhookSendFileOption 机器人发送文件消息请求参数 +type WebhookSendFileOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为file + File struct { + MediaID string `json:"media_id"` // 文件id,通过下文的文件上传接口获取 + } `json:"file"` // 文件类型 +} + +// WebHookSendTempNoticeOption 机器人发送文本通知模版消息请求参数 +type WebHookSendTempNoticeOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时的消息类型固定为template_card + TemplateCard TemplateCard `json:"template_card"` // 具体的模版卡片参数 +} + +// TemplateCard 具体的模版卡片参数 +type TemplateCard struct { + CardType string `json:"card_type"` // 模版卡片的模版类型,文本通知模版卡片的类型为text_notice + Source CardSource `json:"source"` // 卡片来源样式信息,不需要来源样式可不填写 + MainTitle CardTitle `json:"main_title"` // 模版卡片的主要内容,包括一级标题和标题辅助信息 + EmphasisContent CardTitle `json:"emphasis_content"` // 关键数据样式 + QuoteArea CardQuoteArea `json:"quote_area"` // 引用文献样式,建议不与关键数据共用 + SubTitleText string `json:"sub_title_text"` // 二级普通文本,建议不超过112个字。模版卡片主要内容的一级标题main_title.title和二级普通文本sub_title_text必须有一项填写 + HorizontalContentList []CardContent `json:"horizontal_content_list"` // 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 + JumpList []JumpContent `json:"jump_list"` // 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3 + CardAction CardAction `json:"card_action"` // 整体卡片的点击跳转事件,text_notice模版卡片中该字段为必填项 +} + +// CardSource 卡片来源样式信息,不需要来源样式可不填写 +type CardSource struct { + IconURL string `json:"icon_url"` // 来源图片的url + Desc string `json:"desc"` // 来源图片的描述,建议不超过13个字 + DescColor int `json:"desc_color"` // 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色 +} + +// CardTitle 标题和标题辅助信息 +type CardTitle struct { + Title string `json:"title"` // 标题,建议不超过26个字。模版卡片主要内容的一级标题main_title.title和二级普通文本sub_title_text必须有一项填写 + Desc string `json:"desc"` // 标题辅助信息,建议不超过30个字 +} + +// CardQuoteArea 引用文献样式,建议不与关键数据共用 +type CardQuoteArea struct { + Type int `json:"type"` // 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序 + URL string `json:"url,omitempty"` // 点击跳转的url,quote_area.type是1时必填 + Appid string `json:"appid,omitempty"` // 点击跳转的小程序的appid,quote_area.type是2时必填 + Pagepath string `json:"pagepath,omitempty"` // 点击跳转的小程序的pagepath,quote_area.type是2时选填 + Title string `json:"title"` // 引用文献样式的标题 + QuoteText string `json:"quote_text"` // 引用文献样式的引用文案 +} + +// CardContent 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 +type CardContent struct { + KeyName string `json:"keyname"` // 链接类型,0或不填代表是普通文本,1 代表跳转url,2 代表下载附件,3 代表@员工 + Value string `json:"value"` // 二级标题,建议不超过5个字 + Type int `json:"type,omitempty"` // 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过26个字 + URL string `json:"url,omitempty"` // 链接跳转的url,horizontal_content_list.type是1时必填 + MediaID string `json:"media_id,omitempty"` // 附件的media_id,horizontal_content_list.type是2时必填 + UserID string `json:"userid,omitempty"` // 被@的成员的userid,horizontal_content_list.type是3时必填 +} + +// JumpContent 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3 +type JumpContent struct { + Type int `json:"type"` // 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序 + URL string `json:"url,omitempty"` // 跳转链接的url,jump_list.type是1时必填 + Title string `json:"title"` // 跳转链接样式的文案内容,建议不超过13个字 + AppID string `json:"appid,omitempty"` // 跳转链接的小程序的appid,jump_list.type是2时必填 + PagePath string `json:"pagepath,omitempty"` // 跳转链接的小程序的pagepath,jump_list.type是2时选填 +} + +// CardAction 整体卡片的点击跳转事件,text_notice模版卡片中该字段为必填项 +type CardAction struct { + Type int `json:"type"` // 卡片跳转类型,1 代表跳转url,2 代表打开小程序。text_notice模版卡片中该字段取值范围为[1,2] + URL string `json:"url,omitempty"` // 跳转事件的url,card_action.type是1时必填 + Appid string `json:"appid,omitempty"` // 跳转事件的小程序的appid,card_action.type是2时必填 + PagePath string `json:"pagepath,omitempty"` // 跳转事件的小程序的pagepath,card_action.type是2时选填 +} diff --git a/work/work.go b/work/work.go new file mode 100644 index 0000000..24c5773 --- /dev/null +++ b/work/work.go @@ -0,0 +1,93 @@ +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/checkin" + "github.com/silenceper/wechat/v2/work/config" + "github.com/silenceper/wechat/v2/work/context" + "github.com/silenceper/wechat/v2/work/externalcontact" + "github.com/silenceper/wechat/v2/work/invoice" + "github.com/silenceper/wechat/v2/work/kf" + "github.com/silenceper/wechat/v2/work/material" + "github.com/silenceper/wechat/v2/work/message" + "github.com/silenceper/wechat/v2/work/msgaudit" + "github.com/silenceper/wechat/v2/work/oauth" + "github.com/silenceper/wechat/v2/work/robot" +) + +// Work 企业微信 +type Work struct { + ctx *context.Context +} + +// NewWork init work +func NewWork(cfg *config.Config) *Work { + defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache) + ctx := &context.Context{ + Config: cfg, + AccessTokenHandle: defaultAkHandle, + } + return &Work{ctx: ctx} +} + +// GetContext get Context +func (wk *Work) GetContext() *context.Context { + return wk.ctx +} + +// GetOauth get oauth +func (wk *Work) GetOauth() *oauth.Oauth { + return oauth.NewOauth(wk.ctx) +} + +// GetMsgAudit get msgAudit +func (wk *Work) GetMsgAudit() (*msgaudit.Client, error) { + return msgaudit.NewClient(wk.ctx.Config) +} + +// GetKF get kf +func (wk *Work) GetKF() (*kf.Client, error) { + return kf.NewClient(wk.ctx.Config) +} + +// GetExternalContact get external_contact +func (wk *Work) GetExternalContact() *externalcontact.Client { + return externalcontact.NewClient(wk.ctx) +} + +// GetAddressList get address_list +func (wk *Work) GetAddressList() *addresslist.Client { + return addresslist.NewClient(wk.ctx) +} + +// GetMaterial get material +func (wk *Work) GetMaterial() *material.Client { + return material.NewClient(wk.ctx) +} + +// GetRobot get robot +func (wk *Work) GetRobot() *robot.Client { + return robot.NewClient(wk.ctx) +} + +// GetMessage 获取发送应用消息接口实例 +func (wk *Work) GetMessage() *message.Client { + return message.NewClient(wk.ctx) +} + +// GetAppChat 获取应用发送消息到群聊会话接口实例 +func (wk *Work) GetAppChat() *appchat.Client { + return appchat.NewClient(wk.ctx) +} + +// GetInvoice get invoice +func (wk *Work) GetInvoice() *invoice.Client { + return invoice.NewClient(wk.ctx) +} + +// GetCheckin 获取打卡接口实例 +func (wk *Work) GetCheckin() *checkin.Client { + return checkin.NewClient(wk.ctx) +}