#feature: update SQL generator

This commit is contained in:
2026-05-19 14:57:11 +08:00
parent 53bca7d609
commit 3c628eb391
10 changed files with 1043 additions and 165 deletions
+115 -71
View File
@@ -2,17 +2,19 @@
namespace App\Services;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use JiraRestApi\Configuration\ArrayConfiguration;
use JiraRestApi\Issue\IssueService;
use JiraRestApi\JiraException;
use JiraRestApi\Project\ProjectService;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class JiraService
{
private IssueService $issueService;
private ProjectService $projectService;
private array $config;
public function __construct()
@@ -39,7 +41,6 @@ class JiraService
$this->projectService = new ProjectService($clientConfig);
}
/**
* 按项目组织任务数据
*/
@@ -51,7 +52,7 @@ class JiraService
$projectKey = $issue->fields->project->key;
$isSubtask = $issue->fields->issuetype->subtask ?? false;
if (!$organized->has($projectKey)) {
if (! $organized->has($projectKey)) {
$organized->put($projectKey, [
'name' => $issue->fields->project->name,
'tasks' => collect(),
@@ -81,7 +82,7 @@ class JiraService
'summary',
'status',
'project',
'issuetype'
'issuetype',
]);
} catch (JiraException) {
return null;
@@ -93,14 +94,14 @@ class JiraService
$tasks->put($issue->key, [
'key' => $issue->key,
'summary' => $issue->fields->summary,
'url' => $this->config['host'] . '/browse/' . $issue->key,
'url' => $this->config['host'].'/browse/'.$issue->key,
'subtasks' => collect(),
]);
}
private function addSubtask(Collection $tasks, string $parentKey, $issue): void
{
if (!$tasks->has($parentKey)) {
if (! $tasks->has($parentKey)) {
// 获取父任务的真实信息
$parentDetails = $this->getIssueDetails($parentKey);
$parentSummary = $parentDetails ? $parentDetails->fields->summary : '父任务';
@@ -108,7 +109,7 @@ class JiraService
$tasks->put($parentKey, [
'key' => $parentKey,
'summary' => $parentSummary,
'url' => $this->config['host'] . '/browse/' . $parentKey,
'url' => $this->config['host'].'/browse/'.$parentKey,
'subtasks' => collect(),
]);
}
@@ -116,7 +117,7 @@ class JiraService
$tasks[$parentKey]['subtasks']->put($issue->key, [
'key' => $issue->key,
'summary' => $issue->fields->summary,
'url' => $this->config['host'] . '/browse/' . $issue->key,
'url' => $this->config['host'].'/browse/'.$issue->key,
'created' => $issue->fields->created ?? null,
]);
}
@@ -128,7 +129,7 @@ class JiraService
{
$username = $username ?: $this->config['default_user'];
if (!$username) {
if (! $username) {
throw new \InvalidArgumentException('用户名不能为空');
}
@@ -153,14 +154,14 @@ class JiraService
'status',
'project',
'issuetype',
'created'
'created',
]);
if (!empty($issues->issues)) {
if (! empty($issues->issues)) {
return $this->organizeIssuesByProject($issues->issues);
}
} catch (JiraException $e) {
throw new \RuntimeException('获取未来任务失败: ' . $e->getMessage());
throw new \RuntimeException('获取未来任务失败: '.$e->getMessage());
}
return collect();
@@ -169,24 +170,21 @@ class JiraService
/**
* 生成 Markdown 格式的周报
*/
public function generateWeeklyReport(?string $username = null): string
public function generateWeeklyReport(?string $username = null, string $period = 'this_week'): string
{
$username = $username ?: $this->config['default_user'];
// 获取上周的工时记录
$now = Carbon::now();
$startOfWeek = $now->copy()->subWeek()->startOfWeek();
$endOfWeek = $now->copy()->subWeek()->endOfWeek();
$reportPeriod = $this->resolveWeeklyReportRange($period);
$workLogs = $this->getWorkLogs($username, $startOfWeek, $endOfWeek);
$workLogs = $this->getWorkLogs($username, $reportPeriod['start'], $reportPeriod['end']);
$organizedTasks = $this->organizeTasksForReport($workLogs, $username);
$nextWeekTasks = $this->getNextWeekTasks($username);
$markdown = "# 过去一周的任务\n\n";
$markdown = "# {$reportPeriod['title']}\n\n";
if ($organizedTasks->isEmpty()) {
$markdown .= "本周暂无工时记录的任务。\n\n";
$markdown .= "{$reportPeriod['empty_message']}\n\n";
} else {
// 按Sprint分类的需求
if ($organizedTasks->has('sprints') && $organizedTasks['sprints']->isNotEmpty()) {
@@ -311,6 +309,33 @@ class JiraService
return $markdown;
}
/**
* 解析周报统计周期
*
* @return array{start: Carbon, end: Carbon, title: string, empty_message: string}
*/
private function resolveWeeklyReportRange(string $period): array
{
$normalizedPeriod = $period === 'this_week' ? 'this_week' : 'last_week';
$now = Carbon::now();
if ($normalizedPeriod === 'this_week') {
return [
'start' => $now->copy()->startOfWeek(),
'end' => $now->copy()->endOfDay(),
'title' => '本周完成的任务',
'empty_message' => '本周暂无工时记录的任务。',
];
}
return [
'start' => $now->copy()->subWeek()->startOfWeek(),
'end' => $now->copy()->subWeek()->endOfWeek(),
'title' => '上周完成的任务',
'empty_message' => '上周暂无工时记录的任务。',
];
}
/**
* 获取指定日期范围内的工时记录
*/
@@ -345,7 +370,7 @@ class JiraService
'customfield_14305', // 需求类型
]);
if (!empty($issues->issues)) {
if (! empty($issues->issues)) {
$workLogs = $this->extractWorkLogs($issues->issues, $username, $startDate, $endDate);
if ($workLogs->isNotEmpty()) {
@@ -353,8 +378,9 @@ class JiraService
}
}
} catch (JiraException $e) {
throw new \RuntimeException('获取工时记录失败: ' . $e->getMessage());
throw new \RuntimeException('获取工时记录失败: '.$e->getMessage());
}
// 如果所有查询都没有结果,返回空集合
return collect();
}
@@ -377,7 +403,7 @@ class JiraService
// 处理 author 可能是数组或对象的情况
$authorName = is_array($worklog->author) ? ($worklog->author['name'] ?? '') : ($worklog->author->name ?? '');
if (!empty($authorName) && $authorName === $username &&
if (! empty($authorName) && $authorName === $username &&
$worklogDate->between($startDate, $endDate)) {
// 获取父任务信息
@@ -411,7 +437,7 @@ class JiraService
'project_key' => $issue->fields->project->key ?? '',
'issue_key' => $issue->key,
'issue_summary' => $issue->fields->summary ?? '',
'issue_url' => $this->config['host'] . '/browse/' . $issue->key,
'issue_url' => $this->config['host'].'/browse/'.$issue->key,
'issue_status' => $issue->fields->status->name ?? 'Unknown',
'issue_type' => $issue->fields->issuetype->name ?? 'Unknown',
'issue_created' => $issue->fields->created ?? null,
@@ -464,7 +490,7 @@ class JiraService
$sprintField = $issue->fields->customFields['customfield_10004'];
// 处理数组情况
if (is_array($sprintField) && !empty($sprintField)) {
if (is_array($sprintField) && ! empty($sprintField)) {
$lastSprint = end($sprintField);
if (is_string($lastSprint)) {
// 解析Sprint字符串,格式通常为: com.atlassian.greenhopper.service.sprint.Sprint@xxx[name=十月中需求,...]
@@ -486,13 +512,14 @@ class JiraService
if (preg_match('/name=([^,\]]+)/', $sprintField, $matches)) {
return $matches[1];
}
// 如果是纯文本,直接返回
return $sprintField;
}
}
// 尝试从fixVersions获取版本信息作为备选
if (isset($issue->fields->fixVersions) && is_array($issue->fields->fixVersions) && !empty($issue->fields->fixVersions)) {
if (isset($issue->fields->fixVersions) && is_array($issue->fields->fixVersions) && ! empty($issue->fields->fixVersions)) {
return $issue->fields->fixVersions[0]->name ?? null;
}
@@ -526,7 +553,7 @@ class JiraService
$stageValue = null;
}
if ($stageValue && !empty($stageValue)) {
if ($stageValue && ! empty($stageValue)) {
// 标准化阶段名称
if (str_contains($stageValue, 'SIT') || str_contains($stageValue, 'sit') || $stageValue === '测试阶段') {
return 'SIT环境BUG';
@@ -537,8 +564,9 @@ class JiraService
if (str_contains($stageValue, 'UAT') || str_contains($stageValue, 'uat')) {
return 'UAT环境BUG';
}
// 如果不匹配标准格式,直接返回原值
return $stageValue . 'BUG';
return $stageValue.'BUG';
}
}
@@ -572,7 +600,7 @@ class JiraService
// 处理对象类型
if (is_object($type) && isset($type->value)) {
return $type->value;
} elseif (is_string($type) && !empty($type)) {
} elseif (is_string($type) && ! empty($type)) {
return $type;
}
}
@@ -601,7 +629,7 @@ class JiraService
if (isset($issue->fields->customFields['customfield_10115'])) {
$description = $issue->fields->customFields['customfield_10115'];
if (is_string($description) && !empty($description)) {
if (is_string($description) && ! empty($description)) {
return $description;
}
}
@@ -617,7 +645,7 @@ class JiraService
// 从customfield_14305获取需求类型
if (isset($issue->fields->customFields['customfield_14305'])) {
$type = $issue->fields->customFields['customfield_14305'];
if (is_array($type) && !empty($type)) {
if (is_array($type) && ! empty($type)) {
$firstType = $type[0];
if (is_object($firstType) && isset($firstType->value)) {
return $firstType->value;
@@ -657,16 +685,8 @@ class JiraService
*/
private function extractDeveloper($issue): ?string
{
// 从customfield_11000获取开发人
if (isset($issue->fields->customFields['customfield_11000'])) {
$developer = $issue->fields->customFields['customfield_11000'];
if (is_string($developer) && !empty($developer)) {
return $developer;
}
}
return null;
// 从customfield_11000获取开发人(User 类型字段返回对象,兼容字符串/数组)
return $this->extractUserFieldName($issue, 'customfield_11000');
}
/**
@@ -674,13 +694,32 @@ class JiraService
*/
private function extractActualFixer($issue): ?string
{
// 从customfield_11301获取实际修复人
if (isset($issue->fields->customFields['customfield_11301'])) {
$fixer = $issue->fields->customFields['customfield_11301'];
// 从customfield_11301获取实际修复人(User 类型字段返回对象,兼容字符串/数组)
return $this->extractUserFieldName($issue, 'customfield_11301');
}
if (is_string($fixer) && !empty($fixer)) {
return $fixer;
}
/**
* 通用 User 类型自定义字段解析:兼容对象、关联数组、字符串
*/
private function extractUserFieldName($issue, string $fieldKey): ?string
{
if (! isset($issue->fields->customFields[$fieldKey])) {
return null;
}
$value = $issue->fields->customFields[$fieldKey];
if (is_object($value)) {
return $value->name ?? $value->key ?? null;
}
if (is_array($value)) {
// JIRA 用户字段以关联数组形式返回时
return $value['name'] ?? $value['key'] ?? null;
}
if (is_string($value) && $value !== '') {
return $value;
}
return null;
@@ -695,6 +734,7 @@ class JiraService
$summary = preg_replace('/!([^!]+\.(png|jpg|jpeg|gif|bmp))!/i', '', $summary);
// 移除多余的空格和换行
$summary = preg_replace('/\s+/', ' ', $summary);
return trim($summary);
}
@@ -709,6 +749,9 @@ class JiraService
'未开始',
'需求已确认',
'开发中',
'Open',
'To Do',
'In Progress',
'需求调研中',
'需求已调研',
'需求已评审',
@@ -718,7 +761,7 @@ class JiraService
];
// 如果状态不在"未完成"列表中,则标记为已完成
return !in_array($status, $incompleteStatuses, true);
return ! in_array($status, $incompleteStatuses, true);
}
/**
@@ -775,13 +818,13 @@ class JiraService
|| ($actualFixer === $username);
// 如果不是当前用户相关的Bug,跳过
if (!$isUserRelated) {
if (! $isUserRelated) {
continue;
}
// Bug按发现阶段分类
$stage = $workLog['bug_stage'];
if (!$organized['bugs']->has($stage)) {
if (! $organized['bugs']->has($stage)) {
$organized['bugs']->put($stage, collect());
}
@@ -804,15 +847,15 @@ class JiraService
} elseif (($isStory || $isSubtask) && $workLog['sprint']) {
// Story类型或子任务,且有Sprint的,按Sprint分类(需求)
$sprintName = $workLog['sprint'];
if (!$organized['sprints']->has($sprintName)) {
if (! $organized['sprints']->has($sprintName)) {
$organized['sprints']->put($sprintName, collect());
}
$this->addTaskToSprintOrTaskList($organized['sprints'][$sprintName], $workLog);
} elseif ($isStory && !$workLog['sprint']) {
} elseif ($isStory && ! $workLog['sprint']) {
// Story类型但没有Sprint的,放入需求分类
$this->addTaskToSprintOrTaskList($organized['stories'], $workLog);
} elseif ($isSubtask && !$workLog['sprint'] && $workLog['parent_task']) {
} elseif ($isSubtask && ! $workLog['sprint'] && $workLog['parent_task']) {
// 子任务没有Sprint,检查父任务类型来决定分类
$parentKey = $workLog['parent_task']['key'];
$parentDetails = $this->getIssueDetails($parentKey);
@@ -846,7 +889,7 @@ class JiraService
// 子任务
$parentKey = $workLog['parent_task']['key'];
if (!$taskList->has($parentKey)) {
if (! $taskList->has($parentKey)) {
// 获取父任务的真实信息
$parentDetails = $this->getIssueDetails($parentKey);
$parentSummary = $parentDetails ? $parentDetails->fields->summary : $workLog['parent_task']['summary'];
@@ -855,7 +898,7 @@ class JiraService
$taskList->put($parentKey, [
'key' => $parentKey,
'summary' => $parentSummary,
'url' => $this->config['host'] . '/browse/' . $parentKey,
'url' => $this->config['host'].'/browse/'.$parentKey,
'status' => $parentStatus,
'subtasks' => collect(),
]);
@@ -870,7 +913,7 @@ class JiraService
]);
} else {
// 主任务
if (!$taskList->has($workLog['issue_key'])) {
if (! $taskList->has($workLog['issue_key'])) {
$taskList->put($workLog['issue_key'], [
'key' => $workLog['issue_key'],
'summary' => $workLog['issue_summary'],
@@ -886,15 +929,15 @@ class JiraService
* 获取下一个 release 版本
* 根据当前版本号,在 Jira 版本列表中找到下一个版本
*
* @param string $projectKey Jira 项目 key
* @param string|null $currentVersion 当前版本号(来自 master 分支的 version.txt
* @param string $projectKey Jira 项目 key
* @param string|null $currentVersion 当前版本号(来自 master 分支的 version.txt
*/
public function getUpcomingReleaseVersion(string $projectKey, ?string $currentVersion = null): ?array
{
try {
$versions = $this->projectService->getVersions($projectKey);
} catch (JiraException $e) {
throw new \RuntimeException('获取 release 版本失败: ' . $e->getMessage(), previous: $e);
throw new \RuntimeException('获取 release 版本失败: '.$e->getMessage(), previous: $e);
}
if (empty($versions)) {
@@ -903,8 +946,8 @@ class JiraService
// 按版本名称排序(假设版本号格式一致,如 1.0.0, 1.0.1, 1.1.0
$sortedVersions = collect($versions)
->filter(fn($version) => !empty($version->name))
->sortBy(fn($version) => $version->name, SORT_NATURAL)
->filter(fn ($version) => ! empty($version->name))
->sortBy(fn ($version) => $version->name, SORT_NATURAL)
->values();
if ($sortedVersions->isEmpty()) {
@@ -914,17 +957,17 @@ class JiraService
// 如果没有提供当前版本,返回第一个未发布的版本
if (empty($currentVersion)) {
$candidate = $sortedVersions
->filter(fn($version) => !($version->released ?? false))
->filter(fn ($version) => ! ($version->released ?? false))
->first();
if (!$candidate) {
if (! $candidate) {
return null;
}
return [
'version' => $candidate->name,
'description' => $candidate->description ?? null,
'release_date' => !empty($candidate->releaseDate)
'release_date' => ! empty($candidate->releaseDate)
? Carbon::parse($candidate->releaseDate)->toDateString()
: null,
];
@@ -932,7 +975,7 @@ class JiraService
// 找到当前版本在列表中的位置,返回下一个版本
$currentIndex = $sortedVersions->search(
fn($version) => $version->name === $currentVersion
fn ($version) => $version->name === $currentVersion
);
// 如果找不到当前版本,尝试找到第一个大于当前版本的未发布版本
@@ -942,18 +985,19 @@ class JiraService
if ($version->released ?? false) {
return false;
}
return version_compare($version->name, $currentVersion, '>');
})
->first();
if (!$candidate) {
if (! $candidate) {
return null;
}
return [
'version' => $candidate->name,
'description' => $candidate->description ?? null,
'release_date' => !empty($candidate->releaseDate)
'release_date' => ! empty($candidate->releaseDate)
? Carbon::parse($candidate->releaseDate)->toDateString()
: null,
];
@@ -962,17 +1006,17 @@ class JiraService
// 从当前版本的下一个开始,找到第一个未发布的版本
$candidate = $sortedVersions
->slice($currentIndex + 1)
->filter(fn($version) => !($version->released ?? false))
->filter(fn ($version) => ! ($version->released ?? false))
->first();
if (!$candidate) {
if (! $candidate) {
return null;
}
return [
'version' => $candidate->name,
'description' => $candidate->description ?? null,
'release_date' => !empty($candidate->releaseDate)
'release_date' => ! empty($candidate->releaseDate)
? Carbon::parse($candidate->releaseDate)->toDateString()
: null,
];