diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 85c7f17..b279e60 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -25,6 +25,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: 登录到 GitHub Container Registry + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -48,10 +49,10 @@ jobs: with: context: . file: ./Dockerfile - push: true + # Pull request 使用只读令牌,只构建不推送;主分支推送时才发布镜像。 + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - outputs: type=registry - name: 生成中文构建摘要 run: | diff --git a/.github/workflows/php-release.yml b/.github/workflows/php-release.yml index 1786437..dd0c86e 100644 --- a/.github/workflows/php-release.yml +++ b/.github/workflows/php-release.yml @@ -34,12 +34,18 @@ jobs: extensions: mbstring, curl, json, openssl, fileinfo tools: composer + # 提前获取版本号,确保后续构建产物名称始终完整 + - name: 获取版本号 + run: | + echo "VERSION=$(php -r 'echo json_decode(file_get_contents("composer.json"), true)["version"];')" >> "$GITHUB_ENV" + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> "$GITHUB_ENV" + # 缓存 Composer 依赖 - name: 缓存 Composer 依赖 uses: actions/cache@v4 with: path: vendor - key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock', 'composer.json') }} restore-keys: ${{ runner.os }}-composer- cache-on-failure: true # 即使步骤失败也缓存 @@ -47,14 +53,6 @@ jobs: - name: 安装依赖 run: composer install --prefer-dist --no-progress --no-dev --optimize-autoloader - # 获取版本号 - - name: 获取版本号 - run: | - sudo apt-get update - sudo apt-get install -y jq - echo "VERSION=$(jq -r '.version' composer.json)" >> $GITHUB_ENV - echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV - # 创建构建产物 - name: 创建构建产物 run: | @@ -63,7 +61,6 @@ jobs: # 列出打包文件 - name: 列出打包文件 run: unzip -l release-${{ env.VERSION }}-${{ github.run_id }}.zip - if: always() # 即使失败也运行,便于调试 # 上传构建产物 - name: 上传构建产物 @@ -74,6 +71,7 @@ jobs: # 创建 GitHub 发布 - name: 创建 GitHub 发布 + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' uses: softprops/action-gh-release@v2 with: tag_name: v${{ env.VERSION }} diff --git a/Dockerfile b/Dockerfile index 1ea0422..e436c07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,25 @@ RUN apt-get update && apt-get install -y \ libjpeg-dev \ libfreetype6-dev \ libzip-dev \ + libcurl4-openssl-dev \ + libsqlite3-dev \ + curl \ unzip \ git \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install gd zip pdo pdo_mysql + && docker-php-ext-install curl gd zip pdo pdo_mysql pdo_sqlite opcache \ + && rm -rf /var/lib/apt/lists/* + +# 启用 OPcache;当前容器通过 CLI SAPI 运行 ThinkPHP 内置服务器,因此同时开启 enable_cli +RUN { \ + echo 'opcache.enable=1'; \ + echo 'opcache.enable_cli=1'; \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=16'; \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.validate_timestamps=0'; \ + echo 'opcache.save_comments=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini # 安装 Composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer diff --git a/app/controller/Game.php b/app/controller/Game.php index 30db05f..7ea21af 100644 --- a/app/controller/Game.php +++ b/app/controller/Game.php @@ -5,6 +5,8 @@ use app\utils\Response; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; +use GuzzleHttp\Pool; +use think\facade\Cache; use think\facade\Request; use think\response\Json; @@ -12,6 +14,8 @@ class Game { private const IDE_URL = 'https://comm.ams.game.qq.com/ide/'; private const ROLE_INFO_URL = 'https://comm.aci.game.qq.com/main'; + private const PUBLIC_DATA_CACHE_TTL = 300; + private const BATCH_CONCURRENCY = 5; protected Client $client; @@ -20,6 +24,8 @@ public function __construct() $this->client = new Client([ 'verify' => false, 'version' => 1.1, + 'connect_timeout' => 3.0, + 'timeout' => 12.0, ]); } @@ -36,22 +42,30 @@ public function record(): Json ]; $types = [4 => 'gun', 5 => 'operator']; + $requests = []; foreach ($types as $type => $key) { - for ($i = 1; $i <= 5; $i++) { // 长时间未游戏可能需要翻页获取 - $data = $this->requestIde([ + for ($page = 1; $page <= 5; $page++) { // 长时间未游戏可能需要翻页获取 + $requests[$key . ':' . $page] = [ 'iChartId' => 319386, 'iSubChartId' => 319386, 'sIdeToken' => 'zMemOt', 'type' => $type, - 'page' => $i, - ], $cookie); + 'page' => $page, + ]; + } + } + + $responses = $this->requestIdeBatch($requests, $cookie); + foreach (array_values($types) as $key) { + for ($page = 1; $page <= 5; $page++) { + $data = $responses[$key . ':' . $page]; + if ($data['ret'] !== 0) { + return Response::json(-1, '获取失败'); + } if ($data['ret'] === 0 && !empty($data['jData']['data'])) { $gameData[$key] = array_merge($gameData[$key], $data['jData']['data']); } } - if ($data['ret'] != 0) { - return Response::json(-1, '获取失败'); - } } return Response::json(0, '获取成功', $gameData); @@ -73,17 +87,13 @@ public function player(): Json 'money' => 0, ]; - $data = $this->requestIde([ - 'iChartId' => 317814, - 'iSubChartId' => 317814, - 'sIdeToken' => 'QIRBwm', - ], $cookie); - if ($data['ret'] === 0) { - $gameData['player'] = array_merge($data['jData']['userData'], [ - 'charac_name' => urldecode($data['jData']['userData']['charac_name']), - ]); - $gameData['game'] = $data['jData']['careerData']; - } + $requests = [ + 'profile' => [ + 'iChartId' => 317814, + 'iSubChartId' => 317814, + 'sIdeToken' => 'QIRBwm', + ], + ]; $currencyItems = [ 'coin' => 17888808888, @@ -91,13 +101,26 @@ public function player(): Json 'money' => 17020000010, ]; foreach ($currencyItems as $key => $itemId) { - $data = $this->requestIde([ + $requests[$key] = [ 'iChartId' => 319386, 'iSubChartId' => 319386, 'sIdeToken' => 'zMemOt', 'type' => 3, 'item' => $itemId, - ], $cookie); + ]; + } + + $responses = $this->requestIdeBatch($requests, $cookie); + $data = $responses['profile']; + if ($data['ret'] === 0) { + $gameData['player'] = array_merge($data['jData']['userData'], [ + 'charac_name' => urldecode($data['jData']['userData']['charac_name']), + ]); + $gameData['game'] = $data['jData']['careerData']; + } + + foreach (array_keys($currencyItems) as $key) { + $data = $responses[$key]; if ($data['ret'] === 0) { $gameData[$key] = (int) ($data['jData']['data'][0]['totalMoney'] ?? 0); } @@ -108,37 +131,46 @@ public function player(): Json public function config(): Json { - $data = $this->requestIde([ - 'iChartId' => 352143, - 'iSubChartId' => 352143, - 'sIdeToken' => 'YWRywA', - 'source' => 5, - 'method' => 'dfm/config.list', - 'param' => json_encode(['configType' => 'all']), - ]); - $gameData = $data['ret'] === 0 ? $data['jData']['data']['data']['config'] : []; + $gameData = $this->remember('game:config:v1', self::PUBLIC_DATA_CACHE_TTL, function (): ?array { + $data = $this->requestIde([ + 'iChartId' => 352143, + 'iSubChartId' => 352143, + 'sIdeToken' => 'YWRywA', + 'source' => 5, + 'method' => 'dfm/config.list', + 'param' => json_encode(['configType' => 'all']), + ]); - return Response::json(0, '获取成功', $gameData); + return $data['ret'] === 0 ? $data['jData']['data']['data']['config'] : null; + }); + + return Response::json(0, '获取成功', $gameData ?? []); } public function items(): Json { $params = Request::only(['type', 'sub_type', 'item_id']); - $data = $this->requestIde([ - 'iChartId' => 352143, - 'iSubChartId' => 352143, - 'sIdeToken' => 'YWRywA', - 'source' => 2, - 'method' => 'dfm/object.list', - 'param' => json_encode([ - 'primary' => $params['type'] ?? '', - 'second' => $params['sub_type'] ?? '', - 'objectID' => $params['item_id'] ?? '', - ]), - ]); - return $data['ret'] !== 0 + $cacheKey = 'game:items:' . sha1(json_encode($params)); + $gameData = $this->remember($cacheKey, self::PUBLIC_DATA_CACHE_TTL, function () use ($params): ?array { + $data = $this->requestIde([ + 'iChartId' => 352143, + 'iSubChartId' => 352143, + 'sIdeToken' => 'YWRywA', + 'source' => 2, + 'method' => 'dfm/object.list', + 'param' => json_encode([ + 'primary' => $params['type'] ?? '', + 'second' => $params['sub_type'] ?? '', + 'objectID' => $params['item_id'] ?? '', + ]), + ]); + + return $data['ret'] === 0 ? ($data['jData']['data']['data']['list'] ?? []) : null; + }); + + return $gameData === null ? Response::json(-1, '获取失败') - : Response::json(0, '获取成功', $data['jData']['data']['data']['list'] ?? []); + : Response::json(0, '获取成功', $gameData); } public function price(): Json @@ -164,16 +196,20 @@ public function price(): Json $gameData = $data['jData']['data']['data']['dataMap']; if (($params['recent'] ?? 0) == 1) { - foreach ($gameData as $key => &$item) { - $recentData = $this->requestIde([ + $requests = []; + foreach (array_keys($gameData) as $key) { + $requests[$key] = [ 'iChartId' => 352143, 'iSubChartId' => 352143, 'sIdeToken' => 'YWRywA', 'source' => 2, 'method' => 'dfm/object.price.recent', 'param' => json_encode(['objectID' => $key]), - ], $cookie); - $item['recent'] = $recentData['jData']['data']['data']['objectPriceRecent']['list'] ?? []; + ]; + } + $responses = $this->requestIdeBatch($requests, $cookie); + foreach ($gameData as $key => &$item) { + $item['recent'] = $responses[$key]['jData']['data']['data']['objectPriceRecent']['list'] ?? []; } unset($item); } @@ -566,6 +602,70 @@ private function requestIde(array $formParams, ?CookieJar $cookie = null): array return json_decode($response->getBody()->getContents(), true); } + /** + * 并行执行多个相互独立的 IDE 请求,降低接口聚合时的总等待时间。 + * + * @param array $requests + * @return array + */ + private function requestIdeBatch(array $requests, ?CookieJar $cookie = null): array + { + if ($requests === []) { + return []; + } + + $results = []; + $errors = []; + $requestGenerator = function () use ($requests, $cookie) { + foreach ($requests as $key => $formParams) { + yield $key => function () use ($formParams, $cookie) { + $options = ['form_params' => $formParams]; + if ($cookie !== null) { + $options['cookies'] = $cookie; + } + + return $this->client->requestAsync('POST', self::IDE_URL, $options); + }; + } + }; + + $pool = new Pool($this->client, $requestGenerator(), [ + 'concurrency' => self::BATCH_CONCURRENCY, + 'fulfilled' => function ($response, $key) use (&$results): void { + $results[$key] = json_decode($response->getBody()->getContents(), true); + }, + 'rejected' => function ($reason, $key) use (&$errors): void { + $errors[$key] = $reason; + }, + ]); + $pool->promise()->wait(); + + if ($errors !== []) { + $reason = reset($errors); + if ($reason instanceof \Throwable) { + throw $reason; + } + throw new \RuntimeException('批量请求失败'); + } + + return $results; + } + + private function remember(string $key, int $ttl, callable $callback): mixed + { + $cached = Cache::get($key); + if ($cached !== null) { + return $cached; + } + + $value = $callback(); + if ($value !== null) { + Cache::set($key, $value, $ttl); + } + + return $value; + } + private function decodeUrlEncodedItemStack(string $value): array { $parts = explode(',', urldecode($value)); diff --git a/app/controller/Tools.php b/app/controller/Tools.php index b71f602..ce77915 100644 --- a/app/controller/Tools.php +++ b/app/controller/Tools.php @@ -3,9 +3,13 @@ namespace app\controller; use GuzzleHttp\Client; +use think\facade\Cache; class Tools { + private const ITEMS_CACHE_KEY = 'tools:collection-items:v1'; + private const ITEMS_CACHE_TTL = 3600; + protected Client $client; protected array $items = []; @@ -19,8 +23,18 @@ public function __construct() $this->client = new Client([ 'verify' => false, 'version' => $httpVersion, + 'connect_timeout' => 3.0, + 'timeout' => 12.0, ]); - $this->items = $this->items('props', 'collection'); + + $items = Cache::get(self::ITEMS_CACHE_KEY); + if (!is_array($items) || $items === []) { + $items = $this->items('props', 'collection'); + if ($items !== []) { + Cache::set(self::ITEMS_CACHE_KEY, $items, self::ITEMS_CACHE_TTL); + } + } + $this->items = $items; } /** * 前端展示方法,使用流式输出纯文本,延迟基于品阶 @@ -188,4 +202,4 @@ private function items(string $type = '', string $subType = ''): array $data = json_decode($response->getBody()->getContents(), true); return $data['jData']['data']['data']['list']; } -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 33f8133..757b93f 100644 --- a/composer.json +++ b/composer.json @@ -46,13 +46,6 @@ "@php think vendor:publish" ] }, - "repositories": [ - { - "type": "composer", - "url": "https://packagist.mirrors.sjtug.sjtu.edu.cn", - "canonical": false - } - ], "suggest": { "ext-zend opcache": "*" }