Files
webman-buildadmin/app/common/library/game/FinanceCashierConfig.php
zhenhui c7fc754573 1.配置新版支付模块-菜单和接口都已重构
2.优化充值提现页面
3.菜单翻译问题
4.备份数据库
2026-04-30 11:37:46 +08:00

445 lines
19 KiB
PHP
Raw Permalink 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 InvalidArgumentException;
/**
* 移动端支付/收款展示game_config.finance_cashier含 channels 充值渠道)
*
* 货币列表每行含:
* - deposit_coins_per_fiat充值参考每支付 1 单位该货币到账的平台币数量
* - withdraw_coins_per_fiat提现换算每兑换 1 单位该货币所需平台币数量
*/
final class FinanceCashierConfig
{
public const CONFIG_KEY = 'finance_cashier';
/**
* @return array<string, mixed>
*/
public static function defaultPayload(): array
{
return [
'platform_coin' => [
'label_zh' => '钻石',
'label_en' => 'Diamonds',
],
'currencies' => [
[
'code' => 'MYR',
'label_zh' => '马来西亚林吉特',
'label_en' => 'Malaysian Ringgit',
'sort' => 10,
'deposit_coins_per_fiat' => '100',
'withdraw_coins_per_fiat' => '100',
],
[
'code' => 'THB',
'label_zh' => '泰铢',
'label_en' => 'Thai Baht',
'sort' => 20,
'deposit_coins_per_fiat' => '100',
'withdraw_coins_per_fiat' => '100',
],
],
'deposit_banks' => [
['currency_code' => 'MYR', 'code' => 'pbb', 'name_zh' => 'Public Bank', 'name_en' => 'Public Bank', 'sort' => 10],
['currency_code' => 'MYR', 'code' => 'mbb', 'name_zh' => 'Maybank2U', 'name_en' => 'Maybank2U', 'sort' => 20],
['currency_code' => 'THB', 'code' => '106', 'name_zh' => 'BANGKOK BANK PUBLIC COMPANY LTD.', 'name_en' => 'BANGKOK BANK PUBLIC COMPANY LTD.', 'sort' => 10],
['currency_code' => 'THB', 'code' => '107', 'name_zh' => 'KASIKORNBANK PUBLIC COMPANY LIMITED', 'name_en' => 'KASIKORNBANK PUBLIC COMPANY LIMITED', 'sort' => 20],
],
'withdraw_banks' => [
['currency_code' => 'MYR', 'code' => 'pbb', 'name_zh' => 'Public Bank', 'name_en' => 'Public Bank', 'sort' => 10],
['currency_code' => 'MYR', 'code' => 'mbb', 'name_zh' => 'Maybank2U', 'name_en' => 'Maybank2U', 'sort' => 20],
],
'withdraw_limits' => [
'min_ewallet' => '10',
'min_bank' => '10',
],
'withdraw_copy' => [
'rate_hint_zh' => '汇率为参考价格,实际以提现时为准。',
'rate_hint_en' => 'Exchange rates are for reference only; the actual rate at withdrawal time prevails.',
'processing_zh' => '9秒即可到账',
'processing_en' => 'Arrives in seconds',
'fee_note_zh' => '注意RM 10 — RM 99.99 之间的交易将收取最低 RM 1 的提现手续费。',
'fee_note_en' => 'A minimum RM 1 handling fee may apply for withdrawals between RM 10 and RM 99.99.',
'rate_mode' => 'fixed',
],
'channels' => [],
];
}
/**
* @return array<string, mixed>
*/
public static function parseFromConfigValue(mixed $raw): array
{
$out = self::defaultPayload();
if (!is_string($raw) || trim($raw) === '') {
return self::normalizePayload($out, []);
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return self::normalizePayload($out, []);
}
if (isset($decoded['platform_coin']) && is_array($decoded['platform_coin'])) {
$out['platform_coin'] = array_replace($out['platform_coin'], array_intersect_key($decoded['platform_coin'], $out['platform_coin']));
}
$legacyRates = [];
if (isset($decoded['rates']) && is_array($decoded['rates'])) {
$legacyRates = array_values($decoded['rates']);
}
foreach (['currencies', 'deposit_banks', 'withdraw_banks', 'channels'] as $listKey) {
if (isset($decoded[$listKey]) && is_array($decoded[$listKey])) {
$out[$listKey] = array_values($decoded[$listKey]);
}
}
if (isset($decoded['withdraw_limits']) && is_array($decoded['withdraw_limits'])) {
$out['withdraw_limits'] = array_replace($out['withdraw_limits'], array_intersect_key($decoded['withdraw_limits'], $out['withdraw_limits']));
}
if (isset($decoded['withdraw_copy']) && is_array($decoded['withdraw_copy'])) {
$out['withdraw_copy'] = array_replace($out['withdraw_copy'], array_intersect_key($decoded['withdraw_copy'], $out['withdraw_copy']));
}
return self::normalizePayload($out, $legacyRates);
}
public static function encodeForDb(array $payload): string
{
$legacyRates = [];
if (isset($payload['rates']) && is_array($payload['rates'])) {
$legacyRates = array_values($payload['rates']);
}
$normalized = self::normalizePayload($payload, $legacyRates);
$normalized['channels'] = DepositChannel::prepareOverridesForSave(
DepositChannel::expandRowsForAdmin($normalized['channels'] ?? [])
);
self::validate($normalized);
return json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* @param array<string, mixed> $p
* @param list<array<string, mixed>> $legacyRates
*
* @return array<string, mixed>
*/
private static function normalizePayload(array $p, array $legacyRates): array
{
$defaults = self::defaultPayload();
$out = array_replace($defaults, $p);
unset($out['rates'], $out['fx_pairs']);
$withdrawFromLegacyRates = [];
foreach ($legacyRates as $r) {
if (!is_array($r)) {
continue;
}
$cur = isset($r['currency']) && is_string($r['currency']) ? strtoupper(trim($r['currency'])) : '';
$ratio = self::ratioStringFromRow($r, 'diamonds_per_fiat_unit');
if ($cur !== '' && $ratio !== '') {
$withdrawFromLegacyRates[$cur] = $ratio;
}
}
if (isset($out['platform_coin']) && is_array($out['platform_coin'])) {
$out['platform_coin'] = array_replace($defaults['platform_coin'], $out['platform_coin']);
}
if (isset($out['currencies']) && is_array($out['currencies'])) {
foreach ($out['currencies'] as $i => $row) {
if (!is_array($row)) {
unset($out['currencies'][$i]);
continue;
}
$code = isset($row['code']) && is_string($row['code']) ? strtoupper(trim($row['code'])) : '';
$dep = self::ratioStringFromRow($row, 'deposit_coins_per_fiat');
$wdr = self::ratioStringFromRow($row, 'withdraw_coins_per_fiat');
if ($wdr === '') {
$wdr = self::ratioStringFromRow($row, 'diamonds_per_fiat_unit');
}
if ($wdr === '' && $code !== '' && isset($withdrawFromLegacyRates[$code])) {
$wdr = $withdrawFromLegacyRates[$code];
}
if ($dep === '' && $wdr !== '') {
$dep = $wdr;
}
if ($wdr === '' && $dep !== '') {
$wdr = $dep;
}
if ($code !== '' && $dep === '' && $wdr === '') {
$dep = '100';
$wdr = '100';
}
$out['currencies'][$i] = [
'code' => $code,
'label_zh' => isset($row['label_zh']) && is_string($row['label_zh']) ? trim($row['label_zh']) : '',
'label_en' => isset($row['label_en']) && is_string($row['label_en']) ? trim($row['label_en']) : '',
'sort' => self::normalizeSort($row['sort'] ?? 0),
'deposit_coins_per_fiat' => $dep,
'withdraw_coins_per_fiat' => $wdr,
];
}
$out['currencies'] = array_values(array_filter($out['currencies'], static fn ($r) => is_array($r) && $r['code'] !== ''));
}
usort($out['currencies'], static function (array $a, array $b): int {
$sa = $a['sort'] ?? 0;
$sb = $b['sort'] ?? 0;
if ($sa !== $sb) {
return $sa <=> $sb;
}
$ca = $a['code'] ?? '';
$cb = $b['code'] ?? '';
return strcmp($ca, $cb);
});
$out['deposit_banks'] = self::normalizeBanksByCurrency(
isset($out['deposit_banks']) && is_array($out['deposit_banks']) ? $out['deposit_banks'] : []
);
$out['withdraw_banks'] = self::normalizeBanksByCurrency(
isset($out['withdraw_banks']) && is_array($out['withdraw_banks']) ? $out['withdraw_banks'] : []
);
if (isset($out['withdraw_limits']) && is_array($out['withdraw_limits'])) {
$wl = array_replace($defaults['withdraw_limits'], $out['withdraw_limits']);
foreach (['min_ewallet', 'min_bank'] as $k) {
if (isset($wl[$k]) && is_numeric($wl[$k]) && !is_string($wl[$k])) {
$wl[$k] = strval($wl[$k]);
}
if (!isset($wl[$k]) || !is_string($wl[$k])) {
$wl[$k] = $defaults['withdraw_limits'][$k];
}
}
$out['withdraw_limits'] = $wl;
}
if (isset($out['withdraw_copy']) && is_array($out['withdraw_copy'])) {
$out['withdraw_copy'] = array_replace($defaults['withdraw_copy'], array_intersect_key($out['withdraw_copy'], $defaults['withdraw_copy']));
$mode = isset($out['withdraw_copy']['rate_mode']) && is_string($out['withdraw_copy']['rate_mode'])
? strtolower(trim($out['withdraw_copy']['rate_mode']))
: 'fixed';
if (!in_array($mode, ['fixed', 'live'], true)) {
$mode = 'fixed';
}
$out['withdraw_copy']['rate_mode'] = $mode;
}
if (isset($out['channels']) && is_array($out['channels'])) {
$out['channels'] = DepositChannel::normalizeOverrides(array_values($out['channels']));
} else {
$out['channels'] = [];
}
usort($out['channels'], static function (array $a, array $b): int {
$sa = $a['sort'] ?? 0;
$sb = $b['sort'] ?? 0;
if ($sa !== $sb) {
return $sa <=> $sb;
}
return strcmp($a['code'] ?? '', $b['code'] ?? '');
});
unset($out['fx_pairs']);
// 历史 JSON 可能含 withdraw_fields已废弃不再落库与返回给后台表单
unset($out['withdraw_fields']);
return $out;
}
/**
* @param array<string, mixed> $row
*/
private static function ratioStringFromRow(array $row, string $key): string
{
if (!isset($row[$key])) {
return '';
}
if (is_string($row[$key])) {
return trim($row[$key]);
}
if (is_numeric($row[$key])) {
return strval($row[$key]);
}
return '';
}
private static function normalizeSort(mixed $v): int
{
$opt = filter_var($v, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 99999]]);
if ($opt !== false) {
return $opt;
}
if (is_numeric($v)) {
$f = filter_var($v, FILTER_VALIDATE_FLOAT);
if ($f !== false) {
$rounded = round($f);
$opt2 = filter_var($rounded, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 99999]]);
if ($opt2 !== false) {
return $opt2;
}
}
}
return 0;
}
/**
* @param list<mixed> $rows
* @return list<array{currency_code: string, code: string, name_zh: string, name_en: string, sort: int}>
*/
private static function normalizeBanksByCurrency(array $rows): array
{
$out = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$currencyCode = isset($row['currency_code']) && is_string($row['currency_code']) ? strtoupper(trim($row['currency_code'])) : '';
if ($currencyCode === '') {
$currencyCode = 'MYR';
}
$codeRaw = $row['code'] ?? '';
$code = '';
if (is_string($codeRaw)) {
$code = strtolower(trim($codeRaw));
} elseif (is_numeric($codeRaw)) {
$code = strtolower(trim(strval($codeRaw)));
}
$out[] = [
'currency_code' => $currencyCode,
'code' => $code,
'name_zh' => isset($row['name_zh']) && is_string($row['name_zh']) ? trim($row['name_zh']) : '',
'name_en' => isset($row['name_en']) && is_string($row['name_en']) ? trim($row['name_en']) : '',
'sort' => self::normalizeSort($row['sort'] ?? 0),
];
}
$out = array_values(array_filter($out, static fn ($r) => is_array($r) && $r['currency_code'] !== '' && $r['code'] !== ''));
usort($out, static function (array $a, array $b): int {
$ca = isset($a['currency_code']) && is_string($a['currency_code']) ? $a['currency_code'] : '';
$cb = isset($b['currency_code']) && is_string($b['currency_code']) ? $b['currency_code'] : '';
if ($ca !== $cb) {
return strcmp($ca, $cb);
}
$sa = isset($a['sort']) && is_numeric($a['sort']) ? intval(strval($a['sort'])) : 0;
$sb = isset($b['sort']) && is_numeric($b['sort']) ? intval(strval($b['sort'])) : 0;
if ($sa !== $sb) {
return $sa <=> $sb;
}
$ka = isset($a['code']) && is_string($a['code']) ? $a['code'] : '';
$kb = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';
return strcmp($ka, $kb);
});
return $out;
}
/**
* @param array<string, mixed> $p
*/
private static function validate(array $p): void
{
if (!isset($p['platform_coin']) || !is_array($p['platform_coin'])) {
throw new InvalidArgumentException('platform_coin format error');
}
$lz = $p['platform_coin']['label_zh'] ?? '';
$le = $p['platform_coin']['label_en'] ?? '';
if (!is_string($lz) || trim($lz) === '' || !is_string($le) || trim($le) === '') {
throw new InvalidArgumentException('Platform coin labels (zh/en) are required');
}
if (!isset($p['currencies']) || !is_array($p['currencies']) || $p['currencies'] === []) {
throw new InvalidArgumentException('At least one currency is required');
}
$seenCodes = [];
foreach ($p['currencies'] as $idx => $row) {
if (!is_array($row)) {
throw new InvalidArgumentException('Currency row format error');
}
$code = $row['code'] ?? '';
if (!is_string($code) || !preg_match('/^[A-Z0-9]{2,12}$/', $code)) {
throw new InvalidArgumentException('Currency code is invalid');
}
if (isset($seenCodes[$code])) {
throw new InvalidArgumentException('Duplicate currency code');
}
$seenCodes[$code] = true;
$dep = $row['deposit_coins_per_fiat'] ?? '';
$wdr = $row['withdraw_coins_per_fiat'] ?? '';
if (!is_string($dep) || $dep === '' || !is_numeric($dep) || bccomp($dep, '0', 2) <= 0) {
throw new InvalidArgumentException('Deposit rate must be a number greater than 0');
}
if (!is_string($wdr) || $wdr === '' || !is_numeric($wdr) || bccomp($wdr, '0', 2) <= 0) {
throw new InvalidArgumentException('Withdraw rate must be a number greater than 0');
}
}
// 校验 deposit channels 的币种白名单currency_codes 仅允许来自 currencies
if (isset($p['channels']) && is_array($p['channels'])) {
foreach ($p['channels'] as $row) {
if (!is_array($row)) {
continue;
}
$status = isset($row['status']) && is_numeric($row['status']) ? intval($row['status']) : 0;
$cc = array_key_exists('currency_codes', $row) ? ($row['currency_codes'] ?? null) : null;
if ($cc === null) {
continue; // null => 兼容全部币种(历史配置默认行为)
}
if (!is_array($cc)) {
throw new InvalidArgumentException('Channel currency_codes format error');
}
if ($status === 1 && $cc === []) {
throw new InvalidArgumentException('Enabled channel currency_codes can not be empty');
}
foreach ($cc as $c) {
if (!is_string($c) || !preg_match('/^[A-Z0-9]{2,12}$/', $c)) {
throw new InvalidArgumentException('Channel currency_codes contains invalid currency code');
}
if (!isset($seenCodes[$c])) {
throw new InvalidArgumentException('Channel currency_codes contains currency not configured');
}
}
}
}
foreach (['deposit_banks', 'withdraw_banks'] as $bankKey) {
if (!isset($p[$bankKey]) || !is_array($p[$bankKey])) {
continue;
}
$seen = [];
foreach ($p[$bankKey] as $row) {
if (!is_array($row)) {
throw new InvalidArgumentException('Bank row format error');
}
$currencyCode = $row['currency_code'] ?? '';
if (!is_string($currencyCode) || !isset($seenCodes[$currencyCode])) {
throw new InvalidArgumentException('Bank currency_code is invalid');
}
$code = $row['code'] ?? '';
if (!is_string($code) || !preg_match('/^[a-z0-9][a-z0-9_\-]{0,31}$/', $code)) {
throw new InvalidArgumentException('Bank code is invalid');
}
$uniq = $currencyCode . '|' . $code;
if (isset($seen[$uniq])) {
throw new InvalidArgumentException('Duplicate bank code');
}
$seen[$uniq] = true;
}
}
if (isset($p['withdraw_limits']) && is_array($p['withdraw_limits'])) {
foreach (['min_ewallet', 'min_bank'] as $k) {
$v = $p['withdraw_limits'][$k] ?? '0';
if (!is_string($v) || !is_numeric($v) || bccomp($v, '0', 2) < 0) {
throw new InvalidArgumentException('Withdraw min limit must be a number not less than 0');
}
}
}
$reg = DepositChannel::codeRegistry();
if ($reg !== [] && (!isset($p['channels']) || !is_array($p['channels']) || $p['channels'] === [])) {
throw new InvalidArgumentException('Please configure deposit channels');
}
}
}