邮件
This commit is contained in:
parent
48c92ea374
commit
d1d95618ad
@ -43,7 +43,7 @@ type (
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
ReferCode string `json:"refer_code"`
|
||||
RefererId int64 `json:"referer_id"`
|
||||
RefererId *int64 `json:"referer_id"`
|
||||
Enable bool `json:"enable"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
MemberStatus string `json:"member_status"`
|
||||
|
||||
@ -42,7 +42,7 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
l := user.NewDeleteAccountLogic(c.Request.Context(), serverCtx)
|
||||
resp, err := l.DeleteAccount()
|
||||
resp, err := l.DeleteAccountAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@ -2,9 +2,6 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@ -66,8 +63,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
|
||||
// Set MemberStatus and update LastLoginTime from traffic
|
||||
if info, ok := activeSubs[item.Id]; ok {
|
||||
days := math.Ceil(info.ExpireTime.Sub(time.Now()).Hours() / 24)
|
||||
u.MemberStatus = fmt.Sprintf("%s*%d", info.MemberStatus, int(days))
|
||||
u.MemberStatus = info.MemberStatus
|
||||
|
||||
if info.LastTrafficAt != nil {
|
||||
trafficTime := info.LastTrafficAt.Unix()
|
||||
|
||||
@ -135,6 +135,38 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
||||
l.Infow("邮箱已存在,将设备转移到现有邮箱用户",
|
||||
logger.Field("email", req.Email),
|
||||
logger.Field("email_user_id", emailUserId))
|
||||
|
||||
// 补全邀请人逻辑:如果邮箱账号没有邀请人,但设备账号有,则继承设备账号的邀请人
|
||||
emailUser, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
|
||||
if err == nil {
|
||||
updates := make(map[string]interface{})
|
||||
// 1. 处理 RefererId (邀请人)
|
||||
if emailUser.RefererId == 0 && u.RefererId != 0 {
|
||||
updates["referer_id"] = u.RefererId
|
||||
l.Infow("将设备账号邀请人转移给邮箱账号",
|
||||
logger.Field("email_user_id", emailUserId),
|
||||
logger.Field("referer_id", u.RefererId))
|
||||
}
|
||||
// 2. 处理 ReferCode (如果邮箱账号意外没有邀请码,沿用设备的或生成新的) - 这是一个兜底,通常创建用户时已有
|
||||
if emailUser.ReferCode == "" {
|
||||
if u.ReferCode != "" {
|
||||
updates["refer_code"] = u.ReferCode
|
||||
} else {
|
||||
updates["refer_code"] = uuidx.UserInviteCode(emailUserId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
return tx.Model(&user.User{}).Where("id = ?", emailUserId).Updates(updates).Error
|
||||
}); err != nil {
|
||||
l.Errorw("更新邮箱用户信息失败", logger.Field("error", err.Error()))
|
||||
// 不阻断主流程
|
||||
}
|
||||
}
|
||||
} else {
|
||||
l.Errorw("查询目标邮箱用户失败,跳过邀请人合并", logger.Field("error", err.Error()))
|
||||
}
|
||||
// 创建前 需要 吧 原本的 user_devices 表中过的数据删除掉 防止出现两个记录
|
||||
devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
|
||||
@ -131,6 +131,104 @@ func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse,
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteAccountAll 注销账号逻辑 (全部解绑默认创建账号)
|
||||
func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountResponse, err error) {
|
||||
// 获取当前用户
|
||||
currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 获取当前调用设备 ID
|
||||
currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64)
|
||||
|
||||
resp = &types.DeleteAccountResponse{}
|
||||
var newUserId int64
|
||||
|
||||
// 开始数据库事务
|
||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
// 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()))
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果没有识别到调用设备 ID,记录日志但继续执行 (全量注销不应受限)
|
||||
if currentDeviceId == 0 {
|
||||
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)))
|
||||
|
||||
// 删除所有认证方式
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil {
|
||||
return errors.Wrap(err, "删除认证方式失败")
|
||||
}
|
||||
|
||||
// 删除所有设备记录
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}).Error; err != nil {
|
||||
return errors.Wrap(err, "删除设备记录失败")
|
||||
}
|
||||
|
||||
// 删除所有订阅
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{}).Error; err != nil {
|
||||
return errors.Wrap(err, "删除订阅失败")
|
||||
}
|
||||
|
||||
// 删除用户主体
|
||||
if err := tx.Delete(&user.User{}, currentUser.Id).Error; err != nil {
|
||||
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
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 最终清理所有 Session (踢掉所有设备)
|
||||
l.clearAllSessions(currentUser.Id)
|
||||
|
||||
resp.Success = true
|
||||
resp.Message = "注销成功"
|
||||
resp.UserId = newUserId
|
||||
resp.Code = 200
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 辅助方法:通过 ID 查找 Identifier (以防 currentDeviceId 只是 ID)
|
||||
func (l *DeleteAccountLogic) getIdentifierByDeviceID(devices []user.Device, id int64) string {
|
||||
for _, d := range devices {
|
||||
if d.Id == id {
|
||||
return d.Identifier
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// clearCurrentSession 清理当前请求的会话
|
||||
func (l *DeleteAccountLogic) clearCurrentSession(userId int64) {
|
||||
if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" {
|
||||
@ -146,6 +244,33 @@ func (l *DeleteAccountLogic) clearCurrentSession(userId int64) {
|
||||
}
|
||||
}
|
||||
|
||||
// clearAllSessions 清理指定用户的所有会话
|
||||
func (l *DeleteAccountLogic) clearAllSessions(userId int64) {
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
|
||||
|
||||
// 获取所有 session id
|
||||
sessions, err := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result()
|
||||
if err != nil {
|
||||
l.Errorw("获取用户会话列表失败", logger.Field("user_id", userId), logger.Field("error", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 删除每个 session 的详情 key
|
||||
for _, sid := range sessions {
|
||||
sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sid)
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err()
|
||||
|
||||
// 同时尝试删除 detail key (如果存在)
|
||||
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, detailKey).Err()
|
||||
}
|
||||
|
||||
// 删除用户的 session 集合 key
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, sessionsKey).Err()
|
||||
|
||||
l.Infow("[SessionMonitor] 注销账号-清除所有Session", logger.Field("user_id", userId), logger.Field("count", len(sessions)))
|
||||
}
|
||||
|
||||
// generateReferCode 生成推荐码
|
||||
func generateReferCode() string {
|
||||
bytes := make([]byte, 4)
|
||||
|
||||
310
internal/logic/public/user/deleteAccountLogic_test.go
Normal file
310
internal/logic/public/user/deleteAccountLogic_test.go
Normal file
@ -0,0 +1,310 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func createTestSvcCtx(t *testing.T, testName string) (*svc.ServiceContext, *gorm.DB, *miniredis.Miniredis) {
|
||||
// 1. Setup Miniredis
|
||||
mr, err := miniredis.Run()
|
||||
assert.NoError(t, err)
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: mr.Addr(),
|
||||
})
|
||||
|
||||
// 2. Setup GORM with SQLite (File based for reliability)
|
||||
dbName := fmt.Sprintf("test_%s.db", testName)
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Remove(dbName)
|
||||
})
|
||||
|
||||
// Migrate tables (Using Migrator to bypass index collision errors on global schema in SQLite)
|
||||
_ = db.Migrator().CreateTable(&user.User{})
|
||||
_ = db.Migrator().CreateTable(&user.Device{})
|
||||
_ = db.Migrator().CreateTable(&user.AuthMethods{})
|
||||
_ = db.Migrator().CreateTable(&user.Subscribe{})
|
||||
|
||||
// 3. Create ServiceContext
|
||||
c := config.Config{}
|
||||
c.Invite.OnlyFirstPurchase = true
|
||||
|
||||
svcCtx := &svc.ServiceContext{
|
||||
Redis: rdb,
|
||||
DB: db,
|
||||
Config: c,
|
||||
UserModel: user.NewModel(db, rdb),
|
||||
}
|
||||
|
||||
return svcCtx, db, mr
|
||||
}
|
||||
|
||||
func TestDeleteAccount_Guest_SingleDevice(t *testing.T) {
|
||||
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
|
||||
defer mr.Close()
|
||||
|
||||
// Setup: User, 1 Device, No Email
|
||||
u := &user.User{
|
||||
Id: 1,
|
||||
ReferCode: "ref1",
|
||||
}
|
||||
db.Create(u)
|
||||
|
||||
device := &user.Device{
|
||||
Id: 10,
|
||||
UserId: 1,
|
||||
Identifier: "device1_id",
|
||||
}
|
||||
db.Create(device)
|
||||
|
||||
auth := &user.AuthMethods{
|
||||
UserId: 1,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: "device1_id",
|
||||
}
|
||||
db.Create(auth)
|
||||
|
||||
// Context
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, int64(10))
|
||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session1")
|
||||
|
||||
// Run Logic
|
||||
l := NewDeleteAccountLogic(ctx, svcCtx)
|
||||
resp, err := l.DeleteAccountAll()
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteAccountAll failed: %v", err)
|
||||
}
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
// Assertions for Guest User (Should be DELETED)
|
||||
// Because 1 auth (device) and 1 device count -> isMainAccount = false
|
||||
|
||||
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")
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func TestDeleteAccount_User_WithEmail(t *testing.T) {
|
||||
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
|
||||
defer mr.Close()
|
||||
|
||||
// Setup: User, 1 Device, 1 Email
|
||||
u := &user.User{
|
||||
Id: 2,
|
||||
}
|
||||
db.Create(u)
|
||||
|
||||
device := &user.Device{
|
||||
Id: 20,
|
||||
UserId: 2,
|
||||
Identifier: "device2_id",
|
||||
}
|
||||
db.Create(device)
|
||||
|
||||
authDevice := &user.AuthMethods{
|
||||
UserId: 2,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: "device2_id",
|
||||
}
|
||||
db.Create(authDevice)
|
||||
|
||||
authEmail := &user.AuthMethods{
|
||||
UserId: 2,
|
||||
AuthType: "email",
|
||||
AuthIdentifier: "test@example.com",
|
||||
}
|
||||
db.Create(authEmail)
|
||||
|
||||
// Context
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, int64(20))
|
||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session2")
|
||||
|
||||
// Run Logic
|
||||
l := NewDeleteAccountLogic(ctx, svcCtx)
|
||||
resp, err := l.DeleteAccountAll()
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteAccountAll failed: %v", err)
|
||||
}
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
// Assertions for Email User (Should BE deleted now)
|
||||
var userCount int64
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func TestDeleteAccount_User_MultiDevice(t *testing.T) {
|
||||
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
|
||||
defer mr.Close()
|
||||
|
||||
// Setup: User, 2 Devices
|
||||
u := &user.User{
|
||||
Id: 3,
|
||||
}
|
||||
db.Create(u)
|
||||
|
||||
// Device 1 (Current)
|
||||
device1 := &user.Device{
|
||||
Id: 31,
|
||||
UserId: 3,
|
||||
Identifier: "device3_1",
|
||||
}
|
||||
db.Create(device1)
|
||||
auth1 := &user.AuthMethods{
|
||||
UserId: 3,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: "device3_1",
|
||||
}
|
||||
db.Create(auth1)
|
||||
|
||||
// Device 2 (Other)
|
||||
device2 := &user.Device{
|
||||
Id: 32,
|
||||
UserId: 3,
|
||||
Identifier: "device3_2",
|
||||
}
|
||||
db.Create(device2)
|
||||
auth2 := &user.AuthMethods{
|
||||
UserId: 3,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: "device3_2",
|
||||
}
|
||||
db.Create(auth2)
|
||||
|
||||
// Context
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, int64(31)) // Current = Device 1
|
||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session3")
|
||||
|
||||
// Run Logic
|
||||
l := NewDeleteAccountLogic(ctx, svcCtx)
|
||||
resp, err := l.DeleteAccountAll()
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteAccountAll failed: %v", err)
|
||||
}
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
// Assertions for Multi Device User (Should BE deleted now)
|
||||
var userCount int64
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func TestDeleteAccount_MissingDeviceID(t *testing.T) {
|
||||
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
|
||||
defer mr.Close()
|
||||
|
||||
// Setup: User, 1 Device, but context missing DeviceID
|
||||
u := &user.User{
|
||||
Id: 4,
|
||||
ReferCode: "ref4",
|
||||
}
|
||||
db.Create(u)
|
||||
|
||||
device := &user.Device{
|
||||
Id: 40,
|
||||
UserId: 4,
|
||||
Identifier: "device4_id",
|
||||
}
|
||||
db.Create(device)
|
||||
|
||||
// Context (Missing DeviceID)
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session4")
|
||||
|
||||
// Run Logic
|
||||
l := NewDeleteAccountLogic(ctx, svcCtx)
|
||||
resp, err := l.DeleteAccountAll()
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteAccountAll failed: %v", err)
|
||||
}
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
// Assertions: User should be deleted
|
||||
var userCount int64
|
||||
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")
|
||||
}
|
||||
@ -42,7 +42,7 @@ type Subscribe struct {
|
||||
User User `gorm:"foreignKey:UserId;references:Id"`
|
||||
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"`
|
||||
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
|
||||
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
|
||||
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP;not null;comment:Subscription Start Time"`
|
||||
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
|
||||
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
|
||||
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
||||
|
||||
@ -2566,7 +2566,7 @@ type UpdateUserBasiceInfoRequest struct {
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
ReferCode string `json:"refer_code"`
|
||||
RefererId int64 `json:"referer_id"`
|
||||
RefererId *int64 `json:"referer_id"`
|
||||
Enable bool `json:"enable"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
MemberStatus string `json:"member_status"`
|
||||
|
||||
@ -7,8 +7,9 @@ const (
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>
|
||||
{{if eq .Type 1}}注册验证码 / Registration Verification Code{{else}}重置密码验证码 / Password
|
||||
Reset Verification Code{{end}}
|
||||
{{if eq .Type 1}}注册验证码
|
||||
{{else if eq .Type 4}}注销账号验证
|
||||
{{else}}验证码{{end}}
|
||||
</title>
|
||||
<style>
|
||||
body {
|
||||
@ -83,27 +84,23 @@ const (
|
||||
<p class="site-name">{{.SiteName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||
<p class="greeting">Hi, 尊敬的用户</p>
|
||||
<p>
|
||||
{{if eq .Type 1}} 感谢您注册!您的验证码是(请于<span class="highlight">{{.Expire}}</span
|
||||
>分钟内使用):
|
||||
<br />
|
||||
Thank you for registering! Your verification code is (please use it within
|
||||
<span class="highlight">{{.Expire}}</span> minutes): {{else}}
|
||||
您正在重置密码。您的验证码是(请于<span class="highlight">{{.Expire}}</span>分钟内使用):
|
||||
<br />
|
||||
You are resetting your password. Your verification code is (please use it within
|
||||
<span class="highlight">{{.Expire}}</span> minutes): {{end}}
|
||||
{{else if eq .Type 4}} 您正在申请注销账号。您的验证码是(请于<span class="highlight">{{.Expire}}</span>分钟内使用):
|
||||
{{else}}
|
||||
您的验证码是(请于<span class="highlight">{{.Expire}}</span>分钟内使用):
|
||||
{{end}}
|
||||
</p>
|
||||
<div class="code-container">
|
||||
<span class="code">{{.Code}}</span>
|
||||
</div>
|
||||
<p>
|
||||
如果您未请求此验证码,请忽略此邮件。<br />If you did not request this code, please ignore
|
||||
this email.
|
||||
如果您未请求此验证码,请忽略此邮件。
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||
<div class="footer">此为系统邮件,请勿回复</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -114,7 +111,7 @@ const (
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>系统维护通知 / System Maintenance Notice</title>
|
||||
<title>系统维护通知</title>
|
||||
<style>
|
||||
body {
|
||||
color: black;
|
||||
@ -174,25 +171,17 @@ const (
|
||||
<p class="site-name">{{.SiteName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||
<p class="greeting">Hi, 尊敬的用户</p>
|
||||
<p>
|
||||
我们计划在<span class="highlight">{{.MaintenanceDate}}</span
|
||||
>进行系统维护,预计维护时间为<span class="highlight">{{.MaintenanceTime}}</span
|
||||
>。在此期间,您可能会遇到服务中断或无法访问的情况。
|
||||
<br />
|
||||
We will be performing system maintenance on
|
||||
<span class="highlight">{{.MaintenanceDate}}</span>, and the expected maintenance period
|
||||
is <span class="highlight">{{.MaintenanceTime}}</span>. During this time, you may
|
||||
experience service interruptions or unavailability.
|
||||
</p>
|
||||
<p>
|
||||
维护完成后,系统将自动恢复。如果您有任何问题,请随时联系我们的支持团队。
|
||||
<br />
|
||||
The system will resume automatically once the maintenance is completed. If you have any
|
||||
questions, please feel free to contact our support team.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||
<div class="footer">此为系统邮件,请勿回复</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -202,7 +191,7 @@ const (
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>服务到期通知 / Service Expiration Notice</title>
|
||||
<title>服务到期通知</title>
|
||||
<style>
|
||||
body {
|
||||
color: black;
|
||||
@ -262,22 +251,16 @@ const (
|
||||
<p class="site-name">{{.SiteName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||
<p class="greeting">Hi, 尊敬的用户</p>
|
||||
<p>
|
||||
您的服务即将在<span class="highlight">{{.ExpireDate}}</span
|
||||
>到期,请及时续费以保证服务不间断。
|
||||
<br />
|
||||
Your service is set to expire on <span class="highlight">{{.ExpireDate}}</span>. Please
|
||||
renew your subscription to avoid service interruptions.
|
||||
</p>
|
||||
<p>
|
||||
如需帮助,请联系客服团队。感谢您的支持!
|
||||
<br />
|
||||
If you need assistance, please contact our support team. Thank you for your continued
|
||||
support!
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||
<div class="footer">此为系统邮件,请勿回复</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -287,7 +270,7 @@ const (
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>流量用尽通知 / Traffic Exhausted Notice</title>
|
||||
<title>流量用尽通知</title>
|
||||
<style>
|
||||
.container {
|
||||
border-radius: 5px;
|
||||
@ -342,21 +325,15 @@ const (
|
||||
<p class="site-name">{{.SiteName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||
<p class="greeting">Hi, 尊敬的用户</p>
|
||||
<p>
|
||||
您的流量已经用尽,请及时购买流量以继续使用我们的服务。
|
||||
<br />
|
||||
Your traffic has been exhausted. Please purchase additional traffic to continue using our
|
||||
service.
|
||||
</p>
|
||||
<p>
|
||||
如需帮助,请联系客服团队。感谢您的支持!
|
||||
<br />
|
||||
If you need assistance, please contact our support team. Thank you for your continued
|
||||
support!
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||
<div class="footer">此为系统邮件,请勿回复</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
@ -48,10 +49,33 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
var content string
|
||||
switch payload.Type {
|
||||
case types.EmailTypeVerify:
|
||||
tpl, _ := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate)
|
||||
var result bytes.Buffer
|
||||
tplStr := l.svcCtx.Config.Email.VerifyEmailTemplate
|
||||
|
||||
payload.Content["Type"] = uint8(payload.Content["Type"].(float64))
|
||||
// Use int for better template compatibility
|
||||
if t, ok := payload.Content["Type"].(float64); ok {
|
||||
payload.Content["Type"] = int(t)
|
||||
} else if t, ok := payload.Content["Type"].(int); ok {
|
||||
payload.Content["Type"] = t
|
||||
}
|
||||
|
||||
typeVal, _ := payload.Content["Type"].(int)
|
||||
|
||||
// Smart Fallback: If template is empty OR (Type is 4 but template doesn't support it), use default
|
||||
// We check for "Type 4" or "Type eq 4" string in the template as a heuristic
|
||||
needDefault := tplStr == ""
|
||||
if !needDefault && typeVal == 4 &&
|
||||
!strings.Contains(tplStr, "Type 4") &&
|
||||
!strings.Contains(tplStr, "Type eq 4") {
|
||||
logger.WithContext(ctx).Infow("[SendEmailLogic] Configured template might not support DeleteAccount (Type 4), forcing default template")
|
||||
needDefault = true
|
||||
}
|
||||
|
||||
if needDefault {
|
||||
tplStr = email.DefaultEmailVerifyTemplate
|
||||
}
|
||||
|
||||
tpl, _ := template.New("verify").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
@ -63,7 +87,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
}
|
||||
content = result.String()
|
||||
case types.EmailTypeMaintenance:
|
||||
tpl, _ := template.New("maintenance").Parse(l.svcCtx.Config.Email.MaintenanceEmailTemplate)
|
||||
tplStr := l.svcCtx.Config.Email.MaintenanceEmailTemplate
|
||||
if tplStr == "" {
|
||||
tplStr = email.DefaultMaintenanceEmailTemplate
|
||||
}
|
||||
tpl, _ := template.New("maintenance").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
@ -76,7 +104,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
}
|
||||
content = result.String()
|
||||
case types.EmailTypeExpiration:
|
||||
tpl, _ := template.New("expiration").Parse(l.svcCtx.Config.Email.ExpirationEmailTemplate)
|
||||
tplStr := l.svcCtx.Config.Email.ExpirationEmailTemplate
|
||||
if tplStr == "" {
|
||||
tplStr = email.DefaultExpirationEmailTemplate
|
||||
}
|
||||
tpl, _ := template.New("expiration").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
@ -89,7 +121,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
}
|
||||
content = result.String()
|
||||
case types.EmailTypeTrafficExceed:
|
||||
tpl, _ := template.New("traffic_exceed").Parse(l.svcCtx.Config.Email.TrafficExceedEmailTemplate)
|
||||
tplStr := l.svcCtx.Config.Email.TrafficExceedEmailTemplate
|
||||
if tplStr == "" {
|
||||
tplStr = email.DefaultTrafficExceedEmailTemplate
|
||||
}
|
||||
tpl, _ := template.New("traffic_exceed").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user