# 云原生开发规范 > **文档版本:** V1.1 > **创建日期:** 2025-11-16 > **最后更新:** 2025-12-13 🏆 **Postgres-Only 架构规范新增** > **适用对象:** 所有开发人员 > **强制性:** ✅ 必须遵守 > **维护者:** 架构团队 --- ## 📋 文档说明 本文档定义云原生环境(Serverless SAE + RDS + OSS)下的**代码规范**,所有业务模块(ASL、AIA、PKB等)必须遵守。 **阅读时间**:10 分钟 **检查频率**:每次代码提交前 --- ## 🌟 核心原则:复用平台能力 > **⭐ 重要提示(2025-11-16 更新)**:平台已提供完整的基础设施服务 > **详细文档**:[平台基础设施规划](../09-架构实施/04-平台基础设施规划.md) ### 平台已提供的服务 **业务模块(ASL/AIA/PKB/DC等)应该复用以下平台能力,禁止重复实现:** | 服务 | 导入方式 | 用途 | 文档 | |------|---------|------|------| | **存储服务** | `import { storage } from '@/common/storage'` | 文件上传下载 | ✅ 平台级 | | **日志系统** | `import { logger } from '@/common/logging'` | 标准化日志 | ✅ 平台级 | | **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务 | ✅ 平台级 | | **缓存服务** | `import { cache } from '@/common/cache'` | 分布式缓存 | ✅ 平台级 | | **🏆 断点续传** | `import { CheckpointService } from '@/common/jobs'` | 任务断点管理 | ✅ 平台级(新) | | **数据库** | `import { prisma } from '@/config/database'` | 数据库操作 | ✅ 平台级 | | **LLM能力** | `import { LLMFactory } from '@/common/llm'` | LLM调用 | ✅ 平台级 | ### 示例:正确使用平台服务 ```typescript // ✅ 正确:直接导入平台服务 import { storage } from '@/common/storage' import { logger } from '@/common/logging' import { jobQueue } from '@/common/jobs' import { prisma } from '@/config/database' export class LiteratureService { async uploadPDF(projectId: string, pdfBuffer: Buffer) { // 1. 使用平台存储服务 const key = `asl/projects/${projectId}/pdfs/${Date.now()}.pdf` const url = await storage.upload(key, pdfBuffer) // 2. 使用平台日志系统 logger.info('PDF uploaded', { projectId, url }) // 3. 使用平台数据库 const literature = await prisma.aslLiterature.create({ data: { projectId, pdfUrl: url } }) return literature } } ``` ### ❌ 错误:重复实现平台能力 ```typescript // ❌ 错误:在业务模块中自己实现存储 // backend/src/modules/asl/storage/LocalStorage.ts ← 不应该存在! export class LocalStorage { async upload(file: Buffer) { await fs.writeFile('./uploads/file.pdf', file) // ❌ 重复实现 } } // ❌ 错误:在业务模块中自己实现日志 // backend/src/modules/asl/logger/logger.ts ← 不应该存在! export const logger = winston.createLogger({...}) // ❌ 重复实现 ``` **原因**: - ❌ 重复代码,难以维护 - ❌ 不同模块实现不一致 - ❌ 无法统一切换环境(本地/云端) - ❌ 浪费开发时间 --- ## ✅ 推荐做法(DO) ### 1. 文件存储 ✅ ```typescript // ✅ 正确:使用存储抽象层 import { storage } from '@/common/storage/StorageFactory' export async function uploadFile(file: Buffer, filename: string) { const key = `asl/pdfs/${Date.now()}-${filename}` const url = await storage.upload(key, file) return url } // ✅ 正确:Excel 直接从内存解析 import * as xlsx from 'xlsx' export async function importExcel(buffer: Buffer) { const workbook = xlsx.read(buffer, { type: 'buffer' }) // 内存解析 const data = xlsx.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]) return data } ``` **理由**: - 容器重启不会丢失文件 - 本地开发和生产环境代码一致 - 自动根据环境变量切换存储方式 --- ### 2. 数据库连接 ✅ ```typescript // ✅ 正确:使用全局 Prisma Client import { prisma } from '@/config/database' export async function createProject(data: any) { return await prisma.aslScreeningProject.create({ data }) } // ✅ 正确:批量操作使用事务 export async function importLiteratures(literatures: any[]) { return await prisma.$transaction(async (tx) => { return await tx.aslLiterature.createMany({ data: literatures }) }) } ``` **理由**: - 全局实例复用连接 - 避免连接数耗尽 - 事务保证数据一致性 --- ### 3. 环境变量配置 ✅ ```typescript // ✅ 正确:统一配置管理 // backend/src/config/env.ts export const config = { llm: { apiKey: process.env.LLM_API_KEY!, baseUrl: process.env.LLM_BASE_URL!, }, oss: { region: process.env.OSS_REGION!, bucket: process.env.OSS_BUCKET!, }, database: { url: process.env.DATABASE_URL!, } } // ✅ 正确:使用配置对象 import { config } from '@/config/env' const apiKey = config.llm.apiKey ``` **理由**: - 配置集中管理 - 类型安全 - 便于切换环境 --- ### 4. 长时间任务处理 ✅ ```typescript // ✅ 正确:异步任务 + 进度轮询 export async function startScreening(req, res) { // 1. 创建任务记录 const task = await prisma.aslScreeningTask.create({ data: { projectId: req.params.projectId, status: 'pending', totalItems: 100, } }) // 2. 立即返回任务ID res.send({ success: true, taskId: task.id }) // 3. 后台异步执行(不阻塞请求) processScreeningAsync(task.id).catch(err => { console.error('筛选任务失败:', err) }) } // 前端轮询进度 export async function getTaskProgress(req, res) { const task = await prisma.aslScreeningTask.findUnique({ where: { id: req.params.taskId } }) res.send({ status: task.status, progress: Math.round((task.completedItems / task.totalItems) * 100) }) } ``` **理由**: - 避免请求超时(SAE默认30秒) - 用户体验更好 - 支持批量任务 **✨ 完整实践参考**: 详见 [Postgres-Only异步任务处理指南](../02-通用能力层/Postgres-Only异步任务处理指南.md)(基于DC Tool C完整实践) --- ### 5. 日志输出 ✅ ```typescript // ✅ 正确:使用 logger 输出到 stdout import pino from 'pino' const logger = pino({ level: process.env.LOG_LEVEL || 'info' }) export async function uploadFile(req, res) { logger.info({ userId: req.userId, filename: req.file.filename }, 'File uploaded') // ... 上传逻辑 } // ✅ 正确:结构化日志 logger.error({ error: err.message, stack: err.stack, userId: req.userId }, 'Upload failed') ``` **理由**: - SAE 自动采集 stdout 日志 - 结构化便于查询分析 - 集中查看,不会丢失 --- ### 6. 错误处理 ✅ ```typescript // ✅ 正确:统一错误处理 export async function uploadPdf(req, res) { try { const file = await req.file() const buffer = await file.toBuffer() const url = await storage.upload(key, buffer) res.send({ success: true, url }) } catch (error) { logger.error({ error }, 'PDF upload failed') res.status(500).send({ success: false, error: { code: 'UPLOAD_FAILED', message: '文件上传失败,请重试' } }) } } ``` **理由**: - 用户看到友好错误信息 - 日志记录详细错误 - 不暴露内部实现 --- ### 7. 临时文件处理 ✅ ```typescript // ✅ 正确:/tmp 目录用完立即删除 import fs from 'fs/promises' import path from 'path' export async function extractPdfText(ossKey: string): Promise { const tmpPath = path.join('/tmp', `${Date.now()}.pdf`) try { // 1. 从 OSS 下载到 /tmp await storage.download(ossKey, tmpPath) // 2. 提取文本 const text = await extractWithNougat(tmpPath) return text } finally { // 3. 立即删除临时文件(无论成功失败) try { await fs.unlink(tmpPath) } catch (err) { logger.warn({ tmpPath }, 'Failed to delete temp file') } } } ``` **理由**: - /tmp 容量有限(512MB) - 容器重启会清空 - 避免磁盘占满 --- ## 🏆 Postgres-Only 架构规范(2025-12-13 新增) ### 核心理念 **Platform-Only 模式**:所有任务管理信息统一存储在 `platform_schema.job.data`,业务表只存储业务信息。 ### 任务管理的正确做法 #### ✅ DO: 使用 job.data 存储任务管理信息 ```typescript // ✅ 正确:任务拆分和断点信息存储在 job.data import { jobQueue } from '@/common/jobs'; import { CheckpointService } from '@/common/jobs/CheckpointService'; // 推送任务时,包含完整信息 await jobQueue.push('asl:screening:batch', { // 业务信息 taskId: 'xxx', projectId: 'yyy', literatureIds: [...], // ✅ 任务拆分信息(存储在 job.data) batchIndex: 3, totalBatches: 20, startIndex: 150, endIndex: 200, // ✅ 进度追踪 processedCount: 0, successCount: 0, failedCount: 0, }); // Worker 中使用 CheckpointService const checkpointService = new CheckpointService(prisma); // 保存断点到 job.data await checkpointService.saveCheckpoint(job.id, { currentBatchIndex: 5, currentIndex: 250, processedBatches: 5, totalBatches: 20 }); // 加载断点从 job.data const checkpoint = await checkpointService.loadCheckpoint(job.id); if (checkpoint) { resumeFrom = checkpoint.currentIndex; } ``` #### ❌ DON'T: 在业务表中存储任务管理信息 ```typescript // ❌ 错误:在业务表的 Schema 中添加任务管理字段 model AslScreeningTask { id String @id projectId String // ❌ 不要添加这些字段! totalBatches Int // ← 应该在 job.data 中 processedBatches Int // ← 应该在 job.data 中 currentIndex Int // ← 应该在 job.data 中 checkpointData Json? // ← 应该在 job.data 中 } // ❌ 错误:自己实现断点服务 class MyCheckpointService { async save(taskId: string) { await prisma.aslScreeningTask.update({ where: { id: taskId }, data: { checkpointData: {...} } // ❌ 不要这样做! }); } } ``` **为什么不对?** - ❌ 每个模块都要添加相同的字段(代码重复) - ❌ 违反 DRY 原则 - ❌ 违反 3 层架构原则 - ❌ 维护困难(修改逻辑需要改多处) ### 智能阈值判断的规范 #### ✅ DO: 实现智能双模式处理 ```typescript const QUEUE_THRESHOLD = 50; // 推荐阈值 export async function startTask(items: any[]) { const useQueue = items.length >= QUEUE_THRESHOLD; if (useQueue) { // 队列模式:大任务(≥50条) const chunks = splitIntoChunks(items, 50); for (const chunk of chunks) { await jobQueue.push('task:batch', {...}); } } else { // 直接模式:小任务(<50条) processDirectly(items); // 快速响应 } } ``` **为什么这样做?** - ✅ 小任务快速响应(无队列延迟) - ✅ 大任务高可靠(支持断点续传) - ✅ 性能与可靠性平衡 #### ❌ DON'T: 所有任务都走队列 ```typescript // ❌ 错误:即使1条记录也使用队列 export async function startTask(items: any[]) { // 无论多少数据,都推送到队列 await jobQueue.push('task:batch', items); // ❌ 小任务会有延迟 } ``` **为什么不对?** - ❌ 小任务响应慢(队列有轮询间隔) - ❌ 浪费队列资源 - ❌ 用户体验差 ### 阈值推荐值 | 任务类型 | 推荐阈值 | 理由 | |---------|---------|------| | 文献筛选 | 50篇 | 单篇~7秒,50篇~5分钟 | | 数据提取 | 50条 | 单条~5-10秒,50条~5分钟 | | 统计模型 | 30个 | 单个~10秒,30个~5分钟 | | 默认 | 50条 | 通用推荐值 | --- ## ❌ 禁止做法(DON'T) ### 1. 本地文件存储 ❌ ```typescript // ❌ 禁止:本地文件系统存储 import fs from 'fs' export async function uploadFile(req, res) { const file = await req.file() const buffer = await file.toBuffer() // ❌ 错误:容器重启丢失 fs.writeFileSync('./uploads/file.pdf', buffer) res.send({ url: '/uploads/file.pdf' }) } // ❌ 禁止:依赖本地路径 const uploadDir = '/var/app/uploads' const filePath = path.join(uploadDir, filename) ``` **问题**: - 容器重启或扩容后文件丢失 - 多实例间无法共享文件 - 磁盘空间有限 **正确做法**:使用 `storage.upload()` 上传到 OSS --- ### 2. 内存缓存 ❌ ```typescript // ❌ 禁止:内存缓存(多实例不共享) const cache = new Map() export async function getProject(id: string) { // ❌ 错误:扩容后其他实例读不到缓存 if (cache.has(id)) { return cache.get(id) } const project = await prisma.aslScreeningProject.findUnique({ where: { id } }) cache.set(id, project) return project } // ❌ 禁止:全局变量存储状态 let taskStatus = {} // 多实例不同步 ``` **问题**: - 多实例间数据不同步 - 扩容后缓存失效 - 内存占用不可控 **正确做法**:使用 Redis 或数据库 --- ### 3. 硬编码配置 ❌ ```typescript // ❌ 禁止:硬编码 const LLM_API_KEY = 'sk-xxx' const DB_HOST = '192.168.1.100' const OSS_BUCKET = 'my-bucket' // ❌ 禁止:写死端口 app.listen(3001) // ❌ 禁止:写死域名 const baseUrl = 'https://api.example.com' ``` **问题**: - 无法切换环境 - 安全风险(密钥泄露) - 部署困难 **正确做法**: ```typescript // ✅ 使用环境变量 const apiKey = process.env.LLM_API_KEY const port = process.env.PORT || 3001 app.listen(port) ``` --- ### 4. 同步长任务 ❌ ```typescript // ❌ 禁止:同步处理长任务 export async function screenLiteratures(req, res) { const literatures = await prisma.aslLiterature.findMany({...}) // ❌ 错误:100篇可能超过30秒 for (const lit of literatures) { await llmScreening(lit) // 每篇2-3秒 } res.send({ success: true }) // 可能已经超时 } // ❌ 禁止:没有超时保护 const result = await axios.get(url) // 可能永久等待 ``` **问题**: - SAE 请求超时 30 秒 - 前端等待时间过长 - 无法显示进度 **正确做法**:异步任务 + 进度轮询(见 DO 第4条) --- ### 5. 本地日志文件 ❌ ```typescript // ❌ 禁止:写入本地文件 import fs from 'fs' export function logError(error: Error) { // ❌ 错误:容器重启丢失,无法集中查看 fs.appendFileSync('/var/log/app.log', error.message + '\n') } // ❌ 禁止:使用 console.log 而不用 logger console.log('User logged in') // 无结构化,难以查询 ``` **问题**: - 容器重启日志丢失 - 多实例日志分散 - 无法集中分析 **正确做法**: ```typescript // ✅ 输出到 stdout,使用 logger logger.info({ userId, action: 'login' }, 'User logged in') ``` --- ### 6. 新建数据库连接 ❌ ```typescript // ❌ 禁止:每次请求新建连接 import { PrismaClient } from '@prisma/client' export async function getProjects(req, res) { // ❌ 错误:每次新建,连接数暴增 const prisma = new PrismaClient() const projects = await prisma.aslScreeningProject.findMany() await prisma.$disconnect() res.send(projects) } // ❌ 禁止:直接使用 pg 或其他驱动 import { Pool } from 'pg' const pool = new Pool({ connectionString: process.env.DATABASE_URL }) ``` **问题**: - 连接数快速耗尽(RDS限制 400 连接) - 性能低下(连接建立耗时) - 资源浪费 **正确做法**: ```typescript // ✅ 使用全局 Prisma Client import { prisma } from '@/config/database' const projects = await prisma.aslScreeningProject.findMany() ``` --- ### 7. 忽略错误 ❌ ```typescript // ❌ 禁止:空的 catch try { await storage.upload(key, buffer) } catch (error) { // ❌ 错误被吞掉,无法排查 } // ❌ 禁止:不处理 Promise rejection processAsync(taskId) // 没有 .catch() // ❌ 禁止:返回模糊错误 catch (error) { res.status(500).send({ error: 'Something went wrong' }) // 用户不知道什么错了,如何解决 } ``` **问题**: - 错误无法追踪 - 用户体验差 - 排查困难 **正确做法**: ```typescript // ✅ 记录日志 + 友好错误信息 try { await storage.upload(key, buffer) } catch (error) { logger.error({ error, key }, 'Upload failed') res.status(500).send({ success: false, error: { code: 'UPLOAD_FAILED', message: '文件上传失败,请检查网络后重试' } }) } ``` --- ## 🔍 代码审查检查清单 在**提交代码前**,请逐项检查: ### 文件存储 - [ ] 是否使用 `storage.upload()` 而非 `fs.writeFile()`? - [ ] Excel 是否从内存解析,而非保存到本地? - [ ] PDF 提取后是否立即删除临时文件? ### 数据库 - [ ] 是否使用全局 `prisma` 实例? - [ ] 是否避免在循环中执行单条查询?(应该批量操作) - [ ] 批量操作是否使用事务? ### 配置管理 - [ ] 是否所有配置都从 `process.env` 读取? - [ ] 是否没有硬编码的 IP、域名、密钥? - [ ] `.env.example` 是否已更新? ### 长时间任务 - [ ] 超过 10 秒的任务是否改为异步? - [ ] 是否提供了进度查询接口? - [ ] 前端是否有轮询或 WebSocket 获取进度? ### 日志 - [ ] 是否使用 `logger` 而非 `console.log`? - [ ] 日志是否结构化(JSON格式)? - [ ] 是否记录了关键操作(userId、action)? ### 错误处理 - [ ] 所有 async 函数是否有 try-catch? - [ ] 是否记录了详细错误日志? - [ ] 是否返回了友好的错误信息? ### 临时文件 - [ ] `/tmp` 目录使用后是否立即删除? - [ ] 是否在 `finally` 块中清理? - [ ] 是否避免长期依赖 `/tmp`? --- ## 🎯 快速自检(5分钟) **运行以下命令,检查代码中是否有违规**: ```bash # 检查是否有本地文件存储 grep -r "fs.writeFile\|fs.appendFile" backend/src/modules/ # 检查是否有硬编码配置 grep -r "sk-\|http://\|192.168" backend/src/modules/ # 检查是否有新建 Prisma 连接 grep -r "new PrismaClient" backend/src/modules/ # 检查是否有 console.log grep -r "console.log" backend/src/modules/ ``` **预期结果**:所有检查应该返回 **0 个匹配** --- ## 📚 参考文档 - [云原生部署架构指南](../09-架构实施/03-云原生部署架构指南.md) - 包含完整代码示例 - [前后端模块化架构设计-V2](../00-系统总体设计/前后端模块化架构设计-V2.md) - 架构总纲 - [数据库设计规范](./01-数据库设计规范.md) - [API设计规范](./02-API设计规范.md) - [代码规范](./05-代码规范.md) - [Git提交规范](./06-Git提交规范.md) --- ## 📝 更新日志 | 日期 | 版本 | 变更内容 | 维护者 | |------|------|---------|--------| | 2025-11-16 | V1.0 | 创建文档,定义云原生开发规范 | 架构团队 | --- **文档维护者:** 架构团队 **最后更新:** 2025-11-16 **文档状态:** ✅ 已完成 **强制执行:** ✅ 所有代码提交前必须检查