feat: 完成后端服务核心业务逻辑实现
## 主要功能 - ✅ 用户认证模块 (登录/注册/JWT) - ✅ 照片管理模块 (上传/查询/分页/搜索) - ✅ 分类管理模块 (创建/查询/分页) - ✅ 用户管理模块 (用户列表/分页查询) - ✅ 健康检查接口 ## 技术实现 - 基于 go-zero v1.8.0 标准架构 - Handler → Logic → Model 三层架构 - SQLite/PostgreSQL 数据库支持 - JWT 认证机制 - bcrypt 密码加密 - 统一响应格式 - 自定义模型方法 (分页/搜索) ## API 接口 - POST /api/v1/auth/login - 用户登录 - POST /api/v1/auth/register - 用户注册 - GET /api/v1/health - 健康检查 - GET /api/v1/photos - 照片列表 - POST /api/v1/photos - 上传照片 - GET /api/v1/categories - 分类列表 - POST /api/v1/categories - 创建分类 - GET /api/v1/users - 用户列表 ## 配置完成 - 开发环境配置 (SQLite) - 生产环境支持 (PostgreSQL) - JWT 认证配置 - 文件上传配置 - Makefile 构建脚本 服务已验证可正常构建和启动。
This commit is contained in:
@ -1,684 +0,0 @@
|
||||
# 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