mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-05-17 06:20:25 +08:00
feat: 完善支付通道和收款监听链路
新增 ChannelNotifyPayloadInterface 等支付插件通知契约,规范 pay_no 定位和插件返回校验。 新增微信、支付宝、收钱吧、Postar 个人收款插件适配,支持余额识别与备注识别。 新增 receipt-watcher 后端进程、Redis 队列 job 和平台事件监听,覆盖收款流水通知、商户通知、退款派发、转账派发与清算完成。 补齐个人收款监听相关系统配置、仓储、服务费冻结明细、订单后台操作和通道测试能力。 重构支付单创建、回调、费用、风控、结算和通道统计链路,统一状态流转与幂等处理。
This commit is contained in:
@@ -3,31 +3,68 @@
|
||||
namespace app\service\payment\transfer;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\constant\TransferConstant;
|
||||
use app\common\interface\TransferPluginInterface;
|
||||
use app\exception\ConflictException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\merchant\Merchant;
|
||||
use app\model\payment\PaymentChannel;
|
||||
use app\model\payment\TransferOrder;
|
||||
use app\repository\account\balance\MerchantAccountRepository;
|
||||
use app\repository\payment\config\PaymentChannelRepository;
|
||||
use app\repository\payment\trade\TransferOrderRepository;
|
||||
use app\service\account\funds\MerchantAccountService;
|
||||
use app\service\payment\runtime\PaymentPluginManager;
|
||||
use app\service\payment\runtime\PaymentQueueService;
|
||||
use support\Log;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 转账服务。
|
||||
*
|
||||
* 负责 ePay 转账入口的参数校验、幂等创建、商户余额扣减、通道路由、
|
||||
* 队列派发上游转账请求和非终态查单同步。
|
||||
*/
|
||||
class TransferService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 自动查单最大次数。
|
||||
*/
|
||||
private const QUERY_MAX_ATTEMPTS = 5;
|
||||
|
||||
/**
|
||||
* 自动查单退避间隔。
|
||||
*/
|
||||
private const QUERY_DELAY_SECONDS = [60, 120, 300, 600, 900];
|
||||
|
||||
/**
|
||||
* 构造方法。
|
||||
*
|
||||
* @param MerchantAccountRepository $merchantAccountRepository 商户账户仓库
|
||||
* @param TransferOrderRepository $transferOrderRepository 转账单仓库
|
||||
* @param PaymentChannelRepository $paymentChannelRepository 支付通道仓库
|
||||
* @param PaymentPluginManager $paymentPluginManager 支付插件管理器
|
||||
* @param MerchantAccountService $merchantAccountService 商户账户服务
|
||||
* @param PaymentQueueService $paymentQueueService 支付队列服务
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantAccountRepository $merchantAccountRepository,
|
||||
protected TransferOrderRepository $transferOrderRepository
|
||||
protected TransferOrderRepository $transferOrderRepository,
|
||||
protected PaymentChannelRepository $paymentChannelRepository,
|
||||
protected PaymentPluginManager $paymentPluginManager,
|
||||
protected MerchantAccountService $merchantAccountService,
|
||||
protected PaymentQueueService $paymentQueueService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建转账单。
|
||||
* 创建并发起转账单。
|
||||
*
|
||||
* @param Merchant $merchant 商户
|
||||
* @param array $input 请求参数
|
||||
* @param array<string, mixed> $input 请求参数
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function submit(Merchant $merchant, array $input): array
|
||||
@@ -60,50 +97,115 @@ class TransferService extends BaseService
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->formatTransferOrder($existing);
|
||||
return $this->formatTransferOrder($this->syncTransferOrderIfNeeded($existing));
|
||||
}
|
||||
}
|
||||
|
||||
[$channel] = $this->resolveTransferChannelAndPlugin($merchantId, $type, (int) ($input['channel_id'] ?? 0));
|
||||
$transferRate = $this->resolveTransferRate();
|
||||
$costAmount = (int) floor($amount * $transferRate);
|
||||
$bizNo = $this->generateNo('TRF');
|
||||
$traceNo = $this->generateNo('TRC');
|
||||
$totalDebit = $amount + $costAmount;
|
||||
$created = false;
|
||||
|
||||
/** @var TransferOrder $transferOrder */
|
||||
$transferOrder = $this->transferOrderRepository->create([
|
||||
'biz_no' => $bizNo,
|
||||
'trace_no' => $traceNo,
|
||||
'merchant_id' => $merchantId,
|
||||
'merchant_group_id' => (int) ($merchant->group_id ?? 0),
|
||||
'out_biz_no' => $outBizNo !== '' ? $outBizNo : $this->generateNo('OBN'),
|
||||
'type' => $type,
|
||||
'account' => $account,
|
||||
'name' => $name,
|
||||
'amount' => $amount,
|
||||
'cost_amount' => $costAmount,
|
||||
'remark' => (string) ($input['remark'] ?? ''),
|
||||
'bookid' => (string) ($input['bookid'] ?? ''),
|
||||
'channel_id' => (int) ($input['channel_id'] ?? 0),
|
||||
'channel_request_no' => $this->generateNo('TRQ'),
|
||||
'status' => TransferConstant::TRANSFER_STATUS_PENDING,
|
||||
'request_at' => $this->now(),
|
||||
'ext_json' => (array) ($input['ext_json'] ?? []),
|
||||
]);
|
||||
$transferOrder = $this->transactionRetry(function () use (
|
||||
$merchant,
|
||||
$merchantId,
|
||||
$outBizNo,
|
||||
$type,
|
||||
$account,
|
||||
$name,
|
||||
$amount,
|
||||
$costAmount,
|
||||
$totalDebit,
|
||||
$bizNo,
|
||||
$traceNo,
|
||||
$channel,
|
||||
$input,
|
||||
&$created
|
||||
): TransferOrder {
|
||||
if ($outBizNo !== '') {
|
||||
$existing = $this->transferOrderRepository->findForUpdateByOutBizNo($merchantId, $outBizNo);
|
||||
if ($existing instanceof TransferOrder) {
|
||||
return $existing;
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalDebit > 0) {
|
||||
$this->merchantAccountService->debitTransferAmountInCurrentTransaction(
|
||||
$merchantId,
|
||||
$amount,
|
||||
$bizNo,
|
||||
'TRANSFER_DEDUCT:' . $bizNo,
|
||||
[
|
||||
'remark' => '转账本金扣减',
|
||||
'out_biz_no' => $outBizNo,
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
|
||||
if ($costAmount > 0) {
|
||||
$this->merchantAccountService->debitTransferFeeInCurrentTransaction(
|
||||
$merchantId,
|
||||
$costAmount,
|
||||
$bizNo,
|
||||
'TRANSFER_FEE:' . $bizNo,
|
||||
[
|
||||
'remark' => '转账手续费扣减',
|
||||
'out_biz_no' => $outBizNo,
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$createdOrder = $this->transferOrderRepository->create([
|
||||
'biz_no' => $bizNo,
|
||||
'trace_no' => $traceNo,
|
||||
'merchant_id' => $merchantId,
|
||||
'merchant_group_id' => (int) ($merchant->group_id ?? 0),
|
||||
'out_biz_no' => $outBizNo !== '' ? $outBizNo : $this->generateNo('OBN'),
|
||||
'type' => $type,
|
||||
'account' => $account,
|
||||
'name' => $name,
|
||||
'amount' => $amount,
|
||||
'cost_amount' => $costAmount,
|
||||
'remark' => (string) ($input['remark'] ?? ''),
|
||||
'bookid' => (string) ($input['bookid'] ?? ''),
|
||||
'channel_id' => (int) $channel->id,
|
||||
'channel_request_no' => $this->generateNo('TRQ'),
|
||||
'status' => TransferConstant::TRANSFER_STATUS_PROCESSING,
|
||||
'request_at' => $this->now(),
|
||||
'processing_at' => $this->now(),
|
||||
'ext_json' => (array) ($input['ext_json'] ?? []),
|
||||
]);
|
||||
$created = true;
|
||||
|
||||
return $createdOrder;
|
||||
});
|
||||
|
||||
if (!$created) {
|
||||
return $this->formatTransferOrder($this->syncTransferOrderIfNeeded($transferOrder));
|
||||
}
|
||||
|
||||
$this->paymentQueueService->sendTransferDispatch((string) $transferOrder->biz_no);
|
||||
|
||||
return $this->formatTransferOrder($transferOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询转账单。
|
||||
* 查询转账单,并在非终态时尝试主动同步一次通道状态。
|
||||
*
|
||||
* @param Merchant $merchant 商户
|
||||
* @param array $input 请求参数
|
||||
* @param array<string, mixed> $input 请求参数
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function query(Merchant $merchant, array $input): array
|
||||
{
|
||||
$order = $this->resolveTransferOrder($merchant, $input);
|
||||
return $this->formatTransferOrder($order);
|
||||
return $this->formatTransferOrder($this->syncTransferOrderIfNeeded($order));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,11 +223,346 @@ class TransferService extends BaseService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列中派发转账到上游通道。
|
||||
*
|
||||
* @param string $bizNo 转账单号
|
||||
* @return TransferOrder 最新转账单
|
||||
*/
|
||||
public function dispatchQueuedTransfer(string $bizNo): TransferOrder
|
||||
{
|
||||
$order = $this->resolveTransferOrderByBizNo($bizNo);
|
||||
if (TransferConstant::isTerminalStatus((int) $order->status)) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
[$channel, $plugin] = $this->resolveTransferChannelAndPlugin((int) $order->merchant_id, (string) $order->type, (int) $order->channel_id);
|
||||
unset($channel);
|
||||
|
||||
$latest = $this->dispatchTransfer($order, $plugin);
|
||||
$this->enqueueNextTransferQueryIfNeeded($latest, 0);
|
||||
|
||||
return $latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列中查询转账上游状态。
|
||||
*
|
||||
* @param string $bizNo 转账单号
|
||||
* @param int $attempt 当前查单次数
|
||||
* @return TransferOrder 最新转账单
|
||||
*/
|
||||
public function queryQueuedTransfer(string $bizNo, int $attempt = 0): TransferOrder
|
||||
{
|
||||
$order = $this->resolveTransferOrderByBizNo($bizNo);
|
||||
if (TransferConstant::isTerminalStatus((int) $order->status)) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$latest = $this->syncTransferOrderIfNeeded($order);
|
||||
$this->enqueueNextTransferQueryIfNeeded($latest, $attempt);
|
||||
|
||||
return $latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择可处理当前转账类型的通道和插件。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param string $type 转账类型
|
||||
* @param int $channelId 指定通道ID,0 表示自动选择
|
||||
* @return array{0: PaymentChannel, 1: TransferPluginInterface}
|
||||
*/
|
||||
private function resolveTransferChannelAndPlugin(int $merchantId, string $type, int $channelId = 0): array
|
||||
{
|
||||
$query = $this->paymentChannelRepository->query()
|
||||
->where('status', CommonConstant::STATUS_ENABLED)
|
||||
->whereIn('merchant_id', [0, $merchantId])
|
||||
->orderBy('sort_no')
|
||||
->orderBy('id');
|
||||
|
||||
if ($channelId > 0) {
|
||||
$query->whereKey($channelId);
|
||||
}
|
||||
|
||||
$channels = $query->get();
|
||||
foreach ($channels as $channel) {
|
||||
try {
|
||||
$plugin = $this->paymentPluginManager->createTransferByChannel($channel, false);
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$plugin instanceof TransferPluginInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$transferTypes = array_values(array_filter(array_map(static fn ($item) => trim((string) $item), $plugin->getEnabledTransferTypes())));
|
||||
if (!in_array($type, $transferTypes, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [$channel, $plugin];
|
||||
}
|
||||
|
||||
throw new ValidationException('没有可用的转账通道', [
|
||||
'type' => $type,
|
||||
'channel_id' => $channelId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据转账单号解析转账单。
|
||||
*
|
||||
* @param string $bizNo 转账单号
|
||||
* @return TransferOrder 转账单
|
||||
*/
|
||||
private function resolveTransferOrderByBizNo(string $bizNo): TransferOrder
|
||||
{
|
||||
$order = $this->transferOrderRepository->findByBizNo($bizNo);
|
||||
if (!$order) {
|
||||
throw new ResourceNotFoundException('转账单不存在', ['biz_no' => $bizNo]);
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求上游插件发起转账。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @param TransferPluginInterface $plugin 转账插件
|
||||
* @return TransferOrder 最新转账单
|
||||
*/
|
||||
private function dispatchTransfer(TransferOrder $order, TransferPluginInterface $plugin): TransferOrder
|
||||
{
|
||||
try {
|
||||
$result = $plugin->transfer($this->buildPluginTransferPayload($order));
|
||||
return $this->applyPluginTransferResult($order, $result);
|
||||
} catch (Throwable $e) {
|
||||
Log::warning(sprintf(
|
||||
'[TransferService] 转账请求失败 biz_no=%s error=%s',
|
||||
(string) $order->biz_no,
|
||||
$e->getMessage()
|
||||
));
|
||||
|
||||
return $this->markTransferFailed($order, $e->getMessage() ?: '转账请求异常');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非终态转账单主动查单并同步一次状态。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @return TransferOrder 最新转账单
|
||||
*/
|
||||
private function syncTransferOrderIfNeeded(TransferOrder $order): TransferOrder
|
||||
{
|
||||
if (TransferConstant::isTerminalStatus((int) $order->status) || (int) $order->channel_id <= 0) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
try {
|
||||
[$channel, $plugin] = $this->resolveTransferChannelAndPlugin((int) $order->merchant_id, (string) $order->type, (int) $order->channel_id);
|
||||
unset($channel);
|
||||
$result = $plugin->transferQuery($this->buildPluginTransferPayload($order));
|
||||
return $this->applyPluginTransferResult($order, $result);
|
||||
} catch (Throwable $e) {
|
||||
Log::warning(sprintf(
|
||||
'[TransferService] 转账查单失败 biz_no=%s error=%s',
|
||||
(string) $order->biz_no,
|
||||
$e->getMessage()
|
||||
));
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如转账仍在处理中,投递下一次延迟查单。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @param int $attempt 当前查单次数
|
||||
* @return void
|
||||
*/
|
||||
private function enqueueNextTransferQueryIfNeeded(TransferOrder $order, int $attempt): void
|
||||
{
|
||||
if (TransferConstant::isTerminalStatus((int) $order->status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attempt >= self::QUERY_MAX_ATTEMPTS) {
|
||||
$this->markTransferQueryMaxReached($order, $attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
$nextAttempt = $attempt + 1;
|
||||
$delay = $this->resolveTransferQueryDelay($attempt);
|
||||
$this->paymentQueueService->sendTransferQuery((string) $order->biz_no, $nextAttempt, $delay);
|
||||
$this->recordTransferQuerySchedule($order, $nextAttempt, $delay, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用插件转账结果。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @param array<string, mixed> $result 插件结果
|
||||
* @return TransferOrder 最新转账单
|
||||
*/
|
||||
private function applyPluginTransferResult(TransferOrder $order, array $result): TransferOrder
|
||||
{
|
||||
$status = $this->normalizePluginStatus($result);
|
||||
if ($status === TransferConstant::TRANSFER_STATUS_FAILED) {
|
||||
return $this->markTransferFailed($order, (string) ($result['msg'] ?? $result['message'] ?? '转账失败'), $result);
|
||||
}
|
||||
|
||||
if ($status === TransferConstant::TRANSFER_STATUS_SUCCESS) {
|
||||
return $this->transactionRetry(function () use ($order, $result): TransferOrder {
|
||||
$latest = $this->transferOrderRepository->findForUpdateByBizNo((string) $order->biz_no);
|
||||
if (!$latest || TransferConstant::isTerminalStatus((int) $latest->status)) {
|
||||
return $latest ?: $order;
|
||||
}
|
||||
|
||||
$latest->status = TransferConstant::TRANSFER_STATUS_SUCCESS;
|
||||
$latest->succeeded_at = $result['succeeded_at'] ?? $this->now();
|
||||
$latest->channel_order_no = $this->firstText($result, ['channel_order_no', 'chan_order_no', 'orderid']);
|
||||
$latest->channel_trade_no = $this->firstText($result, ['channel_trade_no', 'chan_trade_no', 'trade_no']);
|
||||
$latest->channel_error_code = '';
|
||||
$latest->channel_error_msg = '';
|
||||
$latest->ext_json = array_merge((array) $latest->ext_json, [
|
||||
'plugin_result' => $this->buildPluginResultSnapshot($result),
|
||||
]);
|
||||
$latest->save();
|
||||
|
||||
return $latest->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
return $this->transactionRetry(function () use ($order, $result): TransferOrder {
|
||||
$latest = $this->transferOrderRepository->findForUpdateByBizNo((string) $order->biz_no);
|
||||
if (!$latest || TransferConstant::isTerminalStatus((int) $latest->status)) {
|
||||
return $latest ?: $order;
|
||||
}
|
||||
|
||||
$latest->status = TransferConstant::TRANSFER_STATUS_PROCESSING;
|
||||
$latest->channel_order_no = $this->firstText($result, ['channel_order_no', 'chan_order_no', 'orderid']) ?: $latest->channel_order_no;
|
||||
$latest->channel_trade_no = $this->firstText($result, ['channel_trade_no', 'chan_trade_no', 'trade_no']) ?: $latest->channel_trade_no;
|
||||
$latest->ext_json = array_merge((array) $latest->ext_json, [
|
||||
'plugin_result' => $this->buildPluginResultSnapshot($result),
|
||||
]);
|
||||
$latest->save();
|
||||
|
||||
return $latest->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记转账失败并释放已扣商户余额。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @param string $message 失败原因
|
||||
* @param array<string, mixed> $result 插件结果
|
||||
* @return TransferOrder 最新转账单
|
||||
*/
|
||||
private function markTransferFailed(TransferOrder $order, string $message, array $result = []): TransferOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($order, $message, $result): TransferOrder {
|
||||
$latest = $this->transferOrderRepository->findForUpdateByBizNo((string) $order->biz_no);
|
||||
if (!$latest || TransferConstant::isTerminalStatus((int) $latest->status)) {
|
||||
return $latest ?: $order;
|
||||
}
|
||||
|
||||
$releaseAmount = (int) $latest->amount + (int) $latest->cost_amount;
|
||||
if ($releaseAmount > 0) {
|
||||
$this->merchantAccountService->releaseTransferAmountInCurrentTransaction(
|
||||
(int) $latest->merchant_id,
|
||||
$releaseAmount,
|
||||
(string) $latest->biz_no,
|
||||
'TRANSFER_RELEASE:' . (string) $latest->biz_no,
|
||||
[
|
||||
'remark' => '转账失败释放',
|
||||
'message' => $message,
|
||||
],
|
||||
(string) ($latest->trace_no ?: $latest->biz_no)
|
||||
);
|
||||
}
|
||||
|
||||
$latest->status = TransferConstant::TRANSFER_STATUS_FAILED;
|
||||
$latest->failed_at = $this->now();
|
||||
$latest->channel_error_code = $this->firstText($result, ['channel_error_code', 'code']);
|
||||
$latest->channel_error_msg = $message;
|
||||
$latest->ext_json = array_merge((array) $latest->ext_json, [
|
||||
'plugin_result' => $this->buildPluginResultSnapshot($result),
|
||||
]);
|
||||
$latest->save();
|
||||
|
||||
return $latest->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录转账查单已达到自动查询上限。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @param int $attempt 当前查单次数
|
||||
* @return void
|
||||
*/
|
||||
private function markTransferQueryMaxReached(TransferOrder $order, int $attempt): void
|
||||
{
|
||||
$this->recordTransferQuerySchedule($order, $attempt, 0, true);
|
||||
Log::warning(sprintf(
|
||||
'[TransferService] 转账自动查单达到上限 biz_no=%s attempt=%s',
|
||||
(string) $order->biz_no,
|
||||
$attempt
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录自动查单调度快照。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @param int $attempt 下一次或当前查单次数
|
||||
* @param int $delay 延迟秒数
|
||||
* @param bool $maxReached 是否达到上限
|
||||
* @return void
|
||||
*/
|
||||
private function recordTransferQuerySchedule(TransferOrder $order, int $attempt, int $delay, bool $maxReached): void
|
||||
{
|
||||
try {
|
||||
$this->transactionRetry(function () use ($order, $attempt, $delay, $maxReached): void {
|
||||
$latest = $this->transferOrderRepository->findForUpdateByBizNo((string) $order->biz_no);
|
||||
if (!$latest || TransferConstant::isTerminalStatus((int) $latest->status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$runtime = (array) (((array) ($latest->ext_json ?? []))['runtime'] ?? []);
|
||||
$runtime['transfer_query'] = [
|
||||
'attempt' => $attempt,
|
||||
'max_attempts' => self::QUERY_MAX_ATTEMPTS,
|
||||
'last_scheduled_at' => $this->now(),
|
||||
'next_delay_seconds' => $delay,
|
||||
'max_reached' => $maxReached,
|
||||
];
|
||||
|
||||
$extJson = (array) ($latest->ext_json ?? []);
|
||||
$extJson['runtime'] = $runtime;
|
||||
$latest->ext_json = $extJson;
|
||||
$latest->save();
|
||||
});
|
||||
} catch (Throwable $e) {
|
||||
Log::warning(sprintf(
|
||||
'[TransferService] 记录转账查单调度失败 biz_no=%s error=%s',
|
||||
(string) $order->biz_no,
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析转账单。
|
||||
*
|
||||
* @param Merchant $merchant 商户
|
||||
* @param array $input 请求参数
|
||||
* @param array<string, mixed> $input 请求参数
|
||||
* @return TransferOrder
|
||||
*/
|
||||
private function resolveTransferOrder(Merchant $merchant, array $input): TransferOrder
|
||||
@@ -168,7 +605,7 @@ class TransferService extends BaseService
|
||||
'errmsg' => (string) ($order->channel_error_msg ?? ''),
|
||||
'biz_no' => (string) $order->biz_no,
|
||||
'out_biz_no' => (string) $order->out_biz_no,
|
||||
'orderid' => (string) $order->biz_no,
|
||||
'orderid' => (string) ($order->channel_order_no ?: $order->biz_no),
|
||||
'paydate' => $this->formatDateTime($order->succeeded_at ?? null, ''),
|
||||
'amount' => $this->formatAmount((int) $order->amount),
|
||||
'cost_money' => $this->formatAmount((int) $order->cost_amount),
|
||||
@@ -176,10 +613,103 @@ class TransferService extends BaseService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建插件转账请求载荷。
|
||||
*
|
||||
* @param TransferOrder $order 转账单
|
||||
* @return array<string, mixed> 插件转账参数
|
||||
*/
|
||||
private function buildPluginTransferPayload(TransferOrder $order): array
|
||||
{
|
||||
return [
|
||||
'transfer_no' => (string) $order->biz_no,
|
||||
'biz_no' => (string) $order->biz_no,
|
||||
'out_biz_no' => (string) $order->out_biz_no,
|
||||
'type' => (string) $order->type,
|
||||
'account' => (string) $order->account,
|
||||
'name' => (string) $order->name,
|
||||
'amount' => (int) $order->amount,
|
||||
'money' => $this->formatAmount((int) $order->amount),
|
||||
'cost_amount' => (int) $order->cost_amount,
|
||||
'remark' => (string) $order->remark,
|
||||
'bookid' => (string) $order->bookid,
|
||||
'channel_request_no' => (string) $order->channel_request_no,
|
||||
'channel_order_no' => (string) ($order->channel_order_no ?? ''),
|
||||
'channel_trade_no' => (string) ($order->channel_trade_no ?? ''),
|
||||
'trace_no' => (string) ($order->trace_no ?: $order->biz_no),
|
||||
'extra' => (array) ($order->ext_json ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化插件转账状态。
|
||||
*
|
||||
* @param array<string, mixed> $result 插件结果
|
||||
* @return int 平台转账状态
|
||||
*/
|
||||
private function normalizePluginStatus(array $result): int
|
||||
{
|
||||
$statusText = strtolower(trim((string) ($result['status'] ?? $result['trade_status'] ?? $result['channel_status'] ?? '')));
|
||||
if (in_array($statusText, ['success', 'succeeded', 'finished', 'paid'], true)) {
|
||||
return TransferConstant::TRANSFER_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
if (in_array($statusText, ['failed', 'fail', 'closed', 'error'], true)) {
|
||||
return TransferConstant::TRANSFER_STATUS_FAILED;
|
||||
}
|
||||
|
||||
if (array_key_exists('success', $result) && (bool) $result['success'] === false) {
|
||||
return TransferConstant::TRANSFER_STATUS_FAILED;
|
||||
}
|
||||
|
||||
$statusCode = (int) ($result['status_code'] ?? -1);
|
||||
if ($statusCode === TransferConstant::TRANSFER_STATUS_SUCCESS) {
|
||||
return TransferConstant::TRANSFER_STATUS_SUCCESS;
|
||||
}
|
||||
if ($statusCode === TransferConstant::TRANSFER_STATUS_FAILED) {
|
||||
return TransferConstant::TRANSFER_STATUS_FAILED;
|
||||
}
|
||||
|
||||
return TransferConstant::TRANSFER_STATUS_PROCESSING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建插件结果快照。
|
||||
*
|
||||
* raw_data 可能包含完整上游响应,快照落库时剔除以降低日志和数据表压力。
|
||||
*
|
||||
* @param array<string, mixed> $result 插件结果
|
||||
* @return array<string, mixed> 可落库结果
|
||||
*/
|
||||
private function buildPluginResultSnapshot(array $result): array
|
||||
{
|
||||
unset($result['raw_data']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多个候选字段中取第一个非空文本。
|
||||
*
|
||||
* @param array<string, mixed> $data 数据
|
||||
* @param array<int, string> $keys 候选字段
|
||||
* @return string 字段值
|
||||
*/
|
||||
private function firstText(array $data, array $keys): string
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
$value = $data[$key] ?? null;
|
||||
if (is_scalar($value) && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析转账费率。
|
||||
*
|
||||
* @return float
|
||||
* @return float 转账手续费率
|
||||
*/
|
||||
private function resolveTransferRate(): float
|
||||
{
|
||||
@@ -193,6 +723,19 @@ class TransferService extends BaseService
|
||||
return $floatRate > 0 ? $floatRate : 0.01;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析下一次转账查单延迟。
|
||||
*
|
||||
* @param int $attempt 当前查单次数
|
||||
* @return int 延迟秒数
|
||||
*/
|
||||
private function resolveTransferQueryDelay(int $attempt): int
|
||||
{
|
||||
$index = max(0, min($attempt, count(self::QUERY_DELAY_SECONDS) - 1));
|
||||
|
||||
return self::QUERY_DELAY_SECONDS[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额字符串转分。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user