20 KiB
20 KiB
架构设计与编码规范
📋 项目目标
最终目的:让其他公司能够一键部署或一条命令从Docker部署到自己的NAS或服务器上。
核心原则:
- 性能开销优先 - 最小化资源消耗,确保在低配置设备上也能稳定运行
- 稳定性优先 - 确保长期稳定运行,减少崩溃和错误
- 易于部署 - 零配置或最小配置即可运行
- 可维护性 - 代码清晰,易于理解和维护
- 国内镜像源优先 - 所有依赖、库等外部资源必须使用国内镜像源,确保国内用户快速部署和稳定运行
🏗️ 架构设计原则
1. 资源消耗最小化
1.1 内存管理
-
✅ 禁止内存泄漏
- 所有事件监听器必须正确清理
- 定时器必须清理
- 数据库连接池大小限制
- 文件流必须关闭
-
✅ 避免大对象常驻内存
- 大文件使用流式处理,不全部加载到内存
- 图片处理使用流式API
- 数据库查询使用分页,避免一次性加载大量数据
-
✅ 缓存策略
- 使用内存缓存时设置大小限制和TTL
- 优先使用Redis等外部缓存(如果可用)
- 避免缓存大对象
1.2 CPU优化
-
✅ 避免阻塞主线程
- 所有I/O操作使用异步
- 图片处理使用Worker线程或子进程
- 数据库查询使用连接池
-
✅ 减少不必要的计算
- 禁用Next.js Image Optimizer(图片已在上传时优化)
- 避免重复的图片处理
- 使用数据库索引优化查询
1.3 网络优化
-
✅ 减少网络请求
- 合并API请求
- 使用HTTP/2多路复用
- 静态资源使用CDN或本地缓存
-
✅ 超时和重试机制
- 所有外部API调用设置合理的超时时间
- 实现指数退避重试机制
- 避免无限重试导致资源耗尽
2. 稳定性保障
2.1 错误处理
// ✅ 好的做法:完整的错误处理
try {
const result = await someOperation()
return NextResponse.json({ success: true, data: result })
} catch (error) {
// 记录详细错误信息(用于诊断)
console.error('[Operation] Error:', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
context: { /* 相关上下文信息 */ }
})
// 返回用户友好的错误信息(不暴露内部细节)
return NextResponse.json(
{ error: '操作失败,请稍后重试' },
{ status: 500 }
)
}
// ❌ 不好的做法:忽略错误或暴露内部细节
try {
await someOperation()
} catch (error) {
// 错误被忽略,无法诊断问题
}
2.2 资源清理
// ✅ 好的做法:确保资源清理
async function processFile(file: File) {
let stream: ReadableStream | null = null
try {
stream = file.stream()
// 处理文件
} finally {
// 确保流被关闭
if (stream) {
await stream.cancel()
}
}
}
// ❌ 不好的做法:资源未清理
async function processFile(file: File) {
const stream = file.stream()
// 处理文件,但流可能未关闭
}
2.3 数据库连接管理
// ✅ 好的做法:使用连接池,设置合理的限制
const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
})
// 设置连接池大小(根据服务器配置调整)
// 在DATABASE_URL中设置:?connection_limit=10&pool_timeout=20
// ❌ 不好的做法:无限制的连接
// 可能导致数据库连接耗尽
2.4 文件描述符管理
// ✅ 好的做法:限制并发操作
const MAX_CONCURRENT_UPLOADS = 5
const uploadSemaphore = new Semaphore(MAX_CONCURRENT_UPLOADS)
async function uploadFile(file: File) {
await uploadSemaphore.acquire()
try {
// 上传文件
} finally {
uploadSemaphore.release()
}
}
// ❌ 不好的做法:无限制并发
// 可能导致文件描述符耗尽
3. 部署友好性
3.1 环境变量管理
// ✅ 好的做法:提供合理的默认值
const config = {
port: parseInt(process.env.PORT || '3000', 10),
databaseUrl: process.env.DATABASE_URL || 'postgresql://localhost:5432/app',
// 所有配置都有默认值,可以零配置运行
}
// ❌ 不好的做法:强制要求环境变量
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is required') // 部署时容易出错
}
3.2 健康检查
// ✅ 好的做法:提供健康检查端点
export async function GET() {
try {
// 检查数据库连接
await prisma.$queryRaw`SELECT 1`
// 检查S3连接(如果配置了)
if (process.env.S3_ENDPOINT) {
// 简单的连接测试
}
return NextResponse.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || 'unknown'
})
} catch (error) {
return NextResponse.json(
{ status: 'error', message: 'Health check failed' },
{ status: 503 }
)
}
}
3.3 优雅关闭
// ✅ 好的做法:处理关闭信号
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully...')
// 停止接受新请求
server.close(() => {
console.log('HTTP server closed')
})
// 关闭数据库连接
await prisma.$disconnect()
// 清理其他资源
process.exit(0)
})
4. 监控和诊断
4.1 日志记录
// ✅ 好的做法:结构化日志
function logOperation(operation: string, data: any) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
operation,
...data
}))
}
// 使用示例
logOperation('upload', {
userId: user.id,
fileSize: file.size,
duration: Date.now() - startTime
})
4.2 性能监控
// ✅ 好的做法:记录操作耗时
async function monitoredOperation() {
const startTime = Date.now()
try {
const result = await operation()
const duration = Date.now() - startTime
if (duration > 1000) {
console.warn(`[Performance] Operation took ${duration}ms`)
}
return result
} catch (error) {
const duration = Date.now() - startTime
console.error(`[Performance] Operation failed after ${duration}ms:`, error)
throw error
}
}
📝 编码规范
1. API路由设计
1.1 统一响应格式
// ✅ 好的做法:统一的响应格式
export async function GET() {
try {
const data = await fetchData()
return NextResponse.json({
success: true,
data,
timestamp: new Date().toISOString()
})
} catch (error) {
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
}, { status: 500 })
}
}
1.2 请求验证
// ✅ 好的做法:验证和清理输入
import { z } from 'zod'
const uploadSchema = z.object({
title: z.string().min(1).max(100),
file: z.instanceof(File)
})
export async function POST(request: Request) {
try {
const formData = await request.formData()
const data = uploadSchema.parse({
title: formData.get('title'),
file: formData.get('file')
})
// 处理数据
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.errors },
{ status: 400 }
)
}
throw error
}
}
1.3 权限检查
// ✅ 好的做法:统一的权限检查
async function requireAdmin(session: Session | null) {
if (!session) {
throw new Error('Unauthorized')
}
const isAdmin = session.user.role === 'ADMIN' || (session.user as any).isSuperAdmin
if (!isAdmin) {
throw new Error('Forbidden')
}
return session
}
export async function POST(request: Request) {
const session = await getServerSession(authOptions)
await requireAdmin(session)
// 管理员操作
}
2. 数据库操作
2.1 使用事务
// ✅ 好的做法:关键操作使用事务
await prisma.$transaction(async (tx) => {
// 检查条件
const count = await tx.user.count()
// 执行操作
const user = await tx.user.create({
data: { /* ... */ }
})
return user
})
2.2 查询优化
// ✅ 好的做法:只查询需要的字段
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
username: true,
// 不查询password等大字段
},
take: 20, // 分页
skip: 0
})
// ❌ 不好的做法:查询所有字段
const users = await prisma.user.findMany() // 可能包含大字段
2.3 错误处理
// ✅ 好的做法:处理数据库错误
try {
await prisma.user.create({ data })
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
// 唯一约束冲突
return NextResponse.json(
{ error: '用户已存在' },
{ status: 409 }
)
}
}
throw error
}
3. 文件处理
3.1 流式处理
// ✅ 好的做法:使用流处理大文件
import { pipeline } from 'stream/promises'
import { createReadStream } from 'fs'
async function uploadLargeFile(filePath: string) {
const fileStream = createReadStream(filePath)
const uploadStream = createUploadStream()
await pipeline(fileStream, uploadStream)
// 文件不会全部加载到内存
}
// ❌ 不好的做法:一次性读取整个文件
const fileContent = await fs.readFile(filePath) // 大文件会占用大量内存
3.2 文件大小限制
// ✅ 好的做法:检查文件大小
const MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024 // 10GB
if (file.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: '文件大小超过限制' },
{ status: 413 }
)
}
4. 前端组件
4.1 性能优化
// ✅ 好的做法:使用React.memo和useMemo
import { memo, useMemo } from 'react'
const MaterialCard = memo(({ material }: { material: Material }) => {
const previewUrl = useMemo(() => {
return material.previewUrl || '/placeholder.png'
}, [material.previewUrl])
return (
<Image
src={previewUrl}
alt={material.title}
unoptimized // 图片已在上传时优化,不需要Next.js再次优化
width={300}
height={200}
/>
)
})
4.2 错误边界
// ✅ 好的做法:错误边界处理
'use client'
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error }: { error: Error }) {
return (
<div className="error-container">
<h2>出现错误</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
刷新页面
</button>
</div>
)
}
export default function Page() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
{/* 页面内容 */}
</ErrorBoundary>
)
}
🚀 部署优化
1. Docker优化
1.1 多阶段构建
# ✅ 好的做法:多阶段构建,最小化镜像大小
FROM node:18-alpine AS builder
# 构建阶段
FROM node:18-alpine AS runner
# 运行阶段,只包含必要的文件
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
1.2 层缓存优化
# ✅ 好的做法:依赖文件放在前面,利用Docker缓存
COPY package.json package-lock.json ./
RUN npm ci --production
COPY . .
RUN npm run build
2. 环境配置
2.1 配置优先级
1. 系统环境变量(最高优先级)
2. .env.local(本地开发)
3. .env(项目默认)
4. config-bianliang.env(可迁移配置,最低优先级)
2.2 默认值设计
// ✅ 好的做法:所有配置都有合理的默认值
const config = {
// 数据库:默认使用本地PostgreSQL
databaseUrl: process.env.DATABASE_URL || 'postgresql://postgres:postgres@postgres:5432/youyidian3d',
// S3:默认使用MinIO(Docker Compose中已配置)
s3Endpoint: process.env.S3_ENDPOINT || 'http://minio:9000',
s3Bucket: process.env.S3_BUCKET || 'youyidian3d',
// 端口:默认3000
port: parseInt(process.env.PORT || '3000', 10),
}
3. 国内镜像源配置(必须)
3.1 核心要求
所有外部资源必须使用国内镜像源,包括但不限于:
- npm 包(Node.js 依赖)
- Docker 基础镜像(Alpine、Node.js 等)
- 系统包管理器(apk、apt、yum 等)
- 其他第三方服务(如 CDN、API 等)
3.2 Docker 构建镜像源配置
3.2.1 Alpine Linux 镜像源
# ✅ 好的做法:使用阿里云 Alpine 镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 其他可选镜像源:
# - 清华大学:mirrors.tuna.tsinghua.edu.cn
# - 中科大:mirrors.ustc.edu.cn
3.2.2 npm 镜像源
# ✅ 好的做法:构建时使用国内 npm 镜像源
RUN npm config set registry https://registry.npmmirror.com/
# 安装依赖
RUN npm ci --prefer-offline --no-audit
# 构建完成后清理配置(不在运行镜像中保留)
RUN npm config delete registry
推荐的国内 npm 镜像源:
- npmmirror.com(原淘宝镜像,推荐)
- registry.npmmirror.com(新域名)
- cnpmjs.org(备选)
3.2.3 Docker Hub 镜像源(必须配置)
重要:所有Docker镜像(包括MinIO、PostgreSQL等)必须通过国内镜像加速器拉取。
配置方法(/etc/docker/daemon.json):
{
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com"
]
}
配置后重启Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker
验证配置:
docker info | grep -A 10 "Registry Mirrors"
版本管理要求:
- ✅ 使用特定版本号(如
minio/minio:RELEASE.2025-04-22T22-12-26Z)而不是latest - ✅ 确保版本号在文档中明确记录
- ✅ 版本更新前必须在测试环境验证
3.3 本地开发环境配置
3.3.1 npm 镜像源(本地开发)
# 永久配置(推荐)
npm config set registry https://registry.npmmirror.com/
# 或使用 .npmrc 文件(项目级别)
echo "registry=https://registry.npmmirror.com/" > .npmrc
3.3.2 验证镜像源配置
# 检查 npm 镜像源
npm config get registry
# 检查 Docker 镜像源(如果配置了)
docker info | grep -A 10 "Registry Mirrors"
3.4 代码中的外部资源引用
3.4.1 CDN 资源
// ✅ 好的做法:使用国内 CDN
const CDN_BASE_URL = process.env.CDN_BASE_URL || 'https://cdn.jsdelivr.net'
// 推荐使用:
// - jsDelivr(有国内节点)
// - unpkg(有国内节点)
// - 自建 CDN 或使用阿里云/腾讯云 CDN
3.4.2 外部 API
// ✅ 好的做法:优先使用国内 API 服务
// 如果必须使用国外服务,考虑:
// 1. 使用代理
// 2. 使用国内替代方案
// 3. 实现缓存机制减少请求
3.5 部署脚本中的镜像源配置
3.5.1 PowerShell 部署脚本
# ✅ 好的做法:部署脚本中配置镜像源
# 在 Dockerfile 中已配置,部署脚本无需额外配置
# 但如果需要,可以在构建前设置环境变量
$env:NPM_CONFIG_REGISTRY = "https://registry.npmmirror.com/"
3.5.2 服务器端配置
# 如果需要在服务器上直接安装依赖,配置 npm 镜像源
ssh user@server "npm config set registry https://registry.npmmirror.com/"
3.6 镜像源配置检查清单
在提交代码前,确保:
- Dockerfile 中配置了 Alpine 镜像源
- Dockerfile 中配置了 npm 镜像源(构建阶段)
- 本地开发环境配置了 npm 镜像源
- 服务器上配置了 Docker 镜像加速器
- docker-compose.yml 中使用特定版本号(不使用
latest) - 所有外部 CDN 资源优先使用国内服务
- 部署文档中说明了镜像源配置要求
3.7 Docker镜像版本管理
3.7.1 使用特定版本号(必须)
# ✅ 好的做法:使用特定版本号
services:
minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
postgres:
image: postgres:15-alpine
# ❌ 不好的做法:使用latest(可能导致不稳定)
services:
minio:
image: minio/minio:latest
3.7.2 版本更新流程
- 测试环境验证:在测试环境部署新版本,验证兼容性
- 文档记录:在CHANGELOG中记录版本更新和变更
- 逐步部署:先在部分环境部署,确认稳定后再全面更新
- 回滚准备:保留旧版本配置,确保可以快速回滚
3.7.3 版本号管理建议
# 使用环境变量管理版本(推荐)
MINIO_VERSION=${MINIO_VERSION:-RELEASE.2025-04-22T22-12-26Z}
services:
minio:
image: minio/minio:${MINIO_VERSION}
📊 性能指标
目标指标
-
内存使用
- 空闲时:< 200MB
- 正常运行时:< 500MB
- 峰值:< 1GB
-
CPU使用
- 空闲时:< 5%
- 正常运行时:< 30%
- 峰值:< 80%
-
响应时间
- API响应:< 200ms(95分位)
- 页面加载:< 2s(首次加载)
- 数据库查询:< 100ms(95分位)
-
稳定性
- 正常运行时间:> 99.9%
- 错误率:< 0.1%
- 崩溃恢复时间:< 30s
🔍 代码审查清单
在提交代码前,检查以下项目:
性能
- 是否避免了内存泄漏?
- 是否使用了流式处理大文件?
- 是否限制了并发操作?
- 是否优化了数据库查询?
- 是否禁用了不必要的优化(如Next.js Image Optimizer)?
稳定性
- 是否处理了所有可能的错误?
- 是否清理了所有资源(文件流、数据库连接等)?
- 是否设置了合理的超时时间?
- 是否实现了重试机制?
- 是否添加了健康检查?
部署友好性
- 是否提供了合理的默认值?
- 是否支持零配置运行?
- 是否添加了必要的日志?
- 是否处理了优雅关闭?
- 是否配置了国内镜像源(npm、Docker、系统包管理器)?
- 是否避免了直接引用国外 CDN 或 API?
📚 参考资源
最后更新:2025-11-15 维护者:开发团队
📌 重要提醒
国内镜像源配置(必须遵守)
本项目主要面向国内用户,所有外部资源必须使用国内镜像源:
-
✅ Dockerfile 中已配置:
- Alpine Linux:使用阿里云镜像源
- npm:使用 npmmirror.com 镜像源
-
✅ 本地开发环境:
- 建议配置 npm 镜像源:
npm config set registry https://registry.npmmirror.com/
- 建议配置 npm 镜像源:
-
⚠️ 新增依赖或修改配置时:
- 必须确保使用国内镜像源
- 禁止直接使用国外源(npmjs.org、docker.io 等)
- 代码审查时必须检查镜像源配置
-
📝 文档要求:
- 所有部署文档必须说明镜像源配置
- 新加入的开发者必须了解镜像源要求