Hi, 尊敬的用户
-- {{if eq .Type 1}} 感谢您注册!您的验证码是(请于{{.Expire}}分钟内使用): - {{else if eq .Type 4}} 您正在申请注销账号。您的验证码是(请于{{.Expire}}分钟内使用): - {{else}} - 您的验证码是(请于{{.Expire}}分钟内使用): - {{end}} -
-- 如果您未请求此验证码,请忽略此邮件。 -
-diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index bf72a19..43c78f0 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -63,7 +63,7 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { l := subscribe.NewSubscribeLogic(c, svcCtx) resp, err := l.Handler(&req) if err != nil { - c.String(http.StatusInternalServerError, "Internal Server") + c.String(http.StatusInternalServerError, err.Error()) return } c.Header("subscription-userinfo", resp.Header) diff --git a/internal/logic/common/sendEmailCodeLogic.go b/internal/logic/common/sendEmailCodeLogic.go index 0298d1d..c1ffdc9 100644 --- a/internal/logic/common/sendEmailCodeLogic.go +++ b/internal/logic/common/sendEmailCodeLogic.go @@ -88,7 +88,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty code := random.Key(6, 0) taskPayload.Type = queue.EmailTypeVerify taskPayload.Email = req.Email - taskPayload.Subject = "Verification code" + taskPayload.Subject = "登录验证" expireTime := l.svcCtx.Config.VerifyCode.ExpireTime if expireTime == 0 { diff --git a/internal/logic/public/user/bindEmailWithVerificationLogic.go b/internal/logic/public/user/bindEmailWithVerificationLogic.go index c15072e..603bfd5 100644 --- a/internal/logic/public/user/bindEmailWithVerificationLogic.go +++ b/internal/logic/public/user/bindEmailWithVerificationLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "github.com/perfect-panel/server/internal/config" @@ -42,6 +43,7 @@ func NewBindEmailWithVerificationLogic(ctx context.Context, svcCtx *svc.ServiceC // - *types.BindEmailWithVerificationResponse: 包含绑定结果、消息、token、用户ID // - error: 发生错误时返回具体错误 func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.BindEmailWithVerificationRequest) (*types.BindEmailWithVerificationResponse, error) { + req.Email = strings.ToLower(strings.TrimSpace(req.Email)) // 获取当前用户 u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { @@ -231,6 +233,17 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi // return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "创建邮箱用户设备记录失败") // } } + + // 清除邮箱用户的缓存,确保更新(如邀请人、设备列表等)可见 + userToClear := &user.User{Id: emailUserId} + // 添加当前邮箱做为 AuthMethod 以便 BatchClearRelatedCache 能清除相关索引缓存 + // 注意:虽然 email 映射未变,但清除是一个好习惯,且 BatchClearRelatedCache 依赖 AuthMethods 来清除 email 缓存 key + userToClear.AuthMethods = []user.AuthMethods{ + {AuthType: "email", AuthIdentifier: req.Email}, + } + if err := l.svcCtx.UserModel.BatchClearRelatedCache(l.ctx, userToClear); err != nil { + l.Errorw("清理邮箱用户缓存失败", logger.Field("error", err.Error()), logger.Field("user_id", emailUserId)) + } // 4. 生成新的JWT token token, err := l.generateTokenForUser(emailUserId, deviceIdentifier) if err != nil { diff --git a/internal/logic/public/user/deleteAccountLogic.go b/internal/logic/public/user/deleteAccountLogic.go index 8b1166f..9073801 100644 --- a/internal/logic/public/user/deleteAccountLogic.go +++ b/internal/logic/public/user/deleteAccountLogic.go @@ -147,7 +147,7 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon // 开始数据库事务 err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { - // 1. 预先查找该用户下的所有设备记录 (因为稍后要全删) + // 1. 预先查找该用户下的所有设备记录 (因为稍后要迁移) var userDevices []user.Device if err := tx.Where("user_id = ?", currentUser.Id).Find(&userDevices).Error; err != nil { l.Errorw("查询用户设备列表失败", logger.Field("user_id", currentUser.Id), logger.Field("error", err.Error())) @@ -159,17 +159,52 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon l.Infow("未识别到当前设备 ID,将执行全量注销并尝试迁移所有已知设备", logger.Field("user_id", currentUser.Id), logger.Field("found_devices", len(userDevices))) } - // 2. 无条件执行全量删除 (清理旧账号数据) - l.Infow("执行账号全量注销-清理旧数据", logger.Field("user_id", currentUser.Id), logger.Field("device_count", len(userDevices))) + l.Infow("执行账号全量注销-迁移设备并删除旧数据", logger.Field("user_id", currentUser.Id), logger.Field("device_count", len(userDevices))) - // 删除所有认证方式 - if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil { - return errors.Wrap(err, "删除认证方式失败") + // 2. 循环为每个设备创建新用户并迁移记录 (保留设备ID) + for _, dev := range userDevices { + // A. 创建新匿名用户 + newUser, err := l.createAnonymousUser(tx) + if err != nil { + l.Errorw("为设备分配新用户主体失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error())) + return err + } + + // B. 迁移设备记录 (Update user_id) + if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil { + l.Errorw("迁移设备记录失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error())) + return errors.Wrap(err, "迁移设备记录失败") + } + + // C. 迁移设备认证方式 (Update user_id) + if err := tx.Model(&user.AuthMethods{}). + Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", dev.Identifier). + Update("user_id", newUser.Id).Error; err != nil { + l.Errorw("迁移设备认证失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error())) + return errors.Wrap(err, "迁移设备认证失败") + } + + // 如果是当前请求的设备,记录其新 UserID 返回给前端 + if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) { + newUserId = newUser.Id + } + l.Infow("旧设备已迁移至新匿名账号", + logger.Field("old_user_id", currentUser.Id), + logger.Field("new_user_id", newUser.Id), + logger.Field("device_id", dev.Id), + logger.Field("identifier", dev.Identifier)) } - // 删除所有设备记录 + // 3. 删除旧账号的剩余数据 + // 删除剩余的认证方式 (排除已迁移的device类型,剩下的如email/mobile等) + // 注意:刚才已经把由currentUser拥有的device类型auth都迁移走了,所以这里直接删剩下的即可 + if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil { + return errors.Wrap(err, "删除剩余认证方式失败") + } + + // 设备记录已经全部迁移,理论上 user_id = currentUser.Id 的 device 应该没了,但为了保险可以删一下(或者是0) if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}).Error; err != nil { - return errors.Wrap(err, "删除设备记录失败") + return errors.Wrap(err, "删除残留设备记录失败") } // 删除所有订阅 @@ -182,25 +217,6 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon return errors.Wrap(err, "删除用户失败") } - // 3. 循环为每个设备重新创建匿名用户 (分配账号,避免重新注册领试用) - for _, dev := range userDevices { - // 为该设备创建新用户 - newUser, err := l.registerUserAndDevice(tx, dev.Identifier, dev.Ip, dev.UserAgent) - if err != nil { - l.Errorw("为旧设备分配新账号失败", logger.Field("identifier", dev.Identifier), logger.Field("error", err.Error())) - return err - } - - // 如果是当前请求的设备,记录其新 UserID 返回给前端 - if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) { - newUserId = newUser.Id - } - l.Infow("旧设备已迁移至新匿名账号", logger.Field("old_user_id", currentUser.Id), logger.Field("new_user_id", newUser.Id), logger.Field("identifier", dev.Identifier)) - } - - // 如果循环结束还没找到当前设备的新ID (极端情况,比如currentDeviceId不在列表中),这里可能需要处理 - // 通常第一步查询 userDevices 应该包含 currentDeviceId,除非并发删除 - return nil }) @@ -324,3 +340,25 @@ func (l *DeleteAccountLogic) registerUserAndDevice(tx *gorm.DB, identifier, ip, return userInfo, nil } + +// createAnonymousUser 创建一个新的匿名用户主体 (仅User表) +func (l *DeleteAccountLogic) createAnonymousUser(tx *gorm.DB) (*user.User, error) { + // 1. 创建新用户 + userInfo := &user.User{ + Salt: "default", + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, + } + if err := tx.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) + } + + // 2. 更新推荐码 + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", logger.Field("user_id", userInfo.Id), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + return userInfo, nil +} diff --git a/internal/logic/public/user/deleteAccountLogic_test.go b/internal/logic/public/user/deleteAccountLogic_test.go index 5c85a0b..aeb6990 100644 --- a/internal/logic/public/user/deleteAccountLogic_test.go +++ b/internal/logic/public/user/deleteAccountLogic_test.go @@ -97,19 +97,23 @@ func TestDeleteAccount_Guest_SingleDevice(t *testing.T) { // Assertions for Guest User (Should be DELETED) // Because 1 auth (device) and 1 device count -> isMainAccount = false + // Check Old User deleted var userCount int64 db.Model(&user.User{}).Where("refer_code = ?", "ref1").Count(&userCount) assert.Equal(t, int64(0), userCount, "Old User (by refer code) should be deleted") - // Old device record (ID 10) should be gone - var deviceCount int64 - db.Model(&user.Device{}).Where("id = ?", 10).Count(&deviceCount) - assert.Equal(t, int64(0), deviceCount, "Old device record should be deleted") + // Device record (ID 10) should PRESERVED but have a NEW user_id + var updatedDevice user.Device + err = db.Model(&user.Device{}).Where("id = ?", 10).First(&updatedDevice).Error + assert.NoError(t, err, "Device record should still exist") + assert.NotEqual(t, int64(1), updatedDevice.UserId, "Device should have a new user ID") + assert.Equal(t, "device1_id", updatedDevice.Identifier, "Device identifier should remain unchanged") - // Check if new user created for the device - var newDeviceCount int64 - db.Model(&user.Device{}).Where("identifier = ?", "device1_id").Count(&newDeviceCount) - assert.Equal(t, int64(1), newDeviceCount, "New device record should be created") + // Check AuthMethod updated + var updatedAuth user.AuthMethods + err = db.Where("auth_type = ? AND auth_identifier = ?", "device", "device1_id").First(&updatedAuth).Error + assert.NoError(t, err, "AuthMethod should still exist") + assert.Equal(t, updatedDevice.UserId, updatedAuth.UserId, "AuthMethod should link to new user ID") } func TestDeleteAccount_User_WithEmail(t *testing.T) { @@ -162,14 +166,19 @@ func TestDeleteAccount_User_WithEmail(t *testing.T) { db.Model(&user.User{}).Where("id = ?", 2).Count(&userCount) assert.Equal(t, int64(0), userCount, "User should be deleted") - var deviceCount int64 - db.Model(&user.Device{}).Where("id = ?", 20).Count(&deviceCount) - assert.Equal(t, int64(0), deviceCount, "Old device record should be deleted") + // Device record (ID 20) should PRESERVED with NEW user_id + var updatedDevice user.Device + err = db.Model(&user.Device{}).Where("id = ?", 20).First(&updatedDevice).Error + assert.NoError(t, err, "Device record should still exist") + assert.NotEqual(t, int64(2), updatedDevice.UserId, "Device should have a new user ID") - var authDeviceCount int64 - db.Model(&user.AuthMethods{}).Where("user_id = ? AND auth_type = 'device'", 2).Count(&authDeviceCount) - assert.Equal(t, int64(0), authDeviceCount, "Device auth should be removed") + // Device Auth should PRESERVED with NEW user_id + var updatedAuthDevice user.AuthMethods + err = db.Model(&user.AuthMethods{}).Where("auth_type = 'device' AND auth_identifier = 'device2_id'").First(&updatedAuthDevice).Error + assert.NoError(t, err, "Device auth should still exist") + assert.Equal(t, updatedDevice.UserId, updatedAuthDevice.UserId) + // Email Auth should be REMOVED var authEmailCount int64 db.Model(&user.AuthMethods{}).Where("user_id = ? AND auth_type = 'email'", 2).Count(&authEmailCount) assert.Equal(t, int64(0), authEmailCount, "Email auth should be removed") @@ -232,34 +241,20 @@ func TestDeleteAccount_User_MultiDevice(t *testing.T) { db.Model(&user.User{}).Where("id = ?", 3).Count(&userCount) assert.Equal(t, int64(0), userCount, "Old User should be deleted") - // Old device records should be deleted - var device1Count int64 - db.Model(&user.Device{}).Where("id = ?", 31).Count(&device1Count) - assert.Equal(t, int64(0), device1Count, "Old device 1 record should be deleted") + // Device 1 (ID 31) should be PRESERVED + var updatedDev1 user.Device + err = db.Model(&user.Device{}).Where("id = ?", 31).First(&updatedDev1).Error + assert.NoError(t, err, "Device 1 should still exist") + assert.NotEqual(t, int64(3), updatedDev1.UserId, "Device 1 should have new UserID") - var device2Count int64 - db.Model(&user.Device{}).Where("id = ?", 32).Count(&device2Count) - assert.Equal(t, int64(0), device2Count, "Old device 2 record should be deleted") + // Device 2 (ID 32) should be PRESERVED + var updatedDev2 user.Device + err = db.Model(&user.Device{}).Where("id = ?", 32).First(&updatedDev2).Error + assert.NoError(t, err, "Device 2 should still exist") + assert.NotEqual(t, int64(3), updatedDev2.UserId, "Device 2 should have new UserID") - // NEW Device records should be created - var newDevice1Count int64 - db.Model(&user.Device{}).Where("identifier = ?", "device3_1").Count(&newDevice1Count) - assert.Equal(t, int64(1), newDevice1Count, "New Device 1 should be created") - - var newDevice2Count int64 - db.Model(&user.Device{}).Where("identifier = ?", "device3_2").Count(&newDevice2Count) - assert.Equal(t, int64(1), newDevice2Count, "New Device 2 should be created") - - // Verify they are anonymous (different users, assuming registerUserAndDevice creates new user each time) - var newDev1 user.Device - db.Where("identifier = ?", "device3_1").First(&newDev1) - assert.NotEqual(t, int64(3), newDev1.UserId, "New Device 1 should have new UserID") - - var newDev2 user.Device - db.Where("identifier = ?", "device3_2").First(&newDev2) - assert.NotEqual(t, int64(3), newDev2.UserId, "New Device 2 should have new UserID") - - assert.NotEqual(t, newDev1.UserId, newDev2.UserId, "Devices should have independent user accounts") + // Verify they are independent users + assert.NotEqual(t, updatedDev1.UserId, updatedDev2.UserId, "Devices should have independent user accounts") } func TestDeleteAccount_MissingDeviceID(t *testing.T) { @@ -298,13 +293,9 @@ func TestDeleteAccount_MissingDeviceID(t *testing.T) { db.Model(&user.User{}).Where("refer_code = ?", "ref4").Count(&userCount) assert.Equal(t, int64(0), userCount, "User should be deleted even without device context") - // Old device should be deleted - var deviceCount int64 - db.Model(&user.Device{}).Where("id = ?", 40).Count(&deviceCount) - assert.Equal(t, int64(0), deviceCount, "Old device record should be deleted") - - // New device should be created for the orphaned device - var newDeviceCount int64 - db.Model(&user.Device{}).Where("identifier = ?", "device4_id").Count(&newDeviceCount) - assert.Equal(t, int64(1), newDeviceCount, "Device should be reset to new account even if caller didn't specify deviceID") + // Device record (ID 40) should be PRESERVED + var updatedDevice user.Device + err = db.Model(&user.Device{}).Where("id = ?", 40).First(&updatedDevice).Error + assert.NoError(t, err, "Device record should still exist") + assert.NotEqual(t, int64(4), updatedDevice.UserId, "Device should have a new user ID") } diff --git a/internal/logic/public/user/verifyEmailLogic.go b/internal/logic/public/user/verifyEmailLogic.go index 2c3aa12..7539311 100644 --- a/internal/logic/public/user/verifyEmailLogic.go +++ b/internal/logic/public/user/verifyEmailLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "github.com/perfect-panel/server/internal/config" @@ -37,6 +38,7 @@ type CacheKeyPayload struct { } func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { + req.Email = strings.ToLower(strings.TrimSpace(req.Email)) cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() if err != nil { diff --git a/pkg/aes/manual_decrypt_test.go b/pkg/aes/manual_decrypt_test.go new file mode 100644 index 0000000..b58e45a --- /dev/null +++ b/pkg/aes/manual_decrypt_test.go @@ -0,0 +1,18 @@ +package pkgaes + +import ( + "fmt" + "testing" +) + +func TestManualDecrypt(t *testing.T) { + cipherText := "rLuw+6cV+o3+pVoMdeZ0vOqoRaRvMpUV7VNgEXY9qYFOdGPZ5eQ6KashmOI1d7B6lzbYa0ccqOGFBM2Xfon4GzF/WrYf+jyWD673UIWGiQt4QOsUBZ7k7X1wMHYXsZGaau4mv0YD/8b5raY6s/QNh5mdihXTsdsZ1PIPmpmoiYVMUcOl1WUfoSXg/iSB5aX64Rb9NvPeRFExoo22A+rPpP3n1txMOecmDBBaoCwEr5lUF5I53d/DaZxjzB0BJ9RcA0jaaecuvDG7QJ6n7kVFtWlB+OEBmtklN17a9Bh0m+9DOB9axu3FjBsOaterDa0ufJtfyW/jPvCgKZKclzNS8xhrZrDY9BUt8kIpPRTi6974q8rayvl/ISxQihOm/FiJ+x/zEr6hLekFhXvlDPcV5lyzT6wjUEldkM0u3Ldiqdv3e0eYUyqoaTjnJCjlSfkb2wKX14bn984tYK5IfU6OjLCEUSiAFSzeHtEmpfb+861sJq/EJep7TeEsUqJZNRY2KUAawUjnAtKSlX7kHjvZGFicZqlQUGcha9CPSOpwnGeZz51q5JXJo1H7CqnYGyZZZrIkB+qi4ZK0EGkO0Mm/cLun5a1sWdkgfQixQW4jKRnjrdohAlbLV4AC9tUODUzgl1Ot+7xP2+zo4SbOGQ8zE+sthtBme0NeMHjW00magCHJbpV+bnVZHr3jGpQQUMAdNAyRpQdIn3Nitv3Hun/HLU3EhT38dIBHGkx47RMN0NKkKcePqN3ImIdqsM1jR+GnK5oM2qUlra2tk06bKHMAOo8csmIMIwwh7yhI0bz+UaMDQjPf6/YdYNQaB40vAokbjxC1pEWDvpSR+QSn7NnXzLZf5iwTvkErwWolbJbJCV7YA5zq1PBNVkSY5d3lj0iVe9/oDqcnDWOFgXrbZ1+QowceJSumEXna7M5RMp7tBJUxlyTag7z1jKVBqWv7ydWNQnBT+MpzHLzXOdAuClrKwrYgeaXafqsTZvsNGA7Jtfr+eWIfifT1f/6yqiOa90ZqPv1dmpSIgOTA9NpOPvChQ1VicC9SiS/q0lD9/ZD5H9PvmFylo4DGKGmpEXrnOSy2770WCmNJmjuxf68NBcR/6mN9JBd7XnS+BjGXymybMIVZvy/dsC6zU2AKSolSNjeraP3zNl1fOQNNFDDnd+y/z4RHgbRvGg/BkYygx+sz0Fc87miccLIG2fIaJeGNSg+VFSlWlAFmhv07hZk6k3g/+J+3u1jUiCtQfAoUcE2f18OtdOCSib49e63uCH1NQIHp" + keyStr := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx" + ivStr := "188ec64c36b50c13" + + plaintext, err := Decrypt(cipherText, keyStr, ivStr) + if err != nil { + t.Fatalf("Decryption failed: %v", err) + } + fmt.Printf("\nDEC_START\n%s\nDEC_END\n", plaintext) +} diff --git a/pkg/email/template.go b/pkg/email/template.go index 787bf39..00121a0 100644 --- a/pkg/email/template.go +++ b/pkg/email/template.go @@ -1,108 +1,71 @@ package email const ( - DefaultEmailVerifyTemplate = ` - -
- - -{{.SiteName}}
-Hi, 尊敬的用户
-- {{if eq .Type 1}} 感谢您注册!您的验证码是(请于{{.Expire}}分钟内使用): - {{else if eq .Type 4}} 您正在申请注销账号。您的验证码是(请于{{.Expire}}分钟内使用): - {{else}} - 您的验证码是(请于{{.Expire}}分钟内使用): - {{end}} -
-- 如果您未请求此验证码,请忽略此邮件。 -
-
+
|
+