mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-11 16:22:26 +08:00
Compare commits
45 Commits
e6d960c22f
...
v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4fc0bd615 | ||
|
|
20a8183e88 | ||
|
|
f0f35e2f77 | ||
|
|
eebfb93386 | ||
|
|
9b028e5368 | ||
|
|
2bfc250f21 | ||
|
|
f0866babb5 | ||
|
|
7d93d1b9c8 | ||
|
|
78c00a9124 | ||
|
|
54f08cbd73 | ||
|
|
c34ff2031b | ||
|
|
30c8e77246 | ||
|
|
6f6e95cfdb | ||
|
|
c806a0c172 | ||
|
|
c136b878ce | ||
|
|
d4a81916d5 | ||
|
|
ef1372b98a | ||
|
|
0d666b60ba | ||
|
|
e1122d42b0 | ||
|
|
be3f0d8bd5 | ||
|
|
66f9794d2f | ||
|
|
ee5f045b89 | ||
|
|
d35f0f0865 | ||
|
|
bbad169706 | ||
|
|
5927c26152 | ||
|
|
8ebff5c29c | ||
|
|
86ef690ecd | ||
|
|
ee85790123 | ||
|
|
8a810837a4 | ||
|
|
c51d41ee8a | ||
|
|
24f812d187 | ||
|
|
dd43b7baa3 | ||
|
|
2dfd2ff608 | ||
|
|
23bb10b0c9 | ||
|
|
b639d2235d | ||
|
|
26d2093bd7 | ||
|
|
cf42cd8d54 | ||
|
|
85ee45580b | ||
|
|
208d5c528a | ||
|
|
b5f9a8933e | ||
|
|
52fb5596d3 | ||
|
|
44150c557e | ||
|
|
635a0c640d | ||
|
|
010e49c35c | ||
|
|
9c87d1cb34 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,8 +1,8 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: # silenceper
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
open_collective: gowechat
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
|
||||
38
.github/workflows/ai-dev.yaml
vendored
Normal file
38
.github/workflows/ai-dev.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: "${{ secrets.ANTHROPIC_BASE_URL }}"
|
||||
with:
|
||||
model: "${{ secrets.ANTHROPIC_MODEL }}"
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
17
README.md
17
README.md
@@ -1,12 +1,13 @@
|
||||
# WeChat SDK for Go
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/silenceper/wechat)
|
||||

|
||||
[](https://goreportcard.com/report/github.com/silenceper/wechat/v2)
|
||||
[](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc)
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
使用Golang开发的微信SDK,简单、易用。
|
||||
> 注意:当前版本为v2版本,v1版本已废弃
|
||||
|
||||
## 文档 && 例子
|
||||
|
||||
@@ -75,7 +76,13 @@ server.Send()
|
||||
- 提交issue,描述需要贡献的内容
|
||||
- 完成更改后,提交PR
|
||||
|
||||
## 公众号
|
||||
|
||||
## 感谢以下贡献者
|
||||
|
||||
<a href="https://opencollective.com/gowechat"><img src="https://opencollective.com/gowechat/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
## 作者公众号
|
||||
|
||||

|
||||
|
||||
|
||||
75
cache/redis.go
vendored
75
cache/redis.go
vendored
@@ -2,6 +2,8 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -15,25 +17,76 @@ type Redis struct {
|
||||
|
||||
// RedisOpts redis 连接属性
|
||||
type RedisOpts struct {
|
||||
Host string `yml:"host" json:"host"`
|
||||
Username string `yaml:"username" json:"username"`
|
||||
Password string `yml:"password" json:"password"`
|
||||
Database int `yml:"database" json:"database"`
|
||||
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
||||
MaxActive int `yml:"max_active" json:"max_active"`
|
||||
IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` // second
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Username string `json:"username" yaml:"username"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
Database int `json:"database" yaml:"database"`
|
||||
MinIdleConns int `json:"min_idle_conns" yaml:"min_idle_conns"` // 最小空闲连接数
|
||||
PoolSize int `json:"pool_size" yaml:"pool_size"` // 连接池大小,0 表示使用默认值(即 CPU 核心数 * 10)
|
||||
MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数,-1 表示不重试,0 表示使用默认值(即 3 次)
|
||||
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 实例化
|
||||
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
||||
conn := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
uniOpt := &redis.UniversalOptions{
|
||||
Addrs: []string{opts.Host},
|
||||
DB: opts.Database,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||
MinIdleConns: opts.MaxIdle,
|
||||
})
|
||||
MinIdleConns: opts.MinIdleConns,
|
||||
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 {
|
||||
h, _, err := net.SplitHostPort(opts.Host)
|
||||
if err != nil {
|
||||
h = opts.Host
|
||||
}
|
||||
uniOpt.TLSConfig = &tls.Config{
|
||||
ServerName: h,
|
||||
}
|
||||
}
|
||||
|
||||
conn := redis.NewUniversalClient(uniOpt)
|
||||
return &Redis{ctx: ctx, conn: conn}
|
||||
}
|
||||
|
||||
|
||||
268
cache/redis_test.go
vendored
268
cache/redis_test.go
vendored
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
func TestRedis(t *testing.T) {
|
||||
@@ -18,7 +19,16 @@ func TestRedis(t *testing.T) {
|
||||
timeoutDuration = time.Second
|
||||
ctx = context.Background()
|
||||
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)
|
||||
val = "silenceper"
|
||||
@@ -44,3 +54,259 @@ func TestRedis(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok
|
||||
|
||||
// 构建缓存key
|
||||
var accessTokenCacheKey string
|
||||
|
||||
if ak.AgentID != "" {
|
||||
// 如果设置了AgentID,使用新的key格式
|
||||
accessTokenCacheKey = fmt.Sprintf("%s_access_token_%s_%s", ak.cacheKeyPrefix, ak.CorpID, ak.AgentID)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
context2 "context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
@@ -42,6 +43,16 @@ type ResTicket struct {
|
||||
|
||||
// GetTicket 获取jsapi_ticket
|
||||
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||
return js.GetTicketContext(context2.Background(), accessToken)
|
||||
}
|
||||
|
||||
// GetTicketFromServer 从服务器中获取ticket
|
||||
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||
return GetTicketFromServerContext(context2.Background(), accessToken)
|
||||
}
|
||||
|
||||
// GetTicketContext 获取jsapi_ticket
|
||||
func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, 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 {
|
||||
@@ -57,7 +68,7 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
|
||||
}
|
||||
|
||||
var ticket ResTicket
|
||||
ticket, err = GetTicketFromServer(accessToken)
|
||||
ticket, err = GetTicketFromServerContext(ctx, accessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -67,11 +78,11 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
|
||||
return
|
||||
}
|
||||
|
||||
// GetTicketFromServer 从服务器中获取ticket
|
||||
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||
// GetTicketFromServerContext 从服务器中获取ticket
|
||||
func GetTicketFromServerContext(ctx context2.Context, accessToken string) (ticket ResTicket, err error) {
|
||||
var response []byte
|
||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||
response, err = util.HTTPGet(url)
|
||||
response, err = util.HTTPGetContext(ctx, url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
22
credential/default_js_ticket_test.go
Normal file
22
credential/default_js_ticket_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
// TestGetTicketFromServerContext 测试 GetTicketFromServerContext 函数
|
||||
func TestGetTicketFromServerContext(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.New(fmt.Sprintf(getTicketURL, "arg-ak")).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
||||
|
||||
ticket, err := GetTicketFromServerContext(context.Background(), "arg-ak")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), ticket.ErrCode)
|
||||
assert.Equal(t, "mock-ticket", ticket.Ticket, "they should be equal")
|
||||
assert.Equal(t, int64(10), ticket.ExpiresIn, "they should be equal")
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
package credential
|
||||
|
||||
import context2 "context"
|
||||
|
||||
// JsTicketHandle js ticket获取
|
||||
type JsTicketHandle interface {
|
||||
// GetTicket 获取ticket
|
||||
GetTicket(accessToken string) (ticket string, err error)
|
||||
}
|
||||
|
||||
// JsTicketContextHandle js ticket获取
|
||||
type JsTicketContextHandle interface {
|
||||
JsTicketHandle
|
||||
GetTicketContext(ctx context2.Context, accessToken string) (ticket string, err error)
|
||||
}
|
||||
|
||||
@@ -10,24 +10,37 @@ import (
|
||||
"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"
|
||||
// TicketType ticket类型
|
||||
type TicketType int
|
||||
|
||||
//WorkJsTicket 默认获取js ticket方法
|
||||
const (
|
||||
// TicketTypeCorpJs 企业jsapi ticket
|
||||
TicketTypeCorpJs TicketType = iota
|
||||
// TicketTypeAgentJs 应用jsapi ticket
|
||||
TicketTypeAgentJs
|
||||
)
|
||||
|
||||
// 企业微信相关的 ticket URL
|
||||
const (
|
||||
// 企业微信 jsapi ticket
|
||||
getWorkJsTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s"
|
||||
// 企业微信应用 jsapi ticket
|
||||
getWorkAgentJsTicketURL = "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一个
|
||||
corpID string
|
||||
agentID string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
jsAPITicketLock *sync.Mutex
|
||||
}
|
||||
|
||||
//NewWorkJsTicket new
|
||||
func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
||||
// NewWorkJsTicket new WorkJsTicket
|
||||
func NewWorkJsTicket(corpID, agentID, cacheKeyPrefix string, cache cache.Cache) *WorkJsTicket {
|
||||
return &WorkJsTicket{
|
||||
appID: appID,
|
||||
corpID: corpID,
|
||||
agentID: agentID,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
@@ -35,11 +48,24 @@ func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache
|
||||
}
|
||||
}
|
||||
|
||||
//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 {
|
||||
// GetTicket 根据类型获取相应的jsapi_ticket
|
||||
func (js *WorkJsTicket) GetTicket(accessToken string, ticketType TicketType) (ticketStr string, err error) {
|
||||
var cacheKey string
|
||||
switch ticketType {
|
||||
case TicketTypeCorpJs:
|
||||
cacheKey = fmt.Sprintf("%s_corp_jsapi_ticket_%s", js.cacheKeyPrefix, js.corpID)
|
||||
case TicketTypeAgentJs:
|
||||
if js.agentID == "" {
|
||||
err = fmt.Errorf("agentID is empty")
|
||||
return
|
||||
}
|
||||
cacheKey = fmt.Sprintf("%s_agent_jsapi_ticket_%s_%s", js.cacheKeyPrefix, js.corpID, js.agentID)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported ticket type: %v", ticketType)
|
||||
return
|
||||
}
|
||||
|
||||
if val := js.cache.Get(cacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
@@ -47,28 +73,35 @@ func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err err
|
||||
defer js.jsAPITicketLock.Unlock()
|
||||
|
||||
// 双检,防止重复从微信服务器获取
|
||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||
if val := js.cache.Get(cacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
var ticket ResTicket
|
||||
ticket, err = GetQyWxTicketFromServer(accessToken, js.agentID != "")
|
||||
ticket, err = js.getTicketFromServer(accessToken, ticketType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
expires := ticket.ExpiresIn - 1500
|
||||
err = js.cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
err = js.cache.Set(cacheKey, 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)
|
||||
// getTicketFromServer 从服务器中获取ticket
|
||||
func (js *WorkJsTicket) getTicketFromServer(accessToken string, ticketType TicketType) (ticket ResTicket, err error) {
|
||||
var url string
|
||||
switch ticketType {
|
||||
case TicketTypeCorpJs:
|
||||
url = fmt.Sprintf(getWorkJsTicketURL, accessToken)
|
||||
case TicketTypeAgentJs:
|
||||
url = fmt.Sprintf(getWorkAgentJsTicketURL, accessToken)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported ticket type: %v", ticketType)
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -30,6 +30,8 @@ const (
|
||||
getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s"
|
||||
// 访问页面
|
||||
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
|
||||
// 获取小程序性能数据
|
||||
getPerformanceDataURL = "https://api.weixin.qq.com/wxa/business/performance/boot?access_token=%s"
|
||||
)
|
||||
|
||||
// Analysis analyis 数据分析
|
||||
@@ -315,3 +317,67 @@ func (analysis *Analysis) GetAnalysisVisitPage(beginDate, endDate string) (resul
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPerformanceDataRequest 获取小程序性能数据请求
|
||||
type GetPerformanceDataRequest struct {
|
||||
Module string `json:"module"`
|
||||
Time PerformanceDataTime `json:"time"`
|
||||
Params []PerformanceDataParams `json:"params"`
|
||||
}
|
||||
|
||||
// PerformanceDataTime 获取小程序性能数据开始和结束日期
|
||||
type PerformanceDataTime struct {
|
||||
BeginTimestamp int64 `json:"begin_timestamp"`
|
||||
EndTimestamp int64 `json:"end_timestamp"`
|
||||
}
|
||||
|
||||
// PerformanceDataParams 获取小程序性能数据查询条件
|
||||
type PerformanceDataParams struct {
|
||||
Field string `json:"field"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// GetPerformanceDataResponse 获取小程序性能数据响应
|
||||
type GetPerformanceDataResponse struct {
|
||||
util.CommonError
|
||||
Body PerformanceDataBody `json:"body"`
|
||||
}
|
||||
|
||||
// PerformanceDataBody 性能数据
|
||||
type PerformanceDataBody struct {
|
||||
Tables []PerformanceDataTable `json:"tables"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// PerformanceDataTable 数据数组
|
||||
type PerformanceDataTable struct {
|
||||
ID string `json:"id"`
|
||||
Lines []PerformanceDataTableLine `json:"lines"`
|
||||
Zh string `json:"zh"`
|
||||
}
|
||||
|
||||
// PerformanceDataTableLine 按时间排列的性能数据
|
||||
type PerformanceDataTableLine struct {
|
||||
Fields []PerformanceDataTableLineField `json:"fields"`
|
||||
}
|
||||
|
||||
// PerformanceDataTableLineField 单天的性能数据
|
||||
type PerformanceDataTableLineField struct {
|
||||
RefDate string `json:"refdate"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// GetPerformanceData 获取小程序性能数据
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/data-analysis/others/getPerformanceData.html
|
||||
func (analysis *Analysis) GetPerformanceData(req *GetPerformanceDataRequest) (res GetPerformanceDataResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = analysis.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getPerformanceDataURL, accessToken), req); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetPerformanceData")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,11 +10,22 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// code2SessionURL 小程序登录
|
||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||
|
||||
// checkEncryptedDataURL 检查加密信息
|
||||
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
||||
|
||||
// getPhoneNumber 获取手机号
|
||||
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
||||
// checkSessionURL 检验登录态
|
||||
checkSessionURL = "https://api.weixin.qq.com/wxa/checksession?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||
// resetUserSessionKeyURL 重置登录态
|
||||
resetUserSessionKeyURL = "https://api.weixin.qq.com/wxa/resetusersessionkey?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||
// getPluginOpenPIDURL 获取插件用户openPID
|
||||
getPluginOpenPIDURL = "https://api.weixin.qq.com/wxa/getpluginopenpid?access_token=%s"
|
||||
// getPaidUnionIDURL 支付后获取 UnionID
|
||||
getPaidUnionIDURL = "https://api.weixin.qq.com/wxa/getpaidunionid"
|
||||
// getUserEncryptKeyURL 获取用户encryptKey
|
||||
getUserEncryptKeyURL = "https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||
)
|
||||
|
||||
// Auth 登录/用户信息
|
||||
@@ -65,9 +76,45 @@ func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (resu
|
||||
return
|
||||
}
|
||||
|
||||
type (
|
||||
// GetPaidUnionIDRequest 支付后获取UnionID请求
|
||||
GetPaidUnionIDRequest struct {
|
||||
OpenID string `json:"openid"`
|
||||
TransactionID string `json:"transaction_id,omitempty"`
|
||||
MchID string `json:"mch_id,omitempty"`
|
||||
OutTradeNo string `json:"out_trade_no,omitempty"`
|
||||
}
|
||||
|
||||
// GetPaidUnionIDResponse 支付后获取UnionID响应
|
||||
GetPaidUnionIDResponse struct {
|
||||
util.CommonError
|
||||
UnionID string `json:"unionid"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||
func (auth *Auth) GetPaidUnionID() {
|
||||
// TODO
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPaidUnionid.html
|
||||
func (auth *Auth) GetPaidUnionID(req *GetPaidUnionIDRequest) (string, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var url string
|
||||
if req.TransactionID != "" {
|
||||
url = fmt.Sprintf("%s?access_token=%s&openid=%s&transaction_id=%s", getPaidUnionIDURL, accessToken, req.OpenID, req.TransactionID)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s?access_token=%s&openid=%s&mch_id=%s&out_trade_no=%s", getPaidUnionIDURL, accessToken, req.OpenID, req.MchID, req.OutTradeNo)
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(url); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := &GetPaidUnionIDResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetPaidUnionID")
|
||||
return result.UnionID, err
|
||||
}
|
||||
|
||||
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||
@@ -81,7 +128,7 @@ func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHa
|
||||
var (
|
||||
at string
|
||||
)
|
||||
if at, err = auth.GetAccessToken(); err != nil {
|
||||
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +167,7 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
||||
at string
|
||||
err error
|
||||
)
|
||||
if at, err = auth.GetAccessToken(); err != nil {
|
||||
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
@@ -146,3 +193,115 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
||||
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
||||
return auth.GetPhoneNumberContext(context2.Background(), code)
|
||||
}
|
||||
|
||||
// CheckSession 检验登录态
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/checkSessionKey.html
|
||||
func (auth *Auth) CheckSession(signature, openID string) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(checkSessionURL, accessToken, signature, openID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "CheckSession")
|
||||
}
|
||||
|
||||
// ResetUserSessionKeyResponse 重置登录态响应
|
||||
type ResetUserSessionKeyResponse struct {
|
||||
util.CommonError
|
||||
OpenID string `json:"openid"`
|
||||
SessionKey string `json:"session_key"`
|
||||
}
|
||||
|
||||
// ResetUserSessionKey 重置登录态
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/ResetUserSessionKey.html
|
||||
func (auth *Auth) ResetUserSessionKey(signature, openID string) (*ResetUserSessionKeyResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(resetUserSessionKeyURL, accessToken, signature, openID)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ResetUserSessionKeyResponse{}
|
||||
err = util.DecodeWithError(response, result, "ResetUserSessionKey")
|
||||
return result, err
|
||||
}
|
||||
|
||||
type (
|
||||
// GetPluginOpenPIDRequest 获取插件用户openPID请求
|
||||
GetPluginOpenPIDRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// GetPluginOpenPIDResponse 获取插件用户openPID响应
|
||||
GetPluginOpenPIDResponse struct {
|
||||
util.CommonError
|
||||
OpenPID string `json:"openpid"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetPluginOpenPID 获取插件用户openPID
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPluginOpenPId.html
|
||||
func (auth *Auth) GetPluginOpenPID(code string) (string, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
req := &GetPluginOpenPIDRequest{
|
||||
Code: code,
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getPluginOpenPIDURL, accessToken), req); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := &GetPluginOpenPIDResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetPluginOpenPID")
|
||||
return result.OpenPID, err
|
||||
}
|
||||
|
||||
// GetUserEncryptKeyResponse 获取用户encryptKey响应
|
||||
type GetUserEncryptKeyResponse struct {
|
||||
util.CommonError
|
||||
KeyInfoList []KeyInfo `json:"key_info_list"`
|
||||
}
|
||||
|
||||
// KeyInfo 用户最近三次的加密key
|
||||
type KeyInfo struct {
|
||||
EncryptKey string `json:"encrypt_key"`
|
||||
Version int64 `json:"version"`
|
||||
ExpireIn int64 `json:"expire_in"`
|
||||
Iv string `json:"iv"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
}
|
||||
|
||||
// GetUserEncryptKey 获取用户encryptKey
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/internet/getUserEncryptKey.html
|
||||
func (auth *Auth) GetUserEncryptKey(signature, openID string) (*GetUserEncryptKeyResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getUserEncryptKeyURL, accessToken, signature, openID)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetUserEncryptKeyResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetUserEncryptKey")
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Encryptor struct
|
||||
@@ -108,23 +108,13 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Decrypt 解密数据
|
||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, appid string) (*PlainData, error) {
|
||||
ivB := make([]byte, 16)
|
||||
iv := base64.StdEncoding.EncodeToString(ivB)
|
||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) {
|
||||
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
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
|
||||
err = json.Unmarshal([]byte(cipherTextData), &plainData)
|
||||
err = json.Unmarshal(cipherText, &plainData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
295
miniprogram/express/delivery.go
Normal file
295
miniprogram/express/delivery.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package express
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// 传运单接口,商户使用此接口向微信提供某交易单号对应的运单号。微信后台会跟踪运单的状态变化
|
||||
openMsgTraceWaybillURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/trace_waybill?access_token=%s"
|
||||
|
||||
// 查询运单接口,商户在调用完trace_waybill接口后,可以使用本接口查询到对应运单的详情信息
|
||||
openMsgQueryTraceURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/query_trace?access_token=%s"
|
||||
|
||||
// 更新物流信息,更新物品信息
|
||||
openMsgUpdateWaybillGoodsURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/update_waybill_goods?access_token=%s"
|
||||
|
||||
// 传运单接口,商户使用此接口向微信提供某交易单号对应的运单号。微信后台会跟踪运单的状态变化,在关键物流节点给下单用户推送消息通知
|
||||
openMsgFollowWaybillURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/follow_waybill?access_token=%s"
|
||||
|
||||
// 查运单接口,商户在调用完follow_waybill接口后,可以使用本接口查询到对应运单的详情信息
|
||||
openMsgQueryFollowTraceURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/query_follow_trace?access_token=%s"
|
||||
|
||||
// 更新物品信息接口
|
||||
openMsgUpdateFollowWaybillGoodsURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/update_follow_waybill_goods?access_token=%s"
|
||||
|
||||
// 获取运力id列表
|
||||
openMsgGetDeliveryListURL = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/get_delivery_list?access_token=%s"
|
||||
)
|
||||
|
||||
// TraceWaybill 传运单
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#_2-%E6%8E%A5%E5%8F%A3%E5%88%97%E8%A1%A8
|
||||
func (express *Express) TraceWaybill(ctx context.Context, in *TraceWaybillRequest) (res TraceWaybillResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgTraceWaybillURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "TraceWaybill")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryTrace 查询运单详情信息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#_2-%E6%8E%A5%E5%8F%A3%E5%88%97%E8%A1%A8
|
||||
func (express *Express) QueryTrace(ctx context.Context, in *QueryTraceRequest) (res QueryTraceResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgQueryTraceURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "QueryTrace")
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateWaybillGoods 更新物品信息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#_2-%E6%8E%A5%E5%8F%A3%E5%88%97%E8%A1%A8
|
||||
func (express *Express) UpdateWaybillGoods(ctx context.Context, in *UpdateWaybillGoodsRequest) (err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgUpdateWaybillGoodsURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithCommonError(response, "UpdateWaybillGoods")
|
||||
return
|
||||
}
|
||||
|
||||
// FollowWaybill 传运单
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-1%E3%80%81%E4%BC%A0%E8%BF%90%E5%8D%95%E6%8E%A5%E5%8F%A3-follow-waybill
|
||||
func (express *Express) FollowWaybill(ctx context.Context, in *FollowWaybillRequest) (res FollowWaybillResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgFollowWaybillURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "FollowWaybill")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryFollowTrace 查询运单详情信息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-2%E3%80%81%E6%9F%A5%E8%BF%90%E5%8D%95%E6%8E%A5%E5%8F%A3-query-follow-trace
|
||||
func (express *Express) QueryFollowTrace(ctx context.Context, in *QueryFollowTraceRequest) (res QueryFollowTraceResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgQueryFollowTraceURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "QueryFollowTrace")
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateFollowWaybillGoods 更新物品信息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-3%E3%80%81%E6%9B%B4%E6%96%B0%E7%89%A9%E5%93%81%E4%BF%A1%E6%81%AF%E6%8E%A5%E5%8F%A3-update-follow-waybill-goods
|
||||
func (express *Express) UpdateFollowWaybillGoods(ctx context.Context, in *UpdateFollowWaybillGoodsRequest) (err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgUpdateFollowWaybillGoodsURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithCommonError(response, "UpdateFollowWaybillGoods")
|
||||
return
|
||||
}
|
||||
|
||||
// GetDeliveryList 获取运力id列表
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_open_msg.html#_4-4%E8%8E%B7%E5%8F%96%E8%BF%90%E5%8A%9Bid%E5%88%97%E8%A1%A8get-delivery-list
|
||||
func (express *Express) GetDeliveryList(ctx context.Context) (res GetDeliveryListResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(openMsgGetDeliveryListURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, map[string]interface{}{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "GetDeliveryList")
|
||||
return
|
||||
}
|
||||
|
||||
// TraceWaybillRequest 传运单接口请求参数
|
||||
type TraceWaybillRequest struct {
|
||||
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
|
||||
Openid string `json:"openid"` // 必选,用户openid
|
||||
SenderPhone string `json:"sender_phone"` // 寄件人手机号
|
||||
ReceiverPhone string `json:"receiver_phone"` // 必选,收件人手机号,部分运力需要用户手机号作为查单依据
|
||||
DeliveryID string `json:"delivery_id"` // 运力id(运单号所属运力公司id)
|
||||
WaybillID string `json:"waybill_id"` // 必选,运单号
|
||||
TransID string `json:"trans_id"` // 必选,交易单号(微信支付生成的交易单号,一般以420开头)
|
||||
OrderDetailPath string `json:"order_detail_path"` // 订单详情页地址
|
||||
|
||||
}
|
||||
|
||||
// TraceWaybillResponse 传运单接口返回参数
|
||||
type TraceWaybillResponse struct {
|
||||
util.CommonError
|
||||
WaybillToken string `json:"waybill_token"` // 查询id
|
||||
}
|
||||
|
||||
// QueryTraceRequest 查询运单详情接口请求参数
|
||||
type QueryTraceRequest struct {
|
||||
WaybillToken string `json:"waybill_token"` // 必选,查询id
|
||||
}
|
||||
|
||||
// QueryTraceResponse 查询运单详情接口返回参数
|
||||
type QueryTraceResponse struct {
|
||||
util.CommonError
|
||||
WaybillInfo FlowWaybillInfo `json:"waybill_info"` // 运单信息
|
||||
ShopInfo FollowWaybillShopInfo `json:"shop_info"` // 商品信息
|
||||
DeliveryInfo FlowWaybillDeliveryInfo `json:"delivery_info"` // 运力信息
|
||||
}
|
||||
|
||||
// UpdateWaybillGoodsRequest 更新物品信息接口请求参数
|
||||
type UpdateWaybillGoodsRequest struct {
|
||||
WaybillToken string `json:"waybill_token"` // 必选,查询id
|
||||
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
|
||||
}
|
||||
|
||||
// FollowWaybillRequest 传运单接口请求参数
|
||||
type FollowWaybillRequest struct {
|
||||
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
|
||||
Openid string `json:"openid"` // 必选,用户openid
|
||||
SenderPhone string `json:"sender_phone"` // 寄件人手机号
|
||||
ReceiverPhone string `json:"receiver_phone"` // 必选,收件人手机号,部分运力需要用户手机号作为查单依据
|
||||
DeliveryID string `json:"delivery_id"` // 运力id(运单号所属运力公司id)
|
||||
WaybillID string `json:"waybill_id"` // 必选,运单号
|
||||
TransID string `json:"trans_id"` // 必选,交易单号(微信支付生成的交易单号,一般以420开头)
|
||||
OrderDetailPath string `json:"order_detail_path"` // 订单详情页地址
|
||||
}
|
||||
|
||||
// FollowWaybillGoodsInfo 商品信息
|
||||
type FollowWaybillGoodsInfo struct {
|
||||
DetailList []FollowWaybillGoodsInfoItem `json:"detail_list"`
|
||||
}
|
||||
|
||||
// FollowWaybillShopInfo 商品信息
|
||||
type FollowWaybillShopInfo struct {
|
||||
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 商品信息
|
||||
}
|
||||
|
||||
// FollowWaybillGoodsInfoItem 商品信息详情
|
||||
type FollowWaybillGoodsInfoItem struct {
|
||||
GoodsName string `json:"goods_name"` // 必选,商品名称(最大长度为utf-8编码下的60个字符)
|
||||
GoodsImgURL string `json:"goods_img_url"` // 必选,商品图片url
|
||||
GoodsDesc string `json:"goods_desc,omitempty"` // 商品详情描述,不传默认取“商品名称”值,最多40汉字
|
||||
}
|
||||
|
||||
// FollowWaybillResponse 传运单接口返回参数
|
||||
type FollowWaybillResponse struct {
|
||||
util.CommonError
|
||||
WaybillToken string `json:"waybill_token"` // 查询id
|
||||
}
|
||||
|
||||
// QueryFollowTraceRequest 查询运单详情信息请求参数
|
||||
type QueryFollowTraceRequest struct {
|
||||
WaybillToken string `json:"waybill_token"` // 必选,查询id
|
||||
}
|
||||
|
||||
// QueryFollowTraceResponse 查询运单详情信息返回参数
|
||||
type QueryFollowTraceResponse struct {
|
||||
util.CommonError
|
||||
WaybillInfo FlowWaybillInfo `json:"waybill_info"` // 运单信息
|
||||
ShopInfo FollowWaybillShopInfo `json:"shop_info"` // 商品信息
|
||||
DeliveryInfo FlowWaybillDeliveryInfo `json:"delivery_info"` // 运力信息
|
||||
}
|
||||
|
||||
// FlowWaybillInfo 运单信息
|
||||
type FlowWaybillInfo struct {
|
||||
WaybillID string `json:"waybill_id"` // 运单号
|
||||
Status WaybillStatus `json:"status"` // 运单状态
|
||||
}
|
||||
|
||||
// UpdateFollowWaybillGoodsRequest 修改运单商品信息请求参数
|
||||
type UpdateFollowWaybillGoodsRequest struct {
|
||||
WaybillToken string `json:"waybill_token"` // 必选,查询id
|
||||
GoodsInfo FollowWaybillGoodsInfo `json:"goods_info"` // 必选,商品信息
|
||||
}
|
||||
|
||||
// GetDeliveryListResponse 获取运力id列表返回参数
|
||||
type GetDeliveryListResponse struct {
|
||||
util.CommonError
|
||||
DeliveryList []FlowWaybillDeliveryInfo `json:"delivery_list"` // 运力公司列表
|
||||
Count int `json:"count"` // 运力公司个数
|
||||
}
|
||||
|
||||
// FlowWaybillDeliveryInfo 运力公司信息
|
||||
type FlowWaybillDeliveryInfo struct {
|
||||
DeliveryID string `json:"delivery_id"` // 运力公司id
|
||||
DeliveryName string `json:"delivery_name"` // 运力公司名称
|
||||
}
|
||||
|
||||
// WaybillStatus 运单状态
|
||||
type WaybillStatus int
|
||||
|
||||
const (
|
||||
// WaybillStatusNotExist 运单不存在或者未揽收
|
||||
WaybillStatusNotExist WaybillStatus = iota
|
||||
// WaybillStatusPicked 已揽件
|
||||
WaybillStatusPicked
|
||||
// WaybillStatusTransporting 运输中
|
||||
WaybillStatusTransporting
|
||||
// WaybillStatusDispatching 派件中
|
||||
WaybillStatusDispatching
|
||||
// WaybillStatusSigned 已签收
|
||||
WaybillStatusSigned
|
||||
// WaybillStatusException 异常
|
||||
WaybillStatusException
|
||||
// WaybillStatusSignedByOthers 代签收
|
||||
WaybillStatusSignedByOthers
|
||||
)
|
||||
16
miniprogram/express/express.go
Normal file
16
miniprogram/express/express.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package express
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
)
|
||||
|
||||
// Express 微信物流服务
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/introduction.html
|
||||
type Express struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewExpress init
|
||||
func NewExpress(ctx *context.Context) *Express {
|
||||
return &Express{ctx}
|
||||
}
|
||||
618
miniprogram/express/intracity.go
Normal file
618
miniprogram/express/intracity.go
Normal file
@@ -0,0 +1,618 @@
|
||||
package express
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// 同城配送 API URL
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/intracity_service.html
|
||||
const (
|
||||
// 开通门店权限
|
||||
intracityApplyURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/apply?access_token=%s"
|
||||
// 创建门店
|
||||
intracityCreateStoreURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/createstore?access_token=%s"
|
||||
// 查询门店
|
||||
intracityQueryStoreURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/querystore?access_token=%s"
|
||||
// 更新门店
|
||||
intracityUpdateStoreURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/updatestore?access_token=%s"
|
||||
// 门店运费充值
|
||||
intracityStoreChargeURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/storecharge?access_token=%s"
|
||||
// 门店运费退款
|
||||
intracityStoreRefundURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/storerefund?access_token=%s"
|
||||
// 门店运费流水查询
|
||||
intracityQueryFlowURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/queryflow?access_token=%s"
|
||||
// 门店余额查询
|
||||
intracityBalanceQueryURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/balancequery?access_token=%s"
|
||||
// 预下配送单(查询运费)
|
||||
intracityPreAddOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/preaddorder?access_token=%s"
|
||||
// 创建配送单
|
||||
intracityAddOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/addorder?access_token=%s"
|
||||
// 查询配送单
|
||||
intracityQueryOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/queryorder?access_token=%s"
|
||||
// 取消配送单
|
||||
intracityCancelOrderURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/cancelorder?access_token=%s"
|
||||
// 模拟配送回调(仅用于测试)
|
||||
intracityMockNotifyURL = "https://api.weixin.qq.com/cgi-bin/express/intracity/mocknotify?access_token=%s"
|
||||
)
|
||||
|
||||
// PayMode 充值/扣费主体
|
||||
type PayMode string
|
||||
|
||||
const (
|
||||
// PayModeStore 门店
|
||||
PayModeStore PayMode = "PAY_MODE_STORE"
|
||||
// PayModeApp 小程序
|
||||
PayModeApp PayMode = "PAY_MODE_APP"
|
||||
// PayModeComponent 服务商
|
||||
PayModeComponent PayMode = "PAY_MODE_COMPONENT"
|
||||
)
|
||||
|
||||
// OrderPattern 运力偏好
|
||||
type OrderPattern uint32
|
||||
|
||||
const (
|
||||
// OrderPatternPriceFirst 价格优先
|
||||
OrderPatternPriceFirst OrderPattern = 1
|
||||
// OrderPatternTransFirst 运力优先
|
||||
OrderPatternTransFirst OrderPattern = 2
|
||||
)
|
||||
|
||||
// FlowType 流水类型
|
||||
type FlowType uint32
|
||||
|
||||
const (
|
||||
// FlowTypeCharge 充值流水
|
||||
FlowTypeCharge FlowType = 1
|
||||
// FlowTypeConsume 消费流水
|
||||
FlowTypeConsume FlowType = 2
|
||||
// FlowTypeRefund 退款流水
|
||||
FlowTypeRefund FlowType = 3
|
||||
)
|
||||
|
||||
// IntracityDeliveryStatus 配送单状态
|
||||
type IntracityDeliveryStatus int32
|
||||
|
||||
const (
|
||||
// IntracityDeliveryStatusReady 配送单待接单
|
||||
IntracityDeliveryStatusReady IntracityDeliveryStatus = 100
|
||||
// IntracityDeliveryStatusPickedUp 配送单待取货
|
||||
IntracityDeliveryStatusPickedUp IntracityDeliveryStatus = 101
|
||||
// IntracityDeliveryStatusOngoing 配送单配送中
|
||||
IntracityDeliveryStatusOngoing IntracityDeliveryStatus = 102
|
||||
// IntracityDeliveryStatusFinished 配送单已送达
|
||||
IntracityDeliveryStatusFinished IntracityDeliveryStatus = 200
|
||||
// IntracityDeliveryStatusCancelled 配送单已取消
|
||||
IntracityDeliveryStatusCancelled IntracityDeliveryStatus = 300
|
||||
// IntracityDeliveryStatusAbnormal 配送单异常
|
||||
IntracityDeliveryStatusAbnormal IntracityDeliveryStatus = 400
|
||||
)
|
||||
|
||||
// IntracityAddressInfo 门店地址信息
|
||||
type IntracityAddressInfo struct {
|
||||
Province string `json:"province"` // 省/自治区/直辖市
|
||||
City string `json:"city"` // 地级市
|
||||
Area string `json:"area"` // 县/县级市/区
|
||||
Street string `json:"street"` // 街道
|
||||
House string `json:"house"` // 具体门牌号或详细地址
|
||||
Lat float64 `json:"lat"` // 门店所在地纬度
|
||||
Lng float64 `json:"lng"` // 门店所在地经度
|
||||
Phone string `json:"phone"` // 门店联系电话
|
||||
Name string `json:"name,omitempty"` // 联系人姓名(收货地址时使用)
|
||||
}
|
||||
|
||||
// IntracityStoreInfo 门店信息
|
||||
type IntracityStoreInfo struct {
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
OutStoreID string `json:"out_store_id"` // 自定义门店编号
|
||||
CityID string `json:"city_id"` // 门店所在城市ID
|
||||
StoreName string `json:"store_name"` // 门店名称
|
||||
OrderPattern OrderPattern `json:"order_pattern"` // 运力偏好
|
||||
ServiceTransPrefer string `json:"service_trans_prefer"` // 优先使用的运力ID
|
||||
AddressInfo IntracityAddressInfo `json:"address_info"` // 门店地址信息
|
||||
}
|
||||
|
||||
// ============ 门店管理接口 ============
|
||||
|
||||
// IntracityApply 开通门店权限
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/intracity_service.html
|
||||
func (express *Express) IntracityApply(ctx context.Context) error {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityApplyURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, map[string]interface{}{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "IntracityApply")
|
||||
}
|
||||
|
||||
// CreateStoreRequest 创建门店请求参数
|
||||
type CreateStoreRequest struct {
|
||||
OutStoreID string `json:"out_store_id"` // 自定义门店编号
|
||||
StoreName string `json:"store_name"` // 门店名称
|
||||
OrderPattern OrderPattern `json:"order_pattern,omitempty"` // 运力偏好:1-价格优先,2-运力优先
|
||||
ServiceTransPrefer string `json:"service_trans_prefer,omitempty"` // 优先使用的运力ID,order_pattern=2时必填
|
||||
AddressInfo IntracityAddressInfo `json:"address_info"` // 门店地址信息
|
||||
}
|
||||
|
||||
// CreateStoreResponse 创建门店返回参数
|
||||
type CreateStoreResponse struct {
|
||||
util.CommonError
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
OutStoreID string `json:"out_store_id"` // 自定义门店ID
|
||||
}
|
||||
|
||||
// IntracityCreateStore 创建门店
|
||||
func (express *Express) IntracityCreateStore(ctx context.Context, req *CreateStoreRequest) (res CreateStoreResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityCreateStoreURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityCreateStore")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryStoreRequest 查询门店请求参数
|
||||
type QueryStoreRequest struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
|
||||
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号
|
||||
}
|
||||
|
||||
// QueryStoreResponse 查询门店返回参数
|
||||
type QueryStoreResponse struct {
|
||||
util.CommonError
|
||||
Total uint32 `json:"total"` // 符合条件的门店总数
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
StoreList []IntracityStoreInfo `json:"store_list"` // 门店信息列表
|
||||
}
|
||||
|
||||
// IntracityQueryStore 查询门店
|
||||
func (express *Express) IntracityQueryStore(ctx context.Context, req *QueryStoreRequest) (res QueryStoreResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityQueryStoreURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityQueryStore")
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateStoreKeyInfo 更新门店的key信息
|
||||
type UpdateStoreKeyInfo struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
|
||||
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号,二选一
|
||||
}
|
||||
|
||||
// UpdateStoreContent 更新门店的内容
|
||||
type UpdateStoreContent struct {
|
||||
StoreName string `json:"store_name,omitempty"` // 门店名称
|
||||
OrderPattern OrderPattern `json:"order_pattern,omitempty"` // 运力偏好
|
||||
ServiceTransPrefer string `json:"service_trans_prefer,omitempty"` // 优先使用的运力ID
|
||||
AddressInfo *IntracityAddressInfo `json:"address_info,omitempty"` // 门店地址信息
|
||||
}
|
||||
|
||||
// UpdateStoreRequest 更新门店请求参数
|
||||
type UpdateStoreRequest struct {
|
||||
Keys UpdateStoreKeyInfo `json:"keys"` // 门店编号
|
||||
Content UpdateStoreContent `json:"content"` // 更新内容
|
||||
}
|
||||
|
||||
// IntracityUpdateStore 更新门店
|
||||
func (express *Express) IntracityUpdateStore(ctx context.Context, req *UpdateStoreRequest) error {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityUpdateStoreURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "IntracityUpdateStore")
|
||||
}
|
||||
|
||||
// ============ 充值退款接口 ============
|
||||
|
||||
// StoreChargeRequest 门店运费充值请求参数
|
||||
type StoreChargeRequest struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号,pay_mode=PAY_MODE_STORE时必传
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
Amount uint32 `json:"amount"` // 充值金额,单位:分,50元起充
|
||||
PayMode PayMode `json:"pay_mode,omitempty"` // 充值主体
|
||||
}
|
||||
|
||||
// StoreChargeResponse 门店运费充值返回参数
|
||||
type StoreChargeResponse struct {
|
||||
util.CommonError
|
||||
PayURL string `json:"payurl"` // 充值页面地址
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
}
|
||||
|
||||
// IntracityStoreCharge 门店运费充值
|
||||
func (express *Express) IntracityStoreCharge(ctx context.Context, req *StoreChargeRequest) (res StoreChargeResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityStoreChargeURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityStoreCharge")
|
||||
return
|
||||
}
|
||||
|
||||
// StoreRefundRequest 门店运费退款请求参数
|
||||
type StoreRefundRequest struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
|
||||
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
}
|
||||
|
||||
// StoreRefundResponse 门店运费退款返回参数
|
||||
type StoreRefundResponse struct {
|
||||
util.CommonError
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
RefundAmount uint32 `json:"refund_amount"` // 退款金额,单位:分
|
||||
}
|
||||
|
||||
// IntracityStoreRefund 门店运费退款
|
||||
func (express *Express) IntracityStoreRefund(ctx context.Context, req *StoreRefundRequest) (res StoreRefundResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityStoreRefundURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityStoreRefund")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryFlowRequest 门店运费流水查询请求参数
|
||||
type QueryFlowRequest struct {
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
FlowType FlowType `json:"flow_type"` // 流水类型:1-充值,2-消费,3-退款
|
||||
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID
|
||||
BeginTime uint32 `json:"begin_time,omitempty"` // 开始时间戳
|
||||
EndTime uint32 `json:"end_time,omitempty"` // 结束时间戳
|
||||
PayMode PayMode `json:"pay_mode"` // 扣费主体
|
||||
}
|
||||
|
||||
// FlowRecordInfo 流水记录信息
|
||||
type FlowRecordInfo struct {
|
||||
FlowType FlowType `json:"flow_type"` // 流水类型
|
||||
AppID string `json:"appid"` // appid
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店ID
|
||||
PayOrderID uint64 `json:"pay_order_id,omitempty"` // 充值订单号
|
||||
WxOrderID string `json:"wx_order_id,omitempty"` // 订单ID(消费流水)
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
OpenID string `json:"openid,omitempty"` // 用户openid(消费流水)
|
||||
DeliveryStatus int32 `json:"delivery_status,omitempty"` // 运单状态(消费流水)
|
||||
PayAmount int32 `json:"pay_amount"` // 支付金额,单位:分
|
||||
PayTime uint32 `json:"pay_time,omitempty"` // 支付时间
|
||||
PayStatus string `json:"pay_status,omitempty"` // 支付状态
|
||||
RefundStatus string `json:"refund_status,omitempty"` // 退款状态
|
||||
RefundAmount int32 `json:"refund_amount,omitempty"` // 退款金额
|
||||
RefundTime uint32 `json:"refund_time,omitempty"` // 退款时间
|
||||
DeductAmount int32 `json:"deduct_amount,omitempty"` // 扣除违约金
|
||||
CreateTime uint32 `json:"create_time"` // 创建时间
|
||||
ConsumeDeadline uint32 `json:"consume_deadline,omitempty"` // 有效截止日期
|
||||
BillID string `json:"bill_id,omitempty"` // 运单ID
|
||||
DeliveryFinishedTime uint32 `json:"delivery_finished_time,omitempty"` // 运单完成配送的时间
|
||||
}
|
||||
|
||||
// QueryFlowResponse 门店运费流水查询返回参数
|
||||
type QueryFlowResponse struct {
|
||||
util.CommonError
|
||||
Total uint32 `json:"total"` // 总数
|
||||
FlowList []FlowRecordInfo `json:"flow_list"` // 流水数组
|
||||
TotalPayAmt int `json:"total_pay_amt"` // 总支付金额
|
||||
TotalRefundAmt int `json:"total_refund_amt"` // 总退款金额
|
||||
TotalDeductAmt int `json:"total_deduct_amt"` // 总违约金(消费流水返回)
|
||||
}
|
||||
|
||||
// IntracityQueryFlow 门店运费流水查询
|
||||
func (express *Express) IntracityQueryFlow(ctx context.Context, req *QueryFlowRequest) (res QueryFlowResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityQueryFlowURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityQueryFlow")
|
||||
return
|
||||
}
|
||||
|
||||
// BalanceQueryRequest 门店余额查询请求参数
|
||||
type BalanceQueryRequest struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
|
||||
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID
|
||||
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
|
||||
}
|
||||
|
||||
// BalanceInfo 余额信息
|
||||
type BalanceInfo struct {
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
Balance int32 `json:"balance"` // 余额,单位:分
|
||||
}
|
||||
|
||||
// BalanceQueryResponse 门店余额查询返回参数
|
||||
type BalanceQueryResponse struct {
|
||||
util.CommonError
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
BalanceList []BalanceInfo `json:"balance_list"` // 余额列表
|
||||
}
|
||||
|
||||
// IntracityBalanceQuery 门店余额查询
|
||||
func (express *Express) IntracityBalanceQuery(ctx context.Context, req *BalanceQueryRequest) (res BalanceQueryResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityBalanceQueryURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityBalanceQuery")
|
||||
return
|
||||
}
|
||||
|
||||
// ============ 配送订单接口 ============
|
||||
|
||||
// CargoInfo 货物信息
|
||||
type CargoInfo struct {
|
||||
Name string `json:"name"` // 货物名称
|
||||
Num uint32 `json:"num,omitempty"` // 货物数量
|
||||
Price uint32 `json:"price,omitempty"` // 货物价格,单位:分
|
||||
Weight uint32 `json:"weight,omitempty"` // 货物重量,单位:克
|
||||
}
|
||||
|
||||
// PreAddOrderRequest 预下配送单请求参数
|
||||
type PreAddOrderRequest struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号,二选一
|
||||
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号,二选一
|
||||
UserOpenID string `json:"user_openid"` // 用户openid
|
||||
UserPhone string `json:"user_phone,omitempty"` // 用户联系电话
|
||||
UserName string `json:"user_name,omitempty"` // 用户姓名
|
||||
UserLat float64 `json:"user_lat"` // 用户地址纬度
|
||||
UserLng float64 `json:"user_lng"` // 用户地址经度
|
||||
UserAddress string `json:"user_address"` // 用户详细地址
|
||||
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID,不传则查询所有运力
|
||||
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
|
||||
CargoInfo *CargoInfo `json:"cargo_info,omitempty"` // 货物信息
|
||||
}
|
||||
|
||||
// TransInfo 运力信息
|
||||
type TransInfo struct {
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
ServiceName string `json:"service_name"` // 运力名称
|
||||
Price uint32 `json:"price"` // 配送费用,单位:分
|
||||
Distance uint32 `json:"distance"` // 配送距离,单位:米
|
||||
Errcode int `json:"errcode"` // 错误码,0表示成功
|
||||
Errmsg string `json:"errmsg"` // 错误信息
|
||||
}
|
||||
|
||||
// PreAddOrderResponse 预下配送单返回参数
|
||||
type PreAddOrderResponse struct {
|
||||
util.CommonError
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
TransList []TransInfo `json:"trans_list"` // 运力列表
|
||||
}
|
||||
|
||||
// IntracityPreAddOrder 预下配送单(查询运费)
|
||||
func (express *Express) IntracityPreAddOrder(ctx context.Context, req *PreAddOrderRequest) (res PreAddOrderResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityPreAddOrderURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityPreAddOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// AddOrderRequest 创建配送单请求参数
|
||||
type AddOrderRequest struct {
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号,二选一
|
||||
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号,二选一
|
||||
OutOrderID string `json:"out_order_id"` // 自定义订单号,需唯一
|
||||
ServiceTransID string `json:"service_trans_id,omitempty"` // 运力ID
|
||||
UserOpenID string `json:"user_openid"` // 用户openid
|
||||
UserPhone string `json:"user_phone"` // 用户联系电话
|
||||
UserName string `json:"user_name"` // 用户姓名
|
||||
UserLat float64 `json:"user_lat"` // 用户地址纬度
|
||||
UserLng float64 `json:"user_lng"` // 用户地址经度
|
||||
UserAddress string `json:"user_address"` // 用户详细地址
|
||||
PayMode PayMode `json:"pay_mode,omitempty"` // 充值/扣费主体
|
||||
CargoInfo *CargoInfo `json:"cargo_info,omitempty"` // 货物信息
|
||||
OrderDetailPath string `json:"order_detail_path,omitempty"` // 订单详情页路径
|
||||
CallbackURL string `json:"callback_url,omitempty"` // 配送状态回调URL
|
||||
UseInsurance uint32 `json:"use_insurance,omitempty"` // 是否使用保价:0-不使用,1-使用
|
||||
InsuranceValue uint32 `json:"insurance_value,omitempty"` // 保价金额,单位:分
|
||||
ExpectTime uint32 `json:"expect_time,omitempty"` // 期望送达时间戳
|
||||
Remark string `json:"remark,omitempty"` // 备注
|
||||
}
|
||||
|
||||
// AddOrderResponse 创建配送单返回参数
|
||||
type AddOrderResponse struct {
|
||||
util.CommonError
|
||||
WxOrderID string `json:"wx_order_id"` // 微信订单号
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
OutOrderID string `json:"out_order_id"` // 自定义订单号
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
BillID string `json:"bill_id"` // 运力订单号
|
||||
Price uint32 `json:"price"` // 配送费用,单位:分
|
||||
Distance uint32 `json:"distance"` // 配送距离,单位:米
|
||||
}
|
||||
|
||||
// IntracityAddOrder 创建配送单
|
||||
func (express *Express) IntracityAddOrder(ctx context.Context, req *AddOrderRequest) (res AddOrderResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityAddOrderURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityAddOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryOrderRequest 查询配送单请求参数
|
||||
type QueryOrderRequest struct {
|
||||
WxOrderID string `json:"wx_order_id,omitempty"` // 微信订单号,二选一
|
||||
OutOrderID string `json:"out_order_id,omitempty"` // 自定义订单号,二选一
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
|
||||
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号
|
||||
}
|
||||
|
||||
// RiderInfo 骑手信息
|
||||
type RiderInfo struct {
|
||||
Name string `json:"name"` // 骑手姓名
|
||||
Phone string `json:"phone"` // 骑手电话
|
||||
RiderCode string `json:"rider_code"` // 骑手编号
|
||||
RiderImgURL string `json:"rider_img_url"` // 骑手头像URL
|
||||
}
|
||||
|
||||
// QueryOrderResponse 查询配送单返回参数
|
||||
type QueryOrderResponse struct {
|
||||
util.CommonError
|
||||
WxOrderID string `json:"wx_order_id"` // 微信订单号
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
WxStoreID string `json:"wx_store_id"` // 微信门店编号
|
||||
OutOrderID string `json:"out_order_id"` // 自定义订单号
|
||||
ServiceTransID string `json:"service_trans_id"` // 运力ID
|
||||
BillID string `json:"bill_id"` // 运力订单号
|
||||
DeliveryStatus IntracityDeliveryStatus `json:"delivery_status"` // 配送状态
|
||||
Price uint32 `json:"price"` // 配送费用,单位:分
|
||||
Distance uint32 `json:"distance"` // 配送距离,单位:米
|
||||
CreateTime uint32 `json:"create_time"` // 订单创建时间
|
||||
RiderInfo *RiderInfo `json:"rider_info"` // 骑手信息
|
||||
FinishTime uint32 `json:"finish_time"` // 订单完成时间
|
||||
}
|
||||
|
||||
// IntracityQueryOrder 查询配送单
|
||||
func (express *Express) IntracityQueryOrder(ctx context.Context, req *QueryOrderRequest) (res QueryOrderResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityQueryOrderURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityQueryOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// CancelOrderRequest 取消配送单请求参数
|
||||
type CancelOrderRequest struct {
|
||||
WxOrderID string `json:"wx_order_id,omitempty"` // 微信订单号,二选一
|
||||
OutOrderID string `json:"out_order_id,omitempty"` // 自定义订单号,二选一
|
||||
WxStoreID string `json:"wx_store_id,omitempty"` // 微信门店编号
|
||||
OutStoreID string `json:"out_store_id,omitempty"` // 自定义门店编号
|
||||
CancelReason string `json:"cancel_reason,omitempty"` // 取消原因
|
||||
}
|
||||
|
||||
// CancelOrderResponse 取消配送单返回参数
|
||||
type CancelOrderResponse struct {
|
||||
util.CommonError
|
||||
WxOrderID string `json:"wx_order_id"` // 微信订单号
|
||||
RefundAmount int32 `json:"refund_amount"` // 退款金额,单位:分
|
||||
DeductAmount int32 `json:"deduct_amount"` // 扣除违约金,单位:分
|
||||
}
|
||||
|
||||
// IntracityCancelOrder 取消配送单
|
||||
func (express *Express) IntracityCancelOrder(ctx context.Context, req *CancelOrderRequest) (res CancelOrderResponse, err error) {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityCancelOrderURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &res, "IntracityCancelOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// MockNotifyRequest 模拟配送回调请求参数(仅用于测试)
|
||||
type MockNotifyRequest struct {
|
||||
WxOrderID string `json:"wx_order_id"` // 微信订单号
|
||||
DeliveryStatus IntracityDeliveryStatus `json:"delivery_status"` // 配送状态
|
||||
}
|
||||
|
||||
// IntracityMockNotify 模拟配送回调(仅用于测试)
|
||||
func (express *Express) IntracityMockNotify(ctx context.Context, req *MockNotifyRequest) error {
|
||||
accessToken, err := express.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(intracityMockNotifyURL, accessToken)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "IntracityMockNotify")
|
||||
}
|
||||
@@ -556,7 +556,7 @@ type SubscribeMsgSentEvent struct {
|
||||
type SubscribeMsgSentList struct {
|
||||
TemplateID string `xml:"TemplateId" json:"TemplateId"`
|
||||
MsgID string `xml:"MsgID" json:"MsgID"`
|
||||
ErrorCode int `xml:"ErrorCode" json:"ErrorCode"`
|
||||
ErrorCode string `xml:"ErrorCode" json:"ErrorCode"`
|
||||
ErrorStatus string `xml:"ErrorStatus" json:"ErrorStatus"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// createActivityURL 创建activity_id
|
||||
createActivityURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s"
|
||||
// createActivityIDURL 创建activity_id
|
||||
createActivityIDURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s&unionid=%s&openid=%s"
|
||||
// SendUpdatableMsgURL 修改动态消息
|
||||
setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s"
|
||||
// setChatToolMsgURL 修改小程序聊天工具的动态卡片消息
|
||||
setChatToolMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/chattoolmsg/send?access_token=%s"
|
||||
)
|
||||
|
||||
// UpdatableTargetState 动态消息状态
|
||||
@@ -38,15 +40,26 @@ func NewUpdatableMessage(ctx *context.Context) *UpdatableMessage {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateActivityIDRequest 创建activity_id请求
|
||||
type CreateActivityIDRequest struct {
|
||||
UnionID string
|
||||
OpenID string
|
||||
}
|
||||
|
||||
// CreateActivityID 创建activity_id
|
||||
func (updatableMessage *UpdatableMessage) CreateActivityID() (res CreateActivityIDResponse, err error) {
|
||||
func (updatableMessage *UpdatableMessage) CreateActivityID() (CreateActivityIDResponse, error) {
|
||||
return updatableMessage.CreateActivityIDWithReq(&CreateActivityIDRequest{})
|
||||
}
|
||||
|
||||
// CreateActivityIDWithReq 创建activity_id
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/updatable-message/createActivityId.html
|
||||
func (updatableMessage *UpdatableMessage) CreateActivityIDWithReq(req *CreateActivityIDRequest) (res CreateActivityIDResponse, err error) {
|
||||
accessToken, err := updatableMessage.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(createActivityURL, accessToken)
|
||||
response, err := util.HTTPGet(uri)
|
||||
url := fmt.Sprintf(createActivityIDURL, accessToken, req.UnionID, req.OpenID)
|
||||
response, err := util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -100,3 +113,35 @@ type SendUpdatableMsgReq struct {
|
||||
TemplateInfo UpdatableMsgTemplate `json:"template_info"`
|
||||
TargetState UpdatableTargetState `json:"target_state"`
|
||||
}
|
||||
|
||||
// SetChatToolMsgRequest 修改小程序聊天工具的动态卡片消息请求
|
||||
type SetChatToolMsgRequest struct {
|
||||
VersionType int64 `json:"version_type"`
|
||||
TargetState UpdatableTargetState `json:"target_state"`
|
||||
ActivityID string `json:"activity_id"`
|
||||
TemplateID string `json:"template_id"`
|
||||
ParticipatorInfoList []ParticipatorInfo `json:"participator_info_list,omitempty"`
|
||||
}
|
||||
|
||||
// ParticipatorInfo 更新后的聊天室成员状态
|
||||
type ParticipatorInfo struct {
|
||||
State int `json:"state"`
|
||||
GroupOpenID string `json:"group_openid"`
|
||||
}
|
||||
|
||||
// SetChatToolMsg 修改小程序聊天工具的动态卡片消息
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/updatable-message/setChatToolMsg.html
|
||||
func (updatableMessage *UpdatableMessage) SetChatToolMsg(req *SetChatToolMsgRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = updatableMessage.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(setChatToolMsgURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "SetChatToolMsg")
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
"github.com/silenceper/wechat/v2/miniprogram/content"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/express"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/message"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/minidrama"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/ocr"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/operation"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/order"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
||||
@@ -179,3 +182,23 @@ func (miniProgram *MiniProgram) GetRedPacketCover() *redpacketcover.RedPacketCov
|
||||
func (miniProgram *MiniProgram) GetUpdatableMessage() *message.UpdatableMessage {
|
||||
return message.NewUpdatableMessage(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetOperation 小程序运维中心
|
||||
func (miniProgram *MiniProgram) GetOperation() *operation.Operation {
|
||||
return operation.NewOperation(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetExpress 微信物流服务
|
||||
func (miniProgram *MiniProgram) GetExpress() *express.Express {
|
||||
return express.NewExpress(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetOCR OCR接口
|
||||
func (miniProgram *MiniProgram) GetOCR() *ocr.OCR {
|
||||
return ocr.NewOCR(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetIntracity 同城配送接口
|
||||
func (miniProgram *MiniProgram) GetIntracity() *express.Express {
|
||||
return express.NewExpress(miniProgram.ctx)
|
||||
}
|
||||
|
||||
248
miniprogram/ocr/ocr.go
Normal file
248
miniprogram/ocr/ocr.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
ocrIDCardURL = "https://api.weixin.qq.com/cv/ocr/idcard"
|
||||
ocrBankCardURL = "https://api.weixin.qq.com/cv/ocr/bankcard"
|
||||
ocrDrivingURL = "https://api.weixin.qq.com/cv/ocr/driving"
|
||||
ocrDrivingLicenseURL = "https://api.weixin.qq.com/cv/ocr/drivinglicense"
|
||||
ocrBizLicenseURL = "https://api.weixin.qq.com/cv/ocr/bizlicense"
|
||||
ocrCommonURL = "https://api.weixin.qq.com/cv/ocr/comm"
|
||||
)
|
||||
|
||||
// OCR struct
|
||||
type OCR struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// coordinate 坐标
|
||||
type coordinate struct {
|
||||
X int64 `json:"x,omitempty"`
|
||||
Y int64 `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
// position 位置
|
||||
type position struct {
|
||||
LeftTop coordinate `json:"left_top"`
|
||||
RightTop coordinate `json:"right_top"`
|
||||
RightBottom coordinate `json:"right_bottom"`
|
||||
LeftBottom coordinate `json:"left_bottom"`
|
||||
}
|
||||
|
||||
// imageSize 图片尺寸
|
||||
type imageSize struct {
|
||||
Width int64 `json:"w,omitempty"`
|
||||
Height int64 `json:"h,omitempty"`
|
||||
}
|
||||
|
||||
// ResDriving 行驶证返回结果
|
||||
type ResDriving struct {
|
||||
util.CommonError
|
||||
|
||||
PlateNumber string `json:"plate_num,omitempty"`
|
||||
VehicleType string `json:"vehicle_type,omitempty"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Address string `json:"addr,omitempty"`
|
||||
UseCharacter string `json:"use_character,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Vin string `json:"vin,omitempty"`
|
||||
EngineNumber string `json:"engine_num,omitempty"`
|
||||
RegisterDate string `json:"register_date,omitempty"`
|
||||
IssueDate string `json:"issue_date,omitempty"`
|
||||
PlateNumberB string `json:"plate_num_b,omitempty"`
|
||||
Record string `json:"record,omitempty"`
|
||||
PassengersNumber string `json:"passengers_num,omitempty"`
|
||||
TotalQuality string `json:"total_quality,omitempty"`
|
||||
PrepareQuality string `json:"prepare_quality,omitempty"`
|
||||
OverallSize string `json:"overall_size,omitempty"`
|
||||
CardPositionFront map[string]position `json:"card_position_front,omitempty"`
|
||||
CardPositionBack map[string]position `json:"card_position_back,omitempty"`
|
||||
ImageSize imageSize `json:"img_size,omitempty"`
|
||||
}
|
||||
|
||||
// ResIDCard 身份证返回结果
|
||||
type ResIDCard struct {
|
||||
util.CommonError
|
||||
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Address string `json:"addr,omitempty"`
|
||||
Gender string `json:"gender,omitempty"`
|
||||
Nationality string `json:"nationality,omitempty"`
|
||||
ValidDate string `json:"valid_date,omitempty"`
|
||||
}
|
||||
|
||||
// ResBankCard 银行卡返回结果
|
||||
type ResBankCard struct {
|
||||
util.CommonError
|
||||
|
||||
Number string `json:"number,omitempty"`
|
||||
}
|
||||
|
||||
// ResDrivingLicense 驾驶证返回结果
|
||||
type ResDrivingLicense struct {
|
||||
util.CommonError
|
||||
|
||||
IDNumber string `json:"id_num,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Sex string `json:"sex,omitempty"`
|
||||
Nationality string `json:"nationality,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Birthday string `json:"birth_date,omitempty"`
|
||||
IssueDate string `json:"issue_date,omitempty"`
|
||||
CarClass string `json:"car_class,omitempty"`
|
||||
ValidFrom string `json:"valid_from,omitempty"`
|
||||
ValidTo string `json:"valid_to,omitempty"`
|
||||
OfficialSeal string `json:"official_seal,omitempty"`
|
||||
}
|
||||
|
||||
// ResBizLicense 营业执照返回结果
|
||||
type ResBizLicense struct {
|
||||
util.CommonError
|
||||
|
||||
RegisterNumber string `json:"reg_num,omitempty"`
|
||||
Serial string `json:"serial,omitempty"`
|
||||
LegalRepresentative string `json:"legal_representative,omitempty"`
|
||||
EnterpriseName string `json:"enterprise_name,omitempty"`
|
||||
TypeOfOrganization string `json:"type_of_organization,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
TypeOfEnterprise string `json:"type_of_enterprise,omitempty"`
|
||||
BusinessScope string `json:"business_scope,omitempty"`
|
||||
RegisteredCapital string `json:"registered_capital,omitempty"`
|
||||
PaidInCapital string `json:"paid_in_capital,omitempty"`
|
||||
ValidPeriod string `json:"valid_period,omitempty"`
|
||||
RegisterDate string `json:"registered_date,omitempty"`
|
||||
CertPosition map[string]position `json:"cert_position,omitempty"`
|
||||
ImageSize imageSize `json:"img_size,omitempty"`
|
||||
}
|
||||
|
||||
// ResCommon 公共印刷品返回结果
|
||||
type ResCommon struct {
|
||||
util.CommonError
|
||||
|
||||
Items []commonItem `json:"items,omitempty"`
|
||||
ImageSize imageSize `json:"img_size,omitempty"`
|
||||
}
|
||||
|
||||
// commonItem 公共元素
|
||||
type commonItem struct {
|
||||
Position position `json:"pos"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// NewOCR 实例
|
||||
func NewOCR(c *context.Context) *OCR {
|
||||
ocr := new(OCR)
|
||||
ocr.Context = c
|
||||
return ocr
|
||||
}
|
||||
|
||||
// IDCard 身份证OCR识别接口
|
||||
func (ocr *OCR) IDCard(path string) (resIDCard ResIDCard, err error) {
|
||||
accessToken, err := ocr.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrIDCardURL, url.QueryEscape(path), accessToken), "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resIDCard, "OCRIDCard")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BankCard 银行卡OCR识别接口
|
||||
func (ocr *OCR) BankCard(path string) (resBankCard ResBankCard, err error) {
|
||||
accessToken, err := ocr.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBankCardURL, url.QueryEscape(path), accessToken), "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resBankCard, "OCRBankCard")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Driving 行驶证OCR识别接口
|
||||
func (ocr *OCR) Driving(path string) (resDriving ResDriving, err error) {
|
||||
accessToken, err := ocr.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingURL, url.QueryEscape(path), accessToken), "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resDriving, "OCRDriving")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DrivingLicense 驾驶证OCR识别接口
|
||||
func (ocr *OCR) DrivingLicense(path string) (resDrivingLicense ResDrivingLicense, err error) {
|
||||
accessToken, err := ocr.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingLicenseURL, url.QueryEscape(path), accessToken), "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resDrivingLicense, "OCRDrivingLicense")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BizLicense 营业执照OCR识别接口
|
||||
func (ocr *OCR) BizLicense(path string) (resBizLicense ResBizLicense, err error) {
|
||||
accessToken, err := ocr.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBizLicenseURL, url.QueryEscape(path), accessToken), "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resBizLicense, "OCRBizLicense")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Common 通用印刷体OCR识别接口
|
||||
func (ocr *OCR) Common(path string) (resCommon ResCommon, err error) {
|
||||
accessToken, err := ocr.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrCommonURL, url.QueryEscape(path), accessToken), "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resCommon, "OCRCommon")
|
||||
|
||||
return
|
||||
}
|
||||
456
miniprogram/operation/operation.go
Normal file
456
miniprogram/operation/operation.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package operation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// getDomainInfoURL 查询域名配置
|
||||
getDomainInfoURL = "https://api.weixin.qq.com/wxa/getwxadevinfo?access_token=%s"
|
||||
// getPerformanceURL 获取性能数据
|
||||
getPerformanceURL = "https://api.weixin.qq.com/wxaapi/log/get_performance?access_token=%s"
|
||||
// getSceneListURL 获取访问来源
|
||||
getSceneListURL = "https://api.weixin.qq.com/wxaapi/log/get_scene?access_token=%s"
|
||||
// getVersionListURL 获取客户端版本
|
||||
getVersionListURL = "https://api.weixin.qq.com/wxaapi/log/get_client_version?access_token=%s"
|
||||
// realTimeLogSearchURL 查询实时日志
|
||||
realTimeLogSearchURL = "https://api.weixin.qq.com/wxaapi/userlog/userlog_search?%s"
|
||||
// getFeedbackListURL 获取用户反馈列表
|
||||
getFeedbackListURL = "https://api.weixin.qq.com/wxaapi/feedback/list?%s"
|
||||
// getJsErrDetailURL 查询js错误详情
|
||||
getJsErrDetailURL = "https://api.weixin.qq.com/wxaapi/log/jserr_detail?access_token=%s"
|
||||
// getJsErrListURL 查询错误列表
|
||||
getJsErrListURL = "https://api.weixin.qq.com/wxaapi/log/jserr_list?access_token=%s"
|
||||
// getGrayReleasePlanURL 获取分阶段发布详情
|
||||
getGrayReleasePlanURL = "https://api.weixin.qq.com/wxa/getgrayreleaseplan?access_token=%s"
|
||||
)
|
||||
|
||||
// Operation 运维中心
|
||||
type Operation struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewOperation 实例化
|
||||
func NewOperation(ctx *context.Context) *Operation {
|
||||
return &Operation{ctx}
|
||||
}
|
||||
|
||||
// GetDomainInfoRequest 查询域名配置请求
|
||||
type GetDomainInfoRequest struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// GetDomainInfoResponse 查询域名配置响应
|
||||
type GetDomainInfoResponse struct {
|
||||
util.CommonError
|
||||
RequestDomain []string `json:"requestdomain"`
|
||||
WsRequestDomain []string `json:"wsrequestdomain"`
|
||||
UploadDomain []string `json:"uploaddomain"`
|
||||
DownloadDomain []string `json:"downloaddomain"`
|
||||
UDPDomain []string `json:"udpdomain"`
|
||||
BizDomain []string `json:"bizdomain"`
|
||||
}
|
||||
|
||||
// GetDomainInfo 查询域名配置
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getDomainInfo.html
|
||||
func (o *Operation) GetDomainInfo(req *GetDomainInfoRequest) (res GetDomainInfoResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getDomainInfoURL, accessToken), req); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetDomainInfo")
|
||||
return
|
||||
}
|
||||
|
||||
// GetPerformanceRequest 获取性能数据请求
|
||||
type GetPerformanceRequest struct {
|
||||
CostTimeType int64 `json:"cost_time_type"`
|
||||
DefaultStartTime int64 `json:"default_start_time"`
|
||||
DefaultEndTime int64 `json:"default_end_time"`
|
||||
Device string `json:"device"`
|
||||
IsDownloadCode string `json:"is_download_code"`
|
||||
Scene string `json:"scene"`
|
||||
NetworkType string `json:"networktype"`
|
||||
}
|
||||
|
||||
// GetPerformanceResponse 获取性能数据响应
|
||||
type GetPerformanceResponse struct {
|
||||
util.CommonError
|
||||
DefaultTimeData string `json:"default_time_data"`
|
||||
CompareTimeData string `json:"compare_time_data"`
|
||||
}
|
||||
|
||||
// PerformanceDefaultTimeData 查询数据
|
||||
type PerformanceDefaultTimeData struct {
|
||||
List []DefaultTimeDataItem `json:"list"`
|
||||
}
|
||||
|
||||
// DefaultTimeDataItem 查询数据
|
||||
type DefaultTimeDataItem struct {
|
||||
RefData string `json:"ref_data"`
|
||||
CostTimeType int64 `json:"cost_time_type"`
|
||||
CostTime int64 `json:"cost_time"`
|
||||
}
|
||||
|
||||
// GetPerformance 获取性能数据
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getPerformance.html
|
||||
func (o *Operation) GetPerformance(req *GetPerformanceRequest) (res GetPerformanceResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getPerformanceURL, accessToken), req); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetPerformance")
|
||||
return
|
||||
}
|
||||
|
||||
// GetSceneListResponse 获取访问来源响应
|
||||
type GetSceneListResponse struct {
|
||||
util.CommonError
|
||||
Scene []Scene `json:"scene"`
|
||||
}
|
||||
|
||||
// Scene 访问来源
|
||||
type Scene struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// GetSceneList 获取访问来源
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getSceneList.html
|
||||
func (o *Operation) GetSceneList() (res GetSceneListResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getSceneListURL, accessToken)); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetSceneList")
|
||||
return
|
||||
}
|
||||
|
||||
// GetVersionListResponse 获取客户端版本响应
|
||||
type GetVersionListResponse struct {
|
||||
util.CommonError
|
||||
CvList []ClientVersion `json:"cvlist"`
|
||||
}
|
||||
|
||||
// ClientVersion 客户端版本
|
||||
type ClientVersion struct {
|
||||
Type int64 `json:"type"`
|
||||
ClientVersionList []string `json:"client_version_list"`
|
||||
}
|
||||
|
||||
// GetVersionList 获取客户端版本
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getVersionList.html
|
||||
func (o *Operation) GetVersionList() (res GetVersionListResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getVersionListURL, accessToken)); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetVersionList")
|
||||
return
|
||||
}
|
||||
|
||||
// RealTimeLogSearchRequest 查询实时日志请求
|
||||
type RealTimeLogSearchRequest struct {
|
||||
Date string
|
||||
BeginTime int64
|
||||
EndTime int64
|
||||
Start int64
|
||||
Limit int64
|
||||
Level int64
|
||||
TraceID string
|
||||
URL string
|
||||
ID string
|
||||
FilterMsg string
|
||||
}
|
||||
|
||||
// RealTimeLogSearchResponse 查询实时日志响应
|
||||
type RealTimeLogSearchResponse struct {
|
||||
util.CommonError
|
||||
Data RealTimeLogSearchData `json:"data"`
|
||||
}
|
||||
|
||||
// RealTimeLogSearchData 日志数据和日志条数总量
|
||||
type RealTimeLogSearchData struct {
|
||||
List []RealTimeLogSearchDataList `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// RealTimeLogSearchDataList 日志数据列表
|
||||
type RealTimeLogSearchDataList struct {
|
||||
Level int64 `json:"level"`
|
||||
LibraryVersion string `json:"libraryVersion"`
|
||||
ClientVersion string `json:"clientVersion"`
|
||||
ID string `json:"id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Platform int64 `json:"platform"`
|
||||
URL string `json:"url"`
|
||||
TraceID string `json:"traceid"`
|
||||
FilterMsg string `json:"filterMsg"`
|
||||
Msg []RealTimeLogSearchDataListMsg `json:"msg"`
|
||||
}
|
||||
|
||||
// RealTimeLogSearchDataListMsg 日志内容数组
|
||||
type RealTimeLogSearchDataListMsg struct {
|
||||
Time int64 `json:"time"`
|
||||
Level int64 `json:"level"`
|
||||
Msg []string `json:"msg"`
|
||||
}
|
||||
|
||||
// RealTimeLogSearch 查询实时日志
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/realtimelogSearch.html
|
||||
func (o *Operation) RealTimeLogSearch(req *RealTimeLogSearchRequest) (res RealTimeLogSearchResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"date": req.Date,
|
||||
"begintime": req.BeginTime,
|
||||
"endtime": req.EndTime,
|
||||
}
|
||||
if req.Start > 0 {
|
||||
params["start"] = req.Start
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
params["limit"] = req.Limit
|
||||
}
|
||||
if req.TraceID != "" {
|
||||
params["traceId"] = req.TraceID
|
||||
}
|
||||
if req.URL != "" {
|
||||
params["url"] = req.URL
|
||||
}
|
||||
if req.ID != "" {
|
||||
params["id"] = req.ID
|
||||
}
|
||||
if req.FilterMsg != "" {
|
||||
params["filterMsg"] = req.FilterMsg
|
||||
}
|
||||
if req.Level > 0 {
|
||||
params["level"] = req.Level
|
||||
}
|
||||
query := util.Query(params)
|
||||
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(realTimeLogSearchURL, query)); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "RealTimeLogSearch")
|
||||
return
|
||||
}
|
||||
|
||||
// GetFeedbackListRequest 获取用户反馈列表请求
|
||||
type GetFeedbackListRequest struct {
|
||||
Page int64
|
||||
Num int64
|
||||
Type int64
|
||||
}
|
||||
|
||||
// GetFeedbackListResponse 获取用户反馈列表响应
|
||||
type GetFeedbackListResponse struct {
|
||||
util.CommonError
|
||||
TotalNum int64 `json:"total_num"`
|
||||
List []Feedback `json:"list"`
|
||||
}
|
||||
|
||||
// Feedback 反馈列表
|
||||
type Feedback struct {
|
||||
RecordID int64 `json:"record_id"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
Content string `json:"content"`
|
||||
Phone string `json:"phone"`
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
HeadURL string `json:"head_url"`
|
||||
Type int64 `json:"type"`
|
||||
MediaIDS []string `json:"mediaIds"`
|
||||
SystemInfo string `json:"systemInfo"`
|
||||
}
|
||||
|
||||
// GetFeedbackList 获取用户反馈列表
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getFeedback.html
|
||||
func (o *Operation) GetFeedbackList(req *GetFeedbackListRequest) (res GetFeedbackListResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"page": req.Page,
|
||||
"num": req.Num,
|
||||
}
|
||||
if req.Type > 0 {
|
||||
params["type"] = req.Type
|
||||
}
|
||||
query := util.Query(params)
|
||||
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getFeedbackListURL, query)); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetFeedbackList")
|
||||
return
|
||||
}
|
||||
|
||||
// GetJsErrDetailRequest 查询js错误详情请求
|
||||
type GetJsErrDetailRequest struct {
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
ErrorMsgMd5 string `json:"errorMsgMd5"`
|
||||
ErrorStackMd5 string `json:"errorStackMd5"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
SdkVersion string `json:"sdkVersion"`
|
||||
OsName string `json:"osName"`
|
||||
ClientVersion string `json:"clientVersion"`
|
||||
OpenID string `json:"openid"`
|
||||
Offset int64 `json:"offset"`
|
||||
Limit int64 `json:"limit"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// GetJsErrDetailResponse 查询js错误详情响应
|
||||
type GetJsErrDetailResponse struct {
|
||||
util.CommonError
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
OpenID string `json:"openid"`
|
||||
Data []JsErrDetailData `json:"data"`
|
||||
}
|
||||
|
||||
// JsErrDetailData 错误列表
|
||||
type JsErrDetailData struct {
|
||||
Count string `json:"Count"`
|
||||
SdkVersion string `json:"sdkVersion"`
|
||||
ClientVersion string `json:"ClientVersion"`
|
||||
ErrorStackMd5 string `json:"errorStackMd5"`
|
||||
TimeStamp string `json:"TimeStamp"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
ErrorMsgMd5 string `json:"errorMsgMd5"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
ErrorStack string `json:"errorStack"`
|
||||
Ds string `json:"Ds"`
|
||||
OsName string `json:"OsName"`
|
||||
OpenID string `json:"openId"`
|
||||
PluginVersion string `json:"pluginversion"`
|
||||
AppID string `json:"appId"`
|
||||
DeviceModel string `json:"DeviceModel"`
|
||||
Source string `json:"source"`
|
||||
Route string `json:"route"`
|
||||
Uin string `json:"Uin"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
// GetJsErrDetail 查询js错误详情
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getJsErrDetail.html
|
||||
func (o *Operation) GetJsErrDetail(req *GetJsErrDetailRequest) (res GetJsErrDetailResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getJsErrDetailURL, accessToken), req); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetJsErrDetail")
|
||||
return
|
||||
}
|
||||
|
||||
// GetJsErrListRequest 查询错误列表请求
|
||||
type GetJsErrListRequest struct {
|
||||
AppVersion string `json:"appVersion"`
|
||||
ErrType string `json:"errType"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
Keyword string `json:"keyword"`
|
||||
OpenID string `json:"openid"`
|
||||
OrderBy string `json:"orderby"`
|
||||
Desc string `json:"desc"`
|
||||
Offset int64 `json:"offset"`
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
// GetJsErrListResponse 查询错误列表响应
|
||||
type GetJsErrListResponse struct {
|
||||
util.CommonError
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
OpenID string `json:"openid"`
|
||||
Data []JsErrListData `json:"data"`
|
||||
}
|
||||
|
||||
// JsErrListData 错误列表
|
||||
type JsErrListData struct {
|
||||
ErrorMsgMd5 string `json:"errorMsgMd5"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
Uv int64 `json:"uv"`
|
||||
Pv int64 `json:"pv"`
|
||||
ErrorStackMd5 string `json:"errorStackMd5"`
|
||||
ErrorStack string `json:"errorStack"`
|
||||
PvPercent string `json:"pvPercent"`
|
||||
UvPercent string `json:"uvPercent"`
|
||||
}
|
||||
|
||||
// GetJsErrList 查询错误列表
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getJsErrList.html
|
||||
func (o *Operation) GetJsErrList(req *GetJsErrListRequest) (res GetJsErrListResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getJsErrListURL, accessToken), req); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetJsErrList")
|
||||
return
|
||||
}
|
||||
|
||||
// GetGrayReleasePlanResponse 获取分阶段发布详情响应
|
||||
type GetGrayReleasePlanResponse struct {
|
||||
util.CommonError
|
||||
GrayReleasePlan GrayReleasePlanDetail `json:"gray_release_plan"`
|
||||
}
|
||||
|
||||
// GrayReleasePlanDetail 分阶段发布计划详情
|
||||
type GrayReleasePlanDetail struct {
|
||||
Status int64 `json:"status"`
|
||||
CreateTimestamp int64 `json:"create_timestamp"`
|
||||
GrayPercentage int64 `json:"gray_percentage"`
|
||||
SupportExperiencerFirst bool `json:"support_experiencer_first"`
|
||||
SupportDebugerFirst bool `json:"support_debuger_first"`
|
||||
}
|
||||
|
||||
// GetGrayReleasePlan 获取分阶段发布详情
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/operation/getGrayReleasePlan.html
|
||||
func (o *Operation) GetGrayReleasePlan() (res GetGrayReleasePlanResponse, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = o.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getGrayReleasePlanURL, accessToken)); err != nil {
|
||||
return
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetGrayReleasePlan")
|
||||
return
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
context2 "context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
@@ -64,7 +65,12 @@ type MediaCheckAsyncRequest struct {
|
||||
|
||||
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
|
||||
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
return security.MediaCheckAsyncContext(context2.Background(), in)
|
||||
}
|
||||
|
||||
// MediaCheckAsyncContext 异步校验图片/音频是否含有违法违规内容
|
||||
func (security *Security) MediaCheckAsyncContext(ctx context2.Context, in *MediaCheckAsyncRequest) (traceID string, err error) {
|
||||
accessToken, err := security.GetAccessTokenContext(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -77,7 +83,7 @@ func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID s
|
||||
req.Version = 2
|
||||
|
||||
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -222,7 +228,12 @@ func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err
|
||||
|
||||
// MsgCheck 检查一段文本是否含有违法违规内容
|
||||
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
return security.MsgCheckContext(context2.Background(), in)
|
||||
}
|
||||
|
||||
// MsgCheckContext 检查一段文本是否含有违法违规内容
|
||||
func (security *Security) MsgCheckContext(ctx context2.Context, in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
||||
accessToken, err := security.GetAccessTokenContext(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -235,7 +246,7 @@ func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, e
|
||||
req.Version = 2
|
||||
|
||||
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
response, err := util.PostJSONContext(ctx, uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,22 +12,30 @@ const (
|
||||
// 发送订阅消息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
||||
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
|
||||
|
||||
// 获取当前帐号下的个人模板列表
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
||||
getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
||||
|
||||
// 添加订阅模板
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html
|
||||
addTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate"
|
||||
|
||||
// 删除私有模板
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html
|
||||
delTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate"
|
||||
|
||||
// 统一服务消息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
|
||||
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
|
||||
// getCategoryURL 获取类目
|
||||
getCategoryURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getcategory?access_token=%s"
|
||||
// getPubTemplateKeyWordsByIDURL 获取关键词列表
|
||||
getPubTemplateKeyWordsByIDURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords?access_token=%s&tid=%s"
|
||||
// getPubTemplateTitleListURL 获取所属类目下的公共模板
|
||||
getPubTemplateTitleListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles?access_token=%s&ids=%s&start=%d&limit=%d"
|
||||
// setUserNotifyURL 激活与更新服务卡片
|
||||
setUserNotifyURL = "https://api.weixin.qq.com/wxa/set_user_notify?access_token=%s"
|
||||
// setUserNotifyExtURL 更新服务卡片扩展信息
|
||||
setUserNotifyExtURL = "https://api.weixin.qq.com/wxa/set_user_notifyext?access_token=%s"
|
||||
// getUserNotifyURL 查询服务卡片状态
|
||||
getUserNotifyURL = "https://api.weixin.qq.com/wxa/get_user_notify?access_token=%s"
|
||||
)
|
||||
|
||||
// Subscribe 订阅消息
|
||||
@@ -58,11 +66,18 @@ type DataItem struct {
|
||||
|
||||
// TemplateItem template item
|
||||
type TemplateItem struct {
|
||||
PriTmplID string `json:"priTmplId"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Example string `json:"example"`
|
||||
Type int64 `json:"type"`
|
||||
PriTmplID string `json:"priTmplId"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Example string `json:"example"`
|
||||
Type int64 `json:"type"`
|
||||
KeywordEnumValueList []KeywordEnumValue `json:"keywordEnumValueList"`
|
||||
}
|
||||
|
||||
// KeywordEnumValue 枚举参数值范围
|
||||
type KeywordEnumValue struct {
|
||||
EnumValueList []string `json:"enumValueList"`
|
||||
KeywordCode string `json:"keywordCode"`
|
||||
}
|
||||
|
||||
// TemplateList template list
|
||||
@@ -224,3 +239,200 @@ func (s *Subscribe) Delete(templateID string) (err error) {
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
||||
}
|
||||
|
||||
// GetCategoryResponse 获取类目响应
|
||||
type GetCategoryResponse struct {
|
||||
util.CommonError
|
||||
Data []Category `json:"data"`
|
||||
}
|
||||
|
||||
// Category 类目
|
||||
type Category struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// GetCategory 获取类目
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getCategory.html
|
||||
func (s *Subscribe) GetCategory() ([]Category, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getCategoryURL, accessToken)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetCategoryResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetCategory")
|
||||
return result.Data, err
|
||||
}
|
||||
|
||||
// GetPubTemplateKeywordsByIDResponse 获取关键词列表响应
|
||||
type GetPubTemplateKeywordsByIDResponse struct {
|
||||
util.CommonError
|
||||
Count int64 `json:"count"`
|
||||
Data []PubTemplateKeywords `json:"data"`
|
||||
}
|
||||
|
||||
// PubTemplateKeywords 关键词
|
||||
type PubTemplateKeywords struct {
|
||||
KID int64 `json:"kid"`
|
||||
Name string `json:"name"`
|
||||
Example string `json:"example"`
|
||||
Rule string `json:"rule"`
|
||||
}
|
||||
|
||||
// GetPubTemplateKeywordsByID 获取关键词列表
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getPubTemplateKeyWordsById.html
|
||||
func (s *Subscribe) GetPubTemplateKeywordsByID(tid string) (*GetPubTemplateKeywordsByIDResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getPubTemplateKeyWordsByIDURL, accessToken, tid)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetPubTemplateKeywordsByIDResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetPubTemplateKeywordsByID")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetPubTemplateTitleListRequest 获取所属类目下的公共模板请求
|
||||
type GetPubTemplateTitleListRequest struct {
|
||||
Start int64
|
||||
Limit int64
|
||||
IDs string
|
||||
}
|
||||
|
||||
// GetPubTemplateTitleListResponse 获取所属类目下的公共模板响应
|
||||
type GetPubTemplateTitleListResponse struct {
|
||||
util.CommonError
|
||||
Count int64 `json:"count"`
|
||||
Data []PubTemplateTitle `json:"data"`
|
||||
}
|
||||
|
||||
// PubTemplateTitle 模板标题
|
||||
type PubTemplateTitle struct {
|
||||
Type int64 `json:"type"`
|
||||
TID string `json:"tid"`
|
||||
Title string `json:"title"`
|
||||
CategoryID string `json:"categoryId"`
|
||||
}
|
||||
|
||||
// GetPubTemplateTitleList 获取所属类目下的公共模板
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getPubTemplateTitleList.html
|
||||
func (s *Subscribe) GetPubTemplateTitleList(req *GetPubTemplateTitleListRequest) (*GetPubTemplateTitleListResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(getPubTemplateTitleListURL, accessToken, req.IDs, req.Start, req.Limit)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetPubTemplateTitleListResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetPubTemplateTitleList")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// SetUserNotifyRequest 激活与更新服务卡片请求
|
||||
type SetUserNotifyRequest struct {
|
||||
OpenID string `json:"openid"`
|
||||
NotifyType int64 `json:"notify_type"`
|
||||
NotifyCode string `json:"notify_code"`
|
||||
ContentJSON string `json:"content_json"`
|
||||
CheckJSON string `json:"check_json,omitempty"`
|
||||
}
|
||||
|
||||
// SetUserNotify 激活与更新服务卡片
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/setUserNotify.html
|
||||
func (s *Subscribe) SetUserNotify(req *SetUserNotifyRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(setUserNotifyURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "SetUserNotify")
|
||||
}
|
||||
|
||||
// SetUserNotifyExtRequest 更新服务卡片扩展信息请求
|
||||
type SetUserNotifyExtRequest struct {
|
||||
OpenID string `json:"openid"`
|
||||
NotifyType int64 `json:"notify_type"`
|
||||
NotifyCode string `json:"notify_code"`
|
||||
ExtJSON string `json:"ext_json"`
|
||||
}
|
||||
|
||||
// SetUserNotifyExt 更新服务卡片扩展信息
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/setUserNotifyExt.html
|
||||
func (s *Subscribe) SetUserNotifyExt(req *SetUserNotifyExtRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(setUserNotifyExtURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "SetUserNotifyExt")
|
||||
}
|
||||
|
||||
// GetUserNotifyRequest 查询服务卡片状态请求
|
||||
type GetUserNotifyRequest struct {
|
||||
OpenID string `json:"openid"`
|
||||
NotifyType int64 `json:"notify_type"`
|
||||
NotifyCode string `json:"notify_code"`
|
||||
}
|
||||
|
||||
// GetUserNotifyResponse 查询服务卡片状态响应
|
||||
type GetUserNotifyResponse struct {
|
||||
util.CommonError
|
||||
NotifyInfo NotifyInfo `json:"notify_info"`
|
||||
}
|
||||
|
||||
// NotifyInfo 卡片状态
|
||||
type NotifyInfo struct {
|
||||
NotifyType int64 `json:"notify_type"`
|
||||
ContentJSON string `json:"content_json"`
|
||||
CodeState int64 `json:"code_state"`
|
||||
CodeExpireTime int64 `json:"code_expire_time"`
|
||||
}
|
||||
|
||||
// GetUserNotify 查询服务卡片状态
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getUserNotify.html
|
||||
func (s *Subscribe) GetUserNotify(req *GetUserNotifyRequest) (*GetUserNotifyResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getUserNotifyURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetUserNotifyResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetUserNotify")
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,13 @@ import (
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink"
|
||||
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink?access_token=%s"
|
||||
|
||||
// ULQueryRequest 查询加密URLLink请求
|
||||
type ULQueryRequest struct {
|
||||
URLLink string `json:"url_link"`
|
||||
QueryType int `json:"query_type"`
|
||||
}
|
||||
|
||||
// ULQueryResult 返回的结果
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
|
||||
@@ -28,25 +34,35 @@ type ULQueryResult struct {
|
||||
ResourceAppid string `json:"resource_appid"`
|
||||
} `json:"cloud_base"`
|
||||
} `json:"url_link_info"`
|
||||
VisitOpenid string `json:"visit_openid"`
|
||||
VisitOpenid string `json:"visit_openid"`
|
||||
QuotaInfo QuotaInfo `json:"quota_info"`
|
||||
}
|
||||
|
||||
// QuotaInfo quota 配置
|
||||
type QuotaInfo struct {
|
||||
RemainVisitQuota int64 `json:"remain_visit_quota"`
|
||||
}
|
||||
|
||||
// Query 查询小程序 url_link 配置。
|
||||
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
|
||||
accessToken, err := u.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken)
|
||||
response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp ULQueryResult
|
||||
err = util.DecodeWithError(response, &resp, "URLLink.Query")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
return u.QueryWithType(&ULQueryRequest{URLLink: urlLink})
|
||||
}
|
||||
|
||||
// QueryWithType 查询加密URLLink
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/queryUrlLink.html
|
||||
func (u *URLLink) QueryWithType(req *ULQueryRequest) (*ULQueryResult, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = u.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(queryURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ULQueryResult{}
|
||||
err = util.DecodeWithError(response, result, "URLLink.Query")
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ const (
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||
type QueryScheme struct {
|
||||
// 小程序 scheme 码
|
||||
Scheme string `json:"scheme"`
|
||||
Scheme string `json:"scheme"`
|
||||
QueryType int `json:"query_type"`
|
||||
}
|
||||
|
||||
// SchemeInfo scheme 配置
|
||||
@@ -33,34 +34,47 @@ type SchemeInfo struct {
|
||||
EnvVersion EnvVersion `json:"env_version"`
|
||||
}
|
||||
|
||||
// resQueryScheme 返回结构体
|
||||
// QuotaInfo quota 配置
|
||||
type QuotaInfo struct {
|
||||
RemainVisitQuota int64 `json:"remain_visit_quota"`
|
||||
}
|
||||
|
||||
// ResQueryScheme 返回结构体
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||
type resQueryScheme struct {
|
||||
type ResQueryScheme struct {
|
||||
// 通用错误
|
||||
util.CommonError
|
||||
// scheme 配置
|
||||
SchemeInfo SchemeInfo `json:"scheme_info"`
|
||||
// 访问该链接的openid,没有用户访问过则为空字符串
|
||||
VisitOpenid string `json:"visit_openid"`
|
||||
VisitOpenid string `json:"visit_openid"`
|
||||
QuotaInfo QuotaInfo `json:"quota_info"`
|
||||
}
|
||||
|
||||
// QueryScheme 查询小程序 scheme 码
|
||||
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = u.GetAccessToken()
|
||||
res, err := u.QuerySchemeWithRes(querySchemeParams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urlStr := fmt.Sprintf(querySchemeURL, accessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(urlStr, querySchemeParams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
var res resQueryScheme
|
||||
err = util.DecodeWithError(response, &res, "QueryScheme")
|
||||
return res.SchemeInfo, res.VisitOpenid, err
|
||||
}
|
||||
|
||||
// QuerySchemeWithRes 查询scheme码
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/queryScheme.html
|
||||
func (u *URLScheme) QuerySchemeWithRes(req QueryScheme) (*ResQueryScheme, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = u.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(querySchemeURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ResQueryScheme{}
|
||||
err = util.DecodeWithError(response, result, "QueryScheme")
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -17,7 +17,12 @@ func NewURLScheme(ctx *context.Context) *URLScheme {
|
||||
return &URLScheme{Context: ctx}
|
||||
}
|
||||
|
||||
const generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
|
||||
const (
|
||||
// generateURL 获取加密scheme码
|
||||
generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
|
||||
// generateNFCURL 获取 NFC 的小程序 scheme
|
||||
generateNFCURL = "https://api.weixin.qq.com/wxa/generatenfcscheme?access_token=%s"
|
||||
)
|
||||
|
||||
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
||||
type TExpireType int
|
||||
@@ -50,11 +55,13 @@ type JumpWxa struct {
|
||||
// USParams 请求参数
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
|
||||
type USParams struct {
|
||||
JumpWxa *JumpWxa `json:"jump_wxa"`
|
||||
ExpireType TExpireType `json:"expire_type"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ExpireInterval int `json:"expire_interval"`
|
||||
JumpWxa *JumpWxa `json:"jump_wxa,omitempty"`
|
||||
ExpireType TExpireType `json:"expire_type,omitempty"`
|
||||
ExpireTime int64 `json:"expire_time,omitempty"`
|
||||
ExpireInterval int `json:"expire_interval,omitempty"`
|
||||
IsExpire bool `json:"is_expire,omitempty"`
|
||||
ModelID string `json:"model_id,omitempty"`
|
||||
Sn string `json:"sn,omitempty"`
|
||||
}
|
||||
|
||||
// USResult 返回的结果
|
||||
@@ -81,3 +88,22 @@ func (u *URLScheme) Generate(params *USParams) (string, error) {
|
||||
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
||||
return resp.OpenLink, err
|
||||
}
|
||||
|
||||
// GenerateNFC 获取 NFC 的小程序 scheme
|
||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/generateNFCScheme.html
|
||||
func (u *URLScheme) GenerateNFC(params *USParams) (string, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = u.GetAccessToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(generateNFCURL, accessToken), params); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := &USResult{}
|
||||
err = util.DecodeWithError(response, result, "URLScheme.GenerateNFC")
|
||||
return result.OpenLink, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
context2 "context"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
@@ -39,14 +40,31 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
||||
// GetConfig 获取jssdk需要的配置参数
|
||||
// uri 为当前网页地址
|
||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||
config = new(Config)
|
||||
return js.GetConfigContext(context2.Background(), uri)
|
||||
}
|
||||
|
||||
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
||||
func (js *Js) GetConfigContext(ctx context2.Context, uri string) (config *Config, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = js.GetAccessToken()
|
||||
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
||||
if ctxHandle, ok := js.Context.AccessTokenHandle.(credential.AccessTokenContextHandle); ok {
|
||||
accessToken, err = ctxHandle.GetAccessTokenContext(ctx)
|
||||
} else {
|
||||
// 如果没有实现 AccessTokenContextHandle 接口,调用旧的 GetAccessToken 方法
|
||||
accessToken, err = js.Context.GetAccessToken()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var ticketStr string
|
||||
ticketStr, err = js.GetTicket(accessToken)
|
||||
// 类型断言 jsTicket
|
||||
if ticketCtxHandle, ok := js.JsTicketHandle.(credential.JsTicketContextHandle); ok {
|
||||
ticketStr, err = ticketCtxHandle.GetTicketContext(ctx, accessToken)
|
||||
} else {
|
||||
// 如果没有实现 JsTicketContextHandle 接口,调用旧的 GetTicket 方法
|
||||
ticketStr, err = js.GetTicket(accessToken)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -56,6 +74,7 @@ func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||
sigStr := util.Signature(str)
|
||||
|
||||
config = new(Config)
|
||||
config.AppID = js.AppID
|
||||
config.NonceStr = nonceStr
|
||||
config.Timestamp = timestamp
|
||||
|
||||
@@ -2,7 +2,7 @@ package account
|
||||
|
||||
import "github.com/silenceper/wechat/v2/openplatform/context"
|
||||
|
||||
// Account 开放平台张哈管理
|
||||
// Account 开放平台帐号管理
|
||||
// TODO 实现方法
|
||||
type Account struct {
|
||||
*context.Context
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
||||
componentLoginURL = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=%d&biz_appid=%s"
|
||||
bindComponentURL = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
|
||||
bindComponentURLV2 = "https://open.weixin.qq.com/wxaopen/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
|
||||
// TODO 获取授权方选项信息
|
||||
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||
// TODO 获取已授权的账号信息
|
||||
@@ -137,6 +138,20 @@ func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizApp
|
||||
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
|
||||
}
|
||||
|
||||
// GetBindComponentURLV2Context 获取新版本第三方公众号授权链接(链接跳转,适用移动端)
|
||||
func (ctx *Context) GetBindComponentURLV2Context(stdCtx context.Context, redirectURI string, authType int, bizAppID string) (string, error) {
|
||||
code, err := ctx.GetPreCodeContext(stdCtx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf(bindComponentURLV2, authType, ctx.AppID, code, url.QueryEscape(redirectURI), bizAppID), nil
|
||||
}
|
||||
|
||||
// GetBindComponentURLV2 获取新版本第三方公众号授权链接(链接跳转,适用移动端)
|
||||
func (ctx *Context) GetBindComponentURLV2(redirectURI string, authType int, bizAppID string) (string, error) {
|
||||
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
|
||||
}
|
||||
|
||||
// ID 微信返回接口中各种类型字段
|
||||
type ID struct {
|
||||
ID int `json:"id"`
|
||||
@@ -225,6 +240,10 @@ func (ctx *Context) RefreshAuthrTokenContext(stdCtx context.Context, appid, refr
|
||||
if err := cache.SetContext(stdCtx, ctx.Cache, authrTokenKey, ret.AccessToken, time.Second*time.Duration(ret.ExpiresIn-30)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshTokenKey := "authorizer_refresh_token_" + appid
|
||||
if err := cache.SetContext(stdCtx, ctx.Cache, refreshTokenKey, ret.RefreshToken, 10*365*24*60*60*time.Second); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -238,8 +257,18 @@ func (ctx *Context) GetAuthrAccessTokenContext(stdCtx context.Context, appid str
|
||||
authrTokenKey := "authorizer_access_token_" + appid
|
||||
val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey)
|
||||
if val == nil {
|
||||
return "", fmt.Errorf("cannot get authorizer %s access token", appid)
|
||||
refreshTokenKey := "authorizer_refresh_token_" + appid
|
||||
val := cache.GetContext(stdCtx, ctx.Cache, refreshTokenKey)
|
||||
if val == nil {
|
||||
return "", fmt.Errorf("cannot get authorizer %s refresh token", appid)
|
||||
}
|
||||
token, err := ctx.RefreshAuthrTokenContext(stdCtx, appid, val.(string))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
context2 "context"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
@@ -32,14 +33,31 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
||||
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
||||
// uri 为当前网页地址
|
||||
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
||||
config = new(officialJs.Config)
|
||||
return js.GetConfigContext(context2.Background(), uri, appid)
|
||||
}
|
||||
|
||||
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
||||
func (js *Js) GetConfigContext(ctx context2.Context, uri, appid string) (config *officialJs.Config, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = js.GetAccessToken()
|
||||
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
||||
if ctxHandle, ok := js.Context.AccessTokenHandle.(credential.AccessTokenContextHandle); ok {
|
||||
accessToken, err = ctxHandle.GetAccessTokenContext(ctx)
|
||||
} else {
|
||||
// 如果没有实现 AccessTokenContextHandle 接口,调用旧的 GetAccessToken 方法
|
||||
accessToken, err = js.Context.GetAccessToken()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var ticketStr string
|
||||
ticketStr, err = js.GetTicket(accessToken)
|
||||
// 类型断言 jsTicket
|
||||
if ticketCtxHandle, ok := js.JsTicketHandle.(credential.JsTicketContextHandle); ok {
|
||||
ticketStr, err = ticketCtxHandle.GetTicketContext(ctx, accessToken)
|
||||
} else {
|
||||
// 如果没有实现 JsTicketContextHandle 接口,调用旧的 GetTicket 方法
|
||||
ticketStr, err = js.GetTicket(accessToken)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -49,6 +67,7 @@ func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||
sigStr := util.Signature(str)
|
||||
|
||||
config = new(officialJs.Config)
|
||||
config.AppID = appid
|
||||
config.NonceStr = nonceStr
|
||||
config.Timestamp = timestamp
|
||||
|
||||
147
openplatform/officialaccount/js/js_test.go
Normal file
147
openplatform/officialaccount/js/js_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// 验证 js.GetConfigContext 是否能正确传递上下文到 HTTP 请求,确保上下文正确传播,防止在获取 JSSDK 配置时发生协程泄露。
|
||||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
context2 "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/config"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// mockAccessTokenHandle 模拟 AccessTokenHandle
|
||||
type mockAccessTokenHandle struct{}
|
||||
|
||||
func (m *mockAccessTokenHandle) GetAccessToken() (string, error) {
|
||||
return "mock-access-token", nil
|
||||
}
|
||||
|
||||
func (m *mockAccessTokenHandle) GetAccessTokenContext(_ context2.Context) (string, error) {
|
||||
return "mock-access-token", nil
|
||||
}
|
||||
|
||||
// contextCheckingRoundTripper 自定义 RoundTripper 用于检查 context
|
||||
type contextCheckingRoundTripper struct {
|
||||
originalCtx context2.Context
|
||||
t *testing.T
|
||||
key interface{}
|
||||
expectedVal interface{}
|
||||
}
|
||||
|
||||
func (rt *contextCheckingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// 获取请求中的 context
|
||||
reqCtx := req.Context()
|
||||
|
||||
// 打印 context 比较结果
|
||||
rt.t.Logf("比较上下文的内存地址:\n")
|
||||
if reqCtx == rt.originalCtx {
|
||||
rt.t.Logf("上下文具有相同的内存地址。原始上下文: %p, 请求上下文: %p\n", rt.originalCtx, reqCtx)
|
||||
} else {
|
||||
rt.t.Logf("上下文具有不同的内存地址。原始上下文: %p, 请求上下文: %p\n", rt.originalCtx, reqCtx)
|
||||
}
|
||||
|
||||
// 检查 context 中的键值对
|
||||
if rt.key != nil {
|
||||
value := reqCtx.Value(rt.key)
|
||||
rt.t.Logf("检查请求上下文中的键 %v:\n", rt.key)
|
||||
if value != rt.expectedVal {
|
||||
rt.t.Errorf("上下文键 %v 的值不匹配: 预期 %v, 实际 %v\n", rt.key, rt.expectedVal, value)
|
||||
} else {
|
||||
rt.t.Logf("上下文键 %v 的值匹配: 预期 %v, 实际 %v\n", rt.key, rt.expectedVal, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-reqCtx.Done():
|
||||
return nil, reqCtx.Err() // 返回上下文取消错误
|
||||
default:
|
||||
// 返回模拟的 HTTP 响应,包含有效的 JSON
|
||||
responseBody := `{"ticket":"mock-ticket","expires_in":7200}`
|
||||
response := &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: http.StatusOK,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(responseBody))),
|
||||
ContentLength: int64(len(responseBody)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
response.Header.Set("Content-Type", "application/json")
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
// contextKey 定义自定义上下文键类型,避免使用内置 string 类型
|
||||
type contextKey string
|
||||
|
||||
// setupJsInstance 初始化 Js 实例和 HTTP 客户端
|
||||
func setupJsInstance(t *testing.T, ctx context2.Context, key, val interface{}) (*Js, func()) {
|
||||
cfg := &config.Config{
|
||||
AppID: "test-app-id",
|
||||
AppSecret: "test-app-secret",
|
||||
Cache: cache.NewMemory(),
|
||||
}
|
||||
cacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", credential.CacheKeyOfficialAccountPrefix, cfg.AppID)
|
||||
if err := cfg.Cache.Delete(cacheKey); err != nil {
|
||||
t.Fatalf("清除缓存失败: %v", err)
|
||||
}
|
||||
t.Log("清除 jsapi_ticket 的缓存:", cacheKey)
|
||||
|
||||
ctxHandle := &context.Context{Config: cfg, AccessTokenHandle: &mockAccessTokenHandle{}}
|
||||
jsInstance := NewJs(ctxHandle, cfg.AppID)
|
||||
jsInstance.SetJsTicketHandle(credential.NewDefaultJsTicket(cfg.AppID, credential.CacheKeyOfficialAccountPrefix, cfg.Cache))
|
||||
|
||||
originalClient := util.DefaultHTTPClient
|
||||
util.DefaultHTTPClient = &http.Client{
|
||||
Transport: &contextCheckingRoundTripper{originalCtx: ctx, t: t, key: key, expectedVal: val},
|
||||
}
|
||||
return jsInstance, func() { util.DefaultHTTPClient = originalClient }
|
||||
}
|
||||
|
||||
// TestGetConfigContext 测试GetConfigContext的上下文传递和取消行为。
|
||||
func TestGetConfigContext(t *testing.T) {
|
||||
t.Run("ContextPassing", func(t *testing.T) {
|
||||
ctxKey := contextKey("testKey111") // 使用自定义类型 contextKey
|
||||
ctxValue := "testValue222"
|
||||
ctx := context2.WithValue(context2.Background(), ctxKey, ctxValue)
|
||||
t.Logf("创建的测试上下文: %p, 添加的键值对: %v=%v\n", ctx, ctxKey, ctxValue)
|
||||
|
||||
jsInstance, cleanup := setupJsInstance(t, ctx, ctxKey, ctxValue)
|
||||
defer cleanup()
|
||||
t.Log("调用 GetConfigContext")
|
||||
config2, err := jsInstance.GetConfigContext(ctx, "https://www.baidu.com", "test-app-id")
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfigContext 失败: %v", err)
|
||||
}
|
||||
if config2.AppID != "test-app-id" {
|
||||
t.Errorf("预期 AppID 为 %s,实际为 %s", "test-app-id", config2.AppID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ContextCancellation", func(t *testing.T) {
|
||||
ctx, cancel := context2.WithCancel(context2.Background())
|
||||
defer cancel()
|
||||
|
||||
jsInstance, cleanup := setupJsInstance(t, ctx, nil, nil)
|
||||
defer cleanup()
|
||||
|
||||
cancel()
|
||||
t.Log("调用 GetConfigContext(已取消上下文)")
|
||||
_, err := jsInstance.GetConfigContext(ctx, "https://www.baidu.com", "test-app-id")
|
||||
if err == nil {
|
||||
t.Error("预期上下文取消错误,但 GetConfigContext 未返回错误")
|
||||
} else if !errors.Is(err, context2.Canceled) {
|
||||
t.Errorf("预期错误为 context.Canceled,实际为: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -68,3 +68,18 @@ func DecodeWithError(response []byte, obj interface{}, apiName string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleFileResponse 通用处理微信等接口返回:有时 JSON 错误,有时文件内容
|
||||
func HandleFileResponse(response []byte, apiName string) ([]byte, error) {
|
||||
var commErr CommonError
|
||||
if err := json.Unmarshal(response, &commErr); err == nil {
|
||||
// 能解析成 JSON,判断是否为错误
|
||||
if commErr.ErrCode != 0 {
|
||||
commErr.apiName = apiName
|
||||
return nil, &commErr
|
||||
}
|
||||
// 能解析成 JSON 且没错误码,极少情况(比如微信返回的业务数据是 JSON 但无 errcode 字段),可根据需要调整
|
||||
}
|
||||
// 不能解析成 JSON,或没错误码,直接返回原始内容
|
||||
return response, nil
|
||||
}
|
||||
|
||||
16
util/http.go
16
util/http.go
@@ -166,6 +166,7 @@ func PostFile(fieldName, filePath, uri string) ([]byte, error) {
|
||||
IsFile: true,
|
||||
Fieldname: fieldName,
|
||||
FilePath: filePath,
|
||||
Filename: filePath,
|
||||
},
|
||||
}
|
||||
return PostMultipartForm(fields, uri)
|
||||
@@ -290,7 +291,20 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
trans := (DefaultHTTPClient.Transport.(*http.Transport)).Clone()
|
||||
|
||||
// 安全地获取 *http.Transport
|
||||
var trans *http.Transport
|
||||
// 尝试从 DefaultHTTPClient 获取 Transport,如果失败则使用默认值
|
||||
if DefaultHTTPClient.Transport != nil {
|
||||
if t, ok := DefaultHTTPClient.Transport.(*http.Transport); ok {
|
||||
trans = t.Clone()
|
||||
}
|
||||
}
|
||||
// 如果无法获取有效的 Transport,使用默认值
|
||||
if trans == nil {
|
||||
trans = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
|
||||
trans.TLSClientConfig = config
|
||||
trans.DisableCompression = true
|
||||
client = &http.Client{Transport: trans}
|
||||
|
||||
81
util/http_test.go
Normal file
81
util/http_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestHttpWithTLS_NilTransport tests the scenario where DefaultHTTPClient.Transport is nil
|
||||
func TestHttpWithTLS_NilTransport(t *testing.T) {
|
||||
// Save original transport
|
||||
originalTransport := DefaultHTTPClient.Transport
|
||||
defer func() {
|
||||
DefaultHTTPClient.Transport = originalTransport
|
||||
}()
|
||||
|
||||
// Set Transport to nil to simulate the bug scenario
|
||||
DefaultHTTPClient.Transport = nil
|
||||
|
||||
// This should not panic after the fix
|
||||
// Note: This will fail due to invalid cert path, but shouldn't panic on type assertion
|
||||
_, err := httpWithTLS("./testdata/invalid_cert.p12", "password")
|
||||
|
||||
// We expect an error (cert file not found), but NOT a panic
|
||||
if err == nil {
|
||||
t.Error("Expected error due to invalid cert path, but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHttpWithTLS_CustomTransport tests the scenario where DefaultHTTPClient has a custom Transport
|
||||
func TestHttpWithTLS_CustomTransport(t *testing.T) {
|
||||
// Save original transport
|
||||
originalTransport := DefaultHTTPClient.Transport
|
||||
defer func() {
|
||||
DefaultHTTPClient.Transport = originalTransport
|
||||
}()
|
||||
|
||||
// Set a custom http.Transport
|
||||
customTransport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
}
|
||||
DefaultHTTPClient.Transport = customTransport
|
||||
|
||||
// This should not panic
|
||||
_, err := httpWithTLS("./testdata/invalid_cert.p12", "password")
|
||||
|
||||
// We expect an error (cert file not found), but NOT a panic
|
||||
if err == nil {
|
||||
t.Error("Expected error due to invalid cert path, but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// CustomRoundTripper is a custom implementation of http.RoundTripper
|
||||
type CustomRoundTripper struct{}
|
||||
|
||||
func (c *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// TestHttpWithTLS_CustomRoundTripper tests the edge case where DefaultHTTPClient has a custom RoundTripper
|
||||
// that is NOT *http.Transport
|
||||
func TestHttpWithTLS_CustomRoundTripper(t *testing.T) {
|
||||
// Save original transport
|
||||
originalTransport := DefaultHTTPClient.Transport
|
||||
defer func() {
|
||||
DefaultHTTPClient.Transport = originalTransport
|
||||
}()
|
||||
|
||||
// Set a custom RoundTripper that is NOT *http.Transport
|
||||
customRoundTripper := &CustomRoundTripper{}
|
||||
DefaultHTTPClient.Transport = customRoundTripper
|
||||
|
||||
// Create a recovery handler to catch potential panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("httpWithTLS panicked with custom RoundTripper: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// This might panic if the code doesn't handle non-*http.Transport RoundTripper properly
|
||||
_, _ = httpWithTLS("./testdata/invalid_cert.p12", "password")
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,16 +16,3 @@ func Signature(params ...string) string {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,18 @@ const (
|
||||
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
||||
// convertToUserIDURL openID转userID
|
||||
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
||||
// 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"
|
||||
// getUseridURL 手机号获取userid
|
||||
getUseridURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=%s"
|
||||
// getUseridByEmailURL 邮箱获取userid
|
||||
getUseridByEmailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get_userid_by_email?access_token=%s"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -158,17 +170,20 @@ func (r *Client) UserCreate(req *UserCreateRequest) (*UserCreateResponse, error)
|
||||
|
||||
// UserUpdateRequest 更新成员请求
|
||||
type UserUpdateRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
NewUserID string `json:"new_userid"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Mobile string `json:"mobile"`
|
||||
Department []int `json:"department"`
|
||||
Order []int `json:"order"`
|
||||
Position string `json:"position"`
|
||||
Gender int `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
BizMail string `json:"biz_mail"`
|
||||
UserID string `json:"userid"`
|
||||
NewUserID string `json:"new_userid"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Mobile string `json:"mobile"`
|
||||
Department []int `json:"department"`
|
||||
Order []int `json:"order"`
|
||||
Position string `json:"position"`
|
||||
Gender int `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
BizMail string `json:"biz_mail"`
|
||||
BizMailAlias struct {
|
||||
Item []string `json:"item"`
|
||||
} `json:"biz_mail_alias"`
|
||||
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||
DirectLeader []string `json:"direct_leader"`
|
||||
Enable int `json:"enable"`
|
||||
@@ -444,3 +459,167 @@ func (r *Client) ConvertToUserID(openID string) (string, error) {
|
||||
err = util.DecodeWithError(response, result, "ConvertToUserID")
|
||||
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
|
||||
}
|
||||
|
||||
// GetUseridRequest 手机号获取userid请求
|
||||
type GetUseridRequest struct {
|
||||
Mobile string `json:"mobile"`
|
||||
}
|
||||
|
||||
// GetUseridResponse 获取userid响应
|
||||
type GetUseridResponse struct {
|
||||
util.CommonError
|
||||
Userid string `json:"userid"`
|
||||
}
|
||||
|
||||
// GetUserid 手机号获取userid
|
||||
// see https://developer.work.weixin.qq.com/document/path/95402
|
||||
func (r *Client) GetUserid(req *GetUseridRequest) (*GetUseridResponse, 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(getUseridURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetUseridResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetUserid")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetUseridByEmailRequest 邮箱获取userid请求
|
||||
type GetUseridByEmailRequest struct {
|
||||
Email string `json:"email"`
|
||||
EmailType int `json:"email_type,omitempty"`
|
||||
}
|
||||
|
||||
// GetUseridByEmail 邮箱获取userid
|
||||
// see https://developer.work.weixin.qq.com/document/path/95895
|
||||
func (r *Client) GetUseridByEmail(req *GetUseridByEmailRequest) (*GetUseridResponse, 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(getUseridByEmailURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetUseridResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetUseridByEmail")
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const (
|
||||
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"
|
||||
// addRecordURL 添加打卡记录
|
||||
addRecordURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/add_checkin_record?access_token=%s"
|
||||
)
|
||||
|
||||
// SetScheduleListRequest 为打卡人员排班请求
|
||||
@@ -140,6 +142,7 @@ type OptionGroupRule struct {
|
||||
SyncOutCheckin bool `json:"sync_out_checkin,omitempty"`
|
||||
BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"`
|
||||
BukaRestriction int64 `json:"buka_restriction,omitempty"`
|
||||
CheckinMethodType int64 `json:"checkin_method_type,omitempty"`
|
||||
SpanDayTime int64 `json:"span_day_time,omitempty"`
|
||||
StandardWorkDuration int64 `json:"standard_work_duration,omitempty"`
|
||||
}
|
||||
@@ -155,24 +158,32 @@ type OptionGroupRuleCheckinDate struct {
|
||||
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
|
||||
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
|
||||
LateRule OptionGroupLateRule `json:"late_rule"`
|
||||
Biweekly OptionGroupBiweekly `json:"biweekly,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
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 晚走晚到时间规则信息
|
||||
@@ -189,6 +200,13 @@ type OptionGroupTimeRule struct {
|
||||
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 特殊工作日
|
||||
type OptionGroupSpeWorkdays struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
@@ -385,3 +403,41 @@ func (r *Client) DelOption(req *DelOptionRequest) error {
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DelOption")
|
||||
}
|
||||
|
||||
// AddRecordRequest 添加打卡记录请求
|
||||
type AddRecordRequest struct {
|
||||
Records []Record `json:"records"`
|
||||
}
|
||||
|
||||
// Record 打卡记录
|
||||
type Record struct {
|
||||
UserID string `json:"userid"`
|
||||
CheckinTime int64 `json:"checkin_time"`
|
||||
LocationTitle string `json:"location_title"`
|
||||
LocationDetail string `json:"location_detail"`
|
||||
MediaIDS []string `json:"mediaids"`
|
||||
Notes string `json:"notes"`
|
||||
DeviceType int `json:"device_type"`
|
||||
Lat int64 `json:"lat"`
|
||||
Lng int64 `json:"lng"`
|
||||
DeviceDetail string `json:"device_detail"`
|
||||
WifiName string `json:"wifiname"`
|
||||
WifiMac string `json:"wifimac"`
|
||||
}
|
||||
|
||||
// AddRecord 添加打卡记录
|
||||
// see https://developer.work.weixin.qq.com/document/path/99647
|
||||
func (r *Client) AddRecord(req *AddRecordRequest) 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(addRecordURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "AddRecord")
|
||||
}
|
||||
|
||||
@@ -173,9 +173,15 @@ type (
|
||||
|
||||
// OtInfo 加班信息
|
||||
OtInfo struct {
|
||||
OtStatus int64 `json:"ot_status"`
|
||||
OtDuration int64 `json:"ot_duration"`
|
||||
ExceptionDuration []uint64 `json:"exception_duration"`
|
||||
OtStatus int64 `json:"ot_status"`
|
||||
OtDuration int64 `json:"ot_duration"`
|
||||
ExceptionDuration []uint64 `json:"exception_duration"`
|
||||
WorkdayOverAsVacation int64 `json:"workday_over_as_vacation"`
|
||||
WorkdayOverAsMoney int64 `json:"workday_over_as_money"`
|
||||
RestdayOverAsVacation int64 `json:"restday_over_as_vacation"`
|
||||
RestdayOverAsMoney int64 `json:"restday_over_as_money"`
|
||||
HolidayOverAsVacation int64 `json:"holiday_over_as_vacation"`
|
||||
HolidayOverAsMoney int64 `json:"holiday_over_as_money"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -237,13 +243,20 @@ type (
|
||||
RegularDays int64 `json:"regular_days"`
|
||||
RegularWorkSec int64 `json:"regular_work_sec"`
|
||||
StandardWorkSec int64 `json:"standard_work_sec"`
|
||||
RestDays int64 `json:"rest_days"`
|
||||
}
|
||||
|
||||
// OverWorkInfo 加班情况
|
||||
OverWorkInfo struct {
|
||||
WorkdayOverSec int64 `json:"workday_over_sec"`
|
||||
HolidayOverSec int64 `json:"holidays_over_sec"`
|
||||
RestDayOverSec int64 `json:"restdays_over_sec"`
|
||||
WorkdayOverSec int64 `json:"workday_over_sec"`
|
||||
HolidayOverSec int64 `json:"holidays_over_sec"`
|
||||
RestDayOverSec int64 `json:"restdays_over_sec"`
|
||||
WorkdaysOverAsVacation int64 `json:"workdays_over_as_vacation"`
|
||||
WorkdaysOverAsMoney int64 `json:"workdays_over_as_money"`
|
||||
RestdaysOverAsVacation int64 `json:"restdays_over_as_vacation"`
|
||||
RestdaysOverAsMoney int64 `json:"restdays_over_as_money"`
|
||||
HolidaysOverAsVacation int64 `json:"holidays_over_as_vacation"`
|
||||
HolidaysOverAsMoney int64 `json:"holidays_over_as_money"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -304,6 +317,10 @@ type CorpOptionGroup struct {
|
||||
BukaRestriction int64 `json:"buka_restriction"`
|
||||
ScheduleList []ScheduleList `json:"schedulelist"`
|
||||
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
|
||||
SpanDayTime int64 `json:"span_day_time"`
|
||||
StandardWorkDuration int64 `json:"standard_work_duration"`
|
||||
OpenSpCheckin bool `json:"open_sp_checkin"`
|
||||
CheckinMethodType int64 `json:"checkin_method_type"`
|
||||
}
|
||||
|
||||
// GroupCheckinDate 打卡时间,当规则类型为排班时没有意义
|
||||
@@ -505,6 +522,7 @@ type OptionInfo struct {
|
||||
type OptionGroup struct {
|
||||
GroupType int64 `json:"grouptype"`
|
||||
GroupID int64 `json:"groupid"`
|
||||
OpenSpCheckin bool `json:"open_sp_checkin"`
|
||||
GroupName string `json:"groupname"`
|
||||
CheckinDate []OptionCheckinDate `json:"checkindate"`
|
||||
SpeWorkdays []SpeWorkdays `json:"spe_workdays"`
|
||||
@@ -518,6 +536,10 @@ type OptionGroup struct {
|
||||
LocInfos []LocInfos `json:"loc_infos"`
|
||||
ScheduleList []ScheduleList `json:"schedulelist"`
|
||||
BukaRestriction int64 `json:"buka_restriction"`
|
||||
SpanDayTime int64 `json:"span_day_time"`
|
||||
StandardWorkDuration int64 `json:"standard_work_duration"`
|
||||
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
|
||||
CheckinMethodType int64 `json:"checkin_method_type"`
|
||||
}
|
||||
|
||||
// OptionCheckinDate 打卡时间配置
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -17,6 +17,8 @@ const (
|
||||
listContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_contact_way?access_token=%s"
|
||||
// delContactWayURL 删除企业已配置的「联系我」方式
|
||||
delContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_contact_way?access_token=%s"
|
||||
// closeTempChatURL 结束临时会话
|
||||
closeTempChatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/close_temp_chat?access_token=%s"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -77,6 +79,8 @@ type (
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ChatExpiresIn int `json:"chat_expires_in"`
|
||||
UnionID string `json:"unionid"`
|
||||
IsExclusive bool `json:"is_exclusive"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||
}
|
||||
// AddContactWayResponse 配置客户联系「联系我」方式响应
|
||||
@@ -132,6 +136,7 @@ type (
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ChatExpiresIn int `json:"chat_expires_in"`
|
||||
UnionID string `json:"unionid"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
Conclusions ConclusionsResponse `json:"conclusions"`
|
||||
}
|
||||
)
|
||||
@@ -168,6 +173,7 @@ type (
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ChatExpiresIn int `json:"chat_expires_in"`
|
||||
UnionID string `json:"unionid"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||
}
|
||||
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
|
||||
@@ -263,3 +269,26 @@ func (r *Client) DelContactWay(req *DelContactWayRequest) (*DelContactWayRespons
|
||||
err = util.DecodeWithError(response, result, "DelContactWay")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CloseTempChatRequest 结束临时会话请求
|
||||
type CloseTempChatRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
ExternalUserID string `json:"external_userid"`
|
||||
}
|
||||
|
||||
// CloseTempChat 结束临时会话
|
||||
// @see https://developer.work.weixin.qq.com/document/path/92228
|
||||
func (r *Client) CloseTempChat(req *CloseTempChatRequest) 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(closeTempChatURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "CloseTempChat")
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ const (
|
||||
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
||||
// customerAcquisitionStatistic 查询链接使用详情
|
||||
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
||||
// customerAcquisitionGetChatInfo 获取成员多次收消息详情
|
||||
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 (
|
||||
@@ -66,9 +70,10 @@ type (
|
||||
// GetCustomerAcquisitionResponse 获取获客链接详情响应
|
||||
GetCustomerAcquisitionResponse struct {
|
||||
util.CommonError
|
||||
Link Link `json:"link"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
Link Link `json:"link"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
}
|
||||
// Link 获客链接
|
||||
Link struct {
|
||||
@@ -76,6 +81,8 @@ type (
|
||||
LinkName string `json:"link_name"`
|
||||
URL string `json:"url"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
|
||||
// CustomerAcquisitionRange 该获客链接使用范围
|
||||
@@ -83,6 +90,12 @@ type (
|
||||
UserList []string `json:"user_list"`
|
||||
DepartmentList []int64 `json:"department_list"`
|
||||
}
|
||||
|
||||
// CustomerPriorityOption 该获客链接的优先选项
|
||||
CustomerPriorityOption struct {
|
||||
PriorityType int `json:"priority_type"`
|
||||
PriorityUseridList []string `json:"priority_userid_list"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetCustomerAcquisition 获客助手--获取获客链接详情
|
||||
@@ -107,9 +120,11 @@ func (r *Client) GetCustomerAcquisition(req *GetCustomerAcquisitionRequest) (*Ge
|
||||
type (
|
||||
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
|
||||
CreateCustomerAcquisitionLinkRequest struct {
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
|
||||
CreateCustomerAcquisitionLinkResponse struct {
|
||||
@@ -140,10 +155,12 @@ func (r *Client) CreateCustomerAcquisitionLink(req *CreateCustomerAcquisitionLin
|
||||
type (
|
||||
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
|
||||
UpdateCustomerAcquisitionLinkRequest struct {
|
||||
LinkID string `json:"link_id"`
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
LinkID string `json:"link_id"`
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
|
||||
UpdateCustomerAcquisitionLinkResponse struct {
|
||||
@@ -308,3 +325,69 @@ func (r *Client) CustomerAcquisitionStatistic(req *CustomerAcquisitionStatisticR
|
||||
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
||||
return result, err
|
||||
}
|
||||
|
||||
type (
|
||||
// GetChatInfoRequest 获取成员多次收消息详情请求
|
||||
GetChatInfoRequest struct {
|
||||
ChatKey string `json:"chat_key"`
|
||||
}
|
||||
// GetChatInfoResponse 获取成员多次收消息详情响应
|
||||
GetChatInfoResponse struct {
|
||||
util.CommonError
|
||||
UserID string `json:"userid"`
|
||||
ExternalUserID string `json:"external_userid"`
|
||||
ChatInfo ChatInfo `json:"chat_info"`
|
||||
}
|
||||
// ChatInfo 聊天信息
|
||||
ChatInfo struct {
|
||||
RecvMsgCnt int64 `json:"recv_msg_cnt"` // 成员收到的此客户的消息次数
|
||||
LinkID string `json:"link_id"` // 成员添加客户的获客链接id
|
||||
State string `json:"state"` // 成员添加客户的state
|
||||
}
|
||||
)
|
||||
|
||||
// GetChatInfo 获取成员多次收消息详情
|
||||
// see https://developer.work.weixin.qq.com/document/path/100130
|
||||
func (r *Client) GetChatInfo(req *GetChatInfoRequest) (*GetChatInfoResponse, 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(customerAcquisitionGetChatInfoURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetChatInfoResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetChatInfo")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ type BatchGetExternalUserDetailsRequest struct {
|
||||
type ExternalUserDetailListResponse struct {
|
||||
util.CommonError
|
||||
ExternalContactList []ExternalUserForBatch `json:"external_contact_list"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
}
|
||||
|
||||
// ExternalUserForBatch 批量获取外部联系人客户列表
|
||||
@@ -214,23 +215,23 @@ type FollowInfo struct {
|
||||
|
||||
// BatchGetExternalUserDetails 批量获取外部联系人详情
|
||||
// @see https://developer.work.weixin.qq.com/document/path/92994
|
||||
func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, error) {
|
||||
func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, string, error) {
|
||||
accessToken, err := r.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
var response []byte
|
||||
jsonData, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", fetchBatchExternalContactUserDetailURL, accessToken), string(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
var result ExternalUserDetailListResponse
|
||||
err = util.DecodeWithError(response, &result, "BatchGetExternalUserDetails")
|
||||
return result.ExternalContactList, err
|
||||
return result.ExternalContactList, result.NextCursor, err
|
||||
}
|
||||
|
||||
// UpdateUserRemarkRequest 修改客户备注信息请求体
|
||||
|
||||
@@ -19,6 +19,7 @@ type (
|
||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
|
||||
// AddJoinWayResponse 添加群配置返回值
|
||||
@@ -65,6 +66,7 @@ type (
|
||||
ChatIDList []string `json:"chat_id_list"`
|
||||
QrCode string `json:"qr_code"`
|
||||
State string `json:"state"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
//GetJoinWayResponse 获取群配置的返回值
|
||||
GetJoinWayResponse struct {
|
||||
@@ -103,6 +105,7 @@ type UpdateJoinWayRequest struct {
|
||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
|
||||
// UpdateJoinWay 更新客户群进群方式配置
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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
|
||||
}
|
||||
75
work/jsapi/jsapi.go
Normal file
75
work/jsapi/jsapi.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package jsapi
|
||||
|
||||
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
|
||||
jsTicket *credential.WorkJsTicket
|
||||
}
|
||||
|
||||
// NewJs init
|
||||
func NewJs(context *context.Context) *Js {
|
||||
js := new(Js)
|
||||
js.Context = context
|
||||
js.jsTicket = credential.NewWorkJsTicket(
|
||||
context.Config.CorpID,
|
||||
context.Config.AgentID,
|
||||
credential.CacheKeyWorkPrefix,
|
||||
context.Cache,
|
||||
)
|
||||
return js
|
||||
}
|
||||
|
||||
// Config 返回给用户使用的配置
|
||||
type Config struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// GetConfig 获取企业微信JS配置 https://developer.work.weixin.qq.com/document/path/90514
|
||||
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.jsTicket.GetTicket(accessToken, credential.TicketTypeCorpJs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
config.NonceStr = util.RandomStr(16)
|
||||
config.Timestamp = util.GetCurrTS()
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, config.NonceStr, config.Timestamp, uri)
|
||||
config.Signature = util.Signature(str)
|
||||
return
|
||||
}
|
||||
|
||||
// GetAgentConfig 获取企业微信应用JS配置 https://developer.work.weixin.qq.com/document/path/94313
|
||||
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.jsTicket.GetTicket(accessToken, credential.TicketTypeAgentJs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
config.NonceStr = util.RandomStr(16)
|
||||
config.Timestamp = util.GetCurrTS()
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, config.NonceStr, config.Timestamp, uri)
|
||||
config.Signature = util.Signature(str)
|
||||
return
|
||||
}
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
// SignatureOptions 微信服务器验证参数
|
||||
type SignatureOptions struct {
|
||||
Signature string `form:"msg_signature"`
|
||||
TimeStamp string `form:"timestamp"`
|
||||
Nonce string `form:"nonce"`
|
||||
EchoStr string `form:"echostr"`
|
||||
Signature string `form:"msg_signature" json:"msg_signature"`
|
||||
TimeStamp string `form:"timestamp" json:"timestamp"`
|
||||
Nonce string `form:"nonce" json:"nonce"`
|
||||
EchoStr string `form:"echostr" json:"echostr"`
|
||||
}
|
||||
|
||||
// VerifyURL 验证请求参数是否合法并返回解密后的消息内容
|
||||
|
||||
@@ -191,12 +191,6 @@ func (r *Client) GetTempFile(mediaID string) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查响应是否为错误信息
|
||||
err = util.DecodeWithCommonError(response, "GetTempFile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果不是错误响应,则返回原始数据
|
||||
return response, nil
|
||||
// 检查响应是否为错误信息,如果不是错误响应,则返回原始数据
|
||||
return util.HandleFileResponse(response, "GetTempFile")
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package message
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
MsgType string `json:"msgtype"`
|
||||
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
||||
AgentID int `json:"agentid"`
|
||||
AgentID string `json:"agentid"`
|
||||
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
||||
Safe int `json:"safe"`
|
||||
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package message
|
||||
|
||||
import "errors"
|
||||
|
||||
//ErrInvalidReply 无效的回复
|
||||
var ErrInvalidReply = errors.New("无效的回复消息")
|
||||
|
||||
//ErrUnsupportReply 不支持的回复类型
|
||||
var ErrUnsupportReply = errors.New("无需回复消息")
|
||||
|
||||
//Reply 消息回复
|
||||
type Reply struct {
|
||||
MsgType MsgType
|
||||
MsgData interface{}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -27,6 +27,10 @@ var (
|
||||
getUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s"
|
||||
// getUserDetailURL 获取访问用户敏感信息
|
||||
getUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s"
|
||||
// getTfaInfoURL 获取用户二次验证信息
|
||||
getTfaInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/get_tfa_info?access_token=%s"
|
||||
// tfaSuccURL 使用二次验证
|
||||
tfaSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/tfa_succ?access_token=%s"
|
||||
)
|
||||
|
||||
// NewOauth new init oauth
|
||||
@@ -163,3 +167,57 @@ func (ctr *Oauth) GetUserDetail(req *GetUserDetailRequest) (*GetUserDetailRespon
|
||||
err = util.DecodeWithError(response, result, "GetUserDetail")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetTfaInfoRequest 获取用户二次验证信息请求
|
||||
type GetTfaInfoRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// GetTfaInfoResponse 获取用户二次验证信息响应
|
||||
type GetTfaInfoResponse struct {
|
||||
util.CommonError
|
||||
UserID string `json:"userid"`
|
||||
TfaCode string `json:"tfa_code"`
|
||||
}
|
||||
|
||||
// GetTfaInfo 获取用户二次验证信息
|
||||
// @see https://developer.work.weixin.qq.com/document/path/99499
|
||||
func (ctr *Oauth) GetTfaInfo(req *GetTfaInfoRequest) (*GetTfaInfoResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = ctr.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getTfaInfoURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetTfaInfoResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetTfaInfo")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TfaSuccRequest 使用二次验证请求
|
||||
type TfaSuccRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
TfaCode string `json:"tfa_code"`
|
||||
}
|
||||
|
||||
// TfaSucc 使用二次验证
|
||||
// @see https://developer.work.weixin.qq.com/document/path/99500
|
||||
func (ctr *Oauth) TfaSucc(req *TfaSuccRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = ctr.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(tfaSuccURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "TfaSucc")
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
22
work/work.go
22
work/work.go
@@ -9,15 +9,13 @@ import (
|
||||
"github.com/silenceper/wechat/v2/work/context"
|
||||
"github.com/silenceper/wechat/v2/work/externalcontact"
|
||||
"github.com/silenceper/wechat/v2/work/invoice"
|
||||
"github.com/silenceper/wechat/v2/work/js"
|
||||
"github.com/silenceper/wechat/v2/work/jsapi"
|
||||
"github.com/silenceper/wechat/v2/work/kf"
|
||||
"github.com/silenceper/wechat/v2/work/material"
|
||||
"github.com/silenceper/wechat/v2/work/message"
|
||||
"github.com/silenceper/wechat/v2/work/msgaudit"
|
||||
"github.com/silenceper/wechat/v2/work/oauth"
|
||||
"github.com/silenceper/wechat/v2/work/robot"
|
||||
"github.com/silenceper/wechat/v2/work/server"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Work 企业微信
|
||||
@@ -55,6 +53,11 @@ func (wk *Work) GetKF() (*kf.Client, error) {
|
||||
return kf.NewClient(wk.ctx.Config)
|
||||
}
|
||||
|
||||
// JsSdk get JsSdk
|
||||
func (wk *Work) JsSdk() *jsapi.Js {
|
||||
return jsapi.NewJs(wk.ctx)
|
||||
}
|
||||
|
||||
// GetExternalContact get external_contact
|
||||
func (wk *Work) GetExternalContact() *externalcontact.Client {
|
||||
return externalcontact.NewClient(wk.ctx)
|
||||
@@ -94,16 +97,3 @@ func (wk *Work) GetInvoice() *invoice.Client {
|
||||
func (wk *Work) GetCheckin() *checkin.Client {
|
||||
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