400 lines
14 KiB
PHP
400 lines
14 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\library\game;
|
||
|
||
use InvalidArgumentException;
|
||
|
||
/**
|
||
* 充值档位(game_config.deposit_tier):仅存 JSON 数组
|
||
*
|
||
* 每一项字段(mock/第三方支付模式,已不再保存收款账户信息;支持中英文双语):
|
||
* - id : string,档位稳定唯一标识(如 t_xxxxxxxx),移动端下单 tier_id / tier_key
|
||
* - title : string,档位中文名称(必填,前端中文环境展示)
|
||
* - title_en : string,档位英文名称(可选,前端英文环境展示;为空时回退到 title)
|
||
* - currency : string,支付货币代码(3~8 位大写字母,如 MYR、CNY)
|
||
* - pay_amount : string,玩家支付的法币/支付货币额度(2 位小数)
|
||
* - amount : string,到账基础平台币(2 位小数)
|
||
* - bonus_amount : string,赠送平台币(2 位小数,可为 0)
|
||
* - desc : string,档位中文描述(可空,<=255)
|
||
* - desc_en : string,档位英文描述(可空,<=255,为空时回退到 desc)
|
||
* - sort : int,排序权重(小值在前)
|
||
* - status : int,0=停用,1=启用
|
||
*
|
||
* 历史数据兼容:老字段 name 会在 title 缺失时作为 title 兜底(更老的 account_name 亦会兜底)。
|
||
*/
|
||
final class DepositTier
|
||
{
|
||
public const CONFIG_KEY = 'deposit_tier';
|
||
|
||
/**
|
||
* 从 game_config.config_value 中解析出档位数组(容错)
|
||
*
|
||
* @return list<array{
|
||
* id: string,
|
||
* title: string,
|
||
* title_en: string,
|
||
* currency: string,
|
||
* pay_amount: string,
|
||
* amount: string,
|
||
* bonus_amount: string,
|
||
* desc: string,
|
||
* desc_en: string,
|
||
* sort: int,
|
||
* status: int,
|
||
* }>
|
||
*/
|
||
public static function parseFromConfigValue($raw): array
|
||
{
|
||
if (!is_string($raw) || trim($raw) === '') {
|
||
return [];
|
||
}
|
||
$decoded = json_decode($raw, true);
|
||
if (!is_array($decoded)) {
|
||
return [];
|
||
}
|
||
if (isset($decoded['tiers']) && is_array($decoded['tiers'])) {
|
||
$list = $decoded['tiers'];
|
||
} else {
|
||
$list = $decoded;
|
||
}
|
||
return self::normalizeList($list);
|
||
}
|
||
|
||
/**
|
||
* @param list<mixed> $items
|
||
*/
|
||
public static function normalizeList(array $items): array
|
||
{
|
||
$out = [];
|
||
foreach ($items as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$id = isset($row['id']) && is_string($row['id']) ? trim($row['id']) : '';
|
||
if ($id === '') {
|
||
$id = self::generateId();
|
||
}
|
||
|
||
$title = self::stringField($row, 'title');
|
||
if ($title === '') {
|
||
// 兼容历史:字段名 name 或更老的 account_name
|
||
$title = self::stringField($row, 'name');
|
||
if ($title === '') {
|
||
$title = self::stringField($row, 'account_name');
|
||
}
|
||
}
|
||
$titleEn = self::stringField($row, 'title_en');
|
||
|
||
$currency = self::normalizeCurrency($row['currency'] ?? '');
|
||
if ($currency === '') {
|
||
$currency = 'CNY';
|
||
}
|
||
|
||
$payAmount = self::normalizeAmount($row['pay_amount'] ?? '');
|
||
$amount = self::normalizeAmount($row['amount'] ?? '');
|
||
$bonus = self::normalizeAmount($row['bonus_amount'] ?? '0');
|
||
if (bccomp($payAmount, '0', 2) <= 0 && bccomp($amount, '0', 2) > 0) {
|
||
// 历史数据仅有 amount(平台币)时,用占位同步 pay_amount,运营应在后台改为真实支付额度
|
||
$payAmount = $amount;
|
||
}
|
||
|
||
$desc = self::stringField($row, 'desc');
|
||
if ($desc === '') {
|
||
$desc = self::stringField($row, 'remark');
|
||
}
|
||
$descEn = self::stringField($row, 'desc_en');
|
||
|
||
$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;
|
||
|
||
$out[] = [
|
||
'id' => $id,
|
||
'title' => $title,
|
||
'title_en' => $titleEn,
|
||
'currency' => $currency,
|
||
'pay_amount' => $payAmount,
|
||
'amount' => $amount,
|
||
'bonus_amount' => $bonus,
|
||
'desc' => $desc,
|
||
'desc_en' => $descEn,
|
||
'sort' => $sort,
|
||
'status' => $status,
|
||
];
|
||
}
|
||
usort($out, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
$ida = is_string($a['id']) ? $a['id'] : '';
|
||
$idb = is_string($b['id']) ? $b['id'] : '';
|
||
return strcmp($ida, $idb);
|
||
});
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 校验 POST 数据并输出用于入库的清洁数据
|
||
*
|
||
* @param list<array<string, mixed>> $items
|
||
*
|
||
* @throws InvalidArgumentException
|
||
*/
|
||
public static function prepareItemsForSave(array $items): array
|
||
{
|
||
$seenId = [];
|
||
$out = [];
|
||
foreach ($items as $idx => $row) {
|
||
$no = $idx + 1;
|
||
if (!is_array($row)) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行格式错误');
|
||
}
|
||
|
||
$id = isset($row['id']) && is_string($row['id']) ? trim($row['id']) : '';
|
||
if ($id === '') {
|
||
$id = self::generateId();
|
||
}
|
||
if (!preg_match('/^[a-zA-Z0-9_\-]{1,32}$/', $id)) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行 ID 非法');
|
||
}
|
||
if (isset($seenId[$id])) {
|
||
throw new InvalidArgumentException('档位 ID 重复:' . $id);
|
||
}
|
||
$seenId[$id] = true;
|
||
|
||
$title = self::stringField($row, 'title');
|
||
if ($title === '') {
|
||
// 兼容上游(例如自动迁移脚本)传递历史 name 字段
|
||
$title = self::stringField($row, 'name');
|
||
}
|
||
if ($title === '') {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行中文充值名称不能为空');
|
||
}
|
||
if (mb_strlen($title) > 64) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行中文充值名称过长');
|
||
}
|
||
|
||
$titleEn = self::stringField($row, 'title_en');
|
||
if (mb_strlen($titleEn) > 64) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行英文充值名称过长');
|
||
}
|
||
|
||
$currency = self::normalizeCurrency($row['currency'] ?? '');
|
||
if ($currency === '') {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行支付货币不能为空');
|
||
}
|
||
|
||
$payAmount = self::normalizeAmount($row['pay_amount'] ?? '');
|
||
if (bccomp($payAmount, '0', 2) <= 0) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行支付货币额度必须大于 0');
|
||
}
|
||
|
||
$amount = self::normalizeAmount($row['amount'] ?? '');
|
||
if (bccomp($amount, '0', 2) <= 0) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行基础平台币到账必须大于 0');
|
||
}
|
||
|
||
$bonus = self::normalizeAmount($row['bonus_amount'] ?? '0');
|
||
if (bccomp($bonus, '0', 2) < 0) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行赠送金额不能为负数');
|
||
}
|
||
|
||
$desc = self::stringField($row, 'desc');
|
||
if (mb_strlen($desc) > 255) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行中文描述过长');
|
||
}
|
||
|
||
$descEn = self::stringField($row, 'desc_en');
|
||
if (mb_strlen($descEn) > 255) {
|
||
throw new InvalidArgumentException('第 ' . $no . ' 行英文描述过长');
|
||
}
|
||
|
||
$sort = isset($row['sort']) && is_numeric($row['sort']) ? intval($row['sort']) : 0;
|
||
$statusRaw = isset($row['status']) && is_numeric($row['status']) ? intval($row['status']) : 1;
|
||
$status = $statusRaw === 1 ? 1 : 0;
|
||
|
||
$out[] = [
|
||
'id' => $id,
|
||
'title' => $title,
|
||
'title_en' => $titleEn,
|
||
'currency' => $currency,
|
||
'pay_amount' => $payAmount,
|
||
'amount' => $amount,
|
||
'bonus_amount' => $bonus,
|
||
'desc' => $desc,
|
||
'desc_en' => $descEn,
|
||
'sort' => $sort,
|
||
'status' => $status,
|
||
];
|
||
}
|
||
|
||
usort($out, static function (array $a, array $b): int {
|
||
if ($a['sort'] !== $b['sort']) {
|
||
return $a['sort'] <=> $b['sort'];
|
||
}
|
||
$ida = is_string($a['id']) ? $a['id'] : '';
|
||
$idb = is_string($b['id']) ? $b['id'] : '';
|
||
return strcmp($ida, $idb);
|
||
});
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string, mixed>> $items
|
||
*/
|
||
public static function encodeForDb(array $items): string
|
||
{
|
||
$encoded = json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
if ($encoded === false) {
|
||
throw new InvalidArgumentException('JSON 编码失败');
|
||
}
|
||
return $encoded;
|
||
}
|
||
|
||
/**
|
||
* 过滤出启用档位并按 sort 升序,供移动端选择
|
||
*/
|
||
public static function publicList(array $items): array
|
||
{
|
||
$enabled = array_values(array_filter($items, static function (array $row): bool {
|
||
if (!isset($row['status'])) {
|
||
return false;
|
||
}
|
||
$val = is_numeric($row['status']) ? intval($row['status']) : 0;
|
||
return $val === 1;
|
||
}));
|
||
usort($enabled, static function (array $a, array $b): int {
|
||
$sa = isset($a['sort']) && is_numeric($a['sort']) ? intval($a['sort']) : 0;
|
||
$sb = isset($b['sort']) && is_numeric($b['sort']) ? intval($b['sort']) : 0;
|
||
if ($sa !== $sb) {
|
||
return $sa <=> $sb;
|
||
}
|
||
$ida = isset($a['id']) && is_string($a['id']) ? $a['id'] : '';
|
||
$idb = isset($b['id']) && is_string($b['id']) ? $b['id'] : '';
|
||
return strcmp($ida, $idb);
|
||
});
|
||
return $enabled;
|
||
}
|
||
|
||
/**
|
||
* 按 ID 从档位列表中取出指定档位;未找到返回 null
|
||
*/
|
||
public static function findById(array $items, string $id): ?array
|
||
{
|
||
foreach ($items as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$rid = $row['id'] ?? '';
|
||
if (is_string($rid) && $rid === $id) {
|
||
return $row;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 根据语言选择档位对外展示的 title/desc。
|
||
*
|
||
* @param array<string, mixed> $item
|
||
* @return array{title: string, desc: string}
|
||
*/
|
||
public static function localize(array $item, string $lang): array
|
||
{
|
||
$title = self::stringField($item, 'title');
|
||
$titleEn = self::stringField($item, 'title_en');
|
||
$desc = self::stringField($item, 'desc');
|
||
$descEn = self::stringField($item, 'desc_en');
|
||
|
||
$isEn = self::isEnglishLang($lang);
|
||
$pickedTitle = $isEn ? ($titleEn !== '' ? $titleEn : $title) : ($title !== '' ? $title : $titleEn);
|
||
$pickedDesc = $isEn ? ($descEn !== '' ? $descEn : $desc) : ($desc !== '' ? $desc : $descEn);
|
||
|
||
return [
|
||
'title' => $pickedTitle,
|
||
'desc' => $pickedDesc,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 生成 10 位稳定 ID(t_ + 8 位随机 base32)
|
||
*/
|
||
public static function generateId(): string
|
||
{
|
||
$chars = 'abcdefghijkmnpqrstuvwxyz23456789';
|
||
$len = strlen($chars);
|
||
$id = 't_';
|
||
for ($i = 0; $i < 8; $i++) {
|
||
$id .= $chars[random_int(0, $len - 1)];
|
||
}
|
||
return $id;
|
||
}
|
||
|
||
/**
|
||
* 将金额归一化为 2 位小数字符串;非法输入返回 '0.00'
|
||
*/
|
||
public static function normalizeAmount($raw): string
|
||
{
|
||
if ($raw === null || $raw === '') {
|
||
return '0.00';
|
||
}
|
||
if (is_string($raw)) {
|
||
$s = trim($raw);
|
||
} elseif (is_int($raw) || is_float($raw)) {
|
||
$s = strval($raw);
|
||
} else {
|
||
return '0.00';
|
||
}
|
||
$s = str_replace(',', '.', $s);
|
||
if (!is_numeric($s)) {
|
||
return '0.00';
|
||
}
|
||
return bcadd($s, '0', 2);
|
||
}
|
||
|
||
/**
|
||
* 从数组取字符串字段并 trim,非字符串返回空串
|
||
*
|
||
* @param array<string, mixed> $row
|
||
*/
|
||
private static function stringField(array $row, string $key): string
|
||
{
|
||
if (!isset($row[$key])) {
|
||
return '';
|
||
}
|
||
$v = $row[$key];
|
||
return is_string($v) ? trim($v) : '';
|
||
}
|
||
|
||
/**
|
||
* 支付货币代码:3~8 位字母,统一大写
|
||
*
|
||
* @param mixed $raw
|
||
*/
|
||
private static function normalizeCurrency($raw): string
|
||
{
|
||
if (!is_string($raw)) {
|
||
return '';
|
||
}
|
||
$s = strtoupper(trim($raw));
|
||
if ($s === '') {
|
||
return '';
|
||
}
|
||
if (!preg_match('/^[A-Z]{3,8}$/', $s)) {
|
||
return '';
|
||
}
|
||
return $s;
|
||
}
|
||
|
||
private static function isEnglishLang(string $lang): bool
|
||
{
|
||
$normalized = strtolower(str_replace('_', '-', trim($lang)));
|
||
if ($normalized === '') {
|
||
return false;
|
||
}
|
||
return $normalized === 'en' || str_starts_with($normalized, 'en-');
|
||
}
|
||
}
|