更新统一使用 PHPDoc + PSR-19 标准注释

This commit is contained in:
技术老胡
2026-04-21 08:38:59 +08:00
parent dcd58e24ce
commit 9a16a88640
252 changed files with 9218 additions and 659 deletions

View File

@@ -23,11 +23,27 @@ use app\service\payment\runtime\PaymentRouteService;
* 支付单发起服务。
*
* 负责支付单预创建、通道路由选择、第三方装单和首轮状态落库。
*
* @property MerchantService $merchantService 商户服务
* @property PaymentRouteService $paymentRouteService 支付路由服务
* @property MerchantAccountService $merchantAccountService 商户账户服务
* @property BizOrderRepository $bizOrderRepository 业务订单仓库
* @property PayOrderRepository $payOrderRepository 支付单仓库
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @property PayOrderChannelDispatchService $payOrderChannelDispatchService 支付单渠道派发服务
*/
class PayOrderAttemptService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param MerchantService $merchantService 商户服务
* @param PaymentRouteService $paymentRouteService 支付路由服务
* @param MerchantAccountService $merchantAccountService 商户账户服务
* @param BizOrderRepository $bizOrderRepository 业务订单仓库
* @param PayOrderRepository $payOrderRepository 支付订单仓库
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @param PayOrderChannelDispatchService $payOrderChannelDispatchService 支付单渠道派发服务
*/
public function __construct(
protected MerchantService $merchantService,
@@ -45,8 +61,11 @@ class PayOrderAttemptService extends BaseService
*
* 该方法会完成商户、支付方式、路由、通道限额、串行尝试和自有通道手续费预占的完整预检查。
*
* @param array $input 支付请求参数
* @return array{merchant:mixed,biz_order:mixed,pay_order:mixed,route:array,payment_result:array,pay_params:array}
* @param array $input 支付预创建参数
* @return array 发起结果
* @throws ValidationException
* @throws BusinessStateException
* @throws ConflictException
*/
public function preparePayAttempt(array $input): array
{
@@ -59,6 +78,7 @@ class PayOrderAttemptService extends BaseService
throw new ValidationException('支付入参不完整');
}
// 先校验商户和支付方式是否可用,避免进入事务后才发现前置条件不满足。
$merchant = $this->merchantService->ensureMerchantEnabled($merchantId);
$merchantGroupId = (int) $merchant->group_id;
if ($merchantGroupId <= 0) {
@@ -72,6 +92,7 @@ class PayOrderAttemptService extends BaseService
throw new BusinessStateException('支付方式不支持', ['pay_type_id' => $payTypeId]);
}
// 根据商户分组、支付金额和请求参数选择可用通道。
$route = $this->paymentRouteService->resolveByMerchantGroup($merchantGroupId, $payTypeId, $payAmount, $input);
$selected = $route['selected_channel'];
/** @var PaymentChannel $channel */
@@ -93,10 +114,12 @@ class PayOrderAttemptService extends BaseService
$payNo,
$channelRequestNo
) {
// 在事务中完成业务单和支付单的原子创建,保证幂等与状态一致。
$existingBizOrder = $this->bizOrderRepository->findForUpdateByMerchantAndOrderNo($merchantId, $merchantOrderNo);
$bizTraceNo = '';
if ($existingBizOrder) {
// 同一商户订单号只能复用原业务单,且金额必须完全一致。
if ((int) $existingBizOrder->order_amount !== $payAmount) {
throw new ValidationException('同一商户订单号金额不一致', [
'merchant_id' => $merchantId,
@@ -128,6 +151,7 @@ class PayOrderAttemptService extends BaseService
$bizOrder = $existingBizOrder;
$bizTraceNo = trim((string) ($bizOrder->trace_no ?? ''));
if ($bizTraceNo === '') {
// 旧单如果没有 trace_no就补成业务单号方便后续串起来查。
$bizTraceNo = (string) $bizOrder->biz_no;
$bizOrder->trace_no = $bizTraceNo;
}
@@ -155,9 +179,11 @@ class PayOrderAttemptService extends BaseService
$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,
@@ -214,6 +240,7 @@ class PayOrderAttemptService extends BaseService
$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();
@@ -233,6 +260,7 @@ class PayOrderAttemptService extends BaseService
/** @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'];
@@ -244,6 +272,10 @@ class PayOrderAttemptService extends BaseService
/**
* 计算手续费金额。
*
* @param int $amount 金额(分)
* @param int $bp 费率基点,`10000` 表示 100%
* @return int 手续费金额(分)
*/
private function calculateAmountByBp(int $amount, int $bp): int
{
@@ -251,6 +283,7 @@ class PayOrderAttemptService extends BaseService
return 0;
}
// 基点换算统一向下取整,避免手续费计算时出现超扣。
return (int) floor($amount * $bp / 10000);
}
}

View File

@@ -17,11 +17,21 @@ use support\Response;
* 支付单回调服务。
*
* 负责渠道回调日志记录、插件回调解析和支付状态分发。
*
* @property NotifyService $notifyService 通知服务
* @property PaymentPluginManager $paymentPluginManager 支付插件管理器
* @property PayOrderRepository $payOrderRepository 支付单仓库
* @property PayOrderLifecycleService $payOrderLifecycleService 支付单生命周期服务
*/
class PayOrderCallbackService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param NotifyService $notifyService 通知服务
* @param PaymentPluginManager $paymentPluginManager 支付插件管理器
* @param PayOrderRepository $payOrderRepository 支付单仓库
* @param PayOrderLifecycleService $payOrderLifecycleService 支付单生命周期服务
*/
public function __construct(
protected NotifyService $notifyService,
@@ -32,7 +42,11 @@ class PayOrderCallbackService extends BaseService
}
/**
* 处理渠道回调。
* 处理渠道回调载荷并推进支付状态
*
* @param array $input 回调载荷
* @return PayOrder 支付订单模型
* @throws \InvalidArgumentException
*/
public function handleChannelCallback(array $input): PayOrder
{
@@ -41,6 +55,7 @@ class PayOrderCallbackService extends BaseService
throw new \InvalidArgumentException('pay_no 不能为空');
}
// 先落回调日志,后续无论成功还是失败,都可以从统一表里排查。
$this->notifyService->recordPayCallback([
'pay_no' => $payNo,
'channel_id' => (int) ($input['channel_id'] ?? 0),
@@ -52,6 +67,7 @@ class PayOrderCallbackService extends BaseService
]);
$success = (bool) ($input['success'] ?? false);
// 回调链路只根据插件/渠道给出的结果收口支付单状态。
if ($success) {
return $this->payOrderLifecycleService->markPaySuccess($payNo, $input);
}
@@ -61,9 +77,17 @@ class PayOrderCallbackService extends BaseService
/**
* 按支付单号处理真实第三方回调。
*
* 该方法先定位支付单,再由插件解析原始请求,最后统一交给生命周期服务推进状态。
*
* @param string $payNo 支付单号
* @param Request $request 请求对象
* @return string|Response 插件要求返回的响应内容
* @throws ResourceNotFoundException
*/
public function handlePluginCallback(string $payNo, Request $request): string|Response
{
// 回调必须能定位到具体支付单,找不到就直接终止。
$payOrder = $this->payOrderRepository->findByPayNo($payNo);
if (!$payOrder) {
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
@@ -72,12 +96,16 @@ class PayOrderCallbackService extends BaseService
$plugin = $this->paymentPluginManager->createByPayOrder($payOrder, true);
try {
// 由插件自行解析请求并返回统一结构,控制器层不直接判断渠道格式。
$result = $plugin->notify($request);
$status = (string) ($result['status'] ?? '');
// 老插件可能只返回 success / paid / failed 这类状态字符串,这里统一折算成布尔结果。
$success = array_key_exists('success', $result)
? (bool) $result['success']
: in_array($status, ['success', 'paid'], true);
// 将插件返回值归一化为生命周期服务可消费的回调载荷。
/** @var array<string, mixed> $callbackPayload */
$callbackPayload = [
'pay_no' => $payNo,
'success' => $success,
@@ -97,14 +125,17 @@ class PayOrderCallbackService extends BaseService
'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,
@@ -120,6 +151,7 @@ class PayOrderCallbackService extends BaseService
return $plugin->notifyFail();
} catch (\Throwable $e) {
// 非业务异常同样记为失败,避免渠道重复推送造成状态抖动。
$this->notifyService->recordPayCallback([
'pay_no' => $payNo,
'channel_id' => (int) $payOrder->channel_id,

View File

@@ -18,11 +18,21 @@ use Throwable;
* 支付渠道单据拉起服务。
*
* 负责调用第三方插件、写回渠道订单号,并在失败时推进支付失败状态。
*
* @property PaymentPluginManager $paymentPluginManager 支付插件管理器
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @property PayOrderRepository $payOrderRepository 支付单仓库
* @property PayOrderLifecycleService $payOrderLifecycleService 支付单生命周期服务
*/
class PayOrderChannelDispatchService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param PaymentPluginManager $paymentPluginManager 支付插件管理器
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @param PayOrderRepository $payOrderRepository 支付单仓库
* @param PayOrderLifecycleService $payOrderLifecycleService 支付单生命周期服务
*/
public function __construct(
protected PaymentPluginManager $paymentPluginManager,
@@ -35,20 +45,28 @@ class PayOrderChannelDispatchService extends BaseService
/**
* 拉起第三方支付单并回写渠道响应。
*
* @return array{pay_order:PayOrder,payment_result:array,pay_params:array}
* @param PayOrder $payOrder 支付订单
* @param BizOrder $bizOrder 业务订单
* @param PaymentChannel $channel 渠道
* @return array 拉起结果
* @throws ResourceNotFoundException
* @throws PaymentException
*/
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,
@@ -69,6 +87,7 @@ class PayOrderChannelDispatchService extends BaseService
]);
$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]);
@@ -87,6 +106,7 @@ class PayOrderChannelDispatchService extends BaseService
return $latest->refresh();
});
} catch (PaymentException $e) {
// 插件层异常统一收口为支付失败,避免订单长时间停留在处理中。
$this->payOrderLifecycleService->markPayFailed((string) $payOrder->pay_no, [
'channel_error_msg' => $e->getMessage(),
'channel_error_code' => (string) $e->getCode(),
@@ -97,6 +117,7 @@ class PayOrderChannelDispatchService extends BaseService
throw $e;
} catch (Throwable $e) {
// 非业务异常同样收口为失败态,并保留原始错误信息。
$this->payOrderLifecycleService->markPayFailed((string) $payOrder->pay_no, [
'channel_error_msg' => $e->getMessage(),
'channel_error_code' => 'PLUGIN_CREATE_ORDER_ERROR',
@@ -117,6 +138,9 @@ class PayOrderChannelDispatchService extends BaseService
/**
* 归一化支付参数快照,便于后续页面渲染和排障。
*
* @param array|object|null $payParams 支付参数数组或对象
* @return array<string, mixed> 参数快照
*/
private function normalizePayParamsSnapshot(mixed $payParams): array
{
@@ -125,6 +149,7 @@ class PayOrderChannelDispatchService extends BaseService
}
if (is_object($payParams) && method_exists($payParams, 'toArray')) {
// 有些插件会返回对象,这里统一转成数组,方便后续落库和页面回显。
$data = $payParams->toArray();
return is_array($data) ? $data : [];
}
@@ -132,3 +157,8 @@ class PayOrderChannelDispatchService extends BaseService
return [];
}
}

View File

@@ -12,11 +12,16 @@ use app\service\account\funds\MerchantAccountService;
* 支付单手续费处理服务。
*
* 负责支付成功时的手续费结算,以及终态时的冻结手续费释放。
*
* @property MerchantAccountService $merchantAccountService 商户账户服务
*/
class PayOrderFeeService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param MerchantAccountService $merchantAccountService 商户账户服务
* @return void
*/
public function __construct(
protected MerchantAccountService $merchantAccountService
@@ -25,6 +30,12 @@ class PayOrderFeeService extends BaseService
/**
* 处理支付成功后的手续费结算。
*
* @param PayOrder $payOrder 支付订单
* @param int $actualFee actual手续费
* @param string $payNo 支付单号
* @param string $traceNo 追踪号
* @return void
*/
public function settleSuccessFee(PayOrder $payOrder, int $actualFee, string $payNo, string $traceNo): void
{
@@ -34,6 +45,7 @@ class PayOrderFeeService extends BaseService
$estimated = (int) $payOrder->fee_estimated_amount;
if ($actualFee > $estimated) {
// 实际手续费高于预估值时,先扣掉预冻结部分,再把差额从可用余额里补扣。
if ($estimated > 0) {
$this->merchantAccountService->deductFrozenAmountInCurrentTransaction(
(int) $payOrder->merchant_id,
@@ -66,6 +78,7 @@ class PayOrderFeeService extends BaseService
}
if ($actualFee < $estimated) {
// 实际手续费低于预估值时,先按实际值扣减冻结金额,再把多冻结部分释放回可用余额。
if ($actualFee > 0) {
$this->merchantAccountService->deductFrozenAmountInCurrentTransaction(
(int) $payOrder->merchant_id,
@@ -98,6 +111,7 @@ class PayOrderFeeService extends BaseService
}
if ($actualFee > 0) {
// 实际值和预估值一致时,直接把冻结金额一次性扣减掉即可。
$this->merchantAccountService->deductFrozenAmountInCurrentTransaction(
(int) $payOrder->merchant_id,
$actualFee,
@@ -114,6 +128,12 @@ class PayOrderFeeService extends BaseService
/**
* 释放支付单已冻结的手续费。
*
* @param PayOrder $payOrder 支付订单
* @param string $payNo 支付单号
* @param string $traceNo 追踪号
* @param string $remark 备注
* @return void
*/
public function releaseFrozenFeeIfNeeded(PayOrder $payOrder, string $payNo, string $traceNo, string $remark): void
{
@@ -121,6 +141,7 @@ class PayOrderFeeService extends BaseService
return;
}
// 只有真正处于冻结态的手续费才需要释放,已经扣减或已释放的单子直接跳过。
if ((int) $payOrder->fee_status !== TradeConstant::FEE_STATUS_FROZEN) {
return;
}
@@ -138,3 +159,7 @@ class PayOrderFeeService extends BaseService
);
}
}

View File

@@ -16,11 +16,19 @@ use app\repository\payment\trade\PayOrderRepository;
* 支付单生命周期服务。
*
* 负责支付单状态推进、关闭、超时和手续费处理。
*
* @property PayOrderFeeService $payOrderFeeService 支付单手续费服务
* @property BizOrderRepository $bizOrderRepository 业务订单仓库
* @property PayOrderRepository $payOrderRepository 支付单仓库
*/
class PayOrderLifecycleService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param PayOrderFeeService $payOrderFeeService 支付单手续费服务
* @param BizOrderRepository $bizOrderRepository 业务订单仓库
* @param PayOrderRepository $payOrderRepository 支付订单仓库
*/
public function __construct(
protected PayOrderFeeService $payOrderFeeService,
@@ -35,8 +43,8 @@ class PayOrderLifecycleService extends BaseService
* 用于支付回调或主动查单成功后的状态推进;自有通道在这里完成手续费正式扣减。
*
* @param string $payNo 支付单号
* @param array $input 回调或查单入参
* @return PayOrder
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function markPaySuccess(string $payNo, array $input = []): PayOrder
{
@@ -51,8 +59,10 @@ class PayOrderLifecycleService extends BaseService
* 该方法只处理状态推进和资金动作,不负责外部通道请求。
*
* @param string $payNo 支付单号
* @param array $input 回调或查单入参
* @return PayOrder
* @param array $input 状态数据
* @return PayOrder 支付订单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function markPaySuccessInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -77,16 +87,19 @@ class PayOrderLifecycleService extends BaseService
]);
}
// 成功态优先使用插件回传的实际手续费,没有则沿用预估值。
$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;
@@ -102,6 +115,7 @@ class PayOrderLifecycleService extends BaseService
$payOrder->ext_json = array_merge((array) $payOrder->ext_json, $input['ext_json'] ?? []);
$payOrder->save();
// 业务单状态也要一起收口,保证支付单和业务单一致。
$this->syncBizOrderAfterSuccess($payOrder, $traceNo);
return $payOrder->refresh();
@@ -109,6 +123,10 @@ class PayOrderLifecycleService extends BaseService
/**
* 标记支付失败。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function markPayFailed(string $payNo, array $input = []): PayOrder
{
@@ -119,6 +137,12 @@ class PayOrderLifecycleService extends BaseService
/**
* 在当前事务中标记支付失败。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function markPayFailedInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -144,6 +168,7 @@ class PayOrderLifecycleService extends BaseService
}
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
// 失败时只释放需要冻结的手续费,避免重复扣减或重复释放。
$this->payOrderFeeService->releaseFrozenFeeIfNeeded($payOrder, $payNo, $traceNo, '支付失败释放手续费');
$payOrder->status = TradeConstant::ORDER_STATUS_FAILED;
@@ -159,6 +184,7 @@ class PayOrderLifecycleService extends BaseService
$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();
@@ -166,6 +192,10 @@ class PayOrderLifecycleService extends BaseService
/**
* 关闭支付单。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function closePayOrder(string $payNo, array $input = []): PayOrder
{
@@ -176,6 +206,12 @@ class PayOrderLifecycleService extends BaseService
/**
* 在当前事务中关闭支付单。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function closePayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -201,6 +237,7 @@ class PayOrderLifecycleService extends BaseService
}
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
// 关闭单据时同样要处理冻结手续费,防止资金一直占用。
$this->payOrderFeeService->releaseFrozenFeeIfNeeded($payOrder, $payNo, $traceNo, '支付关闭释放手续费');
$payOrder->status = TradeConstant::ORDER_STATUS_CLOSED;
@@ -217,6 +254,7 @@ class PayOrderLifecycleService extends BaseService
$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();
@@ -224,6 +262,10 @@ class PayOrderLifecycleService extends BaseService
/**
* 标记支付超时。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function timeoutPayOrder(string $payNo, array $input = []): PayOrder
{
@@ -234,6 +276,12 @@ class PayOrderLifecycleService extends BaseService
/**
* 在当前事务中标记支付超时。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function timeoutPayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -259,6 +307,7 @@ class PayOrderLifecycleService extends BaseService
}
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
// 超时单同样释放冻结手续费,确保后续可以重新发起支付。
$this->payOrderFeeService->releaseFrozenFeeIfNeeded($payOrder, $payNo, $traceNo, '支付超时释放手续费');
$payOrder->status = TradeConstant::ORDER_STATUS_TIMEOUT;
@@ -282,6 +331,10 @@ class PayOrderLifecycleService extends BaseService
/**
* 同步支付成功后的业务单状态。
*
* @param PayOrder $payOrder 支付订单
* @param string $traceNo 追踪号
* @return void
*/
private function syncBizOrderAfterSuccess(PayOrder $payOrder, string $traceNo): void
{
@@ -302,11 +355,19 @@ class PayOrderLifecycleService extends BaseService
/**
* 同步支付终态后的业务单状态。
*
* @param PayOrder $payOrder 支付订单
* @param string $payNo 支付单号
* @param string $traceNo 追踪号
* @param int $status 状态
* @param string $timestampField 时间字段名
* @return void
*/
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;
}

View File

@@ -14,14 +14,27 @@ use app\repository\payment\trade\BizOrderRepository;
use app\repository\payment\trade\PayOrderRepository;
/**
* 支付单查询服务。
* 支付单查询与展示拼装服务。
*
* 负责支付单列表类查询与展示格式化,不承载状态推进逻辑。
* 负责支付单列表、详情和筛选辅助数据的查询,不承载状态推进逻辑。
*
* @property PayOrderRepository $payOrderRepository 支付单仓库
* @property BizOrderRepository $bizOrderRepository 业务订单仓库
* @property MerchantAccountLedgerRepository $merchantAccountLedgerRepository 商户账户流水仓库
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @property PayOrderReportService $payOrderReportService 支付单报表服务
*/
class PayOrderQueryService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param PayOrderRepository $payOrderRepository 支付订单仓库
* @param BizOrderRepository $bizOrderRepository 业务订单仓库
* @param MerchantAccountLedgerRepository $merchantAccountLedgerRepository 商户账户流水仓库
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @param PayOrderReportService $payOrderReportService 支付单报表服务
* @return void
*/
public function __construct(
protected PayOrderRepository $payOrderRepository,
@@ -36,12 +49,13 @@ class PayOrderQueryService extends BaseService
* 分页查询支付订单列表。
*
* 后台和商户后台共用同一套查询逻辑,商户侧会额外限制当前商户 ID。
* 返回值会同时带上支付方式选项,方便列表页直接渲染筛选器。
*
* @param array $filters 查询条件
* @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}
* @param int|null $merchantId 商户ID
* @return array{list: array<int, array<string, mixed>>, total: int, page: int, size: int, pay_types: array<int, array{label: string, value: int}>} 支付订单列表结构
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
{
@@ -122,6 +136,7 @@ class PayOrderQueryService extends BaseService
$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 . '%')
@@ -181,9 +196,13 @@ class PayOrderQueryService extends BaseService
/**
* 查询支付订单详情。
*
* 返回支付单、业务单、时间线和资金流水,供管理后台与商户后台共用。
*
* @param string $payNo 支付单号
* @param int|null $merchantId 商户侧强制限定的商户 ID
* @return array{pay_order:mixed,biz_order:mixed,timeline:array,account_ledgers:mixed}
* @param int|null $merchantId 商户ID
* @return array{pay_order: PayOrder, biz_order: \app\model\payment\BizOrder|null, timeline: array<int, array<string, mixed>>, account_ledgers: \Illuminate\Support\Collection} 支付详情结构
* @throws ValidationException
* @throws ResourceNotFoundException
*/
public function detail(string $payNo, ?int $merchantId = null): array
{
@@ -198,6 +217,7 @@ class PayOrderQueryService extends BaseService
}
if ($merchantId !== null && $merchantId > 0 && (int) $payOrder->merchant_id !== $merchantId) {
// 商户后台只允许看自己的单,归属不匹配时直接按不存在处理。
throw new ResourceNotFoundException('支付单不存在', ['pay_no' => $payNo]);
}
@@ -215,6 +235,11 @@ class PayOrderQueryService extends BaseService
/**
* 加载支付相关资金流水。
*
* 优先按追踪号查询,追踪号为空时回退到业务单号,避免漏掉关联流水。
*
* @param PayOrder $payOrder 支付订单
* @return \Illuminate\Support\Collection 支付相关资金流水集合
*/
private function loadPayLedgers(PayOrder $payOrder)
{
@@ -224,7 +249,8 @@ class PayOrderQueryService extends BaseService
: collect();
if ($ledgers->isEmpty()) {
$ledgers = $this->merchantAccountLedgerRepository->listByBizNo((string) $payOrder->pay_no);
// 追踪号没有命中时,回到业务单号继续兜底,避免早期单据漏掉资金流水。
$ledgers = $this->merchantAccountLedgerRepository->listByBizNo((string) $payOrder->biz_no);
}
return $ledgers;
@@ -232,6 +258,8 @@ class PayOrderQueryService extends BaseService
/**
* 返回启用的支付方式选项,供列表筛选使用。
*
* @return array<int, array{label: string, value: int}> 支付方式选项
*/
private function payTypeOptions(): array
{

View File

@@ -11,12 +11,17 @@ use app\model\payment\PayOrder;
/**
* 支付单结果组装服务。
*
* 负责支付单列表详情页的展示字段格式化。
* 负责支付单列表详情页和时间线的展示字段格式化。
*/
class PayOrderReportService extends BaseService
{
/**
* 格式化支付订单行,统一输出前端需要的中文字段。
*
* 该方法只做展示层字段补齐,不修改原始业务语义。
*
* @param array<string, mixed> $row 原始查询行
* @return array<string, mixed> 格式化后的支付单行
*/
public function formatPayOrderRow(array $row): array
{
@@ -58,11 +63,17 @@ class PayOrderReportService extends BaseService
/**
* 构造支付时间线。
*
* 按创建、成功、关闭、失败、超时的顺序输出,方便前端直接渲染状态流转。
*
* @param PayOrder $payOrder 支付订单
* @return array<int, array<string, mixed>> 支付时间线
*/
public function buildPayTimeline(PayOrder $payOrder): array
{
$extJson = (array) ($payOrder->ext_json ?? []);
// 只保留真实发生过的节点,未触发的状态直接过滤掉,避免时间线里出现空占位。
return array_values(array_filter([
[
'status' => 'created',
@@ -75,11 +86,13 @@ class PayOrderReportService extends BaseService
$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 ? [
@@ -90,3 +103,5 @@ class PayOrderReportService extends BaseService
]));
}
}

View File

@@ -8,14 +8,22 @@ use support\Request;
use support\Response;
/**
* 支付单门面服务。
* 支付单服务。
*
* 对外保留原有调用契约,内部委托给查询、发起、生命周期和回调四个子服务
* @property PayOrderQueryService $queryService 查询服务
* @property PayOrderAttemptService $attemptService 发起服务
* @property PayOrderLifecycleService $lifecycleService 生命周期服务
* @property PayOrderCallbackService $callbackService 回调服务
*/
class PayOrderService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param PayOrderQueryService $queryService 查询服务
* @param PayOrderAttemptService $attemptService 发起服务
* @param PayOrderLifecycleService $lifecycleService 生命周期服务
* @param PayOrderCallbackService $callbackService 回调服务
*/
public function __construct(
protected PayOrderQueryService $queryService,
@@ -27,6 +35,12 @@ class PayOrderService extends BaseService
/**
* 分页查询支付订单列表。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @param int|null $merchantId 商户ID
* @return array 分页数据
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
{
@@ -35,6 +49,10 @@ class PayOrderService extends BaseService
/**
* 查询支付订单详情。
*
* @param string $payNo 支付单号
* @param int|null $merchantId 商户ID
* @return array 订单详情
*/
public function detail(string $payNo, ?int $merchantId = null): array
{
@@ -43,6 +61,9 @@ class PayOrderService extends BaseService
/**
* 预创建支付尝试。
*
* @param array $input 下单数据
* @return array 发起结果
*/
public function preparePayAttempt(array $input): array
{
@@ -51,6 +72,10 @@ class PayOrderService extends BaseService
/**
* 标记支付成功。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function markPaySuccess(string $payNo, array $input = []): PayOrder
{
@@ -59,6 +84,10 @@ class PayOrderService extends BaseService
/**
* 在当前事务中标记支付成功。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function markPaySuccessInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -67,6 +96,10 @@ class PayOrderService extends BaseService
/**
* 标记支付失败。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function markPayFailed(string $payNo, array $input = []): PayOrder
{
@@ -75,6 +108,10 @@ class PayOrderService extends BaseService
/**
* 在当前事务中标记支付失败。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function markPayFailedInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -83,6 +120,10 @@ class PayOrderService extends BaseService
/**
* 关闭支付单。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function closePayOrder(string $payNo, array $input = []): PayOrder
{
@@ -91,6 +132,10 @@ class PayOrderService extends BaseService
/**
* 在当前事务中关闭支付单。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function closePayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -99,6 +144,10 @@ class PayOrderService extends BaseService
/**
* 标记支付超时。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function timeoutPayOrder(string $payNo, array $input = []): PayOrder
{
@@ -107,6 +156,10 @@ class PayOrderService extends BaseService
/**
* 在当前事务中标记支付超时。
*
* @param string $payNo 支付单号
* @param array $input 状态数据
* @return PayOrder 支付订单模型
*/
public function timeoutPayOrderInCurrentTransaction(string $payNo, array $input = []): PayOrder
{
@@ -115,6 +168,9 @@ class PayOrderService extends BaseService
/**
* 处理渠道回调。
*
* @param array $input 回调数据
* @return PayOrder 支付订单模型
*/
public function handleChannelCallback(array $input): PayOrder
{
@@ -123,9 +179,16 @@ class PayOrderService extends BaseService
/**
* 按支付单号处理真实第三方回调。
*
* @param string $payNo 支付单号
* @param Request $request 请求对象
* @return string|Response 字符串或响应对象
*/
public function handlePluginCallback(string $payNo, Request $request): string|Response
{
return $this->callbackService->handlePluginCallback($payNo, $request);
}
}

View File

@@ -17,11 +17,18 @@ use app\repository\payment\trade\RefundOrderRepository;
* 退款单创建服务。
*
* 负责退款单创建和幂等校验,不承载状态推进逻辑。
*
* @property PayOrderRepository $payOrderRepository 支付单仓库
* @property RefundOrderRepository $refundOrderRepository 退款单仓库
*/
class RefundCreationService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param PayOrderRepository $payOrderRepository 支付订单仓库
* @param RefundOrderRepository $refundOrderRepository 退款单仓库
* @return void
*/
public function __construct(
protected PayOrderRepository $payOrderRepository,
@@ -34,8 +41,12 @@ class RefundCreationService extends BaseService
*
* 当前仅支持整单全额退款,且同一支付单只能创建一张退款单。
*
* @param array $input 退款请求参数
* @return RefundOrder
* @param array $input 退款参数
* @return RefundOrder 退款单记录
* @throws ValidationException
* @throws ResourceNotFoundException
* @throws BusinessStateException
* @throws ConflictException
*/
public function createRefund(array $input): RefundOrder
{
@@ -44,11 +55,14 @@ class RefundCreationService extends BaseService
throw new ValidationException('pay_no 不能为空');
}
// 退款必须先锁定原支付单,确保状态和金额都满足退款前置条件。
/** @var \app\model\payment\PayOrder|null $payOrder */
$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,
@@ -64,8 +78,11 @@ class RefundCreationService extends BaseService
throw new BusinessStateException('当前仅支持整单全额退款');
}
// 业务系统若传了商户退款单号,就优先按商户幂等键查重。
$merchantRefundNo = trim((string) ($input['merchant_refund_no'] ?? ''));
if ($merchantRefundNo !== '') {
// 商户退款单号是第一层幂等键,优先用它判断是否重复提交。
/** @var RefundOrder|null $existingByMerchantNo */
$existingByMerchantNo = $this->refundOrderRepository->findByMerchantRefundNo((int) $payOrder->merchant_id, $merchantRefundNo);
if ($existingByMerchantNo) {
if ((string) $existingByMerchantNo->pay_no !== $payNo || (int) $existingByMerchantNo->refund_amount !== $refundAmount) {
@@ -80,7 +97,10 @@ class RefundCreationService extends BaseService
}
}
if ($existingByPayNo = $this->refundOrderRepository->findByPayNo($payNo)) {
// 没有商户退款单号时,用支付单号兜底,避免同一支付单重复创建退款单。
/** @var RefundOrder|null $existingByPayNo */
$existingByPayNo = $this->refundOrderRepository->findByPayNo($payNo);
if ($existingByPayNo) {
if ($merchantRefundNo !== '' && (string) $existingByPayNo->merchant_refund_no !== $merchantRefundNo) {
throw new ConflictException('重复退款', ['pay_no' => $payNo]);
}
@@ -90,6 +110,12 @@ class RefundCreationService extends BaseService
$traceNo = (string) ($payOrder->trace_no ?: $payOrder->biz_no);
// 退款单落库时同步追踪号、渠道单号和反向手续费,方便后续退款推进与对账。
/** @var int $feeReverseAmount */
$feeReverseAmount = ((int) $payOrder->channel_type === RouteConstant::CHANNEL_MODE_COLLECT)
? (int) $payOrder->fee_actual_amount
: 0;
// 代收场景下,退款需要把实际手续费作为反向金额记录下来,后续成功态才能正确冲正余额。
return $this->refundOrderRepository->create([
'refund_no' => $this->generateNo('RFD'),
'merchant_id' => (int) $payOrder->merchant_id,
@@ -100,7 +126,7 @@ class RefundCreationService extends BaseService
'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,
'fee_reverse_amount' => $feeReverseAmount,
'status' => TradeConstant::REFUND_STATUS_CREATED,
'channel_request_no' => $this->generateNo('RQR'),
'reason' => (string) ($input['reason'] ?? ''),

View File

@@ -17,11 +17,22 @@ use app\service\account\funds\MerchantAccountService;
* 退款单生命周期服务。
*
* 负责退款单创建、处理中、成功、失败和重试等状态推进。
*
* @property PayOrderRepository $payOrderRepository 支付单仓库
* @property BizOrderRepository $bizOrderRepository 业务订单仓库
* @property RefundOrderRepository $refundOrderRepository 退款单仓库
* @property MerchantAccountService $merchantAccountService 商户账户服务
*/
class RefundLifecycleService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param PayOrderRepository $payOrderRepository 支付订单仓库
* @param BizOrderRepository $bizOrderRepository 业务订单仓库
* @param RefundOrderRepository $refundOrderRepository 退款单仓库
* @param MerchantAccountService $merchantAccountService 商户账户服务
* @return void
*/
public function __construct(
protected PayOrderRepository $payOrderRepository,
@@ -33,6 +44,12 @@ class RefundLifecycleService extends BaseService
/**
* 标记退款处理中。
*
* 由渠道受理后推进到处理中态,幂等地处理重复请求。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundProcessing(string $refundNo, array $input = []): RefundOrder
{
@@ -43,6 +60,12 @@ class RefundLifecycleService extends BaseService
/**
* 退款重试。
*
* 仅允许失败态退款单重新推进到处理中。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function retryRefund(string $refundNo, array $input = []): RefundOrder
{
@@ -53,6 +76,13 @@ class RefundLifecycleService extends BaseService
/**
* 在当前事务中标记退款处理中或重试。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @param bool $isRetry 是否来自重试流程
* @return RefundOrder 退款单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function markRefundProcessingInCurrentTransaction(string $refundNo, array $input = [], bool $isRetry = false): RefundOrder
{
@@ -77,6 +107,7 @@ class RefundLifecycleService extends BaseService
]);
}
// 退款失败后再重试时,只有失败态才允许重新推进到处理中。
if ($currentStatus === TradeConstant::REFUND_STATUS_FAILED && !$isRetry) {
return $refundOrder;
}
@@ -92,6 +123,7 @@ class RefundLifecycleService extends BaseService
}
$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');
}
@@ -99,6 +131,7 @@ class RefundLifecycleService extends BaseService
$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'] ?? []);
@@ -113,8 +146,8 @@ class RefundLifecycleService extends BaseService
* 成功后会推进退款单状态,并在平台代收场景下做余额冲减或结算逆向处理。
*
* @param string $refundNo 退款单号
* @param array $input 回调或查单入参
* @return RefundOrder
* @param array $input 入参
* @return RefundOrder 退款单模型
*/
public function markRefundSuccess(string $refundNo, array $input = []): RefundOrder
{
@@ -127,8 +160,10 @@ class RefundLifecycleService extends BaseService
* 在当前事务中标记退款成功。
*
* @param string $refundNo 退款单号
* @param array $input 回调或查单入参
* @return RefundOrder
* @param array $input 入参
* @return RefundOrder 退款单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function markRefundSuccessInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
{
@@ -150,6 +185,7 @@ class RefundLifecycleService extends BaseService
return $refundOrder;
}
// 先锁定原支付单,避免退款推进时原单状态被并发修改。
$payOrder = $this->payOrderRepository->findForUpdateByPayNo((string) $refundOrder->pay_no);
if (!$payOrder || (int) $payOrder->status !== TradeConstant::ORDER_STATUS_SUCCESS) {
throw new BusinessStateException('原支付单状态不允许退款', [
@@ -160,6 +196,7 @@ class RefundLifecycleService extends BaseService
$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(
@@ -175,10 +212,12 @@ class RefundLifecycleService extends BaseService
);
}
// 已结算的代收单被退款后,状态要回写成 reversed表示结算已被抵消。
$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 ?? '');
@@ -188,6 +227,7 @@ class RefundLifecycleService extends BaseService
$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;
@@ -200,6 +240,10 @@ class RefundLifecycleService extends BaseService
/**
* 退款失败。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundFailed(string $refundNo, array $input = []): RefundOrder
{
@@ -210,6 +254,12 @@ class RefundLifecycleService extends BaseService
/**
* 在当前事务中标记退款失败。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function markRefundFailedInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
{
@@ -234,6 +284,7 @@ class RefundLifecycleService extends BaseService
]);
}
// 失败状态只更新失败信息,不再改动原支付单和业务单。
$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 ?? '');
@@ -241,6 +292,7 @@ class RefundLifecycleService extends BaseService
$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'] ?? []);

View File

@@ -10,14 +10,25 @@ use app\repository\payment\config\PaymentTypeRepository;
use app\repository\payment\trade\RefundOrderRepository;
/**
* 退款单查询服务。
* 退款单查询与展示拼装服务。
*
* 负责退款列表、详情和数据查询,不承载退款状态推进逻辑。
* 负责退款列表、详情和展示辅助数据查询,不承载退款状态推进逻辑。
*
* @property RefundOrderRepository $refundOrderRepository 退款单仓库
* @property MerchantAccountLedgerRepository $merchantAccountLedgerRepository 商户账户流水仓库
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @property RefundReportService $refundReportService 退款报表服务
*/
class RefundQueryService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param RefundOrderRepository $refundOrderRepository 退款单仓库
* @param MerchantAccountLedgerRepository $merchantAccountLedgerRepository 商户账户流水仓库
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @param RefundReportService $refundReportService 退款报表服务
* @return void
*/
public function __construct(
protected RefundOrderRepository $refundOrderRepository,
@@ -30,11 +41,13 @@ class RefundQueryService extends BaseService
/**
* 分页查询退款订单列表。
*
* @param array $filters 查询条件
* 返回列表、总数、分页信息和支付方式选项,供后台和商户后台直接复用。
*
* @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}
* @param int|null $merchantId 商户ID
* @return array{list: array<int, array<string, mixed>>, total: int, page: int, size: int, pay_types: array<int, array{label: string, value: int}>} 退款订单列表结构
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
{
@@ -42,6 +55,7 @@ class RefundQueryService extends BaseService
$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 . '%')
@@ -83,6 +97,7 @@ class RefundQueryService extends BaseService
->orderByDesc('ro.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
// 列表页需要直接显示文案和金额格式,所以在查询层统一做一次格式化。
$list = [];
foreach ($paginator->items() as $item) {
$list[] = $this->refundReportService->formatRefundOrderRow((array) $item);
@@ -100,9 +115,13 @@ class RefundQueryService extends BaseService
/**
* 查询退款订单详情。
*
* 返回退款单、时间线和资金流水,供列表钻取和详情页展示。
*
* @param string $refundNo 退款单号
* @param int|null $merchantId 商户侧强制限定的商户 ID
* @return array{refund_order:array,timeline:array,account_ledgers:array}
* @param int|null $merchantId 商户ID
* @return array{refund_order: array<string, mixed>, timeline: array<int, array<string, mixed>>, account_ledgers: array<int, array<string, mixed>>} 退款详情结构
* @throws ValidationException
* @throws ResourceNotFoundException
*/
public function detail(string $refundNo, ?int $merchantId = null): array
{
@@ -118,6 +137,7 @@ class RefundQueryService extends BaseService
}
$refundOrder = $this->refundReportService->formatRefundOrderRow((array) $row);
// 详情页把原始行再转成展示数组,便于前端直接渲染各类状态和金额字段。
$timeline = $this->refundReportService->buildRefundTimeline($row);
$accountLedgers = $this->loadRefundLedgers($row);
@@ -130,6 +150,11 @@ class RefundQueryService extends BaseService
/**
* 按退款单号查询退款单,可按商户限制。
*
* @param string $refundNo 退款单号
* @param int|null $merchantId 商户ID
* @return \app\model\payment\RefundOrder|null 退款单模型
* @throws ValidationException
*/
public function findByRefundNo(string $refundNo, ?int $merchantId = null): ?\app\model\payment\RefundOrder
{
@@ -157,9 +182,13 @@ class RefundQueryService extends BaseService
/**
* 构建退款订单基础查询,列表与详情共用。
*
* @param int|null $merchantId 商户ID
* @return \Illuminate\Database\Eloquent\Builder 查询构造器
*/
private function buildRefundOrderQuery(?int $merchantId = null)
{
// 退款单详情需要同时展示支付、业务、商户和通道信息,所以一次性把相关表都 join 进来。
$query = $this->refundOrderRepository->query()
->from('ma_refund_order as ro')
->leftJoin('ma_pay_order as po', 'po.pay_no', '=', 'ro.pay_no')
@@ -226,8 +255,13 @@ class RefundQueryService extends BaseService
/**
* 加载退款相关资金流水。
*
* 按追踪号、业务单号、退款单号依次回退查找,尽量把相关流水补齐。
*
* @param object|null $refundOrder 退款订单或查询行
* @return array<int, array<string, mixed>> 退款流水展示结构
*/
private function loadRefundLedgers(mixed $refundOrder): array
private function loadRefundLedgers(object|null $refundOrder): array
{
$traceNo = trim((string) ($refundOrder->trace_no ?? ''));
$bizNo = trim((string) ($refundOrder->biz_no ?? ''));
@@ -239,10 +273,12 @@ class RefundQueryService extends BaseService
}
if (empty($ledgers) && $bizNo !== '') {
// 退款流水优先按追踪号查,查不到再回到业务单号兜底。
$ledgers = $this->collectionToArray($this->merchantAccountLedgerRepository->listByBizNo($bizNo));
}
if (empty($ledgers) && $refundNo !== '') {
// 最后再用退款单号补查,尽量避免详情页缺少资金流水。
$ledgers = $this->collectionToArray($this->merchantAccountLedgerRepository->listByBizNo($refundNo));
}
@@ -256,6 +292,9 @@ class RefundQueryService extends BaseService
/**
* 将查询结果转换成普通数组。
*
* @param iterable $items 查询结果
* @return array<int, mixed> 查询结果列表
*/
private function collectionToArray(iterable $items): array
{
@@ -269,6 +308,8 @@ class RefundQueryService extends BaseService
/**
* 返回启用的支付方式选项,供筛选使用。
*
* @return array<int, array{label: string, value: int}> 支付方式选项
*/
private function payTypeOptions(): array
{

View File

@@ -10,12 +10,15 @@ use app\common\constant\TradeConstant;
/**
* 退款单结果组装服务。
*
* 负责退款详情页和列表页的展示字段格式化。
* 负责退款列表、详情页和资金流水的展示字段格式化。
*/
class RefundReportService extends BaseService
{
/**
* 格式化退款订单行,统一输出前端展示字段。
*
* @param array<string, mixed> $row 原始查询行
* @return array<string, mixed> 格式化后的退款单行
*/
public function formatRefundOrderRow(array $row): array
{
@@ -48,11 +51,17 @@ class RefundReportService extends BaseService
/**
* 构造退款时间线。
*
* 依次输出创建、处理中、成功和失败节点,便于前端直接展示进度。
*
* @param object|null $refundOrder 退款订单或查询行
* @return array<int, array<string, mixed>> 退款时间线
*/
public function buildRefundTimeline(mixed $refundOrder): array
public function buildRefundTimeline(object|null $refundOrder): array
{
$extJson = (array) ($refundOrder->ext_json ?? []);
// 退款时间线同样只展示已经发生的节点,并尽量用扩展信息补全原因字段。
return array_values(array_filter([
[
'status' => 'created',
@@ -64,6 +73,7 @@ class RefundReportService extends BaseService
'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 ? [
@@ -75,6 +85,7 @@ class RefundReportService extends BaseService
'status' => 'failed',
'label' => '退款失败',
'at' => $this->formatDateTime($refundOrder->failed_at, '—'),
// 失败原因先看最后错误,再回退到扩展信息和退款单原始原因。
'reason' => (string) ($refundOrder->last_error ?: ($extJson['fail_reason'] ?? $refundOrder->reason ?? '')),
] : null,
]));
@@ -82,6 +93,9 @@ class RefundReportService extends BaseService
/**
* 格式化退款相关资金流水。
*
* @param array<string, mixed> $row 原始查询行
* @return array<string, mixed> 格式化后的流水行
*/
public function formatLedgerRow(array $row): array
{
@@ -98,3 +112,4 @@ class RefundReportService extends BaseService
return $row;
}
}

View File

@@ -6,14 +6,21 @@ use app\common\base\BaseService;
use app\model\payment\RefundOrder;
/**
* 退款单门面服务。
* 退款单服务。
*
* 对外保留原有调用契约,内部委托给查询、创建和生命周期三个子服务
* @property RefundQueryService $queryService 查询服务
* @property RefundCreationService $creationService 创建服务
* @property RefundLifecycleService $lifecycleService 生命周期服务
*/
class RefundService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param RefundQueryService $queryService 查询服务
* @param RefundCreationService $creationService 创建服务
* @param RefundLifecycleService $lifecycleService 生命周期服务
* @return void
*/
public function __construct(
protected RefundQueryService $queryService,
@@ -24,6 +31,12 @@ class RefundService extends BaseService
/**
* 分页查询退款订单列表。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @param int|null $merchantId 商户ID
* @return array{list: array<int, array<string, mixed>>, total: int, page: int, size: int, pay_types: array<int, array{label: string, value: int}>} 退款列表结构
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10, ?int $merchantId = null): array
{
@@ -32,6 +45,10 @@ class RefundService extends BaseService
/**
* 查询退款订单详情。
*
* @param string $refundNo 退款单号
* @param int|null $merchantId 商户ID
* @return array{refund_order: array<string, mixed>, timeline: array<int, array<string, mixed>>, account_ledgers: array<int, array<string, mixed>>} 退款详情结构
*/
public function detail(string $refundNo, ?int $merchantId = null): array
{
@@ -40,6 +57,9 @@ class RefundService extends BaseService
/**
* 创建退款单。
*
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function createRefund(array $input): RefundOrder
{
@@ -48,6 +68,10 @@ class RefundService extends BaseService
/**
* 标记退款处理中。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundProcessing(string $refundNo, array $input = []): RefundOrder
{
@@ -56,10 +80,17 @@ class RefundService extends BaseService
/**
* 退款重试。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @param int|null $merchantId 商户ID
* @return RefundOrder 退款单模型
* @throws app\exception\ResourceNotFoundException
*/
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]);
@@ -71,6 +102,11 @@ class RefundService extends BaseService
/**
* 在当前事务中标记退款处理中或重试。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @param bool $isRetry 是否来自重试流程
* @return RefundOrder 退款单模型
*/
public function markRefundProcessingInCurrentTransaction(string $refundNo, array $input = [], bool $isRetry = false): RefundOrder
{
@@ -79,6 +115,10 @@ class RefundService extends BaseService
/**
* 退款成功。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundSuccess(string $refundNo, array $input = []): RefundOrder
{
@@ -87,6 +127,10 @@ class RefundService extends BaseService
/**
* 在当前事务中标记退款成功。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundSuccessInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
{
@@ -95,6 +139,10 @@ class RefundService extends BaseService
/**
* 退款失败。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundFailed(string $refundNo, array $input = []): RefundOrder
{
@@ -103,6 +151,10 @@ class RefundService extends BaseService
/**
* 在当前事务中标记退款失败。
*
* @param string $refundNo 退款单号
* @param array $input 输入参数
* @return RefundOrder 退款单模型
*/
public function markRefundFailedInCurrentTransaction(string $refundNo, array $input = []): RefundOrder
{