Compare commits

..
40 changed files with 124 additions and 4506 deletions

No files matched your search

+2 -100
View File
@@ -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
```
## 快速入门
-60
View File
@@ -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
}
-61
View File
@@ -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)
}
}
-23
View File
@@ -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
}
-15
View File
@@ -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())
}
}
-12
View File
@@ -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
}
-116
View File
@@ -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
}
-120
View File
@@ -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
}
-422
View File
@@ -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)
}
}
-186
View File
@@ -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)
}
}
-48
View File
@@ -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"`
}
-137
View File
@@ -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
}
-89
View File
@@ -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
}
-100
View File
@@ -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
}
-33
View File
@@ -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")
}
}
-20
View File
@@ -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))
}
+27 -43
View File
@@ -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)
}
-116
View File
@@ -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()
}
+10 -48
View File
@@ -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 {
+1 -267
View File
@@ -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)
}
}
+2 -6
View File
@@ -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
View File
@@ -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
-618
View File
@@ -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")
}
-83
View File
@@ -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"` // 请求编号
}
-11
View File
@@ -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)
}
-248
View File
@@ -1,248 +0,0 @@
package ocr
import (
"fmt"
"net/url"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)
const (
ocrIDCardURL = "https://api.weixin.qq.com/cv/ocr/idcard"
ocrBankCardURL = "https://api.weixin.qq.com/cv/ocr/bankcard"
ocrDrivingURL = "https://api.weixin.qq.com/cv/ocr/driving"
ocrDrivingLicenseURL = "https://api.weixin.qq.com/cv/ocr/drivinglicense"
ocrBizLicenseURL = "https://api.weixin.qq.com/cv/ocr/bizlicense"
ocrCommonURL = "https://api.weixin.qq.com/cv/ocr/comm"
)
// OCR struct
type OCR struct {
*context.Context
}
// coordinate 坐标
type coordinate struct {
X int64 `json:"x,omitempty"`
Y int64 `json:"y,omitempty"`
}
// position 位置
type position struct {
LeftTop coordinate `json:"left_top"`
RightTop coordinate `json:"right_top"`
RightBottom coordinate `json:"right_bottom"`
LeftBottom coordinate `json:"left_bottom"`
}
// imageSize 图片尺寸
type imageSize struct {
Width int64 `json:"w,omitempty"`
Height int64 `json:"h,omitempty"`
}
// ResDriving 行驶证返回结果
type ResDriving struct {
util.CommonError
PlateNumber string `json:"plate_num,omitempty"`
VehicleType string `json:"vehicle_type,omitempty"`
Owner string `json:"owner,omitempty"`
Address string `json:"addr,omitempty"`
UseCharacter string `json:"use_character,omitempty"`
Model string `json:"model,omitempty"`
Vin string `json:"vin,omitempty"`
EngineNumber string `json:"engine_num,omitempty"`
RegisterDate string `json:"register_date,omitempty"`
IssueDate string `json:"issue_date,omitempty"`
PlateNumberB string `json:"plate_num_b,omitempty"`
Record string `json:"record,omitempty"`
PassengersNumber string `json:"passengers_num,omitempty"`
TotalQuality string `json:"total_quality,omitempty"`
PrepareQuality string `json:"prepare_quality,omitempty"`
OverallSize string `json:"overall_size,omitempty"`
CardPositionFront map[string]position `json:"card_position_front,omitempty"`
CardPositionBack map[string]position `json:"card_position_back,omitempty"`
ImageSize imageSize `json:"img_size,omitempty"`
}
// ResIDCard 身份证返回结果
type ResIDCard struct {
util.CommonError
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
Address string `json:"addr,omitempty"`
Gender string `json:"gender,omitempty"`
Nationality string `json:"nationality,omitempty"`
ValidDate string `json:"valid_date,omitempty"`
}
// ResBankCard 银行卡返回结果
type ResBankCard struct {
util.CommonError
Number string `json:"number,omitempty"`
}
// ResDrivingLicense 驾驶证返回结果
type ResDrivingLicense struct {
util.CommonError
IDNumber string `json:"id_num,omitempty"`
Name string `json:"name,omitempty"`
Sex string `json:"sex,omitempty"`
Nationality string `json:"nationality,omitempty"`
Address string `json:"address,omitempty"`
Birthday string `json:"birth_date,omitempty"`
IssueDate string `json:"issue_date,omitempty"`
CarClass string `json:"car_class,omitempty"`
ValidFrom string `json:"valid_from,omitempty"`
ValidTo string `json:"valid_to,omitempty"`
OfficialSeal string `json:"official_seal,omitempty"`
}
// ResBizLicense 营业执照返回结果
type ResBizLicense struct {
util.CommonError
RegisterNumber string `json:"reg_num,omitempty"`
Serial string `json:"serial,omitempty"`
LegalRepresentative string `json:"legal_representative,omitempty"`
EnterpriseName string `json:"enterprise_name,omitempty"`
TypeOfOrganization string `json:"type_of_organization,omitempty"`
Address string `json:"address,omitempty"`
TypeOfEnterprise string `json:"type_of_enterprise,omitempty"`
BusinessScope string `json:"business_scope,omitempty"`
RegisteredCapital string `json:"registered_capital,omitempty"`
PaidInCapital string `json:"paid_in_capital,omitempty"`
ValidPeriod string `json:"valid_period,omitempty"`
RegisterDate string `json:"registered_date,omitempty"`
CertPosition map[string]position `json:"cert_position,omitempty"`
ImageSize imageSize `json:"img_size,omitempty"`
}
// ResCommon 公共印刷品返回结果
type ResCommon struct {
util.CommonError
Items []commonItem `json:"items,omitempty"`
ImageSize imageSize `json:"img_size,omitempty"`
}
// commonItem 公共元素
type commonItem struct {
Position position `json:"pos"`
Text string `json:"text"`
}
// NewOCR 实例
func NewOCR(c *context.Context) *OCR {
ocr := new(OCR)
ocr.Context = c
return ocr
}
// IDCard 身份证OCR识别接口
func (ocr *OCR) IDCard(path string) (resIDCard ResIDCard, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrIDCardURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resIDCard, "OCRIDCard")
return
}
// BankCard 银行卡OCR识别接口
func (ocr *OCR) BankCard(path string) (resBankCard ResBankCard, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBankCardURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resBankCard, "OCRBankCard")
return
}
// Driving 行驶证OCR识别接口
func (ocr *OCR) Driving(path string) (resDriving ResDriving, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resDriving, "OCRDriving")
return
}
// DrivingLicense 驾驶证OCR识别接口
func (ocr *OCR) DrivingLicense(path string) (resDrivingLicense ResDrivingLicense, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrDrivingLicenseURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resDrivingLicense, "OCRDrivingLicense")
return
}
// BizLicense 营业执照OCR识别接口
func (ocr *OCR) BizLicense(path string) (resBizLicense ResBizLicense, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrBizLicenseURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resBizLicense, "OCRBizLicense")
return
}
// Common 通用印刷体OCR识别接口
func (ocr *OCR) Common(path string) (resCommon ResCommon, err error) {
accessToken, err := ocr.GetAccessToken()
if err != nil {
return
}
response, err := util.HTTPPost(fmt.Sprintf("%s?img_url=%s&access_token=%s", ocrCommonURL, url.QueryEscape(path), accessToken), "")
if err != nil {
return
}
err = util.DecodeWithError(response, &resCommon, "OCRCommon")
return
}
+1 -1
View File
@@ -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 请求并返回二维码二进制数据
-75
View File
@@ -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 (
+1 -443
View File
@@ -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"`
+28 -522
View File
@@ -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
View File
@@ -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}
-81
View File
@@ -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")
}
-10
View File
@@ -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
View File
@@ -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
View File
@@ -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"`
-29
View File
@@ -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")
}
+10 -52
View File
@@ -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
}
+10 -10
View File
@@ -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 批量获取外部联系人跟进人信息
-3
View File
@@ -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 更新客户群进群方式配置
-58
View File
@@ -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")
}