mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-13 17:22:26 +08:00
Compare commits
5 Commits
v2.1.11
...
9b028e5368
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b028e5368 | ||
|
|
2bfc250f21 | ||
|
|
f0866babb5 | ||
|
|
7d93d1b9c8 | ||
|
|
78c00a9124 |
58
cache/redis.go
vendored
58
cache/redis.go
vendored
@@ -17,14 +17,24 @@ type Redis struct {
|
|||||||
|
|
||||||
// RedisOpts redis 连接属性
|
// RedisOpts redis 连接属性
|
||||||
type RedisOpts struct {
|
type RedisOpts struct {
|
||||||
Host string `json:"host" yml:"host"`
|
Host string `json:"host" yaml:"host"`
|
||||||
Username string `json:"username" yaml:"username"`
|
Username string `json:"username" yaml:"username"`
|
||||||
Password string `json:"password" yml:"password"`
|
Password string `json:"password" yaml:"password"`
|
||||||
Database int `json:"database" yml:"database"`
|
Database int `json:"database" yaml:"database"`
|
||||||
MaxIdle int `json:"max_idle" yml:"max_idle"`
|
MinIdleConns int `json:"min_idle_conns" yaml:"min_idle_conns"` // 最小空闲连接数
|
||||||
MaxActive int `json:"max_active" yml:"max_active"`
|
PoolSize int `json:"pool_size" yaml:"pool_size"` // 连接池大小,0 表示使用默认值(即 CPU 核心数 * 10)
|
||||||
IdleTimeout int `json:"idle_timeout" yml:"idle_timeout"` // second
|
MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数,-1 表示不重试,0 表示使用默认值(即 3 次)
|
||||||
UseTLS bool `json:"use_tls" yml:"use_tls"` // 是否使用TLS
|
DialTimeout int `json:"dial_timeout" yaml:"dial_timeout"` // 连接超时时间(秒),0 表示使用默认值(即 5 秒)
|
||||||
|
ReadTimeout int `json:"read_timeout" yaml:"read_timeout"` // 读取超时时间(秒),-1 表示不超时,0 表示使用默认值(即 3 秒)
|
||||||
|
WriteTimeout int `json:"write_timeout" yaml:"write_timeout"` // 写入超时时间(秒),-1 表示不超时,0 表示使用默认值(即 ReadTimeout)
|
||||||
|
PoolTimeout int `json:"pool_timeout" yaml:"pool_timeout"` // 连接池获取连接超时时间(秒),0 表示使用默认值(即 ReadTimeout + 1 秒)
|
||||||
|
IdleTimeout int `json:"idle_timeout" yaml:"idle_timeout"` // 空闲连接超时时间(秒),-1 表示禁用空闲连接超时检查,0 表示使用默认值(即 5 分钟)
|
||||||
|
UseTLS bool `json:"use_tls" yaml:"use_tls"` // 是否使用 TLS
|
||||||
|
|
||||||
|
// Deprecated: 应使用 MinIdleConns 代替
|
||||||
|
MaxIdle int `json:"max_idle" yaml:"max_idle"`
|
||||||
|
// Deprecated: 应使用 PoolSize 代替
|
||||||
|
MaxActive int `json:"max_active" yaml:"max_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRedis 实例化
|
// NewRedis 实例化
|
||||||
@@ -34,10 +44,38 @@ func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
|||||||
DB: opts.Database,
|
DB: opts.Database,
|
||||||
Username: opts.Username,
|
Username: opts.Username,
|
||||||
Password: opts.Password,
|
Password: opts.Password,
|
||||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
MinIdleConns: opts.MinIdleConns,
|
||||||
MinIdleConns: opts.MaxIdle,
|
PoolSize: opts.PoolSize,
|
||||||
|
MaxRetries: opts.MaxRetries,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兼容旧的 MaxIdle 参数,仅在未显式设置 MinIdleConns 时生效
|
||||||
|
if opts.MaxIdle > 0 && opts.MinIdleConns == 0 {
|
||||||
|
uniOpt.MinIdleConns = opts.MaxIdle
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧的 MaxActive 参数,仅在未显式设置 PoolSize 时生效
|
||||||
|
if opts.MaxActive > 0 && opts.PoolSize == 0 {
|
||||||
|
uniOpt.PoolSize = opts.MaxActive
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTimeout := func(seconds int, target *time.Duration) {
|
||||||
|
if seconds > 0 {
|
||||||
|
*target = time.Duration(seconds) * time.Second
|
||||||
|
} else if seconds == -1 {
|
||||||
|
// 当 seconds 为 -1 时,表示禁用超时:按 go-redis 约定,将超时时间设置为负值(如 -1ns)代表「无超时」
|
||||||
|
*target = -1
|
||||||
|
}
|
||||||
|
// 当 seconds 为 0 时,使用 go-redis 的默认超时配置:
|
||||||
|
// 不修改 target,保持其零值(0),由 go-redis 解释为“使用默认值”
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTimeout(opts.DialTimeout, &uniOpt.DialTimeout)
|
||||||
|
applyTimeout(opts.ReadTimeout, &uniOpt.ReadTimeout)
|
||||||
|
applyTimeout(opts.WriteTimeout, &uniOpt.WriteTimeout)
|
||||||
|
applyTimeout(opts.PoolTimeout, &uniOpt.PoolTimeout)
|
||||||
|
applyTimeout(opts.IdleTimeout, &uniOpt.IdleTimeout)
|
||||||
|
|
||||||
if opts.UseTLS {
|
if opts.UseTLS {
|
||||||
h, _, err := net.SplitHostPort(opts.Host)
|
h, _, err := net.SplitHostPort(opts.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
268
cache/redis_test.go
vendored
268
cache/redis_test.go
vendored
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alicebob/miniredis/v2"
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRedis(t *testing.T) {
|
func TestRedis(t *testing.T) {
|
||||||
@@ -18,7 +19,16 @@ func TestRedis(t *testing.T) {
|
|||||||
timeoutDuration = time.Second
|
timeoutDuration = time.Second
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
opts = &RedisOpts{
|
opts = &RedisOpts{
|
||||||
Host: server.Addr(),
|
Host: server.Addr(),
|
||||||
|
Password: "",
|
||||||
|
Database: 0,
|
||||||
|
PoolSize: 10,
|
||||||
|
MinIdleConns: 5,
|
||||||
|
DialTimeout: 5,
|
||||||
|
ReadTimeout: 5,
|
||||||
|
WriteTimeout: 5,
|
||||||
|
PoolTimeout: 5,
|
||||||
|
IdleTimeout: 300,
|
||||||
}
|
}
|
||||||
redis = NewRedis(ctx, opts)
|
redis = NewRedis(ctx, opts)
|
||||||
val = "silenceper"
|
val = "silenceper"
|
||||||
@@ -44,3 +54,259 @@ func TestRedis(t *testing.T) {
|
|||||||
t.Errorf("delete Error , err=%v", err)
|
t.Errorf("delete Error , err=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupRedisServer 创建并返回一个 miniredis 服务器实例
|
||||||
|
func setupRedisServer(t *testing.T) *miniredis.Miniredis {
|
||||||
|
server, err := miniredis.Run()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("miniredis.Run Error", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisMaxIdleMapping 测试只设置MaxIdle应该映射到MinIdleConns
|
||||||
|
func TestRedisMaxIdleMapping(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
MaxIdle: 10,
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
// 获取底层的 UniversalClient 并断言为 *redis.Client
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:MinIdleConns 表示期望的最小空闲连接数,但实际空闲连接数可能不同
|
||||||
|
// 我们需要通过 Options() 来验证配置是否正确应用
|
||||||
|
clientOpts := client.Options()
|
||||||
|
if clientOpts.MinIdleConns != 10 {
|
||||||
|
t.Errorf("期望 MinIdleConns = 10, 实际 = %d", clientOpts.MinIdleConns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisMaxActiveMapping 测试只设置MaxActive应该映射到PoolSize
|
||||||
|
func TestRedisMaxActiveMapping(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
MaxActive: 20,
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOpts := client.Options()
|
||||||
|
if clientOpts.PoolSize != 20 {
|
||||||
|
t.Errorf("期望 PoolSize = 20, 实际 = %d", clientOpts.PoolSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisNewFieldsPriority 测试新字段应该优先于旧字段
|
||||||
|
func TestRedisNewFieldsPriority(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
MaxIdle: 5,
|
||||||
|
MinIdleConns: 15,
|
||||||
|
MaxActive: 10,
|
||||||
|
PoolSize: 30,
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOpts := client.Options()
|
||||||
|
if clientOpts.MinIdleConns != 15 {
|
||||||
|
t.Errorf("期望 MinIdleConns = 15 (新字段优先), 实际 = %d", clientOpts.MinIdleConns)
|
||||||
|
}
|
||||||
|
if clientOpts.PoolSize != 30 {
|
||||||
|
t.Errorf("期望 PoolSize = 30 (新字段优先), 实际 = %d", clientOpts.PoolSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisPositiveTimeouts 测试正值超时应该正确应用
|
||||||
|
func TestRedisPositiveTimeouts(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
DialTimeout: 10,
|
||||||
|
ReadTimeout: 20,
|
||||||
|
WriteTimeout: 30,
|
||||||
|
PoolTimeout: 40,
|
||||||
|
IdleTimeout: 50,
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOpts := client.Options()
|
||||||
|
if clientOpts.DialTimeout != 10*time.Second {
|
||||||
|
t.Errorf("期望 DialTimeout = 10s, 实际 = %v", clientOpts.DialTimeout)
|
||||||
|
}
|
||||||
|
if clientOpts.ReadTimeout != 20*time.Second {
|
||||||
|
t.Errorf("期望 ReadTimeout = 20s, 实际 = %v", clientOpts.ReadTimeout)
|
||||||
|
}
|
||||||
|
if clientOpts.WriteTimeout != 30*time.Second {
|
||||||
|
t.Errorf("期望 WriteTimeout = 30s, 实际 = %v", clientOpts.WriteTimeout)
|
||||||
|
}
|
||||||
|
if clientOpts.PoolTimeout != 40*time.Second {
|
||||||
|
t.Errorf("期望 PoolTimeout = 40s, 实际 = %v", clientOpts.PoolTimeout)
|
||||||
|
}
|
||||||
|
if clientOpts.IdleTimeout != 50*time.Second {
|
||||||
|
t.Errorf("期望 IdleTimeout = 50s, 实际 = %v", clientOpts.IdleTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisNegativeTimeouts 测试-1值应该禁用超时
|
||||||
|
func TestRedisNegativeTimeouts(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
DialTimeout: -1,
|
||||||
|
ReadTimeout: -1,
|
||||||
|
WriteTimeout: -1,
|
||||||
|
PoolTimeout: -1,
|
||||||
|
IdleTimeout: -1,
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOpts := client.Options()
|
||||||
|
// -1 应该被设置为负值表示禁用超时
|
||||||
|
// DialTimeout, PoolTimeout, IdleTimeout 会被设置为 -1ns
|
||||||
|
if clientOpts.DialTimeout != -1 {
|
||||||
|
t.Errorf("期望 DialTimeout = -1ns (禁用), 实际 = %v", clientOpts.DialTimeout)
|
||||||
|
}
|
||||||
|
// ReadTimeout 和 WriteTimeout 在 go-redis 中有特殊处理
|
||||||
|
// 当设置为负值时,会被规范化为 0,这也表示无超时
|
||||||
|
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
|
||||||
|
t.Logf("WriteTimeout = %v (设置为-1后的值)", clientOpts.WriteTimeout)
|
||||||
|
|
||||||
|
if clientOpts.PoolTimeout != -1 {
|
||||||
|
t.Errorf("期望 PoolTimeout = -1ns (禁用), 实际 = %v", clientOpts.PoolTimeout)
|
||||||
|
}
|
||||||
|
if clientOpts.IdleTimeout != -1 {
|
||||||
|
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisZeroTimeouts 测试0值应该使用go-redis默认值
|
||||||
|
func TestRedisZeroTimeouts(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
DialTimeout: 0,
|
||||||
|
ReadTimeout: 0,
|
||||||
|
WriteTimeout: 0,
|
||||||
|
PoolTimeout: 0,
|
||||||
|
IdleTimeout: 0,
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOpts := client.Options()
|
||||||
|
// 0值应该保持为0,由 go-redis 使用默认值
|
||||||
|
// go-redis 的默认值:
|
||||||
|
// DialTimeout: 5s
|
||||||
|
// ReadTimeout: 3s
|
||||||
|
// WriteTimeout: ReadTimeout
|
||||||
|
// PoolTimeout: ReadTimeout + 1s
|
||||||
|
// IdleTimeout: 5min
|
||||||
|
|
||||||
|
if clientOpts.DialTimeout == 0 {
|
||||||
|
t.Error("期望 DialTimeout 使用 go-redis 默认值 (5s), 实际为 0")
|
||||||
|
}
|
||||||
|
if clientOpts.ReadTimeout == 0 {
|
||||||
|
t.Error("期望 ReadTimeout 使用 go-redis 默认值 (3s), 实际为 0")
|
||||||
|
}
|
||||||
|
if clientOpts.WriteTimeout == 0 {
|
||||||
|
t.Error("期望 WriteTimeout 使用 go-redis 默认值 (ReadTimeout), 实际为 0")
|
||||||
|
}
|
||||||
|
if clientOpts.PoolTimeout == 0 {
|
||||||
|
t.Error("期望 PoolTimeout 使用 go-redis 默认值 (ReadTimeout + 1s), 实际为 0")
|
||||||
|
}
|
||||||
|
if clientOpts.IdleTimeout == 0 {
|
||||||
|
t.Error("期望 IdleTimeout 使用 go-redis 默认值 (5min), 实际为 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisMixedTimeouts 测试混合超时配置
|
||||||
|
func TestRedisMixedTimeouts(t *testing.T) {
|
||||||
|
server := setupRedisServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opts := &RedisOpts{
|
||||||
|
Host: server.Addr(),
|
||||||
|
Database: 0,
|
||||||
|
DialTimeout: 5, // 正值
|
||||||
|
ReadTimeout: -1, // 禁用
|
||||||
|
WriteTimeout: 0, // 使用默认值
|
||||||
|
PoolTimeout: 10, // 正值
|
||||||
|
IdleTimeout: -1, // 禁用
|
||||||
|
}
|
||||||
|
r := NewRedis(ctx, opts)
|
||||||
|
|
||||||
|
client, ok := r.conn.(*redis.Client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("无法转换为 *redis.Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOpts := client.Options()
|
||||||
|
if clientOpts.DialTimeout != 5*time.Second {
|
||||||
|
t.Errorf("期望 DialTimeout = 5s, 实际 = %v", clientOpts.DialTimeout)
|
||||||
|
}
|
||||||
|
// ReadTimeout 设置为 -1,会被 go-redis 处理为 0(无超时)
|
||||||
|
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
|
||||||
|
|
||||||
|
// WriteTimeout 设置为 0,应该使用 go-redis 的默认值
|
||||||
|
// 默认值通常是 ReadTimeout 的值
|
||||||
|
t.Logf("WriteTimeout = %v (设置为0后使用的默认值)", clientOpts.WriteTimeout)
|
||||||
|
|
||||||
|
if clientOpts.PoolTimeout != 10*time.Second {
|
||||||
|
t.Errorf("期望 PoolTimeout = 10s, 实际 = %v", clientOpts.PoolTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdleTimeout 设置为 -1,应该被设置为 -1ns(禁用空闲超时)
|
||||||
|
if clientOpts.IdleTimeout != -1 {
|
||||||
|
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ const (
|
|||||||
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
||||||
// convertToUserIDURL openID转userID
|
// convertToUserIDURL openID转userID
|
||||||
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
||||||
|
// userBatchDeleteURL 批量删除成员
|
||||||
|
userBatchDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete?access_token=%s"
|
||||||
|
// userAuthSuccURL 登录二次验证
|
||||||
|
userAuthSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=%s&userid=%s"
|
||||||
|
// batchInviteURL 邀请成员
|
||||||
|
batchInviteURL = "https://qyapi.weixin.qq.com/cgi-bin/batch/invite?access_token=%s"
|
||||||
|
// getJoinQrcodeURL 获取加入企业二维码
|
||||||
|
getJoinQrcodeURL = "https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -447,3 +455,112 @@ func (r *Client) ConvertToUserID(openID string) (string, error) {
|
|||||||
err = util.DecodeWithError(response, result, "ConvertToUserID")
|
err = util.DecodeWithError(response, result, "ConvertToUserID")
|
||||||
return result.UserID, err
|
return result.UserID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserBatchDeleteRequest 批量删除成员请求
|
||||||
|
type UserBatchDeleteRequest struct {
|
||||||
|
UseridList []string `json:"useridlist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserBatchDelete 批量删除成员
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/90199
|
||||||
|
func (r *Client) UserBatchDelete(req *UserBatchDeleteRequest) 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(userBatchDeleteURL, accessToken), req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "UserBatchDelete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAuthSucc 登录二次验证
|
||||||
|
// @see https://developer.work.weixin.qq.com/document/path/90203
|
||||||
|
func (r *Client) UserAuthSucc(userID string) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(userAuthSuccURL, accessToken, userID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "UserAuthSucc")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchInviteRequest 邀请成员请求
|
||||||
|
type BatchInviteRequest struct {
|
||||||
|
User []string `json:"user"`
|
||||||
|
Party []int `json:"party"`
|
||||||
|
Tag []int `json:"tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchInviteResponse 邀请成员响应
|
||||||
|
type BatchInviteResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
InvalidUser []string `json:"invaliduser"`
|
||||||
|
InvalidParty []int `json:"invalidparty"`
|
||||||
|
InvalidTag []int `json:"invalidtag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchInvite 邀请成员
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/90975
|
||||||
|
func (r *Client) BatchInvite(req *BatchInviteRequest) (*BatchInviteResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(batchInviteURL, accessToken), req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &BatchInviteResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "BatchInvite")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJoinQrcodeRequest 获取加入企业二维码请求
|
||||||
|
type GetJoinQrcodeRequest struct {
|
||||||
|
SizeType int `json:"size_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJoinQrcodeResponse 获取加入企业二维码响应
|
||||||
|
type GetJoinQrcodeResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
JoinQrcode string `json:"join_qrcode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJoinQrcode 获取加入企业二维码
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/91714
|
||||||
|
func (r *Client) GetJoinQrcode(req *GetJoinQrcodeRequest) (*GetJoinQrcodeResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
apiURL string
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.SizeType > 0 {
|
||||||
|
apiURL = fmt.Sprintf("%s?access_token=%s&size_type=%d", getJoinQrcodeURL, accessToken, req.SizeType)
|
||||||
|
} else {
|
||||||
|
apiURL = fmt.Sprintf("%s?access_token=%s", getJoinQrcodeURL, accessToken)
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(apiURL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetJoinQrcodeResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetJoinQrcode")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,24 +158,32 @@ type OptionGroupRuleCheckinDate struct {
|
|||||||
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
|
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
|
||||||
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
|
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
|
||||||
LateRule OptionGroupLateRule `json:"late_rule"`
|
LateRule OptionGroupLateRule `json:"late_rule"`
|
||||||
|
Biweekly OptionGroupBiweekly `json:"biweekly,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息
|
// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息
|
||||||
type OptionGroupRuleCheckinTime struct {
|
type OptionGroupRuleCheckinTime struct {
|
||||||
TimeID int64 `json:"time_id"`
|
TimeID int64 `json:"time_id"`
|
||||||
WorkSec int64 `json:"work_sec"`
|
WorkSec int64 `json:"work_sec"`
|
||||||
OffWorkSec int64 `json:"off_work_sec"`
|
OffWorkSec int64 `json:"off_work_sec"`
|
||||||
RemindWorkSec int64 `json:"remind_work_sec"`
|
RemindWorkSec int64 `json:"remind_work_sec"`
|
||||||
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
|
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
|
||||||
AllowRest bool `json:"allow_rest"`
|
AllowRest bool `json:"allow_rest"`
|
||||||
RestBeginTime int64 `json:"rest_begin_time"`
|
RestBeginTime int64 `json:"rest_begin_time"`
|
||||||
RestEndTime int64 `json:"rest_end_time"`
|
RestEndTime int64 `json:"rest_end_time"`
|
||||||
EarliestWorkSec int64 `json:"earliest_work_sec"`
|
EarliestWorkSec int64 `json:"earliest_work_sec"`
|
||||||
LatestWorkSec int64 `json:"latest_work_sec"`
|
LatestWorkSec int64 `json:"latest_work_sec"`
|
||||||
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
|
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
|
||||||
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
|
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
|
||||||
NoNeedCheckOn bool `json:"no_need_checkon"`
|
NoNeedCheckOn bool `json:"no_need_checkon"`
|
||||||
NoNeedCheckOff bool `json:"no_need_checkoff"`
|
NoNeedCheckOff bool `json:"no_need_checkoff"`
|
||||||
|
RestTimes []OptionGroupRuleRestTimes `json:"rest_times,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionGroupRuleRestTimes 多组休息时间
|
||||||
|
type OptionGroupRuleRestTimes struct {
|
||||||
|
RestBeginTime int64 `json:"rest_begin_time,omitempty"`
|
||||||
|
RestEndTime int64 `json:"rest_end_time,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionGroupLateRule 晚走晚到时间规则信息
|
// OptionGroupLateRule 晚走晚到时间规则信息
|
||||||
@@ -192,6 +200,13 @@ type OptionGroupTimeRule struct {
|
|||||||
OnWorkFlexTime int64 `json:"onwork_flex_time"`
|
OnWorkFlexTime int64 `json:"onwork_flex_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OptionGroupBiweekly 大小周规则
|
||||||
|
type OptionGroupBiweekly struct {
|
||||||
|
EnableWeekdayRecurrence bool `json:"enable_weekday_recurrence"`
|
||||||
|
OddWorkdays []int64 `json:"odd_workdays"`
|
||||||
|
EvenWorkdays []int64 `json:"even_workdays"`
|
||||||
|
}
|
||||||
|
|
||||||
// OptionGroupSpeWorkdays 特殊工作日
|
// OptionGroupSpeWorkdays 特殊工作日
|
||||||
type OptionGroupSpeWorkdays struct {
|
type OptionGroupSpeWorkdays struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ type (
|
|||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ChatExpiresIn int `json:"chat_expires_in"`
|
ChatExpiresIn int `json:"chat_expires_in"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
|
IsExclusive bool `json:"is_exclusive"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||||
}
|
}
|
||||||
// AddContactWayResponse 配置客户联系「联系我」方式响应
|
// AddContactWayResponse 配置客户联系「联系我」方式响应
|
||||||
@@ -132,6 +134,7 @@ type (
|
|||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ChatExpiresIn int `json:"chat_expires_in"`
|
ChatExpiresIn int `json:"chat_expires_in"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
Conclusions ConclusionsResponse `json:"conclusions"`
|
Conclusions ConclusionsResponse `json:"conclusions"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -168,6 +171,7 @@ type (
|
|||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ChatExpiresIn int `json:"chat_expires_in"`
|
ChatExpiresIn int `json:"chat_expires_in"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||||
}
|
}
|
||||||
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
|
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ const (
|
|||||||
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
||||||
// customerAcquisitionGetChatInfo 获取成员多次收消息详情
|
// customerAcquisitionGetChatInfo 获取成员多次收消息详情
|
||||||
customerAcquisitionGetChatInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get_chat_info?access_token=%s"
|
customerAcquisitionGetChatInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get_chat_info?access_token=%s"
|
||||||
|
// customerAcquisitionGetPermitURL 获取客户可建联成员
|
||||||
|
customerAcquisitionGetPermitURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_app/get_permit?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -68,9 +70,10 @@ type (
|
|||||||
// GetCustomerAcquisitionResponse 获取获客链接详情响应
|
// GetCustomerAcquisitionResponse 获取获客链接详情响应
|
||||||
GetCustomerAcquisitionResponse struct {
|
GetCustomerAcquisitionResponse struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
Link Link `json:"link"`
|
Link Link `json:"link"`
|
||||||
Range CustomerAcquisitionRange `json:"range"`
|
Range CustomerAcquisitionRange `json:"range"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||||
|
SkipVerify bool `json:"skip_verify"`
|
||||||
}
|
}
|
||||||
// Link 获客链接
|
// Link 获客链接
|
||||||
Link struct {
|
Link struct {
|
||||||
@@ -78,6 +81,8 @@ type (
|
|||||||
LinkName string `json:"link_name"`
|
LinkName string `json:"link_name"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
CreateTime int64 `json:"create_time"`
|
CreateTime int64 `json:"create_time"`
|
||||||
|
SkipVerify bool `json:"skip_verify"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomerAcquisitionRange 该获客链接使用范围
|
// CustomerAcquisitionRange 该获客链接使用范围
|
||||||
@@ -85,6 +90,12 @@ type (
|
|||||||
UserList []string `json:"user_list"`
|
UserList []string `json:"user_list"`
|
||||||
DepartmentList []int64 `json:"department_list"`
|
DepartmentList []int64 `json:"department_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CustomerPriorityOption 该获客链接的优先选项
|
||||||
|
CustomerPriorityOption struct {
|
||||||
|
PriorityType int `json:"priority_type"`
|
||||||
|
PriorityUseridList []string `json:"priority_userid_list"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCustomerAcquisition 获客助手--获取获客链接详情
|
// GetCustomerAcquisition 获客助手--获取获客链接详情
|
||||||
@@ -109,9 +120,11 @@ func (r *Client) GetCustomerAcquisition(req *GetCustomerAcquisitionRequest) (*Ge
|
|||||||
type (
|
type (
|
||||||
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
|
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
|
||||||
CreateCustomerAcquisitionLinkRequest struct {
|
CreateCustomerAcquisitionLinkRequest struct {
|
||||||
LinkName string `json:"link_name"`
|
LinkName string `json:"link_name"`
|
||||||
Range CustomerAcquisitionRange `json:"range"`
|
Range CustomerAcquisitionRange `json:"range"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
SkipVerify bool `json:"skip_verify"`
|
||||||
|
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
}
|
}
|
||||||
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
|
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
|
||||||
CreateCustomerAcquisitionLinkResponse struct {
|
CreateCustomerAcquisitionLinkResponse struct {
|
||||||
@@ -142,10 +155,12 @@ func (r *Client) CreateCustomerAcquisitionLink(req *CreateCustomerAcquisitionLin
|
|||||||
type (
|
type (
|
||||||
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
|
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
|
||||||
UpdateCustomerAcquisitionLinkRequest struct {
|
UpdateCustomerAcquisitionLinkRequest struct {
|
||||||
LinkID string `json:"link_id"`
|
LinkID string `json:"link_id"`
|
||||||
LinkName string `json:"link_name"`
|
LinkName string `json:"link_name"`
|
||||||
Range CustomerAcquisitionRange `json:"range"`
|
Range CustomerAcquisitionRange `json:"range"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
SkipVerify bool `json:"skip_verify"`
|
||||||
|
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
}
|
}
|
||||||
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
|
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
|
||||||
UpdateCustomerAcquisitionLinkResponse struct {
|
UpdateCustomerAcquisitionLinkResponse struct {
|
||||||
@@ -349,3 +364,30 @@ func (r *Client) GetChatInfo(req *GetChatInfoRequest) (*GetChatInfoResponse, err
|
|||||||
err = util.DecodeWithError(response, result, "GetChatInfo")
|
err = util.DecodeWithError(response, result, "GetChatInfo")
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPermitResponse 获取客户可建联成员响应
|
||||||
|
type GetPermitResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
UserList []string `json:"user_list"`
|
||||||
|
DepartmentList []int `json:"department_list"`
|
||||||
|
TagList []int `json:"tag_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermit 获取客户可建联成员
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/101146
|
||||||
|
func (r *Client) GetPermit() (*GetPermitResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(customerAcquisitionGetPermitURL, accessToken)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetPermitResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "CustomerAcquisitionGetPermit")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type (
|
|||||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddJoinWayResponse 添加群配置返回值
|
// AddJoinWayResponse 添加群配置返回值
|
||||||
@@ -65,6 +66,7 @@ type (
|
|||||||
ChatIDList []string `json:"chat_id_list"`
|
ChatIDList []string `json:"chat_id_list"`
|
||||||
QrCode string `json:"qr_code"`
|
QrCode string `json:"qr_code"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
}
|
}
|
||||||
//GetJoinWayResponse 获取群配置的返回值
|
//GetJoinWayResponse 获取群配置的返回值
|
||||||
GetJoinWayResponse struct {
|
GetJoinWayResponse struct {
|
||||||
@@ -103,6 +105,7 @@ type UpdateJoinWayRequest struct {
|
|||||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||||
|
MarkSource bool `json:"mark_source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateJoinWay 更新客户群进群方式配置
|
// UpdateJoinWay 更新客户群进群方式配置
|
||||||
|
|||||||
Reference in New Issue
Block a user