LotteryOdds.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <?php
  2. namespace App\Services;
  3. use DomainException;
  4. use Illuminate\Database\Query\Builder;
  5. use Illuminate\Support\Collection;
  6. use Illuminate\Support\Facades\DB;
  7. final class LotteryOdds
  8. {
  9. public static function gameKeys(): array
  10. {
  11. return [
  12. 'canada28', 'jisu28', 'lhc:1', 'lhc:2', 'lhc:3', 'lhc:4',
  13. 'jisu:1', 'jisu:2', 'jisu:3', 'jisu:4', 'jisu:5', 'jisu:6', 'jisu:7',
  14. ];
  15. }
  16. public static function gameKey(string $game, ?int $type): string
  17. {
  18. $key = $type === null ? $game : "$game:$type";
  19. if (!in_array($key, self::gameKeys(), true)) {
  20. throw new DomainException('彩种无效');
  21. }
  22. return $key;
  23. }
  24. public static function catalog(string $key): Builder
  25. {
  26. [$game, $type] = array_pad(explode(':', $key, 2), 2, null);
  27. return match ($game) {
  28. 'jisu' => DB::table('jisu_gameplay')->where('type', (int) $type),
  29. 'lhc' => DB::table('lhc_number'),
  30. 'canada28', 'jisu28' => DB::table('gameplay_rules'),
  31. };
  32. }
  33. private static function templates(string $key): Builder
  34. {
  35. return DB::table('lottery_odds_templates')->where('game_key', $key);
  36. }
  37. public static function template(string $key, ?int $id = null, bool $write = false): object
  38. {
  39. $query = self::templates($key);
  40. $id === null ? $query->where('is_default', 1) : $query->where('id', $id);
  41. $row = ($write ? $query->lockForUpdate() : $query->sharedLock())->first();
  42. return $row ?? throw new DomainException('赔率模板不存在');
  43. }
  44. public static function items(int $templateId): Builder
  45. {
  46. return DB::table('lottery_odds_items')->where('template_id', $templateId);
  47. }
  48. private static function configuredItems(int $templateId): Builder
  49. {
  50. return self::items($templateId)->whereRaw("TRIM(odds) <> ''")
  51. ->whereNotNull('mininum')->whereNotNull('maxinum');
  52. }
  53. private static function publicTemplate(object $row): array
  54. {
  55. return [
  56. 'id' => (int) $row->id, 'names' => json_decode($row->names, true),
  57. 'is_default' => (bool) $row->is_default, 'enabled' => (bool) $row->enabled,
  58. 'sort' => (int) $row->sort, 'revision' => (int) $row->revision,
  59. ];
  60. }
  61. public static function list(string $key, ?int $id): array
  62. {
  63. return DB::transaction(function () use ($key, $id) {
  64. $template = self::template($key, $id);
  65. return [
  66. 'data' => self::tree($key, self::pricedCatalog($key, (int) $template->id, includeUnconfigured: true)),
  67. 'odds_template_id' => (int) $template->id,
  68. 'odds_templates' => self::templates($key)->orderByDesc('sort')->orderByDesc('is_default')->orderBy('id')
  69. ->get()->map(self::publicTemplate(...))->all(),
  70. ];
  71. });
  72. }
  73. public static function pricedCatalog(string $key, int $templateId, bool $includeUnconfigured = false): Collection
  74. {
  75. $prices = self::configuredItems($templateId)->sharedLock()->get()->keyBy('item_id');
  76. return self::catalog($key)->orderBy('id')->get()
  77. ->filter(fn ($row) => $includeUnconfigured || $prices->has($row->id))
  78. ->map(function ($row) use ($prices) {
  79. $price = $prices->get($row->id);
  80. $row->odds = $price->odds ?? null;
  81. $row->mininum = $price->mininum ?? null;
  82. $row->maxinum = $price->maxinum ?? null;
  83. return $row;
  84. });
  85. }
  86. public static function pc28Price(object $template, string $keywords): ?object
  87. {
  88. $itemId = self::catalog($template->game_key)->where('keywords', $keywords)->sharedLock()->value('id');
  89. return $itemId === null ? null : self::configuredItems($template->id)->where('item_id', $itemId)->sharedLock()->first();
  90. }
  91. private static function tree(string $key, Collection $rows): array
  92. {
  93. $tree = [];
  94. $isLhc = str_starts_with($key, 'lhc:');
  95. foreach ($rows as $row) {
  96. $group = $row->gameplay ?? $row->groups;
  97. $leaf = [
  98. 'id' => (int) $row->id, 'name' => $row->number ?? $row->keywords,
  99. 'configured' => $row->odds !== null,
  100. 'odds' => $row->odds, 'mininum' => $row->mininum, 'maxinum' => $row->maxinum,
  101. ];
  102. if ($isLhc) {
  103. $tree[$row->game] ??= ['name' => $row->game, 'group_game' => $row->game];
  104. $tree[$row->game]['children'][$group] ??= ['name' => $group, 'group_game' => $row->game, 'gameplay' => $group];
  105. $tree[$row->game]['children'][$group]['children'][] = $leaf;
  106. } else {
  107. $tree[$group] ??= ['name' => $group, 'gameplay' => $group];
  108. $tree[$group]['children'][] = $leaf;
  109. }
  110. }
  111. if ($isLhc) {
  112. foreach ($tree as &$parent) {
  113. $parent['children'] = array_values($parent['children']);
  114. }
  115. }
  116. return array_values($tree);
  117. }
  118. public static function saveTemplate(string $key, array $input): array
  119. {
  120. return DB::transaction(function () use ($key, $input) {
  121. // The oldest template is a stable lock even when the default changes.
  122. self::templates($key)->orderBy('id')->lockForUpdate()->first(['id']);
  123. $default = self::template($key, null, true);
  124. $makeDefault = (bool) ($input['make_default'] ?? false);
  125. if (!empty($input['odds_template_id'])) {
  126. $id = (int) $input['odds_template_id'];
  127. $row = (int) $default->id === $id ? $default : self::template($key, $id, true);
  128. $enabled = (bool) ($input['enabled'] ?? $row->enabled);
  129. if (!$enabled && ($row->is_default || $makeDefault)) {
  130. throw new DomainException('默认模板必须启用,请先启用模板或切换默认模板');
  131. }
  132. $values = ['enabled' => $enabled];
  133. if ($enabled !== (bool) $row->enabled) {
  134. $values['revision'] = $row->revision + 1;
  135. }
  136. if (array_key_exists('names', $input)) {
  137. $values['names'] = self::encodeNames($input['names']);
  138. }
  139. if (array_key_exists('sort', $input)) {
  140. $values['sort'] = (int) $input['sort'];
  141. }
  142. if ($makeDefault && !$row->is_default) {
  143. self::templates($key)->where('id', $default->id)->update(['is_default' => null]);
  144. $values['is_default'] = true;
  145. }
  146. self::templates($key)->where('id', $row->id)->update($values);
  147. return self::publicTemplate(self::template($key, $id));
  148. }
  149. if ($makeDefault) {
  150. throw new DomainException('请先创建并启用模板');
  151. }
  152. $id = self::templates($key)->insertGetId([
  153. 'game_key' => $key,
  154. 'names' => self::encodeNames($input['names']),
  155. 'enabled' => false,
  156. 'sort' => (int) ($input['sort'] ?? 0),
  157. ]);
  158. $items = self::configuredItems($default->id)->selectRaw('? as new_template_id', [$id])
  159. ->addSelect('item_id', 'odds', 'mininum', 'maxinum');
  160. DB::table('lottery_odds_items')->insertUsing(['template_id', 'item_id', 'odds', 'mininum', 'maxinum'], $items);
  161. return self::publicTemplate(self::template($key, $id));
  162. }, 3);
  163. }
  164. private static function encodeNames(array $names): string
  165. {
  166. $names = array_map(fn ($name) => trim($name ?? ''), $names);
  167. if (($names['zh'] ?? '') === '') {
  168. throw new DomainException('请输入中文模板名称');
  169. }
  170. return json_encode(array_filter($names, fn ($name) => $name !== ''), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
  171. }
  172. public static function setOdds(string $key, ?int $id, array $input): void
  173. {
  174. DB::transaction(function () use ($key, $id, $input) {
  175. $template = self::template($key, $id, true);
  176. $catalog = self::catalog($key);
  177. $isLhc = str_starts_with($key, 'lhc:');
  178. if (isset($input['id'])) {
  179. $catalog->where('id', $input['id']);
  180. } else {
  181. if ($isLhc && isset($input['group_game'])) {
  182. $catalog->where('game', $input['group_game']);
  183. }
  184. if (isset($input['gameplay'])) {
  185. $group = in_array($key, ['canada28', 'jisu28'], true) ? 'groups' : 'gameplay';
  186. $catalog->where($group, $input['gameplay']);
  187. }
  188. }
  189. $itemIds = $catalog->orderBy('id')->sharedLock()->pluck('id');
  190. if ($itemIds->isEmpty()) {
  191. throw new DomainException('投注项不存在');
  192. }
  193. $prices = ['odds' => $input['odds'], 'mininum' => $input['mininum'], 'maxinum' => $input['maxinum']];
  194. foreach ($itemIds->chunk(100) as $ids) {
  195. $rows = $ids->map(fn ($itemId) => ['template_id' => $template->id, 'item_id' => $itemId] + $prices)->all();
  196. DB::table('lottery_odds_items')->upsert($rows, ['template_id', 'item_id'], array_keys($prices));
  197. }
  198. self::templates($key)->where('id', $template->id)->increment('revision');
  199. }, 3);
  200. }
  201. public static function orderFields(object $template): array
  202. {
  203. return ['odds_template_id' => (int) $template->id, 'odds_template_revision' => (int) $template->revision];
  204. }
  205. public static function lockPc28Templates(): Collection
  206. {
  207. return DB::table('lottery_odds_templates')->whereIn('game_key', ['canada28', 'jisu28'])
  208. ->orderBy('id')->lockForUpdate()->pluck('id');
  209. }
  210. public static function pc28CatalogChanged(Collection $templates, int $itemId, ?array $initial = null, bool $deleted = false): void
  211. {
  212. foreach ($templates as $id) {
  213. if ($deleted) {
  214. self::items($id)->where('item_id', $itemId)->delete();
  215. } elseif ($initial !== null) {
  216. DB::table('lottery_odds_items')->insert(['template_id' => $id, 'item_id' => $itemId] + $initial);
  217. }
  218. }
  219. DB::table('lottery_odds_templates')->whereIn('id', $templates)->increment('revision');
  220. }
  221. }