571 lines
19 KiB
PHP
571 lines
19 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\library\game;
|
||
|
||
use app\common\library\finance\MockPay;
|
||
use InvalidArgumentException;
|
||
use support\think\Db;
|
||
|
||
/**
|
||
* 充值支付渠道:优先读取 game_config.finance_cashier.channels;无此键时回退 game_config.deposit_channel(迁移期镜像)
|
||
*
|
||
* 每项:code(须在代码/环境注册表内)、sort、status(0/1)。内置 `ddpay`(DDPay)、`mock`(模拟支付,见 FINANCE_MOCK_PAY_ENABLED)。
|
||
*
|
||
* 渠道展示名以代码注册表为准;运营只配置开关、排序与支持币种,默认兼容全部充值档位。
|
||
*/
|
||
final class DepositChannel
|
||
{
|
||
public const CONFIG_KEY = 'deposit_channel';
|
||
|
||
/**
|
||
* @return array<string, array{name: string, name_en: string, sort: int}>
|
||
*/
|
||
public static function codeRegistry(): array
|
||
{
|
||
$base = [
|
||
'ddpay' => ['name' => 'DDPay', 'name_en' => 'DDPay', 'sort' => 10],
|
||
'mock' => ['name' => '模拟支付', 'name_en' => 'Mock Pay', 'sort' => 5],
|
||
];
|
||
$extra = self::registryFromEnv();
|
||
foreach ($extra as $code => $meta) {
|
||
if (!is_string($code) || trim($code) === '' || !is_array($meta)) {
|
||
continue;
|
||
}
|
||
$normCode = strtolower(trim($code));
|
||
if (!preg_match('/^[a-z0-9_\-]{1,24}$/', $normCode)) {
|
||
continue;
|
||
}
|
||
$name = isset($meta['name']) && is_string($meta['name']) ? trim($meta['name']) : '';
|
||
$nameEn = isset($meta['name_en']) && is_string($meta['name_en']) ? trim($meta['name_en']) : '';
|
||
$sort = isset($meta['sort']) && is_numeric($meta['sort']) ? intval($meta['sort']) : 50;
|
||
if ($name === '') {
|
||
continue;
|
||
}
|
||
$base[$normCode] = [
|
||
'name' => $name,
|
||
'name_en' => $nameEn !== '' ? $nameEn : $name,
|
||
'sort' => $sort,
|
||
];
|
||
}
|
||
return $base;
|
||
}
|
||
|
||
/**
|
||
* @return array<string, mixed>
|
||
*/
|
||
private static function registryFromEnv(): array
|
||
{
|
||
$raw = getenv('DEPOSIT_CHANNELS_REGISTRY_JSON');
|
||
if (!is_string($raw) || trim($raw) === '') {
|
||
return [];
|
||
}
|
||
$decoded = json_decode($raw, true);
|
||
if (!is_array($decoded)) {
|
||
return [];
|
||
}
|
||
$out = [];
|
||
foreach ($decoded as $item) {
|
||
if (!is_array($item)) {
|
||
continue;
|
||
}
|
||
$code = isset($item['code']) && is_string($item['code']) ? strtolower(trim($item['code'])) : '';
|
||
if ($code === '') {
|
||
continue;
|
||
}
|
||
$out[$code] = $item;
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
public static function parseOverridesFromConfigValue($raw): array
|
||
{
|
||
if (!is_string($raw) || trim($raw) === '') {
|
||
return [];
|
||
}
|
||
$decoded = json_decode($raw, true);
|
||
if (!is_array($decoded)) {
|
||
return [];
|
||
}
|
||
$list = isset($decoded['channels']) && is_array($decoded['channels']) ? $decoded['channels'] : $decoded;
|
||
|
||
return self::normalizeOverrides($list);
|
||
}
|
||
|
||
/**
|
||
* 库中「运营覆盖」原始列表(未与注册表合并):finance_cashier 含 channels 键时用其值;否则读 deposit_channel
|
||
*
|
||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||
*/
|
||
public static function parseStoredOverridesFromDb(): array
|
||
{
|
||
$fcRow = Db::name('game_config')->where('config_key', FinanceCashierConfig::CONFIG_KEY)->find();
|
||
$rawFc = is_array($fcRow) ? ($fcRow['config_value'] ?? null) : null;
|
||
$decoded = null;
|
||
if (is_string($rawFc) && trim($rawFc) !== '') {
|
||
$tmp = json_decode($rawFc, true);
|
||
if (is_array($tmp)) {
|
||
$decoded = $tmp;
|
||
}
|
||
}
|
||
$channelsKeyPresent = is_array($decoded) && array_key_exists('channels', $decoded);
|
||
if ($channelsKeyPresent) {
|
||
$list = isset($decoded['channels']) && is_array($decoded['channels']) ? $decoded['channels'] : [];
|
||
|
||
return self::normalizeOverrides($list);
|
||
}
|
||
$depRow = Db::name('game_config')->where('config_key', self::CONFIG_KEY)->find();
|
||
$depRaw = is_array($depRow) ? ($depRow['config_value'] ?? null) : null;
|
||
|
||
return self::parseOverridesFromConfigValue($depRaw);
|
||
}
|
||
|
||
/**
|
||
* 与注册表合并并排序后的有效渠道行(业务侧统一入口)
|
||
*
|
||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||
*/
|
||
public static function effectiveRowsFromDb(): array
|
||
{
|
||
$stored = self::parseStoredOverridesFromDb();
|
||
|
||
return self::effectiveOverrides(self::expandRowsForAdmin($stored));
|
||
}
|
||
|
||
/**
|
||
* @param list<mixed> $items
|
||
*
|
||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||
*/
|
||
public static function normalizeOverrides(array $items): array
|
||
{
|
||
$out = [];
|
||
foreach ($items as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$code = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||
if ($code === '') {
|
||
continue;
|
||
}
|
||
$sort = isset($row['sort']) && is_numeric($row['sort']) ? intval($row['sort']) : 0;
|
||
$status = isset($row['status']) && is_numeric($row['status']) ? intval($row['status']) : 1;
|
||
$status = $status === 1 ? 1 : 0;
|
||
$currencyCodes = null;
|
||
if (array_key_exists('currency_codes', $row)) {
|
||
if (is_array($row['currency_codes'])) {
|
||
$currencyCodes = self::normalizeCurrencyCodes($row['currency_codes']);
|
||
} else {
|
||
$currencyCodes = null;
|
||
}
|
||
}
|
||
$out[] = [
|
||
'code' => $code,
|
||
'sort' => $sort,
|
||
'status' => $status,
|
||
'tier_ids' => [],
|
||
// null 表示“兼容全部充值币种”(历史配置默认行为)
|
||
// 空数组 [] 表示“不支持任何充值币种”(运营可用作显式禁用某币种)
|
||
'currency_codes' => $currencyCodes,
|
||
];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 归一化为小写/去空并返回大写币种码列表(允许为空数组)。
|
||
*
|
||
* @param mixed $raw
|
||
* @return list<string>
|
||
*/
|
||
private static function normalizeCurrencyCodes(mixed $raw): array
|
||
{
|
||
if (!is_array($raw)) {
|
||
return [];
|
||
}
|
||
$out = [];
|
||
foreach ($raw as $c) {
|
||
if (!is_string($c) && !is_numeric($c)) {
|
||
continue;
|
||
}
|
||
$s = is_string($c) ? trim($c) : strval($c);
|
||
$s = strtoupper($s);
|
||
if (!preg_match('/^[A-Z0-9]{2,12}$/', $s)) {
|
||
continue;
|
||
}
|
||
$out[] = $s;
|
||
}
|
||
$out = array_values(array_unique($out));
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 合并注册表与运营覆盖;若库中无覆盖则对注册表内全部渠道启用默认行
|
||
*
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $overrides
|
||
*
|
||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||
*/
|
||
public static function effectiveOverrides(array $overrides): array
|
||
{
|
||
$registry = self::codeRegistry();
|
||
$byCode = [];
|
||
foreach ($overrides as $row) {
|
||
if (!isset($registry[$row['code']])) {
|
||
continue;
|
||
}
|
||
$byCode[$row['code']] = $row;
|
||
}
|
||
if ($byCode === []) {
|
||
foreach ($registry as $code => $meta) {
|
||
$sortMeta = $meta['sort'] ?? 10;
|
||
$sortVal = is_numeric($sortMeta) ? intval($sortMeta) : 10;
|
||
$byCode[$code] = [
|
||
'code' => $code,
|
||
'sort' => $sortVal,
|
||
'status' => 1,
|
||
'tier_ids' => [],
|
||
'currency_codes' => null, // 默认兼容全部币种(历史行为)
|
||
];
|
||
}
|
||
}
|
||
$list = array_values($byCode);
|
||
usort($list, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
|
||
return strcmp($a['code'], $b['code']);
|
||
});
|
||
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* @param array{code: string, sort: int, status: int, tier_ids: list<string>} $overrideRow
|
||
*/
|
||
public static function isTierAllowed(array $overrideRow, string $tierId): bool
|
||
{
|
||
// 渠道不再配置档位白名单:默认兼容全部充值档位。
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string, mixed>> $effectiveRows
|
||
*/
|
||
public static function findMergedByCode(array $effectiveRows, string $code): ?array
|
||
{
|
||
$norm = strtolower(trim($code));
|
||
foreach ($effectiveRows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$c = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||
if ($c === $norm) {
|
||
return $row;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $overrideRows
|
||
*
|
||
* @return list<array{code: string, name: string, sort: int}>
|
||
*/
|
||
public static function channelsForTier(string $tierId, array $overrideRows, string $lang, string $fiatCurrencyCode = ''): array
|
||
{
|
||
$registry = self::codeRegistry();
|
||
$out = [];
|
||
foreach ($overrideRows as $row) {
|
||
if (($row['status'] ?? 0) !== 1) {
|
||
continue;
|
||
}
|
||
$code = $row['code'];
|
||
if (!isset($registry[$code])) {
|
||
continue;
|
||
}
|
||
if ($code === MockPay::CHANNEL_CODE && !MockPay::isEnabled()) {
|
||
continue;
|
||
}
|
||
if ($fiatCurrencyCode !== '' && !self::isCurrencyAllowedForRow($row, $fiatCurrencyCode)) {
|
||
continue;
|
||
}
|
||
$meta = $registry[$code];
|
||
$name = self::pickLangName($meta, $lang);
|
||
$sortRaw = $row['sort'] ?? 0;
|
||
$sortVal = is_numeric($sortRaw) ? intval($sortRaw) : 0;
|
||
$out[] = [
|
||
'code' => $code,
|
||
'name' => $name,
|
||
'sort' => $sortVal,
|
||
];
|
||
}
|
||
usort($out, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
|
||
return strcmp($a['code'], $b['code']);
|
||
});
|
||
|
||
return $out;
|
||
}
|
||
|
||
private static function isCurrencyAllowedForRow(array $row, string $fiatCurrencyCode): bool
|
||
{
|
||
$normCurrency = strtoupper(trim($fiatCurrencyCode));
|
||
if ($normCurrency === '') {
|
||
return true;
|
||
}
|
||
$cc = $row['currency_codes'] ?? null;
|
||
if ($cc === null) {
|
||
// 历史/默认配置:不填则表示兼容全部币种
|
||
return true;
|
||
}
|
||
if (!is_array($cc)) {
|
||
return true;
|
||
}
|
||
// 显式空数组:不支持任何币种
|
||
if ($cc === []) {
|
||
return false;
|
||
}
|
||
return in_array($normCurrency, $cc, true);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, string> $meta
|
||
*/
|
||
private static function pickLangName(array $meta, string $lang): string
|
||
{
|
||
$name = isset($meta['name']) && is_string($meta['name']) ? $meta['name'] : '';
|
||
$nameEn = isset($meta['name_en']) && is_string($meta['name_en']) ? $meta['name_en'] : '';
|
||
$normalized = strtolower(str_replace('_', '-', trim($lang)));
|
||
$isEn = $normalized === 'en' || str_starts_with($normalized, 'en-');
|
||
if ($isEn) {
|
||
return $nameEn !== '' ? $nameEn : $name;
|
||
}
|
||
|
||
return $name !== '' ? $name : $nameEn;
|
||
}
|
||
|
||
/**
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $effectiveRows
|
||
*/
|
||
public static function assertChannelAllowsTier(string $channelCode, string $tierId, array $effectiveRows): bool
|
||
{
|
||
$row = self::findMergedByCode($effectiveRows, $channelCode);
|
||
if ($row === null) {
|
||
return false;
|
||
}
|
||
if (($row['status'] ?? 0) !== 1) {
|
||
return false;
|
||
}
|
||
|
||
return self::isTierAllowed($row, $tierId);
|
||
}
|
||
|
||
/**
|
||
* @param array<array{code: string, sort: int, status: int, tier_ids: list<string>, currency_codes: (list<string>|null)}> $effectiveRows
|
||
*/
|
||
public static function assertChannelAllowsCurrency(string $channelCode, string $fiatCurrencyCode, array $effectiveRows): bool
|
||
{
|
||
$row = self::findMergedByCode($effectiveRows, $channelCode);
|
||
if ($row === null) {
|
||
return false;
|
||
}
|
||
if (($row['status'] ?? 0) !== 1) {
|
||
return false;
|
||
}
|
||
|
||
return self::isCurrencyAllowedForRow($row, $fiatCurrencyCode);
|
||
}
|
||
|
||
/**
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $effectiveRows
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
public static function enabledPayChannelCodes(array $effectiveRows): array
|
||
{
|
||
$registry = self::codeRegistry();
|
||
$codes = [];
|
||
foreach ($effectiveRows as $row) {
|
||
if (($row['status'] ?? 0) !== 1) {
|
||
continue;
|
||
}
|
||
$c = isset($row['code']) && is_string($row['code']) ? $row['code'] : '';
|
||
if ($c !== '' && isset($registry[$c])) {
|
||
$codes[] = $c;
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($codes));
|
||
}
|
||
|
||
/**
|
||
* 当前服务端已对接自动出金的渠道(与 withdrawCreate 校验一致)
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
public static function withdrawPayoutChannelCodes(): array
|
||
{
|
||
$codes = ['ddpay'];
|
||
if (MockPay::isEnabled()) {
|
||
$codes[] = MockPay::CHANNEL_CODE;
|
||
}
|
||
|
||
return $codes;
|
||
}
|
||
|
||
/**
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $effectiveRows
|
||
*/
|
||
public static function assertChannelEnabled(string $channelCode, array $effectiveRows): bool
|
||
{
|
||
$row = self::findMergedByCode($effectiveRows, $channelCode);
|
||
if ($row === null) {
|
||
return false;
|
||
}
|
||
|
||
return ($row['status'] ?? 0) === 1;
|
||
}
|
||
|
||
/**
|
||
* 提现页可选支付渠道(仅返回已启用且已对接出金的渠道)
|
||
*
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $effectiveRows
|
||
*
|
||
* @return list<array{code: string, name: string, sort: int}>
|
||
*/
|
||
public static function channelsForWithdraw(array $effectiveRows, string $lang): array
|
||
{
|
||
$registry = self::codeRegistry();
|
||
$allowed = self::withdrawPayoutChannelCodes();
|
||
$out = [];
|
||
foreach ($effectiveRows as $row) {
|
||
if (($row['status'] ?? 0) !== 1) {
|
||
continue;
|
||
}
|
||
$code = isset($row['code']) && is_string($row['code']) ? $row['code'] : '';
|
||
if ($code === '' || !in_array($code, $allowed, true)) {
|
||
continue;
|
||
}
|
||
if (!isset($registry[$code])) {
|
||
continue;
|
||
}
|
||
$meta = $registry[$code];
|
||
$sortRaw = $row['sort'] ?? 0;
|
||
$sortVal = is_numeric($sortRaw) ? intval($sortRaw) : 0;
|
||
$out[] = [
|
||
'code' => $code,
|
||
'name' => self::pickLangName($meta, $lang),
|
||
'sort' => $sortVal,
|
||
];
|
||
}
|
||
usort($out, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
|
||
return strcmp($a['code'], $b['code']);
|
||
});
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string, mixed>> $items
|
||
*
|
||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||
*/
|
||
public static function prepareOverridesForSave(array $items): array
|
||
{
|
||
$registry = self::codeRegistry();
|
||
$seen = [];
|
||
$out = [];
|
||
foreach ($items as $idx => $row) {
|
||
if (!is_array($row)) {
|
||
throw new InvalidArgumentException('Row format error');
|
||
}
|
||
$code = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||
if ($code === '' || !isset($registry[$code])) {
|
||
throw new InvalidArgumentException('Channel code is not registered');
|
||
}
|
||
if (isset($seen[$code])) {
|
||
throw new InvalidArgumentException('Duplicate channel code');
|
||
}
|
||
$seen[$code] = true;
|
||
$norm = self::normalizeOverrides([array_merge($row, ['code' => $code])]);
|
||
if ($norm === []) {
|
||
throw new InvalidArgumentException('Invalid channel row data');
|
||
}
|
||
$out[] = $norm[0];
|
||
}
|
||
usort($out, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
|
||
return strcmp($a['code'], $b['code']);
|
||
});
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $items
|
||
*/
|
||
public static function encodeForDb(array $items): string
|
||
{
|
||
$wrapped = ['channels' => $items];
|
||
$encoded = json_encode($wrapped, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
if ($encoded === false) {
|
||
throw new InvalidArgumentException('JSON encode failed');
|
||
}
|
||
|
||
return $encoded;
|
||
}
|
||
|
||
/**
|
||
* 后台编辑用:为注册表内每个 code 补齐一行(合并库内覆盖)
|
||
*
|
||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $storedOverrides
|
||
*
|
||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||
*/
|
||
public static function expandRowsForAdmin(array $storedOverrides): array
|
||
{
|
||
$registry = self::codeRegistry();
|
||
$effective = self::effectiveOverrides($storedOverrides);
|
||
$byCode = [];
|
||
foreach ($effective as $r) {
|
||
$byCode[$r['code']] = $r;
|
||
}
|
||
$items = [];
|
||
foreach ($registry as $code => $meta) {
|
||
$sortMeta = $meta['sort'] ?? 10;
|
||
$sortDefault = is_numeric($sortMeta) ? intval($sortMeta) : 10;
|
||
$items[] = $byCode[$code] ?? [
|
||
'code' => $code,
|
||
'sort' => $sortDefault,
|
||
'status' => 1,
|
||
'tier_ids' => [],
|
||
'currency_codes' => null,
|
||
];
|
||
}
|
||
usort($items, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
|
||
return strcmp($a['code'], $b['code']);
|
||
});
|
||
|
||
return $items;
|
||
}
|
||
}
|