#feature: add project&cronjob management
This commit is contained in:
302
app/Services/ProjectService.php
Normal file
302
app/Services/ProjectService.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user