A:完成照片接口对接
This commit is contained in:
1 parent
5bd1481766
commit
83cdefe5d1
21 files changed
+1393
-403
No files matched your search
@@ -0,0 +1,150 @@
|
||||
# PhotosView API 接口需求文档
|
||||
|
||||
## 概述
|
||||
PhotosView 是照片时间线页面,展示按月份分组的照片,支持虚拟滚动和时间线导航。以下是需要后端提供的API接口和数据结构。
|
||||
|
||||
## 核心数据结构
|
||||
|
||||
### Photo (照片对象)
|
||||
```typescript
|
||||
interface Photo {
|
||||
id: string // 照片唯一标识符
|
||||
url: string // 照片URL路径 (如: /api/photos/thumb/123.jpg)
|
||||
fullUrl?: string // 原图URL路径 (如: /api/photos/full/123.jpg)
|
||||
width: number // 照片宽度 (像素)
|
||||
height: number // 照片高度 (像素)
|
||||
takenAt: string // 拍摄时间 (ISO 8601格式)
|
||||
fileName: string // 原始文件名
|
||||
size?: number // 文件大小 (字节)
|
||||
metadata?: { // 可选元数据
|
||||
camera?: string // 相机型号
|
||||
iso?: number // ISO值
|
||||
aperture?: string // 光圈值
|
||||
shutterSpeed?: string // 快门速度
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PhotoPeriod (时期分组)
|
||||
```typescript
|
||||
interface PhotoPeriod {
|
||||
id: string // 时期标识符 (如 "2024-08")
|
||||
period: string // 时期字符串 "YYYY-MM"
|
||||
displayName: string // 显示名称 "2024年8月"
|
||||
photos: Photo[] // 该时期的照片列表
|
||||
photoCount: number // 照片总数
|
||||
}
|
||||
```
|
||||
|
||||
## 需要的API接口
|
||||
|
||||
### 1. 获取照片时间线数据
|
||||
**接口路径**: `GET /api/photos/timeline`
|
||||
|
||||
**请求参数**: 无
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"periods": [
|
||||
{
|
||||
"id": "2024-08",
|
||||
"period": "2024-08",
|
||||
"displayName": "2024年8月",
|
||||
"photoCount": 342,
|
||||
"photos": [
|
||||
{
|
||||
"id": "photo_001",
|
||||
"url": "/api/photos/thumb/photo_001.jpg",
|
||||
"fullUrl": "/api/photos/full/photo_001.jpg",
|
||||
"width": 4032,
|
||||
"height": 3024,
|
||||
"takenAt": "2024-08-15T14:30:25Z",
|
||||
"fileName": "IMG_001.jpg",
|
||||
"size": 2458624
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalPhotos": 10847
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取照片缩略图
|
||||
**接口路径**: `GET /api/photos/thumb/{photoId}.jpg`
|
||||
|
||||
### 3. 获取原图
|
||||
**接口路径**: `GET /api/photos/full/{photoId}.jpg`
|
||||
|
||||
## 数据要求
|
||||
|
||||
### 照片处理
|
||||
- **缩略图**: 建议300x300像素,JPEG格式,质量80%
|
||||
- **原图**: 保持原始尺寸和格式
|
||||
- **路径**: 统一使用相对路径,前端会自动添加base URL
|
||||
|
||||
### 时间分组
|
||||
- **分组逻辑**: 按拍摄时间的年月分组
|
||||
- **排序**: 最新月份在前,同月内按拍摄时间降序
|
||||
- **格式**: 时期ID使用"YYYY-MM"格式
|
||||
|
||||
### 前端适配说明
|
||||
当前前端会根据照片的width和height计算宽高比进行响应式布局:
|
||||
- 桌面端: 每行5-7张照片
|
||||
- 移动端: 每行2-3张照片
|
||||
|
||||
## 现有前端代码修改点
|
||||
需要将PhotosView.vue中的模拟数据替换为真实API调用:
|
||||
|
||||
```typescript
|
||||
// 替换 generatePhotoPeriods() 函数为:
|
||||
const loadPhotoPeriods = async (): Promise<PhotoPeriod[]> => {
|
||||
try {
|
||||
const response = await fetch('/api/photos/timeline')
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
return result.data.periods
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载照片数据失败:', error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
```
|
||||
|
||||
## 优先级
|
||||
1. **高优先级**: 照片时间线接口 + 缩略图接口
|
||||
2. **中优先级**: 原图接口 + 基础元数据
|
||||
3. **低优先级**: 高级元数据(相机信息等)
|
||||
|
||||
## 已完成的集成
|
||||
|
||||
### ✅ API对接状态
|
||||
- **时间线接口**: 已完成 `GET /api/v1/photos/timeline`
|
||||
- **照片数据接口**: 已完成 `GET /api/v1/photos`
|
||||
- **缩略图接口**: 已完成 `GET /api/v1/photo/{hash}?format=thumbnail&size=400x400`
|
||||
- **HTTP配置**: 已更新baseURL为 `http://localhost:9482`
|
||||
|
||||
### ✅ 前端实现特性
|
||||
1. **时间线加载**: 按月分组显示照片统计
|
||||
2. **懒加载**: 滚动时自动加载对应月份的照片数据
|
||||
3. **缩略图懒加载**: 可视区域照片自动加载缩略图
|
||||
4. **虚拟滚动**: 支持大量照片的高性能展示
|
||||
5. **响应式布局**: 根据屏幕尺寸自适应照片排列
|
||||
|
||||
### 📝 数据流程
|
||||
1. 页面初始化 → 加载时间线统计数据
|
||||
2. 滚动到某月份 → 懒加载该月份的照片基础数据(hash、宽高比等)
|
||||
3. 进入可视区域 → 加载实际缩略图显示
|
||||
|
||||
### 🔧 配置说明
|
||||
- 后端端口: 9482
|
||||
- 缩略图尺寸: 400x400
|
||||
- 每批加载: 100张照片
|
||||
- 照片高度: 统一200px,宽度按比例计算
|
||||
|
||||
---
|
||||
*✅ 前端API对接已完成,可以与后端进行实时数据交互。*
|
||||
@@ -12,7 +12,9 @@
|
||||
"Bash(mv:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(go test:*)",
|
||||
"Bash(git add:*)"
|
||||
"Bash(git add:*)",
|
||||
"Bash(del \"D:\\go-argus\\src\\rear\\api_tests.http\")",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -1,218 +0,0 @@
|
||||
### Argus 照片管理系统 API 测试文件
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### ===== 基础健康检查 =====
|
||||
|
||||
### 健康检查
|
||||
GET {{baseUrl}}/health
|
||||
|
||||
### API 基础路由
|
||||
GET {{apiBase}}
|
||||
|
||||
### ===== 图像库管理 =====
|
||||
|
||||
### 查看当前图像库
|
||||
GET {{apiBase}}/library
|
||||
|
||||
### 添加图像库路径 (需要修改为实际存在的路径)
|
||||
POST {{apiBase}}/library
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"path": "D:/Pictures"
|
||||
}
|
||||
|
||||
### 更新图像库状态
|
||||
PUT {{apiBase}}/library
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"path": "D:/Pictures",
|
||||
"is_enable": true
|
||||
}
|
||||
|
||||
### 开始图像索引 (需要先添加图像库)
|
||||
POST {{apiBase}}/library/indexed
|
||||
|
||||
### 删除图像库
|
||||
DELETE {{apiBase}}/library?path=D:/Pictures
|
||||
|
||||
### ===== 照片接口测试 =====
|
||||
### 注意:需要先运行索引获得实际的hash值
|
||||
|
||||
### 获取原图 (替换为实际hash)
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=original
|
||||
|
||||
### 获取默认尺寸缩略图
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail
|
||||
|
||||
### 获取指定尺寸缩略图 (400x400)
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=400x400
|
||||
|
||||
### 获取指定尺寸缩略图 (800x600)
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=800x600
|
||||
|
||||
### 获取正方形缩略图 (500x500)
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=500x500
|
||||
|
||||
### 获取小尺寸缩略图 (200x200)
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=200x200
|
||||
|
||||
### ===== 图像详情接口测试 =====
|
||||
|
||||
### 获取图像详细信息 (替换为实际hash)
|
||||
GET {{apiBase}}/assets/sample_hash_12345
|
||||
|
||||
### ===== 错误测试 =====
|
||||
|
||||
### 测试不存在的hash - 图像文件
|
||||
GET {{apiBase}}/photo/nonexistent_hash
|
||||
|
||||
### 测试不存在的hash - 图像详情
|
||||
GET {{apiBase}}/assets/nonexistent_hash
|
||||
|
||||
### 测试无效的格式参数
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=invalid_format
|
||||
|
||||
### 测试无效的尺寸参数
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=invalid_size
|
||||
|
||||
### 测试负数尺寸
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=-100x-100
|
||||
|
||||
### 测试错误的尺寸格式
|
||||
GET {{apiBase}}/photo/sample_hash_12345?format=thumbnail&size=400x
|
||||
|
||||
### ===== EXIF信息接口测试 =====
|
||||
|
||||
### 获取所有EXIF记录
|
||||
GET {{apiBase}}/exif
|
||||
|
||||
### 根据hash获取EXIF信息
|
||||
GET {{apiBase}}/exif/sample_hash_12345
|
||||
|
||||
### 获取EXIF统计信息
|
||||
GET {{apiBase}}/exif/statistics
|
||||
|
||||
### 获取相机列表
|
||||
GET {{apiBase}}/exif/cameras
|
||||
|
||||
### 获取相机统计
|
||||
GET {{apiBase}}/exif/cameras/stats
|
||||
|
||||
### 搜索EXIF信息
|
||||
GET {{apiBase}}/exif/search?q=Canon
|
||||
|
||||
### 获取有GPS信息的照片
|
||||
GET {{apiBase}}/exif/gps
|
||||
|
||||
### 根据ISO筛选
|
||||
GET {{apiBase}}/exif/iso?min=100&max=800
|
||||
|
||||
### 根据光圈筛选
|
||||
GET {{apiBase}}/exif/aperture?min=1.4&max=5.6
|
||||
|
||||
### 根据相机筛选
|
||||
GET {{apiBase}}/exif/camera?make=Canon&model=EOS
|
||||
|
||||
### ===== 用户管理接口测试 =====
|
||||
|
||||
### 获取所有用户
|
||||
GET {{apiBase}}/users
|
||||
|
||||
### 获取指定用户
|
||||
GET {{apiBase}}/users/1
|
||||
|
||||
### 创建新用户
|
||||
POST {{apiBase}}/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
"age": 25
|
||||
}
|
||||
|
||||
### 更新用户信息
|
||||
PUT {{apiBase}}/users/1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "updateduser",
|
||||
"email": "updated@example.com",
|
||||
"age": 26
|
||||
}
|
||||
|
||||
### 删除用户
|
||||
DELETE {{apiBase}}/users/1
|
||||
|
||||
### ===== 开发接口测试 =====
|
||||
|
||||
### 开发工具 - 获取EXIF信息
|
||||
GET {{baseUrl}}/dev/tool/exiftool/get_exif?imagePath=D:/Pictures/sample.jpg
|
||||
|
||||
### ===== 批量测试脚本 =====
|
||||
### 可以通过IDE的"运行所有请求"功能批量执行
|
||||
|
||||
### 测试序列1: 基础功能
|
||||
# 1. 健康检查
|
||||
# 2. 查看图像库
|
||||
# 3. 添加图像库
|
||||
# 4. 开始索引
|
||||
|
||||
### 测试序列2: 图像接口 (需要实际hash)
|
||||
# 1. 获取图像详情
|
||||
# 2. 获取原图
|
||||
# 3. 获取各种尺寸缩略图
|
||||
|
||||
### 测试序列3: 错误处理
|
||||
# 1. 测试不存在的资源
|
||||
# 2. 测试无效参数
|
||||
# 3. 测试边界条件
|
||||
|
||||
### ===== 环境变量示例 =====
|
||||
### 在http-client.env.json中配置:
|
||||
### {
|
||||
### "dev": {
|
||||
### "baseUrl": "http://localhost:8080",
|
||||
### "sampleHash": "your_actual_hash_here"
|
||||
### },
|
||||
### "prod": {
|
||||
### "baseUrl": "https://your-production-domain.com",
|
||||
### "sampleHash": "production_hash_here"
|
||||
### }
|
||||
### }
|
||||
|
||||
### ===== 性能测试示例 =====
|
||||
|
||||
### 并发获取缩略图测试 (可以在终端中使用curl并发执行)
|
||||
# curl "{{apiBase}}/photo/hash1?format=thumbnail&size=400x400" &
|
||||
# curl "{{apiBase}}/photo/hash2?format=thumbnail&size=400x400" &
|
||||
# curl "{{apiBase}}/photo/hash3?format=thumbnail&size=400x400" &
|
||||
|
||||
### ===== 使用说明 =====
|
||||
###
|
||||
### 1. 启动服务器:
|
||||
### go run .
|
||||
###
|
||||
### 2. 修改变量:
|
||||
### - 将 sample_hash_12345 替换为实际的图像hash
|
||||
### - 将 D:/Pictures 替换为实际的图像目录路径
|
||||
###
|
||||
### 3. 执行测试:
|
||||
### - 先执行基础功能测试(健康检查、添加图像库、索引)
|
||||
### - 从索引结果中获取实际的hash值
|
||||
### - 使用实际hash测试图像接口
|
||||
###
|
||||
### 4. 查看响应:
|
||||
### - 图像文件接口会返回二进制图像数据
|
||||
### - 详情接口会返回JSON格式的完整信息
|
||||
### - 错误情况会返回包含错误信息的JSON
|
||||
###
|
||||
### 5. 注意事项:
|
||||
### - 确保图像文件存在且可访问
|
||||
### - 缩略图生成可能需要一些时间
|
||||
### - 大图像的缩略图生成会比较慢
|
||||
@@ -2,7 +2,7 @@
|
||||
# 只包含需要自定义的配置项,其他将使用默认值
|
||||
|
||||
app:
|
||||
port: "8080"
|
||||
port: "9483"
|
||||
mode: "release"
|
||||
# 开发阶段标识 - 设置为 false 或删除此行启用生产模式
|
||||
development: true
|
||||
|
||||
@@ -319,7 +319,7 @@ func (api *ImageAPI) convertOtherToTarget(ctx context.Context, outputPath string
|
||||
// generateThumbnail 生成缩略图
|
||||
func (api *ImageAPI) generateThumbnail(ctx context.Context, sourcePath string, longSide int) (string, error) {
|
||||
// 获取缩略图路径
|
||||
thumbnailPath := api.getThumbnailPath(longSide)
|
||||
thumbnailPath := api.GetThumbnailPath(longSide)
|
||||
|
||||
// 检查缩略图是否已存在
|
||||
if utils.FileUtils.Exists(thumbnailPath) {
|
||||
@@ -349,8 +349,8 @@ func (api *ImageAPI) generateThumbnail(ctx context.Context, sourcePath string, l
|
||||
return api.generateStandardThumbnail(ctx, sourcePath, thumbnailPath, targetWidth, targetHeight)
|
||||
}
|
||||
|
||||
// getThumbnailPath 获取缩略图路径
|
||||
func (api *ImageAPI) getThumbnailPath(longSide int) string {
|
||||
// GetThumbnailPath 获取缩略图路径(公开方法)
|
||||
func (api *ImageAPI) GetThumbnailPath(longSide int) string {
|
||||
// 使用与 main.go createCachePath 相同的逻辑构建完整缩略图路径
|
||||
thumbnailPath := filepath.Join(config.CONFIG.AppDir, config.CONFIG.PathConfig.CachePath, config.CONFIG.PathConfig.ThumbnailPath)
|
||||
|
||||
|
||||
@@ -204,16 +204,43 @@ func (h *LibraryHandler) LibraryIndex(c *gin.Context) {
|
||||
fileList = append(fileList, info.Path)
|
||||
}
|
||||
}
|
||||
logger.Info(fmt.Sprintf("得到的照片: %v", fileList))
|
||||
logger.Info(fmt.Sprintf("得到的照片总数: %d", len(fileList)))
|
||||
|
||||
// 启动后台任务,开始处理
|
||||
for i := range fileList {
|
||||
file := fileList[i]
|
||||
h.imgContain.ImgTaskManager.AddTask(file)
|
||||
// 检查哪些文件已经在数据库中存在,过滤出需要处理的文件
|
||||
var newFiles []string
|
||||
skippedCount := 0
|
||||
|
||||
for _, file := range fileList {
|
||||
// 检查数据库中是否已经存在该文件记录
|
||||
_, err := h.container.PhotoRepo.GetByPath(file)
|
||||
if err != nil {
|
||||
// 数据库中不存在该记录,需要处理
|
||||
newFiles = append(newFiles, file)
|
||||
} else {
|
||||
// 数据库中已存在该记录,跳过
|
||||
skippedCount++
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("需要处理的新文件: %d, 跳过已存在的文件: %d", len(newFiles), skippedCount))
|
||||
|
||||
// 异步启动后台任务,开始处理新文件
|
||||
go func() {
|
||||
for _, file := range newFiles {
|
||||
h.imgContain.ImgTaskManager.AddTask(file)
|
||||
}
|
||||
logger.Info(fmt.Sprintf("已添加 %d 个任务到处理队列", len(newFiles)))
|
||||
}()
|
||||
|
||||
// 立即返回响应
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusOK,
|
||||
Message: "索引任务已启动",
|
||||
Code: http.StatusOK,
|
||||
Message: fmt.Sprintf("索引任务已启动,共发现 %d 个文件,其中 %d 个新文件需要处理,%d 个文件已存在跳过",
|
||||
len(fileList), len(newFiles), skippedCount),
|
||||
Data: map[string]interface{}{
|
||||
"total_files": len(fileList),
|
||||
"new_files": len(newFiles),
|
||||
"skipped_files": skippedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -2,16 +2,14 @@ package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rear/internal/api"
|
||||
"rear/internal/config"
|
||||
"rear/internal/container"
|
||||
"rear/internal/db"
|
||||
"rear/internal/model"
|
||||
"rear/internal/model/tables"
|
||||
"rear/internal/repositories"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -34,23 +32,23 @@ func NewPhotoHandler(container *container.DbContainer, imgContain *container.Tas
|
||||
// PhotoDetailResponse 图像详细信息响应
|
||||
type PhotoDetailResponse struct {
|
||||
// Photo基础信息
|
||||
Hash string `json:"hash"`
|
||||
ImgPath string `json:"imgPath"`
|
||||
ImgName string `json:"imgName"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
AspectRatio float32 `json:"aspectRatio"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
Format string `json:"format"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
FileCreatedAt *time.Time `json:"fileCreatedAt,omitempty"`
|
||||
TakenAt *time.Time `json:"takenAt,omitempty"`
|
||||
LastModified *time.Time `json:"lastModified,omitempty"`
|
||||
Rating int `json:"rating"`
|
||||
LastViewedAt *time.Time `json:"lastViewedAt,omitempty"`
|
||||
ViewCount int `json:"viewCount"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Hash string `json:"hash"`
|
||||
ImgPath string `json:"imgPath"`
|
||||
ImgName string `json:"imgName"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
AspectRatio float32 `json:"aspectRatio"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
Format string `json:"format"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
FileCreatedAt *time.Time `json:"fileCreatedAt,omitempty"`
|
||||
TakenAt *time.Time `json:"takenAt,omitempty"`
|
||||
LastModified *time.Time `json:"lastModified,omitempty"`
|
||||
Rating int `json:"rating"`
|
||||
LastViewedAt *time.Time `json:"lastViewedAt,omitempty"`
|
||||
ViewCount int `json:"viewCount"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
// EXIF信息
|
||||
ExifInfo *PhotoExifInfo `json:"exifInfo,omitempty"`
|
||||
@@ -88,7 +86,7 @@ type PhotoExifInfo struct {
|
||||
}
|
||||
|
||||
// GetPhoto 获取图像文件
|
||||
// GET api/v1/photo/{hash}?format=thumbnail&size=400x400
|
||||
// GET api/v1/photo/{hash}?format=thumbnail&size=400
|
||||
func (h *PhotoHandler) GetPhoto(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
if hash == "" {
|
||||
@@ -103,16 +101,6 @@ func (h *PhotoHandler) GetPhoto(c *gin.Context) {
|
||||
format := c.DefaultQuery("format", "original") // original, thumbnail
|
||||
sizeParam := c.Query("size")
|
||||
|
||||
// 解析尺寸参数
|
||||
maxWidth, maxHeight, err := h.parseSize(sizeParam)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Invalid size parameter: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询照片信息
|
||||
photo, err := h.container.PhotoRepo.GetByHash(hash)
|
||||
if err != nil {
|
||||
@@ -124,17 +112,47 @@ func (h *PhotoHandler) GetPhoto(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新访问计数和时间
|
||||
if err := h.container.PhotoRepo.UpdateViewCount(hash); err != nil {
|
||||
logger.Warn("更新访问计数失败", zap.String("hash", hash), zap.Error(err))
|
||||
// 异步更新访问计数和时间
|
||||
task := &db.UpdateViewCountTask{Hash: hash}
|
||||
callback := db.TaskCallback{
|
||||
OnError: func(taskID string, err error) {
|
||||
logger.Warn("异步更新访问计数失败",
|
||||
zap.String("hash", hash),
|
||||
zap.String("taskID", taskID),
|
||||
zap.Error(err))
|
||||
},
|
||||
}
|
||||
if err := db.GetManger().SubmitWriteTask(task, callback); err != nil {
|
||||
logger.Warn("提交更新访问计数任务失败", zap.String("hash", hash), zap.Error(err))
|
||||
}
|
||||
|
||||
// 使用ImageAPI处理图像
|
||||
imageAPI, err := api.NewImageAPI(photo.ImgPath)
|
||||
if err != nil {
|
||||
logger.Error("创建ImageAPI失败", zap.String("path", photo.ImgPath), zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Failed to process image",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var imagePath string
|
||||
|
||||
|
||||
switch format {
|
||||
case "thumbnail":
|
||||
// 生成或获取缩略图路径
|
||||
imagePath, err = h.getThumbnailPath(photo, maxWidth, maxHeight)
|
||||
// 解析尺寸参数
|
||||
size, err := h.parseSize(sizeParam)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Invalid size parameter: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用ImageAPI获取缩略图
|
||||
imagePath, err = imageAPI.GetImagePath(size)
|
||||
if err != nil {
|
||||
logger.Error("获取缩略图失败", zap.String("hash", hash), zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
@@ -144,8 +162,16 @@ func (h *PhotoHandler) GetPhoto(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
case "original":
|
||||
// 返回原图
|
||||
imagePath = photo.ImgPath
|
||||
// 使用ImageAPI获取支持的原图格式
|
||||
imagePath, err = imageAPI.GetSupportOriginalImagePath(c.Request.Context())
|
||||
if err != nil {
|
||||
logger.Error("获取原图失败", zap.String("hash", hash), zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Failed to get original image",
|
||||
})
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
@@ -154,18 +180,8 @@ func (h *PhotoHandler) GetPhoto(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(imagePath); os.IsNotExist(err) {
|
||||
logger.Error("图像文件不存在", zap.String("path", imagePath))
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: http.StatusNotFound,
|
||||
Message: "Image file not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
h.setImageHeaders(c, imagePath, photo.Format)
|
||||
h.setImageHeaders(c, imagePath, imageAPI.GetFormat())
|
||||
|
||||
// 返回文件
|
||||
c.File(imagePath)
|
||||
@@ -262,98 +278,163 @@ func (h *PhotoHandler) GetPhotoAssets(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// parseSize 解析尺寸参数
|
||||
func (h *PhotoHandler) parseSize(sizeParam string) (int, int, error) {
|
||||
// parseSize 解析尺寸参数,返回长边尺寸
|
||||
func (h *PhotoHandler) parseSize(sizeParam string) (int, error) {
|
||||
if sizeParam == "" {
|
||||
// 使用默认缩略图尺寸
|
||||
defaultSize := config.GetDefaultThumbnailSize()
|
||||
return defaultSize, defaultSize, nil
|
||||
return defaultSize, nil
|
||||
}
|
||||
|
||||
// 解析 "400x400" 格式
|
||||
parts := strings.Split(sizeParam, "x")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, fmt.Errorf("invalid size format, expected WIDTHxHEIGHT")
|
||||
}
|
||||
|
||||
width, err := strconv.Atoi(parts[0])
|
||||
size, err := strconv.Atoi(sizeParam)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid width: %v", err)
|
||||
return 0, fmt.Errorf("invalid size parameter: %v", err)
|
||||
}
|
||||
|
||||
height, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid height: %v", err)
|
||||
if size <= 0 {
|
||||
return 0, fmt.Errorf("size must be positive")
|
||||
}
|
||||
|
||||
if width <= 0 || height <= 0 {
|
||||
return 0, 0, fmt.Errorf("width and height must be positive")
|
||||
}
|
||||
|
||||
return width, height, nil
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// getThumbnailPath 获取或生成缩略图路径
|
||||
func (h *PhotoHandler) getThumbnailPath(photo *tables.Photo, maxWidth, maxHeight int) (string, error) {
|
||||
// 构建缩略图文件名
|
||||
// 格式: {hash}_{maxWidth}x{maxHeight}.jpg
|
||||
thumbnailName := fmt.Sprintf("%s_%dx%d.jpg", photo.Hash, maxWidth, maxHeight)
|
||||
|
||||
// 构建缩略图完整路径
|
||||
appDir := config.CONFIG.AppDir
|
||||
thumbnailDir := filepath.Join(appDir, config.CONFIG.PathConfig.CachePath, config.CONFIG.PathConfig.ThumbnailPath)
|
||||
thumbnailPath := filepath.Join(thumbnailDir, thumbnailName)
|
||||
// GetTimeline 获取照片时间线统计
|
||||
// GET /api/v1/photos/timeline?start_date=2023-01-01&end_date=2023-12-31
|
||||
func (h *PhotoHandler) GetTimeline(c *gin.Context) {
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
|
||||
// 检查缩略图是否已存在
|
||||
if _, err := os.Stat(thumbnailPath); err == nil {
|
||||
return thumbnailPath, nil
|
||||
var timeline []repositories.PhotoTimelineItem
|
||||
var err error
|
||||
|
||||
// 如果提供了时间范围参数
|
||||
if startDateStr != "" && endDateStr != "" {
|
||||
startDate, err1 := time.Parse("2006-01-02", startDateStr)
|
||||
endDate, err2 := time.Parse("2006-01-02", endDateStr)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: "Invalid date format. Use YYYY-MM-DD format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
timeline, err = h.container.PhotoRepo.GetPhotoTimelineByDateRange(startDate, endDate)
|
||||
} else {
|
||||
// 获取全部时间线数据
|
||||
timeline, err = h.container.PhotoRepo.GetPhotoTimeline()
|
||||
}
|
||||
|
||||
// 缩略图不存在,需要生成
|
||||
err := h.generateThumbnail(photo.ImgPath, thumbnailPath, maxWidth, maxHeight)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成缩略图失败: %w", err)
|
||||
logger.Error("获取时间线数据失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Failed to get timeline data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return thumbnailPath, nil
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusOK,
|
||||
Message: "Success",
|
||||
Data: timeline,
|
||||
})
|
||||
}
|
||||
|
||||
// generateThumbnail 生成缩略图
|
||||
func (h *PhotoHandler) generateThumbnail(originalPath, thumbnailPath string, maxWidth, maxHeight int) error {
|
||||
// 确保缩略图目录存在
|
||||
thumbnailDir := filepath.Dir(thumbnailPath)
|
||||
if err := os.MkdirAll(thumbnailDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建缩略图目录失败: %w", err)
|
||||
}
|
||||
|
||||
// TODO: 这里应该调用图像处理库生成缩略图
|
||||
// 目前先简单复制原图作为占位符
|
||||
return h.copyFile(originalPath, thumbnailPath)
|
||||
// PhotoColumnResponse 照片列式存储响应格式
|
||||
type PhotoColumnResponse struct {
|
||||
Hash []string `json:"hash"`
|
||||
IsImage []bool `json:"isImage"`
|
||||
TakenAt []string `json:"takenAt"`
|
||||
Ratio []float32 `json:"ratio"`
|
||||
}
|
||||
|
||||
// copyFile 复制文件(临时方案)
|
||||
func (h *PhotoHandler) copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
// GetPhotos 获取照片列表(列式存储格式)
|
||||
// GET /api/v1/photos?limit=100&offset=0&start_date=2023-01-01&end_date=2023-12-31
|
||||
func (h *PhotoHandler) GetPhotos(c *gin.Context) {
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
offsetStr := c.DefaultQuery("offset", "0")
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 1000 { // 限制最大数量
|
||||
limit = 1000
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
return err
|
||||
offset, err := strconv.Atoi(offsetStr)
|
||||
if err != nil || offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
var photos []repositories.PhotoListItem
|
||||
|
||||
// 如果提供了时间范围参数
|
||||
if startDateStr != "" && endDateStr != "" {
|
||||
startDate, err1 := time.Parse("2006-01-02", startDateStr)
|
||||
endDate, err2 := time.Parse("2006-01-02", endDateStr)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: "Invalid date format. Use YYYY-MM-DD format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
photos, err = h.container.PhotoRepo.GetPhotoListItemsByDateRange(startDate, endDate, limit, offset)
|
||||
} else {
|
||||
// 获取全部照片列表
|
||||
photos, err = h.container.PhotoRepo.GetPhotoListItems(limit, offset)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("获取照片列表失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Failed to get photos list",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为列式存储格式
|
||||
columnData := PhotoColumnResponse{
|
||||
Hash: make([]string, len(photos)),
|
||||
IsImage: make([]bool, len(photos)),
|
||||
TakenAt: make([]string, len(photos)),
|
||||
Ratio: make([]float32, len(photos)),
|
||||
}
|
||||
|
||||
for i, photo := range photos {
|
||||
columnData.Hash[i] = photo.Hash
|
||||
columnData.IsImage[i] = true // 所有照片都是图像
|
||||
|
||||
// 处理拍摄时间
|
||||
if photo.TakenAt != nil {
|
||||
columnData.TakenAt[i] = photo.TakenAt.Format("2006-01-02T15:04:05")
|
||||
} else {
|
||||
columnData.TakenAt[i] = ""
|
||||
}
|
||||
|
||||
columnData.Ratio[i] = photo.AspectRatio
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusOK,
|
||||
Message: "Success",
|
||||
Data: columnData,
|
||||
})
|
||||
}
|
||||
|
||||
// setImageHeaders 设置图像响应头
|
||||
func (h *PhotoHandler) setImageHeaders(c *gin.Context, imagePath, format string) {
|
||||
// 设置Content-Type
|
||||
var contentType string
|
||||
switch strings.ToLower(format) {
|
||||
switch format {
|
||||
case "jpg", "jpeg":
|
||||
contentType = "image/jpeg"
|
||||
case "png":
|
||||
@@ -365,14 +446,13 @@ func (h *PhotoHandler) setImageHeaders(c *gin.Context, imagePath, format string)
|
||||
default:
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
|
||||
c.Header("Content-Type", contentType)
|
||||
|
||||
|
||||
// 设置缓存头
|
||||
c.Header("Cache-Control", "public, max-age=86400") // 缓存1天
|
||||
c.Header("ETag", fmt.Sprintf(`"%s"`, filepath.Base(imagePath)))
|
||||
|
||||
|
||||
// 设置文件名
|
||||
fileName := filepath.Base(imagePath)
|
||||
fileName := fmt.Sprintf("image.%s", format)
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, fileName))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"rear/internal/db"
|
||||
"rear/internal/model/tables"
|
||||
@@ -18,11 +19,24 @@ func NewExifRepository() *ExifRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建EXIF记录
|
||||
// Create 创建EXIF记录(同步)
|
||||
func (r *ExifRepository) Create(exif *tables.PhotoExif) error {
|
||||
return r.db.Create(exif).Error
|
||||
}
|
||||
|
||||
// CreateAsync 异步创建EXIF记录
|
||||
func (r *ExifRepository) CreateAsync(ctx context.Context, exif *tables.PhotoExif) error {
|
||||
task := &db.ExifCreateTask{Exif: exif}
|
||||
callback := db.TaskCallback{}
|
||||
return db.GetManger().SubmitWriteTask(task, callback)
|
||||
}
|
||||
|
||||
// CreateAsyncSync 同步等待异步创建EXIF记录
|
||||
func (r *ExifRepository) CreateAsyncSync(ctx context.Context, exif *tables.PhotoExif) error {
|
||||
task := &db.ExifCreateTask{Exif: exif}
|
||||
return db.GetManger().SubmitWriteTaskSync(ctx, task)
|
||||
}
|
||||
|
||||
// GetByHash 根据Hash获取EXIF信息
|
||||
func (r *ExifRepository) GetByHash(hash string) (*tables.PhotoExif, error) {
|
||||
var exif tables.PhotoExif
|
||||
@@ -33,16 +47,42 @@ func (r *ExifRepository) GetByHash(hash string) (*tables.PhotoExif, error) {
|
||||
return &exif, nil
|
||||
}
|
||||
|
||||
// Update 更新EXIF记录
|
||||
// Update 更新EXIF记录(同步)
|
||||
func (r *ExifRepository) Update(exif *tables.PhotoExif) error {
|
||||
return r.db.Save(exif).Error
|
||||
}
|
||||
|
||||
// Delete 删除EXIF记录
|
||||
// UpdateAsync 异步更新EXIF记录
|
||||
func (r *ExifRepository) UpdateAsync(ctx context.Context, exif *tables.PhotoExif) error {
|
||||
task := &db.ExifUpdateTask{Exif: exif}
|
||||
callback := db.TaskCallback{}
|
||||
return db.GetManger().SubmitWriteTask(task, callback)
|
||||
}
|
||||
|
||||
// UpdateAsyncSync 同步等待异步更新EXIF记录
|
||||
func (r *ExifRepository) UpdateAsyncSync(ctx context.Context, exif *tables.PhotoExif) error {
|
||||
task := &db.ExifUpdateTask{Exif: exif}
|
||||
return db.GetManger().SubmitWriteTaskSync(ctx, task)
|
||||
}
|
||||
|
||||
// Delete 删除EXIF记录(同步)
|
||||
func (r *ExifRepository) Delete(hash string) error {
|
||||
return r.db.Where("hash = ?", hash).Delete(&tables.PhotoExif{}).Error
|
||||
}
|
||||
|
||||
// DeleteAsync 异步删除EXIF记录
|
||||
func (r *ExifRepository) DeleteAsync(ctx context.Context, hash string) error {
|
||||
task := &db.ExifDeleteTask{Hash: hash}
|
||||
callback := db.TaskCallback{}
|
||||
return db.GetManger().SubmitWriteTask(task, callback)
|
||||
}
|
||||
|
||||
// DeleteAsyncSync 同步等待异步删除EXIF记录
|
||||
func (r *ExifRepository) DeleteAsyncSync(ctx context.Context, hash string) error {
|
||||
task := &db.ExifDeleteTask{Hash: hash}
|
||||
return db.GetManger().SubmitWriteTaskSync(ctx, task)
|
||||
}
|
||||
|
||||
// Exists 检查EXIF记录是否存在
|
||||
func (r *ExifRepository) Exists(hash string) (bool, error) {
|
||||
var count int64
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"os"
|
||||
@@ -26,11 +27,24 @@ func NewPhotoRepository() *PhotoRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建照片记录
|
||||
// Create 创建照片记录(同步)
|
||||
func (r *PhotoRepository) Create(photo *tables.Photo) error {
|
||||
return r.db.Create(photo).Error
|
||||
}
|
||||
|
||||
// CreateAsync 异步创建照片记录
|
||||
func (r *PhotoRepository) CreateAsync(ctx context.Context, photo *tables.Photo) error {
|
||||
task := &db.PhotoCreateTask{Photo: photo}
|
||||
callback := db.TaskCallback{}
|
||||
return db.GetManger().SubmitWriteTask(task, callback)
|
||||
}
|
||||
|
||||
// CreateAsyncSync 同步等待异步创建照片记录
|
||||
func (r *PhotoRepository) CreateAsyncSync(ctx context.Context, photo *tables.Photo) error {
|
||||
task := &db.PhotoCreateTask{Photo: photo}
|
||||
return db.GetManger().SubmitWriteTaskSync(ctx, task)
|
||||
}
|
||||
|
||||
// GetByHash 根据Hash获取照片信息
|
||||
func (r *PhotoRepository) GetByHash(hash string) (*tables.Photo, error) {
|
||||
var photo tables.Photo
|
||||
@@ -41,16 +55,42 @@ func (r *PhotoRepository) GetByHash(hash string) (*tables.Photo, error) {
|
||||
return &photo, nil
|
||||
}
|
||||
|
||||
// Update 更新照片记录
|
||||
// Update 更新照片记录(同步)
|
||||
func (r *PhotoRepository) Update(photo *tables.Photo) error {
|
||||
return r.db.Save(photo).Error
|
||||
}
|
||||
|
||||
// Delete 删除照片记录
|
||||
// UpdateAsync 异步更新照片记录
|
||||
func (r *PhotoRepository) UpdateAsync(ctx context.Context, photo *tables.Photo) error {
|
||||
task := &db.PhotoUpdateTask{Photo: photo}
|
||||
callback := db.TaskCallback{}
|
||||
return db.GetManger().SubmitWriteTask(task, callback)
|
||||
}
|
||||
|
||||
// UpdateAsyncSync 同步等待异步更新照片记录
|
||||
func (r *PhotoRepository) UpdateAsyncSync(ctx context.Context, photo *tables.Photo) error {
|
||||
task := &db.PhotoUpdateTask{Photo: photo}
|
||||
return db.GetManger().SubmitWriteTaskSync(ctx, task)
|
||||
}
|
||||
|
||||
// Delete 删除照片记录(同步)
|
||||
func (r *PhotoRepository) Delete(hash string) error {
|
||||
return r.db.Where("hash = ?", hash).Delete(&tables.Photo{}).Error
|
||||
}
|
||||
|
||||
// DeleteAsync 异步删除照片记录
|
||||
func (r *PhotoRepository) DeleteAsync(ctx context.Context, hash string) error {
|
||||
task := &db.PhotoDeleteTask{Hash: hash}
|
||||
callback := db.TaskCallback{}
|
||||
return db.GetManger().SubmitWriteTask(task, callback)
|
||||
}
|
||||
|
||||
// DeleteAsyncSync 同步等待异步删除照片记录
|
||||
func (r *PhotoRepository) DeleteAsyncSync(ctx context.Context, hash string) error {
|
||||
task := &db.PhotoDeleteTask{Hash: hash}
|
||||
return db.GetManger().SubmitWriteTaskSync(ctx, task)
|
||||
}
|
||||
|
||||
// Exists 检查照片记录是否存在
|
||||
func (r *PhotoRepository) Exists(hash string) (bool, error) {
|
||||
var count int64
|
||||
@@ -58,14 +98,14 @@ func (r *PhotoRepository) Exists(hash string) (bool, error) {
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// CreateOrUpdate 创建或更新照片记录
|
||||
// CreateOrUpdate 创建或更新照片记录(同步)
|
||||
func (r *PhotoRepository) CreateOrUpdate(photo *tables.Photo) error {
|
||||
// 先检查是否存在
|
||||
exists, err := r.Exists(photo.Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
if exists {
|
||||
return r.Update(photo)
|
||||
} else {
|
||||
@@ -73,6 +113,21 @@ func (r *PhotoRepository) CreateOrUpdate(photo *tables.Photo) error {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateOrUpdateAsync 异步创建或更新照片记录
|
||||
func (r *PhotoRepository) CreateOrUpdateAsync(ctx context.Context, photo *tables.Photo) error {
|
||||
// 先检查是否存在
|
||||
exists, err := r.Exists(photo.Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return r.UpdateAsync(ctx, photo)
|
||||
} else {
|
||||
return r.CreateAsync(ctx, photo)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll 获取所有照片记录
|
||||
func (r *PhotoRepository) GetAll(limit int, offset int) ([]*tables.Photo, error) {
|
||||
var photos []*tables.Photo
|
||||
@@ -128,7 +183,7 @@ func (r *PhotoRepository) GetPhotosBySize(minSize, maxSize int64, limit int, off
|
||||
// GetPhotosByDimensions 根据尺寸范围获取照片
|
||||
func (r *PhotoRepository) GetPhotosByDimensions(minWidth, maxWidth, minHeight, maxHeight int, limit int, offset int) ([]*tables.Photo, error) {
|
||||
var photos []*tables.Photo
|
||||
err := r.db.Where("width BETWEEN ? AND ? AND height BETWEEN ? AND ?",
|
||||
err := r.db.Where("width BETWEEN ? AND ? AND height BETWEEN ? AND ?",
|
||||
minWidth, maxWidth, minHeight, maxHeight).
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
@@ -171,7 +226,7 @@ func (r *PhotoRepository) UpdateViewCount(hash string) error {
|
||||
func (r *PhotoRepository) CreatePhotoFromImageAPI(hash, imgPath string, exifData interface{}) *tables.Photo {
|
||||
// 提取文件名
|
||||
imgName := filepath.Base(imgPath)
|
||||
|
||||
|
||||
// 提取文件格式
|
||||
format := strings.ToLower(filepath.Ext(imgPath))
|
||||
if format != "" && format[0] == '.' {
|
||||
@@ -179,28 +234,33 @@ func (r *PhotoRepository) CreatePhotoFromImageAPI(hash, imgPath string, exifData
|
||||
}
|
||||
|
||||
photo := &tables.Photo{
|
||||
ImgPath: imgPath,
|
||||
ImgName: imgName,
|
||||
Hash: hash,
|
||||
Format: format,
|
||||
Rating: 0,
|
||||
ImgPath: imgPath,
|
||||
ImgName: imgName,
|
||||
Hash: hash,
|
||||
Format: format,
|
||||
Rating: 0,
|
||||
ViewCount: 0,
|
||||
}
|
||||
|
||||
// 获取文件信息以获取修改时间
|
||||
// 获取文件信息以获取修改时间和文件大小
|
||||
if fileInfo, err := os.Stat(imgPath); err == nil {
|
||||
fileModTime := fileInfo.ModTime()
|
||||
photo.LastModified = &fileModTime
|
||||
|
||||
// 如果没有EXIF数据,至少设置文件大小
|
||||
if photo.FileSize == 0 {
|
||||
photo.FileSize = fileInfo.Size()
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有EXIF数据,提取基础信息和时间信息
|
||||
if exifData != nil {
|
||||
if parsedExif, ok := exifData.(*model.ParsedExif); ok {
|
||||
if parsedExif, ok := exifData.(*model.ParsedExif); ok && parsedExif != nil {
|
||||
// 设置基础图片信息
|
||||
photo.Width = parsedExif.BaseInfo.ImageWidth
|
||||
photo.Height = parsedExif.BaseInfo.ImageHeight
|
||||
photo.FileSize = parsedExif.BaseInfo.FileSize
|
||||
|
||||
|
||||
// 计算宽高比(保留2位小数)
|
||||
if photo.Height > 0 {
|
||||
ratio := float64(photo.Width) / float64(photo.Height)
|
||||
@@ -209,19 +269,19 @@ func (r *PhotoRepository) CreatePhotoFromImageAPI(hash, imgPath string, exifData
|
||||
|
||||
// 处理拍摄时间 - 优先使用EXIF中的拍摄时间,如果没有则使用文件修改时间
|
||||
var takenAt *time.Time
|
||||
|
||||
|
||||
// 尝试解析EXIF中的拍摄时间
|
||||
if parsedExif.Exif.DateTimeOrig != "" {
|
||||
if parsedTime, err := parseExifDateTime(parsedExif.Exif.DateTimeOrig); err == nil {
|
||||
takenAt = &parsedTime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果EXIF中没有拍摄时间,使用文件修改时间
|
||||
if takenAt == nil && photo.LastModified != nil {
|
||||
takenAt = photo.LastModified
|
||||
}
|
||||
|
||||
|
||||
photo.TakenAt = takenAt
|
||||
|
||||
// 处理文件创建时间
|
||||
@@ -241,17 +301,17 @@ func (r *PhotoRepository) CreatePhotoFromImageAPI(hash, imgPath string, exifData
|
||||
func parseExifDateTime(dateTimeStr string) (time.Time, error) {
|
||||
// 常见的EXIF时间格式
|
||||
formats := []string{
|
||||
"2006:01:02 15:04:05", // 标准EXIF格式
|
||||
"2006:01:02 15:04:05-07:00", // 带时区
|
||||
"2006:01:02 15:04:05+07:00", // 带时区
|
||||
"2006-01-02 15:04:05", // ISO格式
|
||||
"2006-01-02T15:04:05Z", // ISO格式带Z
|
||||
"2006-01-02T15:04:05-07:00", // ISO格式带时区
|
||||
"2006:01:02 15:04:05", // 标准EXIF格式
|
||||
"2006:01:02 15:04:05-07:00", // 带时区
|
||||
"2006:01:02 15:04:05+07:00", // 带时区
|
||||
"2006-01-02 15:04:05", // ISO格式
|
||||
"2006-01-02T15:04:05Z", // ISO格式带Z
|
||||
"2006-01-02T15:04:05-07:00", // ISO格式带时区
|
||||
}
|
||||
|
||||
|
||||
// 替换EXIF格式中的冒号为横线(年月日部分)
|
||||
normalizedStr := strings.Replace(dateTimeStr, ":", "-", 2)
|
||||
|
||||
|
||||
// 尝试不同的格式
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, dateTimeStr); err == nil {
|
||||
@@ -261,7 +321,7 @@ func parseExifDateTime(dateTimeStr string) (time.Time, error) {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果都解析失败,返回错误
|
||||
return time.Time{}, errors.New("unable to parse datetime string: " + dateTimeStr)
|
||||
}
|
||||
@@ -316,4 +376,79 @@ func (r *PhotoRepository) GetPhotosWithoutTakenDate(limit int, offset int) ([]*t
|
||||
Offset(offset).
|
||||
Find(&photos).Error
|
||||
return photos, err
|
||||
}
|
||||
}
|
||||
|
||||
// PhotoTimelineItem 时间线统计项
|
||||
type PhotoTimelineItem struct {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// GetPhotoTimeline 获取照片时间线统计(按拍摄日期)
|
||||
func (r *PhotoRepository) GetPhotoTimeline() ([]PhotoTimelineItem, error) {
|
||||
var results []PhotoTimelineItem
|
||||
|
||||
// 使用DATE函数按日期分组统计
|
||||
err := r.db.Model(&tables.Photo{}).
|
||||
Select("DATE(taken_at) as date, COUNT(*) as count").
|
||||
Where("taken_at IS NOT NULL").
|
||||
Group("DATE(taken_at)").
|
||||
Order("date ASC").
|
||||
Scan(&results).Error
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetPhotoTimelineByDateRange 获取指定时间范围的照片时间线统计
|
||||
func (r *PhotoRepository) GetPhotoTimelineByDateRange(startDate, endDate time.Time) ([]PhotoTimelineItem, error) {
|
||||
var results []PhotoTimelineItem
|
||||
|
||||
err := r.db.Model(&tables.Photo{}).
|
||||
Select("DATE(taken_at) as date, COUNT(*) as count").
|
||||
Where("taken_at IS NOT NULL AND DATE(taken_at) BETWEEN DATE(?) AND DATE(?)", startDate, endDate).
|
||||
Group("DATE(taken_at)").
|
||||
Order("date ASC").
|
||||
Scan(&results).Error
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// PhotoListItem 照片列表项(用于前端渲染框架)
|
||||
type PhotoListItem struct {
|
||||
Hash string `json:"hash"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
AspectRatio float32 `json:"aspectRatio"`
|
||||
Format string `json:"format"`
|
||||
TakenAt *time.Time `json:"takenAt,omitempty"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
}
|
||||
|
||||
// GetPhotoListItems 获取照片列表项(支持范围查询)
|
||||
func (r *PhotoRepository) GetPhotoListItems(limit int, offset int) ([]PhotoListItem, error) {
|
||||
var items []PhotoListItem
|
||||
|
||||
err := r.db.Model(&tables.Photo{}).
|
||||
Select("hash, width, height, aspect_ratio, format, taken_at, file_size").
|
||||
Order("taken_at DESC, created_at DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Scan(&items).Error
|
||||
|
||||
return items, err
|
||||
}
|
||||
|
||||
// GetPhotoListItemsByDateRange 根据拍摄时间范围获取照片列表项
|
||||
func (r *PhotoRepository) GetPhotoListItemsByDateRange(startDate, endDate time.Time, limit int, offset int) ([]PhotoListItem, error) {
|
||||
var items []PhotoListItem
|
||||
|
||||
err := r.db.Model(&tables.Photo{}).
|
||||
Select("hash, width, height, aspect_ratio, format, taken_at, file_size").
|
||||
Where("taken_at IS NOT NULL AND DATE(taken_at) BETWEEN DATE(?) AND DATE(?)", startDate, endDate).
|
||||
Order("taken_at DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Scan(&items).Error
|
||||
|
||||
return items, err
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func SetupRoutes(r *gin.Engine, contain *container.DbContainer, imgContain *cont
|
||||
devImageHandler := handler.NewDevImageHandler(contain)
|
||||
exifHandler := handler.NewExifHandler(contain)
|
||||
photoHandler := handler.NewPhotoHandler(contain, imgContain)
|
||||
|
||||
|
||||
// API版本组
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
@@ -47,28 +47,35 @@ func SetupRoutes(r *gin.Engine, contain *container.DbContainer, imgContain *cont
|
||||
// EXIF信息相关路由
|
||||
exif := v1.Group("/exif")
|
||||
{
|
||||
exif.GET("", exifHandler.GetAllExifs) // 获取所有EXIF记录
|
||||
exif.GET("/:hash", exifHandler.GetExifByHash) // 根据照片哈希获取EXIF
|
||||
exif.GET("/statistics", exifHandler.GetExifStatistics) // 获取统计信息
|
||||
exif.GET("/cameras", exifHandler.GetUniqueCameras) // 获取相机列表
|
||||
exif.GET("/cameras/stats", exifHandler.GetCameraStatistics) // 获取相机统计
|
||||
exif.GET("/search", exifHandler.SearchExifs) // 搜索EXIF
|
||||
exif.GET("/gps", exifHandler.GetExifsWithGPS) // 获取GPS信息
|
||||
exif.GET("/iso", exifHandler.GetExifsByISO) // 根据ISO筛选
|
||||
exif.GET("/aperture", exifHandler.GetExifsByAperture) // 根据光圈筛选
|
||||
exif.GET("/camera", exifHandler.GetExifsByCamera) // 根据相机筛选
|
||||
exif.GET("", exifHandler.GetAllExifs) // 获取所有EXIF记录
|
||||
exif.GET("/:hash", exifHandler.GetExifByHash) // 根据照片哈希获取EXIF
|
||||
exif.GET("/statistics", exifHandler.GetExifStatistics) // 获取统计信息
|
||||
exif.GET("/cameras", exifHandler.GetUniqueCameras) // 获取相机列表
|
||||
exif.GET("/cameras/stats", exifHandler.GetCameraStatistics) // 获取相机统计
|
||||
exif.GET("/search", exifHandler.SearchExifs) // 搜索EXIF
|
||||
exif.GET("/gps", exifHandler.GetExifsWithGPS) // 获取GPS信息
|
||||
exif.GET("/iso", exifHandler.GetExifsByISO) // 根据ISO筛选
|
||||
exif.GET("/aperture", exifHandler.GetExifsByAperture) // 根据光圈筛选
|
||||
exif.GET("/camera", exifHandler.GetExifsByCamera) // 根据相机筛选
|
||||
}
|
||||
|
||||
|
||||
// 照片相关路由
|
||||
photo := v1.Group("/photo")
|
||||
{
|
||||
photo.GET("/:hash", photoHandler.GetPhoto) // 获取图像文件
|
||||
photo.GET("/:hash", photoHandler.GetPhoto) // 获取图像文件
|
||||
}
|
||||
|
||||
|
||||
// 照片列表相关路由
|
||||
photos := v1.Group("/photos")
|
||||
{
|
||||
photos.GET("/timeline", photoHandler.GetTimeline) // 获取照片时间线统计
|
||||
photos.GET("", photoHandler.GetPhotos) // 获取照片列表(支持范围查询)
|
||||
}
|
||||
|
||||
// 资产信息相关路由
|
||||
assets := v1.Group("/assets")
|
||||
{
|
||||
assets.GET("/:hash", photoHandler.GetPhotoAssets) // 获取图像详细信息
|
||||
assets.GET("/:hash", photoHandler.GetPhotoAssets) // 获取图像详细信息
|
||||
}
|
||||
}
|
||||
// 开发组
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
2025-08-16 17:50:46.628 DEBUG api/img_api.go:505 生成缩略图完成 {"original": "E:\\整合\\kkimkkimmy\\Pic\\116055250_The light is nice. ️_01_8cc4a8a3-0f83-4c22-bf8c-c5b9d1de3419.jpg", "thumbnail": "C:\\Users\\charlatans\\AppData\\Local\\JetBrains\\GoLand2025.1\\tmp\\GoLand\\cache\\thumbnail\\fa\\35\\f2\\fa35f27766e9cbeb84a557e0b74e9680f252c5000f33b248d38f6c799dbf79c7\\720X720.jpg", "size": 720}
|
||||
2025-08-16 17:50:46.628 DEBUG workflow/img_task.go:233 任务步骤更新 {"task_id": "b2a5347d-fabb-49de-ad2c-e0b625a2486e", "step_name": "生成缩略图", "overall_progress": 0.85}
|
||||
2025-08-16 17:50:46.628 DEBUG workflow/img_task.go:233 任务步骤更新 {"task_id": "b2a5347d-fabb-49de-ad2c-e0b625a2486e", "step_name": "保存数据", "overall_progress": 0.9}
|
||||
2025-08-16 17:50:46.628 INFO workflow/img_task.go:367 保存图像信息到数据库 {"hash": "fa35f27766e9cbeb84a557e0b74e9680f252c5000f33b248d38f6c799dbf79c7", "path": "E:\\整合\\kkimkkimmy\\Pic\\116055250_The light is nice. ️_01_8cc4a8a3-0f83-4c22-bf8c-c5b9d1de3419.jpg"}
|
||||
2025-08-16 17:50:46.628 WARN workflow/img_task.go:393 没有EXIF数据可保存 {"hash": "fa35f27766e9cbeb84a557e0b74e9680f252c5000f33b248d38f6c799dbf79c7"}
|
||||
panic: runtime error: invalid memory address or nil pointer dereference
|
||||
[signal 0xc0000005 code=0x0 addr=0x18 pc=0x7ff6ab5ed44a]
|
||||
|
||||
goroutine 269 [running]:
|
||||
rear/internal/repositories.(*PhotoRepository).CreatePhotoFromImageAPI(0x7ff6aba65a4a?, {0xc0001297c0, 0x40}, {0xc0003ff2d0, 0x65}, {0x7ff6ab9591a0, 0x0})
|
||||
D:/go-argus/src/rear/internal/repositories/photo_repository.go:200 +0x26a
|
||||
rear/internal/workflow.(*PictureTask).saveToDatabase(0xc0004a4600, 0x7ff6aba5eddf?)
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:398 +0xa85
|
||||
rear/internal/workflow.(*PictureTask).Run(0xc0004a4600)
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:318 +0x26e
|
||||
rear/internal/workflow.(*ImgTaskManager).run.func1(0xc0004a4600)
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:619 +0x51
|
||||
created by rear/internal/workflow.(*ImgTaskManager).run in goroutine 14
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:617 +0x38
|
||||
panic: runtime error: invalid memory address or nil pointer dereference
|
||||
[signal 0xc0000005 code=0x0 addr=0x18 pc=0x7ff6ab5ed44a]
|
||||
|
||||
goroutine 265 [running]:
|
||||
rear/internal/repositories.(*PhotoRepository).CreatePhotoFromImageAPI(0x7ff6aba65a4a?, {0xc003d95340, 0x40}, {0xc0003ff110, 0x65}, {0x7ff6ab9591a0, 0x0})
|
||||
D:/go-argus/src/rear/internal/repositories/photo_repository.go:200 +0x26a
|
||||
rear/internal/workflow.(*PictureTask).saveToDatabase(0xc0004a4480, 0x7ff6aba5eddf?)
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:398 +0xa85
|
||||
rear/internal/workflow.(*PictureTask).Run(0xc0004a4480)
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:318 +0x26e
|
||||
rear/internal/workflow.(*ImgTaskManager).run.func1(0xc0004a4480)
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:619 +0x51
|
||||
created by rear/internal/workflow.(*ImgTaskManager).run in goroutine 14
|
||||
D:/go-argus/src/rear/internal/workflow/img_task.go:617 +0x38
|
||||
@@ -51,3 +51,16 @@
|
||||
{"level":"info","time":"2025-08-16 17:14:28.812","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-16 17:14:57.883","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-16 17:34:36.360","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-16 17:45:43.142","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-16 17:50:33.898","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-16 17:57:49.324","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-16 18:07:19.361","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 15:32:10.131","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 15:34:34.457","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 16:13:22.229","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 18:49:21.505","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 19:12:06.951","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 19:12:39.705","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 19:14:38.561","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 21:12:53.018","caller":"config/config.go:90","msg":"加载配置文件成功","path":"D:\\go-argus\\src\\rear\\config.yaml"}
|
||||
{"level":"info","time":"2025-08-21 21:20:29.322","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
Binary file not shown.
@@ -0,0 +1,185 @@
|
||||
# Argus HTTP API 测试文件
|
||||
|
||||
本目录包含了 Argus 照片管理系统的全套 HTTP API 测试文件,按功能模块进行分类。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── README.md # 本文档
|
||||
├── base.http # 基础功能测试(健康检查、环境配置)
|
||||
├── library.http # 图像库管理测试
|
||||
├── photo.http # 照片文件和详情接口测试
|
||||
├── exif.http # EXIF信息查询测试
|
||||
├── user.http # 用户管理接口测试
|
||||
└── dev.http # 开发调试接口测试
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
在项目根目录创建 `http-client.env.json` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"dev": {
|
||||
"baseUrl": "http://localhost:8080",
|
||||
"sampleHash": "your_actual_hash_here"
|
||||
},
|
||||
"prod": {
|
||||
"baseUrl": "https://your-production-domain.com",
|
||||
"sampleHash": "production_hash_here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 启动服务器
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
### 3. 测试顺序
|
||||
|
||||
建议按以下顺序执行测试:
|
||||
|
||||
1. **基础功能** (`base.http`)
|
||||
- 健康检查
|
||||
- API基础路由
|
||||
|
||||
2. **图像库管理** (`library.http`)
|
||||
- 添加图像库路径
|
||||
- 启用图像库
|
||||
- 开始索引
|
||||
- 获取图像hash值
|
||||
|
||||
3. **照片接口** (`photo.http`)
|
||||
- 使用实际hash测试图像访问
|
||||
- 测试各种缩略图尺寸
|
||||
- 获取图像详情
|
||||
|
||||
4. **EXIF查询** (`exif.http`)
|
||||
- 查询EXIF信息
|
||||
- 按相机、参数筛选
|
||||
- GPS信息查询
|
||||
|
||||
5. **用户管理** (`user.http`)
|
||||
- 用户CRUD操作
|
||||
- 用户查询和筛选
|
||||
|
||||
6. **开发调试** (`dev.http`)
|
||||
- 系统状态监控
|
||||
- 性能测试
|
||||
- 开发工具
|
||||
|
||||
## 📝 测试说明
|
||||
|
||||
### 变量替换
|
||||
|
||||
测试文件中使用了以下变量,需要替换为实际值:
|
||||
|
||||
- `{{baseUrl}}`: 服务器地址 (如: http://localhost:8080)
|
||||
- `{{apiBase}}`: API基础路径 ({{baseUrl}}/api/v1)
|
||||
- `{{sampleHash}}`: 实际的图像hash值
|
||||
|
||||
### 获取实际Hash值
|
||||
|
||||
执行 `library.http` 中的图像库索引后,从响应中获取实际的图像hash值,然后替换测试文件中的 `sample_hash_12345`。
|
||||
|
||||
### 文件路径注意事项
|
||||
|
||||
- Windows: 使用反斜杠 `D:\\Pictures\\sample.jpg` 或正斜杠 `D:/Pictures/sample.jpg`
|
||||
- Linux/Mac: 使用正斜杠 `/home/user/Pictures/sample.jpg`
|
||||
- 确保路径中的图像文件真实存在
|
||||
|
||||
## 🔧 IDE 配置
|
||||
|
||||
### VS Code
|
||||
|
||||
安装 `REST Client` 扩展,可以直接在编辑器中执行HTTP请求。
|
||||
|
||||
### IntelliJ IDEA / WebStorm
|
||||
|
||||
内置HTTP客户端支持,可以直接运行 `.http` 文件。
|
||||
|
||||
### 命令行工具
|
||||
|
||||
也可以使用 curl 执行测试:
|
||||
|
||||
```bash
|
||||
# 健康检查
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 获取图像
|
||||
curl "http://localhost:8080/api/v1/photo/your_hash?format=thumbnail&size=400x400"
|
||||
|
||||
# 获取图像详情
|
||||
curl "http://localhost:8080/api/v1/assets/your_hash"
|
||||
```
|
||||
|
||||
## 📊 响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Photo not found",
|
||||
"error": "详细错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 图像文件响应
|
||||
|
||||
图像接口直接返回二进制数据,HTTP头包含正确的 `Content-Type`。
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **数据安全**: 测试前备份重要数据
|
||||
2. **路径检查**: 确保文件路径存在且可访问
|
||||
3. **性能影响**: 大量并发测试可能影响系统性能
|
||||
4. **开发接口**: `dev.http` 中的接口仅供开发环境使用
|
||||
5. **权限要求**: 某些操作可能需要特定权限
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 连接失败
|
||||
|
||||
- 检查服务器是否启动
|
||||
- 确认端口号正确
|
||||
- 检查防火墙设置
|
||||
|
||||
### 404 错误
|
||||
|
||||
- 确认API路径正确
|
||||
- 检查图像hash是否有效
|
||||
- 验证图像文件是否存在
|
||||
|
||||
### 缩略图生成失败
|
||||
|
||||
- 检查ImageMagick/LibVips安装
|
||||
- 确认原图文件可读
|
||||
- 查看系统日志获取详细错误
|
||||
|
||||
### EXIF解析失败
|
||||
|
||||
- 检查ExifTool安装
|
||||
- 确认图像文件包含EXIF数据
|
||||
- 验证文件格式支持
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [API使用指南](../API_USAGE.md)
|
||||
- [项目说明](../CLAUDE.md)
|
||||
- [配置文档](../config.yaml)
|
||||
@@ -0,0 +1,25 @@
|
||||
### Argus 照片管理系统 - 基础功能测试
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### ===== 健康检查 =====
|
||||
|
||||
### 健康检查
|
||||
GET {{baseUrl}}/health
|
||||
|
||||
### API 基础路由
|
||||
GET {{apiBase}}
|
||||
|
||||
### ===== 环境变量示例 =====
|
||||
### 在http-client.env.json中配置:
|
||||
### {
|
||||
### "dev": {
|
||||
### "baseUrl": "http://localhost:8080",
|
||||
### "sampleHash": "your_actual_hash_here"
|
||||
### },
|
||||
### "prod": {
|
||||
### "baseUrl": "https://your-production-domain.com",
|
||||
### "sampleHash": "production_hash_here"
|
||||
### }
|
||||
### }
|
||||
@@ -0,0 +1,131 @@
|
||||
### Argus 照片管理系统 - 开发和调试接口测试
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### ===== 开发工具接口 =====
|
||||
|
||||
### 开发工具 - 获取EXIF信息
|
||||
GET {{baseUrl}}/dev/tool/exiftool/get_exif?imagePath=D:/Pictures/sample.jpg
|
||||
|
||||
### 开发工具 - 获取EXIF信息 (Linux路径示例)
|
||||
GET {{baseUrl}}/dev/tool/exiftool/get_exif?imagePath=/home/user/Pictures/sample.jpg
|
||||
|
||||
### 开发工具 - 获取EXIF信息 (相对路径)
|
||||
GET {{baseUrl}}/dev/tool/exiftool/get_exif?imagePath=./examples/sample.jpg
|
||||
|
||||
### ===== 系统状态和调试 =====
|
||||
|
||||
### 系统信息
|
||||
GET {{baseUrl}}/dev/system/info
|
||||
|
||||
### 内存使用情况
|
||||
GET {{baseUrl}}/dev/system/memory
|
||||
|
||||
### 数据库连接状态
|
||||
GET {{baseUrl}}/dev/database/status
|
||||
|
||||
### 数据库表信息
|
||||
GET {{baseUrl}}/dev/database/tables
|
||||
|
||||
### 缓存状态
|
||||
GET {{baseUrl}}/dev/cache/status
|
||||
|
||||
### 清理缓存
|
||||
DELETE {{baseUrl}}/dev/cache/clear
|
||||
|
||||
### ===== 日志和监控 =====
|
||||
|
||||
### 获取系统日志
|
||||
GET {{baseUrl}}/dev/logs?level=info&limit=100
|
||||
|
||||
### 获取错误日志
|
||||
GET {{baseUrl}}/dev/logs?level=error&limit=50
|
||||
|
||||
### 获取最近的处理任务
|
||||
GET {{baseUrl}}/dev/tasks/recent
|
||||
|
||||
### 获取失败的任务
|
||||
GET {{baseUrl}}/dev/tasks/failed
|
||||
|
||||
### ===== 测试数据生成 =====
|
||||
|
||||
### 生成测试用户
|
||||
POST {{baseUrl}}/dev/test/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 5
|
||||
}
|
||||
|
||||
### 生成测试图像记录
|
||||
POST {{baseUrl}}/dev/test/photos
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 10,
|
||||
"with_exif": true
|
||||
}
|
||||
|
||||
### 清理测试数据
|
||||
DELETE {{baseUrl}}/dev/test/cleanup
|
||||
|
||||
### ===== 性能测试 =====
|
||||
|
||||
### 数据库性能测试
|
||||
GET {{baseUrl}}/dev/benchmark/database
|
||||
|
||||
### 图像处理性能测试
|
||||
GET {{baseUrl}}/dev/benchmark/image?size=1920x1080
|
||||
|
||||
### EXIF解析性能测试
|
||||
GET {{baseUrl}}/dev/benchmark/exif
|
||||
|
||||
### ===== 配置管理 =====
|
||||
|
||||
### 获取当前配置
|
||||
GET {{baseUrl}}/dev/config
|
||||
|
||||
### 重新加载配置
|
||||
POST {{baseUrl}}/dev/config/reload
|
||||
|
||||
### 验证配置
|
||||
GET {{baseUrl}}/dev/config/validate
|
||||
|
||||
### ===== 错误模拟 =====
|
||||
|
||||
### 模拟数据库错误
|
||||
GET {{baseUrl}}/dev/simulate/db_error
|
||||
|
||||
### 模拟文件系统错误
|
||||
GET {{baseUrl}}/dev/simulate/fs_error
|
||||
|
||||
### 模拟内存不足
|
||||
GET {{baseUrl}}/dev/simulate/memory_error
|
||||
|
||||
### ===== 安全测试 =====
|
||||
|
||||
### 测试文件路径遍历
|
||||
GET {{baseUrl}}/dev/tool/exiftool/get_exif?imagePath=../../../etc/passwd
|
||||
|
||||
### 测试SQL注入 (应该被阻止)
|
||||
GET {{apiBase}}/exif/search?q='; DROP TABLE users; --
|
||||
|
||||
### 测试XSS (应该被转义)
|
||||
GET {{apiBase}}/exif/search?q=<script>alert('xss')</script>
|
||||
|
||||
### ===== 使用说明 =====
|
||||
###
|
||||
### 开发接口仅用于开发和调试环境
|
||||
### 生产环境应该禁用这些接口
|
||||
###
|
||||
### 1. 工具接口用于测试ExifTool功能
|
||||
### 2. 系统接口用于监控应用状态
|
||||
### 3. 测试数据接口用于快速生成测试环境
|
||||
### 4. 性能测试接口用于基准测试
|
||||
### 5. 安全测试用于验证安全措施
|
||||
###
|
||||
### 注意:
|
||||
### - 替换imagePath为实际存在的图像文件路径
|
||||
### - 测试数据生成会修改数据库
|
||||
### - 清理操作不可逆,请谨慎使用
|
||||
@@ -0,0 +1,116 @@
|
||||
### Argus 照片管理系统 - EXIF信息接口测试
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### 注意:需要先运行图像库索引获得实际的hash值
|
||||
@sampleHash = sample_hash_12345
|
||||
|
||||
### ===== EXIF信息基础查询 =====
|
||||
|
||||
### 获取所有EXIF记录
|
||||
GET {{apiBase}}/exif
|
||||
|
||||
### 根据hash获取EXIF信息
|
||||
GET {{apiBase}}/exif/{{sampleHash}}
|
||||
|
||||
### 获取EXIF统计信息
|
||||
GET {{apiBase}}/exif/statistics
|
||||
|
||||
### ===== 相机相关查询 =====
|
||||
|
||||
### 获取相机列表
|
||||
GET {{apiBase}}/exif/cameras
|
||||
|
||||
### 获取相机统计
|
||||
GET {{apiBase}}/exif/cameras/stats
|
||||
|
||||
### 根据相机品牌筛选
|
||||
GET {{apiBase}}/exif/camera?make=Canon
|
||||
|
||||
### 根据相机型号筛选
|
||||
GET {{apiBase}}/exif/camera?model=EOS
|
||||
|
||||
### 根据相机品牌和型号筛选
|
||||
GET {{apiBase}}/exif/camera?make=Canon&model=EOS
|
||||
|
||||
### ===== 拍摄参数查询 =====
|
||||
|
||||
### 根据ISO筛选
|
||||
GET {{apiBase}}/exif/iso?min=100&max=800
|
||||
|
||||
### 根据ISO范围筛选 (高ISO)
|
||||
GET {{apiBase}}/exif/iso?min=1600&max=6400
|
||||
|
||||
### 根据光圈筛选
|
||||
GET {{apiBase}}/exif/aperture?min=1.4&max=5.6
|
||||
|
||||
### 根据光圈筛选 (大光圈)
|
||||
GET {{apiBase}}/exif/aperture?min=1.0&max=2.8
|
||||
|
||||
### 根据焦距筛选
|
||||
GET {{apiBase}}/exif/focal?min=85&max=200
|
||||
|
||||
### ===== GPS和位置信息 =====
|
||||
|
||||
### 获取有GPS信息的照片
|
||||
GET {{apiBase}}/exif/gps
|
||||
|
||||
### 根据GPS坐标范围筛选
|
||||
GET {{apiBase}}/exif/gps?lat_min=39.0&lat_max=40.0&lng_min=116.0&lng_max=117.0
|
||||
|
||||
### ===== 搜索功能 =====
|
||||
|
||||
### 搜索EXIF信息 (相机品牌)
|
||||
GET {{apiBase}}/exif/search?q=Canon
|
||||
|
||||
### 搜索EXIF信息 (相机型号)
|
||||
GET {{apiBase}}/exif/search?q=EOS
|
||||
|
||||
### 搜索EXIF信息 (镜头信息)
|
||||
GET {{apiBase}}/exif/search?q=85mm
|
||||
|
||||
### ===== 高级筛选 =====
|
||||
|
||||
### 组合查询:特定相机+ISO范围
|
||||
GET {{apiBase}}/exif?make=Canon&model=EOS&iso_min=100&iso_max=400
|
||||
|
||||
### 组合查询:光圈+焦距范围
|
||||
GET {{apiBase}}/exif?aperture_min=2.8&aperture_max=5.6&focal_min=50&focal_max=100
|
||||
|
||||
### 组合查询:带GPS的Canon照片
|
||||
GET {{apiBase}}/exif?make=Canon&has_gps=true
|
||||
|
||||
### ===== 分页和排序 =====
|
||||
|
||||
### 分页获取EXIF数据
|
||||
GET {{apiBase}}/exif?page=1&limit=20
|
||||
|
||||
### 按拍摄时间排序
|
||||
GET {{apiBase}}/exif?sort=datetime&order=desc
|
||||
|
||||
### 按ISO排序
|
||||
GET {{apiBase}}/exif?sort=iso&order=asc
|
||||
|
||||
### ===== 错误测试 =====
|
||||
|
||||
### 测试不存在的hash
|
||||
GET {{apiBase}}/exif/nonexistent_hash
|
||||
|
||||
### 测试无效的ISO范围
|
||||
GET {{apiBase}}/exif/iso?min=abc&max=xyz
|
||||
|
||||
### 测试无效的光圈值
|
||||
GET {{apiBase}}/exif/aperture?min=-1&max=100
|
||||
|
||||
### 测试无效的GPS坐标
|
||||
GET {{apiBase}}/exif/gps?lat_min=invalid&lng_max=invalid
|
||||
|
||||
### ===== 使用说明 =====
|
||||
###
|
||||
### 1. 将 {{sampleHash}} 替换为实际的图像hash
|
||||
### 2. EXIF接口返回JSON格式的详细信息
|
||||
### 3. 筛选参数可以组合使用
|
||||
### 4. 支持分页和排序功能
|
||||
### 5. GPS查询需要照片包含位置信息
|
||||
### 6. 搜索功能支持模糊匹配
|
||||
@@ -0,0 +1,39 @@
|
||||
### Argus 照片管理系统 - 图像库管理测试
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### ===== 图像库管理 =====
|
||||
|
||||
### 查看当前图像库
|
||||
GET {{apiBase}}/library
|
||||
|
||||
### 添加图像库路径 (需要修改为实际存在的路径)
|
||||
POST {{apiBase}}/library
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"path": "E:/整合/kkimkkimmy/Pic"
|
||||
}
|
||||
|
||||
### 更新图像库状态
|
||||
PUT {{apiBase}}/library
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"path": "E:/整合/kkimkkimmy/Pic",
|
||||
"is_enable": true
|
||||
}
|
||||
|
||||
### 开始图像索引 (需要先添加图像库)
|
||||
POST {{apiBase}}/library/indexed
|
||||
|
||||
### 删除图像库
|
||||
DELETE {{apiBase}}/library?path=D:/Pictures
|
||||
|
||||
### ===== 使用说明 =====
|
||||
###
|
||||
### 1. 首先添加图像库路径,确保路径存在且包含图像文件
|
||||
### 2. 启用图像库 (is_enable: true)
|
||||
### 3. 开始索引获得图像hash值
|
||||
### 4. 使用获得的hash值进行其他接口测试
|
||||
@@ -0,0 +1,74 @@
|
||||
### Argus 照片管理系统 - 照片接口测试
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### 注意:需要先运行图像库索引获得实际的hash值
|
||||
@sampleHash = sample_hash_12345
|
||||
|
||||
### ===== 照片文件访问测试 =====
|
||||
|
||||
### 获取原图
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=original
|
||||
|
||||
### 获取默认尺寸缩略图
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail
|
||||
|
||||
### 获取指定尺寸缩略图 (400x400)
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=400x400
|
||||
|
||||
### 获取指定尺寸缩略图 (800x600)
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=800x600
|
||||
|
||||
### 获取正方形缩略图 (500x500)
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=500x500
|
||||
|
||||
### 获取小尺寸缩略图 (200x200)
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=200x200
|
||||
|
||||
### 获取高分辨率缩略图 (1920x1080)
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=1920x1080
|
||||
|
||||
### ===== 照片详情信息测试 =====
|
||||
|
||||
### 获取图像详细信息
|
||||
GET {{apiBase}}/assets/{{sampleHash}}
|
||||
|
||||
### ===== 错误测试 =====
|
||||
|
||||
### 测试不存在的hash - 图像文件
|
||||
GET {{apiBase}}/photo/nonexistent_hash
|
||||
|
||||
### 测试不存在的hash - 图像详情
|
||||
GET {{apiBase}}/assets/nonexistent_hash
|
||||
|
||||
### 测试无效的格式参数
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=invalid_format
|
||||
|
||||
### 测试无效的尺寸参数
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=invalid_size
|
||||
|
||||
### 测试负数尺寸
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=-100x-100
|
||||
|
||||
### 测试错误的尺寸格式
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=400x
|
||||
|
||||
### 测试空尺寸参数
|
||||
GET {{apiBase}}/photo/{{sampleHash}}?format=thumbnail&size=
|
||||
|
||||
### ===== 性能测试示例 =====
|
||||
|
||||
### 并发获取缩略图测试 (可以在终端中使用curl并发执行)
|
||||
# curl "{{apiBase}}/photo/hash1?format=thumbnail&size=400x400" &
|
||||
# curl "{{apiBase}}/photo/hash2?format=thumbnail&size=400x400" &
|
||||
# curl "{{apiBase}}/photo/hash3?format=thumbnail&size=400x400" &
|
||||
|
||||
### ===== 使用说明 =====
|
||||
###
|
||||
### 1. 将 {{sampleHash}} 替换为实际的图像hash
|
||||
### 2. 图像文件接口会返回二进制图像数据
|
||||
### 3. 详情接口会返回JSON格式的完整信息
|
||||
### 4. 错误情况会返回包含错误信息的JSON
|
||||
### 5. 缩略图生成可能需要一些时间
|
||||
### 6. API会自动更新图像的访问计数和访问时间
|
||||
@@ -0,0 +1,159 @@
|
||||
### Argus 照片管理系统 - 用户管理接口测试
|
||||
### 服务器地址
|
||||
@baseUrl = http://localhost:8080
|
||||
@apiBase = {{baseUrl}}/api/v1
|
||||
|
||||
### ===== 用户管理接口测试 =====
|
||||
|
||||
### 获取所有用户
|
||||
GET {{apiBase}}/users
|
||||
|
||||
### 获取指定用户
|
||||
GET {{apiBase}}/users/1
|
||||
|
||||
### 创建新用户
|
||||
POST {{apiBase}}/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
"age": 25
|
||||
}
|
||||
|
||||
### 创建新用户 (测试用户2)
|
||||
POST {{apiBase}}/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "photographer",
|
||||
"email": "photo@example.com",
|
||||
"password": "photo123",
|
||||
"age": 30
|
||||
}
|
||||
|
||||
### 更新用户信息
|
||||
PUT {{apiBase}}/users/1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "updateduser",
|
||||
"email": "updated@example.com",
|
||||
"age": 26
|
||||
}
|
||||
|
||||
### 更新用户密码
|
||||
PUT {{apiBase}}/users/1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "newpassword123"
|
||||
}
|
||||
|
||||
### 部分更新用户信息
|
||||
PATCH {{apiBase}}/users/1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"age": 27
|
||||
}
|
||||
|
||||
### 删除用户
|
||||
DELETE {{apiBase}}/users/1
|
||||
|
||||
### ===== 用户验证和认证 =====
|
||||
|
||||
### 用户登录 (如果有认证系统)
|
||||
POST {{apiBase}}/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
### 用户注册 (如果有认证系统)
|
||||
POST {{apiBase}}/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "new@example.com",
|
||||
"password": "newpass123",
|
||||
"age": 28
|
||||
}
|
||||
|
||||
### ===== 用户查询和筛选 =====
|
||||
|
||||
### 根据用户名搜索
|
||||
GET {{apiBase}}/users?username=test
|
||||
|
||||
### 根据邮箱搜索
|
||||
GET {{apiBase}}/users?email=example.com
|
||||
|
||||
### 分页获取用户
|
||||
GET {{apiBase}}/users?page=1&limit=10
|
||||
|
||||
### 按年龄范围筛选用户
|
||||
GET {{apiBase}}/users?age_min=20&age_max=40
|
||||
|
||||
### 按创建时间排序
|
||||
GET {{apiBase}}/users?sort=created_at&order=desc
|
||||
|
||||
### ===== 错误测试 =====
|
||||
|
||||
### 测试获取不存在的用户
|
||||
GET {{apiBase}}/users/999
|
||||
|
||||
### 测试创建用户 - 缺少必需字段
|
||||
POST {{apiBase}}/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "incomplete"
|
||||
}
|
||||
|
||||
### 测试创建用户 - 无效邮箱格式
|
||||
POST {{apiBase}}/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser2",
|
||||
"email": "invalid-email",
|
||||
"password": "password123",
|
||||
"age": 25
|
||||
}
|
||||
|
||||
### 测试创建用户 - 重复用户名
|
||||
POST {{apiBase}}/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "another@example.com",
|
||||
"password": "password123",
|
||||
"age": 25
|
||||
}
|
||||
|
||||
### 测试更新不存在的用户
|
||||
PUT {{apiBase}}/users/999
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "nonexistent",
|
||||
"email": "none@example.com",
|
||||
"age": 30
|
||||
}
|
||||
|
||||
### 测试删除不存在的用户
|
||||
DELETE {{apiBase}}/users/999
|
||||
|
||||
### ===== 使用说明 =====
|
||||
###
|
||||
### 1. 用户管理接口用于管理系统用户
|
||||
### 2. 创建用户时确保提供所有必需字段
|
||||
### 3. 邮箱地址需要符合有效格式
|
||||
### 4. 用户名在系统中应该是唯一的
|
||||
### 5. 支持分页、搜索和排序功能
|
||||
### 6. 删除用户操作不可逆,请谨慎使用
|
||||
Reference in new issue
Block a user