2025-12-18 13:50:39 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
2026-02-07 19:46:42 +08:00
|
|
|
|
"log"
|
|
|
|
|
|
"time"
|
2025-12-25 17:15:01 +08:00
|
|
|
|
|
2025-12-31 23:42:01 +08:00
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
2025-12-24 21:07:21 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
2025-12-25 20:52:47 +08:00
|
|
|
|
ErrUserNotFound = infraerrors.NotFound("USER_NOT_FOUND", "user not found")
|
|
|
|
|
|
ErrPasswordIncorrect = infraerrors.BadRequest("PASSWORD_INCORRECT", "current password is incorrect")
|
|
|
|
|
|
ErrInsufficientPerms = infraerrors.Forbidden("INSUFFICIENT_PERMISSIONS", "insufficient permissions")
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-01 18:59:38 +08:00
|
|
|
|
// UserListFilters contains all filter options for listing users
|
|
|
|
|
|
type UserListFilters struct {
|
2026-01-01 19:09:06 +08:00
|
|
|
|
Status string // User status filter
|
|
|
|
|
|
Role string // User role filter
|
|
|
|
|
|
Search string // Search in email, username
|
|
|
|
|
|
Attributes map[int64]string // Custom attribute filters: attributeID -> value
|
2026-01-01 18:59:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:15:01 +08:00
|
|
|
|
type UserRepository interface {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
Create(ctx context.Context, user *User) error
|
|
|
|
|
|
GetByID(ctx context.Context, id int64) (*User, error)
|
|
|
|
|
|
GetByEmail(ctx context.Context, email string) (*User, error)
|
|
|
|
|
|
GetFirstAdmin(ctx context.Context) (*User, error)
|
|
|
|
|
|
Update(ctx context.Context, user *User) error
|
2025-12-25 17:15:01 +08:00
|
|
|
|
Delete(ctx context.Context, id int64) error
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
List(ctx context.Context, params pagination.PaginationParams) ([]User, *pagination.PaginationResult, error)
|
2026-01-01 18:59:38 +08:00
|
|
|
|
ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters UserListFilters) ([]User, *pagination.PaginationResult, error)
|
2025-12-25 17:15:01 +08:00
|
|
|
|
|
|
|
|
|
|
UpdateBalance(ctx context.Context, id int64, amount float64) error
|
|
|
|
|
|
DeductBalance(ctx context.Context, id int64, amount float64) error
|
|
|
|
|
|
UpdateConcurrency(ctx context.Context, id int64, amount int) error
|
|
|
|
|
|
ExistsByEmail(ctx context.Context, email string) (bool, error)
|
|
|
|
|
|
RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error)
|
2026-02-28 17:33:30 +08:00
|
|
|
|
// AddGroupToAllowedGroups 将指定分组增量添加到用户的 allowed_groups(幂等,冲突忽略)
|
|
|
|
|
|
AddGroupToAllowedGroups(ctx context.Context, userID int64, groupID int64) error
|
2026-02-02 22:13:50 +08:00
|
|
|
|
|
2026-02-02 22:20:08 +08:00
|
|
|
|
// TOTP 双因素认证
|
2026-02-02 22:13:50 +08:00
|
|
|
|
UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error
|
|
|
|
|
|
EnableTotp(ctx context.Context, userID int64) error
|
|
|
|
|
|
DisableTotp(ctx context.Context, userID int64) error
|
2025-12-25 17:15:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// UpdateProfileRequest 更新用户资料请求
|
|
|
|
|
|
type UpdateProfileRequest struct {
|
|
|
|
|
|
Email *string `json:"email"`
|
2025-12-23 11:26:22 +08:00
|
|
|
|
Username *string `json:"username"`
|
2025-12-18 13:50:39 +08:00
|
|
|
|
Concurrency *int `json:"concurrency"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ChangePasswordRequest 修改密码请求
|
|
|
|
|
|
type ChangePasswordRequest struct {
|
|
|
|
|
|
CurrentPassword string `json:"current_password"`
|
|
|
|
|
|
NewPassword string `json:"new_password"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UserService 用户服务
|
|
|
|
|
|
type UserService struct {
|
2026-01-10 22:23:51 +08:00
|
|
|
|
userRepo UserRepository
|
|
|
|
|
|
authCacheInvalidator APIKeyAuthCacheInvalidator
|
2026-02-07 19:46:42 +08:00
|
|
|
|
billingCache BillingCache
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewUserService 创建用户服务实例
|
2026-02-07 19:46:42 +08:00
|
|
|
|
func NewUserService(userRepo UserRepository, authCacheInvalidator APIKeyAuthCacheInvalidator, billingCache BillingCache) *UserService {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return &UserService{
|
2026-01-10 22:23:51 +08:00
|
|
|
|
userRepo: userRepo,
|
|
|
|
|
|
authCacheInvalidator: authCacheInvalidator,
|
2026-02-07 19:46:42 +08:00
|
|
|
|
billingCache: billingCache,
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
// GetFirstAdmin 获取首个管理员用户(用于 Admin API Key 认证)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *UserService) GetFirstAdmin(ctx context.Context) (*User, error) {
|
2025-12-26 10:42:08 +08:00
|
|
|
|
admin, err := s.userRepo.GetFirstAdmin(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("get first admin: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return admin, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// GetProfile 获取用户资料
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *UserService) GetProfile(ctx context.Context, userID int64) (*User, error) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
user, err := s.userRepo.GetByID(ctx, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("get user: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateProfile 更新用户资料
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req UpdateProfileRequest) (*User, error) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
user, err := s.userRepo.GetByID(ctx, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("get user: %w", err)
|
|
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
oldConcurrency := user.Concurrency
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新字段
|
|
|
|
|
|
if req.Email != nil {
|
|
|
|
|
|
// 检查新邮箱是否已被使用
|
|
|
|
|
|
exists, err := s.userRepo.ExistsByEmail(ctx, *req.Email)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("check email exists: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if exists && *req.Email != user.Email {
|
|
|
|
|
|
return nil, ErrEmailExists
|
|
|
|
|
|
}
|
|
|
|
|
|
user.Email = *req.Email
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 11:26:22 +08:00
|
|
|
|
if req.Username != nil {
|
|
|
|
|
|
user.Username = *req.Username
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if req.Concurrency != nil {
|
|
|
|
|
|
user.Concurrency = *req.Concurrency
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("update user: %w", err)
|
|
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
if s.authCacheInvalidator != nil && user.Concurrency != oldConcurrency {
|
|
|
|
|
|
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ChangePassword 修改密码
|
2025-12-29 17:18:17 -05:00
|
|
|
|
// Security: Increments TokenVersion to invalidate all existing JWT tokens
|
2025-12-18 13:50:39 +08:00
|
|
|
|
func (s *UserService) ChangePassword(ctx context.Context, userID int64, req ChangePasswordRequest) error {
|
|
|
|
|
|
user, err := s.userRepo.GetByID(ctx, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("get user: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证当前密码
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if !user.CheckPassword(req.CurrentPassword) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return ErrPasswordIncorrect
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err := user.SetPassword(req.NewPassword); err != nil {
|
|
|
|
|
|
return fmt.Errorf("set password: %w", err)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 17:18:17 -05:00
|
|
|
|
// Increment TokenVersion to invalidate all existing tokens
|
|
|
|
|
|
// This ensures that any tokens issued before the password change become invalid
|
|
|
|
|
|
user.TokenVersion++
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
|
|
|
|
return fmt.Errorf("update user: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 根据ID获取用户(管理员功能)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
user, err := s.userRepo.GetByID(ctx, id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("get user: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// List 获取用户列表(管理员功能)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *UserService) List(ctx context.Context, params pagination.PaginationParams) ([]User, *pagination.PaginationResult, error) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
users, pagination, err := s.userRepo.List(ctx, params)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, nil, fmt.Errorf("list users: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return users, pagination, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateBalance 更新用户余额(管理员功能)
|
|
|
|
|
|
func (s *UserService) UpdateBalance(ctx context.Context, userID int64, amount float64) error {
|
|
|
|
|
|
if err := s.userRepo.UpdateBalance(ctx, userID, amount); err != nil {
|
|
|
|
|
|
return fmt.Errorf("update balance: %w", err)
|
|
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
if s.authCacheInvalidator != nil {
|
|
|
|
|
|
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
|
|
|
|
|
|
}
|
2026-02-07 19:46:42 +08:00
|
|
|
|
if s.billingCache != nil {
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
if err := s.billingCache.InvalidateUserBalance(cacheCtx, userID); err != nil {
|
|
|
|
|
|
log.Printf("invalidate user balance cache failed: user_id=%d err=%v", userID, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-28 22:19:18 +08:00
|
|
|
|
// UpdateConcurrency 更新用户并发数(管理员功能)
|
|
|
|
|
|
func (s *UserService) UpdateConcurrency(ctx context.Context, userID int64, concurrency int) error {
|
|
|
|
|
|
if err := s.userRepo.UpdateConcurrency(ctx, userID, concurrency); err != nil {
|
|
|
|
|
|
return fmt.Errorf("update concurrency: %w", err)
|
|
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
if s.authCacheInvalidator != nil {
|
|
|
|
|
|
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
|
|
|
|
|
|
}
|
2025-12-28 22:19:18 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// UpdateStatus 更新用户状态(管理员功能)
|
|
|
|
|
|
func (s *UserService) UpdateStatus(ctx context.Context, userID int64, status string) error {
|
|
|
|
|
|
user, err := s.userRepo.GetByID(ctx, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("get user: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
user.Status = status
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
|
|
|
|
return fmt.Errorf("update user: %w", err)
|
|
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
if s.authCacheInvalidator != nil {
|
|
|
|
|
|
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除用户(管理员功能)
|
|
|
|
|
|
func (s *UserService) Delete(ctx context.Context, userID int64) error {
|
2026-01-10 22:23:51 +08:00
|
|
|
|
if s.authCacheInvalidator != nil {
|
|
|
|
|
|
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
|
|
|
|
|
|
}
|
2026-01-10 22:52:13 +08:00
|
|
|
|
if err := s.userRepo.Delete(ctx, userID); err != nil {
|
|
|
|
|
|
return fmt.Errorf("delete user: %w", err)
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|