主要变更: - 采用 go-zero 框架替代 Gin,提升开发效率 - 重构项目结构,API 文件模块化组织 - 将 model 移至 api/internal/model 目录 - 移除 common 包,改为标准 pkg 目录结构 - 实现统一的仓储模式,支持配置驱动数据库切换 - 简化测试策略,专注 API 集成测试 - 更新 CLAUDE.md 文档,提供详细的开发指导 技术栈更新: - 框架: Gin → go-zero v1.6.0+ - 代码生成: 引入 goctl 工具 - 架构模式: 四层架构 → go-zero 三层架构 (Handler→Logic→Model) - 项目布局: 遵循 Go 社区标准和 go-zero 最佳实践
516 lines
14 KiB
Go
516 lines
14 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"photography-backend/internal/model/entity"
|
|
"photography-backend/internal/repository/interfaces"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// userRepositoryImpl 用户仓储实现
|
|
type userRepositoryImpl struct {
|
|
db *gorm.DB
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewUserRepository 创建用户仓储实现
|
|
func NewUserRepository(db *gorm.DB, logger *zap.Logger) interfaces.UserRepository {
|
|
return &userRepositoryImpl{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Create 创建用户
|
|
func (r *userRepositoryImpl) Create(ctx context.Context, user *entity.User) error {
|
|
return r.db.WithContext(ctx).Create(user).Error
|
|
}
|
|
|
|
// GetByID 根据ID获取用户
|
|
func (r *userRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.User, error) {
|
|
var user entity.User
|
|
err := r.db.WithContext(ctx).First(&user, id).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetByEmail 根据邮箱获取用户
|
|
func (r *userRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
|
|
var user entity.User
|
|
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetByUsername 根据用户名获取用户
|
|
func (r *userRepositoryImpl) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
|
|
var user entity.User
|
|
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetByEmailOrUsername 根据邮箱或用户名获取用户
|
|
func (r *userRepositoryImpl) GetByEmailOrUsername(ctx context.Context, emailOrUsername string) (*entity.User, error) {
|
|
var user entity.User
|
|
err := r.db.WithContext(ctx).
|
|
Where("email = ? OR username = ?", emailOrUsername, emailOrUsername).
|
|
First(&user).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// Update 更新用户
|
|
func (r *userRepositoryImpl) Update(ctx context.Context, user *entity.User) error {
|
|
return r.db.WithContext(ctx).Save(user).Error
|
|
}
|
|
|
|
// Delete 删除用户
|
|
func (r *userRepositoryImpl) Delete(ctx context.Context, id uint) error {
|
|
return r.db.WithContext(ctx).Delete(&entity.User{}, id).Error
|
|
}
|
|
|
|
// List 获取用户列表
|
|
func (r *userRepositoryImpl) List(ctx context.Context, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
|
var users []*entity.User
|
|
var total int64
|
|
|
|
query := r.db.WithContext(ctx).Model(&entity.User{})
|
|
|
|
// 应用过滤条件
|
|
if params.Role != nil {
|
|
query = query.Where("role = ?", *params.Role)
|
|
}
|
|
|
|
if params.Status != nil {
|
|
query = query.Where("status = ?", *params.Status)
|
|
}
|
|
|
|
if params.Search != "" {
|
|
query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
|
"%"+params.Search+"%", "%"+params.Search+"%", "%"+params.Search+"%", "%"+params.Search+"%")
|
|
}
|
|
|
|
if params.CreatedFrom != nil {
|
|
query = query.Where("created_at >= ?", *params.CreatedFrom)
|
|
}
|
|
|
|
if params.CreatedTo != nil {
|
|
query = query.Where("created_at <= ?", *params.CreatedTo)
|
|
}
|
|
|
|
if params.LastLoginFrom != nil {
|
|
query = query.Where("last_login_at >= ?", *params.LastLoginFrom)
|
|
}
|
|
|
|
if params.LastLoginTo != nil {
|
|
query = query.Where("last_login_at <= ?", *params.LastLoginTo)
|
|
}
|
|
|
|
// 获取总数
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// 应用排序
|
|
orderBy := "created_at DESC"
|
|
if params.Sort != "" {
|
|
order := "ASC"
|
|
if params.Order == "desc" {
|
|
order = "DESC"
|
|
}
|
|
orderBy = fmt.Sprintf("%s %s", params.Sort, order)
|
|
}
|
|
query = query.Order(orderBy)
|
|
|
|
// 应用分页
|
|
if params.Page > 0 && params.Limit > 0 {
|
|
offset := (params.Page - 1) * params.Limit
|
|
query = query.Offset(offset).Limit(params.Limit)
|
|
}
|
|
|
|
// 查询数据
|
|
if err := query.Find(&users).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return users, total, nil
|
|
}
|
|
|
|
// ListByRole 根据角色获取用户列表
|
|
func (r *userRepositoryImpl) ListByRole(ctx context.Context, role entity.UserRole, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
|
if params == nil {
|
|
params = &entity.UserListParams{}
|
|
}
|
|
params.Role = &role
|
|
return r.List(ctx, params)
|
|
}
|
|
|
|
// ListByStatus 根据状态获取用户列表
|
|
func (r *userRepositoryImpl) ListByStatus(ctx context.Context, status entity.UserStatus, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
|
if params == nil {
|
|
params = &entity.UserListParams{}
|
|
}
|
|
params.Status = &status
|
|
return r.List(ctx, params)
|
|
}
|
|
|
|
// SearchUsers 搜索用户
|
|
func (r *userRepositoryImpl) SearchUsers(ctx context.Context, keyword string, params *entity.UserListParams) ([]*entity.User, int64, error) {
|
|
if params == nil {
|
|
params = &entity.UserListParams{}
|
|
}
|
|
params.Search = keyword
|
|
return r.List(ctx, params)
|
|
}
|
|
|
|
// Count 统计用户总数
|
|
func (r *userRepositoryImpl) Count(ctx context.Context) (int64, error) {
|
|
var count int64
|
|
err := r.db.WithContext(ctx).Model(&entity.User{}).Count(&count).Error
|
|
return count, err
|
|
}
|
|
|
|
// CountByRole 根据角色统计用户数
|
|
func (r *userRepositoryImpl) CountByRole(ctx context.Context, role entity.UserRole) (int64, error) {
|
|
var count int64
|
|
err := r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("role = ?", role).Count(&count).Error
|
|
return count, err
|
|
}
|
|
|
|
// CountByStatus 根据状态统计用户数
|
|
func (r *userRepositoryImpl) CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error) {
|
|
var count int64
|
|
err := r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("status = ?", status).Count(&count).Error
|
|
return count, err
|
|
}
|
|
|
|
// CountActiveUsers 统计活跃用户数
|
|
func (r *userRepositoryImpl) CountActiveUsers(ctx context.Context) (int64, error) {
|
|
return r.CountByStatus(ctx, entity.UserStatusActive)
|
|
}
|
|
|
|
// UpdateStatus 更新用户状态
|
|
func (r *userRepositoryImpl) UpdateStatus(ctx context.Context, id uint, status entity.UserStatus) error {
|
|
return r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("id = ?", id).
|
|
Update("status", status).Error
|
|
}
|
|
|
|
// UpdateLastLogin 更新最后登录时间
|
|
func (r *userRepositoryImpl) UpdateLastLogin(ctx context.Context, id uint) error {
|
|
now := time.Now()
|
|
return r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("id = ?", id).
|
|
Update("last_login_at", now).Error
|
|
}
|
|
|
|
// UpdatePassword 更新密码
|
|
func (r *userRepositoryImpl) UpdatePassword(ctx context.Context, id uint, hashedPassword string) error {
|
|
return r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("id = ?", id).
|
|
Update("password", hashedPassword).Error
|
|
}
|
|
|
|
// SoftDelete 软删除用户
|
|
func (r *userRepositoryImpl) SoftDelete(ctx context.Context, id uint) error {
|
|
return r.db.WithContext(ctx).Delete(&entity.User{}, id).Error
|
|
}
|
|
|
|
// Restore 恢复软删除用户
|
|
func (r *userRepositoryImpl) Restore(ctx context.Context, id uint) error {
|
|
return r.db.WithContext(ctx).Unscoped().Model(&entity.User{}).
|
|
Where("id = ?", id).
|
|
Update("deleted_at", nil).Error
|
|
}
|
|
|
|
// BatchUpdateStatus 批量更新用户状态
|
|
func (r *userRepositoryImpl) BatchUpdateStatus(ctx context.Context, ids []uint, status entity.UserStatus) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("id IN ?", ids).
|
|
Update("status", status).Error
|
|
}
|
|
|
|
// BatchDelete 批量删除用户
|
|
func (r *userRepositoryImpl) BatchDelete(ctx context.Context, ids []uint) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Delete(&entity.User{}, ids).Error
|
|
}
|
|
|
|
// ValidateEmailUnique 验证邮箱唯一性
|
|
func (r *userRepositoryImpl) ValidateEmailUnique(ctx context.Context, email string, excludeID uint) error {
|
|
var count int64
|
|
query := r.db.WithContext(ctx).Model(&entity.User{}).Where("email = ?", email)
|
|
|
|
if excludeID > 0 {
|
|
query = query.Where("id != ?", excludeID)
|
|
}
|
|
|
|
if err := query.Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if count > 0 {
|
|
return errors.New("email already exists")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateUsernameUnique 验证用户名唯一性
|
|
func (r *userRepositoryImpl) ValidateUsernameUnique(ctx context.Context, username string, excludeID uint) error {
|
|
var count int64
|
|
query := r.db.WithContext(ctx).Model(&entity.User{}).Where("username = ?", username)
|
|
|
|
if excludeID > 0 {
|
|
query = query.Where("id != ?", excludeID)
|
|
}
|
|
|
|
if err := query.Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if count > 0 {
|
|
return errors.New("username already exists")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUserPhotos 获取用户照片
|
|
func (r *userRepositoryImpl) GetUserPhotos(ctx context.Context, userID uint, params *entity.PhotoListParams) ([]*entity.Photo, int64, error) {
|
|
var photos []*entity.Photo
|
|
var total int64
|
|
|
|
query := r.db.WithContext(ctx).Model(&entity.Photo{}).Where("user_id = ?", userID)
|
|
|
|
// 应用过滤条件
|
|
if params != nil {
|
|
if params.Status != nil {
|
|
query = query.Where("status = ?", *params.Status)
|
|
}
|
|
|
|
if params.CategoryID != nil {
|
|
query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id").
|
|
Where("photo_categories.category_id = ?", *params.CategoryID)
|
|
}
|
|
|
|
if params.TagID != nil {
|
|
query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
|
|
Where("photo_tags.tag_id = ?", *params.TagID)
|
|
}
|
|
|
|
if params.DateFrom != nil {
|
|
query = query.Where("taken_at >= ?", *params.DateFrom)
|
|
}
|
|
|
|
if params.DateTo != nil {
|
|
query = query.Where("taken_at <= ?", *params.DateTo)
|
|
}
|
|
|
|
if params.Search != "" {
|
|
query = query.Where("title ILIKE ? OR description ILIKE ?",
|
|
"%"+params.Search+"%", "%"+params.Search+"%")
|
|
}
|
|
}
|
|
|
|
// 获取总数
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// 应用排序和分页
|
|
if params != nil {
|
|
orderBy := "created_at DESC"
|
|
if params.Sort != "" {
|
|
order := "ASC"
|
|
if params.Order == "desc" {
|
|
order = "DESC"
|
|
}
|
|
orderBy = fmt.Sprintf("%s %s", params.Sort, order)
|
|
}
|
|
query = query.Order(orderBy)
|
|
|
|
if params.Page > 0 && params.Limit > 0 {
|
|
offset := (params.Page - 1) * params.Limit
|
|
query = query.Offset(offset).Limit(params.Limit)
|
|
}
|
|
}
|
|
|
|
// 预加载关联数据
|
|
query = query.Preload("Categories").Preload("Tags")
|
|
|
|
// 查询数据
|
|
if err := query.Find(&photos).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return photos, total, nil
|
|
}
|
|
|
|
// GetUserStats 获取用户统计信息
|
|
func (r *userRepositoryImpl) GetUserStats(ctx context.Context, userID uint) (*entity.UserStats, error) {
|
|
var stats entity.UserStats
|
|
|
|
// 照片统计
|
|
var photoCount int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
|
Where("user_id = ?", userID).Count(&photoCount).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
stats.PhotoCount = photoCount
|
|
|
|
// 按状态统计照片
|
|
for _, status := range []entity.PhotoStatus{
|
|
entity.PhotoStatusActive,
|
|
entity.PhotoStatusDraft,
|
|
entity.PhotoStatusArchived,
|
|
} {
|
|
var count int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
|
Where("user_id = ? AND status = ?", userID, status).
|
|
Count(&count).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch status {
|
|
case entity.PhotoStatusActive:
|
|
stats.PublishedPhotos = count
|
|
case entity.PhotoStatusDraft:
|
|
stats.DraftPhotos = count
|
|
case entity.PhotoStatusArchived:
|
|
stats.ArchivedPhotos = count
|
|
}
|
|
}
|
|
|
|
// 总浏览数
|
|
var totalViews int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
|
Where("user_id = ?", userID).
|
|
Select("COALESCE(SUM(view_count), 0)").Row().Scan(&totalViews); err != nil {
|
|
return nil, err
|
|
}
|
|
stats.TotalViews = totalViews
|
|
|
|
// 总下载数
|
|
var totalDownloads int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
|
Where("user_id = ?", userID).
|
|
Select("COALESCE(SUM(download_count), 0)").Row().Scan(&totalDownloads); err != nil {
|
|
return nil, err
|
|
}
|
|
stats.TotalDownloads = totalDownloads
|
|
|
|
// 存储空间使用
|
|
var storageUsed int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
|
Where("user_id = ?", userID).
|
|
Select("COALESCE(SUM(file_size), 0)").Row().Scan(&storageUsed); err != nil {
|
|
return nil, err
|
|
}
|
|
stats.StorageUsed = storageUsed
|
|
|
|
// 本月新增照片
|
|
now := time.Now()
|
|
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
|
|
|
var monthlyPhotos int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.Photo{}).
|
|
Where("user_id = ? AND created_at >= ? AND created_at <= ?", userID, startOfMonth, endOfMonth).
|
|
Count(&monthlyPhotos).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
stats.MonthlyPhotos = monthlyPhotos
|
|
|
|
return &stats, nil
|
|
}
|
|
|
|
// GetAllStats 获取全部用户统计信息
|
|
func (r *userRepositoryImpl) GetAllStats(ctx context.Context) (*entity.UserGlobalStats, error) {
|
|
var stats entity.UserGlobalStats
|
|
|
|
// 总用户数
|
|
if total, err := r.Count(ctx); err != nil {
|
|
return nil, err
|
|
} else {
|
|
stats.Total = total
|
|
}
|
|
|
|
// 活跃用户数
|
|
if active, err := r.CountActiveUsers(ctx); err != nil {
|
|
return nil, err
|
|
} else {
|
|
stats.Active = active
|
|
}
|
|
|
|
// 按角色统计
|
|
for _, role := range []entity.UserRole{
|
|
entity.UserRoleAdmin,
|
|
entity.UserRoleEditor,
|
|
entity.UserRoleUser,
|
|
} {
|
|
if count, err := r.CountByRole(ctx, role); err != nil {
|
|
return nil, err
|
|
} else {
|
|
switch role {
|
|
case entity.UserRoleAdmin:
|
|
stats.Admins = count
|
|
case entity.UserRoleEditor:
|
|
stats.Editors = count
|
|
case entity.UserRoleUser:
|
|
stats.Users = count
|
|
}
|
|
}
|
|
}
|
|
|
|
// 本月新注册用户
|
|
now := time.Now()
|
|
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
|
|
|
var monthlyUsers int64
|
|
if err := r.db.WithContext(ctx).Model(&entity.User{}).
|
|
Where("created_at >= ? AND created_at <= ?", startOfMonth, endOfMonth).
|
|
Count(&monthlyUsers).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
stats.MonthlyRegistrations = monthlyUsers
|
|
|
|
return &stats, nil
|
|
} |