mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-26 12:04:28 +08:00
重构初始化
This commit is contained in:
256
app/service/payment/order/PayOrderAttemptService.php
Normal file
256
app/service/payment/order/PayOrderAttemptService.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\constant\NotifyConstant;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ConflictException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\payment\BizOrder;
|
||||
use app\model\payment\PayOrder;
|
||||
use app\repository\payment\config\PaymentTypeRepository;
|
||||
use app\repository\payment\trade\BizOrderRepository;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
use app\service\account\funds\MerchantAccountService;
|
||||
use app\service\merchant\MerchantService;
|
||||
use app\service\payment\runtime\PaymentRouteService;
|
||||
|
||||
/**
|
||||
* 支付单发起服务。
|
||||
*
|
||||
* 负责支付单预创建、通道路由选择、第三方装单和首轮状态落库。
|
||||
*/
|
||||
class PayOrderAttemptService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantService $merchantService,
|
||||
protected PaymentRouteService $paymentRouteService,
|
||||
protected MerchantAccountService $merchantAccountService,
|
||||
protected BizOrderRepository $bizOrderRepository,
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected PaymentTypeRepository $paymentTypeRepository,
|
||||
protected PayOrderChannelDispatchService $payOrderChannelDispatchService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 预创建支付尝试。
|
||||
*
|
||||
* 该方法会完成商户、支付方式、路由、通道限额、串行尝试和自有通道手续费预占的完整预检查。
|
||||
*
|
||||
* @param array $input 支付请求参数
|
||||
* @return array{merchant:mixed,biz_order:mixed,pay_order:mixed,route:array,payment_result:array,pay_params:array}
|
||||
*/
|
||||
public function preparePayAttempt(array $input): array
|
||||
{
|
||||
$merchantId = (int) ($input['merchant_id'] ?? 0);
|
||||
$merchantOrderNo = trim((string) ($input['merchant_order_no'] ?? ''));
|
||||
$payTypeId = (int) ($input['pay_type_id'] ?? 0);
|
||||
$payAmount = (int) ($input['pay_amount'] ?? 0);
|
||||
|
||||
if ($merchantId <= 0 || $merchantOrderNo === '' || $payTypeId <= 0 || $payAmount <= 0) {
|
||||
throw new ValidationException('支付入参不完整');
|
||||
}
|
||||
|
||||
$merchant = $this->merchantService->ensureMerchantEnabled($merchantId);
|
||||
$merchantGroupId = (int) $merchant->group_id;
|
||||
if ($merchantGroupId <= 0) {
|
||||
throw new ValidationException('商户未配置分组', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
$this->merchantService->ensureMerchantGroupEnabled($merchantGroupId);
|
||||
|
||||
/** @var PaymentType|null $paymentType */
|
||||
$paymentType = $this->paymentTypeRepository->find($payTypeId);
|
||||
if (!$paymentType || (int) $paymentType->status !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new BusinessStateException('支付方式不支持', ['pay_type_id' => $payTypeId]);
|
||||
}
|
||||
|
||||
$route = $this->paymentRouteService->resolveByMerchantGroup($merchantGroupId, $payTypeId, $payAmount, $input);
|
||||
$selected = $route['selected_channel'];
|
||||
/** @var PaymentChannel $channel */
|
||||
$channel = $selected['channel'];
|
||||
|
||||
$payNo = $this->generateNo('PAY');
|
||||
$channelRequestNo = $this->generateNo('REQ');
|
||||
|
||||
$prepared = $this->transactionRetry(function () use (
|
||||
$input,
|
||||
$merchant,
|
||||
$merchantId,
|
||||
$merchantGroupId,
|
||||
$merchantOrderNo,
|
||||
$payTypeId,
|
||||
$payAmount,
|
||||
$route,
|
||||
$channel,
|
||||
$payNo,
|
||||
$channelRequestNo
|
||||
) {
|
||||
$existingBizOrder = $this->bizOrderRepository->findForUpdateByMerchantAndOrderNo($merchantId, $merchantOrderNo);
|
||||
$bizTraceNo = '';
|
||||
|
||||
if ($existingBizOrder) {
|
||||
if ((int) $existingBizOrder->order_amount !== $payAmount) {
|
||||
throw new ValidationException('同一商户订单号金额不一致', [
|
||||
'merchant_id' => $merchantId,
|
||||
'merchant_order_no' => $merchantOrderNo,
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array((int) $existingBizOrder->status, [
|
||||
TradeConstant::ORDER_STATUS_SUCCESS,
|
||||
TradeConstant::ORDER_STATUS_CLOSED,
|
||||
TradeConstant::ORDER_STATUS_TIMEOUT,
|
||||
], true)) {
|
||||
throw new BusinessStateException('支付单状态不允许重复创建', [
|
||||
'biz_no' => (string) $existingBizOrder->biz_no,
|
||||
'status' => (int) $existingBizOrder->status,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($existingBizOrder->active_pay_no)) {
|
||||
$activePayOrder = $this->payOrderRepository->findForUpdateByPayNo((string) $existingBizOrder->active_pay_no);
|
||||
if ($activePayOrder && in_array((int) $activePayOrder->status, [TradeConstant::ORDER_STATUS_CREATED, TradeConstant::ORDER_STATUS_PAYING], true)) {
|
||||
throw new ConflictException('重复请求', [
|
||||
'biz_no' => (string) $existingBizOrder->biz_no,
|
||||
'active_pay_no' => (string) $existingBizOrder->active_pay_no,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$bizOrder = $existingBizOrder;
|
||||
$bizTraceNo = trim((string) ($bizOrder->trace_no ?? ''));
|
||||
if ($bizTraceNo === '') {
|
||||
$bizTraceNo = (string) $bizOrder->biz_no;
|
||||
$bizOrder->trace_no = $bizTraceNo;
|
||||
}
|
||||
$attemptNo = (int) $bizOrder->attempt_count + 1;
|
||||
} else {
|
||||
$bizOrder = $this->bizOrderRepository->create([
|
||||
'biz_no' => $this->generateNo('BIZ'),
|
||||
'trace_no' => $this->generateNo('TRC'),
|
||||
'merchant_id' => $merchantId,
|
||||
'merchant_group_id' => $merchantGroupId,
|
||||
'poll_group_id' => (int) $route['poll_group']->id,
|
||||
'merchant_order_no' => $merchantOrderNo,
|
||||
'subject' => (string) ($input['subject'] ?? ''),
|
||||
'body' => (string) ($input['body'] ?? ''),
|
||||
'order_amount' => $payAmount,
|
||||
'paid_amount' => 0,
|
||||
'refund_amount' => 0,
|
||||
'status' => TradeConstant::ORDER_STATUS_CREATED,
|
||||
'attempt_count' => 0,
|
||||
'ext_json' => $input['ext_json'] ?? [],
|
||||
]);
|
||||
$bizTraceNo = (string) $bizOrder->trace_no;
|
||||
$attemptNo = 1;
|
||||
}
|
||||
|
||||
$feeRateBp = (int) $channel->cost_rate_bp;
|
||||
$splitRateBp = (int) $channel->split_rate_bp ?: 10000;
|
||||
$feeEstimated = $this->calculateAmountByBp($payAmount, $feeRateBp);
|
||||
|
||||
if ((int) $channel->channel_mode === RouteConstant::CHANNEL_MODE_SELF && $feeEstimated > 0) {
|
||||
$this->merchantAccountService->freezeAmountInCurrentTransaction(
|
||||
$merchantId,
|
||||
$feeEstimated,
|
||||
$payNo,
|
||||
'PAY_FREEZE:' . $payNo,
|
||||
[
|
||||
'merchant_order_no' => $merchantOrderNo,
|
||||
'pay_type_id' => $payTypeId,
|
||||
'channel_id' => (int) $channel->id,
|
||||
'remark' => '自有通道手续费预占',
|
||||
],
|
||||
$bizTraceNo
|
||||
);
|
||||
}
|
||||
|
||||
$payOrder = $this->payOrderRepository->create([
|
||||
'pay_no' => $payNo,
|
||||
'biz_no' => (string) $bizOrder->biz_no,
|
||||
'trace_no' => $bizTraceNo,
|
||||
'merchant_id' => $merchantId,
|
||||
'merchant_group_id' => $merchantGroupId,
|
||||
'poll_group_id' => (int) $route['poll_group']->id,
|
||||
'attempt_no' => (int) $attemptNo,
|
||||
'channel_id' => (int) $channel->id,
|
||||
'pay_type_id' => $payTypeId,
|
||||
'plugin_code' => (string) $channel->plugin_code,
|
||||
'channel_type' => (int) $channel->channel_mode,
|
||||
'channel_mode' => (int) $channel->channel_mode,
|
||||
'pay_amount' => $payAmount,
|
||||
'fee_rate_bp_snapshot' => $feeRateBp,
|
||||
'split_rate_bp_snapshot' => $splitRateBp,
|
||||
'fee_estimated_amount' => $feeEstimated,
|
||||
'fee_actual_amount' => 0,
|
||||
'status' => TradeConstant::ORDER_STATUS_PAYING,
|
||||
'fee_status' => (int) $channel->channel_mode === RouteConstant::CHANNEL_MODE_SELF ? TradeConstant::FEE_STATUS_FROZEN : TradeConstant::FEE_STATUS_NONE,
|
||||
'settlement_status' => TradeConstant::SETTLEMENT_STATUS_NONE,
|
||||
'channel_request_no' => $channelRequestNo,
|
||||
'request_at' => $this->now(),
|
||||
'callback_status' => NotifyConstant::PROCESS_STATUS_PENDING,
|
||||
'callback_times' => 0,
|
||||
'ext_json' => array_merge($input['ext_json'] ?? [], [
|
||||
'merchant_no' => (string) $merchant->merchant_no,
|
||||
'merchant_group_id' => $merchantGroupId,
|
||||
'poll_group_id' => (int) $route['poll_group']->id,
|
||||
'channel_id' => (int) $channel->id,
|
||||
'channel_mode' => (int) $channel->channel_mode,
|
||||
'trace_no' => $bizTraceNo,
|
||||
]),
|
||||
]);
|
||||
|
||||
$bizOrder->active_pay_no = (string) $payOrder->pay_no;
|
||||
$bizOrder->attempt_count = (int) $attemptNo;
|
||||
$bizOrder->status = TradeConstant::ORDER_STATUS_PAYING;
|
||||
$bizOrder->merchant_group_id = $merchantGroupId;
|
||||
$bizOrder->poll_group_id = (int) $route['poll_group']->id;
|
||||
if ($bizTraceNo !== '' && (string) ($bizOrder->trace_no ?? '') === '') {
|
||||
$bizOrder->trace_no = $bizTraceNo;
|
||||
}
|
||||
$bizOrder->save();
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'biz_order' => $bizOrder->refresh(),
|
||||
'pay_order' => $payOrder,
|
||||
'route' => $route,
|
||||
];
|
||||
});
|
||||
|
||||
/** @var PayOrder $payOrder */
|
||||
$payOrder = $prepared['pay_order'];
|
||||
/** @var BizOrder $bizOrder */
|
||||
$bizOrder = $prepared['biz_order'];
|
||||
/** @var \app\model\payment\PaymentChannel $channel */
|
||||
$channel = $prepared['route']['selected_channel']['channel'];
|
||||
|
||||
$channelDispatchResult = $this->payOrderChannelDispatchService->dispatch($payOrder, $bizOrder, $channel);
|
||||
|
||||
$prepared['pay_order'] = $channelDispatchResult['pay_order'];
|
||||
$prepared['payment_result'] = $channelDispatchResult['payment_result'];
|
||||
$prepared['pay_params'] = $channelDispatchResult['pay_params'];
|
||||
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算手续费金额。
|
||||
*/
|
||||
private function calculateAmountByBp(int $amount, int $bp): int
|
||||
{
|
||||
if ($amount <= 0 || $bp <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) floor($amount * $bp / 10000);
|
||||
}
|
||||
}
|
||||
139
app/service/payment/order/PayOrderCallbackService.php
Normal file
139
app/service/payment/order/PayOrderCallbackService.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\NotifyConstant;
|
||||
use app\exception\PaymentException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\model\payment\PayOrder;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
use app\service\payment\runtime\NotifyService;
|
||||
use app\service\payment\runtime\PaymentPluginManager;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 支付单回调服务。
|
||||
*
|
||||
* 负责渠道回调日志记录、插件回调解析和支付状态分发。
|
||||
*/
|
||||
class PayOrderCallbackService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected NotifyService $notifyService,
|
||||
protected PaymentPluginManager $paymentPluginManager,
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected PayOrderLifecycleService $payOrderLifecycleService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理渠道回调。
|
||||
*/
|
||||
public function handleChannelCallback(array $input): PayOrder
|
||||
{
|
||||
$payNo = trim((string) ($input['pay_no'] ?? ''));
|
||||
if ($payNo === '') {
|
||||
throw new \InvalidArgumentException('pay_no 不能为空');
|
||||
}
|
||||
|
||||
$this->notifyService->recordPayCallback([
|
||||
'pay_no' => $payNo,
|
||||
'channel_id' => (int) ($input['channel_id'] ?? 0),
|
||||
'callback_type' => (int) ($input['callback_type'] ?? NotifyConstant::CALLBACK_TYPE_ASYNC),
|
||||
'request_data' => $input['request_data'] ?? [],
|
||||
'verify_status' => (int) ($input['verify_status'] ?? NotifyConstant::VERIFY_STATUS_UNKNOWN),
|
||||
'process_status' => (int) ($input['process_status'] ?? NotifyConstant::PROCESS_STATUS_PENDING),
|
||||
'process_result' => $input['process_result'] ?? [],
|
||||
]);
|
||||
|
||||
$success = (bool) ($input['success'] ?? false);
|
||||
if ($success) {
|
||||
return $this->payOrderLifecycleService->markPaySuccess($payNo, $input);
|
||||
}
|
||||
|
||||
return $this->payOrderLifecycleService->markPayFailed($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按支付单号处理真实第三方回调。
|
||||
*/
|
||||
public function handlePluginCallback(string $payNo, Request $request): string|Response
|
||||
{
|
||||
$payOrder = $this->payOrderRepository->findByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
$plugin = $this->paymentPluginManager->createByPayOrder($payOrder, true);
|
||||
|
||||
try {
|
||||
$result = $plugin->notify($request);
|
||||
$status = (string) ($result['status'] ?? '');
|
||||
$success = array_key_exists('success', $result)
|
||||
? (bool) $result['success']
|
||||
: in_array($status, ['success', 'paid'], true);
|
||||
|
||||
$callbackPayload = [
|
||||
'pay_no' => $payNo,
|
||||
'success' => $success,
|
||||
'channel_id' => (int) $payOrder->channel_id,
|
||||
'callback_type' => NotifyConstant::CALLBACK_TYPE_ASYNC,
|
||||
'request_data' => array_merge($request->get(), $request->post()),
|
||||
'verify_status' => NotifyConstant::VERIFY_STATUS_SUCCESS,
|
||||
'process_status' => $success ? NotifyConstant::PROCESS_STATUS_SUCCESS : NotifyConstant::PROCESS_STATUS_FAILED,
|
||||
'process_result' => $result,
|
||||
'channel_trade_no' => (string) ($result['chan_trade_no'] ?? ''),
|
||||
'channel_order_no' => (string) ($result['chan_order_no'] ?? ''),
|
||||
'paid_at' => $result['paid_at'] ?? null,
|
||||
'channel_error_code' => (string) ($result['channel_error_code'] ?? ''),
|
||||
'channel_error_msg' => (string) ($result['channel_error_msg'] ?? ''),
|
||||
'ext_json' => [
|
||||
'plugin_code' => (string) $payOrder->plugin_code,
|
||||
'notify_status' => $status,
|
||||
],
|
||||
];
|
||||
if (isset($result['fee_actual_amount'])) {
|
||||
$callbackPayload['fee_actual_amount'] = (int) $result['fee_actual_amount'];
|
||||
}
|
||||
|
||||
$this->handleChannelCallback($callbackPayload);
|
||||
|
||||
return $success ? $plugin->notifySuccess() : $plugin->notifyFail();
|
||||
} catch (PaymentException $e) {
|
||||
$this->notifyService->recordPayCallback([
|
||||
'pay_no' => $payNo,
|
||||
'channel_id' => (int) $payOrder->channel_id,
|
||||
'callback_type' => NotifyConstant::CALLBACK_TYPE_ASYNC,
|
||||
'request_data' => array_merge($request->get(), $request->post()),
|
||||
'verify_status' => NotifyConstant::VERIFY_STATUS_FAILED,
|
||||
'process_status' => NotifyConstant::PROCESS_STATUS_FAILED,
|
||||
'process_result' => [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
],
|
||||
]);
|
||||
|
||||
return $plugin->notifyFail();
|
||||
} catch (\Throwable $e) {
|
||||
$this->notifyService->recordPayCallback([
|
||||
'pay_no' => $payNo,
|
||||
'channel_id' => (int) $payOrder->channel_id,
|
||||
'callback_type' => NotifyConstant::CALLBACK_TYPE_ASYNC,
|
||||
'request_data' => array_merge($request->get(), $request->post()),
|
||||
'verify_status' => NotifyConstant::VERIFY_STATUS_FAILED,
|
||||
'process_status' => NotifyConstant::PROCESS_STATUS_FAILED,
|
||||
'process_result' => [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => 'PLUGIN_NOTIFY_ERROR',
|
||||
],
|
||||
]);
|
||||
|
||||
return $plugin->notifyFail();
|
||||
}
|
||||
}
|
||||
}
|
||||
134
app/service/payment/order/PayOrderChannelDispatchService.php
Normal file
134
app/service/payment/order/PayOrderChannelDispatchService.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exception\PaymentException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\model\payment\BizOrder;
|
||||
use app\model\payment\PayOrder;
|
||||
use app\model\payment\PaymentChannel;
|
||||
use app\model\payment\PaymentType;
|
||||
use app\repository\payment\config\PaymentTypeRepository;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
use app\service\payment\runtime\PaymentPluginManager;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 支付渠道单据拉起服务。
|
||||
*
|
||||
* 负责调用第三方插件、写回渠道订单号,并在失败时推进支付失败状态。
|
||||
*/
|
||||
class PayOrderChannelDispatchService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected PaymentPluginManager $paymentPluginManager,
|
||||
protected PaymentTypeRepository $paymentTypeRepository,
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected PayOrderLifecycleService $payOrderLifecycleService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉起第三方支付单并回写渠道响应。
|
||||
*
|
||||
* @return array{pay_order:PayOrder,payment_result:array,pay_params:array}
|
||||
*/
|
||||
public function dispatch(PayOrder $payOrder, BizOrder $bizOrder, PaymentChannel $channel): array
|
||||
{
|
||||
try {
|
||||
$plugin = $this->paymentPluginManager->createByChannel($channel, (int) $payOrder->pay_type_id);
|
||||
/** @var PaymentType|null $paymentType */
|
||||
$paymentType = $this->paymentTypeRepository->find((int) $payOrder->pay_type_id);
|
||||
$extJson = (array) ($payOrder->ext_json ?? []);
|
||||
$callbackBaseUrl = trim((string) ($extJson['channel_callback_base_url'] ?? ''));
|
||||
$callbackUrl = $callbackBaseUrl === ''
|
||||
? ''
|
||||
: rtrim($callbackBaseUrl, '/') . '/' . $payOrder->pay_no . '/callback';
|
||||
|
||||
$channelResult = $plugin->pay([
|
||||
'pay_no' => (string) $payOrder->pay_no,
|
||||
'order_id' => (string) $payOrder->pay_no,
|
||||
'biz_no' => (string) $payOrder->biz_no,
|
||||
'trace_no' => (string) $payOrder->trace_no,
|
||||
'channel_request_no' => (string) $payOrder->channel_request_no,
|
||||
'merchant_id' => (int) $payOrder->merchant_id,
|
||||
'merchant_no' => (string) ($extJson['merchant_no'] ?? ''),
|
||||
'pay_type_id' => (int) $payOrder->pay_type_id,
|
||||
'pay_type_code' => (string) ($paymentType->code ?? ''),
|
||||
'amount' => (int) $payOrder->pay_amount,
|
||||
'subject' => (string) ($bizOrder->subject ?? ''),
|
||||
'body' => (string) ($bizOrder->body ?? ''),
|
||||
'callback_url' => $callbackUrl,
|
||||
'return_url' => (string) ($extJson['return_url'] ?? ''),
|
||||
'_env' => (string) (($extJson['device'] ?? '') ?: 'pc'),
|
||||
'extra' => $extJson,
|
||||
]);
|
||||
|
||||
$payOrder = $this->transactionRetry(function () use ($payOrder, $channelResult) {
|
||||
$latest = $this->payOrderRepository->findForUpdateByPayNo((string) $payOrder->pay_no);
|
||||
if (!$latest) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => (string) $payOrder->pay_no]);
|
||||
}
|
||||
|
||||
$latest->channel_order_no = (string) ($channelResult['chan_order_no'] ?? $latest->channel_order_no ?? '');
|
||||
$latest->channel_trade_no = (string) ($channelResult['chan_trade_no'] ?? $latest->channel_trade_no ?? '');
|
||||
$latest->ext_json = array_merge((array) $latest->ext_json, [
|
||||
'pay_params_type' => (string) (($channelResult['pay_params']['type'] ?? '') ?: ''),
|
||||
'pay_product' => (string) ($channelResult['pay_product'] ?? ''),
|
||||
'pay_action' => (string) ($channelResult['pay_action'] ?? ''),
|
||||
'pay_params_snapshot' => $this->normalizePayParamsSnapshot($channelResult['pay_params'] ?? []),
|
||||
]);
|
||||
$latest->save();
|
||||
|
||||
return $latest->refresh();
|
||||
});
|
||||
} catch (PaymentException $e) {
|
||||
$this->payOrderLifecycleService->markPayFailed((string) $payOrder->pay_no, [
|
||||
'channel_error_msg' => $e->getMessage(),
|
||||
'channel_error_code' => (string) $e->getCode(),
|
||||
'ext_json' => [
|
||||
'plugin_code' => (string) $payOrder->plugin_code,
|
||||
],
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
$this->payOrderLifecycleService->markPayFailed((string) $payOrder->pay_no, [
|
||||
'channel_error_msg' => $e->getMessage(),
|
||||
'channel_error_code' => 'PLUGIN_CREATE_ORDER_ERROR',
|
||||
'ext_json' => [
|
||||
'plugin_code' => (string) $payOrder->plugin_code,
|
||||
],
|
||||
]);
|
||||
|
||||
throw new PaymentException('创建第三方支付订单失败:' . $e->getMessage(), 40215);
|
||||
}
|
||||
|
||||
return [
|
||||
'pay_order' => $payOrder,
|
||||
'payment_result' => $channelResult,
|
||||
'pay_params' => $channelResult['pay_params'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化支付参数快照,便于后续页面渲染和排障。
|
||||
*/
|
||||
private function normalizePayParamsSnapshot(mixed $payParams): array
|
||||
{
|
||||
if (is_array($payParams)) {
|
||||
return $payParams;
|
||||
}
|
||||
|
||||
if (is_object($payParams) && method_exists($payParams, 'toArray')) {
|
||||
$data = $payParams->toArray();
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
140
app/service/payment/order/PayOrderFeeService.php
Normal file
140
app/service/payment/order/PayOrderFeeService.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
use app\model\payment\PayOrder;
|
||||
use app\service\account\funds\MerchantAccountService;
|
||||
|
||||
/**
|
||||
* 支付单手续费处理服务。
|
||||
*
|
||||
* 负责支付成功时的手续费结算,以及终态时的冻结手续费释放。
|
||||
*/
|
||||
class PayOrderFeeService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantAccountService $merchantAccountService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付成功后的手续费结算。
|
||||
*/
|
||||
public function settleSuccessFee(PayOrder $payOrder, int $actualFee, string $payNo, string $traceNo): void
|
||||
{
|
||||
if ((int) $payOrder->channel_type !== RouteConstant::CHANNEL_MODE_SELF) {
|
||||
return;
|
||||
}
|
||||
|
||||
$estimated = (int) $payOrder->fee_estimated_amount;
|
||||
if ($actualFee > $estimated) {
|
||||
if ($estimated > 0) {
|
||||
$this->merchantAccountService->deductFrozenAmountInCurrentTransaction(
|
||||
(int) $payOrder->merchant_id,
|
||||
$estimated,
|
||||
$payNo,
|
||||
'PAY_DEDUCT:' . $payNo,
|
||||
[
|
||||
'pay_no' => $payNo,
|
||||
'remark' => '自有通道手续费扣减',
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
|
||||
$diff = $actualFee - $estimated;
|
||||
if ($diff > 0) {
|
||||
$this->merchantAccountService->debitAvailableAmountInCurrentTransaction(
|
||||
(int) $payOrder->merchant_id,
|
||||
$diff,
|
||||
$payNo,
|
||||
'PAY_DEDUCT_DIFF:' . $payNo,
|
||||
[
|
||||
'pay_no' => $payNo,
|
||||
'remark' => '自有通道手续费差额扣减',
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($actualFee < $estimated) {
|
||||
if ($actualFee > 0) {
|
||||
$this->merchantAccountService->deductFrozenAmountInCurrentTransaction(
|
||||
(int) $payOrder->merchant_id,
|
||||
$actualFee,
|
||||
$payNo,
|
||||
'PAY_DEDUCT:' . $payNo,
|
||||
[
|
||||
'pay_no' => $payNo,
|
||||
'remark' => '自有通道手续费扣减',
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
|
||||
$diff = $estimated - $actualFee;
|
||||
if ($diff > 0) {
|
||||
$this->merchantAccountService->releaseFrozenAmountInCurrentTransaction(
|
||||
(int) $payOrder->merchant_id,
|
||||
$diff,
|
||||
$payNo,
|
||||
'PAY_RELEASE:' . $payNo,
|
||||
[
|
||||
'pay_no' => $payNo,
|
||||
'remark' => '自有通道手续费释放差额',
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($actualFee > 0) {
|
||||
$this->merchantAccountService->deductFrozenAmountInCurrentTransaction(
|
||||
(int) $payOrder->merchant_id,
|
||||
$actualFee,
|
||||
$payNo,
|
||||
'PAY_DEDUCT:' . $payNo,
|
||||
[
|
||||
'pay_no' => $payNo,
|
||||
'remark' => '自有通道手续费扣减',
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放支付单已冻结的手续费。
|
||||
*/
|
||||
public function releaseFrozenFeeIfNeeded(PayOrder $payOrder, string $payNo, string $traceNo, string $remark): void
|
||||
{
|
||||
if ((int) $payOrder->channel_type !== RouteConstant::CHANNEL_MODE_SELF) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) $payOrder->fee_status !== TradeConstant::FEE_STATUS_FROZEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->merchantAccountService->releaseFrozenAmountInCurrentTransaction(
|
||||
(int) $payOrder->merchant_id,
|
||||
(int) $payOrder->fee_estimated_amount,
|
||||
$payNo,
|
||||
'PAY_RELEASE:' . $payNo,
|
||||
[
|
||||
'pay_no' => $payNo,
|
||||
'remark' => $remark,
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
}
|
||||
322
app/service/payment/order/PayOrderLifecycleService.php
Normal file
322
app/service/payment/order/PayOrderLifecycleService.php
Normal file
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\NotifyConstant;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\model\payment\PayOrder;
|
||||
use app\repository\payment\trade\BizOrderRepository;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
|
||||
/**
|
||||
* 支付单生命周期服务。
|
||||
*
|
||||
* 负责支付单状态推进、关闭、超时和手续费处理。
|
||||
*/
|
||||
class PayOrderLifecycleService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected PayOrderFeeService $payOrderFeeService,
|
||||
protected BizOrderRepository $bizOrderRepository,
|
||||
protected PayOrderRepository $payOrderRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记支付成功。
|
||||
*
|
||||
* 用于支付回调或主动查单成功后的状态推进;自有通道在这里完成手续费正式扣减。
|
||||
*
|
||||
* @param string $payNo 支付单号
|
||||
* @param array $input 回调或查单入参
|
||||
* @return PayOrder
|
||||
*/
|
||||
public function markPaySuccess(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($payNo, $input) {
|
||||
return $this->markPaySuccessInCurrentTransaction($payNo, $input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记支付成功。
|
||||
*
|
||||
* 该方法只处理状态推进和资金动作,不负责外部通道请求。
|
||||
*
|
||||
* @param string $payNo 支付单号
|
||||
* @param array $input 回调或查单入参
|
||||
* @return PayOrder
|
||||
*/
|
||||
public function markPaySuccessInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
$payOrder = $this->payOrderRepository->findForUpdateByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $payOrder->status;
|
||||
if ($currentStatus === TradeConstant::ORDER_STATUS_SUCCESS) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isOrderTerminalStatus($currentStatus)) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (!in_array($currentStatus, TradeConstant::orderMutableStatuses(), true)) {
|
||||
throw new BusinessStateException('支付单状态不允许当前操作', [
|
||||
'pay_no' => $payNo,
|
||||
'status' => $currentStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
$actualFee = array_key_exists('fee_actual_amount', $input)
|
||||
? (int) $input['fee_actual_amount']
|
||||
: (int) $payOrder->fee_estimated_amount;
|
||||
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
|
||||
|
||||
$this->payOrderFeeService->settleSuccessFee($payOrder, $actualFee, $payNo, $traceNo);
|
||||
|
||||
$payOrder->status = TradeConstant::ORDER_STATUS_SUCCESS;
|
||||
$payOrder->paid_at = $input['paid_at'] ?? $this->now();
|
||||
$payOrder->fee_actual_amount = $actualFee;
|
||||
$payOrder->fee_status = (int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_SELF
|
||||
? TradeConstant::FEE_STATUS_DEDUCTED
|
||||
: TradeConstant::FEE_STATUS_NONE;
|
||||
$payOrder->settlement_status = (int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_COLLECT
|
||||
? TradeConstant::SETTLEMENT_STATUS_PENDING
|
||||
: TradeConstant::SETTLEMENT_STATUS_NONE;
|
||||
$payOrder->callback_status = NotifyConstant::PROCESS_STATUS_SUCCESS;
|
||||
$payOrder->channel_trade_no = (string) ($input['channel_trade_no'] ?? $payOrder->channel_trade_no ?? '');
|
||||
$payOrder->channel_order_no = (string) ($input['channel_order_no'] ?? $payOrder->channel_order_no ?? '');
|
||||
$payOrder->channel_error_code = '';
|
||||
$payOrder->channel_error_msg = '';
|
||||
$payOrder->callback_times = (int) $payOrder->callback_times + 1;
|
||||
$payOrder->ext_json = array_merge((array) $payOrder->ext_json, $input['ext_json'] ?? []);
|
||||
$payOrder->save();
|
||||
|
||||
$this->syncBizOrderAfterSuccess($payOrder, $traceNo);
|
||||
|
||||
return $payOrder->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记支付失败。
|
||||
*/
|
||||
public function markPayFailed(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($payNo, $input) {
|
||||
return $this->markPayFailedInCurrentTransaction($payNo, $input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记支付失败。
|
||||
*/
|
||||
public function markPayFailedInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
$payOrder = $this->payOrderRepository->findForUpdateByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $payOrder->status;
|
||||
if ($currentStatus === TradeConstant::ORDER_STATUS_FAILED) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isOrderTerminalStatus($currentStatus)) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (!in_array($currentStatus, TradeConstant::orderMutableStatuses(), true)) {
|
||||
throw new BusinessStateException('支付单状态不允许当前操作', [
|
||||
'pay_no' => $payNo,
|
||||
'status' => $currentStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
|
||||
$this->payOrderFeeService->releaseFrozenFeeIfNeeded($payOrder, $payNo, $traceNo, '支付失败释放手续费');
|
||||
|
||||
$payOrder->status = TradeConstant::ORDER_STATUS_FAILED;
|
||||
$payOrder->fee_status = (int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_SELF
|
||||
? TradeConstant::FEE_STATUS_RELEASED
|
||||
: TradeConstant::FEE_STATUS_NONE;
|
||||
$payOrder->settlement_status = TradeConstant::SETTLEMENT_STATUS_NONE;
|
||||
$payOrder->callback_status = NotifyConstant::PROCESS_STATUS_FAILED;
|
||||
$payOrder->channel_error_code = (string) ($input['channel_error_code'] ?? $payOrder->channel_error_code ?? '');
|
||||
$payOrder->channel_error_msg = (string) ($input['channel_error_msg'] ?? $payOrder->channel_error_msg ?? '支付失败');
|
||||
$payOrder->failed_at = $input['failed_at'] ?? $this->now();
|
||||
$payOrder->callback_times = (int) $payOrder->callback_times + 1;
|
||||
$payOrder->ext_json = array_merge((array) $payOrder->ext_json, $input['ext_json'] ?? []);
|
||||
$payOrder->save();
|
||||
|
||||
$this->syncBizOrderAfterTerminalStatus($payOrder, $payNo, $traceNo, TradeConstant::ORDER_STATUS_FAILED, 'failed_at');
|
||||
|
||||
return $payOrder->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭支付单。
|
||||
*/
|
||||
public function closePayOrder(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($payNo, $input) {
|
||||
return $this->closePayOrderInCurrentTransaction($payNo, $input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中关闭支付单。
|
||||
*/
|
||||
public function closePayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
$payOrder = $this->payOrderRepository->findForUpdateByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $payOrder->status;
|
||||
if ($currentStatus === TradeConstant::ORDER_STATUS_CLOSED) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isOrderTerminalStatus($currentStatus)) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (!in_array($currentStatus, TradeConstant::orderMutableStatuses(), true)) {
|
||||
throw new BusinessStateException('支付单状态不允许当前操作', [
|
||||
'pay_no' => $payNo,
|
||||
'status' => $currentStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
|
||||
$this->payOrderFeeService->releaseFrozenFeeIfNeeded($payOrder, $payNo, $traceNo, '支付关闭释放手续费');
|
||||
|
||||
$payOrder->status = TradeConstant::ORDER_STATUS_CLOSED;
|
||||
$payOrder->fee_status = (int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_SELF
|
||||
? TradeConstant::FEE_STATUS_RELEASED
|
||||
: TradeConstant::FEE_STATUS_NONE;
|
||||
$payOrder->settlement_status = TradeConstant::SETTLEMENT_STATUS_NONE;
|
||||
$payOrder->closed_at = $input['closed_at'] ?? $this->now();
|
||||
$extJson = (array) $payOrder->ext_json;
|
||||
$reason = trim((string) ($input['reason'] ?? ''));
|
||||
if ($reason !== '') {
|
||||
$extJson['close_reason'] = $reason;
|
||||
}
|
||||
$payOrder->ext_json = array_merge($extJson, $input['ext_json'] ?? []);
|
||||
$payOrder->save();
|
||||
|
||||
$this->syncBizOrderAfterTerminalStatus($payOrder, $payNo, $traceNo, TradeConstant::ORDER_STATUS_CLOSED, 'closed_at');
|
||||
|
||||
return $payOrder->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记支付超时。
|
||||
*/
|
||||
public function timeoutPayOrder(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($payNo, $input) {
|
||||
return $this->timeoutPayOrderInCurrentTransaction($payNo, $input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记支付超时。
|
||||
*/
|
||||
public function timeoutPayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
$payOrder = $this->payOrderRepository->findForUpdateByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $payOrder->status;
|
||||
if ($currentStatus === TradeConstant::ORDER_STATUS_TIMEOUT) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isOrderTerminalStatus($currentStatus)) {
|
||||
return $payOrder;
|
||||
}
|
||||
|
||||
if (!in_array($currentStatus, TradeConstant::orderMutableStatuses(), true)) {
|
||||
throw new BusinessStateException('支付单状态不允许当前操作', [
|
||||
'pay_no' => $payNo,
|
||||
'status' => $currentStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
|
||||
$this->payOrderFeeService->releaseFrozenFeeIfNeeded($payOrder, $payNo, $traceNo, '支付超时释放手续费');
|
||||
|
||||
$payOrder->status = TradeConstant::ORDER_STATUS_TIMEOUT;
|
||||
$payOrder->fee_status = (int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_SELF
|
||||
? TradeConstant::FEE_STATUS_RELEASED
|
||||
: TradeConstant::FEE_STATUS_NONE;
|
||||
$payOrder->settlement_status = TradeConstant::SETTLEMENT_STATUS_NONE;
|
||||
$payOrder->timeout_at = $input['timeout_at'] ?? $this->now();
|
||||
$extJson = (array) $payOrder->ext_json;
|
||||
$reason = trim((string) ($input['reason'] ?? ''));
|
||||
if ($reason !== '') {
|
||||
$extJson['timeout_reason'] = $reason;
|
||||
}
|
||||
$payOrder->ext_json = array_merge($extJson, $input['ext_json'] ?? []);
|
||||
$payOrder->save();
|
||||
|
||||
$this->syncBizOrderAfterTerminalStatus($payOrder, $payNo, $traceNo, TradeConstant::ORDER_STATUS_TIMEOUT, 'timeout_at');
|
||||
|
||||
return $payOrder->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步支付成功后的业务单状态。
|
||||
*/
|
||||
private function syncBizOrderAfterSuccess(PayOrder $payOrder, string $traceNo): void
|
||||
{
|
||||
$bizOrder = $this->bizOrderRepository->findForUpdateByBizNo((string) $payOrder->biz_no);
|
||||
if (!$bizOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bizOrder->status = TradeConstant::ORDER_STATUS_SUCCESS;
|
||||
$bizOrder->paid_amount = (int) $bizOrder->paid_amount + (int) $payOrder->pay_amount;
|
||||
$bizOrder->active_pay_no = null;
|
||||
$bizOrder->paid_at = $payOrder->paid_at;
|
||||
if (empty($bizOrder->trace_no)) {
|
||||
$bizOrder->trace_no = $traceNo;
|
||||
}
|
||||
$bizOrder->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步支付终态后的业务单状态。
|
||||
*/
|
||||
private function syncBizOrderAfterTerminalStatus(PayOrder $payOrder, string $payNo, string $traceNo, int $status, string $timestampField): void
|
||||
{
|
||||
$bizOrder = $this->bizOrderRepository->findForUpdateByBizNo((string) $payOrder->biz_no);
|
||||
if (!$bizOrder || (string) $bizOrder->active_pay_no !== $payNo) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bizOrder->status = $status;
|
||||
$bizOrder->active_pay_no = null;
|
||||
$bizOrder->{$timestampField} = $payOrder->{$timestampField};
|
||||
if (empty($bizOrder->trace_no)) {
|
||||
$bizOrder->trace_no = $traceNo;
|
||||
}
|
||||
$bizOrder->save();
|
||||
}
|
||||
|
||||
}
|
||||
253
app/service/payment/order/PayOrderQueryService.php
Normal file
253
app/service/payment/order/PayOrderQueryService.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\payment\PayOrder;
|
||||
use app\model\payment\PaymentType;
|
||||
use app\repository\account\ledger\MerchantAccountLedgerRepository;
|
||||
use app\repository\payment\config\PaymentTypeRepository;
|
||||
use app\repository\payment\trade\BizOrderRepository;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
|
||||
/**
|
||||
* 支付单查询服务。
|
||||
*
|
||||
* 只负责支付单列表类查询与展示格式化,不承载状态推进逻辑。
|
||||
*/
|
||||
class PayOrderQueryService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected BizOrderRepository $bizOrderRepository,
|
||||
protected MerchantAccountLedgerRepository $merchantAccountLedgerRepository,
|
||||
protected PaymentTypeRepository $paymentTypeRepository,
|
||||
protected PayOrderReportService $payOrderReportService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询支付订单列表。
|
||||
*
|
||||
* 后台和商户后台共用同一套查询逻辑,商户侧会额外限制当前商户 ID。
|
||||
*
|
||||
* @param array $filters 查询条件
|
||||
* @param int $page 页码
|
||||
* @param int $pageSize 每页条数
|
||||
* @param int|null $merchantId 商户侧强制限定的商户 ID
|
||||
* @return array{list:array,total:int,page:int,size:int,pay_types:array}
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
|
||||
{
|
||||
$query = $this->payOrderRepository->query()
|
||||
->from('ma_pay_order as po')
|
||||
->leftJoin('ma_biz_order as bo', 'bo.biz_no', '=', 'po.biz_no')
|
||||
->leftJoin('ma_merchant as m', 'm.id', '=', 'po.merchant_id')
|
||||
->leftJoin('ma_merchant_group as g', 'g.id', '=', 'po.merchant_group_id')
|
||||
->leftJoin('ma_payment_channel as c', 'c.id', '=', 'po.channel_id')
|
||||
->leftJoin('ma_payment_type as t', 't.id', '=', 'po.pay_type_id')
|
||||
->select([
|
||||
'po.id',
|
||||
'po.pay_no',
|
||||
'po.biz_no',
|
||||
'po.trace_no',
|
||||
'po.merchant_id',
|
||||
'po.merchant_group_id',
|
||||
'po.poll_group_id',
|
||||
'po.attempt_no',
|
||||
'po.channel_id',
|
||||
'po.pay_type_id',
|
||||
'po.plugin_code',
|
||||
'po.channel_type',
|
||||
'po.channel_mode',
|
||||
'po.pay_amount',
|
||||
'po.fee_rate_bp_snapshot',
|
||||
'po.split_rate_bp_snapshot',
|
||||
'po.fee_estimated_amount',
|
||||
'po.fee_actual_amount',
|
||||
'po.status',
|
||||
'po.fee_status',
|
||||
'po.settlement_status',
|
||||
'po.channel_request_no',
|
||||
'po.channel_order_no',
|
||||
'po.channel_trade_no',
|
||||
'po.channel_error_code',
|
||||
'po.channel_error_msg',
|
||||
'po.request_at',
|
||||
'po.paid_at',
|
||||
'po.expire_at',
|
||||
'po.closed_at',
|
||||
'po.failed_at',
|
||||
'po.timeout_at',
|
||||
'po.callback_status',
|
||||
'po.callback_times',
|
||||
'po.ext_json',
|
||||
'po.created_at',
|
||||
'po.updated_at',
|
||||
'bo.merchant_order_no',
|
||||
'bo.subject',
|
||||
'bo.body',
|
||||
'bo.order_amount as biz_order_amount',
|
||||
'bo.paid_amount as biz_paid_amount',
|
||||
'bo.refund_amount as biz_refund_amount',
|
||||
'bo.status as biz_status',
|
||||
'bo.active_pay_no',
|
||||
'bo.attempt_count as biz_attempt_count',
|
||||
'bo.expire_at as biz_expire_at',
|
||||
'bo.paid_at as biz_paid_at',
|
||||
'bo.closed_at as biz_closed_at',
|
||||
'bo.failed_at as biz_failed_at',
|
||||
'bo.timeout_at as biz_timeout_at',
|
||||
'bo.ext_json as biz_ext_json',
|
||||
'm.merchant_no',
|
||||
'm.merchant_name',
|
||||
'm.merchant_short_name',
|
||||
'g.group_name as merchant_group_name',
|
||||
'c.name as channel_name',
|
||||
'c.plugin_code as channel_plugin_code',
|
||||
't.code as pay_type_code',
|
||||
't.name as pay_type_name',
|
||||
't.icon as pay_type_icon',
|
||||
]);
|
||||
|
||||
if ($merchantId !== null && $merchantId > 0) {
|
||||
$query->where('po.merchant_id', $merchantId);
|
||||
}
|
||||
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($builder) use ($keyword) {
|
||||
$builder->where('po.pay_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('po.biz_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('po.trace_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('po.channel_request_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('po.channel_order_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('po.channel_trade_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('bo.merchant_order_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('bo.subject', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_short_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('g.group_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('c.name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('t.name', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (($merchantFilter = (int) ($filters['merchant_id'] ?? 0)) > 0) {
|
||||
$query->where('po.merchant_id', $merchantFilter);
|
||||
}
|
||||
|
||||
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
|
||||
$query->where('po.pay_type_id', $payTypeId);
|
||||
}
|
||||
|
||||
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
|
||||
$query->where('po.status', (int) $filters['status']);
|
||||
}
|
||||
|
||||
if (array_key_exists('channel_mode', $filters) && $filters['channel_mode'] !== '') {
|
||||
$query->where('po.channel_mode', (int) $filters['channel_mode']);
|
||||
}
|
||||
|
||||
if (array_key_exists('callback_status', $filters) && $filters['callback_status'] !== '') {
|
||||
$query->where('po.callback_status', (int) $filters['callback_status']);
|
||||
}
|
||||
|
||||
$paginator = $query
|
||||
->orderByDesc('po.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
|
||||
$list = [];
|
||||
foreach ($paginator->items() as $item) {
|
||||
$list[] = $this->payOrderReportService->formatPayOrderRow((array) $item);
|
||||
}
|
||||
|
||||
return [
|
||||
'list' => $list,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage(),
|
||||
'pay_types' => $this->payTypeOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单详情。
|
||||
*
|
||||
* @param string $payNo 支付单号
|
||||
* @param int|null $merchantId 商户侧强制限定的商户 ID
|
||||
* @return array{pay_order:mixed,biz_order:mixed,timeline:array,account_ledgers:mixed}
|
||||
*/
|
||||
public function detail(string $payNo, ?int $merchantId = null): array
|
||||
{
|
||||
$payNo = trim($payNo);
|
||||
if ($payNo === '') {
|
||||
throw new ValidationException('pay_no 不能为空');
|
||||
}
|
||||
|
||||
$payOrder = $this->payOrderRepository->findByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
if ($merchantId !== null && $merchantId > 0 && (int) $payOrder->merchant_id !== $merchantId) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
$bizOrder = $this->bizOrderRepository->findByBizNo((string) $payOrder->biz_no);
|
||||
$timeline = $this->payOrderReportService->buildPayTimeline($payOrder);
|
||||
$accountLedgers = $this->loadPayLedgers($payOrder);
|
||||
|
||||
return [
|
||||
'pay_order' => $payOrder,
|
||||
'biz_order' => $bizOrder,
|
||||
'timeline' => $timeline,
|
||||
'account_ledgers' => $accountLedgers,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载支付相关资金流水。
|
||||
*/
|
||||
private function loadPayLedgers(PayOrder $payOrder)
|
||||
{
|
||||
$traceNo = trim((string) ($payOrder->trace_no ?: $payOrder->biz_no));
|
||||
$ledgers = $traceNo !== ''
|
||||
? $this->merchantAccountLedgerRepository->listByTraceNo($traceNo)
|
||||
: collect();
|
||||
|
||||
if ($ledgers->isEmpty()) {
|
||||
$ledgers = $this->merchantAccountLedgerRepository->listByBizNo((string) $payOrder->pay_no);
|
||||
}
|
||||
|
||||
return $ledgers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回启用的支付方式选项,供列表筛选使用。
|
||||
*/
|
||||
private function payTypeOptions(): array
|
||||
{
|
||||
return $this->paymentTypeRepository->query()
|
||||
->where('status', CommonConstant::STATUS_ENABLED)
|
||||
->orderBy('sort_no')
|
||||
->orderByDesc('id')
|
||||
->get(['id', 'name'])
|
||||
->map(function (PaymentType $payType): array {
|
||||
return [
|
||||
'label' => (string) $payType->name,
|
||||
'value' => (int) $payType->id,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
92
app/service/payment/order/PayOrderReportService.php
Normal file
92
app/service/payment/order/PayOrderReportService.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\NotifyConstant;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
use app\model\payment\PayOrder;
|
||||
|
||||
/**
|
||||
* 支付单结果组装服务。
|
||||
*
|
||||
* 负责支付单列表和详情页的展示字段格式化。
|
||||
*/
|
||||
class PayOrderReportService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 格式化支付订单行,统一输出前端需要的中文字段。
|
||||
*/
|
||||
public function formatPayOrderRow(array $row): array
|
||||
{
|
||||
$row['merchant_group_name'] = trim((string) ($row['merchant_group_name'] ?? '')) ?: '未分组';
|
||||
$row['merchant_name'] = trim((string) ($row['merchant_name'] ?? '')) ?: '未知商户';
|
||||
$row['merchant_short_name'] = trim((string) ($row['merchant_short_name'] ?? ''));
|
||||
$row['pay_type_name'] = trim((string) ($row['pay_type_name'] ?? '')) ?: '未知方式';
|
||||
$row['channel_name'] = trim((string) ($row['channel_name'] ?? '')) ?: '未知通道';
|
||||
$row['biz_status_text'] = $this->textFromMap((int) ($row['biz_status'] ?? -1), TradeConstant::orderStatusMap());
|
||||
|
||||
$row['status_text'] = $this->textFromMap((int) ($row['status'] ?? -1), TradeConstant::orderStatusMap());
|
||||
$row['fee_status_text'] = $this->textFromMap((int) ($row['fee_status'] ?? -1), TradeConstant::feeStatusMap());
|
||||
$row['settlement_status_text'] = $this->textFromMap((int) ($row['settlement_status'] ?? -1), TradeConstant::settlementStatusMap());
|
||||
$row['callback_status_text'] = $this->textFromMap((int) ($row['callback_status'] ?? -1), NotifyConstant::processStatusMap());
|
||||
$row['channel_type_text'] = $this->textFromMap((int) ($row['channel_type'] ?? -1), RouteConstant::channelTypeMap());
|
||||
$row['channel_mode_text'] = $this->textFromMap((int) ($row['channel_mode'] ?? -1), RouteConstant::channelModeMap());
|
||||
|
||||
$row['pay_amount_text'] = $this->formatAmount((int) ($row['pay_amount'] ?? 0));
|
||||
$row['fee_estimated_amount_text'] = $this->formatAmount((int) ($row['fee_estimated_amount'] ?? 0));
|
||||
$row['fee_actual_amount_text'] = $this->formatAmount((int) ($row['fee_actual_amount'] ?? 0));
|
||||
$row['biz_order_amount_text'] = $this->formatAmount((int) ($row['biz_order_amount'] ?? 0));
|
||||
$row['biz_paid_amount_text'] = $this->formatAmount((int) ($row['biz_paid_amount'] ?? 0));
|
||||
$row['biz_refund_amount_text'] = $this->formatAmount((int) ($row['biz_refund_amount'] ?? 0));
|
||||
|
||||
$row['request_at_text'] = $this->formatDateTime($row['request_at'] ?? null, '—');
|
||||
$row['paid_at_text'] = $this->formatDateTime($row['paid_at'] ?? null, '—');
|
||||
$row['expire_at_text'] = $this->formatDateTime($row['expire_at'] ?? null, '—');
|
||||
$row['closed_at_text'] = $this->formatDateTime($row['closed_at'] ?? null, '—');
|
||||
$row['failed_at_text'] = $this->formatDateTime($row['failed_at'] ?? null, '—');
|
||||
$row['timeout_at_text'] = $this->formatDateTime($row['timeout_at'] ?? null, '—');
|
||||
$row['biz_expire_at_text'] = $this->formatDateTime($row['biz_expire_at'] ?? null, '—');
|
||||
$row['biz_paid_at_text'] = $this->formatDateTime($row['biz_paid_at'] ?? null, '—');
|
||||
$row['biz_closed_at_text'] = $this->formatDateTime($row['biz_closed_at'] ?? null, '—');
|
||||
$row['biz_failed_at_text'] = $this->formatDateTime($row['biz_failed_at'] ?? null, '—');
|
||||
$row['biz_timeout_at_text'] = $this->formatDateTime($row['biz_timeout_at'] ?? null, '—');
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造支付时间线。
|
||||
*/
|
||||
public function buildPayTimeline(PayOrder $payOrder): array
|
||||
{
|
||||
$extJson = (array) ($payOrder->ext_json ?? []);
|
||||
|
||||
return array_values(array_filter([
|
||||
[
|
||||
'status' => 'created',
|
||||
'at' => $this->formatDateTime($payOrder->request_at ?? $payOrder->created_at ?? null, '—'),
|
||||
],
|
||||
$payOrder->paid_at ? [
|
||||
'status' => 'success',
|
||||
'at' => $this->formatDateTime($payOrder->paid_at, '—'),
|
||||
] : null,
|
||||
$payOrder->closed_at ? [
|
||||
'status' => 'closed',
|
||||
'at' => $this->formatDateTime($payOrder->closed_at, '—'),
|
||||
'reason' => (string) ($extJson['close_reason'] ?? ''),
|
||||
] : null,
|
||||
$payOrder->failed_at ? [
|
||||
'status' => 'failed',
|
||||
'at' => $this->formatDateTime($payOrder->failed_at, '—'),
|
||||
'reason' => (string) ($payOrder->channel_error_msg ?: ($extJson['reason'] ?? '')),
|
||||
] : null,
|
||||
$payOrder->timeout_at ? [
|
||||
'status' => 'timeout',
|
||||
'at' => $this->formatDateTime($payOrder->timeout_at, '—'),
|
||||
'reason' => (string) ($extJson['timeout_reason'] ?? ''),
|
||||
] : null,
|
||||
]));
|
||||
}
|
||||
}
|
||||
131
app/service/payment/order/PayOrderService.php
Normal file
131
app/service/payment/order/PayOrderService.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\model\payment\PayOrder;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 支付单门面服务。
|
||||
*
|
||||
* 对外保留原有调用契约,内部委托给查询、发起、生命周期和回调四个子服务。
|
||||
*/
|
||||
class PayOrderService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected PayOrderQueryService $queryService,
|
||||
protected PayOrderAttemptService $attemptService,
|
||||
protected PayOrderLifecycleService $lifecycleService,
|
||||
protected PayOrderCallbackService $callbackService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询支付订单列表。
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
|
||||
{
|
||||
return $this->queryService->paginate($filters, $page, $pageSize, $merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单详情。
|
||||
*/
|
||||
public function detail(string $payNo, ?int $merchantId = null): array
|
||||
{
|
||||
return $this->queryService->detail($payNo, $merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预创建支付尝试。
|
||||
*/
|
||||
public function preparePayAttempt(array $input): array
|
||||
{
|
||||
return $this->attemptService->preparePayAttempt($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记支付成功。
|
||||
*/
|
||||
public function markPaySuccess(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->markPaySuccess($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记支付成功。
|
||||
*/
|
||||
public function markPaySuccessInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->markPaySuccessInCurrentTransaction($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记支付失败。
|
||||
*/
|
||||
public function markPayFailed(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->markPayFailed($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记支付失败。
|
||||
*/
|
||||
public function markPayFailedInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->markPayFailedInCurrentTransaction($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭支付单。
|
||||
*/
|
||||
public function closePayOrder(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->closePayOrder($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中关闭支付单。
|
||||
*/
|
||||
public function closePayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->closePayOrderInCurrentTransaction($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记支付超时。
|
||||
*/
|
||||
public function timeoutPayOrder(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->timeoutPayOrder($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记支付超时。
|
||||
*/
|
||||
public function timeoutPayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
|
||||
{
|
||||
return $this->lifecycleService->timeoutPayOrderInCurrentTransaction($payNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理渠道回调。
|
||||
*/
|
||||
public function handleChannelCallback(array $input): PayOrder
|
||||
{
|
||||
return $this->callbackService->handleChannelCallback($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按支付单号处理真实第三方回调。
|
||||
*/
|
||||
public function handlePluginCallback(string $payNo, Request $request): string|Response
|
||||
{
|
||||
return $this->callbackService->handlePluginCallback($payNo, $request);
|
||||
}
|
||||
}
|
||||
116
app/service/payment/order/RefundCreationService.php
Normal file
116
app/service/payment/order/RefundCreationService.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ConflictException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\payment\RefundOrder;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
use app\repository\payment\trade\RefundOrderRepository;
|
||||
|
||||
/**
|
||||
* 退款单创建服务。
|
||||
*
|
||||
* 负责退款单创建和幂等校验,不承载状态推进逻辑。
|
||||
*/
|
||||
class RefundCreationService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected RefundOrderRepository $refundOrderRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建退款单。
|
||||
*
|
||||
* 当前仅支持整单全额退款,且同一支付单只能创建一张退款单。
|
||||
*
|
||||
* @param array $input 退款请求参数
|
||||
* @return RefundOrder
|
||||
*/
|
||||
public function createRefund(array $input): RefundOrder
|
||||
{
|
||||
$payNo = trim((string) ($input['pay_no'] ?? ''));
|
||||
if ($payNo === '') {
|
||||
throw new ValidationException('pay_no 不能为空');
|
||||
}
|
||||
|
||||
$payOrder = $this->payOrderRepository->findByPayNo($payNo);
|
||||
if (!$payOrder) {
|
||||
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
if ((int) $payOrder->status !== TradeConstant::ORDER_STATUS_SUCCESS) {
|
||||
throw new BusinessStateException('订单状态不允许退款', [
|
||||
'pay_no' => $payNo,
|
||||
'status' => (int) $payOrder->status,
|
||||
]);
|
||||
}
|
||||
|
||||
$refundAmount = array_key_exists('refund_amount', $input)
|
||||
? (int) $input['refund_amount']
|
||||
: (int) $payOrder->pay_amount;
|
||||
|
||||
if ($refundAmount !== (int) $payOrder->pay_amount) {
|
||||
throw new BusinessStateException('当前仅支持整单全额退款');
|
||||
}
|
||||
|
||||
$merchantRefundNo = trim((string) ($input['merchant_refund_no'] ?? ''));
|
||||
if ($merchantRefundNo !== '') {
|
||||
$existingByMerchantNo = $this->refundOrderRepository->findByMerchantRefundNo((int) $payOrder->merchant_id, $merchantRefundNo);
|
||||
if ($existingByMerchantNo) {
|
||||
if ((string) $existingByMerchantNo->pay_no !== $payNo || (int) $existingByMerchantNo->refund_amount !== $refundAmount) {
|
||||
throw new ConflictException('幂等冲突', [
|
||||
'refund_no' => (string) $existingByMerchantNo->refund_no,
|
||||
'pay_no' => (string) $existingByMerchantNo->pay_no,
|
||||
'merchant_refund_no' => $merchantRefundNo,
|
||||
]);
|
||||
}
|
||||
|
||||
return $existingByMerchantNo;
|
||||
}
|
||||
}
|
||||
|
||||
if ($existingByPayNo = $this->refundOrderRepository->findByPayNo($payNo)) {
|
||||
if ($merchantRefundNo !== '' && (string) $existingByPayNo->merchant_refund_no !== $merchantRefundNo) {
|
||||
throw new ConflictException('重复退款', ['pay_no' => $payNo]);
|
||||
}
|
||||
|
||||
return $existingByPayNo;
|
||||
}
|
||||
|
||||
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
|
||||
|
||||
return $this->refundOrderRepository->create([
|
||||
'refund_no' => $this->generateNo('RFD'),
|
||||
'merchant_id' => (int) $payOrder->merchant_id,
|
||||
'merchant_group_id' => (int) $payOrder->merchant_group_id,
|
||||
'biz_no' => (string) $payOrder->biz_no,
|
||||
'trace_no' => $traceNo,
|
||||
'pay_no' => $payNo,
|
||||
'merchant_refund_no' => $merchantRefundNo !== '' ? $merchantRefundNo : $this->generateNo('MRF'),
|
||||
'channel_id' => (int) $payOrder->channel_id,
|
||||
'refund_amount' => $refundAmount,
|
||||
'fee_reverse_amount' => (int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_COLLECT ? (int) $payOrder->fee_actual_amount : 0,
|
||||
'status' => TradeConstant::REFUND_STATUS_CREATED,
|
||||
'channel_request_no' => $this->generateNo('RQR'),
|
||||
'reason' => (string) ($input['reason'] ?? ''),
|
||||
'request_at' => $this->now(),
|
||||
'processing_at' => null,
|
||||
'retry_count' => 0,
|
||||
'last_error' => '',
|
||||
'ext_json' => array_merge($input['ext_json'] ?? [], [
|
||||
'trace_no' => $traceNo,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
251
app/service/payment/order/RefundLifecycleService.php
Normal file
251
app/service/payment/order/RefundLifecycleService.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\model\payment\RefundOrder;
|
||||
use app\repository\payment\trade\BizOrderRepository;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
use app\repository\payment\trade\RefundOrderRepository;
|
||||
use app\service\account\funds\MerchantAccountService;
|
||||
|
||||
/**
|
||||
* 退款单生命周期服务。
|
||||
*
|
||||
* 负责退款单创建、处理中、成功、失败和重试等状态推进。
|
||||
*/
|
||||
class RefundLifecycleService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected BizOrderRepository $bizOrderRepository,
|
||||
protected RefundOrderRepository $refundOrderRepository,
|
||||
protected MerchantAccountService $merchantAccountService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记退款处理中。
|
||||
*/
|
||||
public function markRefundProcessing(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($refundNo, $input) {
|
||||
return $this->markRefundProcessingInCurrentTransaction($refundNo, $input, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款重试。
|
||||
*/
|
||||
public function retryRefund(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($refundNo, $input) {
|
||||
return $this->markRefundProcessingInCurrentTransaction($refundNo, $input, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记退款处理中或重试。
|
||||
*/
|
||||
public function markRefundProcessingInCurrentTransaction(string $refundNo, array $input = [], bool $isRetry = false): RefundOrder
|
||||
{
|
||||
$refundOrder = $this->refundOrderRepository->findForUpdateByRefundNo($refundNo);
|
||||
if (!$refundOrder) {
|
||||
throw new ResourceNotFoundException('退款单不存在', ['refund_no' => $refundNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $refundOrder->status;
|
||||
if ($currentStatus === TradeConstant::REFUND_STATUS_PROCESSING) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isRefundTerminalStatus($currentStatus)) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if ($currentStatus !== TradeConstant::REFUND_STATUS_CREATED && $currentStatus !== TradeConstant::REFUND_STATUS_FAILED) {
|
||||
throw new BusinessStateException('退款单状态不允许当前操作', [
|
||||
'refund_no' => $refundNo,
|
||||
'status' => $currentStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($currentStatus === TradeConstant::REFUND_STATUS_FAILED && !$isRetry) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if ($isRetry && $currentStatus !== TradeConstant::REFUND_STATUS_FAILED) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
$refundOrder->status = TradeConstant::REFUND_STATUS_PROCESSING;
|
||||
$refundOrder->processing_at = $input['processing_at'] ?? $this->now();
|
||||
if (empty($refundOrder->request_at)) {
|
||||
$refundOrder->request_at = $input['request_at'] ?? $refundOrder->processing_at;
|
||||
}
|
||||
$refundOrder->last_error = (string) ($input['last_error'] ?? $refundOrder->last_error ?? '');
|
||||
if ($isRetry) {
|
||||
$refundOrder->retry_count = (int) $refundOrder->retry_count + 1;
|
||||
$refundOrder->channel_request_no = $this->generateNo('RQR');
|
||||
}
|
||||
|
||||
$extJson = (array) $refundOrder->ext_json;
|
||||
$reason = trim((string) ($input['reason'] ?? ''));
|
||||
if ($reason !== '') {
|
||||
$extJson[$isRetry ? 'retry_reason' : 'processing_reason'] = $reason;
|
||||
}
|
||||
$refundOrder->ext_json = array_merge($extJson, $input['ext_json'] ?? []);
|
||||
$refundOrder->save();
|
||||
|
||||
return $refundOrder->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款成功。
|
||||
*
|
||||
* 成功后会推进退款单状态,并在平台代收场景下做余额冲减或结算逆向处理。
|
||||
*
|
||||
* @param string $refundNo 退款单号
|
||||
* @param array $input 回调或查单入参
|
||||
* @return RefundOrder
|
||||
*/
|
||||
public function markRefundSuccess(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($refundNo, $input) {
|
||||
return $this->markRefundSuccessInCurrentTransaction($refundNo, $input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记退款成功。
|
||||
*
|
||||
* @param string $refundNo 退款单号
|
||||
* @param array $input 回调或查单入参
|
||||
* @return RefundOrder
|
||||
*/
|
||||
public function markRefundSuccessInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
$refundOrder = $this->refundOrderRepository->findForUpdateByRefundNo($refundNo);
|
||||
if (!$refundOrder) {
|
||||
throw new ResourceNotFoundException('退款单不存在', ['refund_no' => $refundNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $refundOrder->status;
|
||||
if ($currentStatus === TradeConstant::REFUND_STATUS_SUCCESS) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if ($currentStatus === TradeConstant::REFUND_STATUS_FAILED) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isRefundTerminalStatus($currentStatus)) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
$payOrder = $this->payOrderRepository->findForUpdateByPayNo((string) $refundOrder->pay_no);
|
||||
if (!$payOrder || (int) $payOrder->status !== TradeConstant::ORDER_STATUS_SUCCESS) {
|
||||
throw new BusinessStateException('原支付单状态不允许退款', [
|
||||
'refund_no' => $refundNo,
|
||||
'pay_no' => (string) $refundOrder->pay_no,
|
||||
]);
|
||||
}
|
||||
|
||||
$traceNo = (string) ($refundOrder->trace_no ?: $refundOrder->biz_no);
|
||||
if ((int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_COLLECT) {
|
||||
$reverseAmount = max(0, (int) $payOrder->pay_amount - (int) $payOrder->fee_actual_amount);
|
||||
if ((int) $payOrder->settlement_status === TradeConstant::SETTLEMENT_STATUS_SETTLED && $reverseAmount > 0) {
|
||||
$this->merchantAccountService->debitAvailableAmountInCurrentTransaction(
|
||||
(int) $refundOrder->merchant_id,
|
||||
$reverseAmount,
|
||||
(string) $refundOrder->refund_no,
|
||||
'REFUND_REVERSE:' . (string) $refundOrder->refund_no,
|
||||
[
|
||||
'pay_no' => (string) $refundOrder->pay_no,
|
||||
'remark' => '平台代收退款冲减',
|
||||
],
|
||||
$traceNo
|
||||
);
|
||||
}
|
||||
|
||||
$payOrder->settlement_status = TradeConstant::SETTLEMENT_STATUS_REVERSED;
|
||||
$payOrder->save();
|
||||
}
|
||||
|
||||
$refundOrder->status = TradeConstant::REFUND_STATUS_SUCCESS;
|
||||
$refundOrder->succeeded_at = $input['succeeded_at'] ?? $this->now();
|
||||
$refundOrder->channel_refund_no = (string) ($input['channel_refund_no'] ?? $refundOrder->channel_refund_no ?? '');
|
||||
$refundOrder->last_error = '';
|
||||
$refundOrder->ext_json = array_merge((array) $refundOrder->ext_json, $input['ext_json'] ?? []);
|
||||
$refundOrder->save();
|
||||
|
||||
$bizOrder = $this->bizOrderRepository->findForUpdateByBizNo((string) $refundOrder->biz_no);
|
||||
if ($bizOrder) {
|
||||
$bizOrder->refund_amount = (int) $bizOrder->order_amount;
|
||||
if (empty($bizOrder->trace_no)) {
|
||||
$bizOrder->trace_no = $traceNo;
|
||||
}
|
||||
$bizOrder->save();
|
||||
}
|
||||
|
||||
return $refundOrder->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款失败。
|
||||
*/
|
||||
public function markRefundFailed(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->transactionRetry(function () use ($refundNo, $input) {
|
||||
return $this->markRefundFailedInCurrentTransaction($refundNo, $input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记退款失败。
|
||||
*/
|
||||
public function markRefundFailedInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
$refundOrder = $this->refundOrderRepository->findForUpdateByRefundNo($refundNo);
|
||||
if (!$refundOrder) {
|
||||
throw new ResourceNotFoundException('退款单不存在', ['refund_no' => $refundNo]);
|
||||
}
|
||||
|
||||
$currentStatus = (int) $refundOrder->status;
|
||||
if ($currentStatus === TradeConstant::REFUND_STATUS_FAILED) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if (TradeConstant::isRefundTerminalStatus($currentStatus)) {
|
||||
return $refundOrder;
|
||||
}
|
||||
|
||||
if ($currentStatus !== TradeConstant::REFUND_STATUS_CREATED && $currentStatus !== TradeConstant::REFUND_STATUS_PROCESSING) {
|
||||
throw new BusinessStateException('退款单状态不允许当前操作', [
|
||||
'refund_no' => $refundNo,
|
||||
'status' => $currentStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
$refundOrder->status = TradeConstant::REFUND_STATUS_FAILED;
|
||||
$refundOrder->failed_at = $input['failed_at'] ?? $this->now();
|
||||
$refundOrder->channel_refund_no = (string) ($input['channel_refund_no'] ?? $refundOrder->channel_refund_no ?? '');
|
||||
$refundOrder->last_error = (string) ($input['last_error'] ?? $refundOrder->last_error ?? '');
|
||||
$extJson = (array) $refundOrder->ext_json;
|
||||
$reason = trim((string) ($input['reason'] ?? ''));
|
||||
if ($reason !== '') {
|
||||
$extJson['fail_reason'] = $reason;
|
||||
}
|
||||
$refundOrder->ext_json = array_merge($extJson, $input['ext_json'] ?? []);
|
||||
$refundOrder->save();
|
||||
|
||||
return $refundOrder->refresh();
|
||||
}
|
||||
}
|
||||
285
app/service/payment/order/RefundQueryService.php
Normal file
285
app/service/payment/order/RefundQueryService.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\repository\account\ledger\MerchantAccountLedgerRepository;
|
||||
use app\repository\payment\config\PaymentTypeRepository;
|
||||
use app\repository\payment\trade\RefundOrderRepository;
|
||||
|
||||
/**
|
||||
* 退款单查询服务。
|
||||
*
|
||||
* 只负责退款列表、详情和数据查询,不承载退款状态推进逻辑。
|
||||
*/
|
||||
class RefundQueryService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected RefundOrderRepository $refundOrderRepository,
|
||||
protected MerchantAccountLedgerRepository $merchantAccountLedgerRepository,
|
||||
protected PaymentTypeRepository $paymentTypeRepository,
|
||||
protected RefundReportService $refundReportService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询退款订单列表。
|
||||
*
|
||||
* @param array $filters 查询条件
|
||||
* @param int $page 页码
|
||||
* @param int $pageSize 每页条数
|
||||
* @param int|null $merchantId 商户侧强制限定的商户 ID
|
||||
* @return array{list:array,total:int,page:int,size:int,pay_types:array}
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
|
||||
{
|
||||
$query = $this->buildRefundOrderQuery($merchantId);
|
||||
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($builder) use ($keyword) {
|
||||
$builder->where('ro.refund_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.pay_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.biz_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.trace_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.merchant_refund_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.channel_request_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.channel_refund_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.reason', 'like', '%' . $keyword . '%')
|
||||
->orWhere('ro.last_error', 'like', '%' . $keyword . '%')
|
||||
->orWhere('bo.merchant_order_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('bo.subject', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_short_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('g.group_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('c.name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('t.name', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (($merchantFilter = (int) ($filters['merchant_id'] ?? 0)) > 0) {
|
||||
$query->where('ro.merchant_id', $merchantFilter);
|
||||
}
|
||||
|
||||
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
|
||||
$query->where('po.pay_type_id', $payTypeId);
|
||||
}
|
||||
|
||||
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
|
||||
$query->where('ro.status', (int) $filters['status']);
|
||||
}
|
||||
|
||||
if (array_key_exists('channel_mode', $filters) && $filters['channel_mode'] !== '') {
|
||||
$query->where('po.channel_mode', (int) $filters['channel_mode']);
|
||||
}
|
||||
|
||||
$paginator = $query
|
||||
->orderByDesc('ro.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
|
||||
$list = [];
|
||||
foreach ($paginator->items() as $item) {
|
||||
$list[] = $this->refundReportService->formatRefundOrderRow((array) $item);
|
||||
}
|
||||
|
||||
return [
|
||||
'list' => $list,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage(),
|
||||
'pay_types' => $this->payTypeOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款订单详情。
|
||||
*
|
||||
* @param string $refundNo 退款单号
|
||||
* @param int|null $merchantId 商户侧强制限定的商户 ID
|
||||
* @return array{refund_order:array,timeline:array,account_ledgers:array}
|
||||
*/
|
||||
public function detail(string $refundNo, ?int $merchantId = null): array
|
||||
{
|
||||
$refundNo = trim($refundNo);
|
||||
if ($refundNo === '') {
|
||||
throw new ValidationException('refund_no 不能为空');
|
||||
}
|
||||
|
||||
$query = $this->buildRefundOrderQuery($merchantId);
|
||||
$row = $query->where('ro.refund_no', $refundNo)->first();
|
||||
if (!$row) {
|
||||
throw new ResourceNotFoundException('退款单不存在', ['refund_no' => $refundNo]);
|
||||
}
|
||||
|
||||
$refundOrder = $this->refundReportService->formatRefundOrderRow((array) $row);
|
||||
$timeline = $this->refundReportService->buildRefundTimeline($row);
|
||||
$accountLedgers = $this->loadRefundLedgers($row);
|
||||
|
||||
return [
|
||||
'refund_order' => $refundOrder,
|
||||
'timeline' => $timeline,
|
||||
'account_ledgers' => $accountLedgers,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按退款单号查询退款单,可按商户限制。
|
||||
*/
|
||||
public function findByRefundNo(string $refundNo, ?int $merchantId = null): ?\app\model\payment\RefundOrder
|
||||
{
|
||||
$refundNo = trim($refundNo);
|
||||
if ($refundNo === '') {
|
||||
throw new ValidationException('refund_no 不能为空');
|
||||
}
|
||||
|
||||
$query = $this->refundOrderRepository->query()
|
||||
->from('ma_refund_order as ro')
|
||||
->select(['ro.*'])
|
||||
->where('ro.refund_no', $refundNo);
|
||||
|
||||
if ($merchantId !== null && $merchantId > 0) {
|
||||
$query->where('ro.merchant_id', $merchantId);
|
||||
}
|
||||
|
||||
$row = $query->first();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建退款订单基础查询,列表与详情共用。
|
||||
*/
|
||||
private function buildRefundOrderQuery(?int $merchantId = null)
|
||||
{
|
||||
$query = $this->refundOrderRepository->query()
|
||||
->from('ma_refund_order as ro')
|
||||
->leftJoin('ma_pay_order as po', 'po.pay_no', '=', 'ro.pay_no')
|
||||
->leftJoin('ma_biz_order as bo', 'bo.biz_no', '=', 'ro.biz_no')
|
||||
->leftJoin('ma_merchant as m', 'm.id', '=', 'ro.merchant_id')
|
||||
->leftJoin('ma_merchant_group as g', 'g.id', '=', 'ro.merchant_group_id')
|
||||
->leftJoin('ma_payment_channel as c', 'c.id', '=', 'ro.channel_id')
|
||||
->leftJoin('ma_payment_type as t', 't.id', '=', 'po.pay_type_id')
|
||||
->select([
|
||||
'ro.id',
|
||||
'ro.refund_no',
|
||||
'ro.merchant_id',
|
||||
'ro.merchant_group_id',
|
||||
'ro.biz_no',
|
||||
'ro.trace_no',
|
||||
'ro.pay_no',
|
||||
'ro.merchant_refund_no',
|
||||
'ro.channel_id',
|
||||
'ro.refund_amount',
|
||||
'ro.fee_reverse_amount',
|
||||
'ro.status',
|
||||
'ro.channel_request_no',
|
||||
'ro.channel_refund_no',
|
||||
'ro.reason',
|
||||
'ro.request_at',
|
||||
'ro.processing_at',
|
||||
'ro.succeeded_at',
|
||||
'ro.failed_at',
|
||||
'ro.retry_count',
|
||||
'ro.last_error',
|
||||
'ro.ext_json',
|
||||
'ro.created_at',
|
||||
'ro.updated_at',
|
||||
'po.channel_mode',
|
||||
'po.channel_type',
|
||||
'po.pay_type_id',
|
||||
'po.pay_amount as pay_order_amount',
|
||||
'po.fee_actual_amount as pay_fee_actual_amount',
|
||||
'po.status as pay_status',
|
||||
'bo.merchant_order_no',
|
||||
'bo.subject',
|
||||
'bo.body',
|
||||
'bo.status as biz_status',
|
||||
'bo.order_amount as biz_order_amount',
|
||||
'bo.paid_amount as biz_paid_amount',
|
||||
'bo.refund_amount as biz_refund_amount',
|
||||
'm.merchant_no',
|
||||
'm.merchant_name',
|
||||
'm.merchant_short_name',
|
||||
'g.group_name as merchant_group_name',
|
||||
'c.name as channel_name',
|
||||
'c.plugin_code as channel_plugin_code',
|
||||
't.code as pay_type_code',
|
||||
't.name as pay_type_name',
|
||||
't.icon as pay_type_icon',
|
||||
]);
|
||||
|
||||
if ($merchantId !== null && $merchantId > 0) {
|
||||
$query->where('ro.merchant_id', $merchantId);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载退款相关资金流水。
|
||||
*/
|
||||
private function loadRefundLedgers(mixed $refundOrder): array
|
||||
{
|
||||
$traceNo = trim((string) ($refundOrder->trace_no ?? ''));
|
||||
$bizNo = trim((string) ($refundOrder->biz_no ?? ''));
|
||||
$refundNo = trim((string) ($refundOrder->refund_no ?? ''));
|
||||
|
||||
$ledgers = [];
|
||||
if ($traceNo !== '') {
|
||||
$ledgers = $this->collectionToArray($this->merchantAccountLedgerRepository->listByTraceNo($traceNo));
|
||||
}
|
||||
|
||||
if (empty($ledgers) && $bizNo !== '') {
|
||||
$ledgers = $this->collectionToArray($this->merchantAccountLedgerRepository->listByBizNo($bizNo));
|
||||
}
|
||||
|
||||
if (empty($ledgers) && $refundNo !== '') {
|
||||
$ledgers = $this->collectionToArray($this->merchantAccountLedgerRepository->listByBizNo($refundNo));
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($ledgers as $ledger) {
|
||||
$rows[] = $this->refundReportService->formatLedgerRow((array) $ledger);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将查询结果转换成普通数组。
|
||||
*/
|
||||
private function collectionToArray(iterable $items): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($items as $item) {
|
||||
$rows[] = $item;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回启用的支付方式选项,供筛选使用。
|
||||
*/
|
||||
private function payTypeOptions(): array
|
||||
{
|
||||
return $this->paymentTypeRepository->enabledList(['id', 'name'])
|
||||
->map(static function ($payType): array {
|
||||
return [
|
||||
'label' => (string) $payType->name,
|
||||
'value' => (int) $payType->id,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
100
app/service/payment/order/RefundReportService.php
Normal file
100
app/service/payment/order/RefundReportService.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\LedgerConstant;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\common\constant\TradeConstant;
|
||||
|
||||
/**
|
||||
* 退款单结果组装服务。
|
||||
*
|
||||
* 负责退款详情页和列表页的展示字段格式化。
|
||||
*/
|
||||
class RefundReportService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 格式化退款订单行,统一输出前端展示字段。
|
||||
*/
|
||||
public function formatRefundOrderRow(array $row): array
|
||||
{
|
||||
$row['merchant_group_name'] = trim((string) ($row['merchant_group_name'] ?? '')) ?: '未分组';
|
||||
$row['merchant_name'] = trim((string) ($row['merchant_name'] ?? '')) ?: '未知商户';
|
||||
$row['merchant_short_name'] = trim((string) ($row['merchant_short_name'] ?? ''));
|
||||
$row['pay_type_name'] = trim((string) ($row['pay_type_name'] ?? '')) ?: '未知方式';
|
||||
$row['channel_name'] = trim((string) ($row['channel_name'] ?? '')) ?: '未知通道';
|
||||
|
||||
$row['status_text'] = $this->textFromMap((int) ($row['status'] ?? -1), TradeConstant::refundStatusMap());
|
||||
$row['pay_status_text'] = $this->textFromMap((int) ($row['pay_status'] ?? -1), TradeConstant::orderStatusMap());
|
||||
$row['channel_type_text'] = $this->textFromMap((int) ($row['channel_type'] ?? -1), RouteConstant::channelTypeMap());
|
||||
$row['channel_mode_text'] = $this->textFromMap((int) ($row['channel_mode'] ?? -1), RouteConstant::channelModeMap());
|
||||
|
||||
$row['refund_amount_text'] = $this->formatAmount((int) ($row['refund_amount'] ?? 0));
|
||||
$row['fee_reverse_amount_text'] = $this->formatAmount((int) ($row['fee_reverse_amount'] ?? 0));
|
||||
$row['pay_order_amount_text'] = $this->formatAmount((int) ($row['pay_order_amount'] ?? 0));
|
||||
$row['pay_fee_actual_amount_text'] = $this->formatAmount((int) ($row['pay_fee_actual_amount'] ?? 0));
|
||||
$row['biz_order_amount_text'] = $this->formatAmount((int) ($row['biz_order_amount'] ?? 0));
|
||||
$row['biz_paid_amount_text'] = $this->formatAmount((int) ($row['biz_paid_amount'] ?? 0));
|
||||
$row['biz_refund_amount_text'] = $this->formatAmount((int) ($row['biz_refund_amount'] ?? 0));
|
||||
|
||||
$row['request_at_text'] = $this->formatDateTime($row['request_at'] ?? null, '—');
|
||||
$row['processing_at_text'] = $this->formatDateTime($row['processing_at'] ?? null, '—');
|
||||
$row['succeeded_at_text'] = $this->formatDateTime($row['succeeded_at'] ?? null, '—');
|
||||
$row['failed_at_text'] = $this->formatDateTime($row['failed_at'] ?? null, '—');
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造退款时间线。
|
||||
*/
|
||||
public function buildRefundTimeline(mixed $refundOrder): array
|
||||
{
|
||||
$extJson = (array) ($refundOrder->ext_json ?? []);
|
||||
|
||||
return array_values(array_filter([
|
||||
[
|
||||
'status' => 'created',
|
||||
'label' => '退款单创建',
|
||||
'at' => $this->formatDateTime($refundOrder->request_at ?? $refundOrder->created_at ?? null, '—'),
|
||||
],
|
||||
$refundOrder->processing_at ? [
|
||||
'status' => 'processing',
|
||||
'label' => '退款处理中',
|
||||
'at' => $this->formatDateTime($refundOrder->processing_at, '—'),
|
||||
'retry_count' => (int) ($refundOrder->retry_count ?? 0),
|
||||
'reason' => (string) ($extJson['retry_reason'] ?? $extJson['processing_reason'] ?? $refundOrder->last_error ?? ''),
|
||||
] : null,
|
||||
$refundOrder->succeeded_at ? [
|
||||
'status' => 'success',
|
||||
'label' => '退款成功',
|
||||
'at' => $this->formatDateTime($refundOrder->succeeded_at, '—'),
|
||||
] : null,
|
||||
$refundOrder->failed_at ? [
|
||||
'status' => 'failed',
|
||||
'label' => '退款失败',
|
||||
'at' => $this->formatDateTime($refundOrder->failed_at, '—'),
|
||||
'reason' => (string) ($refundOrder->last_error ?: ($extJson['fail_reason'] ?? $refundOrder->reason ?? '')),
|
||||
] : null,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化退款相关资金流水。
|
||||
*/
|
||||
public function formatLedgerRow(array $row): array
|
||||
{
|
||||
$row['biz_type_text'] = $this->textFromMap((int) ($row['biz_type'] ?? -1), LedgerConstant::bizTypeMap());
|
||||
$row['event_type_text'] = $this->textFromMap((int) ($row['event_type'] ?? -1), LedgerConstant::eventTypeMap());
|
||||
$row['direction_text'] = $this->textFromMap((int) ($row['direction'] ?? -1), LedgerConstant::directionMap());
|
||||
$row['amount_text'] = $this->formatAmount((int) ($row['amount'] ?? 0));
|
||||
$row['available_before_text'] = $this->formatAmount((int) ($row['available_before'] ?? 0));
|
||||
$row['available_after_text'] = $this->formatAmount((int) ($row['available_after'] ?? 0));
|
||||
$row['frozen_before_text'] = $this->formatAmount((int) ($row['frozen_before'] ?? 0));
|
||||
$row['frozen_after_text'] = $this->formatAmount((int) ($row['frozen_after'] ?? 0));
|
||||
$row['created_at_text'] = $this->formatDateTime($row['created_at'] ?? null, '—');
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
111
app/service/payment/order/RefundService.php
Normal file
111
app/service/payment/order/RefundService.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\payment\order;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\model\payment\RefundOrder;
|
||||
|
||||
/**
|
||||
* 退款单门面服务。
|
||||
*
|
||||
* 对外保留原有调用契约,内部委托给查询、创建和生命周期三个子服务。
|
||||
*/
|
||||
class RefundService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected RefundQueryService $queryService,
|
||||
protected RefundCreationService $creationService,
|
||||
protected RefundLifecycleService $lifecycleService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询退款订单列表。
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
|
||||
{
|
||||
return $this->queryService->paginate($filters, $page, $pageSize, $merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款订单详情。
|
||||
*/
|
||||
public function detail(string $refundNo, ?int $merchantId = null): array
|
||||
{
|
||||
return $this->queryService->detail($refundNo, $merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建退款单。
|
||||
*/
|
||||
public function createRefund(array $input): RefundOrder
|
||||
{
|
||||
return $this->creationService->createRefund($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记退款处理中。
|
||||
*/
|
||||
public function markRefundProcessing(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->lifecycleService->markRefundProcessing($refundNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款重试。
|
||||
*/
|
||||
public function retryRefund(string $refundNo, array $input = [], ?int $merchantId = null): RefundOrder
|
||||
{
|
||||
if ($merchantId !== null && $merchantId > 0) {
|
||||
$refundOrder = $this->queryService->findByRefundNo($refundNo, $merchantId);
|
||||
if (!$refundOrder) {
|
||||
throw new \app\exception\ResourceNotFoundException('退款单不存在', ['refund_no' => $refundNo]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lifecycleService->retryRefund($refundNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记退款处理中或重试。
|
||||
*/
|
||||
public function markRefundProcessingInCurrentTransaction(string $refundNo, array $input = [], bool $isRetry = false): RefundOrder
|
||||
{
|
||||
return $this->lifecycleService->markRefundProcessingInCurrentTransaction($refundNo, $input, $isRetry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款成功。
|
||||
*/
|
||||
public function markRefundSuccess(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->lifecycleService->markRefundSuccess($refundNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记退款成功。
|
||||
*/
|
||||
public function markRefundSuccessInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->lifecycleService->markRefundSuccessInCurrentTransaction($refundNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款失败。
|
||||
*/
|
||||
public function markRefundFailed(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->lifecycleService->markRefundFailed($refundNo, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务中标记退款失败。
|
||||
*/
|
||||
public function markRefundFailedInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
|
||||
{
|
||||
return $this->lifecycleService->markRefundFailedInCurrentTransaction($refundNo, $input);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user