'视讯', 2 => '电子', 3 => '彩票', 4 => '体育', 5 => '电竞', 6 => '捕鱼', 7 => '棋牌', ]; public function items(): JsonResponse { try { request()->validate([ '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'], 'name' => ['nullable', 'string', 'max:128'], 'keyword' => ['nullable', 'string', 'max:128'], 'ingress' => ['nullable', Rule::in(EgameItem::ingressValues())], 'network' => ['nullable', Rule::in(EgameItem::networkValues())], 'status' => ['nullable', 'integer', Rule::in([ EgameItem::STATUS_DISABLED, EgameItem::STATUS_ENABLED, ])], 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1', 'max:200'], ]); $query = EgameItem::query(); foreach (['item_type', 'plat_type', 'game_type', 'game_code', 'status', 'ingress'] as $field) { $value = request()->input($field); if ($value !== null && $value !== '') { $query->where($field, $value); } } $network = trim((string) request()->input('network', '')); if ($network !== '') { $this->applyNetworkFilter($query, $network); } $name = trim((string) request()->input('name', '')); if ($name !== '') { $query->where('name', 'like', "%{$name}%"); } $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 !== '') { $query->where(function ($query) use ($keyword) { $query->where('name', 'like', "%{$keyword}%") ->orWhere('plat_type', 'like', "%{$keyword}%") ->orWhere('game_code', 'like', "%{$keyword}%"); }); } $limit = (int) request()->input('limit', 15); $page = (int) request()->input('page', 1); $total = (clone $query)->count(); $states = $this->catalogStates(); $gameCountsByPlat = $this->gameCountsByPlat(); $list = $query->orderByDesc('sort') ->orderByDesc('id') ->forPage($page, $limit) ->get() ->map(fn (EgameItem $item) => $this->decorateItem($item, $states, $gameCountsByPlat)) ->values(); } catch (ValidationException $e) { return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } 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->gameCountsByPlat(); $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, $platformGameCounts); $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' => EgameItem::optionList(EgameItem::STATUS_LABELS), 'ingress' => EgameItem::optionList(EgameItem::INGRESS_LABELS), 'networks' => array_merge( [['value' => '', 'label' => '不限制']], EgameItem::optionList(EgameItem::NETWORK_LABELS) ), ]); } public function update(): JsonResponse { try { request()->validate([ 'id' => ['nullable', 'integer', 'min:0'], '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'], 'name' => ['nullable', 'string', 'max:128'], 'description' => ['nullable', 'string', 'max:255'], 'ingress' => ['nullable', Rule::in(EgameItem::ingressValues())], 'network' => ['nullable', Rule::in(array_merge([''], EgameItem::networkValues()))], 'lobby_enabled' => ['nullable', 'boolean'], 'logo' => ['nullable', 'string', 'max:500'], 'logo_pc' => ['nullable', 'string', 'max:500'], 'logo_h5' => ['nullable', 'string', 'max:500'], 'logo_langs' => ['nullable', 'array'], 'status' => ['nullable', 'integer', Rule::in([ EgameItem::STATUS_DISABLED, EgameItem::STATUS_ENABLED, ])], 'sort' => ['nullable', 'integer', 'min:-999999', 'max:999999'], 'apply_to_games' => ['nullable', 'boolean'], ]); $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); } $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); } if (!empty($data) || !$item->exists) { $item->fill($data)->save(); } $appliedGameCount = 0; if (array_key_exists('network', $data) && $item->isPlatformRelation()) { $appliedGameCount = $this->applyNetworkToGames( $item, (string) $data['network'], request()->boolean('apply_to_games', true) ); } $item = $item->fresh(); $item->setAttribute('applied_game_count', $appliedGameCount); return $item; }); } catch (ValidationException $e) { return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error((int) $e->getCode(), $e->getMessage()); } return $this->success($this->decorateItem($item, $this->catalogStates(), $this->gameCountsByPlat())); } public function setStatus(): JsonResponse { try { request()->validate([ 'id' => ['required', 'integer', 'min:1'], 'status' => ['required', 'integer', Rule::in([ EgameItem::STATUS_DISABLED, EgameItem::STATUS_ENABLED, ])], ]); $item = EgameItem::query()->find((int) request()->input('id')); if (!$item) { throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR); } $item->status = (int) request()->input('status'); $item->save(); $item = $item->fresh(); } catch (ValidationException $e) { return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error((int) $e->getCode(), $e->getMessage()); } return $this->success($this->decorateItem($item, $this->catalogStates(), $this->gameCountsByPlat())); } public function delete(): JsonResponse { try { request()->validate([ 'id' => ['required', 'integer', 'min:1'], ]); if (!EgameItem::query()->whereKey((int) request()->input('id'))->exists()) { throw new Exception('配置不存在', HttpStatus::CUSTOM_ERROR); } } catch (ValidationException $e) { return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first()); } catch (Exception $e) { 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); } } } private function mutableItemData(EgameItem $item, bool $isNew): array { $input = request()->all(); $data = $isNew ? [ 'name' => '', 'description' => '', 'ingress' => EgameItem::INGRESS_BOTH, 'network' => '', 'lobby_enabled' => 0, 'logo' => '', 'logo_h5' => '', 'logo_langs' => null, 'status' => EgameItem::STATUS_ENABLED, 'sort' => 0, ] : []; foreach (['name', 'description', 'ingress', 'network'] as $field) { if (array_key_exists($field, $input)) { $data[$field] = trim((string) $input[$field]); } } if (array_key_exists('logo_h5', $input)) { $data['logo_h5'] = $this->logoPathForStorage((string) $input['logo_h5']); } // logo_pc 是新版后台使用的 PC Logo 字段,数据库仍统一保存到 logo。 // 两个字段同时出现时以 logo_pc 为准,避免列表整行回传的旧 logo // 覆盖用户刚上传的新 logo_pc;同时修复新增记录只传 logo_pc 无法保存。 if (array_key_exists('logo', $input)) { $data['logo'] = $this->logoPathForStorage((string) $input['logo']); } if (array_key_exists('logo_pc', $input)) { $data['logo'] = $this->logoPathForStorage((string) $input['logo_pc']); } if (array_key_exists('logo_langs', $input)) { $data['logo_langs'] = $item->item_type === EgameItem::TYPE_GAME ? $this->logoLangsForStorage($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->isCategory() && array_key_exists('ingress', $data)) { $data['ingress'] = EgameItem::INGRESS_BOTH; } if ($item->isCategory() && array_key_exists('network', $data)) { $data['network'] = ''; } 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, array $gameCountsByPlat = []): 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); $item->setAttribute('description', (string) ($item->description ?? '')); $logo = $this->absoluteLogoUrl((string) ($item->logo ?? '')); $item->setAttribute('logo', $logo); $item->setAttribute('logo_pc', $logo); $item->setAttribute('logo_h5', $this->absoluteLogoUrl((string) ($item->logo_h5 ?? ''))); if (is_array($item->logo_langs)) { $item->setAttribute('logo_langs', array_map( fn ($path): string => $this->absoluteLogoUrl((string) $path), $item->logo_langs )); } $this->decorateNetwork($item, $globalPlatform, $platformRelation); $item->setAttribute('ingress_name', EgameItem::ingressName($item->ingress ?? '')); $item->setAttribute('status_name', EgameItem::STATUS_LABELS[(int) $item->status] ?? ''); $gameCount = $gameCountsByPlat[$platType] ?? 0; $item->setAttribute( 'has_game_manage', $item->item_type === EgameItem::TYPE_PLATFORM && $gameCount > 0 ); if ($item->item_type === EgameItem::TYPE_PLATFORM) { $item->setAttribute('game_count', $gameCount); } return $item; } private function absoluteLogoUrl(string $path): string { $path = trim($path); if ($path === '' || preg_match('/^(?:https?:)?\/\//i', $path) || preg_match('/^(?:data|blob):/i', $path)) { return $path; } $relativePath = '/' . ltrim($path, '/'); $baseUrl = $this->normalizedLogoBaseUrl((string) config('app.url', '')); if ($baseUrl === '') { $baseUrl = $this->normalizedLogoBaseUrl(request()->getSchemeAndHttpHost()); } return $baseUrl . $relativePath; } private function logoPathForStorage(string $path): string { $path = trim($path); if ($path === '' || !preg_match('/^https?:\/\//i', $path)) { return $path; } $baseUrls = array_filter(array_unique([ $this->normalizedLogoBaseUrl((string) config('app.url', '')), $this->normalizedLogoBaseUrl(request()->getSchemeAndHttpHost()), ])); foreach ($baseUrls as $baseUrl) { if (str_starts_with($path, $baseUrl . '/')) { return '/' . ltrim(substr($path, strlen($baseUrl)), '/'); } } return $path; } private function normalizedLogoBaseUrl(string $baseUrl): string { $baseUrl = rtrim(trim($baseUrl), '/'); if ($baseUrl !== '' && !preg_match('/^https?:\/\//i', $baseUrl)) { $baseUrl = 'https://' . $baseUrl; } return $baseUrl; } private function logoLangsForStorage($logos): array { if (!is_array($logos)) { return []; } return array_map( fn ($path): string => $this->logoPathForStorage((string) $path), $logos ); } private function gameCountsByPlat(): array { $counts = []; foreach ($this->groupedCounts( EgameItem::query()->where('item_type', EgameItem::TYPE_GAME), 'plat_type', 'id' ) as $platType => $count) { $counts[strtolower((string) $platType)] = (int) $count; } return $counts; } private function decorateNetwork( EgameItem $item, ?EgameItem $globalPlatform, ?EgameItem $platformRelation ): void { $own = trim((string) ($item->network ?? '')); $inherited = ''; if ($own === '') { if ($item->item_type === EgameItem::TYPE_GAME) { $parent = trim((string) ($platformRelation->network ?? '')); if ($parent === '') { $parent = trim((string) ($globalPlatform->network ?? '')); } $inherited = $parent; } elseif ($item->isPlatformRelation()) { $inherited = trim((string) ($globalPlatform->network ?? '')); } } $effective = $own !== '' ? $own : $inherited; $item->setAttribute('network', $own); $item->setAttribute('network_effective', $effective); $item->setAttribute('network_inherited', $own === '' && $effective !== ''); $item->setAttribute('network_name', EgameItem::networkName($effective)); } private function applyNetworkToGames(EgameItem $item, string $network, bool $apply): int { if (!$apply) { return 0; } return EgameItem::query() ->where('item_type', EgameItem::TYPE_GAME) ->where('plat_type', strtolower((string) $item->plat_type)) ->where('game_type', (int) $item->game_type) ->update([ 'network' => $network, 'updated_at' => now(), ]); } private function applyNetworkFilter($query, string $network): void { $itemType = (string) request()->input('item_type', ''); $platType = strtolower(trim((string) request()->input('plat_type', ''))); $gameType = request()->input('game_type'); if ($itemType === EgameItem::TYPE_GAME && $platType !== '') { $parentNetwork = $this->parentNetwork($platType, $gameType); if ($parentNetwork === $network) { $query->where(function ($query) use ($network) { $query->where('network', $network) ->orWhere('network', '') ->orWhereNull('network'); }); return; } } $query->where('network', $network); } private function parentNetwork(string $platType, $gameType): string { if ($gameType !== null && $gameType !== '') { $relation = trim((string) EgameItem::query() ->where('item_type', EgameItem::TYPE_PLATFORM) ->where('plat_type', $platType) ->where('game_type', (int) $gameType) ->where('game_code', '') ->value('network')); if ($relation !== '') { return $relation; } } return trim((string) EgameItem::query() ->where('item_type', EgameItem::TYPE_PLATFORM) ->where('plat_type', $platType) ->where('game_type', 0) ->where('game_code', '') ->value('network')); } 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(); } }