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

208 lines
9.3 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.7 运单油费下发是【异步】接口,受理成功不等于发放成功。拿到最终结果有两条路:
// | · 推:3.10 发放结果通知(一个批次一条,整批处理完后企油通主动推给我们)
// | · 拉:7.8 waybillBatch 按 batch_no 查询(兜底,见下)
// |
// | 两条路得出的结论必须完全一致,所以判定与状态跃迁都收敛在本类,
// | QytNotify(推)与 QytStatusQueue(拉)只负责把数据喂进来。
// |
// | 为什么有了推送还要留着拉:
// | 1. 未在企油通后台配置接收地址 -> 根本不推(且不视为失败、不补推)
// | 2. 推送失败重试 5 次后放弃(文档建议此时用 7.8 查)
// | 3. 我们自己的接收端宕机 / 发版重启窗口期
// | 所以轮询保留为安全网,只是间隔放长(见 QytStatusQueue::intervalSql)
// |
// | 并发/幂等:
// | 状态跃迁是条件 UPDATE(... WHERE oil_status_zsy=1),affected=1 才退款/回调,
// | 因此「同一批次重复推送」「推送与轮询同时到达」都只会生效一次。
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\common\service;
use think\facade\Db;
class QytWaybill
{
// 明细级 handle_status(7.8 出参 / 3.10 通知 data.list[].handle_status)
const HANDLE_PENDING = 1;
const HANDLE_SUCCESS = 2;
const HANDLE_FAILED = 3;
// 批次级 status(7.8 出参 data.status / 3.10 通知 data.status)
const BATCH_PENDING = 1; // 待处理
const BATCH_RUNNING = 2; // 处理中
const BATCH_ALL_OK = 3; // 全部成功
const BATCH_PART_OK = 4; // 部分成功
const BATCH_FAILED = 5; // 处理失败
// judge() 的结论
const R_SUCCESS = 'success';
const R_FAILED = 'failed';
const R_PENDING = 'pending';
/**
* 纯函数:根据批次级 status 与本条明细,判定这一单的最终发放结果。
*
* 文档 3.10 明确:**以批次级 status 为准**——批次因「企业信息不存在」之类
* 整体失败时,明细里可能仍然写着「待处理」。只看 handle_status 会让这种单
* 永远停在发放中,钱也永远不退。
*
* @param int $batchStatus 批次级 status
* @param array|null $row 本条明细 ['handle_status','error_msg','remark'],不存在传 null
* @return array ['result' => success|failed|pending, 'text' => string]
*/
public static function judge(int $batchStatus, ?array $row): array
{
$handle = $row ? (int) ($row['handle_status'] ?? 0) : 0;
// 1) 整批处理失败:不管明细写的是什么,这一单都拿不到钱
if ($batchStatus === self::BATCH_FAILED && $handle !== self::HANDLE_SUCCESS) {
$msg = $row ? trim((string) ($row['error_msg'] ?? '')) : '';
return ['result' => self::R_FAILED, 'text' => $msg !== '' ? $msg : '企油通返回整批处理失败'];
}
// 2) 明细里没有这一条:批次还在排队,继续等
if (!$row) {
return ['result' => self::R_PENDING, 'text' => ''];
}
if ($handle === self::HANDLE_SUCCESS) {
return ['result' => self::R_SUCCESS, 'text' => trim((string) ($row['remark'] ?? '')) ?: '发放成功'];
}
if ($handle === self::HANDLE_FAILED) {
return ['result' => self::R_FAILED, 'text' => trim((string) ($row['error_msg'] ?? '')) ?: '企油通发放失败'];
}
// 1 待处理,或未知值 -> 继续等
return ['result' => self::R_PENDING, 'text' => ''];
}
/**
* 从 3.10 通知或 7.8 响应的 data.list 里挑出某条运单流水
* @return array|null ['handle_status','error_msg','remark']
*/
public static function pick(array $data, string $waybillNoCode): ?array
{
return QytOil::pickWaybill($data, $waybillNoCode);
}
/**
* 发放成功:条件跃迁 1 -> 2,胜出者才回调下游 / 同步小程序端
*
* @param array $order partners_order 记录
* @param string $text 备注(上游 remark)
* @param string $from 结果来源,只用于日志:'3.10 通知' / '7.8 轮询'
* @return bool 是否由本次调用完成跃迁
*/
public static function markSuccess(array $order, string $text, string $from = ''): bool
{
$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' => mb_substr($text, 0, 190),
'update_time' => time(),
]);
if (!$affected) {
return false;
}
MallOil::syncGoods($order, 'share_oil');
(new QueueService())->push('notify_queue', json_encode([
'order_id' => $order['order_id'],
'type' => 'add',
'optType' => 'share_oil',
'retry_count' => 0,
], 320));
self::log("{$from} 确认发放成功 order={$order['order_id']} batch_no={$order['batch_no']}");
return true;
}
/**
* 发放失败:条件跃迁 1 -> 3,胜出者才退款(保证只退一次)
*
* @return bool 是否由本次调用完成跃迁
*/
public static function markFailed(array $order, string $reason, string $from = ''): bool
{
$affected = Db::name('partners_order')
->where('order_id', $order['order_id'])
->where('oil_status_zsy', 1)
->update([
'oil_status_zsy' => 3,
'oil_text' => mb_substr('发放失败:' . $reason, 0, 190),
'update_time' => time(),
]);
if (!$affected) {
return false;
}
// 原路退款:合作商单回补合作商余额,商城单退回下单用户余额
MallOil::giveBack($order, (float) $order['total_price'], '发放失败');
MallOil::syncGoods($order, 'share_oil');
self::log("{$from} 确认发放失败 order={$order['order_id']} 原因={$reason},已原路退款 {$order['total_price']}");
return true;
}
/**
* 按判定结果落状态。已经不在「发放中」的单会被安全忽略,
* 但「已判失败又被告知成功」属于资损风险,必须告警而不是静默翻转。
*
* @return string 实际发生的动作:success / failed / pending / skipped / conflict
*/
public static function apply(array $order, array $judged, string $from = ''): string
{
$current = (int) $order['oil_status_zsy'];
if ($judged['result'] === self::R_PENDING) {
return self::R_PENDING;
}
if ($current !== 1) {
// 终态与上游结论不一致 -> 一律只告警、不自动翻转:
// 3 -> 成功:我们已经退过款,钱却可能真发出去了(资损)
// 2 -> 失败:我们已经通知下游发放成功、小程序端也显示可加油了
// 两种都要人工到企油通后台核对,自动改状态只会把问题放大。
$conflict = ($current === 3 && $judged['result'] === self::R_SUCCESS)
|| ($current === 2 && $judged['result'] === self::R_FAILED);
if ($conflict) {
self::log("【告警】order={$order['order_id']} batch_no={$order['batch_no']} 本地发放态为「"
. ($current === 3 ? '发放失败(已退款)' : '发放成功(已通知下游)')
. "」,但{$from}返回「" . ($judged['result'] === self::R_SUCCESS ? '发放成功' : '发放失败')
. "」:{$judged['text']}。请人工到企油通后台核对,系统不会自动翻转状态。");
return 'conflict';
}
return 'skipped';
}
if ($judged['result'] === self::R_SUCCESS) {
return self::markSuccess($order, $judged['text'], $from) ? self::R_SUCCESS : 'skipped';
}
return self::markFailed($order, $judged['text'], $from) ? self::R_FAILED : 'skipped';
}
/**
* 记录本次通知时间与次数,便于排查「这单到底推没推过、推了几次」。
* 纯留痕,失败不能影响发放态跃迁(比如迁移脚本还没跑、少了这两列)。
*/
public static function touchNotify(int $orderId, int $notifyTime): void
{
try {
Db::name('partners_order')->where('order_id', $orderId)->update([
'qyt_notify_time' => $notifyTime ?: time(),
'qyt_notify_num' => Db::raw('qyt_notify_num + 1'),
]);
} catch (\Throwable $e) {
self::log('3.10 通知留痕失败 order=' . $orderId . ':' . $e->getMessage()
. '(不影响发放态,确认 qyt_migration_waybill_notify.sql 是否已执行)');
}
}
protected static function log(string $text): void
{
(new QytOil())->saveLog($text);
}
}