Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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: |
Expand Down
18 changes: 8 additions & 10 deletions .github/workflows/php-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,25 @@ 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 # 即使步骤失败也缓存

# 安装 Composer 依赖
- 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: |
Expand All @@ -63,7 +61,6 @@ jobs:
# 列出打包文件
- name: 列出打包文件
run: unzip -l release-${{ env.VERSION }}-${{ github.run_id }}.zip
if: always() # 即使失败也运行,便于调试

# 上传构建产物
- name: 上传构建产物
Expand All @@ -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 }}
Expand Down
17 changes: 16 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
196 changes: 148 additions & 48 deletions app/controller/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
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;

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;

Expand All @@ -20,6 +24,8 @@ public function __construct()
$this->client = new Client([
'verify' => false,
'version' => 1.1,
'connect_timeout' => 3.0,
'timeout' => 12.0,
]);
}

Expand All @@ -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);
Expand All @@ -73,31 +87,40 @@ 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,
'tickets' => 17888808889,
'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);
}
Expand All @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -566,6 +602,70 @@ private function requestIde(array $formParams, ?CookieJar $cookie = null): array
return json_decode($response->getBody()->getContents(), true);
}

/**
* 并行执行多个相互独立的 IDE 请求,降低接口聚合时的总等待时间。
*
* @param array<string|int, array> $requests
* @return array<string|int, 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));
Expand Down
Loading