'raw_retention_days', 'group' => 'nginx_log', 'value' => '7', 'remark' => '原始日志保留天数(0=永久)', 'sort' => 0, ], [ 'name' => 'stat_retention_days', 'group' => 'nginx_log', 'value' => '365', 'remark' => '统计聚合保留天数(0=永久)', 'sort' => 0, ], [ 'name' => 'topn_default', 'group' => 'nginx_log', 'value' => '10', 'remark' => 'Top N 默认值(热门 URL/Referer/UA)', 'sort' => 0, ], [ 'name' => 'exclude_static', 'group' => 'nginx_log', 'value' => '1', 'remark' => '是否排除静态资源访问(0=不排除,1=排除 /css/.js/.png 等扩展名)', 'sort' => 0, ], ]; /** * 待新增菜单. * 字段顺序与 adminInitData/SystemMenu.php 一致:id/pid/title/icon/href/params/target/sort/status. * 挂到系统管理(pid=228)下,不创建独立顶级分组。 */ public $newMenus = [ [ 'id' => 301, 'pid' => 228, 'title' => '访问统计仪表盘', 'icon' => 'fa fa-line-chart', 'href' => 'system.nginx_log_stat/index', 'params' => '', 'target' => '_self', 'sort' => 0, 'status' => 1, ], [ 'id' => 302, 'pid' => 228, 'title' => '原始访问日志', 'icon' => 'fa fa-file-text', 'href' => 'nginx.access_log/index', 'params' => '', 'target' => '_self', 'sort' => 0, 'status' => 1, ], ]; public function update() { $this->output->writeln('更新代码'); $this->output->info('v2.4.0:nginx_log 分析模块存量升级(补齐 4 条 sysconfig + 2 条菜单)'); $configTable = config('database.connections.mysql.prefix', 'ul_') . 'system_config'; $menuTable = config('database.connections.mysql.prefix', 'ul_') . 'system_menu'; // 确认表存在(防御性检查,参照 v2.3.0.php 范式) $tablesExist = Db::query( "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?, ?)", [$configTable, $menuTable] ); if (count($tablesExist) < 2) { $this->output->warning("表 {$configTable} 或 {$menuTable} 不存在,跳过数据补齐。请先执行 scheme:sync 或 migrate:run 初始化表结构。"); return; } $configAdded = 0; $configSkipped = 0; foreach ($this->newConfigs as $cfg) { // 幂等:用 (group, name) 检查存在性 $exists = Db::name('system_config') ->where('group', $cfg['group']) ->where('name', $cfg['name']) ->find(); if ($exists) { $this->output->writeln(" - 配置 {$cfg['group']}.{$cfg['name']} 已存在,跳过"); ++$configSkipped; continue; } Db::name('system_config')->insert($cfg); $this->output->writeln(" - 新增配置 {$cfg['group']}.{$cfg['name']} 完成"); ++$configAdded; } $menuAdded = 0; $menuSkipped = 0; foreach ($this->newMenus as $menu) { // 幂等:用 id 检查存在性(顶级菜单 href 为空,不能用 href 去重) $exists = Db::name('system_menu') ->where('id', $menu['id']) ->find(); if ($exists) { $this->output->writeln(" - 菜单 id={$menu['id']} ({$menu['title']}) 已存在,跳过"); ++$menuSkipped; continue; } Db::name('system_menu')->insert($menu); $this->output->writeln(" - 新增菜单 id={$menu['id']} ({$menu['title']}) 完成"); ++$menuAdded; } $totalSkipped = $configSkipped + $menuSkipped; $this->output->info("本次新增 {$configAdded} 条配置、{$menuAdded} 条菜单,跳过 {$totalSkipped} 条已存在"); // ==================== node_id 字段 + 唯一键变更(多节点采集支持)==================== // 与 migration 20260801125222_nginx_log_add_node_id.php 保持一致 // 6 张表全部新增 node_id varchar(64) default '' // position / 4 张 stat 表的唯一键前置 node_id(多节点隔离,避免聚合 INSERT 触发 duplicate key) // raw 表(nginx_access_log)加 node_id + time_local 普通索引,无唯一键变更 $this->output->info('v2.4.0:nginx_log 6 张表新增 node_id 字段与唯一键调整'); $prefix = config('database.connections.mysql.prefix', 'ul_'); // 表短名 => 索引变更配置 // drop_idx: 要删除的旧索引名(raw 表无) // add_idx: 新索引 [name, cols, unique] $nodeIdTables = [ 'nginx_log_position' => [ 'drop_idx' => 'uniq_file_path', 'add_idx' => ['name' => 'uniq_node_file', 'cols' => ['node_id', 'file_path'], 'unique' => true], ], 'nginx_access_log' => [ 'add_idx' => ['name' => 'idx_node_time', 'cols' => ['node_id', 'time_local'], 'unique' => false], ], 'nginx_stat_hour' => [ 'drop_idx' => 'uniq_date_hour', 'add_idx' => ['name' => 'uniq_node_date_hour', 'cols' => ['node_id', 'stat_date', 'stat_hour'], 'unique' => true], ], 'nginx_stat_url' => [ 'drop_idx' => 'uniq_date_uri', 'add_idx' => ['name' => 'uniq_node_date_uri', 'cols' => ['node_id', 'stat_date', 'uri'], 'unique' => true], ], 'nginx_stat_referer' => [ 'drop_idx' => 'uniq_date_referer', 'add_idx' => ['name' => 'uniq_node_date_referer', 'cols' => ['node_id', 'stat_date', 'referer_domain'], 'unique' => true], ], 'nginx_stat_ua' => [ 'drop_idx' => 'uniq_date_type_name', 'add_idx' => ['name' => 'uniq_node_date_type_name', 'cols' => ['node_id', 'stat_date', 'ua_type', 'ua_name'], 'unique' => true], ], ]; $colAdded = 0; $colSkipped = 0; foreach ($nodeIdTables as $tableShort => $idxCfg) { $fullTable = $prefix . $tableShort; // 防御性检查:表是否存在(未运行 migration 时跳过,不报错) $tableExists = Db::query( "SELECT COUNT(*) AS cnt FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [$fullTable] ); if (empty($tableExists) || (int) $tableExists[0]['cnt'] === 0) { $this->output->writeln(" - {$fullTable} 表不存在,跳过(请先执行 migrate:run)"); continue; } // 幂等 1:检查 node_id 字段是否存在 $colExists = Db::query( "SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'node_id'", [$fullTable] ); if (!empty($colExists) && (int) $colExists[0]['cnt'] > 0) { $this->output->writeln(" - {$fullTable} node_id 字段已存在,跳过"); ++$colSkipped; } else { Db::execute("ALTER TABLE `{$fullTable}` ADD COLUMN `node_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '采集节点 ID(空=全局汇总)'"); $this->output->writeln(" - {$fullTable} 新增 node_id 字段完成"); ++$colAdded; } // 幂等 2:唯一键/索引变更(检查新索引是否存在) $newIdx = $idxCfg['add_idx']['name'] ?? null; if ($newIdx !== null) { $idxExists = Db::query( "SELECT COUNT(*) AS cnt FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?", [$fullTable, $newIdx] ); if (empty($idxExists) || (int) $idxExists[0]['cnt'] === 0) { // 删除旧索引(position / stat 表有,raw 表无) if (!empty($idxCfg['drop_idx'])) { Db::execute("ALTER TABLE `{$fullTable}` DROP INDEX `{$idxCfg['drop_idx']}`"); } // 新增新索引 $idxType = !empty($idxCfg['add_idx']['unique']) ? 'UNIQUE' : 'INDEX'; $colsStr = implode(', ', array_map(function ($c) { return "`{$c}`"; }, $idxCfg['add_idx']['cols'])); Db::execute("ALTER TABLE `{$fullTable}` ADD {$idxType} `{$newIdx}` ({$colsStr})"); $this->output->writeln(" - {$fullTable} 索引调整:{$newIdx}"); } } } $this->output->info("node_id 迁移完成(新增字段 {$colAdded} 表,跳过 {$colSkipped} 表已存在)"); $this->output->writeln('更新代码完成'); } }