feat(架构): 完善基础架构设计
This commit is contained in:
187
internal/domains/user/services/sms_code_service.go
Normal file
187
internal/domains/user/services/sms_code_service.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/sms"
|
||||
)
|
||||
|
||||
// SMSCodeService 短信验证码服务
|
||||
type SMSCodeService struct {
|
||||
repo *repositories.SMSCodeRepository
|
||||
smsClient sms.Service
|
||||
cache interfaces.CacheService
|
||||
config config.SMSConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSMSCodeService 创建短信验证码服务
|
||||
func NewSMSCodeService(
|
||||
repo *repositories.SMSCodeRepository,
|
||||
smsClient sms.Service,
|
||||
cache interfaces.CacheService,
|
||||
config config.SMSConfig,
|
||||
logger *zap.Logger,
|
||||
) *SMSCodeService {
|
||||
return &SMSCodeService{
|
||||
repo: repo,
|
||||
smsClient: smsClient,
|
||||
cache: cache,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error {
|
||||
// 检查频率限制
|
||||
if err := s.checkRateLimit(ctx, phone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
code := s.smsClient.GenerateCode(s.config.CodeLength)
|
||||
|
||||
// 创建SMS验证码记录
|
||||
smsCode := &entities.SMSCode{
|
||||
ID: uuid.New().String(),
|
||||
Phone: phone,
|
||||
Code: code,
|
||||
Scene: scene,
|
||||
IP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
Used: false,
|
||||
ExpiresAt: time.Now().Add(s.config.ExpireTime),
|
||||
}
|
||||
|
||||
// 保存验证码
|
||||
if err := s.repo.Create(ctx, smsCode); err != nil {
|
||||
s.logger.Error("保存短信验证码失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存验证码失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送短信
|
||||
if err := s.smsClient.SendVerificationCode(ctx, phone, code); err != nil {
|
||||
// 记录发送失败但不删除验证码记录,让其自然过期
|
||||
s.logger.Error("发送短信验证码失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", code),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("短信发送失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新发送记录缓存
|
||||
s.updateSendRecord(ctx, phone)
|
||||
|
||||
s.logger.Info("短信验证码发送成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCode 验证验证码
|
||||
func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, scene entities.SMSScene) error {
|
||||
// 根据手机号和场景获取有效的验证码记录
|
||||
smsCode, err := s.repo.GetValidCode(ctx, phone, scene)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证码无效或已过期")
|
||||
}
|
||||
|
||||
// 验证验证码是否匹配
|
||||
if smsCode.Code != code {
|
||||
return fmt.Errorf("验证码无效或已过期")
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
if err := s.repo.MarkAsUsed(ctx, smsCode.ID); err != nil {
|
||||
s.logger.Error("标记验证码为已使用失败",
|
||||
zap.String("code_id", smsCode.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("验证码状态更新失败")
|
||||
}
|
||||
|
||||
s.logger.Info("短信验证码验证成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRateLimit 检查发送频率限制
|
||||
func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error {
|
||||
now := time.Now()
|
||||
|
||||
// 检查最小发送间隔
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
||||
var lastSent time.Time
|
||||
if err := s.cache.Get(ctx, lastSentKey, &lastSent); err == nil {
|
||||
if now.Sub(lastSent) < s.config.RateLimit.MinInterval {
|
||||
return fmt.Errorf("请等待 %v 后再试", s.config.RateLimit.MinInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查每小时发送限制
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
if hourlyCount >= s.config.RateLimit.HourlyLimit {
|
||||
return fmt.Errorf("每小时最多发送 %d 条短信", s.config.RateLimit.HourlyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查每日发送限制
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
if dailyCount >= s.config.RateLimit.DailyLimit {
|
||||
return fmt.Errorf("每日最多发送 %d 条短信", s.config.RateLimit.DailyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSendRecord 更新发送记录
|
||||
func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string) {
|
||||
now := time.Now()
|
||||
|
||||
// 更新最后发送时间
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
||||
s.cache.Set(ctx, lastSentKey, now, s.config.RateLimit.MinInterval)
|
||||
|
||||
// 更新每小时计数
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
s.cache.Set(ctx, hourlyKey, hourlyCount+1, time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, hourlyKey, 1, time.Hour)
|
||||
}
|
||||
|
||||
// 更新每日计数
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
s.cache.Set(ctx, dailyKey, dailyCount+1, 24*time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, dailyKey, 1, 24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpiredCodes 清理过期验证码
|
||||
func (s *SMSCodeService) CleanExpiredCodes(ctx context.Context) error {
|
||||
return s.repo.CleanupExpired(ctx)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"regexp"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@@ -18,21 +18,24 @@ import (
|
||||
|
||||
// UserService 用户服务实现
|
||||
type UserService struct {
|
||||
repo *repositories.UserRepository
|
||||
eventBus interfaces.EventBus
|
||||
logger *zap.Logger
|
||||
repo *repositories.UserRepository
|
||||
smsCodeService *SMSCodeService
|
||||
eventBus interfaces.EventBus
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务
|
||||
func NewUserService(
|
||||
repo *repositories.UserRepository,
|
||||
smsCodeService *SMSCodeService,
|
||||
eventBus interfaces.EventBus,
|
||||
logger *zap.Logger,
|
||||
) *UserService {
|
||||
return &UserService{
|
||||
repo: repo,
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
smsCodeService: smsCodeService,
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,341 +46,209 @@ func (s *UserService) Name() string {
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (s *UserService) Initialize(ctx context.Context) error {
|
||||
s.logger.Info("User service initialized")
|
||||
s.logger.Info("用户服务已初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (s *UserService) HealthCheck(ctx context.Context) error {
|
||||
// 简单检查:尝试查询用户数量
|
||||
_, err := s.repo.Count(ctx, interfaces.CountOptions{})
|
||||
return err
|
||||
// 简单的健康检查
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (s *UserService) Shutdown(ctx context.Context) error {
|
||||
s.logger.Info("User service shutdown")
|
||||
s.logger.Info("用户服务已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (s *UserService) Create(ctx context.Context, createDTO interface{}) (*entities.User, error) {
|
||||
req, ok := createDTO.(*dto.CreateUserRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid DTO type for user creation")
|
||||
// Register 用户注册
|
||||
func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterRequest) (*entities.User, error) {
|
||||
// 验证手机号格式
|
||||
if !s.isValidPhone(registerReq.Phone) {
|
||||
return nil, fmt.Errorf("手机号格式无效")
|
||||
}
|
||||
|
||||
// 验证业务规则
|
||||
if err := s.ValidateCreate(ctx, req); err != nil {
|
||||
return nil, err
|
||||
// 验证密码确认
|
||||
if registerReq.Password != registerReq.ConfirmPassword {
|
||||
return nil, fmt.Errorf("密码和确认密码不匹配")
|
||||
}
|
||||
|
||||
// 检查用户名和邮箱是否已存在
|
||||
if err := s.checkDuplicates(ctx, req.Username, req.Email); err != nil {
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, registerReq.Phone, registerReq.Code, entities.SMSSceneRegister); err != nil {
|
||||
return nil, fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if err := s.checkPhoneDuplicate(ctx, registerReq.Phone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建用户实体
|
||||
user := req.ToEntity()
|
||||
user := registerReq.ToEntity()
|
||||
user.ID = uuid.New().String()
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := s.hashPassword(req.Password)
|
||||
// 哈希密码
|
||||
hashedPassword, err := s.hashPassword(registerReq.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
return nil, fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
user.Password = hashedPassword
|
||||
|
||||
// 保存用户
|
||||
if err := s.repo.Create(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to create user", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
s.logger.Error("创建用户失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户创建事件
|
||||
event := events.NewUserCreatedEvent(user, s.getCorrelationID(ctx))
|
||||
// 发布用户注册事件
|
||||
event := events.NewUserRegisteredEvent(user, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user created event", zap.Error(err))
|
||||
s.logger.Warn("发布用户注册事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User created successfully",
|
||||
s.logger.Info("用户注册成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("username", user.Username))
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
// LoginWithPassword 密码登录
|
||||
func (s *UserService) LoginWithPassword(ctx context.Context, loginReq *dto.LoginWithPasswordRequest) (*entities.User, error) {
|
||||
// 根据手机号查找用户
|
||||
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (s *UserService) Update(ctx context.Context, id string, updateDTO interface{}) (*entities.User, error) {
|
||||
req, ok := updateDTO.(*dto.UpdateUserRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid DTO type for user update")
|
||||
}
|
||||
|
||||
// 验证业务规则
|
||||
if err := s.ValidateUpdate(ctx, id, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取现有用户
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// 记录变更前的值
|
||||
oldValues := s.captureUserValues(user)
|
||||
|
||||
// 应用更新
|
||||
s.applyUserUpdates(user, req)
|
||||
|
||||
// 保存更新
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户更新事件
|
||||
newValues := s.captureUserValues(user)
|
||||
changes := s.findChanges(oldValues, newValues)
|
||||
if len(changes) > 0 {
|
||||
event := events.NewUserUpdatedEvent(user.ID, changes, oldValues, newValues, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user updated event", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("User updated successfully",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.Int("changes", len(changes)))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (s *UserService) Delete(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
// 获取用户信息用于事件
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// 软删除用户
|
||||
if err := s.repo.SoftDelete(ctx, id); err != nil {
|
||||
s.logger.Error("Failed to delete user", zap.Error(err))
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户删除事件
|
||||
event := events.NewUserDeletedEvent(user.ID, user.Username, user.Email, true, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user deleted event", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User deleted successfully", zap.String("user_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *UserService) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
return s.repo.List(ctx, options)
|
||||
}
|
||||
|
||||
// Search 搜索用户
|
||||
func (s *UserService) Search(ctx context.Context, query string, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
// 设置搜索关键字
|
||||
searchOptions := options
|
||||
searchOptions.Search = query
|
||||
|
||||
return s.repo.List(ctx, searchOptions)
|
||||
}
|
||||
|
||||
// Count 统计用户数量
|
||||
func (s *UserService) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||
return s.repo.Count(ctx, options)
|
||||
}
|
||||
|
||||
// Validate 验证用户实体
|
||||
func (s *UserService) Validate(ctx context.Context, entity *entities.User) error {
|
||||
return entity.Validate()
|
||||
}
|
||||
|
||||
// ValidateCreate 验证创建请求
|
||||
func (s *UserService) ValidateCreate(ctx context.Context, createDTO interface{}) error {
|
||||
req, ok := createDTO.(*dto.CreateUserRequest)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid DTO type")
|
||||
}
|
||||
|
||||
// 基础验证已经由binding标签处理,这里添加业务规则验证
|
||||
if req.Username == "admin" || req.Username == "root" {
|
||||
return fmt.Errorf("username '%s' is reserved", req.Username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate 验证更新请求
|
||||
func (s *UserService) ValidateUpdate(ctx context.Context, id string, updateDTO interface{}) error {
|
||||
_, ok := updateDTO.(*dto.UpdateUserRequest)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid DTO type")
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
|
||||
// Login 用户登录
|
||||
func (s *UserService) Login(ctx context.Context, loginReq *dto.LoginRequest) (*entities.User, error) {
|
||||
// 根据用户名或邮箱查找用户
|
||||
var user *entities.User
|
||||
var err error
|
||||
|
||||
if s.isEmail(loginReq.Login) {
|
||||
user, err = s.repo.FindByEmail(ctx, loginReq.Login)
|
||||
} else {
|
||||
user, err = s.repo.FindByUsername(ctx, loginReq.Login)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !s.checkPassword(loginReq.Password, user.Password) {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if !user.CanLogin() {
|
||||
return nil, fmt.Errorf("account is disabled or suspended")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.UpdateLastLogin()
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
s.logger.Warn("Failed to update last login time", zap.Error(err))
|
||||
}
|
||||
|
||||
// 发布登录事件
|
||||
// 发布用户登录事件
|
||||
event := events.NewUserLoggedInEvent(
|
||||
user.ID, user.Username,
|
||||
user.ID, user.Phone,
|
||||
s.getClientIP(ctx), s.getUserAgent(ctx),
|
||||
s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user logged in event", zap.Error(err))
|
||||
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User logged in successfully",
|
||||
s.logger.Info("用户密码登录成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("username", user.Username))
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// LoginWithSMS 短信验证码登录
|
||||
func (s *UserService) LoginWithSMS(ctx context.Context, loginReq *dto.LoginWithSMSRequest) (*entities.User, error) {
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, loginReq.Phone, loginReq.Code, entities.SMSSceneLogin); err != nil {
|
||||
return nil, fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据手机号查找用户
|
||||
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 发布用户登录事件
|
||||
event := events.NewUserLoggedInEvent(
|
||||
user.ID, user.Phone,
|
||||
s.getClientIP(ctx), s.getUserAgent(ctx),
|
||||
s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("用户短信登录成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
|
||||
// 获取用户
|
||||
// 验证新密码确认
|
||||
if req.NewPassword != req.ConfirmNewPassword {
|
||||
return fmt.Errorf("新密码和确认新密码不匹配")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := s.repo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
return fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, user.Phone, req.Code, entities.SMSSceneChangePassword); err != nil {
|
||||
return fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
if !s.checkPassword(req.OldPassword, user.Password) {
|
||||
return fmt.Errorf("current password is incorrect")
|
||||
return fmt.Errorf("当前密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
// 哈希新密码
|
||||
hashedPassword, err := s.hashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
return fmt.Errorf("新密码加密失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = hashedPassword
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
return fmt.Errorf("密码更新失败: %w", err)
|
||||
}
|
||||
|
||||
// 发布密码修改事件
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Username, s.getCorrelationID(ctx))
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish password changed event", zap.Error(err))
|
||||
s.logger.Warn("发布密码修改事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("Password changed successfully", zap.String("user_id", userID))
|
||||
s.logger.Info("密码修改成功", zap.String("user_id", userID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats 获取用户统计
|
||||
func (s *UserService) GetStats(ctx context.Context) (*dto.UserStatsResponse, error) {
|
||||
total, err := s.repo.Count(ctx, interfaces.CountOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// GetByID 根据ID获取用户
|
||||
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
// 这里可以并行查询不同状态的用户数量
|
||||
// 简化实现,返回基础统计
|
||||
return &dto.UserStatsResponse{
|
||||
TotalUsers: total,
|
||||
ActiveUsers: total, // 简化
|
||||
InactiveUsers: 0,
|
||||
SuspendedUsers: 0,
|
||||
NewUsersToday: 0,
|
||||
NewUsersWeek: 0,
|
||||
NewUsersMonth: 0,
|
||||
}, nil
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
// 工具方法
|
||||
|
||||
// checkDuplicates 检查重复的用户名和邮箱
|
||||
func (s *UserService) checkDuplicates(ctx context.Context, username, email string) error {
|
||||
// 检查用户名
|
||||
if existingUser, err := s.repo.FindByUsername(ctx, username); err == nil && existingUser != nil {
|
||||
return fmt.Errorf("username already exists")
|
||||
// checkPhoneDuplicate 检查手机号重复
|
||||
func (s *UserService) checkPhoneDuplicate(ctx context.Context, phone string) error {
|
||||
if _, err := s.repo.FindByPhone(ctx, phone); err == nil {
|
||||
return fmt.Errorf("手机号已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱
|
||||
if existingUser, err := s.repo.FindByEmail(ctx, email); err == nil && existingUser != nil {
|
||||
return fmt.Errorf("email already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashPassword 加密密码
|
||||
func (s *UserService) hashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
// checkPassword 验证密码
|
||||
@@ -386,63 +257,24 @@ func (s *UserService) checkPassword(password, hash string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// isEmail 检查是否为邮箱格式
|
||||
func (s *UserService) isEmail(str string) bool {
|
||||
return len(str) > 0 && len(str) < 255 &&
|
||||
len(str) > 5 &&
|
||||
str[len(str)-4:] != ".." &&
|
||||
(len(str) > 6 && str[len(str)-4:] == ".com") ||
|
||||
(len(str) > 5 && str[len(str)-3:] == ".cn") ||
|
||||
(len(str) > 6 && str[len(str)-4:] == ".org") ||
|
||||
(len(str) > 6 && str[len(str)-4:] == ".net")
|
||||
// 简化的邮箱检查,实际应该使用正则表达式
|
||||
// isValidPhone 验证手机号格式
|
||||
func (s *UserService) isValidPhone(phone string) bool {
|
||||
// 简单的中国手机号验证(11位数字,以1开头)
|
||||
pattern := `^1[3-9]\d{9}$`
|
||||
matched, _ := regexp.MatchString(pattern, phone)
|
||||
return matched
|
||||
}
|
||||
|
||||
// applyUserUpdates 应用用户更新
|
||||
func (s *UserService) applyUserUpdates(user *entities.User, req *dto.UpdateUserRequest) {
|
||||
if req.FirstName != nil {
|
||||
user.FirstName = *req.FirstName
|
||||
}
|
||||
if req.LastName != nil {
|
||||
user.LastName = *req.LastName
|
||||
}
|
||||
if req.Phone != nil {
|
||||
user.Phone = *req.Phone
|
||||
}
|
||||
if req.Avatar != nil {
|
||||
user.Avatar = *req.Avatar
|
||||
}
|
||||
user.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// captureUserValues 捕获用户值用于变更比较
|
||||
func (s *UserService) captureUserValues(user *entities.User) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"first_name": user.FirstName,
|
||||
"last_name": user.LastName,
|
||||
"phone": user.Phone,
|
||||
"avatar": user.Avatar,
|
||||
}
|
||||
}
|
||||
|
||||
// findChanges 找出变更的字段
|
||||
func (s *UserService) findChanges(oldValues, newValues map[string]interface{}) map[string]interface{} {
|
||||
changes := make(map[string]interface{})
|
||||
|
||||
for key, newValue := range newValues {
|
||||
if oldValue, exists := oldValues[key]; !exists || oldValue != newValue {
|
||||
changes[key] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
// generateUserID 生成用户ID
|
||||
func (s *UserService) generateUserID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// getCorrelationID 获取关联ID
|
||||
func (s *UserService) getCorrelationID(ctx context.Context) string {
|
||||
if id := ctx.Value("correlation_id"); id != nil {
|
||||
if correlationID, ok := id.(string); ok {
|
||||
return correlationID
|
||||
if strID, ok := id.(string); ok {
|
||||
return strID
|
||||
}
|
||||
}
|
||||
return uuid.New().String()
|
||||
@@ -451,19 +283,19 @@ func (s *UserService) getCorrelationID(ctx context.Context) string {
|
||||
// getClientIP 获取客户端IP
|
||||
func (s *UserService) getClientIP(ctx context.Context) string {
|
||||
if ip := ctx.Value("client_ip"); ip != nil {
|
||||
if clientIP, ok := ip.(string); ok {
|
||||
return clientIP
|
||||
if strIP, ok := ip.(string); ok {
|
||||
return strIP
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return ""
|
||||
}
|
||||
|
||||
// getUserAgent 获取用户代理
|
||||
func (s *UserService) getUserAgent(ctx context.Context) string {
|
||||
if ua := ctx.Value("user_agent"); ua != nil {
|
||||
if userAgent, ok := ua.(string); ok {
|
||||
return userAgent
|
||||
if strUA, ok := ua.(string); ok {
|
||||
return strUA
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user