diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index d769e04..151be0b 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,6 +1,6 @@ --- -name: 报告Bug -about: 反馈BUG信息 +name: 报告 Bug +about: 反馈 BUG 信息 title: "[BUG]" labels: bug assignees: '' @@ -18,4 +18,4 @@ assignees: '' **使用的版本** - - SDK版本: [比如 v0.0.0] + - SDK 版本:[比如 v0.0.0] diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index fb34f0d..1c82669 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -1,6 +1,6 @@ --- -name: API需求 -about: 待实现的API接口,SDK的强大离不开社区的帮助,欢迎为项目贡献PR +name: API 需求 +about: 待实现的 API 接口,SDK 的强大离不开社区的帮助,欢迎为项目贡献 PR title: "[Feature]" labels: enhancement assignees: '' @@ -8,8 +8,8 @@ assignees: '' --- -**你想要实现的模块或API** +**你想要实现的模块或 API** diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index db13f82..1151dce 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,6 +1,6 @@ --- name: 使用咨询 -about: 关于SDK使用相关的咨询,在使用前请先阅读官方微信文档 +about: 关于 SDK 使用相关的咨询,在使用前请先阅读官方微信文档 title: "[咨询]" labels: question assignees: '' @@ -9,7 +9,7 @@ assignees: '' **请描述您的问题** diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 784d86f..de28d32 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,25 +2,29 @@ name: Go on: push: - branches: [ master,release-*,v2 ] + branches: [ master,release-*,v2,feature/**,fix/** ] pull_request: - branches: [ master,release-*,v2 ] + branches: [ master,release-*,v2,feature/**,fix/** ] jobs: golangci: strategy: matrix: - go-version: [1.16.x,1.17.x,1.18.x] + go-version: [ '1.16','1.17','1.18','1.19','1.20','1.21.4' ] name: golangci-lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 - - uses: actions/checkout@v3 + - name: Setup Golang ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout + uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v3.2.0 + uses: golangci/golangci-lint-action@v4 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: latest + version: v1.52.2 build: name: Test runs-on: ubuntu-latest @@ -28,7 +32,7 @@ jobs: redis: image: redis ports: - - 6379:6379 + - 6379:6379 options: --entrypoint redis-server memcached: image: memcached @@ -38,14 +42,14 @@ jobs: # strategy set strategy: matrix: - go: ["1.16", "1.17", "1.18"] + go: [ '1.16','1.17','1.18','1.19','1.20','1.21','1.22' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: 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/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 3c97636..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: goreleaser - -on: - push: - tags: - - '*' - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.15 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index fafdac1..bf68478 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -55,7 +55,7 @@ issues: linters-settings: funlen: lines: 66 - statements: 40 + statements: 50 #issues: # include: diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index a097d09..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This is an example goreleaser.yaml file with some sane defaults. -# Make sure to check the documentation at http://goreleaser.com -before: - hooks: - # You may remove this if you don't use go modules. - - go mod download - # you may remove this if you don't need go generate - - go generate ./... -builds: -- skip: true - -archives: - - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 - -checksum: - name_template: 'checksums.txt' -snapshot: - name_template: "{{ .Tag }}-next" -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' 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/redis.go b/cache/redis.go index 24a736f..f51f7bf 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -47,7 +47,12 @@ func (r *Redis) SetRedisCtx(ctx context.Context) { // Get 获取一个值 func (r *Redis) Get(key string) interface{} { - result, err := r.conn.Do(r.ctx, "GET", key).Result() + return r.GetContext(r.ctx, key) +} + +// 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 } @@ -56,17 +61,32 @@ func (r *Redis) Get(key string) interface{} { // Set 设置一个值 func (r *Redis) Set(key string, val interface{}, timeout time.Duration) error { - return r.conn.SetEX(r.ctx, key, val, timeout).Err() + 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 { - result, _ := r.conn.Exists(r.ctx, key).Result() + return r.IsExistContext(r.ctx, key) +} + +// 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 { - return r.conn.Del(r.ctx, key).Err() + 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 8973fe6..a41a2f1 100644 --- a/cache/redis_test.go +++ b/cache/redis_test.go @@ -4,17 +4,23 @@ import ( "context" "testing" "time" + + "github.com/alicebob/miniredis/v2" ) func TestRedis(t *testing.T) { + server, err := miniredis.Run() + if err != nil { + t.Error("miniredis.Run Error", err) + } + t.Cleanup(server.Close) var ( timeoutDuration = time.Second ctx = context.Background() opts = &RedisOpts{ - Host: "127.0.0.1:6379", + Host: server.Addr(), } redis = NewRedis(ctx, opts) - err error val = "silenceper" key = "username" ) diff --git a/credential/default_access_token.go b/credential/default_access_token.go index d58efe6..00ad481 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -12,9 +12,11 @@ import ( ) const ( - // AccessTokenURL 获取access_token的接口 + // accessTokenURL 获取access_token的接口 accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" - // AccessTokenURL 企业微信获取access_token的接口 + // 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_" @@ -64,8 +66,11 @@ func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) { func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (accessToken string, err error) { // 先从cache中取 accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID) + if val := ak.cache.Get(accessTokenCacheKey); val != nil { - return val.(string), nil + if accessToken = val.(string); accessToken != "" { + return + } } // 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token @@ -74,25 +79,97 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access // 双检,防止重复从微信服务器获取 if val := ak.cache.Get(accessTokenCacheKey); val != nil { - return val.(string), nil + if accessToken = val.(string); accessToken != "" { + return + } } // cache失效,从微信服务器获取 var resAccessToken ResAccessToken - resAccessToken, err = GetTokenFromServerContext(ctx, fmt.Sprintf(accessTokenURL, ak.appID, ak.appSecret)) - if err != nil { + if resAccessToken, err = GetTokenFromServerContext(ctx, fmt.Sprintf(accessTokenURL, ak.appID, ak.appSecret)); err != nil { return } expires := resAccessToken.ExpiresIn - 1500 err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second) + + 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 + err = 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 @@ -142,9 +219,7 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok expires := resAccessToken.ExpiresIn - 1500 err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second) - if err != nil { - return - } + accessToken = resAccessToken.AccessToken return } diff --git a/doc/api/work.md b/doc/api/work.md index 117c20b..445d6c0 100644 --- a/doc/api/work.md +++ b/doc/api/work.md @@ -90,10 +90,12 @@ host: https://qyapi.weixin.qq.com/ | 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | |:---------:|------|:----------------------------------------| ---------- | ------------------------------- |----------| | 获取子部门ID列表 | GET | /cgi-bin/department/simplelist | YES | (r *Client) DepartmentSimpleList| MARKWANG | +| 获取部门列表 | GET | /cgi-bin/department/list | YES | (r *Client) DepartmentList| just5325, ourines | | 获取部门成员 | 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) @@ -116,5 +118,14 @@ host: https://qyapi.weixin.qq.com/ | ---------------- | -------- | --------------------- | ---------- | -------------------------- | -------- | | 群机器人发送消息 | 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/domain/openapi/mgnt.go b/domain/openapi/mgnt.go new file mode 100644 index 0000000..9e0754d --- /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 b49244c..0180599 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/silenceper/wechat/v2 go 1.16 require ( + 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/go-redis/redis/v8 v8.11.5 diff --git a/go.sum b/go.sum index f9efc27..64deda9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +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= @@ -71,6 +75,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT 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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -89,6 +95,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ 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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/miniprogram/README.md b/miniprogram/README.md index b7535e1..925be2e 100644 --- a/miniprogram/README.md +++ b/miniprogram/README.md @@ -4,7 +4,7 @@ ## 包说明 -- analysis 数据分析相关API +- analysis 数据分析相关 API ## 快速入门 @@ -18,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/auth/auth.go b/miniprogram/auth/auth.go index 638ac53..b0af59b 100644 --- a/miniprogram/auth/auth.go +++ b/miniprogram/auth/auth.go @@ -40,8 +40,8 @@ type ResCode2Session struct { type RspCheckEncryptedData struct { util.CommonError - Vaild bool `json:"vaild"` // 是否是合法的数据 - CreateTime uint `json:"create_time"` // 加密数据生成的时间戳 + Vaild bool `json:"vaild"` // 是否是合法的数据 + CreateTime uint64 `json:"create_time"` // 加密数据生成的时间戳 } // Code2Session 登录凭证校验。 @@ -138,10 +138,8 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get } var result GetPhoneNumberResponse - if err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber"); err != nil { - return nil, err - } - return &result, nil + err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber") + return &result, err } // GetPhoneNumber 小程序通过code获取用户手机号 diff --git a/miniprogram/business/phone_number.go b/miniprogram/business/phone_number.go index bf99057..7292149 100644 --- a/miniprogram/business/phone_number.go +++ b/miniprogram/business/phone_number.go @@ -45,10 +45,5 @@ func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneI PhoneInfo PhoneInfo `json:"phone_info"` } err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber") - if nil != err { - return - } - - info = resp.PhoneInfo - return + return resp.PhoneInfo, err } diff --git a/miniprogram/config/config.go b/miniprogram/config/config.go index f371883..fb3e151 100644 --- a/miniprogram/config/config.go +++ b/miniprogram/config/config.go @@ -1,4 +1,4 @@ -// Package config 小程序config配置 +// Package config 小程序 config 配置 package config import ( @@ -7,7 +7,11 @@ import ( // Config .config for 小程序 type Config struct { - AppID string `json:"app_id"` // appid - AppSecret string `json:"app_secret"` // appSecret - Cache cache.Cache + 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"` // EncodingAESKey + Cache cache.Cache } diff --git a/miniprogram/message/consts.go b/miniprogram/message/consts.go index cd8f585..496bb9c 100644 --- a/miniprogram/message/consts.go +++ b/miniprogram/message/consts.go @@ -20,6 +20,12 @@ const ( MsgTypeLink = "link" // MsgTypeMiniProgramPage 小程序卡片 MsgTypeMiniProgramPage = "miniprogrampage" + // MsgTypeEvent 事件 + MsgTypeEvent MsgType = "event" + // DataTypeXML XML 格式数据 + DataTypeXML = "xml" + // DataTypeJSON JSON 格式数据 + DataTypeJSON = "json" ) // CommonToken 消息中通用的结构 diff --git a/miniprogram/message/customer_message.go b/miniprogram/message/customer_message.go index ce3830f..0f0abd3 100644 --- a/miniprogram/message/customer_message.go +++ b/miniprogram/message/customer_message.go @@ -28,7 +28,7 @@ type MediaText struct { Content string `json:"content"` } -// MediaResource 消息使用的临时素材id +// MediaResource 消息使用的临时素材 id type MediaResource struct { MediaID string `json:"media_id"` } @@ -51,7 +51,7 @@ type MediaLink struct { // CustomerMessage 客服消息 type CustomerMessage struct { - ToUser string `json:"touser"` // 接受者OpenID + ToUser string `json:"touser"` // 接受者 OpenID Msgtype MsgType `json:"msgtype"` // 客服消息类型 Text *MediaText `json:"text,omitempty"` // 可选 Image *MediaResource `json:"image,omitempty"` // 可选 diff --git a/miniprogram/message/message.go b/miniprogram/message/message.go new file mode 100644 index 0000000..d684986 --- /dev/null +++ b/miniprogram/message/message.go @@ -0,0 +1,579 @@ +package message + +import ( + "encoding/json" + "encoding/xml" + "errors" + "io" + "net/http" + "sort" + "strings" + + "github.com/tidwall/gjson" + + "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" + // EventSubscribePopup 用户操作订阅通知弹窗事件推送,用户在图文等场景内订阅通知的操作 + EventSubscribePopup EventType = "subscribe_msg_popup_event" + // EventSubscribeMsgChange 用户管理订阅通知,用户在服务通知管理页面做通知管理时的操作 + EventSubscribeMsgChange EventType = "subscribe_msg_change_event" + // EventSubscribeMsgSent 发送订阅通知,调用 bizsend 接口发送通知 + EventSubscribeMsgSent EventType = "subscribe_msg_sent_event" + // ConfirmReceiveMethodAuto 自动确认收货 + ConfirmReceiveMethodAuto ConfirmReceiveMethod = 1 + // ConfirmReceiveMethodManual 手动确认收货 + ConfirmReceiveMethodManual ConfirmReceiveMethod = 2 +) + +const ( + // InfoTypeAcceptSubscribeMessage 接受订阅通知 + InfoTypeAcceptSubscribeMessage InfoType = "accept" + // InfoTypeRejectSubscribeMessage 拒绝订阅通知 + InfoTypeRejectSubscribeMessage InfoType = "reject" +) + +// 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 + case EventSubscribePopup: + // 用户操作订阅通知弹窗事件推送 + return receiver.unmarshalSubscribePopup(dataType, decryptMsg) + case EventSubscribeMsgChange: + // 用户管理订阅通知事件推送 + return receiver.unmarshalSubscribeMsgChange(dataType, decryptMsg) + case EventSubscribeMsgSent: + // 用户发送订阅通知事件推送 + return receiver.unmarshalSubscribeMsgSent(dataType, decryptMsg) + } + // 暂不支持其他事件类型,直接返回解密后的数据,由调用方处理 + return decryptMsg, nil +} + +// unmarshal 解析推送的数据 +func (receiver *PushReceiver) unmarshal(dataType string, decryptMsg []byte, pushData interface{}) error { + if dataType == DataTypeXML { + return xml.Unmarshal(decryptMsg, pushData) + } + return json.Unmarshal(decryptMsg, pushData) +} + +// unmarshalSubscribePopup +func (receiver *PushReceiver) unmarshalSubscribePopup(dataType string, decryptMsg []byte) (PushData, error) { + var pushData PushDataSubscribePopup + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + if err == nil { + listData := gjson.Get(string(decryptMsg), "List") + if listData.IsObject() { + listItem := SubscribeMsgPopupEventList{} + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil { + return &pushData, parseErr + } + pushData.SetSubscribeMsgPopupEvents([]SubscribeMsgPopupEventList{listItem}) + } else if listData.IsArray() { + listItems := make([]SubscribeMsgPopupEventList, 0) + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil { + return &pushData, parseErr + } + pushData.SetSubscribeMsgPopupEvents(listItems) + } + } + + return &pushData, err +} + +// unmarshalSubscribeMsgChange 解析用户管理订阅通知事件推送 +func (receiver *PushReceiver) unmarshalSubscribeMsgChange(dataType string, decryptMsg []byte) (PushData, error) { + var pushData PushDataSubscribeMsgChange + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + if err == nil { + listData := gjson.Get(string(decryptMsg), "List") + if listData.IsObject() { + listItem := SubscribeMsgChangeList{} + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil { + return &pushData, parseErr + } + pushData.SetSubscribeMsgChangeEvents([]SubscribeMsgChangeList{listItem}) + } else if listData.IsArray() { + listItems := make([]SubscribeMsgChangeList, 0) + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil { + return &pushData, parseErr + } + pushData.SetSubscribeMsgChangeEvents(listItems) + } + } + return &pushData, err +} + +// unmarshalSubscribeMsgSent 解析用户发送订阅通知事件推送 +func (receiver *PushReceiver) unmarshalSubscribeMsgSent(dataType string, decryptMsg []byte) (PushData, error) { + var pushData PushDataSubscribeMsgSent + err := receiver.unmarshal(dataType, decryptMsg, &pushData) + if err == nil { + listData := gjson.Get(string(decryptMsg), "List") + if listData.IsObject() { + listItem := SubscribeMsgSentList{} + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil { + return &pushData, parseErr + } + pushData.SetSubscribeMsgSentEvents([]SubscribeMsgSentList{listItem}) + } else if listData.IsArray() { + listItems := make([]SubscribeMsgSentList, 0) + if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil { + return &pushData, parseErr + } + pushData.SetSubscribeMsgSentEvents(listItems) + } + } + return &pushData, err +} + +// 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 int64 `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 int64 `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"` // 透传信息 +} + +// PushDataSubscribePopup 用户操作订阅通知弹窗事件推送 +type PushDataSubscribePopup struct { + CommonPushData + subscribeMsgPopupEventList []SubscribeMsgPopupEventList `json:"-"` + SubscribeMsgPopupEvent SubscribeMsgPopupEvent `xml:"SubscribeMsgPopupEvent"` +} + +// SubscribeMsgPopupEvent 用户操作订阅通知弹窗消息回调 +type SubscribeMsgPopupEvent struct { + List []SubscribeMsgPopupEventList `xml:"List"` +} + +// SubscribeMsgPopupEventList 订阅消息事件列表 +type SubscribeMsgPopupEventList struct { + TemplateID string `xml:"TemplateId" json:"TemplateId"` + SubscribeStatusString string `xml:"SubscribeStatusString" json:"SubscribeStatusString"` + PopupScene string `xml:"PopupScene" json:"PopupScene"` +} + +// SetSubscribeMsgPopupEvents 设置订阅消息事件 +func (s *PushDataSubscribePopup) SetSubscribeMsgPopupEvents(list []SubscribeMsgPopupEventList) { + s.subscribeMsgPopupEventList = list +} + +// GetSubscribeMsgPopupEvents 获取订阅消息事件数据 +func (s *PushDataSubscribePopup) GetSubscribeMsgPopupEvents() []SubscribeMsgPopupEventList { + if s.subscribeMsgPopupEventList != nil { + return s.subscribeMsgPopupEventList + } + + if s.SubscribeMsgPopupEvent.List == nil || len(s.SubscribeMsgPopupEvent.List) < 1 { + return nil + } + return s.SubscribeMsgPopupEvent.List +} + +// PushDataSubscribeMsgChange 用户管理订阅通知事件推送 +type PushDataSubscribeMsgChange struct { + CommonPushData + SubscribeMsgChangeEvent SubscribeMsgChangeEvent `xml:"SubscribeMsgChangeEvent"` + subscribeMsgChangeList []SubscribeMsgChangeList `json:"-"` +} + +// SubscribeMsgChangeEvent 用户管理订阅通知回调 +type SubscribeMsgChangeEvent struct { + List []SubscribeMsgChangeList `xml:"List" json:"List"` +} + +// SubscribeMsgChangeList 订阅消息事件列表 +type SubscribeMsgChangeList struct { + TemplateID string `xml:"TemplateId" json:"TemplateId"` + SubscribeStatusString string `xml:"SubscribeStatusString" json:"SubscribeStatusString"` +} + +// SetSubscribeMsgChangeEvents 设置订阅消息事件 +func (s *PushDataSubscribeMsgChange) SetSubscribeMsgChangeEvents(list []SubscribeMsgChangeList) { + s.subscribeMsgChangeList = list +} + +// GetSubscribeMsgChangeEvents 获取订阅消息事件数据 +func (s *PushDataSubscribeMsgChange) GetSubscribeMsgChangeEvents() []SubscribeMsgChangeList { + if s.subscribeMsgChangeList != nil { + return s.subscribeMsgChangeList + } + + if s.SubscribeMsgChangeEvent.List == nil || len(s.SubscribeMsgChangeEvent.List) < 1 { + return nil + } + + return s.SubscribeMsgChangeEvent.List +} + +// PushDataSubscribeMsgSent 用户发送订阅通知事件推送 +type PushDataSubscribeMsgSent struct { + CommonPushData + SubscribeMsgSentEvent SubscribeMsgSentEvent `xml:"SubscribeMsgSentEvent"` + subscribeMsgSentEventList []SubscribeMsgSentList `json:"-"` +} + +// SubscribeMsgSentEvent 用户发送订阅通知回调 +type SubscribeMsgSentEvent struct { + List []SubscribeMsgSentList `xml:"List" json:"List"` +} + +// SubscribeMsgSentList 订阅消息事件列表 +type SubscribeMsgSentList struct { + TemplateID string `xml:"TemplateId" json:"TemplateId"` + MsgID string `xml:"MsgID" json:"MsgID"` + ErrorCode int `xml:"ErrorCode" json:"ErrorCode"` + ErrorStatus string `xml:"ErrorStatus" json:"ErrorStatus"` +} + +// SetSubscribeMsgSentEvents 设置订阅消息事件 +func (s *PushDataSubscribeMsgSent) SetSubscribeMsgSentEvents(list []SubscribeMsgSentList) { + s.subscribeMsgSentEventList = list +} + +// GetSubscribeMsgSentEvents 获取订阅消息事件数据 +func (s *PushDataSubscribeMsgSent) GetSubscribeMsgSentEvents() []SubscribeMsgSentList { + if s.subscribeMsgSentEventList != nil { + return s.subscribeMsgSentEventList + } + + if s.SubscribeMsgSentEvent.List == nil || len(s.SubscribeMsgSentEvent.List) < 1 { + return nil + } + + return s.SubscribeMsgSentEvent.List +} diff --git a/miniprogram/message/reply.go b/miniprogram/message/reply.go new file mode 100644 index 0000000..b7fba06 --- /dev/null +++ b/miniprogram/message/reply.go @@ -0,0 +1,15 @@ +package message + +import "errors" + +// ErrInvalidReply 无效的回复 +var ErrInvalidReply = errors.New("无效的回复信息") + +// ErrUnsupportedReply 不支持的回复类型 +var ErrUnsupportedReply = errors.New("不支持的回复消息") + +// Reply 消息回复 +type Reply struct { + MsgType MsgType + MsgData interface{} +} diff --git a/miniprogram/message/updatable_msg.go b/miniprogram/message/updatable_msg.go new file mode 100644 index 0000000..cf5cdd1 --- /dev/null +++ b/miniprogram/message/updatable_msg.go @@ -0,0 +1,102 @@ +package message + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + // createActivityURL 创建activity_id + createActivityURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s" + // SendUpdatableMsgURL 修改动态消息 + setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s" +) + +// UpdatableTargetState 动态消息状态 +type UpdatableTargetState int + +const ( + // TargetStateNotStarted 未开始 + TargetStateNotStarted UpdatableTargetState = 0 + // TargetStateStarted 已开始 + TargetStateStarted UpdatableTargetState = 1 + // TargetStateFinished 已结束 + TargetStateFinished UpdatableTargetState = 2 +) + +// UpdatableMessage 动态消息 +type UpdatableMessage struct { + *context.Context +} + +// NewUpdatableMessage 实例化 +func NewUpdatableMessage(ctx *context.Context) *UpdatableMessage { + return &UpdatableMessage{ + Context: ctx, + } +} + +// CreateActivityID 创建activity_id +func (updatableMessage *UpdatableMessage) CreateActivityID() (res CreateActivityIDResponse, err error) { + accessToken, err := updatableMessage.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(createActivityURL, accessToken) + response, err := util.HTTPGet(uri) + if err != nil { + return + } + err = util.DecodeWithError(response, &res, "CreateActivityID") + return +} + +// SetUpdatableMsg 修改动态消息 +func (updatableMessage *UpdatableMessage) SetUpdatableMsg(activityID string, targetState UpdatableTargetState, template UpdatableMsgTemplate) (err error) { + accessToken, err := updatableMessage.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(setUpdatableMsgURL, accessToken) + data := SendUpdatableMsgReq{ + ActivityID: activityID, + TargetState: targetState, + TemplateInfo: template, + } + + response, err := util.PostJSON(uri, data) + if err != nil { + return + } + return util.DecodeWithCommonError(response, "SendUpdatableMsg") +} + +// CreateActivityIDResponse 创建activity_id 返回 +type CreateActivityIDResponse struct { + util.CommonError + + ActivityID string `json:"activity_id"` + ExpirationTime int64 `json:"expiration_time"` +} + +// UpdatableMsgTemplate 动态消息模板 +type UpdatableMsgTemplate struct { + ParameterList []UpdatableMsgParameter `json:"parameter_list"` +} + +// UpdatableMsgParameter 动态消息参数 +type UpdatableMsgParameter struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// SendUpdatableMsgReq 修改动态消息参数 +type SendUpdatableMsgReq struct { + ActivityID string `json:"activity_id"` + TemplateInfo UpdatableMsgTemplate `json:"template_info"` + TargetState UpdatableTargetState `json:"target_state"` +} 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 d76d7c2..8adbb00 100644 --- a/miniprogram/miniprogram.go +++ b/miniprogram/miniprogram.go @@ -2,6 +2,7 @@ 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" @@ -10,8 +11,11 @@ import ( "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/redpacketcover" "github.com/silenceper/wechat/v2/miniprogram/riskcontrol" "github.com/silenceper/wechat/v2/miniprogram/security" "github.com/silenceper/wechat/v2/miniprogram/shortlink" @@ -19,15 +23,16 @@ import ( "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" ) -// 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{ @@ -37,7 +42,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 } @@ -67,17 +72,17 @@ func (miniProgram *MiniProgram) GetBusiness() *business.Business { return business.NewBusiness(miniProgram.ctx) } -// GetPrivacy 小程序隐私协议相关API +// GetPrivacy 小程序隐私协议相关 API func (miniProgram *MiniProgram) GetPrivacy() *privacy.Privacy { return privacy.NewPrivacy(miniProgram.ctx) } -// GetQRCode 小程序码相关API +// 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) } @@ -102,7 +107,7 @@ func (miniProgram *MiniProgram) GetContentSecurity() *content.Content { return content.NewContent(miniProgram.ctx) } -// GetURLLink 小程序URL Link接口 +// GetURLLink 小程序 URL Link 接口 func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink { return urllink.NewURLLink(miniProgram.ctx) } @@ -122,7 +127,42 @@ func (miniProgram *MiniProgram) GetShortLink() *shortlink.ShortLink { return shortlink.NewShortLink(miniProgram.ctx) } -// GetSURLScheme 小程序URL Scheme接口 +// 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) +} + +// GetRedPacketCover 小程序微信红包封面 API +func (miniProgram *MiniProgram) GetRedPacketCover() *redpacketcover.RedPacketCover { + return redpacketcover.NewRedPacketCover(miniProgram.ctx) +} + +// GetUpdatableMessage 小程序动态消息 +func (miniProgram *MiniProgram) GetUpdatableMessage() *message.UpdatableMessage { + return message.NewUpdatableMessage(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 index 024e379..176dfc5 100644 --- a/miniprogram/privacy/privacy.go +++ b/miniprogram/privacy/privacy.go @@ -103,11 +103,8 @@ func (s *Privacy) GetPrivacySetting(privacyVer int) (GetPrivacySettingResponse, } // 返回错误信息 var result GetPrivacySettingResponse - if err = util.DecodeWithError(response, &result, "getprivacysetting"); err != nil { - return GetPrivacySettingResponse{}, err - } - - return result, nil + err = util.DecodeWithError(response, &result, "getprivacysetting") + return result, err } // SetPrivacySetting 更新小程序权限配置 @@ -130,11 +127,7 @@ func (s *Privacy) SetPrivacySetting(privacyVer int, ownerSetting OwnerSetting, s } // 返回错误信息 - if err = util.DecodeWithCommonError(response, "setprivacysetting"); err != nil { - return err - } - - return err + return util.DecodeWithCommonError(response, "setprivacysetting") } // UploadPrivacyExtFileResponse 上传权限定义模板响应参数 @@ -159,9 +152,6 @@ func (s *Privacy) UploadPrivacyExtFile(fileData []byte) (UploadPrivacyExtFileRes // 返回错误信息 var result UploadPrivacyExtFileResponse - if err = util.DecodeWithError(response, &result, "setprivacysetting"); err != nil { - return UploadPrivacyExtFileResponse{}, err - } - + err = util.DecodeWithError(response, &result, "setprivacysetting") return result, err } diff --git a/miniprogram/redpacketcover/redpacketcover.go b/miniprogram/redpacketcover/redpacketcover.go new file mode 100644 index 0000000..08e00bf --- /dev/null +++ b/miniprogram/redpacketcover/redpacketcover.go @@ -0,0 +1,59 @@ +package redpacketcover + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + getRedPacketCoverURL = "https://api.weixin.qq.com/redpacketcover/wxapp/cover_url/get_by_token?access_token=%s" +) + +// RedPacketCover struct +type RedPacketCover struct { + *context.Context +} + +// NewRedPacketCover 实例 +func NewRedPacketCover(context *context.Context) *RedPacketCover { + redPacketCover := new(RedPacketCover) + redPacketCover.Context = context + return redPacketCover +} + +// GetRedPacketCoverRequest 获取微信红包封面参数 +type GetRedPacketCoverRequest struct { + // openid 可领取用户的openid + OpenID string `json:"openid"` + // ctoken 在红包封面平台获取发放ctoken(需要指定可以发放的appid) + CToken string `json:"ctoken"` +} + +// GetRedPacketCoverResp 获取微信红包封面 +type GetRedPacketCoverResp struct { + util.CommonError + Data struct { + URL string `json:"url"` + } `json:"data"` // 唯一请求标识 +} + +// GetRedPacketCoverURL 获得指定用户可以领取的红包封面链接。获取参数ctoken参考微信红包封面开放平台 +// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/red-packet-cover/getRedPacketCoverUrl.html +func (cover *RedPacketCover) GetRedPacketCoverURL(coderParams GetRedPacketCoverRequest) (res GetRedPacketCoverResp, err error) { + accessToken, err := cover.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getRedPacketCoverURL, accessToken) + response, err := util.PostJSON(uri, coderParams) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "GetRedPacketCoverURL") + return +} diff --git a/miniprogram/security/security.go b/miniprogram/security/security.go index 7e4d5dc..3e979a5 100644 --- a/miniprogram/security/security.go +++ b/miniprogram/security/security.go @@ -51,12 +51,7 @@ func (security *Security) MediaCheckAsyncV1(in *MediaCheckAsyncV1Request) (trace TraceID string `json:"trace_id"` } err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1") - if err != nil { - return - } - - traceID = res.TraceID - return + return res.TraceID, err } // MediaCheckAsyncRequest 图片/音频异步校验请求参数 @@ -93,12 +88,7 @@ func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID s TraceID string `json:"trace_id"` } err = util.DecodeWithError(response, &res, "MediaCheckAsync") - if err != nil { - return - } - - traceID = res.TraceID - return + return res.TraceID, err } // ImageCheckV1 校验一张图片是否含有违法违规内容(同步) diff --git a/miniprogram/shortlink/shortlink.go b/miniprogram/shortlink/shortlink.go index 34c1700..f55b89b 100644 --- a/miniprogram/shortlink/shortlink.go +++ b/miniprogram/shortlink/shortlink.go @@ -24,10 +24,10 @@ func NewShortLink(ctx *context.Context) *ShortLink { // ShortLinker 请求结构体 type ShortLinker struct { - // pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大1024个字符 + // pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大 1024 个字符 PageURL string `json:"page_url"` - // pageTitle 页面标题,不能包含违法信息,超过20字符会用... 截断代替 + // pageTitle 页面标题,不能包含违法信息,超过 20 字符会用... 截断代替 PageTitle string `json:"page_title"` // isPermanent 生成的 Short Link 类型,短期有效:false,永久有效:true @@ -60,14 +60,10 @@ func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error // 使用通用方法返回错误 var res resShortLinker err = util.DecodeWithError(response, &res, "GenerateShortLink") - if err != nil { - return "", err - } - - return res.Link, nil + return res.Link, err } -// GenerateShortLinkPermanent 生成永久shortLink +// GenerateShortLinkPermanent 生成永久 shortLink func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string) (string, error) { return shortLink.generate(ShortLinker{ PageURL: PageURL, @@ -76,7 +72,7 @@ func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string }) } -// GenerateShortLinkTemp 生成临时shortLink +// GenerateShortLinkTemp 生成临时 shortLink func (shortLink *ShortLink) GenerateShortLinkTemp(PageURL, pageTitle string) (string, error) { return shortLink.generate(ShortLinker{ PageURL: PageURL, diff --git a/miniprogram/subscribe/subscribe.go b/miniprogram/subscribe/subscribe.go index 0015a43..9099044 100644 --- a/miniprogram/subscribe/subscribe.go +++ b/miniprogram/subscribe/subscribe.go @@ -168,11 +168,7 @@ func (s *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templa } var result resSubscribeAdd err = util.DecodeWithError(response, &result, "AddSubscribe") - if err != nil { - return - } - templateID = result.TemplateID - return + return result.TemplateID, err } // Delete 删除私有模板 diff --git a/miniprogram/urllink/urllink.go b/miniprogram/urllink/urllink.go index 378d58a..56975a6 100644 --- a/miniprogram/urllink/urllink.go +++ b/miniprogram/urllink/urllink.go @@ -65,8 +65,5 @@ func (u *URLLink) Generate(params *ULParams) (string, error) { } var resp ULResult err = util.DecodeWithError(response, &resp, "URLLink.Generate") - if err != nil { - return "", err - } - return resp.URLLink, nil + return resp.URLLink, err } diff --git a/miniprogram/urlscheme/query.go b/miniprogram/urlscheme/query.go index d9ff481..72506e8 100644 --- a/miniprogram/urlscheme/query.go +++ b/miniprogram/urlscheme/query.go @@ -37,7 +37,7 @@ type SchemeInfo struct { // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数 type resQueryScheme struct { // 通用错误 - *util.CommonError + util.CommonError // scheme 配置 SchemeInfo SchemeInfo `json:"scheme_info"` // 访问该链接的openid,没有用户访问过则为空字符串 @@ -62,9 +62,5 @@ func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo Schem // 使用通用方法返回错误 var res resQueryScheme err = util.DecodeWithError(response, &res, "QueryScheme") - if err != nil { - return - } - - return res.SchemeInfo, res.VisitOpenid, nil + return res.SchemeInfo, res.VisitOpenid, err } diff --git a/miniprogram/urlscheme/urlscheme.go b/miniprogram/urlscheme/urlscheme.go index 37bcd75..2659cfb 100644 --- a/miniprogram/urlscheme/urlscheme.go +++ b/miniprogram/urlscheme/urlscheme.go @@ -78,8 +78,5 @@ func (u *URLScheme) Generate(params *USParams) (string, error) { } var resp USResult err = util.DecodeWithError(response, &resp, "URLScheme.Generate") - if err != nil { - return "", err - } - return resp.OpenLink, nil + return resp.OpenLink, err } 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/short_url.go b/officialaccount/basic/short_url.go index 94be9bb..a3a1440 100644 --- a/officialaccount/basic/short_url.go +++ b/officialaccount/basic/short_url.go @@ -44,9 +44,6 @@ func (basic *Basic) Long2ShortURL(longURL string) (shortURL string, err error) { if err != nil { return } - if err = util.DecodeWithError(responseBytes, resp, long2shortAction); err != nil { - return - } - shortURL = resp.ShortURL - return + err = util.DecodeWithError(responseBytes, resp, long2shortAction) + return resp.ShortURL, err } diff --git a/officialaccount/broadcast/broadcast.go b/officialaccount/broadcast/broadcast.go index 885f683..edb1d2b 100644 --- a/officialaccount/broadcast/broadcast.go +++ b/officialaccount/broadcast/broadcast.go @@ -79,6 +79,10 @@ type sendRequest struct { Mpnews map[string]interface{} `json:"mpnews,omitempty"` // 发送语音 Voice map[string]interface{} `json:"voice,omitempty"` + // 发送视频 + Mpvideo map[string]interface{} `json:"mpvideo,omitempty"` + // 发送图片-预览使用 + Image map[string]interface{} `json:"image,omitempty"` // 发送图片 Images *Image `json:"images,omitempty"` // 发送卡券 @@ -183,7 +187,13 @@ func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error ToUser: nil, MsgType: MsgTypeImage, } - req.Images = images + if broadcast.preview { + req.Image = map[string]interface{}{ + "media_id": images.MediaIDs[0], + } + } else { + req.Images = images + } req, sendURL := broadcast.chooseTagOrOpenID(user, req) url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) data, err := util.PostJSON(url, req) @@ -205,7 +215,7 @@ func (broadcast *Broadcast) SendVideo(user *User, mediaID string, title, descrip ToUser: nil, MsgType: MsgTypeVideo, } - req.Voice = map[string]interface{}{ + req.Mpvideo = map[string]interface{}{ "media_id": mediaID, "title": title, "description": description, diff --git a/officialaccount/customerservice/manager.go b/officialaccount/customerservice/manager.go index 6422fb7..973a86c 100644 --- a/officialaccount/customerservice/manager.go +++ b/officialaccount/customerservice/manager.go @@ -72,11 +72,7 @@ func (csm *Manager) List() (customerServiceList []*KeFuInfo, err error) { } var res resKeFuList err = util.DecodeWithError(response, &res, "ListCustomerService") - if err != nil { - return - } - customerServiceList = res.KfList - return + return res.KfList, err } // KeFuOnlineInfo 客服在线信息 @@ -107,11 +103,7 @@ func (csm *Manager) OnlineList() (customerServiceOnlineList []*KeFuOnlineInfo, e } var res resKeFuOnlineList err = util.DecodeWithError(response, &res, "ListOnlineCustomerService") - if err != nil { - return - } - customerServiceOnlineList = res.KfOnlineList - return + return res.KfOnlineList, err } // Add 添加客服账号 diff --git a/officialaccount/datacube/publisher.go b/officialaccount/datacube/publisher.go index bd640d0..73997cf 100644 --- a/officialaccount/datacube/publisher.go +++ b/officialaccount/datacube/publisher.go @@ -183,9 +183,6 @@ func (cube *DataCube) fetchData(params ParamsPublisher) (response []byte, err er uri := fmt.Sprintf("%s?%s", publisherURL, v.Encode()) response, err = util.HTTPGet(uri) - if err != nil { - return - } return } diff --git a/officialaccount/draft/draft.go b/officialaccount/draft/draft.go index e7f15d3..76a3751 100644 --- a/officialaccount/draft/draft.go +++ b/officialaccount/draft/draft.go @@ -64,11 +64,7 @@ func (draft *Draft) AddDraft(articles []*Article) (mediaID string, err error) { MediaID string `json:"media_id"` } err = util.DecodeWithError(response, &res, "AddDraft") - if err != nil { - return - } - mediaID = res.MediaID - return + return res.MediaID, err } // GetDraft 获取草稿 @@ -94,12 +90,7 @@ func (draft *Draft) GetDraft(mediaID string) (articles []*Article, err error) { NewsItem []*Article `json:"news_item"` } err = util.DecodeWithError(response, &res, "GetDraft") - if err != nil { - return - } - - articles = res.NewsItem - return + return res.NewsItem, err } // DeleteDraft 删除草稿 @@ -172,12 +163,7 @@ func (draft *Draft) CountDraft() (total uint, err error) { Total uint `json:"total_count"` } err = util.DecodeWithError(response, &res, "CountDraft") - if nil != err { - return - } - - total = res.Total - return + return res.Total, err } // ArticleList 草稿列表 diff --git a/officialaccount/freepublish/freepublish.go b/officialaccount/freepublish/freepublish.go index 041bf66..7c414a8 100644 --- a/officialaccount/freepublish/freepublish.go +++ b/officialaccount/freepublish/freepublish.go @@ -73,12 +73,7 @@ func (freePublish *FreePublish) Publish(mediaID string) (publishID int64, err er PublishID int64 `json:"publish_id"` } err = util.DecodeWithError(response, &res, "SubmitFreePublish") - if err != nil { - return - } - - publishID = res.PublishID - return + return res.PublishID, err } // PublishStatusList 发布任务状态列表 @@ -191,12 +186,7 @@ func (freePublish *FreePublish) First(articleID string) (list []Article, err err NewsItem []Article `json:"news_item"` } err = util.DecodeWithError(response, &res, "FirstFreePublish") - if err != nil { - return - } - - list = res.NewsItem - return + return res.NewsItem, err } // ArticleList 发布列表 diff --git a/officialaccount/material/material.go b/officialaccount/material/material.go index 54daade..5b5f1c6 100644 --- a/officialaccount/material/material.go +++ b/officialaccount/material/material.go @@ -27,7 +27,7 @@ const ( PermanentMaterialTypeImage PermanentMaterialType = "image" // PermanentMaterialTypeVideo 永久素材视频类型(video) PermanentMaterialTypeVideo PermanentMaterialType = "video" - // PermanentMaterialTypeVoice 永久素材语音类型 (voice) + // PermanentMaterialTypeVoice 永久素材语音类型(voice) PermanentMaterialTypeVoice PermanentMaterialType = "voice" // PermanentMaterialTypeNews 永久素材图文类型(news) PermanentMaterialTypeNews PermanentMaterialType = "news" @@ -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"` @@ -337,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/message/customer_message.go b/officialaccount/message/customer_message.go index 67acc8a..6742a61 100644 --- a/officialaccount/message/customer_message.go +++ b/officialaccount/message/customer_message.go @@ -38,6 +38,7 @@ type CustomerMessage struct { Wxcard *MediaWxcard `json:"wxcard,omitempty"` // 可选 Msgmenu *MediaMsgmenu `json:"msgmenu,omitempty"` // 可选 Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` // 可选 + Mpnewsarticle *MediaArticle `json:"mpnewsarticle,omitempty"` // 可选 } // NewCustomerTextMessage 文本消息结构体构造方法 @@ -97,6 +98,11 @@ type MediaResource struct { MediaID string `json:"media_id"` } +// MediaArticle 消息使用的已发布文章id +type MediaArticle struct { + ArticleID string `json:"article_id"` +} + // MediaVideo 视频消息包含的内容 type MediaVideo struct { MediaID string `json:"media_id"` diff --git a/officialaccount/message/message.go b/officialaccount/message/message.go index 8ab75a9..e5fa245 100644 --- a/officialaccount/message/message.go +++ b/officialaccount/message/message.go @@ -27,15 +27,15 @@ const ( MsgTypeVideo MsgType = "video" // MsgTypeMiniprogrampage 表示小程序卡片消息 MsgTypeMiniprogrampage MsgType = "miniprogrampage" - // MsgTypeShortVideo 表示短视频消息[限接收] + // MsgTypeShortVideo 表示短视频消息 [限接收] MsgTypeShortVideo MsgType = "shortvideo" - // MsgTypeLocation 表示坐标消息[限接收] + // MsgTypeLocation 表示坐标消息 [限接收] MsgTypeLocation MsgType = "location" - // MsgTypeLink 表示链接消息[限接收] + // MsgTypeLink 表示链接消息 [限接收] MsgTypeLink MsgType = "link" - // MsgTypeMusic 表示音乐消息[限回复] + // MsgTypeMusic 表示音乐消息 [限回复] MsgTypeMusic MsgType = "music" - // MsgTypeNews 表示图文消息[限回复] + // MsgTypeNews 表示图文消息 [限回复] MsgTypeNews MsgType = "news" // MsgTypeTransfer 表示消息消息转发到客服 MsgTypeTransfer MsgType = "transfer_customer_service" @@ -91,7 +91,7 @@ const ( const ( // 微信开放平台需要用到 - // InfoTypeVerifyTicket 返回ticket + // InfoTypeVerifyTicket 返回 ticket InfoTypeVerifyTicket InfoType = "component_verify_ticket" // InfoTypeAuthorized 授权 InfoTypeAuthorized InfoType = "authorized" @@ -108,8 +108,8 @@ 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"` @@ -123,6 +123,7 @@ type MixMessage struct { Title string `xml:"Title"` Description string `xml:"Description"` URL string `xml:"Url"` + BizMsgMenuID int64 `xml:"bizmsgmenuid"` // 事件相关 Event EventType `xml:"Event" json:"Event"` @@ -165,17 +166,17 @@ type MixMessage struct { // 事件相关:发布能力 PublishEventInfo struct { - PublishID int64 `xml:"publish_id"` // 发布任务id + PublishID int64 `xml:"publish_id"` // 发布任务 id PublishStatus freepublish.PublishStatus `xml:"publish_status"` // 发布状态 - ArticleID string `xml:"article_id"` // 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景 + 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:"article_detail"` // 当发布状态为 0 时(即成功)时,返回内容 + FailIndex []uint `xml:"fail_idx"` // 当发布状态为 2 或 4 时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空 } `xml:"PublishEventInfo"` // 第三方平台相关 @@ -195,6 +196,14 @@ type MixMessage struct { 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"` @@ -213,15 +222,19 @@ 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 的列表,可通过获取永久素材接口拉取截图内容 + // 小程序审核通知 + SuccTime int `xml:"SuccTime"` // 审核成功时的时间戳 + FailTime int `xml:"FailTime"` // 审核不通过的时间戳 + DelayTime int `xml:"DelayTime"` // 审核延后时的时间戳 + Reason string `xml:"Reason"` // 审核不通过的原因 + ScreenShot string `xml:"ScreenShot"` // 审核不通过的截图示例。用 | 分隔的 media_id 的列表,可通过获取永久素材接口拉取截图内容 } // SubscribeMsgPopupEvent 订阅通知事件推送的消息体 @@ -269,7 +282,7 @@ type ResponseEncryptedXMLMsg struct { Nonce string `xml:"Nonce" json:"Nonce"` } -// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略 +// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略 type CDATA string // MarshalXML 实现自己的序列化方法 diff --git a/officialaccount/message/subscribe.go b/officialaccount/message/subscribe.go index 9b6e18a..c0e9516 100644 --- a/officialaccount/message/subscribe.go +++ b/officialaccount/message/subscribe.go @@ -90,11 +90,7 @@ func (tpl *Subscribe) List() (templateList []*PrivateSubscribeItem, err error) { } var res resPrivateSubscribeList err = util.DecodeWithError(response, &res, "ListSubscribe") - if err != nil { - return - } - templateList = res.SubscriptionList - return + return res.SubscriptionList, err } type resSubscribeAdd struct { @@ -123,11 +119,7 @@ func (tpl *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (temp } var result resSubscribeAdd err = util.DecodeWithError(response, &result, "AddSubscribe") - if err != nil { - return - } - templateID = result.TemplateID - return + return result.TemplateID, err } // Delete 删除私有模板 @@ -175,11 +167,7 @@ func (tpl *Subscribe) GetCategory() (categoryList []*PublicTemplateCategory, err } var result resSubscribeCategoryList err = util.DecodeWithError(response, &result, "GetCategory") - if err != nil { - return - } - categoryList = result.CategoryList - return + return result.CategoryList, err } // PublicTemplateKeyWords 模板中的关键词 @@ -210,11 +198,7 @@ func (tpl *Subscribe) GetPubTplKeyWordsByID(titleID string) (keyWordsList []*Pub } var result resPublicTemplateKeyWordsList err = util.DecodeWithError(response, &result, "GetPublicTemplateKeyWords") - if err != nil { - return - } - keyWordsList = result.KeyWordsList - return + return result.KeyWordsList, err } // PublicTemplateTitle 类目下的公共模板 @@ -246,10 +230,5 @@ func (tpl *Subscribe) GetPublicTemplateTitleList(ids string, start int, limit in } var result resPublicTemplateTitleList err = util.DecodeWithError(response, &result, "GetPublicTemplateTitle") - if err != nil { - return - } - count = result.Count - templateTitleList = result.TemplateTitleList - return + return result.Count, result.TemplateTitleList, err } diff --git a/officialaccount/message/template.go b/officialaccount/message/template.go index 79e2e05..1c657ea 100644 --- a/officialaccount/message/template.go +++ b/officialaccount/message/template.go @@ -29,11 +29,12 @@ func NewTemplate(context *context.Context) *Template { // 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必须与发模板消息的公众号是绑定关联关系) @@ -110,11 +111,7 @@ func (tpl *Template) List() (templateList []*TemplateItem, err error) { } var res resTemplateList err = util.DecodeWithError(response, &res, "ListTemplate") - if err != nil { - return - } - templateList = res.TemplateList - return + return res.TemplateList, err } type resTemplateAdd struct { @@ -142,11 +139,7 @@ func (tpl *Template) Add(shortID string) (templateID string, err error) { var result resTemplateAdd err = util.DecodeWithError(response, &result, "AddTemplate") - if err != nil { - return - } - templateID = result.TemplateID - return + return result.TemplateID, err } // Delete 删除私有模板. diff --git a/officialaccount/oauth/oauth.go b/officialaccount/oauth/oauth.go index 385fe9b..58ebfcd 100644 --- a/officialaccount/oauth/oauth.go +++ b/officialaccount/oauth/oauth.go @@ -1,6 +1,7 @@ package oauth import ( + ctx2 "context" "encoding/json" "fmt" "net/http" @@ -64,16 +65,37 @@ 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"` } +// GetUserInfoByCodeContext 通过网页授权的code 换取用户的信息 +func (oauth *Oauth) GetUserInfoByCodeContext(ctx ctx2.Context, code string) (result UserInfo, err error) { + var ( + token ResAccessToken + ) + if token, err = oauth.GetUserAccessTokenContext(ctx, code); err != nil { + return + } + + return oauth.GetUserInfoContext(ctx, token.AccessToken, token.OpenID, "") +} + // GetUserAccessToken 通过网页授权的code 换取access_token(区别于context中的access_token) func (oauth *Oauth) GetUserAccessToken(code string) (result ResAccessToken, err error) { + return oauth.GetUserAccessTokenContext(ctx2.Background(), code) +} + +// GetUserAccessTokenContext 通过网页授权的code 换取access_token(区别于context中的access_token) with context +func (oauth *Oauth) GetUserAccessTokenContext(ctx ctx2.Context, code string) (result ResAccessToken, err error) { urlStr := fmt.Sprintf(accessTokenURL, oauth.AppID, oauth.AppSecret, code) var response []byte - response, err = util.HTTPGet(urlStr) + response, err = util.HTTPGetContext(ctx, urlStr) if err != nil { return } @@ -90,9 +112,14 @@ func (oauth *Oauth) GetUserAccessToken(code string) (result ResAccessToken, err // RefreshAccessToken 刷新access_token func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessToken, err error) { + return oauth.RefreshAccessTokenContext(ctx2.Background(), refreshToken) +} + +// RefreshAccessTokenContext 刷新access_token with context +func (oauth *Oauth) RefreshAccessTokenContext(ctx ctx2.Context, refreshToken string) (result ResAccessToken, err error) { urlStr := fmt.Sprintf(refreshAccessTokenURL, oauth.AppID, refreshToken) var response []byte - response, err = util.HTTPGet(urlStr) + response, err = util.HTTPGetContext(ctx, urlStr) if err != nil { return } @@ -109,9 +136,14 @@ func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessTok // CheckAccessToken 检验access_token是否有效 func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err error) { + return oauth.CheckAccessTokenContext(ctx2.Background(), accessToken, openID) +} + +// CheckAccessTokenContext 检验access_token是否有效 with context +func (oauth *Oauth) CheckAccessTokenContext(ctx ctx2.Context, accessToken, openID string) (b bool, err error) { urlStr := fmt.Sprintf(checkAccessTokenURL, accessToken, openID) var response []byte - response, err = util.HTTPGet(urlStr) + response, err = util.HTTPGetContext(ctx, urlStr) if err != nil { return } @@ -145,12 +177,17 @@ type UserInfo struct { // GetUserInfo 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息 func (oauth *Oauth) GetUserInfo(accessToken, openID, lang string) (result UserInfo, err error) { + return oauth.GetUserInfoContext(ctx2.Background(), accessToken, openID, lang) +} + +// GetUserInfoContext 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息 with context +func (oauth *Oauth) GetUserInfoContext(ctx ctx2.Context, accessToken, openID, lang string) (result UserInfo, err error) { if lang == "" { lang = "zh_CN" } urlStr := fmt.Sprintf(userInfoURL, accessToken, openID, lang) var response []byte - response, err = util.HTTPGet(urlStr) + response, err = util.HTTPGetContext(ctx, urlStr) if err != nil { return } diff --git a/officialaccount/ocr/ocr.go b/officialaccount/ocr/ocr.go index 471d87f..2420a80 100644 --- a/officialaccount/ocr/ocr.go +++ b/officialaccount/ocr/ocr.go @@ -154,134 +154,120 @@ func NewOCR(c *context.Context) *OCR { } // IDCard 身份证OCR识别接口 -func (ocr *OCR) IDCard(path string) (ResIDCard ResIDCard, err error) { +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) { +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) { +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) { +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) { +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) { +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) { +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 3444284..0c00817 100644 --- a/officialaccount/officialaccount.go +++ b/officialaccount/officialaccount.go @@ -4,6 +4,7 @@ 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" @@ -212,3 +213,8 @@ func (officialAccount *OfficialAccount) GetSubscribe() *message.Subscribe { 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 c741ee6..b0547df 100644 --- a/officialaccount/server/server.go +++ b/officialaccount/server/server.go @@ -73,7 +73,7 @@ func (srv *Server) Serve() error { if err != nil { return err } - // 非安全模式下,请求处理方法返回为nil则直接回复success给微信服务器 + // 非安全模式下,请求处理方法返回为 nil 则直接回复 success 给微信服务器 if response == nil && !srv.isSafeMode { srv.String("success") return nil @@ -198,7 +198,7 @@ func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixM if err != nil { return } - // nonstandard json, 目前小程序订阅消息返回数据格式不标准,订阅消息模板单个List返回是对象,多个List返回是数组。 + // nonstandard json, 目前小程序订阅消息返回数据格式不标准,订阅消息模板单个 List 返回是对象,多个 List 返回是数组。 if msg.MsgType == message.MsgTypeEvent { listData := gjson.Get(string(rawXMLMsgBytes), "List") if listData.IsObject() { @@ -284,7 +284,7 @@ func (srv *Server) Send() (err error) { 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/user/migrate.go b/officialaccount/user/migrate.go index 44afe05..cac6a1e 100644 --- a/officialaccount/user/migrate.go +++ b/officialaccount/user/migrate.go @@ -62,10 +62,6 @@ func (user *User) ListChangeOpenIDs(fromAppID string, openIDs ...string) (list * } err = util.DecodeWithError(resp, list, "ListChangeOpenIDs") - if err != nil { - return - } - return } diff --git a/officialaccount/user/tag.go b/officialaccount/user/tag.go index 3a61c5d..6353cfc 100644 --- a/officialaccount/user/tag.go +++ b/officialaccount/user/tag.go @@ -126,10 +126,7 @@ func (user *User) GetTag() (tags []*TagInfo, err error) { Tags []*TagInfo `json:"tags"` } err = json.Unmarshal(response, &result) - if err != nil { - return - } - return result.Tags, nil + return result.Tags, err } // OpenIDListByTag 获取标签下粉丝列表 @@ -154,9 +151,6 @@ func (user *User) OpenIDListByTag(tagID int32, nextOpenID ...string) (userList * } userList = new(TagOpenIDList) err = json.Unmarshal(response, &userList) - if err != nil { - return - } return } diff --git a/officialaccount/user/user.go b/officialaccount/user/user.go index ebbafc9..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,9 +11,10 @@ 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 用户管理 @@ -30,7 +32,11 @@ func NewUser(context *context.Context) *User { // Info 用户基本信息 type Info struct { util.CommonError + userInfo +} +// 用户基本信息 +type userInfo struct { Subscribe int32 `json:"subscribe"` OpenID string `json:"openid"` Nickname string `json:"nickname"` @@ -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/context/accessToken.go b/openplatform/context/accessToken.go index 7d7f47d..465eb17 100644 --- a/openplatform/context/accessToken.go +++ b/openplatform/context/accessToken.go @@ -2,11 +2,13 @@ package context import ( + "context" "encoding/json" "fmt" "net/url" "time" + "github.com/silenceper/wechat/v2/cache" "github.com/silenceper/wechat/v2/util" ) @@ -31,24 +33,29 @@ type ComponentAccessToken struct { 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 } @@ -64,15 +71,20 @@ func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAcce 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 } @@ -80,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 } @@ -88,31 +100,43 @@ func (ctx *Context) GetPreCode() (string, error) { var ret struct { PreCode string `json:"pre_auth_code"` } - if err := json.Unmarshal(body, &ret); err != nil { - return "", err - } - - return ret.PreCode, nil + err = json.Unmarshal(body, &ret) + return ret.PreCode, err } -// 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"` @@ -137,9 +161,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 } @@ -149,7 +173,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 } @@ -169,9 +193,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 } @@ -182,7 +211,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 } @@ -193,22 +222,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"` @@ -258,9 +297,9 @@ type CategoriesInfo struct { Second string `wx:"second"` } -// GetAuthrInfo 获取授权方的帐号基本信息 -func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) { - cat, err := ctx.GetComponentAccessToken() +// 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 } @@ -271,7 +310,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 } @@ -286,3 +325,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 58890ab..ea9a90c 100644 --- a/openplatform/miniprogram/basic/basic.go +++ b/openplatform/miniprogram/basic/basic.go @@ -9,6 +9,12 @@ import ( const ( getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo" + checkNickNameURL = "https://api.weixin.qq.com/cgi-bin/wxverify/checkwxverifynickname" + setNickNameURL = "https://api.weixin.qq.com/wxa/setnickname" + setSignatureURL = "https://api.weixin.qq.com/cgi-bin/account/modifysignature" + setHeadImageURL = "https://api.weixin.qq.com/cgi-bin/account/modifyheadimage" + getSearchStatusURL = "https://api.weixin.qq.com/wxa/getwxasearchstatus" + setSearchStatusURL = "https://api.weixin.qq.com/wxa/changewxasearchstatus" ) // Basic 基础信息设置 @@ -51,3 +57,181 @@ func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) { // TODO // func (encryptor *Basic) modifyDomain() { // } + +// CheckNickNameResp 小程序名称检测结果 +type CheckNickNameResp struct { + util.CommonError + HitCondition bool `json:"hit_condition"` // 是否命中关键字策略。若命中,可以选填关键字材料 + Wording string `json:"wording"` // 命中关键字的说明描述 +} + +// CheckNickName 检测微信认证的名称是否符合规则 +// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/checkNickName.html +func (basic *Basic) CheckNickName(nickname string) (*CheckNickNameResp, error) { + ak, err := basic.GetAuthrAccessToken(basic.AppID) + if err != nil { + return nil, err + } + url := fmt.Sprintf("%s?access_token=%s", checkNickNameURL, ak) + data, err := util.PostJSON(url, map[string]string{ + "nick_name": nickname, + }) + if err != nil { + return nil, err + } + res := &CheckNickNameResp{} + err = util.DecodeWithError(data, res, "CheckNickName") + return res, err +} + +// SetNickNameResp 设置小程序名称结果 +type SetNickNameResp struct { + util.CommonError + AuditID int64 `json:"audit_id"` // 审核单Id,通过用于查询改名审核状态 + Wording string `json:"wording"` // 材料说明 +} + +// SetNickNameParam 设置小程序名称参数 +type SetNickNameParam struct { + NickName string `json:"nick_name"` // 昵称,不支持包含“小程序”关键字的昵称 + IDCard string `json:"id_card,omitempty"` // 身份证照片 mediaid,个人号必填 + License string `json:"license,omitempty"` // 组织机构代码证或营业执照 mediaid,组织号必填 + NameingOtherStuff1 string `json:"naming_other_stuff_1,omitempty"` // 其他证明材料 mediaid,选填 + NameingOtherStuff2 string `json:"naming_other_stuff_2,omitempty"` // 其他证明材料 mediaid,选填 + NameingOtherStuff3 string `json:"naming_other_stuff_3,omitempty"` // 其他证明材料 mediaid,选填 + NameingOtherStuff4 string `json:"naming_other_stuff_4,omitempty"` // 其他证明材料 mediaid,选填 + NameingOtherStuff5 string `json:"naming_other_stuff_5,omitempty"` // 其他证明材料 mediaid,选填 +} + +// SetNickName 设置小程序名称 +func (basic *Basic) SetNickName(nickname string) (*SetNickNameResp, error) { + return basic.SetNickNameFull(&SetNickNameParam{ + NickName: nickname, + }) +} + +// SetNickNameFull 设置小程序名称 +// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setNickName.html +func (basic *Basic) SetNickNameFull(param *SetNickNameParam) (*SetNickNameResp, error) { + ak, err := basic.GetAuthrAccessToken(basic.AppID) + if err != nil { + return nil, err + } + url := fmt.Sprintf("%s?access_token=%s", setNickNameURL, ak) + data, err := util.PostJSON(url, param) + if err != nil { + return nil, err + } + res := &SetNickNameResp{} + err = util.DecodeWithError(data, res, "SetNickName") + return res, err +} + +// SetSignatureResp 小程序功能介绍修改结果 +type SetSignatureResp struct { + util.CommonError +} + +// SetSignature 小程序修改功能介绍 +// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setSignature.html +func (basic *Basic) SetSignature(signature string) error { + ak, err := basic.GetAuthrAccessToken(basic.AppID) + if err != nil { + return err + } + url := fmt.Sprintf("%s?access_token=%s", setSignatureURL, ak) + data, err := util.PostJSON(url, map[string]string{ + "signature": signature, + }) + if err != nil { + return err + } + return util.DecodeWithError(data, &SetSignatureResp{}, "SetSignature") +} + +// GetSearchStatusResp 查询小程序当前是否可被搜索 +type GetSearchStatusResp struct { + util.CommonError + Status int `json:"status"` // 1 表示不可搜索,0 表示可搜索 +} + +// GetSearchStatus 查询小程序当前是否可被搜索 +// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/getSearchStatus.html +func (basic *Basic) GetSearchStatus(signature string) (*GetSearchStatusResp, error) { + ak, err := basic.GetAuthrAccessToken(basic.AppID) + if err != nil { + return nil, err + } + url := fmt.Sprintf("%s?access_token=%s", getSearchStatusURL, ak) + data, err := util.HTTPGet(url) + if err != nil { + return nil, err + } + res := &GetSearchStatusResp{} + err = util.DecodeWithError(data, res, "GetSearchStatus") + return res, err +} + +// SetSearchStatusResp 小程序是否可被搜索修改结果 +type SetSearchStatusResp struct { + util.CommonError +} + +// SetSearchStatus 修改小程序是否可被搜索 +// status: 1 表示不可搜索,0 表示可搜索 +// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setSearchStatus.html +func (basic *Basic) SetSearchStatus(status int) error { + ak, err := basic.GetAuthrAccessToken(basic.AppID) + if err != nil { + return err + } + url := fmt.Sprintf("%s?access_token=%s", setSearchStatusURL, ak) + data, err := util.PostJSON(url, map[string]int{ + "status": status, + }) + if err != nil { + return err + } + return util.DecodeWithError(data, &SetSearchStatusResp{}, "SetSearchStatus") +} + +// SetHeadImageResp 小程序头像修改结果 +type SetHeadImageResp struct { + util.CommonError +} + +// SetHeadImageParam 小程序头像修改参数 +type SetHeadImageParam struct { + HeadImageMediaID string `json:"head_img_media_id"` // 头像素材 media_id + X1 string `json:"x1"` // 裁剪框左上角 x 坐标(取值范围:[0, 1]) + Y1 string `json:"y1"` // 裁剪框左上角 y 坐标(取值范围:[0, 1]) + X2 string `json:"x2"` // 裁剪框右下角 x 坐标(取值范围:[0, 1]) + Y2 string `json:"y2"` // 裁剪框右下角 y 坐标(取值范围:[0, 1]) +} + +// SetHeadImage 修改小程序头像 +func (basic *Basic) SetHeadImage(imgMediaID string) error { + return basic.SetHeadImageFull(&SetHeadImageParam{ + HeadImageMediaID: imgMediaID, + X1: "0", + Y1: "0", + X2: "1", + Y2: "1", + }) +} + +// SetHeadImageFull 修改小程序头像 +// 新增临时素材: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html +// ref: https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/basic-info-management/setHeadImage.html +func (basic *Basic) SetHeadImageFull(param *SetHeadImageParam) error { + ak, err := basic.GetAuthrAccessToken(basic.AppID) + if err != nil { + return err + } + url := fmt.Sprintf("%s?access_token=%s", setHeadImageURL, ak) + data, err := util.PostJSON(url, param) + if err != nil { + return err + } + return util.DecodeWithError(data, &SetHeadImageResp{}, "account/setheadimage") +} diff --git a/openplatform/miniprogram/component/component.go b/openplatform/miniprogram/component/component.go index 2ad70e0..8fff007 100644 --- a/openplatform/miniprogram/component/component.go +++ b/openplatform/miniprogram/component/component.go @@ -36,7 +36,7 @@ type RegisterMiniProgramParam struct { 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) @@ -58,7 +58,7 @@ type GetRegistrationStatusParam struct { 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/openplatform.go b/openplatform/openplatform.go index 80fb82e..5c509b9 100644 --- a/openplatform/openplatform.go +++ b/openplatform/openplatform.go @@ -18,9 +18,6 @@ type OpenPlatform struct { // NewOpenPlatform new openplatform func NewOpenPlatform(cfg *config.Config) *OpenPlatform { - if cfg.Cache == nil { - panic("cache 未设置") - } ctx := &context.Context{ Config: cfg, } diff --git a/pay/pay.go b/pay/pay.go index c1f42f6..95416c2 100644 --- a/pay/pay.go +++ b/pay/pay.go @@ -4,6 +4,7 @@ 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" ) @@ -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/util/error.go b/util/error.go index 5687ce1..72b772f 100644 --- a/util/error.go +++ b/util/error.go @@ -6,7 +6,7 @@ import ( "reflect" ) -// CommonError 微信返回的通用错误json +// CommonError 微信返回的通用错误 json type CommonError struct { apiName string ErrCode int64 `json:"errcode"` @@ -17,7 +17,7 @@ func (c *CommonError) Error() string { return fmt.Sprintf("%s Error , errcode=%d , errmsg=%s", c.apiName, c.ErrCode, c.ErrMsg) } -// NewCommonError 新建CommonError错误,对于无errcode和errmsg的返回也可以返回该通用错误 +// NewCommonError 新建 CommonError 错误,对于无 errcode 和 errmsg 的返回也可以返回该通用错误 func NewCommonError(apiName string, code int64, msg string) *CommonError { return &CommonError{ apiName: apiName, @@ -26,7 +26,7 @@ func NewCommonError(apiName string, code int64, msg string) *CommonError { } } -// DecodeWithCommonError 将返回值按照CommonError解析 +// DecodeWithCommonError 将返回值按照 CommonError 解析 func DecodeWithCommonError(response []byte, apiName string) (err error) { var commError CommonError err = json.Unmarshal(response, &commError) diff --git a/util/http.go b/util/http.go index 38089ae..26e4a7b 100644 --- a/util/http.go +++ b/util/http.go @@ -17,6 +17,19 @@ import ( "golang.org/x/crypto/pkcs12" ) +// URIModifier URI修改器 +type URIModifier func(uri string) string + +var uriModifier URIModifier + +// DefaultHTTPClient 默认httpClient +var DefaultHTTPClient = http.DefaultClient + +// SetURIModifier 设置URI修改器 +func SetURIModifier(fn URIModifier) { + uriModifier = fn +} + // HTTPGet get 请求 func HTTPGet(uri string) ([]byte, error) { return HTTPGetContext(context.Background(), uri) @@ -24,11 +37,14 @@ func HTTPGet(uri string) ([]byte, error) { // HTTPGetContext get 请求 func HTTPGetContext(ctx context.Context, uri string) ([]byte, error) { + if uriModifier != nil { + uri = uriModifier(uri) + } request, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { return nil, err } - response, err := http.DefaultClient.Do(request) + response, err := DefaultHTTPClient.Do(request) if err != nil { return nil, err } @@ -47,6 +63,9 @@ func HTTPPost(uri string, data string) ([]byte, error) { // HTTPPostContext post 请求 func HTTPPostContext(ctx context.Context, uri string, data []byte, header map[string]string) ([]byte, error) { + if uriModifier != nil { + uri = uriModifier(uri) + } body := bytes.NewBuffer(data) request, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, body) if err != nil { @@ -57,7 +76,7 @@ func HTTPPostContext(ctx context.Context, uri string, data []byte, header map[st request.Header.Set(key, value) } - response, err := http.DefaultClient.Do(request) + response, err := DefaultHTTPClient.Do(request) if err != nil { return nil, err } @@ -69,8 +88,11 @@ func HTTPPostContext(ctx context.Context, uri string, data []byte, header map[st 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) { + if uriModifier != nil { + uri = uriModifier(uri) + } jsonBuf := new(bytes.Buffer) enc := json.NewEncoder(jsonBuf) enc.SetEscapeHTML(false) @@ -78,7 +100,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 := DefaultHTTPClient.Do(req) if err != nil { return nil, err } @@ -90,7 +117,12 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) { 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) @@ -100,7 +132,7 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e return nil, "", err } - response, err := http.Post(uri, "application/json;charset=utf-8", jsonBuf) + response, err := DefaultHTTPClient.Post(uri, "application/json;charset=utf-8", jsonBuf) if err != nil { return nil, "", err } @@ -136,6 +168,9 @@ type MultipartFormField struct { // PostMultipartForm 上传文件或其他多个字段 func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte, err error) { + if uriModifier != nil { + uri = uriModifier(uri) + } bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) @@ -173,7 +208,7 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte contentType := bodyWriter.FormDataContentType() bodyWriter.Close() - resp, e := http.Post(uri, contentType, bodyBuf) + resp, e := DefaultHTTPClient.Post(uri, contentType, bodyBuf) if e != nil { err = e return @@ -188,13 +223,16 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte // PostXML perform a HTTP/POST request with XML body func PostXML(uri string, obj interface{}) ([]byte, error) { + if uriModifier != nil { + uri = uriModifier(uri) + } xmlData, err := xml.Marshal(obj) if err != nil { return nil, err } body := bytes.NewBuffer(xmlData) - response, err := http.Post(uri, "application/xml;charset=utf-8", body) + response, err := DefaultHTTPClient.Post(uri, "application/xml;charset=utf-8", body) if err != nil { return nil, err } @@ -206,7 +244,7 @@ func PostXML(uri string, obj interface{}) ([]byte, error) { return io.ReadAll(response.Body) } -// httpWithTLS CA证书 +// httpWithTLS CA 证书 func httpWithTLS(rootCa, key string) (*http.Client, error) { var client *http.Client certData, err := os.ReadFile(rootCa) @@ -217,15 +255,14 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) { config := &tls.Config{ Certificates: []tls.Certificate{cert}, } - tr := &http.Transport{ - TLSClientConfig: config, - DisableCompression: true, - } - client = &http.Client{Transport: tr} + trans := (DefaultHTTPClient.Transport.(*http.Transport)).Clone() + trans.TLSClientConfig = config + trans.DisableCompression = true + client = &http.Client{Transport: trans} 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() { @@ -249,6 +286,9 @@ func pkcs12ToPem(p12 []byte, password string) tls.Certificate { // PostXMLWithTLS perform a HTTP/POST request with XML body and TLS func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { + if uriModifier != nil { + uri = uriModifier(uri) + } xmlData, err := xml.Marshal(obj) if err != nil { return nil, err 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/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/wechat.go b/wechat.go index fc98400..dc2585f 100644 --- a/wechat.go +++ b/wechat.go @@ -1,6 +1,7 @@ package wechat import ( + "net/http" "os" log "github.com/sirupsen/logrus" @@ -14,6 +15,7 @@ import ( openConfig "github.com/silenceper/wechat/v2/openplatform/config" "github.com/silenceper/wechat/v2/pay" payConfig "github.com/silenceper/wechat/v2/pay/config" + "github.com/silenceper/wechat/v2/util" "github.com/silenceper/wechat/v2/work" workConfig "github.com/silenceper/wechat/v2/work/config" ) @@ -40,7 +42,7 @@ func NewWechat() *Wechat { return &Wechat{} } -// SetCache 设置cache +// SetCache 设置 cache func (wc *Wechat) SetCache(cache cache.Cache) { wc.cache = cache } @@ -68,10 +70,21 @@ 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) } + +// SetHTTPClient 设置HTTPClient +func (wc *Wechat) SetHTTPClient(client *http.Client) { + util.DefaultHTTPClient = client +} diff --git a/work/addresslist/department.go b/work/addresslist/department.go index 79a7e46..b2feca0 100644 --- a/work/addresslist/department.go +++ b/work/addresslist/department.go @@ -7,11 +7,32 @@ import ( ) const ( - // DepartmentSimpleListURL 获取子部门ID列表 - DepartmentSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=%s&id=%d" + // 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" + departmentListByIDURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%d" + // departmentGetURL 获取单个部门详情 https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=ACCESS_TOKEN&id=ID + departmentGetURL = "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 @@ -23,8 +44,47 @@ type ( 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) { @@ -36,12 +96,67 @@ func (r *Client) DepartmentSimpleList(departmentID int) ([]*DepartmentID, error) return nil, err } var response []byte - if response, err = util.HTTPGet(fmt.Sprintf(DepartmentSimpleListURL, accessToken, departmentID)); err != nil { + if response, err = util.HTTPGet(fmt.Sprintf(departmentSimpleListURL, accessToken, departmentID)); err != nil { return nil, err } result := &DepartmentSimpleListResponse{} - if err = util.DecodeWithError(response, result, "DepartmentSimpleList"); err != nil { + 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) { + return r.DepartmentListByID(0) +} + +// DepartmentListByID 获取部门列表 +// +// departmentID 部门id。获取指定部门及其下的子部门(以及子部门的子部门等等,递归) +// +// @desc https://developer.work.weixin.qq.com/document/path/90208 +func (r *Client) DepartmentListByID(departmentID int) ([]*Department, error) { + var formatURL string + + // 获取accessToken + accessToken, err := r.GetAccessToken() + if err != nil { return nil, err } - return result.DepartmentID, nil + + if departmentID > 0 { + formatURL = fmt.Sprintf(departmentListByIDURL, accessToken, departmentID) + } else { + formatURL = fmt.Sprintf(departmentListURL, accessToken) + } + + // 发起http请求 + response, err := util.HTTPGet(formatURL) + 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 index 33e9fa1..61bb146 100644 --- a/work/addresslist/user.go +++ b/work/addresslist/user.go @@ -2,17 +2,26 @@ package addresslist import ( "fmt" + "strings" "github.com/silenceper/wechat/v2/util" ) const ( - // UserSimpleListURL 获取部门成员 - UserSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%d" - // UserGetURL 读取成员 - UserGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s" - // UserListIDURL 获取成员ID列表 - UserListIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token=%s" + // 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 ( @@ -41,15 +50,108 @@ func (r *Client) UserSimpleList(departmentID int) ([]*UserList, error) { return nil, err } var response []byte - if response, err = util.HTTPGet(fmt.Sprintf(UserSimpleListURL, accessToken, departmentID)); err != nil { + 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") - if err != nil { + 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 } - return result.UserList, nil + 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 获取部门成员响应 @@ -114,7 +216,7 @@ type UserGetResponse struct { } `json:"external_profile"` // 成员对外属性,字段详情见对外属性;代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 } -// UserGet 获取部门成员 +// UserGet 读取成员 // @see https://developer.work.weixin.qq.com/document/path/90196 func (r *Client) UserGet(UserID string) (*UserGetResponse, error) { var ( @@ -125,15 +227,52 @@ func (r *Client) UserGet(UserID string) (*UserGetResponse, error) { return nil, err } var response []byte - if response, err = util.HTTPGet(fmt.Sprintf(UserGetURL, accessToken, UserID)); err != nil { + + 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") - if err != nil { + 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 } - return result, nil + 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列表请求 @@ -166,12 +305,95 @@ func (r *Client) UserListID(req *UserListIDRequest) (*UserListIDResponse, error) return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(UserListIDURL, accessToken), req); err != nil { + 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{} - if err = util.DecodeWithError(response, result, "UserListID"); err != nil { - return nil, err - } - return result, nil + 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/checkin.go b/work/checkin/checkin.go new file mode 100644 index 0000000..3404852 --- /dev/null +++ b/work/checkin/checkin.go @@ -0,0 +1,387 @@ +package checkin + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // setScheduleListURL 为打卡人员排班 + setScheduleListURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/setcheckinschedulist?access_token=%s" + // punchCorrectionURL 为打卡人员补卡 + punchCorrectionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/punch_correction?access_token=%s" + // addUserFaceURL 录入打卡人员人脸信息 + addUserFaceURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/addcheckinuserface?access_token=%s" + // addOptionURL 创建打卡规则 + addOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/add_checkin_option?access_token=%s" + // updateOptionURL 修改打卡规则 + updateOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/update_checkin_option?access_token=%s" + // clearOptionURL 清空打卡规则数组元素 + clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s" + // delOptionURL 删除打卡规则 + delOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/del_checkin_option?access_token=%s" +) + +// SetScheduleListRequest 为打卡人员排班请求 +type SetScheduleListRequest struct { + GroupID int64 `json:"groupid"` + Items []SetScheduleListItem `json:"items"` + YearMonth int64 `json:"yearmonth"` +} + +// SetScheduleListItem 排班表信息 +type SetScheduleListItem struct { + UserID string `json:"userid"` + Day int64 `json:"day"` + ScheduleID int64 `json:"schedule_id"` +} + +// SetScheduleList 为打卡人员排班 +// see https://developer.work.weixin.qq.com/document/path/93385 +func (r *Client) SetScheduleList(req *SetScheduleListRequest) 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(setScheduleListURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "SetScheduleList") +} + +// PunchCorrectionRequest 为打卡人员补卡请求 +type PunchCorrectionRequest struct { + UserID string `json:"userid"` + ScheduleDateTime int64 `json:"schedule_date_time"` + ScheduleCheckinTime int64 `json:"schedule_checkin_time"` + CheckinTime int64 `json:"checkin_time"` + Remark string `json:"remark"` +} + +// PunchCorrection 为打卡人员补卡 +// see https://developer.work.weixin.qq.com/document/path/95803 +func (r *Client) PunchCorrection(req *PunchCorrectionRequest) 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(punchCorrectionURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "PunchCorrection") +} + +// AddUserFaceRequest 录入打卡人员人脸信息请求 +type AddUserFaceRequest struct { + UserID string `json:"userid"` + UserFace string `json:"userface"` +} + +// AddUserFace 录入打卡人员人脸信息 +// see https://developer.work.weixin.qq.com/document/path/93378 +func (r *Client) AddUserFace(req *AddUserFaceRequest) 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(addUserFaceURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "AddUserFace") +} + +// AddOptionRequest 创建打卡规则请求 +type AddOptionRequest struct { + EffectiveNow bool `json:"effective_now,omitempty"` + Group OptionGroupRule `json:"group,omitempty"` +} + +// OptionGroupRule 打卡规则字段 +type OptionGroupRule struct { + GroupID int64 `json:"groupid,omitempty"` + GroupType int64 `json:"grouptype"` + GroupName string `json:"groupname"` + CheckinDate []OptionGroupRuleCheckinDate `json:"checkindate,omitempty"` + SpeWorkdays []OptionGroupSpeWorkdays `json:"spe_workdays,omitempty"` + SpeOffDays []OptionGroupSpeOffDays `json:"spe_offdays,omitempty"` + SyncHolidays bool `json:"sync_holidays,omitempty"` + NeedPhoto bool `json:"need_photo,omitempty"` + NoteCanUseLocalPic bool `json:"note_can_use_local_pic,omitempty"` + WifiMacInfos []OptionGroupWifiMacInfos `json:"wifimac_infos,omitempty"` + LocInfos []OptionGroupLocInfos `json:"loc_infos,omitempty"` + AllowCheckinOffWorkday bool `json:"allow_checkin_offworkday,omitempty"` + AllowApplyOffWorkday bool `json:"allow_apply_offworkday,omitempty"` + Range []OptionGroupRange `json:"range"` + WhiteUsers []string `json:"white_users,omitempty"` + Type int64 `json:"type,omitempty"` + ReporterInfo OptionGroupReporterInfo `json:"reporterinfo,omitempty"` + AllowApplyBkCnt int64 `json:"allow_apply_bk_cnt,omitempty"` + AllowApplyBkDayLimit int64 `json:"allow_apply_bk_day_limit,omitempty"` + BukaLimitNextMonth int64 `json:"buka_limit_next_month,omitempty"` + OptionOutRange int64 `json:"option_out_range,omitempty"` + ScheduleList []OptionGroupScheduleList `json:"schedulelist,omitempty"` + OffWorkIntervalTime int64 `json:"offwork_interval_time,omitempty"` + UseFaceDetect bool `json:"use_face_detect,omitempty"` + OpenFaceLiveDetect bool `json:"open_face_live_detect,omitempty"` + OtInfoV2 OptionGroupOtInfoV2 `json:"ot_info_v2,omitempty"` + SyncOutCheckin bool `json:"sync_out_checkin,omitempty"` + BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"` + BukaRestriction int64 `json:"buka_restriction,omitempty"` + SpanDayTime int64 `json:"span_day_time,omitempty"` + StandardWorkDuration int64 `json:"standard_work_duration,omitempty"` +} + +// OptionGroupRuleCheckinDate 固定时间上下班打卡时间 +type OptionGroupRuleCheckinDate struct { + Workdays []int64 `json:"workdays"` + CheckinTime []OptionGroupRuleCheckinTime `json:"checkintime"` + FlexTime int64 `json:"flex_time"` + AllowFlex bool `json:"allow_flex"` + FlexOnDutyTime int64 `json:"flex_on_duty_time"` + FlexOffDutyTime int64 `json:"flex_off_duty_time"` + MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"` + MaxAllowArriveLate int64 `json:"max_allow_arrive_late"` + LateRule OptionGroupLateRule `json:"late_rule"` +} + +// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息 +type OptionGroupRuleCheckinTime struct { + TimeID int64 `json:"time_id"` + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + RemindWorkSec int64 `json:"remind_work_sec"` + RemindOffWorkSec int64 `json:"remind_off_work_sec"` + AllowRest bool `json:"allow_rest"` + RestBeginTime int64 `json:"rest_begin_time"` + RestEndTime int64 `json:"rest_end_time"` + EarliestWorkSec int64 `json:"earliest_work_sec"` + LatestWorkSec int64 `json:"latest_work_sec"` + EarliestOffWorkSec int64 `json:"earliest_off_work_sec"` + LatestOffWorkSec int64 `json:"latest_off_work_sec"` + NoNeedCheckOn bool `json:"no_need_checkon"` + NoNeedCheckOff bool `json:"no_need_checkoff"` +} + +// OptionGroupLateRule 晚走晚到时间规则信息 +type OptionGroupLateRule struct { + OffWorkAfterTime int64 `json:"offwork_after_time"` + OnWorkFlexTime int64 `json:"onwork_flex_time"` + AllowOffWorkAfterTime int64 `json:"allow_offwork_after_time"` + TimeRules []OptionGroupTimeRule `json:"timerules"` +} + +// OptionGroupTimeRule 晚走晚到时间规则 +type OptionGroupTimeRule struct { + OffWorkAfterTime int64 `json:"offwork_after_time"` + OnWorkFlexTime int64 `json:"onwork_flex_time"` +} + +// OptionGroupSpeWorkdays 特殊工作日 +type OptionGroupSpeWorkdays struct { + Timestamp int64 `json:"timestamp"` + Notes string `json:"notes"` + CheckinTime []OptionGroupCheckinTime `json:"checkintime"` + Type int64 `json:"type"` + BegTime int64 `json:"begtime"` + EndTime int64 `json:"endtime"` +} + +// OptionGroupCheckinTime 特殊工作日的上下班打卡时间配置 +type OptionGroupCheckinTime struct { + TimeID int64 `json:"time_id"` + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + RemindWorkSec int64 `json:"remind_work_sec"` + RemindOffWorkSec int64 `json:"remind_off_work_sec"` +} + +// OptionGroupSpeOffDays 特殊非工作日 +type OptionGroupSpeOffDays struct { + Timestamp int64 `json:"timestamp"` + Notes string `json:"notes"` + Type int64 `json:"type"` + BegTime int64 `json:"begtime"` + EndTime int64 `json:"endtime"` +} + +// OptionGroupWifiMacInfos WIFI信息 +type OptionGroupWifiMacInfos struct { + WifiName string `json:"wifiname"` + WifiMac string `json:"wifimac"` +} + +// OptionGroupLocInfos 地点信息 +type OptionGroupLocInfos struct { + Lat int64 `json:"lat"` + Lng int64 `json:"lng"` + LocTitle string `json:"loc_title"` + LocDetail string `json:"loc_detail"` + Distance int64 `json:"distance"` +} + +// OptionGroupRange 人员信息 +type OptionGroupRange struct { + PartyID []string `json:"party_id"` + UserID []string `json:"userid"` + TagID []int64 `json:"tagid"` +} + +// OptionGroupReporterInfo 汇报人 +type OptionGroupReporterInfo struct { + Reporters []OptionGroupReporters `json:"reporters"` +} + +// OptionGroupReporters 汇报对象 +type OptionGroupReporters struct { + UserID string `json:"userid"` + TagID int64 `json:"tagid"` +} + +// OptionGroupScheduleList 自定义排班规则所有排班 +type OptionGroupScheduleList struct { + ScheduleID int64 `json:"schedule_id"` + ScheduleName string `json:"schedule_name"` + TimeSection []OptionGroupTimeSection `json:"time_section"` + AllowFlex bool `json:"allow_flex"` + FlexOnDutyTime int64 `json:"flex_on_duty_time"` + FlexOffDutyTime int64 `json:"flex_off_duty_time"` + LateRule OptionGroupLateRule `json:"late_rule"` + MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"` + MaxAllowArriveLate int64 `json:"max_allow_arrive_late"` +} + +// OptionGroupTimeSection 班次上下班时段信息 +type OptionGroupTimeSection struct { + TimeID int64 `json:"time_id"` + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + RemindWorkSec int64 `json:"remind_work_sec"` + RemindOffWorkSec int64 `json:"remind_off_work_sec"` + RestBeginTime int64 `json:"rest_begin_time"` + RestEndTime int64 `json:"rest_end_time"` + AllowRest bool `json:"allow_rest"` + EarliestWorkSec int64 `json:"earliest_work_sec"` + LatestWorkSec int64 `json:"latest_work_sec"` + EarliestOffWorkSec int64 `json:"earliest_off_work_sec"` + LatestOffWorkSec int64 `json:"latest_off_work_sec"` + NoNeedCheckOn bool `json:"no_need_checkon"` + NoNeedCheckOff bool `json:"no_need_checkoff"` +} + +// OptionGroupOtInfoV2 加班配置 +type OptionGroupOtInfoV2 struct { + WorkdayConf OptionGroupWorkdayConf `json:"workdayconf"` +} + +// OptionGroupWorkdayConf 工作日加班配置 +type OptionGroupWorkdayConf struct { + AllowOt bool `json:"allow_ot"` + Type int64 `json:"type"` +} + +// OptionGroupBukaRemind 补卡提醒 +type OptionGroupBukaRemind struct { + OpenRemind bool `json:"open_remind"` + BukaRemindDay int64 `json:"buka_remind_day"` + BukaRemindMonth int64 `json:"buka_remind_month"` +} + +// AddOption 创建打卡规则 +// see https://developer.work.weixin.qq.com/document/path/98041#%E5%88%9B%E5%BB%BA%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99 +func (r *Client) AddOption(req *AddOptionRequest) 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(addOptionURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "AddOption") +} + +// UpdateOptionRequest 修改打卡规则请求 +type UpdateOptionRequest struct { + EffectiveNow bool `json:"effective_now,omitempty"` + Group OptionGroupRule `json:"group,omitempty"` +} + +// UpdateOption 修改打卡规则 +// see https://developer.work.weixin.qq.com/document/path/98041#%E4%BF%AE%E6%94%B9%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99 +func (r *Client) UpdateOption(req *UpdateOptionRequest) 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(updateOptionURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "UpdateOption") +} + +// ClearOptionRequest 清空打卡规则数组元素请求 +type ClearOptionRequest struct { + GroupID int64 `json:"groupid"` + ClearField []int64 `json:"clear_field"` + EffectiveNow bool `json:"effective_now"` +} + +// ClearOption 清空打卡规则数组元素 +// see https://developer.work.weixin.qq.com/document/path/98041#%E6%B8%85%E7%A9%BA%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0 +func (r *Client) ClearOption(req *ClearOptionRequest) 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(clearOptionURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "ClearOption") +} + +// DelOptionRequest 删除打卡规则请求 +type DelOptionRequest struct { + GroupID int64 `json:"groupid"` + EffectiveNow bool `json:"effective_now"` +} + +// DelOption 删除打卡规则 +// see https://developer.work.weixin.qq.com/document/path/98041#%E5%88%A0%E9%99%A4%E6%89%93%E5%8D%A1%E8%A7%84%E5%88%99 +func (r *Client) DelOption(req *DelOptionRequest) 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(delOptionURL, accessToken), req); err != nil { + return err + } + return util.DecodeWithCommonError(response, "DelOption") +} 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..18e97fa --- /dev/null +++ b/work/checkin/record.go @@ -0,0 +1,660 @@ +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" + // getCorpOptionURL 获取企业所有打卡规则 + getCorpOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcorpcheckinoption?access_token=%s" + // getOptionURL 获取员工打卡规则 + getOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckinoption?access_token=%s" + // getScheduleListURL 获取打卡人员排班信息 + getScheduleListURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckinschedulist?access_token=%s" + // getHardwareDataURL获取设备打卡数据 + getHardwareDataURL = "https://qyapi.weixin.qq.com/cgi-bin/hardware/get_hardware_checkin_data?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 +} + +// GetCorpOptionResponse 获取企业所有打卡规则响应 +type GetCorpOptionResponse struct { + util.CommonError + Group []CorpOptionGroup `json:"group"` +} + +// CorpOptionGroup 企业规则信息列表 +type CorpOptionGroup struct { + GroupType int64 `json:"grouptype"` + GroupID int64 `json:"groupid"` + GroupName string `json:"groupname"` + CheckinDate []GroupCheckinDate `json:"checkindate"` + SpeWorkdays []SpeWorkdays `json:"spe_workdays"` + SpeOffDays []SpeOffDays `json:"spe_offdays"` + SyncHolidays bool `json:"sync_holidays"` + NeedPhoto bool `json:"need_photo"` + NoteCanUseLocalPic bool `json:"note_can_use_local_pic"` + AllowCheckinOffWorkday bool `json:"allow_checkin_offworkday"` + AllowApplyOffWorkday bool `json:"allow_apply_offworkday"` + WifiMacInfos []WifiMacInfos `json:"wifimac_infos"` + LocInfos []LocInfos `json:"loc_infos"` + Range []Range `json:"range"` + CreateTime int64 `json:"create_time"` + WhiteUsers []string `json:"white_users"` + Type int64 `json:"type"` + ReporterInfo ReporterInfo `json:"reporterinfo"` + OtInfo GroupOtInfo `json:"ot_info"` + OtApplyInfo OtApplyInfo `json:"otapplyinfo"` + Uptime int64 `json:"uptime"` + AllowApplyBkCnt int64 `json:"allow_apply_bk_cnt"` + OptionOutRange int64 `json:"option_out_range"` + CreateUserID string `json:"create_userid"` + UseFaceDetect bool `json:"use_face_detect"` + AllowApplyBkDayLimit int64 `json:"allow_apply_bk_day_limit"` + UpdateUserID string `json:"update_userid"` + BukaRestriction int64 `json:"buka_restriction"` + ScheduleList []ScheduleList `json:"schedulelist"` + OffWorkIntervalTime int64 `json:"offwork_interval_time"` +} + +// GroupCheckinDate 打卡时间,当规则类型为排班时没有意义 +type GroupCheckinDate struct { + Workdays []int64 `json:"workdays"` + CheckinTime []GroupCheckinTime `json:"checkintime"` + NoNeedOffWork bool `json:"noneed_offwork"` + LimitAheadTime int64 `json:"limit_aheadtime"` + FlexOnDutyTime int64 `json:"flex_on_duty_time"` + FlexOffDutyTime int64 `json:"flex_off_duty_time"` +} + +// GroupCheckinTime 工作日上下班打卡时间信息 +type GroupCheckinTime struct { + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + RemindWorkSec int64 `json:"remind_work_sec"` + RemindOffWorkSec int64 `json:"remind_off_work_sec"` +} + +// SpeWorkdays 特殊日期-必须打卡日期信息 +type SpeWorkdays struct { + Timestamp int64 `json:"timestamp"` + Notes string `json:"notes"` + CheckinTime []GroupCheckinTime `json:"checkintime"` +} + +// SpeOffDays 特殊日期-不用打卡日期信息 +type SpeOffDays struct { + Timestamp int64 `json:"timestamp"` + Notes string `json:"notes"` +} + +// WifiMacInfos 打卡地点-WiFi打卡信息 +type WifiMacInfos struct { + WifiName string `json:"wifiname"` + WifiMac string `json:"wifimac"` +} + +// LocInfos 打卡地点-位置打卡信息 +type LocInfos struct { + Lat int64 `json:"lat"` + Lng int64 `json:"lng"` + LocTitle string `json:"loc_title"` + LocDetail string `json:"loc_detail"` + Distance int64 `json:"distance"` +} + +// Range 打卡人员信息 +type Range struct { + PartyID []string `json:"partyid"` + UserID []string `json:"userid"` + TagID []int64 `json:"tagid"` +} + +// ReporterInfo 汇报对象信息 +type ReporterInfo struct { + Reporters []Reporters `json:"reporters"` + UpdateTime int64 `json:"updatetime"` +} + +// Reporters 汇报对象,每个汇报人用userid表示 +type Reporters struct { + UserID string `json:"userid"` +} + +// GroupOtInfo 加班信息 +type GroupOtInfo struct { + Type int64 `json:"type"` + AllowOtWorkingDay bool `json:"allow_ot_workingday"` + AllowOtNonWorkingDay bool `json:"allow_ot_nonworkingday"` + OtCheckInfo OtCheckInfo `json:"otcheckinfo"` +} + +// OtCheckInfo 以打卡时间为准-加班时长计算规则信息 +type OtCheckInfo struct { + OtWorkingDayTimeStart int64 `json:"ot_workingday_time_start"` + OtWorkingDayTimeMin int64 `json:"ot_workingday_time_min"` + OtWorkingDayTimeMax int64 `json:"ot_workingday_time_max"` + OtNonworkingDayTimeMin int64 `json:"ot_nonworkingday_time_min"` + OtNonworkingDayTimeMax int64 `json:"ot_nonworkingday_time_max"` + OtNonworkingDaySpanDayTime int64 `json:"ot_nonworkingday_spanday_time"` + OtWorkingDayRestInfo OtRestInfo `json:"ot_workingday_restinfo"` + OtNonWorkingDayRestInfo OtRestInfo `json:"ot_nonworkingday_restinfo"` +} + +// OtRestInfo 加班-休息扣除配置信息 +type OtRestInfo struct { + Type int64 `json:"type"` + FixTimeRule FixTimeRule `json:"fix_time_rule"` + CalOtTimeRule CalOtTimeRule `json:"cal_ottime_rule"` +} + +// FixTimeRule 工作日加班-指定休息时间配置信息 +type FixTimeRule struct { + FixTimeBeginSec int64 `json:"fix_time_begin_sec"` + FixTimeEndSec int64 `json:"fix_time_end_sec"` +} + +// CalOtTimeRule 工作日加班-按加班时长扣除配置信息 +type CalOtTimeRule struct { + Items []CalOtTimeRuleItem `json:"items"` +} + +// CalOtTimeRuleItem 工作日加班-按加班时长扣除条件信息 +type CalOtTimeRuleItem struct { + OtTime int64 `json:"ot_time"` + RestTime int64 `json:"rest_time"` +} + +// OtApplyInfo 以加班申请核算打卡记录相关信息 +type OtApplyInfo struct { + AllowOtWorkingDay bool `json:"allow_ot_workingday"` + AllowOtNonWorkingDay bool `json:"allow_ot_nonworkingday"` + Uiptime int64 `json:"uptime"` + OtNonworkingDaySpanDayTime int64 `json:"ot_nonworkingday_spanday_time"` + OtWorkingDayRestInfo OtRestInfo `json:"ot_workingday_restinfo"` + OtNonWorkingDayRestInfo OtRestInfo `json:"ot_nonworkingday_restinfo"` +} + +// ScheduleList 排班信息列表 +type ScheduleList struct { + ScheduleID int64 `json:"schedule_id"` + ScheduleName string `json:"schedule_name"` + TimeSection []TimeSection `json:"time_section"` + LimitAheadTime int64 `json:"limit_aheadtime"` + NoNeedOffWork bool `json:"noneed_offwork"` + LimitOffTime int64 `json:"limit_offtime"` + FlexOnDutyTime int64 `json:"flex_on_duty_time"` + FlexOffDutyTime int64 `json:"flex_off_duty_time"` + AllowFlex bool `json:"allow_flex"` + LateRule LateRule `json:"late_rule"` + MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"` + MaxAllowArriveLate int64 `json:"max_allow_arrive_late"` +} + +// TimeSection 班次上下班时段信息 +type TimeSection struct { + TimeID int64 `json:"time_id"` + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + RemindWorkSec int64 `json:"remind_work_sec"` + RemindOffWorkSec int64 `json:"remind_off_work_sec"` + RestBeginTime int64 `json:"rest_begin_time"` + RestEndTime int64 `json:"rest_end_time"` + AllowRest bool `json:"allow_rest"` +} + +// LateRule 晚走晚到时间规则信息 +type LateRule struct { + AllowOffWorkAfterTime bool `json:"allow_offwork_after_time"` + TimeRules []TimeRule `json:"timerules"` +} + +// TimeRule 迟到规则时间 +type TimeRule struct { + OffWorkAfterTime int64 `json:"offwork_after_time"` + OnWorkFlexTime int64 `json:"onwork_flex_time"` +} + +// GetCorpOption 获取企业所有打卡规则 +// @see https://developer.work.weixin.qq.com/document/path/93384 +func (r *Client) GetCorpOption() (*GetCorpOptionResponse, 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(getCorpOptionURL, accessToken), ""); err != nil { + return nil, err + } + result := &GetCorpOptionResponse{} + err = util.DecodeWithError(response, result, "GetCorpOption") + return result, err +} + +// GetOptionRequest 获取员工打卡规则请求 +type GetOptionRequest struct { + Datetime int64 `json:"datetime"` + UserIDList []string `json:"useridlist"` +} + +// GetOptionResponse 获取员工打卡规则响应 +type GetOptionResponse struct { + util.CommonError + Info []OptionInfo `json:"info"` +} + +// OptionInfo 打卡规则列表 +type OptionInfo struct { + UserID string `json:"userid"` + Group OptionGroup `json:"group"` +} + +// OptionGroup 打卡规则相关信息 +type OptionGroup struct { + GroupType int64 `json:"grouptype"` + GroupID int64 `json:"groupid"` + GroupName string `json:"groupname"` + CheckinDate []OptionCheckinDate `json:"checkindate"` + SpeWorkdays []SpeWorkdays `json:"spe_workdays"` + SpeOffDays []SpeOffDays `json:"spe_offdays"` + SyncHolidays bool `json:"sync_holidays"` + NeedPhoto bool `json:"need_photo"` + WifiMacInfos []WifiMacInfos `json:"wifimac_infos"` + NoteCanUseLocalPic bool `json:"note_can_use_local_pic"` + AllowCheckinOffWorkday bool `json:"allow_checkin_offworkday"` + AllowApplyOffWorkday bool `json:"allow_apply_offworkday"` + LocInfos []LocInfos `json:"loc_infos"` + ScheduleList []ScheduleList `json:"schedulelist"` + BukaRestriction int64 `json:"buka_restriction"` +} + +// OptionCheckinDate 打卡时间配置 +type OptionCheckinDate struct { + Workdays []int64 `json:"workdays"` + CheckinTime []GroupCheckinTime `json:"checkintime"` + FlexTime int64 `json:"flex_time"` + NoNeedOffWork bool `json:"noneed_offwork"` + LimitAheadTime int64 `json:"limit_aheadtime"` + FlexOnDutyTime int64 `json:"flex_on_duty_time"` + FlexOffDutyTime int64 `json:"flex_off_duty_time"` +} + +// GetOption 获取员工打卡规则 +// see https://developer.work.weixin.qq.com/document/path/90263 +func (r *Client) GetOption(req *GetOptionRequest) (*GetOptionResponse, 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(getOptionURL, accessToken), req); err != nil { + return nil, err + } + result := &GetOptionResponse{} + err = util.DecodeWithError(response, result, "GetOption") + return result, err +} + +// GetScheduleListRequest 获取打卡人员排班信息请求 +type GetScheduleListRequest struct { + StartTime int64 `json:"starttime"` + EndTime int64 `json:"endtime"` + UserIDList []string `json:"useridlist"` +} + +// GetScheduleListResponse 获取打卡人员排班信息响应 +type GetScheduleListResponse struct { + util.CommonError + ScheduleList []ScheduleItem `json:"schedule_list"` +} + +// ScheduleItem 排班表信息 +type ScheduleItem struct { + UserID string `json:"userid"` + YearMonth int64 `json:"yearmonth"` + GroupID int64 `json:"groupid"` + GroupName string `json:"groupname"` + Schedule Schedule `json:"schedule"` +} + +// Schedule 个人排班信息 +type Schedule struct { + ScheduleList []ScheduleListItem `json:"scheduleList"` +} + +// ScheduleListItem 个人排班表信息 +type ScheduleListItem struct { + Day int64 `json:"day"` + ScheduleInfo ScheduleInfo `json:"schedule_info"` +} + +// ScheduleInfo 个人当日排班信息 +type ScheduleInfo struct { + ScheduleID int64 `json:"schedule_id"` + ScheduleName string `json:"schedule_name"` + TimeSection []ScheduleTimeSection `json:"time_section"` +} + +// ScheduleTimeSection 班次上下班时段信息 +type ScheduleTimeSection struct { + ID int64 `json:"id"` + WorkSec int64 `json:"work_sec"` + OffWorkSec int64 `json:"off_work_sec"` + RemindWorkSec int64 `json:"remind_work_sec"` + RemindOffWorkSec int64 `json:"remind_off_work_sec"` +} + +// GetScheduleList 获取打卡人员排班信息 +// see https://developer.work.weixin.qq.com/document/path/93380 +func (r *Client) GetScheduleList(req *GetScheduleListRequest) (*GetScheduleListResponse, 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(getScheduleListURL, accessToken), req); err != nil { + return nil, err + } + result := &GetScheduleListResponse{} + err = util.DecodeWithError(response, result, "GetScheduleList") + return result, err +} + +// GetHardwareDataRequest 获取设备打卡数据请求 +type GetHardwareDataRequest struct { + FilterType int64 `json:"filter_type"` + StartTime int64 `json:"starttime"` + EndTime int64 `json:"endtime"` + UserIDList []string `json:"useridlist"` +} + +// GetHardwareDataResponse 获取设备打卡数据响应 +type GetHardwareDataResponse struct { + util.CommonError + CheckinData []HardwareCheckinData `json:"checkindata"` +} + +// HardwareCheckinData 设备打卡数据 +type HardwareCheckinData struct { + UserID string `json:"userid"` + CheckinTime int64 `json:"checkin_time"` + DeviceSn string `json:"device_sn"` + DeviceName string `json:"device_name"` +} + +// GetHardwareData 获取设备打卡数据 +// see https://developer.work.weixin.qq.com/document/path/94126 +func (r *Client) GetHardwareData(req *GetHardwareDataRequest) (*GetHardwareDataResponse, 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(getHardwareDataURL, accessToken), req); err != nil { + return nil, err + } + result := &GetHardwareDataResponse{} + err = util.DecodeWithError(response, result, "GetHardwareData") + return result, err +} diff --git a/work/externalcontact/callback.go b/work/externalcontact/callback.go index f570340..14aa1c9 100644 --- a/work/externalcontact/callback.go +++ b/work/externalcontact/callback.go @@ -38,8 +38,6 @@ func (r *Client) GetCallbackMessage(encryptedMsg []byte) (msg EventCallbackMessa if err != nil { return } - if err = xml.Unmarshal(bData, &msg); err != nil { - return - } + err = xml.Unmarshal(bData, &msg) return } diff --git a/work/externalcontact/contact_way.go b/work/externalcontact/contact_way.go index 5afdc19..6420006 100644 --- a/work/externalcontact/contact_way.go +++ b/work/externalcontact/contact_way.go @@ -7,16 +7,16 @@ import ( ) 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" + // 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 ( @@ -98,14 +98,12 @@ func (r *Client) AddContactWay(req *AddContactWayRequest) (*AddContactWayRespons return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(AddContactWayURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(addContactWayURL, accessToken), req); err != nil { return nil, err } result := &AddContactWayResponse{} - if err = util.DecodeWithError(response, result, "AddContactWay"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "AddContactWay") + return result, err } type ( @@ -149,14 +147,12 @@ func (r *Client) GetContactWay(req *GetContactWayRequest) (*GetContactWayRespons return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(GetContactWayURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(getContactWayURL, accessToken), req); err != nil { return nil, err } result := &GetContactWayResponse{} - if err = util.DecodeWithError(response, result, "GetContactWay"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetContactWay") + return result, err } type ( @@ -191,14 +187,12 @@ func (r *Client) UpdateContactWay(req *UpdateContactWayRequest) (*UpdateContactW return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(UpdateContactWayURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(updateContactWayURL, accessToken), req); err != nil { return nil, err } result := &UpdateContactWayResponse{} - if err = util.DecodeWithError(response, result, "UpdateContactWay"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "UpdateContactWay") + return result, err } type ( @@ -232,14 +226,12 @@ func (r *Client) ListContactWay(req *ListContactWayRequest) (*ListContactWayResp return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(ListContactWayURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(listContactWayURL, accessToken), req); err != nil { return nil, err } result := &ListContactWayResponse{} - if err = util.DecodeWithError(response, result, "ListContactWay"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "ListContactWay") + return result, err } type ( @@ -264,12 +256,10 @@ func (r *Client) DelContactWay(req *DelContactWayRequest) (*DelContactWayRespons return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(DelContactWayURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(delContactWayURL, accessToken), req); err != nil { return nil, err } result := &DelContactWayResponse{} - if err = util.DecodeWithError(response, result, "DelContactWay"); err != nil { - return nil, err - } - return result, nil + 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 index acbc013..9ee219e 100644 --- a/work/externalcontact/external_user.go +++ b/work/externalcontact/external_user.go @@ -8,14 +8,26 @@ import ( ) 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" + // 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 外部联系人列表响应 @@ -32,16 +44,13 @@ func (r *Client) GetExternalUserList(userID string) ([]string, error) { return nil, err } var response []byte - response, err = util.HTTPGet(fmt.Sprintf("%s?access_token=%v&userid=%v", FetchExternalContactUserListURL, accessToken, userID)) + 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") - if err != nil { - return nil, err - } - return result.ExternalUserID, nil + return result.ExternalUserID, err } // ExternalUserDetailResponse 外部联系人详情响应 @@ -54,16 +63,16 @@ type ExternalUserDetailResponse struct { // 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"` + 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 *ExternalProfile `json:"external_profile,omitempty"` } // FollowUser 跟进用户(指企业内部用户) @@ -71,7 +80,7 @@ type FollowUser struct { UserID string `json:"userid"` Remark string `json:"remark"` Description string `json:"description"` - CreateTime string `json:"create_time"` + CreateTime int64 `json:"createtime"` Tags []Tag `json:"tags"` RemarkCorpName string `json:"remark_corp_name"` RemarkMobiles []string `json:"remark_mobiles"` @@ -92,7 +101,47 @@ type Tag struct { // WechatChannel 视频号添加的场景 type WechatChannel struct { NickName string `json:"nickname"` - Source string `json:"source"` + Source int `json:"source"` +} + +// ExternalProfile 外部联系人的自定义展示信息,可以有多个字段和多种类型,包括文本,网页和小程序 +type ExternalProfile struct { + ExternalCorpName string `json:"external_corp_name"` + WechatChannels WechatChannels `json:"wechat_channels"` + ExternalAttr []ExternalAttr `json:"external_attr"` +} + +// WechatChannels 视频号属性。须从企业绑定到企业微信的视频号中选择,可在“我的企业”页中查看绑定的视频号 +type WechatChannels struct { + Nickname string `json:"nickname"` + Status int `json:"status"` +} + +// ExternalAttr 属性列表,目前支持文本、网页、小程序三种类型 +type ExternalAttr struct { + Type int `json:"type"` + Name string `json:"name"` + Text *Text `json:"text,omitempty"` + Web *Web `json:"web,omitempty"` + MiniProgram *MiniProgram `json:"miniprogram,omitempty"` +} + +// Text 文本 +type Text struct { + Value string `json:"value"` +} + +// Web 网页 +type Web struct { + URL string `json:"url"` + Title string `json:"title"` +} + +// MiniProgram 小程序 +type MiniProgram struct { + AppID string `json:"appid"` + Pagepath string `json:"pagepath"` + Title string `json:"title"` } // GetExternalUserDetail 获取外部联系人详情 @@ -107,22 +156,20 @@ func (r *Client) GetExternalUserDetail(externalUserID string, nextCursor ...stri 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)) + 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") - if err != nil { - return nil, err - } - return result, nil + return result, err } // BatchGetExternalUserDetailsRequest 批量获取外部联系人详情请求 type BatchGetExternalUserDetailsRequest struct { UserIDList []string `json:"userid_list"` Cursor string `json:"cursor"` + Limit int `json:"limit,omitempty"` } // ExternalUserDetailListResponse 批量获取外部联系人详情响应 @@ -156,7 +203,7 @@ type FollowInfo struct { UserID string `json:"userid"` Remark string `json:"remark"` Description string `json:"description"` - CreateTime int `json:"create_time"` + CreateTime int64 `json:"createtime"` TagID []string `json:"tag_id"` RemarkCorpName string `json:"remark_corp_name"` RemarkMobiles []string `json:"remark_mobiles"` @@ -177,16 +224,13 @@ func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetails if err != nil { return nil, err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", FetchBatchExternalContactUserDetailURL, accessToken), string(jsonData)) + 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") - if err != nil { - return nil, err - } - return result.ExternalContactList, nil + return result.ExternalContactList, err } // UpdateUserRemarkRequest 修改客户备注信息请求体 @@ -212,9 +256,234 @@ func (r *Client) UpdateUserRemark(request UpdateUserRemarkRequest) error { if err != nil { return err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", UpdateUserRemarkURL, accessToken), string(jsonData)) + 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 index 1cfc361..43d9c49 100644 --- a/work/externalcontact/follow_user.go +++ b/work/externalcontact/follow_user.go @@ -7,8 +7,8 @@ import ( ) const ( - // FetchFollowUserListURL 获取配置了客户联系功能的成员列表 - FetchFollowUserListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list" + // fetchFollowUserListURL 获取配置了客户联系功能的成员列表 + fetchFollowUserListURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list" ) // followerUserResponse 客户联系功能的成员列表响应 @@ -25,14 +25,11 @@ func (r *Client) GetFollowUserList() ([]string, error) { return nil, err } var response []byte - response, err = util.HTTPGet(fmt.Sprintf("%s?access_token=%s", FetchFollowUserListURL, accessToken)) + 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") - if err != nil { - return nil, err - } - return result.FollowUser, nil + return result.FollowUser, err } diff --git a/work/externalcontact/groupchat.go b/work/externalcontact/groupchat.go index 0aa9ccd..3dc800f 100644 --- a/work/externalcontact/groupchat.go +++ b/work/externalcontact/groupchat.go @@ -6,8 +6,8 @@ import ( "github.com/silenceper/wechat/v2/util" ) -// OpengIDToChatIDURL 客户群opengid转换URL -const OpengIDToChatIDURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/opengid_to_chatid" +// opengIDToChatIDURL 客户群opengid转换URL +const opengIDToChatIDURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/opengid_to_chatid" type ( //GroupChatListRequest 获取客户群列表的请求参数 @@ -39,15 +39,13 @@ func (r *Client) GetGroupChatList(req *GroupChatListRequest) (*GroupChatListResp return nil, err } var response []byte - response, err = util.PostJSON(fmt.Sprintf("%s/list?access_token=%s", GroupChatURL, accessToken), req) + response, err = util.PostJSON(fmt.Sprintf("%s/list?access_token=%s", groupChatURL, accessToken), req) if err != nil { return nil, err } result := &GroupChatListResponse{} - if err = util.DecodeWithError(response, result, "GetGroupChatList"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupChatList") + return result, err } type ( @@ -70,6 +68,7 @@ type ( 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 { @@ -77,13 +76,14 @@ type ( } //GroupChat 客户群详情 GroupChat struct { - ChatID string `json:"chat_id"` //客户群ID - Name string `json:"name"` //群名 - Owner string `json:"owner"` //群主ID - CreateTime int `json:"create_time"` //群的创建时间 - Notice string `json:"notice"` //群公告 - MemberList []GroupChatMember `json:"member_list"` //群成员列表 - AdminList []GroupChatAdmin `json:"admin_list"` //群管理员列表 + 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"` //群管理员列表 + MemberVersion string `json:"member_version"` //当前群成员版本号。可以配合客户群变更事件减少主动调用本接口的次数 } //GroupChatDetailResponse 客户群详情 返回值 GroupChatDetailResponse struct { @@ -100,15 +100,13 @@ func (r *Client) GetGroupChatDetail(req *GroupChatDetailRequest) (*GroupChatDeta return nil, err } var response []byte - response, err = util.PostJSON(fmt.Sprintf("%s/get?access_token=%s", GroupChatURL, accessToken), req) + response, err = util.PostJSON(fmt.Sprintf("%s/get?access_token=%s", groupChatURL, accessToken), req) if err != nil { return nil, err } result := &GroupChatDetailResponse{} - if err = util.DecodeWithError(response, result, "GetGroupChatDetail"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupChatDetail") + return result, err } type ( @@ -131,13 +129,11 @@ func (r *Client) OpengIDToChatID(req *OpengIDToChatIDRequest) (*OpengIDToChatIDR return nil, err } var response []byte - response, err = util.PostJSON(fmt.Sprintf("%s?access_token=%s", OpengIDToChatIDURL, accessToken), req) + response, err = util.PostJSON(fmt.Sprintf("%s?access_token=%s", opengIDToChatIDURL, accessToken), req) if err != nil { return nil, err } result := &OpengIDToChatIDResponse{} - if err = util.DecodeWithError(response, result, "GetGroupChatDetail"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupChatDetail") + return result, err } diff --git a/work/externalcontact/join_way.go b/work/externalcontact/join_way.go index 2396103..b9c1140 100644 --- a/work/externalcontact/join_way.go +++ b/work/externalcontact/join_way.go @@ -6,8 +6,8 @@ import ( "github.com/silenceper/wechat/v2/util" ) -// GroupChatURL 客户群 -const GroupChatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat" +// groupChatURL 客户群 +const groupChatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat" type ( // AddJoinWayRequest 添加群配置请求参数 @@ -39,15 +39,13 @@ func (r *Client) AddJoinWay(req *AddJoinWayRequest) (*AddJoinWayResponse, error) 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) + response, err = util.PostJSON(fmt.Sprintf("%s/add_join_way?access_token=%s", groupChatURL, accessToken), req) if err != nil { return nil, err } result := &AddJoinWayResponse{} - if err = util.DecodeWithError(response, result, "AddJoinWay"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "AddJoinWay") + return result, err } type ( @@ -86,15 +84,13 @@ func (r *Client) GetJoinWay(req *JoinWayConfigRequest) (*GetJoinWayResponse, err 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) + response, err = util.PostJSON(fmt.Sprintf("%s/get_join_way?access_token=%s", groupChatURL, accessToken), req) if err != nil { return nil, err } result := &GetJoinWayResponse{} - if err = util.DecodeWithError(response, result, "GetJoinWay"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetJoinWay") + return result, err } // UpdateJoinWayRequest 更新群配置的请求参数 @@ -120,7 +116,7 @@ func (r *Client) UpdateJoinWay(req *UpdateJoinWayRequest) error { 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) + response, err = util.PostJSON(fmt.Sprintf("%s/update_join_way?access_token=%s", groupChatURL, accessToken), req) if err != nil { return err } @@ -138,7 +134,7 @@ func (r *Client) DelJoinWay(req *JoinWayConfigRequest) error { 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) + response, err = util.PostJSON(fmt.Sprintf("%s/del_join_way?access_token=%s", groupChatURL, accessToken), req) if err != nil { return err } 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 index 4d72ab4..9ebd324 100644 --- a/work/externalcontact/msg.go +++ b/work/externalcontact/msg.go @@ -7,24 +7,28 @@ import ( ) 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" + // 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 创建企业群发请求 @@ -34,8 +38,23 @@ type AddMsgTemplateRequest struct { 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"` @@ -98,21 +117,19 @@ func (r *Client) AddMsgTemplate(req *AddMsgTemplateRequest) (*AddMsgTemplateResp return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(AddMsgTemplateURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(addMsgTemplateURL, accessToken), req); err != nil { return nil, err } result := &AddMsgTemplateResponse{} - if err = util.DecodeWithError(response, result, "AddMsgTemplate"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "AddMsgTemplate") + return result, err } // GetGroupMsgListV2Request 获取群发记录列表请求 type GetGroupMsgListV2Request struct { ChatType string `json:"chat_type"` - StartTime int `json:"start_time"` - EndTime int `json:"end_time"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` Creator string `json:"creator,omitempty"` FilterType int `json:"filter_type"` Limit int `json:"limit"` @@ -130,7 +147,7 @@ type GetGroupMsgListV2Response struct { type GroupMsg struct { MsgID string `json:"msgid"` Creator string `json:"creator"` - CreateTime int `json:"create_time"` + CreateTime int64 `json:"create_time"` CreateType int `json:"create_type"` Text MsgText `json:"text"` Attachments []*Attachment `json:"attachments"` @@ -147,14 +164,12 @@ func (r *Client) GetGroupMsgListV2(req *GetGroupMsgListV2Request) (*GetGroupMsgL return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(GetGroupMsgListV2URL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(getGroupMsgListV2URL, accessToken), req); err != nil { return nil, err } result := &GetGroupMsgListV2Response{} - if err = util.DecodeWithError(response, result, "GetGroupMsgListV2"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupMsgListV2") + return result, err } // GetGroupMsgTaskRequest 获取群发成员发送任务列表请求 @@ -189,14 +204,12 @@ func (r *Client) GetGroupMsgTask(req *GetGroupMsgTaskRequest) (*GetGroupMsgTaskR return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(GetGroupMsgTaskURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(getGroupMsgTaskURL, accessToken), req); err != nil { return nil, err } result := &GetGroupMsgTaskResponse{} - if err = util.DecodeWithError(response, result, "GetGroupMsgTask"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupMsgTask") + return result, err } // GetGroupMsgSendResultRequest 获取企业群发成员执行结果请求 @@ -234,14 +247,12 @@ func (r *Client) GetGroupMsgSendResult(req *GetGroupMsgSendResultRequest) (*GetG return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(GetGroupMsgSendResultURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(getGroupMsgSendResultURL, accessToken), req); err != nil { return nil, err } result := &GetGroupMsgSendResultResponse{} - if err = util.DecodeWithError(response, result, "GetGroupMsgSendResult"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupMsgSendResult") + return result, err } // SendWelcomeMsgRequest 发送新客户欢迎语请求 @@ -267,26 +278,23 @@ func (r *Client) SendWelcomeMsg(req *SendWelcomeMsgRequest) error { return err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(SendWelcomeMsgURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(sendWelcomeMsgURL, accessToken), req); err != nil { return err } result := &SendWelcomeMsgResponse{} - if err = util.DecodeWithError(response, result, "SendWelcomeMsg"); err != nil { - return err - } - return nil + 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"` + Text MsgText `json:"text"` + Image *AttachmentImg `json:"image,omitempty"` + Link *AttachmentLink `json:"link,omitempty"` + MiniProgram *AttachmentMiniProgram `json:"miniprogram,omitempty"` + File *AttachmentFile `json:"file,omitempty"` + Video *AttachmentVideo `json:"video,omitempty"` + AgentID int `json:"agentid,omitempty"` + Notify int `json:"notify,omitempty"` } // AddGroupWelcomeTemplateResponse 添加入群欢迎语素材响应 @@ -306,26 +314,24 @@ func (r *Client) AddGroupWelcomeTemplate(req *AddGroupWelcomeTemplateRequest) (* return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(AddGroupWelcomeTemplateURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(addGroupWelcomeTemplateURL, accessToken), req); err != nil { return nil, err } result := &AddGroupWelcomeTemplateResponse{} - if err = util.DecodeWithError(response, result, "AddGroupWelcomeTemplate"); err != nil { - return nil, err - } - return result, nil + 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"` + 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 编辑入群欢迎语素材响应 @@ -344,14 +350,11 @@ func (r *Client) EditGroupWelcomeTemplate(req *EditGroupWelcomeTemplateRequest) return err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(EditGroupWelcomeTemplateURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(editGroupWelcomeTemplateURL, accessToken), req); err != nil { return err } result := &EditGroupWelcomeTemplateResponse{} - if err = util.DecodeWithError(response, result, "EditGroupWelcomeTemplate"); err != nil { - return err - } - return nil + return util.DecodeWithError(response, result, "EditGroupWelcomeTemplate") } // GetGroupWelcomeTemplateRequest 获取入群欢迎语素材请求 @@ -363,11 +366,11 @@ type GetGroupWelcomeTemplateRequest struct { 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"` + Image AttachmentImg `json:"image,omitempty"` + Link AttachmentLink `json:"link,omitempty"` + MiniProgram AttachmentMiniProgram `json:"miniprogram,omitempty"` + File AttachmentFile `json:"file,omitempty"` + Video AttachmentVideo `json:"video,omitempty"` } // GetGroupWelcomeTemplate 获取入群欢迎语素材 @@ -381,14 +384,12 @@ func (r *Client) GetGroupWelcomeTemplate(req *GetGroupWelcomeTemplateRequest) (* return nil, err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(GetGroupWelcomeTemplateURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(getGroupWelcomeTemplateURL, accessToken), req); err != nil { return nil, err } result := &GetGroupWelcomeTemplateResponse{} - if err = util.DecodeWithError(response, result, "GetGroupWelcomeTemplate"); err != nil { - return nil, err - } - return result, nil + err = util.DecodeWithError(response, result, "GetGroupWelcomeTemplate") + return result, err } // DelGroupWelcomeTemplateRequest 删除入群欢迎语素材请求 @@ -413,12 +414,53 @@ func (r *Client) DelGroupWelcomeTemplate(req *DelGroupWelcomeTemplateRequest) er return err } var response []byte - if response, err = util.PostJSON(fmt.Sprintf(DelGroupWelcomeTemplateURL, accessToken), req); err != nil { + if response, err = util.PostJSON(fmt.Sprintf(delGroupWelcomeTemplateURL, accessToken), req); err != nil { return err } result := &DelGroupWelcomeTemplateResponse{} - if err = util.DecodeWithError(response, result, "DelGroupWelcomeTemplate"); err != nil { + 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 } - return nil + 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 index e308f66..e639062 100644 --- a/work/externalcontact/statistic.go +++ b/work/externalcontact/statistic.go @@ -8,12 +8,12 @@ import ( ) 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" + // 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 ( @@ -54,16 +54,13 @@ func (r *Client) GetUserBehaviorData(req *GetUserBehaviorRequest) ([]BehaviorDat if err != nil { return nil, err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", GetUserBehaviorDataURL, accessToken), string(jsonData)) + 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") - if err != nil { - return nil, err - } - return result.BehaviorData, nil + return result.BehaviorData, err } type ( @@ -120,16 +117,13 @@ func (r *Client) GetGroupChatStat(req *GetGroupChatStatRequest) (*GetGroupChatSt if err != nil { return nil, err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", GetGroupChatStatURL, accessToken), string(jsonData)) + 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") - if err != nil { - return nil, err - } - return result, nil + return result, err } type ( @@ -163,14 +157,11 @@ func (r *Client) GetGroupChatStatByDay(req *GetGroupChatStatByDayRequest) ([]Get if err != nil { return nil, err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", GetGroupChatStatByDayURL, accessToken), string(jsonData)) + 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") - if err != nil { - return nil, err - } - return result.Items, nil + return result.Items, err } diff --git a/work/externalcontact/tag.go b/work/externalcontact/tag.go index 6095010..00c2680 100644 --- a/work/externalcontact/tag.go +++ b/work/externalcontact/tag.go @@ -8,16 +8,24 @@ import ( ) 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" + // 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 获取企业标签请求 @@ -36,7 +44,7 @@ type GetCropTagListResponse struct { type TagGroup struct { GroupID string `json:"group_id"` GroupName string `json:"group_name"` - CreateTime int `json:"create_time"` + CreateTime int64 `json:"create_time"` GroupOrder int `json:"group_order"` Deleted bool `json:"deleted"` Tag []TagGroupTagItem `json:"tag"` @@ -46,7 +54,7 @@ type TagGroup struct { type TagGroupTagItem struct { ID string `json:"id"` Name string `json:"name"` - CreateTime int `json:"create_time"` + CreateTime int64 `json:"create_time"` Order int `json:"order"` Deleted bool `json:"deleted"` } @@ -63,16 +71,13 @@ func (r *Client) GetCropTagList(req GetCropTagRequest) ([]TagGroup, error) { if err != nil { return nil, err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", GetCropTagURL, accessToken), string(jsonData)) + 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") - if err != nil { - return nil, err - } - return result.TagGroup, nil + return result.TagGroup, err } // AddCropTagRequest 添加企业标签请求 @@ -109,16 +114,13 @@ func (r *Client) AddCropTag(req AddCropTagRequest) (*TagGroup, error) { if err != nil { return nil, err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", AddCropTagURL, accessToken), string(jsonData)) + 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") - if err != nil { - return nil, err - } - return &result.TagGroup, nil + return &result.TagGroup, err } // EditCropTagRequest 编辑客户企业标签请求 @@ -141,7 +143,7 @@ func (r *Client) EditCropTag(req EditCropTagRequest) error { if err != nil { return err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", EditCropTagURL, accessToken), string(jsonData)) + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", editCropTagURL, accessToken), string(jsonData)) if err != nil { return err } @@ -167,7 +169,7 @@ func (r *Client) DeleteCropTag(req DeleteCropTagRequest) error { if err != nil { return err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", DelCropTagURL, accessToken), string(jsonData)) + response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", delCropTagURL, accessToken), string(jsonData)) if err != nil { return err } @@ -195,9 +197,163 @@ func (r *Client) MarkTag(request MarkTagRequest) error { if err != nil { return err } - response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", MarkCropTagURL, accessToken), string(jsonData)) + 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/callback.go b/work/kf/callback.go index 149137c..a62cb1a 100644 --- a/work/kf/callback.go +++ b/work/kf/callback.go @@ -52,11 +52,12 @@ type callbackOriginMessage struct { // CallbackMessage 微信客服回调消息 type CallbackMessage struct { - ToUserName string `json:"to_user_name"` // 微信客服组件ID - CreateTime int `json:"create_time"` // 消息创建时间,unix时间戳 - MsgType string `json:"msgtype"` // 消息的类型,此时固定为 event - Event string `json:"event"` // 事件的类型,此时固定为 kf_msg_or_event - Token string `json:"token"` // 调用拉取消息接口时,需要传此token,用于校验请求的合法性 + 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 获取回调事件中的消息内容 @@ -91,8 +92,6 @@ func (r *Client) GetCallbackMessage(encryptedMsg []byte) (msg CallbackMessage, e if err != nil { return msg, NewSDKErr(40016) } - if err = xml.Unmarshal(bData, &msg); err != nil { - return msg, err - } + err = xml.Unmarshal(bData, &msg) return msg, err } 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/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 index 20d06f5..55ac079 100644 --- a/work/kf/syncmsg.go +++ b/work/kf/syncmsg.go @@ -16,9 +16,11 @@ const ( // 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字段判断是否继续请求。 + 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 获取消息查询响应内容 diff --git a/work/material/media.go b/work/material/media.go index 0d2ac8d..b32785a 100644 --- a/work/material/media.go +++ b/work/material/media.go @@ -7,8 +7,12 @@ import ( ) const ( - // UploadImgURL 上传图片 - UploadImgURL = "https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s" + // 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 上传图片响应 @@ -17,6 +21,22 @@ type UploadImgResponse struct { 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) { @@ -28,12 +48,51 @@ func (r *Client) UploadImg(filename string) (*UploadImgResponse, error) { return nil, err } var response []byte - if response, err = util.PostFile("media", filename, fmt.Sprintf(UploadImgURL, accessToken)); err != nil { + if response, err = util.PostFile("media", filename, fmt.Sprintf(uploadImgURL, accessToken)); err != nil { return nil, err } result := &UploadImgResponse{} - if err = util.DecodeWithError(response, result, "UploadImg"); err != nil { + 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 } - return result, nil + 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 index 2047ebd..b50b37e 100644 --- a/work/message/client.go +++ b/work/message/client.go @@ -1,17 +1,16 @@ +// Package message 消息推送,实现企业微信消息推送相关接口:https://developer.work.weixin.qq.com/document/path/90235 package message import ( "github.com/silenceper/wechat/v2/work/context" ) -// Client 应用消息 +// Client 消息推送接口实例 type Client struct { *context.Context } -// NewClient 实例化 +// NewClient 初始化实例 func NewClient(ctx *context.Context) *Client { - return &Client{ - ctx, - } + return &Client{ctx} } diff --git a/work/message/message.go b/work/message/message.go index a8078c8..ef6d24d 100644 --- a/work/message/message.go +++ b/work/message/message.go @@ -1,285 +1,130 @@ package message import ( - "encoding/xml" + "encoding/json" + "fmt" - "github.com/silenceper/wechat/v2/officialaccount/device" -) - -// MsgType 企业微信普通消息类型 -type MsgType string - -// EventType 企业微信事件消息类型 -type EventType string - -// InfoType 第三方平台授权事件类型 -type InfoType string - -const ( - //MsgTypeEvent 表示事件推送消息 [限接收] - MsgTypeEvent = "event" - - //MsgTypeText 表示文本消息 - MsgTypeText MsgType = "text" - //MsgTypeImage 表示图片消息 - MsgTypeImage MsgType = "image" - //MsgTypeVoice 表示语音消息 - MsgTypeVoice MsgType = "voice" - //MsgTypeVideo 表示视频消息 - MsgTypeVideo MsgType = "video" - //MsgTypeNews 表示图文消息[限回复与发送应用消息] - MsgTypeNews MsgType = "news" - - //MsgTypeLink 表示链接消息[限接收] - MsgTypeLink MsgType = "link" - //MsgTypeLocation 表示坐标消息[限接收] - MsgTypeLocation MsgType = "location" - - //MsgTypeUpdateButton 更新点击用户的按钮文案[限回复应用消息] - MsgTypeUpdateButton MsgType = "update_button" - //MsgTypeUpdateTemplateCard 更新点击用户的整张卡片[限回复应用消息] - MsgTypeUpdateTemplateCard MsgType = "update_template_card" - - //MsgTypeFile 文件消息[限发送应用消息] - MsgTypeFile MsgType = "file" - //MsgTypeTextCard 文本卡片消息[限发送应用消息] - MsgTypeTextCard MsgType = "textcard" - //MsgTypeMpNews 图文消息[限发送应用消息] 跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信 - MsgTypeMpNews MsgType = "mpnews" - //MsgTypeMarkdown markdown消息[限发送应用消息] - MsgTypeMarkdown MsgType = "markdown" - //MsgTypeMiniprogramNotice 小程序通知消息[限发送应用消息] - MsgTypeMiniprogramNotice MsgType = "miniprogram_notice" - //MsgTypeTemplateCard 模板卡片消息[限发送应用消息] - MsgTypeTemplateCard MsgType = "template_card" + "github.com/silenceper/wechat/v2/util" ) const ( - //EventSubscribe 成员关注,成员已经加入企业,管理员添加成员到应用可见范围(或移除可见范围)时 - EventSubscribe EventType = "subscribe" - //EventUnsubscribe 成员取消关注,成员已经在应用可见范围,成员加入(或退出)企业时 - EventUnsubscribe EventType = "unsubscribe" - //EventEnterAgent 本事件在成员进入企业微信的应用时触发 - EventEnterAgent EventType = "enter_agent" - //EventLocation 上报地理位置事件 - EventLocation EventType = "LOCATION" - //EventBatchJobResult 异步任务完成事件推送 - EventBatchJobResult EventType = "batch_job_result" - //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" - - //EventOpenApprovalChange 审批状态通知事件推送 - EventOpenApprovalChange EventType = "open_approval_change" - - //EventShareAgentChange 共享应用事件回调 - EventShareAgentChange EventType = "share_agent_change" - - //EventTemplateCard 模板卡片事件推送 - EventTemplateCard EventType = "template_card_event" - - //EventTemplateCardMenu 通用模板卡片右上角菜单事件推送 - EventTemplateCardMenu EventType = "template_card_menu_event" - - //EventChangeExternalContact 企业客户事件推送 - //add_external_contact 添加 - //edit_external_contact 编辑 - //add_half_external_contact 免验证添加 - //del_external_contact 员工删除客户 - //del_follow_user 客户删除跟进员工 - //transfer_fail 企业将客户分配给新的成员接替后,客户添加失败 - //change_external_chat 客户群创建事件 - EventChangeExternalContact EventType = "change_external_contact" - - //EventChangeExternalChat 企业客户群变更事件推送 - //create 客户群创建 - //update 客户群变更 - //dismiss 客户群解散 - EventChangeExternalChat EventType = "change_external_chat" - - //EventChangeExternalTag 企业客户标签创建事件推送 - //create 创建标签 - //update 变更标签 - //delete 删除标签 - //shuffle 重新排序 - EventChangeExternalTag EventType = "change_external_tag" - - //EventKfMsg 企业微信客服回调事件 - EventKfMsg EventType = "kf_msg_or_event" - //EventLivingStatusChange 直播回调事件 - EventLivingStatusChange EventType = "living_status_change" - - //EventMsgauditNotify 会话内容存档开启后,产生会话回调事件 - EventMsgauditNotify EventType = "msgaudit_notify" + // 发送应用消息的接口地址 + sendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" ) -//todo 第三方应用开发 -/*const ( - //微信开放平台需要用到 +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"` + } - // InfoTypeVerifyTicket 返回ticket - InfoTypeVerifyTicket InfoType = "component_verify_ticket" - // InfoTypeAuthorized 授权 - InfoTypeAuthorized = "authorized" - // InfoTypeUnauthorized 取消授权 - InfoTypeUnauthorized = "unauthorized" - // InfoTypeUpdateAuthorized 更新授权 - InfoTypeUpdateAuthorized = "updateauthorized" -)*/ + // SendTextRequest 发送文本消息的请求 + SendTextRequest struct { + *SendRequestCommon + Text TextField `json:"text"` + } + // TextField 文本消息参数 + TextField struct { + // 消息内容,最长不超过2048个字节,超过将截断(支持id转译) + Content string `json:"content"` + } -//MixMessage 存放所有企业微信官方发送过来的消息和事件 -type MixMessage struct { - CommonToken + // SendImageRequest 发送图片消息的请求 + SendImageRequest struct { + *SendRequestCommon + Image ImageField `json:"image"` + } + // ImageField 图片消息参数 + ImageField struct { + // 图片媒体文件id,可以调用上传临时素材接口获取 + MediaID string `json:"media_id"` + } - //接收普通消息 - MsgID int64 `xml:"MsgId"` //其他消息推送过来是MsgId - AgentID int `xml:"AgentID"` //企业应用的id,整型。可在应用的设置页面查看 + // SendVoiceRequest 发送语音消息的请求 + SendVoiceRequest struct { + *SendRequestCommon + Voice VoiceField `json:"voice"` + } + // VoiceField 语音消息参数 + VoiceField struct { + // 语音文件id,可以调用上传临时素材接口获取 + MediaID string `json:"media_id"` + } +) - Content string `xml:"Content,omitempty"` //文本消息内容 - Format string `xml:"Format,omitempty"` //语音消息格式,如amr,speex等 - ThumbMediaID string `xml:"ThumbMediaId,omitempty"` //视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取数据,仅三天内有效 - - Title string `xml:"Title,omitempty"` //链接消息,标题 - Description string `xml:"Description,omitempty"` //链接消息,描述 - URL string `xml:"Url,omitempty"` //链接消息,链接跳转的url - - PicURL string `xml:"PicUrl,omitempty"` ////图片消息或者链接消息,封面缩略图的url - MediaID string `xml:"MediaId,omitempty"` //图片媒体文件id//语音媒体文件id//视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取,仅三天内有效 - - LocationX float64 `xml:"Location_X,omitempty"` //位置消息,地理位置纬度 - LocationY float64 `xml:"Location_Y,omitempty"` //位置消息,地理位置经度 - Scale float64 `xml:"Scale,omitempty"` //位置消息,地图缩放大小 - Label string `xml:"Label,omitempty"` //位置消息,地理位置信息 - - AppType string `xml:"AppType,omitempty"` //接收地理位置时存在,app类型,在企业微信固定返回wxwork,在微信不返回该字段 - - //TemplateMsgID int64 `xml:"MsgID"` //模板消息推送成功的消息是MsgID - ///Recognition string `xml:"Recognition"` - - //事件相关 - Event EventType `xml:"Event,omitempty"` - EventKey string `xml:"EventKey,omitempty"` - ChangeType string `xml:"ChangeType,omitempty"` - - //模板卡片事件推送 https://developer.work.weixin.qq.com/document/path/90240#%E6%A8%A1%E6%9D%BF%E5%8D%A1%E7%89%87%E4%BA%8B%E4%BB%B6%E6%8E%A8%E9%80%81 - TaskId string `xml:"TaskId,omitempty"` //与发送模板卡片消息时指定的task_id相同 - CardType string `xml:"CardType,omitempty"` //通用模板卡片的类型,类型有"text_notice", "news_notice", "button_interaction", "vote_interaction", "multiple_interaction"五种 - ResponseCode string `xml:"ResponseCode,omitempty"` //用于调用更新卡片接口的ResponseCode,24小时内有效,且只能使用一次 - SelectedItems struct { - SelectedItem struct { - QuestionKey string `xml:"QuestionKey"` //问题的key值 - OptionIds struct { //对应问题的选项列表 - OptionId string `xml:"OptionId"` - } `xml:"OptionIds"` - } `xml:"SelectedItem"` - } `xml:"SelectedItems,omitempty"` - - //仅上报地理位置事件 - Latitude string `xml:"Latitude,omitempty"` //地理位置纬度 - Longitude string `xml:"Longitude,omitempty"` //地理位置经度 - Precision string `xml:"Precision,omitempty"` //地理位置精度 - - //仅异步任务完成事件 - JobId string `xml:"JobId,omitempty"` //异步任务id,最大长度为64字符 - JobType string `xml:"JobType,omitempty"` //异步任务,操作类型,字符串,目前分别有:sync_user(增量更新成员)、 replace_user(全量覆盖成员)、invite_user(邀请成员关注)、replace_party(全量覆盖部门) - ErrCode int `xml:"ErrCode,omitempty"` //异步任务,返回码 - ErrMsg string `xml:"ErrMsg,omitempty"` //异步任务,对返回码的文本描述内容 - - //开启通讯录回调通知 https://open.work.weixin.qq.com/api/doc/90000/90135/90967 - UserID string `xml:"UserID,omitempty"` //用户userid - ExternalUserID string `xml:"ExternalUserID,omitempty"` //外部联系人userid - State string `xml:"State,omitempty"` //添加此用户的「联系我」方式配置的state参数,可用于识别添加此用户的渠道 - WelcomeCode string `xml:"WelcomeCode,omitempty"` //欢迎码,当state为1时,该值有效 - Source string `xml:"Source,omitempty"` //删除客户的操作来源,DELETE_BY_TRANSFER表示此客户是因在职继承自动被转接成员删除 - - // todo 第三方平台相关 字段名可能不准确 - /*InfoType InfoType `xml:"InfoType"` - AppID string `xml:"AppId"` - ComponentVerifyTicket string `xml:"ComponentVerifyTicket"` - AuthorizerAppid string `xml:"AuthorizerAppid"` - AuthorizationCode string `xml:"AuthorizationCode"``````````````````````````````````````` - AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"` - PreAuthCode string `xml:"PreAuthCode"`*/ - - //设备相关 - device.MsgDevice +// 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 } -//EventPic 发图事件推送 -type EventPic struct { - PicMd5Sum string `xml:"PicMd5Sum"` +// SendText 发送文本消息 +func (r *Client) SendText(request SendTextRequest) (*SendResponse, error) { + // 发送文本消息MsgType参数固定为:text + request.MsgType = "text" + return r.Send("MessageSendText", request) } -//EncryptedXMLMsg 安全模式下的消息体 -type EncryptedXMLMsg struct { - XMLName struct{} `xml:"xml" json:"-"` - ToUserName string `xml:"ToUserName" json:"ToUserName"` - AgentID string `xml:"AgentID" json:"AgentID"` - EncryptedMsg string `xml:"Encrypt" json:"Encrypt"` +// SendImage 发送图片消息 +func (r *Client) SendImage(request SendImageRequest) (*SendResponse, error) { + // 发送图片消息MsgType参数固定为:image + request.MsgType = "image" + return r.Send("MessageSendImage", request) } -//ResponseEncryptedXMLMsg 需要返回的消息体 -type ResponseEncryptedXMLMsg struct { - XMLName struct{} `xml:"xml" json:"-"` - EncryptedMsg string `xml:"Encrypt" json:"Encrypt"` - MsgSignature string `xml:"MsgSignature" json:"MsgSignature"` - Timestamp int64 `xml:"TimeStamp" json:"TimeStamp"` - Nonce string `xml:"Nonce" json:"Nonce"` +// SendVoice 发送语音消息 +func (r *Client) SendVoice(request SendVoiceRequest) (*SendResponse, error) { + // 发送语音消息MsgType参数固定为:voice + request.MsgType = "voice" + return r.Send("MessageSendVoice", request) } -// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略 -type CDATA string - -// MarshalXML 实现自己的序列化方法 -func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - return e.EncodeElement(struct { - string `xml:",cdata"` - }{string(c)}, start) -} - -// 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"` -} - -//SetToUserName set ToUserName -func (msg *CommonToken) SetToUserName(toUserName CDATA) { - msg.ToUserName = toUserName -} - -//SetFromUserName set FromUserName -func (msg *CommonToken) SetFromUserName(fromUserName CDATA) { - msg.FromUserName = fromUserName -} - -//SetCreateTime set createTime -func (msg *CommonToken) SetCreateTime(createTime int64) { - msg.CreateTime = createTime -} - -//SetMsgType set MsgType -func (msg *CommonToken) SetMsgType(msgType MsgType) { - msg.MsgType = msgType -} - -//GetOpenID get the FromUserName value -func (msg *CommonToken) GetOpenID() string { - return string(msg.FromUserName) -} +// 以上实现了部分常用消息推送:SendText 发送文本消息、SendImage 发送图片消息、SendVoice 发送语音消息, +// 如需扩展其他消息类型,建议按照以上格式,扩展对应消息类型的参数即可 +// 也可以直接使用Send方法,按照企业微信消息推送的接口文档传对应消息类型的参数来使用 diff --git a/work/msgaudit/client_linux.go b/work/msgaudit/client_linux.go index 9b1c04d..80d21a2 100644 --- a/work/msgaudit/client_linux.go +++ b/work/msgaudit/client_linux.go @@ -149,10 +149,7 @@ func (s *Client) GetRawChatData(seq uint64, limit uint64, proxy string, passwd s var data ChatDataResponse err := json.Unmarshal(buf, &data) - if err != nil { - return ChatDataResponse{}, err - } - return data, nil + return data, err } // DecryptData 解析密文.企业微信自有解密内容 diff --git a/work/msgaudit/message.go b/work/msgaudit/message.go index 91202de..0d8e990 100644 --- a/work/msgaudit/message.go +++ b/work/msgaudit/message.go @@ -150,7 +150,7 @@ type TodoMessage struct { BaseMessage Todo struct { Title string `json:"title,omitempty"` // 代办的来源文本 - Content string `json:"content,omitempty"` // 代办的具体内容 + Content string `json:"content,omitempty"` // 代办的具体内容 } `json:"todo,omitempty"` } @@ -169,7 +169,7 @@ type CollectMessage struct { Collect struct { RoomName string `json:"room_name,omitempty"` // 填表消息所在的群名称。 Creator string `json:"creator,omitempty"` // 创建者在群中的名字 - CreateTime string `json:"create_time,omitempty"` // 创建的时间 + CreateTime int64 `json:"create_time,omitempty"` // 创建的时间 Details []CollectDetails `json:"details,omitempty"` // 表内容 } `json:"collect,omitempty"` } @@ -266,10 +266,10 @@ type VoipDocShareMessage struct { type ExternalRedPacketMessage struct { BaseMessage RedPacket struct { - Type int32 `json:"type,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包。Uint32类型 - Wish int32 `json:"wish,omitempty"` // 红包祝福语。String类型 - TotalCnt int32 `json:"totalcnt,omitempty"` // 红包总个数。Uint32类型 - TotalAmount int32 `json:"totalamount,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包。Uint32类型 + 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"` } @@ -277,9 +277,9 @@ type ExternalRedPacketMessage struct { type SphFeedMessage struct { BaseMessage SphFeed struct { - FeedType string `json:"feed_type,omitempty"` // 视频号消息类型 - SphName string `json:"sph_name,omitempty"` // 视频号账号名称 - FeedDesc uint64 `json:"feed_desc,omitempty"` // 视频号账号名称 + FeedType uint32 `json:"feed_type,omitempty"` // 视频号消息类型。2 图片、4 视频、9 直播。Uint32类型 + SphName string `json:"sph_name,omitempty"` // 视频号账号名称。String类型 + FeedDesc string `json:"feed_desc,omitempty"` // 视频号消息描述。String类型 } } diff --git a/work/oauth/oauth.go b/work/oauth/oauth.go index 9307c5f..add4b02 100644 --- a/work/oauth/oauth.go +++ b/work/oauth/oauth.go @@ -23,8 +23,10 @@ var ( 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" - //code2Session 获取用户信息地址 - code2SessionURL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code" + // 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 @@ -76,7 +78,6 @@ type ResUserInfo struct { // 非企业成员授权时返回 OpenID string `json:"OpenId"` ExternalUserID string `json:"external_userid"` - UserTicket string `json:"user_ticket"` } // UserFromCode 根据code获取用户信息 @@ -97,23 +98,68 @@ func (ctr *Oauth) UserFromCode(code string) (result ResUserInfo, err error) { return } -func (ctr *Oauth) Code2Session(code string) (result ResUserInfo, err error) { - var accessToken string - accessToken, err = ctr.GetAccessToken() - if err != nil { - 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 - response, err = util.HTTPGet( - fmt.Sprintf(code2SessionURL, accessToken, code), - ) - if err != nil { - return + if response, err = util.HTTPGet(fmt.Sprintf(getUserInfoURL, accessToken, code)); err != nil { + return nil, err } - err = json.Unmarshal(response, &result) - if result.ErrCode != 0 { - err = fmt.Errorf("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) - return - } - return + 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/robot.go b/work/robot/robot.go index 145f63d..eee3837 100644 --- a/work/robot/robot.go +++ b/work/robot/robot.go @@ -8,15 +8,15 @@ import ( ) const ( - // WebhookSendURL 机器人发送群组消息 - WebhookSendURL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s" + // 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 { + if data, err = util.PostJSON(fmt.Sprintf(webhookSendURL, webhookKey), options); err != nil { return } if err = json.Unmarshal(data, &info); err != nil { diff --git a/work/work.go b/work/work.go index cef426d..24c5773 100644 --- a/work/work.go +++ b/work/work.go @@ -3,20 +3,18 @@ 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/js" + "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" - "github.com/silenceper/wechat/v2/work/server" - "github.com/silenceper/wechat/v2/work/tools" - "github.com/silenceper/wechat/v2/work/user" - "net/http" ) // Work 企业微信 @@ -39,24 +37,11 @@ func (wk *Work) GetContext() *context.Context { return wk.ctx } -// GetServer 消息管理:接收事件,被动回复消息管理 -func (wk *Work) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server { - srv := server.NewServer(wk.ctx) - srv.Request = req - srv.Writer = writer - return srv -} - // GetOauth get oauth func (wk *Work) GetOauth() *oauth.Oauth { return oauth.NewOauth(wk.ctx) } -// GetJs js-sdk配置 -func (wk *Work) GetJs() *js.Js { - return js.NewJs(wk.ctx) -} - // GetMsgAudit get msgAudit func (wk *Work) GetMsgAudit() (*msgaudit.Client, error) { return msgaudit.NewClient(wk.ctx.Config) @@ -67,26 +52,11 @@ func (wk *Work) GetKF() (*kf.Client, error) { return kf.NewClient(wk.ctx.Config) } -// GetUser get user -func (wk *Work) GetUser() *user.User { - return user.NewUser(wk.ctx) -} - -// GetCalendar get calendar -func (wk *Work) GetCalendar() *tools.Calendar { - return tools.NewCalendar(wk.ctx) -} - -// GetExternalContact 客户联系 +// GetExternalContact get external_contact func (wk *Work) GetExternalContact() *externalcontact.Client { return externalcontact.NewClient(wk.ctx) } -// GetMessageApp 发送应用消息 -func (wk *Work) GetMessageApp() *message.Client { - return message.NewClient(wk.ctx) -} - // GetAddressList get address_list func (wk *Work) GetAddressList() *addresslist.Client { return addresslist.NewClient(wk.ctx) @@ -101,3 +71,23 @@ func (wk *Work) GetMaterial() *material.Client { 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) +}