#add jira & message sync
This commit is contained in:
37
app/Clients/AgentClient.php
Normal file
37
app/Clients/AgentClient.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Clients;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class AgentClient
|
||||
{
|
||||
private string $baseUrl;
|
||||
private PendingRequest $http;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Clients/MonoClient.php
Normal file
37
app/Clients/MonoClient.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Clients;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class MonoClient
|
||||
{
|
||||
private string $baseUrl;
|
||||
private PendingRequest $http;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=备份名 # 删除备份');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
30
app/Console/Commands/JiraTestCommand.php
Normal file
30
app/Console/Commands/JiraTestCommand.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class JiraTestCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:jira-test-command';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
16
app/Http/Controllers/AdminController.php
Normal file
16
app/Http/Controllers/AdminController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示管理系统主页概览
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.index');
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,7 @@ class EnvController extends Controller
|
||||
$this->envManager = $envManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示环境管理页面
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('env.index');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有项目列表
|
||||
|
||||
135
app/Http/Controllers/JiraController.php
Normal file
135
app/Http/Controllers/JiraController.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\JiraService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class JiraController extends Controller
|
||||
{
|
||||
private JiraService $jiraService;
|
||||
|
||||
public function __construct(JiraService $jiraService)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
207
app/Http/Controllers/MessageDispatchController.php
Normal file
207
app/Http/Controllers/MessageDispatchController.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MessageDispatchService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class MessageDispatchController extends Controller
|
||||
{
|
||||
private MessageDispatchService $messageDispatchService;
|
||||
|
||||
public function __construct(MessageDispatchService $messageDispatchService)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
322
app/Http/Controllers/MessageSyncController.php
Normal file
322
app/Http/Controllers/MessageSyncController.php
Normal file
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MessageSyncService;
|
||||
use App\Services\EventConsumerSyncService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MessageSyncController extends Controller
|
||||
{
|
||||
private MessageSyncService $messageSyncService;
|
||||
private EventConsumerSyncService $eventConsumerSyncService;
|
||||
|
||||
public function __construct(
|
||||
MessageSyncService $messageSyncService,
|
||||
EventConsumerSyncService $eventConsumerSyncService
|
||||
) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
// 注册 JIRA 服务
|
||||
$this->app->singleton(\App\Services\JiraService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
33
app/Providers/ClientServiceProvider.php
Normal file
33
app/Providers/ClientServiceProvider.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Clients\AgentClient;
|
||||
use App\Clients\MonoClient;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ClientServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(AgentClient::class, function () {
|
||||
return new AgentClient();
|
||||
});
|
||||
|
||||
$this->app->singleton(MonoClient::class, function () {
|
||||
return new MonoClient();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
271
app/Services/EventConsumerSyncService.php
Normal file
271
app/Services/EventConsumerSyncService.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class EventConsumerSyncService
|
||||
{
|
||||
/**
|
||||
* Agent监听的所有topic列表
|
||||
*/
|
||||
private const AGENT_LISTENED_TOPICS = [
|
||||
// Case
|
||||
'CASE_CREATE',
|
||||
'CASE_FILE',
|
||||
'CASE_3D_SEND',
|
||||
'CASE_3D_CONFIRM',
|
||||
'CASE_3D_CANCEL_CONFIRM',
|
||||
'CASE_STAGE_FINISH',
|
||||
'CASE_STAGE_CREATE',
|
||||
'CASE_FINISH',
|
||||
'CASE_PAUSE',
|
||||
'CASE_DELETED',
|
||||
'CASE_CONTINUE',
|
||||
'CASE_MONEY_IN',
|
||||
'CASE_BASIC_INFO_CHANGE',
|
||||
'CASE_REFERRAL',
|
||||
'CASE_TAGS_CHANGE',
|
||||
'CASE_RECOVER',
|
||||
'CASE_PRODUCT_WAIT_CONFIRM',
|
||||
'CASE_PRODUCT_CONFIRM',
|
||||
'CASE_PRODUCT_CANCEL_CONFIRM',
|
||||
'CASE_START_DESIGN',
|
||||
'CASE_NOT_TREATED',
|
||||
'CASE_REOPEN',
|
||||
'MEDICAL_DESIGN_EVENT',
|
||||
// Production
|
||||
'PRODUCTION_CREATE',
|
||||
'PRODUCTION_DELIVER',
|
||||
'SHIPPING_STATUS',
|
||||
'PRODUCTION_INFO_CHANGE',
|
||||
// Doctor
|
||||
'DOCTOR_CREATE',
|
||||
'DOCTOR_INFO_CHANGE',
|
||||
'DOCTOR_STATUS_CHANGE',
|
||||
'DOCTOR_DELETE',
|
||||
// Hospital
|
||||
'ACCOUNT_CREATE',
|
||||
'ACCOUNT_INFO_CHANGE',
|
||||
'ACCOUNT_AUTH_CHANGE',
|
||||
'ACCOUNT_STATUS_CHANGE',
|
||||
// BA
|
||||
'BA_CREATE',
|
||||
'BA_INFO_CHANGE',
|
||||
// Business Document
|
||||
'BUSINESS_ORDER_CREATE',
|
||||
'BUSINESS_ORDER_DATA_CHANGE',
|
||||
'BUSINESS_ORDER_STATUS_CHANGE',
|
||||
'BUSINESS_ORDER_CHECK',
|
||||
// Sale Document
|
||||
'SALES_ORDER_CREATE',
|
||||
'SALES_ORDER_DATA_CHANGE',
|
||||
'SALES_ORDER_STATUS_CHANGE',
|
||||
// Contract
|
||||
'CONTRACT_CREATE',
|
||||
'CONTRACT_INFO_CHANGE',
|
||||
// Lead Hospital
|
||||
'LEAD_HOSPITAL_CREATE',
|
||||
'LEAD_HOSPITAL_CHANGE',
|
||||
// Lead Doctor
|
||||
'CREATE_LEADS',
|
||||
'LEAD_UPDATE',
|
||||
// GROUP
|
||||
'GROUP_CREATE',
|
||||
'GROUP_INFO_CHANGE',
|
||||
// Hospital Enter
|
||||
'ACCOUNT_ENTER_INFO_REJECT',
|
||||
// Orthodontic
|
||||
'APPLIANCE_CREATE',
|
||||
'APPLIANCE_CHANGE',
|
||||
'RETAINER_CHANGE',
|
||||
'SET_MENU_CHANGE',
|
||||
];
|
||||
|
||||
/**
|
||||
* 查询CRM事件消费者表的消息数据
|
||||
* 只查询Agent监听的topic
|
||||
*/
|
||||
public function getCrmEventConsumers(
|
||||
?Carbon $startTime = null,
|
||||
?Carbon $endTime = null,
|
||||
array $excludeMessages = []
|
||||
): array {
|
||||
try {
|
||||
$query = DB::connection('crmslave')
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
742
app/Services/JiraService.php
Normal file
742
app/Services/JiraService.php
Normal file
@@ -0,0 +1,742 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use JiraRestApi\Configuration\ArrayConfiguration;
|
||||
use JiraRestApi\Issue\IssueService;
|
||||
use JiraRestApi\JiraException;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class JiraService
|
||||
{
|
||||
private IssueService $issueService;
|
||||
private array $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
349
app/Services/MessageDispatchService.php
Normal file
349
app/Services/MessageDispatchService.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Clients\MonoClient;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MessageDispatchService
|
||||
{
|
||||
private MonoClient $monoClient;
|
||||
|
||||
public function __construct(MonoClient $monoClient)
|
||||
{
|
||||
$this->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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
191
app/Services/MessageSyncService.php
Normal file
191
app/Services/MessageSyncService.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Clients\AgentClient;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Collection;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MessageSyncService
|
||||
{
|
||||
private AgentClient $agentClient;
|
||||
|
||||
public function __construct(AgentClient $agentClient)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user