Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3993fa9b32 | |||
| 20b92b7d1c | |||
| 05b9a94d41 | |||
| 49f4475991 | |||
| 76bfdd03dd | |||
| c18f2fdcf7 | |||
| 74c6154c59 |
+14
-1
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
// 更新未开始的任务状态为队列中
|
||||
|
||||
@@ -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