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

221 lines
9.1 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
// +----------------------------------------------------------------------
// | 企油通「运单发放结果」轮询 (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);
}
}