221 lines
9.1 KiB
PHP
221 lines
9.1 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | 企油通「运单发放结果」轮询 (7.8 waybillBatch)
|
||
// +----------------------------------------------------------------------
|
||
// | 为什么需要它:7.7 运单油费下发是【异步】接口,返回成功只代表受理。
|
||
// |
|
||
// | 拿最终结果有两条路,判定逻辑共用 app\common\service\QytWaybill:
|
||
// | · 推:3.10 发放结果通知 -> public/qytwaybill.php -> callback:queue(主路径)
|
||
// | · 拉:本命令按 batch_no 调 7.8 查询(安全网)
|
||
// |
|
||
// | 接上推送后本命令不会下线:上游未配置接收地址不推送、推送重试 5 次后放弃、
|
||
// | 我方接收端宕机或发版重启,这三种情况都只能靠轮询兜底。此时退避间隔会自动放长,
|
||
// | 见 intervalSql()。
|
||
// |
|
||
// | 状态流转(oil_status_zsy):
|
||
// | 0 未提交 --PartnersQueue 调7.7受理--> 1 发放中 --本命令查7.8--> 2 成功 / 3 失败
|
||
// |
|
||
// | 7.8 单条 handle_status: 1 待处理 / 2 成功 / 3 失败
|
||
// | 7.8 整批 status: 1 待处理 / 2 处理中 / 3 全部成功 / 4 部分成功 / 5 处理失败
|
||
// |
|
||
// | 并发/原子性:
|
||
// | · 每个 order_id 用 RedisLock 串行化,可多进程运行
|
||
// | · 状态跃迁是条件 UPDATE(... WHERE oil_status_zsy=1),affected=1 才回调/退款,天然幂等
|
||
// |
|
||
// | ⚠️ 绝不因为「查不到结果」而自动判失败退款:
|
||
// | 钱可能已经发到司机手里,自动退款会造成资损。查询失败或长期待处理只会持续
|
||
// | 重试并在日志里告警,由人工介入处理。
|
||
// |
|
||
// | 启动:php think qyt:status
|
||
// +----------------------------------------------------------------------
|
||
declare (strict_types=1);
|
||
|
||
namespace app\console\command;
|
||
|
||
use app\common\lock\RedisLock;
|
||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||
use app\common\service\QytConfig;
|
||
use app\common\service\QytOil;
|
||
use app\common\service\QytWaybill;
|
||
use think\console\Command;
|
||
use think\console\Input;
|
||
use think\console\Output;
|
||
use think\facade\Db;
|
||
|
||
class QytStatusQueue extends Command
|
||
{
|
||
/** 每轮扫描的最大订单数 */
|
||
const BATCH_LIMIT = 100;
|
||
|
||
/** 空转时的休眠秒数(只是一条轻量 SQL,不打上游,可以压到 1 秒) */
|
||
const IDLE_SLEEP = 1;
|
||
|
||
/** 查询次数达到该值后仍未定论,视为异常滞留,写告警日志 */
|
||
const STUCK_NUM = 100;
|
||
|
||
protected function configure()
|
||
{
|
||
$this->setName('qyt:status')
|
||
->setDescription('轮询企油通 7.8 运单发放结果,确定订单最终发放态');
|
||
}
|
||
|
||
protected function execute(Input $input, Output $output)
|
||
{
|
||
// 通知下游 / 同步小程序端都在 QytWaybill 里做,这里只负责查结果
|
||
while (true) {
|
||
$handled = 0;
|
||
try {
|
||
$handled = $this->scan();
|
||
} catch (\Throwable $e) {
|
||
$this->log('轮询主循环异常:' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||
}
|
||
// 有活干就立刻进入下一轮,没有就歇一会儿
|
||
$handled === 0 && sleep(self::IDLE_SLEEP);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 扫描一批「发放中」的订单并查询结果
|
||
* @return int 本轮实际处理的订单数
|
||
*/
|
||
protected function scan(): int
|
||
{
|
||
$now = time();
|
||
// 按退避间隔挑出到点该查的单:qyt_query_time + interval(num) <= now
|
||
$ids = Db::name('partners_order')
|
||
->where('oil_status_zsy', 1)
|
||
->where('order_status', 10)
|
||
->whereRaw('qyt_query_time + ' . $this->intervalSql() . ' <= ' . $now)
|
||
->order('qyt_query_time', 'asc')
|
||
->limit(self::BATCH_LIMIT)
|
||
->column('order_id');
|
||
|
||
foreach ($ids as $orderId) {
|
||
$this->handleOne((int) $orderId);
|
||
}
|
||
return count($ids);
|
||
}
|
||
|
||
/**
|
||
* 退避间隔(秒)的 SQL 表达式:查得越久间隔越长,避免高频打上游
|
||
*
|
||
* 为什么前几次要这么密:7.7 受理后第一次 7.8 几乎必然返回「待处理」,
|
||
* 如果第二次要等 30 秒,哪怕企油通第 8 秒就发完了,用户也得等到第 35 秒。
|
||
* 绝大多数单在前 20 秒内就有结果,所以把这一段压到 3 秒一次,
|
||
* 代价是每单多打上游几次;拖久的单自动退到长间隔,总 QPS 不会失控。
|
||
*
|
||
* 注意:若已接入 3.10 发放结果通知(后台「企油通对接设置」里打开开关),
|
||
* 走的是上面那条「安全网」曲线,不是下面这条抢时间的曲线。
|
||
*
|
||
* 次数 <6 -> 3s (覆盖 0~18s,正常单在这里出结果)
|
||
* 次数 <16 -> 10s (覆盖至约 2 分钟)
|
||
* 次数 <26 -> 30s (覆盖至约 7 分钟)
|
||
* 次数 <46 -> 120s (覆盖至约 47 分钟)
|
||
* 之后 -> 600s
|
||
*/
|
||
protected function intervalSql(): string
|
||
{
|
||
// 接上 3.10 发放结果通知后,结果以推送为主,轮询退为安全网:
|
||
// 只覆盖「上游没配接收地址 / 推送重试 5 次仍失败 / 我方接收端当时不可用」
|
||
// 这三种情况,没必要再抢在推送前面把上游打一遍。
|
||
if (!empty(QytConfig::get()['waybill_notify'])) {
|
||
return 'CASE'
|
||
. ' WHEN qyt_query_num < 3 THEN 60'
|
||
. ' WHEN qyt_query_num < 8 THEN 300'
|
||
. ' ELSE 900 END';
|
||
}
|
||
return 'CASE'
|
||
. ' WHEN qyt_query_num < 6 THEN 3'
|
||
. ' WHEN qyt_query_num < 16 THEN 10'
|
||
. ' WHEN qyt_query_num < 26 THEN 30'
|
||
. ' WHEN qyt_query_num < 46 THEN 120'
|
||
. ' ELSE 600 END';
|
||
}
|
||
|
||
/**
|
||
* 查询单个订单的发放结果
|
||
*/
|
||
protected function handleOne(int $orderId): void
|
||
{
|
||
$lock = new RedisLock('qyt:status:' . $orderId, 60);
|
||
if (!$lock->acquireLock()) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$order = Db::name('partners_order')->where('order_id', $orderId)->find();
|
||
// 锁内重读:可能已被其它进程处理完
|
||
if (!$order || (int) $order['oil_status_zsy'] !== 1) {
|
||
return;
|
||
}
|
||
if (empty($order['batch_no']) || empty($order['waybill_no_code'])) {
|
||
$this->log("order={$orderId} 处于发放中但缺少 batch_no/waybill_no_code,无法查询,需人工核对");
|
||
$this->touch($orderId);
|
||
return;
|
||
}
|
||
|
||
$enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']);
|
||
if (!$enterprise) {
|
||
// 不判失败:单子可能真的已经发出去了,只是企业记录被删了
|
||
$this->log("order={$orderId} 绑定的企业不存在,无法查询 7.8,需人工核对");
|
||
$this->touch($orderId);
|
||
return;
|
||
}
|
||
|
||
$qyt = QytOil::forEnterprise($enterprise);
|
||
$res = $qyt->waybillBatch((string) $order['batch_no']);
|
||
|
||
// 记一次查询(无论成败都要更新时间,否则会死循环打上游)
|
||
$this->touch($orderId);
|
||
|
||
if ($res['code'] !== '00') {
|
||
$this->log("order={$orderId} 7.8 查询失败:{$res['message']}(保持发放中,稍后重试)");
|
||
$this->alertIfStuck($order, $qyt);
|
||
return;
|
||
}
|
||
|
||
// 判定与状态跃迁跟 3.10 推送共用 QytWaybill,两条路结论必须一致
|
||
$batchStatus = (int) ($res['data']['status'] ?? 0);
|
||
$row = QytWaybill::pick($res['data'], (string) $order['waybill_no_code']);
|
||
$judged = QytWaybill::judge($batchStatus, $row);
|
||
|
||
if ($judged['result'] === QytWaybill::R_PENDING) {
|
||
$this->alertIfStuck($order, $qyt);
|
||
return;
|
||
}
|
||
QytWaybill::apply($order, $judged, '7.8 轮询');
|
||
} catch (\Throwable $e) {
|
||
$this->log("order={$orderId} 轮询异常:" . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||
} finally {
|
||
$lock->releaseLock();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 长期拿不到结果时告警。只写日志,不改状态、不退款
|
||
*/
|
||
protected function alertIfStuck(array $order, QytOil $qyt): void
|
||
{
|
||
$num = (int) $order['qyt_query_num'] + 1;
|
||
if ($num < self::STUCK_NUM || $num % 20 !== 0) {
|
||
return;
|
||
}
|
||
$qyt->saveLog("【告警】order={$order['order_id']} batch_no={$order['batch_no']} 已查询 {$num} 次"
|
||
. '仍未拿到 7.8 结果,长期处于「发放中」。不会自动判失败(款可能已发出),请人工到企油通后台核对。');
|
||
}
|
||
|
||
/** 记录一次查询:更新时间与次数,驱动退避 */
|
||
protected function touch(int $orderId): void
|
||
{
|
||
Db::name('partners_order')->where('order_id', $orderId)->update([
|
||
'qyt_query_time' => time(),
|
||
'qyt_query_num' => Db::raw('qyt_query_num + 1'),
|
||
]);
|
||
}
|
||
|
||
protected function log(string $text): void
|
||
{
|
||
(new QytOil())->saveLog('qyt:status ' . $text);
|
||
}
|
||
}
|