/
client.go
518 lines (440 loc) · 19.5 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
/*
* Justap API
*
* 欢迎阅读 Justap Api 文档 Justap 是为移动端应用和PC端应用打造的下一代聚合支付SAAS服务平台,通过一个 SDK 即可快速的支持各种形式的应用,并且一次接口完成多个不同支付渠道的接入。平台除了支持服务商子商户模式,同时还对商家自有商户(即自己前往微信、支付宝等机构开户)提供了完整的支持。 感谢您的支持,我们将不断探索,为您提供更优质的服务!如需技术支持可前往商户中心提交工单,支持工程师会尽快与您取得联系! # 文档说明 采用 REST 风格设计。所有接口请求地址都是可预期的以及面向资源的。使用规范的 HTTP 响应代码来表示请求结果的正确或错误信息。使用 HTTP 内置的特性,如 HTTP Authentication 和 HTTP 请求方法让接口易于理解。 ## HTTP 状态码 HTTP 状态码可以用于表明服务的状态。服务器返回的 HTTP 状态码遵循 [RFC 7231](http://tools.ietf.org/html/rfc7231#section-6) 和 [IANA Status Code Registry](http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) 标准。 ## 认证 在调用 API 时,必须提供 API Key 作为每个请求的身份验证。你可以在管理平台内管理你的 API Key。API Key 是商户在系统中的身份标识,请安全存储,确保其不要被泄露。如需获取或更新 API Key ,也可以在商户中心内进行操作。 Api Key 在使用自定义的 HTTP Header 进行传递。 ``` X-Justap-Api-Key ``` API Key 分为 live 和 test 两种模式。分别对应真实交易环境和模拟测试交易环境并且可以实时切换。 测试模式下的 API Key 会模拟交易等请求,但是不会产生任何真实交易行为和费用,便于调试和接入。 **⚠️ 注意**:在使用 live 模式前,需要先前往 `商户中心 -> 应用设置 -> 开发参数` 开启 live 模式。 <SecurityDefinitions /> ## 请求类型 所有的 API 请求只支持 HTTPS 方式调用。 ## 路由参数 路由参数是指出现在 URL 路径中的可变变量。在本文档中,使用 `{}` 包裹的部分。 例如: `{charge_id}`,在实际使用是,需要将 `{charge_id}` 替换为实际值 `charge_8a8sdf888888` ## MIME Type MIME 类型用于指示服务器返回的数据格式。服务器目前默认采用 `application/json`。 例如: ``` application/json ``` ## 错误 服务器使用 HTTP 状态码 (status code) 来表明一个 API 请求的成功或失败状态。返回 HTTP 2XX 表明 API 请求成功。返回 HTTP 4XX 表明在请求 API 时提供了错误信息,例如参数缺失、参数错误、支付渠道错误等。返回 HTTP 5XX 表明 API 请求时,服务器发生了错误。 在返回错误的状态码时,回同时返回一些错误信息提示出错原因。 具体的错误码我们正在整理当中。 ## 分页 所有的 Justap 资源都可以被 list API 方法支持,例如分页 charges 和 refunds。这些 list API 方法拥有相同的数据结构。Justap 是基于 cursor 的分页机制,使用参数 starting_after 来决定列表从何处开始,使用参数 ending_before 来决定列表从何处结束。 ## 参数说明 请求参数中包含的以下字段释义请参考: - REQUIRED: 必填参数 - OPTIONAL: 可选参数,可以在请求当前接口时按需传入 - CONDITIONAL: 在某些条件下必传 - RESPONSE-ONLY: 标示该参数仅在接口返回参数中出现,调用 API 时无需传入 # 如何保证幂等性 如果发生请求超时或服务器内部错误,客户端可能会尝试重发请求。您可以在请求中设置 ClientToken 参数避免多次重试带来重复操作的问题。 ## 什么是幂等性 在数学计算或者计算机科学中,幂等性(idempotence)是指相同操作或资源在一次或多次请求中具有同样效果的作用。幂等性是在分布式系统设计中具有十分重要的地位。 ## 保证幂等性 通常情况下,客户端只需要在500(InternalErrorInternalError)或503(ServiceUnavailable)错误,或者无法获取响应结果时重试。充实时您可以从客户端生成一个参数值不超过64个的ASCII字符,并将值赋予 ClientToken,保证重试请求的幂等性。 ## ClientToken 详解 ClientToken参数的详细信息如下所示。 - ClientToken 是一个由客户端生成的唯一的、大小写敏感、不超过64个ASCII字符的字符串。例如,`ClientToken=123e4567-e89b-12d3-a456-426655440000`。 - 如果您提供了一个已经使用过的 ClientToken,但其他请求参数**有变化**,则服务器会返回 IdempotentParameterMismatch 的错误代码。 - 如果您提供了一个已经使用过的 ClientToken,且其他请求参数**不变**,则服务器会尝试返回 ClientToken 对应的记录。 ## API列表 以下为部分包含了 ClientToken 参数的API,供您参考。具体哪些API支持 ClientToken 参数请以各 API 文档为准,此处不一一列举。 - [申请退款接口](https://www.justap.cn/docs#operation/TradeService_Refunds) # 签名 为保证安全,JUSTAP 所有接口均需要对请求进行签名。服务器收到请求后进行签名的验证。如果签名验证不通过,将会拒绝处理请求,并返回 401 Unauthorized。 签名算法: ``` base64Encode(hamc-sha256(md5(请求 body + 请求时间戳 + 一次性随机字符串) + 一次性随机字符串)) ``` ## 准备 首先需要在 Justap 创建一个应用,商户需要生成一对 RSA 密钥对,并将公钥配置到 `商户中心 -> 开发配置`。 RSA 可以使用支付宝提供的 [密钥生成工具](https://opendocs.alipay.com/common/02kipl) 来生成。 商户在使用时,可以按照下述步骤生成请求的签名。 ## 算法描述: - 在请求发送前,取完整的**请求 body** - 生成一个随机的32位字符串,得到 **一次性随机字符串** - 获取当前时间的时间戳,得到 **请求时间戳** - 在请求字符串后面拼接上 **请求时间戳** 和 **一次性随机字符串**,得到 **待 Hash 字符串** - 对 **待 Hash 字符串** 转换为 utf8 编码并计算 md5,得到 **待签名字符串** - **待签名字符串** 后面拼接上 一次性随机字符串,得到完整的 **待签名字符串** - 使用商户 RSA 私钥,对 **待签名字符串** 计算签名,并对 结果 进行 base64 编码,即可得到 **签名** ## 设置HTTP头 Justap 要求请求通过 自定义头部 来传递签名。具体定义如下: ``` X-Justap-Signature: 签名 X-Justap-Request-Time: 请求时间戳 X-Justap-Nonce: 一次性随机字符串 X-Justap-Body-Hash: 待签名字符串 ``` 具体的签名算法实现,可参考我们提供的各语言 SDK。 # WebHooks
*
* API version: 1.0
* Contact: support@justap.net
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
*/
package justap
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"golang.org/x/oauth2"
)
var (
jsonCheck = regexp.MustCompile("(?i:(?:application|text)/json)")
xmlCheck = regexp.MustCompile("(?i:(?:application|text)/xml)")
)
// APIClient manages communication with the Justap API API v1.0
// In most cases there should be only one, shared, APIClient.
type APIClient struct {
cfg *Configuration
common service // Reuse a single struct instead of allocating one for each service on the heap.
// API Services
DefaultApi *DefaultApiService
CheckoutServiceApi *CheckoutServiceApiService
CustomerServiceApi *CustomerServiceApiService
}
type service struct {
client *APIClient
}
// NewAPIClient creates a new API client. Requires a userAgent string describing your application.
// optionally a custom http.Client to allow for advanced features such as caching.
func NewAPIClient(cfg *Configuration) *APIClient {
if cfg.HTTPClient == nil {
cfg.HTTPClient = http.DefaultClient
}
c := &APIClient{}
c.cfg = cfg
c.common.client = c
// API Services
c.DefaultApi = (*DefaultApiService)(&c.common)
c.CheckoutServiceApi = (*CheckoutServiceApiService)(&c.common)
c.CustomerServiceApi = (*CustomerServiceApiService)(&c.common)
return c
}
func atoi(in string) (int, error) {
return strconv.Atoi(in)
}
// selectHeaderContentType select a content type from the available list.
func selectHeaderContentType(contentTypes []string) string {
if len(contentTypes) == 0 {
return ""
}
if contains(contentTypes, "application/json") {
return "application/json"
}
return contentTypes[0] // use the first content type specified in 'consumes'
}
// selectHeaderAccept join all accept types and return
func selectHeaderAccept(accepts []string) string {
if len(accepts) == 0 {
return ""
}
if contains(accepts, "application/json") {
return "application/json"
}
return strings.Join(accepts, ",")
}
// contains is a case insenstive match, finding needle in a haystack
func contains(haystack []string, needle string) bool {
for _, a := range haystack {
if strings.ToLower(a) == strings.ToLower(needle) {
return true
}
}
return false
}
// Verify optional parameters are of the correct type.
func typeCheckParameter(obj interface{}, expected string, name string) error {
// Make sure there is an object.
if obj == nil {
return nil
}
// Check the type is as expected.
if reflect.TypeOf(obj).String() != expected {
return fmt.Errorf("Expected %s to be of type %s but received %s.", name, expected, reflect.TypeOf(obj).String())
}
return nil
}
// parameterToString convert interface{} parameters to string, using a delimiter if format is provided.
func parameterToString(obj interface{}, collectionFormat string) string {
var delimiter string
switch collectionFormat {
case "pipes":
delimiter = "|"
case "ssv":
delimiter = " "
case "tsv":
delimiter = "\t"
case "csv":
delimiter = ","
}
if reflect.TypeOf(obj).Kind() == reflect.Slice {
return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]")
}
return fmt.Sprintf("%v", obj)
}
// callAPI do the request.
func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) {
return c.cfg.HTTPClient.Do(request)
}
// Change base path to allow switching to mocks
func (c *APIClient) ChangeBasePath(path string) {
c.cfg.BasePath = path
}
// prepareRequest build the request
func (c *APIClient) prepareRequest(
ctx context.Context,
path string, method string,
postBody interface{},
headerParams map[string]string,
queryParams url.Values,
formParams url.Values,
fileName string,
fileBytes []byte) (localVarRequest *http.Request, err error) {
var body *bytes.Buffer
// Detect postBody type and post.
if postBody != nil {
contentType := headerParams["Content-Type"]
if contentType == "" {
contentType = detectContentType(postBody)
headerParams["Content-Type"] = contentType
}
body, err = setBody(postBody, contentType)
if err != nil {
return nil, err
}
}
// add form parameters and file if available.
if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(fileBytes) > 0 && fileName != "") {
if body != nil {
return nil, errors.New("Cannot specify postBody and multipart form at the same time.")
}
body = &bytes.Buffer{}
w := multipart.NewWriter(body)
for k, v := range formParams {
for _, iv := range v {
if strings.HasPrefix(k, "@") { // file
err = addFile(w, k[1:], iv)
if err != nil {
return nil, err
}
} else { // form value
w.WriteField(k, iv)
}
}
}
if len(fileBytes) > 0 && fileName != "" {
w.Boundary()
//_, fileNm := filepath.Split(fileName)
part, err := w.CreateFormFile("file", filepath.Base(fileName))
if err != nil {
return nil, err
}
_, err = part.Write(fileBytes)
if err != nil {
return nil, err
}
// Set the Boundary in the Content-Type
headerParams["Content-Type"] = w.FormDataContentType()
}
// Set Content-Length
headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len())
w.Close()
}
if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 {
if body != nil {
return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.")
}
body = &bytes.Buffer{}
body.WriteString(formParams.Encode())
// Set Content-Length
headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len())
}
// Setup path and query parameters
url, err := url.Parse(path)
if err != nil {
return nil, err
}
// Adding Query Param
query := url.Query()
for k, v := range queryParams {
for _, iv := range v {
query.Add(k, iv)
}
}
// Encode the parameters.
url.RawQuery = query.Encode()
// Generate a new request
if body != nil {
localVarRequest, err = http.NewRequest(method, url.String(), body)
} else {
localVarRequest, err = http.NewRequest(method, url.String(), nil)
}
if err != nil {
return nil, err
}
// add header parameters, if any
if len(headerParams) > 0 {
headers := http.Header{}
for h, v := range headerParams {
headers.Set(h, v)
}
localVarRequest.Header = headers
}
// Override request host, if applicable
if c.cfg.Host != "" {
localVarRequest.Host = c.cfg.Host
}
// Add the user agent to the request.
localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent)
if ctx != nil {
// add context to the request
localVarRequest = localVarRequest.WithContext(ctx)
// Walk through any authentication.
// OAuth2 authentication
if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok {
// We were able to grab an oauth2 token from the context
var latestToken *oauth2.Token
if latestToken, err = tok.Token(); err != nil {
return nil, err
}
latestToken.SetAuthHeader(localVarRequest)
}
// Basic HTTP Authentication
if auth, ok := ctx.Value(ContextBasicAuth).(BasicAuth); ok {
localVarRequest.SetBasicAuth(auth.UserName, auth.Password)
}
// AccessToken Authentication
if auth, ok := ctx.Value(ContextAccessToken).(string); ok {
localVarRequest.Header.Add("Authorization", "Bearer "+auth)
}
}
for header, value := range c.cfg.DefaultHeader {
localVarRequest.Header.Add(header, value)
}
// sign request
var dataToBeSignBuf *bytes.Buffer
var dataToBeSign []byte
dataToBeSign = make([]byte, body.Len())
if postBody != nil {
dataToBeSignBuf, err = setBody(postBody, headerParams["Content-Type"])
if err != nil {
return nil, err
}
_, err = dataToBeSignBuf.Read(dataToBeSign)
if err != nil {
if err == io.EOF {
} else {
return nil, err
}
}
}
nonceStr := RandomString(20)
requestTime := fmt.Sprintf("%d", time.Now().Unix())
dataToBeMd5Str := fmt.Sprintf("%s%s%s", string(dataToBeSign), requestTime, nonceStr)
bodyMd5 := Md5Hash(dataToBeMd5Str)
localVarRequest.Header.Add("X-Justap-Body-Hash", bodyMd5)
dataToBeSignStr := fmt.Sprintf("%s%s", bodyMd5, nonceStr)
sign, err := RsaPrivateKeySign([]byte(dataToBeSignStr), c.cfg.MerchantRsaPrivateKey)
if err != nil {
return nil, err
}
localVarRequest.Header.Add("X-Justap-Signature", sign)
localVarRequest.Header.Add("X-Justap-Request-Time", requestTime)
localVarRequest.Header.Add("X-Justap-Nonce", nonceStr)
// end sign
return localVarRequest, nil
}
func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {
if strings.Contains(contentType, "application/xml") {
if err = xml.Unmarshal(b, v); err != nil {
return err
}
return nil
} else if strings.Contains(contentType, "application/json") {
if err = json.Unmarshal(b, v); err != nil {
return err
}
return nil
}
return errors.New("undefined response type")
}
// Add a file to the multipart request
func addFile(w *multipart.Writer, fieldName, path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
part, err := w.CreateFormFile(fieldName, filepath.Base(path))
if err != nil {
return err
}
_, err = io.Copy(part, file)
return err
}
// Prevent trying to import "fmt"
func reportError(format string, a ...interface{}) error {
return fmt.Errorf(format, a...)
}
// Set request body from an interface{}
func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
if bodyBuf == nil {
bodyBuf = &bytes.Buffer{}
}
if reader, ok := body.(io.Reader); ok {
_, err = bodyBuf.ReadFrom(reader)
} else if b, ok := body.([]byte); ok {
_, err = bodyBuf.Write(b)
} else if s, ok := body.(string); ok {
_, err = bodyBuf.WriteString(s)
} else if s, ok := body.(*string); ok {
_, err = bodyBuf.WriteString(*s)
} else if jsonCheck.MatchString(contentType) {
err = json.NewEncoder(bodyBuf).Encode(body)
} else if xmlCheck.MatchString(contentType) {
xml.NewEncoder(bodyBuf).Encode(body)
}
if err != nil {
return nil, err
}
if bodyBuf.Len() == 0 {
err = fmt.Errorf("Invalid body type %s\n", contentType)
return nil, err
}
return bodyBuf, nil
}
// detectContentType method is used to figure out `Request.Body` content type for request header
func detectContentType(body interface{}) string {
contentType := "text/plain; charset=utf-8"
kind := reflect.TypeOf(body).Kind()
switch kind {
case reflect.Struct, reflect.Map, reflect.Ptr:
contentType = "application/json; charset=utf-8"
case reflect.String:
contentType = "text/plain; charset=utf-8"
default:
if b, ok := body.([]byte); ok {
contentType = http.DetectContentType(b)
} else if kind == reflect.Slice {
contentType = "application/json; charset=utf-8"
}
}
return contentType
}
// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go
type cacheControl map[string]string
func parseCacheControl(headers http.Header) cacheControl {
cc := cacheControl{}
ccHeader := headers.Get("Cache-Control")
for _, part := range strings.Split(ccHeader, ",") {
part = strings.Trim(part, " ")
if part == "" {
continue
}
if strings.ContainsRune(part, '=') {
keyval := strings.Split(part, "=")
cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",")
} else {
cc[part] = ""
}
}
return cc
}
// CacheExpires helper function to determine remaining time before repeating a request.
func CacheExpires(r *http.Response) time.Time {
// Figure out when the cache expires.
var expires time.Time
now, err := time.Parse(time.RFC1123, r.Header.Get("date"))
if err != nil {
return time.Now()
}
respCacheControl := parseCacheControl(r.Header)
if maxAge, ok := respCacheControl["max-age"]; ok {
lifetime, err := time.ParseDuration(maxAge + "s")
if err != nil {
expires = now
}
expires = now.Add(lifetime)
} else {
expiresHeader := r.Header.Get("Expires")
if expiresHeader != "" {
expires, err = time.Parse(time.RFC1123, expiresHeader)
if err != nil {
expires = now
}
}
}
return expires
}
func strlen(s string) int {
return utf8.RuneCountInString(s)
}
// GenericSwaggerError Provides access to the body, error and model on returned errors.
type GenericSwaggerError struct {
body []byte
error string
model interface{}
}
// Error returns non-empty string if there was an error.
func (e GenericSwaggerError) Error() string {
return e.error
}
// Body returns the raw bytes of the response
func (e GenericSwaggerError) Body() []byte {
return e.body
}
// Model returns the unpacked model of the error
func (e GenericSwaggerError) Model() interface{} {
return e.model
}