perf(api): 优化节点统计查询与 Docker 构建缓存

节点流量累计统计改为统一从 v2_stat_server 聚合,
避免节点当前累计字段重置后出现月统计大于累计的错误。

Docker 构建改为先复制 composer 清单并缓存依赖安装,
同时移除 composer.lock 的忽略规则以提升缓存命中率。
This commit is contained in:
yinjianm
2026-04-28 17:13:06 +08:00
parent 1739f7a2f9
commit 16b22bc8a7
6 changed files with 47 additions and 19 deletions
-1
View File
@@ -18,7 +18,6 @@ Homestead.yaml
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
composer.phar composer.phar
composer.lock
yarn.lock yarn.lock
docker-compose.yml docker-compose.yml
.DS_Store .DS_Store
+14
View File
@@ -1,5 +1,19 @@
# CHANGELOG # CHANGELOG
## [0.6.16] - 2026-04-28
### 快速修改
- **[ci-workflows]**: 优化后端 Docker 构建缓存命中;`composer.lock` 现在进入镜像构建上下文,Composer 依赖安装提前到源码复制前并使用 BuildKit 缓存挂载,构建期不再重复执行全量 `chown/chmod` — by yinjianm
- 类型: 快速修改(无方案包)
- 文件: Dockerfile:1-44, .dockerignore:18-22
## [0.6.15] - 2026-04-28
### 快速修改
- **[admin-frontend]**: 修正节点 hover 流量详情的累计统计口径,今日、本月、累计现在全部从 `v2_stat_server` 按节点聚合,避免节点当前累计字段被重置后出现“本月大于累计”的展示错误 — by yinjianm
- 类型: 快速修改(无方案包)
- 文件: app/Http/Controllers/V2/Admin/Server/ManageController.php:34-66, .helloagents/modules/admin-frontend.md:49
## [0.6.14] - 2026-04-28 ## [0.6.14] - 2026-04-28
### 修复 ### 修复
@@ -42,7 +42,7 @@
## 2. 方案 ## 2. 方案
### 技术方案 ### 技术方案
在后端 `ManageController::getNodes()` 中基于当前节点 ID 集合批量聚合 `v2_stat_server`,按今日起点本月起点计算 `SUM(u)``SUM(d)``SUM(u + d)`;累计流量以 `v2_server.u/d` 为节点当前累计真相源。三组结果统一挂载到每个节点的 `traffic_stats` 字段。前端扩展节点类型定义和 `nodes.ts` 工具函数,提供统一的流量格式化与详情数据结构;`NodesView.vue` 将节点名称区域包裹为 Element Plus popover,并以 Apple 风格的克制统计网格展示日、月、总三组上下行数据。 在后端 `ManageController::getNodes()` 中基于当前节点 ID 集合批量聚合 `v2_stat_server`,按今日起点本月起点和全量统计三种窗口计算 `SUM(u)``SUM(d)``SUM(u + d)`。三组结果统一挂载到每个节点的 `traffic_stats` 字段。前端扩展节点类型定义和 `nodes.ts` 工具函数,提供统一的流量格式化与详情数据结构;`NodesView.vue` 将节点名称区域包裹为 Element Plus popover,并以 Apple 风格的克制统计网格展示日、月、总三组上下行数据。
### 影响范围 ### 影响范围
```yaml ```yaml
@@ -61,7 +61,7 @@
### 方案取舍 ### 方案取舍
```yaml ```yaml
唯一方案理由: 当前需求需要“日、月、总”三种统计,后端已有 StatServer 日志表和 Server.u/d 累计字段,最合理路径是在 getNodes 返回时一次性挂载聚合结果,前端只负责展示。 唯一方案理由: 当前需求需要“日、月、总”三种统计,后端已有 StatServer 日志表,最合理路径是在 getNodes 返回时一次性挂载聚合结果,前端只负责展示。
放弃的替代路径: 放弃的替代路径:
- 前端逐节点请求统计接口: 会产生 N+1 网络请求,表格分页和 hover 体验不稳定 - 前端逐节点请求统计接口: 会产生 N+1 网络请求,表格分页和 hover 体验不稳定
- 只展示 v2_server.u/d: 只能表示节点当前累计字段,不能满足日/月维度 - 只展示 v2_server.u/d: 只能表示节点当前累计字段,不能满足日/月维度
@@ -77,7 +77,6 @@
```mermaid ```mermaid
flowchart TD flowchart TD
A[StatServer v2_stat_server] --> B[ManageController getNodes 批量聚合] A[StatServer v2_stat_server] --> B[ManageController getNodes 批量聚合]
G[Server u/d] --> B
C[ServerService getAllServers] --> B C[ServerService getAllServers] --> B
B --> D[traffic_stats today/month/total] B --> D[traffic_stats today/month/total]
D --> E[AdminNodeItem 类型] D --> E[AdminNodeItem 类型]
@@ -103,7 +102,7 @@ traffic_stats?: {
| `traffic_stats.today.download` | number | 今日节点下行字节数 | | `traffic_stats.today.download` | number | 今日节点下行字节数 |
| `traffic_stats.today.total` | number | 今日节点总流量 | | `traffic_stats.today.total` | number | 今日节点总流量 |
| `traffic_stats.month.*` | number | 本月节点流量统计 | | `traffic_stats.month.*` | number | 本月节点流量统计 |
| `traffic_stats.total.*` | number | 节点当前累计流量统计,来源为 `v2_server.u/d` | | `traffic_stats.total.*` | number | 节点量统计流量,来源为 `v2_stat_server` |
--- ---
+1 -1
View File
@@ -46,7 +46,7 @@
- 节点管理页现支持墙状态展示、墙状态筛选与关键词搜索;父节点可通过行级或批量操作发起检测,子节点不单独检测并显示“随父节点”的继承状态 - 节点管理页现支持墙状态展示、墙状态筛选与关键词搜索;父节点可通过行级或批量操作发起检测,子节点不单独检测并显示“随父节点”的继承状态
- 节点管理页现支持“墙检测托管”开关、批量设置和刷新数据按钮;父节点开启后参与 `sync:server-gfw-checks` 自动检测,自动墙检统计只计算父节点;子节点不独立检测但可控制是否随父节点自动隐藏 / 恢复 - 节点管理页现支持“墙检测托管”开关、批量设置和刷新数据按钮;父节点开启后参与 `sync:server-gfw-checks` 自动检测,自动墙检统计只计算父节点;子节点不独立检测但可控制是否随父节点自动隐藏 / 恢复
- 节点行级菜单现已补齐“置顶节点”,会复用当前排序结果生成新的顺序 payload 并提交到 `server/manage/sort` - 节点行级菜单现已补齐“置顶节点”,会复用当前排序结果生成新的顺序 payload 并提交到 `server/manage/sort`
- 节点列表中鼠标悬停节点名称会显示节点流量详情卡;`server/manage/getNodes` 会返回 `traffic_stats.today/month/total`其中今日和本月来自 `v2_stat_server` 按节点聚合,累计来自 `v2_server.u/d`前端统一按 B/KB/MB/GB/TB 自适应格式化展示上行、下行和合计 - 节点列表中鼠标悬停节点名称会显示节点流量详情卡;`server/manage/getNodes` 会返回 `traffic_stats.today/month/total`三组数据均来自 `v2_stat_server` 按节点聚合,前端统一按 B/KB/MB/GB/TB 自适应格式化展示上行、下行和合计
- 权限组管理页使用真实后端 `server/group/fetch``server/group/save``server/group/drop`,支持关键字搜索、新增/编辑中央弹窗、删除确认,以及从节点数量列跳转到 `#/nodes?group={id}` 的筛选联动 - 权限组管理页使用真实后端 `server/group/fetch``server/group/save``server/group/drop`,支持关键字搜索、新增/编辑中央弹窗、删除确认,以及从节点数量列跳转到 `#/nodes?group={id}` 的筛选联动
- 路由管理页使用真实后端 `server/route/fetch``server/route/save``server/route/drop`,支持路由列表、关键词搜索、新增/编辑中央弹窗、删除与动作值展示 - 路由管理页使用真实后端 `server/route/fetch``server/route/save``server/route/drop`,支持路由列表、关键词搜索、新增/编辑中央弹窗、删除与动作值展示
- 路由管理页的节点引用摘要由 `server/manage/getNodes` 返回的 `route_ids` 推导,不在前端伪造额外接口 - 路由管理页的节点引用摘要由 `server/manage/getNodes` 返回的 `route_ids` 推导,不在前端伪造额外接口
+24 -10
View File
@@ -1,33 +1,47 @@
# syntax=docker/dockerfile:1.7
FROM phpswoole/swoole:php8.2-alpine FROM phpswoole/swoole:php8.2-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
# Install PHP extensions one by one with lower optimization level for ARM64 compatibility # Install PHP extensions one by one with lower optimization level for ARM64 compatibility
RUN CFLAGS="-O0" install-php-extensions pcntl && \ RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
CFLAGS="-O0" install-php-extensions pcntl && \
CFLAGS="-O0 -g0" install-php-extensions bcmath && \ CFLAGS="-O0 -g0" install-php-extensions bcmath && \
install-php-extensions zip && \ install-php-extensions zip && \
install-php-extensions redis && \ install-php-extensions redis && \
apk --no-cache add shadow sqlite mysql-client mysql-dev mariadb-connector-c git patch supervisor redis caddy && \ apk add --update-cache --no-progress shadow sqlite mysql-client mysql-dev mariadb-connector-c git patch supervisor redis caddy && \
addgroup -S -g 1000 www && adduser -S -G www -u 1000 www && \ addgroup -S -g 1000 www && adduser -S -G www -u 1000 www && \
(getent group redis || addgroup -S redis) && \ (getent group redis || addgroup -S redis) && \
(getent passwd redis || adduser -S -G redis -H -h /data redis) (getent passwd redis || adduser -S -G redis -H -h /data redis) && \
mkdir -p /data && \
chown redis:redis /data
WORKDIR /www WORKDIR /www
COPY .docker / ENV COMPOSER_ALLOW_SUPERUSER=1
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer-cache,sharing=locked \
COMPOSER_CACHE_DIR=/tmp/composer-cache composer install \
--no-dev \
--prefer-dist \
--no-interaction \
--no-progress \
--no-security-blocking \
--no-scripts \
--no-autoloader
COPY . /www COPY . /www
COPY .docker /
COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY .docker/caddy/Caddyfile /etc/caddy/Caddyfile COPY .docker/caddy/Caddyfile /etc/caddy/Caddyfile
COPY .docker/php/zz-xboard.ini /usr/local/etc/php/conf.d/zz-xboard.ini COPY .docker/php/zz-xboard.ini /usr/local/etc/php/conf.d/zz-xboard.ini
RUN composer install --no-cache --no-dev --no-security-blocking \ RUN composer dump-autoload --no-dev --optimize --no-interaction \
&& php artisan storage:link \ && php artisan storage:link
&& chown -R www:www /www \
&& chmod -R 775 /www \
&& mkdir -p /data \
&& chown redis:redis /data
ENV ENABLE_WEB=true \ ENV ENABLE_WEB=true \
ENABLE_HORIZON=true \ ENABLE_HORIZON=true \
@@ -37,7 +37,6 @@ class ManageController extends Controller
foreach ($servers as $server) { foreach ($servers as $server) {
$serverId = (int) $server->id; $serverId = (int) $server->id;
$stats[$serverId] = $this->emptyNodeTrafficStats(); $stats[$serverId] = $this->emptyNodeTrafficStats();
$stats[$serverId]['total'] = $this->buildTrafficAmount($server->u ?? 0, $server->d ?? 0);
} }
if (empty($stats)) { if (empty($stats)) {
@@ -46,17 +45,20 @@ class ManageController extends Controller
$this->fillTrafficWindow($stats, 'today', strtotime('today')); $this->fillTrafficWindow($stats, 'today', strtotime('today'));
$this->fillTrafficWindow($stats, 'month', strtotime(date('Y-m-01'))); $this->fillTrafficWindow($stats, 'month', strtotime(date('Y-m-01')));
$this->fillTrafficWindow($stats, 'total');
return $stats; return $stats;
} }
private function fillTrafficWindow(array &$stats, string $key, int $startAt): void private function fillTrafficWindow(array &$stats, string $key, ?int $startAt = null): void
{ {
$rows = StatServer::query() $rows = StatServer::query()
->selectRaw('server_id, COALESCE(SUM(u), 0) as upload, COALESCE(SUM(d), 0) as download') ->selectRaw('server_id, COALESCE(SUM(u), 0) as upload, COALESCE(SUM(d), 0) as download')
->whereIn('server_id', array_keys($stats)) ->whereIn('server_id', array_keys($stats))
->where('record_type', 'd') ->where('record_type', 'd')
->where('record_at', '>=', $startAt) ->when($startAt !== null, function ($query) use ($startAt) {
$query->where('record_at', '>=', $startAt);
})
->groupBy('server_id') ->groupBy('server_id')
->get(); ->get();