init: multi-language OpenAPI demos (java/php/nodejs/python)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
82c7f3406d
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.env
|
||||
*.class
|
||||
node_modules/
|
||||
__pycache__/
|
||||
.idea/
|
||||
*.iml
|
||||
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# 代付 OpenAPI Demo
|
||||
|
||||
多语言对接示例:签名算法 + 下单 / 查单 / 查余额。
|
||||
|
||||
- 仓库:https://git.weilai-pay.com/echo/open-withdrawal-demo
|
||||
- 文档:https://sh.weilai-pay.com/doc/znhtbr/
|
||||
- 网关占位:`https://example.com`(请换成实际域名)
|
||||
|
||||
## 目录
|
||||
|
||||
| 语言 | 目录 |
|
||||
| --- | --- |
|
||||
| Java | `java/` |
|
||||
| PHP | `php/` |
|
||||
| Node.js | `nodejs/` |
|
||||
| Python | `python/` |
|
||||
|
||||
## 配置(环境变量)
|
||||
|
||||
| 变量 | 说明 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| `BASE_URL` | 网关根地址 | `https://example.com` |
|
||||
| `ACCESS_KEY` | 商户号 | `your_access_key` |
|
||||
| `SECRET_KEY` | 商户密钥 | `your_secret_key` |
|
||||
| `PAY_CHANNEL_ID` | 通道编码 | `821` |
|
||||
|
||||
## 快速跑(以 Python 为例)
|
||||
|
||||
```bash
|
||||
cd python
|
||||
export BASE_URL=https://example.com
|
||||
export ACCESS_KEY=your_access_key
|
||||
export SECRET_KEY=your_secret_key
|
||||
export PAY_CHANNEL_ID=821
|
||||
python3 demo.py balance
|
||||
python3 demo.py submit
|
||||
python3 demo.py query <商户订单号>
|
||||
```
|
||||
|
||||
## 签名规则(各语言一致)
|
||||
|
||||
1. 去掉 `Sign` 字段;值为 `null` 或空字符串 `""` 的字段不参与
|
||||
2. 按字段名字典序(ASCII)拼接:`k=v&k2=v2`
|
||||
3. 末尾追加 `&SecretKey=<密钥>`
|
||||
4. 对整串做 MD5(32 位小写)得到 `Sign`
|
||||
98
java/Demo.java
Normal file
98
java/Demo.java
Normal file
@ -0,0 +1,98 @@
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 代付 Demo。编译:javac -encoding UTF-8 *.java
|
||||
* 运行:java Demo balance|submit|query <OrderNo>
|
||||
*/
|
||||
public class Demo {
|
||||
static final String BASE_URL = env("BASE_URL", "https://example.com").replaceAll("/$", "");
|
||||
static final String ACCESS_KEY = env("ACCESS_KEY", "your_access_key");
|
||||
static final String SECRET_KEY = env("SECRET_KEY", "your_secret_key");
|
||||
static final String PAY_CHANNEL_ID = env("PAY_CHANNEL_ID", "821");
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length < 1) {
|
||||
System.out.println("usage: java Demo balance|submit|query <OrderNo>");
|
||||
return;
|
||||
}
|
||||
String cmd = args[0];
|
||||
if ("balance".equals(cmd)) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("Timestamp", System.currentTimeMillis() / 1000);
|
||||
body.put("AccessKey", ACCESS_KEY);
|
||||
System.out.println(post("/api/apiv1/open/withdrawal/querybalance", body));
|
||||
} else if ("submit".equals(cmd)) {
|
||||
String orderNo = "OrderNo" + (System.currentTimeMillis() / 1000);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("Timestamp", System.currentTimeMillis() / 1000);
|
||||
body.put("AccessKey", ACCESS_KEY);
|
||||
body.put("PayChannelId", PAY_CHANNEL_ID);
|
||||
body.put("OrderNo", orderNo);
|
||||
body.put("Amount", "100.01");
|
||||
body.put("CallbackUrl", "https://xxxxx.com/xxxx/callback");
|
||||
body.put("Ext", "-");
|
||||
body.put("Payee", "张三");
|
||||
body.put("PayeeNo", "6235753100001261596");
|
||||
body.put("PayeeAddress", "招商银行");
|
||||
System.out.println(post("/api/apiv1/open/withdrawal/submit", body));
|
||||
} else if ("query".equals(cmd)) {
|
||||
if (args.length < 2) {
|
||||
System.out.println("usage: java Demo query <OrderNo>");
|
||||
return;
|
||||
}
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("Timestamp", System.currentTimeMillis() / 1000);
|
||||
body.put("AccessKey", ACCESS_KEY);
|
||||
body.put("OrderNo", args[1]);
|
||||
System.out.println(post("/api/apiv1/open/withdrawal/queryorder", body));
|
||||
} else {
|
||||
System.out.println("unknown command: " + cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static String post(String path, Map<String, Object> body) throws Exception {
|
||||
body.put("Sign", SignUtil.buildSign(body, SECRET_KEY));
|
||||
String json = toJson(body);
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(BASE_URL + path).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(30000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
try (InputStream in = conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream()) {
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
static String env(String k, String def) {
|
||||
String v = System.getenv(k);
|
||||
return v == null || v.isEmpty() ? def : v;
|
||||
}
|
||||
|
||||
/** 极简 JSON(仅本 Demo 字段类型) */
|
||||
static String toJson(Map<String, Object> map) {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Object> e : map.entrySet()) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
sb.append('"').append(e.getKey()).append('"').append(':');
|
||||
Object v = e.getValue();
|
||||
if (v instanceof Number) {
|
||||
sb.append(v);
|
||||
} else {
|
||||
sb.append('"').append(String.valueOf(v).replace("\\", "\\\\").replace("\"", "\\\"")).append('"');
|
||||
}
|
||||
}
|
||||
return sb.append('}').toString();
|
||||
}
|
||||
}
|
||||
7
java/README.md
Normal file
7
java/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Java
|
||||
|
||||
```bash
|
||||
javac -encoding UTF-8 *.java
|
||||
export BASE_URL=https://example.com
|
||||
java Demo balance
|
||||
```
|
||||
41
java/SignUtil.java
Normal file
41
java/SignUtil.java
Normal file
@ -0,0 +1,41 @@
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/** 空值不参与,字典序,MD5 小写 */
|
||||
public final class SignUtil {
|
||||
private SignUtil() {}
|
||||
|
||||
public static String buildSign(Map<String, Object> params, String secretKey) throws Exception {
|
||||
TreeMap<String, Object> sorted = new TreeMap<>();
|
||||
for (Map.Entry<String, Object> e : params.entrySet()) {
|
||||
if ("Sign".equals(e.getKey())) {
|
||||
continue;
|
||||
}
|
||||
sorted.put(e.getKey(), e.getValue());
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (Map.Entry<String, Object> e : sorted.entrySet()) {
|
||||
Object v = e.getValue();
|
||||
if (v == null) {
|
||||
continue;
|
||||
}
|
||||
String s = String.valueOf(v);
|
||||
if (s.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
parts.add(e.getKey() + "=" + s);
|
||||
}
|
||||
String raw = String.join("&", parts) + "&SecretKey=" + secretKey;
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] dig = md.digest(raw.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : dig) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
8
nodejs/README.md
Normal file
8
nodejs/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Node.js
|
||||
|
||||
需要 Node 18+(内置 fetch)。
|
||||
|
||||
```bash
|
||||
export BASE_URL=https://example.com
|
||||
node demo.js balance
|
||||
```
|
||||
64
nodejs/demo.js
Normal file
64
nodejs/demo.js
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const { buildSign } = require('./sign');
|
||||
|
||||
const BASE_URL = (process.env.BASE_URL || 'https://example.com').replace(/\/$/, '');
|
||||
const ACCESS_KEY = process.env.ACCESS_KEY || 'your_access_key';
|
||||
const SECRET_KEY = process.env.SECRET_KEY || 'your_secret_key';
|
||||
const PAY_CHANNEL_ID = process.env.PAY_CHANNEL_ID || '821';
|
||||
|
||||
async function post(path, body) {
|
||||
const payload = { ...body, Sign: buildSign(body, SECRET_KEY) };
|
||||
const res = await fetch(BASE_URL + path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [cmd, orderNo] = process.argv.slice(2);
|
||||
if (!cmd) {
|
||||
console.log('usage: node demo.js balance|submit|query <OrderNo>');
|
||||
process.exit(1);
|
||||
}
|
||||
if (cmd === 'balance') {
|
||||
console.log(JSON.stringify(await post('/api/apiv1/open/withdrawal/querybalance', {
|
||||
Timestamp: Math.floor(Date.now() / 1000),
|
||||
AccessKey: ACCESS_KEY,
|
||||
}), null, 2));
|
||||
} else if (cmd === 'submit') {
|
||||
const OrderNo = `OrderNo${Math.floor(Date.now() / 1000)}`;
|
||||
console.log(JSON.stringify(await post('/api/apiv1/open/withdrawal/submit', {
|
||||
Timestamp: Math.floor(Date.now() / 1000),
|
||||
AccessKey: ACCESS_KEY,
|
||||
PayChannelId: PAY_CHANNEL_ID,
|
||||
OrderNo,
|
||||
Amount: '100.01',
|
||||
CallbackUrl: 'https://xxxxx.com/xxxx/callback',
|
||||
Ext: '-',
|
||||
Payee: '张三',
|
||||
PayeeNo: '6235753100001261596',
|
||||
PayeeAddress: '招商银行',
|
||||
}), null, 2));
|
||||
} else if (cmd === 'query') {
|
||||
if (!orderNo) {
|
||||
console.log('usage: node demo.js query <OrderNo>');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(await post('/api/apiv1/open/withdrawal/queryorder', {
|
||||
Timestamp: Math.floor(Date.now() / 1000),
|
||||
AccessKey: ACCESS_KEY,
|
||||
OrderNo: orderNo,
|
||||
}), null, 2));
|
||||
} else {
|
||||
console.log('unknown command:', cmd);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
1
nodejs/package.json
Normal file
1
nodejs/package.json
Normal file
@ -0,0 +1 @@
|
||||
{"name":"open-api-demo","private":true,"type":"commonjs"}
|
||||
19
nodejs/sign.js
Normal file
19
nodejs/sign.js
Normal file
@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
const crypto = require('crypto');
|
||||
|
||||
/** 空值不参与,字典序,MD5 小写 */
|
||||
function buildSign(params, secretKey) {
|
||||
const keys = Object.keys(params).filter((k) => k !== 'Sign').sort();
|
||||
const parts = [];
|
||||
for (const k of keys) {
|
||||
const v = params[k];
|
||||
if (v === null || v === undefined) continue;
|
||||
const s = String(v);
|
||||
if (s === '') continue;
|
||||
parts.push(`${k}=${s}`);
|
||||
}
|
||||
const raw = parts.join('&') + `&SecretKey=${secretKey}`;
|
||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
module.exports = { buildSign };
|
||||
8
php/README.md
Normal file
8
php/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# PHP
|
||||
|
||||
需要 php-curl。
|
||||
|
||||
```bash
|
||||
export BASE_URL=https://example.com
|
||||
php demo.php balance
|
||||
```
|
||||
62
php/demo.php
Normal file
62
php/demo.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require __DIR__ . '/sign.php';
|
||||
|
||||
$BASE_URL = rtrim(getenv('BASE_URL') ?: 'https://example.com', '/');
|
||||
$ACCESS_KEY = getenv('ACCESS_KEY') ?: 'your_access_key';
|
||||
$SECRET_KEY = getenv('SECRET_KEY') ?: 'your_secret_key';
|
||||
$PAY_CHANNEL_ID = getenv('PAY_CHANNEL_ID') ?: '821';
|
||||
|
||||
function post_json(string $path, array $body): array {
|
||||
global $BASE_URL, $SECRET_KEY;
|
||||
$body['Sign'] = build_sign($body, $SECRET_KEY);
|
||||
$ch = curl_init($BASE_URL . $path);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
if ($resp === false) {
|
||||
throw new RuntimeException(curl_error($ch));
|
||||
}
|
||||
curl_close($ch);
|
||||
return json_decode($resp, true) ?: [];
|
||||
}
|
||||
|
||||
$cmd = $argv[1] ?? '';
|
||||
if ($cmd === 'balance') {
|
||||
echo json_encode(post_json('/api/apiv1/open/withdrawal/querybalance', [
|
||||
'Timestamp' => time(),
|
||||
'AccessKey' => $ACCESS_KEY,
|
||||
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), PHP_EOL;
|
||||
} elseif ($cmd === 'submit') {
|
||||
$orderNo = 'OrderNo' . time();
|
||||
echo json_encode(post_json('/api/apiv1/open/withdrawal/submit', [
|
||||
'Timestamp' => time(),
|
||||
'AccessKey' => $ACCESS_KEY,
|
||||
'PayChannelId' => $PAY_CHANNEL_ID,
|
||||
'OrderNo' => $orderNo,
|
||||
'Amount' => '100.01',
|
||||
'CallbackUrl' => 'https://xxxxx.com/xxxx/callback',
|
||||
'Ext' => '-',
|
||||
'Payee' => '张三',
|
||||
'PayeeNo' => '6235753100001261596',
|
||||
'PayeeAddress' => '招商银行',
|
||||
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), PHP_EOL;
|
||||
} elseif ($cmd === 'query') {
|
||||
$orderNo = $argv[2] ?? '';
|
||||
if ($orderNo === '') {
|
||||
fwrite(STDERR, "usage: php demo.php query <OrderNo>\n");
|
||||
exit(1);
|
||||
}
|
||||
echo json_encode(post_json('/api/apiv1/open/withdrawal/queryorder', [
|
||||
'Timestamp' => time(),
|
||||
'AccessKey' => $ACCESS_KEY,
|
||||
'OrderNo' => $orderNo,
|
||||
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), PHP_EOL;
|
||||
} else {
|
||||
fwrite(STDERR, "usage: php demo.php balance|submit|query <OrderNo>\n");
|
||||
exit(1);
|
||||
}
|
||||
19
php/sign.php
Normal file
19
php/sign.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/** 空值不参与,字典序,MD5 小写 */
|
||||
function build_sign(array $params, string $secretKey): string {
|
||||
unset($params['Sign']);
|
||||
ksort($params, SORT_STRING);
|
||||
$parts = [];
|
||||
foreach ($params as $k => $v) {
|
||||
if ($v === null) {
|
||||
continue;
|
||||
}
|
||||
$s = (string)$v;
|
||||
if ($s === '') {
|
||||
continue;
|
||||
}
|
||||
$parts[] = $k . '=' . $s;
|
||||
}
|
||||
$raw = implode('&', $parts) . '&SecretKey=' . $secretKey;
|
||||
return md5($raw);
|
||||
}
|
||||
8
python/README.md
Normal file
8
python/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Python
|
||||
|
||||
```bash
|
||||
export BASE_URL=https://example.com
|
||||
export ACCESS_KEY=...
|
||||
export SECRET_KEY=...
|
||||
python3 demo.py balance
|
||||
```
|
||||
85
python/demo.py
Normal file
85
python/demo.py
Normal file
@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""代付 Demo:balance / submit / query"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from sign import build_sign
|
||||
|
||||
BASE_URL = os.environ.get("BASE_URL", "https://example.com").rstrip("/")
|
||||
ACCESS_KEY = os.environ.get("ACCESS_KEY", "your_access_key")
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", "your_secret_key")
|
||||
PAY_CHANNEL_ID = os.environ.get("PAY_CHANNEL_ID", "821")
|
||||
|
||||
|
||||
def post(path: str, body: dict) -> dict:
|
||||
body = dict(body)
|
||||
body["Sign"] = build_sign(body, SECRET_KEY)
|
||||
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
BASE_URL + path,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def cmd_balance() -> None:
|
||||
print(json.dumps(post("/api/apiv1/open/withdrawal/querybalance", {
|
||||
"Timestamp": int(time.time()),
|
||||
"AccessKey": ACCESS_KEY,
|
||||
}), ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def cmd_submit() -> None:
|
||||
order_no = f"OrderNo{int(time.time())}"
|
||||
print(json.dumps(post("/api/apiv1/open/withdrawal/submit", {
|
||||
"Timestamp": int(time.time()),
|
||||
"AccessKey": ACCESS_KEY,
|
||||
"PayChannelId": PAY_CHANNEL_ID,
|
||||
"OrderNo": order_no,
|
||||
"Amount": "100.01",
|
||||
"CallbackUrl": "https://xxxxx.com/xxxx/callback",
|
||||
"Ext": "-",
|
||||
"Payee": "张三",
|
||||
"PayeeNo": "6235753100001261596",
|
||||
"PayeeAddress": "招商银行",
|
||||
}), ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def cmd_query(order_no: str) -> None:
|
||||
print(json.dumps(post("/api/apiv1/open/withdrawal/queryorder", {
|
||||
"Timestamp": int(time.time()),
|
||||
"AccessKey": ACCESS_KEY,
|
||||
"OrderNo": order_no,
|
||||
}), ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: demo.py balance|submit|query <OrderNo>")
|
||||
sys.exit(1)
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "balance":
|
||||
cmd_balance()
|
||||
elif cmd == "submit":
|
||||
cmd_submit()
|
||||
elif cmd == "query":
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: demo.py query <OrderNo>")
|
||||
sys.exit(1)
|
||||
cmd_query(sys.argv[2])
|
||||
else:
|
||||
print("unknown command:", cmd)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
python/sign.py
Normal file
22
python/sign.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""OpenAPI 签名:空值不参与,字典序,MD5 小写。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
def build_sign(params: Mapping[str, Any], secret_key: str) -> str:
|
||||
items = []
|
||||
for k in sorted(params.keys()):
|
||||
if k == "Sign":
|
||||
continue
|
||||
v = params[k]
|
||||
if v is None:
|
||||
continue
|
||||
s = str(v)
|
||||
if s == "":
|
||||
continue
|
||||
items.append(f"{k}={s}")
|
||||
raw = "&".join(items) + f"&SecretKey={secret_key}"
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest()
|
||||
Loading…
Reference in New Issue
Block a user