package service import ( "context" cryptorand "crypto/rand" "encoding/hex" "errors" "fmt" "log" "net/smtp" "time" "github.com/user-management-system/internal/auth" "github.com/user-management-system/internal/cache" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/security" ) // PasswordResetConfig controls reset-token issuance and SMTP delivery. type PasswordResetConfig struct { TokenTTL time.Duration SMTPHost string SMTPPort int SMTPUser string SMTPPass string FromEmail string SiteURL string PasswordMinLen int PasswordRequireSpecial bool PasswordRequireNumber bool } func DefaultPasswordResetConfig() *PasswordResetConfig { return &PasswordResetConfig{ TokenTTL: 15 * time.Minute, SMTPHost: "", SMTPPort: 587, SMTPUser: "", SMTPPass: "", FromEmail: "noreply@example.com", SiteURL: "http://localhost:8080", PasswordMinLen: 8, PasswordRequireSpecial: false, PasswordRequireNumber: false, } } type PasswordResetService struct { userRepo userRepositoryInterface cache *cache.CacheManager config *PasswordResetConfig } func NewPasswordResetService( userRepo userRepositoryInterface, cache *cache.CacheManager, config *PasswordResetConfig, ) *PasswordResetService { if config == nil { config = DefaultPasswordResetConfig() } return &PasswordResetService{ userRepo: userRepo, cache: cache, config: config, } } func (s *PasswordResetService) ForgotPassword(ctx context.Context, email string) error { user, err := s.userRepo.GetByEmail(ctx, email) if err != nil { return nil } tokenBytes := make([]byte, 32) if _, err := cryptorand.Read(tokenBytes); err != nil { return fmt.Errorf("生成重置Token失败: %w", err) } resetToken := hex.EncodeToString(tokenBytes) cacheKey := "pwd_reset:" + resetToken ttl := s.config.TokenTTL if err := s.cache.Set(ctx, cacheKey, user.ID, ttl, ttl); err != nil { return fmt.Errorf("缓存重置Token失败: %w", err) } go s.sendResetEmail(domain.DerefStr(user.Email), user.Username, resetToken) return nil } func (s *PasswordResetService) ResetPassword(ctx context.Context, token, newPassword string) error { if token == "" || newPassword == "" { return errors.New("参数不完整") } cacheKey := "pwd_reset:" + token val, ok := s.cache.Get(ctx, cacheKey) if !ok { return errors.New("重置链接已失效或不存在,请重新申请") } userID, ok := int64Value(val) if !ok { return errors.New("重置Token数据异常") } user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return errors.New("用户不存在") } if err := s.doResetPassword(ctx, user, newPassword); err != nil { return err } if err := s.cache.Delete(ctx, cacheKey); err != nil { return fmt.Errorf("清理重置Token失败: %w", err) } return nil } func (s *PasswordResetService) ValidateResetToken(ctx context.Context, token string) (bool, error) { if token == "" { return false, errors.New("token不能为空") } _, ok := s.cache.Get(ctx, "pwd_reset:"+token) return ok, nil } func (s *PasswordResetService) sendResetEmail(email, username, token string) { if s.config.SMTPHost == "" { return } resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.config.SiteURL, token) subject := "密码重置请求" body := fmt.Sprintf(`您好 %s: 您收到此邮件,是因为有人请求重置账户密码。 请点击以下链接重置密码(链接将在 %s 后失效): %s 如果不是您本人操作,请忽略此邮件,您的密码不会被修改。 用户管理系统团队`, username, s.config.TokenTTL.String(), resetURL) var authInfo smtp.Auth if s.config.SMTPUser != "" || s.config.SMTPPass != "" { authInfo = smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, s.config.SMTPHost) } msg := fmt.Sprintf( "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", s.config.FromEmail, email, subject, body, ) addr := fmt.Sprintf("%s:%d", s.config.SMTPHost, s.config.SMTPPort) if err := smtp.SendMail(addr, authInfo, s.config.FromEmail, []string{email}, []byte(msg)); err != nil { log.Printf("password-reset-email: send failed to=%s err=%v", email, err) } } // ForgotPasswordByPhoneRequest 短信密码重置请求 type ForgotPasswordByPhoneRequest struct { Phone string `json:"phone" binding:"required"` } // ForgotPasswordByPhone 通过手机验证码重置密码 - 发送验证码 func (s *PasswordResetService) ForgotPasswordByPhone(ctx context.Context, phone string) (string, error) { user, err := s.userRepo.GetByPhone(ctx, phone) if err != nil { return "", nil // 用户不存在不提示,防止用户枚举 } // 生成6位数字验证码 code, err := generateSMSCode() if err != nil { return "", fmt.Errorf("生成验证码失败: %w", err) } // 存储验证码,关联用户ID cacheKey := fmt.Sprintf("pwd_reset_sms:%s", phone) ttl := s.config.TokenTTL if err := s.cache.Set(ctx, cacheKey, user.ID, ttl, ttl); err != nil { return "", fmt.Errorf("缓存验证码失败: %w", err) } // 存储验证码到另一个key,用于后续校验 codeKey := fmt.Sprintf("pwd_reset_sms_code:%s", phone) if err := s.cache.Set(ctx, codeKey, code, ttl, ttl); err != nil { return "", fmt.Errorf("缓存验证码失败: %w", err) } return code, nil } // ResetPasswordByPhoneRequest 通过手机验证码重置密码请求 type ResetPasswordByPhoneRequest struct { Phone string `json:"phone" binding:"required"` Code string `json:"code" binding:"required"` NewPassword string `json:"new_password" binding:"required"` } // ResetPasswordByPhone 通过手机验证码重置密码 - 验证并重置 func (s *PasswordResetService) ResetPasswordByPhone(ctx context.Context, req *ResetPasswordByPhoneRequest) error { if req.Phone == "" || req.Code == "" || req.NewPassword == "" { return errors.New("参数不完整") } codeKey := fmt.Sprintf("pwd_reset_sms_code:%s", req.Phone) storedCode, ok := s.cache.Get(ctx, codeKey) if !ok { return errors.New("验证码已失效,请重新获取") } code, ok := storedCode.(string) if !ok || code != req.Code { return errors.New("验证码不正确") } // 获取用户ID cacheKey := fmt.Sprintf("pwd_reset_sms:%s", req.Phone) val, ok := s.cache.Get(ctx, cacheKey) if !ok { return errors.New("验证码已失效,请重新获取") } userID, ok := int64Value(val) if !ok { return errors.New("验证码数据异常") } user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return errors.New("用户不存在") } if err := s.doResetPassword(ctx, user, req.NewPassword); err != nil { return err } // 清理验证码 s.cache.Delete(ctx, codeKey) s.cache.Delete(ctx, cacheKey) return nil } func (s *PasswordResetService) doResetPassword(ctx context.Context, user *domain.User, newPassword string) error { policy := security.PasswordPolicy{ MinLength: s.config.PasswordMinLen, RequireSpecial: s.config.PasswordRequireSpecial, RequireNumber: s.config.PasswordRequireNumber, }.Normalize() if err := policy.Validate(newPassword); err != nil { return err } hashedPassword, err := auth.HashPassword(newPassword) if err != nil { return fmt.Errorf("密码加密失败: %w", err) } user.Password = hashedPassword if err := s.userRepo.Update(ctx, user); err != nil { return fmt.Errorf("更新密码失败: %w", err) } return nil }