init doc
This commit is contained in:
543
docs/后端开发文档.md
Normal file
543
docs/后端开发文档.md
Normal file
@ -0,0 +1,543 @@
|
||||
# 摄影作品集网站 - 后端开发文档
|
||||
|
||||
## 1. 技术架构
|
||||
|
||||
### 1.1 技术栈选择
|
||||
- **后端框架**:Node.js + Express/Fastify
|
||||
- **数据库**:PostgreSQL + Redis
|
||||
- **图片处理**:Sharp + ImageMagick
|
||||
- **文件存储**:MinIO/AWS S3
|
||||
- **缓存策略**:Redis + CDN
|
||||
- **任务队列**:Bull Queue + Redis
|
||||
|
||||
### 1.2 系统架构
|
||||
```
|
||||
[客户端] <-> [CDN] <-> [API网关] <-> [应用服务器]
|
||||
├── [图片处理服务]
|
||||
├── [文件管理服务]
|
||||
└── [元数据服务]
|
||||
↓
|
||||
[缓存层 Redis] <-> [数据库 PostgreSQL]
|
||||
↓
|
||||
[对象存储 MinIO/S3]
|
||||
```
|
||||
|
||||
## 2. 多格式图片处理系统
|
||||
|
||||
### 2.1 图片格式管理
|
||||
```javascript
|
||||
// services/ImageProcessingService.js
|
||||
class ImageProcessingService {
|
||||
async processRawPhoto(rawFile, metadata) {
|
||||
const formats = {};
|
||||
|
||||
// 保存原始RAW文件
|
||||
formats.raw = await this.saveRawFile(rawFile);
|
||||
|
||||
// 生成高质量JPG
|
||||
formats.jpg = await this.generateJPG(rawFile, { quality: 95 });
|
||||
|
||||
// 生成WebP格式
|
||||
formats.webp = await this.generateWebP(rawFile, { quality: 85 });
|
||||
|
||||
// 生成多种尺寸的缩略图
|
||||
formats.thumbnails = await this.generateThumbnails(rawFile, [
|
||||
{ name: 'thumb', width: 300 },
|
||||
{ name: 'medium', width: 800 },
|
||||
{ name: 'large', width: 1600 }
|
||||
]);
|
||||
|
||||
return formats;
|
||||
}
|
||||
|
||||
async generateWebP(sourceFile, options) {
|
||||
return sharp(sourceFile)
|
||||
.webp(options)
|
||||
.toBuffer()
|
||||
.then(buffer => this.uploadToStorage(buffer, 'webp'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 智能压缩算法
|
||||
```javascript
|
||||
// utils/compressionUtils.js
|
||||
export class SmartCompression {
|
||||
static async optimizeForDevice(imageBuffer, deviceType) {
|
||||
const config = {
|
||||
mobile: { width: 800, quality: 75 },
|
||||
tablet: { width: 1200, quality: 80 },
|
||||
desktop: { width: 1920, quality: 85 }
|
||||
};
|
||||
|
||||
return sharp(imageBuffer)
|
||||
.resize(config[deviceType].width, null, {
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.jpeg({ quality: config[deviceType].quality })
|
||||
.toBuffer();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 数据模型设计
|
||||
|
||||
### 3.1 数据库Schema
|
||||
```sql
|
||||
-- 图片表
|
||||
CREATE TABLE photos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
original_filename VARCHAR(255),
|
||||
file_size BIGINT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
-- 元数据
|
||||
camera VARCHAR(100),
|
||||
lens VARCHAR(100),
|
||||
iso INTEGER,
|
||||
aperture VARCHAR(10),
|
||||
shutter_speed VARCHAR(20),
|
||||
focal_length VARCHAR(20),
|
||||
|
||||
-- 位置信息
|
||||
latitude DECIMAL(10, 8),
|
||||
longitude DECIMAL(11, 8),
|
||||
location_name VARCHAR(255),
|
||||
|
||||
-- 状态
|
||||
status VARCHAR(20) DEFAULT 'processing',
|
||||
visibility VARCHAR(20) DEFAULT 'public'
|
||||
);
|
||||
|
||||
-- 文件格式表
|
||||
CREATE TABLE photo_formats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
photo_id UUID REFERENCES photos(id) ON DELETE CASCADE,
|
||||
format_type VARCHAR(10) NOT NULL, -- 'raw', 'jpg', 'webp', 'thumb'
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_size BIGINT,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 收藏夹表
|
||||
CREATE TABLE collections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
cover_photo_id UUID REFERENCES photos(id),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 图片收藏夹关联表
|
||||
CREATE TABLE photo_collections (
|
||||
photo_id UUID REFERENCES photos(id) ON DELETE CASCADE,
|
||||
collection_id UUID REFERENCES collections(id) ON DELETE CASCADE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (photo_id, collection_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 数据访问层
|
||||
```javascript
|
||||
// models/PhotoModel.js
|
||||
class PhotoModel {
|
||||
static async findWithFormats(photoId) {
|
||||
const query = `
|
||||
SELECT
|
||||
p.*,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'format_type', pf.format_type,
|
||||
'file_path', pf.file_path,
|
||||
'width', pf.width,
|
||||
'height', pf.height
|
||||
)
|
||||
) as formats
|
||||
FROM photos p
|
||||
LEFT JOIN photo_formats pf ON p.id = pf.photo_id
|
||||
WHERE p.id = $1
|
||||
GROUP BY p.id
|
||||
`;
|
||||
|
||||
return await db.query(query, [photoId]);
|
||||
}
|
||||
|
||||
static async findByTimeline(year, month = null) {
|
||||
let query = `
|
||||
SELECT * FROM photos
|
||||
WHERE EXTRACT(YEAR FROM created_at) = $1
|
||||
`;
|
||||
|
||||
const params = [year];
|
||||
|
||||
if (month) {
|
||||
query += ` AND EXTRACT(MONTH FROM created_at) = $2`;
|
||||
params.push(month);
|
||||
}
|
||||
|
||||
query += ` ORDER BY created_at DESC`;
|
||||
|
||||
return await db.query(query, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. API设计
|
||||
|
||||
### 4.1 RESTful API结构
|
||||
```javascript
|
||||
// routes/api/photos.js
|
||||
const router = express.Router();
|
||||
|
||||
// 获取图片列表
|
||||
router.get('/', async (req, res) => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
collection,
|
||||
year,
|
||||
month
|
||||
} = req.query;
|
||||
|
||||
try {
|
||||
const photos = await PhotoService.getPhotos({
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
collection,
|
||||
year: year ? parseInt(year) : null,
|
||||
month: month ? parseInt(month) : null
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: photos,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: photos.total
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取时间线数据
|
||||
router.get('/timeline', async (req, res) => {
|
||||
try {
|
||||
const timeline = await PhotoService.getTimeline();
|
||||
res.json({ data: timeline });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 上传图片
|
||||
router.post('/', upload.single('photo'), async (req, res) => {
|
||||
try {
|
||||
const result = await PhotoService.uploadPhoto(req.file, req.body);
|
||||
res.status(201).json({ data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4.2 GraphQL Schema (可选)
|
||||
```graphql
|
||||
type Photo {
|
||||
id: ID!
|
||||
title: String!
|
||||
description: String
|
||||
createdAt: DateTime!
|
||||
|
||||
formats: [PhotoFormat!]!
|
||||
metadata: PhotoMetadata
|
||||
collections: [Collection!]!
|
||||
}
|
||||
|
||||
type PhotoFormat {
|
||||
type: FormatType!
|
||||
url: String!
|
||||
width: Int
|
||||
height: Int
|
||||
fileSize: Int
|
||||
}
|
||||
|
||||
enum FormatType {
|
||||
RAW
|
||||
JPG
|
||||
WEBP
|
||||
THUMBNAIL
|
||||
}
|
||||
|
||||
type Query {
|
||||
photos(
|
||||
first: Int
|
||||
after: String
|
||||
collection: ID
|
||||
year: Int
|
||||
month: Int
|
||||
): PhotoConnection!
|
||||
|
||||
timeline: [TimelineGroup!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
uploadPhoto(input: PhotoUploadInput!): Photo!
|
||||
updatePhoto(id: ID!, input: PhotoUpdateInput!): Photo!
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 文件存储管理
|
||||
|
||||
### 5.1 存储策略
|
||||
```javascript
|
||||
// services/StorageService.js
|
||||
class StorageService {
|
||||
constructor() {
|
||||
this.storage = process.env.NODE_ENV === 'production'
|
||||
? new S3Storage()
|
||||
: new LocalStorage();
|
||||
}
|
||||
|
||||
async uploadPhoto(buffer, metadata) {
|
||||
const filename = this.generateFilename(metadata);
|
||||
const path = this.getStoragePath(metadata.date, filename);
|
||||
|
||||
// 上传到存储服务
|
||||
const url = await this.storage.upload(buffer, path);
|
||||
|
||||
// 更新CDN缓存
|
||||
await this.invalidateCache(url);
|
||||
|
||||
return { url, path };
|
||||
}
|
||||
|
||||
generateFilename(metadata) {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substr(2, 9);
|
||||
return `${timestamp}_${random}.${metadata.extension}`;
|
||||
}
|
||||
|
||||
getStoragePath(date, filename) {
|
||||
const year = new Date(date).getFullYear();
|
||||
const month = String(new Date(date).getMonth() + 1).padStart(2, '0');
|
||||
return `photos/${year}/${month}/${filename}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 CDN集成
|
||||
```javascript
|
||||
// services/CDNService.js
|
||||
class CDNService {
|
||||
static getOptimizedUrl(originalUrl, options = {}) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
quality = 85,
|
||||
format = 'auto'
|
||||
} = options;
|
||||
|
||||
// 使用ImageKit或Cloudinary等服务
|
||||
const transformations = [];
|
||||
|
||||
if (width) transformations.push(`w_${width}`);
|
||||
if (height) transformations.push(`h_${height}`);
|
||||
if (quality) transformations.push(`q_${quality}`);
|
||||
if (format !== 'auto') transformations.push(`f_${format}`);
|
||||
|
||||
return `${CDN_BASE_URL}/${transformations.join(',')}/auto/${originalUrl}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 自动化处理流程
|
||||
|
||||
### 6.1 图片上传队列
|
||||
```javascript
|
||||
// jobs/imageProcessingJob.js
|
||||
const Queue = require('bull');
|
||||
const imageQueue = new Queue('image processing');
|
||||
|
||||
imageQueue.process(async (job) => {
|
||||
const { photoId, rawFilePath } = job.data;
|
||||
|
||||
try {
|
||||
// 1. 提取EXIF数据
|
||||
const metadata = await extractMetadata(rawFilePath);
|
||||
|
||||
// 2. 生成多种格式
|
||||
const formats = await generateFormats(rawFilePath);
|
||||
|
||||
// 3. 保存到数据库
|
||||
await savePhotoFormats(photoId, formats);
|
||||
|
||||
// 4. 清理临时文件
|
||||
await cleanupTempFiles(rawFilePath);
|
||||
|
||||
// 5. 发送完成通知
|
||||
await notifyProcessingComplete(photoId);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Image processing failed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加任务到队列
|
||||
const addImageProcessingJob = (photoId, rawFilePath) => {
|
||||
return imageQueue.add({
|
||||
photoId,
|
||||
rawFilePath
|
||||
}, {
|
||||
attempts: 3,
|
||||
backoff: 'exponential',
|
||||
delay: 2000
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 自动分类系统
|
||||
```javascript
|
||||
// services/AutoTaggingService.js
|
||||
class AutoTaggingService {
|
||||
static async analyzePhoto(photoBuffer) {
|
||||
// 使用AI服务进行图片分析
|
||||
const analysis = await this.callVisionAPI(photoBuffer);
|
||||
|
||||
const tags = [];
|
||||
|
||||
// 提取颜色主题
|
||||
const colors = analysis.colors.map(c => c.name);
|
||||
tags.push(...colors);
|
||||
|
||||
// 识别物体和场景
|
||||
const objects = analysis.objects.map(o => o.name);
|
||||
tags.push(...objects);
|
||||
|
||||
// 检测拍摄风格
|
||||
const style = this.detectPhotographyStyle(analysis);
|
||||
if (style) tags.push(style);
|
||||
|
||||
return {
|
||||
tags,
|
||||
confidence: analysis.confidence,
|
||||
description: analysis.description
|
||||
};
|
||||
}
|
||||
|
||||
static detectPhotographyStyle(analysis) {
|
||||
// 基于构图和内容判断摄影风格
|
||||
if (analysis.faces.length > 0) return 'portrait';
|
||||
if (analysis.landscape) return 'landscape';
|
||||
if (analysis.architecture) return 'architecture';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 缓存策略
|
||||
|
||||
### 7.1 多级缓存
|
||||
```javascript
|
||||
// services/CacheService.js
|
||||
class CacheService {
|
||||
constructor() {
|
||||
this.redis = new Redis(process.env.REDIS_URL);
|
||||
this.memCache = new NodeCache({ stdTTL: 300 }); // 5分钟
|
||||
}
|
||||
|
||||
async getPhotos(cacheKey, fetcher) {
|
||||
// L1: 内存缓存
|
||||
let data = this.memCache.get(cacheKey);
|
||||
if (data) return data;
|
||||
|
||||
// L2: Redis缓存
|
||||
data = await this.redis.get(cacheKey);
|
||||
if (data) {
|
||||
data = JSON.parse(data);
|
||||
this.memCache.set(cacheKey, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
// L3: 数据库查询
|
||||
data = await fetcher();
|
||||
|
||||
// 缓存结果
|
||||
await this.redis.setex(cacheKey, 3600, JSON.stringify(data)); // 1小时
|
||||
this.memCache.set(cacheKey, data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async invalidatePattern(pattern) {
|
||||
const keys = await this.redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
}
|
||||
|
||||
// 清理内存缓存
|
||||
this.memCache.flushAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 性能监控
|
||||
|
||||
### 8.1 APM集成
|
||||
```javascript
|
||||
// middleware/monitoring.js
|
||||
const performanceMiddleware = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// 记录性能指标
|
||||
metrics.timing('api.response_time', duration, {
|
||||
route: req.route?.path,
|
||||
method: req.method,
|
||||
status: res.statusCode
|
||||
});
|
||||
|
||||
// 慢查询告警
|
||||
if (duration > 1000) {
|
||||
logger.warn('Slow API response', {
|
||||
url: req.url,
|
||||
duration,
|
||||
method: req.method
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
### 8.2 健康检查
|
||||
```javascript
|
||||
// routes/health.js
|
||||
router.get('/health', async (req, res) => {
|
||||
const checks = {
|
||||
database: await checkDatabase(),
|
||||
redis: await checkRedis(),
|
||||
storage: await checkStorage(),
|
||||
imageProcessing: await checkImageService()
|
||||
};
|
||||
|
||||
const healthy = Object.values(checks).every(check => check.status === 'ok');
|
||||
|
||||
res.status(healthy ? 200 : 503).json({
|
||||
status: healthy ? 'healthy' : 'unhealthy',
|
||||
checks,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user