Files
oil/server-tp6/deploy/qyt_waybill_push.php
T
vipnub fafd3e2c16 1、合作商余额查询
2、分油账户司机可用额度
2026-10-20 23:21:10 +08:00

334 lines
15 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
// +----------------------------------------------------------------------
// | 模拟企油通 3.10「发放结果通知」 —— 联调 / 补推用(CLI)
// +----------------------------------------------------------------------
// | 按企油通的真实口径推送:multipart/form-data,业务参数放在表单字段 data 里,
// | 另带一个 sign 字段(我方暂未验签,填什么都行)。
// |
// | 用法(在站点根目录执行):
// | php deploy/qyt_waybill_push.php --fake # 【通链路自检】不需要任何订单
// | php deploy/qyt_waybill_push.php --order=6 # 该单发放成功
// | php deploy/qyt_waybill_push.php --order=6 --result=failed # 该单发放失败(会触发原路退款)
// | php deploy/qyt_waybill_push.php --order=6 --batch-status=5 # 整批处理失败(明细仍写待处理)
// | php deploy/qyt_waybill_push.php --batch=HMB2026... # 按批次号推,批次内所有单一起推
// | php deploy/qyt_waybill_push.php --order=6 --dry-run # 只打印报文不发送
// |
// | 常用参数:
// | --fake 自检模式:造一条不存在的运单推过去,只验「入口 -> 解析 ->
// | 队列 -> 消费者」这条链路通不通,不碰数据库、不改任何订单。
// | 预期结果是「未匹配=1」—— 那正是自检通过的标志。
// | --order=订单ID 单推一条(推荐)
// | --batch=批次号 按 batch_no 把该批次的单全推一遍
// | --result=success|failed|pending 明细级 handle_status,默认 success
// | --batch-status=3|4|5 批次级 status,默认按 result 推导(成功=3 失败=5)
// | --msg=文案 成功时写 remark,失败时写 error_msg
// | --url=通知地址 默认取 .env 的 QYT.WAYBILL_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';
}
}
$fake = !empty($opt['fake']);
$orderId = isset($opt['order']) ? (int) $opt['order'] : 0;
$batchNo = isset($opt['batch']) ? trim((string) $opt['batch']) : '';
$dryRun = !empty($opt['dry_run']);
$result = isset($opt['result']) ? strtolower(trim((string) $opt['result'])) : 'success';
$msg = isset($opt['msg']) ? (string) $opt['msg'] : '';
if (!$fake && $orderId <= 0 && $batchNo === '') {
exit("用法:php deploy/qyt_waybill_push.php --fake # 不需要订单,自检链路\n"
. " php deploy/qyt_waybill_push.php --order=<订单ID> [--result=success|failed]\n"
. " php deploy/qyt_waybill_push.php --batch=<批次号>\n"
. "详细参数见本文件头部注释。\n");
}
if (!in_array($result, ['success', 'failed', 'pending'], true)) {
exit("--result 只能是 success / failed / pending\n");
}
// 明细级 handle_status:1 待处理 / 2 成功 / 3 失败
$handleStatus = ['success' => 2, 'failed' => 3, 'pending' => 1][$result];
// 批次级 status:3 全部成功 / 4 部分成功 / 5 处理失败
$batchStatus = isset($opt['batch_status'])
? (int) $opt['batch_status']
: ($result === 'success' ? 3 : ($result === 'failed' ? 5 : 2));
// ---------------- 读取 .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['WAYBILL_NOTIFY_URL'] ?? ''));
if ($url === '') {
$url = 'http://127.0.0.1/qytwaybill.php';
}
// ---------------- 取订单(自检模式跳过) ----------------
$pdo = null;
$orders = [];
if ($fake) {
// 造一条绝不会命中任何订单的运单流水号:
// 服务端应当「收下 -> 入队 -> 消费者判为未匹配」,整条链路都走到,但不动任何数据
$batchNo === '' && $batchNo = 'SELFCHECK' . date('YmdHis');
$fakeCode = 'SELFCHECKCODE' . date('YmdHis') . mt_rand(100, 999);
} else {
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) {
exit("连不上数据库:{$e->getMessage()}\n(本脚本需要读订单的 batch_no / waybill_no_code)\n");
}
if ($orderId > 0) {
$st = $pdo->prepare('SELECT * FROM yoshop_partners_order WHERE order_id = ? LIMIT 1');
$st->execute([$orderId]);
$orders = array_filter([$st->fetch() ?: null]);
if (!$orders) {
exit("未找到订单 order_id={$orderId}\n");
}
} else {
$st = $pdo->prepare('SELECT * FROM yoshop_partners_order WHERE batch_no = ? ORDER BY order_id');
$st->execute([$batchNo]);
$orders = $st->fetchAll();
if (!$orders) {
exit("未找到批次 batch_no={$batchNo} 下的订单\n");
}
}
}
// ---------------- 组装报文(字段与 3.10 文档一致) ----------------
$list = [];
$now = date('Y-m-d H:i:s');
$i = 0;
if ($fake) {
$list[] = [
'id' => 1,
'batch_no' => $batchNo,
'waybill_no' => 'SELFCHECKWB' . date('YmdHis'),
'waybill_no_code' => $fakeCode,
'mobile' => '13800000000',
'id_card' => '110101199001011234',
'name' => '自检',
'quota_amount' => 1,
'voucher_name' => '运单卡',
'effective_start' => '',
'effective_end' => '',
'handle_status' => $handleStatus,
'remark' => '链路自检,不对应任何真实订单',
'error_msg' => '',
'create_time' => $now,
'update_time' => $now,
];
}
foreach ($orders as $o) {
if ((string) $o['waybill_no_code'] === '') {
echo "⚠️ 跳过 order_id={$o['order_id']}:还没有运单流水号,说明 7.7 还没成功受理\n";
continue;
}
$batchNo === '' && $batchNo = (string) $o['batch_no'];
$list[] = [
'id' => ++$i,
'batch_no' => (string) $o['batch_no'],
'waybill_no' => (string) $o['waybill_no'],
'waybill_no_code' => (string) $o['waybill_no_code'],
'mobile' => (string) $o['mobile'],
'id_card' => (string) $o['id_card'],
'name' => (string) $o['driver_name'],
'quota_amount' => (int) round((float) $o['total_price'] * 100),
'voucher_name' => '运单卡',
'effective_start' => '',
'effective_end' => '',
'handle_status' => $handleStatus,
'remark' => $result === 'success' ? ($msg !== '' ? $msg : '发放成功') : '',
'error_msg' => $result === 'failed' ? ($msg !== '' ? $msg : '模拟发放失败') : '',
'create_time' => $now,
'update_time' => $now,
];
}
if (!$list) {
exit("没有可推送的明细\n");
}
$statusText = [3 => '全部成功', 4 => '部分成功', 5 => '处理失败'][$batchStatus] ?? '处理中';
$data = [
'appId' => (string) ($qyt['APP_ID'] ?? 'SIMAPPID'),
'scene' => 'oil',
'business_license' => '',
'batch_no' => $batchNo,
'passageway' => ((string) ($orders[0]['passageway'] ?? 'alipay')) === 'pingan' ? 2 : 1, // 自检模式下固定 1
'status' => $batchStatus,
'status_text' => $statusText,
'remark' => '模拟推送',
'notify_time' => $now,
'list' => $list,
];
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
echo "通知地址:{$url}\n";
echo "批次:{$batchNo} 批次状态:{$batchStatus}({$statusText}) 明细:" . count($list) . " 条\n";
if ($fake) {
echo "自检模式:运单流水号 {$fakeCode}(刻意不对应任何订单,不会改动任何数据)\n";
}
foreach ($orders as $o) {
printf(" order_id=%s 油卡号=%s 金额=%s 当前发放态=%s\n",
$o['order_id'], $o['oilCardNo'], $o['total_price'], $o['oil_status_zsy']);
}
echo "data:" . mb_substr($json, 0, 1500) . (mb_strlen($json) > 1500 ? ' …(截断)' : '') . "\n\n";
if ($dryRun) {
echo "--dry-run:未发送\n";
exit;
}
// ---------------- 发送:multipart/form-data,data + sign ----------------
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
// 传数组 => cURL 自动用 multipart/form-data 编码,与企油通口径一致
CURLOPT_POSTFIELDS => ['data' => $json, 'sign' => 'SIMULATED'],
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";
// 3.10 的成功判据就是 HTTP 2xx,响应体不做要求
$ok = $http >= 200 && $http < 300;
echo $ok ? "接收端已受理(HTTP 2xx)\n" : "⚠️ 接收端未返回 2xx,企油通会重试最多 5 次;请查 runtime/storelog 下当天的 qyt_*.txt\n";
if (!$ok) {
exit;
}
// ---------------- 自检模式:靠日志验证整条链路 ----------------
if ($fake) {
// 日志目录:接收端与队列进程现在统一写 runtime/storelog(见 app/common.php 的
// store_log_path)。为了兼容「代码还没全量更新」和历史日志,这里把多应用模式下的
// 旧目录 runtime/{api,store}/storelog 也一并扫上。
$sub = date('Ym') . '/' . date('Ymd');
$dirs = [
$root . '/runtime/storelog/' . $sub,
$root . '/runtime/api/storelog/' . $sub,
$root . '/runtime/store/storelog/' . $sub,
];
$grep = static function (array $dirs, string $pattern, string $needle): bool {
foreach ($dirs as $dir) {
foreach ((array) glob($dir . '/' . $pattern) as $f) {
if (strpos((string) @file_get_contents($f), $needle) !== false) {
return true;
}
}
}
return false;
};
echo "\n等待 callback:queue 处理";
$received = false;
$consumed = false;
for ($n = 0; $n < 10; $n++) {
sleep(1);
echo '.';
$received = $received || $grep($dirs, 'qyt_*.txt', $batchNo);
$consumed = $grep($dirs, '[0-9]*.txt', '3.10 发放结果通知处理完成 batch_no=' . $batchNo);
if ($consumed) {
break;
}
}
echo "\n\n自检结果:\n";
printf(" %s 1/3 入口可达,返回 HTTP %d\n", '✅', $http);
printf(" %s 2/3 接收端解析到报文(%s 里出现了 batch_no)\n",
$received ? '✅' : '❌', $dirs[0] . '/qyt_*.txt');
printf(" %s 3/3 callback:queue 消费并完成判定\n", $consumed ? '✅' : '❌');
// 3/3 过了就说明整条链路都走到了(接收端日志受「详细日志」开关控制,没有不代表没收到)
if ($consumed) {
echo "\n链路通了。日志里那条「未匹配=1」是预期结果——自检用的运单号本来就不对应任何订单,\n"
. "说明「入口 -> 表单解析 -> 入队 -> 消费者取出 -> 逐条判定」每一步都走到了。\n"
. "真实通知到达时,这一条会变成「成功=1」或「失败=1」。\n";
if (!$received) {
echo "\n注:2/3 没打钩不影响功能,常见原因是「详细日志」开关关着\n"
. " (后台 ->「企油通对接设置」->「详细日志」)。联调期建议打开,\n"
. " 出问题时能看到上游推过来的原文:\n"
. " tail -n 50 {$dirs[0]}/qyt_*.txt\n";
}
exit;
}
echo "\n排查方向(按顺序排查):\n";
if (!$received) {
echo " 1) 请求可能根本没打到本站点:确认 --url 指向的就是这台机器,\n"
. " 且 nginx 能访问到 public/qytwaybill.php(伪静态别把它拦掉)。\n"
. " 先把「详细日志」打开再跑一次,能看到原文就说明收到了。\n";
}
echo " 2) callback:queue 进程多半没在跑,确认一下:\n"
. " /www/server/panel/pyenv/bin/supervisorctl status | grep callback\n"
. " 也可以先前台手动跑一个看报错:php think callback:queue\n";
echo " 3) Redis 不通的话入队就失败了,接收端日志里会有异常:redis-cli ping\n";
exit;
}
// ---------------- 等队列处理并对比结果 ----------------
echo "\n等待 callback:queue 处理";
$ids = array_column($orders, 'order_id');
$in = implode(',', array_map('intval', $ids));
$before = [];
foreach ($orders as $o) {
$before[$o['order_id']] = (int) $o['oil_status_zsy'];
}
$changed = false;
for ($n = 0; $n < 10; $n++) {
sleep(1);
echo '.';
$rows = $pdo->query("SELECT order_id, oil_status, oil_status_zsy, oil_text FROM yoshop_partners_order WHERE order_id IN ({$in})")->fetchAll();
$changed = true;
foreach ($rows as $r) {
if ((int) $r['oil_status_zsy'] === $before[$r['order_id']]) {
$changed = false;
}
}
if ($changed) {
break;
}
}
echo "\n";
$textOf = [0 => '未提交', 1 => '发放中', 2 => '发放成功', 3 => '发放失败'];
$rows = $pdo->query("SELECT order_id, oil_status, oil_status_zsy, oil_text FROM yoshop_partners_order WHERE order_id IN ({$in})")->fetchAll();
foreach ($rows as $r) {
printf("%s order_id=%s 发放态 %s -> %s %s\n",
(int) $r['oil_status_zsy'] === $before[$r['order_id']] ? '⚠️' : '✅',
$r['order_id'],
$textOf[$before[$r['order_id']]] ?? $before[$r['order_id']],
$textOf[(int) $r['oil_status_zsy']] ?? $r['oil_status_zsy'],
$r['oil_text']);
}
if (!$changed) {
echo "\n可能原因:\n"
. " 1) callback:queue 进程没在跑(supervisorctl status 看一下)\n"
. " 2) 订单当前不是「发放中」,已经是终态了(状态跃迁只从 1 出发,这是幂等设计)\n"
. " 3) 处理异常,查 runtime/storelog 下当天的 qyt_" . date('YmdH') . ".txt\n";
}