Files
photography/docs/原始 prd/后端开发文档.md
xujiang 9e381c783d
All checks were successful
Deploy Frontend / deploy (push) Successful in 2m38s
feat: 重构项目为模块化结构,拆分 CLAUDE.md 文档
## 📁 模块化重构

### 新增模块 CLAUDE.md
- `frontend/CLAUDE.md` - 前端开发指导 (Next.js, React, TypeScript)
- `docs/deployment/CLAUDE.md` - 部署配置指导 (Caddy, 服务器配置)
- `.gitea/workflows/CLAUDE.md` - CI/CD 流程指导 (Gitea Actions)

### 根目录 CLAUDE.md 优化
- 重构为项目概览和模块导航
- 提供模块选择指导
- 减少单个文件的上下文长度

### 自动化机制
- 创建 `scripts/update-claude-docs.sh` 自动更新脚本
- 集成到 pre-commit hooks 中
- 文件变更时自动更新对应模块的 CLAUDE.md

## 🎯 优化效果

### 上下文优化
- 每个模块独立的 CLAUDE.md 文件
- 大幅减少单次处理的上下文长度
- 提高 Claude 处理效率和准确性

### 开发体验
- 根据工作内容选择对应模块
- 模块化的文档更聚焦和专业
- 自动维护文档时间戳

### 项目结构
```
photography/
├── CLAUDE.md                    # 项目概览和模块导航
├── frontend/CLAUDE.md          # 前端开发指导
├── docs/deployment/CLAUDE.md   # 部署配置指导
├── .gitea/workflows/CLAUDE.md  # CI/CD 流程指导
└── scripts/update-claude-docs.sh # 自动更新脚本
```

现在 Claude 工作时只需关注单个模块的文档,大幅提升处理效率!
2025-07-09 10:54:08 +08:00

543 lines
12 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.

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