fix(用户绑定): 优化邮箱绑定逻辑并完善设备绑定功能
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m39s

修复绑定相同邮箱时的错误提示问题,允许重复绑定相同邮箱
重构设备绑定逻辑,增加详细注释和日志记录
移除无用的WebSocket测试端点
更新测试脚本中的默认配置和测试用例
This commit is contained in:
shanshanzhong 2025-10-23 04:11:02 -07:00
parent bafeaa35cd
commit 38655c0d38
8 changed files with 225 additions and 61 deletions

View File

@ -23,4 +23,4 @@ func BindEmailWithPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Contex
err := l.BindEmailWithPassword(&req)
result.HttpResult(c, nil, err)
}
}
}

View File

@ -22,7 +22,6 @@ import (
adminTicket "github.com/perfect-panel/server/internal/handler/admin/ticket"
adminTool "github.com/perfect-panel/server/internal/handler/admin/tool"
adminUser "github.com/perfect-panel/server/internal/handler/admin/user"
app "github.com/perfect-panel/server/internal/handler/app"
auth "github.com/perfect-panel/server/internal/handler/auth"
authOauth "github.com/perfect-panel/server/internal/handler/auth/oauth"
common "github.com/perfect-panel/server/internal/handler/common"
@ -848,12 +847,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
}
// 新增App 组的 WebSocket 测试端点
appGroupRouter := router.Group("/v1/app")
{
appGroupRouter.GET("/ws/:userid/:device_number", app.DeviceWebSocketHandler(serverCtx))
}
serverGroupRouter := router.Group("/v1/server")
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))
@ -874,10 +867,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
}
serverGroupRouter = router.Group("/v2/server")
serverGroupRouterV2 := router.Group("/v2/server")
{
// Get Server Protocol Config
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
serverGroupRouterV2.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
}
}

View File

@ -11,12 +11,21 @@ import (
"gorm.io/gorm"
)
// BindDeviceLogic 设备绑定逻辑处理器
// 负责处理设备与用户的绑定关系,包括新设备创建、设备重新绑定、数据迁移等核心功能
// 主要解决设备用户与邮箱用户之间的数据合并问题
type BindDeviceLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
logger.Logger // 日志记录器,用于记录操作过程和错误信息
ctx context.Context // 上下文,用于传递请求信息和控制超时
svcCtx *svc.ServiceContext // 服务上下文,包含数据库连接、配置等依赖
}
// NewBindDeviceLogic 创建设备绑定逻辑处理器实例
// 参数:
// - ctx: 请求上下文,用于传递请求信息和控制超时
// - svcCtx: 服务上下文,包含数据库连接、配置等依赖
// 返回:
// - *BindDeviceLogic: 设备绑定逻辑处理器实例
func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic {
return &BindDeviceLogic{
Logger: logger.WithContext(ctx),
@ -25,27 +34,47 @@ func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDe
}
}
// BindDeviceToUser binds a device to a user
// If the device is already bound to another user, it will disable that user and bind the device to the current user
// BindDeviceToUser 将设备绑定到用户
// 这是设备绑定的核心入口函数,处理三种主要场景:
// 1. 新设备绑定:设备不存在时,创建新的设备记录并绑定到当前用户
// 2. 设备已绑定当前用户更新设备的IP和UserAgent信息
// 3. 设备已绑定其他用户:执行设备重新绑定,可能涉及数据迁移
//
// 参数:
// - identifier: 设备唯一标识符如设备ID、MAC地址等
// - ip: 设备当前IP地址
// - userAgent: 设备的User-Agent信息
// - currentUserId: 当前要绑定的用户ID
//
// 返回:
// - error: 绑定过程中的错误nil表示成功
//
// 注意:如果设备已绑定其他用户且该用户为"纯设备用户"(无其他认证方式),
// 将触发完整的数据迁移并禁用原用户
func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error {
// 检查设备标识符是否为空
if identifier == "" {
// No device identifier provided, skip binding
// 没有提供设备标识符,跳过绑定过程
return nil
}
// 记录设备绑定开始的日志
l.Infow("binding device to user",
logger.Field("identifier", identifier),
logger.Field("user_id", currentUserId),
logger.Field("ip", ip),
)
// Check if device exists
// 第一步:查询设备是否已存在
// 通过设备标识符查找现有的设备记录
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Device not found, create new device record
// 设备不存在,创建新设备记录并绑定到当前用户
// 这是场景1新设备绑定
return l.createDeviceForUser(identifier, ip, userAgent, currentUserId)
}
// 数据库查询出错,记录错误并返回
l.Errorw("failed to query device",
logger.Field("identifier", identifier),
logger.Field("error", err.Error()),
@ -53,9 +82,10 @@ func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, cur
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error())
}
// Device exists, check if it's bound to current user
// 第二步:检查设备是否已绑定到当前用户
if deviceInfo.UserId == currentUserId {
// Already bound to current user, just update IP and UserAgent
// 设备已绑定到当前用户只需更新IP和UserAgent
// 这是场景2设备已绑定当前用户
l.Infow("device already bound to current user, updating info",
logger.Field("identifier", identifier),
logger.Field("user_id", currentUserId),
@ -72,29 +102,49 @@ func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, cur
return nil
}
// Device is bound to another user, need to disable old user and rebind
// 第三步:设备绑定到其他用户,需要重新绑定
// 这是场景3设备已绑定其他用户需要执行重新绑定逻辑
l.Infow("device bound to another user, rebinding",
logger.Field("identifier", identifier),
logger.Field("old_user_id", deviceInfo.UserId),
logger.Field("new_user_id", currentUserId),
)
// 调用重新绑定函数,可能涉及数据迁移
return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId)
}
// createDeviceForUser 为用户创建新的设备记录
// 当设备标识符在系统中不存在时调用此函数,执行完整的设备创建流程
// 包括创建设备认证方法记录和设备信息记录,确保数据一致性
//
// 参数:
// - identifier: 设备唯一标识符
// - ip: 设备IP地址
// - userAgent: 设备User-Agent信息
// - userId: 要绑定的用户ID
//
// 返回:
// - error: 创建过程中的错误nil表示成功
//
// 注意:此函数使用数据库事务确保认证方法和设备记录的原子性创建
func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error {
// 记录开始创建设备的日志
l.Infow("creating new device for user",
logger.Field("identifier", identifier),
logger.Field("user_id", userId),
)
// 使用数据库事务确保数据一致性
// 如果任何一步失败,整个操作都会回滚
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
// Create device auth method
// 第一步:创建设备认证方法记录
// 在auth_methods表中记录该设备的认证信息
authMethod := &user.AuthMethods{
UserId: userId,
AuthType: "device",
AuthIdentifier: identifier,
Verified: true,
UserId: userId, // 关联的用户ID
AuthType: "device", // 认证类型为设备认证
AuthIdentifier: identifier, // 设备标识符
Verified: true, // 设备认证默认为已验证状态
}
if err := db.Create(authMethod).Error; err != nil {
l.Errorw("failed to create device auth method",
@ -105,14 +155,15 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
}
// Create device record
// 第二步:创建设备信息记录
// 在device表中记录设备的详细信息
deviceInfo := &user.Device{
Ip: ip,
UserId: userId,
UserAgent: userAgent,
Identifier: identifier,
Enabled: true,
Online: false,
Ip: ip, // 设备IP地址
UserId: userId, // 关联的用户ID
UserAgent: userAgent, // 设备User-Agent信息
Identifier: identifier, // 设备唯一标识符
Enabled: true, // 设备默认启用状态
Online: false, // 设备默认离线状态
}
if err := db.Create(deviceInfo).Error; err != nil {
l.Errorw("failed to create device",
@ -126,6 +177,7 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
return nil
})
// 检查事务执行结果
if err != nil {
l.Errorw("device creation failed",
logger.Field("identifier", identifier),
@ -135,6 +187,7 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
return err
}
// 记录设备创建成功的日志
l.Infow("device created successfully",
logger.Field("identifier", identifier),
logger.Field("user_id", userId),
@ -143,11 +196,35 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
return nil
}
// rebindDeviceToNewUser 将设备重新绑定到新用户
// 这是设备绑定合并逻辑的核心函数,处理设备从一个用户转移到另一个用户的复杂场景
// 主要解决"设备用户"与"邮箱用户"之间的数据合并问题
//
// 核心判断逻辑:
// 1. 如果原用户是"纯设备用户"(只有设备认证,无邮箱等其他认证方式):
// - 执行完整数据迁移(订单、订阅、余额、赠送金额)
// - 禁用原用户账户
// 2. 如果原用户有其他认证方式(如邮箱、手机等):
// - 只转移设备绑定关系
// - 保留原用户账户和数据
//
// 参数:
// - deviceInfo: 现有的设备信息记录
// - ip: 设备新的IP地址
// - userAgent: 设备新的User-Agent信息
// - newUserId: 要绑定到的新用户ID
//
// 返回:
// - error: 重新绑定过程中的错误nil表示成功
//
// 注意:整个过程在数据库事务中执行,确保数据一致性
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
oldUserId := deviceInfo.UserId
// 使用数据库事务确保所有操作的原子性
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
// Check if old user has other auth methods besides device
// 第一步:查询原用户的所有认证方式
// 用于判断原用户是否为"纯设备用户"
var authMethods []user.AuthMethods
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
l.Errorw("failed to query auth methods for old user",
@ -157,7 +234,8 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
}
// Count non-device auth methods
// 第二步:统计非设备认证方式的数量
// 如果只有设备认证,说明是"纯设备用户"
nonDeviceAuthCount := 0
for _, auth := range authMethods {
if auth.AuthType != "device" {
@ -165,9 +243,11 @@ 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
// 原用户是"纯设备用户",执行完整的数据迁移和用户禁用
// 3.1 先执行数据迁移(订单、订阅、余额等)
if err := l.migrateUserData(db, oldUserId, newUserId); err != nil {
l.Errorw("failed to migrate user data",
logger.Field("old_user_id", oldUserId),
@ -177,6 +257,8 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "migrate user data failed: %v", err)
}
// 3.2 禁用原用户账户
// 使用指针确保布尔值正确传递给GORM
falseVal := false
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
l.Errorw("failed to disable old user",
@ -191,13 +273,15 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
logger.Field("new_user_id", newUserId),
)
} else {
// 原用户有其他认证方式,只转移设备绑定,保留原用户
l.Infow("old user has other auth methods, not disabling",
logger.Field("old_user_id", oldUserId),
logger.Field("non_device_auth_count", nonDeviceAuthCount),
)
}
// Update device auth method to new user
// 第四步:更新设备认证方法的用户归属
// 将auth_methods表中的设备认证记录转移到新用户
if err := db.Model(&user.AuthMethods{}).
Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).
Update("user_id", newUserId).Error; err != nil {
@ -208,11 +292,12 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
}
// Update device record
deviceInfo.UserId = newUserId
deviceInfo.Ip = ip
deviceInfo.UserAgent = userAgent
deviceInfo.Enabled = true
// 第五步:更新设备记录信息
// 更新device表中的设备信息包括用户ID、IP、UserAgent等
deviceInfo.UserId = newUserId // 更新设备归属用户
deviceInfo.Ip = ip // 更新设备IP
deviceInfo.UserAgent = userAgent // 更新设备UserAgent
deviceInfo.Enabled = true // 确保设备处于启用状态
if err := db.Save(deviceInfo).Error; err != nil {
l.Errorw("failed to update device",
@ -225,6 +310,7 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return nil
})
// 检查事务执行结果
if err != nil {
l.Errorw("device rebinding failed",
logger.Field("identifier", deviceInfo.Identifier),
@ -235,6 +321,7 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return err
}
// 记录设备重新绑定成功的日志
l.Infow("device rebound successfully",
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("old_user_id", oldUserId),
@ -244,15 +331,38 @@ 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
// migrateUserData 执行用户数据迁移
// 当"纯设备用户"需要合并到"邮箱用户"时,此函数负责迁移所有相关的用户数据
// 确保用户的历史数据(订单、订阅、余额等)不会丢失
//
// 迁移的数据类型包括:
// 1. 订单数据:将所有历史订单从原用户转移到新用户
// 2. 订阅数据:将所有订阅记录从原用户转移到新用户(注意:存在重复套餐问题)
// 3. 用户余额:将原用户的账户余额累加到新用户
// 4. 赠送金额:将原用户的赠送金额累加到新用户
//
// 参数:
// - db: 数据库事务对象,确保所有操作在同一事务中执行
// - oldUserId: 原用户ID数据来源
// - newUserId: 新用户ID数据目标
//
// 返回:
// - error: 迁移过程中的错误nil表示成功
//
// 已知问题:
// - 订阅迁移采用简单的user_id更新可能导致重复套餐问题
// - 缺少智能合并策略来处理相同套餐的订阅记录
// - 流量使用统计在订阅级别跟踪,不需要单独迁移
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
// 第一步:迁移订单数据
// 将order表中所有属于原用户的订单转移到新用户
// 使用表名直接操作以提高性能
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),
@ -262,7 +372,10 @@ func (l *BindDeviceLogic) migrateUserData(db *gorm.DB, oldUserId, newUserId int6
return err
}
// Migrate user subscriptions - using Subscribe model
// 第二步:迁移用户订阅数据
// 将subscribe表中所有属于原用户的订阅转移到新用户
// 注意这里存在重复套餐问题简单的user_id更新可能导致用户拥有多个相同的套餐订阅
// TODO: 实现智能合并策略,合并相同套餐的订阅记录
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),
@ -272,7 +385,8 @@ func (l *BindDeviceLogic) migrateUserData(db *gorm.DB, oldUserId, newUserId int6
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",
@ -282,11 +396,13 @@ func (l *BindDeviceLogic) migrateUserData(db *gorm.DB, oldUserId, newUserId int6
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),
"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),
@ -299,10 +415,12 @@ func (l *BindDeviceLogic) migrateUserData(db *gorm.DB, oldUserId, newUserId int6
}
}
// 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
// 注意事项说明:
// 1. 认证方法auth_methods不在此处迁移由调用方单独处理
// 2. 流量使用统计Upload/Download在订阅级别跟踪随订阅一起迁移
// 3. 其他用户相关表如需迁移,可在此处添加
// 记录数据迁移完成的日志
l.Infow("user data migration completed",
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),

View File

@ -61,8 +61,17 @@ func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailW
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId error")
}
// 如果当前用户已经绑定了邮箱,不允许重复绑定
// 如果当前用户已经绑定了邮箱,检查是否是同一个邮箱
if currentEmailMethod.Id > 0 {
// 如果绑定的是同一个邮箱,直接返回成功
if currentEmailMethod.AuthIdentifier == req.Email {
l.Infow("user is binding the same email that is already bound",
logger.Field("user_id", currentUser.Id),
logger.Field("email", req.Email),
)
return nil
}
// 如果是不同的邮箱,不允许重复绑定
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "current user already has email bound")
}
@ -76,13 +85,13 @@ func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailW
if existingEmailMethod.Id > 0 && existingEmailMethod.UserId != currentUser.Id {
// 调用设备绑定逻辑,这会触发数据迁移
bindLogic := auth.NewBindDeviceLogic(l.ctx, l.svcCtx)
// 获取当前用户的设备标识符
deviceMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "device", currentUser.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId device error")
}
if deviceMethod.Id == 0 {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "current user has no device identifier")
}
@ -111,7 +120,7 @@ func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailW
AuthIdentifier: req.Email,
Verified: true, // 通过密码验证,直接设为已验证
}
if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, emailMethod); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error")
}
@ -123,4 +132,4 @@ func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailW
}
return nil
}
}

BIN
ppanel-server Executable file

Binary file not shown.

14
script/aaa.txt Normal file
View File

@ -0,0 +1,14 @@
已经绑定过的设备,删除重装,这个时候绑定邮箱用哪个接口
删除重新装: 设备号不变, 现在拿着设备登录;实际上还是老的 邮箱+设备
1. 该设备已经绑定过邮箱了; 没办法进行绑定 需要解绑后再次绑定
这个场景 如果走 绑定邮箱 需要先解绑 再调用 bind_email_with_password
如果直接走登录: /v1/auth/login;
新设备,未绑定邮箱,用哪个接口
bind_email_with_password

View File

@ -173,7 +173,7 @@ func bindEmailWithPassword(serverURL, secret, token, email, password, userAgent
func main() {
secret := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx"
serverURL := "http://localhost:8080"
identifier := "AP4A.241205.014"
identifier := "AP4A.241205.013"
userAgent := "ppanel-go-test/1.0"
plain := map[string]interface{}{
@ -237,3 +237,33 @@ func main() {
fmt.Println("跳过绑定:缺少 token、EMAIL 或 PASSWORD")
}
}
/*
测试 1:
设备A AP4A.241205.017 3节点
邮箱B client05@gmail.com 无套餐
设备A 绑定 邮箱B
结果
设备A 设备A 没有套餐了
邮箱B 套餐到了 邮箱B的体系下 设备也和邮箱绑定上了
以邮箱为主
测试 2:
设备A AP4A.241205.018 无套餐
邮箱B client06@gmail.com 3节点
设备A 绑定 邮箱B
结果
设备A 设备A 没有套餐了
邮箱B 原有套餐还存在 设备也和邮箱绑定上了
以邮箱为主
测试 3:
设备A AP4A.241205.019 3节点 2025/11/2 13:12:21 2025/10/23 13:12:21
邮箱B client07@gmail.com day套餐
设备A 绑定 邮箱B
结果
设备A 设备A 没有套餐了
邮箱B 原有套餐还存在 设备也和邮箱绑定上了
以邮箱为主
*/

View File

@ -13,7 +13,7 @@ import (
func main() {
// 默认配置,可用环境变量覆盖
baseURL := getenvDefault("SERVER_URL", "ws://127.0.0.1:8080")
baseURL := getenvDefault("SERVER_URL", "wss://api.hifast.biz")
userID := getenvDefault("USER_ID", "23")
deviceID := getenvDefault("DEVICE_ID", "c76463cff8512722")
auth := "dkjw6TCQTBlgreyhjN8u32gP0A6RrQ/V50vf8wjNFwFL9hgKJrOOv+ziS03GCQ/8E0fWUzjc4aCoMcVMzUN8vR7CwqR45HbtogoT9iNoElW9rgzpQNbwQ4BHK/Q25WvcgdrhfRzE19nPqUTOcN+4iY6NmeiwHEMLBTzDEeu8wGn/yjVLRMCyh5QJuQizllbrDR5LuTiNEcdSdBSx9cFZYtnJIIyi1b60BZYo4lIyRADCH6smTsLDhoZG0nJvJw3C0XCGvf0jC/4d4u40IvbzKOm1TBSK0lgOzNjvkSfS/DJibAi4l7qNTYmFlQ1wp+iW1MNllqd+OtSavZYoajoZGA=="