✨ 忘记密码 & 重发邮件
This commit is contained in:
+3
-3
@@ -10,8 +10,8 @@ port = 8090
|
||||
url = "https://devocr.likedream.xyz"
|
||||
openapiPath= "/api.json"
|
||||
swaggerPath= "/swagger"
|
||||
emailConfirmBaseUrl = "https://dev.likedream.xyz/confirm/"
|
||||
|
||||
emailConfirmBaseUrl = "http://localhost:3200/confirm/"
|
||||
emailResetPasswordUrl = "http://localhost:3200/reset-password/"
|
||||
[env]
|
||||
# 可选值: dev, prod
|
||||
mode = "dev"
|
||||
@@ -30,7 +30,7 @@ smtpUsername = "demo@likedream.xyz"
|
||||
smtpPassword = "baca122..."
|
||||
# 发件人邮箱和显示名称
|
||||
fromEmail = "demo@likedream.xyz"
|
||||
fromName = "今日校园系统"
|
||||
fromName = "若梦系统"
|
||||
|
||||
|
||||
[s3]
|
||||
|
||||
@@ -33,3 +33,29 @@ type UserConfirmReq struct {
|
||||
g.Meta `path:"/user/confirm" method:"POST" tags:"UserLogin" summary:"邮箱二次验证"`
|
||||
Token string `json:"token" v:"required"`
|
||||
}
|
||||
|
||||
type ResendVerificationReq struct {
|
||||
g.Meta `path:"/user/resendVerification" method:"POST" tags:"UserLogin" summary:"重新发送验证邮件"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type ForgotPasswordReq struct {
|
||||
g.Meta `path:"/user/forgotPassword" method:"POST" tags:"UserLogin" summary:"忘记密码-发送重置密码邮件"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type ForgetResetPasswordReq struct {
|
||||
g.Meta `path:"/user/forgetResetPassword" method:"POST" tags:"UserLogin" summary:"忘记密码-重置密码"`
|
||||
Token string `json:"token" v:"required#token不能为空"`
|
||||
NewPassword string `json:"newPassword" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间"`
|
||||
ConfirmPassword string `json:"confirmPassword" v:"required|same:newPassword#确认密码不能为空|两次输入的密码不一致"`
|
||||
}
|
||||
|
||||
type ResetPasswordInfoReq struct {
|
||||
g.Meta `path:"/user/resetPassword/info" method:"POST" tags:"UserLogin" summary:"获取重置密码用户信息"`
|
||||
Token string `json:"token" v:"required#token不能为空"`
|
||||
}
|
||||
|
||||
@@ -3,22 +3,23 @@ package cst
|
||||
import "time"
|
||||
|
||||
const (
|
||||
ConfigOcrServerUrl = "ocr.url"
|
||||
ConfigEnvMode = "env.mode"
|
||||
ConfigDatabaseType = "database.type"
|
||||
ConfigDatabaseDsn = "database.dsn"
|
||||
ConfigServerPort = "server.port"
|
||||
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
|
||||
ConfigAuthorizationKey = "server.authorization"
|
||||
ConfigJwtKey = "server.jwt"
|
||||
ConfigRuoliCaptchaBaseUrl = "ruoliCaptcha.baseUrl"
|
||||
ConfigS3AccessKey = "s3.accessKey"
|
||||
ConfigS3SecretKey = "s3.secretKey"
|
||||
ConfigS3BaseUrl = "s3.baseUrl"
|
||||
ConfigS3Bucket = "s3.bucket"
|
||||
ConfigCpdailyAppVersion = "cpdaily.appVersion"
|
||||
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
|
||||
ConfigCpdailyCalVersion = "cpdaily.calVersion"
|
||||
ConfigOcrServerUrl = "ocr.url"
|
||||
ConfigEnvMode = "env.mode"
|
||||
ConfigDatabaseType = "database.type"
|
||||
ConfigDatabaseDsn = "database.dsn"
|
||||
ConfigServerPort = "server.port"
|
||||
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
|
||||
ConfigEmailResetPasswordUrl = "server.emailResetPasswordUrl"
|
||||
ConfigAuthorizationKey = "server.authorization"
|
||||
ConfigJwtKey = "server.jwt"
|
||||
ConfigRuoliCaptchaBaseUrl = "ruoliCaptcha.baseUrl"
|
||||
ConfigS3AccessKey = "s3.accessKey"
|
||||
ConfigS3SecretKey = "s3.secretKey"
|
||||
ConfigS3BaseUrl = "s3.baseUrl"
|
||||
ConfigS3Bucket = "s3.bucket"
|
||||
ConfigCpdailyAppVersion = "cpdaily.appVersion"
|
||||
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
|
||||
ConfigCpdailyCalVersion = "cpdaily.calVersion"
|
||||
|
||||
ConfigRedisUrl = "redis.url"
|
||||
|
||||
@@ -35,8 +36,8 @@ const (
|
||||
|
||||
const (
|
||||
// 应用程序常量
|
||||
AppName = "今日校园系统"
|
||||
AppCopyright = "© 2024 今日校园系统. 保留所有权利."
|
||||
AppName = "若梦系统"
|
||||
AppCopyright = "© 2025 若梦系统. 保留所有权利."
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,6 +3,7 @@ package cst
|
||||
const (
|
||||
RedisUserInfo = "userinfo:"
|
||||
RedisVerifyCode = "verifycode:"
|
||||
RedisResetPassword = "resetpassword:"
|
||||
RedisCpUserLogin = "cpuserlogin:"
|
||||
RedisCpUserAuthorize = "cpuser:authorize:"
|
||||
)
|
||||
@@ -10,6 +11,7 @@ const (
|
||||
var (
|
||||
RedisUserInfoExpire = int64(86400)
|
||||
RedisVerifyCodeExpire = int64(86400)
|
||||
RedisResetPasswordExpire = int64(3600) // 重置密码链接1小时有效
|
||||
RedisCpUserLoginExpire = int64(180)
|
||||
RedisCpUserAuthorizeExpire = int64(1800)
|
||||
)
|
||||
|
||||
@@ -13,4 +13,8 @@ type IUserLoginV1 interface {
|
||||
Login(ctx context.Context, req *v1.UserLoginReq) (*utils.Response[any], error)
|
||||
Register(ctx context.Context, req *v1.UserRegisterReq) (*utils.Response[any], error)
|
||||
Confirm(ctx context.Context, req *v1.UserConfirmReq) (*utils.Response[any], error)
|
||||
ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) (*utils.Response[any], error)
|
||||
ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (*utils.Response[any], error)
|
||||
ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) (*utils.Response[any], error)
|
||||
GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) (*utils.Response[any], error)
|
||||
}
|
||||
|
||||
@@ -66,3 +66,43 @@ func (c *UserLoginV1) Register(ctx context.Context, req *v1.UserRegisterReq) (re
|
||||
func (c *UserLoginV1) Confirm(ctx context.Context, req *v1.UserConfirmReq) (*utils.Response[any], error) {
|
||||
return service.Global.UserLogin.Confirm(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = json.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserLogin.ResendVerification(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = json.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserLogin.ForgotPassword(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) (*utils.Response[any], error) {
|
||||
return service.Global.UserLogin.ResetPassword(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) (*utils.Response[any], error) {
|
||||
return service.Global.UserLogin.GetResetPasswordInfo(ctx, req), nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,29 @@ func (s *EmailServiceV1) SendTaskNotificationEmail(ctx context.Context, toEmail,
|
||||
return utils.SuccessWithMsg("通知邮件发送成功")
|
||||
}
|
||||
|
||||
// SendResetPasswordEmail 发送重置密码邮件
|
||||
func (s *EmailServiceV1) SendResetPasswordEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
|
||||
if s.config.SMTPUsername == "" || s.config.SMTPPassword == "" {
|
||||
logger.Logger.Error("邮件服务未配置")
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("重置%s账户密码", cst2.AppName)
|
||||
body, err := s.renderResetPasswordEmailTemplate(username, token)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件模板渲染失败")
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件发送失败")
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("重置密码邮件发送成功")
|
||||
}
|
||||
|
||||
// sendEmail 发送邮件的底层方法
|
||||
func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string) error {
|
||||
|
||||
@@ -190,3 +213,26 @@ func (s *EmailServiceV1) renderTaskNotificationTemplate(taskName string, status
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// renderResetPasswordEmailTemplate 渲染重置密码邮件模板
|
||||
func (s *EmailServiceV1) renderResetPasswordEmailTemplate(username string, token string) (string, error) {
|
||||
// 单独解析重置密码模板
|
||||
tmpl, err := template.ParseFS(templateFiles, "templates/layout.gohtml", "templates/reset_password.gohtml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析重置密码邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
get, err := g.Cfg().Get(gctx.New(), cst2.ConfigEmailResetPasswordUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data := NewResetPasswordTemplateData(username, cst2.AppName, cst2.AppCopyright, get.String()+token)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, "reset_password.gohtml", data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("渲染重置密码邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -64,12 +64,36 @@ func NewTaskNotificationTemplateData(taskName string, status email.TaskStatus, d
|
||||
}
|
||||
}
|
||||
|
||||
// ResetPasswordTemplateData 重置密码邮件模板数据
|
||||
type ResetPasswordTemplateData struct {
|
||||
BaseTemplateData
|
||||
Username string `json:"username"` // 用户名
|
||||
ResetURL string `json:"reset_url"` // 重置密码链接
|
||||
}
|
||||
|
||||
// NewResetPasswordTemplateData 创建重置密码邮件模板数据
|
||||
func NewResetPasswordTemplateData(username, appName, copyright string, resetURL string) *ResetPasswordTemplateData {
|
||||
return &ResetPasswordTemplateData{
|
||||
BaseTemplateData: BaseTemplateData{
|
||||
Title: "重置密码 - " + appName,
|
||||
HeaderIcon: "🔒",
|
||||
HeaderTitle: "重置您的密码",
|
||||
HeaderColor: "#f56565",
|
||||
AppName: appName,
|
||||
AppCopyright: copyright,
|
||||
},
|
||||
Username: username,
|
||||
ResetURL: resetURL,
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateType 模板类型枚举
|
||||
type TemplateType string
|
||||
|
||||
const (
|
||||
TemplateTypeSignup TemplateType = "signup" // 注册模板
|
||||
TemplateTypeTaskNotification TemplateType = "task_notification" // 任务通知模板
|
||||
TemplateTypeResetPassword TemplateType = "reset_password" // 重置密码模板
|
||||
)
|
||||
|
||||
// GetTemplatePath 获取模板文件路径
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{{- /*gotype: likedream-goframe/internal/service/email.ResetPasswordTemplateData*/ -}}
|
||||
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "extra-styles"}}
|
||||
<style>
|
||||
.warning-box {
|
||||
background: linear-gradient(90deg, #fff5f5 0%, #ffe5e5 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
border-left: 4px solid #f56565;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.warning-box p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: linear-gradient(90deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
margin: 24px 0;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h2>亲爱的 <span class="highlight-text">{{.Username}}</span>,您好!</h2>
|
||||
<p>我们收到了您重置<strong>{{.AppName}}</strong>账户密码的请求。请点击下面的链接重置您的密码:</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{.ResetURL}}"
|
||||
style="background-color: #f56565; color: white; padding: 12px 24px;
|
||||
text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
重置密码
|
||||
</a>
|
||||
</div>
|
||||
<p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
|
||||
<p style="word-break: break-all; color: #666;">{{.ResetURL}}</p>
|
||||
|
||||
<div class="info-box">
|
||||
<p style="margin: 0; color: #1e40af;"><strong>⏰ 重要提示:链接有效期为 1 小时</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<p style="margin: 0;"><strong>⚠️ 安全提醒</strong></p>
|
||||
<p>如果您没有请求重置密码,请忽略此邮件。您的账户仍然是安全的。</p>
|
||||
<p>请勿将此链接分享给任何人!</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>如果您在重置密码过程中遇到任何问题,请随时联系我们的技术支持团队。</p>
|
||||
<p><strong>祝您使用愉快!</strong> 🔒</p>
|
||||
{{end}}
|
||||
@@ -84,11 +84,14 @@ func (s *UserLoginServiceV1) Register(ctx context.Context, req *v1.UserRegisterR
|
||||
// 注册成功后发送欢迎邮件(异步发送,不影响注册结果)
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
err2 := redis.GetClient().SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
|
||||
err2 := client.SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
// 保存邮箱到 token 的映射,方便重新发送时删除旧 token
|
||||
client.SetEx(ctx2, cst2.RedisVerifyCode+"email:"+req.Email, tokenStr, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second)
|
||||
// 创建新的context避免原context被取消
|
||||
emailService := email.NewV1()
|
||||
emailService.SendSignupEmail(ctx2, req.Email, req.Username, tokenStr)
|
||||
@@ -110,6 +113,8 @@ func (s *UserLoginServiceV1) Confirm(ctx context.Context, req *v1.UserConfirmReq
|
||||
if emailStr == "" {
|
||||
return utils.FailParam("非法的验证")
|
||||
}
|
||||
// 删除邮箱对应的 token 映射
|
||||
client.Del(ctx, cst2.RedisVerifyCode+"email:"+emailStr)
|
||||
// 查询用户
|
||||
user := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).OnlyX(ctx)
|
||||
db.Global.SysUser.UpdateOne(user).SetVerify(true).SaveX(ctx)
|
||||
@@ -118,3 +123,135 @@ func (s *UserLoginServiceV1) Confirm(ctx context.Context, req *v1.UserConfirmReq
|
||||
r.Response.Header().Set(cst2.AuthorizationKey, utils.GenerateToken(user.ID))
|
||||
return utils.SuccessWithMsg("验证成功")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) *utils.Response[any] {
|
||||
// 查询数据库中是否存在该邮箱的未激活用户
|
||||
user, err := db.Global.SysUser.Query().Where(
|
||||
sysuser.EmailEQ(req.Email),
|
||||
sysuser.VerifyEQ(false),
|
||||
).Only(ctx)
|
||||
|
||||
// 如果用户存在且未激活,则重新发送激活邮件
|
||||
if err == nil && user != nil {
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
|
||||
// 获取并删除旧的 token(如果存在)
|
||||
oldTokenKey := cst2.RedisVerifyCode + "email:" + req.Email
|
||||
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
|
||||
if oldToken != "" {
|
||||
// 删除旧 token 对应的验证码记录
|
||||
client.Del(ctx2, cst2.RedisVerifyCode+oldToken)
|
||||
}
|
||||
|
||||
// 生成新的 token
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
|
||||
// 设置新的 token 到 Redis
|
||||
err2 := client.SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
|
||||
// 保存邮箱到 token 的映射,方便下次删除
|
||||
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second)
|
||||
|
||||
// 发送激活邮件
|
||||
emailService := email.NewV1()
|
||||
emailService.SendSignupEmail(ctx2, req.Email, user.Username, tokenStr)
|
||||
}()
|
||||
}
|
||||
|
||||
// 无论用户是否存在,都返回成功,避免泄露用户信息
|
||||
return utils.SuccessWithMsg("如果该邮箱存在未激活账户,验证邮件已重新发送")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) *utils.Response[any] {
|
||||
// 查询数据库中是否存在该邮箱的已激活用户
|
||||
user, err := db.Global.SysUser.Query().Where(
|
||||
sysuser.EmailEQ(req.Email),
|
||||
sysuser.VerifyEQ(true),
|
||||
).Only(ctx)
|
||||
|
||||
// 如果用户存在且已激活,则发送重置密码邮件
|
||||
if err == nil && user != nil {
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
|
||||
// 获取并删除旧的 token(如果存在)
|
||||
oldTokenKey := cst2.RedisResetPassword + "email:" + req.Email
|
||||
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
|
||||
if oldToken != "" {
|
||||
// 删除旧 token 对应的重置密码记录
|
||||
client.Del(ctx2, cst2.RedisResetPassword+oldToken)
|
||||
}
|
||||
|
||||
// 生成新的 token
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
|
||||
// 设置新的 token 到 Redis
|
||||
err2 := client.SetEx(ctx2, cst2.RedisResetPassword+tokenStr, req.Email, time.Duration(cst2.RedisResetPasswordExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
|
||||
// 保存邮箱到 token 的映射,方便下次删除
|
||||
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst2.RedisResetPasswordExpire)*time.Second)
|
||||
|
||||
// 发送重置密码邮件
|
||||
emailService := email.NewV1()
|
||||
emailService.SendResetPasswordEmail(ctx2, req.Email, user.Username, tokenStr)
|
||||
}()
|
||||
}
|
||||
|
||||
// 无论用户是否存在,都返回成功,避免泄露用户信息
|
||||
return utils.SuccessWithMsg("如果该邮箱已注册,重置密码邮件已发送")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) *utils.Response[any] {
|
||||
client := redis.GetClient()
|
||||
emailStr := client.GetDel(ctx, cst2.RedisResetPassword+req.Token).Val()
|
||||
if emailStr == "" {
|
||||
return utils.FailParam("无效或已过期的重置密码链接")
|
||||
}
|
||||
// 删除邮箱对应的 token 映射
|
||||
client.Del(ctx, cst2.RedisResetPassword+"email:"+emailStr)
|
||||
|
||||
// 查询用户
|
||||
user, err := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).Only(ctx)
|
||||
if ent.IsNotFound(err) {
|
||||
return utils.FailParam("用户不存在")
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
db.Global.SysUser.UpdateOne(user).SetPassword(utils.PasswordBcryptHash(req.NewPassword)).SaveX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("密码重置成功,请使用新密码登录")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) *utils.Response[any] {
|
||||
client := redis.GetClient()
|
||||
// 使用 Get 而不是 GetDel,不删除 token
|
||||
emailStr := client.Get(ctx, cst2.RedisResetPassword+req.Token).Val()
|
||||
if emailStr == "" {
|
||||
return utils.FailParam("无效或已过期的重置密码链接")
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
user, err := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).Only(ctx)
|
||||
if ent.IsNotFound(err) {
|
||||
return utils.FailParam("无效或已过期的重置密码链接")
|
||||
}
|
||||
type ResetPasswordInfoRes struct {
|
||||
Username string `json:"username"` // 用户名
|
||||
Email string `json:"email"` // 邮箱
|
||||
}
|
||||
|
||||
// 返回用户信息
|
||||
return utils.SuccessWithData[any](&ResetPasswordInfoRes{
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user