feat(split): 新增分账订单线下结案功能 - 添加 SplitCloseService 类用于处理订单结案逻辑 - 实现订单结案状态检查和标记功能 - 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试 - 更新分账手动服务,添加结案操作类型 - 扩展钱包结算类,增加结案相关诊断方法 - 修改测试脚本,支持批量标记订单为已结案状态 - 添加数据库升级脚本,增加结案相关字段和菜单权限 ```
649 行
27 KiB
PHP
649 行
27 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro_huifu\library;
|
||
|
||
use addons\shopro\library\split\SplitCalculator;
|
||
use addons\shopro\library\split\SplitCloseService;
|
||
use addons\shopro\library\split\SplitManualService;
|
||
use addons\shopro\library\split\SplitWalletSettlement;
|
||
use addons\shopro\library\split\SplitOrderHelper;
|
||
use addons\shopro\library\split\Config as SplitConfig;
|
||
use addons\shopro\model\Order;
|
||
use addons\shopro\model\split\SplitOrder;
|
||
use addons\shopro\model\split\SplitOrderReceiver;
|
||
use think\Db;
|
||
|
||
class HuifuSplitService
|
||
{
|
||
protected $client;
|
||
protected $config;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->config = HuifuConfig::getInstance();
|
||
$this->client = new HuifuClient();
|
||
}
|
||
|
||
public function executeByOrderId($orderId)
|
||
{
|
||
$order = Order::get($orderId);
|
||
if (!$order || $order['pay_type'] !== 'huifu_wechat') {
|
||
return false;
|
||
}
|
||
|
||
if ((new SplitCloseService())->isClosed($orderId)) {
|
||
\think\Log::info('[shopro_huifu] split skipped order_id=' . $orderId . ' reason=closed_offline');
|
||
return false;
|
||
}
|
||
|
||
$retryBlock = SplitWalletSettlement::getHuifuRetryBlockReason($orderId);
|
||
if ($retryBlock !== null) {
|
||
throw new \RuntimeException($retryBlock);
|
||
}
|
||
|
||
$done = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->where('status', 2)->find();
|
||
if ($done) {
|
||
return true;
|
||
}
|
||
SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->where('status', 'in', [1, 4])->delete();
|
||
|
||
$manualService = new SplitManualService();
|
||
$isManual = $manualService->hasOverride($orderId);
|
||
|
||
$calculator = new SplitCalculator();
|
||
$items = $calculator->calculate($orderId);
|
||
if (empty($items)) {
|
||
$rewardCount = \addons\shopro\model\commission\Reward::where('order_id', $orderId)->where('status', '>=', 0)->count();
|
||
$regionalCount = \addons\shopro\model\regional_agent\Reward::where('order_id', $orderId)->where('status', '>=', 0)->count();
|
||
if ($rewardCount + $regionalCount > 0) {
|
||
\think\Log::warning('[shopro_huifu] split skipped order_id=' . $orderId . ' reason=receiver_missing_or_no_huifu_id');
|
||
}
|
||
$msg = $calculator->formatEmptySplitFailureMessage($orderId);
|
||
$this->markFailed($orderId, 'HFPS' . date('YmdHis') . $orderId, $order, [], $msg);
|
||
$manualService->finishPendingRetryLog($orderId, false, $msg);
|
||
return false;
|
||
}
|
||
|
||
$payFee = number_format((float) $order['pay_fee'], 2, '.', '');
|
||
$confirmBase = $this->resolveConfirmPayFee($order['order_sn'], $payFee);
|
||
if (bccomp($confirmBase, $payFee, 2) !== 0) {
|
||
\think\Log::info('[shopro_huifu] split confirm base: order_id=' . $orderId
|
||
. ' pay_fee=' . $payFee . ' confirm_base=' . $confirmBase);
|
||
}
|
||
$allocated = '0.00';
|
||
foreach ($items as $item) {
|
||
$allocated = bcadd($allocated, number_format((float) $item['amount'], 2, '.', ''), 2);
|
||
}
|
||
if (bccomp($allocated, $confirmBase, 2) > 0) {
|
||
$items = $this->scaleSplitItemsToCap($items, $allocated, $confirmBase);
|
||
}
|
||
$acctInfos = $this->buildAcctInfos($items, $confirmBase);
|
||
$splitAmount = '0.00';
|
||
foreach ($items as $item) {
|
||
$splitAmount = bcadd($splitAmount, number_format((float) $item['amount'], 2, '.', ''), 2);
|
||
}
|
||
|
||
$outOrderNo = 'HFPS' . date('YmdHis') . $orderId;
|
||
$ext = HuifuPayService::getPayExt($orderId);
|
||
$blockReason = $this->getDelayTransBlockReason($order['order_sn'], $orderId);
|
||
if ($blockReason !== null) {
|
||
$this->markFailed($orderId, $outOrderNo, $order, $items, $blockReason);
|
||
$manualService->finishPendingRetryLog($orderId, false, $blockReason);
|
||
return false;
|
||
}
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$splitOrder = SplitOrder::create([
|
||
'order_id' => $orderId,
|
||
'order_sn' => $order['order_sn'],
|
||
'channel' => 'huifu',
|
||
'transaction_id' => $order['transaction_id'],
|
||
'out_order_no' => $outOrderNo,
|
||
'total_amount' => $order['pay_fee'],
|
||
'split_amount' => $splitAmount,
|
||
'status' => 1,
|
||
'split_source' => $isManual ? 'manual' : 'auto',
|
||
]);
|
||
|
||
foreach ($items as $item) {
|
||
SplitOrderReceiver::create([
|
||
'split_order_id' => $splitOrder->id,
|
||
'receiver_id' => $item['receiver_id'],
|
||
'role_type' => $item['role_type'],
|
||
'amount' => $item['amount'],
|
||
'description' => $item['description'],
|
||
'status' => 0,
|
||
]);
|
||
}
|
||
|
||
$confirmResult = $this->confirm($ext, $acctInfos, $outOrderNo, $confirmBase);
|
||
$finalConfirmSeq = $confirmResult['hf_seq_id'] ?? ($confirmResult['confirm_hf_seq_id'] ?? '');
|
||
try {
|
||
$remainderResult = $this->releaseCollectorRemainderIfNeeded($orderId, $order['order_sn'], $confirmBase, $acctInfos);
|
||
if ($remainderResult !== null) {
|
||
$confirmResult['collector_remainder_confirm'] = $remainderResult;
|
||
$seq = $remainderResult['hf_seq_id'] ?? '';
|
||
if ($seq !== '') {
|
||
$finalConfirmSeq = $seq;
|
||
}
|
||
}
|
||
} catch (\Exception $remainderEx) {
|
||
\think\Log::error('[shopro_huifu] collector remainder confirm failed order_id=' . $orderId
|
||
. ' msg=' . $remainderEx->getMessage());
|
||
$confirmResult['collector_remainder_error'] = $remainderEx->getMessage();
|
||
}
|
||
|
||
$splitOrder->status = 2;
|
||
$splitOrder->confirm_hf_seq_id = $finalConfirmSeq;
|
||
$splitOrder->hf_req_seq_id = $outOrderNo;
|
||
$splitOrder->wx_result = json_encode($confirmResult, JSON_UNESCAPED_UNICODE);
|
||
$splitOrder->save();
|
||
|
||
SplitOrderReceiver::where('split_order_id', $splitOrder->id)->update(['status' => 1]);
|
||
HuifuPayService::savePayExt($orderId, $order['order_sn'], [
|
||
'confirm_hf_seq_id' => $finalConfirmSeq,
|
||
'confirm_status' => $this->resolveDelayConfirmStatus($order['order_sn']),
|
||
]);
|
||
Db::commit();
|
||
$manualService->finishPendingRetryLog($orderId, true, '', $splitOrder->id);
|
||
return true;
|
||
} catch (\Exception $e) {
|
||
Db::rollback();
|
||
$this->markFailed($orderId, $outOrderNo, $order, $items, $e->getMessage());
|
||
$manualService->finishPendingRetryLog($orderId, false, $e->getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public function retry($splitOrderId)
|
||
{
|
||
$splitOrder = SplitOrder::where('id', $splitOrderId)->find();
|
||
if (!$splitOrder || $splitOrder['channel'] !== 'huifu') {
|
||
return false;
|
||
}
|
||
if (intval($splitOrder['status']) === SplitCloseService::STATUS_CLOSED) {
|
||
throw new \RuntimeException('该订单已标记线下结案,不可重试');
|
||
}
|
||
if ((new SplitCloseService())->isClosed((int) $splitOrder['order_id'])) {
|
||
throw new \RuntimeException('该订单已标记线下结案,不可重试');
|
||
}
|
||
$orderId = (int) $splitOrder['order_id'];
|
||
$ok = $this->executeByOrderId($orderId);
|
||
if ($ok) {
|
||
$event = (new SplitConfig())->getTiming();
|
||
SplitWalletSettlement::finalizeAfterSplitAttempt($orderId, $event, true);
|
||
}
|
||
return $ok;
|
||
}
|
||
|
||
public function retryByOrderId($orderId)
|
||
{
|
||
$orderId = (int) $orderId;
|
||
if ($orderId <= 0) {
|
||
return false;
|
||
}
|
||
$latest = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->order('id', 'desc')->find();
|
||
if (!$latest) {
|
||
return $this->executeByOrderId($orderId);
|
||
}
|
||
return $this->retry((int) $latest['id']);
|
||
}
|
||
|
||
/**
|
||
* 预览延时分账 confirm 请求体(调试 22000002)
|
||
*/
|
||
public function previewDelaytransConfirmExtend($orderId)
|
||
{
|
||
$order = Order::get($orderId);
|
||
if (!$order || $order['pay_type'] !== 'huifu_wechat') {
|
||
return null;
|
||
}
|
||
$calculator = new SplitCalculator();
|
||
$items = $calculator->calculate($orderId);
|
||
if (empty($items)) {
|
||
return ['error' => '分账明细为空'];
|
||
}
|
||
$payFee = number_format((float) $order['pay_fee'], 2, '.', '');
|
||
$confirmBase = $this->resolveConfirmPayFee($order['order_sn'], $payFee);
|
||
$allocated = '0.00';
|
||
foreach ($items as $item) {
|
||
$allocated = bcadd($allocated, number_format((float) $item['amount'], 2, '.', ''), 2);
|
||
}
|
||
if (bccomp($allocated, $confirmBase, 2) > 0) {
|
||
$items = $this->scaleSplitItemsToCap($items, $allocated, $confirmBase);
|
||
}
|
||
$acctInfos = $this->buildAcctInfos($items, $confirmBase);
|
||
$ext = HuifuPayService::getPayExt($orderId);
|
||
$bunch = json_encode(['acct_infos' => $acctInfos], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
return [
|
||
'confirm_base' => $confirmBase,
|
||
'payment_huifu_id' => $this->config->get('huifu_id'),
|
||
'org_req_date' => $ext['req_date'] ?? '',
|
||
'org_req_seq_id' => $ext['req_seq_id'] ?? '',
|
||
'org_hf_seq_id' => $ext['hf_seq_id'] ?? '',
|
||
'acct_split_bunch' => $bunch,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 仅交易确认、不传分账串(用于排查 22000002 是否来自分账接收方配置)
|
||
*/
|
||
public function executeConfirmOnlyByOrderId($orderId, $syncPayExt = true)
|
||
{
|
||
$order = Order::get($orderId);
|
||
if (!$order || $order['pay_type'] !== 'huifu_wechat') {
|
||
return false;
|
||
}
|
||
$ext = HuifuPayService::getPayExt($orderId);
|
||
if (!$ext) {
|
||
throw new \RuntimeException('无 shopro_huifu_pay_ext');
|
||
}
|
||
$blockReason = $this->getDelayFundsBlockReason($order['order_sn']);
|
||
if ($blockReason !== null) {
|
||
throw new \RuntimeException($blockReason);
|
||
}
|
||
$outOrderNo = 'HFPC' . date('YmdHis') . $orderId;
|
||
SdkLoader::requireRequest('V2TradePaymentDelaytransConfirmRequest.php');
|
||
$request = new \BsPaySdk\request\V2TradePaymentDelaytransConfirmRequest();
|
||
$request->setReqDate(date('Ymd'));
|
||
$request->setReqSeqId($outOrderNo);
|
||
$request->setHuifuId($this->config->get('huifu_id'));
|
||
$extend = [
|
||
'org_req_date' => $ext['req_date'],
|
||
'org_req_seq_id' => $ext['req_seq_id'],
|
||
'org_hf_seq_id' => $ext['hf_seq_id'] ?? '',
|
||
];
|
||
\think\Log::write('[shopro_huifu] delaytrans confirm-only extend=' . json_encode($extend, JSON_UNESCAPED_UNICODE), 'info');
|
||
$request->setExtendInfo($extend);
|
||
$result = $this->client->postRequest($request);
|
||
$data = $this->client->parseResponse($result);
|
||
if ($syncPayExt) {
|
||
$this->syncPayExtAfterDelayConfirm($orderId, $order['order_sn'], $data);
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 主商户=收款户时,分账 confirm 只确认 acct_infos 部分,尾款需再调一次无分账串的 confirm。
|
||
*
|
||
* @return array|null 汇付返回;无需补确认时 null
|
||
*/
|
||
protected function releaseCollectorRemainderIfNeeded($orderId, $orderSn, $confirmBase, array $acctInfos)
|
||
{
|
||
$paymentHuifuId = trim((string) $this->config->get('huifu_id'));
|
||
$platformHuifuId = trim((string) ((new SplitConfig())->all()['platform_huifu_id'] ?? $paymentHuifuId));
|
||
if ($platformHuifuId === '' || $platformHuifuId !== $paymentHuifuId) {
|
||
return null;
|
||
}
|
||
$confirmBase = number_format((float) $confirmBase, 2, '.', '');
|
||
$splitSum = '0.00';
|
||
foreach ($acctInfos as $info) {
|
||
$splitSum = bcadd($splitSum, number_format((float) ($info['div_amt'] ?? 0), 2, '.', ''), 2);
|
||
}
|
||
$expectedRemain = bcsub($confirmBase, $splitSum, 2);
|
||
if (bccomp($expectedRemain, '0.00', 2) <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
||
$unconfirm = $this->queryFieldRecursive($query, 'unconfirm_amt');
|
||
if (!$this->isPositiveMoney($unconfirm)) {
|
||
return null;
|
||
}
|
||
|
||
\think\Log::info('[shopro_huifu] delaytrans collector remainder confirm order_id=' . $orderId
|
||
. ' expected_remain=' . $expectedRemain . ' unconfirm_amt=' . $unconfirm);
|
||
|
||
return $this->executeConfirmOnlyByOrderId($orderId, false);
|
||
}
|
||
|
||
protected function resolveDelayConfirmStatus($orderSn)
|
||
{
|
||
try {
|
||
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
||
$unconfirm = $this->queryFieldRecursive($query, 'unconfirm_amt');
|
||
if ($this->isPositiveMoney($unconfirm)) {
|
||
return 'P';
|
||
}
|
||
} catch (\Throwable $e) {
|
||
\think\Log::warning('[shopro_huifu] resolveDelayConfirmStatus query failed: ' . $e->getMessage());
|
||
}
|
||
return 'S';
|
||
}
|
||
|
||
protected function syncPayExtAfterDelayConfirm($orderId, $orderSn, array $confirmData)
|
||
{
|
||
$seq = $confirmData['hf_seq_id'] ?? ($confirmData['confirm_hf_seq_id'] ?? '');
|
||
HuifuPayService::savePayExt($orderId, $orderSn, [
|
||
'confirm_hf_seq_id' => $seq,
|
||
'confirm_status' => $this->resolveDelayConfirmStatus($orderSn),
|
||
]);
|
||
}
|
||
|
||
protected function confirm($ext, array $acctInfos, $confirmReqSeqId, $confirmBase = null)
|
||
{
|
||
SdkLoader::requireRequest('V2TradePaymentDelaytransConfirmRequest.php');
|
||
$request = new \BsPaySdk\request\V2TradePaymentDelaytransConfirmRequest();
|
||
$request->setReqDate(date('Ymd'));
|
||
$request->setReqSeqId($confirmReqSeqId);
|
||
$request->setHuifuId($this->config->get('huifu_id'));
|
||
|
||
$bunch = ['acct_infos' => $acctInfos];
|
||
$acctSplitBunch = json_encode($bunch, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
$extend = [
|
||
'org_req_date' => $ext['req_date'],
|
||
'org_req_seq_id' => $ext['req_seq_id'],
|
||
'org_hf_seq_id' => $ext['hf_seq_id'] ?? '',
|
||
'acct_split_bunch' => $acctSplitBunch,
|
||
];
|
||
\think\Log::write('[shopro_huifu] delaytrans confirm extend=' . json_encode($extend, JSON_UNESCAPED_UNICODE), 'info');
|
||
$request->setExtendInfo($extend);
|
||
|
||
$result = $this->client->postRequest($request);
|
||
return $this->client->parseResponse($result);
|
||
}
|
||
|
||
protected function buildAcctInfos(array $items, $payFee)
|
||
{
|
||
$payFee = number_format((float) $payFee, 2, '.', '');
|
||
$paymentHuifuId = trim((string) $this->config->get('huifu_id'));
|
||
$platformHuifuId = trim((string) ((new SplitConfig())->all()['platform_huifu_id'] ?? $paymentHuifuId));
|
||
if ($platformHuifuId === '') {
|
||
$platformHuifuId = $paymentHuifuId;
|
||
}
|
||
|
||
$infos = [];
|
||
$allocated = '0.00';
|
||
foreach ($items as $item) {
|
||
$amt = number_format((float) $item['amount'], 2, '.', '');
|
||
if (bccomp($amt, '0.00', 2) <= 0) {
|
||
continue;
|
||
}
|
||
$receiverHuifuId = trim((string) ($item['receiver_account'] ?? ''));
|
||
if ($receiverHuifuId === '') {
|
||
continue;
|
||
}
|
||
// 汇付要求:分账方勿与收款主商户相同(剩余款留在主商户,勿写入 acct_infos)
|
||
if ($paymentHuifuId !== '' && $receiverHuifuId === $paymentHuifuId) {
|
||
\think\Log::warning('[shopro_huifu] split skip self receiver in acct_infos huifu_id=' . $receiverHuifuId);
|
||
continue;
|
||
}
|
||
$infos[] = [
|
||
'huifu_id' => $receiverHuifuId,
|
||
'div_amt' => $amt,
|
||
];
|
||
$allocated = bcadd($allocated, $amt, 2);
|
||
}
|
||
|
||
$remain = bcsub($payFee, $allocated, 2);
|
||
if (bccomp($remain, '0.00', 2) > 0) {
|
||
$platformIsCollector = ($platformHuifuId === $paymentHuifuId);
|
||
if (!$platformIsCollector) {
|
||
$infos[] = [
|
||
'huifu_id' => $platformHuifuId,
|
||
'div_amt' => $remain,
|
||
];
|
||
} else {
|
||
\think\Log::info('[shopro_huifu] split platform remainder stays on collector: remain=' . $remain
|
||
. ' huifu_id=' . $paymentHuifuId);
|
||
}
|
||
}
|
||
|
||
$sum = '0.00';
|
||
foreach ($infos as $info) {
|
||
$sum = bcadd($sum, $info['div_amt'], 2);
|
||
}
|
||
if (empty($infos)) {
|
||
throw new \RuntimeException('分账明细为空:无有效分账接收方(勿将主商户号作为分账方)');
|
||
}
|
||
if (bccomp($sum, $payFee, 2) > 0) {
|
||
throw new \RuntimeException('分账金额之和不能超过待确认金额');
|
||
}
|
||
return $infos;
|
||
}
|
||
|
||
/**
|
||
* 延时分账确认总额须与汇付「待确认/交易金额」一致;内扣手续费时往往小于商城 pay_fee。
|
||
*/
|
||
public function previewConfirmPayFee($orderSn, $orderPayFee)
|
||
{
|
||
return $this->resolveConfirmPayFee($orderSn, $orderPayFee);
|
||
}
|
||
|
||
/**
|
||
* 延时分账 confirm 前置拦截(非延迟入账 / 微信 FREEZE)
|
||
*
|
||
* @return string|null 不可 confirm 时返回说明文案,否则 null
|
||
*/
|
||
protected function getDelayTransBlockReason($orderSn, $orderId)
|
||
{
|
||
$nonDelay = $this->getNonDelayAcctBlockReason($orderSn, $orderId);
|
||
if ($nonDelay !== null) {
|
||
return $nonDelay;
|
||
}
|
||
return $this->getDelayFundsBlockReason($orderSn);
|
||
}
|
||
|
||
/**
|
||
* 23000004「交易非延迟入账」:支付时未带 delay_acct_flag=Y,不能调 delaytrans/confirm
|
||
*/
|
||
protected function getNonDelayAcctBlockReason($orderSn, $orderId)
|
||
{
|
||
$ext = HuifuPayService::getPayExt($orderId);
|
||
$local = strtoupper(trim((string) ($ext['delay_acct_flag'] ?? '')));
|
||
if ($local === 'N') {
|
||
return '该笔支付为实时入账(delay_acct_flag=N),不支持 delaytrans/confirm(23000004 交易非延迟入账)。'
|
||
. '请在分账配置开启「延时分账」后重新下单;历史订单无法补打延迟标记,勿反复重试分账。';
|
||
}
|
||
|
||
try {
|
||
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
||
} catch (\Throwable $e) {
|
||
return null;
|
||
}
|
||
$remote = strtoupper(trim((string) ($this->queryFieldRecursive($query, 'delay_acct_flag') ?? '')));
|
||
if ($remote !== '' && $remote !== 'Y') {
|
||
return '汇付查单 delay_acct_flag=' . $remote . '(非延迟入账),不能延时分账确认(23000004)。'
|
||
. ' 请核对下单是否 setDelayAcctFlag(Y)、商户 delay_flag 是否开通。';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 微信小程序 + 延时分账:通道侧资金可能处于 FREEZE,需发货/确认收货或等待解冻后才能 delaytrans/confirm。
|
||
*/
|
||
protected function getDelayFundsBlockReason($orderSn)
|
||
{
|
||
try {
|
||
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
||
} catch (\Throwable $e) {
|
||
return null;
|
||
}
|
||
$freezeStat = strtoupper(trim((string) ($this->queryFieldRecursive($query, 'fund_freeze_stat') ?? '')));
|
||
if ($freezeStat !== 'FREEZE') {
|
||
return null;
|
||
}
|
||
$unfreezeTime = trim((string) ($this->queryFieldRecursive($query, 'unfreeze_time') ?? ''));
|
||
$hint = '汇付查单 fund_freeze_stat=FREEZE:延迟交易资金仍被通道冻结,暂不能交易确认分账。'
|
||
. '小程序场景通常需:后台发货并同步微信 upload_shipping_info,用户在微信确认收货(或等待自动解冻,约 2–3 天)后再分账;'
|
||
. '建议分账时机改为「确认收货」而非「支付成功」。';
|
||
if ($unfreezeTime !== '') {
|
||
$hint .= ' 预计解冻时间(unfreeze_time)=' . $unfreezeTime;
|
||
}
|
||
return $hint;
|
||
}
|
||
|
||
protected function resolveConfirmPayFee($orderSn, $orderPayFee)
|
||
{
|
||
$fallback = number_format((float) $orderPayFee, 2, '.', '');
|
||
try {
|
||
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
||
$delayAcct = strtoupper(trim((string) ($this->queryFieldRecursive($query, 'delay_acct_flag') ?? ''))) === 'Y';
|
||
|
||
$unconfirm = $this->queryFieldRecursive($query, 'unconfirm_amt');
|
||
if ($this->isPositiveMoney($unconfirm)) {
|
||
return number_format((float) $unconfirm, 2, '.', '');
|
||
}
|
||
|
||
$settlement = $this->queryFieldRecursive($query, 'settlement_amt');
|
||
if ($this->isPositiveMoney($settlement) && bccomp((string) $settlement, $fallback, 2) < 0) {
|
||
return number_format((float) $settlement, 2, '.', '');
|
||
}
|
||
|
||
$transAmt = $this->pickTransAmt($query);
|
||
$feeAmt = $this->pickFeeAmt($query);
|
||
if ($transAmt !== null && $feeAmt !== null) {
|
||
$net = bcsub($transAmt, $feeAmt, 2);
|
||
if (bccomp($net, '0.00', 2) > 0 && bccomp($net, $fallback, 2) <= 0) {
|
||
\think\Log::info('[shopro_huifu] split confirm base from trans-fee: trans=' . $transAmt . ' fee=' . $feeAmt . ' net=' . $net);
|
||
return $net;
|
||
}
|
||
}
|
||
|
||
if ($transAmt !== null && !$delayAcct && bccomp($transAmt, '0.00', 2) > 0) {
|
||
return $transAmt;
|
||
}
|
||
|
||
if ($delayAcct && $transAmt !== null && bccomp($transAmt, $fallback, 2) === 0) {
|
||
\think\Log::warning('[shopro_huifu] split confirm base: delay_acct=Y but unconfirm/fee missing, still using pay_fee=' . $fallback
|
||
. ' query_keys=' . implode(',', array_slice(array_keys($query), 0, 20)));
|
||
}
|
||
} catch (\Throwable $e) {
|
||
\think\Log::warning('[shopro_huifu] split confirm amount query fallback: ' . $e->getMessage());
|
||
}
|
||
return $fallback;
|
||
}
|
||
|
||
protected function pickTransAmt(array $query)
|
||
{
|
||
$raw = $this->queryFieldRecursive($query, 'trans_amt');
|
||
if (!$this->isPositiveMoney($raw)) {
|
||
return null;
|
||
}
|
||
return number_format((float) $raw, 2, '.', '');
|
||
}
|
||
|
||
protected function pickFeeAmt(array $query)
|
||
{
|
||
foreach (['actual_fee_amt', 'receivable_fee_amt', 'fee_amt', 'fee_amount', 'trans_fee', 'combinedpay_fee_amt'] as $key) {
|
||
$raw = $this->queryFieldRecursive($query, $key);
|
||
if ($this->isPositiveMoney($raw)) {
|
||
return number_format((float) $raw, 2, '.', '');
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
protected function isPositiveMoney($value)
|
||
{
|
||
if ($value === null || $value === '') {
|
||
return false;
|
||
}
|
||
if (!is_numeric($value)) {
|
||
return false;
|
||
}
|
||
return bccomp(number_format((float) $value, 2, '.', ''), '0.00', 2) > 0;
|
||
}
|
||
|
||
/**
|
||
* @param array|string|mixed $data
|
||
*/
|
||
protected function queryFieldRecursive($data, $key)
|
||
{
|
||
if (is_string($data)) {
|
||
$trim = trim($data);
|
||
if ($trim !== '' && ($trim[0] === '{' || $trim[0] === '[')) {
|
||
$decoded = json_decode($trim, true);
|
||
if (is_array($decoded)) {
|
||
return $this->queryFieldRecursive($decoded, $key);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
if (!is_array($data)) {
|
||
return null;
|
||
}
|
||
if (array_key_exists($key, $data) && $data[$key] !== '' && $data[$key] !== null) {
|
||
return $data[$key];
|
||
}
|
||
foreach ($data as $value) {
|
||
$found = $this->queryFieldRecursive($value, $key);
|
||
if ($found !== null && $found !== '') {
|
||
return $found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
protected function scaleSplitItemsToCap(array $items, $allocated, $cap)
|
||
{
|
||
if (empty($items) || bccomp($allocated, '0.00', 2) <= 0) {
|
||
return $items;
|
||
}
|
||
$cap = number_format((float) $cap, 2, '.', '');
|
||
$allocated = number_format((float) $allocated, 2, '.', '');
|
||
if (bccomp($allocated, $cap, 2) <= 0) {
|
||
return $items;
|
||
}
|
||
$scaled = [];
|
||
$sum = '0.00';
|
||
$count = count($items);
|
||
foreach ($items as $index => $item) {
|
||
if ($index === $count - 1) {
|
||
$item['amount'] = (float) bcsub($cap, $sum, 2);
|
||
} else {
|
||
$ratio = bcdiv($item['amount'], $allocated, 8);
|
||
$part = bcmul($cap, $ratio, 2);
|
||
$item['amount'] = (float) $part;
|
||
$sum = bcadd($sum, $part, 2);
|
||
}
|
||
if ($item['amount'] > 0) {
|
||
$scaled[] = $item;
|
||
}
|
||
}
|
||
return $scaled;
|
||
}
|
||
|
||
public function querySplit($hfSeqId)
|
||
{
|
||
SdkLoader::requireRequest('V2TradeTransSplitQueryRequest.php');
|
||
$request = new \BsPaySdk\request\V2TradeTransSplitQueryRequest();
|
||
$request->setHuifuId($this->config->get('huifu_id'));
|
||
$request->setHfSeqId($hfSeqId);
|
||
$request->setOrdType('consume');
|
||
$result = $this->client->postRequest($request);
|
||
return $this->client->parseResponse($result);
|
||
}
|
||
|
||
protected function markFailed($orderId, $outOrderNo, $order, $items, $message)
|
||
{
|
||
$splitOrder = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->find();
|
||
$splitConfig = new SplitConfig();
|
||
$isManual = (new SplitManualService())->hasOverride($orderId);
|
||
if (!$splitOrder) {
|
||
$splitOrder = SplitOrder::create([
|
||
'order_id' => $orderId,
|
||
'order_sn' => $order['order_sn'],
|
||
'channel' => 'huifu',
|
||
'transaction_id' => $order['transaction_id'] ?? '',
|
||
'out_order_no' => $outOrderNo,
|
||
'total_amount' => $order['pay_fee'],
|
||
'split_amount' => array_sum(array_column($items, 'amount')),
|
||
'status' => 4,
|
||
'split_source' => $isManual ? 'manual' : 'auto',
|
||
'wx_result' => $message,
|
||
'retry_count' => 1,
|
||
'next_retry_time'=> time() + $splitConfig->getRetryInterval(),
|
||
]);
|
||
} else {
|
||
$splitOrder->status = 4;
|
||
$splitOrder->wx_result = $message;
|
||
$splitOrder->retry_count = $splitOrder['retry_count'] + 1;
|
||
$splitOrder->next_retry_time = time() + $splitConfig->getRetryInterval();
|
||
$splitOrder->save();
|
||
}
|
||
SplitOrderHelper::syncReceivers($splitOrder->id, $items);
|
||
}
|
||
}
|