200 lines
8.9 KiB
PHP
200 lines
8.9 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | 模拟企油通 7.10「运单加油明细推送」 —— 联调 / 补推用(CLI)
|
||
// +----------------------------------------------------------------------
|
||
// | 按企油通的真实口径推送:multipart/form-data,业务参数放在表单字段 params 里。
|
||
// |
|
||
// | 用法(在站点根目录执行):
|
||
// | php deploy/qyt_consume_push.php --order=6 --amount=100
|
||
// | php deploy/qyt_consume_push.php --code=HMCTEST20260914233556192 --amount=100
|
||
// | php deploy/qyt_consume_push.php --order=6 --amount=30 --dry-run # 只打印报文不发送
|
||
// |
|
||
// | 常用参数:
|
||
// | --order=订单ID 从数据库取运单号等信息(推荐)
|
||
// | --code=运单唯一流水 不查库直接指定 waybill_no_code
|
||
// | --amount=金额(元) 本次加油占用金额,默认取订单剩余未加油金额
|
||
// | --order-code=消费单号 去重键,默认自动生成;想测【重复推送】就填上次用过的
|
||
// | --station=加油站名 默认「模拟加油站」
|
||
// | --url=回调地址 默认取 .env 的 QYT.CONSUME_NOTIFY_URL
|
||
// | --dry-run 只打印,不实际发送
|
||
// |
|
||
// | ⚠️ 这会写入真实的加油流水、累计订单已加油金额、并触发给下单平台的加油通知,
|
||
// | 只用于联调环境。联调结束请删除本文件。
|
||
// +----------------------------------------------------------------------
|
||
declare (strict_types=1);
|
||
|
||
if (PHP_SAPI !== 'cli') {
|
||
http_response_code(403);
|
||
exit('仅限命令行执行');
|
||
}
|
||
|
||
$root = dirname(__DIR__);
|
||
|
||
// ---------------- 参数解析 ----------------
|
||
$opt = [];
|
||
foreach (array_slice($argv, 1) as $arg) {
|
||
if (preg_match('/^--([a-zA-Z0-9\-_]+)(?:=(.*))?$/', $arg, $m)) {
|
||
$opt[str_replace('-', '_', $m[1])] = $m[2] ?? '1';
|
||
}
|
||
}
|
||
$orderId = isset($opt['order']) ? (int) $opt['order'] : 0;
|
||
$code = isset($opt['code']) ? trim((string) $opt['code']) : '';
|
||
$dryRun = !empty($opt['dry_run']);
|
||
$station = isset($opt['station']) ? (string) $opt['station'] : '模拟加油站';
|
||
$orderCode = isset($opt['order_code']) ? trim((string) $opt['order_code']) : '';
|
||
|
||
if ($orderId <= 0 && $code === '') {
|
||
exit("用法:php deploy/qyt_consume_push.php --order=<订单ID> [--amount=金额] [--dry-run]\n"
|
||
. " php deploy/qyt_consume_push.php --code=<运单唯一流水> --amount=金额\n"
|
||
. "详细参数见本文件头部注释。\n");
|
||
}
|
||
|
||
// ---------------- 读取 .env ----------------
|
||
$ini = @parse_ini_file($root . '/.env', true, INI_SCANNER_RAW) ?: [];
|
||
$db = $ini['DATABASE'] ?? [];
|
||
$qyt = $ini['QYT'] ?? [];
|
||
$url = isset($opt['url']) ? (string) $opt['url'] : trim((string) ($qyt['CONSUME_NOTIFY_URL'] ?? ''));
|
||
if ($url === '') {
|
||
$url = 'http://127.0.0.1/qytconsume.php';
|
||
}
|
||
|
||
// ---------------- 取订单(可选) ----------------
|
||
$pdo = null;
|
||
$order = null;
|
||
try {
|
||
$pdo = new PDO(
|
||
'mysql:host=' . ($db['HOSTNAME'] ?? '127.0.0.1') . ';port=' . ($db['HOSTPORT'] ?? '3306')
|
||
. ';dbname=' . ($db['DATABASE'] ?? 'oil') . ';charset=' . ($db['CHARSET'] ?? 'utf8'),
|
||
$db['USERNAME'] ?? 'root', $db['PASSWORD'] ?? '',
|
||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||
);
|
||
} catch (Throwable $e) {
|
||
echo "提示:连不上数据库({$e->getMessage()}),将只发送推送、不做前后对比\n\n";
|
||
}
|
||
|
||
if ($pdo) {
|
||
$sql = $orderId > 0
|
||
? 'SELECT * FROM yoshop_partners_order WHERE order_id = ? LIMIT 1'
|
||
: 'SELECT * FROM yoshop_partners_order WHERE waybill_no_code = ? LIMIT 1';
|
||
$st = $pdo->prepare($sql);
|
||
$st->execute([$orderId > 0 ? $orderId : $code]);
|
||
$order = $st->fetch() ?: null;
|
||
if (!$order && $orderId > 0) {
|
||
exit("未找到订单 order_id={$orderId}\n");
|
||
}
|
||
}
|
||
|
||
if ($order) {
|
||
$code = (string) $order['waybill_no_code'];
|
||
if ($code === '') {
|
||
exit("订单 {$order['order_id']} 还没有运单流水号(waybill_no_code 为空),说明 7.7 还没成功下发,不能模拟加油\n");
|
||
}
|
||
}
|
||
if ($code === '') {
|
||
exit("缺少 waybill_no_code:请用 --code 指定,或用 --order 指定订单\n");
|
||
}
|
||
|
||
// ---------------- 金额 ----------------
|
||
$remain = $order ? round((float) $order['total_price'] - (float) $order['used_amount'], 2) : 0.0;
|
||
$amount = isset($opt['amount']) ? round((float) $opt['amount'], 2) : $remain;
|
||
if ($amount <= 0) {
|
||
exit("加油金额必须大于 0(订单剩余未加油金额为 " . number_format($remain, 2) . " 元,可用 --amount 指定)\n");
|
||
}
|
||
if ($order && $amount > $remain + 0.005) {
|
||
echo "⚠️ 本次金额 {$amount} 元 超过订单剩余未加油金额 " . number_format($remain, 2) . " 元,"
|
||
. "服务端会照常入账并写超额告警日志\n";
|
||
}
|
||
|
||
// ---------------- 组装报文(字段与 7.10 文档一致) ----------------
|
||
$fen = (int) round($amount * 100);
|
||
$item = [
|
||
'waybill_no' => $order['waybill_no'] ?? ('SIMW' . $code),
|
||
'order_code' => $orderCode !== '' ? $orderCode : ('SIM' . date('YmdHis') . mt_rand(1000, 9999)),
|
||
'waybill_no_code' => $code,
|
||
'quota_id' => $order['qyt_order_code'] ?? ('SIMQ' . mt_rand(100000, 999999)),
|
||
'passageway' => ($order['passageway'] ?? 'alipay') === 'pingan' ? 2 : 1,
|
||
'station_code' => '',
|
||
'station_name' => $station,
|
||
'pay_fee' => $fen,
|
||
'pay_time' => date('Y-m-d H:i:s'),
|
||
'consume_amount' => $fen,
|
||
'litter' => isset($opt['litter']) ? (string) $opt['litter'] : '',
|
||
'oil_name' => isset($opt['oil_name']) ? (string) $opt['oil_name'] : '',
|
||
'oil_price' => '',
|
||
'actual_oil_price' => '',
|
||
'scene' => 'oil',
|
||
];
|
||
$params = json_encode(['order_list' => [$item]], JSON_UNESCAPED_UNICODE);
|
||
|
||
echo "回调地址:{$url}\n";
|
||
if ($order) {
|
||
printf("订单:order_id=%s 油卡号=%s 金额=%s 已加油=%s 剩余=%s 加油次数=%s\n",
|
||
$order['order_id'], $order['oilCardNo'], $order['total_price'], $order['used_amount'],
|
||
number_format($remain, 2), $order['used_num']);
|
||
}
|
||
echo "本次加油:{$amount} 元({$fen} 分) 消费单号(去重键):{$item['order_code']}\n";
|
||
echo "params:{$params}\n\n";
|
||
|
||
if ($dryRun) {
|
||
echo "--dry-run:未发送\n";
|
||
exit;
|
||
}
|
||
|
||
// ---------------- 发送:multipart/form-data,params 放表单字段 ----------------
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_POST => true,
|
||
// 传数组 => cURL 自动用 multipart/form-data 编码,与企油通口径一致
|
||
CURLOPT_POSTFIELDS => ['params' => $params],
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 30,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => false,
|
||
]);
|
||
$resp = curl_exec($ch);
|
||
$http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$err = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
echo "HTTP {$http}\n响应:" . ($resp === false ? "cURL 失败:{$err}" : $resp) . "\n";
|
||
$ok = is_string($resp) && strpos($resp, '"100"') !== false;
|
||
echo $ok ? "接收端已受理(code=100)\n" : "⚠️ 接收端未返回 code=100,请查 runtime/storelog 下当天的 qyt_*.txt\n";
|
||
|
||
// ---------------- 等队列处理并对比结果 ----------------
|
||
if (!$ok || !$pdo || !$order) {
|
||
echo "\n提示:真正的落库与订单累计由 callback:queue 进程完成,请确认该进程在运行。\n";
|
||
exit;
|
||
}
|
||
echo "\n等待 callback:queue 处理";
|
||
$after = null;
|
||
for ($i = 0; $i < 10; $i++) {
|
||
sleep(1);
|
||
echo '.';
|
||
$st = $pdo->prepare('SELECT used_amount, used_num, use_status, refund_status FROM yoshop_partners_order WHERE order_id = ?');
|
||
$st->execute([$order['order_id']]);
|
||
$after = $st->fetch();
|
||
if ($after && (float) $after['used_amount'] > (float) $order['used_amount']) {
|
||
break;
|
||
}
|
||
}
|
||
echo "\n";
|
||
|
||
if ($after && (float) $after['used_amount'] > (float) $order['used_amount']) {
|
||
printf("✅ 已入账:已加油 %s -> %s,加油次数 %s -> %s,加油状态=%s\n",
|
||
$order['used_amount'], $after['used_amount'], $order['used_num'], $after['used_num'],
|
||
(int) $after['use_status'] === 1 ? '已加油' : '未加油');
|
||
$st = $pdo->prepare('SELECT id, order_code, consume_amount, notify_status FROM yoshop_qyt_consume WHERE order_id = ? ORDER BY id DESC LIMIT 3');
|
||
$st->execute([$order['order_id']]);
|
||
echo "最近消费流水:\n";
|
||
foreach ($st->fetchAll() as $r) {
|
||
printf(" #%s %s %s元 通知状态=%s\n", $r['id'], $r['order_code'],
|
||
number_format($r['consume_amount'] / 100, 2), $r['notify_status'] ? '已通知' : '待通知');
|
||
}
|
||
} else {
|
||
echo "⚠️ 10 秒内订单没有变化,可能原因:\n"
|
||
. " 1) callback:queue 进程没在跑(supervisorctl status 看一下)\n"
|
||
. " 2) 该消费单号已存在,被唯一键判为重复推送(换一个 --order-code 再试)\n"
|
||
. " 3) 处理异常,查 runtime/storelog 下当天的 " . date('YmdH') . ".txt\n";
|
||
}
|