Files
photography/backend-old/internal/model/CLAUDE.md
xujiang 604b9e59ba fix
2025-07-10 18:09:11 +08:00

21 KiB
Raw Blame History

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          # 自定义类型

🏗️ 实体模型设计

基础实体

// 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 ""
}

用户实体

// 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
}

照片实体

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

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

// 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
}

📊 类型定义

枚举和常量

// 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
)

自定义类型

// 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)
}

🎯 验证和转换

数据验证

// 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 注入

本模块是数据层的基础,确保模型设计的合理性和一致性是关键。