更新统一使用 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

@@ -16,11 +16,20 @@ use app\repository\ops\log\PayCallbackLogRepository;
* 通知服务。
*
* 负责渠道通知日志、支付回调日志和商户通知任务的统一管理,核心目标是去重、留痕和可重试。
*
* @property ChannelNotifyLogRepository $channelNotifyLogRepository 渠道通知日志仓库
* @property PayCallbackLogRepository $payCallbackLogRepository 支付回调日志仓库
* @property NotifyTaskRepository $notifyTaskRepository 通知任务仓库
*/
class NotifyService extends BaseService
{
/**
* 构造函数,注入对应依赖
* 构造方法
*
* @param ChannelNotifyLogRepository $channelNotifyLogRepository 渠道通知日志仓库
* @param PayCallbackLogRepository $payCallbackLogRepository 支付回调日志仓库
* @param NotifyTaskRepository $notifyTaskRepository 通知任务仓库
* @return void
*/
public function __construct(
protected ChannelNotifyLogRepository $channelNotifyLogRepository,
@@ -33,6 +42,10 @@ class NotifyService extends BaseService
* 记录渠道通知日志。
*
* 同一通道、通知类型和业务单号只保留一条重复记录。
*
* @param array $input 通知数据
* @return ChannelNotifyLog 渠道通知日志
* @throws InvalidArgumentException
*/
public function recordChannelNotify(array $input): ChannelNotifyLog
{
@@ -44,6 +57,7 @@ class NotifyService extends BaseService
throw new \InvalidArgumentException('渠道通知入参不完整');
}
// 同一业务单如果已经记录过相同类型的通知,就直接复用旧日志,避免重复落库。
if ($duplicate = $this->channelNotifyLogRepository->findDuplicate($channelId, $notifyType, $bizNo)) {
return $duplicate;
}
@@ -69,6 +83,10 @@ class NotifyService extends BaseService
* 记录支付回调日志。
*
* 以支付单号 + 回调类型作为去重依据。
*
* @param array $input 回调数据
* @return PayCallbackLog 支付回调日志
* @throws InvalidArgumentException
*/
public function recordPayCallback(array $input): PayCallbackLog
{
@@ -80,6 +98,7 @@ class NotifyService extends BaseService
$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;
}
@@ -100,6 +119,9 @@ class NotifyService extends BaseService
* 创建商户通知任务。
*
* 通常用于支付成功、退款成功或清算完成后的商户异步通知。
*
* @param array $input 通知任务数据
* @return NotifyTask 通知任务
*/
public function enqueueMerchantNotify(array $input): NotifyTask
{
@@ -123,6 +145,11 @@ class NotifyService extends BaseService
* 标记商户通知成功。
*
* 成功后会刷新最后通知时间和响应内容。
*
* @param string $notifyNo 通知号
* @param array $input 附加数据
* @return NotifyTask 通知任务
* @throws InvalidArgumentException
*/
public function markTaskSuccess(string $notifyNo, array $input = []): NotifyTask
{
@@ -143,6 +170,11 @@ class NotifyService extends BaseService
* 标记商户通知失败并计算下次重试时间。
*
* 失败后会累计重试次数,并根据退避策略生成下一次重试时间。
*
* @param string $notifyNo 通知号
* @param array $input 附加数据
* @return NotifyTask 通知任务
* @throws InvalidArgumentException
*/
public function markTaskFailed(string $notifyNo, array $input = []): NotifyTask
{
@@ -151,6 +183,7 @@ class NotifyService extends BaseService
throw new \InvalidArgumentException('通知任务不存在');
}
// 每次失败都累计一次重试,并根据新的次数重新计算下一次触发时间。
$retryCount = (int) $task->retry_count + 1;
$task->status = NotifyConstant::TASK_STATUS_FAILED;
$task->retry_count = $retryCount;
@@ -164,6 +197,8 @@ class NotifyService extends BaseService
/**
* 获取待重试任务。
*
* @return iterable 待重试任务集合
*/
public function listRetryableTasks(): iterable
{
@@ -174,6 +209,9 @@ class NotifyService extends BaseService
* 根据重试次数计算下次重试时间。
*
* 使用简单的指数退避思路控制重试频率。
*
* @param int $retryCount 重试次数
* @return string 下次重试时间
*/
private function nextRetryAt(int $retryCount): string
{
@@ -189,3 +227,8 @@ class NotifyService extends BaseService
}
}

View File

@@ -18,9 +18,23 @@ use app\repository\payment\config\PaymentTypeRepository;
* 支付插件工厂服务。
*
* 负责解析插件定义、装配配置并实例化插件。
*
* @property PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @property PaymentPluginConfRepository $paymentPluginConfRepository 支付插件配置仓库
* @property PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
*/
class PaymentPluginFactoryService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @param PaymentPluginConfRepository $paymentPluginConfRepository 支付插件配置仓库
* @param PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @return void
*/
public function __construct(
protected PaymentPluginRepository $paymentPluginRepository,
protected PaymentPluginConfRepository $paymentPluginConfRepository,
@@ -28,6 +42,15 @@ class PaymentPluginFactoryService extends BaseService
protected PaymentTypeRepository $paymentTypeRepository
) {}
/**
* 根据渠道创建支付插件实例。
*
* @param PaymentChannel|int $channel 渠道对象或渠道ID
* @param int|null $payTypeId 支付类型ID
* @param bool $allowDisabled 是否允许已禁用插件
* @return PaymentInterface&PayPluginInterface 插件实例
* @throws PaymentException
*/
public function createByChannel(PaymentChannel|int $channel, ?int $payTypeId = null, bool $allowDisabled = false): PaymentInterface & PayPluginInterface
{
$channelModel = $channel instanceof PaymentChannel
@@ -39,6 +62,7 @@ class PaymentPluginFactoryService extends BaseService
}
$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, [
@@ -54,13 +78,30 @@ class PaymentPluginFactoryService extends BaseService
return $instance;
}
/**
* 根据支付订单创建支付插件实例。
*
* @param PayOrder $payOrder 支付订单
* @param bool $allowDisabled 是否允许已禁用插件
* @return PaymentInterface&PayPluginInterface 插件实例
*/
public function createByPayOrder(PayOrder $payOrder, bool $allowDisabled = true): PaymentInterface
{
// 支付单已经带了渠道和支付方式快照,这里直接复用渠道工厂逻辑,避免两套实例化口径分叉。
return $this->createByChannel((int) $payOrder->channel_id, (int) $payOrder->pay_type_id, $allowDisabled);
}
/**
* 校验渠道是否支持指定支付方式。
*
* @param PaymentChannel $channel 渠道
* @param int $payTypeId 支付类型ID
* @return void
* @throws PaymentException
*/
public function ensureChannelSupportsPayType(PaymentChannel $channel, int $payTypeId): void
{
// 只做能力校验,不实例化插件,便于后台在保存配置前先拦住不兼容组合。
$plugin = $this->resolvePlugin((string) $channel->plugin_code, false);
$payTypeCode = $this->resolvePayTypeCode($payTypeId);
@@ -73,6 +114,13 @@ class PaymentPluginFactoryService extends BaseService
}
}
/**
* 获取插件支持的支付方式编码。
*
* @param string $pluginCode 插件编码
* @param bool $allowDisabled 是否允许已禁用插件
* @return array 支付方式编码列表
*/
public function pluginPayTypes(string $pluginCode, bool $allowDisabled = false): array
{
$plugin = $this->resolvePlugin($pluginCode, $allowDisabled);
@@ -80,12 +128,21 @@ class PaymentPluginFactoryService extends BaseService
return $this->normalizeCodes($plugin->pay_types ?? []);
}
/**
* 组装渠道初始化配置。
*
* @param PaymentChannel $channel 渠道
* @param PaymentPlugin $plugin 插件
* @return array 初始化配置
* @throws PaymentException
*/
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, [
@@ -103,10 +160,12 @@ class PaymentPluginFactoryService extends BaseService
}
$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;
@@ -120,6 +179,13 @@ class PaymentPluginFactoryService extends BaseService
return $config;
}
/**
* 实例化支付插件。
*
* @param string $className 类名
* @return PaymentInterface&PayPluginInterface 插件实例
* @throws PaymentException
*/
private function instantiatePlugin(string $className): PaymentInterface & PayPluginInterface
{
$className = $this->resolvePluginClassName($className);
@@ -131,7 +197,9 @@ class PaymentPluginFactoryService extends BaseService
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]);
}
@@ -139,6 +207,12 @@ class PaymentPluginFactoryService extends BaseService
return $instance;
}
/**
* 规范化插件类名。
*
* @param string $className 类名
* @return string 完整类名
*/
private function resolvePluginClassName(string $className): string
{
$className = trim($className);
@@ -153,6 +227,14 @@ class PaymentPluginFactoryService extends BaseService
return 'app\\common\\payment\\' . $className;
}
/**
* 根据编码解析支付插件。
*
* @param string $pluginCode 插件编码
* @param bool $allowDisabled 是否允许已禁用插件
* @return PaymentPlugin 插件模型
* @throws PaymentException
*/
private function resolvePlugin(string $pluginCode, bool $allowDisabled): PaymentPlugin
{
/** @var PaymentPlugin|null $plugin */
@@ -168,6 +250,13 @@ class PaymentPluginFactoryService extends BaseService
return $plugin;
}
/**
* 根据支付类型 ID 解析支付方式编码。
*
* @param int $payTypeId 支付类型ID
* @return string 支付方式编码
* @throws PaymentException
*/
private function resolvePayTypeCode(int $payTypeId): string
{
$paymentType = $this->paymentTypeRepository->find($payTypeId);
@@ -178,6 +267,13 @@ class PaymentPluginFactoryService extends BaseService
return trim((string) $paymentType->code);
}
/**
* 判断插件是否支持指定支付方式。
*
* @param PaymentPlugin $plugin 插件
* @param string $payTypeCode 支付方式编码
* @return bool 是否支持
*/
private function pluginSupportsPayType(PaymentPlugin $plugin, string $payTypeCode): bool
{
$payTypeCode = trim($payTypeCode);
@@ -188,6 +284,14 @@ class PaymentPluginFactoryService extends BaseService
return in_array($payTypeCode, $this->normalizeCodes($plugin->pay_types ?? []), true);
}
/**
* 规范化编码列表。
*
* 支持数组和 JSON 字符串两种输入形式,输出去重后的纯字符串数组。
*
* @param array|string|null $codes 原始编码集合
* @return array<int, string> 编码列表
*/
private function normalizeCodes(mixed $codes): array
{
if (is_string($codes)) {

View File

@@ -9,34 +9,72 @@ use app\model\payment\PayOrder;
use app\model\payment\PaymentChannel;
/**
* 支付插件门面服务。
* 支付插件服务。
*
* 对外保留原有调用契约,内部委托给插件工厂服务
* @property PaymentPluginFactoryService $factoryService 插件工厂服务
*/
class PaymentPluginManager extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPluginFactoryService $factoryService 插件工厂服务
* @return void
*/
public function __construct(
protected PaymentPluginFactoryService $factoryService
) {
}
/**
* 根据渠道创建支付插件实例。
*
* @param PaymentChannel|int $channel 渠道对象或渠道ID
* @param int|null $payTypeId 支付类型ID
* @param bool $allowDisabled 是否允许已禁用插件
* @return PaymentInterface&PayPluginInterface 插件实例
*/
public function createByChannel(PaymentChannel|int $channel, ?int $payTypeId = null, bool $allowDisabled = false): PaymentInterface & PayPluginInterface
{
return $this->factoryService->createByChannel($channel, $payTypeId, $allowDisabled);
}
/**
* 根据支付订单创建支付插件实例。
*
* @param PayOrder $payOrder 支付订单
* @param bool $allowDisabled 是否允许已禁用插件
* @return PaymentInterface&PayPluginInterface 插件实例
*/
public function createByPayOrder(PayOrder $payOrder, bool $allowDisabled = true): PaymentInterface & PayPluginInterface
{
return $this->factoryService->createByPayOrder($payOrder, $allowDisabled);
}
/**
* 校验渠道是否支持指定支付方式。
*
* @param PaymentChannel $channel 渠道
* @param int $payTypeId 支付类型ID
* @return void
*/
public function ensureChannelSupportsPayType(PaymentChannel $channel, int $payTypeId): void
{
$this->factoryService->ensureChannelSupportsPayType($channel, $payTypeId);
}
/**
* 获取插件支持的支付方式编码。
*
* @param string $pluginCode 插件编码
* @param bool $allowDisabled 是否允许已禁用插件
* @return array 支付方式编码列表
*/
public function pluginPayTypes(string $pluginCode, bool $allowDisabled = false): array
{
return $this->factoryService->pluginPayTypes($pluginCode, $allowDisabled);
}
}

View File

@@ -23,10 +23,30 @@ use support\Redis;
/**
* 支付路由解析服务。
*
* 负责商户分组 -> 轮询组 -> 支付通道的编排与选择。
* 负责商户分组、轮询组、支付类型和支付通道之间的筛选、排序与最终选择。
*
* @property PaymentPollGroupBindRepository $bindRepository 绑定仓库
* @property PaymentPollGroupRepository $pollGroupRepository 轮询分组仓库
* @property PaymentPollGroupChannelRepository $pollGroupChannelRepository 轮询分组渠道仓库
* @property PaymentChannelRepository $channelRepository 渠道仓库
* @property ChannelDailyStatRepository $channelDailyStatRepository 渠道日统计仓库
* @property PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
*/
class PaymentRouteResolverService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPollGroupBindRepository $bindRepository 绑定仓库
* @param PaymentPollGroupRepository $pollGroupRepository 轮询分组仓库
* @param PaymentPollGroupChannelRepository $pollGroupChannelRepository 轮询分组渠道仓库
* @param PaymentChannelRepository $channelRepository 渠道仓库
* @param ChannelDailyStatRepository $channelDailyStatRepository 渠道日统计仓库
* @param PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @return void
*/
public function __construct(
protected PaymentPollGroupBindRepository $bindRepository,
protected PaymentPollGroupRepository $pollGroupRepository,
@@ -41,7 +61,17 @@ class PaymentRouteResolverService extends BaseService
/**
* 按商户分组和支付方式解析路由。
*
* @return array{bind:mixed,poll_group:mixed,candidates:array,selected_channel:array}
* 先读取有效的商户分组绑定和轮询组,再按支付类型、插件支持、金额区间和日限额过滤候选通道,
* 最后依据轮询组策略选出实际使用的通道。
*
* @param int $merchantGroupId 商户分组ID
* @param int $payTypeId 支付类型ID
* @param int $payAmount 支付金额(分)
* @param array $context 路由上下文,支持传入 `stat_date` 等辅助参数
* @return array 路由解析结果
* @throws ValidationException
* @throws ResourceNotFoundException
* @throws BusinessStateException
*/
public function resolveByMerchantGroup(int $merchantGroupId, int $payTypeId, int $payAmount, array $context = []): array
{
@@ -74,7 +104,9 @@ class PaymentRouteResolverService extends BaseService
]);
}
// 先拿到轮询组下的编排记录,再去批量加载通道、插件和统计数据,避免逐条查库。
$channelIds = $candidateRows->pluck('channel_id')->all();
// 先一次性拉出通道和插件信息,避免候选过滤过程中频繁查库。
$channels = $this->channelRepository->query()
->whereIn('id', $channelIds)
->where('status', CommonConstant::STATUS_ENABLED)
@@ -83,6 +115,7 @@ class PaymentRouteResolverService extends BaseService
$pluginCodes = $channels->pluck('plugin_code')->filter()->unique()->values()->all();
$plugins = [];
if (!empty($pluginCodes)) {
// 通道会复用同一个插件实现,插件信息也按编码批量加载一次即可。
$plugins = $this->paymentPluginRepository->query()
->whereIn('code', $pluginCodes)
->get()
@@ -92,6 +125,7 @@ class PaymentRouteResolverService extends BaseService
$paymentType = $this->paymentTypeRepository->find($payTypeId);
$payTypeCode = trim((string) ($paymentType->code ?? ''));
// 默认统计日期取当天,路由预览时也可以由外部显式传入历史日期。
$statDate = $context['stat_date'] ?? FormatHelper::timestamp(time(), 'Y-m-d');
$payAmount = (int) $payAmount;
$eligible = [];
@@ -105,30 +139,36 @@ class PaymentRouteResolverService extends BaseService
continue;
}
// 先按支付方式收口,避免插件和通道配置不一致时误选。
if ((int) $channel->pay_type_id !== $payTypeId) {
continue;
}
/** @var \app\model\payment\PaymentPlugin|null $plugin */
$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,
@@ -143,6 +183,7 @@ class PaymentRouteResolverService extends BaseService
}
if (empty($eligible)) {
// 所有候选都被过滤后,直接判定通道不可用。
throw new BusinessStateException('支付通道不可用', [
'poll_group_id' => (int) $pollGroup->id,
'merchant_group_id' => $merchantGroupId,
@@ -150,10 +191,12 @@ class PaymentRouteResolverService extends BaseService
]);
}
// 按路由模式进行排序,然后再选出最终通道。
$routeMode = (int) $pollGroup->route_mode;
$ordered = $this->sortCandidates($eligible, $routeMode);
$selected = $this->selectChannel($ordered, $routeMode, (int) $pollGroup->id);
// 返回绑定、轮询组、候选集和最终选中项,供路由预览和实际支付共用。
return [
'bind' => $bind,
'poll_group' => $pollGroup,
@@ -162,6 +205,13 @@ class PaymentRouteResolverService extends BaseService
];
}
/**
* 判断通道是否满足金额区间。
*
* @param PaymentChannel $channel 渠道
* @param int $payAmount 支付金额(分)
* @return bool 是否可用
*/
private function isAmountAllowed(PaymentChannel $channel, int $payAmount): bool
{
if ((int) $channel->min_amount > 0 && $payAmount < (int) $channel->min_amount) {
@@ -175,6 +225,15 @@ class PaymentRouteResolverService extends BaseService
return true;
}
/**
* 判断通道是否满足日限额和日成功笔数。
*
* @param PaymentChannel $channel 渠道
* @param int $payAmount 支付金额(分)
* @param string $statDate 统计日期
* @param object|null $stat 当日统计数据
* @return bool 是否可用
*/
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) {
@@ -196,9 +255,17 @@ class PaymentRouteResolverService extends BaseService
return true;
}
/**
* 按路由模式整理候选通道顺序。
*
* @param array $candidates 候选通道列表
* @param int $routeMode 路由模式
* @return array 排序后的候选列表
*/
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']
@@ -216,6 +283,14 @@ class PaymentRouteResolverService extends BaseService
return $candidates;
}
/**
* 根据路由模式选择最终通道。
*
* @param array $candidates 候选通道列表
* @param int $routeMode 路由模式
* @param int $pollGroupId 轮询分组ID
* @return array 选中的通道候选
*/
private function selectChannel(array $candidates, int $routeMode, int $pollGroupId): array
{
if (count($candidates) === 1) {
@@ -230,6 +305,12 @@ class PaymentRouteResolverService extends BaseService
};
}
/**
* 按权重随机选择通道。
*
* @param array $candidates 候选通道列表
* @return array 选中的通道候选
*/
private function selectWeightedChannel(array $candidates): array
{
$totalWeight = array_sum(array_map(static fn (array $item) => max(1, (int) $item['weight']), $candidates));
@@ -245,6 +326,13 @@ class PaymentRouteResolverService extends BaseService
return $candidates[0];
}
/**
* 按轮询游标顺序选择通道。
*
* @param array $candidates 候选通道列表
* @param int $pollGroupId 轮询分组ID
* @return array 选中的通道候选
*/
private function selectSequentialChannel(array $candidates, int $pollGroupId): array
{
if ($pollGroupId <= 0) {
@@ -252,17 +340,27 @@ class PaymentRouteResolverService extends BaseService
}
try {
// 用 Redis 维护跨进程共享的轮询游标,避免每个 PHP 进程各选各的。
$cursorKey = sprintf('payment:route:round_robin:%d', $pollGroupId);
$cursor = (int) Redis::incr($cursorKey);
// 游标保留一个较长的生命周期,避免 Redis 清理后轮询顺序完全丢失。
Redis::expire($cursorKey, 30 * 86400);
// Redis 自增从 1 开始,这里转成 0 基索引后再对候选集取模。
$index = max(0, ($cursor - 1) % count($candidates));
return $candidates[$index] ?? $candidates[0];
} catch (\Throwable) {
// Redis 不可用时降级成首个候选,保证路由还能继续往下走。
return $candidates[0];
}
}
/**
* 优先返回默认通道,否则返回首个候选。
*
* @param array $candidates 候选通道列表
* @return array 选中的通道候选
*/
private function selectDefaultChannel(array $candidates): array
{
foreach ($candidates as $candidate) {

View File

@@ -5,12 +5,18 @@ namespace app\service\payment\runtime;
use app\common\base\BaseService;
/**
* 支付路由门面服务。
* 支付路由服务。
*
* 对外保留原有调用契约,内部委托给路由解析服务
* @property PaymentRouteResolverService $resolverService 路由解析服务
*/
class PaymentRouteService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentRouteResolverService $resolverService 路由解析服务
* @return void
*/
public function __construct(
protected PaymentRouteResolverService $resolverService
) {
@@ -18,9 +24,18 @@ class PaymentRouteService extends BaseService
/**
* 按商户分组和支付方式解析路由。
*
* @param int $merchantGroupId 商户分组ID
* @param int $payTypeId 支付类型ID
* @param int $payAmount 支付金额(分)
* @param array $context 路由上下文,例如统计日期、额外筛选条件
* @return array 路由解析结果
*/
public function resolveByMerchantGroup(int $merchantGroupId, int $payTypeId, int $payAmount, array $context = []): array
{
return $this->resolverService->resolveByMerchantGroup($merchantGroupId, $payTypeId, $payAmount, $context);
}
}