fadd
This commit is contained in:
@@ -31,8 +31,6 @@ func ProcessQYGL101ARequest(ctx context.Context, params []byte, deps *processors
|
|||||||
return nil, errors.Join(processors.ErrSystem, errors.New("北京正信服务未初始化"))
|
return nil, errors.Join(processors.ErrSystem, errors.New("北京正信服务未初始化"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiPath 入参;CallAPI 内拼接为 /openapi/v1/a01/person-company-relations
|
|
||||||
// 上游 idCard 要求 32 位 MD5(兼容 64 位 SHA-256),平台侧收明文后在此做摘要
|
|
||||||
result, err := deps.YuyuechaService.CallAPI(ctx, "a01/person-company-relations", map[string]interface{}{
|
result, err := deps.YuyuechaService.CallAPI(ctx, "a01/person-company-relations", map[string]interface{}{
|
||||||
"name": paramsDto.Name,
|
"name": paramsDto.Name,
|
||||||
"idCard": yuyuecha.HashIDCard(paramsDto.IDCard),
|
"idCard": yuyuecha.HashIDCard(paramsDto.IDCard),
|
||||||
@@ -37,16 +37,21 @@ func isHexDigest(s string, length int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignHeaders 按 OpenAPI 规范生成签名请求头
|
// SignHeaders 按 OpenAPI 规范生成签名请求头。
|
||||||
|
// nonce 传入本地请求流水 ID(与日志 request_id 一致),便于上下游按同一 ID 排查;为空时回退随机生成。
|
||||||
// canonical:
|
// canonical:
|
||||||
//
|
//
|
||||||
// clientId={id}\ntimestamp={ms}\nnonce={hex}\nmethod={METHOD}\npath={path}\nbodySha256={sha256}
|
// clientId={id}\ntimestamp={ms}\nnonce={nonce}\nmethod={METHOD}\npath={path}\nbodySha256={sha256}
|
||||||
func SignHeaders(clientID, clientSecret, method, path string, body []byte) (map[string]string, error) {
|
func SignHeaders(clientID, clientSecret, method, path string, body []byte, nonce string) (map[string]string, error) {
|
||||||
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||||
nonce, err := randomNonce(16)
|
nonce = strings.TrimSpace(nonce)
|
||||||
|
if nonce == "" {
|
||||||
|
var err error
|
||||||
|
nonce, err = randomNonce(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("生成 nonce 失败: %w", err)
|
return nil, fmt.Errorf("生成 nonce 失败: %w", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sum := sha256.Sum256(body)
|
sum := sha256.Sum256(body)
|
||||||
bodySHA256 := hex.EncodeToString(sum[:])
|
bodySHA256 := hex.EncodeToString(sum[:])
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
package yuyuecha
|
package yuyuecha
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 签名头固定顺序,便于阅读与对比
|
||||||
|
var preferredHeaderOrder = []string{
|
||||||
|
"Content-Type",
|
||||||
|
"clientId",
|
||||||
|
"timestamp",
|
||||||
|
"nonce",
|
||||||
|
"signature",
|
||||||
|
}
|
||||||
|
|
||||||
// generateCurlCommand 生成可复现的 curl 命令,便于数据源排查
|
// generateCurlCommand 生成可复现的 curl 命令,便于数据源排查
|
||||||
func generateCurlCommand(method, url string, headers map[string]string, body string) string {
|
func generateCurlCommand(method, url string, headers map[string]string, body string) string {
|
||||||
@@ -11,11 +23,11 @@ func generateCurlCommand(method, url string, headers map[string]string, body str
|
|||||||
cmd.WriteString(url)
|
cmd.WriteString(url)
|
||||||
cmd.WriteString("'")
|
cmd.WriteString("'")
|
||||||
|
|
||||||
for key, value := range headers {
|
for _, key := range orderedHeaderKeys(headers) {
|
||||||
cmd.WriteString(" \\\n -H '")
|
cmd.WriteString(" \\\n -H '")
|
||||||
cmd.WriteString(escapeShellSingleQuote(key))
|
cmd.WriteString(escapeShellSingleQuote(key))
|
||||||
cmd.WriteString(": ")
|
cmd.WriteString(": ")
|
||||||
cmd.WriteString(escapeShellSingleQuote(value))
|
cmd.WriteString(escapeShellSingleQuote(headers[key]))
|
||||||
cmd.WriteString("'")
|
cmd.WriteString("'")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +40,25 @@ func generateCurlCommand(method, url string, headers map[string]string, body str
|
|||||||
return cmd.String()
|
return cmd.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func orderedHeaderKeys(headers map[string]string) []string {
|
||||||
|
seen := make(map[string]struct{}, len(headers))
|
||||||
|
keys := make([]string, 0, len(headers))
|
||||||
|
for _, key := range preferredHeaderOrder {
|
||||||
|
if _, ok := headers[key]; ok {
|
||||||
|
keys = append(keys, key)
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rest := make([]string, 0, len(headers))
|
||||||
|
for key := range headers {
|
||||||
|
if _, ok := seen[key]; !ok {
|
||||||
|
rest = append(rest, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(rest)
|
||||||
|
return append(keys, rest...)
|
||||||
|
}
|
||||||
|
|
||||||
func escapeShellSingleQuote(s string) string {
|
func escapeShellSingleQuote(s string) string {
|
||||||
return strings.ReplaceAll(s, "'", `'\''`)
|
return strings.ReplaceAll(s, "'", `'\''`)
|
||||||
}
|
}
|
||||||
|
|||||||
107
internal/infrastructure/external/yuyuecha/debug_exchange.go
vendored
Normal file
107
internal/infrastructure/external/yuyuecha/debug_exchange.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package yuyuecha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const debugExchangeFileName = "dev_curl.log"
|
||||||
|
|
||||||
|
var debugExchangeMu sync.Mutex
|
||||||
|
|
||||||
|
type debugExchangeRecord struct {
|
||||||
|
RequestID string
|
||||||
|
TransactionID string
|
||||||
|
API string
|
||||||
|
Status int
|
||||||
|
Duration time.Duration
|
||||||
|
Curl string
|
||||||
|
ResponseBody []byte
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDebugExchange 开发环境写入单一排查文件:一次调用一条记录,含 curl + response
|
||||||
|
func writeDebugExchange(logDir string, rec debugExchangeRecord) {
|
||||||
|
if strings.TrimSpace(logDir) == "" || strings.TrimSpace(rec.Curl) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(strings.Repeat("=", 72))
|
||||||
|
b.WriteByte('\n')
|
||||||
|
b.WriteString("time: ")
|
||||||
|
b.WriteString(time.Now().Format("2006-01-02 15:04:05.000"))
|
||||||
|
b.WriteByte('\n')
|
||||||
|
b.WriteString("request_id: ")
|
||||||
|
b.WriteString(rec.RequestID)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
// 与上游签名头 nonce 一致,对外排查只提供 request_id 即可
|
||||||
|
b.WriteString("nonce: ")
|
||||||
|
b.WriteString(rec.RequestID)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
if rec.TransactionID != "" {
|
||||||
|
b.WriteString("transaction_id: ")
|
||||||
|
b.WriteString(rec.TransactionID)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
b.WriteString("api: ")
|
||||||
|
b.WriteString(rec.API)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
if rec.Status > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf("status: %d\n", rec.Status))
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf("duration: %s\n", rec.Duration.Round(time.Millisecond)))
|
||||||
|
if rec.Error != "" {
|
||||||
|
b.WriteString("error: ")
|
||||||
|
b.WriteString(rec.Error)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteByte('\n')
|
||||||
|
b.WriteString("[curl]\n")
|
||||||
|
b.WriteString(rec.Curl)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
|
||||||
|
b.WriteByte('\n')
|
||||||
|
b.WriteString("[response]\n")
|
||||||
|
if len(rec.ResponseBody) == 0 {
|
||||||
|
b.WriteString("(empty)\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString(prettyJSONForLog(rec.ResponseBody))
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
b.WriteString(strings.Repeat("=", 72))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
debugExchangeMu.Lock()
|
||||||
|
defer debugExchangeMu.Unlock()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := filepath.Join(logDir, debugExchangeFileName)
|
||||||
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, _ = f.WriteString(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyJSONForLog(raw []byte) string {
|
||||||
|
trimmed := bytes.TrimSpace(raw)
|
||||||
|
if len(trimmed) == 0 {
|
||||||
|
return "(empty)"
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := json.Indent(&buf, trimmed, "", " "); err != nil {
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package yuyuecha
|
package yuyuecha
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"tyapi-server/internal/config"
|
"tyapi-server/internal/config"
|
||||||
"tyapi-server/internal/shared/external_logger"
|
"tyapi-server/internal/shared/external_logger"
|
||||||
)
|
)
|
||||||
@@ -38,12 +40,18 @@ func NewYuyuechaServiceWithConfig(cfg *config.Config) (*YuyuechaService, error)
|
|||||||
timeout = cfg.Yuyuecha.Timeout
|
timeout = cfg.Yuyuecha.Timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logDir := cfg.Yuyuecha.Logging.LogDir
|
||||||
|
if logDir == "" {
|
||||||
|
logDir = "logs/external_services"
|
||||||
|
}
|
||||||
|
|
||||||
serviceCfg := ServiceConfig{
|
serviceCfg := ServiceConfig{
|
||||||
BaseURL: cfg.Yuyuecha.BaseURL,
|
BaseURL: cfg.Yuyuecha.BaseURL,
|
||||||
ClientID: cfg.Yuyuecha.ClientID,
|
ClientID: cfg.Yuyuecha.ClientID,
|
||||||
ClientSecret: cfg.Yuyuecha.ClientSecret,
|
ClientSecret: cfg.Yuyuecha.ClientSecret,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
Debug: cfg.App.IsDevelopment(),
|
Debug: cfg.App.IsDevelopment(),
|
||||||
|
DebugLogDir: filepath.Join(logDir, "yuyuecha"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewYuyuechaService(serviceCfg, logger), nil
|
return NewYuyuechaService(serviceCfg, logger), nil
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tyapi-server/internal/shared/external_logger"
|
"tyapi-server/internal/shared/external_logger"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -39,8 +37,10 @@ type ServiceConfig struct {
|
|||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// Debug 开发环境开启时输出可复现 curl 与完整响应体,便于数据源排查
|
// Debug 开发环境开启时写入单一排查文件(curl + response),不重复打 request/response 日志
|
||||||
Debug bool
|
Debug bool
|
||||||
|
// DebugLogDir 排查日志目录,文件名为 dev_curl.log
|
||||||
|
DebugLogDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// YuyuechaService 愉悦查 OpenAPI 客户端
|
// YuyuechaService 愉悦查 OpenAPI 客户端
|
||||||
@@ -127,11 +127,13 @@ func (s *YuyuechaService) doRequest(ctx context.Context, method, path string, bo
|
|||||||
baseURL := strings.TrimSuffix(s.config.BaseURL, "/")
|
baseURL := strings.TrimSuffix(s.config.BaseURL, "/")
|
||||||
reqURL := baseURL + path
|
reqURL := baseURL + path
|
||||||
|
|
||||||
if s.logger != nil {
|
// dev 排查走单一文件,避免 request/response/info 重复打点
|
||||||
|
if !s.config.Debug && s.logger != nil {
|
||||||
s.logger.LogRequest(requestID, transactionID, path, reqURL)
|
s.logger.LogRequest(requestID, transactionID, path, reqURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
signHeaders, err := SignHeaders(s.config.ClientID, s.config.ClientSecret, method, path, bodyBytes)
|
// nonce 与本地 request_id 保持一致,后续仅凭 request_id 即可对接上游排查
|
||||||
|
signHeaders, err := SignHeaders(s.config.ClientID, s.config.ClientSecret, method, path, bodyBytes, requestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Join(ErrSystem, err)
|
err = errors.Join(ErrSystem, err)
|
||||||
s.logError(requestID, transactionID, path, err, map[string]interface{}{"request_params": logParams})
|
s.logError(requestID, transactionID, path, err, map[string]interface{}{"request_params": logParams})
|
||||||
@@ -172,11 +174,8 @@ func (s *YuyuechaService) doRequest(ctx context.Context, method, path string, bo
|
|||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = wrapHTTPError(err)
|
err = wrapHTTPError(err)
|
||||||
errParams := map[string]interface{}{"request_params": logParams}
|
s.logDebugExchange(requestID, transactionID, path, curlCmd, 0, time.Since(startTime), nil, err.Error())
|
||||||
if curlCmd != "" {
|
s.logError(requestID, transactionID, path, err, map[string]interface{}{"request_params": logParams})
|
||||||
errParams["curl"] = curlCmd
|
|
||||||
}
|
|
||||||
s.logError(requestID, transactionID, path, err, errParams)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -185,45 +184,34 @@ func (s *YuyuechaService) doRequest(ctx context.Context, method, path string, bo
|
|||||||
raw, err := io.ReadAll(resp.Body)
|
raw, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Join(ErrSystem, err)
|
err = errors.Join(ErrSystem, err)
|
||||||
errParams := map[string]interface{}{"request_params": logParams}
|
s.logDebugExchange(requestID, transactionID, path, curlCmd, resp.StatusCode, duration, nil, err.Error())
|
||||||
if curlCmd != "" {
|
s.logError(requestID, transactionID, path, err, map[string]interface{}{"request_params": logParams})
|
||||||
errParams["curl"] = curlCmd
|
|
||||||
}
|
|
||||||
s.logError(requestID, transactionID, path, err, errParams)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logDebugCurlAndResponse(requestID, transactionID, path, curlCmd, resp.StatusCode, duration, raw)
|
s.logDebugExchange(requestID, transactionID, path, curlCmd, resp.StatusCode, duration, raw, "")
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
mapped := mapHTTPStatusError(resp.StatusCode, raw)
|
mapped := mapHTTPStatusError(resp.StatusCode, raw)
|
||||||
errParams := map[string]interface{}{
|
s.logError(requestID, transactionID, path, mapped, map[string]interface{}{
|
||||||
"request_params": logParams,
|
"request_params": logParams,
|
||||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||||
"http_status": resp.StatusCode,
|
"http_status": resp.StatusCode,
|
||||||
}
|
})
|
||||||
if curlCmd != "" {
|
|
||||||
errParams["curl"] = curlCmd
|
|
||||||
}
|
|
||||||
s.logError(requestID, transactionID, path, mapped, errParams)
|
|
||||||
return nil, mapped
|
return nil, mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.logger != nil {
|
if !s.config.Debug && s.logger != nil {
|
||||||
s.logger.LogResponse(requestID, transactionID, path, resp.StatusCode, duration)
|
s.logger.LogResponse(requestID, transactionID, path, resp.StatusCode, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
var outer APIResponse
|
var outer APIResponse
|
||||||
if err := json.Unmarshal(raw, &outer); err != nil {
|
if err := json.Unmarshal(raw, &outer); err != nil {
|
||||||
parseErr := errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
parseErr := errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||||
errParams := map[string]interface{}{
|
s.logError(requestID, transactionID, path, parseErr, map[string]interface{}{
|
||||||
"request_params": logParams,
|
"request_params": logParams,
|
||||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||||
}
|
})
|
||||||
if curlCmd != "" {
|
|
||||||
errParams["curl"] = curlCmd
|
|
||||||
}
|
|
||||||
s.logError(requestID, transactionID, path, parseErr, errParams)
|
|
||||||
return nil, parseErr
|
return nil, parseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,14 +223,10 @@ func (s *YuyuechaService) doRequest(ctx context.Context, method, path string, bo
|
|||||||
msg = *outer.ErrCode
|
msg = *outer.ErrCode
|
||||||
}
|
}
|
||||||
mapped := errors.Join(ErrDatasource, errors.New(msg))
|
mapped := errors.Join(ErrDatasource, errors.New(msg))
|
||||||
errParams := map[string]interface{}{
|
s.logError(requestID, transactionID, path, mapped, map[string]interface{}{
|
||||||
"request_params": logParams,
|
"request_params": logParams,
|
||||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||||
}
|
})
|
||||||
if curlCmd != "" {
|
|
||||||
errParams["curl"] = curlCmd
|
|
||||||
}
|
|
||||||
s.logError(requestID, transactionID, path, mapped, errParams)
|
|
||||||
return nil, mapped
|
return nil, mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,18 +313,18 @@ func (s *YuyuechaService) logError(requestID, transactionID, apiCode string, err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// logDebugCurlAndResponse 仅在 Debug(开发环境)输出完整 curl 与响应体
|
func (s *YuyuechaService) logDebugExchange(requestID, transactionID, apiCode, curlCmd string, statusCode int, duration time.Duration, raw []byte, errMsg string) {
|
||||||
func (s *YuyuechaService) logDebugCurlAndResponse(requestID, transactionID, apiCode, curlCmd string, statusCode int, duration time.Duration, raw []byte) {
|
if !s.config.Debug {
|
||||||
if !s.config.Debug || s.logger == nil || curlCmd == "" {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.LogInfo("愉悦查请求响应(dev)",
|
writeDebugExchange(s.config.DebugLogDir, debugExchangeRecord{
|
||||||
zap.String("request_id", requestID),
|
RequestID: requestID,
|
||||||
zap.String("transaction_id", transactionID),
|
TransactionID: transactionID,
|
||||||
zap.String("api_code", apiCode),
|
API: apiCode,
|
||||||
zap.Int("http_status", statusCode),
|
Status: statusCode,
|
||||||
zap.Duration("duration", duration),
|
Duration: duration,
|
||||||
zap.String("curl", curlCmd),
|
Curl: curlCmd,
|
||||||
zap.String("response_body", string(raw)),
|
ResponseBody: raw,
|
||||||
)
|
Error: errMsg,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,31 @@ func TestHashIDCard(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateCurlCommandHeaderOrder(t *testing.T) {
|
||||||
|
cmd := generateCurlCommand("POST", "https://example.com/openapi/v1/a01/x", map[string]string{
|
||||||
|
"signature": "sig",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"nonce": "n",
|
||||||
|
"clientId": "cid",
|
||||||
|
"timestamp": "1",
|
||||||
|
}, `{"name":"a"}`)
|
||||||
|
wantOrder := []string{
|
||||||
|
"-H 'Content-Type: application/json'",
|
||||||
|
"-H 'clientId: cid'",
|
||||||
|
"-H 'timestamp: 1'",
|
||||||
|
"-H 'nonce: n'",
|
||||||
|
"-H 'signature: sig'",
|
||||||
|
}
|
||||||
|
pos := 0
|
||||||
|
for _, part := range wantOrder {
|
||||||
|
idx := strings.Index(cmd[pos:], part)
|
||||||
|
if idx < 0 {
|
||||||
|
t.Fatalf("missing ordered header %q in:\n%s", part, cmd)
|
||||||
|
}
|
||||||
|
pos += idx + len(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sandboxService() *YuyuechaService {
|
func sandboxService() *YuyuechaService {
|
||||||
return NewYuyuechaService(ServiceConfig{
|
return NewYuyuechaService(ServiceConfig{
|
||||||
BaseURL: "https://sandbox-api.yuyuecha.com",
|
BaseURL: "https://sandbox-api.yuyuecha.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user