#feature: add project&cronjob management
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
@@ -22,6 +23,24 @@ class CodeContextService
|
||||
*/
|
||||
public function getRepoPath(string $appName): ?string
|
||||
{
|
||||
// 优先从 Project 模型查找
|
||||
$project = Project::findByAppName($appName);
|
||||
|
||||
if ($project) {
|
||||
$env = $project->log_env ?? 'production';
|
||||
try {
|
||||
$envContent = $this->envService->getEnvContent($project->slug, $env);
|
||||
$repoPath = $this->parseEnvValue($envContent, 'LOG_ANALYSIS_CODE_REPO_PATH');
|
||||
|
||||
if ($repoPath && is_dir($repoPath)) {
|
||||
return $repoPath;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 忽略错误,继续尝试旧配置
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到旧的配置方式(兼容迁移前的情况)
|
||||
$appEnvMap = $this->configService->get('log_analysis.app_env_map', []);
|
||||
|
||||
if (!isset($appEnvMap[$appName])) {
|
||||
@@ -29,15 +48,15 @@ class CodeContextService
|
||||
}
|
||||
|
||||
$mapping = $appEnvMap[$appName];
|
||||
$project = $mapping['project'] ?? null;
|
||||
$projectSlug = $mapping['project'] ?? null;
|
||||
$env = $mapping['env'] ?? null;
|
||||
|
||||
if (!$project || !$env) {
|
||||
if (!$projectSlug || !$env) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$envContent = $this->envService->getEnvContent($project, $env);
|
||||
$envContent = $this->envService->getEnvContent($projectSlug, $env);
|
||||
$repoPath = $this->parseEnvValue($envContent, 'LOG_ANALYSIS_CODE_REPO_PATH');
|
||||
|
||||
if ($repoPath && is_dir($repoPath)) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Project;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -10,9 +11,8 @@ use Symfony\Component\Process\Process;
|
||||
|
||||
class GitMonitorService
|
||||
{
|
||||
private const RELEASE_CACHE_KEY = 'git_monitor.current_versions';
|
||||
private const LAST_CHECKED_KEY = 'git_monitor.last_checked_commits';
|
||||
private const DEVELOP_BRANCH = 'develop';
|
||||
private const RELEASE_CACHE_KEY = 'git-monitor.release_cache';
|
||||
|
||||
/**
|
||||
* 项目配置(只包含允许巡检的项目)
|
||||
@@ -30,12 +30,6 @@ class GitMonitorService
|
||||
) {
|
||||
$this->projectsPath = $this->resolveProjectsPath();
|
||||
$this->projects = $this->resolveProjects();
|
||||
|
||||
$enabled = config('git-monitor.enabled_projects', []);
|
||||
if (!empty($enabled)) {
|
||||
$this->projects = array_intersect_key($this->projects, array_flip($enabled));
|
||||
}
|
||||
|
||||
$this->commitScanLimit = (int) config('git-monitor.commit_scan_limit', 30);
|
||||
$this->gitTimeout = (int) config('git-monitor.git_timeout', 180);
|
||||
}
|
||||
@@ -46,10 +40,11 @@ class GitMonitorService
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查是否需要刷新缓存
|
||||
if (!$force) {
|
||||
$cached = $this->configService->get(self::RELEASE_CACHE_KEY);
|
||||
if (!empty($cached) && !$this->shouldRefreshCache($cached)) {
|
||||
return $cached;
|
||||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
||||
if ($anyProject && !$this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||||
return $this->buildCachePayload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +67,29 @@ class GitMonitorService
|
||||
// 根据当前版本号获取下一个版本
|
||||
$version = $this->jiraService->getUpcomingReleaseVersion($projectKey, $currentVersion);
|
||||
if ($version) {
|
||||
$branch = 'release/' . $version['version'];
|
||||
$payload['repositories'][$repoKey] = [
|
||||
'version' => $version['version'],
|
||||
'description' => $version['description'] ?? null,
|
||||
'release_date' => $version['release_date'],
|
||||
'branch' => 'release/' . $version['version'],
|
||||
'branch' => $branch,
|
||||
'current_version' => $currentVersion,
|
||||
];
|
||||
|
||||
// 更新 Project 模型
|
||||
$project = Project::findBySlug($repoKey);
|
||||
if ($project) {
|
||||
$project->update([
|
||||
'git_current_version' => $currentVersion,
|
||||
'git_release_branch' => $branch,
|
||||
'git_version_cached_at' => now(),
|
||||
]);
|
||||
|
||||
// 如果启用了自动创建 release 分支,检查并创建
|
||||
if ($project->auto_create_release_branch) {
|
||||
$this->ensureReleaseBranchExists($repoKey, $repoConfig, $branch, $version['description']);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to fetch release version from Jira', [
|
||||
@@ -87,24 +99,44 @@ class GitMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
$this->configService->set(
|
||||
self::RELEASE_CACHE_KEY,
|
||||
$payload,
|
||||
'Cached release versions fetched from Jira'
|
||||
);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
public function ensureReleaseCache(): array
|
||||
{
|
||||
$cached = $this->configService->get(self::RELEASE_CACHE_KEY);
|
||||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
||||
|
||||
if (empty($cached) || $this->shouldRefreshCache($cached)) {
|
||||
if (!$anyProject || $this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||||
return $this->refreshReleaseCache(true);
|
||||
}
|
||||
|
||||
return $cached;
|
||||
return $this->buildCachePayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Project 模型构建缓存数据
|
||||
*/
|
||||
private function buildCachePayload(): array
|
||||
{
|
||||
$projects = Project::query()
|
||||
->where('git_monitor_enabled', true)
|
||||
->whereNotNull('git_release_branch')
|
||||
->get();
|
||||
|
||||
$payload = [
|
||||
'cached_at' => $projects->first()?->git_version_cached_at?->toDateTimeString() ?? now()->toDateTimeString(),
|
||||
'repositories' => [],
|
||||
];
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$payload['repositories'][$project->slug] = [
|
||||
'version' => $project->git_current_version,
|
||||
'branch' => $project->git_release_branch,
|
||||
'current_version' => $project->git_current_version,
|
||||
];
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
public function checkRepositories(bool $refreshCacheIfNeeded = true): array
|
||||
@@ -151,15 +183,15 @@ class GitMonitorService
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function shouldRefreshCache(array $cached): bool
|
||||
private function shouldRefreshCache(Carbon|\DateTimeInterface|string|null $cachedAt): bool
|
||||
{
|
||||
$cachedAt = Arr::get($cached, 'cached_at');
|
||||
if (empty($cachedAt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($cachedAt)->lt(Carbon::now()->startOfDay());
|
||||
$cachedTime = $cachedAt instanceof Carbon ? $cachedAt : Carbon::parse($cachedAt);
|
||||
return $cachedTime->lt(Carbon::now()->startOfDay());
|
||||
} catch (\Throwable) {
|
||||
return true;
|
||||
}
|
||||
@@ -177,7 +209,9 @@ class GitMonitorService
|
||||
$remoteBranch = 'origin/' . $branch;
|
||||
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
|
||||
|
||||
$lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey);
|
||||
// 从 Project 模型获取 lastChecked
|
||||
$project = Project::findBySlug($repoKey);
|
||||
$lastChecked = $project?->git_last_checked_commit;
|
||||
$commits = $this->collectCommits($path, $remoteBranch, $lastChecked);
|
||||
|
||||
$issues = [
|
||||
@@ -378,14 +412,10 @@ class GitMonitorService
|
||||
|
||||
private function updateLastChecked(string $repoKey, string $sha): void
|
||||
{
|
||||
$current = $this->configService->get(self::LAST_CHECKED_KEY, []);
|
||||
$current[$repoKey] = $sha;
|
||||
|
||||
$this->configService->set(
|
||||
self::LAST_CHECKED_KEY,
|
||||
$current,
|
||||
'Last scanned release commits'
|
||||
);
|
||||
$project = Project::findBySlug($repoKey);
|
||||
if ($project) {
|
||||
$project->update(['git_last_checked_commit' => $sha]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getCommitMetadata(string $repoPath, string $commit): array
|
||||
@@ -478,22 +508,37 @@ class GitMonitorService
|
||||
*/
|
||||
private function resolveProjects(): array
|
||||
{
|
||||
$projects = $this->configService->get('workspace.repositories');
|
||||
// 优先从 Project 模型获取启用 Git 监控的项目
|
||||
$projectModels = Project::query()
|
||||
->where('git_monitor_enabled', true)
|
||||
->get();
|
||||
|
||||
if ($projects === null) {
|
||||
$fallback = $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
|
||||
if (!empty($fallback)) {
|
||||
Log::warning('configs 表未设置 workspace.repositories,已使用 git-monitor.enabled_projects。');
|
||||
if ($projectModels->isNotEmpty()) {
|
||||
$projects = [];
|
||||
foreach ($projectModels as $project) {
|
||||
$projects[$project->slug] = [
|
||||
'jira_project' => $project->jira_project_code,
|
||||
'directory' => $project->directory ?? $project->slug,
|
||||
'path' => $project->absolute_path,
|
||||
'display' => $project->name,
|
||||
];
|
||||
}
|
||||
return $fallback;
|
||||
return $projects;
|
||||
}
|
||||
|
||||
if (!is_array($projects)) {
|
||||
Log::warning('configs 表 workspace.repositories 配置格式不正确,已降级使用 git-monitor.enabled_projects。');
|
||||
return $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
|
||||
// 回退到旧的配置方式(兼容迁移前的情况)
|
||||
$configProjects = $this->configService->get('workspace.repositories');
|
||||
|
||||
if ($configProjects !== null && is_array($configProjects)) {
|
||||
$enabled = config('git-monitor.enabled_projects', []);
|
||||
if (!empty($enabled)) {
|
||||
return array_intersect_key($configProjects, array_flip($enabled));
|
||||
}
|
||||
return $configProjects;
|
||||
}
|
||||
|
||||
return $projects;
|
||||
// 最后回退到环境变量配置
|
||||
return $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,4 +599,100 @@ class GitMonitorService
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查远程是否存在指定分支
|
||||
*/
|
||||
private function remoteBranchExists(string $path, string $branch): bool
|
||||
{
|
||||
try {
|
||||
$this->runGit($path, ['git', 'fetch', 'origin']);
|
||||
$this->runGit($path, ['git', 'ls-remote', '--exit-code', '--heads', 'origin', $branch]);
|
||||
return true;
|
||||
} catch (ProcessFailedException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 release 分支存在,如果不存在则从 master 创建并推送
|
||||
*/
|
||||
private function ensureReleaseBranchExists(string $repoKey, array $repoConfig, string $branch, ?string $description): void
|
||||
{
|
||||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
||||
|
||||
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
|
||||
Log::warning('Invalid git repository path for branch creation', ['repository' => $repoKey, 'path' => $path]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查远程分支是否已存在
|
||||
if ($this->remoteBranchExists($path, $branch)) {
|
||||
Log::info('Release branch already exists on remote', ['repository' => $repoKey, 'branch' => $branch]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从分支名中提取版本号(release/2.63.0.0 -> 2.63.0.0)
|
||||
$version = str_replace('release/', '', $branch);
|
||||
|
||||
try {
|
||||
// 从 origin/master 创建新分支
|
||||
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
|
||||
|
||||
// 创建本地分支(基于 origin/master)
|
||||
try {
|
||||
// 先尝试删除可能存在的本地分支
|
||||
$this->runGit($path, ['git', 'branch', '-D', $branch]);
|
||||
} catch (ProcessFailedException) {
|
||||
// 忽略,分支可能不存在
|
||||
}
|
||||
|
||||
$this->runGit($path, ['git', 'checkout', '-b', $branch, 'origin/master']);
|
||||
|
||||
// 修改 version.txt 文件
|
||||
$versionFile = $path . DIRECTORY_SEPARATOR . 'version.txt';
|
||||
if (!file_put_contents($versionFile, $version)) {
|
||||
throw new \RuntimeException("Failed to write version.txt");
|
||||
}
|
||||
|
||||
// 添加并提交更改
|
||||
$this->runGit($path, ['git', 'add', 'version.txt']);
|
||||
|
||||
// 构建提交信息:分支名 + 空格 + Jira 描述
|
||||
$commitMessage = $branch . ($description ? ' ' . $description : '');
|
||||
$this->runGit($path, ['git', 'commit', '-m', $commitMessage]);
|
||||
|
||||
// 推送到远程
|
||||
$this->runGit($path, ['git', 'push', '-u', 'origin', $branch]);
|
||||
|
||||
Log::info('Created and pushed release branch', [
|
||||
'repository' => $repoKey,
|
||||
'branch' => $branch,
|
||||
'version' => $version,
|
||||
'message' => $commitMessage,
|
||||
]);
|
||||
|
||||
// 发送钉钉通知
|
||||
$this->dingTalkService->sendText(sprintf(
|
||||
"【Release 分支创建】\n项目: %s\n分支: %s\n版本: %s\n描述: %s",
|
||||
$repoConfig['display'] ?? $repoKey,
|
||||
$branch,
|
||||
$version,
|
||||
$description ?? '无'
|
||||
));
|
||||
} catch (ProcessFailedException $e) {
|
||||
Log::error('Failed to create release branch', [
|
||||
'repository' => $repoKey,
|
||||
'branch' => $branch,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
} finally {
|
||||
// 切回 develop 分支,避免影响后续操作
|
||||
try {
|
||||
$this->runGit($path, ['git', 'checkout', self::DEVELOP_BRANCH]);
|
||||
} catch (ProcessFailedException) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,6 +798,7 @@ class JiraService
|
||||
|
||||
return [
|
||||
'version' => $candidate->name,
|
||||
'description' => $candidate->description ?? null,
|
||||
'release_date' => !empty($candidate->releaseDate)
|
||||
? Carbon::parse($candidate->releaseDate)->toDateString()
|
||||
: null,
|
||||
@@ -826,6 +827,7 @@ class JiraService
|
||||
|
||||
return [
|
||||
'version' => $candidate->name,
|
||||
'description' => $candidate->description ?? null,
|
||||
'release_date' => !empty($candidate->releaseDate)
|
||||
? Carbon::parse($candidate->releaseDate)->toDateString()
|
||||
: null,
|
||||
@@ -844,6 +846,7 @@ class JiraService
|
||||
|
||||
return [
|
||||
'version' => $candidate->name,
|
||||
'description' => $candidate->description ?? null,
|
||||
'release_date' => !empty($candidate->releaseDate)
|
||||
? Carbon::parse($candidate->releaseDate)->toDateString()
|
||||
: null,
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
133
app/Services/ScheduledTaskService.php
Normal file
133
app/Services/ScheduledTaskService.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
|
||||
class ScheduledTaskService
|
||||
{
|
||||
private const CONFIG_KEY = 'scheduled_tasks';
|
||||
|
||||
private static ?ConfigService $configServiceInstance = null;
|
||||
|
||||
public function __construct(
|
||||
private ConfigService $configService,
|
||||
private Schedule $schedule
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 静态方法:检查任务是否启用(供 console.php 中的 when() 回调使用)
|
||||
*/
|
||||
public static function isEnabled(string $name): bool
|
||||
{
|
||||
try {
|
||||
self::$configServiceInstance ??= app(ConfigService::class);
|
||||
$enabled = self::$configServiceInstance->get(self::CONFIG_KEY, []);
|
||||
return $enabled[$name] ?? false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有任务(从 Schedule 实例动态获取)
|
||||
*/
|
||||
public function getAllTasks(): array
|
||||
{
|
||||
$this->ensureTasksLoaded();
|
||||
|
||||
$enabledTasks = $this->configService->get(self::CONFIG_KEY, []);
|
||||
$tasks = [];
|
||||
|
||||
foreach ($this->schedule->events() as $event) {
|
||||
$name = $this->getEventName($event);
|
||||
$tasks[] = [
|
||||
'name' => $name,
|
||||
'command' => $this->getEventCommand($event),
|
||||
'description' => $event->description ?: $name,
|
||||
'frequency' => $this->getFrequencyLabel($event->expression),
|
||||
'cron' => $event->expression,
|
||||
'enabled' => $enabledTasks[$name] ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务启用状态
|
||||
*/
|
||||
public function setTaskEnabled(string $name, bool $enabled): void
|
||||
{
|
||||
$this->ensureTasksLoaded();
|
||||
|
||||
// 验证任务是否存在
|
||||
$exists = false;
|
||||
foreach ($this->schedule->events() as $event) {
|
||||
if ($this->getEventName($event) === $name) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
throw new \InvalidArgumentException("未知任务: {$name}");
|
||||
}
|
||||
|
||||
$tasks = $this->configService->get(self::CONFIG_KEY, []);
|
||||
$tasks[$name] = $enabled;
|
||||
|
||||
$this->configService->set(self::CONFIG_KEY, $tasks, '定时任务启用状态');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 console.php 中的任务已加载
|
||||
*/
|
||||
private function ensureTasksLoaded(): void
|
||||
{
|
||||
if (count($this->schedule->events()) === 0) {
|
||||
require_once base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
||||
private function getEventName($event): string
|
||||
{
|
||||
if (property_exists($event, 'mutexName') && $event->mutexName) {
|
||||
return $event->mutexName;
|
||||
}
|
||||
return $this->getEventCommand($event);
|
||||
}
|
||||
|
||||
private function getEventCommand($event): string
|
||||
{
|
||||
if (property_exists($event, 'command') && $event->command) {
|
||||
$command = $event->command;
|
||||
if (str_contains($command, 'artisan')) {
|
||||
$command = preg_replace('/^.*artisan\s+/', '', $command);
|
||||
}
|
||||
return trim(str_replace("'", '', $command));
|
||||
}
|
||||
return 'closure';
|
||||
}
|
||||
|
||||
private function getFrequencyLabel(string $expression): string
|
||||
{
|
||||
$map = [
|
||||
'* * * * *' => '每分钟',
|
||||
'*/5 * * * *' => '每 5 分钟',
|
||||
'*/10 * * * *' => '每 10 分钟',
|
||||
'*/15 * * * *' => '每 15 分钟',
|
||||
'*/30 * * * *' => '每 30 分钟',
|
||||
'0 * * * *' => '每小时',
|
||||
'0 */2 * * *' => '每 2 小时',
|
||||
'0 */4 * * *' => '每 4 小时',
|
||||
'0 */6 * * *' => '每 6 小时',
|
||||
'0 */12 * * *' => '每 12 小时',
|
||||
'0 0 * * *' => '每天凌晨 0:00',
|
||||
'0 2 * * *' => '每天凌晨 2:00',
|
||||
'0 0 * * 0' => '每周日凌晨',
|
||||
'0 0 1 * *' => '每月 1 日凌晨',
|
||||
];
|
||||
return $map[$expression] ?? $expression;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user