All checks were successful
Deploy Frontend / deploy (push) Successful in 2m38s
## 📁 模块化重构 ### 新增模块 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 工作时只需关注单个模块的文档,大幅提升处理效率!
543 lines
12 KiB
Markdown
543 lines
12 KiB
Markdown
# 摄影作品集网站 - 后端开发文档
|
||
|
||
## 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()
|
||
});
|
||
});
|
||
``` |