diff --git a/.env.example b/.env.example index 35db1dd..ef8f534 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,41 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# JIRA Configuration +JIRA_HOST=http://jira.eainc.com:8080 +JIRA_USERNAME= +JIRA_PASSWORD= +JIRA_API_TOKEN= +JIRA_AUTH_TYPE=basic +JIRA_TIMEOUT=30 +JIRA_DEFAULT_USER= + +# CRM Slave Database Configuration +CRMSLAVE_DB_HOST=127.0.0.1 +CRMSLAVE_DB_PORT=3306 +CRMSLAVE_DB_DATABASE=crmslave +CRMSLAVE_DB_USERNAME=root +CRMSLAVE_DB_PASSWORD= + +# Agent Slave Database Configuration +AGENTSLAVE_DB_HOST=127.0.0.1 +AGENTSLAVE_DB_PORT=3306 +AGENTSLAVE_DB_DATABASE=agent +AGENTSLAVE_DB_USERNAME=root +AGENTSLAVE_DB_PASSWORD= + +# Mono Slave Database Configuration +MONOSLAVE_DB_HOST=127.0.0.1 +MONOSLAVE_DB_PORT=3306 +MONOSLAVE_DB_DATABASE=mono +MONOSLAVE_DB_USERNAME=root +MONOSLAVE_DB_PASSWORD= + +# Agent Service Configuration +AGENT_URL=http://localhost:8080 +AGENT_TIMEOUT=30 + +# Mono Service Configuration +MONO_URL=http://localhost:8081 +MONO_TIMEOUT=30 diff --git a/app/Clients/AgentClient.php b/app/Clients/AgentClient.php new file mode 100644 index 0000000..c5e1390 --- /dev/null +++ b/app/Clients/AgentClient.php @@ -0,0 +1,37 @@ +baseUrl = config('services.agent.url'); + $this->http = Http::timeout(config('services.agent.timeout', 30)) + ->withoutVerifying(); + } + + /** + * 分发消息到Agent + */ + public function dispatchMessage(array $data): Response + { + return $this->http->post($this->baseUrl . '/rpc/dispatchMessage', $data); + } + + /** + * 测试连接 + */ + public function testConnection(): Response + { + return $this->http->get($this->baseUrl . '/health'); + } +} + diff --git a/app/Clients/MonoClient.php b/app/Clients/MonoClient.php new file mode 100644 index 0000000..5bb1c8e --- /dev/null +++ b/app/Clients/MonoClient.php @@ -0,0 +1,37 @@ +baseUrl = config('services.mono.url'); + $this->http = Http::timeout(config('services.mono.timeout', 30)) + ->withoutVerifying(); + } + + /** + * 测试连接 + */ + public function testConnection(): Response + { + return $this->http->get($this->baseUrl . '/health'); + } + + /** + * 更新消息分发状态 + */ + public function updateDispatch(array $data): Response + { + return $this->http->post($this->baseUrl . '/rpc/datadispatch/message/update-dispatch', $data); + } +} + diff --git a/app/Console/Commands/EnvCommand.php b/app/Console/Commands/EnvCommand.php index 12e1667..abf8d27 100644 --- a/app/Console/Commands/EnvCommand.php +++ b/app/Console/Commands/EnvCommand.php @@ -8,9 +8,10 @@ use Illuminate\Console\Command; class EnvCommand extends Command { protected $signature = 'env:manage - {action : 操作类型 (list|environments|apply|save|import|delete)} + {action : 操作类型 (list|environments|apply|save|import|delete|backups|restore|delete-backup)} {--project= : 项目名称} {--environment= : 环境名称} + {--backup= : 备份名称} {--content= : 环境文件内容}'; protected $description = '环境文件管理工具'; @@ -41,6 +42,12 @@ class EnvCommand extends Command return $this->importEnvironment(); case 'delete': return $this->deleteEnvironment(); + case 'backups': + return $this->listBackups(); + case 'restore': + return $this->restoreBackup(); + case 'delete-backup': + return $this->deleteBackup(); default: $this->error("未知操作: {$action}"); $this->showUsage(); @@ -234,18 +241,113 @@ class EnvCommand extends Command return 0; } + /** + * 列出项目备份 + */ + private function listBackups(): int + { + $project = $this->option('project'); + + if (!$project) { + $this->error('请指定项目名称: --project=项目名'); + return 1; + } + + $backups = $this->envManager->getProjectBackups($project); + + if (empty($backups)) { + $this->info("项目 {$project} 没有备份文件"); + return 0; + } + + $this->info("项目 {$project} 的备份文件:"); + $this->table( + ['备份名称', '文件大小', '创建时间'], + array_map(function ($backup) { + return [ + $backup['name'], + $this->formatBytes($backup['size']), + $backup['created_at'] + ]; + }, $backups) + ); + + return 0; + } + + /** + * 恢复备份 + */ + private function restoreBackup(): int + { + $project = $this->option('project'); + $backup = $this->option('backup'); + + if (!$project || !$backup) { + $this->error('请指定项目名称和备份名称: --project=项目名 --backup=备份名'); + return 1; + } + + if ($this->confirm("确定要将备份 {$backup} 恢复到项目 {$project} 吗?")) { + $success = $this->envManager->restoreBackup($project, $backup); + + if ($success) { + $this->info("成功将备份 {$backup} 恢复到项目 {$project}"); + return 0; + } else { + $this->error("恢复备份失败"); + return 1; + } + } + + $this->info('操作已取消'); + return 0; + } + + /** + * 删除备份 + */ + private function deleteBackup(): int + { + $project = $this->option('project'); + $backup = $this->option('backup'); + + if (!$project || !$backup) { + $this->error('请指定项目名称和备份名称: --project=项目名 --backup=备份名'); + return 1; + } + + if ($this->confirm("确定要删除备份 {$project}/{$backup} 吗?")) { + $success = $this->envManager->deleteBackup($project, $backup); + + if ($success) { + $this->info("成功删除备份 {$project}/{$backup}"); + return 0; + } else { + $this->error("删除备份失败"); + return 1; + } + } + + $this->info('操作已取消'); + return 0; + } + /** * 显示使用说明 */ private function showUsage(): void { $this->info('使用说明:'); - $this->line(' php artisan env:manage list # 列出所有项目'); - $this->line(' php artisan env:manage environments --project=项目名 # 列出项目环境'); - $this->line(' php artisan env:manage apply --project=项目名 --environment=环境名 # 应用环境'); - $this->line(' php artisan env:manage save --project=项目名 --environment=环境名 # 保存环境'); - $this->line(' php artisan env:manage import --project=项目名 --environment=环境名 # 导入环境'); - $this->line(' php artisan env:manage delete --project=项目名 --environment=环境名 # 删除环境'); + $this->line(' php artisan env:manage list # 列出所有项目'); + $this->line(' php artisan env:manage environments --project=项目名 # 列出项目环境'); + $this->line(' php artisan env:manage apply --project=项目名 --environment=环境名 # 应用环境'); + $this->line(' php artisan env:manage save --project=项目名 --environment=环境名 # 保存环境'); + $this->line(' php artisan env:manage import --project=项目名 --environment=环境名 # 导入环境'); + $this->line(' php artisan env:manage delete --project=项目名 --environment=环境名 # 删除环境'); + $this->line(' php artisan env:manage backups --project=项目名 # 列出项目备份'); + $this->line(' php artisan env:manage restore --project=项目名 --backup=备份名 # 恢复备份'); + $this->line(' php artisan env:manage delete-backup --project=项目名 --backup=备份名 # 删除备份'); } /** diff --git a/app/Console/Commands/JiraTestCommand.php b/app/Console/Commands/JiraTestCommand.php new file mode 100644 index 0000000..3356e1c --- /dev/null +++ b/app/Console/Commands/JiraTestCommand.php @@ -0,0 +1,30 @@ +envManager = $envManager; } - /** - * 显示环境管理页面 - */ - public function index() - { - return view('env.index'); - } + /** * 获取所有项目列表 diff --git a/app/Http/Controllers/JiraController.php b/app/Http/Controllers/JiraController.php new file mode 100644 index 0000000..ffac2af --- /dev/null +++ b/app/Http/Controllers/JiraController.php @@ -0,0 +1,135 @@ +jiraService = $jiraService; + } + + + + /** + * 生成上周周报 + */ + public function generateWeeklyReport(Request $request): JsonResponse + { + try { + $username = $request->input('username') ?: config('jira.default_user'); + + if (!$username) { + return response()->json([ + 'success' => false, + 'message' => '请提供用户名' + ], 400); + } + + $report = $this->jiraService->generateWeeklyReport($username); + + return response()->json([ + 'success' => true, + 'data' => [ + 'report' => $report, + 'username' => $username, + 'generated_at' => Carbon::now()->format('Y-m-d H:i:s') + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '生成周报失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 获取工时记录 + */ + public function getWorkLogs(Request $request): JsonResponse + { + $request->validate([ + 'username' => 'required|string', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + ]); + + try { + $username = $request->input('username'); + $startDate = Carbon::parse($request->input('start_date'))->startOfDay(); + $endDate = Carbon::parse($request->input('end_date'))->endOfDay(); + + $workLogs = $this->jiraService->getWorkLogs($username, $startDate, $endDate); + + return response()->json([ + 'success' => true, + 'data' => [ + 'work_logs' => $workLogs->values()->toArray(), + 'total_hours' => $workLogs->sum('hours'), + 'total_records' => $workLogs->count(), + 'date_range' => [ + 'start' => $startDate->format('Y-m-d'), + 'end' => $endDate->format('Y-m-d') + ] + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '获取工时记录失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 获取JIRA配置信息 + */ + public function getConfig(): JsonResponse + { + return response()->json([ + 'success' => true, + 'data' => [ + 'default_user' => config('jira.default_user', ''), + 'host' => config('jira.host', '') + ] + ]); + } + + /** + * 下载周报文件 + */ + public function downloadWeeklyReport(Request $request) + { + try { + $username = $request->input('username') ?: config('jira.default_user'); + + if (!$username) { + return response()->json([ + 'success' => false, + 'message' => '请提供用户名' + ], 400); + } + + $report = $this->jiraService->generateWeeklyReport($username); + $filename = sprintf('weekly_report_%s_%s.md', $username, Carbon::now()->subWeek()->format('Y-m-d')); + + return response($report) + ->header('Content-Type', 'text/markdown') + ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '下载周报失败: ' . $e->getMessage() + ], 500); + } + } + +} diff --git a/app/Http/Controllers/MessageDispatchController.php b/app/Http/Controllers/MessageDispatchController.php new file mode 100644 index 0000000..98159eb --- /dev/null +++ b/app/Http/Controllers/MessageDispatchController.php @@ -0,0 +1,207 @@ +messageDispatchService = $messageDispatchService; + } + + /** + * 获取可用的服务列表 + */ + public function getAvailableServices(): JsonResponse + { + try { + $services = $this->messageDispatchService->getAvailableServices(); + + return response()->json([ + 'success' => true, + 'data' => $services, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '获取服务列表失败', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * 获取可用的国家代码列表 + */ + public function getAvailableCountryCodes(): JsonResponse + { + try { + $codes = $this->messageDispatchService->getAvailableCountryCodes(); + + return response()->json([ + 'success' => true, + 'data' => $codes, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '获取国家代码列表失败', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * 获取可用的域名列表 + */ + public function getAvailableDomains(): JsonResponse + { + try { + $domains = $this->messageDispatchService->getAvailableDomains(); + + return response()->json([ + 'success' => true, + 'data' => $domains, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '获取域名列表失败', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * 查询异常的消息分发数据 + */ + public function getAbnormalDispatches(Request $request): JsonResponse + { + try { + $request->validate([ + 'msg_ids' => 'nullable|array', + 'msg_ids.*' => 'string', + 'request_status' => 'nullable|integer', + 'business_status' => 'nullable|integer', + 'target_services' => 'nullable|array', + 'target_services.*' => 'integer', + 'country_codes' => 'nullable|array', + 'domains' => 'nullable|array', + ]); + + $msgIds = $request->input('msg_ids'); + $requestStatus = $request->input('request_status'); + $businessStatus = $request->input('business_status'); + $targetServices = $request->input('target_services'); + $countryCodes = $request->input('country_codes'); + $domains = $request->input('domains'); + + $results = $this->messageDispatchService->getAbnormalDispatches( + $msgIds, + $requestStatus, + $businessStatus, + $targetServices, + $countryCodes, + $domains + ); + + return response()->json([ + 'success' => true, + 'data' => $results, + 'total' => count($results), + ]); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '查询异常消息失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 获取服务路由列表 + */ + public function getServiceRoutes(): JsonResponse + { + try { + $routes = $this->messageDispatchService->getServiceRoutes(); + + return response()->json([ + 'success' => true, + 'data' => $routes + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * 批量更新消息分发状态 + */ + public function batchUpdateDispatch(Request $request): JsonResponse + { + try { + $request->validate([ + 'updates' => 'required|array|min:1', + 'updates.*.id' => 'required', + 'updates.*.request_status' => 'nullable|in:0,1,2,3,4,5', + 'updates.*.business_status' => 'nullable|in:0,1,2', + 'updates.*.retry_count' => 'nullable|numeric|min:0', + 'updates.*.request_error_message' => 'nullable|string|max:1000', + 'updates.*.request_error_code' => 'nullable|string|max:50', + 'updates.*.business_error_message' => 'nullable|string|max:1000', + 'updates.*.business_error_code' => 'nullable|string|max:50', + 'updates.*.processing_time_ms' => 'nullable|numeric|min:0', + 'updates.*.country_code' => 'nullable|string|max:10', + 'updates.*.target_service' => 'nullable', + ]); + + $updates = $request->input('updates'); + + $results = $this->messageDispatchService->batchUpdateDispatch($updates); + + $successCount = count(array_filter($results, fn($r) => $r['success'])); + $failureCount = count($results) - $successCount; + + return response()->json([ + 'success' => true, + 'data' => [ + 'results' => $results, + 'summary' => [ + 'total' => count($results), + 'success' => $successCount, + 'failure' => $failureCount, + ] + ] + ]); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '批量更新失败: ' . $e->getMessage() + ], 500); + } + } +} + diff --git a/app/Http/Controllers/MessageSyncController.php b/app/Http/Controllers/MessageSyncController.php new file mode 100644 index 0000000..22bee36 --- /dev/null +++ b/app/Http/Controllers/MessageSyncController.php @@ -0,0 +1,322 @@ +messageSyncService = $messageSyncService; + $this->eventConsumerSyncService = $eventConsumerSyncService; + } + + + + /** + * 批量查询消息数据 + */ + public function queryMessages(Request $request): JsonResponse + { + try { + $request->validate([ + 'message_ids' => 'required|array|min:1', + 'message_ids.*' => 'required|string|max:255', + ]); + + $messageIds = array_filter(array_map('trim', $request->input('message_ids'))); + + if (empty($messageIds)) { + return response()->json([ + 'success' => false, + 'message' => '请提供有效的消息ID列表' + ], 400); + } + + // 验证消息ID格式 + $validationErrors = $this->messageSyncService->validateMessageIds($messageIds); + if (!empty($validationErrors)) { + return response()->json([ + 'success' => false, + 'message' => '消息ID格式验证失败', + 'errors' => $validationErrors + ], 400); + } + + // 查询消息数据 + $messages = $this->messageSyncService->getMessagesByIds($messageIds); + $stats = $this->messageSyncService->getMessageStats($messageIds); + + return response()->json([ + 'success' => true, + 'data' => [ + 'messages' => $messages, + 'stats' => $stats + ] + ]); + + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '查询消息失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 执行消息同步 + */ + public function syncMessages(Request $request): JsonResponse + { + try { + $request->validate([ + 'message_ids' => 'required|array|min:1', + 'message_ids.*' => 'required|string|max:255', + ]); + + $messageIds = array_filter(array_map('trim', $request->input('message_ids'))); + + if (empty($messageIds)) { + return response()->json([ + 'success' => false, + 'message' => '请提供有效的消息ID列表' + ], 400); + } + + // 验证消息ID格式 + $validationErrors = $this->messageSyncService->validateMessageIds($messageIds); + if (!empty($validationErrors)) { + return response()->json([ + 'success' => false, + 'message' => '消息ID格式验证失败', + 'errors' => $validationErrors + ], 400); + } + + // 执行同步 + $results = $this->messageSyncService->syncMessages($messageIds); + + // 统计同步结果 + $successCount = count(array_filter($results, fn($r) => $r['success'])); + $failureCount = count($results) - $successCount; + + return response()->json([ + 'success' => true, + 'data' => [ + 'results' => $results, + 'summary' => [ + 'total' => count($results), + 'success' => $successCount, + 'failure' => $failureCount + ] + ] + ]); + + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '消息同步失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 获取agent配置信息 + */ + public function getAgentConfig(): JsonResponse + { + try { + $config = [ + 'agent_url' => config('services.agent.url'), + 'timeout' => config('services.agent.timeout', 30), + ]; + + return response()->json([ + 'success' => true, + 'data' => $config + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '获取配置失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 测试数据库连接 + */ + public function testConnection(): JsonResponse + { + try { + // 测试crmslave数据库连接 + $connection = \DB::connection('crmslave'); + $connection->getPdo(); + + // 测试表是否存在 + $tableExists = $connection->getSchemaBuilder()->hasTable('system_publish_event'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'connection' => 'ok', + 'table_exists' => $tableExists + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '数据库连接测试失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 对比CRM和Agent的事件消费者消息同步状态 + */ + public function compareEventConsumerSync(Request $request): JsonResponse + { + try { + $request->validate([ + 'start_time' => 'nullable|date_format:Y-m-d H:i:s', + 'end_time' => 'nullable|date_format:Y-m-d H:i:s', + 'message_name' => 'nullable|string|max:255', + 'exclude_messages' => 'nullable|array', + 'exclude_messages.*' => 'string|max:255', + ]); + + $startTime = $request->input('start_time') + ? Carbon::createFromFormat('Y-m-d H:i:s', $request->input('start_time')) + : null; + + $endTime = $request->input('end_time') + ? Carbon::createFromFormat('Y-m-d H:i:s', $request->input('end_time')) + : null; + + $messageName = $request->input('message_name'); + $excludeMessages = $request->input('exclude_messages', []); + + $result = $this->eventConsumerSyncService->compareSyncStatus( + $startTime, + $endTime, + $excludeMessages, + $messageName + ); + + return response()->json([ + 'success' => true, + 'data' => $result + ]); + + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '对比消息同步状态失败: ' . $e->getMessage() + ], 500); + } + } + + /** + * 导出缺失的消息为Excel + */ + public function exportMissingMessages(Request $request): JsonResponse + { + try { + $request->validate([ + 'start_time' => 'nullable|date_format:Y-m-d H:i:s', + 'end_time' => 'nullable|date_format:Y-m-d H:i:s', + 'exclude_messages' => 'nullable|array', + 'exclude_messages.*' => 'string|max:255', + ]); + + $startTime = $request->input('start_time') + ? Carbon::createFromFormat('Y-m-d H:i:s', $request->input('start_time')) + : null; + + $endTime = $request->input('end_time') + ? Carbon::createFromFormat('Y-m-d H:i:s', $request->input('end_time')) + : null; + + $excludeMessages = $request->input('exclude_messages', []); + + $result = $this->eventConsumerSyncService->compareSyncStatus( + $startTime, + $endTime, + $excludeMessages + ); + + $missingMessages = $result['missing_messages']; + + if (empty($missingMessages)) { + return response()->json([ + 'success' => true, + 'message' => '没有缺失的消息', + 'data' => [] + ]); + } + + // 生成CSV数据 + $csv = "msg_id,event_name,msg_body,created,updated\n"; + foreach ($missingMessages as $msg) { + $csv .= sprintf( + '"%s","%s","%s","%s","%s"' . "\n", + $msg['msg_id'], + $msg['event_name'], + str_replace('"', '""', $msg['msg_body']), + $msg['created'], + $msg['updated'] + ); + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'csv' => $csv, + 'filename' => 'missing_messages_' . date('YmdHis') . '.csv', + 'count' => count($missingMessages) + ] + ]); + + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '导出消息失败: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..0a38c0d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,7 +11,8 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + // 注册 JIRA 服务 + $this->app->singleton(\App\Services\JiraService::class); } /** diff --git a/app/Providers/ClientServiceProvider.php b/app/Providers/ClientServiceProvider.php new file mode 100644 index 0000000..5a2ce1f --- /dev/null +++ b/app/Providers/ClientServiceProvider.php @@ -0,0 +1,33 @@ +app->singleton(AgentClient::class, function () { + return new AgentClient(); + }); + + $this->app->singleton(MonoClient::class, function () { + return new MonoClient(); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} + diff --git a/app/Services/EnvService.php b/app/Services/EnvService.php index ccd18c3..321f92f 100644 --- a/app/Services/EnvService.php +++ b/app/Services/EnvService.php @@ -12,16 +12,23 @@ class EnvService { private string $projectsPath; private string $envStoragePath; + private string $backupStoragePath; public function __construct() { - $this->projectsPath = '/var/www/Project'; + $this->projectsPath = '/home/tradewind/Projects'; $this->envStoragePath = storage_path('app/env'); - + $this->backupStoragePath = storage_path('app/env/backups'); + // 确保env存储目录存在 if (!File::exists($this->envStoragePath)) { File::makeDirectory($this->envStoragePath, 0755, true); } + + // 确保备份存储目录存在 + if (!File::exists($this->backupStoragePath)) { + File::makeDirectory($this->backupStoragePath, 0755, true); + } } /** @@ -55,7 +62,7 @@ class EnvService public function getProjectEnvs(string $projectName): array { $projectEnvPath = $this->envStoragePath . '/' . $projectName; - + if (!File::exists($projectEnvPath)) { return []; } @@ -85,13 +92,13 @@ class EnvService { $projectEnvPath = $this->envStoragePath . '/' . $projectName; - + if (!File::exists($projectEnvPath)) { File::makeDirectory($projectEnvPath, 0755, true); } $filePath = $projectEnvPath . '/' . $env . '.env'; - + return File::put($filePath, $content) !== false; } @@ -125,10 +132,9 @@ class EnvService $envContent = $this->getEnvContent($projectName, $env); $targetEnvPath = $projectPath . '/.env'; - // 备份现有的.env文件 + // 备份现有的.env文件到当前项目的storage目录 if (File::exists($targetEnvPath)) { - $backupPath = $targetEnvPath . '.backup.' . Carbon::now()->format('Y-m-d-H-i-s'); - File::copy($targetEnvPath, $backupPath); + $this->backupCurrentEnv($projectName); } return File::put($targetEnvPath, $envContent) !== false; @@ -172,7 +178,7 @@ class EnvService { $projectEnvPath = $this->projectsPath . '/' . $projectName . '/.env'; - + if (!File::exists($projectEnvPath)) { return null; } @@ -221,4 +227,113 @@ class EnvService return File::put($filePath, $defaultContent) !== false; } + + /** + * 备份项目当前的.env文件到storage目录 + */ + private function backupCurrentEnv(string $projectName): bool + { + $projectPath = $this->projectsPath . '/' . $projectName; + $currentEnvPath = $projectPath . '/.env'; + + if (!File::exists($currentEnvPath)) { + return true; // 没有.env文件,无需备份 + } + + // 创建项目备份目录 + $projectBackupPath = $this->backupStoragePath . '/' . $projectName; + if (!File::exists($projectBackupPath)) { + File::makeDirectory($projectBackupPath, 0755, true); + } + + // 生成备份文件名 + $timestamp = Carbon::now()->format('Y-m-d-H-i-s'); + $backupFileName = "env-backup-{$timestamp}.env"; + $backupPath = $projectBackupPath . '/' . $backupFileName; + + return File::copy($currentEnvPath, $backupPath); + } + + /** + * 获取项目的所有备份文件 + */ + public function getProjectBackups(string $projectName): array + { + $projectBackupPath = $this->backupStoragePath . '/' . $projectName; + + if (!File::exists($projectBackupPath)) { + return []; + } + + $backups = []; + $files = File::files($projectBackupPath); + + foreach ($files as $file) { + if ($file->getExtension() === 'env' && str_starts_with($file->getFilename(), 'env-backup-')) { + $backups[] = [ + 'name' => $file->getFilenameWithoutExtension(), + 'file_path' => $file->getPathname(), + 'size' => $file->getSize(), + 'created_at' => Carbon::createFromTimestamp($file->getMTime())->format('Y-m-d H:i:s') + ]; + } + } + + // 按创建时间倒序排列 + usort($backups, function ($a, $b) { + return strcmp($b['created_at'], $a['created_at']); + }); + + return $backups; + } + + /** + * 恢复备份文件到项目 + */ + public function restoreBackup(string $projectName, string $backupName): bool + { + $projectPath = $this->projectsPath . '/' . $projectName; + + if (!File::exists($projectPath)) { + throw new InvalidArgumentException("项目不存在: {$projectName}"); + } + + $backupPath = $this->backupStoragePath . '/' . $projectName . '/' . $backupName . '.env'; + + if (!File::exists($backupPath)) { + throw new InvalidArgumentException("备份文件不存在: {$backupName}"); + } + + $targetEnvPath = $projectPath . '/.env'; + + // 在恢复前先备份当前的.env文件 + if (File::exists($targetEnvPath)) { + $this->backupCurrentEnv($projectName); + } + + $backupContent = File::get($backupPath); + return File::put($targetEnvPath, $backupContent) !== false; + } + + /** + * 删除备份文件 + */ + public function deleteBackup(string $projectName, string $backupName): bool + { + $backupPath = $this->backupStoragePath . '/' . $projectName . '/' . $backupName . '.env'; + + if (!File::exists($backupPath)) { + return true; // 文件不存在,视为删除成功 + } + + return File::delete($backupPath); + } + + /** + * 获取备份存储路径 + */ + public function getBackupStoragePath(): string + { + return $this->backupStoragePath; + } } diff --git a/app/Services/EventConsumerSyncService.php b/app/Services/EventConsumerSyncService.php new file mode 100644 index 0000000..411f01e --- /dev/null +++ b/app/Services/EventConsumerSyncService.php @@ -0,0 +1,271 @@ +table('crm_api.crm_event_consumer'); + + // 只查询Agent监听的topic + $query->whereIn('event_name', self::AGENT_LISTENED_TOPICS); + + if ($startTime) { + $query->where('created', '>=', $startTime); + } + + if ($endTime) { + $query->where('created', '<=', $endTime); + } + + if (!empty($excludeMessages)) { + $query->whereNotIn('event_name', $excludeMessages); + } + + $messages = $query->select([ + 'msg_id', + 'event_name', + 'msg_body', + 'created', + 'updated' + ])->get(); + + return $messages->map(function ($msg) { + return [ + 'msg_id' => $msg->msg_id, + 'event_name' => $msg->event_name, + 'msg_body' => $msg->msg_body, + 'created' => $msg->created, + 'updated' => $msg->updated, + ]; + })->toArray(); + } catch (\Exception $e) { + throw new \RuntimeException('查询CRM事件消费者表失败: ' . $e->getMessage()); + } + } + + /** + * 查询Agent事件消费者表的消息数据(通过msg_id列表,分批查询) + * @param array $msgIds msg_id列表 + * @param int $batchSize 每批查询的数量,默认1000 + */ + public function getAgentEventConsumersByMsgIds(array $msgIds, int $batchSize = 1000): array { + try { + if (empty($msgIds)) { + return []; + } + + $messages = []; + + // 分批查询,避免一次性查询过多数据导致慢查询 + $batches = array_chunk($msgIds, $batchSize); + + foreach ($batches as $batch) { + $batchMessages = DB::connection('agentslave') + ->table('crm_event_consumer') + ->whereIn('msg_id', $batch) + ->select([ + 'msg_id', + 'event_name', + 'msg_body', + 'created', + 'updated' + ])->get(); + + foreach ($batchMessages as $msg) { + $messages[] = [ + 'msg_id' => $msg->msg_id, + 'event_name' => $msg->event_name, + 'msg_body' => $msg->msg_body, + 'created' => $msg->created, + 'updated' => $msg->updated, + ]; + } + } + + return $messages; + } catch (\Exception $e) { + throw new \RuntimeException('查询Agent事件消费者表失败: ' . $e->getMessage()); + } + } + + /** + * 对比CRM和Agent的消息,找出缺失的消息 + * 策略:先查CRM的msg_id,然后用这些msg_id到Agent中查询,避免时间差异导致的缺失 + */ + public function compareSyncStatus( + ?Carbon $startTime = null, + ?Carbon $endTime = null, + array $excludeMessages = [], + ?string $messageName = null + ): array { + // 1. 先查询CRM中的所有消息 + $crmMessages = $this->getCrmEventConsumers($startTime, $endTime, $excludeMessages); + + // 如果指定了消息名称,则进一步过滤 + if ($messageName) { + $crmMessages = array_filter($crmMessages, function ($msg) use ($messageName) { + return $msg['event_name'] === $messageName; + }); + } + + $crmMsgIds = array_column($crmMessages, 'msg_id'); + + // 2. 用CRM的msg_id到Agent中查询(不受时间限制) + $agentMessages = $this->getAgentEventConsumersByMsgIds($crmMsgIds); + $agentMsgIds = array_column($agentMessages, 'msg_id'); + + // 3. 找出在CRM中但不在Agent中的消息 + $missingMsgIds = array_diff($crmMsgIds, $agentMsgIds); + + $missingMessages = array_filter($crmMessages, function ($msg) use ($missingMsgIds) { + return in_array($msg['msg_id'], $missingMsgIds); + }); + + // 4. 按topic统计缺失消息数量 + $missingByTopic = $this->groupMissingMessagesByTopic($missingMessages); + + return [ + 'crm_total' => count($crmMessages), + 'agent_total' => count($agentMessages), + 'missing_count' => count($missingMessages), + 'sync_rate' => count($crmMessages) > 0 + ? round((count($crmMessages) - count($missingMessages)) / count($crmMessages) * 100, 2) + : 100, + 'missing_messages' => array_values($missingMessages), + 'missing_by_topic' => $missingByTopic, + 'summary' => [ + 'start_time' => $startTime?->toDateTimeString(), + 'end_time' => $endTime?->toDateTimeString(), + 'message_name' => $messageName, + 'excluded_messages' => $excludeMessages, + ] + ]; + } + + /** + * 获取Agent监听的所有topic列表 + */ + public function getAgentListenedTopics(): array { + return self::AGENT_LISTENED_TOPICS; + } + + /** + * 按topic统计缺失消息数量 + */ + private function groupMissingMessagesByTopic(array $missingMessages): array { + $grouped = []; + + foreach ($missingMessages as $msg) { + $topic = $msg['event_name'] ?? 'unknown'; + + if (!isset($grouped[$topic])) { + $grouped[$topic] = [ + 'topic' => $topic, + 'count' => 0, + 'messages' => [] + ]; + } + + $grouped[$topic]['count']++; + $grouped[$topic]['messages'][] = $msg['msg_id']; + } + + // 按缺失数量降序排序 + uasort($grouped, function ($a, $b) { + return $b['count'] - $a['count']; + }); + + return array_values($grouped); + } +} + diff --git a/app/Services/JiraService.php b/app/Services/JiraService.php new file mode 100644 index 0000000..6e2fcbb --- /dev/null +++ b/app/Services/JiraService.php @@ -0,0 +1,742 @@ +config = config('jira'); + $this->initializeJiraClient(); + } + + private function initializeJiraClient(): void + { + $jiraConfig = new ArrayConfiguration([ + 'jiraHost' => $this->config['host'], + 'jiraUser' => $this->config['username'], + 'jiraPassword' => $this->config['auth_type'] === 'token' + ? $this->config['api_token'] + : $this->config['password'], + 'timeout' => $this->config['timeout'], + ]); + + $this->issueService = new IssueService($jiraConfig); + } + + + /** + * 按项目组织任务数据 + */ + private function organizeIssuesByProject(array $issues): Collection + { + $organized = collect(); + + foreach ($issues as $issue) { + $projectKey = $issue->fields->project->key; + $isSubtask = $issue->fields->issuetype->subtask ?? false; + + if (!$organized->has($projectKey)) { + $organized->put($projectKey, [ + 'name' => $issue->fields->project->name, + 'tasks' => collect(), + ]); + } + + if ($isSubtask && isset($issue->fields->parent)) { + // 子任务 + $parentKey = $issue->fields->parent->key; + $this->addSubtask($organized[$projectKey]['tasks'], $parentKey, $issue); + } else { + // 主任务 + $this->addMainTask($organized[$projectKey]['tasks'], $issue); + } + } + + return $organized; + } + + /** + * 获取单个任务的详细信息 + */ + private function getIssueDetails(string $issueKey): ?object + { + try { + return $this->issueService->get($issueKey, [ + 'summary', + 'status', + 'project', + 'issuetype' + ]); + } catch (JiraException) { + return null; + } + } + + private function addMainTask(Collection $tasks, $issue): void + { + $tasks->put($issue->key, [ + 'key' => $issue->key, + 'summary' => $issue->fields->summary, + 'url' => $this->config['host'] . '/browse/' . $issue->key, + 'subtasks' => collect(), + ]); + } + + private function addSubtask(Collection $tasks, string $parentKey, $issue): void + { + if (!$tasks->has($parentKey)) { + // 获取父任务的真实信息 + $parentDetails = $this->getIssueDetails($parentKey); + $parentSummary = $parentDetails ? $parentDetails->fields->summary : '父任务'; + + $tasks->put($parentKey, [ + 'key' => $parentKey, + 'summary' => $parentSummary, + 'url' => $this->config['host'] . '/browse/' . $parentKey, + 'subtasks' => collect(), + ]); + } + + $tasks[$parentKey]['subtasks']->put($issue->key, [ + 'key' => $issue->key, + 'summary' => $issue->fields->summary, + 'url' => $this->config['host'] . '/browse/' . $issue->key, + 'created' => $issue->fields->created ?? null, + ]); + } + + /** + * 获取未来一周的任务 + */ + public function getNextWeekTasks(?string $username = null): Collection + { + $username = $username ?: $this->config['default_user']; + + if (!$username) { + throw new \InvalidArgumentException('用户名不能为空'); + } + + // 查询分配给用户且未完成的任务(不包括子任务) + $jql = sprintf( + 'assignee = "%s" AND status != "Done" AND issuetype != "Sub-task" ORDER BY created ASC', + $username + ); + + try { + $issues = $this->issueService->search($jql, 0, 50, [ + 'summary', + 'status', + 'project', + 'issuetype', + 'created' + ]); + + if (!empty($issues->issues)) { + return $this->organizeIssuesByProject($issues->issues); + } + } catch (JiraException $e) { + throw new \RuntimeException('获取未来任务失败: ' . $e->getMessage()); + } + + return collect(); + } + + /** + * 生成 Markdown 格式的周报 + */ + public function generateWeeklyReport(?string $username = null): string + { + $username = $username ?: $this->config['default_user']; + + // 获取上周的工时记录 + $now = Carbon::now(); + $startOfWeek = $now->copy()->subWeek()->startOfWeek(); + $endOfWeek = $now->copy()->subWeek()->endOfWeek(); + + $workLogs = $this->getWorkLogs($username, $startOfWeek, $endOfWeek); + $organizedTasks = $this->organizeTasksForReport($workLogs); + + $nextWeekTasks = $this->getNextWeekTasks($username); + + $markdown = "# 过去一周的任务\n\n"; + + if ($organizedTasks->isEmpty()) { + $markdown .= "本周暂无工时记录的任务。\n\n"; + } else { + // 按Sprint分类的需求 + if ($organizedTasks->has('sprints') && $organizedTasks['sprints']->isNotEmpty()) { + foreach ($organizedTasks['sprints'] as $sprintName => $tasks) { + $markdown .= "### {$sprintName}\n"; + foreach ($tasks as $task) { + $checkbox = $this->isTaskCompleted($task['status']) ? '[x]' : '[ ]'; + $markdown .= "- {$checkbox} [{$task['key']}]({$task['url']}) {$task['summary']}\n"; + + if ($task['subtasks']->isNotEmpty()) { + // 按创建时间排序子任务 + $sortedSubtasks = $task['subtasks']->sortBy('created'); + foreach ($sortedSubtasks as $subtask) { + $subtaskCheckbox = $this->isTaskCompleted($subtask['status']) ? '[x]' : '[ ]'; + $markdown .= " - {$subtaskCheckbox} [{$subtask['key']}]({$subtask['url']}) {$subtask['summary']}\n"; + } + } + } + $markdown .= "\n"; + } + } + + // 单独列出的任务 + if ($organizedTasks->has('tasks') && $organizedTasks['tasks']->isNotEmpty()) { + $markdown .= "### 任务\n"; + foreach ($organizedTasks['tasks'] as $task) { + $checkbox = $this->isTaskCompleted($task['status']) ? '[x]' : '[ ]'; + $markdown .= "- {$checkbox} [{$task['key']}]({$task['url']}) {$task['summary']}\n"; + + if ($task['subtasks']->isNotEmpty()) { + // 按创建时间排序子任务 + $sortedSubtasks = $task['subtasks']->sortBy('created'); + foreach ($sortedSubtasks as $subtask) { + $subtaskCheckbox = $this->isTaskCompleted($subtask['status']) ? '[x]' : '[ ]'; + $markdown .= " - {$subtaskCheckbox} [{$subtask['key']}]({$subtask['url']}) {$subtask['summary']}\n"; + } + } + } + $markdown .= "\n"; + } + + // 按发现阶段分类的Bug + if ($organizedTasks->has('bugs') && $organizedTasks['bugs']->isNotEmpty()) { + foreach ($organizedTasks['bugs'] as $stage => $bugs) { + $markdown .= "### {$stage}\n"; + + // 按父任务分组Bug + $groupedBugs = collect($bugs)->groupBy(function ($bug) { + return $bug['parent_key'] ?? 'standalone'; + }); + + foreach ($groupedBugs as $parentKey => $bugGroup) { + if ($parentKey === 'standalone') { + // 独立的Bug + foreach ($bugGroup as $bug) { + $checkbox = $this->isTaskCompleted($bug['status']) ? '[x]' : '[ ]'; + $summary = $this->cleanSummary($bug['summary']); + // 只标记非代码错误的Bug类型,并附加修复描述 + $bugTypeLabel = ''; + if ($bug['bug_type'] && $bug['bug_type'] !== '代码错误') { + $bugTypeLabel = "\n - {$bug['bug_type']}"; + if ($bug['bug_description']) { + $bugTypeLabel .= ";{$bug['bug_description']}"; + } + } + $markdown .= "- {$checkbox} [{$bug['key']}]({$bug['url']}) {$summary}{$bugTypeLabel}\n"; + } + } else { + // 有父任务的Bug + $firstBug = $bugGroup->first(); + $markdown .= "- [x] {$firstBug['parent_summary']}\n"; + foreach ($bugGroup as $bug) { + $checkbox = $this->isTaskCompleted($bug['status']) ? '[x]' : '[ ]'; + $summary = $this->cleanSummary($bug['summary']); + // 只标记非代码错误的Bug类型,并附加修复描述 + $bugTypeLabel = ''; + if ($bug['bug_type'] && $bug['bug_type'] !== '代码错误') { + $bugTypeLabel = "\n - {$bug['bug_type']}"; + if ($bug['bug_description']) { + $bugTypeLabel .= ";{$bug['bug_description']}"; + } + } + $markdown .= " - {$checkbox} [{$bug['key']}]({$bug['url']}) {$summary}{$bugTypeLabel}\n"; + } + } + } + $markdown .= "\n"; + } + } + } + + $markdown .= "\n# 未来一周的任务\n\n"; + + foreach ($nextWeekTasks as $project) { + foreach ($project['tasks'] as $task) { + $markdown .= "- [ ] [{$task['key']}]({$task['url']}) {$task['summary']}\n"; + } + } + + return $markdown; + } + + /** + * 获取指定日期范围内的工时记录 + */ + public function getWorkLogs(string $username, Carbon $startDate, Carbon $endDate): Collection + { + // 标准工时查询 - 注意:某些JIRA版本可能不支持worklogAuthor和worklogDate + $jql = sprintf( + 'worklogAuthor = "%s" AND worklogDate >= "%s" AND worklogDate <= "%s" ORDER BY updated DESC', + $username, + $startDate->format('Y-m-d'), + $endDate->format('Y-m-d') + ); + + try { + $issues = $this->issueService->search($jql, 0, 100, [ + 'summary', + 'project', + 'worklog', + 'parent', + 'issuetype', + 'status', + 'created', + 'fixVersions', + 'labels', + 'customfield_10004', // Sprint字段 + 'customfield_10900', // Bug发现阶段 + 'customfield_12700', // Bug错误类型 + 'customfield_10115', // Bug修复描述 + 'customfield_14305', // 需求类型 + ]); + + if (!empty($issues->issues)) { + $workLogs = $this->extractWorkLogs($issues->issues, $username, $startDate, $endDate); + + if ($workLogs->isNotEmpty()) { + return $workLogs; + } + } + } catch (JiraException $e) { + throw new \RuntimeException('获取工时记录失败: ' . $e->getMessage()); + } + // 如果所有查询都没有结果,返回空集合 + return collect(); + } + + /** + * 提取工时记录 + */ + private function extractWorkLogs(array $issues, string $username, Carbon $startDate, Carbon $endDate): Collection + { + $workLogs = collect(); + + foreach ($issues as $issue) { + try { + $worklogData = $this->issueService->getWorklog($issue->key); + + foreach ($worklogData->worklogs as $worklog) { + try { + $worklogDate = Carbon::parse($worklog->started); + + // 处理 author 可能是数组或对象的情况 + $authorName = is_array($worklog->author) ? ($worklog->author['name'] ?? '') : ($worklog->author->name ?? ''); + + if (!empty($authorName) && $authorName === $username && + $worklogDate->between($startDate, $endDate)) { + + // 获取父任务信息 + $parentTask = null; + if (isset($issue->fields->parent)) { + $parentTask = [ + 'key' => $issue->fields->parent->key ?? '', + 'summary' => $issue->fields->parent->fields->summary ?? '', + ]; + } + + // 提取Sprint信息 + $sprint = $this->extractSprintInfo($issue); + + // 提取Bug相关信息 + $bugStage = $this->extractBugStage($issue); + $bugType = $this->extractBugType($issue); + $bugDescription = $this->extractBugDescription($issue); + + // 提取需求类型 + $requirementType = $this->extractRequirementType($issue); + + $workLogs->push([ + 'id' => $worklog->id ?? '', + 'project' => $issue->fields->project->name ?? '', + 'project_key' => $issue->fields->project->key ?? '', + 'issue_key' => $issue->key, + 'issue_summary' => $issue->fields->summary ?? '', + '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, + 'parent_task' => $parentTask, + 'sprint' => $sprint, + 'bug_stage' => $bugStage, + 'bug_type' => $bugType, + 'bug_description' => $bugDescription, + 'requirement_type' => $requirementType, + 'date' => $worklogDate->format('Y-m-d'), + 'time' => $worklogDate->format('H:i'), + 'hours' => round(($worklog->timeSpentSeconds ?? 0) / 3600, 2), + 'time_spent_seconds' => $worklog->timeSpentSeconds ?? 0, + 'time_spent' => $worklog->timeSpent ?? '', + 'comment' => $worklog->comment ?? '', + 'author' => [ + 'name' => $authorName, + 'display_name' => is_array($worklog->author) ? ($worklog->author['displayName'] ?? '') : ($worklog->author->displayName ?? ''), + 'email' => is_array($worklog->author) ? ($worklog->author['emailAddress'] ?? '') : ($worklog->author->emailAddress ?? ''), + ], + 'created' => isset($worklog->created) ? Carbon::parse($worklog->created)->format('Y-m-d H:i:s') : '', + 'updated' => isset($worklog->updated) ? Carbon::parse($worklog->updated)->format('Y-m-d H:i:s') : '', + 'started' => $worklogDate->format('Y-m-d H:i:s'), + ]); + } + } catch (\Exception) { + // 跳过有问题的工时记录 + continue; + } + } + } catch (JiraException) { + // 跳过无法获取工时记录的任务 + continue; + } + } + + return $workLogs->sortByDesc('date'); + } + + /** + * 提取Sprint信息 + */ + private function extractSprintInfo($issue): ?string + { + // 尝试从customfield_10004获取Sprint信息 + if (isset($issue->fields->customFields['customfield_10004'])) { + $sprintField = $issue->fields->customFields['customfield_10004']; + + // 处理数组情况 + if (is_array($sprintField) && !empty($sprintField)) { + $lastSprint = end($sprintField); + if (is_string($lastSprint)) { + // 解析Sprint字符串,格式通常为: com.atlassian.greenhopper.service.sprint.Sprint@xxx[name=十月中需求,...] + if (preg_match('/name=([^,\]]+)/', $lastSprint, $matches)) { + return $matches[1]; + } + } elseif (is_object($lastSprint) && isset($lastSprint->name)) { + return $lastSprint->name; + } + } + + // 处理对象情况 + if (is_object($sprintField) && isset($sprintField->name)) { + return $sprintField->name; + } + + // 处理字符串情况 + if (is_string($sprintField)) { + 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)) { + return $issue->fields->fixVersions[0]->name ?? null; + } + + // 尝试从summary中提取Sprint信息(如果summary包含【十月中需求】这样的标记) + if (isset($issue->fields->summary)) { + $summary = $issue->fields->summary; + // 匹配【xxx需求】或【xxx】格式 + if (preg_match('/【([^】]*需求)】/', $summary, $matches)) { + return $matches[1]; + } + } + + return null; + } + + /** + * 提取Bug发现阶段 + */ + private function extractBugStage($issue): ?string + { + // 从customfield_10900获取Bug阶段 + if (isset($issue->fields->customFields['customfield_10900'])) { + $stage = $issue->fields->customFields['customfield_10900']; + + // 处理对象类型 + if (is_object($stage) && isset($stage->value)) { + $stageValue = $stage->value; + } elseif (is_string($stage)) { + $stageValue = $stage; + } else { + $stageValue = null; + } + + if ($stageValue && !empty($stageValue)) { + // 标准化阶段名称 + if (str_contains($stageValue, 'SIT') || str_contains($stageValue, 'sit') || $stageValue === '测试阶段') { + return 'SIT环境BUG'; + } + if (str_contains($stageValue, '生产') || str_contains($stageValue, 'PROD') || str_contains($stageValue, 'prod') || $stageValue === '生产环境') { + return '生产环境BUG'; + } + if (str_contains($stageValue, 'UAT') || str_contains($stageValue, 'uat')) { + return 'UAT环境BUG'; + } + // 如果不匹配标准格式,直接返回原值 + return $stageValue . 'BUG'; + } + } + + // 从labels中提取Bug阶段 + if (isset($issue->fields->labels) && is_array($issue->fields->labels)) { + foreach ($issue->fields->labels as $label) { + if (str_contains($label, 'SIT') || str_contains($label, 'sit')) { + return 'SIT环境BUG'; + } + if (str_contains($label, '生产') || str_contains($label, 'PROD') || str_contains($label, 'prod')) { + return '生产环境BUG'; + } + if (str_contains($label, 'UAT') || str_contains($label, 'uat')) { + return 'UAT环境BUG'; + } + } + } + + return null; + } + + /** + * 提取Bug错误类型 + */ + private function extractBugType($issue): ?string + { + // 从customfield_12700获取Bug类型 + if (isset($issue->fields->customFields['customfield_12700'])) { + $type = $issue->fields->customFields['customfield_12700']; + + // 处理对象类型 + if (is_object($type) && isset($type->value)) { + return $type->value; + } elseif (is_string($type) && !empty($type)) { + return $type; + } + } + + // 从labels中提取Bug类型 + if (isset($issue->fields->labels) && is_array($issue->fields->labels)) { + $bugTypes = ['需求未说明', '沟通问题', '接口文档未说明', '数据问题', '配置问题', '环境问题']; + foreach ($issue->fields->labels as $label) { + foreach ($bugTypes as $bugType) { + if (str_contains($label, $bugType)) { + return $bugType; + } + } + } + } + + return null; + } + + /** + * 提取Bug修复描述 + */ + private function extractBugDescription($issue): ?string + { + // 从customfield_10115获取Bug修复描述 + if (isset($issue->fields->customFields['customfield_10115'])) { + $description = $issue->fields->customFields['customfield_10115']; + + if (is_string($description) && !empty($description)) { + return $description; + } + } + + return null; + } + + /** + * 提取需求类型 + */ + private function extractRequirementType($issue): ?string + { + // 从customfield_14305获取需求类型 + if (isset($issue->fields->customFields['customfield_14305'])) { + $type = $issue->fields->customFields['customfield_14305']; + if (is_array($type) && !empty($type)) { + $firstType = $type[0]; + if (is_object($firstType) && isset($firstType->value)) { + return $firstType->value; + } + } elseif (is_object($type) && isset($type->value)) { + return $type->value; + } elseif (is_string($type)) { + return $type; + } + } + + return null; + } + + /** + * 清理摘要中的图片链接 + */ + private function cleanSummary(string $summary): string + { + // 移除 Jira 图片标记 !image-xxx.png! 和 !https://xxx.png! + $summary = preg_replace('/!([^!]+\.(png|jpg|jpeg|gif|bmp))!/i', '', $summary); + // 移除多余的空格和换行 + $summary = preg_replace('/\s+/', ' ', $summary); + return trim($summary); + } + + /** + * 判断任务状态是否应该标记为完成 + */ + private function isTaskCompleted(string $status): bool + { + // 不打勾的状态(未完成状态) + $incompleteStatuses = [ + '开发中', + '需求已排期', + '需求已评审', + 'In Progress', + 'To Do', + 'Open', + 'Reopened', + 'In Review', + 'Code Review', + 'Testing', + 'Ready for Testing' + ]; + + return !in_array($status, $incompleteStatuses, true); + } + + /** + * 组织任务数据用于周报生成 + */ + private function organizeTasksForReport(Collection $workLogs): Collection + { + $organized = collect([ + 'sprints' => collect(), + 'tasks' => collect(), + 'bugs' => collect(), + ]); + + $processedIssues = collect(); + + foreach ($workLogs as $workLog) { + $issueKey = $workLog['issue_key']; + $issueType = $workLog['issue_type'] ?? 'Unknown'; + + // 避免重复处理同一个任务 + if ($processedIssues->has($issueKey)) { + continue; + } + $processedIssues->put($issueKey, true); + + // 判断是否为Bug(通过issuetype判断) + $isBug = in_array($issueType, ['Bug', 'BUG', 'bug', '缺陷', 'Defect']); + + // 判断是否为需求(Story类型) + $isStory = in_array($issueType, ['Story', 'story', '需求']); + + // 判断是否为子任务 + $isSubtask = in_array($issueType, ['Sub-task', 'sub-task', '子任务']); + + if ($isBug && $workLog['bug_stage']) { + // Bug按发现阶段分类 + $stage = $workLog['bug_stage']; + if (!$organized['bugs']->has($stage)) { + $organized['bugs']->put($stage, collect()); + } + + $bugData = [ + 'key' => $workLog['issue_key'], + 'summary' => $workLog['issue_summary'], + 'url' => $workLog['issue_url'], + 'status' => $workLog['issue_status'], + 'bug_type' => $workLog['bug_type'], + 'bug_description' => $workLog['bug_description'], + ]; + + // 如果有父任务,添加父任务信息 + if ($workLog['parent_task']) { + $bugData['parent_key'] = $workLog['parent_task']['key']; + $bugData['parent_summary'] = $workLog['parent_task']['summary']; + } + + $organized['bugs'][$stage]->push($bugData); + } elseif (($isStory || $isSubtask) && $workLog['sprint']) { + // Story类型或子任务,且有Sprint的,按Sprint分类(需求) + $sprintName = $workLog['sprint']; + if (!$organized['sprints']->has($sprintName)) { + $organized['sprints']->put($sprintName, collect()); + } + + $this->addTaskToSprintOrTaskList($organized['sprints'][$sprintName], $workLog); + } else { + // 其他任务单独列出(非Story/子任务类型或没有Sprint的) + $this->addTaskToSprintOrTaskList($organized['tasks'], $workLog); + } + } + + return $organized; + } + + /** + * 添加任务到Sprint或任务列表 + */ + private function addTaskToSprintOrTaskList(Collection $taskList, array $workLog): void + { + $status = $workLog['issue_status'] ?? 'Unknown'; + + if ($workLog['parent_task']) { + // 子任务 + $parentKey = $workLog['parent_task']['key']; + + if (!$taskList->has($parentKey)) { + // 获取父任务的真实信息 + $parentDetails = $this->getIssueDetails($parentKey); + $parentSummary = $parentDetails ? $parentDetails->fields->summary : $workLog['parent_task']['summary']; + $parentStatus = $parentDetails ? $parentDetails->fields->status->name : 'Unknown'; + + $taskList->put($parentKey, [ + 'key' => $parentKey, + 'summary' => $parentSummary, + 'url' => $this->config['host'] . '/browse/' . $parentKey, + 'status' => $parentStatus, + 'subtasks' => collect(), + ]); + } + + $taskList[$parentKey]['subtasks']->put($workLog['issue_key'], [ + 'key' => $workLog['issue_key'], + 'summary' => $workLog['issue_summary'], + 'url' => $workLog['issue_url'], + 'status' => $status, + 'created' => $workLog['issue_created'], + ]); + } else { + // 主任务 + if (!$taskList->has($workLog['issue_key'])) { + $taskList->put($workLog['issue_key'], [ + 'key' => $workLog['issue_key'], + 'summary' => $workLog['issue_summary'], + 'url' => $workLog['issue_url'], + 'status' => $status, + 'subtasks' => collect(), + ]); + } + } + } + + +} diff --git a/app/Services/MessageDispatchService.php b/app/Services/MessageDispatchService.php new file mode 100644 index 0000000..9e162ac --- /dev/null +++ b/app/Services/MessageDispatchService.php @@ -0,0 +1,349 @@ +monoClient = $monoClient; + } + + /** + * 查询异常的消息分发数据 + */ + public function getAbnormalDispatches( + ?array $msgIds = null, + ?int $requestStatus = null, + ?int $businessStatus = null, + ?array $targetServices = null, + ?array $countryCodes = null, + ?array $domains = null + ): array + { + try { + $query = DB::connection('monoslave') + ->table('message_dispatch as md') + ->join('message_consumer as mc', 'mc.msg_id', '=', 'md.msg_id') + ->leftJoin('service_routes as sr', 'md.target_service', '=', 'sr.id') + ->where('md.request_status', '<>', 5) + ->where(function ($q) { + $q->where('md.request_status', '<>', 1) + ->orWhere('md.business_status', '<>', 1); + }) + ->where('md.created', '<', Carbon::now()->subMinutes(5)); + + // 筛选条件 + if ($msgIds && count($msgIds) > 0) { + $query->whereIn('md.msg_id', $msgIds); + } + + if ($requestStatus !== null) { + $query->where('md.request_status', $requestStatus); + } + + if ($businessStatus !== null) { + $query->where('md.business_status', $businessStatus); + } + + if ($targetServices && count($targetServices) > 0) { + $query->whereIn('md.target_service', $targetServices); + } + + if ($countryCodes && count($countryCodes) > 0) { + $query->where(function ($q) use ($countryCodes) { + $hasNull = in_array('', $countryCodes) || in_array(null, $countryCodes); + $nonNullCodes = array_filter($countryCodes, fn($c) => $c !== '' && $c !== null); + + if ($hasNull) { + $q->whereNull('sr.country_code'); + if (count($nonNullCodes) > 0) { + $q->orWhereIn('sr.country_code', $nonNullCodes); + } + } else { + $q->whereIn('sr.country_code', $nonNullCodes); + } + }); + } + + if ($domains && count($domains) > 0) { + $query->where(function ($q) use ($domains) { + $hasNull = in_array('', $domains) || in_array(null, $domains); + $nonNullDomains = array_filter($domains, fn($d) => $d !== '' && $d !== null); + + if ($hasNull) { + $q->whereNull('sr.service_endpoint'); + if (count($nonNullDomains) > 0) { + $q->orWhere(function ($subQ) use ($nonNullDomains) { + foreach ($nonNullDomains as $domain) { + $subQ->orWhere('sr.service_endpoint', 'like', "%{$domain}%"); + } + }); + } + } else { + $q->where(function ($subQ) use ($nonNullDomains) { + foreach ($nonNullDomains as $domain) { + $subQ->orWhere('sr.service_endpoint', 'like', "%{$domain}%"); + } + }); + } + }); + } + + $results = $query->select([ + 'md.id', + 'md.msg_id', + 'md.target_service', + 'md.request_status', + 'md.business_status', + 'md.retry_count', + 'md.request_error_message', + 'md.business_error_message', + 'mc.event_name', + 'mc.entity_code', + DB::raw("mc.msg_body->>'$.data.delAccountList' as delAccountList"), + DB::raw("mc.msg_body->>'$.data.afterStatus' as afterStatus"), + 'mc.msg_body', + 'md.created', + 'md.updated', + 'sr.country_code', + 'sr.service_endpoint' + ])->get(); + + // 获取所有msg_id,排除US域名的消息 + $nonUsMsgIds = $results->filter(function ($item) { + $domain = $item->service_endpoint ? (parse_url($item->service_endpoint, PHP_URL_HOST) ?? $item->service_endpoint) : null; + return $domain !== 'partner-us.eainc.com'; + })->pluck('msg_id')->unique()->toArray(); + + // 从Agent库查询consumer状态(仅非US域名) + $consumerStatuses = $this->getConsumerStatuses($nonUsMsgIds); + + return $results->map(function ($item) use ($consumerStatuses) { + $domain = $item->service_endpoint ? (parse_url($item->service_endpoint, PHP_URL_HOST) ?? $item->service_endpoint) : null; + $isUsDomain = $domain === 'partner-us.eainc.com'; + + return [ + 'id' => $item->id, + 'msg_id' => $item->msg_id, + 'target_service' => $item->target_service, + 'country_code' => $item->country_code, + 'domain' => $domain, + 'request_status' => $item->request_status, + 'business_status' => $item->business_status, + 'retry_count' => $item->retry_count, + 'request_error_message' => $item->request_error_message, + 'request_error_code' => $item->request_error_code ?? null, + 'business_error_message' => $item->business_error_message, + 'business_error_code' => $item->business_error_code ?? null, + 'event_name' => $item->event_name, + 'entity_code' => $item->entity_code, + 'delAccountList' => $item->delAccountList, + 'afterStatus' => $item->afterStatus, + 'msg_body' => $item->msg_body, + 'created' => $item->created, + 'updated' => $item->updated, + 'consumer_status' => $isUsDomain ? null : ($consumerStatuses[$item->msg_id] ?? null), + ]; + })->toArray(); + } catch (\Exception $e) { + throw new \RuntimeException('查询异常消息分发数据失败: ' . $e->getMessage()); + } + } + + /** + * 从Agent库查询消费者状态 + */ + private function getConsumerStatuses(array $msgIds): array + { + if (empty($msgIds)) { + return []; + } + + try { + $consumers = DB::connection('agentslave') + ->table('crm_event_consumer') + ->whereIn('msg_id', $msgIds) + ->select(['msg_id', 'status']) + ->get(); + + $statuses = []; + foreach ($consumers as $consumer) { + $statuses[$consumer->msg_id] = $consumer->status; + } + + return $statuses; + } catch (\Exception $e) { + return []; + } + } + + /** + * 格式化服务名称:country_code(域名) + */ + private function formatServiceName(?string $countryCode, ?string $serviceEndpoint): string + { + if (!$countryCode || !$serviceEndpoint) { + return 'Unknown'; + } + + // 从URL中提取域名 + $domain = parse_url($serviceEndpoint, PHP_URL_HOST) ?? $serviceEndpoint; + + return "{$countryCode}({$domain})"; + } + + /** + * 获取所有可用的服务列表 + */ + public function getAvailableServices(): array + { + try { + $services = DB::connection('monoslave') + ->table('service_routes') + ->select(['id', 'country_code', 'service_endpoint']) + ->get(); + + return $services->map(function ($service) { + return [ + 'id' => $service->id, + 'name' => $this->formatServiceName($service->country_code, $service->service_endpoint), + ]; + })->toArray(); + } catch (\Exception $e) { + return []; + } + } + + /** + * 获取所有国家代码列表 + */ + public function getAvailableCountryCodes(): array + { + try { + $codes = DB::connection('monoslave') + ->table('service_routes') + ->select('country_code') + ->distinct() + ->orderBy('country_code') + ->get() + ->pluck('country_code') + ->filter() + ->values() + ->toArray(); + + return $codes; + } catch (\Exception $e) { + return []; + } + } + + /** + * 获取所有域名列表 + */ + public function getAvailableDomains(): array + { + try { + $endpoints = DB::connection('monoslave') + ->table('service_routes') + ->select('service_endpoint') + ->distinct() + ->get() + ->pluck('service_endpoint') + ->filter() + ->map(function ($endpoint) { + return parse_url($endpoint, PHP_URL_HOST) ?? $endpoint; + }) + ->unique() + ->values() + ->toArray(); + + return $endpoints; + } catch (\Exception $e) { + return []; + } + } + + /** + * 获取所有服务路由列表 + */ + public function getServiceRoutes(): array + { + try { + $routes = DB::connection('monoslave') + ->table('service_routes') + ->select('id', 'country_code', 'service_endpoint') + ->orderBy('country_code') + ->orderBy('id') + ->get() + ->map(function ($route) { + $domain = parse_url($route->service_endpoint, PHP_URL_HOST) ?? $route->service_endpoint; + return [ + 'id' => $route->id, + 'country_code' => $route->country_code, + 'domain' => $domain, + 'display_name' => ($route->country_code ?: '(空)') . ' - ' . $domain, + ]; + }) + ->toArray(); + + return $routes; + } catch (\Exception $e) { + return []; + } + } + + /** + * 批量更新消息分发状态 + */ + public function batchUpdateDispatch(array $updates): array + { + $results = []; + + foreach ($updates as $update) { + try { + $result = $this->updateDispatch($update); + $results[] = [ + 'id' => $update['id'], + 'success' => $result['success'], + 'message' => $result['message'] ?? null, + ]; + } catch (\Exception $e) { + $results[] = [ + 'id' => $update['id'], + 'success' => false, + 'message' => $e->getMessage(), + ]; + } + } + + return $results; + } + + /** + * 更新单个消息分发状态 + */ + private function updateDispatch(array $data): array + { + $response = $this->monoClient->updateDispatch($data); + + if ($response->successful()) { + return [ + 'success' => true, + 'data' => $response->json(), + ]; + } + + return [ + 'success' => false, + 'message' => 'HTTP ' . $response->status() . ': ' . $response->body(), + ]; + } +} + diff --git a/app/Services/MessageSyncService.php b/app/Services/MessageSyncService.php new file mode 100644 index 0000000..62994a8 --- /dev/null +++ b/app/Services/MessageSyncService.php @@ -0,0 +1,191 @@ +agentClient = $agentClient; + } + + /** + * 根据消息ID列表从crmslave数据库获取消息数据 + */ + public function getMessagesByIds(array $messageIds): Collection + { + if (empty($messageIds)) { + return collect(); + } + + try { + $messages = DB::connection('crmslave') + ->table('system_publish_event') + ->whereIn('msg_id', $messageIds) + ->select([ + 'msg_id', + 'event_type', + 'trace_id', + 'event_param', + 'event_property', + 'timestamp' + ]) + ->get(); + + return $messages->map(function ($message) { + return [ + 'msg_id' => $message->msg_id, + 'event_type' => $message->event_type, + 'trace_id' => $message->trace_id, + 'event_param' => $message->event_param, + 'event_property' => $message->event_property, + 'timestamp' => $message->timestamp, + 'parsed_param' => $this->parseJsonField($message->event_param), + 'parsed_property' => $this->parseJsonField($message->event_property), + ]; + }); + } catch (\Exception $e) { + throw new \RuntimeException('从crmslave数据库获取消息失败: ' . $e->getMessage()); + } + } + + /** + * 批量同步消息到agent + */ + public function syncMessages(array $messageIds): array + { + $messages = $this->getMessagesByIds($messageIds); + $results = []; + + foreach ($messages as $message) { + $result = $this->syncSingleMessage($message); + $results[] = [ + 'msg_id' => $message['msg_id'], + 'success' => $result['success'], + 'response' => $result['response'] ?? null, + 'error' => $result['error'] ?? null, + 'request_data' => $result['request_data'] ?? null, + ]; + } + + return $results; + } + + /** + * 同步单个消息到agent + */ + private function syncSingleMessage(array $message): array + { + try { + $requestData = $this->buildAgentRequest($message); + + $response = $this->agentClient->dispatchMessage($requestData); + + if ($response->successful()) { + return [ + 'success' => true, + 'response' => $response->json(), + 'request_data' => $requestData, + ]; + } else { + return [ + 'success' => false, + 'error' => 'HTTP ' . $response->status() . ': ' . $response->body(), + 'request_data' => $requestData, + ]; + } + } catch (\Exception $e) { + return [ + 'success' => false, + 'error' => '请求失败: ' . $e->getMessage(), + 'request_data' => $requestData ?? null, + ]; + } + } + + /** + * 构建agent接口请求数据 + */ + private function buildAgentRequest(array $message): array + { + $parsedParam = $message['parsed_param']; + $parsedProperty = $message['parsed_property']; + + return [ + 'topic_name' => $message['event_type'], + 'msg_body' => [ + 'id' => $message['msg_id'], + 'data' => $parsedParam, + 'timestamp' => $message['timestamp'], + 'property' => $parsedProperty, + ], + 'target_service' => [1], // 默认目标服务 + 'trace_id' => $message['trace_id'], + ]; + } + + /** + * 解析JSON字段 + */ + private function parseJsonField(?string $jsonString): mixed + { + if (empty($jsonString)) { + return null; + } + + try { + return json_decode($jsonString, true); + } catch (\Exception $e) { + return $jsonString; // 如果解析失败,返回原始字符串 + } + } + + /** + * 验证消息ID格式 + */ + public function validateMessageIds(array $messageIds): array + { + $errors = []; + + foreach ($messageIds as $index => $messageId) { + if (empty($messageId)) { + $errors[] = "第 " . ($index + 1) . " 行: 消息ID不能为空"; + continue; + } + + if (!is_string($messageId) && !is_numeric($messageId)) { + $errors[] = "第 " . ($index + 1) . " 行: 消息ID格式无效"; + continue; + } + + // 可以添加更多的格式验证规则 + } + + return $errors; + } + + /** + * 获取消息统计信息 + */ + public function getMessageStats(array $messageIds): array + { + $messages = $this->getMessagesByIds($messageIds); + + $stats = [ + 'total_requested' => count($messageIds), + 'total_found' => $messages->count(), + 'total_missing' => count($messageIds) - $messages->count(), + 'event_types' => $messages->groupBy('event_type')->map->count(), + 'missing_ids' => array_diff($messageIds, $messages->pluck('msg_id')->toArray()), + ]; + + return $stats; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..fdfaa9a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,11 +7,18 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + // 为 API 路由添加 CSRF 豁免 + $middleware->validateCsrfTokens(except: [ + 'api/jira/*', + 'api/env/*', + 'api/message-sync/*', + 'api/message-dispatch/*' + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 7f5cf75..bddfa3b 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,5 +2,6 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\ClientServiceProvider::class, App\Providers\EnvServiceProvider::class, ]; diff --git a/composer.json b/composer.json index dfe9c82..31b8114 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,9 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "lesstif/php-jira-rest-client": "5.10.0", + "ext-pdo": "*" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 3002027..d158fc6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "88970a0117c062eed55fa8728fc43833", + "content-hash": "5955fb8cae2c82cdb7352006103c8c0c", "packages": [ { "name": "brick/math", @@ -2006,6 +2006,73 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "lesstif/php-jira-rest-client", + "version": "5.10.0", + "source": { + "type": "git", + "url": "https://github.com/lesstif/php-jira-rest-client.git", + "reference": "46b20408c34138615acbdd58a86b8b624d864d0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lesstif/php-jira-rest-client/zipball/46b20408c34138615acbdd58a86b8b624d864d0c", + "reference": "46b20408c34138615acbdd58a86b8b624d864d0c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "monolog/monolog": "^2.0|^3.0", + "netresearch/jsonmapper": "^4.2", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0|^2.0", + "phpstan/phpstan": "^1.0|^2.0", + "phpunit/phpunit": "^9.0|^10.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0" + }, + "suggest": { + "vlucas/phpdotenv": "^5.0|^6.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "JiraRestApi\\JiraRestApiServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "JiraRestApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "KwangSeob Jeong", + "email": "lesstif@gmail.com", + "homepage": "https://lesstif.com/" + } + ], + "description": "JIRA REST API Client for PHP Users.", + "keywords": [ + "jira", + "jira-php", + "jira-rest", + "rest" + ], + "support": { + "issues": "https://github.com/lesstif/php-jira-rest-client/issues", + "source": "https://github.com/lesstif/php-jira-rest-client/tree/5.10.0" + }, + "time": "2025-04-05T12:50:11+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -2214,6 +2281,57 @@ ], "time": "2025-06-21T15:19:35+00:00" }, + { + "name": "netresearch/jsonmapper", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" + }, + "time": "2024-09-08T10:13:13+00:00" + }, { "name": "nette/schema", "version": "v1.3.2", diff --git a/config/database.php b/config/database.php index 5b318f5..53e8df5 100644 --- a/config/database.php +++ b/config/database.php @@ -112,6 +112,66 @@ return [ // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], + 'crmslave' => [ + 'driver' => 'mysql', + 'url' => env('CRMSLAVE_DB_URL'), + 'host' => env('CRMSLAVE_DB_HOST', '127.0.0.1'), + 'port' => env('CRMSLAVE_DB_PORT', '3306'), + 'database' => env('CRMSLAVE_DB_DATABASE', 'crmslave'), + 'username' => env('CRMSLAVE_DB_USERNAME', 'root'), + 'password' => env('CRMSLAVE_DB_PASSWORD', ''), + 'unix_socket' => env('CRMSLAVE_DB_SOCKET', ''), + 'charset' => env('CRMSLAVE_DB_CHARSET', 'utf8mb4'), + 'collation' => env('CRMSLAVE_DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('CRMSLAVE_MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'agentslave' => [ + 'driver' => 'mysql', + 'url' => env('AGENTSLAVE_DB_URL'), + 'host' => env('AGENTSLAVE_DB_HOST', '127.0.0.1'), + 'port' => env('AGENTSLAVE_DB_PORT', '3306'), + 'database' => env('AGENTSLAVE_DB_DATABASE', 'crm'), + 'username' => env('AGENTSLAVE_DB_USERNAME', 'root'), + 'password' => env('AGENTSLAVE_DB_PASSWORD', ''), + 'unix_socket' => env('AGENTSLAVE_DB_SOCKET', ''), + 'charset' => env('AGENTSLAVE_DB_CHARSET', 'utf8mb4'), + 'collation' => env('AGENTSLAVE_DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('AGENTSLAVE_MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'monoslave' => [ + 'driver' => 'mysql', + 'url' => env('MONOSLAVE_DB_URL'), + 'host' => env('MONOSLAVE_DB_HOST', '127.0.0.1'), + 'port' => env('MONOSLAVE_DB_PORT', '3306'), + 'database' => env('MONOSLAVE_DB_DATABASE', 'mono'), + 'username' => env('MONOSLAVE_DB_USERNAME', 'root'), + 'password' => env('MONOSLAVE_DB_PASSWORD', ''), + 'unix_socket' => env('MONOSLAVE_DB_SOCKET', ''), + 'charset' => env('MONOSLAVE_DB_CHARSET', 'utf8mb4'), + 'collation' => env('MONOSLAVE_DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MONOSLAVE_MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ], /* diff --git a/config/jira.php b/config/jira.php new file mode 100644 index 0000000..c8f1ae3 --- /dev/null +++ b/config/jira.php @@ -0,0 +1,32 @@ + env('JIRA_HOST', 'http://jira.eainc.com:8080'), + 'username' => env('JIRA_USERNAME'), + 'password' => env('JIRA_PASSWORD'), + 'api_token' => env('JIRA_API_TOKEN'), + + // 认证方式: 'basic' 或 'token' + 'auth_type' => env('JIRA_AUTH_TYPE', 'basic'), + + // 连接超时时间(秒) + 'timeout' => (int) env('JIRA_TIMEOUT', 30), + + // 项目代码映射 + 'project_codes' => [ + 'WP' => 'WP', + 'AM' => 'AM', + ], + + // 默认用户(如果未指定) + 'default_user' => env('JIRA_DEFAULT_USER'), +]; diff --git a/config/services.php b/config/services.php index 6182e4b..a7c90ff 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,14 @@ return [ ], ], + 'agent' => [ + 'url' => env('AGENT_URL'), + 'timeout' => env('AGENT_TIMEOUT', 30), + ], + + 'mono' => [ + 'url' => env('MONO_URL'), + 'timeout' => env('MONO_TIMEOUT', 30), + ], + ]; diff --git a/package-lock.json b/package-lock.json index ab2fbd0..f35c554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,17 @@ "packages": { "": { "dependencies": { + "@codemirror/autocomplete": "^6.18.3", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/language": "^6.10.6", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.1", "@vitejs/plugin-vue": "^6.0.1", + "codemirror": "^6.0.2", "vue": "^3.5.18" }, "devDependencies": { @@ -77,6 +87,157 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript/node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", + "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", + "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -544,6 +705,80 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", + "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.4.tgz", + "integrity": "sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.29", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", @@ -1323,6 +1558,32 @@ "node": ">=12" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror/node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1382,6 +1643,12 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2296,6 +2563,12 @@ "node": ">=8" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2499,6 +2772,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 18aa0b6..ebce9c0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,17 @@ "vite": "^7.0.4" }, "dependencies": { + "@codemirror/autocomplete": "^6.18.3", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/language": "^6.10.6", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.1", "@vitejs/plugin-vue": "^6.0.1", + "codemirror": "^6.0.2", "vue": "^3.5.18" } } diff --git a/resources/js/app.js b/resources/js/app.js index 17dc621..20e9782 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,11 +1,11 @@ import './bootstrap'; import { createApp } from 'vue'; -import EnvManager from './components/EnvManager.vue'; +import AdminDashboard from './components/admin/AdminDashboard.vue'; console.log('App.js loading...'); const app = createApp({}); -app.component('env-manager', EnvManager); +app.component('admin-dashboard', AdminDashboard); console.log('Mounting app...'); app.mount('#app'); diff --git a/resources/js/components/AdminLayout.vue b/resources/js/components/AdminLayout.vue deleted file mode 100644 index d7c3cff..0000000 --- a/resources/js/components/AdminLayout.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - diff --git a/resources/js/components/CodeEditor.vue b/resources/js/components/CodeEditor.vue deleted file mode 100644 index b53f863..0000000 --- a/resources/js/components/CodeEditor.vue +++ /dev/null @@ -1,969 +0,0 @@ - - - - - diff --git a/resources/js/components/EnvManager.vue b/resources/js/components/admin/AdminDashboard.vue similarity index 53% rename from resources/js/components/EnvManager.vue rename to resources/js/components/admin/AdminDashboard.vue index 388745e..9ad29ff 100644 --- a/resources/js/components/EnvManager.vue +++ b/resources/js/components/admin/AdminDashboard.vue @@ -9,6 +9,36 @@ ref="envManagement" /> + + + + + + + + + + + + + + +
@@ -32,18 +62,27 @@
- diff --git a/resources/js/components/admin/AdminLayout.vue b/resources/js/components/admin/AdminLayout.vue new file mode 100644 index 0000000..61f1902 --- /dev/null +++ b/resources/js/components/admin/AdminLayout.vue @@ -0,0 +1,322 @@ + + + + + diff --git a/resources/js/components/env/CodeEditor.vue b/resources/js/components/env/CodeEditor.vue new file mode 100644 index 0000000..0d3d623 --- /dev/null +++ b/resources/js/components/env/CodeEditor.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/resources/js/components/EnvManagement.vue b/resources/js/components/env/EnvManagement.vue similarity index 91% rename from resources/js/components/EnvManagement.vue rename to resources/js/components/env/EnvManagement.vue index b5ebe6e..0e4bb72 100644 --- a/resources/js/components/EnvManagement.vue +++ b/resources/js/components/env/EnvManagement.vue @@ -9,8 +9,13 @@ 项目选择 -
- 共 {{ projects.length }} 个项目 +
+
共 {{ projects.length }} 个项目
+
+ 当前 APP_ENV: + {{ currentAppEnv }} + 未检测到 +
@@ -143,8 +148,19 @@
-
-

{{ env.name }}

+
+

+ {{ env.name }} + + 当前 + +

{{ formatFileSize(env.size) }} @@ -268,7 +284,7 @@
-
+

@@ -313,7 +329,7 @@

-
+

@@ -403,6 +419,7 @@ export default { importEnvName: '', editorLanguage: 'env', editorTheme: 'light', + currentAppEnv: '', message: { text: '', type: '' @@ -457,7 +474,7 @@ export default { methods: { async loadProjects() { try { - const response = await fetch('/env/api/projects'); + const response = await fetch('/api/env/projects'); const data = await response.json(); if (data.success) { @@ -477,11 +494,12 @@ export default { } try { - const response = await fetch(`/env/api/projects/${this.selectedProject}/envs`); + const response = await fetch(`/api/env/projects/${this.selectedProject}/envs`); const data = await response.json(); - + if (data.success) { - this.envs = data.data; + this.envs = Array.isArray(data.data) ? data.data : []; + this.tryAutoSelectCurrentEnv(); } else { this.showMessage('加载环境列表失败', 'error'); } @@ -490,12 +508,51 @@ export default { } }, + async loadCurrentProjectEnv() { + if (!this.selectedProject) { + this.currentAppEnv = ''; + return; + } + + try { + const response = await fetch(`/api/env/projects/${this.selectedProject}/current-env`); + const data = await response.json(); + + if (!data.success || !data.data || typeof data.data.content !== 'string') { + this.currentAppEnv = ''; + return; + } + + const content = data.data.content; + const match = content.match(/^\s*APP_ENV\s*=\s*([^\r\n#]+)/m); + this.currentAppEnv = match ? match[1].trim() : ''; + this.tryAutoSelectCurrentEnv(); + } catch (error) { + this.currentAppEnv = ''; + } + }, + + tryAutoSelectCurrentEnv() { + if (!this.currentAppEnv || !Array.isArray(this.envs) || this.envs.length === 0) { + return; + } + + if (this.selectedEnv === this.currentAppEnv) { + return; + } + + const matched = this.envs.find(env => env.name === this.currentAppEnv); + if (matched) { + this.loadEnvContent(matched.name); + } + }, + async loadEnvContent(envName) { this.selectedEnv = envName; this.newEnvName = envName; try { - const response = await fetch(`/env/api/projects/${this.selectedProject}/envs/${envName}`); + const response = await fetch(`/api/env/projects/${this.selectedProject}/envs/${envName}`); const data = await response.json(); if (data.success) { @@ -521,7 +578,7 @@ export default { } try { - const response = await fetch('/env/api/envs/save', { + const response = await fetch('/api/env/envs/save', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -555,7 +612,7 @@ export default { } try { - const response = await fetch('/env/api/envs/apply', { + const response = await fetch('/api/env/envs/apply', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -568,9 +625,11 @@ export default { }); const data = await response.json(); - + if (data.success) { this.showMessage('环境配置应用成功', 'success'); + await this.loadCurrentProjectEnv(); + await this.loadEnvs(); } else { this.showMessage(data.message || '应用失败', 'error'); } @@ -585,7 +644,7 @@ export default { } try { - const response = await fetch(`/env/api/projects/${this.selectedProject}/envs/${envName}`, { + const response = await fetch(`/api/env/projects/${this.selectedProject}/envs/${envName}`, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') @@ -616,7 +675,7 @@ export default { } try { - const response = await fetch('/env/api/envs/import', { + const response = await fetch('/api/env/envs/import', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -650,7 +709,7 @@ export default { } try { - const response = await fetch('/env/api/envs/create', { + const response = await fetch('/api/env/envs/create', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -704,7 +763,12 @@ export default { this.projectSearchQuery = project.name; this.showProjectDropdown = false; this.highlightedIndex = -1; + this.selectedEnv = ''; + this.envContent = ''; + this.newEnvName = ''; + this.currentAppEnv = ''; this.loadEnvs(); + this.loadCurrentProjectEnv(); }, handleProjectFocus() { diff --git a/resources/js/components/jira/JiraWorklog.vue b/resources/js/components/jira/JiraWorklog.vue new file mode 100644 index 0000000..9dca889 --- /dev/null +++ b/resources/js/components/jira/JiraWorklog.vue @@ -0,0 +1,503 @@ + + + diff --git a/resources/js/components/jira/WeeklyReport.vue b/resources/js/components/jira/WeeklyReport.vue new file mode 100644 index 0000000..59812ee --- /dev/null +++ b/resources/js/components/jira/WeeklyReport.vue @@ -0,0 +1,169 @@ + + + diff --git a/resources/js/components/message-sync/EventConsumerSync.vue b/resources/js/components/message-sync/EventConsumerSync.vue new file mode 100644 index 0000000..761682a --- /dev/null +++ b/resources/js/components/message-sync/EventConsumerSync.vue @@ -0,0 +1,373 @@ + + + + diff --git a/resources/js/components/message-sync/MessageDispatch.vue b/resources/js/components/message-sync/MessageDispatch.vue new file mode 100644 index 0000000..22517ba --- /dev/null +++ b/resources/js/components/message-sync/MessageDispatch.vue @@ -0,0 +1,746 @@ + + + + diff --git a/resources/js/components/message-sync/MessageSync.vue b/resources/js/components/message-sync/MessageSync.vue new file mode 100644 index 0000000..2569374 --- /dev/null +++ b/resources/js/components/message-sync/MessageSync.vue @@ -0,0 +1,358 @@ + + + diff --git a/resources/js/lang-env.js b/resources/js/lang-env.js new file mode 100644 index 0000000..52ba0a3 --- /dev/null +++ b/resources/js/lang-env.js @@ -0,0 +1,44 @@ +import { StreamLanguage } from '@codemirror/language' + +// 简单的.env语法高亮 +const envLanguage = StreamLanguage.define({ + token(stream) { + // 注释 + if (stream.match(/^\s*#.*/)) { + return 'comment' + } + + // 变量名 + if (stream.match(/^[A-Z_][A-Z0-9_]*/)) { + return 'variableName' + } + + // 等号 + if (stream.match(/=/)) { + return 'operator' + } + + // 字符串 + if (stream.match(/^"[^"]*"/) || stream.match(/^'[^']*'/)) { + return 'string' + } + + // 布尔值 + if (stream.match(/^(true|false)\b/i)) { + return 'keyword' + } + + // 数字 + if (stream.match(/^\d+(\.\d+)?/)) { + return 'number' + } + + // 跳过其他字符 + stream.next() + return null + } +}) + +export function env() { + return envLanguage +} diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php new file mode 100644 index 0000000..b78df4f --- /dev/null +++ b/resources/views/admin/index.blade.php @@ -0,0 +1,9 @@ +@extends('layouts.app') + +@section('title', 'Tradewind Toolbox') + +@section('content') +
+ +
+@endsection diff --git a/resources/views/env/index.blade.php b/resources/views/layouts/app.blade.php similarity index 72% rename from resources/views/env/index.blade.php rename to resources/views/layouts/app.blade.php index e4546b5..2cb6124 100644 --- a/resources/views/env/index.blade.php +++ b/resources/views/layouts/app.blade.php @@ -4,12 +4,11 @@ - 环境文件管理系统 + @yield('title', 'Tradewind Toolbox') @vite(['resources/css/app.css', 'resources/js/app.js']) -
- -
+ @yield('content') + @stack('scripts') diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..3cc0b2a --- /dev/null +++ b/routes/api.php @@ -0,0 +1,50 @@ +group(function () { + Route::get('/projects', [EnvController::class, 'getProjects']); + Route::get('/projects/{project}/envs', [EnvController::class, 'getEnvs']); + Route::get('/projects/{project}/envs/{env}', [EnvController::class, 'getEnvContent']); + Route::get('/projects/{project}/current-env', [EnvController::class, 'getCurrentEnv']); + + Route::post('/envs/save', [EnvController::class, 'saveEnv']); + Route::post('/envs/apply', [EnvController::class, 'applyEnv']); + Route::post('/envs/import', [EnvController::class, 'importEnv']); + Route::post('/envs/create', [EnvController::class, 'createEnv']); + + Route::delete('/projects/{project}/envs/{env}', [EnvController::class, 'deleteEnv']); +}); + +// JIRA API路由 +Route::prefix('jira')->group(function () { + Route::get('/config', [JiraController::class, 'getConfig']); + Route::post('/weekly-report', [JiraController::class, 'generateWeeklyReport']); + Route::post('/work-logs', [JiraController::class, 'getWorkLogs']); + Route::get('/weekly-report/download', [JiraController::class, 'downloadWeeklyReport']); +}); + +// 消息同步API路由 +Route::prefix('message-sync')->group(function () { + Route::post('/query', [MessageSyncController::class, 'queryMessages']); + Route::post('/sync', [MessageSyncController::class, 'syncMessages']); + Route::get('/config', [MessageSyncController::class, 'getAgentConfig']); + Route::get('/test-connection', [MessageSyncController::class, 'testConnection']); + Route::post('/compare-event-consumer', [MessageSyncController::class, 'compareEventConsumerSync']); + Route::post('/export-missing-messages', [MessageSyncController::class, 'exportMissingMessages']); +}); + +// 消息分发API路由 +Route::prefix('message-dispatch')->group(function () { + Route::get('/services', [MessageDispatchController::class, 'getAvailableServices']); + Route::get('/country-codes', [MessageDispatchController::class, 'getAvailableCountryCodes']); + Route::get('/domains', [MessageDispatchController::class, 'getAvailableDomains']); + Route::get('/service-routes', [MessageDispatchController::class, 'getServiceRoutes']); + Route::get('/abnormal', [MessageDispatchController::class, 'getAbnormalDispatches']); + Route::post('/batch-update', [MessageDispatchController::class, 'batchUpdateDispatch']); +}); diff --git a/routes/web.php b/routes/web.php index b681761..f1f33bd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,27 +1,17 @@ name('home'); -// 环境管理路由 -Route::prefix('env')->group(function () { - // 页面路由 - Route::get('/', [EnvController::class, 'index'])->name('env.index'); - - // API路由 - Route::prefix('api')->group(function () { - Route::get('/projects', [EnvController::class, 'getProjects']); - Route::get('/projects/{project}/envs', [EnvController::class, 'getEnvs']); - Route::get('/projects/{project}/envs/{env}', [EnvController::class, 'getEnvContent']); - Route::get('/projects/{project}/current-env', [EnvController::class, 'getCurrentEnv']); - - Route::post('/envs/save', [EnvController::class, 'saveEnv']); - Route::post('/envs/apply', [EnvController::class, 'applyEnv']); - Route::post('/envs/import', [EnvController::class, 'importEnv']); - Route::post('/envs/create', [EnvController::class, 'createEnv']); - - Route::delete('/projects/{project}/envs/{env}', [EnvController::class, 'deleteEnv']); - }); -}); +// 前端路由 - 所有页面都通过admin框架显示 +Route::get('/env', [AdminController::class, 'index'])->name('admin.env'); +Route::get('/weekly-report', [AdminController::class, 'index'])->name('admin.weekly-report'); +Route::get('/worklog', [AdminController::class, 'index'])->name('admin.worklog'); +Route::get('/message-sync', [AdminController::class, 'index'])->name('admin.message-sync'); +Route::get('/event-consumer-sync', [AdminController::class, 'index'])->name('admin.event-consumer-sync'); +Route::get('/message-dispatch', [AdminController::class, 'index'])->name('admin.message-dispatch'); +Route::get('/settings', [AdminController::class, 'index'])->name('admin.settings'); +Route::get('/logs', [AdminController::class, 'index'])->name('admin.logs'); diff --git a/storage/env/test-syntax/development.env b/storage/env/test-syntax/development.env new file mode 100644 index 0000000..57cbf9f --- /dev/null +++ b/storage/env/test-syntax/development.env @@ -0,0 +1,36 @@ +# 应用配置 +APP_NAME="测试应用" +APP_ENV=development +APP_DEBUG=true +APP_URL=http://localhost:8000 + +# 数据库配置 +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=test_db +DB_USERNAME=root +DB_PASSWORD="secret123" + +# 缓存配置 +CACHE_DRIVER=redis +SESSION_DRIVER=file +QUEUE_CONNECTION=sync + +# Redis配置 +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +# 邮件配置 +MAIL_MAILER=smtp +MAIL_HOST=smtp.gmail.com +MAIL_PORT=587 +MAIL_USERNAME=test@example.com +MAIL_PASSWORD="email_password" + +# 其他配置 +ENABLE_FEATURE=true +MAX_CONNECTIONS=100 +TIMEOUT=30.5 +LOG_LEVEL=info diff --git a/storage/env/test/development.env b/storage/env/test/development.env new file mode 100644 index 0000000..04c8223 --- /dev/null +++ b/storage/env/test/development.env @@ -0,0 +1,20 @@ +# 应用配置 +APP_NAME="测试应用" +APP_ENV=development +APP_DEBUG=true +APP_URL=http://localhost:8000 + +# 数据库配置 +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=test_db +DB_USERNAME=root +DB_PASSWORD="secret123" + +# 其他配置 +CACHE_DRIVER=redis +SESSION_DRIVER=file +ENABLE_FEATURE=true +MAX_CONNECTIONS=100 +TIMEOUT=30.5 diff --git a/tests/Feature/EventConsumerSyncTest.php b/tests/Feature/EventConsumerSyncTest.php new file mode 100644 index 0000000..444e0e0 --- /dev/null +++ b/tests/Feature/EventConsumerSyncTest.php @@ -0,0 +1,70 @@ +postJson('/api/message-sync/compare-event-consumer', [ + 'start_time' => Carbon::now()->subDays(7)->format('Y-m-d H:i:s'), + 'end_time' => Carbon::now()->format('Y-m-d H:i:s'), + 'exclude_messages' => [] + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'success', + 'data' => [ + 'crm_total', + 'agent_total', + 'missing_count', + 'sync_rate', + 'missing_messages', + 'summary' + ] + ]); + } + + /** + * 测试导出缺失消息 API + */ + public function test_export_missing_messages_api() + { + $response = $this->postJson('/api/message-sync/export-missing-messages', [ + 'start_time' => Carbon::now()->subDays(7)->format('Y-m-d H:i:s'), + 'end_time' => Carbon::now()->format('Y-m-d H:i:s'), + 'exclude_messages' => [] + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'success', + 'data' => [ + 'csv', + 'filename', + 'count' + ] + ]); + } + + /** + * 测试无效的时间格式 + */ + public function test_invalid_time_format() + { + $response = $this->postJson('/api/message-sync/compare-event-consumer', [ + 'start_time' => 'invalid-date', + 'end_time' => 'invalid-date' + ]); + + $response->assertStatus(422); + } +} + diff --git a/tests/Feature/MessageSyncTest.php b/tests/Feature/MessageSyncTest.php new file mode 100644 index 0000000..d385ff7 --- /dev/null +++ b/tests/Feature/MessageSyncTest.php @@ -0,0 +1,98 @@ +get('/'); + $response->assertStatus(200); + $response->assertViewIs('admin.index'); + } + + /** + * 测试查询消息API端点 + */ + public function test_query_messages_api_validation(): void + { + // 测试空请求 + $response = $this->postJson('/api/message-sync/query', []); + $response->assertStatus(422); + $response->assertJsonStructure([ + 'success', + 'message', + 'errors' + ]); + + // 测试无效的消息ID格式 + $response = $this->postJson('/api/message-sync/query', [ + 'message_ids' => [''] + ]); + $response->assertStatus(422); + $response->assertJson([ + 'success' => false + ]); + } + + /** + * 测试同步消息API端点 + */ + public function test_sync_messages_api_validation(): void + { + // 测试空请求 + $response = $this->postJson('/api/message-sync/sync', []); + $response->assertStatus(422); + $response->assertJsonStructure([ + 'success', + 'message', + 'errors' + ]); + + // 测试无效的消息ID格式 + $response = $this->postJson('/api/message-sync/sync', [ + 'message_ids' => [''] + ]); + $response->assertStatus(422); + $response->assertJson([ + 'success' => false + ]); + } + + /** + * 测试获取agent配置API端点 + */ + public function test_get_agent_config_api(): void + { + $response = $this->getJson('/api/message-sync/config'); + $response->assertStatus(200); + $response->assertJsonStructure([ + 'success', + 'data' => [ + 'agent_url', + 'timeout' + ] + ]); + } + + /** + * 测试数据库连接测试API端点 + */ + public function test_database_connection_test_api(): void + { + $response = $this->getJson('/api/message-sync/test-connection'); + + // 由于测试环境可能没有配置crmslave数据库,这里测试端点是否存在 + // 可能返回200(连接成功)或500(连接失败),都是正常的 + $this->assertContains($response->status(), [200, 500]); + $response->assertJsonStructure([ + 'success' + ]); + } +} diff --git a/tests/Unit/JiraServiceTest.php b/tests/Unit/JiraServiceTest.php new file mode 100644 index 0000000..daac7de --- /dev/null +++ b/tests/Unit/JiraServiceTest.php @@ -0,0 +1,184 @@ + 'https://test-jira.example.com', + 'jira.username' => 'test-user', + 'jira.password' => 'test-password', + 'jira.default_user' => 'test-user' + ]); + + $this->jiraService = app(JiraService::class); + } + + public function test_is_task_completed_returns_false_for_incomplete_statuses() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('isTaskCompleted'); + + // 测试不完成的状态 + $incompleteStatuses = [ + '开发中', + '需求已排期', + '需求已评审', + 'In Progress', + 'To Do', + 'Open' + ]; + + foreach ($incompleteStatuses as $status) { + $this->assertFalse( + $method->invoke($this->jiraService, $status), + "状态 '{$status}' 应该返回 false" + ); + } + } + + public function test_is_task_completed_returns_true_for_complete_statuses() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('isTaskCompleted'); + + // 测试完成的状态 + $completeStatuses = [ + 'Done', + 'Closed', + 'Resolved', + '已完成', + 'Complete' + ]; + + foreach ($completeStatuses as $status) { + $this->assertTrue( + $method->invoke($this->jiraService, $status), + "状态 '{$status}' 应该返回 true" + ); + } + } + + public function test_organize_tasks_for_report_handles_empty_collection() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('organizeTasksForReport'); + + $emptyWorkLogs = collect(); + $result = $method->invoke($this->jiraService, $emptyWorkLogs); + + $this->assertInstanceOf(Collection::class, $result); + $this->assertTrue($result->has('sprints')); + $this->assertTrue($result->has('tasks')); + $this->assertTrue($result->has('bugs')); + } + + public function test_organize_tasks_for_report_categorizes_by_sprint() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('organizeTasksForReport'); + + $workLogs = collect([ + [ + 'issue_key' => 'WP-1234', + 'issue_summary' => '测试需求', + 'issue_url' => 'https://test-jira.example.com/browse/WP-1234', + 'issue_status' => 'Done', + 'issue_type' => 'Story', + 'sprint' => '十月中需求', + 'bug_stage' => null, + 'bug_type' => null, + 'parent_task' => null, + ] + ]); + + $result = $method->invoke($this->jiraService, $workLogs); + + $this->assertTrue($result['sprints']->has('十月中需求')); + $this->assertCount(1, $result['sprints']['十月中需求']); + } + + public function test_organize_tasks_for_report_categorizes_bugs_by_stage() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('organizeTasksForReport'); + + $workLogs = collect([ + [ + 'issue_key' => 'WP-5678', + 'issue_summary' => '测试Bug', + 'issue_url' => 'https://test-jira.example.com/browse/WP-5678', + 'issue_status' => 'Done', + 'issue_type' => 'Bug', + 'sprint' => null, + 'bug_stage' => 'SIT环境BUG', + 'bug_type' => '需求未说明', + 'parent_task' => null, + ] + ]); + + $result = $method->invoke($this->jiraService, $workLogs); + + $this->assertTrue($result['bugs']->has('SIT环境BUG')); + $this->assertCount(1, $result['bugs']['SIT环境BUG']); + $this->assertEquals('需求未说明', $result['bugs']['SIT环境BUG'][0]['bug_type']); + } + + public function test_extract_sprint_info_from_string() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractSprintInfo'); + + $issue = (object)[ + 'fields' => (object)[ + 'customfield_10020' => [ + 'com.atlassian.greenhopper.service.sprint.Sprint@xxx[name=十月中需求,state=ACTIVE]' + ] + ] + ]; + + $result = $method->invoke($this->jiraService, $issue); + $this->assertEquals('十月中需求', $result); + } + + public function test_extract_bug_stage_from_labels() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractBugStage'); + + $issue = (object)[ + 'fields' => (object)[ + 'labels' => ['SIT', 'bug'] + ] + ]; + + $result = $method->invoke($this->jiraService, $issue); + $this->assertEquals('SIT环境BUG', $result); + } + + public function test_extract_bug_type_from_labels() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractBugType'); + + $issue = (object)[ + 'fields' => (object)[ + 'labels' => ['需求未说明', 'bug'] + ] + ]; + + $result = $method->invoke($this->jiraService, $issue); + $this->assertEquals('需求未说明', $result); + } +} diff --git a/vite.config.js b/vite.config.js index b9f6ed7..27aae3f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -27,5 +27,28 @@ export default defineConfig({ alias: { 'vue': 'vue/dist/vue.esm-bundler.js' } + }, + build: { + chunkSizeWarningLimit: 1000, + rollupOptions: { + output: { + manualChunks: { + 'vue-vendor': ['vue'], + 'codemirror-vendor': [ + 'codemirror', + '@codemirror/autocomplete', + '@codemirror/commands', + '@codemirror/lang-javascript', + '@codemirror/lang-php', + '@codemirror/language', + '@codemirror/search', + '@codemirror/state', + '@codemirror/theme-one-dark', + '@codemirror/view' + ], + 'utils-vendor': ['axios'] + } + } + } } });