mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-11 16:22:26 +08:00
Compare commits
33 Commits
v2
...
a532cdcbd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a532cdcbd2 | ||
|
|
0e7907e9ac | ||
|
|
d08b18983b | ||
|
|
d28c61ad4a | ||
|
|
961d5604d5 | ||
|
|
0f3c9cd773 | ||
|
|
06e92dd5ce | ||
|
|
8a044dcd58 | ||
|
|
a7347f9506 | ||
|
|
bcdb2fa6ca | ||
|
|
61bcd6b0e4 | ||
|
|
4a4339fc32 | ||
|
|
06c225c351 | ||
|
|
1c2360737c | ||
|
|
158fbca872 | ||
|
|
e351d0bc66 | ||
|
|
589de19257 | ||
|
|
1cd6133420 | ||
|
|
b4402678b3 | ||
|
|
cec81779a5 | ||
|
|
da20182300 | ||
|
|
502a781811 | ||
|
|
586a3b058e | ||
|
|
96e7945d20 | ||
|
|
6313e3d580 | ||
|
|
88f07bc5fb | ||
|
|
1f80c26a15 | ||
|
|
5704abb3b0 | ||
|
|
7ae8e08a3e | ||
|
|
73adb7dcdd | ||
|
|
af5115fb4e | ||
|
|
829356eee9 | ||
|
|
7e24cb9e8d |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: # silenceper
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: gowechat
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
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
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
|||||||
38
.github/workflows/ai-dev.yaml
vendored
38
.github/workflows/ai-dev.yaml
vendored
@@ -1,38 +0,0 @@
|
|||||||
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,13 +1,12 @@
|
|||||||
# WeChat SDK for Go
|
# WeChat SDK for Go
|
||||||
|
|
||||||

|

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

|

|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
使用Golang开发的微信SDK,简单、易用。
|
使用Golang开发的微信SDK,简单、易用。
|
||||||
|
> 注意:当前版本为v2版本,v1版本已废弃
|
||||||
|
|
||||||
## 文档 && 例子
|
## 文档 && 例子
|
||||||
|
|
||||||
@@ -76,13 +75,7 @@ server.Send()
|
|||||||
- 提交issue,描述需要贡献的内容
|
- 提交issue,描述需要贡献的内容
|
||||||
- 完成更改后,提交PR
|
- 完成更改后,提交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,8 +2,6 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
@@ -17,76 +15,25 @@ type Redis struct {
|
|||||||
|
|
||||||
// RedisOpts redis 连接属性
|
// RedisOpts redis 连接属性
|
||||||
type RedisOpts struct {
|
type RedisOpts struct {
|
||||||
Host string `json:"host" yaml:"host"`
|
Host string `yml:"host" json:"host"`
|
||||||
Username string `json:"username" yaml:"username"`
|
Username string `yaml:"username" json:"username"`
|
||||||
Password string `json:"password" yaml:"password"`
|
Password string `yml:"password" json:"password"`
|
||||||
Database int `json:"database" yaml:"database"`
|
Database int `yml:"database" json:"database"`
|
||||||
MinIdleConns int `json:"min_idle_conns" yaml:"min_idle_conns"` // 最小空闲连接数
|
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
||||||
PoolSize int `json:"pool_size" yaml:"pool_size"` // 连接池大小,0 表示使用默认值(即 CPU 核心数 * 10)
|
MaxActive int `yml:"max_active" json:"max_active"`
|
||||||
MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数,-1 表示不重试,0 表示使用默认值(即 3 次)
|
IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` // second
|
||||||
DialTimeout int `json:"dial_timeout" yaml:"dial_timeout"` // 连接超时时间(秒),0 表示使用默认值(即 5 秒)
|
|
||||||
ReadTimeout int `json:"read_timeout" yaml:"read_timeout"` // 读取超时时间(秒),-1 表示不超时,0 表示使用默认值(即 3 秒)
|
|
||||||
WriteTimeout int `json:"write_timeout" yaml:"write_timeout"` // 写入超时时间(秒),-1 表示不超时,0 表示使用默认值(即 ReadTimeout)
|
|
||||||
PoolTimeout int `json:"pool_timeout" yaml:"pool_timeout"` // 连接池获取连接超时时间(秒),0 表示使用默认值(即 ReadTimeout + 1 秒)
|
|
||||||
IdleTimeout int `json:"idle_timeout" yaml:"idle_timeout"` // 空闲连接超时时间(秒),-1 表示禁用空闲连接超时检查,0 表示使用默认值(即 5 分钟)
|
|
||||||
UseTLS bool `json:"use_tls" yaml:"use_tls"` // 是否使用 TLS
|
|
||||||
|
|
||||||
// Deprecated: 应使用 MinIdleConns 代替
|
|
||||||
MaxIdle int `json:"max_idle" yaml:"max_idle"`
|
|
||||||
// Deprecated: 应使用 PoolSize 代替
|
|
||||||
MaxActive int `json:"max_active" yaml:"max_active"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRedis 实例化
|
// NewRedis 实例化
|
||||||
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
||||||
uniOpt := &redis.UniversalOptions{
|
conn := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||||
Addrs: []string{opts.Host},
|
Addrs: []string{opts.Host},
|
||||||
DB: opts.Database,
|
DB: opts.Database,
|
||||||
Username: opts.Username,
|
Username: opts.Username,
|
||||||
Password: opts.Password,
|
Password: opts.Password,
|
||||||
MinIdleConns: opts.MinIdleConns,
|
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||||
PoolSize: opts.PoolSize,
|
MinIdleConns: opts.MaxIdle,
|
||||||
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}
|
return &Redis{ctx: ctx, conn: conn}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
268
cache/redis_test.go
vendored
268
cache/redis_test.go
vendored
@@ -6,7 +6,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alicebob/miniredis/v2"
|
"github.com/alicebob/miniredis/v2"
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRedis(t *testing.T) {
|
func TestRedis(t *testing.T) {
|
||||||
@@ -19,16 +18,7 @@ func TestRedis(t *testing.T) {
|
|||||||
timeoutDuration = time.Second
|
timeoutDuration = time.Second
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
opts = &RedisOpts{
|
opts = &RedisOpts{
|
||||||
Host: server.Addr(),
|
Host: server.Addr(),
|
||||||
Password: "",
|
|
||||||
Database: 0,
|
|
||||||
PoolSize: 10,
|
|
||||||
MinIdleConns: 5,
|
|
||||||
DialTimeout: 5,
|
|
||||||
ReadTimeout: 5,
|
|
||||||
WriteTimeout: 5,
|
|
||||||
PoolTimeout: 5,
|
|
||||||
IdleTimeout: 300,
|
|
||||||
}
|
}
|
||||||
redis = NewRedis(ctx, opts)
|
redis = NewRedis(ctx, opts)
|
||||||
val = "silenceper"
|
val = "silenceper"
|
||||||
@@ -54,259 +44,3 @@ func TestRedis(t *testing.T) {
|
|||||||
t.Errorf("delete Error , err=%v", err)
|
t.Errorf("delete Error , err=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupRedisServer 创建并返回一个 miniredis 服务器实例
|
|
||||||
func setupRedisServer(t *testing.T) *miniredis.Miniredis {
|
|
||||||
server, err := miniredis.Run()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("miniredis.Run Error", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisMaxIdleMapping 测试只设置MaxIdle应该映射到MinIdleConns
|
|
||||||
func TestRedisMaxIdleMapping(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
MaxIdle: 10,
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
// 获取底层的 UniversalClient 并断言为 *redis.Client
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注意:MinIdleConns 表示期望的最小空闲连接数,但实际空闲连接数可能不同
|
|
||||||
// 我们需要通过 Options() 来验证配置是否正确应用
|
|
||||||
clientOpts := client.Options()
|
|
||||||
if clientOpts.MinIdleConns != 10 {
|
|
||||||
t.Errorf("期望 MinIdleConns = 10, 实际 = %d", clientOpts.MinIdleConns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisMaxActiveMapping 测试只设置MaxActive应该映射到PoolSize
|
|
||||||
func TestRedisMaxActiveMapping(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
MaxActive: 20,
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts := client.Options()
|
|
||||||
if clientOpts.PoolSize != 20 {
|
|
||||||
t.Errorf("期望 PoolSize = 20, 实际 = %d", clientOpts.PoolSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisNewFieldsPriority 测试新字段应该优先于旧字段
|
|
||||||
func TestRedisNewFieldsPriority(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
MaxIdle: 5,
|
|
||||||
MinIdleConns: 15,
|
|
||||||
MaxActive: 10,
|
|
||||||
PoolSize: 30,
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts := client.Options()
|
|
||||||
if clientOpts.MinIdleConns != 15 {
|
|
||||||
t.Errorf("期望 MinIdleConns = 15 (新字段优先), 实际 = %d", clientOpts.MinIdleConns)
|
|
||||||
}
|
|
||||||
if clientOpts.PoolSize != 30 {
|
|
||||||
t.Errorf("期望 PoolSize = 30 (新字段优先), 实际 = %d", clientOpts.PoolSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisPositiveTimeouts 测试正值超时应该正确应用
|
|
||||||
func TestRedisPositiveTimeouts(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
DialTimeout: 10,
|
|
||||||
ReadTimeout: 20,
|
|
||||||
WriteTimeout: 30,
|
|
||||||
PoolTimeout: 40,
|
|
||||||
IdleTimeout: 50,
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts := client.Options()
|
|
||||||
if clientOpts.DialTimeout != 10*time.Second {
|
|
||||||
t.Errorf("期望 DialTimeout = 10s, 实际 = %v", clientOpts.DialTimeout)
|
|
||||||
}
|
|
||||||
if clientOpts.ReadTimeout != 20*time.Second {
|
|
||||||
t.Errorf("期望 ReadTimeout = 20s, 实际 = %v", clientOpts.ReadTimeout)
|
|
||||||
}
|
|
||||||
if clientOpts.WriteTimeout != 30*time.Second {
|
|
||||||
t.Errorf("期望 WriteTimeout = 30s, 实际 = %v", clientOpts.WriteTimeout)
|
|
||||||
}
|
|
||||||
if clientOpts.PoolTimeout != 40*time.Second {
|
|
||||||
t.Errorf("期望 PoolTimeout = 40s, 实际 = %v", clientOpts.PoolTimeout)
|
|
||||||
}
|
|
||||||
if clientOpts.IdleTimeout != 50*time.Second {
|
|
||||||
t.Errorf("期望 IdleTimeout = 50s, 实际 = %v", clientOpts.IdleTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisNegativeTimeouts 测试-1值应该禁用超时
|
|
||||||
func TestRedisNegativeTimeouts(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
DialTimeout: -1,
|
|
||||||
ReadTimeout: -1,
|
|
||||||
WriteTimeout: -1,
|
|
||||||
PoolTimeout: -1,
|
|
||||||
IdleTimeout: -1,
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts := client.Options()
|
|
||||||
// -1 应该被设置为负值表示禁用超时
|
|
||||||
// DialTimeout, PoolTimeout, IdleTimeout 会被设置为 -1ns
|
|
||||||
if clientOpts.DialTimeout != -1 {
|
|
||||||
t.Errorf("期望 DialTimeout = -1ns (禁用), 实际 = %v", clientOpts.DialTimeout)
|
|
||||||
}
|
|
||||||
// ReadTimeout 和 WriteTimeout 在 go-redis 中有特殊处理
|
|
||||||
// 当设置为负值时,会被规范化为 0,这也表示无超时
|
|
||||||
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
|
|
||||||
t.Logf("WriteTimeout = %v (设置为-1后的值)", clientOpts.WriteTimeout)
|
|
||||||
|
|
||||||
if clientOpts.PoolTimeout != -1 {
|
|
||||||
t.Errorf("期望 PoolTimeout = -1ns (禁用), 实际 = %v", clientOpts.PoolTimeout)
|
|
||||||
}
|
|
||||||
if clientOpts.IdleTimeout != -1 {
|
|
||||||
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisZeroTimeouts 测试0值应该使用go-redis默认值
|
|
||||||
func TestRedisZeroTimeouts(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
DialTimeout: 0,
|
|
||||||
ReadTimeout: 0,
|
|
||||||
WriteTimeout: 0,
|
|
||||||
PoolTimeout: 0,
|
|
||||||
IdleTimeout: 0,
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts := client.Options()
|
|
||||||
// 0值应该保持为0,由 go-redis 使用默认值
|
|
||||||
// go-redis 的默认值:
|
|
||||||
// DialTimeout: 5s
|
|
||||||
// ReadTimeout: 3s
|
|
||||||
// WriteTimeout: ReadTimeout
|
|
||||||
// PoolTimeout: ReadTimeout + 1s
|
|
||||||
// IdleTimeout: 5min
|
|
||||||
|
|
||||||
if clientOpts.DialTimeout == 0 {
|
|
||||||
t.Error("期望 DialTimeout 使用 go-redis 默认值 (5s), 实际为 0")
|
|
||||||
}
|
|
||||||
if clientOpts.ReadTimeout == 0 {
|
|
||||||
t.Error("期望 ReadTimeout 使用 go-redis 默认值 (3s), 实际为 0")
|
|
||||||
}
|
|
||||||
if clientOpts.WriteTimeout == 0 {
|
|
||||||
t.Error("期望 WriteTimeout 使用 go-redis 默认值 (ReadTimeout), 实际为 0")
|
|
||||||
}
|
|
||||||
if clientOpts.PoolTimeout == 0 {
|
|
||||||
t.Error("期望 PoolTimeout 使用 go-redis 默认值 (ReadTimeout + 1s), 实际为 0")
|
|
||||||
}
|
|
||||||
if clientOpts.IdleTimeout == 0 {
|
|
||||||
t.Error("期望 IdleTimeout 使用 go-redis 默认值 (5min), 实际为 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisMixedTimeouts 测试混合超时配置
|
|
||||||
func TestRedisMixedTimeouts(t *testing.T) {
|
|
||||||
server := setupRedisServer(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := &RedisOpts{
|
|
||||||
Host: server.Addr(),
|
|
||||||
Database: 0,
|
|
||||||
DialTimeout: 5, // 正值
|
|
||||||
ReadTimeout: -1, // 禁用
|
|
||||||
WriteTimeout: 0, // 使用默认值
|
|
||||||
PoolTimeout: 10, // 正值
|
|
||||||
IdleTimeout: -1, // 禁用
|
|
||||||
}
|
|
||||||
r := NewRedis(ctx, opts)
|
|
||||||
|
|
||||||
client, ok := r.conn.(*redis.Client)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("无法转换为 *redis.Client")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts := client.Options()
|
|
||||||
if clientOpts.DialTimeout != 5*time.Second {
|
|
||||||
t.Errorf("期望 DialTimeout = 5s, 实际 = %v", clientOpts.DialTimeout)
|
|
||||||
}
|
|
||||||
// ReadTimeout 设置为 -1,会被 go-redis 处理为 0(无超时)
|
|
||||||
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
|
|
||||||
|
|
||||||
// WriteTimeout 设置为 0,应该使用 go-redis 的默认值
|
|
||||||
// 默认值通常是 ReadTimeout 的值
|
|
||||||
t.Logf("WriteTimeout = %v (设置为0后使用的默认值)", clientOpts.WriteTimeout)
|
|
||||||
|
|
||||||
if clientOpts.PoolTimeout != 10*time.Second {
|
|
||||||
t.Errorf("期望 PoolTimeout = 10s, 实际 = %v", clientOpts.PoolTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdleTimeout 设置为 -1,应该被设置为 -1ns(禁用空闲超时)
|
|
||||||
if clientOpts.IdleTimeout != -1 {
|
|
||||||
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,16 +7,6 @@ type AccessTokenHandle interface {
|
|||||||
GetAccessToken() (accessToken string, err error)
|
GetAccessToken() (accessToken string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessTokenCompatibleHandle 同时实现 AccessTokenHandle 和 AccessTokenContextHandle
|
|
||||||
type AccessTokenCompatibleHandle struct {
|
|
||||||
AccessTokenHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccessTokenContext 获取access_token,先从cache中获取,没有则从服务端获取
|
|
||||||
func (c AccessTokenCompatibleHandle) GetAccessTokenContext(_ context.Context) (accessToken string, err error) {
|
|
||||||
return c.GetAccessToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccessTokenContextHandle AccessToken 接口
|
// AccessTokenContextHandle AccessToken 接口
|
||||||
type AccessTokenContextHandle interface {
|
type AccessTokenContextHandle interface {
|
||||||
AccessTokenHandle
|
AccessTokenHandle
|
||||||
|
|||||||
@@ -101,11 +101,10 @@ func (ak *DefaultAccessToken) GetAccessTokenContext(ctx context.Context) (access
|
|||||||
// 不强制更新access_token,可用于不同环境不同服务而不需要分布式锁以及公用缓存,避免access_token争抢
|
// 不强制更新access_token,可用于不同环境不同服务而不需要分布式锁以及公用缓存,避免access_token争抢
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
|
// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
|
||||||
type StableAccessToken struct {
|
type StableAccessToken struct {
|
||||||
appID string
|
appID string
|
||||||
appSecret string
|
appSecret string
|
||||||
cacheKeyPrefix string
|
cacheKeyPrefix string
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
accessTokenLock *sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStableAccessToken new StableAccessToken
|
// NewStableAccessToken new StableAccessToken
|
||||||
@@ -114,11 +113,10 @@ func NewStableAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.C
|
|||||||
panic("cache is need")
|
panic("cache is need")
|
||||||
}
|
}
|
||||||
return &StableAccessToken{
|
return &StableAccessToken{
|
||||||
appID: appID,
|
appID: appID,
|
||||||
appSecret: appSecret,
|
appSecret: appSecret,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
cacheKeyPrefix: cacheKeyPrefix,
|
cacheKeyPrefix: cacheKeyPrefix,
|
||||||
accessTokenLock: new(sync.Mutex),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,20 +130,7 @@ func (ak *StableAccessToken) GetAccessTokenContext(ctx context.Context) (accessT
|
|||||||
// 先从cache中取
|
// 先从cache中取
|
||||||
accessTokenCacheKey := fmt.Sprintf("%s_stable_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
accessTokenCacheKey := fmt.Sprintf("%s_stable_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
||||||
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
||||||
if accessToken = val.(string); accessToken != "" {
|
return val.(string), nil
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
|
||||||
ak.accessTokenLock.Lock()
|
|
||||||
defer ak.accessTokenLock.Unlock()
|
|
||||||
|
|
||||||
// 双检,防止重复从微信服务器获取
|
|
||||||
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
|
||||||
if accessToken = val.(string); accessToken != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache失效,从微信服务器获取
|
// cache失效,从微信服务器获取
|
||||||
@@ -189,27 +174,19 @@ func (ak *StableAccessToken) GetAccessTokenDirectly(ctx context.Context, forceRe
|
|||||||
type WorkAccessToken struct {
|
type WorkAccessToken struct {
|
||||||
CorpID string
|
CorpID string
|
||||||
CorpSecret string
|
CorpSecret string
|
||||||
AgentID string // 可选,用于区分不同应用
|
|
||||||
cacheKeyPrefix string
|
cacheKeyPrefix string
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
accessTokenLock *sync.Mutex
|
accessTokenLock *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkAccessToken new WorkAccessToken (保持向后兼容)
|
// NewWorkAccessToken new WorkAccessToken
|
||||||
func NewWorkAccessToken(corpID, corpSecret, agentID, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle {
|
func NewWorkAccessToken(corpID, corpSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle {
|
||||||
// 调用新方法,保持兼容性
|
|
||||||
return NewWorkAccessTokenWithAgentID(corpID, corpSecret, agentID, cacheKeyPrefix, cache)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWorkAccessTokenWithAgentID new WorkAccessToken with agentID
|
|
||||||
func NewWorkAccessTokenWithAgentID(corpID, corpSecret, agentID, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle {
|
|
||||||
if cache == nil {
|
if cache == nil {
|
||||||
panic("cache is needed")
|
panic("cache the not exist")
|
||||||
}
|
}
|
||||||
return &WorkAccessToken{
|
return &WorkAccessToken{
|
||||||
CorpID: corpID,
|
CorpID: corpID,
|
||||||
CorpSecret: corpSecret,
|
CorpSecret: corpSecret,
|
||||||
AgentID: agentID,
|
|
||||||
cache: cache,
|
cache: cache,
|
||||||
cacheKeyPrefix: cacheKeyPrefix,
|
cacheKeyPrefix: cacheKeyPrefix,
|
||||||
accessTokenLock: new(sync.Mutex),
|
accessTokenLock: new(sync.Mutex),
|
||||||
@@ -226,18 +203,7 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok
|
|||||||
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
||||||
ak.accessTokenLock.Lock()
|
ak.accessTokenLock.Lock()
|
||||||
defer ak.accessTokenLock.Unlock()
|
defer ak.accessTokenLock.Unlock()
|
||||||
|
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID)
|
||||||
// 构建缓存key
|
|
||||||
var accessTokenCacheKey string
|
|
||||||
|
|
||||||
if ak.AgentID != "" {
|
|
||||||
// 如果设置了AgentID,使用新的key格式
|
|
||||||
accessTokenCacheKey = fmt.Sprintf("%s_access_token_%s_%s", ak.cacheKeyPrefix, ak.CorpID, ak.AgentID)
|
|
||||||
} else {
|
|
||||||
// 兼容历史版本的key格式
|
|
||||||
accessTokenCacheKey = fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID)
|
|
||||||
}
|
|
||||||
|
|
||||||
val := ak.cache.Get(accessTokenCacheKey)
|
val := ak.cache.Get(accessTokenCacheKey)
|
||||||
if val != nil {
|
if val != nil {
|
||||||
accessToken = val.(string)
|
accessToken = val.(string)
|
||||||
@@ -253,9 +219,6 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok
|
|||||||
|
|
||||||
expires := resAccessToken.ExpiresIn - 1500
|
expires := resAccessToken.ExpiresIn - 1500
|
||||||
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken = resAccessToken.AccessToken
|
accessToken = resAccessToken.AccessToken
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context2 "context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -43,16 +42,6 @@ type ResTicket struct {
|
|||||||
|
|
||||||
// GetTicket 获取jsapi_ticket
|
// GetTicket 获取jsapi_ticket
|
||||||
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
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中取
|
// 先从cache中取
|
||||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
@@ -68,7 +57,7 @@ func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, accessToken st
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ticket ResTicket
|
var ticket ResTicket
|
||||||
ticket, err = GetTicketFromServerContext(ctx, accessToken)
|
ticket, err = GetTicketFromServer(accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -78,11 +67,11 @@ func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, accessToken st
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicketFromServerContext 从服务器中获取ticket
|
// GetTicketFromServer 从服务器中获取ticket
|
||||||
func GetTicketFromServerContext(ctx context2.Context, accessToken string) (ticket ResTicket, err error) {
|
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||||
var response []byte
|
var response []byte
|
||||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||||
response, err = util.HTTPGetContext(ctx, url)
|
response, err = util.HTTPGet(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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,15 +1,7 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
import context2 "context"
|
|
||||||
|
|
||||||
// JsTicketHandle js ticket获取
|
// JsTicketHandle js ticket获取
|
||||||
type JsTicketHandle interface {
|
type JsTicketHandle interface {
|
||||||
// GetTicket 获取ticket
|
// GetTicket 获取ticket
|
||||||
GetTicket(accessToken string) (ticket string, err error)
|
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,37 +10,24 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TicketType ticket类型
|
//获取ticket的url https://developer.work.weixin.qq.com/document/path/90506
|
||||||
type TicketType int
|
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"
|
||||||
|
|
||||||
const (
|
//WorkJsTicket 默认获取js ticket方法
|
||||||
// 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 {
|
type WorkJsTicket struct {
|
||||||
corpID string
|
appID string
|
||||||
agentID string
|
agentID string
|
||||||
cacheKeyPrefix string
|
cacheKeyPrefix string
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
|
//jsAPITicket 读写锁 同一个AppID一个
|
||||||
jsAPITicketLock *sync.Mutex
|
jsAPITicketLock *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkJsTicket new WorkJsTicket
|
//NewWorkJsTicket new
|
||||||
func NewWorkJsTicket(corpID, agentID, cacheKeyPrefix string, cache cache.Cache) *WorkJsTicket {
|
func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
||||||
return &WorkJsTicket{
|
return &WorkJsTicket{
|
||||||
corpID: corpID,
|
appID: appID,
|
||||||
agentID: agentID,
|
agentID: agentID,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
cacheKeyPrefix: cacheKeyPrefix,
|
cacheKeyPrefix: cacheKeyPrefix,
|
||||||
@@ -48,24 +35,11 @@ func NewWorkJsTicket(corpID, agentID, cacheKeyPrefix string, cache cache.Cache)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicket 根据类型获取相应的jsapi_ticket
|
//GetTicket 获取企业微信jsapi_ticket
|
||||||
func (js *WorkJsTicket) GetTicket(accessToken string, ticketType TicketType) (ticketStr string, err error) {
|
func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||||
var cacheKey string
|
//先从cache中取
|
||||||
switch ticketType {
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||||
case TicketTypeCorpJs:
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
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
|
return val.(string), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,35 +47,28 @@ func (js *WorkJsTicket) GetTicket(accessToken string, ticketType TicketType) (ti
|
|||||||
defer js.jsAPITicketLock.Unlock()
|
defer js.jsAPITicketLock.Unlock()
|
||||||
|
|
||||||
// 双检,防止重复从微信服务器获取
|
// 双检,防止重复从微信服务器获取
|
||||||
if val := js.cache.Get(cacheKey); val != nil {
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
return val.(string), nil
|
return val.(string), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticket ResTicket
|
var ticket ResTicket
|
||||||
ticket, err = js.getTicketFromServer(accessToken, ticketType)
|
ticket, err = GetQyWxTicketFromServer(accessToken, js.agentID != "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expires := ticket.ExpiresIn - 1500
|
expires := ticket.ExpiresIn - 1500
|
||||||
err = js.cache.Set(cacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
err = js.cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||||
ticketStr = ticket.Ticket
|
ticketStr = ticket.Ticket
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTicketFromServer 从服务器中获取ticket
|
//GetQyWxTicketFromServer 从企业微信服务器中获取ticket
|
||||||
func (js *WorkJsTicket) getTicketFromServer(accessToken string, ticketType TicketType) (ticket ResTicket, err error) {
|
func GetQyWxTicketFromServer(accessToken string, isApp bool) (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
|
var response []byte
|
||||||
|
url := fmt.Sprintf(getQyWxTicketURL, accessToken)
|
||||||
|
if isApp {
|
||||||
|
url = fmt.Sprintf(getQyAppTicketURL, accessToken)
|
||||||
|
}
|
||||||
response, err = util.HTTPGet(url)
|
response, err = util.HTTPGet(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ const (
|
|||||||
getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s"
|
getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s"
|
||||||
// 访问页面
|
// 访问页面
|
||||||
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?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 数据分析
|
// Analysis analyis 数据分析
|
||||||
@@ -317,67 +315,3 @@ func (analysis *Analysis) GetAnalysisVisitPage(beginDate, endDate string) (resul
|
|||||||
}
|
}
|
||||||
return
|
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,22 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// code2SessionURL 小程序登录
|
|
||||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
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"
|
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"
|
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 登录/用户信息
|
// Auth 登录/用户信息
|
||||||
@@ -76,45 +65,9 @@ func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (resu
|
|||||||
return
|
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,无需用户授权
|
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPaidUnionid.html
|
func (auth *Auth) GetPaidUnionID() {
|
||||||
func (auth *Auth) GetPaidUnionID(req *GetPaidUnionIDRequest) (string, error) {
|
// TODO
|
||||||
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天生成的加密数据
|
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||||
@@ -128,7 +81,7 @@ func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHa
|
|||||||
var (
|
var (
|
||||||
at string
|
at string
|
||||||
)
|
)
|
||||||
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
|
if at, err = auth.GetAccessToken(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +120,7 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
|||||||
at string
|
at string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
|
if at, err = auth.GetAccessToken(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
@@ -193,115 +146,3 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
|||||||
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
||||||
return auth.GetPhoneNumberContext(context2.Background(), code)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package business
|
package business
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
@@ -29,18 +28,13 @@ type PhoneInfo struct {
|
|||||||
|
|
||||||
// GetPhoneNumber code换取用户手机号。 每个code只能使用一次,code的有效期为5min
|
// GetPhoneNumber code换取用户手机号。 每个code只能使用一次,code的有效期为5min
|
||||||
func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneInfo, err error) {
|
func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneInfo, err error) {
|
||||||
return business.GetPhoneNumberWithContext(context.Background(), in)
|
accessToken, err := business.GetAccessToken()
|
||||||
}
|
|
||||||
|
|
||||||
// GetPhoneNumberWithContext 利用context将code换取用户手机号。 每个code只能使用一次,code的有效期为5min
|
|
||||||
func (business *Business) GetPhoneNumberWithContext(ctx context.Context, in *GetPhoneNumberRequest) (info PhoneInfo, err error) {
|
|
||||||
accessToken, err := business.GetAccessTokenContext(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uri := fmt.Sprintf(getPhoneNumberURL, accessToken)
|
uri := fmt.Sprintf(getPhoneNumberURL, accessToken)
|
||||||
response, err := util.PostJSONContext(ctx, uri, in)
|
response, err := util.PostJSON(uri, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ type Config struct {
|
|||||||
Token string `json:"token"` // token
|
Token string `json:"token"` // token
|
||||||
EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey
|
EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
UseStableAK bool // use the stable access_token
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ import (
|
|||||||
// Context struct
|
// Context struct
|
||||||
type Context struct {
|
type Context struct {
|
||||||
*config.Config
|
*config.Config
|
||||||
credential.AccessTokenContextHandle
|
credential.AccessTokenHandle
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Encryptor struct
|
// Encryptor struct
|
||||||
@@ -108,13 +108,23 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt 解密数据
|
// Decrypt 解密数据
|
||||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) {
|
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, appid string) (*PlainData, error) {
|
||||||
|
ivB := make([]byte, 16)
|
||||||
|
iv := base64.StdEncoding.EncodeToString(ivB)
|
||||||
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
length := string(cipherText[:20])
|
||||||
|
|
||||||
|
cipherTextData := strings.TrimPrefix(string(cipherText), string(cipherText[:20]))
|
||||||
|
cipherTextData = strings.TrimSuffix(cipherTextData, appid)
|
||||||
|
|
||||||
|
if len(length) != len(cipherTextData) {
|
||||||
|
return nil, fmt.Errorf("length not match, %d != %d", length, len(cipherTextData))
|
||||||
|
}
|
||||||
var plainData PlainData
|
var plainData PlainData
|
||||||
err = json.Unmarshal(cipherText, &plainData)
|
err = json.Unmarshal([]byte(cipherTextData), &plainData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,295 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
||||||
@@ -1,618 +0,0 @@
|
|||||||
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 {
|
type SubscribeMsgSentList struct {
|
||||||
TemplateID string `xml:"TemplateId" json:"TemplateId"`
|
TemplateID string `xml:"TemplateId" json:"TemplateId"`
|
||||||
MsgID string `xml:"MsgID" json:"MsgID"`
|
MsgID string `xml:"MsgID" json:"MsgID"`
|
||||||
ErrorCode string `xml:"ErrorCode" json:"ErrorCode"`
|
ErrorCode int `xml:"ErrorCode" json:"ErrorCode"`
|
||||||
ErrorStatus string `xml:"ErrorStatus" json:"ErrorStatus"`
|
ErrorStatus string `xml:"ErrorStatus" json:"ErrorStatus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// createActivityIDURL 创建activity_id
|
// createActivityURL 创建activity_id
|
||||||
createActivityIDURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s&unionid=%s&openid=%s"
|
createActivityURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s"
|
||||||
// SendUpdatableMsgURL 修改动态消息
|
// SendUpdatableMsgURL 修改动态消息
|
||||||
setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s"
|
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 动态消息状态
|
// UpdatableTargetState 动态消息状态
|
||||||
@@ -40,26 +38,15 @@ func NewUpdatableMessage(ctx *context.Context) *UpdatableMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateActivityIDRequest 创建activity_id请求
|
|
||||||
type CreateActivityIDRequest struct {
|
|
||||||
UnionID string
|
|
||||||
OpenID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateActivityID 创建activity_id
|
// CreateActivityID 创建activity_id
|
||||||
func (updatableMessage *UpdatableMessage) CreateActivityID() (CreateActivityIDResponse, error) {
|
func (updatableMessage *UpdatableMessage) CreateActivityID() (res CreateActivityIDResponse, err 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()
|
accessToken, err := updatableMessage.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf(createActivityIDURL, accessToken, req.UnionID, req.OpenID)
|
|
||||||
response, err := util.HTTPGet(url)
|
uri := fmt.Sprintf(createActivityURL, accessToken)
|
||||||
|
response, err := util.HTTPGet(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -113,35 +100,3 @@ type SendUpdatableMsgReq struct {
|
|||||||
TemplateInfo UpdatableMsgTemplate `json:"template_info"`
|
TemplateInfo UpdatableMsgTemplate `json:"template_info"`
|
||||||
TargetState UpdatableTargetState `json:"target_state"`
|
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,11 +10,8 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/miniprogram/content"
|
"github.com/silenceper/wechat/v2/miniprogram/content"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
"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/message"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/minidrama"
|
"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/order"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
||||||
@@ -37,30 +34,17 @@ type MiniProgram struct {
|
|||||||
|
|
||||||
// NewMiniProgram 实例化小程序 API
|
// NewMiniProgram 实例化小程序 API
|
||||||
func NewMiniProgram(cfg *config.Config) *MiniProgram {
|
func NewMiniProgram(cfg *config.Config) *MiniProgram {
|
||||||
var defaultAkHandle credential.AccessTokenContextHandle
|
defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyMiniProgramPrefix, cfg.Cache)
|
||||||
const cacheKeyPrefix = credential.CacheKeyMiniProgramPrefix
|
|
||||||
if cfg.UseStableAK {
|
|
||||||
defaultAkHandle = credential.NewStableAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache)
|
|
||||||
} else {
|
|
||||||
defaultAkHandle = credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache)
|
|
||||||
}
|
|
||||||
ctx := &context.Context{
|
ctx := &context.Context{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
AccessTokenContextHandle: defaultAkHandle,
|
AccessTokenHandle: defaultAkHandle,
|
||||||
}
|
}
|
||||||
return &MiniProgram{ctx}
|
return &MiniProgram{ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAccessTokenHandle 自定义 access_token 获取方式
|
// SetAccessTokenHandle 自定义 access_token 获取方式
|
||||||
func (miniProgram *MiniProgram) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
|
func (miniProgram *MiniProgram) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
|
||||||
miniProgram.ctx.AccessTokenContextHandle = credential.AccessTokenCompatibleHandle{
|
miniProgram.ctx.AccessTokenHandle = accessTokenHandle
|
||||||
AccessTokenHandle: accessTokenHandle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAccessTokenContextHandle 自定义 access_token 获取方式
|
|
||||||
func (miniProgram *MiniProgram) SetAccessTokenContextHandle(accessTokenContextHandle credential.AccessTokenContextHandle) {
|
|
||||||
miniProgram.ctx.AccessTokenContextHandle = accessTokenContextHandle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContext get Context
|
// GetContext get Context
|
||||||
@@ -182,23 +166,3 @@ func (miniProgram *MiniProgram) GetRedPacketCover() *redpacketcover.RedPacketCov
|
|||||||
func (miniProgram *MiniProgram) GetUpdatableMessage() *message.UpdatableMessage {
|
func (miniProgram *MiniProgram) GetUpdatableMessage() *message.UpdatableMessage {
|
||||||
return message.NewUpdatableMessage(miniProgram.ctx)
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context2 "context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -65,12 +64,7 @@ type MediaCheckAsyncRequest struct {
|
|||||||
|
|
||||||
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
|
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
|
||||||
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
|
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
|
||||||
return security.MediaCheckAsyncContext(context2.Background(), in)
|
accessToken, err := security.GetAccessToken()
|
||||||
}
|
|
||||||
|
|
||||||
// MediaCheckAsyncContext 异步校验图片/音频是否含有违法违规内容
|
|
||||||
func (security *Security) MediaCheckAsyncContext(ctx context2.Context, in *MediaCheckAsyncRequest) (traceID string, err error) {
|
|
||||||
accessToken, err := security.GetAccessTokenContext(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,7 +77,7 @@ func (security *Security) MediaCheckAsyncContext(ctx context2.Context, in *Media
|
|||||||
req.Version = 2
|
req.Version = 2
|
||||||
|
|
||||||
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
||||||
response, err := util.PostJSONContext(ctx, uri, req)
|
response, err := util.PostJSON(uri, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -228,12 +222,7 @@ func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err
|
|||||||
|
|
||||||
// MsgCheck 检查一段文本是否含有违法违规内容
|
// MsgCheck 检查一段文本是否含有违法违规内容
|
||||||
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
||||||
return security.MsgCheckContext(context2.Background(), in)
|
accessToken, err := security.GetAccessToken()
|
||||||
}
|
|
||||||
|
|
||||||
// MsgCheckContext 检查一段文本是否含有违法违规内容
|
|
||||||
func (security *Security) MsgCheckContext(ctx context2.Context, in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
|
||||||
accessToken, err := security.GetAccessTokenContext(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -246,7 +235,7 @@ func (security *Security) MsgCheckContext(ctx context2.Context, in *MsgCheckRequ
|
|||||||
req.Version = 2
|
req.Version = 2
|
||||||
|
|
||||||
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
||||||
response, err := util.PostJSONContext(ctx, uri, req)
|
response, err := util.PostJSON(uri, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package subscribe
|
package subscribe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
@@ -12,30 +11,22 @@ const (
|
|||||||
// 发送订阅消息
|
// 发送订阅消息
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
||||||
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
|
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
|
// 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"
|
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
|
// 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"
|
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
|
// 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"
|
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
|
// 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"
|
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 订阅消息
|
// Subscribe 订阅消息
|
||||||
@@ -66,18 +57,11 @@ type DataItem struct {
|
|||||||
|
|
||||||
// TemplateItem template item
|
// TemplateItem template item
|
||||||
type TemplateItem struct {
|
type TemplateItem struct {
|
||||||
PriTmplID string `json:"priTmplId"`
|
PriTmplID string `json:"priTmplId"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Example string `json:"example"`
|
Example string `json:"example"`
|
||||||
Type int64 `json:"type"`
|
Type int64 `json:"type"`
|
||||||
KeywordEnumValueList []KeywordEnumValue `json:"keywordEnumValueList"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeywordEnumValue 枚举参数值范围
|
|
||||||
type KeywordEnumValue struct {
|
|
||||||
EnumValueList []string `json:"enumValueList"`
|
|
||||||
KeywordCode string `json:"keywordCode"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateList template list
|
// TemplateList template list
|
||||||
@@ -86,13 +70,6 @@ type TemplateList struct {
|
|||||||
Data []TemplateItem `json:"data"`
|
Data []TemplateItem `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// resTemplateSend 发送获取 msg id
|
|
||||||
type resTemplateSend struct {
|
|
||||||
util.CommonError
|
|
||||||
|
|
||||||
MsgID int64 `json:"msgid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send 发送订阅消息
|
// Send 发送订阅消息
|
||||||
func (s *Subscribe) Send(msg *Message) (err error) {
|
func (s *Subscribe) Send(msg *Message) (err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
@@ -108,33 +85,6 @@ func (s *Subscribe) Send(msg *Message) (err error) {
|
|||||||
return util.DecodeWithCommonError(response, "Send")
|
return util.DecodeWithCommonError(response, "Send")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendGetMsgID 发送订阅消息返回 msgid
|
|
||||||
func (s *Subscribe) SendGetMsgID(msg *Message) (msgID int64, err error) {
|
|
||||||
var accessToken string
|
|
||||||
accessToken, err = s.GetAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uri := fmt.Sprintf("%s?access_token=%s", subscribeSendURL, accessToken)
|
|
||||||
response, err := util.PostJSON(uri, msg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var result resTemplateSend
|
|
||||||
if err = json.Unmarshal(response, &result); 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTemplates 获取当前帐号下的个人模板列表
|
// ListTemplates 获取当前帐号下的个人模板列表
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
||||||
func (s *Subscribe) ListTemplates() (*TemplateList, error) {
|
func (s *Subscribe) ListTemplates() (*TemplateList, error) {
|
||||||
@@ -239,200 +189,3 @@ func (s *Subscribe) Delete(templateID string) (err error) {
|
|||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
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,13 +6,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink?access_token=%s"
|
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink"
|
||||||
|
|
||||||
// ULQueryRequest 查询加密URLLink请求
|
|
||||||
type ULQueryRequest struct {
|
|
||||||
URLLink string `json:"url_link"`
|
|
||||||
QueryType int `json:"query_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ULQueryResult 返回的结果
|
// ULQueryResult 返回的结果
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
|
||||||
@@ -34,35 +28,25 @@ type ULQueryResult struct {
|
|||||||
ResourceAppid string `json:"resource_appid"`
|
ResourceAppid string `json:"resource_appid"`
|
||||||
} `json:"cloud_base"`
|
} `json:"cloud_base"`
|
||||||
} `json:"url_link_info"`
|
} `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 配置。
|
// Query 查询小程序 url_link 配置。
|
||||||
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
|
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
|
||||||
return u.QueryWithType(&ULQueryRequest{URLLink: urlLink})
|
accessToken, err := u.GetAccessToken()
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// QueryWithType 查询加密URLLink
|
uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken)
|
||||||
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/queryUrlLink.html
|
response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink})
|
||||||
func (u *URLLink) QueryWithType(req *ULQueryRequest) (*ULQueryResult, error) {
|
if err != nil {
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = u.GetAccessToken(); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var response []byte
|
var resp ULQueryResult
|
||||||
if response, err = util.PostJSON(fmt.Sprintf(queryURL, accessToken), req); err != nil {
|
err = util.DecodeWithError(response, &resp, "URLLink.Query")
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := &ULQueryResult{}
|
return &resp, nil
|
||||||
err = util.DecodeWithError(response, result, "URLLink.Query")
|
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ const (
|
|||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||||
type QueryScheme struct {
|
type QueryScheme struct {
|
||||||
// 小程序 scheme 码
|
// 小程序 scheme 码
|
||||||
Scheme string `json:"scheme"`
|
Scheme string `json:"scheme"`
|
||||||
QueryType int `json:"query_type"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchemeInfo scheme 配置
|
// SchemeInfo scheme 配置
|
||||||
@@ -34,47 +33,34 @@ type SchemeInfo struct {
|
|||||||
EnvVersion EnvVersion `json:"env_version"`
|
EnvVersion EnvVersion `json:"env_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuotaInfo quota 配置
|
// resQueryScheme 返回结构体
|
||||||
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#参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||||
type ResQueryScheme struct {
|
type resQueryScheme struct {
|
||||||
// 通用错误
|
// 通用错误
|
||||||
util.CommonError
|
util.CommonError
|
||||||
// scheme 配置
|
// scheme 配置
|
||||||
SchemeInfo SchemeInfo `json:"scheme_info"`
|
SchemeInfo SchemeInfo `json:"scheme_info"`
|
||||||
// 访问该链接的openid,没有用户访问过则为空字符串
|
// 访问该链接的openid,没有用户访问过则为空字符串
|
||||||
VisitOpenid string `json:"visit_openid"`
|
VisitOpenid string `json:"visit_openid"`
|
||||||
QuotaInfo QuotaInfo `json:"quota_info"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryScheme 查询小程序 scheme 码
|
// QueryScheme 查询小程序 scheme 码
|
||||||
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
|
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
|
||||||
res, err := u.QuerySchemeWithRes(querySchemeParams)
|
var accessToken string
|
||||||
|
accessToken, err = u.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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
|
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,12 +17,7 @@ func NewURLScheme(ctx *context.Context) *URLScheme {
|
|||||||
return &URLScheme{Context: ctx}
|
return &URLScheme{Context: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
|
||||||
// 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 失效类型 (指定时间戳/指定间隔)
|
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
||||||
type TExpireType int
|
type TExpireType int
|
||||||
@@ -55,13 +50,10 @@ type JumpWxa struct {
|
|||||||
// USParams 请求参数
|
// USParams 请求参数
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
|
||||||
type USParams struct {
|
type USParams struct {
|
||||||
JumpWxa *JumpWxa `json:"jump_wxa,omitempty"`
|
JumpWxa *JumpWxa `json:"jump_wxa"`
|
||||||
ExpireType TExpireType `json:"expire_type,omitempty"`
|
ExpireType TExpireType `json:"expire_type"`
|
||||||
ExpireTime int64 `json:"expire_time,omitempty"`
|
ExpireTime int64 `json:"expire_time"`
|
||||||
ExpireInterval int `json:"expire_interval,omitempty"`
|
ExpireInterval int `json:"expire_interval"`
|
||||||
IsExpire bool `json:"is_expire,omitempty"`
|
|
||||||
ModelID string `json:"model_id,omitempty"`
|
|
||||||
Sn string `json:"sn,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// USResult 返回的结果
|
// USResult 返回的结果
|
||||||
@@ -88,22 +80,3 @@ func (u *URLScheme) Generate(params *USParams) (string, error) {
|
|||||||
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
||||||
return resp.OpenLink, err
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ type Config struct {
|
|||||||
Token string `json:"token"` // token
|
Token string `json:"token"` // token
|
||||||
EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey
|
EncodingAESKey string `json:"encoding_aes_key"` // EncodingAESKey
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
UseStableAK bool // use the stable access_token
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package js
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context2 "context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -40,31 +39,14 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
|||||||
// GetConfig 获取jssdk需要的配置参数
|
// GetConfig 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
// uri 为当前网页地址
|
||||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||||
return js.GetConfigContext(context2.Background(), uri)
|
config = new(Config)
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
|
||||||
func (js *Js) GetConfigContext(ctx context2.Context, uri string) (config *Config, err error) {
|
|
||||||
var accessToken string
|
var accessToken string
|
||||||
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
accessToken, err = js.GetAccessToken()
|
||||||
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketStr string
|
var ticketStr string
|
||||||
// 类型断言 jsTicket
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,7 +56,6 @@ func (js *Js) GetConfigContext(ctx context2.Context, uri string) (config *Config
|
|||||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
sigStr := util.Signature(str)
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
config = new(Config)
|
|
||||||
config.AppID = js.AppID
|
config.AppID = js.AppID
|
||||||
config.NonceStr = nonceStr
|
config.NonceStr = nonceStr
|
||||||
config.Timestamp = timestamp
|
config.Timestamp = timestamp
|
||||||
|
|||||||
@@ -49,13 +49,7 @@ type OfficialAccount struct {
|
|||||||
|
|
||||||
// NewOfficialAccount 实例化公众号API
|
// NewOfficialAccount 实例化公众号API
|
||||||
func NewOfficialAccount(cfg *config.Config) *OfficialAccount {
|
func NewOfficialAccount(cfg *config.Config) *OfficialAccount {
|
||||||
var defaultAkHandle credential.AccessTokenContextHandle
|
defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyOfficialAccountPrefix, cfg.Cache)
|
||||||
const cacheKeyPrefix = credential.CacheKeyOfficialAccountPrefix
|
|
||||||
if cfg.UseStableAK {
|
|
||||||
defaultAkHandle = credential.NewStableAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache)
|
|
||||||
} else {
|
|
||||||
defaultAkHandle = credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, cacheKeyPrefix, cfg.Cache)
|
|
||||||
}
|
|
||||||
ctx := &context.Context{
|
ctx := &context.Context{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
AccessTokenHandle: defaultAkHandle,
|
AccessTokenHandle: defaultAkHandle,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package account
|
|||||||
|
|
||||||
import "github.com/silenceper/wechat/v2/openplatform/context"
|
import "github.com/silenceper/wechat/v2/openplatform/context"
|
||||||
|
|
||||||
// Account 开放平台帐号管理
|
// Account 开放平台张哈管理
|
||||||
// TODO 实现方法
|
// TODO 实现方法
|
||||||
type Account struct {
|
type Account struct {
|
||||||
*context.Context
|
*context.Context
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const (
|
|||||||
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
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"
|
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"
|
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 获取授权方选项信息
|
// TODO 获取授权方选项信息
|
||||||
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||||
// TODO 获取已授权的账号信息
|
// TODO 获取已授权的账号信息
|
||||||
@@ -138,20 +137,6 @@ func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizApp
|
|||||||
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
|
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 微信返回接口中各种类型字段
|
// ID 微信返回接口中各种类型字段
|
||||||
type ID struct {
|
type ID struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
@@ -240,10 +225,6 @@ 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 {
|
if err := cache.SetContext(stdCtx, ctx.Cache, authrTokenKey, ret.AccessToken, time.Second*time.Duration(ret.ExpiresIn-30)); err != nil {
|
||||||
return nil, err
|
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
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,18 +238,8 @@ func (ctx *Context) GetAuthrAccessTokenContext(stdCtx context.Context, appid str
|
|||||||
authrTokenKey := "authorizer_access_token_" + appid
|
authrTokenKey := "authorizer_access_token_" + appid
|
||||||
val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey)
|
val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey)
|
||||||
if val == nil {
|
if val == nil {
|
||||||
refreshTokenKey := "authorizer_refresh_token_" + appid
|
return "", fmt.Errorf("cannot get authorizer %s access 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
|
return val.(string), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package miniprogram
|
package miniprogram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
originalContext "context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -38,22 +37,6 @@ func (miniProgram *MiniProgram) GetAccessToken() (string, error) {
|
|||||||
return akRes.AccessToken, nil
|
return akRes.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessTokenContext 利用ctx获取ak
|
|
||||||
func (miniProgram *MiniProgram) GetAccessTokenContext(ctx originalContext.Context) (string, error) {
|
|
||||||
ak, akErr := miniProgram.openContext.GetAuthrAccessTokenContext(ctx, miniProgram.AppID)
|
|
||||||
if akErr == nil {
|
|
||||||
return ak, nil
|
|
||||||
}
|
|
||||||
if miniProgram.authorizerRefreshToken == "" {
|
|
||||||
return "", fmt.Errorf("please set the authorizer_refresh_token first")
|
|
||||||
}
|
|
||||||
akRes, akResErr := miniProgram.GetComponent().RefreshAuthrTokenContext(ctx, miniProgram.AppID, miniProgram.authorizerRefreshToken)
|
|
||||||
if akResErr != nil {
|
|
||||||
return "", akResErr
|
|
||||||
}
|
|
||||||
return akRes.AccessToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuthorizerRefreshToken 设置代执操作业务授权账号authorizer_refresh_token
|
// SetAuthorizerRefreshToken 设置代执操作业务授权账号authorizer_refresh_token
|
||||||
func (miniProgram *MiniProgram) SetAuthorizerRefreshToken(authorizerRefreshToken string) *MiniProgram {
|
func (miniProgram *MiniProgram) SetAuthorizerRefreshToken(authorizerRefreshToken string) *MiniProgram {
|
||||||
miniProgram.authorizerRefreshToken = authorizerRefreshToken
|
miniProgram.authorizerRefreshToken = authorizerRefreshToken
|
||||||
@@ -85,7 +68,7 @@ func (miniProgram *MiniProgram) GetBasic() *basic.Basic {
|
|||||||
// GetURLLink 小程序URL Link接口 调用前需确认已调用 SetAuthorizerRefreshToken 避免由于缓存中 authorizer_access_token 过期执行中断
|
// GetURLLink 小程序URL Link接口 调用前需确认已调用 SetAuthorizerRefreshToken 避免由于缓存中 authorizer_access_token 过期执行中断
|
||||||
func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink {
|
func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink {
|
||||||
return urllink.NewURLLink(&miniContext.Context{
|
return urllink.NewURLLink(&miniContext.Context{
|
||||||
AccessTokenContextHandle: miniProgram,
|
AccessTokenHandle: miniProgram,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package js
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context2 "context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -33,31 +32,14 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
|||||||
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
// uri 为当前网页地址
|
||||||
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
||||||
return js.GetConfigContext(context2.Background(), uri, appid)
|
config = new(officialJs.Config)
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
|
||||||
func (js *Js) GetConfigContext(ctx context2.Context, uri, appid string) (config *officialJs.Config, err error) {
|
|
||||||
var accessToken string
|
var accessToken string
|
||||||
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
accessToken, err = js.GetAccessToken()
|
||||||
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketStr string
|
var ticketStr string
|
||||||
// 类型断言 jsTicket
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,7 +49,6 @@ func (js *Js) GetConfigContext(ctx context2.Context, uri, appid string) (config
|
|||||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
sigStr := util.Signature(str)
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
config = new(officialJs.Config)
|
|
||||||
config.AppID = appid
|
config.AppID = appid
|
||||||
config.NonceStr = nonceStr
|
config.NonceStr = nonceStr
|
||||||
config.Timestamp = timestamp
|
config.Timestamp = timestamp
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
// 验证 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,18 +68,3 @@ func DecodeWithError(response []byte, obj interface{}, apiName string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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,7 +166,6 @@ func PostFile(fieldName, filePath, uri string) ([]byte, error) {
|
|||||||
IsFile: true,
|
IsFile: true,
|
||||||
Fieldname: fieldName,
|
Fieldname: fieldName,
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
Filename: filePath,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return PostMultipartForm(fields, uri)
|
return PostMultipartForm(fields, uri)
|
||||||
@@ -291,20 +290,7 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
|
|||||||
config := &tls.Config{
|
config := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
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.TLSClientConfig = config
|
||||||
trans.DisableCompression = true
|
trans.DisableCompression = true
|
||||||
client = &http.Client{Transport: trans}
|
client = &http.Client{Transport: trans}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
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,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,3 +17,16 @@ func Signature(params ...string) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CalSignature(params ...string) string {
|
||||||
|
sort.Strings(params)
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, value := range params {
|
||||||
|
buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := sha1.New()
|
||||||
|
sha.Write(buffer.Bytes())
|
||||||
|
signature := fmt.Sprintf("%x", sha.Sum(nil))
|
||||||
|
return string(signature)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,16 +9,12 @@ import (
|
|||||||
const (
|
const (
|
||||||
// departmentCreateURL 创建部门
|
// departmentCreateURL 创建部门
|
||||||
departmentCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s"
|
departmentCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s"
|
||||||
// departmentUpdateURL 更新部门
|
|
||||||
departmentUpdateURL = "https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=%s"
|
|
||||||
// departmentDeleteURL 删除部门
|
|
||||||
departmentDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/department/delete?access_token=%s&id=%d"
|
|
||||||
// departmentSimpleListURL 获取子部门ID列表
|
// departmentSimpleListURL 获取子部门ID列表
|
||||||
departmentSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=%s&id=%d"
|
departmentSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=%s&id=%d"
|
||||||
// departmentListURL 获取部门列表
|
// departmentListURL 获取部门列表
|
||||||
departmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s"
|
departmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s"
|
||||||
departmentListByIDURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%d"
|
departmentListByIDURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%d"
|
||||||
// departmentGetURL 获取单个部门详情
|
// departmentGetURL 获取单个部门详情 https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=ACCESS_TOKEN&id=ID
|
||||||
departmentGetURL = "https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=%s&id=%d"
|
departmentGetURL = "https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=%s&id=%d"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,49 +85,6 @@ func (r *Client) DepartmentCreate(req *DepartmentCreateRequest) (*DepartmentCrea
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DepartmentUpdateRequest 更新部门请求
|
|
||||||
type DepartmentUpdateRequest struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
NameEn string `json:"name_en,omitempty"`
|
|
||||||
ParentID int `json:"parentid,omitempty"`
|
|
||||||
Order int `json:"order,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DepartmentUpdate 更新部门
|
|
||||||
// see https://developer.work.weixin.qq.com/document/path/90206
|
|
||||||
func (r *Client) DepartmentUpdate(req *DepartmentUpdateRequest) 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(departmentUpdateURL, accessToken), req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return util.DecodeWithCommonError(response, "DepartmentUpdate")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DepartmentDelete 删除部门
|
|
||||||
// @see https://developer.work.weixin.qq.com/document/path/90207
|
|
||||||
func (r *Client) DepartmentDelete(departmentID int) error {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.HTTPGet(fmt.Sprintf(departmentDeleteURL, accessToken, departmentID)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return util.DecodeWithCommonError(response, "DepartmentDelete")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DepartmentSimpleList 获取子部门ID列表
|
// DepartmentSimpleList 获取子部门ID列表
|
||||||
// see https://developer.work.weixin.qq.com/document/path/95350
|
// see https://developer.work.weixin.qq.com/document/path/95350
|
||||||
func (r *Client) DepartmentSimpleList(departmentID int) ([]*DepartmentID, error) {
|
func (r *Client) DepartmentSimpleList(departmentID int) ([]*DepartmentID, error) {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ const (
|
|||||||
userSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist"
|
userSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist"
|
||||||
// userCreateURL 创建成员
|
// userCreateURL 创建成员
|
||||||
userCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s"
|
userCreateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s"
|
||||||
// userUpdateURL 更新成员
|
|
||||||
userUpdateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/update?access_token=%s"
|
|
||||||
// userGetURL 读取成员
|
// userGetURL 读取成员
|
||||||
userGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
userGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
||||||
// userDeleteURL 删除成员
|
// userDeleteURL 删除成员
|
||||||
@@ -24,18 +22,6 @@ const (
|
|||||||
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
||||||
// convertToUserIDURL openID转userID
|
// convertToUserIDURL openID转userID
|
||||||
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
||||||
// userBatchDeleteURL 批量删除成员
|
|
||||||
userBatchDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete?access_token=%s"
|
|
||||||
// userAuthSuccURL 登录二次验证
|
|
||||||
userAuthSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=%s&userid=%s"
|
|
||||||
// batchInviteURL 邀请成员
|
|
||||||
batchInviteURL = "https://qyapi.weixin.qq.com/cgi-bin/batch/invite?access_token=%s"
|
|
||||||
// getJoinQrcodeURL 获取加入企业二维码
|
|
||||||
getJoinQrcodeURL = "https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode"
|
|
||||||
// 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 (
|
type (
|
||||||
@@ -168,54 +154,6 @@ func (r *Client) UserCreate(req *UserCreateRequest) (*UserCreateResponse, error)
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"`
|
|
||||||
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"`
|
|
||||||
AvatarMediaid string `json:"avatar_mediaid"`
|
|
||||||
Telephone string `json:"telephone"`
|
|
||||||
Address string `json:"address"`
|
|
||||||
MainDepartment int `json:"main_department"`
|
|
||||||
Extattr struct {
|
|
||||||
Attrs []ExtraAttr `json:"attrs"`
|
|
||||||
} `json:"extattr"`
|
|
||||||
ToInvite bool `json:"to_invite"`
|
|
||||||
ExternalPosition string `json:"external_position"`
|
|
||||||
ExternalProfile ExternalProfile `json:"external_profile"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserUpdate 更新成员
|
|
||||||
// see https://developer.work.weixin.qq.com/document/path/90197
|
|
||||||
func (r *Client) UserUpdate(req *UserUpdateRequest) 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(userUpdateURL, accessToken), req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return util.DecodeWithCommonError(response, "UserUpdate")
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserGetResponse 获取部门成员响应
|
// UserGetResponse 获取部门成员响应
|
||||||
type UserGetResponse struct {
|
type UserGetResponse struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
@@ -459,167 +397,3 @@ func (r *Client) ConvertToUserID(openID string) (string, error) {
|
|||||||
err = util.DecodeWithError(response, result, "ConvertToUserID")
|
err = util.DecodeWithError(response, result, "ConvertToUserID")
|
||||||
return result.UserID, err
|
return result.UserID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserBatchDeleteRequest 批量删除成员请求
|
|
||||||
type UserBatchDeleteRequest struct {
|
|
||||||
UseridList []string `json:"useridlist"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserBatchDelete 批量删除成员
|
|
||||||
// see https://developer.work.weixin.qq.com/document/path/90199
|
|
||||||
func (r *Client) UserBatchDelete(req *UserBatchDeleteRequest) error {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.PostJSON(fmt.Sprintf(userBatchDeleteURL, accessToken), req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return util.DecodeWithCommonError(response, "UserBatchDelete")
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserAuthSucc 登录二次验证
|
|
||||||
// @see https://developer.work.weixin.qq.com/document/path/90203
|
|
||||||
func (r *Client) UserAuthSucc(userID string) error {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.HTTPGet(fmt.Sprintf(userAuthSuccURL, accessToken, userID)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return util.DecodeWithCommonError(response, "UserAuthSucc")
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchInviteRequest 邀请成员请求
|
|
||||||
type BatchInviteRequest struct {
|
|
||||||
User []string `json:"user"`
|
|
||||||
Party []int `json:"party"`
|
|
||||||
Tag []int `json:"tag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchInviteResponse 邀请成员响应
|
|
||||||
type BatchInviteResponse struct {
|
|
||||||
util.CommonError
|
|
||||||
InvalidUser []string `json:"invaliduser"`
|
|
||||||
InvalidParty []int `json:"invalidparty"`
|
|
||||||
InvalidTag []int `json:"invalidtag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchInvite 邀请成员
|
|
||||||
// see https://developer.work.weixin.qq.com/document/path/90975
|
|
||||||
func (r *Client) BatchInvite(req *BatchInviteRequest) (*BatchInviteResponse, error) {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.PostJSON(fmt.Sprintf(batchInviteURL, accessToken), req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := &BatchInviteResponse{}
|
|
||||||
err = util.DecodeWithError(response, result, "BatchInvite")
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJoinQrcodeRequest 获取加入企业二维码请求
|
|
||||||
type GetJoinQrcodeRequest struct {
|
|
||||||
SizeType int `json:"size_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJoinQrcodeResponse 获取加入企业二维码响应
|
|
||||||
type GetJoinQrcodeResponse struct {
|
|
||||||
util.CommonError
|
|
||||||
JoinQrcode string `json:"join_qrcode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJoinQrcode 获取加入企业二维码
|
|
||||||
// see https://developer.work.weixin.qq.com/document/path/91714
|
|
||||||
func (r *Client) GetJoinQrcode(req *GetJoinQrcodeRequest) (*GetJoinQrcodeResponse, error) {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
apiURL string
|
|
||||||
)
|
|
||||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if req.SizeType > 0 {
|
|
||||||
apiURL = fmt.Sprintf("%s?access_token=%s&size_type=%d", getJoinQrcodeURL, accessToken, req.SizeType)
|
|
||||||
} else {
|
|
||||||
apiURL = fmt.Sprintf("%s?access_token=%s", getJoinQrcodeURL, accessToken)
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.HTTPGet(apiURL); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := &GetJoinQrcodeResponse{}
|
|
||||||
err = util.DecodeWithError(response, result, "GetJoinQrcode")
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,8 +21,6 @@ const (
|
|||||||
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
|
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
|
||||||
// delOptionURL 删除打卡规则
|
// delOptionURL 删除打卡规则
|
||||||
delOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/del_checkin_option?access_token=%s"
|
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 为打卡人员排班请求
|
// SetScheduleListRequest 为打卡人员排班请求
|
||||||
@@ -142,7 +140,6 @@ type OptionGroupRule struct {
|
|||||||
SyncOutCheckin bool `json:"sync_out_checkin,omitempty"`
|
SyncOutCheckin bool `json:"sync_out_checkin,omitempty"`
|
||||||
BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"`
|
BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"`
|
||||||
BukaRestriction int64 `json:"buka_restriction,omitempty"`
|
BukaRestriction int64 `json:"buka_restriction,omitempty"`
|
||||||
CheckinMethodType int64 `json:"checkin_method_type,omitempty"`
|
|
||||||
SpanDayTime int64 `json:"span_day_time,omitempty"`
|
SpanDayTime int64 `json:"span_day_time,omitempty"`
|
||||||
StandardWorkDuration int64 `json:"standard_work_duration,omitempty"`
|
StandardWorkDuration int64 `json:"standard_work_duration,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -158,32 +155,24 @@ type OptionGroupRuleCheckinDate struct {
|
|||||||
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
|
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
|
||||||
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
|
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
|
||||||
LateRule OptionGroupLateRule `json:"late_rule"`
|
LateRule OptionGroupLateRule `json:"late_rule"`
|
||||||
Biweekly OptionGroupBiweekly `json:"biweekly,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息
|
// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息
|
||||||
type OptionGroupRuleCheckinTime struct {
|
type OptionGroupRuleCheckinTime struct {
|
||||||
TimeID int64 `json:"time_id"`
|
TimeID int64 `json:"time_id"`
|
||||||
WorkSec int64 `json:"work_sec"`
|
WorkSec int64 `json:"work_sec"`
|
||||||
OffWorkSec int64 `json:"off_work_sec"`
|
OffWorkSec int64 `json:"off_work_sec"`
|
||||||
RemindWorkSec int64 `json:"remind_work_sec"`
|
RemindWorkSec int64 `json:"remind_work_sec"`
|
||||||
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
|
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
|
||||||
AllowRest bool `json:"allow_rest"`
|
AllowRest bool `json:"allow_rest"`
|
||||||
RestBeginTime int64 `json:"rest_begin_time"`
|
RestBeginTime int64 `json:"rest_begin_time"`
|
||||||
RestEndTime int64 `json:"rest_end_time"`
|
RestEndTime int64 `json:"rest_end_time"`
|
||||||
EarliestWorkSec int64 `json:"earliest_work_sec"`
|
EarliestWorkSec int64 `json:"earliest_work_sec"`
|
||||||
LatestWorkSec int64 `json:"latest_work_sec"`
|
LatestWorkSec int64 `json:"latest_work_sec"`
|
||||||
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
|
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
|
||||||
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
|
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
|
||||||
NoNeedCheckOn bool `json:"no_need_checkon"`
|
NoNeedCheckOn bool `json:"no_need_checkon"`
|
||||||
NoNeedCheckOff bool `json:"no_need_checkoff"`
|
NoNeedCheckOff bool `json:"no_need_checkoff"`
|
||||||
RestTimes []OptionGroupRuleRestTimes `json:"rest_times,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionGroupRuleRestTimes 多组休息时间
|
|
||||||
type OptionGroupRuleRestTimes struct {
|
|
||||||
RestBeginTime int64 `json:"rest_begin_time,omitempty"`
|
|
||||||
RestEndTime int64 `json:"rest_end_time,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionGroupLateRule 晚走晚到时间规则信息
|
// OptionGroupLateRule 晚走晚到时间规则信息
|
||||||
@@ -200,13 +189,6 @@ type OptionGroupTimeRule struct {
|
|||||||
OnWorkFlexTime int64 `json:"onwork_flex_time"`
|
OnWorkFlexTime int64 `json:"onwork_flex_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionGroupBiweekly 大小周规则
|
|
||||||
type OptionGroupBiweekly struct {
|
|
||||||
EnableWeekdayRecurrence bool `json:"enable_weekday_recurrence"`
|
|
||||||
OddWorkdays []int64 `json:"odd_workdays"`
|
|
||||||
EvenWorkdays []int64 `json:"even_workdays"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionGroupSpeWorkdays 特殊工作日
|
// OptionGroupSpeWorkdays 特殊工作日
|
||||||
type OptionGroupSpeWorkdays struct {
|
type OptionGroupSpeWorkdays struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
@@ -403,41 +385,3 @@ func (r *Client) DelOption(req *DelOptionRequest) error {
|
|||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "DelOption")
|
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,15 +173,9 @@ type (
|
|||||||
|
|
||||||
// OtInfo 加班信息
|
// OtInfo 加班信息
|
||||||
OtInfo struct {
|
OtInfo struct {
|
||||||
OtStatus int64 `json:"ot_status"`
|
OtStatus int64 `json:"ot_status"`
|
||||||
OtDuration int64 `json:"ot_duration"`
|
OtDuration int64 `json:"ot_duration"`
|
||||||
ExceptionDuration []uint64 `json:"exception_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"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,20 +237,13 @@ type (
|
|||||||
RegularDays int64 `json:"regular_days"`
|
RegularDays int64 `json:"regular_days"`
|
||||||
RegularWorkSec int64 `json:"regular_work_sec"`
|
RegularWorkSec int64 `json:"regular_work_sec"`
|
||||||
StandardWorkSec int64 `json:"standard_work_sec"`
|
StandardWorkSec int64 `json:"standard_work_sec"`
|
||||||
RestDays int64 `json:"rest_days"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverWorkInfo 加班情况
|
// OverWorkInfo 加班情况
|
||||||
OverWorkInfo struct {
|
OverWorkInfo struct {
|
||||||
WorkdayOverSec int64 `json:"workday_over_sec"`
|
WorkdayOverSec int64 `json:"workday_over_sec"`
|
||||||
HolidayOverSec int64 `json:"holidays_over_sec"`
|
HolidayOverSec int64 `json:"holidays_over_sec"`
|
||||||
RestDayOverSec int64 `json:"restdays_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"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -317,10 +304,6 @@ type CorpOptionGroup struct {
|
|||||||
BukaRestriction int64 `json:"buka_restriction"`
|
BukaRestriction int64 `json:"buka_restriction"`
|
||||||
ScheduleList []ScheduleList `json:"schedulelist"`
|
ScheduleList []ScheduleList `json:"schedulelist"`
|
||||||
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
|
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 打卡时间,当规则类型为排班时没有意义
|
// GroupCheckinDate 打卡时间,当规则类型为排班时没有意义
|
||||||
@@ -522,7 +505,6 @@ type OptionInfo struct {
|
|||||||
type OptionGroup struct {
|
type OptionGroup struct {
|
||||||
GroupType int64 `json:"grouptype"`
|
GroupType int64 `json:"grouptype"`
|
||||||
GroupID int64 `json:"groupid"`
|
GroupID int64 `json:"groupid"`
|
||||||
OpenSpCheckin bool `json:"open_sp_checkin"`
|
|
||||||
GroupName string `json:"groupname"`
|
GroupName string `json:"groupname"`
|
||||||
CheckinDate []OptionCheckinDate `json:"checkindate"`
|
CheckinDate []OptionCheckinDate `json:"checkindate"`
|
||||||
SpeWorkdays []SpeWorkdays `json:"spe_workdays"`
|
SpeWorkdays []SpeWorkdays `json:"spe_workdays"`
|
||||||
@@ -536,10 +518,6 @@ type OptionGroup struct {
|
|||||||
LocInfos []LocInfos `json:"loc_infos"`
|
LocInfos []LocInfos `json:"loc_infos"`
|
||||||
ScheduleList []ScheduleList `json:"schedulelist"`
|
ScheduleList []ScheduleList `json:"schedulelist"`
|
||||||
BukaRestriction int64 `json:"buka_restriction"`
|
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 打卡时间配置
|
// OptionCheckinDate 打卡时间配置
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import (
|
|||||||
|
|
||||||
// Config for 企业微信
|
// Config for 企业微信
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CorpID string `json:"corp_id"` // corp_id
|
CorpID string `json:"corp_id"` // corp_id
|
||||||
CorpSecret string `json:"corp_secret"` // corp_secret,如果需要获取会话存档实例,当前参数请填写聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
|
CorpSecret string `json:"corp_secret"` // corp_secret,如果需要获取会话存档实例,当前参数请填写聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
|
||||||
AgentID string `json:"agent_id"` // agent_id
|
AgentID string `json:"agent_id"` // agent_id
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存
|
RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存
|
||||||
|
|
||||||
Token string `json:"token"` // 微信客服回调配置,用于生成签名校验回调请求的合法性
|
Token string `json:"token"` // 微信客服回调配置,用于生成签名校验回调请求的合法性
|
||||||
EncodingAESKey string `json:"encoding_aes_key"` // 微信客服回调p配置,用于解密回调消息内容对应的密文
|
EncodingAESKey string `json:"encoding_aes_key"` // 微信客服回调p配置,用于解密回调消息内容对应的密文
|
||||||
}
|
}
|
||||||
|
|||||||
90
work/externalcontact/add_msg_template.go
Normal file
90
work/externalcontact/add_msg_template.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package externalcontact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
addMsgTemplateUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatTypeSingle ChatType = "single"
|
||||||
|
ChatTypeGroup ChatType = "group"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReqMessage 企业群发参数
|
||||||
|
type ReqMessage struct {
|
||||||
|
ChatType ChatType `json:"chat_type"` //群发任务的类型,默认为single,表示发送给客户,group表示发送给客户群
|
||||||
|
ExternalUserid []string `json:"external_userid"` // 客户的外部联系人id列表,仅在chat_type为single时有效,不可与sender同时为空,最多可传入1万个客户
|
||||||
|
Sender string `json:"sender"` //发送企业群发消息的成员userid,当类型为发送给客户群时必填
|
||||||
|
Text struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"text"`
|
||||||
|
Attachments []struct {
|
||||||
|
Msgtype string `json:"msgtype"`
|
||||||
|
Image MsgImage `json:"image"`
|
||||||
|
Link MsgLink `json:"link"`
|
||||||
|
Miniprogram MsgMiniprogram `json:"miniprogram"`
|
||||||
|
Video MsgVideo `json:"video"`
|
||||||
|
File MsgFile `json:"file"`
|
||||||
|
} `json:"attachments"`
|
||||||
|
}
|
||||||
|
type MsgImage struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
PicUrl string `json:"pic_url"`
|
||||||
|
}
|
||||||
|
type MsgLink struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Picurl string `json:"picurl"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
type MsgMiniprogram struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
PicMediaId string `json:"pic_media_id"`
|
||||||
|
Appid string `json:"appid"`
|
||||||
|
Page string `json:"page"`
|
||||||
|
}
|
||||||
|
type MsgVideo struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
}
|
||||||
|
type MsgFile struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resTemplateSend struct {
|
||||||
|
util.CommonError
|
||||||
|
FailList string `json:"fail_list"`
|
||||||
|
MsgID int64 `json:"msgid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 发送应用消息
|
||||||
|
func (r *Client) Send(msg *ReqMessage) (msgID int64, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", addMsgTemplateUrl, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resTemplateSend
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgID = result.MsgID
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -17,8 +17,6 @@ const (
|
|||||||
listContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_contact_way?access_token=%s"
|
listContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_contact_way?access_token=%s"
|
||||||
// delContactWayURL 删除企业已配置的「联系我」方式
|
// delContactWayURL 删除企业已配置的「联系我」方式
|
||||||
delContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_contact_way?access_token=%s"
|
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 (
|
type (
|
||||||
@@ -79,8 +77,6 @@ type (
|
|||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ChatExpiresIn int `json:"chat_expires_in"`
|
ChatExpiresIn int `json:"chat_expires_in"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
IsExclusive bool `json:"is_exclusive"`
|
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||||
}
|
}
|
||||||
// AddContactWayResponse 配置客户联系「联系我」方式响应
|
// AddContactWayResponse 配置客户联系「联系我」方式响应
|
||||||
@@ -136,7 +132,6 @@ type (
|
|||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ChatExpiresIn int `json:"chat_expires_in"`
|
ChatExpiresIn int `json:"chat_expires_in"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
Conclusions ConclusionsResponse `json:"conclusions"`
|
Conclusions ConclusionsResponse `json:"conclusions"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -173,7 +168,6 @@ type (
|
|||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ChatExpiresIn int `json:"chat_expires_in"`
|
ChatExpiresIn int `json:"chat_expires_in"`
|
||||||
UnionID string `json:"unionid"`
|
UnionID string `json:"unionid"`
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||||
}
|
}
|
||||||
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
|
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
|
||||||
@@ -269,26 +263,3 @@ func (r *Client) DelContactWay(req *DelContactWayRequest) (*DelContactWayRespons
|
|||||||
err = util.DecodeWithError(response, result, "DelContactWay")
|
err = util.DecodeWithError(response, result, "DelContactWay")
|
||||||
return result, err
|
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,10 +23,6 @@ const (
|
|||||||
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
||||||
// customerAcquisitionStatistic 查询链接使用详情
|
// customerAcquisitionStatistic 查询链接使用详情
|
||||||
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
||||||
// customerAcquisitionGetChatInfo 获取成员多次收消息详情
|
|
||||||
customerAcquisitionGetChatInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get_chat_info?access_token=%s"
|
|
||||||
// customerAcquisitionGetPermitURL 获取客户可建联成员
|
|
||||||
customerAcquisitionGetPermitURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_app/get_permit?access_token=%s"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -70,10 +66,9 @@ type (
|
|||||||
// GetCustomerAcquisitionResponse 获取获客链接详情响应
|
// GetCustomerAcquisitionResponse 获取获客链接详情响应
|
||||||
GetCustomerAcquisitionResponse struct {
|
GetCustomerAcquisitionResponse struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
Link Link `json:"link"`
|
Link Link `json:"link"`
|
||||||
Range CustomerAcquisitionRange `json:"range"`
|
Range CustomerAcquisitionRange `json:"range"`
|
||||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
SkipVerify bool `json:"skip_verify"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
|
||||||
}
|
}
|
||||||
// Link 获客链接
|
// Link 获客链接
|
||||||
Link struct {
|
Link struct {
|
||||||
@@ -81,8 +76,6 @@ type (
|
|||||||
LinkName string `json:"link_name"`
|
LinkName string `json:"link_name"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
CreateTime int64 `json:"create_time"`
|
CreateTime int64 `json:"create_time"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomerAcquisitionRange 该获客链接使用范围
|
// CustomerAcquisitionRange 该获客链接使用范围
|
||||||
@@ -90,12 +83,6 @@ type (
|
|||||||
UserList []string `json:"user_list"`
|
UserList []string `json:"user_list"`
|
||||||
DepartmentList []int64 `json:"department_list"`
|
DepartmentList []int64 `json:"department_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomerPriorityOption 该获客链接的优先选项
|
|
||||||
CustomerPriorityOption struct {
|
|
||||||
PriorityType int `json:"priority_type"`
|
|
||||||
PriorityUseridList []string `json:"priority_userid_list"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCustomerAcquisition 获客助手--获取获客链接详情
|
// GetCustomerAcquisition 获客助手--获取获客链接详情
|
||||||
@@ -120,11 +107,9 @@ func (r *Client) GetCustomerAcquisition(req *GetCustomerAcquisitionRequest) (*Ge
|
|||||||
type (
|
type (
|
||||||
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
|
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
|
||||||
CreateCustomerAcquisitionLinkRequest struct {
|
CreateCustomerAcquisitionLinkRequest struct {
|
||||||
LinkName string `json:"link_name"`
|
LinkName string `json:"link_name"`
|
||||||
Range CustomerAcquisitionRange `json:"range"`
|
Range CustomerAcquisitionRange `json:"range"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
SkipVerify bool `json:"skip_verify"`
|
||||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
}
|
}
|
||||||
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
|
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
|
||||||
CreateCustomerAcquisitionLinkResponse struct {
|
CreateCustomerAcquisitionLinkResponse struct {
|
||||||
@@ -155,12 +140,10 @@ func (r *Client) CreateCustomerAcquisitionLink(req *CreateCustomerAcquisitionLin
|
|||||||
type (
|
type (
|
||||||
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
|
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
|
||||||
UpdateCustomerAcquisitionLinkRequest struct {
|
UpdateCustomerAcquisitionLinkRequest struct {
|
||||||
LinkID string `json:"link_id"`
|
LinkID string `json:"link_id"`
|
||||||
LinkName string `json:"link_name"`
|
LinkName string `json:"link_name"`
|
||||||
Range CustomerAcquisitionRange `json:"range"`
|
Range CustomerAcquisitionRange `json:"range"`
|
||||||
SkipVerify bool `json:"skip_verify"`
|
SkipVerify bool `json:"skip_verify"`
|
||||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
}
|
}
|
||||||
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
|
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
|
||||||
UpdateCustomerAcquisitionLinkResponse struct {
|
UpdateCustomerAcquisitionLinkResponse struct {
|
||||||
@@ -325,69 +308,3 @@ func (r *Client) CustomerAcquisitionStatistic(req *CustomerAcquisitionStatisticR
|
|||||||
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
||||||
return result, err
|
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,7 +176,6 @@ type BatchGetExternalUserDetailsRequest struct {
|
|||||||
type ExternalUserDetailListResponse struct {
|
type ExternalUserDetailListResponse struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
ExternalContactList []ExternalUserForBatch `json:"external_contact_list"`
|
ExternalContactList []ExternalUserForBatch `json:"external_contact_list"`
|
||||||
NextCursor string `json:"next_cursor"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalUserForBatch 批量获取外部联系人客户列表
|
// ExternalUserForBatch 批量获取外部联系人客户列表
|
||||||
@@ -215,23 +214,23 @@ type FollowInfo struct {
|
|||||||
|
|
||||||
// BatchGetExternalUserDetails 批量获取外部联系人详情
|
// BatchGetExternalUserDetails 批量获取外部联系人详情
|
||||||
// @see https://developer.work.weixin.qq.com/document/path/92994
|
// @see https://developer.work.weixin.qq.com/document/path/92994
|
||||||
func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, string, error) {
|
func (r *Client) BatchGetExternalUserDetails(request BatchGetExternalUserDetailsRequest) ([]ExternalUserForBatch, error) {
|
||||||
accessToken, err := r.GetAccessToken()
|
accessToken, err := r.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
var response []byte
|
var response []byte
|
||||||
jsonData, err := json.Marshal(request)
|
jsonData, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", fetchBatchExternalContactUserDetailURL, accessToken), string(jsonData))
|
response, err = util.HTTPPost(fmt.Sprintf("%s?access_token=%v", fetchBatchExternalContactUserDetailURL, accessToken), string(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
var result ExternalUserDetailListResponse
|
var result ExternalUserDetailListResponse
|
||||||
err = util.DecodeWithError(response, &result, "BatchGetExternalUserDetails")
|
err = util.DecodeWithError(response, &result, "BatchGetExternalUserDetails")
|
||||||
return result.ExternalContactList, result.NextCursor, err
|
return result.ExternalContactList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserRemarkRequest 修改客户备注信息请求体
|
// UpdateUserRemarkRequest 修改客户备注信息请求体
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ type (
|
|||||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddJoinWayResponse 添加群配置返回值
|
// AddJoinWayResponse 添加群配置返回值
|
||||||
@@ -66,7 +65,6 @@ type (
|
|||||||
ChatIDList []string `json:"chat_id_list"`
|
ChatIDList []string `json:"chat_id_list"`
|
||||||
QrCode string `json:"qr_code"`
|
QrCode string `json:"qr_code"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
}
|
}
|
||||||
//GetJoinWayResponse 获取群配置的返回值
|
//GetJoinWayResponse 获取群配置的返回值
|
||||||
GetJoinWayResponse struct {
|
GetJoinWayResponse struct {
|
||||||
@@ -105,7 +103,6 @@ type UpdateJoinWayRequest struct {
|
|||||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||||
MarkSource bool `json:"mark_source"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateJoinWay 更新客户群进群方式配置
|
// UpdateJoinWay 更新客户群进群方式配置
|
||||||
|
|||||||
162
work/externalcontact/user.go
Normal file
162
work/externalcontact/user.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package externalcontact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
listUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list"
|
||||||
|
getUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get"
|
||||||
|
getByUserBatchUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReqGetByUser struct {
|
||||||
|
UseridList []string `json:"userid_list"`
|
||||||
|
Cursor string `json:"cursor"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
type OneUser struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalContact ExternalContact `json:"external_contact"`
|
||||||
|
FollowUser []FollowInfo `json:"follow_user"` //注意,仅获取单个客户详情的时候这里返回的是跟进人列表
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
type resUserList struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalContactList []UserInfo `json:"external_contact_list"`
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
type resUserids struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalUserid []string `json:"external_userid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
ExternalContact ExternalContact `json:"external_contact"`
|
||||||
|
FollowInfo FollowInfo `json:"follow_info"` //企业成员客户跟进人信息,可以参考获取客户详情,但标签信息只会返回企业标签和规则组标签的tag_id,个人标签将不再返回
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseridList 获取我的客户列表
|
||||||
|
func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&userid=%s", listUrl, accessToken, myUserid)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resUserids
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
externalUserid = result.ExternalUserid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseridList 获取我的全部客户列表及详情
|
||||||
|
func (tpl *Client) GetQyUserInfoList(qyUserid []string) ([]UserInfo, error) {
|
||||||
|
var userInfoList []UserInfo
|
||||||
|
var req ReqGetByUser
|
||||||
|
req.UseridList = qyUserid
|
||||||
|
req.Limit = 100
|
||||||
|
for {
|
||||||
|
userInfoPage, resCursor, err := tpl.GetUserInfoListByUserIds(req)
|
||||||
|
if err != nil {
|
||||||
|
return userInfoList, err
|
||||||
|
}
|
||||||
|
userInfoList = append(userInfoList, userInfoPage...)
|
||||||
|
if resCursor != "" {
|
||||||
|
req.Cursor = resCursor
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userInfoList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoAndAllFollow 获取客户详情以及全部跟进人
|
||||||
|
func (tpl *Client) GetUserInfoAndAllFollow(userid string) (OneUser, error) {
|
||||||
|
var result, res OneUser
|
||||||
|
var err error
|
||||||
|
var cursor string
|
||||||
|
for {
|
||||||
|
res, err = tpl.GetUserInfo(userid, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result.FollowUser = append(result.FollowUser, res.FollowUser...)
|
||||||
|
result.ExternalContact = res.ExternalContact
|
||||||
|
if res.NextCursor != "" {
|
||||||
|
cursor = res.NextCursor
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取客户详情
|
||||||
|
func (tpl *Client) GetUserInfo(externalUserid string, cursor ...string) (result OneUser, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var page = ""
|
||||||
|
if len(cursor) > 0 {
|
||||||
|
page = cursor[0]
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&external_userid=%s&cursor=%s", getUrl, accessToken, externalUserid, page)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoListByUserId 批量获取客户详情
|
||||||
|
func (tpl *Client) GetUserInfoListByUserIds(req ReqGetByUser) (userList []UserInfo, nextCursor string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", getByUserBatchUrl, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resUserList
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userList = result.ExternalContactList
|
||||||
|
nextCursor = result.NextCursor
|
||||||
|
return
|
||||||
|
}
|
||||||
93
work/js/js.go
Normal file
93
work/js/js.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Js struct
|
||||||
|
type Js struct {
|
||||||
|
*context.Context
|
||||||
|
credential.JsTicketHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 返回给用户jssdk配置信息
|
||||||
|
type Config struct {
|
||||||
|
CorpID string `json:"corp_id"`
|
||||||
|
Agentid string `json:"agentid"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
NonceStr string `json:"nonce_str"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewJs init
|
||||||
|
func NewJs(context *context.Context) *Js {
|
||||||
|
js := new(Js)
|
||||||
|
js.Context = context
|
||||||
|
jsTicketHandle := credential.NewWorkJsTicket(context.CorpID, context.AgentID, credential.CacheKeyWorkPrefix, context.Cache)
|
||||||
|
js.SetJsTicketHandle(jsTicketHandle)
|
||||||
|
return js
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetJsTicketHandle 自定义js ticket取值方式
|
||||||
|
func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
||||||
|
js.JsTicketHandle = ticketHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetConfig 获取jssdk需要的配置参数
|
||||||
|
//uri 为当前网页地址
|
||||||
|
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||||
|
config = new(Config)
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = js.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ticketStr string
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceStr := util.RandomStr(16)
|
||||||
|
timestamp := util.GetCurrTS()
|
||||||
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
|
config.CorpID = js.CorpID
|
||||||
|
config.Agentid = js.AgentID
|
||||||
|
config.NonceStr = nonceStr
|
||||||
|
config.Timestamp = timestamp
|
||||||
|
config.Signature = sigStr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetAgentConfig 获取jssdk需要的配置参数
|
||||||
|
//uri 为当前网页地址
|
||||||
|
func (js *Js) GetAgentConfig(uri string) (config *Config, err error) {
|
||||||
|
config = new(Config)
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = js.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ticketStr string
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceStr := util.RandomStr(16)
|
||||||
|
timestamp := util.GetCurrTS()
|
||||||
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
|
config.CorpID = js.CorpID
|
||||||
|
config.NonceStr = nonceStr
|
||||||
|
config.Timestamp = timestamp
|
||||||
|
config.Signature = sigStr
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,6 @@ type AccountAddSchema struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AccountAdd 添加客服账号
|
// AccountAdd 添加客服账号
|
||||||
// see https://developer.work.weixin.qq.com/document/path/94662
|
|
||||||
func (r *Client) AccountAdd(options AccountAddOptions) (info AccountAddSchema, err error) {
|
func (r *Client) AccountAdd(options AccountAddOptions) (info AccountAddSchema, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -60,7 +59,6 @@ type AccountDelOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AccountDel 删除客服账号
|
// AccountDel 删除客服账号
|
||||||
// see https://developer.work.weixin.qq.com/document/path/94663
|
|
||||||
func (r *Client) AccountDel(options AccountDelOptions) (info util.CommonError, err error) {
|
func (r *Client) AccountDel(options AccountDelOptions) (info util.CommonError, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -88,8 +86,7 @@ type AccountUpdateOptions struct {
|
|||||||
MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
|
MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountUpdate 修改客服账号
|
// AccountUpdate 修复客服账号
|
||||||
// see https://developer.work.weixin.qq.com/document/path/94664
|
|
||||||
func (r *Client) AccountUpdate(options AccountUpdateOptions) (info util.CommonError, err error) {
|
func (r *Client) AccountUpdate(options AccountUpdateOptions) (info util.CommonError, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -112,10 +109,9 @@ func (r *Client) AccountUpdate(options AccountUpdateOptions) (info util.CommonEr
|
|||||||
|
|
||||||
// AccountInfoSchema 客服详情
|
// AccountInfoSchema 客服详情
|
||||||
type AccountInfoSchema struct {
|
type AccountInfoSchema struct {
|
||||||
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
||||||
Name string `json:"name"` // 客服帐号名称
|
Name string `json:"name"` // 客服帐号名称
|
||||||
Avatar string `json:"avatar"` // 客服头像URL
|
Avatar string `json:"avatar"` // 客服头像URL
|
||||||
ManagePrivilege bool `json:"manage_privilege"` // 当前调用接口的应用身份,是否有该客服账号的管理权限(编辑客服账号信息、分配会话和收发消息)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountListSchema 获取客服账号列表响应内容
|
// AccountListSchema 获取客服账号列表响应内容
|
||||||
@@ -145,31 +141,6 @@ func (r *Client) AccountList() (info AccountListSchema, err error) {
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountPagingRequest 分页获取客服账号列表请求
|
|
||||||
type AccountPagingRequest struct {
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountPaging 分页获取客服账号列表
|
|
||||||
// see https://developer.work.weixin.qq.com/document/path/94661
|
|
||||||
func (r *Client) AccountPaging(req *AccountPagingRequest) (*AccountListSchema, error) {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = r.ctx.GetAccessToken(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.PostJSON(fmt.Sprintf(accountListAddr, accessToken), req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := &AccountListSchema{}
|
|
||||||
err = util.DecodeWithError(response, result, "AccountPaging")
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddContactWayOptions 获取客服账号链接
|
// AddContactWayOptions 获取客服账号链接
|
||||||
// 1.若scene非空,返回的客服链接开发者可拼接scene_param=SCENE_PARAM参数使用,用户进入会话事件会将SCENE_PARAM原样返回。其中SCENE_PARAM需要urlencode,且长度不能超过128字节。
|
// 1.若scene非空,返回的客服链接开发者可拼接scene_param=SCENE_PARAM参数使用,用户进入会话事件会将SCENE_PARAM原样返回。其中SCENE_PARAM需要urlencode,且长度不能超过128字节。
|
||||||
// 如 https://work.weixin.qq.com/kf/kfcbf8f8d07ac7215f?enc_scene=ENCGFSDF567DF&scene_param=a%3D1%26b%3D2
|
// 如 https://work.weixin.qq.com/kf/kfcbf8f8d07ac7215f?enc_scene=ENCGFSDF567DF&scene_param=a%3D1%26b%3D2
|
||||||
@@ -187,7 +158,6 @@ type AddContactWaySchema struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddContactWay 获取客服账号链接
|
// AddContactWay 获取客服账号链接
|
||||||
// see https://developer.work.weixin.qq.com/document/path/94665
|
|
||||||
func (r *Client) AddContactWay(options AddContactWayOptions) (info AddContactWaySchema, err error) {
|
func (r *Client) AddContactWay(options AddContactWayOptions) (info AddContactWaySchema, err error) {
|
||||||
var (
|
var (
|
||||||
accessToken string
|
accessToken string
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
|
|
||||||
// SignatureOptions 微信服务器验证参数
|
// SignatureOptions 微信服务器验证参数
|
||||||
type SignatureOptions struct {
|
type SignatureOptions struct {
|
||||||
Signature string `form:"msg_signature" json:"msg_signature"`
|
Signature string `form:"msg_signature"`
|
||||||
TimeStamp string `form:"timestamp" json:"timestamp"`
|
TimeStamp string `form:"timestamp"`
|
||||||
Nonce string `form:"nonce" json:"nonce"`
|
Nonce string `form:"nonce"`
|
||||||
EchoStr string `form:"echostr" json:"echostr"`
|
EchoStr string `form:"echostr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyURL 验证请求参数是否合法并返回解密后的消息内容
|
// VerifyURL 验证请求参数是否合法并返回解密后的消息内容
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func NewClient(cfg *config.Config) (client *Client, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 AccessToken Handle
|
// 初始化 AccessToken Handle
|
||||||
defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, cfg.AgentID, credential.CacheKeyWorkPrefix, cfg.Cache)
|
defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache)
|
||||||
ctx := &context.Context{
|
ctx := &context.Context{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
AccessTokenHandle: defaultAkHandle,
|
AccessTokenHandle: defaultAkHandle,
|
||||||
|
|||||||
@@ -59,30 +59,6 @@ func (r *Client) UploadImg(filename string) (*UploadImgResponse, error) {
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadImgFromReader 从 io.Reader 上传图片
|
|
||||||
// @see https://developer.work.weixin.qq.com/document/path/90256
|
|
||||||
func (r *Client) UploadImgFromReader(filename string, reader io.Reader) (*UploadImgResponse, error) {
|
|
||||||
var (
|
|
||||||
accessToken string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var byteData []byte
|
|
||||||
byteData, err = io.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var response []byte
|
|
||||||
if response, err = util.PostFileByStream("media", filename, fmt.Sprintf(uploadImgURL, accessToken), byteData); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := &UploadImgResponse{}
|
|
||||||
err = util.DecodeWithError(response, result, "UploadImg")
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadTempFile 上传临时素材
|
// UploadTempFile 上传临时素材
|
||||||
// @see https://developer.work.weixin.qq.com/document/path/90253
|
// @see https://developer.work.weixin.qq.com/document/path/90253
|
||||||
// @mediaType 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file)
|
// @mediaType 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file)
|
||||||
@@ -190,7 +166,5 @@ func (r *Client) GetTempFile(mediaID string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return response, nil
|
||||||
// 检查响应是否为错误信息,如果不是错误响应,则返回原始数据
|
|
||||||
return util.HandleFileResponse(response, "GetTempFile")
|
|
||||||
}
|
}
|
||||||
|
|||||||
0
work/message/README.md
Normal file
0
work/message/README.md
Normal file
1
work/message/group.go
Normal file
1
work/message/group.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package message
|
||||||
16
work/message/image.go
Normal file
16
work/message/image.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Image 图片消息
|
||||||
|
type Image struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Image struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
} `xml:"Image" json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewImage 回复图片消息
|
||||||
|
func NewImage(mediaID string) *Image {
|
||||||
|
image := new(Image)
|
||||||
|
image.Image.MediaID = mediaID
|
||||||
|
return image
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ type (
|
|||||||
// 消息类型,此时固定为:text
|
// 消息类型,此时固定为:text
|
||||||
MsgType string `json:"msgtype"`
|
MsgType string `json:"msgtype"`
|
||||||
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
||||||
AgentID string `json:"agentid"`
|
AgentID int `json:"agentid"`
|
||||||
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
||||||
Safe int `json:"safe"`
|
Safe int `json:"safe"`
|
||||||
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
||||||
|
|||||||
285
work/message/mix_message.go
Normal file
285
work/message/mix_message.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MsgType 企业微信普通消息类型
|
||||||
|
type MsgType string
|
||||||
|
|
||||||
|
// EventType 企业微信事件消息类型
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
// InfoType 第三方平台授权事件类型
|
||||||
|
type InfoType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
//MsgTypeEvent 表示事件推送消息 [限接收]
|
||||||
|
MsgTypeEvent = "event"
|
||||||
|
|
||||||
|
//MsgTypeText 表示文本消息
|
||||||
|
MsgTypeText MsgType = "text"
|
||||||
|
//MsgTypeImage 表示图片消息
|
||||||
|
MsgTypeImage MsgType = "image"
|
||||||
|
//MsgTypeVoice 表示语音消息
|
||||||
|
MsgTypeVoice MsgType = "voice"
|
||||||
|
//MsgTypeVideo 表示视频消息
|
||||||
|
MsgTypeVideo MsgType = "video"
|
||||||
|
//MsgTypeNews 表示图文消息[限回复与发送应用消息]
|
||||||
|
MsgTypeNews MsgType = "news"
|
||||||
|
|
||||||
|
//MsgTypeLink 表示链接消息[限接收]
|
||||||
|
MsgTypeLink MsgType = "link"
|
||||||
|
//MsgTypeLocation 表示坐标消息[限接收]
|
||||||
|
MsgTypeLocation MsgType = "location"
|
||||||
|
|
||||||
|
//MsgTypeUpdateButton 更新点击用户的按钮文案[限回复应用消息]
|
||||||
|
MsgTypeUpdateButton MsgType = "update_button"
|
||||||
|
//MsgTypeUpdateTemplateCard 更新点击用户的整张卡片[限回复应用消息]
|
||||||
|
MsgTypeUpdateTemplateCard MsgType = "update_template_card"
|
||||||
|
|
||||||
|
//MsgTypeFile 文件消息[限发送应用消息]
|
||||||
|
MsgTypeFile MsgType = "file"
|
||||||
|
//MsgTypeTextCard 文本卡片消息[限发送应用消息]
|
||||||
|
MsgTypeTextCard MsgType = "textcard"
|
||||||
|
//MsgTypeMpNews 图文消息[限发送应用消息] 跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信
|
||||||
|
MsgTypeMpNews MsgType = "mpnews"
|
||||||
|
//MsgTypeMarkdown markdown消息[限发送应用消息]
|
||||||
|
MsgTypeMarkdown MsgType = "markdown"
|
||||||
|
//MsgTypeMiniprogramNotice 小程序通知消息[限发送应用消息]
|
||||||
|
MsgTypeMiniprogramNotice MsgType = "miniprogram_notice"
|
||||||
|
//MsgTypeTemplateCard 模板卡片消息[限发送应用消息]
|
||||||
|
MsgTypeTemplateCard MsgType = "template_card"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//EventSubscribe 成员关注,成员已经加入企业,管理员添加成员到应用可见范围(或移除可见范围)时
|
||||||
|
EventSubscribe EventType = "subscribe"
|
||||||
|
//EventUnsubscribe 成员取消关注,成员已经在应用可见范围,成员加入(或退出)企业时
|
||||||
|
EventUnsubscribe EventType = "unsubscribe"
|
||||||
|
//EventEnterAgent 本事件在成员进入企业微信的应用时触发
|
||||||
|
EventEnterAgent EventType = "enter_agent"
|
||||||
|
//EventLocation 上报地理位置事件
|
||||||
|
EventLocation EventType = "LOCATION"
|
||||||
|
//EventBatchJobResult 异步任务完成事件推送
|
||||||
|
EventBatchJobResult EventType = "batch_job_result"
|
||||||
|
//EventClick 点击菜单拉取消息时的事件推送
|
||||||
|
EventClick EventType = "click"
|
||||||
|
//EventView 点击菜单跳转链接时的事件推送
|
||||||
|
EventView EventType = "view"
|
||||||
|
//EventScancodePush 扫码推事件的事件推送
|
||||||
|
EventScancodePush EventType = "scancode_push"
|
||||||
|
//EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送
|
||||||
|
EventScancodeWaitmsg EventType = "scancode_waitmsg"
|
||||||
|
//EventPicSysphoto 弹出系统拍照发图的事件推送
|
||||||
|
EventPicSysphoto EventType = "pic_sysphoto"
|
||||||
|
//EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送
|
||||||
|
EventPicPhotoOrAlbum EventType = "pic_photo_or_album"
|
||||||
|
//EventPicWeixin 弹出微信相册发图器的事件推送
|
||||||
|
EventPicWeixin EventType = "pic_weixin"
|
||||||
|
//EventLocationSelect 弹出地理位置选择器的事件推送
|
||||||
|
EventLocationSelect EventType = "location_select"
|
||||||
|
|
||||||
|
//EventOpenApprovalChange 审批状态通知事件推送
|
||||||
|
EventOpenApprovalChange EventType = "open_approval_change"
|
||||||
|
|
||||||
|
//EventShareAgentChange 共享应用事件回调
|
||||||
|
EventShareAgentChange EventType = "share_agent_change"
|
||||||
|
|
||||||
|
//EventTemplateCard 模板卡片事件推送
|
||||||
|
EventTemplateCard EventType = "template_card_event"
|
||||||
|
|
||||||
|
//EventTemplateCardMenu 通用模板卡片右上角菜单事件推送
|
||||||
|
EventTemplateCardMenu EventType = "template_card_menu_event"
|
||||||
|
|
||||||
|
//EventChangeExternalContact 企业客户事件推送
|
||||||
|
//add_external_contact 添加
|
||||||
|
//edit_external_contact 编辑
|
||||||
|
//add_half_external_contact 免验证添加
|
||||||
|
//del_external_contact 员工删除客户
|
||||||
|
//del_follow_user 客户删除跟进员工
|
||||||
|
//transfer_fail 企业将客户分配给新的成员接替后,客户添加失败
|
||||||
|
//change_external_chat 客户群创建事件
|
||||||
|
EventChangeExternalContact EventType = "change_external_contact"
|
||||||
|
|
||||||
|
//EventChangeExternalChat 企业客户群变更事件推送
|
||||||
|
//create 客户群创建
|
||||||
|
//update 客户群变更
|
||||||
|
//dismiss 客户群解散
|
||||||
|
EventChangeExternalChat EventType = "change_external_chat"
|
||||||
|
|
||||||
|
//EventChangeExternalTag 企业客户标签创建事件推送
|
||||||
|
//create 创建标签
|
||||||
|
//update 变更标签
|
||||||
|
//delete 删除标签
|
||||||
|
//shuffle 重新排序
|
||||||
|
EventChangeExternalTag EventType = "change_external_tag"
|
||||||
|
|
||||||
|
//EventKfMsg 企业微信客服回调事件
|
||||||
|
EventKfMsg EventType = "kf_msg_or_event"
|
||||||
|
//EventLivingStatusChange 直播回调事件
|
||||||
|
EventLivingStatusChange EventType = "living_status_change"
|
||||||
|
|
||||||
|
//EventMsgauditNotify 会话内容存档开启后,产生会话回调事件
|
||||||
|
EventMsgauditNotify EventType = "msgaudit_notify"
|
||||||
|
)
|
||||||
|
|
||||||
|
//todo 第三方应用开发
|
||||||
|
/*const (
|
||||||
|
//微信开放平台需要用到
|
||||||
|
|
||||||
|
// InfoTypeVerifyTicket 返回ticket
|
||||||
|
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
|
||||||
|
// InfoTypeAuthorized 授权
|
||||||
|
InfoTypeAuthorized = "authorized"
|
||||||
|
// InfoTypeUnauthorized 取消授权
|
||||||
|
InfoTypeUnauthorized = "unauthorized"
|
||||||
|
// InfoTypeUpdateAuthorized 更新授权
|
||||||
|
InfoTypeUpdateAuthorized = "updateauthorized"
|
||||||
|
)*/
|
||||||
|
|
||||||
|
// MixMessage 存放所有企业微信官方发送过来的消息和事件
|
||||||
|
type MixMessage struct {
|
||||||
|
CommonToken
|
||||||
|
|
||||||
|
//接收普通消息
|
||||||
|
MsgID int64 `xml:"MsgId"` //其他消息推送过来是MsgId
|
||||||
|
AgentID int `xml:"AgentID"` //企业应用的id,整型。可在应用的设置页面查看
|
||||||
|
|
||||||
|
Content string `xml:"Content,omitempty"` //文本消息内容
|
||||||
|
Format string `xml:"Format,omitempty"` //语音消息格式,如amr,speex等
|
||||||
|
ThumbMediaID string `xml:"ThumbMediaId,omitempty"` //视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取数据,仅三天内有效
|
||||||
|
|
||||||
|
Title string `xml:"Title,omitempty"` //链接消息,标题
|
||||||
|
Description string `xml:"Description,omitempty"` //链接消息,描述
|
||||||
|
URL string `xml:"Url,omitempty"` //链接消息,链接跳转的url
|
||||||
|
|
||||||
|
PicURL string `xml:"PicUrl,omitempty"` ////图片消息或者链接消息,封面缩略图的url
|
||||||
|
MediaID string `xml:"MediaId,omitempty"` //图片媒体文件id//语音媒体文件id//视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取,仅三天内有效
|
||||||
|
|
||||||
|
LocationX float64 `xml:"Location_X,omitempty"` //位置消息,地理位置纬度
|
||||||
|
LocationY float64 `xml:"Location_Y,omitempty"` //位置消息,地理位置经度
|
||||||
|
Scale float64 `xml:"Scale,omitempty"` //位置消息,地图缩放大小
|
||||||
|
Label string `xml:"Label,omitempty"` //位置消息,地理位置信息
|
||||||
|
|
||||||
|
AppType string `xml:"AppType,omitempty"` //接收地理位置时存在,app类型,在企业微信固定返回wxwork,在微信不返回该字段
|
||||||
|
|
||||||
|
//TemplateMsgID int64 `xml:"MsgID"` //模板消息推送成功的消息是MsgID
|
||||||
|
///Recognition string `xml:"Recognition"`
|
||||||
|
|
||||||
|
//事件相关
|
||||||
|
Event EventType `xml:"Event,omitempty"`
|
||||||
|
EventKey string `xml:"EventKey,omitempty"`
|
||||||
|
ChangeType string `xml:"ChangeType,omitempty"`
|
||||||
|
|
||||||
|
//模板卡片事件推送 https://developer.work.weixin.qq.com/document/path/90240#%E6%A8%A1%E6%9D%BF%E5%8D%A1%E7%89%87%E4%BA%8B%E4%BB%B6%E6%8E%A8%E9%80%81
|
||||||
|
TaskId string `xml:"TaskId,omitempty"` //与发送模板卡片消息时指定的task_id相同
|
||||||
|
CardType string `xml:"CardType,omitempty"` //通用模板卡片的类型,类型有"text_notice", "news_notice", "button_interaction", "vote_interaction", "multiple_interaction"五种
|
||||||
|
ResponseCode string `xml:"ResponseCode,omitempty"` //用于调用更新卡片接口的ResponseCode,24小时内有效,且只能使用一次
|
||||||
|
SelectedItems struct {
|
||||||
|
SelectedItem struct {
|
||||||
|
QuestionKey string `xml:"QuestionKey"` //问题的key值
|
||||||
|
OptionIds struct { //对应问题的选项列表
|
||||||
|
OptionId string `xml:"OptionId"`
|
||||||
|
} `xml:"OptionIds"`
|
||||||
|
} `xml:"SelectedItem"`
|
||||||
|
} `xml:"SelectedItems,omitempty"`
|
||||||
|
|
||||||
|
//仅上报地理位置事件
|
||||||
|
Latitude string `xml:"Latitude,omitempty"` //地理位置纬度
|
||||||
|
Longitude string `xml:"Longitude,omitempty"` //地理位置经度
|
||||||
|
Precision string `xml:"Precision,omitempty"` //地理位置精度
|
||||||
|
|
||||||
|
//仅异步任务完成事件
|
||||||
|
JobId string `xml:"JobId,omitempty"` //异步任务id,最大长度为64字符
|
||||||
|
JobType string `xml:"JobType,omitempty"` //异步任务,操作类型,字符串,目前分别有:sync_user(增量更新成员)、 replace_user(全量覆盖成员)、invite_user(邀请成员关注)、replace_party(全量覆盖部门)
|
||||||
|
ErrCode int `xml:"ErrCode,omitempty"` //异步任务,返回码
|
||||||
|
ErrMsg string `xml:"ErrMsg,omitempty"` //异步任务,对返回码的文本描述内容
|
||||||
|
|
||||||
|
//开启通讯录回调通知 https://open.work.weixin.qq.com/api/doc/90000/90135/90967
|
||||||
|
UserID string `xml:"UserID,omitempty"` //用户userid
|
||||||
|
ExternalUserID string `xml:"ExternalUserID,omitempty"` //外部联系人userid
|
||||||
|
State string `xml:"State,omitempty"` //添加此用户的「联系我」方式配置的state参数,可用于识别添加此用户的渠道
|
||||||
|
WelcomeCode string `xml:"WelcomeCode,omitempty"` //欢迎码,当state为1时,该值有效
|
||||||
|
Source string `xml:"Source,omitempty"` //删除客户的操作来源,DELETE_BY_TRANSFER表示此客户是因在职继承自动被转接成员删除
|
||||||
|
|
||||||
|
// todo 第三方平台相关 字段名可能不准确
|
||||||
|
/*InfoType InfoType `xml:"InfoType"`
|
||||||
|
AppID string `xml:"AppId"`
|
||||||
|
ComponentVerifyTicket string `xml:"ComponentVerifyTicket"`
|
||||||
|
AuthorizerAppid string `xml:"AuthorizerAppid"`
|
||||||
|
AuthorizationCode string `xml:"AuthorizationCode"```````````````````````````````````````
|
||||||
|
AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"`
|
||||||
|
PreAuthCode string `xml:"PreAuthCode"`*/
|
||||||
|
|
||||||
|
//设备相关
|
||||||
|
device.MsgDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventPic 发图事件推送
|
||||||
|
type EventPic struct {
|
||||||
|
PicMd5Sum string `xml:"PicMd5Sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedXMLMsg 安全模式下的消息体
|
||||||
|
type EncryptedXMLMsg struct {
|
||||||
|
XMLName struct{} `xml:"xml" json:"-"`
|
||||||
|
ToUserName string `xml:"ToUserName" json:"ToUserName"`
|
||||||
|
AgentID string `xml:"AgentID" json:"AgentID"`
|
||||||
|
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseEncryptedXMLMsg 需要返回的消息体
|
||||||
|
type ResponseEncryptedXMLMsg struct {
|
||||||
|
XMLName struct{} `xml:"xml" json:"-"`
|
||||||
|
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
|
||||||
|
MsgSignature string `xml:"MsgSignature" json:"MsgSignature"`
|
||||||
|
Timestamp int64 `xml:"TimeStamp" json:"TimeStamp"`
|
||||||
|
Nonce string `xml:"Nonce" json:"Nonce"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略
|
||||||
|
type CDATA string
|
||||||
|
|
||||||
|
// MarshalXML 实现自己的序列化方法
|
||||||
|
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return e.EncodeElement(struct {
|
||||||
|
string `xml:",cdata"`
|
||||||
|
}{string(c)}, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonToken 消息中通用的结构
|
||||||
|
type CommonToken struct {
|
||||||
|
XMLName xml.Name `xml:"xml"`
|
||||||
|
ToUserName CDATA `xml:"ToUserName"`
|
||||||
|
FromUserName CDATA `xml:"FromUserName"`
|
||||||
|
CreateTime int64 `xml:"CreateTime"`
|
||||||
|
MsgType MsgType `xml:"MsgType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetToUserName set ToUserName
|
||||||
|
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
|
||||||
|
msg.ToUserName = toUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFromUserName set FromUserName
|
||||||
|
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
|
||||||
|
msg.FromUserName = fromUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCreateTime set createTime
|
||||||
|
func (msg *CommonToken) SetCreateTime(createTime int64) {
|
||||||
|
msg.CreateTime = createTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMsgType set MsgType
|
||||||
|
func (msg *CommonToken) SetMsgType(msgType MsgType) {
|
||||||
|
msg.MsgType = msgType
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOpenID get the FromUserName value
|
||||||
|
func (msg *CommonToken) GetOpenID() string {
|
||||||
|
return string(msg.FromUserName)
|
||||||
|
}
|
||||||
41
work/message/news.go
Normal file
41
work/message/news.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//News 图文消息
|
||||||
|
type News struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
ArticleCount int `xml:"ArticleCount" json:"-"`
|
||||||
|
Articles []*Article `xml:"Articles>item,omitempty" json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewNews 初始化图文消息
|
||||||
|
func NewNews(articles []*Article) *News {
|
||||||
|
news := new(News)
|
||||||
|
news.ArticleCount = len(articles)
|
||||||
|
news.Articles = articles
|
||||||
|
return news
|
||||||
|
}
|
||||||
|
|
||||||
|
//Article 单篇文章
|
||||||
|
type Article struct {
|
||||||
|
Title string `xml:"Title,omitempty" json:"title"`
|
||||||
|
Description string `xml:"Description,omitempty" json:"description"`
|
||||||
|
PicURL string `xml:"PicUrl,omitempty" json:"picurl"`
|
||||||
|
URL string `xml:"Url,omitempty" json:"url"`
|
||||||
|
Appid string `xml:"-" json:"appid"` //仅在发送应用消息时需要
|
||||||
|
Pagepath string `xml:"-" json:"pagepath"` //仅在发送应用消息时需要
|
||||||
|
}
|
||||||
|
|
||||||
|
//MpNews 图文消息
|
||||||
|
type MpNews struct {
|
||||||
|
Articles []*MpNewsArticle `xml:"-" json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//MpNewsArticle mpnews类型的图文消息,跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信
|
||||||
|
type MpNewsArticle struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ThumbMediaId string `json:"thumb_media_id"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
ContentSourceUrl string `json:"content_source_url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
}
|
||||||
15
work/message/reply.go
Normal file
15
work/message/reply.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
//ErrInvalidReply 无效的回复
|
||||||
|
var ErrInvalidReply = errors.New("无效的回复消息")
|
||||||
|
|
||||||
|
//ErrUnsupportReply 不支持的回复类型
|
||||||
|
var ErrUnsupportReply = errors.New("无需回复消息")
|
||||||
|
|
||||||
|
//Reply 消息回复
|
||||||
|
type Reply struct {
|
||||||
|
MsgType MsgType
|
||||||
|
MsgData interface{}
|
||||||
|
}
|
||||||
129
work/message/template_card.go
Normal file
129
work/message/template_card.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
messageUpdateTemplateCardURL = "https://api.weixin.qq.com/cgi-bin/message/update_template_card"
|
||||||
|
messageDelURL = "https://api.weixin.qq.com/cgi-bin/message/recall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateButton 模板卡片按钮
|
||||||
|
type UpdateButton struct {
|
||||||
|
//CommonToken `json:"-"`
|
||||||
|
Button struct {
|
||||||
|
ReplaceName string `xml:"ReplaceName" json:"replace_name"`
|
||||||
|
} `xml:"Button" json:"button"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUpdateButton 更新点击用户的按钮文案
|
||||||
|
func NewUpdateButton(replaceName string) *UpdateButton {
|
||||||
|
btn := new(UpdateButton)
|
||||||
|
btn.Button.ReplaceName = replaceName
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateCard 被动回复模板卡片
|
||||||
|
// https://open.work.weixin.qq.com/api/doc/90000/90135/90241
|
||||||
|
type TemplateCard struct {
|
||||||
|
//CommonToken `json:"-"`
|
||||||
|
TemplateCard interface{} `xml:"TemplateCard" json:"template_card"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplateCard 更新点击用户的整张卡片
|
||||||
|
func NewTemplateCard(cardXml interface{}) *TemplateCard {
|
||||||
|
card := new(TemplateCard)
|
||||||
|
card.TemplateCard = cardXml
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushFile struct {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
}
|
||||||
|
type PushTextCard struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Btntxt string `json:"btntxt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resTemplateSend struct {
|
||||||
|
util.CommonError
|
||||||
|
Invaliduser string `json:"invaliduser"` //不合法的userid,不区分大小写,统一转为小写
|
||||||
|
Invalidparty string `json:"invalidparty"` //不合法的partyid
|
||||||
|
Invalidtag string `json:"invalidtag"` //不合法的标签id
|
||||||
|
MsgID string `json:"msgid"` //消息id,用于撤回应用消息
|
||||||
|
ResponseCode string `json:"response_code"` //仅消息类型为“按钮交互型”,“投票选择型”和“多项选择型”的模板卡片消息返回,应用可使用response_code调用更新模版卡片消息接口,24小时内有效,且只能使用一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateUpdate 更新模版卡片消息内容
|
||||||
|
type TemplateUpdate struct {
|
||||||
|
Userids []string `json:"userids"`
|
||||||
|
Partyids []int `json:"partyids"`
|
||||||
|
Tagids []int `json:"tagids"`
|
||||||
|
Atall int `json:"atall"`
|
||||||
|
Agentid int `json:"agentid"`
|
||||||
|
ResponseCode string `json:"response_code"`
|
||||||
|
*UpdateButton
|
||||||
|
*TemplateCard
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTemplate 更新模版卡片消息
|
||||||
|
func (r *Client) UpdateTemplate(msg *TemplateUpdate) (msgID string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", messageUpdateTemplateCardURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resTemplateSend
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgID = result.MsgID
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReqRecall struct {
|
||||||
|
MsgID int64 `json:"msgid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recall 撤回应用消息
|
||||||
|
func (r *Client) Recall(msgID int64) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", messageDelURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, &ReqRecall{
|
||||||
|
MsgID: msgID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result util.CommonError
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
14
work/message/text.go
Normal file
14
work/message/text.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Text 文本消息
|
||||||
|
type Text struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Content CDATA `json:"content" xml:"Content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewText 初始化文本消息
|
||||||
|
func NewText(content string) *Text {
|
||||||
|
text := new(Text)
|
||||||
|
text.Content = CDATA(content)
|
||||||
|
return text
|
||||||
|
}
|
||||||
20
work/message/video.go
Normal file
20
work/message/video.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Video 视频消息
|
||||||
|
type Video struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Video struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
Title string `xml:"Title,omitempty" json:"title"`
|
||||||
|
Description string `xml:"Description,omitempty" json:"description"`
|
||||||
|
} `xml:"Video" json:"video"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewVideo 回复图片消息
|
||||||
|
func NewVideo(mediaID, title, description string) *Video {
|
||||||
|
video := new(Video)
|
||||||
|
video.Video.MediaID = mediaID
|
||||||
|
video.Video.Title = title
|
||||||
|
video.Video.Description = description
|
||||||
|
return video
|
||||||
|
}
|
||||||
16
work/message/voice.go
Normal file
16
work/message/voice.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Voice 语音消息
|
||||||
|
type Voice struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Voice struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
} `xml:"Voice" json:"voice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewVoice 回复语音消息
|
||||||
|
func NewVoice(mediaID string) *Voice {
|
||||||
|
voice := new(Voice)
|
||||||
|
voice.Voice.MediaID = mediaID
|
||||||
|
return voice
|
||||||
|
}
|
||||||
@@ -27,10 +27,6 @@ var (
|
|||||||
getUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s"
|
getUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s"
|
||||||
// getUserDetailURL 获取访问用户敏感信息
|
// getUserDetailURL 获取访问用户敏感信息
|
||||||
getUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s"
|
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
|
// NewOauth new init oauth
|
||||||
@@ -167,57 +163,3 @@ func (ctr *Oauth) GetUserDetail(req *GetUserDetailRequest) (*GetUserDetailRespon
|
|||||||
err = util.DecodeWithError(response, result, "GetUserDetail")
|
err = util.DecodeWithError(response, result, "GetUserDetail")
|
||||||
return result, err
|
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")
|
|
||||||
}
|
|
||||||
|
|||||||
64
work/oauth/user.go
Normal file
64
work/oauth/user.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
code2SessionURL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code"
|
||||||
|
launchCode = "https://qyapi.weixin.qq.com/cgi-bin/get_launch_code?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ctr *Oauth) Code2Session(code string) (result ResUserInfo, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = ctr.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(
|
||||||
|
fmt.Sprintf(code2SessionURL, accessToken, code),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespLaunchCode struct {
|
||||||
|
util.CommonError
|
||||||
|
LaunchCode string `json:"launch_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLaunchCode 用于打开个人聊天窗口schema
|
||||||
|
func (ctr *Oauth) GetLaunchCode(userID, other string) (userInfo *RespLaunchCode, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = ctr.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(launchCode, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, map[string]interface{}{"operator_userid": userID, "single_chat": map[string]string{"userid": other}})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userInfo = new(RespLaunchCode)
|
||||||
|
err = json.Unmarshal(response, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userInfo.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
66
work/server/error.go
Normal file
66
work/server/error.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error 错误
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SDKValidateSignatureError Error = "签名验证错误" //-40001
|
||||||
|
SDKParseJsonError Error = "xml/json解析失败" //-40002
|
||||||
|
SDKComputeSignatureError Error = "sha加密生成签名失败" //-40003
|
||||||
|
SDKIllegalAesKey Error = "AESKey 非法" //-40004
|
||||||
|
SDKValidateCorpidError Error = "ReceiveId 校验错误" //-40005
|
||||||
|
SDKEncryptAESError Error = "AES 加密失败" //-40006
|
||||||
|
SDKDecryptAESError Error = "AES 解密失败" //-40007
|
||||||
|
SDKIllegalBuffer Error = "解密后得到的buffer非法" //-40008
|
||||||
|
SDKEncodeBase64Error Error = "base64加密失败" //-40009
|
||||||
|
SDKDecodeBase64Error Error = "base64解密失败" //-40010
|
||||||
|
SDKGenJsonError Error = "生成xml/json失败" //-40011
|
||||||
|
SDKIllegalProtocolType Error = "协议类型非法" //-40012
|
||||||
|
SDKUnknownError Error = "未知错误"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Error 输出错误信息
|
||||||
|
func (r Error) Error() string {
|
||||||
|
return reflect.ValueOf(r).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSDKErr 初始化SDK实例错误信息
|
||||||
|
func NewSDKErr(code int64, msgList ...string) Error {
|
||||||
|
switch code {
|
||||||
|
case 40001:
|
||||||
|
return SDKValidateSignatureError
|
||||||
|
case 40002:
|
||||||
|
return SDKParseJsonError
|
||||||
|
case 40003:
|
||||||
|
return SDKComputeSignatureError
|
||||||
|
case 40004:
|
||||||
|
return SDKIllegalAesKey
|
||||||
|
case 40005:
|
||||||
|
return SDKValidateCorpidError
|
||||||
|
case 40006:
|
||||||
|
return SDKEncryptAESError
|
||||||
|
case 40007:
|
||||||
|
return SDKDecryptAESError
|
||||||
|
case 40008:
|
||||||
|
return SDKIllegalBuffer
|
||||||
|
case 40009:
|
||||||
|
return SDKEncodeBase64Error
|
||||||
|
case 40010:
|
||||||
|
return SDKDecodeBase64Error
|
||||||
|
case 40011:
|
||||||
|
return SDKGenJsonError
|
||||||
|
case 40012:
|
||||||
|
return SDKIllegalProtocolType
|
||||||
|
default:
|
||||||
|
//返回未知的自定义错误
|
||||||
|
if len(msgList) > 0 {
|
||||||
|
return Error(strings.Join(msgList, ","))
|
||||||
|
}
|
||||||
|
return SDKUnknownError
|
||||||
|
}
|
||||||
|
}
|
||||||
228
work/server/server.go
Normal file
228
work/server/server.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
"github.com/silenceper/wechat/v2/work/message"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Server struct
|
||||||
|
type Server struct {
|
||||||
|
*context.Context
|
||||||
|
Writer http.ResponseWriter
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
|
skipValidate bool
|
||||||
|
|
||||||
|
messageHandler func(*message.MixMessage) *message.Reply
|
||||||
|
|
||||||
|
RequestRawXMLMsg []byte
|
||||||
|
RequestMsg *message.MixMessage
|
||||||
|
ResponseRawXMLMsg []byte
|
||||||
|
ResponseMsg interface{}
|
||||||
|
|
||||||
|
random []byte
|
||||||
|
nonce string
|
||||||
|
timestamp int64
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewServer init
|
||||||
|
func NewServer(context *context.Context) *Server {
|
||||||
|
srv := new(Server)
|
||||||
|
srv.Context = context
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) VerifyURL() (string, error) {
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
signature := srv.Query("msg_signature")
|
||||||
|
echoStr := srv.Query("echostr")
|
||||||
|
if signature != util.Signature(srv.Token, timestamp, nonce, echoStr) {
|
||||||
|
return "", NewSDKErr(40001)
|
||||||
|
}
|
||||||
|
_, bData, err := util.DecryptMsg(srv.CorpID, echoStr, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", NewSDKErr(40002)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipValidate set skip validate
|
||||||
|
func (srv *Server) SkipValidate(skip bool) {
|
||||||
|
srv.skipValidate = skip
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serve 处理企业微信的请求消息
|
||||||
|
func (srv *Server) Serve() error {
|
||||||
|
response, err := srv.handleRequest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//debug print request msg
|
||||||
|
log.Debugf("request msg =%s", string(srv.RequestRawXMLMsg))
|
||||||
|
return srv.buildResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Validate 校验请求是否合法
|
||||||
|
func (srv *Server) Validate() bool {
|
||||||
|
if srv.skipValidate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
signature := srv.Query("msg_signature")
|
||||||
|
log.Debugf("validate signature, timestamp=%s, nonce=%s", timestamp, nonce)
|
||||||
|
return signature == util.Signature(srv.Token, timestamp, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
//HandleRequest 处理企业微信的请求
|
||||||
|
func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
||||||
|
|
||||||
|
var msg interface{}
|
||||||
|
msg, err = srv.getMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mixMessage, success := msg.(*message.MixMessage)
|
||||||
|
if !success {
|
||||||
|
err = errors.New("消息类型转换失败")
|
||||||
|
}
|
||||||
|
srv.RequestMsg = mixMessage
|
||||||
|
reply = srv.messageHandler(mixMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//getMessage 解析企业微信返回的消息
|
||||||
|
func (srv *Server) getMessage() (interface{}, error) {
|
||||||
|
var rawXMLMsgBytes []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var encryptedXMLMsg message.EncryptedXMLMsg
|
||||||
|
if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("从body中解析xml失败,err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//验证消息签名
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
srv.timestamp, err = strconv.ParseInt(timestamp, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
srv.nonce = nonce
|
||||||
|
msgSignature := srv.Query("msg_signature")
|
||||||
|
msgSignatureGen := util.Signature(srv.Token, timestamp, nonce, encryptedXMLMsg.EncryptedMsg)
|
||||||
|
if msgSignature != msgSignatureGen {
|
||||||
|
return nil, fmt.Errorf("消息不合法,验证签名失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
//解密
|
||||||
|
srv.random, rawXMLMsgBytes, err = util.DecryptMsg(srv.CorpID, encryptedXMLMsg.EncryptedMsg, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("消息解密失败, err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.RequestRawXMLMsg = rawXMLMsgBytes
|
||||||
|
return srv.parseRequestMessage(rawXMLMsgBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixMessage, err error) {
|
||||||
|
msg = &message.MixMessage{}
|
||||||
|
err = xml.Unmarshal(rawXMLMsgBytes, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetMessageHandler 设置用户自定义的回调方法
|
||||||
|
func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) {
|
||||||
|
srv.messageHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) buildResponse(reply *message.Reply) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if reply == nil {
|
||||||
|
//do nothing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msgType := reply.MsgType
|
||||||
|
switch msgType {
|
||||||
|
case message.MsgTypeText:
|
||||||
|
case message.MsgTypeImage:
|
||||||
|
case message.MsgTypeVoice:
|
||||||
|
case message.MsgTypeVideo:
|
||||||
|
case message.MsgTypeNews:
|
||||||
|
case message.MsgTypeUpdateButton:
|
||||||
|
case message.MsgTypeUpdateTemplateCard:
|
||||||
|
default:
|
||||||
|
err = message.ErrUnsupportReply
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgData := reply.MsgData
|
||||||
|
value := reflect.ValueOf(msgData)
|
||||||
|
//msgData must be a ptr
|
||||||
|
kind := value.Kind().String()
|
||||||
|
if kind != "ptr" {
|
||||||
|
return message.ErrUnsupportReply
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make([]reflect.Value, 1)
|
||||||
|
params[0] = reflect.ValueOf(srv.RequestMsg.FromUserName)
|
||||||
|
value.MethodByName("SetToUserName").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(srv.RequestMsg.ToUserName)
|
||||||
|
value.MethodByName("SetFromUserName").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(msgType)
|
||||||
|
value.MethodByName("SetMsgType").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(util.GetCurrTS())
|
||||||
|
value.MethodByName("SetCreateTime").Call(params)
|
||||||
|
srv.ResponseMsg = msgData
|
||||||
|
srv.ResponseRawXMLMsg, err = xml.Marshal(msgData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Send 将自定义的消息发送
|
||||||
|
func (srv *Server) Send() (err error) {
|
||||||
|
replyMsg := srv.ResponseMsg
|
||||||
|
log.Debugf("response msg =%+v", replyMsg)
|
||||||
|
|
||||||
|
//安全模式下对消息进行加密
|
||||||
|
var encryptedMsg []byte
|
||||||
|
encryptedMsg, err = util.EncryptMsg(srv.random, srv.ResponseRawXMLMsg, srv.CorpID, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//TODO 如果获取不到timestamp nonce 则自己生成
|
||||||
|
timestamp := srv.timestamp
|
||||||
|
timestampStr := strconv.FormatInt(timestamp, 10)
|
||||||
|
msgSignature := util.Signature(srv.Token, timestampStr, srv.nonce, string(encryptedMsg))
|
||||||
|
replyMsg = message.ResponseEncryptedXMLMsg{
|
||||||
|
EncryptedMsg: string(encryptedMsg),
|
||||||
|
MsgSignature: msgSignature,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Nonce: srv.nonce,
|
||||||
|
}
|
||||||
|
|
||||||
|
if replyMsg != nil {
|
||||||
|
srv.XML(replyMsg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
58
work/server/util.go
Normal file
58
work/server/util.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||||
|
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||||
|
|
||||||
|
func writeContextType(w http.ResponseWriter, value []string) {
|
||||||
|
header := w.Header()
|
||||||
|
if val := header["Content-Type"]; len(val) == 0 {
|
||||||
|
header["Content-Type"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Render render from bytes
|
||||||
|
func (srv *Server) Render(bytes []byte) {
|
||||||
|
//debug
|
||||||
|
//fmt.Println("response msg = ", string(bytes))
|
||||||
|
srv.Writer.WriteHeader(200)
|
||||||
|
_, err := srv.Writer.Write(bytes)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//String render from string
|
||||||
|
func (srv *Server) String(str string) {
|
||||||
|
writeContextType(srv.Writer, plainContentType)
|
||||||
|
srv.Render([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
//XML render to xml
|
||||||
|
func (srv *Server) XML(obj interface{}) {
|
||||||
|
writeContextType(srv.Writer, xmlContentType)
|
||||||
|
bytes, err := xml.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
srv.Render(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns the keyed url query value if it exists
|
||||||
|
func (srv *Server) Query(key string) string {
|
||||||
|
value, _ := srv.GetQuery(key)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuery is like Query(), it returns the keyed url query value
|
||||||
|
func (srv *Server) GetQuery(key string) (string, bool) {
|
||||||
|
req := srv.Request
|
||||||
|
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
|
||||||
|
return values[0], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
0
work/tools/calendar.go
Normal file
0
work/tools/calendar.go
Normal file
194
work/user/user.go
Normal file
194
work/user/user.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s"
|
||||||
|
updateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%s&fetch_child=1"
|
||||||
|
userListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User 用户管理
|
||||||
|
type User struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUser 实例化
|
||||||
|
func NewUser(context *context.Context) *User {
|
||||||
|
user := new(User)
|
||||||
|
user.Context = context
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info 用户基本信息
|
||||||
|
type Info struct {
|
||||||
|
util.CommonError
|
||||||
|
Userid string `json:"userid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Department []int `json:"department"`
|
||||||
|
Order []int `json:"order"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
Gender string `json:"gender"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
ThumbAvatar string `json:"thumb_avatar"`
|
||||||
|
Telephone string `json:"telephone"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
OpenUserid string `json:"open_userid"`
|
||||||
|
MainDepartment int `json:"main_department"`
|
||||||
|
Extattr struct {
|
||||||
|
Attrs []struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"text,omitempty"`
|
||||||
|
Web struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"web,omitempty"`
|
||||||
|
} `json:"attrs"`
|
||||||
|
} `json:"extattr"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
QrCode string `json:"qr_code"`
|
||||||
|
ExternalPosition string `json:"external_position"`
|
||||||
|
ExternalProfile struct {
|
||||||
|
ExternalCorpName string `json:"external_corp_name"`
|
||||||
|
WechatChannels struct {
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"wechat_channels"`
|
||||||
|
ExternalAttr []struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"text,omitempty"`
|
||||||
|
Web struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"web,omitempty"`
|
||||||
|
Miniprogram struct {
|
||||||
|
Appid string `json:"appid"`
|
||||||
|
Pagepath string `json:"pagepath"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"miniprogram,omitempty"`
|
||||||
|
} `json:"external_attr"`
|
||||||
|
} `json:"external_profile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenidList 用户列表
|
||||||
|
type OpenidList struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
Total int `json:"total"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Data struct {
|
||||||
|
OpenIDs []string `json:"openid"`
|
||||||
|
} `json:"data"`
|
||||||
|
NextOpenID string `json:"next_openid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户基本信息
|
||||||
|
func (user *User) GetUserInfo(userID string) (userInfo *Info, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(userInfoURL, accessToken, userID)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userInfo = new(Info)
|
||||||
|
err = json.Unmarshal(response, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userInfo.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新员工资料
|
||||||
|
func (user *User) Update(userID, external_position string) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(updateURL, accessToken, userID)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, map[string]string{"userid": userID, "external_position": external_position})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.DecodeWithCommonError(response, "updateURL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserOpenIDs 返回用户列表
|
||||||
|
func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) {
|
||||||
|
accessToken, err := user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri, _ := url.Parse(userListURL)
|
||||||
|
q := uri.Query()
|
||||||
|
q.Set("access_token", accessToken)
|
||||||
|
if len(nextOpenid) > 0 && nextOpenid[0] != "" {
|
||||||
|
q.Set("next_openid", nextOpenid[0])
|
||||||
|
}
|
||||||
|
uri.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
response, err := util.HTTPGet(uri.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userlist := OpenidList{}
|
||||||
|
|
||||||
|
err = util.DecodeWithError(response, &userlist, "ListUserOpenIDs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllUserOpenIDs 返回所有用户OpenID列表
|
||||||
|
func (user *User) ListAllUserOpenIDs() ([]string, error) {
|
||||||
|
nextOpenid := ""
|
||||||
|
openids := make([]string, 0)
|
||||||
|
count := 0
|
||||||
|
for {
|
||||||
|
ul, err := user.ListUserOpenIDs(nextOpenid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
openids = append(openids, ul.Data.OpenIDs...)
|
||||||
|
count += ul.Count
|
||||||
|
if ul.Total > count {
|
||||||
|
nextOpenid = ul.NextOpenID
|
||||||
|
} else {
|
||||||
|
return openids, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
work/work.go
24
work/work.go
@@ -9,13 +9,15 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/work/context"
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
"github.com/silenceper/wechat/v2/work/externalcontact"
|
"github.com/silenceper/wechat/v2/work/externalcontact"
|
||||||
"github.com/silenceper/wechat/v2/work/invoice"
|
"github.com/silenceper/wechat/v2/work/invoice"
|
||||||
"github.com/silenceper/wechat/v2/work/jsapi"
|
"github.com/silenceper/wechat/v2/work/js"
|
||||||
"github.com/silenceper/wechat/v2/work/kf"
|
"github.com/silenceper/wechat/v2/work/kf"
|
||||||
"github.com/silenceper/wechat/v2/work/material"
|
"github.com/silenceper/wechat/v2/work/material"
|
||||||
"github.com/silenceper/wechat/v2/work/message"
|
"github.com/silenceper/wechat/v2/work/message"
|
||||||
"github.com/silenceper/wechat/v2/work/msgaudit"
|
"github.com/silenceper/wechat/v2/work/msgaudit"
|
||||||
"github.com/silenceper/wechat/v2/work/oauth"
|
"github.com/silenceper/wechat/v2/work/oauth"
|
||||||
"github.com/silenceper/wechat/v2/work/robot"
|
"github.com/silenceper/wechat/v2/work/robot"
|
||||||
|
"github.com/silenceper/wechat/v2/work/server"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Work 企业微信
|
// Work 企业微信
|
||||||
@@ -25,7 +27,7 @@ type Work struct {
|
|||||||
|
|
||||||
// NewWork init work
|
// NewWork init work
|
||||||
func NewWork(cfg *config.Config) *Work {
|
func NewWork(cfg *config.Config) *Work {
|
||||||
defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, cfg.AgentID, credential.CacheKeyWorkPrefix, cfg.Cache)
|
defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache)
|
||||||
ctx := &context.Context{
|
ctx := &context.Context{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
AccessTokenHandle: defaultAkHandle,
|
AccessTokenHandle: defaultAkHandle,
|
||||||
@@ -53,11 +55,6 @@ func (wk *Work) GetKF() (*kf.Client, error) {
|
|||||||
return kf.NewClient(wk.ctx.Config)
|
return kf.NewClient(wk.ctx.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JsSdk get JsSdk
|
|
||||||
func (wk *Work) JsSdk() *jsapi.Js {
|
|
||||||
return jsapi.NewJs(wk.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalContact get external_contact
|
// GetExternalContact get external_contact
|
||||||
func (wk *Work) GetExternalContact() *externalcontact.Client {
|
func (wk *Work) GetExternalContact() *externalcontact.Client {
|
||||||
return externalcontact.NewClient(wk.ctx)
|
return externalcontact.NewClient(wk.ctx)
|
||||||
@@ -97,3 +94,16 @@ func (wk *Work) GetInvoice() *invoice.Client {
|
|||||||
func (wk *Work) GetCheckin() *checkin.Client {
|
func (wk *Work) GetCheckin() *checkin.Client {
|
||||||
return checkin.NewClient(wk.ctx)
|
return checkin.NewClient(wk.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJs js-sdk配置
|
||||||
|
func (wk *Work) GetJs() *js.Js {
|
||||||
|
return js.NewJs(wk.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServer 消息管理:接收事件,被动回复消息管理
|
||||||
|
func (wk *Work) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
|
||||||
|
srv := server.NewServer(wk.ctx)
|
||||||
|
srv.Request = req
|
||||||
|
srv.Writer = writer
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user