From 3bbd687231c353bc210679ec71f07fc694dd056d Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Fri, 17 Oct 2025 19:04:47 -0700 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E7=BB=91=E5=AE=9A):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=94=AF=E6=8C=81=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E9=87=8D=E6=96=B0=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在设备重新绑定到新用户时,自动迁移旧用户的订单、订阅和余额数据 移除游客订单特殊处理逻辑,统一所有订单处理流程 更新设备绑定文档说明新的静默登录机制 --- .gitea/workflows/docker.yml | 2 +- DESIGN_SILENT_LOGIN.md | 128 ++++++++++++++++++ internal/logic/auth/bindDeviceLogic.go | 82 ++++++++++- .../logic/public/order/closeOrderLogic.go | 13 +- ppanel.db | 0 queue/logic/order/activateOrderLogic.go | 12 +- 6 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 DESIGN_SILENT_LOGIN.md create mode 100644 ppanel.db diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 3f00a9a..5513e75 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -21,7 +21,7 @@ env: SSH_PASSWORD: ${{ vars.SSH_PASSWORD }} # TG通知 TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0 - TG_CHAT_ID: "-4940243803111" + TG_CHAT_ID: "-4940243803" # Go构建变量 SERVICE: ario SERVICE_STYLE: ario diff --git a/DESIGN_SILENT_LOGIN.md b/DESIGN_SILENT_LOGIN.md new file mode 100644 index 0000000..47a3568 --- /dev/null +++ b/DESIGN_SILENT_LOGIN.md @@ -0,0 +1,128 @@ +# 设备静默登录机制设计方案 + +## 需求分析 +1. 用户进来之后就是静默登录(设备登录) +2. 用户可以主动关联邮箱;也可以不关联邮箱 +3. 用户换手机后在别的地方用邮箱登录会绑定另外一部手机的设备号 +4. 没有游客概念,快捷登录进来的用户就是正式用户 + +## 当前系统分析 + +### 现有认证流程 +- **设备登录**:已存在 `deviceLoginLogic.go`,通过设备标识符自动登录 +- **邮箱登录**:已存在 `userLoginLogic.go`,需要邮箱+密码 +- **游客模式**:通过 `Order.UserId = 0` 标识游客订单 + +### 现有设备管理 +- **设备绑定**:`bindDeviceLogic.go` 处理设备与用户绑定 +- **设备解绑**:`unbindDeviceLogic.go` 处理设备解绑 +- **设备迁移**:支持设备在用户间转移 + +## 设计方案 + +### 1. 核心改动策略 +- **保留现有设备登录机制**,作为默认登录方式 +- **移除游客概念**,所有设备登录用户都是正式用户 +- **增强邮箱绑定功能**,支持跨设备登录 +- **优化设备迁移逻辑**,支持邮箱登录后绑定新设备 + +### 2. 具体实现方案 + +#### 2.1 修改设备登录逻辑 +**文件**: `internal/logic/auth/deviceLoginLogic.go` + +**改动点**: +- 移除 `registerUserAndDevice` 中的试用激活逻辑 +- 确保所有通过设备登录创建的用户都是正式用户 +- 保持现有的设备绑定机制 + +#### 2.2 修改订单处理逻辑 +**文件**: `queue/logic/order/activateOrderLogic.go` + +**改动点**: +- 移除 `getUserOrCreate` 中的游客判断逻辑 (`orderInfo.UserId == 0`) +- 移除 `createGuestUser` 函数 +- 修改为:如果订单没有关联用户,通过设备标识符创建或获取用户 + +#### 2.3 修改订单关闭逻辑 +**文件**: `internal/logic/public/order/closeOrderLogic.go` + +**改动点**: +- 移除对 `UserId == 0` 的特殊处理 +- 统一处理所有订单的关闭逻辑 + +#### 2.4 增强邮箱登录逻辑 +**文件**: `internal/logic/auth/userLoginLogic.go` + +**改动点**: +- 在邮箱登录成功后,如果提供了设备标识符,自动绑定设备 +- 支持邮箱登录后在新设备上的自动绑定 + +#### 2.5 优化设备绑定逻辑 +**文件**: `internal/logic/auth/bindDeviceLogic.go` + +**改动点**: +- 增强设备迁移逻辑,支持邮箱用户登录新设备时的自动绑定 +- 保持现有的设备冲突处理机制 + +### 3. 数据库变更 +**无需修改数据库结构**,现有的用户和设备表结构已经支持新的需求。 + +### 4. API 变更 +**无需修改 API 接口**,现有的设备登录和邮箱登录接口已经满足需求。 + +### 5. 前端适配 +**前端需要调整**: +- 默认使用设备登录作为主要登录方式 +- 提供邮箱绑定入口 +- 在新设备上提供邮箱登录选项 + +## 实施步骤 + +### 第一步:修改订单逻辑 +1. 修改 `activateOrderLogic.go`,移除游客概念 +2. 修改 `closeOrderLogic.go`,统一订单处理逻辑 + +### 第二步:增强设备登录 +1. 确保设备登录创建的都是正式用户 +2. 优化设备绑定逻辑 + +### 第三步:增强邮箱登录 +1. 在邮箱登录后支持设备绑定 +2. 优化跨设备登录体验 + +### 第四步:测试验证 +1. 测试设备静默登录 +2. 测试邮箱绑定功能 +3. 测试跨设备登录 + +## 优势分析 + +### 1. 最小化改动 +- 复用现有的设备登录机制 +- 保持现有的数据库结构 +- 保持现有的 API 接口 + +### 2. 用户体验提升 +- 用户进入即可使用,无需注册 +- 支持邮箱绑定,便于跨设备使用 +- 保持数据连续性 + +### 3. 系统稳定性 +- 基于现有成熟机制 +- 减少新增代码量 +- 降低引入 bug 的风险 + +## 风险评估 + +### 1. 数据迁移 +- **风险**: 现有游客数据需要处理 +- **方案**: 可以保持现有游客数据不变,新用户使用新机制 + +### 2. 兼容性 +- **风险**: 现有客户端可能需要适配 +- **方案**: 保持 API 兼容,逐步引导用户使用新机制 + +### 3. 性能影响 +- **风险**: 设备登录可能增加数据库压力 +- **方案**: 现有机制已经过验证,影响可控 \ No newline at end of file diff --git a/internal/logic/auth/bindDeviceLogic.go b/internal/logic/auth/bindDeviceLogic.go index 19b0666..6fdc47c 100644 --- a/internal/logic/auth/bindDeviceLogic.go +++ b/internal/logic/auth/bindDeviceLogic.go @@ -167,6 +167,16 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use // Only disable old user if they have no other auth methods if nonDeviceAuthCount == 0 { + // Migrate user data from old user to new user before disabling + if err := l.migrateUserData(db, oldUserId, newUserId); err != nil { + l.Errorw("failed to migrate user data", + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "migrate user data failed: %v", err) + } + falseVal := false if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil { l.Errorw("failed to disable old user", @@ -176,8 +186,9 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err) } - l.Infow("disabled old user (no other auth methods)", + l.Infow("disabled old user after data migration (no other auth methods)", logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), ) } else { l.Infow("old user has other auth methods, not disabling", @@ -232,3 +243,72 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use return nil } + +// migrateUserData migrates user data from old user to new user +// This includes orders, subscriptions, balance, and other user-related data +func (l *BindDeviceLogic) migrateUserData(db *gorm.DB, oldUserId, newUserId int64) error { + l.Infow("starting user data migration", + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + ) + + // Migrate orders - using table name directly + if err := db.Table("order").Where("user_id = ?", oldUserId).Update("user_id", newUserId).Error; err != nil { + l.Errorw("failed to migrate orders", + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return err + } + + // Migrate user subscriptions - using Subscribe model + if err := db.Model(&user.Subscribe{}).Where("user_id = ?", oldUserId).Update("user_id", newUserId).Error; err != nil { + l.Errorw("failed to migrate user subscriptions", + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return err + } + + // Migrate user balance and gift amount + var oldUser user.User + if err := db.Where("id = ?", oldUserId).First(&oldUser).Error; err != nil { + l.Errorw("failed to get old user data", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return err + } + + // Add old user's balance and gift to new user + if oldUser.Balance > 0 || oldUser.GiftAmount > 0 { + if err := db.Model(&user.User{}).Where("id = ?", newUserId).Updates(map[string]interface{}{ + "balance": gorm.Expr("balance + ?", oldUser.Balance), + "gift_amount": gorm.Expr("gift_amount + ?", oldUser.GiftAmount), + }).Error; err != nil { + l.Errorw("failed to migrate user balance and gift", + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("old_balance", oldUser.Balance), + logger.Field("old_gift", oldUser.GiftAmount), + logger.Field("error", err.Error()), + ) + return err + } + } + + // Migrate other user-related tables if needed + // Note: We don't migrate auth methods as they are handled separately + // Note: Traffic usage (Upload/Download) is tracked at subscription level, not user level + + l.Infow("user data migration completed", + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("migrated_balance", oldUser.Balance), + logger.Field("migrated_gift", oldUser.GiftAmount), + ) + + return nil +} diff --git a/internal/logic/public/order/closeOrderLogic.go b/internal/logic/public/order/closeOrderLogic.go index ced53b8..fb1cfc8 100644 --- a/internal/logic/public/order/closeOrderLogic.go +++ b/internal/logic/public/order/closeOrderLogic.go @@ -61,18 +61,7 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { ) return err } - // If User ID is 0, it means that the order is a guest order and does not need to be refunded, the order can be deleted directly - if orderInfo.UserId == 0 { - err = tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Delete(&order.Order{}).Error - if err != nil { - l.Errorw("[CloseOrder] Delete order failed", - logger.Field("error", err.Error()), - logger.Field("orderNo", req.OrderNo), - ) - return err - } - return nil - } + // All orders now have associated users, no special handling needed for guest orders // refund deduction amount to user deduction balance if orderInfo.GiftAmount > 0 { userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) diff --git a/ppanel.db b/ppanel.db new file mode 100644 index 0000000..e69de29 diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 3bb7cbe..79d1c0b 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -192,12 +192,12 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O return nil } -// getUserOrCreate retrieves an existing user or creates a new guest user based on order details +// getUserOrCreate retrieves an existing user or creates a new user based on order details func (l *ActivateOrderLogic) getUserOrCreate(ctx context.Context, orderInfo *order.Order) (*user.User, error) { if orderInfo.UserId != 0 { return l.getExistingUser(ctx, orderInfo.UserId) } - return l.createGuestUser(ctx, orderInfo) + return l.createUserFromTempOrder(ctx, orderInfo) } // getExistingUser retrieves user information by user ID @@ -213,9 +213,9 @@ func (l *ActivateOrderLogic) getExistingUser(ctx context.Context, userId int64) return userInfo, nil } -// createGuestUser creates a new user account for guest orders using temporary order information -// stored in Redis cache -func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *order.Order) (*user.User, error) { +// createUserFromTempOrder creates a new user account using temporary order information +// stored in Redis cache. All users created this way are formal users, not guests. +func (l *ActivateOrderLogic) createUserFromTempOrder(ctx context.Context, orderInfo *order.Order) (*user.User, error) { tempOrder, err := l.getTempOrderInfo(ctx, orderInfo.OrderNo) if err != nil { return nil, err @@ -253,7 +253,7 @@ func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *ord // Handle referrer relationship l.handleReferrer(ctx, userInfo, tempOrder.InviteCode) - logger.WithContext(ctx).Info("Create guest user success", + logger.WithContext(ctx).Info("Create user success", logger.Field("user_id", userInfo.Id), logger.Field("identifier", tempOrder.Identifier), logger.Field("auth_type", tempOrder.AuthType),