MaintenancePolicy.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Support;
  4. use DateTimeImmutable;
  5. use DateTimeZone;
  6. /** Framework independent; keep the member API copy identical except for its namespace. */
  7. final class MaintenancePolicy
  8. {
  9. public const CATEGORIES = ['third:1' => '真人', 'third:2' => '电子', 'third:3' => '彩票',
  10. 'third:4' => '体育', 'third:5' => '电竞', 'third:6' => '捕鱼', 'third:7' => '棋牌',
  11. 'self:sport' => 'F1体育', 'self:lottery' => 'F1彩票'];
  12. /** Seed for the sport_list config; the admin list is the source of truth afterwards. */
  13. public const SPORTS = ['football' => '足球', 'basketball' => '篮球', 'tabletennis' => '乒乓球',
  14. 'volleyball' => '排球', 'cricket' => '板球', 'tennis' => '网球'];
  15. public const LANGUAGES = ['zh' => '中文', 'en' => '英文', 'vi' => '越南语'];
  16. public const STATES = ['disabled' => '已关闭', 'scheduled' => '待开始', 'active' => '维护中', 'expired' => '已结束'];
  17. public const SITE_CODE = 11001;
  18. public const GAME_CODE = 11002;
  19. public static function scopes(): array { return ['site' => '整站'] + self::CATEGORIES; }
  20. /**
  21. * Subcategory of one game_list entry. List `type` is only a display id; the code follows the game
  22. * and its params.type, which are what the bet and lobby endpoints receive.
  23. */
  24. public static function lotterySubcategory(array $item): ?string
  25. {
  26. $game = strtolower(trim((string) ($item['game'] ?? '')));
  27. $type = $item['params']['type'] ?? null;
  28. $type = is_numeric($type) && (int) $type == $type && (int) $type > 0 && (int) $type < 1000 ? (int) $type : null;
  29. if ($game === 'canada28') return 'self:lottery:pc28';
  30. if ($game === 'jisu28') return 'self:lottery:jisu28';
  31. if (in_array($game, ['lhc', 'jisu'], true) && $type !== null) return 'self:lottery:' . $game . ':' . $type;
  32. return null;
  33. }
  34. /** Decodes a game_list / sport_list config value; malformed values read as an empty list. */
  35. public static function configList($json): array
  36. {
  37. $list = is_string($json) ? json_decode($json, true) : null;
  38. return is_array($list) ? array_values(array_filter($list, 'is_array')) : [];
  39. }
  40. public static function sportSubcategory(string $code): ?string
  41. {
  42. $code = strtolower(trim($code));
  43. return preg_match('/^[a-z0-9]{1,32}$/', $code) ? 'self:sport:' . $code : null;
  44. }
  45. public static function thirdSubcategory(int $gameType, string $platType): ?string
  46. {
  47. $category = 'third:' . $gameType;
  48. if (!isset(self::CATEGORIES[$category])) return null;
  49. $plat = strtolower(trim($platType));
  50. if ($plat === '' || strlen($plat) > 32 || !preg_match('/^[a-z0-9]+$/', $plat)) return null;
  51. return $category . ':' . $plat;
  52. }
  53. /** Parent category of a platform, lottery game or sport; null for site and category scopes. */
  54. public static function parentScope(string $code): ?string
  55. {
  56. if (preg_match('/^(third:[1-7]):([a-z0-9]{1,32})$/', $code, $match) && isset(self::CATEGORIES[$match[1]])) return $match[1];
  57. if (preg_match('/^self:lottery:(pc28|jisu28|(lhc|jisu):[1-9][0-9]{0,2})$/', $code)) return 'self:lottery';
  58. if (preg_match('/^self:sport:[a-z0-9]{1,32}$/', $code)) return 'self:sport';
  59. return null;
  60. }
  61. public static function platform($value): ?string
  62. {
  63. return is_string($value) && in_array(strtoupper(trim($value)), ['PC', 'H5', 'APP'], true)
  64. ? strtoupper(trim($value)) : null;
  65. }
  66. public static function language($value): ?string
  67. {
  68. if (!is_string($value)) return null;
  69. $value = strtolower(str_replace('_', '-', trim($value)));
  70. $value = explode('-', $value)[0];
  71. return isset(self::LANGUAGES[$value]) ? $value : null;
  72. }
  73. public static function state(array $row, int $now): string
  74. {
  75. if (!(int) $row['enabled']) return 'disabled';
  76. if (self::timestamp($row['starts_at']) > $now) return 'scheduled';
  77. if ($row['ends_at'] !== null && self::timestamp($row['ends_at']) <= $now) return 'expired';
  78. return 'active';
  79. }
  80. public static function platforms(array $row): array
  81. {
  82. return is_array($row['platforms']) ? $row['platforms'] : json_decode($row['platforms'], true, 512, JSON_THROW_ON_ERROR);
  83. }
  84. public static function matches(array $row, ?string $platform, int $now): bool
  85. {
  86. // Unknown clients conservatively match every platform; omission cannot bypass a rule.
  87. return self::state($row, $now) === 'active' && ($platform === null || in_array($platform, self::platforms($row), true));
  88. }
  89. public static function select(array $rows, ?string $platform, ?string $category, int $now, ?string $subcategory = null): ?array
  90. {
  91. $game = null; $child = null;
  92. foreach ($rows as $row) {
  93. if (!self::matches($row, $platform, $now)) continue;
  94. if ($row['scope_code'] === 'site') return $row;
  95. if ($row['scope_code'] === $category) $game = $row;
  96. if ($subcategory !== null && $row['scope_code'] === $subcategory && self::parentScope($subcategory) === $category) $child = $row;
  97. }
  98. return $child ?? $game;
  99. }
  100. public static function publicRule(array $row, string $language): array
  101. {
  102. $zone = new DateTimeZone($row['timezone']);
  103. $start = new DateTimeImmutable($row['starts_at'], new DateTimeZone('UTC'));
  104. $end = $row['ends_at'] === null ? null : new DateTimeImmutable($row['ends_at'], new DateTimeZone('UTC'));
  105. $fallback = ['zh' => '系统维护中,请稍后再试', 'en' => 'Maintenance in progress. Please try again later.',
  106. 'vi' => 'Hệ thống đang bảo trì. Vui lòng thử lại sau.'];
  107. $content = (string) ($row['content'] ?? '');
  108. $parent = self::parentScope($row['scope_code']);
  109. return ['scope' => $row['scope_code'] === 'site' ? 'site' : 'game',
  110. 'category' => $row['scope_code'] === 'site' ? null : ($parent ?? $row['scope_code']),
  111. 'subcategory' => $parent === null ? null : $row['scope_code'],
  112. 'platforms' => self::platforms($row), 'timezone' => $row['timezone'],
  113. 'starts_at' => $start->setTimezone($zone)->format('Y-m-d H:i:s'),
  114. 'ends_at' => $end ? $end->setTimezone($zone)->format('Y-m-d H:i:s') : null,
  115. 'starts_at_utc' => $start->format('Y-m-d\TH:i:s\Z'), 'ends_at_utc' => $end ? $end->format('Y-m-d\TH:i:s\Z') : null,
  116. 'language' => $language, 'content' => trim($content) === '' ? $fallback[$language] : $content];
  117. }
  118. public static function failure(array $row, ?string $platform, string $language, int $now): array
  119. {
  120. $data = self::publicRule($row, $language) + ['platform' => $platform, 'platform_unknown' => $platform === null,
  121. 'server_time' => gmdate('Y-m-d\TH:i:s\Z', $now)];
  122. return ['code' => $row['scope_code'] === 'site' ? self::SITE_CODE : self::GAME_CODE, 'msg' => $data['content'], 'data' => $data];
  123. }
  124. private static function timestamp(string $value): int { return (new DateTimeImmutable($value, new DateTimeZone('UTC')))->getTimestamp(); }
  125. }