refactor: 重构后端架构,采用 Go 风格四层设计模式
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 1m37s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped

## 主要变更

### 🏗️ 架构重构
- 采用简洁的四层架构:API → Service → Repository → Model
- 遵循 Go 语言最佳实践和命名规范
- 实现依赖注入和接口导向设计
- 统一错误处理和响应格式

### 📁 目录结构优化
- 删除重复模块 (application/, domain/, infrastructure/ 等)
- 规范化命名 (使用 Go 风格的 snake_case)
- 清理无关文件 (package.json, node_modules/ 等)
- 新增规范化的测试目录结构

### 📚 文档系统
- 为每个模块创建详细的 CLAUDE.md 指导文件
- 包含开发规范、最佳实践和使用示例
- 支持模块化开发,缩短上下文长度

### 🔧 开发规范
- 统一接口命名规范 (UserServicer, PhotoRepositoryr)
- 标准化错误处理机制
- 完善的测试策略 (单元测试、集成测试、性能测试)
- 规范化的配置管理

### 🗂️ 新增文件
- cmd/server/ - 服务启动入口和配置
- internal/model/ - 数据模型层 (entity, dto, request)
- pkg/ - 共享工具包 (logger, response, validator)
- tests/ - 完整测试结构
- docs/ - API 文档和架构设计
- .gitignore - Git 忽略文件配置

### 🗑️ 清理内容
- 删除 Node.js 相关文件 (package.json, node_modules/)
- 移除重复的架构目录
- 清理临时文件和构建产物
- 删除重复的文档文件

## 影响
- 提高代码可维护性和可扩展性
- 统一开发规范,提升团队协作效率
- 优化项目结构,符合 Go 语言生态标准
- 完善文档体系,降低上手难度
This commit is contained in:
xujiang
2025-07-10 11:20:59 +08:00
parent 540593f1dc
commit a2f2f66f88
40 changed files with 9682 additions and 1798 deletions

313
backend/internal/CLAUDE.md Normal file
View File

@ -0,0 +1,313 @@
# Internal 包 - CLAUDE.md
此文件为 Claude Code 在 internal 包中工作时提供指导。internal 包包含了应用程序的核心业务逻辑,不对外暴露。
## 🎯 模块概览
internal 包采用分层架构设计,包含以下核心层次:
- **API Layer** (`api/`): HTTP 接口层,处理请求和响应
- **Domain Layer** (`domain/`): 领域模型层,定义业务实体和规则
- **Application Layer** (`application/`): 应用服务层,编排业务逻辑
- **Infrastructure Layer** (`infrastructure/`): 基础设施层,外部依赖
- **Shared Layer** (`shared/`): 共享工具和常量
## 📁 目录结构
```
internal/
├── CLAUDE.md # 🔍 当前文件 - Internal 包指南
├── api/ # 🌐 API 接口层
│ ├── CLAUDE.md # API 层开发指南
│ ├── handlers/ # HTTP 处理器
│ ├── middleware/ # 中间件
│ ├── routes/ # 路由配置
│ └── validators/ # 输入验证
├── domain/ # 🏗️ 领域模型层
│ ├── CLAUDE.md # 领域层开发指南
│ ├── entities/ # 业务实体
│ ├── repositories/ # 仓储接口
│ └── services/ # 领域服务接口
├── application/ # 🔧 应用服务层
│ ├── CLAUDE.md # 应用层开发指南
│ ├── dto/ # 数据传输对象
│ └── services/ # 应用服务实现
├── infrastructure/ # 🏭 基础设施层
│ ├── CLAUDE.md # 基础设施指南
│ ├── config/ # 配置管理
│ ├── database/ # 数据库操作
│ ├── cache/ # 缓存服务
│ ├── storage/ # 文件存储
│ └── repositories/ # 仓储实现
└── shared/ # 🔗 共享组件
├── CLAUDE.md # 共享组件指南
├── constants/ # 常量定义
├── errors/ # 错误处理
└── utils/ # 工具函数
```
## 🏗️ 分层架构原则
### 依赖方向
```
API Layer ──→ Application Layer ──→ Domain Layer
↓ ↓ ↑
Infrastructure Layer ──────────────────┘
```
### 层次职责
#### 1. API Layer (api/)
- **职责**: 处理 HTTP 请求和响应
- **依赖**: Application Layer
- **不允许**: 直接调用 Infrastructure Layer 或包含业务逻辑
#### 2. Application Layer (application/)
- **职责**: 编排业务逻辑,协调 Domain Services
- **依赖**: Domain Layer
- **不允许**: 包含具体的基础设施实现
#### 3. Domain Layer (domain/)
- **职责**: 定义业务实体、规则和接口
- **依赖**: 无(最核心层)
- **不允许**: 依赖外部框架或基础设施
#### 4. Infrastructure Layer (infrastructure/)
- **职责**: 实现外部依赖,如数据库、缓存、文件存储
- **依赖**: Domain Layer (实现接口)
- **不允许**: 包含业务逻辑
#### 5. Shared Layer (shared/)
- **职责**: 提供跨层共享的工具和常量
- **依赖**: 最小化依赖
- **原则**: 保持稳定,避免频繁变更
## 🔧 开发规范
### 包导入规则
#### 标准导入顺序
```go
import (
// 1. 标准库
"context"
"fmt"
"time"
// 2. 第三方库
"github.com/gin-gonic/gin"
"gorm.io/gorm"
// 3. 项目内部包 - 按依赖层次
"photography-backend/internal/domain/entities"
"photography-backend/internal/application/dto"
"photography-backend/internal/shared/errors"
)
```
#### 禁止的依赖
- Domain Layer 不能导入 Infrastructure Layer
- Application Layer 不能导入 API Layer
- 下层不能导入上层
### 接口设计原则
#### 1. 依赖倒置
```go
// ✅ 正确:在 domain/repositories 定义接口
type UserRepository interface {
FindByID(id uint) (*entities.User, error)
Save(user *entities.User) error
}
// ✅ 在 infrastructure/repositories 实现接口
type userRepository struct {
db *gorm.DB
}
func (r *userRepository) FindByID(id uint) (*entities.User, error) {
// 实现细节
}
```
#### 2. 接口隔离
```go
// ✅ 正确:小而专注的接口
type UserReader interface {
FindByID(id uint) (*entities.User, error)
FindByEmail(email string) (*entities.User, error)
}
type UserWriter interface {
Save(user *entities.User) error
Delete(id uint) error
}
// ❌ 错误:过大的接口
type UserRepository interface {
// 包含太多方法...
}
```
### 错误处理规范
#### 1. 错误传播
```go
// Domain Layer: 定义业务错误
var ErrUserNotFound = errors.New("user not found")
// Application Layer: 处理和转换错误
func (s *UserService) GetUser(id uint) (*dto.UserResponse, error) {
user, err := s.repo.FindByID(id)
if err != nil {
if errors.Is(err, domain.ErrUserNotFound) {
return nil, shared.ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return s.toDTO(user), nil
}
// API Layer: 转换为 HTTP 响应
func (h *UserHandler) GetUser(c *gin.Context) {
user, err := h.service.GetUser(id)
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, user)
}
```
## 🚀 开发工作流
### 1. 新功能开发流程
#### Step 1: Domain Layer
```bash
# 1. 定义实体
vim internal/domain/entities/new_entity.go
# 2. 定义仓储接口
vim internal/domain/repositories/new_repository.go
# 3. 定义领域服务接口(如需要)
vim internal/domain/services/new_service.go
```
#### Step 2: Application Layer
```bash
# 1. 定义 DTO
vim internal/application/dto/request/new_request.go
vim internal/application/dto/response/new_response.go
# 2. 实现应用服务
vim internal/application/services/new_service.go
```
#### Step 3: Infrastructure Layer
```bash
# 1. 实现仓储
vim internal/infrastructure/repositories/new_repository.go
# 2. 实现其他基础设施(如需要)
```
#### Step 4: API Layer
```bash
# 1. 实现处理器
vim internal/api/handlers/new_handler.go
# 2. 添加路由
vim internal/api/routes/api_v1.go
# 3. 添加验证器(如需要)
vim internal/api/validators/new_validator.go
```
### 2. 测试策略
#### 单元测试
- Domain Layer: 测试业务逻辑
- Application Layer: 测试服务编排
- Infrastructure Layer: 测试数据访问
#### 集成测试
- API Layer: 端到端测试
- Repository Layer: 数据库集成测试
#### 测试隔离
```go
// 使用依赖注入便于测试
type UserService struct {
repo domain.UserRepository
}
// 测试时注入 Mock
func TestUserService_GetUser(t *testing.T) {
mockRepo := &MockUserRepository{}
service := NewUserService(mockRepo)
// 测试逻辑
}
```
## 📋 代码审查清单
### 架构合规性
- [ ] 是否遵循分层架构原则
- [ ] 是否违反依赖方向
- [ ] 接口设计是否合理
- [ ] 错误处理是否完善
### 代码质量
- [ ] 命名是否清晰明确
- [ ] 函数是否单一职责
- [ ] 是否有适当的注释
- [ ] 是否有相应的测试
### 性能考虑
- [ ] 是否有 N+1 查询问题
- [ ] 是否需要添加缓存
- [ ] 数据库操作是否高效
## 🎯 最佳实践
### 1. 保持层次清晰
- 每层只关注自己的职责
- 避免跨层直接调用
- 使用接口解耦
### 2. 依赖注入
- 使用构造函数注入
- 避免全局变量
- 便于测试和替换
### 3. 错误处理
- 使用带类型的错误
- 适当包装错误信息
- 保持错误处理一致性
### 4. 性能优化
- 合理使用缓存
- 数据库查询优化
- 避免过度设计
## 🔍 故障排查
### 常见问题
1. **循环依赖**: 检查包导入关系
2. **接口未实现**: 确认所有方法都已实现
3. **依赖注入失败**: 检查构造函数和依赖配置
4. **测试失败**: 确认 Mock 对象配置正确
### 调试技巧
```go
// 使用结构化日志
logger.Debug("Processing request",
logger.String("handler", "UserHandler.GetUser"),
logger.Uint("user_id", userID),
)
```
这个架构设计确保了代码的可维护性、可测试性和可扩展性。遵循这些指导原则,可以构建出高质量的后端服务。

View File

@ -0,0 +1,565 @@
# HTTP 接口层 - CLAUDE.md
本文件为 Claude Code 在 HTTP 接口层模块中工作时提供指导。
## 🎯 模块概览
HTTP 接口层负责处理所有 HTTP 请求,包括路由定义、请求处理、响应格式化、中间件管理等。
### 职责范围
- 🌐 HTTP 路由定义和管理
- 📝 请求处理和响应格式化
- 🛡️ 中间件管理和应用
- ✅ 请求参数验证
- 📊 HTTP 状态码管理
### 文件结构
```
internal/api/
├── CLAUDE.md # 📋 当前文件 - API 接口指导
├── handlers/ # 🎯 HTTP 处理器
│ ├── user.go # 用户相关处理器
│ ├── photo.go # 照片相关处理器
│ ├── category.go # 分类相关处理器
│ ├── tag.go # 标签相关处理器
│ ├── auth.go # 认证相关处理器
│ ├── upload.go # 上传相关处理器
│ └── health.go # 健康检查处理器
├── middleware/ # 🛡️ 中间件
│ ├── auth.go # 认证中间件
│ ├── cors.go # CORS 中间件
│ ├── logger.go # 日志中间件
│ ├── rate_limit.go # 限流中间件
│ ├── recovery.go # 错误恢复中间件
│ └── validator.go # 验证中间件
├── routes/ # 🗺️ 路由定义
│ ├── v1.go # API v1 路由
│ ├── auth.go # 认证路由
│ ├── public.go # 公共路由
│ └── admin.go # 管理员路由
└── validators/ # ✅ 请求验证器
├── user.go # 用户验证器
├── photo.go # 照片验证器
├── category.go # 分类验证器
└── common.go # 通用验证器
```
## 🎯 处理器模式
### 标准处理器结构
```go
type UserHandler struct {
userService service.UserServiceInterface
logger *zap.Logger
}
func NewUserHandler(userService service.UserServiceInterface, logger *zap.Logger) *UserHandler {
return &UserHandler{
userService: userService,
logger: logger,
}
}
// 创建用户
func (h *UserHandler) Create(c *gin.Context) {
var req validators.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "请求参数无效", err)
return
}
user, err := h.userService.Create(c.Request.Context(), req)
if err != nil {
h.logger.Error("创建用户失败", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "CREATE_USER_FAILED", "创建用户失败", err)
return
}
response.Success(c, user, "用户创建成功")
}
```
### 处理器职责
1. **请求绑定**: 解析 HTTP 请求参数
2. **参数验证**: 验证请求参数的有效性
3. **服务调用**: 调用应用服务层处理业务逻辑
4. **响应格式化**: 格式化响应数据
5. **错误处理**: 处理和记录错误信息
## 🗺️ 路由设计
### API 版本管理
```go
// v1 路由组
v1 := router.Group("/api/v1")
{
// 公共路由 (无需认证)
v1.POST("/auth/login", authHandler.Login)
v1.POST("/auth/register", authHandler.Register)
v1.GET("/photos/public", photoHandler.GetPublicPhotos)
// 需要认证的路由
authenticated := v1.Group("")
authenticated.Use(middleware.AuthRequired())
{
authenticated.GET("/users/profile", userHandler.GetProfile)
authenticated.PUT("/users/profile", userHandler.UpdateProfile)
authenticated.POST("/photos", photoHandler.Create)
authenticated.GET("/photos", photoHandler.List)
}
// 管理员路由
admin := v1.Group("/admin")
admin.Use(middleware.AuthRequired(), middleware.AdminRequired())
{
admin.GET("/users", userHandler.ListUsers)
admin.DELETE("/users/:id", userHandler.DeleteUser)
admin.GET("/photos/all", photoHandler.ListAllPhotos)
}
}
```
### RESTful API 设计
```go
// 用户资源
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/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/categories/:id # 获取分类详情
PUT /api/v1/categories/:id # 更新分类
DELETE /api/v1/categories/:id # 删除分类
```
## 🛡️ 中间件管理
### 认证中间件
```go
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
response.Error(c, http.StatusUnauthorized, "MISSING_TOKEN", "缺少认证令牌", nil)
c.Abort()
return
}
// 验证 JWT Token
claims, err := jwt.VerifyToken(token)
if err != nil {
response.Error(c, http.StatusUnauthorized, "INVALID_TOKEN", "无效的认证令牌", err)
c.Abort()
return
}
// 设置用户信息到上下文
c.Set("user_id", claims.UserID)
c.Set("user_role", claims.Role)
c.Next()
}
}
```
### 日志中间件
```go
func Logger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start)
logger.Info("HTTP Request",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.String("query", c.Request.URL.RawQuery),
zap.Int("status", c.Writer.Status()),
zap.Duration("duration", duration),
zap.String("user_agent", c.Request.UserAgent()),
zap.String("ip", c.ClientIP()),
)
}
}
```
### CORS 中间件
```go
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Max-Age", "86400")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
```
### 限流中间件
```go
func RateLimit(limit int, window time.Duration) gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Every(window), limit)
return func(c *gin.Context) {
if !limiter.Allow() {
response.Error(c, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "请求过于频繁", nil)
c.Abort()
return
}
c.Next()
}
}
```
## ✅ 请求验证
### 验证器结构
```go
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=20"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Role string `json:"role" binding:"oneof=user editor admin"`
}
type UpdateUserRequest struct {
Username string `json:"username,omitempty" binding:"omitempty,min=3,max=20"`
Email string `json:"email,omitempty" binding:"omitempty,email"`
Role string `json:"role,omitempty" binding:"omitempty,oneof=user editor admin"`
}
```
### 自定义验证器
```go
func ValidateUsername(fl validator.FieldLevel) bool {
username := fl.Field().String()
// 用户名只能包含字母、数字和下划线
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
return matched
}
func ValidatePhotoFormat(fl validator.FieldLevel) bool {
format := fl.Field().String()
allowedFormats := []string{"jpg", "jpeg", "png", "gif", "webp"}
for _, allowed := range allowedFormats {
if format == allowed {
return true
}
}
return false
}
```
### 验证错误处理
```go
func FormatValidationErrors(err error) map[string]string {
errors := make(map[string]string)
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrors {
field := strings.ToLower(e.Field())
switch e.Tag() {
case "required":
errors[field] = "此字段为必填项"
case "email":
errors[field] = "请输入有效的邮箱地址"
case "min":
errors[field] = fmt.Sprintf("最小长度为 %s", e.Param())
case "max":
errors[field] = fmt.Sprintf("最大长度为 %s", e.Param())
default:
errors[field] = "字段值无效"
}
}
}
return errors
}
```
## 📊 响应格式
### 统一响应结构
```go
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message"`
Error *ErrorInfo `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
}
```
### 响应帮助函数
```go
func Success(c *gin.Context, data interface{}, message string) {
c.JSON(http.StatusOK, Response{
Success: true,
Data: data,
Message: message,
Timestamp: time.Now(),
})
}
func Error(c *gin.Context, statusCode int, errorCode, message string, details interface{}) {
c.JSON(statusCode, Response{
Success: false,
Message: message,
Error: &ErrorInfo{
Code: errorCode,
Message: message,
Details: details,
},
Timestamp: time.Now(),
})
}
```
### 分页响应
```go
type PaginatedResponse struct {
Data interface{} `json:"data"`
Pagination Pagination `json:"pagination"`
}
type Pagination struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrevious bool `json:"has_previous"`
}
```
## 🔍 错误处理
### 错误分类
```go
const (
// 请求错误
ErrInvalidRequest = "INVALID_REQUEST"
ErrMissingParameter = "MISSING_PARAMETER"
ErrInvalidParameter = "INVALID_PARAMETER"
// 认证错误
ErrMissingToken = "MISSING_TOKEN"
ErrInvalidToken = "INVALID_TOKEN"
ErrTokenExpired = "TOKEN_EXPIRED"
ErrInsufficientPermission = "INSUFFICIENT_PERMISSION"
// 业务错误
ErrUserNotFound = "USER_NOT_FOUND"
ErrUserAlreadyExists = "USER_ALREADY_EXISTS"
ErrPhotoNotFound = "PHOTO_NOT_FOUND"
ErrCategoryNotFound = "CATEGORY_NOT_FOUND"
// 系统错误
ErrInternalServer = "INTERNAL_SERVER_ERROR"
ErrDatabaseError = "DATABASE_ERROR"
ErrStorageError = "STORAGE_ERROR"
)
```
### 错误恢复中间件
```go
func Recovery(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
logger.Error("Panic recovered",
zap.Any("error", err),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
response.Error(c, http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "服务器内部错误", nil)
}
}()
c.Next()
}
}
```
## 📊 性能优化
### 响应压缩
```go
func Compression() gin.HandlerFunc {
return gzip.Gzip(gzip.DefaultCompression)
}
```
### 缓存控制
```go
func CacheControl(maxAge int) gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", maxAge))
c.Next()
}
}
```
### 请求大小限制
```go
func LimitRequestSize(maxSize int64) gin.HandlerFunc {
return func(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
c.Next()
}
}
```
## 🧪 测试策略
### 处理器测试
```go
func TestUserHandler_Create(t *testing.T) {
// 模拟服务
mockService := &MockUserService{}
handler := NewUserHandler(mockService, zap.NewNop())
// 创建测试请求
reqBody := `{"username":"test","email":"test@example.com","password":"123456"}`
req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
// 执行请求
w := httptest.NewRecorder()
router := gin.New()
router.POST("/api/v1/users", handler.Create)
router.ServeHTTP(w, req)
// 验证结果
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "success")
}
```
### 中间件测试
```go
func TestAuthMiddleware(t *testing.T) {
middleware := AuthRequired()
// 测试缺少 token
req, _ := http.NewRequest("GET", "/api/v1/users", nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
middleware(c)
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Contains(t, w.Body.String(), "MISSING_TOKEN")
}
```
## 📚 API 文档
### Swagger 注释
```go
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建新用户账户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param user body validators.CreateUserRequest true "用户信息"
// @Success 200 {object} response.Response{data=models.User} "成功创建用户"
// @Failure 400 {object} response.Response "请求参数错误"
// @Failure 500 {object} response.Response "服务器内部错误"
// @Router /api/v1/users [post]
func (h *UserHandler) Create(c *gin.Context) {
// 处理器实现
}
```
### API 文档生成
```bash
# 安装 swag
go install github.com/swaggo/swag/cmd/swag@latest
# 生成文档
swag init -g cmd/server/main.go
# 启动时访问文档
# http://localhost:8080/swagger/index.html
```
## 🔧 开发工具
### 路由调试
```go
func PrintRoutes(router *gin.Engine) {
routes := router.Routes()
for _, route := range routes {
fmt.Printf("[%s] %s -> %s\n", route.Method, route.Path, route.Handler)
}
}
```
### 请求日志
```go
func RequestLogger() gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[%s] %s %s %d %s\n",
param.TimeStamp.Format("2006-01-02 15:04:05"),
param.Method,
param.Path,
param.StatusCode,
param.Latency,
)
})
}
```
## 💡 最佳实践
### 处理器设计
- 保持处理器轻量,业务逻辑放在服务层
- 统一错误处理和响应格式
- 完善的参数验证和错误提示
- 适当的日志记录
### 中间件使用
- 按需应用中间件,避免过度使用
- 注意中间件的执行顺序
- 错误处理中间件应该最先应用
- 认证中间件应该在业务中间件之前
### 路由设计
- 遵循 RESTful 设计原则
- 合理的路由分组和版本管理
- 清晰的路由命名和结构
- 适当的权限控制
### 性能考虑
- 使用响应压缩减少传输大小
- 适当的缓存控制
- 限制请求大小和频率
- 异步处理耗时操作
本模块是整个 API 的入口和门面,确保接口设计合理、响应格式统一、错误处理完善是项目成功的关键。

View File

@ -120,7 +120,6 @@ type RateLimitConfig struct {
Burst int `mapstructure:"burst"`
}
var AppConfig *Config
// LoadConfig 加载配置
func LoadConfig(configPath string) (*Config, error) {
@ -159,7 +158,6 @@ func LoadConfig(configPath string) (*Config, error) {
return nil, fmt.Errorf("config validation failed: %w", err)
}
AppConfig = &config
return &config, nil
}

View File

@ -0,0 +1,684 @@
# Model Layer - CLAUDE.md
本文件为 Claude Code 在数据模型层中工作时提供指导。
## 🎯 模块概览
Model 层负责定义数据结构、实体模型和数据传输对象,是整个应用的数据基础。
### 主要职责
- 📦 定义数据库实体模型
- 🔄 定义数据传输对象DTO
- 📝 定义请求和响应结构
- 🔗 实现数据转换和验证
- 📊 定义枚举和常量
## 📁 模块结构
```
internal/model/
├── CLAUDE.md # 📋 当前文件 - 数据模型设计指导
├── entity/ # 📦 实体模型
│ ├── user.go # 用户实体
│ ├── photo.go # 照片实体
│ ├── category.go # 分类实体
│ ├── tag.go # 标签实体
│ ├── album.go # 相册实体
│ └── base.go # 基础实体
├── dto/ # 🔄 数据传输对象
│ ├── user_dto.go # 用户 DTO
│ ├── photo_dto.go # 照片 DTO
│ ├── category_dto.go # 分类 DTO
│ ├── auth_dto.go # 认证 DTO
│ └── common_dto.go # 通用 DTO
├── request/ # 📝 请求模型
│ ├── user_request.go # 用户请求
│ ├── photo_request.go # 照片请求
│ ├── category_request.go # 分类请求
│ └── auth_request.go # 认证请求
├── response/ # 📤 响应模型
│ ├── user_response.go # 用户响应
│ ├── photo_response.go # 照片响应
│ ├── category_response.go # 分类响应
│ └── common_response.go # 通用响应
└── types/ # 📊 类型定义
├── enums.go # 枚举类型
├── constants.go # 常量定义
└── custom_types.go # 自定义类型
```
## 🏗️ 实体模型设计
### 基础实体
```go
// entity/base.go - 基础实体模型
package entity
import (
"time"
"gorm.io/gorm"
)
// BaseEntity 基础实体,包含通用字段
type BaseEntity struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
}
// CreatedBy 创建者字段
type CreatedBy struct {
CreatedBy uint `gorm:"column:created_by;index" json:"created_by"`
}
// UpdatedBy 更新者字段
type UpdatedBy struct {
UpdatedBy uint `gorm:"column:updated_by;index" json:"updated_by"`
}
// SoftDelete 软删除字段
type SoftDelete struct {
DeletedBy uint `gorm:"column:deleted_by;index" json:"deleted_by,omitempty"`
}
// TableName 实现 gorm.Tabler 接口
func (BaseEntity) TableName() string {
return ""
}
```
### 用户实体
```go
// entity/user.go - 用户实体
package entity
import (
"time"
"gorm.io/gorm"
)
// User 用户实体
type User struct {
BaseEntity
// 基本信息
Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null" json:"username"`
Email string `gorm:"column:email;type:varchar(100);uniqueIndex;not null" json:"email"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"`
// 个人信息
FirstName string `gorm:"column:first_name;type:varchar(50)" json:"first_name"`
LastName string `gorm:"column:last_name;type:varchar(50)" json:"last_name"`
Avatar string `gorm:"column:avatar;type:varchar(255)" json:"avatar"`
Bio string `gorm:"column:bio;type:text" json:"bio"`
// 系统字段
Role UserRole `gorm:"column:role;type:varchar(20);default:'user'" json:"role"`
Status UserStatus `gorm:"column:status;type:varchar(20);default:'active'" json:"status"`
// 时间字段
LastLoginAt *time.Time `gorm:"column:last_login_at" json:"last_login_at"`
EmailVerifiedAt *time.Time `gorm:"column:email_verified_at" json:"email_verified_at"`
// 关联关系
Photos []Photo `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"photos,omitempty"`
Albums []Album `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"albums,omitempty"`
Categories []Category `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"categories,omitempty"`
}
// UserRole 用户角色枚举
type UserRole string
const (
UserRoleAdmin UserRole = "admin"
UserRoleEditor UserRole = "editor"
UserRoleUser UserRole = "user"
)
// UserStatus 用户状态枚举
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusBanned UserStatus = "banned"
)
// TableName 指定表名
func (User) TableName() string {
return "users"
}
// IsAdmin 检查是否为管理员
func (u *User) IsAdmin() bool {
return u.Role == UserRoleAdmin
}
// IsActive 检查是否为活跃用户
func (u *User) IsActive() bool {
return u.Status == UserStatusActive
}
// GetFullName 获取完整姓名
func (u *User) GetFullName() string {
if u.FirstName == "" && u.LastName == "" {
return u.Username
}
return u.FirstName + " " + u.LastName
}
```
### 照片实体
```go
// entity/photo.go - 照片实体
package entity
import (
"time"
)
// Photo 照片实体
type Photo struct {
BaseEntity
// 基本信息
Title string `gorm:"column:title;type:varchar(255);not null" json:"title"`
Description string `gorm:"column:description;type:text" json:"description"`
// 文件信息
Filename string `gorm:"column:filename;type:varchar(255);not null" json:"filename"`
FilePath string `gorm:"column:file_path;type:varchar(500);not null" json:"file_path"`
FileSize int64 `gorm:"column:file_size;not null" json:"file_size"`
MimeType string `gorm:"column:mime_type;type:varchar(100);not null" json:"mime_type"`
// 图片属性
Width int `gorm:"column:width;default:0" json:"width"`
Height int `gorm:"column:height;default:0" json:"height"`
// 元数据
ExifData string `gorm:"column:exif_data;type:text" json:"exif_data,omitempty"`
TakenAt *time.Time `gorm:"column:taken_at" json:"taken_at"`
Location string `gorm:"column:location;type:varchar(255)" json:"location"`
Camera string `gorm:"column:camera;type:varchar(100)" json:"camera"`
Lens string `gorm:"column:lens;type:varchar(100)" json:"lens"`
// 系统字段
UserID uint `gorm:"column:user_id;not null;index" json:"user_id"`
Status PhotoStatus `gorm:"column:status;type:varchar(20);default:'active'" json:"status"`
// 统计字段
ViewCount int `gorm:"column:view_count;default:0" json:"view_count"`
DownloadCount int `gorm:"column:download_count;default:0" json:"download_count"`
// 关联关系
User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"user,omitempty"`
Categories []Category `gorm:"many2many:photo_categories;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"categories,omitempty"`
Tags []Tag `gorm:"many2many:photo_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"tags,omitempty"`
Albums []Album `gorm:"many2many:album_photos;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"albums,omitempty"`
}
// PhotoStatus 照片状态枚举
type PhotoStatus string
const (
PhotoStatusActive PhotoStatus = "active"
PhotoStatusInactive PhotoStatus = "inactive"
PhotoStatusPrivate PhotoStatus = "private"
PhotoStatusDeleted PhotoStatus = "deleted"
)
// TableName 指定表名
func (Photo) TableName() string {
return "photos"
}
// IsActive 检查照片是否可见
func (p *Photo) IsActive() bool {
return p.Status == PhotoStatusActive
}
// GetURL 获取照片URL
func (p *Photo) GetURL(baseURL string) string {
return baseURL + "/" + p.FilePath
}
// GetThumbnailURL 获取缩略图URL
func (p *Photo) GetThumbnailURL(baseURL string) string {
return baseURL + "/thumbnails/" + p.FilePath
}
```
## 🔄 数据传输对象
### 用户 DTO
```go
// dto/user_dto.go - 用户数据传输对象
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
FirstName string `json:"first_name" binding:"max=50"`
LastName string `json:"last_name" binding:"max=50"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username string `json:"username" binding:"omitempty,min=3,max=50"`
Email string `json:"email" binding:"omitempty,email"`
FirstName string `json:"first_name" binding:"max=50"`
LastName string `json:"last_name" binding:"max=50"`
Avatar string `json:"avatar" binding:"max=255"`
Bio string `json:"bio" binding:"max=500"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
// UserResponse 用户响应
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
Role string `json:"role"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserListResponse 用户列表响应
type UserListResponse struct {
Users []UserResponse `json:"users"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// ListUsersOptions 用户列表选项
type ListUsersOptions struct {
Page int `form:"page" binding:"min=1"`
Limit int `form:"limit" binding:"min=1,max=100"`
Sort string `form:"sort" binding:"oneof=id username email created_at"`
Order string `form:"order" binding:"oneof=asc desc"`
Status string `form:"status" binding:"oneof=active inactive banned"`
Role string `form:"role" binding:"oneof=admin editor user"`
Search string `form:"search" binding:"max=100"`
}
// ToUserResponse 转换为用户响应
func ToUserResponse(user *entity.User) *UserResponse {
return &UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
Avatar: user.Avatar,
Bio: user.Bio,
Role: string(user.Role),
Status: string(user.Status),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
```
### 照片 DTO
```go
// dto/photo_dto.go - 照片数据传输对象
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// CreatePhotoRequest 创建照片请求
type CreatePhotoRequest struct {
Title string `json:"title" binding:"required,max=255"`
Description string `json:"description" binding:"max=1000"`
CategoryIDs []uint `json:"category_ids"`
TagIDs []uint `json:"tag_ids"`
AlbumIDs []uint `json:"album_ids"`
Location string `json:"location" binding:"max=255"`
TakenAt *time.Time `json:"taken_at"`
}
// UpdatePhotoRequest 更新照片请求
type UpdatePhotoRequest struct {
Title string `json:"title" binding:"omitempty,max=255"`
Description string `json:"description" binding:"max=1000"`
CategoryIDs []uint `json:"category_ids"`
TagIDs []uint `json:"tag_ids"`
AlbumIDs []uint `json:"album_ids"`
Location string `json:"location" binding:"max=255"`
TakenAt *time.Time `json:"taken_at"`
Status string `json:"status" binding:"oneof=active inactive private"`
}
// PhotoResponse 照片响应
type PhotoResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Filename string `json:"filename"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
ThumbnailURL string `json:"thumbnail_url"`
Location string `json:"location"`
TakenAt *time.Time `json:"taken_at"`
Status string `json:"status"`
ViewCount int `json:"view_count"`
UserID uint `json:"user_id"`
User *UserResponse `json:"user,omitempty"`
Categories []CategoryResponse `json:"categories,omitempty"`
Tags []TagResponse `json:"tags,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PhotoListResponse 照片列表响应
type PhotoListResponse struct {
Photos []PhotoResponse `json:"photos"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// ListPhotosOptions 照片列表选项
type ListPhotosOptions struct {
Page int `form:"page" binding:"min=1"`
Limit int `form:"limit" binding:"min=1,max=100"`
Sort string `form:"sort" binding:"oneof=id title created_at taken_at view_count"`
Order string `form:"order" binding:"oneof=asc desc"`
Status string `form:"status" binding:"oneof=active inactive private"`
CategoryID uint `form:"category_id"`
TagID uint `form:"tag_id"`
AlbumID uint `form:"album_id"`
UserID uint `form:"user_id"`
Search string `form:"search" binding:"max=100"`
}
// SearchPhotosOptions 搜索照片选项
type SearchPhotosOptions struct {
ListPhotosOptions
Query string `form:"query" binding:"required,min=1,max=100"`
Fields []string `form:"fields" binding:"dive,oneof=title description location"`
DateFrom *time.Time `form:"date_from"`
DateTo *time.Time `form:"date_to"`
}
// ProcessPhotoOptions 处理照片选项
type ProcessPhotoOptions struct {
Resize bool `json:"resize"`
Width int `json:"width"`
Height int `json:"height"`
Quality int `json:"quality"`
Watermark bool `json:"watermark"`
Thumbnail bool `json:"thumbnail"`
}
// ToPhotoResponse 转换为照片响应
func ToPhotoResponse(photo *entity.Photo, baseURL string) *PhotoResponse {
resp := &PhotoResponse{
ID: photo.ID,
Title: photo.Title,
Description: photo.Description,
Filename: photo.Filename,
FilePath: photo.FilePath,
FileSize: photo.FileSize,
MimeType: photo.MimeType,
Width: photo.Width,
Height: photo.Height,
URL: photo.GetURL(baseURL),
ThumbnailURL: photo.GetThumbnailURL(baseURL),
Location: photo.Location,
TakenAt: photo.TakenAt,
Status: string(photo.Status),
ViewCount: photo.ViewCount,
UserID: photo.UserID,
CreatedAt: photo.CreatedAt,
UpdatedAt: photo.UpdatedAt,
}
// 加载关联数据
if photo.User.ID != 0 {
resp.User = ToUserResponse(&photo.User)
}
if len(photo.Categories) > 0 {
resp.Categories = make([]CategoryResponse, len(photo.Categories))
for i, category := range photo.Categories {
resp.Categories[i] = *ToCategoryResponse(&category)
}
}
if len(photo.Tags) > 0 {
resp.Tags = make([]TagResponse, len(photo.Tags))
for i, tag := range photo.Tags {
resp.Tags[i] = *ToTagResponse(&tag)
}
}
return resp
}
```
## 📊 类型定义
### 枚举和常量
```go
// types/enums.go - 枚举类型定义
package types
// SortOrder 排序方向
type SortOrder string
const (
SortOrderAsc SortOrder = "asc"
SortOrderDesc SortOrder = "desc"
)
// FileType 文件类型
type FileType string
const (
FileTypeImage FileType = "image"
FileTypeVideo FileType = "video"
FileTypeAudio FileType = "audio"
FileTypeDocument FileType = "document"
)
// MimeType 媒体类型
const (
MimeTypeJPEG = "image/jpeg"
MimeTypePNG = "image/png"
MimeTypeGIF = "image/gif"
MimeTypeWebP = "image/webp"
)
// 分页常量
const (
DefaultPage = 1
DefaultLimit = 20
MaxLimit = 100
DefaultSort = "created_at"
DefaultOrder = "desc"
)
// 文件大小常量
const (
KB = 1024
MB = KB * 1024
GB = MB * 1024
MaxFileSize = 10 * MB // 10MB
)
```
### 自定义类型
```go
// types/custom_types.go - 自定义类型
package types
import (
"database/sql/driver"
"encoding/json"
"errors"
)
// JSON 自定义JSON类型
type JSON map[string]interface{}
// Value 实现 driver.Valuer 接口
func (j JSON) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
return json.Marshal(j)
}
// Scan 实现 sql.Scanner 接口
func (j *JSON) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return errors.New("failed to scan JSON value")
}
return json.Unmarshal(bytes, j)
}
// StringArray 字符串数组类型
type StringArray []string
// Value 实现 driver.Valuer 接口
func (s StringArray) Value() (driver.Value, error) {
if s == nil {
return nil, nil
}
return json.Marshal(s)
}
// Scan 实现 sql.Scanner 接口
func (s *StringArray) Scan(value interface{}) error {
if value == nil {
*s = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return errors.New("failed to scan StringArray value")
}
return json.Unmarshal(bytes, s)
}
```
## 🎯 验证和转换
### 数据验证
```go
// validation/validators.go - 自定义验证器
package validation
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
// RegisterCustomValidators 注册自定义验证器
func RegisterCustomValidators(v *validator.Validate) {
v.RegisterValidation("username", validateUsername)
v.RegisterValidation("password", validatePassword)
v.RegisterValidation("phone", validatePhone)
v.RegisterValidation("slug", validateSlug)
}
// validateUsername 验证用户名
func validateUsername(fl validator.FieldLevel) bool {
username := fl.Field().String()
// 3-50个字符只允许字母、数字、下划线
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]{3,50}$`, username)
return matched
}
// validatePassword 验证密码强度
func validatePassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
// 至少6个字符包含字母和数字
if len(password) < 6 {
return false
}
hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
return hasLetter && hasNumber
}
// validatePhone 验证手机号
func validatePhone(fl validator.FieldLevel) bool {
phone := fl.Field().String()
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
return matched
}
// validateSlug 验证 URL 友好字符串
func validateSlug(fl validator.FieldLevel) bool {
slug := fl.Field().String()
matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug)
return matched
}
```
## 💡 最佳实践
### 模型设计原则
1. **职责分离**: 实体、DTO、请求、响应分别定义
2. **数据验证**: 使用标签进行数据验证
3. **关联关系**: 合理定义实体间的关联关系
4. **索引优化**: 为常用查询字段添加索引
5. **软删除**: 重要数据使用软删除
### 性能优化
1. **延迟加载**: 避免不必要的关联查询
2. **选择性字段**: 只查询需要的字段
3. **批量操作**: 使用批量插入和更新
4. **缓存策略**: 缓存频繁访问的数据
### 安全考虑
1. **敏感字段**: 密码等敏感字段不参与序列化
2. **输入验证**: 严格验证所有输入数据
3. **权限控制**: 在模型层实现基础权限检查
4. **SQL 注入**: 使用 ORM 防止 SQL 注入
本模块是数据层的基础,确保模型设计的合理性和一致性是关键。

View File

@ -1,85 +0,0 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Category 分类模型
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `gorm:"type:text" json:"description"`
ParentID *uint `json:"parent_id"`
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Color string `gorm:"size:7;default:#3b82f6" json:"color"`
CoverImage string `gorm:"size:500" json:"cover_image"`
Sort int `gorm:"default:0" json:"sort"`
IsActive bool `gorm:"default:true" json:"is_active"`
PhotoCount int `gorm:"-" json:"photo_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 返回分类表名
func (Category) TableName() string {
return "categories"
}
// CreateCategoryRequest 创建分类请求
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required,max=100"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
Color string `json:"color" binding:"omitempty,hexcolor"`
CoverImage string `json:"cover_image" binding:"omitempty,max=500"`
Sort int `json:"sort"`
}
// UpdateCategoryRequest 更新分类请求
type UpdateCategoryRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Description *string `json:"description"`
ParentID *uint `json:"parent_id"`
Color *string `json:"color" binding:"omitempty,hexcolor"`
CoverImage *string `json:"cover_image" binding:"omitempty,max=500"`
Sort *int `json:"sort"`
IsActive *bool `json:"is_active"`
}
// CategoryListParams 分类列表查询参数
type CategoryListParams struct {
IncludeStats bool `form:"include_stats"`
IncludeTree bool `form:"include_tree"`
ParentID uint `form:"parent_id"`
IsActive bool `form:"is_active"`
}
// CategoryResponse 分类响应
type CategoryResponse struct {
*Category
}
// CategoryTreeNode 分类树节点
type CategoryTreeNode struct {
ID uint `json:"id"`
Name string `json:"name"`
PhotoCount int `json:"photo_count"`
Children []CategoryTreeNode `json:"children"`
}
// CategoryListResponse 分类列表响应
type CategoryListResponse struct {
Categories []CategoryResponse `json:"categories"`
Tree []CategoryTreeNode `json:"tree,omitempty"`
Stats *CategoryStats `json:"stats,omitempty"`
}
// CategoryStats 分类统计
type CategoryStats struct {
TotalCategories int `json:"total_categories"`
MaxLevel int `json:"max_level"`
FeaturedCount int `json:"featured_count"`
}

View File

@ -1,99 +0,0 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Photo 照片模型
type Photo struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Description string `gorm:"type:text" json:"description"`
Filename string `gorm:"size:255;not null" json:"filename"`
FilePath string `gorm:"size:500;not null" json:"file_path"`
FileSize int64 `json:"file_size"`
MimeType string `gorm:"size:100" json:"mime_type"`
Width int `json:"width"`
Height int `json:"height"`
CategoryID uint `json:"category_id"`
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Tags []Tag `gorm:"many2many:photo_tags;" json:"tags,omitempty"`
EXIF string `gorm:"type:jsonb" json:"exif"`
TakenAt *time.Time `json:"taken_at"`
Location string `gorm:"size:255" json:"location"`
IsPublic bool `gorm:"default:true" json:"is_public"`
Status string `gorm:"size:20;default:draft" json:"status"`
ViewCount int `gorm:"default:0" json:"view_count"`
LikeCount int `gorm:"default:0" json:"like_count"`
UserID uint `gorm:"not null" json:"user_id"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 返回照片表名
func (Photo) TableName() string {
return "photos"
}
// PhotoStatus 照片状态常量
const (
StatusDraft = "draft"
StatusPublished = "published"
StatusArchived = "archived"
)
// CreatePhotoRequest 创建照片请求
type CreatePhotoRequest struct {
Title string `json:"title" binding:"required,max=255"`
Description string `json:"description"`
CategoryID uint `json:"category_id" binding:"required"`
TagIDs []uint `json:"tag_ids"`
TakenAt *time.Time `json:"taken_at"`
Location string `json:"location" binding:"max=255"`
IsPublic *bool `json:"is_public"`
Status string `json:"status" binding:"omitempty,oneof=draft published archived"`
}
// UpdatePhotoRequest 更新照片请求
type UpdatePhotoRequest struct {
Title *string `json:"title" binding:"omitempty,max=255"`
Description *string `json:"description"`
CategoryID *uint `json:"category_id"`
TagIDs []uint `json:"tag_ids"`
TakenAt *time.Time `json:"taken_at"`
Location *string `json:"location" binding:"omitempty,max=255"`
IsPublic *bool `json:"is_public"`
Status *string `json:"status" binding:"omitempty,oneof=draft published archived"`
}
// PhotoListParams 照片列表查询参数
type PhotoListParams struct {
Page int `form:"page,default=1" binding:"min=1"`
Limit int `form:"limit,default=20" binding:"min=1,max=100"`
CategoryID uint `form:"category_id"`
TagID uint `form:"tag_id"`
UserID uint `form:"user_id"`
Status string `form:"status" binding:"omitempty,oneof=draft published archived"`
Search string `form:"search"`
SortBy string `form:"sort_by,default=created_at" binding:"omitempty,oneof=created_at taken_at title view_count like_count"`
SortOrder string `form:"sort_order,default=desc" binding:"omitempty,oneof=asc desc"`
Year int `form:"year"`
Month int `form:"month" binding:"min=1,max=12"`
}
// PhotoResponse 照片响应
type PhotoResponse struct {
*Photo
ThumbnailURLs map[string]string `json:"thumbnail_urls,omitempty"`
}
// PhotoListResponse 照片列表响应
type PhotoListResponse struct {
Photos []PhotoResponse `json:"photos"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}

View File

@ -1,242 +0,0 @@
package models
import "time"
// 通用请求和响应结构
// ErrorResponse 错误响应
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
// SuccessResponse 成功响应
type SuccessResponse struct {
Message string `json:"message"`
}
// BatchDeleteRequest 批量删除请求
type BatchDeleteRequest struct {
IDs []uint `json:"ids" binding:"required,min=1"`
}
// GenerateSlugRequest 生成slug请求
type GenerateSlugRequest struct {
Name string `json:"name" binding:"required"`
}
// GenerateSlugResponse 生成slug响应
type GenerateSlugResponse struct {
Slug string `json:"slug"`
}
// 照片相关请求
// CreatePhotoRequest 创建照片请求
type CreatePhotoRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
OriginalFilename string `json:"original_filename"`
FileSize int64 `json:"file_size"`
Status string `json:"status" binding:"oneof=draft published archived processing"`
CategoryIDs []uint `json:"category_ids"`
TagIDs []uint `json:"tag_ids"`
Camera string `json:"camera"`
Lens string `json:"lens"`
ISO int `json:"iso"`
Aperture string `json:"aperture"`
ShutterSpeed string `json:"shutter_speed"`
FocalLength string `json:"focal_length"`
TakenAt *time.Time `json:"taken_at"`
}
// UpdatePhotoRequest 更新照片请求
type UpdatePhotoRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
Status *string `json:"status" binding:"omitempty,oneof=draft published archived processing"`
CategoryIDs *[]uint `json:"category_ids"`
TagIDs *[]uint `json:"tag_ids"`
Camera *string `json:"camera"`
Lens *string `json:"lens"`
ISO *int `json:"iso"`
Aperture *string `json:"aperture"`
ShutterSpeed *string `json:"shutter_speed"`
FocalLength *string `json:"focal_length"`
TakenAt *time.Time `json:"taken_at"`
}
// BatchUpdatePhotosRequest 批量更新照片请求
type BatchUpdatePhotosRequest struct {
IDs []uint `json:"ids" binding:"required,min=1"`
Status *string `json:"status" binding:"omitempty,oneof=draft published archived processing"`
CategoryIDs *[]uint `json:"category_ids"`
TagIDs *[]uint `json:"tag_ids"`
}
// PhotoStats 照片统计信息
type PhotoStats struct {
Total int64 `json:"total"`
ThisMonth int64 `json:"this_month"`
Today int64 `json:"today"`
TotalSize int64 `json:"total_size"`
StatusStats map[string]int64 `json:"status_stats"`
}
// 分类相关请求
// CreateCategoryRequest 创建分类请求
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required"`
Slug string `json:"slug" binding:"required"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
}
// UpdateCategoryRequest 更新分类请求
type UpdateCategoryRequest struct {
Name *string `json:"name"`
Slug *string `json:"slug"`
Description *string `json:"description"`
ParentID *uint `json:"parent_id"`
SortOrder *int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
// ReorderCategoriesRequest 重新排序分类请求
type ReorderCategoriesRequest struct {
ParentID *uint `json:"parent_id"`
CategoryIDs []uint `json:"category_ids" binding:"required,min=1"`
}
// CategoryStats 分类统计信息
type CategoryStats struct {
Total int64 `json:"total"`
Active int64 `json:"active"`
TopLevel int64 `json:"top_level"`
PhotoCounts map[string]int64 `json:"photo_counts"`
}
// CategoryTree 分类树结构
type CategoryTree struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
SortOrder int `json:"sort_order"`
IsActive bool `json:"is_active"`
PhotoCount int64 `json:"photo_count"`
Children []CategoryTree `json:"children"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 标签相关请求
// CreateTagRequest 创建标签请求
type CreateTagRequest struct {
Name string `json:"name" binding:"required"`
Slug string `json:"slug" binding:"required"`
Description string `json:"description"`
Color string `json:"color"`
}
// UpdateTagRequest 更新标签请求
type UpdateTagRequest struct {
Name *string `json:"name"`
Slug *string `json:"slug"`
Description *string `json:"description"`
Color *string `json:"color"`
IsActive *bool `json:"is_active"`
}
// TagStats 标签统计信息
type TagStats struct {
Total int64 `json:"total"`
Active int64 `json:"active"`
Used int64 `json:"used"`
Unused int64 `json:"unused"`
AvgPhotosPerTag float64 `json:"avg_photos_per_tag"`
}
// TagWithCount 带照片数量的标签
type TagWithCount struct {
Tag
PhotoCount int64 `json:"photo_count"`
}
// TagCloudItem 标签云项目
type TagCloudItem struct {
Name string `json:"name"`
Slug string `json:"slug"`
Color string `json:"color"`
Count int64 `json:"count"`
}
// 用户相关请求
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"oneof=user editor admin"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
Role *string `json:"role" binding:"omitempty,oneof=user editor admin"`
IsActive *bool `json:"is_active"`
}
// UpdateCurrentUserRequest 更新当前用户请求
type UpdateCurrentUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应
type LoginResponse struct {
User *UserResponse `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
// RefreshTokenRequest 刷新token请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// RefreshTokenResponse 刷新token响应
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
// UserResponse 用户响应(隐藏敏感信息)
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -1,95 +0,0 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Tag 标签模型
type Tag struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;not null;unique" json:"name"`
Color string `gorm:"size:7;default:#6b7280" json:"color"`
UseCount int `gorm:"default:0" json:"use_count"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 返回标签表名
func (Tag) TableName() string {
return "tags"
}
// CreateTagRequest 创建标签请求
type CreateTagRequest struct {
Name string `json:"name" binding:"required,max=50"`
Color string `json:"color" binding:"omitempty,hexcolor"`
}
// UpdateTagRequest 更新标签请求
type UpdateTagRequest struct {
Name *string `json:"name" binding:"omitempty,max=50"`
Color *string `json:"color" binding:"omitempty,hexcolor"`
IsActive *bool `json:"is_active"`
}
// TagListParams 标签列表查询参数
type TagListParams struct {
Page int `form:"page,default=1" binding:"min=1"`
Limit int `form:"limit,default=50" binding:"min=1,max=100"`
Search string `form:"search"`
SortBy string `form:"sort_by,default=use_count" binding:"omitempty,oneof=use_count name created_at"`
SortOrder string `form:"sort_order,default=desc" binding:"omitempty,oneof=asc desc"`
IsActive bool `form:"is_active"`
}
// TagSuggestionsParams 标签建议查询参数
type TagSuggestionsParams struct {
Query string `form:"q" binding:"required"`
Limit int `form:"limit,default=10" binding:"min=1,max=20"`
}
// TagResponse 标签响应
type TagResponse struct {
*Tag
MatchScore float64 `json:"match_score,omitempty"`
}
// TagListResponse 标签列表响应
type TagListResponse struct {
Tags []TagResponse `json:"tags"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
Groups *TagGroups `json:"groups,omitempty"`
}
// TagGroups 标签分组
type TagGroups struct {
Style TagGroup `json:"style"`
Subject TagGroup `json:"subject"`
Technique TagGroup `json:"technique"`
Location TagGroup `json:"location"`
}
// TagGroup 标签组
type TagGroup struct {
Name string `json:"name"`
Count int `json:"count"`
}
// TagCloudItem 标签云项目
type TagCloudItem struct {
ID uint `json:"id"`
Name string `json:"name"`
UseCount int `json:"use_count"`
RelativeSize int `json:"relative_size"`
Color string `json:"color"`
}
// TagCloudResponse 标签云响应
type TagCloudResponse struct {
Tags []TagCloudItem `json:"tags"`
}

View File

@ -1,76 +0,0 @@
package models
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;not null;unique" json:"username"`
Email string `gorm:"size:100;not null;unique" json:"email"`
Password string `gorm:"size:255;not null" json:"-"`
Name string `gorm:"size:100" json:"name"`
Avatar string `gorm:"size:500" json:"avatar"`
Role string `gorm:"size:20;default:user" json:"role"`
IsActive bool `gorm:"default:true" json:"is_active"`
LastLogin *time.Time `json:"last_login"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 返回用户表名
func (User) TableName() string {
return "users"
}
// UserRole 用户角色常量
const (
RoleUser = "user"
RoleEditor = "editor"
RoleAdmin = "admin"
)
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Name string `json:"name" binding:"max=100"`
Role string `json:"role" binding:"omitempty,oneof=user editor admin"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Avatar *string `json:"avatar" binding:"omitempty,max=500"`
IsActive *bool `json:"is_active"`
}
// UpdatePasswordRequest 更新密码请求
type UpdatePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
User *User `json:"user"`
}
// RefreshTokenRequest 刷新令牌请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}

View File

@ -0,0 +1,873 @@
# Repository Layer - CLAUDE.md
本文件为 Claude Code 在数据访问层中工作时提供指导。
## 🎯 模块概览
Repository 层负责数据访问逻辑,提供数据库操作的抽象接口,隔离业务逻辑与数据存储细节。
### 主要职责
- 🔧 提供数据库操作接口
- 📊 实现 CRUD 操作
- 🔍 提供复杂查询支持
- 💾 管理数据库连接和事务
- 🚀 优化查询性能
- 🔄 支持多种数据源
## 📁 模块结构
```
internal/repository/
├── CLAUDE.md # 📋 当前文件 - 数据访问开发指导
├── interfaces/ # 🔗 仓储接口定义
│ ├── user_repository.go # 用户仓储接口
│ ├── photo_repository.go # 照片仓储接口
│ ├── category_repository.go # 分类仓储接口
│ ├── tag_repository.go # 标签仓储接口
│ └── album_repository.go # 相册仓储接口
├── postgres/ # 🐘 PostgreSQL 实现
│ ├── user_repository.go # 用户仓储实现
│ ├── photo_repository.go # 照片仓储实现
│ ├── category_repository.go # 分类仓储实现
│ ├── tag_repository.go # 标签仓储实现
│ ├── album_repository.go # 相册仓储实现
│ └── base_repository.go # 基础仓储实现
├── redis/ # 🟥 Redis 缓存实现
│ ├── user_cache.go # 用户缓存
│ ├── photo_cache.go # 照片缓存
│ └── cache_manager.go # 缓存管理器
├── sqlite/ # 📦 SQLite 实现(开发用)
│ ├── user_repository.go # 用户仓储实现
│ ├── photo_repository.go # 照片仓储实现
│ └── base_repository.go # 基础仓储实现
├── mocks/ # 🧪 模拟对象(测试用)
│ ├── user_repository_mock.go # 用户仓储模拟
│ ├── photo_repository_mock.go # 照片仓储模拟
│ └── generate.go # 模拟对象生成
└── errors.go # 📝 仓储层错误定义
```
## 🔗 接口设计
### 基础仓储接口
```go
// interfaces/base_repository.go - 基础仓储接口
package interfaces
import (
"context"
"gorm.io/gorm"
)
// BaseRepositoryr 基础仓储接口
type BaseRepositoryr[T any] interface {
// 基础 CRUD 操作
Create(ctx context.Context, entity *T) (*T, error)
GetByID(ctx context.Context, id uint) (*T, error)
Update(ctx context.Context, entity *T) (*T, error)
Delete(ctx context.Context, id uint) error
// 批量操作
CreateBatch(ctx context.Context, entities []*T) error
UpdateBatch(ctx context.Context, entities []*T) error
DeleteBatch(ctx context.Context, ids []uint) error
// 查询操作
List(ctx context.Context, opts *ListOptions) ([]*T, int64, error)
Count(ctx context.Context, conditions map[string]interface{}) (int64, error)
Exists(ctx context.Context, conditions map[string]interface{}) (bool, error)
// 事务操作
WithTx(tx *gorm.DB) BaseRepositoryr[T]
Transaction(ctx context.Context, fn func(repo BaseRepositoryr[T]) error) error
}
// ListOptions 查询选项
type ListOptions struct {
Page int `json:"page"`
Limit int `json:"limit"`
Sort string `json:"sort"`
Order string `json:"order"`
Conditions map[string]interface{} `json:"conditions"`
Preloads []string `json:"preloads"`
}
```
### 用户仓储接口
```go
// interfaces/user_repository.go - 用户仓储接口
package interfaces
import (
"context"
"photography-backend/internal/model/entity"
)
// UserRepositoryr 用户仓储接口
type UserRepositoryr interface {
BaseRepositoryr[entity.User]
// 用户特定查询
GetByEmail(ctx context.Context, email string) (*entity.User, error)
GetByUsername(ctx context.Context, username string) (*entity.User, error)
GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error)
// 用户列表查询
ListByRole(ctx context.Context, role entity.UserRole, opts *ListOptions) ([]*entity.User, int64, error)
ListByStatus(ctx context.Context, status entity.UserStatus, opts *ListOptions) ([]*entity.User, int64, error)
SearchUsers(ctx context.Context, keyword string, opts *ListOptions) ([]*entity.User, int64, error)
// 用户统计
CountByRole(ctx context.Context, role entity.UserRole) (int64, error)
CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error)
CountActiveUsers(ctx context.Context) (int64, error)
// 用户状态更新
UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error
UpdateLastLogin(ctx context.Context, id uint) error
// 密码相关
UpdatePassword(ctx context.Context, id uint, hashedPassword string) error
// 软删除恢复
Restore(ctx context.Context, id uint) error
}
```
### 照片仓储接口
```go
// interfaces/photo_repository.go - 照片仓储接口
package interfaces
import (
"context"
"time"
"photography-backend/internal/model/entity"
)
// PhotoRepositoryr 照片仓储接口
type PhotoRepositoryr interface {
BaseRepositoryr[entity.Photo]
// 照片查询
GetByFilename(ctx context.Context, filename string) (*entity.Photo, error)
GetByUserID(ctx context.Context, userID uint) ([]*entity.Photo, error)
ListByUserID(ctx context.Context, userID uint, opts *ListOptions) ([]*entity.Photo, int64, error)
ListByStatus(ctx context.Context, status entity.PhotoStatus, opts *ListOptions) ([]*entity.Photo, int64, error)
// 分类和标签查询
ListByCategory(ctx context.Context, categoryID uint, opts *ListOptions) ([]*entity.Photo, int64, error)
ListByTag(ctx context.Context, tagID uint, opts *ListOptions) ([]*entity.Photo, int64, error)
ListByAlbum(ctx context.Context, albumID uint, opts *ListOptions) ([]*entity.Photo, int64, error)
// 搜索功能
SearchPhotos(ctx context.Context, keyword string, opts *SearchOptions) ([]*entity.Photo, int64, error)
SearchByMetadata(ctx context.Context, metadata map[string]interface{}, opts *ListOptions) ([]*entity.Photo, int64, error)
// 时间范围查询
ListByDateRange(ctx context.Context, startDate, endDate time.Time, opts *ListOptions) ([]*entity.Photo, int64, error)
ListByCreatedDateRange(ctx context.Context, startDate, endDate time.Time, opts *ListOptions) ([]*entity.Photo, int64, error)
// 统计查询
CountByUser(ctx context.Context, userID uint) (int64, error)
CountByStatus(ctx context.Context, status entity.PhotoStatus) (int64, error)
CountByCategory(ctx context.Context, categoryID uint) (int64, error)
CountByTag(ctx context.Context, tagID uint) (int64, error)
// 关联操作
AddCategories(ctx context.Context, photoID uint, categoryIDs []uint) error
RemoveCategories(ctx context.Context, photoID uint, categoryIDs []uint) error
AddTags(ctx context.Context, photoID uint, tagIDs []uint) error
RemoveTags(ctx context.Context, photoID uint, tagIDs []uint) error
// 统计更新
IncrementViewCount(ctx context.Context, id uint) error
IncrementDownloadCount(ctx context.Context, id uint) error
// 批量操作
BatchUpdateStatus(ctx context.Context, ids []uint, status entity.PhotoStatus) error
BatchDelete(ctx context.Context, ids []uint) error
}
// SearchOptions 搜索选项
type SearchOptions struct {
ListOptions
Fields []string `json:"fields"`
DateFrom *time.Time `json:"date_from"`
DateTo *time.Time `json:"date_to"`
MinWidth int `json:"min_width"`
MaxWidth int `json:"max_width"`
MinHeight int `json:"min_height"`
MaxHeight int `json:"max_height"`
}
```
## 🔧 仓储实现
### 基础仓储实现
```go
// postgres/base_repository.go - 基础仓储实现
package postgres
import (
"context"
"errors"
"fmt"
"reflect"
"gorm.io/gorm"
"go.uber.org/zap"
"photography-backend/internal/repository/interfaces"
"photography-backend/pkg/logger"
)
// BaseRepository 基础仓储实现
type BaseRepository[T any] struct {
db *gorm.DB
logger logger.Logger
}
// NewBaseRepository 创建基础仓储
func NewBaseRepository[T any](db *gorm.DB, logger logger.Logger) *BaseRepository[T] {
return &BaseRepository[T]{
db: db,
logger: logger,
}
}
// Create 创建记录
func (r *BaseRepository[T]) Create(ctx context.Context, entity *T) (*T, error) {
if err := r.db.WithContext(ctx).Create(entity).Error; err != nil {
r.logger.Error("failed to create entity", zap.Error(err))
return nil, err
}
return entity, nil
}
// GetByID 根据ID获取记录
func (r *BaseRepository[T]) GetByID(ctx context.Context, id uint) (*T, error) {
var entity T
if err := r.db.WithContext(ctx).First(&entity, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
r.logger.Error("failed to get entity by id", zap.Error(err), zap.Uint("id", id))
return nil, err
}
return &entity, nil
}
// Update 更新记录
func (r *BaseRepository[T]) Update(ctx context.Context, entity *T) (*T, error) {
if err := r.db.WithContext(ctx).Save(entity).Error; err != nil {
r.logger.Error("failed to update entity", zap.Error(err))
return nil, err
}
return entity, nil
}
// Delete 删除记录
func (r *BaseRepository[T]) Delete(ctx context.Context, id uint) error {
var entity T
if err := r.db.WithContext(ctx).Delete(&entity, id).Error; err != nil {
r.logger.Error("failed to delete entity", zap.Error(err), zap.Uint("id", id))
return err
}
return nil
}
// List 列表查询
func (r *BaseRepository[T]) List(ctx context.Context, opts *interfaces.ListOptions) ([]*T, int64, error) {
var entities []*T
var total int64
db := r.db.WithContext(ctx)
// 应用条件
if opts.Conditions != nil {
for key, value := range opts.Conditions {
db = db.Where(key, value)
}
}
// 获取总数
if err := db.Model(new(T)).Count(&total).Error; err != nil {
r.logger.Error("failed to count entities", zap.Error(err))
return nil, 0, err
}
// 应用排序
if opts.Sort != "" {
order := "ASC"
if opts.Order == "desc" {
order = "DESC"
}
db = db.Order(fmt.Sprintf("%s %s", opts.Sort, order))
}
// 应用分页
if opts.Page > 0 && opts.Limit > 0 {
offset := (opts.Page - 1) * opts.Limit
db = db.Offset(offset).Limit(opts.Limit)
}
// 应用预加载
if opts.Preloads != nil {
for _, preload := range opts.Preloads {
db = db.Preload(preload)
}
}
// 查询数据
if err := db.Find(&entities).Error; err != nil {
r.logger.Error("failed to list entities", zap.Error(err))
return nil, 0, err
}
return entities, total, nil
}
// Transaction 事务执行
func (r *BaseRepository[T]) Transaction(ctx context.Context, fn func(repo interfaces.BaseRepositoryr[T]) error) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repo := &BaseRepository[T]{
db: tx,
logger: r.logger,
}
return fn(repo)
})
}
// WithTx 使用事务
func (r *BaseRepository[T]) WithTx(tx *gorm.DB) interfaces.BaseRepositoryr[T] {
return &BaseRepository[T]{
db: tx,
logger: r.logger,
}
}
```
### 用户仓储实现
```go
// postgres/user_repository.go - 用户仓储实现
package postgres
import (
"context"
"errors"
"strings"
"time"
"gorm.io/gorm"
"go.uber.org/zap"
"photography-backend/internal/model/entity"
"photography-backend/internal/repository/interfaces"
"photography-backend/pkg/logger"
)
// UserRepository 用户仓储实现
type UserRepository struct {
*BaseRepository[entity.User]
db *gorm.DB
logger logger.Logger
}
// NewUserRepository 创建用户仓储
func NewUserRepository(db *gorm.DB, logger logger.Logger) interfaces.UserRepositoryr {
return &UserRepository{
BaseRepository: NewBaseRepository[entity.User](db, logger),
db: db,
logger: logger,
}
}
// GetByEmail 根据邮箱获取用户
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
var user entity.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
r.logger.Error("failed to get user by email", zap.Error(err), zap.String("email", email))
return nil, err
}
return &user, nil
}
// GetByUsername 根据用户名获取用户
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
var user entity.User
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
r.logger.Error("failed to get user by username", zap.Error(err), zap.String("username", username))
return nil, err
}
return &user, nil
}
// GetByEmailOrUsername 根据邮箱或用户名获取用户
func (r *UserRepository) GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error) {
var user entity.User
err := r.db.WithContext(ctx).Where("email = ? OR username = ?", emailOrUsername, emailOrUsername).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
r.logger.Error("failed to get user by email or username", zap.Error(err), zap.String("emailOrUsername", emailOrUsername))
return nil, err
}
return &user, nil
}
// SearchUsers 搜索用户
func (r *UserRepository) SearchUsers(ctx context.Context, keyword string, opts *interfaces.ListOptions) ([]*entity.User, int64, error) {
var users []*entity.User
var total int64
db := r.db.WithContext(ctx)
// 搜索条件
searchCondition := fmt.Sprintf("username ILIKE %s OR email ILIKE %s OR first_name ILIKE %s OR last_name ILIKE %s",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
db = db.Where(searchCondition)
// 应用其他条件
if opts.Conditions != nil {
for key, value := range opts.Conditions {
db = db.Where(key, value)
}
}
// 获取总数
if err := db.Model(&entity.User{}).Count(&total).Error; err != nil {
r.logger.Error("failed to count users", zap.Error(err))
return nil, 0, err
}
// 应用排序和分页
if opts.Sort != "" {
order := "ASC"
if opts.Order == "desc" {
order = "DESC"
}
db = db.Order(fmt.Sprintf("%s %s", opts.Sort, order))
}
if opts.Page > 0 && opts.Limit > 0 {
offset := (opts.Page - 1) * opts.Limit
db = db.Offset(offset).Limit(opts.Limit)
}
// 查询数据
if err := db.Find(&users).Error; err != nil {
r.logger.Error("failed to search users", zap.Error(err))
return nil, 0, err
}
return users, total, nil
}
// UpdateStatus 更新用户状态
func (r *UserRepository) UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error {
err := r.db.WithContext(ctx).Model(&entity.User{}).Where("id = ?", id).Update("status", status).Error
if err != nil {
r.logger.Error("failed to update user status", zap.Error(err), zap.Uint("id", id), zap.String("status", string(status)))
return err
}
return nil
}
// UpdateLastLogin 更新最后登录时间
func (r *UserRepository) UpdateLastLogin(ctx context.Context, id uint) error {
now := time.Now()
err := r.db.WithContext(ctx).Model(&entity.User{}).Where("id = ?", id).Update("last_login_at", now).Error
if err != nil {
r.logger.Error("failed to update last login time", zap.Error(err), zap.Uint("id", id))
return err
}
return nil
}
// UpdatePassword 更新密码
func (r *UserRepository) UpdatePassword(ctx context.Context, id uint, hashedPassword string) error {
err := r.db.WithContext(ctx).Model(&entity.User{}).Where("id = ?", id).Update("password", hashedPassword).Error
if err != nil {
r.logger.Error("failed to update password", zap.Error(err), zap.Uint("id", id))
return err
}
return nil
}
// CountByRole 按角色统计用户数
func (r *UserRepository) CountByRole(ctx context.Context, role entity.UserRole) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entity.User{}).Where("role = ?", role).Count(&count).Error
if err != nil {
r.logger.Error("failed to count users by role", zap.Error(err), zap.String("role", string(role)))
return 0, err
}
return count, nil
}
// CountActiveUsers 统计活跃用户数
func (r *UserRepository) CountActiveUsers(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entity.User{}).Where("status = ?", entity.UserStatusActive).Count(&count).Error
if err != nil {
r.logger.Error("failed to count active users", zap.Error(err))
return 0, err
}
return count, nil
}
```
## 💾 Redis 缓存实现
### 缓存管理器
```go
// redis/cache_manager.go - 缓存管理器
package redis
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
"photography-backend/pkg/logger"
)
// CacheManager 缓存管理器
type CacheManager struct {
client *redis.Client
logger logger.Logger
}
// NewCacheManager 创建缓存管理器
func NewCacheManager(client *redis.Client, logger logger.Logger) *CacheManager {
return &CacheManager{
client: client,
logger: logger,
}
}
// Set 设置缓存
func (cm *CacheManager) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
cm.logger.Error("failed to marshal cache value", zap.Error(err), zap.String("key", key))
return err
}
err = cm.client.Set(ctx, key, data, ttl).Err()
if err != nil {
cm.logger.Error("failed to set cache", zap.Error(err), zap.String("key", key))
return err
}
return nil
}
// Get 获取缓存
func (cm *CacheManager) Get(ctx context.Context, key string, dest interface{}) error {
data, err := cm.client.Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return ErrCacheNotFound
}
cm.logger.Error("failed to get cache", zap.Error(err), zap.String("key", key))
return err
}
err = json.Unmarshal([]byte(data), dest)
if err != nil {
cm.logger.Error("failed to unmarshal cache value", zap.Error(err), zap.String("key", key))
return err
}
return nil
}
// Delete 删除缓存
func (cm *CacheManager) Delete(ctx context.Context, key string) error {
err := cm.client.Del(ctx, key).Err()
if err != nil {
cm.logger.Error("failed to delete cache", zap.Error(err), zap.String("key", key))
return err
}
return nil
}
// DeletePattern 批量删除缓存
func (cm *CacheManager) DeletePattern(ctx context.Context, pattern string) error {
keys, err := cm.client.Keys(ctx, pattern).Result()
if err != nil {
cm.logger.Error("failed to get keys by pattern", zap.Error(err), zap.String("pattern", pattern))
return err
}
if len(keys) > 0 {
err = cm.client.Del(ctx, keys...).Err()
if err != nil {
cm.logger.Error("failed to delete keys by pattern", zap.Error(err), zap.String("pattern", pattern))
return err
}
}
return nil
}
// Exists 检查缓存是否存在
func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) {
count, err := cm.client.Exists(ctx, key).Result()
if err != nil {
cm.logger.Error("failed to check cache existence", zap.Error(err), zap.String("key", key))
return false, err
}
return count > 0, nil
}
// SetTTL 设置过期时间
func (cm *CacheManager) SetTTL(ctx context.Context, key string, ttl time.Duration) error {
err := cm.client.Expire(ctx, key, ttl).Err()
if err != nil {
cm.logger.Error("failed to set cache ttl", zap.Error(err), zap.String("key", key))
return err
}
return nil
}
```
### 用户缓存
```go
// redis/user_cache.go - 用户缓存
package redis
import (
"context"
"fmt"
"time"
"photography-backend/internal/model/entity"
"photography-backend/pkg/logger"
)
// UserCache 用户缓存
type UserCache struct {
*CacheManager
ttl time.Duration
}
// NewUserCache 创建用户缓存
func NewUserCache(cm *CacheManager, ttl time.Duration) *UserCache {
return &UserCache{
CacheManager: cm,
ttl: ttl,
}
}
// SetUser 缓存用户
func (uc *UserCache) SetUser(ctx context.Context, user *entity.User) error {
key := fmt.Sprintf("user:id:%d", user.ID)
return uc.Set(ctx, key, user, uc.ttl)
}
// GetUser 获取用户缓存
func (uc *UserCache) GetUser(ctx context.Context, id uint) (*entity.User, error) {
key := fmt.Sprintf("user:id:%d", id)
var user entity.User
err := uc.Get(ctx, key, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// SetUserByEmail 按邮箱缓存用户
func (uc *UserCache) SetUserByEmail(ctx context.Context, user *entity.User) error {
key := fmt.Sprintf("user:email:%s", user.Email)
return uc.Set(ctx, key, user, uc.ttl)
}
// GetUserByEmail 按邮箱获取用户缓存
func (uc *UserCache) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) {
key := fmt.Sprintf("user:email:%s", email)
var user entity.User
err := uc.Get(ctx, key, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// DeleteUser 删除用户缓存
func (uc *UserCache) DeleteUser(ctx context.Context, id uint) error {
key := fmt.Sprintf("user:id:%d", id)
return uc.Delete(ctx, key)
}
// DeleteUserByEmail 按邮箱删除用户缓存
func (uc *UserCache) DeleteUserByEmail(ctx context.Context, email string) error {
key := fmt.Sprintf("user:email:%s", email)
return uc.Delete(ctx, key)
}
// InvalidateUserCache 失效用户相关缓存
func (uc *UserCache) InvalidateUserCache(ctx context.Context, userID uint) error {
pattern := fmt.Sprintf("user:*:%d", userID)
return uc.DeletePattern(ctx, pattern)
}
```
## 🔍 错误处理
### 仓储层错误定义
```go
// errors.go - 仓储层错误定义
package repository
import "errors"
var (
// 通用错误
ErrNotFound = errors.New("record not found")
ErrDuplicateKey = errors.New("duplicate key")
ErrInvalidParameter = errors.New("invalid parameter")
ErrDatabaseError = errors.New("database error")
ErrTransactionError = errors.New("transaction error")
// 缓存错误
ErrCacheNotFound = errors.New("cache not found")
ErrCacheError = errors.New("cache error")
ErrCacheExpired = errors.New("cache expired")
// 连接错误
ErrConnectionFailed = errors.New("connection failed")
ErrConnectionTimeout = errors.New("connection timeout")
)
```
## 🧪 测试
### 仓储测试
```go
// postgres/user_repository_test.go - 用户仓储测试
package postgres
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"photography-backend/internal/model/entity"
"photography-backend/pkg/logger"
)
type UserRepositoryTestSuite struct {
suite.Suite
db *gorm.DB
repo *UserRepository
}
func (suite *UserRepositoryTestSuite) SetupTest() {
// 使用内存数据库进行测试
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
suite.Require().NoError(err)
// 自动迁移
err = db.AutoMigrate(&entity.User{})
suite.Require().NoError(err)
suite.db = db
suite.repo = NewUserRepository(db, logger.NewNoop()).(*UserRepository)
}
func (suite *UserRepositoryTestSuite) TearDownTest() {
sqlDB, _ := suite.db.DB()
sqlDB.Close()
}
func (suite *UserRepositoryTestSuite) TestCreateUser() {
ctx := context.Background()
user := &entity.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Role: entity.UserRoleUser,
Status: entity.UserStatusActive,
}
createdUser, err := suite.repo.Create(ctx, user)
assert.NoError(suite.T(), err)
assert.NotZero(suite.T(), createdUser.ID)
assert.Equal(suite.T(), user.Username, createdUser.Username)
assert.Equal(suite.T(), user.Email, createdUser.Email)
}
func (suite *UserRepositoryTestSuite) TestGetUserByEmail() {
ctx := context.Background()
// 创建测试用户
user := &entity.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Role: entity.UserRoleUser,
Status: entity.UserStatusActive,
}
createdUser, err := suite.repo.Create(ctx, user)
suite.Require().NoError(err)
// 根据邮箱获取用户
foundUser, err := suite.repo.GetByEmail(ctx, user.Email)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), createdUser.ID, foundUser.ID)
assert.Equal(suite.T(), createdUser.Email, foundUser.Email)
}
func TestUserRepositoryTestSuite(t *testing.T) {
suite.Run(t, new(UserRepositoryTestSuite))
}
```
## 💡 最佳实践
### 设计原则
1. **接口隔离**: 定义清晰的仓储接口
2. **依赖倒置**: 依赖接口而非具体实现
3. **单一职责**: 每个仓储只负责一个实体
4. **错误处理**: 统一错误处理和日志记录
5. **事务支持**: 提供事务操作支持
### 性能优化
1. **查询优化**: 使用适当的索引和查询条件
2. **批量操作**: 支持批量插入和更新
3. **缓存策略**: 合理使用缓存减少数据库访问
4. **连接池**: 使用连接池管理数据库连接
5. **预加载**: 避免 N+1 查询问题
### 测试策略
1. **单元测试**: 为每个仓储编写单元测试
2. **集成测试**: 测试数据库交互
3. **模拟对象**: 使用 Mock 对象进行测试
4. **测试数据**: 准备充分的测试数据
5. **性能测试**: 测试查询性能和并发性能
本模块是数据访问的核心,确保数据操作的正确性和性能是关键。

View File

@ -142,7 +142,7 @@ func (r *categoryRepository) GetTree() ([]*models.Category, error) {
// 第一次遍历:建立映射
for _, category := range categories {
categoryMap[category.ID] = category
category.Children = []*models.Category{}
category.Children = []models.Category{}
}
// 第二次遍历:构建树形结构
@ -151,7 +151,7 @@ func (r *categoryRepository) GetTree() ([]*models.Category, error) {
rootCategories = append(rootCategories, category)
} else {
if parent, exists := categoryMap[*category.ParentID]; exists {
parent.Children = append(parent.Children, category)
parent.Children = append(parent.Children, *category)
}
}
}
@ -177,9 +177,11 @@ func (r *categoryRepository) GetStats() (*models.CategoryStats, error) {
var stats models.CategoryStats
// 总分类数
if err := r.db.Model(&models.Category{}).Count(&stats.TotalCategories).Error; err != nil {
var totalCount int64
if err := r.db.Model(&models.Category{}).Count(&totalCount).Error; err != nil {
return nil, fmt.Errorf("failed to count total categories: %w", err)
}
stats.TotalCategories = int(totalCount)
// 计算最大层级
// 这里简化处理,实际应用中可能需要递归查询

View File

@ -0,0 +1,549 @@
# Service Layer - CLAUDE.md
本文件为 Claude Code 在业务逻辑层中工作时提供指导。
## 🎯 模块概览
Service 层是业务逻辑的核心,负责处理业务规则、数据转换和服务编排。
### 主要职责
- 📋 业务逻辑处理和规则验证
- 🔄 数据转换和格式化
- 🧩 服务编排和组合
- 🚀 事务管理和数据一致性
- 🔐 业务权限控制
- 📊 性能优化和缓存策略
## 📁 模块结构
```
internal/service/
├── CLAUDE.md # 📋 当前文件 - 业务逻辑开发指导
├── interfaces.go # 🔗 服务接口定义
├── auth/ # 🔐 认证服务
│ ├── auth_service.go # 认证业务逻辑
│ ├── jwt_service.go # JWT 令牌服务
│ └── auth_service_test.go # 认证服务测试
├── user/ # 👤 用户服务
│ ├── user_service.go # 用户业务逻辑
│ ├── user_validator.go # 用户数据验证
│ └── user_service_test.go # 用户服务测试
├── photo/ # 📸 照片服务
│ ├── photo_service.go # 照片业务逻辑
│ ├── photo_processor.go # 照片处理器
│ └── photo_service_test.go # 照片服务测试
├── category/ # 📂 分类服务
│ ├── category_service.go # 分类业务逻辑
│ └── category_service_test.go # 分类服务测试
└── storage/ # 📁 存储服务
├── storage_service.go # 存储业务逻辑
├── local_storage.go # 本地存储实现
└── s3_storage.go # S3 存储实现
```
## 🔧 Go 风格服务设计
### 接口定义规范
```go
// interfaces.go - 服务接口定义
package service
import (
"context"
"mime/multipart"
"photography-backend/internal/model/dto"
"photography-backend/internal/model/entity"
)
// UserServicer 用户服务接口
type UserServicer interface {
// 用户管理
CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error)
GetUser(ctx context.Context, id uint) (*entity.User, error)
GetUserByEmail(ctx context.Context, email string) (*entity.User, error)
UpdateUser(ctx context.Context, id uint, req *dto.UpdateUserRequest) (*entity.User, error)
DeleteUser(ctx context.Context, id uint) error
ListUsers(ctx context.Context, opts *dto.ListUsersOptions) ([]*entity.User, int64, error)
// 用户验证
ValidateUser(ctx context.Context, email, password string) (*entity.User, error)
ChangePassword(ctx context.Context, id uint, req *dto.ChangePasswordRequest) error
}
// PhotoServicer 照片服务接口
type PhotoServicer interface {
// 照片管理
CreatePhoto(ctx context.Context, req *dto.CreatePhotoRequest) (*entity.Photo, error)
GetPhoto(ctx context.Context, id uint) (*entity.Photo, error)
UpdatePhoto(ctx context.Context, id uint, req *dto.UpdatePhotoRequest) (*entity.Photo, error)
DeletePhoto(ctx context.Context, id uint) error
ListPhotos(ctx context.Context, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error)
// 照片处理
UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error)
ProcessPhoto(ctx context.Context, photoID uint, opts *dto.ProcessPhotoOptions) error
// 照片查询
GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error)
GetPhotosByCategory(ctx context.Context, categoryID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error)
SearchPhotos(ctx context.Context, query string, opts *dto.SearchPhotosOptions) ([]*entity.Photo, int64, error)
}
// AuthServicer 认证服务接口
type AuthServicer interface {
// 认证
Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, error)
Register(ctx context.Context, req *dto.RegisterRequest) (*dto.RegisterResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error)
Logout(ctx context.Context, token string) error
// 令牌管理
ValidateToken(ctx context.Context, token string) (*dto.TokenClaims, error)
RevokeToken(ctx context.Context, token string) error
}
```
### 服务实现规范
```go
// user/user_service.go - 用户服务实现
package user
import (
"context"
"errors"
"fmt"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"photography-backend/internal/model/dto"
"photography-backend/internal/model/entity"
"photography-backend/internal/repository"
"photography-backend/pkg/logger"
)
type UserService struct {
userRepo repository.UserRepositoryr
logger logger.Logger
}
func NewUserService(userRepo repository.UserRepositoryr, logger logger.Logger) *UserService {
return &UserService{
userRepo: userRepo,
logger: logger,
}
}
func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) {
// 1. 验证输入
if err := s.validateCreateUserRequest(req); err != nil {
s.logger.Error("invalid create user request", zap.Error(err))
return nil, err
}
// 2. 检查用户是否存在
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, repository.ErrNotFound) {
s.logger.Error("failed to check existing user", zap.Error(err))
return nil, err
}
if existingUser != nil {
return nil, errors.New("user already exists")
}
// 3. 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("failed to hash password", zap.Error(err))
return nil, err
}
// 4. 创建用户实体
user := &entity.User{
Email: req.Email,
Username: req.Username,
Password: string(hashedPassword),
Role: entity.UserRoleUser,
Status: entity.UserStatusActive,
}
// 5. 保存用户
createdUser, err := s.userRepo.Create(ctx, user)
if err != nil {
s.logger.Error("failed to create user", zap.Error(err))
return nil, err
}
s.logger.Info("user created successfully",
zap.String("user_id", fmt.Sprintf("%d", createdUser.ID)),
zap.String("email", createdUser.Email))
return createdUser, nil
}
func (s *UserService) GetUser(ctx context.Context, id uint) (*entity.User, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, errors.New("user not found")
}
s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", id))
return nil, err
}
return user, nil
}
func (s *UserService) ValidateUser(ctx context.Context, email, password string) (*entity.User, error) {
// 1. 获取用户
user, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, errors.New("invalid credentials")
}
s.logger.Error("failed to get user by email", zap.Error(err))
return nil, err
}
// 2. 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return nil, errors.New("invalid credentials")
}
// 3. 检查用户状态
if user.Status != entity.UserStatusActive {
return nil, errors.New("user account is not active")
}
return user, nil
}
func (s *UserService) validateCreateUserRequest(req *dto.CreateUserRequest) error {
if req.Email == "" {
return errors.New("email is required")
}
if req.Username == "" {
return errors.New("username is required")
}
if req.Password == "" {
return errors.New("password is required")
}
if len(req.Password) < 6 {
return errors.New("password must be at least 6 characters")
}
return nil
}
```
### 照片服务实现
```go
// photo/photo_service.go - 照片服务实现
package photo
import (
"context"
"errors"
"fmt"
"mime/multipart"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
"photography-backend/internal/model/dto"
"photography-backend/internal/model/entity"
"photography-backend/internal/repository"
"photography-backend/internal/service"
"photography-backend/pkg/logger"
)
type PhotoService struct {
photoRepo repository.PhotoRepositoryr
userRepo repository.UserRepositoryr
storageService service.StorageServicer
logger logger.Logger
}
func NewPhotoService(
photoRepo repository.PhotoRepositoryr,
userRepo repository.UserRepositoryr,
storageService service.StorageServicer,
logger logger.Logger,
) *PhotoService {
return &PhotoService{
photoRepo: photoRepo,
userRepo: userRepo,
storageService: storageService,
logger: logger,
}
}
func (s *PhotoService) UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error) {
// 1. 验证文件
if err := s.validatePhotoFile(file); err != nil {
s.logger.Error("invalid photo file", zap.Error(err))
return nil, err
}
// 2. 验证用户
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", userID))
return nil, err
}
// 3. 生成文件名
filename := s.generatePhotoFilename(file.Filename, userID)
// 4. 上传文件
uploadedFile, err := s.storageService.UploadFile(ctx, file, filename)
if err != nil {
s.logger.Error("failed to upload photo", zap.Error(err))
return nil, err
}
// 5. 创建照片记录
photo := &entity.Photo{
UserID: userID,
Title: strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)),
Description: "",
Filename: uploadedFile.Filename,
FilePath: uploadedFile.Path,
FileSize: uploadedFile.Size,
MimeType: uploadedFile.MimeType,
Width: uploadedFile.Width,
Height: uploadedFile.Height,
Status: entity.PhotoStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 6. 保存照片记录
createdPhoto, err := s.photoRepo.Create(ctx, photo)
if err != nil {
// 如果数据库保存失败,删除已上传的文件
s.storageService.DeleteFile(ctx, uploadedFile.Path)
s.logger.Error("failed to create photo record", zap.Error(err))
return nil, err
}
s.logger.Info("photo uploaded successfully",
zap.String("photo_id", fmt.Sprintf("%d", createdPhoto.ID)),
zap.String("filename", createdPhoto.Filename),
zap.Uint("user_id", userID))
return createdPhoto, nil
}
func (s *PhotoService) GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) {
// 设置默认选项
if opts == nil {
opts = &dto.ListPhotosOptions{
Page: 1,
Limit: 20,
Sort: "created_at",
Order: "desc",
}
}
// 查询照片
photos, total, err := s.photoRepo.ListByUserID(ctx, userID, &repository.ListPhotosOptions{
Page: opts.Page,
Limit: opts.Limit,
Sort: opts.Sort,
Order: opts.Order,
Status: string(entity.PhotoStatusActive),
})
if err != nil {
s.logger.Error("failed to list photos by user", zap.Error(err), zap.Uint("user_id", userID))
return nil, 0, err
}
return photos, total, nil
}
func (s *PhotoService) validatePhotoFile(file *multipart.FileHeader) error {
// 检查文件大小 (10MB 限制)
const maxFileSize = 10 * 1024 * 1024
if file.Size > maxFileSize {
return errors.New("file size exceeds limit")
}
// 检查文件类型
ext := strings.ToLower(filepath.Ext(file.Filename))
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
isAllowed := false
for _, allowedExt := range allowedExts {
if ext == allowedExt {
isAllowed = true
break
}
}
if !isAllowed {
return errors.New("file type not allowed")
}
return nil
}
func (s *PhotoService) generatePhotoFilename(originalFilename string, userID uint) string {
ext := filepath.Ext(originalFilename)
timestamp := time.Now().Unix()
return fmt.Sprintf("photos/%d_%d%s", userID, timestamp, ext)
}
```
## 🧪 服务测试
### 单元测试规范
```go
// user/user_service_test.go - 用户服务测试
package user
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"photography-backend/internal/model/dto"
"photography-backend/internal/model/entity"
"photography-backend/internal/repository/mocks"
"photography-backend/pkg/logger"
)
func TestUserService_CreateUser(t *testing.T) {
// Setup
mockUserRepo := new(mocks.UserRepositoryr)
mockLogger := logger.NewNoop()
userService := NewUserService(mockUserRepo, mockLogger)
ctx := context.Background()
req := &dto.CreateUserRequest{
Email: "test@example.com",
Username: "testuser",
Password: "password123",
}
// Mock expectations
mockUserRepo.On("GetByEmail", ctx, req.Email).Return(nil, repository.ErrNotFound)
mockUserRepo.On("Create", ctx, mock.AnythingOfType("*entity.User")).
Return(&entity.User{
ID: 1,
Email: req.Email,
Username: req.Username,
Role: entity.UserRoleUser,
Status: entity.UserStatusActive,
}, nil)
// Execute
user, err := userService.CreateUser(ctx, req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, req.Email, user.Email)
assert.Equal(t, req.Username, user.Username)
assert.Equal(t, entity.UserRoleUser, user.Role)
assert.Equal(t, entity.UserStatusActive, user.Status)
mockUserRepo.AssertExpectations(t)
}
func TestUserService_CreateUser_UserAlreadyExists(t *testing.T) {
// Setup
mockUserRepo := new(mocks.UserRepositoryr)
mockLogger := logger.NewNoop()
userService := NewUserService(mockUserRepo, mockLogger)
ctx := context.Background()
req := &dto.CreateUserRequest{
Email: "test@example.com",
Username: "testuser",
Password: "password123",
}
existingUser := &entity.User{
ID: 1,
Email: req.Email,
}
// Mock expectations
mockUserRepo.On("GetByEmail", ctx, req.Email).Return(existingUser, nil)
// Execute
user, err := userService.CreateUser(ctx, req)
// Assert
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "user already exists")
mockUserRepo.AssertExpectations(t)
}
```
## 🔍 错误处理
### 自定义错误类型
```go
// errors.go - 服务层错误定义
package service
import "errors"
var (
// 通用错误
ErrInvalidInput = errors.New("invalid input")
ErrInternalError = errors.New("internal error")
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
// 用户相关错误
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
ErrInvalidCredentials = errors.New("invalid credentials")
ErrUserInactive = errors.New("user account is not active")
// 照片相关错误
ErrPhotoNotFound = errors.New("photo not found")
ErrInvalidFileType = errors.New("invalid file type")
ErrFileSizeExceeded = errors.New("file size exceeded")
ErrUploadFailed = errors.New("file upload failed")
// 认证相关错误
ErrInvalidToken = errors.New("invalid token")
ErrTokenExpired = errors.New("token expired")
ErrTokenRevoked = errors.New("token revoked")
)
```
## 💡 最佳实践
### 服务设计原则
1. **单一职责**: 每个服务只负责一个业务领域
2. **接口优先**: 先定义接口,再实现具体逻辑
3. **依赖注入**: 通过构造函数注入依赖
4. **错误处理**: 明确的错误类型和处理
5. **日志记录**: 关键操作的日志记录
### 性能优化
1. **缓存策略**: 合理使用缓存减少数据库查询
2. **批量操作**: 优化批量数据处理
3. **异步处理**: 使用 goroutine 处理耗时操作
4. **数据库优化**: 合理使用索引和查询优化
### 安全考虑
1. **输入验证**: 严格验证所有输入参数
2. **权限检查**: 确保用户只能操作自己的数据
3. **敏感信息**: 避免在日志中记录敏感信息
4. **SQL 注入**: 使用参数化查询防止 SQL 注入
本模块是业务逻辑的核心,确保代码质量和测试覆盖率是关键。

View File

@ -2,7 +2,6 @@ package auth
import (
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
"photography-backend/internal/models"
"photography-backend/internal/repository/postgres"

View File

@ -32,10 +32,13 @@ type TokenPair struct {
// NewJWTService 创建JWT服务
func NewJWTService(cfg *config.JWTConfig) *JWTService {
accessDuration, _ := time.ParseDuration(cfg.ExpiresIn)
refreshDuration, _ := time.ParseDuration(cfg.RefreshExpiresIn)
return &JWTService{
secretKey: []byte(cfg.Secret),
accessTokenDuration: config.AppConfig.GetJWTExpiration(),
refreshTokenDuration: config.AppConfig.GetJWTRefreshExpiration(),
accessTokenDuration: accessDuration,
refreshTokenDuration: refreshDuration,
}
}

View File

@ -40,14 +40,14 @@ type LocalStorageService struct {
// NewLocalStorageService 创建本地存储服务
func NewLocalStorageService(config *config.Config, logger *zap.Logger) *LocalStorageService {
uploadDir := config.Upload.Path
uploadDir := config.Storage.Local.BasePath
if uploadDir == "" {
uploadDir = "./uploads"
}
baseURL := config.Upload.BaseURL
baseURL := config.Storage.Local.BaseURL
if baseURL == "" {
baseURL = fmt.Sprintf("http://localhost:%d/uploads", config.Server.Port)
baseURL = fmt.Sprintf("http://localhost:%d/uploads", config.App.Port)
}
// 确保上传目录存在
@ -95,7 +95,7 @@ func (s *LocalStorageService) UploadPhoto(ctx context.Context, file multipart.Fi
}
// 获取文件信息
fileInfo, err := out.Stat()
_, err = out.Stat()
if err != nil {
s.logger.Error("Failed to get file info", zap.Error(err))
return nil, err
@ -203,7 +203,7 @@ func (s *LocalStorageService) getMimeType(filename string) string {
// NewStorageService 根据配置创建存储服务
func NewStorageService(config *config.Config, logger *zap.Logger) StorageService {
switch config.Upload.Type {
switch config.Storage.Type {
case "s3":
// TODO: 实现 S3 存储服务
logger.Warn("S3 storage not implemented yet, using local storage")

View File

@ -0,0 +1,187 @@
package upload
import (
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"photography-backend/internal/config"
)
// UploadService 文件上传服务
type UploadService struct {
config *config.Config
uploadDir string
baseURL string
}
// UploadResult 上传结果
type UploadResult struct {
Filename string `json:"filename"`
OriginalName string `json:"original_name"`
FilePath string `json:"file_path"`
FileURL string `json:"file_url"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type"`
}
// NewUploadService 创建文件上传服务
func NewUploadService(cfg *config.Config) *UploadService {
uploadDir := cfg.Storage.Local.BasePath
if uploadDir == "" {
uploadDir = "./uploads"
}
baseURL := cfg.Storage.Local.BaseURL
if baseURL == "" {
baseURL = fmt.Sprintf("http://localhost:%d/uploads", cfg.App.Port)
}
// 确保上传目录存在
os.MkdirAll(uploadDir, 0755)
// 创建子目录
subdirs := []string{"photos", "thumbnails", "temp"}
for _, subdir := range subdirs {
os.MkdirAll(filepath.Join(uploadDir, subdir), 0755)
}
return &UploadService{
config: cfg,
uploadDir: uploadDir,
baseURL: baseURL,
}
}
// UploadPhoto 上传照片
func (s *UploadService) UploadPhoto(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
// 验证文件类型
if !s.isValidImageType(header.Header.Get("Content-Type")) {
return nil, fmt.Errorf("不支持的文件类型: %s", header.Header.Get("Content-Type"))
}
// 验证文件大小
if header.Size > s.config.Upload.MaxFileSize {
return nil, fmt.Errorf("文件大小超过限制: %d bytes", header.Size)
}
// 生成唯一文件名
filename := s.generateUniqueFilename(header.Filename)
// 保存文件到 photos 目录
photoPath := filepath.Join(s.uploadDir, "photos", filename)
// 创建目标文件
dst, err := os.Create(photoPath)
if err != nil {
return nil, fmt.Errorf("创建文件失败: %w", err)
}
defer dst.Close()
// 复制文件内容
_, err = io.Copy(dst, file)
if err != nil {
return nil, fmt.Errorf("保存文件失败: %w", err)
}
// 构造文件URL
fileURL := fmt.Sprintf("%s/photos/%s", s.baseURL, filename)
return &UploadResult{
Filename: filename,
OriginalName: header.Filename,
FilePath: photoPath,
FileURL: fileURL,
FileSize: header.Size,
MimeType: header.Header.Get("Content-Type"),
}, nil
}
// DeletePhoto 删除照片
func (s *UploadService) DeletePhoto(filename string) error {
// 删除原图
photoPath := filepath.Join(s.uploadDir, "photos", filename)
if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("删除文件失败: %w", err)
}
// 删除缩略图
thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename)
os.Remove(thumbnailPath) // 忽略错误,因为缩略图可能不存在
return nil
}
// GetPhotoURL 获取照片URL
func (s *UploadService) GetPhotoURL(filename string) string {
return fmt.Sprintf("%s/photos/%s", s.baseURL, filename)
}
// isValidImageType 验证是否为有效的图片类型
func (s *UploadService) isValidImageType(mimeType string) bool {
allowedTypes := s.config.Upload.AllowedTypes
if len(allowedTypes) == 0 {
// 默认允许的图片类型
allowedTypes = []string{
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/webp",
"image/tiff",
}
}
for _, allowedType := range allowedTypes {
if mimeType == allowedType {
return true
}
}
return false
}
// generateUniqueFilename 生成唯一文件名
func (s *UploadService) generateUniqueFilename(originalName string) string {
ext := filepath.Ext(originalName)
timestamp := time.Now().Unix()
// 清理原文件名
baseName := strings.TrimSuffix(originalName, ext)
baseName = strings.ReplaceAll(baseName, " ", "_")
return fmt.Sprintf("%s_%d%s", baseName, timestamp, ext)
}
// GetUploadStats 获取上传统计信息
func (s *UploadService) GetUploadStats() (map[string]interface{}, error) {
photosDir := filepath.Join(s.uploadDir, "photos")
var totalFiles int
var totalSize int64
err := filepath.Walk(photosDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalFiles++
totalSize += info.Size()
}
return nil
})
if err != nil {
return nil, err
}
return map[string]interface{}{
"total_files": totalFiles,
"total_size": totalSize,
"upload_dir": s.uploadDir,
"base_url": s.baseURL,
}, nil
}

View File

@ -1,243 +0,0 @@
package utils
import (
"crypto/md5"
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
"unicode"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
// Contains 检查字符串切片是否包含指定字符串
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// ContainsUint 检查 uint 切片是否包含指定值
func ContainsUint(slice []uint, item uint) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// GenerateUniqueFilename 生成唯一文件名
func GenerateUniqueFilename(originalFilename string) string {
ext := filepath.Ext(originalFilename)
name := strings.TrimSuffix(originalFilename, ext)
// 生成时间戳和哈希
timestamp := time.Now().Unix()
hash := md5.Sum([]byte(fmt.Sprintf("%s%d", name, timestamp)))
return fmt.Sprintf("%d_%x%s", timestamp, hash[:8], ext)
}
// GenerateSlug 生成 URL 友好的 slug
func GenerateSlug(text string) string {
// 转换为小写
slug := strings.ToLower(text)
// 移除重音符号
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
slug, _, _ = transform.String(t, slug)
// 只保留字母、数字、连字符和下划线
reg := regexp.MustCompile(`[^a-z0-9\-_\s]`)
slug = reg.ReplaceAllString(slug, "")
// 将空格替换为连字符
slug = regexp.MustCompile(`\s+`).ReplaceAllString(slug, "-")
// 移除多余的连字符
slug = regexp.MustCompile(`-+`).ReplaceAllString(slug, "-")
// 移除开头和结尾的连字符
slug = strings.Trim(slug, "-")
return slug
}
// ValidateEmail 验证邮箱格式
func ValidateEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
return emailRegex.MatchString(strings.ToLower(email))
}
// ValidatePassword 验证密码强度
func ValidatePassword(password string) bool {
if len(password) < 8 {
return false
}
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasDigit := regexp.MustCompile(`\d`).MatchString(password)
return hasLower && hasUpper && hasDigit
}
// Paginate 计算分页参数
func Paginate(page, limit int) (offset int) {
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
offset = (page - 1) * limit
return offset
}
// CalculatePages 计算总页数
func CalculatePages(total int64, limit int) int {
if limit <= 0 {
return 0
}
return int((total + int64(limit) - 1) / int64(limit))
}
// TruncateString 截断字符串
func TruncateString(s string, maxLength int) string {
if len(s) <= maxLength {
return s
}
return s[:maxLength] + "..."
}
// FormatFileSize 格式化文件大小
func FormatFileSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
sizes := []string{"KB", "MB", "GB", "TB", "PB"}
return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), sizes[exp])
}
// ParseSortOrder 解析排序方向
func ParseSortOrder(order string) string {
order = strings.ToLower(strings.TrimSpace(order))
if order == "asc" || order == "desc" {
return order
}
return "desc" // 默认降序
}
// SanitizeSearchQuery 清理搜索查询
func SanitizeSearchQuery(query string) string {
// 移除特殊字符,只保留字母、数字、空格和常用标点
reg := regexp.MustCompile(`[^\w\s\-\.\_\@]`)
query = reg.ReplaceAllString(query, "")
// 移除多余的空格
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
return strings.TrimSpace(query)
}
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// 使用当前时间作为种子
timestamp := time.Now().UnixNano()
result := make([]byte, length)
for i := range result {
result[i] = charset[(timestamp+int64(i))%int64(len(charset))]
}
return string(result)
}
// IsValidImageExtension 检查是否为有效的图片扩展名
func IsValidImageExtension(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
return Contains(validExts, ext)
}
// GetImageMimeType 根据文件扩展名获取 MIME 类型
func GetImageMimeType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
mimeTypes := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".tiff": "image/tiff",
}
if mimeType, exists := mimeTypes[ext]; exists {
return mimeType
}
return "application/octet-stream"
}
// RemoveEmptyStrings 移除字符串切片中的空字符串
func RemoveEmptyStrings(slice []string) []string {
var result []string
for _, s := range slice {
if strings.TrimSpace(s) != "" {
result = append(result, strings.TrimSpace(s))
}
}
return result
}
// UniqueStrings 去重字符串切片
func UniqueStrings(slice []string) []string {
keys := make(map[string]bool)
var result []string
for _, item := range slice {
if !keys[item] {
keys[item] = true
result = append(result, item)
}
}
return result
}
// UniqueUints 去重 uint 切片
func UniqueUints(slice []uint) []uint {
keys := make(map[uint]bool)
var result []uint
for _, item := range slice {
if !keys[item] {
keys[item] = true
result = append(result, item)
}
}
return result
}