mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-27 12:34:28 +08:00
更新统一使用 PHPDoc + PSR-19 标准注释
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user