## 主要变更 ### 🏗️ 架构重构 - 采用简洁的四层架构: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 语言生态标准 - 完善文档体系,降低上手难度
21 KiB
21 KiB
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
}
💡 最佳实践
模型设计原则
- 职责分离: 实体、DTO、请求、响应分别定义
- 数据验证: 使用标签进行数据验证
- 关联关系: 合理定义实体间的关联关系
- 索引优化: 为常用查询字段添加索引
- 软删除: 重要数据使用软删除
性能优化
- 延迟加载: 避免不必要的关联查询
- 选择性字段: 只查询需要的字段
- 批量操作: 使用批量插入和更新
- 缓存策略: 缓存频繁访问的数据
安全考虑
- 敏感字段: 密码等敏感字段不参与序列化
- 输入验证: 严格验证所有输入数据
- 权限控制: 在模型层实现基础权限检查
- SQL 注入: 使用 ORM 防止 SQL 注入
本模块是数据层的基础,确保模型设计的合理性和一致性是关键。