Files
photography/docs/后端开发文档.md
2025-07-09 00:13:41 +08:00

12 KiB
Raw Blame History

摄影作品集网站 - 后端开发文档

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 图片格式管理

// 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 智能压缩算法

// 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

-- 图片表
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 数据访问层

// 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结构

// 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 (可选)

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 存储策略

// 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集成

// 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 图片上传队列

// 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 自动分类系统

// 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 多级缓存

// 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集成

// 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 健康检查

// 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()
  });
});