fix
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 5m8s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped

This commit is contained in:
xujiang
2025-07-10 18:09:11 +08:00
parent 35004f224e
commit 010fe2a8c7
96 changed files with 23709 additions and 19 deletions

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