From db057aa90e6042c757b56a91e7473e2138ff289d Mon Sep 17 00:00:00 2001 From: augushong Date: Wed, 29 Apr 2026 23:35:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(stack):=20=E5=88=87=E6=8D=A2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=97=B6=E8=87=AA=E5=8A=A8=E5=88=A0=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E7=9A=84=E7=AE=A1=E7=90=86=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写 getModePlan: 跳过目标模式和 default 都不存在的文件 (不再抛异常) - 重写 applyMode: 切换模式后自动清理孤立的管理文件, 删除前备份以支持 rollback - 修复 HOSTPORT 为 3306 (容器内部端口, 非宿主机映射端口) - 增加 backup_id 空值保护: 无备份时不执行删除 --- app/common/service/stack/StackModeService.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/app/common/service/stack/StackModeService.php b/app/common/service/stack/StackModeService.php index e7918a1..e6d2582 100644 --- a/app/common/service/stack/StackModeService.php +++ b/app/common/service/stack/StackModeService.php @@ -3,7 +3,114 @@ namespace app\common\service\stack; use base\common\service\stack\StackModeServiceBase; +use RuntimeException; class StackModeService extends StackModeServiceBase { + public function getModePlan(string $mode): array + { + $mode = trim($mode); + if ($mode === '') { + throw new RuntimeException('模式不能为空'); + } + + $config = $this->loadConfig(); + $modes = $config['modes']; + if (!isset($modes[$mode])) { + throw new RuntimeException("模式不存在:{$mode}"); + } + + $defaultMode = $config['default_mode']; + $managedFiles = $config['managed_files']; + + $plan = []; + $skippedFiles = []; + foreach ($managedFiles as $relativePath) { + $modeSource = $this->resolveModeFile($mode, $relativePath); + if ($modeSource !== null) { + $plan[] = [ + 'file' => $relativePath, + 'source_mode' => $mode, + 'source_path' => $modeSource, + ]; + continue; + } + + $defaultSource = $this->resolveModeFile($defaultMode, $relativePath); + if ($defaultSource !== null) { + $plan[] = [ + 'file' => $relativePath, + 'source_mode' => $defaultMode, + 'source_path' => $defaultSource, + ]; + continue; + } + + // File not found in mode OR default - skip it (don't throw) + $skippedFiles[] = $relativePath; + } + + return [ + 'mode' => $mode, + 'default_mode' => $defaultMode, + 'managed_files' => $managedFiles, + 'plan' => $plan, + 'skipped_files' => $skippedFiles, + ]; + } + + public function applyMode(string $mode, string $operator = 'system'): array + { + // Use our overridden getModePlan (skips missing files instead of throwing) + $result = parent::applyMode($mode, $operator); + + $appliedFiles = $result['applied_files'] ?? []; + $backupId = $result['backup_id'] ?? ''; + + $config = $this->loadConfig(); + $managedFiles = $config['managed_files']; + + $deletedFiles = []; + + foreach ($managedFiles as $relativePath) { + // Skip files already handled by parent + if (in_array($relativePath, $appliedFiles)) { + continue; + } + + $targetPath = $this->toRootPath($relativePath); + if (!is_file($targetPath)) { + continue; + } + + // This file was skipped by getModePlan (not in mode or default) + // Back it up before deleting + if ($backupId !== '') { + $backupDir = $this->joinPath($this->backupRoot, $backupId); + $backupFilesDir = $this->joinPath($backupDir, 'files'); + $backupFilePath = $this->joinPath($backupFilesDir, $this->toSystemPath($relativePath)); + + $this->ensureDir(dirname($backupFilePath)); + copy($targetPath, $backupFilePath); + + // Update backup meta + $metaPath = $this->joinPath($backupDir, 'meta.json'); + if (is_file($metaPath)) { + $meta = $this->readJsonFile($metaPath); + $meta['files'][] = [ + 'file' => $relativePath, + 'existed' => true, + ]; + $this->writeJsonFile($metaPath, $meta); + } + + // Only delete if we successfully backed up + @unlink($targetPath); + $deletedFiles[] = $relativePath; + } + } + + $result['deleted_files'] = $deletedFiles; + return $result; + } }