refactor: 重构后端架构,采用 Go 风格四层设计模式
## 主要变更 ### 🏗️ 架构重构 - 采用简洁的四层架构: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 语言生态标准 - 完善文档体系,降低上手难度
This commit is contained in:
684
backend/internal/model/CLAUDE.md
Normal file
684
backend/internal/model/CLAUDE.md
Normal 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 注入
|
||||
|
||||
本模块是数据层的基础,确保模型设计的合理性和一致性是关键。
|
||||
Reference in New Issue
Block a user