Files
oil/server-tp6/public/qyt_test.php
T

433 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* ============================================================
* 慧明宝入站接口 · 联调测试脚本(模拟「下单平台」)
* ------------------------------------------------------------
* 放在 public/ 下,浏览器访问:http://你的域名/qyt_test.php
* 也可命令行:php qyt_test.php share
*
* 它做四件事:
* 1) 从数据库读取合作商(appKey / RSA 私钥 / orgNo)
* 2) 按「慧明宝」规范组装 RSA+AES 信封并调用我方入站接口
* 3) 展示明文请求 / 待签串 / 密文报文 / 服务端响应
* 4) act=notify_receiver 作为回调接收端,解密+验签我方推送的
* syb.notify.oilCard.change,并返回 {"code":"00"}
*
* ⚠️ 仅用于联调,正式环境请删除本文件。
* ============================================================
*/
const TEST_APPKEY = 'qyttest0000000000000000000001'; // 测试合作商 appKey
const API_VERSION = '1.0.0';
date_default_timezone_set('Asia/Shanghai');
$ROOT = dirname(__DIR__);
// ------------------------------------------------------------
// 基础工具
// ------------------------------------------------------------
function envConf($root)
{
$file = $root . '/.env';
if (!is_file($file)) {
throw new Exception('.env 不存在:' . $file);
}
$ini = parse_ini_file($file, true, INI_SCANNER_RAW) ?: [];
$db = isset($ini['DATABASE']) ? $ini['DATABASE'] : [];
return [
'host' => isset($db['HOSTNAME']) ? $db['HOSTNAME'] : '127.0.0.1',
'port' => isset($db['HOSTPORT']) ? $db['HOSTPORT'] : '3306',
'name' => isset($db['DATABASE']) ? $db['DATABASE'] : 'oil',
'user' => isset($db['USERNAME']) ? $db['USERNAME'] : 'root',
'pass' => isset($db['PASSWORD']) ? $db['PASSWORD'] : '',
'char' => isset($db['CHARSET']) ? $db['CHARSET'] : 'utf8',
];
}
function db($root)
{
static $pdo = null;
if ($pdo === null) {
$c = envConf($root);
$dsn = 'mysql:host=' . $c['host'] . ';port=' . $c['port'] . ';dbname=' . $c['name'] . ';charset=' . $c['char'];
$pdo = new PDO($dsn, $c['user'], $c['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}
/** 读取合作商(下单平台账户) */
function getPartner($root, $appKey)
{
$st = db($root)->prepare('SELECT * FROM yoshop_partners WHERE appKey = ? LIMIT 1');
$st->execute([$appKey]);
$row = $st->fetch();
if (!$row) {
throw new Exception('未找到合作商,appKey=' . $appKey . '(请先执行 deploy/seed_test_partner.sql)');
}
return $row;
}
/** 平台公钥:加密 AES 密钥 / 验签我方回调 */
function platformPublicKey($root)
{
$p = $root . '/rsa/public_key.pem';
if (!is_file($p)) {
throw new Exception('平台公钥不存在:' . $p);
}
return file_get_contents($p);
}
function uniqueRequestId($appKey)
{
return $appKey . strtoupper(bin2hex(random_bytes(12)));
}
/**
* 组装请求信封(口径对齐 app\common\crypto\RsaUtil::processResponse)
* 待签串:http_build_query([version, appKey, method, requestId, body])
* body :json_encode($body, JSON_UNESCAPED_UNICODE)
* 签名 :SHA256withRSA,用【合作商私钥】
* AES :AES-256-ECB,随机 32 字节密钥;密钥用【平台公钥】RSA 加密
*/
function buildEnvelope($partner, $platformPub, $method, array $body)
{
$bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE);
$requestId = uniqueRequestId($partner['appKey']);
$signData = http_build_query([
'version' => API_VERSION,
'appKey' => $partner['appKey'],
'method' => $method,
'requestId' => $requestId,
'body' => $bodyJson,
]);
$pkey = openssl_pkey_get_private($partner['privateKey']);
if ($pkey === false) {
throw new Exception('合作商私钥无效:' . openssl_error_string());
}
$signature = '';
openssl_sign($signData, $signature, $pkey, OPENSSL_ALGO_SHA256);
// AES-256-ECB 加密 body(与服务端 encryptData/decryptData 完全一致)
$aesKey = openssl_random_pseudo_bytes(32);
$encBody = base64_encode(openssl_encrypt($bodyJson, 'AES-256-ECB', $aesKey, OPENSSL_PKCS1_PADDING));
// 平台公钥加密 AES 密钥
$pub = openssl_pkey_get_public($platformPub);
if ($pub === false) {
throw new Exception('平台公钥无效:' . openssl_error_string());
}
$encKey = '';
openssl_public_encrypt($aesKey, $encKey, $pub, OPENSSL_PKCS1_PADDING);
return [
'headers' => [
'version' => API_VERSION,
'appKey' => $partner['appKey'],
'method' => $method,
'requestId' => $requestId,
'sign' => base64_encode($signature),
],
'body' => ['key' => base64_encode($encKey), 'data' => $encBody],
'plain' => $body,
'signData' => $signData,
];
}
function httpPost($url, array $headers, array $jsonBody)
{
$h = ['Content-Type: application/json', 'Charset: UTF-8'];
foreach ($headers as $k => $v) {
$h[] = $k . ': ' . $v;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($jsonBody, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => $h,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$resp = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['http' => $code, 'body' => $resp, 'error' => $err];
}
/** 入站接口地址:public/partners.php/<控制器>/<方法> */
function apiUrl($path)
{
if (!empty($_GET['api'])) {
return rtrim($_GET['api'], '/') . '/partners.php' . $path;
}
if (PHP_SAPI === 'cli') {
return 'http://127.0.0.1/partners.php' . $path;
}
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
return $scheme . '://' . $_SERVER['HTTP_HOST'] . '/partners.php' . $path;
}
// ------------------------------------------------------------
// 回调接收端(模拟下单平台的 callback 地址)
// 我方 NotifyQueue 用 RsaUtil::prepareRequest 推送,注意它的待签串是
// 「原样拼接」,与入站的 http_build_query 不同。
// ------------------------------------------------------------
function notifyReceiver($root)
{
$raw = file_get_contents('php://input');
$headers = [];
foreach ($_SERVER as $k => $v) {
if (strpos($k, 'HTTP_') === 0) {
$headers[strtolower(str_replace('_', '', substr($k, 5)))] = $v;
}
}
$log = $root . '/runtime/qyt_test_notify.log';
@file_put_contents($log, date('Y-m-d H:i:s') . " 收到回调\nheaders=" . json_encode($headers, 320) . "\nbody=" . $raw . "\n", FILE_APPEND);
try {
$data = json_decode($raw, true);
$appKey = isset($headers['appkey']) ? $headers['appkey'] : TEST_APPKEY;
$partner = getPartner($root, $appKey);
// 1) 合作商私钥解出 AES 密钥
$pkey = openssl_pkey_get_private($partner['privateKey']);
$aesKey = '';
openssl_private_decrypt(base64_decode($data['key']), $aesKey, $pkey, OPENSSL_PKCS1_PADDING);
// 2) 解密 body
$plain = openssl_decrypt(base64_decode($data['data']), 'AES-256-ECB', $aesKey, OPENSSL_PKCS1_PADDING);
$body = json_decode($plain, true);
// 3) 平台公钥验签
$signData = 'version=' . API_VERSION
. '&appKey=' . $partner['appKey']
. '&method=' . (isset($headers['method']) ? $headers['method'] : '')
. '&requestId=' . (isset($headers['requestid']) ? $headers['requestid'] : '')
. '&body=' . json_encode($body);
$sign = isset($headers['sign']) ? $headers['sign'] : '';
$ok = openssl_verify($signData, base64_decode($sign), openssl_pkey_get_public(platformPublicKey($root)), OPENSSL_ALGO_SHA256);
@file_put_contents($log, '解密结果=' . json_encode($body, 320) . ' 验签=' . ($ok === 1 ? 'OK' : 'FAIL') . "\n\n", FILE_APPEND);
header('Content-Type: application/json');
echo json_encode(['code' => '00', 'message' => '请求成功'], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
@file_put_contents($log, '异常:' . $e->getMessage() . "\n\n", FILE_APPEND);
header('Content-Type: application/json');
echo json_encode(['code' => '99', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
exit;
}
// ------------------------------------------------------------
// 各接口默认入参
// ------------------------------------------------------------
function defaultBody($act, $orgNo)
{
$saleOrderNo = 'TEST' . date('YmdHis') . mt_rand(100, 999);
$oilCardNo = isset($_GET['oilCardNo']) ? $_GET['oilCardNo'] : '';
$mobile = isset($_GET['mobile']) ? $_GET['mobile'] : '13800000001';
if ($act === 'share') {
return [
'method' => 'syb.oil.oilCard.share',
'path' => '/oilCard/share',
'body' => [
'orgNo' => $orgNo,
'amount' => isset($_GET['amount']) ? $_GET['amount'] : '100.00',
'mobile' => $mobile,
'name' => isset($_GET['name']) ? $_GET['name'] : '张三',
'saleOrderNo' => $saleOrderNo,
'oilCardType' => 'zsy_ticket',
],
];
}
if ($act === 'qrcode') {
return [
'method' => 'syb.pay.qrCode.request',
'path' => '/oilCard/getPayQrCode',
'body' => ['orgNo' => $orgNo, 'oilCardNo' => $oilCardNo, 'mobile' => $mobile],
];
}
if ($act === 'status') {
return [
'method' => 'syb.oil.oilCard.orderStatus',
'path' => '/oilCard/orderStatus',
'body' => ['orgNo' => $orgNo, 'oilCardNo' => $oilCardNo],
];
}
if ($act === 'sharelist') {
return [
'method' => 'syb.oil.oilCard.shareList',
'path' => '/oilCard/shareList',
'body' => [
'orgNo' => $orgNo,
'mobile' => $mobile,
'beginDate' => date('Ymd', strtotime('-7 day')),
'endDate' => date('Ymd'),
'pageLimit' => '10',
'pageNum' => '1',
],
];
}
return null;
}
// ------------------------------------------------------------
// 主流程
// ------------------------------------------------------------
$act = isset($_GET['act']) ? $_GET['act'] : (isset($argv[1]) ? $argv[1] : '');
$fatal = '';
$result = null;
$partner = null;
try {
if ($act === 'notify_receiver') {
notifyReceiver($ROOT);
}
$partner = getPartner($ROOT, TEST_APPKEY);
$platformPub = platformPublicKey($ROOT);
if (in_array($act, ['share', 'qrcode', 'status', 'sharelist'], true)) {
$def = defaultBody($act, $partner['orgNo']);
$env = buildEnvelope($partner, $platformPub, $def['method'], $def['body']);
$url = apiUrl($def['path']);
$rsp = httpPost($url, $env['headers'], $env['body']);
$result = ['def' => $def, 'env' => $env, 'url' => $url, 'rsp' => $rsp];
}
} catch (Throwable $e) {
$fatal = $e->getMessage();
}
// ---------- 命令行输出 ----------
if (PHP_SAPI === 'cli') {
if ($fatal !== '') {
echo '错误:' . $fatal . "\n";
exit(1);
}
if (!$result) {
echo "用法:php qyt_test.php [share|qrcode|status|sharelist]\n";
exit;
}
echo '接口:' . $result['def']['method'] . "\n";
echo 'URL :' . $result['url'] . "\n";
echo '明文:' . json_encode($result['env']['plain'], 320) . "\n";
echo 'HTTP:' . $result['rsp']['http'] . "\n";
echo '响应:' . $result['rsp']['body'] . "\n";
if ($result['rsp']['error'] !== '') {
echo 'cURL:' . $result['rsp']['error'] . "\n";
}
exit;
}
$rspArr = $result ? json_decode($result['rsp']['body'], true) : null;
$okFlag = is_array($rspArr) && isset($rspArr['code']) && $rspArr['code'] === '00';
$newOil = is_array($rspArr) && isset($rspArr['data']['oilCardNo']) ? $rspArr['data']['oilCardNo'] : '';
$curOil = isset($_GET['oilCardNo']) ? $_GET['oilCardNo'] : '';
?><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>慧明宝入站接口 · 联调测试</title>
<style>
body{font:14px/1.7 -apple-system,"Microsoft YaHei",sans-serif;margin:0;background:#f5f6f8;color:#222}
.wrap{max-width:1000px;margin:0 auto;padding:24px}
h1{font-size:20px;margin:0 0 4px}
.sub{color:#888;font-size:13px;margin-bottom:18px}
.card{background:#fff;border:1px solid #e6e8eb;border-radius:8px;padding:16px 18px;margin-bottom:16px}
.card h2{font-size:15px;margin:0 0 12px;padding-bottom:8px;border-bottom:1px solid #eee}
.btns a{display:inline-block;margin:0 8px 8px 0;padding:7px 16px;background:#1890ff;color:#fff;
border-radius:4px;text-decoration:none;font-size:13px}
.btns a.g{background:#52c41a}.btns a.o{background:#faad14}.btns a.p{background:#722ed1}
table{border-collapse:collapse;width:100%;font-size:13px}
th,td{border:1px solid #eee;padding:6px 10px;text-align:left;vertical-align:top;word-break:break-all}
th{background:#fafafa;width:150px;font-weight:600;color:#555}
pre{background:#282c34;color:#abb2bf;padding:12px;border-radius:6px;overflow:auto;
font-size:12px;line-height:1.6;margin:0;white-space:pre-wrap;word-break:break-all}
.ok{color:#52c41a;font-weight:600}.err{color:#ff4d4f;font-weight:600}
.tip{background:#fffbe6;border:1px solid #ffe58f;padding:10px 14px;border-radius:6px;font-size:13px;margin-bottom:16px}
.lbl{margin:12px 0 6px;color:#555}
</style>
</head>
<body><div class="wrap">
<h1>慧明宝入站接口 · 联调测试</h1>
<div class="sub">模拟「下单平台」调用我方 API(RSA + AES 信封)。仅供联调,正式环境请删除本文件。</div>
<?php if ($fatal !== ''): ?>
<div class="card"><h2>初始化失败</h2><p class="err"><?php echo htmlspecialchars($fatal); ?></p></div>
<?php else: ?>
<div class="card">
<h2>当前合作商</h2>
<table>
<tr><th>partners_id</th><td><?php echo (int)$partner['partners_id']; ?></td></tr>
<tr><th>名称</th><td><?php echo htmlspecialchars($partner['name']); ?></td></tr>
<tr><th>orgNo</th><td><?php echo htmlspecialchars($partner['orgNo']); ?></td></tr>
<tr><th>appKey</th><td><?php echo htmlspecialchars($partner['appKey']); ?></td></tr>
<tr><th>余额</th><td><b><?php echo htmlspecialchars($partner['balance']); ?></b> 元 (下单会扣减,不足返回 2028)</td></tr>
<tr><th>回调地址</th><td><?php echo htmlspecialchars($partner['callback']); ?></td></tr>
</table>
</div>
<div class="tip">
接口地址默认取当前域名;如需指定,链接后加 <code>&amp;api=http://你的域名</code>。<br>
<b>下单前提</b>:三个队列进程已启动(partners:queue / callback:queue / notify:queue)、Redis 可用、合作商余额充足。
</div>
<div class="card">
<h2>发起测试</h2>
<div class="btns">
<a class="g" href="?act=share">① 下单分油 (share)</a>
<a class="o" href="?act=qrcode&amp;oilCardNo=<?php echo urlencode($curOil); ?>">② 获取加油链接 (qrCode)</a>
<a class="p" href="?act=status&amp;oilCardNo=<?php echo urlencode($curOil); ?>">③ 订单状态 (orderStatus)</a>
<a href="?act=sharelist">④ 分油记录 (shareList)</a>
</div>
<div class="sub" style="margin:6px 0 0">
②③ 需要 oilCardNo:先点①拿到后会自动带上。<br>
① 可自定义参数:<code>?act=share&amp;amount=50.00&amp;mobile=13800000002&amp;name=李四</code>
</div>
</div>
<?php if ($result): ?>
<div class="card">
<h2>请求 · <?php echo htmlspecialchars($result['def']['method']); ?></h2>
<table>
<tr><th>URL</th><td><?php echo htmlspecialchars($result['url']); ?></td></tr>
<tr><th>requestId</th><td><?php echo htmlspecialchars($result['env']['headers']['requestId']); ?></td></tr>
</table>
<p class="lbl">明文 body</p>
<pre><?php echo htmlspecialchars(json_encode($result['env']['plain'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?></pre>
<p class="lbl">待签串(http_build_query)</p>
<pre><?php echo htmlspecialchars($result['env']['signData']); ?></pre>
<p class="lbl">实际发送(密文信封)</p>
<pre><?php echo htmlspecialchars(json_encode($result['env']['body'], JSON_PRETTY_PRINT)); ?></pre>
</div>
<div class="card">
<h2>响应 <?php echo $okFlag ? '<span class="ok">成功</span>' : '<span class="err">失败</span>'; ?></h2>
<table>
<tr><th>HTTP 状态</th><td><?php echo (int)$result['rsp']['http']; ?></td></tr>
<?php if ($result['rsp']['error'] !== ''): ?>
<tr><th>cURL 错误</th><td class="err"><?php echo htmlspecialchars($result['rsp']['error']); ?></td></tr>
<?php endif; ?>
</table>
<p class="lbl">响应体</p>
<pre><?php echo htmlspecialchars(is_array($rspArr) ? json_encode($rspArr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $result['rsp']['body']); ?></pre>
<?php if ($newOil !== ''): ?>
<div class="btns" style="margin-top:14px">
<a class="o" href="?act=qrcode&amp;oilCardNo=<?php echo urlencode($newOil); ?>">用 <?php echo htmlspecialchars($newOil); ?> 取加油链接</a>
<a class="p" href="?act=status&amp;oilCardNo=<?php echo urlencode($newOil); ?>">查该单状态</a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div></body></html>