Files
webman-buildadmin/app/common/library/game/DepositChannel.php
zhenhui 1b8d947f97 0.使用模拟数据进行充值和提现
1.优化提现接口/api/finance/withdrawCreate
2.优化充值接口/api/finance/depositCreate
2026-05-20 15:57:19 +08:00

571 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}