*/ 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 $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> $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('Row format error'); } $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('Tier id is invalid'); } if (isset($seenId[$id])) { throw new InvalidArgumentException('Duplicate tier id'); } $seenId[$id] = true; $title = self::stringField($row, 'title'); if ($title === '') { // 兼容上游(例如自动迁移脚本)传递历史 name 字段 $title = self::stringField($row, 'name'); } if ($title === '') { throw new InvalidArgumentException('Tier title (zh) is required'); } if (mb_strlen($title) > 64) { throw new InvalidArgumentException('Tier title (zh) is too long'); } $titleEn = self::stringField($row, 'title_en'); if (mb_strlen($titleEn) > 64) { throw new InvalidArgumentException('Tier title (en) is too long'); } $currency = self::normalizeCurrency($row['currency'] ?? ''); if ($currency === '') { throw new InvalidArgumentException('Currency is required'); } $payAmount = self::normalizeAmount($row['pay_amount'] ?? ''); if (bccomp($payAmount, '0', 2) <= 0) { throw new InvalidArgumentException('Pay amount must be greater than 0'); } $amount = self::normalizeAmount($row['amount'] ?? ''); if (bccomp($amount, '0', 2) <= 0) { throw new InvalidArgumentException('Base credit amount must be greater than 0'); } $bonus = self::normalizeAmount($row['bonus_amount'] ?? '0'); if (bccomp($bonus, '0', 2) < 0) { throw new InvalidArgumentException('Bonus amount cannot be negative'); } $desc = self::stringField($row, 'desc'); if (mb_strlen($desc) > 255) { throw new InvalidArgumentException('Description (zh) is too long'); } $descEn = self::stringField($row, 'desc_en'); if (mb_strlen($descEn) > 255) { throw new InvalidArgumentException('Description (en) is too long'); } $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> $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 encode failed'); } 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 $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 $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-'); } }