feat: 完成前后端API联调测试并修复配置问题

- 启动后端go-zero API服务 (端口8080)
- 修复前端API配置中的端口号 (8888→8080)
- 完善前端API状态监控组件
- 创建categoryService服务层
- 更新前端数据查询和转换逻辑
- 完成完整API集成测试,验证所有接口正常工作
- 验证用户认证、分类管理、照片管理等核心功能
- 创建API集成测试脚本
- 更新任务进度文档

测试结果:
 后端健康检查正常
 用户认证功能正常 (admin/admin123)
 分类API正常 (5个分类)
 照片API正常 (0张照片,数据库为空)
 前后端API连接完全正常

下一步: 实现照片展示页面和搜索过滤功能
This commit is contained in:
xujiang
2025-07-11 11:42:14 +08:00
parent b26a05f089
commit af222afc33
20 changed files with 1760 additions and 258 deletions

View File

@ -2,7 +2,9 @@ import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
baseURL: process.env.NEXT_PUBLIC_USE_REAL_API === 'true'
? (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1')
: (process.env.NEXT_PUBLIC_MOCK_API_URL || 'http://localhost:3001/api'),
timeout: 10000,
headers: {
'Content-Type': 'application/json',
@ -27,6 +29,15 @@ api.interceptors.request.use(
// 响应拦截器
api.interceptors.response.use(
(response) => {
// 处理后端API的响应格式: { code: number, message: string, data: any }
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const { code, message, data } = response.data
if (code !== 200) {
return Promise.reject(new Error(message || '请求失败'))
}
return data // 返回data部分
}
// Mock API直接返回数据
return response.data
},
(error) => {

View File

@ -0,0 +1,52 @@
import api from './api'
import { Category } from './queries'
// 分类服务 - 处理分类相关的API调用
class CategoryService {
private categoryCache: Map<number, string> = new Map()
// 获取所有分类
async getAllCategories(): Promise<Category[]> {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const response: any = await api.get('/categories?page=1&page_size=100')
return response?.categories || []
} else {
// Mock API 返回字符串数组,需要转换
const categories: string[] = await api.get('/categories')
return categories.map((name: string, index: number) => ({
id: index + 1,
name,
description: '',
created_at: Date.now() / 1000,
updated_at: Date.now() / 1000
}))
}
}
// 根据分类ID获取分类名称
async getCategoryName(categoryId: number): Promise<string> {
if (this.categoryCache.has(categoryId)) {
return this.categoryCache.get(categoryId)!
}
try {
const categories = await this.getAllCategories()
// 缓存所有分类
categories.forEach(cat => {
this.categoryCache.set(cat.id, cat.name)
})
return this.categoryCache.get(categoryId) || 'unknown'
} catch (error) {
console.error('获取分类名称失败:', error)
return 'unknown'
}
}
// 清除缓存
clearCache() {
this.categoryCache.clear()
}
}
export const categoryService = new CategoryService()

View File

@ -1,12 +1,13 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from './api'
import { categoryService } from './categoryService'
// 照片数据类型
// 照片数据类型 - 统一的显示格式
export interface Photo {
id: number
src: string
title: string
description: string
src: string
category: string
tags: string[]
date: string
@ -16,6 +17,38 @@ export interface Photo {
settings: string
location: string
}
// 后端原始数据 (仅供内部使用)
file_path?: string
thumbnail_path?: string
user_id?: number
category_id?: number
created_at?: number
updated_at?: number
}
// 分类数据类型
export interface Category {
id: number
name: string
description: string
created_at: number
updated_at: number
}
// 后端分页响应格式
export interface PageResponse<T> {
total: number
page: number
size: number
photos?: T[]
categories?: T[]
}
// 后端API基础响应格式
export interface ApiResponse<T> {
code: number
message: string
data: T
}
// 查询键
@ -25,11 +58,77 @@ export const queryKeys = {
categories: ['categories'] as const,
}
// 数据转换工具
const transformPhoto = async (backendPhoto: any): Promise<Photo> => {
// 如果使用Mock API直接返回
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
return {
...backendPhoto,
src: backendPhoto.src || '/placeholder.jpg',
category: backendPhoto.category || 'general',
tags: backendPhoto.tags || [],
date: backendPhoto.date || new Date().toISOString().split('T')[0],
exif: backendPhoto.exif || {
camera: '未知',
lens: '未知',
settings: '未知',
location: '未知'
}
}
}
// 获取分类名称
const categoryName = await categoryService.getCategoryName(backendPhoto.category_id)
// 转换后端API数据格式
return {
id: backendPhoto.id,
title: backendPhoto.title || '无标题',
description: backendPhoto.description || '',
src: backendPhoto.file_path ? `http://localhost:8080${backendPhoto.file_path}` : '/placeholder.jpg',
category: categoryName,
tags: [], // 后端暂无标签系统,使用空数组
date: new Date(backendPhoto.created_at * 1000).toISOString().split('T')[0],
exif: {
camera: '未知',
lens: '未知',
settings: '未知',
location: '未知'
},
// 保留原始数据供内部使用
file_path: backendPhoto.file_path,
thumbnail_path: backendPhoto.thumbnail_path,
user_id: backendPhoto.user_id,
category_id: backendPhoto.category_id,
created_at: backendPhoto.created_at,
updated_at: backendPhoto.updated_at
}
}
const transformCategory = (backendCategory: any): string => {
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
return backendCategory
}
return backendCategory.name
}
// 获取所有照片
export const usePhotos = () => {
return useQuery({
queryKey: queryKeys.photos,
queryFn: (): Promise<Photo[]> => api.get('/photos'),
queryFn: async (): Promise<Photo[]> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
// 使用真实API带分页参数
const response: any = await api.get('/photos?page=1&page_size=100')
const photos = response?.photos || []
// 并发处理所有照片的转换
return Promise.all(photos.map(transformPhoto))
} else {
// 使用Mock API
const photos: any[] = await api.get('/photos')
return Promise.all(photos.map(transformPhoto))
}
},
staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
})
}
@ -38,7 +137,14 @@ export const usePhotos = () => {
export const usePhoto = (id: number) => {
return useQuery({
queryKey: queryKeys.photo(id),
queryFn: (): Promise<Photo> => api.get(`/photos/${id}`),
queryFn: async (): Promise<Photo> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const response = await api.get(`/photos/${id}`)
return await transformPhoto(response)
} else {
return api.get(`/photos/${id}`)
}
},
enabled: !!id,
})
}
@ -47,7 +153,16 @@ export const usePhoto = (id: number) => {
export const useCategories = () => {
return useQuery({
queryKey: queryKeys.categories,
queryFn: (): Promise<string[]> => api.get('/categories'),
queryFn: async (): Promise<string[]> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const response: any = await api.get('/categories?page=1&page_size=100')
const categories = response?.categories || []
return categories.map((cat: Category) => cat.name)
} else {
const categories: string[] = await api.get('/categories')
return categories
}
},
staleTime: 10 * 60 * 1000, // 10分钟内不重新获取
})
}