doge пре 1 недеља
родитељ
комит
587cbb463c

+ 475 - 75
app/Http/Controllers/admin/Egame.php

@@ -13,16 +13,37 @@ use Illuminate\Validation\ValidationException;
 
 
 class Egame extends Controller
 class Egame extends Controller
 {
 {
+    private const FALLBACK_GAME_TYPE_NAMES = [
+        1 => '视讯',
+        2 => '电子',
+        3 => '彩票',
+        4 => '体育',
+        5 => '电竞',
+        6 => '捕鱼',
+        7 => '棋牌',
+    ];
+
     public function items(): JsonResponse
     public function items(): JsonResponse
     {
     {
         try {
         try {
             request()->validate([
             request()->validate([
-                'item_type' => ['nullable', Rule::in(['platform', 'game'])],
+                'item_type' => ['nullable', Rule::in([
+                    EgameItem::TYPE_CATEGORY,
+                    EgameItem::TYPE_PLATFORM,
+                    EgameItem::TYPE_GAME,
+                ])],
+                'platform_scope' => ['nullable', Rule::in([
+                    EgameItem::PLATFORM_SCOPE_GLOBAL,
+                    EgameItem::PLATFORM_SCOPE_CATEGORY,
+                ])],
                 'plat_type' => ['nullable', 'string', 'max:32'],
                 'plat_type' => ['nullable', 'string', 'max:32'],
                 'game_type' => ['nullable', 'integer', 'min:0', 'max:255'],
                 'game_type' => ['nullable', 'integer', 'min:0', 'max:255'],
                 'game_code' => ['nullable', 'string', 'max:128'],
                 'game_code' => ['nullable', 'string', 'max:128'],
                 'keyword' => ['nullable', 'string', 'max:128'],
                 'keyword' => ['nullable', 'string', 'max:128'],
-                'status' => ['nullable', 'integer', Rule::in([0, 1])],
+                'status' => ['nullable', 'integer', Rule::in([
+                    EgameItem::STATUS_DISABLED,
+                    EgameItem::STATUS_ENABLED,
+                ])],
                 'page' => ['nullable', 'integer', 'min:1'],
                 'page' => ['nullable', 'integer', 'min:1'],
                 'limit' => ['nullable', 'integer', 'min:1', 'max:200'],
                 'limit' => ['nullable', 'integer', 'min:1', 'max:200'],
             ]);
             ]);
@@ -35,7 +56,21 @@ class Egame extends Controller
                 }
                 }
             }
             }
 
 
-            $keyword = trim((string)request()->input('keyword', ''));
+            $platformScope = (string) request()->input('platform_scope', '');
+            if ($platformScope !== '') {
+                $itemType = (string) request()->input('item_type', '');
+                if ($itemType !== '' && $itemType !== EgameItem::TYPE_PLATFORM) {
+                    throw new Exception('platform_scope 只适用于平台配置', HttpStatus::CUSTOM_ERROR);
+                }
+                $query->where('item_type', EgameItem::TYPE_PLATFORM);
+                if ($platformScope === EgameItem::PLATFORM_SCOPE_GLOBAL) {
+                    $query->where('game_type', 0);
+                } else {
+                    $query->where('game_type', '>', 0);
+                }
+            }
+
+            $keyword = trim((string) request()->input('keyword', ''));
             if ($keyword !== '') {
             if ($keyword !== '') {
                 $query->where(function ($query) use ($keyword) {
                 $query->where(function ($query) use ($keyword) {
                     $query->where('name', 'like', "%{$keyword}%")
                     $query->where('name', 'like', "%{$keyword}%")
@@ -44,13 +79,16 @@ class Egame extends Controller
                 });
                 });
             }
             }
 
 
-            $limit = (int)request()->input('limit', 15);
-            $page = (int)request()->input('page', 1);
+            $limit = (int) request()->input('limit', 15);
+            $page = (int) request()->input('page', 1);
             $total = (clone $query)->count();
             $total = (clone $query)->count();
+            $states = $this->catalogStates();
             $list = $query->orderByDesc('sort')
             $list = $query->orderByDesc('sort')
                 ->orderByDesc('id')
                 ->orderByDesc('id')
                 ->forPage($page, $limit)
                 ->forPage($page, $limit)
-                ->get();
+                ->get()
+                ->map(fn (EgameItem $item) => $this->decorateItem($item, $states))
+                ->values();
         } catch (ValidationException $e) {
         } catch (ValidationException $e) {
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
         } catch (Exception $e) {
         } catch (Exception $e) {
@@ -60,84 +98,144 @@ class Egame extends Controller
         return $this->success(['total' => $total, 'data' => $list]);
         return $this->success(['total' => $total, 'data' => $list]);
     }
     }
 
 
+    public function options(): JsonResponse
+    {
+        try {
+            $states = $this->catalogStates();
+            $categoryPlatformCounts = $this->groupedCounts(
+                EgameItem::query()
+                    ->where('item_type', EgameItem::TYPE_PLATFORM)
+                    ->where('game_type', '>', 0),
+                'game_type',
+                'plat_type'
+            );
+            $categoryGameCounts = $this->groupedCounts(
+                EgameItem::query()->where('item_type', EgameItem::TYPE_GAME),
+                'game_type',
+                'id'
+            );
+            $platformCategoryCounts = $this->groupedCounts(
+                EgameItem::query()
+                    ->where('item_type', EgameItem::TYPE_PLATFORM)
+                    ->where('game_type', '>', 0),
+                'plat_type',
+                'game_type'
+            );
+            $platformGameCounts = $this->groupedCounts(
+                EgameItem::query()->where('item_type', EgameItem::TYPE_GAME),
+                'plat_type',
+                'id'
+            );
+
+            $categories = EgameItem::query()
+                ->where('item_type', EgameItem::TYPE_CATEGORY)
+                ->orderByDesc('sort')
+                ->orderBy('game_type')
+                ->get()
+                ->map(function (EgameItem $item) use ($states, $categoryPlatformCounts, $categoryGameCounts) {
+                    $this->decorateItem($item, $states);
+                    $key = (string) $item->game_type;
+                    $item->setAttribute('platform_count', $categoryPlatformCounts[$key] ?? 0);
+                    $item->setAttribute('game_count', $categoryGameCounts[$key] ?? 0);
+                    return $item;
+                })
+                ->values();
+
+            $platforms = EgameItem::query()
+                ->where('item_type', EgameItem::TYPE_PLATFORM)
+                ->where('game_type', 0)
+                ->orderByDesc('sort')
+                ->orderBy('name')
+                ->orderBy('plat_type')
+                ->get()
+                ->map(function (EgameItem $item) use ($states, $platformCategoryCounts, $platformGameCounts) {
+                    $this->decorateItem($item, $states);
+                    $key = strtolower((string) $item->plat_type);
+                    $item->setAttribute('category_count', $platformCategoryCounts[$key] ?? 0);
+                    $item->setAttribute('game_count', $platformGameCounts[$key] ?? 0);
+                    return $item;
+                })
+                ->values();
+        } catch (Exception $e) {
+            return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
+        }
+
+        return $this->success([
+            'categories' => $categories,
+            'platforms' => $platforms,
+            'statuses' => [
+                ['value' => EgameItem::STATUS_ENABLED, 'label' => '开启'],
+                ['value' => EgameItem::STATUS_DISABLED, 'label' => '关闭'],
+            ],
+            'ingress' => [
+                ['value' => '1', 'label' => '仅电脑'],
+                ['value' => '2', 'label' => '仅手机'],
+                ['value' => '3', 'label' => '通用'],
+            ],
+        ]);
+    }
+
     public function update(): JsonResponse
     public function update(): JsonResponse
     {
     {
-        DB::beginTransaction();
         try {
         try {
             request()->validate([
             request()->validate([
                 'id' => ['nullable', 'integer', 'min:0'],
                 'id' => ['nullable', 'integer', 'min:0'],
-                'item_type' => ['required', Rule::in(['platform', 'game'])],
-                'plat_type' => ['required', 'string', 'max:32'],
-                'game_type' => ['required', 'integer', 'min:0', 'max:255'],
+                'item_type' => ['nullable', Rule::in([
+                    EgameItem::TYPE_CATEGORY,
+                    EgameItem::TYPE_PLATFORM,
+                    EgameItem::TYPE_GAME,
+                ])],
+                'platform_scope' => ['nullable', Rule::in([
+                    EgameItem::PLATFORM_SCOPE_GLOBAL,
+                    EgameItem::PLATFORM_SCOPE_CATEGORY,
+                ])],
+                'plat_type' => ['nullable', 'string', 'max:32'],
+                'game_type' => ['nullable', 'integer', 'min:0', 'max:255'],
                 'game_code' => ['nullable', 'string', 'max:128'],
                 'game_code' => ['nullable', 'string', 'max:128'],
                 'name' => ['nullable', 'string', 'max:128'],
                 'name' => ['nullable', 'string', 'max:128'],
-                'ingress' => ['nullable', 'string', 'max:16'],
+                'ingress' => ['nullable', Rule::in(['1', '2', '3'])],
+                'lobby_enabled' => ['nullable', 'boolean'],
                 'logo' => ['nullable', 'string', 'max:500'],
                 'logo' => ['nullable', 'string', 'max:500'],
                 'logo_langs' => ['nullable', 'array'],
                 'logo_langs' => ['nullable', 'array'],
-                'status' => ['nullable', 'integer', Rule::in([0, 1])],
+                'status' => ['nullable', 'integer', Rule::in([
+                    EgameItem::STATUS_DISABLED,
+                    EgameItem::STATUS_ENABLED,
+                ])],
                 'sort' => ['nullable', 'integer', 'min:-999999', 'max:999999'],
                 'sort' => ['nullable', 'integer', 'min:-999999', 'max:999999'],
             ]);
             ]);
 
 
-            $data = [
-                'item_type' => request()->input('item_type'),
-                'plat_type' => strtolower(trim((string)request()->input('plat_type'))),
-                'game_type' => (int)request()->input('game_type'),
-                'game_code' => trim((string)request()->input('game_code', '')),
-                'name' => trim((string)request()->input('name', '')),
-                'ingress' => trim((string)request()->input('ingress', '3')),
-                'logo' => trim((string)request()->input('logo', '')),
-                'logo_langs' => request()->input('logo_langs', null),
-                'status' => (int)request()->input('status', 1),
-                'sort' => (int)request()->input('sort', 0),
-            ];
-
-            if ($data['item_type'] === 'platform') {
-                $data['game_code'] = '';
-                $data['ingress'] = '3';
-            }
-            if ($data['item_type'] === 'game' && $data['game_code'] === '') {
-                throw new Exception('游戏配置必须填写 game_code', HttpStatus::CUSTOM_ERROR);
-            }
-
-            $id = (int)request()->input('id', 0);
-            if ($id > 0) {
-                $item = EgameItem::query()->find($id);
-                if (!$item) {
-                    throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR);
+            $id = (int) request()->input('id', 0);
+            $item = DB::transaction(function () use ($id) {
+                if ($id > 0) {
+                    $item = EgameItem::query()->find($id);
+                    if (!$item) {
+                        throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR);
+                    }
+                    $this->assertIdentityUnchanged($item);
+                } else {
+                    $identity = $this->normalizedIdentityForCreate();
+                    $item = EgameItem::query()->firstOrNew($identity);
                 }
                 }
-                $duplicate = EgameItem::query()
-                    ->where('item_type', $data['item_type'])
-                    ->where('plat_type', $data['plat_type'])
-                    ->where('game_type', $data['game_type'])
-                    ->where('game_code', $data['game_code'])
-                    ->where('id', '<>', $id)
-                    ->exists();
-                if ($duplicate) {
-                    throw new Exception('相同平台/类型/游戏代码配置已存在', HttpStatus::CUSTOM_ERROR);
+
+                $data = $this->mutableItemData($item, !$item->exists);
+                $name = array_key_exists('name', $data) ? $data['name'] : (string) $item->name;
+                if ($item->isCategory() && trim($name) === '') {
+                    throw new Exception('分类名称不能为空', HttpStatus::CUSTOM_ERROR);
                 }
                 }
-                $item->fill($data)->save();
-            } else {
-                EgameItem::query()->updateOrCreate(
-                    [
-                        'item_type' => $data['item_type'],
-                        'plat_type' => $data['plat_type'],
-                        'game_type' => $data['game_type'],
-                        'game_code' => $data['game_code'],
-                    ],
-                    $data
-                );
-            }
-
-            DB::commit();
+
+                if (!empty($data) || !$item->exists) {
+                    $item->fill($data)->save();
+                }
+
+                return $item->fresh();
+            });
         } catch (ValidationException $e) {
         } catch (ValidationException $e) {
-            DB::rollBack();
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
         } catch (Exception $e) {
         } catch (Exception $e) {
-            DB::rollBack();
-            return $this->error((int)$e->getCode(), $e->getMessage());
+            return $this->error((int) $e->getCode(), $e->getMessage());
         }
         }
 
 
-        return $this->success();
+        return $this->success($this->decorateItem($item, $this->catalogStates()));
     }
     }
 
 
     public function setStatus(): JsonResponse
     public function setStatus(): JsonResponse
@@ -145,22 +243,26 @@ class Egame extends Controller
         try {
         try {
             request()->validate([
             request()->validate([
                 'id' => ['required', 'integer', 'min:1'],
                 'id' => ['required', 'integer', 'min:1'],
-                'status' => ['required', 'integer', Rule::in([0, 1])],
+                'status' => ['required', 'integer', Rule::in([
+                    EgameItem::STATUS_DISABLED,
+                    EgameItem::STATUS_ENABLED,
+                ])],
             ]);
             ]);
 
 
-            $item = EgameItem::query()->find((int)request()->input('id'));
+            $item = EgameItem::query()->find((int) request()->input('id'));
             if (!$item) {
             if (!$item) {
                 throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR);
                 throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR);
             }
             }
-            $item->status = (int)request()->input('status');
+            $item->status = (int) request()->input('status');
             $item->save();
             $item->save();
+            $item = $item->fresh();
         } catch (ValidationException $e) {
         } catch (ValidationException $e) {
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
         } catch (Exception $e) {
         } catch (Exception $e) {
-            return $this->error((int)$e->getCode(), $e->getMessage());
+            return $this->error((int) $e->getCode(), $e->getMessage());
         }
         }
 
 
-        return $this->success();
+        return $this->success($this->decorateItem($item, $this->catalogStates()));
     }
     }
 
 
     public function delete(): JsonResponse
     public function delete(): JsonResponse
@@ -170,17 +272,315 @@ class Egame extends Controller
                 'id' => ['required', 'integer', 'min:1'],
                 'id' => ['required', 'integer', 'min:1'],
             ]);
             ]);
 
 
-            $item = EgameItem::query()->find((int)request()->input('id'));
-            if (!$item) {
+            if (!EgameItem::query()->whereKey((int) request()->input('id'))->exists()) {
                 throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR);
                 throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR);
             }
             }
-            $item->delete();
         } catch (ValidationException $e) {
         } catch (ValidationException $e) {
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
             return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
         } catch (Exception $e) {
         } catch (Exception $e) {
-            return $this->error((int)$e->getCode(), $e->getMessage());
+            return $this->error((int) $e->getCode(), $e->getMessage());
+        }
+
+        return $this->error(
+            HttpStatus::CUSTOM_ERROR,
+            '第三方游戏目录由同步任务维护,不能删除,请将状态设为关闭'
+        );
+    }
+
+    private function normalizedIdentityForCreate(): array
+    {
+        $input = request()->all();
+        if (!array_key_exists('item_type', $input) || !array_key_exists('game_type', $input)) {
+            throw new Exception('新增配置必须填写 item_type 和 game_type', HttpStatus::CUSTOM_ERROR);
+        }
+
+        $itemType = (string) request()->input('item_type', '');
+        $platformScope = (string) request()->input('platform_scope', '');
+        if (!in_array($itemType, [
+            EgameItem::TYPE_CATEGORY,
+            EgameItem::TYPE_PLATFORM,
+            EgameItem::TYPE_GAME,
+        ], true)) {
+            throw new Exception('item_type 参数错误', HttpStatus::CUSTOM_ERROR);
         }
         }
+        if ($platformScope !== '' && $itemType !== EgameItem::TYPE_PLATFORM) {
+            throw new Exception('platform_scope 只适用于平台配置', HttpStatus::CUSTOM_ERROR);
+        }
+
+        $identity = [
+            'item_type' => $itemType,
+            'plat_type' => strtolower(trim((string) request()->input('plat_type', ''))),
+            'game_type' => (int) request()->input('game_type'),
+            'game_code' => trim((string) request()->input('game_code', '')),
+        ];
+
+        if ($itemType === EgameItem::TYPE_CATEGORY) {
+            if ($identity['game_type'] <= 0) {
+                throw new Exception('分类 game_type 必须大于 0', HttpStatus::CUSTOM_ERROR);
+            }
+            $identity['plat_type'] = '';
+            $identity['game_code'] = '';
+            return $identity;
+        }
+
+        if ($identity['plat_type'] === '') {
+            throw new Exception('平台代码不能为空', HttpStatus::CUSTOM_ERROR);
+        }
+
+        if ($itemType === EgameItem::TYPE_PLATFORM) {
+            if ($platformScope === EgameItem::PLATFORM_SCOPE_GLOBAL) {
+                $identity['game_type'] = 0;
+            }
+            if ($platformScope === EgameItem::PLATFORM_SCOPE_CATEGORY && $identity['game_type'] <= 0) {
+                throw new Exception('平台分类关联的 game_type 必须大于 0', HttpStatus::CUSTOM_ERROR);
+            }
+            $identity['game_code'] = '';
+            return $identity;
+        }
+
+        if ($identity['game_type'] <= 0) {
+            throw new Exception('游戏 game_type 必须大于 0', HttpStatus::CUSTOM_ERROR);
+        }
+        if ($identity['game_code'] === '') {
+            throw new Exception('游戏配置必须填写 game_code', HttpStatus::CUSTOM_ERROR);
+        }
+
+        return $identity;
+    }
+
+    private function assertIdentityUnchanged(EgameItem $item): void
+    {
+        $input = request()->all();
+        $checks = [
+            'item_type' => (string) $item->item_type,
+            'plat_type' => strtolower((string) $item->plat_type),
+            'game_type' => (int) $item->game_type,
+            'game_code' => (string) $item->game_code,
+        ];
+
+        foreach ($checks as $field => $expected) {
+            if (!array_key_exists($field, $input) || $input[$field] === null) {
+                continue;
+            }
+            $actual = $input[$field];
+            if ($field === 'plat_type') {
+                $actual = strtolower(trim((string) $actual));
+            } elseif ($field === 'game_type') {
+                $actual = (int) $actual;
+            } else {
+                $actual = trim((string) $actual);
+            }
+            if ($actual !== $expected) {
+                throw new Exception('目录标识不允许修改:' . $field, HttpStatus::CUSTOM_ERROR);
+            }
+        }
+
+        if (
+            array_key_exists('platform_scope', $input)
+            && $input['platform_scope'] !== null
+            && $input['platform_scope'] !== ''
+        ) {
+            $expectedScope = '';
+            if ($item->isGlobalPlatform()) {
+                $expectedScope = EgameItem::PLATFORM_SCOPE_GLOBAL;
+            } elseif ($item->isPlatformRelation()) {
+                $expectedScope = EgameItem::PLATFORM_SCOPE_CATEGORY;
+            }
+            if ((string) $input['platform_scope'] !== $expectedScope) {
+                throw new Exception('目录标识不允许修改:platform_scope', HttpStatus::CUSTOM_ERROR);
+            }
+        }
+    }
 
 
-        return $this->success([], '删除成功');
+    private function mutableItemData(EgameItem $item, bool $isNew): array
+    {
+        $input = request()->all();
+        $data = $isNew ? [
+            'name' => '',
+            'ingress' => '3',
+            'lobby_enabled' => 0,
+            'logo' => '',
+            'logo_langs' => null,
+            'status' => EgameItem::STATUS_ENABLED,
+            'sort' => 0,
+        ] : [];
+
+        foreach (['name', 'ingress', 'logo'] as $field) {
+            if (array_key_exists($field, $input)) {
+                $data[$field] = trim((string) $input[$field]);
+            }
+        }
+        if (array_key_exists('logo_langs', $input)) {
+            $data['logo_langs'] = $item->item_type === EgameItem::TYPE_GAME
+                ? $input['logo_langs']
+                : null;
+        }
+        if (array_key_exists('status', $input)) {
+            $data['status'] = (int) $input['status'];
+        }
+        if (array_key_exists('sort', $input)) {
+            $data['sort'] = (int) $input['sort'];
+        }
+        if (array_key_exists('lobby_enabled', $input)) {
+            $data['lobby_enabled'] = $item->isPlatformRelation()
+                ? (int) request()->boolean('lobby_enabled')
+                : 0;
+        }
+
+        if ($item->item_type !== EgameItem::TYPE_GAME && array_key_exists('ingress', $data)) {
+            $data['ingress'] = '3';
+        }
+        if ($item->item_type !== EgameItem::TYPE_GAME && array_key_exists('logo_langs', $data)) {
+            $data['logo_langs'] = null;
+        }
+        if (!$item->isPlatformRelation() && array_key_exists('lobby_enabled', $data)) {
+            $data['lobby_enabled'] = 0;
+        }
+
+        return $data;
+    }
+
+    private function catalogStates(): array
+    {
+        $states = [
+            'categories' => [],
+            'global_platforms' => [],
+            'platform_relations' => [],
+        ];
+
+        $rows = EgameItem::query()
+            ->whereIn('item_type', [EgameItem::TYPE_CATEGORY, EgameItem::TYPE_PLATFORM])
+            ->get();
+
+        foreach ($rows as $row) {
+            if ($row->isCategory()) {
+                $states['categories'][(string) $row->game_type] = $row;
+                continue;
+            }
+
+            $platType = strtolower((string) $row->plat_type);
+            if ($row->isGlobalPlatform()) {
+                $states['global_platforms'][$platType] = $row;
+                continue;
+            }
+            if ($row->isPlatformRelation()) {
+                $states['platform_relations'][$platType][(string) $row->game_type] = $row;
+            }
+        }
+
+        return $states;
+    }
+
+    private function decorateItem(EgameItem $item, array $states): EgameItem
+    {
+        $platType = strtolower((string) $item->plat_type);
+        $gameType = (string) $item->game_type;
+        $disabledBy = null;
+
+        $category = $states['categories'][$gameType] ?? null;
+        $globalPlatform = $states['global_platforms'][$platType] ?? null;
+        $platformRelation = $states['platform_relations'][$platType][$gameType] ?? null;
+
+        if ($item->isCategory()) {
+            if (!$this->enabled($item)) {
+                $disabledBy = 'category';
+            }
+        } elseif ($item->isGlobalPlatform()) {
+            if (!$this->enabled($item)) {
+                $disabledBy = 'platform';
+            }
+        } elseif ($item->isPlatformRelation()) {
+            if (!$this->enabled($category)) {
+                $disabledBy = 'category';
+            } elseif (!$this->enabled($globalPlatform)) {
+                $disabledBy = 'platform';
+            } elseif (!$this->enabled($item)) {
+                $disabledBy = 'platform_category';
+            }
+        } else {
+            if (!$this->enabled($category)) {
+                $disabledBy = 'category';
+            } elseif (!$this->enabled($globalPlatform)) {
+                $disabledBy = 'platform';
+            } elseif (!$this->enabled($platformRelation)) {
+                $disabledBy = 'platform_category';
+            } elseif (!$this->enabled($item)) {
+                $disabledBy = 'game';
+            }
+        }
+
+        $item->setAttribute('game_type_name', $this->gameTypeName((int) $item->game_type, $states));
+        $item->setAttribute(
+            'platform_name',
+            $this->platformName($item, $globalPlatform, $platformRelation)
+        );
+        if ($item->item_type === EgameItem::TYPE_PLATFORM) {
+            $item->setAttribute(
+                'platform_scope',
+                $item->isGlobalPlatform()
+                    ? EgameItem::PLATFORM_SCOPE_GLOBAL
+                    : EgameItem::PLATFORM_SCOPE_CATEGORY
+            );
+        } else {
+            $item->setAttribute('platform_scope', null);
+        }
+        $item->setAttribute('effective_status', $disabledBy === null ? 1 : 0);
+        $item->setAttribute('disabled_by', $disabledBy);
+
+        return $item;
+    }
+
+    private function enabled(?EgameItem $item): bool
+    {
+        return $item !== null && $item->status === EgameItem::STATUS_ENABLED;
+    }
+
+    private function platformName(
+        EgameItem $item,
+        ?EgameItem $globalPlatform,
+        ?EgameItem $platformRelation
+    ): string {
+        if ($item->isCategory()) {
+            return '';
+        }
+        if ($item->isGlobalPlatform() && trim((string) $item->name) !== '') {
+            return (string) $item->name;
+        }
+        if ($globalPlatform && trim((string) $globalPlatform->name) !== '') {
+            return (string) $globalPlatform->name;
+        }
+        if ($platformRelation && trim((string) $platformRelation->name) !== '') {
+            return (string) $platformRelation->name;
+        }
+        if ($item->item_type === EgameItem::TYPE_PLATFORM && trim((string) $item->name) !== '') {
+            return (string) $item->name;
+        }
+
+        return (string) $item->plat_type;
+    }
+
+    private function gameTypeName(int $gameType, array $states): string
+    {
+        if ($gameType <= 0) {
+            return '';
+        }
+
+        $category = $states['categories'][(string) $gameType] ?? null;
+        if ($category && trim((string) $category->name) !== '') {
+            return (string) $category->name;
+        }
+
+        return self::FALLBACK_GAME_TYPE_NAMES[$gameType] ?? ('分类' . $gameType);
+    }
+
+    private function groupedCounts($query, string $groupColumn, string $countColumn): array
+    {
+        return $query
+            ->select($groupColumn)
+            ->selectRaw("COUNT(DISTINCT {$countColumn}) AS aggregate")
+            ->groupBy($groupColumn)
+            ->pluck('aggregate', $groupColumn)
+            ->map(fn ($count) => (int) $count)
+            ->all();
     }
     }
 }
 }

+ 30 - 0
app/Models/EgameItem.php

@@ -4,6 +4,16 @@ namespace App\Models;
 
 
 class EgameItem extends BaseModel
 class EgameItem extends BaseModel
 {
 {
+    public const TYPE_CATEGORY = 'category';
+    public const TYPE_PLATFORM = 'platform';
+    public const TYPE_GAME = 'game';
+
+    public const PLATFORM_SCOPE_GLOBAL = 'global';
+    public const PLATFORM_SCOPE_CATEGORY = 'category';
+
+    public const STATUS_DISABLED = 0;
+    public const STATUS_ENABLED = 1;
+
     protected $table = 'egame_items';
     protected $table = 'egame_items';
 
 
     protected $fillable = [
     protected $fillable = [
@@ -13,6 +23,7 @@ class EgameItem extends BaseModel
         'game_code',
         'game_code',
         'name',
         'name',
         'ingress',
         'ingress',
+        'lobby_enabled',
         'logo',
         'logo',
         'logo_langs',
         'logo_langs',
         'status',
         'status',
@@ -20,6 +31,25 @@ class EgameItem extends BaseModel
     ];
     ];
 
 
     protected $casts = [
     protected $casts = [
+        'game_type' => 'integer',
+        'lobby_enabled' => 'integer',
         'logo_langs' => 'array',
         'logo_langs' => 'array',
+        'status' => 'integer',
+        'sort' => 'integer',
     ];
     ];
+
+    public function isCategory(): bool
+    {
+        return $this->item_type === self::TYPE_CATEGORY;
+    }
+
+    public function isGlobalPlatform(): bool
+    {
+        return $this->item_type === self::TYPE_PLATFORM && $this->game_type === 0;
+    }
+
+    public function isPlatformRelation(): bool
+    {
+        return $this->item_type === self::TYPE_PLATFORM && $this->game_type > 0;
+    }
 }
 }

+ 425 - 0
database/migrations/2026_07_21_120000_add_catalog_management_to_egame_items_table.php

@@ -0,0 +1,425 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+    public function up(): void
+    {
+        if (!Schema::hasTable('egame_items')) {
+            return;
+        }
+
+        if (!Schema::hasColumn('egame_items', 'lobby_enabled')) {
+            Schema::table('egame_items', function (Blueprint $table) {
+                $table->unsignedTinyInteger('lobby_enabled')
+                    ->default(0)
+                    ->after('logo_langs')
+                    ->comment('1允许无gameCode进入平台大厅 0不允许');
+            });
+        }
+
+        $now = date('Y-m-d H:i:s');
+        $gameTypes = $this->gameTypes();
+        $platforms = $this->platforms();
+        $lobbyTypes = $this->platformGameTypes();
+        $visibleTypes = $this->platformVisibleTypes();
+        $categoryLogos = $this->categoryLogos();
+
+        $categorySort = count($gameTypes);
+        foreach ($gameTypes as $gameType => $name) {
+            $this->ensureCatalogRow(
+                'category',
+                '',
+                $gameType,
+                $name,
+                $categoryLogos[$gameType] ?? '',
+                1,
+                0,
+                $categorySort--,
+                $now
+            );
+        }
+
+        $existingNames = $this->existingPlatformNames();
+        $allPlatformNames = $platforms;
+        $configuredPlatformTypes = array_fill_keys(array_keys($platforms), true);
+        foreach (array_keys($lobbyTypes) as $platType) {
+            $this->addPlatformName($allPlatformNames, $platType, $existingNames);
+        }
+        foreach ($visibleTypes as $platTypes) {
+            foreach ($platTypes as $platType) {
+                $this->addPlatformName($allPlatformNames, $platType, $existingNames);
+            }
+        }
+        foreach ($existingNames as $platType => $name) {
+            $this->addPlatformName($allPlatformNames, $platType, $existingNames);
+        }
+
+        $platformSort = count($platforms);
+        foreach ($allPlatformNames as $platType => $name) {
+            $this->ensureCatalogRow(
+                'platform',
+                $platType,
+                0,
+                $name,
+                '',
+                isset($configuredPlatformTypes[$platType]) ? 1 : 0,
+                0,
+                isset($platforms[$platType]) ? $platformSort-- : 0,
+                $now
+            );
+        }
+
+        $visibleRelations = $this->relationSet($visibleTypes);
+        $lobbyRelations = $this->relationSet($lobbyTypes);
+
+        // Existing relation switches are only tightened here. A visible relation
+        // that an administrator already closed must never be reopened.
+        $existingRelations = DB::table('egame_items')
+            ->where('item_type', 'platform')
+            ->where('game_type', '>', 0)
+            ->get(['id', 'plat_type', 'game_type', 'status', 'lobby_enabled']);
+        foreach ($existingRelations as $relation) {
+            $key = $this->relationKey((string)$relation->plat_type, (int)$relation->game_type);
+            $updates = [];
+            if (!isset($visibleRelations[$key]) && (int)$relation->status !== 0) {
+                $updates['status'] = 0;
+            }
+            if (isset($lobbyRelations[$key]) && (int)$relation->lobby_enabled !== 1) {
+                $updates['lobby_enabled'] = 1;
+            }
+            if ($updates !== []) {
+                $updates['updated_at'] = $now;
+                DB::table('egame_items')->where('id', $relation->id)->update($updates);
+            }
+        }
+
+        $relationPairs = $visibleRelations + $lobbyRelations;
+        $existingGamePairs = DB::table('egame_items')
+            ->where('item_type', 'game')
+            ->where('plat_type', '<>', '')
+            ->where('game_type', '>', 0)
+            ->distinct()
+            ->get(['plat_type', 'game_type']);
+        foreach ($existingGamePairs as $pair) {
+            $key = $this->relationKey((string)$pair->plat_type, (int)$pair->game_type);
+            $relationPairs[$key] = [
+                'plat_type' => strtolower(trim((string)$pair->plat_type)),
+                'game_type' => (int)$pair->game_type,
+            ];
+        }
+
+        foreach ($relationPairs as $key => $relation) {
+            $platType = $relation['plat_type'];
+            $gameType = $relation['game_type'];
+            $isVisible = isset($visibleRelations[$key]);
+            $isLobby = isset($lobbyRelations[$key]);
+            $name = $allPlatformNames[$platType]
+                ?? $existingNames[$platType]
+                ?? strtoupper($platType);
+
+            $this->ensureCatalogRow(
+                'platform',
+                $platType,
+                $gameType,
+                $name,
+                '',
+                $isVisible ? 1 : 0,
+                $isLobby ? 1 : 0,
+                0,
+                $now
+            );
+
+            $query = $this->catalogRowQuery('platform', $platType, $gameType);
+            $current = $query->first(['status', 'lobby_enabled']);
+            $updates = [];
+            if (!$isVisible && $current && (int)$current->status !== 0) {
+                $updates['status'] = 0;
+            }
+            if ($isLobby && $current && (int)$current->lobby_enabled !== 1) {
+                $updates['lobby_enabled'] = 1;
+            }
+            if ($updates !== []) {
+                $updates['updated_at'] = $now;
+                $query->update($updates);
+            }
+        }
+    }
+
+    public function down(): void
+    {
+        // This is an irreversible data migration. Keep both the seeded catalog
+        // rows and lobby_enabled state; dropping only the column would leave the
+        // catalog marker rows behind and create an unsafe half-rollback state.
+    }
+
+    private function ensureCatalogRow(
+        string $itemType,
+        string $platType,
+        int $gameType,
+        string $name,
+        string $logo,
+        int $status,
+        int $lobbyEnabled,
+        int $sort,
+        string $now
+    ): bool {
+        $query = $this->catalogRowQuery($itemType, $platType, $gameType);
+        $row = $query->first(['id', 'name', 'logo']);
+        if ($row) {
+            $updates = [];
+            if (trim((string)$row->name) === '' && $name !== '') {
+                $updates['name'] = $name;
+            }
+            if (trim((string)$row->logo) === '' && $logo !== '') {
+                $updates['logo'] = $logo;
+            }
+            if ($updates !== []) {
+                $updates['updated_at'] = $now;
+                DB::table('egame_items')->where('id', $row->id)->update($updates);
+            }
+            return false;
+        }
+
+        DB::table('egame_items')->insertOrIgnore([
+            'item_type' => $itemType,
+            'plat_type' => $platType,
+            'game_type' => $gameType,
+            'game_code' => '',
+            'name' => $name,
+            'ingress' => '3',
+            'logo' => $logo,
+            'logo_langs' => null,
+            'lobby_enabled' => $lobbyEnabled,
+            'status' => $status,
+            'sort' => $sort,
+            'created_at' => $now,
+            'updated_at' => $now,
+        ]);
+
+        return true;
+    }
+
+    private function catalogRowQuery(string $itemType, string $platType, int $gameType)
+    {
+        return DB::table('egame_items')
+            ->where('item_type', $itemType)
+            ->where('plat_type', strtolower(trim($platType)))
+            ->where('game_type', $gameType)
+            ->where('game_code', '');
+    }
+
+    private function existingPlatformNames(): array
+    {
+        $names = [];
+        $rows = DB::table('egame_items')
+            ->where('plat_type', '<>', '')
+            ->orderBy('id')
+            ->get(['plat_type', 'name']);
+        foreach ($rows as $row) {
+            $platType = strtolower(trim((string)$row->plat_type));
+            if ($platType === '') {
+                continue;
+            }
+            $name = trim((string)$row->name);
+            if (!isset($names[$platType]) || ($names[$platType] === '' && $name !== '')) {
+                $names[$platType] = $name !== '' ? $name : strtoupper($platType);
+            }
+        }
+
+        return $names;
+    }
+
+    private function addPlatformName(array &$platforms, string $platType, array $existingNames): void
+    {
+        $platType = strtolower(trim($platType));
+        if ($platType === '' || isset($platforms[$platType])) {
+            return;
+        }
+        $platforms[$platType] = $existingNames[$platType] ?? strtoupper($platType);
+    }
+
+    private function relationSet(array $typesByPlatformOrType): array
+    {
+        $relations = [];
+        $isPlatformMap = $typesByPlatformOrType !== []
+            && is_string(array_key_first($typesByPlatformOrType));
+
+        if ($isPlatformMap) {
+            foreach ($typesByPlatformOrType as $platType => $gameTypes) {
+                foreach ($gameTypes as $gameType) {
+                    $this->addRelation($relations, (string)$platType, (int)$gameType);
+                }
+            }
+            return $relations;
+        }
+
+        foreach ($typesByPlatformOrType as $gameType => $platTypes) {
+            foreach ($platTypes as $platType) {
+                $this->addRelation($relations, (string)$platType, (int)$gameType);
+            }
+        }
+
+        return $relations;
+    }
+
+    private function addRelation(array &$relations, string $platType, int $gameType): void
+    {
+        $platType = strtolower(trim($platType));
+        if ($platType === '' || $gameType <= 0) {
+            return;
+        }
+        $relations[$this->relationKey($platType, $gameType)] = [
+            'plat_type' => $platType,
+            'game_type' => $gameType,
+        ];
+    }
+
+    private function relationKey(string $platType, int $gameType): string
+    {
+        return strtolower(trim($platType)) . '|' . $gameType;
+    }
+
+    private function gameTypes(): array
+    {
+        return [
+            1 => '视讯',
+            2 => '电子',
+            3 => '彩票',
+            4 => '体育',
+            5 => '电竞',
+            6 => '捕鱼',
+            7 => '棋牌',
+        ];
+    }
+
+    private function categoryLogos(): array
+    {
+        return [
+            1 => '/static/img/game-type/sx.png',
+            2 => '/static/img/game-type/dz.png',
+            3 => '/static/img/game-type/cp1.png',
+            4 => '/static/img/game-type/ty.png',
+            5 => '/static/img/game-type/dj.png',
+            6 => '/static/img/game-type/by.png',
+            7 => '/static/img/game-type/qp.png',
+        ];
+    }
+
+    private function platforms(): array
+    {
+        return [
+            'ag' => 'Choice',
+            'allbet' => '欧博',
+            'ap' => '平博',
+            'bbin' => '宝盈',
+            'bg' => 'BG',
+            'boya' => '博雅',
+            'cmd' => 'CMD',
+            'cq9' => 'CQ9',
+            'crown' => '皇冠',
+            'db1' => 'DB视讯',
+            'db2' => 'DB2',
+            'db3' => 'DB彩票',
+            'db5' => 'DB电竞',
+            'db6' => 'DB6',
+            'db7' => 'DB7',
+            'dg' => 'DG',
+            'esb' => '电竞牛',
+            'evo' => 'EVO',
+            'fb' => 'FB',
+            'fc' => 'FC',
+            'fg' => 'FG',
+            'im' => 'IM体育',
+            'jdb' => '夺宝',
+            'joker' => 'Joker',
+            'km' => 'KM',
+            'ky' => '开元棋牌',
+            'leg' => 'LEG',
+            'lgd' => 'LGD',
+            'mg' => 'MG',
+            'mt' => 'MT',
+            'mw' => 'MW',
+            'newbb' => 'NewBB',
+            'nw' => 'NW',
+            'og' => 'OG',
+            'panda' => '熊猫体育',
+            'pg' => 'PG',
+            'pgs' => 'PGS',
+            'png' => 'PNG',
+            'pp' => '王者',
+            'pt' => 'PT',
+            'rsg' => 'RSG',
+            'sa' => '沙龙',
+            'saba' => '沙巴体育',
+            'sexy' => 'Sexy',
+            'sg' => 'SG',
+            'sgwin' => '双赢彩票',
+            'ss' => '三昇',
+            't1' => 'T1',
+            'tcg' => '天成彩票',
+            'tf' => '雷火电竞',
+            'v8' => 'V8棋牌',
+            'vg' => 'VG',
+            'vr' => 'VR彩票',
+            'we' => 'WE',
+            'wl' => 'WL',
+            'wm' => '完美',
+            'ww' => 'WW',
+            'xgd' => 'XGD',
+            'xj' => '小金体育',
+            'yoo' => 'YOO',
+        ];
+    }
+
+    private function platformGameTypes(): array
+    {
+        return [
+            'allbet' => [1],
+            'ap' => [4],
+            'bg' => [1, 6, 7],
+            'cmd' => [4],
+            'crown' => [4],
+            'db1' => [1],
+            'db3' => [3],
+            'db5' => [5],
+            'dg' => [1],
+            'esb' => [5],
+            'evo' => [1],
+            'fb' => [4],
+            'im' => [4],
+            'newbb' => [4],
+            'og' => [1],
+            'ag' => [1, 2, 6],
+            'panda' => [4],
+            'png' => [2],
+            'sa' => [1],
+            'saba' => [4],
+            'sexy' => [1],
+            'sg' => [2],
+            'sgwin' => [3],
+            'ss' => [4],
+            'tcg' => [3],
+            'tf' => [5],
+            'v8' => [7],
+            'vr' => [3],
+            'we' => [1],
+            'wm' => [1],
+            'xj' => [4],
+        ];
+    }
+
+    private function platformVisibleTypes(): array
+    {
+        return [
+            1 => ['db1', 'og', 'bbin', 'bg', 'ag'],
+            2 => ['ag', 'as', 'bbin', 'db2', 'fg', 'jdb', 'pg'],
+            4 => ['crown', 'im', 'panda', 'saba'],
+            6 => ['db6', 'fg', 'jdb', 'cq9', 'ag'],
+            7 => ['db7', 'jdb', 'ky', 'bbin'],
+        ];
+    }
+};

+ 110 - 0
database/migrations/2026_07_21_121000_add_egame_admin_menus.php

@@ -0,0 +1,110 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+    private const MENU_URI = 'egame';
+
+    private const MENU_PERMISSION_NAME = 'egame.catalog.menu';
+
+    private const BUTTONS = [
+        'admin/egame/options' => '筛选选项',
+        'admin/egame/items' => '配置列表',
+        'admin/egame/update' => '保存配置',
+        'admin/egame/setStatus' => '切换状态',
+        'admin/egame/delete' => '删除配置',
+    ];
+
+    private const BUTTON_PERMISSION_NAMES = [
+        'admin/egame/options' => 'egame.catalog.options',
+        'admin/egame/items' => 'egame.catalog.items',
+        'admin/egame/update' => 'egame.catalog.update',
+        'admin/egame/setStatus' => 'egame.catalog.status',
+        'admin/egame/delete' => 'egame.catalog.delete',
+    ];
+
+    public function up(): void
+    {
+        if (!Schema::hasTable('menus')) {
+            return;
+        }
+
+        $now = date('Y-m-d H:i:s');
+        $menu = DB::table('menus')
+            ->where('uri', self::MENU_URI)
+            ->where('type', 1)
+            ->first(['id', 'status']);
+
+        if (!$menu) {
+            $menuId = DB::table('menus')->insertGetId([
+                'parent_id' => 0,
+                'title' => '第三方游戏',
+                'icon' => null,
+                'uri' => self::MENU_URI,
+                'permission_name' => self::MENU_PERMISSION_NAME,
+                'sort' => 0,
+                'status' => 1,
+                'type' => 1,
+                'created_at' => $now,
+                'updated_at' => $now,
+            ]);
+        } else {
+            $menuId = (int)$menu->id;
+            if ((int)$menu->status !== 1) {
+                DB::table('menus')->where('id', $menuId)->update([
+                    'status' => 1,
+                    'updated_at' => $now,
+                ]);
+            }
+        }
+
+        $sort = 0;
+        foreach (self::BUTTONS as $uri => $title) {
+            $button = DB::table('menus')
+                ->where('uri', $uri)
+                ->first(['id', 'parent_id', 'title', 'status', 'type']);
+            if ($button) {
+                $updates = [];
+                if ((int)$button->parent_id !== $menuId) {
+                    $updates['parent_id'] = $menuId;
+                }
+                if ((int)$button->type !== 2) {
+                    $updates['type'] = 2;
+                }
+                if (trim((string)$button->title) === '') {
+                    $updates['title'] = $title;
+                }
+                if ((int)$button->status !== 1) {
+                    $updates['status'] = 1;
+                }
+                if ($updates !== []) {
+                    $updates['updated_at'] = $now;
+                    DB::table('menus')->where('id', $button->id)->update($updates);
+                }
+            } else {
+                DB::table('menus')->insert([
+                    'parent_id' => $menuId,
+                    'title' => $title,
+                    'icon' => null,
+                    'uri' => $uri,
+                    'permission_name' => self::BUTTON_PERMISSION_NAMES[$uri],
+                    'sort' => $sort,
+                    'status' => 1,
+                    'type' => 2,
+                    'created_at' => $now,
+                    'updated_at' => $now,
+                ]);
+            }
+            $sort++;
+        }
+    }
+
+    public function down(): void
+    {
+        // This is an irreversible menu and permission data migration. Existing
+        // rows may have been reused, reparented, and re-enabled, so their prior
+        // parent and status state cannot be reconstructed safely on rollback.
+    }
+};

+ 1 - 0
routes/admin.php

@@ -341,6 +341,7 @@ Route::middleware(['admin.jwt'])->group(function () {
 
 
         Route::prefix('/egame')->group(function () {
         Route::prefix('/egame')->group(function () {
             Route::get('/items', [Egame::class, 'items']);
             Route::get('/items', [Egame::class, 'items']);
+            Route::get('/options', [Egame::class, 'options']);
             Route::post('/update', [Egame::class, 'update']);
             Route::post('/update', [Egame::class, 'update']);
             Route::post('/setStatus', [Egame::class, 'setStatus']);
             Route::post('/setStatus', [Egame::class, 'setStatus']);
             Route::post('/delete', [Egame::class, 'delete']);
             Route::post('/delete', [Egame::class, 'delete']);