重构初始化

This commit is contained in:
技术老胡
2026-04-15 11:45:46 +08:00
parent 72d72d735b
commit 7612026773
381 changed files with 28287 additions and 14717 deletions

View File

@@ -0,0 +1,191 @@
<?php
namespace app\service\payment\runtime;
use app\common\base\BaseService;
use app\common\constant\NotifyConstant;
use app\common\util\FormatHelper;
use app\model\admin\ChannelNotifyLog;
use app\model\payment\NotifyTask;
use app\model\admin\PayCallbackLog;
use app\repository\ops\log\ChannelNotifyLogRepository;
use app\repository\payment\notify\NotifyTaskRepository;
use app\repository\ops\log\PayCallbackLogRepository;
/**
* 通知服务。
*
* 负责渠道通知日志、支付回调日志和商户通知任务的统一管理,核心目标是去重、留痕和可重试。
*/
class NotifyService extends BaseService
{
/**
* 构造函数,注入对应依赖。
*/
public function __construct(
protected ChannelNotifyLogRepository $channelNotifyLogRepository,
protected PayCallbackLogRepository $payCallbackLogRepository,
protected NotifyTaskRepository $notifyTaskRepository
) {
}
/**
* 记录渠道通知日志。
*
* 同一通道、通知类型和业务单号只保留一条重复记录。
*/
public function recordChannelNotify(array $input): ChannelNotifyLog
{
$channelId = (int) ($input['channel_id'] ?? 0);
$notifyType = (int) ($input['notify_type'] ?? NotifyConstant::NOTIFY_TYPE_ASYNC);
$bizNo = trim((string) ($input['biz_no'] ?? ''));
if ($channelId <= 0 || $bizNo === '') {
throw new \InvalidArgumentException('渠道通知入参不完整');
}
if ($duplicate = $this->channelNotifyLogRepository->findDuplicate($channelId, $notifyType, $bizNo)) {
return $duplicate;
}
return $this->channelNotifyLogRepository->create([
'notify_no' => (string) ($input['notify_no'] ?? $this->generateNo('CNL')),
'channel_id' => $channelId,
'notify_type' => $notifyType,
'biz_no' => $bizNo,
'pay_no' => (string) ($input['pay_no'] ?? ''),
'channel_request_no' => (string) ($input['channel_request_no'] ?? ''),
'channel_trade_no' => (string) ($input['channel_trade_no'] ?? ''),
'raw_payload' => $input['raw_payload'] ?? [],
'verify_status' => (int) ($input['verify_status'] ?? NotifyConstant::VERIFY_STATUS_UNKNOWN),
'process_status' => (int) ($input['process_status'] ?? NotifyConstant::PROCESS_STATUS_PENDING),
'retry_count' => (int) ($input['retry_count'] ?? 0),
'next_retry_at' => $input['next_retry_at'] ?? null,
'last_error' => (string) ($input['last_error'] ?? ''),
]);
}
/**
* 记录支付回调日志。
*
* 以支付单号 + 回调类型作为去重依据。
*/
public function recordPayCallback(array $input): PayCallbackLog
{
$payNo = trim((string) ($input['pay_no'] ?? ''));
if ($payNo === '') {
throw new \InvalidArgumentException('pay_no 不能为空');
}
$callbackType = (int) ($input['callback_type'] ?? NotifyConstant::CALLBACK_TYPE_ASYNC);
$logs = $this->payCallbackLogRepository->listByPayNo($payNo);
foreach ($logs as $log) {
if ((int) $log->callback_type === $callbackType) {
return $log;
}
}
return $this->payCallbackLogRepository->create([
'pay_no' => $payNo,
'channel_id' => (int) ($input['channel_id'] ?? 0),
'callback_type' => $callbackType,
'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'] ?? [],
]);
}
/**
* 创建商户通知任务。
*
* 通常用于支付成功、退款成功或清算完成后的商户异步通知。
*/
public function enqueueMerchantNotify(array $input): NotifyTask
{
return $this->notifyTaskRepository->create([
'notify_no' => (string) ($input['notify_no'] ?? $this->generateNo('NTF')),
'merchant_id' => (int) ($input['merchant_id'] ?? 0),
'merchant_group_id' => (int) ($input['merchant_group_id'] ?? 0),
'biz_no' => (string) ($input['biz_no'] ?? ''),
'pay_no' => (string) ($input['pay_no'] ?? ''),
'notify_url' => (string) ($input['notify_url'] ?? ''),
'notify_data' => $input['notify_data'] ?? [],
'status' => (int) ($input['status'] ?? NotifyConstant::TASK_STATUS_PENDING),
'retry_count' => (int) ($input['retry_count'] ?? 0),
'next_retry_at' => $input['next_retry_at'] ?? $this->nextRetryAt(0),
'last_notify_at' => $input['last_notify_at'] ?? null,
'last_response' => (string) ($input['last_response'] ?? ''),
]);
}
/**
* 标记商户通知成功。
*
* 成功后会刷新最后通知时间和响应内容。
*/
public function markTaskSuccess(string $notifyNo, array $input = []): NotifyTask
{
$task = $this->notifyTaskRepository->findByNotifyNo($notifyNo);
if (!$task) {
throw new \InvalidArgumentException('通知任务不存在');
}
$task->status = NotifyConstant::TASK_STATUS_SUCCESS;
$task->last_notify_at = $input['last_notify_at'] ?? $this->now();
$task->last_response = (string) ($input['last_response'] ?? '');
$task->save();
return $task->refresh();
}
/**
* 标记商户通知失败并计算下次重试时间。
*
* 失败后会累计重试次数,并根据退避策略生成下一次重试时间。
*/
public function markTaskFailed(string $notifyNo, array $input = []): NotifyTask
{
$task = $this->notifyTaskRepository->findByNotifyNo($notifyNo);
if (!$task) {
throw new \InvalidArgumentException('通知任务不存在');
}
$retryCount = (int) $task->retry_count + 1;
$task->status = NotifyConstant::TASK_STATUS_FAILED;
$task->retry_count = $retryCount;
$task->last_notify_at = $input['last_notify_at'] ?? $this->now();
$task->last_response = (string) ($input['last_response'] ?? '');
$task->next_retry_at = $this->nextRetryAt($retryCount);
$task->save();
return $task->refresh();
}
/**
* 获取待重试任务。
*/
public function listRetryableTasks(): iterable
{
return $this->notifyTaskRepository->listRetryable(NotifyConstant::TASK_STATUS_FAILED);
}
/**
* 根据重试次数计算下次重试时间。
*
* 使用简单的指数退避思路控制重试频率。
*/
private function nextRetryAt(int $retryCount): string
{
$retryCount = max(0, $retryCount);
$delay = match (true) {
$retryCount <= 0 => 60,
$retryCount === 1 => 300,
$retryCount === 2 => 900,
default => 1800,
};
return FormatHelper::timestamp(time() + $delay);
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace app\service\payment\runtime;
use app\common\base\BaseService;
use app\common\interface\PaymentInterface;
use app\common\interface\PayPluginInterface;
use app\exception\PaymentException;
use app\model\payment\PayOrder;
use app\model\payment\PaymentChannel;
use app\model\payment\PaymentPlugin;
use app\repository\payment\config\PaymentChannelRepository;
use app\repository\payment\config\PaymentPluginConfRepository;
use app\repository\payment\config\PaymentPluginRepository;
use app\repository\payment\config\PaymentTypeRepository;
/**
* 支付插件工厂服务。
*
* 负责解析插件定义、装配配置并实例化插件。
*/
class PaymentPluginFactoryService extends BaseService
{
public function __construct(
protected PaymentPluginRepository $paymentPluginRepository,
protected PaymentPluginConfRepository $paymentPluginConfRepository,
protected PaymentChannelRepository $paymentChannelRepository,
protected PaymentTypeRepository $paymentTypeRepository
) {}
public function createByChannel(PaymentChannel|int $channel, ?int $payTypeId = null, bool $allowDisabled = false): PaymentInterface & PayPluginInterface
{
$channelModel = $channel instanceof PaymentChannel
? $channel
: $this->paymentChannelRepository->find((int) $channel);
if (!$channelModel) {
throw new PaymentException('支付通道不存在', 40402, ['channel_id' => (int) $channel]);
}
$plugin = $this->resolvePlugin((string) $channelModel->plugin_code, $allowDisabled);
$payTypeCode = $this->resolvePayTypeCode((int) ($payTypeId ?: $channelModel->pay_type_id));
if (!$allowDisabled && !$this->pluginSupportsPayType($plugin, $payTypeCode)) {
throw new PaymentException('支付插件不支持当前支付方式', 40210, [
'plugin_code' => (string) $plugin->code,
'pay_type_code' => $payTypeCode,
'channel_id' => (int) $channelModel->id,
]);
}
$instance = $this->instantiatePlugin((string) $plugin->class_name);
$instance->init($this->buildChannelConfig($channelModel, $plugin));
return $instance;
}
public function createByPayOrder(PayOrder $payOrder, bool $allowDisabled = true): PaymentInterface
{
return $this->createByChannel((int) $payOrder->channel_id, (int) $payOrder->pay_type_id, $allowDisabled);
}
public function ensureChannelSupportsPayType(PaymentChannel $channel, int $payTypeId): void
{
$plugin = $this->resolvePlugin((string) $channel->plugin_code, false);
$payTypeCode = $this->resolvePayTypeCode($payTypeId);
if (!$this->pluginSupportsPayType($plugin, $payTypeCode)) {
throw new PaymentException('支付插件不支持当前支付方式', 40210, [
'plugin_code' => (string) $plugin->code,
'pay_type_code' => $payTypeCode,
'channel_id' => (int) $channel->id,
]);
}
}
public function pluginPayTypes(string $pluginCode, bool $allowDisabled = false): array
{
$plugin = $this->resolvePlugin($pluginCode, $allowDisabled);
return $this->normalizeCodes($plugin->pay_types ?? []);
}
private function buildChannelConfig(PaymentChannel $channel, PaymentPlugin $plugin): array
{
$config = [];
$configId = (int) $channel->api_config_id;
if ($configId > 0) {
$pluginConf = $this->paymentPluginConfRepository->find($configId);
if (!$pluginConf) {
throw new PaymentException('支付插件配置不存在', 40403, [
'api_config_id' => $configId,
'channel_id' => (int) $channel->id,
]);
}
if ((string) $pluginConf->plugin_code !== (string) $plugin->code) {
throw new PaymentException('支付插件与配置不匹配', 40211, [
'channel_id' => (int) $channel->id,
'plugin_code' => (string) $plugin->code,
'config_plugin_code' => (string) $pluginConf->plugin_code,
]);
}
$config = (array) ($pluginConf->config ?? []);
$config['settlement_cycle_type'] = (int) ($pluginConf->settlement_cycle_type ?? 1);
$config['settlement_cutoff_time'] = (string) ($pluginConf->settlement_cutoff_time ?? '23:59:59');
}
$config['plugin_code'] = (string) $plugin->code;
$config['plugin_name'] = (string) $plugin->name;
$config['channel_id'] = (int) $channel->id;
$config['merchant_id'] = (int) $channel->merchant_id;
$config['channel_mode'] = (int) $channel->channel_mode;
$config['pay_type_id'] = (int) $channel->pay_type_id;
$config['api_config_id'] = $configId;
$config['enabled_pay_types'] = $this->normalizeCodes($plugin->pay_types ?? []);
$config['enabled_transfer_types'] = $this->normalizeCodes($plugin->transfer_types ?? []);
return $config;
}
private function instantiatePlugin(string $className): PaymentInterface & PayPluginInterface
{
$className = $this->resolvePluginClassName($className);
if ($className === '') {
throw new PaymentException('支付插件未配置实现类', 40212);
}
if (!class_exists($className)) {
throw new PaymentException('支付插件实现类不存在', 40404, ['class_name' => $className]);
}
$instance = container_make($className, []);
if (!$instance instanceof PaymentInterface || !$instance instanceof PayPluginInterface) {
throw new PaymentException('支付插件必须同时实现 PaymentInterface 与 PayPluginInterface', 40213, ['class_name' => $className]);
}
return $instance;
}
private function resolvePluginClassName(string $className): string
{
$className = trim($className);
if ($className === '') {
return '';
}
if (str_contains($className, '\\')) {
return $className;
}
return 'app\\common\\payment\\' . $className;
}
private function resolvePlugin(string $pluginCode, bool $allowDisabled): PaymentPlugin
{
/** @var PaymentPlugin|null $plugin */
$plugin = $this->paymentPluginRepository->findByCode($pluginCode);
if (!$plugin) {
throw new PaymentException('支付插件不存在', 40401, ['plugin_code' => $pluginCode]);
}
if (!$allowDisabled && (int) $plugin->status !== 1) {
throw new PaymentException('支付插件已禁用', 40214, ['plugin_code' => $pluginCode]);
}
return $plugin;
}
private function resolvePayTypeCode(int $payTypeId): string
{
$paymentType = $this->paymentTypeRepository->find($payTypeId);
if (!$paymentType) {
throw new PaymentException('支付方式不存在', 40405, ['pay_type_id' => $payTypeId]);
}
return trim((string) $paymentType->code);
}
private function pluginSupportsPayType(PaymentPlugin $plugin, string $payTypeCode): bool
{
$payTypeCode = trim($payTypeCode);
if ($payTypeCode === '') {
return false;
}
return in_array($payTypeCode, $this->normalizeCodes($plugin->pay_types ?? []), true);
}
private function normalizeCodes(mixed $codes): array
{
if (is_string($codes)) {
$decoded = json_decode($codes, true);
$codes = is_array($decoded) ? $decoded : [$codes];
}
if (!is_array($codes)) {
return [];
}
$normalized = [];
foreach ($codes as $code) {
$value = trim((string) $code);
if ($value !== '') {
$normalized[] = $value;
}
}
return array_values(array_unique($normalized));
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace app\service\payment\runtime;
use app\common\base\BaseService;
use app\common\interface\PaymentInterface;
use app\common\interface\PayPluginInterface;
use app\model\payment\PayOrder;
use app\model\payment\PaymentChannel;
/**
* 支付插件门面服务。
*
* 对外保留原有调用契约,内部委托给插件工厂服务。
*/
class PaymentPluginManager extends BaseService
{
public function __construct(
protected PaymentPluginFactoryService $factoryService
) {
}
public function createByChannel(PaymentChannel|int $channel, ?int $payTypeId = null, bool $allowDisabled = false): PaymentInterface & PayPluginInterface
{
return $this->factoryService->createByChannel($channel, $payTypeId, $allowDisabled);
}
public function createByPayOrder(PayOrder $payOrder, bool $allowDisabled = true): PaymentInterface & PayPluginInterface
{
return $this->factoryService->createByPayOrder($payOrder, $allowDisabled);
}
public function ensureChannelSupportsPayType(PaymentChannel $channel, int $payTypeId): void
{
$this->factoryService->ensureChannelSupportsPayType($channel, $payTypeId);
}
public function pluginPayTypes(string $pluginCode, bool $allowDisabled = false): array
{
return $this->factoryService->pluginPayTypes($pluginCode, $allowDisabled);
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace app\service\payment\runtime;
use app\common\base\BaseService;
use app\common\constant\CommonConstant;
use app\common\constant\RouteConstant;
use app\common\util\FormatHelper;
use app\exception\BusinessStateException;
use app\exception\ResourceNotFoundException;
use app\exception\ValidationException;
use app\model\payment\PaymentChannel;
use app\model\payment\PaymentPollGroup;
use app\repository\ops\stat\ChannelDailyStatRepository;
use app\repository\payment\config\PaymentChannelRepository;
use app\repository\payment\config\PaymentPollGroupBindRepository;
use app\repository\payment\config\PaymentPollGroupChannelRepository;
use app\repository\payment\config\PaymentPollGroupRepository;
use app\repository\payment\config\PaymentPluginRepository;
use app\repository\payment\config\PaymentTypeRepository;
use support\Redis;
/**
* 支付路由解析服务。
*
* 负责商户分组 -> 轮询组 -> 支付通道的编排与选择。
*/
class PaymentRouteResolverService extends BaseService
{
public function __construct(
protected PaymentPollGroupBindRepository $bindRepository,
protected PaymentPollGroupRepository $pollGroupRepository,
protected PaymentPollGroupChannelRepository $pollGroupChannelRepository,
protected PaymentChannelRepository $channelRepository,
protected ChannelDailyStatRepository $channelDailyStatRepository,
protected PaymentPluginRepository $paymentPluginRepository,
protected PaymentTypeRepository $paymentTypeRepository
) {
}
/**
* 按商户分组和支付方式解析路由。
*
* @return array{bind:mixed,poll_group:mixed,candidates:array,selected_channel:array}
*/
public function resolveByMerchantGroup(int $merchantGroupId, int $payTypeId, int $payAmount, array $context = []): array
{
if ($merchantGroupId <= 0 || $payTypeId <= 0 || $payAmount <= 0) {
throw new ValidationException('路由参数不合法');
}
$bind = $this->bindRepository->findActiveByMerchantGroupAndPayType($merchantGroupId, $payTypeId);
if (!$bind) {
throw new ResourceNotFoundException('路由不存在', [
'merchant_group_id' => $merchantGroupId,
'pay_type_id' => $payTypeId,
]);
}
/** @var PaymentPollGroup|null $pollGroup */
$pollGroup = $this->pollGroupRepository->find((int) $bind->poll_group_id);
if (!$pollGroup || (int) $pollGroup->status !== CommonConstant::STATUS_ENABLED) {
throw new ResourceNotFoundException('路由不存在', [
'merchant_group_id' => $merchantGroupId,
'pay_type_id' => $payTypeId,
'poll_group_id' => (int) ($bind->poll_group_id ?? 0),
]);
}
$candidateRows = $this->pollGroupChannelRepository->listByPollGroupId((int) $pollGroup->id);
if ($candidateRows->isEmpty()) {
throw new BusinessStateException('支付通道不可用', [
'poll_group_id' => (int) $pollGroup->id,
]);
}
$channelIds = $candidateRows->pluck('channel_id')->all();
$channels = $this->channelRepository->query()
->whereIn('id', $channelIds)
->where('status', CommonConstant::STATUS_ENABLED)
->get()
->keyBy('id');
$pluginCodes = $channels->pluck('plugin_code')->filter()->unique()->values()->all();
$plugins = [];
if (!empty($pluginCodes)) {
$plugins = $this->paymentPluginRepository->query()
->whereIn('code', $pluginCodes)
->get()
->keyBy('code')
->all();
}
$paymentType = $this->paymentTypeRepository->find($payTypeId);
$payTypeCode = trim((string) ($paymentType->code ?? ''));
$statDate = $context['stat_date'] ?? FormatHelper::timestamp(time(), 'Y-m-d');
$payAmount = (int) $payAmount;
$eligible = [];
foreach ($candidateRows as $row) {
$channelId = (int) $row->channel_id;
/** @var PaymentChannel|null $channel */
$channel = $channels->get($channelId);
if (!$channel) {
continue;
}
if ((int) $channel->pay_type_id !== $payTypeId) {
continue;
}
$plugin = $plugins[(string) $channel->plugin_code] ?? null;
if (!$plugin || (int) $plugin->status !== CommonConstant::STATUS_ENABLED) {
continue;
}
$pluginPayTypes = is_array($plugin->pay_types) ? $plugin->pay_types : [];
$pluginPayTypes = array_values(array_filter(array_map(static fn ($item) => trim((string) $item), $pluginPayTypes)));
if ($payTypeCode === '' || !in_array($payTypeCode, $pluginPayTypes, true)) {
continue;
}
if (!$this->isAmountAllowed($channel, $payAmount)) {
continue;
}
$stat = $this->channelDailyStatRepository->findByChannelAndDate($channelId, $statDate);
if (!$this->isDailyLimitAllowed($channel, $payAmount, $statDate, $stat)) {
continue;
}
$eligible[] = [
'channel' => $channel,
'poll_group_channel' => $row,
'daily_stat' => $stat,
'health_score' => (int) ($stat->health_score ?? 0),
'success_rate_bp' => (int) ($stat->success_rate_bp ?? 0),
'avg_latency_ms' => (int) ($stat->avg_latency_ms ?? 0),
'weight' => max(1, (int) $row->weight),
'is_default' => (int) $row->is_default,
'sort_no' => (int) $row->sort_no,
];
}
if (empty($eligible)) {
throw new BusinessStateException('支付通道不可用', [
'poll_group_id' => (int) $pollGroup->id,
'merchant_group_id' => $merchantGroupId,
'pay_type_id' => $payTypeId,
]);
}
$routeMode = (int) $pollGroup->route_mode;
$ordered = $this->sortCandidates($eligible, $routeMode);
$selected = $this->selectChannel($ordered, $routeMode, (int) $pollGroup->id);
return [
'bind' => $bind,
'poll_group' => $pollGroup,
'candidates' => $ordered,
'selected_channel' => $selected,
];
}
private function isAmountAllowed(PaymentChannel $channel, int $payAmount): bool
{
if ((int) $channel->min_amount > 0 && $payAmount < (int) $channel->min_amount) {
return false;
}
if ((int) $channel->max_amount > 0 && $payAmount > (int) $channel->max_amount) {
return false;
}
return true;
}
private function isDailyLimitAllowed(PaymentChannel $channel, int $payAmount, string $statDate, ?object $stat = null): bool
{
if ((int) $channel->daily_limit_amount <= 0 && (int) $channel->daily_limit_count <= 0) {
return true;
}
$stat ??= $this->channelDailyStatRepository->findByChannelAndDate((int) $channel->id, $statDate);
$currentAmount = (int) ($stat->pay_amount ?? 0);
$currentCount = (int) ($stat->pay_success_count ?? 0);
if ((int) $channel->daily_limit_amount > 0 && $currentAmount + $payAmount > (int) $channel->daily_limit_amount) {
return false;
}
if ((int) $channel->daily_limit_count > 0 && $currentCount + 1 > (int) $channel->daily_limit_count) {
return false;
}
return true;
}
private function sortCandidates(array $candidates, int $routeMode): array
{
usort($candidates, function (array $left, array $right) use ($routeMode) {
if (
$routeMode === RouteConstant::ROUTE_MODE_FIRST_AVAILABLE
&& (int) $left['is_default'] !== (int) $right['is_default']
) {
return (int) $right['is_default'] <=> (int) $left['is_default'];
}
if ((int) $left['sort_no'] !== (int) $right['sort_no']) {
return (int) $left['sort_no'] <=> (int) $right['sort_no'];
}
return (int) $left['channel']->id <=> (int) $right['channel']->id;
});
return $candidates;
}
private function selectChannel(array $candidates, int $routeMode, int $pollGroupId): array
{
if (count($candidates) === 1) {
return $candidates[0];
}
return match ($routeMode) {
RouteConstant::ROUTE_MODE_WEIGHTED => $this->selectWeightedChannel($candidates),
RouteConstant::ROUTE_MODE_ORDER => $this->selectSequentialChannel($candidates, $pollGroupId),
RouteConstant::ROUTE_MODE_FIRST_AVAILABLE => $this->selectDefaultChannel($candidates),
default => $candidates[0],
};
}
private function selectWeightedChannel(array $candidates): array
{
$totalWeight = array_sum(array_map(static fn (array $item) => max(1, (int) $item['weight']), $candidates));
$random = random_int(1, max(1, $totalWeight));
foreach ($candidates as $candidate) {
$random -= max(1, (int) $candidate['weight']);
if ($random <= 0) {
return $candidate;
}
}
return $candidates[0];
}
private function selectSequentialChannel(array $candidates, int $pollGroupId): array
{
if ($pollGroupId <= 0) {
return $candidates[0];
}
try {
$cursorKey = sprintf('payment:route:round_robin:%d', $pollGroupId);
$cursor = (int) Redis::incr($cursorKey);
Redis::expire($cursorKey, 30 * 86400);
$index = max(0, ($cursor - 1) % count($candidates));
return $candidates[$index] ?? $candidates[0];
} catch (\Throwable) {
return $candidates[0];
}
}
private function selectDefaultChannel(array $candidates): array
{
foreach ($candidates as $candidate) {
if ((int) ($candidate['is_default'] ?? 0) === 1) {
return $candidate;
}
}
return $candidates[0];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace app\service\payment\runtime;
use app\common\base\BaseService;
/**
* 支付路由门面服务。
*
* 对外保留原有调用契约,内部委托给路由解析服务。
*/
class PaymentRouteService extends BaseService
{
public function __construct(
protected PaymentRouteResolverService $resolverService
) {
}
/**
* 按商户分组和支付方式解析路由。
*/
public function resolveByMerchantGroup(int $merchantGroupId, int $payTypeId, int $payAmount, array $context = []): array
{
return $this->resolverService->resolveByMerchantGroup($merchantGroupId, $payTypeId, $payAmount, $context);
}
}