feat(设备绑定): 添加用户数据迁移功能以支持设备重新绑定

在设备重新绑定到新用户时,自动迁移旧用户的订单、订阅和余额数据
移除游客订单特殊处理逻辑,统一所有订单处理流程
更新设备绑定文档说明新的静默登录机制
This commit is contained in:
shanshanzhong 2025-10-17 19:04:47 -07:00
parent bfbc675e1a
commit 3bbd687231
6 changed files with 217 additions and 20 deletions

View File

@ -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

128
DESIGN_SILENT_LOGIN.md Normal file
View File

@ -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. 性能影响
- **风险**: 设备登录可能增加数据库压力
- **方案**: 现有机制已经过验证,影响可控

View File

@ -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
}

View File

@ -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)

0
ppanel.db Normal file
View File

View File

@ -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),