# 摄影作品集网站 - 后端开发文档 ## 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() }); }); ```