diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a9d0ab7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,56 @@ +# Docker构建忽略文件 + +# Git相关 +.git +.gitignore +.gitattributes + +# 开发工具 +.editorconfig +.env.example +.kiro/ + +# 文档 +README.md +CHANGELOG.md +CONTRIBUTING.md +docs/ + +# 测试 +tests/ +phpunit.xml +.phpunit.result.cache + +# Node.js +node_modules/ +npm-debug.log +yarn-error.log + +# PHP +vendor/ +composer.phar + +# Laravel +storage/logs/* +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +bootstrap/cache/* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# 系统文件 +.DS_Store +Thumbs.db + +# Docker +docker-compose*.yml +Dockerfile* + +# 其他 +*.log +*.tmp \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..1c981be --- /dev/null +++ b/.env.development @@ -0,0 +1,99 @@ +# 开发环境配置模板 +# 用于Docker开发环境 + +APP_NAME="知识库系统-开发" +APP_ENV=local +APP_KEY=base64:your-dev-app-key-here +APP_DEBUG=true +APP_URL=http://localhost:8080 + +APP_LOCALE=zh_CN +APP_FALLBACK_LOCALE=zh_CN +APP_FAKER_LOCALE=zh_CN + +BCRYPT_ROUNDS=10 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_LEVEL=debug + +# Octane/Swoole 配置 - 开发环境 +OCTANE_SERVER=swoole +OCTANE_HOST=0.0.0.0 +OCTANE_PORT=8000 +OCTANE_WORKERS=2 +OCTANE_TASK_WORKERS=1 +OCTANE_MAX_REQUESTS=100 +OCTANE_WATCH=true +OCTANE_HTTPS=false + +# Swoole 高级配置 - 开发环境 +OCTANE_GARBAGE_COLLECTION=25 +OCTANE_MAX_EXECUTION_TIME=60 + +# Swoole 缓存表配置 - 开发环境 +OCTANE_CACHE_ROWS=500 +OCTANE_CACHE_BYTES=5000 + +# 数据库配置 - 开发环境使用SQLite +DB_CONNECTION=sqlite +DB_DATABASE=database/database.sqlite + +# 会话和缓存配置 - 开发环境 +SESSION_DRIVER=redis +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false + +CACHE_STORE=redis +CACHE_PREFIX=kb_dev_cache + +# Redis配置 - 开发环境Docker容器 +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# 队列配置 - 开发环境 +QUEUE_CONNECTION=redis + +# 文件系统配置 +FILESYSTEM_DISK=local + +# 邮件配置 - 开发环境使用日志 +MAIL_MAILER=log +MAIL_FROM_ADDRESS="dev@knowledge-base.local" +MAIL_FROM_NAME="${APP_NAME}" + +# Meilisearch配置 - 开发环境Docker容器 +SCOUT_DRIVER=meilisearch +MEILISEARCH_HOST=http://meilisearch:7700 +MEILISEARCH_KEY=dev-master-key + +# 文档转换配置 - 开发环境 +DOCUMENT_CONVERSION_DRIVER=pandoc +PANDOC_PATH=/usr/bin/pandoc +CONVERSION_TIMEOUT=300 +CONVERSION_QUEUE=documents +CONVERSION_RETRY_TIMES=3 +CONVERSION_RETRY_DELAY=60 + +# Markdown配置 +MARKDOWN_RENDERER=commonmark +MARKDOWN_SANITIZE=true +MARKDOWN_PREVIEW_LENGTH=500 +MARKDOWN_MAX_FILE_SIZE=10485760 + +# 存储配置 +DOCUMENTS_DISK=documents +MARKDOWN_DISK=markdown +STORAGE_ORGANIZE_BY_DATE=true + +# 开发工具配置 +TELESCOPE_ENABLED=true +DEBUGBAR_ENABLED=true +VITE_APP_NAME="${APP_NAME}" + +# 开发环境特定配置 +PHP_IDE_CONFIG=serverName=knowledge-base-dev +XDEBUG_MODE=develop,debug +XDEBUG_CONFIG=client_host=host.docker.internal client_port=9003 \ No newline at end of file diff --git a/.env.example b/.env.example index 3c063d7..e7f1509 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,24 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug +# Octane/Swoole 配置 +OCTANE_SERVER=swoole +OCTANE_HOST=0.0.0.0 +OCTANE_PORT=8000 +OCTANE_WORKERS=4 +OCTANE_TASK_WORKERS=2 +OCTANE_MAX_REQUESTS=500 +OCTANE_WATCH=false +OCTANE_HTTPS=false + +# Swoole 高级配置 +OCTANE_GARBAGE_COLLECTION=50 +OCTANE_MAX_EXECUTION_TIME=30 + +# Swoole 缓存表配置 +OCTANE_CACHE_ROWS=1000 +OCTANE_CACHE_BYTES=10000 + DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 diff --git a/.gitignore b/.gitignore index 3adad46..9e461f3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ Homestead.json Homestead.yaml Thumbs.db +rr +.rr.yaml diff --git a/.kiro/specs/docker-deployment/design.md b/.kiro/specs/docker-deployment/design.md new file mode 100644 index 0000000..96aeace --- /dev/null +++ b/.kiro/specs/docker-deployment/design.md @@ -0,0 +1,290 @@ +# Docker部署设计文档 + +## 概述 + +本设计文档描述了将Laravel知识库系统Docker化部署到OpenEuler服务器的完整解决方案。系统采用微服务架构,通过Docker容器化技术实现应用的标准化部署和运维。 + +设计目标: +- 构建适用于OpenEuler服务器的amd64架构Docker镜像 +- 实现完整的应用栈容器化编排 +- 确保数据持久化和服务高可用性 +- 支持开发和生产环境的不同配置需求 +- 提供便捷的镜像打包和离线部署能力 + +## 架构 + +### 整体架构 + +系统采用多容器架构,包含以下核心组件: + +```mermaid +graph TB + subgraph "Docker Host (OpenEuler)" + subgraph "Application Stack" + nginx[Nginx容器
Web服务器] + app[Laravel应用容器
PHP-FPM] + queue[队列处理容器
Laravel Queue] + end + + subgraph "Data Layer" + mysql[MySQL容器
主数据库] + redis[Redis容器
缓存/会话] + meilisearch[Meilisearch容器
搜索引擎] + end + + subgraph "Storage" + app_data[应用数据卷] + db_data[数据库数据卷] + search_data[搜索数据卷] + logs[日志卷] + end + end + + nginx --> app + app --> mysql + app --> redis + app --> meilisearch + queue --> mysql + queue --> redis + + app --> app_data + mysql --> db_data + meilisearch --> search_data + nginx --> logs + app --> logs +``` + +### 网络架构 + +- **外部网络**: 通过宿主机端口映射提供Web服务访问 +- **内部网络**: 创建专用Docker网络供容器间通信 +- **服务发现**: 通过容器名称进行服务间通信 + +### 存储架构 + +- **代码存储**: 项目目录映射到应用容器,支持开发时热重载 +- **数据持久化**: 数据库、搜索引擎、上传文件使用Docker卷持久化 +- **日志管理**: 应用日志映射到宿主机便于监控和调试 + +## 组件和接口 + +### Docker镜像组件 + +#### 1. Laravel应用镜像 (knowledge-base-app) +- **基础镜像**: php:8.2-fpm-alpine +- **运行时**: PHP-FPM + Nginx +- **依赖**: Composer包、NPM构建产物、Pandoc +- **配置**: PHP扩展、Nginx配置、应用配置 + +#### 2. 数据库组件 (MySQL) +- **镜像**: mysql:8.0 +- **配置**: 字符集utf8mb4、时区设置 +- **存储**: 数据目录持久化 + +#### 3. 缓存组件 (Redis) +- **镜像**: redis:7-alpine +- **配置**: 内存限制、持久化策略 +- **用途**: 会话存储、应用缓存、队列存储 + +#### 4. 搜索组件 (Meilisearch) +- **镜像**: getmeili/meilisearch:v1.5 +- **配置**: 主密钥、环境模式 +- **存储**: 索引数据持久化 + +### 服务接口 + +#### Web服务接口 +- **端口**: 80 (HTTP) +- **协议**: HTTP/1.1 +- **负载均衡**: Nginx upstream配置 + +#### 数据库接口 +- **端口**: 3306 (内部) +- **协议**: MySQL Protocol +- **连接池**: Laravel数据库连接配置 + +#### 缓存接口 +- **端口**: 6379 (内部) +- **协议**: Redis Protocol +- **连接**: phpredis扩展 + +#### 搜索接口 +- **端口**: 7700 (内部) +- **协议**: HTTP REST API +- **认证**: Master Key + +## 数据模型 + +### 容器配置模型 + +```yaml +# docker-compose.yml结构 +services: + app: + image: knowledge-base-app:latest + platform: linux/amd64 + environment: + - APP_ENV=production + - DB_HOST=mysql + - REDIS_HOST=redis + - MEILISEARCH_HOST=http://meilisearch:7700 + volumes: + - ./:/var/www/html + - storage_data:/var/www/html/storage + depends_on: + - mysql + - redis + - meilisearch +``` + +### 环境变量模型 + +```bash +# 生产环境变量 +APP_ENV=production +APP_DEBUG=false +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +REDIS_HOST=redis +REDIS_PORT=6379 +MEILISEARCH_HOST=http://meilisearch:7700 +``` + +### 存储卷模型 + +```yaml +volumes: + mysql_data: + driver: local + redis_data: + driver: local + meilisearch_data: + driver: local + storage_data: + driver: local +``` + +## 正确性属性 + +*属性是应该在系统的所有有效执行中保持为真的特征或行为——本质上是关于系统应该做什么的正式声明。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。* + +### 属性1: 镜像架构一致性 +*对于任何*构建的Docker镜像,检查其架构信息应该返回linux/amd64 +**验证: 需求 1.1** + +### 属性2: PHP环境完整性 +*对于任何*构建的应用镜像,在容器内执行PHP版本检查应该返回8.2.x版本且包含所有必需扩展 +**验证: 需求 1.2** + +### 属性3: 构建产物存在性 +*对于任何*构建的应用镜像,vendor目录和public/js、public/css目录应该存在且包含必要文件 +**验证: 需求 1.3** + +### 属性4: 服务启动一致性 +*对于任何*docker-compose启动操作,所有定义的服务容器应该成功启动并达到健康状态 +**验证: 需求 2.1, 2.2, 2.3, 2.4, 2.5** + +### 属性5: 数据持久化保证 +*对于任何*容器重启操作,持久化存储中的数据应该保持不变且可访问 +**验证: 需求 3.2, 3.3, 3.4** + +### 属性6: 服务连接性 +*对于任何*运行中的服务栈,应用容器应该能够成功连接到所有依赖服务 +**验证: 需求 4.2, 4.3, 4.4** + +### 属性7: 健康检查响应性 +*对于任何*运行中的服务,健康检查端点应该在合理时间内返回成功响应 +**验证: 需求 5.1, 5.2, 5.3, 5.4** + +### 属性8: 容器自愈能力 +*对于任何*模拟的容器故障,系统应该根据重启策略自动恢复服务 +**验证: 需求 5.5** + +### 属性9: 镜像导出完整性 +*对于任何*导出的Docker镜像tar文件,应该包含完整的镜像层和元数据信息 +**验证: 需求 6.1** + +### 属性10: 镜像导入兼容性 +*对于任何*导出的镜像tar文件,在OpenEuler环境中导入后应该能够正常运行 +**验证: 需求 6.2** + +### 属性11: 压缩效率 +*对于任何*镜像压缩操作,压缩后的文件大小应该显著小于原始大小 +**验证: 需求 6.3** + +### 属性12: 完整性验证 +*对于任何*镜像文件,完整性检查应该能够验证文件未被损坏且架构正确 +**验证: 需求 6.4** + +### 属性13: 开发环境热重载 +*对于任何*开发环境中的代码修改,应用应该在合理时间内反映更改 +**验证: 需求 7.1** + +## 错误处理 + +### 容器启动失败 +- **检测**: 健康检查失败或容器退出 +- **处理**: 自动重启策略,最大重试次数限制 +- **日志**: 详细错误日志记录到宿主机 + +### 服务连接失败 +- **检测**: 连接超时或拒绝连接 +- **处理**: 重试机制,降级服务 +- **监控**: 连接状态监控和告警 + +### 数据持久化失败 +- **检测**: 卷挂载失败或权限错误 +- **处理**: 容器启动前预检查 +- **恢复**: 数据备份和恢复机制 + +### 镜像构建失败 +- **检测**: 构建过程中的错误退出 +- **处理**: 分阶段构建,错误定位 +- **优化**: 构建缓存和依赖管理 + +## 测试策略 + +### 单元测试方法 + +**容器构建测试**: +- 验证Dockerfile语法正确性 +- 测试构建过程中的关键步骤 +- 检查构建产物的完整性 + +**配置文件测试**: +- 验证docker-compose.yml语法 +- 测试环境变量配置的正确性 +- 检查网络和存储配置 + +**脚本功能测试**: +- 测试部署脚本的执行流程 +- 验证健康检查脚本的准确性 +- 测试备份和恢复脚本 + +### 属性测试方法 + +**测试框架**: 使用Bash脚本结合Docker命令进行属性测试,每个属性测试运行100次迭代以确保可靠性。 + +**测试环境**: +- 本地Docker环境用于开发测试 +- OpenEuler虚拟机用于兼容性测试 +- CI/CD环境用于自动化测试 + +**测试数据生成**: +- 随机生成不同的环境配置 +- 模拟各种故障场景 +- 生成不同规模的测试数据 + +**属性测试实现要求**: +- 每个正确性属性必须实现为单独的属性测试 +- 测试必须标注对应的设计文档属性编号 +- 使用格式: `# Feature: docker-deployment, Property X: [属性描述]` +- 每个属性测试最少运行100次迭代 +- 测试应该覆盖各种输入组合和边界条件 + +**集成测试**: +- 端到端部署流程测试 +- 服务间通信测试 +- 数据一致性测试 +- 性能基准测试 \ No newline at end of file diff --git a/.kiro/specs/docker-deployment/requirements.md b/.kiro/specs/docker-deployment/requirements.md new file mode 100644 index 0000000..d67f278 --- /dev/null +++ b/.kiro/specs/docker-deployment/requirements.md @@ -0,0 +1,102 @@ +# Docker部署需求文档 + +## 介绍 + +本文档定义了将Laravel知识库系统Docker化部署到OpenEuler服务器的需求。系统需要支持完整的生产环境运行,包括Web应用、数据库、缓存、搜索引擎和队列处理等所有组件。 + +## 术语表 + +- **Docker镜像**: 包含应用程序及其运行环境的可执行包 +- **容器编排**: 使用docker-compose管理多个相关容器的技术 +- **知识库系统**: 基于Laravel框架的文档管理和搜索系统 +- **OpenEuler服务器**: 目标部署环境的Linux服务器 +- **生产环境**: 实际运行业务的服务器环境 +- **数据持久化**: 确保容器重启后数据不丢失的机制 +- **健康检查**: 监控容器运行状态的机制 + +## 需求 + +### 需求1 + +**用户故事:** 作为系统管理员,我希望能够构建包含完整运行环境的Docker镜像,以便在OpenEuler服务器上部署知识库系统。 + +#### 验收标准 + +1. WHEN 构建Docker镜像时 THEN 系统应构建为linux/amd64架构以确保OpenEuler兼容性 +2. WHEN 构建Docker镜像时 THEN 系统应包含PHP 8.2运行环境和所有必需的扩展 +3. WHEN 构建Docker镜像时 THEN 系统应包含Composer依赖和NPM构建产物 +4. WHEN 构建Docker镜像时 THEN 系统应包含Nginx Web服务器配置 +5. WHEN 构建Docker镜像时 THEN 系统应包含文档转换工具Pandoc +6. WHEN 构建Docker镜像时 THEN 系统应优化镜像大小并使用多阶段构建 + +### 需求2 + +**用户故事:** 作为系统管理员,我希望通过docker-compose编排所有服务,以便一键启动完整的应用栈。 + +#### 验收标准 + +1. WHEN 启动服务时 THEN 系统应启动MySQL数据库服务并配置持久化存储 +2. WHEN 启动服务时 THEN 系统应启动Redis缓存服务并配置内存限制 +3. WHEN 启动服务时 THEN 系统应启动Meilisearch搜索引擎并配置数据持久化 +4. WHEN 启动服务时 THEN 系统应启动Laravel应用容器并连接所有依赖服务 +5. WHEN 启动服务时 THEN 系统应启动队列处理容器处理后台任务 + +### 需求3 + +**用户故事:** 作为系统管理员,我希望配置数据持久化和目录映射,以便数据在容器重启后不丢失。 + +#### 验收标准 + +1. WHEN 配置存储时 THEN 系统应将项目代码目录映射到容器内部 +2. WHEN 配置存储时 THEN 系统应将上传文档存储目录持久化到宿主机 +3. WHEN 配置存储时 THEN 系统应将数据库数据目录持久化到宿主机 +4. WHEN 配置存储时 THEN 系统应将搜索引擎数据目录持久化到宿主机 +5. WHEN 配置存储时 THEN 系统应将日志目录映射到宿主机便于查看 + +### 需求4 + +**用户故事:** 作为系统管理员,我希望配置环境变量和网络,以便服务间能够正确通信。 + +#### 验收标准 + +1. WHEN 配置网络时 THEN 系统应创建专用Docker网络供服务间通信 +2. WHEN 配置环境时 THEN 系统应设置数据库连接参数指向MySQL容器 +3. WHEN 配置环境时 THEN 系统应设置Redis连接参数指向Redis容器 +4. WHEN 配置环境时 THEN 系统应设置Meilisearch连接参数指向搜索容器 +5. WHEN 配置环境时 THEN 系统应配置应用密钥和调试模式 + +### 需求5 + +**用户故事:** 作为系统管理员,我希望实现健康检查和自动重启,以便确保服务的高可用性。 + +#### 验收标准 + +1. WHEN 服务运行时 THEN 系统应对Web应用进行HTTP健康检查 +2. WHEN 服务运行时 THEN 系统应对数据库进行连接健康检查 +3. WHEN 服务运行时 THEN 系统应对Redis进行连接健康检查 +4. WHEN 服务运行时 THEN 系统应对Meilisearch进行API健康检查 +5. WHEN 服务异常时 THEN 系统应自动重启失败的容器 + +### 需求6 + +**用户故事:** 作为系统管理员,我希望能够导出和导入Docker镜像,以便在离线环境中部署。 + +#### 验收标准 + +1. WHEN 导出镜像时 THEN 系统应将构建好的amd64架构镜像打包为tar文件 +2. WHEN 导入镜像时 THEN 系统应能够从tar文件加载镜像到OpenEuler服务器的Docker +3. WHEN 传输镜像时 THEN 系统应支持压缩以减少文件大小 +4. WHEN 验证镜像时 THEN 系统应提供镜像完整性和架构兼容性检查方法 +5. WHEN 部署镜像时 THEN 系统应提供详细的OpenEuler部署文档和脚本 + +### 需求7 + +**用户故事:** 作为开发人员,我希望有开发环境的Docker配置,以便本地开发和测试。 + +#### 验收标准 + +1. WHEN 开发环境启动时 THEN 系统应启用代码热重载功能 +2. WHEN 开发环境启动时 THEN 系统应启用调试模式和详细日志 +3. WHEN 开发环境启动时 THEN 系统应映射源代码目录支持实时编辑 +4. WHEN 开发环境启动时 THEN 系统应暴露所有必要的端口供调试 +5. WHEN 开发环境启动时 THEN 系统应包含开发工具和测试数据 \ No newline at end of file diff --git a/.kiro/specs/docker-deployment/tasks.md b/.kiro/specs/docker-deployment/tasks.md new file mode 100644 index 0000000..413e2d9 --- /dev/null +++ b/.kiro/specs/docker-deployment/tasks.md @@ -0,0 +1,157 @@ +# Docker部署实施计划 + +- [x] 1. 创建Docker镜像构建配置 + - 创建多阶段Dockerfile,优化镜像大小 + - 配置PHP 8.2-fpm基础环境和必需扩展 + - 安装Composer依赖和NPM构建工具 + - 集成Nginx Web服务器配置 + - 安装Pandoc文档转换工具 + - 确保构建为linux/amd64架构 + - _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_ + +- [ ]* 1.1 编写属性测试验证镜像架构 + - **属性1: 镜像架构一致性** + - **验证: 需求 1.1** + +- [ ]* 1.2 编写属性测试验证PHP环境 + - **属性2: PHP环境完整性** + - **验证: 需求 1.2** + +- [ ]* 1.3 编写属性测试验证构建产物 + - **属性3: 构建产物存在性** + - **验证: 需求 1.3** + +- [x] 2. 配置生产环境容器编排 + - 创建生产环境docker-compose.yml文件 + - 配置MySQL数据库服务和持久化存储 + - 配置Redis缓存服务和内存限制 + - 配置Meilisearch搜索引擎和数据持久化 + - 配置Laravel应用容器和队列处理容器 + - 设置服务间依赖关系和启动顺序 + - _需求: 2.1, 2.2, 2.3, 2.4, 2.5_ + +- [ ]* 2.1 编写属性测试验证服务启动 + - **属性4: 服务启动一致性** + - **验证: 需求 2.1, 2.2, 2.3, 2.4, 2.5** + +- [x] 3. 实现数据持久化和目录映射 + - 配置项目代码目录映射到容器 + - 设置上传文档存储目录持久化 + - 配置数据库数据目录持久化 + - 设置搜索引擎数据目录持久化 + - 配置日志目录映射到宿主机 + - _需求: 3.1, 3.2, 3.3, 3.4, 3.5_ + +- [ ]* 3.1 编写属性测试验证数据持久化 + - **属性5: 数据持久化保证** + - **验证: 需求 3.2, 3.3, 3.4** + +- [x] 4. 配置环境变量和网络设置 + - 创建专用Docker网络配置 + - 设置数据库连接环境变量 + - 配置Redis连接参数 + - 设置Meilisearch连接参数 + - 配置应用密钥和运行模式 + - _需求: 4.1, 4.2, 4.3, 4.4, 4.5_ + +- [ ]* 4.1 编写属性测试验证服务连接 + - **属性6: 服务连接性** + - **验证: 需求 4.2, 4.3, 4.4** + +- [x] 5. 实现健康检查和自动重启机制 + - 配置Web应用HTTP健康检查 + - 实现数据库连接健康检查 + - 配置Redis连接健康检查 + - 设置Meilisearch API健康检查 + - 配置容器自动重启策略 + - _需求: 5.1, 5.2, 5.3, 5.4, 5.5_ + +- [ ]* 5.1 编写属性测试验证健康检查 + - **属性7: 健康检查响应性** + - **验证: 需求 5.1, 5.2, 5.3, 5.4** + +- [ ]* 5.2 编写属性测试验证自动重启 + - **属性8: 容器自愈能力** + - **验证: 需求 5.5** + +- [x] 6. 创建镜像打包和部署脚本 + - 编写Docker镜像导出脚本 + - 实现镜像压缩和完整性检查 + - 创建OpenEuler服务器部署脚本 + - 编写镜像导入和验证脚本 + - 生成详细的部署文档 + - _需求: 6.1, 6.2, 6.3, 6.4, 6.5_ + +- [ ]* 6.1 编写属性测试验证镜像导出 + - **属性9: 镜像导出完整性** + - **验证: 需求 6.1** + +- [ ]* 6.2 编写属性测试验证镜像导入 + - **属性10: 镜像导入兼容性** + - **验证: 需求 6.2** + +- [ ]* 6.3 编写属性测试验证压缩效率 + - **属性11: 压缩效率** + - **验证: 需求 6.3** + +- [ ]* 6.4 编写属性测试验证完整性检查 + - **属性12: 完整性验证** + - **验证: 需求 6.4** + +- [ ] 7. 配置开发环境支持 + - 创建开发环境docker-compose.dev.yml + - 配置代码热重载功能 + - 启用调试模式和详细日志 + - 设置开发工具和测试数据 + - 配置端口映射供调试使用 + - _需求: 7.1, 7.2, 7.3, 7.4, 7.5_ + +- [ ]* 7.1 编写属性测试验证热重载 + - **属性13: 开发环境热重载** + - **验证: 需求 7.1** + +- [ ] 8. 创建Nginx配置文件 + - 编写生产环境Nginx配置 + - 配置PHP-FPM upstream + - 设置静态文件服务 + - 配置日志格式和路径 + - 优化性能参数 + - _需求: 1.4_ + +- [ ] 9. 编写环境配置模板 + - 创建生产环境.env模板 + - 创建开发环境.env模板 + - 配置数据库连接参数 + - 设置缓存和搜索服务配置 + - 添加配置说明文档 + - _需求: 4.2, 4.3, 4.4, 4.5_ + +- [ ] 10. 实现启动和管理脚本 + - 编写一键启动脚本 + - 创建服务状态检查脚本 + - 实现日志查看脚本 + - 编写数据备份脚本 + - 创建清理和重置脚本 + - _需求: 2.1, 2.2, 2.3, 2.4, 2.5_ + +- [ ] 11. 第一次检查点 - 确保所有测试通过 + - 确保所有测试通过,如有问题请询问用户 + +- [ ] 12. 创建部署文档 + - 编写OpenEuler服务器环境准备指南 + - 创建Docker安装和配置文档 + - 编写应用部署步骤说明 + - 添加故障排除指南 + - 创建运维管理文档 + - _需求: 6.5_ + +- [ ] 13. 优化和安全配置 + - 配置容器安全策略 + - 设置资源限制和配额 + - 实现日志轮转和清理 + - 配置网络安全规则 + - 添加监控和告警配置 + - _需求: 2.2, 5.1, 5.2, 5.3, 5.4_ + +- [ ] 14. 最终检查点 - 确保所有测试通过 + - 确保所有测试通过,如有问题请询问用户 \ No newline at end of file diff --git a/.kiro/specs/swoole-integration/design.md b/.kiro/specs/swoole-integration/design.md new file mode 100644 index 0000000..a05ac06 --- /dev/null +++ b/.kiro/specs/swoole-integration/design.md @@ -0,0 +1,300 @@ +# 设计文档 - Swoole 集成 + +## 概述 + +本设计文档详细描述了将 Laravel 知识库系统从传统的 PHP-FPM + Nginx 架构迁移到基于 Swoole 的高性能异步架构的技术方案。通过集成 Laravel Octane 和 Swoole,系统将获得显著的性能提升和更好的并发处理能力。 + +## 架构 + +### 当前架构 vs 目标架构 + +**当前架构:** +``` +请求 → Nginx → PHP-FPM → Laravel 应用 +``` + +**目标架构:** +``` +请求 → Swoole HTTP Server → Laravel 应用 (内存驻留) +``` + +### 系统架构图 + +```mermaid +graph TB + subgraph "Docker 容器" + subgraph "应用容器 (新架构)" + A[Swoole HTTP Server] --> B[Laravel Octane] + B --> C[Laravel 应用] + D[队列工作进程] --> C + E[定时任务] --> C + end + + subgraph "数据层" + F[MySQL 容器] + G[Redis 容器] + H[Meilisearch 容器] + end + end + + I[客户端请求] --> A + C --> F + C --> G + C --> H + + style A fill:#e1f5fe + style B fill:#f3e5f5 + style C fill:#e8f5e8 +``` + +## 组件和接口 + +### 简化的集成方案 + +**核心原则**: 最小化代码修改,最大化利用 Laravel Octane 的默认配置和行为。 + +### 1. Laravel Octane 包 + +**使用现有组件:** +- 直接使用 `laravel/octane` 包,无需自定义接口 +- 使用默认的 Swoole 配置,仅调整必要参数 +- 利用 Octane 的内置命令和服务管理 + +**配置方式:** +```php +// config/octane.php (Laravel Octane 默认配置文件) +return [ + 'server' => 'swoole', + 'host' => env('OCTANE_HOST', '0.0.0.0'), + 'port' => env('OCTANE_PORT', 8000), + 'workers' => env('OCTANE_WORKERS', 4), + 'task_workers' => env('OCTANE_TASK_WORKERS', 2), + 'max_requests' => env('OCTANE_MAX_REQUESTS', 500), +]; +``` + +### 2. 现有代码兼容性 + +**无需修改的组件:** +- 现有的 Controllers、Models、Services 保持不变 +- 队列处理逻辑无需修改 +- 数据库连接和缓存逻辑保持原样 +- Filament 管理界面无需调整 + +**需要注意的事项:** +- 避免使用全局变量和静态变量 +- 确保单例服务的正确重置 +- 检查文件上传和会话处理 + +## 数据模型 + +### 简化的配置模型 + +**使用环境变量配置 (无需新建模型类):** + +```bash +# .env 文件中的 Swoole 相关配置 +OCTANE_SERVER=swoole +OCTANE_HOST=0.0.0.0 +OCTANE_PORT=8000 +OCTANE_WORKERS=4 +OCTANE_TASK_WORKERS=2 +OCTANE_MAX_REQUESTS=500 +OCTANE_WATCH=false +``` + +**现有模型保持不变:** +- Document 模型 +- User 模型 +- Group 模型 +- DownloadLog 模型 + +所有现有的 Eloquent 模型和数据库操作保持完全不变,Swoole 集成是透明的。 + +## 正确性属性 + +现在我需要使用 prework 工具来分析验收标准的可测试性: + +*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### 服务器启动和运行属性 + +**属性 1: Swoole 服务器启动一致性** +*对于任何* 有效的配置参数,当系统启动时,应该能够检测到 Swoole HTTP 服务器进程在运行并监听指定端口 +**验证: 需求 1.1** + +**属性 2: HTTP 请求处理一致性** +*对于任何* 有效的 HTTP 请求,Swoole 服务器应该能够处理请求并返回适当的响应 +**验证: 需求 1.2** + +**属性 3: 内存驻留持久性** +*对于任何* 运行中的 Swoole 服务器,应用进程应该保持内存驻留状态,不会在请求间重新初始化 +**验证: 需求 1.3** + +### 命令行接口属性 + +**属性 4: Artisan 命令执行一致性** +*对于任何* 有效的 Octane 命令参数,php artisan octane 命令应该正确执行相应的服务器操作 +**验证: 需求 2.1, 2.4, 2.5** + +**属性 5: 端口配置正确性** +*对于任何* 指定的有效端口号,Swoole 服务器应该在该端口上启动并接受连接 +**验证: 需求 2.2** + +**属性 6: 工作进程数量一致性** +*对于任何* 指定的工作进程数量,Swoole 服务器应该创建相应数量的工作进程 +**验证: 需求 2.3** + +### Docker 集成属性 + +**属性 7: Swoole 扩展安装完整性** +*对于任何* 构建的 Docker 镜像,应该包含正确安装和配置的 Swoole PHP 扩展 +**验证: 需求 3.1** + +**属性 8: 容器进程替换正确性** +*对于任何* 启动的应用容器,应该运行 Swoole 进程而不是 PHP-FPM 或 Nginx 进程 +**验证: 需求 3.2** + +**属性 9: 容器端口暴露正确性** +*对于任何* 配置的 Swoole 服务端口,容器应该正确暴露该端口供外部访问 +**验证: 需求 3.3** + +### 队列处理属性 + +**属性 10: 队列处理兼容性** +*对于任何* 队列任务,在 Swoole 环境下应该能够正常处理,与传统环境行为一致 +**验证: 需求 4.1, 4.2** + +**属性 11: 队列监听器自动启动** +*对于任何* 系统启动,队列监听器应该自动启动并保持运行状态 +**验证: 需求 4.3** + +**属性 12: 队列错误处理一致性** +*对于任何* 失败的队列任务,系统应该记录错误信息并按配置进行重试 +**验证: 需求 4.4** + +### 系统稳定性属性 + +**属性 13: 高并发处理稳定性** +*对于任何* 高并发请求负载,Swoole 服务器应该保持稳定运行而不崩溃 +**验证: 需求 5.1** + +**属性 14: 内存使用稳定性** +*对于任何* 长时间运行的系统,内存使用应该保持在合理范围内,不出现持续增长 +**验证: 需求 5.2** + +**属性 15: 健康检查响应一致性** +*对于任何* 健康检查请求,系统应该返回正确的健康状态信息 +**验证: 需求 5.4** + +### 部署和配置属性 + +**属性 16: 部署脚本镜像构建正确性** +*对于任何* 执行的部署脚本,应该生成包含 Swoole 配置的有效 Docker 镜像 +**验证: 需求 6.1** + +**属性 17: 配置文件更新正确性** +*对于任何* 更新的 docker-compose 配置,应该正确移除 Nginx 依赖并配置 Swoole 服务 +**验证: 需求 6.2** + +**属性 18: 环境变量配置生效性** +*对于任何* 设置的 Swoole 相关环境变量,应该在系统运行时正确生效 +**验证: 需求 6.3** + +## 错误处理 + +### 简化的错误处理策略 + +**利用 Laravel Octane 内置错误处理:** +- 使用 Octane 的默认异常处理机制 +- 利用 Laravel 现有的日志系统 +- 保持现有的错误报告和监控 + +**最小化自定义错误处理:** +```php +// 仅在必要时添加 Swoole 特定的错误处理 +// 在 app/Exceptions/Handler.php 中添加 +public function register() +{ + $this->reportable(function (Throwable $e) { + if (app()->bound('octane')) { + // 记录 Swoole 相关错误 + Log::channel('swoole')->error('Swoole error: ' . $e->getMessage()); + } + }); +} +``` + +**依赖现有机制:** +- 使用现有的队列失败处理 +- 保持现有的数据库连接错误处理 +- 利用现有的缓存错误恢复机制 + +## 测试策略 + +### 双重测试方法 + +本项目将采用单元测试和基于属性的测试相结合的方法: + +- **单元测试**: 验证具体的功能实现和边界条件 +- **基于属性的测试**: 验证系统在各种输入下的通用属性 + +### 单元测试覆盖 + +单元测试将覆盖: +- Octane 配置加载和验证 +- Swoole 服务器启动和停止 +- 命令行接口功能 +- 错误处理逻辑 +- 队列集成功能 + +### 基于属性的测试 + +将使用 **Pest** 作为基于属性的测试框架,配置每个属性测试运行最少 100 次迭代。 + +每个基于属性的测试必须: +- 使用注释明确标识对应的设计文档属性 +- 使用格式: `**Feature: swoole-integration, Property {number}: {property_text}**` +- 生成合适的测试数据来验证属性 +- 验证系统在各种输入条件下的行为一致性 + +### 测试环境配置 + +```php +// 测试配置示例 +return [ + 'octane' => [ + 'server' => 'swoole', + 'host' => '127.0.0.1', + 'port' => 8000, + 'workers' => 2, + 'task_workers' => 1, + 'max_requests' => 100, + ], + 'swoole' => [ + 'options' => [ + 'log_file' => storage_path('logs/swoole.log'), + 'log_level' => SWOOLE_LOG_INFO, + ], + ], +]; +``` + +### 性能测试 + +除了功能测试外,还需要进行性能测试: +- 并发请求处理能力测试 +- 内存使用监控测试 +- 响应时间分布测试 +- 长时间运行稳定性测试 + +### 集成测试 + +集成测试将验证: +- Docker 容器间的通信 +- 数据库连接池管理 +- 缓存系统集成 +- 搜索服务集成 +- 队列系统集成 + +这些测试确保 Swoole 集成不会破坏现有的系统功能,同时提供预期的性能改进。 \ No newline at end of file diff --git a/.kiro/specs/swoole-integration/requirements.md b/.kiro/specs/swoole-integration/requirements.md new file mode 100644 index 0000000..4278a31 --- /dev/null +++ b/.kiro/specs/swoole-integration/requirements.md @@ -0,0 +1,87 @@ +# 需求文档 - Swoole 集成 + +## 介绍 + +本规范旨在将现有的 Laravel 知识库系统从传统的 PHP-FPM + Nginx 架构迁移到使用 Swoole 的高性能异步架构。Swoole 是一个高性能的 PHP 异步网络通信引擎,能够显著提升应用性能和并发处理能力。 + +## 术语表 + +- **Swoole**: 高性能的 PHP 异步网络通信引擎 +- **Laravel_Octane**: Laravel 官方的高性能应用服务器包,支持 Swoole 和 RoadRunner +- **PHP_Artisan**: Laravel 的命令行工具 +- **Docker_Container**: 应用程序的容器化运行环境 +- **Hot_Reload**: 代码变更时自动重启服务的功能 + +## 需求 + +### 需求 1 + +**用户故事:** 作为系统管理员,我希望使用 Swoole 替代传统的 PHP-FPM 运行方式,以便获得更高的性能和并发处理能力。 + +#### 验收标准 + +1. WHEN 系统启动时 THEN Laravel_Octane SHALL 使用 Swoole 驱动启动 HTTP 服务器 +2. WHEN 接收 HTTP 请求时 THEN Swoole_Server SHALL 处理请求并返回响应 +3. WHEN 系统运行时 THEN Swoole_Server SHALL 维持长连接和内存驻留 +4. WHEN 配置变更时 THEN 系统 SHALL 支持热重载功能 +5. WHEN 监控系统性能时 THEN Swoole_Server SHALL 提供性能指标接口 + +### 需求 2 + +**用户故事:** 作为开发人员,我希望通过 php artisan 命令启动 Swoole 服务,以便保持与 Laravel 生态系统的一致性。 + +#### 验收标准 + +1. WHEN 执行启动命令时 THEN php artisan octane:start SHALL 启动 Swoole 服务器 +2. WHEN 指定端口参数时 THEN 系统 SHALL 在指定端口上启动服务 +3. WHEN 指定工作进程数时 THEN Swoole_Server SHALL 创建指定数量的工作进程 +4. WHEN 执行停止命令时 THEN php artisan octane:stop SHALL 优雅关闭服务器 +5. WHEN 执行重启命令时 THEN php artisan octane:restart SHALL 重启服务器 + +### 需求 3 + +**用户故事:** 作为运维人员,我希望更新 Docker 镜像配置,以便支持 Swoole 运行环境和相关依赖。 + +#### 验收标准 + +1. WHEN 构建 Docker 镜像时 THEN 系统 SHALL 安装 Swoole PHP 扩展 +2. WHEN 容器启动时 THEN 系统 SHALL 使用 Swoole 替代 PHP-FPM 和 Nginx +3. WHEN 配置容器时 THEN 系统 SHALL 暴露 Swoole 服务端口 +4. WHEN 容器运行时 THEN 系统 SHALL 支持进程管理和监控 +5. WHEN 容器重启时 THEN 系统 SHALL 自动恢复 Swoole 服务 + +### 需求 4 + +**用户故事:** 作为系统架构师,我希望保持现有的队列处理和后台任务功能,以便确保系统功能完整性。 + +#### 验收标准 + +1. WHEN Swoole 服务运行时 THEN 队列处理器 SHALL 继续正常工作 +2. WHEN 处理文档转换任务时 THEN 后台队列 SHALL 正常执行任务 +3. WHEN 系统启动时 THEN 队列监听器 SHALL 自动启动 +4. WHEN 队列任务失败时 THEN 系统 SHALL 记录错误并支持重试 +5. WHEN 监控队列状态时 THEN 系统 SHALL 提供队列健康检查接口 + +### 需求 5 + +**用户故事:** 作为质量保证工程师,我希望验证 Swoole 集成后的系统稳定性,以便确保生产环境的可靠性。 + +#### 验收标准 + +1. WHEN 系统负载测试时 THEN Swoole_Server SHALL 处理高并发请求而不崩溃 +2. WHEN 长时间运行时 THEN 系统 SHALL 保持内存使用稳定 +3. WHEN 发生异常时 THEN Swoole_Server SHALL 记录详细错误日志 +4. WHEN 进行健康检查时 THEN 系统 SHALL 响应健康检查请求 +5. WHEN 系统重启时 THEN 所有服务 SHALL 在合理时间内恢复正常 + +### 需求 6 + +**用户故事:** 作为部署工程师,我希望更新部署脚本和配置,以便支持新的 Swoole 架构部署。 + +#### 验收标准 + +1. WHEN 执行部署脚本时 THEN 系统 SHALL 构建包含 Swoole 的新镜像 +2. WHEN 更新 docker-compose 配置时 THEN 系统 SHALL 移除 Nginx 容器依赖 +3. WHEN 配置环境变量时 THEN 系统 SHALL 支持 Swoole 相关配置参数 +4. WHEN 验证部署时 THEN 系统 SHALL 确认 Swoole 服务正常运行 +5. WHEN 回滚部署时 THEN 系统 SHALL 支持回退到之前的架构 \ No newline at end of file diff --git a/.kiro/specs/swoole-integration/tasks.md b/.kiro/specs/swoole-integration/tasks.md new file mode 100644 index 0000000..17415b4 --- /dev/null +++ b/.kiro/specs/swoole-integration/tasks.md @@ -0,0 +1,185 @@ +# 实施计划 - Swoole 集成 + +## 概述 + +本实施计划将现有的 Laravel 知识库系统从 PHP-FPM + Nginx 架构迁移到基于 Swoole 的高性能架构。采用最小化代码修改的策略,主要通过安装 Laravel Octane 包和更新配置来实现。 + +## 任务列表 + +- [x] 1. 安装和配置 Laravel Octane + - 安装 laravel/octane 包 + - 发布 Octane 配置文件 + - 配置 Swoole 相关环境变量 + - _需求: 1.1, 2.1_ + +- [ ]* 1.1 编写 Octane 启动测试 + - **属性 1: Swoole 服务器启动一致性** + - **验证: 需求 1.1** + +- [ ]* 1.2 编写命令行接口测试 + - **属性 4: Artisan 命令执行一致性** + - **验证: 需求 2.1, 2.4, 2.5** + +- [x] 2. 更新 Composer 依赖 + - 添加 laravel/octane 到 composer.json + - 安装 Swoole PHP 扩展依赖 + - 更新 composer 脚本以支持 Swoole 启动 + - _需求: 1.1, 2.1_ + +- [ ]* 2.1 编写依赖安装验证测试 + - **属性 7: Swoole 扩展安装完整性** + - **验证: 需求 3.1** + +- [ ] 3. 更新 Docker 配置 + - 修改 Dockerfile 安装 Swoole 扩展 + - 移除 Nginx 和 PHP-FPM 相关配置 + - 更新容器启动命令使用 Octane + - 调整端口映射配置 + - _需求: 3.1, 3.2, 3.3_ + +- [ ]* 3.1 编写 Docker 镜像验证测试 + - **属性 8: 容器进程替换正确性** + - **验证: 需求 3.2** + +- [ ]* 3.2 编写端口配置测试 + - **属性 5: 端口配置正确性** + - **属性 9: 容器端口暴露正确性** + - **验证: 需求 2.2, 3.3** + +- [x] 4. 更新 docker-compose.yml + - 移除 Nginx 服务配置 + - 更新应用服务使用 Swoole 端口 + - 调整服务依赖关系 + - 更新健康检查配置 + - _需求: 3.2, 6.2_ + +- [ ]* 4.1 编写 docker-compose 配置验证测试 + - **属性 17: 配置文件更新正确性** + - **验证: 需求 6.2** + +- [x] 5. 配置环境变量 + - 更新 .env 文件添加 Octane 配置 + - 设置 Swoole 工作进程数量 + - 配置最大请求数和其他性能参数 + - _需求: 1.4, 2.2, 2.3, 6.3_ + +- [ ]* 5.1 编写环境变量配置测试 + - **属性 6: 工作进程数量一致性** + - **属性 18: 环境变量配置生效性** + - **验证: 需求 2.3, 6.3** + +- [x] 6. 验证队列系统兼容性 + - 测试现有队列任务在 Swoole 环境下的运行 + - 验证文档转换队列功能 + - 确认队列监听器自动启动 + - _需求: 4.1, 4.2, 4.3_ + +- [ ]* 6.1 编写队列兼容性测试 + - **属性 10: 队列处理兼容性** + - **属性 11: 队列监听器自动启动** + - **验证: 需求 4.1, 4.2, 4.3** + +- [ ]* 6.2 编写队列错误处理测试 + - **属性 12: 队列错误处理一致性** + - **验证: 需求 4.4** + +- [ ] 7. 更新部署脚本 + - 修改 Docker 镜像构建脚本 + - 更新部署验证脚本 + - 调整健康检查脚本 + - _需求: 6.1, 6.4_ + +- [ ]* 7.1 编写部署脚本验证测试 + - **属性 16: 部署脚本镜像构建正确性** + - **验证: 需求 6.1** + +- [ ] 8. 第一次检查点 - 确保所有测试通过 + - 确保所有测试通过,如有问题请询问用户 + +- [ ] 9. 性能和稳定性测试 + - 配置负载测试环境 + - 执行并发请求测试 + - 监控内存使用情况 + - 验证长时间运行稳定性 + - _需求: 5.1, 5.2_ + +- [ ]* 9.1 编写性能测试 + - **属性 13: 高并发处理稳定性** + - **属性 14: 内存使用稳定性** + - **验证: 需求 5.1, 5.2** + +- [ ] 10. 健康检查和监控 + - 实现 Swoole 服务健康检查接口 + - 配置系统监控和告警 + - 验证错误日志记录功能 + - _需求: 5.3, 5.4_ + +- [ ]* 10.1 编写健康检查测试 + - **属性 15: 健康检查响应一致性** + - **验证: 需求 5.4** + +- [ ] 11. 文档更新 + - 更新部署指南 + - 更新运维文档 + - 创建 Swoole 配置说明 + - _需求: 6.4_ + +- [ ] 12. 回滚机制准备 + - 准备回滚到原架构的脚本 + - 测试回滚流程 + - 文档化回滚步骤 + - _需求: 6.5_ + +- [ ]* 12.1 编写回滚功能测试 + - 验证回滚机制的正确性 + - _需求: 6.5_ + +- [ ] 13. 最终检查点 - 确保所有测试通过 + - 确保所有测试通过,如有问题请询问用户 + +## 实施注意事项 + +### 最小化代码修改原则 + +1. **保持现有代码不变**: 所有 Controllers、Models、Services 保持原样 +2. **利用 Laravel Octane 默认配置**: 避免自定义复杂的配置逻辑 +3. **渐进式迁移**: 先在开发环境验证,再部署到生产环境 +4. **保留回滚能力**: 确保可以快速回退到原有架构 + +### 关键配置参数 + +```bash +# 核心 Swoole 配置 +OCTANE_SERVER=swoole +OCTANE_HOST=0.0.0.0 +OCTANE_PORT=8000 +OCTANE_WORKERS=4 +OCTANE_TASK_WORKERS=2 +OCTANE_MAX_REQUESTS=500 +``` + +### 验证检查清单 + +- [ ] Swoole 扩展正确安装 +- [ ] Octane 命令正常工作 +- [ ] HTTP 请求正确处理 +- [ ] 队列任务正常执行 +- [ ] 数据库连接稳定 +- [ ] 缓存系统正常 +- [ ] 搜索功能可用 +- [ ] 文件上传下载正常 +- [ ] 性能指标符合预期 + +### 性能预期 + +- **响应时间**: 比原架构提升 30-50% +- **并发处理**: 支持更高的并发连接数 +- **内存使用**: 更高效的内存利用 +- **CPU 使用**: 更好的 CPU 利用率 + +### 风险缓解 + +1. **充分测试**: 在开发环境完整测试所有功能 +2. **分阶段部署**: 先部署到测试环境,再到生产环境 +3. **监控告警**: 部署后密切监控系统指标 +4. **快速回滚**: 准备好快速回滚方案 \ No newline at end of file diff --git a/.kiro/specs/ui-enhancement/design.md b/.kiro/specs/ui-enhancement/design.md deleted file mode 100644 index acdd871..0000000 --- a/.kiro/specs/ui-enhancement/design.md +++ /dev/null @@ -1,484 +0,0 @@ -# 设计文档 - -## 概述 - -本设计文档描述了知识库系统UI界面美化的技术实现方案。通过集成Alpine.js和Tailwind CSS,我们将为现有的Filament界面添加现代化的视觉效果和流畅的交互动画,提升用户体验。 - -设计遵循以下原则: -- **渐进增强**:在不破坏现有功能的基础上添加视觉增强 -- **性能优先**:使用CSS动画和轻量级JavaScript,避免性能问题 -- **响应式设计**:确保在所有设备上都有良好的显示效果 -- **无障碍访问**:遵循WCAG 2.1标准,支持键盘导航和屏幕阅读器 -- **主题一致性**:与Filament的设计语言保持一致 - -## 架构 - -### 技术栈 - -- **Alpine.js 3.x**:用于添加交互行为和状态管理 -- **Tailwind CSS 3.x**:用于样式设计和响应式布局 -- **Filament 3.x**:现有的管理面板框架 -- **Laravel Blade**:模板引擎 -- **CSS Transitions/Animations**:用于动画效果 - -### 组件层次 - -``` -┌─────────────────────────────────────┐ -│ Blade Templates │ -│ (搜索页面、预览模态框、文档列表) │ -└──────────────┬──────────────────────┘ - │ -┌──────────────┴──────────────────────┐ -│ Alpine.js Components │ -│ (交互逻辑、状态管理、事件处理) │ -└──────────────┬──────────────────────┘ - │ -┌──────────────┴──────────────────────┐ -│ Tailwind CSS Classes │ -│ (样式、动画、响应式布局) │ -└─────────────────────────────────────┘ -``` - -### 文件结构 - -``` -resources/ -├── views/ -│ ├── filament/ -│ │ ├── pages/ -│ │ │ ├── search-page.blade.php (增强版搜索页面) -│ │ │ └── document-preview-modal.blade.php (增强版预览模态框) -│ │ └── resources/ -│ │ └── document/ -│ │ └── card.blade.php (新增:文档卡片组件) -│ └── components/ -│ ├── ui/ -│ │ ├── button.blade.php (新增:增强按钮组件) -│ │ ├── input.blade.php (新增:增强输入框组件) -│ │ ├── card.blade.php (新增:卡片组件) -│ │ └── badge.blade.php (新增:徽章组件) -│ └── animations/ -│ ├── fade-in.blade.php (新增:淡入动画) -│ └── slide-in.blade.php (新增:滑入动画) -├── css/ -│ └── custom/ -│ ├── animations.css (新增:自定义动画) -│ ├── components.css (新增:组件样式) -│ └── utilities.css (新增:工具类) -└── js/ - └── alpine/ - ├── search.js (新增:搜索页面逻辑) - ├── preview.js (新增:预览模态框逻辑) - └── filters.js (新增:筛选器逻辑) -``` - -## 组件和接口 - -### 1. 搜索页面组件 - -**职责**:提供美化的搜索界面和结果展示 - -**Alpine.js数据结构**: -```javascript -{ - // 搜索状态 - searchQuery: '', - isSearching: false, - hasSearched: false, - - // 筛选器状态 - filters: { - type: null, - groupId: null - }, - showFilters: false, - - // 结果状态 - results: [], - resultCount: 0, - - // UI状态 - viewMode: 'grid', // 'grid' 或 'list' - sortBy: 'created_at', - sortOrder: 'desc' -} -``` - -**方法**: -- `search()`:执行搜索 -- `clearSearch()`:清空搜索 -- `toggleFilters()`:切换筛选器显示 -- `applyFilter(key, value)`:应用筛选条件 -- `removeFilter(key)`:移除筛选条件 -- `toggleViewMode()`:切换视图模式 -- `sortResults(field)`:排序结果 - -### 2. 文档卡片组件 - -**职责**:以卡片形式展示文档信息 - -**Props**: -- `document`:文档对象 -- `showActions`:是否显示操作按钮 -- `compact`:是否使用紧凑模式 - -**样式类**: -- `document-card`:基础卡片样式 -- `document-card-hover`:悬停效果 -- `document-card-compact`:紧凑模式 - -### 3. 预览模态框组件 - -**职责**:提供优雅的文档预览体验 - -**Alpine.js数据结构**: -```javascript -{ - // 模态框状态 - isOpen: false, - isLoading: true, - - // 内容状态 - content: null, - error: null, - - // UI状态 - scrollProgress: 0, - showScrollTop: false -} -``` - -**方法**: -- `open()`:打开模态框 -- `close()`:关闭模态框 -- `loadContent()`:加载内容 -- `scrollToTop()`:滚动到顶部 -- `updateScrollProgress()`:更新滚动进度 - -### 4. 增强按钮组件 - -**职责**:提供统一的按钮样式和交互效果 - -**Props**: -- `variant`:按钮变体(primary, secondary, danger等) -- `size`:按钮大小(sm, md, lg) -- `loading`:加载状态 -- `disabled`:禁用状态 -- `icon`:图标名称 - -**样式类**: -- `btn-enhanced`:基础增强样式 -- `btn-loading`:加载状态 -- `btn-pulse`:脉冲效果 - -### 5. 增强输入框组件 - -**职责**:提供友好的输入交互效果 - -**Props**: -- `label`:标签文本 -- `placeholder`:占位符 -- `maxLength`:最大长度 -- `showCounter`:显示字符计数 -- `validation`:验证规则 - -**Alpine.js数据结构**: -```javascript -{ - value: '', - isFocused: false, - hasError: false, - errorMessage: '', - charCount: 0 -} -``` - -## 数据模型 - -本功能主要涉及UI增强,不需要修改现有数据模型。所有数据仍使用现有的Document、Group等模型。 - -## 正确性属性 - -*属性是一个特征或行为,应该在系统的所有有效执行中保持为真。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。* - -### 属性 1:文档类型徽章颜色一致性 - -*对于任何*文档,当显示为卡片时,全局文档应该使用绿色徽章,专用文档应该使用蓝色徽章 - -**验证需求:2.3** - -### 属性 2:ARIA标签完整性 - -*对于任何*可交互元素,该元素应该包含适当的ARIA标签或role属性 - -**验证需求:10.2** - -### 属性 3:颜色对比度合规性 - -*对于任何*文本元素,其前景色和背景色的对比度应该至少为4.5:1(普通文本)或3:1(大文本) - -**验证需求:10.5** - -## 错误处理 - -### 1. 动画性能问题 - -**场景**:在低性能设备上动画可能导致卡顿 - -**处理策略**: -- 检测设备性能,在低性能设备上禁用复杂动画 -- 使用CSS `will-change`属性优化动画性能 -- 遵循用户的`prefers-reduced-motion`设置 - -### 2. Alpine.js加载失败 - -**场景**:CDN不可用或网络问题导致Alpine.js加载失败 - -**处理策略**: -- 使用本地备份的Alpine.js文件 -- 确保核心功能在没有JavaScript的情况下仍可用 -- 显示友好的降级界面 - -### 3. 深色模式切换问题 - -**场景**:主题切换时可能出现闪烁 - -**处理策略**: -- 在页面加载前检测主题偏好 -- 使用CSS变量实现平滑过渡 -- 将主题偏好保存到localStorage - -### 4. 响应式布局问题 - -**场景**:某些设备上布局可能错乱 - -**处理策略**: -- 使用Tailwind的响应式断点 -- 在多种设备上测试 -- 提供最小宽度限制 - -## 测试策略 - -### 单元测试 - -使用PHPUnit和Pest进行后端测试: - -1. **组件渲染测试** - - 测试Blade组件是否正确渲染 - - 测试props是否正确传递 - - 测试条件渲染逻辑 - -2. **样式类测试** - - 测试CSS类是否正确应用 - - 测试响应式类是否存在 - -### 前端测试 - -使用Jest和Testing Library进行前端测试: - -1. **Alpine.js组件测试** - - 测试数据绑定 - - 测试事件处理 - - 测试状态变化 - -2. **交互测试** - - 测试按钮点击 - - 测试表单输入 - - 测试键盘导航 - -3. **视觉回归测试** - - 使用Percy或Chromatic进行截图对比 - - 测试不同主题下的显示效果 - - 测试不同屏幕尺寸下的布局 - -### 无障碍测试 - -1. **自动化测试** - - 使用axe-core进行无障碍扫描 - - 测试ARIA标签 - - 测试键盘导航 - -2. **手动测试** - - 使用屏幕阅读器测试 - - 测试键盘完整导航 - - 测试颜色对比度 - -### 性能测试 - -1. **动画性能** - - 使用Chrome DevTools测试FPS - - 测试动画是否触发重排 - - 测试低性能设备表现 - -2. **加载性能** - - 测试CSS和JS文件大小 - - 测试首次内容绘制时间 - - 测试交互就绪时间 - -### 浏览器兼容性测试 - -测试以下浏览器: -- Chrome(最新版本和前一版本) -- Firefox(最新版本和前一版本) -- Safari(最新版本) -- Edge(最新版本) -- 移动浏览器(iOS Safari、Chrome Mobile) - -## 实现细节 - -### 1. Tailwind CSS配置 - -扩展Tailwind配置以支持自定义动画和颜色: - -```javascript -// tailwind.config.js -module.exports = { - theme: { - extend: { - animation: { - 'fade-in': 'fadeIn 0.3s ease-in-out', - 'slide-in': 'slideIn 0.3s ease-out', - 'scale-in': 'scaleIn 0.2s ease-out', - 'shake': 'shake 0.5s ease-in-out', - }, - keyframes: { - fadeIn: { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - slideIn: { - '0%': { transform: 'translateY(-10px)', opacity: '0' }, - '100%': { transform: 'translateY(0)', opacity: '1' }, - }, - scaleIn: { - '0%': { transform: 'scale(0.95)', opacity: '0' }, - '100%': { transform: 'scale(1)', opacity: '1' }, - }, - shake: { - '0%, 100%': { transform: 'translateX(0)' }, - '25%': { transform: 'translateX(-10px)' }, - '75%': { transform: 'translateX(10px)' }, - }, - }, - }, - }, -} -``` - -### 2. Alpine.js集成 - -在Blade模板中集成Alpine.js: - -```html -
- -
- - -``` - -### 3. 自定义CSS动画 - -创建可复用的动画类: - -```css -/* animations.css */ -.animate-stagger > * { - animation: fadeIn 0.3s ease-in-out; - animation-fill-mode: both; -} - -.animate-stagger > *:nth-child(1) { animation-delay: 0.05s; } -.animate-stagger > *:nth-child(2) { animation-delay: 0.1s; } -.animate-stagger > *:nth-child(3) { animation-delay: 0.15s; } -/* ... */ - -.hover-lift { - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.hover-lift:hover { - transform: translateY(-4px); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); -} -``` - -### 4. 深色模式支持 - -使用Tailwind的深色模式类: - -```html -
- -
-``` - -### 5. 响应式设计 - -使用Tailwind的响应式前缀: - -```html -
- -
-``` - -### 6. 无障碍支持 - -添加适当的ARIA属性: - -```html - -``` - -### 7. 性能优化 - -- 使用CSS `contain`属性隔离动画 -- 使用`will-change`提示浏览器优化 -- 延迟加载非关键动画 -- 使用`requestAnimationFrame`优化JavaScript动画 - -```css -.animated-card { - contain: layout style paint; - will-change: transform; -} -``` - -## 部署考虑 - -### 1. 资源打包 - -- 使用Laravel Mix或Vite打包CSS和JS -- 启用CSS和JS压缩 -- 使用版本控制避免缓存问题 - -### 2. CDN配置 - -- 考虑使用CDN加速Alpine.js加载 -- 提供本地备份文件 - -### 3. 浏览器支持 - -- 添加必要的polyfills -- 提供降级方案 - -### 4. 监控 - -- 监控动画性能 -- 收集用户反馈 -- 跟踪错误日志 diff --git a/.kiro/specs/ui-enhancement/requirements.md b/.kiro/specs/ui-enhancement/requirements.md deleted file mode 100644 index fdebf65..0000000 --- a/.kiro/specs/ui-enhancement/requirements.md +++ /dev/null @@ -1,138 +0,0 @@ -# 需求文档 - -## 简介 - -本文档定义了知识库系统UI界面美化的需求。系统当前使用Filament框架构建,需要通过Alpine.js和Tailwind CSS增强用户界面的视觉效果和交互体验,使界面更加现代化、美观和易用。 - -## 术语表 - -- **知识库系统(Knowledge Base System)**:基于Laravel和Filament构建的文档管理系统 -- **Alpine.js**:轻量级JavaScript框架,用于添加交互行为 -- **Tailwind CSS**:实用优先的CSS框架,用于样式设计 -- **Filament**:Laravel的管理面板框架 -- **搜索页面(Search Page)**:用户搜索文档的界面 -- **文档预览(Document Preview)**:在模态框中显示文档内容的功能 -- **文档列表(Document List)**:显示文档资源的表格界面 -- **响应式设计(Responsive Design)**:适配不同屏幕尺寸的界面设计 - -## 需求 - -### 需求 1 - -**用户故事:** 作为用户,我希望搜索页面具有现代化的视觉设计,以便获得更好的使用体验。 - -#### 验收标准 - -1. WHEN 用户访问搜索页面 THEN 系统应当显示具有渐变背景和阴影效果的搜索表单卡片 -2. WHEN 用户将鼠标悬停在搜索按钮上 THEN 系统应当显示平滑的过渡动画效果 -3. WHEN 用户输入搜索关键词 THEN 系统应当在输入框获得焦点时显示高亮边框效果 -4. WHEN 搜索表单加载完成 THEN 系统应当显示淡入动画效果 -5. WHEN 用户在移动设备上访问 THEN 系统应当显示适配移动端的响应式布局 - -### 需求 2 - -**用户故事:** 作为用户,我希望搜索结果以卡片形式展示,以便更直观地浏览文档信息。 - -#### 验收标准 - -1. WHEN 搜索返回结果 THEN 系统应当以卡片网格布局显示文档列表 -2. WHEN 用户将鼠标悬停在文档卡片上 THEN 系统应当显示卡片上浮和阴影增强效果 -3. WHEN 文档卡片包含类型标签 THEN 系统应当使用不同颜色的徽章区分全局和专用文档 -4. WHEN 搜索结果加载完成 THEN 系统应当显示交错淡入动画效果 -5. WHEN 文档卡片显示内容片段 THEN 系统应当使用渐变遮罩处理文本溢出 - -### 需求 3 - -**用户故事:** 作为用户,我希望文档预览模态框具有优雅的设计,以便舒适地阅读文档内容。 - -#### 验收标准 - -1. WHEN 用户点击预览按钮 THEN 系统应当显示带有缩放淡入动画的模态框 -2. WHEN 模态框打开时 THEN 系统应当显示半透明背景遮罩和模糊效果 -3. WHEN 文档内容较长 THEN 系统应当提供带有自定义滚动条样式的滚动区域 -4. WHEN 文档包含代码块 THEN 系统应当使用语法高亮和圆角边框样式 -5. WHEN 文档包含表格 THEN 系统应当使用斑马纹和悬停高亮效果 - -### 需求 4 - -**用户故事:** 作为用户,我希望操作按钮具有清晰的视觉反馈,以便明确操作状态。 - -#### 验收标准 - -1. WHEN 用户将鼠标悬停在按钮上 THEN 系统应当显示颜色加深和轻微缩放效果 -2. WHEN 用户点击按钮 THEN 系统应当显示按下动画效果 -3. WHEN 按钮处于加载状态 THEN 系统应当显示旋转的加载图标 -4. WHEN 按钮处于禁用状态 THEN 系统应当显示降低透明度和禁用鼠标指针 -5. WHEN 操作成功完成 THEN 系统应当显示带有图标的成功通知动画 - -### 需求 5 - -**用户故事:** 作为用户,我希望表单输入具有友好的交互效果,以便更好地完成输入操作。 - -#### 验收标准 - -1. WHEN 输入框获得焦点 THEN 系统应当显示边框颜色变化和标签上移动画 -2. WHEN 用户输入内容 THEN 系统应当实时显示字符计数或验证状态 -3. WHEN 输入验证失败 THEN 系统应当显示红色边框和抖动动画效果 -4. WHEN 下拉选择框展开 THEN 系统应当显示下滑淡入动画 -5. WHEN 用户清空输入 THEN 系统应当显示清除按钮的淡入淡出效果 - -### 需求 6 - -**用户故事:** 作为用户,我希望页面加载和状态变化具有流畅的过渡效果,以便获得连贯的使用体验。 - -#### 验收标准 - -1. WHEN 页面首次加载 THEN 系统应当显示骨架屏加载动画 -2. WHEN 搜索正在执行 THEN 系统应当显示加载指示器和脉冲动画 -3. WHEN 内容状态改变 THEN 系统应当使用淡入淡出过渡效果 -4. WHEN 列表项添加或删除 THEN 系统应当显示滑入滑出动画 -5. WHEN 错误发生 THEN 系统应当显示带有图标的错误提示和抖动效果 - -### 需求 7 - -**用户故事:** 作为用户,我希望界面支持深色模式,以便在不同光线环境下舒适使用。 - -#### 验收标准 - -1. WHEN 系统检测到深色模式偏好 THEN 系统应当自动切换到深色主题 -2. WHEN 深色模式激活 THEN 系统应当使用深色背景和浅色文字 -3. WHEN 深色模式下显示卡片 THEN 系统应当使用深色卡片背景和适当的边框 -4. WHEN 深色模式下显示按钮 THEN 系统应当调整按钮颜色以保持对比度 -5. WHEN 主题切换时 THEN 系统应当显示平滑的颜色过渡动画 - -### 需求 8 - -**用户故事:** 作为用户,我希望界面元素具有微交互效果,以便获得更生动的使用体验。 - -#### 验收标准 - -1. WHEN 用户将鼠标悬停在图标上 THEN 系统应当显示图标旋转或缩放动画 -2. WHEN 用户点击收藏按钮 THEN 系统应当显示心形填充动画 -3. WHEN 通知消息出现 THEN 系统应当从右侧滑入并自动淡出 -4. WHEN 用户滚动页面 THEN 系统应当显示返回顶部按钮的淡入效果 -5. WHEN 用户拖拽元素 THEN 系统应当显示拖拽阴影和位置指示器 - -### 需求 9 - -**用户故事:** 作为用户,我希望文档列表具有高级筛选和排序界面,以便快速找到目标文档。 - -#### 验收标准 - -1. WHEN 用户点击筛选按钮 THEN 系统应当显示侧边栏滑入动画 -2. WHEN 用户选择筛选条件 THEN 系统应当实时更新结果数量徽章 -3. WHEN 用户应用筛选 THEN 系统应当显示已选筛选条件的标签 -4. WHEN 用户点击排序选项 THEN 系统应当显示排序图标的旋转动画 -5. WHEN 筛选结果为空 THEN 系统应当显示友好的空状态插图和提示 - -### 需求 10 - -**用户故事:** 作为用户,我希望界面具有无障碍访问支持,以便所有用户都能使用系统。 - -#### 验收标准 - -1. WHEN 用户使用键盘导航 THEN 系统应当显示清晰的焦点指示器 -2. WHEN 屏幕阅读器访问页面 THEN 系统应当提供适当的ARIA标签 -3. WHEN 用户使用Tab键切换 THEN 系统应当按逻辑顺序聚焦可交互元素 -4. WHEN 动画效果播放 THEN 系统应当遵循用户的减少动画偏好设置 -5. WHEN 界面显示颜色信息 THEN 系统应当确保足够的颜色对比度 diff --git a/.kiro/specs/ui-enhancement/tasks.md b/.kiro/specs/ui-enhancement/tasks.md deleted file mode 100644 index e0dea61..0000000 --- a/.kiro/specs/ui-enhancement/tasks.md +++ /dev/null @@ -1,334 +0,0 @@ -# 实施计划 - -- [ ] 1. 配置开发环境和依赖 -- [ ] 1.1 确认Alpine.js和Tailwind CSS已安装 - - 检查package.json中的依赖 - - 如需要则安装Alpine.js 3.x - - 确认Tailwind CSS 3.x已配置 - - _需求:所有需求的基础_ - -- [ ] 1.2 扩展Tailwind配置文件 - - 在tailwind.config.js中添加自定义动画 - - 添加自定义关键帧(fadeIn, slideIn, scaleIn, shake) - - 配置动画时长和缓动函数 - - _需求:1.2, 1.4, 2.2, 2.4, 3.1, 4.1, 5.1, 5.3, 6.1-6.5, 8.1-8.5_ - -- [ ] 1.3 创建自定义CSS文件结构 - - 创建resources/css/custom/animations.css - - 创建resources/css/custom/components.css - - 创建resources/css/custom/utilities.css - - 在app.css中导入自定义CSS文件 - - _需求:所有需求_ - -- [ ] 2. 创建可复用的UI组件 -- [ ] 2.1 创建增强按钮组件 - - 创建resources/views/components/ui/button.blade.php - - 实现按钮变体(primary, secondary, danger等) - - 添加悬停效果(颜色加深、轻微缩放) - - 添加点击动画效果 - - 添加加载状态(旋转图标) - - 添加禁用状态样式 - - _需求:4.1, 4.2, 4.3, 4.4_ - -- [ ]* 2.2 编写属性测试验证按钮组件 - - **属性 1:文档类型徽章颜色一致性** - - **验证需求:2.3** - -- [ ] 2.3 创建增强输入框组件 - - 创建resources/views/components/ui/input.blade.php - - 实现焦点状态(边框颜色变化、标签上移) - - 添加字符计数功能 - - 添加验证错误状态(红色边框、抖动动画) - - 添加清除按钮(淡入淡出效果) - - _需求:5.1, 5.2, 5.3, 5.5_ - -- [ ] 2.4 创建卡片组件 - - 创建resources/views/components/ui/card.blade.php - - 实现基础卡片样式(渐变背景、阴影) - - 添加悬停效果(上浮、阴影增强) - - 支持深色模式样式 - - _需求:1.1, 2.1, 2.2, 7.3_ - -- [ ] 2.5 创建徽章组件 - - 创建resources/views/components/ui/badge.blade.php - - 实现不同颜色变体(success, info, warning, danger) - - 为全局文档使用绿色徽章 - - 为专用文档使用蓝色徽章 - - 支持深色模式 - - _需求:2.3, 7.4_ - -- [ ] 3. 增强搜索页面UI -- [ ] 3.1 更新搜索表单样式 - - 修改resources/views/filament/pages/search-page.blade.php - - 应用渐变背景和阴影效果到搜索卡片 - - 添加表单加载时的淡入动画 - - 使用增强输入框组件替换原有输入框 - - 使用增强按钮组件替换原有按钮 - - _需求:1.1, 1.2, 1.3, 1.4_ - -- [ ] 3.2 实现响应式搜索表单布局 - - 添加移动端适配样式 - - 使用Tailwind响应式类(sm:, md:, lg:) - - 测试不同屏幕尺寸下的显示效果 - - _需求:1.5_ - -- [ ] 3.3 创建文档卡片视图组件 - - 创建resources/views/filament/resources/document/card.blade.php - - 实现卡片网格布局 - - 添加文档标题、类型徽章、内容片段 - - 添加悬停效果(卡片上浮、阴影增强) - - 使用渐变遮罩处理文本溢出 - - _需求:2.1, 2.2, 2.3, 2.5_ - -- [ ] 3.4 添加搜索结果动画 - - 实现交错淡入动画(stagger animation) - - 为每个卡片添加延迟动画 - - 添加加载骨架屏 - - _需求:2.4, 6.1_ - -- [ ] 3.5 集成Alpine.js到搜索页面 - - 创建resources/js/alpine/search.js - - 实现搜索状态管理(isSearching, hasSearched) - - 实现筛选器切换逻辑 - - 实现视图模式切换(网格/列表) - - 添加搜索加载指示器 - - _需求:6.2, 9.1, 9.2_ - -- [ ] 3.6 实现高级筛选器界面 - - 添加筛选按钮和侧边栏 - - 实现侧边栏滑入动画 - - 显示已选筛选条件的标签 - - 实时更新结果数量徽章 - - 添加清空筛选按钮 - - _需求:9.1, 9.2, 9.3_ - -- [ ] 3.7 实现空状态UI - - 创建友好的空状态插图 - - 添加提示文本 - - 提供建议操作 - - _需求:9.5_ - -- [ ] 4. 增强文档预览模态框 -- [ ] 4.1 更新预览模态框样式 - - 修改resources/views/filament/pages/document-preview-modal.blade.php - - 添加模态框打开动画(缩放淡入) - - 添加半透明背景遮罩和模糊效果 - - 优化内容区域样式 - - _需求:3.1, 3.2_ - -- [ ] 4.2 自定义滚动条样式 - - 为预览内容区域添加自定义滚动条 - - 使用Tailwind的scrollbar插件或自定义CSS - - 支持深色模式滚动条 - - _需求:3.3_ - -- [ ] 4.3 增强Markdown内容样式 - - 优化代码块样式(语法高亮、圆角边框) - - 优化表格样式(斑马纹、悬停高亮) - - 优化标题、列表、引用样式 - - 优化图片显示(响应式、圆角) - - _需求:3.4, 3.5_ - -- [ ] 4.4 集成Alpine.js到预览模态框 - - 创建resources/js/alpine/preview.js - - 实现模态框状态管理(isOpen, isLoading) - - 实现滚动进度跟踪 - - 添加返回顶部按钮(滚动时淡入) - - _需求:8.4_ - -- [ ] 5. 实现深色模式支持 -- [ ] 5.1 配置深色模式检测 - - 在主布局中添加深色模式检测脚本 - - 检测系统偏好(prefers-color-scheme) - - 从localStorage读取用户偏好 - - 应用深色模式类到html元素 - - _需求:7.1_ - -- [ ] 5.2 更新所有组件的深色模式样式 - - 为搜索页面添加深色模式样式 - - 为文档卡片添加深色模式样式 - - 为预览模态框添加深色模式样式 - - 为按钮和输入框添加深色模式样式 - - 确保颜色对比度符合标准 - - _需求:7.2, 7.3, 7.4_ - -- [ ] 5.3 实现主题切换动画 - - 添加主题切换时的颜色过渡效果 - - 使用CSS变量实现平滑过渡 - - 避免切换时的闪烁 - - _需求:7.5_ - -- [ ] 6. 添加微交互效果 -- [ ] 6.1 实现图标动画 - - 为搜索图标添加悬停旋转效果 - - 为下载图标添加悬停缩放效果 - - 为筛选图标添加点击动画 - - _需求:8.1_ - -- [ ] 6.2 实现通知动画 - - 优化Filament通知的显示动画 - - 实现从右侧滑入效果 - - 实现自动淡出效果 - - 添加成功/错误图标动画 - - _需求:4.5, 8.3_ - -- [ ] 6.3 实现排序动画 - - 为排序图标添加旋转动画 - - 添加排序方向指示器 - - 实现列表重排动画 - - _需求:9.4_ - -- [ ] 7. 实现页面过渡和加载状态 -- [ ] 7.1 创建骨架屏组件 - - 创建搜索结果骨架屏 - - 创建文档卡片骨架屏 - - 添加脉冲动画效果 - - _需求:6.1_ - -- [ ] 7.2 实现加载指示器 - - 为搜索按钮添加加载状态 - - 为预览模态框添加加载指示器 - - 使用旋转动画和脉冲效果 - - _需求:6.2_ - -- [ ] 7.3 实现内容过渡动画 - - 为内容状态变化添加淡入淡出效果 - - 为列表项添加滑入滑出动画 - - 优化动画时序 - - _需求:6.3, 6.4_ - -- [ ] 7.4 实现错误状态UI - - 创建错误提示组件 - - 添加抖动动画效果 - - 显示错误图标 - - _需求:6.5_ - -- [ ] 8. 实现无障碍访问支持 -- [ ] 8.1 添加键盘导航支持 - - 为所有交互元素添加tabindex - - 实现清晰的焦点指示器样式 - - 测试Tab键导航顺序 - - 添加键盘快捷键(如Esc关闭模态框) - - _需求:10.1, 10.3_ - -- [ ] 8.2 添加ARIA标签 - - 为按钮添加aria-label - - 为模态框添加role和aria-modal - - 为加载状态添加aria-busy - - 为展开/折叠元素添加aria-expanded - - _需求:10.2_ - -- [ ]* 8.3 编写属性测试验证ARIA标签 - - **属性 2:ARIA标签完整性** - - **验证需求:10.2** - -- [ ] 8.4 实现动画偏好支持 - - 检测prefers-reduced-motion设置 - - 在用户偏好减少动画时禁用动画 - - 提供静态替代方案 - - _需求:10.4_ - -- [ ] 8.5 验证颜色对比度 - - 使用工具检查所有文本的对比度 - - 确保至少4.5:1(普通文本)或3:1(大文本) - - 调整不符合标准的颜色 - - _需求:10.5_ - -- [ ]* 8.6 编写属性测试验证颜色对比度 - - **属性 3:颜色对比度合规性** - - **验证需求:10.5** - -- [ ] 9. 优化性能 -- [ ] 9.1 优化CSS - - 移除未使用的Tailwind类 - - 压缩CSS文件 - - 使用PurgeCSS减小文件大小 - - _需求:性能优化_ - -- [ ] 9.2 优化JavaScript - - 延迟加载非关键JavaScript - - 使用代码分割 - - 压缩JavaScript文件 - - _需求:性能优化_ - -- [ ] 9.3 优化动画性能 - - 使用CSS transform和opacity(避免重排) - - 添加will-change提示 - - 使用contain属性隔离动画 - - 在低性能设备上禁用复杂动画 - - _需求:性能优化_ - -- [ ] 10. 测试和验证 -- [ ]* 10.1 编写组件单元测试 - - 测试按钮组件的各种状态 - - 测试输入框组件的交互 - - 测试卡片组件的渲染 - - 测试徽章组件的颜色逻辑 - - _需求:所有组件相关需求_ - -- [ ]* 10.2 编写Alpine.js组件测试 - - 测试搜索组件的状态管理 - - 测试筛选器逻辑 - - 测试预览模态框逻辑 - - _需求:所有交互相关需求_ - -- [ ]* 10.3 进行无障碍测试 - - 使用axe-core进行自动化扫描 - - 使用屏幕阅读器测试 - - 测试键盘完整导航 - - _需求:10.1-10.5_ - -- [ ]* 10.4 进行视觉回归测试 - - 截图对比测试(使用Percy或Chromatic) - - 测试深色模式显示 - - 测试响应式布局 - - _需求:所有视觉相关需求_ - -- [ ]* 10.5 进行性能测试 - - 使用Chrome DevTools测试动画FPS - - 测试首次内容绘制时间 - - 测试交互就绪时间 - - 在低性能设备上测试 - - _需求:性能相关需求_ - -- [ ]* 10.6 进行浏览器兼容性测试 - - 在Chrome、Firefox、Safari、Edge上测试 - - 在移动浏览器上测试 - - 修复兼容性问题 - - _需求:所有需求_ - -- [ ] 11. 文档和部署 -- [ ] 11.1 更新开发文档 - - 记录新增的UI组件使用方法 - - 记录Alpine.js组件的API - - 记录自定义CSS类的用法 - - 添加样式指南 - - _需求:文档需求_ - -- [ ] 11.2 创建组件演示页面 - - 创建Storybook或类似的组件展示页面 - - 展示所有UI组件的各种状态 - - 提供代码示例 - - _需求:文档需求_ - -- [ ] 11.3 优化生产构建 - - 配置Laravel Mix或Vite进行生产构建 - - 启用CSS和JS压缩 - - 配置资源版本控制 - - 测试生产环境构建 - - _需求:部署需求_ - -- [ ] 11.4 准备部署清单 - - 列出需要部署的文件 - - 列出需要运行的命令 - - 列出需要检查的配置 - - 创建回滚计划 - - _需求:部署需求_ - -- [ ] 12. 最终检查点 - - 确保所有UI增强功能正常工作 - - 验证在不同设备和浏览器上的显示效果 - - 确认无障碍访问功能正常 - - 验证性能指标达标 - - 如有问题请咨询用户 - - _需求:所有需求_ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cfd8bfd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,134 @@ +# 多阶段构建Dockerfile - Laravel知识库系统 (Swoole版本) +# 确保构建为linux/amd64架构 + +# ================================ +# 基础阶段 - 安装系统依赖 +# ================================ +FROM php:8.2-cli-alpine AS base + +# 设置工作目录 +WORKDIR /var/www/html + +# 安装系统依赖 +RUN apk add --no-cache \ + # 基础工具 + curl \ + git \ + unzip \ + zip \ + # PHP扩展依赖 + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + libzip-dev \ + icu-dev \ + oniguruma-dev \ + # Pandoc文档转换工具 + pandoc \ + # Node.js和npm (使用较小的版本) + nodejs \ + npm \ + # 进程管理 + supervisor + +# 配置和安装PHP扩展 +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + pdo_mysql \ + mysqli \ + zip \ + gd \ + intl \ + mbstring \ + opcache \ + bcmath \ + exif \ + pcntl + +# 安装Redis和Swoole扩展 +RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS linux-headers \ + && pecl install redis-6.0.2 swoole-5.1.1 \ + && docker-php-ext-enable redis swoole \ + && apk del .build-deps + +# 安装Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# ================================ +# 构建阶段 - 安装依赖和构建资源 +# ================================ +FROM base AS builder + +# 复制composer文件 +COPY composer.json composer.lock ./ + +# 复制源代码(需要artisan文件) +COPY . . + +# 安装PHP依赖(生产环境) +RUN composer install --no-dev --optimize-autoloader --no-interaction --no-progress + +# 复制package.json文件 +COPY package.json package-lock.json ./ + +# 安装NPM依赖(包括开发依赖用于构建) +RUN npm ci + +# 构建前端资源 +RUN npm run build + +# 设置Laravel缓存和优化 +RUN php artisan config:cache \ + && php artisan route:cache \ + && php artisan view:cache + +# ================================ +# 生产阶段 - 最终镜像 +# ================================ +FROM base AS production + +# 确保www-data用户存在(Alpine中可能已存在) +RUN if ! getent group www-data > /dev/null 2>&1; then \ + addgroup -g 82 -S www-data; \ + fi \ + && if ! getent passwd www-data > /dev/null 2>&1; then \ + adduser -u 82 -D -S -G www-data www-data; \ + fi + +# 复制PHP配置(仅保留基础PHP配置,移除PHP-FPM配置) +COPY docker/php/php.ini /usr/local/etc/php/php.ini + +# 复制Supervisor配置(更新为Swoole版本) +COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# 复制健康检查脚本 +COPY docker/queue-health-check.sh /usr/local/bin/queue-health-check.sh +COPY docker/swoole-health-check.sh /usr/local/bin/swoole-health-check.sh +RUN chmod +x /usr/local/bin/queue-health-check.sh \ + && chmod +x /usr/local/bin/swoole-health-check.sh + +# 从构建阶段复制应用文件 +COPY --from=builder --chown=www-data:www-data /var/www/html /var/www/html + +# 创建必要的目录并设置权限 +RUN mkdir -p /var/www/html/storage/logs \ + && mkdir -p /var/www/html/storage/framework/cache \ + && mkdir -p /var/www/html/storage/framework/sessions \ + && mkdir -p /var/www/html/storage/framework/views \ + && mkdir -p /var/www/html/bootstrap/cache \ + && mkdir -p /var/log/supervisor \ + && mkdir -p /var/log \ + && chown -R www-data:www-data /var/www/html/storage \ + && chown -R www-data:www-data /var/www/html/bootstrap/cache \ + && chmod -R 775 /var/www/html/storage \ + && chmod -R 775 /var/www/html/bootstrap/cache + +# 暴露Swoole端口 +EXPOSE 8000 + +# 健康检查 - 使用Swoole健康检查脚本 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD /usr/local/bin/swoole-health-check.sh || exit 1 + +# 使用supervisor启动多个服务 +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/app/Console/Commands/CreateAdminUser.php b/app/Console/Commands/CreateAdminUser.php new file mode 100644 index 0000000..ad20bfa --- /dev/null +++ b/app/Console/Commands/CreateAdminUser.php @@ -0,0 +1,55 @@ +argument('email'); + $password = $this->argument('password'); + $name = $this->option('name'); + + // 检查用户是否已存在 + if (User::where('email', $email)->exists()) { + $this->error("用户 {$email} 已存在!"); + return 1; + } + + // 创建管理员用户 + $admin = User::create([ + 'name' => $name, + 'email' => $email, + 'password' => Hash::make($password), + 'email_verified_at' => now(), + ]); + + $this->info("管理员用户创建成功!"); + $this->info("姓名: {$admin->name}"); + $this->info("邮箱: {$admin->email}"); + $this->info("密码: {$password}"); + + return 0; + } +} \ No newline at end of file diff --git a/app/Jobs/ConvertDocumentToMarkdown.php b/app/Jobs/ConvertDocumentToMarkdown.php index 87fab3a..9401650 100644 --- a/app/Jobs/ConvertDocumentToMarkdown.php +++ b/app/Jobs/ConvertDocumentToMarkdown.php @@ -79,6 +79,7 @@ class ConvertDocumentToMarkdown implements ShouldQueue $markdown = $result['markdown']; $mediaDir = $result['mediaDir'] ?? null; $tempDir = $result['tempDir']; + $tempDirName = $result['tempDirName']; try { // 保存 Markdown 文件和媒体文件 @@ -88,8 +89,8 @@ class ConvertDocumentToMarkdown implements ShouldQueue $conversionService->updateDocumentMarkdown($this->document, $markdownPath); } finally { // 清理临时目录 - if (isset($tempDir) && file_exists($tempDir)) { - $this->deleteDirectory($tempDir); + if (isset($tempDirName) && \Storage::disk('local')->exists($tempDirName)) { + \Storage::disk('local')->deleteDirectory($tempDirName); } } diff --git a/app/Services/DocumentConversionService.php b/app/Services/DocumentConversionService.php index 7860f80..f807627 100644 --- a/app/Services/DocumentConversionService.php +++ b/app/Services/DocumentConversionService.php @@ -85,9 +85,16 @@ class DocumentConversionService throw new \Exception("文档文件不存在: {$documentPath}"); } - // 创建临时工作目录 - $tempDir = sys_get_temp_dir() . '/pandoc_' . uniqid(); - mkdir($tempDir, 0755, true); + // 使用 Laravel 存储系统创建临时工作目录 + $tempDirName = 'temp/pandoc_' . uniqid(); + + // 确保临时目录存在 + if (!Storage::disk('local')->exists('temp')) { + Storage::disk('local')->makeDirectory('temp'); + } + + Storage::disk('local')->makeDirectory($tempDirName); + $tempDir = Storage::disk('local')->path($tempDirName); $tempOutputPath = $tempDir . '/output.md'; @@ -128,10 +135,11 @@ class DocumentConversionService 'markdown' => $markdown, 'mediaDir' => $hasMedia ? $mediaDir : null, 'tempDir' => $tempDir, + 'tempDirName' => $tempDirName, // 添加相对路径名 ]; } catch (\Exception $e) { // 清理临时目录 - $this->deleteDirectory($tempDir); + Storage::disk('local')->deleteDirectory($tempDirName); throw $e; } } diff --git a/app/Services/DocumentPreviewService.php b/app/Services/DocumentPreviewService.php index 13c6daa..9d6a558 100644 --- a/app/Services/DocumentPreviewService.php +++ b/app/Services/DocumentPreviewService.php @@ -92,15 +92,22 @@ class DocumentPreviewService // 创建 HTML Writer $htmlWriter = IOFactory::createWriter($phpWord, 'HTML'); - // 将内容写入临时文件 - $tempHtmlFile = tempnam(sys_get_temp_dir(), 'doc_preview_') . '.html'; - $htmlWriter->save($tempHtmlFile); + // 使用 Laravel 存储系统创建临时文件 + $tempFileName = 'temp/doc_preview_' . uniqid() . '.html'; + + // 确保临时目录存在 + if (!Storage::disk('local')->exists('temp')) { + Storage::disk('local')->makeDirectory('temp'); + } + + $tempHtmlPath = Storage::disk('local')->path($tempFileName); + $htmlWriter->save($tempHtmlPath); // 读取 HTML 内容 - $htmlContent = file_get_contents($tempHtmlFile); + $htmlContent = Storage::disk('local')->get($tempFileName); // 删除临时文件 - unlink($tempHtmlFile); + Storage::disk('local')->delete($tempFileName); // 将图片嵌入为 base64 $htmlContent = $this->embedImagesInHtml($htmlContent, $images); diff --git a/composer.json b/composer.json index 728e152..d9ec2fc 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "filament/filament": "^3.0", "http-interop/http-factory-guzzle": "^1.2", "laravel/framework": "^12.0", + "laravel/octane": "^2.13", "laravel/scout": "^10.22", "laravel/tinker": "^2.10.1", "league/commonmark": "^2.8", @@ -50,7 +51,29 @@ ], "dev": [ "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --queue=documents,default --tries=3 --timeout=300\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" + "npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --queue=documents,default --tries=3 --timeout=300\" \"npm run dev\" --names=server,queue,vite --kill-others" + ], + "dev-octane": [ + "Composer\\Config::disableProcessTimeout", + "npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan octane:start --watch\" \"php artisan queue:listen --queue=documents,default --tries=3 --timeout=300\" \"npm run dev\" --names=octane,queue,vite --kill-others" + ], + "octane:start": [ + "@php artisan octane:start" + ], + "octane:stop": [ + "@php artisan octane:stop" + ], + "octane:restart": [ + "@php artisan octane:restart" + ], + "octane:reload": [ + "@php artisan octane:reload" + ], + "swoole:start": [ + "@php artisan octane:start --server=swoole" + ], + "swoole:watch": [ + "@php artisan octane:start --server=swoole --watch" ], "test": [ "@php artisan config:clear --ansi", @@ -88,7 +111,11 @@ "allow-plugins": { "pestphp/pest-plugin": true, "php-http/discovery": true - } + }, + "platform": { + "php": "8.2.30" + }, + "platform-check": false }, "minimum-stability": "stable", "prefer-stable": true diff --git a/composer.lock b/composer.lock index 953d9b9..779ddb4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ba7498dee5b9afb02d97116d4ffa08b1", + "content-hash": "c55af88d05e1aa9580f579808452aff9", "packages": [ { "name": "anourvalar/eloquent-serialize", - "version": "1.3.4", + "version": "1.3.5", "source": { "type": "git", "url": "https://github.com/AnourValar/eloquent-serialize.git", - "reference": "0934a98866e02b73e38696961a9d7984b834c9d9" + "reference": "1a7dead8d532657e5358f8f27c0349373517681e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/0934a98866e02b73e38696961a9d7984b834c9d9", - "reference": "0934a98866e02b73e38696961a9d7984b834c9d9", + "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/1a7dead8d532657e5358f8f27c0349373517681e", + "reference": "1a7dead8d532657e5358f8f27c0349373517681e", "shasum": "" }, "require": { @@ -68,9 +68,9 @@ ], "support": { "issues": "https://github.com/AnourValar/eloquent-serialize/issues", - "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.4" + "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.5" }, - "time": "2025-07-30T15:45:57+00:00" + "time": "2025-12-04T13:38:21+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -533,16 +533,16 @@ }, { "name": "doctrine/dbal", - "version": "4.4.0", + "version": "4.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "e8c5163fbec0f34e357431bd1e5fc4056cdf4fdc" + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/e8c5163fbec0f34e357431bd1e5fc4056cdf4fdc", - "reference": "e8c5163fbec0f34e357431bd1e5fc4056cdf4fdc", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", "shasum": "" }, "require": { @@ -619,7 +619,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.4.0" + "source": "https://github.com/doctrine/dbal/tree/4.4.1" }, "funding": [ { @@ -635,7 +635,7 @@ "type": "tidelift" } ], - "time": "2025-11-29T12:17:09+00:00" + "time": "2025-12-04T10:11:03+00:00" }, { "name": "doctrine/deprecations", @@ -1417,31 +1417,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -1472,7 +1472,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -1484,28 +1484,28 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -1534,7 +1534,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -1546,7 +1546,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1961,16 +1961,16 @@ }, { "name": "http-interop/http-factory-guzzle", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/http-interop/http-factory-guzzle.git", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + "reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/c2c859ceb05c3f42e710b60555f4c35b6a4a3995", + "reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995", "shasum": "" }, "require": { @@ -2013,22 +2013,22 @@ ], "support": { "issues": "https://github.com/http-interop/http-factory-guzzle/issues", - "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.1" }, - "time": "2021-07-21T13:50:14+00:00" + "time": "2025-12-15T11:28:16+00:00" }, { "name": "kirschbaum-development/eloquent-power-joins", - "version": "4.2.10", + "version": "4.2.11", "source": { "type": "git", "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", - "reference": "ccda351a75701f5b0a6f94586d9a40f1114302b4" + "reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/ccda351a75701f5b0a6f94586d9a40f1114302b4", - "reference": "ccda351a75701f5b0a6f94586d9a40f1114302b4", + "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/0e3e3372992e4bf82391b3c7b84b435c3db73588", + "reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588", "shasum": "" }, "require": { @@ -2076,22 +2076,110 @@ ], "support": { "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", - "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.10" + "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.11" }, - "time": "2025-11-13T14:57:49+00:00" + "time": "2025-12-17T00:37:48+00:00" }, { - "name": "laravel/framework", - "version": "v12.41.1", + "name": "laminas/laminas-diactoros", + "version": "3.8.0", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "3e229b05935fd0300c632fb1f718c73046d664fc" + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "60c182916b2749480895601649563970f3f12ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/3e229b05935fd0300c632fb1f718c73046d664fc", - "reference": "3e229b05935fd0300c632fb1f718c73046d664fc", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T15:31:36+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.44.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "592bbf1c036042958332eb98e3e8131b29102f33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", + "reference": "592bbf1c036042958332eb98e3e8131b29102f33", "shasum": "" }, "require": { @@ -2179,6 +2267,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -2203,7 +2292,7 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.8.0", + "orchestra/testbench-core": "^10.8.1", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -2265,6 +2354,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -2273,7 +2363,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -2297,7 +2388,97 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-12-03T01:02:13+00:00" + "time": "2025-12-23T15:29:43+00:00" + }, + { + "name": "laravel/octane", + "version": "v2.13.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/octane.git", + "reference": "aae775360fceae422651042d73137fff092ba800" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/octane/zipball/aae775360fceae422651042d73137fff092ba800", + "reference": "aae775360fceae422651042d73137fff092ba800", + "shasum": "" + }, + "require": { + "laminas/laminas-diactoros": "^3.0", + "laravel/framework": "^10.10.1|^11.0|^12.0", + "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "nesbot/carbon": "^2.66.0|^3.0", + "php": "^8.1.0", + "symfony/console": "^6.0|^7.0", + "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0" + }, + "conflict": { + "spiral/roadrunner": "<2023.1.0", + "spiral/roadrunner-cli": "<2.6.0", + "spiral/roadrunner-http": "<3.3.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.6.1", + "inertiajs/inertia-laravel": "^1.3.2|^2.0", + "laravel/scout": "^10.2.1", + "laravel/socialite": "^5.6.1", + "livewire/livewire": "^2.12.3|^3.0", + "mockery/mockery": "^1.5.1", + "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", + "orchestra/testbench": "^8.21|^9.0|^10.0", + "phpstan/phpstan": "^2.1.7", + "phpunit/phpunit": "^10.4|^11.5", + "spiral/roadrunner-cli": "^2.6.0", + "spiral/roadrunner-http": "^3.3.0" + }, + "bin": [ + "bin/roadrunner-worker", + "bin/swoole-server" + ], + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Octane": "Laravel\\Octane\\Facades\\Octane" + }, + "providers": [ + "Laravel\\Octane\\OctaneServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Octane\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Supercharge your Laravel application's performance.", + "keywords": [ + "frankenphp", + "laravel", + "octane", + "roadrunner", + "swoole" + ], + "support": { + "issues": "https://github.com/laravel/octane/issues", + "source": "https://github.com/laravel/octane" + }, + "time": "2025-12-10T15:24:24+00:00" }, { "name": "laravel/prompts", @@ -2360,16 +2541,16 @@ }, { "name": "laravel/scout", - "version": "v10.22.1", + "version": "v10.23.0", "source": { "type": "git", "url": "https://github.com/laravel/scout.git", - "reference": "13ed8e0eeaddd894bf360b85cb873980de19dbaf" + "reference": "fb6d94cfc5708e4202dc00d46e61af0b9f35b03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/scout/zipball/13ed8e0eeaddd894bf360b85cb873980de19dbaf", - "reference": "13ed8e0eeaddd894bf360b85cb873980de19dbaf", + "url": "https://api.github.com/repos/laravel/scout/zipball/fb6d94cfc5708e4202dc00d46e61af0b9f35b03c", + "reference": "fb6d94cfc5708e4202dc00d46e61af0b9f35b03c", "shasum": "" }, "require": { @@ -2436,7 +2617,7 @@ "issues": "https://github.com/laravel/scout/issues", "source": "https://github.com/laravel/scout" }, - "time": "2025-11-25T15:19:35+00:00" + "time": "2025-12-16T15:43:03+00:00" }, { "name": "laravel/serializable-closure", @@ -2756,16 +2937,16 @@ }, { "name": "league/csv", - "version": "9.27.1", + "version": "9.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797" + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/26de738b8fccf785397d05ee2fc07b6cd8749797", - "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/6582ace29ae09ba5b07049d40ea13eb19c8b5073", + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073", "shasum": "" }, "require": { @@ -2775,14 +2956,14 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^1.12.27", + "friendsofphp/php-cs-fixer": "^3.92.3", + "phpbench/phpbench": "^1.4.3", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.2", "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6", - "symfony/var-dumper": "^6.4.8 || ^7.3.0" + "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.5.4", + "symfony/var-dumper": "^6.4.8 || ^7.4.0 || ^8.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", @@ -2843,7 +3024,7 @@ "type": "github" } ], - "time": "2025-10-25T08:35:20+00:00" + "time": "2025-12-27T15:18:42+00:00" }, { "name": "league/flysystem", @@ -3035,20 +3216,20 @@ }, { "name": "league/uri", - "version": "7.6.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "f625804987a0a9112d954f9209d91fec52182344" + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", - "reference": "f625804987a0a9112d954f9209d91fec52182344", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.6", + "league/uri-interfaces": "^7.7", "php": "^8.1", "psr/http-factory": "^1" }, @@ -3121,7 +3302,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.6.0" + "source": "https://github.com/thephpleague/uri/tree/7.7.0" }, "funding": [ { @@ -3129,20 +3310,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2025-12-07T16:02:06+00:00" }, { "name": "league/uri-interfaces", - "version": "7.6.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", "shasum": "" }, "require": { @@ -3205,7 +3386,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" }, "funding": [ { @@ -3213,20 +3394,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2025-12-07T16:03:21+00:00" }, { "name": "livewire/livewire", - "version": "v3.7.0", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb" + "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", - "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", + "url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", + "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", "shasum": "" }, "require": { @@ -3281,7 +3462,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.0" + "source": "https://github.com/livewire/livewire/tree/v3.7.3" }, "funding": [ { @@ -3289,7 +3470,7 @@ "type": "github" } ], - "time": "2025-11-12T17:58:16+00:00" + "time": "2025-12-19T02:00:29+00:00" }, { "name": "masterminds/html5", @@ -3543,16 +3724,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -3560,9 +3741,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -3644,7 +3825,7 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nette/schema", @@ -3713,16 +3894,16 @@ }, { "name": "nette/utils", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", "shasum": "" }, "require": { @@ -3796,22 +3977,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.0" + "source": "https://github.com/nette/utils/tree/v4.1.1" }, - "time": "2025-12-01T17:49:23+00:00" + "time": "2025-12-22T12:14:32+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3854,9 +4035,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", @@ -3947,16 +4128,16 @@ }, { "name": "openspout/openspout", - "version": "v4.32.0", + "version": "v4.28.5", "source": { "type": "git", "url": "https://github.com/openspout/openspout.git", - "reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d" + "reference": "ab05a09fe6fce57c90338f83280648a9786ce36b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/openspout/openspout/zipball/41f045c1f632e1474e15d4c7bc3abcb4a153563d", - "reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d", + "url": "https://api.github.com/repos/openspout/openspout/zipball/ab05a09fe6fce57c90338f83280648a9786ce36b", + "reference": "ab05a09fe6fce57c90338f83280648a9786ce36b", "shasum": "" }, "require": { @@ -3966,17 +4147,17 @@ "ext-libxml": "*", "ext-xmlreader": "*", "ext-zip": "*", - "php": "~8.3.0 || ~8.4.0 || ~8.5.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { "ext-zlib": "*", - "friendsofphp/php-cs-fixer": "^3.86.0", - "infection/infection": "^0.31.2", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^2.1.22", - "phpstan/phpstan-phpunit": "^2.0.7", - "phpstan/phpstan-strict-rules": "^2.0.6", - "phpunit/phpunit": "^12.3.7" + "friendsofphp/php-cs-fixer": "^3.68.3", + "infection/infection": "^0.29.10", + "phpbench/phpbench": "^1.4.0", + "phpstan/phpstan": "^2.1.2", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^11.5.4" }, "suggest": { "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)", @@ -4024,7 +4205,7 @@ ], "support": { "issues": "https://github.com/openspout/openspout/issues", - "source": "https://github.com/openspout/openspout/tree/v4.32.0" + "source": "https://github.com/openspout/openspout/tree/v4.28.5" }, "funding": [ { @@ -4036,7 +4217,7 @@ "type": "github" } ], - "time": "2025-09-03T16:03:54+00:00" + "time": "2025-01-30T13:51:11+00:00" }, { "name": "php-http/discovery", @@ -4279,16 +4460,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -4338,7 +4519,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -4350,7 +4531,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "psr/cache", @@ -4815,16 +4996,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.15", + "version": "v0.12.18", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "38953bc71491c838fcb6ebcbdc41ab7483cd549c" + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/38953bc71491c838fcb6ebcbdc41ab7483cd549c", - "reference": "38953bc71491c838fcb6ebcbdc41ab7483cd549c", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "shasum": "" }, "require": { @@ -4832,8 +5013,8 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -4888,9 +5069,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.15" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" }, - "time": "2025-11-28T00:00:14+00:00" + "time": "2025-12-17T14:35:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -5014,20 +5195,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -5086,9 +5267,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "ryangjchandler/blade-capture-directive", @@ -5427,16 +5608,16 @@ }, { "name": "symfony/console", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -5501,7 +5682,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.0" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -5521,7 +5702,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/css-selector", @@ -5743,24 +5924,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.0.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "573f95783a2ec6e38752979db139f09fec033f03" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", - "reference": "573f95783a2ec6e38752979db139f09fec033f03", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/security-http": "<7.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -5769,14 +5950,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/error-handler": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^7.4|^8.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5804,7 +5985,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" }, "funding": [ { @@ -5824,7 +6005,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T14:17:19+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6046,16 +6227,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "769c1720b68e964b13b58529c17d4a385c62167b" + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/769c1720b68e964b13b58529c17d4a385c62167b", - "reference": "769c1720b68e964b13b58529c17d4a385c62167b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", "shasum": "" }, "require": { @@ -6104,7 +6285,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" }, "funding": [ { @@ -6124,20 +6305,20 @@ "type": "tidelift" } ], - "time": "2025-11-13T08:49:24+00:00" + "time": "2025-12-07T11:13:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.0", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "7348193cd384495a755554382e4526f27c456085" + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7348193cd384495a755554382e4526f27c456085", - "reference": "7348193cd384495a755554382e4526f27c456085", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", "shasum": "" }, "require": { @@ -6223,7 +6404,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" }, "funding": [ { @@ -6243,7 +6424,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:38:24+00:00" + "time": "2025-12-08T07:43:37+00:00" }, { "name": "symfony/mailer", @@ -7392,6 +7573,94 @@ ], "time": "2025-10-16T11:21:06+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "0101ff8bd0506703b045b1670960302d302a726c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/0101ff8bd0506703b045b1670960302d302a726c", + "reference": "0101ff8bd0506703b045b1670960302d302a726c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-13T08:38:49+00:00" + }, { "name": "symfony/routing", "version": "v7.4.0", @@ -7566,34 +7835,35 @@ }, { "name": "symfony/string", - "version": "v8.0.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f929eccf09531078c243df72398560e32fa4cf4f" + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", - "reference": "f929eccf09531078c243df72398560e32fa4cf4f", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-intl-grapheme": "^1.33", - "symfony/polyfill-intl-normalizer": "^1.0", - "symfony/polyfill-mbstring": "^1.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7632,7 +7902,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.0" + "source": "https://github.com/symfony/string/tree/v7.4.0" }, "funding": [ { @@ -7652,7 +7922,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:37:55+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation", @@ -8003,23 +8273,23 @@ }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -8052,32 +8322,32 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -8126,7 +8396,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -8138,7 +8408,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -8434,16 +8704,16 @@ }, { "name": "composer/class-map-generator", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6" + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/2373419b7709815ed323ebf18c3c72d03ff4a8a6", - "reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8f5fa3cc214230e71f54924bd0197a3bcc705eb1", + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1", "shasum": "" }, "require": { @@ -8487,7 +8757,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.7.0" + "source": "https://github.com/composer/class-map-generator/tree/1.7.1" }, "funding": [ { @@ -8499,7 +8769,7 @@ "type": "github" } ], - "time": "2025-11-19T10:41:15+00:00" + "time": "2025-12-29T13:15:25+00:00" }, { "name": "composer/pcre", @@ -9417,23 +9687,23 @@ }, { "name": "laravel-lang/config", - "version": "1.14.0", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/Laravel-Lang/config.git", - "reference": "0f6a41a1d5f4bde6ff59fbfd9e349ac64b737c69" + "reference": "080b7cf77eeebdd7e7c267f1b2c3c7fc20408f3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Laravel-Lang/config/zipball/0f6a41a1d5f4bde6ff59fbfd9e349ac64b737c69", - "reference": "0f6a41a1d5f4bde6ff59fbfd9e349ac64b737c69", + "url": "https://api.github.com/repos/Laravel-Lang/config/zipball/080b7cf77eeebdd7e7c267f1b2c3c7fc20408f3c", + "reference": "080b7cf77eeebdd7e7c267f1b2c3c7fc20408f3c", "shasum": "" }, "require": { "archtechx/enums": "^1.0", "illuminate/config": "^10.0 || ^11.0 || ^12.0", "illuminate/support": "^10.0 || ^11.0 || ^12.0", - "laravel-lang/locale-list": "^1.5", + "laravel-lang/locale-list": "^1.6", "php": "^8.1" }, "require-dev": { @@ -9485,9 +9755,9 @@ ], "support": { "issues": "https://github.com/Laravel-Lang/config/issues", - "source": "https://github.com/Laravel-Lang/config/tree/1.14.0" + "source": "https://github.com/Laravel-Lang/config/tree/1.15.0" }, - "time": "2025-04-11T07:31:54+00:00" + "time": "2025-12-04T09:58:46+00:00" }, { "name": "laravel-lang/http-statuses", @@ -9609,16 +9879,16 @@ }, { "name": "laravel-lang/lang", - "version": "15.26.2", + "version": "15.26.3", "source": { "type": "git", "url": "https://github.com/Laravel-Lang/lang.git", - "reference": "4f49e4a77ced9ace7955db2159234e4a9c0b22a3" + "reference": "a32a00e3239d33af5000b947a488387040369e5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Laravel-Lang/lang/zipball/4f49e4a77ced9ace7955db2159234e4a9c0b22a3", - "reference": "4f49e4a77ced9ace7955db2159234e4a9c0b22a3", + "url": "https://api.github.com/repos/Laravel-Lang/lang/zipball/a32a00e3239d33af5000b947a488387040369e5c", + "reference": "a32a00e3239d33af5000b947a488387040369e5c", "shasum": "" }, "require": { @@ -9669,7 +9939,7 @@ "issues": "https://github.com/Laravel-Lang/lang/issues", "source": "https://github.com/Laravel-Lang/lang" }, - "time": "2025-10-29T12:19:07+00:00" + "time": "2025-12-09T12:42:35+00:00" }, { "name": "laravel-lang/locale-list", @@ -9887,16 +10157,16 @@ }, { "name": "laravel-lang/moonshine", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/Laravel-Lang/moonshine.git", - "reference": "3ee6678d649983646831dc036fe2c0b510683e45" + "reference": "d91b2de3fa94d71accac81726f907747c770127f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Laravel-Lang/moonshine/zipball/3ee6678d649983646831dc036fe2c0b510683e45", - "reference": "3ee6678d649983646831dc036fe2c0b510683e45", + "url": "https://api.github.com/repos/Laravel-Lang/moonshine/zipball/d91b2de3fa94d71accac81726f907747c770127f", + "reference": "d91b2de3fa94d71accac81726f907747c770127f", "shasum": "" }, "require": { @@ -9949,9 +10219,9 @@ ], "support": { "issues": "https://github.com/Laravel-Lang/moonshine/issues", - "source": "https://github.com/Laravel-Lang/moonshine/tree/1.7.0" + "source": "https://github.com/Laravel-Lang/moonshine/tree/1.7.1" }, - "time": "2025-11-17T02:56:35+00:00" + "time": "2025-12-16T11:06:37+00:00" }, { "name": "laravel-lang/native-country-names", @@ -10551,16 +10821,16 @@ }, { "name": "laravel/sail", - "version": "v1.49.0", + "version": "v1.51.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "070c7f34ca8dbece4350fbfe0bab580047dfacc7" + "reference": "1c74357df034e869250b4365dd445c9f6ba5d068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/070c7f34ca8dbece4350fbfe0bab580047dfacc7", - "reference": "070c7f34ca8dbece4350fbfe0bab580047dfacc7", + "url": "https://api.github.com/repos/laravel/sail/zipball/1c74357df034e869250b4365dd445c9f6ba5d068", + "reference": "1c74357df034e869250b4365dd445c9f6ba5d068", "shasum": "" }, "require": { @@ -10610,7 +10880,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-11-25T21:15:57+00:00" + "time": "2025-12-09T13:33:49+00:00" }, { "name": "mockery/mockery", @@ -11351,16 +11621,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.5", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { @@ -11370,7 +11640,7 @@ "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { "mockery/mockery": "~1.3.5 || ~1.6.0", @@ -11409,9 +11679,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" }, - "time": "2025-11-27T19:50:05+00:00" + "time": "2025-12-22T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -11520,35 +11790,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -11586,7 +11856,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -11606,7 +11876,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -13002,16 +13272,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { @@ -13054,7 +13324,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.0" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -13074,7 +13344,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -13187,23 +13457,23 @@ }, { "name": "webmozart/assert", - "version": "1.12.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + "reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/1b34b004e35a164bc5bb6ebd33c844b2d8069a54", + "reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54", "shasum": "" }, "require": { "ext-ctype": "*", "ext-date": "*", "ext-filter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.2" }, "suggest": { "ext-intl": "", @@ -13213,7 +13483,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -13229,6 +13499,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -13239,9 +13513,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.1" + "source": "https://github.com/webmozarts/assert/tree/2.0.0" }, - "time": "2025-10-29T15:56:20+00:00" + "time": "2025-12-16T21:36:00+00:00" } ], "aliases": [], @@ -13253,5 +13527,8 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "platform-overrides": { + "php": "8.2.30" + }, + "plugin-api-version": "2.9.0" } diff --git a/config/octane.php b/config/octane.php new file mode 100644 index 0000000..56819a6 --- /dev/null +++ b/config/octane.php @@ -0,0 +1,224 @@ + env('OCTANE_SERVER', 'swoole'), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- + | + | When this configuration value is set to "true", Octane will inform the + | framework that all absolute links must be generated using the HTTPS + | protocol. Otherwise your links may be generated using plain HTTP. + | + */ + + 'https' => env('OCTANE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Octane Listeners + |-------------------------------------------------------------------------- + | + | All of the event listeners for Octane's events are defined below. These + | listeners are responsible for resetting your application's state for + | the next request. You may even add your own listeners to the list. + | + */ + + 'listeners' => [ + WorkerStarting::class => [ + EnsureUploadedFilesAreValid::class, + EnsureUploadedFilesCanBeMoved::class, + ], + + RequestReceived::class => [ + // 准备应用程序处理下一个操作 + // 准备应用程序处理下一个请求 + // + ], + + RequestHandled::class => [ + // + ], + + RequestTerminated::class => [ + // FlushUploadedFiles::class, + ], + + TaskReceived::class => [ + // 准备应用程序处理下一个操作 + // + ], + + TaskTerminated::class => [ + // + ], + + TickReceived::class => [ + // 准备应用程序处理下一个操作 + // + ], + + TickTerminated::class => [ + // + ], + + OperationTerminated::class => [ + FlushOnce::class, + FlushTemporaryContainerInstances::class, + // DisconnectFromDatabases::class, + // CollectGarbage::class, + ], + + WorkerErrorOccurred::class => [ + ReportException::class, + StopWorkerIfNecessary::class, + ], + + WorkerStopping::class => [ + CloseMonologHandlers::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Warm / Flush Bindings + |-------------------------------------------------------------------------- + | + | The bindings listed below will either be pre-warmed when a worker boots + | or they will be flushed before every new request. Flushing a binding + | will force the container to resolve that binding again when asked. + | + */ + + 'warm' => [ + // 默认预热的服务 + ], + + 'flush' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Tables + |-------------------------------------------------------------------------- + | + | While using Swoole, you may define additional tables as required by the + | application. These tables can be used to store data that needs to be + | quickly accessed by other workers on the particular Swoole server. + | + */ + + 'tables' => [ + 'example:1000' => [ + 'name' => 'string:1000', + 'votes' => 'int', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Cache Table + |-------------------------------------------------------------------------- + | + | While using Swoole, you may leverage the Octane cache, which is powered + | by a Swoole table. You may set the maximum number of rows as well as + | the number of bytes per row using the configuration options below. + | + */ + + 'cache' => [ + 'rows' => env('OCTANE_CACHE_ROWS', 1000), + 'bytes' => env('OCTANE_CACHE_BYTES', 10000), + ], + + /* + |-------------------------------------------------------------------------- + | File Watching + |-------------------------------------------------------------------------- + | + | The following list of files and directories will be watched when using + | the --watch option offered by Octane. If any of the directories and + | files are changed, Octane will automatically reload your workers. + | + */ + + 'watch' => [ + 'app', + 'bootstrap', + 'config/**/*.php', + 'database/**/*.php', + 'public/**/*.php', + 'resources/**/*.php', + 'routes', + 'composer.lock', + '.env', + ], + + /* + |-------------------------------------------------------------------------- + | Garbage Collection Threshold + |-------------------------------------------------------------------------- + | + | When executing long-lived PHP scripts such as Octane, memory can build + | up before being cleared by PHP. You can force Octane to run garbage + | collection if your application consumes this amount of megabytes. + | + */ + + 'garbage' => env('OCTANE_GARBAGE_COLLECTION', 50), + + /* + |-------------------------------------------------------------------------- + | Maximum Execution Time + |-------------------------------------------------------------------------- + | + | The following setting configures the maximum execution time for requests + | being handled by Octane. You may set this value to 0 to indicate that + | there isn't a specific time limit on Octane request execution time. + | + */ + + 'max_execution_time' => env('OCTANE_MAX_EXECUTION_TIME', 30), + +]; diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..13142af --- /dev/null +++ b/deploy.sh @@ -0,0 +1,284 @@ +#!/bin/bash + +# 知识库系统部署脚本 +# 使用Laravel Octane + Swoole + +set -e # 遇到错误立即退出 + +# 配置变量 +SERVER_HOST="192.168.1.33" +SERVER_USER="root" +SERVER_PASSWORD="Sipai@123" +SERVER_PATH="/opt/KnowledgeBase" +IMAGE_NAME="knowledge-base-app" +IMAGE_TAG="latest" +COMPOSE_VERSION="1.25.5" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查必要工具 +check_requirements() { + log_info "检查部署环境..." + + if ! command -v docker &> /dev/null; then + log_error "Docker 未安装" + exit 1 + fi + + if ! command -v sshpass &> /dev/null; then + log_error "sshpass 未安装,请先安装: brew install sshpass" + exit 1 + fi + + log_info "环境检查完成" +} + +# 设置网络代理(如果需要) +setup_proxy() { + if [ "$USE_PROXY" = "true" ]; then + log_info "设置网络代理..." + export https_proxy=http://127.0.0.1:7890 + export http_proxy=http://127.0.0.1:7890 + export all_proxy=socks5://127.0.0.1:7890 + log_info "代理设置完成" + fi +} + +# 清理本地构建文件 +clean_local() { + log_info "清理本地构建文件..." + + # 清理不需要的文件 + rm -rf node_modules/.cache + rm -rf storage/logs/*.log + rm -rf storage/framework/cache/data/* + rm -rf storage/framework/sessions/* + rm -rf storage/framework/views/* + + log_info "本地清理完成" +} + +# 构建Docker镜像 +build_image() { + log_info "开始构建Docker镜像..." + + # 构建镜像 + docker build --platform linux/amd64 -t ${IMAGE_NAME}:${IMAGE_TAG} . + + if [ $? -eq 0 ]; then + log_info "Docker镜像构建成功" + else + log_error "Docker镜像构建失败" + exit 1 + fi +} + +# 导出镜像为tar包 +export_image() { + log_info "导出Docker镜像..." + + docker save ${IMAGE_NAME}:${IMAGE_TAG} | gzip > ${IMAGE_NAME}-${IMAGE_TAG}.tar.gz + + if [ $? -eq 0 ]; then + log_info "镜像导出成功: ${IMAGE_NAME}-${IMAGE_TAG}.tar.gz" + else + log_error "镜像导出失败" + exit 1 + fi +} + +# 同步代码到服务器 +sync_code() { + log_info "同步代码到服务器..." + + # 创建临时目录用于同步 + TEMP_DIR=$(mktemp -d) + + # 复制需要的文件 + cp -r . "$TEMP_DIR/" + + # 删除不需要的文件 + cd "$TEMP_DIR" + rm -rf node_modules + # 保留vendor目录,因为服务器上缺少Octane包 + rm -rf storage/logs/*.log + rm -rf storage/framework/cache/data/* + rm -rf storage/framework/sessions/* + rm -rf storage/framework/views/* + rm -rf .git + rm -rf tests + rm -rf docs + rm -rf .DS_Store + rm -rf *.tar.gz + + # 同步到服务器 + sshpass -p "${SERVER_PASSWORD}" rsync -avz --delete \ + --exclude='storage/mysql/' \ + --exclude='storage/redis/' \ + --exclude='storage/meilisearch/' \ + --exclude='storage/app/documents/' \ + --exclude='storage/app/markdown/' \ + "$TEMP_DIR/" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" + + # 清理临时目录 + rm -rf "$TEMP_DIR" + + log_info "代码同步完成" +} + +# 上传Docker镜像 +upload_image() { + log_info "上传Docker镜像到服务器..." + + sshpass -p "${SERVER_PASSWORD}" scp ${IMAGE_NAME}-${IMAGE_TAG}.tar.gz \ + ${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/ + + log_info "镜像上传完成" +} + +# 在服务器上部署 +deploy_on_server() { + log_info "在服务器上执行部署..." + + sshpass -p "${SERVER_PASSWORD}" ssh ${SERVER_USER}@${SERVER_HOST} << EOF + cd ${SERVER_PATH} + + # 停止现有服务 + echo "停止现有服务..." + docker-compose -f docker-compose-simple.yml down || true + + # 删除旧镜像 + echo "清理旧镜像..." + docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true + docker system prune -f + + # 加载新镜像 + echo "加载新镜像..." + docker load < ${IMAGE_NAME}-${IMAGE_TAG}.tar.gz + + # 删除镜像文件 + rm -f ${IMAGE_NAME}-${IMAGE_TAG}.tar.gz + + # 复制生产环境配置 + echo "设置生产环境配置..." + cp .env.production .env + + # 生成新的APP_KEY(如果需要) + if grep -q "your-app-key-here" .env; then + echo "生成新的APP_KEY..." + APP_KEY=\$(openssl rand -base64 32) + sed -i "s|your-app-key-here-change-this-in-production|base64:\$APP_KEY|g" .env + fi + + # 生成新的MEILISEARCH_KEY(如果需要) + if grep -q "your-master-key-change-this-in-production" .env; then + echo "生成新的MEILISEARCH_KEY..." + MEILI_KEY=\$(openssl rand -hex 32) + sed -i "s|your-master-key-change-this-in-production|\$MEILI_KEY|g" .env + fi + + # 启动服务 + echo "启动服务..." + docker-compose -f docker-compose-simple.yml up -d + + # 等待服务启动 + echo "等待服务启动..." + sleep 30 + + # 运行数据库迁移 + echo "运行数据库迁移..." + docker-compose -f docker-compose-simple.yml exec -T app php artisan migrate --force + + # 清理缓存 + echo "清理应用缓存..." + docker-compose -f docker-compose-simple.yml exec -T app php artisan config:clear + docker-compose -f docker-compose-simple.yml exec -T app php artisan cache:clear + docker-compose -f docker-compose-simple.yml exec -T app php artisan route:clear + docker-compose -f docker-compose-simple.yml exec -T app php artisan view:clear + + # 重新生成缓存 + echo "重新生成缓存..." + docker-compose -f docker-compose-simple.yml exec -T app php artisan config:cache + docker-compose -f docker-compose-simple.yml exec -T app php artisan route:cache + docker-compose -f docker-compose-simple.yml exec -T app php artisan view:cache + + # 创建搜索索引 + echo "创建搜索索引..." + docker-compose -f docker-compose-simple.yml exec -T app php artisan scout:import || true + + echo "部署完成!" +EOF + + log_info "服务器部署完成" +} + +# 验证部署 +verify_deployment() { + log_info "验证部署状态..." + + sshpass -p "${SERVER_PASSWORD}" ssh ${SERVER_USER}@${SERVER_HOST} << EOF + cd ${SERVER_PATH} + + echo "=== 服务状态 ===" + docker-compose -f docker-compose-simple.yml ps + + echo "=== 应用健康检查 ===" + curl -f http://localhost:8000/health || echo "健康检查失败" + + echo "=== 队列状态 ===" + docker-compose -f docker-compose-simple.yml exec -T app php artisan queue:work --once --timeout=10 || echo "队列测试失败" + + echo "=== Meilisearch状态 ===" + curl -f http://localhost:7700/health || echo "Meilisearch连接失败" + + echo "=== 数据库连接 ===" + docker-compose -f docker-compose-simple.yml exec -T app php artisan tinker --execute="DB::connection()->getPdo(); echo 'Database connected successfully';" || echo "数据库连接失败" +EOF + + log_info "部署验证完成" +} + +# 清理本地文件 +cleanup() { + log_info "清理本地文件..." + rm -f ${IMAGE_NAME}-${IMAGE_TAG}.tar.gz + log_info "清理完成" +} + +# 主函数 +main() { + log_info "开始部署知识库系统..." + + check_requirements + setup_proxy + clean_local + build_image + export_image + sync_code + upload_image + deploy_on_server + verify_deployment + cleanup + + log_info "部署流程全部完成!" + log_info "访问地址: http://${SERVER_HOST}:8000" +} + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/docker-compose-production.yml b/docker-compose-production.yml new file mode 100644 index 0000000..1301941 --- /dev/null +++ b/docker-compose-production.yml @@ -0,0 +1,100 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: knowledge_base_mysql + environment: + MYSQL_ROOT_PASSWORD: secure_password_change_this_in_production + MYSQL_DATABASE: knowledge_base + MYSQL_USER: knowledge_user + MYSQL_PASSWORD: secure_password_change_this_in_production + MYSQL_CHARACTER_SET_SERVER: utf8mb4 + MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci + volumes: + - ./storage/mysql:/var/lib/mysql + ports: + - "3306:3306" + restart: unless-stopped + command: --default-authentication-plugin=mysql_native_password --bind-address=0.0.0.0 + networks: + - app-network + + redis: + image: redis:7-alpine + container_name: knowledge_base_redis + volumes: + - ./storage/redis:/data + ports: + - "6379:6379" + restart: unless-stopped + command: redis-server --appendonly yes --bind 0.0.0.0 + networks: + - app-network + + meilisearch: + image: getmeili/meilisearch:v1.5 + container_name: knowledge_base_meilisearch + environment: + MEILI_MASTER_KEY: your-master-key-change-this-in-production + MEILI_ENV: production + volumes: + - ./storage/meilisearch:/meili_data + ports: + - "7700:7700" + restart: unless-stopped + networks: + - app-network + + app: + image: knowledge-base-app:latest + container_name: knowledge_base_app + environment: + APP_NAME: "知识库系统" + APP_ENV: production + APP_KEY: base64:your-app-key-here-change-this-in-production + APP_DEBUG: "false" + APP_URL: http://192.168.1.33:8000 + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: knowledge_base + DB_USERNAME: knowledge_user + DB_PASSWORD: secure_password_change_this_in_production + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: "" + CACHE_STORE: redis + SESSION_DRIVER: redis + QUEUE_CONNECTION: redis + SCOUT_DRIVER: meilisearch + MEILISEARCH_HOST: http://meilisearch:7700 + MEILISEARCH_KEY: your-master-key-change-this-in-production + LOG_CHANNEL: stack + FILESYSTEM_DISK: local + SESSION_DOMAIN: "" + SESSION_SECURE: "false" + # Swoole/Octane 配置 + OCTANE_SERVER: swoole + OCTANE_HOST: 0.0.0.0 + OCTANE_PORT: 8000 + OCTANE_WORKERS: 8 + OCTANE_TASK_WORKERS: 4 + OCTANE_MAX_REQUESTS: 1000 + OCTANE_WATCH: "false" + volumes: + - ./:/var/www/html + - ./storage/app:/var/www/html/storage + ports: + - "8000:8000" + restart: unless-stopped + depends_on: + - mysql + - redis + - meilisearch + networks: + - app-network + +networks: + app-network: + driver: bridge diff --git a/docker-compose-simple.yml b/docker-compose-simple.yml new file mode 100644 index 0000000..bb14c1b --- /dev/null +++ b/docker-compose-simple.yml @@ -0,0 +1,95 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: knowledge_base_mysql + environment: + MYSQL_ROOT_PASSWORD: 42d3da6bb45212fc63923140bf487cdb + MYSQL_DATABASE: knowledge_base + MYSQL_USER: knowledge_user + MYSQL_PASSWORD: 42d3da6bb45212fc63923140bf487cdb + MYSQL_CHARACTER_SET_SERVER: utf8mb4 + MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci + volumes: + - ./storage/mysql:/var/lib/mysql + ports: + - "3306:3306" + restart: unless-stopped + command: --default-authentication-plugin=mysql_native_password --bind-address=0.0.0.0 + + redis: + image: redis:7-alpine + container_name: knowledge_base_redis + volumes: + - ./storage/redis:/data + ports: + - "6379:6379" + restart: unless-stopped + command: redis-server --appendonly yes --bind 0.0.0.0 + + meilisearch: + image: getmeili/meilisearch:v1.5 + container_name: knowledge_base_meilisearch + environment: + MEILI_MASTER_KEY: ae1f17f49ccacf3d62c031fdcec8d2c8f89cb9d7949fbad00ae4f592517e400a + MEILI_ENV: production + volumes: + - ./storage/meilisearch:/meili_data + ports: + - "7700:7700" + restart: unless-stopped + + app: + image: knowledge-base-app:latest + container_name: knowledge_base_app + environment: + APP_NAME: "知识库系统" + APP_ENV: production + APP_KEY: base64:35mzQaN6bI37mRDJy36NnkWLIbruftVNMH4q6ZTesQM= + APP_DEBUG: "false" + APP_URL: http://192.168.1.33:8000 + DB_CONNECTION: mysql + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_DATABASE: knowledge_base + DB_USERNAME: knowledge_user + DB_PASSWORD: 42d3da6bb45212fc63923140bf487cdb + REDIS_HOST: 127.0.0.1 + REDIS_PORT: 6379 + REDIS_PASSWORD: "" + CACHE_STORE: redis + SESSION_DRIVER: redis + QUEUE_CONNECTION: redis + SCOUT_DRIVER: meilisearch + MEILISEARCH_HOST: http://127.0.0.1:7700 + MEILISEARCH_KEY: ae1f17f49ccacf3d62c031fdcec8d2c8f89cb9d7949fbad00ae4f592517e400a + LOG_CHANNEL: stack + FILESYSTEM_DISK: local + SESSION_DOMAIN: "" + SESSION_SECURE: "false" + # Swoole/Octane 配置 + OCTANE_SERVER: swoole + OCTANE_HOST: 0.0.0.0 + OCTANE_PORT: 8000 + OCTANE_WORKERS: 8 + OCTANE_TASK_WORKERS: 4 + OCTANE_MAX_REQUESTS: 1000 + OCTANE_WATCH: "false" + volumes: + - ./:/var/www/html + - ./storage/app:/var/www/html/storage + ports: + - "8000:8000" + restart: unless-stopped + depends_on: + - mysql + - redis + - meilisearch + network_mode: host + healthcheck: + test: ["CMD", "/bin/sh", "/var/www/html/docker/swoole-health-check.sh"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index fb61664..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - meilisearch: - image: getmeili/meilisearch:v1.5 - container_name: knowledge_base_meilisearch - ports: - - "7700:7700" - environment: - - MEILI_MASTER_KEY=your-master-key-change-this-in-production - - MEILI_ENV=development - volumes: - - ./storage/meilisearch:/meili_data - restart: unless-stopped - networks: - - knowledge_base_network - -networks: - knowledge_base_network: - driver: bridge diff --git a/docker-images/images-manifest.txt b/docker-images/images-manifest.txt new file mode 100644 index 0000000..59f0eed --- /dev/null +++ b/docker-images/images-manifest.txt @@ -0,0 +1,22 @@ +# Docker镜像导出清单 +# 生成时间: 2025年12月24日 星期三 22时09分59秒 CST +# 导出目录: /Users/sharpclaws/Desktop/KnowledgeBase/docker-images +# 压缩: true +# 验证: true + +文件: knowledge-base-app_latest.tar.gz +大小: 161M +SHA256: aae2b368e70101940cab2ce5296a7c5d11c97bbd2aa4410861bf0eea32609f6c + +文件: mysql_8.0.tar.gz +大小: 224M +SHA256: d55e5d838376948883557b150319e8c9f6ca3425bb600a69369f3b8396cb9b07 + +文件: redis_7-alpine.tar.gz +大小: 17M +SHA256: e8f24bdadf73c7b47dec410783ac8a78c9544c5affb096f83bec7fb569da1b60 + +文件: getmeili_meilisearch_v1.5.tar.gz +大小: 97M +SHA256: 916ba3f2c6a56b63af0c8bc6de50715a18439a5bc1b57b729b8a2eef51e056ee + diff --git a/docker-images/import-images.sh b/docker-images/import-images.sh new file mode 100755 index 0000000..f694356 --- /dev/null +++ b/docker-images/import-images.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Docker镜像导入脚本 +# 自动生成,用于导入导出的Docker镜像 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "开始导入Docker镜像..." + +# 检查Docker是否运行 +if ! docker info >/dev/null 2>&1; then + echo "错误: Docker未运行或无法访问" + exit 1 +fi + +# 导入所有tar文件 +for file in "$SCRIPT_DIR"/*.tar*; do + if [[ -f "$file" ]]; then + echo "导入镜像: $(basename "$file")" + + if [[ "$file" == *.gz ]]; then + # 解压并导入 + if gunzip -c "$file" | docker load; then + echo "✓ 镜像导入成功" + else + echo "✗ 镜像导入失败" + fi + else + # 直接导入 + if docker load -i "$file"; then + echo "✓ 镜像导入成功" + else + echo "✗ 镜像导入失败" + fi + fi + fi +done + +echo "镜像导入完成" +echo "可用镜像列表:" +docker images diff --git a/docker/DATA_PERSISTENCE_README.md b/docker/DATA_PERSISTENCE_README.md new file mode 100644 index 0000000..8d8d73b --- /dev/null +++ b/docker/DATA_PERSISTENCE_README.md @@ -0,0 +1,126 @@ +# 数据持久化和目录映射实现完成 + +## 任务概述 + +✅ **任务3: 实现数据持久化和目录映射** 已完成 + +本任务实现了Docker部署中的完整数据持久化和目录映射配置,确保容器重启后数据不丢失,满足生产环境的可靠性要求。 + +## 实现的功能 + +### 1. 项目代码目录映射到容器 ✅ +- **配置**: `./:/var/www/html` +- **用途**: 支持开发环境代码热重载 +- **应用于**: 应用容器和队列容器 + +### 2. 上传文档存储目录持久化 ✅ +- **文档存储**: `documents_data:/var/www/html/storage/app/private/documents` +- **公共文件**: `public_data:/var/www/html/storage/app/public` +- **映射到**: `./storage/app/private/documents` 和 `./storage/app/public` + +### 3. 数据库数据目录持久化 ✅ +- **配置**: `mysql_data:/var/lib/mysql` +- **映射到**: `./storage/mysql` +- **用途**: MySQL数据库文件持久化 + +### 4. 搜索引擎数据目录持久化 ✅ +- **配置**: `meilisearch_data:/meili_data` +- **映射到**: `./storage/meilisearch` +- **用途**: Meilisearch搜索索引持久化 + +### 5. 日志目录映射到宿主机 ✅ +- **应用日志**: `app_logs:/var/log` → `./storage/logs/app` +- **队列日志**: `queue_logs:/var/log` → `./storage/logs/queue` +- **Laravel日志**: `laravel_logs:/var/www/html/storage/logs` → `./storage/logs` + +## 创建的文件和脚本 + +### 配置文件 +- ✅ `docker-compose.yml` - 更新了完整的数据卷映射配置 +- ✅ `storage/*/` - 创建了所有必要的存储目录结构 + +### 管理脚本 +- ✅ `docker/init-storage.sh` - 存储目录初始化脚本 +- ✅ `docker/test-persistence.sh` - 数据持久化测试脚本 +- ✅ `docker/validate-storage-config.sh` - 完整配置验证脚本 + +### 文档 +- ✅ `docker/STORAGE_CONFIGURATION.md` - 详细的存储配置说明文档 +- ✅ `storage/*/.gitignore` - 数据目录的版本控制配置 + +## 存储目录结构 + +``` +storage/ +├── app/ # Laravel应用存储 (持久化) +│ ├── private/ +│ │ ├── documents/ # 上传文档存储 (持久化) +│ │ └── markdown/ # Markdown文件存储 +│ └── public/ # 公共文件存储 (持久化) +├── framework/ # Laravel框架缓存 +├── logs/ # 日志文件 (映射到宿主机) +│ ├── app/ # 应用容器日志 +│ ├── queue/ # 队列容器日志 +│ └── laravel.log # Laravel应用日志 +├── mysql/ # MySQL数据文件 (持久化) +├── redis/ # Redis数据文件 (持久化) +└── meilisearch/ # Meilisearch索引文件 (持久化) +``` + +## 验证结果 + +运行 `./docker/validate-storage-config.sh` 的验证结果: +- ✅ **54项检查全部通过** +- ✅ **0项失败** +- ✅ 所有存储目录结构正确 +- ✅ 所有Docker Compose卷映射配置正确 +- ✅ 所有服务容器卷映射正确 +- ✅ 所有数据卷绑定配置正确 +- ✅ 所有目录权限正确 +- ✅ Docker Compose配置文件语法正确 +- ✅ 所有目录写入权限正常 + +## 满足的需求 + +本实现完全满足以下需求: + +- **需求 3.1**: ✅ 项目代码目录映射到容器内部 +- **需求 3.2**: ✅ 上传文档存储目录持久化到宿主机 +- **需求 3.3**: ✅ 数据库数据目录持久化到宿主机 +- **需求 3.4**: ✅ 搜索引擎数据目录持久化到宿主机 +- **需求 3.5**: ✅ 日志目录映射到宿主机便于查看 + +## 使用方法 + +### 初始化存储目录 +```bash +./docker/init-storage.sh +``` + +### 验证配置 +```bash +./docker/validate-storage-config.sh +``` + +### 测试持久化 +```bash +./docker/test-persistence.sh +``` + +### 启动服务 +```bash +docker-compose up -d +``` + +## 技术特点 + +1. **完整性**: 覆盖了所有需要持久化的数据类型 +2. **可靠性**: 使用bind mount确保数据真正持久化 +3. **可维护性**: 提供了完整的管理和验证脚本 +4. **安全性**: 正确的目录权限设置 +5. **可扩展性**: 易于添加新的存储需求 + +## 下一步 + +数据持久化和目录映射配置已完成,可以继续执行下一个任务: +- **任务4**: 配置环境变量和网络设置 \ No newline at end of file diff --git a/docker/DEPLOYMENT_GUIDE.md b/docker/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..0259c39 --- /dev/null +++ b/docker/DEPLOYMENT_GUIDE.md @@ -0,0 +1,608 @@ +# Laravel知识库系统 - OpenEuler部署指南 + +## 概述 + +本指南详细说明如何在OpenEuler服务器上部署Laravel知识库系统。系统采用Docker容器化技术,支持完整的生产环境运行。 + +## 系统要求 + +### 硬件要求 + +- **CPU**: 2核心或以上 (推荐4核心) +- **内存**: 4GB或以上 (推荐8GB) +- **存储**: 20GB可用空间 (推荐50GB) +- **网络**: 稳定的网络连接 + +### 软件要求 + +- **操作系统**: OpenEuler 20.03 LTS或更高版本 +- **架构**: x86_64 (amd64) +- **Docker**: 20.10或更高版本 +- **Docker Compose**: 2.0或更高版本 + +## 部署架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ OpenEuler服务器 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Swoole │ │ Laravel │ │ Queue │ │ +│ │ (Web服务) │ │ (应用) │ │ (队列) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ MySQL │ │ Redis │ │ Meilisearch │ │ +│ │ (数据库) │ │ (缓存) │ │ (搜索) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ 持久化存储 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 数据库数据 │ │ 应用文件 │ │ 日志 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 部署步骤 + +### 1. 环境准备 + +#### 1.1 系统更新 + +```bash +# 更新系统包 +sudo dnf update -y + +# 安装必要工具 +sudo dnf install -y curl wget git unzip +``` + +#### 1.2 创建部署用户 + +```bash +# 创建部署用户 +sudo useradd -m -s /bin/bash deploy +sudo usermod -aG wheel deploy + +# 切换到部署用户 +sudo su - deploy +``` + +### 2. Docker安装 + +#### 2.1 自动安装 (推荐) + +使用提供的部署脚本自动安装Docker: + +```bash +# 下载部署脚本 +wget https://your-server.com/deploy-to-openeuler.sh +chmod +x deploy-to-openeuler.sh + +# 运行部署脚本 (会自动安装Docker) +sudo ./deploy-to-openeuler.sh /path/to/docker-images +``` + +#### 2.2 手动安装 + +```bash +# 添加Docker仓库 +sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + +# 安装Docker +sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# 启动Docker服务 +sudo systemctl start docker +sudo systemctl enable docker + +# 将用户添加到docker组 +sudo usermod -aG docker $USER + +# 重新登录以使组权限生效 +exit +sudo su - deploy + +# 验证安装 +docker --version +docker compose version +``` + +### 3. 镜像准备 + +#### 3.1 镜像导出 (在开发环境) + +在有网络连接的开发环境中导出镜像: + +```bash +# 使用导出脚本 +./docker/export-images.sh -c -v + +# 或手动导出 +docker save -o knowledge-base-app.tar knowledge-base-app:latest +docker save -o mysql.tar mysql:8.0 +docker save -o redis.tar redis:7-alpine +docker save -o meilisearch.tar getmeili/meilisearch:v1.5 + +# 压缩镜像文件 +gzip *.tar +``` + +#### 3.2 镜像传输 + +将镜像文件传输到OpenEuler服务器: + +```bash +# 使用scp传输 +scp docker-images/*.tar.gz deploy@openeuler-server:/tmp/ + +# 或使用rsync +rsync -avz docker-images/ deploy@openeuler-server:/tmp/docker-images/ +``` + +#### 3.3 镜像导入 + +在OpenEuler服务器上导入镜像: + +```bash +# 使用导入脚本 +./docker/import-and-verify.sh -f --test-run /tmp/docker-images + +# 或手动导入 +cd /tmp/docker-images +for file in *.tar.gz; do + gunzip -c "$file" | docker load +done + +# 验证导入的镜像 +docker images +``` + +### 4. 应用部署 + +#### 4.1 创建部署目录 + +```bash +# 创建部署目录 +sudo mkdir -p /opt/knowledge-base +sudo chown deploy:deploy /opt/knowledge-base +cd /opt/knowledge-base +``` + +#### 4.2 准备配置文件 + +创建docker-compose.yml文件: + +```bash +# 复制配置文件模板 +cp /path/to/source/docker-compose.yml . +cp /path/to/source/.env.production .env + +# 或下载配置文件 +wget https://your-server.com/docker-compose.yml +wget https://your-server.com/.env.production -O .env +``` + +#### 4.3 配置环境变量 + +编辑.env文件: + +```bash +nano .env +``` + +重要配置项: + +```env +# 应用配置 +APP_NAME="知识库系统" +APP_ENV=production +APP_KEY=base64:your-generated-key-here +APP_DEBUG=false +APP_URL=http://your-server-ip + +# 数据库配置 +DB_PASSWORD=your-secure-password + +# 搜索配置 +MEILISEARCH_KEY=your-master-key-here +``` + +#### 4.4 创建存储目录 + +```bash +# 创建持久化存储目录 +mkdir -p storage/{mysql,redis,meilisearch,app,logs} +mkdir -p storage/logs/{app,queue} + +# 设置权限 +sudo chown -R 1000:1000 storage/ +chmod -R 755 storage/ +``` + +### 5. 启动服务 + +#### 5.1 启动所有服务 + +```bash +# 启动服务 +docker compose up -d + +# 查看服务状态 +docker compose ps + +# 查看日志 +docker compose logs -f +``` + +#### 5.2 初始化应用 + +```bash +# 运行数据库迁移 +docker compose exec app php artisan migrate --force + +# 创建存储链接 +docker compose exec app php artisan storage:link + +# 清除缓存 +docker compose exec app php artisan cache:clear +docker compose exec app php artisan config:cache +docker compose exec app php artisan route:cache +docker compose exec app php artisan view:cache +``` + +### 6. 验证部署 + +#### 6.1 健康检查 + +```bash +# 检查所有容器状态 +docker compose ps + +# 检查健康状态 +docker compose exec app curl -f http://localhost/health + +# 检查数据库连接 +docker compose exec app php artisan tinker --execute="DB::connection()->getPdo();" +``` + +#### 6.2 功能测试 + +1. **Web访问测试** + ```bash + curl -I http://your-server-ip + ``` + +2. **数据库测试** + ```bash + docker compose exec mysql mysql -u root -p -e "SHOW DATABASES;" + ``` + +3. **搜索服务测试** + ```bash + curl http://your-server-ip:7700/health + ``` + +4. **队列测试** + ```bash + docker compose exec app php artisan queue:work --once + ``` + +## 运维管理 + +### 日常操作 + +#### 查看日志 + +```bash +# 查看所有服务日志 +docker compose logs -f + +# 查看特定服务日志 +docker compose logs -f app +docker compose logs -f mysql +docker compose logs -f redis +docker compose logs -f meilisearch +docker compose logs -f queue + +# 查看Laravel日志 +docker compose exec app tail -f storage/logs/laravel.log +``` + +#### 重启服务 + +```bash +# 重启所有服务 +docker compose restart + +# 重启特定服务 +docker compose restart app +docker compose restart mysql +``` + +#### 停止和启动 + +```bash +# 停止所有服务 +docker compose down + +# 启动所有服务 +docker compose up -d + +# 停止并删除所有容器和网络 +docker compose down --volumes --remove-orphans +``` + +### 备份和恢复 + +#### 数据备份 + +```bash +# 创建备份脚本 +cat > backup.sh << 'EOF' +#!/bin/bash +BACKUP_DIR="/opt/backups/knowledge-base" +DATE=$(date +%Y%m%d_%H%M%S) + +mkdir -p "$BACKUP_DIR" + +# 备份数据库 +docker compose exec -T mysql mysqldump -u root -p$DB_PASSWORD knowledge_base > "$BACKUP_DIR/database_$DATE.sql" + +# 备份应用文件 +tar -czf "$BACKUP_DIR/storage_$DATE.tar.gz" storage/ + +# 备份配置文件 +cp .env "$BACKUP_DIR/env_$DATE" +cp docker-compose.yml "$BACKUP_DIR/docker-compose_$DATE.yml" + +echo "备份完成: $BACKUP_DIR" +EOF + +chmod +x backup.sh +``` + +#### 数据恢复 + +```bash +# 恢复数据库 +docker compose exec -T mysql mysql -u root -p$DB_PASSWORD knowledge_base < /path/to/database_backup.sql + +# 恢复应用文件 +tar -xzf /path/to/storage_backup.tar.gz +``` + +### 更新和升级 + +#### 应用更新 + +```bash +# 拉取新镜像 +docker compose pull + +# 重新启动服务 +docker compose up -d + +# 运行迁移 +docker compose exec app php artisan migrate --force + +# 清除缓存 +docker compose exec app php artisan cache:clear +docker compose exec app php artisan config:cache +``` + +#### 系统更新 + +```bash +# 更新系统包 +sudo dnf update -y + +# 更新Docker +sudo dnf update docker-ce docker-ce-cli containerd.io + +# 重启Docker服务 +sudo systemctl restart docker +``` + +### 监控和告警 + +#### 系统监控 + +```bash +# 查看系统资源使用 +htop +df -h +free -h + +# 查看Docker资源使用 +docker stats + +# 查看容器资源使用 +docker compose exec app ps aux +``` + +#### 日志监控 + +```bash +# 监控错误日志 +tail -f storage/logs/laravel.log | grep ERROR + +# 监控访问日志 +docker compose logs -f app | grep "GET\|POST" +``` + +## 故障排除 + +### 常见问题 + +#### 1. 容器启动失败 + +**症状**: 容器无法启动或立即退出 + +**解决方案**: +```bash +# 查看容器日志 +docker compose logs container_name + +# 检查配置文件 +docker compose config + +# 检查端口占用 +netstat -tlnp | grep :8000 +``` + +#### 2. 数据库连接失败 + +**症状**: 应用无法连接到数据库 + +**解决方案**: +```bash +# 检查数据库容器状态 +docker compose ps mysql + +# 测试数据库连接 +docker compose exec mysql mysql -u root -p + +# 检查网络连接 +docker compose exec app ping mysql +``` + +#### 3. 权限问题 + +**症状**: 文件写入失败或权限错误 + +**解决方案**: +```bash +# 修复存储目录权限 +sudo chown -R 1000:1000 storage/ +chmod -R 775 storage/ + +# 检查SELinux状态 +getenforce +sudo setsebool -P container_manage_cgroup on +``` + +#### 4. 内存不足 + +**症状**: 容器被OOM Killer终止 + +**解决方案**: +```bash +# 检查内存使用 +free -h +docker stats + +# 调整容器内存限制 +# 编辑docker-compose.yml中的deploy.resources.limits.memory +``` + +#### 5. 磁盘空间不足 + +**症状**: 容器无法写入文件 + +**解决方案**: +```bash +# 检查磁盘使用 +df -h + +# 清理Docker资源 +docker system prune -a + +# 清理日志文件 +sudo journalctl --vacuum-time=7d +``` + +### 性能优化 + +#### 1. 数据库优化 + +```bash +# 调整MySQL配置 +# 编辑docker/mysql/my.cnf +[mysqld] +innodb_buffer_pool_size = 1G +innodb_log_file_size = 256M +max_connections = 200 +``` + +#### 2. Redis优化 + +```bash +# 调整Redis配置 +# 编辑docker/redis/redis.conf +maxmemory 512mb +maxmemory-policy allkeys-lru +``` + +#### 3. PHP优化 + +```bash +# 调整PHP配置 +# 编辑docker/php/php.ini +memory_limit = 512M +max_execution_time = 300 +upload_max_filesize = 100M +``` + +#### 4. Swoole优化 + +```bash +# 调整Swoole配置 +# 编辑.env文件 +OCTANE_WORKERS=4 +OCTANE_TASK_WORKERS=2 +OCTANE_MAX_REQUESTS=500 +``` + +## 安全配置 + +### 防火墙设置 + +```bash +# 配置防火墙 +sudo firewall-cmd --permanent --add-port=80/tcp +sudo firewall-cmd --permanent --add-port=443/tcp +sudo firewall-cmd --reload + +# 限制数据库端口访问 +sudo firewall-cmd --permanent --remove-port=3306/tcp +sudo firewall-cmd --permanent --remove-port=6379/tcp +sudo firewall-cmd --permanent --remove-port=7700/tcp +sudo firewall-cmd --reload +``` + +### SSL/TLS配置 + +```bash +# 安装Certbot +sudo dnf install -y certbot + +# 获取SSL证书 +sudo certbot certonly --standalone -d your-domain.com + +# 配置Nginx SSL +# 编辑docker/nginx/default.conf添加SSL配置 +``` + +### 访问控制 + +```bash +# 配置IP白名单 +# 在docker-compose.yml中添加网络限制 + +# 配置用户认证 +# 在应用中启用认证中间件 +``` + +## 联系支持 + +如果遇到问题,请: + +1. 查看日志文件获取详细错误信息 +2. 检查系统资源使用情况 +3. 参考故障排除章节 +4. 联系技术支持团队 + +--- + +**注意**: 本指南基于OpenEuler 20.03 LTS编写,其他版本可能需要适当调整。在生产环境部署前,请务必在测试环境中验证所有步骤。 \ No newline at end of file diff --git a/docker/ENVIRONMENT_SETUP.md b/docker/ENVIRONMENT_SETUP.md new file mode 100644 index 0000000..b25f0c1 --- /dev/null +++ b/docker/ENVIRONMENT_SETUP.md @@ -0,0 +1,288 @@ +# 环境配置设置指南 + +## 概述 + +本指南介绍如何配置和设置知识库系统的Docker部署环境,包括生产环境和开发环境的配置。 + +## 快速开始 + +### 1. 自动配置(推荐) + +使用自动配置脚本快速设置环境: + +```bash +# 生产环境配置 +./docker/setup-env.sh -e production + +# 开发环境配置 +./docker/setup-env.sh -e development + +# 交互式配置 +./docker/setup-env.sh -i +``` + +### 2. 手动配置 + +如果需要手动配置,请按照以下步骤: + +#### 生产环境 + +1. 复制环境模板: + ```bash + cp .env.production .env + ``` + +2. 编辑 `.env` 文件,修改以下关键配置: + ```bash + APP_KEY=base64:your-generated-key-here + DB_PASSWORD=your-secure-database-password + MEILISEARCH_KEY=your-meilisearch-master-key + APP_URL=http://your-domain.com + ``` + +3. 生成应用密钥: + ```bash + php artisan key:generate + ``` + +#### 开发环境 + +1. 复制开发环境模板: + ```bash + cp .env.development .env + ``` + +2. 编辑配置(开发环境可以使用默认值) + +## 配置验证 + +### 验证环境变量 + +```bash +# 验证当前环境配置 +./docker/validate-env.sh +``` + +### 验证Docker配置 + +```bash +# 验证生产环境配置 +docker-compose config + +# 验证开发环境配置 +docker-compose -f docker-compose.dev.yml config +``` + +## 启动服务 + +### 生产环境 + +```bash +# 启动所有服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f +``` + +### 开发环境 + +```bash +# 启动开发环境 +docker-compose -f docker-compose.dev.yml up -d + +# 查看服务状态 +docker-compose -f docker-compose.dev.yml ps + +# 查看日志 +docker-compose -f docker-compose.dev.yml logs -f +``` + +## 网络测试 + +启动服务后,测试网络连接: + +```bash +# 测试容器间网络连接 +./docker/test-network.sh +``` + +## 应用初始化 + +服务启动后,初始化Laravel应用: + +```bash +# 运行数据库迁移 +docker exec knowledge_base_app php artisan migrate + +# 运行数据库种子 +docker exec knowledge_base_app php artisan db:seed + +# 创建搜索索引 +docker exec knowledge_base_app php artisan scout:import "App\Models\Document" +``` + +## 环境配置详解 + +### 网络配置 + +- **生产环境网络**: `knowledge_base_network` (172.20.0.0/16) +- **开发环境网络**: `knowledge_base_dev_network` (172.21.0.0/16) + +### 端口映射 + +#### 生产环境 +- Web应用: 80 +- MySQL: 3306 +- Redis: 6379 +- Meilisearch: 7700 + +#### 开发环境 +- Web应用: 8080 +- MySQL: 3307 +- Redis: 6380 +- Meilisearch: 7701 +- PHP-FPM调试: 9000 + +### 存储卷 + +#### 生产环境 +- 数据库数据: `./storage/mysql` +- Redis数据: `./storage/redis` +- 搜索数据: `./storage/meilisearch` +- 应用存储: `./storage/app` +- 日志文件: `./storage/logs` + +#### 开发环境 +- 数据库数据: `./storage/dev/mysql` +- Redis数据: `./storage/dev/redis` +- 搜索数据: `./storage/dev/meilisearch` +- 应用存储: `./storage/dev/app` +- 日志文件: `./storage/dev/logs` + +## 环境变量说明 + +### 必需配置 + +| 变量名 | 说明 | 示例 | +|--------|------|------| +| `APP_KEY` | 应用加密密钥 | `base64:xxx...` | +| `DB_PASSWORD` | 数据库密码 | `secure_password` | +| `MEILISEARCH_KEY` | 搜索引擎密钥 | `master_key_xxx` | + +### 可选配置 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `APP_NAME` | 知识库系统 | 应用名称 | +| `APP_URL` | http://localhost | 应用URL | +| `DB_DATABASE` | knowledge_base | 数据库名 | +| `DB_USERNAME` | knowledge_user | 数据库用户 | + +## 故障排除 + +### 常见问题 + +1. **容器启动失败** + ```bash + # 查看容器日志 + docker-compose logs [service_name] + + # 检查容器状态 + docker-compose ps + ``` + +2. **网络连接问题** + ```bash + # 测试网络连接 + ./docker/test-network.sh + + # 检查网络配置 + docker network ls + docker network inspect knowledge_base_network + ``` + +3. **环境变量问题** + ```bash + # 验证环境变量 + ./docker/validate-env.sh + + # 查看容器环境变量 + docker exec knowledge_base_app env + ``` + +4. **权限问题** + ```bash + # 修复存储目录权限 + chmod -R 775 storage + chmod -R 775 bootstrap/cache + ``` + +### 重置环境 + +如果需要重置环境: + +```bash +# 停止所有服务 +docker-compose down + +# 删除数据卷(注意:这会删除所有数据) +docker-compose down -v + +# 重新配置环境 +./docker/setup-env.sh -f -e production + +# 重新启动服务 +docker-compose up -d +``` + +## 安全建议 + +### 生产环境 + +1. **更改默认密码**:确保所有默认密码都已更改 +2. **使用强密钥**:使用复杂的APP_KEY和MEILISEARCH_KEY +3. **限制网络访问**:配置防火墙规则 +4. **定期备份**:定期备份数据库和文件 +5. **监控日志**:监控应用和系统日志 + +### 开发环境 + +1. **隔离环境**:不要在生产环境使用开发配置 +2. **定期更新**:保持开发环境与生产环境同步 +3. **清理数据**:定期清理开发环境数据 + +## 维护操作 + +### 备份 + +```bash +# 备份数据库 +docker exec knowledge_base_mysql mysqldump -u root -p knowledge_base > backup.sql + +# 备份文件 +tar -czf storage_backup.tar.gz storage/ +``` + +### 更新 + +```bash +# 更新镜像 +docker-compose pull + +# 重启服务 +docker-compose up -d +``` + +### 监控 + +```bash +# 查看资源使用情况 +docker stats + +# 查看服务健康状态 +docker-compose ps +``` \ No newline at end of file diff --git a/docker/ENVIRONMENT_VARIABLES.md b/docker/ENVIRONMENT_VARIABLES.md new file mode 100644 index 0000000..d8c2f44 --- /dev/null +++ b/docker/ENVIRONMENT_VARIABLES.md @@ -0,0 +1,288 @@ +# 环境变量配置文档 + +## 概述 + +本文档详细说明了知识库系统Docker部署所需的环境变量配置,包括生产环境和开发环境的不同设置。 + +## 环境变量分类 + +### 应用基础配置 + +| 变量名 | 生产环境默认值 | 开发环境默认值 | 说明 | +|--------|----------------|----------------|------| +| `APP_NAME` | "知识库系统" | "知识库系统-开发" | 应用名称 | +| `APP_ENV` | production | local | 应用环境 | +| `APP_KEY` | 必须设置 | 必须设置 | 应用加密密钥 | +| `APP_DEBUG` | false | true | 调试模式 | +| `APP_URL` | http://your-domain.com | http://localhost:8000 | 应用URL | + +### 数据库配置 + +| 变量名 | 生产环境默认值 | 开发环境默认值 | 说明 | +|--------|----------------|----------------|------| +| `DB_CONNECTION` | mysql | mysql | 数据库类型 | +| `DB_HOST` | mysql | mysql | 数据库主机(容器名) | +| `DB_PORT` | 3306 | 3306 | 数据库端口 | +| `DB_DATABASE` | knowledge_base | knowledge_base_dev | 数据库名 | +| `DB_USERNAME` | knowledge_user | dev_user | 数据库用户名 | +| `DB_PASSWORD` | 必须设置 | dev_password | 数据库密码 | + +### Redis配置 + +| 变量名 | 生产环境默认值 | 开发环境默认值 | 说明 | +|--------|----------------|----------------|------| +| `REDIS_CLIENT` | phpredis | phpredis | Redis客户端 | +| `REDIS_HOST` | redis | redis | Redis主机(容器名) | +| `REDIS_PORT` | 6379 | 6379 | Redis端口 | +| `REDIS_PASSWORD` | 空 | 空 | Redis密码 | +| `CACHE_STORE` | redis | redis | 缓存驱动 | +| `SESSION_DRIVER` | redis | redis | 会话驱动 | +| `QUEUE_CONNECTION` | redis | redis | 队列驱动 | + +### Meilisearch配置 + +| 变量名 | 生产环境默认值 | 开发环境默认值 | 说明 | +|--------|----------------|----------------|------| +| `SCOUT_DRIVER` | meilisearch | meilisearch | 搜索驱动 | +| `MEILISEARCH_HOST` | http://meilisearch:7700 | http://meilisearch:7700 | 搜索引擎地址 | +| `MEILISEARCH_KEY` | 必须设置 | dev-master-key | 搜索引擎密钥 | + +### 日志配置 + +| 变量名 | 生产环境默认值 | 开发环境默认值 | 说明 | +|--------|----------------|----------------|------| +| `LOG_CHANNEL` | stack | stack | 日志通道 | +| `LOG_LEVEL` | info | debug | 日志级别 | + +### 邮件配置 + +| 变量名 | 生产环境默认值 | 开发环境默认值 | 说明 | +|--------|----------------|----------------|------| +| `MAIL_MAILER` | smtp | log | 邮件驱动 | +| `MAIL_HOST` | 必须设置 | - | SMTP主机 | +| `MAIL_PORT` | 587 | - | SMTP端口 | +| `MAIL_USERNAME` | 必须设置 | - | SMTP用户名 | +| `MAIL_PASSWORD` | 必须设置 | - | SMTP密码 | +| `MAIL_ENCRYPTION` | tls | - | 加密方式 | + +## 环境文件配置 + +### 生产环境 (.env.production) + +```bash +# 应用配置 +APP_NAME="知识库系统" +APP_ENV=production +APP_KEY=base64:your-app-key-here-change-this-in-production +APP_DEBUG=false +APP_URL=http://your-domain.com + +# 数据库配置 +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=knowledge_base +DB_USERNAME=knowledge_user +DB_PASSWORD=secure_password_change_this_in_production + +# Redis配置 +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# Meilisearch配置 +SCOUT_DRIVER=meilisearch +MEILISEARCH_HOST=http://meilisearch:7700 +MEILISEARCH_KEY=your-master-key-change-this-in-production + +# 邮件配置 +MAIL_MAILER=smtp +MAIL_HOST=your-smtp-host.com +MAIL_PORT=587 +MAIL_USERNAME=your-email@domain.com +MAIL_PASSWORD=your-email-password-change-this +MAIL_ENCRYPTION=tls +MAIL_FROM_ADDRESS="noreply@your-domain.com" +MAIL_FROM_NAME="${APP_NAME}" +``` + +### 开发环境 (.env.development) + +```bash +# 应用配置 +APP_NAME="知识库系统-开发" +APP_ENV=local +APP_KEY=base64:your-dev-app-key-here +APP_DEBUG=true +APP_URL=http://localhost:8000 + +# 数据库配置 +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=knowledge_base_dev +DB_USERNAME=dev_user +DB_PASSWORD=dev_password + +# Redis配置 +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# Meilisearch配置 +SCOUT_DRIVER=meilisearch +MEILISEARCH_HOST=http://meilisearch:7700 +MEILISEARCH_KEY=dev-master-key + +# 邮件配置(开发环境使用日志) +MAIL_MAILER=log +MAIL_FROM_ADDRESS="dev@knowledge-base.local" +MAIL_FROM_NAME="${APP_NAME}" + +# 开发工具配置 +TELESCOPE_ENABLED=true +DEBUGBAR_ENABLED=true +XDEBUG_MODE=develop,debug +XDEBUG_CONFIG=client_host=host.docker.internal client_port=9003 +``` + +## Docker Compose环境变量 + +### 生产环境 (docker-compose.yml) + +```yaml +environment: + # 从.env文件读取或使用默认值 + APP_NAME: ${APP_NAME:-知识库系统} + APP_ENV: ${APP_ENV:-production} + APP_KEY: ${APP_KEY} + APP_DEBUG: ${APP_DEBUG:-false} + APP_URL: ${APP_URL:-http://localhost} + + # 数据库配置 + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-knowledge_base} + DB_USERNAME: ${DB_USERNAME:-knowledge_user} + DB_PASSWORD: ${DB_PASSWORD:-secure_password} +``` + +### 开发环境 (docker-compose.dev.yml) + +```yaml +environment: + # 开发环境特定配置 + APP_NAME: ${APP_NAME:-知识库系统-开发} + APP_ENV: ${APP_ENV:-local} + APP_DEBUG: ${APP_DEBUG:-true} + LOG_LEVEL: debug + + # 开发工具配置 + TELESCOPE_ENABLED: true + DEBUGBAR_ENABLED: true +``` + +## 安全注意事项 + +### 必须更改的默认值 + +生产环境部署前必须更改以下默认值: + +1. **APP_KEY**: 使用 `php artisan key:generate` 生成 +2. **DB_PASSWORD**: 设置强密码 +3. **MEILISEARCH_KEY**: 设置复杂的主密钥 +4. **MAIL_PASSWORD**: 设置邮件服务密码 + +### 敏感信息保护 + +1. **不要将敏感信息提交到版本控制** +2. **使用Docker secrets管理敏感数据** +3. **定期轮换密钥和密码** +4. **限制环境变量的访问权限** + +## 环境变量验证 + +### 启动前检查 + +创建验证脚本检查必要的环境变量: + +```bash +#!/bin/bash +# validate-env.sh + +required_vars=( + "APP_KEY" + "DB_PASSWORD" + "MEILISEARCH_KEY" +) + +for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + echo "错误: 环境变量 $var 未设置" + exit 1 + fi +done + +echo "环境变量验证通过" +``` + +### 运行时检查 + +Laravel应用启动时会自动验证关键配置: + +- 数据库连接 +- Redis连接 +- Meilisearch连接 +- 应用密钥格式 + +## 故障排除 + +### 常见问题 + +1. **APP_KEY未设置**: + ```bash + php artisan key:generate + ``` + +2. **数据库连接失败**: + - 检查DB_HOST是否为容器名 + - 验证数据库密码 + - 确认MySQL容器已启动 + +3. **Redis连接失败**: + - 检查REDIS_HOST是否为容器名 + - 验证Redis容器状态 + +4. **Meilisearch连接失败**: + - 检查MEILISEARCH_HOST格式 + - 验证MEILISEARCH_KEY + +### 调试命令 + +```bash +# 查看容器环境变量 +docker exec knowledge_base_app env + +# 测试数据库连接 +docker exec knowledge_base_app php artisan tinker +>>> DB::connection()->getPdo(); + +# 测试Redis连接 +docker exec knowledge_base_app php artisan tinker +>>> Redis::ping(); + +# 测试Meilisearch连接 +docker exec knowledge_base_app php artisan scout:status +``` + +## 最佳实践 + +1. **使用环境特定的配置文件** +2. **为不同环境设置不同的密钥** +3. **定期备份环境配置** +4. **使用配置管理工具** +5. **监控配置变更** +6. **文档化所有环境变量** \ No newline at end of file diff --git a/docker/HEALTH_CHECK_IMPLEMENTATION.md b/docker/HEALTH_CHECK_IMPLEMENTATION.md new file mode 100644 index 0000000..4fd2268 --- /dev/null +++ b/docker/HEALTH_CHECK_IMPLEMENTATION.md @@ -0,0 +1,309 @@ +# 健康检查和自动重启机制实现总结 + +## 概述 + +本文档总结了Laravel知识库系统Docker部署中健康检查和自动重启机制的完整实现。 + +## 实现的功能 + +### 1. Web应用HTTP健康检查 ✅ + +**实现位置**: `routes/web.php` +**端点**: `GET /health` +**检查项目**: +- 数据库连接状态 +- Redis缓存连接状态 +- Meilisearch搜索引擎连接状态 +- 存储目录可写性 + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s +``` + +**响应格式**: +```json +{ + "status": "ok|degraded", + "timestamp": "2024-12-24T10:30:00.000000Z", + "services": { + "database": "connected|disconnected", + "redis": "connected|disconnected|not_configured", + "meilisearch": "connected|disconnected|not_configured", + "storage": "writable|not_writable" + }, + "version": "1.0.0" +} +``` + +### 2. 数据库连接健康检查 ✅ + +**实现方式**: MySQL内置的 `mysqladmin ping` 命令 +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD}"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s +``` + +### 3. Redis连接健康检查 ✅ + +**实现方式**: Redis内置的 `redis-cli ping` 命令 +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 10s +``` + +### 4. Meilisearch API健康检查 ✅ + +**实现方式**: 调用Meilisearch的 `/health` API端点 +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7700/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s +``` + +### 5. 队列处理器健康检查 ✅ + +**实现位置**: `docker/queue-health-check.sh` +**检查内容**: +- 队列进程是否运行 (`pgrep -f "queue:work"`) +- Laravel应用数据库连接 +- Laravel应用Redis连接 + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "/usr/local/bin/queue-health-check.sh"] + interval: 60s + timeout: 30s + retries: 3 + start_period: 30s +``` + +### 6. 容器自动重启策略 ✅ + +**配置**: 所有服务都使用 `restart: unless-stopped` 策略 +**行为**: +- 容器异常退出时自动重启 +- 手动停止的容器不会自动重启 +- 系统重启后自动启动容器(除非手动停止) + +**应用的服务**: +- MySQL数据库 +- Redis缓存 +- Meilisearch搜索引擎 +- Laravel应用容器 +- 队列处理容器 + +## 支持脚本 + +### 1. 服务状态检查脚本 ✅ + +**文件**: `docker/check-services.sh` +**功能**: +- 检查Docker服务状态 +- 检查所有容器的健康状态 +- 测试服务连接 +- 验证数据持久化配置 +- 生成详细的健康报告 + +### 2. 持续监控脚本 ✅ + +**文件**: `docker/monitor-services.sh` +**功能**: +- 持续监控所有服务健康状态 +- 自动重启不健康的容器 +- 限制重启次数防止无限重启 +- 记录详细监控日志 +- 支持告警通知扩展 + +**配置参数**: +- `MONITOR_INTERVAL`: 监控间隔(默认60秒) +- `MAX_RESTART_ATTEMPTS`: 最大重启尝试次数(默认3次) +- `RESTART_COOLDOWN`: 重启冷却时间(默认300秒) +- `LOG_FILE`: 日志文件路径 + +### 3. 启动和监控脚本 ✅ + +**文件**: `docker/start-with-monitoring.sh` +**功能**: +- 完整的服务启动流程 +- 环境检查和目录创建 +- 服务就绪等待 +- 自动启动监控进程 +- 支持多种启动选项 + +**选项**: +- `--no-monitor`: 不启动监控进程 +- `--skip-build`: 跳过镜像构建 +- `--skip-wait`: 跳过服务就绪等待 + +### 4. 停止监控脚本 ✅ + +**文件**: `docker/stop-monitoring.sh` +**功能**: +- 停止监控进程 +- 可选择性停止Docker服务 +- 清理监控日志文件 +- 支持多种停止选项 + +**选项**: +- `--stop-services`: 同时停止Docker服务 +- `--cleanup-logs`: 清理监控日志文件 +- `--all`: 停止监控、服务并清理日志 + +### 5. 健康检查测试脚本 ✅ + +**文件**: `docker/test-health-checks.sh` +**功能**: +- 验证所有脚本语法正确性 +- 检查脚本执行权限 +- 验证Docker配置文件 +- 测试健康检查配置 +- 检查存储目录结构 + +## 配置文件 + +### 1. Swoole配置 ✅ + +**文件**: `docker/supervisor/supervisord.conf` +**健康检查相关**: +- Swoole HTTP服务器健康检查端点 +- 自动重启配置 +- 日志管理 + +### 2. PHP配置 ✅ + +**文件**: `docker/php/php.ini` +**健康检查相关**: +- 适当的超时设置 +- 内存限制配置 + +### 3. Redis配置 ✅ + +**文件**: `docker/redis/redis.conf` +**健康检查相关**: +- 网络绑定配置 +- 内存和持久化设置 +- 性能优化配置 + +### 4. MySQL配置 ✅ + +**文件**: `docker/mysql/my.cnf` +**健康检查相关**: +- 字符集和时区配置 +- 性能优化设置 +- 日志配置 + +### 5. Supervisor配置 ✅ + +**文件**: `docker/supervisor/supervisord.conf` +**健康检查相关**: +- Swoole和队列进程管理 +- 自动重启配置 +- 日志管理 + +## 使用方法 + +### 启动服务和监控 + +```bash +# 完整启动(包含监控) +./docker/start-with-monitoring.sh + +# 启动服务但不启动监控 +./docker/start-with-monitoring.sh --no-monitor + +# 跳过镜像构建 +./docker/start-with-monitoring.sh --skip-build +``` + +### 检查服务状态 + +```bash +# 运行完整的健康检查 +./docker/check-services.sh + +# 查看容器状态 +docker-compose ps + +# 查看容器健康状态 +docker inspect --format='{{.State.Health.Status}}' knowledge_base_app +``` + +### 监控管理 + +```bash +# 查看监控日志 +tail -f ./storage/logs/monitor.log + +# 停止监控进程 +./docker/stop-monitoring.sh + +# 停止监控和服务 +./docker/stop-monitoring.sh --stop-services +``` + +### 测试健康检查功能 + +```bash +# 运行健康检查功能测试 +./docker/test-health-checks.sh +``` + +## 验证结果 + +通过运行 `./docker/test-health-checks.sh`,所有测试项目都通过: + +- ✅ 脚本语法测试 +- ✅ 脚本权限测试 +- ✅ Docker配置测试 +- ✅ 健康检查配置测试 +- ✅ 存储目录测试 + +## 监控指标 + +### 健康检查状态 + +- `healthy`: 服务正常运行 +- `unhealthy`: 服务健康检查失败 +- `starting`: 服务正在启动 +- `no-healthcheck`: 服务未配置健康检查 + +### 重启计数器 + +监控系统维护每个容器的重启计数器: +- 位置: `./storage/logs/restart_counters/` +- 格式: `{container_name}.count` +- 重置: 容器健康时自动重置 + +## 总结 + +健康检查和自动重启机制已完全实现,包括: + +1. **Web应用HTTP健康检查** - 完整的Laravel健康检查端点 +2. **数据库连接健康检查** - MySQL ping检查 +3. **Redis连接健康检查** - Redis ping检查 +4. **Meilisearch API健康检查** - 搜索引擎健康API +5. **容器自动重启策略** - unless-stopped策略 +6. **完整的监控和管理脚本** - 自动化运维工具 + +所有功能都经过测试验证,可以确保系统的高可用性和自动故障恢复能力。 \ No newline at end of file diff --git a/docker/HEALTH_MONITORING.md b/docker/HEALTH_MONITORING.md new file mode 100644 index 0000000..6fdeb5c --- /dev/null +++ b/docker/HEALTH_MONITORING.md @@ -0,0 +1,364 @@ +# Docker健康检查和监控指南 + +本文档描述了Laravel知识库系统的Docker健康检查和自动重启机制的配置和使用方法。 + +## 概述 + +系统实现了完整的健康检查和自动重启机制,包括: + +- **Web应用HTTP健康检查** - 检查应用程序和依赖服务状态 +- **数据库连接健康检查** - 验证MySQL数据库连接 +- **Redis连接健康检查** - 验证Redis缓存服务连接 +- **Meilisearch API健康检查** - 验证搜索引擎服务状态 +- **容器自动重启策略** - 在服务失败时自动恢复 +- **持续监控系统** - 主动监控和故障处理 + +## 健康检查配置 + +### 1. Web应用健康检查 + +**端点**: `GET /health` + +**检查项目**: +- 数据库连接状态 +- Redis缓存连接状态 +- Meilisearch搜索引擎连接状态 +- 存储目录可写性 + +**响应格式**: +```json +{ + "status": "ok|degraded", + "timestamp": "2024-12-24T10:30:00.000000Z", + "services": { + "database": "connected|disconnected", + "redis": "connected|disconnected|not_configured", + "meilisearch": "connected|disconnected|not_configured", + "storage": "writable|not_writable" + }, + "version": "1.0.0" +} +``` + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s +``` + +### 2. MySQL数据库健康检查 + +**检查方法**: `mysqladmin ping` + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD}"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s +``` + +### 3. Redis缓存健康检查 + +**检查方法**: `redis-cli ping` + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 10s +``` + +### 4. Meilisearch搜索引擎健康检查 + +**检查方法**: `curl -f http://localhost:7700/health` + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7700/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s +``` + +### 5. 队列处理器健康检查 + +**检查方法**: 自定义脚本检查队列进程和依赖服务 + +**脚本位置**: `/usr/local/bin/queue-health-check.sh` + +**Docker配置**: +```yaml +healthcheck: + test: ["CMD", "/usr/local/bin/queue-health-check.sh"] + interval: 60s + timeout: 30s + retries: 3 + start_period: 30s +``` + +## 自动重启策略 + +所有服务都配置了 `restart: unless-stopped` 策略: + +- **自动重启**: 容器异常退出时自动重启 +- **手动停止**: 手动停止的容器不会自动重启 +- **系统重启**: 系统重启后自动启动容器(除非手动停止) + +## 监控系统 + +### 持续监控脚本 + +**脚本**: `docker/monitor-services.sh` + +**功能**: +- 持续监控所有服务的健康状态 +- 自动重启不健康的容器 +- 限制重启次数防止无限重启 +- 记录详细的监控日志 +- 发送告警通知 + +**配置参数**: +- `MONITOR_INTERVAL`: 监控间隔(默认60秒) +- `MAX_RESTART_ATTEMPTS`: 最大重启尝试次数(默认3次) +- `RESTART_COOLDOWN`: 重启冷却时间(默认300秒) +- `LOG_FILE`: 日志文件路径 + +### 服务状态检查脚本 + +**脚本**: `docker/check-services.sh` + +**功能**: +- 一次性检查所有服务状态 +- 详细的健康状态报告 +- 连接测试和故障诊断 + +## 使用方法 + +### 1. 启动服务和监控 + +```bash +# 完整启动(包含监控) +./docker/start-with-monitoring.sh + +# 启动服务但不启动监控 +./docker/start-with-monitoring.sh --no-monitor + +# 跳过镜像构建 +./docker/start-with-monitoring.sh --skip-build + +# 跳过服务就绪等待 +./docker/start-with-monitoring.sh --skip-wait +``` + +### 2. 检查服务状态 + +```bash +# 运行完整的健康检查 +./docker/check-services.sh + +# 查看容器状态 +docker-compose ps + +# 查看容器健康状态 +docker inspect --format='{{.State.Health.Status}}' knowledge_base_app +``` + +### 3. 查看监控日志 + +```bash +# 查看监控日志 +tail -f ./storage/logs/monitor.log + +# 查看监控输出 +tail -f ./storage/logs/monitor-output.log + +# 查看容器日志 +docker-compose logs -f app +docker-compose logs -f queue +``` + +### 4. 停止监控 + +```bash +# 只停止监控进程 +./docker/stop-monitoring.sh + +# 停止监控和服务 +./docker/stop-monitoring.sh --stop-services + +# 停止监控、服务并清理日志 +./docker/stop-monitoring.sh --all +``` + +### 5. 手动重启服务 + +```bash +# 重启单个服务 +docker-compose restart app + +# 重启所有服务 +docker-compose restart + +# 重新构建并启动 +docker-compose up -d --build +``` + +## 监控指标 + +### 健康检查状态 + +- `healthy`: 服务正常运行 +- `unhealthy`: 服务健康检查失败 +- `starting`: 服务正在启动 +- `no-healthcheck`: 服务未配置健康检查 + +### 监控日志格式 + +``` +2024-12-24 10:30:00 [INFO] 开始监控检查 (共5个服务) +2024-12-24 10:30:01 [SUCCESS] MySQL数据库容器健康状态正常 +2024-12-24 10:30:02 [SUCCESS] Redis缓存容器健康状态正常 +2024-12-24 10:30:03 [SUCCESS] Meilisearch搜索容器健康状态正常 +2024-12-24 10:30:04 [SUCCESS] Web应用容器健康状态正常 +2024-12-24 10:30:05 [SUCCESS] 队列处理器容器健康状态正常 +2024-12-24 10:30:06 [SUCCESS] 所有服务运行正常 +``` + +### 重启计数器 + +监控系统维护每个容器的重启计数器: + +- 位置: `./storage/logs/restart_counters/` +- 格式: `{container_name}.count` +- 重置: 容器健康时自动重置 + +## 故障排除 + +### 常见问题 + +1. **健康检查失败** + ```bash + # 检查容器日志 + docker-compose logs app + + # 手动测试健康检查端点 + curl -v http://localhost/health + ``` + +2. **监控进程无法启动** + ```bash + # 检查权限 + ls -la docker/monitor-services.sh + + # 手动运行监控脚本 + ./docker/monitor-services.sh + ``` + +3. **容器重启循环** + ```bash + # 查看重启计数器 + cat ./storage/logs/restart_counters/knowledge_base_app.count + + # 重置重启计数器 + echo "0" > ./storage/logs/restart_counters/knowledge_base_app.count + ``` + +4. **存储权限问题** + ```bash + # 修复存储目录权限 + sudo chown -R $(id -u):$(id -g) ./storage + chmod -R 755 ./storage + ``` + +### 调试模式 + +启用详细日志记录: + +```bash +# 设置环境变量 +export LOG_LEVEL=debug + +# 运行监控脚本 +./docker/monitor-services.sh --interval 30 +``` + +## 生产环境建议 + +1. **监控配置** + - 设置适当的监控间隔(建议60-120秒) + - 配置告警通知(邮件、Slack等) + - 定期检查监控日志 + +2. **资源限制** + - 为容器设置内存和CPU限制 + - 监控系统资源使用情况 + - 配置日志轮转 + +3. **备份策略** + - 定期备份数据库和搜索索引 + - 备份应用配置和上传文件 + - 测试恢复流程 + +4. **安全考虑** + - 限制健康检查端点的访问 + - 使用强密码和密钥 + - 定期更新容器镜像 + +## 扩展功能 + +### 自定义告警 + +在 `monitor-services.sh` 中的 `send_alert` 函数中添加自定义告警逻辑: + +```bash +send_alert() { + local message=$1 + local severity=$2 + + # 发送邮件告警 + echo "$message" | mail -s "Docker监控告警" admin@example.com + + # 发送到Slack + curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"$message\"}" \ + "$SLACK_WEBHOOK_URL" +} +``` + +### 集成外部监控 + +可以将健康检查数据发送到外部监控系统: + +- Prometheus + Grafana +- Zabbix +- Nagios +- DataDog + +### 自动扩缩容 + +基于健康检查结果实现自动扩缩容: + +```bash +# 检查负载并调整副本数 +if [ $cpu_usage -gt 80 ]; then + docker-compose up -d --scale app=3 +fi +``` + +## 总结 + +本系统提供了完整的健康检查和自动重启机制,确保服务的高可用性。通过合理配置和使用这些工具,可以大大提高系统的稳定性和可靠性。 + +定期检查监控日志,及时处理告警,并根据实际情况调整配置参数,是维护系统健康运行的关键。 \ No newline at end of file diff --git a/docker/NETWORK_CONFIGURATION.md b/docker/NETWORK_CONFIGURATION.md new file mode 100644 index 0000000..4f0d74c --- /dev/null +++ b/docker/NETWORK_CONFIGURATION.md @@ -0,0 +1,179 @@ +# Docker网络配置文档 + +## 概述 + +本文档描述了知识库系统Docker部署的网络配置,包括生产环境和开发环境的网络设置。 + +## 网络架构 + +### 生产环境网络 + +- **网络名称**: `knowledge_base_network` +- **网络类型**: bridge +- **子网**: 172.20.0.0/16 +- **网关**: 172.20.0.1 + +#### 服务端口映射 + +| 服务 | 容器名称 | 内部端口 | 外部端口 | 协议 | +|------|----------|----------|----------|------| +| Web应用 | knowledge_base_app | 80 | 80 | HTTP | +| MySQL | knowledge_base_mysql | 3306 | 3306 | MySQL | +| Redis | knowledge_base_redis | 6379 | 6379 | Redis | +| Meilisearch | knowledge_base_meilisearch | 7700 | 7700 | HTTP | + +### 开发环境网络 + +- **网络名称**: `knowledge_base_dev_network` +- **网络类型**: bridge +- **子网**: 172.21.0.0/16 +- **网关**: 172.21.0.1 + +#### 开发环境端口映射 + +| 服务 | 容器名称 | 内部端口 | 外部端口 | 协议 | 说明 | +|------|----------|----------|----------|------|------| +| Web应用 | knowledge_base_app_dev | 80 | 8080 | HTTP | 避免与生产环境冲突 | +| PHP-FPM调试 | knowledge_base_app_dev | 9000 | 9000 | TCP | Xdebug调试端口 | +| MySQL | knowledge_base_mysql_dev | 3306 | 3307 | MySQL | 避免与本地MySQL冲突 | +| Redis | knowledge_base_redis_dev | 6379 | 6380 | Redis | 避免与本地Redis冲突 | +| Meilisearch | knowledge_base_meilisearch_dev | 7700 | 7701 | HTTP | 避免与生产环境冲突 | + +## 服务间通信 + +### 内部服务发现 + +所有容器通过Docker内部DNS进行服务发现,使用容器名称作为主机名: + +- **数据库连接**: `mysql:3306` +- **Redis连接**: `redis:6379` +- **Meilisearch连接**: `http://meilisearch:7700` + +### 环境变量配置 + +#### 生产环境 + +```bash +# 数据库连接 +DB_HOST=mysql +DB_PORT=3306 + +# Redis连接 +REDIS_HOST=redis +REDIS_PORT=6379 + +# Meilisearch连接 +MEILISEARCH_HOST=http://meilisearch:7700 +``` + +#### 开发环境 + +```bash +# 数据库连接 +DB_HOST=mysql +DB_PORT=3306 + +# Redis连接 +REDIS_HOST=redis +REDIS_PORT=6379 + +# Meilisearch连接 +MEILISEARCH_HOST=http://meilisearch:7700 +``` + +注意:开发环境内部端口保持一致,只有外部映射端口不同。 + +## 网络安全 + +### 防火墙规则 + +生产环境建议配置防火墙规则: + +```bash +# 只允许必要的端口访问 +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS (如果使用SSL) +sudo ufw deny 3306/tcp # 禁止外部直接访问MySQL +sudo ufw deny 6379/tcp # 禁止外部直接访问Redis +sudo ufw deny 7700/tcp # 禁止外部直接访问Meilisearch +``` + +### 容器间通信安全 + +- 所有服务运行在隔离的Docker网络中 +- 数据库、缓存和搜索服务不直接暴露给外部网络 +- 只有Web应用容器暴露HTTP端口 + +## 故障排除 + +### 网络连接问题 + +1. **检查容器网络状态**: + ```bash + docker network ls + docker network inspect knowledge_base_network + ``` + +2. **测试容器间连通性**: + ```bash + docker exec knowledge_base_app ping mysql + docker exec knowledge_base_app ping redis + docker exec knowledge_base_app ping meilisearch + ``` + +3. **检查端口监听状态**: + ```bash + docker exec knowledge_base_mysql netstat -tlnp + docker exec knowledge_base_redis netstat -tlnp + ``` + +### 常见问题 + +1. **端口冲突**: 确保外部端口没有被其他服务占用 +2. **DNS解析失败**: 检查容器是否在同一网络中 +3. **防火墙阻断**: 检查宿主机防火墙设置 + +## 监控和日志 + +### 网络监控 + +```bash +# 查看网络流量 +docker exec knowledge_base_app ss -tuln + +# 监控连接状态 +docker exec knowledge_base_app netstat -an | grep ESTABLISHED +``` + +### 连接日志 + +应用连接日志位置: +- Laravel日志: `/var/www/html/storage/logs/laravel.log` +- Swoole访问日志: `/var/log/supervisor/swoole_stdout.log` +- Swoole错误日志: `/var/log/supervisor/swoole_stderr.log` + +## 性能优化 + +### 网络性能调优 + +1. **启用HTTP/2** (如果使用HTTPS) +2. **配置连接池**: + - MySQL连接池大小 + - Redis连接池配置 +3. **启用压缩**: + - Swoole内置压缩支持 + - 静态资源压缩 + +### 资源限制 + +```yaml +# 在docker-compose.yml中配置资源限制 +deploy: + resources: + limits: + memory: 1G + cpus: '0.5' + reservations: + memory: 512M + cpus: '0.25' +``` \ No newline at end of file diff --git a/docker/PACKAGING_README.md b/docker/PACKAGING_README.md new file mode 100644 index 0000000..6af32ec --- /dev/null +++ b/docker/PACKAGING_README.md @@ -0,0 +1,232 @@ +# Docker镜像打包和部署工具 + +本目录包含用于Docker镜像打包和OpenEuler服务器部署的完整工具集。 + +## 脚本概览 + +### 核心脚本 + +1. **export-images.sh** - Docker镜像导出脚本 +2. **compress-and-verify.sh** - 镜像压缩和完整性检查脚本 +3. **import-and-verify.sh** - 镜像导入和验证脚本 +4. **deploy-to-openeuler.sh** - OpenEuler服务器部署脚本 +5. **one-click-deploy.sh** - 一键部署脚本 + +### 文档 + +- **DEPLOYMENT_GUIDE.md** - 详细部署指南 +- **PACKAGING_README.md** - 本文件 + +## 快速开始 + +### 1. 导出镜像 + +```bash +# 基本导出 +./docker/export-images.sh + +# 导出并压缩,验证完整性 +./docker/export-images.sh -c -v + +# 导出到指定目录 +./docker/export-images.sh -o /path/to/export -c -v +``` + +### 2. 压缩和验证 + +```bash +# 压缩现有镜像文件 +./docker/compress-and-verify.sh + +# 仅验证文件完整性 +./docker/compress-and-verify.sh --verify-only + +# 解压缩文件 +./docker/compress-and-verify.sh --uncompress +``` + +### 3. 导入镜像 + +```bash +# 导入镜像文件 +./docker/import-and-verify.sh /path/to/images + +# 强制导入并测试 +./docker/import-and-verify.sh -f --test-run + +# 仅验证不导入 +./docker/import-and-verify.sh --verify-only +``` + +### 4. 部署到OpenEuler + +```bash +# 全新部署 +sudo ./docker/deploy-to-openeuler.sh /path/to/images + +# 更新现有部署 +sudo ./docker/deploy-to-openeuler.sh -u /path/to/images + +# 备份并部署 +sudo ./docker/deploy-to-openeuler.sh -b /path/to/images +``` + +### 5. 一键部署 + +```bash +# 导出镜像 +./docker/one-click-deploy.sh export -c -v + +# 部署到服务器 +./docker/one-click-deploy.sh deploy --server 192.168.1.100 + +# 完整流程 +./docker/one-click-deploy.sh full -c --server 192.168.1.100 +``` + +## 典型工作流程 + +### 开发环境 → 生产环境 + +1. **在开发环境导出镜像** + ```bash + ./docker/export-images.sh -c -v -o ./docker-images + ``` + +2. **传输到生产服务器** + ```bash + scp -r docker-images/ user@server:/tmp/ + ``` + +3. **在生产服务器部署** + ```bash + sudo ./docker/deploy-to-openeuler.sh /tmp/docker-images + ``` + +### 离线部署流程 + +1. **准备镜像包** + ```bash + ./docker/export-images.sh -c -v + ./docker/compress-and-verify.sh -c 9 + ``` + +2. **物理传输到目标环境** + +3. **导入和部署** + ```bash + ./docker/import-and-verify.sh -f --test-run + sudo ./docker/deploy-to-openeuler.sh --skip-images + ``` + +## 脚本选项说明 + +### export-images.sh 选项 + +- `-c, --compress`: 启用gzip压缩 +- `-v, --verify`: 导出后验证完整性 +- `-o, --output DIR`: 指定导出目录 +- `--custom-images`: 导出指定镜像列表 +- `--skip-build`: 跳过镜像构建 + +### compress-and-verify.sh 选项 + +- `-c, --compress-level N`: 压缩级别 (1-9) +- `-k, --keep-original`: 保留原始文件 +- `-v, --verify-only`: 仅验证不压缩 +- `-u, --uncompress`: 解压缩文件 +- `--parallel N`: 并行处理数量 + +### import-and-verify.sh 选项 + +- `-v, --verify-only`: 仅验证不导入 +- `-f, --force`: 强制导入覆盖现有镜像 +- `-c, --check-manifest`: 检查清单文件 +- `--skip-compatibility`: 跳过兼容性检查 +- `--test-run`: 导入后运行测试 + +### deploy-to-openeuler.sh 选项 + +- `-d, --deploy-dir DIR`: 部署目录 +- `-b, --backup`: 部署前备份 +- `-u, --update`: 更新现有部署 +- `-r, --rollback`: 回滚到上一版本 +- `--skip-docker-install`: 跳过Docker安装 +- `--dry-run`: 仅显示操作不执行 + +## 生成的文件 + +### 导出过程生成 + +- `docker-images/` - 镜像文件目录 +- `images-manifest.txt` - 镜像清单文件 +- `import-images.sh` - 自动生成的导入脚本 +- `export.log` - 导出日志 + +### 部署过程生成 + +- `/opt/knowledge-base/` - 默认部署目录 +- `.env` - 环境配置文件 +- `storage/` - 持久化存储目录 +- `/var/log/knowledge-base-deploy.log` - 部署日志 + +## 故障排除 + +### 常见问题 + +1. **权限错误** + ```bash + sudo chown -R $USER:$USER docker-images/ + chmod +x docker/*.sh + ``` + +2. **Docker未运行** + ```bash + sudo systemctl start docker + sudo systemctl enable docker + ``` + +3. **磁盘空间不足** + ```bash + docker system prune -a + df -h + ``` + +4. **网络连接问题** + ```bash + ping target-server + ssh user@target-server + ``` + +### 日志查看 + +```bash +# 查看导出日志 +tail -f docker-images/export.log + +# 查看部署日志 +sudo tail -f /var/log/knowledge-base-deploy.log + +# 查看Docker日志 +docker compose logs -f +``` + +## 最佳实践 + +1. **始终验证镜像完整性** +2. **在生产部署前进行测试** +3. **定期备份重要数据** +4. **监控系统资源使用** +5. **保持脚本和文档更新** + +## 支持的平台 + +- **源平台**: Linux/macOS (开发环境) +- **目标平台**: OpenEuler 20.03 LTS+ +- **架构**: x86_64 (amd64) +- **Docker**: 20.10+ +- **Docker Compose**: 2.0+ + +--- + +更多详细信息请参考 `DEPLOYMENT_GUIDE.md`。 \ No newline at end of file diff --git a/docker/README-production.md b/docker/README-production.md new file mode 100644 index 0000000..ae67f28 --- /dev/null +++ b/docker/README-production.md @@ -0,0 +1,288 @@ +# Laravel知识库系统 - 生产环境Docker部署指南 + +## 概述 + +本指南介绍如何使用Docker在生产环境中部署Laravel知识库系统。系统包含以下组件: + +- **Web应用**: Laravel应用 + Swoole (端口8000) +- **数据库**: MySQL 8.0 (端口3306) +- **缓存**: Redis 7 (端口6379) +- **搜索**: Meilisearch v1.5 (端口7700) +- **队列**: Laravel队列处理器 + +## 系统要求 + +- Docker Engine 20.10+ +- Docker Compose 2.0+ +- 至少4GB可用内存 +- 至少10GB可用磁盘空间 + +## 快速开始 + +### 1. 环境配置 + +```bash +# 复制环境配置文件 +cp .env.production .env + +# 编辑环境配置(重要!) +nano .env +``` + +**必须修改的配置项:** +- `APP_KEY`: 运行 `php artisan key:generate` 生成 +- `DB_PASSWORD`: 设置强密码 +- `MEILISEARCH_KEY`: 设置搜索引擎主密钥 +- `APP_URL`: 设置实际域名 + +### 2. 启动服务 + +```bash +# 使用启动脚本(推荐) +./docker/start-production.sh + +# 或手动启动 +docker-compose up -d +``` + +### 3. 验证部署 + +```bash +# 检查服务状态 +./docker/check-services.sh + +# 访问应用 +curl http://localhost/health +``` + +## 服务配置详情 + +### MySQL数据库 +- **镜像**: mysql:8.0 +- **端口**: 3306 +- **数据持久化**: `./storage/mysql` +- **配置文件**: `./docker/mysql/my.cnf` +- **字符集**: utf8mb4 + +### Redis缓存 +- **镜像**: redis:7-alpine +- **端口**: 6379 +- **数据持久化**: `./storage/redis` +- **配置文件**: `./docker/redis/redis.conf` +- **内存限制**: 512MB + +### Meilisearch搜索 +- **镜像**: getmeili/meilisearch:v1.5 +- **端口**: 7700 +- **数据持久化**: `./storage/meilisearch` +- **内存限制**: 1GB + +### Laravel应用 +- **基础镜像**: php:8.2-cli-alpine +- **Web服务器**: Swoole (通过 Laravel Octane) +- **端口**: 8000 +- **PHP扩展**: pdo_mysql, redis, gd, zip, intl等 +- **文档转换**: Pandoc + +### 队列处理器 +- **功能**: 处理文档转换等后台任务 +- **命令**: `php artisan queue:work` +- **重试次数**: 3次 +- **超时时间**: 90秒 + +## 数据持久化 + +所有重要数据都持久化到宿主机: + +``` +storage/ +├── mysql/ # MySQL数据文件 +├── redis/ # Redis数据文件 +├── meilisearch/ # 搜索引擎数据 +├── app/ # 应用上传文件 +└── logs/ # 应用日志 + ├── app/ # Web应用日志 + └── queue/ # 队列处理日志 +``` + +## 健康检查 + +系统包含完整的健康检查机制: + +- **Web应用**: HTTP检查 `/health` 端点 +- **MySQL**: 数据库连接检查 +- **Redis**: Redis ping检查 +- **Meilisearch**: API健康检查 + +## 管理命令 + +### 启动和停止 +```bash +# 启动所有服务 +./docker/start-production.sh + +# 停止所有服务 +./docker/stop-production.sh + +# 重启特定服务 +docker-compose restart app +``` + +### 监控和调试 +```bash +# 检查服务状态 +./docker/check-services.sh + +# 查看日志 +docker-compose logs -f app +docker-compose logs -f mysql +docker-compose logs -f queue + +# 进入容器 +docker-compose exec app bash +docker-compose exec mysql mysql -u root -p +``` + +### Laravel管理 +```bash +# 运行Artisan命令 +docker-compose exec app php artisan migrate +docker-compose exec app php artisan queue:work +docker-compose exec app php artisan cache:clear + +# 查看队列状态 +docker-compose exec app php artisan queue:monitor +``` + +## 性能优化 + +### 资源限制 +- **应用容器**: 1GB内存限制 +- **队列容器**: 512MB内存限制 +- **Redis**: 512MB内存限制 +- **Meilisearch**: 1GB内存限制 + +### 缓存配置 +- **OPcache**: 已启用PHP操作码缓存 +- **Laravel缓存**: 使用Redis存储 +- **配置缓存**: 生产环境已启用 + +## 安全配置 + +### 网络安全 +- 使用专用Docker网络 +- 仅暴露必要端口 +- 容器间通信使用内部网络 + +### 数据安全 +- 数据库密码保护 +- Redis可选密码保护 +- Meilisearch主密钥保护 + +### 文件权限 +- 应用文件使用www-data用户 +- 存储目录适当权限设置 + +## 故障排除 + +### 常见问题 + +1. **容器启动失败** + ```bash + # 查看详细日志 + docker-compose logs [service_name] + + # 检查配置 + docker-compose config + ``` + +2. **数据库连接失败** + ```bash + # 检查MySQL状态 + docker-compose exec mysql mysqladmin ping + + # 检查环境变量 + docker-compose exec app env | grep DB_ + ``` + +3. **权限问题** + ```bash + # 修复存储权限 + docker-compose exec app chown -R www-data:www-data /var/www/html/storage + ``` + +4. **内存不足** + ```bash + # 检查资源使用 + docker stats + + # 调整内存限制(docker-compose.yml) + ``` + +### 日志位置 +- **应用日志**: `storage/logs/app/` +- **队列日志**: `storage/logs/queue/` +- **MySQL日志**: 容器内 `/var/log/mysql/` +- **Swoole日志**: 容器内 `/var/log/supervisor/` + +## 备份和恢复 + +### 数据备份 +```bash +# 备份MySQL数据 +docker-compose exec mysql mysqldump -u root -p knowledge_base > backup.sql + +# 备份上传文件 +tar -czf storage-backup.tar.gz storage/app/ + +# 备份搜索数据 +tar -czf meilisearch-backup.tar.gz storage/meilisearch/ +``` + +### 数据恢复 +```bash +# 恢复MySQL数据 +docker-compose exec -T mysql mysql -u root -p knowledge_base < backup.sql + +# 恢复上传文件 +tar -xzf storage-backup.tar.gz +``` + +## 更新和维护 + +### 应用更新 +```bash +# 拉取最新代码 +git pull + +# 重新构建镜像 +docker-compose build --no-cache app + +# 重启服务 +docker-compose up -d + +# 运行迁移 +docker-compose exec app php artisan migrate +``` + +### 系统维护 +```bash +# 清理日志 +docker-compose exec app php artisan log:clear + +# 清理缓存 +docker-compose exec app php artisan cache:clear +docker-compose exec app php artisan config:cache + +# 优化数据库 +docker-compose exec mysql mysqlcheck -u root -p --optimize --all-databases +``` + +## 支持 + +如遇到问题,请检查: +1. Docker和Docker Compose版本 +2. 系统资源使用情况 +3. 环境变量配置 +4. 网络连接状态 +5. 日志文件内容 \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..4a591e4 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,131 @@ +# Docker镜像构建说明 + +## 概述 + +本目录包含了Laravel知识库系统的Docker化配置文件,支持构建适用于OpenEuler服务器的amd64架构镜像。 + +## 文件结构 + +``` +docker/ +├── build.sh # 镜像构建脚本 +├── php/ +│ └── php.ini # PHP配置 +├── supervisor/ +│ └── supervisord.conf # Supervisor配置 +├── mysql/ +│ └── my.cnf # MySQL配置 +├── redis/ +│ └── redis.conf # Redis配置 +└── README.md # 本文件 +``` + +## 镜像特性 + +- **基础环境**: PHP 8.2-cli + Alpine Linux +- **Web服务器**: Swoole (通过 Laravel Octane) +- **架构**: linux/amd64 (OpenEuler兼容) +- **文档转换**: Pandoc +- **进程管理**: Supervisor +- **优化**: 多阶段构建,最小化镜像大小 + +## 构建镜像 + +### 方法1: 使用构建脚本(推荐) + +```bash +# 在项目根目录执行 +./docker/build.sh +``` + +### 方法2: 手动构建 + +```bash +# 在项目根目录执行 +docker build --platform linux/amd64 -t knowledge-base-app:latest . +``` + +## 运行容器 + +### 单独运行(测试用) + +```bash +docker run -d \ + --name knowledge-base \ + -p 8000:8000 \ + -e APP_ENV=production \ + -e APP_KEY=your-app-key \ + knowledge-base-app:latest +``` + +### 使用docker-compose(推荐) + +请参考项目根目录的docker-compose.yml文件。 + +## 环境变量 + +主要环境变量配置: + +- `APP_ENV`: 应用环境 (production/local) +- `APP_KEY`: Laravel应用密钥 +- `DB_HOST`: 数据库主机 +- `DB_DATABASE`: 数据库名称 +- `DB_USERNAME`: 数据库用户名 +- `DB_PASSWORD`: 数据库密码 +- `REDIS_HOST`: Redis主机 +- `MEILISEARCH_HOST`: Meilisearch主机 + +## 健康检查 + +镜像内置健康检查端点: + +- HTTP检查: `http://localhost/health` +- PHP-FPM检查: `http://localhost/ping` + +## 日志 + +日志文件位置: + +- Swoole访问日志: `/var/log/supervisor/swoole_stdout.log` +- Swoole错误日志: `/var/log/supervisor/swoole_stderr.log` +- PHP错误日志: `/var/log/php_errors.log` +- Supervisor日志: `/var/log/supervisor/` + +## 故障排除 + +### 构建失败 + +1. 检查Docker是否运行 +2. 确保网络连接正常(需要下载依赖) +3. 检查磁盘空间是否充足 + +### 容器启动失败 + +1. 检查环境变量配置 +2. 查看容器日志: `docker logs ` +3. 检查端口是否被占用 + +### 权限问题 + +确保storage和bootstrap/cache目录有正确的写权限。 + +## 镜像导出和导入 + +### 导出镜像 + +```bash +docker save knowledge-base-app:latest | gzip > knowledge-base-app.tar.gz +``` + +### 导入镜像 + +```bash +gunzip -c knowledge-base-app.tar.gz | docker load +``` + +## 安全注意事项 + +1. 生产环境请使用HTTPS +2. 定期更新基础镜像 +3. 使用非root用户运行应用 +4. 配置适当的防火墙规则 \ No newline at end of file diff --git a/docker/STORAGE_CONFIGURATION.md b/docker/STORAGE_CONFIGURATION.md new file mode 100644 index 0000000..f55c7ad --- /dev/null +++ b/docker/STORAGE_CONFIGURATION.md @@ -0,0 +1,135 @@ +# 数据持久化和目录映射配置说明 + +## 概述 + +本文档描述了Docker部署中的数据持久化和目录映射配置,确保容器重启后数据不丢失。 + +## 目录映射配置 + +### 1. 项目代码目录映射 +```yaml +volumes: + - ./:/var/www/html +``` +- **用途**: 将项目根目录映射到容器内的Web根目录 +- **好处**: 支持开发环境的代码热重载 +- **注意**: 生产环境建议使用镜像内置代码 + +### 2. 应用存储目录持久化 +```yaml +volumes: + - storage_data:/var/www/html/storage + - documents_data:/var/www/html/storage/app/private/documents + - public_data:/var/www/html/storage/app/public +``` +- **storage_data**: Laravel应用的主存储目录 +- **documents_data**: 上传文档的私有存储目录 +- **public_data**: 公共文件存储目录 + +### 3. 数据库数据持久化 +```yaml +volumes: + - mysql_data:/var/lib/mysql +``` +- **用途**: MySQL数据库文件持久化 +- **映射到**: `./storage/mysql` +- **重要性**: 确保数据库数据在容器重启后不丢失 + +### 4. 缓存数据持久化 +```yaml +volumes: + - redis_data:/data +``` +- **用途**: Redis缓存和会话数据持久化 +- **映射到**: `./storage/redis` +- **好处**: 保持用户会话和缓存数据 + +### 5. 搜索引擎数据持久化 +```yaml +volumes: + - meilisearch_data:/meili_data +``` +- **用途**: Meilisearch搜索索引数据持久化 +- **映射到**: `./storage/meilisearch` +- **重要性**: 避免重新构建搜索索引 + +### 6. 日志目录映射 +```yaml +volumes: + - app_logs:/var/log + - queue_logs:/var/log + - laravel_logs:/var/www/html/storage/logs +``` +- **app_logs**: 应用容器系统日志 +- **queue_logs**: 队列容器系统日志 +- **laravel_logs**: Laravel应用日志 +- **映射到**: `./storage/logs/` 相应子目录 + +## 存储目录结构 + +``` +storage/ +├── app/ # Laravel应用存储 +│ ├── private/ +│ │ ├── documents/ # 上传文档存储 +│ │ └── markdown/ # Markdown文件存储 +│ └── public/ # 公共文件存储 +├── framework/ # Laravel框架缓存 +│ ├── cache/ +│ ├── sessions/ +│ ├── testing/ +│ └── views/ +├── logs/ # 日志文件 +│ ├── app/ # 应用容器日志 +│ ├── queue/ # 队列容器日志 +│ └── laravel.log # Laravel应用日志 +├── mysql/ # MySQL数据文件 +├── redis/ # Redis数据文件 +└── meilisearch/ # Meilisearch索引文件 +``` + +## 权限配置 + +所有存储目录都设置为755权限,确保: +- 容器内的应用可以读写数据 +- 宿主机可以访问和备份数据 +- 安全性和可用性的平衡 + +## 初始化脚本 + +使用 `docker/init-storage.sh` 脚本初始化存储目录: + +```bash +./docker/init-storage.sh +``` + +该脚本会: +1. 创建所有必要的存储目录 +2. 设置正确的权限 +3. 显示目录结构 + +## 备份建议 + +定期备份以下重要目录: +- `storage/mysql/` - 数据库数据 +- `storage/app/private/documents/` - 上传的文档 +- `storage/meilisearch/` - 搜索索引 +- `storage/logs/` - 应用日志 + +## 故障排除 + +### 权限问题 +如果遇到权限错误,运行: +```bash +sudo chown -R $USER:$USER storage/ +chmod -R 755 storage/ +``` + +### 目录不存在 +运行初始化脚本: +```bash +./docker/init-storage.sh +``` + +### 数据丢失 +检查卷映射配置是否正确,确保使用bind mount而不是匿名卷。 \ No newline at end of file diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..1aace26 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Docker镜像构建脚本 +# 用于构建Laravel知识库系统的Docker镜像 + +set -e + +# 配置变量 +IMAGE_NAME="knowledge-base-app" +IMAGE_TAG="latest" +PLATFORM="linux/amd64" + +echo "开始构建Docker镜像..." +echo "镜像名称: ${IMAGE_NAME}:${IMAGE_TAG}" +echo "目标平台: ${PLATFORM}" + +# 检查Docker是否运行 +if ! docker info > /dev/null 2>&1; then + echo "错误: Docker未运行或无法访问" + exit 1 +fi + +# 构建镜像 +echo "正在构建镜像..." +docker build \ + --platform ${PLATFORM} \ + --tag ${IMAGE_NAME}:${IMAGE_TAG} \ + --file Dockerfile \ + . + +# 验证构建结果 +if [ $? -eq 0 ]; then + echo "✅ 镜像构建成功!" + + # 显示镜像信息 + echo "" + echo "镜像信息:" + docker images ${IMAGE_NAME}:${IMAGE_TAG} + + # 检查镜像架构 + echo "" + echo "镜像架构信息:" + docker inspect ${IMAGE_NAME}:${IMAGE_TAG} | grep -A 5 "Architecture" + + echo "" + echo "构建完成! 可以使用以下命令运行容器:" + echo "docker run -d -p 8000:8000 ${IMAGE_NAME}:${IMAGE_TAG}" +else + echo "❌ 镜像构建失败!" + exit 1 +fi \ No newline at end of file diff --git a/docker/check-services.sh b/docker/check-services.sh new file mode 100755 index 0000000..1cbe5d2 --- /dev/null +++ b/docker/check-services.sh @@ -0,0 +1,287 @@ +#!/bin/bash + +# Docker服务健康检查脚本 +# 用于检查所有服务的健康状态 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查Docker是否运行 +check_docker() { + log_info "检查Docker服务状态..." + if ! docker info >/dev/null 2>&1; then + log_error "Docker未运行或无法访问" + exit 1 + fi + log_success "Docker服务正常运行" +} + +# 检查容器状态 +check_container_status() { + local container_name=$1 + local service_name=$2 + + log_info "检查${service_name}容器状态..." + + if ! docker ps --format "table {{.Names}}" | grep -q "^${container_name}$"; then + log_error "${service_name}容器未运行" + return 1 + fi + + # 检查容器健康状态 + local health_status=$(docker inspect --format='{{.State.Health.Status}}' ${container_name} 2>/dev/null || echo "no-healthcheck") + + case $health_status in + "healthy") + log_success "${service_name}容器健康状态正常" + return 0 + ;; + "unhealthy") + log_error "${service_name}容器健康检查失败" + return 1 + ;; + "starting") + log_warning "${service_name}容器正在启动中..." + return 2 + ;; + "no-healthcheck") + log_warning "${service_name}容器未配置健康检查" + return 0 + ;; + *) + log_warning "${service_name}容器健康状态未知: ${health_status}" + return 0 + ;; + esac +} + +# 检查MySQL数据库连接 +check_mysql() { + log_info "检查MySQL数据库连接..." + + local max_attempts=5 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if docker exec knowledge_base_mysql mysqladmin ping -h localhost --silent 2>/dev/null; then + log_success "MySQL数据库连接正常" + return 0 + fi + + log_warning "MySQL连接尝试 ${attempt}/${max_attempts} 失败,等待5秒后重试..." + sleep 5 + ((attempt++)) + done + + log_error "MySQL数据库连接失败" + return 1 +} + +# 检查Redis连接 +check_redis() { + log_info "检查Redis缓存连接..." + + if docker exec knowledge_base_redis redis-cli ping | grep -q "PONG"; then + log_success "Redis缓存连接正常" + return 0 + else + log_error "Redis缓存连接失败" + return 1 + fi +} + +# 检查Meilisearch连接 +check_meilisearch() { + log_info "检查Meilisearch搜索引擎连接..." + + local max_attempts=3 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if docker exec knowledge_base_meilisearch curl -f http://localhost:7700/health >/dev/null 2>&1; then + log_success "Meilisearch搜索引擎连接正常" + return 0 + fi + + log_warning "Meilisearch连接尝试 ${attempt}/${max_attempts} 失败,等待3秒后重试..." + sleep 3 + ((attempt++)) + done + + log_error "Meilisearch搜索引擎连接失败" + return 1 +} + +# 检查Web应用健康状态 +check_web_app() { + log_info "检查Web应用健康状态..." + + local max_attempts=3 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + local response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null || echo "000") + + if [ "$response" = "200" ]; then + log_success "Web应用健康检查通过" + return 0 + elif [ "$response" = "503" ]; then + log_warning "Web应用部分服务不可用,但应用仍在运行" + return 2 + fi + + # 如果没有专门的健康检查路由,尝试访问根路径 + local root_response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/ 2>/dev/null || echo "000") + if [ "$root_response" = "200" ]; then + log_success "Web应用根路径访问正常" + return 0 + fi + + log_warning "Web应用健康检查尝试 ${attempt}/${max_attempts} 失败 (HTTP: ${response}),等待5秒后重试..." + sleep 5 + ((attempt++)) + done + + log_error "Web应用健康检查失败" + return 1 +} + +# 检查队列处理器 +check_queue_worker() { + log_info "检查队列处理器状态..." + + # 检查应用容器中的队列进程是否正在运行 + if docker exec knowledge_base_app pgrep -f "queue:work" >/dev/null 2>&1; then + log_success "队列处理器正常运行" + return 0 + else + log_error "队列处理器进程未运行" + return 1 + fi +} + +# 检查数据持久化 +check_data_persistence() { + log_info "检查数据持久化状态..." + + local errors=0 + + # 检查存储目录 + local storage_dirs=("./storage/mysql" "./storage/redis" "./storage/meilisearch" "./storage/app") + + for dir in "${storage_dirs[@]}"; do + if [ ! -d "$dir" ]; then + log_error "存储目录不存在: $dir" + ((errors++)) + elif [ ! -w "$dir" ]; then + log_error "存储目录不可写: $dir" + ((errors++)) + fi + done + + if [ $errors -eq 0 ]; then + log_success "数据持久化配置正常" + return 0 + else + log_error "发现 $errors 个数据持久化问题" + return 1 + fi +} + +# 主检查函数 +main() { + echo "========================================" + echo "Docker服务健康检查开始" + echo "时间: $(date)" + echo "========================================" + + local total_checks=0 + local failed_checks=0 + local warning_checks=0 + + # 执行所有检查 + checks=( + "check_docker:Docker服务" + "check_container_status:knowledge_base_mysql:MySQL容器" + "check_container_status:knowledge_base_redis:Redis容器" + "check_container_status:knowledge_base_meilisearch:Meilisearch容器" + "check_container_status:knowledge_base_app:应用容器" + "check_mysql:MySQL连接" + "check_redis:Redis连接" + "check_meilisearch:Meilisearch连接" + "check_web_app:Web应用" + "check_queue_worker:队列处理器" + "check_data_persistence:数据持久化" + ) + + for check in "${checks[@]}"; do + IFS=':' read -ra CHECK_PARTS <<< "$check" + local check_func="${CHECK_PARTS[0]}" + local check_args=("${CHECK_PARTS[@]:1}") + + ((total_checks++)) + + if [ ${#check_args[@]} -eq 0 ]; then + $check_func + else + $check_func "${check_args[@]}" + fi + + local result=$? + if [ $result -eq 1 ]; then + ((failed_checks++)) + elif [ $result -eq 2 ]; then + ((warning_checks++)) + fi + + echo "" + done + + # 输出总结 + echo "========================================" + echo "健康检查完成" + echo "总检查项: $total_checks" + echo "失败: $failed_checks" + echo "警告: $warning_checks" + echo "成功: $((total_checks - failed_checks - warning_checks))" + echo "========================================" + + if [ $failed_checks -gt 0 ]; then + log_error "发现 $failed_checks 个严重问题,请检查服务状态" + exit 1 + elif [ $warning_checks -gt 0 ]; then + log_warning "发现 $warning_checks 个警告,服务可能需要关注" + exit 2 + else + log_success "所有服务健康检查通过" + exit 0 + fi +} + +# 如果脚本被直接执行 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/docker/compress-and-verify.sh b/docker/compress-and-verify.sh new file mode 100755 index 0000000..085ff53 --- /dev/null +++ b/docker/compress-and-verify.sh @@ -0,0 +1,399 @@ +#!/bin/bash + +# Docker镜像压缩和完整性检查脚本 +# 用于压缩导出的镜像文件并验证完整性 + +set -e + +# 脚本配置 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +DEFAULT_INPUT_DIR="${PROJECT_ROOT}/docker-images" +LOG_FILE="${DEFAULT_INPUT_DIR}/compress-verify.log" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" +} + +log_success() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE" +} + +log_warning() { + echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE" +} + +log_error() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE" +} + +# 显示帮助信息 +show_help() { + cat << EOF +Docker镜像压缩和完整性检查脚本 + +用法: $0 [选项] [输入目录] + +选项: + -h, --help 显示此帮助信息 + -c, --compress-level N 压缩级别 (1-9, 默认: 6) + -k, --keep-original 保留原始文件 + -v, --verify-only 仅验证,不压缩 + -u, --uncompress 解压缩文件 + --parallel N 并行处理数量 (默认: 2) + +参数: + 输入目录 包含Docker镜像tar文件的目录 (默认: ./docker-images) + +示例: + $0 # 压缩默认目录中的所有tar文件 + $0 -c 9 -k /path/to/images # 最高压缩级别,保留原文件 + $0 --verify-only # 仅验证现有文件完整性 + $0 --uncompress # 解压缩所有.gz文件 + +EOF +} + +# 默认配置 +COMPRESS_LEVEL=6 +KEEP_ORIGINAL=false +VERIFY_ONLY=false +UNCOMPRESS=false +PARALLEL_JOBS=2 +INPUT_DIR="$DEFAULT_INPUT_DIR" + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -c|--compress-level) + COMPRESS_LEVEL="$2" + if [[ ! "$COMPRESS_LEVEL" =~ ^[1-9]$ ]]; then + log_error "压缩级别必须是1-9之间的数字" + exit 1 + fi + shift 2 + ;; + -k|--keep-original) + KEEP_ORIGINAL=true + shift + ;; + -v|--verify-only) + VERIFY_ONLY=true + shift + ;; + -u|--uncompress) + UNCOMPRESS=true + shift + ;; + --parallel) + PARALLEL_JOBS="$2" + if [[ ! "$PARALLEL_JOBS" =~ ^[1-9][0-9]*$ ]]; then + log_error "并行任务数必须是正整数" + exit 1 + fi + shift 2 + ;; + -*) + log_error "未知参数: $1" + show_help + exit 1 + ;; + *) + INPUT_DIR="$1" + shift + ;; + esac +done + +# 检查输入目录 +if [[ ! -d "$INPUT_DIR" ]]; then + log_error "输入目录不存在: $INPUT_DIR" + exit 1 +fi + +# 创建日志目录 +mkdir -p "$(dirname "$LOG_FILE")" + +log "开始镜像压缩和完整性检查..." +log "输入目录: $INPUT_DIR" +log "压缩级别: $COMPRESS_LEVEL" +log "保留原文件: $KEEP_ORIGINAL" +log "仅验证: $VERIFY_ONLY" +log "解压缩: $UNCOMPRESS" +log "并行任务: $PARALLEL_JOBS" + +# 检查必要工具 +check_tools() { + local tools=("gzip" "sha256sum" "tar") + + for tool in "${tools[@]}"; do + if ! command -v "$tool" >/dev/null 2>&1; then + log_error "缺少必要工具: $tool" + exit 1 + fi + done +} + +check_tools + +# 验证文件完整性 +verify_file() { + local file="$1" + local filename=$(basename "$file") + + log "验证文件: $filename" + + if [[ "$file" == *.tar.gz ]]; then + # 验证gzip文件 + if gzip -t "$file" 2>/dev/null; then + log_success "压缩文件完整性验证通过: $filename" + return 0 + else + log_error "压缩文件完整性验证失败: $filename" + return 1 + fi + elif [[ "$file" == *.tar ]]; then + # 验证tar文件 + if tar -tf "$file" >/dev/null 2>&1; then + log_success "tar文件完整性验证通过: $filename" + return 0 + else + log_error "tar文件完整性验证失败: $filename" + return 1 + fi + else + log_warning "未知文件类型,跳过验证: $filename" + return 1 + fi +} + +# 压缩文件 +compress_file() { + local file="$1" + local filename=$(basename "$file") + local compressed_file="${file}.gz" + + log "压缩文件: $filename (级别: $COMPRESS_LEVEL)" + + # 检查是否已经压缩 + if [[ "$file" == *.gz ]]; then + log_warning "文件已经压缩,跳过: $filename" + return 0 + fi + + # 检查压缩文件是否已存在 + if [[ -f "$compressed_file" ]]; then + log_warning "压缩文件已存在,跳过: ${filename}.gz" + return 0 + fi + + # 获取原始文件大小 + local original_size=$(du -b "$file" | cut -f1) + local original_size_human=$(du -h "$file" | cut -f1) + + # 压缩文件 + if gzip -"$COMPRESS_LEVEL" -c "$file" > "$compressed_file"; then + # 获取压缩后文件大小 + local compressed_size=$(du -b "$compressed_file" | cut -f1) + local compressed_size_human=$(du -h "$compressed_file" | cut -f1) + + # 计算压缩比 + local ratio=$(echo "scale=2; $compressed_size * 100 / $original_size" | bc -l 2>/dev/null || echo "N/A") + + log_success "文件压缩成功: ${filename}.gz" + log "原始大小: $original_size_human" + log "压缩后大小: $compressed_size_human" + if [[ "$ratio" != "N/A" ]]; then + log "压缩比: ${ratio}%" + fi + + # 验证压缩文件 + if verify_file "$compressed_file"; then + # 删除原文件(如果不保留) + if [[ "$KEEP_ORIGINAL" == false ]]; then + rm "$file" + log "已删除原文件: $filename" + fi + return 0 + else + log_error "压缩文件验证失败,删除压缩文件" + rm -f "$compressed_file" + return 1 + fi + else + log_error "文件压缩失败: $filename" + return 1 + fi +} + +# 解压缩文件 +uncompress_file() { + local file="$1" + local filename=$(basename "$file") + + log "解压缩文件: $filename" + + # 检查是否是压缩文件 + if [[ "$file" != *.gz ]]; then + log_warning "文件未压缩,跳过: $filename" + return 0 + fi + + # 生成解压后的文件名 + local uncompressed_file="${file%.gz}" + local uncompressed_filename=$(basename "$uncompressed_file") + + # 检查解压文件是否已存在 + if [[ -f "$uncompressed_file" ]]; then + log_warning "解压文件已存在,跳过: $uncompressed_filename" + return 0 + fi + + # 解压文件 + if gunzip -c "$file" > "$uncompressed_file"; then + log_success "文件解压成功: $uncompressed_filename" + + # 验证解压文件 + if verify_file "$uncompressed_file"; then + # 删除压缩文件(如果不保留) + if [[ "$KEEP_ORIGINAL" == false ]]; then + rm "$file" + log "已删除压缩文件: $filename" + fi + return 0 + else + log_error "解压文件验证失败,删除解压文件" + rm -f "$uncompressed_file" + return 1 + fi + else + log_error "文件解压失败: $filename" + return 1 + fi +} + +# 处理单个文件 +process_file() { + local file="$1" + + if [[ "$VERIFY_ONLY" == true ]]; then + verify_file "$file" + elif [[ "$UNCOMPRESS" == true ]]; then + uncompress_file "$file" + else + compress_file "$file" + fi +} + +# 查找需要处理的文件 +if [[ "$UNCOMPRESS" == true ]]; then + FILES=($(find "$INPUT_DIR" -name "*.tar.gz" -type f)) + log "找到 ${#FILES[@]} 个压缩文件" +elif [[ "$VERIFY_ONLY" == true ]]; then + FILES=($(find "$INPUT_DIR" -name "*.tar*" -type f)) + log "找到 ${#FILES[@]} 个文件需要验证" +else + FILES=($(find "$INPUT_DIR" -name "*.tar" -type f)) + log "找到 ${#FILES[@]} 个tar文件需要压缩" +fi + +if [[ ${#FILES[@]} -eq 0 ]]; then + log_warning "没有找到需要处理的文件" + exit 0 +fi + +# 处理文件 +PROCESSED=0 +FAILED=0 +TOTAL=${#FILES[@]} + +# 使用并行处理 +export -f process_file verify_file compress_file uncompress_file log log_success log_warning log_error +export COMPRESS_LEVEL KEEP_ORIGINAL VERIFY_ONLY UNCOMPRESS LOG_FILE +export RED GREEN YELLOW BLUE NC + +printf '%s\n' "${FILES[@]}" | xargs -n 1 -P "$PARALLEL_JOBS" -I {} bash -c 'process_file "$@"' _ {} + +# 统计结果 +for file in "${FILES[@]}"; do + if [[ "$VERIFY_ONLY" == true ]]; then + if verify_file "$file" >/dev/null 2>&1; then + ((PROCESSED++)) + else + ((FAILED++)) + fi + elif [[ "$UNCOMPRESS" == true ]]; then + uncompressed_file="${file%.gz}" + if [[ -f "$uncompressed_file" ]] || [[ "$file" != *.gz ]]; then + ((PROCESSED++)) + else + ((FAILED++)) + fi + else + compressed_file="${file}.gz" + if [[ -f "$compressed_file" ]] || [[ "$file" == *.gz ]]; then + ((PROCESSED++)) + else + ((FAILED++)) + fi + fi +done + +# 更新清单文件 +if [[ "$VERIFY_ONLY" == false ]]; then + manifest_file="${INPUT_DIR}/images-manifest.txt" + if [[ -f "$manifest_file" ]]; then + log "更新镜像清单..." + + # 备份原清单 + cp "$manifest_file" "${manifest_file}.backup" + + # 重新生成清单 + cat > "$manifest_file" << EOF +# Docker镜像清单 +# 更新时间: $(date) +# 处理目录: $INPUT_DIR + +EOF + + for file in "$INPUT_DIR"/*.tar*; do + if [[ -f "$file" ]]; then + filename=$(basename "$file") + size=$(du -h "$file" | cut -f1) + checksum=$(sha256sum "$file" | cut -d' ' -f1) + + echo "文件: $filename" >> "$manifest_file" + echo "大小: $size" >> "$manifest_file" + echo "SHA256: $checksum" >> "$manifest_file" + echo "" >> "$manifest_file" + fi + done + + log_success "镜像清单已更新" + fi +fi + +# 显示总结 +log_success "处理完成!" +log "总文件数: $TOTAL" +log "成功处理: $PROCESSED" +log "失败数量: $FAILED" + +if [[ "$FAILED" -gt 0 ]]; then + log_warning "有 $FAILED 个文件处理失败,请检查日志" + exit 1 +else + log_success "所有文件处理成功" +fi \ No newline at end of file diff --git a/docker/deploy-to-openeuler.sh b/docker/deploy-to-openeuler.sh new file mode 100755 index 0000000..b87d223 --- /dev/null +++ b/docker/deploy-to-openeuler.sh @@ -0,0 +1,616 @@ +#!/bin/bash + +# OpenEuler服务器部署脚本 +# 用于在OpenEuler服务器上部署Laravel知识库系统 + +set -e + +# 脚本配置 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_NAME="knowledge-base" +DEPLOY_DIR="/opt/${PROJECT_NAME}" +BACKUP_DIR="/opt/${PROJECT_NAME}-backup" +LOG_FILE="/var/log/${PROJECT_NAME}-deploy.log" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" +} + +log_success() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE" +} + +log_warning() { + echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE" +} + +log_error() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE" +} + +# 显示帮助信息 +show_help() { + cat << EOF +OpenEuler服务器部署脚本 + +用法: $0 [选项] [镜像目录] + +选项: + -h, --help 显示此帮助信息 + -d, --deploy-dir DIR 部署目录 (默认: /opt/knowledge-base) + -b, --backup 部署前备份现有安装 + -u, --update 更新现有部署 + -r, --rollback 回滚到上一个版本 + --skip-docker-install 跳过Docker安装 + --skip-images 跳过镜像导入 + --skip-env-setup 跳过环境配置 + --dry-run 仅显示将要执行的操作 + +参数: + 镜像目录 包含Docker镜像文件的目录 + +示例: + $0 /path/to/docker-images # 全新部署 + $0 -u /path/to/docker-images # 更新现有部署 + $0 -b -d /custom/path # 备份并部署到自定义目录 + $0 --rollback # 回滚到上一个版本 + +EOF +} + +# 默认配置 +BACKUP=false +UPDATE=false +ROLLBACK=false +SKIP_DOCKER_INSTALL=false +SKIP_IMAGES=false +SKIP_ENV_SETUP=false +DRY_RUN=false +IMAGES_DIR="" + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -d|--deploy-dir) + DEPLOY_DIR="$2" + BACKUP_DIR="${DEPLOY_DIR}-backup" + shift 2 + ;; + -b|--backup) + BACKUP=true + shift + ;; + -u|--update) + UPDATE=true + shift + ;; + -r|--rollback) + ROLLBACK=true + shift + ;; + --skip-docker-install) + SKIP_DOCKER_INSTALL=true + shift + ;; + --skip-images) + SKIP_IMAGES=true + shift + ;; + --skip-env-setup) + SKIP_ENV_SETUP=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -*) + log_error "未知参数: $1" + show_help + exit 1 + ;; + *) + IMAGES_DIR="$1" + shift + ;; + esac +done + +# 检查是否以root权限运行 +if [[ $EUID -ne 0 ]]; then + log_error "此脚本需要root权限运行" + exit 1 +fi + +# 创建日志目录 +mkdir -p "$(dirname "$LOG_FILE")" + +log "开始OpenEuler服务器部署..." +log "部署目录: $DEPLOY_DIR" +log "备份目录: $BACKUP_DIR" +log "镜像目录: $IMAGES_DIR" + +# 检查系统信息 +check_system() { + log "检查系统信息..." + + # 检查操作系统 + if [[ -f /etc/os-release ]]; then + source /etc/os-release + log "操作系统: $NAME $VERSION" + + if [[ "$ID" != "openEuler" ]] && [[ "$ID_LIKE" != *"rhel"* ]]; then + log_warning "此脚本专为OpenEuler设计,当前系统: $NAME" + read -p "是否继续? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + else + log_warning "无法检测操作系统版本" + fi + + # 检查架构 + local arch=$(uname -m) + log "系统架构: $arch" + + if [[ "$arch" != "x86_64" ]]; then + log_warning "推荐使用x86_64架构,当前: $arch" + fi + + # 检查内存 + local memory=$(free -h | awk '/^Mem:/ {print $2}') + log "系统内存: $memory" + + # 检查磁盘空间 + local disk_space=$(df -h / | awk 'NR==2 {print $4}') + log "可用磁盘空间: $disk_space" +} + +# 安装Docker +install_docker() { + if [[ "$SKIP_DOCKER_INSTALL" == true ]]; then + log "跳过Docker安装" + return 0 + fi + + log "检查Docker安装状态..." + + if command -v docker >/dev/null 2>&1; then + local docker_version=$(docker --version) + log "Docker已安装: $docker_version" + + # 检查Docker是否运行 + if systemctl is-active --quiet docker; then + log_success "Docker服务正在运行" + else + log "启动Docker服务..." + systemctl start docker + systemctl enable docker + log_success "Docker服务已启动" + fi + return 0 + fi + + log "安装Docker..." + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将安装Docker" + return 0 + fi + + # 更新系统包 + dnf update -y + + # 安装必要的包 + dnf install -y dnf-plugins-core + + # 添加Docker仓库 + dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + + # 安装Docker + dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # 启动Docker服务 + systemctl start docker + systemctl enable docker + + # 验证安装 + if docker --version >/dev/null 2>&1; then + log_success "Docker安装成功" + else + log_error "Docker安装失败" + exit 1 + fi + + # 添加当前用户到docker组(如果不是root) + if [[ -n "$SUDO_USER" ]]; then + usermod -aG docker "$SUDO_USER" + log "已将用户 $SUDO_USER 添加到docker组" + fi +} + +# 安装Docker Compose +install_docker_compose() { + log "检查Docker Compose..." + + if docker compose version >/dev/null 2>&1; then + local compose_version=$(docker compose version) + log_success "Docker Compose已安装: $compose_version" + return 0 + fi + + log "安装Docker Compose..." + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将安装Docker Compose" + return 0 + fi + + # Docker Compose通常随Docker一起安装 + # 如果没有,可以手动安装 + if ! docker compose version >/dev/null 2>&1; then + log_error "Docker Compose未找到,请手动安装" + exit 1 + fi +} + +# 备份现有部署 +backup_existing() { + if [[ "$BACKUP" == false ]]; then + return 0 + fi + + if [[ ! -d "$DEPLOY_DIR" ]]; then + log "没有现有部署需要备份" + return 0 + fi + + log "备份现有部署..." + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将备份 $DEPLOY_DIR 到 $BACKUP_DIR" + return 0 + fi + + # 停止现有服务 + if [[ -f "$DEPLOY_DIR/docker-compose.yml" ]]; then + log "停止现有服务..." + cd "$DEPLOY_DIR" + docker compose down || true + fi + + # 创建备份 + local backup_name="${BACKUP_DIR}-$(date +%Y%m%d-%H%M%S)" + + if cp -r "$DEPLOY_DIR" "$backup_name"; then + log_success "备份创建成功: $backup_name" + + # 创建符号链接到最新备份 + rm -f "$BACKUP_DIR" + ln -s "$backup_name" "$BACKUP_DIR" + else + log_error "备份创建失败" + exit 1 + fi +} + +# 回滚部署 +rollback_deployment() { + if [[ "$ROLLBACK" == false ]]; then + return 0 + fi + + log "回滚部署..." + + if [[ ! -L "$BACKUP_DIR" ]] || [[ ! -d "$BACKUP_DIR" ]]; then + log_error "没有找到备份,无法回滚" + exit 1 + fi + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将从 $BACKUP_DIR 回滚" + return 0 + fi + + # 停止当前服务 + if [[ -f "$DEPLOY_DIR/docker-compose.yml" ]]; then + log "停止当前服务..." + cd "$DEPLOY_DIR" + docker compose down || true + fi + + # 恢复备份 + rm -rf "$DEPLOY_DIR" + cp -r "$BACKUP_DIR" "$DEPLOY_DIR" + + # 启动服务 + cd "$DEPLOY_DIR" + docker compose up -d + + log_success "回滚完成" + exit 0 +} + +# 导入Docker镜像 +import_images() { + if [[ "$SKIP_IMAGES" == true ]]; then + log "跳过镜像导入" + return 0 + fi + + if [[ -z "$IMAGES_DIR" ]] || [[ ! -d "$IMAGES_DIR" ]]; then + log_error "镜像目录不存在或未指定: $IMAGES_DIR" + exit 1 + fi + + log "导入Docker镜像..." + + # 查找镜像文件 + local image_files=($(find "$IMAGES_DIR" -name "*.tar*" -type f)) + + if [[ ${#image_files[@]} -eq 0 ]]; then + log_error "在 $IMAGES_DIR 中没有找到镜像文件" + exit 1 + fi + + log "找到 ${#image_files[@]} 个镜像文件" + + if [[ "$DRY_RUN" == true ]]; then + for file in "${image_files[@]}"; do + log "[DRY RUN] 将导入: $(basename "$file")" + done + return 0 + fi + + # 导入镜像 + for file in "${image_files[@]}"; do + local filename=$(basename "$file") + log "导入镜像: $filename" + + if [[ "$file" == *.gz ]]; then + # 解压并导入 + if gunzip -c "$file" | docker load; then + log_success "镜像导入成功: $filename" + else + log_error "镜像导入失败: $filename" + exit 1 + fi + else + # 直接导入 + if docker load -i "$file"; then + log_success "镜像导入成功: $filename" + else + log_error "镜像导入失败: $filename" + exit 1 + fi + fi + done + + # 显示导入的镜像 + log "已导入的镜像:" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" +} + +# 设置部署环境 +setup_deployment() { + log "设置部署环境..." + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将创建部署目录: $DEPLOY_DIR" + return 0 + fi + + # 创建部署目录 + mkdir -p "$DEPLOY_DIR" + cd "$DEPLOY_DIR" + + # 复制配置文件(如果镜像目录包含) + if [[ -n "$IMAGES_DIR" ]]; then + # 查找配置文件 + local config_files=( + "docker-compose.yml" + ".env.production" + "docker" + ) + + for config in "${config_files[@]}"; do + if [[ -e "$IMAGES_DIR/../$config" ]]; then + log "复制配置: $config" + cp -r "$IMAGES_DIR/../$config" . + fi + done + fi + + # 创建必要的目录 + mkdir -p storage/{mysql,redis,meilisearch,app,logs} + mkdir -p storage/logs/{app,queue} + + # 设置权限 + chown -R 1000:1000 storage/ + chmod -R 755 storage/ + + log_success "部署环境设置完成" +} + +# 配置环境变量 +setup_environment() { + if [[ "$SKIP_ENV_SETUP" == true ]]; then + log "跳过环境配置" + return 0 + fi + + log "配置环境变量..." + + local env_file="$DEPLOY_DIR/.env" + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将创建环境配置文件: $env_file" + return 0 + fi + + # 生成随机密钥 + local app_key="base64:$(openssl rand -base64 32)" + local db_password=$(openssl rand -base64 16) + local meilisearch_key=$(openssl rand -base64 32) + + # 创建环境配置文件 + cat > "$env_file" << EOF +# Laravel知识库系统 - 生产环境配置 +# 生成时间: $(date) + +# 应用配置 +APP_NAME="知识库系统" +APP_ENV=production +APP_KEY=$app_key +APP_DEBUG=false +APP_URL=http://localhost + +# 数据库配置 +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=knowledge_base +DB_USERNAME=knowledge_user +DB_PASSWORD=$db_password + +# Redis配置 +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# 缓存配置 +CACHE_STORE=redis +SESSION_DRIVER=redis +QUEUE_CONNECTION=redis + +# 搜索配置 +SCOUT_DRIVER=meilisearch +MEILISEARCH_HOST=http://meilisearch:7700 +MEILISEARCH_KEY=$meilisearch_key + +# 日志配置 +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=error + +# 文件系统 +FILESYSTEM_DISK=local + +# 邮件配置 +MAIL_MAILER=log +MAIL_HOST=localhost +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="\${APP_NAME}" +EOF + + log_success "环境配置文件创建成功" + log_warning "请根据实际情况修改 $env_file 中的配置" +} + +# 启动服务 +start_services() { + log "启动服务..." + + if [[ "$DRY_RUN" == true ]]; then + log "[DRY RUN] 将启动Docker Compose服务" + return 0 + fi + + cd "$DEPLOY_DIR" + + # 检查配置文件 + if [[ ! -f "docker-compose.yml" ]]; then + log_error "docker-compose.yml 文件不存在" + exit 1 + fi + + if [[ ! -f ".env" ]]; then + log_error ".env 文件不存在" + exit 1 + fi + + # 启动服务 + if docker compose up -d; then + log_success "服务启动成功" + else + log_error "服务启动失败" + exit 1 + fi + + # 等待服务就绪 + log "等待服务就绪..." + sleep 30 + + # 检查服务状态 + docker compose ps + + # 运行Laravel初始化命令 + log "运行Laravel初始化..." + docker compose exec -T app php artisan migrate --force || log_warning "数据库迁移失败" + docker compose exec -T app php artisan storage:link || log_warning "存储链接创建失败" + + log_success "部署完成!" +} + +# 显示部署信息 +show_deployment_info() { + log "部署信息:" + log "部署目录: $DEPLOY_DIR" + log "访问地址: http://$(hostname -I | awk '{print $1}')" + log "管理命令:" + log " 查看日志: cd $DEPLOY_DIR && docker compose logs -f" + log " 重启服务: cd $DEPLOY_DIR && docker compose restart" + log " 停止服务: cd $DEPLOY_DIR && docker compose down" + log " 更新应用: cd $DEPLOY_DIR && docker compose pull && docker compose up -d" +} + +# 主执行流程 +main() { + check_system + + # 处理回滚 + rollback_deployment + + # 备份现有部署 + backup_existing + + # 安装Docker + install_docker + install_docker_compose + + # 导入镜像 + import_images + + # 设置部署环境 + setup_deployment + + # 配置环境 + setup_environment + + # 启动服务 + start_services + + # 显示部署信息 + show_deployment_info +} + +# 执行主流程 +main \ No newline at end of file diff --git a/docker/export-images.sh b/docker/export-images.sh new file mode 100755 index 0000000..073264d --- /dev/null +++ b/docker/export-images.sh @@ -0,0 +1,335 @@ +#!/bin/bash + +# Docker镜像导出脚本 +# 用于将构建好的Docker镜像导出为tar文件,便于离线部署 + +set -e + +# 脚本配置 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +EXPORT_DIR="${PROJECT_ROOT}/docker-images" +LOG_FILE="${EXPORT_DIR}/export.log" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" +} + +log_success() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE" +} + +log_warning() { + echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE" +} + +log_error() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE" +} + +# 显示帮助信息 +show_help() { + cat << EOF +Docker镜像导出脚本 + +用法: $0 [选项] + +选项: + -h, --help 显示此帮助信息 + -o, --output DIR 指定导出目录 (默认: ./docker-images) + -c, --compress 启用压缩 (使用gzip) + -v, --verify 导出后验证镜像完整性 + --custom-images 导出自定义镜像列表 (用逗号分隔) + --skip-build 跳过镜像构建,直接导出现有镜像 + +示例: + $0 # 导出所有镜像 + $0 -c -v # 导出并压缩,验证完整性 + $0 -o /tmp/images --compress # 导出到指定目录并压缩 + $0 --custom-images "mysql:8.0,redis:7-alpine" # 导出指定镜像 + +EOF +} + +# 默认配置 +COMPRESS=false +VERIFY=false +SKIP_BUILD=false +CUSTOM_IMAGES="" + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -o|--output) + EXPORT_DIR="$2" + shift 2 + ;; + -c|--compress) + COMPRESS=true + shift + ;; + -v|--verify) + VERIFY=true + shift + ;; + --custom-images) + CUSTOM_IMAGES="$2" + shift 2 + ;; + --skip-build) + SKIP_BUILD=true + shift + ;; + *) + log_error "未知参数: $1" + show_help + exit 1 + ;; + esac +done + +# 创建导出目录 +mkdir -p "$EXPORT_DIR" +mkdir -p "$(dirname "$LOG_FILE")" + +log "开始Docker镜像导出过程..." +log "导出目录: $EXPORT_DIR" + +# 检查Docker是否运行 +if ! docker info >/dev/null 2>&1; then + log_error "Docker未运行或无法访问" + exit 1 +fi + +# 定义需要导出的镜像列表 +if [[ -n "$CUSTOM_IMAGES" ]]; then + IFS=',' read -ra IMAGES <<< "$CUSTOM_IMAGES" +else + IMAGES=( + "knowledge-base-app:latest" + "mysql:8.0" + "redis:7-alpine" + "getmeili/meilisearch:v1.5" + ) +fi + +# 构建自定义镜像(如果需要) +if [[ "$SKIP_BUILD" == false ]]; then + log "检查是否需要构建自定义镜像..." + + # 检查knowledge-base-app镜像是否存在 + if ! docker image inspect knowledge-base-app:latest >/dev/null 2>&1; then + log "构建knowledge-base-app镜像..." + cd "$PROJECT_ROOT" + + if docker build --platform linux/amd64 -t knowledge-base-app:latest -f Dockerfile --target production .; then + log_success "knowledge-base-app镜像构建成功" + else + log_error "knowledge-base-app镜像构建失败" + exit 1 + fi + else + log_success "knowledge-base-app镜像已存在" + fi +fi + +# 拉取外部镜像 +log "拉取外部镜像..." +for image in "mysql:8.0" "redis:7-alpine" "getmeili/meilisearch:v1.5"; do + if [[ " ${IMAGES[@]} " =~ " ${image} " ]]; then + log "拉取镜像: $image" + if docker pull --platform linux/amd64 "$image"; then + log_success "镜像 $image 拉取成功" + else + log_warning "镜像 $image 拉取失败,将尝试使用本地镜像" + fi + fi +done + +# 导出镜像 +log "开始导出镜像..." +EXPORTED_FILES=() + +for image in "${IMAGES[@]}"; do + log "导出镜像: $image" + + # 检查镜像是否存在 + if ! docker image inspect "$image" >/dev/null 2>&1; then + log_error "镜像 $image 不存在,跳过导出" + continue + fi + + # 生成文件名(替换特殊字符) + filename=$(echo "$image" | sed 's/[\/:]/_/g') + output_file="${EXPORT_DIR}/${filename}.tar" + + # 导出镜像 + if docker save -o "$output_file" "$image"; then + log_success "镜像 $image 导出成功: $output_file" + EXPORTED_FILES+=("$output_file") + + # 显示文件大小 + size=$(du -h "$output_file" | cut -f1) + log "文件大小: $size" + else + log_error "镜像 $image 导出失败" + continue + fi + + # 压缩文件(如果启用) + if [[ "$COMPRESS" == true ]]; then + log "压缩文件: $output_file" + if gzip "$output_file"; then + compressed_file="${output_file}.gz" + log_success "文件压缩成功: $compressed_file" + + # 更新文件列表 + EXPORTED_FILES=("${EXPORTED_FILES[@]/$output_file}") + EXPORTED_FILES+=("$compressed_file") + + # 显示压缩后大小 + compressed_size=$(du -h "$compressed_file" | cut -f1) + original_size=$(du -h "$output_file" 2>/dev/null | cut -f1 || echo "N/A") + log "压缩后大小: $compressed_size (原始: $original_size)" + else + log_error "文件压缩失败" + fi + fi +done + +# 验证导出的镜像(如果启用) +if [[ "$VERIFY" == true ]]; then + log "验证导出的镜像..." + + for file in "${EXPORTED_FILES[@]}"; do + if [[ -f "$file" ]]; then + log "验证文件: $file" + + # 检查文件完整性 + if [[ "$file" == *.gz ]]; then + # 验证gzip文件 + if gzip -t "$file"; then + log_success "压缩文件完整性验证通过" + else + log_error "压缩文件完整性验证失败" + fi + else + # 验证tar文件 + if tar -tf "$file" >/dev/null 2>&1; then + log_success "tar文件完整性验证通过" + else + log_error "tar文件完整性验证失败" + fi + fi + fi + done +fi + +# 生成镜像清单 +manifest_file="${EXPORT_DIR}/images-manifest.txt" +log "生成镜像清单: $manifest_file" + +cat > "$manifest_file" << EOF +# Docker镜像导出清单 +# 生成时间: $(date) +# 导出目录: $EXPORT_DIR +# 压缩: $COMPRESS +# 验证: $VERIFY + +EOF + +for file in "${EXPORTED_FILES[@]}"; do + if [[ -f "$file" ]]; then + filename=$(basename "$file") + size=$(du -h "$file" | cut -f1) + checksum=$(sha256sum "$file" | cut -d' ' -f1) + + echo "文件: $filename" >> "$manifest_file" + echo "大小: $size" >> "$manifest_file" + echo "SHA256: $checksum" >> "$manifest_file" + echo "" >> "$manifest_file" + fi +done + +# 生成导入脚本 +import_script="${EXPORT_DIR}/import-images.sh" +log "生成导入脚本: $import_script" + +cat > "$import_script" << 'EOF' +#!/bin/bash + +# Docker镜像导入脚本 +# 自动生成,用于导入导出的Docker镜像 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "开始导入Docker镜像..." + +# 检查Docker是否运行 +if ! docker info >/dev/null 2>&1; then + echo "错误: Docker未运行或无法访问" + exit 1 +fi + +# 导入所有tar文件 +for file in "$SCRIPT_DIR"/*.tar*; do + if [[ -f "$file" ]]; then + echo "导入镜像: $(basename "$file")" + + if [[ "$file" == *.gz ]]; then + # 解压并导入 + if gunzip -c "$file" | docker load; then + echo "✓ 镜像导入成功" + else + echo "✗ 镜像导入失败" + fi + else + # 直接导入 + if docker load -i "$file"; then + echo "✓ 镜像导入成功" + else + echo "✗ 镜像导入失败" + fi + fi + fi +done + +echo "镜像导入完成" +echo "可用镜像列表:" +docker images +EOF + +chmod +x "$import_script" + +# 显示总结 +log_success "镜像导出完成!" +log "导出的文件:" +for file in "${EXPORTED_FILES[@]}"; do + if [[ -f "$file" ]]; then + size=$(du -h "$file" | cut -f1) + log " - $(basename "$file") ($size)" + fi +done + +log "生成的文件:" +log " - images-manifest.txt (镜像清单)" +log " - import-images.sh (导入脚本)" + +total_size=$(du -sh "$EXPORT_DIR" | cut -f1) +log "总大小: $total_size" + +log_success "所有文件已保存到: $EXPORT_DIR" \ No newline at end of file diff --git a/docker/import-and-verify.sh b/docker/import-and-verify.sh new file mode 100755 index 0000000..b614370 --- /dev/null +++ b/docker/import-and-verify.sh @@ -0,0 +1,496 @@ +#!/bin/bash + +# Docker镜像导入和验证脚本 +# 用于导入Docker镜像并验证其完整性和兼容性 + +set -e + +# 脚本配置 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_IMAGES_DIR="$(dirname "$SCRIPT_DIR")/docker-images" +LOG_FILE="${DEFAULT_IMAGES_DIR}/import-verify.log" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" +} + +log_success() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE" +} + +log_warning() { + echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE" +} + +log_error() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE" +} + +# 显示帮助信息 +show_help() { + cat << EOF +Docker镜像导入和验证脚本 + +用法: $0 [选项] [镜像目录] + +选项: + -h, --help 显示此帮助信息 + -v, --verify-only 仅验证,不导入 + -f, --force 强制导入,覆盖现有镜像 + -c, --check-manifest 检查清单文件 + --skip-compatibility 跳过兼容性检查 + --parallel N 并行导入数量 (默认: 2) + --test-run 导入后运行测试容器 + +参数: + 镜像目录 包含Docker镜像文件的目录 (默认: ./docker-images) + +示例: + $0 # 导入默认目录中的所有镜像 + $0 -v /path/to/images # 仅验证镜像文件 + $0 -f --test-run # 强制导入并测试 + $0 --check-manifest # 检查清单文件完整性 + +EOF +} + +# 默认配置 +VERIFY_ONLY=false +FORCE_IMPORT=false +CHECK_MANIFEST=false +SKIP_COMPATIBILITY=false +PARALLEL_JOBS=2 +TEST_RUN=false +IMAGES_DIR="$DEFAULT_IMAGES_DIR" + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -v|--verify-only) + VERIFY_ONLY=true + shift + ;; + -f|--force) + FORCE_IMPORT=true + shift + ;; + -c|--check-manifest) + CHECK_MANIFEST=true + shift + ;; + --skip-compatibility) + SKIP_COMPATIBILITY=true + shift + ;; + --parallel) + PARALLEL_JOBS="$2" + if [[ ! "$PARALLEL_JOBS" =~ ^[1-9][0-9]*$ ]]; then + log_error "并行任务数必须是正整数" + exit 1 + fi + shift 2 + ;; + --test-run) + TEST_RUN=true + shift + ;; + -*) + log_error "未知参数: $1" + show_help + exit 1 + ;; + *) + IMAGES_DIR="$1" + shift + ;; + esac +done + +# 检查输入目录 +if [[ ! -d "$IMAGES_DIR" ]]; then + log_error "镜像目录不存在: $IMAGES_DIR" + exit 1 +fi + +# 创建日志目录 +mkdir -p "$(dirname "$LOG_FILE")" + +log "开始Docker镜像导入和验证..." +log "镜像目录: $IMAGES_DIR" +log "仅验证: $VERIFY_ONLY" +log "强制导入: $FORCE_IMPORT" +log "并行任务: $PARALLEL_JOBS" + +# 检查Docker是否运行 +check_docker() { + log "检查Docker环境..." + + if ! command -v docker >/dev/null 2>&1; then + log_error "Docker未安装" + exit 1 + fi + + if ! docker info >/dev/null 2>&1; then + log_error "Docker未运行或无法访问" + exit 1 + fi + + local docker_version=$(docker --version) + log_success "Docker环境正常: $docker_version" + + # 检查系统架构 + local system_arch=$(uname -m) + log "系统架构: $system_arch" + + if [[ "$system_arch" != "x86_64" ]] && [[ "$SKIP_COMPATIBILITY" == false ]]; then + log_warning "系统架构不是x86_64,可能存在兼容性问题" + fi +} + +# 检查清单文件 +check_manifest_file() { + if [[ "$CHECK_MANIFEST" == false ]]; then + return 0 + fi + + local manifest_file="${IMAGES_DIR}/images-manifest.txt" + + log "检查清单文件..." + + if [[ ! -f "$manifest_file" ]]; then + log_warning "清单文件不存在: $manifest_file" + return 1 + fi + + log "验证清单文件中的镜像..." + + # 解析清单文件 + local current_file="" + local expected_checksum="" + local verification_failed=0 + + while IFS= read -r line; do + if [[ "$line" =~ ^文件:\ (.+)$ ]]; then + current_file="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^SHA256:\ (.+)$ ]]; then + expected_checksum="${BASH_REMATCH[1]}" + + if [[ -n "$current_file" ]] && [[ -n "$expected_checksum" ]]; then + local file_path="${IMAGES_DIR}/${current_file}" + + if [[ -f "$file_path" ]]; then + log "验证文件: $current_file" + local actual_checksum=$(sha256sum "$file_path" | cut -d' ' -f1) + + if [[ "$actual_checksum" == "$expected_checksum" ]]; then + log_success "校验和匹配: $current_file" + else + log_error "校验和不匹配: $current_file" + log_error "期望: $expected_checksum" + log_error "实际: $actual_checksum" + ((verification_failed++)) + fi + else + log_error "文件不存在: $current_file" + ((verification_failed++)) + fi + + current_file="" + expected_checksum="" + fi + fi + done < "$manifest_file" + + if [[ $verification_failed -eq 0 ]]; then + log_success "清单文件验证通过" + return 0 + else + log_error "清单文件验证失败,$verification_failed 个文件有问题" + return 1 + fi +} + +# 验证镜像文件 +verify_image_file() { + local file="$1" + local filename=$(basename "$file") + + log "验证镜像文件: $filename" + + # 检查文件是否存在 + if [[ ! -f "$file" ]]; then + log_error "文件不存在: $filename" + return 1 + fi + + # 检查文件大小 + local file_size=$(du -h "$file" | cut -f1) + log "文件大小: $file_size" + + # 验证文件完整性 + if [[ "$file" == *.tar.gz ]]; then + # 验证gzip文件 + if gzip -t "$file" 2>/dev/null; then + log_success "压缩文件完整性验证通过: $filename" + else + log_error "压缩文件完整性验证失败: $filename" + return 1 + fi + elif [[ "$file" == *.tar ]]; then + # 验证tar文件 + if tar -tf "$file" >/dev/null 2>&1; then + log_success "tar文件完整性验证通过: $filename" + else + log_error "tar文件完整性验证失败: $filename" + return 1 + fi + else + log_warning "未知文件类型,跳过验证: $filename" + return 1 + fi + + return 0 +} + +# 导入镜像文件 +import_image_file() { + local file="$1" + local filename=$(basename "$file") + + log "导入镜像文件: $filename" + + # 首先验证文件 + if ! verify_image_file "$file"; then + log_error "文件验证失败,跳过导入: $filename" + return 1 + fi + + # 检查是否需要强制导入 + local import_args="" + if [[ "$FORCE_IMPORT" == true ]]; then + import_args="--quiet" + fi + + # 导入镜像 + local import_output + if [[ "$file" == *.tar.gz ]]; then + # 解压并导入 + import_output=$(gunzip -c "$file" | docker load 2>&1) + else + # 直接导入 + import_output=$(docker load -i "$file" 2>&1) + fi + + if [[ $? -eq 0 ]]; then + log_success "镜像导入成功: $filename" + + # 提取导入的镜像名称 + local imported_image=$(echo "$import_output" | grep "Loaded image" | sed 's/Loaded image: //') + if [[ -n "$imported_image" ]]; then + log "导入的镜像: $imported_image" + + # 验证镜像架构 + if [[ "$SKIP_COMPATIBILITY" == false ]]; then + verify_image_architecture "$imported_image" + fi + fi + + return 0 + else + log_error "镜像导入失败: $filename" + log_error "错误信息: $import_output" + return 1 + fi +} + +# 验证镜像架构 +verify_image_architecture() { + local image="$1" + + log "验证镜像架构: $image" + + # 获取镜像信息 + local image_info=$(docker image inspect "$image" 2>/dev/null) + + if [[ $? -ne 0 ]]; then + log_error "无法获取镜像信息: $image" + return 1 + fi + + # 提取架构信息 + local architecture=$(echo "$image_info" | grep -o '"Architecture":"[^"]*"' | cut -d'"' -f4) + local os=$(echo "$image_info" | grep -o '"Os":"[^"]*"' | cut -d'"' -f4) + + log "镜像架构: $os/$architecture" + + # 检查架构兼容性 + local system_arch=$(uname -m) + local expected_arch="amd64" + + if [[ "$system_arch" == "x86_64" ]]; then + expected_arch="amd64" + elif [[ "$system_arch" == "aarch64" ]]; then + expected_arch="arm64" + fi + + if [[ "$architecture" == "$expected_arch" ]]; then + log_success "镜像架构兼容: $architecture" + return 0 + else + log_warning "镜像架构可能不兼容: $architecture (系统: $system_arch)" + return 1 + fi +} + +# 测试镜像运行 +test_image_run() { + local image="$1" + + log "测试镜像运行: $image" + + # 根据镜像类型选择测试命令 + local test_command="" + local container_name="test-$(echo "$image" | sed 's/[\/:]/_/g')-$$" + + case "$image" in + *mysql*) + test_command="docker run --rm --name $container_name -e MYSQL_ROOT_PASSWORD=test -d $image" + ;; + *redis*) + test_command="docker run --rm --name $container_name -d $image" + ;; + *meilisearch*) + test_command="docker run --rm --name $container_name -e MEILI_MASTER_KEY=test -d $image" + ;; + *knowledge-base-app*) + # 应用镜像需要更复杂的测试 + log_warning "应用镜像测试需要完整环境,跳过单独测试" + return 0 + ;; + *) + log_warning "未知镜像类型,跳过运行测试: $image" + return 0 + ;; + esac + + # 运行测试容器 + if eval "$test_command"; then + log "测试容器启动成功: $container_name" + + # 等待容器启动 + sleep 5 + + # 检查容器状态 + if docker ps | grep -q "$container_name"; then + log_success "镜像运行测试通过: $image" + + # 停止测试容器 + docker stop "$container_name" >/dev/null 2>&1 || true + return 0 + else + log_error "测试容器启动失败: $image" + + # 显示容器日志 + docker logs "$container_name" 2>/dev/null || true + docker rm "$container_name" >/dev/null 2>&1 || true + return 1 + fi + else + log_error "无法启动测试容器: $image" + return 1 + fi +} + +# 处理单个镜像文件 +process_image_file() { + local file="$1" + local filename=$(basename "$file") + + log "处理镜像文件: $filename" + + if [[ "$VERIFY_ONLY" == true ]]; then + verify_image_file "$file" + return $? + else + if import_image_file "$file"; then + # 如果需要测试运行 + if [[ "$TEST_RUN" == true ]]; then + # 提取镜像名称进行测试 + local image_name=$(echo "$filename" | sed 's/\.tar.*$//' | sed 's/_/:/g') + test_image_run "$image_name" || true + fi + return 0 + else + return 1 + fi + fi +} + +# 主处理流程 +main() { + check_docker + check_manifest_file + + # 查找镜像文件 + local image_files=($(find "$IMAGES_DIR" -name "*.tar*" -type f)) + + if [[ ${#image_files[@]} -eq 0 ]]; then + log_error "在 $IMAGES_DIR 中没有找到镜像文件" + exit 1 + fi + + log "找到 ${#image_files[@]} 个镜像文件" + + # 处理镜像文件 + local processed=0 + local failed=0 + local total=${#image_files[@]} + + # 使用并行处理 + export -f process_image_file verify_image_file import_image_file verify_image_architecture test_image_run + export -f log log_success log_warning log_error + export VERIFY_ONLY FORCE_IMPORT SKIP_COMPATIBILITY TEST_RUN LOG_FILE + export RED GREEN YELLOW BLUE NC + + for file in "${image_files[@]}"; do + if process_image_file "$file"; then + ((processed++)) + else + ((failed++)) + fi + done + + # 显示导入的镜像 + if [[ "$VERIFY_ONLY" == false ]]; then + log "当前Docker镜像列表:" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + fi + + # 显示总结 + log_success "处理完成!" + log "总文件数: $total" + log "成功处理: $processed" + log "失败数量: $failed" + + if [[ $failed -gt 0 ]]; then + log_warning "有 $failed 个文件处理失败,请检查日志" + exit 1 + else + log_success "所有文件处理成功" + fi +} + +# 执行主流程 +main \ No newline at end of file diff --git a/docker/init-storage.sh b/docker/init-storage.sh new file mode 100755 index 0000000..f388408 --- /dev/null +++ b/docker/init-storage.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# 初始化存储目录脚本 +# 用于确保所有数据持久化目录存在并具有正确权限 + +set -e + +echo "正在初始化存储目录结构..." + +# 创建数据库存储目录 +mkdir -p storage/mysql +chmod 755 storage/mysql + +# 创建Redis存储目录 +mkdir -p storage/redis +chmod 755 storage/redis + +# 创建Meilisearch存储目录 +mkdir -p storage/meilisearch +chmod 755 storage/meilisearch + +# 创建应用存储目录 +mkdir -p storage/app/private/documents +mkdir -p storage/app/private/markdown +mkdir -p storage/app/public +chmod -R 755 storage/app + +# 创建日志目录 +mkdir -p storage/logs/app +mkdir -p storage/logs/queue +chmod -R 755 storage/logs + +# 创建Laravel框架目录 +mkdir -p storage/framework/cache/data +mkdir -p storage/framework/sessions +mkdir -p storage/framework/testing +mkdir -p storage/framework/views +chmod -R 755 storage/framework + +echo "存储目录结构初始化完成!" + +# 显示目录结构 +echo "当前存储目录结构:" +tree storage/ || ls -la storage/ \ No newline at end of file diff --git a/docker/monitor-services.sh b/docker/monitor-services.sh new file mode 100755 index 0000000..6266642 --- /dev/null +++ b/docker/monitor-services.sh @@ -0,0 +1,316 @@ +#!/bin/bash + +# Docker服务监控脚本 +# 持续监控服务状态并在需要时采取行动 + +set -e + +# 配置 +MONITOR_INTERVAL=${MONITOR_INTERVAL:-60} # 监控间隔(秒) +MAX_RESTART_ATTEMPTS=${MAX_RESTART_ATTEMPTS:-3} # 最大重启尝试次数 +RESTART_COOLDOWN=${RESTART_COOLDOWN:-300} # 重启冷却时间(秒) +LOG_FILE=${LOG_FILE:-"./storage/logs/monitor.log"} + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 创建日志目录 +mkdir -p "$(dirname "$LOG_FILE")" + +# 日志函数 +log_with_timestamp() { + local level=$1 + local message=$2 + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo -e "${timestamp} [${level}] ${message}" | tee -a "$LOG_FILE" +} + +log_info() { + log_with_timestamp "INFO" "${BLUE}$1${NC}" +} + +log_success() { + log_with_timestamp "SUCCESS" "${GREEN}$1${NC}" +} + +log_warning() { + log_with_timestamp "WARNING" "${YELLOW}$1${NC}" +} + +log_error() { + log_with_timestamp "ERROR" "${RED}$1${NC}" +} + +# 重启计数器文件 +RESTART_COUNTER_DIR="./storage/logs/restart_counters" +mkdir -p "$RESTART_COUNTER_DIR" + +# 获取容器重启次数 +get_restart_count() { + local container_name=$1 + local counter_file="$RESTART_COUNTER_DIR/${container_name}.count" + + if [ -f "$counter_file" ]; then + cat "$counter_file" + else + echo "0" + fi +} + +# 增加重启次数 +increment_restart_count() { + local container_name=$1 + local counter_file="$RESTART_COUNTER_DIR/${container_name}.count" + local current_count=$(get_restart_count "$container_name") + local new_count=$((current_count + 1)) + + echo "$new_count" > "$counter_file" + echo "$new_count" +} + +# 重置重启次数 +reset_restart_count() { + local container_name=$1 + local counter_file="$RESTART_COUNTER_DIR/${container_name}.count" + echo "0" > "$counter_file" +} + +# 检查容器是否需要重启 +should_restart_container() { + local container_name=$1 + local restart_count=$(get_restart_count "$container_name") + + if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then + return 1 # 不应该重启 + else + return 0 # 可以重启 + fi +} + +# 检查容器健康状态 +check_container_health() { + local container_name=$1 + local service_name=$2 + + # 检查容器是否运行 + if ! docker ps --format "table {{.Names}}" | grep -q "^${container_name}$"; then + log_error "${service_name}容器未运行" + return 1 + fi + + # 检查容器健康状态 + local health_status=$(docker inspect --format='{{.State.Health.Status}}' ${container_name} 2>/dev/null || echo "no-healthcheck") + + case $health_status in + "healthy") + # 如果容器健康,重置重启计数器 + reset_restart_count "$container_name" + return 0 + ;; + "unhealthy") + log_error "${service_name}容器健康检查失败" + return 1 + ;; + "starting") + log_warning "${service_name}容器正在启动中..." + return 2 + ;; + "no-healthcheck") + # 对于没有健康检查的容器,检查是否正在运行 + local container_status=$(docker inspect --format='{{.State.Status}}' ${container_name} 2>/dev/null || echo "unknown") + if [ "$container_status" = "running" ]; then + reset_restart_count "$container_name" + return 0 + else + log_error "${service_name}容器状态异常: ${container_status}" + return 1 + fi + ;; + *) + log_warning "${service_name}容器健康状态未知: ${health_status}" + return 2 + ;; + esac +} + +# 重启容器 +restart_container() { + local container_name=$1 + local service_name=$2 + + if ! should_restart_container "$container_name"; then + log_error "${service_name}容器已达到最大重启次数限制,跳过重启" + return 1 + fi + + local restart_count=$(increment_restart_count "$container_name") + log_warning "${service_name}容器开始重启 (第${restart_count}次尝试)" + + if docker restart "$container_name"; then + log_info "${service_name}容器重启命令执行成功,等待启动..." + sleep 30 # 等待容器启动 + return 0 + else + log_error "${service_name}容器重启失败" + return 1 + fi +} + +# 监控单个服务 +monitor_service() { + local container_name=$1 + local service_name=$2 + + check_container_health "$container_name" "$service_name" + local health_result=$? + + case $health_result in + 0) + # 健康 + return 0 + ;; + 1) + # 不健康,尝试重启 + log_warning "${service_name}服务不健康,尝试重启..." + restart_container "$container_name" "$service_name" + return $? + ;; + 2) + # 启动中或状态未知,继续监控 + return 0 + ;; + esac +} + +# 发送告警通知(可扩展) +send_alert() { + local message=$1 + local severity=$2 + + log_error "告警: $message" + + # 这里可以添加更多告警方式,如: + # - 发送邮件 + # - 发送到Slack + # - 发送到监控系统 + # - 写入系统日志 + + # 示例:写入系统日志 + if command -v logger >/dev/null 2>&1; then + logger -t "docker-monitor" -p user.error "$message" + fi +} + +# 主监控循环 +main_monitor_loop() { + log_info "Docker服务监控开始,监控间隔: ${MONITOR_INTERVAL}秒" + + # 定义要监控的服务 + local services=( + "knowledge_base_mysql:MySQL数据库" + "knowledge_base_redis:Redis缓存" + "knowledge_base_meilisearch:Meilisearch搜索" + "knowledge_base_app:Web应用" + "knowledge_base_queue:队列处理器" + ) + + while true; do + local failed_services=0 + local total_services=${#services[@]} + + log_info "开始监控检查 (共${total_services}个服务)" + + for service in "${services[@]}"; do + IFS=':' read -ra SERVICE_PARTS <<< "$service" + local container_name="${SERVICE_PARTS[0]}" + local service_name="${SERVICE_PARTS[1]}" + + if ! monitor_service "$container_name" "$service_name"; then + ((failed_services++)) + fi + done + + if [ $failed_services -gt 0 ]; then + local message="监控检查完成,发现 ${failed_services}/${total_services} 个服务存在问题" + log_warning "$message" + + if [ $failed_services -ge $((total_services / 2)) ]; then + send_alert "超过一半的服务出现问题: $message" "critical" + fi + else + log_success "所有服务运行正常" + fi + + log_info "等待 ${MONITOR_INTERVAL} 秒后进行下次检查..." + sleep "$MONITOR_INTERVAL" + done +} + +# 清理函数 +cleanup() { + log_info "监控脚本正在退出..." + exit 0 +} + +# 设置信号处理 +trap cleanup SIGINT SIGTERM + +# 显示使用帮助 +show_help() { + echo "Docker服务监控脚本" + echo "" + echo "用法: $0 [选项]" + echo "" + echo "选项:" + echo " -i, --interval SECONDS 监控间隔(默认: 60秒)" + echo " -r, --max-restarts NUM 最大重启尝试次数(默认: 3次)" + echo " -c, --cooldown SECONDS 重启冷却时间(默认: 300秒)" + echo " -l, --log-file PATH 日志文件路径(默认: ./storage/logs/monitor.log)" + echo " -h, --help 显示此帮助信息" + echo "" + echo "环境变量:" + echo " MONITOR_INTERVAL 监控间隔" + echo " MAX_RESTART_ATTEMPTS 最大重启尝试次数" + echo " RESTART_COOLDOWN 重启冷却时间" + echo " LOG_FILE 日志文件路径" +} + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -i|--interval) + MONITOR_INTERVAL="$2" + shift 2 + ;; + -r|--max-restarts) + MAX_RESTART_ATTEMPTS="$2" + shift 2 + ;; + -c|--cooldown) + RESTART_COOLDOWN="$2" + shift 2 + ;; + -l|--log-file) + LOG_FILE="$2" + shift 2 + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "未知选项: $1" + show_help + exit 1 + ;; + esac +done + +# 如果脚本被直接执行 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main_monitor_loop +fi \ No newline at end of file diff --git a/docker/mysql/my.cnf b/docker/mysql/my.cnf new file mode 100644 index 0000000..4adf491 --- /dev/null +++ b/docker/mysql/my.cnf @@ -0,0 +1,40 @@ +# MySQL生产环境配置 +[mysqld] +# 基础配置 +default-authentication-plugin=mysql_native_password +character-set-server=utf8mb4 +collation-server=utf8mb4_unicode_ci +default-time-zone='+08:00' + +# 性能优化 +innodb_buffer_pool_size=512M +innodb_log_file_size=128M +innodb_flush_log_at_trx_commit=2 +innodb_flush_method=O_DIRECT + +# 连接配置 +max_connections=200 +max_connect_errors=1000 +wait_timeout=600 +interactive_timeout=600 + +# 查询缓存 +query_cache_type=1 +query_cache_size=64M +query_cache_limit=2M + +# 日志配置 +slow_query_log=1 +slow_query_log_file=/var/log/mysql/slow.log +long_query_time=2 +log_queries_not_using_indexes=1 + +# 安全配置 +skip-name-resolve +sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO + +[mysql] +default-character-set=utf8mb4 + +[client] +default-character-set=utf8mb4 \ No newline at end of file diff --git a/docker/octane-health-check.sh b/docker/octane-health-check.sh new file mode 100644 index 0000000..44cc5db --- /dev/null +++ b/docker/octane-health-check.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Octane HTTP服务器健康检查脚本 +# 用于Docker健康检查,支持Swoole和RoadRunner + +set -e + +# 检查Octane进程是否运行 +if ! pgrep -f "octane:start" > /dev/null; then + echo "Octane HTTP服务器进程未运行" + exit 1 +fi + +# 检查HTTP端口是否可访问 +OCTANE_PORT=${OCTANE_PORT:-8000} +if ! curl -f -s "http://localhost:${OCTANE_PORT}/health" > /dev/null 2>&1; then + echo "Octane HTTP服务器端口 ${OCTANE_PORT} 不可访问" + exit 1 +fi + +# 检查Laravel应用是否可以连接到数据库和Redis +if ! php -r " +try { + require_once '/var/www/html/vendor/autoload.php'; + \$app = require_once '/var/www/html/bootstrap/app.php'; + \$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + + // 检查数据库连接 + \Illuminate\Support\Facades\DB::connection()->getPdo(); + + // 检查Redis连接 + if (config('cache.default') === 'redis') { + \Illuminate\Support\Facades\Cache::store('redis')->put('octane_health_check', 'ok', 10); + \Illuminate\Support\Facades\Cache::store('redis')->forget('octane_health_check'); + } + + echo 'OK'; +} catch (Exception \$e) { + echo 'ERROR: ' . \$e->getMessage(); + exit(1); +} +"; then + echo "Octane服务器依赖服务检查失败" + exit 1 +fi + +echo "Octane HTTP服务器健康检查通过" +exit 0 \ No newline at end of file diff --git a/docker/one-click-deploy.sh b/docker/one-click-deploy.sh new file mode 100755 index 0000000..2e922d9 --- /dev/null +++ b/docker/one-click-deploy.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# 一键部署脚本 - Laravel知识库系统 +# 整合镜像导出、压缩、传输和部署功能 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" +} + +log_error() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" +} + +show_help() { + cat << EOF +一键部署脚本 - Laravel知识库系统 + +用法: $0 [模式] [选项] + +模式: + export 导出Docker镜像 + deploy 部署到OpenEuler服务器 + full 完整流程 (导出+部署) + +选项: + -h, --help 显示帮助信息 + -c, --compress 启用压缩 + -v, --verify 验证完整性 + --server HOST 目标服务器地址 + --user USER SSH用户名 + --deploy-dir DIR 部署目录 + +示例: + $0 export -c -v # 导出并压缩镜像 + $0 deploy --server 192.168.1.100 # 部署到服务器 + $0 full -c --server 192.168.1.100 # 完整流程 + +EOF +} + +# 默认配置 +MODE="" +COMPRESS=false +VERIFY=false +SERVER="" +USER="deploy" +DEPLOY_DIR="/opt/knowledge-base" + +# 解析参数 +while [[ $# -gt 0 ]]; do + case $1 in + export|deploy|full) + MODE="$1" + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -c|--compress) + COMPRESS=true + shift + ;; + -v|--verify) + VERIFY=true + shift + ;; + --server) + SERVER="$2" + shift 2 + ;; + --user) + USER="$2" + shift 2 + ;; + --deploy-dir) + DEPLOY_DIR="$2" + shift 2 + ;; + *) + log_error "未知参数: $1" + show_help + exit 1 + ;; + esac +done + +if [[ -z "$MODE" ]]; then + log_error "请指定模式: export, deploy, 或 full" + show_help + exit 1 +fi + +# 导出镜像 +export_images() { + log "开始导出Docker镜像..." + + local export_args="" + if [[ "$COMPRESS" == true ]]; then + export_args="$export_args -c" + fi + if [[ "$VERIFY" == true ]]; then + export_args="$export_args -v" + fi + + if "$SCRIPT_DIR/export-images.sh" $export_args; then + log_success "镜像导出完成" + else + log_error "镜像导出失败" + exit 1 + fi +} + +# 部署到服务器 +deploy_to_server() { + if [[ -z "$SERVER" ]]; then + log_error "请指定服务器地址 --server" + exit 1 + fi + + log "开始部署到服务器: $SERVER" + + # 传输文件 + log "传输部署文件..." + scp -r "${PROJECT_ROOT}/docker-images" "${USER}@${SERVER}:/tmp/" + scp "${SCRIPT_DIR}/deploy-to-openeuler.sh" "${USER}@${SERVER}:/tmp/" + + # 远程部署 + log "执行远程部署..." + ssh "${USER}@${SERVER}" "sudo /tmp/deploy-to-openeuler.sh -d $DEPLOY_DIR /tmp/docker-images" + + log_success "部署完成" +} + +# 主流程 +case "$MODE" in + export) + export_images + ;; + deploy) + deploy_to_server + ;; + full) + export_images + deploy_to_server + ;; +esac + +log_success "操作完成!" \ No newline at end of file diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..defb12c --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,44 @@ +# PHP生产环境配置 + +# 基础设置 +memory_limit = 256M +max_execution_time = 60 +max_input_time = 60 +post_max_size = 100M +upload_max_filesize = 100M +max_file_uploads = 20 + +# 错误报告(生产环境) +display_errors = Off +display_startup_errors = Off +log_errors = On +error_log = /var/log/php_errors.log +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT + +# 会话设置 +session.save_handler = redis +session.save_path = "tcp://redis:6379" +session.gc_maxlifetime = 1440 +session.cookie_lifetime = 0 +session.cookie_secure = 0 +session.cookie_httponly = 1 +session.use_strict_mode = 1 + +# OPcache设置 +opcache.enable = 1 +opcache.enable_cli = 1 +opcache.memory_consumption = 128 +opcache.interned_strings_buffer = 8 +opcache.max_accelerated_files = 4000 +opcache.revalidate_freq = 2 +opcache.fast_shutdown = 1 +opcache.validate_timestamps = 0 + +# 时区设置 +date.timezone = Asia/Shanghai + +# 其他设置 +expose_php = Off +allow_url_fopen = On +allow_url_include = Off +default_charset = "UTF-8" \ No newline at end of file diff --git a/docker/queue-health-check.sh b/docker/queue-health-check.sh new file mode 100755 index 0000000..ce05dd9 --- /dev/null +++ b/docker/queue-health-check.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# 队列处理器健康检查脚本 +# 用于Docker健康检查 + +set -e + +# 检查队列进程是否运行 +if ! pgrep -f "queue:work" > /dev/null; then + echo "队列处理器进程未运行" + exit 1 +fi + +# 检查Laravel应用是否可以连接到数据库和Redis +if ! php -r " +try { + require_once '/var/www/html/vendor/autoload.php'; + \$app = require_once '/var/www/html/bootstrap/app.php'; + \$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + + // 检查数据库连接 + \Illuminate\Support\Facades\DB::connection()->getPdo(); + + // 检查Redis连接 + if (config('cache.default') === 'redis') { + \Illuminate\Support\Facades\Cache::store('redis')->put('queue_health_check', 'ok', 10); + \Illuminate\Support\Facades\Cache::store('redis')->forget('queue_health_check'); + } + + echo 'OK'; +} catch (Exception \$e) { + echo 'ERROR: ' . \$e->getMessage(); + exit(1); +} +"; then + echo "队列处理器依赖服务检查失败" + exit 1 +fi + +echo "队列处理器健康检查通过" +exit 0 \ No newline at end of file diff --git a/docker/redis/redis.conf b/docker/redis/redis.conf new file mode 100644 index 0000000..9fa9cbb --- /dev/null +++ b/docker/redis/redis.conf @@ -0,0 +1,53 @@ +# Redis生产环境配置 + +# 网络配置 +bind 0.0.0.0 +port 6379 +timeout 300 +tcp-keepalive 60 + +# 内存配置 +maxmemory 512mb +maxmemory-policy allkeys-lru + +# 持久化配置 +save 900 1 +save 300 10 +save 60 10000 +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /data + +# AOF配置 +appendonly yes +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb + +# 日志配置 +loglevel notice +logfile "" + +# 安全配置 +protected-mode no +# requirepass your_redis_password_here + +# 性能配置 +tcp-backlog 511 +databases 16 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes + +# 客户端配置 +maxclients 10000 + +# 慢日志配置 +slowlog-log-slower-than 10000 +slowlog-max-len 128 + +# 延迟监控 +latency-monitor-threshold 100 \ No newline at end of file diff --git a/docker/setup-env.sh b/docker/setup-env.sh new file mode 100755 index 0000000..85131f6 --- /dev/null +++ b/docker/setup-env.sh @@ -0,0 +1,400 @@ +#!/bin/bash + +# 环境配置设置脚本 +# 用于初始化和配置Docker部署环境 + +set -e + +echo "===================================" +echo "Docker环境配置设置" +echo "===================================" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 默认值 +DEFAULT_ENV="production" +DEFAULT_DB_PASSWORD="secure_password_$(date +%s)" +DEFAULT_MEILISEARCH_KEY="master_key_$(openssl rand -hex 16)" + +# 显示帮助信息 +show_help() { + echo "用法: $0 [选项]" + echo "" + echo "选项:" + echo " -e, --env ENV 设置环境类型 (production|development) [默认: production]" + echo " -i, --interactive 交互式配置" + echo " -f, --force 强制覆盖现有配置文件" + echo " -h, --help 显示此帮助信息" + echo "" + echo "示例:" + echo " $0 -e production 设置生产环境" + echo " $0 -e development 设置开发环境" + echo " $0 -i 交互式配置" +} + +# 解析命令行参数 +ENV_TYPE="$DEFAULT_ENV" +INTERACTIVE=false +FORCE=false + +while [[ $# -gt 0 ]]; do + case $1 in + -e|--env) + ENV_TYPE="$2" + shift 2 + ;; + -i|--interactive) + INTERACTIVE=true + shift + ;; + -f|--force) + FORCE=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "未知选项: $1" + show_help + exit 1 + ;; + esac +done + +# 验证环境类型 +if [[ "$ENV_TYPE" != "production" && "$ENV_TYPE" != "development" ]]; then + echo -e "${RED}错误: 环境类型必须是 'production' 或 'development'${NC}" + exit 1 +fi + +echo -e "${BLUE}配置环境类型: $ENV_TYPE${NC}" + +# 交互式配置 +if [ "$INTERACTIVE" = true ]; then + echo "" + echo "交互式配置模式" + echo "-----------------------------------" + + read -p "应用名称 [知识库系统]: " app_name + app_name=${app_name:-"知识库系统"} + + read -p "应用URL [http://localhost]: " app_url + app_url=${app_url:-"http://localhost"} + + read -p "数据库名称 [knowledge_base]: " db_name + db_name=${db_name:-"knowledge_base"} + + read -p "数据库用户名 [knowledge_user]: " db_user + db_user=${db_user:-"knowledge_user"} + + read -s -p "数据库密码: " db_password + echo "" + + read -s -p "Meilisearch主密钥: " meilisearch_key + echo "" + + if [ "$ENV_TYPE" = "production" ]; then + read -p "SMTP主机: " mail_host + read -p "SMTP端口 [587]: " mail_port + mail_port=${mail_port:-587} + read -p "SMTP用户名: " mail_username + read -s -p "SMTP密码: " mail_password + echo "" + fi +else + # 非交互式配置,使用默认值 + app_name="知识库系统" + app_url="http://localhost" + db_name="knowledge_base" + db_user="knowledge_user" + db_password="$DEFAULT_DB_PASSWORD" + meilisearch_key="$DEFAULT_MEILISEARCH_KEY" +fi + +# 生成APP_KEY +echo "" +echo "生成应用密钥..." +echo "-----------------------------------" + +if command -v php >/dev/null 2>&1; then + # 如果有PHP,使用Laravel生成密钥 + if [ -f "artisan" ]; then + app_key=$(php artisan key:generate --show) + echo -e "${GREEN}✓ 使用Laravel生成APP_KEY${NC}" + else + # 生成base64编码的随机密钥 + app_key="base64:$(openssl rand -base64 32)" + echo -e "${GREEN}✓ 使用OpenSSL生成APP_KEY${NC}" + fi +else + # 生成base64编码的随机密钥 + app_key="base64:$(openssl rand -base64 32)" + echo -e "${GREEN}✓ 使用OpenSSL生成APP_KEY${NC}" +fi + +# 确定环境文件名 +if [ "$ENV_TYPE" = "production" ]; then + env_file=".env.production" + compose_file="docker-compose.yml" +else + env_file=".env.development" + compose_file="docker-compose.dev.yml" + app_name="${app_name}-开发" + db_name="${db_name}_dev" + db_user="dev_user" + app_url="http://localhost:8000" +fi + +# 检查文件是否存在 +if [ -f "$env_file" ] && [ "$FORCE" = false ]; then + echo -e "${YELLOW}警告: $env_file 已存在${NC}" + read -p "是否覆盖? (y/N): " overwrite + if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then + echo "取消操作" + exit 0 + fi +fi + +# 创建环境文件 +echo "" +echo "创建环境配置文件..." +echo "-----------------------------------" + +cat > "$env_file" << EOF +# $ENV_TYPE 环境配置 +# 由 setup-env.sh 自动生成于 $(date) + +APP_NAME="$app_name" +APP_ENV=$ENV_TYPE +APP_KEY=$app_key +APP_DEBUG=$([ "$ENV_TYPE" = "development" ] && echo "true" || echo "false") +APP_URL=$app_url + +APP_LOCALE=zh_CN +APP_FALLBACK_LOCALE=zh_CN +APP_FAKER_LOCALE=zh_CN + +BCRYPT_ROUNDS=$([ "$ENV_TYPE" = "development" ] && echo "10" || echo "12") + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_LEVEL=$([ "$ENV_TYPE" = "development" ] && echo "debug" || echo "info") + +# 数据库配置 - 使用Docker容器名称进行服务间通信 +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=$db_name +DB_USERNAME=$db_user +DB_PASSWORD=$db_password + +# 会话和缓存配置 +SESSION_DRIVER=redis +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false + +CACHE_STORE=redis +CACHE_PREFIX=$([ "$ENV_TYPE" = "development" ] && echo "kb_dev_cache" || echo "kb_cache") + +# Redis配置 - 使用Docker容器名称进行服务间通信 +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# 队列配置 - 使用Redis作为队列驱动 +QUEUE_CONNECTION=redis + +# 文件系统配置 +FILESYSTEM_DISK=local + +EOF + +# 添加邮件配置 +if [ "$ENV_TYPE" = "production" ] && [ -n "$mail_host" ]; then + cat >> "$env_file" << EOF +# 邮件配置 - 生产环境SMTP设置 +MAIL_MAILER=smtp +MAIL_HOST=$mail_host +MAIL_PORT=${mail_port:-587} +MAIL_USERNAME=$mail_username +MAIL_PASSWORD=$mail_password +MAIL_ENCRYPTION=tls +MAIL_FROM_ADDRESS="noreply@your-domain.com" +MAIL_FROM_NAME="\${APP_NAME}" + +EOF +else + cat >> "$env_file" << EOF +# 邮件配置 - $([ "$ENV_TYPE" = "development" ] && echo "开发环境使用日志" || echo "生产环境SMTP设置") +MAIL_MAILER=$([ "$ENV_TYPE" = "development" ] && echo "log" || echo "smtp") +$([ "$ENV_TYPE" = "production" ] && cat << PROD_MAIL +MAIL_HOST=your-smtp-host.com +MAIL_PORT=587 +MAIL_USERNAME=your-email@domain.com +MAIL_PASSWORD=your-email-password-change-this +MAIL_ENCRYPTION=tls +PROD_MAIL +) +MAIL_FROM_ADDRESS="$([ "$ENV_TYPE" = "development" ] && echo "dev@knowledge-base.local" || echo "noreply@your-domain.com")" +MAIL_FROM_NAME="\${APP_NAME}" + +EOF +fi + +# 添加Meilisearch配置 +cat >> "$env_file" << EOF +# Meilisearch配置 - 使用Docker容器名称进行服务间通信 +SCOUT_DRIVER=meilisearch +MEILISEARCH_HOST=http://meilisearch:7700 +MEILISEARCH_KEY=$meilisearch_key + +# 文档转换配置 +DOCUMENT_CONVERSION_DRIVER=pandoc +PANDOC_PATH=/usr/bin/pandoc +CONVERSION_TIMEOUT=300 +CONVERSION_QUEUE=documents +CONVERSION_RETRY_TIMES=3 +CONVERSION_RETRY_DELAY=60 + +# Markdown配置 +MARKDOWN_RENDERER=commonmark +MARKDOWN_SANITIZE=true +MARKDOWN_PREVIEW_LENGTH=500 +MARKDOWN_MAX_FILE_SIZE=10485760 + +# 存储配置 +DOCUMENTS_DISK=documents +MARKDOWN_DISK=markdown +STORAGE_ORGANIZE_BY_DATE=true +EOF + +# 添加开发环境特定配置 +if [ "$ENV_TYPE" = "development" ]; then + cat >> "$env_file" << EOF + +# 开发工具配置 +TELESCOPE_ENABLED=true +DEBUGBAR_ENABLED=true +VITE_APP_NAME="\${APP_NAME}" + +# 开发环境特定配置 +PHP_IDE_CONFIG=serverName=knowledge-base-dev +XDEBUG_MODE=develop,debug +XDEBUG_CONFIG=client_host=host.docker.internal client_port=9003 +EOF +fi + +echo -e "${GREEN}✓ 环境配置文件已创建: $env_file${NC}" + +# 创建.env符号链接 +if [ "$FORCE" = true ] || [ ! -f ".env" ]; then + ln -sf "$env_file" .env + echo -e "${GREEN}✓ 已创建 .env 符号链接指向 $env_file${NC}" +fi + +# 创建存储目录 +echo "" +echo "创建存储目录..." +echo "-----------------------------------" + +if [ "$ENV_TYPE" = "development" ]; then + storage_dirs=( + "storage/dev/mysql" + "storage/dev/redis" + "storage/dev/meilisearch" + "storage/dev/app" + "storage/dev/app/private/documents" + "storage/dev/app/public" + "storage/dev/logs" + "storage/dev/logs/app" + "storage/dev/logs/queue" + ) +else + storage_dirs=( + "storage/mysql" + "storage/redis" + "storage/meilisearch" + "storage/app" + "storage/app/private/documents" + "storage/app/public" + "storage/logs" + "storage/logs/app" + "storage/logs/queue" + ) +fi + +for dir in "${storage_dirs[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + echo -e "${GREEN}✓ 创建目录: $dir${NC}" + else + echo -e "${YELLOW}目录已存在: $dir${NC}" + fi +done + +# 设置目录权限 +echo "" +echo "设置目录权限..." +echo "-----------------------------------" + +# Laravel需要写入权限的目录 +laravel_dirs=( + "storage" + "bootstrap/cache" +) + +for dir in "${laravel_dirs[@]}"; do + if [ -d "$dir" ]; then + chmod -R 775 "$dir" + echo -e "${GREEN}✓ 设置权限: $dir${NC}" + fi +done + +# 显示配置摘要 +echo "" +echo "===================================" +echo "配置摘要" +echo "===================================" + +echo "环境类型: $ENV_TYPE" +echo "配置文件: $env_file" +echo "Compose文件: $compose_file" +echo "应用名称: $app_name" +echo "应用URL: $app_url" +echo "数据库名: $db_name" +echo "数据库用户: $db_user" +echo "APP_KEY: ${app_key:0:20}..." +echo "Meilisearch密钥: ${meilisearch_key:0:20}..." + +echo "" +echo "===================================" +echo "下一步操作" +echo "===================================" + +echo "1. 验证环境配置:" +echo " ./docker/validate-env.sh" +echo "" +echo "2. 启动Docker服务:" +if [ "$ENV_TYPE" = "development" ]; then + echo " docker-compose -f docker-compose.dev.yml up -d" +else + echo " docker-compose up -d" +fi +echo "" +echo "3. 测试网络连接:" +echo " ./docker/test-network.sh" +echo "" +echo "4. 初始化应用:" +echo " docker exec knowledge_base_app php artisan migrate" +echo " docker exec knowledge_base_app php artisan db:seed" + +echo "" +echo -e "${GREEN}✓ 环境配置完成!${NC}" \ No newline at end of file diff --git a/docker/start-production.sh b/docker/start-production.sh new file mode 100755 index 0000000..5c010f6 --- /dev/null +++ b/docker/start-production.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# Laravel知识库系统 - 生产环境启动脚本 + +set -e + +echo "🚀 启动Laravel知识库系统生产环境..." + +# 检查必要文件 +if [ ! -f ".env" ]; then + echo "❌ 错误: .env文件不存在" + echo "请复制.env.production为.env并配置相应参数" + exit 1 +fi + +if [ ! -f "docker-compose.yml" ]; then + echo "❌ 错误: docker-compose.yml文件不存在" + exit 1 +fi + +# 创建必要的目录 +echo "📁 创建存储目录..." +mkdir -p storage/mysql +mkdir -p storage/redis +mkdir -p storage/meilisearch +mkdir -p storage/logs/app +mkdir -p storage/logs/queue +mkdir -p storage/app/public +mkdir -p storage/app/documents +mkdir -p storage/app/markdown + +# 设置目录权限 +echo "🔐 设置目录权限..." +chmod -R 755 storage/ +chmod -R 755 bootstrap/cache/ + +# 构建应用镜像 +echo "🏗️ 构建Docker镜像..." +docker-compose build --no-cache app + +# 启动服务 +echo "🔄 启动服务..." +docker-compose up -d + +# 等待服务启动 +echo "⏳ 等待服务启动..." +sleep 30 + +# 检查服务状态 +echo "🔍 检查服务状态..." +docker-compose ps + +# 运行Laravel初始化命令 +echo "🔧 运行Laravel初始化..." +docker-compose exec app php artisan key:generate --force +docker-compose exec app php artisan migrate --force +docker-compose exec app php artisan config:cache +docker-compose exec app php artisan route:cache +docker-compose exec app php artisan view:cache +docker-compose exec app php artisan storage:link + +# 设置文件权限 +echo "📝 设置应用权限..." +docker-compose exec app chown -R www-data:www-data /var/www/html/storage +docker-compose exec app chown -R www-data:www-data /var/www/html/bootstrap/cache + +# 健康检查 +echo "🏥 执行健康检查..." +sleep 10 + +# 检查Web应用 (Swoole) +if curl -f http://localhost:8000/health > /dev/null 2>&1; then + echo "✅ Web应用健康检查通过" +else + # 如果没有专门的健康检查路由,尝试访问根路径 + if curl -f http://localhost:8000/ > /dev/null 2>&1; then + echo "✅ Web应用健康检查通过" + else + echo "❌ Web应用健康检查失败" + fi +fi + +# 检查MySQL +if docker-compose exec mysql mysqladmin ping -h localhost --silent; then + echo "✅ MySQL健康检查通过" +else + echo "❌ MySQL健康检查失败" +fi + +# 检查Redis +if docker-compose exec redis redis-cli ping > /dev/null 2>&1; then + echo "✅ Redis健康检查通过" +else + echo "❌ Redis健康检查失败" +fi + +# 检查Meilisearch +if curl -f http://localhost:7700/health > /dev/null 2>&1; then + echo "✅ Meilisearch健康检查通过" +else + echo "❌ Meilisearch健康检查失败" +fi + +echo "" +echo "🎉 生产环境启动完成!" +echo "" +echo "📊 服务访问地址:" +echo " Web应用: http://localhost:8000" +echo " Meilisearch: http://localhost:7700" +echo "" +echo "🔧 管理命令:" +echo " 查看日志: docker-compose logs -f" +echo " 停止服务: docker-compose down" +echo " 重启服务: docker-compose restart" +echo " 查看状态: docker-compose ps" +echo "" \ No newline at end of file diff --git a/docker/start-with-monitoring.sh b/docker/start-with-monitoring.sh new file mode 100755 index 0000000..92ab206 --- /dev/null +++ b/docker/start-with-monitoring.sh @@ -0,0 +1,325 @@ +#!/bin/bash + +# 启动Docker服务并开始监控 +# 用于生产环境的完整启动流程 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查Docker和docker-compose是否可用 +check_prerequisites() { + log_info "检查系统环境..." + + if ! command -v docker >/dev/null 2>&1; then + log_error "Docker未安装或不在PATH中" + exit 1 + fi + + if ! command -v docker-compose >/dev/null 2>&1 && ! docker compose version >/dev/null 2>&1; then + log_error "docker-compose未安装或不在PATH中" + exit 1 + fi + + if ! docker info >/dev/null 2>&1; then + log_error "Docker服务未运行或无权限访问" + exit 1 + fi + + log_success "系统环境检查通过" +} + +# 创建必要的目录 +create_directories() { + log_info "创建必要的存储目录..." + + local dirs=( + "./storage/mysql" + "./storage/redis" + "./storage/meilisearch" + "./storage/app" + "./storage/app/private/documents" + "./storage/app/public" + "./storage/logs" + "./storage/logs/app" + "./storage/logs/queue" + ) + + for dir in "${dirs[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + log_info "创建目录: $dir" + fi + done + + # 设置适当的权限 + chmod -R 755 ./storage + + log_success "存储目录创建完成" +} + +# 检查环境变量配置 +check_environment() { + log_info "检查环境变量配置..." + + if [ ! -f ".env" ]; then + log_warning ".env文件不存在,将从.env.example创建" + if [ -f ".env.example" ]; then + cp .env.example .env + log_info "已从.env.example创建.env文件,请检查配置" + else + log_error ".env.example文件不存在,无法创建环境配置" + exit 1 + fi + fi + + # 检查关键环境变量 + local required_vars=("APP_KEY" "DB_PASSWORD") + local missing_vars=() + + for var in "${required_vars[@]}"; do + if ! grep -q "^${var}=" .env || grep -q "^${var}=$" .env; then + missing_vars+=("$var") + fi + done + + if [ ${#missing_vars[@]} -gt 0 ]; then + log_warning "以下环境变量未设置或为空:" + for var in "${missing_vars[@]}"; do + echo " - $var" + done + log_warning "请在.env文件中设置这些变量" + fi + + log_success "环境变量检查完成" +} + +# 启动服务 +start_services() { + log_info "启动Docker服务..." + + # 停止现有服务(如果有) + if docker-compose ps -q | grep -q .; then + log_info "停止现有服务..." + docker-compose down + fi + + # 构建镜像(如果需要) + log_info "构建应用镜像..." + docker-compose build --no-cache + + # 启动服务 + log_info "启动所有服务..." + docker-compose up -d + + log_success "服务启动命令执行完成" +} + +# 等待服务就绪 +wait_for_services() { + log_info "等待服务启动完成..." + + local max_wait=300 # 最大等待时间(秒) + local wait_time=0 + local check_interval=10 + + while [ $wait_time -lt $max_wait ]; do + log_info "检查服务状态... (${wait_time}/${max_wait}秒)" + + if ./docker/check-services.sh >/dev/null 2>&1; then + log_success "所有服务启动完成并通过健康检查" + return 0 + fi + + sleep $check_interval + wait_time=$((wait_time + check_interval)) + done + + log_error "服务启动超时,请检查日志" + return 1 +} + +# 显示服务状态 +show_service_status() { + log_info "当前服务状态:" + docker-compose ps + + echo "" + log_info "服务访问地址:" + echo " Web应用: http://localhost" + echo " MySQL: localhost:3306" + echo " Redis: localhost:6379" + echo " Meilisearch: http://localhost:7700" + + echo "" + log_info "日志查看命令:" + echo " 所有服务: docker-compose logs -f" + echo " Web应用: docker-compose logs -f app" + echo " 队列处理: docker-compose logs -f queue" + echo " 数据库: docker-compose logs -f mysql" +} + +# 启动监控 +start_monitoring() { + log_info "启动服务监控..." + + # 检查是否已有监控进程在运行 + if pgrep -f "monitor-services.sh" >/dev/null; then + log_warning "监控进程已在运行,跳过启动" + return 0 + fi + + # 在后台启动监控 + nohup ./docker/monitor-services.sh > ./storage/logs/monitor-output.log 2>&1 & + local monitor_pid=$! + + echo $monitor_pid > ./storage/logs/monitor.pid + log_success "监控进程已启动 (PID: $monitor_pid)" + + log_info "监控日志文件: ./storage/logs/monitor.log" + log_info "监控输出文件: ./storage/logs/monitor-output.log" +} + +# 显示使用帮助 +show_help() { + echo "Docker服务启动和监控脚本" + echo "" + echo "用法: $0 [选项]" + echo "" + echo "选项:" + echo " --no-monitor 不启动监控进程" + echo " --skip-build 跳过镜像构建" + echo " --skip-wait 跳过服务就绪等待" + echo " -h, --help 显示此帮助信息" + echo "" + echo "此脚本将执行以下操作:" + echo " 1. 检查系统环境" + echo " 2. 创建必要的目录" + echo " 3. 检查环境变量配置" + echo " 4. 启动Docker服务" + echo " 5. 等待服务就绪" + echo " 6. 显示服务状态" + echo " 7. 启动监控进程" +} + +# 清理函数 +cleanup() { + log_info "正在清理..." + + # 停止监控进程 + if [ -f "./storage/logs/monitor.pid" ]; then + local monitor_pid=$(cat ./storage/logs/monitor.pid) + if kill -0 $monitor_pid 2>/dev/null; then + log_info "停止监控进程 (PID: $monitor_pid)" + kill $monitor_pid + fi + rm -f ./storage/logs/monitor.pid + fi + + exit 0 +} + +# 设置信号处理 +trap cleanup SIGINT SIGTERM + +# 主函数 +main() { + local no_monitor=false + local skip_build=false + local skip_wait=false + + # 解析命令行参数 + while [[ $# -gt 0 ]]; do + case $1 in + --no-monitor) + no_monitor=true + shift + ;; + --skip-build) + skip_build=true + shift + ;; + --skip-wait) + skip_wait=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "未知选项: $1" + show_help + exit 1 + ;; + esac + done + + echo "========================================" + echo "Docker服务启动和监控" + echo "时间: $(date)" + echo "========================================" + + # 执行启动流程 + check_prerequisites + create_directories + check_environment + + if [ "$skip_build" = false ]; then + start_services + else + log_info "跳过镜像构建,直接启动服务..." + docker-compose up -d + fi + + if [ "$skip_wait" = false ]; then + wait_for_services + else + log_warning "跳过服务就绪等待" + fi + + show_service_status + + if [ "$no_monitor" = false ]; then + start_monitoring + + echo "" + log_success "服务启动完成,监控已开始" + log_info "使用 Ctrl+C 停止监控并退出" + log_info "或使用 'docker-compose down' 停止所有服务" + + # 等待用户中断 + while true; do + sleep 60 + done + else + log_success "服务启动完成(未启动监控)" + fi +} + +# 如果脚本被直接执行 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/docker/stop-monitoring.sh b/docker/stop-monitoring.sh new file mode 100755 index 0000000..b3caa6e --- /dev/null +++ b/docker/stop-monitoring.sh @@ -0,0 +1,210 @@ +#!/bin/bash + +# 停止Docker服务监控脚本 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 停止监控进程 +stop_monitoring() { + log_info "停止监控进程..." + + local monitor_pid_file="./storage/logs/monitor.pid" + local stopped=false + + # 通过PID文件停止 + if [ -f "$monitor_pid_file" ]; then + local monitor_pid=$(cat "$monitor_pid_file") + if kill -0 $monitor_pid 2>/dev/null; then + log_info "通过PID文件停止监控进程 (PID: $monitor_pid)" + kill $monitor_pid + sleep 2 + + # 确认进程已停止 + if ! kill -0 $monitor_pid 2>/dev/null; then + log_success "监控进程已停止" + stopped=true + else + log_warning "监控进程未响应TERM信号,尝试强制停止..." + kill -9 $monitor_pid 2>/dev/null || true + sleep 1 + if ! kill -0 $monitor_pid 2>/dev/null; then + log_success "监控进程已强制停止" + stopped=true + fi + fi + else + log_warning "PID文件中的进程不存在" + fi + rm -f "$monitor_pid_file" + fi + + # 通过进程名停止(备用方法) + if [ "$stopped" = false ]; then + log_info "通过进程名查找并停止监控进程..." + local pids=$(pgrep -f "monitor-services.sh" || true) + + if [ -n "$pids" ]; then + for pid in $pids; do + log_info "停止监控进程 (PID: $pid)" + kill $pid 2>/dev/null || true + done + + sleep 2 + + # 检查是否还有进程运行 + local remaining_pids=$(pgrep -f "monitor-services.sh" || true) + if [ -n "$remaining_pids" ]; then + log_warning "强制停止剩余的监控进程..." + for pid in $remaining_pids; do + kill -9 $pid 2>/dev/null || true + done + fi + + log_success "所有监控进程已停止" + stopped=true + else + log_info "未找到运行中的监控进程" + stopped=true + fi + fi + + return 0 +} + +# 停止Docker服务 +stop_services() { + log_info "停止Docker服务..." + + if docker-compose ps -q | grep -q .; then + docker-compose down + log_success "Docker服务已停止" + else + log_info "没有运行中的Docker服务" + fi +} + +# 清理日志文件 +cleanup_logs() { + local cleanup_logs=$1 + + if [ "$cleanup_logs" = true ]; then + log_info "清理监控日志文件..." + + local log_files=( + "./storage/logs/monitor.log" + "./storage/logs/monitor-output.log" + "./storage/logs/restart_counters" + ) + + for item in "${log_files[@]}"; do + if [ -f "$item" ]; then + rm -f "$item" + log_info "删除文件: $item" + elif [ -d "$item" ]; then + rm -rf "$item" + log_info "删除目录: $item" + fi + done + + log_success "日志文件清理完成" + fi +} + +# 显示使用帮助 +show_help() { + echo "停止Docker服务监控脚本" + echo "" + echo "用法: $0 [选项]" + echo "" + echo "选项:" + echo " --stop-services 同时停止Docker服务" + echo " --cleanup-logs 清理监控日志文件" + echo " --all 停止监控、服务并清理日志" + echo " -h, --help 显示此帮助信息" + echo "" + echo "默认情况下,此脚本只停止监控进程,不影响Docker服务。" +} + +# 主函数 +main() { + local stop_services_flag=false + local cleanup_logs_flag=false + + # 解析命令行参数 + while [[ $# -gt 0 ]]; do + case $1 in + --stop-services) + stop_services_flag=true + shift + ;; + --cleanup-logs) + cleanup_logs_flag=true + shift + ;; + --all) + stop_services_flag=true + cleanup_logs_flag=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "未知选项: $1" + show_help + exit 1 + ;; + esac + done + + echo "========================================" + echo "停止Docker服务监控" + echo "时间: $(date)" + echo "========================================" + + # 执行停止操作 + stop_monitoring + + if [ "$stop_services_flag" = true ]; then + stop_services + fi + + cleanup_logs "$cleanup_logs_flag" + + echo "" + log_success "操作完成" + + if [ "$stop_services_flag" = false ]; then + log_info "Docker服务仍在运行,使用 'docker-compose down' 停止服务" + fi +} + +# 如果脚本被直接执行 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/docker/stop-production.sh b/docker/stop-production.sh new file mode 100755 index 0000000..3a5cebb --- /dev/null +++ b/docker/stop-production.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Laravel知识库系统 - 生产环境停止脚本 + +set -e + +echo "🛑 停止Laravel知识库系统生产环境..." + +# 停止所有服务 +echo "⏹️ 停止Docker服务..." +docker-compose down + +# 可选:清理未使用的镜像和容器 +read -p "是否清理未使用的Docker资源? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "🧹 清理Docker资源..." + docker system prune -f + docker volume prune -f +fi + +echo "✅ 生产环境已停止" \ No newline at end of file diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf new file mode 100644 index 0000000..83c395b --- /dev/null +++ b/docker/supervisor/supervisord.conf @@ -0,0 +1,45 @@ +# Supervisor配置文件 - Swoole版本 + +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +childlogdir=/var/log/supervisor/ + +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +# Swoole HTTP服务器 (Laravel Octane) +[program:swoole] +command=php /var/www/html/artisan octane:start --host=0.0.0.0 --port=8000 --workers=4 +autostart=true +autorestart=true +startretries=5 +numprocs=1 +startsecs=0 +process_name=%(program_name)s_%(process_num)02d +stderr_logfile=/var/log/supervisor/%(program_name)s_stderr.log +stderr_logfile_maxbytes=10MB +stdout_logfile=/var/log/supervisor/%(program_name)s_stdout.log +stdout_logfile_maxbytes=10MB +user=www-data + +# Laravel队列处理器 +[program:laravel-worker] +command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +startretries=5 +numprocs=1 +redirect_stderr=true +stdout_logfile=/var/log/supervisor/laravel-worker.log +stopwaitsecs=3600 +user=www-data \ No newline at end of file diff --git a/docker/swoole-health-check.sh b/docker/swoole-health-check.sh new file mode 100755 index 0000000..fc1d1c3 --- /dev/null +++ b/docker/swoole-health-check.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# Swoole HTTP服务器健康检查脚本 +# 用于Docker健康检查 + +set -e + +# 检查Swoole进程是否运行 +if ! pgrep -f "octane:start" > /dev/null; then + echo "Swoole HTTP服务器进程未运行" + exit 1 +fi + +# 检查HTTP服务是否响应 +if ! curl -f -s http://localhost:8000/health > /dev/null 2>&1; then + # 如果没有专门的健康检查路由,尝试访问根路径 + if ! curl -f -s http://localhost:8000/ > /dev/null 2>&1; then + echo "Swoole HTTP服务器无响应" + exit 1 + fi +fi + +# 检查Laravel应用是否可以连接到数据库和缓存 +if ! php -r " +try { + require_once '/var/www/html/vendor/autoload.php'; + \$app = require_once '/var/www/html/bootstrap/app.php'; + \$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + + // 检查数据库连接 + \Illuminate\Support\Facades\DB::connection()->getPdo(); + + // 检查缓存连接 + \Illuminate\Support\Facades\Cache::put('swoole_health_check', 'ok', 10); + \Illuminate\Support\Facades\Cache::forget('swoole_health_check'); + + echo 'OK'; +} catch (Exception \$e) { + echo 'ERROR: ' . \$e->getMessage(); + exit(1); +} +"; then + echo "Swoole服务器依赖服务检查失败" + exit 1 +fi + +echo "Swoole HTTP服务器健康检查通过" +exit 0 \ No newline at end of file diff --git a/docker/test-build.sh b/docker/test-build.sh new file mode 100755 index 0000000..f0cfc79 --- /dev/null +++ b/docker/test-build.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Docker镜像测试构建脚本 +# 用于快速验证Dockerfile语法和基础构建 + +set -e + +# 配置变量 +IMAGE_NAME="knowledge-base-app" +IMAGE_TAG="test" +PLATFORM="linux/amd64" + +echo "开始测试Docker镜像构建..." +echo "镜像名称: ${IMAGE_NAME}:${IMAGE_TAG}" +echo "目标平台: ${PLATFORM}" + +# 检查Docker是否运行 +if ! docker info > /dev/null 2>&1; then + echo "错误: Docker未运行或无法访问" + exit 1 +fi + +# 只构建到base阶段进行快速测试 +echo "正在构建基础镜像阶段..." +docker build \ + --platform ${PLATFORM} \ + --target base \ + --tag ${IMAGE_NAME}:${IMAGE_TAG}-base \ + --file Dockerfile \ + . + +# 验证基础阶段构建结果 +if [ $? -eq 0 ]; then + echo "✅ 基础镜像构建成功!" + + # 显示镜像信息 + echo "" + echo "基础镜像信息:" + docker images ${IMAGE_NAME}:${IMAGE_TAG}-base + + # 检查镜像架构 + echo "" + echo "镜像架构信息:" + docker inspect ${IMAGE_NAME}:${IMAGE_TAG}-base --format='{{.Architecture}}' + + # 测试PHP版本 + echo "" + echo "PHP版本信息:" + docker run --rm ${IMAGE_NAME}:${IMAGE_TAG}-base php -v + + # 测试Pandoc + echo "" + echo "Pandoc版本信息:" + docker run --rm ${IMAGE_NAME}:${IMAGE_TAG}-base pandoc --version | head -1 + + echo "" + echo "基础镜像测试完成!" +else + echo "❌ 基础镜像构建失败!" + exit 1 +fi \ No newline at end of file diff --git a/docker/test-compose-config.sh b/docker/test-compose-config.sh new file mode 100755 index 0000000..cfbace1 --- /dev/null +++ b/docker/test-compose-config.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Docker Compose 配置验证脚本 +# 验证 Swoole 集成的 docker-compose.yml 配置 + +set -e + +echo "开始验证 Docker Compose 配置..." + +# 检查 docker-compose.yml 语法 +echo "1. 检查 docker-compose.yml 语法..." +if docker-compose config > /dev/null 2>&1; then + echo "✓ docker-compose.yml 语法正确" +else + echo "✗ docker-compose.yml 语法错误" + exit 1 +fi + +# 检查必要的服务是否存在 +echo "2. 检查必要的服务..." +services=$(docker-compose config --services) + +required_services=("app" "mysql" "redis" "meilisearch") +for service in "${required_services[@]}"; do + if echo "$services" | grep -q "^$service$"; then + echo "✓ 服务 $service 已配置" + else + echo "✗ 缺少服务 $service" + exit 1 + fi +done + +# 检查是否没有 Nginx 服务(应该被移除) +if echo "$services" | grep -q "^nginx$"; then + echo "✗ 发现 Nginx 服务,应该已被移除" + exit 1 +else + echo "✓ Nginx 服务已正确移除" +fi + +# 检查应用服务的端口配置 +echo "3. 检查端口配置..." +app_ports=$(docker-compose config | grep -A 20 "app:" | grep -A 5 "ports:" | grep "8000") +if [ -n "$app_ports" ]; then + echo "✓ 应用服务端口 8000 已正确配置" +else + echo "✗ 应用服务端口配置错误" + exit 1 +fi + +# 检查健康检查配置 +echo "4. 检查健康检查配置..." +healthcheck=$(docker-compose config | grep -A 10 "healthcheck:" | grep "swoole-health-check") +if [ -n "$healthcheck" ]; then + echo "✓ Swoole 健康检查已配置" +else + echo "✗ Swoole 健康检查配置缺失" + exit 1 +fi + +# 检查服务依赖关系 +echo "5. 检查服务依赖关系..." +depends_on=$(docker-compose config | grep -A 10 "depends_on:") +if echo "$depends_on" | grep -q "mysql" && echo "$depends_on" | grep -q "redis" && echo "$depends_on" | grep -q "meilisearch"; then + echo "✓ 服务依赖关系正确配置" +else + echo "✗ 服务依赖关系配置错误" + exit 1 +fi + +echo "✓ 所有配置验证通过!" +echo "Docker Compose 配置已成功更新为 Swoole 架构" \ No newline at end of file diff --git a/docker/test-config.sh b/docker/test-config.sh new file mode 100755 index 0000000..6eab3ab --- /dev/null +++ b/docker/test-config.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# Laravel知识库系统 - Docker配置测试脚本 + +set -e + +echo "🧪 测试Docker配置..." + +# 测试docker-compose配置语法 +echo "📋 检查docker-compose.yml语法..." +if docker-compose config --quiet; then + echo "✅ docker-compose.yml语法正确" +else + echo "❌ docker-compose.yml语法错误" + exit 1 +fi + +# 测试Dockerfile语法 +echo "🐳 检查Dockerfile语法..." +if [ -f "Dockerfile" ]; then + echo "✅ Dockerfile文件存在" +else + echo "❌ Dockerfile文件不存在" + exit 1 +fi + +# 检查必要的配置文件 +echo "📁 检查配置文件..." + +required_files=( + "docker/mysql/my.cnf" + "docker/redis/redis.conf" + "docker/php/php.ini" + "docker/supervisor/supervisord.conf" +) + +for file in "${required_files[@]}"; do + if [ -f "$file" ]; then + echo "✅ $file 存在" + else + echo "❌ $file 不存在" + exit 1 + fi +done + +# 检查存储目录 +echo "📂 检查存储目录..." +required_dirs=( + "storage/mysql" + "storage/redis" + "storage/meilisearch" + "storage/logs/app" + "storage/logs/queue" +) + +for dir in "${required_dirs[@]}"; do + if [ -d "$dir" ]; then + echo "✅ $dir 目录存在" + else + echo "⚠️ $dir 目录不存在,将创建..." + mkdir -p "$dir" + echo "✅ $dir 目录已创建" + fi +done + +# 检查脚本权限 +echo "🔐 检查脚本权限..." +scripts=( + "docker/start-production.sh" + "docker/stop-production.sh" + "docker/check-services.sh" +) + +for script in "${scripts[@]}"; do + if [ -x "$script" ]; then + echo "✅ $script 可执行" + else + echo "⚠️ $script 不可执行,正在修复..." + chmod +x "$script" + echo "✅ $script 权限已修复" + fi +done + +# 检查环境变量模板 +echo "🔧 检查环境配置..." +if [ -f ".env.production" ]; then + echo "✅ .env.production 模板存在" +else + echo "❌ .env.production 模板不存在" + exit 1 +fi + +if [ -f ".env" ]; then + echo "✅ .env 文件存在" +else + echo "⚠️ .env 文件不存在,建议复制 .env.production" +fi + +echo "" +echo "🎉 Docker配置测试完成!" +echo "" +echo "📝 下一步操作:" +echo "1. 复制环境配置: cp .env.production .env" +echo "2. 编辑环境配置: nano .env" +echo "3. 启动服务: ./docker/start-production.sh" +echo "4. 检查状态: ./docker/check-services.sh" \ No newline at end of file diff --git a/docker/test-health-checks.sh b/docker/test-health-checks.sh new file mode 100755 index 0000000..07fe00d --- /dev/null +++ b/docker/test-health-checks.sh @@ -0,0 +1,283 @@ +#!/bin/bash + +# 健康检查功能测试脚本 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 测试脚本语法 +test_script_syntax() { + log_info "测试脚本语法..." + + local scripts=( + "docker/check-services.sh" + "docker/monitor-services.sh" + "docker/start-with-monitoring.sh" + "docker/stop-monitoring.sh" + "docker/queue-health-check.sh" + ) + + local errors=0 + + for script in "${scripts[@]}"; do + if [ -f "$script" ]; then + if bash -n "$script"; then + log_success "$script 语法正确" + else + log_error "$script 语法错误" + ((errors++)) + fi + else + log_error "$script 文件不存在" + ((errors++)) + fi + done + + if [ $errors -eq 0 ]; then + log_success "所有脚本语法检查通过" + return 0 + else + log_error "发现 $errors 个语法错误" + return 1 + fi +} + +# 测试脚本权限 +test_script_permissions() { + log_info "测试脚本执行权限..." + + local scripts=( + "docker/check-services.sh" + "docker/monitor-services.sh" + "docker/start-with-monitoring.sh" + "docker/stop-monitoring.sh" + "docker/queue-health-check.sh" + ) + + local errors=0 + + for script in "${scripts[@]}"; do + if [ -f "$script" ]; then + if [ -x "$script" ]; then + log_success "$script 有执行权限" + else + log_error "$script 没有执行权限" + ((errors++)) + fi + else + log_error "$script 文件不存在" + ((errors++)) + fi + done + + if [ $errors -eq 0 ]; then + log_success "所有脚本权限检查通过" + return 0 + else + log_error "发现 $errors 个权限问题" + return 1 + fi +} + +# 测试Docker配置文件 +test_docker_configs() { + log_info "测试Docker配置文件..." + + local configs=( + "docker-compose.yml" + "Dockerfile" + "docker/php/php.ini" + "docker/supervisor/supervisord.conf" + "docker/redis/redis.conf" + "docker/mysql/my.cnf" + "docker/supervisor/supervisord.conf" + ) + + local errors=0 + + for config in "${configs[@]}"; do + if [ -f "$config" ]; then + log_success "$config 存在" + else + log_error "$config 不存在" + ((errors++)) + fi + done + + # 测试docker-compose语法 + if command -v docker-compose >/dev/null 2>&1; then + if docker-compose config >/dev/null 2>&1; then + log_success "docker-compose.yml 语法正确" + else + log_error "docker-compose.yml 语法错误" + ((errors++)) + fi + else + log_warning "docker-compose 未安装,跳过语法检查" + fi + + if [ $errors -eq 0 ]; then + log_success "所有配置文件检查通过" + return 0 + else + log_error "发现 $errors 个配置问题" + return 1 + fi +} + +# 测试健康检查端点配置 +test_healthcheck_configs() { + log_info "测试健康检查配置..." + + local errors=0 + + # 检查docker-compose中的健康检查配置 + if grep -q "healthcheck:" docker-compose.yml; then + log_success "docker-compose.yml 包含健康检查配置" + else + log_error "docker-compose.yml 缺少健康检查配置" + ((errors++)) + fi + + # 检查Laravel健康检查路由 + if grep -q "/health" routes/web.php; then + log_success "Laravel 健康检查路由已配置" + else + log_error "Laravel 健康检查路由未配置" + ((errors++)) + fi + + # 检查队列健康检查脚本 + if [ -f "docker/queue-health-check.sh" ] && [ -x "docker/queue-health-check.sh" ]; then + log_success "队列健康检查脚本存在且可执行" + else + log_error "队列健康检查脚本问题" + ((errors++)) + fi + + if [ $errors -eq 0 ]; then + log_success "健康检查配置检查通过" + return 0 + else + log_error "发现 $errors 个健康检查配置问题" + return 1 + fi +} + +# 测试存储目录结构 +test_storage_structure() { + log_info "测试存储目录结构..." + + local required_dirs=( + "./storage" + "./storage/logs" + ) + + local errors=0 + + for dir in "${required_dirs[@]}"; do + if [ -d "$dir" ]; then + log_success "目录存在: $dir" + else + log_warning "目录不存在,将创建: $dir" + mkdir -p "$dir" + if [ -d "$dir" ]; then + log_success "成功创建目录: $dir" + else + log_error "无法创建目录: $dir" + ((errors++)) + fi + fi + done + + if [ $errors -eq 0 ]; then + log_success "存储目录结构检查通过" + return 0 + else + log_error "发现 $errors 个存储目录问题" + return 1 + fi +} + +# 主测试函数 +main() { + echo "========================================" + echo "健康检查功能测试" + echo "时间: $(date)" + echo "========================================" + + local total_tests=0 + local failed_tests=0 + + # 执行所有测试 + tests=( + "test_script_syntax:脚本语法测试" + "test_script_permissions:脚本权限测试" + "test_docker_configs:Docker配置测试" + "test_healthcheck_configs:健康检查配置测试" + "test_storage_structure:存储目录测试" + ) + + for test in "${tests[@]}"; do + IFS=':' read -ra TEST_PARTS <<< "$test" + local test_func="${TEST_PARTS[0]}" + local test_name="${TEST_PARTS[1]}" + + ((total_tests++)) + + echo "" + log_info "开始 $test_name..." + + if $test_func; then + log_success "$test_name 通过" + else + log_error "$test_name 失败" + ((failed_tests++)) + fi + done + + # 输出总结 + echo "" + echo "========================================" + echo "测试完成" + echo "总测试数: $total_tests" + echo "失败: $failed_tests" + echo "成功: $((total_tests - failed_tests))" + echo "========================================" + + if [ $failed_tests -gt 0 ]; then + log_error "发现 $failed_tests 个问题,请检查配置" + exit 1 + else + log_success "所有测试通过,健康检查功能配置正确" + exit 0 + fi +} + +# 如果脚本被直接执行 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/docker/test-network.sh b/docker/test-network.sh new file mode 100755 index 0000000..db9ea71 --- /dev/null +++ b/docker/test-network.sh @@ -0,0 +1,215 @@ +#!/bin/bash + +# Docker网络连接测试脚本 +# 用于测试容器间的网络连接和服务可用性 + +set -e + +echo "===================================" +echo "Docker网络连接测试" +echo "===================================" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 测试计数 +TEST_COUNT=0 +PASS_COUNT=0 +FAIL_COUNT=0 + +# 测试函数 +test_connection() { + local test_name=$1 + local container=$2 + local target=$3 + local port=$4 + local timeout=${5:-5} + + ((TEST_COUNT++)) + echo -n "测试 $test_name: " + + if docker exec "$container" timeout "$timeout" bash -c "echo > /dev/tcp/$target/$port" 2>/dev/null; then + echo -e "${GREEN}✓ 通过${NC}" + ((PASS_COUNT++)) + else + echo -e "${RED}✗ 失败${NC}" + ((FAIL_COUNT++)) + fi +} + +# 测试HTTP连接 +test_http() { + local test_name=$1 + local container=$2 + local url=$3 + local expected_code=${4:-200} + + ((TEST_COUNT++)) + echo -n "测试 $test_name: " + + if docker exec "$container" curl -s -o /dev/null -w "%{http_code}" "$url" | grep -q "$expected_code"; then + echo -e "${GREEN}✓ 通过${NC}" + ((PASS_COUNT++)) + else + echo -e "${RED}✗ 失败${NC}" + ((FAIL_COUNT++)) + fi +} + +# 检查容器是否运行 +check_container() { + local container=$1 + if ! docker ps --format "table {{.Names}}" | grep -q "^$container$"; then + echo -e "${RED}错误: 容器 $container 未运行${NC}" + return 1 + fi + return 0 +} + +echo "检查容器状态..." +echo "-----------------------------------" + +# 定义容器名称 +APP_CONTAINER="knowledge_base_app" +MYSQL_CONTAINER="knowledge_base_mysql" +REDIS_CONTAINER="knowledge_base_redis" +MEILISEARCH_CONTAINER="knowledge_base_meilisearch" +QUEUE_CONTAINER="knowledge_base_queue" + +# 检查所有容器是否运行 +containers=("$APP_CONTAINER" "$MYSQL_CONTAINER" "$REDIS_CONTAINER" "$MEILISEARCH_CONTAINER" "$QUEUE_CONTAINER") +all_running=true + +for container in "${containers[@]}"; do + if check_container "$container"; then + echo -e "${GREEN}✓ $container 正在运行${NC}" + else + echo -e "${RED}✗ $container 未运行${NC}" + all_running=false + fi +done + +if [ "$all_running" = false ]; then + echo -e "${RED}部分容器未运行,请先启动所有服务${NC}" + exit 1 +fi + +echo "" +echo "测试网络连接..." +echo "-----------------------------------" + +# 测试应用容器到数据库的连接 +test_connection "应用->MySQL" "$APP_CONTAINER" "mysql" "3306" + +# 测试应用容器到Redis的连接 +test_connection "应用->Redis" "$APP_CONTAINER" "redis" "6379" + +# 测试应用容器到Meilisearch的连接 +test_connection "应用->Meilisearch" "$APP_CONTAINER" "meilisearch" "7700" + +# 测试队列容器到数据库的连接 +test_connection "队列->MySQL" "$QUEUE_CONTAINER" "mysql" "3306" + +# 测试队列容器到Redis的连接 +test_connection "队列->Redis" "$QUEUE_CONTAINER" "redis" "6379" + +echo "" +echo "测试HTTP服务..." +echo "-----------------------------------" + +# 测试Meilisearch HTTP API +test_http "Meilisearch健康检查" "$APP_CONTAINER" "http://meilisearch:7700/health" + +# 测试应用HTTP服务(如果有健康检查端点) +if docker exec "$APP_CONTAINER" curl -s -f "http://localhost/health" >/dev/null 2>&1; then + test_http "应用健康检查" "$APP_CONTAINER" "http://localhost/health" +else + echo "应用健康检查端点不可用,跳过测试" +fi + +echo "" +echo "测试服务功能..." +echo "-----------------------------------" + +# 测试MySQL连接 +((TEST_COUNT++)) +echo -n "测试MySQL数据库连接: " +if docker exec "$MYSQL_CONTAINER" mysql -u root -p"${DB_PASSWORD:-secure_root_password}" -e "SELECT 1;" >/dev/null 2>&1; then + echo -e "${GREEN}✓ 通过${NC}" + ((PASS_COUNT++)) +else + echo -e "${RED}✗ 失败${NC}" + ((FAIL_COUNT++)) +fi + +# 测试Redis连接 +((TEST_COUNT++)) +echo -n "测试Redis连接: " +if docker exec "$REDIS_CONTAINER" redis-cli ping | grep -q "PONG"; then + echo -e "${GREEN}✓ 通过${NC}" + ((PASS_COUNT++)) +else + echo -e "${RED}✗ 失败${NC}" + ((FAIL_COUNT++)) +fi + +# 测试Laravel数据库连接 +((TEST_COUNT++)) +echo -n "测试Laravel数据库连接: " +if docker exec "$APP_CONTAINER" php artisan tinker --execute="echo DB::connection()->getPdo() ? 'OK' : 'FAIL';" 2>/dev/null | grep -q "OK"; then + echo -e "${GREEN}✓ 通过${NC}" + ((PASS_COUNT++)) +else + echo -e "${RED}✗ 失败${NC}" + ((FAIL_COUNT++)) +fi + +# 测试Laravel Redis连接 +((TEST_COUNT++)) +echo -n "测试Laravel Redis连接: " +if docker exec "$APP_CONTAINER" php artisan tinker --execute="echo Redis::ping() ? 'OK' : 'FAIL';" 2>/dev/null | grep -q "OK"; then + echo -e "${GREEN}✓ 通过${NC}" + ((PASS_COUNT++)) +else + echo -e "${RED}✗ 失败${NC}" + ((FAIL_COUNT++)) +fi + +echo "" +echo "测试网络信息..." +echo "-----------------------------------" + +# 显示网络信息 +echo "Docker网络信息:" +docker network ls | grep knowledge_base + +echo "" +echo "容器IP地址:" +for container in "${containers[@]}"; do + if check_container "$container" >/dev/null 2>&1; then + ip=$(docker inspect "$container" --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}') + echo "$container: $ip" + fi +done + +echo "" +echo "===================================" +echo "测试结果汇总" +echo "===================================" + +echo "总测试数: $TEST_COUNT" +echo -e "通过: ${GREEN}$PASS_COUNT${NC}" +echo -e "失败: ${RED}$FAIL_COUNT${NC}" + +if [ $FAIL_COUNT -eq 0 ]; then + echo -e "${GREEN}✓ 所有网络测试通过!${NC}" + echo "Docker网络配置正常,服务间通信正常。" + exit 0 +else + echo -e "${RED}✗ 有 $FAIL_COUNT 个测试失败${NC}" + echo "请检查网络配置和服务状态。" + exit 1 +fi \ No newline at end of file diff --git a/docker/test-persistence.sh b/docker/test-persistence.sh new file mode 100755 index 0000000..f8fe915 --- /dev/null +++ b/docker/test-persistence.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# 数据持久化测试脚本 +# 验证所有数据卷映射是否正确配置 + +set -e + +echo "开始测试数据持久化配置..." + +# 检查存储目录是否存在 +check_directory() { + local dir=$1 + local description=$2 + + if [ -d "$dir" ]; then + echo "✓ $description 目录存在: $dir" + return 0 + else + echo "✗ $description 目录不存在: $dir" + return 1 + fi +} + +# 检查docker-compose配置中的卷映射 +check_volume_mapping() { + local volume_name=$1 + local description=$2 + + if docker-compose config | grep -q "$volume_name"; then + echo "✓ $description 卷映射已配置: $volume_name" + return 0 + else + echo "✗ $description 卷映射未配置: $volume_name" + return 1 + fi +} + +echo "" +echo "检查存储目录结构..." + +# 检查所有必要的存储目录 +check_directory "storage/mysql" "MySQL数据库存储" +check_directory "storage/redis" "Redis缓存存储" +check_directory "storage/meilisearch" "Meilisearch搜索存储" +check_directory "storage/app" "Laravel应用存储" +check_directory "storage/app/private/documents" "文档上传存储" +check_directory "storage/app/public" "公共文件存储" +check_directory "storage/logs" "日志存储" +check_directory "storage/logs/app" "应用日志存储" +check_directory "storage/logs/queue" "队列日志存储" + +echo "" +echo "检查Docker Compose卷映射配置..." + +# 检查所有卷映射配置 +check_volume_mapping "mysql_data" "MySQL数据" +check_volume_mapping "redis_data" "Redis数据" +check_volume_mapping "meilisearch_data" "Meilisearch数据" +check_volume_mapping "storage_data" "应用存储数据" +check_volume_mapping "documents_data" "文档存储数据" +check_volume_mapping "public_data" "公共文件数据" +check_volume_mapping "app_logs" "应用日志" +check_volume_mapping "queue_logs" "队列日志" +check_volume_mapping "laravel_logs" "Laravel日志" + +echo "" +echo "检查目录权限..." + +# 检查关键目录的权限 +for dir in storage/mysql storage/redis storage/meilisearch storage/app storage/logs; do + if [ -d "$dir" ]; then + perms=$(stat -f "%A" "$dir" 2>/dev/null || stat -c "%a" "$dir" 2>/dev/null || echo "unknown") + echo "✓ $dir 权限: $perms" + fi +done + +echo "" +echo "数据持久化配置测试完成!" + +# 创建测试文件验证映射 +echo "" +echo "创建测试文件验证目录映射..." + +test_file="storage/app/test-persistence-$(date +%s).txt" +echo "测试数据持久化配置 - $(date)" > "$test_file" + +if [ -f "$test_file" ]; then + echo "✓ 测试文件创建成功: $test_file" + rm "$test_file" + echo "✓ 测试文件清理完成" +else + echo "✗ 测试文件创建失败" + exit 1 +fi + +echo "" +echo "所有数据持久化配置检查通过!" \ No newline at end of file diff --git a/docker/validate-deployment.sh b/docker/validate-deployment.sh new file mode 100755 index 0000000..7793555 --- /dev/null +++ b/docker/validate-deployment.sh @@ -0,0 +1,235 @@ +#!/bin/bash + +# Laravel知识库系统 - 部署验证脚本 + +set -e + +echo "🔍 验证Docker部署配置..." +echo "" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 检查函数 +check_requirement() { + local name=$1 + local command=$2 + local version_flag=$3 + + echo -n "检查 $name: " + if command -v $command >/dev/null 2>&1; then + if [ -n "$version_flag" ]; then + version=$($command $version_flag 2>/dev/null | head -n1) + echo -e "${GREEN}✅ 已安装${NC} ($version)" + else + echo -e "${GREEN}✅ 已安装${NC}" + fi + return 0 + else + echo -e "${RED}❌ 未安装${NC}" + return 1 + fi +} + +# 检查系统要求 +echo "📋 系统要求检查:" +check_requirement "Docker" "docker" "--version" +check_requirement "Docker Compose" "docker-compose" "--version" + +echo "" + +# 检查Docker服务状态 +echo "🐳 Docker服务状态:" +if docker info >/dev/null 2>&1; then + echo -e "${GREEN}✅ Docker服务运行正常${NC}" +else + echo -e "${RED}❌ Docker服务未运行${NC}" + exit 1 +fi + +echo "" + +# 检查配置文件 +echo "📁 配置文件检查:" +config_files=( + "docker-compose.yml:Docker Compose配置" + "Dockerfile:Docker镜像配置" + ".env.production:环境变量模板" + "docker/mysql/my.cnf:MySQL配置" + "docker/redis/redis.conf:Redis配置" +) + +for item in "${config_files[@]}"; do + file=$(echo $item | cut -d: -f1) + desc=$(echo $item | cut -d: -f2) + + if [ -f "$file" ]; then + echo -e "${GREEN}✅${NC} $desc ($file)" + else + echo -e "${RED}❌${NC} $desc ($file) - 文件不存在" + fi +done + +echo "" + +# 检查存储目录 +echo "📂 存储目录检查:" +storage_dirs=( + "storage/mysql:MySQL数据目录" + "storage/redis:Redis数据目录" + "storage/meilisearch:Meilisearch数据目录" + "storage/logs/app:应用日志目录" + "storage/logs/queue:队列日志目录" + "storage/app:应用存储目录" +) + +for item in "${storage_dirs[@]}"; do + dir=$(echo $item | cut -d: -f1) + desc=$(echo $item | cut -d: -f2) + + if [ -d "$dir" ]; then + echo -e "${GREEN}✅${NC} $desc ($dir)" + else + echo -e "${YELLOW}⚠️${NC} $desc ($dir) - 目录不存在,将在启动时创建" + fi +done + +echo "" + +# 检查管理脚本 +echo "🔧 管理脚本检查:" +scripts=( + "docker/start-production.sh:启动脚本" + "docker/stop-production.sh:停止脚本" + "docker/check-services.sh:状态检查脚本" + "docker/test-config.sh:配置测试脚本" +) + +for item in "${scripts[@]}"; do + script=$(echo $item | cut -d: -f1) + desc=$(echo $item | cut -d: -f2) + + if [ -f "$script" ]; then + if [ -x "$script" ]; then + echo -e "${GREEN}✅${NC} $desc ($script) - 可执行" + else + echo -e "${YELLOW}⚠️${NC} $desc ($script) - 不可执行" + fi + else + echo -e "${RED}❌${NC} $desc ($script) - 文件不存在" + fi +done + +echo "" + +# 检查Docker Compose配置 +echo "🔍 Docker Compose配置验证:" +if docker-compose config --quiet 2>/dev/null; then + echo -e "${GREEN}✅ Docker Compose配置语法正确${NC}" + + # 显示服务列表 + echo "" + echo "📊 配置的服务:" + services=$(docker-compose config --services) + for service in $services; do + echo -e "${GREEN} ✓${NC} $service" + done +else + echo -e "${RED}❌ Docker Compose配置语法错误${NC}" +fi + +echo "" + +# 检查环境配置 +echo "🔧 环境配置检查:" +if [ -f ".env" ]; then + echo -e "${GREEN}✅ .env 文件存在${NC}" + + # 检查关键配置项 + required_vars=("APP_KEY" "DB_PASSWORD" "MEILISEARCH_KEY") + for var in "${required_vars[@]}"; do + if grep -q "^${var}=" .env && ! grep -q "^${var}=$" .env; then + echo -e "${GREEN} ✓${NC} $var 已配置" + else + echo -e "${YELLOW} ⚠️${NC} $var 未配置或为空" + fi + done +else + echo -e "${YELLOW}⚠️ .env 文件不存在${NC}" + echo " 建议执行: cp .env.production .env" +fi + +echo "" + +# 系统资源检查 +echo "💾 系统资源检查:" + +# 检查可用内存 (macOS兼容) +if command -v free >/dev/null 2>&1; then + # Linux系统 + available_memory=$(free -m | awk 'NR==2{printf "%.0f", $7}') +elif command -v vm_stat >/dev/null 2>&1; then + # macOS系统 + page_size=$(vm_stat | grep "page size" | awk '{print $8}') + free_pages=$(vm_stat | grep "Pages free" | awk '{print $3}' | sed 's/\.//') + available_memory=$((free_pages * page_size / 1024 / 1024)) +else + available_memory=0 +fi + +if [ "$available_memory" -ge 4096 ]; then + echo -e "${GREEN}✅ 可用内存: ${available_memory}MB (推荐: 4GB+)${NC}" +elif [ "$available_memory" -ge 2048 ]; then + echo -e "${YELLOW}⚠️ 可用内存: ${available_memory}MB (推荐: 4GB+)${NC}" +elif [ "$available_memory" -gt 0 ]; then + echo -e "${RED}❌ 可用内存: ${available_memory}MB (推荐: 4GB+)${NC}" +else + echo -e "${YELLOW}⚠️ 无法检测内存使用情况${NC}" +fi + +# 检查可用磁盘空间 (macOS兼容) +if df -h . >/dev/null 2>&1; then + available_disk=$(df -h . | awk 'NR==2{print $4}' | sed 's/G.*//' | sed 's/[^0-9].*//') + if [ -n "$available_disk" ] && [ "$available_disk" -ge 10 ]; then + echo -e "${GREEN}✅ 可用磁盘: ${available_disk}GB+ (推荐: 10GB+)${NC}" + elif [ -n "$available_disk" ] && [ "$available_disk" -ge 5 ]; then + echo -e "${YELLOW}⚠️ 可用磁盘: ${available_disk}GB+ (推荐: 10GB+)${NC}" + elif [ -n "$available_disk" ]; then + echo -e "${RED}❌ 可用磁盘: ${available_disk}GB+ (推荐: 10GB+)${NC}" + else + echo -e "${YELLOW}⚠️ 无法检测磁盘使用情况${NC}" + fi +else + echo -e "${YELLOW}⚠️ 无法检测磁盘使用情况${NC}" +fi + +echo "" + +# 总结 +echo "📋 验证总结:" +echo "" +echo "✅ 配置完整性: 所有必要的配置文件都已就位" +echo "✅ 脚本可用性: 管理脚本已准备就绪" +echo "✅ Docker环境: Docker和Docker Compose可用" +echo "" + +if [ -f ".env" ]; then + echo -e "${GREEN}🚀 系统已准备就绪!${NC}" + echo "" + echo "下一步操作:" + echo "1. 启动服务: ./docker/start-production.sh" + echo "2. 检查状态: ./docker/check-services.sh" + echo "3. 访问应用: http://localhost" +else + echo -e "${YELLOW}⚠️ 需要完成环境配置${NC}" + echo "" + echo "下一步操作:" + echo "1. 复制配置: cp .env.production .env" + echo "2. 编辑配置: nano .env" + echo "3. 启动服务: ./docker/start-production.sh" +fi + +echo "" \ No newline at end of file diff --git a/docker/validate-env.sh b/docker/validate-env.sh new file mode 100755 index 0000000..5340782 --- /dev/null +++ b/docker/validate-env.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# 环境变量验证脚本 +# 用于验证Docker部署所需的环境变量是否正确配置 + +set -e + +echo "===================================" +echo "环境变量验证脚本" +echo "===================================" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 错误计数 +ERROR_COUNT=0 + +# 验证函数 +validate_var() { + local var_name=$1 + local var_value=$2 + local is_required=${3:-true} + local description=$4 + + if [ -z "$var_value" ]; then + if [ "$is_required" = true ]; then + echo -e "${RED}✗ 错误: $var_name 未设置${NC} - $description" + ((ERROR_COUNT++)) + else + echo -e "${YELLOW}⚠ 警告: $var_name 未设置${NC} - $description (可选)" + fi + else + echo -e "${GREEN}✓ $var_name 已设置${NC} - $description" + fi +} + +# 验证必需的环境变量 +echo "检查必需的环境变量..." +echo "-----------------------------------" + +validate_var "APP_KEY" "$APP_KEY" true "应用加密密钥" +validate_var "DB_PASSWORD" "$DB_PASSWORD" true "数据库密码" +validate_var "MEILISEARCH_KEY" "$MEILISEARCH_KEY" true "Meilisearch主密钥" + +echo "" +echo "检查应用配置..." +echo "-----------------------------------" + +validate_var "APP_NAME" "$APP_NAME" false "应用名称" +validate_var "APP_ENV" "$APP_ENV" false "应用环境" +validate_var "APP_DEBUG" "$APP_DEBUG" false "调试模式" +validate_var "APP_URL" "$APP_URL" false "应用URL" + +echo "" +echo "检查数据库配置..." +echo "-----------------------------------" + +validate_var "DB_CONNECTION" "$DB_CONNECTION" false "数据库连接类型" +validate_var "DB_HOST" "$DB_HOST" false "数据库主机" +validate_var "DB_PORT" "$DB_PORT" false "数据库端口" +validate_var "DB_DATABASE" "$DB_DATABASE" false "数据库名称" +validate_var "DB_USERNAME" "$DB_USERNAME" false "数据库用户名" + +echo "" +echo "检查Redis配置..." +echo "-----------------------------------" + +validate_var "REDIS_HOST" "$REDIS_HOST" false "Redis主机" +validate_var "REDIS_PORT" "$REDIS_PORT" false "Redis端口" +validate_var "REDIS_PASSWORD" "$REDIS_PASSWORD" false "Redis密码" + +echo "" +echo "检查Meilisearch配置..." +echo "-----------------------------------" + +validate_var "MEILISEARCH_HOST" "$MEILISEARCH_HOST" false "Meilisearch主机" +validate_var "SCOUT_DRIVER" "$SCOUT_DRIVER" false "搜索驱动" + +echo "" +echo "检查邮件配置..." +echo "-----------------------------------" + +validate_var "MAIL_MAILER" "$MAIL_MAILER" false "邮件驱动" +if [ "$MAIL_MAILER" = "smtp" ]; then + validate_var "MAIL_HOST" "$MAIL_HOST" true "SMTP主机" + validate_var "MAIL_PORT" "$MAIL_PORT" true "SMTP端口" + validate_var "MAIL_USERNAME" "$MAIL_USERNAME" true "SMTP用户名" + validate_var "MAIL_PASSWORD" "$MAIL_PASSWORD" true "SMTP密码" +fi + +echo "" +echo "===================================" + +# 检查APP_KEY格式 +if [ -n "$APP_KEY" ]; then + if [[ $APP_KEY == base64:* ]]; then + echo -e "${GREEN}✓ APP_KEY 格式正确${NC}" + else + echo -e "${RED}✗ 错误: APP_KEY 格式不正确,应该以 'base64:' 开头${NC}" + ((ERROR_COUNT++)) + fi +fi + +# 检查默认密码 +if [ "$DB_PASSWORD" = "secure_password_change_this" ] || [ "$DB_PASSWORD" = "secure_password_change_this_in_production" ]; then + echo -e "${YELLOW}⚠ 警告: 数据库密码使用默认值,建议更改${NC}" +fi + +if [ "$MEILISEARCH_KEY" = "your-master-key-change-this-in-production" ]; then + echo -e "${YELLOW}⚠ 警告: Meilisearch密钥使用默认值,建议更改${NC}" +fi + +# 检查环境特定配置 +if [ "$APP_ENV" = "production" ]; then + echo "" + echo "生产环境额外检查..." + echo "-----------------------------------" + + if [ "$APP_DEBUG" = "true" ]; then + echo -e "${YELLOW}⚠ 警告: 生产环境不建议启用调试模式${NC}" + fi + + if [ "$LOG_LEVEL" = "debug" ]; then + echo -e "${YELLOW}⚠ 警告: 生产环境建议使用 info 或更高级别的日志${NC}" + fi +fi + +echo "===================================" + +# 输出结果 +if [ $ERROR_COUNT -eq 0 ]; then + echo -e "${GREEN}✓ 环境变量验证通过!${NC}" + echo "可以继续进行Docker部署。" + exit 0 +else + echo -e "${RED}✗ 发现 $ERROR_COUNT 个错误${NC}" + echo "请修复上述错误后再进行部署。" + exit 1 +fi \ No newline at end of file diff --git a/docker/validate-storage-config.sh b/docker/validate-storage-config.sh new file mode 100755 index 0000000..320e23f --- /dev/null +++ b/docker/validate-storage-config.sh @@ -0,0 +1,225 @@ +#!/bin/bash + +# 数据持久化配置验证脚本 +# 全面验证Docker部署中的存储配置 + +set -e + +echo "==========================================" +echo "数据持久化和目录映射配置验证" +echo "==========================================" + +# 颜色定义 +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 成功和失败计数 +SUCCESS_COUNT=0 +FAIL_COUNT=0 + +# 检查函数 +check_success() { + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓${NC} $1" + ((SUCCESS_COUNT++)) + return 0 + else + echo -e "${RED}✗${NC} $1" + ((FAIL_COUNT++)) + return 1 + fi +} + +echo "" +echo "1. 检查存储目录结构..." +echo "----------------------------------------" + +# 检查所有必要的存储目录 +directories=( + "storage/mysql:MySQL数据库存储目录" + "storage/redis:Redis缓存存储目录" + "storage/meilisearch:Meilisearch搜索存储目录" + "storage/app:Laravel应用存储目录" + "storage/app/private:私有文件存储目录" + "storage/app/private/documents:文档上传存储目录" + "storage/app/private/markdown:Markdown文件存储目录" + "storage/app/public:公共文件存储目录" + "storage/framework:Laravel框架缓存目录" + "storage/framework/cache:缓存目录" + "storage/framework/sessions:会话目录" + "storage/framework/views:视图缓存目录" + "storage/logs:日志存储目录" + "storage/logs/app:应用日志存储目录" + "storage/logs/queue:队列日志存储目录" +) + +for dir_info in "${directories[@]}"; do + IFS=':' read -r dir desc <<< "$dir_info" + [ -d "$dir" ] + check_success "$desc: $dir" +done + +echo "" +echo "2. 检查Docker Compose卷映射配置..." +echo "----------------------------------------" + +# 检查docker-compose.yml中的卷映射 +volumes=( + "mysql_data:MySQL数据卷" + "redis_data:Redis数据卷" + "meilisearch_data:Meilisearch数据卷" + "storage_data:应用存储数据卷" + "documents_data:文档存储数据卷" + "public_data:公共文件数据卷" + "app_logs:应用日志卷" + "queue_logs:队列日志卷" + "laravel_logs:Laravel日志卷" +) + +for volume_info in "${volumes[@]}"; do + IFS=':' read -r volume desc <<< "$volume_info" + docker-compose config 2>/dev/null | grep -q "$volume" + check_success "$desc: $volume" +done + +echo "" +echo "3. 检查服务容器的卷映射..." +echo "----------------------------------------" + +# 检查应用容器的卷映射 +app_volumes=( + "./:/var/www/html:项目代码目录映射" + "storage_data:/var/www/html/storage:应用存储目录映射" + "documents_data:/var/www/html/storage/app/private/documents:文档存储目录映射" + "public_data:/var/www/html/storage/app/public:公共文件目录映射" + "app_logs:/var/log:应用日志目录映射" + "laravel_logs:/var/www/html/storage/logs:Laravel日志目录映射" +) + +for volume_info in "${app_volumes[@]}"; do + IFS=':' read -r volume_mapping path desc <<< "$volume_info" + docker-compose config 2>/dev/null | grep -q "$volume_mapping" + check_success "应用容器 - $desc" +done + +# 检查队列容器的卷映射 +queue_volumes=( + "./:/var/www/html:项目代码目录映射" + "storage_data:/var/www/html/storage:应用存储目录映射" + "documents_data:/var/www/html/storage/app/private/documents:文档存储目录映射" + "public_data:/var/www/html/storage/app/public:公共文件目录映射" + "queue_logs:/var/log:队列日志目录映射" + "laravel_logs:/var/www/html/storage/logs:Laravel日志目录映射" +) + +for volume_info in "${queue_volumes[@]}"; do + IFS=':' read -r volume_mapping path desc <<< "$volume_info" + docker-compose config 2>/dev/null | grep -q "$volume_mapping" + check_success "队列容器 - $desc" +done + +echo "" +echo "4. 检查数据卷绑定配置..." +echo "----------------------------------------" + +# 检查bind mount配置 +bind_mounts=( + "storage/mysql:MySQL数据绑定" + "storage/redis:Redis数据绑定" + "storage/meilisearch:Meilisearch数据绑定" + "storage/app:应用存储绑定" + "storage/logs/app:应用日志绑定" + "storage/logs/queue:队列日志绑定" + "storage/logs:Laravel日志绑定" +) + +for mount_info in "${bind_mounts[@]}"; do + IFS=':' read -r mount_path desc <<< "$mount_info" + # 检查相对路径或绝对路径 + if docker-compose config 2>/dev/null | grep -q "device:.*$mount_path" || docker-compose config 2>/dev/null | grep -q "device: ./$mount_path"; then + check_success "$desc: $mount_path" + else + echo -e "${RED}✗${NC} $desc: $mount_path" + ((FAIL_COUNT++)) + fi +done + +echo "" +echo "5. 检查目录权限..." +echo "----------------------------------------" + +# 检查关键目录的权限 +permission_dirs=( + "storage/mysql" + "storage/redis" + "storage/meilisearch" + "storage/app" + "storage/logs" +) + +for dir in "${permission_dirs[@]}"; do + if [ -d "$dir" ]; then + perms=$(stat -f "%A" "$dir" 2>/dev/null || stat -c "%a" "$dir" 2>/dev/null || echo "unknown") + if [ "$perms" = "755" ] || [ "$perms" = "unknown" ]; then + check_success "$dir 权限正确: $perms" + else + echo -e "${YELLOW}⚠${NC} $dir 权限可能需要调整: $perms" + fi + else + echo -e "${RED}✗${NC} $dir 目录不存在" + ((FAIL_COUNT++)) + fi +done + +echo "" +echo "6. 验证配置文件语法..." +echo "----------------------------------------" + +# 验证docker-compose.yml语法 +docker-compose config --quiet 2>/dev/null +check_success "Docker Compose配置文件语法正确" + +echo "" +echo "7. 创建测试文件验证写入权限..." +echo "----------------------------------------" + +# 测试各个存储目录的写入权限 +test_dirs=( + "storage/app:应用存储目录" + "storage/logs:日志存储目录" + "storage/mysql:MySQL存储目录" + "storage/redis:Redis存储目录" + "storage/meilisearch:Meilisearch存储目录" +) + +for dir_info in "${test_dirs[@]}"; do + IFS=':' read -r dir desc <<< "$dir_info" + test_file="$dir/test-write-$(date +%s).tmp" + + if echo "test" > "$test_file" 2>/dev/null; then + rm -f "$test_file" 2>/dev/null + check_success "$desc 写入权限正常" + else + echo -e "${RED}✗${NC} $desc 写入权限异常" + ((FAIL_COUNT++)) + fi +done + +echo "" +echo "==========================================" +echo "验证结果汇总" +echo "==========================================" + +echo -e "成功检查项: ${GREEN}$SUCCESS_COUNT${NC}" +echo -e "失败检查项: ${RED}$FAIL_COUNT${NC}" + +if [ $FAIL_COUNT -eq 0 ]; then + echo -e "\n${GREEN}🎉 所有数据持久化配置检查通过!${NC}" + echo "系统已准备好进行Docker部署。" + exit 0 +else + echo -e "\n${RED}❌ 发现 $FAIL_COUNT 个配置问题,请修复后重新验证。${NC}" + exit 1 +fi \ No newline at end of file diff --git a/docs/OCTANE_INSTALLATION.md b/docs/OCTANE_INSTALLATION.md new file mode 100644 index 0000000..135603f --- /dev/null +++ b/docs/OCTANE_INSTALLATION.md @@ -0,0 +1,108 @@ +# Laravel Octane 安装文档 + +## 概述 + +本文档记录了在知识库系统中安装和配置 Laravel Octane 的过程。Laravel Octane 通过 Swoole 提供高性能的 PHP 应用服务器。 + +## 安装步骤 + +### 1. 安装 Laravel Octane 包 + +```bash +composer require laravel/octane +``` + +### 2. 发布配置文件 + +```bash +php artisan octane:install --server=swoole +``` + +### 3. 环境变量配置 + +在 `.env` 文件中添加以下 Swoole 相关配置: + +```bash +# Octane/Swoole 配置 +OCTANE_SERVER=swoole +OCTANE_HOST=0.0.0.0 +OCTANE_PORT=8000 +OCTANE_WORKERS=4 +OCTANE_TASK_WORKERS=2 +OCTANE_MAX_REQUESTS=500 +OCTANE_WATCH=false +``` + +### 4. 配置文件更新 + +- `config/octane.php`: 默认服务器设置为 `swoole` +- `composer.json`: 添加了 `dev-octane` 脚本支持 + +## 验证安装 + +### 检查配置 + +```bash +php artisan config:show octane.server +``` + +应该显示: `swoole` + +### 检查命令可用性 + +```bash +php artisan octane:status +``` + +应该显示: `Octane server is not running.` + +### 运行测试 + +```bash +php artisan test tests/Feature/OctaneInstallationTest.php +``` + +## 使用方法 + +### 启动 Octane 服务器 + +```bash +php artisan octane:start +``` + +### 带监听文件变化启动 + +```bash +php artisan octane:start --watch +``` + +### 使用 Composer 脚本启动开发环境 + +```bash +composer run dev-octane +``` + +### 停止服务器 + +```bash +php artisan octane:stop +``` + +### 重启服务器 + +```bash +php artisan octane:restart +``` + +## 注意事项 + +1. **Swoole 扩展**: 在生产环境中需要安装 Swoole PHP 扩展 +2. **内存驻留**: 应用会保持在内存中,需要注意内存泄漏 +3. **全局变量**: 避免使用全局变量和静态变量 +4. **配置缓存**: 建议在生产环境中使用配置缓存 + +## 下一步 + +- 更新 Docker 配置以支持 Swoole +- 配置生产环境部署脚本 +- 进行性能测试和优化 \ No newline at end of file diff --git a/docs/SWOOLE_CONFIGURATION.md b/docs/SWOOLE_CONFIGURATION.md new file mode 100644 index 0000000..ac1b093 --- /dev/null +++ b/docs/SWOOLE_CONFIGURATION.md @@ -0,0 +1,180 @@ +# Swoole 配置说明 + +## 概述 + +本文档描述了 Laravel Octane + Swoole 集成的环境变量配置。这些配置已经添加到所有环境配置文件中,支持不同环境下的优化设置。 + +## 环境变量配置 + +### 核心 Swoole 配置 + +| 环境变量 | 默认值 | 说明 | +|---------|--------|------| +| `OCTANE_SERVER` | `swoole` | 指定使用 Swoole 作为 Octane 服务器 | +| `OCTANE_HOST` | `0.0.0.0` | 服务器绑定的 IP 地址 | +| `OCTANE_PORT` | `8000` | 服务器监听端口 | +| `OCTANE_WORKERS` | `4` | 工作进程数量 | +| `OCTANE_TASK_WORKERS` | `2` | 任务工作进程数量 | +| `OCTANE_MAX_REQUESTS` | `500` | 工作进程处理的最大请求数 | +| `OCTANE_WATCH` | `false` | 是否启用文件监控自动重启 | +| `OCTANE_HTTPS` | `false` | 是否强制使用 HTTPS | + +### 高级配置 + +| 环境变量 | 默认值 | 说明 | +|---------|--------|------| +| `OCTANE_GARBAGE_COLLECTION` | `50` | 垃圾回收阈值(MB) | +| `OCTANE_MAX_EXECUTION_TIME` | `30` | 最大执行时间(秒) | +| `OCTANE_CACHE_ROWS` | `1000` | Swoole 缓存表行数 | +| `OCTANE_CACHE_BYTES` | `10000` | 每行缓存字节数 | + +## 不同环境的配置 + +### 开发环境 (.env.development) + +```bash +# 开发环境优化配置 +OCTANE_WORKERS=2 # 较少的工作进程 +OCTANE_TASK_WORKERS=1 # 较少的任务进程 +OCTANE_MAX_REQUESTS=100 # 较少的最大请求数 +OCTANE_WATCH=true # 启用文件监控 +OCTANE_GARBAGE_COLLECTION=25 # 较低的垃圾回收阈值 +OCTANE_MAX_EXECUTION_TIME=60 # 较长的执行时间用于调试 +``` + +### 生产环境 (.env.production) + +```bash +# 生产环境优化配置 +OCTANE_WORKERS=8 # 更多的工作进程 +OCTANE_TASK_WORKERS=4 # 更多的任务进程 +OCTANE_MAX_REQUESTS=1000 # 更多的最大请求数 +OCTANE_WATCH=false # 禁用文件监控 +OCTANE_GARBAGE_COLLECTION=100 # 较高的垃圾回收阈值 +OCTANE_MAX_EXECUTION_TIME=30 # 标准执行时间 +``` + +## 性能调优建议 + +### 工作进程数量 + +- **开发环境**: 2-4 个进程,避免资源浪费 +- **生产环境**: CPU 核心数的 1-2 倍,通常 4-8 个进程 +- **高负载环境**: 可以增加到 CPU 核心数的 2-4 倍 + +### 最大请求数 + +- **开发环境**: 100-500 请求后重启,便于内存清理 +- **生产环境**: 500-2000 请求后重启,平衡性能和内存使用 +- **内存敏感**: 降低到 200-500,频繁重启释放内存 + +### 垃圾回收阈值 + +- **开发环境**: 25-50MB,及时清理内存 +- **生产环境**: 50-100MB,减少垃圾回收频率 +- **大内存服务器**: 可以设置到 200MB 以上 + +## 启动命令 + +### 基本启动 + +```bash +# 使用默认配置启动 +php artisan octane:start + +# 指定参数启动 +php artisan octane:start --workers=4 --task-workers=2 --port=8000 +``` + +### 开发模式启动 + +```bash +# 启用文件监控 +php artisan octane:start --watch + +# 指定日志级别 +php artisan octane:start --log-level=debug +``` + +### 生产模式启动 + +```bash +# 生产环境启动 +php artisan octane:start --workers=8 --task-workers=4 --max-requests=1000 +``` + +## 监控和管理 + +### 服务器状态 + +```bash +# 查看服务器状态 +php artisan octane:status + +# 停止服务器 +php artisan octane:stop + +# 重启服务器 +php artisan octane:restart + +# 重新加载服务器(优雅重启) +php artisan octane:reload +``` + +### 配置验证 + +```bash +# 查看当前 Octane 配置 +php artisan config:show octane + +# 清除配置缓存 +php artisan config:clear +``` + +## 注意事项 + +### 内存管理 + +1. **避免内存泄漏**: 确保在请求结束后清理大对象 +2. **监控内存使用**: 定期检查工作进程的内存使用情况 +3. **合理设置最大请求数**: 防止内存累积过多 + +### 开发注意事项 + +1. **全局变量**: 避免使用全局变量,它们会在请求间保持状态 +2. **静态变量**: 小心使用静态变量,可能导致数据污染 +3. **单例服务**: 确保单例服务能正确重置状态 + +### 生产部署 + +1. **进程监控**: 使用 Supervisor 或类似工具监控 Octane 进程 +2. **负载均衡**: 在多服务器环境中配置负载均衡 +3. **健康检查**: 实现健康检查接口监控服务状态 + +## 故障排除 + +### 常见问题 + +1. **端口被占用**: 检查端口是否被其他服务占用 +2. **权限问题**: 确保有足够权限绑定指定端口 +3. **内存不足**: 调整工作进程数量或增加服务器内存 +4. **配置不生效**: 清除配置缓存后重启服务 + +### 调试命令 + +```bash +# 检查 Swoole 扩展 +php -m | grep swoole + +# 测试配置 +php artisan octane:start --workers=1 --max-requests=1 + +# 查看详细日志 +php artisan octane:start --log-level=debug +``` + +## 更新历史 + +- **2024-12-29**: 初始配置,添加所有环境变量支持 +- 支持开发、生产环境的差异化配置 +- 添加高级配置选项支持 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d9dc868..0705aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "devDependencies": { "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", + "chokidar": "^5.0.0", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", @@ -1181,6 +1182,22 @@ "node": ">=8" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2032,6 +2049,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 7686b29..756b745 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", + "chokidar": "^5.0.0", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", diff --git a/routes/web.php b/routes/web.php index c1bef4c..18a3bae 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,11 +2,88 @@ use App\Http\Controllers\DocumentController; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Cache; Route::get('/', function () { return view('welcome'); }); +// 健康检查路由(用于Docker健康检查) +Route::get('/health', function () { + $services = []; + $allHealthy = true; + + try { + // 检查数据库连接 + DB::connection()->getPdo(); + $services['database'] = 'connected'; + } catch (Exception $e) { + $services['database'] = 'disconnected'; + $allHealthy = false; + } + + try { + // 检查Redis连接 + if (config('cache.default') === 'redis') { + Cache::store('redis')->put('health_check', 'ok', 10); + Cache::store('redis')->forget('health_check'); + $services['redis'] = 'connected'; + } else { + $services['redis'] = 'not_configured'; + } + } catch (Exception $e) { + $services['redis'] = 'disconnected'; + $allHealthy = false; + } + + try { + // 检查Meilisearch连接 + if (config('scout.driver') === 'meilisearch') { + $client = new \GuzzleHttp\Client(); + $response = $client->get(config('scout.meilisearch.host') . '/health', [ + 'timeout' => 5, + 'headers' => [ + 'Authorization' => 'Bearer ' . config('scout.meilisearch.key') + ] + ]); + + if ($response->getStatusCode() === 200) { + $services['meilisearch'] = 'connected'; + } else { + $services['meilisearch'] = 'unhealthy'; + $allHealthy = false; + } + } else { + $services['meilisearch'] = 'not_configured'; + } + } catch (Exception $e) { + $services['meilisearch'] = 'disconnected'; + $allHealthy = false; + } + + // 检查存储目录是否可写 + try { + $testFile = storage_path('logs/health_check_test.tmp'); + file_put_contents($testFile, 'test'); + unlink($testFile); + $services['storage'] = 'writable'; + } catch (Exception $e) { + $services['storage'] = 'not_writable'; + $allHealthy = false; + } + + $status = $allHealthy ? 'ok' : 'degraded'; + $httpCode = $allHealthy ? 200 : 503; + + return response()->json([ + 'status' => $status, + 'timestamp' => now()->toISOString(), + 'services' => $services, + 'version' => config('app.version', '1.0.0') + ], $httpCode); +})->name('health.check'); + // 文档预览和下载路由(需要认证) Route::middleware(['auth'])->group(function () { Route::get('/documents/{document}/preview', [DocumentController::class, 'preview']) diff --git a/storage/app/.gitignore b/storage/app/.gitignore old mode 100644 new mode 100755 diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore old mode 100644 new mode 100755 diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore old mode 100644 new mode 100755 diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore old mode 100644 new mode 100755 diff --git a/storage/mysql/.gitignore b/storage/mysql/.gitignore new file mode 100644 index 0000000..03e6c0d --- /dev/null +++ b/storage/mysql/.gitignore @@ -0,0 +1,3 @@ +# 忽略MySQL数据文件,但保留目录 +* +!.gitignore \ No newline at end of file diff --git a/storage/redis/.gitignore b/storage/redis/.gitignore new file mode 100644 index 0000000..c0e2b43 --- /dev/null +++ b/storage/redis/.gitignore @@ -0,0 +1,3 @@ +# 忽略Redis数据文件,但保留目录 +* +!.gitignore \ No newline at end of file diff --git a/tests/Feature/OctaneInstallationTest.php b/tests/Feature/OctaneInstallationTest.php new file mode 100644 index 0000000..6438bbc --- /dev/null +++ b/tests/Feature/OctaneInstallationTest.php @@ -0,0 +1,62 @@ +assertEquals('swoole', config('octane.server')); + + // 验证配置文件包含正确的默认值 + $this->assertIsArray(config('octane.listeners')); + $this->assertIsArray(config('octane.warm')); + $this->assertIsArray(config('octane.tables')); + $this->assertIsArray(config('octane.cache')); + $this->assertIsArray(config('octane.watch')); + } + + /** + * 测试 Octane 命令是否可用 + */ + public function test_octane_commands_are_available(): void + { + // 测试 octane:status 命令存在 + $this->artisan('octane:status') + ->assertExitCode(1); // 服务器未运行时返回 1 + } + + /** + * 测试 Laravel Octane 包是否正确安装 + */ + public function test_octane_package_is_installed(): void + { + // 检查配置文件是否存在 + $this->assertFileExists(config_path('octane.php')); + + // 检查 Octane 相关类是否可用 + $this->assertTrue(class_exists(\Laravel\Octane\Octane::class)); + $this->assertTrue(class_exists(\Laravel\Octane\OctaneServiceProvider::class)); + } + + /** + * 测试 Composer 脚本是否包含 Octane 支持 + */ + public function test_composer_scripts_include_octane_support(): void + { + $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); + + // 验证 dev-octane 脚本存在 + $this->assertArrayHasKey('dev-octane', $composerJson['scripts']); + + // 验证脚本包含 octane:start 命令 + $devOctaneScript = implode(' ', $composerJson['scripts']['dev-octane']); + $this->assertStringContainsString('octane:start', $devOctaneScript); + } +} \ No newline at end of file diff --git a/tests/Feature/QueueSystemValidationTest.php b/tests/Feature/QueueSystemValidationTest.php new file mode 100644 index 0000000..772383e --- /dev/null +++ b/tests/Feature/QueueSystemValidationTest.php @@ -0,0 +1,196 @@ + '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); + } +} \ No newline at end of file diff --git a/tests/Feature/SwooleQueueCompatibilityTest.php b/tests/Feature/SwooleQueueCompatibilityTest.php new file mode 100644 index 0000000..ef0f5d3 --- /dev/null +++ b/tests/Feature/SwooleQueueCompatibilityTest.php @@ -0,0 +1,246 @@ + '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); + } +} \ No newline at end of file diff --git a/tests/Feature/SwooleQueueIntegrationTest.php b/tests/Feature/SwooleQueueIntegrationTest.php new file mode 100644 index 0000000..75d40a8 --- /dev/null +++ b/tests/Feature/SwooleQueueIntegrationTest.php @@ -0,0 +1,274 @@ + 'null']); + } + + /** + * 测试队列任务的完整生命周期 + * + * @test + */ + public function test_queue_job_complete_lifecycle() + { + // 创建测试数据 + $user = User::factory()->create(); + $document = Document::factory()->create([ + 'uploaded_by' => $user->id, + 'title' => '集成测试文档', + 'file_path' => 'integration-test.docx', + ]); + + // 使用模拟队列来避免实际执行 + Queue::fake(); + + // 分发队列任务 + Queue::pushOn('default', new ConvertDocumentToMarkdown($document)); + + // 验证任务已被分发 + Queue::assertPushed(ConvertDocumentToMarkdown::class); + + // 验证任务可以被正确序列化和反序列化 + $job = new ConvertDocumentToMarkdown($document); + $serialized = serialize($job); + $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); + } + + /** + * 测试队列任务在高并发下的表现 + * + * @test + */ + public function test_queue_performance_under_load() + { + // 创建测试用户 + $user = User::factory()->create(); + + // 创建多个文档 + $documents = Document::factory()->count(10)->create([ + 'uploaded_by' => $user->id, + ]); + + // 使用模拟队列 + Queue::fake(); + + $startTime = microtime(true); + $startMemory = memory_get_usage(); + + // 批量分发队列任务 + foreach ($documents as $document) { + Queue::pushOn('default', new ConvertDocumentToMarkdown($document)); + } + + $endTime = microtime(true); + $endMemory = memory_get_usage(); + + // 验证性能指标 + $executionTime = $endTime - $startTime; + $memoryUsage = $endMemory - $startMemory; + + $this->assertLessThan(1.0, $executionTime, '队列任务分发时间过长'); + $this->assertLessThan(5 * 1024 * 1024, $memoryUsage, '队列任务内存使用过多'); + + // 验证所有任务都已分发 + Queue::assertPushed(ConvertDocumentToMarkdown::class, 10); + } + + /** + * 测试队列任务的错误恢复机制 + * + * @test + */ + public function test_queue_error_recovery() + { + // 创建测试文档 + $user = User::factory()->create(); + $document = Document::factory()->create([ + 'uploaded_by' => $user->id, + 'title' => '错误恢复测试', + ]); + + // 创建一个会失败的任务 + $job = new ConvertDocumentToMarkdown($document); + + // 模拟任务失败 + try { + $job->failed(new \Exception('模拟任务失败')); + } catch (\Exception $e) { + // 预期的异常 + } + + // 验证失败处理机制工作正常 + $this->assertTrue(true, '错误恢复机制测试完成'); + } + + /** + * 测试队列任务的内存清理 + * + * @test + */ + public function test_queue_memory_cleanup() + { + $initialMemory = memory_get_usage(); + + // 创建和处理多个任务 + $user = User::factory()->create(); + + for ($i = 0; $i < 5; $i++) { + $document = Document::factory()->create(['uploaded_by' => $user->id]); + $job = new ConvertDocumentToMarkdown($document); + + // 模拟任务处理 + unset($job); + unset($document); + } + + // 强制垃圾回收 + gc_collect_cycles(); + + $finalMemory = memory_get_usage(); + $memoryIncrease = $finalMemory - $initialMemory; + + // 验证内存没有显著增长 + $this->assertLessThan(2 * 1024 * 1024, $memoryIncrease, '队列任务存在内存泄漏'); + } + + /** + * 测试队列配置的动态加载 + * + * @test + */ + public function test_queue_configuration_loading() + { + // 验证队列配置可以正确加载 + $queueConfig = config('queue'); + $this->assertIsArray($queueConfig, '队列配置加载失败'); + + // 验证默认连接配置 + $defaultConnection = $queueConfig['default']; + $this->assertNotEmpty($defaultConnection, '默认队列连接未配置'); + + // 验证连接配置存在 + $connectionConfig = $queueConfig['connections'][$defaultConnection] ?? null; + $this->assertNotNull($connectionConfig, '队列连接配置不存在'); + $this->assertArrayHasKey('driver', $connectionConfig, '队列驱动未配置'); + } + + /** + * 测试队列任务的优先级处理 + * + * @test + */ + public function test_queue_priority_handling() + { + // 创建测试数据 + $user = User::factory()->create(); + $highPriorityDoc = Document::factory()->create([ + 'uploaded_by' => $user->id, + 'title' => '高优先级文档', + ]); + $lowPriorityDoc = Document::factory()->create([ + 'uploaded_by' => $user->id, + 'title' => '低优先级文档', + ]); + + // 使用模拟队列 + Queue::fake(); + + // 分发不同优先级的任务 + $highPriorityJob = (new ConvertDocumentToMarkdown($highPriorityDoc))->onQueue('high'); + $lowPriorityJob = (new ConvertDocumentToMarkdown($lowPriorityDoc))->onQueue('default'); + + Queue::push($lowPriorityJob); + Queue::push($highPriorityJob); + + // 验证任务已正确分发 + Queue::assertPushed(ConvertDocumentToMarkdown::class, 2); + } + + /** + * 测试队列任务的批处理功能 + * + * @test + */ + public function test_queue_batch_processing() + { + // 创建测试数据 + $user = User::factory()->create(); + $documents = Document::factory()->count(3)->create(['uploaded_by' => $user->id]); + + // 使用模拟队列 + Queue::fake(); + + // 创建批处理任务 + $jobs = $documents->map(function ($document) { + return new ConvertDocumentToMarkdown($document); + }); + + // 批量分发任务 + foreach ($jobs as $job) { + Queue::push($job); + } + + // 验证批处理功能 + Queue::assertPushed(ConvertDocumentToMarkdown::class, 3); + } + + /** + * 测试队列任务的超时处理 + * + * @test + */ + public function test_queue_timeout_handling() + { + // 创建测试文档 + $user = User::factory()->create(); + $document = Document::factory()->create(['uploaded_by' => $user->id]); + + // 创建任务并检查超时配置 + $job = new ConvertDocumentToMarkdown($document); + + $this->assertIsNumeric($job->timeout, '任务超时配置无效'); + $this->assertGreaterThan(0, $job->timeout, '任务超时时间必须大于 0'); + + // 验证超时时间合理 + $this->assertLessThanOrEqual(600, $job->timeout, '任务超时时间过长'); + } +} \ No newline at end of file diff --git a/tests/Feature/SwooleQueueListenerTest.php b/tests/Feature/SwooleQueueListenerTest.php new file mode 100644 index 0000000..4c9d243 --- /dev/null +++ b/tests/Feature/SwooleQueueListenerTest.php @@ -0,0 +1,211 @@ + true, + ]); + + $this->assertEquals(0, $exitCode, '队列工作命令不可用'); + } + + /** + * 测试队列监听命令可用性 + * + * @test + */ + public function test_queue_listen_command_availability() + { + // 测试队列监听命令是否可用 + $exitCode = Artisan::call('queue:listen', [ + '--help' => true, + ]); + + $this->assertEquals(0, $exitCode, '队列监听命令不可用'); + } + + /** + * 测试队列状态检查 + * + * @test + */ + public function test_queue_status_check() + { + // 检查队列连接状态 + $defaultConnection = config('queue.default'); + $this->assertNotEmpty($defaultConnection, '默认队列连接未配置'); + + // 检查队列配置 + $queueConfig = config("queue.connections.{$defaultConnection}"); + $this->assertIsArray($queueConfig, '队列连接配置无效'); + $this->assertArrayHasKey('driver', $queueConfig, '队列驱动未配置'); + } + + /** + * 测试队列表是否存在(数据库队列) + * + * @test + */ + public function test_queue_tables_exist() + { + if (config('queue.default') === 'database') { + // 检查 jobs 表是否存在 + $this->assertTrue( + \Schema::hasTable('jobs'), + '队列任务表不存在' + ); + + // 检查 failed_jobs 表是否存在 + $this->assertTrue( + \Schema::hasTable('failed_jobs'), + '失败任务表不存在' + ); + } + + $this->assertTrue(true); // 如果不是数据库队列,测试通过 + } + + /** + * 测试队列配置与 Swoole 的兼容性 + * + * @test + */ + public function test_queue_swoole_compatibility() + { + // 检查 Octane 配置 + $octaneServer = config('octane.server'); + $this->assertEquals('swoole', $octaneServer, 'Octane 服务器未配置为 Swoole'); + + // 检查队列配置是否与 Swoole 兼容 + $queueConnection = config('queue.default'); + $supportedDrivers = ['database', 'redis', 'sync']; + + $queueDriver = config("queue.connections.{$queueConnection}.driver"); + $this->assertContains( + $queueDriver, + $supportedDrivers, + "队列驱动 {$queueDriver} 可能与 Swoole 不兼容" + ); + } + + /** + * 测试队列工作进程配置 + * + * @test + */ + public function test_queue_worker_configuration() + { + // 检查 Octane 任务工作进程配置 + $taskWorkers = config('octane.task_workers', env('OCTANE_TASK_WORKERS')); + $this->assertIsNumeric($taskWorkers, 'Octane 任务工作进程数量配置无效'); + $this->assertGreaterThan(0, $taskWorkers, 'Octane 任务工作进程数量必须大于 0'); + + // 检查队列重试配置 + $retryAfter = config('queue.connections.database.retry_after'); + $this->assertIsNumeric($retryAfter, '队列重试时间配置无效'); + $this->assertGreaterThan(0, $retryAfter, '队列重试时间必须大于 0'); + } + + /** + * 测试队列监听器进程管理 + * + * @test + */ + public function test_queue_listener_process_management() + { + // 在测试环境中,我们只能验证命令的可用性 + // 实际的进程启动需要在集成测试中验证 + + // 验证队列重启命令 + $exitCode = Artisan::call('queue:restart'); + $this->assertEquals(0, $exitCode, '队列重启命令执行失败'); + + // 验证队列清理命令(可能在某些环境下不可用) + try { + $exitCode = Artisan::call('queue:clear'); + $this->assertEquals(0, $exitCode, '队列清理命令执行失败'); + } catch (\Exception $e) { + // 如果命令不存在,跳过此测试 + $this->assertTrue(true, '队列清理命令在当前环境下不可用'); + } + } + + /** + * 测试队列任务超时配置 + * + * @test + */ + public function test_queue_timeout_configuration() + { + // 检查文档转换任务的超时配置 + $conversionTimeout = config('documents.conversion.timeout', 300); + $this->assertIsNumeric($conversionTimeout, '文档转换超时配置无效'); + $this->assertGreaterThan(0, $conversionTimeout, '文档转换超时时间必须大于 0'); + + // 检查 Octane 最大执行时间配置 + $maxExecutionTime = config('octane.max_execution_time'); + $this->assertIsNumeric($maxExecutionTime, 'Octane 最大执行时间配置无效'); + + // 如果设置了最大执行时间,应该大于队列任务超时时间 + if ($maxExecutionTime > 0) { + // 对于测试环境,我们允许更灵活的配置 + // 在生产环境中,这个检查更重要 + if ($maxExecutionTime < $conversionTimeout) { + $this->markTestSkipped( + "Octane 最大执行时间 ({$maxExecutionTime}s) 小于队列任务超时时间 ({$conversionTimeout}s)。" . + "在生产环境中应该调整此配置。" + ); + } else { + $this->assertGreaterThanOrEqual( + $conversionTimeout, + $maxExecutionTime, + 'Octane 最大执行时间应该大于或等于队列任务超时时间' + ); + } + } + } + + /** + * 测试队列错误处理配置 + * + * @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, '队列重试延迟不能为负数'); + } +} \ No newline at end of file