| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <?php
- namespace App\Services;
- use DomainException;
- use Illuminate\Database\Query\Builder;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\DB;
- final class LotteryOdds
- {
- public static function gameKeys(): array
- {
- return [
- 'canada28', 'jisu28', 'lhc:1', 'lhc:2', 'lhc:3', 'lhc:4',
- 'jisu:1', 'jisu:2', 'jisu:3', 'jisu:4', 'jisu:5', 'jisu:6', 'jisu:7',
- ];
- }
- public static function gameKey(string $game, ?int $type): string
- {
- $key = $type === null ? $game : "$game:$type";
- if (!in_array($key, self::gameKeys(), true)) {
- throw new DomainException('彩种无效');
- }
- return $key;
- }
- public static function catalog(string $key): Builder
- {
- [$game, $type] = array_pad(explode(':', $key, 2), 2, null);
- return match ($game) {
- 'jisu' => DB::table('jisu_gameplay')->where('type', (int) $type),
- 'lhc' => DB::table('lhc_number'),
- 'canada28', 'jisu28' => DB::table('gameplay_rules'),
- };
- }
- private static function templates(string $key): Builder
- {
- return DB::table('lottery_odds_templates')->where('game_key', $key);
- }
- public static function template(string $key, ?int $id = null, bool $write = false): object
- {
- $query = self::templates($key);
- $id === null ? $query->where('is_default', 1) : $query->where('id', $id);
- $row = ($write ? $query->lockForUpdate() : $query->sharedLock())->first();
- return $row ?? throw new DomainException('赔率模板不存在');
- }
- public static function items(int $templateId): Builder
- {
- return DB::table('lottery_odds_items')->where('template_id', $templateId);
- }
- private static function configuredItems(int $templateId): Builder
- {
- return self::items($templateId)->whereRaw("TRIM(odds) <> ''")
- ->whereNotNull('mininum')->whereNotNull('maxinum');
- }
- private static function publicTemplate(object $row): array
- {
- return [
- 'id' => (int) $row->id, 'names' => json_decode($row->names, true),
- 'is_default' => (bool) $row->is_default, 'enabled' => (bool) $row->enabled,
- 'sort' => (int) $row->sort, 'revision' => (int) $row->revision,
- ];
- }
- public static function list(string $key, ?int $id): array
- {
- return DB::transaction(function () use ($key, $id) {
- $template = self::template($key, $id);
- return [
- 'data' => self::tree($key, self::pricedCatalog($key, (int) $template->id, includeUnconfigured: true)),
- 'odds_template_id' => (int) $template->id,
- 'odds_templates' => self::templates($key)->orderByDesc('sort')->orderByDesc('is_default')->orderBy('id')
- ->get()->map(self::publicTemplate(...))->all(),
- ];
- });
- }
- public static function pricedCatalog(string $key, int $templateId, bool $includeUnconfigured = false): Collection
- {
- $prices = self::configuredItems($templateId)->sharedLock()->get()->keyBy('item_id');
- return self::catalog($key)->orderBy('id')->get()
- ->filter(fn ($row) => $includeUnconfigured || $prices->has($row->id))
- ->map(function ($row) use ($prices) {
- $price = $prices->get($row->id);
- $row->odds = $price->odds ?? null;
- $row->mininum = $price->mininum ?? null;
- $row->maxinum = $price->maxinum ?? null;
- return $row;
- });
- }
- public static function pc28Price(object $template, string $keywords): ?object
- {
- $itemId = self::catalog($template->game_key)->where('keywords', $keywords)->sharedLock()->value('id');
- return $itemId === null ? null : self::configuredItems($template->id)->where('item_id', $itemId)->sharedLock()->first();
- }
- private static function tree(string $key, Collection $rows): array
- {
- $tree = [];
- $isLhc = str_starts_with($key, 'lhc:');
- foreach ($rows as $row) {
- $group = $row->gameplay ?? $row->groups;
- $leaf = [
- 'id' => (int) $row->id, 'name' => $row->number ?? $row->keywords,
- 'configured' => $row->odds !== null,
- 'odds' => $row->odds, 'mininum' => $row->mininum, 'maxinum' => $row->maxinum,
- ];
- if ($isLhc) {
- $tree[$row->game] ??= ['name' => $row->game, 'group_game' => $row->game];
- $tree[$row->game]['children'][$group] ??= ['name' => $group, 'group_game' => $row->game, 'gameplay' => $group];
- $tree[$row->game]['children'][$group]['children'][] = $leaf;
- } else {
- $tree[$group] ??= ['name' => $group, 'gameplay' => $group];
- $tree[$group]['children'][] = $leaf;
- }
- }
- if ($isLhc) {
- foreach ($tree as &$parent) {
- $parent['children'] = array_values($parent['children']);
- }
- }
- return array_values($tree);
- }
- public static function saveTemplate(string $key, array $input): array
- {
- return DB::transaction(function () use ($key, $input) {
- // The oldest template is a stable lock even when the default changes.
- self::templates($key)->orderBy('id')->lockForUpdate()->first(['id']);
- $default = self::template($key, null, true);
- $makeDefault = (bool) ($input['make_default'] ?? false);
- if (!empty($input['odds_template_id'])) {
- $id = (int) $input['odds_template_id'];
- $row = (int) $default->id === $id ? $default : self::template($key, $id, true);
- $enabled = (bool) ($input['enabled'] ?? $row->enabled);
- if (!$enabled && ($row->is_default || $makeDefault)) {
- throw new DomainException('默认模板必须启用,请先启用模板或切换默认模板');
- }
- $values = ['enabled' => $enabled];
- if ($enabled !== (bool) $row->enabled) {
- $values['revision'] = $row->revision + 1;
- }
- if (array_key_exists('names', $input)) {
- $values['names'] = self::encodeNames($input['names']);
- }
- if (array_key_exists('sort', $input)) {
- $values['sort'] = (int) $input['sort'];
- }
- if ($makeDefault && !$row->is_default) {
- self::templates($key)->where('id', $default->id)->update(['is_default' => null]);
- $values['is_default'] = true;
- }
- self::templates($key)->where('id', $row->id)->update($values);
- return self::publicTemplate(self::template($key, $id));
- }
- if ($makeDefault) {
- throw new DomainException('请先创建并启用模板');
- }
- $id = self::templates($key)->insertGetId([
- 'game_key' => $key,
- 'names' => self::encodeNames($input['names']),
- 'enabled' => false,
- 'sort' => (int) ($input['sort'] ?? 0),
- ]);
- $items = self::configuredItems($default->id)->selectRaw('? as new_template_id', [$id])
- ->addSelect('item_id', 'odds', 'mininum', 'maxinum');
- DB::table('lottery_odds_items')->insertUsing(['template_id', 'item_id', 'odds', 'mininum', 'maxinum'], $items);
- return self::publicTemplate(self::template($key, $id));
- }, 3);
- }
- private static function encodeNames(array $names): string
- {
- $names = array_map(fn ($name) => trim($name ?? ''), $names);
- if (($names['zh'] ?? '') === '') {
- throw new DomainException('请输入中文模板名称');
- }
- return json_encode(array_filter($names, fn ($name) => $name !== ''), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
- }
- public static function setOdds(string $key, ?int $id, array $input): void
- {
- DB::transaction(function () use ($key, $id, $input) {
- $template = self::template($key, $id, true);
- $catalog = self::catalog($key);
- $isLhc = str_starts_with($key, 'lhc:');
- if (isset($input['id'])) {
- $catalog->where('id', $input['id']);
- } else {
- if ($isLhc && isset($input['group_game'])) {
- $catalog->where('game', $input['group_game']);
- }
- if (isset($input['gameplay'])) {
- $group = in_array($key, ['canada28', 'jisu28'], true) ? 'groups' : 'gameplay';
- $catalog->where($group, $input['gameplay']);
- }
- }
- $itemIds = $catalog->orderBy('id')->sharedLock()->pluck('id');
- if ($itemIds->isEmpty()) {
- throw new DomainException('投注项不存在');
- }
- $prices = ['odds' => $input['odds'], 'mininum' => $input['mininum'], 'maxinum' => $input['maxinum']];
- foreach ($itemIds->chunk(100) as $ids) {
- $rows = $ids->map(fn ($itemId) => ['template_id' => $template->id, 'item_id' => $itemId] + $prices)->all();
- DB::table('lottery_odds_items')->upsert($rows, ['template_id', 'item_id'], array_keys($prices));
- }
- self::templates($key)->where('id', $template->id)->increment('revision');
- }, 3);
- }
- public static function orderFields(object $template): array
- {
- return ['odds_template_id' => (int) $template->id, 'odds_template_revision' => (int) $template->revision];
- }
- public static function lockPc28Templates(): Collection
- {
- return DB::table('lottery_odds_templates')->whereIn('game_key', ['canada28', 'jisu28'])
- ->orderBy('id')->lockForUpdate()->pluck('id');
- }
- public static function pc28CatalogChanged(Collection $templates, int $itemId, ?array $initial = null, bool $deleted = false): void
- {
- foreach ($templates as $id) {
- if ($deleted) {
- self::items($id)->where('item_id', $itemId)->delete();
- } elseif ($initial !== null) {
- DB::table('lottery_odds_items')->insert(['template_id' => $id, 'item_id' => $itemId] + $initial);
- }
- }
- DB::table('lottery_odds_templates')->whereIn('id', $templates)->increment('revision');
- }
- }
|