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
+

[](https://goreportcard.com/report/github.com/silenceper/wechat)
[](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc)
+
使用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
## 公众号
+

## 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)
+
+说明:「发表记录」包括群发和发布。
+
+注意:该接口,只能处理 "发布" 相关的信息,无法操作和获取 "群发" 相关内容!
+
+- 群发:主动推送给粉丝,历史消息可看,被搜一搜收录,可以限定部分的粉丝接收到。
+- 发布:不会主动推给粉丝,历史消息列表看不到,但是是公开给所有人的文章。也不会占用群发的次数。每天可以发布多篇内容。可以用于自动回复、自定义菜单、页面模板和话题中,发布成功时会生成一个永久链接。
+
+| 名称 | 请求方式 | 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)
+}