package authser import ( "context" "fmt" "strconv" "strings" "time" "hkdj-web-server/app/appg" "hkdj-web-server/app/service/configser" "hkdj-web-server/pub/common/enum/redisconst" "hkdj-web-server/pub/common/log" "hkdj-web-server/pub/common/netutil" "hkdj-web-server/pub/common/srvdef" "hkdj-web-server/pub/common/useragent" "hkdj-web-server/pub/models/usermod" "hkdj-web-server/pub/mongos" "hkdj-web-server/pub/pubdef" "hkdj-web-server/pub/redis" "go.mongodb.org/mongo-driver/bson" ) const ( deviceUserLockRetry = 10 deviceUserLockInterval = 100 * time.Millisecond ) // ResolveDeviceUser 按设备ID识别用户:先查缓存,再查库,都没有则自动创建 func ResolveDeviceUser(ctx context.Context, ua useragent.UA, ip, channel string) (int64, *srvdef.SrvErr) { devID := strings.TrimSpace(ua.DevID) if devID == "" { return 0, srvdef.New(srvdef.ErrParamError, "devID不能为空", "") } ua.DevID = devID cacheKey := redisconst.AppDeviceUserKey(devID) if v, err := redis.Get(cacheKey); err == nil && v != nil { if userId, perr := strconv.ParseInt(*v, 10, 64); perr == nil && userId > 0 { return userId, nil } } user, serr := findOrCreateDeviceUser(ctx, ua, ip, channel) if serr != nil { return 0, serr } if user.Status == pubdef.UserStatusDisable { return 0, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") } if err := redis.Set(cacheKey, user.Id, redisconst.AppDeviceUserExpire); err != nil { log.Warn("authser:ResolveDeviceUser cache set failed", log.Any("devID", devID), log.E(err)) } return user.Id, nil } // findOrCreateDeviceUser 按设备ID查找用户,不存在时加锁创建,避免同一设备并发请求建出多个用户 func findOrCreateDeviceUser(ctx context.Context, ua useragent.UA, ip, channel string) (*usermod.User, *srvdef.SrvErr) { lockKey := redisconst.AppDeviceUserLockKey(ua.DevID) for i := 0; i < deviceUserLockRetry; i++ { user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: ua.DevID}) if serr == nil { touchDeviceUser(ctx, user, ua, ip, channel) return user, nil } if serr.Code != srvdef.ErrUserNotExsit { return nil, serr } ok, err := redis.Setnx_NewOK(lockKey, 1, redisconst.AppDeviceUserLockExpire) if err != nil { return nil, srvdef.New(srvdef.StoreLevel, "用户创建失败", "") } if ok { defer func() { _ = redis.Del(lockKey) }() return createGuestUser(ctx, ua, ip, channel) } time.Sleep(deviceUserLockInterval) } return nil, srvdef.New(srvdef.StoreLevel, "用户创建失败", "") } // touchDeviceUser 更新最近访问信息(缓存未命中时执行,频率受缓存时长限制) func touchDeviceUser(ctx context.Context, user *usermod.User, ua useragent.UA, ip, channel string) { now := time.Now() setDoc := bson.M{ usermod.FN.User.LastLoginAt: now, usermod.FN.User.UpdatedAt: now, usermod.FN.User.LastIp: ip, usermod.FN.User.LoginType: pubdef.LoginTypeDevice, } if v := strings.TrimSpace(ua.DevType); v != "" { setDoc[usermod.FN.User.Device] = v } if v := strings.TrimSpace(ua.SysType); v != "" { setDoc[usermod.FN.User.SysType] = v } if v := strings.TrimSpace(ua.Ver); v != "" { setDoc[usermod.FN.User.AppVer] = v } mergeUAExtFieldsToSet(setDoc, user, ua) applyChannelIfMissing(setDoc, user, channel) _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc}) } func createGuestUser(ctx context.Context, ua useragent.UA, ip, channel string) (*usermod.User, *srvdef.SrvErr) { now := time.Now() uid := mongos.Lid.User.Get() districtInfo := resolveUserDistrict(channel, "", ip, ua.SysType) user := &usermod.User{ Id: uid, Type: pubdef.UserTypeGuest, Status: pubdef.UserStatusNormal, IsRegister: false, LoginType: pubdef.LoginTypeDevice, NickName: fmt.Sprintf("游客%d", uid), Avatar: randomAvatar(), InviteCode: generateUniqueInviteCode(ctx), DistrictCode: normalizeChannel(channel), IsDirect: districtInfo.IsDirect, RegIp: ip, LastIp: ip, DevID: ua.DevID, Device: ua.DevType, SysType: ua.SysType, AppVer: ua.Ver, OriVer: ua.Ver, SessionID: ua.SessionID, DeviceBrand: ua.DeviceBrand, DeviceModel: ua.DeviceModel, SystemName: ua.SystemName, SystemVersion: ua.SystemVersion, LastLoginAt: now, CreatedAt: now, UpdatedAt: now, } if user.NickName == "" { user.NickName = appg.GenerateNickname() } if serr := usermod.InsertOneUser(ctx, user); serr != nil { return nil, serr } return user, nil } func toLoginResp(ctx context.Context, user *usermod.User, withCDN bool) *LoginResp { avatar := user.Avatar if withCDN { cdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx)) avatar = netutil.MergeURL(cdn, user.Avatar) } return &LoginResp{ UserId: user.Id, AccountNo: user.AccountNo, Email: user.Email, NickName: user.NickName, Avatar: avatar, Introduction: user.Introduction, DevID: user.DevID, Mac: user.Mac, DeviceId: user.DevV2, InviteCode: user.InviteCode, AgentInviteCode: user.AgentInviteCode, Invites: user.Invites, Status: user.Status, Type: user.Type, LoginType: user.LoginType, IsRegister: user.IsRegister, PrivacyMode: user.PrivacyMode, Score: user.Score, Recharge: user.Recharge, } } func normalizeChannel(channel string) string { return strings.TrimSpace(channel) } func hasDistrictCode(code string) bool { return strings.TrimSpace(code) != "" } func applyChannelIfMissing(setDoc bson.M, user *usermod.User, channel string) { if user == nil || hasDistrictCode(user.DistrictCode) { return } code := normalizeChannel(channel) if code == "" { return } setDoc[usermod.FN.User.DistrictCode] = code districtInfo := resolveUserDistrict(channel, user.AgentInviteCode, user.LastIp, user.SysType) setDoc[usermod.FN.User.IsDirect] = districtInfo.IsDirect }