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 注入
|
||||
|
||||
本模块是数据层的基础,确保模型设计的合理性和一致性是关键。
|
||||
81
backend/internal/model/categorymodel.go
Executable file
81
backend/internal/model/categorymodel.go
Executable file
@ -0,0 +1,81 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
var _ CategoryModel = (*customCategoryModel)(nil)
|
||||
|
||||
type (
|
||||
// CategoryModel is an interface to be customized, add more methods here,
|
||||
// and implement the added methods in customCategoryModel.
|
||||
CategoryModel interface {
|
||||
categoryModel
|
||||
withSession(session sqlx.Session) CategoryModel
|
||||
FindList(ctx context.Context, page, pageSize int, keyword string) ([]*Category, error)
|
||||
Count(ctx context.Context, keyword string) (int64, error)
|
||||
}
|
||||
|
||||
customCategoryModel struct {
|
||||
*defaultCategoryModel
|
||||
}
|
||||
)
|
||||
|
||||
// NewCategoryModel returns a model for the database table.
|
||||
func NewCategoryModel(conn sqlx.SqlConn) CategoryModel {
|
||||
return &customCategoryModel{
|
||||
defaultCategoryModel: newCategoryModel(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *customCategoryModel) withSession(session sqlx.Session) CategoryModel {
|
||||
return NewCategoryModel(sqlx.NewSqlConnFromSession(session))
|
||||
}
|
||||
|
||||
// FindList 分页查询分类列表
|
||||
func (m *customCategoryModel) FindList(ctx context.Context, page, pageSize int, keyword string) ([]*Category, error) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if keyword != "" {
|
||||
conditions = append(conditions, "(`name` LIKE ? OR `description` LIKE ?)")
|
||||
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
whereClause := ""
|
||||
if len(conditions) > 0 {
|
||||
whereClause = " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
args = append(args, pageSize, offset)
|
||||
|
||||
query := fmt.Sprintf("select %s from %s%s ORDER BY `created_at` DESC LIMIT ? OFFSET ?", categoryRows, m.table, whereClause)
|
||||
var resp []*Category
|
||||
err := m.conn.QueryRowsCtx(ctx, &resp, query, args...)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Count 统计分类数量
|
||||
func (m *customCategoryModel) Count(ctx context.Context, keyword string) (int64, error) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if keyword != "" {
|
||||
conditions = append(conditions, "(`name` LIKE ? OR `description` LIKE ?)")
|
||||
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
whereClause := ""
|
||||
if len(conditions) > 0 {
|
||||
whereClause = " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("select count(*) from %s%s", m.table, whereClause)
|
||||
var count int64
|
||||
err := m.conn.QueryRowCtx(ctx, &count, query, args...)
|
||||
return count, err
|
||||
}
|
||||
104
backend/internal/model/categorymodel_gen.go
Executable file
104
backend/internal/model/categorymodel_gen.go
Executable file
@ -0,0 +1,104 @@
|
||||
// Code generated by goctl. DO NOT EDIT.
|
||||
// versions:
|
||||
// goctl version: 1.8.4
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/builder"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
"github.com/zeromicro/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
var (
|
||||
categoryFieldNames = builder.RawFieldNames(&Category{})
|
||||
categoryRows = strings.Join(categoryFieldNames, ",")
|
||||
categoryRowsExpectAutoSet = strings.Join(stringx.Remove(categoryFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
|
||||
categoryRowsWithPlaceHolder = strings.Join(stringx.Remove(categoryFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
|
||||
)
|
||||
|
||||
type (
|
||||
categoryModel interface {
|
||||
Insert(ctx context.Context, data *Category) (sql.Result, error)
|
||||
FindOne(ctx context.Context, id int64) (*Category, error)
|
||||
FindOneByName(ctx context.Context, name string) (*Category, error)
|
||||
Update(ctx context.Context, data *Category) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
defaultCategoryModel struct {
|
||||
conn sqlx.SqlConn
|
||||
table string
|
||||
}
|
||||
|
||||
Category struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"name"` // 分类名称
|
||||
Description sql.NullString `db:"description"` // 分类描述
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
)
|
||||
|
||||
func newCategoryModel(conn sqlx.SqlConn) *defaultCategoryModel {
|
||||
return &defaultCategoryModel{
|
||||
conn: conn,
|
||||
table: "`category`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultCategoryModel) Delete(ctx context.Context, id int64) error {
|
||||
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
|
||||
_, err := m.conn.ExecCtx(ctx, query, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultCategoryModel) FindOne(ctx context.Context, id int64) (*Category, error) {
|
||||
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", categoryRows, m.table)
|
||||
var resp Category
|
||||
err := m.conn.QueryRowCtx(ctx, &resp, query, id)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultCategoryModel) FindOneByName(ctx context.Context, name string) (*Category, error) {
|
||||
var resp Category
|
||||
query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", categoryRows, m.table)
|
||||
err := m.conn.QueryRowCtx(ctx, &resp, query, name)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultCategoryModel) Insert(ctx context.Context, data *Category) (sql.Result, error) {
|
||||
query := fmt.Sprintf("insert into %s (%s) values (?, ?)", m.table, categoryRowsExpectAutoSet)
|
||||
ret, err := m.conn.ExecCtx(ctx, query, data.Name, data.Description)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (m *defaultCategoryModel) Update(ctx context.Context, newData *Category) error {
|
||||
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, categoryRowsWithPlaceHolder)
|
||||
_, err := m.conn.ExecCtx(ctx, query, newData.Name, newData.Description, newData.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultCategoryModel) tableName() string {
|
||||
return m.table
|
||||
}
|
||||
@ -1,196 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// CreateAlbumRequest 创建相册请求
|
||||
type CreateAlbumRequest struct {
|
||||
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
|
||||
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
Slug string `json:"slug" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"`
|
||||
CategoryID *uint `json:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
IsPublic bool `json:"is_public" binding:"omitempty"`
|
||||
IsFeatured bool `json:"is_featured" binding:"omitempty"`
|
||||
Password string `json:"password" binding:"omitempty,min=6" validate:"omitempty,min=6"`
|
||||
}
|
||||
|
||||
// UpdateAlbumRequest 更新相册请求
|
||||
type UpdateAlbumRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"`
|
||||
Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
Slug *string `json:"slug" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"`
|
||||
CoverPhotoID *uint `json:"cover_photo_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
CategoryID *uint `json:"category_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
IsPublic *bool `json:"is_public" binding:"omitempty"`
|
||||
IsFeatured *bool `json:"is_featured" binding:"omitempty"`
|
||||
Password *string `json:"password" binding:"omitempty,min=0" validate:"omitempty,min=0"` // 空字符串表示移除密码
|
||||
SortOrder *int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
}
|
||||
|
||||
// AlbumResponse 相册响应
|
||||
type AlbumResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
CoverPhotoID *uint `json:"cover_photo_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
ViewCount int `json:"view_count"`
|
||||
LikeCount int `json:"like_count"`
|
||||
PhotoCount int `json:"photo_count"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
Category *CategoryResponse `json:"category,omitempty"`
|
||||
CoverPhoto *PhotoListItem `json:"cover_photo,omitempty"`
|
||||
RecentPhotos []PhotoListItem `json:"recent_photos,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AlbumListItem 相册列表项(简化版)
|
||||
type AlbumListItem struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
ViewCount int `json:"view_count"`
|
||||
LikeCount int `json:"like_count"`
|
||||
PhotoCount int `json:"photo_count"`
|
||||
CoverPhoto *PhotoListItem `json:"cover_photo,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ListAlbumsOptions 相册列表查询选项
|
||||
type ListAlbumsOptions struct {
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id title created_at updated_at view_count like_count photo_count" validate:"omitempty,oneof=id title created_at updated_at view_count like_count photo_count"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
|
||||
IsFeatured *bool `json:"is_featured" form:"is_featured" binding:"omitempty"`
|
||||
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
// AlbumListResponse 相册列表响应
|
||||
type AlbumListResponse struct {
|
||||
Albums []AlbumListItem `json:"albums"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// AddPhotosToAlbumRequest 向相册添加照片请求
|
||||
type AddPhotosToAlbumRequest struct {
|
||||
PhotoIDs []uint `json:"photo_ids" binding:"required,min=1" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// RemovePhotosFromAlbumRequest 从相册移除照片请求
|
||||
type RemovePhotosFromAlbumRequest struct {
|
||||
PhotoIDs []uint `json:"photo_ids" binding:"required,min=1" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// AlbumPasswordRequest 相册密码验证请求
|
||||
type AlbumPasswordRequest struct {
|
||||
Password string `json:"password" binding:"required" validate:"required"`
|
||||
}
|
||||
|
||||
// AlbumStatsResponse 相册统计响应
|
||||
type AlbumStatsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Published int64 `json:"published"`
|
||||
Private int64 `json:"private"`
|
||||
Featured int64 `json:"featured"`
|
||||
WithPassword int64 `json:"with_password"`
|
||||
TotalViews int64 `json:"total_views"`
|
||||
TotalLikes int64 `json:"total_likes"`
|
||||
TotalPhotos int64 `json:"total_photos"`
|
||||
CategoryCounts map[string]int64 `json:"category_counts"`
|
||||
Recent []AlbumListItem `json:"recent"`
|
||||
Popular []AlbumListItem `json:"popular"`
|
||||
}
|
||||
|
||||
// ConvertToAlbumResponse 将相册实体转换为响应DTO
|
||||
func ConvertToAlbumResponse(album *entity.Album) *AlbumResponse {
|
||||
if album == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &AlbumResponse{
|
||||
ID: album.ID,
|
||||
Title: album.Title,
|
||||
Description: album.Description,
|
||||
Slug: album.Slug,
|
||||
CoverPhotoID: album.CoverPhotoID,
|
||||
UserID: album.UserID,
|
||||
CategoryID: album.CategoryID,
|
||||
IsPublic: album.IsPublic,
|
||||
IsFeatured: album.IsFeatured,
|
||||
HasPassword: album.HasPassword(),
|
||||
ViewCount: album.ViewCount,
|
||||
LikeCount: album.LikeCount,
|
||||
PhotoCount: album.PhotoCount,
|
||||
SortOrder: album.SortOrder,
|
||||
CreatedAt: album.CreatedAt,
|
||||
UpdatedAt: album.UpdatedAt,
|
||||
}
|
||||
|
||||
// 转换关联对象
|
||||
if album.User.ID != 0 {
|
||||
response.User = ConvertToUserResponse(&album.User)
|
||||
}
|
||||
if album.Category != nil {
|
||||
response.Category = ConvertToCategoryResponse(album.Category)
|
||||
}
|
||||
if album.CoverPhoto != nil {
|
||||
coverPhoto := ConvertToPhotoListItem(album.CoverPhoto)
|
||||
response.CoverPhoto = &coverPhoto
|
||||
}
|
||||
|
||||
// 转换最近照片
|
||||
if len(album.Photos) > 0 {
|
||||
recentPhotos := make([]PhotoListItem, 0, len(album.Photos))
|
||||
for _, photo := range album.Photos {
|
||||
recentPhotos = append(recentPhotos, ConvertToPhotoListItem(&photo))
|
||||
}
|
||||
response.RecentPhotos = recentPhotos
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ConvertToAlbumListItem 将相册实体转换为列表项DTO
|
||||
func ConvertToAlbumListItem(album *entity.Album) AlbumListItem {
|
||||
item := AlbumListItem{
|
||||
ID: album.ID,
|
||||
Title: album.Title,
|
||||
Slug: album.Slug,
|
||||
IsPublic: album.IsPublic,
|
||||
IsFeatured: album.IsFeatured,
|
||||
HasPassword: album.HasPassword(),
|
||||
ViewCount: album.ViewCount,
|
||||
LikeCount: album.LikeCount,
|
||||
PhotoCount: album.PhotoCount,
|
||||
CreatedAt: album.CreatedAt,
|
||||
UpdatedAt: album.UpdatedAt,
|
||||
}
|
||||
|
||||
// 转换封面照片
|
||||
if album.CoverPhoto != nil {
|
||||
coverPhoto := ConvertToPhotoListItem(album.CoverPhoto)
|
||||
item.CoverPhoto = &coverPhoto
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email" validate:"required,email"`
|
||||
Password string `json:"password" binding:"required" validate:"required"`
|
||||
}
|
||||
|
||||
// RegisterRequest 注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50" validate:"required,min=3,max=50"`
|
||||
Email string `json:"email" binding:"required,email" validate:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6" validate:"required,min=6"`
|
||||
Name string `json:"name" binding:"max=100" validate:"max=100"`
|
||||
}
|
||||
|
||||
// RefreshTokenRequest 刷新令牌请求
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required" validate:"required"`
|
||||
}
|
||||
|
||||
// TokenResponse 令牌响应
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Token TokenResponse `json:"token"`
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
|
||||
// RegisterResponse 注册响应
|
||||
type RegisterResponse struct {
|
||||
Token TokenResponse `json:"token"`
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
|
||||
// TokenClaims JWT 令牌声明
|
||||
type TokenClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role entity.UserRole `json:"role"`
|
||||
TokenID string `json:"token_id"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// ResetPasswordRequest 重置密码请求
|
||||
type ResetPasswordRequest struct {
|
||||
Email string `json:"email" binding:"required,email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// ConfirmResetPasswordRequest 确认重置密码请求
|
||||
type ConfirmResetPasswordRequest struct {
|
||||
Token string `json:"token" binding:"required" validate:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// VerifyEmailRequest 验证邮箱请求
|
||||
type VerifyEmailRequest struct {
|
||||
Token string `json:"token" binding:"required" validate:"required"`
|
||||
}
|
||||
|
||||
// LogoutRequest 登出请求
|
||||
type LogoutRequest struct {
|
||||
Token string `json:"token" binding:"required" validate:"required"`
|
||||
}
|
||||
|
||||
// AuthStatsResponse 认证统计响应
|
||||
type AuthStatsResponse struct {
|
||||
TotalLogins int64 `json:"total_logins"`
|
||||
ActiveSessions int64 `json:"active_sessions"`
|
||||
FailedAttempts int64 `json:"failed_attempts"`
|
||||
RecentLogins []LoginInfo `json:"recent_logins"`
|
||||
LoginsByHour map[string]int64 `json:"logins_by_hour"`
|
||||
LoginsByDevice map[string]int64 `json:"logins_by_device"`
|
||||
}
|
||||
|
||||
// LoginInfo 登录信息
|
||||
type LoginInfo struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
LoginTime time.Time `json:"login_time"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ValidateTokenResponse 验证令牌响应
|
||||
type ValidateTokenResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Claims *TokenClaims `json:"claims,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
@ -1,143 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// CreateCategoryRequest 创建分类请求
|
||||
type CreateCategoryRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=100" validate:"required,min=1,max=100"`
|
||||
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
Slug string `json:"slug" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
ParentID *uint `json:"parent_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Color string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
|
||||
CoverImage string `json:"cover_image" binding:"omitempty,url" validate:"omitempty,url"`
|
||||
SortOrder int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
}
|
||||
|
||||
// UpdateCategoryRequest 更新分类请求
|
||||
type UpdateCategoryRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
Slug *string `json:"slug" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
ParentID *uint `json:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
Color *string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
|
||||
CoverImage *string `json:"cover_image" binding:"omitempty,url" validate:"omitempty,url"`
|
||||
SortOrder *int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
IsActive *bool `json:"is_active" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// CategoryResponse 分类响应
|
||||
type CategoryResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color string `json:"color"`
|
||||
CoverImage string `json:"cover_image"`
|
||||
Sort int `json:"sort"`
|
||||
IsActive bool `json:"is_active"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
AlbumCount int64 `json:"album_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CategoryTreeResponse 分类树响应
|
||||
type CategoryTreeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color string `json:"color"`
|
||||
CoverImage string `json:"cover_image"`
|
||||
Sort int `json:"sort"`
|
||||
IsActive bool `json:"is_active"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
AlbumCount int64 `json:"album_count"`
|
||||
Children []CategoryTreeResponse `json:"children"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ListCategoriesOptions 分类列表查询选项
|
||||
type ListCategoriesOptions struct {
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id name sort created_at updated_at" validate:"omitempty,oneof=id name sort created_at updated_at"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
ParentID *uint `json:"parent_id" form:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
|
||||
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
WithCount bool `json:"with_count" form:"with_count" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// CategoryListResponse 分类列表响应
|
||||
type CategoryListResponse struct {
|
||||
Categories []CategoryResponse `json:"categories"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// ReorderCategoriesRequest 重新排序分类请求
|
||||
type ReorderCategoriesRequest struct {
|
||||
ParentID *uint `json:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
CategoryIDs []uint `json:"category_ids" binding:"required,min=1" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// CategoryStatsResponse 分类统计响应
|
||||
type CategoryStatsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
TopLevel int64 `json:"top_level"`
|
||||
PhotoCounts map[string]int64 `json:"photo_counts"`
|
||||
Popular []CategoryResponse `json:"popular"`
|
||||
}
|
||||
|
||||
// ConvertToCategoryResponse 将分类实体转换为响应DTO
|
||||
func ConvertToCategoryResponse(category *entity.Category) *CategoryResponse {
|
||||
if category == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
ParentID: category.ParentID,
|
||||
Color: category.Color,
|
||||
CoverImage: category.CoverImage,
|
||||
Sort: category.Sort,
|
||||
IsActive: category.IsActive,
|
||||
PhotoCount: category.PhotoCount,
|
||||
CreatedAt: category.CreatedAt,
|
||||
UpdatedAt: category.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToCategoryTreeResponse 将分类树转换为响应DTO
|
||||
func ConvertToCategoryTreeResponse(tree []entity.CategoryTree) []CategoryTreeResponse {
|
||||
result := make([]CategoryTreeResponse, len(tree))
|
||||
for i, category := range tree {
|
||||
result[i] = CategoryTreeResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
ParentID: category.ParentID,
|
||||
Color: category.Color,
|
||||
CoverImage: category.CoverImage,
|
||||
Sort: category.Sort,
|
||||
IsActive: category.IsActive,
|
||||
PhotoCount: category.PhotoCount,
|
||||
Children: ConvertToCategoryTreeResponse(category.Children),
|
||||
CreatedAt: category.CreatedAt,
|
||||
UpdatedAt: category.UpdatedAt,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -1,264 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// CreatePhotoRequest 创建照片请求
|
||||
type CreatePhotoRequest struct {
|
||||
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
|
||||
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
Filename string `json:"filename" binding:"required" validate:"required"`
|
||||
OriginalURL string `json:"original_url" binding:"required,url" validate:"required,url"`
|
||||
FileSize int64 `json:"file_size" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
MimeType string `json:"mime_type" binding:"omitempty" validate:"omitempty"`
|
||||
Width int `json:"width" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
Height int `json:"height" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
UserID uint `json:"user_id" binding:"required,min=1" validate:"required,min=1"`
|
||||
AlbumID *uint `json:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
CategoryID *uint `json:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
TagIDs []uint `json:"tag_ids" binding:"omitempty" validate:"omitempty"`
|
||||
IsPublic bool `json:"is_public" binding:"omitempty"`
|
||||
IsFeatured bool `json:"is_featured" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// UpdatePhotoRequest 更新照片请求
|
||||
type UpdatePhotoRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"`
|
||||
Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
AlbumID *uint `json:"album_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
CategoryID *uint `json:"category_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
|
||||
TagIDs []uint `json:"tag_ids" binding:"omitempty" validate:"omitempty"`
|
||||
IsPublic *bool `json:"is_public" binding:"omitempty"`
|
||||
IsFeatured *bool `json:"is_featured" binding:"omitempty"`
|
||||
LocationName *string `json:"location_name" binding:"omitempty,max=200" validate:"omitempty,max=200"`
|
||||
Latitude *float64 `json:"latitude" binding:"omitempty,min=-90,max=90" validate:"omitempty,min=-90,max=90"`
|
||||
Longitude *float64 `json:"longitude" binding:"omitempty,min=-180,max=180" validate:"omitempty,min=-180,max=180"`
|
||||
}
|
||||
|
||||
// UploadPhotoRequest 上传照片请求
|
||||
type UploadPhotoRequest struct {
|
||||
File *multipart.FileHeader `form:"photo" binding:"required" validate:"required"`
|
||||
Title string `form:"title" binding:"omitempty,max=200" validate:"omitempty,max=200"`
|
||||
Description string `form:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
AlbumID *uint `form:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
CategoryID *uint `form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
TagNames []string `form:"tag_names" binding:"omitempty" validate:"omitempty"`
|
||||
IsPublic bool `form:"is_public" binding:"omitempty"`
|
||||
IsFeatured bool `form:"is_featured" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// PhotoResponse 照片响应
|
||||
type PhotoResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Filename string `json:"filename"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
MediumURL string `json:"medium_url"`
|
||||
LargeURL string `json:"large_url"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
AspectRatio float64 `json:"aspect_ratio"`
|
||||
|
||||
// EXIF 信息
|
||||
CameraMake string `json:"camera_make"`
|
||||
CameraModel string `json:"camera_model"`
|
||||
LensModel string `json:"lens_model"`
|
||||
FocalLength *float64 `json:"focal_length"`
|
||||
Aperture *float64 `json:"aperture"`
|
||||
ShutterSpeed string `json:"shutter_speed"`
|
||||
ISO *int `json:"iso"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
|
||||
// 地理位置
|
||||
LocationName string `json:"location_name"`
|
||||
Latitude *float64 `json:"latitude"`
|
||||
Longitude *float64 `json:"longitude"`
|
||||
|
||||
// 关联信息
|
||||
UserID uint `json:"user_id"`
|
||||
AlbumID *uint `json:"album_id"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
Album *AlbumResponse `json:"album,omitempty"`
|
||||
Category *CategoryResponse `json:"category,omitempty"`
|
||||
Tags []TagResponse `json:"tags,omitempty"`
|
||||
|
||||
// 状态和统计
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
ViewCount int `json:"view_count"`
|
||||
LikeCount int `json:"like_count"`
|
||||
DownloadCount int `json:"download_count"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PhotoListItem 照片列表项(简化版)
|
||||
type PhotoListItem struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
AspectRatio float64 `json:"aspect_ratio"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
ViewCount int `json:"view_count"`
|
||||
LikeCount int `json:"like_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListPhotosOptions 照片列表查询选项
|
||||
type ListPhotosOptions struct {
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id title created_at updated_at taken_at view_count like_count" validate:"omitempty,oneof=id title created_at updated_at taken_at view_count like_count"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
AlbumID *uint `json:"album_id" form:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
TagIDs []uint `json:"tag_ids" form:"tag_ids" binding:"omitempty" validate:"omitempty"`
|
||||
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
|
||||
IsFeatured *bool `json:"is_featured" form:"is_featured" binding:"omitempty"`
|
||||
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
Year *int `json:"year" form:"year" binding:"omitempty,min=1900,max=2100" validate:"omitempty,min=1900,max=2100"`
|
||||
Month *int `json:"month" form:"month" binding:"omitempty,min=1,max=12" validate:"omitempty,min=1,max=12"`
|
||||
}
|
||||
|
||||
// SearchPhotosOptions 照片搜索选项
|
||||
type SearchPhotosOptions struct {
|
||||
Query string `json:"query" form:"query" binding:"required,min=1" validate:"required,min=1"`
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=relevance created_at view_count like_count" validate:"omitempty,oneof=relevance created_at view_count like_count"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
TagIDs []uint `json:"tag_ids" form:"tag_ids" binding:"omitempty" validate:"omitempty"`
|
||||
UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// PhotoListResponse 照片列表响应
|
||||
type PhotoListResponse struct {
|
||||
Photos []PhotoListItem `json:"photos"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// ProcessPhotoOptions 照片处理选项
|
||||
type ProcessPhotoOptions struct {
|
||||
GenerateThumbnails bool `json:"generate_thumbnails"`
|
||||
ThumbnailSizes []string `json:"thumbnail_sizes"`
|
||||
ExtractEXIF bool `json:"extract_exif"`
|
||||
GenerateHash bool `json:"generate_hash"`
|
||||
OptimizeSize bool `json:"optimize_size"`
|
||||
WatermarkEnabled bool `json:"watermark_enabled"`
|
||||
}
|
||||
|
||||
// PhotoStatsResponse 照片统计响应
|
||||
type PhotoStatsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Published int64 `json:"published"`
|
||||
Private int64 `json:"private"`
|
||||
Featured int64 `json:"featured"`
|
||||
TotalViews int64 `json:"total_views"`
|
||||
TotalLikes int64 `json:"total_likes"`
|
||||
TotalDownloads int64 `json:"total_downloads"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
CategoryCounts map[string]int64 `json:"category_counts"`
|
||||
TagCounts map[string]int64 `json:"tag_counts"`
|
||||
Recent []PhotoListItem `json:"recent"`
|
||||
Popular []PhotoListItem `json:"popular"`
|
||||
}
|
||||
|
||||
// ConvertToPhotoResponse 将照片实体转换为响应DTO
|
||||
func ConvertToPhotoResponse(photo *entity.Photo) *PhotoResponse {
|
||||
if photo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &PhotoResponse{
|
||||
ID: photo.ID,
|
||||
Title: photo.Title,
|
||||
Description: photo.Description,
|
||||
Filename: photo.Filename,
|
||||
OriginalURL: photo.OriginalURL,
|
||||
ThumbnailURL: photo.ThumbnailURL,
|
||||
MediumURL: photo.MediumURL,
|
||||
LargeURL: photo.LargeURL,
|
||||
FileSize: photo.FileSize,
|
||||
MimeType: photo.MimeType,
|
||||
Width: photo.Width,
|
||||
Height: photo.Height,
|
||||
AspectRatio: photo.GetAspectRatio(),
|
||||
|
||||
// EXIF
|
||||
CameraMake: photo.CameraMake,
|
||||
CameraModel: photo.CameraModel,
|
||||
LensModel: photo.LensModel,
|
||||
FocalLength: photo.FocalLength,
|
||||
Aperture: photo.Aperture,
|
||||
ShutterSpeed: photo.ShutterSpeed,
|
||||
ISO: photo.ISO,
|
||||
TakenAt: photo.TakenAt,
|
||||
|
||||
// 地理位置
|
||||
LocationName: photo.LocationName,
|
||||
Latitude: photo.Latitude,
|
||||
Longitude: photo.Longitude,
|
||||
|
||||
// 关联
|
||||
UserID: photo.UserID,
|
||||
AlbumID: photo.AlbumID,
|
||||
CategoryID: photo.CategoryID,
|
||||
|
||||
// 状态
|
||||
IsPublic: photo.IsPublic,
|
||||
IsFeatured: photo.IsFeatured,
|
||||
ViewCount: photo.ViewCount,
|
||||
LikeCount: photo.LikeCount,
|
||||
DownloadCount: photo.DownloadCount,
|
||||
SortOrder: photo.SortOrder,
|
||||
|
||||
CreatedAt: photo.CreatedAt,
|
||||
UpdatedAt: photo.UpdatedAt,
|
||||
}
|
||||
|
||||
// 转换关联对象
|
||||
if photo.User.ID != 0 {
|
||||
response.User = ConvertToUserResponse(&photo.User)
|
||||
}
|
||||
if photo.Category != nil {
|
||||
response.Category = ConvertToCategoryResponse(photo.Category)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ConvertToPhotoListItem 将照片实体转换为列表项DTO
|
||||
func ConvertToPhotoListItem(photo *entity.Photo) PhotoListItem {
|
||||
return PhotoListItem{
|
||||
ID: photo.ID,
|
||||
Title: photo.Title,
|
||||
ThumbnailURL: photo.ThumbnailURL,
|
||||
Width: photo.Width,
|
||||
Height: photo.Height,
|
||||
AspectRatio: photo.GetAspectRatio(),
|
||||
IsPublic: photo.IsPublic,
|
||||
IsFeatured: photo.IsFeatured,
|
||||
ViewCount: photo.ViewCount,
|
||||
LikeCount: photo.LikeCount,
|
||||
CreatedAt: photo.CreatedAt,
|
||||
}
|
||||
}
|
||||
@ -1,135 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// CreateTagRequest 创建标签请求
|
||||
type CreateTagRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=50" validate:"required,min=1,max=50"`
|
||||
Color string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
|
||||
}
|
||||
|
||||
// UpdateTagRequest 更新标签请求
|
||||
type UpdateTagRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,min=1,max=50" validate:"omitempty,min=1,max=50"`
|
||||
Color *string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
|
||||
IsActive *bool `json:"is_active" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// TagResponse 标签响应
|
||||
type TagResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
UseCount int `json:"use_count"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsPopular bool `json:"is_popular"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TagListItem 标签列表项(简化版)
|
||||
type TagListItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
UseCount int `json:"use_count"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsPopular bool `json:"is_popular"`
|
||||
}
|
||||
|
||||
// ListTagsOptions 标签列表查询选项
|
||||
type ListTagsOptions struct {
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id name use_count created_at updated_at" validate:"omitempty,oneof=id name use_count created_at updated_at"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
|
||||
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
Popular bool `json:"popular" form:"popular" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// TagListResponse 标签列表响应
|
||||
type TagListResponse struct {
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// TagCloudResponse 标签云响应
|
||||
type TagCloudResponse struct {
|
||||
Tags []TagCloudItem `json:"tags"`
|
||||
}
|
||||
|
||||
// TagCloudItem 标签云项
|
||||
type TagCloudItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
UseCount int `json:"use_count"`
|
||||
Weight int `json:"weight"` // 1-10 的权重,用于控制标签大小
|
||||
}
|
||||
|
||||
// TagStatsResponse 标签统计响应
|
||||
type TagStatsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Popular []TagResponse `json:"popular"`
|
||||
PhotoCounts map[string]int64 `json:"photo_counts"`
|
||||
Recent []TagResponse `json:"recent"`
|
||||
}
|
||||
|
||||
// ConvertToTagResponse 将标签实体转换为响应DTO
|
||||
func ConvertToTagResponse(tag *entity.Tag) *TagResponse {
|
||||
if tag == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &TagResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
UseCount: tag.UseCount,
|
||||
IsActive: tag.IsActive,
|
||||
IsPopular: tag.IsPopular(),
|
||||
CreatedAt: tag.CreatedAt,
|
||||
UpdatedAt: tag.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToTagListItem 将标签实体转换为列表项DTO
|
||||
func ConvertToTagListItem(tag *entity.Tag) TagListItem {
|
||||
return TagListItem{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
UseCount: tag.UseCount,
|
||||
IsActive: tag.IsActive,
|
||||
IsPopular: tag.IsPopular(),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToTagCloudItem 将标签实体转换为标签云项
|
||||
func ConvertToTagCloudItem(tag *entity.Tag, maxUseCount int) TagCloudItem {
|
||||
// 计算权重(1-10)
|
||||
weight := 1
|
||||
if maxUseCount > 0 {
|
||||
weight = int(float64(tag.UseCount)/float64(maxUseCount)*9) + 1
|
||||
if weight > 10 {
|
||||
weight = 10
|
||||
}
|
||||
}
|
||||
|
||||
return TagCloudItem{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
UseCount: tag.UseCount,
|
||||
Weight: weight,
|
||||
}
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// CreateUserRequest 创建用户请求
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50" validate:"required,min=3,max=50"`
|
||||
Email string `json:"email" binding:"required,email" validate:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6" validate:"required,min=6"`
|
||||
Name string `json:"name" binding:"max=100" validate:"max=100"`
|
||||
Role entity.UserRole `json:"role" binding:"omitempty,oneof=user admin photographer" validate:"omitempty,oneof=user admin photographer"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
Username *string `json:"username" binding:"omitempty,min=3,max=50" validate:"omitempty,min=3,max=50"`
|
||||
Email *string `json:"email" binding:"omitempty,email" validate:"omitempty,email"`
|
||||
Name *string `json:"name" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
Avatar *string `json:"avatar" binding:"omitempty,url" validate:"omitempty,url"`
|
||||
Bio *string `json:"bio" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
|
||||
Website *string `json:"website" binding:"omitempty,url" validate:"omitempty,url"`
|
||||
Location *string `json:"location" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
IsActive *bool `json:"is_active" binding:"omitempty"`
|
||||
IsPublic *bool `json:"is_public" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required" validate:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// UserResponse 用户响应
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Bio string `json:"bio"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
Role entity.UserRole `json:"role"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
LoginCount int `json:"login_count"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
AlbumCount int64 `json:"album_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UserProfileResponse 用户档案响应(公开信息)
|
||||
type UserProfileResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Bio string `json:"bio"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
Role entity.UserRole `json:"role"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
AlbumCount int64 `json:"album_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListUsersOptions 用户列表查询选项
|
||||
type ListUsersOptions struct {
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id username email created_at updated_at" validate:"omitempty,oneof=id username email created_at updated_at"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
Role entity.UserRole `json:"role" form:"role" binding:"omitempty,oneof=user admin photographer" validate:"omitempty,oneof=user admin photographer"`
|
||||
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
|
||||
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
|
||||
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
// UserListResponse 用户列表响应
|
||||
type UserListResponse struct {
|
||||
Users []UserResponse `json:"users"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// UserStatsResponse 用户统计响应
|
||||
type UserStatsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Inactive int64 `json:"inactive"`
|
||||
Verified int64 `json:"verified"`
|
||||
Unverified int64 `json:"unverified"`
|
||||
RoleCounts map[entity.UserRole]int64 `json:"role_counts"`
|
||||
RecentLogins []UserResponse `json:"recent_logins"`
|
||||
}
|
||||
|
||||
// ConvertToUserResponse 将用户实体转换为响应DTO
|
||||
func ConvertToUserResponse(user *entity.User) *UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
Avatar: user.Avatar,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
IsPublic: user.IsPublic,
|
||||
EmailVerified: user.EmailVerified,
|
||||
LastLogin: user.LastLogin,
|
||||
LoginCount: user.LoginCount,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToUserProfile 将用户实体转换为公开档案DTO
|
||||
func ConvertToUserProfile(user *entity.User) *UserProfileResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
Avatar: user.Avatar,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
Role: user.Role,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Album 相册实体
|
||||
type Album struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
Title string `json:"title" gorm:"not null;size:200"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Slug string `json:"slug" gorm:"uniqueIndex;size:255"`
|
||||
CoverPhotoID *uint `json:"cover_photo_id" gorm:"index"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
CategoryID *uint `json:"category_id" gorm:"index"`
|
||||
IsPublic bool `json:"is_public" gorm:"default:true;index"`
|
||||
IsFeatured bool `json:"is_featured" gorm:"default:false;index"`
|
||||
Password string `json:"-" gorm:"size:255"` // 私密相册密码
|
||||
ViewCount int `json:"view_count" gorm:"default:0;index"`
|
||||
LikeCount int `json:"like_count" gorm:"default:0;index"`
|
||||
PhotoCount int `json:"photo_count" gorm:"default:0;index"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0;index"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
|
||||
// 关联
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
|
||||
CoverPhoto *Photo `json:"cover_photo,omitempty" gorm:"foreignKey:CoverPhotoID"`
|
||||
Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:AlbumID"`
|
||||
}
|
||||
|
||||
// AlbumStats 相册统计信息
|
||||
type AlbumStats struct {
|
||||
Total int64 `json:"total"` // 总相册数
|
||||
Published int64 `json:"published"` // 已发布相册数
|
||||
Private int64 `json:"private"` // 私有相册数
|
||||
Featured int64 `json:"featured"` // 精选相册数
|
||||
TotalViews int64 `json:"total_views"` // 总浏览量
|
||||
TotalLikes int64 `json:"total_likes"` // 总点赞数
|
||||
TotalPhotos int64 `json:"total_photos"` // 总照片数
|
||||
CategoryCounts map[string]int64 `json:"category_counts"` // 各分类相册数量
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Album) TableName() string {
|
||||
return "albums"
|
||||
}
|
||||
|
||||
// HasPassword 检查是否设置了密码
|
||||
func (a *Album) HasPassword() bool {
|
||||
return a.Password != ""
|
||||
}
|
||||
|
||||
// IsEmpty 检查相册是否为空
|
||||
func (a *Album) IsEmpty() bool {
|
||||
return a.PhotoCount == 0
|
||||
}
|
||||
|
||||
// CanViewBy 检查指定用户是否可以查看相册
|
||||
func (a *Album) CanViewBy(user *User) bool {
|
||||
// 公开相册
|
||||
if a.IsPublic && !a.HasPassword() {
|
||||
return true
|
||||
}
|
||||
|
||||
// 相册所有者或管理员
|
||||
if user != nil && (user.ID == a.UserID || user.IsAdmin()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CanEditBy 检查指定用户是否可以编辑相册
|
||||
func (a *Album) CanEditBy(user *User) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
return user.ID == a.UserID || user.IsAdmin()
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Category 分类实体
|
||||
type Category struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
Name string `json:"name" gorm:"not null;size:100"`
|
||||
Slug string `json:"slug" gorm:"uniqueIndex;not null;size:100"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
ParentID *uint `json:"parent_id" gorm:"index"`
|
||||
Color string `json:"color" gorm:"default:#3b82f6;size:7"`
|
||||
CoverImage string `json:"cover_image" gorm:"size:500"`
|
||||
Sort int `json:"sort" gorm:"default:0;index"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0;index"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true;index"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
|
||||
// 关联
|
||||
Parent *Category `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
|
||||
Children []Category `json:"children,omitempty" gorm:"foreignKey:ParentID"`
|
||||
Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:CategoryID"`
|
||||
Albums []Album `json:"albums,omitempty" gorm:"foreignKey:CategoryID"`
|
||||
PhotoCount int64 `json:"photo_count" gorm:"-"` // 照片数量,不存储在数据库中
|
||||
}
|
||||
|
||||
// CategoryTree 分类树结构(用于前端显示)
|
||||
type CategoryTree struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color string `json:"color"`
|
||||
CoverImage string `json:"cover_image"`
|
||||
Sort int `json:"sort"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
Children []CategoryTree `json:"children"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CategoryStats 分类统计信息
|
||||
type CategoryStats struct {
|
||||
Total int64 `json:"total"` // 总分类数
|
||||
Active int64 `json:"active"` // 活跃分类数
|
||||
TopLevel int64 `json:"top_level"` // 顶级分类数
|
||||
TotalCategories int64 `json:"total_categories"` // 总分类数(别名)
|
||||
MaxLevel int64 `json:"max_level"` // 最大层级
|
||||
FeaturedCount int64 `json:"featured_count"` // 特色分类数
|
||||
PhotoCounts map[string]int64 `json:"photo_counts"` // 各分类照片数量
|
||||
}
|
||||
|
||||
// CategoryListParams 分类列表查询参数
|
||||
type CategoryListParams struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Search string `json:"search" form:"search"`
|
||||
ParentID *uint `json:"parent_id" form:"parent_id"`
|
||||
IsActive *bool `json:"is_active" form:"is_active"`
|
||||
IncludeStats bool `json:"include_stats" form:"include_stats"`
|
||||
SortBy string `json:"sort_by" form:"sort_by"`
|
||||
Order string `json:"order" form:"order"`
|
||||
}
|
||||
|
||||
// CreateCategoryRequest 创建分类请求
|
||||
type CreateCategoryRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Slug string `json:"slug" binding:"required,max=100"`
|
||||
Description string `json:"description" binding:"max=500"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color string `json:"color" binding:"max=7"`
|
||||
CoverImage string `json:"cover_image" binding:"max=500"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// UpdateCategoryRequest 更新分类请求
|
||||
type UpdateCategoryRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Slug *string `json:"slug" binding:"omitempty,max=100"`
|
||||
Description *string `json:"description" binding:"max=500"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color *string `json:"color" binding:"omitempty,max=7"`
|
||||
CoverImage *string `json:"cover_image" binding:"omitempty,max=500"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// ReorderCategoriesRequest 重新排序分类请求
|
||||
type ReorderCategoriesRequest struct {
|
||||
ParentID *uint `json:"parent_id"`
|
||||
CategoryIDs []uint `json:"category_ids" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// GenerateSlugRequest 生成slug请求
|
||||
type GenerateSlugRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
}
|
||||
|
||||
// GenerateSlugResponse 生成slug响应
|
||||
type GenerateSlugResponse struct {
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
// SuccessResponse 成功响应
|
||||
type SuccessResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Category) TableName() string {
|
||||
return "categories"
|
||||
}
|
||||
|
||||
// IsTopLevel 检查是否为顶级分类
|
||||
func (c *Category) IsTopLevel() bool {
|
||||
return c.ParentID == nil
|
||||
}
|
||||
|
||||
// HasChildren 检查是否有子分类
|
||||
func (c *Category) HasChildren() bool {
|
||||
return len(c.Children) > 0
|
||||
}
|
||||
@ -1,244 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Photo 照片实体
|
||||
type Photo struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
Title string `json:"title" gorm:"not null;size:200"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Filename string `json:"filename" gorm:"not null;size:255"`
|
||||
OriginalFilename string `json:"original_filename" gorm:"not null;size:255"`
|
||||
UniqueFilename string `json:"unique_filename" gorm:"not null;size:255"`
|
||||
FilePath string `json:"file_path" gorm:"not null;size:500"`
|
||||
OriginalURL string `json:"original_url" gorm:"not null;size:500"`
|
||||
ThumbnailURL string `json:"thumbnail_url" gorm:"size:500"`
|
||||
MediumURL string `json:"medium_url" gorm:"size:500"`
|
||||
LargeURL string `json:"large_url" gorm:"size:500"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type" gorm:"size:100"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Status PhotoStatus `json:"status" gorm:"default:active;size:20"`
|
||||
|
||||
// EXIF 信息
|
||||
Camera string `json:"camera" gorm:"size:100"`
|
||||
Lens string `json:"lens" gorm:"size:100"`
|
||||
CameraMake string `json:"camera_make" gorm:"size:100"`
|
||||
CameraModel string `json:"camera_model" gorm:"size:100"`
|
||||
LensModel string `json:"lens_model" gorm:"size:100"`
|
||||
FocalLength *float64 `json:"focal_length" gorm:"type:decimal(5,2)"`
|
||||
Aperture *float64 `json:"aperture" gorm:"type:decimal(3,1)"`
|
||||
ShutterSpeed string `json:"shutter_speed" gorm:"size:20"`
|
||||
ISO *int `json:"iso"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
|
||||
// 地理位置信息
|
||||
LocationName string `json:"location_name" gorm:"size:200"`
|
||||
Latitude *float64 `json:"latitude" gorm:"type:decimal(10,8)"`
|
||||
Longitude *float64 `json:"longitude" gorm:"type:decimal(11,8)"`
|
||||
|
||||
// 关联
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
AlbumID *uint `json:"album_id" gorm:"index"`
|
||||
CategoryID *uint `json:"category_id" gorm:"index"`
|
||||
|
||||
// 状态和统计
|
||||
IsPublic bool `json:"is_public" gorm:"default:true;index"`
|
||||
IsFeatured bool `json:"is_featured" gorm:"default:false;index"`
|
||||
ViewCount int `json:"view_count" gorm:"default:0;index"`
|
||||
LikeCount int `json:"like_count" gorm:"default:0;index"`
|
||||
DownloadCount int `json:"download_count" gorm:"default:0"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0;index"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
|
||||
// 关联对象
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Album *Album `json:"album,omitempty" gorm:"foreignKey:AlbumID"`
|
||||
Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
|
||||
Tags []Tag `json:"tags,omitempty" gorm:"many2many:photo_tags;"`
|
||||
}
|
||||
|
||||
// PhotoStatus 照片状态枚举
|
||||
type PhotoStatus string
|
||||
|
||||
const (
|
||||
PhotoStatusActive PhotoStatus = "active"
|
||||
PhotoStatusInactive PhotoStatus = "inactive"
|
||||
PhotoStatusDeleted PhotoStatus = "deleted"
|
||||
PhotoStatusDraft PhotoStatus = "draft"
|
||||
PhotoStatusPrivate PhotoStatus = "private"
|
||||
)
|
||||
|
||||
// Status constants for compatibility
|
||||
const (
|
||||
StatusPublished PhotoStatus = "active"
|
||||
StatusDraft PhotoStatus = "draft"
|
||||
StatusArchived PhotoStatus = "inactive"
|
||||
)
|
||||
|
||||
// PhotoTag 照片标签关联表
|
||||
type PhotoTag struct {
|
||||
PhotoID uint `json:"photo_id" gorm:"primaryKey"`
|
||||
TagID uint `json:"tag_id" gorm:"primaryKey"`
|
||||
}
|
||||
|
||||
// PhotoStats 照片统计信息
|
||||
type PhotoStats struct {
|
||||
Total int64 `json:"total"` // 总照片数
|
||||
Published int64 `json:"published"` // 已发布照片数
|
||||
Private int64 `json:"private"` // 私有照片数
|
||||
Featured int64 `json:"featured"` // 精选照片数
|
||||
TotalViews int64 `json:"total_views"` // 总浏览量
|
||||
TotalLikes int64 `json:"total_likes"` // 总点赞数
|
||||
TotalDownloads int64 `json:"total_downloads"` // 总下载数
|
||||
FileSize int64 `json:"file_size"` // 总文件大小
|
||||
TotalSize int64 `json:"total_size"` // 总大小(别名)
|
||||
ThisMonth int64 `json:"this_month"` // 本月新增
|
||||
Today int64 `json:"today"` // 今日新增
|
||||
StatusStats map[string]int64 `json:"status_stats"` // 状态统计
|
||||
CategoryCounts map[string]int64 `json:"category_counts"` // 各分类照片数量
|
||||
TagCounts map[string]int64 `json:"tag_counts"` // 各标签照片数量
|
||||
}
|
||||
|
||||
// PhotoListParams 照片列表查询参数
|
||||
type PhotoListParams struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Sort string `json:"sort" form:"sort"`
|
||||
Order string `json:"order" form:"order"`
|
||||
Search string `json:"search" form:"search"`
|
||||
UserID *uint `json:"user_id" form:"user_id"`
|
||||
Status *PhotoStatus `json:"status" form:"status"`
|
||||
CategoryID *uint `json:"category_id" form:"category_id"`
|
||||
TagID *uint `json:"tag_id" form:"tag_id"`
|
||||
DateFrom *time.Time `json:"date_from" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to" form:"date_to"`
|
||||
}
|
||||
|
||||
// CreatePhotoRequest 创建照片请求
|
||||
type CreatePhotoRequest struct {
|
||||
Title string `json:"title" binding:"required,max=200"`
|
||||
Description string `json:"description" binding:"max=1000"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
Status string `json:"status" binding:"oneof=active inactive"`
|
||||
Camera string `json:"camera" binding:"max=100"`
|
||||
Lens string `json:"lens" binding:"max=100"`
|
||||
ISO *int `json:"iso"`
|
||||
Aperture *float64 `json:"aperture"`
|
||||
ShutterSpeed string `json:"shutter_speed" binding:"max=20"`
|
||||
FocalLength *float64 `json:"focal_length"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
CategoryIDs []uint `json:"category_ids"`
|
||||
TagIDs []uint `json:"tag_ids"`
|
||||
}
|
||||
|
||||
// UpdatePhotoRequest 更新照片请求
|
||||
type UpdatePhotoRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||
Description *string `json:"description" binding:"max=1000"`
|
||||
Status *string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||
Camera *string `json:"camera" binding:"omitempty,max=100"`
|
||||
Lens *string `json:"lens" binding:"omitempty,max=100"`
|
||||
ISO *int `json:"iso"`
|
||||
Aperture *float64 `json:"aperture"`
|
||||
ShutterSpeed *string `json:"shutter_speed" binding:"omitempty,max=20"`
|
||||
FocalLength *float64 `json:"focal_length"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
CategoryIDs *[]uint `json:"category_ids"`
|
||||
TagIDs *[]uint `json:"tag_ids"`
|
||||
}
|
||||
|
||||
// BatchUpdatePhotosRequest 批量更新照片请求
|
||||
type BatchUpdatePhotosRequest struct {
|
||||
Status *string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||
CategoryIDs *[]uint `json:"category_ids"`
|
||||
TagIDs *[]uint `json:"tag_ids"`
|
||||
}
|
||||
|
||||
// PhotoFormat 照片格式
|
||||
type PhotoFormat struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
PhotoID uint `json:"photo_id" gorm:"not null;index"`
|
||||
Format string `json:"format" gorm:"not null;size:20"` // jpg, png, webp
|
||||
Quality int `json:"quality" gorm:"not null"` // 1-100
|
||||
Width int `json:"width" gorm:"not null"`
|
||||
Height int `json:"height" gorm:"not null"`
|
||||
FileSize int64 `json:"file_size" gorm:"not null"`
|
||||
URL string `json:"url" gorm:"not null;size:500"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (PhotoFormat) TableName() string {
|
||||
return "photo_formats"
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Photo) TableName() string {
|
||||
return "photos"
|
||||
}
|
||||
|
||||
// TableName 指定关联表名
|
||||
func (PhotoTag) TableName() string {
|
||||
return "photo_tags"
|
||||
}
|
||||
|
||||
// GetAspectRatio 获取宽高比
|
||||
func (p *Photo) GetAspectRatio() float64 {
|
||||
if p.Height == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(p.Width) / float64(p.Height)
|
||||
}
|
||||
|
||||
// IsLandscape 是否为横向
|
||||
func (p *Photo) IsLandscape() bool {
|
||||
return p.Width > p.Height
|
||||
}
|
||||
|
||||
// IsPortrait 是否为纵向
|
||||
func (p *Photo) IsPortrait() bool {
|
||||
return p.Width < p.Height
|
||||
}
|
||||
|
||||
// IsSquare 是否为正方形
|
||||
func (p *Photo) IsSquare() bool {
|
||||
return p.Width == p.Height
|
||||
}
|
||||
|
||||
// HasLocation 是否有地理位置信息
|
||||
func (p *Photo) HasLocation() bool {
|
||||
return p.Latitude != nil && p.Longitude != nil
|
||||
}
|
||||
|
||||
// HasEXIF 是否有EXIF信息
|
||||
func (p *Photo) HasEXIF() bool {
|
||||
return p.CameraMake != "" || p.CameraModel != "" || p.TakenAt != nil
|
||||
}
|
||||
|
||||
// GetDisplayURL 获取显示URL(根据尺寸)
|
||||
func (p *Photo) GetDisplayURL(size string) string {
|
||||
switch size {
|
||||
case "thumbnail":
|
||||
if p.ThumbnailURL != "" {
|
||||
return p.ThumbnailURL
|
||||
}
|
||||
case "medium":
|
||||
if p.MediumURL != "" {
|
||||
return p.MediumURL
|
||||
}
|
||||
case "large":
|
||||
if p.LargeURL != "" {
|
||||
return p.LargeURL
|
||||
}
|
||||
}
|
||||
return p.OriginalURL
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tag 标签实体
|
||||
type Tag struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;not null;size:50"`
|
||||
Slug string `json:"slug" gorm:"uniqueIndex;not null;size:50"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Color string `json:"color" gorm:"default:#6b7280;size:7"`
|
||||
UseCount int `json:"use_count" gorm:"default:0;index"`
|
||||
PhotoCount int64 `json:"photo_count" gorm:"-"` // 不存储在数据库中
|
||||
IsActive bool `json:"is_active" gorm:"default:true;index"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
|
||||
// 关联
|
||||
Photos []Photo `json:"photos,omitempty" gorm:"many2many:photo_tags;"`
|
||||
}
|
||||
|
||||
// TagStats 标签统计信息
|
||||
type TagStats struct {
|
||||
Total int64 `json:"total"` // 总标签数
|
||||
Active int64 `json:"active"` // 活跃标签数
|
||||
Used int64 `json:"used"` // 已使用标签数
|
||||
Unused int64 `json:"unused"` // 未使用标签数
|
||||
AvgPhotosPerTag float64 `json:"avg_photos_per_tag"` // 平均每个标签的照片数
|
||||
Popular []Tag `json:"popular"` // 热门标签
|
||||
PhotoCounts map[string]int64 `json:"photo_counts"` // 各标签照片数量
|
||||
}
|
||||
|
||||
// TagListParams 标签列表查询参数
|
||||
type TagListParams struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Search string `json:"search" form:"search"`
|
||||
IsActive *bool `json:"is_active" form:"is_active"`
|
||||
SortBy string `json:"sort_by" form:"sort_by"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
||||
}
|
||||
|
||||
// CreateTagRequest 创建标签请求
|
||||
type CreateTagRequest struct {
|
||||
Name string `json:"name" binding:"required,max=50"`
|
||||
Slug string `json:"slug" binding:"required,max=50"`
|
||||
Description string `json:"description" binding:"max=500"`
|
||||
Color string `json:"color" binding:"max=7"`
|
||||
}
|
||||
|
||||
// UpdateTagRequest 更新标签请求
|
||||
type UpdateTagRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=50"`
|
||||
Slug *string `json:"slug" binding:"omitempty,max=50"`
|
||||
Description *string `json:"description" binding:"max=500"`
|
||||
Color *string `json:"color" binding:"omitempty,max=7"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// TagWithCount 带有照片数量的标签
|
||||
type TagWithCount struct {
|
||||
Tag
|
||||
PhotoCount int64 `json:"photo_count"`
|
||||
}
|
||||
|
||||
// TagCloudItem 标签云项目
|
||||
type TagCloudItem struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Color string `json:"color"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Tag) TableName() string {
|
||||
return "tags"
|
||||
}
|
||||
|
||||
// IsPopular 检查是否为热门标签(使用次数 >= 10)
|
||||
func (t *Tag) IsPopular() bool {
|
||||
return t.UseCount >= 10
|
||||
}
|
||||
|
||||
// IncrementUseCount 增加使用次数
|
||||
func (t *Tag) IncrementUseCount() {
|
||||
t.UseCount++
|
||||
}
|
||||
|
||||
// DecrementUseCount 减少使用次数
|
||||
func (t *Tag) DecrementUseCount() {
|
||||
if t.UseCount > 0 {
|
||||
t.UseCount--
|
||||
}
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserRole 用户角色枚举
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
UserRoleUser UserRole = "user"
|
||||
UserRoleAdmin UserRole = "admin"
|
||||
UserRolePhotographer UserRole = "photographer"
|
||||
)
|
||||
|
||||
// User 用户实体
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
Username string `json:"username" gorm:"uniqueIndex;not null;size:50"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null;size:100"`
|
||||
Password string `json:"-" gorm:"not null;size:255"`
|
||||
Name string `json:"name" gorm:"size:100"`
|
||||
Avatar string `json:"avatar" gorm:"size:500"`
|
||||
Bio string `json:"bio" gorm:"type:text"`
|
||||
Website string `json:"website" gorm:"size:200"`
|
||||
Location string `json:"location" gorm:"size:100"`
|
||||
Role UserRole `json:"role" gorm:"default:user;size:20"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true"`
|
||||
IsPublic bool `json:"is_public" gorm:"default:true"`
|
||||
EmailVerified bool `json:"email_verified" gorm:"default:false"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
LoginCount int `json:"login_count" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
|
||||
// 关联
|
||||
Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:UserID"`
|
||||
Albums []Album `json:"albums,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// IsAdmin 检查是否为管理员
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role == UserRoleAdmin
|
||||
}
|
||||
|
||||
// IsPhotographer 检查是否为摄影师
|
||||
func (u *User) IsPhotographer() bool {
|
||||
return u.Role == UserRolePhotographer || u.Role == UserRoleAdmin
|
||||
}
|
||||
|
||||
// CanManagePhoto 检查是否可以管理指定照片
|
||||
func (u *User) CanManagePhoto(photo *Photo) bool {
|
||||
return u.ID == photo.UserID || u.IsAdmin()
|
||||
}
|
||||
|
||||
// CanManageAlbum 检查是否可以管理指定相册
|
||||
func (u *User) CanManageAlbum(album *Album) bool {
|
||||
return u.ID == album.UserID || u.IsAdmin()
|
||||
}
|
||||
|
||||
// UserStats 用户统计信息
|
||||
type UserStats struct {
|
||||
Total int64 `json:"total"` // 总用户数
|
||||
Active int64 `json:"active"` // 活跃用户数
|
||||
ThisMonth int64 `json:"this_month"` // 本月新增
|
||||
Today int64 `json:"today"` // 今日新增
|
||||
RoleStats map[string]int64 `json:"role_stats"` // 角色统计
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Name string `json:"name" binding:"max=100"`
|
||||
Role UserRole `json:"role" binding:"oneof=user admin photographer"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
|
||||
Email *string `json:"email" binding:"omitempty,email"`
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Avatar *string `json:"avatar" binding:"omitempty,max=500"`
|
||||
Bio *string `json:"bio" binding:"omitempty,max=1000"`
|
||||
Website *string `json:"website" binding:"omitempty,max=200"`
|
||||
Location *string `json:"location" binding:"omitempty,max=100"`
|
||||
Role *UserRole `json:"role" binding:"omitempty,oneof=user admin photographer"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UpdateCurrentUserRequest 更新当前用户请求
|
||||
type UpdateCurrentUserRequest struct {
|
||||
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
|
||||
Email *string `json:"email" binding:"omitempty,email"`
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Avatar *string `json:"avatar" binding:"omitempty,max=500"`
|
||||
Bio *string `json:"bio" binding:"omitempty,max=1000"`
|
||||
Website *string `json:"website" binding:"omitempty,max=200"`
|
||||
Location *string `json:"location" binding:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// UserStatus 用户状态
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusInactive UserStatus = "inactive"
|
||||
UserStatusBanned UserStatus = "banned"
|
||||
UserStatusPending UserStatus = "pending"
|
||||
)
|
||||
|
||||
// UserListParams 用户列表查询参数
|
||||
type UserListParams struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Sort string `json:"sort"`
|
||||
Order string `json:"order"`
|
||||
Role *UserRole `json:"role"`
|
||||
Status *UserStatus `json:"status"`
|
||||
Search string `json:"search"`
|
||||
CreatedFrom *time.Time `json:"created_from"`
|
||||
CreatedTo *time.Time `json:"created_to"`
|
||||
LastLoginFrom *time.Time `json:"last_login_from"`
|
||||
LastLoginTo *time.Time `json:"last_login_to"`
|
||||
}
|
||||
|
||||
// UserGlobalStats 全局用户统计信息
|
||||
type UserGlobalStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Admins int64 `json:"admins"`
|
||||
Editors int64 `json:"editors"`
|
||||
Users int64 `json:"users"`
|
||||
MonthlyRegistrations int64 `json:"monthly_registrations"`
|
||||
}
|
||||
101
backend/internal/model/photomodel.go
Executable file
101
backend/internal/model/photomodel.go
Executable file
@ -0,0 +1,101 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
var _ PhotoModel = (*customPhotoModel)(nil)
|
||||
|
||||
type (
|
||||
// PhotoModel is an interface to be customized, add more methods here,
|
||||
// and implement the added methods in customPhotoModel.
|
||||
PhotoModel interface {
|
||||
photoModel
|
||||
withSession(session sqlx.Session) PhotoModel
|
||||
FindList(ctx context.Context, page, pageSize int, categoryId, userId int64, keyword string) ([]*Photo, error)
|
||||
Count(ctx context.Context, categoryId, userId int64, keyword string) (int64, error)
|
||||
}
|
||||
|
||||
customPhotoModel struct {
|
||||
*defaultPhotoModel
|
||||
}
|
||||
)
|
||||
|
||||
// NewPhotoModel returns a model for the database table.
|
||||
func NewPhotoModel(conn sqlx.SqlConn) PhotoModel {
|
||||
return &customPhotoModel{
|
||||
defaultPhotoModel: newPhotoModel(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *customPhotoModel) withSession(session sqlx.Session) PhotoModel {
|
||||
return NewPhotoModel(sqlx.NewSqlConnFromSession(session))
|
||||
}
|
||||
|
||||
// FindList 分页查询照片列表
|
||||
func (m *customPhotoModel) FindList(ctx context.Context, page, pageSize int, categoryId, userId int64, keyword string) ([]*Photo, error) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if categoryId > 0 {
|
||||
conditions = append(conditions, "`category_id` = ?")
|
||||
args = append(args, categoryId)
|
||||
}
|
||||
|
||||
if userId > 0 {
|
||||
conditions = append(conditions, "`user_id` = ?")
|
||||
args = append(args, userId)
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
conditions = append(conditions, "(`title` LIKE ? OR `description` LIKE ?)")
|
||||
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
whereClause := ""
|
||||
if len(conditions) > 0 {
|
||||
whereClause = " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
args = append(args, pageSize, offset)
|
||||
|
||||
query := fmt.Sprintf("select %s from %s%s ORDER BY `created_at` DESC LIMIT ? OFFSET ?", photoRows, m.table, whereClause)
|
||||
var resp []*Photo
|
||||
err := m.conn.QueryRowsCtx(ctx, &resp, query, args...)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Count 统计照片数量
|
||||
func (m *customPhotoModel) Count(ctx context.Context, categoryId, userId int64, keyword string) (int64, error) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if categoryId > 0 {
|
||||
conditions = append(conditions, "`category_id` = ?")
|
||||
args = append(args, categoryId)
|
||||
}
|
||||
|
||||
if userId > 0 {
|
||||
conditions = append(conditions, "`user_id` = ?")
|
||||
args = append(args, userId)
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
conditions = append(conditions, "(`title` LIKE ? OR `description` LIKE ?)")
|
||||
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
whereClause := ""
|
||||
if len(conditions) > 0 {
|
||||
whereClause = " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("select count(*) from %s%s", m.table, whereClause)
|
||||
var count int64
|
||||
err := m.conn.QueryRowCtx(ctx, &count, query, args...)
|
||||
return count, err
|
||||
}
|
||||
93
backend/internal/model/photomodel_gen.go
Executable file
93
backend/internal/model/photomodel_gen.go
Executable file
@ -0,0 +1,93 @@
|
||||
// Code generated by goctl. DO NOT EDIT.
|
||||
// versions:
|
||||
// goctl version: 1.8.4
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/builder"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
"github.com/zeromicro/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
var (
|
||||
photoFieldNames = builder.RawFieldNames(&Photo{})
|
||||
photoRows = strings.Join(photoFieldNames, ",")
|
||||
photoRowsExpectAutoSet = strings.Join(stringx.Remove(photoFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
|
||||
photoRowsWithPlaceHolder = strings.Join(stringx.Remove(photoFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
|
||||
)
|
||||
|
||||
type (
|
||||
photoModel interface {
|
||||
Insert(ctx context.Context, data *Photo) (sql.Result, error)
|
||||
FindOne(ctx context.Context, id int64) (*Photo, error)
|
||||
Update(ctx context.Context, data *Photo) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
defaultPhotoModel struct {
|
||||
conn sqlx.SqlConn
|
||||
table string
|
||||
}
|
||||
|
||||
Photo struct {
|
||||
Id int64 `db:"id"`
|
||||
Title string `db:"title"` // 照片标题
|
||||
Description sql.NullString `db:"description"` // 照片描述
|
||||
FilePath string `db:"file_path"` // 原图路径
|
||||
ThumbnailPath string `db:"thumbnail_path"` // 缩略图路径
|
||||
UserId int64 `db:"user_id"` // 用户ID
|
||||
CategoryId int64 `db:"category_id"` // 分类ID
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
)
|
||||
|
||||
func newPhotoModel(conn sqlx.SqlConn) *defaultPhotoModel {
|
||||
return &defaultPhotoModel{
|
||||
conn: conn,
|
||||
table: "`photo`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultPhotoModel) Delete(ctx context.Context, id int64) error {
|
||||
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
|
||||
_, err := m.conn.ExecCtx(ctx, query, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultPhotoModel) FindOne(ctx context.Context, id int64) (*Photo, error) {
|
||||
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", photoRows, m.table)
|
||||
var resp Photo
|
||||
err := m.conn.QueryRowCtx(ctx, &resp, query, id)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultPhotoModel) Insert(ctx context.Context, data *Photo) (sql.Result, error) {
|
||||
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, photoRowsExpectAutoSet)
|
||||
ret, err := m.conn.ExecCtx(ctx, query, data.Title, data.Description, data.FilePath, data.ThumbnailPath, data.UserId, data.CategoryId)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (m *defaultPhotoModel) Update(ctx context.Context, data *Photo) error {
|
||||
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, photoRowsWithPlaceHolder)
|
||||
_, err := m.conn.ExecCtx(ctx, query, data.Title, data.Description, data.FilePath, data.ThumbnailPath, data.UserId, data.CategoryId, data.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultPhotoModel) tableName() string {
|
||||
return m.table
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package request
|
||||
|
||||
// PaginationRequest 分页请求
|
||||
type PaginationRequest struct {
|
||||
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
|
||||
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
|
||||
}
|
||||
|
||||
// SortRequest 排序请求
|
||||
type SortRequest struct {
|
||||
Sort string `json:"sort" form:"sort" binding:"omitempty" validate:"omitempty"`
|
||||
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
// SearchRequest 搜索请求
|
||||
type SearchRequest struct {
|
||||
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
// BaseListRequest 基础列表请求
|
||||
type BaseListRequest struct {
|
||||
PaginationRequest
|
||||
SortRequest
|
||||
SearchRequest
|
||||
}
|
||||
|
||||
// IDRequest ID 请求
|
||||
type IDRequest struct {
|
||||
ID uint `json:"id" uri:"id" binding:"required,min=1" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// SlugRequest Slug 请求
|
||||
type SlugRequest struct {
|
||||
Slug string `json:"slug" uri:"slug" binding:"required,min=1" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// BulkIDsRequest 批量 ID 请求
|
||||
type BulkIDsRequest struct {
|
||||
IDs []uint `json:"ids" binding:"required,min=1" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// StatusRequest 状态请求
|
||||
type StatusRequest struct {
|
||||
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// TimeRangeRequest 时间范围请求
|
||||
type TimeRangeRequest struct {
|
||||
StartDate string `json:"start_date" form:"start_date" binding:"omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `json:"end_date" form:"end_date" binding:"omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||
}
|
||||
|
||||
// GetDefaultPagination 获取默认分页参数
|
||||
func (p *PaginationRequest) GetDefaultPagination() (int, int) {
|
||||
page := p.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := p.Limit
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
return page, limit
|
||||
}
|
||||
|
||||
// GetDefaultSort 获取默认排序参数
|
||||
func (s *SortRequest) GetDefaultSort(defaultSort, defaultOrder string) (string, string) {
|
||||
sort := s.Sort
|
||||
if sort == "" {
|
||||
sort = defaultSort
|
||||
}
|
||||
|
||||
order := s.Order
|
||||
if order == "" {
|
||||
order = defaultOrder
|
||||
}
|
||||
|
||||
return sort, order
|
||||
}
|
||||
|
||||
// GetOffset 计算偏移量
|
||||
func (p *PaginationRequest) GetOffset() int {
|
||||
page, limit := p.GetDefaultPagination()
|
||||
return (page - 1) * limit
|
||||
}
|
||||
9
backend/internal/model/sql/category.sql
Normal file
9
backend/internal/model/sql/category.sql
Normal file
@ -0,0 +1,9 @@
|
||||
CREATE TABLE `category` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(64) NOT NULL COMMENT '分类名称',
|
||||
`description` text COMMENT '分类描述',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';
|
||||
15
backend/internal/model/sql/photo.sql
Normal file
15
backend/internal/model/sql/photo.sql
Normal file
@ -0,0 +1,15 @@
|
||||
CREATE TABLE `photo` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(255) NOT NULL COMMENT '照片标题',
|
||||
`description` text COMMENT '照片描述',
|
||||
`file_path` varchar(500) NOT NULL COMMENT '原图路径',
|
||||
`thumbnail_path` varchar(500) NOT NULL COMMENT '缩略图路径',
|
||||
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||||
`category_id` bigint NOT NULL COMMENT '分类ID',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_category_id` (`category_id`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='照片表';
|
||||
13
backend/internal/model/sql/user.sql
Normal file
13
backend/internal/model/sql/user.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE TABLE `user` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(64) NOT NULL COMMENT '用户名',
|
||||
`email` varchar(255) NOT NULL COMMENT '邮箱',
|
||||
`password` varchar(255) NOT NULL COMMENT '密码',
|
||||
`avatar` varchar(255) DEFAULT '' COMMENT '头像',
|
||||
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 1:正常 0:禁用',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_username` (`username`),
|
||||
UNIQUE KEY `idx_email` (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||
81
backend/internal/model/usermodel.go
Executable file
81
backend/internal/model/usermodel.go
Executable file
@ -0,0 +1,81 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
var _ UserModel = (*customUserModel)(nil)
|
||||
|
||||
type (
|
||||
// UserModel is an interface to be customized, add more methods here,
|
||||
// and implement the added methods in customUserModel.
|
||||
UserModel interface {
|
||||
userModel
|
||||
withSession(session sqlx.Session) UserModel
|
||||
FindList(ctx context.Context, page, pageSize int, keyword string) ([]*User, error)
|
||||
Count(ctx context.Context, keyword string) (int64, error)
|
||||
}
|
||||
|
||||
customUserModel struct {
|
||||
*defaultUserModel
|
||||
}
|
||||
)
|
||||
|
||||
// NewUserModel returns a model for the database table.
|
||||
func NewUserModel(conn sqlx.SqlConn) UserModel {
|
||||
return &customUserModel{
|
||||
defaultUserModel: newUserModel(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *customUserModel) withSession(session sqlx.Session) UserModel {
|
||||
return NewUserModel(sqlx.NewSqlConnFromSession(session))
|
||||
}
|
||||
|
||||
// FindList 分页查询用户列表
|
||||
func (m *customUserModel) FindList(ctx context.Context, page, pageSize int, keyword string) ([]*User, error) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if keyword != "" {
|
||||
conditions = append(conditions, "(`username` LIKE ? OR `email` LIKE ?)")
|
||||
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
whereClause := ""
|
||||
if len(conditions) > 0 {
|
||||
whereClause = " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
args = append(args, pageSize, offset)
|
||||
|
||||
query := fmt.Sprintf("select %s from %s%s ORDER BY `created_at` DESC LIMIT ? OFFSET ?", userRows, m.table, whereClause)
|
||||
var resp []*User
|
||||
err := m.conn.QueryRowsCtx(ctx, &resp, query, args...)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Count 统计用户数量
|
||||
func (m *customUserModel) Count(ctx context.Context, keyword string) (int64, error) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if keyword != "" {
|
||||
conditions = append(conditions, "(`username` LIKE ? OR `email` LIKE ?)")
|
||||
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
whereClause := ""
|
||||
if len(conditions) > 0 {
|
||||
whereClause = " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("select count(*) from %s%s", m.table, whereClause)
|
||||
var count int64
|
||||
err := m.conn.QueryRowCtx(ctx, &count, query, args...)
|
||||
return count, err
|
||||
}
|
||||
122
backend/internal/model/usermodel_gen.go
Executable file
122
backend/internal/model/usermodel_gen.go
Executable file
@ -0,0 +1,122 @@
|
||||
// Code generated by goctl. DO NOT EDIT.
|
||||
// versions:
|
||||
// goctl version: 1.8.4
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/builder"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
"github.com/zeromicro/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builder.RawFieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
|
||||
)
|
||||
|
||||
type (
|
||||
userModel interface {
|
||||
Insert(ctx context.Context, data *User) (sql.Result, error)
|
||||
FindOne(ctx context.Context, id int64) (*User, error)
|
||||
FindOneByEmail(ctx context.Context, email string) (*User, error)
|
||||
FindOneByUsername(ctx context.Context, username string) (*User, error)
|
||||
Update(ctx context.Context, data *User) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
defaultUserModel struct {
|
||||
conn sqlx.SqlConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
Username string `db:"username"` // 用户名
|
||||
Email string `db:"email"` // 邮箱
|
||||
Password string `db:"password"` // 密码
|
||||
Avatar string `db:"avatar"` // 头像
|
||||
Status int64 `db:"status"` // 状态 1:正常 0:禁用
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
)
|
||||
|
||||
func newUserModel(conn sqlx.SqlConn) *defaultUserModel {
|
||||
return &defaultUserModel{
|
||||
conn: conn,
|
||||
table: "`user`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) Delete(ctx context.Context, id int64) error {
|
||||
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
|
||||
_, err := m.conn.ExecCtx(ctx, query, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) {
|
||||
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
|
||||
var resp User
|
||||
err := m.conn.QueryRowCtx(ctx, &resp, query, id)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*User, error) {
|
||||
var resp User
|
||||
query := fmt.Sprintf("select %s from %s where `email` = ? limit 1", userRows, m.table)
|
||||
err := m.conn.QueryRowCtx(ctx, &resp, query, email)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOneByUsername(ctx context.Context, username string) (*User, error) {
|
||||
var resp User
|
||||
query := fmt.Sprintf("select %s from %s where `username` = ? limit 1", userRows, m.table)
|
||||
err := m.conn.QueryRowCtx(ctx, &resp, query, username)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) Insert(ctx context.Context, data *User) (sql.Result, error) {
|
||||
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet)
|
||||
ret, err := m.conn.ExecCtx(ctx, query, data.Username, data.Email, data.Password, data.Avatar, data.Status)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) Update(ctx context.Context, newData *User) error {
|
||||
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder)
|
||||
_, err := m.conn.ExecCtx(ctx, query, newData.Username, newData.Email, newData.Password, newData.Avatar, newData.Status, newData.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) tableName() string {
|
||||
return m.table
|
||||
}
|
||||
5
backend/internal/model/vars.go
Normal file
5
backend/internal/model/vars.go
Normal file
@ -0,0 +1,5 @@
|
||||
package model
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
|
||||
var ErrNotFound = sqlx.ErrNotFound
|
||||
Reference in New Issue
Block a user