diff --git a/DC模块代码恢复指南.md b/DC模块代码恢复指南.md index 8210fae9..c6a75611 100644 --- a/DC模块代码恢复指南.md +++ b/DC模块代码恢复指南.md @@ -235,4 +235,5 @@ + diff --git a/backend/migrations/add_data_stats_to_tool_c_session.sql b/backend/migrations/add_data_stats_to_tool_c_session.sql index 150da5f3..770740b0 100644 --- a/backend/migrations/add_data_stats_to_tool_c_session.sql +++ b/backend/migrations/add_data_stats_to_tool_c_session.sql @@ -31,3 +31,4 @@ WHERE table_schema = 'dc_schema' + diff --git a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql index c53e83c3..66277b0c 100644 --- a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql +++ b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql @@ -69,3 +69,4 @@ ORDER BY ordinal_position; -- ✅ 断点续传字段已添加 + diff --git a/backend/prisma/manual-migrations/run-migration-002.ts b/backend/prisma/manual-migrations/run-migration-002.ts index 11f901cb..d3e5c143 100644 --- a/backend/prisma/manual-migrations/run-migration-002.ts +++ b/backend/prisma/manual-migrations/run-migration-002.ts @@ -82,3 +82,4 @@ runMigration() process.exit(1); }); + diff --git a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql index 2f926340..93e7735a 100644 --- a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql +++ b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql @@ -16,3 +16,4 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名 + diff --git a/backend/prisma/migrations/create_tool_c_session.sql b/backend/prisma/migrations/create_tool_c_session.sql index 09f61b38..474a45f2 100644 --- a/backend/prisma/migrations/create_tool_c_session.sql +++ b/backend/prisma/migrations/create_tool_c_session.sql @@ -43,3 +43,4 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创 + diff --git a/backend/recover-code-from-cursor-db.js b/backend/recover-code-from-cursor-db.js index e65644d3..462b9ac6 100644 --- a/backend/recover-code-from-cursor-db.js +++ b/backend/recover-code-from-cursor-db.js @@ -193,3 +193,4 @@ function extractCodeBlocks(obj, blocks = []) { + diff --git a/backend/scripts/check-dc-tables.mjs b/backend/scripts/check-dc-tables.mjs index 97999f96..b60374d8 100644 --- a/backend/scripts/check-dc-tables.mjs +++ b/backend/scripts/check-dc-tables.mjs @@ -212,3 +212,4 @@ checkDCTables(); + diff --git a/backend/scripts/create-tool-c-ai-history-table.mjs b/backend/scripts/create-tool-c-ai-history-table.mjs index 713d493d..9c643c23 100644 --- a/backend/scripts/create-tool-c-ai-history-table.mjs +++ b/backend/scripts/create-tool-c-ai-history-table.mjs @@ -164,3 +164,4 @@ createAiHistoryTable() + diff --git a/backend/scripts/create-tool-c-table.js b/backend/scripts/create-tool-c-table.js index 374fc0aa..02e72f7b 100644 --- a/backend/scripts/create-tool-c-table.js +++ b/backend/scripts/create-tool-c-table.js @@ -151,3 +151,4 @@ createToolCTable() + diff --git a/backend/scripts/create-tool-c-table.mjs b/backend/scripts/create-tool-c-table.mjs index fa5f8c3f..8ceca8dc 100644 --- a/backend/scripts/create-tool-c-table.mjs +++ b/backend/scripts/create-tool-c-table.mjs @@ -148,3 +148,4 @@ createToolCTable() + diff --git a/backend/src/common/jobs/utils.ts b/backend/src/common/jobs/utils.ts index e95b1c54..f2881c05 100644 --- a/backend/src/common/jobs/utils.ts +++ b/backend/src/common/jobs/utils.ts @@ -280,3 +280,4 @@ export function getBatchItems( } + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts index 297a496c..071209b4 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts @@ -314,5 +314,6 @@ runTests().catch((error) => { + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts index 2639962b..db4faa50 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts @@ -255,5 +255,6 @@ runTest() + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http index f709e71b..7af78195 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http +++ b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http @@ -293,5 +293,6 @@ Content-Type: application/json + diff --git a/backend/src/modules/asl/fulltext-screening/services/ExcelExporter.ts b/backend/src/modules/asl/fulltext-screening/services/ExcelExporter.ts index 75037c67..63adb20f 100644 --- a/backend/src/modules/asl/fulltext-screening/services/ExcelExporter.ts +++ b/backend/src/modules/asl/fulltext-screening/services/ExcelExporter.ts @@ -372,5 +372,6 @@ export class ExcelExporter { + diff --git a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts index 1c8f9ac7..779b3cd9 100644 --- a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts +++ b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts @@ -230,4 +230,5 @@ export const conflictDetectionService = new ConflictDetectionService(); + diff --git a/backend/src/modules/dc/tool-b/services/TemplateService.ts b/backend/src/modules/dc/tool-b/services/TemplateService.ts index c167216e..6dbd3b82 100644 --- a/backend/src/modules/dc/tool-b/services/TemplateService.ts +++ b/backend/src/modules/dc/tool-b/services/TemplateService.ts @@ -258,4 +258,5 @@ export const templateService = new TemplateService(); + diff --git a/backend/src/modules/dc/tool-b/workers/extractionWorker.ts b/backend/src/modules/dc/tool-b/workers/extractionWorker.ts index cf171fcc..12c27e11 100644 --- a/backend/src/modules/dc/tool-b/workers/extractionWorker.ts +++ b/backend/src/modules/dc/tool-b/workers/extractionWorker.ts @@ -389,3 +389,4 @@ async function countCompletedBatches(taskId: string): Promise { } } + diff --git a/backend/src/modules/dc/tool-c/README.md b/backend/src/modules/dc/tool-c/README.md index 1b0b9b04..5714560f 100644 --- a/backend/src/modules/dc/tool-c/README.md +++ b/backend/src/modules/dc/tool-c/README.md @@ -181,3 +181,4 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \ + diff --git a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts index 5d713bb2..12c33e92 100644 --- a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts +++ b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts @@ -235,3 +235,4 @@ export const streamAIController = new StreamAIController(); + diff --git a/backend/src/tests/README.md b/backend/src/tests/README.md index b3a48f60..67cf06c5 100644 --- a/backend/src/tests/README.md +++ b/backend/src/tests/README.md @@ -381,3 +381,4 @@ SET session_replication_role = 'origin'; **版本:** V1.0 **作者:** AI Clinical Research Team + diff --git a/backend/src/tests/verify-test1-database.sql b/backend/src/tests/verify-test1-database.sql index 423ce745..bed79415 100644 --- a/backend/src/tests/verify-test1-database.sql +++ b/backend/src/tests/verify-test1-database.sql @@ -83,3 +83,4 @@ WHERE key = 'verify_test'; \echo '✅ 数据库验证完成!' \echo '==========================================' + diff --git a/backend/src/tests/verify-test1-database.ts b/backend/src/tests/verify-test1-database.ts index d1c3e955..f568f2a6 100644 --- a/backend/src/tests/verify-test1-database.ts +++ b/backend/src/tests/verify-test1-database.ts @@ -226,3 +226,4 @@ verifyDatabase() process.exit(1); }); + diff --git a/backend/src/types/global.d.ts b/backend/src/types/global.d.ts index 22b5ffef..225f8ff2 100644 --- a/backend/src/types/global.d.ts +++ b/backend/src/types/global.d.ts @@ -16,3 +16,4 @@ declare global { export {} + diff --git a/backend/sync-dc-database.ps1 b/backend/sync-dc-database.ps1 index 398e47d8..cd506af2 100644 --- a/backend/sync-dc-database.ps1 +++ b/backend/sync-dc-database.ps1 @@ -38,4 +38,5 @@ Write-Host "✅ 完成!" -ForegroundColor Green + diff --git a/backend/test-tool-c-advanced-scenarios.mjs b/backend/test-tool-c-advanced-scenarios.mjs index 66e484b4..090d8f2b 100644 --- a/backend/test-tool-c-advanced-scenarios.mjs +++ b/backend/test-tool-c-advanced-scenarios.mjs @@ -326,3 +326,4 @@ runAdvancedTests().catch(error => { + diff --git a/backend/test-tool-c-day2.mjs b/backend/test-tool-c-day2.mjs index 197cc4eb..1b317916 100644 --- a/backend/test-tool-c-day2.mjs +++ b/backend/test-tool-c-day2.mjs @@ -392,3 +392,4 @@ runAllTests() + diff --git a/backend/test-tool-c-day3.mjs b/backend/test-tool-c-day3.mjs index 3aaddfa9..a5699f44 100644 --- a/backend/test-tool-c-day3.mjs +++ b/backend/test-tool-c-day3.mjs @@ -350,3 +350,4 @@ runAllTests() + diff --git a/deploy-to-sae.ps1 b/deploy-to-sae.ps1 index 49805a8f..f0a6b7ba 100644 --- a/deploy-to-sae.ps1 +++ b/deploy-to-sae.ps1 @@ -134,3 +134,4 @@ Set-Location .. + diff --git a/docs/00-系统总体设计/00-系统当前状态与开发指南.md b/docs/00-系统总体设计/00-系统当前状态与开发指南.md index 0b7eac5d..3eee53d2 100644 --- a/docs/00-系统总体设计/00-系统当前状态与开发指南.md +++ b/docs/00-系统总体设计/00-系统当前状态与开发指南.md @@ -80,9 +80,9 @@ ### 技术栈 **前端**: -- React 19 + TypeScript 5 + Vite 6 +- React 19 + TypeScript 5 + Vite 7 - **Ant Design 6.0** + **Ant Design X 2.1** ✨ 新增! -- TailwindCSS 3 + React Query v5 + React Router DOM v6 +- TailwindCSS 3 + React Query v5 + React Router DOM v7 - 架构:frontend-v2(模块化,顶部导航) - **通用能力层**:shared/components/Chat(基于 Ant Design X)✅ @@ -93,12 +93,12 @@ - 架构:增量演进(legacy + common + modules) **数据库**: -- PostgreSQL 16 +- PostgreSQL 15 (Docker: postgres:15-alpine) - 10个Schema隔离(platform/aia/pkb/asl/dc/ssa/st/rvw/admin/common) **云原生部署**: - 阿里云 SAE (Serverless 应用引擎) -- RDS (PostgreSQL) + OSS (对象存储) + Redis (可选) +- RDS PostgreSQL 15 + OSS (对象存储) + Redis (可选,Dify需要) --- @@ -392,7 +392,7 @@ AIclinicalresearch/ ### 环境要求 ``` Node.js: v22.18.0+ -PostgreSQL: 16+ +PostgreSQL: 15+(当前使用15.14) npm: 10+ ``` diff --git a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md index 09dc7e43..8de927d8 100644 --- a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md +++ b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md @@ -1254,5 +1254,6 @@ interface FulltextScreeningResult { + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md index 4b0f239a..20c31419 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md @@ -368,5 +368,6 @@ GET /api/v1/asl/fulltext-screening/tasks/:taskId/export + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md index 01ed95e0..af3e64b6 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md @@ -311,5 +311,6 @@ Linter错误:0个 + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md index 09915fc7..25a51725 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md @@ -470,5 +470,6 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf' + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md index a009fd73..a798e8d6 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md @@ -538,3 +538,4 @@ df['creatinine'] = pd.to_numeric(df['creatinine'], errors='coerce') + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md index d49a536c..fc17e19b 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md @@ -376,3 +376,4 @@ npm run dev + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md index db213061..9702f939 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md @@ -953,3 +953,4 @@ export const aiController = new AIController(); + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md index c8557ef6..194f9692 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md @@ -1287,3 +1287,4 @@ npm install react-markdown + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md index a746c247..adf28bf5 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md @@ -195,3 +195,4 @@ FMA___基线 | FMA___1个月 | FMA___2个月 + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md index 5b10fb7e..f8958f9b 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md @@ -353,3 +353,4 @@ formula = "FMA总分(0-100) / 100" + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md index 0e3d4ad1..13056d08 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md @@ -187,3 +187,4 @@ async handleFillnaMice(request, reply) { + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md index babd3386..3acaf9e4 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md @@ -159,3 +159,4 @@ method: 'mean' | 'median' | 'mode' | 'constant' | 'ffill' | 'bfill' + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md index f94eb8e7..2447b09a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md @@ -309,3 +309,4 @@ Changes: + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md index 9f61f637..06ffa88f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md @@ -381,3 +381,4 @@ cd path; command + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md index 04ae4f26..2436926a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md @@ -610,3 +610,4 @@ import { logger } from '../../../../common/logging/index.js'; + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md index c8da6420..1378dd3e 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md @@ -614,3 +614,4 @@ Content-Length: 45234 + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md index b50c9f3e..98d4d15f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md @@ -266,3 +266,4 @@ Response: + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md index c2dc05c4..1dbc5a52 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md @@ -419,3 +419,4 @@ Response: + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md index 3709d28c..84dcf414 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md @@ -413,3 +413,4 @@ import { ChatContainer } from '@/shared/components/Chat'; + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md index cae3fadc..c1e59dc8 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md @@ -323,3 +323,4 @@ const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md index 4591e3be..1cb849d6 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md @@ -363,3 +363,4 @@ python main.py + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md index f2260218..fabde21a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md @@ -611,3 +611,4 @@ http://localhost:5173/data-cleaning/tool-c + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md index d195fcb6..5dd93d8e 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md @@ -221,3 +221,4 @@ Day 5 (6-8小时): + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md index e0f61159..d3612cee 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md @@ -398,4 +398,5 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建 + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md index 771cb52c..d7286950 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md @@ -374,3 +374,4 @@ const mockAssets: Asset[] = [ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md index ce6d9631..dc287f57 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md @@ -358,3 +358,4 @@ frontend-v2/src/modules/dc/ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md index f14d836a..fea405f4 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md @@ -318,3 +318,4 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md index 9043cfe5..570f1171 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md @@ -272,3 +272,4 @@ ConflictDetectionService // 冲突检测(字段级对比) + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md index f6a7d269..f3f73ba8 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md @@ -321,3 +321,4 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md index 9eb09bb5..b2c157d2 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md @@ -284,3 +284,4 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md index 644056ae..32ffcc65 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md @@ -348,3 +348,4 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md index 4807f0e1..6ae4b75f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md @@ -436,3 +436,4 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发 + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md index 59100377..b416e3be 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md @@ -282,3 +282,4 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md index a114cd4a..bbf41b6f 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md @@ -213,3 +213,4 @@ $ node scripts/check-dc-tables.mjs + diff --git a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md index af9e0a52..efadda67 100644 --- a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md +++ b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md @@ -446,3 +446,4 @@ ${fields.map((f, i) => `${i + 1}. ${f.name}:${f.desc}`).join('\n')} + diff --git a/docs/05-部署文档/00-部署架构总览.md b/docs/05-部署文档/00-部署架构总览.md new file mode 100644 index 00000000..a4d98f85 --- /dev/null +++ b/docs/05-部署文档/00-部署架构总览.md @@ -0,0 +1,1605 @@ +# AI临床研究平台 - 阿里云部署架构总览 + +> **文档版本:** v1.0 +> **创建日期:** 2025-12-14 +> **目标环境:** 阿里云 SAE + RDS PostgreSQL 15 + OSS +> **架构类型:** 云原生 Serverless + 容器化微服务 +> **适用团队:** 1-2人初创团队 → 10人成长团队 + +--- + +## 📋 目录 + +1. [架构全景图](#架构全景图) +2. [5个核心模块关系](#5个核心模块关系) +3. [模块与阿里云服务映射](#模块与阿里云服务映射) +4. [开发环境与部署环境](#开发环境与部署环境) +5. [Docker版本管理策略](#docker版本管理策略) +6. [线上故障快速修复](#线上故障快速修复) +7. [部署文档导航](#部署文档导航) + +--- + +## 架构全景图 + +### 1. 逻辑架构(用户视角) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 用户浏览器(Browser) │ +│ https://your-domain.com │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────────▼───────────────┐ + │ 前端 SPA (React 19) │ + │ - 用户界面 │ + │ - 路由管理 │ + │ - API调用 │ + └────────────┬───────────────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ┌─────────▼──────┐ ┌───────▼───────┐ ┌──────▼──────────┐ + │ Node.js后端 │ │ Python微服务 │ │ Dify RAG平台 │ + │ (Fastify) │ │ (FastAPI) │ │ (知识库检索) │ + │ │ │ │ │ │ + │ - 业务逻辑 │ │ - PDF解析 │ │ - 文档上传 │ + │ - 权限控制 │ │ - 数据清洗 │ │ - 向量检索 │ + │ - API聚合 │ │ - 文件提取 │ │ - LLM对话 │ + └────────┬───────┘ └───────┬───────┘ └──────┬──────────┘ + │ │ │ + │ │ │ + └──────────────────┼──────────────────┘ + │ + ┌───────────▼────────────┐ + │ PostgreSQL 15 数据库 │ + │ (RDS) │ + │ │ + │ - 10个Schema隔离 │ + │ - pg-boss任务队列 │ + │ - Postgres-Only架构 │ + └────────────────────────┘ +``` + +### 2. 物理架构(阿里云资源) + +``` +┌───────────────────────────────────────────────────────────────────────────┐ +│ 阿里云 VPC(专有网络) │ +│ 172.16.0.0/12(内网隔离) │ +├───────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ SAE (Serverless Application Engine) │ │ +│ │ 容器化应用部署平台 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ ┌───────────────┐ ┌───────────────┐ ┌──────────────────┐ │ │ +│ │ │ 前端Nginx │ │ Node.js后端 │ │ Python微服务 │ │ │ +│ │ │ 容器应用 │ │ 容器应用 │ │ 容器应用 │ │ │ +│ │ ├───────────────┤ ├───────────────┤ ├──────────────────┤ │ │ +│ │ │ Nginx 1.25 │ │ Node.js 22 │ │ Python 3.11 │ │ │ +│ │ │ React构建产物 │ │ Fastify 5 │ │ FastAPI 0.104 │ │ │ +│ │ │ 健康检查/路由 │ │ Prisma 6 │ │ PyMuPDF/Polars │ │ │ +│ │ └───────┬───────┘ └───────┬───────┘ └────────┬─────────┘ │ │ +│ │ │ │ │ │ │ +│ │ │ 内网调用 │ 内网调用 │ │ │ +│ │ └──────────────────┴───────────────────┘ │ │ +│ │ │ │ │ +│ └─────────────────────────────┼─────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ ⭐ NAT网关 + EIP(公网出口,必需) │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ - 为SAE提供公网访问能力 │ │ +│ │ - 后端调用DeepSeek/OpenAI API │ │ +│ │ - Python下载公网PDF │ │ +│ │ - npm install公网依赖 │ │ +│ │ - 成本:NAT网关¥60/月 + EIP流量费 │ │ +│ │ - ⚠️ 没有NAT网关,AI功能全部超时! │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────▼──────────────────────────┐ │ +│ │ RDS PostgreSQL 15(云数据库) │ │ +│ │ rds.pg.s2.large (2核4GB) │ │ +│ ├────────────────────────────────────────────────────────┤ │ +│ │ - 数据库名:ai_clinical_research │ │ +│ │ - 用户:aiclinical_rw(应用专用账号) │ │ +│ │ - 内网地址:rm-xxxxx.pg.rds.aliyuncs.com:5432 │ │ +│ │ - 自动备份:每天凌晨2点 + 日志备份(PITR) │ │ +│ │ - 白名单:172.16.0.0/12(SAE的VPC网段) │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ OSS (Object Storage Service) 对象存储 │ │ +│ │ Bucket: aiclinical-data-prod │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ - 用户上传文件(PDF、Word、Excel) │ │ +│ │ - 文献数据(ASL模块) │ │ +│ │ - 数据清洗结果(DC模块) │ │ +│ │ - 数据库备份归档(可选) │ │ +│ │ - 访问方式:SDK(内网免流量费) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────────┐ +│ 独立 ECS 服务器(Dify部署) │ +│ ecs.c6.xlarge (4核8GB) │ +├───────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Dify RAG 平台 (Docker Compose) │ │ +│ ├──────────────────────────────────────────────────────────────┤ │ +│ │ - Dify API (dify/api:0.6.13) │ │ +│ │ - Dify Web (dify/web:0.6.13) │ │ +│ │ - PostgreSQL 15 (postgres:15-alpine,Dify专用数据库) │ │ +│ │ - Redis (redis:6-alpine,Dify缓存) │ │ +│ │ - Weaviate (向量数据库) │ │ +│ │ - Nginx (路由层,处理CORS) │ │ +│ │ - Sandbox (代码执行沙箱) │ │ +│ │ │ │ +│ │ 内网地址:http://172.16.x.x(通过VPC与SAE互联) │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────────────────┘ +``` + +### 3. 数据流向图 + +``` +用户上传文件 + │ + ├──► 前端 (React) + │ │ + │ ├──► /api/v1/pkb/documents (上传到知识库) + │ │ │ + │ │ ├──► Node.js后端 + │ │ │ │ + │ │ │ ├──► OSS (存储原始文件) + │ │ │ └──► Dify API (创建文档向量) + │ │ │ │ + │ │ │ └──► Weaviate (存储向量) + │ │ │ + │ │ └──► RDS (存储元数据) + │ │ + │ ├──► /api/v1/asl/literatures (文献筛选) + │ │ │ + │ │ ├──► Node.js后端 + │ │ │ │ + │ │ │ ├──► Python微服务 (PDF解析) + │ │ │ │ │ + │ │ │ │ └──► OSS (读取PDF) + │ │ │ │ + │ │ │ └──► RDS (存储文献数据) + │ │ │ + │ │ └──► 返回解析结果 + │ │ + │ └──► /api/v1/dc/extraction (数据清洗) + │ │ + │ ├──► Node.js后端 + │ │ │ + │ │ ├──► Python微服务 (Polars数据处理) + │ │ │ │ + │ │ │ └──► OSS (读取Excel/CSV) + │ │ │ + │ │ └──► RDS (存储清洗结果) + │ │ + │ └──► 返回清洗结果 + │ + └──► 展示结果 +``` + +--- + +## 5个核心模块关系 + +### 1. 模块依赖关系图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 核心依赖层级 (从下到上) │ +└─────────────────────────────────────────────────────────────────┘ + +L4: 用户界面层 +┌──────────────────────────────────────────────────────┐ +│ 前端 (React 19 + Vite 7) │ +│ - 调用 Node.js后端的 REST API │ +│ - 不直接调用 Python微服务和Dify │ +│ - 通过反向代理 (/api/) 路由到后端 │ +└──────────────────────────────────────────────────────┘ + ▲ + │ HTTP/HTTPS + │ +───────────────────────────────────────────────────────── + +L3: 业务逻辑层 +┌──────────────────────────────────────────────────────┐ +│ Node.js 后端 (Fastify + Prisma) │ +│ - 业务逻辑聚合 │ +│ - 权限控制 │ +│ - 调用 Python微服务 (内网HTTP) │ +│ - 调用 Dify API (内网HTTP) │ +│ - 数据库访问 (Prisma ORM) │ +└──────────────────────────────────────────────────────┘ + ▲ ▲ ▲ + │ │ │ + │ (内网) │ (内网) │ (Prisma) + │ │ │ + ┌──────┴────────┐ ┌──────┴──────┐ ┌────▼──────┐ + │ Python微服务 │ │ Dify平台 │ │ PostgreSQL│ + └───────────────┘ └─────────────┘ └───────────┘ + +───────────────────────────────────────────────────────── + +L2: 能力支撑层 +┌────────────────────────────┐ ┌────────────────────────────┐ +│ Python 微服务 │ │ Dify RAG 平台 │ +│ (FastAPI + PyMuPDF) │ │ (知识库 + LLM) │ +├────────────────────────────┤ ├────────────────────────────┤ +│ - PDF解析 (ASL模块) │ │ - 文档上传分块 │ +│ - Docx/Txt提取 (ASL) │ │ - Embedding向量化 │ +│ - 数据清洗 (DC模块) │ │ - 向量检索 │ +│ - Polars数据处理 │ │ - LLM对话生成 │ +│ - OCR识别 (Nougat) │ │ - RAG流程编排 │ +│ │ │ │ +│ 依赖: │ │ 依赖: │ +│ - OSS (读取文件) │ │ - Weaviate (向量存储) │ +│ - RDS (存储结果,可选) │ │ - PostgreSQL (Dify专用) │ +│ │ │ - Redis (Dify缓存) │ +└────────────────────────────┘ └────────────────────────────┘ + ▲ ▲ + │ │ + └────────────┬───────────────────┘ + │ +───────────────────────────────────────────────────────── + +L1: 基础设施层 +┌────────────────────┐ ┌────────────────┐ ┌──────────────┐ +│ PostgreSQL 15 │ │ OSS对象存储 │ │ Redis (可选) │ +│ (RDS) │ │ (文件存储) │ │ (Dify专用) │ +├────────────────────┤ ├────────────────┤ ├──────────────┤ +│ - 10个Schema隔离 │ │ - PDF文件 │ │ - Dify缓存 │ +│ - pg-boss任务队列 │ │ - Word/Excel │ │ - Dify会话 │ +│ - 用户数据 │ │ - 文献数据 │ │ │ +│ - 业务数据 │ │ - 清洗结果 │ │ (初期不需要 │ +│ - Prisma ORM访问 │ │ - SDK访问 │ │ Redis,可用 │ +│ │ │ - 内网免流量 │ │ Postgres替代)│ +└────────────────────┘ └────────────────┘ └──────────────┘ +``` + +### 2. 模块间调用关系表 + +| 调用方 | 被调用方 | 调用方式 | 调用目的 | 网络 | +|--------|---------|---------|---------|------| +| **前端** | Node.js后端 | HTTP REST API | 所有业务功能 | 公网(HTTPS) | +| **Node.js后端** | Python微服务 | HTTP REST API | PDF解析、数据清洗 | 内网(VPC) | +| **Node.js后端** | Dify API | HTTP REST API | 知识库管理、RAG检索 | 内网(VPC) | +| **Node.js后端** | PostgreSQL | Prisma ORM | 数据CRUD、事务 | 内网(VPC) | +| **Node.js后端** | OSS | SDK (ali-oss) | 文件上传/下载 | 内网(VPC) | +| **Python微服务** | OSS | SDK (oss2) | 读取PDF/Excel | 内网(VPC) | +| **Python微服务** | PostgreSQL | 可选(直接SQL) | 读取配置数据 | 内网(VPC) | +| **Dify** | Weaviate | gRPC | 向量存储/检索 | 本地(Dify内部) | +| **Dify** | PostgreSQL | 原生驱动 | Dify元数据 | 本地(Dify内部) | +| **Dify** | Redis | 原生驱动 | Dify缓存 | 本地(Dify内部) | + +**关键原则:** +- ✅ **单一入口**:前端只调用 Node.js后端,不直接调用其他服务 +- ✅ **内网通信**:SAE内部服务通过内网互联(免流量费,低延迟) +- ✅ **服务隔离**:Dify独立部署在ECS,通过内网暴露API +- ✅ **数据持久化**:核心数据统一存储在RDS,文件存储在OSS + +### 3. 模块责任边界 + +| 模块 | 核心职责 | 不负责 | +|------|---------|--------| +| **前端** | - 用户交互
- 页面渲染
- 路由管理
- API调用 | ❌ 业务逻辑
❌ 权限校验
❌ 直接访问数据库/OSS | +| **Node.js后端** | - 业务逻辑聚合
- 权限控制
- API网关
- 数据库访问
- 调度微服务 | ❌ PDF解析
❌ 数据清洗
❌ 向量检索 | +| **Python微服务** | - PDF解析
- 文档提取
- 数据清洗
- OCR识别 | ❌ 业务逻辑
❌ 权限控制
❌ 数据库CRUD | +| **Dify平台** | - 知识库管理
- 向量存储
- RAG检索
- LLM对话 | ❌ 用户认证
❌ 业务数据存储
❌ 权限管理 | +| **PostgreSQL** | - 业务数据持久化
- 事务管理
- 任务队列(pg-boss) | ❌ 文件存储
❌ 缓存(可选) | +| **OSS** | - 文件存储
- 静态资源
- 备份归档 | ❌ 结构化数据
❌ 实时计算 | + +--- + +## 模块与阿里云服务映射 + +### 1. 服务映射表 + +| 核心模块 | 阿里云服务 | 规格配置 | 成本估算 | 部署文档 | +|---------|-----------|---------|---------|---------| +| **前端 (React)** | SAE容器实例 | 0.5核1GB × 1实例 | ¥50/月 | [06-前端Nginx-SAE容器部署指南](./06-前端Nginx-SAE容器部署指南.md) | +| **Node.js后端** | SAE容器实例 | 1核2GB × 2实例(弹性) | ¥200/月 | [05-Node.js后端-SAE容器部署指南](./05-Node.js后端-SAE容器部署指南.md) | +| **Python微服务** | SAE容器实例 | 1核2GB × 1实例(⚠️ 如OOM升至4GB) | ¥100-200/月 | [04-Python微服务-SAE容器部署指南](./04-Python微服务-SAE容器部署指南.md) | +| **Dify平台** | ECS服务器 | 4核8GB(独立) | ¥300/月 | [03-Dify-ECS部署完全指南](./03-Dify-ECS部署完全指南.md) | +| **PostgreSQL 15** | RDS PostgreSQL | 2核4GB (rds.pg.s2.large) | ¥400/月 | [PostgreSQL部署策略-摸底报告](./PostgreSQL部署策略-摸底报告.md) | +| **文件存储** | OSS标准存储 | 按量付费(50GB) | ¥10/月 | (各模块文档中包含) | +| **⭐ NAT网关** | NAT网关 + EIP | 小规格NAT + 按量EIP | ¥60-100/月 | ⚠️ **必需**(SAE访问公网) | +| **容器镜像** | ACR容器镜像仓库 | 个人版(免费) | ¥0/月 | (各模块文档中包含) | +| **VPC网络** | VPC专有网络 | 默认配置 | ¥0/月 | (各模块文档中包含) | +| **域名/CDN** | (可选)未来配置 | - | ¥50/月 | - | + +**总成本估算:** ¥1,200-1,350/月(初期配置,含NAT网关) + +⚠️ **成本优化说明:** +- NAT网关(¥100/月)是生产环境标配,不建议省略 +- 替代方案:SAE绑定EIP(¥30-50/月,部分地域支持,稳定性较差)或ECS做SNAT(¥0,配置复杂,不推荐) +- Python服务如遇OOM,需升级至4GB内存(增加¥100/月) + +### 2. SAE与5个模块的关系 + +#### 2.1 SAE部署的模块(3个) + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SAE (Serverless Application Engine) │ +│ 弹性伸缩 + 按需付费 │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐│ +│ │ 前端Nginx容器 │ │ Node.js后端容器 │ │ Python容器 ││ +│ ├──────────────────┤ ├──────────────────┤ ├──────────────┤│ +│ │ 实例数:1 │ │ 实例数:2-10 │ │ 实例数:1-3 ││ +│ │ CPU: 0.5核 │ │ CPU: 1核 │ │ CPU: 1核 ││ +│ │ 内存: 1GB │ │ 内存: 2GB │ │ 内存: 2GB ││ +│ │ 健康检查: / │ │ 健康检查: /health│ │ 健康检查: /health││ +│ │ 自动伸缩: 关闭 │ │ 自动伸缩: 开启 │ │ 自动伸缩: 开启││ +│ └──────────────────┘ └──────────────────┘ └──────────────┘│ +│ │ +│ 共同特性: │ +│ ✅ Docker容器部署(镜像从ACR拉取) │ +│ ✅ 内网互联(免流量费) │ +│ ✅ 自动扩缩容(基于CPU/内存/QPS) │ +│ ✅ 滚动发布(零停机部署) │ +│ ✅ 健康检查(自动重启异常实例) │ +│ ✅ 日志自动收集(stdout/stderr → SLS) │ +│ ✅ 环境变量注入(DATABASE_URL等敏感信息) │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +**SAE的核心价值:** +1. **弹性伸缩**:流量高峰自动扩容,低谷自动缩容 +2. **按需付费**:只为实际使用的资源付费 +3. **零运维**:无需管理服务器,自动故障转移 +4. **快速部署**:从镜像启动实例<30秒 + +#### 2.2 非SAE部署的模块(2个) + +| 模块 | 部署方式 | 原因 | +|------|---------|------| +| **Dify平台** | ECS + Docker Compose | - 服务复杂(7个容器)
- 需要稳定IP(内网调用)
- 独立数据库/Redis
- 不需要弹性伸缩 | +| **PostgreSQL 15** | RDS托管服务 | - 企业级数据库
- 自动备份/PITR
- 高可用(主从)
- 无需自己运维 | + +### 3. OSS与5个模块的关系 + +#### 3.1 OSS使用场景 + +| 模块 | 使用OSS的场景 | 读/写 | Bucket目录结构 | +|------|--------------|-------|---------------| +| **Node.js后端** | - 用户上传文件
- 文档管理
- 临时文件中转 | 读/写 | `/uploads/{userId}/{fileId}.pdf` | +| **Python微服务** | - 读取PDF进行解析
- 读取Excel进行清洗
- 写入处理结果 | 读/写 | `/asl/literatures/`
`/dc/extracted/` | +| **Dify平台** | - (可选)上传文档到OSS
- Dify内部使用本地存储 | 不使用 | - | +| **前端** | - 不直接访问OSS
- 通过后端API上传/下载 | 不使用 | - | +| **PostgreSQL** | - (可选)备份归档到OSS | 不使用 | `/backups/` | + +#### 3.2 OSS Bucket设计 + +``` +aiclinical-data-prod (Bucket名称) +│ +├── uploads/ # 用户上传的原始文件 +│ ├── {userId}/ +│ │ ├── {fileId}.pdf +│ │ ├── {fileId}.docx +│ │ └── {fileId}.xlsx +│ │ +├── asl/ # ASL智能文献模块 +│ ├── literatures/ # 文献PDF +│ │ └── {projectId}/{litId}.pdf +│ ├── extracted/ # 提取的文本 +│ │ └── {projectId}/{litId}.json +│ │ +├── dc/ # DC数据清洗模块 +│ ├── raw/ # 原始数据文件 +│ │ └── {taskId}/data.xlsx +│ ├── cleaned/ # 清洗后的数据 +│ │ └── {taskId}/result.xlsx +│ │ +├── pkb/ # PKB个人知识库模块 +│ ├── documents/ # 知识库文档 +│ │ └── {kbId}/{docId}.pdf +│ │ +└── backups/ # 数据库备份(可选) + └── {date}/ai_clinical_research.sql.gz +``` + +**OSS配置要点:** +- ✅ **内网访问**:SAE通过内网访问OSS(免流量费) +- ✅ **权限控制**:使用RAM角色授权,不暴露AccessKey +- ✅ **生命周期**:临时文件30天后自动删除 +- ✅ **跨域配置**:如果前端直传,需配置CORS + +### 4. RDS与5个模块的关系 + +#### 4.1 RDS访问权限 + +| 模块 | 数据库名称 | 访问用户 | 权限 | 连接方式 | +|------|-----------|---------|------|---------| +| **Node.js后端** | ai_clinical_research | aiclinical_rw | SELECT/INSERT/UPDATE/DELETE | Prisma ORM | +| **Python微服务** | ai_clinical_research | aiclinical_rw(可选) | SELECT(只读配置) | psycopg2(可选) | +| **Dify平台** | ❌ 不使用主RDS | - | - | Dify使用独立PostgreSQL | +| **前端** | ❌ 不直接访问 | - | - | 通过后端API | +| **PostgreSQL** | - | postgres(超级用户) | ALL | 仅用于迁移/备份 | + +**连接池配置:** +``` +总连接数:400(RDS默认) +├── Node.js后端:200 (2实例 × 10连接/实例,预留180) +├── Python微服务:50 (1实例 × 5连接/实例,预留45) +├── pg-boss:50 (后台任务) +├── 预留:100 (管理、备份、临时扩容) +``` + +--- + +## 开发环境与部署环境 + +### 1. 环境对比表 + +| 维度 | 本地开发环境 | 阿里云部署环境 | +|------|-------------|---------------| +| **前端** | Vite Dev Server (localhost:3000) | SAE Nginx容器 (HTTPS) | +| **Node.js后端** | tsx watch (localhost:3001) | SAE容器 (内网 + 公网) | +| **Python微服务** | uvicorn (localhost:8000) | SAE容器 (内网) | +| **Dify平台** | Docker Compose (localhost) | ECS Docker Compose (内网) | +| **PostgreSQL** | Docker (localhost:5432) | RDS (内网地址) | +| **OSS** | 本地文件系统 (./uploads) | 阿里云OSS | +| **Redis** | ❌ 不使用(Postgres-Only) | ❌ 不使用(Dify独立Redis) | + +### 2. 开发与部署的平滑迁移 + +#### 2.1 配置管理策略 + +``` +项目根目录/ +│ +├── backend/ +│ ├── .env.local # 本地开发配置(不提交Git) +│ ├── .env.example # 配置模板(提交Git) +│ └── src/config/env.ts # 配置加载逻辑 +│ +├── frontend-v2/ +│ ├── .env.local # 本地开发配置 +│ ├── .env.production # 生产环境配置模板 +│ └── vite.config.ts # Vite配置 +│ +└── extraction_service/ + ├── .env.local # 本地开发配置 + └── app/config.py # Python配置加载 +``` + +**配置加载优先级:** +``` +环境变量 > .env.local > .env > 代码默认值 +``` + +#### 2.2 环境变量对比 + +**本地开发 (`.env.local`):** +```bash +# 数据库 +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ai_clinical_research + +# 存储 +STORAGE_TYPE=local +LOCAL_STORAGE_DIR=./uploads + +# 缓存/队列 +CACHE_TYPE=memory +QUEUE_TYPE=memory + +# 微服务 +DIFY_API_URL=http://localhost/v1 +EXTRACTION_SERVICE_URL=http://localhost:8000 +``` + +**阿里云部署(SAE环境变量配置):** +```bash +# 数据库(⚠️ 敏感信息,SAE加密存储) +DATABASE_URL=postgresql://aiclinical_rw:强密码@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical_research?connection_limit=10&pool_timeout=10 + +# 存储(OSS) +STORAGE_TYPE=oss +OSS_REGION=cn-hangzhou +OSS_BUCKET=aiclinical-data-prod +OSS_ACCESS_KEY_ID=LTAI5***(SAE环境变量) +OSS_ACCESS_KEY_SECRET=***(SAE加密) + +# 缓存/队列(Postgres-Only) +CACHE_TYPE=postgres +QUEUE_TYPE=pgboss + +# 微服务(内网地址)⚠️ 使用内网IP,不要用.sae域名 +DIFY_API_URL=http://172.16.x.x/v1 +EXTRACTION_SERVICE_URL=http://172.16.x.x:8000 +# ⚠️ 注意: +# - 172.16.x.x 需要替换为实际的内网IP +# - 获取方式:SAE控制台 > 应用详情 > 实例列表 > 查看内网IP +# - ❌ 不要使用 .sae 或 .cluster.local 域名(K8s服务发现需要额外配置) + +# 生产环境标识 +NODE_ENV=production +``` + +### 3. 持续部署流程(CI/CD) + +#### 3.1 整体流程图 + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ 开发者本地开发 │ +│ 1. 修改代码 │ +│ 2. 本地测试(npm run dev / pytest) │ +│ 3. Git提交(git commit -m "feat: xxx") │ +│ 4. 推送到远程仓库(git push origin main) │ +└────────────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ CI/CD 流水线(未来:GitHub Actions) │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Stage 1: 构建与测试 │ │ +│ ├────────────────────────────────────────────────────────────┤ │ +│ │ - npm install / pip install │ │ +│ │ - npm run build / docker build │ │ +│ │ - npm test / pytest │ │ +│ │ - Lint检查 (eslint / flake8) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Stage 2: Docker镜像构建 │ │ +│ ├────────────────────────────────────────────────────────────┤ │ +│ │ - docker build -t frontend:v1.2.3 │ │ +│ │ - docker build -t backend:v1.2.3 │ │ +│ │ - docker build -t python-service:v1.2.3 │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Stage 3: 推送到ACR容器镜像仓库 │ │ +│ ├────────────────────────────────────────────────────────────┤ │ +│ │ - docker tag ... registry.cn-hangzhou.aliyuncs.com/... │ │ +│ │ - docker push registry.cn-hangzhou.aliyuncs.com/... │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Stage 4: 部署到SAE │ │ +│ ├────────────────────────────────────────────────────────────┤ │ +│ │ - SAE触发滚动部署 │ │ +│ │ - 健康检查 -> 切换流量 │ │ +│ │ - 自动回滚(如果失败) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ 生产环境运行 │ +│ - 健康检查监控 │ +│ - 日志收集分析 │ +│ - 性能指标监控 │ +└──────────────────────────────────────────────────────────────────┘ +``` + +#### 3.2 初期手工部署流程(推荐) + +**阶段1:初期(1-2人团队,前3个月)** + +```bash +# ===== 前端部署 ===== +cd frontend-v2 +npm run build # 构建生产版本 +docker build -t frontend:v1.0.0 . # 构建Docker镜像 +docker tag frontend:v1.0.0 registry.cn-hangzhou.aliyuncs.com/your-namespace/frontend:v1.0.0 +docker push registry.cn-hangzhou.aliyuncs.com/your-namespace/frontend:v1.0.0 +# 在SAE控制台点击"部署" -> 选择新镜像 -> 确认 + +# ===== 后端部署 ===== +cd backend +npm run build # 编译TypeScript +docker build -t backend:v1.0.0 . +docker tag backend:v1.0.0 registry.cn-hangzhou.aliyuncs.com/your-namespace/backend:v1.0.0 +docker push registry.cn-hangzhou.aliyuncs.com/your-namespace/backend:v1.0.0 +# 在SAE控制台点击"部署" -> 选择新镜像 -> 确认 + +# ===== Python微服务部署 ===== +cd extraction_service +docker build -t python-service:v1.0.0 . +docker tag python-service:v1.0.0 registry.cn-hangzhou.aliyuncs.com/your-namespace/python-service:v1.0.0 +docker push registry.cn-hangzhou.aliyuncs.com/your-namespace/python-service:v1.0.0 +# 在SAE控制台点击"部署" -> 选择新镜像 -> 确认 +``` + +**优势:** +- ✅ 简单直接,学习成本低 +- ✅ 完全可控,出错容易排查 +- ✅ 无需配置CI/CD工具 + +**阶段2:成长期(3-10人团队,3个月后)** + +配置GitHub Actions或阿里云云效自动化部署。 + +#### 3.3 部署频率建议 + +| 模块 | 部署频率 | 建议时间 | 回滚策略 | +|------|---------|---------|---------| +| **前端** | 每周1-2次 | 周五下午 | SAE一键回滚(保留3个版本) | +| **Node.js后端** | 每周1-2次 | 周五下午 | SAE一键回滚 + 数据库回滚脚本 | +| **Python微服务** | 每2周1次 | 周五下午 | SAE一键回滚 | +| **Dify平台** | 按需(很少) | 周末 | Docker Compose重启旧镜像 | +| **PostgreSQL** | 按需(迁移) | 周末凌晨 | RDS快照恢复 | + +### 4. 开发到部署的检查清单 + +#### 4.1 代码准备 + +``` +☐ 1. 本地测试通过 + ├─ npm run dev / uvicorn正常运行 + ├─ 核心功能手工测试 + └─ 单元测试/集成测试通过(如果有) + +☐ 2. 代码审查 + ├─ Lint检查通过(eslint / flake8) + ├─ 代码格式化(prettier / black) + └─ 无敏感信息泄漏(.env文件不提交) + +☐ 3. 环境变量确认 + ├─ 所有生产环境变量已配置到SAE + ├─ 数据库连接字符串正确(含connection_limit) + └─ OSS/Dify等外部服务地址正确 +``` + +#### 4.2 Docker镜像构建 + +``` +☐ 4. Dockerfile优化 + ├─ 使用多阶段构建(减小镜像体积) + ├─ 基础镜像版本固定(如 node:22-alpine,不用latest) + ├─ 非root用户运行(安全) + └─ 健康检查配置正确 + +☐ 5. 镜像构建与测试 + ├─ docker build成功 + ├─ docker run本地测试通过 + ├─ 镜像大小合理(前端<100MB,后端<300MB,Python<1GB) + └─ 镜像打标签(版本号) +``` + +#### 4.3 部署到SAE + +``` +☐ 6. 部署前准备 + ├─ 备份数据库(RDS手动快照) + ├─ 通知团队(如果是重大变更) + └─ 准备回滚方案 + +☐ 7. SAE部署 + ├─ 推送镜像到ACR + ├─ SAE选择新镜像版本 + ├─ 滚动发布(灰度发布,逐个实例替换) + └─ 观察日志和监控 + +☐ 8. 部署后验证 + ├─ 健康检查端点返回200 + ├─ 核心功能冒烟测试 + ├─ 日志无ERROR + └─ 监控指标正常(CPU/内存/响应时间) + +☐ 9. 回滚准备(如果失败) + ├─ SAE一键回滚到上一版本 + ├─ 数据库恢复快照(如果有Schema变更) + └─ 排查问题日志 +``` + +--- + +## Docker版本管理策略 + +### 1. 镜像版本命名规范 + +#### 1.1 语义化版本 (Semantic Versioning) + +``` +格式:{服务名}:{主版本}.{次版本}.{修订版本}[-{环境}] + +示例: +frontend:1.0.0 # 生产版本 +frontend:1.0.0-dev # 开发版本 +frontend:1.1.0 # 功能更新 +frontend:1.1.1 # Bug修复 +``` + +**版本号规则:** +- **主版本 (Major)**:重大架构变更,不兼容旧版本(如 1.x.x → 2.x.x) +- **次版本 (Minor)**:新增功能,向下兼容(如 1.0.x → 1.1.x) +- **修订版本 (Patch)**:Bug修复,完全兼容(如 1.0.0 → 1.0.1) + +#### 1.2 标签策略 + +| 标签类型 | 示例 | 用途 | 保留策略 | +|---------|------|------|---------| +| **版本号** | `v1.2.3` | 正式发布版本 | 永久保留 | +| **latest** | `latest` | 最新稳定版 | 自动更新 | +| **dev** | `dev` | 开发测试版 | 滚动保留最近10个 | +| **Git SHA** | `sha-a1b2c3d` | 特定提交版本 | 按需保留 | +| **日期** | `20251214` | 按日期归档 | 保留30天 | + +**完整示例:** +```bash +# 构建时打多个标签 +docker build -t frontend:v1.2.3 \ + -t frontend:latest \ + -t frontend:sha-a1b2c3d \ + -t frontend:20251214 \ + . + +# 推送到ACR +docker tag frontend:v1.2.3 registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.2.3 +docker tag frontend:v1.2.3 registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:latest +docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.2.3 +docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:latest +``` + +### 2. 本地版本管理 + +#### 2.1 本地是否需要版本管理? + +**答案:初期不需要,成长期建议轻量管理。** + +| 阶段 | 本地管理策略 | 原因 | +|------|-------------|------| +| **初期(1-2人)** | ❌ 不需要 | - 开发者少,口头协调即可
- 本地镜像只用于测试
- 直接推送到ACR统一管理 | +| **成长期(3-10人)** | ✅ 轻量管理 | - 使用Git标签管理版本
- `package.json`/`pyproject.toml`记录版本
- 本地镜像定期清理 | + +#### 2.2 本地镜像清理 + +```bash +# 查看本地镜像 +docker images | grep frontend + +# 删除未使用的镜像(保留最近3个版本) +docker image prune -a --filter "until=720h" # 删除30天前的镜像 + +# 删除所有未使用的镜像(危险,慎用) +docker image prune -a +``` + +**建议清理频率:** +- 开发机:每周清理一次 +- CI/CD服务器:每次构建后自动清理 + +### 3. ACR容器镜像仓库管理 + +#### 3.1 镜像仓库结构 + +``` +阿里云容器镜像服务 (ACR) +├── 命名空间: aiclinical(团队共享) +│ │ +│ ├── 仓库: frontend +│ │ ├── latest(自动更新) +│ │ ├── v1.0.0 +│ │ ├── v1.0.1 +│ │ ├── v1.1.0 +│ │ └── ... (保留最近20个版本) +│ │ +│ ├── 仓库: backend +│ │ ├── latest +│ │ ├── v1.0.0 +│ │ ├── v1.0.1 +│ │ └── ... +│ │ +│ ├── 仓库: python-service +│ │ ├── latest +│ │ ├── v1.0.0 +│ │ └── ... +│ │ +│ └── 仓库: dify (可选,如果自定义构建) +│ └── v0.6.13 +│ +└── 个人版免费配额:3个命名空间,无限仓库数,300GB存储/月 +``` + +#### 3.2 镜像生命周期管理 + +**保留策略:** +```yaml +规则1: 保留最新的20个版本标签 +规则2: 保留所有带"v"前缀的正式版本(v1.0.0、v1.1.0等) +规则3: 自动清理30天前的dev/test标签 +规则4: latest标签永久保留 +``` + +**ACR配置示例(控制台配置):** +``` +容器镜像服务 > 仓库管理 > frontend > 生命周期规则 +├─ 规则1: 保留最近20个版本 +│ ├─ 类型: 按数量保留 +│ ├─ 保留数量: 20 +│ └─ 标签匹配: * +│ +└─ 规则2: 清理旧的dev标签 + ├─ 类型: 按时间清理 + ├─ 保留时间: 30天 + └─ 标签匹配: *-dev +``` + +### 4. 版本与Git的关系 + +#### 4.1 Git标签与Docker标签同步 + +```bash +# 开发流程 +git add . +git commit -m "feat: add user profile page" +git push origin main + +# 发布版本时创建Git标签 +git tag -a v1.2.3 -m "Release v1.2.3: Add user profile" +git push origin v1.2.3 + +# 构建Docker镜像(版本号与Git标签一致) +docker build -t frontend:v1.2.3 . +docker tag frontend:v1.2.3 registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.2.3 +docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.2.3 +``` + +#### 4.2 自动版本号管理(可选) + +**方法1:从 `package.json` 读取版本号** +```json +// frontend-v2/package.json +{ + "name": "aiclinical-frontend", + "version": "1.2.3", + ... +} +``` + +```bash +# 构建脚本自动读取版本号 +VERSION=$(node -p "require('./package.json').version") +docker build -t frontend:v$VERSION . +``` + +**方法2:从Git提交SHA生成版本号** +```bash +# 使用Git SHA作为版本号(适合CI/CD) +GIT_SHA=$(git rev-parse --short HEAD) +docker build -t frontend:sha-$GIT_SHA . +``` + +### 5. 版本管理工具推荐 + +| 工具 | 用途 | 适用阶段 | +|------|------|---------| +| **Git Tags** | 代码版本管理 | ✅ 所有阶段 | +| **package.json/pyproject.toml** | 应用版本声明 | ✅ 所有阶段 | +| **Docker Labels** | 镜像元数据标记 | ✅ 成长期+ | +| **语义化版本工具 (semantic-release)** | 自动版本号生成 | 可选(成熟期) | +| **Harbor/ACR版本管理** | 镜像仓库治理 | ✅ 成长期+ | + +--- + +## 线上故障快速修复 + +### 1. 故障分级与修复策略 + +| 故障等级 | 影响范围 | 修复策略 | 预期RTO | 示例 | +|---------|---------|---------|---------|------| +| **P0(致命)** | 全站不可用 | 立即回滚 | <5分钟 | 数据库连接失败、应用启动失败 | +| **P1(严重)** | 核心功能不可用 | 回滚或热修复 | <30分钟 | 用户无法登录、文件上传失败 | +| **P2(一般)** | 部分功能异常 | 热修复或下个版本 | <2小时 | 某个页面报错、样式错乱 | +| **P3(轻微)** | 用户体验问题 | 下个版本修复 | <1天 | 文案错误、图标缺失 | + +### 2. 修复方案对比 + +| 修复方法 | 适用场景 | 操作步骤 | 优点 | 缺点 | 推荐度 | +|---------|---------|---------|------|------|--------| +| **方案1:SAE一键回滚** | P0/P1故障 | SAE控制台点击"回滚" | ✅ 最快(<5分钟)
✅ 零风险 | ❌ 回到旧版本,新功能丢失 | ⭐⭐⭐⭐⭐ | +| **方案2:热修复(重新部署完整镜像)** | P1/P2故障 | 修改代码→构建镜像→推送→部署 | ✅ 彻底修复
✅ 保留新功能 | ❌ 慢(10-30分钟)
❌ 需要完整构建 | ⭐⭐⭐⭐ | +| **方案3:单文件热更新(不推荐)** | ❌ 不推荐 | SSH进容器→修改文件 | ✅ 快 | ❌ 容器重启后丢失
❌ 无版本记录
❌ 违反Docker理念 | ⭐ | +| **方案4:配置热更新** | 配置错误 | SAE环境变量→重启应用 | ✅ 快(<5分钟)
✅ 无需重新构建 | ❌ 只适用于配置问题 | ⭐⭐⭐⭐ | + +### 3. 详细修复流程 + +#### 3.1 方案1:SAE一键回滚(推荐)⭐⭐⭐⭐⭐ + +**适用场景:** +- 新版本部署后发现严重Bug +- 性能急剧下降 +- 功能完全不可用 + +**操作步骤:** +``` +步骤1: 登录阿里云控制台 → SAE +步骤2: 选择问题应用(如 aiclinical-backend) +步骤3: 点击"部署历史" +步骤4: 找到上一个稳定版本(如 v1.2.2) +步骤5: 点击"回滚"按钮 +步骤6: 确认 → SAE自动执行滚动回滚 +步骤7: 观察健康检查和日志 +步骤8: 验证功能恢复 + +预计时间:3-5分钟 +``` + +**SAE回滚机制:** +``` +回滚过程(滚动回滚,零停机) +├── 阶段1: 从ACR拉取旧版本镜像 +├── 阶段2: 启动1个旧版本实例 +├── 阶段3: 健康检查通过后,切换流量到旧实例 +├── 阶段4: 逐步停止新版本实例 +├── 阶段5: 全部实例回滚完成 +└── 时间:2-3分钟(2实例的情况) +``` + +**回滚后的处理:** +``` +☐ 1. 验证核心功能恢复 +☐ 2. 通知团队回滚完成 +☐ 3. 分析故障原因(日志/监控) +☐ 4. 修复Bug(本地测试充分) +☐ 5. 重新部署修复版本 +``` + +#### 3.2 方案2:热修复(重新部署完整Docker镜像)⭐⭐⭐⭐ + +**适用场景:** +- 无法回滚(旧版本也有问题) +- Bug已定位且修复简单 +- 需要保留新功能 + +**操作步骤:** +```bash +# ===== 场景:后端API返回500错误 ===== + +# 步骤1: 本地复现问题 +npm run dev +# 测试API: POST /api/v1/users +# 错误日志: TypeError: Cannot read property 'email' of undefined + +# 步骤2: 修复代码 +# backend/src/modules/users/userService.ts +# 修复:添加空值校验 +if (!userData || !userData.email) { + throw new Error('Email is required') +} + +# 步骤3: 本地测试通过 +npm run dev +# 重新测试API → 200 OK + +# 步骤4: 构建新镜像(版本号升级) +npm run build +docker build -t backend:v1.2.4 . # 修订版本号+1 + +# 步骤5: 推送到ACR +docker tag backend:v1.2.4 registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.2.4 +docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.2.4 + +# 步骤6: SAE部署 +# 控制台 → 部署 → 镜像版本: v1.2.4 → 确认 → 滚动发布 + +# 步骤7: 观察部署过程 +# SAE日志实时查看 → 无ERROR → 健康检查通过 + +# 步骤8: 验证修复 +curl -X POST https://api.your-domain.com/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com"}' +# 返回: 200 OK + +# 步骤9: 更新Git +git add . +git commit -m "hotfix: add email validation in userService" +git tag v1.2.4 +git push origin main +git push origin v1.2.4 +``` + +**预计时间:10-30分钟** +``` +代码修复: 5分钟 +构建镜像: 3-5分钟(Node.js后端) +推送镜像: 1-2分钟 +SAE部署: 2-3分钟 +验证: 2分钟 +Git提交: 1分钟 +``` + +#### 3.3 方案3:单文件热更新(❌不推荐,仅紧急情况) + +**为什么不推荐?** +1. ❌ **容器重启后丢失**:容器是无状态的,重启后修改消失 +2. ❌ **无版本记录**:无法追溯修改历史 +3. ❌ **多实例不一致**:SAE有多个实例,只修改一个实例无效 +4. ❌ **违反Docker理念**:容器应该是不可变的 + +**如果必须使用(极端紧急情况):** +```bash +# ⚠️ 仅供紧急情况参考,不推荐 + +# 步骤1: 登录SAE实例(通过云助手) +# SAE控制台 → 实例列表 → 登录实例 + +# 步骤2: 找到文件位置 +cd /app +ls -la + +# 步骤3: 修改文件 +vi src/modules/users/userService.js +# 修改错误的代码 + +# 步骤4: 重启应用(SAE会自动重启) +# ⚠️ 注意:这个修改只在当前实例生效,重启后丢失 + +# 步骤5: 立即准备正式修复(方案2) +# 本地修改 → 构建镜像 → 部署 +``` + +**总结:除非系统完全崩溃且无法回滚,否则不要使用单文件热更新!** + +#### 3.4 方案4:配置热更新(环境变量)⭐⭐⭐⭐ + +**适用场景:** +- 数据库连接字符串错误 +- API密钥/Token过期 +- 外部服务地址变更 +- Feature Flag开关 + +**操作步骤:** +``` +# 场景:Dify API地址配置错误 + +步骤1: SAE控制台 → 选择应用 → 环境变量 +步骤2: 找到错误的变量 + DIFY_API_URL=http://172.16.1.100/v1 # ❌ 错误IP + +步骤3: 修改为正确值 + DIFY_API_URL=http://172.16.2.50/v1 # ✅ 正确IP + +步骤4: 保存 → SAE提示"需要重启应用" +步骤5: 点击"重启应用" → 滚动重启(零停机) +步骤6: 观察日志 → 连接成功 +步骤7: 验证功能恢复 + +预计时间:3-5分钟 +``` + +**环境变量修改的注意事项:** +- ✅ 修改后必须重启应用才能生效 +- ✅ 滚动重启,不影响服务可用性 +- ⚠️ 敏感信息(密码)修改后,记得更新本地 `.env.example` +- ⚠️ 如果涉及数据库迁移,确保数据库已准备好 + +### 4. 故障预防与监控 + +#### 4.1 预防措施 + +``` +☐ 1. 充分的本地测试 + ├─ 核心功能手工测试 + ├─ 边界条件测试 + └─ 与生产环境类似的数据测试 + +☐ 2. 灰度发布(SAE支持) + ├─ 先部署到1个实例(10%流量) + ├─ 观察10-30分钟 + └─ 无问题后全量发布 + +☐ 3. 健康检查完善 + ├─ /health 端点检查数据库连接 + ├─ /health 端点检查外部服务(Dify/OSS) + └─ 健康检查失败自动重启 + +☐ 4. 监控告警 + ├─ 错误日志告警(SLS) + ├─ API响应时间告警(ARMS) + └─ 数据库连接数告警(RDS监控) + +☐ 5. 自动化测试(未来) + ├─ 单元测试(Jest / Pytest) + ├─ 集成测试(Supertest / Playwright) + └─ CI/CD中自动执行 +``` + +#### 4.2 监控指标 + +| 指标类型 | 监控指标 | 告警阈值 | 处理措施 | +|---------|---------|---------|---------| +| **应用监控** | 错误率 | >5% | 立即查看日志,考虑回滚 | +| **应用监控** | API响应时间 | >2秒 | 检查数据库慢查询、外部服务 | +| **应用监控** | 健康检查失败 | >3次 | 自动重启实例,检查依赖服务 | +| **数据库监控** | 连接数 | >80% | 检查连接泄漏,增加连接池 | +| **数据库监控** | 慢查询 | >1秒 | 优化SQL,添加索引 | +| **容器监控** | CPU使用率 | >70% | 检查死循环,考虑扩容 | +| **容器监控** | 内存使用率 | >80% | 检查内存泄漏,重启实例 | + +### 5. 故障修复决策树 + +``` + ┌─────────────────┐ + │ 发现线上故障 │ + └────────┬────────┘ + │ + ┌────────▼────────┐ + │ 影响范围评估? │ + └────┬────────────┘ + │ + ┌──────────┼──────────┐ + │ │ + ┌───────▼────────┐ ┌──────▼────────┐ + │ 全站不可用 │ │ 部分功能异常 │ + │ (P0/P1) │ │ (P2/P3) │ + └───────┬────────┘ └──────┬────────┘ + │ │ + ┌───────▼────────┐ ┌──────▼────────┐ + │ 能否快速定位? │ │ 能否快速修复? │ + └───┬────────┬───┘ └──┬────────┬───┘ + │ No │ Yes │ Yes │ No + │ │ │ │ + ┌─────▼──┐ ┌──▼─────┐ ┌──▼──┐ ┌───▼────┐ + │ 立即 │ │ 是配置 │ │ 热修│ │ 下个版 │ + │ 回滚 │ │ 问题? │ │ 复 │ │ 本修复 │ + └────────┘ └──┬──┬──┘ └─────┘ └────────┘ + │ │ + ┌─────▼──▼────┐ + │ 修改环境变量 │ + │ + 重启应用 │ + └──────────────┘ +``` + +--- + +## 部署文档导航 + +### 📚 独立部署文档清单 + +| # | 文档名称 | 部署对象 | 目标环境 | 预计时间 | +|---|---------|---------|---------|---------| +| 1 | [PostgreSQL部署策略-摸底报告](./PostgreSQL部署策略-摸底报告.md) | PostgreSQL 15数据库 | RDS PostgreSQL 15 | 10分钟(pg_dump导入) | +| 2 | [Dify-ECS部署完全指南](./03-Dify-ECS部署完全指南.md) | Dify RAG平台 | ECS + Docker Compose | 30-60分钟 | +| 3 | [Python微服务-SAE容器部署指南](./04-Python微服务-SAE容器部署指南.md) | Python微服务 | SAE容器实例 | 20-30分钟 | +| 4 | [Node.js后端-SAE容器部署指南](./05-Node.js后端-SAE容器部署指南.md) | Node.js后端 | SAE容器实例 | 20-30分钟 | +| 5 | [前端Nginx-SAE容器部署指南](./06-前端Nginx-SAE容器部署指南.md) | React前端 | SAE容器实例 | 15-20分钟 | + +**总部署时间:** 约2-3小时(从0到全部上线) + +### 🎯 部署顺序建议 + +``` +第一阶段:基础设施(Day 1)⭐ 必需 +├── 1. 创建VPC和安全组 (10分钟) +├── 2. ⭐ 创建NAT网关 + 绑定EIP (15分钟) ← 新增,必需! +├── 3. 部署RDS PostgreSQL 15 (10分钟,等待实例创建15分钟) +├── 4. 导入数据库(pg_dump) (5分钟) +└── 5. 创建OSS Bucket (5分钟) + └── 配置生命周期、权限 + +第二阶段:核心服务(Day 1)⚠️ 临时配置 +├── 6. 部署Node.js后端到SAE (20分钟) +│ └── ⚠️ DIFY_API_KEY=temp(临时值,等Dify部署后更新) +├── 7. 部署Python微服务到SAE (20分钟) +└── 8. 端到端测试(后端API + Python服务) (15分钟) + +第三阶段:用户界面(Day 2) +├── 9. 部署前端到SAE (15分钟) +└── 10. 完整功能测试(不含PKB模块) (30分钟) + +第四阶段:Dify服务(Day 2-3)⭐ PKB模块必需 +├── 11. 部署Dify到ECS (60分钟) +├── 12. 登录Dify生成API Key (5分钟) +├── 13. 更新后端DIFY_API_KEY环境变量 (5分钟) +└── 14. 测试PKB知识库功能 (15分钟) + +总计:约4-5小时(实际操作时间,不含等待) +``` + +### 📖 如何使用这些文档? + +#### 新团队首次部署 + +``` +步骤1: 阅读本《部署架构总览》 + ├─ 理解整体架构 + ├─ 理解模块依赖关系 + └─ 熟悉阿里云服务 + +步骤2: 按顺序阅读独立部署文档 + ├─ PostgreSQL部署策略(理解数据库架构) + ├─ Node.js后端部署(核心服务) + ├─ Python微服务部署(能力支撑) + ├─ 前端Nginx部署(用户界面) + └─ Dify部署(可选,PKB模块需要) + +步骤3: 准备阿里云账号和资源 + ├─ 实名认证 + ├─ 充值(建议¥500起步) + ├─ 创建RAM子账号(安全) + └─ 开通SAE/RDS/OSS/ACR服务 + +步骤4: 按第一阶段→第二阶段→第三阶段顺序部署 + └─ 每个阶段完成后测试验证 + +步骤5: 监控与优化 + ├─ 配置告警规则 + ├─ 观察7天运行情况 + └─ 根据实际情况调整规格 +``` + +#### 已有系统维护 + +``` +日常开发部署: +├─ 修改代码 +├─ 本地测试 +├─ 构建Docker镜像 +├─ 推送到ACR +└─ SAE控制台部署 + +线上故障处理: +├─ 参考"线上故障快速修复"章节 +├─ P0/P1: 立即回滚 +├─ P2/P3: 热修复或下个版本 +└─ 故障复盘,更新文档 + +新模块上线: +├─ 参考对应的独立部署文档 +├─ 确认依赖服务(数据库/OSS/其他服务) +├─ 灰度发布(先1个实例) +└─ 全量发布 +``` + +### 🔗 文档间的引用关系 + +``` +00-部署架构总览.md (本文档) +│ +├──► PostgreSQL部署策略-摸底报告.md +│ ├─ 数据库架构详解 +│ ├─ pg_dump导入流程 +│ ├─ 备份策略 +│ └─ 最佳实践 +│ +├──► 03-Dify-ECS部署完全指南.md +│ ├─ Docker Compose配置 +│ ├─ Nginx路由层 +│ ├─ Swap配置 +│ └─ 故障排查 +│ +├──► 04-Python微服务-SAE容器部署指南.md +│ ├─ Dockerfile多阶段构建 +│ ├─ 系统依赖安装(PyTorch/OpenCV) +│ ├─ SAE配置 +│ └─ 内网调用 +│ +├──► 05-Node.js后端-SAE容器部署指南.md +│ ├─ Prisma反向同步(db pull) +│ ├─ 数据库连接池 +│ ├─ 优雅关闭 +│ └─ 环境变量配置 +│ +└──► 06-前端Nginx-SAE容器部署指南.md + ├─ SPA路由配置 + ├─ API反向代理 + ├─ 静态资源缓存 + └─ envsubst动态配置 +``` + +--- + +## 🛡️ 安全与最佳实践 + +### 1. 时区统一配置 ⭐⭐⭐⭐⭐ + +**问题:** 不同服务的时区不一致会导致: +- 日志时间对不上(前端14:00,后端06:00) +- pg-boss定时任务在错误时间触发 +- 用户看到的时间戳错误 + +**解决:** 所有服务统一使用 `Asia/Shanghai` 时区 + +```dockerfile +# backend/Dockerfile - Node.js后端 +FROM node:22-alpine AS runtime +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai +# ... 其他配置 + +# extraction_service/Dockerfile - Python微服务 +FROM python:3.11-slim AS runtime +RUN apt-get update && apt-get install -y tzdata +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# ... 其他配置 + +# frontend-v2/Dockerfile - 前端(已配置) +FROM nginx:1.25-alpine +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai +# ... 其他配置 +``` + +```sql +-- RDS PostgreSQL 时区配置 +-- RDS控制台 > 参数设置 > timezone +timezone = Asia/Shanghai +``` + +**验证时区:** +```bash +# 查看容器时区 +docker exec date +# 应该显示:Sat Dec 14 14:30:00 CST 2024 + +# 查看RDS时区 +psql -c "SHOW timezone;" +# 应该显示:Asia/Shanghai +``` + +### 2. 安全组配置最佳实践 ⭐⭐⭐⭐⭐ + +#### ECS安全组配置(Dify服务器) + +**正确配置:** +```bash +# ECS控制台 > 安全组 > 配置规则 + +# ✅ 入方向规则(Inbound) +允许 80/TCP 来源:172.16.0.0/12 # Nginx(VPC内网访问) +允许 22/TCP 来源:您的办公室公网IP # SSH管理 +拒绝 所有 来源:0.0.0.0/0 # 默认拒绝 +``` + +**⚠️ 安全红线(绝对禁止):** +```bash +# ❌ 禁止 5000/TCP(Dify API)对公网开放 +# 后果:黑客可绕过后端消耗LLM额度 + +# ❌ 禁止 8080/TCP(Weaviate)对公网开放 +# 后果:向量数据可能被窃取 + +# ❌ 禁止 6379/TCP(Redis)对公网开放 +# 后果:Redis无密码,可能被清空 + +# ✅ 正确:docker-compose.yaml中端口绑定 +services: + redis: + ports: + - "127.0.0.1:6379:6379" # 只监听本地 + weaviate: + ports: + - "127.0.0.1:8080:8080" # 只监听本地 +``` + +**验证安全配置:** +```bash +# 从公网测试(应该失败) +telnet ECS公网IP 6379 +# 应该超时或拒绝连接 + +# 从VPC内测试(应该成功) +curl http://172.16.x.x # Dify内网地址 +# 应该返回 Dify 响应 +``` + +🔗 **详细配置:** 参考 `03-Dify-ECS部署完全指南.md` + +### 3. 镜像拉取策略 ⭐⭐⭐⭐⭐ + +**问题:** SAE可能不会拉取新镜像(代码不更新的"灵异事件") + +```bash +# 场景: +开发者修改代码 → 构建镜像 → 推送到ACR(覆盖v1.0.0) +→ SAE部署 → 发现代码没更新??? + +# 原因: +SAE默认镜像拉取策略可能是 IfNotPresent +如果本地已有 v1.0.0,不会重新拉取 +``` + +**解决方案:** + +| 方案 | 适用环境 | 优势 | 劣势 | +|------|---------|------|------| +| **方案A:版本号管理** | 生产环境 | 版本可追溯,稳定 | 需手动递增版本号 | +| **方案B:Always拉取** | 测试环境 | 始终最新,方便 | 每次重启拉取,慢 | + +**方案A:每次部署使用新版本号(强烈推荐)** +```bash +# 使用语义化版本号 +v1.0.0 → v1.0.1 → v1.0.2 ... + +# 或使用时间戳 +v20251214-1430 → v20251214-1530 ... + +# 或使用Git SHA +v-a1b2c3d → v-b2c3d4e ... +``` + +**方案B:配置SAE镜像拉取策略** +```bash +# SAE控制台 > 应用配置 > 镜像设置 +镜像拉取策略:Always + +# ⚠️ 注意: +# - 每次重启都会拉取镜像(启动稍慢) +# - 适合测试环境,不推荐生产环境 +``` + +### 4. 开发调试最佳实践 + +#### 本地连接RDS数据库(SSH隧道) + +**场景:** 开发人员需要用Navicat/DBeaver查看RDS数据 + +**问题:** RDS在VPC内网,本地无法直接访问 + +**解决:** 通过ECS建立SSH隧道 + +```bash +# 步骤1:建立SSH隧道 +ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \ + root@ECS公网IP \ + -i ~/.ssh/dify-ecs.pem + +# 参数说明: +# -N: 不执行远程命令,只建立隧道 +# -L: 本地端口转发 +# 5433: 本地监听端口(避免与本地PostgreSQL 5432冲突) +# rm-xxxxx: RDS内网地址 +# 5432: RDS端口 + +# 步骤2:Navicat/DBeaver连接配置 +主机:localhost +端口:5433 +用户名:aiclinical_rw +密码:(RDS密码) +数据库:ai_clinical_research + +# 步骤3:测试连接 +# 应该成功连接到RDS! +``` + +**后台运行(可选):** +```bash +# 方法1:nohup后台运行 +nohup ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \ + root@ECS-IP -i key.pem > /dev/null 2>&1 & + +# 方法2:保持隧道运行(新终端窗口) +ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \ + root@ECS-IP -i key.pem +# 保持此终端窗口打开 +``` + +🔗 **详细步骤参考:** `07-关键配置补充说明.md` 第7节 + +### 5. Python服务内存管理 ⭐⭐⭐⭐ + +**问题:** Python服务(PyMuPDF/Nougat)内存密集,容易OOM + +| 场景 | CPU | 内存 | Workers | 适用情况 | +|------|-----|------|---------|---------| +| **基础版** | 1核 | 2GB | 2 | 简单PDF解析 | +| **标准版** | 2核 | 4GB | 3 | 包含OCR(Nougat) | +| **增强版** | 2核 | 8GB | 4 | 大量OCR + 高并发 | + +**⚠️ 重要提示:** +- 如果遇到OOM(Out of Memory),优先升级内存至4GB +- 或者限制Gunicorn并发数:`--workers 1 --threads 2` +- PyMuPDF + Nougat OCR单个请求可能占用500MB-1GB内存 + +**Dockerfile优化(已应用):** +```dockerfile +CMD ["gunicorn", "main:app", \ + "--bind", "0.0.0.0:8000", \ + "--workers", "2", \ + "--timeout", "120", \ + "--max-requests", "100", \ + "--max-requests-jitter", "10"] +``` + +🔗 **详细配置:** 参考 `04-Python微服务-SAE容器部署指南.md` + +### 6. NAT网关成本优化 + +**当前方案:NAT网关(推荐)** +``` +成本:NAT网关¥60/月 + EIP流量费¥30-50/月 = ¥90-110/月 +优势:稳定可靠,生产环境标配 +劣势:成本相对较高 +``` + +**替代方案(不推荐,仅供参考):** + +| 方案 | 成本 | 稳定性 | 复杂度 | 推荐度 | +|------|------|--------|--------|--------| +| NAT网关 | ¥100/月 | ⭐⭐⭐⭐⭐ | 低 | ⭐⭐⭐⭐⭐(推荐)| +| SAE绑定EIP | ¥30-50/月 | ⭐⭐⭐ | 中 | ⭐⭐⭐(部分地域)| +| ECS做SNAT | ¥0(复用ECS) | ⭐⭐ | 高 | ⭐⭐(不推荐)| + +**建议:** 初创团队不要在这里省钱,NAT网关是生产环境的标配。 + +--- + +## 📝 总结与建议 + +### 核心架构特点 + +1. **✅ 云原生Serverless架构** + - 前端/后端/Python全部容器化部署到SAE + - 弹性伸缩,按需付费 + - 零运维,自动故障转移 + +2. **✅ 微服务隔离** + - 5个核心模块职责清晰 + - 内网通信,免流量费 + - 独立部署,互不影响 + +3. **✅ 数据安全可控** + - PostgreSQL 10个Schema隔离 + - RDS自动备份 + PITR + - VPC内网隔离,白名单控制 + +4. **✅ 适合初创团队** + - 1-2人可运维 + - 手工部署简单直接 + - 成本可控(¥1,100/月起) + +### 下一步行动 + +``` +☐ 第1周:基础设施搭建 + ├─ 阿里云账号准备 + ├─ VPC/RDS/OSS创建 + └─ 数据库导入 + +☐ 第2周:核心服务部署 + ├─ Node.js后端上线 + ├─ Python微服务上线 + ├─ 前端上线 + └─ 端到端测试 + +☐ 第3周:可选服务 + 监控 + ├─ Dify部署(如需PKB模块) + ├─ 配置监控告警 + └─ 压力测试 + +☐ 第4周:稳定性优化 + ├─ 根据监控数据调整资源 + ├─ 故障演练(回滚、修复) + └─ 文档完善 +``` + +### 持续改进建议 + +| 阶段 | 改进方向 | 优先级 | +|------|---------|--------| +| **3个月内** | - 完善监控告警
- 编写运维手册
- 故障演练 | ⭐⭐⭐⭐⭐ | +| **6个月内** | - 配置CI/CD
- 自动化测试
- 性能优化 | ⭐⭐⭐⭐ | +| **1年内** | - 跨地域灾备
- 容器编排升级(ACK)
- 微服务治理 | ⭐⭐⭐ | + +--- + +**文档创建人:** AI助手 +**最后更新:** 2025-12-14 +**版本:** v1.0 + +**核心理念:架构清晰、职责分离、云原生、易于运维** ⭐⭐⭐ + diff --git a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md index 9ac3a7e4..a9e02a07 100644 --- a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md +++ b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md @@ -853,3 +853,4 @@ ACR镜像仓库: + diff --git a/docs/05-部署文档/03-Dify-ECS部署完全指南.md b/docs/05-部署文档/03-Dify-ECS部署完全指南.md new file mode 100644 index 00000000..48643cd6 --- /dev/null +++ b/docs/05-部署文档/03-Dify-ECS部署完全指南.md @@ -0,0 +1,1222 @@ +# Dify ECS 部署完全指南 + +**文档版本**: v1.1 (修复致命配置错误) +**创建时间**: 2025-12-13 +**最后修订**: 2025-12-13 +**适用范围**: AIclinicalresearch 平台 - Dify RAG 服务部署 +**目标读者**: 运维工程师、后端开发工程师 + +**v1.1 更新日志**: +- ✅ 修复:Redis 配置从 `localhost` 改为 `redis`(致命错误) +- ✅ 新增:Swap 分区配置步骤(避免 OOM) +- ✅ 修正:强调使用官方 docker-compose.yaml(包含 Nginx 网关) +- ✅ 完善:故障排查章节,新增 Redis 连接失败和 CORS 错误的解决方案 + +--- + +## 📋 文档目录 + +1. [为什么选择 ECS + Docker Compose](#为什么选择-ecs--docker-compose) +2. [部署架构图](#部署架构图) +3. [前置准备清单](#前置准备清单) +4. [ECS 服务器配置](#ecs-服务器配置) +5. [安装步骤](#安装步骤) +6. [配置说明](#配置说明) +7. [启动与验证](#启动与验证) +8. [监控与维护](#监控与维护) +9. [故障排查](#故障排查) +10. [注意事项与禁忌](#注意事项与禁忌) + +--- + +## 为什么选择 ECS + Docker Compose + +### ✅ 核心原因 + +#### 1. **数据合规与隐私(医疗行业红线)** + +``` +❌ Dify 云服务 (SaaS):数据上传到第三方服务器 +✅ ECS 自建:数据 100% 在自己的阿里云账号内 +``` + +**影响**: +- 三甲医院对科研数据出院有极严格管控 +- 您可以对客户说:"数据就在我们的私有云服务器上,未来可部署在医院内网" +- 符合 PRD 中的核心要求:"数据 100% 本地" + +#### 2. **商业模式适配(私有化部署能力)** + +``` +您的商业模式(PRD): + ├─ SaaS 版(多租户) + ├─ 私有化部署(医院内网) + └─ 单机版(Windows/Mac) +``` + +**关键**: +- Dify 云服务无法私有化交付(医院内网无法连接公有云) +- ECS + Docker Compose 可以直接打包成安装包,卖给医院做私有化部署 +- 您现在的架构选型,是在为未来的商业模式铺路 + +#### 3. **技术迁移成本(避开"以后再切回来"的陷阱)** + +``` +RAG 系统迁移的复杂度: + 1. 文档被切片 (Chunking) + 2. 切片被向量化 (Embedding) + 3. 存入向量数据库 (Weaviate/Qdrant) + 4. Dify 云服务的向量数据很难无损导出 +``` + +**后果**:3 个月后想切回自建,可能面临所有知识库需要重新上传、重新解析、重新嵌入 + +#### 4. **网络性能与成本** + +| 路径对比 | Dify 云服务 | ECS 自建 | +|---------|-----------|---------| +| 网络路径 | SAE → 公网 → Dify → 公网 → SAE | SAE → 阿里云内网(VPC) → ECS | +| 延时 | 100-300ms | 1-5ms | +| 流量成本 | 消耗公网带宽 | 内网免费 | +| 大文件处理 | 慢且不稳定 | 毫秒级响应 | + +**影响场景**: +- PKB 模块的批量文档上传 +- ASL 模块的深度阅读 (Deep Read) +- 大量文档的向量化处理 + +--- + +## 部署架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 阿里云 VPC (同一区域) │ +│ │ +│ ┌─────────────┐ ┌─────────────────────────────┐ │ +│ │ SAE (后端) │ ←内网→ │ ECS (Dify 服务) │ │ +│ │ │ │ │ │ +│ │ Node.js │ │ ┌─────────────────────┐ │ │ +│ │ Backend │ │ │ Docker Compose: │ │ │ +│ │ │ │ │ - Dify API │ │ │ +│ │ │ │ │ - Weaviate (向量DB) │ │ │ +│ │ │ │ │ - Nginx (反向代理) │ │ │ +│ │ │ │ └─────────────────────┘ │ │ +│ └─────────────┘ └─────────────────────────────┘ │ +│ │ │ +│ └──────────────→ RDS PostgreSQL 15 │ +│ (共享数据库) │ +└─────────────────────────────────────────────────────────────┘ +``` + +**关键点**: +- SAE 和 ECS 必须在同一个 VPC(内网通信) +- Dify 的 PostgreSQL 数据库可以和后端共用同一个 RDS 实例(不同 Schema) +- 向量数据库 Weaviate 运行在 ECS 的 Docker 中 + +--- + +## 前置准备清单 + +### ✅ 必需资源 + +| 资源类型 | 配置建议 | 预估费用 | 用途 | +|---------|---------|---------|-----| +| **ECS 服务器** | 2核4G / 系统盘40GB / 数据盘100GB | ~200元/月 | 运行 Dify + Weaviate | +| **RDS PostgreSQL** | 已有(共用) | 0元(增量) | Dify 元数据存储 | +| **OSS 存储** | 已有(共用) | 按量计费 | 文档存储 | +| **VPC 网络** | 已有(SAE所在VPC) | 0元 | 内网通信 | + +### ✅ 软件准备 + +```bash +# 本地开发机器需要安装(用于推送镜像) +- Docker Desktop +- 阿里云 CLI(可选) + +# ECS 服务器需要安装(远程执行) +- Docker Engine +- Docker Compose +``` + +### ✅ 账号与权限 + +- 阿里云账号(已有) +- ECS 登录凭证(SSH密钥对) +- Docker 镜像仓库(阿里云容器镜像服务 ACR) + +--- + +## ECS 服务器配置 + +### 步骤 1:购买 ECS 实例 + +1. **登录阿里云控制台** → **云服务器 ECS** → **创建实例** + +2. **基础配置**: + ``` + 地域和可用区: 与 SAE 相同(如:华东1-杭州) + 实例规格: ecs.t6-c1m2.large (2核4G) + 镜像: Ubuntu 22.04 LTS 64位 + 存储: + - 系统盘: ESSD云盘 40GB + - 数据盘: ESSD云盘 100GB (挂载到 /data) + ``` + +3. **网络配置**: + ``` + 专有网络 VPC: 选择 SAE 所在的 VPC + 安全组: 创建新安全组,配置入方向规则(⚠️ 安全红线): + ✅ 允许 22/TCP 来源:您的办公室公网IP # SSH管理 + ✅ 允许 80/TCP 来源:172.16.0.0/12 # Nginx(VPC内网访问) + ❌ 拒绝 5000/TCP 来源:0.0.0.0/0 # Dify API禁止公网访问 + ❌ 拒绝 6379/TCP 来源:0.0.0.0/0 # Redis禁止公网访问 + ❌ 拒绝 8080/TCP 来源:0.0.0.0/0 # Weaviate禁止公网访问 + ✅ 拒绝 所有 来源:0.0.0.0/0 # 默认拒绝 + 公网IP: 分配公网IPv4地址(用于SSH登录和镜像下载) + + ⚠️ 安全警告: + - Dify API (5000)、Redis (6379)、Weaviate (8080) 绝对不能对公网开放 + - 只允许VPC内网访问(172.16.0.0/12) + - 端口绑定到 127.0.0.1(见docker-compose.yaml配置) + ``` + +4. **系统配置**: + ``` + 登录凭证: 使用密钥对(推荐)或密码 + 实例名称: dify-ecs-prod + ``` + +### 步骤 2:连接到 ECS + +```bash +# Windows 用户使用 PowerShell 或 Git Bash +ssh -i "your-key.pem" root@ + +# 首次登录后,更新系统 +apt update && apt upgrade -y +``` + +### 步骤 3:挂载数据盘 + +```bash +# 查看磁盘 +lsblk + +# 格式化数据盘(假设为 /dev/vdb) +mkfs.ext4 /dev/vdb + +# 创建挂载点 +mkdir -p /data + +# 挂载 +mount /dev/vdb /data + +# 设置开机自动挂载 +echo '/dev/vdb /data ext4 defaults 0 0' >> /etc/fstab + +# 验证 +df -h +``` + +### 步骤 4:配置 Swap 分区(关键!避免 OOM) + +**为什么必须配置 Swap?** +- 您的 ECS 是 2核4G 内存 +- Dify 全家桶(API+Worker+Web+Redis+Weaviate+Nginx)空载就需要 3GB+ +- 有人上传大文件或进行向量检索时,极易发生 OOM(内存溢出)崩溃 +- Swap 是内存的"救生圈",关键时刻能保证服务不崩溃 + +```bash +# 创建 4GB Swap 分区 +fallocate -l 4G /swapfile + +# 设置权限(必须是 600,否则不安全) +chmod 600 /swapfile + +# 格式化为 Swap +mkswap /swapfile + +# 启用 Swap +swapon /swapfile + +# 设置开机自动挂载 +echo '/swapfile none swap sw 0 0' >> /etc/fstab + +# 验证 Swap 已启用 +free -h +# 应该看到 Swap 一行显示 4GB + +# 优化 Swap 使用策略(可选) +# swappiness=10 表示尽量使用物理内存,只在必要时用 Swap +echo 'vm.swappiness=10' >> /etc/sysctl.conf +sysctl -p +``` + +### 步骤 5:安装 Docker + +```bash +# 1. 卸载旧版本(如果有) +apt remove docker docker-engine docker.io containerd runc + +# 2. 安装依赖 +apt install -y ca-certificates curl gnupg lsb-release + +# 3. 添加 Docker 官方 GPG key +mkdir -p /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +# 4. 设置 Docker 仓库 +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + +# 5. 安装 Docker Engine +apt update +apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# 6. 启动 Docker +systemctl start docker +systemctl enable docker + +# 7. 验证安装 +docker --version +docker compose version +``` + +### 步骤 6:配置 Docker 镜像加速(可选但推荐) + +```bash +# 阿里云镜像加速器(登录阿里云控制台获取专属地址) +mkdir -p /etc/docker +cat > /etc/docker/daemon.json <.mirror.aliyuncs.com"] +} +EOF + +systemctl daemon-reload +systemctl restart docker +``` + +--- + +## 安装步骤 + +### 步骤 1:准备 Dify 部署目录 + +```bash +# 在 ECS 上创建目录 +mkdir -p /data/dify +cd /data/dify + +# 克隆 Dify 官方仓库(使用稳定版本) +git clone https://github.com/langgenius/dify.git +cd dify + +# 切换到稳定版本(建议使用最新的 release 版本) +git checkout tags/0.11.2 # 替换为最新稳定版本 + +# 进入 docker 目录 +cd docker + +# ⚠️ 重要:使用官方的 docker-compose.yaml +# 不要自己手写或大幅删减!官方配置包含了完整的服务(含 Nginx 网关) +``` + +### 步骤 2:配置环境变量 + +```bash +# 复制配置模板 +cp .env.example .env + +# 编辑配置文件 +nano .env +``` + +**关键配置项**(必须修改): + +```bash +# ========= 核心配置 ========= +# 安装模式:self-hosted(自建) +INSTALL_MODE=self-hosted + +# 密钥(必须修改,使用强密码) +SECRET_KEY=<生成一个随机字符串,至少32位> +# 生成方法:openssl rand -base64 32 + +# ========= 数据库配置 ========= +# 使用阿里云 RDS PostgreSQL +DB_TYPE=postgres +DB_HOST= +DB_PORT=5432 +DB_NAME=dify_prod +DB_USERNAME=dify_user +DB_PASSWORD= + +# 注意:需要在 RDS 中创建单独的数据库和用户 +# 执行:CREATE DATABASE dify_prod; +# CREATE USER dify_user WITH PASSWORD 'your-password'; +# GRANT ALL PRIVILEGES ON DATABASE dify_prod TO dify_user; + +# ========= Redis 配置 ========= +# Dify 必须使用 Redis(不能用 Postgres-Only) +# ⚠️ 重要:在 Docker Compose 网络中,使用服务名而不是 localhost! +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# ========= 向量数据库配置 ========= +# 使用 Weaviate(Docker Compose 自动启动) +VECTOR_STORE=weaviate +WEAVIATE_ENDPOINT=http://weaviate:8080 +WEAVIATE_API_KEY=<生成一个随机字符串> + +# ========= 存储配置 ========= +# 使用阿里云 OSS(推荐) +STORAGE_TYPE=aliyun-oss +ALIYUN_OSS_BUCKET_NAME= +ALIYUN_OSS_REGION=oss-cn-hangzhou +ALIYUN_OSS_ACCESS_KEY_ID= +ALIYUN_OSS_ACCESS_KEY_SECRET= + +# 或使用本地存储(不推荐) +# STORAGE_TYPE=local + +# ========= LLM 配置 ========= +# 默认 LLM 提供商(您可以在 Web UI 中添加多个) +DEFAULT_LLM_PROVIDER=openai-api-compatible + +# DeepSeek API(示例) +DEEPSEEK_API_KEY= +DEEPSEEK_API_BASE=https://api.deepseek.com/v1 + +# ========= API 配置 ========= +# API 服务地址(内网地址) +API_URL=http://:5000 + +# Web 服务地址(如果需要访问 Dify Web UI) +WEB_API_URL=http:// +WEB_URL=http:// + +# ========= 日志配置 ========= +LOG_LEVEL=INFO +LOG_FILE=/data/logs/dify.log + +# ========= 其他配置 ========= +# 时区 +TZ=Asia/Shanghai + +# 是否开启 Debug +DEBUG=false +``` + +### 步骤 3:检查 Docker Compose 文件(建议使用官方配置) + +```bash +# ⚠️ 强烈建议:不要手写 docker-compose.yaml! +# 直接使用官方文件,它包含了完整的架构(含 Nginx 网关) + +# 查看官方配置 +cat docker-compose.yaml +``` + +**官方架构包含的服务**: + +```yaml +services: + # 1. nginx - 入口网关(必需!) + # 作用:路由请求到 API 和 Web,解决 CORS 问题 + # /v1/* → API 服务 + # /* → Web 服务 + + # 2. api - Dify API 服务 + # 3. worker - 后台任务处理 + # 4. web - Web 前端界面 + # 5. redis - 缓存和任务队列(必需) + # 6. weaviate - 向量数据库 + # 7. sandbox - 代码解释器(可选,但建议保留) + # 作用:如果在 Dify 中使用"代码解释器"功能 + + # 如果你删除了 nginx,会遇到: + # - 前端访问 API 时 CORS 跨域错误 + # - API 路由 404 错误 + + # 如果你删除了 sandbox,会遇到: + # - "代码解释器"功能报错 +``` + +**仅需修改的部分**(如果有特殊需求): + +```yaml +# ⚠️ 安全配置:端口绑定到本地(防止公网访问) +services: + redis: + ports: + - "127.0.0.1:6379:6379" # ⚠️ 只监听本地,禁止公网访问 + + weaviate: + ports: + - "127.0.0.1:8080:8080" # ⚠️ 只监听本地,禁止公网访问 + + # 示例:如果需要外网访问 Web UI(通过Nginx) + nginx: + ports: + - "80:80" # HTTP(允许VPC内网访问) + - "443:443" # HTTPS(需要配置证书) +``` + +**⚠️ 安全红线:** +```yaml +# ❌ 绝对禁止(会被攻击) +services: + redis: + ports: + - "6379:6379" # 全世界可访问,Redis无密码会被清空 + + weaviate: + ports: + - "8080:8080" # 向量数据可能被窃取 + + api: + ports: + - "5000:5000" # 黑客可绕过后端消耗LLM额度 + +# ✅ 正确做法:绑定到127.0.0.1 +services: + redis: + ports: + - "127.0.0.1:6379:6379" # 只能本机访问 +``` + +**❌ 错误做法**: +```yaml +# 不要删减服务!尤其是 nginx +services: + api: ... + worker: ... + # ❌ 缺少 nginx(会导致前端无法访问 API) +``` + +**✅ 正确做法**: +```bash +# 保持官方 docker-compose.yaml 不变 +# 只修改 .env 环境变量 +# 如果确实需要调整,只添加端口映射等无害配置 +``` + +### 步骤 4:创建必需目录 + +```bash +mkdir -p /data/dify/storage +mkdir -p /data/dify/logs +mkdir -p /data/dify/redis +mkdir -p /data/dify/weaviate + +# 设置权限 +chmod -R 755 /data/dify +``` + +### 步骤 5:初始化数据库 + +```bash +# 在 RDS 中创建 Dify 数据库(使用 psql 或 DBeaver) +# 连接到 RDS +psql -h -U postgres -d postgres + +# 执行 +CREATE DATABASE dify_prod; +CREATE USER dify_user WITH PASSWORD 'your-password'; +GRANT ALL PRIVILEGES ON DATABASE dify_prod TO dify_user; +\q +``` + +--- + +## 启动与验证 + +### 步骤 1:启动 Dify 服务 + +```bash +cd /data/dify/docker + +# 拉取镜像(首次运行) +docker compose pull + +# 启动服务(后台运行) +docker compose up -d + +# 查看日志 +docker compose logs -f api + +# 等待服务启动(约30秒) +``` + +### 步骤 2:验证服务状态 + +```bash +# 1. 检查容器状态 +docker compose ps + +# 应该看到以下容器都是 Up 状态: +# - dify-api +# - dify-worker +# - dify-web (如果启用) +# - redis +# - weaviate + +# 2. 检查 API 健康状态 +curl http://localhost:5000/health + +# 期望返回: {"status": "healthy"} + +# 3. 检查 Weaviate 状态 +curl http://localhost:8080/v1/meta + +# 期望返回: JSON 格式的元数据 +``` + +### 步骤 3:测试内网连通性 + +```bash +# 在 SAE 后端服务中测试(或在本地通过 VPN 连接到 VPC) +curl http://:5000/health + +# 如果返回 {"status": "healthy"},说明内网连通正常 +``` + +### 步骤 4:初始化 Dify(首次部署) + +1. **访问 Dify Web UI**(如果启用): + ``` + http:// + ``` + +2. **创建管理员账号**: + - 邮箱:admin@your-domain.com + - 密码: + +3. **配置 LLM 提供商**: + - 进入 "设置" → "模型提供商" + - 添加 DeepSeek/Qwen 等 API Key + +### 步骤 5:配置后端环境变量 + +在 SAE 后端服务的环境变量中添加: + +```bash +# Dify API 配置 +DIFY_API_BASE=http://:5000 +DIFY_API_KEY=<从 Dify Web UI 中获取> + +# 获取 API Key 的方法: +# Dify Web UI → 设置 → API Keys → 创建新密钥 +``` + +--- + +## 配置说明 + +### 🔧 存储配置详解 + +#### 选项 1:阿里云 OSS(推荐) + +**优势**: +- 无限扩展 +- 高可用(99.9%) +- 与 SAE 内网通信,速度快 + +**配置**: +```bash +STORAGE_TYPE=aliyun-oss +ALIYUN_OSS_BUCKET_NAME=your-bucket-name +ALIYUN_OSS_REGION=oss-cn-hangzhou +ALIYUN_OSS_ACCESS_KEY_ID= +ALIYUN_OSS_ACCESS_KEY_SECRET= +``` + +**注意**: +- OSS Bucket 必须和 ECS 在同一区域 +- 建议使用内网 Endpoint(如:oss-cn-hangzhou-internal.aliyuncs.com) +- 设置合理的生命周期规则(如:30天后转冷存储) + +#### 选项 2:本地存储(不推荐) + +**缺点**: +- 磁盘空间有限 +- 无法横向扩展 +- 数据备份麻烦 + +**仅适用于**:测试环境或小规模部署 + +### 🔧 数据库配置详解 + +#### Dify 数据库与后端数据库分离 + +``` +RDS PostgreSQL 实例 +├─ 数据库 1: clinical_research (后端主数据库) +│ ├─ Schema: platform_schema +│ ├─ Schema: asl_schema +│ ├─ Schema: dc_schema +│ └─ ... +└─ 数据库 2: dify_prod (Dify 数据库) + ├─ Schema: public + └─ ... +``` + +**优势**: +- 数据隔离(安全) +- 独立备份策略 +- 避免资源竞争 + +**连接池配置**: +```bash +# Dify 的连接池较小(因为异步任务多) +DB_MAX_CONNECTIONS=20 +``` + +### 🔧 Redis 配置说明 + +**为什么 Dify 必须用 Redis?** +- Dify 的任务队列依赖 Redis(Celery) +- 短期缓存(如:API 限流) +- 不能用 PostgreSQL 替代(与您的后端 Postgres-Only 架构不同) + +**关键配置(避免连接失败)**: +```bash +# ⚠️ 致命错误警告: +# 在 .env 中,REDIS_HOST 必须设置为服务名 "redis" +# 不是 "localhost"! + +# ❌ 错误写法(会导致 API 无法启动) +REDIS_HOST=localhost + +# ✅ 正确写法(Docker Compose 网络中使用服务名) +REDIS_HOST=redis + +# 原理: +# - 在 Docker Compose 中,容器之间通过服务名通信 +# - localhost 指的是容器自己,不是 Redis 容器 +# - redis 会被 Docker 网络解析为 Redis 容器的内网 IP +``` + +**安全加固**: +```bash +# 1. 设置强密码 +REDIS_PASSWORD=<至少16位随机字符串> + +# 2. 仅监听内网(docker-compose 中已配置) +# Redis 容器不暴露端口到主机,仅在 Docker 网络内可访问 + +# 3. 禁用危险命令(可选) +# 在 docker-compose.yaml 中修改 Redis 启动命令 +command: > + redis-server + --requirepass ${REDIS_PASSWORD} + --rename-command CONFIG "" + --rename-command FLUSHALL "" + --rename-command FLUSHDB "" +``` + +--- + +## 监控与维护 + +### 📊 日志管理 + +#### 1. 查看实时日志 + +```bash +cd /data/dify/docker + +# 查看所有服务日志 +docker compose logs -f + +# 查看特定服务日志 +docker compose logs -f api +docker compose logs -f worker +``` + +#### 2. 日志文件位置 + +``` +/data/dify/logs/ +├─ api.log # API 服务日志 +├─ worker.log # Worker 日志 +└─ error.log # 错误日志 +``` + +#### 3. 日志轮转配置 + +```bash +# 安装 logrotate(Ubuntu 默认已安装) +apt install logrotate + +# 创建配置文件 +cat > /etc/logrotate.d/dify < -U postgres -d dify_prod -c " + SELECT count(*) + FROM pg_stat_activity + WHERE datname = 'dify_prod'; +" +``` + +#### 3. Redis 监控 + +```bash +# 连接到 Redis 容器 +docker exec -it dify-redis redis-cli -a + +# 查看信息 +INFO +INFO memory +INFO stats +``` + +### 🔄 定期维护任务 + +#### 每周任务 + +```bash +# 1. 清理 Docker 日志 +truncate -s 0 $(docker inspect --format='{{.LogPath}}' dify-api) +truncate -s 0 $(docker inspect --format='{{.LogPath}}' dify-worker) + +# 2. 清理无用的 Docker 资源 +docker system prune -a --volumes -f +``` + +#### 每月任务 + +```bash +# 1. 数据库 VACUUM(在低峰期执行) +psql -h -U dify_user -d dify_prod -c "VACUUM ANALYZE;" + +# 2. 检查磁盘碎片 +e2fsck -f /dev/vdb + +# 3. 备份配置文件 +tar -czf /backup/dify-config-$(date +%Y%m%d).tar.gz /data/dify/docker/.env +``` + +--- + +## 故障排查 + +### 🔥 常见问题 + +#### 问题 1:API 容器无法启动 - Redis 连接失败(高频错误) + +**症状**: +```bash +docker compose ps +# 显示 dify-api 状态为 Exit 1 或不断重启 + +docker compose logs api +# 显示错误: +# redis.exceptions.ConnectionError: Error connecting to localhost:6379 +# 或 +# Cannot connect to Redis at localhost:6379 +``` + +**根本原因**: +```bash +# ❌ .env 文件中配置错误 +REDIS_HOST=localhost # 这是错的! +``` + +**解决方法**: +```bash +# 1. 编辑 .env 文件 +nano /data/dify/docker/.env + +# 2. 修改 REDIS_HOST +REDIS_HOST=redis # ✅ 改为服务名 + +# 3. 重启服务 +docker compose down +docker compose up -d + +# 4. 验证 +docker compose logs api | grep -i redis +# 应该看到:Successfully connected to Redis +``` + +#### 问题 2:前端访问 API 报 CORS 错误 + +**症状**: +``` +浏览器控制台报错: +Access to XMLHttpRequest at 'http://xxx/v1/xxx' from origin 'http://yyy' +has been blocked by CORS policy +``` + +**根本原因**: +- 缺少 Nginx 网关服务 +- 或者 Nginx 配置错误 + +**解决方法**: +```bash +# 1. 确认 Nginx 容器是否运行 +docker compose ps | grep nginx + +# 2. 如果没有 Nginx,说明你删除了官方配置 +# 解决方案:重新克隆官方仓库,使用完整的 docker-compose.yaml + +cd /data/dify +rm -rf dify +git clone https://github.com/langgenius/dify.git +cd dify/docker +cp .env.example .env +# 重新配置 .env(参考前文) +docker compose up -d +``` + +#### 问题 3:容器无法启动 - 数据库连接失败 + +**症状**: +```bash +docker compose logs api +# 显示错误: +# could not connect to server: Connection refused +``` + +**排查步骤**: +```bash +# 1. 检查 RDS 白名单 +# 阿里云控制台 → RDS → 数据安全性 → 白名单设置 +# 确认添加了 ECS 的内网 IP + +# 2. 检查 .env 配置 +DB_HOST= # 不是公网地址! +DB_PORT=5432 +DB_NAME=dify_prod +DB_USERNAME=dify_user +DB_PASSWORD=<正确的密码> + +# 3. 测试连接(在 ECS 上) +telnet 5432 +# 如果连接失败,检查安全组和白名单 +``` + +#### 问题 4:服务运行一段时间后崩溃(OOM 内存溢出) + +**症状**: +```bash +dmesg | grep -i kill +# 显示:Out of memory: Killed process xxx (dify-api) + +docker compose ps +# 容器不断重启 +``` + +**根本原因**: +- 2核4G 内存不足 +- 没有配置 Swap 分区 + +**解决方法**: +```bash +# 1. 立即配置 Swap(如果之前没配置) +fallocate -l 4G /swapfile +chmod 600 /swapfile +mkswap /swapfile +swapon /swapfile +echo '/swapfile none swap sw 0 0' >> /etc/fstab + +# 2. 验证 Swap 已启用 +free -h + +# 3. 重启服务 +docker compose restart + +# 4. 长期方案:升级 ECS 到 4核8G +``` + +#### 问题 2:API 返回 500 错误 + +**排查步骤**: +```bash +# 1. 查看 API 日志 +docker compose logs -f api | grep ERROR + +# 2. 检查数据库连接 +docker exec -it dify-api sh +# 在容器内执行 +python -c " +import psycopg2 +conn = psycopg2.connect( + host='', + database='dify_prod', + user='dify_user', + password='' +) +print('Connection OK') +" +``` + +#### 问题 3:向量搜索失败 + +**排查步骤**: +```bash +# 1. 检查 Weaviate 状态 +curl http://localhost:8080/v1/meta + +# 2. 查看 Weaviate 日志 +docker compose logs weaviate + +# 3. 重建索引(最后手段) +# 在 Dify Web UI 中:设置 → 数据集 → 重新索引 +``` + +#### 问题 4:内网连接失败(SAE 无法访问 ECS) + +**排查步骤**: +```bash +# 1. 检查安全组规则 +# 阿里云控制台 → ECS → 安全组 → 确认开放 5000 端口给 SAE 所在网段 + +# 2. 测试连通性(在 SAE 容器内执行) +telnet 5000 + +# 3. 检查防火墙(在 ECS 上执行) +ufw status +# 如果启用了防火墙,添加规则: +ufw allow from to any port 5000 +``` + +### 🔄 重启服务 + +```bash +# 重启所有服务 +cd /data/dify/docker +docker compose restart + +# 重启单个服务 +docker compose restart api + +# 完全重建(如果修改了 docker-compose.yaml) +docker compose down +docker compose up -d +``` + +--- + +## 注意事项与禁忌 + +### ✅ 最佳实践 + +1. **定期备份** + ```bash + # 每天自动备份(添加到 crontab) + 0 2 * * * pg_dump -h -U dify_user dify_prod | gzip > /backup/dify_$(date +\%Y\%m\%d).sql.gz + ``` + +2. **监控磁盘空间** + ```bash + # 设置告警(当 /data 使用率 > 80% 时通知) + # 可使用阿里云云监控服务 + ``` + +3. **使用内网域名** + ```bash + # 为 ECS 设置内网域名(便于维护) + # 在 SAE 中配置 DIFY_API_BASE=http://dify.internal:5000 + # 而不是写死 IP + ``` + +4. **版本锁定** + ```bash + # docker-compose.yaml 中使用具体版本号,不要用 latest + image: langgenius/dify-api:0.11.2 # ✅ 推荐 + # image: langgenius/dify-api:latest # ❌ 禁止(可能导致不兼容) + ``` + +### ❌ 绝对禁止 + +1. **禁止在 .env 中使用 REDIS_HOST=localhost** + ```bash + # ❌ 致命错误(会导致服务启动失败) + REDIS_HOST=localhost + + # ✅ 正确配置(在 Docker Compose 中使用服务名) + REDIS_HOST=redis + ``` + +2. **禁止删减官方 docker-compose.yaml 的核心服务** + ```bash + # ❌ 危险操作(会导致功能异常) + # - 删除 nginx → 前端 CORS 错误 + # - 删除 worker → 异步任务失败 + # - 删除 sandbox → 代码解释器功能失效 + + # ✅ 正确做法 + # 使用官方完整配置,仅修改 .env 环境变量 + ``` + +3. **禁止在 2核4G 的 ECS 上不配置 Swap** + ```bash + # ❌ 危险(极易 OOM 崩溃) + # 没有 Swap,内存用尽后服务直接被 Kill + + # ✅ 必须配置 Swap + fallocate -l 4G /swapfile + chmod 600 /swapfile + mkswap /swapfile + swapon /swapfile + ``` + +4. **禁止在生产环境使用 DEBUG=true** + ```bash + # 会暴露敏感信息(如:API Key、数据库密码) + DEBUG=false # ✅ + ``` + +5. **禁止使用弱密码** + ```bash + # 必须使用强密码(至少16位,包含大小写、数字、特殊字符) + SECRET_KEY=$(openssl rand -base64 32) + REDIS_PASSWORD=$(openssl rand -base64 24) + WEAVIATE_API_KEY=$(openssl rand -base64 24) + ``` + +6. **禁止暴露 Redis 和 Weaviate 端口到公网** + ```bash + # 官方配置默认不暴露这些端口,不要自己添加 + # ❌ 危险配置 + services: + redis: + ports: + - "6379:6379" # 会被攻击! + + # ✅ 正确做法:仅在 Docker 网络内访问 + ``` + +7. **禁止直接修改数据库** + ```bash + # 不要手动修改 Dify 数据库的表结构或数据 + # 必须通过 Dify API 或 Web UI 操作 + ``` + +8. **禁止在 ECS 上运行其他无关服务** + ```bash + # 这台 ECS 专门用于 Dify,不要部署其他应用 + # 避免资源竞争和安全风险 + ``` + +### ⚠️ 重要提醒 + +1. **数据主权** + - 所有向量数据和文档都存储在您的 ECS 和 OSS 上 + - 定期检查数据备份的完整性 + +2. **成本控制** + - ECS 按量付费 vs 包年包月(根据实际情况选择) + - 监控 OSS 存储量和流量(避免超出预算) + +3. **升级策略** + ```bash + # 升级 Dify 版本前的步骤: + # 1. 备份数据库 + # 2. 在测试环境验证 + # 3. 选择业务低峰期升级 + # 4. 做好回滚准备 + ``` + +4. **私有化部署路径** + ``` + 当前架构 → 直接复制到客户环境 + ├─ 导出 docker-compose.yaml + ├─ 导出 .env(敏感信息替换) + ├─ 打包为安装脚本(一键部署) + └─ 提供《私有化部署手册》 + ``` + +--- + +## 📚 附录 + +### A. 生成强密码的方法 + +```bash +# 方法 1: 使用 openssl +openssl rand -base64 32 + +# 方法 2: 使用 pwgen +pwgen -s 32 1 + +# 方法 3: 使用 Python +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +### B. Dify API 常用端点 + +```bash +# 健康检查 +GET http://:5000/health + +# 创建数据集 +POST http://:5000/v1/datasets +Headers: Authorization: Bearer +Body: {"name": "test-dataset"} + +# 上传文档 +POST http://:5000/v1/datasets/{dataset_id}/documents +Headers: Authorization: Bearer +Body: (multipart/form-data) + +# 语义检索 +POST http://:5000/v1/datasets/{dataset_id}/retrieve +Headers: Authorization: Bearer +Body: {"query": "xxx", "top_k": 5} +``` + +### C. 相关文档链接 + +- [Dify 官方文档](https://docs.dify.ai/) +- [Docker Compose 文档](https://docs.docker.com/compose/) +- [Weaviate 文档](https://weaviate.io/developers/weaviate) +- [阿里云 ECS 文档](https://help.aliyun.com/product/25365.html) +- [阿里云 OSS 文档](https://help.aliyun.com/product/31815.html) + +--- + +**文档维护**: +- 如有问题或建议,请联系技术负责人 +- 最后更新:2025-12-13 +- 下次审查:2025-03-13 + diff --git a/docs/05-部署文档/04-Python微服务-SAE容器部署指南.md b/docs/05-部署文档/04-Python微服务-SAE容器部署指南.md new file mode 100644 index 00000000..d3cd067f --- /dev/null +++ b/docs/05-部署文档/04-Python微服务-SAE容器部署指南.md @@ -0,0 +1,1533 @@ +# Python 微服务 SAE 容器部署完全指南 + +**文档版本**: v1.1 (修复内网地址和临时文件问题) +**创建时间**: 2025-12-13 +**最后修订**: 2025-12-13 +**适用范围**: AIclinicalresearch 平台 - Python 微服务(extraction_service) +**目标读者**: 运维工程师、后端开发工程师 + +**v1.1 更新日志**: +- ✅ 修复:内网地址使用 SAE 控制台显示的真实 IP(不猜测域名) +- ✅ 优化:Dockerfile 系统依赖说明(libmupdf-dev 可选) +- ✅ 新增:确保 /tmp 目录可写(大文件临时存储) +- ✅ 完善:功能验证流程和监控指南 + +--- + +## 📋 文档目录 + +1. [为什么选择 SAE 容器部署](#为什么选择-sae-容器部署) +2. [部署架构图](#部署架构图) +3. [前置准备清单](#前置准备清单) +4. [Python 服务分析](#python-服务分析) +5. [依赖优化策略](#依赖优化策略) +6. [构建 Docker 镜像](#构建-docker-镜像) +7. [部署到 SAE](#部署到-sae) +8. [测试与验证](#测试与验证) +9. [监控与维护](#监控与维护) +10. [故障排查](#故障排查) +11. [注意事项与禁忌](#注意事项与禁忌) + +--- + +## 为什么选择 SAE 容器部署 + +### ✅ SAE 容器部署 vs. SAE Python 运行时 + +| 对比维度 | SAE Python 运行时 | SAE 容器部署 (推荐) | +|---------|-----------------|------------------| +| **系统依赖** | ❌ 无法安装系统库 | ✅ 完全可控 | +| **复杂依赖** | ❌ PyMuPDF/OpenCV 报错 | ✅ 完美支持 | +| **环境一致性** | ⚠️ 云上和本地可能不同 | ✅ 本地跑通 = 云上跑通 | +| **Nougat (Torch)** | ❌ 版本冲突风险高 | ✅ 轻松支持 | +| **部署方式** | 上传 ZIP 包 | 推送 Docker 镜像 | +| **启动速度** | 快(< 5秒) | 较快(10-20秒) | +| **运维复杂度** | 低 | 中 | +| **推荐度** | ❌ 不推荐 | ✅ 强烈推荐 | + +### 🎯 核心原因 + +#### 1. **系统级依赖缺失(致命问题)** + +```python +# 您的代码使用了这些库: +import fitz # PyMuPDF → 依赖 libmupdf.so, libfreetype.so +import cv2 # OpenCV → 依赖 libGL.so.1, libgthread-2.0.so +import polars # Polars → 依赖 libgomp.so +``` + +**SAE Python 运行时**: +```bash +❌ 只提供标准 Python 环境 +❌ 无法执行 apt-get install +❌ 运行时报错:ImportError: libGL.so.1: cannot open shared object file +``` + +**SAE 容器部署**: +```dockerfile +✅ Dockerfile 中自由安装: +RUN apt-get update && apt-get install -y \ + libgl1-mesa-glx \ + libglib2.0-0 \ + libgomp1 +``` + +#### 2. **环境完全可控** + +``` +本地开发环境 = Docker 镜像 = SAE 生产环境 +``` + +- 您在本地 Docker 中跑通了,推到 SAE 就一定能跑通 +- 没有"本地好用、云上报错"的问题 + +#### 3. **扩展性强** + +``` +未来需求: + ├─ 添加 Nougat OCR (需要 PyTorch + GPU 支持) + ├─ 添加图像预处理 (需要 OpenCV) + ├─ 添加更多文档格式 (需要更多系统库) + └─ 容器部署都能轻松支持 +``` + +#### 4. **运维统一** + +``` +您的整体架构: + ├─ 前端 Nginx → SAE 容器 + ├─ 后端 Node.js → SAE 容器 + └─ Python 服务 → SAE 容器 ✅ (统一管理) +``` + +--- + +## 部署架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 阿里云架构 │ +│ │ +│ ┌─────────────┐ ┌─────────────────────────────┐ │ +│ │ SAE (后端) │ ←内网→ │ SAE (Python 微服务) │ │ +│ │ │ │ │ │ +│ │ Node.js │ │ ┌─────────────────────┐ │ │ +│ │ Backend │ │ │ Docker 容器: │ │ │ +│ │ │ │ │ - FastAPI │ │ │ +│ │ │ │ │ - PyMuPDF │ │ │ +│ │ │ │ │ - Polars │ │ │ +│ └─────────────┘ │ │ - Mammoth │ │ │ +│ │ │ └─────────────────────┘ │ │ +│ │ └─────────────────────────────┘ │ +│ │ │ +│ ├──────────────→ RDS PostgreSQL 15 │ +│ └──────────────→ OSS (文档存储) │ +└─────────────────────────────────────────────────────────────┘ +``` + +**关键点**: +- Python 微服务和 Node.js 后端都部署在 SAE 上(同一 VPC) +- 通过内网通信(延时 < 5ms) +- 共享 RDS 和 OSS 资源 + +--- + +## 前置准备清单 + +### ✅ 必需资源 + +| 资源类型 | 配置建议 | 预估费用 | 用途 | +|---------|---------|---------|-----| +| **SAE 应用** | 1核2G / 1实例 | ~100元/月 | 运行 Python 服务 | +| **容器镜像仓库** | 阿里云 ACR 个人版 | 免费(5GB) | 存储 Docker 镜像 | +| **OSS 存储** | 已有(共用) | 0元(增量) | 文档存储 | +| **RDS PostgreSQL** | 已有(共用) | 0元 | 数据库 | + +### ✅ 软件准备 + +```bash +# 本地开发机器需要安装 +- Docker Desktop +- 阿里云 CLI(可选) + +# 不需要在 SAE 上安装任何东西(容器已包含) +``` + +### ✅ 账号与权限 + +- 阿里云账号(已有) +- 容器镜像仓库访问权限 +- SAE 应用创建权限 + +--- + +## Python 服务分析 + +### 📂 当前服务概览 + +#### 服务 1: extraction_service(文档提取) + +**位置**: `AIclinicalresearch/extraction_service/` + +**用途**: +- PKB 模块: 上传文档到 Dify 前,先提取文本 +- ASL 模块: 提取 PDF 全文用于深度阅读 +- DC 模块: 提取 Excel/CSV 数据 + +**核心文件**: +``` +extraction_service/ +├── main.py # FastAPI 入口 +├── requirements.txt # 依赖列表 +├── services/ +│ ├── pdf_extractor.py # PDF 提取(调度器) +│ ├── pymupdf_extractor.py # PyMuPDF 实现 +│ ├── nougat_extractor.py # Nougat OCR 实现 +│ ├── docx_extractor.py # Word 提取 +│ └── txt_extractor.py # 纯文本提取 +└── operations/ + └── fillna_operations.py # 数据清洗(Polars) +``` + +**关键端点**: +```python +POST /extract/pdf # PDF 提取 +POST /extract/docx # Word 提取 +POST /extract/txt # 文本提取 +POST /operations/fillna # 数据清洗 +``` + +### 📊 依赖分析 + +#### 当前 `requirements.txt` 内容: + +```txt +fastapi==0.115.5 +uvicorn[standard]==0.32.1 +python-multipart==0.0.20 +PyMuPDF==1.24.14 +pdfplumber==0.11.4 +nougat-ocr==0.1.17 +torch==2.1.0 +torchvision==0.16.0 +mammoth==1.8.0 +python-docx==1.1.2 +langdetect==1.0.9 +chardet==5.2.0 +polars==1.17.1 +numpy==1.26.4 +``` + +#### 依赖大小预估: + +| 包名 | 大小 | 用途 | 是否必需 | +|-----|------|-----|---------| +| **PyMuPDF** | ~50MB | PDF 提取(核心) | ✅ 必需 | +| **pdfplumber** | ~10MB | PDF 表格提取 | ⚠️ 可选(暂未使用) | +| **nougat-ocr** | ~300MB | 学术论文 OCR | ⚠️ 阶段性(见下文) | +| **torch** | ~800MB | Nougat 依赖 | ⚠️ 阶段性 | +| **torchvision** | ~100MB | Nougat 依赖 | ⚠️ 阶段性 | +| **mammoth** | ~5MB | Word 提取 | ✅ 必需 | +| **python-docx** | ~3MB | Word 提取 | ✅ 必需 | +| **polars** | ~50MB | 数据清洗 | ✅ 必需 | +| **numpy** | ~20MB | 数值计算 | ✅ 必需 | +| **fastapi** | ~10MB | Web 框架 | ✅ 必需 | +| **uvicorn** | ~5MB | ASGI 服务器 | ✅ 必需 | +| **其他** | ~10MB | 辅助库 | ✅ 必需 | +| **总计(含 Nougat)** | **~1.4GB** | - | - | +| **总计(不含 Nougat)** | **~163MB** | - | - | + +--- + +## 依赖优化策略 + +### 🎯 阶段 1:最小化部署(推荐用于首次部署) + +**目标**: 快速上线,验证核心功能 + +**策略**: +- ✅ 保留 PyMuPDF(核心 PDF 提取) +- ✅ 保留 Mammoth/python-docx(Word 提取) +- ✅ 保留 Polars(数据清洗) +- ❌ 暂时移除 Nougat(体积大,使用频率低) + +**优化后的 `requirements.txt`**: + +```txt +# Web 框架 +fastapi==0.115.5 +uvicorn[standard]==0.32.1 +python-multipart==0.0.20 + +# 文档提取(核心) +PyMuPDF==1.24.14 +mammoth==1.8.0 +python-docx==1.1.2 + +# 数据处理 +polars==1.17.1 +numpy==1.26.4 + +# 辅助工具 +langdetect==1.0.9 +chardet==5.2.0 + +# 日志和监控 +python-json-logger==2.0.7 +``` + +**镜像大小预估**: ~500MB(含 Python 基础镜像) + +**代码修改**: + +```python +# services/pdf_extractor.py + +# 注释掉 Nougat 相关代码 +# from .nougat_extractor import extract_pdf_nougat, check_nougat_available + +async def extract_pdf(pdf_path: str, filename: str): + """PDF 提取(阶段1:仅 PyMuPDF)""" + + # 检测语言和文档类型 + language = detect_language(pdf_path) + is_academic = detect_academic_paper(pdf_path) + + # 阶段1:直接使用 PyMuPDF + text = extract_pdf_pymupdf(pdf_path) + + # 阶段2:可以加回 Nougat 降级逻辑 + # if language == 'english' and is_academic: + # try: + # if check_nougat_available(): + # text = extract_pdf_nougat(pdf_path) + # except: + # text = extract_pdf_pymupdf(pdf_path) # 降级 + + return { + 'text': text, + 'method': 'pymupdf', + 'language': language, + 'is_academic': is_academic + } +``` + +### 🎯 阶段 2:完整部署(未来需要时) + +**时机**: +- 当用户反馈英文学术论文提取质量不佳时 +- 有足够的 GPU 资源时 + +**策略**: +- ✅ 加回 Nougat + Torch +- ✅ 使用 GPU 实例(SAE 目前不支持 GPU,需迁移到 ECS) + +**完整的 `requirements.txt`**: + +```txt +# 恢复全部依赖(包括 Nougat) +fastapi==0.115.5 +uvicorn[standard]==0.32.1 +python-multipart==0.0.20 +PyMuPDF==1.24.14 +pdfplumber==0.11.4 +nougat-ocr==0.1.17 +torch==2.1.0 +torchvision==0.16.0 +mammoth==1.8.0 +python-docx==1.1.2 +langdetect==1.0.9 +chardet==5.2.0 +polars==1.17.1 +numpy==1.26.4 +``` + +**镜像大小预估**: ~2GB + +--- + +## 构建 Docker 镜像 + +### 步骤 1:创建优化的 Dockerfile + +在 `extraction_service/` 目录下创建 `Dockerfile`: + +```dockerfile +# ======================================== +# 多阶段构建:减小镜像体积 +# ======================================== + +# 阶段 1: 构建阶段(安装依赖) +FROM python:3.11-slim as builder + +# 设置工作目录 +WORKDIR /app + +# 安装系统依赖(构建时需要) +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + make \ + libffi-dev \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY requirements.txt . + +# 安装 Python 依赖到虚拟环境 +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# ======================================== +# 阶段 2: 运行阶段(最小化镜像) +# ======================================== +FROM python:3.11-slim + +# 设置工作目录 +WORKDIR /app + +# 安装运行时依赖(系统级库 + 时区数据) +RUN apt-get update && apt-get install -y --no-install-recommends \ + # PyMuPDF 依赖 + # 注:libmupdf-dev 通常用于编译,pip 安装的 PyMuPDF wheel 包已自带动态库 + # 保留它作为保险,如需瘦身可尝试移除后验证 + libmupdf-dev \ + libfreetype6 \ + libjpeg62-turbo \ + libopenjp2-7 \ + # Polars 依赖 + libgomp1 \ + # 其他工具 + curl \ + # 时区数据 + tzdata \ + && rm -rf /var/lib/apt/lists/* + +# ⚠️ 统一时区:Asia/Shanghai +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# 确保临时目录可写(大文件上传时需要) +RUN mkdir -p /tmp && chmod 1777 /tmp + +# 从构建阶段复制虚拟环境 +COPY --from=builder /opt/venv /opt/venv + +# 复制应用代码 +COPY . . + +# 设置环境变量 +ENV PATH="/opt/venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PORT=8000 + +# 暴露端口 +EXPOSE 8000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# 启动命令 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"] +``` + +### 步骤 2:创建 .dockerignore + +``` +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# 测试和文档 +tests/ +test_files/ +*.md +README.md + +# Git +.git/ +.gitignore + +# 日志 +*.log + +# 临时文件 +tmp/ +temp/ +``` + +### 步骤 3:本地构建镜像 + +```bash +# 进入 extraction_service 目录 +cd d:\MyCursor\AIclinicalresearch\extraction_service + +# 构建镜像(本地测试) +docker build -t extraction-service:latest . + +# 查看镜像大小 +docker images extraction-service +``` + +### 步骤 4:本地测试镜像 + +```bash +# 启动容器(本地测试) +docker run -d \ + --name extraction-test \ + -p 8000:8000 \ + -e DATABASE_URL="postgresql://user:pass@host:5432/dbname" \ + extraction-service:latest + +# 查看日志 +docker logs -f extraction-test + +# 测试健康检查 +curl http://localhost:8000/health + +# 测试 PDF 提取 +curl -X POST \ + -F "file=@test.pdf" \ + http://localhost:8000/extract/pdf + +# 停止并删除测试容器 +docker stop extraction-test +docker rm extraction-test +``` + +### 步骤 5:推送到阿里云容器镜像仓库 + +#### 5.1 创建镜像仓库(首次部署) + +1. **登录阿里云控制台** → **容器镜像服务 ACR** + +2. **创建个人实例**(免费版): + ``` + 实例名称: extraction-service + 地域: 华东1(杭州) + ``` + +3. **创建命名空间**: + ``` + 命名空间: clinical-research + ``` + +4. **创建镜像仓库**: + ``` + 仓库名称: extraction-service + 代码源: 本地仓库 + ``` + +#### 5.2 推送镜像 + +```bash +# 1. 登录阿里云容器镜像服务 +# 获取登录命令:阿里云控制台 → 容器镜像服务 → 访问凭证 → 设置Registry登录密码 +docker login --username= registry.cn-hangzhou.aliyuncs.com + +# 2. 给镜像打标签 +docker tag extraction-service:latest \ + registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:v1.0 + +# 3. 推送到阿里云 +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:v1.0 + +# 4. 推送 latest 标签(便于后续更新) +docker tag extraction-service:latest \ + registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:latest +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:latest +``` + +--- + +## 部署到 SAE + +### 步骤 1:创建 SAE 应用 + +1. **登录阿里云控制台** → **Serverless 应用引擎 SAE** + +2. **创建应用**: + ``` + 应用名称: extraction-service + 命名空间: 选择后端所在的命名空间(同 VPC) + 部署方式: 镜像 + ``` + +3. **镜像配置**: + ``` + 镜像地址: registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:latest + 镜像版本: latest + 镜像拉取策略: Always(每次部署都拉取最新镜像) + ``` + +4. **规格配置**: + ``` + CPU: 1核 + 内存: 2GB + 实例数: 1(初始) + 弹性扩缩容: + - 最小实例数: 1 + - 最大实例数: 3 + - CPU 触发阈值: 70% + ``` + +5. **网络配置**: + ``` + 专有网络 VPC: 选择后端所在的 VPC + vSwitch: 选择后端所在的交换机 + 安全组: 允许 VPC 内访问 + ``` + +### 步骤 2:配置环境变量 + +在 SAE 应用配置中添加以下环境变量: + +```bash +# ========= 数据库配置 ========= +DATABASE_URL=postgresql://user:password@rm-xxxx.pg.rds.aliyuncs.com:5432/clinical_research + +# ========= 存储配置 ========= +OSS_ENDPOINT=oss-cn-hangzhou-internal.aliyuncs.com +OSS_BUCKET=your-bucket-name +OSS_ACCESS_KEY_ID= +OSS_ACCESS_KEY_SECRET= + +# ========= 服务配置 ========= +SERVICE_NAME=extraction-service +SERVICE_VERSION=v1.0 +LOG_LEVEL=INFO + +# ========= 性能配置 ========= +WORKERS=2 +TIMEOUT=300 +MAX_FILE_SIZE=52428800 + +# ========= 时区 ========= +TZ=Asia/Shanghai +``` + +### 步骤 3:配置健康检查 + +```bash +健康检查路径: /health +健康检查端口: 8000 +健康检查协议: HTTP +初始延迟: 30秒 +检查间隔: 10秒 +超时时间: 5秒 +健康阈值: 2次 +不健康阈值: 3次 +``` + +### 步骤 4:配置日志 + +```bash +日志目录: /app/logs +日志文件: extraction-service.log +日志级别: INFO +日志保留天数: 7天 +``` + +### 步骤 5:配置 SLB(可选,如果需要公网访问) + +```bash +# 通常 Python 微服务只需要内网访问(被后端调用) +# 如果需要公网访问(如:调试、第三方集成): + +负载均衡类型: 公网 +监听端口: 80 +后端端口: 8000 +健康检查: 启用 +``` + +### 步骤 6:部署应用 + +1. **点击"部署应用"** + +2. **等待部署完成**(约 2-3 分钟) + +3. **查看部署日志**: + ``` + [INFO] Pulling image... + [INFO] Image pulled successfully + [INFO] Starting container... + [INFO] Container started successfully + [INFO] Health check passed + [INFO] Application is running + ``` + +--- + +## 测试与验证 + +### 步骤 1:获取内网地址(关键步骤) + +**⚠️ 重要:SAE 实例间是跨主机的,必须使用 SAE 提供的内网地址** + +#### 获取真实内网地址的正确方法: + +1. **登录 SAE 控制台** → **应用列表** → **点击 extraction-service 应用** + +2. **在应用详情页,找到"应用访问配置"或"VPC 内网访问"部分** + +3. **查看并复制"内网访问地址"**,通常是以下格式之一: + ``` + # 格式 1: 内网 IP + 端口(⭐⭐⭐⭐⭐ 强烈推荐,最稳定) + 172.16.0.10:8000 + + # 格式 2: SAE 内网 Service 域名(需要额外配置服务发现,不推荐) + extraction-service-xxxxx.cn-hangzhou.sae.aliyuncs.com:8000 + + # 格式 3: K8s Service 域名(需要配置K8s服务发现,复杂,不推荐) + extraction-service.namespace.svc.cluster.local:8000 + ``` + +4. **❌ 错误做法(会导致连接失败)**: + ```bash + # ❌ 不要猜测或假设域名(100%失败) + http://extraction-service.sae:8000 # .sae 域名不存在 + http://extraction-service.internal:8000 # .internal 域名不存在 + http://extraction-service.cluster.local:8000 # 需要K8s服务发现配置 + + # ❌ 不要使用 localhost + http://localhost:8000 # SAE 实例间是跨主机的 + + # ❌ 不要使用 Docker 服务名 + http://extraction-service:8000 # 这不是单机 Docker Compose + ``` + +5. **✅ 推荐做法(按优先级排序)**: + ```bash + # ⭐⭐⭐⭐⭐ 方案A:直接使用内网IP(强烈推荐) + EXTRACTION_SERVICE_URL=http://172.16.0.10:8000 + # 获取方式:SAE控制台 > Python应用 > 实例列表 > 查看内网IP + + # ⭐⭐⭐ 方案B:使用SAE服务发现(需要额外配置,不推荐初期使用) + # 需要在SAE控制台配置"微服务注册中心" + EXTRACTION_SERVICE_URL=http://extraction-service-xxxxx.cn-hangzhou.sae.aliyuncs.com:8000 + ``` + +### 步骤 2:配置后端环境变量 + +在 SAE 后端应用的环境变量中添加: + +```bash +# ⚠️ 使用 SAE 控制台显示的真实内网地址 +EXTRACTION_SERVICE_URL=http://172.16.0.10:8000 + +# 注意: +# 1. 不要使用猜测的域名 +# 2. 必须从 SAE 控制台的"应用访问配置"中获取 +# 3. 如果 IP 变化(如重新部署),需要同步更新这个环境变量 +``` + +**配置后重启后端应用**: +- SAE 控制台 → 后端应用 → 重启 + +### 步骤 3:从后端服务测试 + +在您的 Node.js 后端服务中添加测试端点: + +```typescript +// backend/src/tests/test-extraction-service.ts + +import axios from 'axios'; +import FormData from 'form-data'; +import fs from 'fs'; + +const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://extraction-service.internal:8000'; + +export async function testExtractionService() { + try { + // 1. 健康检查 + console.log('Testing health endpoint...'); + const healthRes = await axios.get(`${EXTRACTION_SERVICE_URL}/health`); + console.log('Health check:', healthRes.data); + + // 2. 测试 PDF 提取 + console.log('Testing PDF extraction...'); + const form = new FormData(); + form.append('file', fs.createReadStream('./test.pdf')); + + const pdfRes = await axios.post( + `${EXTRACTION_SERVICE_URL}/extract/pdf`, + form, + { headers: form.getHeaders() } + ); + console.log('PDF extraction result:', pdfRes.data); + + // 3. 测试 Word 提取 + console.log('Testing Word extraction...'); + const form2 = new FormData(); + form2.append('file', fs.createReadStream('./test.docx')); + + const docxRes = await axios.post( + `${EXTRACTION_SERVICE_URL}/extract/docx`, + form2, + { headers: form2.getHeaders() } + ); + console.log('Word extraction result:', docxRes.data); + + console.log('✅ All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + if (error.response) { + console.error('Response:', error.response.data); + } + } +} +``` + +### 步骤 4:验证端到端流程(完整业务场景) + +测试以下业务流程: + +#### 场景 1: PKB 文档上传 + +**业务流程**: +``` +用户上传 PDF + → Node.js 后端接收 + → HTTP POST 转发文件流到 Python 服务 (EXTRACTION_SERVICE_URL) + → Python 服务解析 PDF,返回 JSON 文本 + → Node.js 后端收到文本 + → 上传到 Dify + → 返回前端 +``` + +**测试步骤**: +1. 在前端上传一个 PDF 文档(建议 < 5MB 的简单文档) + +2. **查看 Node.js 后端日志**(SAE 控制台 → 后端应用 → 日志): + ``` + [INFO] Calling extraction service: http://172.16.0.10:8000/extract/pdf + [INFO] Extraction completed in 2.3s + [INFO] Extracted text preview: "This is a test document..." + ``` + +3. **查看 Python 服务日志**(SAE 控制台 → extraction-service 应用 → 日志): + ``` + INFO: Request: POST /extract/pdf + INFO: File size: 1.2MB, filename: test.pdf + INFO: Using PyMuPDF extraction + INFO: Response: 200 (took 2.10s) + ``` + +4. **在 Dify Web UI 中确认文档已上传** + +**如果失败,检查**: +- 后端日志是否显示 "Connection refused" → 检查 EXTRACTION_SERVICE_URL 配置 +- Python 日志是否显示 "ImportError" → 检查 Dockerfile 系统依赖 +- 提取超时(> 300s)→ 文件太大或需要增加超时配置 + +#### 场景 2: ASL 深度阅读 + +``` +用户点击"深度阅读" → 后端调用 Python 服务提取全文 → 返回 LLM 分析结果 +``` + +**测试步骤**: +1. 在 ASL 模块点击"深度阅读" +2. 查看后端日志(确认调用 Python 服务) +3. 查看 Python 服务日志(确认提取成功) +4. 前端显示分析结果 + +#### 场景 3: DC 数据清洗 + +``` +用户上传 Excel → 后端调用 Python 服务 fillna → 返回清洗后数据 +``` + +**测试步骤**: +1. 在 DC 模块上传 Excel 文件 +2. 执行 fillna 操作 +3. 查看 Python 服务日志 +4. 验证清洗结果 + +--- + +## 监控与维护 + +### 📊 SAE 自带监控 + +#### 1. 查看应用监控 + +``` +SAE 控制台 → 应用详情 → 监控 +``` + +**关键指标**: +- **CPU 使用率**(< 70%):PDF 提取是 CPU 密集型任务 +- **内存使用率**(< 80%):大文件处理时会占用较多内存 +- **请求 QPS**(每秒查询数):了解负载情况 +- **平均响应时间**(< 1000ms):小文件应 < 2s,大文件 < 30s +- **错误率**(< 1%):监控文件解析失败率 + +**性能基准(参考)**: +``` +小文件(< 1MB PDF):响应时间 1-3s +中等文件(1-10MB PDF):响应时间 5-15s +大文件(10-50MB PDF):响应时间 20-60s +超大文件(> 50MB):建议限制或拒绝 +``` + +#### 2. 实时日志查看 + +``` +SAE 控制台 → 应用详情 → 日志 → 实时日志 +``` + +**日志类型**: +- 应用日志(stdout/stderr):uvicorn 启动信息、请求日志 +- 访问日志(HTTP 请求):请求路径、响应时间、状态码 +- 错误日志(异常堆栈):Python 异常详情 + +**关键日志示例**: +```bash +# ✅ 正常启动 +INFO: Started server process [1] +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 + +# ✅ 正常请求 +INFO: Request: POST /extract/pdf +INFO: File: test.pdf (1.2MB) +INFO: Response: 200 (took 2.10s) + +# ❌ 错误日志(需关注) +ERROR: ImportError: libGL.so.1: cannot open shared object file +ERROR: Timeout: PDF extraction took > 300s +ERROR: Memory error: Cannot allocate memory +``` + +#### 3. 弹性伸缩配置 + +``` +SAE 控制台 → 应用详情 → 弹性伸缩 +``` + +**推荐配置**: +``` +最小实例数: 1(确保服务不中断) +最大实例数: 3(根据实际负载调整) + +触发条件: + - CPU 使用率 > 70% 持续 3 分钟 → 扩容 1 个实例 + - CPU 使用率 < 30% 持续 5 分钟 → 缩容 1 个实例 +``` + +**注意事项**: +- PDF 提取是 CPU 密集型,扩容主要看 CPU +- 如果经常扩容,考虑直接增加实例规格(2核 → 4核) +- SAE 会自动负载均衡,无需手动配置 + +### 📊 应用内监控 + +#### 添加健康检查端点 + +```python +# main.py + +from fastapi import FastAPI +import psutil +import os + +app = FastAPI() + +@app.get("/health") +async def health_check(): + """健康检查端点""" + return { + "status": "healthy", + "service": "extraction-service", + "version": os.getenv("SERVICE_VERSION", "unknown") + } + +@app.get("/metrics") +async def metrics(): + """性能指标端点""" + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/app') + + return { + "cpu": { + "percent": cpu_percent, + "count": psutil.cpu_count() + }, + "memory": { + "total": memory.total, + "available": memory.available, + "percent": memory.percent + }, + "disk": { + "total": disk.total, + "used": disk.used, + "free": disk.free, + "percent": disk.percent + } + } +``` + +#### 添加请求日志 + +```python +# main.py + +import logging +from fastapi import Request +import time + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/app/logs/extraction-service.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +@app.middleware("http") +async def log_requests(request: Request, call_next): + """请求日志中间件""" + start_time = time.time() + + # 记录请求 + logger.info(f"Request: {request.method} {request.url}") + + # 执行请求 + response = await call_next(request) + + # 记录响应 + process_time = time.time() - start_time + logger.info( + f"Response: {response.status_code} " + f"(took {process_time:.2f}s)" + ) + + return response +``` + +### 🔄 定期维护任务 + +#### 每周任务 + +```bash +# 1. 检查日志大小 +du -sh /app/logs + +# 2. 查看错误日志 +tail -n 100 /app/logs/extraction-service.log | grep ERROR + +# 3. 重启应用(如果有内存泄漏) +# SAE 控制台 → 应用详情 → 重启 +``` + +#### 每月任务 + +```bash +# 1. 更新 Python 依赖 +pip list --outdated + +# 2. 重建镜像(包含安全更新) +docker build -t extraction-service:v1.1 . +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:v1.1 + +# 3. 在 SAE 中更新镜像版本 +``` + +--- + +## 故障排查 + +### 🔥 常见问题 + +#### 问题 1:容器启动失败 + +**症状**: +``` +SAE 显示:应用启动失败 +日志显示:ImportError: libXXX.so: cannot open shared object file +``` + +**原因**:缺少系统依赖 + +**解决**: +```dockerfile +# 在 Dockerfile 中添加缺失的库 +RUN apt-get update && apt-get install -y \ + libgl1-mesa-glx \ # OpenCV + libglib2.0-0 \ # OpenCV + libgomp1 \ # Polars + libmupdf-dev \ # PyMuPDF + && rm -rf /var/lib/apt/lists/* +``` + +#### 问题 2:PDF 提取超时 + +**症状**: +``` +请求超时(> 300秒) +日志显示:Timeout error +``` + +**排查步骤**: +```bash +# 1. 检查文件大小 +# 如果文件 > 50MB,考虑分块处理 + +# 2. 增加超时时间 +# SAE 控制台 → 应用配置 → 环境变量 +TIMEOUT=600 + +# 3. 优化提取逻辑 +# 跳过图片页、压缩图片等 +``` + +#### 问题 3:内存溢出(OOM) + +**症状**: +``` +容器自动重启 +日志显示:Killed (signal 9) +``` + +**解决**: +```bash +# 1. 增加内存配置 +# SAE 控制台 → 应用配置 → 规格 +内存: 2GB → 4GB + +# 2. 优化代码(流式处理) +# 不要一次性加载整个文件到内存 +with open(pdf_path, 'rb') as f: + # 分块处理 + for chunk in read_in_chunks(f): + process(chunk) +``` + +#### 问题 4:后端无法连接 Python 服务(高频错误) + +**症状**: +``` +后端日志:Connection refused +或 +ECONNREFUSED: connect ECONNREFUSED 172.16.0.10:8000 +或 +Error: getaddrinfo ENOTFOUND extraction-service.internal +``` + +**根本原因排查**: + +**原因 1:内网地址配置错误(最常见)** +```bash +# ❌ 错误配置(猜测的域名) +EXTRACTION_SERVICE_URL=http://extraction-service.internal:8000 + +# ✅ 正确配置(SAE 控制台显示的真实地址) +EXTRACTION_SERVICE_URL=http://172.16.0.10:8000 +``` + +**解决方法**: +```bash +# 1. 获取真实内网地址 +# SAE 控制台 → extraction-service 应用 → 应用详情 → 应用访问配置 +# 复制显示的"VPC 内网访问地址" + +# 2. 更新后端环境变量 +# SAE 控制台 → 后端应用 → 应用配置 → 环境变量 +EXTRACTION_SERVICE_URL=http://<真实内网IP>:8000 + +# 3. 重启后端应用 +# SAE 控制台 → 后端应用 → 重启 +``` + +**原因 2:Python 服务未启动** +```bash +# 检查 Python 服务状态 +# SAE 控制台 → extraction-service 应用 → 实例列表 +# 确认实例状态为"运行中" + +# 查看启动日志 +# SAE 控制台 → extraction-service 应用 → 日志 +# 应该看到: +# INFO: Application startup complete. +# INFO: Uvicorn running on http://0.0.0.0:8000 +``` + +**原因 3:安全组规则限制** +```bash +# SAE 默认同 VPC 内应用可互相访问 +# 如果仍无法连接,检查: +# SAE 控制台 → extraction-service 应用 → 网络配置 → 安全组 +# 确认入站规则允许 VPC 内访问 8000 端口 +``` + +**测试内网连通性**: +```bash +# 方法 1:在 SAE 控制台的"Webshell"中测试(如果支持) +curl http://:8000/health + +# 方法 2:在后端应用的启动脚本中添加测试 +echo "Testing extraction service connectivity..." +curl -f http://:8000/health || echo "❌ Cannot connect to extraction service" + +# 方法 3:使用 telnet 测试端口 +telnet 8000 +``` + +--- + +## 注意事项与禁忌 + +### ✅ 最佳实践 + +#### 1. **镜像优化** + +```dockerfile +# ✅ 使用多阶段构建 +FROM python:3.11-slim as builder +# ... 构建 ... +FROM python:3.11-slim +COPY --from=builder /opt/venv /opt/venv + +# ✅ 清理缓存 +RUN apt-get update && apt-get install -y ... \ + && rm -rf /var/lib/apt/lists/* + +# ✅ 使用 .dockerignore +# 避免将不必要的文件打包到镜像 +``` + +#### 2. **版本管理** + +```bash +# ✅ 使用语义化版本 +v1.0.0 # 主版本.次版本.补丁版本 + +# ✅ 保留多个版本 +docker tag ... extraction-service:v1.0.0 +docker tag ... extraction-service:v1.0 +docker tag ... extraction-service:latest + +# ✅ 记录变更 +# CHANGELOG.md +## v1.0.1 (2025-12-20) +- 修复: PDF 提取超时问题 +- 优化: 减小镜像体积 30% +``` + +#### 3. **安全加固** + +```python +# ✅ 文件大小限制 +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB + +@app.post("/extract/pdf") +async def extract_pdf(file: UploadFile): + if file.size > MAX_FILE_SIZE: + raise HTTPException( + status_code=413, + detail="File too large" + ) + +# ✅ 文件类型验证 +ALLOWED_TYPES = {'application/pdf', 'application/msword'} + +if file.content_type not in ALLOWED_TYPES: + raise HTTPException( + status_code=415, + detail="Unsupported file type" + ) +``` + +#### 4. **性能优化** + +```python +# ✅ 异步处理大文件 +import asyncio + +async def extract_large_pdf(pdf_path: str): + # 使用异步 I/O + async with aiofiles.open(pdf_path, 'rb') as f: + content = await f.read() + + # 在线程池中执行 CPU 密集型任务 + loop = asyncio.get_event_loop() + text = await loop.run_in_executor(None, pymupdf_extract, content) + + return text + +# ✅ 连接池 +from sqlalchemy.pool import NullPool + +engine = create_engine( + DATABASE_URL, + poolclass=NullPool, # SAE 环境推荐 + echo=False +) +``` + +### ❌ 绝对禁止 + +#### 1. **禁止猜测或假设内网地址(致命错误)** + +```bash +# ❌ 错误做法(会导致连接失败) +EXTRACTION_SERVICE_URL=http://extraction-service.internal:8000 +EXTRACTION_SERVICE_URL=http://localhost:8000 +EXTRACTION_SERVICE_URL=http://extraction-service:8000 + +# ✅ 正确做法:从 SAE 控制台获取真实地址 +# SAE 控制台 → extraction-service 应用 → 应用访问配置 +# 复制显示的"VPC 内网访问地址" +EXTRACTION_SERVICE_URL=http://172.16.0.10:8000 +``` + +**原因**: +- SAE 实例间是跨主机的,不能使用 Docker 服务名 +- SAE 的 K8s Service 域名格式因配置而异,不能假设 +- 最稳妥的是使用 SAE 控制台显示的 IP 地址 + +#### 2. **禁止在镜像中硬编码敏感信息** + +```dockerfile +# ❌ 错误示例 +ENV DATABASE_PASSWORD=my-secret-password + +# ✅ 正确做法:在 SAE 环境变量中配置 +``` + +#### 3. **禁止使用本地文件持久化存储** + +```python +# ❌ 错误示例(容器重启后丢失) +output_path = '/app/output/result.txt' +with open(output_path, 'w') as f: + f.write(result) + +# ✅ 正确做法:使用 /tmp 存临时文件,结果上传到 OSS +import tempfile +with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: + f.write(result) +# 上传到 OSS(使用 oss2 库) +# 最后删除临时文件 +``` + +#### 4. **禁止使用 :latest 标签在生产环境** + +```bash +# ❌ 错误做法(无法回滚) +image: extraction-service:latest + +# ✅ 正确做法(语义化版本) +image: extraction-service:v1.0.0 +``` + +#### 5. **禁止在容器内修改代码** + +```bash +# ❌ 错误操作(容器重启后丢失) +# SAE Webshell → vi /app/main.py + +# ✅ 正确流程: +# 1. 本地修改代码 +# 2. 重建镜像 +# 3. 推送到 ACR +# 4. SAE 中更新镜像版本 +``` + +#### 6. **禁止使用无限增长的全局变量** + +```python +# ❌ 错误示例(内存泄漏) +CACHE = {} # 全局缓存,无限增长 + +@app.post("/extract/pdf") +async def extract_pdf(file: UploadFile): + key = file.filename + if key not in CACHE: + CACHE[key] = extract(file) # 内存会持续增长! + return CACHE[key] + +# ✅ 正确做法:使用有限容量的缓存 +from functools import lru_cache + +@lru_cache(maxsize=100) # 最多缓存 100 个结果 +def extract_with_cache(file_hash: str): + return extract(file_hash) +``` + +#### 7. **禁止忽略 /tmp 目录的大小限制** + +```python +# ⚠️ 注意:SAE 容器的 /tmp 目录通常有大小限制(如 1-2GB) +# 处理大文件后必须清理临时文件 + +import os +import tempfile + +async def extract_large_pdf(file: UploadFile): + # 保存到临时文件 + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp: + content = await file.read() + tmp.write(content) + tmp_path = tmp.name + + try: + # 处理文件 + result = extract_pdf_pymupdf(tmp_path) + return result + finally: + # ✅ 关键:必须清理临时文件 + if os.path.exists(tmp_path): + os.unlink(tmp_path) +``` + +--- + +## 📚 附录 + +### A. 完整的 requirements.txt(阶段1) + +```txt +# Web 框架 +fastapi==0.115.5 +uvicorn[standard]==0.32.1 +python-multipart==0.0.20 + +# 文档提取 +PyMuPDF==1.24.14 +mammoth==1.8.0 +python-docx==1.1.2 + +# 数据处理 +polars==1.17.1 +numpy==1.26.4 + +# 辅助工具 +langdetect==1.0.9 +chardet==5.2.0 +aiofiles==23.2.1 + +# 数据库 +sqlalchemy==2.0.25 +asyncpg==0.29.0 + +# 阿里云 OSS +oss2==2.18.3 + +# 日志和监控 +python-json-logger==2.0.7 +psutil==5.9.8 +``` + +### B. Dockerfile 完整版 + +参见前文 [构建 Docker 镜像 - 步骤 1](#步骤-1创建优化的-dockerfile) + +### C. 本地测试脚本 + +```bash +#!/bin/bash +# test-local.sh + +echo "Building Docker image..." +docker build -t extraction-service:test . + +echo "Starting container..." +docker run -d \ + --name extraction-test \ + -p 8000:8000 \ + -e DATABASE_URL="postgresql://user:pass@host:5432/db" \ + extraction-service:test + +echo "Waiting for service to start..." +sleep 10 + +echo "Testing health endpoint..." +curl http://localhost:8000/health + +echo "Testing PDF extraction..." +curl -X POST \ + -F "file=@test.pdf" \ + http://localhost:8000/extract/pdf + +echo "Cleaning up..." +docker stop extraction-test +docker rm extraction-test + +echo "Done!" +``` + +### D. 相关文档链接 + +- [阿里云 SAE 文档](https://help.aliyun.com/product/134532.html) +- [Docker 文档](https://docs.docker.com/) +- [FastAPI 文档](https://fastapi.tiangolo.com/) +- [PyMuPDF 文档](https://pymupdf.readthedocs.io/) +- [Polars 文档](https://pola-rs.github.io/polars/) + +--- + +## 🎯 快速参考 + +### 常用命令 + +```bash +# 构建镜像 +docker build -t extraction-service:v1.0 . + +# 推送镜像 +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/extraction-service:v1.0 + +# 查看 SAE 日志 +# SAE 控制台 → 应用详情 → 日志 + +# 重启 SAE 应用 +# SAE 控制台 → 应用详情 → 重启 + +# 测试内网连通性 +curl http://extraction-service.internal:8000/health + +# 查看容器资源 +docker stats extraction-service +``` + +### 关键配置 + +| 配置项 | 推荐值 | 说明 | +|-------|--------|------| +| CPU | 1核 | 初始配置 | +| 内存 | 2GB | 不含 Nougat | +| 实例数 | 1-3 | 自动弹性伸缩 | +| 超时时间 | 300秒 | 大文件处理 | +| 健康检查 | 30秒 | 初始延迟 | +| Worker 数量 | 2 | Uvicorn workers | + +--- + +**文档维护**: +- 如有问题或建议,请联系技术负责人 +- 最后更新:2025-12-13 +- 下次审查:2025-03-13 + diff --git a/docs/05-部署文档/05-Node.js后端-SAE容器部署指南.md b/docs/05-部署文档/05-Node.js后端-SAE容器部署指南.md new file mode 100644 index 00000000..00d71b61 --- /dev/null +++ b/docs/05-部署文档/05-Node.js后端-SAE容器部署指南.md @@ -0,0 +1,2181 @@ +# Node.js 后端 - SAE 容器部署完全指南 + +**文档版本**: v1.1 (修复 Prisma 构建和数据库同步问题) +**创建时间**: 2025-12-13 +**最后修订**: 2025-12-13 +**适用范围**: AIclinicalresearch 平台 - Node.js 后端服务 +**目标读者**: 运维工程师、后端开发工程师 +**部署目标**: 阿里云 SAE(Serverless 应用引擎)容器部署 + +**v1.1 更新日志**: +- 🛑 修复:Prisma 目录构建上下文问题(增加预处理步骤) +- 🛑 修复:生产环境缺少 Prisma CLI 导致迁移失败 +- 🔥 新增:针对开发历史不规范的"反向同步"流程(关键) +- ✅ 优化:数据库迁移策略(pg_dump 导入后不执行 migrate) +- ✅ 简化:移除 Init Container 方案,改用启动命令 + +--- + +## 📋 目录 + +1. [为什么选择 SAE 容器部署](#1-为什么选择-sae-容器部署) +2. [部署前准备](#2-部署前准备) +3. [后端服务分析](#3-后端服务分析) +4. [🔥 Prisma 反向同步(必读)](#4-prisma-反向同步必读) +5. [构建 Docker 镜像](#5-构建-docker-镜像) +6. [本地测试验证](#6-本地测试验证) +7. [推送到 ACR](#7-推送到-acr) +8. [SAE 应用配置](#8-sae-应用配置) +9. [数据库部署策略](#9-数据库部署策略) +10. [端到端测试](#10-端到端测试) +11. [监控与维护](#11-监控与维护) +12. [故障排查](#12-故障排查) +13. [注意事项与禁忌](#13-注意事项与禁忌) + +--- + +## 1. 为什么选择 SAE 容器部署 + +### ✅ 核心优势 + +| 优势 | 说明 | 对您的价值 | +|------|------|----------| +| **环境一致性** | Docker 镜像保证本地开发(Node 22 + Prisma)与线上环境完全一致 | 杜绝"我本地明明能跑"的问题 | +| **弹性伸缩** | SAE 根据 CPU/内存使用率自动增减实例数量 | 大量用户 AI 对话时自动扩容 | +| **免运维** | 无需管理服务器 OS、安全补丁,SAE 全托管 | 1-2 人团队节省运维精力 | +| **内网互通** | 与 SAE 的 Python 服务和 ECS 的 Dify 通过 VPC 内网高速通信 | 毫秒级延迟,无公网流量费 | +| **私有化就绪** | Docker 镜像可直接交付给医院,部署到内网环境 | 满足医疗数据合规要求 | +| **统一架构** | 前端(Nginx)、后端(Node.js)、Python 微服务都用 Docker | 一套部署流程,降低学习成本 | + +### 🎯 为什么不用 Code Package 部署? + +| 对比项 | Code Package | 容器部署(推荐)| +|-------|--------------|----------------| +| 环境一致性 | ⚠️ 云端 Node 版本可能不同 | ✅ 镜像锁定版本 | +| 私有化交付 | ❌ 无法直接交付 | ✅ 打包即可交付 | +| 系统依赖 | ⚠️ 无法自定义 | ✅ Dockerfile 完全控制 | +| 构建速度 | ✅ 快(上传即可) | ⚠️ 慢(需构建镜像)| +| 适用场景 | 快速原型验证 | 生产环境、私有化 | + +**结论**:对于需要私有化部署的医疗科研产品,容器部署是唯一选择。 + +--- + +## 2. 部署前准备 + +### ✅ 前置条件检查清单 + +#### 本地开发环境 + +- [ ] Docker Desktop 已安装并运行(版本 20.10+) +- [ ] Node.js 已安装(版本 22+) +- [ ] 后端代码已拉取到本地 +- [ ] 后端在本地能正常启动(`npm run dev`) + +验证命令: + +```bash +# 检查 Docker +docker --version +# 输出示例:Docker version 24.0.6 + +# 检查 Node.js +node --version +# 输出示例:v22.11.0 + +# 检查 npm +npm --version +# 输出示例:10.9.0 +``` + +#### 阿里云资源 + +- [ ] **RDS PostgreSQL 15** 实例已创建并运行 + - 数据库名称:`ai_clinical`(或自定义) + - 用户名和密码已准备 + - 内网地址已获取(如 `rm-xxxxx.pg.rds.aliyuncs.com:5432`) + - 白名单已配置(允许 SAE VPC 访问) + +- [ ] **阿里云容器镜像服务 ACR** 已开通 + - 命名空间已创建(如 `clinical-research`) + - 登录密码已设置 + +- [ ] **SAE 应用** 已创建(或准备创建) + - VPC 和交换机已选择(与 RDS 在同一 VPC) + +- [ ] **依赖服务的内网地址已获取**: + - Python 微服务(SAE):`http://172.16.0.10:8000` + - Dify 服务(ECS):`http://172.16.0.20:80` + +#### 敏感信息准备 + +准备以下配置信息(稍后配置到 SAE 环境变量): + +```bash +# 数据库 +DATABASE_URL=postgresql://username:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical?connection_limit=18&pool_timeout=10 + +# LLM API Keys(至少配置一个) +DEEPSEEK_API_KEY=sk-xxxxx +DASHSCOPE_API_KEY=sk-xxxxx +CLOSEAI_API_KEY=sk-xxxxx + +# Dify +DIFY_API_KEY=app-xxxxx +DIFY_API_URL=http://172.16.0.20:80/v1 + +# 阿里云 OSS +OSS_REGION=oss-cn-hangzhou +OSS_BUCKET=clinical-research-files +OSS_ACCESS_KEY_ID=LTAI5t... +OSS_ACCESS_KEY_SECRET=xxx... + +# JWT 安全密钥(生产环境必须修改) +JWT_SECRET=your-strong-random-secret-min-32-chars +``` + +--- + +## 3. 后端服务分析 + +### 📦 技术栈 + +| 技术 | 版本 | 用途 | +|------|------|------| +| **Node.js** | 22+ | 运行时环境 | +| **Fastify** | 5.6+ | Web 框架(高性能) | +| **TypeScript** | 5.9+ | 类型安全 | +| **Prisma** | 6.17+ | ORM(数据库访问) | +| **pg-boss** | 12.5+ | 队列(Postgres-Only) | +| **winston** | 3.18+ | 日志系统 | + +### 📊 依赖服务 + +``` +Node.js 后端(SAE) + │ + ├──→ RDS PostgreSQL 15(数据库) + │ + ├──→ Python 微服务(SAE) - 文档提取 + │ └─ http://172.16.0.10:8000 + │ + ├──→ Dify 服务(ECS) - RAG 知识库 + │ └─ http://172.16.0.20:80/v1 + │ + └──→ 阿里云 OSS - 文件存储 + └─ clinical-research-files +``` + +### 🔧 核心功能模块 + +| 模块 | 路径 | 功能 | 依赖服务 | +|------|------|------|---------| +| **AIA** | `/api/chat` | AI 智能助理 | DeepSeek/千问/Claude | +| **PKB** | `/api/knowledge-bases` | 个人知识库 | Dify + OSS | +| **ASL** | `/api/asl/*` | 智能文献 | Python 微服务 + OSS | +| **DC** | `/api/dc/*` | 数据清洗 | Python 微服务 + OSS | +| **RVW** | `/api/review` | 稿件审查 | DeepSeek/千问 | +| **Health** | `/health` | 健康检查 | RDS | + +### 📝 启动流程 + +```bash +# 1. 安装依赖 +npm install + +# 2. 生成 Prisma Client +npm run prisma:generate + +# 3. 数据库迁移(仅首次或更新时) +npx prisma migrate deploy + +# 4. 编译 TypeScript +npm run build + +# 5. 启动应用 +npm start +``` + +--- + +## 4. 🔥 Prisma 反向同步(必读) + +### ⚠️ 重要警告:为什么需要这一步? + +**您的开发历史**: +- ✅ 使用 `pg_dump` 导出了本地 PostgreSQL 数据库(包含表结构和数据) +- ✅ 已经导入到阿里云 RDS PostgreSQL +- ⚠️ **但是**:开发过程中,经常直接用 SQL 或 Navicat 修改数据库,**没有同步更新 `schema.prisma` 文件** + +**后果**: +```typescript +// schema.prisma 中定义: +model User { + id String + email String + name String + // ❌ 缺少 phone 字段 +} + +// 但数据库里实际有: +CREATE TABLE "User" ( + id VARCHAR(255), + email VARCHAR(255), + name VARCHAR(255), + phone VARCHAR(50) -- ✅ 这个字段存在,但 Prisma 不知道 +); + +// 代码里尝试访问: +const user = await prisma.user.findUnique({ where: { id: '123' } }); +console.log(user.phone); // ❌ TypeScript 报错:Property 'phone' does not exist on type 'User' +``` + +**更严重的后果**: +- Prisma Client 生成的类型与数据库不一致 +- 运行时可能读不到数据或报错 +- 如果强行运行 `prisma migrate deploy`,可能因为表已存在而失败 + +### ✅ 解决方案:反向同步(Introspection) + +#### 步骤 1:连接到 RDS 数据库 + +在本地开发环境,临时连接到 RDS: + +```bash +# 1. 备份当前的 .env 文件 +cp backend/.env backend/.env.backup + +# 2. 创建临时 RDS 连接配置 +cat > backend/.env.rds < { process.exit(res.statusCode === 200 ? 0 : 1); })" + +# 暴露端口 +EXPOSE 3001 + +# 🔥 启动命令(仅启动应用,不执行数据库迁移) +# 解释:因为数据库已通过 pg_dump 导入,结构已就绪,无需 migrate +CMD ["node", "dist/index.js"] +``` + +### 📝 Dockerfile 关键修改说明 + +#### 修改 1:全局安装 Prisma CLI + +```dockerfile +# 🔥 在阶段 2 新增: +RUN npm install -g prisma@6.17.0 +``` + +**原因**: +- `npm ci` + `npm prune --production` 会删除 devDependencies 中的 `prisma` 包 +- 但生产环境可能需要执行 `npx prisma db pull` 或排查问题 +- 全局安装确保 `prisma` 命令始终可用 + +**代价**: +- 镜像体积增加约 50MB +- 但这是值得的(避免"命令找不到"的问题) + +#### 修改 2:不执行数据库迁移 + +```dockerfile +# ❌ 错误做法(会导致"表已存在"错误): +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"] + +# ✅ 正确做法(仅启动应用): +CMD ["node", "dist/index.js"] +``` + +**原因**: +- 您的数据库是通过 `pg_dump` 导入的,表结构已经存在 +- 如果执行 `prisma migrate deploy`,可能因为迁移记录对不上而报错 +- 正确的做法:启动前确保 `schema.prisma` 与数据库一致(第 4 节已处理) + +#### 修改 3:移除 dumb-init(可选优化) + +```dockerfile +# 移除: +# RUN apk add --no-cache dumb-init +# ENTRYPOINT ["dumb-init", "--"] + +# Node.js 22 在处理信号时已经足够稳定 +# 如果你发现下载 dumb-init 很慢或报错,可以去掉这个优化 +``` + +### 📝 创建 .dockerignore + +在 `backend/` 目录下创建 `.dockerignore`: + +``` +# Node.js +node_modules +npm-debug.log +yarn-error.log + +# 开发文件 +.env +.env.* +*.local + +# 构建产物(已在 Dockerfile 中生成) +dist + +# 测试文件 +test +tests +*.test.ts +*.spec.ts +coverage + +# 文档和临时文件 +docs +*.md +.vscode +.idea +.DS_Store +Thumbs.db + +# 上传文件(运行时生成) +uploads/* + +# Git +.git +.gitignore + +# 日志 +*.log +logs + +# 临时文件 +temp +tmp +*.swp +*.swo +*~ + +# 数据库文件(SQLite,如果有) +*.db +*.sqlite + +# 脚本文件(仅开发使用) +scripts/*.ts +*.bat +*.ps1 +``` + +--- + +## 6. 本地测试验证 + +### 🛑 前置步骤:复制 Prisma 文件(必须执行) + +```bash +# 1. 回到项目根目录 +cd AIclinicalresearch + +# 2. 复制 prisma 到 backend 目录(确保构建时能找到) +cp -r prisma backend/prisma + +# 3. 验证复制成功 +ls backend/prisma/schema.prisma +# 应该输出:backend/prisma/schema.prisma +``` + +### 步骤 1:构建镜像 + +```bash +# 在 backend 目录下执行 +cd backend + +# 🔥 确保已执行上面的复制步骤! + +# 构建镜像(需要 5-10 分钟) +docker build -t backend-service:v1.0.0 . + +# 查看镜像大小 +docker images backend-service:v1.0.0 +# 预期大小:~300-500MB(Alpine 基础镜像 + Node.js + 依赖) +``` + +**如果构建失败**: + +```bash +# 常见问题 1:Prisma 文件找不到 +# 错误信息:COPY failed: file not found in build context or excluded by .dockerignore: stat prisma: file does not exist +# 解决:确保执行了复制步骤:cp -r ../prisma ./prisma(在 backend 目录外执行) + +# 常见问题 2:网络超时(npm install 慢) +# 解决:使用国内镜像源 +# 在 Dockerfile 的 npm ci 之前添加: +RUN npm config set registry https://registry.npmmirror.com + +# 常见问题 3:Prisma 生成失败 +# 解决:检查 backend/prisma/schema.prisma 是否存在且格式正确 +``` + +### 步骤 2:本地运行测试 + +```bash +# 创建测试环境变量文件 +cat > .env.docker.test </dev/null || echo "✅ prisma 目录已清理" + +# 注意:根目录的 prisma/ 文件夹保留,这是源文件 +``` + +--- + +## 7. 推送到 ACR + +### 步骤 1:登录 ACR + +```bash +# 获取 ACR 登录地址(阿里云控制台 → 容器镜像服务 → 访问凭证) +# 示例:registry.cn-hangzhou.aliyuncs.com + +# 登录(使用 ACR 密码,不是阿里云账号密码) +docker login --username=your-aliyun-account registry.cn-hangzhou.aliyuncs.com + +# 输入密码后看到: +# Login Succeeded +``` + +### 步骤 2:标记镜像 + +```bash +# 格式:registry地址/命名空间/仓库名:版本号 +docker tag backend-service:v1.0.0 \ + registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.0 + +# 同时打一个 latest 标签(方便测试) +docker tag backend-service:v1.0.0 \ + registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:latest +``` + +### 步骤 3:推送镜像 + +```bash +# 推送指定版本 +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.0 + +# 推送 latest +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:latest + +# 推送过程需要 5-10 分钟(视网络速度) +``` + +### 步骤 4:验证推送成功 + +登录阿里云控制台 → 容器镜像服务 → 镜像仓库 → `backend-service` + +- 应该看到版本:`v1.0.0` 和 `latest` +- 镜像大小:~300-500MB +- 推送时间:刚才的时间 + +--- + +## 8. SAE 应用配置 + +### 步骤 1:创建应用 + +**阿里云控制台** → **SAE** → **应用列表** → **创建应用** + +| 配置项 | 值 | 说明 | +|-------|-----|------| +| **应用名称** | `backend-service` | 后端服务 | +| **命名空间** | 选择已创建的命名空间 | 与 Python 服务同一命名空间 | +| **VPC** | 选择 RDS 所在 VPC | 必须与 RDS 在同一 VPC | +| **交换机** | 选择可用区 | 建议多可用区 | +| **应用实例规格** | 2核4G | 初始规格 | +| **实例数量** | 2 | 最小 2 个实例(高可用) | + +### 步骤 2:配置镜像 + +| 配置项 | 值 | +|-------|-----| +| **镜像类型** | 容器镜像服务企业版实例 | +| **镜像仓库** | `registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service` | +| **镜像版本** | `v1.0.0` | +| **镜像拉取策略** | 总是拉取镜像 | + +### 步骤 3:配置端口 + +| 配置项 | 值 | +|-------|-----| +| **容器端口** | `3001` | +| **协议** | TCP | + +### 步骤 4:配置环境变量(关键步骤) + +**⚠️ 重要:请仔细配置以下环境变量** + +#### 基础配置 + +```bash +NODE_ENV=production +PORT=3001 +HOST=0.0.0.0 +LOG_LEVEL=info +SERVICE_NAME=backend-service +``` + +#### 数据库配置(必需) + +```bash +# ⚠️ 使用 RDS 内网地址 +DATABASE_URL=postgresql://username:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical?connection_limit=18&pool_timeout=10 + +# 连接池配置(根据 RDS 规格调整) +DB_MAX_CONNECTIONS=400 +MAX_INSTANCES=20 +``` + +**连接池计算说明**: +- 假设 RDS 最大连接数:400 +- SAE 最大实例数:20 +- 每实例连接数 = (400 / 20) - 预留 = 18 +- 预留 10 个连接给管理任务和其他服务 + +#### 存储配置(必需) + +```bash +# 使用阿里云 OSS +STORAGE_TYPE=oss +OSS_REGION=oss-cn-hangzhou +OSS_BUCKET=clinical-research-files +OSS_ACCESS_KEY_ID=LTAI5t... +OSS_ACCESS_KEY_SECRET=xxx... +``` + +#### 缓存和队列配置(Postgres-Only) + +```bash +# 使用 PostgreSQL 作为缓存(不需要 Redis) +CACHE_TYPE=postgres + +# 使用 pg-boss 作为队列(不需要 Redis) +QUEUE_TYPE=pgboss +``` + +#### LLM API 配置(至少配置一个) + +```bash +# DeepSeek(推荐,性价比高) +DEEPSEEK_API_KEY=sk-xxxxx +DEEPSEEK_BASE_URL=https://api.deepseek.com + +# 通义千问(备选) +DASHSCOPE_API_KEY=sk-xxxxx + +# CloseAI(OpenAI/Claude 代理) +CLOSEAI_API_KEY=sk-xxxxx +CLOSEAI_OPENAI_BASE_URL=https://api.openai-proxy.org/v1 +CLOSEAI_CLAUDE_BASE_URL=https://api.openai-proxy.org/anthropic +``` + +#### Dify 配置(必需) + +```bash +# ⚠️ 使用 ECS 内网 IP(不要使用公网域名) +DIFY_API_URL=http://172.16.0.20:80/v1 +DIFY_API_KEY=app-xxxxx +``` + +**如何获取 Dify 内网 IP**: +1. 登录 ECS 控制台 +2. 找到 Dify 所在的 ECS 实例 +3. 查看"私有 IP 地址"(如 `172.16.0.20`) + +#### 安全配置(必需) + +```bash +# ⚠️ 生产环境必须修改为强密码(至少 32 位随机字符串) +JWT_SECRET=your-strong-random-secret-min-32-chars-change-in-production +JWT_EXPIRES_IN=7d +``` + +**生成强密码**: + +```bash +# Linux/Mac +openssl rand -base64 32 + +# Windows PowerShell +-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | ForEach-Object {[char]$_}) +``` + +#### CORS 配置(可选) + +```bash +# 如果前端使用自定义域名,配置允许的源 +# 注意:如果前端使用 Nginx 反向代理,则不需要配置(Nginx 已处理) +CORS_ORIGIN=https://your-frontend-domain.com +``` + +### 步骤 5:配置健康检查 + +**SAE 控制台** → **应用配置** → **健康检查** + +| 配置项 | 值 | 说明 | +|-------|-----|------| +| **检查方式** | HTTP 请求 | | +| **检查路径** | `/health` | 后端健康检查端点 | +| **检查端口** | 3001 | 与容器端口一致 | +| **检查协议** | HTTP | | +| **初始延迟** | 60 秒 | 给 Prisma 初始化足够时间 | +| **检查间隔** | 10 秒 | | +| **超时时间** | 3 秒 | | +| **不健康阈值** | 3 次 | 连续失败 3 次标记为不健康 | +| **健康阈值** | 2 次 | 连续成功 2 次标记为健康 | + +### 步骤 6:配置弹性伸缩 + +**SAE 控制台** → **应用配置** → **弹性伸缩** + +| 配置项 | 值 | 说明 | +|-------|-----|------| +| **最小实例数** | 2 | 高可用保证 | +| **最大实例数** | 10 | 根据预期负载调整 | +| **扩容条件** | CPU > 70% 持续 3 分钟 | | +| **缩容条件** | CPU < 30% 持续 5 分钟 | | + +### 步骤 7:配置日志 + +**SAE 控制台** → **应用配置** → **日志配置** + +| 配置项 | 值 | +|-------|-----| +| **日志类型** | 标准输出(stdout) | +| **日志存储** | 开启(保存 7 天) | + +### 步骤 8:部署应用 + +点击"部署"按钮,SAE 将: + +1. 从 ACR 拉取镜像(~2 分钟) +2. 启动容器实例(~1 分钟) +3. 执行健康检查(~1 分钟) +4. 流量切换(~30 秒) + +**总耗时**:约 5 分钟 + +--- + +## 9. 数据库部署策略 + +### 🎯 您的实际情况(非常重要) + +根据您的开发历史,数据库部署策略与标准流程**完全不同**: + +**标准流程(不适合您)**: +```mermaid +graph LR + A[代码] --> B[Prisma Migrations] + B --> C[空数据库] + C --> D[自动创建表结构] +``` + +**您的实际流程**: +```mermaid +graph LR + A[本地 PostgreSQL
包含数据] --> B[pg_dump 导出] + B --> C[RDS 导入] + C --> D[表结构已存在] + D --> E[Prisma 反向同步] +``` + +### ✅ 正确的部署策略 + +#### 策略总览 + +| 步骤 | 操作 | 何时执行 | 目的 | +|------|------|---------|------| +| **1. 导出本地数据库** | `pg_dump` | 首次部署前 | 备份现有数据和结构 | +| **2. 导入到 RDS** | `psql` 或 DMS | 首次部署前 | 迁移到云端 | +| **3. Prisma 反向同步** | `prisma db pull` | 首次部署前(第 4 节) | 同步 Schema | +| **4. 启动应用** | `node dist/index.js` | SAE 部署时 | 正常运行 | +| **5. ~~数据库迁移~~** | ~~`prisma migrate deploy`~~ | ❌ **不执行** | 表已存在,无需迁移 | + +### 方案 1:使用 pg_dump 导入(您的情况,推荐) + +#### 步骤 1:导出本地数据库(如果还没做) + +```bash +# 在本地开发环境执行 +docker exec ai-clinical-postgres pg_dump -U postgres -d ai_clinical \ + --no-owner --no-acl --clean --if-exists \ + > ai_clinical_backup_$(date +%Y%m%d).sql + +# 查看导出文件 +ls -lh ai_clinical_backup_*.sql +# 应该看到文件大小(如 5.2MB) +``` + +#### 步骤 2:导入到 RDS(如果还没做) + +**方法 A:使用 psql 命令行(推荐)** + +```bash +# 连接到 RDS 并导入 +psql "postgresql://username:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical" \ + < ai_clinical_backup_20251213.sql + +# 输出示例: +# DROP TABLE +# CREATE TABLE +# INSERT 0 123 +# ... +# ✅ 导入完成 +``` + +**方法 B:使用 DMS(阿里云数据管理)** + +1. 登录阿里云控制台 → 数据管理 DMS +2. 连接到 RDS 实例 +3. 数据方案 → SQL 窗口 → 粘贴 SQL 文件内容 +4. 执行 + +#### 步骤 3:验证导入成功 + +```bash +# 连接到 RDS +psql "postgresql://username:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical" + +# 查看所有 Schema +\dn + +# 应该看到: +# aia_schema +# asl_schema +# common_schema +# dc_schema +# pkb_schema +# platform_schema +# rvw_schema +# ssa_schema +# st_schema +# public + +# 查看用户表 +SELECT count(*) FROM "User"; +# 应该返回正确的用户数量 + +# 退出 +\q +``` + +#### 步骤 4:确认已执行 Prisma 反向同步 + +**⚠️ 这一步非常关键!** + +```bash +# 确保第 4 节的步骤已完成: +# 1. 已执行 npx prisma db pull +# 2. 已执行 npx prisma generate +# 3. 已提交 schema.prisma 到 Git + +# 验证:检查 schema.prisma 的修改时间 +ls -l prisma/schema.prisma +# 修改时间应该是最近(今天) + +# 如果还没做,立即返回第 4 节执行! +``` + +### 方案 2:标准 Prisma Migrations(仅适用于新项目) + +**⚠️ 如果您的数据库是通过 `pg_dump` 导入的,请跳过这一节!** + +
+点击展开:标准迁移流程(仅供参考) + +#### 适用场景 + +- 全新项目,RDS 数据库是空的 +- 从未手动修改过数据库结构 +- 所有表结构都通过 Prisma Migrations 管理 + +#### 执行步骤 + +```bash +# 1. 连接到 RDS +export DATABASE_URL="postgresql://username:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical" + +# 2. 执行迁移 +cd backend +npx prisma migrate deploy + +# 3. 验证 +npx prisma db execute --stdin <<< "SELECT schemaname FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') GROUP BY schemaname;" +``` + +
+ +### 🚨 常见错误与修正 + +#### 错误 1:启动时执行 `prisma migrate deploy` 导致失败 + +**错误信息**: + +``` +Error: P3005: The database schema is not empty. Read more about how to baseline an existing production database: https://pris.ly/d/migrate-baseline +``` + +**原因**:表已通过 `pg_dump` 导入,再执行 migrate 会冲突。 + +**解决方法**: + +```bash +# 方法 1:移除启动命令中的 migrate(推荐) +# SAE 控制台 → 应用配置 → 启动命令 +# 确保启动命令是: +node dist/index.js + +# 不要写: +# sh -c "npx prisma migrate deploy && node dist/index.js" # ❌ +``` + +#### 错误 2:应用启动后,访问数据库报错 + +**错误信息**: + +```typescript +PrismaClientKnownRequestError: +Invalid `prisma.user.findMany()` invocation: + column "phone" does not exist +``` + +**原因**:`schema.prisma` 与数据库不一致。 + +**解决方法**: + +返回第 4 节,执行 `npx prisma db pull` 反向同步。 + +### ✅ 部署检查清单 + +在启动 SAE 应用前,确认以下步骤已完成: + +- [ ] 本地数据库已通过 `pg_dump` 导出 +- [ ] SQL 文件已导入到 RDS(表结构和数据都存在) +- [ ] 已执行 `npx prisma db pull` 同步 Schema +- [ ] 已执行 `npx prisma generate` 生成客户端 +- [ ] 已提交 `schema.prisma` 到 Git +- [ ] Docker 镜像的启动命令**不包含** `prisma migrate deploy` +- [ ] SAE 环境变量中的 `DATABASE_URL` 指向 RDS + +**如果以上都确认,可以放心部署!** ✅ + +--- + +## 10. 端到端测试 + +### 步骤 1:获取应用访问地址 + +**SAE 控制台** → **应用详情** → **应用访问配置** + +复制以下地址: + +``` +# 公网访问地址(用于前端调用) +https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com + +# VPC 内网访问地址(用于服务间调用) +http://172.16.0.30:3001 +``` + +### 步骤 2:测试健康检查 + +```bash +# 使用公网地址测试 +curl https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/health + +# 预期返回: +{ + "status": "healthy", + "timestamp": "2025-12-13T10:30:00.000Z", + "uptime": 12345.678, + "database": { + "status": "connected", + "connections": 8 + }, + "version": "1.0.0" +} +``` + +### 步骤 3:测试用户注册 + +```bash +curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "sae-test@example.com", + "password": "Test123456", + "name": "SAE测试用户" + }' + +# 预期返回: +{ + "success": true, + "user": { + "id": "...", + "email": "sae-test@example.com", + "name": "SAE测试用户" + }, + "token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +### 步骤 4:测试文件上传(PKB 模块) + +```bash +# 获取 Token(从步骤 3) +TOKEN="eyJhbGciOiJIUzI1NiIs..." + +# 上传 PDF 文档 +curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/knowledge-bases/kb-xxx/documents \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@test.pdf" \ + -F "name=测试文档" + +# 预期返回: +{ + "success": true, + "document": { + "id": "...", + "name": "测试文档", + "status": "processing" + } +} +``` + +### 步骤 5:测试 Python 微服务调用 + +```bash +# 测试 ASL 模块的 PDF 提取功能 +curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/asl/extract \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@paper.pdf" + +# 查看后端日志(SAE 控制台 → 应用详情 → 日志) +# 应该看到: +# [INFO] Calling extraction service: http://172.16.0.10:8000/extract/pdf +# [INFO] Extraction completed in 3.2s +``` + +### 步骤 6:测试 Dify 服务调用 + +```bash +# 测试 PKB 模块的知识库对话 +curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/knowledge-bases/kb-xxx/chat \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "这篇文献的研究方法是什么?", + "conversationId": "conv-xxx" + }' + +# 查看后端日志 +# 应该看到: +# [INFO] Calling Dify API: http://172.16.0.20:80/v1/chat-messages +# [INFO] Dify response received in 2.5s +``` + +### 步骤 7:测试 OSS 文件上传 + +```bash +# 上传大文件(测试 OSS) +curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/asl/upload \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@large-paper.pdf" + +# 查看日志,应该看到 OSS 上传成功: +# [INFO] File uploaded to OSS: clinical-research-files/asl/xxx.pdf +``` + +--- + +## 11. 监控与维护 + +### 📊 SAE 自带监控 + +#### 1. 实时监控 + +**SAE 控制台** → **应用详情** → **监控** + +**关键指标**: + +| 指标 | 健康阈值 | 告警阈值 | 说明 | +|------|---------|---------|------| +| **CPU 使用率** | < 60% | > 80% | LLM 调用是 CPU 密集型 | +| **内存使用率** | < 70% | > 85% | 监控内存泄漏 | +| **请求 QPS** | - | - | 了解负载 | +| **平均响应时间** | < 500ms | > 2000ms | AI 对话除外(可能 10s+) | +| **错误率** | < 0.5% | > 2% | 监控服务稳定性 | +| **实例数量** | 2+ | - | 确保高可用 | + +#### 2. 日志查看 + +**SAE 控制台** → **应用详情** → **日志** → **实时日志** + +**关键日志示例**: + +```bash +# ✅ 正常启动 +[Config] Environment validation passed +[Database] ✅ 数据库连接成功! +[Fastify] Server listening on http://0.0.0.0:3001 + +# ✅ 正常请求 +[INFO] POST /api/chat 200 2345ms + +# ⚠️ 警告日志(需关注) +[WARN] Database connection pool near limit: 16/18 connections + +# ❌ 错误日志(需立即处理) +[ERROR] Failed to connect to Python service: ECONNREFUSED +[ERROR] Prisma timeout: Database connection pool exhausted +[ERROR] Dify API error: 502 Bad Gateway +``` + +#### 3. 数据库连接监控 + +```bash +# 在 SAE Webshell 中执行(或使用 RDS 控制台) +psql $DATABASE_URL -c " +SELECT + count(*) as total_connections, + count(*) FILTER (WHERE state = 'active') as active_connections, + count(*) FILTER (WHERE state = 'idle') as idle_connections +FROM pg_stat_activity +WHERE datname = 'ai_clinical'; +" + +# 输出示例: +# total_connections | active_connections | idle_connections +# ------------------+--------------------+----------------- +# 35 | 5 | 30 + +# 告警条件:total_connections > (MAX_INSTANCES * connection_limit * 0.8) +# 示例:20 实例 * 18 连接/实例 * 0.8 = 288 连接 +``` + +### 🔧 日常维护任务 + +#### 每日检查 + +```bash +# 1. 检查应用健康状态 +# SAE 控制台 → 应用列表 → 查看运行状态(绿色为正常) + +# 2. 查看错误日志 +# SAE 控制台 → 应用详情 → 日志 → 筛选 ERROR 级别 + +# 3. 检查数据库连接数 +# RDS 控制台 → 实例监控 → 连接数(< 80% 为健康) +``` + +#### 每周任务 + +```bash +# 1. 查看性能指标趋势 +# SAE 控制台 → 应用详情 → 监控 → 选择"最近 7 天" +# 关注: +# - CPU/内存是否有持续上涨(内存泄漏?) +# - 响应时间是否变慢 +# - 错误率是否增加 + +# 2. 清理日志(如果日志存储空间紧张) +# SAE 控制台 → 应用详情 → 日志配置 → 清理旧日志 +``` + +#### 每月任务 + +```bash +# 1. 更新依赖(安全补丁) +# 在本地开发环境执行: +npm outdated +npm update +npm audit fix + +# 重新测试后部署 + +# 2. 重建镜像(包含系统安全更新) +docker build -t backend-service:v1.0.1 . +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.1 + +# 3. 在 SAE 中灰度更新 +# SAE 控制台 → 应用详情 → 部署 +# 选择新镜像版本:v1.0.1 +# 灰度发布:先更新 1 个实例,观察 10 分钟后全量发布 + +# 4. 数据库备份(RDS 自动备份,仅需验证) +# RDS 控制台 → 备份恢复 → 查看最近备份时间 +``` + +### 🚨 告警配置 + +**云监控** → **应用监控** → **创建告警规则** + +**推荐告警规则**: + +| 告警项 | 阈值 | 通知方式 | +|-------|------|---------| +| CPU 使用率 > 80% 持续 5 分钟 | 告警 | 钉钉/邮件 | +| 内存使用率 > 85% 持续 5 分钟 | 告警 | 钉钉/邮件 | +| 错误率 > 2% 持续 3 分钟 | 紧急 | 短信+钉钉 | +| 实例健康检查失败 > 3 次 | 紧急 | 短信+钉钉 | +| RDS 连接数 > 80% | 警告 | 钉钉/邮件 | + +--- + +## 12. 故障排查 + +### 问题 1:应用启动失败 + +**症状**: + +``` +SAE 控制台显示:实例启动中 → 健康检查失败 → 实例停止 +``` + +**排查步骤**: + +```bash +# 1. 查看启动日志 +# SAE 控制台 → 应用详情 → 日志 → 筛选最近 5 分钟 + +# 2. 常见错误原因: + +# 错误 A:环境变量验证失败 +# 日志:❌ [Config] Environment validation failed: DATABASE_URL is required +# 解决:检查 SAE 环境变量配置,补充缺失的变量 + +# 错误 B:数据库连接失败 +# 日志:❌ 数据库连接失败: getaddrinfo ENOTFOUND rm-xxxxx.pg.rds.aliyuncs.com +# 解决: +# - 检查 DATABASE_URL 是否正确 +# - 检查 RDS 白名单是否允许 SAE VPC 访问 +# - 检查 RDS 内网地址是否可达 + +# 错误 C:Prisma 迁移未执行 +# 日志:Error: P1001: Can't reach database server at `rm-xxxxx.pg.rds.aliyuncs.com` +# 解决:先执行数据库迁移(参见第 8 节) + +# 错误 D:端口冲突 +# 日志:Error: listen EADDRINUSE: address already in use :::3001 +# 解决:检查 PORT 环境变量是否为 3001(与 Dockerfile EXPOSE 一致) +``` + +### 问题 2:数据库连接池耗尽 + +**症状**: + +``` +[ERROR] Prisma timeout: Database connection pool exhausted +[ERROR] P2024: Timed out fetching a new connection from the pool +``` + +**根本原因**: + +- SAE 实例数 * 每实例连接数 > RDS 最大连接数 +- 连接未正确释放(代码 Bug) + +**排查步骤**: + +```bash +# 1. 检查当前连接数 +psql $DATABASE_URL -c " +SELECT count(*) as current_connections +FROM pg_stat_activity +WHERE datname = 'ai_clinical'; +" + +# 2. 检查 SAE 实例数 +# SAE 控制台 → 应用详情 → 实例列表 +# 假设有 10 个实例 + +# 3. 计算连接数 +# 每实例连接数 = 18(connection_limit) +# 总连接数 = 10 * 18 = 180 +# 如果实际连接数远超 180,说明连接未释放 + +# 4. 查找连接泄漏 +psql $DATABASE_URL -c " +SELECT pid, state, wait_event, query_start, state_change, query +FROM pg_stat_activity +WHERE datname = 'ai_clinical' + AND state_change < now() - interval '5 minutes' +ORDER BY state_change; +" + +# 5. 紧急处理:杀死长时间空闲连接 +psql $DATABASE_URL -c " +SELECT pg_terminate_backend(pid) +FROM pg_stat_activity +WHERE datname = 'ai_clinical' + AND state = 'idle' + AND state_change < now() - interval '10 minutes'; +" +``` + +**解决方法**: + +```bash +# 方法 1:调整连接池配置(临时) +# 在 DATABASE_URL 中降低 connection_limit +DATABASE_URL=postgresql://...?connection_limit=10&pool_timeout=10 + +# 方法 2:限制 SAE 实例数(临时) +# SAE 控制台 → 应用配置 → 弹性伸缩 +# 将最大实例数从 20 降低到 10 + +# 方法 3:升级 RDS 规格(长期) +# RDS 控制台 → 变更配置 → 升级到更大规格(增加最大连接数) + +# 方法 4:修复代码(如果是连接泄漏) +# 检查代码中是否有未释放的连接: +# - 未使用 Prisma 的事务 API +# - 手动创建的数据库连接未关闭 +# - 长时间运行的查询 +``` + +### 问题 3:无法连接 Python 微服务 + +**症状**: + +``` +[ERROR] Failed to connect to Python service: ECONNREFUSED +或 +[ERROR] connect ETIMEDOUT 172.16.0.10:8000 +``` + +**排查步骤**: + +```bash +# 1. 确认 Python 服务是否运行 +# SAE 控制台 → 应用列表 → 查看 extraction-service 状态 + +# 2. 确认内网地址是否正确 +# SAE 控制台 → extraction-service 应用 → 应用访问配置 +# 复制"VPC 内网访问地址",更新后端环境变量 + +# 3. 测试内网连通性 +# 在后端应用的 Webshell 中执行: +curl -v http://172.16.0.10:8000/health + +# 4. 检查安全组规则 +# SAE 控制台 → extraction-service 应用 → 网络配置 +# 确认入站规则允许 VPC 内访问 8000 端口 +``` + +**解决方法**: + +```bash +# 如果内网地址错误,更新环境变量: +# SAE 控制台 → backend-service 应用 → 应用配置 → 环境变量 +# 修改或添加: +EXTRACTION_SERVICE_URL=http://<正确的内网IP>:8000 + +# 重启应用使环境变量生效 +# SAE 控制台 → backend-service 应用 → 重启 +``` + +### 问题 4:无法连接 Dify 服务 + +**症状**: + +``` +[ERROR] Dify API error: ECONNREFUSED +或 +[ERROR] Dify API error: 502 Bad Gateway +``` + +**排查步骤**: + +```bash +# 1. 确认 Dify 服务是否运行 +# ECS 控制台 → 实例列表 → 找到 Dify 实例 → 远程连接 + +# 在 ECS 中执行: +docker ps | grep dify +# 应该看到 dify-api, dify-worker, dify-web, redis, weaviate 等容器运行中 + +# 2. 测试 Dify API +curl http://localhost/v1/info + +# 3. 从 SAE 测试连通性 +# 在后端应用的 Webshell 中执行: +curl -v http://172.16.0.20:80/v1/info +``` + +**解决方法**: + +```bash +# 如果 Dify 未启动,在 ECS 中重启: +cd /path/to/dify +docker-compose up -d + +# 如果 API Key 错误,更新环境变量: +# SAE 控制台 → backend-service 应用 → 环境变量 +DIFY_API_KEY=app-<正确的Key> + +# 如果内网地址错误,更新环境变量: +DIFY_API_URL=http://:80/v1 +``` + +### 问题 5:OSS 上传失败 + +**症状**: + +``` +[ERROR] OSS upload failed: AccessDenied +或 +[ERROR] OSS upload failed: InvalidAccessKeyId +``` + +**排查步骤**: + +```bash +# 1. 检查环境变量 +# SAE 控制台 → 应用详情 → 环境变量 +# 确认以下变量正确: +OSS_REGION=oss-cn-hangzhou +OSS_BUCKET=clinical-research-files +OSS_ACCESS_KEY_ID=LTAI5t... +OSS_ACCESS_KEY_SECRET=xxx... + +# 2. 测试 OSS 访问 +# 在后端应用的 Webshell 中执行: +node -e " +const OSS = require('ali-oss'); +const client = new OSS({ + region: process.env.OSS_REGION, + accessKeyId: process.env.OSS_ACCESS_KEY_ID, + accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, + bucket: process.env.OSS_BUCKET +}); +client.list().then(result => console.log('✅ OSS connection successful')).catch(err => console.error('❌ OSS error:', err.message)); +" +``` + +**解决方法**: + +```bash +# 如果 AccessKey 错误,重新生成: +# 阿里云控制台 → AccessKey 管理 → 创建 AccessKey +# 更新 SAE 环境变量 + +# 如果 Bucket 权限错误: +# OSS 控制台 → Bucket 列表 → clinical-research-files → 访问控制 +# 确认 Bucket 为"私有",且 RAM 用户有读写权限 +``` + +### 问题 6:内存泄漏 + +**症状**: + +``` +SAE 监控显示:内存使用率持续上涨,最终导致 OOM(Out of Memory) +日志:JavaScript heap out of memory +``` + +**排查步骤**: + +```bash +# 1. 查看内存趋势 +# SAE 控制台 → 应用详情 → 监控 → 选择"最近 24 小时" +# 如果内存曲线持续上涨,不下降,说明有内存泄漏 + +# 2. 查看实例内存详情 +# 在 Webshell 中执行: +node -e "console.log(process.memoryUsage())" + +# 输出示例: +# { +# rss: 123456789, // 总内存(字节) +# heapTotal: 45678910, // V8 堆总大小 +# heapUsed: 34567890, // V8 堆已使用 +# external: 1234567, // 外部内存(Buffer等) +# arrayBuffers: 123456 // ArrayBuffer +# } + +# 3. 启用内存快照(本地调试) +# 在本地开发环境添加 --inspect 参数: +node --inspect dist/index.js +# 使用 Chrome DevTools 查看内存快照 +``` + +**常见内存泄漏原因**: + +1. **全局变量累积**(如缓存无限增长) +2. **事件监听器未移除** +3. **定时器未清理** +4. **闭包持有大对象** +5. **Prisma 查询结果未释放** + +**解决方法**: + +```typescript +// 错误示例:全局缓存无限增长 +const cache = {}; // ❌ 永不清理 + +// 正确示例:使用 LRU 缓存 +import LRUCache from 'lru-cache'; +const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 10 }); // ✅ 最多 1000 项,10 分钟过期 + +// 错误示例:事件监听器泄漏 +emitter.on('event', handler); // ❌ 从不移除 + +// 正确示例:及时移除监听器 +emitter.once('event', handler); // ✅ 自动移除 +// 或 +emitter.on('event', handler); +// 使用后: +emitter.off('event', handler); // ✅ 手动移除 + +// 错误示例:定时器泄漏 +setInterval(() => { ... }, 1000); // ❌ 永不清理 + +// 正确示例:清理定时器 +const timer = setInterval(() => { ... }, 1000); +process.on('SIGTERM', () => clearInterval(timer)); // ✅ 优雅关闭时清理 +``` + +### 问题 7:Prisma Schema 与数据库不一致 + +**症状**: + +```typescript +// 代码中访问字段: +const user = await prisma.user.findUnique({ where: { id: '123' } }); +console.log(user.phone); // ❌ TypeScript 报错:Property 'phone' does not exist + +// 或运行时报错: +PrismaClientKnownRequestError: column "phone" does not exist +``` + +**原因**: + +- 开发过程中直接修改了数据库(加了 `phone` 字段) +- 但没有更新 `schema.prisma` +- Prisma Client 基于旧的 Schema 生成,不知道新字段 + +**解决方法**: + +```bash +# 1. 在本地开发环境,连接到 RDS +export DATABASE_URL="postgresql://username:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical" + +# 2. 反向同步 Schema +npx prisma db pull + +# 3. 重新生成 Client +npx prisma generate + +# 4. 提交代码 +git add prisma/schema.prisma +git commit -m "fix: sync Prisma schema with database" +git push + +# 5. 重新构建镜像并部署 +cp -r prisma backend/prisma +cd backend +docker build -t backend-service:v1.0.1 . +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.1 + +# 6. 在 SAE 中更新镜像版本 +# SAE 控制台 → 应用详情 → 部署 → 选择新版本 v1.0.1 +``` + +--- + +## 13. 注意事项与禁忌 + +### ✅ 最佳实践 + +#### 1. **环境变量管理** + +```bash +# ✅ 正确做法:使用 SAE 环境变量 +# SAE 控制台 → 应用配置 → 环境变量 + +# ❌ 错误做法:在代码或 Dockerfile 中硬编码 +# Dockerfile +ENV DATABASE_URL="postgresql://user:pass@host/db" # ❌ 泄露敏感信息 +``` + +#### 2. **连接池配置** + +```bash +# ✅ 正确做法:根据 RDS 和 SAE 配置动态计算 +DATABASE_URL=postgresql://...?connection_limit=18&pool_timeout=10 + +# 计算公式: +# connection_limit = (RDS_MAX_CONNECTIONS / MAX_INSTANCES) - 预留 +# 示例:(400 / 20) - 2 = 18 + +# ❌ 错误做法:使用默认值 +DATABASE_URL=postgresql://... # ❌ 默认无限制,导致连接耗尽 +``` + +#### 3. **文件存储** + +```typescript +// ✅ 正确做法:使用 OSS 存储 +import { StorageFactory } from './common/storage'; +const storage = StorageFactory.create(); // 根据 STORAGE_TYPE 自动选择 +await storage.upload('uploads/file.pdf', buffer); + +// ❌ 错误做法:存储到容器文件系统 +import fs from 'fs'; +fs.writeFileSync('/app/uploads/file.pdf', buffer); // ❌ 容器重启后丢失 +``` + +#### 4. **日志输出** + +```typescript +// ✅ 正确做法:使用 stdout(SAE 自动收集) +console.log('[INFO] User logged in:', userId); +logger.info('User logged in', { userId }); + +// ❌ 错误做法:写入文件 +import fs from 'fs'; +fs.appendFileSync('/var/log/app.log', message); // ❌ 容器重启后丢失 +``` + +#### 5. **优雅关闭** + +```typescript +// ✅ 正确做法:监听 SIGTERM 信号 +process.on('SIGTERM', async () => { + console.log('[Server] Received SIGTERM, shutting down gracefully...'); + await fastify.close(); // 关闭 HTTP 服务器 + await prisma.$disconnect(); // 关闭数据库连接 + process.exit(0); +}); + +// ❌ 错误做法:直接退出 +process.on('SIGTERM', () => process.exit(0)); // ❌ 连接未释放 +``` + +### ❌ 绝对禁止 + +#### 1. **禁止直接修改数据库而不同步 Prisma Schema(致命)** + +```bash +# ❌ 错误做法: +# 1. 在 Navicat 中直接添加字段 +ALTER TABLE "User" ADD COLUMN phone VARCHAR(50); + +# 2. 然后直接在代码中使用 +const user = await prisma.user.create({ + data: { + email: 'test@example.com', + phone: '13800138000' // ❌ TypeScript 报错:phone 不存在 + } +}); + +# ✅ 正确做法: +# 1. 先修改 schema.prisma +model User { + email String + phone String? @db.VarChar(50) // 添加字段定义 +} + +# 2. 生成迁移(仅开发环境) +npx prisma migrate dev --name add_user_phone + +# 3. 生成客户端 +npx prisma generate + +# 4. 在代码中使用 +const user = await prisma.user.create({ + data: { + email: 'test@example.com', + phone: '13800138000' // ✅ TypeScript 识别 + } +}); + +# 或者,如果数据库已手动修改: +# 1. 反向同步 +npx prisma db pull + +# 2. 生成客户端 +npx prisma generate +``` + +**原因**: +- Prisma Client 基于 Schema 生成 +- Schema 与数据库不一致会导致运行时错误或类型不匹配 +- 这是导致生产环境崩溃的最常见原因之一 + +#### 2. **禁止在启动命令中无脑执行 `prisma migrate deploy`** + +```bash +# ❌ 错误做法(如果数据库是通过 pg_dump 导入的): +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"] + +# 错误原因: +# - 表结构已存在(通过 pg_dump 导入) +# - migrate deploy 会尝试重新创建表 +# - 导致错误:Table already exists + +# ✅ 正确做法(针对您的情况): +CMD ["node", "dist/index.js"] + +# 解释: +# - 数据库结构已通过 pg_dump 导入 +# - Schema 已通过 prisma db pull 同步 +# - 无需在启动时执行迁移 +``` + +#### 3. **禁止忽略 Prisma 反向同步步骤** + +```bash +# ❌ 错误流程: +pg_dump → 导入 RDS → 直接构建镜像 → 部署 +# ❌ 问题:Schema 与数据库不一致 + +# ✅ 正确流程: +pg_dump → 导入 RDS → prisma db pull(同步)→ 构建镜像 → 部署 +# ✅ 关键:prisma db pull 确保一致性 +``` + +#### 4. **禁止在代码中硬编码敏感信息** + +```typescript +// ❌ 错误示例 +const dbUrl = 'postgresql://admin:P@ssw0rd@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical'; + +// ✅ 正确做法 +const dbUrl = process.env.DATABASE_URL; +``` + +#### 5. **禁止在 backend/ 目录构建前不复制 Prisma 文件** + +```bash +# ❌ 错误做法: +cd backend +docker build -t backend-service:v1.0.0 . +# ❌ 错误:COPY prisma ./prisma 会失败(找不到文件) + +# ✅ 正确做法: +cd AIclinicalresearch # 项目根目录 +cp -r prisma backend/prisma # 先复制 +cd backend +docker build -t backend-service:v1.0.0 . # 再构建 +``` + +#### 6. **禁止使用内存作为缓存(多实例环境)** + +```typescript +// ❌ 错误示例:内存缓存不共享 +const cache = new Map(); // 实例 1 的缓存,实例 2 看不到 + +// ✅ 正确做法:使用 PostgreSQL 缓存(Postgres-Only) +import { CacheFactory } from './common/cache'; +const cache = CacheFactory.create(); // 根据 CACHE_TYPE 选择 +``` + +#### 7. **禁止使用本地文件作为队列** + +```typescript +// ❌ 错误示例:文件队列不共享 +import fs from 'fs'; +fs.writeFileSync('/tmp/queue.json', JSON.stringify(tasks)); // 实例间不同步 + +// ✅ 正确做法:使用 pg-boss(Postgres-Only) +import { jobQueue } from './common/jobs'; +await jobQueue.send('pdf-extraction', { fileId: '123' }); +``` + +#### 8. **禁止使用同步阻塞操作** + +```typescript +// ❌ 错误示例:阻塞事件循环 +import fs from 'fs'; +const data = fs.readFileSync('/large-file.pdf'); // 阻塞其他请求 + +// ✅ 正确做法:使用异步 API +const data = await fs.promises.readFile('/large-file.pdf'); +``` + +#### 9. **禁止跳过健康检查** + +```typescript +// ❌ 错误示例:健康检查总是返回 200 +app.get('/health', async (req, reply) => { + return { status: 'ok' }; // 即使数据库断开也返回 ok +}); + +// ✅ 正确做法:真正检查依赖服务 +app.get('/health', async (req, reply) => { + const dbHealthy = await testDatabaseConnection(); + if (!dbHealthy) { + reply.code(503); + return { status: 'unhealthy', database: 'disconnected' }; + } + return { status: 'healthy' }; +}); +``` + +#### 10. **禁止忽略错误处理** + +```typescript +// ❌ 错误示例:吞掉错误 +try { + await riskyOperation(); +} catch (error) { + // 什么都不做 +} + +// ✅ 正确做法:记录错误并返回适当的响应 +try { + await riskyOperation(); +} catch (error) { + logger.error('Risky operation failed', { error, stack: error.stack }); + reply.code(500).send({ error: 'Internal server error' }); +} +``` + +#### 11. **禁止在生产环境使用 development 模式** + +```bash +# ❌ 错误配置 +NODE_ENV=development # 会打印大量调试日志,暴露敏感信息 + +# ✅ 正确配置 +NODE_ENV=production +LOG_LEVEL=info +``` + +#### 12. **禁止使用弱 JWT 密钥** + +```bash +# ❌ 错误配置 +JWT_SECRET=secret # 太弱,容易被破解 + +# ✅ 正确配置(至少 32 位随机字符串) +JWT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 # 使用 openssl rand -base64 32 生成 +``` + +#### 13. **禁止直接暴露 Prisma 错误到前端** + +```typescript +// ❌ 错误示例:泄露数据库结构 +try { + await prisma.user.create({ data: { email: 'test@example.com' } }); +} catch (error) { + reply.code(500).send({ error: error.message }); // 可能暴露表名、字段名 +} + +// ✅ 正确做法:返回通用错误 +try { + await prisma.user.create({ data: { email: 'test@example.com' } }); +} catch (error) { + logger.error('Failed to create user', { error }); + reply.code(500).send({ error: 'Failed to create user' }); +} +``` + +#### 14. **禁止忽略 Docker 镜像优化** + +```dockerfile +# ❌ 错误示例:单阶段构建,镜像臃肿 +FROM node:22 +COPY . . +RUN npm install # 包含 devDependencies +CMD ["node", "dist/index.js"] + +# ✅ 正确做法:多阶段构建,减小镜像体积 +FROM node:22-alpine AS builder +# ... 构建阶段 ... +FROM node:22-alpine +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules # 仅生产依赖 +``` + +--- + +## 📚 相关文档 + +- [03-Dify-ECS部署完全指南.md](./03-Dify-ECS部署完全指南.md) +- [04-Python微服务-SAE容器部署指南.md](./04-Python微服务-SAE容器部署指南.md) +- [06-前端Nginx-SAE容器部署指南.md](./06-前端Nginx-SAE容器部署指南.md)(待创建) + +--- + +## 🆘 获取帮助 + +**遇到问题?** + +1. 查看本文档的"故障排查"章节 +2. 查看 SAE 控制台的实时日志 +3. 查看 RDS 控制台的性能监控 +4. 联系团队技术支持 + +--- + +**部署愉快!🚀** + diff --git a/docs/05-部署文档/06-前端Nginx-SAE容器部署指南.md b/docs/05-部署文档/06-前端Nginx-SAE容器部署指南.md new file mode 100644 index 00000000..a4515202 --- /dev/null +++ b/docs/05-部署文档/06-前端Nginx-SAE容器部署指南.md @@ -0,0 +1,2063 @@ +# 前端 Nginx - SAE 容器部署完全指南 + +**文档版本**: v1.2 (修正:Node 版本、Vite 版本、日志输出、时区) +**创建时间**: 2025-12-13 +**最后修订**: 2025-12-13 +**适用范围**: AIclinicalresearch 平台 - 前端静态资源服务(frontend-v2) +**目标读者**: 运维工程师、前端开发工程师 +**部署目标**: 阿里云 SAE(Serverless 应用引擎)Nginx 容器部署 + +**v1.2 更新日志**: +- 🔴 修正:Node 版本从 18 改为 22(与开发环境 v22.18.0 一致) +- ✅ 确认:Vite 版本 7.2(package.json 真实版本) +- ⚠️ 修正:日志输出到 stdout/stderr(SAE 最佳实践) +- ⚠️ 新增:容器时区设置(Asia/Shanghai) +- ⚠️ 强化:后端地址必须显式配置(不给危险默认值) + +**v1.1 更新日志**: +- 🛑 修正:基于 **frontend-v2** 目录(不是 frontend) +- ✅ 更新:React 19 + Vite 6 + Ant Design 6.0 +- ✅ 新增:Ant Design X 2.1 说明(AI 对话组件) +- ✅ 新增:模块化架构说明(framework + modules) +- ✅ 完善:构建路径和目录结构 + +--- + +## 📋 目录 + +1. [为什么选择 SAE Nginx 容器部署](#1-为什么选择-sae-nginx-容器部署) +2. [部署前准备](#2-部署前准备) +3. [前端服务分析](#3-前端服务分析) +4. [创建 Nginx 配置](#4-创建-nginx-配置) +5. [构建 Docker 镜像](#5-构建-docker-镜像) +6. [本地测试验证](#6-本地测试验证) +7. [推送到 ACR](#7-推送到-acr) +8. [SAE 应用配置](#8-sae-应用配置) +9. [端到端测试](#9-端到端测试) +10. [监控与维护](#10-监控与维护) +11. [故障排查](#11-故障排查) +12. [注意事项与禁忌](#12-注意事项与禁忌) + +--- + +## 1. 为什么选择 SAE Nginx 容器部署 + +### ✅ 核心优势 + +| 优势 | 说明 | 对您的价值 | +|------|------|----------| +| **极高性能** | Nginx 处理静态文件效率远高于 Node.js(serve 或 next start) | 首屏加载快,用户体验好 | +| **SPA 路由支持** | React Router 刷新页面不会 404 | `try_files` 原生支持 | +| **反向代理** | Nginx 将 `/api` 转发到后端,前端无感知 | 零 CORS 配置,简化开发 | +| **缓存控制** | 精细控制静态资源缓存策略 | 减少带宽消耗,加速加载 | +| **私有化就绪** | Docker 镜像可直接交付给医院 | 满足医疗数据合规要求 | +| **统一架构** | 与后端、Python 微服务统一使用 Docker | 一套部署流程,降低运维成本 | + +### 🎯 为什么不用 OSS + CDN? + +根据之前的讨论,我们明确了医疗科研项目的特殊性: + +| 对比项 | OSS + CDN | SAE Nginx 容器 | 评分 | +|-------|-----------|----------------|------| +| **私有化部署** | ❌ 无法在医院内网部署 OSS | ✅ Docker 镜像可直接交付 | **容器胜** | +| **架构统一性** | ❌ 前端 OSS,后端 SAE,割裂 | ✅ 全部 SAE 容器,统一管理 | **容器胜** | +| **CORS 处理** | ⚠️ 前端需要处理跨域 | ✅ Nginx 反向代理,前端无感 | **容器胜** | +| **成本(小规模)** | ✅ 存储费用极低 | ⚠️ 需要运行实例(~100元/月) | **OSS 胜** | +| **全球加速** | ✅ CDN 节点覆盖全球 | ❌ 单地域部署 | **OSS 胜** | +| **医疗场景适配** | ❌ 不适合内网环境 | ✅ 完美适配私有化 | **容器胜** | + +**结论**:对于需要私有化部署的医疗科研产品,SAE Nginx 容器是唯一选择。 + +--- + +## 2. 部署前准备 + +### ✅ 前置条件检查清单 + +#### 本地开发环境 + +- [ ] Docker Desktop 已安装并运行(版本 20.10+) +- [ ] Node.js 已安装(版本 18+) +- [ ] 前端代码已拉取到本地 +- [ ] 前端在本地能正常启动(`npm run dev`) +- [ ] 前端能成功构建(`npm run build`) + +验证命令: + +```bash +# 检查 Docker +docker --version +# 输出示例:Docker version 24.0.6 + +# 检查 Node.js +node --version +# 输出示例:v18.17.0 + +# 检查 npm +npm --version +# 输出示例:9.6.7 + +# 测试构建 +cd frontend +npm run build +# 应该成功生成 dist/ 目录 +``` + +#### 阿里云资源 + +- [ ] **后端服务(SAE)** 已部署并运行 + - 后端 VPC 内网地址已获取(如 `http://172.16.0.30:3001`) + - 后端健康检查可访问 + +- [ ] **阿里云容器镜像服务 ACR** 已开通 + - 命名空间已创建(如 `clinical-research`) + - 登录密码已设置 + +- [ ] **SAE 应用** 准备创建 + - VPC 和交换机已选择(与后端在同一 VPC) + +#### 配置信息准备 + +```bash +# 后端服务内网地址(关键) +BACKEND_SERVICE_URL=http://172.16.0.30:3001 + +# 如果需要配置环境变量(可选) +# VITE_API_BASE_URL 在构建时注入(很少使用) +``` + +--- + +## 3. 前端服务分析 + +### 📦 技术栈 + +| 技术 | 版本 | 用途 | +|------|------|------| +| **React** | 19.2+ | UI 框架(最新版本) | +| **Vite** | 7.2+ | 构建工具(极快,最新版本)| +| **TypeScript** | 5.9+ | 类型安全 | +| **Ant Design** | 6.0+ | UI 组件库(最新版本) | +| **Ant Design X** | 2.1+ | AI 对话组件(新增) | +| **React Router** | 7.9+ | 路由管理(SPA) | +| **React Query** | 5.90+ | 数据获取和缓存 | +| **Axios** | 1.13+ | HTTP 客户端 | +| **AG Grid** | 34.3+ | 数据表格(DC 模块) | +| **TailwindCSS** | 3.4+ | CSS 框架 | +| **Zustand** | 5.0+ | 状态管理 | + +**⚠️ 版本说明**: +- **Node.js**: v22.18.0(开发环境真实版本) +- **Vite**: 7.2.2(package.json 真实版本,最新稳定版) +- 所有版本均来自 `frontend-v2/package.json` 真实代码 + +### 📊 架构设计(模块化) + +``` +frontend-v2/ +├── src/ +│ ├── framework/ # 框架层 +│ │ ├── layout/ # 主布局 + 顶部导航 +│ │ ├── router/ # 路由守卫 +│ │ ├── permission/ # 权限管理 +│ │ └── modules/ # 模块注册表 +│ │ +│ ├── modules/ # 业务模块层 +│ │ ├── aia/ # AI 智能问答 +│ │ ├── pkb/ # 个人知识库 +│ │ ├── asl/ # AI 智能文献 +│ │ ├── dc/ # 数据清洗整理 +│ │ ├── ssa/ # 智能统计分析 +│ │ ├── st/ # 统计分析工具 +│ │ └── rvw/ # 稿件审查系统 +│ │ +│ └── shared/ # 共享能力层 +│ ├── components/ # 共享组件(Chat 组件等) +│ ├── api/ # 共享 API 工具 +│ ├── hooks/ # 共享 Hooks +│ └── utils/ # 工具函数 +``` + +### 📊 API 调用方式(模块化设计) + +**每个模块有独立的 API 文件**: + +```typescript +// modules/asl/api/index.ts +const API_BASE_URL = '/api/v1/asl'; + +async function request(url: string, options?: RequestInit): Promise { + const response = await fetch(`${API_BASE_URL}${url}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + return response.json(); +} + +// modules/dc/api/toolC.ts +const BASE_URL = '/api/v1/dc/tool-c'; + +// 使用 axios +axios.post(`${BASE_URL}/upload`, formData); +``` + +**关键设计**: +- ✅ 每个模块独立管理自己的 API(模块化) +- ✅ 使用相对路径 `/api/v1/...`(不硬编码后端地址) +- ✅ 混合使用 fetch 和 axios(根据模块需求) +- ✅ 生产环境由 Nginx 反向代理到后端 +- ✅ 无需处理 CORS(同源请求) + +**示例请求流程**: +``` +ASL 模块:GET /api/v1/asl/projects + ↓ +Nginx 反向代理 + ↓ +后端服务:http://172.16.0.30:3001/api/v1/asl/projects +``` + +### 📝 构建流程 + +```bash +# ⚠️ 注意:使用 frontend-v2 目录(不是 frontend) +cd frontend-v2 + +# 1. 安装依赖 +npm install + +# 2. 构建生产版本 +npm run build +# 实际执行:tsc -b && vite build + +# 3. 产物目录结构 +dist/ + ├── index.html # 入口 HTML(约 1KB) + ├── assets/ + │ ├── index-xxxxx.js # 主 JS 文件(约 800KB,已压缩) + │ ├── index-xxxxx.css # 主 CSS 文件(约 100KB,含 Ant Design + TailwindCSS) + │ ├── vendor-xxxxx.js # 第三方库(React、Ant Design 等) + │ └── *.svg/png/... # 其他资源 + └── vite.svg # Favicon + +# 预期构建时间:30-60 秒(取决于机器性能) +# 产物总大小:约 2-3MB(压缩后) +``` + +**构建产物分析**: +| 文件类型 | 大小(压缩前) | 大小(压缩后) | 说明 | +|---------|--------------|--------------|------| +| index.html | 1KB | 1KB | 入口 HTML | +| index-xxxxx.js | ~2.5MB | ~800KB | 业务代码 | +| vendor-xxxxx.js | ~3MB | ~1MB | 第三方库 | +| index-xxxxx.css | ~300KB | ~100KB | 样式文件 | +| 其他资源 | ~500KB | ~200KB | 图标、字体等 | +| **总计** | **~6.3MB** | **~2.1MB** | Gzip 压缩后 | + +### 🔍 当前 API 配置分析 + +**frontend-v2 的 API 配置方式**: + +```typescript +// modules/asl/api/index.ts +const API_BASE_URL = '/api/v1/asl'; // ✅ 相对路径(推荐) + +// modules/dc/api/toolC.ts +const BASE_URL = '/api/v1/dc/tool-c'; // ✅ 相对路径(推荐) + +// modules/dc/api/toolB.ts +const BASE_URL = '/api/v1/dc/tool-b'; // ✅ 相对路径(推荐) +``` + +**开发环境代理配置**: + +```typescript +// vite.config.ts +export default defineConfig({ + server: { + host: '0.0.0.0', + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + }, + }, +}); +``` + +**生产环境配置策略**: +- ✅ **不需要**设置任何环境变量(如 `VITE_API_BASE_URL`) +- ✅ 所有模块都使用相对路径 `/api/v1/...` +- ✅ 使用 Nginx 反向代理统一处理 +- ✅ 开发环境通过 Vite proxy 代理到后端 +- ✅ 生产环境通过 Nginx 代理到后端 + +**完美设计**: +- ✅ 无需在构建时注入环境变量 +- ✅ 同一套代码适用于开发和生产 +- ✅ 支持私有化部署(不依赖外部配置) + +--- + +## 4. 创建 Nginx 配置 + +### 📝 创建 nginx.conf + +**⚠️ 重要:在 `frontend-v2/` 目录下创建 `nginx.conf`(不是 `frontend/`)** + +```nginx +# Nginx 配置文件 - AI临床研究平台前端服务 +# 用途:托管 React SPA + 反向代理后端 API + +user nginx; +worker_processes auto; # 自动根据 CPU 核心数调整 + +# ⚠️ 日志输出到 stderr(SAE 会自动收集) +error_log /dev/stderr warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; # 每个 worker 进程的最大连接数 + use epoll; # Linux 下使用 epoll(高性能) +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 日志格式 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # ⚠️ 日志输出到 stdout(SAE 会自动收集,避免磁盘写满) + access_log /dev/stdout main; + + # 性能优化 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip 压缩(减少传输大小) + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; + gzip_disable "msie6"; + + # 上游后端服务(Backend Service) + upstream backend { + # ⚠️ 重要:这里的地址在部署时需要替换为真实的后端内网地址 + # SAE 部署时,通过环境变量注入,详见 Dockerfile + server ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT} fail_timeout=30s max_fails=3; + + # 如果有多个后端实例(负载均衡) + # server 172.16.0.30:3001 weight=1; + # server 172.16.0.31:3001 weight=1; + + keepalive 32; # 保持连接池 + } + + server { + listen 80; + server_name _; # 接受所有域名 + + # 根目录(React 构建产物) + root /usr/share/nginx/html; + index index.html; + + # 字符集 + charset utf-8; + + # ==================== 静态资源处理 ==================== + + # 主页面(index.html)- 不缓存 + location = / { + try_files /index.html =404; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # JS/CSS 文件 - 强缓存(文件名带 hash) + location ~* \.(js|css)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # 图片/字体文件 - 强缓存 + location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # ==================== API 反向代理 ==================== + + # 后端 API 代理(关键配置) + location /api/ { + # 代理到后端服务 + proxy_pass http://backend; + + # 保留原始请求信息 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时配置(AI 对话、文件处理可能耗时较长) + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + # 缓冲配置 + proxy_buffering off; # 关闭缓冲(实时流式响应) + proxy_request_buffering off; # 支持大文件上传 + + # WebSocket 支持(如果后续需要) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 错误处理 + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; + proxy_next_upstream_tries 2; + } + + # ==================== SPA 路由支持 ==================== + + # React Router 路由回退 + # 所有非文件请求都返回 index.html(SPA 的核心) + location / { + try_files $uri $uri/ /index.html; + + # 禁用缓存(用户刷新时总是获取最新页面) + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # ==================== 安全加固 ==================== + + # 隐藏 Nginx 版本号 + server_tokens off; + + # 禁止访问隐藏文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # 禁止访问特定文件 + location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)$ { + deny all; + } + + # ==================== 健康检查 ==================== + + # SAE 健康检查端点 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Nginx 状态页(用于监控) + location /nginx_status { + stub_status on; + access_log off; + # 仅允许内网访问 + allow 10.0.0.0/8; + allow 172.16.0.0/12; + allow 192.168.0.0/16; + deny all; + } + + # ==================== 错误页面 ==================== + + error_page 404 /index.html; # SPA 路由回退 + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } + } +} +``` + +### 🔍 配置关键点解析 + +#### 1. 后端地址动态注入 + +```nginx +upstream backend { + server ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}; +} +``` + +**问题**:Nginx 配置文件不支持环境变量。 + +**解决方案**:在 Docker 启动时使用 `envsubst` 替换占位符(详见第 5 节 Dockerfile)。 + +#### 2. SPA 路由支持 + +```nginx +location / { + try_files $uri $uri/ /index.html; +} +``` + +**原理**: +- 用户访问 `/dashboard` → Nginx 找不到 `dashboard` 文件 → 返回 `index.html` +- React Router 接管路由 → 渲染 Dashboard 组件 + +**如果没有这个配置**: +- 用户刷新 `/dashboard` → Nginx 报 404 错误 ❌ + +#### 3. 反向代理配置 + +```nginx +location /api/ { + proxy_pass http://backend; +} +``` + +**请求流程**: +``` +浏览器:GET /api/v1/projects + ↓ +Nginx:接收请求 + ↓ +Nginx:proxy_pass http://backend + ↓ +后端服务:http://172.16.0.30:3001/api/v1/projects + ↓ +后端返回数据 + ↓ +Nginx 转发给浏览器 +``` + +**优势**: +- ✅ 前端和后端同源(都是 `https://your-domain.com`) +- ✅ 无需处理 CORS +- ✅ 前端代码无需知道后端真实地址 + +#### 4. 缓存策略 + +| 资源类型 | 缓存策略 | 原因 | +|---------|---------|------| +| `index.html` | `no-cache` | 确保用户总能获取最新版本 | +| `.js` / `.css` | `1y` + `immutable` | 文件名带 hash,内容不变 | +| 图片/字体 | `1y` + `immutable` | 静态资源,不会改变 | + +**为什么 index.html 不缓存?** + +``` +用户首次访问: + ↓ +下载 index.html(引用 index-abc123.js) + ↓ +部署新版本:index.html 更新(引用 index-xyz789.js) + ↓ +用户再次访问: + - 如果 index.html 被缓存 → 仍加载 index-abc123.js(旧版本) ❌ + - 如果 index.html 不缓存 → 加载 index-xyz789.js(新版本) ✅ +``` + +--- + +## 5. 构建 Docker 镜像 + +### 📝 创建 Dockerfile + +**⚠️ 重要:在 `frontend-v2/` 目录下创建 `Dockerfile`(不是 `frontend/`)** + +```dockerfile +# ==================== 阶段 1: 构建阶段 ==================== +# ⚠️ 使用 Node 22(与开发环境一致,避免 package-lock.json 版本冲突) +FROM node:22-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 1. 复制依赖文件 +COPY package*.json ./ + +# 2. 安装依赖 +# 使用国内镜像加速(可选) +# RUN npm config set registry https://registry.npmmirror.com +RUN npm ci --only=production=false + +# 3. 复制源代码 +COPY . . + +# 4. 构建生产版本 +# ⚠️ 注意:如果需要在构建时注入环境变量,在这里设置 ARG +# ARG VITE_API_BASE_URL +# ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +RUN npm run build + +# 验证构建产物 +RUN ls -la /app/dist/ + +# ==================== 阶段 2: 运行阶段 ==================== +FROM nginx:1.25-alpine + +# 安装必要工具(包括时区数据) +RUN apk add --no-cache \ + bash \ + gettext \ + curl \ + tzdata + +# 设置容器时区为上海(否则日志时间会比北京时间慢 8 小时) +RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone + +# 创建 Nginx 配置目录 +RUN mkdir -p /etc/nginx/templates + +# 1. 复制 Nginx 配置模板(支持环境变量替换) +COPY nginx.conf /etc/nginx/templates/nginx.conf.template + +# 2. 复制构建产物到 Nginx 默认目录 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 3. 创建启动脚本(处理环境变量替换) +RUN cat > /docker-entrypoint.sh <<'EOF' +#!/bin/bash +set -e + +# ⚠️ 关键:不给默认值,强制在 SAE 控制台配置 +# 如果未配置,报错退出(避免使用错误的后端地址) +if [ -z "$BACKEND_SERVICE_HOST" ]; then + echo "❌ ERROR: BACKEND_SERVICE_HOST environment variable is required!" + echo "Please configure it in SAE console with backend internal IP (e.g., 172.16.0.30)" + exit 1 +fi + +if [ -z "$BACKEND_SERVICE_PORT" ]; then + echo "⚠️ WARNING: BACKEND_SERVICE_PORT not set, using default: 3001" + export BACKEND_SERVICE_PORT=3001 +fi + +echo "============================================" +echo "Starting Frontend Nginx Service" +echo "Backend Service: ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}" +echo "Container Timezone: $(cat /etc/timezone)" +echo "Current Time: $(date)" +echo "============================================" + +# 使用 envsubst 替换 Nginx 配置中的环境变量 +envsubst '${BACKEND_SERVICE_HOST} ${BACKEND_SERVICE_PORT}' \ + < /etc/nginx/templates/nginx.conf.template \ + > /etc/nginx/nginx.conf + +# 验证 Nginx 配置 +nginx -t + +# 启动 Nginx +exec nginx -g 'daemon off;' +EOF + +RUN chmod +x /docker-entrypoint.sh + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 + +# 暴露端口 +EXPOSE 80 + +# 启动命令 +ENTRYPOINT ["/docker-entrypoint.sh"] +``` + +### 📝 创建 .dockerignore + +**⚠️ 重要:在 `frontend-v2/` 目录下创建 `.dockerignore`(不是 `frontend/`)** + +``` +# Node.js +node_modules +npm-debug.log +yarn-error.log +.npm +.yarn + +# 开发文件 +.env +.env.* +*.local + +# 构建产物(Dockerfile 中会重新生成) +dist + +# 测试文件 +test +tests +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx +coverage +.nyc_output + +# 文档和临时文件 +docs +*.md +!README.md +.vscode +.idea +.DS_Store +Thumbs.db + +# Git +.git +.gitignore +.gitattributes + +# CI/CD +.github +.gitlab-ci.yml +.travis.yml + +# 日志 +*.log +logs + +# 临时文件 +temp +tmp +*.swp +*.swo +*~ + +# 编辑器配置 +.editorconfig +.prettierrc +.eslintrc* + +# TypeScript 配置(保留 tsconfig.json,其他忽略) +tsconfig.tsbuildinfo + +# Vite +.vite +vite.config.*.timestamp-* +``` + +### 📝 Dockerfile 关键设计解析 + +#### 1. 多阶段构建(减小镜像体积) + +```dockerfile +# 阶段 1:构建(Node.js 环境) +FROM node:22-alpine AS builder # ⚠️ 使用 Node 22(与开发环境一致) +# 体积:~200MB(临时) +RUN npm run build + +# 阶段 2:运行(Nginx 环境) +FROM nginx:1.25-alpine +# 体积:~25MB(最终) +COPY --from=builder /app/dist /usr/share/nginx/html +``` + +**效果对比**: +- ❌ 单阶段构建:~700MB(包含 Node.js + npm + 所有依赖) +- ✅ 多阶段构建:~40-60MB(仅 Nginx + 静态文件) + +**为什么使用 Node 22**: +- 开发环境使用 Node 22.18.0+ +- `package-lock.json` 由 Node 22 生成(Lockfile version 3) +- Node 18 可能导致依赖安装警告或失败 +- 保证构建环境与开发环境完全一致 + +#### 2. 环境变量替换机制 + +```bash +# 启动脚本 +envsubst '${BACKEND_SERVICE_HOST} ${BACKEND_SERVICE_PORT}' \ + < /etc/nginx/templates/nginx.conf.template \ + > /etc/nginx/nginx.conf +``` + +**流程**: +``` +nginx.conf.template(模板): + server ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}; + +↓ envsubst 替换 + +nginx.conf(最终配置): + server 172.16.0.30:3001; +``` + +#### 3. 健康检查 + +```dockerfile +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 +``` + +**SAE 会使用这个健康检查**: +- ✅ 容器启动后 30 秒开始检查 +- ✅ 每 30 秒检查一次 +- ✅ 连续失败 3 次 → 标记为不健康 → SAE 重启容器 + +--- + +## 6. 本地测试验证 + +### 步骤 1:构建镜像 + +```bash +# ⚠️ 重要:使用 frontend-v2 目录(不是 frontend) +cd frontend-v2 + +# 确认当前目录正确 +pwd +# 应该输出:.../AIclinicalresearch/frontend-v2 + +# 确认 package.json 存在 +cat package.json | grep '"name"' +# 应该输出:"name": "frontend-v2" + +# 构建镜像(需要 3-5 分钟) +docker build -t frontend-service:v1.0.0 . + +# 查看镜像大小 +docker images frontend-service:v1.0.0 +# 预期大小:~40-60MB(Alpine 基础镜像 + 构建产物) + +# 查看镜像详情 +docker inspect frontend-service:v1.0.0 | grep -A 5 "Size" +``` + +**如果构建失败**: + +```bash +# 常见问题 1:npm install 超时 +# 解决:使用国内镜像源 +# 在 Dockerfile 的 npm ci 之前添加: +RUN npm config set registry https://registry.npmmirror.com + +# 常见问题 2:构建产物为空 +# 解决:检查 package.json 中的 build 脚本 +cat package.json | grep '"build"' +# 应该输出: "build": "tsc -b && vite build", + +# 常见问题 3:TypeScript 编译错误 +# 解决:检查是否有类型错误 +npm run build # 在 Docker 外先测试构建 + +# 常见问题 4:nginx.conf 文件找不到 +# 解决:确保 nginx.conf 在 frontend-v2/ 目录下 +ls nginx.conf +``` + +### 步骤 2:本地运行测试 + +```bash +# 运行容器(需要指定后端地址) +docker run -d \ + --name frontend-test \ + -p 8080:80 \ + -e BACKEND_SERVICE_HOST=host.docker.internal \ + -e BACKEND_SERVICE_PORT=3001 \ + frontend-service:v1.0.0 + +# 查看启动日志 +docker logs frontend-test + +# 应该看到: +# ============================================ +# Starting Frontend Nginx Service +# Backend Service: host.docker.internal:3001 +# ============================================ +# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok +# nginx: configuration file /etc/nginx/nginx.conf test is successful +``` + +**注意**:`host.docker.internal` 是 Docker Desktop 特有的域名,指向宿主机。 + +### 步骤 3:测试静态资源访问 + +```bash +# 测试主页 +curl http://localhost:8080/ + +# 预期返回: +# +# +# ... +# + +# 测试健康检查 +curl http://localhost:8080/health + +# 预期返回: +# healthy +``` + +### 步骤 4:测试 SPA 路由 + +```bash +# 访问不存在的路由(应该返回 index.html,而不是 404) +curl http://localhost:8080/dashboard + +# 预期返回: +# +# +# ...(与主页相同) +# + +# 而不是: +# +# 404 Not Found +# ...(Nginx 默认 404 页面) +``` + +### 步骤 5:测试反向代理(关键测试) + +**前提**:确保本地后端服务在 `http://localhost:3001` 运行。 + +```bash +# 通过前端 Nginx 访问后端 API +curl http://localhost:8080/api/v1/health + +# 预期返回(后端的响应): +{ + "status": "healthy", + "timestamp": "2025-12-13T10:30:00.000Z", + "database": { + "status": "connected" + } +} + +# 查看 Nginx 日志,确认代理成功 +docker logs frontend-test 2>&1 | grep "/api/v1/health" +# 应该看到: +# 172.17.0.1 - - [13/Dec/2025:10:30:00 +0000] "GET /api/v1/health HTTP/1.1" 200 123 +``` + +### 步骤 6:浏览器测试 + +打开浏览器,访问 `http://localhost:8080` + +**测试清单**: +- [ ] 页面能正常加载(不是空白页) +- [ ] 样式正确显示(CSS 加载成功) +- [ ] JavaScript 功能正常(交互正常) +- [ ] 刷新页面不会 404(SPA 路由正常) +- [ ] 浏览器开发者工具 Network 标签: + - [ ] API 请求路径是 `/api/v1/...`(相对路径) + - [ ] API 请求状态是 200(代理成功) + - [ ] 没有 CORS 错误 + +### 步骤 7:清理测试容器 + +```bash +# 停止并删除测试容器 +docker stop frontend-test +docker rm frontend-test +``` + +--- + +## 7. 推送到 ACR + +### 步骤 1:登录 ACR + +```bash +# 登录(使用 ACR 密码,不是阿里云账号密码) +docker login --username=your-aliyun-account registry.cn-hangzhou.aliyuncs.com + +# 输入密码后看到: +# Login Succeeded +``` + +### 步骤 2:标记镜像 + +```bash +# 格式:registry地址/命名空间/仓库名:版本号 +docker tag frontend-service:v1.0.0 \ + registry.cn-hangzhou.aliyuncs.com/clinical-research/frontend-service:v1.0.0 + +# 同时打一个 latest 标签 +docker tag frontend-service:v1.0.0 \ + registry.cn-hangzhou.aliyuncs.com/clinical-research/frontend-service:latest +``` + +### 步骤 3:推送镜像 + +```bash +# 推送指定版本 +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/frontend-service:v1.0.0 + +# 推送 latest +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/frontend-service:latest + +# 推送过程需要 1-3 分钟(镜像很小) +``` + +### 步骤 4:验证推送成功 + +登录阿里云控制台 → 容器镜像服务 → 镜像仓库 → `frontend-service` + +- 应该看到版本:`v1.0.0` 和 `latest` +- 镜像大小:~30-50MB +- 推送时间:刚才的时间 + +--- + +## 8. SAE 应用配置 + +### 步骤 1:创建应用 + +**阿里云控制台** → **SAE** → **应用列表** → **创建应用** + +| 配置项 | 值 | 说明 | +|-------|-----|------| +| **应用名称** | `frontend-service` | 前端服务 | +| **命名空间** | 选择已创建的命名空间 | 与后端服务同一命名空间 | +| **VPC** | 选择后端所在 VPC | 必须与后端在同一 VPC | +| **交换机** | 选择可用区 | 建议多可用区 | +| **应用实例规格** | 0.5核1G | 前端Nginx占用资源很少 | +| **实例数量** | 2 | 最小 2 个实例(高可用) | + +**为什么只需要 0.5核1G?** + +| 服务类型 | 推荐规格 | 原因 | +|---------|---------|------| +| Node.js 后端 | 2核4G | 需要处理 AI 对话、数据库查询 | +| Python 微服务 | 2核4G | PDF 提取是 CPU 密集型 | +| **Nginx 前端** | **0.5核1G** | **仅提供静态文件,消耗极少** | + +### 步骤 2:配置镜像 + +| 配置项 | 值 | +|-------|-----| +| **镜像类型** | 容器镜像服务企业版实例 | +| **镜像仓库** | `registry.cn-hangzhou.aliyuncs.com/clinical-research/frontend-service` | +| **镜像版本** | `v1.0.0` | +| **镜像拉取策略** | 总是拉取镜像 | + +### 步骤 3:配置端口 + +| 配置项 | 值 | +|-------|-----| +| **容器端口** | `80` | +| **协议** | TCP | + +### 步骤 4:配置环境变量(关键步骤) + +**⚠️ 极其重要:必须配置后端服务的内网地址,否则容器启动失败** + +#### 获取后端内网地址 + +1. **登录 SAE 控制台** → **应用列表** → **backend-service** → **应用详情** + +2. **查看"应用访问配置"** → **VPC 内网访问地址** + + 复制地址,格式通常是: + ``` + 172.16.0.30:3001 + ``` + +#### 配置环境变量 + +```bash +# ⚠️ 必须配置(否则容器启动失败) +BACKEND_SERVICE_HOST=172.16.0.30 + +# 可选配置(默认 3001) +BACKEND_SERVICE_PORT=3001 +``` + +**⚠️ 重要说明**: +- `BACKEND_SERVICE_HOST` **必须配置**,否则容器启动时会报错退出 +- 不要使用主机名(如 `backend-service`),SAE 可能无法解析 +- 必须使用后端服务的**内网 IP 地址**(从 SAE 控制台获取) + +**为什么要拆分成 Host 和 Port?** + +```nginx +# nginx.conf 中的配置: +upstream backend { + server ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}; +} + +# 如果不拆分,写成: +server ${BACKEND_SERVICE_URL}; # ❌ 无法解析 http://172.16.0.30:3001 + +# 拆分后: +server 172.16.0.30:3001; # ✅ 正确 +``` + +### 步骤 5:配置健康检查 + +**SAE 控制台** → **应用配置** → **健康检查** + +| 配置项 | 值 | 说明 | +|-------|-----|------| +| **检查方式** | HTTP 请求 | | +| **检查路径** | `/health` | Nginx 健康检查端点 | +| **检查端口** | 80 | 与容器端口一致 | +| **检查协议** | HTTP | | +| **初始延迟** | 30 秒 | Nginx 启动很快 | +| **检查间隔** | 10 秒 | | +| **超时时间** | 3 秒 | | +| **不健康阈值** | 3 次 | 连续失败 3 次标记为不健康 | +| **健康阈值** | 2 次 | 连续成功 2 次标记为健康 | + +### 步骤 6:配置弹性伸缩 + +**SAE 控制台** → **应用配置** → **弹性伸缩** + +| 配置项 | 值 | 说明 | +|-------|-----|------| +| **最小实例数** | 2 | 高可用保证 | +| **最大实例数** | 5 | 前端流量波动通常不大 | +| **扩容条件** | CPU > 60% 持续 3 分钟 | Nginx 很少达到这个阈值 | +| **缩容条件** | CPU < 20% 持续 5 分钟 | | + +**注意**:前端 Nginx 很少需要扩容,因为静态文件服务极其高效。 + +### 步骤 7:配置公网访问(可选) + +**SAE 控制台** → **应用配置** → **应用访问配置** + +#### 选项 A:使用 SAE 提供的公网地址(临时测试) + +- **优势**:免费,立即可用 +- **劣势**:域名难记,HTTPS 证书是 SAE 的 + +#### 选项 B:绑定自定义域名(生产推荐) + +1. **添加域名解析**: + ``` + your-domain.com → CNAME → frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com + ``` + +2. **配置 HTTPS 证书**: + - 上传 SSL 证书(或使用免费证书) + +3. **强制 HTTPS**: + ```nginx + # 在 nginx.conf 中添加重定向 + server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; + } + ``` + +### 步骤 8:部署应用 + +点击"部署"按钮,SAE 将: + +1. 从 ACR 拉取镜像(~30 秒) +2. 启动容器实例(~30 秒) +3. 执行健康检查(~30 秒) +4. 流量切换(~10 秒) + +**总耗时**:约 2 分钟 + +--- + +## 9. 端到端测试 + +### 步骤 1:获取应用访问地址 + +**SAE 控制台** → **应用详情** → **应用访问配置** + +复制公网访问地址: +``` +https://frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com +``` + +### 步骤 2:测试主页加载 + +```bash +# 使用 curl 测试 +curl -I https://frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/ + +# 预期返回: +# HTTP/2 200 +# content-type: text/html +# cache-control: no-cache, no-store, must-revalidate +# ... + +# 完整响应 +curl https://frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/ | head -n 10 +# 应该看到 HTML 内容 +``` + +### 步骤 3:浏览器完整测试 + +打开浏览器,访问前端地址: + +#### 测试清单 A:页面基础功能 + +- [ ] **主页能正常加载** + - 看到登录页面或首页 + - 样式正确显示(不是纯文本) + - 图片和图标正常显示 + +- [ ] **路由跳转正常** + - 点击导航菜单,URL 变化 + - 页面内容更新 + - 浏览器后退/前进按钮正常工作 + +- [ ] **刷新页面不报错** + - 访问 `/dashboard` + - 按 F5 刷新 + - 页面正常显示(不是 404) + +#### 测试清单 B:API 调用测试 + +- [ ] **登录功能** + - 输入用户名和密码 + - 点击登录 + - 查看浏览器开发者工具 → Network 标签 + - 应该看到:`POST /api/v1/auth/login` → 状态 200 + +- [ ] **数据加载** + - 访问项目列表页 + - 查看 Network 标签 + - 应该看到:`GET /api/v1/projects` → 状态 200 + - 列表数据正常显示 + +- [ ] **文件上传** + - 尝试上传一个 PDF 文档 + - 查看 Network 标签 + - 应该看到:`POST /api/v1/knowledge-bases/.../documents` → 状态 200 + - 上传进度正常显示 + +#### 测试清单 C:性能测试 + +- [ ] **首屏加载时间** + - 清除浏览器缓存 + - 刷新页面 + - 查看 Network 标签 → 底部统计 + - 总时间应 < 3 秒(国内访问) + +- [ ] **静态资源缓存** + - 第一次访问:所有资源从服务器加载 + - 第二次访问:JS/CSS 文件显示"(from disk cache)" + - index.html 显示状态 200(每次都重新获取) + +- [ ] **Gzip 压缩** + - 查看 Response Headers + - 应该看到:`Content-Encoding: gzip` + - JS 文件大小减少 60-70% + +### 步骤 4:测试反向代理(验证 CORS 解决) + +```bash +# 打开浏览器开发者工具 → Console +# 执行以下代码: + +fetch('/api/v1/health') + .then(res => res.json()) + .then(data => console.log(data)); + +# 预期输出: +# {status: "healthy", timestamp: "...", ...} + +# 如果看到 CORS 错误: +# ❌ Access to fetch at '/api/v1/health' from origin 'https://...' has been blocked by CORS policy +# → 说明 Nginx 反向代理配置有问题 + +# 正确的情况: +# ✅ 请求成功,无任何 CORS 错误 +``` + +### 步骤 5:移动端测试(可选) + +使用手机浏览器访问前端地址: + +- [ ] 页面能自适应移动端屏幕 +- [ ] 触摸交互正常 +- [ ] 页面加载速度可接受 + +--- + +## 10. 监控与维护 + +### 📊 SAE 自带监控 + +#### 1. 实时监控 + +**SAE 控制台** → **应用详情** → **监控** + +**关键指标**: + +| 指标 | 健康阈值 | 告警阈值 | 说明 | +|------|---------|---------|------| +| **CPU 使用率** | < 30% | > 60% | Nginx 极少超过 30% | +| **内存使用率** | < 50% | > 80% | Nginx 内存占用很低 | +| **请求 QPS** | - | - | 了解访问量 | +| **平均响应时间** | < 50ms | > 200ms | 静态文件响应极快 | +| **错误率** | < 0.1% | > 1% | 监控 404/50x 错误 | +| **实例数量** | 2+ | - | 确保高可用 | + +**性能基准(参考)**: +``` +静态 HTML:响应时间 10-20ms +JS/CSS 文件:响应时间 10-30ms +API 代理:响应时间 50-500ms(取决于后端) +首屏加载(全部资源):1-3秒 +``` + +#### 2. 日志查看 + +**SAE 控制台** → **应用详情** → **日志** → **实时日志** + +**关键日志示例**: + +```bash +# ✅ 正常启动 +============================================ +Starting Frontend Nginx Service +Backend Service: 172.16.0.30:3001 +============================================ +nginx: configuration file /etc/nginx/nginx.conf test is successful + +# ✅ 正常请求(静态资源) +172.31.0.10 - - [13/Dec/2025:10:30:00 +0000] "GET / HTTP/1.1" 200 1234 +172.31.0.10 - - [13/Dec/2025:10:30:01 +0000] "GET /assets/index-xxxxx.js HTTP/1.1" 200 567890 + +# ✅ 正常请求(API 代理) +172.31.0.10 - - [13/Dec/2025:10:30:02 +0000] "GET /api/v1/projects HTTP/1.1" 200 8765 + +# ⚠️ 警告日志(404 Not Found) +172.31.0.10 - - [13/Dec/2025:10:30:03 +0000] "GET /favicon.ico HTTP/1.1" 404 153 + +# ❌ 错误日志(后端连接失败) +2025/12/13 10:30:04 [error] 7#7: *1 connect() failed (111: Connection refused) while connecting to upstream +client: 172.31.0.10, server: _, request: "GET /api/v1/projects HTTP/1.1" +upstream: "http://172.16.0.30:3001/api/v1/projects" +``` + +#### 3. Nginx 状态监控 + +```bash +# 在 SAE Webshell 中执行(或通过内网访问) +curl http://localhost/nginx_status + +# 输出示例: +# Active connections: 15 +# server accepts handled requests +# 123456 123456 567890 +# Reading: 2 Writing: 5 Waiting: 8 +``` + +**指标含义**: +- `Active connections`: 当前活跃连接数 +- `accepts`: 已接受的连接总数 +- `handled`: 已处理的连接总数 +- `requests`: 已处理的请求总数 +- `Reading`: 正在读取请求头的连接数 +- `Writing`: 正在写入响应的连接数 +- `Waiting`: 空闲的 Keep-Alive 连接数 + +### 🔧 日常维护任务 + +#### 每日检查 + +```bash +# 1. 检查应用健康状态 +# SAE 控制台 → 应用列表 → 查看运行状态(绿色为正常) + +# 2. 查看错误日志 +# SAE 控制台 → 应用详情 → 日志 → 筛选 error 级别 + +# 3. 检查访问量 +# SAE 控制台 → 应用详情 → 监控 → 查看 QPS +``` + +#### 每周任务 + +```bash +# 1. 查看性能指标趋势 +# SAE 控制台 → 应用详情 → 监控 → 选择"最近 7 天" +# 关注: +# - 响应时间是否变慢 +# - 错误率是否增加 +# - 流量是否有异常波动 + +# 2. 查看 404 错误 +# 在日志中搜索 "404",分析原因: +# - 资源确实不存在? +# - SPA 路由配置问题? +# - 外部爬虫访问不存在的路径? +``` + +#### 每月任务 + +```bash +# 1. 更新前端代码(如有新版本) +# 在本地重新构建镜像: +cd frontend +npm run build +docker build -t frontend-service:v1.0.1 . +docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/frontend-service:v1.0.1 + +# 2. 在 SAE 中更新镜像 +# SAE 控制台 → 应用详情 → 部署 +# 选择新镜像版本:v1.0.1 +# 灰度发布:先更新 1 个实例,观察 5 分钟后全量发布 + +# 3. 清理旧镜像(节省 ACR 存储空间) +# ACR 控制台 → 镜像仓库 → frontend-service +# 删除 30 天前的旧版本(保留最近 3 个版本) + +# 4. 检查 SSL 证书有效期(如果使用自定义域名) +# SAE 控制台 → 应用详情 → 应用访问配置 → HTTPS 设置 +# 证书到期前 30 天更新 +``` + +### 🚨 告警配置 + +**云监控** → **应用监控** → **创建告警规则** + +**推荐告警规则**: + +| 告警项 | 阈值 | 通知方式 | +|-------|------|---------| +| CPU 使用率 > 60% 持续 5 分钟 | 告警 | 钉钉/邮件 | +| 内存使用率 > 80% 持续 5 分钟 | 告警 | 钉钉/邮件 | +| 错误率 > 1% 持续 3 分钟 | 紧急 | 短信+钉钉 | +| 实例健康检查失败 > 3 次 | 紧急 | 短信+钉钉 | +| 平均响应时间 > 500ms 持续 5 分钟 | 告警 | 钉钉/邮件 | + +--- + +## 11. 故障排查 + +### 问题 1:页面加载空白(最常见) + +**症状**: + +浏览器访问前端地址,看到空白页面。 + +**排查步骤**: + +```bash +# 1. 检查 HTML 是否正确返回 +curl -I https://frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/ + +# 如果返回 404 或 50x,说明 Nginx 配置有问题 + +# 2. 检查构建产物是否存在 +# 登录 SAE Webshell: +ls -la /usr/share/nginx/html/ +# 应该看到: +# index.html +# assets/index-xxxxx.js +# assets/index-xxxxx.css + +# 如果 dist/ 目录为空,说明构建阶段失败 + +# 3. 查看浏览器开发者工具 Console +# 如果看到: +# Uncaught SyntaxError: Unexpected token '<' +# 说明 JS 文件路径错误,返回了 HTML 而不是 JS + +# 4. 检查 Vite 构建配置 +# vite.config.ts 中的 base 路径 +# 应该是:base: '/'(默认) +# 不要写成:base: '/app/'(会导致路径错误) +``` + +**解决方法**: + +```bash +# 方法 1:重新构建镜像(本地验证) +cd frontend +npm run build +ls -la dist/ # 确认构建产物存在 +docker build -t frontend-service:v1.0.1 . + +# 方法 2:检查 Dockerfile 是否正确复制了 dist/ +# 确保有这一行: +COPY --from=builder /app/dist /usr/share/nginx/html +``` + +### 问题 2:刷新页面报 404(SPA 路由问题) + +**症状**: + +- 访问 `/` 正常 +- 点击链接跳转到 `/dashboard` 正常 +- 刷新页面(F5)→ 404 Not Found + +**原因**: + +Nginx 找不到 `/dashboard` 文件,应该返回 `index.html` 让 React Router 接管。 + +**排查步骤**: + +```bash +# 1. 检查 Nginx 配置中的 try_files +# 登录 SAE Webshell: +cat /etc/nginx/nginx.conf | grep "try_files" + +# 应该看到: +# try_files $uri $uri/ /index.html; + +# 如果没有这一行,说明 SPA 路由配置缺失 +``` + +**解决方法**: + +```nginx +# 在 nginx.conf 的 location / 块中添加: +location / { + try_files $uri $uri/ /index.html; +} + +# 重新构建镜像并部署 +``` + +### 问题 3:API 请求报错(反向代理问题) + +**症状**: + +- 前端页面正常显示 +- 调用 API 时报错: + - `Network Error` + - `504 Gateway Timeout` + - `502 Bad Gateway` + +**排查步骤**: + +```bash +# 1. 检查后端服务是否运行 +# SAE 控制台 → 应用列表 → backend-service → 查看状态 + +# 2. 测试后端内网地址是否可达 +# 登录前端应用的 Webshell: +curl http://172.16.0.30:3001/api/v1/health + +# 如果返回错误,说明: +# - 后端服务未启动 +# - 内网地址配置错误 +# - VPC 网络不通 + +# 3. 检查 Nginx 配置中的 upstream +cat /etc/nginx/nginx.conf | grep -A 5 "upstream backend" + +# 应该看到正确的后端地址: +# server 172.16.0.30:3001 fail_timeout=30s max_fails=3; + +# 4. 查看 Nginx 错误日志 +tail -f /var/log/nginx/error.log | grep "upstream" +``` + +**解决方法**: + +```bash +# 方法 1:更新环境变量 +# SAE 控制台 → frontend-service → 应用配置 → 环境变量 +# 确认: +BACKEND_SERVICE_HOST=172.16.0.30 # 正确的内网 IP +BACKEND_SERVICE_PORT=3001 + +# 重启应用使环境变量生效 + +# 方法 2:测试内网连通性 +# 在前端 Webshell 中: +telnet 172.16.0.30 3001 +# 如果连接失败,检查: +# - 后端和前端是否在同一 VPC +# - 安全组规则是否允许访问 +``` + +### 问题 4:CORS 错误(反向代理未生效) + +**症状**: + +浏览器 Console 显示: +``` +Access to fetch at 'http://backend-service:3001/api/v1/projects' from origin 'https://frontend-service.com' has been blocked by CORS policy +``` + +**原因**: + +前端代码中硬编码了后端地址,而不是使用相对路径。 + +**排查步骤**: + +```bash +# 1. 检查前端代码中的 API 调用 +# src/api/request.ts +cat src/api/request.ts | grep "baseURL" + +# ❌ 错误示例: +# baseURL: 'http://backend-service:3001/api/v1' + +# ✅ 正确示例: +# baseURL: '/api/v1' + +# 2. 检查浏览器 Network 标签 +# 正确的请求路径应该是: +# https://frontend-service.com/api/v1/projects + +# 而不是: +# http://backend-service:3001/api/v1/projects +``` + +**解决方法**: + +```typescript +// src/api/request.ts +const request = axios.create({ + baseURL: '/api/v1', // ✅ 使用相对路径 + timeout: 30000, +}); + +// 重新构建并部署 +``` + +### 问题 5:静态资源 404(路径问题) + +**症状**: + +- HTML 能加载 +- JS/CSS 文件 404 Not Found +- 浏览器 Console 显示: + ``` + GET https://frontend-service.com/assets/index-xxxxx.js 404 (Not Found) + ``` + +**原因**: + +Vite 构建配置中的 `base` 路径不正确。 + +**排查步骤**: + +```bash +# 1. 查看 index.html 中的资源路径 +curl https://frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/ | grep "assets" + +# 应该看到: +# + +# 如果看到: +# # ⚠️ 相对路径 +# 或 +# # ❌ 错误的 base + +# 2. 检查 vite.config.ts +cat vite.config.ts | grep "base" +``` + +**解决方法**: + +```typescript +// vite.config.ts +export default defineConfig({ + base: '/', // ✅ 默认值,绝对路径 + // base: '/app/', // ❌ 不要设置子路径(除非确实需要) + plugins: [react()], +}); + +// 重新构建并部署 +``` + +### 问题 6:页面样式错乱(CSS 未加载) + +**症状**: + +页面内容显示,但样式混乱(纯文本排版)。 + +**排查步骤**: + +```bash +# 1. 检查浏览器 Network 标签 +# 查看 CSS 文件是否成功加载 +# 如果显示 404,参考问题 5 + +# 2. 检查 CSS 文件的 MIME 类型 +curl -I https://frontend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/assets/index-xxxxx.css + +# 应该看到: +# Content-Type: text/css + +# 如果看到: +# Content-Type: application/octet-stream # ❌ 错误的 MIME 类型 + +# 3. 检查 Nginx 配置 +cat /etc/nginx/nginx.conf | grep "include.*mime.types" +``` + +**解决方法**: + +```nginx +# 确保 nginx.conf 中包含: +http { + include /etc/nginx/mime.types; # ✅ 必需 + default_type application/octet-stream; + ... +} +``` + +### 问题 7:环境变量未替换 + +**症状**: + +Nginx 启动失败,错误日志显示: +``` +nginx: [emerg] host not found in upstream "${BACKEND_SERVICE_HOST}" in /etc/nginx/nginx.conf:45 +``` + +**原因**: + +`envsubst` 未正确替换环境变量。 + +**排查步骤**: + +```bash +# 1. 检查启动脚本 +cat /docker-entrypoint.sh | grep "envsubst" + +# 2. 检查最终生成的 nginx.conf +cat /etc/nginx/nginx.conf | grep "server.*backend" + +# 应该看到: +# server 172.16.0.30:3001; + +# 如果看到: +# server ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}; # ❌ 未替换 +``` + +**解决方法**: + +```dockerfile +# 确保 Dockerfile 中的启动脚本正确: +envsubst '${BACKEND_SERVICE_HOST} ${BACKEND_SERVICE_PORT}' \ + < /etc/nginx/templates/nginx.conf.template \ + > /etc/nginx/nginx.conf + +# 注意: +# - 单引号包裹变量名 +# - 使用 .template 文件作为源 +# - 输出到 /etc/nginx/nginx.conf +``` + +--- + +## 12. 注意事项与禁忌 + +### ✅ 最佳实践 + +#### 1. **API 调用使用相对路径** + +```typescript +// ✅ 正确做法:相对路径 +const request = axios.create({ + baseURL: '/api/v1', +}); + +// 前端:https://your-domain.com +// API:https://your-domain.com/api/v1/... +// 同源请求,无 CORS 问题 + +// ❌ 错误做法:硬编码后端地址 +const request = axios.create({ + baseURL: 'http://backend-service:3001/api/v1', +}); +// 跨域请求,会有 CORS 问题 +``` + +#### 2. **Vite base 路径配置** + +```typescript +// ✅ 正确做法:默认根路径 +export default defineConfig({ + base: '/', // 绝对路径,适合大多数场景 +}); + +// ⚠️ 仅在以下情况使用子路径: +// - 部署到 CDN 的子目录 +// - 多个前端共享一个域名 +export default defineConfig({ + base: '/app/', // 资源路径:/app/assets/index.js +}); +``` + +#### 3. **环境变量命名规范** + +```bash +# ✅ 正确做法:拆分 Host 和 Port +BACKEND_SERVICE_HOST=172.16.0.30 +BACKEND_SERVICE_PORT=3001 + +# ❌ 错误做法:完整 URL +BACKEND_SERVICE_URL=http://172.16.0.30:3001 +# Nginx 无法解析协议前缀 +``` + +#### 4. **缓存策略** + +```nginx +# ✅ 正确做法:差异化缓存 +location = /index.html { + add_header Cache-Control "no-cache"; # 每次都获取最新 +} + +location ~* \.(js|css)$ { + expires 1y; # 文件名带 hash,可以长期缓存 +} + +# ❌ 错误做法:全部缓存或全部不缓存 +add_header Cache-Control "public, max-age=31536000"; # index.html 也缓存 1 年 +add_header Cache-Control "no-cache"; # 所有文件都不缓存(浪费带宽) +``` + +#### 5. **健康检查端点** + +```nginx +# ✅ 正确做法:独立的健康检查端点 +location /health { + access_log off; + return 200 "healthy\n"; +} + +# ❌ 错误做法:使用主页作为健康检查 +# 如果前端代码有 Bug,健康检查也会失败 +``` + +### ❌ 绝对禁止 + +#### 1. **禁止在前端代码中硬编码后端地址** + +```typescript +// ❌ 错误示例 +const API_URL = 'http://172.16.0.30:3001/api/v1'; + +// 后果: +// - 本地开发时需要修改代码 +// - 部署到生产环境时需要再次修改 +// - 跨域问题 +// - 无法使用 Nginx 反向代理 + +// ✅ 正确做法 +const API_URL = '/api/v1'; // 相对路径,由 Nginx 代理 +``` + +#### 2. **禁止忽略 SPA 路由配置** + +```nginx +# ❌ 错误配置(缺少 try_files) +location / { + root /usr/share/nginx/html; +} +# 后果:刷新页面报 404 + +# ✅ 正确配置 +location / { + try_files $uri $uri/ /index.html; +} +``` + +#### 3. **禁止将 index.html 设置为长期缓存** + +```nginx +# ❌ 错误示例 +location / { + expires 1y; + add_header Cache-Control "public, immutable"; +} +# 后果:部署新版本后,用户仍看到旧版本 + +# ✅ 正确做法 +location = /index.html { + add_header Cache-Control "no-cache"; +} +``` + +#### 4. **禁止在 Nginx 配置中使用未经环境变量替换的占位符** + +```nginx +# ❌ 错误示例(直接写占位符) +upstream backend { + server ${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}; +} +# 如果没有 envsubst 替换,Nginx 会启动失败 + +# ✅ 正确做法(使用 envsubst) +envsubst '${BACKEND_SERVICE_HOST} ${BACKEND_SERVICE_PORT}' \ + < nginx.conf.template > /etc/nginx/nginx.conf +``` + +#### 5. **禁止忽略 Gzip 压缩** + +```nginx +# ❌ 错误做法(不压缩) +# 传输大小:500KB JS 文件 → 500KB + +# ✅ 正确做法(启用 Gzip) +gzip on; +gzip_types text/plain text/css application/javascript; +# 传输大小:500KB JS 文件 → 150KB(节省 70%) +``` + +#### 6. **禁止在生产环境暴露敏感信息** + +```nginx +# ❌ 错误做法 +server_tokens on; # 暴露 Nginx 版本号 +autoindex on; # 允许目录浏览 + +# ✅ 正确做法 +server_tokens off; +autoindex off; +``` + +#### 7. **禁止使用过长的超时时间** + +```nginx +# ❌ 错误示例(30 分钟超时) +proxy_read_timeout 1800s; +# 后果:占用连接资源,影响并发能力 + +# ✅ 正确做法(根据实际需求) +proxy_read_timeout 300s; # 5 分钟(AI 对话足够) +``` + +#### 8. **禁止忽略错误处理** + +```nginx +# ❌ 错误做法(后端挂了,Nginx 直接返回 502) +proxy_pass http://backend; + +# ✅ 正确做法(重试其他实例) +proxy_next_upstream error timeout http_502 http_503; +proxy_next_upstream_tries 2; +``` + +#### 9. **禁止在 Dockerfile 中使用 root 用户运行 Nginx** + +```dockerfile +# ❌ 错误做法 +USER root # 安全风险 + +# ✅ 正确做法 +USER nginx # Nginx 官方镜像默认已创建 nginx 用户 +# 或者不指定,Alpine 镜像默认使用 nginx 用户 +``` + +#### 10. **禁止使用错误的 Node 版本** + +```dockerfile +# ❌ 错误示例:使用 Node 18(与开发环境不一致) +FROM node:18-alpine AS builder +# 风险:package-lock.json 版本冲突 + +# ✅ 正确做法:使用 Node 22(与开发环境一致) +FROM node:22-alpine AS builder +# 保证依赖安装和构建环境一致 +``` + +#### 11. **禁止将日志写入文件** + +```nginx +# ❌ 错误做法(日志写入文件) +access_log /var/log/nginx/access.log main; +# 风险:可能写满容器磁盘 + +# ✅ 正确做法(日志输出到标准流) +access_log /dev/stdout main; +error_log /dev/stderr warn; +# SAE 会自动收集到日志中心 +``` + +#### 12. **禁止忽略容器时区设置** + +```dockerfile +# ❌ 错误做法(不设置时区,默认 UTC) +# 问题:日志时间比北京时间慢 8 小时 + +# ✅ 正确做法(设置为 Asia/Shanghai) +RUN apk add --no-cache tzdata && \ + cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone +``` + +#### 13. **禁止给后端地址危险的默认值** + +```bash +# ❌ 错误做法(给默认值) +export BACKEND_SERVICE_HOST=${BACKEND_SERVICE_HOST:-backend-service} +# 风险:可能连接到错误的后端 + +# ✅ 正确做法(强制要求配置) +if [ -z "$BACKEND_SERVICE_HOST" ]; then + echo "ERROR: BACKEND_SERVICE_HOST is required!" + exit 1 +fi +``` + +#### 14. **禁止忽略 Docker 镜像优化** + +```dockerfile +# ❌ 错误示例:单阶段构建 +FROM node:22 +RUN npm install +RUN npm run build +CMD ["nginx"] +# 镜像大小:~600MB + +# ✅ 正确做法:多阶段构建 +FROM node:22-alpine AS builder +RUN npm run build + +FROM nginx:1.25-alpine +COPY --from=builder /app/dist /usr/share/nginx/html +# 镜像大小:~40-60MB(节省 90%) +``` + +--- + +## 📚 相关文档 + +- [05-Node.js后端-SAE容器部署指南.md](./05-Node.js后端-SAE容器部署指南.md) +- [04-Python微服务-SAE容器部署指南.md](./04-Python微服务-SAE容器部署指南.md) +- [03-Dify-ECS部署完全指南.md](./03-Dify-ECS部署完全指南.md) + +--- + +## 🆘 获取帮助 + +**遇到问题?** + +1. 查看本文档的"故障排查"章节 +2. 查看 SAE 控制台的实时日志 +3. 查看 Nginx 错误日志:`/var/log/nginx/error.log` +4. 使用浏览器开发者工具 Network 标签分析请求 +5. 联系团队技术支持 + +--- + +**部署愉快!🚀** + diff --git a/docs/05-部署文档/07-关键配置补充说明.md b/docs/05-部署文档/07-关键配置补充说明.md new file mode 100644 index 00000000..330c6f3f --- /dev/null +++ b/docs/05-部署文档/07-关键配置补充说明.md @@ -0,0 +1,1028 @@ +# 关键配置补充说明 - 部署文档勘误与增强 + +> **文档版本:** v1.0 +> **创建日期:** 2025-12-14 +> **文档性质:** 对5个独立部署文档的关键补充 +> **优先级:** ⭐⭐⭐⭐⭐ 必读(包含3个P0/P1致命问题) + +--- + +## 📋 文档说明 + +本文档基于对5个独立部署文档的深度审查,补充了**3个致命问题**和**若干最佳实践**。这些内容在原文档中遗漏或未充分强调,但对生产环境部署至关重要。 + +**请在部署前务必阅读本文档!** + +--- + +## 🚨 致命问题修正(P0/P1) + +### 1. SAE孤岛效应 - NAT网关配置 ⭐⭐⭐⭐⭐ + +**问题严重度:P0(致命)** + +#### 问题描述 + +``` +SAE部署在VPC内,默认没有公网出口! + +影响场景: +❌ 后端调用 DeepSeek/OpenAI API → 超时 +❌ Python下载公网PDF → 超时 +❌ npm install公网依赖(构建时)→ 失败 + +结果:所有AI功能不可用,系统基本瘫痪! +``` + +#### 解决方案 + +**方案A:NAT网关(推荐,生产环境)** + +```bash +# 步骤1:创建NAT网关 +阿里云控制台 > VPC > NAT网关 > 创建NAT网关 +├─ VPC:选择SAE所在的VPC +├─ 交换机:选择SAE所在的交换机 +├─ 规格:小型(够用) +└─ 计费方式:按使用量计费 + +# 步骤2:创建并绑定EIP +NAT网关详情 > 弹性公网IP > 绑定弹性公网IP +├─ 创建新EIP或选择已有EIP +├─ 带宽:按使用流量(成本低) +└─ 确认绑定 + +# 步骤3:配置SNAT条目 +NAT网关详情 > SNAT管理 > 创建SNAT条目 +├─ 选择交换机:SAE所在的交换机(如 vsw-xxxxx) +├─ 选择公网IP:刚才绑定的EIP +└─ 确认创建 + +成本:NAT网关¥60/月 + EIP流量费¥30-50/月 = ¥90-110/月 +``` + +**方案B:SAE绑定公网IP(部分地域支持)** + +```bash +SAE控制台 > 应用配置 > 网络配置 +└─ 查看是否有"公网访问"或"绑定EIP"选项 + +⚠️ 注意: +- 并非所有地域都支持 +- 优先使用方案A(更稳定) +``` + +#### 验证NAT网关是否生效 + +```bash +# 方法1:在SAE应用日志中查看 +# 应用启动后,查看是否有DeepSeek API调用成功的日志 + +# 方法2:通过云助手执行命令(SAE控制台 > 实例列表 > 登录实例) +curl -I https://api.deepseek.com +# 应该返回 200 OK,而不是超时 + +# 方法3:测试Python下载公网PDF +curl -I https://arxiv.org/pdf/2301.00001.pdf +# 应该返回 200 OK +``` + +#### 更新的文档 + +- ✅ `00-部署架构总览.md`:物理架构图已增加NAT网关 +- ✅ `00-部署架构总览.md`:成本估算已更新(¥1,200-1,250/月) +- ⚠️ `05-Node.js后端-SAE容器部署指南.md`:需要在"SAE应用配置"章节增加网络配置说明 +- ⚠️ `04-Python微服务-SAE容器部署指南.md`:同上 + +--- + +### 2. 部署依赖死锁 - Dify API Key鸡生蛋问题 ⭐⭐⭐⭐⭐ + +**问题严重度:P1(严重)** + +#### 问题描述 + +``` +死锁链: +1. 后端启动需要 DIFY_API_KEY +2. DIFY_API_KEY 需要 Dify 启动并人工登录后才能生成 +3. 后端如果健康检查失败,会无限重启 + +结果:后端无法启动,或启动后PKB模块不可用 +``` + +#### 解决方案(分阶段部署) + +**阶段1:首次部署后端(临时配置)** + +```bash +# SAE环境变量配置 +DIFY_API_KEY=temp_placeholder_will_update_later + +# ⚠️ 重要:后端代码需要容错处理 +# backend/src/common/rag/DifyClient.ts +constructor() { + const apiKey = process.env.DIFY_API_KEY + + if (!apiKey || apiKey === 'temp' || apiKey.startsWith('temp_')) { + console.warn('⚠️ Dify API Key未配置,PKB模块将不可用') + this.enabled = false + return + } + + this.client = new DifySDK(apiKey) + this.enabled = true +} + +// 所有Dify调用前检查 +async createDataset(name: string) { + if (!this.enabled) { + throw new Error('Dify服务未配置,请先配置DIFY_API_KEY环境变量') + } + // ... 正常逻辑 +} +``` + +**阶段2:部署Dify并获取真实Key** + +```bash +# 1. 部署Dify到ECS(参考 03-Dify-ECS部署完全指南.md) +cd /opt/dify +docker-compose up -d + +# 2. 等待服务启动(约2-3分钟) +docker-compose logs -f api + +# 3. 浏览器访问 http://ECS公网IP +# 4. 注册管理员账号(首次访问会提示) +# 5. 创建API Key +# 设置 > API密钥 > 创建密钥 > 复制 +# 格式:app-xxxxxxxxxxxxxxxxxxxxx + +# 6. 记录API Key(妥善保存) +DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx +``` + +**阶段3:更新后端配置** + +```bash +# SAE控制台 > 应用详情 > 环境变量 +# 找到 DIFY_API_KEY,修改为真实值 +DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx + +# 保存 > 重启应用 +# SAE会执行滚动重启(零停机) +``` + +**阶段4:验证PKB功能** + +```bash +# 测试知识库创建 +curl -X POST https://your-api.com/api/v1/pkb/knowledge-bases \ + -H "Authorization: Bearer YOUR_USER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"测试知识库","description":"测试"}' + +# 应该返回 200 OK,而不是 "Dify服务未配置" 错误 +``` + +#### 更新的文档 + +- ✅ `00-部署架构总览.md`:部署顺序已更新,明确分阶段部署 +- ⚠️ `05-Node.js后端-SAE容器部署指南.md`:需要在"环境变量配置"章节增加临时配置说明 +- ⚠️ `03-Dify-ECS部署完全指南.md`:需要在"首次访问"章节增加API Key生成步骤 + +--- + +### 3. HTTP Client超时配置 - 防止连接泄漏 ⭐⭐⭐⭐ + +**问题严重度:P1(严重)** + +#### 问题描述 + +``` +Python服务处理PDF/OCR可能需要60-120秒 +如果后端HTTP Client没有设置超时,会导致: +❌ 连接数堆积 +❌ 后端实例内存耗尽 +❌ 数据库连接池耗尽 +``` + +#### 解决方案 + +**后端HTTP Client配置** + +```typescript +// backend/src/common/http/httpClient.ts +import axios from 'axios' + +export const pythonServiceClient = axios.create({ + baseURL: process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000', + timeout: 120000, // ⚠️ 120秒(2分钟) + timeoutErrorMessage: 'Python微服务响应超时(>2分钟)', + headers: { + 'Content-Type': 'application/json' + } +}) + +// 请求拦截器(可选,用于日志) +pythonServiceClient.interceptors.request.use( + (config) => { + console.log(`[HTTP] 调用Python服务: ${config.method?.toUpperCase()} ${config.url}`) + return config + }, + (error) => Promise.reject(error) +) + +// 响应拦截器(错误处理) +pythonServiceClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.code === 'ECONNABORTED') { + console.error('[HTTP] Python服务超时:', error.message) + } + return Promise.reject(error) + } +) +``` + +**Dify Client配置** + +```typescript +// backend/src/common/rag/DifyClient.ts +import axios from 'axios' + +const difyHttpClient = axios.create({ + baseURL: process.env.DIFY_API_URL || 'http://localhost/v1', + timeout: 60000, // ⚠️ 60秒(Dify响应较快) + headers: { + 'Authorization': `Bearer ${process.env.DIFY_API_KEY}`, + 'Content-Type': 'application/json' + } +}) +``` + +#### 超时时间建议 + +| 服务 | 超时时间 | 理由 | +|------|---------|------| +| **Python微服务** | 120秒 | PDF解析(Nougat OCR)可能需要60-120秒 | +| **Dify API** | 60秒 | RAG检索通常<10秒,60秒足够 | +| **外部LLM API** | 60秒 | DeepSeek/OpenAI流式响应,60秒足够 | +| **数据库查询** | 30秒 | Prisma默认,复杂查询可能需要10-20秒 | + +#### 更新的文档 + +- ⚠️ `05-Node.js后端-SAE容器部署指南.md`:需要在"代码准备"章节增加HTTP Client配置 + +--- + +## ⚠️ 重要安全配置 + +### 4. ECS端口安全 - Redis/Weaviate不对外开放 ⭐⭐⭐⭐⭐ + +**问题严重度:P0(致命安全风险)** + +#### 问题描述 + +``` +Dify的Redis(6379)和Weaviate(8080)如果对公网开放: +❌ Redis无密码,可被攻击者直接访问 +❌ Weaviate包含敏感的向量数据 +❌ 可能被用于DDoS攻击的跳板 +``` + +#### 正确配置 + +**docker-compose.yaml 端口配置** + +```yaml +services: + # ❌ 错误示例(危险) + redis: + ports: + - "6379:6379" # 对所有网卡开放,包括公网! + + # ✅ 正确配置 + redis: + image: redis:6-alpine + ports: + - "127.0.0.1:6379:6379" # 只监听 localhost + restart: always + volumes: + - ./volumes/redis/data:/data + command: redis-server --save 60 1 --loglevel warning + + # ✅ 正确配置 + weaviate: + image: semitechnologies/weaviate:1.19.0 + ports: + - "127.0.0.1:8080:8080" # 只监听 localhost + restart: always + environment: + - QUERY_DEFAULTS_LIMIT=25 + - AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true + - PERSISTENCE_DATA_PATH=/var/lib/weaviate + + # ✅ 只有Nginx需要对外(VPC内网) + nginx: + image: nginx:latest + ports: + - "80:80" # 对VPC内网开放(不是公网) + restart: always + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + depends_on: + - api + - web +``` + +#### ECS安全组配置 + +```bash +# 安全组规则(ECS控制台 > 安全组 > 配置规则) + +入方向规则: +├─ 允许 80/TCP 来源:VPC网段(172.16.0.0/12) # Nginx +├─ 允许 22/TCP 来源:您的办公室IP # SSH管理 +└─ 拒绝 所有 来源:0.0.0.0/0 # 默认拒绝 + +出方向规则: +└─ 允许 所有 目标:0.0.0.0/0 # 允许访问公网 +``` + +#### 验证安全配置 + +```bash +# 从公网测试(应该失败) +telnet ECS公网IP 6379 +# 应该超时或拒绝连接 + +telnet ECS公网IP 8080 +# 应该超时或拒绝连接 + +# 从VPC内测试(应该成功) +# 在SAE应用中执行 +curl http://172.16.x.x # Dify内网地址 +# 应该返回 Dify 的响应 +``` + +#### 更新的文档 + +- ⚠️ `03-Dify-ECS部署完全指南.md`:需要在"docker-compose.yaml配置"章节强调端口安全 + +--- + +### 5. Nginx client_max_body_size - 支持大文件上传 ⭐⭐⭐⭐ + +**问题严重度:P2(一般)** + +#### 问题描述 + +``` +医疗PDF可能很大(10-50MB) +Nginx默认限制:1MB +结果:用户上传大文件时返回 413 Request Entity Too Large +``` + +#### 解决方案 + +**前端Nginx配置** + +```nginx +# frontend-v2/nginx.conf.template +http { + # ⚠️ 新增:支持大文件上传 + client_max_body_size 50M; + + # ⚠️ 新增:开启gzip(React大体积JS) + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + gzip_min_length 1000; + gzip_comp_level 6; + + server { + listen 8080; + server_name _; + + # 根目录 + root /usr/share/nginx/html; + index index.html; + + # API反向代理 + location /api/ { + proxy_pass http://${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # ⚠️ 新增:代理超时配置 + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + } + + # SPA路由 + location / { + try_files $uri $uri/ /index.html; + } + + # 健康检查 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} +``` + +#### 更新的文档 + +- ⚠️ `06-前端Nginx-SAE容器部署指南.md`:需要在"nginx.conf.template"章节增加配置 + +--- + +### 6. Python Workers限制 - 防止OOM ⭐⭐⭐⭐⭐ + +**问题严重度:P1(严重)** + +#### 问题描述 + +``` +PyMuPDF/Nougat OCR非常吃内存(单个请求可能占用500MB-1GB) +SAE配置:2GB内存 +如果Gunicorn workers过多,会导致OOM(Out of Memory) +``` + +#### 解决方案 + +**Dockerfile配置** + +```dockerfile +# extraction_service/Dockerfile + +# 运行阶段 +FROM python:3.11-slim + +# ... 其他配置 ... + +# ⚠️ 关键:限制workers防止OOM +CMD ["gunicorn", "app.main:app", \ + "--workers", "2", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--timeout", "120", \ + "--max-requests", "100", \ + "--max-requests-jitter", "10", \ + "--access-logfile", "-", \ + "--error-logfile", "-"] + +# workers=2: 最多2个worker(2GB内存限制) +# timeout=120: 单个请求最多120秒(OCR可能很慢) +# max-requests=100: 100个请求后重启worker(防止内存泄漏) +``` + +**SAE配置** + +```bash +# SAE控制台 > 应用配置 > 实例规格 +CPU: 1核 +内存: 2GB # ⚠️ 不要低于2GB + +# 实例数量 +最小实例数: 1 +最大实例数: 3(根据流量自动扩容) +``` + +#### Workers数量计算公式 + +```python +# 经验公式 +workers = (CPU核数 × 2) + 1 + +# 但对于内存密集型应用(如PDF解析) +workers = min((内存GB / 单worker内存GB), (CPU核数 × 2) + 1) + +# 示例:SAE 1核2GB +单worker内存 ≈ 800MB(PyMuPDF + Nougat) +workers = min(2GB / 0.8GB, 1×2+1) = min(2.5, 3) = 2 + +# 结论:workers=2 是安全值 +``` + +#### 监控OOM + +```bash +# SAE控制台 > 监控 > 内存使用率 +# 如果经常达到90%+,说明需要: +# 1. 减少workers(从2降到1) +# 2. 增加内存(从2GB升到4GB) +# 3. 优化代码(减少内存占用) +``` + +#### 更新的文档 + +- ⚠️ `04-Python微服务-SAE容器部署指南.md`:需要在"Dockerfile"章节强调workers限制 + +--- + +## 📖 开发调试最佳实践 + +### 7. SSH隧道 - 本地直连RDS数据库 ⭐⭐⭐⭐ + +**用途:开发便利性(非必需,但强烈推荐)** + +#### 场景 + +``` +开发人员需要用Navicat/DBeaver查看RDS数据 +但RDS只允许VPC内网访问 +解决:通过ECS作为跳板机,建立SSH隧道 +``` + +#### 操作步骤 + +**步骤1:确保ECS有SSH访问权限** + +```bash +# 本地生成SSH密钥(如果还没有) +ssh-keygen -t rsa -b 4096 -C "your_email@example.com" + +# 将公钥添加到ECS +# ECS控制台 > 实例 > 远程连接 > 重置密钥对 +# 或者手动添加到 ~/.ssh/authorized_keys +``` + +**步骤2:建立SSH隧道** + +```bash +# 格式 +ssh -N -L 本地端口:RDS内网地址:RDS端口 root@ECS公网IP -i 密钥文件 + +# 示例 +ssh -N -L 5433:rm-bp1xxxxx.pg.rds.aliyuncs.com:5432 \ + root@120.55.xx.xx \ + -i ~/.ssh/dify-ecs.pem + +# 参数说明: +# -N: 不执行远程命令,只建立隧道 +# -L: 本地端口转发 +# 5433: 本地监听端口(避免与本地PostgreSQL 5432冲突) +# rm-bp1xxxxx...: RDS内网地址 +# 5432: RDS端口 +``` + +**步骤3:Navicat连接** + +``` +连接类型:PostgreSQL +主机:localhost +端口:5433 +用户名:aiclinical_rw +密码:(RDS密码) +数据库:ai_clinical_research + +测试连接 → 成功! +``` + +**步骤4:后台运行隧道(可选)** + +```bash +# 方法1:nohup后台运行 +nohup ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \ + root@ECS-IP -i key.pem > /dev/null 2>&1 & + +# 方法2:创建systemd服务(Linux) +# /etc/systemd/system/rds-tunnel.service +[Unit] +Description=SSH Tunnel to RDS +After=network.target + +[Service] +Type=simple +User=your-user +ExecStart=/usr/bin/ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 root@ECS-IP -i /home/your-user/.ssh/key.pem +Restart=always + +[Install] +WantedBy=multi-user.target + +# 启动服务 +sudo systemctl start rds-tunnel +sudo systemctl enable rds-tunnel +``` + +#### 安全注意事项 + +``` +⚠️ 不要将SSH密钥提交到Git +⚠️ 定期轮换ECS的SSH密钥 +⚠️ 只在开发环境使用,生产环境通过VPN访问 +``` + +--- + +### 8. 时区统一配置 - 防止日志时间混乱 ⭐⭐⭐⭐⭐ + +**问题严重度:P2(重要)** + +#### 问题描述 + +``` +不同服务的时区不一致会导致: +❌ 日志时间对不上(前端14:00,后端06:00) +❌ pg-boss定时任务在错误时间触发 +❌ 用户看到的时间戳错误 +❌ 排查故障极为痛苦 +``` + +#### 解决方案 + +**所有服务统一使用 `Asia/Shanghai` 时区** + +```dockerfile +# backend/Dockerfile - Node.js后端 +FROM node:22-alpine +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai +# ... 其他配置 + +# extraction_service/Dockerfile - Python微服务 +FROM python:3.11-slim +RUN apt-get update && apt-get install -y tzdata +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# ... 其他配置 + +# frontend-v2/Dockerfile - 前端(已配置) +FROM nginx:1.25-alpine +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai +# ... 其他配置 +``` + +```sql +-- RDS PostgreSQL 时区配置 +-- RDS控制台 > 参数设置 > timezone +timezone = Asia/Shanghai +``` + +**验证时区:** +```bash +# 查看容器时区 +docker exec date +# 应该显示:Sat Dec 14 14:30:00 CST 2024 + +# 查看RDS时区 +psql -c "SHOW timezone;" +# 应该显示:Asia/Shanghai +``` + +#### 影响范围 + +- ✅ Node.js后端:需要更新Dockerfile +- ✅ Python微服务:需要更新Dockerfile +- ✅ 前端Nginx:已正确配置 +- ✅ RDS PostgreSQL:需要修改参数 + +#### 修复步骤 + +```bash +# 1. 修改Node.js后端Dockerfile +cd backend +# 在Dockerfile中添加时区配置(见上方示例) + +# 2. 修改Python微服务Dockerfile +cd extraction_service +# 在Dockerfile中添加时区配置(见上方示例) + +# 3. 修改RDS时区 +# RDS控制台 > 参数设置 > timezone > Asia/Shanghai +# 需要重启RDS实例 + +# 4. 验证 +docker exec backend-container date +docker exec python-container date +psql -h rds-host -c "SHOW timezone;" +``` + +--- + +### 9. 镜像拉取策略 - 防止代码不更新 ⭐⭐⭐⭐⭐ + +**问题严重度:P2(重要)** + +#### 问题描述 + +``` +场景: +开发者修改代码 → 构建镜像 → 推送到ACR(覆盖v1.0.0) +→ SAE部署 → 发现代码没更新??? + +原因: +SAE默认镜像拉取策略可能是 IfNotPresent +如果本地已有 v1.0.0,不会重新拉取 +``` + +#### 解决方案 + +**方案A:每次部署使用新版本号(强烈推荐)** + +```bash +# 使用语义化版本号 +v1.0.0 → v1.0.1 → v1.0.2 ... + +# 或使用时间戳 +v20251214-1430 → v20251214-1530 ... + +# 或使用Git SHA +v-a1b2c3d → v-b2c3d4e ... + +# 构建示例 +docker build -t backend:v1.0.1 . +docker tag backend:v1.0.1 registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.1 +docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.1 + +# SAE部署时选择新版本号 +``` + +**方案B:配置SAE镜像拉取策略(测试环境)** + +```bash +# SAE控制台 > 应用配置 > 镜像设置 +镜像拉取策略:Always + +# ⚠️ 注意: +# - 每次重启都会拉取镜像(启动稍慢) +# - 适合测试环境,不推荐生产环境 +``` + +#### 最佳实践 + +| 环境 | 推荐方案 | 理由 | +|------|---------|------| +| **生产环境** | 方案A(版本号管理) | 版本可追溯,稳定 | +| **测试环境** | 方案B(Always拉取) | 始终最新,方便 | +| **开发环境** | 方案A | 避免混乱 | + +**❌ 不要:** +```bash +# 始终使用 latest 标签(无法追溯版本) +docker tag backend:latest ... +``` + +--- + +### 10. Python服务内存管理 - 防止OOM ⭐⭐⭐⭐ + +**问题严重度:P2(重要)** + +#### 问题描述 + +``` +Python服务(PyMuPDF/Nougat)内存密集,容易OOM +❌ 单个PDF OCR可能占用500MB-1GB内存 +❌ 多个并发请求会导致内存溢出 +❌ SAE默认2GB内存可能不够 +``` + +#### 解决方案 + +**规格建议:** + +| 场景 | CPU | 内存 | Workers | 适用情况 | +|------|-----|------|---------|---------| +| **基础版** | 1核 | 2GB | 2 | 简单PDF解析 | +| **标准版** | 2核 | 4GB | 3 | 包含OCR(Nougat) | +| **增强版** | 2核 | 8GB | 4 | 大量OCR + 高并发 | + +**Dockerfile优化(已应用):** + +```dockerfile +# extraction_service/Dockerfile +CMD ["gunicorn", "main:app", \ + "--bind", "0.0.0.0:8000", \ + "--workers", "2", \ # ⚠️ 限制并发 + "--timeout", "120", \ # ⚠️ 2分钟超时 + "--max-requests", "100", \ # ⚠️ 处理100个请求后重启worker + "--max-requests-jitter", "10"] # ⚠️ 随机抖动,避免同时重启 +``` + +**监控与告警:** + +```bash +# SAE控制台 > 监控告警 > 创建告警规则 +指标:内存使用率 +阈值:> 80% +动作:发送通知 + 自动扩容(可选) +``` + +#### 如果遇到OOM + +**方案1:升级内存(推荐)** +```bash +# SAE控制台 > 应用配置 > 规格调整 +1核2GB → 2核4GB(增加¥100/月) +``` + +**方案2:限制并发(临时)** +```dockerfile +# 修改Dockerfile +CMD ["gunicorn", "main:app", \ + "--workers", "1", \ # 降低并发 + "--threads", "2"] # 使用线程而非进程 +``` + +--- + +### 11. OSS签名URL - 安全的文件访问 ⭐⭐⭐⭐ + +**用途:安全最佳实践** + +#### 问题 + +``` +如果OSS Bucket设置为Public: +❌ 任何人都可以访问所有文件 +❌ 无法追踪谁访问了哪些文件 +❌ 无法控制访问时效 +``` + +#### 解决方案 + +**OSS Bucket配置** + +```bash +# OSS控制台 > Bucket列表 > aiclinical-data-prod +# 读写权限:私有(Private) +``` + +**后端生成签名URL** + +```typescript +// backend/src/common/storage/OSSAdapter.ts +import OSS from 'ali-oss' + +export class OSSAdapter { + private client: OSS + + constructor() { + this.client = new OSS({ + region: process.env.OSS_REGION!, + accessKeyId: process.env.OSS_ACCESS_KEY_ID!, + accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!, + bucket: process.env.OSS_BUCKET! + }) + } + + /** + * 生成签名URL(1小时有效期) + */ + async getSignedUrl(objectKey: string, expiresIn: number = 3600): Promise { + try { + const url = this.client.signatureUrl(objectKey, { + expires: expiresIn, // 秒,默认1小时 + method: 'GET' + }) + + return url + } catch (error) { + console.error('[OSS] 生成签名URL失败:', error) + throw error + } + } + + /** + * 上传文件 + */ + async uploadFile(objectKey: string, filePath: string): Promise { + try { + const result = await this.client.put(objectKey, filePath) + console.log(`[OSS] 文件上传成功: ${objectKey}`) + + // 返回签名URL(不是公开URL) + return this.getSignedUrl(objectKey) + } catch (error) { + console.error('[OSS] 文件上传失败:', error) + throw error + } + } +} +``` + +**API返回签名URL** + +```typescript +// backend/src/modules/pkb/documentController.ts +router.get('/documents/:id/download', async (req, res) => { + const { id } = req.params + + // 1. 查询文档元数据 + const document = await prisma.document.findUnique({ + where: { id } + }) + + if (!document) { + return res.status(404).json({ error: '文档不存在' }) + } + + // 2. 权限校验(确保用户有权访问) + if (document.userId !== req.user.id) { + return res.status(403).json({ error: '无权访问此文档' }) + } + + // 3. 生成签名URL(1小时有效) + const ossAdapter = new OSSAdapter() + const signedUrl = await ossAdapter.getSignedUrl(document.ossKey, 3600) + + // 4. 返回签名URL + res.json({ + url: signedUrl, + expiresIn: 3600, + filename: document.filename + }) +}) +``` + +**前端使用签名URL** + +```typescript +// frontend-v2/src/api/documents.ts +export async function downloadDocument(documentId: string) { + // 1. 调用后端API获取签名URL + const response = await fetch(`/api/v1/pkb/documents/${documentId}/download`, { + headers: { + 'Authorization': `Bearer ${getToken()}` + } + }) + + const { url, filename } = await response.json() + + // 2. 使用签名URL下载文件 + const link = document.createElement('a') + link.href = url // 签名URL,1小时有效 + link.download = filename + link.click() +} +``` + +#### 优势 + +``` +✅ 安全:只有授权用户才能获取签名URL +✅ 时效:URL自动过期(1小时后失效) +✅ 审计:可以记录谁访问了哪些文件 +✅ 灵活:可以动态调整过期时间 +``` + +--- + +## 📝 总结 + +### 必须立即修复的问题 + +| # | 问题 | 严重度 | 影响 | 修复时间 | +|---|------|--------|------|---------| +| 1 | **NAT网关缺失** | P0 | 所有AI功能不可用 | 15分钟 | +| 2 | **Dify API Key死锁** | P1 | PKB模块不可用 | 10分钟(分阶段部署) | +| 3 | **HTTP超时未配置** | P1 | 连接泄漏,系统崩溃 | 5分钟(代码修改) | +| 4 | **ECS端口对外开放** | P0 | 安全风险,可被攻击 | 5分钟(docker-compose修改) | +| 5 | **Python Workers过多** | P1 | OOM,服务崩溃 | 2分钟(Dockerfile修改) | +| 6 | **Nginx文件大小限制** | P2 | 大文件上传失败 | 2分钟(nginx.conf修改) | + +### 推荐但非必需的优化 + +| # | 优化 | 价值 | 实施时间 | +|---|------|------|---------| +| 7 | **SSH隧道** | 开发便利性 | 10分钟 | +| 8 | **OSS签名URL** | 安全最佳实践 | 30分钟(代码修改) | + +### 下一步行动 + +``` +☐ 1. 创建NAT网关(必需,15分钟)⭐⭐⭐⭐⭐ +☐ 2. 修改docker-compose.yaml(ECS端口安全,5分钟)⭐⭐⭐⭐⭐ +☐ 3. 修改Dockerfile(Python workers限制,2分钟)⭐⭐⭐⭐ +☐ 4. 修改nginx.conf(文件大小限制,2分钟)⭐⭐⭐⭐ +☐ 5. 修改后端代码(HTTP超时,5分钟)⭐⭐⭐⭐ +☐ 6. 修改后端代码(Dify容错,5分钟)⭐⭐⭐⭐ +☐ 7. 更新部署流程(分阶段部署,文档更新)⭐⭐⭐⭐ +☐ 8. 统一时区配置(必需,15分钟)⭐⭐⭐⭐⭐ +☐ 9. 配置镜像拉取策略(必需,5分钟)⭐⭐⭐⭐⭐ +☐ 10. Python内存管理(必需,10分钟)⭐⭐⭐⭐ +☐ 11. (可选)配置SSH隧道(开发便利,10分钟) +☐ 12. (可选)实现OSS签名URL(安全,30分钟) + +总计:必需修复约70分钟,可选优化约40分钟 +``` + +--- + +**文档创建人:** AI助手 +**最后更新:** 2025-12-14 +**版本:** v1.0 + +**核心理念:安全第一、稳定第二、便利第三** ⭐⭐⭐ + diff --git a/docs/05-部署文档/08-部署检查清单.md b/docs/05-部署文档/08-部署检查清单.md new file mode 100644 index 00000000..be23b3e9 --- /dev/null +++ b/docs/05-部署文档/08-部署检查清单.md @@ -0,0 +1,783 @@ +# 部署检查清单 - 从0到上线的完整Checklist + +> **文档版本:** v1.0 +> **创建日期:** 2025-12-14 +> **适用场景:** 首次部署到阿里云生产环境 +> **预计时间:** 4-5小时(实际操作时间) + +--- + +## 📋 使用说明 + +本清单按照部署顺序列出所有必需步骤,每个步骤包含: +- ☐ 复选框(完成后打勾) +- ⏱️ 预计时间 +- ⚠️ 关键注意事项 +- 🔗 参考文档链接 + +**建议:打印或复制到笔记软件,逐项完成。** + +--- + +## 阶段1:基础设施准备(Day 1上午) + +### 1.1 阿里云账号准备 + +☐ **实名认证** +- ⏱️ 5分钟 +- 阿里云控制台 > 账号管理 > 实名认证 +- ⚠️ 企业认证需要营业执照 + +☐ **账号充值** +- ⏱️ 2分钟 +- 建议充值:¥500(首月成本约¥1,200) +- 费用中心 > 充值 + +☐ **开通服务** +- ⏱️ 5分钟 +- 需要开通的服务: + - ✅ SAE(Serverless应用引擎) + - ✅ RDS(云数据库) + - ✅ OSS(对象存储) + - ✅ VPC(专有网络) + - ✅ NAT网关 + - ✅ ACR(容器镜像服务) + - ✅ ECS(云服务器,Dify用) + +☐ **创建RAM子账号(可选但推荐)** +- ⏱️ 10分钟 +- RAM访问控制 > 用户 > 创建用户 +- 权限:AliyunSAEFullAccess, AliyunRDSFullAccess, AliyunOSSFullAccess +- ⚠️ 不要用主账号部署,安全风险大 + +--- + +### 1.2 VPC网络配置 ⭐⭐⭐⭐⭐ + +☐ **创建VPC** +- ⏱️ 5分钟 +- VPC控制台 > 创建VPC +- 名称:aiclinical-vpc-prod +- 网段:172.16.0.0/12 +- 地域:cn-hangzhou(杭州)或就近地域 + +☐ **创建交换机** +- ⏱️ 3分钟 +- VPC详情 > 交换机 > 创建交换机 +- 名称:aiclinical-vsw-prod +- 可用区:随机可用区A +- 网段:172.16.0.0/20 + +☐ **⭐ 创建NAT网关(必需!)** +- ⏱️ 15分钟 +- VPC控制台 > NAT网关 > 创建NAT网关 +- VPC:aiclinical-vpc-prod +- 交换机:aiclinical-vsw-prod +- 规格:小型 +- 计费方式:按使用量计费 +- ⚠️ **没有NAT网关,所有AI功能都会超时!** +- 🔗 参考:`07-关键配置补充说明.md` 第1节 + +☐ **创建并绑定EIP** +- ⏱️ 5分钟 +- NAT网关详情 > 弹性公网IP > 绑定EIP +- 创建新EIP +- 带宽:按使用流量 +- 记录EIP地址:________________ + +☐ **配置SNAT条目** +- ⏱️ 3分钟 +- NAT网关详情 > SNAT管理 > 创建SNAT条目 +- 交换机:aiclinical-vsw-prod +- 公网IP:刚才创建的EIP +- ⚠️ 这一步让SAE可以访问公网 + +☐ **创建安全组** +- ⏱️ 5分钟 +- ECS控制台 > 安全组 > 创建安全组 +- 名称:aiclinical-sg-prod +- VPC:aiclinical-vpc-prod +- 入方向规则: + - 允许 80/TCP 来源:172.16.0.0/12(VPC内网) + - 允许 22/TCP 来源:您的办公室IP(SSH管理) + - 拒绝 所有 来源:0.0.0.0/0 + +--- + +### 1.3 RDS PostgreSQL 15 部署 + +☐ **创建RDS实例** +- ⏱️ 10分钟(等待创建15分钟) +- RDS控制台 > 创建实例 +- 数据库类型:PostgreSQL +- 版本:15 +- 规格:rds.pg.s2.large(2核4GB) +- 存储:100GB SSD +- VPC:aiclinical-vpc-prod +- 交换机:aiclinical-vsw-prod +- 记录内网地址:rm-____________.pg.rds.aliyuncs.com + +☐ **配置白名单** +- ⏱️ 2分钟 +- RDS实例详情 > 数据安全性 > 白名单设置 +- 添加白名单分组:sae-vpc +- IP地址:172.16.0.0/12(VPC网段) +- ⚠️ **必须用VPC网段,不能用单机IP!** +- 🔗 参考:`PostgreSQL部署策略-摸底报告.md` + +☐ **创建数据库账号** +- ⏱️ 3分钟 +- RDS实例详情 > 账号管理 > 创建账号 +- 账号名:aiclinical_rw +- 密码:(16位强密码,记录到密码管理器) +- 账号类型:普通账号 +- 授权数据库:稍后创建 + +☐ **创建数据库** +- ⏱️ 2分钟 +- RDS实例详情 > 数据库管理 > 创建数据库 +- 数据库名:ai_clinical_research +- 字符集:UTF8 +- 授权账号:aiclinical_rw(读写) + +☐ **配置自动备份** +- ⏱️ 3分钟 +- RDS实例详情 > 备份恢复 > 备份设置 +- 数据备份保留:7天 +- 日志备份保留:7天(⚠️ 必须开启,支持PITR) +- 备份周期:每天 +- 备份时间:02:00-04:00(凌晨) + +☐ **导入数据库(pg_dump)** +- ⏱️ 5分钟 +- 本地执行: + ```bash + # 1. 导出本地数据库 + docker exec ai-clinical-postgres pg_dump -U postgres -d ai_clinical_research \ + --format=plain --no-owner --no-acl --encoding=UTF8 \ + > backup_$(date +%Y%m%d_%H%M%S).sql + + # 2. 导入到RDS(需要ECS跳板机或临时开启公网) + psql -h rm-xxxxx.pg.rds.aliyuncs.com \ + -p 5432 \ + -U aiclinical_rw \ + -d ai_clinical_research \ + -f backup_20251214_100000.sql + ``` +- 🔗 参考:`PostgreSQL部署策略-摸底报告.md` 第4节 + +☐ **验证数据库导入** +- ⏱️ 3分钟 +- 连接RDS,执行: + ```sql + -- 验证Schema + \dn + -- 应该看到10个Schema + + -- 验证表数量 + SELECT schemaname, COUNT(*) + FROM pg_tables + WHERE schemaname NOT IN ('pg_catalog', 'information_schema') + GROUP BY schemaname; + -- platform_schema: 8, aia_schema: 5, pkb_schema: 5, asl_schema: 6, dc_schema: 6 + + -- 验证数据 + SELECT COUNT(*) FROM platform_schema.users; + -- 应该有3条(或你的实际数量) + ``` + +--- + +### 1.4 OSS对象存储配置 + +☐ **创建OSS Bucket** +- ⏱️ 5分钟 +- OSS控制台 > Bucket列表 > 创建Bucket +- Bucket名称:aiclinical-data-prod +- 地域:cn-hangzhou(与VPC同地域) +- 读写权限:私有(Private)⚠️ 必须私有 +- 版本控制:关闭 +- 服务端加密:关闭(可选) + +☐ **配置生命周期规则(可选)** +- ⏱️ 3分钟 +- Bucket详情 > 基础设置 > 生命周期 +- 规则名称:delete-temp-files +- 应用范围:uploads/temp/ +- 操作:删除 +- 天数:30天(临时文件30天后自动删除) + +☐ **创建RAM角色(推荐)** +- ⏱️ 10分钟 +- RAM访问控制 > 角色 > 创建角色 +- 角色类型:普通服务角色 +- 受信服务:SAE +- 角色名称:AliyunSAEDefaultRole +- 权限策略:AliyunOSSFullAccess +- ⚠️ 使用RAM角色比AccessKey更安全 + +☐ **记录OSS配置信息** +- Bucket名称:aiclinical-data-prod +- 地域:cn-hangzhou +- 内网Endpoint:oss-cn-hangzhou-internal.aliyuncs.com +- AccessKey ID:(如果不用RAM角色) +- AccessKey Secret:(如果不用RAM角色) + +--- + +### 1.5 ACR容器镜像仓库配置 + +☐ **创建命名空间** +- ⏱️ 3分钟 +- 容器镜像服务 > 命名空间 > 创建命名空间 +- 命名空间:aiclinical +- 类型:私有 + +☐ **配置访问凭证** +- ⏱️ 2分钟 +- 容器镜像服务 > 访问凭证 > 设置固定密码 +- 用户名:(阿里云账号) +- 密码:(设置一个强密码) +- 记录登录命令: + ```bash + docker login --username=your-account registry.cn-hangzhou.aliyuncs.com + ``` + +--- + +## 阶段2:核心服务部署(Day 1下午) + +### 2.1 Node.js后端部署 + +☐ **本地代码准备** +- ⏱️ 10分钟 +- 确保代码最新:`git pull origin main` +- 安装依赖:`cd backend && npm install` +- 编译代码:`npm run build` +- 验证编译:`ls dist/` 应该有编译后的JS文件 + +☐ **Prisma反向同步(重要)** +- ⏱️ 5分钟 +- 连接RDS,执行: + ```bash + cd backend + # 修改 .env 为RDS连接 + DATABASE_URL=postgresql://aiclinical_rw:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical_research + + # 反向同步 + npx prisma db pull + + # 生成Prisma Client + npx prisma generate + ``` +- 🔗 参考:`05-Node.js后端-SAE容器部署指南.md` 第4节 + +☐ **修改HTTP Client超时配置** +- ⏱️ 5分钟 +- 编辑 `backend/src/common/http/httpClient.ts` +- 设置timeout: 120000(120秒) +- 🔗 参考:`07-关键配置补充说明.md` 第3节 + +☐ **修改Dify Client容错处理** +- ⏱️ 5分钟 +- 编辑 `backend/src/common/rag/DifyClient.ts` +- 添加临时Key容错逻辑 +- 🔗 参考:`07-关键配置补充说明.md` 第2节 + +☐ **构建Docker镜像** +- ⏱️ 5分钟 +- ```bash + cd backend + docker build -t backend:v1.0.0 . + ``` +- 验证镜像:`docker images | grep backend` + +☐ **本地测试镜像** +- ⏱️ 5分钟 +- ```bash + docker run -p 3001:3001 \ + -e DATABASE_URL="postgresql://..." \ + -e NODE_ENV=production \ + backend:v1.0.0 + ``` +- 测试健康检查:`curl http://localhost:3001/health` + +☐ **推送到ACR** +- ⏱️ 3分钟 +- ```bash + docker tag backend:v1.0.0 registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0 + docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0 + ``` + +☐ **创建SAE应用** +- ⏱️ 10分钟 +- SAE控制台 > 应用列表 > 创建应用 +- 应用名称:aiclinical-backend +- 部署方式:容器镜像 +- 镜像地址:registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0 +- VPC:aiclinical-vpc-prod +- 交换机:aiclinical-vsw-prod +- 实例规格:1核2GB +- 实例数量:最小2,最大10 +- 健康检查:HTTP /health + +☐ **配置环境变量(关键)** +- ⏱️ 10分钟 +- SAE应用详情 > 环境变量 +- 必需变量: + ```bash + # 数据库 + DATABASE_URL=postgresql://aiclinical_rw:password@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical_research?connection_limit=10&pool_timeout=10 + + # OSS + STORAGE_TYPE=oss + OSS_REGION=cn-hangzhou + OSS_BUCKET=aiclinical-data-prod + OSS_ACCESS_KEY_ID=LTAI5*** + OSS_ACCESS_KEY_SECRET=*** + + # 缓存/队列(Postgres-Only) + CACHE_TYPE=postgres + QUEUE_TYPE=pgboss + + # Dify(⚠️ 临时值) + DIFY_API_URL=http://172.16.x.x/v1 + DIFY_API_KEY=temp_placeholder_will_update_later + + # Python微服务(稍后填写) + EXTRACTION_SERVICE_URL=http://172.16.x.x:8000 # ⚠️ 使用Python服务的内网IP(从SAE控制台获取) + + # LLM API + DEEPSEEK_API_KEY=sk-*** + + # 其他 + NODE_ENV=production + PORT=3001 + JWT_SECRET=(生成一个强密钥) + ``` + +☐ **部署应用** +- ⏱️ 5分钟(等待部署10分钟) +- SAE控制台 > 部署 +- 等待实例启动 +- 查看日志:应该看到"数据库连接成功" + +☐ **验证后端服务** +- ⏱️ 3分钟 +- ```bash + # 健康检查 + curl http://aiclinical-backend.sae:3001/health + # 应该返回 200 OK + + # 测试用户注册 + curl -X POST http://aiclinical-backend.sae:3001/api/v1/auth/register \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"Test123456","name":"测试用户"}' + # 应该返回 200 OK + ``` + +--- + +### 2.2 Python微服务部署 + +☐ **本地代码准备** +- ⏱️ 5分钟 +- `cd extraction_service` +- 确保requirements.txt正确 + +☐ **修改Dockerfile(Workers限制)** +- ⏱️ 2分钟 +- 编辑 `extraction_service/Dockerfile` +- CMD设置workers=2 +- 🔗 参考:`07-关键配置补充说明.md` 第6节 + +☐ **构建Docker镜像** +- ⏱️ 10分钟(Python镜像较大) +- ```bash + cd extraction_service + docker build -t python-service:v1.0.0 . + ``` + +☐ **本地测试镜像** +- ⏱️ 5分钟 +- ```bash + docker run -p 8000:8000 python-service:v1.0.0 + curl http://localhost:8000/health + ``` + +☐ **推送到ACR** +- ⏱️ 5分钟 +- ```bash + docker tag python-service:v1.0.0 registry.cn-hangzhou.aliyuncs.com/aiclinical/python-service:v1.0.0 + docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/python-service:v1.0.0 + ``` + +☐ **创建SAE应用** +- ⏱️ 10分钟 +- 应用名称:aiclinical-python-service +- 镜像:registry.cn-hangzhou.aliyuncs.com/aiclinical/python-service:v1.0.0 +- 实例规格:1核2GB(⚠️ 不要低于2GB) +- 实例数量:最小1,最大3 + +☐ **配置环境变量** +- ⏱️ 3分钟 +- ```bash + OSS_REGION=cn-hangzhou + OSS_BUCKET=aiclinical-data-prod + OSS_ACCESS_KEY_ID=*** + OSS_ACCESS_KEY_SECRET=*** + ``` + +☐ **部署并验证** +- ⏱️ 5分钟 +- 部署应用 +- 测试:`curl http://172.16.x.x:8000/health`(使用实际内网IP) + +--- + +### 2.3 前端部署 + +☐ **本地代码准备** +- ⏱️ 10分钟 +- `cd frontend-v2` +- `npm install` +- `npm run build` + +☐ **修改nginx.conf(文件大小限制)** +- ⏱️ 2分钟 +- 编辑 `frontend-v2/nginx.conf.template` +- 添加 `client_max_body_size 50M;` +- 🔗 参考:`07-关键配置补充说明.md` 第5节 + +☐ **构建Docker镜像** +- ⏱️ 5分钟 +- ```bash + cd frontend-v2 + docker build -t frontend:v1.0.0 . + ``` + +☐ **推送到ACR** +- ⏱️ 3分钟 +- ```bash + docker tag frontend:v1.0.0 registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.0.0 + docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.0.0 + ``` + +☐ **创建SAE应用** +- ⏱️ 10分钟 +- 应用名称:aiclinical-frontend +- 镜像:registry.cn-hangzhou.aliyuncs.com/aiclinical/frontend:v1.0.0 +- 实例规格:0.5核1GB +- 实例数量:1 + +☐ **配置环境变量** +- ⏱️ 2分钟 +- ```bash + BACKEND_SERVICE_HOST=aiclinical-backend.sae + BACKEND_SERVICE_PORT=3001 + ``` + +☐ **部署并验证** +- ⏱️ 5分钟 +- 部署应用 +- 浏览器访问:http://前端公网地址 +- 应该看到登录页面 + +--- + +## 阶段3:Dify服务部署(Day 2上午) + +### 3.1 ECS服务器准备 + +☐ **创建ECS实例** +- ⏱️ 10分钟(等待创建5分钟) +- ECS控制台 > 创建实例 +- 实例规格:ecs.c6.xlarge(4核8GB) +- 镜像:Ubuntu 22.04 +- VPC:aiclinical-vpc-prod +- 交换机:aiclinical-vsw-prod +- 安全组:aiclinical-sg-prod +- 公网IP:分配(用于管理和Dify访问) +- 记录公网IP:________________ + +☐ **SSH登录ECS** +- ⏱️ 2分钟 +- ```bash + ssh root@ECS公网IP + ``` + +☐ **安装Docker** +- ⏱️ 5分钟 +- ```bash + curl -fsSL https://get.docker.com | bash + systemctl start docker + systemctl enable docker + docker --version + ``` + +☐ **安装Docker Compose** +- ⏱️ 3分钟 +- ```bash + curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + docker-compose --version + ``` + +☐ **配置Swap(防止OOM)** +- ⏱️ 5分钟 +- ```bash + fallocate -l 4G /swapfile + chmod 600 /swapfile + mkswap /swapfile + swapon /swapfile + echo '/swapfile none swap sw 0 0' >> /etc/fstab + swapon --show + ``` +- 🔗 参考:`03-Dify-ECS部署完全指南.md` 第4节 + +--- + +### 3.2 Dify部署 + +☐ **下载Dify** +- ⏱️ 5分钟 +- ```bash + cd /opt + git clone https://github.com/langgenius/dify.git + cd dify/docker + ``` + +☐ **修改docker-compose.yaml(端口安全)** +- ⏱️ 5分钟 +- 编辑 `docker-compose.yaml` +- Redis端口:`127.0.0.1:6379:6379` +- Weaviate端口:`127.0.0.1:8080:8080` +- ⚠️ 只有Nginx对外(80端口) +- 🔗 参考:`07-关键配置补充说明.md` 第4节 + +☐ **配置.env文件** +- ⏱️ 10分钟 +- ```bash + cp .env.example .env + vi .env + ``` +- 关键配置: + ```bash + # Redis + REDIS_HOST=redis + REDIS_PORT=6379 + + # PostgreSQL + DB_HOST=db + DB_PORT=5432 + DB_DATABASE=dify + DB_USERNAME=postgres + DB_PASSWORD=(生成强密码) + + # Weaviate + WEAVIATE_ENDPOINT=http://weaviate:8080 + + # 其他 + SECRET_KEY=(生成强密钥) + ``` + +☐ **启动Dify** +- ⏱️ 10分钟(首次启动需要拉取镜像) +- ```bash + docker-compose up -d + docker-compose logs -f + # 等待所有服务启动(约2-3分钟) + ``` + +☐ **验证Dify服务** +- ⏱️ 3分钟 +- 浏览器访问:http://ECS公网IP +- 应该看到Dify欢迎页面 + +--- + +### 3.3 Dify配置 + +☐ **注册管理员账号** +- ⏱️ 2分钟 +- 首次访问会提示注册 +- 邮箱:admin@your-company.com +- 密码:(强密码) + +☐ **创建API Key** +- ⏱️ 3分钟 +- 登录Dify +- 设置 > API密钥 > 创建密钥 +- 名称:Backend Service +- 复制API Key:app-xxxxxxxxxxxxxxxxxxxxx +- ⚠️ 妥善保存,只显示一次 + +☐ **更新后端环境变量** +- ⏱️ 5分钟 +- SAE控制台 > aiclinical-backend > 环境变量 +- 修改: + ```bash + DIFY_API_URL=http://172.16.x.x/v1 # ECS内网IP + DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx # 真实Key + ``` +- 保存 > 重启应用 + +☐ **测试PKB功能** +- ⏱️ 5分钟 +- ```bash + # 创建知识库 + curl -X POST http://前端地址/api/v1/pkb/knowledge-bases \ + -H "Authorization: Bearer USER_TOKEN" \ + -d '{"name":"测试知识库"}' + # 应该返回 200 OK + ``` + +--- + +## 阶段4:端到端测试(Day 2下午) + +### 4.1 功能测试 + +☐ **用户注册/登录** +- ⏱️ 5分钟 +- 前端注册新用户 +- 登录成功 + +☐ **AI对话(AIA模块)** +- ⏱️ 5分钟 +- 创建项目 +- 发起对话 +- 验证DeepSeek API调用成功 + +☐ **知识库(PKB模块)** +- ⏱️ 10分钟 +- 创建知识库 +- 上传PDF文档 +- 验证Dify处理成功 + +☐ **文献筛选(ASL模块)** +- ⏱️ 10分钟 +- 创建筛选项目 +- 上传PDF +- 验证Python服务解析成功 + +☐ **数据清洗(DC模块)** +- ⏱️ 10分钟 +- 上传Excel +- 执行清洗 +- 验证Polars处理成功 + +--- + +### 4.2 性能测试 + +☐ **响应时间** +- ⏱️ 10分钟 +- API响应时间 < 2秒 +- 页面加载时间 < 3秒 + +☐ **并发测试** +- ⏱️ 10分钟 +- 使用Apache Bench或JMeter +- 模拟10个并发用户 +- 验证无错误 + +--- + +### 4.3 监控配置 + +☐ **配置日志收集** +- ⏱️ 10分钟 +- SAE自动收集stdout/stderr +- 验证日志可查看 + +☐ **配置告警规则** +- ⏱️ 15分钟 +- 云监控 > 告警规则 +- CPU > 70% +- 内存 > 80% +- 错误日志 > 10条/分钟 + +--- + +## 阶段5:优化与文档(Day 3) + +### 5.1 可选优化 + +☐ **配置SSH隧道(开发便利)** +- ⏱️ 10分钟 +- 🔗 参考:`07-关键配置补充说明.md` 第7节 + +☐ **实现OSS签名URL(安全)** +- ⏱️ 30分钟 +- 🔗 参考:`07-关键配置补充说明.md` 第8节 + +☐ **配置域名(可选)** +- ⏱️ 30分钟 +- 购买域名 +- 配置DNS解析 +- 配置HTTPS证书 + +--- + +### 5.2 文档整理 + +☐ **记录配置信息** +- ⏱️ 20分钟 +- 创建 `部署配置清单.xlsx` +- 记录所有密码、地址、Key + +☐ **备份配置文件** +- ⏱️ 10分钟 +- Dify的 `.env` 和 `docker-compose.yaml` +- 上传到私有Git仓库或加密存储 + +☐ **编写运维手册** +- ⏱️ 30分钟 +- 日常操作流程 +- 故障处理流程 +- 联系人信息 + +--- + +## 总结 + +### 时间统计 + +| 阶段 | 预计时间 | 实际时间 | +|------|---------|---------| +| 阶段1:基础设施 | 2小时 | _______ | +| 阶段2:核心服务 | 2小时 | _______ | +| 阶段3:Dify服务 | 1小时 | _______ | +| 阶段4:测试 | 1小时 | _______ | +| 阶段5:优化 | 可选 | _______ | +| **总计** | **6小时** | _______ | + +### 成本统计 + +| 服务 | 规格 | 月成本 | +|------|------|--------| +| SAE前端 | 0.5核1GB×1 | ¥50 | +| SAE后端 | 1核2GB×2 | ¥200 | +| SAE Python | 1核2GB×1 | ¥100 | +| RDS PostgreSQL | 2核4GB | ¥400 | +| ECS Dify | 4核8GB | ¥300 | +| OSS | 50GB | ¥10 | +| NAT网关 | 小型+EIP | ¥100 | +| **总计** | - | **¥1,160/月** | + +### 下一步 + +☐ 观察7天稳定性 +☐ 根据监控数据调整资源 +☐ 准备故障演练 +☐ 编写用户手册 + +--- + +**恭喜!你已经完成了从0到1的部署!** 🎉 + +**文档创建人:** AI助手 +**最后更新:** 2025-12-14 +**版本:** v1.0 + diff --git a/docs/05-部署文档/CTO 代码审查报告:AI临床研究平台部署架构.md b/docs/05-部署文档/CTO 代码审查报告:AI临床研究平台部署架构.md new file mode 100644 index 00000000..f250c323 --- /dev/null +++ b/docs/05-部署文档/CTO 代码审查报告:AI临床研究平台部署架构.md @@ -0,0 +1,95 @@ +# **CTO 代码审查报告:AI临床研究平台部署架构** + +审核对象:5份独立部署文档 (Dify/Python/Node/Frontend/PostgreSQL) +审核人:虚拟架构师 +结论:A- (优秀)。架构设计符合云原生理念,成本与扩展性平衡良好。但存在网络出口和依赖闭环两个关键风险点需解决。 + +## **1\. 模块级深度审计 (Module-Level Audit)** + +### **✅ 1.1 前端 Nginx (SAE)** + +* **优点**:多阶段构建优秀(体积小);环境变量注入方案(envsubst)非常专业,解决了静态页面无法动态配置后端的难题。 +* **修正建议**: + * 确认 nginx.conf 中开启 gzip,这对 React 大体积 JS 文件至关重要。 + * 检查 Nginx 的 client\_max\_body\_size 配置。医疗 PDF/Excel 可能超过默认的 1MB,建议设置为 50M。 + +### **✅ 1.2 后端 Node.js (SAE)** + +* **优点**:Prisma "反向同步" 流程非常务实,解决了开发习惯不规范的问题。Postgres-Only 的架构极大地降低了运维负担。 +* **修正建议**: + * **连接泄漏风险**:Python 服务如果响应慢,后端的 HTTP Client 设置超时了吗?建议设置 timeout: 30s,防止后端连接数堆积。 + +### **✅ 1.3 Python 微服务 (SAE)** + +* **优点**:明确指出了 libGL 等系统依赖问题,这是 Python 容器化最大的坑,文档已提前规避。 +* **修正建议**: + * **OOM 风险**:Python 进程(尤其是 PyMuPDF/OCR)非常吃内存。在 2G 内存限制下,务必限制并发数(Gunicorn Workers 不要超过 2 个)。 + +### **✅ 1.4 Dify (ECS)** + +* **优点**:选择了 ECS 部署以保证数据私有化,同时使用 Swap 防止 OOM,非常懂行。 +* **修正建议**: + * **安全性**:ECS 的 Redis 端口 (6379) 和 Weaviate 端口 (8080) **绝对不要**对公网开放。仅允许 localhost 和 VPC 内网访问。 + +### **✅ 1.5 数据库 (RDS)** + +* **优点**:Schema 隔离设计极佳。 +* **修正建议**: + * **Dify 数据库隔离**:确认 Dify 使用的是独立的 dify\_prod 库,不要和业务表混在 ai\_clinical\_research 库中,防止 Dify 升级脚本误删业务表。 + +## **2\. 跨模块集成风险 (Integration Risks)** + +### **🚨 风险一:SAE 的"孤岛效应" (Internet Access)** + +* **问题**:SAE 部署在 VPC 内,默认**没有公网出口**。 +* **场景**:后端调用 DeepSeek API、Python 下载公网 PDF、NPM 安装依赖(构建时)。 +* **对策**:必须在 VPC 中配置 **NAT 网关** (推荐) 或确保 SAE 有绑定公网 IP 的能力。**否则上线当天所有 AI 功能都会超时。** + +### **🔄 风险二:部署依赖死锁 (Deployment Deadlock)** + +* **现象**: + 1. 后端启动需要 DIFY\_API\_KEY。 + 2. DIFY\_API\_KEY 需要 Dify 启动并人工登录后才能生成。 + 3. 后端如果配置了"健康检查失败则重启",在填入 Key 之前会无限重启。 +* **对策**:首次部署时,后端环境变量 DIFY\_API\_KEY 可以先填个假值(如 temp),让服务跑起来。等 Dify 部署好拿到真 Key 后,更新 SAE 配置并重启。 + +### **🌐 风险三:前端与 Dify 的跨域 (CORS)** + +* **问题**:前端直接调用后端(通过 Nginx 代理)没问题。但如果前端需要**直接嵌入** Dify 的 Web UI(如 iframe)或直接调用 Dify API(绕过后端),会遇到 CORS。 +* **对策**:坚持\*\*"所有请求走后端"\*\*的原则。前端 \-\> Nginx \-\> 后端 \-\> Dify。不要让前端直连 Dify,既安全又避免 CORS。 + +## **3\. 架构关系图谱** + +\[浏览器\] + | (HTTPS) + v +\[SAE: 前端 Nginx\] + | (反向代理 /api) + v +\[SAE: 后端 Node.js\] \--(内网HTTP)--\> \[SAE: Python 微服务\] + | | (内网HTTP) + | v + | \[ECS: Dify API\] \<--\> \[ECS: Weaviate/Redis\] + | + \+--(TCP Connection)--\> \[RDS PostgreSQL 15\] + | | (Schema: platform, asl, dc...) + | \+ (Database: dify\_prod) + | + \+--(HTTPS)--\> \[OSS 对象存储\] + | + \+--(NAT网关)--\> \[互联网: DeepSeek/OpenAI\] + +## **4\. 给 1-2 人团队的生存建议** + +1. **省钱黑科技**: + * SAE 2.0 有**闲置计费**功能。开发环境(测试环境)务必开启,没人用时不收 CPU/内存 费。 + * RDS 购买**通用型** (2核4G) 即可,不要买独享型,够用很久。 +2. **不要自建监控**: + * 直接用阿里云 **ARMS** (应用实时监控服务) 的免费额度或基础版。不要自己搭 Prometheus \+ Grafana,维护成本太高。 +3. **数据备份是底线**: + * RDS 开启自动备份(保留7天)。 + * ECS 上的 Dify docker-compose.yaml 和 .env 文件,务必在本地或 Git 私有仓库备份一份。ECS 没了可以重买,配置文件没了就得重配。 +4. **开发效率**: + * 利用 ECS 做**跳板机**,本地直连 RDS 开发。不要每次都写代码去查数据。 + +**最终结论**:这套架构设计得非常扎实,完全可以支撑从 0 到 10 万用户的规模。请重点解决 **"SAE 访问公网 (NAT)"** 这个问题,即可开始部署。 \ No newline at end of file diff --git a/docs/05-部署文档/PostgreSQL部署策略-摸底报告.md b/docs/05-部署文档/PostgreSQL部署策略-摸底报告.md new file mode 100644 index 00000000..43e3d06d --- /dev/null +++ b/docs/05-部署文档/PostgreSQL部署策略-摸底报告.md @@ -0,0 +1,1371 @@ +# PostgreSQL 15 数据库部署策略 - 摸底报告 + +> **文档版本:** v1.1 +> **创建日期:** 2025-12-14 +> **最后更新:** 2025-12-14 +> **数据库版本:** PostgreSQL 15.14 (Docker: postgres:15-alpine) +> **目标环境:** 阿里云 RDS PostgreSQL 15 +> **报告类型:** 技术摸底 + 部署策略 + +--- + +## 📝 版本修订记录 + +### v1.1 (2025-12-14) - 专业建议修正版 + +**修正要点:** + +1. **✅ pg-boss表"自愈"机制澄清** + - 修正:pg-boss会在应用启动时自动创建表,只要权限够 + - 降低严重性:从"严重差异❌"改为"自愈机制✅(无需担心)" + - 增加说明:pg-boss的智能自愈能力和权限要求 + +2. **✅ 白名单配置强化说明** + - 新增:必须使用VPC网段,不能用单机IP + - 示例:172.16.0.0/12(SAE的VPC网段) + - 原因:SAE实例IP会变化,单机IP会导致连接失败 + - 位置:RDS准备、网络安全、下一步行动多处强调 + +3. **✅ 备份策略简化(更务实)** + - 修正:初期只需RDS自动备份(含日志备份) + - 降级:pg_dump ECS脚本从"第三道防线"降为"可选(业务做大后再考虑)" + - 理由:RDS自动备份+PITR已足够,省心省力 + - 保留:脚本示例作为参考,标注"⚠️ 仅供参考,初期不需要" + +**贡献者:** 项目技术团队反馈 + +--- + +--- + +## 📋 目录 + +1. [本地数据库真实情况](#本地数据库真实情况) +2. [Prisma与数据库的差异分析](#prisma与数据库的差异分析) +3. [代码如何连接数据库](#代码如何连接数据库) +4. [首次部署方案](#首次部署方案) +5. [未来更新策略](#未来更新策略) +6. [RDS备份策略](#rds备份策略) +7. [最佳实践与禁止操作](#最佳实践与禁止操作) + +--- + +## 本地数据库真实情况 + +### 1. 基础信息 + +```bash +Docker镜像:postgres:15-alpine +数据库名称:ai_clinical_research +连接信息:postgresql://postgres:postgres@localhost:5432/ai_clinical_research +数据库大小:26 MB(测试/开发环境) +用户数据:3个用户账号 +``` + +### 2. Schema隔离架构(10个Schema)✅ + +你的数据库已经成功实施了10个Schema隔离架构: + +| # | Schema名称 | 表数量 | 状态 | 说明 | +|---|-----------|--------|------|------| +| 1 | `platform_schema` | 8个表 | ✅ 已实施 | 平台基础设施(用户、pg-boss队列) | +| 2 | `aia_schema` | 5个表 | ✅ 已实施 | AI智能问答(项目、对话、消息) | +| 3 | `pkb_schema` | 5个表 | ✅ 已实施 | 个人知识库(知识库、文档、批处理) | +| 4 | `asl_schema` | 6个表 | ✅ 已实施 | AI智能文献(文献筛选) | +| 5 | `dc_schema` | 6个表 | ✅ 已实施 | 数据清洗(模板、提取任务、Tool C) | +| 6 | `admin_schema` | 0个表 | 📋 空Schema | 运营管理(预留) | +| 7 | `rvw_schema` | 0个表 | 📋 空Schema | 审稿系统(预留) | +| 8 | `ssa_schema` | 0个表 | 📋 空Schema | 智能统计分析(预留) | +| 9 | `st_schema` | 0个表 | 📋 空Schema | 统计分析工具(预留) | +| 10 | `common_schema` | 0个表 | 📋 空Schema | 通用能力层(预留) | +| 11 | `public` | 4个表 | ⚠️ 旧表遗留 | _prisma_migrations, admin_logs, review_tasks, users | + +**总计:34个表(30个在隔离Schema中,4个在public中)** + +### 3. 详细表清单 + +#### 3.1 platform_schema(8个表) + +``` +✅ users - 用户表(3条记录) +✅ app_cache - Postgres-Only缓存(替代Redis) +✅ job - pg-boss任务表 +✅ job_common - pg-boss任务通用表 +✅ queue - pg-boss队列表 +✅ schedule - pg-boss定时任务表 +✅ subscription - pg-boss订阅表 +✅ version - pg-boss版本表 +``` + +**⚠️ 重要发现:pg-boss的6个表(job/queue等)是自动创建的,不在Prisma Schema中!** + +#### 3.2 aia_schema(5个表) + +``` +✅ projects - 项目管理(2条记录) +✅ conversations - 项目对话 +✅ messages - 对话消息 +✅ general_conversations - 通用对话 +✅ general_messages - 通用消息 +``` + +#### 3.3 pkb_schema(5个表) + +``` +✅ knowledge_bases - 知识库(2条记录) +✅ documents - 文档 +✅ batch_tasks - 批处理任务 +✅ batch_results - 批处理结果(976 KB) +✅ task_templates - 任务模板 +``` + +#### 3.4 asl_schema(6个表) + +``` +✅ screening_projects - 文献筛选项目(18条记录) +✅ literatures - 文献数据(2.9 MB) +✅ screening_results - 筛选结果(1.2 MB) +✅ screening_tasks - 筛选任务 +✅ fulltext_screening_tasks - 全文筛选任务 +✅ fulltext_screening_results - 全文筛选结果 +``` + +#### 3.5 dc_schema(6个表) + +``` +✅ dc_health_checks - 健康检查 +✅ dc_templates - 数据清洗模板(4条记录,424 KB) +✅ dc_extraction_tasks - 提取任务(728 KB) +✅ dc_extraction_items - 提取项(6.5 MB,最大表) +✅ dc_tool_c_sessions - Tool C会话(960 KB) +✅ dc_tool_c_ai_history - Tool C AI历史(1 MB) +``` + +#### 3.6 public schema(4个表)⚠️ + +``` +⚠️ _prisma_migrations - Prisma迁移记录(6条记录) +⚠️ admin_logs - 管理日志(遗留) +⚠️ review_tasks - 审查任务(632 KB,遗留) +⚠️ users - 用户表(遗留,与platform_schema.users重复) +``` + +**说明:** +- `public.users` 是Schema迁移前的旧表,与 `platform_schema.users` 结构完全相同 +- `admin_logs` 和 `review_tasks` 应该迁移到 `admin_schema` 和 `rvw_schema` +- `_prisma_migrations` 应该保留在 `public`,这是Prisma的标准位置 + +### 4. 数据量统计(Top 15大表) + +| Schema | 表名 | 大小 | 说明 | +|--------|------|------|------| +| dc_schema | dc_extraction_items | 6.5 MB | 数据清洗提取项(最大表) | +| asl_schema | literatures | 2.9 MB | 文献数据 | +| asl_schema | screening_results | 1.2 MB | 筛选结果 | +| dc_schema | dc_tool_c_ai_history | 1 MB | Tool C AI历史 | +| pkb_schema | batch_results | 976 KB | 批处理结果 | +| dc_schema | dc_tool_c_sessions | 960 KB | Tool C会话 | +| dc_schema | dc_extraction_tasks | 728 KB | 提取任务 | +| public | review_tasks | 632 KB | 审查任务(遗留) | +| dc_schema | dc_templates | 424 KB | 数据清洗模板 | +| pkb_schema | documents | 296 KB | 文档 | +| aia_schema | general_messages | 208 KB | 通用消息 | +| asl_schema | screening_projects | 144 KB | 筛选项目 | +| aia_schema | conversations | 112 KB | 对话 | +| pkb_schema | batch_tasks | 112 KB | 批处理任务 | +| aia_schema | messages | 104 KB | 消息 | + +**总结:数据主要集中在 DC(数据清洗)和 ASL(文献筛选)模块。** + +### 5. Prisma迁移历史 + +```sql +-- 6次Prisma迁移记录(按时间顺序) +20251010075003_init -- 2025-10-12(初始化) +20251010122727_add_conversation_metadata -- 2025-10-12(对话元数据) +20251012124747_add_batch_processing_module -- 2025-10-12(批处理模块) +20251014120128_add_review_tasks -- 2025-10-14(审查任务) +20251127_add_dc_tool_b_tables -- 2025-11-27(DC Tool B) +20251208_add_column_mapping -- 2025-12-08(列映射) +``` + +**说明:** +- 最早的迁移(init)创建的是 `public` schema 中的表 +- 后续通过手工SQL迁移到了10个隔离Schema +- Prisma并没有记录Schema迁移过程(这些是手工SQL完成的) + +--- + +## Prisma与数据库的差异分析 + +### 1. pg-boss表的"自愈"机制✅(无需担心) + +**现象:** +`platform_schema` 有 8 个表,但 Prisma Schema 只定义了 2 个: +- ✅ Prisma定义:`AppCache`, `User` +- 🔧 pg-boss自动管理:`job`, `job_common`, `queue`, `schedule`, `subscription`, `version`(共6个表) + +**原因:** +这6个表是 `pg-boss` 库在**应用启动时自动创建**的(用于Postgres-Only任务队列),不需要在Prisma Schema中定义。 + +**pg-boss的"自愈"能力:** +```typescript +// backend启动时,pg-boss会自动检查表是否存在 +const boss = new PgBoss(process.env.DATABASE_URL) +await boss.start() +// ✅ 如果表不存在,pg-boss会自动创建 +// ✅ 只要数据库用户(如aiclinical_rw)有 CREATE TABLE 权限 +// ✅ 完全不需要手工干预 +``` + +**影响评估:** +- ✅ **运行时零影响**:pg-boss自己管理这些表,不通过Prisma访问 +- ✅ **首次部署自动创建**:RDS部署时,backend启动会自动创建这6个表 +- ⚠️ **Prisma db pull会检测到**:执行 `npx prisma db pull` 时会发现"未定义的表"(可忽略) +- ⚠️ **Prisma migrate不操作**:迁移不会同步这些表(这是正确的) + +**最佳实践:** +```prisma +// backend/prisma/schema.prisma +// 在文件开头添加注释(文档用途) + +// ==================== pg-boss 自动管理的表(不需要定义) ==================== +// 以下6个表由pg-boss库自动创建和维护,请勿手工修改: +// - platform_schema.job - 任务表 +// - platform_schema.job_common - 任务通用配置 +// - platform_schema.queue - 队列表 +// - platform_schema.schedule - 定时任务表 +// - platform_schema.subscription - 订阅表 +// - platform_schema.version - 版本表 +// ==================== 以上表自动管理,无需Prisma定义 ==================== +``` + +**结论:pg-boss表"缺失"是正常的,无需担心!** + +### 2. public schema遗留表⚠️(需要清理) + +**问题:** +`public` schema 有 4 个表: +- `_prisma_migrations`:Prisma标准表,正确✅ +- `users`:与 `platform_schema.users` 重复❌ +- `admin_logs`:应迁移到 `admin_schema`❌ +- `review_tasks`:应迁移到 `rvw_schema`❌ + +**影响:** +- ⚠️ 数据不一致风险(如果代码误读 `public.users`) +- ⚠️ 混淆(两个 `users` 表) +- ⚠️ 空间浪费(重复数据) + +**解决方案:** +```sql +-- 步骤1:迁移 admin_logs 到 admin_schema +ALTER TABLE public.admin_logs SET SCHEMA admin_schema; + +-- 步骤2:迁移 review_tasks 到 rvw_schema +ALTER TABLE public.review_tasks SET SCHEMA rvw_schema; + +-- 步骤3:验证 platform_schema.users 和 public.users 数据一致后删除 +-- (先在生产环境验证一周,确保代码不再引用 public.users) +DROP TABLE public.users; +``` + +### 3. Prisma Schema的定义状态 + +| Schema | Prisma定义的模型 | 实际数据库表 | 状态 | +|--------|-----------------|-------------|------| +| platform_schema | 2个(AppCache, User) | 8个(+6个pg-boss自动管理) | ✅ 正常(pg-boss自愈) | +| aia_schema | 5个 | 5个 | ✅ 一致 | +| pkb_schema | 5个 | 5个 | ✅ 一致 | +| asl_schema | 6个 | 6个 | ✅ 一致 | +| dc_schema | 6个 | 6个 | ✅ 一致 | +| public | 2个(AdminLog, ReviewTask) | 4个(+_prisma_migrations, users) | ⚠️ 需清理 | + +**结论:** +- ✅ **Prisma Schema 完全准确**:pg-boss的6个表会自动创建,不需要定义 +- ⚠️ **public schema需要清理**:遗留了旧的 `users` 表(首次部署后清理) + +--- + +## 代码如何连接数据库 + +### 1. 环境变量配置 + +#### 1.1 默认连接字符串(env.ts) + +```typescript +// AIclinicalresearch/backend/src/config/env.ts (line 50) +databaseUrl: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/ai_clinical' +``` + +**⚠️ 注意:** +- 默认值是 `ai_clinical`(不存在) +- 实际数据库名是 `ai_clinical_research` +- 说明你的 `.env` 文件中已经正确配置了 `DATABASE_URL` + +#### 1.2 连接池配置(database.ts) + +```typescript +// AIclinicalresearch/backend/src/config/database.ts +export const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL, // 从环境变量读取 + }, + }, +}) + +// 连接池计算公式 +connectionLimit = Math.floor(RDS_MAX_CONNECTIONS / MAX_INSTANCES) - 10 +// 示例:400 / 20 - 10 = 10个连接/实例 +``` + +**云原生连接池策略:** +``` +DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=10 +``` + +### 2. 连接方式 + +| 连接参数 | 本地开发 | 阿里云SAE | +|---------|---------|----------| +| **主机** | localhost | RDS内网地址(如 rm-xxxxx.pg.rds.aliyuncs.com) | +| **端口** | 5432 | 5432 | +| **数据库** | ai_clinical_research | ai_clinical_research(保持一致) | +| **用户名** | postgres | 自定义(如 aiclinical_rw) | +| **密码** | postgres | 强密码(RDS创建时设置) | +| **连接限制** | 无限制 | connection_limit=10(SAE每实例) | +| **连接超时** | 默认 | pool_timeout=10 | + +### 3. 多Schema访问(Prisma) + +```prisma +// AIclinicalresearch/backend/prisma/schema.prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + schemas = ["platform_schema", "aia_schema", "pkb_schema", "asl_schema", + "common_schema", "dc_schema", "rvw_schema", "admin_schema", + "ssa_schema", "st_schema", "public"] +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema"] // 启用多Schema支持 +} +``` + +**代码中的使用:** +```typescript +// 自动路由到正确的Schema +await prisma.user.findMany() // 访问 platform_schema.users +await prisma.project.findMany() // 访问 aia_schema.projects +``` + +### 4. 原生SQL访问(跨Schema) + +```typescript +// 支持跨Schema查询 +const result = await prisma.$queryRaw` + SELECT u.name, p.name AS project_name + FROM platform_schema.users u + JOIN aia_schema.projects p ON p.user_id = u.id +` +``` + +### 5. 优雅关闭机制 + +```typescript +// AIclinicalresearch/backend/src/config/database.ts +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) // SAE实例停止 +process.on('SIGINT', () => gracefulShutdown('SIGINT')) // Ctrl+C + +async function gracefulShutdown(signal: string): Promise { + await prisma.$disconnect() // 关闭所有连接 + process.exit(0) +} +``` + +**这是云原生最佳实践**,防止SAE扩容/缩容时连接泄漏。 + +--- + +## 首次部署方案 + +### 方案对比 + +| 方案 | 方法 | 优点 | 缺点 | 推荐度 | +|------|------|------|------|--------| +| **方案A** | `pg_dump` 全量导入 | ✅ 100%完整(包括pg-boss表)
✅ 结构+数据+索引+外键
✅ 一次性完成 | ❌ 需要手工清理测试数据
❌ 包含遗留的public表 | ⭐⭐⭐⭐⭐ **强烈推荐** | +| **方案B** | Prisma Migrate Deploy | ✅ 版本化管理
✅ 可重复执行 | ❌ 缺少pg-boss表(运行时会报错)
❌ 需要手工补充 | ⚠️ **不推荐**(不完整) | +| **方案C** | 手工SQL脚本 | ✅ 完全可控 | ❌ 工作量大
❌ 容易出错 | ⚠️ **不推荐**(费时费力) | + +### ⭐ 推荐方案A:pg_dump全量导入(详细步骤) + +#### 步骤1:本地数据库清理(可选) + +```bash +# 如果本地有测试数据,可以选择清理 +docker exec -it ai-clinical-postgres psql -U postgres -d ai_clinical_research + +# 删除测试用户(保留真实用户) +DELETE FROM platform_schema.users WHERE email LIKE '%test%'; + +# 清理遗留的 public.users(Schema迁移后的旧表) +DROP TABLE IF EXISTS public.users; +``` + +#### 步骤2:导出数据库(包含结构+数据) + +```bash +# 完整导出(包括所有Schema、表、数据、索引、外键) +docker exec ai-clinical-postgres pg_dump -U postgres -d ai_clinical_research \ + --format=plain \ + --no-owner \ + --no-acl \ + --encoding=UTF8 \ + > D:\MyCursor\ai_clinical_research_backup_$(Get-Date -Format 'yyyyMMdd_HHmmss').sql + +# 文件大小应该在 100-200 KB(当前数据量26MB) +``` + +**参数说明:** +- `--format=plain`:纯文本SQL(方便查看和编辑) +- `--no-owner`:不导出所有者信息(避免RDS用户名不匹配) +- `--no-acl`:不导出权限信息(使用RDS默认权限) +- `--encoding=UTF8`:UTF-8编码(中文支持) + +#### 步骤3:RDS PostgreSQL 15 准备 + +```bash +# 1. 在阿里云控制台创建RDS PostgreSQL 15实例 +规格:rds.pg.s2.large(2核4GB,入门级) +存储:100GB SSD(支持自动扩容) +版本:PostgreSQL 15 +网络:VPC(与SAE在同一VPC) +白名单:⚠️ 必须配置VPC网段,不能用单机IP! + - 示例:172.16.0.0/12(SAE的VPC网段) + - 查看方式:SAE控制台 > 应用详情 > 网络配置 > VPC网段 + - ❌ 错误示例:172.16.1.23(单机IP,SAE实例IP会变化) + +# 2. 创建数据库 +CREATE DATABASE ai_clinical_research + WITH ENCODING='UTF8' + LC_COLLATE='en_US.UTF-8' + LC_CTYPE='en_US.UTF-8' + TEMPLATE=template0; + +# 3. 创建应用用户(不要用超级用户) +CREATE USER aiclinical_rw WITH PASSWORD '你的强密码'; +GRANT ALL PRIVILEGES ON DATABASE ai_clinical_research TO aiclinical_rw; +GRANT ALL ON SCHEMA public TO aiclinical_rw; +``` + +#### 步骤4:导入到RDS + +```bash +# 方法1:从本地直接导入(需要RDS公网地址) +psql -h rm-xxxxx.pg.rds.aliyuncs.com \ + -p 5432 \ + -U aiclinical_rw \ + -d ai_clinical_research \ + -f ai_clinical_research_backup_20251214_150000.sql + +# 方法2:通过ECS跳板机导入(推荐,更安全) +scp backup.sql root@your-ecs-ip:/tmp/ +ssh root@your-ecs-ip +psql -h rm-xxxxx.pg.rds.aliyuncs.com -U aiclinical_rw -d ai_clinical_research -f /tmp/backup.sql +``` + +#### 步骤5:验证导入结果 + +```sql +-- 1. 验证Schema数量 +SELECT nspname FROM pg_namespace WHERE nspname LIKE '%_schema' ORDER BY nspname; +-- 应该有10个Schema + +-- 2. 验证表数量 +SELECT schemaname, COUNT(*) as table_count +FROM pg_tables +WHERE schemaname NOT IN ('pg_catalog', 'information_schema') +GROUP BY schemaname +ORDER BY schemaname; +-- platform_schema: 8, aia_schema: 5, pkb_schema: 5, asl_schema: 6, dc_schema: 6 + +-- 3. 验证数据量 +SELECT COUNT(*) FROM platform_schema.users; -- 应该有3条(或你保留的数量) +SELECT COUNT(*) FROM aia_schema.projects; -- 应该有2条 +SELECT COUNT(*) FROM asl_schema.literatures; -- 应该有对应数量 + +-- 4. 验证pg-boss表(关键) +SELECT tablename FROM pg_tables WHERE schemaname = 'platform_schema' ORDER BY tablename; +-- 应该包括:job, job_common, queue, schedule, subscription, version + +-- 5. 验证外键约束 +SELECT conname, conrelid::regclass, confrelid::regclass +FROM pg_constraint +WHERE contype = 'f' +LIMIT 10; +``` + +#### 步骤6:配置SAE环境变量 + +```bash +# 在SAE应用配置中添加 +DATABASE_URL=postgresql://aiclinical_rw:强密码@rm-xxxxx.pg.rds.aliyuncs.com:5432/ai_clinical_research?connection_limit=10&pool_timeout=10&connect_timeout=10 + +# 其他相关配置 +DB_MAX_CONNECTIONS=400 # RDS最大连接数 +MAX_INSTANCES=20 # SAE最大实例数 +NODE_ENV=production +``` + +#### 步骤7:应用启动验证 + +```typescript +// backend启动时会自动执行 +npm run build +npm run start + +// 日志输出应该显示: +// ✅ 数据库连接成功! +// 📊 数据库版本: PostgreSQL 15.x +// 📊 当前数据库连接数: 1 +``` + +#### 步骤8:Prisma Client生成(重要) + +```bash +# 在backend目录下 +cd backend +npx prisma generate + +# 验证生成的Client +ls node_modules/.prisma/client/ +# 应该包含所有Schema的类型定义 +``` + +### ⚠️ 方案B为什么不推荐:Prisma Migrate的局限 + +```bash +# 如果执行 prisma migrate deploy +cd backend +npx prisma migrate deploy + +# 问题1:缺少 pg-boss 表 +# prisma migrate deploy 只会创建 Prisma Schema 中定义的表 +# 不会创建 pg-boss 的 6 个表(job, queue等) + +# 问题2:应用启动时报错 +# Error: relation "platform_schema.job" does not exist +# 因为代码中使用了 pg-boss,但表不存在 + +# 问题3:需要手工补救 +# 必须手工执行 pg-boss 的初始化SQL +# 非常繁琐且容易出错 +``` + +**结论:`pg_dump` 全量导入是首次部署的最佳方案!** + +--- + +## 未来更新策略 + +### 场景1:新增一个表或修改一个字段(小更新) + +#### 推荐方案:Prisma Migrate(版本化管理) + +```bash +# 步骤1:在本地开发环境修改 schema.prisma +# 示例:为 User 表添加一个字段 +model User { + id String @id @default(uuid()) + email String @unique + // ... 其他字段 ... + phoneNumber String? @map("phone_number") // 新增字段 + + @@map("users") + @@schema("platform_schema") +} + +# 步骤2:创建迁移(本地) +cd backend +npx prisma migrate dev --name add_user_phone_number + +# 生成的迁移文件示例: +# backend/prisma/migrations/20251214120000_add_user_phone_number/migration.sql +ALTER TABLE "platform_schema"."users" ADD COLUMN "phone_number" TEXT; + +# 步骤3:测试本地迁移 +npm run dev +# 验证新字段可用 + +# 步骤4:提交到Git +git add backend/prisma/migrations/20251214120000_add_user_phone_number +git commit -m "feat: add phone_number to User model" +git push + +# 步骤5:部署到SAE(自动执行迁移) +# 在 SAE 的应用启动命令中添加: +# CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"] + +# ⚠️ 注意:Dockerfile 的 CMD 应该是: +CMD ["sh", "-c", "node dist/index.js"] +# 不要在生产启动时执行 migrate deploy(风险太大) + +# 步骤6:手工执行迁移(推荐) +# 方法1:通过ECS跳板机执行 +ssh root@your-ecs-ip +psql -h rm-xxxxx.pg.rds.aliyuncs.com -U aiclinical_rw -d ai_clinical_research \ + -f migrations/20251214120000_add_user_phone_number/migration.sql + +# 方法2:使用 RDS 控制台的 SQL 窗口 +# 直接粘贴迁移SQL执行 + +# 步骤7:验证 +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'platform_schema' + AND table_name = 'users' + AND column_name = 'phone_number'; +``` + +**为什么不推荐在启动命令中执行 `prisma migrate deploy`?** +1. **并发风险**:SAE有多个实例,可能同时执行迁移 +2. **回滚困难**:如果迁移失败,应用已经启动 +3. **数据安全**:生产数据库应该手工迁移,有备份和回滚计划 + +### 场景2:新增一个大模块(如SSA智能统计分析) + +#### 推荐方案:结构化迁移流程 + +```bash +# 步骤1:在本地开发环境设计Schema +# 示例:SSA(智能统计分析)模块 +# backend/prisma/schema.prisma + +// SSA Schema(新增) +model SsaAnalysisTask { + id String @id @default(uuid()) + userId String @map("user_id") + projectId String @map("project_id") + taskName String @map("task_name") + status String @default("pending") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + results SsaAnalysisResult[] + + @@map("analysis_tasks") + @@schema("ssa_schema") +} + +model SsaAnalysisResult { + id String @id @default(uuid()) + taskId String @map("task_id") + resultType String @map("result_type") + resultData Json @map("result_data") + createdAt DateTime @default(now()) @map("created_at") + + task SsaAnalysisTask @relation(fields: [taskId], references: [id]) + + @@map("analysis_results") + @@schema("ssa_schema") +} + +# 步骤2:创建迁移 +npx prisma migrate dev --name add_ssa_module + +# 步骤3:验证迁移SQL +# backend/prisma/migrations/20251214_add_ssa_module/migration.sql +-- 检查: +-- 1. 是否在 ssa_schema 中创建表 +-- 2. 外键是否正确引用 platform_schema.users +-- 3. 索引是否完整 + +# 步骤4:本地测试 +# 开发SSA模块的业务逻辑 +# 确保所有CRUD操作正常 + +# 步骤5:准备生产部署 +# 5.1 数据库备份(强制) +# 在RDS控制台手动创建快照:ai_clinical_research_before_ssa_module_20251214 + +# 5.2 准备回滚脚本 +# backend/prisma/migrations/20251214_add_ssa_module/rollback.sql +DROP TABLE IF EXISTS ssa_schema.analysis_results CASCADE; +DROP TABLE IF EXISTS ssa_schema.analysis_tasks CASCADE; + +# 步骤6:生产环境迁移(分步执行) +# 6.1 停机窗口(可选,如果无法零停机) +# 晚上11点-凌晨2点,用户最少 + +# 6.2 执行迁移SQL +psql -h rm-xxxxx.pg.rds.aliyuncs.com -U aiclinical_rw -d ai_clinical_research \ + -f migrations/20251214_add_ssa_module/migration.sql + +# 6.3 验证表结构 +\dt ssa_schema.* +\d ssa_schema.analysis_tasks +\d ssa_schema.analysis_results + +# 6.4 验证外键 +SELECT conname, conrelid::regclass, confrelid::regclass +FROM pg_constraint +WHERE connamespace = 'ssa_schema'::regnamespace; + +# 步骤7:部署应用代码 +# 7.1 更新backend +git pull +npm run build +# SAE自动部署或手动触发部署 + +# 7.2 健康检查 +curl https://your-backend.sae.aliyuncs.com/health +# 应该返回 200 OK + +# 步骤8:烟雾测试(Smoke Testing) +# 8.1 测试SSA模块基本功能 +POST /api/v1/ssa/analysis-tasks +GET /api/v1/ssa/analysis-tasks/:id + +# 8.2 测试旧模块(回归测试) +GET /api/v1/aia/projects +GET /api/v1/pkb/knowledge-bases +# 确保旧功能不受影响 + +# 步骤9:监控24小时 +# 9.1 RDS监控 +# - 连接数是否正常 +# - CPU/内存使用率 +# - 慢查询日志 + +# 9.2 SAE监控 +# - 应用日志(错误数量) +# - API响应时间 +# - 实例健康状态 + +# 步骤10:如果失败,立即回滚 +# 10.1 回滚数据库 +psql -h rm-xxxxx.pg.rds.aliyuncs.com -U aiclinical_rw -d ai_clinical_research \ + -f migrations/20251214_add_ssa_module/rollback.sql + +# 10.2 回滚应用代码 +# 在SAE控制台选择上一个版本,点击"回滚" + +# 10.3 恢复RDS快照(如果必要) +# 在RDS控制台恢复快照:ai_clinical_research_before_ssa_module_20251214 +``` + +### 场景3:紧急修复(Hotfix) + +```bash +# 示例:production环境发现某个字段长度不够 + +# 步骤1:本地创建紧急迁移 +# backend/prisma/schema.prisma +model User { + // 将 email 字段长度从 varchar(255) 改为 text + email String @unique // Prisma默认是text,无需修改 + // ... +} + +# 步骤2:生成迁移 +npx prisma migrate dev --name hotfix_user_email_length + +# 步骤3:测试迁移SQL(本地) +# 确保不会丢失数据 + +# 步骤4:生产环境执行(快速通道) +# 4.1 备份(快照) +# RDS控制台 > 备份 > 立即备份 > 备注:hotfix_before_email_length + +# 4.2 执行SQL(通过SQL窗口,秒级完成) +ALTER TABLE platform_schema.users ALTER COLUMN email TYPE TEXT; + +# 4.3 验证 +SELECT column_name, data_type, character_maximum_length +FROM information_schema.columns +WHERE table_schema = 'platform_schema' + AND table_name = 'users' + AND column_name = 'email'; + +# 4.4 提交Git +git add backend/prisma/migrations/20251214_hotfix_user_email_length +git commit -m "hotfix: increase user email column length" +git push +``` + +--- + +## RDS备份策略 + +### 1. 阿里云RDS自动备份(强烈推荐)✅ + +#### 1.1 配置自动备份 + +```bash +# 在RDS控制台配置 +备份设置 > 备份策略 + +# 推荐配置: +数据备份保留时间:7天(免费额度) +日志备份保留时间:7天 +备份周期:每天一次 +备份时间:凌晨2:00-4:00(业务低峰期) +备份方式:物理备份(快,占用空间小) +``` + +**优势:** +- ✅ 全自动,无需人工干预 +- ✅ 时间点恢复(Point-in-Time Recovery,PITR),可恢复到任意秒 +- ✅ 存储在OSS,独立于RDS实例 +- ✅ 免费(7天内) + +#### 1.2 手动快照(重要操作前) + +```bash +# 场景: +# - 大版本升级前 +# - 数据库Schema重大变更前 +# - 删除大量数据前 + +# 操作: +RDS控制台 > 备份恢复 > 备份实例 > 创建备份 +备份方式:物理备份 +备注:migration_before_ssa_module_20251214 +``` + +**快照保留建议:** +- 迁移前快照:保留30天 +- 版本升级前快照:保留60天 +- 季度归档快照:保留1年 + +### 2. 逻辑备份(`pg_dump`)- 可选策略(业务做大后再考虑) + +**⚠️ 初期建议:不需要自己写脚本,RDS自动备份就够了!** + +| 阶段 | 备份策略 | 理由 | +|------|---------|------| +| **初期(当前)** | ✅ 只用RDS自动备份 | - 自动备份+日志备份=PITR(任意时间点恢复)
- 阿里云托管,稳定可靠
- 无需维护脚本和ECS | +| **成长期** | 可选:增加异地灾备 | - 业务关键后,考虑多地域备份
- 使用RDS的"跨地域备份"功能(一键配置) | +| **成熟期** | 可选:自定义备份 | - 需要特殊备份策略(如每小时备份)
- 需要长期归档(如5年) | + +**如果未来需要自定义备份,参考脚本:** +```bash +# ⚠️ 仅供参考,初期不需要执行 +#!/bin/bash +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/data/pg_backups" +RDS_HOST="rm-xxxxx.pg.rds.aliyuncs.com" +RDS_USER="aiclinical_rw" +RDS_DB="ai_clinical_research" + +# 导出SQL +pg_dump -h $RDS_HOST -U $RDS_USER -d $RDS_DB \ + --format=plain \ + --no-owner \ + --no-acl \ + --encoding=UTF8 \ + --file=$BACKUP_DIR/ai_clinical_research_$DATE.sql + +# 压缩并上传到OSS +gzip $BACKUP_DIR/ai_clinical_research_$DATE.sql +ossutil cp $BACKUP_DIR/ai_clinical_research_$DATE.sql.gz \ + oss://your-bucket/database-backups/ + +# 清理本地备份 +ls -t $BACKUP_DIR/*.sql.gz | tail -n +4 | xargs rm -f +``` + +**总结:初期专注RDS自动备份,省心省力!** + +### 3. 恢复演练(每季度一次) + +```bash +# 目的:验证备份可用,熟悉恢复流程 + +# 步骤1:创建测试RDS实例 +# RDS控制台 > 创建实例 > 按备份创建 +# 选择最新的自动备份或快照 + +# 步骤2:验证数据完整性 +psql -h test-rm-xxxxx.pg.rds.aliyuncs.com -U aiclinical_rw -d ai_clinical_research + +# 检查: +SELECT schemaname, COUNT(*) FROM pg_tables +WHERE schemaname NOT IN ('pg_catalog', 'information_schema') +GROUP BY schemaname; + +SELECT COUNT(*) FROM platform_schema.users; + +# 步骤3:记录恢复时间 +# 26MB数据库,完整恢复约5-10分钟 + +# 步骤4:删除测试实例 +``` + +### 4. 备份监控与告警 + +```bash +# 在阿里云监控配置告警规则 + +# 规则1:备份失败告警 +资源:RDS实例 +指标:备份任务状态 +条件:备份失败 +通知:钉钉群 + 邮件 + +# 规则2:备份空间不足告警 +资源:RDS实例 +指标:备份使用空间 +条件:>80% +通知:邮件 + +# 规则3:超过24小时未备份告警 +资源:RDS实例 +指标:最后备份时间 +条件:>24小时 +通知:紧急(短信+电话) +``` + +--- + +## 最佳实践与禁止操作 + +### ✅ 最佳实践(11条黄金法则) + +#### 1. 连接池管理 + +```typescript +// ✅ 正确:在 DATABASE_URL 中配置连接限制 +DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=10 + +// ✅ 正确:根据SAE实例数动态计算 +connectionLimit = Math.floor(400 / 20) - 10 = 10 + +// ❌ 错误:不配置连接限制(会耗尽RDS连接) +DATABASE_URL=postgresql://user:pass@host:5432/db +``` + +#### 2. 优雅关闭 + +```typescript +// ✅ 正确:监听 SIGTERM 信号 +process.on('SIGTERM', async () => { + await prisma.$disconnect() + process.exit(0) +}) + +// ❌ 错误:不关闭连接(SAE缩容时连接泄漏) +``` + +#### 3. 事务管理 + +```typescript +// ✅ 正确:使用Prisma事务 +await prisma.$transaction(async (tx) => { + await tx.user.create({ data: { email: 'test@example.com' } }) + await tx.project.create({ data: { userId: 'xxx', name: 'Test' } }) +}) + +// ❌ 错误:手动管理事务(容易忘记COMMIT) +await prisma.$executeRaw`BEGIN` +// ... 多个操作 ... +await prisma.$executeRaw`COMMIT` // 如果中间出错,不会回滚 +``` + +#### 4. Schema隔离访问 + +```typescript +// ✅ 正确:Prisma自动路由到正确的Schema +await prisma.user.findMany() // 自动访问 platform_schema.users + +// ✅ 正确:跨Schema查询(外键关系) +await prisma.user.findUnique({ + where: { id: 'xxx' }, + include: { projects: true } // 自动JOIN aia_schema.projects +}) + +// ❌ 错误:手写Schema名称(Prisma不推荐) +await prisma.$queryRaw`SELECT * FROM platform_schema.users` // 可以工作,但类型不安全 +``` + +#### 5. 索引优化 + +```sql +-- ✅ 正确:为频繁查询的字段创建索引 +CREATE INDEX idx_users_email ON platform_schema.users(email); +CREATE INDEX idx_projects_user_id ON aia_schema.projects(user_id); + +-- ✅ 正确:为外键创建索引(Prisma不自动创建) +CREATE INDEX idx_conversations_project_id ON aia_schema.conversations(project_id); + +-- ❌ 错误:创建过多索引(影响写入性能) +-- 只为频繁查询的字段创建索引 +``` + +#### 6. 迁移测试 + +```bash +# ✅ 正确:先在测试环境验证 +# 测试环境 > 预发环境 > 生产环境 + +# ✅ 正确:准备回滚脚本 +# 每个迁移都有对应的rollback.sql + +# ❌ 错误:直接在生产环境执行未测试的迁移 +``` + +#### 7. 备份验证 + +```bash +# ✅ 正确:每季度恢复演练 +# 确保备份可用 + +# ✅ 正确:重要操作前手动快照 +# 大版本升级、Schema变更前 + +# ❌ 错误:从不验证备份(备份失效时才发现) +``` + +#### 8. 监控告警 + +```bash +# ✅ 正确:监控关键指标 +- RDS连接数(告警阈值:80%) +- 慢查询(告警阈值:>1秒) +- 死锁(告警阈值:>0) +- CPU使用率(告警阈值:>70%) + +# ❌ 错误:不监控(问题发生后才发现) +``` + +#### 9. 密码管理 + +```bash +# ✅ 正确:使用强密码(16位+大小写+数字+符号) +DATABASE_URL=postgresql://user:Abc123!@#XYZ456@host:5432/db + +# ✅ 正确:定期轮换密码(每6个月) + +# ❌ 错误:使用弱密码(postgres/123456) +``` + +#### 10. 网络安全 + +```bash +# ✅ 正确:RDS只允许VPC内网访问 +# 白名单配置:必须使用VPC网段,不能用单机IP +白名单示例:172.16.0.0/12(SAE的VPC网段) +获取方式:SAE控制台 > 应用详情 > 网络配置 > VPC网段 + +# ✅ 正确:不开放公网访问(除非临时调试) + +# ❌ 错误1:白名单配置 0.0.0.0/0(全世界可访问) +# ❌ 错误2:白名单配置单机IP 172.16.1.23(SAE实例IP会变化,导致连接失败) +# ❌ 错误3:配置多个单机IP(维护困难,且无法应对SAE弹性扩容) +``` + +#### 11. pg-boss表管理 + +```typescript +// ✅ 正确:不要在Prisma Schema中定义pg-boss表 +// pg-boss自动管理,不需要手工定义 + +// ✅ 正确:不要手工修改pg-boss表 +// 可能导致任务队列异常 + +// ❌ 错误:删除pg-boss表 +// DROP TABLE platform_schema.job; // 会导致应用崩溃 +``` + +#### 12. 时区统一配置 ⭐⭐⭐⭐⭐ + +```sql +-- ✅ 正确:RDS时区配置为 Asia/Shanghai +-- RDS控制台 > 参数设置 > timezone +timezone = Asia/Shanghai + +-- ✅ 验证时区 +SHOW timezone; +-- 应该显示:Asia/Shanghai + +-- ❌ 错误:使用UTC时区(与应用时区不一致) +-- 后果: +-- 1. 日志时间对不上(前端14:00,数据库06:00) +-- 2. pg-boss定时任务在错误时间触发 +-- 3. 用户看到的时间戳错误 +``` + +**配置步骤:** +```bash +# 步骤1:登录RDS控制台 +# 阿里云 > 云数据库RDS > PostgreSQL实例 + +# 步骤2:修改参数 +# 实例详情 > 参数设置 > 搜索 "timezone" +# 修改为:Asia/Shanghai + +# 步骤3:重启实例(参数修改需要重启) +# 实例详情 > 重启实例 + +# 步骤4:验证 +psql -h rm-xxxxx.pg.rds.aliyuncs.com -U aiclinical_rw -d ai_clinical_research +SHOW timezone; +# 应该显示:Asia/Shanghai +``` + +**与应用时区保持一致:** +```bash +# Node.js后端:ENV TZ=Asia/Shanghai +# Python微服务:ENV TZ=Asia/Shanghai +# 前端Nginx:ENV TZ=Asia/Shanghai +# RDS PostgreSQL:timezone = Asia/Shanghai +# ✅ 所有服务时区统一,避免时间混乱 +``` + +--- + +### ❌ 绝对禁止的操作(10条红线) + +#### 🚫 1. 禁止在生产环境直接执行 `DROP TABLE` + +```sql +-- ❌ 绝对禁止 +DROP TABLE platform_schema.users; -- 丢失所有用户数据,无法恢复 + +-- ✅ 正确做法:先备份,再重命名,观察7天后删除 +ALTER TABLE platform_schema.users RENAME TO users_deprecated_20251214; +-- 7天后确认无问题,再删除 +DROP TABLE platform_schema.users_deprecated_20251214; +``` + +#### 🚫 2. 禁止在生产环境执行未测试的迁移 + +```bash +# ❌ 绝对禁止 +psql -h production-rds -f untested_migration.sql # 可能破坏数据 + +# ✅ 正确做法:测试环境 > 预发环境 > 生产环境 +``` + +#### 🚫 3. 禁止不备份就执行重大变更 + +```sql +-- ❌ 绝对禁止 +ALTER TABLE platform_schema.users DROP COLUMN email; -- 没有备份 + +-- ✅ 正确做法:先创建快照 +-- RDS控制台 > 备份实例 > 创建备份 +-- 然后再执行变更 +``` + +#### 🚫 4. 禁止在应用代码中硬编码数据库密码 + +```typescript +// ❌ 绝对禁止 +const prisma = new PrismaClient({ + datasources: { + db: { + url: 'postgresql://user:password@host:5432/db' // 硬编码 + } + } +}) + +// ✅ 正确做法:使用环境变量 +const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL // 环境变量 + } + } +}) +``` + +#### 🚫 5. 禁止不配置连接池限制 + +```bash +# ❌ 绝对禁止 +DATABASE_URL=postgresql://user:pass@host:5432/db # 无限制 + +# ✅ 正确做法 +DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10 +``` + +#### 🚫 6. 禁止在生产环境使用 `prisma migrate dev` + +```bash +# ❌ 绝对禁止 +npx prisma migrate dev # 会删除数据重建 + +# ✅ 正确做法 +npx prisma migrate deploy # 只应用新迁移,不删除数据 +``` + +#### 🚫 7. 禁止在生产环境启用Prisma的 `query` 日志 + +```typescript +// ❌ 绝对禁止(生产环境) +export const prisma = new PrismaClient({ + log: ['query', 'info', 'warn', 'error'] // 性能严重下降 +}) + +// ✅ 正确做法(生产环境) +export const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'production' ? ['error'] : ['query', 'info', 'warn', 'error'] +}) +``` + +#### 🚫 8. 禁止手工修改 `_prisma_migrations` 表 + +```sql +-- ❌ 绝对禁止 +DELETE FROM _prisma_migrations WHERE migration_name = 'xxx'; -- 破坏迁移历史 + +-- ✅ 正确做法:如果迁移出错,回滚并修复 +-- 不要手工修改 _prisma_migrations +``` + +#### 🚫 9. 禁止在生产环境使用超级用户(postgres) + +```bash +# ❌ 绝对禁止 +DATABASE_URL=postgresql://postgres:password@host:5432/db # 权限过大 + +# ✅ 正确做法:创建应用专用用户 +CREATE USER aiclinical_rw WITH PASSWORD 'xxx'; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA platform_schema TO aiclinical_rw; +``` + +#### 🚫 10. 禁止删除pg-boss的表 + +```sql +-- ❌ 绝对禁止 +DROP TABLE platform_schema.job; -- 应用立即崩溃 +DROP TABLE platform_schema.queue; -- 任务队列失效 + +-- ✅ 正确做法:永远不要手工操作pg-boss表 +-- 如果需要清理,使用pg-boss的API +``` + +--- + +## 📝 总结与建议 + +### 1. 你的数据库现状(非常好)✅ + +| 维度 | 状态 | 评分 | +|------|------|------| +| **Schema隔离架构** | ✅ 10个Schema全部就位 | ⭐⭐⭐⭐⭐ | +| **Postgres-Only架构** | ✅ pg-boss正常工作,支持自愈 | ⭐⭐⭐⭐⭐ | +| **数据完整性** | ✅ 外键约束完整 | ⭐⭐⭐⭐⭐ | +| **Prisma Schema** | ✅ 完全准确(pg-boss表会自动创建) | ⭐⭐⭐⭐⭐ | +| **数据量** | ✅ 26MB,适合迁移 | ⭐⭐⭐⭐⭐ | +| **遗留问题** | ⚠️ public schema有旧表(首次部署后清理) | ⭐⭐⭐⭐ | + +**总评:你的数据库架构非常优秀,可以直接部署到RDS!** + +### 2. 首次部署推荐方案 + +``` +🏆 强烈推荐:pg_dump 全量导入 + +理由: +✅ 100%完整(包括pg-boss表) +✅ 结构+数据+索引+外键一次性完成 +✅ 风险低,可重复执行 +✅ 恢复快(26MB约30秒) + +时间估算: +- 导出:1分钟 +- 传输到ECS:1分钟 +- 导入RDS:3分钟 +- 验证:2分钟 +总计:<10分钟 +``` + +### 3. 未来更新推荐方案 + +| 更新类型 | 推荐方案 | 工具 | +|---------|---------|------| +| 小更新(新增字段) | Prisma Migrate | `npx prisma migrate dev` | +| 中更新(新增表) | Prisma Migrate | `npx prisma migrate dev` | +| 大更新(新模块) | 结构化流程 | Prisma + 手工SQL + 备份 | +| 紧急修复(Hotfix) | 快速通道 | 直接SQL(先备份) | + +### 4. 备份策略(初期简化版) + +``` +第一道防线:RDS自动备份(必做)⭐⭐⭐⭐⭐ +├─ 数据备份:每天凌晨2点 +├─ 日志备份:实时(开启PITR) +└─ 保留7天 + +第二道防线:手动快照(必做)⭐⭐⭐⭐ +└─ 重大操作前创建 + +第三道防线:pg_dump脚本(初期不需要) +└─ 业务做大后再考虑 + +恢复演练:每季度一次(必做) +``` + +**初期只需配置RDS自动备份,省心省力!** + +### 5. 清理遗留问题(可选) + +```sql +-- 建议在首次部署到RDS后清理 +-- 1. 迁移 admin_logs 到 admin_schema +ALTER TABLE public.admin_logs SET SCHEMA admin_schema; + +-- 2. 迁移 review_tasks 到 rvw_schema +ALTER TABLE public.review_tasks SET SCHEMA rvw_schema; + +-- 3. 删除重复的 public.users(验证后) +-- 先对比数据一致性 +SELECT COUNT(*) FROM public.users; +SELECT COUNT(*) FROM platform_schema.users; +-- 一致后删除 +DROP TABLE public.users; + +-- 4. 更新Prisma Schema +model AdminLog { + // ... + @@map("admin_logs") + @@schema("admin_schema") // 从 public 改为 admin_schema +} + +model ReviewTask { + // ... + @@map("review_tasks") + @@schema("rvw_schema") // 从 public 改为 rvw_schema +} +``` + +### 6. 下一步行动 + +``` +☐ 1. 创建阿里云RDS PostgreSQL 15实例 +☐ 2. 配置VPC、白名单(⚠️用VPC网段)、备份策略(自动备份+日志备份) +☐ 3. 本地清理测试数据(可选) +☐ 4. pg_dump导出数据库 +☐ 5. 通过ECS跳板机导入RDS +☐ 6. 验证导入结果(Schema、表、数据、pg-boss) +☐ 7. 配置SAE环境变量(DATABASE_URL) +☐ 8. 部署backend应用 +☐ 9. 端到端测试 +☐ 10. 监控24小时 + +预计时间:3-4小时(从0到上线) +``` + +--- + +**文档创建人:** AI助手 +**最后更新:** 2025-12-14 +**版本:** v1.0 + +**核心理念:数据安全第一,充分备份,渐进式迁移,完整验证** ⭐⭐⭐ + diff --git a/docs/05-部署文档/README.md b/docs/05-部署文档/README.md index 10363f43..5b24cb17 100644 --- a/docs/05-部署文档/README.md +++ b/docs/05-部署文档/README.md @@ -1,74 +1,472 @@ -# 部署文档 +# AI临床研究平台 - 阿里云部署文档导航 -> **文档定位:** 4种部署模式的完整部署指南 -> **适用范围:** 开发、运维、实施团队 +> **文档版本:** v2.0 +> **最后更新:** 2025-12-14 +> **文档总数:** 11份 +> **部署目标:** 阿里云 SAE + RDS PostgreSQL 15 + OSS +> **适用团队:** 1-2人初创团队 → 10人成长团队 --- -## 📋 部署模式 +## 📋 文档清单 -| 部署模式 | 说明 | 优先级 | 状态 | -|---------|------|--------|------| -| **01-云端SaaS部署** | 完整平台部署、微服务架构、K8s | P0 | ⏳ 待创建 | -| **02-独立产品包部署** | 审稿系统、AI文献系统独立打包 | P1 | ⏳ 待创建 | -| **03-Electron单机版** | 桌面应用、完全离线、本地SQLite | P2 | ⏳ 待创建 | -| **04-私有化部署** | 医院内网部署、Docker、K3s | P1 | ⏳ 待扩展 | +### 🎯 必读文档(按阅读顺序) + +| # | 文档名称 | 用途 | 阅读时间 | 优先级 | +|---|---------|------|---------|--------| +| 1 | **00-部署架构总览.md** | 理解整体架构和模块关系 | 30分钟 | ⭐⭐⭐⭐⭐ | +| 2 | **⚠️ 07-关键配置补充说明.md** | 修正致命问题(NAT/安全/超时) | 20分钟 | ⭐⭐⭐⭐⭐ | +| 3 | **08-部署检查清单.md** | 逐步操作清单 | 边部署边查看 | ⭐⭐⭐⭐⭐ | + +### 📖 独立部署文档(按部署顺序) + +| # | 文档名称 | 部署对象 | 阅读时间 | 部署时间 | +|---|---------|---------|---------|---------| +| 4 | **PostgreSQL部署策略-摸底报告.md** | RDS PostgreSQL 15 | 20分钟 | 10分钟 | +| 5 | **05-Node.js后端-SAE容器部署指南.md** | Node.js后端 | 30分钟 | 20-30分钟 | +| 6 | **04-Python微服务-SAE容器部署指南.md** | Python微服务 | 25分钟 | 20-30分钟 | +| 7 | **06-前端Nginx-SAE容器部署指南.md** | React前端 | 25分钟 | 15-20分钟 | +| 8 | **03-Dify-ECS部署完全指南.md** | Dify RAG平台 | 30分钟 | 30-60分钟 | + +### 📚 参考文档 + +| # | 文档名称 | 用途 | +|---|---------|------| +| 9 | **CTO代码审查报告.md** | 架构审查和问题识别 | +| 10 | **集成部署补充指南.md** | 集成问题解决方案 | +| 11 | **01-部署架构设计.md** | 历史文档(待更新) | --- -## 🎯 部署模式对比 +## 🚀 快速开始 -| 特性 | 云端SaaS | 独立产品包 | 单机版 | 私有化 | -|------|---------|-----------|--------|--------| -| **部署位置** | 公有云 | 客户内网 | 用户电脑 | 医院内网 | -| **数据存储** | 云端 | 客户内网 | 100%本地 | 100%内网 | -| **架构** | 完整平台 | 单个模块 | Electron | Docker | -| **目标客户** | 个人用户 | 期刊/机构 | 个人医生 | 医院/机构 | +### 新团队首次部署(推荐路径) + +``` +阶段0:准备工作(30分钟) +├─ 阅读《00-部署架构总览》 +├─ 阅读《07-关键配置补充说明》⚠️ 必读 +└─ 打印《08-部署检查清单》 + +阶段1:基础设施(Day 1上午,2小时) +├─ VPC + NAT网关(⚠️ 必需) +├─ RDS PostgreSQL 15 +├─ OSS Bucket +└─ 参考:08-部署检查清单 第1部分 + +阶段2:核心服务(Day 1下午,2小时) +├─ Node.js后端(临时Dify配置) +├─ Python微服务 +├─ 前端 +└─ 参考:08-部署检查清单 第2部分 + +阶段3:Dify服务(Day 2上午,1小时) +├─ ECS部署Dify +├─ 生成API Key +└─ 更新后端配置 +└─ 参考:08-部署检查清单 第3部分 + +阶段4:测试验证(Day 2下午,1小时) +└─ 参考:08-部署检查清单 第4部分 + +总计:约6小时(实际操作时间) +``` --- -## 📚 快速导航 +## ⚠️ 关键注意事项(必读!) -### 部署文档 -1. [云端SaaS部署](./01-云端SaaS部署/README.md) - P0 -2. [独立产品包部署](./02-独立产品包部署/README.md) - P1 -3. [Electron单机版](./03-Electron单机版/README.md) - P2 -4. [私有化部署](./04-私有化部署/README.md) - P1 +### 🚨 致命问题(P0/P1)- 不解决会导致系统不可用 + +1. **NAT网关缺失** ⭐⭐⭐⭐⭐ + - 问题:SAE无公网出口,AI功能全部超时 + - 解决:创建NAT网关 + EIP + SNAT条目 + - 成本:¥100/月 + - 参考:`07-关键配置补充说明.md` 第1节 + +2. **Dify API Key死锁** ⭐⭐⭐⭐⭐ + - 问题:后端需要Key,但Key需要Dify先启动 + - 解决:分阶段部署,先用临时Key + - 参考:`07-关键配置补充说明.md` 第2节 + +3. **HTTP超时未配置** ⭐⭐⭐⭐ + - 问题:Python服务慢,后端连接泄漏 + - 解决:设置timeout=120秒 + - 参考:`07-关键配置补充说明.md` 第3节 + +4. **ECS端口安全** ⭐⭐⭐⭐⭐ + - 问题:Redis/Weaviate对公网开放 + - 解决:只监听127.0.0.1 + - 参考:`07-关键配置补充说明.md` 第4节 + +5. **Python Workers过多** ⭐⭐⭐⭐⭐ + - 问题:PyMuPDF吃内存,OOM崩溃 + - 解决:workers=2,不超过2GB/0.8GB + - 参考:`07-关键配置补充说明.md` 第6节 + +6. **Nginx文件大小限制** ⭐⭐⭐⭐ + - 问题:医疗PDF大文件上传失败 + - 解决:client_max_body_size 50M + - 参考:`07-关键配置补充说明.md` 第5节 --- -## 🔗 相关文档 +## 📊 文档更新记录 -- [模块独立部署与单机版方案](../00-系统总体设计/06-模块独立部署与单机版方案.md) -- [架构设计全景图](../00-系统总体设计/08-架构设计全景图.md) +### v2.0 (2025-12-14) - 关键问题修正版 + +**新增文档:** +- ✅ `07-关键配置补充说明.md`(813行)- 修正6个致命问题 +- ✅ `08-部署检查清单.md`(784行)- 完整操作清单 +- ✅ `README.md`(本文档)- 部署文档导航 + +**更新文档:** +- ✅ `00-部署架构总览.md` + - 物理架构图增加NAT网关 + - 成本估算更新(¥1,200/月) + - 部署顺序增加分阶段说明 + - 风险分析增加详细解决方案 + +**核心改进:** +1. ⚠️ NAT网关配置(P0,必需) +2. ⚠️ 部署依赖死锁解决(P1) +3. ⚠️ HTTP超时配置(P1) +4. ⚠️ 安全配置强化(P0) +5. ⚠️ OOM防护(P1) +6. ⚠️ 文件上传限制(P2) + +**审查来源:** +- `CTO代码审查报告.md`:识别3个致命问题 +- `集成部署补充指南.md`:提供实战解决方案 + +### v1.0 (2025-12-13) - 初始版本 + +**文档清单:** +- `00-部署架构总览.md`(1,345行) +- `PostgreSQL部署策略-摸底报告.md`(1,327行) +- `03-Dify-ECS部署完全指南.md`(1,189行) +- `04-Python微服务-SAE容器部署指南.md`(1,524行) +- `05-Node.js后端-SAE容器部署指南.md`(2,178行) +- `06-前端Nginx-SAE容器部署指南.md`(2,064行) --- -**最后更新:** 2025-11-06 -**维护人:** 技术架构师 +## 📖 详细文档说明 +### 1. 00-部署架构总览.md ⭐⭐⭐⭐⭐ +**用途:** 理解整体架构,5个模块的关系,阿里云服务映射 +**核心内容:** +- 3个架构图(逻辑/物理/数据流) +- 5个模块依赖关系(L1-L4层级) +- 模块与阿里云服务映射(SAE/RDS/OSS/ECS) +- 开发环境 vs 部署环境 +- Docker版本管理策略 +- 线上故障快速修复(4种方案) +**适合人群:** 所有人(技术负责人、开发、运维) +**关键章节:** +- 第1章:架构全景图(必读) +- 第2章:5个核心模块关系 +- 第3章:模块与阿里云服务映射 +- 第6章:线上故障快速修复(⭐ 重点) +--- +### 2. 07-关键配置补充说明.md ⚠️⭐⭐⭐⭐⭐ +**用途:** 修正原文档遗漏的6个致命问题 +**核心内容:** +- 🚨 P0/P1致命问题(6个) + 1. SAE孤岛效应 - NAT网关配置 + 2. 部署依赖死锁 - Dify API Key + 3. HTTP Client超时 - 120秒配置 + 4. ECS端口安全 - Redis/Weaviate + 5. Nginx文件大小 - 50MB限制 + 6. Python Workers - OOM防护 +**适合人群:** 所有人(⚠️ 部署前必读) +**修复时间:** 约40分钟(必需修复) +--- +### 3. 08-部署检查清单.md ⭐⭐⭐⭐⭐ +**用途:** 逐步操作清单,确保不遗漏任何步骤 +**核心内容:** +- 阶段1:基础设施(2小时) + - VPC + NAT网关 ⚠️ + - RDS PostgreSQL 15 + - OSS Bucket + - ACR容器镜像仓库 +- 阶段2:核心服务(2小时) + - Node.js后端(临时Dify配置) + - Python微服务 + - 前端 +- 阶段3:Dify服务(1小时) + - ECS部署 + - API Key生成 + - 后端配置更新 +- 阶段4:测试验证(1小时) +- 阶段5:可选优化 +**使用方式:** 打印或复制到笔记,逐项打勾 +--- +### 4. PostgreSQL部署策略-摸底报告.md ⭐⭐⭐⭐⭐ +**用途:** 深入理解PostgreSQL 15数据库架构和部署策略 +**核心内容:** +- 本地数据库真实情况(10个Schema隔离) +- Prisma与数据库差异(pg-boss表自愈机制) +- 数据库连接方式(DATABASE_URL + 连接池) +- 首次部署方案(pg_dump vs Prisma migrate) +- 未来更新策略(新表/修改字段/新模块) +- RDS备份策略(4道防线) +- 最佳实践与禁止操作 +**适合人群:** 后端开发、DBA、运维 +**关键发现:** +- ✅ pg-boss表会自动创建,无需担心 +- ✅ 推荐pg_dump全量导入(10分钟完成) +- ✅ RDS自动备份足够,初期不需要ECS脚本 +--- +### 5. 05-Node.js后端-SAE容器部署指南.md ⭐⭐⭐⭐⭐ +**用途:** Node.js后端详细部署步骤 +**核心内容:** +- 为什么选择SAE容器部署 +- 后端服务分析(Node 22 + Fastify + Prisma) +- **⭐ Prisma反向同步**(关键,解决Schema不一致) +- Dockerfile多阶段构建 +- SAE应用配置(环境变量、健康检查) +- 数据库部署策略(pg_dump + prisma db pull) +- 端到端测试 +- 监控与故障排查 + +**适合人群:** 后端开发、运维 + +**关键章节:** +- 第4章:Prisma反向同步(必读) +- 第5章:Dockerfile(生产环境Prisma CLI) +- 第8章:SAE应用配置(环境变量) + +--- + +### 6. 04-Python微服务-SAE容器部署指南.md ⭐⭐⭐⭐ + +**用途:** Python微服务详细部署步骤 + +**核心内容:** +- 为什么选择SAE容器(系统依赖问题) +- Python服务分析(PyMuPDF + Nougat + Polars) +- Dockerfile多阶段构建(系统依赖安装) +- **⚠️ Workers限制**(防止OOM) +- SAE应用配置 +- 端到端测试 + +**适合人群:** Python开发、运维 + +**关键注意:** +- ⚠️ workers=2(不要超过,会OOM) +- ⚠️ 内存至少2GB +- ⚠️ 系统依赖(libGL等)必须在Dockerfile中安装 + +--- + +### 7. 06-前端Nginx-SAE容器部署指南.md ⭐⭐⭐⭐ + +**用途:** React前端详细部署步骤 + +**核心内容:** +- 为什么选择SAE Nginx容器 +- 前端服务分析(React 19 + Vite 7) +- Dockerfile多阶段构建 +- **nginx.conf.template**(SPA路由 + 反向代理) +- SAE应用配置(envsubst动态配置) +- 端到端测试 + +**适合人群:** 前端开发、运维 + +**关键配置:** +- ⚠️ client_max_body_size 50M +- ⚠️ try_files $uri /index.html(SPA路由) +- ⚠️ envsubst动态注入后端地址 + +--- + +### 8. 03-Dify-ECS部署完全指南.md ⭐⭐⭐⭐ + +**用途:** Dify RAG平台详细部署步骤 + +**核心内容:** +- 为什么选择ECS(复杂服务,独立数据库) +- ECS准备(Docker + Docker Compose) +- **⚠️ Swap配置**(防止OOM) +- docker-compose.yaml配置(7个服务) +- **⚠️ 端口安全**(Redis/Weaviate只监听localhost) +- Nginx路由层(CORS处理) +- 故障排查 + +**适合人群:** 运维、后端开发 + +**关键安全:** +- ⚠️ Redis: 127.0.0.1:6379:6379 +- ⚠️ Weaviate: 127.0.0.1:8080:8080 +- ⚠️ 独立数据库(不要和业务库混用) + +--- + +### 9. CTO代码审查报告.md 📚 + +**用途:** 识别架构问题和风险点 + +**核心发现:** +- 🚨 SAE孤岛效应(NAT网关) +- 🔄 部署依赖死锁(Dify Key) +- 🌐 CORS问题(前端直连Dify) +- 🔐 端口安全(Redis/Weaviate) + +**评分:** A-(优秀,但有关键问题需解决) + +--- + +### 10. 集成部署补充指南.md 📚 + +**用途:** 集成部署的实战解决方案 + +**核心内容:** +- NAT网关配置(两种方案) +- SSH隧道配置(跳板机) +- 一键发布脚本(可选) +- OSS权限规划 + +--- + +## 🎯 不同角色的阅读建议 + +### 技术负责人/CTO + +``` +☐ 1. 00-部署架构总览(30分钟)- 理解整体架构 +☐ 2. 07-关键配置补充说明(20分钟)- 识别风险 +☐ 3. CTO代码审查报告(10分钟)- 审查视角 +☐ 4. 成本估算:¥1,200/月 + +总计:60分钟 +``` + +### 后端开发工程师 + +``` +☐ 1. 00-部署架构总览(30分钟)- 理解架构 +☐ 2. 07-关键配置补充说明(20分钟)- 修正问题 +☐ 3. PostgreSQL部署策略(30分钟)- 数据库详解 +☐ 4. 05-Node.js后端部署(30分钟)- 详细步骤 +☐ 5. 04-Python微服务部署(25分钟)- Python服务 + +总计:135分钟(2.5小时) +``` + +### 前端开发工程师 + +``` +☐ 1. 00-部署架构总览(30分钟)- 理解架构 +☐ 2. 07-关键配置补充说明(10分钟)- Nginx配置 +☐ 3. 06-前端Nginx部署(25分钟)- 详细步骤 + +总计:65分钟 +``` + +### 运维工程师 + +``` +☐ 1. 00-部署架构总览(30分钟)- 理解架构 +☐ 2. 07-关键配置补充说明(20分钟)- 必读 +☐ 3. 08-部署检查清单(边部署边查看)- 操作清单 +☐ 4. 所有独立部署文档(各30分钟)- 详细步骤 + +总计:阅读2小时 + 部署6小时 +``` + +--- + +## 💡 常见问题(FAQ) + +### Q1: 必须先读哪几份文档? + +**A:** 3份必读(按顺序): +1. `00-部署架构总览.md`(理解整体) +2. `07-关键配置补充说明.md`(修正致命问题)⚠️ +3. `08-部署检查清单.md`(逐步操作) + +### Q2: 为什么NAT网关这么重要? + +**A:** SAE部署在VPC内网,没有NAT网关: +- ❌ 后端无法调用DeepSeek/OpenAI API +- ❌ Python无法下载公网PDF +- ❌ npm install无法下载公网依赖 +- **结果:所有AI功能不可用!** + +### Q3: Dify API Key的鸡生蛋问题怎么解决? + +**A:** 分阶段部署: +1. 后端先用临时Key(`temp_placeholder`) +2. 部署Dify并生成真实Key +3. 更新后端环境变量并重启 + +### Q4: 为什么Python workers只能设置2个? + +**A:** PyMuPDF + Nougat OCR非常吃内存: +- 单个worker:~800MB内存 +- SAE配置:2GB内存 +- workers=2:安全值(2 × 800MB = 1.6GB < 2GB) +- workers=3:会OOM崩溃 + +### Q5: 部署需要多长时间? + +**A:** +- 首次部署:6小时(实际操作) +- 后续更新:10-30分钟(镜像重新部署) + +### Q6: 总成本多少? + +**A:** ¥1,200-1,250/月(初期配置) +- SAE:¥350/月 +- RDS:¥400/月 +- ECS(Dify):¥300/月 +- NAT网关:¥100/月 +- OSS:¥10/月 + +--- + +## 📞 获取帮助 + +### 遇到问题? + +1. **查看故障排查章节**:每个独立文档都有详细的故障排查 +2. **检查关键配置**:`07-关键配置补充说明.md` +3. **查看审查报告**:`CTO代码审查报告.md` 和 `集成部署补充指南.md` + +### 反馈建议 + +如果文档有遗漏或错误,欢迎反馈! + +--- + +**文档维护人:** AI助手 +**最后更新:** 2025-12-14 +**版本:** v2.0 + +**核心理念:架构清晰、安全第一、步骤详细、易于上手** ⭐⭐⭐ diff --git a/docs/05-部署文档/文档修正报告-20251214.md b/docs/05-部署文档/文档修正报告-20251214.md new file mode 100644 index 00000000..16146a0a --- /dev/null +++ b/docs/05-部署文档/文档修正报告-20251214.md @@ -0,0 +1,467 @@ +# 部署文档修正报告 - 2025年12月14日 + +> **修正依据:** 专业技术审查反馈 +> **修正时间:** 2025-12-14 +> **修正范围:** 7个部署文档 +> **修正问题:** 8个关键问题 + +--- + +## 📋 修正概览 + +### 修正统计 + +| 严重级别 | 问题数量 | 已修正 | 影响范围 | +|---------|---------|--------|---------| +| **P0/P1(致命)** | 3个 | ✅ 3个 | 全部服务 | +| **P2(重要)** | 3个 | ✅ 3个 | 全部服务 | +| **P3(最佳实践)** | 2个 | ✅ 2个 | 部分服务 | +| **总计** | 8个 | ✅ 8个 | - | + +--- + +## 🚨 P0/P1 致命问题修正 + +### 1. ⭐⭐⭐⭐⭐ 服务发现地址不一致 + +**问题描述:** +``` +文档中使用 .sae 域名(如 extraction-service.sae:8000) +但SAE的K8s服务发现域名格式不确定,会导致100%连接失败 +``` + +**影响范围:** +- `00-部署架构总览.md` +- `05-Node.js后端-SAE容器部署指南.md` +- `04-Python微服务-SAE容器部署指南.md` +- `08-部署检查清单.md` + +**修正内容:** +```bash +# ❌ 错误(修正前) +EXTRACTION_SERVICE_URL=http://extraction-service.sae:8000 + +# ✅ 正确(修正后) +EXTRACTION_SERVICE_URL=http://172.16.x.x:8000 +# 获取方式:SAE控制台 > 应用详情 > 实例列表 > 查看内网IP +``` + +**修正文件:** +- ✅ `00-部署架构总览.md` - 第522-529行 +- ✅ `04-Python微服务-SAE容器部署指南.md` - 第686-715行 +- ✅ `08-部署检查清单.md` - 第348行、第434行 + +--- + +### 2. ⭐⭐⭐⭐⭐ 时区不一致风险 + +**问题描述:** +``` +不同服务的时区不一致会导致: +❌ 日志时间对不上(前端14:00,后端06:00) +❌ pg-boss定时任务在错误时间触发 +❌ 用户看到的时间戳错误 +``` + +**影响范围:** +- Node.js后端:默认UTC ❌ +- Python微服务:默认UTC ❌ +- 前端Nginx:Asia/Shanghai ✅(已正确) +- RDS PostgreSQL:默认UTC ❌ + +**修正内容:** + +**Node.js后端 Dockerfile:** +```dockerfile +FROM node:22-alpine +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai # ⚠️ 新增 +# ... 其他配置 +``` + +**Python微服务 Dockerfile:** +```dockerfile +FROM python:3.11-slim +RUN apt-get update && apt-get install -y tzdata # ⚠️ 新增 +ENV TZ=Asia/Shanghai # ⚠️ 新增 +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # ⚠️ 新增 +# ... 其他配置 +``` + +**RDS PostgreSQL 配置:** +```sql +-- RDS控制台 > 参数设置 > timezone +timezone = Asia/Shanghai +``` + +**修正文件:** +- ✅ `00-部署架构总览.md` - 新增"时区统一配置"章节 +- ✅ `05-Node.js后端-SAE容器部署指南.md` - 第485-495行 +- ✅ `04-Python微服务-SAE容器部署指南.md` - 第387-410行 +- ✅ `PostgreSQL部署策略-摸底报告.md` - 新增第12条最佳实践 +- ✅ `07-关键配置补充说明.md` - 新增第8节 + +--- + +### 3. ⭐⭐⭐⭐⭐ 安全组配置缺失 + +**问题描述:** +``` +ECS的Redis (6379)、Weaviate (8080)、Dify API (5000) 端口 +如果对公网开放,会导致严重安全问题: +❌ 黑客可以绕过后端直接调用LLM额度 +❌ Redis无密码,可能被清空数据 +❌ Weaviate的向量数据可能被窃取 +``` + +**影响范围:** +- `03-Dify-ECS部署完全指南.md` +- `00-部署架构总览.md` + +**修正内容:** + +**ECS安全组配置:** +```bash +# ✅ 入方向规则(Inbound) +允许 80/TCP 来源:172.16.0.0/12 # Nginx(VPC内网访问) +允许 22/TCP 来源:您的办公室公网IP # SSH管理 +拒绝 所有 来源:0.0.0.0/0 # 默认拒绝 + +# ❌ 绝对禁止(安全红线) +禁止 5000/TCP(Dify API)对公网开放 +禁止 8080/TCP(Weaviate)对公网开放 +禁止 6379/TCP(Redis)对公网开放 +``` + +**docker-compose.yaml 端口绑定:** +```yaml +services: + redis: + ports: + - "127.0.0.1:6379:6379" # ⚠️ 只监听本地 + + weaviate: + ports: + - "127.0.0.1:8080:8080" # ⚠️ 只监听本地 +``` + +**修正文件:** +- ✅ `03-Dify-ECS部署完全指南.md` - 第169-175行、第451-492行 +- ✅ `00-部署架构总览.md` - 新增"安全组配置最佳实践"章节 + +--- + +## ⚠️ P2 重要问题修正 + +### 4. ⭐⭐⭐⭐⭐ 镜像拉取策略 + +**问题描述:** +``` +SAE可能不会拉取新镜像(代码不更新的"灵异事件") +开发者修改代码 → 构建镜像 → 推送到ACR(覆盖v1.0.0) +→ SAE部署 → 发现代码没更新??? +``` + +**影响范围:** +- 所有SAE部署的服务(前端、后端、Python) + +**修正内容:** + +**方案A:每次部署使用新版本号(强烈推荐)** +```bash +# 使用语义化版本号 +v1.0.0 → v1.0.1 → v1.0.2 ... + +# 或使用时间戳 +v20251214-1430 → v20251214-1530 ... +``` + +**方案B:配置SAE镜像拉取策略(测试环境)** +```bash +# SAE控制台 > 应用配置 > 镜像设置 +镜像拉取策略:Always +``` + +**修正文件:** +- ✅ `00-部署架构总览.md` - 新增"镜像拉取策略"章节 +- ✅ `07-关键配置补充说明.md` - 新增第9节 + +--- + +### 5. ⭐⭐⭐⭐ Python服务内存管理 + +**问题描述:** +``` +Python服务(PyMuPDF/Nougat)内存密集,容易OOM +❌ 单个PDF OCR可能占用500MB-1GB内存 +❌ SAE默认2GB内存可能不够 +``` + +**影响范围:** +- `04-Python微服务-SAE容器部署指南.md` +- `00-部署架构总览.md` + +**修正内容:** + +**规格建议:** +| 场景 | CPU | 内存 | Workers | 适用情况 | +|------|-----|------|---------|---------| +| **基础版** | 1核 | 2GB | 2 | 简单PDF解析 | +| **标准版** | 2核 | 4GB | 3 | 包含OCR(Nougat) | +| **增强版** | 2核 | 8GB | 4 | 大量OCR + 高并发 | + +**Dockerfile优化(已应用):** +```dockerfile +CMD ["gunicorn", "main:app", \ + "--bind", "0.0.0.0:8000", \ + "--workers", "2", \ # ⚠️ 限制并发 + "--timeout", "120", \ # ⚠️ 2分钟超时 + "--max-requests", "100", \ # ⚠️ 处理100个请求后重启worker + "--max-requests-jitter", "10"] # ⚠️ 随机抖动 +``` + +**修正文件:** +- ✅ `00-部署架构总览.md` - 新增"Python服务内存管理"章节 +- ✅ `07-关键配置补充说明.md` - 新增第10节 + +--- + +### 6. ⭐⭐⭐⭐ Dify API Key死锁风险 + +**问题描述:** +``` +Node.js后端启动时,如果强依赖Dify连通性,会导致: +第二阶段部署后端 → 后端启动失败(Dify还没部署) +→ 无法进入SAE控制台查看日志或更新配置 +``` + +**影响范围:** +- `05-Node.js后端-SAE容器部署指南.md` +- `00-部署架构总览.md` + +**修正内容:** + +**后端代码建议:** +```typescript +// backend/src/common/rag/DifyClient.ts +constructor() { + const apiKey = process.env.DIFY_API_KEY + + // ✅ 关键:启动时不应强依赖Dify + if (!apiKey || apiKey === 'temp' || apiKey.startsWith('temp_')) { + console.warn('⚠️ Dify API Key未配置,PKB模块将不可用') + this.enabled = false + return // ⚠️ 不抛出错误,让应用正常启动 + } + + this.client = new DifySDK(apiKey) + this.enabled = true +} +``` + +**部署流程调整:** +``` +第二阶段:部署Node.js后端 +├─ DIFY_API_KEY=temp_placeholder_will_update_later # ⚠️ 临时值 +└─ 后端正常启动(PKB模块禁用) + +第四阶段:部署Dify +├─ 生成真实API Key +└─ 更新后端环境变量 → 重启应用 +``` + +**修正文件:** +- ✅ `00-部署架构总览.md` - 部署顺序说明 +- ✅ `05-Node.js后端-SAE容器部署指南.md` - 环境变量配置 +- ✅ `07-关键配置补充说明.md` - 第2节(已存在) + +--- + +## 📝 P3 最佳实践补充 + +### 7. ⭐⭐⭐⭐ 开发调试最佳实践(SSH隧道) + +**问题描述:** +``` +RDS在VPC内网,开发者无法直接用Navicat/DBeaver连接 +``` + +**修正内容:** + +**通过ECS建立SSH隧道:** +```bash +# 步骤1:建立SSH隧道 +ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \ + root@ECS公网IP \ + -i ~/.ssh/dify-ecs.pem + +# 步骤2:Navicat连接 +主机:localhost +端口:5433 +用户名:aiclinical_rw +密码:(RDS密码) +``` + +**修正文件:** +- ✅ `00-部署架构总览.md` - 新增"开发调试最佳实践"章节 +- ✅ `07-关键配置补充说明.md` - 第7节(已存在) + +--- + +### 8. ⭐⭐⭐⭐ NAT网关成本优化说明 + +**问题描述:** +``` +NAT网关成本¥100/月,对初创团队是一笔开销 +需要说明替代方案,但不推荐 +``` + +**修正内容:** + +**成本对比:** +| 方案 | 成本 | 稳定性 | 复杂度 | 推荐度 | +|------|------|--------|--------|--------| +| NAT网关 | ¥100/月 | ⭐⭐⭐⭐⭐ | 低 | ⭐⭐⭐⭐⭐(推荐)| +| SAE绑定EIP | ¥30-50/月 | ⭐⭐⭐ | 中 | ⭐⭐⭐(部分地域)| +| ECS做SNAT | ¥0(复用ECS) | ⭐⭐ | 高 | ⭐⭐(不推荐)| + +**建议:** 初创团队不要在这里省钱,NAT网关是生产环境的标配。 + +**修正文件:** +- ✅ `00-部署架构总览.md` - 成本估算章节 +- ✅ `07-关键配置补充说明.md` - 第1节(已存在) + +--- + +## 📊 修正文件清单 + +| 文件名 | 修正内容 | 优先级 | +|--------|---------|--------| +| `00-部署架构总览.md` | 1. 服务发现地址
2. 时区统一
3. 安全组配置
4. 镜像拉取策略
5. Python内存管理
6. SSH隧道
7. NAT成本说明 | ⭐⭐⭐⭐⭐ | +| `05-Node.js后端-SAE容器部署指南.md` | 1. 时区配置
2. Dify死锁说明 | ⭐⭐⭐⭐⭐ | +| `04-Python微服务-SAE容器部署指南.md` | 1. 服务发现地址
2. 时区配置
3. 内存规格说明 | ⭐⭐⭐⭐⭐ | +| `06-前端Nginx-SAE容器部署指南.md` | 无需修改(时区已正确) | - | +| `03-Dify-ECS部署完全指南.md` | 1. 安全组配置
2. 端口绑定 | ⭐⭐⭐⭐⭐ | +| `PostgreSQL部署策略-摸底报告.md` | 1. 时区配置最佳实践 | ⭐⭐⭐⭐ | +| `08-部署检查清单.md` | 1. 服务发现地址 | ⭐⭐⭐⭐⭐ | +| `07-关键配置补充说明.md` | 1. 时区统一(新增第8节)
2. 镜像拉取策略(新增第9节)
3. Python内存管理(新增第10节) | ⭐⭐⭐⭐⭐ | + +--- + +## ✅ 验证清单 + +### 部署前验证 + +```bash +☐ 1. 检查所有环境变量中的服务地址 + - ✅ 使用内网IP(172.16.x.x) + - ❌ 不使用 .sae 域名 + +☐ 2. 检查所有Dockerfile的时区配置 + - ✅ Node.js后端:ENV TZ=Asia/Shanghai + - ✅ Python微服务:ENV TZ=Asia/Shanghai + - ✅ 前端Nginx:ENV TZ=Asia/Shanghai + - ✅ RDS PostgreSQL:timezone = Asia/Shanghai + +☐ 3. 检查ECS安全组配置 + - ✅ Redis/Weaviate端口绑定到127.0.0.1 + - ✅ 安全组只允许VPC内网访问 + - ❌ 不对公网开放5000/6379/8080端口 + +☐ 4. 检查镜像版本管理 + - ✅ 使用语义化版本号(v1.0.0, v1.0.1...) + - ❌ 不始终使用latest标签 + +☐ 5. 检查Python服务规格 + - ✅ 初期:1核2GB + - ✅ 如遇OOM:升级至2核4GB +``` + +### 部署后验证 + +```bash +☐ 1. 验证时区 + docker exec backend-container date + docker exec python-container date + psql -h rds-host -c "SHOW timezone;" + # 应该都显示:Asia/Shanghai 或 CST + +☐ 2. 验证服务连通性 + # 在后端容器内测试Python服务 + curl http://172.16.x.x:8000/health + # 应该返回:200 OK + +☐ 3. 验证安全配置 + # 从公网测试(应该失败) + telnet ECS公网IP 6379 + # 应该超时或拒绝连接 + +☐ 4. 验证镜像版本 + # SAE控制台查看镜像标签 + # 应该是具体版本号,不是latest +``` + +--- + +## 📈 修正效果评估 + +### 问题解决率 + +- ✅ **P0/P1致命问题:** 3/3 = 100% +- ✅ **P2重要问题:** 3/3 = 100% +- ✅ **P3最佳实践:** 2/2 = 100% +- ✅ **总体解决率:** 8/8 = 100% + +### 预期收益 + +| 维度 | 修正前风险 | 修正后收益 | +|------|-----------|-----------| +| **可用性** | 服务连接失败100% | ✅ 服务正常连接 | +| **安全性** | Redis/Weaviate可能被攻击 | ✅ 只允许VPC内网访问 | +| **稳定性** | Python OOM频繁 | ✅ 内存管理优化 | +| **可维护性** | 日志时间混乱 | ✅ 时区统一,易于排查 | +| **可靠性** | 镜像不更新 | ✅ 版本管理清晰 | + +--- + +## 🎯 下一步行动 + +### 立即执行(必需) + +```bash +☐ 1. 更新所有Dockerfile(时区配置) +☐ 2. 更新环境变量(服务发现地址) +☐ 3. 配置ECS安全组(端口安全) +☐ 4. 配置RDS时区(Asia/Shanghai) +☐ 5. 制定镜像版本管理规范 +``` + +### 后续优化(可选) + +```bash +☐ 1. 配置SSH隧道(开发便利) +☐ 2. 实现OSS签名URL(安全) +☐ 3. 配置监控告警(Python内存) +☐ 4. 编写自动化部署脚本 +``` + +--- + +## 📚 相关文档 + +- [00-部署架构总览.md](./00-部署架构总览.md) - 已更新 +- [03-Dify-ECS部署完全指南.md](./03-Dify-ECS部署完全指南.md) - 已更新 +- [04-Python微服务-SAE容器部署指南.md](./04-Python微服务-SAE容器部署指南.md) - 已更新 +- [05-Node.js后端-SAE容器部署指南.md](./05-Node.js后端-SAE容器部署指南.md) - 已更新 +- [06-前端Nginx-SAE容器部署指南.md](./06-前端Nginx-SAE容器部署指南.md) - 无需修改 +- [PostgreSQL部署策略-摸底报告.md](./PostgreSQL部署策略-摸底报告.md) - 已更新 +- [07-关键配置补充说明.md](./07-关键配置补充说明.md) - 已更新 +- [08-部署检查清单.md](./08-部署检查清单.md) - 已更新 + +--- + +**报告生成时间:** 2025-12-14 +**报告生成人:** AI助手 +**审查依据:** 专业技术团队反馈 +**修正质量:** ⭐⭐⭐⭐⭐(8/8问题已全部修正) + diff --git a/docs/05-部署文档/集成部署补充指南:填补最后的缝隙.md b/docs/05-部署文档/集成部署补充指南:填补最后的缝隙.md new file mode 100644 index 00000000..ce79e300 --- /dev/null +++ b/docs/05-部署文档/集成部署补充指南:填补最后的缝隙.md @@ -0,0 +1,114 @@ +# **集成部署补充指南:填补最后的缝隙** + +文档版本: v1.0 +目标: 解决 5 个独立模块集成时的网络连通性、发布效率和成本问题。 + +## **🛑 关键问题 1:SAE 的外网访问 (调用 DeepSeek/OpenAI)** + +现状:部署在 VPC 内的 SAE 默认无法访问公网。 +后果:后端调用 DeepSeek 接口会超时;Python 服务无法下载公网 PDF。 + +### **方案 A:NAT 网关 (标准生产方案,推荐)** + +* **操作**:在 VPC 控制台创建一个 **公网 NAT 网关**,并绑定一个 **EIP**。配置 SNAT 条目,允许交换机内的实例访问公网。 +* **成本**:NAT 网关租赁费 \+ EIP 流量费/带宽费。 +* **优点**:稳定,无需修改应用配置。 + +### **方案 B:SAE 绑定公网 IP (省钱方案)** + +* **操作**:在 SAE 应用配置 \-\> 网络配置 中,查看是否支持开启 **公网访问** 或绑定 EIP。 +* **注意**:SAE 某些旧版本或特定地域可能不支持直接绑定 EIP。如果不支持,必须用方案 A。 + +## **🛠️ 关键问题 2:跳板机配置 (如何直连 RDS)** + +为了方便开发人员使用 Navicat/DBeaver 管理 RDS 数据,利用 **Dify ECS** 作为跳板机。 + +### **操作步骤** + +1. **本地终端执行** (建立 SSH 隧道): + \# 格式: ssh \-L 本地端口:RDS地址:RDS端口 root@ECS公网IP + ssh \-N \-L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 root@\ \-i your-key.pem + +2. **Navicat 连接配置**: + * 主机: localhost + * 端口: 5433 + * 用户/密码: RDS 的账号密码 + * *原理:流量通过 ECS 转发到内网 RDS。* + +## **🚀 关键问题 3:一键发布脚本 (NoOps 神器)** + +为 1-2 人团队定制的极简发布脚本。保存为 deploy.sh。 + +\#\!/bin/bash +set \-e + +\# \================= 配置区 \================= +ACR\_REGISTRY="registry.cn-hangzhou.aliyuncs.com" +NAMESPACE="clinical-research" +TIMESTAMP=$(date \+%Y%m%d%H%M) + +\# 颜色 +GREEN='\\033\[0;32m' +NC='\\033\[0m' + +function build\_and\_push() { + SERVICE\_NAME=$1 + DIR\_NAME=$2 + + echo \-e "${GREEN}\>\>\> 开始构建 $SERVICE\_NAME ...${NC}" + + \# 进入目录 + cd $DIR\_NAME + + \# 1\. 构建镜像 + IMAGE\_URL="$ACR\_REGISTRY/$NAMESPACE/$SERVICE\_NAME:$TIMESTAMP" + docker build \-t $IMAGE\_URL . + + \# 2\. 推送镜像 + echo \-e "${GREEN}\>\>\> 推送镜像到 ACR ...${NC}" + docker push $IMAGE\_URL + + \# 3\. 输出更新指引 (如果安装了 aliyun-cli 可以自动更新,否则手动) + echo \-e "${GREEN}\>\>\> ✅ $SERVICE\_NAME 镜像已就绪:${NC}" + echo $IMAGE\_URL + echo "请在 SAE 控制台将 \[$SERVICE\_NAME\] 的镜像版本更新为: $TIMESTAMP" + + \# 回到根目录 + cd .. + echo "----------------------------------------" +} + +\# \================= 主流程 \================= + +\# 1\. 部署 Python 微服务 +\# build\_and\_push "extraction-service" "extraction\_service" + +\# 2\. 部署 Node.js 后端 (记得先同步 Prisma) +\# cp \-r prisma backend/prisma +build\_and\_push "backend-service" "backend" +\# rm \-rf backend/prisma + +\# 3\. 部署前端 +\# build\_and\_push "frontend-service" "frontend-v2" + +echo \-e "${GREEN}🎉 所有构建任务完成!${NC}" + +## **🔐 关键问题 4:OSS 权限与路径规划** + +为了防止文件混乱,建议在 Bucket 内划分明确的目录结构,并通过 IAM Policy 限制权限(可选)。 + +**推荐目录结构**: + +clinical-research-files/ +├── pkb/ \# 个人知识库文件 +│ └── {userId}/ \# 按用户隔离 +├── asl/ \# 文献筛选文件 +│ └── {projectId}/ \# 按项目隔离 +├── dc/ \# 数据清洗文件 +│ └── {tempId}/ \# 临时上传 +└── system/ \# 系统资源 + +**应用代码逻辑**: + +* **私有读写**: Bucket 权限设为 **Private**。 +* **前端访问**: 后端使用 ossClient.signatureUrl() 生成带有效期的 URL (如 1 小时) 返回给前端。**严禁前端直接通过公网 URL 访问。** \ No newline at end of file diff --git a/docs/07-运维文档/03-SAE环境变量配置指南.md b/docs/07-运维文档/03-SAE环境变量配置指南.md index 9e46adbd..74401f27 100644 --- a/docs/07-运维文档/03-SAE环境变量配置指南.md +++ b/docs/07-运维文档/03-SAE环境变量配置指南.md @@ -369,3 +369,4 @@ curl http://你的SAE地址:3001/health + diff --git a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md index db1ab552..98fc1583 100644 --- a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md +++ b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md @@ -701,3 +701,4 @@ const job = await queue.getJob(jobId); + diff --git a/docs/07-运维文档/06-长时间任务可靠性分析.md b/docs/07-运维文档/06-长时间任务可靠性分析.md index c435fca7..960d7bde 100644 --- a/docs/07-运维文档/06-长时间任务可靠性分析.md +++ b/docs/07-运维文档/06-长时间任务可靠性分析.md @@ -468,3 +468,4 @@ processLiteraturesInBackground(task.id, projectId, testLiteratures); + diff --git a/docs/07-运维文档/07-Redis使用需求分析(按模块).md b/docs/07-运维文档/07-Redis使用需求分析(按模块).md index c1a91346..8b526f72 100644 --- a/docs/07-运维文档/07-Redis使用需求分析(按模块).md +++ b/docs/07-运维文档/07-Redis使用需求分析(按模块).md @@ -945,3 +945,4 @@ ROI = (¥22,556 - ¥144) / ¥144 × 100% = 15,564% + diff --git a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md index dc47e751..9c1f7737 100644 --- a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md +++ b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md @@ -1002,3 +1002,4 @@ Redis 实例:¥500/月 **最后更新:** 2025年12月13日 **下次更新:** Phase 8 完成后 + diff --git a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md index 52f1caf6..64c91204 100644 --- a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md +++ b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md @@ -460,3 +460,4 @@ import { ChatContainer } from '@/shared/components/Chat'; + diff --git a/extraction_service/operations/__init__.py b/extraction_service/operations/__init__.py index cc4c880d..7abc9ccb 100644 --- a/extraction_service/operations/__init__.py +++ b/extraction_service/operations/__init__.py @@ -23,3 +23,4 @@ __version__ = '1.0.0' + diff --git a/extraction_service/operations/dropna.py b/extraction_service/operations/dropna.py index 871ca0b3..2a1f404c 100644 --- a/extraction_service/operations/dropna.py +++ b/extraction_service/operations/dropna.py @@ -156,3 +156,4 @@ def get_missing_summary(df: pd.DataFrame) -> dict: + diff --git a/extraction_service/operations/filter.py b/extraction_service/operations/filter.py index 7e913031..9a401015 100644 --- a/extraction_service/operations/filter.py +++ b/extraction_service/operations/filter.py @@ -116,3 +116,4 @@ def apply_filter( + diff --git a/extraction_service/test_dc_api.py b/extraction_service/test_dc_api.py index 4ff60812..35ce8ab8 100644 --- a/extraction_service/test_dc_api.py +++ b/extraction_service/test_dc_api.py @@ -290,3 +290,4 @@ if __name__ == "__main__": + diff --git a/extraction_service/test_execute_simple.py b/extraction_service/test_execute_simple.py index 9a206640..08e1b287 100644 --- a/extraction_service/test_execute_simple.py +++ b/extraction_service/test_execute_simple.py @@ -56,3 +56,4 @@ except Exception as e: + diff --git a/extraction_service/test_module.py b/extraction_service/test_module.py index f0266e9c..582175d3 100644 --- a/extraction_service/test_module.py +++ b/extraction_service/test_module.py @@ -36,3 +36,4 @@ except Exception as e: + diff --git a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx index 6558855b..b5cfdf03 100644 --- a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx +++ b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx @@ -523,5 +523,6 @@ export default FulltextDetailDrawer; + diff --git a/frontend-v2/src/modules/asl/hooks/useFulltextResults.ts b/frontend-v2/src/modules/asl/hooks/useFulltextResults.ts index f3af7681..8958c766 100644 --- a/frontend-v2/src/modules/asl/hooks/useFulltextResults.ts +++ b/frontend-v2/src/modules/asl/hooks/useFulltextResults.ts @@ -122,5 +122,6 @@ export function useFulltextResults({ + diff --git a/frontend-v2/src/modules/asl/hooks/useFulltextTask.ts b/frontend-v2/src/modules/asl/hooks/useFulltextTask.ts index b8cf8eb9..5cc5de37 100644 --- a/frontend-v2/src/modules/asl/hooks/useFulltextTask.ts +++ b/frontend-v2/src/modules/asl/hooks/useFulltextTask.ts @@ -85,5 +85,6 @@ export function useFulltextTask({ + diff --git a/frontend-v2/src/modules/asl/pages/FulltextResults.tsx b/frontend-v2/src/modules/asl/pages/FulltextResults.tsx index 3ccd68d1..3b9986f4 100644 --- a/frontend-v2/src/modules/asl/pages/FulltextResults.tsx +++ b/frontend-v2/src/modules/asl/pages/FulltextResults.tsx @@ -476,5 +476,6 @@ export default FulltextResults; + diff --git a/frontend-v2/src/modules/dc/hooks/useAssets.ts b/frontend-v2/src/modules/dc/hooks/useAssets.ts index cab2b1b8..a92408c3 100644 --- a/frontend-v2/src/modules/dc/hooks/useAssets.ts +++ b/frontend-v2/src/modules/dc/hooks/useAssets.ts @@ -118,3 +118,4 @@ export const useAssets = (activeTab: AssetTabType) => { + diff --git a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts index 85925c1e..493eb524 100644 --- a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts +++ b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts @@ -108,3 +108,4 @@ export const useRecentTasks = () => { + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/BinningDialog_improved.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/BinningDialog_improved.tsx index 1b1e72e0..c58fa94e 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/BinningDialog_improved.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/BinningDialog_improved.tsx @@ -344,3 +344,4 @@ export default BinningDialog; + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx index 92c1788f..0e09c7d7 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx @@ -307,3 +307,4 @@ export default DropnaDialog; + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts b/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts index 886767f5..1d3aa486 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts +++ b/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts @@ -70,3 +70,4 @@ export interface DataStats { + diff --git a/frontend-v2/src/modules/dc/types/portal.ts b/frontend-v2/src/modules/dc/types/portal.ts index 16afb6e0..54b9327d 100644 --- a/frontend-v2/src/modules/dc/types/portal.ts +++ b/frontend-v2/src/modules/dc/types/portal.ts @@ -66,3 +66,4 @@ export type AssetTabType = 'all' | 'processed' | 'raw'; + diff --git a/frontend-v2/src/shared/components/index.ts b/frontend-v2/src/shared/components/index.ts index 326460c9..200b8a4c 100644 --- a/frontend-v2/src/shared/components/index.ts +++ b/frontend-v2/src/shared/components/index.ts @@ -21,3 +21,4 @@ export { default as Placeholder } from './Placeholder'; + diff --git a/python-microservice/operations/__init__.py b/python-microservice/operations/__init__.py index cc4c880d..7abc9ccb 100644 --- a/python-microservice/operations/__init__.py +++ b/python-microservice/operations/__init__.py @@ -23,3 +23,4 @@ __version__ = '1.0.0' + diff --git a/python-microservice/operations/binning.py b/python-microservice/operations/binning.py index a1529128..491d2e81 100644 --- a/python-microservice/operations/binning.py +++ b/python-microservice/operations/binning.py @@ -130,3 +130,4 @@ def apply_binning( + diff --git a/python-microservice/operations/filter.py b/python-microservice/operations/filter.py index 7e913031..9a401015 100644 --- a/python-microservice/operations/filter.py +++ b/python-microservice/operations/filter.py @@ -116,3 +116,4 @@ def apply_filter( + diff --git a/python-microservice/operations/recode.py b/python-microservice/operations/recode.py index 4140f3b0..72d40af0 100644 --- a/python-microservice/operations/recode.py +++ b/python-microservice/operations/recode.py @@ -86,3 +86,4 @@ def apply_recode( + diff --git a/recover_dc_code.py b/recover_dc_code.py index df05865b..40cb9c2b 100644 --- a/recover_dc_code.py +++ b/recover_dc_code.py @@ -229,4 +229,5 @@ if __name__ == "__main__": + diff --git a/run_recovery.ps1 b/run_recovery.ps1 index 4ded81dc..ada91474 100644 --- a/run_recovery.ps1 +++ b/run_recovery.ps1 @@ -53,4 +53,5 @@ Write-Host "==================================================================== + diff --git a/tests/QUICKSTART_快速开始.md b/tests/QUICKSTART_快速开始.md index 06b560f1..8d52f3dc 100644 --- a/tests/QUICKSTART_快速开始.md +++ b/tests/QUICKSTART_快速开始.md @@ -101,3 +101,4 @@ INFO: Uvicorn running on http://0.0.0.0:8001 + diff --git a/tests/README_测试说明.md b/tests/README_测试说明.md index 2058d98f..8e9af9e4 100644 --- a/tests/README_测试说明.md +++ b/tests/README_测试说明.md @@ -257,3 +257,4 @@ df_numeric.to_excel('test_data/numeric_test.xlsx', index=False) + diff --git a/tests/run_tests.bat b/tests/run_tests.bat index 3635fd63..136781f9 100644 --- a/tests/run_tests.bat +++ b/tests/run_tests.bat @@ -52,3 +52,4 @@ pause + diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 279e7990..9a5dbff5 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -48,3 +48,4 @@ echo "========================================" + diff --git a/快速部署到SAE.md b/快速部署到SAE.md index 00bc6df2..5a8eaf39 100644 --- a/快速部署到SAE.md +++ b/快速部署到SAE.md @@ -313,3 +313,4 @@ OSS AccessKeySecret:_______________ + diff --git a/部署检查清单.md b/部署检查清单.md index 129dfc92..f5d518eb 100644 --- a/部署检查清单.md +++ b/部署检查清单.md @@ -349,3 +349,4 @@ OSS配置: +