| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- <?php
- namespace App\Services\Payment;
- use InvalidArgumentException;
- /** 支付商及协议目录。独立于商户配置表;只登记已实现的支付服务。 */
- class PaymentProviderCatalog
- {
- public function __construct(private ?array $providerStatuses = null)
- {
- }
- private const PROVIDERS = [
- 'sanjin' => ['label' => '三斤支付', 'directions' => [1], 'signature' => 'MD5'],
- 'qianbao' => ['label' => '钱宝支付', 'directions' => [2], 'signature' => 'MD5'],
- 'jd' => ['label' => 'JD支付', 'directions' => [1, 2], 'signature' => 'MD5'],
- 'no' => ['label' => 'NO支付', 'directions' => [1, 2], 'signature' => 'SHA256'],
- 'zimu' => ['label' => '808支付', 'directions' => [1, 2], 'signature' => 'MD5'],
- ];
- // data_type => 通道 type => [支付商代码, 支付方式名称]
- private const METHODS = [
- 1 => [
- 'wxsm' => ['sanjin', '微信扫码'],
- 'ylsm' => ['sanjin', '银联扫码'],
- 'szrmb' => ['sanjin', '数字人民币'],
- 'zfbsm' => ['sanjin', '支付宝扫码'],
- 'zfbge' => ['sanjin', '支付宝固额'],
- 'zfbzk' => ['sanjin', '支付宝转卡'],
- 'sdjt' => ['sanjin', '手动金条'],
- 'JDpay' => ['jd', 'JD钱包'],
- 'NOpay12' => ['no', 'NO扫码支付'],
- 'NOpay13' => ['no', 'NO余额支付'],
- 'ZIMUpay' => ['zimu', '808充值'],
- ],
- 2 => [
- 'DF001' => ['qianbao', '银行卡'],
- 'DF002' => ['qianbao', '支付宝'],
- 'DF005' => ['qianbao', '数字人民币'],
- 'JDpay' => ['jd', 'JD钱包'],
- 'NOwithdraw' => ['no', 'NO快捷提现'],
- 'ZIMUwithdraw' => ['zimu', '808账户提现'],
- 'ZIMUcash' => ['zimu', '808人民币代付'],
- ],
- ];
- public static function codes(): array
- {
- return array_keys(self::PROVIDERS);
- }
- public static function definitions(): array
- {
- return self::PROVIDERS;
- }
- public static function label(string $code): string
- {
- return self::PROVIDERS[$code]['label'] ?? $code;
- }
- public static function channelTypesForProvider(string $provider, ?int $direction = null): array
- {
- $types = [];
- foreach (self::METHODS as $dataType => $methods) {
- if ($direction !== null && $direction !== $dataType) continue;
- foreach ($methods as $type => [$code]) {
- if ($code === $provider) $types[] = $type;
- }
- }
- return array_values(array_unique($types));
- }
- /** Definitions for creating channels, intentionally independent of existing channels/groups. */
- public static function channelFormOptions(int $dataType, ?array $statuses = null): array
- {
- if (!in_array($dataType, [1, 2, 3], true)) throw new InvalidArgumentException('通道方向错误');
- $companies = [];
- $types = [];
- foreach (self::PROVIDERS as $code => $provider) {
- if (in_array($dataType, $provider['directions'], true)) {
- $enabled = $statuses === null || ($statuses[$code] ?? 0) === 1;
- $companies[] = ['value' => $code, 'label' => $provider['label'], 'status' => $enabled ? 1 : 0,
- 'disabled' => !$enabled, 'disabled_reason' => $enabled ? '' : '支付公司已禁用'];
- }
- }
- foreach (self::METHODS[$dataType] ?? [] as $type => [$company, $label]) {
- $enabled = $statuses === null || ($statuses[$company] ?? 0) === 1;
- $types[] = ['value' => $type, 'label' => $label, 'from' => 1,
- 'payment_company' => $company, 'payment_company_label' => self::label($company),
- 'disabled' => !$enabled, 'disabled_reason' => $enabled ? '' : '支付公司已禁用'];
- }
- $local = match ($dataType) {
- 1 => ['usdt' => [2, 'USDT充值'], 'rgcz' => [3, '人工充值']],
- 2 => ['usdt' => [2, 'USDT提现'], 'rgtx' => [3, '人工提现']],
- 3 => ['recharge' => [1, '即充即送'], 'old_user' => [1, '老用户回归'], 'yuebao' => [1, '余额宝']],
- };
- foreach ($local as $type => [$from, $label]) {
- $types[] = ['value' => $type, 'label' => $label, 'from' => $from,
- 'payment_company' => null, 'payment_company_label' => '', 'disabled' => false, 'disabled_reason' => ''];
- }
- return [
- 'data_type' => $dataType,
- 'from_options' => $dataType === 3 ? [['value' => 1, 'label' => '活动']] : [
- ['value' => 1, 'label' => '第三方支付'], ['value' => 2, 'label' => 'USDT'], ['value' => 3, 'label' => '人工'],
- ],
- 'payment_companies' => $companies,
- 'types' => $types,
- ];
- }
- public static function normalizeChannelSelection(array $data): array
- {
- $definitions = array_column(self::channelFormOptions((int)$data['data_type'])['types'], null, 'value');
- $definition = $definitions[(string)$data['type']] ?? null;
- if (!$definition) throw new InvalidArgumentException('请选择已接入的通道类型');
- $company = (string)($data['payment_company'] ?? '');
- if ($company !== '' && $company !== $definition['payment_company']) {
- throw new InvalidArgumentException('支付公司与通道类型不匹配,请重新选择类型');
- }
- if (isset($data['from']) && (int)$data['from'] !== $definition['from']) {
- throw new InvalidArgumentException('通道来源与类型不匹配');
- }
- $data['from'] = $definition['from'];
- unset($data['payment_company']);
- return $data;
- }
- public static function channelDisplay(array $channel, ?array $statuses = null): array
- {
- $identity = self::channelIdentity($channel);
- $label = $identity ? self::label($identity['provider_code']) : '';
- $companyStatus = $identity ? ($statuses === null ? 1 : (int)($statuses[$identity['provider_code']] ?? 0)) : null;
- return [
- 'payment_company' => $identity['provider_code'] ?? null,
- 'payment_company_label' => $label,
- 'display_name' => ($label === '' ? '' : $label . ' / ') . (string)$channel['name'],
- 'company_status' => $companyStatus,
- 'effective_status' => (int)($channel['status'] ?? 1) === 1 && $companyStatus !== 0 && empty($channel['deleted_at']) ? 1 : 0,
- ];
- }
- public static function direction(?string $kind): ?int
- {
- return match ($kind) {
- null, '' => null,
- 'deposit', 'collection_scan', 'collection_third_party' => 1,
- 'withdraw' => 2,
- default => throw new InvalidArgumentException('不支持的支付配置类型'),
- };
- }
- public static function channelIdentity(array $channel): ?array
- {
- if ((int)($channel['from'] ?? 0) !== 1) return null;
- $type = (string)($channel['type'] ?? '');
- $method = self::METHODS[(int)($channel['data_type'] ?? 0)][$type] ?? null;
- if (!$method) return null;
- return ['provider_code' => $method[0], 'method_code' => $type, 'method_label' => $method[1]];
- }
- public static function validateSelection(array $channel, string $provider, string $method, int $direction): void
- {
- $identity = self::channelIdentity($channel);
- if ((int)($channel['data_type'] ?? 0) !== $direction || !$identity) {
- throw new InvalidArgumentException('该通道不属于已接入的第三方支付通道');
- }
- if ($identity['provider_code'] !== $provider) {
- throw new InvalidArgumentException('支付公司与所选通道不匹配');
- }
- if ($identity['method_code'] !== $method) {
- throw new InvalidArgumentException('支付方式与所选通道不匹配');
- }
- }
- public function options(array $channels, array $groups, array $filters = []): array
- {
- $direction = self::direction($filters['kind'] ?? null);
- $isCollection = in_array($filters['kind'] ?? '', ['collection_scan', 'collection_third_party'], true);
- $company = (string)($filters['payment_company'] ?? '');
- $collector = (string)($filters['provider_name'] ?? '');
- $selectedProvider = $isCollection ? $collector : $company;
- $selectedMethod = (string)($isCollection ? ($filters['collection_method'] ?? '') : ($filters['payment_method'] ?? ''));
- $groups = array_map(function ($group) {
- foreach (['recharge_type', 'withdraw_type', 'activity_type'] as $field) {
- $group[$field] = self::types($group[$field] ?? []);
- }
- return ['id' => (int)$group['id'], 'name' => (string)$group['name']] + $group;
- }, $groups);
- $formatted = [];
- foreach ($channels as $channel) {
- $dataType = (int)$channel['data_type'];
- $groupField = [1 => 'recharge_type', 2 => 'withdraw_type', 3 => 'activity_type'][$dataType] ?? null;
- $ids = [];
- foreach ($groups as $group) {
- if ($groupField && in_array((string)$channel['type'], $group[$groupField], true)) $ids[] = $group['id'];
- }
- $identity = self::channelIdentity($channel);
- $enabled = (int)$channel['status'] === 1;
- $companyEnabled = !$identity || $this->companyEnabled($identity['provider_code']);
- $formatted[] = [
- 'id' => (int)$channel['id'], 'data_type' => $dataType,
- 'name' => (string)$channel['name'], 'key' => (string)$channel['key'], 'type' => (string)$channel['type'],
- 'rate' => bcadd((string)$channel['rate'], '0', 4),
- 'fee_rate' => bcmul((string)$channel['rate'], '100', 4),
- 'min_amount' => $channel['min'] ?? null, 'max_amount' => $channel['max'] ?? null,
- 'fixed_amounts' => self::types($channel['fixed'] ?? []),
- 'sort' => (int)$channel['sort'], 'status' => (int)$channel['status'],
- 'company_status' => $identity ? ($companyEnabled ? 1 : 0) : null,
- 'available_group_ids' => $ids,
- 'provider_code' => $identity['provider_code'] ?? null,
- 'payment_method' => $identity['method_code'] ?? null,
- 'payment_method_label' => $identity['method_label'] ?? null,
- 'selectable' => $identity && $companyEnabled && $enabled && $ids !== [],
- 'unavailable_reason' => !$identity ? '不属于第三方支付通道' : (!$companyEnabled ? '支付公司已禁用' : (!$enabled ? '通道已停用' : ($ids === [] ? '没有包含此通道的组合' : ''))),
- ];
- }
- $selectedChannels = array_values(array_filter($formatted, static function ($channel) use ($direction, $selectedProvider, $selectedMethod) {
- if ($direction !== null && ($channel['data_type'] !== $direction || !$channel['provider_code'])) return false;
- if ($selectedProvider !== '' && $channel['provider_code'] !== $selectedProvider) return false;
- return $selectedMethod === '' || $channel['payment_method'] === $selectedMethod;
- }));
- return [
- 'payment_companies' => $this->companies($formatted, $direction),
- 'payment_methods' => $this->methods($formatted, $direction, $company),
- 'collection_providers' => $this->companies($formatted, 1),
- 'collection_methods' => $this->methods($formatted, 1, $collector),
- 'recharge_channels' => $selectedChannels,
- 'channel_groups' => $groups,
- 'levels' => array_map(static fn ($group) => ['value' => $group['id'], 'label' => $group['name']], $groups),
- 'credential_source' => 'environment',
- ];
- }
- private function companies(array $channels, ?int $direction): array
- {
- $result = [];
- foreach (self::PROVIDERS as $code => $provider) {
- if ($direction !== null && !in_array($direction, $provider['directions'], true)) continue;
- $matching = array_filter($channels, static fn ($channel) => $channel['provider_code'] === $code
- && ($direction === null || $channel['data_type'] === $direction));
- $result[] = [
- 'value' => $code, 'label' => $provider['label'],
- 'signature_algorithm' => $provider['signature'],
- 'secret_field' => $direction === null ? null : ($direction === 2 ? 'withdrawal_secret' : 'deposit_secret'),
- ] + $this->companyAvailability($code, $matching);
- }
- return $result;
- }
- private function methods(array $channels, ?int $direction, string $provider): array
- {
- $result = [];
- foreach (self::METHODS as $dataType => $methods) {
- if ($direction !== null && $direction !== $dataType) continue;
- foreach ($methods as $code => [$company, $label]) {
- if ($provider !== '' && $provider !== $company) continue;
- $matching = array_filter($channels, static fn ($channel) => $channel['provider_code'] === $company
- && $channel['data_type'] === $dataType && $channel['payment_method'] === $code);
- $selectable = array_filter($matching, static fn ($channel) => $channel['selectable']);
- $result[] = [
- 'value' => $code, 'label' => $label, 'provider_code' => $company, 'data_type' => $dataType,
- 'channel_ids' => array_values(array_column($matching, 'id')),
- 'selectable_channel_ids' => array_values(array_column($selectable, 'id')),
- ] + $this->companyAvailability($company, $matching);
- }
- }
- return $result;
- }
- private function companyEnabled(string $code): bool
- {
- return $this->providerStatuses === null || ($this->providerStatuses[$code] ?? 0) === 1;
- }
- private function companyAvailability(string $code, array $channels): array
- {
- if (!$this->companyEnabled($code)) {
- return ['disabled' => true, 'disabled_reason_code' => 'provider_disabled', 'disabled_reason' => '支付公司已禁用'];
- }
- return $this->availability($channels);
- }
- private function availability(array $channels): array
- {
- if (array_filter($channels, static fn ($channel) => $channel['selectable']) !== []) {
- return ['disabled' => false, 'disabled_reason_code' => '', 'disabled_reason' => ''];
- }
- if ($channels === []) {
- $code = 'channel_missing';
- $reason = '未配置匹配的通道';
- } elseif (array_filter($channels, static fn ($channel) => $channel['status'] === 1) === []) {
- $code = 'channel_disabled';
- $reason = '匹配通道均已停用';
- } else {
- $code = 'group_missing';
- $reason = '已启用通道尚未加入通道组合';
- }
- return ['disabled' => true, 'disabled_reason_code' => $code, 'disabled_reason' => $reason];
- }
- private static function types($value): array
- {
- $values = is_array($value) ? $value : explode(',', (string)$value);
- return array_values(array_filter(array_map(static fn ($item) => trim((string)$item), $values), static fn ($item) => $item !== ''));
- }
- }
|