From cd11a856bb1bab279829302f1a945e5752a26f42 Mon Sep 17 00:00:00 2001 From: tradewind Date: Thu, 25 Dec 2025 18:39:23 +0800 Subject: [PATCH] #feature: add config page --- .../Controllers/Admin/ConfigController.php | 106 ++++ app/Services/GitMonitorService.php | 123 ++-- config/git-monitor.php | 2 +- database/factories/IpUserMappingFactory.php | 23 + database/factories/OperationLogFactory.php | 35 ++ .../js/components/admin/AdminDashboard.vue | 2 +- .../js/components/admin/SystemSettings.vue | 527 ++++++++++++++++-- routes/api.php | 5 + 8 files changed, 704 insertions(+), 119 deletions(-) create mode 100644 app/Http/Controllers/Admin/ConfigController.php create mode 100644 database/factories/IpUserMappingFactory.php create mode 100644 database/factories/OperationLogFactory.php diff --git a/app/Http/Controllers/Admin/ConfigController.php b/app/Http/Controllers/Admin/ConfigController.php new file mode 100644 index 0000000..53bfe03 --- /dev/null +++ b/app/Http/Controllers/Admin/ConfigController.php @@ -0,0 +1,106 @@ +orderBy('key') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'configs' => $configs, + ], + ]); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'key' => ['required', 'string', 'max:255', 'unique:configs,key'], + 'value' => ['nullable', 'string'], + 'description' => ['nullable', 'string', 'max:255'], + ]); + + $config = Config::query()->create([ + 'key' => $data['key'], + 'value' => $this->decodeValue($data['value'] ?? null), + 'description' => $data['description'] ?? null, + ]); + + return response()->json([ + 'success' => true, + 'data' => [ + 'config' => $config, + ], + ]); + } + + public function update(Request $request, Config $config): JsonResponse + { + $data = $request->validate([ + 'key' => [ + 'required', + 'string', + 'max:255', + Rule::unique('configs', 'key')->ignore($config->id), + ], + 'value' => ['nullable', 'string'], + 'description' => ['nullable', 'string', 'max:255'], + ]); + + $config->update([ + 'key' => $data['key'], + 'value' => $this->decodeValue($data['value'] ?? null), + 'description' => $data['description'] ?? null, + ]); + + return response()->json([ + 'success' => true, + 'data' => [ + 'config' => $config->refresh(), + ], + ]); + } + + public function destroy(Config $config): JsonResponse + { + $config->delete(); + + return response()->json([ + 'success' => true, + ]); + } + + private function decodeValue(?string $raw): mixed + { + if ($raw === null) { + return null; + } + + $trimmed = trim($raw); + if ($trimmed === '') { + return null; + } + + $decoded = json_decode($trimmed, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw ValidationException::withMessages([ + 'value' => 'value 必须是合法 JSON', + ]); + } + + return $decoded; + } +} diff --git a/app/Services/GitMonitorService.php b/app/Services/GitMonitorService.php index 465a373..32fbc05 100644 --- a/app/Services/GitMonitorService.php +++ b/app/Services/GitMonitorService.php @@ -163,79 +163,57 @@ class GitMonitorService private function inspectRepository(string $repoKey, array $repoConfig, string $branch): array { $path = $this->resolveProjectPath($repoKey, $repoConfig); - $restoreBranch = null; if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) { throw new \RuntimeException("Project path {$path} is not a valid git repository"); } - try { - $restoreBranch = $this->synchronizeRepository($path, $branch); - $head = $this->runGit($path, ['git', 'rev-parse', 'HEAD']); + $this->synchronizeRepository($path, $branch); + $remoteBranch = 'origin/' . $branch; + $head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]); - $lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey); - $commits = $this->collectCommits($path, $branch, $lastChecked); + $lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey); + $commits = $this->collectCommits($path, $remoteBranch, $lastChecked); - $issues = [ - 'develop_merges' => [], - 'missing_functions' => [], - ]; + $issues = [ + 'develop_merges' => [], + 'missing_functions' => [], + ]; - foreach ($commits as $commit) { - if ($this->isDevelopMerge($path, $commit)) { - $issues['develop_merges'][] = $this->getCommitMetadata($path, $commit); - } - - $missingFunctions = $this->detectMissingFunctions($path, $commit); - if (!empty($missingFunctions)) { - $issues['missing_functions'][] = [ - 'commit' => $this->getCommitMetadata($path, $commit), - 'details' => $missingFunctions, - ]; - } + foreach ($commits as $commit) { + if ($this->isDevelopMerge($path, $commit)) { + $issues['develop_merges'][] = $this->getCommitMetadata($path, $commit); } - $this->updateLastChecked($repoKey, $head); - - return [ - 'repository' => $repoKey, - 'display' => $repoConfig['display'] ?? ucfirst($repoKey), - 'branch' => $branch, - 'path' => $path, - 'head' => $head, - 'commits_scanned' => count($commits), - 'issues' => $issues, - ]; - } finally { - if ($restoreBranch) { - try { - $this->runGit($path, ['git', 'checkout', $restoreBranch]); - } catch (\Throwable $e) { - Log::warning('Failed to restore branch', [ - 'repository' => $repoKey, - 'branch' => $restoreBranch, - 'error' => $e->getMessage(), - ]); - } + $missingFunctions = $this->detectMissingFunctions($path, $commit); + if (!empty($missingFunctions)) { + $issues['missing_functions'][] = [ + 'commit' => $this->getCommitMetadata($path, $commit), + 'details' => $missingFunctions, + ]; } } + + $this->updateLastChecked($repoKey, $head); + + return [ + 'repository' => $repoKey, + 'display' => $repoConfig['display'] ?? ucfirst($repoKey), + 'branch' => $branch, + 'path' => $path, + 'head' => $head, + 'commits_scanned' => count($commits), + 'issues' => $issues, + ]; } - private function synchronizeRepository(string $path, string $branch): ?string + private function synchronizeRepository(string $path, string $branch): void { - $currentBranch = trim($this->runGit($path, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'])); - $restoreBranch = $currentBranch !== $branch && $currentBranch !== 'HEAD' - ? $currentBranch - : null; - $this->runGit($path, ['git', 'fetch', 'origin']); $this->runGit($path, ['git', 'fetch', 'origin', $branch]); $this->runGit($path, ['git', 'fetch', 'origin', self::DEVELOP_BRANCH]); - $this->checkoutBranch($path, $branch); - $this->runGit($path, ['git', 'pull', '--ff-only', 'origin', $branch]); - - return $restoreBranch; + $this->runGit($path, ['git', 'show-ref', '--verify', "refs/remotes/origin/{$branch}"]); } private function checkoutBranch(string $path, string $branch): void @@ -468,8 +446,41 @@ class GitMonitorService { $projects = $this->configService->get('workspace.repositories'); - if (!is_array($projects) || empty($projects)) { - throw new \RuntimeException('configs 表未设置 workspace.repositories。'); + if ($projects === null) { + $fallback = $this->buildFallbackProjects(config('git-monitor.enabled_projects', [])); + if (!empty($fallback)) { + Log::warning('configs 表未设置 workspace.repositories,已使用 git-monitor.enabled_projects。'); + } + return $fallback; + } + + if (!is_array($projects)) { + Log::warning('configs 表 workspace.repositories 配置格式不正确,已降级使用 git-monitor.enabled_projects。'); + return $this->buildFallbackProjects(config('git-monitor.enabled_projects', [])); + } + + return $projects; + } + + /** + * @return array> + */ + private function buildFallbackProjects(array $enabled): array + { + $projects = []; + $jiraKeyMap = [ + 'portal-be' => 'WP', + 'agent-be' => 'AM', + ]; + + foreach ($enabled as $repoKey) { + if (!is_string($repoKey) || $repoKey === '') { + continue; + } + + $projects[$repoKey] = [ + 'jira_project' => $jiraKeyMap[$repoKey] ?? $repoKey, + ]; } return $projects; diff --git a/config/git-monitor.php b/config/git-monitor.php index ace713d..c1ffd03 100644 --- a/config/git-monitor.php +++ b/config/git-monitor.php @@ -3,7 +3,7 @@ return [ 'enabled_projects' => array_values(array_filter(array_map( 'trim', - explode(',', env('GIT_MONITOR_PROJECTS', 'service,portal-be,agent-be')) + explode(',', env('GIT_MONITOR_PROJECTS', 'portal-be,agent-be')) ))), 'commit_scan_limit' => 30, diff --git a/database/factories/IpUserMappingFactory.php b/database/factories/IpUserMappingFactory.php new file mode 100644 index 0000000..2699d34 --- /dev/null +++ b/database/factories/IpUserMappingFactory.php @@ -0,0 +1,23 @@ + + */ +class IpUserMappingFactory extends Factory +{ + protected $model = IpUserMapping::class; + + public function definition(): array + { + return [ + 'ip_address' => $this->faker->unique()->ipv4(), + 'user_name' => $this->faker->userName(), + 'remark' => $this->faker->optional()->sentence(6), + ]; + } +} diff --git a/database/factories/OperationLogFactory.php b/database/factories/OperationLogFactory.php new file mode 100644 index 0000000..228cd47 --- /dev/null +++ b/database/factories/OperationLogFactory.php @@ -0,0 +1,35 @@ + + */ +class OperationLogFactory extends Factory +{ + protected $model = OperationLog::class; + + public function definition(): array + { + $payload = $this->faker->optional()->randomElement([ + ['action' => 'create', 'status' => 'ok'], + ['item_id' => 1, 'count' => 2], + ['note' => 'sample'], + ]); + + return [ + 'ip_address' => $this->faker->ipv4(), + 'user_label' => $this->faker->optional()->userName(), + 'method' => $this->faker->randomElement(['POST', 'PUT', 'PATCH', 'DELETE']), + 'path' => '/' . $this->faker->slug(2), + 'route_name' => $this->faker->optional()->slug(2), + 'status_code' => $this->faker->randomElement([200, 201, 204, 400, 403, 422, 500]), + 'duration_ms' => $this->faker->numberBetween(1, 5000), + 'request_payload' => $payload, + 'user_agent' => $this->faker->userAgent(), + ]; + } +} diff --git a/resources/js/components/admin/AdminDashboard.vue b/resources/js/components/admin/AdminDashboard.vue index dc1f281..e587329 100644 --- a/resources/js/components/admin/AdminDashboard.vue +++ b/resources/js/components/admin/AdminDashboard.vue @@ -47,7 +47,7 @@ /> - + diff --git a/resources/js/components/admin/SystemSettings.vue b/resources/js/components/admin/SystemSettings.vue index 2131475..f488909 100644 --- a/resources/js/components/admin/SystemSettings.vue +++ b/resources/js/components/admin/SystemSettings.vue @@ -1,74 +1,233 @@