feat: 新增 Docker 部署支持、Swoole/Octane 集成及相关优化

- 添加 Dockerfile 与多套 docker-compose 配置(开发/生产环境)
- 集成 Laravel Octane (Swoole) 提升性能
- 新增健康检查、监控脚本及部署文档
- 新增 Docker 镜像离线导入包(MySQL/Redis/Meilisearch)
- 优化文档转换、预览服务及队列任务
- 添加 CreateAdminUser 命令与路由健康检查接口
- 新增 Swoole 队列兼容性测试套件

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 15:51:19 +08:00
parent acf549c43c
commit 3c206e9e06
90 changed files with 12731 additions and 1255 deletions

View File

@@ -0,0 +1,196 @@
<?php
namespace Tests\Feature;
use App\Jobs\ConvertDocumentToMarkdown;
use App\Models\Document;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
/**
* 队列系统验证测试
*
* 验证队列系统在 Swoole 环境下的基本功能
*/
class QueueSystemValidationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// 禁用搜索功能以避免 Meilisearch 连接问题
config(['scout.driver' => 'null']);
}
/**
* 测试队列系统基本功能
*
* @test
*/
public function test_queue_system_basic_functionality()
{
// 验证队列配置
$this->assertNotEmpty(config('queue.default'), '队列默认连接未配置');
// 验证队列连接配置
$defaultConnection = config('queue.default');
$connectionConfig = config("queue.connections.{$defaultConnection}");
$this->assertIsArray($connectionConfig, '队列连接配置无效');
$this->assertArrayHasKey('driver', $connectionConfig, '队列驱动未配置');
// 验证队列命令可用性
$exitCode = Artisan::call('queue:work', ['--help' => true]);
$this->assertEquals(0, $exitCode, '队列工作命令不可用');
}
/**
* 测试文档转换任务的基本功能
*
* @test
*/
public function test_document_conversion_job_basic_functionality()
{
// 创建测试数据
$user = User::factory()->create();
$document = Document::factory()->create([
'uploaded_by' => $user->id,
'title' => '验证测试文档',
]);
// 使用模拟队列
Queue::fake();
// 分发任务
ConvertDocumentToMarkdown::dispatch($document);
// 验证任务已分发
Queue::assertPushed(ConvertDocumentToMarkdown::class);
// 验证任务配置
$job = new ConvertDocumentToMarkdown($document);
$this->assertIsNumeric($job->tries, '任务重试次数配置无效');
$this->assertIsNumeric($job->timeout, '任务超时配置无效');
$this->assertIsNumeric($job->backoff, '任务重试延迟配置无效');
}
/**
* 测试 Swoole 与队列的兼容性配置
*
* @test
*/
public function test_swoole_queue_compatibility_configuration()
{
// 验证 Octane 配置
$octaneServer = config('octane.server');
$this->assertEquals('swoole', $octaneServer, 'Octane 服务器未配置为 Swoole');
// 验证 Octane 任务工作进程配置
$taskWorkers = config('octane.task_workers', env('OCTANE_TASK_WORKERS'));
$this->assertIsNumeric($taskWorkers, 'Octane 任务工作进程数量配置无效');
$this->assertGreaterThan(0, $taskWorkers, 'Octane 任务工作进程数量必须大于 0');
// 验证队列与 Swoole 的兼容性
$queueConnection = config('queue.default');
$queueDriver = config("queue.connections.{$queueConnection}.driver");
$supportedDrivers = ['database', 'redis', 'sync'];
$this->assertContains(
$queueDriver,
$supportedDrivers,
"队列驱动 {$queueDriver} 可能与 Swoole 不兼容"
);
}
/**
* 测试队列任务的内存管理
*
* @test
*/
public function test_queue_memory_management()
{
$initialMemory = memory_get_usage();
// 创建多个任务实例
$user = User::factory()->create();
$jobs = [];
for ($i = 0; $i < 10; $i++) {
$document = Document::factory()->create(['uploaded_by' => $user->id]);
$jobs[] = new ConvertDocumentToMarkdown($document);
}
// 清理任务实例
unset($jobs);
gc_collect_cycles();
$finalMemory = memory_get_usage();
$memoryIncrease = $finalMemory - $initialMemory;
// 验证内存使用合理
$this->assertLessThan(5 * 1024 * 1024, $memoryIncrease, '队列任务内存使用过多');
}
/**
* 测试队列错误处理配置
*
* @test
*/
public function test_queue_error_handling_configuration()
{
// 验证失败队列配置
$failedDriver = config('queue.failed.driver');
$this->assertNotEmpty($failedDriver, '失败队列驱动未配置');
// 验证文档转换相关配置
$retryTimes = config('documents.conversion.retry_times', 3);
$this->assertIsNumeric($retryTimes, '队列重试次数配置无效');
$this->assertGreaterThan(0, $retryTimes, '队列重试次数必须大于 0');
$retryDelay = config('documents.conversion.retry_delay', 60);
$this->assertIsNumeric($retryDelay, '队列重试延迟配置无效');
$this->assertGreaterThanOrEqual(0, $retryDelay, '队列重试延迟不能为负数');
$timeout = config('documents.conversion.timeout', 300);
$this->assertIsNumeric($timeout, '队列任务超时配置无效');
$this->assertGreaterThan(0, $timeout, '队列任务超时时间必须大于 0');
}
/**
* 测试队列系统的整体健康状态
*
* @test
*/
public function test_queue_system_health()
{
// 验证数据库表存在(如果使用数据库队列)
if (config('queue.default') === 'database') {
$this->assertTrue(\Schema::hasTable('jobs'), '队列任务表不存在');
$this->assertTrue(\Schema::hasTable('failed_jobs'), '失败任务表不存在');
}
// 验证队列重启命令
$exitCode = Artisan::call('queue:restart');
$this->assertEquals(0, $exitCode, '队列重启命令执行失败');
// 验证基本的队列操作
Queue::fake();
$user = User::factory()->create();
$document = Document::factory()->create(['uploaded_by' => $user->id]);
// 测试任务分发
ConvertDocumentToMarkdown::dispatch($document);
Queue::assertPushed(ConvertDocumentToMarkdown::class);
// 测试任务序列化
$job = new ConvertDocumentToMarkdown($document);
$serialized = serialize($job);
$unserialized = unserialize($serialized);
$this->assertInstanceOf(ConvertDocumentToMarkdown::class, $unserialized);
}
}