feat: 重构项目为模块化结构,拆分 CLAUDE.md 文档
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 工作时只需关注单个模块的文档,大幅提升处理效率!
This commit is contained in:
xujiang
2025-07-09 10:54:08 +08:00
parent 87c34179fa
commit 9e381c783d
11 changed files with 1122 additions and 222 deletions

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