mirror of
https://github.com/silenceper/wechat.git
synced 2026-11-16 05:12:03 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8157cad2cb | ||
|
|
be6e95e987 |
No files matched your search
+2
-100
@@ -1,103 +1,5 @@
|
||||
# 智能对话
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html)
|
||||
[官方文档](https://developers.weixin.qq.com/doc/aispeech/platform/INTERFACEDOCUMENT.html)
|
||||
|
||||
## 快速入门
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2"
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/aispeech/dialog"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
wc := wechat.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
|
||||
ai := wc.GetAISpeech(&config.Config{
|
||||
AppID: "xxx",
|
||||
Token: "xxx",
|
||||
AESKey: "xxx",
|
||||
Account: "admin",
|
||||
Cache: memory,
|
||||
})
|
||||
|
||||
dialogClient := ai.GetDialog()
|
||||
|
||||
accessToken, err := dialogClient.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := dialogClient.Query(&dialog.QueryRequest{
|
||||
Query: "你好",
|
||||
Env: "online",
|
||||
UserID: "user-1",
|
||||
})
|
||||
```
|
||||
|
||||
## 简单问答导入与发布
|
||||
|
||||
```go
|
||||
task, err := dialogClient.ImportJSON(&dialog.ImportJSONRequest{
|
||||
Mode: 0,
|
||||
Data: []dialog.BotIntent{{
|
||||
Skill: "售前咨询",
|
||||
Intent: "查询营业时间",
|
||||
Disable: false,
|
||||
Questions: []string{"你们几点开门", "营业时间是什么时候"},
|
||||
Answers: []string{"我们的营业时间是周一至周五 9:00-18:00"},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asyncResult, err := dialogClient.FetchAsync(&dialog.FetchAsyncRequest{
|
||||
TaskID: task.TaskID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publish, err := dialogClient.Publish()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progress, err := dialogClient.GetEffectiveProgress(&dialog.EffectiveProgressRequest{
|
||||
Env: "online",
|
||||
})
|
||||
```
|
||||
|
||||
当前封装微信智能对话开放接口中“接入智能对话”的 6 个接口:
|
||||
|
||||
- `POST /v2/token`
|
||||
- `POST /v2/bot/import/json`
|
||||
- `POST /v2/async/fetch`
|
||||
- `POST /v2/bot/publish`
|
||||
- `POST /v2/bot/effective_progress`
|
||||
- `POST /v2/bot/query`
|
||||
|
||||
## 真实接口验证
|
||||
|
||||
默认单元测试不会访问真实微信接口。如需验证 token 和 query 只读链路,可设置以下环境变量后运行:
|
||||
|
||||
```bash
|
||||
AISPEECH_INTEGRATION=1
|
||||
AISPEECH_APPID=xxx
|
||||
AISPEECH_TOKEN=xxx
|
||||
AISPEECH_AES_KEY=xxx
|
||||
go test ./aispeech/dialog -run TestIntegrationAccessTokenAndQuery
|
||||
```
|
||||
|
||||
如需验证导入、异步任务查询、发布、发布进度查询和命中回答的完整链路,可显式开启变更型测试。该测试会导入一个 `CodexSmokeTest` 临时问答并发布到线上机器人:
|
||||
|
||||
```bash
|
||||
AISPEECH_INTEGRATION_MUTATION=1
|
||||
AISPEECH_APPID=xxx
|
||||
AISPEECH_TOKEN=xxx
|
||||
AISPEECH_AES_KEY=xxx
|
||||
go test ./aispeech/dialog -run TestIntegrationFullDialogFlow -v
|
||||
```
|
||||
## 快速入门
|
||||
@@ -1,60 +0,0 @@
|
||||
package aispeech
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/aispeech/context"
|
||||
"github.com/silenceper/wechat/v2/aispeech/dialog"
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
)
|
||||
|
||||
// AISpeech 微信智能对话相关 API.
|
||||
type AISpeech struct {
|
||||
ctx *context.Context
|
||||
dialog *dialog.Dialog
|
||||
}
|
||||
|
||||
// NewAISpeech 实例化智能对话 API.
|
||||
func NewAISpeech(cfg *config.Config) *AISpeech {
|
||||
ctx := &context.Context{
|
||||
Config: cfg,
|
||||
AccessTokenContextHandle: dialog.NewAccessToken(cfg),
|
||||
}
|
||||
return &AISpeech{ctx: ctx}
|
||||
}
|
||||
|
||||
// GetContext get Context.
|
||||
func (a *AISpeech) GetContext() *context.Context {
|
||||
return a.ctx
|
||||
}
|
||||
|
||||
// SetAccessTokenHandle 自定义 access_token 获取方式.
|
||||
func (a *AISpeech) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
|
||||
a.ctx.AccessTokenContextHandle = credential.AccessTokenCompatibleHandle{
|
||||
AccessTokenHandle: accessTokenHandle,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccessTokenContextHandle 自定义 access_token 获取方式.
|
||||
func (a *AISpeech) SetAccessTokenContextHandle(accessTokenContextHandle credential.AccessTokenContextHandle) {
|
||||
a.ctx.AccessTokenContextHandle = accessTokenContextHandle
|
||||
}
|
||||
|
||||
// GetAccessToken 获取 access token.
|
||||
func (a *AISpeech) GetAccessToken() (string, error) {
|
||||
return a.ctx.GetAccessToken()
|
||||
}
|
||||
|
||||
// GetAccessTokenContext 获取 access token.
|
||||
func (a *AISpeech) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
|
||||
return a.ctx.GetAccessTokenContext(ctx)
|
||||
}
|
||||
|
||||
// GetDialog 获取对话平台 API.
|
||||
func (a *AISpeech) GetDialog() *dialog.Dialog {
|
||||
if a.dialog == nil {
|
||||
a.dialog = dialog.NewDialog(a.ctx)
|
||||
}
|
||||
return a.dialog
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package aispeech
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"testing"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
type staticAccessToken struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (s staticAccessToken) GetAccessToken() (string, error) {
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
type staticAccessTokenContext struct {
|
||||
token string
|
||||
}
|
||||
|
||||
type contextTokenKey struct{}
|
||||
|
||||
func (s staticAccessTokenContext) GetAccessToken() (string, error) {
|
||||
return s.GetAccessTokenContext(stdcontext.Background())
|
||||
}
|
||||
|
||||
func (s staticAccessTokenContext) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
|
||||
if v := ctx.Value(contextTokenKey{}); v != nil {
|
||||
return v.(string), nil
|
||||
}
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
func TestSetAccessTokenHandle(t *testing.T) {
|
||||
ai := NewAISpeech(&config.Config{Cache: cache.NewMemory()})
|
||||
ai.SetAccessTokenHandle(staticAccessToken{token: "custom-token"})
|
||||
|
||||
token, err := ai.GetAccessToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessToken error: %v", err)
|
||||
}
|
||||
if token != "custom-token" {
|
||||
t.Fatalf("bad token: %s", token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAccessTokenContextHandle(t *testing.T) {
|
||||
ai := NewAISpeech(&config.Config{Cache: cache.NewMemory()})
|
||||
ai.SetAccessTokenContextHandle(staticAccessTokenContext{token: "custom-token"})
|
||||
|
||||
ctx := stdcontext.WithValue(stdcontext.Background(), contextTokenKey{}, "context-token")
|
||||
token, err := ai.GetAccessTokenContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessTokenContext error: %v", err)
|
||||
}
|
||||
if token != "context-token" {
|
||||
t.Fatalf("bad token: %s", token)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package config
|
||||
|
||||
import "github.com/silenceper/wechat/v2/cache"
|
||||
|
||||
const defaultBaseURL = "https://openaiapi.weixin.qq.com"
|
||||
|
||||
// Config for 微信智能对话.
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
Token string `json:"token"`
|
||||
AESKey string `json:"aes_key"`
|
||||
Account string `json:"account"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Cache cache.Cache
|
||||
}
|
||||
|
||||
// GetBaseURL returns the API base URL.
|
||||
func (cfg *Config) GetBaseURL() string {
|
||||
if cfg.BaseURL == "" {
|
||||
return defaultBaseURL
|
||||
}
|
||||
return cfg.BaseURL
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetBaseURL(t *testing.T) {
|
||||
cfg := &Config{}
|
||||
if cfg.GetBaseURL() != defaultBaseURL {
|
||||
t.Fatalf("bad default base url: %s", cfg.GetBaseURL())
|
||||
}
|
||||
|
||||
cfg.BaseURL = "http://example.com"
|
||||
if cfg.GetBaseURL() != "http://example.com" {
|
||||
t.Fatalf("bad custom base url: %s", cfg.GetBaseURL())
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
*config.Config
|
||||
credential.AccessTokenContextHandle
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/encryptor"
|
||||
)
|
||||
|
||||
// GetAccessToken 获取 access token.
|
||||
func (d *Dialog) GetAccessToken() (string, error) {
|
||||
return d.Context.GetAccessToken()
|
||||
}
|
||||
|
||||
// GetAccessTokenContext 获取 access token.
|
||||
func (d *Dialog) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
|
||||
return d.Context.GetAccessTokenContext(ctx)
|
||||
}
|
||||
|
||||
// ImportJSON 简单问答导入.
|
||||
func (d *Dialog) ImportJSON(req *ImportJSONRequest) (*ImportJSONResponse, error) {
|
||||
return d.ImportJSONContext(stdcontext.Background(), req)
|
||||
}
|
||||
|
||||
// ImportJSONContext 简单问答导入.
|
||||
func (d *Dialog) ImportJSONContext(ctx stdcontext.Context, req *ImportJSONRequest) (*ImportJSONResponse, error) {
|
||||
var res ImportJSONResponse
|
||||
requestID, err := d.postJSON(ctx, importJSONPath, req, &res, "AISpeechImportJSON")
|
||||
res.RequestID = requestID
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// Publish 发布机器人.
|
||||
func (d *Dialog) Publish() (*PublishResponse, error) {
|
||||
return d.PublishContext(stdcontext.Background())
|
||||
}
|
||||
|
||||
// PublishContext 发布机器人.
|
||||
func (d *Dialog) PublishContext(ctx stdcontext.Context) (*PublishResponse, error) {
|
||||
var res PublishResponse
|
||||
requestID, err := d.postEmpty(ctx, publishPath, &res, "AISpeechPublish")
|
||||
res.RequestID = requestID
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// GetEffectiveProgress 查询机器人发布进度.
|
||||
func (d *Dialog) GetEffectiveProgress(req *EffectiveProgressRequest) (*EffectiveProgressResponse, error) {
|
||||
return d.GetEffectiveProgressContext(stdcontext.Background(), req)
|
||||
}
|
||||
|
||||
// GetEffectiveProgressContext 查询机器人发布进度.
|
||||
func (d *Dialog) GetEffectiveProgressContext(ctx stdcontext.Context, req *EffectiveProgressRequest) (*EffectiveProgressResponse, error) {
|
||||
var res EffectiveProgressResponse
|
||||
requestID, err := d.postJSON(ctx, effectiveProgressPath, req, &res, "AISpeechGetEffectiveProgress")
|
||||
res.RequestID = requestID
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// FetchAsync 查询异步任务.
|
||||
func (d *Dialog) FetchAsync(req *FetchAsyncRequest) (*FetchAsyncResponse, error) {
|
||||
return d.FetchAsyncContext(stdcontext.Background(), req)
|
||||
}
|
||||
|
||||
// FetchAsyncContext 查询异步任务.
|
||||
func (d *Dialog) FetchAsyncContext(ctx stdcontext.Context, req *FetchAsyncRequest) (*FetchAsyncResponse, error) {
|
||||
var res FetchAsyncResponse
|
||||
requestID, err := d.postJSON(ctx, fetchAsyncPath, req, &res, "AISpeechFetchAsync")
|
||||
res.RequestID = requestID
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// Query 调用智能对话.
|
||||
func (d *Dialog) Query(req *QueryRequest) (*QueryResponse, error) {
|
||||
return d.QueryContext(stdcontext.Background(), req)
|
||||
}
|
||||
|
||||
// QueryContext 调用智能对话.
|
||||
func (d *Dialog) QueryContext(ctx stdcontext.Context, req *QueryRequest) (*QueryResponse, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedBody, err := encryptor.Encrypt(d.AESKey, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessToken, err := d.GetAccessTokenContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if accessToken == "" {
|
||||
return nil, errEmptyAccessToken
|
||||
}
|
||||
response, err := post(ctx, d.Config, queryPath, []byte(encryptedBody), "text/plain", accessToken, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plainResponse := response
|
||||
if !json.Valid(response) {
|
||||
plainResponse, err = encryptor.Decrypt(d.AESKey, string(response))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var res QueryResponse
|
||||
requestID, err := decodeResponse(plainResponse, &res, "AISpeechQuery")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.RequestID = requestID
|
||||
if json.Valid([]byte(res.Answer)) {
|
||||
res.RawAnswer = json.RawMessage(res.Answer)
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/aispeech/context"
|
||||
"github.com/silenceper/wechat/v2/aispeech/encryptor"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// tokenPath 获取 AccessToken.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
|
||||
tokenPath = "/v2/token"
|
||||
// importJSONPath 简单问答 JSON 导入.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/import.html
|
||||
importJSONPath = "/v2/bot/import/json"
|
||||
// fetchAsyncPath 异步任务查询.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/fetch.html
|
||||
fetchAsyncPath = "/v2/async/fetch"
|
||||
// publishPath 发布机器人.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/publish.html
|
||||
publishPath = "/v2/bot/publish"
|
||||
// effectiveProgressPath 查询机器人发布进度.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/progress.html
|
||||
effectiveProgressPath = "/v2/bot/effective_progress"
|
||||
// queryPath 调用智能对话.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/query.html
|
||||
queryPath = "/v2/bot/query"
|
||||
)
|
||||
|
||||
// Dialog 智能对话平台.
|
||||
type Dialog struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewDialog init.
|
||||
func NewDialog(ctx *context.Context) *Dialog {
|
||||
return &Dialog{ctx}
|
||||
}
|
||||
|
||||
func (d *Dialog) postJSON(ctx stdcontext.Context, path string, req interface{}, res interface{}, apiName string) (string, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
accessToken, err := d.GetAccessTokenContext(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if accessToken == "" {
|
||||
return "", errEmptyAccessToken
|
||||
}
|
||||
response, err := post(ctx, d.Config, path, body, "application/json", accessToken, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decodeResponse(response, res, apiName)
|
||||
}
|
||||
|
||||
func (d *Dialog) postEmpty(ctx stdcontext.Context, path string, res interface{}, apiName string) (string, error) {
|
||||
accessToken, err := d.GetAccessTokenContext(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if accessToken == "" {
|
||||
return "", errEmptyAccessToken
|
||||
}
|
||||
response, err := post(ctx, d.Config, path, nil, "application/json", accessToken, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decodeResponse(response, res, apiName)
|
||||
}
|
||||
|
||||
func post(ctx stdcontext.Context, cfg *config.Config, path string, body []byte, contentType, accessToken, appID string) ([]byte, error) {
|
||||
timestamp := time.Now().Unix()
|
||||
nonce, err := randomString(16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestID, err := randomString(32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := map[string]string{
|
||||
"request_id": requestID,
|
||||
"timestamp": fmt.Sprintf("%d", timestamp),
|
||||
"nonce": nonce,
|
||||
"sign": encryptor.Sign(cfg.Token, timestamp, nonce, body),
|
||||
"Content-Type": contentType,
|
||||
}
|
||||
if accessToken != "" {
|
||||
header["X-OPENAI-TOKEN"] = accessToken
|
||||
} else {
|
||||
header["X-APPID"] = appID
|
||||
}
|
||||
|
||||
return util.HTTPPostContext(ctx, buildURL(cfg.GetBaseURL(), path), body, header)
|
||||
}
|
||||
|
||||
func buildURL(baseURL, path string) string {
|
||||
return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/")
|
||||
}
|
||||
|
||||
func randomString(length int) (string, error) {
|
||||
buf := make([]byte, (length+1)/2)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(buf)[:length], nil
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
aispeechContext "github.com/silenceper/wechat/v2/aispeech/context"
|
||||
"github.com/silenceper/wechat/v2/aispeech/encryptor"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
testAccessToken = "access-token"
|
||||
testAccount = "admin"
|
||||
testAESKey = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz"
|
||||
testAppID = "appid"
|
||||
testImportRequest = "import-rid"
|
||||
testImportTaskID = "task-import"
|
||||
testPublishTaskID = "task-publish"
|
||||
testQuery = "hello"
|
||||
testQueryAnswer = "hello answer"
|
||||
testQueryRequestID = "query-rid"
|
||||
testToken = "token"
|
||||
testTokenRequestID = "token-rid"
|
||||
)
|
||||
|
||||
type emptyAccessTokenHandle struct{}
|
||||
|
||||
func (emptyAccessTokenHandle) GetAccessToken() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (emptyAccessTokenHandle) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func TestAccessTokenCache(t *testing.T) {
|
||||
var tokenRequests int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenRequests++
|
||||
body := readBody(t, r)
|
||||
assertSign(t, r, testToken, body)
|
||||
if r.Header.Get("X-APPID") != testAppID {
|
||||
t.Fatalf("bad X-APPID: %s", r.Header.Get("X-APPID"))
|
||||
}
|
||||
_, _ = w.Write([]byte(accessTokenResponse("rid")))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ak := NewAccessToken(&config.Config{
|
||||
AppID: testAppID,
|
||||
Token: testToken,
|
||||
Account: testAccount,
|
||||
BaseURL: srv.URL,
|
||||
Cache: cache.NewMemory(),
|
||||
})
|
||||
|
||||
token, err := ak.GetAccessToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessToken error: %v", err)
|
||||
}
|
||||
if token != testAccessToken {
|
||||
t.Fatalf("bad token: %s", token)
|
||||
}
|
||||
token, err = ak.GetAccessToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessToken second error: %v", err)
|
||||
}
|
||||
if token != testAccessToken {
|
||||
t.Fatalf("bad token second: %s", token)
|
||||
}
|
||||
if tokenRequests != 1 {
|
||||
t.Fatalf("token requests = %d, want 1", tokenRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessTokenCacheByAccount(t *testing.T) {
|
||||
var tokenRequests int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenRequests++
|
||||
body := readBody(t, r)
|
||||
assertSign(t, r, testToken, body)
|
||||
|
||||
var req AccessTokenRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Fatalf("bad token body: %v", err)
|
||||
}
|
||||
_, _ = w.Write([]byte(accountAccessTokenResponse(req.Account)))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
memory := cache.NewMemory()
|
||||
cfgA := testDialogConfig(srv.URL)
|
||||
cfgA.Account = "admin-a"
|
||||
cfgA.Cache = memory
|
||||
cfgB := testDialogConfig(srv.URL)
|
||||
cfgB.Account = "admin-b"
|
||||
cfgB.Cache = memory
|
||||
|
||||
tokenA, err := NewAccessToken(cfgA).GetAccessToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessToken A error: %v", err)
|
||||
}
|
||||
tokenB, err := NewAccessToken(cfgB).GetAccessToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessToken B error: %v", err)
|
||||
}
|
||||
if tokenA != "admin-a-token" || tokenB != "admin-b-token" {
|
||||
t.Fatalf("bad tokens: %s %s", tokenA, tokenB)
|
||||
}
|
||||
if tokenRequests != 2 {
|
||||
t.Fatalf("token requests = %d, want 2", tokenRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessTokenEmptyToken(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"rid","data":{"access_token":""}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ak := NewAccessToken(&config.Config{
|
||||
AppID: testAppID,
|
||||
Token: testToken,
|
||||
BaseURL: srv.URL,
|
||||
Cache: cache.NewMemory(),
|
||||
})
|
||||
|
||||
if _, err := ak.GetAccessToken(); err == nil {
|
||||
t.Fatal("GetAccessToken should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogAPIs(t *testing.T) {
|
||||
srv := newDialogAPITestServer(t)
|
||||
defer srv.Close()
|
||||
|
||||
d := newTestDialog(srv.URL)
|
||||
assertDialogImportJSON(t, d)
|
||||
assertDialogPublish(t, d)
|
||||
assertDialogProgress(t, d)
|
||||
assertDialogFetchAsync(t, d)
|
||||
assertDialogQuery(t, d)
|
||||
}
|
||||
|
||||
func TestDialogEmptyAccessToken(t *testing.T) {
|
||||
d := NewDialog(&aispeechContext.Context{
|
||||
Config: &config.Config{
|
||||
AESKey: testAESKey,
|
||||
},
|
||||
AccessTokenContextHandle: emptyAccessTokenHandle{},
|
||||
})
|
||||
|
||||
if _, err := d.ImportJSON(&ImportJSONRequest{}); err == nil {
|
||||
t.Fatal("ImportJSON should return error")
|
||||
}
|
||||
if _, err := d.Publish(); err == nil {
|
||||
t.Fatal("Publish should return error")
|
||||
}
|
||||
if _, err := d.Query(&QueryRequest{}); err == nil {
|
||||
t.Fatal("Query should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogAPIError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case tokenPath:
|
||||
_, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
|
||||
case importJSONPath:
|
||||
_, _ = w.Write([]byte(`{"code":210202,"msg":"forbidden","request_id":"import-rid","data":{"reason":"forbidden"}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := newTestDialog(srv.URL)
|
||||
|
||||
_, err := d.ImportJSON(&ImportJSONRequest{})
|
||||
if err == nil {
|
||||
t.Fatal("ImportJSON should return error")
|
||||
}
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok {
|
||||
t.Fatalf("ImportJSON error should be *APIError but %T", err)
|
||||
}
|
||||
if apiErr.Code != 210202 || apiErr.Msg != "forbidden" || apiErr.RequestID != testImportRequest {
|
||||
t.Fatalf("bad api error: %+v", apiErr)
|
||||
}
|
||||
if string(apiErr.Data) != `{"reason":"forbidden"}` {
|
||||
t.Fatalf("bad api error data: %s", apiErr.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryPlainJSONError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case tokenPath:
|
||||
_, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
|
||||
case queryPath:
|
||||
_, _ = w.Write([]byte(`{"code":110002,"msg":"bad param","request_id":"query-rid"}`))
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := newTestDialog(srv.URL)
|
||||
|
||||
_, err := d.Query(&QueryRequest{Query: testQuery})
|
||||
if err == nil {
|
||||
t.Fatal("Query should return error")
|
||||
}
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok {
|
||||
t.Fatalf("Query error should be *APIError but %T", err)
|
||||
}
|
||||
if apiErr.Code != 110002 || apiErr.RequestID != testQueryRequestID {
|
||||
t.Fatalf("bad api error: %+v", apiErr)
|
||||
}
|
||||
}
|
||||
|
||||
func newDialogAPITestServer(t *testing.T) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := readBody(t, r)
|
||||
assertSign(t, r, testToken, body)
|
||||
switch r.URL.Path {
|
||||
case tokenPath:
|
||||
handleDialogToken(t, w, r)
|
||||
case importJSONPath:
|
||||
handleDialogImport(t, w, r, body)
|
||||
case publishPath:
|
||||
handleDialogPublish(t, w, r, body)
|
||||
case effectiveProgressPath:
|
||||
assertToken(t, r)
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"progress-rid","data":{"end_time":"","progress":100,"status":1}}`))
|
||||
case fetchAsyncPath:
|
||||
assertToken(t, r)
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"fetch-rid","data":{"state":2,"msg":"","progress":100,"start":1,"end":2,"url":"","success_skill_info":[{"id":1,"name":"AAA","intents":[{"id":2,"name":"BBB"}]}]}}`))
|
||||
case queryPath:
|
||||
handleDialogQuery(t, w, r, body)
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func handleDialogToken(t *testing.T, w io.Writer, r *http.Request) {
|
||||
t.Helper()
|
||||
if r.Header.Get("X-APPID") != testAppID {
|
||||
t.Fatalf("bad X-APPID: %s", r.Header.Get("X-APPID"))
|
||||
}
|
||||
_, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
|
||||
}
|
||||
|
||||
func handleDialogImport(t *testing.T, w io.Writer, r *http.Request, body []byte) {
|
||||
t.Helper()
|
||||
assertToken(t, r)
|
||||
var req ImportJSONRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Fatalf("bad import body: %v", err)
|
||||
}
|
||||
if len(req.Data) != 1 || req.Data[0].Skill != "pre-sale" {
|
||||
t.Fatalf("bad import request: %+v", req)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"import-rid","data":{"task_id":"task-import"}}`))
|
||||
}
|
||||
|
||||
func handleDialogPublish(t *testing.T, w io.Writer, r *http.Request, body []byte) {
|
||||
t.Helper()
|
||||
assertToken(t, r)
|
||||
if len(body) != 0 {
|
||||
t.Fatalf("publish body should be empty: %s", body)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"publish-rid","data":{"task_id":"task-publish"}}`))
|
||||
}
|
||||
|
||||
func handleDialogQuery(t *testing.T, w io.Writer, r *http.Request, body []byte) {
|
||||
t.Helper()
|
||||
assertToken(t, r)
|
||||
if !strings.HasPrefix(r.Header.Get("Content-Type"), "text/plain") {
|
||||
t.Fatalf("bad content type: %s", r.Header.Get("Content-Type"))
|
||||
}
|
||||
plain, err := encryptor.Decrypt(testAESKey, string(body))
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt query body error: %v", err)
|
||||
}
|
||||
var req QueryRequest
|
||||
if err = json.Unmarshal(plain, &req); err != nil {
|
||||
t.Fatalf("bad query body: %v", err)
|
||||
}
|
||||
if req.Query != testQuery {
|
||||
t.Fatalf("bad query: %+v", req)
|
||||
}
|
||||
_, _ = w.Write([]byte(encryptQueryResponse(t)))
|
||||
}
|
||||
|
||||
func assertDialogImportJSON(t *testing.T, d *Dialog) {
|
||||
t.Helper()
|
||||
res, err := d.ImportJSON(&ImportJSONRequest{
|
||||
Mode: 0,
|
||||
Data: []BotIntent{{
|
||||
Skill: "pre-sale",
|
||||
Intent: "business-hours",
|
||||
Disable: false,
|
||||
Questions: []string{"when are you open"},
|
||||
Answers: []string{"9:00-18:00"},
|
||||
}},
|
||||
})
|
||||
if err != nil || res.TaskID != testImportTaskID || res.RequestID != testImportRequest {
|
||||
t.Fatalf("ImportJSON = %+v, %v", res, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDialogPublish(t *testing.T, d *Dialog) {
|
||||
t.Helper()
|
||||
res, err := d.Publish()
|
||||
if err != nil || res.TaskID != testPublishTaskID {
|
||||
t.Fatalf("Publish = %+v, %v", res, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDialogProgress(t *testing.T, d *Dialog) {
|
||||
t.Helper()
|
||||
res, err := d.GetEffectiveProgress(&EffectiveProgressRequest{Env: "online"})
|
||||
if err != nil || res.Progress != 100 {
|
||||
t.Fatalf("GetEffectiveProgress = %+v, %v", res, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDialogFetchAsync(t *testing.T, d *Dialog) {
|
||||
t.Helper()
|
||||
res, err := d.FetchAsync(&FetchAsyncRequest{TaskID: testImportTaskID})
|
||||
if err != nil || res.State != 2 || len(res.SuccessSkillInfo) != 1 {
|
||||
t.Fatalf("FetchAsync = %+v, %v", res, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDialogQuery(t *testing.T, d *Dialog) {
|
||||
t.Helper()
|
||||
res, err := d.Query(&QueryRequest{Query: testQuery, Env: "online"})
|
||||
if err != nil || res.Answer != testQueryAnswer || res.RequestID != testQueryRequestID {
|
||||
t.Fatalf("Query = %+v, %v", res, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestDialog(baseURL string) *Dialog {
|
||||
cfg := testDialogConfig(baseURL)
|
||||
return NewDialog(&aispeechContext.Context{
|
||||
Config: cfg,
|
||||
AccessTokenContextHandle: NewAccessToken(cfg),
|
||||
})
|
||||
}
|
||||
|
||||
func testDialogConfig(baseURL string) *config.Config {
|
||||
return &config.Config{
|
||||
AppID: testAppID,
|
||||
Token: testToken,
|
||||
AESKey: testAESKey,
|
||||
Account: testAccount,
|
||||
BaseURL: baseURL,
|
||||
Cache: cache.NewMemory(),
|
||||
}
|
||||
}
|
||||
|
||||
func accountAccessTokenResponse(account string) string {
|
||||
return `{"code":0,"msg":"success","request_id":"rid","data":{"access_token":"` + account + `-token"}}`
|
||||
}
|
||||
|
||||
func accessTokenResponse(requestID string) string {
|
||||
return `{"code":0,"msg":"success","request_id":"` + requestID + `","data":{"access_token":"` + testAccessToken + `"}}`
|
||||
}
|
||||
|
||||
func encryptQueryResponse(t *testing.T) string {
|
||||
t.Helper()
|
||||
cipherText, err := encryptor.Encrypt(testAESKey, []byte(queryResponse()))
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt response error: %v", err)
|
||||
}
|
||||
return cipherText
|
||||
}
|
||||
|
||||
func queryResponse() string {
|
||||
return `{"code":0,"msg":"success","request_id":"query-rid","data":{"answer":"hello answer","answer_type":"text","skill_name":"skill","intent_name":"intent","msg_id":"msg","status":"FAQ","slots":[{"name":"n","value":"v","norm":"v"}]}}`
|
||||
}
|
||||
|
||||
func readBody(t *testing.T, r *http.Request) []byte {
|
||||
t.Helper()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body error: %v", err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func assertToken(t *testing.T, r *http.Request) {
|
||||
t.Helper()
|
||||
if r.Header.Get("X-OPENAI-TOKEN") != testAccessToken {
|
||||
t.Fatalf("bad X-OPENAI-TOKEN: %s", r.Header.Get("X-OPENAI-TOKEN"))
|
||||
}
|
||||
}
|
||||
|
||||
func assertSign(t *testing.T, r *http.Request, token string, body []byte) {
|
||||
t.Helper()
|
||||
timestamp, err := strconv.ParseInt(r.Header.Get("timestamp"), 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("bad timestamp: %v", err)
|
||||
}
|
||||
want := encryptor.Sign(token, timestamp, r.Header.Get("nonce"), body)
|
||||
if got := r.Header.Get("sign"); got != want {
|
||||
t.Fatalf("bad sign: got %s want %s body %s", got, want, body)
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
aispeechContext "github.com/silenceper/wechat/v2/aispeech/context"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
func TestIntegrationAccessTokenAndQuery(t *testing.T) {
|
||||
if os.Getenv("AISPEECH_INTEGRATION") != "1" {
|
||||
t.Skip("set AISPEECH_INTEGRATION=1 to run real aispeech API test")
|
||||
}
|
||||
|
||||
cfg := &config.Config{
|
||||
AppID: os.Getenv("AISPEECH_APPID"),
|
||||
Token: os.Getenv("AISPEECH_TOKEN"),
|
||||
AESKey: os.Getenv("AISPEECH_AES_KEY"),
|
||||
Cache: cache.NewMemory(),
|
||||
}
|
||||
if cfg.AppID == "" || cfg.Token == "" || cfg.AESKey == "" {
|
||||
t.Fatal("AISPEECH_APPID, AISPEECH_TOKEN and AISPEECH_AES_KEY are required")
|
||||
}
|
||||
|
||||
d := NewDialog(&aispeechContext.Context{
|
||||
Config: cfg,
|
||||
AccessTokenContextHandle: NewAccessToken(cfg),
|
||||
})
|
||||
|
||||
accessToken, err := d.GetAccessToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessToken error: %v", err)
|
||||
}
|
||||
if accessToken == "" {
|
||||
t.Fatal("access token is empty")
|
||||
}
|
||||
|
||||
res, err := d.Query(&QueryRequest{
|
||||
Query: "你好",
|
||||
Env: "online",
|
||||
UserID: "gowechat-integration-test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Query error: %v", err)
|
||||
}
|
||||
if res.RequestID == "" {
|
||||
t.Fatal("query request_id is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationFullDialogFlow(t *testing.T) {
|
||||
if os.Getenv("AISPEECH_INTEGRATION_MUTATION") != "1" {
|
||||
t.Skip("set AISPEECH_INTEGRATION_MUTATION=1 to run import/publish/query real aispeech API test")
|
||||
}
|
||||
|
||||
d := newIntegrationDialog(t)
|
||||
now := time.Now().Unix()
|
||||
question := "codex-smoke-test-question-" + time.Unix(now, 0).Format("20060102150405")
|
||||
answer := "codex-smoke-test-answer-" + time.Unix(now, 0).Format("20060102150405")
|
||||
|
||||
importRes, err := d.ImportJSON(&ImportJSONRequest{
|
||||
Mode: 0,
|
||||
Data: []BotIntent{{
|
||||
Skill: "CodexSmokeTest",
|
||||
Intent: question,
|
||||
Disable: false,
|
||||
Questions: []string{question},
|
||||
Answers: []string{answer},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ImportJSON error: %v", err)
|
||||
}
|
||||
if importRes.TaskID == "" {
|
||||
t.Fatal("ImportJSON task_id is empty")
|
||||
}
|
||||
t.Logf("ImportJSON request_id=%s task_id=%s", importRes.RequestID, importRes.TaskID)
|
||||
|
||||
importTask := waitAsyncTask(t, d, importRes.TaskID, 2*time.Minute)
|
||||
if importTask.State != 2 {
|
||||
t.Fatalf("import task failed: state=%d msg=%s progress=%d", importTask.State, importTask.Msg, importTask.Progress)
|
||||
}
|
||||
t.Logf("FetchAsync import request_id=%s state=%d progress=%d", importTask.RequestID, importTask.State, importTask.Progress)
|
||||
|
||||
publishRes, err := d.Publish()
|
||||
if err != nil {
|
||||
t.Fatalf("Publish error: %v", err)
|
||||
}
|
||||
if publishRes.TaskID == "" {
|
||||
t.Fatal("Publish task_id is empty")
|
||||
}
|
||||
t.Logf("Publish request_id=%s task_id=%s", publishRes.RequestID, publishRes.TaskID)
|
||||
|
||||
progress := waitEffectiveProgress(t, d, "online", 3*time.Minute)
|
||||
if progress.Status != 1 {
|
||||
t.Fatalf("publish progress failed: status=%d progress=%d", progress.Status, progress.Progress)
|
||||
}
|
||||
t.Logf("GetEffectiveProgress request_id=%s status=%d progress=%d", progress.RequestID, progress.Status, progress.Progress)
|
||||
|
||||
queryRes := waitQueryAnswer(t, d, question, answer, 2*time.Minute)
|
||||
t.Logf("Query request_id=%s status=%s answer_type=%s answer=%s", queryRes.RequestID, queryRes.Status, queryRes.AnswerType, queryRes.Answer)
|
||||
}
|
||||
|
||||
func newIntegrationDialog(t *testing.T) *Dialog {
|
||||
t.Helper()
|
||||
|
||||
cfg := &config.Config{
|
||||
AppID: os.Getenv("AISPEECH_APPID"),
|
||||
Token: os.Getenv("AISPEECH_TOKEN"),
|
||||
AESKey: os.Getenv("AISPEECH_AES_KEY"),
|
||||
Account: os.Getenv("AISPEECH_ACCOUNT"),
|
||||
Cache: cache.NewMemory(),
|
||||
}
|
||||
if cfg.AppID == "" || cfg.Token == "" || cfg.AESKey == "" {
|
||||
t.Fatal("AISPEECH_APPID, AISPEECH_TOKEN and AISPEECH_AES_KEY are required")
|
||||
}
|
||||
|
||||
return NewDialog(&aispeechContext.Context{
|
||||
Config: cfg,
|
||||
AccessTokenContextHandle: NewAccessToken(cfg),
|
||||
})
|
||||
}
|
||||
|
||||
func waitAsyncTask(t *testing.T, d *Dialog, taskID string, timeout time.Duration) *FetchAsyncResponse {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
res, err := d.FetchAsync(&FetchAsyncRequest{TaskID: taskID})
|
||||
if err != nil {
|
||||
t.Fatalf("FetchAsync error: %v", err)
|
||||
}
|
||||
if res.State == 2 || res.State == 3 {
|
||||
return res
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("FetchAsync timeout: state=%d progress=%d msg=%s", res.State, res.Progress, res.Msg)
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func waitEffectiveProgress(t *testing.T, d *Dialog, env string, timeout time.Duration) *EffectiveProgressResponse {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
res, err := d.GetEffectiveProgress(&EffectiveProgressRequest{Env: env})
|
||||
if err != nil {
|
||||
t.Fatalf("GetEffectiveProgress error: %v", err)
|
||||
}
|
||||
if res.Status == 1 || res.Status == 2 {
|
||||
return res
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("GetEffectiveProgress timeout: status=%d progress=%d", res.Status, res.Progress)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func waitQueryAnswer(t *testing.T, d *Dialog, question, answer string, timeout time.Duration) *QueryResponse {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
res, err := d.Query(&QueryRequest{
|
||||
Query: question,
|
||||
Env: "online",
|
||||
UserID: "gowechat-full-integration-test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Query error: %v", err)
|
||||
}
|
||||
if res.Answer == answer {
|
||||
return res
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("Query timeout: status=%s answer=%s want=%s", res.Status, res.Answer, answer)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package dialog
|
||||
|
||||
// AccessTokenRequest 获取 access token 请求.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
|
||||
type AccessTokenRequest struct {
|
||||
Account string `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
// ImportJSONRequest 简单问答导入请求.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/import.html
|
||||
type ImportJSONRequest struct {
|
||||
Mode int `json:"mode"`
|
||||
Data []BotIntent `json:"data"`
|
||||
}
|
||||
|
||||
// BotIntent 简单问答意图.
|
||||
type BotIntent struct {
|
||||
Skill string `json:"skill"`
|
||||
Intent string `json:"intent"`
|
||||
Threshold string `json:"threshold,omitempty"`
|
||||
Disable bool `json:"disable"`
|
||||
Questions []string `json:"questions"`
|
||||
Answers []string `json:"answers"`
|
||||
}
|
||||
|
||||
// EffectiveProgressRequest 发布进度查询请求.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/progress.html
|
||||
type EffectiveProgressRequest struct {
|
||||
Env string `json:"env"`
|
||||
}
|
||||
|
||||
// FetchAsyncRequest 异步任务查询请求.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/fetch.html
|
||||
type FetchAsyncRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
// QueryRequest 调用智能对话请求.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/query.html
|
||||
type QueryRequest struct {
|
||||
Query string `json:"query"`
|
||||
Env string `json:"env,omitempty"`
|
||||
FirstPrioritySkills []string `json:"first_priority_skills,omitempty"`
|
||||
SecondPrioritySkills []string `json:"second_priority_skills,omitempty"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
UserID string `json:"userid,omitempty"`
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type apiResponse struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Code int64 `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// APIError 智能对话接口错误.
|
||||
type APIError struct {
|
||||
APIName string
|
||||
Code int64
|
||||
Msg string
|
||||
RequestID string
|
||||
Data json.RawMessage
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("%s Error , code=%d , msg=%s , request_id=%s", e.APIName, e.Code, e.Msg, e.RequestID)
|
||||
}
|
||||
|
||||
type accessTokenData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// ImportJSONResponse 简单问答导入响应.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/import.html
|
||||
type ImportJSONResponse struct {
|
||||
RequestID string `json:"-"`
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
// PublishResponse 发布响应.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/publish.html
|
||||
type PublishResponse struct {
|
||||
RequestID string `json:"-"`
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
// EffectiveProgressResponse 发布进度查询响应.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/progress.html
|
||||
type EffectiveProgressResponse struct {
|
||||
RequestID string `json:"-"`
|
||||
EndTime string `json:"end_time"`
|
||||
Progress int `json:"progress"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// FetchAsyncResponse 异步任务查询响应.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/fetch.html
|
||||
type FetchAsyncResponse struct {
|
||||
RequestID string `json:"-"`
|
||||
State int `json:"state"`
|
||||
Msg string `json:"msg"`
|
||||
Progress int `json:"progress"`
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
URL string `json:"url"`
|
||||
TotalCount int `json:"total_count"`
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailCount int `json:"fail_count"`
|
||||
ReplaceCount int `json:"replace_count"`
|
||||
SuccessUploadCount int `json:"success_upload_count"`
|
||||
SuccessSkillInfo []SkillInfo `json:"success_skill_info"`
|
||||
FailSkillInfo []SkillInfo `json:"fail_skill_info"`
|
||||
}
|
||||
|
||||
// SkillInfo 技能信息.
|
||||
type SkillInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Intents []IntentInfo `json:"intents"`
|
||||
}
|
||||
|
||||
// IntentInfo 意图信息.
|
||||
type IntentInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// QueryResponse 调用智能对话响应.
|
||||
// 官方文档: https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/bot/query.html
|
||||
type QueryResponse struct {
|
||||
RequestID string `json:"-"`
|
||||
Answer string `json:"answer"`
|
||||
RawAnswer json.RawMessage `json:"-"`
|
||||
AnswerType string `json:"answer_type"`
|
||||
SkillName string `json:"skill_name"`
|
||||
IntentName string `json:"intent_name"`
|
||||
MsgID string `json:"msg_id"`
|
||||
Options []Option `json:"options"`
|
||||
Status string `json:"status"`
|
||||
Slots []SlotDetail `json:"slots"`
|
||||
}
|
||||
|
||||
// Option 推荐问题.
|
||||
type Option struct {
|
||||
AnsNodeName string `json:"ans_node_name"`
|
||||
Title string `json:"title"`
|
||||
Answer string `json:"answer"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
// SlotDetail 槽位数据.
|
||||
type SlotDetail struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Norm string `json:"norm"`
|
||||
}
|
||||
|
||||
func decodeResponse(response []byte, data interface{}, apiName string) (string, error) {
|
||||
var res apiResponse
|
||||
if err := json.Unmarshal(response, &res); err != nil {
|
||||
return "", fmt.Errorf("json Unmarshal Error, err=%v", err)
|
||||
}
|
||||
if res.Code != 0 {
|
||||
return "", &APIError{
|
||||
APIName: apiName,
|
||||
Code: res.Code,
|
||||
Msg: res.Msg,
|
||||
RequestID: res.RequestID,
|
||||
Data: res.Data,
|
||||
}
|
||||
}
|
||||
if data != nil && len(res.Data) > 0 && string(res.Data) != "null" {
|
||||
if err := json.Unmarshal(res.Data, data); err != nil {
|
||||
return "", fmt.Errorf("json Unmarshal Data Error, err=%v", err)
|
||||
}
|
||||
}
|
||||
return res.RequestID, nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
const accessTokenCacheKeyPrefix = "gowechat_aispeech_"
|
||||
|
||||
var errEmptyAccessToken = errors.New("aispeech access_token is empty")
|
||||
|
||||
// AccessToken 微信智能对话 access token.
|
||||
type AccessToken struct {
|
||||
cfg *config.Config
|
||||
accessTokenLock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewAccessToken new AccessToken.
|
||||
func NewAccessToken(cfg *config.Config) *AccessToken {
|
||||
if cfg.Cache == nil {
|
||||
panic("cache is needed")
|
||||
}
|
||||
return &AccessToken{
|
||||
cfg: cfg,
|
||||
accessTokenLock: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAccessToken 获取 access token.
|
||||
func (ak *AccessToken) GetAccessToken() (string, error) {
|
||||
return ak.GetAccessTokenContext(stdcontext.Background())
|
||||
}
|
||||
|
||||
// GetAccessTokenContext 获取 access token.
|
||||
func (ak *AccessToken) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s_access_token_%s_%s", accessTokenCacheKeyPrefix, ak.cfg.AppID, ak.cfg.Account)
|
||||
if val := cache.GetContext(ctx, ak.cfg.Cache, cacheKey); val != nil {
|
||||
if accessToken, ok := val.(string); ok && accessToken != "" {
|
||||
return accessToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
ak.accessTokenLock.Lock()
|
||||
defer ak.accessTokenLock.Unlock()
|
||||
|
||||
if val := cache.GetContext(ctx, ak.cfg.Cache, cacheKey); val != nil {
|
||||
if accessToken, ok := val.(string); ok && accessToken != "" {
|
||||
return accessToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
accessToken, err := ak.getAccessTokenFromServer(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = cache.SetContext(ctx, ak.cfg.Cache, cacheKey, accessToken, 110*time.Minute); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
func (ak *AccessToken) getAccessTokenFromServer(ctx stdcontext.Context) (string, error) {
|
||||
req := &AccessTokenRequest{Account: ak.cfg.Account}
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
response, err := post(ctx, ak.cfg, tokenPath, body, "application/json", "", ak.cfg.AppID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var res accessTokenData
|
||||
if _, err = decodeResponse(response, &res, "AISpeechGetAccessToken"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.AccessToken == "" {
|
||||
return "", errEmptyAccessToken
|
||||
}
|
||||
return res.AccessToken, nil
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package encryptor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidBlockSize block size 不合法.
|
||||
ErrInvalidBlockSize = errors.New("invalid block size")
|
||||
// ErrInvalidPKCS7Data PKCS7 数据不合法.
|
||||
ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data")
|
||||
// ErrInvalidPKCS7Padding padding 不合法.
|
||||
ErrInvalidPKCS7Padding = errors.New("invalid PKCS7 padding")
|
||||
)
|
||||
|
||||
// Encrypt encrypts plaintext with AES-CBC and returns base64 ciphertext.
|
||||
func Encrypt(aesKey string, plaintext []byte) (string, error) {
|
||||
key, err := decodeAESKey(aesKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plaintext = pkcs7Pad(plaintext, block.BlockSize())
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
mode := cipher.NewCBCEncrypter(block, key[:aes.BlockSize])
|
||||
mode.CryptBlocks(ciphertext, plaintext)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts base64 AES-CBC ciphertext.
|
||||
func Decrypt(aesKey, ciphertext string) ([]byte, error) {
|
||||
key, err := decodeAESKey(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 || len(data)%aes.BlockSize != 0 {
|
||||
return nil, ErrInvalidPKCS7Data
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plaintext := make([]byte, len(data))
|
||||
mode := cipher.NewCBCDecrypter(block, key[:aes.BlockSize])
|
||||
mode.CryptBlocks(plaintext, data)
|
||||
return pkcs7Unpad(plaintext, block.BlockSize())
|
||||
}
|
||||
|
||||
func decodeAESKey(encodingAESKey string) ([]byte, error) {
|
||||
key, err := base64.StdEncoding.DecodeString(encodingAESKey + "=")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(key) != 32 {
|
||||
return nil, fmt.Errorf("encodingAESKey invalid")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(data, padText...)
|
||||
}
|
||||
|
||||
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
if blockSize <= 0 {
|
||||
return nil, ErrInvalidBlockSize
|
||||
}
|
||||
if len(data) == 0 || len(data)%blockSize != 0 {
|
||||
return nil, ErrInvalidPKCS7Data
|
||||
}
|
||||
padding := int(data[len(data)-1])
|
||||
if padding == 0 || padding > blockSize || padding > len(data) {
|
||||
return nil, ErrInvalidPKCS7Padding
|
||||
}
|
||||
for i := len(data) - padding; i < len(data); i++ {
|
||||
if int(data[i]) != padding {
|
||||
return nil, ErrInvalidPKCS7Padding
|
||||
}
|
||||
}
|
||||
return data[:len(data)-padding], nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package encryptor
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
sign := Sign("YV78Pyj1VvqdNGpMJ1pHic0bIBOWMv", 1711001766, "abc", nil)
|
||||
if sign != "fff8dae1356e7867ea98743439f0e9f8" {
|
||||
t.Fatalf("bad sign: %s", sign)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
aesKey := "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz"
|
||||
plainText := []byte(`{"query":"hello"}`)
|
||||
|
||||
cipherText, err := Encrypt(aesKey, plainText)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt error: %v", err)
|
||||
}
|
||||
got, err := Decrypt(aesKey, cipherText)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt error: %v", err)
|
||||
}
|
||||
if string(got) != string(plainText) {
|
||||
t.Fatalf("bad plaintext: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptInvalidKey(t *testing.T) {
|
||||
if _, err := Encrypt("bad-key", []byte("hello")); err == nil {
|
||||
t.Fatal("Encrypt should return error")
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package encryptor
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Sign calculates the aispeech request signature.
|
||||
func Sign(token string, timestamp int64, nonce string, body []byte) string {
|
||||
bodyMD5 := md5.Sum(body)
|
||||
bodySign := hex.EncodeToString(bodyMD5[:])
|
||||
|
||||
h := md5.New()
|
||||
h.Write([]byte(token))
|
||||
h.Write([]byte(strconv.FormatInt(timestamp, 10)))
|
||||
h.Write([]byte(nonce))
|
||||
h.Write([]byte(bodySign))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
Vendored
+27
-43
@@ -5,64 +5,53 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Memory is an concurrent-safe in-process cache backed by a plain map.
|
||||
// Memory struct contains *memcache.Client
|
||||
type Memory struct {
|
||||
mu sync.RWMutex
|
||||
sync.Mutex
|
||||
|
||||
data map[string]*data
|
||||
}
|
||||
|
||||
// data holds a single cached value together with its expiry time.
|
||||
type data struct {
|
||||
Data interface{}
|
||||
Expired time.Time
|
||||
}
|
||||
|
||||
// NewMemory returns a ready-to-use in-memory cache.
|
||||
// NewMemory create new memcache
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
data: map[string]*data{},
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the cached value for key.
|
||||
// Returns nil if the key does not exist or has expired.
|
||||
// Get return cached value
|
||||
func (mem *Memory) Get(key string) interface{} {
|
||||
mem.mu.RLock()
|
||||
ret, ok := mem.data[key]
|
||||
expired := ok && ret.Expired.Before(time.Now())
|
||||
mem.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
if ret, ok := mem.data[key]; ok {
|
||||
if ret.Expired.Before(time.Now()) {
|
||||
mem.deleteKey(key)
|
||||
return nil
|
||||
}
|
||||
return ret.Data
|
||||
}
|
||||
if expired {
|
||||
mem.deleteExpiredKey(key)
|
||||
return nil
|
||||
}
|
||||
return ret.Data
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExist check value exists in memcache.
|
||||
func (mem *Memory) IsExist(key string) bool {
|
||||
mem.mu.RLock()
|
||||
ret, ok := mem.data[key]
|
||||
expired := ok && ret.Expired.Before(time.Now())
|
||||
mem.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
if ret, ok := mem.data[key]; ok {
|
||||
if ret.Expired.Before(time.Now()) {
|
||||
mem.deleteKey(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if expired {
|
||||
mem.deleteExpiredKey(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Set cached value with key and expire time.
|
||||
func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err error) {
|
||||
mem.mu.Lock()
|
||||
defer mem.mu.Unlock()
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
|
||||
mem.data[key] = &data{
|
||||
Data: val,
|
||||
@@ -73,18 +62,13 @@ func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err
|
||||
|
||||
// Delete delete value in memcache.
|
||||
func (mem *Memory) Delete(key string) error {
|
||||
mem.mu.Lock()
|
||||
defer mem.mu.Unlock()
|
||||
delete(mem.data, key)
|
||||
mem.deleteKey(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteExpiredKey deletes a key only if it has expired.
|
||||
// Caller must NOT hold any lock.
|
||||
func (mem *Memory) deleteExpiredKey(key string) {
|
||||
mem.mu.Lock()
|
||||
defer mem.mu.Unlock()
|
||||
if d, ok := mem.data[key]; ok && d.Expired.Before(time.Now()) {
|
||||
delete(mem.data, key)
|
||||
}
|
||||
// deleteKey
|
||||
func (mem *Memory) deleteKey(key string) {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
delete(mem.data, key)
|
||||
}
|
||||
Vendored
-116
@@ -1,116 +0,0 @@
|
||||
// 运行测试:go test -race -v ./cache/ -run "TestMemory" -count=1
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMemoryGet(t *testing.T) {
|
||||
mem := NewMemory()
|
||||
|
||||
err := mem.Set("username", "silenceper", 10*time.Second)
|
||||
assert.NoError(t, err)
|
||||
|
||||
val := mem.Get("username")
|
||||
assert.Equal(t, "silenceper", val)
|
||||
|
||||
val = mem.Get("unknown-key")
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestMemoryIsExist(t *testing.T) {
|
||||
mem := NewMemory()
|
||||
|
||||
err := mem.Set("username", "silenceper", 10*time.Second)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, mem.IsExist("username"))
|
||||
assert.False(t, mem.IsExist("unknown-key"))
|
||||
}
|
||||
|
||||
func TestMemoryDelete(t *testing.T) {
|
||||
mem := NewMemory()
|
||||
|
||||
err := mem.Set("username", "silenceper", 10*time.Second)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = mem.Delete("username")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// delete 不存在的 key 不应报错
|
||||
err = mem.Delete("unknown-key")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMemoryExpire(t *testing.T) {
|
||||
mem := NewMemory()
|
||||
|
||||
err := mem.Set("username", "silenceper", 10*time.Millisecond)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, mem.IsExist("username"))
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
assert.False(t, mem.IsExist("username"))
|
||||
assert.Nil(t, mem.Get("username"))
|
||||
}
|
||||
|
||||
// TestMemoryConcurrentOps 验证 Get/IsExist/Set/Delete 并发执行不产生数据竞争
|
||||
func TestMemoryConcurrentOps(t *testing.T) {
|
||||
mem := NewMemory()
|
||||
_ = mem.Set("key", "value", time.Minute)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(4)
|
||||
// 并发读
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = mem.Get("key")
|
||||
}()
|
||||
// 并发 IsExist
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = mem.IsExist("key")
|
||||
}()
|
||||
// 并发写
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
_ = mem.Set("key", i, time.Minute)
|
||||
}(i)
|
||||
// 并发删除
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = mem.Delete("key")
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestMemoryConcurrentExpireAndRead 验证过期惰性删除在并发场景下不会死锁
|
||||
func TestMemoryConcurrentExpireAndRead(t *testing.T) {
|
||||
mem := NewMemory()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(2)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
key := "expire-key"
|
||||
_ = mem.Set(key, i, 5*time.Millisecond)
|
||||
}(i)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(3 * time.Millisecond)
|
||||
// 此时 key 可能已过期,触发 deleteKey,验证不死锁
|
||||
_ = mem.Get("expire-key")
|
||||
_ = mem.IsExist("expire-key")
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
Vendored
+10
-48
@@ -17,24 +17,14 @@ type Redis struct {
|
||||
|
||||
// RedisOpts redis 连接属性
|
||||
type RedisOpts struct {
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Username string `json:"username" yaml:"username"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
Database int `json:"database" yaml:"database"`
|
||||
MinIdleConns int `json:"min_idle_conns" yaml:"min_idle_conns"` // 最小空闲连接数
|
||||
PoolSize int `json:"pool_size" yaml:"pool_size"` // 连接池大小,0 表示使用默认值(即 CPU 核心数 * 10)
|
||||
MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数,-1 表示不重试,0 表示使用默认值(即 3 次)
|
||||
DialTimeout int `json:"dial_timeout" yaml:"dial_timeout"` // 连接超时时间(秒),0 表示使用默认值(即 5 秒)
|
||||
ReadTimeout int `json:"read_timeout" yaml:"read_timeout"` // 读取超时时间(秒),-1 表示不超时,0 表示使用默认值(即 3 秒)
|
||||
WriteTimeout int `json:"write_timeout" yaml:"write_timeout"` // 写入超时时间(秒),-1 表示不超时,0 表示使用默认值(即 ReadTimeout)
|
||||
PoolTimeout int `json:"pool_timeout" yaml:"pool_timeout"` // 连接池获取连接超时时间(秒),0 表示使用默认值(即 ReadTimeout + 1 秒)
|
||||
IdleTimeout int `json:"idle_timeout" yaml:"idle_timeout"` // 空闲连接超时时间(秒),-1 表示禁用空闲连接超时检查,0 表示使用默认值(即 5 分钟)
|
||||
UseTLS bool `json:"use_tls" yaml:"use_tls"` // 是否使用 TLS
|
||||
|
||||
// Deprecated: 应使用 MinIdleConns 代替
|
||||
MaxIdle int `json:"max_idle" yaml:"max_idle"`
|
||||
// Deprecated: 应使用 PoolSize 代替
|
||||
MaxActive int `json:"max_active" yaml:"max_active"`
|
||||
Host string `json:"host" yml:"host"`
|
||||
Username string `json:"username" yaml:"username"`
|
||||
Password string `json:"password" yml:"password"`
|
||||
Database int `json:"database" yml:"database"`
|
||||
MaxIdle int `json:"max_idle" yml:"max_idle"`
|
||||
MaxActive int `json:"max_active" yml:"max_active"`
|
||||
IdleTimeout int `json:"idle_timeout" yml:"idle_timeout"` // second
|
||||
UseTLS bool `json:"use_tls" yml:"use_tls"` // 是否使用TLS
|
||||
}
|
||||
|
||||
// NewRedis 实例化
|
||||
@@ -44,38 +34,10 @@ func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
||||
DB: opts.Database,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
MinIdleConns: opts.MinIdleConns,
|
||||
PoolSize: opts.PoolSize,
|
||||
MaxRetries: opts.MaxRetries,
|
||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||
MinIdleConns: opts.MaxIdle,
|
||||
}
|
||||
|
||||
// 兼容旧的 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 {
|
||||
|
||||
Vendored
+1
-267
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
func TestRedis(t *testing.T) {
|
||||
@@ -19,16 +18,7 @@ func TestRedis(t *testing.T) {
|
||||
timeoutDuration = time.Second
|
||||
ctx = context.Background()
|
||||
opts = &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Password: "",
|
||||
Database: 0,
|
||||
PoolSize: 10,
|
||||
MinIdleConns: 5,
|
||||
DialTimeout: 5,
|
||||
ReadTimeout: 5,
|
||||
WriteTimeout: 5,
|
||||
PoolTimeout: 5,
|
||||
IdleTimeout: 300,
|
||||
Host: server.Addr(),
|
||||
}
|
||||
redis = NewRedis(ctx, opts)
|
||||
val = "silenceper"
|
||||
@@ -54,259 +44,3 @@ func TestRedis(t *testing.T) {
|
||||
t.Errorf("delete Error , err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupRedisServer 创建并返回一个 miniredis 服务器实例
|
||||
func setupRedisServer(t *testing.T) *miniredis.Miniredis {
|
||||
server, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Fatal("miniredis.Run Error", err)
|
||||
}
|
||||
t.Cleanup(server.Close)
|
||||
return server
|
||||
}
|
||||
|
||||
// TestRedisMaxIdleMapping 测试只设置MaxIdle应该映射到MinIdleConns
|
||||
func TestRedisMaxIdleMapping(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
MaxIdle: 10,
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
// 获取底层的 UniversalClient 并断言为 *redis.Client
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
// 注意:MinIdleConns 表示期望的最小空闲连接数,但实际空闲连接数可能不同
|
||||
// 我们需要通过 Options() 来验证配置是否正确应用
|
||||
clientOpts := client.Options()
|
||||
if clientOpts.MinIdleConns != 10 {
|
||||
t.Errorf("期望 MinIdleConns = 10, 实际 = %d", clientOpts.MinIdleConns)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisMaxActiveMapping 测试只设置MaxActive应该映射到PoolSize
|
||||
func TestRedisMaxActiveMapping(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
MaxActive: 20,
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
clientOpts := client.Options()
|
||||
if clientOpts.PoolSize != 20 {
|
||||
t.Errorf("期望 PoolSize = 20, 实际 = %d", clientOpts.PoolSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisNewFieldsPriority 测试新字段应该优先于旧字段
|
||||
func TestRedisNewFieldsPriority(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
MaxIdle: 5,
|
||||
MinIdleConns: 15,
|
||||
MaxActive: 10,
|
||||
PoolSize: 30,
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
clientOpts := client.Options()
|
||||
if clientOpts.MinIdleConns != 15 {
|
||||
t.Errorf("期望 MinIdleConns = 15 (新字段优先), 实际 = %d", clientOpts.MinIdleConns)
|
||||
}
|
||||
if clientOpts.PoolSize != 30 {
|
||||
t.Errorf("期望 PoolSize = 30 (新字段优先), 实际 = %d", clientOpts.PoolSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisPositiveTimeouts 测试正值超时应该正确应用
|
||||
func TestRedisPositiveTimeouts(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
DialTimeout: 10,
|
||||
ReadTimeout: 20,
|
||||
WriteTimeout: 30,
|
||||
PoolTimeout: 40,
|
||||
IdleTimeout: 50,
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
clientOpts := client.Options()
|
||||
if clientOpts.DialTimeout != 10*time.Second {
|
||||
t.Errorf("期望 DialTimeout = 10s, 实际 = %v", clientOpts.DialTimeout)
|
||||
}
|
||||
if clientOpts.ReadTimeout != 20*time.Second {
|
||||
t.Errorf("期望 ReadTimeout = 20s, 实际 = %v", clientOpts.ReadTimeout)
|
||||
}
|
||||
if clientOpts.WriteTimeout != 30*time.Second {
|
||||
t.Errorf("期望 WriteTimeout = 30s, 实际 = %v", clientOpts.WriteTimeout)
|
||||
}
|
||||
if clientOpts.PoolTimeout != 40*time.Second {
|
||||
t.Errorf("期望 PoolTimeout = 40s, 实际 = %v", clientOpts.PoolTimeout)
|
||||
}
|
||||
if clientOpts.IdleTimeout != 50*time.Second {
|
||||
t.Errorf("期望 IdleTimeout = 50s, 实际 = %v", clientOpts.IdleTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisNegativeTimeouts 测试-1值应该禁用超时
|
||||
func TestRedisNegativeTimeouts(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
DialTimeout: -1,
|
||||
ReadTimeout: -1,
|
||||
WriteTimeout: -1,
|
||||
PoolTimeout: -1,
|
||||
IdleTimeout: -1,
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
clientOpts := client.Options()
|
||||
// -1 应该被设置为负值表示禁用超时
|
||||
// DialTimeout, PoolTimeout, IdleTimeout 会被设置为 -1ns
|
||||
if clientOpts.DialTimeout != -1 {
|
||||
t.Errorf("期望 DialTimeout = -1ns (禁用), 实际 = %v", clientOpts.DialTimeout)
|
||||
}
|
||||
// ReadTimeout 和 WriteTimeout 在 go-redis 中有特殊处理
|
||||
// 当设置为负值时,会被规范化为 0,这也表示无超时
|
||||
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
|
||||
t.Logf("WriteTimeout = %v (设置为-1后的值)", clientOpts.WriteTimeout)
|
||||
|
||||
if clientOpts.PoolTimeout != -1 {
|
||||
t.Errorf("期望 PoolTimeout = -1ns (禁用), 实际 = %v", clientOpts.PoolTimeout)
|
||||
}
|
||||
if clientOpts.IdleTimeout != -1 {
|
||||
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisZeroTimeouts 测试0值应该使用go-redis默认值
|
||||
func TestRedisZeroTimeouts(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
DialTimeout: 0,
|
||||
ReadTimeout: 0,
|
||||
WriteTimeout: 0,
|
||||
PoolTimeout: 0,
|
||||
IdleTimeout: 0,
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
clientOpts := client.Options()
|
||||
// 0值应该保持为0,由 go-redis 使用默认值
|
||||
// go-redis 的默认值:
|
||||
// DialTimeout: 5s
|
||||
// ReadTimeout: 3s
|
||||
// WriteTimeout: ReadTimeout
|
||||
// PoolTimeout: ReadTimeout + 1s
|
||||
// IdleTimeout: 5min
|
||||
|
||||
if clientOpts.DialTimeout == 0 {
|
||||
t.Error("期望 DialTimeout 使用 go-redis 默认值 (5s), 实际为 0")
|
||||
}
|
||||
if clientOpts.ReadTimeout == 0 {
|
||||
t.Error("期望 ReadTimeout 使用 go-redis 默认值 (3s), 实际为 0")
|
||||
}
|
||||
if clientOpts.WriteTimeout == 0 {
|
||||
t.Error("期望 WriteTimeout 使用 go-redis 默认值 (ReadTimeout), 实际为 0")
|
||||
}
|
||||
if clientOpts.PoolTimeout == 0 {
|
||||
t.Error("期望 PoolTimeout 使用 go-redis 默认值 (ReadTimeout + 1s), 实际为 0")
|
||||
}
|
||||
if clientOpts.IdleTimeout == 0 {
|
||||
t.Error("期望 IdleTimeout 使用 go-redis 默认值 (5min), 实际为 0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisMixedTimeouts 测试混合超时配置
|
||||
func TestRedisMixedTimeouts(t *testing.T) {
|
||||
server := setupRedisServer(t)
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &RedisOpts{
|
||||
Host: server.Addr(),
|
||||
Database: 0,
|
||||
DialTimeout: 5, // 正值
|
||||
ReadTimeout: -1, // 禁用
|
||||
WriteTimeout: 0, // 使用默认值
|
||||
PoolTimeout: 10, // 正值
|
||||
IdleTimeout: -1, // 禁用
|
||||
}
|
||||
r := NewRedis(ctx, opts)
|
||||
|
||||
client, ok := r.conn.(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("无法转换为 *redis.Client")
|
||||
}
|
||||
|
||||
clientOpts := client.Options()
|
||||
if clientOpts.DialTimeout != 5*time.Second {
|
||||
t.Errorf("期望 DialTimeout = 5s, 实际 = %v", clientOpts.DialTimeout)
|
||||
}
|
||||
// ReadTimeout 设置为 -1,会被 go-redis 处理为 0(无超时)
|
||||
t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout)
|
||||
|
||||
// WriteTimeout 设置为 0,应该使用 go-redis 的默认值
|
||||
// 默认值通常是 ReadTimeout 的值
|
||||
t.Logf("WriteTimeout = %v (设置为0后使用的默认值)", clientOpts.WriteTimeout)
|
||||
|
||||
if clientOpts.PoolTimeout != 10*time.Second {
|
||||
t.Errorf("期望 PoolTimeout = 10s, 实际 = %v", clientOpts.PoolTimeout)
|
||||
}
|
||||
|
||||
// IdleTimeout 设置为 -1,应该被设置为 -1ns(禁用空闲超时)
|
||||
if clientOpts.IdleTimeout != -1 {
|
||||
t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout)
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,7 @@ func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, accessToken st
|
||||
// 先从cache中取
|
||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||
if ticketStr = val.(string); ticketStr != "" {
|
||||
return
|
||||
}
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
js.jsAPITicketLock.Lock()
|
||||
@@ -66,9 +64,7 @@ func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, accessToken st
|
||||
|
||||
// 双检,防止重复从微信服务器获取
|
||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||
if ticketStr = val.(string); ticketStr != "" {
|
||||
return
|
||||
}
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
var ticket ResTicket
|
||||
|
||||
+1
-10
@@ -1,12 +1,3 @@
|
||||
# 智能对话
|
||||
|
||||
## 接入智能对话
|
||||
|
||||
- 获取 Accesstoken:`POST /v2/token`
|
||||
- 简单问答导入:`POST /v2/bot/import/json`
|
||||
- 异步任务查询:`POST /v2/async/fetch`
|
||||
- 发布机器人:`POST /v2/bot/publish`
|
||||
- 查询机器人发布进度:`POST /v2/bot/effective_progress`
|
||||
- 调用智能对话:`POST /v2/bot/query`
|
||||
|
||||
官方文档:https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
|
||||
TODO
|
||||
@@ -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")
|
||||
}
|
||||
@@ -41,12 +41,6 @@ const (
|
||||
EventTypeXpayGoodsDeliverNotify EventType = "xpay_goods_deliver_notify"
|
||||
// EventTypeXpayCoinPayNotify 代币支付推送事件
|
||||
EventTypeXpayCoinPayNotify EventType = "xpay_coin_pay_notify"
|
||||
// EventTypeXpayRefundNotify 退款推送事件
|
||||
EventTypeXpayRefundNotify EventType = "xpay_refund_notify"
|
||||
// EventTypeXpaySubscribeIosRefundQueryNotify iOS Apple 支付退款问询事件
|
||||
EventTypeXpaySubscribeIosRefundQueryNotify EventType = "xpay_subscribe_ios_refund_query_notify"
|
||||
// EventTypeXpayComplaintNotify 用户投诉推送事件
|
||||
EventTypeXpayComplaintNotify EventType = "xpay_complaint_notify"
|
||||
// EventSubscribePopup 用户操作订阅通知弹窗事件推送,用户在图文等场景内订阅通知的操作
|
||||
EventSubscribePopup EventType = "subscribe_msg_popup_event"
|
||||
// EventSubscribeMsgChange 用户管理订阅通知,用户在服务通知管理页面做通知管理时的操作
|
||||
@@ -209,14 +203,6 @@ func (receiver *PushReceiver) getEvent(dataType string, eventType EventType, dec
|
||||
var pushData PushDataXpayCoinPayNotify
|
||||
err := receiver.unmarshal(dataType, decryptMsg, &pushData)
|
||||
return &pushData, err
|
||||
case EventTypeXpayRefundNotify:
|
||||
// 退款推送事件
|
||||
return receiver.unmarshalPushData(dataType, decryptMsg, &PushDataXpayRefundNotify{})
|
||||
case EventTypeXpaySubscribeIosRefundQueryNotify: // iOS Apple 支付退款问询事件
|
||||
return receiver.unmarshalPushData(dataType, decryptMsg, &PushDataXpaySubscribeIosRefundQueryNotify{})
|
||||
case EventTypeXpayComplaintNotify:
|
||||
// 用户投诉推送事件
|
||||
return receiver.unmarshalPushData(dataType, decryptMsg, &PushDataXpayComplaintNotify{})
|
||||
case EventSubscribePopup:
|
||||
// 用户操作订阅通知弹窗事件推送
|
||||
return receiver.unmarshalSubscribePopup(dataType, decryptMsg)
|
||||
@@ -239,12 +225,6 @@ func (receiver *PushReceiver) unmarshal(dataType string, decryptMsg []byte, push
|
||||
return json.Unmarshal(decryptMsg, pushData)
|
||||
}
|
||||
|
||||
// unmarshalPushData unmarshal push data helper
|
||||
func (receiver *PushReceiver) unmarshalPushData(dataType string, decryptMsg []byte, pushData interface{}) (PushData, error) {
|
||||
err := receiver.unmarshal(dataType, decryptMsg, pushData)
|
||||
return pushData, err
|
||||
}
|
||||
|
||||
// unmarshalSubscribePopup
|
||||
func (receiver *PushReceiver) unmarshalSubscribePopup(dataType string, decryptMsg []byte) (PushData, error) {
|
||||
var pushData PushDataSubscribePopup
|
||||
@@ -452,7 +432,6 @@ type PushDataXpayGoodsDeliverNotify struct {
|
||||
Env int `json:"Env" xml:"Env"` // ,环境配置 0:现网环境(也叫正式环境)1:沙箱环境
|
||||
WeChatPayInfo WeChatPayInfo `json:"WeChatPayInfo" xml:"WeChatPayInfo"` // 微信支付信息 非微信支付渠道可能没有
|
||||
GoodsInfo GoodsInfo `json:"GoodsInfo" xml:"GoodsInfo"` // 道具参数信息
|
||||
TeamInfo XpayTeamInfo `json:"TeamInfo" xml:"TeamInfo"` // 拼团信息
|
||||
}
|
||||
|
||||
// WeChatPayInfo 微信支付信息
|
||||
@@ -598,65 +577,3 @@ func (s *PushDataSubscribeMsgSent) GetSubscribeMsgSentEvents() []SubscribeMsgSen
|
||||
|
||||
return s.SubscribeMsgSentEvent.List
|
||||
}
|
||||
|
||||
// XpayTeamInfo team purchase info
|
||||
type XpayTeamInfo struct {
|
||||
ActivityID string `json:"ActivityId" xml:"ActivityId"` // 活动 id
|
||||
TeamID string `json:"TeamId" xml:"TeamId"` // 团 id
|
||||
TeamType int `json:"TeamType" xml:"TeamType"` // 团类型 1-支付全部,拼成退款
|
||||
TeamAction int `json:"TeamAction" xml:"TeamAction"` // 0-创团 1-参团
|
||||
}
|
||||
|
||||
// PushDataXpayRefundNotify 退款推送
|
||||
type PushDataXpayRefundNotify struct {
|
||||
CommonPushData
|
||||
OpenID string `json:"OpenId" xml:"OpenId"` // 用户 openid
|
||||
WxRefundID string `json:"WxRefundId" xml:"WxRefundId"` // 微信退款单号
|
||||
MchRefundID string `json:"MchRefundId" xml:"MchRefundId"` // 商户退款单号
|
||||
WxOrderID string `json:"WxOrderId" xml:"WxOrderId"` // 退款单对应支付单的微信单号
|
||||
MchOrderID string `json:"MchOrderId" xml:"MchOrderId"` // 退款单对应支付单的商户单号
|
||||
RefundFee int `json:"RefundFee" xml:"RefundFee"` // 退款金额,单位分
|
||||
RetCode int `json:"RetCode" xml:"RetCode"` // 退款结果,0 为成功
|
||||
RetMsg string `json:"RetMsg" xml:"RetMsg"` // 退款结果详情
|
||||
RefundStartTimestamp int64 `json:"RefundStartTimestamp" xml:"RefundStartTimestamp"` // 开始退款时间,秒级时间戳
|
||||
RefundSuccTimestamp int64 `json:"RefundSuccTimestamp" xml:"RefundSuccTimestamp"` // 结束退款时间,秒级时间戳
|
||||
WxpayRefundTransactionID string `json:"WxpayRefundTransactionId" xml:"WxpayRefundTransactionId"` // 退款单的微信支付单号
|
||||
RetryTimes int `json:"RetryTimes" xml:"RetryTimes"` // 重试次数,从 0 开始
|
||||
TeamInfo XpayTeamInfo `json:"TeamInfo" xml:"TeamInfo"` // 拼团信息
|
||||
}
|
||||
|
||||
// PushDataXpaySubscribeIosRefundQueryNotify iOS Apple 支付退款问询事件
|
||||
// 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html
|
||||
type PushDataXpaySubscribeIosRefundQueryNotify struct {
|
||||
CommonPushData
|
||||
RefundTime string `json:"refund_time" xml:"refund_time"` // 问询时间,Unix 时间戳
|
||||
OrderTime string `json:"order_time" xml:"order_time"` // 该笔退款的订单时间,Unix 时间戳
|
||||
ChannelBill string `json:"channel_bill" xml:"channel_bill"` // Apple 支付票据号
|
||||
BundleID string `json:"bundleid" xml:"bundleid"` // 应用的 Apple bundleid
|
||||
ProductID string `json:"product_id" xml:"product_id"` // 道具 id
|
||||
PCount string `json:"p_count" xml:"p_count"` // 道具/代币数量
|
||||
RefundRequestReason string `json:"refund_request_reason" xml:"refund_request_reason"` // 用户请求退款的原因
|
||||
ProvideStatus string `json:"provide_status" xml:"provide_status"` // 发货状态,0:未发货 1:已发货 2:发货中
|
||||
PayOrderID string `json:"pay_order_id" xml:"pay_order_id"` // 退款对应支付订单号
|
||||
}
|
||||
|
||||
// IosRefundQueryResponse iOS Apple 支付退款问询应答
|
||||
type IosRefundQueryResponse struct {
|
||||
ResultCode int32 `json:"result_code" xml:"result_code"` // 结果码,0-放过,建议退款;1-拦截,拒绝退款
|
||||
ResultInfo string `json:"result_info" xml:"result_info"` // 结果描述
|
||||
Evidence string `json:"evidence" xml:"evidence"` // 决策凭据,必填,用于退款审计
|
||||
}
|
||||
|
||||
// PushDataXpayComplaintNotify 用户投诉推送
|
||||
type PushDataXpayComplaintNotify struct {
|
||||
CommonPushData
|
||||
OpenID string `json:"OpenId" xml:"OpenId"` // 用户 openid
|
||||
WxOrderID string `json:"WxOrderId" xml:"WxOrderId"` // 微信单号
|
||||
MchOrderID string `json:"MchOrderId" xml:"MchOrderId"` // 商户单号
|
||||
TransactionID string `json:"TransactionId" xml:"TransactionId"` // 微信支付交易单号
|
||||
ComplaintID string `json:"ComplaintId" xml:"ComplaintId"` // 投诉单号
|
||||
ComplaintDetail string `json:"ComplaintDetail" xml:"ComplaintDetail"` // 投诉详情
|
||||
ComplaintTime int64 `json:"ComplaintTime" xml:"ComplaintTime"` // 投诉时间,秒级时间戳
|
||||
RetryTimes int `json:"RetryTimes" xml:"RetryTimes"` // 重试次数,从 0 开始
|
||||
RequestID string `json:"RequestId" xml:"RequestId"` // 请求编号
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/silenceper/wechat/v2/miniprogram/express"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/message"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/minidrama"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/ocr"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/operation"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/order"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
||||
@@ -192,13 +191,3 @@ func (miniProgram *MiniProgram) GetOperation() *operation.Operation {
|
||||
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
|
||||
}
|
||||
@@ -55,7 +55,7 @@ type QRCoder struct {
|
||||
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
|
||||
EnvVersion string `json:"env_version,omitempty"`
|
||||
// ShowSplashAd 控制通过该小程序码进入小程序是否展示封面广告1、默认为true,展示封面广告2、传入为false时,不展示封面广告
|
||||
ShowSplashAd *bool `json:"show_splash_ad,omitempty"`
|
||||
ShowSplashAd bool `json:"show_splash_ad,omitempty"`
|
||||
}
|
||||
|
||||
// fetchCode 请求并返回二维码二进制数据
|
||||
|
||||
@@ -51,30 +51,6 @@ const (
|
||||
SessionKeyExpiredError ErrCode = 268490009
|
||||
// BillGeneratingError 错误码 268490011、账单数据生成中,请稍后调用本接口获取
|
||||
BillGeneratingError ErrCode = 268490011
|
||||
// BatchTaskRunningError 错误码 268490012、批量任务运行中,请等待完成后才能再次运行
|
||||
BatchTaskRunningError ErrCode = 268490012
|
||||
// RefundVerifiedOrderError 错误码 268490013、禁止对核销状态的单进行退款
|
||||
RefundVerifiedOrderError ErrCode = 268490013
|
||||
// RefundInProgressError 错误码 268490014、退款操作进行中,稍后可以使用相同参数重试
|
||||
RefundInProgressError ErrCode = 268490014
|
||||
// FrequencyLimitError 错误码 268490015、频率限制
|
||||
FrequencyLimitError ErrCode = 268490015
|
||||
// RefundLeftFeeMismatchError 错误码 268490016、退款的 left_fee 字段与实际不符
|
||||
RefundLeftFeeMismatchError ErrCode = 268490016
|
||||
// AdFundIndustryMismatchError 错误码 268490018、广告金充值帐户行业 id 不匹配
|
||||
AdFundIndustryMismatchError ErrCode = 268490018
|
||||
// AdFundAccountBoundError 错误码 268490019、广告金充值帐户 id 已绑定其他 appid
|
||||
AdFundAccountBoundError ErrCode = 268490019
|
||||
// AdFundAccountNameError 错误码 268490020、广告金充值帐户主体名称错误
|
||||
AdFundAccountNameError ErrCode = 268490020
|
||||
// AccountNotVerifiedError 错误码 268490021、账户未完成进件
|
||||
AccountNotVerifiedError ErrCode = 268490021
|
||||
// AdFundAccountInvalidError 错误码 268490022、广告金充值账户无效
|
||||
AdFundAccountInvalidError ErrCode = 268490022
|
||||
// AdFundInsufficientError 错误码 268490023、广告金余额不足
|
||||
AdFundInsufficientError ErrCode = 268490023
|
||||
// AdFundAmountMustPositiveError 错误码 268490024、广告金充值金额必须大于 0
|
||||
AdFundAmountMustPositiveError ErrCode = 268490024
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -141,57 +117,6 @@ const (
|
||||
|
||||
// queryPublishGoods 查询批量发布道具任务状态
|
||||
queryPublishGoods = "/xpay/query_publish_goods"
|
||||
|
||||
// startDownloadOrder 发起下载小程序订单明细任务
|
||||
startDownloadOrder = "/xpay/start_download_order"
|
||||
|
||||
// queryDownloadOrder 查询下载订单任务结果
|
||||
queryDownloadOrder = "/xpay/query_download_order"
|
||||
|
||||
// queryBizBalance 查询商家账户可提现余额
|
||||
queryBizBalance = "/xpay/query_biz_balance"
|
||||
|
||||
// queryTransferAccount 查询广告金充值账户
|
||||
queryTransferAccount = "/xpay/query_transfer_account"
|
||||
|
||||
// queryAdverFunds 查询广告金发放记录
|
||||
queryAdverFunds = "/xpay/query_adver_funds"
|
||||
|
||||
// createFundsBill 充值广告金
|
||||
createFundsBill = "/xpay/create_funds_bill"
|
||||
|
||||
// bindTransferAccount 绑定广告金充值账户
|
||||
bindTransferAccount = "/xpay/bind_transfer_accout"
|
||||
|
||||
// queryFundsBill 查询广告金充值记录
|
||||
queryFundsBill = "/xpay/query_funds_bill"
|
||||
|
||||
// queryRecoverBill 查询广告金回收记录
|
||||
queryRecoverBill = "/xpay/query_recover_bill"
|
||||
|
||||
// downloadAdverFundsOrder 下载广告金对应的商户订单信息
|
||||
downloadAdverFundsOrder = "/xpay/download_adverfunds_order"
|
||||
|
||||
// getComplaintList 获取投诉列表
|
||||
getComplaintList = "/xpay/get_complaint_list"
|
||||
|
||||
// getComplaintDetail 获取投诉详情
|
||||
getComplaintDetail = "/xpay/get_complaint_detail"
|
||||
|
||||
// getNegotiationHistory 获取协商历史
|
||||
getNegotiationHistory = "/xpay/get_negotiation_history"
|
||||
|
||||
// responseComplaint 回复用户
|
||||
responseComplaint = "/xpay/response_complaint"
|
||||
|
||||
// completeComplaint 完成投诉处理
|
||||
completeComplaint = "/xpay/complete_complaint"
|
||||
|
||||
// uploadVPFile 上传媒体文件
|
||||
uploadVPFile = "/xpay/upload_vp_file"
|
||||
|
||||
// getUploadFileSign 获取微信支付反馈投诉图片的签名头部
|
||||
getUploadFileSign = "/xpay/get_upload_file_sign"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// VirtualPayment mini program virtual payment
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html#_2-3-%E6%9C%8D%E5%8A%A1%E5%99%A8API
|
||||
type VirtualPayment struct {
|
||||
ctx *context.Context
|
||||
sessionKey string
|
||||
@@ -368,14 +368,12 @@ type AsyncXPayGoodsDeliverNotifyRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
WechatPayInfo *WeChatPayInfo `json:"WechatPayInfo"` // 微信支付订单信息
|
||||
GoodsInfo *GoodsInfo `json:"GoodsInfo"` // 道具信息
|
||||
TeamInfo *TeamInfo `json:"TeamInfo"` // 拼团信息
|
||||
}
|
||||
|
||||
// WeChatPayInfo 微信支付信息 非微信支付渠道可能没有
|
||||
type WeChatPayInfo struct {
|
||||
MchOrderNo string `json:"MchOrderNo"` // 商户订单号
|
||||
TransactionID string `json:"TransactionId"` // 微信支付订单号
|
||||
PaidTime int64 `json:"PaidTime"` // 用户支付时间,Linux 秒级时间戳
|
||||
}
|
||||
|
||||
// GoodsInfo 道具参数信息
|
||||
@@ -387,14 +385,6 @@ type GoodsInfo struct {
|
||||
Attach string `json:"Attach"` // 透传信息
|
||||
}
|
||||
|
||||
// TeamInfo 拼团信息
|
||||
type TeamInfo struct {
|
||||
ActivityID string `json:"ActivityId"` // 活动 id
|
||||
TeamID string `json:"TeamId"` // 团 id
|
||||
TeamType int `json:"TeamType"` // 团类型 1-支付全部,拼成退款
|
||||
TeamAction int `json:"TeamAction"` // 0-创团 1-参团
|
||||
}
|
||||
|
||||
// AsyncXPayGoodsDeliverNotifyResponse 异步通知发货响应参数
|
||||
type AsyncXPayGoodsDeliverNotifyResponse struct {
|
||||
util.CommonError
|
||||
@@ -427,438 +417,6 @@ type AsyncXPayCoinPayNotifyResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// AsyncXPayRefundNotifyRequest 异步通知退款推送,请求参数
|
||||
type AsyncXPayRefundNotifyRequest struct {
|
||||
ToUserName string `json:"ToUserName"` // 小程序的原始 ID
|
||||
FromUserName string `json:"FromUserName"` // 发送方帐号(一个 OpenID)
|
||||
CreateTime int `json:"CreateTime"` // 消息发送时间(整型)
|
||||
MsgType string `json:"MsgType"` // 消息类型,此时固定为:event
|
||||
Event string `json:"Event"` // 事件类型,此时固定为:xpay_refund_notify
|
||||
Openid string `json:"openid"` // 用户 openid
|
||||
WxRefundID string `json:"WxRefundId"` // 微信退款单号
|
||||
MchRefundID string `json:"MchRefundId"` // 商户退款单号
|
||||
WxOrderID string `json:"WxOrderId"` // 退款单对应支付单的微信单号
|
||||
MchOrderID string `json:"MchOrderId"` // 退款单对应支付单的商户单号
|
||||
RefundFee int `json:"RefundFee"` // 退款金额,单位分
|
||||
RetCode int `json:"RetCode"` // 退款结果,0 为成功,非0 为失败
|
||||
RetMsg string `json:"RetMsg"` // 退款结果详情
|
||||
RefundStartTimestamp int64 `json:"RefundStartTimestamp"` // 开始退款时间,秒级时间戳
|
||||
RefundSuccTimestamp int64 `json:"RefundSuccTimestamp"` // 结束退款时间,秒级时间戳
|
||||
WxpayRefundTransactionID string `json:"WxpayRefundTransactionId"` // 退款单的微信支付单号
|
||||
RetryTimes int `json:"RetryTimes"` // 重试次数,从 0 开始
|
||||
TeamInfo *TeamInfo `json:"TeamInfo"` // 拼团信息
|
||||
}
|
||||
|
||||
// AsyncXPayRefundNotifyResponse 异步通知退款推送响应参数
|
||||
type AsyncXPayRefundNotifyResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// AsyncXPaySubscribeIosRefundQueryNotifyRequest iOS Apple 支付退款问询消息
|
||||
// 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html
|
||||
type AsyncXPaySubscribeIosRefundQueryNotifyRequest struct {
|
||||
ToUserName string `json:"ToUserName"` // 小程序的原始 ID
|
||||
FromUserName string `json:"FromUserName"` // 发送方帐号
|
||||
CreateTime int `json:"CreateTime"` // 消息发送时间
|
||||
MsgType string `json:"MsgType"` // 消息类型,此时固定为 event
|
||||
Event string `json:"Event"` // 事件类型,此时固定为 xpay_subscribe_ios_refund_query_notify
|
||||
|
||||
RefundTime string `json:"refund_time"` // 问询时间,Unix 时间戳
|
||||
OrderTime string `json:"order_time"` // 该笔退款的订单时间,Unix 时间戳
|
||||
ChannelBill string `json:"channel_bill"` // Apple 支付票据号
|
||||
BundleID string `json:"bundleid"` // 应用的 Apple bundleid
|
||||
ProductID string `json:"product_id"` // 道具 id
|
||||
PCount string `json:"p_count"` // 道具/代币数量
|
||||
RefundRequestReason string `json:"refund_request_reason"` // 用户请求退款的原因
|
||||
ProvideStatus string `json:"provide_status"` // 发货状态,0:未发货 1:已发货 2:发货中
|
||||
PayOrderID string `json:"pay_order_id"` // 退款对应支付订单号
|
||||
}
|
||||
|
||||
// AsyncXPaySubscribeIosRefundQueryNotifyResponse iOS Apple 支付退款问询应答
|
||||
type AsyncXPaySubscribeIosRefundQueryNotifyResponse struct {
|
||||
ResultCode int32 `json:"result_code"` // 结果码,0-放过,建议退款;1-拦截,拒绝退款
|
||||
ResultInfo string `json:"result_info"` // 结果描述
|
||||
Evidence string `json:"evidence"` // 决策凭据,必填,用于退款审计
|
||||
}
|
||||
|
||||
// AsyncXPayComplaintNotifyRequest 异步通知用户投诉推送,请求参数
|
||||
type AsyncXPayComplaintNotifyRequest struct {
|
||||
ToUserName string `json:"ToUserName"` // 小程序的原始 ID
|
||||
FromUserName string `json:"FromUserName"` // 发送方帐号(一个 OpenID)
|
||||
CreateTime int `json:"CreateTime"` // 消息发送时间(整型)
|
||||
MsgType string `json:"MsgType"` // 消息类型,此时固定为:event
|
||||
Event string `json:"Event"` // 事件类型,此时固定为:xpay_complaint_notify
|
||||
Openid string `json:"openid"` // 用户 openid
|
||||
WxOrderID string `json:"WxOrderId"` // 微信单号
|
||||
MchOrderID string `json:"MchOrderId"` // 商户单号
|
||||
TransactionID string `json:"TransactionId"` // 微信支付交易单号
|
||||
ComplaintID string `json:"ComplaintId"` // 投诉单号
|
||||
ComplaintDetail string `json:"ComplaintDetail"` // 投诉详情
|
||||
ComplaintTime int64 `json:"ComplaintTime"` // 投诉时间,秒级时间戳
|
||||
RetryTimes int `json:"RetryTimes"` // 重试次数,从 0 开始
|
||||
RequestID string `json:"RequestId"` // 请求编号
|
||||
}
|
||||
|
||||
// AsyncXPayComplaintNotifyResponse 异步通知用户投诉推送响应参数
|
||||
type AsyncXPayComplaintNotifyResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// ==================== 下载订单 ====================
|
||||
|
||||
// StartDownloadOrderRequest 发起下载小程序订单明细任务,请求参数
|
||||
type StartDownloadOrderRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
BeginDs string `json:"begin_ds"` // 账单开始日期,格式为 yyyymmdd
|
||||
EndDs string `json:"end_ds"` // 账单结束日期,格式为 yyyymmdd
|
||||
}
|
||||
|
||||
// StartDownloadOrderResponse 发起下载小程序订单明细任务 响应参数
|
||||
type StartDownloadOrderResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// QueryDownloadOrderRequest 查询下载订单任务结果,请求参数
|
||||
type QueryDownloadOrderRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
BeginDs string `json:"begin_ds"` // 账单开始日期,格式为 yyyymmdd
|
||||
EndDs string `json:"end_ds"` // 账单结束日期,格式为 yyyymmdd
|
||||
}
|
||||
|
||||
// QueryDownloadOrderResponse 查询下载订单任务结果 响应参数
|
||||
type QueryDownloadOrderResponse struct {
|
||||
util.CommonError
|
||||
URL string `json:"url"` // 订单下载地址
|
||||
}
|
||||
|
||||
// ==================== 资金管理 ====================
|
||||
|
||||
// QueryBizBalanceRequest 查询商家账户可提现余额,请求参数
|
||||
type QueryBizBalanceRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境(仅作为签名校验,查询结果都是正式环境的)
|
||||
}
|
||||
|
||||
// BizBalanceAvailable 可提现余额
|
||||
type BizBalanceAvailable struct {
|
||||
Amount string `json:"amount"` // 可提现余额,单位元
|
||||
CurrencyCode string `json:"currency_code"` // 币种(一般为 CNY)
|
||||
}
|
||||
|
||||
// QueryBizBalanceResponse 查询商家账户可提现余额 响应参数
|
||||
type QueryBizBalanceResponse struct {
|
||||
util.CommonError
|
||||
BalanceAvailable *BizBalanceAvailable `json:"balance_available"` // 可提现余额
|
||||
}
|
||||
|
||||
// ==================== 广告金 ====================
|
||||
|
||||
// TransferAccount 广告金充值账户信息
|
||||
type TransferAccount struct {
|
||||
TransferAccountName string `json:"transfer_account_name"` // 账户名称
|
||||
TransferAccountUID int64 `json:"transfer_account_uid"` // 账户 UID
|
||||
TransferAccountAgencyID int64 `json:"transfer_account_agency_id"` // 代理商 ID
|
||||
TransferAccountAgencyName string `json:"transfer_account_agency_name"` // 代理商名称
|
||||
State int `json:"state"` // 审核状态 0-待审核 1-已通过 2-已驳回
|
||||
BindResult int `json:"bind_result"` // 绑定结果 1-绑定成功 2-绑定失败
|
||||
ErrorMsg string `json:"error_msg"` // 错误信息
|
||||
}
|
||||
|
||||
// QueryTransferAccountRequest 查询广告金充值账户,请求参数
|
||||
type QueryTransferAccountRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
}
|
||||
|
||||
// QueryTransferAccountResponse 查询广告金充值账户 响应参数
|
||||
type QueryTransferAccountResponse struct {
|
||||
util.CommonError
|
||||
AcctList []*TransferAccount `json:"acct_list"` // 充值账户列表
|
||||
}
|
||||
|
||||
// AdverFundsFilter 广告金发放记录查询过滤条件
|
||||
type AdverFundsFilter struct {
|
||||
SettleBegin int64 `json:"settle_begin,omitempty"` // 结算周期开始时间,unix 秒级时间戳
|
||||
SettleEnd int64 `json:"settle_end,omitempty"` // 结算周期结束时间,unix 秒级时间戳
|
||||
FundType int `json:"fund_type,omitempty"` // 资金类型 0-普通赠送 1-广告激励 2-定向激励
|
||||
}
|
||||
|
||||
// AdverFundsRecord 广告金发放记录
|
||||
type AdverFundsRecord struct {
|
||||
SettleBegin int64 `json:"settle_begin"` // 结算周期开始时间,unix 秒级时间戳
|
||||
SettleEnd int64 `json:"settle_end"` // 结算周期结束时间,unix 秒级时间戳
|
||||
TotalAmount int64 `json:"total_amount"` // 发放总金额,单位分
|
||||
RemainAmount int64 `json:"remain_amount"` // 剩余可用金额,单位分
|
||||
ExpireTime int64 `json:"expire_time"` // 过期时间,unix 秒级时间戳
|
||||
FundType int `json:"fund_type"` // 资金类型 0-普通赠送 1-广告激励 2-定向激励
|
||||
FundID string `json:"fund_id"` // 广告金发放 ID
|
||||
}
|
||||
|
||||
// QueryAdverFundsRequest 查询广告金发放记录,请求参数
|
||||
type QueryAdverFundsRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
Page int `json:"page,omitempty"` // 页码,>= 1
|
||||
PageSize int `json:"page_size,omitempty"` // 每页记录数
|
||||
Filter *AdverFundsFilter `json:"filter,omitempty"` // 查询过滤条件
|
||||
}
|
||||
|
||||
// QueryAdverFundsResponse 查询广告金发放记录 响应参数
|
||||
type QueryAdverFundsResponse struct {
|
||||
util.CommonError
|
||||
AdverFundsList []*AdverFundsRecord `json:"adver_funds_list"` // 发放记录列表
|
||||
TotalPage int `json:"total_page"` // 总页数
|
||||
}
|
||||
|
||||
// CreateFundsBillRequest 充值广告金,请求参数
|
||||
type CreateFundsBillRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
TransferAmount int64 `json:"transfer_amount"` // 充值金额,单位分
|
||||
TransferAccountUID int64 `json:"transfer_account_uid"` // 充值账户 UID
|
||||
TransferAccountName string `json:"transfer_account_name"` // 充值账户名称
|
||||
TransferAccountAgencyID int64 `json:"transfer_account_agency_id"` // 代理商 ID
|
||||
RequestID string `json:"request_id"` // 幂等请求 ID,最长 1024 字符
|
||||
SettleBegin int64 `json:"settle_begin"` // 结算周期开始时间,unix 秒级时间戳
|
||||
SettleEnd int64 `json:"settle_end"` // 结算周期结束时间,unix 秒级时间戳
|
||||
AuthorizeAdvertise int `json:"authorize_advertise"` // 是否授权广告数据 0-否 1-是
|
||||
FundType int `json:"fund_type"` // 资金类型 0-普通赠送 1-广告激励 2-定向激励
|
||||
}
|
||||
|
||||
// CreateFundsBillResponse 充值广告金 响应参数
|
||||
type CreateFundsBillResponse struct {
|
||||
util.CommonError
|
||||
BillID string `json:"bill_id"` // 充值订单号
|
||||
}
|
||||
|
||||
// BindTransferAccountRequest 绑定广告金充值账户,请求参数
|
||||
type BindTransferAccountRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
TransferAccountUID int64 `json:"transfer_account_uid,omitempty"` // 充值账户 UID
|
||||
TransferAccountOrgName string `json:"transfer_account_org_name,omitempty"` // 充值账户主体名称
|
||||
}
|
||||
|
||||
// BindTransferAccountResponse 绑定广告金充值账户 响应参数
|
||||
type BindTransferAccountResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// FundsBillFilter 广告金充值记录查询过滤条件
|
||||
type FundsBillFilter struct {
|
||||
OperTimeBegin int64 `json:"oper_time_begin"` // 充值开始时间,unix 秒级时间戳
|
||||
OperTimeEnd int64 `json:"oper_time_end"` // 充值结束时间,unix 秒级时间戳
|
||||
BillID string `json:"bill_id,omitempty"` // 充值订单号
|
||||
RequestID string `json:"request_id,omitempty"` // 幂等请求 ID
|
||||
}
|
||||
|
||||
// FundsBillRecord 广告金充值记录
|
||||
type FundsBillRecord struct {
|
||||
BillID string `json:"bill_id"` // 充值订单号
|
||||
OperTime int64 `json:"oper_time"` // 充值时间,unix 秒级时间戳
|
||||
SettleBegin int64 `json:"settle_begin"` // 对应广告金结算开始时间,unix 秒级时间戳
|
||||
SettleEnd int64 `json:"settle_end"` // 对应广告金结算结束时间,unix 秒级时间戳
|
||||
FundID string `json:"fund_id"` // 对应广告金发放 ID
|
||||
TransferAccountName string `json:"transfer_account_name"` // 充值账户名称
|
||||
TransferAccountUID int64 `json:"transfer_account_uid"` // 充值账户 UID
|
||||
TransferAmount int64 `json:"transfer_amount"` // 充值金额,单位分
|
||||
Status int `json:"status"` // 状态 0-充值中 1-成功 2-失败
|
||||
RequestID string `json:"request_id"` // 幂等请求 ID
|
||||
}
|
||||
|
||||
// QueryFundsBillRequest 查询广告金充值记录,请求参数
|
||||
type QueryFundsBillRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
Page int `json:"page"` // 页码,>= 1
|
||||
PageSize int `json:"page_size"` // 每页记录数
|
||||
Filter FundsBillFilter `json:"filter"` // 查询过滤条件
|
||||
}
|
||||
|
||||
// QueryFundsBillResponse 查询广告金充值记录 响应参数
|
||||
type QueryFundsBillResponse struct {
|
||||
util.CommonError
|
||||
BillList []*FundsBillRecord `json:"bill_list"` // 充值记录列表
|
||||
TotalPage int `json:"total_page"` // 总页数
|
||||
}
|
||||
|
||||
// RecoverBillFilter 广告金回收记录查询过滤条件
|
||||
type RecoverBillFilter struct {
|
||||
RecoverTimeBegin int64 `json:"recover_time_begin"` // 回收开始时间,unix 秒级时间戳
|
||||
RecoverTimeEnd int64 `json:"recover_time_end"` // 回收结束时间,unix 秒级时间戳
|
||||
BillID string `json:"bill_id,omitempty"` // 回收订单号
|
||||
}
|
||||
|
||||
// RecoverBillRecord 广告金回收记录
|
||||
type RecoverBillRecord struct {
|
||||
BillID string `json:"bill_id"` // 回收订单号
|
||||
RecoverTime int64 `json:"recover_time"` // 回收时间,unix 秒级时间戳
|
||||
SettleBegin int64 `json:"settle_begin"` // 结算周期开始时间,unix 秒级时间戳
|
||||
SettleEnd int64 `json:"settle_end"` // 结算周期结束时间,unix 秒级时间戳
|
||||
FundID string `json:"fund_id"` // 对应广告金发放 ID
|
||||
RecoverAccountName string `json:"recover_account_name"` // 回收账户
|
||||
RecoverAmount int64 `json:"recover_amount"` // 回收金额,单位分
|
||||
RefundOrderList []string `json:"refund_order_list"` // 对应的退款订单号列表
|
||||
}
|
||||
|
||||
// QueryRecoverBillRequest 查询广告金回收记录,请求参数
|
||||
type QueryRecoverBillRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
Page int `json:"page"` // 页码,>= 1
|
||||
PageSize int `json:"page_size"` // 每页记录数
|
||||
Filter RecoverBillFilter `json:"filter"` // 查询过滤条件
|
||||
}
|
||||
|
||||
// QueryRecoverBillResponse 查询广告金回收记录 响应参数
|
||||
type QueryRecoverBillResponse struct {
|
||||
util.CommonError
|
||||
BillList []*RecoverBillRecord `json:"bill_list"` // 回收记录列表
|
||||
TotalPage int `json:"total_page"` // 总页数
|
||||
}
|
||||
|
||||
// DownloadAdverFundsOrderRequest 下载广告金对应的商户订单信息,请求参数
|
||||
type DownloadAdverFundsOrderRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
FundID string `json:"fund_id"` // 广告金发放 ID
|
||||
}
|
||||
|
||||
// DownloadAdverFundsOrderResponse 下载广告金对应的商户订单信息 响应参数
|
||||
type DownloadAdverFundsOrderResponse struct {
|
||||
util.CommonError
|
||||
URL string `json:"url"` // 订单下载地址
|
||||
}
|
||||
|
||||
// ==================== 投诉处理 ====================
|
||||
|
||||
// ComplaintOrderInfo 投诉关联订单信息
|
||||
type ComplaintOrderInfo struct {
|
||||
TransactionID string `json:"transaction_id"` // 微信支付交易单号
|
||||
MchOrderNo string `json:"mch_order_no"` // 商户订单号
|
||||
RefundID string `json:"refund_id"` // 退款订单号
|
||||
}
|
||||
|
||||
// ComplaintMedia 投诉媒体信息
|
||||
type ComplaintMedia struct {
|
||||
MediaType int `json:"media_type"` // 媒体类型
|
||||
MediaURL string `json:"media_url"` // 媒体 URL
|
||||
MediaTags []string `json:"media_tags"` // 媒体标签
|
||||
}
|
||||
|
||||
// ComplaintItem 投诉详情
|
||||
type ComplaintItem struct {
|
||||
ComplaintID string `json:"complaint_id"` // 投诉单号
|
||||
ComplaintTime string `json:"complaint_time"` // 投诉时间
|
||||
ComplaintDetail string `json:"complaint_detail"` // 投诉内容
|
||||
ComplaintState string `json:"complaint_state"` // 投诉状态 PENDING/PROCESSING/PROCESSED
|
||||
PayerPhone string `json:"payer_phone"` // 投诉人联系方式
|
||||
PayerOpenID string `json:"payer_openid"` // 投诉人 OpenID
|
||||
ComplaintOrderInfo []*ComplaintOrderInfo `json:"complaint_order_info"` // 关联订单信息
|
||||
ComplaintFullRefunded bool `json:"complaint_full_refunded"` // 是否全部退款
|
||||
IncomingUserResponse bool `json:"incoming_user_response"` // 是否有待处理的用户消息
|
||||
UserComplaintTimes int `json:"user_complaint_times"` // 用户投诉次数
|
||||
ComplaintMediaList []*ComplaintMedia `json:"complaint_media_list"` // 用户上传的证据
|
||||
}
|
||||
|
||||
// GetComplaintListRequest 获取投诉列表,请求参数
|
||||
type GetComplaintListRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
BeginDate string `json:"begin_date"` // 查询开始日期,格式 yyyy-mm-dd
|
||||
EndDate string `json:"end_date"` // 查询结束日期,格式 yyyy-mm-dd
|
||||
Offset int `json:"offset"` // 偏移量,从 0 开始
|
||||
Limit int `json:"limit"` // 最大返回记录数
|
||||
}
|
||||
|
||||
// GetComplaintListResponse 获取投诉列表 响应参数
|
||||
type GetComplaintListResponse struct {
|
||||
util.CommonError
|
||||
Total int `json:"total"` // 总数
|
||||
Complaints []*ComplaintItem `json:"complaints"` // 投诉列表
|
||||
}
|
||||
|
||||
// GetComplaintDetailRequest 获取投诉详情,请求参数
|
||||
type GetComplaintDetailRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
ComplaintID string `json:"complaint_id"` // 投诉单号
|
||||
}
|
||||
|
||||
// GetComplaintDetailResponse 获取投诉详情 响应参数
|
||||
type GetComplaintDetailResponse struct {
|
||||
util.CommonError
|
||||
Complaint *ComplaintItem `json:"complaint"` // 投诉详情
|
||||
}
|
||||
|
||||
// NegotiationHistory 协商历史记录
|
||||
type NegotiationHistory struct {
|
||||
LogID string `json:"log_id"` // 操作流水号
|
||||
Operator string `json:"operator"` // 操作人
|
||||
OperateTime string `json:"operate_time"` // 操作时间
|
||||
OperateType string `json:"operate_type"` // 操作类型
|
||||
OperateDetails string `json:"operate_details"` // 操作详情
|
||||
ComplaintMediaList []*ComplaintMedia `json:"complaint_media_list"` // 上传的证据
|
||||
}
|
||||
|
||||
// GetNegotiationHistoryRequest 获取协商历史,请求参数
|
||||
type GetNegotiationHistoryRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
ComplaintID string `json:"complaint_id"` // 投诉单号
|
||||
Offset int `json:"offset"` // 偏移量,从 0 开始
|
||||
Limit int `json:"limit"` // 最大返回记录数
|
||||
}
|
||||
|
||||
// GetNegotiationHistoryResponse 获取协商历史 响应参数
|
||||
type GetNegotiationHistoryResponse struct {
|
||||
util.CommonError
|
||||
Total int `json:"total"` // 总数
|
||||
History []*NegotiationHistory `json:"history"` // 协商历史列表
|
||||
}
|
||||
|
||||
// ResponseComplaintRequest 回复用户投诉,请求参数
|
||||
type ResponseComplaintRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
ComplaintID string `json:"complaint_id"` // 投诉单号
|
||||
ResponseContent string `json:"response_content"` // 回复内容
|
||||
ResponseImages []string `json:"response_images"` // 图片文件 ID 列表(来自 upload_vp_file)
|
||||
}
|
||||
|
||||
// ResponseComplaintResponse 回复用户投诉 响应参数
|
||||
type ResponseComplaintResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// CompleteComplaintRequest 完成投诉处理,请求参数
|
||||
type CompleteComplaintRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
ComplaintID string `json:"complaint_id"` // 投诉单号
|
||||
}
|
||||
|
||||
// CompleteComplaintResponse 完成投诉处理 响应参数
|
||||
type CompleteComplaintResponse struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
// UploadVPFileRequest 上传媒体文件,请求参数
|
||||
type UploadVPFileRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
Base64Img string `json:"base64_img,omitempty"` // Base64 编码的图片,最大 1MB
|
||||
ImgURL string `json:"img_url,omitempty"` // 图片 URL(可直接下载,不支持 302 跳转),最大 2MB,优先使用此字段
|
||||
FileName string `json:"file_name"` // 图片名称
|
||||
}
|
||||
|
||||
// UploadVPFileResponse 上传媒体文件 响应参数
|
||||
type UploadVPFileResponse struct {
|
||||
util.CommonError
|
||||
FileID string `json:"file_id"` // 返回的文件 ID(用于 response_complaint)
|
||||
}
|
||||
|
||||
// GetUploadFileSignRequest 获取微信支付反馈投诉图片的签名头部,请求参数
|
||||
type GetUploadFileSignRequest struct {
|
||||
Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
|
||||
WxpayURL string `json:"wxpay_url"` // 微信支付图片 URL,格式 "https://api.mch.weixin.qq.com/v3/merchant-service/images/{xxxxxx}"
|
||||
ConvertCOS bool `json:"convert_cos"` // 是否转换为 COS,获取临时下载链接(有效期 30 分钟)
|
||||
ComplaintID string `json:"complaint_id"` // 对应的投诉单号
|
||||
}
|
||||
|
||||
// GetUploadFileSignResponse 获取微信支付反馈投诉图片的签名头部 响应参数
|
||||
type GetUploadFileSignResponse struct {
|
||||
util.CommonError
|
||||
Sign string `json:"sign"` // Authorization 头部值
|
||||
CosURL string `json:"cos_url"` // 当 convert_cos=true 时返回的 COS URL,有效期 30 分钟
|
||||
}
|
||||
|
||||
// URLParams url parameter
|
||||
type URLParams struct {
|
||||
Path string `json:"path"`
|
||||
|
||||
@@ -55,7 +55,7 @@ func (s *VirtualPayment) QueryUserBalance(ctx context.Context, in *QueryUserBala
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (s *VirtualPayment) CurrencyPay(ctx context.Context, in *CurrencyPayRequest
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (s *VirtualPayment) QueryOrder(ctx context.Context, in *QueryOrderRequest)
|
||||
return
|
||||
}
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (s *VirtualPayment) CancelCurrencyPay(ctx context.Context, in *CancelCurren
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (s *VirtualPayment) NotifyProvideGoods(ctx context.Context, in *NotifyProvi
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func (s *VirtualPayment) PresentCurrency(ctx context.Context, in *PresentCurrenc
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ func (s *VirtualPayment) DownloadBill(ctx context.Context, in *DownloadBillReque
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ func (s *VirtualPayment) RefundOrder(ctx context.Context, in *RefundOrderRequest
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ func (s *VirtualPayment) CreateWithdrawOrder(ctx context.Context, in *CreateWith
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ func (s *VirtualPayment) QueryWithdrawOrder(ctx context.Context, in *QueryWithdr
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ func (s *VirtualPayment) StartUploadGoods(ctx context.Context, in *StartUploadGo
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ func (s *VirtualPayment) QueryUploadGoods(ctx context.Context, in *QueryUploadGo
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ func (s *VirtualPayment) StartPublishGoods(ctx context.Context, in *StartPublish
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ func (s *VirtualPayment) QueryPublishGoods(ctx context.Context, in *QueryPublish
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
if response, err = util.PostJSONContext(ctx, address, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -439,499 +439,6 @@ func (s *VirtualPayment) QueryPublishGoods(ctx context.Context, in *QueryPublish
|
||||
return
|
||||
}
|
||||
|
||||
// StartDownloadOrder 发起下载小程序订单明细任务
|
||||
func (s *VirtualPayment) StartDownloadOrder(ctx context.Context, in *StartDownloadOrderRequest) (out StartDownloadOrderResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: startDownloadOrder,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "StartDownloadOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryDownloadOrder 查询下载订单任务结果
|
||||
func (s *VirtualPayment) QueryDownloadOrder(ctx context.Context, in *QueryDownloadOrderRequest) (out QueryDownloadOrderResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: queryDownloadOrder,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "QueryDownloadOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryBizBalance 查询商家账户可提现余额
|
||||
func (s *VirtualPayment) QueryBizBalance(ctx context.Context, in *QueryBizBalanceRequest) (out QueryBizBalanceResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: queryBizBalance,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "QueryBizBalance")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryTransferAccount 查询广告金充值账户
|
||||
func (s *VirtualPayment) QueryTransferAccount(ctx context.Context, in *QueryTransferAccountRequest) (out QueryTransferAccountResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: queryTransferAccount,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "QueryTransferAccount")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryAdverFunds 查询广告金发放记录
|
||||
func (s *VirtualPayment) QueryAdverFunds(ctx context.Context, in *QueryAdverFundsRequest) (out QueryAdverFundsResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: queryAdverFunds,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "QueryAdverFunds")
|
||||
return
|
||||
}
|
||||
|
||||
// CreateFundsBill 充值广告金
|
||||
func (s *VirtualPayment) CreateFundsBill(ctx context.Context, in *CreateFundsBillRequest) (out CreateFundsBillResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: createFundsBill,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "CreateFundsBill")
|
||||
return
|
||||
}
|
||||
|
||||
// BindTransferAccount 绑定广告金充值账户
|
||||
func (s *VirtualPayment) BindTransferAccount(ctx context.Context, in *BindTransferAccountRequest) (out BindTransferAccountResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: bindTransferAccount,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "BindTransferAccount")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryFundsBill 查询广告金充值记录
|
||||
func (s *VirtualPayment) QueryFundsBill(ctx context.Context, in *QueryFundsBillRequest) (out QueryFundsBillResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: queryFundsBill,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "QueryFundsBill")
|
||||
return
|
||||
}
|
||||
|
||||
// QueryRecoverBill 查询广告金回收记录
|
||||
func (s *VirtualPayment) QueryRecoverBill(ctx context.Context, in *QueryRecoverBillRequest) (out QueryRecoverBillResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: queryRecoverBill,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "QueryRecoverBill")
|
||||
return
|
||||
}
|
||||
|
||||
// DownloadAdverFundsOrder 下载广告金对应的商户订单信息
|
||||
func (s *VirtualPayment) DownloadAdverFundsOrder(ctx context.Context, in *DownloadAdverFundsOrderRequest) (out DownloadAdverFundsOrderResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: downloadAdverFundsOrder,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "DownloadAdverFundsOrder")
|
||||
return
|
||||
}
|
||||
|
||||
// GetComplaintList 获取投诉列表
|
||||
func (s *VirtualPayment) GetComplaintList(ctx context.Context, in *GetComplaintListRequest) (out GetComplaintListResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: getComplaintList,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "GetComplaintList")
|
||||
return
|
||||
}
|
||||
|
||||
// GetComplaintDetail 获取投诉详情
|
||||
func (s *VirtualPayment) GetComplaintDetail(ctx context.Context, in *GetComplaintDetailRequest) (out GetComplaintDetailResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: getComplaintDetail,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "GetComplaintDetail")
|
||||
return
|
||||
}
|
||||
|
||||
// GetNegotiationHistory 获取协商历史
|
||||
func (s *VirtualPayment) GetNegotiationHistory(ctx context.Context, in *GetNegotiationHistoryRequest) (out GetNegotiationHistoryResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: getNegotiationHistory,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "GetNegotiationHistory")
|
||||
return
|
||||
}
|
||||
|
||||
// ResponseComplaint 回复用户
|
||||
func (s *VirtualPayment) ResponseComplaint(ctx context.Context, in *ResponseComplaintRequest) (out ResponseComplaintResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: responseComplaint,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "ResponseComplaint")
|
||||
return
|
||||
}
|
||||
|
||||
// CompleteComplaint 完成投诉处理
|
||||
func (s *VirtualPayment) CompleteComplaint(ctx context.Context, in *CompleteComplaintRequest) (out CompleteComplaintResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: completeComplaint,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "CompleteComplaint")
|
||||
return
|
||||
}
|
||||
|
||||
// UploadVPFile 上传媒体文件
|
||||
func (s *VirtualPayment) UploadVPFile(ctx context.Context, in *UploadVPFileRequest) (out UploadVPFileResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: uploadVPFile,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "UploadVPFile")
|
||||
return
|
||||
}
|
||||
|
||||
// GetUploadFileSign 获取微信支付反馈投诉图片的签名头部
|
||||
func (s *VirtualPayment) GetUploadFileSign(ctx context.Context, in *GetUploadFileSignRequest) (out GetUploadFileSignResponse, err error) {
|
||||
var jsonByte []byte
|
||||
if jsonByte, err = json.Marshal(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
params = URLParams{
|
||||
Path: getUploadFileSign,
|
||||
Content: string(jsonByte),
|
||||
Signature: EmptyString,
|
||||
}
|
||||
address string
|
||||
)
|
||||
if address, err = s.requestAddress(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
if response, err = postJSONBytesContext(ctx, address, jsonByte); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &out, "GetUploadFileSign")
|
||||
return
|
||||
}
|
||||
|
||||
// hmacSha256 hmac sha256
|
||||
func (s *VirtualPayment) hmacSha256(key, data string) string {
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
@@ -939,13 +446,6 @@ func (s *VirtualPayment) hmacSha256(key, data string) string {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// postJSONBytesContext POST JSON bytes with context
|
||||
func postJSONBytesContext(ctx context.Context, address string, jsonByte []byte) ([]byte, error) {
|
||||
return util.HTTPPostContext(ctx, address, jsonByte, map[string]string{
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
})
|
||||
}
|
||||
|
||||
// PaySign pay sign
|
||||
func (s *VirtualPayment) PaySign(url, data string) (string, error) {
|
||||
if strings.TrimSpace(s.ctx.Config.AppKey) == "" {
|
||||
@@ -976,17 +476,23 @@ func (s *VirtualPayment) PaySignature(url, data string) (paySign, signature stri
|
||||
// requestURL .组合 URL
|
||||
func (s *VirtualPayment) requestAddress(params URLParams) (url string, err error) {
|
||||
switch params.Path {
|
||||
case queryUserBalance, currencyPay, cancelCurrencyPay:
|
||||
case queryUserBalance:
|
||||
case currencyPay:
|
||||
case cancelCurrencyPay:
|
||||
if params.PaySign, params.Signature, err = s.PaySignature(params.Path, params.Content); err != nil {
|
||||
return
|
||||
}
|
||||
case queryOrder, notifyProvideGoods, presentCurrency, downloadBill, refundOrder,
|
||||
createWithdrawOrder, queryWithdrawOrder, startUploadGoods, queryUploadGoods,
|
||||
startPublishGoods, queryPublishGoods, startDownloadOrder, queryDownloadOrder,
|
||||
queryBizBalance, queryTransferAccount, queryAdverFunds, createFundsBill,
|
||||
bindTransferAccount, queryFundsBill, queryRecoverBill, downloadAdverFundsOrder,
|
||||
getComplaintList, getComplaintDetail, getNegotiationHistory, responseComplaint,
|
||||
completeComplaint, uploadVPFile, getUploadFileSign:
|
||||
case queryOrder:
|
||||
case notifyProvideGoods:
|
||||
case presentCurrency:
|
||||
case downloadBill:
|
||||
case refundOrder:
|
||||
case createWithdrawOrder:
|
||||
case queryWithdrawOrder:
|
||||
case startUploadGoods:
|
||||
case queryUploadGoods:
|
||||
case startPublishGoods:
|
||||
case queryPublishGoods:
|
||||
if params.PaySign, err = s.PaySign(params.Path, params.Content); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
+5
-11
@@ -292,19 +292,13 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
// 安全地获取 *http.Transport
|
||||
var trans *http.Transport
|
||||
// 尝试从 DefaultHTTPClient 获取 Transport,如果失败则使用默认值
|
||||
var baseTransport http.RoundTripper
|
||||
if DefaultHTTPClient.Transport != nil {
|
||||
if t, ok := DefaultHTTPClient.Transport.(*http.Transport); ok {
|
||||
trans = t.Clone()
|
||||
}
|
||||
baseTransport = DefaultHTTPClient.Transport
|
||||
} else {
|
||||
baseTransport = http.DefaultTransport
|
||||
}
|
||||
// 如果无法获取有效的 Transport,使用默认值
|
||||
if trans == nil {
|
||||
trans = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
|
||||
trans := baseTransport.(*http.Transport).Clone()
|
||||
trans.TLSClientConfig = config
|
||||
trans.DisableCompression = true
|
||||
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")
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/silenceper/wechat/v2/aispeech"
|
||||
aispeechConfig "github.com/silenceper/wechat/v2/aispeech/config"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
"github.com/silenceper/wechat/v2/miniprogram"
|
||||
miniConfig "github.com/silenceper/wechat/v2/miniprogram/config"
|
||||
@@ -86,14 +84,6 @@ func (wc *Wechat) GetWork(cfg *workConfig.Config) *work.Work {
|
||||
return work.NewWork(cfg)
|
||||
}
|
||||
|
||||
// GetAISpeech 获取微信智能对话的实例
|
||||
func (wc *Wechat) GetAISpeech(cfg *aispeechConfig.Config) *aispeech.AISpeech {
|
||||
if cfg.Cache == nil {
|
||||
cfg.Cache = wc.cache
|
||||
}
|
||||
return aispeech.NewAISpeech(cfg)
|
||||
}
|
||||
|
||||
// SetHTTPClient 设置HTTPClient
|
||||
func (wc *Wechat) SetHTTPClient(client *http.Client) {
|
||||
util.DefaultHTTPClient = client
|
||||
|
||||
+12
-190
@@ -24,18 +24,6 @@ const (
|
||||
convertToOpenIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid"
|
||||
// convertToUserIDURL openID转userID
|
||||
convertToUserIDURL = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid"
|
||||
// userBatchDeleteURL 批量删除成员
|
||||
userBatchDeleteURL = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete?access_token=%s"
|
||||
// userAuthSuccURL 登录二次验证
|
||||
userAuthSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=%s&userid=%s"
|
||||
// batchInviteURL 邀请成员
|
||||
batchInviteURL = "https://qyapi.weixin.qq.com/cgi-bin/batch/invite?access_token=%s"
|
||||
// getJoinQrcodeURL 获取加入企业二维码
|
||||
getJoinQrcodeURL = "https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode"
|
||||
// getUseridURL 手机号获取userid
|
||||
getUseridURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=%s"
|
||||
// getUseridByEmailURL 邮箱获取userid
|
||||
getUseridByEmailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get_userid_by_email?access_token=%s"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -170,20 +158,18 @@ func (r *Client) UserCreate(req *UserCreateRequest) (*UserCreateResponse, error)
|
||||
|
||||
// UserUpdateRequest 更新成员请求
|
||||
type UserUpdateRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
NewUserID string `json:"new_userid"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Mobile string `json:"mobile"`
|
||||
Department []int `json:"department"`
|
||||
Order []int `json:"order"`
|
||||
Position string `json:"position"`
|
||||
Gender int `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
BizMail string `json:"biz_mail"`
|
||||
BizMailAlias struct {
|
||||
Item []string `json:"item"`
|
||||
} `json:"biz_mail_alias"`
|
||||
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 string `json:"biz_mail_alias"`
|
||||
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||
DirectLeader []string `json:"direct_leader"`
|
||||
Enable int `json:"enable"`
|
||||
@@ -459,167 +445,3 @@ func (r *Client) ConvertToUserID(openID string) (string, error) {
|
||||
err = util.DecodeWithError(response, result, "ConvertToUserID")
|
||||
return result.UserID, err
|
||||
}
|
||||
|
||||
// UserBatchDeleteRequest 批量删除成员请求
|
||||
type UserBatchDeleteRequest struct {
|
||||
UseridList []string `json:"useridlist"`
|
||||
}
|
||||
|
||||
// UserBatchDelete 批量删除成员
|
||||
// see https://developer.work.weixin.qq.com/document/path/90199
|
||||
func (r *Client) UserBatchDelete(req *UserBatchDeleteRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(userBatchDeleteURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "UserBatchDelete")
|
||||
}
|
||||
|
||||
// UserAuthSucc 登录二次验证
|
||||
// @see https://developer.work.weixin.qq.com/document/path/90203
|
||||
func (r *Client) UserAuthSucc(userID string) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(fmt.Sprintf(userAuthSuccURL, accessToken, userID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "UserAuthSucc")
|
||||
}
|
||||
|
||||
// BatchInviteRequest 邀请成员请求
|
||||
type BatchInviteRequest struct {
|
||||
User []string `json:"user"`
|
||||
Party []int `json:"party"`
|
||||
Tag []int `json:"tag"`
|
||||
}
|
||||
|
||||
// BatchInviteResponse 邀请成员响应
|
||||
type BatchInviteResponse struct {
|
||||
util.CommonError
|
||||
InvalidUser []string `json:"invaliduser"`
|
||||
InvalidParty []int `json:"invalidparty"`
|
||||
InvalidTag []int `json:"invalidtag"`
|
||||
}
|
||||
|
||||
// BatchInvite 邀请成员
|
||||
// see https://developer.work.weixin.qq.com/document/path/90975
|
||||
func (r *Client) BatchInvite(req *BatchInviteRequest) (*BatchInviteResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(batchInviteURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &BatchInviteResponse{}
|
||||
err = util.DecodeWithError(response, result, "BatchInvite")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetJoinQrcodeRequest 获取加入企业二维码请求
|
||||
type GetJoinQrcodeRequest struct {
|
||||
SizeType int `json:"size_type"`
|
||||
}
|
||||
|
||||
// GetJoinQrcodeResponse 获取加入企业二维码响应
|
||||
type GetJoinQrcodeResponse struct {
|
||||
util.CommonError
|
||||
JoinQrcode string `json:"join_qrcode"`
|
||||
}
|
||||
|
||||
// GetJoinQrcode 获取加入企业二维码
|
||||
// see https://developer.work.weixin.qq.com/document/path/91714
|
||||
func (r *Client) GetJoinQrcode(req *GetJoinQrcodeRequest) (*GetJoinQrcodeResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
apiURL string
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.SizeType > 0 {
|
||||
apiURL = fmt.Sprintf("%s?access_token=%s&size_type=%d", getJoinQrcodeURL, accessToken, req.SizeType)
|
||||
} else {
|
||||
apiURL = fmt.Sprintf("%s?access_token=%s", getJoinQrcodeURL, accessToken)
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(apiURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetJoinQrcodeResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetJoinQrcode")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetUseridRequest 手机号获取userid请求
|
||||
type GetUseridRequest struct {
|
||||
Mobile string `json:"mobile"`
|
||||
}
|
||||
|
||||
// GetUseridResponse 获取userid响应
|
||||
type GetUseridResponse struct {
|
||||
util.CommonError
|
||||
Userid string `json:"userid"`
|
||||
}
|
||||
|
||||
// GetUserid 手机号获取userid
|
||||
// see https://developer.work.weixin.qq.com/document/path/95402
|
||||
func (r *Client) GetUserid(req *GetUseridRequest) (*GetUseridResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getUseridURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetUseridResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetUserid")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetUseridByEmailRequest 邮箱获取userid请求
|
||||
type GetUseridByEmailRequest struct {
|
||||
Email string `json:"email"`
|
||||
EmailType int `json:"email_type,omitempty"`
|
||||
}
|
||||
|
||||
// GetUseridByEmail 邮箱获取userid
|
||||
// see https://developer.work.weixin.qq.com/document/path/95895
|
||||
func (r *Client) GetUseridByEmail(req *GetUseridByEmailRequest) (*GetUseridResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getUseridByEmailURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetUseridResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetUseridByEmail")
|
||||
return result, err
|
||||
}
|
||||
+14
-29
@@ -158,32 +158,24 @@ type OptionGroupRuleCheckinDate struct {
|
||||
MaxAllowArriveEarly int64 `json:"max_allow_arrive_early"`
|
||||
MaxAllowArriveLate int64 `json:"max_allow_arrive_late"`
|
||||
LateRule OptionGroupLateRule `json:"late_rule"`
|
||||
Biweekly OptionGroupBiweekly `json:"biweekly,omitempty"`
|
||||
}
|
||||
|
||||
// OptionGroupRuleCheckinTime 工作日上下班打卡时间信息
|
||||
type OptionGroupRuleCheckinTime struct {
|
||||
TimeID int64 `json:"time_id"`
|
||||
WorkSec int64 `json:"work_sec"`
|
||||
OffWorkSec int64 `json:"off_work_sec"`
|
||||
RemindWorkSec int64 `json:"remind_work_sec"`
|
||||
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
|
||||
AllowRest bool `json:"allow_rest"`
|
||||
RestBeginTime int64 `json:"rest_begin_time"`
|
||||
RestEndTime int64 `json:"rest_end_time"`
|
||||
EarliestWorkSec int64 `json:"earliest_work_sec"`
|
||||
LatestWorkSec int64 `json:"latest_work_sec"`
|
||||
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
|
||||
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
|
||||
NoNeedCheckOn bool `json:"no_need_checkon"`
|
||||
NoNeedCheckOff bool `json:"no_need_checkoff"`
|
||||
RestTimes []OptionGroupRuleRestTimes `json:"rest_times,omitempty"`
|
||||
}
|
||||
|
||||
// OptionGroupRuleRestTimes 多组休息时间
|
||||
type OptionGroupRuleRestTimes struct {
|
||||
RestBeginTime int64 `json:"rest_begin_time,omitempty"`
|
||||
RestEndTime int64 `json:"rest_end_time,omitempty"`
|
||||
TimeID int64 `json:"time_id"`
|
||||
WorkSec int64 `json:"work_sec"`
|
||||
OffWorkSec int64 `json:"off_work_sec"`
|
||||
RemindWorkSec int64 `json:"remind_work_sec"`
|
||||
RemindOffWorkSec int64 `json:"remind_off_work_sec"`
|
||||
AllowRest bool `json:"allow_rest"`
|
||||
RestBeginTime int64 `json:"rest_begin_time"`
|
||||
RestEndTime int64 `json:"rest_end_time"`
|
||||
EarliestWorkSec int64 `json:"earliest_work_sec"`
|
||||
LatestWorkSec int64 `json:"latest_work_sec"`
|
||||
EarliestOffWorkSec int64 `json:"earliest_off_work_sec"`
|
||||
LatestOffWorkSec int64 `json:"latest_off_work_sec"`
|
||||
NoNeedCheckOn bool `json:"no_need_checkon"`
|
||||
NoNeedCheckOff bool `json:"no_need_checkoff"`
|
||||
}
|
||||
|
||||
// OptionGroupLateRule 晚走晚到时间规则信息
|
||||
@@ -200,13 +192,6 @@ type OptionGroupTimeRule struct {
|
||||
OnWorkFlexTime int64 `json:"onwork_flex_time"`
|
||||
}
|
||||
|
||||
// OptionGroupBiweekly 大小周规则
|
||||
type OptionGroupBiweekly struct {
|
||||
EnableWeekdayRecurrence bool `json:"enable_weekday_recurrence"`
|
||||
OddWorkdays []int64 `json:"odd_workdays"`
|
||||
EvenWorkdays []int64 `json:"even_workdays"`
|
||||
}
|
||||
|
||||
// OptionGroupSpeWorkdays 特殊工作日
|
||||
type OptionGroupSpeWorkdays struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
|
||||
@@ -17,8 +17,6 @@ const (
|
||||
listContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_contact_way?access_token=%s"
|
||||
// delContactWayURL 删除企业已配置的「联系我」方式
|
||||
delContactWayURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/del_contact_way?access_token=%s"
|
||||
// closeTempChatURL 结束临时会话
|
||||
closeTempChatURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/close_temp_chat?access_token=%s"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -79,8 +77,6 @@ type (
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ChatExpiresIn int `json:"chat_expires_in"`
|
||||
UnionID string `json:"unionid"`
|
||||
IsExclusive bool `json:"is_exclusive"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||
}
|
||||
// AddContactWayResponse 配置客户联系「联系我」方式响应
|
||||
@@ -136,7 +132,6 @@ type (
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ChatExpiresIn int `json:"chat_expires_in"`
|
||||
UnionID string `json:"unionid"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
Conclusions ConclusionsResponse `json:"conclusions"`
|
||||
}
|
||||
)
|
||||
@@ -173,7 +168,6 @@ type (
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ChatExpiresIn int `json:"chat_expires_in"`
|
||||
UnionID string `json:"unionid"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
Conclusions ConclusionsRequest `json:"conclusions"`
|
||||
}
|
||||
// UpdateContactWayResponse 更新企业已配置的「联系我」方式响应
|
||||
@@ -269,26 +263,3 @@ func (r *Client) DelContactWay(req *DelContactWayRequest) (*DelContactWayRespons
|
||||
err = util.DecodeWithError(response, result, "DelContactWay")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CloseTempChatRequest 结束临时会话请求
|
||||
type CloseTempChatRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
ExternalUserID string `json:"external_userid"`
|
||||
}
|
||||
|
||||
// CloseTempChat 结束临时会话
|
||||
// @see https://developer.work.weixin.qq.com/document/path/92228
|
||||
func (r *Client) CloseTempChat(req *CloseTempChatRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(closeTempChatURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "CloseTempChat")
|
||||
}
|
||||
@@ -25,8 +25,6 @@ const (
|
||||
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 (
|
||||
@@ -70,10 +68,9 @@ type (
|
||||
// GetCustomerAcquisitionResponse 获取获客链接详情响应
|
||||
GetCustomerAcquisitionResponse struct {
|
||||
util.CommonError
|
||||
Link Link `json:"link"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
Link Link `json:"link"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
}
|
||||
// Link 获客链接
|
||||
Link struct {
|
||||
@@ -81,8 +78,6 @@ type (
|
||||
LinkName string `json:"link_name"`
|
||||
URL string `json:"url"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
|
||||
// CustomerAcquisitionRange 该获客链接使用范围
|
||||
@@ -90,12 +85,6 @@ type (
|
||||
UserList []string `json:"user_list"`
|
||||
DepartmentList []int64 `json:"department_list"`
|
||||
}
|
||||
|
||||
// CustomerPriorityOption 该获客链接的优先选项
|
||||
CustomerPriorityOption struct {
|
||||
PriorityType int `json:"priority_type"`
|
||||
PriorityUseridList []string `json:"priority_userid_list"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetCustomerAcquisition 获客助手--获取获客链接详情
|
||||
@@ -120,11 +109,9 @@ func (r *Client) GetCustomerAcquisition(req *GetCustomerAcquisitionRequest) (*Ge
|
||||
type (
|
||||
// CreateCustomerAcquisitionLinkRequest 创建获客链接请求
|
||||
CreateCustomerAcquisitionLinkRequest struct {
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
}
|
||||
// CreateCustomerAcquisitionLinkResponse 创建获客链接响应
|
||||
CreateCustomerAcquisitionLinkResponse struct {
|
||||
@@ -155,12 +142,10 @@ func (r *Client) CreateCustomerAcquisitionLink(req *CreateCustomerAcquisitionLin
|
||||
type (
|
||||
// UpdateCustomerAcquisitionLinkRequest 编辑获客链接请求
|
||||
UpdateCustomerAcquisitionLinkRequest struct {
|
||||
LinkID string `json:"link_id"`
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
PriorityOption CustomerPriorityOption `json:"priority_option"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
LinkID string `json:"link_id"`
|
||||
LinkName string `json:"link_name"`
|
||||
Range CustomerAcquisitionRange `json:"range"`
|
||||
SkipVerify bool `json:"skip_verify"`
|
||||
}
|
||||
// UpdateCustomerAcquisitionLinkResponse 编辑获客链接响应
|
||||
UpdateCustomerAcquisitionLinkResponse struct {
|
||||
@@ -364,30 +349,3 @@ func (r *Client) GetChatInfo(req *GetChatInfoRequest) (*GetChatInfoResponse, err
|
||||
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
|
||||
}
|
||||
@@ -187,16 +187,16 @@ type ExternalUserForBatch struct {
|
||||
|
||||
// ExternalContact 批量获取外部联系人用户信息
|
||||
type ExternalContact struct {
|
||||
ExternalUserID string `json:"external_userid"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position"`
|
||||
Avatar string `json:"avatar"`
|
||||
CorpName string `json:"corp_name"`
|
||||
CorpFullName string `json:"corp_full_name"`
|
||||
Type int64 `json:"type"`
|
||||
Gender int64 `json:"gender"`
|
||||
UnionID string `json:"unionid"`
|
||||
ExternalProfile *ExternalProfile `json:"external_profile"`
|
||||
ExternalUserID string `json:"external_userid"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position"`
|
||||
Avatar string `json:"avatar"`
|
||||
CorpName string `json:"corp_name"`
|
||||
CorpFullName string `json:"corp_full_name"`
|
||||
Type int64 `json:"type"`
|
||||
Gender int64 `json:"gender"`
|
||||
UnionID string `json:"unionid"`
|
||||
ExternalProfile string `json:"external_profile"`
|
||||
}
|
||||
|
||||
// FollowInfo 批量获取外部联系人跟进人信息
|
||||
|
||||
@@ -19,7 +19,6 @@ type (
|
||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
|
||||
// AddJoinWayResponse 添加群配置返回值
|
||||
@@ -66,7 +65,6 @@ type (
|
||||
ChatIDList []string `json:"chat_id_list"`
|
||||
QrCode string `json:"qr_code"`
|
||||
State string `json:"state"`
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
//GetJoinWayResponse 获取群配置的返回值
|
||||
GetJoinWayResponse struct {
|
||||
@@ -105,7 +103,6 @@ type UpdateJoinWayRequest struct {
|
||||
RoomBaseID int `json:"room_base_id"` //非必填 自动建群的群起始序号,当auto_create_room为1时有效
|
||||
ChatIDList []string `json:"chat_id_list"` //必填 使用该配置的客户群ID列表,支持5个。见客户群ID获取方法
|
||||
State string `json:"state"` //非必填 企业自定义的state参数,用于区分不同的入群渠道。不超过30个UTF-8字符
|
||||
MarkSource bool `json:"mark_source"`
|
||||
}
|
||||
|
||||
// UpdateJoinWay 更新客户群进群方式配置
|
||||
|
||||
@@ -27,10 +27,6 @@ var (
|
||||
getUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s"
|
||||
// getUserDetailURL 获取访问用户敏感信息
|
||||
getUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s"
|
||||
// getTfaInfoURL 获取用户二次验证信息
|
||||
getTfaInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/get_tfa_info?access_token=%s"
|
||||
// tfaSuccURL 使用二次验证
|
||||
tfaSuccURL = "https://qyapi.weixin.qq.com/cgi-bin/user/tfa_succ?access_token=%s"
|
||||
)
|
||||
|
||||
// NewOauth new init oauth
|
||||
@@ -167,57 +163,3 @@ func (ctr *Oauth) GetUserDetail(req *GetUserDetailRequest) (*GetUserDetailRespon
|
||||
err = util.DecodeWithError(response, result, "GetUserDetail")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetTfaInfoRequest 获取用户二次验证信息请求
|
||||
type GetTfaInfoRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// GetTfaInfoResponse 获取用户二次验证信息响应
|
||||
type GetTfaInfoResponse struct {
|
||||
util.CommonError
|
||||
UserID string `json:"userid"`
|
||||
TfaCode string `json:"tfa_code"`
|
||||
}
|
||||
|
||||
// GetTfaInfo 获取用户二次验证信息
|
||||
// @see https://developer.work.weixin.qq.com/document/path/99499
|
||||
func (ctr *Oauth) GetTfaInfo(req *GetTfaInfoRequest) (*GetTfaInfoResponse, error) {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = ctr.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getTfaInfoURL, accessToken), req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &GetTfaInfoResponse{}
|
||||
err = util.DecodeWithError(response, result, "GetTfaInfo")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TfaSuccRequest 使用二次验证请求
|
||||
type TfaSuccRequest struct {
|
||||
UserID string `json:"userid"`
|
||||
TfaCode string `json:"tfa_code"`
|
||||
}
|
||||
|
||||
// TfaSucc 使用二次验证
|
||||
// @see https://developer.work.weixin.qq.com/document/path/99500
|
||||
func (ctr *Oauth) TfaSucc(req *TfaSuccRequest) error {
|
||||
var (
|
||||
accessToken string
|
||||
err error
|
||||
)
|
||||
if accessToken, err = ctr.GetAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(fmt.Sprintf(tfaSuccURL, accessToken), req); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "TfaSucc")
|
||||
}
|
||||
Reference in new issue
Block a user