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,246 @@
<?php
namespace Tests\Feature;
use App\Jobs\ConvertDocumentToMarkdown;
use App\Models\Document;
use App\Models\User;
use App\Services\DocumentConversionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* Swoole 队列系统兼容性测试
*
* 验证现有队列任务在 Swoole 环境下的正常运行
*/
class SwooleQueueCompatibilityTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// 设置测试存储磁盘
Storage::fake('documents');
Storage::fake('markdown');
// 禁用搜索功能以避免 Meilisearch 连接问题
config(['scout.driver' => 'null']);
}
/**
* 测试文档转换队列任务可以正常分发
*
* @test
*/
public function test_document_conversion_job_can_be_dispatched()
{
// 创建测试用户和文档
$user = User::factory()->create();
$document = Document::factory()->create([
'uploaded_by' => $user->id,
'title' => '测试文档',
'file_path' => 'test-document.docx',
]);
// 模拟队列
Queue::fake();
// 分发队列任务
ConvertDocumentToMarkdown::dispatch($document);
// 验证任务已被分发
Queue::assertPushed(ConvertDocumentToMarkdown::class, function ($job) use ($document) {
// 使用反射来访问受保护的属性
$reflection = new \ReflectionClass($job);
$documentProperty = $reflection->getProperty('document');
$documentProperty->setAccessible(true);
$jobDocument = $documentProperty->getValue($job);
return $jobDocument->id === $document->id;
});
}
/**
* 测试队列任务在 Swoole 环境下的执行
*
* @test
*/
public function test_queue_job_execution_in_swoole_environment()
{
// 创建测试用户和文档
$user = User::factory()->create();
$document = Document::factory()->create([
'uploaded_by' => $user->id,
'title' => '测试文档转换',
'file_path' => 'test-conversion.docx',
]);
// 创建模拟的文档文件
Storage::disk('documents')->put($document->file_path, 'test content');
// 模拟转换服务
$conversionService = $this->createMock(DocumentConversionService::class);
$conversionService->expects($this->once())
->method('convertToMarkdown')
->with($document)
->willReturn([
'markdown' => '# 测试文档\n\n这是测试内容',
'tempDir' => '/tmp/test',
]);
$conversionService->expects($this->once())
->method('saveMarkdownToFile')
->willReturn('markdown/test-document.md');
$conversionService->expects($this->once())
->method('updateDocumentMarkdown');
$this->app->instance(DocumentConversionService::class, $conversionService);
// 执行队列任务
$job = new ConvertDocumentToMarkdown($document);
$job->handle($conversionService);
// 验证任务执行成功(没有抛出异常)
$this->assertTrue(true);
}
/**
* 测试队列任务失败处理
*
* @test
*/
public function test_queue_job_failure_handling()
{
// 创建测试用户和文档
$user = User::factory()->create();
$document = Document::factory()->create([
'uploaded_by' => $user->id,
'title' => '失败测试文档',
'file_path' => 'fail-test.docx',
]);
// 模拟转换服务抛出异常
$conversionService = $this->createMock(DocumentConversionService::class);
$conversionService->expects($this->once())
->method('convertToMarkdown')
->willThrowException(new \Exception('转换失败'));
$this->app->instance(DocumentConversionService::class, $conversionService);
// 执行队列任务并期望异常
$job = new ConvertDocumentToMarkdown($document);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('转换失败');
$job->handle($conversionService);
}
/**
* 测试队列任务的重试机制
*
* @test
*/
public function test_queue_job_retry_mechanism()
{
// 创建测试文档
$user = User::factory()->create();
$document = Document::factory()->create([
'uploaded_by' => $user->id,
'title' => '重试测试文档',
]);
// 创建队列任务实例
$job = new ConvertDocumentToMarkdown($document);
// 验证重试配置
$this->assertEquals(config('documents.conversion.retry_times', 3), $job->tries);
$this->assertEquals(config('documents.conversion.timeout', 300), $job->timeout);
$this->assertEquals(config('documents.conversion.retry_delay', 60), $job->backoff);
}
/**
* 测试队列连接配置
*
* @test
*/
public function test_queue_connection_configuration()
{
// 验证队列连接配置
$defaultConnection = config('queue.default');
$this->assertNotEmpty($defaultConnection);
// 验证数据库队列连接配置
$databaseConfig = config('queue.connections.database');
$this->assertIsArray($databaseConfig);
$this->assertEquals('database', $databaseConfig['driver']);
$this->assertEquals('jobs', $databaseConfig['table']);
}
/**
* 测试队列在 Swoole 环境下的内存管理
*
* @test
*/
public function test_queue_memory_management_in_swoole()
{
// 获取初始内存使用量
$initialMemory = memory_get_usage();
// 创建多个队列任务
$user = User::factory()->create();
$documents = Document::factory()->count(5)->create(['uploaded_by' => $user->id]);
Queue::fake();
// 分发多个任务
foreach ($documents as $document) {
ConvertDocumentToMarkdown::dispatch($document);
}
// 验证任务都被分发
Queue::assertPushed(ConvertDocumentToMarkdown::class, 5);
// 检查内存使用是否在合理范围内
$currentMemory = memory_get_usage();
$memoryIncrease = $currentMemory - $initialMemory;
// 内存增长应该在合理范围内(小于 10MB
$this->assertLessThan(10 * 1024 * 1024, $memoryIncrease, '队列任务内存使用过多');
}
/**
* 测试队列任务的序列化和反序列化
*
* @test
*/
public function test_queue_job_serialization()
{
// 创建测试文档
$user = User::factory()->create();
$document = Document::factory()->create(['uploaded_by' => $user->id]);
// 创建队列任务
$job = new ConvertDocumentToMarkdown($document);
// 序列化任务
$serialized = serialize($job);
$this->assertIsString($serialized);
// 反序列化任务
$unserialized = unserialize($serialized);
$this->assertInstanceOf(ConvertDocumentToMarkdown::class, $unserialized);
// 使用反射来访问受保护的属性
$reflection = new \ReflectionClass($unserialized);
$documentProperty = $reflection->getProperty('document');
$documentProperty->setAccessible(true);
$jobDocument = $documentProperty->getValue($unserialized);
$this->assertEquals($document->id, $jobDocument->id);
}
}