mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-12 00:32:26 +08:00
Compare commits
52 Commits
v2.1.6
...
a532cdcbd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a532cdcbd2 | ||
|
|
0e7907e9ac | ||
|
|
d08b18983b | ||
|
|
d28c61ad4a | ||
|
|
961d5604d5 | ||
|
|
0f3c9cd773 | ||
|
|
06e92dd5ce | ||
|
|
8a044dcd58 | ||
|
|
a7347f9506 | ||
|
|
990ba6ede9 | ||
|
|
44b09c7c3b | ||
|
|
bcdb2fa6ca | ||
|
|
61bcd6b0e4 | ||
|
|
4a4339fc32 | ||
|
|
2e0708845b | ||
|
|
c1770130a0 | ||
|
|
c22a036b7f | ||
|
|
05ac7148d4 | ||
|
|
6b3532cc2d | ||
|
|
06c225c351 | ||
|
|
1138a7db0e | ||
|
|
1842f08f4c | ||
|
|
d28ca4f334 | ||
|
|
1c2360737c | ||
|
|
74795e86ee | ||
|
|
179704187b | ||
|
|
ceb006bf58 | ||
|
|
9f14122a87 | ||
|
|
7fcd7a34dd | ||
|
|
5b29289dbb | ||
|
|
74e3e9c04e | ||
|
|
97c9f7d908 | ||
|
|
97e1af5904 | ||
|
|
158fbca872 | ||
|
|
e351d0bc66 | ||
|
|
589de19257 | ||
|
|
1cd6133420 | ||
|
|
b4402678b3 | ||
|
|
cec81779a5 | ||
|
|
da20182300 | ||
|
|
502a781811 | ||
|
|
586a3b058e | ||
|
|
96e7945d20 | ||
|
|
6313e3d580 | ||
|
|
88f07bc5fb | ||
|
|
1f80c26a15 | ||
|
|
5704abb3b0 | ||
|
|
7ae8e08a3e | ||
|
|
73adb7dcdd | ||
|
|
af5115fb4e | ||
|
|
829356eee9 | ||
|
|
7e24cb9e8d |
6
.github/ISSUE_TEMPLATE/bug.md
vendored
6
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: 报告Bug
|
name: 报告 Bug
|
||||||
about: 反馈BUG信息
|
about: 反馈 BUG 信息
|
||||||
title: "[BUG]"
|
title: "[BUG]"
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -18,4 +18,4 @@ assignees: ''
|
|||||||
|
|
||||||
|
|
||||||
**使用的版本**
|
**使用的版本**
|
||||||
- SDK版本: [比如 v0.0.0]
|
- SDK 版本:[比如 v0.0.0]
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/feature.md
vendored
8
.github/ISSUE_TEMPLATE/feature.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: API需求
|
name: API 需求
|
||||||
about: 待实现的API接口,SDK的强大离不开社区的帮助,欢迎为项目贡献PR
|
about: 待实现的 API 接口,SDK 的强大离不开社区的帮助,欢迎为项目贡献 PR
|
||||||
title: "[Feature]"
|
title: "[Feature]"
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -8,8 +8,8 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
!!!SDK的强大离不开社区的帮助,欢迎为本项目贡献PR!!!
|
!!!SDK 的强大离不开社区的帮助,欢迎为本项目贡献 PR!!!
|
||||||
-->
|
-->
|
||||||
**你想要实现的模块或API**
|
**你想要实现的模块或 API**
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/question.md
vendored
6
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: 使用咨询
|
name: 使用咨询
|
||||||
about: 关于SDK使用相关的咨询,在使用前请先阅读官方微信文档
|
about: 关于 SDK 使用相关的咨询,在使用前请先阅读官方微信文档
|
||||||
title: "[咨询]"
|
title: "[咨询]"
|
||||||
labels: question
|
labels: question
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -9,7 +9,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
重要:
|
重要:
|
||||||
1、在使用本SDK前请先阅读对应的官方微信API文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
|
1、在使用本 SDK 前请先阅读对应的官方微信 API 文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
|
||||||
2、本SDK部分接口文档: https://silenceper.com/wechat/
|
2、本 SDK 部分接口文档:https://silenceper.com/wechat/
|
||||||
-->
|
-->
|
||||||
**请描述您的问题**
|
**请描述您的问题**
|
||||||
|
|||||||
18
.github/workflows/go.yml
vendored
18
.github/workflows/go.yml
vendored
@@ -2,26 +2,26 @@ name: Go
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master,release-*,v2,feature/** ]
|
branches: [ master,release-*,v2,feature/**,fix/** ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master,release-*,v2,feature/** ]
|
branches: [ master,release-*,v2,feature/**,fix/** ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [ '1.16','1.17','1.18','1.19','1.20','1.21' ]
|
go-version: [ '1.16','1.17','1.18','1.19','1.20','1.21.4' ]
|
||||||
name: golangci-lint
|
name: golangci-lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Golang ${{ matrix.go-version }}
|
- name: Setup Golang ${{ matrix.go-version }}
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
version: v1.52.2
|
version: v1.52.2
|
||||||
@@ -42,12 +42,12 @@ jobs:
|
|||||||
# strategy set
|
# strategy set
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go: [ '1.16','1.17','1.18','1.19','1.20','1.21' ]
|
go: [ '1.16','1.17','1.18','1.19','1.20','1.21','1.22' ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
id: go
|
id: go
|
||||||
|
|||||||
2
cache/redis.go
vendored
2
cache/redis.go
vendored
@@ -16,6 +16,7 @@ type Redis struct {
|
|||||||
// RedisOpts redis 连接属性
|
// RedisOpts redis 连接属性
|
||||||
type RedisOpts struct {
|
type RedisOpts struct {
|
||||||
Host string `yml:"host" json:"host"`
|
Host string `yml:"host" json:"host"`
|
||||||
|
Username string `yaml:"username" json:"username"`
|
||||||
Password string `yml:"password" json:"password"`
|
Password string `yml:"password" json:"password"`
|
||||||
Database int `yml:"database" json:"database"`
|
Database int `yml:"database" json:"database"`
|
||||||
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
||||||
@@ -28,6 +29,7 @@ func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
|||||||
conn := redis.NewUniversalClient(&redis.UniversalOptions{
|
conn := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||||
Addrs: []string{opts.Host},
|
Addrs: []string{opts.Host},
|
||||||
DB: opts.Database,
|
DB: opts.Database,
|
||||||
|
Username: opts.Username,
|
||||||
Password: opts.Password,
|
Password: opts.Password,
|
||||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||||
MinIdleConns: opts.MaxIdle,
|
MinIdleConns: opts.MaxIdle,
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(resAccessToken.ExpiresIn-1500)*time.Second); err != nil {
|
expires := resAccessToken.ExpiresIn - 1500
|
||||||
return
|
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||||
}
|
|
||||||
accessToken = resAccessToken.AccessToken
|
accessToken = resAccessToken.AccessToken
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,7 @@ func (ak *StableAccessToken) GetAccessTokenContext(ctx context.Context) (accessT
|
|||||||
}
|
}
|
||||||
|
|
||||||
expires := resAccessToken.ExpiresIn - 300
|
expires := resAccessToken.ExpiresIn - 300
|
||||||
_ = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||||
|
|
||||||
accessToken = resAccessToken.AccessToken
|
accessToken = resAccessToken.AccessToken
|
||||||
return
|
return
|
||||||
@@ -219,9 +219,7 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok
|
|||||||
|
|
||||||
expires := resAccessToken.ExpiresIn - 1500
|
expires := resAccessToken.ExpiresIn - 1500
|
||||||
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
accessToken = resAccessToken.AccessToken
|
accessToken = resAccessToken.AccessToken
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
85
credential/work_js_ticket.go
Normal file
85
credential/work_js_ticket.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package credential
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
//获取ticket的url https://developer.work.weixin.qq.com/document/path/90506
|
||||||
|
const getQyWxTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s"
|
||||||
|
const getQyAppTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=%s&type=agent_config"
|
||||||
|
|
||||||
|
//WorkJsTicket 默认获取js ticket方法
|
||||||
|
type WorkJsTicket struct {
|
||||||
|
appID string
|
||||||
|
agentID string
|
||||||
|
cacheKeyPrefix string
|
||||||
|
cache cache.Cache
|
||||||
|
//jsAPITicket 读写锁 同一个AppID一个
|
||||||
|
jsAPITicketLock *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewWorkJsTicket new
|
||||||
|
func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
||||||
|
return &WorkJsTicket{
|
||||||
|
appID: appID,
|
||||||
|
agentID: agentID,
|
||||||
|
cache: cache,
|
||||||
|
cacheKeyPrefix: cacheKeyPrefix,
|
||||||
|
jsAPITicketLock: new(sync.Mutex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetTicket 获取企业微信jsapi_ticket
|
||||||
|
func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||||
|
//先从cache中取
|
||||||
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||||
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
js.jsAPITicketLock.Lock()
|
||||||
|
defer js.jsAPITicketLock.Unlock()
|
||||||
|
|
||||||
|
// 双检,防止重复从微信服务器获取
|
||||||
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticket ResTicket
|
||||||
|
ticket, err = GetQyWxTicketFromServer(accessToken, js.agentID != "")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expires := ticket.ExpiresIn - 1500
|
||||||
|
err = js.cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||||
|
ticketStr = ticket.Ticket
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetQyWxTicketFromServer 从企业微信服务器中获取ticket
|
||||||
|
func GetQyWxTicketFromServer(accessToken string, isApp bool) (ticket ResTicket, err error) {
|
||||||
|
var response []byte
|
||||||
|
url := fmt.Sprintf(getQyWxTicketURL, accessToken)
|
||||||
|
if isApp {
|
||||||
|
url = fmt.Sprintf(getQyAppTicketURL, accessToken)
|
||||||
|
}
|
||||||
|
response, err = util.HTTPGet(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &ticket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ticket.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -138,10 +138,8 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result GetPhoneNumberResponse
|
var result GetPhoneNumberResponse
|
||||||
if err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber"); err != nil {
|
err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber")
|
||||||
return nil, err
|
return &result, err
|
||||||
}
|
|
||||||
return &result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPhoneNumber 小程序通过code获取用户手机号
|
// GetPhoneNumber 小程序通过code获取用户手机号
|
||||||
|
|||||||
@@ -45,10 +45,5 @@ func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneI
|
|||||||
PhoneInfo PhoneInfo `json:"phone_info"`
|
PhoneInfo PhoneInfo `json:"phone_info"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber")
|
err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber")
|
||||||
if nil != err {
|
return resp.PhoneInfo, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
info = resp.PhoneInfo
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Encryptor struct
|
// Encryptor struct
|
||||||
@@ -108,13 +108,23 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt 解密数据
|
// Decrypt 解密数据
|
||||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) {
|
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, appid string) (*PlainData, error) {
|
||||||
|
ivB := make([]byte, 16)
|
||||||
|
iv := base64.StdEncoding.EncodeToString(ivB)
|
||||||
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
length := string(cipherText[:20])
|
||||||
|
|
||||||
|
cipherTextData := strings.TrimPrefix(string(cipherText), string(cipherText[:20]))
|
||||||
|
cipherTextData = strings.TrimSuffix(cipherTextData, appid)
|
||||||
|
|
||||||
|
if len(length) != len(cipherTextData) {
|
||||||
|
return nil, fmt.Errorf("length not match, %d != %d", length, len(cipherTextData))
|
||||||
|
}
|
||||||
var plainData PlainData
|
var plainData PlainData
|
||||||
err = json.Unmarshal(cipherText, &plainData)
|
err = json.Unmarshal([]byte(cipherTextData), &plainData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ type PushDataSecVodUpload struct {
|
|||||||
|
|
||||||
// SecVodUploadEvent 短剧媒资上传完成事件
|
// SecVodUploadEvent 短剧媒资上传完成事件
|
||||||
type SecVodUploadEvent struct {
|
type SecVodUploadEvent struct {
|
||||||
MediaID string `json:"media_id" xml:"media_id"` // 媒资 id
|
MediaID int64 `json:"media_id" xml:"media_id"` // 媒资 id
|
||||||
SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值。
|
SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值。
|
||||||
ErrCode int `json:"errcode" xml:"errcode"` // 错误码,上传失败时该值非
|
ErrCode int `json:"errcode" xml:"errcode"` // 错误码,上传失败时该值非
|
||||||
ErrMsg string `json:"errmsg" xml:"errmsg"` // 错误提示
|
ErrMsg string `json:"errmsg" xml:"errmsg"` // 错误提示
|
||||||
@@ -412,7 +412,7 @@ type PushDataSecVodAudit struct {
|
|||||||
|
|
||||||
// SecVodAuditEvent 短剧媒资审核状态事件
|
// SecVodAuditEvent 短剧媒资审核状态事件
|
||||||
type SecVodAuditEvent struct {
|
type SecVodAuditEvent struct {
|
||||||
DramaID string `json:"drama_id" xml:"drama_id"` // 剧目 id
|
DramaID int64 `json:"drama_id" xml:"drama_id"` // 剧目 id
|
||||||
SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值
|
SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值
|
||||||
AuditDetail DramaAuditDetail `json:"audit_detail" xml:"audit_detail"` // 剧目审核结果,单独每一集的审核结果可以根据 drama_id 查询剧集详情得到
|
AuditDetail DramaAuditDetail `json:"audit_detail" xml:"audit_detail"` // 剧目审核结果,单独每一集的审核结果可以根据 drama_id 查询剧集详情得到
|
||||||
}
|
}
|
||||||
|
|||||||
102
miniprogram/message/updatable_msg.go
Normal file
102
miniprogram/message/updatable_msg.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/miniprogram/order"
|
"github.com/silenceper/wechat/v2/miniprogram/order"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
"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/riskcontrol"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/security"
|
"github.com/silenceper/wechat/v2/miniprogram/security"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/shortlink"
|
"github.com/silenceper/wechat/v2/miniprogram/shortlink"
|
||||||
@@ -155,3 +156,13 @@ func (miniProgram *MiniProgram) GetShipping() *order.Shipping {
|
|||||||
func (miniProgram *MiniProgram) GetMiniDrama() *minidrama.MiniDrama {
|
func (miniProgram *MiniProgram) GetMiniDrama() *minidrama.MiniDrama {
|
||||||
return minidrama.NewMiniDrama(miniProgram.ctx)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,11 +103,8 @@ func (s *Privacy) GetPrivacySetting(privacyVer int) (GetPrivacySettingResponse,
|
|||||||
}
|
}
|
||||||
// 返回错误信息
|
// 返回错误信息
|
||||||
var result GetPrivacySettingResponse
|
var result GetPrivacySettingResponse
|
||||||
if err = util.DecodeWithError(response, &result, "getprivacysetting"); err != nil {
|
err = util.DecodeWithError(response, &result, "getprivacysetting")
|
||||||
return GetPrivacySettingResponse{}, err
|
return result, err
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPrivacySetting 更新小程序权限配置
|
// SetPrivacySetting 更新小程序权限配置
|
||||||
@@ -130,11 +127,7 @@ func (s *Privacy) SetPrivacySetting(privacyVer int, ownerSetting OwnerSetting, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 返回错误信息
|
// 返回错误信息
|
||||||
if err = util.DecodeWithCommonError(response, "setprivacysetting"); err != nil {
|
return util.DecodeWithCommonError(response, "setprivacysetting")
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadPrivacyExtFileResponse 上传权限定义模板响应参数
|
// UploadPrivacyExtFileResponse 上传权限定义模板响应参数
|
||||||
@@ -159,9 +152,6 @@ func (s *Privacy) UploadPrivacyExtFile(fileData []byte) (UploadPrivacyExtFileRes
|
|||||||
|
|
||||||
// 返回错误信息
|
// 返回错误信息
|
||||||
var result UploadPrivacyExtFileResponse
|
var result UploadPrivacyExtFileResponse
|
||||||
if err = util.DecodeWithError(response, &result, "setprivacysetting"); err != nil {
|
err = util.DecodeWithError(response, &result, "setprivacysetting")
|
||||||
return UploadPrivacyExtFileResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ type QRCoder struct {
|
|||||||
IsHyaline bool `json:"is_hyaline,omitempty"`
|
IsHyaline bool `json:"is_hyaline,omitempty"`
|
||||||
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
|
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
|
||||||
EnvVersion string `json:"env_version,omitempty"`
|
EnvVersion string `json:"env_version,omitempty"`
|
||||||
|
// ShowSplashAd 控制通过该小程序码进入小程序是否展示封面广告1、默认为true,展示封面广告2、传入为false时,不展示封面广告
|
||||||
|
ShowSplashAd bool `json:"show_splash_ad,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchCode 请求并返回二维码二进制数据
|
// fetchCode 请求并返回二维码二进制数据
|
||||||
|
|||||||
59
miniprogram/redpacketcover/redpacketcover.go
Normal file
59
miniprogram/redpacketcover/redpacketcover.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -51,12 +51,7 @@ func (security *Security) MediaCheckAsyncV1(in *MediaCheckAsyncV1Request) (trace
|
|||||||
TraceID string `json:"trace_id"`
|
TraceID string `json:"trace_id"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1")
|
err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1")
|
||||||
if err != nil {
|
return res.TraceID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
traceID = res.TraceID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaCheckAsyncRequest 图片/音频异步校验请求参数
|
// MediaCheckAsyncRequest 图片/音频异步校验请求参数
|
||||||
@@ -93,12 +88,7 @@ func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID s
|
|||||||
TraceID string `json:"trace_id"`
|
TraceID string `json:"trace_id"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "MediaCheckAsync")
|
err = util.DecodeWithError(response, &res, "MediaCheckAsync")
|
||||||
if err != nil {
|
return res.TraceID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
traceID = res.TraceID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageCheckV1 校验一张图片是否含有违法违规内容(同步)
|
// ImageCheckV1 校验一张图片是否含有违法违规内容(同步)
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error
|
|||||||
// 使用通用方法返回错误
|
// 使用通用方法返回错误
|
||||||
var res resShortLinker
|
var res resShortLinker
|
||||||
err = util.DecodeWithError(response, &res, "GenerateShortLink")
|
err = util.DecodeWithError(response, &res, "GenerateShortLink")
|
||||||
if err != nil {
|
return res.Link, err
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Link, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateShortLinkPermanent 生成永久 shortLink
|
// GenerateShortLinkPermanent 生成永久 shortLink
|
||||||
|
|||||||
@@ -168,11 +168,7 @@ func (s *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templa
|
|||||||
}
|
}
|
||||||
var result resSubscribeAdd
|
var result resSubscribeAdd
|
||||||
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
||||||
if err != nil {
|
return result.TemplateID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
templateID = result.TemplateID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除私有模板
|
// Delete 删除私有模板
|
||||||
|
|||||||
@@ -65,8 +65,5 @@ func (u *URLLink) Generate(params *ULParams) (string, error) {
|
|||||||
}
|
}
|
||||||
var resp ULResult
|
var resp ULResult
|
||||||
err = util.DecodeWithError(response, &resp, "URLLink.Generate")
|
err = util.DecodeWithError(response, &resp, "URLLink.Generate")
|
||||||
if err != nil {
|
return resp.URLLink, err
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return resp.URLLink, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ type SchemeInfo struct {
|
|||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||||
type resQueryScheme struct {
|
type resQueryScheme struct {
|
||||||
// 通用错误
|
// 通用错误
|
||||||
*util.CommonError
|
util.CommonError
|
||||||
// scheme 配置
|
// scheme 配置
|
||||||
SchemeInfo SchemeInfo `json:"scheme_info"`
|
SchemeInfo SchemeInfo `json:"scheme_info"`
|
||||||
// 访问该链接的openid,没有用户访问过则为空字符串
|
// 访问该链接的openid,没有用户访问过则为空字符串
|
||||||
@@ -62,9 +62,5 @@ func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo Schem
|
|||||||
// 使用通用方法返回错误
|
// 使用通用方法返回错误
|
||||||
var res resQueryScheme
|
var res resQueryScheme
|
||||||
err = util.DecodeWithError(response, &res, "QueryScheme")
|
err = util.DecodeWithError(response, &res, "QueryScheme")
|
||||||
if err != nil {
|
return res.SchemeInfo, res.VisitOpenid, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.SchemeInfo, res.VisitOpenid, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,5 @@ func (u *URLScheme) Generate(params *USParams) (string, error) {
|
|||||||
}
|
}
|
||||||
var resp USResult
|
var resp USResult
|
||||||
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
||||||
if err != nil {
|
return resp.OpenLink, err
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return resp.OpenLink, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ func (basic *Basic) Long2ShortURL(longURL string) (shortURL string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = util.DecodeWithError(responseBytes, resp, long2shortAction); err != nil {
|
err = util.DecodeWithError(responseBytes, resp, long2shortAction)
|
||||||
return
|
return resp.ShortURL, err
|
||||||
}
|
|
||||||
shortURL = resp.ShortURL
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ type sendRequest struct {
|
|||||||
Mpnews map[string]interface{} `json:"mpnews,omitempty"`
|
Mpnews map[string]interface{} `json:"mpnews,omitempty"`
|
||||||
// 发送语音
|
// 发送语音
|
||||||
Voice map[string]interface{} `json:"voice,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"`
|
Images *Image `json:"images,omitempty"`
|
||||||
// 发送卡券
|
// 发送卡券
|
||||||
@@ -183,7 +187,13 @@ func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error
|
|||||||
ToUser: nil,
|
ToUser: nil,
|
||||||
MsgType: MsgTypeImage,
|
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)
|
req, sendURL := broadcast.chooseTagOrOpenID(user, req)
|
||||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||||
data, err := util.PostJSON(url, req)
|
data, err := util.PostJSON(url, req)
|
||||||
@@ -205,7 +215,7 @@ func (broadcast *Broadcast) SendVideo(user *User, mediaID string, title, descrip
|
|||||||
ToUser: nil,
|
ToUser: nil,
|
||||||
MsgType: MsgTypeVideo,
|
MsgType: MsgTypeVideo,
|
||||||
}
|
}
|
||||||
req.Voice = map[string]interface{}{
|
req.Mpvideo = map[string]interface{}{
|
||||||
"media_id": mediaID,
|
"media_id": mediaID,
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
|
|||||||
@@ -72,11 +72,7 @@ func (csm *Manager) List() (customerServiceList []*KeFuInfo, err error) {
|
|||||||
}
|
}
|
||||||
var res resKeFuList
|
var res resKeFuList
|
||||||
err = util.DecodeWithError(response, &res, "ListCustomerService")
|
err = util.DecodeWithError(response, &res, "ListCustomerService")
|
||||||
if err != nil {
|
return res.KfList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
customerServiceList = res.KfList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeFuOnlineInfo 客服在线信息
|
// KeFuOnlineInfo 客服在线信息
|
||||||
@@ -107,11 +103,7 @@ func (csm *Manager) OnlineList() (customerServiceOnlineList []*KeFuOnlineInfo, e
|
|||||||
}
|
}
|
||||||
var res resKeFuOnlineList
|
var res resKeFuOnlineList
|
||||||
err = util.DecodeWithError(response, &res, "ListOnlineCustomerService")
|
err = util.DecodeWithError(response, &res, "ListOnlineCustomerService")
|
||||||
if err != nil {
|
return res.KfOnlineList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
customerServiceOnlineList = res.KfOnlineList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 添加客服账号
|
// Add 添加客服账号
|
||||||
|
|||||||
@@ -183,9 +183,6 @@ func (cube *DataCube) fetchData(params ParamsPublisher) (response []byte, err er
|
|||||||
uri := fmt.Sprintf("%s?%s", publisherURL, v.Encode())
|
uri := fmt.Sprintf("%s?%s", publisherURL, v.Encode())
|
||||||
|
|
||||||
response, err = util.HTTPGet(uri)
|
response, err = util.HTTPGet(uri)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,11 +64,7 @@ func (draft *Draft) AddDraft(articles []*Article) (mediaID string, err error) {
|
|||||||
MediaID string `json:"media_id"`
|
MediaID string `json:"media_id"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "AddDraft")
|
err = util.DecodeWithError(response, &res, "AddDraft")
|
||||||
if err != nil {
|
return res.MediaID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
mediaID = res.MediaID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDraft 获取草稿
|
// GetDraft 获取草稿
|
||||||
@@ -94,12 +90,7 @@ func (draft *Draft) GetDraft(mediaID string) (articles []*Article, err error) {
|
|||||||
NewsItem []*Article `json:"news_item"`
|
NewsItem []*Article `json:"news_item"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "GetDraft")
|
err = util.DecodeWithError(response, &res, "GetDraft")
|
||||||
if err != nil {
|
return res.NewsItem, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
articles = res.NewsItem
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDraft 删除草稿
|
// DeleteDraft 删除草稿
|
||||||
@@ -172,12 +163,7 @@ func (draft *Draft) CountDraft() (total uint, err error) {
|
|||||||
Total uint `json:"total_count"`
|
Total uint `json:"total_count"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "CountDraft")
|
err = util.DecodeWithError(response, &res, "CountDraft")
|
||||||
if nil != err {
|
return res.Total, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
total = res.Total
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArticleList 草稿列表
|
// ArticleList 草稿列表
|
||||||
|
|||||||
@@ -73,12 +73,7 @@ func (freePublish *FreePublish) Publish(mediaID string) (publishID int64, err er
|
|||||||
PublishID int64 `json:"publish_id"`
|
PublishID int64 `json:"publish_id"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "SubmitFreePublish")
|
err = util.DecodeWithError(response, &res, "SubmitFreePublish")
|
||||||
if err != nil {
|
return res.PublishID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
publishID = res.PublishID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishStatusList 发布任务状态列表
|
// PublishStatusList 发布任务状态列表
|
||||||
@@ -191,12 +186,7 @@ func (freePublish *FreePublish) First(articleID string) (list []Article, err err
|
|||||||
NewsItem []Article `json:"news_item"`
|
NewsItem []Article `json:"news_item"`
|
||||||
}
|
}
|
||||||
err = util.DecodeWithError(response, &res, "FirstFreePublish")
|
err = util.DecodeWithError(response, &res, "FirstFreePublish")
|
||||||
if err != nil {
|
return res.NewsItem, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
list = res.NewsItem
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArticleList 发布列表
|
// ArticleList 发布列表
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
@@ -160,8 +163,8 @@ type resAddMaterial struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMaterial 上传永久性素材(处理视频需要单独上传)
|
// AddMaterialFromReader 上传永久性素材(处理视频需要单独上传),从 io.Reader 中读取
|
||||||
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
|
func (material *Material) AddMaterialFromReader(mediaType MediaType, filePath string, reader io.Reader) (mediaID string, url string, err error) {
|
||||||
if mediaType == MediaTypeVideo {
|
if mediaType == MediaTypeVideo {
|
||||||
err = errors.New("永久视频素材上传使用 AddVideo 方法")
|
err = errors.New("永久视频素材上传使用 AddVideo 方法")
|
||||||
return
|
return
|
||||||
@@ -173,8 +176,10 @@ func (material *Material) AddMaterial(mediaType MediaType, filename string) (med
|
|||||||
}
|
}
|
||||||
|
|
||||||
uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType)
|
uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType)
|
||||||
|
// 获取文件名
|
||||||
|
filename := path.Base(filePath)
|
||||||
var response []byte
|
var response []byte
|
||||||
response, err = util.PostFile("media", filename, uri)
|
response, err = util.PostFileFromReader("media", filePath, filename, uri, reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -192,13 +197,24 @@ func (material *Material) AddMaterial(mediaType MediaType, filename string) (med
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddMaterial 上传永久性素材(处理视频需要单独上传)
|
||||||
|
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
return material.AddMaterialFromReader(mediaType, filename, f)
|
||||||
|
}
|
||||||
|
|
||||||
type reqVideo struct {
|
type reqVideo struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Introduction string `json:"introduction"`
|
Introduction string `json:"introduction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddVideo 永久视频素材文件上传
|
// AddVideoFromReader 永久视频素材文件上传,从 io.Reader 中读取
|
||||||
func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) {
|
func (material *Material) AddVideoFromReader(filePath, title, introduction string, reader io.Reader) (mediaID string, url string, err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
accessToken, err = material.GetAccessToken()
|
accessToken, err = material.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,16 +232,19 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fileName := path.Base(filePath)
|
||||||
fields := []util.MultipartFormField{
|
fields := []util.MultipartFormField{
|
||||||
{
|
{
|
||||||
IsFile: true,
|
IsFile: true,
|
||||||
Fieldname: "media",
|
Fieldname: "media",
|
||||||
Filename: filename,
|
FilePath: filePath,
|
||||||
|
Filename: fileName,
|
||||||
|
FileReader: reader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IsFile: false,
|
IsFile: false,
|
||||||
Fieldname: "description",
|
Fieldname: "description",
|
||||||
|
Filename: fileName,
|
||||||
Value: fieldValue,
|
Value: fieldValue,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -250,6 +269,17 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddVideo 永久视频素材文件上传
|
||||||
|
func (material *Material) AddVideo(directory, title, introduction string) (mediaID string, url string, err error) {
|
||||||
|
f, err := os.Open(directory)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
return material.AddVideoFromReader(directory, title, introduction, f)
|
||||||
|
}
|
||||||
|
|
||||||
type reqDeleteMaterial struct {
|
type reqDeleteMaterial struct {
|
||||||
MediaID string `json:"media_id"`
|
MediaID string `json:"media_id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package material
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
@@ -62,6 +63,38 @@ func (material *Material) MediaUpload(mediaType MediaType, filename string) (med
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MediaUploadFromReader 临时素材上传
|
||||||
|
func (material *Material) MediaUploadFromReader(mediaType MediaType, filename string, reader io.Reader) (media Media, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = material.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&type=%s", mediaUploadURL, accessToken, mediaType)
|
||||||
|
|
||||||
|
var byteData []byte
|
||||||
|
byteData, err = io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostFileByStream("media", filename, uri, byteData)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &media)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if media.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("MediaUpload error : errcode=%v , errmsg=%v", media.ErrCode, media.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetMediaURL 返回临时素材的下载地址供用户自己处理
|
// GetMediaURL 返回临时素材的下载地址供用户自己处理
|
||||||
// NOTICE: URL 不可公开,因为含access_token 需要立即另存文件
|
// NOTICE: URL 不可公开,因为含access_token 需要立即另存文件
|
||||||
func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) {
|
func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) {
|
||||||
|
|||||||
@@ -90,11 +90,7 @@ func (tpl *Subscribe) List() (templateList []*PrivateSubscribeItem, err error) {
|
|||||||
}
|
}
|
||||||
var res resPrivateSubscribeList
|
var res resPrivateSubscribeList
|
||||||
err = util.DecodeWithError(response, &res, "ListSubscribe")
|
err = util.DecodeWithError(response, &res, "ListSubscribe")
|
||||||
if err != nil {
|
return res.SubscriptionList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
templateList = res.SubscriptionList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type resSubscribeAdd struct {
|
type resSubscribeAdd struct {
|
||||||
@@ -123,11 +119,7 @@ func (tpl *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (temp
|
|||||||
}
|
}
|
||||||
var result resSubscribeAdd
|
var result resSubscribeAdd
|
||||||
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
||||||
if err != nil {
|
return result.TemplateID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
templateID = result.TemplateID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除私有模板
|
// Delete 删除私有模板
|
||||||
@@ -175,11 +167,7 @@ func (tpl *Subscribe) GetCategory() (categoryList []*PublicTemplateCategory, err
|
|||||||
}
|
}
|
||||||
var result resSubscribeCategoryList
|
var result resSubscribeCategoryList
|
||||||
err = util.DecodeWithError(response, &result, "GetCategory")
|
err = util.DecodeWithError(response, &result, "GetCategory")
|
||||||
if err != nil {
|
return result.CategoryList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
categoryList = result.CategoryList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicTemplateKeyWords 模板中的关键词
|
// PublicTemplateKeyWords 模板中的关键词
|
||||||
@@ -210,11 +198,7 @@ func (tpl *Subscribe) GetPubTplKeyWordsByID(titleID string) (keyWordsList []*Pub
|
|||||||
}
|
}
|
||||||
var result resPublicTemplateKeyWordsList
|
var result resPublicTemplateKeyWordsList
|
||||||
err = util.DecodeWithError(response, &result, "GetPublicTemplateKeyWords")
|
err = util.DecodeWithError(response, &result, "GetPublicTemplateKeyWords")
|
||||||
if err != nil {
|
return result.KeyWordsList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
keyWordsList = result.KeyWordsList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicTemplateTitle 类目下的公共模板
|
// PublicTemplateTitle 类目下的公共模板
|
||||||
@@ -246,10 +230,5 @@ func (tpl *Subscribe) GetPublicTemplateTitleList(ids string, start int, limit in
|
|||||||
}
|
}
|
||||||
var result resPublicTemplateTitleList
|
var result resPublicTemplateTitleList
|
||||||
err = util.DecodeWithError(response, &result, "GetPublicTemplateTitle")
|
err = util.DecodeWithError(response, &result, "GetPublicTemplateTitle")
|
||||||
if err != nil {
|
return result.Count, result.TemplateTitleList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
count = result.Count
|
|
||||||
templateTitleList = result.TemplateTitleList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ func (tpl *Template) Send(msg *TemplateMessage) (msgID int64, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uri := fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken)
|
var (
|
||||||
var response []byte
|
uri = fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken)
|
||||||
response, err = util.PostJSON(uri, msg)
|
response []byte
|
||||||
if err != nil {
|
)
|
||||||
|
if response, err = util.PostJSON(uri, msg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var result resTemplateSend
|
var result resTemplateSend
|
||||||
err = json.Unmarshal(response, &result)
|
if err = json.Unmarshal(response, &result); err != nil {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if result.ErrCode != 0 {
|
if result.ErrCode != 0 {
|
||||||
@@ -103,19 +103,16 @@ func (tpl *Template) List() (templateList []*TemplateItem, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uri := fmt.Sprintf("%s?access_token=%s", templateListURL, accessToken)
|
var (
|
||||||
var response []byte
|
uri = fmt.Sprintf("%s?access_token=%s", templateListURL, accessToken)
|
||||||
response, err = util.HTTPGet(uri)
|
response []byte
|
||||||
if err != nil {
|
)
|
||||||
|
if response, err = util.HTTPGet(uri); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var res resTemplateList
|
var res resTemplateList
|
||||||
err = util.DecodeWithError(response, &res, "ListTemplate")
|
err = util.DecodeWithError(response, &res, "ListTemplate")
|
||||||
if err != nil {
|
return res.TemplateList, err
|
||||||
return
|
|
||||||
}
|
|
||||||
templateList = res.TemplateList
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type resTemplateAdd struct {
|
type resTemplateAdd struct {
|
||||||
@@ -125,29 +122,26 @@ type resTemplateAdd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add 添加模板.
|
// Add 添加模板.
|
||||||
func (tpl *Template) Add(shortID string) (templateID string, err error) {
|
func (tpl *Template) Add(shortID string, keyNameList []string) (templateID string, err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
accessToken, err = tpl.GetAccessToken()
|
accessToken, err = tpl.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var msg = struct {
|
var (
|
||||||
ShortID string `json:"template_id_short"`
|
msg = struct {
|
||||||
}{ShortID: shortID}
|
ShortID string `json:"template_id_short"`
|
||||||
uri := fmt.Sprintf("%s?access_token=%s", templateAddURL, accessToken)
|
KeyNameList []string `json:"keyword_name_list"`
|
||||||
var response []byte
|
}{ShortID: shortID, KeyNameList: keyNameList}
|
||||||
response, err = util.PostJSON(uri, msg)
|
uri = fmt.Sprintf("%s?access_token=%s", templateAddURL, accessToken)
|
||||||
if err != nil {
|
response []byte
|
||||||
|
)
|
||||||
|
if response, err = util.PostJSON(uri, msg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var result resTemplateAdd
|
var result resTemplateAdd
|
||||||
err = util.DecodeWithError(response, &result, "AddTemplate")
|
err = util.DecodeWithError(response, &result, "AddTemplate")
|
||||||
if err != nil {
|
return result.TemplateID, err
|
||||||
return
|
|
||||||
}
|
|
||||||
templateID = result.TemplateID
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除私有模板.
|
// Delete 删除私有模板.
|
||||||
@@ -157,14 +151,14 @@ func (tpl *Template) Delete(templateID string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var msg = struct {
|
var (
|
||||||
TemplateID string `json:"template_id"`
|
msg = struct {
|
||||||
}{TemplateID: templateID}
|
TemplateID string `json:"template_id"`
|
||||||
|
}{TemplateID: templateID}
|
||||||
uri := fmt.Sprintf("%s?access_token=%s", templateDelURL, accessToken)
|
uri = fmt.Sprintf("%s?access_token=%s", templateDelURL, accessToken)
|
||||||
var response []byte
|
response []byte
|
||||||
response, err = util.PostJSON(uri, msg)
|
)
|
||||||
if err != nil {
|
if response, err = util.PostJSON(uri, msg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "DeleteTemplate")
|
return util.DecodeWithCommonError(response, "DeleteTemplate")
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ func (user *User) ListChangeOpenIDs(fromAppID string, openIDs ...string) (list *
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = util.DecodeWithError(resp, list, "ListChangeOpenIDs")
|
err = util.DecodeWithError(resp, list, "ListChangeOpenIDs")
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,10 +126,7 @@ func (user *User) GetTag() (tags []*TagInfo, err error) {
|
|||||||
Tags []*TagInfo `json:"tags"`
|
Tags []*TagInfo `json:"tags"`
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(response, &result)
|
err = json.Unmarshal(response, &result)
|
||||||
if err != nil {
|
return result.Tags, err
|
||||||
return
|
|
||||||
}
|
|
||||||
return result.Tags, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenIDListByTag 获取标签下粉丝列表
|
// OpenIDListByTag 获取标签下粉丝列表
|
||||||
@@ -154,9 +151,6 @@ func (user *User) OpenIDListByTag(tagID int32, nextOpenID ...string) (userList *
|
|||||||
}
|
}
|
||||||
userList = new(TagOpenIDList)
|
userList = new(TagOpenIDList)
|
||||||
err = json.Unmarshal(response, &userList)
|
err = json.Unmarshal(response, &userList)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,11 +100,8 @@ func (ctx *Context) GetPreCodeContext(stdCtx context.Context) (string, error) {
|
|||||||
var ret struct {
|
var ret struct {
|
||||||
PreCode string `json:"pre_auth_code"`
|
PreCode string `json:"pre_auth_code"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &ret); err != nil {
|
err = json.Unmarshal(body, &ret)
|
||||||
return "", err
|
return ret.PreCode, err
|
||||||
}
|
|
||||||
|
|
||||||
return ret.PreCode, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPreCode 获取预授权码
|
// GetPreCode 获取预授权码
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo"
|
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 基础信息设置
|
// Basic 基础信息设置
|
||||||
@@ -51,3 +57,181 @@ func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) {
|
|||||||
// TODO
|
// TODO
|
||||||
// func (encryptor *Basic) modifyDomain() {
|
// 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")
|
||||||
|
}
|
||||||
|
|||||||
93
util/http.go
93
util/http.go
@@ -22,6 +22,9 @@ type URIModifier func(uri string) string
|
|||||||
|
|
||||||
var uriModifier URIModifier
|
var uriModifier URIModifier
|
||||||
|
|
||||||
|
// DefaultHTTPClient 默认httpClient
|
||||||
|
var DefaultHTTPClient = http.DefaultClient
|
||||||
|
|
||||||
// SetURIModifier 设置URI修改器
|
// SetURIModifier 设置URI修改器
|
||||||
func SetURIModifier(fn URIModifier) {
|
func SetURIModifier(fn URIModifier) {
|
||||||
uriModifier = fn
|
uriModifier = fn
|
||||||
@@ -41,7 +44,7 @@ func HTTPGetContext(ctx context.Context, uri string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
response, err := http.DefaultClient.Do(request)
|
response, err := DefaultHTTPClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,7 @@ func HTTPPostContext(ctx context.Context, uri string, data []byte, header map[st
|
|||||||
request.Header.Set(key, value)
|
request.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
response, err := DefaultHTTPClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -102,7 +105,7 @@ func PostJSONContext(ctx context.Context, uri string, obj interface{}) ([]byte,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json;charset=utf-8")
|
req.Header.Set("Content-Type", "application/json;charset=utf-8")
|
||||||
response, err := http.DefaultClient.Do(req)
|
response, err := DefaultHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -129,7 +132,7 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e
|
|||||||
return nil, "", err
|
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 {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -143,13 +146,40 @@ func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, e
|
|||||||
return responseData, contentType, err
|
return responseData, contentType, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostFileByStream 上传文件
|
||||||
|
func PostFileByStream(fieldName, fileName, uri string, byteData []byte) ([]byte, error) {
|
||||||
|
fields := []MultipartFormField{
|
||||||
|
{
|
||||||
|
IsFile: false,
|
||||||
|
Fieldname: fieldName,
|
||||||
|
Filename: fileName,
|
||||||
|
Value: byteData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return PostMultipartForm(fields, uri)
|
||||||
|
}
|
||||||
|
|
||||||
// PostFile 上传文件
|
// PostFile 上传文件
|
||||||
func PostFile(fieldName, filename, uri string) ([]byte, error) {
|
func PostFile(fieldName, filePath, uri string) ([]byte, error) {
|
||||||
fields := []MultipartFormField{
|
fields := []MultipartFormField{
|
||||||
{
|
{
|
||||||
IsFile: true,
|
IsFile: true,
|
||||||
Fieldname: fieldName,
|
Fieldname: fieldName,
|
||||||
Filename: filename,
|
FilePath: filePath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return PostMultipartForm(fields, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostFileFromReader 上传文件,从 io.Reader 中读取
|
||||||
|
func PostFileFromReader(filedName, filePath, fileName, uri string, reader io.Reader) ([]byte, error) {
|
||||||
|
fields := []MultipartFormField{
|
||||||
|
{
|
||||||
|
IsFile: true,
|
||||||
|
Fieldname: filedName,
|
||||||
|
FilePath: filePath,
|
||||||
|
Filename: fileName,
|
||||||
|
FileReader: reader,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return PostMultipartForm(fields, uri)
|
return PostMultipartForm(fields, uri)
|
||||||
@@ -157,10 +187,12 @@ func PostFile(fieldName, filename, uri string) ([]byte, error) {
|
|||||||
|
|
||||||
// MultipartFormField 保存文件或其他字段信息
|
// MultipartFormField 保存文件或其他字段信息
|
||||||
type MultipartFormField struct {
|
type MultipartFormField struct {
|
||||||
IsFile bool
|
IsFile bool
|
||||||
Fieldname string
|
Fieldname string
|
||||||
Value []byte
|
Value []byte
|
||||||
Filename string
|
FilePath string
|
||||||
|
Filename string
|
||||||
|
FileReader io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostMultipartForm 上传文件或其他多个字段
|
// PostMultipartForm 上传文件或其他多个字段
|
||||||
@@ -179,18 +211,24 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fh, e := os.Open(field.Filename)
|
if field.FileReader == nil {
|
||||||
if e != nil {
|
fh, e := os.Open(field.FilePath)
|
||||||
err = fmt.Errorf("error opening file , err=%v", e)
|
if e != nil {
|
||||||
return
|
err = fmt.Errorf("error opening file , err=%v", e)
|
||||||
}
|
return
|
||||||
defer fh.Close()
|
}
|
||||||
|
_, err = io.Copy(fileWriter, fh)
|
||||||
if _, err = io.Copy(fileWriter, fh); err != nil {
|
_ = fh.Close()
|
||||||
return
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err = io.Copy(fileWriter, field.FileReader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
partWriter, e := bodyWriter.CreateFormField(field.Fieldname)
|
partWriter, e := bodyWriter.CreateFormFile(field.Fieldname, field.Filename)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
err = e
|
err = e
|
||||||
return
|
return
|
||||||
@@ -205,14 +243,14 @@ func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte
|
|||||||
contentType := bodyWriter.FormDataContentType()
|
contentType := bodyWriter.FormDataContentType()
|
||||||
bodyWriter.Close()
|
bodyWriter.Close()
|
||||||
|
|
||||||
resp, e := http.Post(uri, contentType, bodyBuf)
|
resp, e := DefaultHTTPClient.Post(uri, contentType, bodyBuf)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
err = e
|
err = e
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, err
|
return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, resp.StatusCode)
|
||||||
}
|
}
|
||||||
respBody, err = io.ReadAll(resp.Body)
|
respBody, err = io.ReadAll(resp.Body)
|
||||||
return
|
return
|
||||||
@@ -229,7 +267,7 @@ func PostXML(uri string, obj interface{}) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body := bytes.NewBuffer(xmlData)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -252,11 +290,10 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
|
|||||||
config := &tls.Config{
|
config := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
tr := &http.Transport{
|
trans := (DefaultHTTPClient.Transport.(*http.Transport)).Clone()
|
||||||
TLSClientConfig: config,
|
trans.TLSClientConfig = config
|
||||||
DisableCompression: true,
|
trans.DisableCompression = true
|
||||||
}
|
client = &http.Client{Transport: trans}
|
||||||
client = &http.Client{Transport: tr}
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,3 +17,16 @@ func Signature(params ...string) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CalSignature(params ...string) string {
|
||||||
|
sort.Strings(params)
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, value := range params {
|
||||||
|
buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := sha1.New()
|
||||||
|
sha.Write(buffer.Bytes())
|
||||||
|
signature := fmt.Sprintf("%x", sha.Sum(nil))
|
||||||
|
return string(signature)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package wechat
|
package wechat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
openConfig "github.com/silenceper/wechat/v2/openplatform/config"
|
openConfig "github.com/silenceper/wechat/v2/openplatform/config"
|
||||||
"github.com/silenceper/wechat/v2/pay"
|
"github.com/silenceper/wechat/v2/pay"
|
||||||
payConfig "github.com/silenceper/wechat/v2/pay/config"
|
payConfig "github.com/silenceper/wechat/v2/pay/config"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
"github.com/silenceper/wechat/v2/work"
|
"github.com/silenceper/wechat/v2/work"
|
||||||
workConfig "github.com/silenceper/wechat/v2/work/config"
|
workConfig "github.com/silenceper/wechat/v2/work/config"
|
||||||
)
|
)
|
||||||
@@ -81,3 +83,8 @@ func (wc *Wechat) GetWork(cfg *workConfig.Config) *work.Work {
|
|||||||
}
|
}
|
||||||
return work.NewWork(cfg)
|
return work.NewWork(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHTTPClient 设置HTTPClient
|
||||||
|
func (wc *Wechat) SetHTTPClient(client *http.Client) {
|
||||||
|
util.DefaultHTTPClient = client
|
||||||
|
}
|
||||||
|
|||||||
387
work/checkin/checkin.go
Normal file
387
work/checkin/checkin.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
90
work/externalcontact/add_msg_template.go
Normal file
90
work/externalcontact/add_msg_template.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package externalcontact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
addMsgTemplateUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatTypeSingle ChatType = "single"
|
||||||
|
ChatTypeGroup ChatType = "group"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReqMessage 企业群发参数
|
||||||
|
type ReqMessage struct {
|
||||||
|
ChatType ChatType `json:"chat_type"` //群发任务的类型,默认为single,表示发送给客户,group表示发送给客户群
|
||||||
|
ExternalUserid []string `json:"external_userid"` // 客户的外部联系人id列表,仅在chat_type为single时有效,不可与sender同时为空,最多可传入1万个客户
|
||||||
|
Sender string `json:"sender"` //发送企业群发消息的成员userid,当类型为发送给客户群时必填
|
||||||
|
Text struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"text"`
|
||||||
|
Attachments []struct {
|
||||||
|
Msgtype string `json:"msgtype"`
|
||||||
|
Image MsgImage `json:"image"`
|
||||||
|
Link MsgLink `json:"link"`
|
||||||
|
Miniprogram MsgMiniprogram `json:"miniprogram"`
|
||||||
|
Video MsgVideo `json:"video"`
|
||||||
|
File MsgFile `json:"file"`
|
||||||
|
} `json:"attachments"`
|
||||||
|
}
|
||||||
|
type MsgImage struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
PicUrl string `json:"pic_url"`
|
||||||
|
}
|
||||||
|
type MsgLink struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Picurl string `json:"picurl"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
type MsgMiniprogram struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
PicMediaId string `json:"pic_media_id"`
|
||||||
|
Appid string `json:"appid"`
|
||||||
|
Page string `json:"page"`
|
||||||
|
}
|
||||||
|
type MsgVideo struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
}
|
||||||
|
type MsgFile struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resTemplateSend struct {
|
||||||
|
util.CommonError
|
||||||
|
FailList string `json:"fail_list"`
|
||||||
|
MsgID int64 `json:"msgid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 发送应用消息
|
||||||
|
func (r *Client) Send(msg *ReqMessage) (msgID int64, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", addMsgTemplateUrl, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resTemplateSend
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgID = result.MsgID
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -63,16 +63,16 @@ type ExternalUserDetailResponse struct {
|
|||||||
|
|
||||||
// ExternalUser 外部联系人
|
// ExternalUser 外部联系人
|
||||||
type ExternalUser struct {
|
type ExternalUser struct {
|
||||||
ExternalUserID string `json:"external_userid"`
|
ExternalUserID string `json:"external_userid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Type int64 `json:"type"`
|
Type int64 `json:"type"`
|
||||||
Gender int64 `json:"gender"`
|
Gender int64 `json:"gender"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
Position string `json:"position"`
|
Position string `json:"position"`
|
||||||
CorpName string `json:"corp_name"`
|
CorpName string `json:"corp_name"`
|
||||||
CorpFullName string `json:"corp_full_name"`
|
CorpFullName string `json:"corp_full_name"`
|
||||||
ExternalProfile string `json:"external_profile"`
|
ExternalProfile *ExternalProfile `json:"external_profile,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FollowUser 跟进用户(指企业内部用户)
|
// FollowUser 跟进用户(指企业内部用户)
|
||||||
@@ -104,6 +104,46 @@ type WechatChannel struct {
|
|||||||
Source int `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 获取外部联系人详情
|
// GetExternalUserDetail 获取外部联系人详情
|
||||||
// @see https://developer.work.weixin.qq.com/document/path/92114
|
// @see https://developer.work.weixin.qq.com/document/path/92114
|
||||||
func (r *Client) GetExternalUserDetail(externalUserID string, nextCursor ...string) (*ExternalUserDetailResponse, error) {
|
func (r *Client) GetExternalUserDetail(externalUserID string, nextCursor ...string) (*ExternalUserDetailResponse, error) {
|
||||||
|
|||||||
@@ -76,13 +76,14 @@ type (
|
|||||||
}
|
}
|
||||||
//GroupChat 客户群详情
|
//GroupChat 客户群详情
|
||||||
GroupChat struct {
|
GroupChat struct {
|
||||||
ChatID string `json:"chat_id"` //客户群ID
|
ChatID string `json:"chat_id"` //客户群ID
|
||||||
Name string `json:"name"` //群名
|
Name string `json:"name"` //群名
|
||||||
Owner string `json:"owner"` //群主ID
|
Owner string `json:"owner"` //群主ID
|
||||||
CreateTime int64 `json:"create_time"` //群的创建时间
|
CreateTime int64 `json:"create_time"` //群的创建时间
|
||||||
Notice string `json:"notice"` //群公告
|
Notice string `json:"notice"` //群公告
|
||||||
MemberList []GroupChatMember `json:"member_list"` //群成员列表
|
MemberList []GroupChatMember `json:"member_list"` //群成员列表
|
||||||
AdminList []GroupChatAdmin `json:"admin_list"` //群管理员列表
|
AdminList []GroupChatAdmin `json:"admin_list"` //群管理员列表
|
||||||
|
MemberVersion string `json:"member_version"` //当前群成员版本号。可以配合客户群变更事件减少主动调用本接口的次数
|
||||||
}
|
}
|
||||||
//GroupChatDetailResponse 客户群详情 返回值
|
//GroupChatDetailResponse 客户群详情 返回值
|
||||||
GroupChatDetailResponse struct {
|
GroupChatDetailResponse struct {
|
||||||
|
|||||||
162
work/externalcontact/user.go
Normal file
162
work/externalcontact/user.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package externalcontact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
listUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list"
|
||||||
|
getUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get"
|
||||||
|
getByUserBatchUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReqGetByUser struct {
|
||||||
|
UseridList []string `json:"userid_list"`
|
||||||
|
Cursor string `json:"cursor"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
type OneUser struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalContact ExternalContact `json:"external_contact"`
|
||||||
|
FollowUser []FollowInfo `json:"follow_user"` //注意,仅获取单个客户详情的时候这里返回的是跟进人列表
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
type resUserList struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalContactList []UserInfo `json:"external_contact_list"`
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
type resUserids struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalUserid []string `json:"external_userid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
ExternalContact ExternalContact `json:"external_contact"`
|
||||||
|
FollowInfo FollowInfo `json:"follow_info"` //企业成员客户跟进人信息,可以参考获取客户详情,但标签信息只会返回企业标签和规则组标签的tag_id,个人标签将不再返回
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseridList 获取我的客户列表
|
||||||
|
func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&userid=%s", listUrl, accessToken, myUserid)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resUserids
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
externalUserid = result.ExternalUserid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseridList 获取我的全部客户列表及详情
|
||||||
|
func (tpl *Client) GetQyUserInfoList(qyUserid []string) ([]UserInfo, error) {
|
||||||
|
var userInfoList []UserInfo
|
||||||
|
var req ReqGetByUser
|
||||||
|
req.UseridList = qyUserid
|
||||||
|
req.Limit = 100
|
||||||
|
for {
|
||||||
|
userInfoPage, resCursor, err := tpl.GetUserInfoListByUserIds(req)
|
||||||
|
if err != nil {
|
||||||
|
return userInfoList, err
|
||||||
|
}
|
||||||
|
userInfoList = append(userInfoList, userInfoPage...)
|
||||||
|
if resCursor != "" {
|
||||||
|
req.Cursor = resCursor
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userInfoList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoAndAllFollow 获取客户详情以及全部跟进人
|
||||||
|
func (tpl *Client) GetUserInfoAndAllFollow(userid string) (OneUser, error) {
|
||||||
|
var result, res OneUser
|
||||||
|
var err error
|
||||||
|
var cursor string
|
||||||
|
for {
|
||||||
|
res, err = tpl.GetUserInfo(userid, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result.FollowUser = append(result.FollowUser, res.FollowUser...)
|
||||||
|
result.ExternalContact = res.ExternalContact
|
||||||
|
if res.NextCursor != "" {
|
||||||
|
cursor = res.NextCursor
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取客户详情
|
||||||
|
func (tpl *Client) GetUserInfo(externalUserid string, cursor ...string) (result OneUser, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var page = ""
|
||||||
|
if len(cursor) > 0 {
|
||||||
|
page = cursor[0]
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&external_userid=%s&cursor=%s", getUrl, accessToken, externalUserid, page)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoListByUserId 批量获取客户详情
|
||||||
|
func (tpl *Client) GetUserInfoListByUserIds(req ReqGetByUser) (userList []UserInfo, nextCursor string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", getByUserBatchUrl, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resUserList
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userList = result.ExternalContactList
|
||||||
|
nextCursor = result.NextCursor
|
||||||
|
return
|
||||||
|
}
|
||||||
93
work/js/js.go
Normal file
93
work/js/js.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Js struct
|
||||||
|
type Js struct {
|
||||||
|
*context.Context
|
||||||
|
credential.JsTicketHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 返回给用户jssdk配置信息
|
||||||
|
type Config struct {
|
||||||
|
CorpID string `json:"corp_id"`
|
||||||
|
Agentid string `json:"agentid"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
NonceStr string `json:"nonce_str"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewJs init
|
||||||
|
func NewJs(context *context.Context) *Js {
|
||||||
|
js := new(Js)
|
||||||
|
js.Context = context
|
||||||
|
jsTicketHandle := credential.NewWorkJsTicket(context.CorpID, context.AgentID, credential.CacheKeyWorkPrefix, context.Cache)
|
||||||
|
js.SetJsTicketHandle(jsTicketHandle)
|
||||||
|
return js
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetJsTicketHandle 自定义js ticket取值方式
|
||||||
|
func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
||||||
|
js.JsTicketHandle = ticketHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetConfig 获取jssdk需要的配置参数
|
||||||
|
//uri 为当前网页地址
|
||||||
|
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||||
|
config = new(Config)
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = js.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ticketStr string
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceStr := util.RandomStr(16)
|
||||||
|
timestamp := util.GetCurrTS()
|
||||||
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
|
config.CorpID = js.CorpID
|
||||||
|
config.Agentid = js.AgentID
|
||||||
|
config.NonceStr = nonceStr
|
||||||
|
config.Timestamp = timestamp
|
||||||
|
config.Signature = sigStr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetAgentConfig 获取jssdk需要的配置参数
|
||||||
|
//uri 为当前网页地址
|
||||||
|
func (js *Js) GetAgentConfig(uri string) (config *Config, err error) {
|
||||||
|
config = new(Config)
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = js.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ticketStr string
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceStr := util.RandomStr(16)
|
||||||
|
timestamp := util.GetCurrTS()
|
||||||
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
|
config.CorpID = js.CorpID
|
||||||
|
config.NonceStr = nonceStr
|
||||||
|
config.Timestamp = timestamp
|
||||||
|
config.Signature = sigStr
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -18,20 +18,23 @@ const (
|
|||||||
|
|
||||||
// ReceptionistOptions 添加接待人员请求参数
|
// ReceptionistOptions 添加接待人员请求参数
|
||||||
type ReceptionistOptions struct {
|
type ReceptionistOptions struct {
|
||||||
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
||||||
UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。
|
UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。
|
||||||
|
DepartmentIDList []int `json:"department_id_list"` // 接待人员部门id列表 可填充个数:0 ~ 100。超过100个需分批调用。
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceptionistSchema 添加接待人员响应内容
|
// ReceptionistSchema 添加接待人员响应内容
|
||||||
type ReceptionistSchema struct {
|
type ReceptionistSchema struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
ResultList []struct {
|
ResultList []struct {
|
||||||
UserID string `json:"userid"`
|
UserID string `json:"userid"`
|
||||||
|
DepartmentID int `json:"department_id"`
|
||||||
util.CommonError
|
util.CommonError
|
||||||
} `json:"result_list"`
|
} `json:"result_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceptionistAdd 添加接待人员
|
// ReceptionistAdd 添加接待人员
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/94646
|
||||||
func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) {
|
func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info ReceptionistSchema, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -49,10 +52,11 @@ func (r *Client) ReceptionistAdd(options ReceptionistOptions) (info Receptionist
|
|||||||
if info.ErrCode != 0 {
|
if info.ErrCode != 0 {
|
||||||
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
||||||
}
|
}
|
||||||
return info, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceptionistDel 删除接待人员
|
// ReceptionistDel 删除接待人员
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/94647
|
||||||
func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) {
|
func (r *Client) ReceptionistDel(options ReceptionistOptions) (info ReceptionistSchema, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -72,19 +76,22 @@ func (r *Client) ReceptionistDel(options ReceptionistOptions) (info Receptionist
|
|||||||
if info.ErrCode != 0 {
|
if info.ErrCode != 0 {
|
||||||
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
||||||
}
|
}
|
||||||
return info, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceptionistListSchema 获取接待人员列表响应内容
|
// ReceptionistListSchema 获取接待人员列表响应内容
|
||||||
type ReceptionistListSchema struct {
|
type ReceptionistListSchema struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
ReceptionistList []struct {
|
ReceptionistList []struct {
|
||||||
UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid,即open_userid
|
UserID string `json:"userid"` // 接待人员的userid。第三方应用获取到的为密文userid,即open_userid
|
||||||
Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取
|
Status int `json:"status"` // 接待人员的接待状态。0:接待中,1:停止接待。第三方应用需具有“管理帐号、分配会话和收发消息”权限才可获取
|
||||||
|
DepartmentID int `json:"department_id"` // 接待人员部门的id
|
||||||
|
StopType int `json:"stop_type"` // 接待人员的接待状态为「停止接待」的子类型。0:停止接待,1:暂时挂起
|
||||||
} `json:"servicer_list"`
|
} `json:"servicer_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceptionistList 获取接待人员列表
|
// ReceptionistList 获取接待人员列表
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/94645
|
||||||
func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) {
|
func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -104,5 +111,5 @@ func (r *Client) ReceptionistList(kfID string) (info ReceptionistListSchema, err
|
|||||||
if info.ErrCode != 0 {
|
if info.ErrCode != 0 {
|
||||||
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
||||||
}
|
}
|
||||||
return info, nil
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package material
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
@@ -13,6 +14,8 @@ const (
|
|||||||
uploadTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
|
uploadTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
|
||||||
// uploadAttachment 上传附件资源
|
// uploadAttachment 上传附件资源
|
||||||
uploadAttachment = "https://qyapi.weixin.qq.com/cgi-bin/media/upload_attachment?access_token=%s&media_type=%s&attachment_type=%d"
|
uploadAttachment = "https://qyapi.weixin.qq.com/cgi-bin/media/upload_attachment?access_token=%s&media_type=%s&attachment_type=%d"
|
||||||
|
// getTempFile 获取临时素材
|
||||||
|
getTempFile = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadImgResponse 上传图片响应
|
// UploadImgResponse 上传图片响应
|
||||||
@@ -96,3 +99,72 @@ func (r *Client) UploadAttachment(filename string, mediaType string, attachmentT
|
|||||||
err = util.DecodeWithError(response, result, "UploadAttachment")
|
err = util.DecodeWithError(response, result, "UploadAttachment")
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadTempFileFromReader 上传临时素材
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/90253
|
||||||
|
// @mediaType 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file)
|
||||||
|
func (r *Client) UploadTempFileFromReader(filename, mediaType string, reader io.Reader) (*UploadTempFileResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var byteData []byte
|
||||||
|
byteData, err = io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostFileByStream("media", filename, fmt.Sprintf(uploadTempFile, accessToken, mediaType), byteData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &UploadTempFileResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "UploadTempFile")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadAttachmentFromReader 上传附件资源
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/95098
|
||||||
|
// @mediaType 媒体文件类型,分别有图片(image)、视频(video)、普通文件(file)
|
||||||
|
// @attachment_type 附件类型,不同的附件类型用于不同的场景。1:朋友圈;2:商品图册
|
||||||
|
func (r *Client) UploadAttachmentFromReader(filename, mediaType string, reader io.Reader, attachmentType int) (*UploadAttachmentResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var byteData []byte
|
||||||
|
byteData, err = io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostFileByStream("media", filename, fmt.Sprintf(uploadAttachment, accessToken, mediaType, attachmentType), byteData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &UploadAttachmentResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "UploadAttachment")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTempFile 获取临时素材
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/90254
|
||||||
|
func (r *Client) GetTempFile(mediaID string) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf(getTempFile, accessToken, mediaID)
|
||||||
|
response, err := util.HTTPGet(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|||||||
0
work/message/README.md
Normal file
0
work/message/README.md
Normal file
1
work/message/group.go
Normal file
1
work/message/group.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package message
|
||||||
16
work/message/image.go
Normal file
16
work/message/image.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Image 图片消息
|
||||||
|
type Image struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Image struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
} `xml:"Image" json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewImage 回复图片消息
|
||||||
|
func NewImage(mediaID string) *Image {
|
||||||
|
image := new(Image)
|
||||||
|
image.Image.MediaID = mediaID
|
||||||
|
return image
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ type (
|
|||||||
// 消息类型,此时固定为:text
|
// 消息类型,此时固定为:text
|
||||||
MsgType string `json:"msgtype"`
|
MsgType string `json:"msgtype"`
|
||||||
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
||||||
AgentID string `json:"agentid"`
|
AgentID int `json:"agentid"`
|
||||||
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
||||||
Safe int `json:"safe"`
|
Safe int `json:"safe"`
|
||||||
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
||||||
|
|||||||
285
work/message/mix_message.go
Normal file
285
work/message/mix_message.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
//todo 第三方应用开发
|
||||||
|
/*const (
|
||||||
|
//微信开放平台需要用到
|
||||||
|
|
||||||
|
// InfoTypeVerifyTicket 返回ticket
|
||||||
|
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
|
||||||
|
// InfoTypeAuthorized 授权
|
||||||
|
InfoTypeAuthorized = "authorized"
|
||||||
|
// InfoTypeUnauthorized 取消授权
|
||||||
|
InfoTypeUnauthorized = "unauthorized"
|
||||||
|
// InfoTypeUpdateAuthorized 更新授权
|
||||||
|
InfoTypeUpdateAuthorized = "updateauthorized"
|
||||||
|
)*/
|
||||||
|
|
||||||
|
// MixMessage 存放所有企业微信官方发送过来的消息和事件
|
||||||
|
type MixMessage struct {
|
||||||
|
CommonToken
|
||||||
|
|
||||||
|
//接收普通消息
|
||||||
|
MsgID int64 `xml:"MsgId"` //其他消息推送过来是MsgId
|
||||||
|
AgentID int `xml:"AgentID"` //企业应用的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
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventPic 发图事件推送
|
||||||
|
type EventPic struct {
|
||||||
|
PicMd5Sum string `xml:"PicMd5Sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
41
work/message/news.go
Normal file
41
work/message/news.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//News 图文消息
|
||||||
|
type News struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
ArticleCount int `xml:"ArticleCount" json:"-"`
|
||||||
|
Articles []*Article `xml:"Articles>item,omitempty" json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewNews 初始化图文消息
|
||||||
|
func NewNews(articles []*Article) *News {
|
||||||
|
news := new(News)
|
||||||
|
news.ArticleCount = len(articles)
|
||||||
|
news.Articles = articles
|
||||||
|
return news
|
||||||
|
}
|
||||||
|
|
||||||
|
//Article 单篇文章
|
||||||
|
type Article struct {
|
||||||
|
Title string `xml:"Title,omitempty" json:"title"`
|
||||||
|
Description string `xml:"Description,omitempty" json:"description"`
|
||||||
|
PicURL string `xml:"PicUrl,omitempty" json:"picurl"`
|
||||||
|
URL string `xml:"Url,omitempty" json:"url"`
|
||||||
|
Appid string `xml:"-" json:"appid"` //仅在发送应用消息时需要
|
||||||
|
Pagepath string `xml:"-" json:"pagepath"` //仅在发送应用消息时需要
|
||||||
|
}
|
||||||
|
|
||||||
|
//MpNews 图文消息
|
||||||
|
type MpNews struct {
|
||||||
|
Articles []*MpNewsArticle `xml:"-" json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//MpNewsArticle mpnews类型的图文消息,跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信
|
||||||
|
type MpNewsArticle struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ThumbMediaId string `json:"thumb_media_id"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
ContentSourceUrl string `json:"content_source_url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
}
|
||||||
15
work/message/reply.go
Normal file
15
work/message/reply.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
//ErrInvalidReply 无效的回复
|
||||||
|
var ErrInvalidReply = errors.New("无效的回复消息")
|
||||||
|
|
||||||
|
//ErrUnsupportReply 不支持的回复类型
|
||||||
|
var ErrUnsupportReply = errors.New("无需回复消息")
|
||||||
|
|
||||||
|
//Reply 消息回复
|
||||||
|
type Reply struct {
|
||||||
|
MsgType MsgType
|
||||||
|
MsgData interface{}
|
||||||
|
}
|
||||||
129
work/message/template_card.go
Normal file
129
work/message/template_card.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
messageUpdateTemplateCardURL = "https://api.weixin.qq.com/cgi-bin/message/update_template_card"
|
||||||
|
messageDelURL = "https://api.weixin.qq.com/cgi-bin/message/recall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateButton 模板卡片按钮
|
||||||
|
type UpdateButton struct {
|
||||||
|
//CommonToken `json:"-"`
|
||||||
|
Button struct {
|
||||||
|
ReplaceName string `xml:"ReplaceName" json:"replace_name"`
|
||||||
|
} `xml:"Button" json:"button"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUpdateButton 更新点击用户的按钮文案
|
||||||
|
func NewUpdateButton(replaceName string) *UpdateButton {
|
||||||
|
btn := new(UpdateButton)
|
||||||
|
btn.Button.ReplaceName = replaceName
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateCard 被动回复模板卡片
|
||||||
|
// https://open.work.weixin.qq.com/api/doc/90000/90135/90241
|
||||||
|
type TemplateCard struct {
|
||||||
|
//CommonToken `json:"-"`
|
||||||
|
TemplateCard interface{} `xml:"TemplateCard" json:"template_card"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplateCard 更新点击用户的整张卡片
|
||||||
|
func NewTemplateCard(cardXml interface{}) *TemplateCard {
|
||||||
|
card := new(TemplateCard)
|
||||||
|
card.TemplateCard = cardXml
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushFile struct {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
}
|
||||||
|
type PushTextCard struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Btntxt string `json:"btntxt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resTemplateSend struct {
|
||||||
|
util.CommonError
|
||||||
|
Invaliduser string `json:"invaliduser"` //不合法的userid,不区分大小写,统一转为小写
|
||||||
|
Invalidparty string `json:"invalidparty"` //不合法的partyid
|
||||||
|
Invalidtag string `json:"invalidtag"` //不合法的标签id
|
||||||
|
MsgID string `json:"msgid"` //消息id,用于撤回应用消息
|
||||||
|
ResponseCode string `json:"response_code"` //仅消息类型为“按钮交互型”,“投票选择型”和“多项选择型”的模板卡片消息返回,应用可使用response_code调用更新模版卡片消息接口,24小时内有效,且只能使用一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateUpdate 更新模版卡片消息内容
|
||||||
|
type TemplateUpdate struct {
|
||||||
|
Userids []string `json:"userids"`
|
||||||
|
Partyids []int `json:"partyids"`
|
||||||
|
Tagids []int `json:"tagids"`
|
||||||
|
Atall int `json:"atall"`
|
||||||
|
Agentid int `json:"agentid"`
|
||||||
|
ResponseCode string `json:"response_code"`
|
||||||
|
*UpdateButton
|
||||||
|
*TemplateCard
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTemplate 更新模版卡片消息
|
||||||
|
func (r *Client) UpdateTemplate(msg *TemplateUpdate) (msgID string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", messageUpdateTemplateCardURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resTemplateSend
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgID = result.MsgID
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReqRecall struct {
|
||||||
|
MsgID int64 `json:"msgid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recall 撤回应用消息
|
||||||
|
func (r *Client) Recall(msgID int64) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", messageDelURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, &ReqRecall{
|
||||||
|
MsgID: msgID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result util.CommonError
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
14
work/message/text.go
Normal file
14
work/message/text.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Text 文本消息
|
||||||
|
type Text struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Content CDATA `json:"content" xml:"Content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewText 初始化文本消息
|
||||||
|
func NewText(content string) *Text {
|
||||||
|
text := new(Text)
|
||||||
|
text.Content = CDATA(content)
|
||||||
|
return text
|
||||||
|
}
|
||||||
20
work/message/video.go
Normal file
20
work/message/video.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Video 视频消息
|
||||||
|
type Video struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Video struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
Title string `xml:"Title,omitempty" json:"title"`
|
||||||
|
Description string `xml:"Description,omitempty" json:"description"`
|
||||||
|
} `xml:"Video" json:"video"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewVideo 回复图片消息
|
||||||
|
func NewVideo(mediaID, title, description string) *Video {
|
||||||
|
video := new(Video)
|
||||||
|
video.Video.MediaID = mediaID
|
||||||
|
video.Video.Title = title
|
||||||
|
video.Video.Description = description
|
||||||
|
return video
|
||||||
|
}
|
||||||
16
work/message/voice.go
Normal file
16
work/message/voice.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Voice 语音消息
|
||||||
|
type Voice struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Voice struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
} `xml:"Voice" json:"voice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewVoice 回复语音消息
|
||||||
|
func NewVoice(mediaID string) *Voice {
|
||||||
|
voice := new(Voice)
|
||||||
|
voice.Voice.MediaID = mediaID
|
||||||
|
return voice
|
||||||
|
}
|
||||||
64
work/oauth/user.go
Normal file
64
work/oauth/user.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
code2SessionURL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code"
|
||||||
|
launchCode = "https://qyapi.weixin.qq.com/cgi-bin/get_launch_code?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ctr *Oauth) Code2Session(code string) (result ResUserInfo, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = ctr.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(
|
||||||
|
fmt.Sprintf(code2SessionURL, accessToken, code),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespLaunchCode struct {
|
||||||
|
util.CommonError
|
||||||
|
LaunchCode string `json:"launch_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLaunchCode 用于打开个人聊天窗口schema
|
||||||
|
func (ctr *Oauth) GetLaunchCode(userID, other string) (userInfo *RespLaunchCode, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = ctr.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(launchCode, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, map[string]interface{}{"operator_userid": userID, "single_chat": map[string]string{"userid": other}})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userInfo = new(RespLaunchCode)
|
||||||
|
err = json.Unmarshal(response, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userInfo.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
66
work/server/error.go
Normal file
66
work/server/error.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error 错误
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SDKValidateSignatureError Error = "签名验证错误" //-40001
|
||||||
|
SDKParseJsonError Error = "xml/json解析失败" //-40002
|
||||||
|
SDKComputeSignatureError Error = "sha加密生成签名失败" //-40003
|
||||||
|
SDKIllegalAesKey Error = "AESKey 非法" //-40004
|
||||||
|
SDKValidateCorpidError Error = "ReceiveId 校验错误" //-40005
|
||||||
|
SDKEncryptAESError Error = "AES 加密失败" //-40006
|
||||||
|
SDKDecryptAESError Error = "AES 解密失败" //-40007
|
||||||
|
SDKIllegalBuffer Error = "解密后得到的buffer非法" //-40008
|
||||||
|
SDKEncodeBase64Error Error = "base64加密失败" //-40009
|
||||||
|
SDKDecodeBase64Error Error = "base64解密失败" //-40010
|
||||||
|
SDKGenJsonError Error = "生成xml/json失败" //-40011
|
||||||
|
SDKIllegalProtocolType Error = "协议类型非法" //-40012
|
||||||
|
SDKUnknownError Error = "未知错误"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Error 输出错误信息
|
||||||
|
func (r Error) Error() string {
|
||||||
|
return reflect.ValueOf(r).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSDKErr 初始化SDK实例错误信息
|
||||||
|
func NewSDKErr(code int64, msgList ...string) Error {
|
||||||
|
switch code {
|
||||||
|
case 40001:
|
||||||
|
return SDKValidateSignatureError
|
||||||
|
case 40002:
|
||||||
|
return SDKParseJsonError
|
||||||
|
case 40003:
|
||||||
|
return SDKComputeSignatureError
|
||||||
|
case 40004:
|
||||||
|
return SDKIllegalAesKey
|
||||||
|
case 40005:
|
||||||
|
return SDKValidateCorpidError
|
||||||
|
case 40006:
|
||||||
|
return SDKEncryptAESError
|
||||||
|
case 40007:
|
||||||
|
return SDKDecryptAESError
|
||||||
|
case 40008:
|
||||||
|
return SDKIllegalBuffer
|
||||||
|
case 40009:
|
||||||
|
return SDKEncodeBase64Error
|
||||||
|
case 40010:
|
||||||
|
return SDKDecodeBase64Error
|
||||||
|
case 40011:
|
||||||
|
return SDKGenJsonError
|
||||||
|
case 40012:
|
||||||
|
return SDKIllegalProtocolType
|
||||||
|
default:
|
||||||
|
//返回未知的自定义错误
|
||||||
|
if len(msgList) > 0 {
|
||||||
|
return Error(strings.Join(msgList, ","))
|
||||||
|
}
|
||||||
|
return SDKUnknownError
|
||||||
|
}
|
||||||
|
}
|
||||||
228
work/server/server.go
Normal file
228
work/server/server.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
"github.com/silenceper/wechat/v2/work/message"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Server struct
|
||||||
|
type Server struct {
|
||||||
|
*context.Context
|
||||||
|
Writer http.ResponseWriter
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
|
skipValidate bool
|
||||||
|
|
||||||
|
messageHandler func(*message.MixMessage) *message.Reply
|
||||||
|
|
||||||
|
RequestRawXMLMsg []byte
|
||||||
|
RequestMsg *message.MixMessage
|
||||||
|
ResponseRawXMLMsg []byte
|
||||||
|
ResponseMsg interface{}
|
||||||
|
|
||||||
|
random []byte
|
||||||
|
nonce string
|
||||||
|
timestamp int64
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewServer init
|
||||||
|
func NewServer(context *context.Context) *Server {
|
||||||
|
srv := new(Server)
|
||||||
|
srv.Context = context
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) VerifyURL() (string, error) {
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
signature := srv.Query("msg_signature")
|
||||||
|
echoStr := srv.Query("echostr")
|
||||||
|
if signature != util.Signature(srv.Token, timestamp, nonce, echoStr) {
|
||||||
|
return "", NewSDKErr(40001)
|
||||||
|
}
|
||||||
|
_, bData, err := util.DecryptMsg(srv.CorpID, echoStr, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", NewSDKErr(40002)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipValidate set skip validate
|
||||||
|
func (srv *Server) SkipValidate(skip bool) {
|
||||||
|
srv.skipValidate = skip
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serve 处理企业微信的请求消息
|
||||||
|
func (srv *Server) Serve() error {
|
||||||
|
response, err := srv.handleRequest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//debug print request msg
|
||||||
|
log.Debugf("request msg =%s", string(srv.RequestRawXMLMsg))
|
||||||
|
return srv.buildResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Validate 校验请求是否合法
|
||||||
|
func (srv *Server) Validate() bool {
|
||||||
|
if srv.skipValidate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
signature := srv.Query("msg_signature")
|
||||||
|
log.Debugf("validate signature, timestamp=%s, nonce=%s", timestamp, nonce)
|
||||||
|
return signature == util.Signature(srv.Token, timestamp, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
//HandleRequest 处理企业微信的请求
|
||||||
|
func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
||||||
|
|
||||||
|
var msg interface{}
|
||||||
|
msg, err = srv.getMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mixMessage, success := msg.(*message.MixMessage)
|
||||||
|
if !success {
|
||||||
|
err = errors.New("消息类型转换失败")
|
||||||
|
}
|
||||||
|
srv.RequestMsg = mixMessage
|
||||||
|
reply = srv.messageHandler(mixMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//getMessage 解析企业微信返回的消息
|
||||||
|
func (srv *Server) getMessage() (interface{}, error) {
|
||||||
|
var rawXMLMsgBytes []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var encryptedXMLMsg message.EncryptedXMLMsg
|
||||||
|
if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("从body中解析xml失败,err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//验证消息签名
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
srv.timestamp, err = strconv.ParseInt(timestamp, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
srv.nonce = nonce
|
||||||
|
msgSignature := srv.Query("msg_signature")
|
||||||
|
msgSignatureGen := util.Signature(srv.Token, timestamp, nonce, encryptedXMLMsg.EncryptedMsg)
|
||||||
|
if msgSignature != msgSignatureGen {
|
||||||
|
return nil, fmt.Errorf("消息不合法,验证签名失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
//解密
|
||||||
|
srv.random, rawXMLMsgBytes, err = util.DecryptMsg(srv.CorpID, encryptedXMLMsg.EncryptedMsg, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("消息解密失败, err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.RequestRawXMLMsg = rawXMLMsgBytes
|
||||||
|
return srv.parseRequestMessage(rawXMLMsgBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixMessage, err error) {
|
||||||
|
msg = &message.MixMessage{}
|
||||||
|
err = xml.Unmarshal(rawXMLMsgBytes, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetMessageHandler 设置用户自定义的回调方法
|
||||||
|
func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) {
|
||||||
|
srv.messageHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) buildResponse(reply *message.Reply) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if reply == nil {
|
||||||
|
//do nothing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msgType := reply.MsgType
|
||||||
|
switch msgType {
|
||||||
|
case message.MsgTypeText:
|
||||||
|
case message.MsgTypeImage:
|
||||||
|
case message.MsgTypeVoice:
|
||||||
|
case message.MsgTypeVideo:
|
||||||
|
case message.MsgTypeNews:
|
||||||
|
case message.MsgTypeUpdateButton:
|
||||||
|
case message.MsgTypeUpdateTemplateCard:
|
||||||
|
default:
|
||||||
|
err = message.ErrUnsupportReply
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgData := reply.MsgData
|
||||||
|
value := reflect.ValueOf(msgData)
|
||||||
|
//msgData must be a ptr
|
||||||
|
kind := value.Kind().String()
|
||||||
|
if kind != "ptr" {
|
||||||
|
return message.ErrUnsupportReply
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make([]reflect.Value, 1)
|
||||||
|
params[0] = reflect.ValueOf(srv.RequestMsg.FromUserName)
|
||||||
|
value.MethodByName("SetToUserName").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(srv.RequestMsg.ToUserName)
|
||||||
|
value.MethodByName("SetFromUserName").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(msgType)
|
||||||
|
value.MethodByName("SetMsgType").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(util.GetCurrTS())
|
||||||
|
value.MethodByName("SetCreateTime").Call(params)
|
||||||
|
srv.ResponseMsg = msgData
|
||||||
|
srv.ResponseRawXMLMsg, err = xml.Marshal(msgData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Send 将自定义的消息发送
|
||||||
|
func (srv *Server) Send() (err error) {
|
||||||
|
replyMsg := srv.ResponseMsg
|
||||||
|
log.Debugf("response msg =%+v", replyMsg)
|
||||||
|
|
||||||
|
//安全模式下对消息进行加密
|
||||||
|
var encryptedMsg []byte
|
||||||
|
encryptedMsg, err = util.EncryptMsg(srv.random, srv.ResponseRawXMLMsg, srv.CorpID, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//TODO 如果获取不到timestamp nonce 则自己生成
|
||||||
|
timestamp := srv.timestamp
|
||||||
|
timestampStr := strconv.FormatInt(timestamp, 10)
|
||||||
|
msgSignature := util.Signature(srv.Token, timestampStr, srv.nonce, string(encryptedMsg))
|
||||||
|
replyMsg = message.ResponseEncryptedXMLMsg{
|
||||||
|
EncryptedMsg: string(encryptedMsg),
|
||||||
|
MsgSignature: msgSignature,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Nonce: srv.nonce,
|
||||||
|
}
|
||||||
|
|
||||||
|
if replyMsg != nil {
|
||||||
|
srv.XML(replyMsg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
58
work/server/util.go
Normal file
58
work/server/util.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||||
|
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||||
|
|
||||||
|
func writeContextType(w http.ResponseWriter, value []string) {
|
||||||
|
header := w.Header()
|
||||||
|
if val := header["Content-Type"]; len(val) == 0 {
|
||||||
|
header["Content-Type"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Render render from bytes
|
||||||
|
func (srv *Server) Render(bytes []byte) {
|
||||||
|
//debug
|
||||||
|
//fmt.Println("response msg = ", string(bytes))
|
||||||
|
srv.Writer.WriteHeader(200)
|
||||||
|
_, err := srv.Writer.Write(bytes)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//String render from string
|
||||||
|
func (srv *Server) String(str string) {
|
||||||
|
writeContextType(srv.Writer, plainContentType)
|
||||||
|
srv.Render([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
//XML render to xml
|
||||||
|
func (srv *Server) XML(obj interface{}) {
|
||||||
|
writeContextType(srv.Writer, xmlContentType)
|
||||||
|
bytes, err := xml.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
srv.Render(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns the keyed url query value if it exists
|
||||||
|
func (srv *Server) Query(key string) string {
|
||||||
|
value, _ := srv.GetQuery(key)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuery is like Query(), it returns the keyed url query value
|
||||||
|
func (srv *Server) GetQuery(key string) (string, bool) {
|
||||||
|
req := srv.Request
|
||||||
|
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
|
||||||
|
return values[0], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
0
work/tools/calendar.go
Normal file
0
work/tools/calendar.go
Normal file
194
work/user/user.go
Normal file
194
work/user/user.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s"
|
||||||
|
updateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%s&fetch_child=1"
|
||||||
|
userListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User 用户管理
|
||||||
|
type User struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUser 实例化
|
||||||
|
func NewUser(context *context.Context) *User {
|
||||||
|
user := new(User)
|
||||||
|
user.Context = context
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info 用户基本信息
|
||||||
|
type Info struct {
|
||||||
|
util.CommonError
|
||||||
|
Userid string `json:"userid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Department []int `json:"department"`
|
||||||
|
Order []int `json:"order"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
Gender string `json:"gender"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
ThumbAvatar string `json:"thumb_avatar"`
|
||||||
|
Telephone string `json:"telephone"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
OpenUserid string `json:"open_userid"`
|
||||||
|
MainDepartment int `json:"main_department"`
|
||||||
|
Extattr struct {
|
||||||
|
Attrs []struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"text,omitempty"`
|
||||||
|
Web struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"web,omitempty"`
|
||||||
|
} `json:"attrs"`
|
||||||
|
} `json:"extattr"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
QrCode string `json:"qr_code"`
|
||||||
|
ExternalPosition string `json:"external_position"`
|
||||||
|
ExternalProfile struct {
|
||||||
|
ExternalCorpName string `json:"external_corp_name"`
|
||||||
|
WechatChannels struct {
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"wechat_channels"`
|
||||||
|
ExternalAttr []struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"text,omitempty"`
|
||||||
|
Web struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"web,omitempty"`
|
||||||
|
Miniprogram struct {
|
||||||
|
Appid string `json:"appid"`
|
||||||
|
Pagepath string `json:"pagepath"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"miniprogram,omitempty"`
|
||||||
|
} `json:"external_attr"`
|
||||||
|
} `json:"external_profile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenidList 用户列表
|
||||||
|
type OpenidList struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
Total int `json:"total"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Data struct {
|
||||||
|
OpenIDs []string `json:"openid"`
|
||||||
|
} `json:"data"`
|
||||||
|
NextOpenID string `json:"next_openid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户基本信息
|
||||||
|
func (user *User) GetUserInfo(userID string) (userInfo *Info, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(userInfoURL, accessToken, userID)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userInfo = new(Info)
|
||||||
|
err = json.Unmarshal(response, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userInfo.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新员工资料
|
||||||
|
func (user *User) Update(userID, external_position string) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(updateURL, accessToken, userID)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, map[string]string{"userid": userID, "external_position": external_position})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.DecodeWithCommonError(response, "updateURL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserOpenIDs 返回用户列表
|
||||||
|
func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) {
|
||||||
|
accessToken, err := user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri, _ := url.Parse(userListURL)
|
||||||
|
q := uri.Query()
|
||||||
|
q.Set("access_token", accessToken)
|
||||||
|
if len(nextOpenid) > 0 && nextOpenid[0] != "" {
|
||||||
|
q.Set("next_openid", nextOpenid[0])
|
||||||
|
}
|
||||||
|
uri.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
response, err := util.HTTPGet(uri.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userlist := OpenidList{}
|
||||||
|
|
||||||
|
err = util.DecodeWithError(response, &userlist, "ListUserOpenIDs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllUserOpenIDs 返回所有用户OpenID列表
|
||||||
|
func (user *User) ListAllUserOpenIDs() ([]string, error) {
|
||||||
|
nextOpenid := ""
|
||||||
|
openids := make([]string, 0)
|
||||||
|
count := 0
|
||||||
|
for {
|
||||||
|
ul, err := user.ListUserOpenIDs(nextOpenid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
openids = append(openids, ul.Data.OpenIDs...)
|
||||||
|
count += ul.Count
|
||||||
|
if ul.Total > count {
|
||||||
|
nextOpenid = ul.NextOpenID
|
||||||
|
} else {
|
||||||
|
return openids, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
work/work.go
16
work/work.go
@@ -9,12 +9,15 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/work/context"
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
"github.com/silenceper/wechat/v2/work/externalcontact"
|
"github.com/silenceper/wechat/v2/work/externalcontact"
|
||||||
"github.com/silenceper/wechat/v2/work/invoice"
|
"github.com/silenceper/wechat/v2/work/invoice"
|
||||||
|
"github.com/silenceper/wechat/v2/work/js"
|
||||||
"github.com/silenceper/wechat/v2/work/kf"
|
"github.com/silenceper/wechat/v2/work/kf"
|
||||||
"github.com/silenceper/wechat/v2/work/material"
|
"github.com/silenceper/wechat/v2/work/material"
|
||||||
"github.com/silenceper/wechat/v2/work/message"
|
"github.com/silenceper/wechat/v2/work/message"
|
||||||
"github.com/silenceper/wechat/v2/work/msgaudit"
|
"github.com/silenceper/wechat/v2/work/msgaudit"
|
||||||
"github.com/silenceper/wechat/v2/work/oauth"
|
"github.com/silenceper/wechat/v2/work/oauth"
|
||||||
"github.com/silenceper/wechat/v2/work/robot"
|
"github.com/silenceper/wechat/v2/work/robot"
|
||||||
|
"github.com/silenceper/wechat/v2/work/server"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Work 企业微信
|
// Work 企业微信
|
||||||
@@ -91,3 +94,16 @@ func (wk *Work) GetInvoice() *invoice.Client {
|
|||||||
func (wk *Work) GetCheckin() *checkin.Client {
|
func (wk *Work) GetCheckin() *checkin.Client {
|
||||||
return checkin.NewClient(wk.ctx)
|
return checkin.NewClient(wk.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJs js-sdk配置
|
||||||
|
func (wk *Work) GetJs() *js.Js {
|
||||||
|
return js.NewJs(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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user