默认企业),含是否要求身份证号、机构号是否已配置 * 3) 按「慧明宝」规范组装 RSA+AES 信封并调用我方入站接口 * 4) 展示明文请求 / 待签串 / 密文报文 / 服务端响应 * 5) 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; } /** * 解析某个合作商下单时实际使用的企油通企业与通道。 * 口径与 app\common\model\qyt\Enterprise::resolve 一致: * 合作商绑定的企业 -> 默认企业(is_default=1 且启用) * 接入方级参数(域名 / appId / 密钥)仍取 .env 的 [QYT] 段与后台设置。 * 本脚本是独立入口、不加载 ThinkPHP,所以重新实现了一遍,改动时两边要同步。 */ function qytConf($root, $partner) { static $conf = null; if ($conf !== null) { return $conf; } $ini = parse_ini_file($root . '/.env', true, INI_SCANNER_RAW) ?: []; $q = isset($ini['QYT']) ? $ini['QYT'] : []; $get = function ($k, $d = '') use ($q) { return isset($q[$k]) && trim((string)$q[$k]) !== '' ? trim((string)$q[$k]) : $d; }; $conf = [ 'base_url' => $get('BASE_URL'), 'no_prefix' => $get('NO_PREFIX', 'HM'), 'source' => '.env', 'enterprise' => null, ]; // 后台「企油通对接设置」覆盖接入方级默认值 try { $st = db($root)->prepare("SELECT `values` FROM yoshop_store_setting WHERE `key`='qyt' ORDER BY store_id ASC LIMIT 1"); $st->execute(); $json = $st->fetchColumn(); $row = $json ? json_decode($json, true) : null; if (is_array($row)) { $conf['source'] = '.env + 后台设置'; foreach (['base_url', 'no_prefix'] as $k) { if (isset($row[$k]) && trim((string)$row[$k]) !== '') { $conf[$k] = trim((string)$row[$k]); } } } } catch (Throwable $e) { // 忽略 } // 企业:合作商绑定的 -> 默认企业 try { $pdo = db($root); $ent = null; $bind = (int)(isset($partner['qyt_enterprise_id']) ? $partner['qyt_enterprise_id'] : 0); if ($bind > 0) { $st = $pdo->prepare('SELECT * FROM yoshop_qyt_enterprise WHERE enterprise_id = ? AND is_enable = 1 LIMIT 1'); $st->execute([$bind]); $ent = $st->fetch() ?: null; } if (!$ent) { $st = $pdo->query('SELECT * FROM yoshop_qyt_enterprise WHERE is_enable = 1 AND is_default = 1 ORDER BY enterprise_id ASC LIMIT 1'); $ent = $st->fetch() ?: null; $conf['from_default'] = (bool)$ent; } else { $conf['from_default'] = false; } if ($ent) { $pw = isset($ent['passageway']) && $ent['passageway'] === 'pingan' ? 'pingan' : 'alipay'; $ent['passageway'] = $pw; $ent['passageway_code'] = $pw === 'pingan' ? 2 : 1; $ent['passageway_name'] = $pw === 'pingan' ? '平安银行' : '支付宝企业码'; $ent['require_id_card'] = $pw === 'pingan'; // 企业可覆盖编号前缀 if (trim((string)$ent['no_prefix']) !== '') { $conf['no_prefix'] = trim((string)$ent['no_prefix']); } $conf['enterprise'] = $ent; } } catch (Throwable $e) { $conf['enterprise_error'] = $e->getMessage(); } return $conf; } /** 平台公钥:加密 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') { $body = [ 'orgNo' => $orgNo, 'amount' => isset($_GET['amount']) ? $_GET['amount'] : '100.00', 'mobile' => $mobile, 'name' => isset($_GET['name']) ? $_GET['name'] : '张三', 'saleOrderNo' => $saleOrderNo, 'oilCardType' => 'zsy_ticket', ]; // 司机身份证号:平安银行通道必传,支付宝企业码通道可不传。 // 加 &noIdCard=1 可故意不传,用来验证实名通道下的 0001 拒单是否生效。 if (empty($_GET['noIdCard'])) { $body['idCard'] = isset($_GET['idCard']) ? $_GET['idCard'] : '450103199001011238'; } return [ 'method' => 'syb.oil.oilCard.share', 'path' => '/oilCard/share', 'body' => $body, ]; } 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; $qconf = null; try { if ($act === 'notify_receiver') { notifyReceiver($ROOT); } $partner = getPartner($ROOT, TEST_APPKEY); $platformPub = platformPublicKey($ROOT); $qconf = qytConf($ROOT, $partner); 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); } $ent = $qconf['enterprise']; if (!$ent) { echo "当前企业:【未匹配到可用企业】请先在后台「设置 -> 企业管理」新建企业并设为默认,否则下单会被拒绝 "; } else { echo '当前企业:' . $ent['company_name'] . '(' . $ent['business_license'] . ')' . ($qconf['from_default'] ? ' [默认企业]' : ' [合作商绑定]') . " "; echo '当前通道:' . $ent['passageway_name'] . '(passageway=' . $ent['passageway_code'] . ')' . ' 身份证号' . ($ent['require_id_card'] ? '必传' : '可不传') . ' 机构号' . ($ent['institution_id'] !== '' ? '已配置' : '【未配置,下发必失败】') . ' 企业状态' . $ent['status'] . " "; echo '编号前缀:' . $qconf['no_prefix'] . ' 接入方配置来源:' . $qconf['source'] . " "; } 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'] : ''; ?>
| partners_id | |
|---|---|
| 名称 | |
| orgNo | |
| appKey | |
| 余额 | 元 (下单会扣减,不足返回 2028) |
| 回调地址 |
未匹配到可用企业。请先到后台「设置 → 企业管理」新建企业并设为默认,否则下单接口会直接拒绝。
读取企业表出错:
| 企业 | 默认企业兜底)' : '(该合作商已绑定此企业)'; ?> |
|---|---|
| 统一社会信用代码 | |
| 企业状态 | 审核通过 未通过审核时 7.15 免登地址会报「企业尚未通过审核」 |
| 支付通道 | (passageway=) |
| 司机身份证号 | 必传 该通道要求实名,下单不传 idCard 会返回 0001 拒单 可不传 该通道不要求实名,传了也不会上送给企油通 |
| 机构号 | 已配置 未配置 支付宝企业码通道必填,下发时会失败并原路退回余额 平安通道无需机构号 |
| 编号前缀 | |
| 企油通域名 | 未配置'; ?> 来源: |
&api=http://你的域名。?act=share&amount=50.00&mobile=13800000002&name=李四&idCard=110101199003071233| URL | |
|---|---|
| requestId |
明文 body
待签串(http_build_query)
实际发送(密文信封)