1、合作商余额查询

2、分油账户司机可用额度
This commit is contained in:
2026-10-20 23:21:10 +08:00
parent 45452c1799
commit fafd3e2c16
134 changed files with 2344 additions and 352 deletions
@@ -8,6 +8,8 @@ use think\console\Input;
use think\console\Output;
use app\store\model\order\Record as RecordModel;
use app\common\service\SybOil;
use app\common\service\QytWaybill;
use app\common\lock\RedisLock;
use think\facade\Db;
class CallbackQueue extends Command
@@ -427,6 +429,58 @@ class CallbackQueue extends Command
$SybOil->saveLogStore('企油通消费回调处理成功:' . json_encode($c, 320));
}
break;
case "qyt_waybill_result":
// 企油通 3.10「发放结果通知」:一个批次一条,整批明细都在 res.list 里。
// 判定与状态跃迁跟 7.8 轮询共用 QytWaybill,保证两条路结论一致、互相幂等。
$batchNo = (string)($res['batch_no'] ?? '');
$batchStatus = (int)($res['status'] ?? 0);
$notifyTime = !empty($res['notify_time']) ? (strtotime((string)$res['notify_time']) ?: time()) : time();
$list = (array)($res['list'] ?? []);
$stat = ['success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0, 'conflict' => 0, 'miss' => 0];
foreach ($list as $row) {
$code = trim((string)($row['waybill_no_code'] ?? ''));
if ($code === '') {
$stat['miss']++;
continue;
}
$order = Db::name('partners_order')->where('waybill_no_code', $code)->find();
if (!$order) {
// 同一个 appId 下可能还挂着别的接入系统提交的批次,匹配不到不算异常
$stat['miss']++;
continue;
}
// 留痕:这一单到底收没收到过推送、收了几次
QytWaybill::touchNotify((int)$order['order_id'], $notifyTime);
// 每单串行,避免与 qyt:status 轮询同时跃迁
$lock = new RedisLock('qyt:status:' . $order['order_id'], 60);
if (!$lock->acquireLock()) {
// 轮询正在处理这一单,跳过即可:两条路的结论一样
$stat['skipped']++;
continue;
}
try {
// 锁内重读,拿到最新状态
$fresh = Db::name('partners_order')->where('order_id', $order['order_id'])->find();
$judged = QytWaybill::judge($batchStatus, [
'handle_status' => (int)($row['handle_status'] ?? 0),
'error_msg' => (string)($row['error_msg'] ?? ''),
'remark' => (string)($row['remark'] ?? ''),
]);
$stat[QytWaybill::apply($fresh ?: $order, $judged, '3.10 通知')]++;
} finally {
$lock->releaseLock();
}
}
$SybOil->saveLogStore('3.10 发放结果通知处理完成 batch_no=' . $batchNo
. ' 批次状态=' . $batchStatus . ' 明细=' . count($list)
. ' 成功=' . $stat['success'] . ' 失败=' . $stat['failed']
. ' 待处理=' . $stat['pending'] . ' 已是终态=' . $stat['skipped']
. ' 未匹配=' . $stat['miss']
. ($stat['conflict'] ? ' 【冲突=' . $stat['conflict'] . ',需人工核对】' : ''));
break;
default:
$SybOil->saveLogStore('队列数据处理异常,未知:【'.$message.'】');
}
@@ -1,6 +1,7 @@
<?php
namespace app\console\command;
use app\common\service\MallOil;
use app\common\service\PartnersOil;
use app\common\service\QueueService;
use think\console\Command;
@@ -82,6 +83,17 @@ class NotifyQueue extends Command
if(!$order){
return false;
}
// 小程序(商城)单没有下游下单平台可回调:把状态回写到 yoshop_order_goods,
// 小程序端的订单列表/详情、后台订单页的基础列都读那张表。直接算成功,不重试。
if (MallOil::isMall($order)) {
MallOil::syncGoods($order, (string)($data['optType'] ?? ''));
// 逐笔标记已处理,否则后台「重推加油结果」会一直认为这些流水没通知过
if (!empty($data['consume_id'])) {
Db::name('qyt_consume')->where('id', (int)$data['consume_id'])
->update(['notify_status' => 1, 'notify_time' => time()]);
}
return true;
}
$partners = partnersModel::detail($order['partners_id']);
if(!$partners){
return false;
@@ -22,7 +22,7 @@ namespace app\console\command;
use app\common\lock\RedisLock;
use app\common\model\partners\Order as OrderModel;
use app\common\model\qyt\Enterprise as EnterpriseModel;
use app\common\service\PartnersBalance;
use app\common\service\MallOil;
use app\common\service\QueueService;
use app\common\service\QytOil;
use think\console\Command;
@@ -48,12 +48,13 @@ class PartnersQueue extends Command
{
$this->queueService = new QueueService();
while (true) {
$message = $this->queueService->pop('partners_queue');
// 阻塞式取队列:有单立刻开始下发,不用等轮询间隔。
// 5 秒超时只是为了让空闲时也能跑到下面的兜底巡检(内部已限频 60s)。
$message = $this->queueService->bpop('partners_queue', 5);
if ($message) {
$this->processMessage($message);
} else {
$this->reconcile();
sleep(1);
}
}
}
@@ -198,18 +199,14 @@ class PartnersQueue extends Command
}
/**
* 发放失败 -> 回补合作商余额(仅在状态跃迁胜出时调用,天然幂等)
* 发放失败 -> 原路退款(仅在状态跃迁胜出时调用,天然幂等)
* · 合作商单 -> 回补 partners.balance
* · 商城单 -> 退回下单用户余额(按实付折算),并把失败状态回写 order_goods
*/
protected function refundBalance($order)
{
PartnersBalance::refund(
(int) $order['partners_id'],
(float) $order['total_price'],
(string) $order['partners_order_no'],
(int) $order['order_id'],
(int) $order['store_id']
);
(new QytOil())->saveLog("发放失败已回补合作商余额 order={$order['order_id']} amount={$order['total_price']}");
MallOil::giveBack($order, (float) $order['total_price'], '发放失败');
MallOil::syncGoods($order);
}
/**
@@ -2,8 +2,15 @@
// +----------------------------------------------------------------------
// | 企油通「运单发放结果」轮询 (7.8 waybillBatch)
// +----------------------------------------------------------------------
// | 为什么需要它:7.7 运单油费下发是【异步】接口,返回成功只代表受理,
// | 真实发放结果必须按 batch_no 调 7.8 查(《企油通接口对接文档.md》§5 第 7 条)。
// | 为什么需要它: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 失败
@@ -27,9 +34,9 @@ namespace app\console\command;
use app\common\lock\RedisLock;
use app\common\model\qyt\Enterprise as EnterpriseModel;
use app\common\service\PartnersBalance;
use app\common\service\QueueService;
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;
@@ -40,15 +47,12 @@ class QytStatusQueue extends Command
/** 每轮扫描的最大订单数 */
const BATCH_LIMIT = 100;
/** 空转时的休眠秒数 */
const IDLE_SLEEP = 5;
/** 空转时的休眠秒数(只是一条轻量 SQL,不打上游,可以压到 1 秒) */
const IDLE_SLEEP = 1;
/** 查询次数达到该值后仍未定论,视为异常滞留,写告警日志 */
const STUCK_NUM = 100;
/** @var QueueService */
protected $queueService;
protected function configure()
{
$this->setName('qyt:status')
@@ -57,7 +61,7 @@ class QytStatusQueue extends Command
protected function execute(Input $input, Output $output)
{
$this->queueService = new QueueService();
// 通知下游 / 同步小程序端都在 QytWaybill 里做,这里只负责查结果
while (true) {
$handled = 0;
try {
@@ -94,11 +98,38 @@ class QytStatusQueue extends Command
/**
* 退避间隔(秒)的 SQL 表达式:查得越久间隔越长,避免高频打上游
* 前 10 次 30s,10~30 次 120s,之后 600s
*
* 为什么前几次要这么密: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
{
return 'CASE WHEN qyt_query_num < 10 THEN 30 WHEN qyt_query_num < 30 THEN 120 ELSE 600 END';
// 接上 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';
}
/**
@@ -143,31 +174,16 @@ class QytStatusQueue extends Command
return;
}
// 判定与状态跃迁跟 3.10 推送共用 QytWaybill,两条路结论必须一致
$batchStatus = (int) ($res['data']['status'] ?? 0);
$row = QytOil::pickWaybill($res['data'], (string) $order['waybill_no_code']);
$row = QytWaybill::pick($res['data'], (string) $order['waybill_no_code']);
$judged = QytWaybill::judge($batchStatus, $row);
// 整批处理失败,且明细里没有本条 -> 判失败
if (!$row && $batchStatus === 5) {
$this->markFailed($order, '企油通返回整批处理失败', $qyt);
return;
}
// 明细里还没有本条:批次还在排队,继续等
if (!$row) {
if ($judged['result'] === QytWaybill::R_PENDING) {
$this->alertIfStuck($order, $qyt);
return;
}
switch ($row['handle_status']) {
case 2: // 成功
$this->markSuccess($order, $row['remark'] ?: '发放成功', $qyt);
break;
case 3: // 失败
$this->markFailed($order, $row['error_msg'] ?: '企油通发放失败', $qyt);
break;
default: // 1 待处理,或未知值 -> 继续等
$this->alertIfStuck($order, $qyt);
break;
}
QytWaybill::apply($order, $judged, '7.8 轮询');
} catch (\Throwable $e) {
$this->log("order={$orderId} 轮询异常:" . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
} finally {
@@ -175,60 +191,6 @@ class QytStatusQueue extends Command
}
}
/**
* 发放成功:条件跃迁 1 -> 2,胜出者才回调下单平台
*/
protected function markSuccess(array $order, string $text, QytOil $qyt): void
{
$affected = Db::name('partners_order')
->where('order_id', $order['order_id'])
->where('oil_status_zsy', 1)
->update([
'oil_status' => 1,
'oil_status_zsy' => 2,
'oil_time' => time(),
'oil_text' => $text,
'update_time' => time(),
]);
if (!$affected) {
return;
}
$this->queueService->push('notify_queue', json_encode([
'order_id' => $order['order_id'],
'type' => 'add',
'optType' => 'share_oil',
'retry_count' => 0,
], 320));
$qyt->saveLog("7.8 确认发放成功 order={$order['order_id']} batch_no={$order['batch_no']}"
. " 查询次数={$order['qyt_query_num']}");
}
/**
* 发放失败:条件跃迁 1 -> 3,胜出者才退款(保证只退一次)
*/
protected function markFailed(array $order, string $reason, QytOil $qyt): void
{
$affected = Db::name('partners_order')
->where('order_id', $order['order_id'])
->where('oil_status_zsy', 1)
->update([
'oil_status_zsy' => 3,
'oil_text' => '发放失败:' . $reason,
'update_time' => time(),
]);
if (!$affected) {
return;
}
PartnersBalance::refund(
(int) $order['partners_id'],
(float) $order['total_price'],
(string) $order['partners_order_no'],
(int) $order['order_id'],
(int) $order['store_id']
);
$qyt->saveLog("7.8 确认发放失败 order={$order['order_id']} 原因={$reason},已回补合作商余额 {$order['total_price']}");
}
/**
* 长期拿不到结果时告警。只写日志,不改状态、不退款
*/