Files
AIclinicalresearch/docs/04-开发规范/08-云原生开发规范.md
HaHafeng 66255368b7 feat(admin): Add user management and upgrade to module permission system
Features - User Management (Phase 4.1):
- Database: Add user_modules table for fine-grained module permissions
- Database: Add 4 user permissions (view/create/edit/delete) to role_permissions
- Backend: UserService (780 lines) - CRUD with tenant isolation
- Backend: UserController + UserRoutes (648 lines) - 13 API endpoints
- Backend: Batch import users from Excel
- Frontend: UserListPage (412 lines) - list/filter/search/pagination
- Frontend: UserFormPage (341 lines) - create/edit with module config
- Frontend: UserDetailPage (393 lines) - details/tenant/module management
- Frontend: 3 modal components (592 lines) - import/assign/configure
- API: GET/POST/PUT/DELETE /api/admin/users/* endpoints

Architecture Upgrade - Module Permission System:
- Backend: Add getUserModules() method in auth.service
- Backend: Login API returns modules array in user object
- Frontend: AuthContext adds hasModule() method
- Frontend: Navigation filters modules based on user.modules
- Frontend: RouteGuard checks requiredModule instead of requiredVersion
- Frontend: Remove deprecated version-based permission system
- UX: Only show accessible modules in navigation (clean UI)
- UX: Smart redirect after login (avoid 403 for regular users)

Fixes:
- Fix UTF-8 encoding corruption in ~100 docs files
- Fix pageSize type conversion in userService (String to Number)
- Fix authUser undefined error in TopNavigation
- Fix login redirect logic with role-based access check
- Update Git commit guidelines v1.2 with UTF-8 safety rules

Database Changes:
- CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled)
- ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code)
- INSERT 4 permissions + role assignments
- UPDATE PUBLIC tenant with 8 module subscriptions

Technical:
- Backend: 5 new files (~2400 lines)
- Frontend: 10 new files (~2500 lines)
- Docs: 1 development record + 2 status updates + 1 guideline update
- Total: ~4900 lines of code

Status: User management 100% complete, module permission system operational
2026-01-16 13:42:10 +08:00

779 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云原生开发规范
> **文档版本:** 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<string> {
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<string, any>()
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
**文档状态:** ✅ 已完成
**强制执行:** ✅ 所有代码提交前必须检查