From 66da85f94537989c39da7612c07ccc103ecb791b Mon Sep 17 00:00:00 2001 From: Mac Date: Wed, 22 Jul 2026 21:53:35 +0800 Subject: [PATCH 1/3] =?UTF-8?q?perf(api):=20=E4=BC=98=E5=8C=96=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E8=AF=B7=E6=B1=82=E3=80=81=E7=BC=93=E5=AD=98=E5=92=8C?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E8=BF=90=E8=A1=8C=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 64 +++++++++++++ Dockerfile | 17 +++- app/controller/Game.php | 196 +++++++++++++++++++++++++++++---------- app/controller/Tools.php | 18 +++- 4 files changed, 244 insertions(+), 51 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2dd2478 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,64 @@ +# 更新日志 + +本项目的主要变更都会记录在此文件中,版本记录参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 格式。 + +## [Unreleased] + +### 性能优化 + +- 将 `Game::record()` 的 10 个独立上游请求改为批量并发执行,最大并发数为 5。 +- 将 `Game::player()` 的角色资料和三类货币请求改为并行执行。 +- 将 `/game/price` 在 `recent=1` 时的历史价格请求改为批量并发执行。 +- 为 `/game/config` 和 `/game/items` 增加 300 秒缓存,减少重复访问上游接口。 +- 为 `Tools` 控制器使用的物品集合增加 3600 秒缓存,降低初始化开销。 + +### 稳定性 + +- 为 Guzzle HTTP 客户端增加 3 秒连接超时和 12 秒请求超时,避免上游异常导致请求长期占用。 +- 批量请求统一收集失败结果,并在任一上游请求失败时抛出异常,避免返回不完整数据。 + +### 部署 + +- Docker 镜像增加 cURL、SQLite PDO 和 OPcache 扩展。 +- 为 CLI 模式启用并配置 OPcache。 +- 清理 APT 软件包索引,减小最终镜像体积。 + +## [1.5.7] - 2026-04-26 + +### 新增 + +- 添加弹药配置文件。 + +### 改进 + +- 优化武器和弹药配置数据处理逻辑。 +- 更新项目版本号至 `1.5.7`。 + +## [1.5.6] - 2026-04-26 + +### 新增 + +- 军需处接口增加集市商品数据。 + +## [1.5.5] - 2026-04-26 + +### 新增 + +- 添加军需处接口。 + +### 修复 + +- 修复 QQ 登录相关问题。 +- 移除 QQ 登录调试信息。 + +## [1.5.4] - 2026-04-17 + +### 修复 + +- 修复 QQ 鉴权缺少 `getQrSig` 方法的问题。 + +[Unreleased]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.7...HEAD +[1.5.7]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.6...v1.5.7 +[1.5.6]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.5...v1.5.6 +[1.5.5]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.4...v1.5.5 +[1.5.4]: https://github.com/WangBen137/deltaforce-data-api/releases/tag/v1.5.4 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 +} From e6d09a824a51196c0753165a927887e4830af15d Mon Sep 17 00:00:00 2001 From: Ben Wang Date: Wed, 22 Jul 2026 22:54:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=20Composer=20?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E4=B8=8E=20PR=20=E6=9E=84=E5=BB=BA=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 5 +++-- .github/workflows/php-release.yml | 18 ++++++++---------- composer.json | 7 ------- 3 files changed, 11 insertions(+), 19 deletions(-) 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/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": "*" } From cb57749caa8f8ca3d5a1e14e577895bb0b59d4f0 Mon Sep 17 00:00:00 2001 From: Ben Wang Date: Thu, 23 Jul 2026 20:10:22 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=97=A5=E5=BF=97=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 64 ---------------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2dd2478..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,64 +0,0 @@ -# 更新日志 - -本项目的主要变更都会记录在此文件中,版本记录参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 格式。 - -## [Unreleased] - -### 性能优化 - -- 将 `Game::record()` 的 10 个独立上游请求改为批量并发执行,最大并发数为 5。 -- 将 `Game::player()` 的角色资料和三类货币请求改为并行执行。 -- 将 `/game/price` 在 `recent=1` 时的历史价格请求改为批量并发执行。 -- 为 `/game/config` 和 `/game/items` 增加 300 秒缓存,减少重复访问上游接口。 -- 为 `Tools` 控制器使用的物品集合增加 3600 秒缓存,降低初始化开销。 - -### 稳定性 - -- 为 Guzzle HTTP 客户端增加 3 秒连接超时和 12 秒请求超时,避免上游异常导致请求长期占用。 -- 批量请求统一收集失败结果,并在任一上游请求失败时抛出异常,避免返回不完整数据。 - -### 部署 - -- Docker 镜像增加 cURL、SQLite PDO 和 OPcache 扩展。 -- 为 CLI 模式启用并配置 OPcache。 -- 清理 APT 软件包索引,减小最终镜像体积。 - -## [1.5.7] - 2026-04-26 - -### 新增 - -- 添加弹药配置文件。 - -### 改进 - -- 优化武器和弹药配置数据处理逻辑。 -- 更新项目版本号至 `1.5.7`。 - -## [1.5.6] - 2026-04-26 - -### 新增 - -- 军需处接口增加集市商品数据。 - -## [1.5.5] - 2026-04-26 - -### 新增 - -- 添加军需处接口。 - -### 修复 - -- 修复 QQ 登录相关问题。 -- 移除 QQ 登录调试信息。 - -## [1.5.4] - 2026-04-17 - -### 修复 - -- 修复 QQ 鉴权缺少 `getQrSig` 方法的问题。 - -[Unreleased]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.7...HEAD -[1.5.7]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.6...v1.5.7 -[1.5.6]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.5...v1.5.6 -[1.5.5]: https://github.com/WangBen137/deltaforce-data-api/compare/v1.5.4...v1.5.5 -[1.5.4]: https://github.com/WangBen137/deltaforce-data-api/releases/tag/v1.5.4