3 Commits

Author SHA1 Message Date
010fe2a8c7 fix
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 5m8s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
2025-07-10 18:09:11 +08:00
35004f224e feat: 完善照片更新和删除业务逻辑
Some checks failed
部署后端服务 / 🚀 构建并部署 (push) Has been cancelled
部署后端服务 / 🔄 回滚部署 (push) Has been cancelled
部署后端服务 / 🧪 测试后端 (push) Has been cancelled
- 实现照片更新功能 (updatePhotoLogic.go)
  - 支持部分字段更新 (title, description, category_id)
  - 添加用户权限验证,只能更新自己的照片
  - 添加分类存在性验证
  - 完善错误处理和响应格式

- 实现照片删除功能 (deletePhotoLogic.go)
  - 添加用户权限验证,只能删除自己的照片
  - 同时删除数据库记录和文件系统文件
  - 安全的文件删除处理

- 更新Handler使用统一响应格式
  - updatePhotoHandler.go: 使用response.Response统一处理
  - deletePhotoHandler.go: 使用response.Response统一处理

- 添加完整API测试用例 (test_photo_crud.http)
  - 涵盖正常场景和错误场景测试
  - 包含权限验证测试

- 更新项目进度 (TASK_PROGRESS.md)
  - 完成率从8%提升到12%
  - 更新API接口状态
  - 记录技术成果和里程碑
2025-07-10 18:08:22 +08:00
317dc170f9 feat: 完成后端服务核心业务逻辑实现
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 10m41s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
## 主要功能
-  用户认证模块 (登录/注册/JWT)
-  照片管理模块 (上传/查询/分页/搜索)
-  分类管理模块 (创建/查询/分页)
-  用户管理模块 (用户列表/分页查询)
-  健康检查接口

## 技术实现
- 基于 go-zero v1.8.0 标准架构
- Handler → Logic → Model 三层架构
- SQLite/PostgreSQL 数据库支持
- JWT 认证机制
- bcrypt 密码加密
- 统一响应格式
- 自定义模型方法 (分页/搜索)

## API 接口
- POST /api/v1/auth/login - 用户登录
- POST /api/v1/auth/register - 用户注册
- GET /api/v1/health - 健康检查
- GET /api/v1/photos - 照片列表
- POST /api/v1/photos - 上传照片
- GET /api/v1/categories - 分类列表
- POST /api/v1/categories - 创建分类
- GET /api/v1/users - 用户列表

## 配置完成
- 开发环境配置 (SQLite)
- 生产环境支持 (PostgreSQL)
- JWT 认证配置
- 文件上传配置
- Makefile 构建脚本

服务已验证可正常构建和启动。
2025-07-10 16:12:12 +08:00
163 changed files with 8864 additions and 1772 deletions

View 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
---

View 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)。

View 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) 了解当前后端开发进度。

View 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
}
```
遵循这些约定可以保持代码的一致性和可维护性。

View 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) 记录:
- 完成的任务
- 遇到的问题
- 明日计划
保持项目进度透明化和可追踪性。

View 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) 进行版本控制和任务跟踪。

View 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

View 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
---

241
TASK_PROGRESS.md Normal file
View File

@ -0,0 +1,241 @@
# Photography Portfolio 项目任务进度
> 最后更新: 2025-01-10
> 项目状态: 开发中 🚧
## 📊 总体进度概览
- **总任务数**: 26
- **已完成**: 3 ✅
- **进行中**: 0 🔄
- **待开始**: 23 ⏳
- **完成率**: 12%
---
## 🔥 高优先级任务 (9/26)
### ✅ 已完成 (3/9)
#### 1. ✅ 完善照片上传功能
**状态**: 已完成 ✅
**完成时间**: 2025-01-10
**完成内容**:
- 创建了完整的文件处理工具包 (`pkg/utils/file/file.go`)
- 实现了图片上传、缩略图生成、文件存储
- 支持多种图片格式 (JPEG, PNG, GIF, WebP)
- 添加了文件大小和类型验证
- 实现了自动缩略图生成 (300px宽度)
- 更新了上传处理器支持 multipart form
- 更新了业务逻辑层处理文件上传
- 添加了静态文件服务 (`/uploads/*`)
- 集成了图片处理库 (`github.com/disintegration/imaging`)
#### 2. ✅ 实现 JWT 认证中间件
**状态**: 已完成 ✅
**完成时间**: 2025-01-10
**完成内容**:
- 创建了 JWT 认证中间件 (`internal/middleware/auth.go`)
- 实现了 Bearer Token 验证
- 支持用户信息注入到请求上下文
- 完善的错误处理和响应
- 与现有 JWT 工具包集成
- go-zero 框架已内置 JWT 支持,路由配置完整
#### 3. ✅ 完善照片更新和删除业务逻辑
**状态**: 已完成 ✅
**完成时间**: 2025-01-10
**完成内容**:
- 实现了完整的照片更新逻辑 (`updatePhotoLogic.go`)
- 参数验证和权限检查
- 支持部分字段更新 (title, description, category_id)
- 分类存在性验证
- 用户权限验证 (只能更新自己的照片)
- 实现了完整的照片删除逻辑 (`deletePhotoLogic.go`)
- 权限验证 (只能删除自己的照片)
- 同时删除数据库记录和文件系统文件
- 安全的文件删除处理 (即使文件删除失败也不回滚)
- 更新了 Handler 使用统一响应格式
- 创建了完整的 API 测试用例 (`test_photo_crud.http`)
- 包含正常场景和错误场景的测试覆盖
### 🔄 进行中 (0/9)
### ⏳ 待开始 (6/9)
#### 4. 完善分类更新和删除业务逻辑
**优先级**: 高 🔥
**预估工作量**: 0.5天
**依赖**: 无
#### 5. 前端与后端 API 集成测试
**优先级**: 高 🔥
**预估工作量**: 1天
**依赖**: 前端项目
#### 6. 实现用户认证流程 (登录/注册界面)
**优先级**: 高 🔥
**预估工作量**: 1天
**依赖**: 前端项目
#### 7. 实现照片上传界面和进度显示
**优先级**: 高 🔥
**预估工作量**: 1天
**依赖**: 前端项目
#### 8. 配置生产环境数据库 (PostgreSQL)
**优先级**: 高 🔥
**预估工作量**: 0.5天
**依赖**: 生产环境服务器
#### 9. 更新 CI/CD 流程支持后端部署
**优先级**: 高 🔥
**预估工作量**: 1天
**依赖**: 部署环境
---
## 📋 中优先级任务 (11/26)
### 后端功能 (5项)
- **完善用户管理 CRUD 操作** ⏳
- **添加数据库迁移脚本和种子数据** ⏳
- **实现 CORS 中间件和安全配置** ⏳
- **添加 API 接口测试用例** ⏳
- **实现日志中间件和错误处理** ⏳
### 前端功能 (3项)
- **完善照片管理界面 (编辑/删除)** ⏳
- **实现分类管理界面** ⏳
- **添加响应式设计优化** ⏳
### 部署和运维 (2项)
- **配置反向代理 (前后端统一域名)** ⏳
- **配置文件存储服务 (图片上传)** ⏳
### 测试和文档 (2项)
- **编写 API 集成测试** ⏳
- **完善 API 接口文档** ⏳
---
## 📌 低优先级任务 (6/26)
### 后端扩展 (3项)
- **完善生产环境配置 (PostgreSQL)** ⏳
- **添加 Docker 容器化配置** ⏳
- **实现 API 文档生成 (Swagger)** ⏳
### 部署优化 (1项)
- **设置监控和日志收集** ⏳
### 测试和文档 (2项)
- **编写前端 E2E 测试** ⏳
- **编写部署文档** ⏳
---
## 🎯 里程碑规划
### 第一阶段:核心功能完善 (本周)
- [x] 照片上传功能
- [x] JWT 认证中间件
- [ ] 照片和分类的完整 CRUD
- [ ] 前后端 API 集成
**目标**: 实现核心业务功能的完整闭环
### 第二阶段:功能完整性 (下周)
- [ ] 用户界面完善
- [ ] 数据库配置
- [ ] 基础测试用例
- [ ] 安全性配置
**目标**: 功能完整性和用户体验
### 第三阶段:部署和优化 (后续)
- [ ] CI/CD 更新
- [ ] 生产环境部署
- [ ] 性能优化
- [ ] 监控和文档
**目标**: 生产环境就绪
---
## 💻 技术成果
### ✅ 已实现的核心功能
- **文件上传系统**: 完整的图片上传和缩略图生成
- **JWT 认证体系**: 用户认证和权限管理
- **静态文件服务**: 图片资源访问
- **图片处理能力**: 自动缩放和格式支持
- **安全文件验证**: 类型和大小检查
- **照片CRUD完整**: 创建、读取、更新、删除全功能
- **权限控制**: 用户只能操作自己的照片
- **文件系统管理**: 删除照片时同步删除文件
### 📊 API 接口状态
-`POST /api/v1/auth/login` - 用户登录
-`POST /api/v1/auth/register` - 用户注册
-`GET /api/v1/health` - 健康检查
-`GET /api/v1/photos` - 照片列表
-`POST /api/v1/photos` - 上传照片 (支持文件上传)
-`GET /api/v1/photos/:id` - 获取照片详情
-`PUT /api/v1/photos/:id` - 更新照片 (支持权限验证)
-`DELETE /api/v1/photos/:id` - 删除照片 (同时删除文件)
-`GET /api/v1/categories` - 分类列表
-`POST /api/v1/categories` - 创建分类
-`GET /api/v1/users` - 用户列表
-`GET /uploads/*` - 静态文件访问
-`PUT /api/v1/categories/:id` - 更新分类
-`DELETE /api/v1/categories/:id` - 删除分类
### 🛠️ 技术栈
- **后端框架**: go-zero v1.8.0
- **数据库**: SQLite (开发) / PostgreSQL (生产)
- **认证**: JWT Token
- **文件处理**: imaging + uuid
- **构建工具**: Go 1.23+ + Makefile
---
## 📈 每日进度记录
### 2025-01-10
-**照片上传功能完成**: 实现文件处理、缩略图生成、静态服务
-**JWT 认证中间件完成**: Bearer Token 验证和用户上下文注入
-**照片更新删除功能完成**: 实现权限验证、文件同步删除、完整CRUD
- 📝 **下一步**: 完善分类的更新删除功能
### 待补充...
---
## 🔄 更新日志
### v0.2.0 - 2025-01-10
- 新增完整的文件上传系统
- 新增 JWT 认证中间件
- 新增静态文件服务
- 优化图片处理能力
- 完善照片更新和删除功能
- 实现用户权限控制
- 添加文件系统同步管理
### v0.1.0 - 2025-01-09
- 初始化 go-zero 项目架构
- 实现基础 CRUD 接口
- 配置开发环境
---
## 📞 联系信息
**项目负责人**: iriver
**项目仓库**: https://git.iriver.top/iriver/photography
**更新频率**: 每日更新
---
*本文档自动同步项目进度,如有疑问请查看具体模块的 CLAUDE.md 文件*

596
backend-old/CLAUDE.md Normal file
View File

@ -0,0 +1,596 @@
# Backend API Service - CLAUDE.md
本文件为 Claude Code 在后端 API 服务模块中工作时提供指导。
## 🎯 模块概览
这是一个基于 Go + Gin 框架的 REST API 后端服务,采用简洁的四层架构模式,遵循 Go 语言的简洁设计哲学。
### 主要特性
- 🏗️ 简洁四层架构 (API → Service → Repository → Model)
- 🚀 多种部署模式 (生产/开发/Mock)
- 📊 多数据库支持 (PostgreSQL + SQLite + Redis)
- 🔐 JWT 认证 + 基于角色的访问控制
- 📁 文件上传和存储管理
- 🐳 Docker 容器化部署
- 📊 健康检查和监控
- 📚 API 文档生成
### 技术栈
- **语言**: Go 1.23+
- **框架**: Gin v1.10.1
- **数据库**: PostgreSQL (生产) + SQLite (开发) + Redis (缓存)
- **ORM**: GORM v1.30.0
- **认证**: JWT (golang-jwt/jwt/v5)
- **日志**: Uber Zap
- **配置**: Viper
- **容器化**: Docker + Docker Compose
## 📁 简洁架构设计
### 核心模块结构(重构后)
```
backend/
├── CLAUDE.md # 📋 当前文件 - 后端总览
├── cmd/ # 🚀 应用入口模块
│ ├── server/ # 服务启动器
│ │ ├── CLAUDE.md # 启动服务配置指导
│ │ └── main.go # 统一入口(支持多模式)
│ └── migrate/ # 数据库迁移工具
│ └── main.go
├── internal/ # 📦 核心业务模块
│ ├── api/ # 🌐 HTTP 接口层
│ │ ├── CLAUDE.md # API 路由和处理器指导
│ │ ├── handlers/ # HTTP 处理器
│ │ ├── middleware/ # 中间件
│ │ ├── routes/ # 路由定义
│ │ └── validators/ # 请求验证
│ ├── service/ # 📋 业务逻辑层
│ │ ├── CLAUDE.md # 业务逻辑开发指导
│ │ ├── auth/ # 认证服务
│ │ ├── user/ # 用户服务
│ │ ├── photo/ # 照片服务
│ │ ├── category/ # 分类服务
│ │ └── storage/ # 文件存储服务
│ ├── repository/ # 🔧 数据访问层
│ │ ├── CLAUDE.md # 数据访问开发指导
│ │ ├── interfaces/ # 仓储接口
│ │ ├── postgres/ # PostgreSQL 实现
│ │ ├── redis/ # Redis 实现
│ │ └── sqlite/ # SQLite 实现
│ ├── model/ # 📦 数据模型层
│ │ ├── CLAUDE.md # 数据模型设计指导
│ │ ├── entity/ # 实体模型
│ │ ├── dto/ # 数据传输对象
│ │ └── request/ # 请求响应模型
│ └── config/ # ⚙️ 配置管理
│ ├── CLAUDE.md # 配置文件管理指导
│ └── config.go # 配置结构体
├── pkg/ # 📦 共享包模块
│ ├── CLAUDE.md # 公共工具包指导
│ ├── logger/ # 日志工具
│ ├── response/ # 响应格式
│ ├── validator/ # 验证器
│ └── utils/ # 通用工具
├── configs/ # 📋 配置文件
├── migrations/ # 📊 数据库迁移
├── tests/ # 🧪 测试模块
│ ├── CLAUDE.md # 测试编写和执行指导
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── mocks/ # 模拟对象
└── docs/ # 📚 文档模块
├── CLAUDE.md # API 文档和接口设计指导
└── api/ # API 文档
```
### Go 风格的四层架构
#### 🌐 API 层 (`internal/api/`)
- **职责**: HTTP 请求处理、路由定义、中间件、参数验证
- **文件**: `handlers/`, `middleware/`, `routes/`, `validators/`
- **指导**: `internal/api/CLAUDE.md`
#### 📋 Service 层 (`internal/service/`)
- **职责**: 业务逻辑处理、服务编排、事务管理
- **文件**: `auth/`, `user/`, `photo/`, `category/`, `storage/`
- **指导**: `internal/service/CLAUDE.md`
#### 🔧 Repository 层 (`internal/repository/`)
- **职责**: 数据访问、数据库操作、缓存管理
- **文件**: `interfaces/`, `postgres/`, `redis/`, `sqlite/`
- **指导**: `internal/repository/CLAUDE.md`
#### 📦 Model 层 (`internal/model/`)
- **职责**: 数据模型、实体定义、DTO 转换
- **文件**: `entity/`, `dto/`, `request/`
- **指导**: `internal/model/CLAUDE.md`
### 简洁性原则
1. **单一职责**: 每个模块只负责一个明确的功能
2. **依赖注入**: 使用接口解耦,便于测试和扩展
3. **配置集中**: 所有配置统一管理,支持多环境
4. **错误处理**: 统一的错误处理机制
5. **代码生成**: 减少重复代码,提高开发效率
## 🚀 快速开始
### 开发环境设置
```bash
# 1. 环境准备
cd backend/
make setup # 初始化开发环境
# 2. 开发模式选择
make dev-simple # Mock 服务器 (前端开发)
make dev # SQLite 开发服务器 (全功能)
make dev-full # PostgreSQL 开发服务器 (生产环境)
# 3. 生产部署
make prod-up # Docker 容器部署
```
### 服务模式说明
- **Mock 模式**: 快速响应的模拟 API用于前端开发
- **开发模式**: 完整功能的 SQLite 数据库,用于本地开发
- **生产模式**: PostgreSQL + Redis用于生产环境
## 🔧 Go 风格开发规范
### 代码结构规范
1. **四层架构**: API → Service → Repository → Model
2. **接口导向**: 使用接口定义契约,便于测试和替换
3. **依赖注入**: 构造函数注入,避免全局变量
4. **错误处理**: 显式错误处理,避免 panic
5. **并发安全**: 使用 context 和 sync 包确保并发安全
### Go 语言命名规范
```
文件和目录:
- 文件名: snake_case (user_service.go)
- 包名: 小写单词 (userservice 或 user)
- 目录名: 小写单词 (auth, user, photo)
代码命名:
- 结构体: PascalCase (UserService, PhotoEntity)
- 接口: PascalCase + er结尾 (UserServicer, PhotoStorer)
- 方法/函数: PascalCase (GetUser, CreatePhoto)
- 变量: camelCase (userService, photoList)
- 常量: PascalCase (MaxUserCount, DefaultPageSize)
- 枚举: PascalCase (UserStatusActive, UserStatusInactive)
```
### 接口设计规范
```go
// 接口定义
type UserServicer interface {
GetUser(ctx context.Context, id uint) (*entity.User, error)
CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error)
UpdateUser(ctx context.Context, id uint, req *dto.UpdateUserRequest) error
DeleteUser(ctx context.Context, id uint) error
ListUsers(ctx context.Context, opts *dto.ListUsersOptions) ([]*entity.User, int64, error)
}
// 实现规范
type UserService struct {
userRepo repository.UserRepositoryr
logger logger.Logger
}
func NewUserService(userRepo repository.UserRepositoryr, logger logger.Logger) UserServicer {
return &UserService{
userRepo: userRepo,
logger: logger,
}
}
```
### RESTful API 设计规范
```
资源路径规范:
GET /api/v1/users # 获取用户列表
POST /api/v1/users # 创建用户
GET /api/v1/users/:id # 获取用户详情
PUT /api/v1/users/:id # 更新用户
DELETE /api/v1/users/:id # 删除用户
嵌套资源:
GET /api/v1/users/:id/photos # 获取用户的照片
POST /api/v1/users/:id/photos # 为用户创建照片
查询参数:
GET /api/v1/users?page=1&limit=20&sort=created_at&order=desc
```
### 统一响应格式
```go
// 成功响应
type SuccessResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// 错误响应
type ErrorResponse struct {
Success bool `json:"success"`
Error Error `json:"error"`
Timestamp int64 `json:"timestamp"`
}
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
```
### 错误处理规范
```go
// 自定义错误类型
type AppError struct {
Code string
Message string
Details string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
// 错误码定义
const (
ErrCodeUserNotFound = "USER_NOT_FOUND"
ErrCodeInvalidParameter = "INVALID_PARAMETER"
ErrCodePermissionDenied = "PERMISSION_DENIED"
ErrCodeInternalError = "INTERNAL_ERROR"
)
// 错误处理函数
func HandleError(c *gin.Context, err error) {
var appErr *AppError
if errors.As(err, &appErr) {
c.JSON(http.StatusBadRequest, ErrorResponse{
Success: false,
Error: Error{
Code: appErr.Code,
Message: appErr.Message,
Details: appErr.Details,
},
Timestamp: time.Now().Unix(),
})
return
}
// 未知错误
c.JSON(http.StatusInternalServerError, ErrorResponse{
Success: false,
Error: Error{
Code: ErrCodeInternalError,
Message: "内部服务器错误",
},
Timestamp: time.Now().Unix(),
})
}
```
### 日志记录规范
```go
// 结构化日志
logger.Info("user created successfully",
zap.String("user_id", user.ID),
zap.String("username", user.Username),
zap.String("operation", "create_user"),
)
// 错误日志
logger.Error("failed to create user",
zap.Error(err),
zap.String("username", req.Username),
zap.String("operation", "create_user"),
)
```
### 配置管理规范
```go
// 配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
Storage StorageConfig `mapstructure:"storage"`
}
// 环境变量映射
type ServerConfig struct {
Port string `mapstructure:"port" env:"SERVER_PORT"`
Mode string `mapstructure:"mode" env:"SERVER_MODE"`
LogLevel string `mapstructure:"log_level" env:"LOG_LEVEL"`
}
```
## 📊 数据库设计
### 主要实体
- **User**: 用户信息和权限
- **Photo**: 照片信息和元数据
- **Category**: 照片分类
- **Tag**: 照片标签
- **Album**: 相册管理
### 关系设计
```
User (1:N) Photo
Photo (N:M) Category
Photo (N:M) Tag
User (1:N) Album
Album (N:M) Photo
```
## 🔐 认证和授权
### JWT 认证流程
1. 用户登录 → 验证凭据
2. 生成 JWT Token (AccessToken + RefreshToken)
3. 客户端携带 Token 访问受保护资源
4. 服务器验证 Token 有效性
### 权限角色
- **Admin**: 系统管理员 (所有权限)
- **Editor**: 内容编辑者 (内容管理)
- **User**: 普通用户 (查看权限)
## 🧪 测试策略
### 测试类型
- **单元测试**: 业务逻辑和工具函数
- **集成测试**: API 接口和数据库交互
- **性能测试**: 接口响应时间和并发测试
### 测试工具
- **Go Testing**: 内置测试框架
- **Testify**: 断言和模拟工具
- **Mockery**: 接口模拟生成
## 📚 API 文档
### 文档生成
- **Swagger/OpenAPI**: 自动生成 API 文档
- **Postman Collection**: 接口测试集合
- **README**: 快速开始指南
### 文档维护
- 接口变更时同步更新文档
- 提供完整的请求/响应示例
- 包含错误码和处理说明
## 🚀 部署配置
### 环境变量
```bash
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=photography
DB_USER=postgres
DB_PASSWORD=password
# JWT 配置
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=24h
# 文件存储
STORAGE_TYPE=local
STORAGE_PATH=./uploads
```
### Docker 部署
```bash
# 构建镜像
make build-image
# 启动服务
make prod-up
# 查看日志
make logs
```
## 📋 常用命令
### 开发命令
```bash
# 代码生成
make generate # 生成代码 (mocks, swagger)
# 代码检查
make lint # 代码检查
make fmt # 代码格式化
make vet # 代码分析
# 测试
make test # 运行测试
make test-cover # 测试覆盖率
make test-integration # 集成测试
# 构建
make build # 构建二进制文件
make build-image # 构建 Docker 镜像
```
### 数据库命令
```bash
# 迁移
make migrate-up # 应用迁移
make migrate-down # 回滚迁移
make migrate-create # 创建迁移文件
# 数据库管理
make db-reset # 重置数据库
make db-seed # 导入种子数据
```
## 🔍 问题排查
### 常见问题
1. **数据库连接失败**: 检查配置文件和环境变量
2. **JWT 验证失败**: 检查密钥配置和 Token 格式
3. **文件上传失败**: 检查存储配置和权限设置
4. **API 响应慢**: 检查数据库查询和缓存配置
### 日志查看
```bash
# 查看应用日志
tail -f logs/app.log
# 查看错误日志
tail -f logs/error.log
# 查看访问日志
tail -f logs/access.log
```
## 🎯 模块工作指南
### 根据工作内容选择模块
#### 🚀 应用启动和配置
```bash
cd cmd/server/
# 参考 cmd/server/CLAUDE.md
```
**适用场景**: 服务启动、配置初始化、依赖注入
#### 🌐 API 接口开发
```bash
cd internal/api/
# 参考 internal/api/CLAUDE.md
```
**适用场景**: 路由定义、HTTP 处理器、中间件、请求验证
#### 📋 业务逻辑开发
```bash
cd internal/application/
# 参考 internal/application/CLAUDE.md
```
**适用场景**: 业务逻辑、服务编排、数据传输对象
#### 🏢 领域模型设计
```bash
cd internal/domain/
# 参考 internal/domain/CLAUDE.md
```
**适用场景**: 业务实体、业务规则、仓储接口
#### 🔧 基础设施开发
```bash
cd internal/infrastructure/
# 参考 internal/infrastructure/CLAUDE.md
```
**适用场景**: 数据库、缓存、文件存储、外部服务
#### 📦 工具包开发
```bash
cd pkg/
# 参考 pkg/CLAUDE.md
```
**适用场景**: 通用工具、日志、验证器、响应格式
#### 🧪 测试开发
```bash
cd tests/
# 参考 tests/CLAUDE.md
```
**适用场景**: 单元测试、集成测试、性能测试
#### 📚 文档维护
```bash
cd docs/
# 参考 docs/CLAUDE.md
```
**适用场景**: API 文档、架构设计、部署指南
## 🔄 最佳实践
### 开发流程
1. **功能分析**: 确定需求和技术方案
2. **选择模块**: 根据工作内容选择对应模块
3. **阅读指导**: 详细阅读模块的 CLAUDE.md 文件
4. **编码实现**: 遵循模块规范进行开发
5. **测试验证**: 编写和运行相关测试
6. **文档更新**: 同步更新相关文档
### 代码质量
- **代码审查**: 提交前进行代码审查
- **测试覆盖**: 保持合理的测试覆盖率
- **性能优化**: 关注接口响应时间和资源使用
- **安全检查**: 验证认证、授权和数据验证
### 模块协调
- **接口一致性**: 确保模块间接口的一致性
- **依赖管理**: 合理管理模块间的依赖关系
- **配置统一**: 统一配置管理和环境变量
- **错误处理**: 统一错误处理和响应格式
## 📈 项目状态
### 已完成功能
- ✅ 清洁架构设计
- ✅ 多数据库支持
- ✅ JWT 认证系统
- ✅ 文件上传功能
- ✅ Docker 部署
- ✅ 基础 API 接口
### 开发中功能
- 🔄 完整的测试覆盖
- 🔄 API 文档生成
- 🔄 性能监控
- 🔄 缓存优化
### 计划中功能
- 📋 微服务架构
- 📋 分布式文件存储
- 📋 消息队列集成
- 📋 监控和报警系统
## 🔧 开发环境
### 必需工具
- **Go 1.23+**: 编程语言
- **PostgreSQL 14+**: 主数据库
- **Redis 6+**: 缓存数据库
- **Docker**: 容器化部署
- **Make**: 构建工具
### 推荐工具
- **GoLand/VSCode**: 代码编辑器
- **Postman**: API 测试
- **DBeaver**: 数据库管理
- **Redis Desktop Manager**: Redis 管理
## 💡 开发技巧
### 性能优化
- 使用数据库连接池
- 实现查询结果缓存
- 优化 SQL 查询语句
- 使用异步处理
### 安全防护
- 输入参数验证
- SQL 注入防护
- XSS 攻击防护
- 访问频率限制
### 错误处理
- 统一错误响应格式
- 详细的错误日志记录
- 适当的错误码设计
- 友好的错误提示
本 CLAUDE.md 文件为后端开发提供了全面的指导,每个子模块都有详细的 CLAUDE.md 文件,确保开发过程中可以快速获取相关信息,提高开发效率。

197
backend-old/Makefile Normal file
View File

@ -0,0 +1,197 @@
# Photography Backend Makefile
# Simple and functional Makefile for Go backend project with Docker support
.PHONY: help dev dev-up dev-down build clean docker-build docker-run prod-up prod-down status health fmt mod
# Color definitions
GREEN := \033[0;32m
YELLOW := \033[1;33m
BLUE := \033[0;34m
RED := \033[0;31m
NC := \033[0m # No Color
# Application configuration
APP_NAME := photography-backend
VERSION := 1.0.0
BUILD_TIME := $(shell date +%Y%m%d_%H%M%S)
LDFLAGS := -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)
# Build configuration
BUILD_DIR := bin
MAIN_FILE := cmd/server/main.go
# Database configuration
DB_URL := postgres://postgres:password@localhost:5432/photography?sslmode=disable
MIGRATION_DIR := migrations
# Default target
.DEFAULT_GOAL := help
##@ Development Environment Commands
dev: ## Start development server with SQLite database
@printf "$(GREEN)🚀 Starting development server with SQLite...\n$(NC)"
@go run cmd/server/main_with_db.go
dev-simple: ## Start simple development server (mock data)
@printf "$(GREEN)🚀 Starting simple development server...\n$(NC)"
@go run cmd/server/simple_main.go
dev-up: ## Start development environment with Docker
@printf "$(GREEN)🐳 Starting development environment...\n$(NC)"
@docker-compose -f docker-compose.dev.yml up -d
@printf "$(GREEN)✅ Development environment started successfully!\n$(NC)"
dev-down: ## Stop development environment
@printf "$(GREEN)🛑 Stopping development environment...\n$(NC)"
@docker-compose -f docker-compose.dev.yml down
@printf "$(GREEN)✅ Development environment stopped!\n$(NC)"
##@ Build Commands
build: ## Build the Go application
@printf "$(GREEN)🔨 Building $(APP_NAME)...\n$(NC)"
@mkdir -p $(BUILD_DIR)
@CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_FILE)
@printf "$(GREEN)✅ Build completed: $(BUILD_DIR)/$(APP_NAME)\n$(NC)"
clean: ## Clean build artifacts
@printf "$(GREEN)🧹 Cleaning build files...\n$(NC)"
@rm -rf $(BUILD_DIR)
@rm -f coverage.out coverage.html
@printf "$(GREEN)✅ Clean completed!\n$(NC)"
##@ Docker Commands
docker-build: ## Build Docker image
@printf "$(GREEN)🐳 Building Docker image...\n$(NC)"
@docker build -t $(APP_NAME):$(VERSION) .
@docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest
@printf "$(GREEN)✅ Docker image built: $(APP_NAME):$(VERSION)\n$(NC)"
docker-run: ## Run application in Docker container
@printf "$(GREEN)🐳 Running Docker container...\n$(NC)"
@docker-compose up -d
@printf "$(GREEN)✅ Docker container started!\n$(NC)"
##@ Production Commands
prod-up: ## Start production environment
@printf "$(GREEN)🚀 Starting production environment...\n$(NC)"
@docker-compose up -d
@printf "$(GREEN)✅ Production environment started!\n$(NC)"
prod-down: ## Stop production environment
@printf "$(GREEN)🛑 Stopping production environment...\n$(NC)"
@docker-compose down
@printf "$(GREEN)✅ Production environment stopped!\n$(NC)"
##@ Health Check & Status Commands
status: ## Check application and services status
@printf "$(GREEN)📊 Checking application status...\n$(NC)"
@printf "$(BLUE)Docker containers:$(NC)\n"
@docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "($(APP_NAME)|postgres|redis)" || echo "No containers running"
@printf "$(BLUE)Application build:$(NC)\n"
@if [ -f "$(BUILD_DIR)/$(APP_NAME)" ]; then \
printf "$(GREEN)✅ Binary exists: $(BUILD_DIR)/$(APP_NAME)\n$(NC)"; \
ls -lh $(BUILD_DIR)/$(APP_NAME); \
else \
printf "$(RED)❌ Binary not found. Run 'make build' first.\n$(NC)"; \
fi
health: ## Check health of running services
@printf "$(GREEN)🏥 Checking service health...\n$(NC)"
@printf "$(BLUE)Testing application endpoint...\n$(NC)"
@curl -f http://localhost:8080/health 2>/dev/null && printf "$(GREEN)✅ Application is healthy\n$(NC)" || printf "$(RED)❌ Application is not responding\n$(NC)"
@printf "$(BLUE)Database connection...\n$(NC)"
@docker exec photography-postgres pg_isready -U postgres 2>/dev/null && printf "$(GREEN)✅ Database is ready\n$(NC)" || printf "$(RED)❌ Database is not ready\n$(NC)"
##@ Code Quality Commands
fmt: ## Format Go code
@printf "$(GREEN)🎨 Formatting Go code...\n$(NC)"
@go fmt ./...
@printf "$(GREEN)✅ Code formatted!\n$(NC)"
mod: ## Tidy Go modules
@printf "$(GREEN)📦 Tidying Go modules...\n$(NC)"
@go mod tidy
@go mod download
@printf "$(GREEN)✅ Modules tidied!\n$(NC)"
lint: ## Run code linter
@printf "$(GREEN)🔍 Running linter...\n$(NC)"
@golangci-lint run
@printf "$(GREEN)✅ Linting completed!\n$(NC)"
test: ## Run tests
@printf "$(GREEN)🧪 Running tests...\n$(NC)"
@go test -v ./...
@printf "$(GREEN)✅ Tests completed!\n$(NC)"
##@ Utility Commands
install: ## Install dependencies
@printf "$(GREEN)📦 Installing dependencies...\n$(NC)"
@go mod download
@go mod tidy
@printf "$(GREEN)✅ Dependencies installed!\n$(NC)"
logs: ## Show application logs
@printf "$(GREEN)📄 Showing application logs...\n$(NC)"
@docker-compose logs -f $(APP_NAME)
migrate-up: ## Run database migrations
@printf "$(GREEN)🗄️ Running database migrations...\n$(NC)"
@migrate -path $(MIGRATION_DIR) -database "$(DB_URL)" up
@printf "$(GREEN)✅ Migrations completed!\n$(NC)"
migrate-down: ## Rollback database migrations
@printf "$(GREEN)🗄️ Rolling back database migrations...\n$(NC)"
@migrate -path $(MIGRATION_DIR) -database "$(DB_URL)" down
@printf "$(GREEN)✅ Migrations rolled back!\n$(NC)"
##@ Database Commands
db-reset: ## Reset SQLite database (delete and recreate)
@printf "$(GREEN)🗄️ Resetting SQLite database...\n$(NC)"
@rm -f photography.db
@printf "$(GREEN)✅ Database reset! Will be recreated on next startup.\n$(NC)"
db-backup: ## Backup SQLite database
@printf "$(GREEN)💾 Backing up SQLite database...\n$(NC)"
@cp photography.db photography_backup_$(BUILD_TIME).db
@printf "$(GREEN)✅ Database backed up to photography_backup_$(BUILD_TIME).db\n$(NC)"
db-shell: ## Open SQLite database shell
@printf "$(GREEN)🐚 Opening SQLite database shell...\n$(NC)"
@sqlite3 photography.db
db-status: ## Show database status and table info
@printf "$(GREEN)📊 Database status:\n$(NC)"
@if [ -f "photography.db" ]; then \
printf "$(BLUE)Database file: photography.db ($(shell ls -lh photography.db | awk '{print $$5}'))\\n$(NC)"; \
printf "$(BLUE)Tables:\\n$(NC)"; \
sqlite3 photography.db ".tables"; \
printf "$(BLUE)Row counts:\\n$(NC)"; \
sqlite3 photography.db "SELECT 'Users: ' || COUNT(*) FROM users; SELECT 'Photos: ' || COUNT(*) FROM photos; SELECT 'Categories: ' || COUNT(*) FROM categories; SELECT 'Tags: ' || COUNT(*) FROM tags;"; \
else \
printf "$(RED)❌ Database not found. Run 'make dev' to create it.\\n$(NC)"; \
fi
##@ Help
help: ## Display this help message
@printf "$(GREEN)Photography Backend Makefile\n$(NC)"
@printf "$(GREEN)============================\n$(NC)"
@printf "$(YELLOW)Simple and functional Makefile for Go backend project with Docker support\n$(NC)\n"
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*?##/ { printf "$(BLUE)%-15s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(GREEN)%s\n$(NC)", substr($$0, 5) } ' $(MAKEFILE_LIST)
@printf "\n$(YELLOW)Examples:\n$(NC)"
@printf "$(BLUE) make dev$(NC) - Start development server\n"
@printf "$(BLUE) make dev-up$(NC) - Start development environment\n"
@printf "$(BLUE) make build$(NC) - Build the application\n"
@printf "$(BLUE) make docker-build$(NC) - Build Docker image\n"
@printf "$(BLUE) make status$(NC) - Check application status\n"
@printf "$(BLUE) make health$(NC) - Check service health\n"
@printf "\n$(GREEN)For more information, visit: https://github.com/iriver/photography\n$(NC)"

67
backend-old/go.mod Normal file
View File

@ -0,0 +1,67 @@
module photography-backend
go 1.23.0
toolchain go1.24.4
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.39.0
golang.org/x/text v0.26.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0
)
require (
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

155
backend-old/go.sum Normal file
View File

@ -0,0 +1,155 @@
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -0,0 +1,229 @@
package config
import (
"fmt"
"time"
"github.com/spf13/viper"
)
// Config 应用配置
type Config struct {
App AppConfig `mapstructure:"app"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
Storage StorageConfig `mapstructure:"storage"`
Upload UploadConfig `mapstructure:"upload"`
Logger LoggerConfig `mapstructure:"logger"`
CORS CORSConfig `mapstructure:"cors"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
}
// AppConfig 应用配置
type AppConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
Environment string `mapstructure:"environment"`
Port int `mapstructure:"port"`
Debug bool `mapstructure:"debug"`
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
SSLMode string `mapstructure:"ssl_mode"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"`
}
// RedisConfig Redis配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
Database int `mapstructure:"database"`
PoolSize int `mapstructure:"pool_size"`
MinIdleConns int `mapstructure:"min_idle_conns"`
}
// JWTConfig JWT配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpiresIn string `mapstructure:"expires_in"`
RefreshExpiresIn string `mapstructure:"refresh_expires_in"`
}
// StorageConfig 存储配置
type StorageConfig struct {
Type string `mapstructure:"type"`
Local LocalConfig `mapstructure:"local"`
S3 S3Config `mapstructure:"s3"`
}
// LocalConfig 本地存储配置
type LocalConfig struct {
BasePath string `mapstructure:"base_path"`
BaseURL string `mapstructure:"base_url"`
}
// S3Config S3存储配置
type S3Config struct {
Region string `mapstructure:"region"`
Bucket string `mapstructure:"bucket"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Endpoint string `mapstructure:"endpoint"`
}
// UploadConfig 上传配置
type UploadConfig struct {
MaxFileSize int64 `mapstructure:"max_file_size"`
AllowedTypes []string `mapstructure:"allowed_types"`
ThumbnailSizes []ThumbnailSize `mapstructure:"thumbnail_sizes"`
}
// ThumbnailSize 缩略图尺寸
type ThumbnailSize struct {
Name string `mapstructure:"name"`
Width int `mapstructure:"width"`
Height int `mapstructure:"height"`
}
// LoggerConfig 日志配置
type LoggerConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Output string `mapstructure:"output"`
Filename string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// CORSConfig CORS配置
type CORSConfig struct {
AllowedOrigins []string `mapstructure:"allowed_origins"`
AllowedMethods []string `mapstructure:"allowed_methods"`
AllowedHeaders []string `mapstructure:"allowed_headers"`
AllowCredentials bool `mapstructure:"allow_credentials"`
}
// RateLimitConfig 限流配置
type RateLimitConfig struct {
Enabled bool `mapstructure:"enabled"`
RequestsPerMinute int `mapstructure:"requests_per_minute"`
Burst int `mapstructure:"burst"`
}
// LoadConfig 加载配置
func LoadConfig(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
// 设置环境变量前缀
viper.SetEnvPrefix("PHOTOGRAPHY")
viper.AutomaticEnv()
// 环境变量替换配置
viper.BindEnv("database.host", "DB_HOST")
viper.BindEnv("database.port", "DB_PORT")
viper.BindEnv("database.username", "DB_USER")
viper.BindEnv("database.password", "DB_PASSWORD")
viper.BindEnv("database.database", "DB_NAME")
viper.BindEnv("redis.host", "REDIS_HOST")
viper.BindEnv("redis.port", "REDIS_PORT")
viper.BindEnv("redis.password", "REDIS_PASSWORD")
viper.BindEnv("jwt.secret", "JWT_SECRET")
viper.BindEnv("storage.s3.access_key", "AWS_ACCESS_KEY_ID")
viper.BindEnv("storage.s3.secret_key", "AWS_SECRET_ACCESS_KEY")
viper.BindEnv("app.port", "PORT")
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// 验证配置
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
return &config, nil
}
// validateConfig 验证配置
func validateConfig(config *Config) error {
if config.App.Name == "" {
return fmt.Errorf("app name is required")
}
if config.Database.Host == "" {
return fmt.Errorf("database host is required")
}
if config.JWT.Secret == "" {
return fmt.Errorf("jwt secret is required")
}
return nil
}
// GetJWTExpiration 获取JWT过期时间
func (c *Config) GetJWTExpiration() time.Duration {
duration, err := time.ParseDuration(c.JWT.ExpiresIn)
if err != nil {
return 24 * time.Hour // 默认24小时
}
return duration
}
// GetJWTRefreshExpiration 获取JWT刷新过期时间
func (c *Config) GetJWTRefreshExpiration() time.Duration {
duration, err := time.ParseDuration(c.JWT.RefreshExpiresIn)
if err != nil {
return 7 * 24 * time.Hour // 默认7天
}
return duration
}
// GetDatabaseDSN 获取数据库DSN
func (c *Config) GetDatabaseDSN() string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
c.Database.Host,
c.Database.Port,
c.Database.Username,
c.Database.Password,
c.Database.Database,
c.Database.SSLMode,
)
}
// GetRedisAddr 获取Redis地址
func (c *Config) GetRedisAddr() string {
return fmt.Sprintf("%s:%d", c.Redis.Host, c.Redis.Port)
}
// GetServerAddr 获取服务器地址
func (c *Config) GetServerAddr() string {
return fmt.Sprintf(":%d", c.App.Port)
}
// IsDevelopment 是否为开发环境
func (c *Config) IsDevelopment() bool {
return c.App.Environment == "development"
}
// IsProduction 是否为生产环境
func (c *Config) IsProduction() bool {
return c.App.Environment == "production"
}

View File

@ -0,0 +1,165 @@
package response
import (
"net/http"
"time"
)
// Response 统一响应结构
type Response struct {
Success bool `json:"success"`
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
// Meta 元数据
type Meta struct {
Timestamp string `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
}
// PaginatedResponse 分页响应
type PaginatedResponse struct {
Success bool `json:"success"`
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Pagination *Pagination `json:"pagination"`
Meta *Meta `json:"meta,omitempty"`
}
// Pagination 分页信息
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Success 成功响应
func Success(data interface{}) *Response {
return &Response{
Success: true,
Code: http.StatusOK,
Message: "Success",
Data: data,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Error 错误响应
func Error(code int, message string) *Response {
return &Response{
Success: false,
Code: code,
Message: message,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Created 创建成功响应
func Created(data interface{}) *Response {
return &Response{
Success: true,
Code: http.StatusCreated,
Message: "Created successfully",
Data: data,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Updated 更新成功响应
func Updated(data interface{}) *Response {
return &Response{
Success: true,
Code: http.StatusOK,
Message: "Updated successfully",
Data: data,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Deleted 删除成功响应
func Deleted() *Response {
return &Response{
Success: true,
Code: http.StatusOK,
Message: "Deleted successfully",
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Paginated 分页响应
func Paginated(data interface{}, page, limit int, total int64) *PaginatedResponse {
totalPages := int((total + int64(limit) - 1) / int64(limit))
return &PaginatedResponse{
Success: true,
Code: http.StatusOK,
Message: "Success",
Data: data,
Pagination: &Pagination{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
},
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// BadRequest 400错误
func BadRequest(message string) *Response {
return Error(http.StatusBadRequest, message)
}
// Unauthorized 401错误
func Unauthorized(message string) *Response {
return Error(http.StatusUnauthorized, message)
}
// Forbidden 403错误
func Forbidden(message string) *Response {
return Error(http.StatusForbidden, message)
}
// NotFound 404错误
func NotFound(message string) *Response {
return Error(http.StatusNotFound, message)
}
// InternalServerError 500错误
func InternalServerError(message string) *Response {
return Error(http.StatusInternalServerError, message)
}
// ValidationError 验证错误
func ValidationError(errors map[string]string) *Response {
return &Response{
Success: false,
Code: http.StatusUnprocessableEntity,
Message: "Validation failed",
Data: errors,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,197 +1,102 @@
# Photography Backend Makefile
# Simple and functional Makefile for Go backend project with Docker support
.PHONY: help dev dev-up dev-down build clean docker-build docker-run prod-up prod-down status health fmt mod
# 默认配置
BINARY_NAME=photography-api
BUILD_DIR=bin
CONFIG_FILE=etc/photographyapi-api.yaml
# Color definitions
GREEN := \033[0;32m
YELLOW := \033[1;33m
BLUE := \033[0;34m
RED := \033[0;31m
NC := \033[0m # No Color
# 环境变量
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
# Application configuration
APP_NAME := photography-backend
VERSION := 1.0.0
BUILD_TIME := $(shell date +%Y%m%d_%H%M%S)
LDFLAGS := -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)
# Build configuration
BUILD_DIR := bin
MAIN_FILE := cmd/server/main.go
# Database configuration
DB_URL := postgres://postgres:password@localhost:5432/photography?sslmode=disable
MIGRATION_DIR := migrations
# Default target
.DEFAULT_GOAL := help
##@ Development Environment Commands
dev: ## Start development server with SQLite database
@printf "$(GREEN)🚀 Starting development server with SQLite...\n$(NC)"
@go run cmd/server/main_with_db.go
dev-simple: ## Start simple development server (mock data)
@printf "$(GREEN)🚀 Starting simple development server...\n$(NC)"
@go run cmd/server/simple_main.go
dev-up: ## Start development environment with Docker
@printf "$(GREEN)🐳 Starting development environment...\n$(NC)"
@docker-compose -f docker-compose.dev.yml up -d
@printf "$(GREEN)✅ Development environment started successfully!\n$(NC)"
dev-down: ## Stop development environment
@printf "$(GREEN)🛑 Stopping development environment...\n$(NC)"
@docker-compose -f docker-compose.dev.yml down
@printf "$(GREEN)✅ Development environment stopped!\n$(NC)"
##@ Build Commands
build: ## Build the Go application
@printf "$(GREEN)🔨 Building $(APP_NAME)...\n$(NC)"
# 构建
build:
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
@CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_FILE)
@printf "$(GREEN)✅ Build completed: $(BUILD_DIR)/$(APP_NAME)\n$(NC)"
go build -o $(BUILD_DIR)/$(BINARY_NAME) cmd/api/main.go
clean: ## Clean build artifacts
@printf "$(GREEN)🧹 Cleaning build files...\n$(NC)"
# 运行
run:
@echo "Running $(BINARY_NAME)..."
@./$(BUILD_DIR)/$(BINARY_NAME) -f $(CONFIG_FILE)
# 开发模式(构建并运行)
dev: build run
# 快速启动(跳过构建)
quick:
@echo "Quick start $(BINARY_NAME)..."
@go run cmd/api/main.go -f $(CONFIG_FILE)
# 安装依赖
install:
@echo "Installing dependencies..."
@go mod tidy
# 代码生成
gen:
@echo "Generating code..."
@goctl api go -api api/desc/photography.api -dir ./ --style=goZero
# 生成模型
gen-model:
@echo "Generating models..."
@goctl model mysql ddl -src internal/model/sql/user.sql -dir internal/model/
@goctl model mysql ddl -src internal/model/sql/photo.sql -dir internal/model/
@goctl model mysql ddl -src internal/model/sql/category.sql -dir internal/model/
# 清理
clean:
@echo "Cleaning..."
@rm -rf $(BUILD_DIR)
@rm -f coverage.out coverage.html
@printf "$(GREEN)✅ Clean completed!\n$(NC)"
@go clean
##@ Docker Commands
docker-build: ## Build Docker image
@printf "$(GREEN)🐳 Building Docker image...\n$(NC)"
@docker build -t $(APP_NAME):$(VERSION) .
@docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest
@printf "$(GREEN)✅ Docker image built: $(APP_NAME):$(VERSION)\n$(NC)"
docker-run: ## Run application in Docker container
@printf "$(GREEN)🐳 Running Docker container...\n$(NC)"
@docker-compose up -d
@printf "$(GREEN)✅ Docker container started!\n$(NC)"
##@ Production Commands
prod-up: ## Start production environment
@printf "$(GREEN)🚀 Starting production environment...\n$(NC)"
@docker-compose up -d
@printf "$(GREEN)✅ Production environment started!\n$(NC)"
prod-down: ## Stop production environment
@printf "$(GREEN)🛑 Stopping production environment...\n$(NC)"
@docker-compose down
@printf "$(GREEN)✅ Production environment stopped!\n$(NC)"
##@ Health Check & Status Commands
status: ## Check application and services status
@printf "$(GREEN)📊 Checking application status...\n$(NC)"
@printf "$(BLUE)Docker containers:$(NC)\n"
@docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "($(APP_NAME)|postgres|redis)" || echo "No containers running"
@printf "$(BLUE)Application build:$(NC)\n"
@if [ -f "$(BUILD_DIR)/$(APP_NAME)" ]; then \
printf "$(GREEN)✅ Binary exists: $(BUILD_DIR)/$(APP_NAME)\n$(NC)"; \
ls -lh $(BUILD_DIR)/$(APP_NAME); \
else \
printf "$(RED)❌ Binary not found. Run 'make build' first.\n$(NC)"; \
fi
health: ## Check health of running services
@printf "$(GREEN)🏥 Checking service health...\n$(NC)"
@printf "$(BLUE)Testing application endpoint...\n$(NC)"
@curl -f http://localhost:8080/health 2>/dev/null && printf "$(GREEN)✅ Application is healthy\n$(NC)" || printf "$(RED)❌ Application is not responding\n$(NC)"
@printf "$(BLUE)Database connection...\n$(NC)"
@docker exec photography-postgres pg_isready -U postgres 2>/dev/null && printf "$(GREEN)✅ Database is ready\n$(NC)" || printf "$(RED)❌ Database is not ready\n$(NC)"
##@ Code Quality Commands
fmt: ## Format Go code
@printf "$(GREEN)🎨 Formatting Go code...\n$(NC)"
@go fmt ./...
@printf "$(GREEN)✅ Code formatted!\n$(NC)"
mod: ## Tidy Go modules
@printf "$(GREEN)📦 Tidying Go modules...\n$(NC)"
@go mod tidy
@go mod download
@printf "$(GREEN)✅ Modules tidied!\n$(NC)"
lint: ## Run code linter
@printf "$(GREEN)🔍 Running linter...\n$(NC)"
# 代码检查
lint:
@echo "Running linter..."
@golangci-lint run
@printf "$(GREEN)✅ Linting completed!\n$(NC)"
test: ## Run tests
@printf "$(GREEN)🧪 Running tests...\n$(NC)"
# 格式化代码
fmt:
@echo "Formatting code..."
@go fmt ./...
# 运行测试
test:
@echo "Running tests..."
@go test -v ./...
@printf "$(GREEN)✅ Tests completed!\n$(NC)"
##@ Utility Commands
# 创建必要目录
setup:
@echo "Setting up directories..."
@mkdir -p data uploads $(BUILD_DIR)
install: ## Install dependencies
@printf "$(GREEN)📦 Installing dependencies...\n$(NC)"
@go mod download
@go mod tidy
@printf "$(GREEN)✅ Dependencies installed!\n$(NC)"
# 健康检查
status:
@echo "API Status:"
@curl -s http://localhost:8080/api/v1/health || echo "API is not running"
logs: ## Show application logs
@printf "$(GREEN)📄 Showing application logs...\n$(NC)"
@docker-compose logs -f $(APP_NAME)
# 部署准备
deploy-prep: clean install lint test build
@echo "Deployment preparation complete."
migrate-up: ## Run database migrations
@printf "$(GREEN)🗄️ Running database migrations...\n$(NC)"
@migrate -path $(MIGRATION_DIR) -database "$(DB_URL)" up
@printf "$(GREEN)✅ Migrations completed!\n$(NC)"
# 显示帮助
help:
@echo "Available commands:"
@echo " build - Build the application"
@echo " run - Run the application"
@echo " dev - Build and run in development mode"
@echo " quick - Quick start without building"
@echo " install - Install dependencies"
@echo " gen - Generate API code"
@echo " gen-model - Generate model code"
@echo " clean - Clean build artifacts"
@echo " lint - Run code linter"
@echo " fmt - Format code"
@echo " test - Run tests"
@echo " setup - Create necessary directories"
@echo " status - Check API status"
@echo " deploy-prep - Prepare for deployment"
@echo " help - Show this help message"
migrate-down: ## Rollback database migrations
@printf "$(GREEN)🗄️ Rolling back database migrations...\n$(NC)"
@migrate -path $(MIGRATION_DIR) -database "$(DB_URL)" down
@printf "$(GREEN)✅ Migrations rolled back!\n$(NC)"
##@ Database Commands
db-reset: ## Reset SQLite database (delete and recreate)
@printf "$(GREEN)🗄️ Resetting SQLite database...\n$(NC)"
@rm -f photography.db
@printf "$(GREEN)✅ Database reset! Will be recreated on next startup.\n$(NC)"
db-backup: ## Backup SQLite database
@printf "$(GREEN)💾 Backing up SQLite database...\n$(NC)"
@cp photography.db photography_backup_$(BUILD_TIME).db
@printf "$(GREEN)✅ Database backed up to photography_backup_$(BUILD_TIME).db\n$(NC)"
db-shell: ## Open SQLite database shell
@printf "$(GREEN)🐚 Opening SQLite database shell...\n$(NC)"
@sqlite3 photography.db
db-status: ## Show database status and table info
@printf "$(GREEN)📊 Database status:\n$(NC)"
@if [ -f "photography.db" ]; then \
printf "$(BLUE)Database file: photography.db ($(shell ls -lh photography.db | awk '{print $$5}'))\\n$(NC)"; \
printf "$(BLUE)Tables:\\n$(NC)"; \
sqlite3 photography.db ".tables"; \
printf "$(BLUE)Row counts:\\n$(NC)"; \
sqlite3 photography.db "SELECT 'Users: ' || COUNT(*) FROM users; SELECT 'Photos: ' || COUNT(*) FROM photos; SELECT 'Categories: ' || COUNT(*) FROM categories; SELECT 'Tags: ' || COUNT(*) FROM tags;"; \
else \
printf "$(RED)❌ Database not found. Run 'make dev' to create it.\\n$(NC)"; \
fi
##@ Help
help: ## Display this help message
@printf "$(GREEN)Photography Backend Makefile\n$(NC)"
@printf "$(GREEN)============================\n$(NC)"
@printf "$(YELLOW)Simple and functional Makefile for Go backend project with Docker support\n$(NC)\n"
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*?##/ { printf "$(BLUE)%-15s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(GREEN)%s\n$(NC)", substr($$0, 5) } ' $(MAKEFILE_LIST)
@printf "\n$(YELLOW)Examples:\n$(NC)"
@printf "$(BLUE) make dev$(NC) - Start development server\n"
@printf "$(BLUE) make dev-up$(NC) - Start development environment\n"
@printf "$(BLUE) make build$(NC) - Build the application\n"
@printf "$(BLUE) make docker-build$(NC) - Build Docker image\n"
@printf "$(BLUE) make status$(NC) - Check application status\n"
@printf "$(BLUE) make health$(NC) - Check service health\n"
@printf "\n$(GREEN)For more information, visit: https://github.com/iriver/photography\n$(NC)"
.PHONY: build run dev quick install gen gen-model clean lint fmt test setup status deploy-prep help

48
backend/api/desc/auth.api Normal file
View File

@ -0,0 +1,48 @@
syntax = "v1"
import "common.api"
// 登录请求
type LoginRequest {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
// 登录响应
type LoginResponse {
BaseResponse
Data LoginData `json:"data"`
}
type LoginData {
Token string `json:"token"`
User User `json:"user"`
}
// 注册请求
type RegisterRequest {
Username string `json:"username" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
}
// 注册响应
type RegisterResponse {
BaseResponse
Data User `json:"data"`
}
// 认证接口
@server(
group: auth
prefix: /api/v1/auth
)
service photography-api {
@doc "用户登录"
@handler login
post /login (LoginRequest) returns (LoginResponse)
@doc "用户注册"
@handler register
post /register (RegisterRequest) returns (RegisterResponse)
}

View File

@ -0,0 +1,96 @@
syntax = "v1"
import "common.api"
// 分类管理接口
// 获取分类列表请求
type GetCategoryListRequest {
PageRequest
Keyword string `form:"keyword,optional"`
}
// 获取分类列表响应
type GetCategoryListResponse {
BaseResponse
Data CategoryListData `json:"data"`
}
type CategoryListData {
PageResponse
Categories []Category `json:"categories"`
}
// 获取分类详情请求
type GetCategoryRequest {
Id int64 `path:"id"`
}
// 获取分类详情响应
type GetCategoryResponse {
BaseResponse
Data Category `json:"data"`
}
// 创建分类请求
type CreateCategoryRequest {
Name string `json:"name" validate:"required"`
Description string `json:"description,optional"`
}
// 创建分类响应
type CreateCategoryResponse {
BaseResponse
Data Category `json:"data"`
}
// 更新分类请求
type UpdateCategoryRequest {
Id int64 `path:"id"`
Name string `json:"name,optional"`
Description string `json:"description,optional"`
}
// 更新分类响应
type UpdateCategoryResponse {
BaseResponse
Data Category `json:"data"`
}
// 删除分类请求
type DeleteCategoryRequest {
Id int64 `path:"id"`
}
// 删除分类响应
type DeleteCategoryResponse {
BaseResponse
}
// 分类管理接口组
@server(
group: category
prefix: /api/v1/categories
jwt: Auth
)
service photography-api {
@doc "获取分类列表"
@handler getCategoryList
get / (GetCategoryListRequest) returns (GetCategoryListResponse)
@doc "创建分类"
@handler createCategory
post / (CreateCategoryRequest) returns (CreateCategoryResponse)
@doc "获取分类详情"
@handler getCategory
get /:id (GetCategoryRequest) returns (GetCategoryResponse)
@doc "更新分类"
@handler updateCategory
put /:id (UpdateCategoryRequest) returns (UpdateCategoryResponse)
@doc "删除分类"
@handler deleteCategory
delete /:id (DeleteCategoryRequest) returns (DeleteCategoryResponse)
}

View File

@ -0,0 +1,53 @@
syntax = "v1"
// 公共响应结构
type BaseResponse {
Code int `json:"code"`
Message string `json:"message"`
}
// 分页请求
type PageRequest {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
}
// 分页响应
type PageResponse {
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// 用户信息
type User {
Id int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Status int `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// 照片信息
type Photo {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
FilePath string `json:"file_path"`
ThumbnailPath string `json:"thumbnail_path"`
UserId int64 `json:"user_id"`
CategoryId int64 `json:"category_id"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// 分类信息
type Category {
Id int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}

100
backend/api/desc/photo.api Normal file
View File

@ -0,0 +1,100 @@
syntax = "v1"
import "common.api"
// 照片管理接口
// 获取照片列表请求
type GetPhotoListRequest {
PageRequest
CategoryId int64 `form:"category_id,optional"`
UserId int64 `form:"user_id,optional"`
Keyword string `form:"keyword,optional"`
}
// 获取照片列表响应
type GetPhotoListResponse {
BaseResponse
Data PhotoListData `json:"data"`
}
type PhotoListData {
PageResponse
Photos []Photo `json:"photos"`
}
// 获取照片详情请求
type GetPhotoRequest {
Id int64 `path:"id"`
}
// 获取照片详情响应
type GetPhotoResponse {
BaseResponse
Data Photo `json:"data"`
}
// 上传照片请求
type UploadPhotoRequest {
Title string `json:"title" validate:"required"`
Description string `json:"description,optional"`
CategoryId int64 `json:"category_id" validate:"required"`
}
// 上传照片响应
type UploadPhotoResponse {
BaseResponse
Data Photo `json:"data"`
}
// 更新照片请求
type UpdatePhotoRequest {
Id int64 `path:"id"`
Title string `json:"title,optional"`
Description string `json:"description,optional"`
CategoryId int64 `json:"category_id,optional"`
}
// 更新照片响应
type UpdatePhotoResponse {
BaseResponse
Data Photo `json:"data"`
}
// 删除照片请求
type DeletePhotoRequest {
Id int64 `path:"id"`
}
// 删除照片响应
type DeletePhotoResponse {
BaseResponse
}
// 照片管理接口组
@server(
group: photo
prefix: /api/v1/photos
jwt: Auth
)
service photography-api {
@doc "获取照片列表"
@handler getPhotoList
get / (GetPhotoListRequest) returns (GetPhotoListResponse)
@doc "上传照片"
@handler uploadPhoto
post / (UploadPhotoRequest) returns (UploadPhotoResponse)
@doc "获取照片详情"
@handler getPhoto
get /:id (GetPhotoRequest) returns (GetPhotoResponse)
@doc "更新照片"
@handler updatePhoto
put /:id (UpdatePhotoRequest) returns (UpdatePhotoResponse)
@doc "删除照片"
@handler deletePhoto
delete /:id (DeletePhotoRequest) returns (DeletePhotoResponse)
}

View File

@ -0,0 +1,33 @@
syntax = "v1"
info (
title: "Photography Portfolio API"
desc: "摄影作品集 API 服务"
author: "Photography Team"
email: "team@photography.com"
version: "v1.0.0"
)
import "common.api"
import "auth.api"
import "user.api"
import "photo.api"
import "category.api"
// JWT 认证配置
@server (
jwt: Auth
)
service photography-api { // 健康检查接口 (无需认证)}
// 健康检查接口 (无需认证)
@server (
group: health
prefix: /api/v1
)
service photography-api {
@doc "健康检查"
@handler health
get /health returns (BaseResponse)
}

99
backend/api/desc/user.api Normal file
View File

@ -0,0 +1,99 @@
syntax = "v1"
import "common.api"
// 用户管理接口
// 获取用户列表请求
type GetUserListRequest {
PageRequest
Keyword string `form:"keyword,optional"`
}
// 获取用户列表响应
type GetUserListResponse {
BaseResponse
Data UserListData `json:"data"`
}
type UserListData {
PageResponse
Users []User `json:"users"`
}
// 获取用户详情请求
type GetUserRequest {
Id int64 `path:"id"`
}
// 获取用户详情响应
type GetUserResponse {
BaseResponse
Data User `json:"data"`
}
// 创建用户请求
type CreateUserRequest {
Username string `json:"username" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
}
// 创建用户响应
type CreateUserResponse {
BaseResponse
Data User `json:"data"`
}
// 更新用户请求
type UpdateUserRequest {
Id int64 `path:"id"`
Username string `json:"username,optional"`
Email string `json:"email,optional"`
Avatar string `json:"avatar,optional"`
Status int `json:"status,optional"`
}
// 更新用户响应
type UpdateUserResponse {
BaseResponse
Data User `json:"data"`
}
// 删除用户请求
type DeleteUserRequest {
Id int64 `path:"id"`
}
// 删除用户响应
type DeleteUserResponse {
BaseResponse
}
// 用户管理接口组
@server(
group: user
prefix: /api/v1/users
jwt: Auth
)
service photography-api {
@doc "获取用户列表"
@handler getUserList
get / (GetUserListRequest) returns (GetUserListResponse)
@doc "创建用户"
@handler createUser
post / (CreateUserRequest) returns (CreateUserResponse)
@doc "获取用户详情"
@handler getUser
get /:id (GetUserRequest) returns (GetUserResponse)
@doc "更新用户"
@handler updateUser
put /:id (UpdateUserRequest) returns (UpdateUserResponse)
@doc "删除用户"
@handler deleteUser
delete /:id (DeleteUserRequest) returns (DeleteUserResponse)
}

BIN
backend/bin/photography-api Executable file

Binary file not shown.

41
backend/cmd/api/main.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"flag"
"fmt"
"net/http"
"photography-backend/internal/config"
"photography-backend/internal/handler"
"photography-backend/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
)
var configFile = flag.String("f", "etc/photographyapi-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 添加静态文件服务
server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/uploads/:path",
Handler: func(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/uploads/", http.FileServer(http.Dir(c.FileUpload.UploadDir))).ServeHTTP(w, r)
},
})
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}

View File

@ -0,0 +1,3 @@
Name: photography-api
Host: 0.0.0.0
Port: 8888

Some files were not shown because too many files have changed in this diff Show More