feat(split): 优化汇付分账服务逻辑并增强重试功能 - 修改HuifuSplitService中分账状态查询逻辑,从检查状态[1,2]改为仅检查状态2 - 更新删除条件,从删除状态4改为删除状态[1,4] - 在分账明细为空时添加详细的错误提示信息 - 新增retryByOrderId方法支持订单ID重试 - 优化后台控制器中的重试逻辑,添加分账记录存在性验证 - 改进错误提示信息,提供更准确的失败原因说明 ```
294 行
12 KiB
PHP
294 行
12 KiB
PHP
<?php
|
|
|
|
namespace addons\shopro_huifu\library;
|
|
|
|
use addons\shopro\library\split\SplitCalculator;
|
|
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;
|
|
}
|
|
|
|
$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();
|
|
|
|
$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 = '分账明细为空:请检查分账金额来源、汇付接收方(huifu_id)与佣金/规则是否大于0';
|
|
$this->markFailed($orderId, 'HFPS' . date('YmdHis') . $orderId, $order, [], $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);
|
|
|
|
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,
|
|
]);
|
|
|
|
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);
|
|
$splitOrder->status = 2;
|
|
$splitOrder->confirm_hf_seq_id = $confirmResult['hf_seq_id'] ?? ($confirmResult['confirm_hf_seq_id'] ?? '');
|
|
$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' => $splitOrder->confirm_hf_seq_id,
|
|
'confirm_status' => 'S',
|
|
]);
|
|
Db::commit();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
Db::rollback();
|
|
$this->markFailed($orderId, $outOrderNo, $order, $items, $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function retry($splitOrderId)
|
|
{
|
|
$splitOrder = SplitOrder::where('id', $splitOrderId)->find();
|
|
if (!$splitOrder || $splitOrder['channel'] !== 'huifu') {
|
|
return false;
|
|
}
|
|
return $this->executeByOrderId((int) $splitOrder['order_id']);
|
|
}
|
|
|
|
public function retryByOrderId($orderId)
|
|
{
|
|
$orderId = (int) $orderId;
|
|
if ($orderId <= 0) {
|
|
return false;
|
|
}
|
|
return $this->executeByOrderId($orderId);
|
|
}
|
|
|
|
protected function confirm($ext, array $acctInfos, $confirmReqSeqId)
|
|
{
|
|
SdkLoader::requireRequest('V2TradePaymentDelaytransConfirmRequest.php');
|
|
$request = new \BsPaySdk\request\V2TradePaymentDelaytransConfirmRequest();
|
|
$request->setReqDate(date('Ymd'));
|
|
$request->setReqSeqId($confirmReqSeqId);
|
|
$request->setHuifuId($this->config->get('huifu_id'));
|
|
|
|
$acctSplitBunch = json_encode(['acct_infos' => $acctInfos], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$request->setExtendInfo([
|
|
'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,
|
|
]);
|
|
|
|
$result = $this->client->postRequest($request);
|
|
return $this->client->parseResponse($result);
|
|
}
|
|
|
|
protected function buildAcctInfos(array $items, $payFee)
|
|
{
|
|
$infos = [];
|
|
$allocated = '0.00';
|
|
foreach ($items as $item) {
|
|
$amt = number_format((float) $item['amount'], 2, '.', '');
|
|
if (bccomp($amt, '0.00', 2) <= 0) {
|
|
continue;
|
|
}
|
|
$infos[] = [
|
|
'huifu_id' => $item['receiver_account'],
|
|
'div_amt' => $amt,
|
|
];
|
|
$allocated = bcadd($allocated, $amt, 2);
|
|
}
|
|
|
|
$platformHuifuId = (new SplitConfig())->all()['platform_huifu_id'] ?? $this->config->get('huifu_id');
|
|
$remain = bcsub($payFee, $allocated, 2);
|
|
if (bccomp($remain, '0.00', 2) > 0) {
|
|
$infos[] = [
|
|
'huifu_id' => $platformHuifuId,
|
|
'div_amt' => $remain,
|
|
];
|
|
}
|
|
|
|
$sum = '0.00';
|
|
foreach ($infos as $info) {
|
|
$sum = bcadd($sum, $info['div_amt'], 2);
|
|
}
|
|
if (bccomp($sum, $payFee, 2) !== 0) {
|
|
throw new \RuntimeException('分账金额之和必须等于订单实付');
|
|
}
|
|
return $infos;
|
|
}
|
|
|
|
/**
|
|
* 延时分账确认总额须与汇付「待确认/交易金额」一致;内扣手续费时往往小于商城 pay_fee。
|
|
*/
|
|
protected function resolveConfirmPayFee($orderSn, $orderPayFee)
|
|
{
|
|
$fallback = number_format((float) $orderPayFee, 2, '.', '');
|
|
try {
|
|
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
|
$unconfirm = trim((string) ($query['unconfirm_amt'] ?? ''));
|
|
if ($unconfirm !== '' && is_numeric($unconfirm)) {
|
|
$amt = number_format((float) $unconfirm, 2, '.', '');
|
|
if (bccomp($amt, '0.00', 2) > 0) {
|
|
return $amt;
|
|
}
|
|
}
|
|
$trans = trim((string) ($query['trans_amt'] ?? ''));
|
|
if ($trans !== '' && is_numeric($trans)) {
|
|
$transAmt = number_format((float) $trans, 2, '.', '');
|
|
foreach (['fee_amt', 'fee_amount', 'trans_fee'] as $feeKey) {
|
|
$feeRaw = trim((string) ($query[$feeKey] ?? ''));
|
|
if ($feeRaw === '' || !is_numeric($feeRaw)) {
|
|
continue;
|
|
}
|
|
$net = bcsub($transAmt, number_format((float) $feeRaw, 2, '.', ''), 2);
|
|
if (bccomp($net, '0.00', 2) > 0 && bccomp($net, $fallback, 2) < 0) {
|
|
return $net;
|
|
}
|
|
}
|
|
if (bccomp($transAmt, '0.00', 2) > 0) {
|
|
return $transAmt;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
\think\Log::warning('[shopro_huifu] split confirm amount query fallback: ' . $e->getMessage());
|
|
}
|
|
return $fallback;
|
|
}
|
|
|
|
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();
|
|
if (!$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,
|
|
'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();
|
|
}
|
|
}
|
|
}
|