# 对接文档

> AppKey：**{{.payKey}}**  
> AppSecret：**{{.paySecret}}**

## 1.请求支付页面

1.DEMO文件链接[Python demo文件](/assets/demo.zip)

请求卡密支付页面，页面如下：  
![img.png](/images/pay/AB4467C3-FA86-069A-99BB-C3997694311C.png)

### 请求参数

地址：{{.shopHost}}/?sign=xxx  
方法：GET

| 参数名称  | 类型   | 是否必须 | 描述     | 示例                                                                |
| --------- | ------ | -------- | -------- | ------------------------------------------------------------------- |
| sign      | string | 是       | 签名     | 6634ddd39796e4e0a98d6f5e919ae50ddc82edbf255f15aa2e30c5c70381e7b9648 |
| returnUrl | string | 否       | 返回地址 | 订单完成（成功或失败）后的返回地址 https://baidu.com/notify         |

### sign生成算法

> 注意：生成签名时，需要提前按照参数的参数名进行排序，按照 a-z 顺序排序

| 参数名称      | 类型   | 是否必须 | 描述               | 示例                            |
| ------------- | ------ | -------- | ------------------ | ------------------------------- |
| payKey        | string | 是       | 支付 key           | 20190520123456789               |
| generatedTime | number | 是       | 支付时间           | 默认为 10 位当前时间（单位/秒） |
| duration      | number | 是       | 订单存续时间       | 默认为 24 小时                  |
| orderNo       | string | 是       | 订单号(自定义生成) | 20190520123456789               |
| notifyUrl     | string | 是       | 回调地址           | https://baidu.com/notify        |
| showMMValue   | string | 是       | 卡片面额           | 100                             |
| productCode   | string | 是       | 卡片编码           | 苹果卡                          |

### 通道编码

> 通道编码需参考后台配置，以下仅作为示例

| 通道                         | 编码 | 说明                             |
| ---------------------------- | ---- | -------------------------------- |
| 苹果充值卡                   | 8545 | 苹果充值卡面，由卡号和卡密组成   |
| 沃尔玛充值卡                 | 8645 | 沃尔玛充值卡面，有卡号和卡密组成 |
| 天猫店铺充值兑换\|确认收货版 | 8745 | 天猫店铺充值，用户秒收货秒回调   |
| 天猫店铺充值兑换\|好评版     | 8645 | 天猫店铺充值，用户秒好评秒回调   |

### 加密算法

需要将上述参数加密生成 sign 参数，sign 参数需要拼接在 url
后面，如：`http://121.37.253.228:12305/?sign=6634ddd39796e4e0a98d6f5e919ae50ddc82edbf255f15aa2e30c5c70381e7b96489f102aa574e70a4ea1b874685dfafec957b237b02d5980f0a22139d860c7d7a8d5c58e3dfbde606a0de08186a4d45228bcc1917a35768f1903bee921b78b118e8b31d25642737cb3e094c140c81f7&showMM=100&linkID=736797719398`。
sign 加密算法为 AES/CBC 加密，数据填充模式为 PKCS7 填充，加密 key 和 iv 如下：

> key = thisis32bitlongpassphraseimusing  
> iv = 1234567890123456

##### 加密算法参考

```go
package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/hex"
	"encoding/json"
	"fmt"
)

type OrderParams struct {
	OrderNo       string  `json:"orderNo"`
	PayKey        string  `json:"payKey"`        // 支付明文秘钥
	NotifyUrl     string  `json:"notifyUrl"`     // 回调地址
	GeneratedTime int64   `json:"generatedTime"` // 过期时间
	Duration      int     `json:"duration"`      // 存续时间（小时）
	ProductCode   string  `json:"productCode"`
	ShowMMValue   float64 `json:"showMMValue"`
}

func String(p *OrderParams) string {
	r, err := json.Marshal(p)
	if err != nil {
		return ""
	}
	logs.Info(string(r))
	return string(r)
}

func Encrypt(p *OrderParams) string {
	prepareData := String(p)
	print(prepareData, "\n")
	if prepareData == "" {
		return prepareData
	}

	//	加密密码数据
	key := "thisis32bitlongpassphraseimusing"
	iv := "1234567890123456"
	result, _ := AesCBCEncrypt([]byte(prepareData), []byte(key), []byte(iv))
	return hex.EncodeToString(result)
}

// AesCBCEncrypt AES/CBC/PKCS7Padding 加密
func AesCBCEncrypt(plaintext []byte, key []byte, iv []byte) ([]byte, error) {
	// AES
	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err)
	}

	// PKCS7 填充
	plaintext = paddingPKCS7(plaintext, aes.BlockSize)

	// CBC 加密
	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(plaintext, plaintext)

	return plaintext, nil
}

// PKCS7 填充
func paddingPKCS7(plaintext []byte, blockSize int) []byte {
	paddingSize := blockSize - len(plaintext)%blockSize
	paddingText := bytes.Repeat([]byte{byte(paddingSize)}, paddingSize)
	return append(plaintext, paddingText...)
}

func main() {
	orderParams := OrderParams{
		OrderNo:       "20190520123456789",
		PayKey:        "20190520123456789",
		NotifyUrl:     "https://baidu.com/notify",
		GeneratedTime: 1558312000,
		Duration:      24,
		ProductCode:   "8645",
		ShowMMValue:   100,
	}
	sign := Encrypt(&orderParams)
	// 原始本文
	fmt.Println(sign)
}

// 原始文本：{"orderNo":"20190520123456789","payKey":"20190520123456789","notifyUrl":"https://baidu.com/notify","generatedTime":1558312000,"duration":24,"productCode":"8645","showMMValue":100}
// aes加密后的文本：94db297041a61fa78ac2176b9e8e1e9b34d2ae33b6d46fc8c731a34b4c2b76eea7738993c2c175e6d61648c31c00b5d6d465246aa39b5fba5d6b77599f112c791370486b9fb19ad675e228b1d36ec6ad890ac692a262a73f126dda2bffae493452ea20d8604874af1ff2ba67cf6aab5c5821d100beec4d4c4de86d4c014fc8fe0bfe61b259752007f498dbee92f0136b2cbb756ff5405f95fc7763405022c835812f58dda86c9d72f84c6d576d2e98909d0e499e3c4b4da552f478bfbce3ba63
// 签名：key=thisis32bitlongpassphraseimusing iv=1234567890123456
// 算法：CBC非对称加密/PKCS7填充
```

## 2.请求实际支付金额(可选)

> 通过面额获取实际支付金额。注意，实际支付面额可能会随时修改，建议每次拉取订单前重新获取最新面额，切勿缓存。  
> 当前接口会返回当前面额在各个平台设置的实际支付金额，以及设置各个平台的链接，根据需要获取。

接口：{{.remoteHost}}/gateway/getAllowedMM  
请求方式：GET

| 参数名称    | 类型   | 是否必须 | 描述     | 实例              |
| ----------- | ------ | -------- | -------- | ----------------- |
| showMMValue | string | 是       | 展示面额 | 充值卡上的面额    |
| payKey      | string | 是       | 支付 key | 20190520123456789 |
| productCode | string | 卡片编码 | 是       | 8456              |

#### 返回参数

| 参数名称 | 参数类型  | 描述         | 说明            |
| -------- | --------- | ------------ | --------------- |
| code     | int       | 平台参数     | 0 成功 其他失败 |
| msg      | string    | 信息提示     | 错误原因        |
| data     | interface | 具体返回数据 | 返回数据如下    |

具体成功返回数据

| 参数名称  | 参数类型 | 描述           | 说明 |
| --------- | -------- | -------------- | ---- |
| showLabel | string   | 卡面面值       |      |
| factLabel | string   | 实际面值       |      |
| linkID    | string   | 需要下单的链接 |      |

## 3.提交订单(创建订单)

> 提交订单接口，此接口用于第三方直接提交卡号/卡密，使用自有拉单页面

接口：{{.remoteHost}}/gateway/scan  
请求方式：POST

### 请求参数

| 参数名称    | 类型   | 是否必须 | 描述               | 示例                                                |
| ----------- | ------ | -------- | ------------------ | --------------------------------------------------- |
| exValue     | string | 是       | 扩展参数           | 卡密数据（具体格式如下）                            |
| orderNo     | string | 是       | 订单号             | 20190520123456789                                   |
| orderPeriod | int    | 否       | 订单有效期（小时） | 24                                                  |
| orderPrice  | float  | 是       | 订单价格           | 100.12                                              |
| notifyUrl   | string | 是       | 回调地址           | https://baidu.com/notify?sign=xxx                   |
| payKey      | string | 是       | 商户秘钥           |                                                     |
| timestamp   | int    | 是       | 时间戳             | Unix 时间戳,时区为 GMT+8 允许与服务器最大误差 30 秒 |
| sign        | string | 是       | 签名               | 签名算法如下                                        |
| productCode | string | 卡片编码 | 是                 | 8456                                                |
| ip          | string | 否       | 客户端IP           | 127.0.0.1                                           |
| deviceId  | string | 否       | 设备唯一标识       | 127.0.0.1                                           |

### 参数说明

#### exValue

内需参数

| 参数名称     | 参数类型 | 描述     | 说明                   |
| ------------ | -------- | -------- | ---------------------- |
| data         | string   | 卡密     | ---                    |
| cardNo       | string   | 面额     | ---                    |
| recoveryType | string   | 回收类型 | 2.仅有卡密 8.卡密/卡号 |
| faceType | string | 面额 | --- |

### 返回参数

返回参数

| 参数名称 | 参数类型  | 描述         | 说明            |
| -------- | --------- | ------------ | --------------- |
| code     | int       | 平台参数     | 0 成功 其他失败 |
| msg      | string    | 信息提示     |                 |
| data     | interface | 具体返回数据 | 返回数据如下    |

具体成功返回数据

| 参数名称   | 类型   | 是否必须 | 描述         | 示例              |
| ---------- | ------ | -------- | ------------ | ----------------- |
| statusCode | string | 是       | 返回状态码   | 00 成功，其他失败 |
| orderNo    | string | 是       | 订单号       | 20190520123456789 |
| orderPrice | string | 是       | 订单价格     | 100.12            |
| sign       | string | 是       | 签名         | xxx               |
| msg        | string | 是       | 附带返回信息 | 成功              |

## 4.查询订单

地址：{{.remoteHost}}/gateway/merchant/query
方法：Get

查询参数

| 参数名称  | 参数类型 | 是否必须 | 描述     | 说明                                                |
| --------- | -------- | -------- | -------- | --------------------------------------------------- |
| orderNo   | string   | 是       | 订单号   |                                                     |
| appKey    | string   | 是       | 应用 key |                                                     |
| timestamp | int      | 是       | 时间戳   | Unix 时间戳,时区为 GMT+8 允许与服务器最大误差 30 秒 |
| sign      | string   | 是       | 签名     | xxx                                                 |

### 返回参数

| 参数名称 | 参数类型  | 描述         | 说明            |
| -------- | --------- | ------------ | --------------- |
| code     | int       | 平台参数     | 0 成功 其他失败 |
| msg      | string    | 信息提示     |                 |
| data     | interface | 具体返回数据 | 返回数据如下    |

具体成功返回数据

| 参数名称 | 参数类型 | 描述     | 说明                |
| -------- | -------- | -------- | ------------------- |
| orderNo  | string   | 订单号   |                     |
| cardNo   | string   | 卡号     |                     |
| cardPwd  | string   | 卡密     |                     |
| status   | string   | 订单状态 | success，fail, wait |
| faceVal  | float    | 面额     |                     |
| amount   | string   | 实际面额 |                     |
| CardReturnData  | string   | 充值失败原因 |                     |

## 4.创建订单(新)

> 创建订单，并返回下单单号，地址等信息

地址：{{.remoteHost}}/gateway/createOrder

### 请求参数

| 参数名称    | 类型   | 是否必须 | 描述               | 示例                                                |
| ----------- | ------ | -------- | ------------------ | --------------------------------------------------- |
| orderNo     | string | 是       | 订单号             | 20190520123456789                                   |
| orderPeriod | int    | 否       | 订单有效期（小时） | 24                                                  |
| orderPrice  | float  | 是       | 订单价格           | 100.12                                              |
| notifyUrl   | string | 是       | 回调地址           | https://baidu.com/notify?sign=xxx                   |
| payKey      | string | 是       | 商户秘钥           |                                                     |
| timestamp   | int    | 是       | 时间戳             | Unix 时间戳,时区为 GMT+8 允许与服务器最大误差 30 秒 |
| sign        | string | 是       | 签名               | 签名算法如下                                        |
| productCode | string | 卡片编码 | 是                 | 8456                                                |

### 返回参数

| 参数名称 | 参数类型  | 描述         | 说明            |
| -------- | --------- | ------------ | --------------- |
| code     | int       | 平台参数     | 0 成功 -1失败 |
| msg      | string    | 信息提示     | 失败的具体原因                |
| data     | interface | 具体返回数据 | 返回数据如下    |

具体成功返回数据

| 参数名称 | 参数类型 | 描述     | 说明                |
| -------- | -------- | -------- | ------------------- |
| productCode  | string   | 产品编码   |                     |
| paymentName   | string   | 产品名称     |                     |
| transactionType  | string   | 卡密     |  是兑换还是充值 暂时无用                   |
| payUrl   | string   | 支付链接 | 最终构建的支付链接
| merchantOrderNo  | string    | 商户订单号     |  来源自请求传入订单号                    |
| orderNo   | string   | 订单编号 |  系统编号                   |


#### 签名算法（sign）

> 注意：  
> 1 对参数进行签名时，一定不要写死！！！，请按签名算法进行计算！！  
> 2 所有提交参数都需要参与签名计算 为空的也需要

##### 参数签名算法示例算法

请注意: 1.对参数进行签名时，一定不要写死！！！，请按签名算法进行计算！！ 2.所有提交参数都需要参与签名计算 为空的也需要3.下面的参数只是示例，具体参数要根据实际接口参数进行验签

```go
package main

import (
	"crypto/md5"
	"encoding/hex"
	"sort"
	"strconv"
	"time"
)

func main() {
	//实际获取的appKey值
	appKey := "1f2811dcd32b2c5c"
	//实际获取的appSecret值
	appSecret := "3C569A210D1260B2"

	// 如果有其它参数，依次添加 key=value
	params := map[string]string{}

	params["appKey"] = appKey
	params["phone"] = "13800138000"
	params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
	params["amount"] = "100"

	strArr := SortMap(params)
	signStr := ""
	for i := 0; i < len(strArr); i++ {
		k := strArr[i]
		if len(params[k]) == 0 {
			signStr += k
		} else {
			signStr += k + params[k]
		}
	}
	signStr += appSecret
	h := md5.New()
	h.Write([]byte(signStr))
	hex.EncodeToString(h.Sum(nil))
}

// SortMap 对map的key值进行排序
func SortMap(m map[string]string) []string {
	var arr []string
	for k := range m {
		arr = append(arr, k)
	}
	sort.Strings(arr)
	return arr
}


// 原始值
// {
//     "amount": "100",
//     "appKey": "1f2811dcd32b2c5c",
//     "phone": "13800138000",
//     "timestamp": "1737284505"
// }

// appSecret：3C569A210D1260B2

// 加密前的值
// amount100appKey1f2811dcd32b2c5cphone13800138000timestamp17372845053C569A210D1260B2
// 加密后的值：96ff597f7b89322e83e234e3ad1d42e5
```

Java 算法

```Java
String appSecret = "3C569A210D1260B2";

TreeMap<String, String> params = new TreeMap<String,String>();
params.put("timestamp", ""+(System.currentTimeMillis()/1000L));
params.put("phone", "13800138000");
params.put("amount", "100");

StringBuilder sb = new StringBuilder();
for(Entry<String, String> item : params.entrySet()){
     if(item.getKey().equals("sign"))
         continue;
     sb.append(item.getKey()).append(item.getValue());
}

sb.append(appSecret);
params.put("sign",md5(sb.toString()));
```

## 订单回调接口签名检验

### 回调接口示例

地址：http://baidu.com/notify?param1=xx&param2=xx  
方法：Get

返回参数：

| 参数名称    | 参数类型 | 含义                       | 示例              |
| ----------- | -------- | -------------------------- | ----------------- |
| orderNo     | string   | 订单号(己方账号)           | 20190520123456789 |
| orderPrice  | string   | 订单价格(面值，一般为整数) | 100               |
| factPrice   | string   | 实际价格                   | 98.89             |
| orderTime   | string   | 回调时间                   | 2006010215:04:05  |
| trxNo       | string   | 交易(系统账户)流水号       | 20190520123456789 |
| payKey      | string   | 支付通道 key               | 20190520123456789 |
| sign        | string   | 签名                       | xxx               |
| statusCode | int   | 交易状态                   | 1 成功，0 通用失败 其他状态码待添加           |
| failReason  | string   | 订单状态                   | 失败的具体原因           |

### 签名算法

签名算法参见`提交订单(创建订单)`接口 ↑
