# 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 注入 本模块是数据层的基础,确保模型设计的合理性和一致性是关键。