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

684 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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