# 支付API文檔

## 1. 創建訂單 API

### 介面訊息
- **URL**: `/pay/create`
- **方法**: `POST`
- **Content-Type**: `application/x-www-form-urlencoded`

### 請求參數

| 參數名 | 類型 | 必填 | 說明 |
|--------|------|------|------|
| mid | int | 是 | 商戶ID |
| orderid | string | 是 | 商戶訂單號，唯一標識 |
| amount | float | 是 | 訂單金額（法幣） |
| notifyurl | string | 是 | 支付結果通知URL |
| sign | string | 是 | 簽章，請參考文檔末尾簽章方法 |


### 回應格式

#### 成功回應
```json
{
    "status": "ok",
    "orderid": "ORDER123456",
    "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
    "uamount": "0.12",
    "url": "https://demosite.com/pay?id=abc123def456"
}
```

#### 錯誤回應
```json
{
    "status": "error",
    "message": "系統繁忙，請稍後再試！"
}
```

### 回應欄位說明

| 欄位名 | 類型 | 說明 |
|--------|------|------|
| status | string | 回應狀態：ok-成功，error-失敗 |
| orderid | string | 商戶訂單號 |
| address | string | 錢包地址 |
| uamount | string | 需要支付的USDT數量 |
| url | string | 支付頁面URL，請將使用者浏覽器跳轉到此地址。或者商戶自行展示上述錢包和金額訊息。 |
| message | string | 錯誤訊息（僅失敗時回傳） |

### 注意事項

1. **訂單號唯一性**：每個商戶的訂單號必須唯一。
2. **金額精度**：支援最多2位小數
3. **通知URL**：必須是可訪问的HTTPS地址
4. **金額調整**：在交易繁忙時，系統會自動調整USDT支付金額（最多+0.09）
5. **來源 IP 白名單**：管理後台可為商戶設定收款 API IP 白名單；設定後，僅白名單中的 IP 可建立收款訂單。

## 2. 查詢訂單狀態 API

### 介面訊息
- **URL**: `/pay/checkorder`
- **方法**: `POST`
- **Content-Type**: `application/x-www-form-urlencoded`

### 請求參數

| 參數名 | 類型 | 必填 | 說明 |
|--------|------|------|------|
| mid | string | 是 | 商戶ID |
| orderid | string | 是 | 商戶訂單號 |
| sign | string | 是 | 簽章，請參考文檔末尾簽章方法 |

### 回應格式

#### 成功回應
```json
{
    "status": "ok",
    "orderid": "ORDER123456",
    "status_code": 0,
    "amount": 100.00,
    "uamount": 0.12,
    "updatetime": 1640995200
}
```

#### 錯誤回應
```json
{
    "status": "error",
    "message": "訂單不存在"
}
```

### 回應欄位說明

| 欄位名 | 類型 | 說明 |
|--------|------|------|
| orderid | string | 商戶訂單號 |
| status_code | int | 訂單狀態：0-未支付，1-已支付 |
| amount | float | 訂單金額（法幣） |
| uamount | float | USDT金額 |
| updatetime | int | 更新時間戳（status_code=0時為創建時間，status=1時為成功時間） |
| status | string | 回應狀態：ok-成功，error-失敗 |
| message | string | 錯誤訊息（僅失敗時回傳） |

### 注意事項

1. **訂單查詢**：只能查詢本商戶的訂單
2. **時間戳**：updatetime欄位根據訂單狀態回傳不同時間
   - status=0：回傳訂單創建時間
   - status=1：回傳訂單成功時間

## 3. 支付回調通知

### 回調機制說明

當訂單支付成功時，系統會主動向商戶提供的`notifyurl`發送POST請求通知支付結果。

### 回調參數

| 參數名 | 類型 | 說明 |
|--------|------|------|
| orderid | string | 商戶訂單號 |
| amount | float | 訂單金額（法幣） |
| uamount | float | USDT支付金額 |
| created_time | int | 訂單創建時間戳 |
| success_time | int | USDT轉帳時間 |
| sign | string | 平台以 RSA-SHA256 生成的 Base64 簽章 |


### 回調回應要求

商戶收到回調後，必須回傳HTTP狀態碼200，表示成功接收。如果回傳非200狀態碼，系統會认為回調失敗並進行重試。  通知收妥後務請回應成功訊息，避免系統徒勞的多次重試堵塞通道。

**成功回應示例：**
```text
success
```


### 回調失敗重試機制

- **重試次數**：最多重試9次
- **重試間隔**：採用指數退避算法
  - 第1次失敗：30秒後重試
  - 第2次失敗：60秒後重試  
  - 第3次失敗：120秒後重試
  - 第4次及以後：2^(失敗次數-1) * 60秒
- **逾時处理**：超過9次失敗後，系統會放棄此訂單的通知。


## 4. 簽章規則

### RSA-SHA256 簽章

商戶在後台創建商戶資料時上傳 **RSA 公鑰（PEM / PKIX）**，並自行安全保管配對私鑰。所有 API 請求由商戶私鑰使用 `RSA-SHA256`（PKCS#1 v1.5 + SHA-256）簽章；平台用該商戶公鑰驗簽。平台會自動生成自身金鑰對，後台“平台公鑰”介面回傳公鑰；商戶必須儲存該公鑰並用它驗證回調。

1. 將除 `sign` 外的參數按參數名 ASCII 升序排序。
2. 拼為 `key=value&key=value`，原始值不做 URL 编碼。
3. 對 UTF-8 原文計算 SHA-256，並以 RSA PKCS#1 v1.5 簽章。
4. 將簽章字節作標準 Base64 编碼，放入 `sign` 參數。

### 簽章示例

```
原始參數：
mid=1001
orderid=ORDER123456
amount=100.00
notifyurl=https://example.com/notify

排序後：
amount=100.00&mid=1001&notifyurl=https://example.com/notify&orderid=ORDER123456

以上排序後的字符串即簽章原文；用商戶 RSA 私鑰簽章並 Base64 编碼。
```

### 注意事項

- 所有API介面都需要使用相同的簽章規則
- 簽章驗證失敗會導致請求被拒絕
- 請妥善保管商戶 RSA 私鑰，絕不可上傳或洩露；後台只儲存公鑰。

### 簽章核心邏輯

以下示例先用實際業務欄位構建請求參數與簽章原文，`privateKey` 為商戶私鑰；將得到的 `sign` 放回參數集合後送出。回調驗簽時改用平台公鑰和對應語言的驗簽 API。

```php
$params = ['mid'=>'1001', 'orderid'=>'ORDER123456', 'amount'=>'100.00', 'notifyurl'=>'https://example.com/notify'];
ksort($params, SORT_STRING);
$source = implode('&', array_map(fn($k) => $k.'='.$params[$k], array_keys($params)));
openssl_sign($source, $raw, $privateKey, OPENSSL_ALGO_SHA256);
$params['sign'] = base64_encode($raw);
```

```go
params := map[string]string{"mid":"1001", "orderid":"ORDER123456", "amount":"100.00", "notifyurl":"https://example.com/notify"}
keys := make([]string, 0, len(params)); for k := range params { keys = append(keys, k) }; sort.Strings(keys)
parts := make([]string, 0, len(keys)); for _, k := range keys { parts = append(parts, k+"="+params[k]) }; source := strings.Join(parts, "&")
hash := sha256.Sum256([]byte(source))
raw, _ := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
params["sign"] = base64.StdEncoding.EncodeToString(raw)
```

```java
Map<String,String> params = new TreeMap<>();
params.put("mid", "1001"); params.put("orderid", "ORDER123456"); params.put("amount", "100.00"); params.put("notifyurl", "https://example.com/notify");
String source = params.entrySet().stream().map(e -> e.getKey()+"="+e.getValue()).collect(Collectors.joining("&"));
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey); signer.update(source.getBytes(StandardCharsets.UTF_8));
params.put("sign", Base64.getEncoder().encodeToString(signer.sign()));
```

### 回調驗簽核心邏輯

回調收到後，將 `sign` 取出並從參數集合移除；其餘參數按同樣規則排序拼接。`platformPublicKey` 為後台展示的平台公鑰。

```php
$signature = base64_decode($params['sign']); unset($params['sign']); ksort($params, SORT_STRING);
$source = implode('&', array_map(fn($k) => $k.'='.$params[$k], array_keys($params)));
$valid = openssl_verify($source, $signature, $platformPublicKey, OPENSSL_ALGO_SHA256) === 1;
```

```go
signature, _ := base64.StdEncoding.DecodeString(params["sign"]); delete(params, "sign")
keys := make([]string, 0, len(params)); for k := range params { keys = append(keys, k) }; sort.Strings(keys)
parts := make([]string, 0, len(keys)); for _, k := range keys { parts = append(parts, k+"="+params[k]) }; digest := sha256.Sum256([]byte(strings.Join(parts, "&")))
err := rsa.VerifyPKCS1v15(platformPublicKey, crypto.SHA256, digest[:], signature)
```

```java
byte[] signature = Base64.getDecoder().decode(params.remove("sign"));
String source = params.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(e -> e.getKey()+"="+e.getValue()).collect(Collectors.joining("&"));
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(platformPublicKey); verifier.update(source.getBytes(StandardCharsets.UTF_8));
boolean valid = verifier.verify(signature);
```
