#feature: add project&cronjob management

This commit is contained in:
2026-01-16 12:14:43 +08:00
parent bbe68839e3
commit 381d5e6e49
19 changed files with 2157 additions and 84 deletions

View File

@@ -0,0 +1,302 @@
<?php
namespace App\Services;
use App\Models\Project;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class ProjectService
{
private string $projectsPath;
public function __construct(private readonly ConfigService $configService)
{
$this->projectsPath = $this->resolveProjectsPath();
}
private function resolveProjectsPath(): string
{
$path = $this->configService->get('workspace.projects_path');
if (empty($path)) {
throw new RuntimeException('configs 表未设置 workspace.projects_path。');
}
return rtrim($path, '/');
}
/**
* 获取项目根目录路径
*/
public function getProjectsPath(): string
{
return $this->projectsPath;
}
/**
* 获取所有项目
*/
public function getAllProjects(): Collection
{
return Project::query()->orderBy('name')->get();
}
/**
* 根据 slug 获取项目
*/
public function getBySlug(string $slug): ?Project
{
return Project::findBySlug($slug);
}
/**
* 创建项目
*/
public function create(array $data): Project
{
return Project::query()->create($data);
}
/**
* 更新项目
*/
public function update(Project $project, array $data): Project
{
$project->update($data);
return $project->refresh();
}
/**
* 删除项目
*/
public function delete(Project $project): bool
{
return $project->delete();
}
/**
* 获取项目状态
*/
public function getProjectStatus(Project $project): array
{
$fullPath = $project->getFullPath($this->projectsPath);
$pathValid = $project->isPathValid($this->projectsPath);
$isGitRepo = $pathValid && $project->isGitRepository($this->projectsPath);
$status = [
'path_valid' => $pathValid,
'full_path' => $fullPath,
'is_git_repo' => $isGitRepo,
'current_branch' => null,
'has_uncommitted_changes' => null,
];
if ($isGitRepo) {
try {
$status['current_branch'] = $this->getCurrentBranch($fullPath);
$status['has_uncommitted_changes'] = $this->hasUncommittedChanges($fullPath);
} catch (\Exception $e) {
// 忽略 Git 命令错误
}
}
return $status;
}
/**
* 发现新项目(扫描 projects_path 目录)
*/
public function discoverProjects(): array
{
if (!File::exists($this->projectsPath)) {
return [];
}
$existingSlugs = Project::query()->pluck('slug')->toArray();
$discovered = [];
$directories = File::directories($this->projectsPath);
foreach ($directories as $directory) {
$dirName = basename($directory);
// 跳过已存在的项目
if (in_array($dirName, $existingSlugs, true)) {
continue;
}
// 跳过隐藏目录
if (str_starts_with($dirName, '.')) {
continue;
}
$isGitRepo = is_dir($directory . DIRECTORY_SEPARATOR . '.git');
$discovered[] = [
'slug' => $dirName,
'name' => ucfirst(str_replace(['-', '_'], ' ', $dirName)),
'directory' => $dirName,
'path' => $directory,
'is_git_repo' => $isGitRepo,
];
}
return $discovered;
}
/**
* 批量添加发现的项目
*/
public function addDiscoveredProjects(array $slugs): array
{
$discovered = $this->discoverProjects();
$added = [];
foreach ($discovered as $project) {
if (in_array($project['slug'], $slugs, true)) {
$created = Project::query()->create([
'slug' => $project['slug'],
'name' => $project['name'],
'directory' => $project['directory'],
]);
$added[] = $created;
}
}
return $added;
}
/**
* 同步项目的 Git 信息
*/
public function syncGitInfo(Project $project): Project
{
$fullPath = $project->getFullPath($this->projectsPath);
if (!$project->isGitRepository($this->projectsPath)) {
return $project;
}
try {
// 获取当前版本(从 version.txt
$version = $this->getMasterVersion($fullPath);
if ($version !== null) {
$project->git_current_version = $version;
}
// 获取当前分支
$branch = $this->getCurrentBranch($fullPath);
if ($branch !== null) {
$project->git_release_branch = $branch;
}
$project->git_version_cached_at = now();
$project->save();
} catch (\Exception $e) {
// 忽略错误
}
return $project->refresh();
}
/**
* 获取 master 分支的版本号
*/
private function getMasterVersion(string $path): ?string
{
try {
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
$version = $this->runGit($path, ['git', 'show', 'origin/master:version.txt']);
return trim($version) ?: null;
} catch (ProcessFailedException $e) {
return null;
}
}
/**
* 获取当前分支
*/
private function getCurrentBranch(string $path): ?string
{
try {
$branch = $this->runGit($path, ['git', 'rev-parse', '--abbrev-ref', 'HEAD']);
return trim($branch) ?: null;
} catch (ProcessFailedException $e) {
return null;
}
}
/**
* 检查是否有未提交的更改
*/
private function hasUncommittedChanges(string $path): bool
{
try {
$output = $this->runGit($path, ['git', 'status', '--porcelain']);
return !empty(trim($output));
} catch (ProcessFailedException $e) {
return false;
}
}
/**
* 运行 Git 命令
*/
private function runGit(string $cwd, array $command): string
{
$process = new Process($command, $cwd);
$process->setTimeout(60);
$process->mustRun();
return $process->getOutput();
}
/**
* 获取所有启用 Git 监控的项目
*/
public function getGitMonitorEnabledProjects(): Collection
{
return Project::getGitMonitorEnabled();
}
/**
* 根据 app 名称查找项目
*/
public function findByAppName(string $appName): ?Project
{
return Project::findByAppName($appName);
}
/**
* 一键同步所有项目的 Git 信息
*/
public function syncAllGitInfo(): array
{
$projects = $this->getAllProjects();
$synced = [];
$failed = [];
foreach ($projects as $project) {
try {
if ($project->isGitRepository($this->projectsPath)) {
$this->syncGitInfo($project);
$synced[] = $project->slug;
}
} catch (\Exception $e) {
$failed[] = [
'slug' => $project->slug,
'error' => $e->getMessage(),
];
}
}
return [
'synced' => $synced,
'failed' => $failed,
];
}
}