Compare commits

..

7 Commits

Author SHA1 Message Date
sugarBecomer 3993fa9b32 ✨ 忘记密码 & 重发邮件 2025-11-11 14:08:07 +08:00
sugarBecomer 20b92b7d1c 🐛 注册验证码 2025-11-11 12:55:53 +08:00
sugarBecomer 05b9a94d41 🐛 ai初始化 & 错误处理 2025-11-11 11:57:50 +08:00
sugarBecomer 49f4475991 🐛 干掉global id 2025-11-10 22:53:05 +08:00
sugarBecomer 76bfdd03dd 🐛 cookie更新 2025-11-10 22:20:34 +08:00
sugarBecomer c18f2fdcf7 🐛 细节问题 2025-11-10 21:51:08 +08:00
sugarBecomer 74c6154c59 🐛 验证码处理 & 查询条件 2025-11-10 21:36:59 +08:00
27 changed files with 485 additions and 152 deletions
+14 -1
View File
@@ -14,7 +14,20 @@ func Run() {
IDType: &field.TypeInfo{
Type: field.TypeInt64,
},
Features: gen.AllFeatures,
Features: []gen.Feature{
gen.FeaturePrivacy,
gen.FeatureIntercept,
gen.FeatureEntQL,
gen.FeatureNamedEdges,
gen.FeatureBidiEdgeRefs,
gen.FeatureSnapshot,
gen.FeatureSchemaConfig,
gen.FeatureLock,
gen.FeatureModifier,
gen.FeatureExecQuery,
gen.FeatureUpsert,
gen.FeatureVersionedMigration,
},
})
utils.OkOrPanic(err)
+3 -3
View File
@@ -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]
+37 -9
View File
@@ -12,22 +12,50 @@ type CaptchaReq struct {
type UserLoginReq struct {
g.Meta `path:"/user/login" method:"POST" tags:"UserLogin" summary:"用户登录"`
Username string `json:"username" v:"required"`
Password string `json:"password" v:"required"`
CaptchaKey string `json:"captchaKey" v:"required"`
Dots []captcha.ConfirmData `json:"dots" v:"required"`
Username string `json:"username" v:"required#用户名或密码不能为空"`
Password string `json:"password" v:"required#用户名或密码不能为空"`
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
}
type UserRegisterReq struct {
g.Meta `path:"/user/register" method:"POST" tags:"UserLogin" summary:"用户注册"`
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
Username string `json:"username" v:"required|length:2,20#用户名不能为空|用户名长度应在2-20之间" `
Password string `json:"password" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间" `
ConfirmPassword string `json:"confirmPassword" v:"required|same:password#确认密码不能为空|两次输入的密码不一致"`
InviteCode string `json:"inviteCode"`
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
Username string `json:"username" v:"required|length:2,20#用户名不能为空|用户名长度应在2-20之间" `
Password string `json:"password" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间" `
ConfirmPassword string `json:"confirmPassword" v:"required|same:password#确认密码不能为空|两次输入的密码不一致"`
InviteCode string `json:"inviteCode"`
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
}
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不能为空"`
}
@@ -544,9 +544,10 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
return utils.Catch(func() {
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
})
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
}, retry.Delay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
if e != nil {
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
}
resultForm["ticket"] = gvar.New(ticket)
} else {
resultForm["ticket"] = gvar.New("")
@@ -1,6 +1,7 @@
package classifier
import (
"embed"
_ "embed"
"likedream-goframe/internal/application/recognizer"
"likedream-goframe/internal/framework/logger"
@@ -11,11 +12,8 @@ import (
"go.uber.org/zap"
)
//go:embed static/classes.txt
var classesBytes []byte
//go:embed static/model.onnx
var modelBytes []byte
//go:embed static/*
var fs embed.FS
var imageClassifier *ImageClassifier
func init() {
@@ -39,10 +37,12 @@ func initModel() (modelPath, classesPath string) {
os.Mkdir(staticPath, 0755)
}
modelPath = filepath.Join(staticPath, "model.onnx")
modelBytes, _ := fs.ReadFile("static/model.onnx")
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
os.WriteFile(modelPath, modelBytes, 0644)
}
classesPath = filepath.Join(staticPath, "classes.txt")
classesBytes, _ := fs.ReadFile("static/classes.txt")
if _, err := os.Stat(classesPath); os.IsNotExist(err) {
os.WriteFile(classesPath, classesBytes, 0644)
}
@@ -1,6 +1,7 @@
package ddddocr
import (
"embed"
_ "embed"
"likedream-goframe/internal/application/recognizer"
"os"
@@ -8,8 +9,8 @@ import (
"strings"
)
//go:embed static/common_old.onnx
var commonOldBytes []byte
//go:embed static/*
var fs embed.FS
func initModel() string {
if !recognizer.OnnxIsInit {
@@ -24,8 +25,9 @@ func initModel() string {
os.Mkdir(staticPath, 0755)
}
modelPath := filepath.Join(staticPath, "common_old.onnx")
modelBytes, _ := fs.ReadFile("static/common_old.onnx")
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
os.WriteFile(modelPath, commonOldBytes, 0644)
os.WriteFile(modelPath, modelBytes, 0644)
}
return modelPath
}
+6 -11
View File
@@ -1,6 +1,7 @@
package recognizer
import (
"embed"
_ "embed"
"likedream-goframe/internal/framework/logger"
"os"
@@ -12,14 +13,8 @@ import (
"go.uber.org/zap"
)
//go:embed assets/onnxruntime.dll
var onnxRuntimeFileWindows []byte
//go:embed assets/libonnxruntime.1.23.2.dylib
var onnxRuntimeFileDarwin []byte
//go:embed assets/libonnxruntime.so.1.23.2
var onnxRuntimeFileLinux []byte
//go:embed static/*
var fs embed.FS
// OnnxIsInit 初始化onnx配置
var OnnxIsInit = false
@@ -48,15 +43,15 @@ func getOnnxRuntimeName() (onnxRuntimeFileName string, onnxRuntimeFileBytes []by
switch systemOS {
case "windows":
onnxRuntimeFileName = "onnxruntime.dll"
onnxRuntimeFileBytes = onnxRuntimeFileWindows
onnxRuntimeFileBytes, _ = fs.ReadFile("static/" + onnxRuntimeFileName)
return
case "darwin":
onnxRuntimeFileName = "onnxruntime.dylib"
onnxRuntimeFileBytes = onnxRuntimeFileDarwin
onnxRuntimeFileBytes, _ = fs.ReadFile("static/" + onnxRuntimeFileName)
return
case "linux":
onnxRuntimeFileName = "onnxruntime.so"
onnxRuntimeFileBytes = onnxRuntimeFileLinux
onnxRuntimeFileBytes, _ = fs.ReadFile("static/" + onnxRuntimeFileName)
return
}
logger.Logger.Panic("暂不支持的操作系统:", zap.String("系统名称", systemOS))
+19 -18
View File
@@ -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 (
+2
View File
@@ -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)
)
+26 -26
View File
@@ -13,7 +13,7 @@ import (
)
func CheckHasData() bool {
count, err := Global.SysUser.Query().Count(context.Background())
count, err := Global.Debug().SysUser.Query().Count(context.Background())
utils.OkOrPanic(err, func(err error) error {
return err
})
@@ -36,7 +36,7 @@ func GenerateData() {
// 生成用户
func generateUser() {
ctx := gctx.New()
Global.SysUser.Create().
Global.Debug().SysUser.Create().
SetUsername("admin").
SetPassword(utils.PasswordBcryptHash("123456")).
SetEmail("admin@qq.com").
@@ -50,15 +50,15 @@ func generateUser() {
// 生成角色
func generateRoles() {
ctx := gctx.New()
Global.SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
Global.SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
Global.SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
Global.Debug().SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
Global.Debug().SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
Global.Debug().SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
}
// 生成菜单
func generateMenus() {
ctx := gctx.New()
sysM := Global.SysMenu.Create().
sysM := Global.Debug().SysMenu.Create().
SetName("系统管理").
SetLabel("system").
SetType(sysmenu.TypeGroup).
@@ -66,7 +66,7 @@ func generateMenus() {
SetRoute("/system").
SetOrder(1).
SaveX(ctx)
userM := Global.SysMenu.Create().
userM := Global.Debug().SysMenu.Create().
SetName("用户管理").
SetLabel("user").
SetType(sysmenu.TypeMenu).
@@ -77,7 +77,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -85,7 +85,7 @@ func generateMenus() {
SetOrder(1).
SetParent(userM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -94,7 +94,7 @@ func generateMenus() {
SetParent(userM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -103,7 +103,7 @@ func generateMenus() {
SetParent(userM).
SaveX(ctx)
// 列表
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -111,7 +111,7 @@ func generateMenus() {
SetOrder(4).
SetParent(userM).
SaveX(ctx)
roleM := Global.SysMenu.Create().
roleM := Global.Debug().SysMenu.Create().
SetName("角色管理").
SetLabel("role").
SetType(sysmenu.TypeMenu).
@@ -122,7 +122,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -130,7 +130,7 @@ func generateMenus() {
SetOrder(1).
SetParent(roleM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -139,7 +139,7 @@ func generateMenus() {
SetParent(roleM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -147,7 +147,7 @@ func generateMenus() {
SetOrder(3).
SetParent(roleM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -155,7 +155,7 @@ func generateMenus() {
SetOrder(4).
SetParent(roleM).
SaveX(ctx)
menuM := Global.SysMenu.Create().
menuM := Global.Debug().SysMenu.Create().
SetName("菜单管理").
SetLabel("menu").
SetType(sysmenu.TypeMenu).
@@ -166,7 +166,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -174,7 +174,7 @@ func generateMenus() {
SetOrder(1).
SetParent(menuM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -183,7 +183,7 @@ func generateMenus() {
SetParent(menuM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -191,7 +191,7 @@ func generateMenus() {
SetOrder(3).
SetParent(menuM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -204,14 +204,14 @@ func generateMenus() {
// 关联角色
func relationRoles() {
ctx := gctx.New()
admin := Global.SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
role := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
Global.SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
admin := Global.Debug().SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
role := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
Global.Debug().SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
}
// 关联菜单
func relationMenus() {
ctx := gctx.New()
adminRole := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
Global.SysRole.UpdateOne(adminRole).AddMenus(Global.SysMenu.Query().AllX(ctx)...).ExecX(ctx)
adminRole := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
Global.Debug().SysRole.UpdateOne(adminRole).AddMenus(Global.Debug().SysMenu.Query().AllX(ctx)...).ExecX(ctx)
}
+1 -1
View File
@@ -23,7 +23,7 @@ func InitDB() {
client, err := ent.Open(dbType.String(), dbDsn.String())
utils.OkOrPanic(err)
// 自动迁移
if err := client.Schema.Create(context.Background()); err != nil {
if err := client.Debug().Schema.Create(context.Background()); err != nil {
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
}
Global = client
@@ -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)
}
@@ -49,9 +49,60 @@ func (c *UserLoginV1) Login(ctx context.Context, req *v1.UserLoginReq) (res *uti
func (c *UserLoginV1) Register(ctx context.Context, req *v1.UserRegisterReq) (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.Register(ctx, req), nil
}
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
}
-5
View File
@@ -1,5 +0,0 @@
// ENT数据库文件,请勿编辑。
package internal
const IncrementStarts = "{\"cp_image\":0,\"cp_schools\":4294967296,\"cp_task_logs\":12884901888,\"cp_tasks\":8589934592,\"cp_users\":17179869184,\"sys_agent\":21474836480,\"sys_card\":25769803776,\"sys_menu\":30064771072,\"sys_role\":34359738368,\"sys_user\":38654705664,\"sys_user_logs\":42949672960}"
File diff suppressed because one or more lines are too long
+6 -27
View File
@@ -380,56 +380,35 @@ var (
func init() {
CpImageTable.ForeignKeys[0].RefTable = SysUserTable
CpImageTable.Annotation = &entsql.Annotation{
Table: "cp_image",
IncrementStart: func(i int) *int { return &i }(0),
}
CpSchoolsTable.Annotation = &entsql.Annotation{
IncrementStart: func(i int) *int { return &i }(4294967296),
Table: "cp_image",
}
CpTasksTable.ForeignKeys[0].RefTable = CpUsersTable
CpTasksTable.ForeignKeys[1].RefTable = SysUserTable
CpTasksTable.Annotation = &entsql.Annotation{
IncrementStart: func(i int) *int { return &i }(8589934592),
}
CpTaskLogsTable.ForeignKeys[0].RefTable = CpTasksTable
CpTaskLogsTable.ForeignKeys[1].RefTable = CpUsersTable
CpTaskLogsTable.ForeignKeys[2].RefTable = SysUserTable
CpTaskLogsTable.Annotation = &entsql.Annotation{
IncrementStart: func(i int) *int { return &i }(12884901888),
}
CpUsersTable.ForeignKeys[0].RefTable = CpSchoolsTable
CpUsersTable.ForeignKeys[1].RefTable = SysUserTable
CpUsersTable.Annotation = &entsql.Annotation{
IncrementStart: func(i int) *int { return &i }(17179869184),
}
SysAgentTable.ForeignKeys[0].RefTable = SysUserTable
SysAgentTable.Annotation = &entsql.Annotation{
Table: "sys_agent",
IncrementStart: func(i int) *int { return &i }(21474836480),
Table: "sys_agent",
}
SysCardTable.ForeignKeys[0].RefTable = SysUserTable
SysCardTable.ForeignKeys[1].RefTable = SysUserTable
SysCardTable.ForeignKeys[2].RefTable = SysUserTable
SysCardTable.Annotation = &entsql.Annotation{
Table: "sys_card",
IncrementStart: func(i int) *int { return &i }(25769803776),
Table: "sys_card",
}
SysMenuTable.ForeignKeys[0].RefTable = SysMenuTable
SysMenuTable.Annotation = &entsql.Annotation{
Table: "sys_menu",
IncrementStart: func(i int) *int { return &i }(30064771072),
Table: "sys_menu",
}
SysRoleTable.Annotation = &entsql.Annotation{
Table: "sys_role",
IncrementStart: func(i int) *int { return &i }(34359738368),
Table: "sys_role",
}
SysUserTable.ForeignKeys[0].RefTable = SysUserTable
SysUserTable.Annotation = &entsql.Annotation{
Table: "sys_user",
IncrementStart: func(i int) *int { return &i }(38654705664),
}
SysUserLogsTable.Annotation = &entsql.Annotation{
IncrementStart: func(i int) *int { return &i }(42949672960),
Table: "sys_user",
}
SysRoleMenusTable.ForeignKeys[0].RefTable = SysRoleTable
SysRoleMenusTable.ForeignKeys[1].RefTable = SysMenuTable
@@ -90,16 +90,17 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
// 查询学校详情
schoolDetail := school.GetSchoolDetail(ctx, targetTask.Edges.CpUser.SchoolID)
restyClient := login.GetClient(ctx, targetTask.Edges.CpUser.Cookie)
cookie := targetTask.Edges.CpUser.Cookie
restyClient := login.GetClient(ctx, cookie)
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
cookie := loginExecutor.GetCookieString()
cookie = loginExecutor.GetCookieString()
db.Global.CpUser.UpdateOne(targetTask.Edges.CpUser).SetCookie(cookie).ExecX(ctx)
restyClient = login.GetClient(ctx, cookie)
}
// 获取任务执行handler
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, targetTask.Edges.CpUser.Cookie, targetTask.Edges.CpUser.SchoolID)
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, cookie, targetTask.Edges.CpUser.SchoolID)
cpdailyRealTask := taskExecutor.QueryFormByTitle(ctx, targetTask.Title)
+11 -3
View File
@@ -17,6 +17,7 @@ import (
"strings"
"time"
"entgo.io/ent/dialect/sql"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/hibiken/asynq"
@@ -79,7 +80,6 @@ func (ts *TaskScanner) StopScan() {
case <-ts.stopSign:
}
}
func (ts *TaskScanner) scanCpdailyTask() {
currentTime := time.Now().In(cst2.TimeZoneShangHai)
@@ -88,7 +88,15 @@ func (ts *TaskScanner) scanCpdailyTask() {
minExecuteTime := currentTime.Add(-30 * time.Minute).Unix()
allTasks := db.Global.CpTask.Query().
Where(cptask.NextExecuteTimeLTE(currentTime.Unix()), cptask.NextExecuteTimeNotNil(), cptask.NextExecuteTimeGT(0), cptask.StepEQ(cptask.StepComplete), cptask.TaskStatusEQ(cptask.TaskStatusNotStart)).
Where(
cptask.NextExecuteTimeLTE(currentTime.Unix()),
cptask.NextExecuteTimeNotNil(),
cptask.NextExecuteTimeGT(0),
cptask.StepEQ(cptask.StepComplete),
cptask.TaskStatusEQ(cptask.TaskStatusNotStart),
func(selector *sql.Selector) {
selector.Where(sql.ExprP("success_count < frequency"))
}).
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime).
AllX(ts.ctx)
@@ -117,7 +125,7 @@ func (ts *TaskScanner) scanCpdailyTask() {
e = utils.Catch(func() {
queue.AddTask(ts.ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
asynq.TaskID(fmt.Sprintf(cst2.TaskCpdailyId, task.ID)),
asynq.MaxRetry(3),
asynq.MaxRetry(4),
asynq.Unique(10*time.Second),
asynq.ProcessAt(execTime),
asynq.Deadline(execTime.Add(12*time.Minute)),
+10 -6
View File
@@ -18,17 +18,17 @@ func ExceptionHandler(r *ghttp.Request) {
if r.Response.Status == 404 {
return
}
if r.Response.BufferLength() > 0 {
return
}
if r.Response.Header().Get("Content-Type") == "image/png" {
return
}
var (
err = r.GetError()
res = r.GetHandlerResponse()
code = gerror.Code(err)
)
if err == nil && r.Response.BufferLength() > 0 {
return
}
if r.Response.Header().Get("Content-Type") == "image/png" {
return
}
// 如果没有异常,则直接返回
if err == nil && res != nil {
@@ -49,6 +49,10 @@ func ExceptionHandler(r *ghttp.Request) {
tmpErr := errors.CodeParam
tmpErr.SetMessage((err.(*ent.ConstraintError)).Error())
code = tmpErr.Code()
} else if ent.IsNotFound(err) {
tmpErr := errors.CodeNotFound
tmpErr.SetMessage("数据未找到")
code = tmpErr.Code()
} else if code == gcode.CodeNil {
code = errors.CodeInternal.Code()
} else if code == gcode.CodeValidationFailed {
+9 -26
View File
@@ -178,9 +178,6 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
utils.OkOrPanic(errors2.NewBusinessError(400, "执行次数应不低于原有次数"))
}
// 记录原始任务状态,用于判断是否需要立即扫描
originalStatus := cpTask.TaskStatus
//扣积分
basePoint := decimal.NewFromInt(cst2.BasePointCpdaily)
if user.Edges.Agent != nil {
@@ -190,35 +187,20 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
balance.BalanceService.ChangeUserBalance(ctx, user.ID, needPoint.Neg())
// 更新任务
e := db2.WithTx(ctx, func(ctx context.Context, tx *ent.Tx) {
cpTask := db2.Global.CpTask.UpdateOne(cpTask).
update := db2.Global.CpTask.UpdateOne(cpTask).
SetStep(cptask.StepComplete).
SetFrequency(req.Frequency).
SetTaskStatus(cptask.TaskStatusNotStart).
SetNotificationType(req.NotificationType).
SetExecuteHour(req.ExecuteHour).
SetExecuteMinute(req.ExecuteMinute).
SaveX(ctx)
// 计算下次执行时间
var nextExecuteAt int64
// 只有在原始状态为未开始时,才考虑5分钟窗口立即扫描
if originalStatus == cptask.TaskStatusNotStart {
currentTime := time.Now().In(cst2.TimeZoneShangHai)
executeTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), req.ExecuteHour, req.ExecuteMinute, 0, 0, cst2.TimeZoneShangHai)
timeDiff := executeTime.Sub(currentTime)
if timeDiff >= -5*time.Minute && timeDiff <= 5*time.Minute {
// 执行时间在当前时间前后5分钟内,设置为当前时间,让扫描器立即扫描到
nextExecuteAt = currentTime.Unix()
} else {
// 否则使用正常的计算逻辑
nextExecuteAt = utils.GetCpdailyNextExecuteTime(cpTask)
}
} else {
// 其他状态(暂停、失败等),使用正常的计算逻辑
nextExecuteAt = utils.GetCpdailyNextExecuteTime(cpTask)
SetExecuteMinute(req.ExecuteMinute)
if cpTask.Step == cptask.StepExecute {
update.SetTaskStatus(cptask.TaskStatusNotStart)
}
cpTask := update.SaveX(ctx)
// 其他状态(暂停、失败等),使用正常的计算逻辑
nextExecuteAt := utils.GetCpdailyNextExecuteTime(cpTask)
db2.Global.CpTask.UpdateOne(cpTask).SetNextExecuteTime(nextExecuteAt).ExecX(ctx)
}, &sql.TxOptions{})
@@ -283,6 +265,7 @@ func (v CpTaskServiceV1) ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq)
asynq.Unique(5*time.Second),
asynq.MaxRetry(0),
asynq.Timeout(10*time.Minute),
asynq.Deadline(time.Now().Add(10*time.Minute)),
asynq.Queue(cst2.TaskQueueCpdaily),
)
// 更新未开始的任务状态为队列中
+46
View File
@@ -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
}
+24
View File
@@ -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,
})
}