Files
yunzerwebsiteallinone/go/services/password_reset.go
T
2026-07-15 22:38:33 +08:00

261 lines
7.2 KiB
Go

package services
import (
"errors"
"fmt"
"math/rand"
"strings"
"sync"
"time"
"server/models"
"server/pkg/passwordutil"
)
// resetCodeItem 存储找回密码的验证码
type resetCodeItem struct {
Code string
Channel string
ExpiredAt time.Time
}
var resetCodeStore sync.Map
// resetCodeKey 生成密码重置验证码的存储key
func resetCodeKey(tenantName, account, phone, channel string) string {
key := strings.ToLower(strings.TrimSpace(tenantName)) + "|" +
strings.ToLower(strings.TrimSpace(account)) + "|" +
strings.ToLower(strings.TrimSpace(phone)) + "|" +
strings.TrimSpace(channel)
return key
}
// VerifyTenantAccount 第一步:验证租户和账号是否存在
// 返回该账号关联的手机号和邮箱(用于第二步选择验证方式)
func VerifyTenantAccount(tenantName, account string) (phone, email string, err error) {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
if tenantName == "" || account == "" {
return "", "", errors.New("租户名称和账号不能为空")
}
// 验证租户是否存在
var tenant models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("tenant_name", tenantName).
One(&tenant); err != nil {
return "", "", errors.New("租户不存在")
}
if tenant.Status != 1 {
return "", "", errors.New("租户已停用")
}
// 验证该租户下的账号是否存在
var tenantUser models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser); err != nil {
return "", "", errors.New("账号不存在")
}
if tenantUser.Status == 0 {
return "", "", errors.New("账号已禁用")
}
// 返回该账号的手机号和邮箱
phoneStr := ""
if tenantUser.Phone != nil {
phoneStr = strings.TrimSpace(*tenantUser.Phone)
}
emailStr := ""
if tenantUser.Email != nil {
emailStr = strings.TrimSpace(*tenantUser.Email)
}
if phoneStr == "" && emailStr == "" {
return "", "", errors.New("账号未绑定手机号或邮箱,无法重置密码")
}
return phoneStr, emailStr, nil
}
// SendResetCode 第二步:验证手机号并发送验证码
func SendResetCode(tenantName, account, phone, channel string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
phone = strings.TrimSpace(phone)
channel = strings.TrimSpace(channel)
if tenantName == "" || account == "" || phone == "" {
return errors.New("租户名称、账号和手机号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
// 验证租户是否存在
var tenant models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("tenant_name", tenantName).
One(&tenant); err != nil {
return errors.New("租户不存在")
}
// 验证该租户下的账号和手机号是否匹配
var tenantUser models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser); err != nil {
return errors.New("账号不存在")
}
if tenantUser.Status == 0 {
return errors.New("账号已禁用")
}
// 根据验证渠道验证用户信息
if channel == "sms" {
if tenantUser.Phone == nil || strings.TrimSpace(*tenantUser.Phone) != phone {
return errors.New("手机号不匹配,请确认您输入的手机号正确")
}
} else if channel == "email" {
if tenantUser.Email == nil || strings.TrimSpace(*tenantUser.Email) != phone {
return errors.New("邮箱不匹配,请确认您输入的邮箱正确")
}
}
// 生成验证码
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
// 发送验证码
if channel == "sms" {
content := "密码重置验证码:" + code
if err := enqueueSMSTaskForPasswordReset(tenant.ID, phone, content, code); err != nil {
return errors.New("短信发送失败,请重试")
}
}
// TODO: 实现邮箱验证码发送逻辑
// 存储验证码(5分钟有效期)
resetCodeStore.Store(resetCodeKey(tenantName, account, phone, channel), resetCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
// VerifyResetCode 验证重置密码的验证码
func VerifyResetCode(tenantName, account, phone, channel, code string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
phone = strings.TrimSpace(phone)
channel = strings.TrimSpace(channel)
code = strings.TrimSpace(code)
if code == "" {
return errors.New("验证码不能为空")
}
key := resetCodeKey(tenantName, account, phone, channel)
val, ok := resetCodeStore.Load(key)
if !ok {
return errors.New("验证码不存在或已失效")
}
item, ok := val.(resetCodeItem)
if !ok {
return errors.New("验证码状态异常")
}
if time.Now().After(item.ExpiredAt) {
resetCodeStore.Delete(key)
return errors.New("验证码已过期")
}
if item.Code != code {
return errors.New("验证码错误")
}
// 验证通过后删除验证码
resetCodeStore.Delete(key)
return nil
}
// ResetPassword 第三步:重置密码
func ResetPassword(tenantName, account, phone, smsCode, newPassword, confirmPassword string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
phone = strings.TrimSpace(phone)
smsCode = strings.TrimSpace(smsCode)
newPassword = strings.TrimSpace(newPassword)
confirmPassword = strings.TrimSpace(confirmPassword)
if tenantName == "" || account == "" || phone == "" {
return errors.New("租户名称、账号和手机号不能为空")
}
if newPassword == "" {
return errors.New("新密码不能为空")
}
if newPassword != confirmPassword {
return errors.New("两次密码不一致")
}
if len(newPassword) < 6 {
return errors.New("密码长度不能少于6个字符")
}
// 验证验证码(验证码验证后会被删除)
if err := VerifyResetCode(tenantName, account, phone, "sms", smsCode); err != nil {
return err
}
// 验证租户
var tenant models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("tenant_name", tenantName).
One(&tenant); err != nil {
return errors.New("租户不存在")
}
// 查询用户
var tenantUser models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser); err != nil {
return errors.New("账号不存在")
}
// 验证手机号
if tenantUser.Phone == nil || strings.TrimSpace(*tenantUser.Phone) != phone {
return errors.New("手机号不匹配")
}
// 哈希新密码
hashedPassword, err := passwordutil.Hash(newPassword)
if err != nil {
return errors.New("密码处理失败")
}
// 更新密码
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", tenantUser.ID).
Update(map[string]interface{}{
"password": hashedPassword,
})
if err != nil {
return errors.New("密码更新失败")
}
return nil
}
// enqueueSMSTaskForPasswordReset 发送密码重置短信任务
func enqueueSMSTaskForPasswordReset(tid uint64, phone, content, code string) error {
// 重用已有的短信发送逻辑
return enqueueSMSTaskForLogin(tid, phone, content, code)
}