fix
This commit is contained in:
624
.cursor/rules/admin/react-vite.mdc
Normal file
624
.cursor/rules/admin/react-vite.mdc
Normal file
@ -0,0 +1,624 @@
|
||||
# 管理后台 React + Vite 开发规则
|
||||
|
||||
## 🏗️ 项目架构
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
admin/
|
||||
├── src/
|
||||
│ ├── components/ # React 组件
|
||||
│ │ ├── ui/ # shadcn/ui 基础组件
|
||||
│ │ ├── DashboardLayout.tsx # 布局组件
|
||||
│ │ ├── ProtectedRoute.tsx # 路由守卫
|
||||
│ │ └── ErrorBoundary.tsx # 错误边界
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Dashboard.tsx # 仪表盘
|
||||
│ │ ├── Photos.tsx # 照片管理
|
||||
│ │ ├── Categories.tsx # 分类管理
|
||||
│ │ ├── Users.tsx # 用户管理
|
||||
│ │ └── LoginPage.tsx # 登录页
|
||||
│ ├── services/ # API 服务
|
||||
│ │ ├── api.ts # API 基础配置
|
||||
│ │ ├── authService.ts # 认证服务
|
||||
│ │ ├── photoService.ts # 照片服务
|
||||
│ │ └── categoryService.ts # 分类服务
|
||||
│ ├── stores/ # 状态管理
|
||||
│ │ └── authStore.ts # 认证状态
|
||||
│ ├── types/ # TypeScript 类型
|
||||
│ │ └── index.ts # 类型定义
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── lib/ # 库配置
|
||||
│ │ └── utils.ts # shadcn utils
|
||||
│ ├── App.tsx # 应用根组件
|
||||
│ └── main.tsx # 应用入口
|
||||
├── public/ # 静态资源
|
||||
├── index.html # HTML 模板
|
||||
├── vite.config.ts # Vite 配置
|
||||
└── package.json # 依赖配置
|
||||
```
|
||||
|
||||
## 🎯 开发规范
|
||||
|
||||
### 应用入口配置
|
||||
```tsx
|
||||
// main.tsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5分钟
|
||||
retry: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
)
|
||||
```
|
||||
|
||||
### 应用根组件
|
||||
```tsx
|
||||
// App.tsx
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||
import { DashboardLayout } from '@/components/DashboardLayout'
|
||||
import { LoginPage } from '@/pages/LoginPage'
|
||||
import { Dashboard } from '@/pages/Dashboard'
|
||||
import { Photos } from '@/pages/Photos'
|
||||
import { PhotoUpload } from '@/pages/PhotoUpload'
|
||||
import { Categories } from '@/pages/Categories'
|
||||
import { Users } from '@/pages/Users'
|
||||
import { Settings } from '@/pages/Settings'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/photos" element={<Photos />} />
|
||||
<Route path="/photos/upload" element={<PhotoUpload />} />
|
||||
<Route path="/categories" element={<Categories />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
## 🧩 核心组件
|
||||
|
||||
### 布局组件
|
||||
```tsx
|
||||
// components/DashboardLayout.tsx
|
||||
import { useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Image,
|
||||
FolderOpen,
|
||||
Users,
|
||||
Settings,
|
||||
LogOut,
|
||||
Menu
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const navigation = [
|
||||
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: '照片管理', href: '/photos', icon: Image },
|
||||
{ name: '分类管理', href: '/categories', icon: FolderOpen },
|
||||
{ name: '用户管理', href: '/users', icon: Users },
|
||||
{ name: '系统设置', href: '/settings', icon: Settings },
|
||||
]
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const location = useLocation()
|
||||
const { user, logout } = useAuthStore()
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
{/* 侧边栏 */}
|
||||
<div className={`
|
||||
bg-white shadow-lg transition-all duration-300
|
||||
${sidebarOpen ? 'w-64' : 'w-16'}
|
||||
`}>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<h1 className={`font-bold text-xl ${!sidebarOpen && 'hidden'}`}>
|
||||
摄影管理
|
||||
</h1>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<nav className="mt-8">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
className={`
|
||||
flex items-center px-4 py-3 text-sm font-medium
|
||||
${location.pathname === item.href
|
||||
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<item.icon className="h-5 w-5 mr-3" />
|
||||
{sidebarOpen && item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={logout}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
{sidebarOpen && '退出登录'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* 顶部导航 */}
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{navigation.find(item => item.href === location.pathname)?.name || '管理后台'}
|
||||
</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
欢迎, {user?.username}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 页面内容 */}
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 路由守卫
|
||||
```tsx
|
||||
// components/ProtectedRoute.tsx
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { Loading } from '@/components/Loading'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { token, isLoading } = useAuthStore()
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
```
|
||||
|
||||
## 🎛️ 状态管理 (Zustand)
|
||||
|
||||
### 认证状态管理
|
||||
```typescript
|
||||
// stores/authStore.ts
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { authService } from '@/services/authService'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
role: string
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: User | null
|
||||
token: string | null
|
||||
isLoading: boolean
|
||||
|
||||
// Actions
|
||||
login: (credentials: LoginCredentials) => Promise<void>
|
||||
logout: () => void
|
||||
checkAuth: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
user: null,
|
||||
token: null,
|
||||
isLoading: false,
|
||||
|
||||
login: async (credentials) => {
|
||||
set({ isLoading: true })
|
||||
try {
|
||||
const response = await authService.login(credentials)
|
||||
set({
|
||||
user: response.user,
|
||||
token: response.token,
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
set({ isLoading: false })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
set({ user: null, token: null })
|
||||
// 清除持久化数据
|
||||
localStorage.removeItem('auth-storage')
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
const { token } = get()
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
const user = await authService.getCurrentUser()
|
||||
set({ user })
|
||||
} catch (error) {
|
||||
// Token 无效,清除状态
|
||||
get().logout()
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({
|
||||
token: state.token,
|
||||
user: state.user
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## 🌐 API 服务层
|
||||
|
||||
### API 基础配置
|
||||
```typescript
|
||||
// services/api.ts
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8888/api/v1',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().token
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
useAuthStore.getState().logout()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// API 响应类型
|
||||
export interface ApiResponse<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
```
|
||||
|
||||
### 照片服务
|
||||
```typescript
|
||||
// services/photoService.ts
|
||||
import { api, ApiResponse } from './api'
|
||||
|
||||
export interface Photo {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
filename: string
|
||||
thumbnail: string
|
||||
category_id?: string
|
||||
user_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PhotoListResponse {
|
||||
photos: Photo[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export const photoService = {
|
||||
// 获取照片列表
|
||||
getPhotos: async (params: {
|
||||
page?: number
|
||||
limit?: number
|
||||
category_id?: string
|
||||
keyword?: string
|
||||
}): Promise<PhotoListResponse> => {
|
||||
const { data } = await api.get<ApiResponse<PhotoListResponse>>('/photos', {
|
||||
params,
|
||||
})
|
||||
return data.data
|
||||
},
|
||||
|
||||
// 上传照片
|
||||
uploadPhoto: async (formData: FormData): Promise<Photo> => {
|
||||
const { data } = await api.post<ApiResponse<Photo>>('/photos', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
return data.data
|
||||
},
|
||||
|
||||
// 更新照片
|
||||
updatePhoto: async (id: string, updates: Partial<Photo>): Promise<void> => {
|
||||
await api.put(`/photos/${id}`, updates)
|
||||
},
|
||||
|
||||
// 删除照片
|
||||
deletePhoto: async (id: string): Promise<void> => {
|
||||
await api.delete(`/photos/${id}`)
|
||||
},
|
||||
|
||||
// 获取照片详情
|
||||
getPhoto: async (id: string): Promise<Photo> => {
|
||||
const { data } = await api.get<ApiResponse<Photo>>(`/photos/${id}`)
|
||||
return data.data
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 页面组件
|
||||
|
||||
### 照片管理页面
|
||||
```tsx
|
||||
// pages/Photos.tsx
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Edit, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { photoService } from '@/services/photoService'
|
||||
|
||||
export function Photos() {
|
||||
const [page, setPage] = useState(1)
|
||||
const { toast } = useToast()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// 获取照片列表
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['photos', page],
|
||||
queryFn: () => photoService.getPhotos({ page, limit: 12 }),
|
||||
})
|
||||
|
||||
// 删除照片
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: photoService.deletePhoto,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||||
toast({
|
||||
title: '成功',
|
||||
description: '照片已删除',
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '删除失败',
|
||||
variant: 'destructive',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (confirm('确认删除这张照片吗?')) {
|
||||
deleteMutation.mutate(id)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <div>加载中...</div>
|
||||
if (error) return <div>加载失败</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">照片管理</h1>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
上传照片
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{data?.photos.map((photo) => (
|
||||
<Card key={photo.id} className="group">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative aspect-square overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
src={photo.thumbnail}
|
||||
alt={photo.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||||
/>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex space-x-1">
|
||||
<Button size="sm" variant="secondary">
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => handleDelete(photo.id)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold truncate">{photo.title}</h3>
|
||||
{photo.description && (
|
||||
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
||||
{photo.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<Badge variant="secondary">
|
||||
{photo.category_id ? '已分类' : '未分类'}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(photo.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{data && data.total > 12 && (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage(page - 1)}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={page * 12 >= data.total}
|
||||
onClick={() => setPage(page + 1)}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 开发工具配置
|
||||
|
||||
### Vite 配置
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8888',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
```bash
|
||||
# .env.local
|
||||
VITE_API_URL=http://localhost:8888/api/v1
|
||||
VITE_APP_TITLE=摄影作品管理后台
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解管理后台开发进度。
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
750
.cursor/rules/backend/api-development.mdc
Normal file
750
.cursor/rules/backend/api-development.mdc
Normal file
@ -0,0 +1,750 @@
|
||||
---
|
||||
globs: backend/api/**/*.api
|
||||
alwaysApply: false
|
||||
---
|
||||
# 后端 API 开发规则
|
||||
|
||||
## 🔄 API 开发工作流
|
||||
|
||||
### 1. 需求分析
|
||||
- 明确API功能和参数
|
||||
- 确定请求/响应格式
|
||||
- 考虑错误处理场景
|
||||
|
||||
### 2. API 定义 (.api文件)
|
||||
在 `backend/api/desc/` 目录下定义:
|
||||
|
||||
```api
|
||||
// photo.api
|
||||
syntax = "v1"
|
||||
|
||||
info(
|
||||
title: "Photography Photo API"
|
||||
desc: "照片管理相关API"
|
||||
author: "iriver"
|
||||
email: "iriver@example.com"
|
||||
version: "v1"
|
||||
)
|
||||
|
||||
import "common.api"
|
||||
|
||||
type (
|
||||
UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
Description string `form:"description,optional"`
|
||||
CategoryId string `form:"category_id,optional"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
|
||||
UploadPhotoResponse {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Filename string `json:"filename"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
UpdatePhotoRequest {
|
||||
Id string `path:"id"`
|
||||
Title string `json:"title,optional"`
|
||||
Description string `json:"description,optional"`
|
||||
CategoryId string `json:"category_id,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: photo
|
||||
prefix: /api/v1
|
||||
)
|
||||
service photography-api {
|
||||
@doc "上传照片"
|
||||
@handler uploadPhoto
|
||||
post /photos (UploadPhotoRequest) returns (UploadPhotoResponse)
|
||||
|
||||
@doc "更新照片信息"
|
||||
@handler updatePhoto
|
||||
put /photos/:id (UpdatePhotoRequest) returns (BaseResponse)
|
||||
|
||||
@doc "删除照片"
|
||||
@handler deletePhoto
|
||||
delete /photos/:id (IdPathRequest) returns (BaseResponse)
|
||||
|
||||
@doc "获取照片详情"
|
||||
@handler getPhoto
|
||||
get /photos/:id (IdPathRequest) returns (PhotoResponse)
|
||||
|
||||
@doc "获取照片列表"
|
||||
@handler getPhotoList
|
||||
get /photos (PhotoListRequest) returns (PhotoListResponse)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 代码生成
|
||||
```bash
|
||||
cd backend
|
||||
make api
|
||||
```
|
||||
|
||||
### 4. Handler 实现模式
|
||||
```go
|
||||
func (h *UploadPhotoHandler) UploadPhoto(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadPhotoRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := photo.NewUploadPhotoLogic(r.Context(), h.svcCtx)
|
||||
resp, err := l.UploadPhoto(&req, r)
|
||||
response.Response(w, resp, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Logic 实现模式
|
||||
```go
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest, r *http.Request) (resp *types.UploadPhotoResponse, err error) {
|
||||
// 1. 参数验证
|
||||
if err = l.validateRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 获取上传文件
|
||||
fileHeader, err := l.getUploadFile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 文件处理
|
||||
savedPath, thumbnail, err := l.processFile(fileHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 数据持久化
|
||||
photo, err := l.savePhoto(req, savedPath, thumbnail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 构造响应
|
||||
return l.buildResponse(photo), nil
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 API 接口规范
|
||||
|
||||
### 请求规范
|
||||
```go
|
||||
// 路径参数
|
||||
type IdPathRequest {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
type PhotoListRequest {
|
||||
Page int `form:"page,default=1"`
|
||||
Limit int `form:"limit,default=10"`
|
||||
CategoryId string `form:"category_id,optional"`
|
||||
Keyword string `form:"keyword,optional"`
|
||||
}
|
||||
|
||||
// JSON请求体
|
||||
type UpdatePhotoRequest {
|
||||
Title string `json:"title,optional"`
|
||||
Description string `json:"description,optional"`
|
||||
CategoryId string `json:"category_id,optional"`
|
||||
}
|
||||
|
||||
// 文件上传 (multipart/form-data)
|
||||
type UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
```
|
||||
|
||||
### 响应规范
|
||||
```go
|
||||
// 基础响应
|
||||
type BaseResponse {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// 单个资源响应
|
||||
type PhotoResponse {
|
||||
BaseResponse
|
||||
Data Photo `json:"data"`
|
||||
}
|
||||
|
||||
// 列表响应
|
||||
type PhotoListResponse {
|
||||
BaseResponse
|
||||
Data PhotoListData `json:"data"`
|
||||
}
|
||||
|
||||
type PhotoListData {
|
||||
List []Photo `json:"list"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 API 测试策略
|
||||
|
||||
### 1. 手动测试 (curl)
|
||||
```bash
|
||||
# 健康检查
|
||||
curl -X GET "http://localhost:8888/api/v1/health"
|
||||
|
||||
# 用户登录
|
||||
curl -X POST "http://localhost:8888/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"123456"}'
|
||||
|
||||
# 上传照片
|
||||
curl -X POST "http://localhost:8888/api/v1/photos" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "title=测试照片" \
|
||||
-F "description=这是一张测试照片" \
|
||||
-F "file=@test.jpg"
|
||||
|
||||
# 获取照片列表
|
||||
curl -X GET "http://localhost:8888/api/v1/photos?page=1&limit=10" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 更新照片
|
||||
curl -X PUT "http://localhost:8888/api/v1/photos/123" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"更新后的标题"}'
|
||||
|
||||
# 删除照片
|
||||
curl -X DELETE "http://localhost:8888/api/v1/photos/123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 2. HTTP文件测试
|
||||
创建 `test_api.http` 文件:
|
||||
```http
|
||||
### 登录获取token
|
||||
POST http://localhost:8888/api/v1/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
|
||||
### 设置变量
|
||||
@token = {{login.response.body.data.token}}
|
||||
|
||||
### 上传照片
|
||||
POST http://localhost:8888/api/v1/photos
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: multipart/form-data; boundary=boundary
|
||||
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="title"
|
||||
|
||||
测试照片标题
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="file"; filename="test.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
< ./test.jpg
|
||||
--boundary--
|
||||
|
||||
### 获取照片列表
|
||||
GET http://localhost:8888/api/v1/photos?page=1&limit=5
|
||||
Authorization: Bearer {{token}}
|
||||
```
|
||||
|
||||
## 🛡️ 安全和验证
|
||||
|
||||
### 认证验证
|
||||
```go
|
||||
// 获取当前用户ID
|
||||
func (l *UploadPhotoLogic) getCurrentUserID() (string, error) {
|
||||
userID := l.ctx.Value("userID")
|
||||
if userID == nil {
|
||||
return "", errors.New("用户未认证")
|
||||
}
|
||||
return userID.(string), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 参数验证
|
||||
```go
|
||||
func (l *UploadPhotoLogic) validateRequest(req *types.UploadPhotoRequest) error {
|
||||
if req.Title == "" {
|
||||
return errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
if len(req.Title) > 100 {
|
||||
return errorx.NewDefaultError("照片标题不能超过100个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 文件验证
|
||||
```go
|
||||
func (l *UploadPhotoLogic) validateFile(fileHeader *multipart.FileHeader) error {
|
||||
// 文件大小验证
|
||||
if fileHeader.Size > file.MaxFileSize {
|
||||
return errorx.NewDefaultError("文件大小不能超过10MB")
|
||||
}
|
||||
|
||||
// 文件类型验证
|
||||
if !file.IsImageFile(fileHeader.Filename) {
|
||||
return errorx.NewDefaultError("只支持图片文件")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 错误处理
|
||||
|
||||
### 标准错误响应
|
||||
```go
|
||||
// 参数错误
|
||||
return nil, errorx.NewCodeError(400, "参数错误")
|
||||
|
||||
// 认证错误
|
||||
return nil, errorx.NewCodeError(401, "未认证")
|
||||
|
||||
// 权限错误
|
||||
return nil, errorx.NewCodeError(403, "权限不足")
|
||||
|
||||
// 资源不存在
|
||||
return nil, errorx.NewCodeError(404, "照片不存在")
|
||||
|
||||
// 服务器错误
|
||||
return nil, errorx.NewCodeError(500, "服务器内部错误")
|
||||
```
|
||||
|
||||
### 业务错误处理
|
||||
```go
|
||||
func (l *UploadPhotoLogic) handleBusinessError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return errorx.NewCodeError(404, "资源不存在")
|
||||
case strings.Contains(err.Error(), "duplicate"):
|
||||
return errorx.NewCodeError(409, "资源已存在")
|
||||
default:
|
||||
logx.Errorf("业务处理失败: %v", err)
|
||||
return errorx.NewCodeError(500, "处理失败")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 开发工具
|
||||
|
||||
### Makefile 命令
|
||||
```makefile
|
||||
# 生成API代码
|
||||
api:
|
||||
goctl api go -api api/desc/photography.api -dir ./
|
||||
|
||||
# 启动服务
|
||||
run:
|
||||
go run cmd/api/main.go
|
||||
|
||||
# 构建
|
||||
build:
|
||||
go build -o bin/photography-api cmd/api/main.go
|
||||
|
||||
# 测试
|
||||
test:
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### 调试技巧
|
||||
```go
|
||||
// 添加调试日志
|
||||
logx.Infof("处理上传照片请求: %+v", req)
|
||||
logx.Errorf("文件保存失败: %v", err)
|
||||
|
||||
// 检查请求context
|
||||
userID := l.ctx.Value("userID")
|
||||
logx.Infof("当前用户: %v", userID)
|
||||
```
|
||||
|
||||
当前API开发状态参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md)。
|
||||
# 后端 API 开发规则
|
||||
|
||||
## 🔄 API 开发工作流
|
||||
|
||||
### 1. 需求分析
|
||||
- 明确API功能和参数
|
||||
- 确定请求/响应格式
|
||||
- 考虑错误处理场景
|
||||
|
||||
### 2. API 定义 (.api文件)
|
||||
在 `backend/api/desc/` 目录下定义:
|
||||
|
||||
```api
|
||||
// photo.api
|
||||
syntax = "v1"
|
||||
|
||||
info(
|
||||
title: "Photography Photo API"
|
||||
desc: "照片管理相关API"
|
||||
author: "iriver"
|
||||
email: "iriver@example.com"
|
||||
version: "v1"
|
||||
)
|
||||
|
||||
import "common.api"
|
||||
|
||||
type (
|
||||
UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
Description string `form:"description,optional"`
|
||||
CategoryId string `form:"category_id,optional"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
|
||||
UploadPhotoResponse {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Filename string `json:"filename"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
UpdatePhotoRequest {
|
||||
Id string `path:"id"`
|
||||
Title string `json:"title,optional"`
|
||||
Description string `json:"description,optional"`
|
||||
CategoryId string `json:"category_id,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: photo
|
||||
prefix: /api/v1
|
||||
)
|
||||
service photography-api {
|
||||
@doc "上传照片"
|
||||
@handler uploadPhoto
|
||||
post /photos (UploadPhotoRequest) returns (UploadPhotoResponse)
|
||||
|
||||
@doc "更新照片信息"
|
||||
@handler updatePhoto
|
||||
put /photos/:id (UpdatePhotoRequest) returns (BaseResponse)
|
||||
|
||||
@doc "删除照片"
|
||||
@handler deletePhoto
|
||||
delete /photos/:id (IdPathRequest) returns (BaseResponse)
|
||||
|
||||
@doc "获取照片详情"
|
||||
@handler getPhoto
|
||||
get /photos/:id (IdPathRequest) returns (PhotoResponse)
|
||||
|
||||
@doc "获取照片列表"
|
||||
@handler getPhotoList
|
||||
get /photos (PhotoListRequest) returns (PhotoListResponse)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 代码生成
|
||||
```bash
|
||||
cd backend
|
||||
make api
|
||||
```
|
||||
|
||||
### 4. Handler 实现模式
|
||||
```go
|
||||
func (h *UploadPhotoHandler) UploadPhoto(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadPhotoRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := photo.NewUploadPhotoLogic(r.Context(), h.svcCtx)
|
||||
resp, err := l.UploadPhoto(&req, r)
|
||||
response.Response(w, resp, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Logic 实现模式
|
||||
```go
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest, r *http.Request) (resp *types.UploadPhotoResponse, err error) {
|
||||
// 1. 参数验证
|
||||
if err = l.validateRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 获取上传文件
|
||||
fileHeader, err := l.getUploadFile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 文件处理
|
||||
savedPath, thumbnail, err := l.processFile(fileHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 数据持久化
|
||||
photo, err := l.savePhoto(req, savedPath, thumbnail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 构造响应
|
||||
return l.buildResponse(photo), nil
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 API 接口规范
|
||||
|
||||
### 请求规范
|
||||
```go
|
||||
// 路径参数
|
||||
type IdPathRequest {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
type PhotoListRequest {
|
||||
Page int `form:"page,default=1"`
|
||||
Limit int `form:"limit,default=10"`
|
||||
CategoryId string `form:"category_id,optional"`
|
||||
Keyword string `form:"keyword,optional"`
|
||||
}
|
||||
|
||||
// JSON请求体
|
||||
type UpdatePhotoRequest {
|
||||
Title string `json:"title,optional"`
|
||||
Description string `json:"description,optional"`
|
||||
CategoryId string `json:"category_id,optional"`
|
||||
}
|
||||
|
||||
// 文件上传 (multipart/form-data)
|
||||
type UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
```
|
||||
|
||||
### 响应规范
|
||||
```go
|
||||
// 基础响应
|
||||
type BaseResponse {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// 单个资源响应
|
||||
type PhotoResponse {
|
||||
BaseResponse
|
||||
Data Photo `json:"data"`
|
||||
}
|
||||
|
||||
// 列表响应
|
||||
type PhotoListResponse {
|
||||
BaseResponse
|
||||
Data PhotoListData `json:"data"`
|
||||
}
|
||||
|
||||
type PhotoListData {
|
||||
List []Photo `json:"list"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 API 测试策略
|
||||
|
||||
### 1. 手动测试 (curl)
|
||||
```bash
|
||||
# 健康检查
|
||||
curl -X GET "http://localhost:8888/api/v1/health"
|
||||
|
||||
# 用户登录
|
||||
curl -X POST "http://localhost:8888/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"123456"}'
|
||||
|
||||
# 上传照片
|
||||
curl -X POST "http://localhost:8888/api/v1/photos" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "title=测试照片" \
|
||||
-F "description=这是一张测试照片" \
|
||||
-F "file=@test.jpg"
|
||||
|
||||
# 获取照片列表
|
||||
curl -X GET "http://localhost:8888/api/v1/photos?page=1&limit=10" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 更新照片
|
||||
curl -X PUT "http://localhost:8888/api/v1/photos/123" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"更新后的标题"}'
|
||||
|
||||
# 删除照片
|
||||
curl -X DELETE "http://localhost:8888/api/v1/photos/123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 2. HTTP文件测试
|
||||
创建 `test_api.http` 文件:
|
||||
```http
|
||||
### 登录获取token
|
||||
POST http://localhost:8888/api/v1/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
|
||||
### 设置变量
|
||||
@token = {{login.response.body.data.token}}
|
||||
|
||||
### 上传照片
|
||||
POST http://localhost:8888/api/v1/photos
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: multipart/form-data; boundary=boundary
|
||||
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="title"
|
||||
|
||||
测试照片标题
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="file"; filename="test.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
< ./test.jpg
|
||||
--boundary--
|
||||
|
||||
### 获取照片列表
|
||||
GET http://localhost:8888/api/v1/photos?page=1&limit=5
|
||||
Authorization: Bearer {{token}}
|
||||
```
|
||||
|
||||
## 🛡️ 安全和验证
|
||||
|
||||
### 认证验证
|
||||
```go
|
||||
// 获取当前用户ID
|
||||
func (l *UploadPhotoLogic) getCurrentUserID() (string, error) {
|
||||
userID := l.ctx.Value("userID")
|
||||
if userID == nil {
|
||||
return "", errors.New("用户未认证")
|
||||
}
|
||||
return userID.(string), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 参数验证
|
||||
```go
|
||||
func (l *UploadPhotoLogic) validateRequest(req *types.UploadPhotoRequest) error {
|
||||
if req.Title == "" {
|
||||
return errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
if len(req.Title) > 100 {
|
||||
return errorx.NewDefaultError("照片标题不能超过100个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 文件验证
|
||||
```go
|
||||
func (l *UploadPhotoLogic) validateFile(fileHeader *multipart.FileHeader) error {
|
||||
// 文件大小验证
|
||||
if fileHeader.Size > file.MaxFileSize {
|
||||
return errorx.NewDefaultError("文件大小不能超过10MB")
|
||||
}
|
||||
|
||||
// 文件类型验证
|
||||
if !file.IsImageFile(fileHeader.Filename) {
|
||||
return errorx.NewDefaultError("只支持图片文件")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 错误处理
|
||||
|
||||
### 标准错误响应
|
||||
```go
|
||||
// 参数错误
|
||||
return nil, errorx.NewCodeError(400, "参数错误")
|
||||
|
||||
// 认证错误
|
||||
return nil, errorx.NewCodeError(401, "未认证")
|
||||
|
||||
// 权限错误
|
||||
return nil, errorx.NewCodeError(403, "权限不足")
|
||||
|
||||
// 资源不存在
|
||||
return nil, errorx.NewCodeError(404, "照片不存在")
|
||||
|
||||
// 服务器错误
|
||||
return nil, errorx.NewCodeError(500, "服务器内部错误")
|
||||
```
|
||||
|
||||
### 业务错误处理
|
||||
```go
|
||||
func (l *UploadPhotoLogic) handleBusinessError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return errorx.NewCodeError(404, "资源不存在")
|
||||
case strings.Contains(err.Error(), "duplicate"):
|
||||
return errorx.NewCodeError(409, "资源已存在")
|
||||
default:
|
||||
logx.Errorf("业务处理失败: %v", err)
|
||||
return errorx.NewCodeError(500, "处理失败")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 开发工具
|
||||
|
||||
### Makefile 命令
|
||||
```makefile
|
||||
# 生成API代码
|
||||
api:
|
||||
goctl api go -api api/desc/photography.api -dir ./
|
||||
|
||||
# 启动服务
|
||||
run:
|
||||
go run cmd/api/main.go
|
||||
|
||||
# 构建
|
||||
build:
|
||||
go build -o bin/photography-api cmd/api/main.go
|
||||
|
||||
# 测试
|
||||
test:
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### 调试技巧
|
||||
```go
|
||||
// 添加调试日志
|
||||
logx.Infof("处理上传照片请求: %+v", req)
|
||||
logx.Errorf("文件保存失败: %v", err)
|
||||
|
||||
// 检查请求context
|
||||
userID := l.ctx.Value("userID")
|
||||
logx.Infof("当前用户: %v", userID)
|
||||
```
|
||||
|
||||
当前API开发状态参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md)。
|
||||
492
.cursor/rules/backend/go-zero-framework.mdc
Normal file
492
.cursor/rules/backend/go-zero-framework.mdc
Normal file
@ -0,0 +1,492 @@
|
||||
---
|
||||
globs: backend/*,backend/**/*.go
|
||||
alwaysApply: false
|
||||
---
|
||||
# Go-Zero 框架开发规则
|
||||
|
||||
## 🚀 架构规范
|
||||
|
||||
### 核心概念
|
||||
- **Handler**: HTTP请求处理层,只做参数验证和调用Logic
|
||||
- **Logic**: 业务逻辑层,包含核心业务代码
|
||||
- **Model**: 数据访问层,处理数据库操作
|
||||
- **Types**: 请求/响应类型定义
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
backend/
|
||||
├── cmd/api/main.go # 服务启动入口
|
||||
├── etc/photography-api.yaml # 配置文件
|
||||
├── api/desc/ # API定义文件
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP处理器
|
||||
│ ├── logic/ # 业务逻辑
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── svc/ # 服务上下文
|
||||
│ └── types/ # 类型定义
|
||||
└── pkg/ # 工具包
|
||||
```
|
||||
|
||||
## 🎯 开发流程
|
||||
|
||||
### 1. API定义
|
||||
在 `api/desc/` 目录下定义接口:
|
||||
```api
|
||||
service photography-api {
|
||||
@handler uploadPhoto
|
||||
post /api/v1/photos (UploadPhotoRequest) returns (UploadPhotoResponse)
|
||||
}
|
||||
|
||||
type UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
Description string `form:"description,optional"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
|
||||
type UploadPhotoResponse {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Filename string `json:"filename"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 代码生成
|
||||
```bash
|
||||
cd backend
|
||||
make api # 生成handler和logic骨架
|
||||
```
|
||||
|
||||
### 3. Handler实现
|
||||
```go
|
||||
func (h *UploadPhotoHandler) UploadPhoto(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadPhotoRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewUploadPhotoLogic(r.Context(), h.svcCtx)
|
||||
resp, err := l.UploadPhoto(&req)
|
||||
response.Response(w, resp, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Logic实现
|
||||
```go
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (*types.UploadPhotoResponse, error) {
|
||||
// 1. 参数验证
|
||||
if req.Title == "" {
|
||||
return nil, errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
// 2. 业务逻辑
|
||||
photoID := uuid.New().String()
|
||||
|
||||
// 3. 数据持久化
|
||||
photo := &model.Photo{
|
||||
ID: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}
|
||||
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.UploadPhotoResponse{
|
||||
Id: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 工具包使用
|
||||
|
||||
### 文件处理
|
||||
使用 [pkg/utils/file/file.go](mdc:backend/pkg/utils/file/file.go):
|
||||
```go
|
||||
import "photography/pkg/utils/file"
|
||||
|
||||
// 验证图片文件
|
||||
if !file.IsImageFile(filename) {
|
||||
return errors.New("不支持的文件类型")
|
||||
}
|
||||
|
||||
// 保存文件并生成缩略图
|
||||
savedPath, thumbnail, err := file.SaveImage(fileData, filename)
|
||||
```
|
||||
|
||||
### JWT认证
|
||||
使用 [pkg/utils/jwt/jwt.go](mdc:backend/pkg/utils/jwt/jwt.go):
|
||||
```go
|
||||
import "photography/pkg/utils/jwt"
|
||||
|
||||
// 生成token
|
||||
token, err := jwt.GenerateToken(userID, username)
|
||||
|
||||
// 验证token
|
||||
userID, err := jwt.ParseToken(tokenString)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
使用 [pkg/errorx/errorx.go](mdc:backend/pkg/errorx/errorx.go):
|
||||
```go
|
||||
import "photography/pkg/errorx"
|
||||
|
||||
// 业务错误
|
||||
return nil, errorx.NewDefaultError("用户不存在")
|
||||
|
||||
// 自定义错误码
|
||||
return nil, errorx.NewCodeError(40001, "参数错误")
|
||||
```
|
||||
|
||||
## 🛡️ 中间件
|
||||
|
||||
### JWT认证中间件
|
||||
在 `internal/middleware/auth.go` 中实现:
|
||||
```go
|
||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
httpx.Error(w, errors.New("未提供认证token"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := jwt.ParseToken(strings.TrimPrefix(token, "Bearer "))
|
||||
if err != nil {
|
||||
httpx.Error(w, errors.New("token无效"))
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户ID注入到context
|
||||
ctx := context.WithValue(r.Context(), "userID", userID)
|
||||
next(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 数据模型
|
||||
|
||||
### 模型定义示例
|
||||
```go
|
||||
type Photo struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Filename string `db:"filename" json:"filename"`
|
||||
Thumbnail string `db:"thumbnail" json:"thumbnail"`
|
||||
CategoryID string `db:"category_id" json:"category_id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
```go
|
||||
// 插入
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
|
||||
// 查询
|
||||
photo, err := l.svcCtx.PhotoModel.FindOne(l.ctx, photoID)
|
||||
|
||||
// 更新
|
||||
err := l.svcCtx.PhotoModel.Update(l.ctx, photo)
|
||||
|
||||
// 删除
|
||||
err := l.svcCtx.PhotoModel.Delete(l.ctx, photoID)
|
||||
```
|
||||
|
||||
## 🔄 配置管理
|
||||
|
||||
### 配置文件: `etc/photography-api.yaml`
|
||||
```yaml
|
||||
Name: photography-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
|
||||
Auth:
|
||||
AccessSecret: your-secret-key
|
||||
AccessExpire: 86400
|
||||
|
||||
DataSource: photography.db
|
||||
|
||||
Log:
|
||||
ServiceName: photography-api
|
||||
Mode: file
|
||||
Path: logs
|
||||
Level: info
|
||||
```
|
||||
|
||||
## 🧪 测试规范
|
||||
|
||||
### 单元测试
|
||||
```go
|
||||
func TestUploadPhotoLogic(t *testing.T) {
|
||||
// 准备测试数据
|
||||
req := &types.UploadPhotoRequest{
|
||||
Title: "测试照片",
|
||||
File: "test.jpg",
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
logic := NewUploadPhotoLogic(context.Background(), svcCtx)
|
||||
resp, err := logic.UploadPhoto(req)
|
||||
|
||||
// 断言结果
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Id)
|
||||
assert.Equal(t, "测试照片", resp.Title)
|
||||
}
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解当前后端开发进度。
|
||||
# Go-Zero 框架开发规则
|
||||
|
||||
## 🚀 架构规范
|
||||
|
||||
### 核心概念
|
||||
- **Handler**: HTTP请求处理层,只做参数验证和调用Logic
|
||||
- **Logic**: 业务逻辑层,包含核心业务代码
|
||||
- **Model**: 数据访问层,处理数据库操作
|
||||
- **Types**: 请求/响应类型定义
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
backend/
|
||||
├── cmd/api/main.go # 服务启动入口
|
||||
├── etc/photography-api.yaml # 配置文件
|
||||
├── api/desc/ # API定义文件
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP处理器
|
||||
│ ├── logic/ # 业务逻辑
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── svc/ # 服务上下文
|
||||
│ └── types/ # 类型定义
|
||||
└── pkg/ # 工具包
|
||||
```
|
||||
|
||||
## 🎯 开发流程
|
||||
|
||||
### 1. API定义
|
||||
在 `api/desc/` 目录下定义接口:
|
||||
```api
|
||||
service photography-api {
|
||||
@handler uploadPhoto
|
||||
post /api/v1/photos (UploadPhotoRequest) returns (UploadPhotoResponse)
|
||||
}
|
||||
|
||||
type UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
Description string `form:"description,optional"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
|
||||
type UploadPhotoResponse {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Filename string `json:"filename"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 代码生成
|
||||
```bash
|
||||
cd backend
|
||||
make api # 生成handler和logic骨架
|
||||
```
|
||||
|
||||
### 3. Handler实现
|
||||
```go
|
||||
func (h *UploadPhotoHandler) UploadPhoto(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadPhotoRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewUploadPhotoLogic(r.Context(), h.svcCtx)
|
||||
resp, err := l.UploadPhoto(&req)
|
||||
response.Response(w, resp, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Logic实现
|
||||
```go
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (*types.UploadPhotoResponse, error) {
|
||||
// 1. 参数验证
|
||||
if req.Title == "" {
|
||||
return nil, errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
// 2. 业务逻辑
|
||||
photoID := uuid.New().String()
|
||||
|
||||
// 3. 数据持久化
|
||||
photo := &model.Photo{
|
||||
ID: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}
|
||||
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.UploadPhotoResponse{
|
||||
Id: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 工具包使用
|
||||
|
||||
### 文件处理
|
||||
使用 [pkg/utils/file/file.go](mdc:backend/pkg/utils/file/file.go):
|
||||
```go
|
||||
import "photography/pkg/utils/file"
|
||||
|
||||
// 验证图片文件
|
||||
if !file.IsImageFile(filename) {
|
||||
return errors.New("不支持的文件类型")
|
||||
}
|
||||
|
||||
// 保存文件并生成缩略图
|
||||
savedPath, thumbnail, err := file.SaveImage(fileData, filename)
|
||||
```
|
||||
|
||||
### JWT认证
|
||||
使用 [pkg/utils/jwt/jwt.go](mdc:backend/pkg/utils/jwt/jwt.go):
|
||||
```go
|
||||
import "photography/pkg/utils/jwt"
|
||||
|
||||
// 生成token
|
||||
token, err := jwt.GenerateToken(userID, username)
|
||||
|
||||
// 验证token
|
||||
userID, err := jwt.ParseToken(tokenString)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
使用 [pkg/errorx/errorx.go](mdc:backend/pkg/errorx/errorx.go):
|
||||
```go
|
||||
import "photography/pkg/errorx"
|
||||
|
||||
// 业务错误
|
||||
return nil, errorx.NewDefaultError("用户不存在")
|
||||
|
||||
// 自定义错误码
|
||||
return nil, errorx.NewCodeError(40001, "参数错误")
|
||||
```
|
||||
|
||||
## 🛡️ 中间件
|
||||
|
||||
### JWT认证中间件
|
||||
在 `internal/middleware/auth.go` 中实现:
|
||||
```go
|
||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
httpx.Error(w, errors.New("未提供认证token"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := jwt.ParseToken(strings.TrimPrefix(token, "Bearer "))
|
||||
if err != nil {
|
||||
httpx.Error(w, errors.New("token无效"))
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户ID注入到context
|
||||
ctx := context.WithValue(r.Context(), "userID", userID)
|
||||
next(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 数据模型
|
||||
|
||||
### 模型定义示例
|
||||
```go
|
||||
type Photo struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Filename string `db:"filename" json:"filename"`
|
||||
Thumbnail string `db:"thumbnail" json:"thumbnail"`
|
||||
CategoryID string `db:"category_id" json:"category_id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
```go
|
||||
// 插入
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
|
||||
// 查询
|
||||
photo, err := l.svcCtx.PhotoModel.FindOne(l.ctx, photoID)
|
||||
|
||||
// 更新
|
||||
err := l.svcCtx.PhotoModel.Update(l.ctx, photo)
|
||||
|
||||
// 删除
|
||||
err := l.svcCtx.PhotoModel.Delete(l.ctx, photoID)
|
||||
```
|
||||
|
||||
## 🔄 配置管理
|
||||
|
||||
### 配置文件: `etc/photography-api.yaml`
|
||||
```yaml
|
||||
Name: photography-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
|
||||
Auth:
|
||||
AccessSecret: your-secret-key
|
||||
AccessExpire: 86400
|
||||
|
||||
DataSource: photography.db
|
||||
|
||||
Log:
|
||||
ServiceName: photography-api
|
||||
Mode: file
|
||||
Path: logs
|
||||
Level: info
|
||||
```
|
||||
|
||||
## 🧪 测试规范
|
||||
|
||||
### 单元测试
|
||||
```go
|
||||
func TestUploadPhotoLogic(t *testing.T) {
|
||||
// 准备测试数据
|
||||
req := &types.UploadPhotoRequest{
|
||||
Title: "测试照片",
|
||||
File: "test.jpg",
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
logic := NewUploadPhotoLogic(context.Background(), svcCtx)
|
||||
resp, err := logic.UploadPhoto(req)
|
||||
|
||||
// 断言结果
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Id)
|
||||
assert.Equal(t, "测试照片", resp.Title)
|
||||
}
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解当前后端开发进度。
|
||||
262
.cursor/rules/common/code-style.mdc
Normal file
262
.cursor/rules/common/code-style.mdc
Normal file
@ -0,0 +1,262 @@
|
||||
---
|
||||
description: Code style and naming conventions
|
||||
---
|
||||
|
||||
# 代码风格和约定规则
|
||||
|
||||
## 📝 通用代码风格
|
||||
|
||||
### 文件命名
|
||||
- **Go文件**: `camelCase.go` (例: `uploadPhotoHandler.go`)
|
||||
- **TypeScript**: `kebab-case.tsx` 或 `PascalCase.tsx` (组件)
|
||||
- **API文件**: `kebab-case.api` (例: `photo.api`)
|
||||
- **配置文件**: `kebab-case.yaml/.json`
|
||||
|
||||
### 注释规范
|
||||
```go
|
||||
// ✅ Go - 函数注释
|
||||
// UploadPhoto 上传照片到服务器
|
||||
// 支持JPEG、PNG、GIF、WebP格式,最大10MB
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (*types.UploadPhotoResponse, error) {
|
||||
// 实现逻辑
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ✅ TypeScript - 接口注释
|
||||
/**
|
||||
* 照片数据接口
|
||||
* @interface Photo
|
||||
*/
|
||||
interface Photo {
|
||||
/** 照片唯一标识符 */
|
||||
id: string
|
||||
/** 照片标题 */
|
||||
title: string
|
||||
/** 文件名 */
|
||||
filename: string
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 命名约定
|
||||
|
||||
### 变量命名
|
||||
```go
|
||||
// ✅ Go - 驼峰命名
|
||||
var photoID string
|
||||
var userList []User
|
||||
var maxFileSize int64 = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// ❌ 避免
|
||||
var photo_id string
|
||||
var PhotoId string
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ✅ TypeScript - 驼峰命名
|
||||
const photoList: Photo[] = []
|
||||
const isLoading = false
|
||||
const handlePhotoUpload = () => {}
|
||||
|
||||
// ✅ 常量 - 大写下划线
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
```
|
||||
|
||||
### 函数命名
|
||||
- **动词开头**: `getPhotoList`, `uploadPhoto`, `deleteCategory`
|
||||
- **布尔值**: `isVisible`, `hasPermission`, `canEdit`
|
||||
- **事件处理**: `handleClick`, `onPhotoSelect`, `onUploadSuccess`
|
||||
|
||||
## 🛡️ 错误处理
|
||||
|
||||
### Go 错误处理
|
||||
```go
|
||||
// ✅ 标准错误处理
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (*types.UploadPhotoResponse, error) {
|
||||
if !file.IsImageFile(req.File) {
|
||||
return nil, errorx.NewDefaultError("不支持的文件类型")
|
||||
}
|
||||
|
||||
photoID, err := l.savePhoto(req)
|
||||
if err != nil {
|
||||
logx.Errorf("保存照片失败: %v", err)
|
||||
return nil, errorx.NewDefaultError("照片保存失败")
|
||||
}
|
||||
|
||||
return &types.UploadPhotoResponse{
|
||||
Id: photoID,
|
||||
// ...
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript 错误处理
|
||||
```typescript
|
||||
// ✅ 异步操作错误处理
|
||||
try {
|
||||
const response = await api.post<UploadResponse>('/photos', formData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const message = error.response?.data?.msg || '上传失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 导入组织
|
||||
|
||||
### Go 导入顺序
|
||||
```go
|
||||
import (
|
||||
// 标准库
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
// 第三方库
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
|
||||
// 本地包
|
||||
"photography/internal/logic/photo"
|
||||
"photography/internal/svc"
|
||||
"photography/internal/types"
|
||||
)
|
||||
```
|
||||
|
||||
### TypeScript 导入顺序
|
||||
```typescript
|
||||
// React 相关
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
// 第三方库
|
||||
import axios from 'axios'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
// UI 组件
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
// 本地模块
|
||||
import { api } from '@/lib/api'
|
||||
import { Photo } from '@/types/api'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
```
|
||||
|
||||
## 🎨 CSS/样式约定
|
||||
|
||||
### Tailwind CSS 类名顺序
|
||||
```tsx
|
||||
// ✅ 推荐顺序:布局 → 尺寸 → 间距 → 颜色 → 其他
|
||||
<div className="flex flex-col w-full h-full p-4 bg-white border rounded-lg shadow-md">
|
||||
<img
|
||||
className="w-full h-48 object-cover rounded-md"
|
||||
src={photo.thumbnail}
|
||||
alt={photo.title}
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 响应式设计
|
||||
```tsx
|
||||
// ✅ 移动优先响应式
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{photos.map(photo => (
|
||||
<PhotoCard key={photo.id} photo={photo} />
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🔧 类型定义
|
||||
|
||||
### TypeScript 接口规范
|
||||
```typescript
|
||||
// ✅ 明确的接口定义
|
||||
interface PhotoCardProps {
|
||||
photo: Photo
|
||||
onEdit?: (id: string) => void
|
||||
onDelete?: (id: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
// ✅ API 响应类型
|
||||
interface ApiResponse<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
type PhotoListResponse = ApiResponse<{
|
||||
photos: Photo[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}>
|
||||
```
|
||||
|
||||
### Go 结构体规范
|
||||
```go
|
||||
// ✅ 结构体标签完整
|
||||
type Photo struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Filename string `json:"filename" db:"filename"`
|
||||
Thumbnail string `json:"thumbnail" db:"thumbnail"`
|
||||
CategoryID string `json:"category_id" db:"category_id"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 性能约定
|
||||
|
||||
### 避免性能陷阱
|
||||
```typescript
|
||||
// ✅ 使用 useMemo 避免重复计算
|
||||
const expensiveValue = useMemo(() => {
|
||||
return photos.filter(photo => photo.category_id === selectedCategory)
|
||||
}, [photos, selectedCategory])
|
||||
|
||||
// ✅ 使用 useCallback 避免重复渲染
|
||||
const handlePhotoSelect = useCallback((id: string) => {
|
||||
setSelectedPhoto(photos.find(p => p.id === id))
|
||||
}, [photos])
|
||||
```
|
||||
|
||||
## 🔒 安全约定
|
||||
|
||||
### 输入验证
|
||||
```go
|
||||
// ✅ 后端输入验证
|
||||
if req.Title == "" {
|
||||
return nil, errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
if len(req.Title) > 100 {
|
||||
return nil, errorx.NewDefaultError("照片标题不能超过100个字符")
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ✅ 前端输入验证
|
||||
const validatePhoto = (data: PhotoFormData): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.title.trim()) {
|
||||
errors.push('标题不能为空')
|
||||
}
|
||||
|
||||
if (data.title.length > 100) {
|
||||
errors.push('标题不能超过100个字符')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
```
|
||||
|
||||
遵循这些约定可以保持代码的一致性和可维护性。
|
||||
151
.cursor/rules/common/development-workflow.mdc
Normal file
151
.cursor/rules/common/development-workflow.mdc
Normal file
@ -0,0 +1,151 @@
|
||||
---
|
||||
description: General development workflow and best practices
|
||||
---
|
||||
|
||||
# 开发工作流规则
|
||||
|
||||
## 🎯 当前优先级任务
|
||||
|
||||
基于 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md),当前高优先级任务:
|
||||
|
||||
### 🔥 立即处理
|
||||
1. **完善照片更新和删除业务逻辑** - 后端CRUD完整性
|
||||
2. **完善分类更新和删除业务逻辑** - 后端CRUD完整性
|
||||
3. **前端与后端API集成测试** - 端到端功能验证
|
||||
|
||||
### 📋 本周目标
|
||||
- 实现完整的照片和分类CRUD操作
|
||||
- 前后端API集成调试
|
||||
- 用户认证流程实现
|
||||
|
||||
## 🔄 开发流程规范
|
||||
|
||||
### Git 工作流
|
||||
```bash
|
||||
# 功能开发
|
||||
git checkout -b feature/photo-update-api
|
||||
git add .
|
||||
git commit -m "feat: 实现照片更新API"
|
||||
git push origin feature/photo-update-api
|
||||
|
||||
# 代码审查后合并
|
||||
git checkout main
|
||||
git merge feature/photo-update-api
|
||||
```
|
||||
|
||||
### 提交信息规范
|
||||
```bash
|
||||
feat: 新功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式
|
||||
refactor: 重构
|
||||
test: 测试
|
||||
chore: 构建/工具
|
||||
```
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
### 后端测试
|
||||
```bash
|
||||
# API 测试
|
||||
curl -X GET http://localhost:8888/api/v1/health
|
||||
curl -X POST http://localhost:8888/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"123456"}'
|
||||
|
||||
# 功能测试
|
||||
make test
|
||||
```
|
||||
|
||||
### 前端测试
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
cd admin && bun run dev # 管理后台:5173
|
||||
cd frontend && pnpm dev # 用户界面:3000
|
||||
cd ui && pnpm dev # 组件库:6006
|
||||
```
|
||||
|
||||
## 🚀 部署流程
|
||||
|
||||
### 开发环境
|
||||
```bash
|
||||
# 后端
|
||||
cd backend
|
||||
make run # 端口:8888
|
||||
|
||||
# 前端项目并行启动
|
||||
cd admin && bun run dev &
|
||||
cd frontend && pnpm dev &
|
||||
```
|
||||
|
||||
### 生产环境准备
|
||||
1. 配置PostgreSQL数据库
|
||||
2. 更新CI/CD流程
|
||||
3. 配置反向代理
|
||||
4. 设置监控和日志
|
||||
|
||||
## 📁 文件组织原则
|
||||
|
||||
### 后端文件
|
||||
- 新Handler: `internal/handler/{module}/{action}Handler.go`
|
||||
- 新Logic: `internal/logic/{module}/{action}Logic.go`
|
||||
- 新API: `api/desc/{module}.api`
|
||||
|
||||
### 前端文件
|
||||
- 新页面: `src/pages/{PageName}.tsx`
|
||||
- 新组件: `src/components/{ComponentName}.tsx`
|
||||
- 新服务: `src/services/{serviceName}.ts`
|
||||
|
||||
## 🔧 开发工具配置
|
||||
|
||||
### VS Code 推荐插件
|
||||
- Go语言:`golang.go`
|
||||
- React:`ES7+ React/Redux/React-Native snippets`
|
||||
- Tailwind:`Tailwind CSS IntelliSense`
|
||||
- API测试:`REST Client`
|
||||
|
||||
### 本地环境变量
|
||||
```bash
|
||||
# backend/.env
|
||||
DATABASE_URL="sqlite:photography.db"
|
||||
JWT_SECRET="your-secret-key"
|
||||
UPLOAD_PATH="./uploads"
|
||||
|
||||
# frontend/.env.local
|
||||
NEXT_PUBLIC_API_URL="http://localhost:8888/api/v1"
|
||||
```
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 常见问题
|
||||
1. **端口冲突**: 检查8888(后端)、3000(前端)、5173(管理后台)
|
||||
2. **依赖问题**: 删除node_modules重新安装
|
||||
3. **Go模块**: 运行`go mod tidy`清理依赖
|
||||
4. **API调用失败**: 检查CORS设置和认证token
|
||||
|
||||
### 调试命令
|
||||
```bash
|
||||
# 检查服务状态
|
||||
lsof -i :8888
|
||||
netstat -tlnp | grep 8888
|
||||
|
||||
# 查看日志
|
||||
tail -f backend/logs/photography.log
|
||||
```
|
||||
|
||||
## 📊 进度跟踪
|
||||
|
||||
### 完成标准
|
||||
- ✅ 代码通过测试
|
||||
- ✅ API接口可正常调用
|
||||
- ✅ 前端界面功能正常
|
||||
- ✅ 更新TASK_PROGRESS.md状态
|
||||
|
||||
### 每日更新
|
||||
在 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 记录:
|
||||
- 完成的任务
|
||||
- 遇到的问题
|
||||
- 明日计划
|
||||
|
||||
保持项目进度透明化和可追踪性。
|
||||
248
.cursor/rules/common/git-workflow.mdc
Normal file
248
.cursor/rules/common/git-workflow.mdc
Normal file
@ -0,0 +1,248 @@
|
||||
# Git 工作流规则
|
||||
|
||||
## 🌿 分支管理
|
||||
|
||||
### 分支命名规范
|
||||
```bash
|
||||
# 功能分支
|
||||
feature/photo-upload-api
|
||||
feature/admin-dashboard
|
||||
feature/responsive-design
|
||||
|
||||
# 修复分支
|
||||
fix/photo-delete-bug
|
||||
fix/login-redirect-issue
|
||||
|
||||
# 优化分支
|
||||
refactor/api-structure
|
||||
refactor/component-organization
|
||||
|
||||
# 文档分支
|
||||
docs/api-documentation
|
||||
docs/deployment-guide
|
||||
```
|
||||
|
||||
### 分支工作流
|
||||
```bash
|
||||
# 1. 从主分支创建功能分支
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b feature/photo-update-api
|
||||
|
||||
# 2. 开发和提交
|
||||
git add .
|
||||
git commit -m "feat: 实现照片更新API"
|
||||
|
||||
# 3. 推送分支
|
||||
git push origin feature/photo-update-api
|
||||
|
||||
# 4. 创建Pull Request
|
||||
# 在GitHub/GitLab上创建PR
|
||||
|
||||
# 5. 代码审查后合并
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git branch -d feature/photo-update-api
|
||||
```
|
||||
|
||||
## 📝 提交信息规范
|
||||
|
||||
### 提交类型
|
||||
```bash
|
||||
feat: 新功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式化(不影响代码逻辑)
|
||||
refactor: 重构(既不是新增功能,也不是修复bug)
|
||||
perf: 性能优化
|
||||
test: 添加测试
|
||||
chore: 构建过程或辅助工具的变动
|
||||
ci: CI/CD配置文件和脚本的变动
|
||||
```
|
||||
|
||||
### 提交信息格式
|
||||
```bash
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### 提交示例
|
||||
```bash
|
||||
# 简单提交
|
||||
git commit -m "feat: 添加照片上传功能"
|
||||
git commit -m "fix: 修复登录页面重定向问题"
|
||||
|
||||
# 详细提交
|
||||
git commit -m "feat(api): 实现照片批量删除功能
|
||||
|
||||
- 添加批量删除API接口
|
||||
- 支持选择多张照片删除
|
||||
- 添加删除确认对话框
|
||||
- 更新照片列表组件
|
||||
|
||||
Closes #123"
|
||||
```
|
||||
|
||||
## 🔄 工作流最佳实践
|
||||
|
||||
### 代码同步
|
||||
```bash
|
||||
# 每日开始工作前
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# 功能开发中定期同步
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout feature/your-branch
|
||||
git rebase main # 或者 git merge main
|
||||
```
|
||||
|
||||
### 提交频率
|
||||
- **小而频繁的提交**:每个逻辑单元完成后提交
|
||||
- **有意义的提交**:每次提交都应该是可运行的状态
|
||||
- **原子性提交**:一次提交只做一件事
|
||||
|
||||
### 代码审查
|
||||
```bash
|
||||
# PR 标题规范
|
||||
feat: 实现照片分类管理功能
|
||||
fix: 修复图片上传失败问题
|
||||
docs: 更新API文档
|
||||
|
||||
# PR 描述模板
|
||||
## 变更说明
|
||||
- 实现了什么功能
|
||||
- 修复了什么问题
|
||||
|
||||
## 测试说明
|
||||
- 如何测试这个变更
|
||||
- 相关的测试用例
|
||||
|
||||
## 影响范围
|
||||
- 影响的模块
|
||||
- 是否有破坏性变更
|
||||
```
|
||||
|
||||
## 🏷️ 版本管理
|
||||
|
||||
### 语义化版本
|
||||
```bash
|
||||
# 版本格式:MAJOR.MINOR.PATCH
|
||||
v1.0.0 # 初始版本
|
||||
v1.0.1 # 补丁版本(修复bug)
|
||||
v1.1.0 # 次要版本(新功能,向后兼容)
|
||||
v2.0.0 # 主要版本(破坏性变更)
|
||||
```
|
||||
|
||||
### 标签管理
|
||||
```bash
|
||||
# 创建标签
|
||||
git tag -a v1.0.0 -m "Release version 1.0.0"
|
||||
git push origin v1.0.0
|
||||
|
||||
# 查看标签
|
||||
git tag -l
|
||||
git show v1.0.0
|
||||
```
|
||||
|
||||
## 🚀 发布流程
|
||||
|
||||
### 发布分支
|
||||
```bash
|
||||
# 创建发布分支
|
||||
git checkout -b release/v1.1.0 main
|
||||
|
||||
# 在发布分支上修复最后的问题
|
||||
git commit -m "fix: 修复发布前的小问题"
|
||||
|
||||
# 合并回主分支和开发分支
|
||||
git checkout main
|
||||
git merge release/v1.1.0
|
||||
git tag -a v1.1.0 -m "Release v1.1.0"
|
||||
|
||||
# 删除发布分支
|
||||
git branch -d release/v1.1.0
|
||||
```
|
||||
|
||||
## 🔧 Git 配置
|
||||
|
||||
### 全局配置
|
||||
```bash
|
||||
# 用户信息
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.email "your.email@example.com"
|
||||
|
||||
# 编辑器
|
||||
git config --global core.editor "code --wait"
|
||||
|
||||
# 别名
|
||||
git config --global alias.co checkout
|
||||
git config --global alias.br branch
|
||||
git config --global alias.ci commit
|
||||
git config --global alias.st status
|
||||
git config --global alias.unstage 'reset HEAD --'
|
||||
git config --global alias.last 'log -1 HEAD'
|
||||
git config --global alias.visual '!gitk'
|
||||
```
|
||||
|
||||
### 项目配置
|
||||
```bash
|
||||
# .gitignore 示例
|
||||
# 依赖
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# 构建产物
|
||||
dist/
|
||||
build/
|
||||
*.exe
|
||||
|
||||
# 环境文件
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# 日志
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 临时文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
```
|
||||
|
||||
## 📊 常用命令
|
||||
|
||||
### 日常操作
|
||||
```bash
|
||||
# 查看状态
|
||||
git status
|
||||
git log --oneline -10
|
||||
|
||||
# 暂存操作
|
||||
git stash
|
||||
git stash pop
|
||||
git stash list
|
||||
|
||||
# 撤销操作
|
||||
git reset HEAD~1 # 撤销最后一次提交(保留更改)
|
||||
git reset --hard HEAD~1 # 撤销最后一次提交(丢弃更改)
|
||||
git revert HEAD # 创建一个撤销提交
|
||||
|
||||
# 查看差异
|
||||
git diff
|
||||
git diff --staged
|
||||
git diff main..feature/branch
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 进行版本控制和任务跟踪。
|
||||
36
.cursor/rules/common/project-structure.mdc
Normal file
36
.cursor/rules/common/project-structure.mdc
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Photography Portfolio 项目结构指南
|
||||
|
||||
## 🏗️ 项目架构概览
|
||||
|
||||
这是一个全栈摄影作品集项目,采用微服务架构:
|
||||
|
||||
### 后端服务 (`backend/`)
|
||||
- **框架**: go-zero v1.8.0
|
||||
- **主要入口**: [cmd/api/main.go](mdc:backend/cmd/api/main.go)
|
||||
- **配置文件**: [etc/photography-api.yaml](mdc:backend/etc/photography-api.yaml)
|
||||
- **API 定义**: [api/desc/](mdc:backend/api/desc/) 目录下的 `.api` 文件
|
||||
|
||||
### 前端项目
|
||||
- **管理后台**: [admin/](mdc:admin/) - React + TypeScript + Vite
|
||||
- **用户界面**: [frontend/](mdc:frontend/) - Next.js + TypeScript
|
||||
- **UI 组件库**: [ui/](mdc:ui/) - 共享组件
|
||||
|
||||
### 核心目录结构
|
||||
- `backend/internal/handler/` - HTTP 处理器
|
||||
- `backend/internal/logic/` - 业务逻辑层
|
||||
- `backend/internal/model/` - 数据模型
|
||||
- `backend/pkg/` - 共享工具包
|
||||
- `docs/` - 项目文档
|
||||
- `scripts/` - 部署和维护脚本
|
||||
|
||||
## 📋 开发状态
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解当前开发进度和优先级任务。
|
||||
|
||||
## 🔧 依赖管理
|
||||
- 后端:Go modules (`go.mod`)
|
||||
- 前端:pnpm (推荐) / bun
|
||||
- 管理后台:bun
|
||||
429
.cursor/rules/frontend/nextjs-structure.mdc
Normal file
429
.cursor/rules/frontend/nextjs-structure.mdc
Normal file
@ -0,0 +1,429 @@
|
||||
---
|
||||
globs: frontend/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
# Next.js 前端项目结构规则
|
||||
|
||||
## 🏗️ 项目架构
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
frontend/
|
||||
├── app/ # Next.js 14 App Router
|
||||
│ ├── globals.css # 全局样式
|
||||
│ ├── layout.tsx # 根布局
|
||||
│ ├── page.tsx # 首页
|
||||
│ ├── gallery/ # 照片画廊页面
|
||||
│ ├── about/ # 关于页面
|
||||
│ └── contact/ # 联系页面
|
||||
├── components/ # React 组件
|
||||
│ ├── ui/ # shadcn/ui 组件
|
||||
│ ├── photo-gallery.tsx # 照片画廊组件
|
||||
│ ├── photo-modal.tsx # 照片弹窗
|
||||
│ ├── navigation.tsx # 导航组件
|
||||
│ └── filter-bar.tsx # 筛选栏
|
||||
├── lib/ # 工具库
|
||||
│ ├── api.ts # API 客户端
|
||||
│ ├── queries.ts # TanStack Query
|
||||
│ └── utils.ts # 工具函数
|
||||
├── hooks/ # 自定义 Hooks
|
||||
├── styles/ # 样式文件
|
||||
└── public/ # 静态资源
|
||||
```
|
||||
|
||||
## 🎯 开发规范
|
||||
|
||||
### App Router 页面组件
|
||||
```tsx
|
||||
// app/gallery/page.tsx
|
||||
import { Metadata } from 'next'
|
||||
import { PhotoGallery } from '@/components/photo-gallery'
|
||||
import { FilterBar } from '@/components/filter-bar'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '照片画廊 | Photography Portfolio',
|
||||
description: '浏览我的摄影作品集',
|
||||
}
|
||||
|
||||
export default function GalleryPage() {
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">照片画廊</h1>
|
||||
<FilterBar />
|
||||
<PhotoGallery />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 布局组件
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { QueryProvider } from '@/components/providers/query-provider'
|
||||
import { ThemeProvider } from '@/components/theme-provider'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Photography Portfolio',
|
||||
description: '专业摄影作品展示',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider>
|
||||
<QueryProvider>
|
||||
<Navigation />
|
||||
{children}
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧩 组件开发规范
|
||||
|
||||
### 功能组件模式
|
||||
```tsx
|
||||
// components/photo-gallery.tsx
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { usePhotos } from '@/hooks/use-photos'
|
||||
import { PhotoCard } from './photo-card'
|
||||
import { LoadingSpinner } from './loading-spinner'
|
||||
|
||||
interface PhotoGalleryProps {
|
||||
categoryId?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export function PhotoGallery({ categoryId, limit = 20 }: PhotoGalleryProps) {
|
||||
const [page, setPage] = useState(1)
|
||||
const { data, isLoading, error } = usePhotos({
|
||||
page,
|
||||
limit,
|
||||
categoryId
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-8 text-red-600">
|
||||
加载照片失败:{error.message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{data?.photos.map((photo) => (
|
||||
<PhotoCard
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onView={() => handlePhotoView(photo)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function handlePhotoView(photo: Photo) {
|
||||
// 打开照片详情弹窗
|
||||
}
|
||||
```
|
||||
|
||||
### UI组件模式
|
||||
```tsx
|
||||
// components/photo-card.tsx
|
||||
import Image from 'next/image'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface PhotoCardProps {
|
||||
photo: Photo
|
||||
onView?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PhotoCard({ photo, onView, className }: PhotoCardProps) {
|
||||
return (
|
||||
<Card className={cn("group cursor-pointer hover:shadow-lg transition-shadow", className)}>
|
||||
<CardContent className="p-0">
|
||||
<div className="relative aspect-square overflow-hidden rounded-t-lg">
|
||||
<Image
|
||||
src={photo.thumbnail || '/placeholder.jpg'}
|
||||
alt={photo.title}
|
||||
fill
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
onClick={onView}
|
||||
/>
|
||||
{photo.category && (
|
||||
<Badge className="absolute top-2 left-2">
|
||||
{photo.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold truncate">{photo.title}</h3>
|
||||
{photo.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
||||
{photo.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{new Date(photo.created_at).toLocaleDateString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🌐 API 集成
|
||||
|
||||
### API 客户端配置
|
||||
```typescript
|
||||
// lib/api.ts
|
||||
import axios, { AxiosError } from 'axios'
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8888/api/v1',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use((config) => {
|
||||
// 从localStorage获取token (仅在客户端)
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
// 清除过期token
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### TanStack Query 集成
|
||||
```typescript
|
||||
// lib/queries.ts
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { api } from './api'
|
||||
|
||||
// 照片相关查询
|
||||
export function usePhotos(params: PhotoListParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: ['photos', params],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<ApiResponse<PhotoListData>>('/photos', {
|
||||
params,
|
||||
})
|
||||
return data.data
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5分钟
|
||||
})
|
||||
}
|
||||
|
||||
export function usePhoto(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['photo', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<ApiResponse<Photo>>(`/photos/${id}`)
|
||||
return data.data
|
||||
},
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
// 分类查询
|
||||
export function useCategories() {
|
||||
return useQuery({
|
||||
queryKey: ['categories'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<ApiResponse<Category[]>>('/categories')
|
||||
return data.data
|
||||
},
|
||||
staleTime: 10 * 60 * 1000, // 10分钟
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义 Hooks
|
||||
```typescript
|
||||
// hooks/use-photos.ts
|
||||
import { useState, useCallback } from 'react'
|
||||
import { usePhotos as usePhotosQuery } from '@/lib/queries'
|
||||
|
||||
export function usePhotos(initialParams: PhotoListParams = {}) {
|
||||
const [params, setParams] = useState(initialParams)
|
||||
const query = usePhotosQuery(params)
|
||||
|
||||
const updateParams = useCallback((newParams: Partial<PhotoListParams>) => {
|
||||
setParams(prev => ({ ...prev, ...newParams }))
|
||||
}, [])
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
setParams(prev => ({ ...prev, page: (prev.page || 1) + 1 }))
|
||||
}, [])
|
||||
|
||||
const prevPage = useCallback(() => {
|
||||
setParams(prev => ({ ...prev, page: Math.max((prev.page || 1) - 1, 1) }))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
...query,
|
||||
params,
|
||||
updateParams,
|
||||
nextPage,
|
||||
prevPage,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 样式和主题
|
||||
|
||||
### Tailwind CSS 配置
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
// ...其他 shadcn 颜色
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
}
|
||||
```
|
||||
|
||||
### CSS变量配置
|
||||
```css
|
||||
/* app/globals.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
/* ...其他CSS变量 */
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
/* ...暗色模式变量 */
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.photo-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6;
|
||||
}
|
||||
|
||||
.photo-card-hover {
|
||||
@apply hover:shadow-lg hover:scale-105 transition-all duration-300;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 性能优化
|
||||
|
||||
### Image 优化
|
||||
```tsx
|
||||
import Image from 'next/image'
|
||||
|
||||
// 响应式图片
|
||||
<Image
|
||||
src={photo.thumbnail}
|
||||
alt={photo.title}
|
||||
width={400}
|
||||
height={300}
|
||||
className="w-full h-auto"
|
||||
priority={index < 4} // 首屏图片优先加载
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
|
||||
/>
|
||||
```
|
||||
|
||||
### 懒加载和分页
|
||||
```tsx
|
||||
// 无限滚动
|
||||
function useInfinitePhotos() {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['photos'],
|
||||
queryFn: ({ pageParam = 1 }) => fetchPhotos({ page: pageParam }),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解前端开发进度。
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
Reference in New Issue
Block a user