'zodiac', '猛兽' => 'beast', '飞禽' => 'fowl', '虫蛇' => 'vermin', '神兽' => 'divine', 'zodiac' => 'zodiac', 'beast' => 'beast', 'fowl' => 'fowl', 'vermin' => 'vermin', 'divine' => 'divine', ]; /** * 默认 36 条(与 PRD 一致,category 为英文) * * @return list */ public static function defaultItems(): array { return [ ['no' => 1, 'name' => '鼠', 'category' => 'zodiac'], ['no' => 2, 'name' => '牛', 'category' => 'zodiac'], ['no' => 3, 'name' => '虎', 'category' => 'zodiac'], ['no' => 4, 'name' => '兔', 'category' => 'zodiac'], ['no' => 5, 'name' => '龙', 'category' => 'zodiac'], ['no' => 6, 'name' => '蛇', 'category' => 'zodiac'], ['no' => 7, 'name' => '马', 'category' => 'zodiac'], ['no' => 8, 'name' => '羊', 'category' => 'zodiac'], ['no' => 9, 'name' => '猴', 'category' => 'zodiac'], ['no' => 10, 'name' => '鸡', 'category' => 'zodiac'], ['no' => 11, 'name' => '狗', 'category' => 'zodiac'], ['no' => 12, 'name' => '猪', 'category' => 'zodiac'], ['no' => 13, 'name' => '狮', 'category' => 'beast'], ['no' => 14, 'name' => '象', 'category' => 'beast'], ['no' => 15, 'name' => '鹿', 'category' => 'beast'], ['no' => 16, 'name' => '熊', 'category' => 'beast'], ['no' => 17, 'name' => '狼', 'category' => 'beast'], ['no' => 18, 'name' => '豹', 'category' => 'beast'], ['no' => 19, 'name' => '鹰', 'category' => 'fowl'], ['no' => 20, 'name' => '鹤', 'category' => 'fowl'], ['no' => 21, 'name' => '孔雀', 'category' => 'fowl'], ['no' => 22, 'name' => '鸳鸯', 'category' => 'fowl'], ['no' => 23, 'name' => '鸵鸟', 'category' => 'fowl'], ['no' => 24, 'name' => '天鹅', 'category' => 'fowl'], ['no' => 25, 'name' => '蟾蜍', 'category' => 'vermin'], ['no' => 26, 'name' => '蜘蛛', 'category' => 'vermin'], ['no' => 27, 'name' => '蝙蝠', 'category' => 'vermin'], ['no' => 28, 'name' => '蜈蚣', 'category' => 'vermin'], ['no' => 29, 'name' => '蝎', 'category' => 'vermin'], ['no' => 30, 'name' => '蛇蜥', 'category' => 'vermin'], ['no' => 31, 'name' => '麒麟', 'category' => 'divine'], ['no' => 32, 'name' => '凤凰', 'category' => 'divine'], ['no' => 33, 'name' => '青龙', 'category' => 'divine'], ['no' => 34, 'name' => '白虎', 'category' => 'divine'], ['no' => 35, 'name' => '朱雀', 'category' => 'divine'], ['no' => 36, 'name' => '玄武', 'category' => 'divine'], ]; } /** * @return list */ public static function parseFromConfigValue(?string $configValue): array { if ($configValue === null || trim($configValue) === '') { return self::defaultItems(); } $decoded = json_decode($configValue, true); if (!is_array($decoded)) { return self::defaultItems(); } if (isset($decoded['items']) && is_array($decoded['items'])) { return self::normalizeItemsList($decoded['items']); } if ($decoded !== [] && array_is_list($decoded)) { return self::normalizeItemsList($decoded); } return self::defaultItems(); } /** * @param list $items * @return list */ public static function normalizeItemsList(array $items): array { $defaults = self::defaultItems(); $defaultByNo = []; foreach ($defaults as $row) { $defaultByNo[$row['no']] = $row; } $out = []; foreach ($items as $row) { if (!is_array($row)) { continue; } $no = $row['no'] ?? null; if (!is_numeric($no)) { continue; } $n = intval($no, 10); if ($n < 1 || $n > 36) { continue; } $name = isset($row['name']) && is_string($row['name']) ? trim($row['name']) : ''; if ($name === '') { $name = $defaultByNo[$n]['name'] ?? ''; } $catRaw = $row['category'] ?? 'zodiac'; $catStr = is_string($catRaw) ? trim($catRaw) : ''; $category = self::LEGACY_CATEGORY_MAP[$catStr] ?? (in_array($catStr, self::CATEGORIES, true) ? $catStr : 'zodiac'); $out[] = ['no' => $n, 'name' => $name, 'category' => $category]; } if (count($out) !== 36) { return self::defaultItems(); } usort($out, static fn (array $a, array $b): int => $a['no'] <=> $b['no']); $seen = []; foreach ($out as $row) { if (isset($seen[$row['no']])) { return self::defaultItems(); } $seen[$row['no']] = true; } for ($i = 1; $i <= 36; $i++) { if (!isset($seen[$i])) { return self::defaultItems(); } } return $out; } /** * 校验 POST 数据并输出入库用的 36 条(no 升序) * * @param list $items * * @return list * * @throws InvalidArgumentException */ public static function prepareItemsForSave(array $items): array { if (count($items) !== 36) { throw new InvalidArgumentException('Must be exactly 36 items'); } $nos = []; $out = []; foreach ($items as $idx => $row) { if (!is_array($row)) { throw new InvalidArgumentException('Row format error'); } $no = $row['no'] ?? null; if (!is_numeric($no)) { throw new InvalidArgumentException('No must be numeric'); } $n = intval($no, 10); if ($n < 1 || $n > 36) { throw new InvalidArgumentException('No must be between 1 and 36'); } if (isset($nos[$n])) { throw new InvalidArgumentException('Duplicate no'); } $nos[$n] = true; $name = $row['name'] ?? ''; if (!is_string($name) || trim($name) === '') { throw new InvalidArgumentException('Name is required'); } $cat = $row['category'] ?? ''; $catTrim = is_string($cat) ? trim($cat) : ''; if (!in_array($catTrim, self::CATEGORIES, true)) { throw new InvalidArgumentException('Category is invalid'); } $out[] = ['no' => $n, 'name' => trim($name), 'category' => $catTrim]; } for ($i = 1; $i <= 36; $i++) { if (!isset($nos[$i])) { throw new InvalidArgumentException('Missing no'); } } usort($out, static fn (array $a, array $b): int => $a['no'] <=> $b['no']); return $out; } /** * @param list $items */ public static function encodeForDb(array $items): string { usort($items, static fn (array $a, array $b): int => $a['no'] <=> $b['no']); $encoded = json_encode($items, JSON_UNESCAPED_UNICODE); if ($encoded === false) { throw new InvalidArgumentException('JSON encode failed'); } return $encoded; } }