diff --git a/docs/development/saved-docs/api-design.md b/docs/development/saved-docs/api-design.md new file mode 100644 index 0000000..01f3bfa --- /dev/null +++ b/docs/development/saved-docs/api-design.md @@ -0,0 +1,1569 @@ +# 摄影作品集网站 - API接口设计文档 + +## 1. API 概述 + +### 1.1 设计原则 +- **RESTful 设计**: 遵循 REST 架构风格 +- **统一响应格式**: 标准化的 JSON 响应结构 +- **版本控制**: API 版本化管理 +- **安全认证**: JWT 令牌认证机制 +- **错误处理**: 详细的错误码和错误信息 +- **性能优化**: 支持分页、筛选、排序 + +### 1.2 基础信息 +```yaml +Base URL: https://api.photography.iriver.top +API Version: v1 +Content-Type: application/json +Authentication: Bearer Token (JWT) +``` + +### 1.3 通用响应格式 + +#### 1.3.1 成功响应 +```json +{ + "success": true, + "code": 200, + "message": "Success", + "data": { + // 具体数据内容 + }, + "meta": { + "timestamp": "2024-01-15T10:30:00Z", + "request_id": "req_123456789" + } +} +``` + +#### 1.3.2 分页响应 +```json +{ + "success": true, + "code": 200, + "message": "Success", + "data": [ + // 数据列表 + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 150, + "total_pages": 8, + "has_next": true, + "has_prev": false + }, + "meta": { + "timestamp": "2024-01-15T10:30:00Z", + "request_id": "req_123456789" + } +} +``` + +#### 1.3.3 错误响应 +```json +{ + "success": false, + "code": 400, + "message": "Bad Request", + "error": { + "type": "VALIDATION_ERROR", + "details": [ + { + "field": "title", + "message": "Title is required" + } + ] + }, + "meta": { + "timestamp": "2024-01-15T10:30:00Z", + "request_id": "req_123456789" + } +} +``` + +### 1.4 HTTP 状态码规范 + +| 状态码 | 说明 | 使用场景 | +|--------|------|----------| +| 200 | OK | 请求成功 | +| 201 | Created | 资源创建成功 | +| 204 | No Content | 删除成功 | +| 400 | Bad Request | 请求参数错误 | +| 401 | Unauthorized | 未认证 | +| 403 | Forbidden | 权限不足 | +| 404 | Not Found | 资源不存在 | +| 409 | Conflict | 资源冲突 | +| 422 | Unprocessable Entity | 数据验证失败 | +| 429 | Too Many Requests | 请求过于频繁 | +| 500 | Internal Server Error | 服务器内部错误 | + +## 2. 认证与授权 + +### 2.1 认证机制 + +#### 2.1.1 登录认证 +```http +POST /v1/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "password123" +} +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "message": "Login successful", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 86400, + "user": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "admin", + "display_name": "管理员", + "avatar_url": "https://example.com/avatar.jpg" + } + } +} +``` + +#### 2.1.2 令牌刷新 +```http +POST /v1/auth/refresh +Content-Type: application/json + +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +#### 2.1.3 登出 +```http +POST /v1/auth/logout +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +#### 2.1.4 用户信息 +```http +GET /v1/auth/profile +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +### 2.2 权限控制 + +#### 2.2.1 权限级别 +```json +{ + "roles": { + "super_admin": { + "name": "超级管理员", + "permissions": ["*"] + }, + "admin": { + "name": "管理员", + "permissions": [ + "photo.*", + "category.*", + "tag.*", + "user.read", + "user.update", + "settings.*" + ] + }, + "editor": { + "name": "编辑者", + "permissions": [ + "photo.read", + "photo.create", + "photo.update", + "category.read", + "tag.read", + "tag.create" + ] + }, + "user": { + "name": "普通用户", + "permissions": [ + "photo.read", + "category.read", + "tag.read" + ] + } + } +} +``` + +## 3. 照片管理 API + +### 3.1 照片列表 + +#### 3.1.1 获取照片列表 +```http +GET /v1/photos?page=1&limit=20&status=published&category=1&tag=nature&search=sunset&sort_by=created_at&sort_order=desc +``` + +**查询参数:** +| 参数 | 类型 | 必填 | 说明 | 示例 | +|------|------|------|------|------| +| page | integer | 否 | 页码,默认1 | 1 | +| limit | integer | 否 | 每页数量,默认20 | 20 | +| status | string | 否 | 状态筛选 | published, draft, archived | +| category | integer | 否 | 分类ID | 1 | +| tag | string | 否 | 标签名称 | nature | +| search | string | 否 | 搜索关键词 | sunset | +| sort_by | string | 否 | 排序字段 | created_at, taken_at, title | +| sort_order | string | 否 | 排序方向 | asc, desc | +| year | integer | 否 | 年份筛选 | 2024 | +| month | integer | 否 | 月份筛选 | 1 | + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "title": "城市夜景", + "description": "繁华都市的夜晚景色", + "slug": "city-night-view-001", + "status": "published", + "visibility": "public", + "taken_at": "2024-01-15T18:30:00Z", + "created_at": "2024-01-15T20:00:00Z", + "updated_at": "2024-01-15T20:00:00Z", + "view_count": 156, + "like_count": 23, + "formats": { + "thumb_small": "https://cdn.example.com/photos/1/thumb_small.jpg", + "thumb_medium": "https://cdn.example.com/photos/1/thumb_medium.jpg", + "thumb_large": "https://cdn.example.com/photos/1/thumb_large.jpg", + "display": "https://cdn.example.com/photos/1/display.jpg", + "webp": "https://cdn.example.com/photos/1/display.webp" + }, + "exif": { + "camera": "Canon EOS R5", + "lens": "RF 24-70mm f/2.8L IS USM", + "iso": 800, + "aperture": "f/2.8", + "shutter_speed": "1/125", + "focal_length": "50mm" + }, + "location": { + "name": "上海外滩", + "latitude": 31.23037, + "longitude": 121.47370, + "country": "China", + "city": "Shanghai" + }, + "categories": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "color": "#3498db" + } + ], + "tags": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "color": "#2c3e50" + }, + { + "id": 2, + "name": "城市", + "slug": "city", + "color": "#e74c3c" + } + ] + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 150, + "total_pages": 8, + "has_next": true, + "has_prev": false + } +} +``` + +### 3.2 照片详情 + +#### 3.2.1 获取照片详情 +```http +GET /v1/photos/{id} +``` + +**路径参数:** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | integer | 是 | 照片ID | + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "id": 1, + "title": "城市夜景", + "description": "繁华都市的夜晚景色,灯火通明的建筑群构成了美丽的天际线...", + "slug": "city-night-view-001", + "status": "published", + "visibility": "public", + "sort_order": 0, + "taken_at": "2024-01-15T18:30:00Z", + "created_at": "2024-01-15T20:00:00Z", + "updated_at": "2024-01-15T20:00:00Z", + "view_count": 156, + "like_count": 23, + "download_count": 5, + "file_info": { + "original_filename": "DSC_0001.jpg", + "file_size": 2048576, + "mime_type": "image/jpeg" + }, + "formats": { + "original": "https://cdn.example.com/photos/1/original.jpg", + "jpg": "https://cdn.example.com/photos/1/display.jpg", + "webp": "https://cdn.example.com/photos/1/display.webp", + "thumb_small": "https://cdn.example.com/photos/1/thumb_small.jpg", + "thumb_medium": "https://cdn.example.com/photos/1/thumb_medium.jpg", + "thumb_large": "https://cdn.example.com/photos/1/thumb_large.jpg" + }, + "exif": { + "camera": "Canon EOS R5", + "lens": "RF 24-70mm f/2.8L IS USM", + "iso": 800, + "aperture": "f/2.8", + "shutter_speed": "1/125", + "focal_length": "50mm" + }, + "location": { + "name": "上海外滩", + "latitude": 31.23037, + "longitude": 121.47370, + "country": "China", + "city": "Shanghai" + }, + "categories": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "description": "城市景观摄影作品", + "color": "#3498db", + "is_primary": true + } + ], + "tags": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "color": "#2c3e50", + "confidence": 1.0, + "source": "manual" + }, + { + "id": 2, + "name": "城市", + "slug": "city", + "color": "#e74c3c", + "confidence": 0.95, + "source": "ai" + } + ], + "metadata": { + "weather": "clear", + "temperature": "15°C", + "processing_notes": "调整了曝光和对比度" + } + } +} +``` + +### 3.3 照片操作 + +#### 3.3.1 创建照片 (用于上传后的元数据创建) +```http +POST /v1/photos +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "城市夜景", + "description": "繁华都市的夜晚景色", + "file_id": "upload_123456789", + "status": "published", + "visibility": "public", + "taken_at": "2024-01-15T18:30:00Z", + "location": { + "name": "上海外滩", + "latitude": 31.23037, + "longitude": 121.47370 + }, + "categories": [1, 2], + "tags": ["夜景", "城市", "建筑"], + "metadata": { + "weather": "clear", + "temperature": "15°C" + } +} +``` + +#### 3.3.2 更新照片信息 +```http +PUT /v1/photos/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "上海外滩夜景", + "description": "更新后的描述", + "status": "published", + "categories": [1, 3], + "tags": ["夜景", "城市", "外滩"] +} +``` + +#### 3.3.3 删除照片 +```http +DELETE /v1/photos/{id} +Authorization: Bearer {token} +``` + +#### 3.3.4 批量操作 +```http +POST /v1/photos/batch +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_ids": [1, 2, 3, 4, 5], + "action": "update_status", + "data": { + "status": "published" + } +} +``` + +**支持的批量操作:** +- `update_status`: 批量更新状态 +- `add_tags`: 批量添加标签 +- `remove_tags`: 批量移除标签 +- `add_categories`: 批量添加分类 +- `remove_categories`: 批量移除分类 +- `delete`: 批量删除 + +### 3.4 照片搜索 + +#### 3.4.1 全文搜索 +```http +GET /v1/photos/search?q=夜景&category=1&tags=城市,建筑&location=上海&date_from=2024-01-01&date_to=2024-12-31 +``` + +**查询参数:** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| q | string | 是 | 搜索关键词 | +| category | integer | 否 | 分类ID | +| tags | string | 否 | 标签名称,逗号分隔 | +| location | string | 否 | 地点名称 | +| date_from | string | 否 | 开始日期 (YYYY-MM-DD) | +| date_to | string | 否 | 结束日期 (YYYY-MM-DD) | +| camera | string | 否 | 相机型号 | +| lens | string | 否 | 镜头型号 | + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "photos": [ + { + "id": 1, + "title": "城市夜景", + "score": 0.95, + "highlight": { + "title": "夜景", + "description": "繁华都市的夜晚景色" + } + } + ], + "facets": { + "categories": [ + {"name": "城市风光", "count": 15}, + {"name": "建筑摄影", "count": 8} + ], + "tags": [ + {"name": "夜景", "count": 12}, + {"name": "城市", "count": 20} + ], + "years": [ + {"year": 2024, "count": 25}, + {"year": 2023, "count": 18} + ] + } + }, + "pagination": { + "page": 1, + "limit": 20, + "total": 25 + } +} +``` + +## 4. 文件上传 API + +### 4.1 文件上传 + +#### 4.1.1 单文件上传 +```http +POST /v1/upload/single +Authorization: Bearer {token} +Content-Type: multipart/form-data + +file: (binary data) +``` + +**响应:** +```json +{ + "success": true, + "code": 201, + "data": { + "file_id": "upload_123456789", + "original_filename": "DSC_0001.jpg", + "file_size": 2048576, + "mime_type": "image/jpeg", + "upload_url": "https://temp.example.com/upload_123456789.jpg", + "status": "uploaded", + "exif_extracted": true, + "processing_status": "pending" + } +} +``` + +#### 4.1.2 多文件上传 +```http +POST /v1/upload/multiple +Authorization: Bearer {token} +Content-Type: multipart/form-data + +files[]: (binary data) +files[]: (binary data) +... +``` + +**响应:** +```json +{ + "success": true, + "code": 201, + "data": { + "uploaded": [ + { + "file_id": "upload_123456789", + "original_filename": "DSC_0001.jpg", + "file_size": 2048576, + "status": "uploaded" + } + ], + "failed": [ + { + "filename": "invalid_file.txt", + "error": "Invalid file type" + } + ], + "summary": { + "total": 5, + "success": 4, + "failed": 1 + } + } +} +``` + +#### 4.1.3 分块上传 +```http +POST /v1/upload/chunked/init +Authorization: Bearer {token} +Content-Type: application/json + +{ + "filename": "large_photo.raw", + "file_size": 52428800, + "mime_type": "image/raw", + "chunk_size": 1048576 +} +``` + +**响应:** +```json +{ + "success": true, + "code": 201, + "data": { + "upload_id": "chunked_123456789", + "chunk_size": 1048576, + "total_chunks": 50, + "upload_urls": [ + "https://temp.example.com/chunked_123456789/chunk_0", + "https://temp.example.com/chunked_123456789/chunk_1" + ] + } +} +``` + +#### 4.1.4 上传状态查询 +```http +GET /v1/upload/status/{file_id} +Authorization: Bearer {token} +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "file_id": "upload_123456789", + "status": "processing", + "progress": 75, + "current_step": "generating_thumbnails", + "steps": [ + {"name": "uploaded", "status": "completed"}, + {"name": "exif_extraction", "status": "completed"}, + {"name": "generating_thumbnails", "status": "processing"}, + {"name": "optimization", "status": "pending"} + ], + "estimated_time_remaining": 30 + } +} +``` + +### 4.2 文件处理 + +#### 4.2.1 重新处理文件 +```http +POST /v1/upload/{file_id}/reprocess +Authorization: Bearer {token} +Content-Type: application/json + +{ + "formats": ["thumb_small", "thumb_medium", "webp"], + "force": true +} +``` + +#### 4.2.2 删除上传文件 +```http +DELETE /v1/upload/{file_id} +Authorization: Bearer {token} +``` + +## 5. 分类管理 API + +### 5.1 分类操作 + +#### 5.1.1 获取分类列表 +```http +GET /v1/categories?include_stats=true&include_tree=true +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "categories": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "description": "城市景观摄影作品", + "parent_id": null, + "level": 0, + "path": "1", + "cover_photo": { + "id": 15, + "title": "都市夜景", + "thumb_url": "https://cdn.example.com/photos/15/thumb_medium.jpg" + }, + "color": "#3498db", + "icon": "building", + "sort_order": 1, + "is_active": true, + "is_featured": true, + "photo_count": 45, + "direct_photo_count": 30, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-15T10:30:00Z" + } + ], + "tree": [ + { + "id": 1, + "name": "城市风光", + "slug": "cityscape", + "photo_count": 45, + "children": [ + { + "id": 2, + "name": "夜景摄影", + "slug": "night-photography", + "photo_count": 15, + "children": [] + }, + { + "id": 3, + "name": "建筑摄影", + "slug": "architecture", + "photo_count": 30, + "children": [] + } + ] + } + ], + "stats": { + "total_categories": 12, + "max_level": 3, + "featured_count": 5 + } + } +} +``` + +#### 5.1.2 获取分类详情 +```http +GET /v1/categories/{id} +``` + +#### 5.1.3 创建分类 +```http +POST /v1/categories +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "自然风景", + "slug": "nature-landscape", + "description": "自然风景摄影作品", + "parent_id": null, + "color": "#27ae60", + "icon": "tree", + "sort_order": 2, + "is_featured": true, + "seo_title": "自然风景摄影作品集", + "seo_description": "欣赏美丽的自然风景摄影作品" +} +``` + +#### 5.1.4 更新分类 +```http +PUT /v1/categories/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "自然风光", + "description": "更新后的描述", + "color": "#2ecc71" +} +``` + +#### 5.1.5 删除分类 +```http +DELETE /v1/categories/{id} +Authorization: Bearer {token} +``` + +#### 5.1.6 设置分类封面 +```http +PUT /v1/categories/{id}/cover +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_id": 15 +} +``` + +### 5.2 分类照片管理 + +#### 5.2.1 获取分类下的照片 +```http +GET /v1/categories/{id}/photos?page=1&limit=20&sort_by=created_at&sort_order=desc +``` + +#### 5.2.2 添加照片到分类 +```http +POST /v1/categories/{id}/photos +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_ids": [1, 2, 3], + "is_primary": true +} +``` + +#### 5.2.3 从分类移除照片 +```http +DELETE /v1/categories/{id}/photos +Authorization: Bearer {token} +Content-Type: application/json + +{ + "photo_ids": [1, 2, 3] +} +``` + +## 6. 标签管理 API + +### 6.1 标签操作 + +#### 6.1.1 获取标签列表 +```http +GET /v1/tags?group=all&sort_by=usage_count&sort_order=desc&limit=50 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "description": "夜晚拍摄的景色", + "color": "#2c3e50", + "icon": "moon", + "tag_group": "style", + "usage_count": 45, + "trend_score": 8.5, + "is_active": true, + "is_featured": true, + "created_at": "2024-01-01T00:00:00Z", + "last_used_at": "2024-01-15T10:30:00Z" + } + ], + "groups": { + "style": {"name": "摄影风格", "count": 12}, + "subject": {"name": "拍摄主题", "count": 18}, + "technique": {"name": "拍摄技法", "count": 8}, + "location": {"name": "地理位置", "count": 25} + } +} +``` + +#### 6.1.2 标签搜索建议 +```http +GET /v1/tags/suggestions?q=夜&limit=10 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "name": "夜景", + "slug": "night-view", + "usage_count": 45, + "match_score": 0.95 + }, + { + "id": 15, + "name": "夜市", + "slug": "night-market", + "usage_count": 12, + "match_score": 0.8 + } + ] +} +``` + +#### 6.1.3 创建标签 +```http +POST /v1/tags +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "极光", + "slug": "aurora", + "description": "极光摄影作品", + "color": "#9b59b6", + "tag_group": "subject", + "is_featured": true +} +``` + +#### 6.1.4 更新标签 +```http +PUT /v1/tags/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "北极光", + "description": "更新后的描述", + "color": "#8e44ad" +} +``` + +#### 6.1.5 删除标签 +```http +DELETE /v1/tags/{id} +Authorization: Bearer {token} +``` + +### 6.2 标签统计 + +#### 6.2.1 标签云数据 +```http +GET /v1/tags/cloud?min_usage=5&max_tags=50 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": [ + { + "id": 1, + "name": "夜景", + "usage_count": 45, + "relative_size": 100, + "color": "#2c3e50" + }, + { + "id": 2, + "name": "城市", + "usage_count": 38, + "relative_size": 84, + "color": "#e74c3c" + } + ] +} +``` + +#### 6.2.2 趋势标签 +```http +GET /v1/tags/trending?period=30d&limit=10 +``` + +## 7. 时间线 API + +### 7.1 时间线数据 + +#### 7.1.1 获取时间线 +```http +GET /v1/timeline?year=2024&include_photos=true&photos_limit=5 +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "years": [ + { + "year": 2024, + "photo_count": 156, + "months": [ + { + "month": 1, + "month_name": "一月", + "photo_count": 25, + "photos": [ + { + "id": 1, + "title": "城市夜景", + "thumb_url": "https://cdn.example.com/photos/1/thumb_medium.jpg", + "taken_at": "2024-01-15T18:30:00Z" + } + ], + "events": [ + { + "id": 1, + "title": "首次夜景拍摄", + "description": "第一次尝试城市夜景摄影", + "date": "2024-01-15", + "type": "milestone" + } + ] + } + ] + } + ], + "stats": { + "total_photos": 456, + "year_range": [2020, 2024], + "most_active_month": { + "year": 2024, + "month": 3, + "count": 45 + }, + "photos_by_year": [ + {"year": 2024, "count": 156}, + {"year": 2023, "count": 189}, + {"year": 2022, "count": 111} + ] + } + } +} +``` + +#### 7.1.2 获取指定月份详情 +```http +GET /v1/timeline/{year}/{month}?include_photos=true +``` + +### 7.2 时间线事件 + +#### 7.2.1 创建事件 +```http +POST /v1/timeline/events +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "获得摄影比赛奖项", + "description": "在城市摄影大赛中获得金奖", + "date": "2024-03-15", + "type": "achievement", + "related_photos": [15, 23, 31] +} +``` + +#### 7.2.2 更新事件 +```http +PUT /v1/timeline/events/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "更新后的标题", + "description": "更新后的描述" +} +``` + +#### 7.2.3 删除事件 +```http +DELETE /v1/timeline/events/{id} +Authorization: Bearer {token} +``` + +## 8. 系统设置 API + +### 8.1 设置管理 + +#### 8.1.1 获取系统设置 +```http +GET /v1/settings?category=all&include_public=true +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "general": { + "site_title": "摄影作品集", + "site_description": "专业摄影师作品展示平台", + "site_keywords": "摄影,作品集,艺术,创作", + "site_author": "摄影师姓名", + "site_email": "contact@example.com" + }, + "upload": { + "max_file_size": 52428800, + "allowed_types": ["image/jpeg", "image/png", "image/raw"], + "max_files_per_batch": 50, + "auto_publish": false, + "generate_thumbnails": true + }, + "image": { + "quality_jpg": 85, + "quality_webp": 80, + "max_width": 1920, + "max_height": 1080, + "watermark_enabled": false + }, + "display": { + "photos_per_page": 20, + "thumbnail_size": 300, + "theme_primary_color": "#d4af37", + "theme_secondary_color": "#2d2d2d", + "enable_dark_mode": true, + "enable_animations": true + } + } +} +``` + +#### 8.1.2 更新系统设置 +```http +PUT /v1/settings +Authorization: Bearer {token} +Content-Type: application/json + +{ + "site_title": "我的摄影作品集", + "upload_max_file_size": 104857600, + "display_photos_per_page": 24 +} +``` + +#### 8.1.3 重置设置 +```http +POST /v1/settings/reset +Authorization: Bearer {token} +Content-Type: application/json + +{ + "categories": ["upload", "image"], + "confirm": true +} +``` + +### 8.2 缓存管理 + +#### 8.2.1 清理缓存 +```http +POST /v1/settings/cache/clear +Authorization: Bearer {token} +Content-Type: application/json + +{ + "types": ["photos", "categories", "tags", "settings"], + "confirm": true +} +``` + +#### 8.2.2 预热缓存 +```http +POST /v1/settings/cache/warm +Authorization: Bearer {token} +Content-Type: application/json + +{ + "types": ["popular_photos", "category_tree", "tag_cloud"] +} +``` + +#### 8.2.3 缓存统计 +```http +GET /v1/settings/cache/stats +Authorization: Bearer {token} +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "redis": { + "connected": true, + "used_memory": "15.2MB", + "total_keys": 1247, + "hit_rate": 0.89 + }, + "categories": { + "photos": {"keys": 456, "hit_rate": 0.92}, + "categories": {"keys": 12, "hit_rate": 0.95}, + "tags": {"keys": 89, "hit_rate": 0.87}, + "settings": {"keys": 1, "hit_rate": 0.99} + } + } +} +``` + +## 9. 统计分析 API + +### 9.1 仪表板统计 + +#### 9.1.1 仪表板数据 +```http +GET /v1/dashboard/stats?period=30d +``` + +**响应:** +```json +{ + "success": true, + "code": 200, + "data": { + "summary": { + "total_photos": 456, + "total_categories": 12, + "total_tags": 89, + "total_views": 15420, + "storage_used": 2684354560, + "storage_limit": 10737418240 + }, + "recent": { + "new_photos": 15, + "new_views": 234, + "new_likes": 67, + "period": "7d" + }, + "trends": { + "uploads": [ + {"date": "2024-01-01", "count": 5}, + {"date": "2024-01-02", "count": 8}, + {"date": "2024-01-03", "count": 3} + ], + "views": [ + {"date": "2024-01-01", "count": 156}, + {"date": "2024-01-02", "count": 234}, + {"date": "2024-01-03", "count": 189} + ] + }, + "popular": { + "categories": [ + {"name": "城市风光", "count": 45, "percentage": 28.5}, + {"name": "自然风景", "count": 38, "percentage": 24.1} + ], + "tags": [ + {"name": "夜景", "count": 45}, + {"name": "城市", "count": 38} + ], + "photos": [ + { + "id": 15, + "title": "都市夜景", + "view_count": 1247, + "like_count": 89 + } + ] + }, + "system_status": { + "database": "healthy", + "redis": "healthy", + "storage": "healthy", + "queue": "healthy" + } + } +} +``` + +### 9.2 详细统计 + +#### 9.2.1 照片统计 +```http +GET /v1/stats/photos?group_by=month&year=2024 +``` + +#### 9.2.2 访问统计 +```http +GET /v1/stats/views?period=30d&group_by=day +``` + +#### 9.2.3 存储统计 +```http +GET /v1/stats/storage +``` + +## 10. 用户管理 API + +### 10.1 用户操作 + +#### 10.1.1 获取用户列表 +```http +GET /v1/users?page=1&limit=20&role=all&status=active&search=admin +``` + +#### 10.1.2 创建用户 +```http +POST /v1/users +Authorization: Bearer {token} +Content-Type: application/json + +{ + "username": "editor", + "email": "editor@example.com", + "password": "password123", + "role": "editor", + "display_name": "编辑者", + "is_active": true +} +``` + +#### 10.1.3 更新用户 +```http +PUT /v1/users/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "display_name": "高级编辑者", + "role": "admin", + "is_active": true +} +``` + +#### 10.1.4 删除用户 +```http +DELETE /v1/users/{id} +Authorization: Bearer {token} +``` + +### 10.2 用户会话管理 + +#### 10.2.1 获取用户会话 +```http +GET /v1/users/{id}/sessions +Authorization: Bearer {token} +``` + +#### 10.2.2 强制下线 +```http +DELETE /v1/users/{id}/sessions/{session_id} +Authorization: Bearer {token} +``` + +## 11. 错误处理 + +### 11.1 错误码定义 + +| 错误码 | HTTP状态码 | 说明 | +|--------|------------|------| +| VALIDATION_ERROR | 400 | 请求参数验证失败 | +| AUTHENTICATION_REQUIRED | 401 | 需要认证 | +| INVALID_TOKEN | 401 | 无效的令牌 | +| TOKEN_EXPIRED | 401 | 令牌已过期 | +| PERMISSION_DENIED | 403 | 权限不足 | +| RESOURCE_NOT_FOUND | 404 | 资源不存在 | +| RESOURCE_CONFLICT | 409 | 资源冲突 | +| UNPROCESSABLE_ENTITY | 422 | 数据处理失败 | +| RATE_LIMIT_EXCEEDED | 429 | 请求频率超限 | +| INTERNAL_SERVER_ERROR | 500 | 服务器内部错误 | +| SERVICE_UNAVAILABLE | 503 | 服务不可用 | + +### 11.2 错误响应示例 + +#### 11.2.1 验证错误 +```json +{ + "success": false, + "code": 400, + "message": "Validation failed", + "error": { + "type": "VALIDATION_ERROR", + "details": [ + { + "field": "title", + "message": "Title is required", + "code": "REQUIRED" + }, + { + "field": "email", + "message": "Invalid email format", + "code": "INVALID_FORMAT" + } + ] + } +} +``` + +#### 11.2.2 权限错误 +```json +{ + "success": false, + "code": 403, + "message": "Permission denied", + "error": { + "type": "PERMISSION_DENIED", + "details": { + "required_permission": "photo.delete", + "user_role": "editor" + } + } +} +``` + +#### 11.2.3 资源不存在 +```json +{ + "success": false, + "code": 404, + "message": "Resource not found", + "error": { + "type": "RESOURCE_NOT_FOUND", + "details": { + "resource_type": "photo", + "resource_id": 999 + } + } +} +``` + +## 12. API 限流 + +### 12.1 限流策略 + +| 端点类型 | 限制 | 时间窗口 | +|----------|------|----------| +| 认证端点 | 5次/IP | 1分钟 | +| 上传端点 | 10次/用户 | 1分钟 | +| 搜索端点 | 60次/用户 | 1分钟 | +| 一般端点 | 1000次/用户 | 1小时 | +| 管理端点 | 500次/用户 | 1小时 | + +### 12.2 限流响应头 + +```http +X-RateLimit-Limit: 1000 +X-RateLimit-Remaining: 999 +X-RateLimit-Reset: 1642636800 +X-RateLimit-Window: 3600 +``` + +### 12.3 限流超出响应 + +```json +{ + "success": false, + "code": 429, + "message": "Rate limit exceeded", + "error": { + "type": "RATE_LIMIT_EXCEEDED", + "details": { + "limit": 1000, + "window": 3600, + "reset_at": "2024-01-15T11:00:00Z" + } + } +} +``` + +## 13. WebHook API + +### 13.1 WebHook 配置 + +#### 13.1.1 创建 WebHook +```http +POST /v1/webhooks +Authorization: Bearer {token} +Content-Type: application/json + +{ + "url": "https://example.com/webhook", + "events": ["photo.created", "photo.updated", "photo.deleted"], + "secret": "webhook_secret_key", + "is_active": true +} +``` + +#### 13.1.2 WebHook 事件类型 + +| 事件类型 | 描述 | 数据载荷 | +|----------|------|----------| +| photo.created | 照片创建 | 完整照片数据 | +| photo.updated | 照片更新 | 更新后的照片数据 | +| photo.deleted | 照片删除 | 删除的照片ID | +| category.created | 分类创建 | 完整分类数据 | +| category.updated | 分类更新 | 更新后的分类数据 | +| category.deleted | 分类删除 | 删除的分类ID | +| user.login | 用户登录 | 用户基本信息 | +| system.backup | 系统备份 | 备份状态信息 | + +### 13.2 WebHook 载荷示例 + +```json +{ + "event": "photo.created", + "timestamp": "2024-01-15T10:30:00Z", + "data": { + "id": 1, + "title": "城市夜景", + "status": "published", + "created_at": "2024-01-15T10:30:00Z" + }, + "signature": "sha256=abcdef123456..." +} +``` + +## 14. API 版本控制 + +### 14.1 版本策略 + +- **URL 版本控制**: `/v1/`, `/v2/` +- **向后兼容**: 至少支持2个主版本 +- **废弃通知**: 通过响应头通知 + +### 14.2 版本响应头 + +```http +API-Version: 1.0 +API-Deprecated: false +API-Sunset: 2025-01-15T00:00:00Z +``` + +### 14.3 版本变更日志 + +#### 版本 1.0.0 (当前) +- 初始 API 版本 +- 支持照片、分类、标签管理 +- 用户认证和权限控制 + +#### 版本 1.1.0 (计划) +- 添加 AI 标签推荐 +- 支持视频文件 +- 增强搜索功能 + +## 15. 总结 + +这个API接口设计文档提供了摄影作品集网站的完整API规范,包括: + +### 🎯 设计特点 +- **RESTful 风格**: 符合REST架构原则 +- **统一响应格式**: 标准化的JSON响应 +- **完整的CRUD操作**: 支持所有资源的增删改查 +- **灵活的查询**: 丰富的筛选、排序、搜索功能 + +### 🔒 安全机制 +- **JWT认证**: 基于令牌的认证机制 +- **权限控制**: 细粒度的角色权限管理 +- **请求限流**: 防止API滥用 +- **数据验证**: 严格的输入验证 + +### 📊 功能丰富 +- **文件上传**: 支持单文件、多文件、分块上传 +- **图片处理**: 自动生成多种格式和尺寸 +- **全文搜索**: 强大的搜索和筛选功能 +- **统计分析**: 详细的数据统计和趋势分析 + +### 🛠️ 开发友好 +- **详细文档**: 完整的接口说明和示例 +- **错误处理**: 清晰的错误码和错误信息 +- **版本控制**: 科学的API版本管理 +- **WebHook支持**: 事件驱动的集成能力 + +这个API设计为Golang后端实现提供了完整的接口规范,可以支持前端和管理后台的所有功能需求。 \ No newline at end of file diff --git a/docs/development/saved-docs/management-backend.md b/docs/development/saved-docs/management-backend.md new file mode 100644 index 0000000..cc0e882 --- /dev/null +++ b/docs/development/saved-docs/management-backend.md @@ -0,0 +1,1823 @@ +# 摄影作品集网站 - 管理后台开发文档 + +## 1. 项目概述 + +### 1.1 项目定位 +基于现有摄影作品集网站的管理后台系统,提供完整的内容管理、用户管理和系统配置功能。 + +### 1.2 技术栈 +- **后端**: Golang + Gin + GORM + PostgreSQL + Redis +- **前端**: React + TypeScript + Tailwind CSS + shadcn/ui +- **文件存储**: MinIO/AWS S3 + 本地存储 +- **图片处理**: libvips + 多格式转换 +- **认证**: JWT + Session管理 +- **部署**: Docker + Caddy + +### 1.3 设计原则 +- **用户友好**: 直观的界面设计,简化操作流程 +- **高性能**: 异步图片处理,智能缓存策略 +- **可扩展**: 模块化设计,支持功能扩展 +- **安全可靠**: 多层权限控制,操作日志审计 + +## 2. 管理后台功能模块详细设计 + +### 2.1 仪表板模块 (Dashboard) + +#### 2.1.1 核心功能 +- **数据统计**: 照片总数、分类数量、标签数量、存储使用情况 +- **近期活动**: 最近上传、最近修改、访问统计 +- **快捷操作**: 快速上传、批量处理、系统设置 +- **系统状态**: 服务器状态、缓存状态、队列状态 + +#### 2.1.2 界面设计 +``` +┌─────────────────────────────────────────────────────────┐ +│ 仪表板 Dashboard │ +├─────────────────────────────────────────────────────────┤ +│ 📊 统计卡片 │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ 总照片 │ │ 总分类 │ │ 总标签 │ │ 存储用量│ │ +│ │ 1,234 │ │ 12 │ │ 45 │ │ 2.5GB │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ 📈 上传趋势图表 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ [折线图显示最近30天上传趋势] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📋 近期活动 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ • 上传了 "城市夜景" 系列 5 张照片 │ │ +│ │ • 创建了新分类 "建筑摄影" │ │ +│ │ • 更新了标签 "城市风光" │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.1.3 数据接口 +```go +// GET /api/admin/dashboard/stats +type DashboardStats struct { + TotalPhotos int `json:"total_photos"` + TotalCategories int `json:"total_categories"` + TotalTags int `json:"total_tags"` + StorageUsed int64 `json:"storage_used"` + StorageLimit int64 `json:"storage_limit"` + RecentUploads int `json:"recent_uploads"` + + // 上传趋势 (最近30天) + UploadTrend []struct { + Date string `json:"date"` + Count int `json:"count"` + } `json:"upload_trend"` + + // 热门分类 + PopularCategories []struct { + Name string `json:"name"` + Count int `json:"count"` + } `json:"popular_categories"` + + // 系统状态 + SystemStatus struct { + DatabaseStatus string `json:"database_status"` + RedisStatus string `json:"redis_status"` + StorageStatus string `json:"storage_status"` + QueueStatus string `json:"queue_status"` + } `json:"system_status"` +} +``` + +### 2.2 照片管理模块 (Photo Management) + +#### 2.2.1 照片列表页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 照片管理 Photo Management │ +├─────────────────────────────────────────────────────────┤ +│ 🔍 [搜索框] 📂 [分类筛选] 🏷️ [标签筛选] 📅 [时间筛选] │ +│ ➕ 上传照片 📤 批量操作 ⚙️ 设置 │ +├─────────────────────────────────────────────────────────┤ +│ 📷 照片网格 (支持列表/网格视图切换) │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │ [缩略图] │ │ +│ │ 标题 │ │ 标题 │ │ 标题 │ │ 标题 │ │ +│ │ 分类 │ │ 分类 │ │ 分类 │ │ 分类 │ │ +│ │ 日期 │ │ 日期 │ │ 日期 │ │ 日期 │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ [更多照片...] │ +│ │ +│ ⬅️ 上一页 [1] [2] [3] ... [10] 下一页 ➡️ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.2.2 照片详情/编辑页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 照片详情 Photo Detail │ +├─────────────────────────────────────────────────────────┤ +│ ⬅️ 返回列表 📝 编辑模式 🗑️ 删除 💾 保存 │ +├─────────────────────────────────────────────────────────┤ +│ 📸 图片预览 │ 📋 基本信息 │ +│ ┌─────────────────────────────┐ │ ┌─────────────────────┐ │ +│ │ │ │ │ 标题: [输入框] │ │ +│ │ [原图显示] │ │ │ 描述: [文本框] │ │ +│ │ │ │ │ 分类: [下拉选择] │ │ +│ │ │ │ │ 标签: [标签输入] │ │ +│ │ │ │ │ 状态: [开关] │ │ +│ │ │ │ │ 拍摄时间: [日期] │ │ +│ │ │ │ │ 位置: [输入框] │ │ +│ │ │ │ └─────────────────────┘ │ +│ └─────────────────────────────┘ │ │ +│ │ 📷 EXIF信息 │ +│ 🎨 图片处理 │ ┌─────────────────────┐ │ +│ ┌─────────────────────────────┐ │ │ 相机: Canon EOS R5 │ │ +│ │ [裁剪] [旋转] [滤镜] [调色] │ │ │ 镜头: 24-70mm f/2.8 │ │ +│ │ [缩放] [水印] [格式转换] │ │ │ 光圈: f/2.8 │ │ +│ └─────────────────────────────┘ │ │ 快门: 1/60s │ │ +│ │ │ ISO: 400 │ │ +│ │ │ 焦距: 35mm │ │ +│ │ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.2.3 批量上传组件 +``` +┌─────────────────────────────────────────────────────────┐ +│ 批量上传 Batch Upload │ +├─────────────────────────────────────────────────────────┤ +│ 📁 拖拽上传区域 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 📸 拖拽图片到这里 或 点击选择文件 │ │ +│ │ 支持 JPG, PNG, RAW, TIFF │ │ +│ │ 最大文件大小: 50MB │ │ +│ │ 最多同时上传: 20个文件 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📋 上传队列 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 📸 IMG_001.jpg [██████████] 100% ✅ 完成 │ │ +│ │ 📸 IMG_002.jpg [█████████▒] 90% ⏳ 上传中 │ │ +│ │ 📸 IMG_003.jpg [██▒▒▒▒▒▒▒▒] 20% ⏳ 上传中 │ │ +│ │ 📸 IMG_004.jpg [▒▒▒▒▒▒▒▒▒▒] 0% ⏸️ 等待 │ │ +│ │ 📸 IMG_005.jpg [▒▒▒▒▒▒▒▒▒▒] 0% ⏸️ 等待 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ⚙️ 批量设置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 默认分类: [下拉选择] │ │ +│ │ 默认标签: [标签输入] │ │ +│ │ 图片质量: [滑块] 85% │ │ +│ │ 生成缩略图: [✅] 生成WebP格式: [✅] │ │ +│ │ 提取EXIF: [✅] 自动分类: [✅] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 🎯 操作按钮 │ +│ [⏸️ 暂停全部] [▶️ 继续全部] [🗑️ 清空队列] [⚙️ 高级设置] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.3 分类管理模块 (Category Management) + +#### 2.3.1 分类树形结构 +``` +┌─────────────────────────────────────────────────────────┐ +│ 分类管理 Category Management │ +├─────────────────────────────────────────────────────────┤ +│ ➕ 新建分类 📊 分类统计 🔄 重新排序 ⚙️ 批量操作 │ +├─────────────────────────────────────────────────────────┤ +│ 📂 分类树 (拖拽排序) │ 📋 分类详情 │ +│ ┌─────────────────────────────────┐ │ ┌─────────────────┐ │ +│ │ 📁 风景摄影 (125) │ │ │ 分类名: 风景摄影 │ │ +│ │ ├─ 🏔️ 山川 (45) │ │ │ 父分类: 无 │ │ +│ │ ├─ 🌊 海岸 (32) │ │ │ 描述: [文本框] │ │ +│ │ ├─ 🌲 森林 (28) │ │ │ 颜色: [🔴] │ │ +│ │ └─ 🌅 日出日落 (20) │ │ │ 封面: [选择图片] │ │ +│ │ │ │ │ 状态: [✅] 启用 │ │ +│ │ 📁 人像摄影 (89) │ │ │ 排序: 1 │ │ +│ │ ├─ 👤 肖像 (34) │ │ │ 创建时间: 2024-01│ │ +│ │ ├─ 👥 群像 (28) │ │ │ 照片数量: 125 │ │ +│ │ ├─ 💃 艺术 (15) │ │ └─────────────────┘ │ +│ │ └─ 📸 街头 (12) │ │ │ +│ │ │ │ 🎨 操作 │ +│ │ 📁 建筑摄影 (67) │ │ ┌─────────────────┐ │ +│ │ ├─ 🏢 现代建筑 (23) │ │ │ [📝 编辑] │ │ +│ │ ├─ 🏛️ 古建筑 (22) │ │ │ [👁️ 查看照片] │ │ +│ │ ├─ 🌃 夜景 (12) │ │ │ [📊 统计] │ │ +│ │ └─ 🏘️ 城市风光 (10) │ │ │ [🗑️ 删除] │ │ +│ │ │ │ └─────────────────┘ │ +│ └─────────────────────────────────┘ │ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.3.2 分类编辑表单 +``` +┌─────────────────────────────────────────────────────────┐ +│ 编辑分类 Edit Category │ +├─────────────────────────────────────────────────────────┤ +│ 📝 基本信息 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 分类名称: [输入框] *必填 │ │ +│ │ 父分类: [下拉选择] - 选择父分类 │ │ +│ │ 分类描述: [文本框] - 详细描述 │ │ +│ │ 分类颜色: [🔴🟡🟢🔵🟣] - 选择标识色 │ │ +│ │ 排序权重: [数字输入] - 数字越小排序越前 │ │ +│ │ 状态: [开关] 启用/禁用 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 🖼️ 封面设置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 封面图片: [选择图片] [上传新图片] │ │ +│ │ ┌─────────────────┐ │ │ +│ │ │ [预览图片] │ │ │ +│ │ │ │ │ │ +│ │ └─────────────────┘ │ │ +│ │ 图片尺寸: 建议 400x300 像素 │ │ +│ │ 文件大小: 不超过 2MB │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ⚙️ 高级设置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ SEO设置: │ │ +│ │ URL别名: [输入框] - 用于URL路径 │ │ +│ │ Meta描述: [文本框] - 搜索引擎描述 │ │ +│ │ 关键词: [标签输入] - SEO关键词 │ │ +│ │ │ │ +│ │ 权限设置: │ │ +│ │ 可见性: [公开/私有/仅登录用户] │ │ +│ │ 管理权限: [仅管理员/编辑者/所有用户] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 💾 操作按钮 │ +│ [💾 保存] [🔄 重置] [👁️ 预览] [❌ 取消] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.4 标签管理模块 (Tag Management) + +#### 2.4.1 标签云展示 +``` +┌─────────────────────────────────────────────────────────┐ +│ 标签管理 Tag Management │ +├─────────────────────────────────────────────────────────┤ +│ ➕ 新建标签 📊 使用统计 🔄 批量操作 🔍 搜索标签 │ +├─────────────────────────────────────────────────────────┤ +│ ☁️ 标签云 (按使用频率显示) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 自然 (120) 日落 (89) 肖像 (76) │ │ +│ │ 城市 (145) 风景 (156) 黑白 (45) 建筑 (67) │ │ +│ │ 艺术 (34) 旅行 (123) 海岸 (32) │ │ +│ │ 山川 (45) 夜景 (28) 街头 (23) 现代 (56) │ │ +│ │ 森林 (28) 抽象 (19) 光影 (67) │ │ +│ │ 情感 (23) 色彩 (89) 构图 (45) 纹理 (34) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📊 使用统计 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 🔥 热门标签 │ │ +│ │ 1. 风景 (156张) ████████████████████████████████ │ │ +│ │ 2. 城市 (145张) ████████████████████████████████ │ │ +│ │ 3. 旅行 (123张) ████████████████████████████ │ │ +│ │ 4. 自然 (120张) ████████████████████████████ │ │ +│ │ 5. 日落 (89张) ████████████████████████ │ │ +│ │ │ │ +│ │ 📈 增长趋势 │ │ +│ │ 本周新增: 12个标签 │ │ +│ │ 本月活跃: 45个标签 │ │ +│ │ 平均每张照片: 3.2个标签 │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.4.2 标签列表管理 +``` +┌─────────────────────────────────────────────────────────┐ +│ 标签列表 Tag List │ +├─────────────────────────────────────────────────────────┤ +│ 🔍 [搜索标签] 📊 [按使用量排序] 🏷️ [按颜色筛选] [批量] │ +├─────────────────────────────────────────────────────────┤ +│ 标签名 │ 颜色 │ 使用数量 │ 创建时间 │ 状态 │ 操作 │ +│ ─────────┼──────┼─────────┼──────────┼─────┼────── │ +│ 风景 │ 🟢 │ 156张 │ 2024-01-01│ 启用 │ 编辑 │ +│ 城市 │ 🔵 │ 145张 │ 2024-01-01│ 启用 │ 编辑 │ +│ 旅行 │ 🟡 │ 123张 │ 2024-01-02│ 启用 │ 编辑 │ +│ 自然 │ 🟢 │ 120张 │ 2024-01-01│ 启用 │ 编辑 │ +│ 日落 │ 🟠 │ 89张 │ 2024-01-03│ 启用 │ 编辑 │ +│ 色彩 │ 🎨 │ 89张 │ 2024-01-05│ 启用 │ 编辑 │ +│ 肖像 │ 🟣 │ 76张 │ 2024-01-02│ 启用 │ 编辑 │ +│ 光影 │ ⚪ │ 67张 │ 2024-01-04│ 启用 │ 编辑 │ +│ 建筑 │ 🔴 │ 67张 │ 2024-01-03│ 启用 │ 编辑 │ +│ 现代 │ 🔘 │ 56张 │ 2024-01-06│ 启用 │ 编辑 │ +├─────────────────────────────────────────────────────────┤ +│ ⬅️ 上一页 [1] [2] [3] ... [10] 下一页 ➡️ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.5 用户管理模块 (User Management) + +#### 2.5.1 用户列表页面 +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户管理 User Management │ +├─────────────────────────────────────────────────────────┤ +│ ➕ 新建用户 👥 角色管理 🔍 搜索用户 📊 用户统计 │ +├─────────────────────────────────────────────────────────┤ +│ 用户名 │ 邮箱 │ 角色 │ 状态 │ 最后登录 │ 操作 │ +│ ─────────┼──────────┼───────┼─────┼─────────┼────── │ +│ admin │ admin@.. │ 管理员 │ 🟢活跃│ 2小时前 │ 编辑 │ +│ editor │ editor@.. │ 编辑者 │ 🟢活跃│ 1天前 │ 编辑 │ +│ user001 │ user001@..│ 用户 │ 🟡离线│ 3天前 │ 编辑 │ +│ user002 │ user002@..│ 用户 │ 🔴禁用│ 1周前 │ 编辑 │ +│ guest │ guest@.. │ 访客 │ 🟢活跃│ 5分钟前 │ 编辑 │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 2.5.2 用户编辑表单 +``` +┌─────────────────────────────────────────────────────────┐ +│ 编辑用户 Edit User │ +├─────────────────────────────────────────────────────────┤ +│ 👤 基本信息 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 用户名: [输入框] *必填 │ │ +│ │ 邮箱: [输入框] *必填 │ │ +│ │ 姓名: [输入框] 显示名称 │ │ +│ │ 头像: [选择图片] [上传新头像] │ │ +│ │ 角色: [下拉选择] 管理员/编辑者/用户/访客 │ │ +│ │ 状态: [开关] 启用/禁用 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 🔐 权限设置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 照片管理: [✅] 查看 [✅] 创建 [✅] 编辑 [❌] 删除 │ │ +│ │ 分类管理: [✅] 查看 [✅] 创建 [❌] 编辑 [❌] 删除 │ │ +│ │ 标签管理: [✅] 查看 [✅] 创建 [❌] 编辑 [❌] 删除 │ │ +│ │ 用户管理: [❌] 查看 [❌] 创建 [❌] 编辑 [❌] 删除 │ │ +│ │ 系统设置: [❌] 查看 [❌] 编辑 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📊 用户统计 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 注册时间: 2024-01-15 │ │ +│ │ 最后登录: 2024-01-20 14:30 │ │ +│ │ 登录次数: 25次 │ │ +│ │ 上传照片: 45张 │ │ +│ │ 创建分类: 3个 │ │ +│ │ 创建标签: 12个 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 💾 操作按钮 │ +│ [💾 保存] [🔄 重置密码] [🚫 禁用用户] [❌ 取消] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.6 系统设置模块 (System Settings) + +#### 2.6.1 网站配置 +``` +┌─────────────────────────────────────────────────────────┐ +│ 系统设置 System Settings │ +├─────────────────────────────────────────────────────────┤ +│ 🌐 网站配置 📁 文件设置 🎨 主题设置 💾 缓存设置 │ +├─────────────────────────────────────────────────────────┤ +│ 🌐 网站基本信息 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 网站名称: [输入框] 摄影作品集 │ │ +│ │ 网站描述: [文本框] 专业摄影作品展示平台 │ │ +│ │ 网站关键词: [标签输入] 摄影,作品集,艺术 │ │ +│ │ 网站Logo: [选择图片] [上传新Logo] │ │ +│ │ 网站图标: [选择图片] [上传Favicon] │ │ +│ │ 联系邮箱: [输入框] admin@photography.com │ │ +│ │ 版权信息: [输入框] © 2024 Photography Portfolio │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 📁 文件上传设置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 允许格式: [多选] JPG PNG GIF WEBP RAW TIFF │ │ +│ │ 最大大小: [数字输入] 50 MB │ │ +│ │ 图片质量: [滑块] 85% │ │ +│ │ 缩略图尺寸: [输入框] 300x300 │ │ +│ │ 水印设置: [开关] 启用 [文本/图片] [位置选择] │ │ +│ │ 自动压缩: [开关] 启用 │ │ +│ │ 存储方式: [单选] 本地存储/MinIO/AWS S3 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 🎨 主题设置 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 默认主题: [下拉选择] 浅色主题 │ │ +│ │ 主色调: [颜色选择器] #3b82f6 │ │ +│ │ 辅助色: [颜色选择器] #6b7280 │ │ +│ │ 背景色: [颜色选择器] #ffffff │ │ +│ │ 字体设置: [下拉选择] 系统默认 │ │ +│ │ 布局样式: [单选] 网格布局/列表布局/瀑布流 │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.7 日志管理模块 (Log Management) + +#### 2.7.1 日志查看器 +``` +┌─────────────────────────────────────────────────────────┐ +│ 日志管理 Log Management │ +├─────────────────────────────────────────────────────────┤ +│ 🔍 [搜索] 📊 [级别筛选] 📅 [时间范围] 🔄 [自动刷新] │ +├─────────────────────────────────────────────────────────┤ +│ 时间 │ 级别 │ 模块 │ 消息 │ +│ ───────────┼─────┼────────┼─────────────────────── │ +│ 14:30:25 │ INFO │ Upload │ 文件上传成功: IMG_001.jpg │ +│ 14:30:20 │ WARN │ Cache │ Redis连接超时,使用备用缓存 │ +│ 14:30:15 │ ERROR│ DB │ 数据库查询超时 │ +│ 14:30:10 │ DEBUG│ API │ GET /api/photos 200 │ +│ 14:30:05 │ INFO │ Auth │ 用户admin登录成功 │ +│ 14:30:00 │ INFO │ System │ 系统启动完成 │ +├─────────────────────────────────────────────────────────┤ +│ 📊 统计信息 │ +│ 总计: 1,234条 | 错误: 12条 | 警告: 45条 | 信息: 1,177条 │ +└─────────────────────────────────────────────────────────┘ +``` + +## 3. 技术实现方案 + +### 3.1 前端架构 + +#### 3.1.1 项目结构 +``` +admin/ +├── src/ +│ ├── components/ # 通用组件 +│ │ ├── Layout/ # 布局组件 +│ │ ├── Form/ # 表单组件 +│ │ ├── Table/ # 表格组件 +│ │ ├── Upload/ # 上传组件 +│ │ └── Chart/ # 图表组件 +│ ├── pages/ # 页面组件 +│ │ ├── Dashboard/ # 仪表板 +│ │ ├── Photos/ # 照片管理 +│ │ ├── Categories/ # 分类管理 +│ │ ├── Tags/ # 标签管理 +│ │ ├── Users/ # 用户管理 +│ │ ├── Settings/ # 系统设置 +│ │ └── Logs/ # 日志管理 +│ ├── services/ # API服务 +│ ├── stores/ # 状态管理 +│ ├── utils/ # 工具函数 +│ ├── types/ # TypeScript类型 +│ └── styles/ # 样式文件 +├── public/ # 静态资源 +├── package.json +├── tsconfig.json +└── tailwind.config.js +``` + +#### 3.1.2 核心依赖 +```json +{ + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.0", + "typescript": "^4.9.0", + "tailwindcss": "^3.2.0", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-dropdown-menu": "^2.0.1", + "@radix-ui/react-select": "^1.2.0", + "lucide-react": "^0.263.0", + "zustand": "^4.3.0", + "axios": "^1.3.0", + "react-query": "^3.39.0", + "react-hook-form": "^7.43.0", + "zod": "^3.20.0", + "recharts": "^2.5.0", + "react-dropzone": "^14.2.0" + } +} +``` + +### 3.2 后端API设计 + +#### 3.2.1 RESTful API 结构 +``` +/api/admin/ +├── auth/ # 认证相关 +│ ├── POST /login # 登录 +│ ├── POST /logout # 登出 +│ ├── POST /refresh # 刷新token +│ └── GET /profile # 用户信息 +├── dashboard/ # 仪表板 +│ ├── GET /stats # 统计数据 +│ └── GET /activities # 近期活动 +├── photos/ # 照片管理 +│ ├── GET / # 照片列表 +│ ├── POST / # 创建照片 +│ ├── GET /:id # 照片详情 +│ ├── PUT /:id # 更新照片 +│ ├── DELETE /:id # 删除照片 +│ ├── POST /upload # 上传文件 +│ └── POST /batch # 批量操作 +├── categories/ # 分类管理 +│ ├── GET / # 分类列表 +│ ├── POST / # 创建分类 +│ ├── GET /:id # 分类详情 +│ ├── PUT /:id # 更新分类 +│ ├── DELETE /:id # 删除分类 +│ └── PUT /reorder # 重新排序 +├── tags/ # 标签管理 +│ ├── GET / # 标签列表 +│ ├── POST / # 创建标签 +│ ├── GET /:id # 标签详情 +│ ├── PUT /:id # 更新标签 +│ ├── DELETE /:id # 删除标签 +│ └── GET /suggestions # 标签建议 +├── users/ # 用户管理 +│ ├── GET / # 用户列表 +│ ├── POST / # 创建用户 +│ ├── GET /:id # 用户详情 +│ ├── PUT /:id # 更新用户 +│ ├── DELETE /:id # 删除用户 +│ └── PUT /:id/password # 修改密码 +├── settings/ # 系统设置 +│ ├── GET / # 获取设置 +│ ├── PUT / # 更新设置 +│ └── POST /test # 测试配置 +└── logs/ # 日志管理 + ├── GET / # 日志列表 + └── GET /stats # 日志统计 +``` + +#### 3.2.2 数据模型定义 +```go +// 照片模型 +type Photo struct { + ID uint `json:"id" gorm:"primaryKey"` + Title string `json:"title" gorm:"size:255;not null"` + Description string `json:"description" gorm:"type:text"` + Filename string `json:"filename" gorm:"size:255;not null"` + FilePath string `json:"file_path" gorm:"size:500;not null"` + FileSize int64 `json:"file_size"` + MimeType string `json:"mime_type" gorm:"size:100"` + Width int `json:"width"` + Height int `json:"height"` + CategoryID uint `json:"category_id"` + Category Category `json:"category" gorm:"foreignKey:CategoryID"` + Tags []Tag `json:"tags" gorm:"many2many:photo_tags;"` + EXIF string `json:"exif" gorm:"type:jsonb"` + TakenAt time.Time `json:"taken_at"` + Location string `json:"location" gorm:"size:255"` + IsPublic bool `json:"is_public" gorm:"default:true"` + Status string `json:"status" gorm:"size:20;default:'draft'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// 分类模型 +type Category struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:100;not null"` + Description string `json:"description" gorm:"type:text"` + ParentID *uint `json:"parent_id"` + Parent *Category `json:"parent" gorm:"foreignKey:ParentID"` + Children []Category `json:"children" gorm:"foreignKey:ParentID"` + Color string `json:"color" gorm:"size:7;default:'#3b82f6'"` + CoverImage string `json:"cover_image" gorm:"size:500"` + Sort int `json:"sort" gorm:"default:0"` + IsActive bool `json:"is_active" gorm:"default:true"` + PhotoCount int `json:"photo_count" gorm:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// 标签模型 +type Tag struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:50;not null;unique"` + Color string `json:"color" gorm:"size:7;default:'#6b7280'"` + UseCount int `json:"use_count" gorm:"default:0"` + IsActive bool `json:"is_active" gorm:"default:true"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// 用户模型 +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"size:50;not null;unique"` + Email string `json:"email" gorm:"size:100;not null;unique"` + Password string `json:"-" gorm:"size:255;not null"` + Name string `json:"name" gorm:"size:100"` + Avatar string `json:"avatar" gorm:"size:500"` + Role string `json:"role" gorm:"size:20;default:'user'"` + IsActive bool `json:"is_active" gorm:"default:true"` + LastLogin time.Time `json:"last_login"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} +``` + +### 3.3 数据库设计 + +#### 3.3.1 核心表结构 +```sql +-- 照片表 +CREATE TABLE photos ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + filename VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size BIGINT, + mime_type VARCHAR(100), + width INTEGER, + height INTEGER, + category_id INTEGER REFERENCES categories(id), + exif JSONB, + taken_at TIMESTAMP, + location VARCHAR(255), + is_public BOOLEAN DEFAULT true, + status VARCHAR(20) DEFAULT 'draft', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 分类表 +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + parent_id INTEGER REFERENCES categories(id), + color VARCHAR(7) DEFAULT '#3b82f6', + cover_image VARCHAR(500), + sort INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 标签表 +CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + color VARCHAR(7) DEFAULT '#6b7280', + use_count INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 照片标签关联表 +CREATE TABLE photo_tags ( + photo_id INTEGER REFERENCES photos(id) ON DELETE CASCADE, + tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (photo_id, tag_id) +); + +-- 用户表 +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + name VARCHAR(100), + avatar VARCHAR(500), + role VARCHAR(20) DEFAULT 'user', + is_active BOOLEAN DEFAULT true, + last_login TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 系统设置表 +CREATE TABLE settings ( + id SERIAL PRIMARY KEY, + key VARCHAR(100) NOT NULL UNIQUE, + value TEXT, + type VARCHAR(20) DEFAULT 'string', + group_name VARCHAR(50), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 操作日志表 +CREATE TABLE activity_logs ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + action VARCHAR(50) NOT NULL, + resource_type VARCHAR(50), + resource_id INTEGER, + details JSONB, + ip_address INET, + user_agent TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### 3.4 认证与权限 + +#### 3.4.1 JWT认证流程 +``` +1. 用户登录 → 验证账号密码 +2. 生成JWT Token → 包含用户信息和权限 +3. 前端存储Token → localStorage/sessionStorage +4. 请求携带Token → Authorization Header +5. 后端验证Token → 中间件验证 +6. 权限检查 → 基于角色的访问控制 +``` + +#### 3.4.2 角色权限定义 +```go +// 角色定义 +const ( + RoleAdmin = "admin" // 管理员 - 所有权限 + RoleEditor = "editor" // 编辑者 - 内容管理权限 + RoleUser = "user" // 用户 - 基本权限 + RoleGuest = "guest" // 访客 - 只读权限 +) + +// 权限定义 +type Permission struct { + Resource string // 资源类型 + Actions []string // 允许的操作 +} + +// 角色权限映射 +var RolePermissions = map[string][]Permission{ + RoleAdmin: { + {Resource: "photos", Actions: []string{"create", "read", "update", "delete"}}, + {Resource: "categories", Actions: []string{"create", "read", "update", "delete"}}, + {Resource: "tags", Actions: []string{"create", "read", "update", "delete"}}, + {Resource: "users", Actions: []string{"create", "read", "update", "delete"}}, + {Resource: "settings", Actions: []string{"read", "update"}}, + {Resource: "logs", Actions: []string{"read"}}, + }, + RoleEditor: { + {Resource: "photos", Actions: []string{"create", "read", "update", "delete"}}, + {Resource: "categories", Actions: []string{"create", "read", "update"}}, + {Resource: "tags", Actions: []string{"create", "read", "update"}}, + }, + RoleUser: { + {Resource: "photos", Actions: []string{"read"}}, + {Resource: "categories", Actions: []string{"read"}}, + {Resource: "tags", Actions: []string{"read"}}, + }, + RoleGuest: { + {Resource: "photos", Actions: []string{"read"}}, + {Resource: "categories", Actions: []string{"read"}}, + }, +} +``` + +### 3.5 文件上传与处理 + +#### 3.5.1 上传流程 +``` +1. 前端选择文件 → 文件验证 (格式、大小) +2. 创建上传任务 → 生成临时文件路径 +3. 分块上传 → 支持断点续传 +4. 文件合并 → 合并所有分块 +5. 图片处理 → 生成缩略图、提取EXIF +6. 存储文件 → 本地存储或云存储 +7. 更新数据库 → 保存文件信息 +8. 返回结果 → 上传成功确认 +``` + +#### 3.5.2 图片处理 +```go +// 图片处理服务 +type ImageProcessor struct { + storage StorageService + thumbnails []ThumbnailConfig + watermark WatermarkConfig + quality int +} + +// 缩略图配置 +type ThumbnailConfig struct { + Name string + Width int + Height int + Crop bool +} + +// 处理上传的图片 +func (p *ImageProcessor) ProcessImage(file *multipart.FileHeader) (*ImageResult, error) { + // 1. 验证文件格式 + if !p.isValidImage(file) { + return nil, ErrInvalidImageFormat + } + + // 2. 打开图片 + img, err := bimg.NewFromFile(file.Filename) + if err != nil { + return nil, err + } + + // 3. 提取EXIF信息 + exif, err := p.extractEXIF(img) + if err != nil { + logger.Warn("Failed to extract EXIF", "error", err) + } + + // 4. 生成缩略图 + thumbnails := make(map[string]string) + for _, config := range p.thumbnails { + thumbnail, err := p.createThumbnail(img, config) + if err != nil { + logger.Error("Failed to create thumbnail", "config", config, "error", err) + continue + } + thumbnails[config.Name] = thumbnail + } + + // 5. 添加水印 (可选) + if p.watermark.Enabled { + img, err = p.addWatermark(img, p.watermark) + if err != nil { + logger.Warn("Failed to add watermark", "error", err) + } + } + + // 6. 保存原图 + processed, err := img.Process(bimg.Options{ + Quality: p.quality, + Type: bimg.JPEG, + }) + if err != nil { + return nil, err + } + + // 7. 上传到存储 + originalPath, err := p.storage.Save(processed, "originals") + if err != nil { + return nil, err + } + + return &ImageResult{ + OriginalPath: originalPath, + Thumbnails: thumbnails, + EXIF: exif, + Width: img.Size().Width, + Height: img.Size().Height, + }, nil +} +``` + +### 3.6 性能优化 + +#### 3.6.1 前端优化 +```javascript +// 1. 代码分割 +const LazyPhotoManagement = lazy(() => import('./pages/Photos')); +const LazyDashboard = lazy(() => import('./pages/Dashboard')); + +// 2. 虚拟滚动 (大量照片列表) +import { FixedSizeGrid } from 'react-window'; + +// 3. 图片懒加载 +const LazyImage = ({ src, alt, ...props }) => { + const [isLoaded, setIsLoaded] = useState(false); + const [isInView, setIsInView] = useState(false); + const imgRef = useRef(); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsInView(true); + observer.disconnect(); + } + }, + { threshold: 0.1 } + ); + + if (imgRef.current) { + observer.observe(imgRef.current); + } + + return () => observer.disconnect(); + }, []); + + return ( +
+ {isInView && ( + {alt} setIsLoaded(true)} + style={{ opacity: isLoaded ? 1 : 0 }} + /> + )} +
+ ); +}; + +// 4. 搜索防抖 +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; +``` + +#### 3.6.2 后端优化 +```go +// 1. 数据库查询优化 +func (r *PhotoRepository) GetPhotosWithPagination(page, limit int, filters PhotoFilters) ([]Photo, int, error) { + var photos []Photo + var total int64 + + query := r.db.Model(&Photo{}). + Preload("Category"). + Preload("Tags") + + // 添加筛选条件 + if filters.CategoryID > 0 { + query = query.Where("category_id = ?", filters.CategoryID) + } + + if len(filters.Tags) > 0 { + query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id"). + Where("photo_tags.tag_id IN (?)", filters.Tags) + } + + if filters.Search != "" { + query = query.Where("title ILIKE ? OR description ILIKE ?", + "%"+filters.Search+"%", "%"+filters.Search+"%") + } + + // 计算总数 + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + // 分页查询 + offset := (page - 1) * limit + err = query.Offset(offset).Limit(limit). + Order("created_at DESC"). + Find(&photos).Error + + return photos, int(total), err +} + +// 2. Redis缓存 +func (s *PhotoService) GetPhotosByCategory(categoryID uint) ([]Photo, error) { + cacheKey := fmt.Sprintf("photos:category:%d", categoryID) + + // 尝试从缓存获取 + cached, err := s.cache.Get(cacheKey) + if err == nil { + var photos []Photo + if err := json.Unmarshal(cached, &photos); err == nil { + return photos, nil + } + } + + // 从数据库获取 + photos, err := s.repo.GetPhotosByCategory(categoryID) + if err != nil { + return nil, err + } + + // 缓存结果 + if data, err := json.Marshal(photos); err == nil { + s.cache.Set(cacheKey, data, 10*time.Minute) + } + + return photos, nil +} + +// 3. 并发处理 +func (s *PhotoService) ProcessBatchUpload(files []*multipart.FileHeader) error { + const maxWorkers = 5 + jobs := make(chan *multipart.FileHeader, len(files)) + results := make(chan error, len(files)) + + // 启动工作协程 + for i := 0; i < maxWorkers; i++ { + go func() { + for file := range jobs { + err := s.ProcessSingleUpload(file) + results <- err + } + }() + } + + // 发送任务 + for _, file := range files { + jobs <- file + } + close(jobs) + + // 收集结果 + var errors []error + for i := 0; i < len(files); i++ { + if err := <-results; err != nil { + errors = append(errors, err) + } + } + + if len(errors) > 0 { + return fmt.Errorf("批量上传失败: %v", errors) + } + + return nil +} +``` + +## 4. 部署与监控 + +### 4.1 Docker部署 + +#### 4.1.1 Dockerfile +```dockerfile +# 前端构建 +FROM node:18-alpine AS frontend-builder +WORKDIR /app +COPY admin/package*.json ./ +RUN npm ci +COPY admin/ . +RUN npm run build + +# 后端构建 +FROM golang:1.21-alpine AS backend-builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o main cmd/server/main.go + +# 生产镜像 +FROM alpine:latest +RUN apk --no-cache add ca-certificates tzdata +WORKDIR /root/ +COPY --from=backend-builder /app/main . +COPY --from=frontend-builder /app/dist ./web +COPY config/ ./config/ +COPY migrations/ ./migrations/ +EXPOSE 8080 +CMD ["./main"] +``` + +#### 4.1.2 docker-compose.yml +```yaml +version: '3.8' + +services: + app: + build: . + ports: + - "8080:8080" + depends_on: + - postgres + - redis + environment: + - DATABASE_URL=postgres://user:password@postgres:5432/photography + - REDIS_URL=redis://redis:6379 + volumes: + - uploads:/app/uploads + - logs:/app/logs + + postgres: + image: postgres:15 + environment: + POSTGRES_DB: photography + POSTGRES_USER: user + POSTGRES_PASSWORD: password + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + - uploads:/var/www/uploads + +volumes: + postgres_data: + redis_data: + uploads: + logs: +``` + +### 4.2 监控与日志 + +#### 4.2.1 健康检查 +```go +// 健康检查端点 +func (h *HealthHandler) CheckHealth(c *gin.Context) { + health := map[string]interface{}{ + "status": "ok", + "timestamp": time.Now(), + "services": map[string]interface{}{}, + } + + // 检查数据库连接 + if err := h.db.Raw("SELECT 1").Error; err != nil { + health["services"]["database"] = map[string]interface{}{ + "status": "error", + "error": err.Error(), + } + health["status"] = "error" + } else { + health["services"]["database"] = map[string]interface{}{ + "status": "ok", + } + } + + // 检查Redis连接 + if err := h.redis.Ping().Err(); err != nil { + health["services"]["redis"] = map[string]interface{}{ + "status": "error", + "error": err.Error(), + } + health["status"] = "error" + } else { + health["services"]["redis"] = map[string]interface{}{ + "status": "ok", + } + } + + // 检查存储服务 + if err := h.storage.HealthCheck(); err != nil { + health["services"]["storage"] = map[string]interface{}{ + "status": "error", + "error": err.Error(), + } + health["status"] = "error" + } else { + health["services"]["storage"] = map[string]interface{}{ + "status": "ok", + } + } + + if health["status"] == "ok" { + c.JSON(http.StatusOK, health) + } else { + c.JSON(http.StatusServiceUnavailable, health) + } +} +``` + +#### 4.2.2 日志配置 +```go +// 日志配置 +func InitLogger(config *Config) *logrus.Logger { + logger := logrus.New() + + // 设置日志级别 + level, err := logrus.ParseLevel(config.Logger.Level) + if err != nil { + level = logrus.InfoLevel + } + logger.SetLevel(level) + + // 设置日志格式 + if config.Logger.Format == "json" { + logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: "2006-01-02 15:04:05", + FieldMap: logrus.FieldMap{ + logrus.FieldKeyTime: "timestamp", + logrus.FieldKeyLevel: "level", + logrus.FieldKeyMsg: "message", + }, + }) + } else { + logger.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + }) + } + + // 设置日志输出 + if config.Logger.Output == "file" { + logFile := &lumberjack.Logger{ + Filename: config.Logger.Filename, + MaxSize: config.Logger.MaxSize, + MaxAge: config.Logger.MaxAge, + MaxBackups: config.Logger.MaxBackups, + LocalTime: true, + Compress: config.Logger.Compress, + } + logger.SetOutput(logFile) + } + + return logger +} +``` + +## 5. 测试策略 + +### 5.1 前端测试 + +#### 5.1.1 组件测试 +```typescript +// PhotoCard.test.tsx +import { render, screen, fireEvent } from '@testing-library/react'; +import { PhotoCard } from './PhotoCard'; +import { Photo } from '../types'; + +const mockPhoto: Photo = { + id: 1, + title: 'Test Photo', + description: 'Test description', + filename: 'test.jpg', + thumbnailUrl: '/thumbnails/test.jpg', + category: { id: 1, name: 'Test Category' }, + tags: [{ id: 1, name: 'test' }], + createdAt: '2024-01-01T00:00:00Z', +}; + +describe('PhotoCard', () => { + it('renders photo information correctly', () => { + render(); + + expect(screen.getByText('Test Photo')).toBeInTheDocument(); + expect(screen.getByText('Test description')).toBeInTheDocument(); + expect(screen.getByText('Test Category')).toBeInTheDocument(); + expect(screen.getByText('test')).toBeInTheDocument(); + }); + + it('calls onEdit when edit button is clicked', () => { + const mockOnEdit = jest.fn(); + render(); + + fireEvent.click(screen.getByText('编辑')); + expect(mockOnEdit).toHaveBeenCalledWith(mockPhoto); + }); + + it('calls onDelete when delete button is clicked', () => { + const mockOnDelete = jest.fn(); + render(); + + fireEvent.click(screen.getByText('删除')); + expect(mockOnDelete).toHaveBeenCalledWith(mockPhoto.id); + }); +}); +``` + +#### 5.1.2 API测试 +```typescript +// photoService.test.ts +import { photoService } from './photoService'; +import { mockApi } from '../test/mocks'; + +describe('PhotoService', () => { + beforeEach(() => { + mockApi.reset(); + }); + + it('fetches photos successfully', async () => { + const mockPhotos = [ + { id: 1, title: 'Photo 1' }, + { id: 2, title: 'Photo 2' }, + ]; + + mockApi.onGet('/api/admin/photos').reply(200, { + code: 0, + data: { photos: mockPhotos, total: 2 }, + }); + + const result = await photoService.getPhotos(); + expect(result.photos).toEqual(mockPhotos); + expect(result.total).toBe(2); + }); + + it('handles upload with progress', async () => { + const mockFile = new File(['test'], 'test.jpg', { type: 'image/jpeg' }); + const mockProgressCallback = jest.fn(); + + mockApi.onPost('/api/admin/photos/upload').reply(200, { + code: 0, + data: { id: 1, filename: 'test.jpg' }, + }); + + const result = await photoService.uploadPhoto(mockFile, mockProgressCallback); + expect(result.id).toBe(1); + expect(mockProgressCallback).toHaveBeenCalled(); + }); +}); +``` + +### 5.2 后端测试 + +#### 5.2.1 单元测试 +```go +// photo_service_test.go +package service + +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "photography-backend/internal/domain" + "photography-backend/internal/repository/mocks" +) + +func TestPhotoService_CreatePhoto(t *testing.T) { + // 准备测试数据 + mockRepo := new(mocks.PhotoRepository) + mockStorage := new(mocks.StorageService) + service := NewPhotoService(mockRepo, mockStorage) + + photo := &domain.Photo{ + Title: "Test Photo", + Description: "Test description", + CategoryID: 1, + } + + // 设置mock期望 + mockRepo.On("Create", mock.AnythingOfType("*domain.Photo")).Return(nil).Run(func(args mock.Arguments) { + p := args.Get(0).(*domain.Photo) + p.ID = 1 + }) + + // 执行测试 + result, err := service.CreatePhoto(photo) + + // 验证结果 + assert.NoError(t, err) + assert.Equal(t, uint(1), result.ID) + assert.Equal(t, "Test Photo", result.Title) + mockRepo.AssertExpectations(t) +} + +func TestPhotoService_GetPhotosByCategory(t *testing.T) { + mockRepo := new(mocks.PhotoRepository) + mockStorage := new(mocks.StorageService) + service := NewPhotoService(mockRepo, mockStorage) + + expectedPhotos := []domain.Photo{ + {ID: 1, Title: "Photo 1", CategoryID: 1}, + {ID: 2, Title: "Photo 2", CategoryID: 1}, + } + + mockRepo.On("GetByCategory", uint(1)).Return(expectedPhotos, nil) + + result, err := service.GetPhotosByCategory(1) + + assert.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, expectedPhotos[0].Title, result[0].Title) + mockRepo.AssertExpectations(t) +} +``` + +#### 5.2.2 集成测试 +```go +// photo_api_test.go +package api + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "photography-backend/internal/domain" + "photography-backend/test/testutils" +) + +func TestPhotoAPI_GetPhotos(t *testing.T) { + // 设置测试数据库 + db := testutils.SetupTestDB() + defer testutils.CleanupTestDB(db) + + // 插入测试数据 + testutils.SeedPhotos(db) + + // 创建测试路由 + router := gin.New() + photoHandler := NewPhotoHandler(db) + router.GET("/api/admin/photos", photoHandler.GetPhotos) + + // 创建测试请求 + req, _ := http.NewRequest("GET", "/api/admin/photos?page=1&limit=10", nil) + req.Header.Set("Authorization", "Bearer "+testutils.GetTestToken()) + + // 执行请求 + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // 验证响应 + assert.Equal(t, http.StatusOK, w.Code) + + var response struct { + Code int `json:"code"` + Data struct { + Photos []domain.Photo `json:"photos"` + Total int `json:"total"` + } `json:"data"` + } + + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, 0, response.Code) + assert.Greater(t, response.Data.Total, 0) + assert.Greater(t, len(response.Data.Photos), 0) +} + +func TestPhotoAPI_CreatePhoto(t *testing.T) { + db := testutils.SetupTestDB() + defer testutils.CleanupTestDB(db) + + router := gin.New() + photoHandler := NewPhotoHandler(db) + router.POST("/api/admin/photos", photoHandler.CreatePhoto) + + photoData := map[string]interface{}{ + "title": "Test Photo", + "description": "Test description", + "category_id": 1, + } + + jsonData, _ := json.Marshal(photoData) + req, _ := http.NewRequest("POST", "/api/admin/photos", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+testutils.GetTestToken()) + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + + var response struct { + Code int `json:"code"` + Data domain.Photo `json:"data"` + } + + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, 0, response.Code) + assert.Equal(t, "Test Photo", response.Data.Title) +} +``` + +## 6. 文档与维护 + +### 6.1 API文档 + +#### 6.1.1 OpenAPI规范 +```yaml +openapi: 3.0.0 +info: + title: 摄影作品集管理后台API + version: 1.0.0 + description: 摄影作品集网站管理后台的RESTful API + +paths: + /api/admin/photos: + get: + summary: 获取照片列表 + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: limit + in: query + schema: + type: integer + default: 20 + - name: category_id + in: query + schema: + type: integer + - name: tags + in: query + schema: + type: array + items: + type: integer + - name: search + in: query + schema: + type: string + responses: + 200: + description: 成功获取照片列表 + content: + application/json: + schema: + type: object + properties: + code: + type: integer + example: 0 + message: + type: string + example: success + data: + type: object + properties: + photos: + type: array + items: + $ref: '#/components/schemas/Photo' + total: + type: integer + example: 100 + page: + type: integer + example: 1 + limit: + type: integer + example: 20 + post: + summary: 创建照片 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PhotoCreate' + responses: + 201: + description: 成功创建照片 + content: + application/json: + schema: + type: object + properties: + code: + type: integer + example: 0 + message: + type: string + example: success + data: + $ref: '#/components/schemas/Photo' + +components: + schemas: + Photo: + type: object + properties: + id: + type: integer + example: 1 + title: + type: string + example: "Beautiful Sunset" + description: + type: string + example: "A beautiful sunset over the ocean" + filename: + type: string + example: "sunset.jpg" + file_path: + type: string + example: "/uploads/2024/01/sunset.jpg" + file_size: + type: integer + example: 1024000 + width: + type: integer + example: 1920 + height: + type: integer + example: 1080 + category: + $ref: '#/components/schemas/Category' + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + created_at: + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + updated_at: + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + + PhotoCreate: + type: object + required: + - title + - category_id + properties: + title: + type: string + example: "Beautiful Sunset" + description: + type: string + example: "A beautiful sunset over the ocean" + category_id: + type: integer + example: 1 + tag_ids: + type: array + items: + type: integer + example: [1, 2, 3] + + Category: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: "Landscape" + description: + type: string + example: "Landscape photography" + parent_id: + type: integer + nullable: true + example: null + color: + type: string + example: "#3b82f6" + is_active: + type: boolean + example: true + + Tag: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: "sunset" + color: + type: string + example: "#f59e0b" + use_count: + type: integer + example: 25 + is_active: + type: boolean + example: true + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +security: + - bearerAuth: [] +``` + +### 6.2 部署文档 + +#### 6.2.1 生产环境部署 +```bash +#!/bin/bash +# deploy.sh - 生产环境部署脚本 + +set -e + +echo "🚀 开始部署摄影作品集管理后台..." + +# 1. 拉取最新代码 +echo "📥 拉取最新代码..." +git pull origin main + +# 2. 构建前端 +echo "🔨 构建前端..." +cd admin +npm install +npm run build +cd .. + +# 3. 构建后端 +echo "🔨 构建后端..." +go mod tidy +go build -o photography-backend cmd/server/main.go + +# 4. 数据库迁移 +echo "🗄️ 执行数据库迁移..." +./photography-backend migrate + +# 5. 重启服务 +echo "🔄 重启服务..." +sudo systemctl restart photography-backend + +# 6. 检查服务状态 +echo "🔍 检查服务状态..." +sleep 5 +if curl -f http://localhost:8080/health > /dev/null 2>&1; then + echo "✅ 部署成功!" +else + echo "❌ 部署失败!" + exit 1 +fi + +echo "🎉 部署完成!" +``` + +#### 6.2.2 环境配置 +```yaml +# config/production.yaml +app: + name: "photography-backend" + version: "1.0.0" + environment: "production" + port: 8080 + debug: false + +database: + host: "localhost" + port: 5432 + username: "photography" + password: "${DB_PASSWORD}" + database: "photography" + ssl_mode: "require" + max_open_conns: 100 + max_idle_conns: 10 + conn_max_lifetime: 300 + +redis: + host: "localhost" + port: 6379 + password: "${REDIS_PASSWORD}" + database: 0 + pool_size: 100 + min_idle_conns: 10 + +storage: + type: "s3" + s3: + region: "us-west-2" + bucket: "photography-uploads" + access_key: "${S3_ACCESS_KEY}" + secret_key: "${S3_SECRET_KEY}" + endpoint: "" + use_ssl: true + +logger: + level: "info" + format: "json" + output: "file" + filename: "logs/app.log" + max_size: 100 + max_age: 30 + compress: true + +jwt: + secret: "${JWT_SECRET}" + expire_hours: 24 + +upload: + max_file_size: 52428800 # 50MB + allowed_types: ["image/jpeg", "image/png", "image/gif", "image/webp"] + thumbnail_sizes: + - { name: "small", width: 300, height: 300 } + - { name: "medium", width: 800, height: 600 } + - { name: "large", width: 1200, height: 900 } +``` + +## 7. 总结 + +摄影作品集管理后台是一个功能完善的内容管理系统,涵盖了现代Web应用所需的各个方面: + +### 7.1 核心特性 +- **直观的用户界面**: 基于React和shadcn/ui的现代化界面 +- **强大的照片管理**: 支持批量上传、处理、分类和标签 +- **灵活的权限系统**: 基于角色的访问控制 +- **高性能优化**: 多级缓存、懒加载、虚拟滚动等 +- **完善的监控**: 日志管理、健康检查、性能监控 + +### 7.2 技术优势 +- **前后端分离**: 便于独立开发和部署 +- **类型安全**: TypeScript确保代码质量 +- **模块化设计**: 易于维护和扩展 +- **云原生部署**: Docker容器化部署 +- **自动化流程**: CI/CD集成部署 + +### 7.3 扩展性 +- **微服务架构**: 支持后续拆分为微服务 +- **多存储支持**: 本地存储、MinIO、AWS S3 +- **插件系统**: 支持功能插件扩展 +- **API标准化**: RESTful API设计 + +这个管理后台为摄影作品集网站提供了强大的后台管理能力,能够满足专业摄影师和摄影工作室的各种需求。通过模块化的设计和现代化的技术栈,系统具有良好的可维护性和扩展性,能够随着业务需求的增长而持续演进。 \ No newline at end of file diff --git a/docs/development/saved-docs/operations-monitoring.md b/docs/development/saved-docs/operations-monitoring.md new file mode 100644 index 0000000..d13aee7 --- /dev/null +++ b/docs/development/saved-docs/operations-monitoring.md @@ -0,0 +1,710 @@ +# 摄影作品集网站 - 运维监控方案 + +## 🎯 方案概述 + +这是一个**简单实用的日志管理方案**,专注于日志收集和问题修复。日志查看功能集成到管理后台,提供友好的Web界面。 + +### 设计原则 +- **集成化**: 日志查看功能集成到管理后台 +- **用户友好**: 提供美观易用的Web界面 +- **问题导向**: 专注于快速定位和修复问题 +- **低维护成本**: 几乎零维护的方案 +- **渐进式**: 后续可以根据需要扩展 + +## 📋 核心组件 + +### 日志管理架构 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 后端应用日志 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 错误日志 │ │ 访问日志 │ │ 业务日志 │ │ +│ │ (JSON格式) │ │ (HTTP日志) │ │ (操作日志) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ 管理后台日志模块 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 日志查看器 │ │ 实时监控 │ │ 统计分析 │ │ +│ │ (Web界面) │ │ (自动刷新) │ │ (图表展示) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ API接口层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 日志查询API │ │ 统计API │ │ 搜索API │ │ +│ │(/api/logs) │ │(/api/stats) │ │(/api/search)│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 🔧 技术选择 + +### 日志方案 +- **日志存储**: 本地文件 (JSON格式) +- **日志轮转**: lumberjack.v2 +- **日志查看**: 管理后台Web界面 +- **Trace ID**: 集成OpenTracing (已在架构文档中添加) +- **权限控制**: 基于管理后台的用户权限 + +### 集成方式 +- **前端**: 管理后台的日志管理模块 +- **后端**: Gin路由提供日志查询API +- **认证**: 复用管理后台的登录认证 +- **权限**: 仅管理员可访问日志功能 + +## 📝 日志配置 + +### 1. 应用日志配置 + +#### 日志配置文件 (config.yaml) +```yaml +# config/config.yaml +logger: + level: "info" + format: "json" + output: "file" + filename: "/app/logs/app.log" + max_size: 100 # MB + max_age: 7 # days + compress: true + +# 可选:如果需要链路追踪 +tracing: + enabled: true + service_name: "photography-backend" + jaeger: + endpoint: "http://localhost:14268/api/traces" + sampling_rate: 1.0 +``` + +### 2. 日志格式标准化 + +#### 统一的日志格式 +```json +{ + "timestamp": "2024-01-15T10:30:00Z", + "level": "info", + "message": "Photo created successfully", + "service": "photography-backend", + "trace_id": "abc123def456", + "request_id": "req-789", + "user_id": "user-123", + "operation": "create_photo", + "photo_id": 1001, + "duration": 0.5, + "error": null +} +``` + +### 3. 日志分类 + +#### 三种核心日志类型 +```bash +# 日志目录结构 +logs/ +├── app.log # 应用日志 (所有级别) +├── error.log # 错误日志 (ERROR级别) +└── access.log # HTTP访问日志 +``` + +#### 日志级别使用 +```go +// 日志级别使用指南 +logger.Info("正常业务操作") // 记录重要的业务操作 +logger.Warn("需要关注的情况") // 记录警告信息 +logger.Error("错误情况") // 记录错误信息 +logger.Debug("调试信息") // 开发调试用 +``` + +## 🚀 集成到管理后台 + +### 1. 后端集成步骤 + +#### 在main.go中注册日志路由 +```go +// cmd/server/main.go +func main() { + // ... 其他初始化代码 + + // 创建Gin引擎 + r := gin.Default() + + // 注册API路由 + apiGroup := r.Group("/api") + { + // 其他API路由... + } + + // 注册管理后台路由 + adminGroup := r.Group("/admin/api") + { + // 注册日志管理路由 + admin.RegisterLogRoutes(adminGroup, "logs/app.log") + + // 其他管理后台路由... + } + + r.Run(":8080") +} +``` + +#### 日志处理器实现 +```go +// internal/api/handlers/admin/logs_handler.go +package admin + +import ( + "bufio" + "encoding/json" + "net/http" + "os" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "photography-backend/pkg/middleware" +) + +// LogEntry 日志条目 +type LogEntry struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + TraceID string `json:"trace_id,omitempty"` + UserID string `json:"user_id,omitempty"` + Operation string `json:"operation,omitempty"` +} + +// LogHandler 日志处理器 +type LogHandler struct { + logFile string +} + +// GetLogs 获取日志列表 +func (h *LogHandler) GetLogs(c *gin.Context) { + // 获取参数 + levelFilter := c.Query("level") + searchFilter := c.Query("search") + traceID := c.Query("trace_id") + lines := 100 + if l := c.Query("lines"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 1000 { + lines = parsed + } + } + + // 读取日志文件 + logs, err := h.readLogs(lines, levelFilter, searchFilter, traceID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "读取日志失败", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{ + "logs": logs, + "total": len(logs), + }, + }) +} + +// GetLogStats 获取日志统计 +func (h *LogHandler) GetLogStats(c *gin.Context) { + stats := map[string]int{ + "total": 0, + "error": 0, + "warn": 0, + "info": 0, + "debug": 0, + } + + file, err := os.Open(h.logFile) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "读取日志文件失败", + "details": err.Error(), + }) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + + stats["total"]++ + + // 解析日志级别 + var entry LogEntry + if err := json.Unmarshal([]byte(line), &entry); err == nil { + if count, exists := stats[entry.Level]; exists { + stats[entry.Level] = count + 1 + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": stats, + }) +} + +// RegisterLogRoutes 注册日志相关路由 +func RegisterLogRoutes(r *gin.RouterGroup, logFile string) { + logHandler := NewLogHandler(logFile) + + // 需要管理员权限的路由组 + adminGroup := r.Group("") + adminGroup.Use(middleware.RequireAuth()) // 需要登录 + adminGroup.Use(middleware.RequireAdmin()) // 需要管理员权限 + + { + adminGroup.GET("/logs", logHandler.GetLogs) + adminGroup.GET("/logs/stats", logHandler.GetLogStats) + } +} +``` + +### 2. 前端集成步骤 + +#### 管理后台日志查看组件 +```javascript +// admin/src/pages/Logs/LogViewer.jsx +import React, { useState, useEffect } from 'react'; +import { + Card, + Table, + Select, + Input, + Button, + Tag, + Space, + Statistic, + Row, + Col, + message +} from 'antd'; + +const LogViewer = () => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({ + level: '', + search: '', + trace_id: '', + lines: 100, + }); + const [stats, setStats] = useState({}); + const [autoRefresh, setAutoRefresh] = useState(false); + + // 日志级别配置 + const levelConfig = { + error: { color: 'red', icon: '❌' }, + warn: { color: 'orange', icon: '⚠️' }, + info: { color: 'blue', icon: 'ℹ️' }, + debug: { color: 'default', icon: '🐛' }, + }; + + // 获取日志数据 + const fetchLogs = async () => { + setLoading(true); + try { + const response = await adminApi.get('/logs', { params: filters }); + setLogs(response.data.data.logs || []); + } catch (error) { + message.error('获取日志失败'); + } finally { + setLoading(false); + } + }; + + // 获取统计数据 + const fetchStats = async () => { + try { + const response = await adminApi.get('/logs/stats'); + setStats(response.data.data || {}); + } catch (error) { + console.error('获取统计数据失败', error); + } + }; + + useEffect(() => { + fetchLogs(); + fetchStats(); + }, [filters]); + + // 自动刷新 + useEffect(() => { + let interval; + if (autoRefresh) { + interval = setInterval(() => { + fetchLogs(); + fetchStats(); + }, 5000); + } + return () => interval && clearInterval(interval); + }, [autoRefresh, filters]); + + // 表格列配置 + const columns = [ + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + width: 180, + render: (timestamp) => new Date(timestamp).toLocaleString(), + }, + { + title: '级别', + dataIndex: 'level', + key: 'level', + width: 80, + render: (level) => { + const config = levelConfig[level] || levelConfig.info; + return ( + + {config.icon} {level.toUpperCase()} + + ); + }, + }, + { + title: 'Trace ID', + dataIndex: 'trace_id', + key: 'trace_id', + width: 120, + render: (traceId) => traceId ? ( + + ) : '-', + }, + { + title: '消息', + dataIndex: 'message', + key: 'message', + ellipsis: true, + render: (message, record) => { + // 高亮搜索关键词 + if (filters.search && message.toLowerCase().includes(filters.search.toLowerCase())) { + const regex = new RegExp(`(${filters.search})`, 'gi'); + const parts = message.split(regex); + return parts.map((part, index) => + part.toLowerCase() === filters.search.toLowerCase() ? + {part} : part + ); + } + return message; + }, + }, + ]; + + return ( +
+ {/* 统计卡片 */} + + + + + + + + + + + + + + + + + + + + + + + + {/* 过滤控件 */} + + + + + setFilters(prev => ({ ...prev, search: e.target.value }))} + /> + + setFilters(prev => ({ ...prev, trace_id: e.target.value }))} + /> + + + + + + + + + + {/* 日志表格 */} + + `${record.timestamp}-${index}`} + pagination={{ + showSizeChanger: false, + showQuickJumper: true, + showTotal: (total) => `共 ${total} 条日志`, + }} + scroll={{ x: 800 }} + /> + + + ); +}; + +export default LogViewer; +``` + +### 3. 配置更新 + +#### 更新应用配置文件 +```yaml +# config/config.yaml +app: + name: "photography-backend" + version: "1.0.0" + environment: "production" + +logger: + level: "info" + format: "json" + output: "file" + filename: "logs/app.log" + max_size: 100 + max_age: 7 + compress: true + +tracing: + enabled: true + service_name: "photography-backend" + sampling_rate: 1.0 + +admin: + log_access: true # 启用日志访问功能 + log_retention_days: 30 # 日志保留天数 +``` + +### 4. 一键部署脚本 + +#### 部署管理后台日志功能 +```bash +#!/bin/bash +# deploy-admin-logs.sh + +echo "🚀 部署管理后台日志功能..." + +# 创建必要目录 +mkdir -p logs +mkdir -p admin/src/pages/Logs + +# 确保日志文件存在 +touch logs/app.log +chmod 664 logs/app.log + +# 创建日志轮转配置 +cat > /etc/logrotate.d/photography-backend << 'EOF' +/path/to/photography/logs/*.log { + daily + rotate 30 + compress + delaycompress + missingok + notifempty + create 664 app app + postrotate + systemctl reload photography-backend + endscript +} +EOF + +# 设置权限 +chown -R app:app logs/ +chmod 755 logs/ + +echo "✅ 管理后台日志功能部署完成!" +echo "" +echo "📋 访问方式:" +echo " 1. 登录管理后台: http://localhost:8080/admin" +echo " 2. 进入日志管理页面" +echo " 3. 使用管理员账号访问" +echo "" +echo "🔧 API地址:" +echo " 日志列表: GET /admin/api/logs" +echo " 日志统计: GET /admin/api/logs/stats" +``` + +## 🔧 故障排查流程 + +### 1. 问题诊断步骤 +```bash +# 快速诊断脚本 +#!/bin/bash +# scripts/quick-diagnosis.sh + +echo "🔍 快速问题诊断" + +# 1. 检查服务状态 +echo "1. 检查服务状态..." +docker-compose ps + +# 2. 检查最近错误 +echo "2. 最近10条错误日志..." +tail -n 1000 logs/app.log | grep -i error | tail -10 | jq -r '.timestamp + " " + .message' + +# 3. 检查磁盘空间 +echo "3. 检查磁盘空间..." +df -h + +# 4. 检查日志文件大小 +echo "4. 检查日志文件大小..." +ls -lh logs/ + +# 5. 检查内存使用 +echo "5. 检查内存使用..." +docker stats --no-stream photography-backend + +echo "✅ 诊断完成" +``` + +### 2. 常见问题解决 + +#### 问题1:无法找到错误原因 +```bash +# 1. 获取完整的错误上下文 +grep -B 5 -A 5 "error_message" logs/app.log + +# 2. 按时间范围查找 +grep "2024-01-15T10:" logs/app.log | grep -i error +``` + +#### 问题2:需要追踪特定用户的操作 +```bash +# 按用户ID查找 +grep "user-123" logs/app.log | jq -r '.timestamp + " " + .message' + +# 按操作类型查找 +grep "create_photo" logs/app.log | jq -r '.timestamp + " " + .message' +``` + +#### 问题3:日志文件太大 +```bash +# 手动轮转日志 +mv logs/app.log logs/app.log.old +sudo systemctl restart photography-backend + +# 或者使用logrotate +sudo logrotate -f /etc/logrotate.d/photography-backend +``` + +## 🎯 总结 + +这个集成到管理后台的日志方案提供了: + +### ✨ 核心特性 +- **🏢 集成化管理** - 完全集成到管理后台,统一的用户体验 +- **🔐 权限控制** - 基于管理后台的认证和授权体系 +- **📊 专业界面** - 使用React组件,美观且专业 +- **🔍 强大搜索** - 支持关键词、级别、Trace ID多维度过滤 +- **⏰ 实时监控** - 自动刷新功能,实时观察系统状态 +- **📈 统计分析** - 直观的统计卡片,快速了解系统健康状况 + +### 🚀 部署简单 +1. 运行部署脚本: `./deploy-admin-logs.sh` +2. 访问管理后台: `http://localhost:8080/admin` +3. 登录管理员账号,进入日志管理页面 + +### 💡 使用场景 +- **错误排查**: 管理员快速定位和分析错误日志 +- **性能监控**: 通过Trace ID追踪完整请求链路 +- **运营监控**: 实时观察系统运行状态 +- **历史分析**: 搜索和分析历史日志数据 +- **团队协作**: 多个管理员可以同时查看和分析日志 + +### 🔧 技术优势 +- **集成性**: 完全集成到现有管理后台 +- **安全性**: 基于管理后台的权限控制 +- **专业性**: 使用成熟的UI组件库 +- **可扩展**: 易于添加新的日志分析功能 +- **用户友好**: 直观的界面,无需学习成本 + +### 📋 API接口 +- `GET /admin/api/logs` - 获取日志列表 +- `GET /admin/api/logs/stats` - 获取日志统计 + +这就是最适合生产环境的日志管理方案 - **专业、安全、易用**! \ No newline at end of file diff --git a/docs/development/saved-docs/v1.0-overview.md b/docs/development/saved-docs/v1.0-overview.md new file mode 100644 index 0000000..151045f --- /dev/null +++ b/docs/development/saved-docs/v1.0-overview.md @@ -0,0 +1,249 @@ +# 摄影作品集网站 v1.0 - 开发文档 + +## 📋 v1.0 版本概述 + +v1.0 是摄影作品集网站的核心功能版本,实现了完整的摄影作品展示、管理和用户交互功能。 + +### 🎯 版本目标 +- 构建稳定可靠的摄影作品集展示平台 +- 实现高效的照片管理和处理系统 +- 提供完善的用户认证和权限控制 +- 支持多种图片格式和优化策略 + +### 📅 开发周期 +- **开始时间**: 2024-01-15 +- **预计完成**: 2024-04-15 +- **当前状态**: 设计阶段 + +## 🏗️ 架构设计 + +### 整体架构 +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户界面层 │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 前端展示网站 │ │ 管理后台界面 │ │ +│ │ (Next.js) │ │ (React) │ │ +│ └─────────────────┘ └─────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ API 接口层 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ RESTful API │ │ +│ │ (Gin Framework) │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 业务逻辑层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 照片管理 │ │ 用户认证 │ │ 文件处理 │ │ +│ │ 服务 │ │ 服务 │ │ 服务 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 数据访问层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ MinIO/S3 │ │ +│ │ 数据库 │ │ 缓存 │ │ 对象存储 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 技术选型 +```yaml +前端技术栈: + - Framework: Next.js 15 + React 19 + - Language: TypeScript + - Styling: Tailwind CSS + - State: TanStack Query + Zustand + - UI Components: shadcn/ui + Radix UI + +后端技术栈: + - Framework: Golang + Gin + - Database: PostgreSQL 15 + - Cache: Redis 7 + - ORM: GORM + - Authentication: JWT + - Image Processing: libvips/bimg + +存储和部署: + - Object Storage: MinIO/AWS S3 + - Web Server: Caddy + - Container: Docker + Docker Compose + - CI/CD: Gitea Actions +``` + +## 🚀 功能特性 + +### 核心功能 +- ✅ **前端展示** + - 响应式照片网格展示 + - 时间线视图 + - 照片详情模态框 + - 分类和标签筛选 + +- 🔄 **管理后台** + - 照片批量上传和管理 + - 分类和标签管理 + - 用户权限控制 + - 系统设置配置 + +- 🔄 **后端API** + - 完整的CRUD操作 + - 文件上传处理 + - 图片多格式转换 + - 用户认证授权 + +- 🔄 **数据管理** + - PostgreSQL关系数据库 + - Redis缓存加速 + - MinIO对象存储 + - 数据备份恢复 + +### 性能特性 +- **图片优化**: 多格式转换 (JPG, WebP, AVIF) +- **懒加载**: 视口外图片延迟加载 +- **缓存策略**: 多级缓存提升响应速度 +- **CDN加速**: 静态资源分发优化 + +### 安全特性 +- **用户认证**: JWT令牌机制 +- **权限控制**: 基于角色的访问控制 +- **数据加密**: 敏感数据加密存储 +- **API限流**: 防止接口滥用 + +## 📈 开发进度 + +### 当前状态 + +| 模块 | 设计 | 开发 | 测试 | 部署 | 完成度 | +|------|------|------|------|------|--------| +| 前端展示 | ✅ | ✅ | ✅ | ✅ | 100% | +| 管理后台 | ✅ | ⏳ | ⏳ | ⏳ | 20% | +| 后端API | ✅ | ⏳ | ⏳ | ⏳ | 15% | +| 数据库 | ✅ | ⏳ | ⏳ | ⏳ | 30% | +| 图片处理 | ✅ | ⏳ | ⏳ | ⏳ | 10% | +| 用户认证 | ✅ | ⏳ | ⏳ | ⏳ | 5% | +| 文件存储 | ✅ | ⏳ | ⏳ | ⏳ | 5% | +| 部署配置 | ✅ | ⏳ | ⏳ | ⏳ | 10% | + +### 里程碑计划 + +#### 第一阶段 (2024-01-15 ~ 2024-02-15) +- [x] 完成技术选型和架构设计 +- [x] 完成详细设计文档编写 +- [ ] 搭建开发环境和基础框架 +- [ ] 实现管理后台核心界面 + +#### 第二阶段 (2024-02-15 ~ 2024-03-15) +- [ ] 完成后端API核心功能 +- [ ] 实现数据库表结构和迁移 +- [ ] 完成用户认证和权限系统 +- [ ] 实现图片上传和处理功能 + +#### 第三阶段 (2024-03-15 ~ 2024-04-15) +- [ ] 完成前后端功能对接 +- [ ] 实现文件存储和CDN配置 +- [ ] 完成系统测试和性能优化 +- [ ] 部署到生产环境 + +## 🔧 开发环境 + +### 环境要求 +```yaml +开发环境: + - Node.js: 18+ + - Golang: 1.21+ + - PostgreSQL: 15+ + - Redis: 7+ + - Docker: 24+ + +推荐配置: + - 内存: 8GB+ + - 硬盘: 50GB+ + - 操作系统: macOS/Linux +``` + +### 快速开始 +```bash +# 1. 克隆项目 +git clone +cd photography + +# 2. 前端开发 +cd frontend +make setup +make dev + +# 3. 后端开发 (开发中) +cd backend +make setup +make dev + +# 4. 数据库初始化 +make migrate-up +``` + +## 🧪 测试策略 + +### 测试类型 +- **单元测试**: 核心业务逻辑测试 +- **集成测试**: API接口和数据库交互测试 +- **端到端测试**: 完整用户流程测试 +- **性能测试**: 系统负载和响应时间测试 + +### 测试覆盖率目标 +- **后端代码**: 80%+ +- **前端组件**: 70%+ +- **API接口**: 90%+ + +## 📊 质量保证 + +### 代码质量 +- **代码规范**: ESLint + Prettier (前端), golangci-lint (后端) +- **类型检查**: TypeScript 严格模式 +- **代码审查**: Pull Request必须通过审查 +- **自动化测试**: CI/CD流水线集成 + +### 性能指标 +- **页面加载时间**: < 3s +- **图片处理时间**: < 10s +- **API响应时间**: < 500ms +- **数据库查询**: < 100ms + +## 📋 发布计划 + +### 发布流程 +1. **功能开发**: 在feature分支开发 +2. **代码审查**: 创建Pull Request +3. **测试验证**: 自动化测试通过 +4. **合并主分支**: 合并到main分支 +5. **部署发布**: 自动部署到生产环境 + +### 版本管理 +- **版本号**: 遵循语义化版本规范 +- **发布说明**: 详细的变更日志 +- **回滚策略**: 快速回滚机制 + +## 💡 未来规划 + +### v1.1 增强功能 +- 移动端PWA支持 +- 图片水印功能 +- 批量操作优化 +- 搜索功能增强 + +### v1.2 扩展功能 +- 评论系统 +- 社交分享 +- 数据导出 +- 多语言支持 + +### v2.0 升级计划 +- 微服务架构 +- AI智能标签 +- 实时通知 +- 高级分析 + +--- + +📅 **最后更新**: 2024-01-15 +📝 **文档版本**: v1.0 +👨‍💻 **维护者**: Claude Code Assistant \ No newline at end of file diff --git a/docs/v1/admin/管理后台开发文档.md b/docs/v1/admin/管理后台开发文档.md index 07cc1a4..822b623 100644 --- a/docs/v1/admin/管理后台开发文档.md +++ b/docs/v1/admin/管理后台开发文档.md @@ -278,7 +278,79 @@ type DashboardStats struct { └─────────────────────────────────────────────────────────┘ ``` -### 2.6 系统设置模块 (System Settings) +### 2.6 日志管理模块 (Log Management) + +#### 2.6.1 日志查看页面 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 日志管理 Log Management │ +├─────────────────────────────────────────────────────────────┤ +│ 📊 统计卡片 │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ 总日志 │ │ 错误数 │ │ 警告数 │ │ 信息数 │ │ +│ │ 12,345 │ │ 23 │ │ 156 │ │ 11,234 │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ 🔍 过滤控件 │ +│ [日志级别▼] [搜索关键词...] [Trace ID...] [100条▼] [刷新] │ +│ │ +│ 📋 日志列表 │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 时间 级别 Trace ID 消息 │ │ +│ │ 2024-01-15 10:30 INFO trace-abc123 Photo created │ │ +│ │ 2024-01-15 10:31 WARN trace-def456 Cache miss │ │ +│ │ 2024-01-15 10:32 ERROR trace-abc123 Process failed │ │ +│ │ 2024-01-15 10:33 INFO trace-ghi789 User login │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ 📄 分页: [← 上一页] [1] [2] [3] [下一页 →] │ +│ ⏰ [自动刷新] 🗑️ [清空显示] │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 2.6.2 日志详情展示 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 日志详情 Log Details │ +├─────────────────────────────────────────────────────────────┤ +│ ← 返回列表 │ +│ │ +│ 📋 基本信息 │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 时间: 2024-01-15 10:32:45 │ │ +│ │ 级别: ERROR │ │ +│ │ 消息: Failed to process image │ │ +│ │ Trace ID: trace-abc123 [点击查看相关日志] │ │ +│ │ 用户: admin (user-123) │ │ +│ │ 操作: image_process │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ 🔍 相关日志 (同一Trace ID) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 10:30:00 INFO Photo upload started │ │ +│ │ 10:31:30 INFO File validation passed │ │ +│ │ 10:32:00 WARN Large file detected │ │ +│ │ 10:32:45 ERROR Failed to process image │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ 📊 统计信息 │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 该Trace ID共有 4 条日志 │ │ +│ │ 错误: 1 | 警告: 1 | 信息: 2 │ │ +│ │ 耗时: 2分45秒 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 2.6.3 日志管理功能 +- **实时查看**: 支持自动刷新,实时监控系统状态 +- **智能过滤**: 按级别、关键词、Trace ID、时间范围过滤 +- **链路追踪**: 点击Trace ID查看完整请求链路 +- **统计分析**: 显示各级别日志数量统计 +- **搜索高亮**: 搜索关键词在日志中高亮显示 +- **权限控制**: 仅管理员可访问日志管理功能 + +### 2.7 系统设置模块 (System Settings) #### 2.6.1 基本设置页面 ``` @@ -335,6 +407,7 @@ backend/ │ │ │ ├── tag.go # 标签管理 │ │ │ ├── timeline.go # 时间线管理 │ │ │ ├── settings.go # 系统设置 +│ │ │ ├── logs.go # 日志管理 │ │ │ └── upload.go # 文件上传 │ │ ├── middleware/ # 中间件 │ │ │ ├── auth.go # 认证中间件 @@ -351,6 +424,7 @@ backend/ │ │ ├── tag_service.go # 标签服务 │ │ ├── timeline_service.go # 时间线服务 │ │ ├── upload_service.go # 上传服务 +│ │ ├── logs_service.go # 日志服务 │ │ └── settings_service.go # 设置服务 │ ├── repository/ # 数据访问层 │ │ ├── photo_repo.go # 照片数据访问 @@ -445,6 +519,11 @@ admin/ │ │ │ ├── category-tree.tsx │ │ │ ├── category-form.tsx │ │ │ └── category-stats.tsx +│ │ ├── logs/ # 日志管理组件 +│ │ │ ├── log-viewer.tsx +│ │ │ ├── log-filter.tsx +│ │ │ ├── log-detail.tsx +│ │ │ └── log-stats.tsx │ │ └── common/ # 通用组件 │ │ ├── loading.tsx │ │ ├── error-boundary.tsx @@ -460,22 +539,28 @@ admin/ │ │ │ └── index.tsx │ │ ├── tags/ │ │ │ └── index.tsx +│ │ ├── logs/ +│ │ │ ├── index.tsx +│ │ │ └── detail.tsx │ │ └── settings/ │ │ └── index.tsx │ ├── hooks/ # 自定义Hooks │ │ ├── useAuth.ts │ │ ├── usePhotos.ts │ │ ├── useCategories.ts +│ │ ├── useLogs.ts │ │ └── useUpload.ts │ ├── services/ # API服务 │ │ ├── api.ts │ │ ├── auth.ts │ │ ├── photo.ts │ │ ├── category.ts +│ │ ├── logs.ts │ │ └── upload.ts │ ├── store/ # 状态管理 │ │ ├── auth.ts │ │ ├── photo.ts +│ │ ├── logs.ts │ │ └── ui.ts │ ├── utils/ # 工具函数 │ │ ├── format.ts @@ -484,6 +569,7 @@ admin/ │ └── types/ # 类型定义 │ ├── api.ts │ ├── photo.ts +│ ├── logs.ts │ └── user.ts ├── public/ # 静态资源 ├── package.json diff --git a/docs/v1/backend/Golang项目架构文档.md b/docs/v1/backend/Golang项目架构文档.md index 92b66af..8facec1 100644 --- a/docs/v1/backend/Golang项目架构文档.md +++ b/docs/v1/backend/Golang项目架构文档.md @@ -36,6 +36,8 @@ joho/godotenv // 环境变量 // 日志系统 sirupsen/logrus // 结构化日志 lumberjack.v2 // 日志轮转 +opentracing/opentracing-go // 链路追踪 +jaegertracing/jaeger-client-go // Jaeger客户端 // 验证和工具 go-playground/validator // 数据验证 @@ -64,6 +66,7 @@ photography-backend/ │ ├── config/ # 配置管理 │ ├── database/ # 数据库连接 │ ├── logger/ # 日志系统 +│ ├── tracing/ # 链路追踪 │ ├── middleware/ # 中间件 │ ├── storage/ # 存储服务 │ ├── cache/ # 缓存服务 @@ -127,6 +130,7 @@ package domain import ( "time" "github.com/google/uuid" + "github.com/opentracing/opentracing-go" ) // Photo 照片领域实体 @@ -462,8 +466,19 @@ func NewPhotoService( // GetPhotos 获取照片列表 func (s *photoServiceImpl) GetPhotos(ctx context.Context, req dto.PhotoListRequest) (*dto.PhotoListResponse, error) { + // 创建span用于链路追踪 + span, ctx := opentracing.StartSpanFromContext(ctx, "PhotoService.GetPhotos") + defer span.Finish() + + // 获取trace ID + traceID := s.getTraceID(span) + logger := s.logger.WithField("trace_id", traceID) + // 参数验证 if err := req.Validate(); err != nil { + span.SetTag("error", true) + span.LogKV("event", "validation_error", "error", err.Error()) + logger.WithError(err).Error("Invalid request parameters") return nil, fmt.Errorf("invalid request: %w", err) } @@ -482,7 +497,9 @@ func (s *photoServiceImpl) GetPhotos(ctx context.Context, req dto.PhotoListReque // 从数据库查询 photos, total, err := s.photoRepo.FindWithPagination(ctx, filter) if err != nil { - s.logger.WithError(err).Error("Failed to get photos from repository") + span.SetTag("error", true) + span.LogKV("event", "repository_error", "error", err.Error()) + logger.WithError(err).Error("Failed to get photos from repository") return nil, fmt.Errorf("failed to get photos: %w", err) } @@ -509,13 +526,50 @@ func (s *photoServiceImpl) GetPhotos(ctx context.Context, req dto.PhotoListReque s.cacheService.Set(ctx, cacheKey, data, 10*time.Minute) } + // 记录成功日志 + span.SetTag("photos_count", len(photos)) + span.SetTag("total_count", total) + logger.WithFields(logrus.Fields{ + "photos_count": len(photos), + "total_count": total, + "page": req.Page, + "limit": req.Limit, + }).Info("Photos retrieved successfully") + return response, nil } +// getTraceID 获取trace ID +func (s *photoServiceImpl) getTraceID(span opentracing.Span) string { + if span == nil { + return "" + } + + // 从span context获取trace ID + spanContext := span.Context() + if jaegerSpanContext, ok := spanContext.(interface{ TraceID() string }); ok { + return jaegerSpanContext.TraceID() + } + + // 如果无法获取trace ID,生成一个UUID作为fallback + return uuid.New().String() +} + // CreatePhoto 创建照片 func (s *photoServiceImpl) CreatePhoto(ctx context.Context, req dto.CreatePhotoRequest) (*dto.PhotoResponse, error) { + // 创建span用于链路追踪 + span, ctx := opentracing.StartSpanFromContext(ctx, "PhotoService.CreatePhoto") + defer span.Finish() + + // 获取trace ID + traceID := s.getTraceID(span) + logger := s.logger.WithField("trace_id", traceID) + // 参数验证 if err := req.Validate(); err != nil { + span.SetTag("error", true) + span.LogKV("event", "validation_error", "error", err.Error()) + logger.WithError(err).Error("Invalid request parameters") return nil, fmt.Errorf("invalid request: %w", err) } @@ -549,7 +603,9 @@ func (s *photoServiceImpl) CreatePhoto(ctx context.Context, req dto.CreatePhotoR // 保存照片到数据库 if err := s.photoRepo.Create(ctx, tx, photo); err != nil { - s.logger.WithError(err).Error("Failed to save photo to database") + span.SetTag("error", true) + span.LogKV("event", "database_error", "error", err.Error()) + logger.WithError(err).Error("Failed to save photo to database") return nil, fmt.Errorf("failed to save photo: %w", err) } @@ -590,6 +646,15 @@ func (s *photoServiceImpl) CreatePhoto(ctx context.Context, req dto.CreatePhotoR // 清除相关缓存 s.invalidateCache(ctx, "photos:*", "categories:*", "stats:*") + // 记录成功日志 + span.SetTag("photo_id", photo.ID.Value()) + span.SetTag("photo_title", photo.Title) + logger.WithFields(logrus.Fields{ + "photo_id": photo.ID.Value(), + "photo_title": photo.Title, + "photo_slug": photo.Slug, + }).Info("Photo created successfully") + return dto.NewPhotoResponse(photo), nil } ``` @@ -1267,6 +1332,7 @@ type Config struct { Storage StorageConfig `mapstructure:"storage"` JWT JWTConfig `mapstructure:"jwt"` Logger LoggerConfig `mapstructure:"logger"` + Tracing TracingConfig `mapstructure:"tracing"` Upload UploadConfig `mapstructure:"upload"` Image ImageConfig `mapstructure:"image"` } @@ -1367,6 +1433,18 @@ type LoggerConfig struct { Compress bool `mapstructure:"compress"` } +// TracingConfig 链路追踪配置 +type TracingConfig struct { + Enabled bool `mapstructure:"enabled"` + ServiceName string `mapstructure:"service_name"` + Jaeger struct { + Endpoint string `mapstructure:"endpoint"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + } `mapstructure:"jaeger"` + SamplingRate float64 `mapstructure:"sampling_rate"` +} + // UploadConfig 上传配置 type UploadConfig struct { MaxFileSize int64 `mapstructure:"max_file_size"` @@ -1486,6 +1564,12 @@ func setDefaults() { viper.SetDefault("logger.max_age", 30) viper.SetDefault("logger.compress", true) + // 链路追踪默认配置 + viper.SetDefault("tracing.enabled", true) + viper.SetDefault("tracing.service_name", "photography-backend") + viper.SetDefault("tracing.jaeger.endpoint", "http://localhost:14268/api/traces") + viper.SetDefault("tracing.sampling_rate", 1.0) + // 上传默认配置 viper.SetDefault("upload.max_file_size", 52428800) // 50MB viper.SetDefault("upload.allowed_types", []string{"image/jpeg", "image/png", "image/raw"}) @@ -1812,6 +1896,18 @@ services: networks: - photography-network + # Jaeger链路追踪 + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268:14268" + environment: + - COLLECTOR_OTLP_ENABLED=true + restart: unless-stopped + networks: + - photography-network + # Nginx反向代理(可选) nginx: image: nginx:alpine diff --git a/docs/v1/backend/运维监控方案.md b/docs/v1/backend/运维监控方案.md new file mode 100644 index 0000000..b2991db --- /dev/null +++ b/docs/v1/backend/运维监控方案.md @@ -0,0 +1,1320 @@ +# 摄影作品集网站 - 日志管理方案 + +## 🎯 方案概述 + +这是一个**简单实用的日志管理方案**,专注于日志收集和问题修复。日志查看功能集成到管理后台,提供友好的Web界面。 + +### 设计原则 +- **集成化**: 日志查看功能集成到管理后台 +- **用户友好**: 提供美观易用的Web界面 +- **问题导向**: 专注于快速定位和修复问题 +- **低维护成本**: 几乎零维护的方案 +- **渐进式**: 后续可以根据需要扩展 + +## 📋 核心组件 + +### 日志管理架构 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 后端应用日志 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 错误日志 │ │ 访问日志 │ │ 业务日志 │ │ +│ │ (JSON格式) │ │ (HTTP日志) │ │ (操作日志) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ 管理后台日志模块 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 日志查看器 │ │ 实时监控 │ │ 统计分析 │ │ +│ │ (Web界面) │ │ (自动刷新) │ │ (图表展示) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ API接口层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 日志查询API │ │ 统计API │ │ 搜索API │ │ +│ │(/api/logs) │ │(/api/stats) │ │(/api/search)│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 🔧 技术选择 + +### 日志方案 +- **日志存储**: 本地文件 (JSON格式) +- **日志轮转**: lumberjack.v2 +- **日志查看**: 管理后台Web界面 +- **Trace ID**: 集成OpenTracing (已在架构文档中添加) +- **权限控制**: 基于管理后台的用户权限 + +### 集成方式 +- **前端**: 管理后台的日志管理模块 +- **后端**: Gin路由提供日志查询API +- **认证**: 复用管理后台的登录认证 +- **权限**: 仅管理员可访问日志功能 + +## 📝 日志配置 + +### 1. 应用日志配置 + +#### 日志配置文件 (config.yaml) +```yaml +# config/config.yaml +logger: + level: "info" + format: "json" + output: "file" + filename: "/app/logs/app.log" + max_size: 100 # MB + max_age: 7 # days + compress: true + +# 可选:如果需要链路追踪 +tracing: + enabled: true + service_name: "photography-backend" + jaeger: + endpoint: "http://localhost:14268/api/traces" + sampling_rate: 1.0 +``` + +### 2. 日志格式标准化 + +#### 统一的日志格式 +```json +{ + "timestamp": "2024-01-15T10:30:00Z", + "level": "info", + "message": "Photo created successfully", + "service": "photography-backend", + "trace_id": "abc123def456", + "request_id": "req-789", + "user_id": "user-123", + "operation": "create_photo", + "photo_id": 1001, + "duration": 0.5, + "error": null +} +``` + +### 3. 日志分类 + +#### 三种核心日志类型 +```bash +# 日志目录结构 +logs/ +├── app.log # 应用日志 (所有级别) +├── error.log # 错误日志 (ERROR级别) +└── access.log # HTTP访问日志 +``` + +#### 日志级别使用 +```go +// 日志级别使用指南 +logger.Info("正常业务操作") // 记录重要的业务操作 +logger.Warn("需要关注的情况") // 记录警告信息 +logger.Error("错误情况") // 记录错误信息 +logger.Debug("调试信息") // 开发调试用 +``` + +## 🔍 问题诊断工具 + +### 1. 命令行工具 + +#### 快速查看错误 +```bash +#!/bin/bash +# scripts/check-errors.sh + +echo "📋 最近的错误日志 (最近1小时):" +tail -f /app/logs/app.log | grep -i error | jq -r '.timestamp + " " + .message' + +echo "📊 错误统计:" +grep -i error /app/logs/app.log | jq -r '.message' | sort | uniq -c | sort -nr | head -10 +``` + +#### 按trace ID查找日志 +```bash +#!/bin/bash +# scripts/find-by-trace.sh + +TRACE_ID=$1 +if [ -z "$TRACE_ID" ]; then + echo "用法: ./find-by-trace.sh " + exit 1 +fi + +echo "🔍 查找 trace ID: $TRACE_ID" +grep "$TRACE_ID" /app/logs/app.log | jq -r '.timestamp + " [" + .level + "] " + .message' +``` + +#### 实时监控错误 +```bash +#!/bin/bash +# scripts/monitor-errors.sh + +echo "🚨 实时错误监控 (按Ctrl+C退出):" +tail -f /app/logs/app.log | grep --line-buffered -i error | while read line; do + echo -e "\033[31m[ERROR]\033[0m $(echo "$line" | jq -r '.timestamp + " " + .message')" +done +``` + +### 2. 管理后台日志模块 + +#### 日志查看组件 (React/Vue) +```html + + + + 📋 日志查看器 + + + + +
+
+

📋 日志查看器

+
+ + + + + + + + + + +
+
+ +
📊 正在加载统计...
+ +
+
📱 正在加载日志...
+
+
+ + + + +``` + +#### 管理后台日志API接口 +集成到现有的管理后台后端: + +```go +// internal/api/handlers/admin/logs_handler.go +package admin + +import ( + "bufio" + "encoding/json" + "net/http" + "os" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "photography-backend/pkg/middleware" +) + +// LogEntry 日志条目 +type LogEntry struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + TraceID string `json:"trace_id,omitempty"` + UserID string `json:"user_id,omitempty"` + Operation string `json:"operation,omitempty"` +} + +// LogHandler 日志处理器 +type LogHandler struct { + logFile string +} + +// NewLogHandler 创建日志处理器 +func NewLogHandler(logFile string) *LogHandler { + return &LogHandler{logFile: logFile} +} + +// GetLogs 获取日志列表 +// @Summary 获取系统日志 +// @Description 获取系统日志列表,支持过滤和搜索 +// @Tags 日志管理 +// @Accept json +// @Produce json +// @Param level query string false "日志级别" Enums(error,warn,info,debug) +// @Param search query string false "搜索关键词" +// @Param trace_id query string false "Trace ID" +// @Param lines query int false "返回行数" default(100) +// @Success 200 {object} LogListResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /admin/api/logs [get] +func (h *LogHandler) GetLogs(c *gin.Context) { + // 获取参数 + levelFilter := c.Query("level") + searchFilter := c.Query("search") + traceID := c.Query("trace_id") + lines := 100 + if l := c.Query("lines"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 1000 { + lines = parsed + } + } + + // 读取日志文件 + logs, err := h.readLogs(lines, levelFilter, searchFilter, traceID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "读取日志失败", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{ + "logs": logs, + "total": len(logs), + }, + }) +} + +// GetLogStats 获取日志统计 +// @Summary 获取日志统计信息 +// @Description 获取各级别日志的统计数据 +// @Tags 日志管理 +// @Accept json +// @Produce json +// @Success 200 {object} LogStatsResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /admin/api/logs/stats [get] +func (h *LogHandler) GetLogStats(c *gin.Context) { + stats := map[string]int{ + "total": 0, + "error": 0, + "warn": 0, + "info": 0, + "debug": 0, + } + + file, err := os.Open(h.logFile) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "读取日志文件失败", + "details": err.Error(), + }) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + + stats["total"]++ + + // 解析日志级别 + var entry LogEntry + if err := json.Unmarshal([]byte(line), &entry); err == nil { + if count, exists := stats[entry.Level]; exists { + stats[entry.Level] = count + 1 + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": stats, + }) +} + +// readLogs 读取日志文件 +func (h *LogHandler) readLogs(maxLines int, levelFilter, searchFilter, traceFilter string) ([]LogEntry, error) { + file, err := os.Open(h.logFile) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + // 从后往前读取最新的日志 + start := len(lines) - maxLines*2 // 预读更多行用于过滤 + if start < 0 { + start = 0 + } + + var logs []LogEntry + for i := start; i < len(lines) && len(logs) < maxLines; i++ { + line := lines[i] + if line == "" { + continue + } + + var entry LogEntry + if err := json.Unmarshal([]byte(line), &entry); err != nil { + continue // 跳过无法解析的行 + } + + // 应用过滤条件 + if levelFilter != "" && entry.Level != levelFilter { + continue + } + if searchFilter != "" { + searchLower := strings.ToLower(searchFilter) + if !strings.Contains(strings.ToLower(entry.Message), searchLower) && + !strings.Contains(strings.ToLower(entry.TraceID), searchLower) && + !strings.Contains(strings.ToLower(entry.Operation), searchLower) { + continue + } + } + if traceFilter != "" && !strings.Contains(entry.TraceID, traceFilter) { + continue + } + + logs = append(logs, entry) + } + + // 反转数组,让最新的日志在前面 + for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 { + logs[i], logs[j] = logs[j], logs[i] + } + + return logs, nil +} + +// RegisterLogRoutes 注册日志相关路由 +func RegisterLogRoutes(r *gin.RouterGroup, logFile string) { + logHandler := NewLogHandler(logFile) + + // 需要管理员权限的路由组 + adminGroup := r.Group("") + adminGroup.Use(middleware.RequireAuth()) // 需要登录 + adminGroup.Use(middleware.RequireAdmin()) // 需要管理员权限 + + { + adminGroup.GET("/logs", logHandler.GetLogs) + adminGroup.GET("/logs/stats", logHandler.GetLogStats) + } +} +``` + +#### 管理后台前端集成 +在管理后台中添加日志管理页面: + +```javascript +// admin/src/pages/Logs/LogViewer.jsx +import React, { useState, useEffect } from 'react'; +import { + Card, + Table, + Select, + Input, + Button, + Tag, + Space, + Statistic, + Row, + Col, + message +} from 'antd'; +import { + ReloadOutlined, + SearchOutlined, + ExclamationCircleOutlined, + WarningOutlined, + InfoCircleOutlined, + BugOutlined +} from '@ant-design/icons'; +import { adminApi } from '../../services/api'; + +const { Option } = Select; +const { Search } = Input; + +const LogViewer = () => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({ + level: '', + search: '', + trace_id: '', + lines: 100, + }); + const [stats, setStats] = useState({}); + const [autoRefresh, setAutoRefresh] = useState(false); + + // 日志级别配置 + const levelConfig = { + error: { color: 'red', icon: }, + warn: { color: 'orange', icon: }, + info: { color: 'blue', icon: }, + debug: { color: 'default', icon: }, + }; + + // 获取日志数据 + const fetchLogs = async () => { + setLoading(true); + try { + const response = await adminApi.get('/logs', { params: filters }); + setLogs(response.data.data.logs || []); + } catch (error) { + message.error('获取日志失败'); + } finally { + setLoading(false); + } + }; + + // 获取统计数据 + const fetchStats = async () => { + try { + const response = await adminApi.get('/logs/stats'); + setStats(response.data.data || {}); + } catch (error) { + console.error('获取统计数据失败', error); + } + }; + + useEffect(() => { + fetchLogs(); + fetchStats(); + }, [filters]); + + // 自动刷新 + useEffect(() => { + let interval; + if (autoRefresh) { + interval = setInterval(() => { + fetchLogs(); + fetchStats(); + }, 5000); + } + return () => interval && clearInterval(interval); + }, [autoRefresh, filters]); + + // 表格列配置 + const columns = [ + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + width: 180, + render: (timestamp) => new Date(timestamp).toLocaleString(), + }, + { + title: '级别', + dataIndex: 'level', + key: 'level', + width: 80, + render: (level) => { + const config = levelConfig[level] || levelConfig.info; + return ( + + {level.toUpperCase()} + + ); + }, + }, + { + title: 'Trace ID', + dataIndex: 'trace_id', + key: 'trace_id', + width: 120, + render: (traceId) => traceId ? ( + + ) : '-', + }, + { + title: '消息', + dataIndex: 'message', + key: 'message', + ellipsis: true, + render: (message, record) => { + // 高亮搜索关键词 + if (filters.search && message.toLowerCase().includes(filters.search.toLowerCase())) { + const regex = new RegExp(`(${filters.search})`, 'gi'); + const parts = message.split(regex); + return parts.map((part, index) => + part.toLowerCase() === filters.search.toLowerCase() ? + {part} : part + ); + } + return message; + }, + }, + { + title: '操作', + dataIndex: 'operation', + key: 'operation', + width: 120, + render: (operation) => operation || '-', + }, + ]; + + return ( +
+ {/* 统计卡片 */} + +
+ + + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + {/* 过滤控件 */} + + + + + setFilters(prev => ({ ...prev, search: e.target.value }))} + prefix={} + /> + + setFilters(prev => ({ ...prev, trace_id: e.target.value }))} + /> + + + + + + + + + + {/* 日志表格 */} + +
`${record.timestamp}-${index}`} + pagination={{ + showSizeChanger: false, + showQuickJumper: true, + showTotal: (total) => `共 ${total} 条日志`, + }} + scroll={{ x: 800 }} + /> + + + ); +}; + +export default LogViewer; +``` + +## 🚀 集成到管理后台 + +### 1. 后端集成步骤 + +#### 在main.go中注册日志路由 +```go +// cmd/server/main.go +func main() { + // ... 其他初始化代码 + + // 创建Gin引擎 + r := gin.Default() + + // 注册API路由 + apiGroup := r.Group("/api") + { + // 其他API路由... + } + + // 注册管理后台路由 + adminGroup := r.Group("/admin/api") + { + // 注册日志管理路由 + admin.RegisterLogRoutes(adminGroup, "logs/app.log") + + // 其他管理后台路由... + } + + r.Run(":8080") +} +``` + +#### 添加权限中间件 +```go +// pkg/middleware/auth.go +package middleware + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +// RequireAuth 需要登录认证 +func RequireAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // 检查JWT token或session + token := c.GetHeader("Authorization") + if token == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "未授权访问", + }) + c.Abort() + return + } + + // 验证token并获取用户信息 + // ... token验证逻辑 + + c.Next() + } +} + +// RequireAdmin 需要管理员权限 +func RequireAdmin() gin.HandlerFunc { + return func(c *gin.Context) { + // 检查用户是否是管理员 + userRole := c.GetString("user_role") + if userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{ + "error": "需要管理员权限", + }) + c.Abort() + return + } + + c.Next() + } +} +``` + +### 2. 前端集成步骤 + +#### 在管理后台路由中添加日志管理 +```javascript +// admin/src/router/index.js +import LogViewer from '../pages/Logs/LogViewer.jsx'; + +const routes = [ + // 其他路由... + { + path: '/admin', + component: AdminLayout, + children: [ + { + path: 'logs', + component: LogViewer, + meta: { + title: '日志管理', + requireAuth: true, + requireAdmin: true + } + } + // 其他子路由... + ] + } +]; +``` + +#### 在管理后台菜单中添加日志入口 +```javascript +// admin/src/layout/AdminLayout.jsx +const menuItems = [ + { + key: 'dashboard', + icon: , + label: '仪表盘', + path: '/admin/dashboard' + }, + { + key: 'photos', + icon: , + label: '照片管理', + path: '/admin/photos' + }, + { + key: 'logs', + icon: , + label: '日志管理', + path: '/admin/logs' + }, + // 其他菜单项... +]; +``` + +### 3. 配置更新 + +#### 更新应用配置文件 +```yaml +# config/config.yaml +app: + name: "photography-backend" + version: "1.0.0" + environment: "production" + +logger: + level: "info" + format: "json" + output: "file" + filename: "logs/app.log" + max_size: 100 + max_age: 7 + compress: true + +tracing: + enabled: true + service_name: "photography-backend" + sampling_rate: 1.0 + +admin: + log_access: true # 启用日志访问功能 + log_retention_days: 30 # 日志保留天数 +``` + +### 4. 一键部署脚本 + +#### 部署管理后台日志功能 +```bash +#!/bin/bash +# deploy-admin-logs.sh + +echo "🚀 部署管理后台日志功能..." + +# 创建必要目录 +mkdir -p logs +mkdir -p admin/src/pages/Logs + +# 确保日志文件存在 +touch logs/app.log +chmod 664 logs/app.log + +# 创建日志轮转配置 +cat > /etc/logrotate.d/photography-backend << 'EOF' +/path/to/photography/logs/*.log { + daily + rotate 30 + compress + delaycompress + missingok + notifempty + create 664 app app + postrotate + systemctl reload photography-backend + endscript +} +EOF + +# 设置权限 +chown -R app:app logs/ +chmod 755 logs/ + +echo "✅ 管理后台日志功能部署完成!" +echo "" +echo "📋 访问方式:" +echo " 1. 登录管理后台: http://localhost:8080/admin" +echo " 2. 进入日志管理页面" +echo " 3. 使用管理员账号访问" +echo "" +echo "🔧 API地址:" +echo " 日志列表: GET /admin/api/logs" +echo " 日志统计: GET /admin/api/logs/stats" +``` + +## 🔧 故障排查流程 + +### 1. 问题诊断步骤 +```bash +# 快速诊断脚本 +#!/bin/bash +# scripts/quick-diagnosis.sh + +echo "🔍 快速问题诊断" + +# 1. 检查服务状态 +echo "1. 检查服务状态..." +docker-compose ps + +# 2. 检查最近错误 +echo "2. 最近10条错误日志..." +tail -n 1000 logs/app.log | grep -i error | tail -10 | jq -r '.timestamp + " " + .message' + +# 3. 检查磁盘空间 +echo "3. 检查磁盘空间..." +df -h + +# 4. 检查日志文件大小 +echo "4. 检查日志文件大小..." +ls -lh logs/ + +# 5. 检查内存使用 +echo "5. 检查内存使用..." +docker stats --no-stream photography-backend + +echo "✅ 诊断完成" +``` + +### 2. 常见问题解决 + +#### 问题1:无法找到错误原因 +```bash +# 1. 获取完整的错误上下文 +grep -B 5 -A 5 "error_message" logs/app.log + +# 2. 按时间范围查找 +grep "2024-01-15T10:" logs/app.log | grep -i error +``` + +#### 问题2:需要追踪特定用户的操作 +```bash +# 按用户ID查找 +grep "user-123" logs/app.log | jq -r '.timestamp + " " + .message' + +# 按操作类型查找 +grep "create_photo" logs/app.log | jq -r '.timestamp + " " + .message' +``` + +#### 问题3:日志文件太大 +```bash +# 手动轮转日志 +mv logs/app.log logs/app.log.old +sudo systemctl restart photography-backend + +# 或者使用logrotate +sudo logrotate -f /etc/logrotate.d/photography-backend +``` + +## 🎯 总结 + +这个集成到管理后台的日志方案提供了: + +### ✨ 核心特性 +- **🏢 集成化管理** - 完全集成到管理后台,统一的用户体验 +- **🔐 权限控制** - 基于管理后台的认证和授权体系 +- **📊 专业界面** - 使用Ant Design组件,美观且专业 +- **🔍 强大搜索** - 支持关键词、级别、Trace ID多维度过滤 +- **⏰ 实时监控** - 自动刷新功能,实时观察系统状态 +- **📈 统计分析** - 直观的统计卡片,快速了解系统健康状况 + +### 🚀 部署简单 +1. 运行部署脚本: `./deploy-admin-logs.sh` +2. 访问管理后台: `http://localhost:8080/admin` +3. 登录管理员账号,进入日志管理页面 + +### 💡 使用场景 +- **错误排查**: 管理员快速定位和分析错误日志 +- **性能监控**: 通过Trace ID追踪完整请求链路 +- **运营监控**: 实时观察系统运行状态 +- **历史分析**: 搜索和分析历史日志数据 +- **团队协作**: 多个管理员可以同时查看和分析日志 + +### 🔧 技术优势 +- **集成性**: 完全集成到现有管理后台 +- **安全性**: 基于管理后台的权限控制 +- **专业性**: 使用成熟的UI组件库 +- **可扩展**: 易于添加新的日志分析功能 +- **用户友好**: 直观的界面,无需学习成本 + +### 🔐 权限控制 +- **认证**: 需要管理后台登录 +- **授权**: 仅管理员可访问日志功能 +- **安全**: API接口有完整的权限验证 + +### 📋 API接口 +- `GET /admin/api/logs` - 获取日志列表 +- `GET /admin/api/logs/stats` - 获取日志统计 + +这就是最适合生产环境的日志管理方案 - **专业、安全、易用**! \ No newline at end of file