package authser import ( "context" "math/rand" "strconv" "time" "videoweb/app/appg" "videoweb/app/service/configser" "videoweb/pub/common/enum/consts" "videoweb/pub/common/log" "videoweb/pub/common/netutil" "videoweb/pub/common/srvdef" "videoweb/pub/common/utils" "videoweb/pub/models/usermod" "videoweb/pub/mongos" "videoweb/pub/pubdef" "videoweb/pub/redis" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" "golang.org/x/crypto/bcrypt" ) var tokener = utils.NewTokenRedis() var defaultAvatars = []string{ "v3/image/bo/ty/1016/girl1.jpeg", "v3/image/re/9y/1016/girl2.jpeg", "v3/image/5f/k4/1016/girl3.jpeg", "v3/image/ej/ss/1016/girl4.jpeg", "v3/image/yq/ys/1016/girl5.jpeg", "v3/image/9e/d3/1016/girl6.jpeg", } func randomAvatar() string { return defaultAvatars[time.Now().UnixNano()%int64(len(defaultAvatars))] } // generateUniqueInviteCode 生成 8 位唯一邀请码(含大小写字母),最多重试 10 次 func generateUniqueInviteCode(ctx context.Context) string { for i := 0; i < 10; i++ { code := utils.GetRandomLetters(8) exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.InviteCode: code}) if exist == nil { return code } log.Warn("authser:generateUniqueInviteCode collision, retrying", log.Any("code", code)) } // 极小概率走到这里:用更长的随机串兜底 return utils.GetRandomLetters(12) } // LoginResp 登录/注册成功后的响应数据 type LoginResp struct { Token string `json:"token"` UserId int64 `json:"userId"` AccountNo string `json:"accountNo"` NickName string `json:"nickName"` Status pubdef.UserStatus `json:"status"` Avatar string `json:"avatar"` // 头像 InviteCode string `json:"inviteCode"` // 邀请码 LoginType pubdef.LoginType `json:"loginType"` // 登录方式 0=设备 3=账号 IsRegister bool `json:"isRegister"` // 是否已完成账号注册 IsNewUser bool `json:"isNewUser"` // 是否本次请求新创建的用户 Recharge int64 `json:"recharge"` // 当前金币个数 } // UserInfoResp 当前登录用户信息 type UserInfoResp struct { UserId int64 `json:"userId"` AccountNo string `json:"accountNo"` NickName string `json:"nickName"` Status pubdef.UserStatus `json:"status"` Avatar string `json:"avatar"` InviteCode string `json:"inviteCode"` LoginType pubdef.LoginType `json:"loginType"` IsRegister bool `json:"isRegister"` Recharge int64 `json:"recharge"` } // UAInfo 从 UA 中间件提取的设备/系统信息 type UAInfo struct { Device string // 设备型号(DevType,如 "iPhone") SysType pubdef.SysType // 系统类型(ios / android / h5 等) AppVer string // App 版本号 IP string // 客户端 IP } // GuestLoginReq 游客登录请求 type GuestLoginReq struct { DevID string // 设备 ID(首选) Mac string // 设备 MAC 地址(备用) DeviceId string // 设备 ID 第二版本(备用) UA UAInfo // 设备/系统信息 } // hashPassword 使用 bcrypt 对明文密码进行哈希 func hashPassword(plain string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil } // checkPassword 验证明文密码与 bcrypt 哈希是否匹配 func checkPassword(plain, hash string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil } // generateAndSaveToken 生成 JWT 并存入 Redis func generateAndSaveToken(userId int64) (string, *srvdef.SrvErr) { token, err := utils.GenerateUserToken(utils.UserClaims{ UserID: userId, Expire: consts.UserJwtTokenExpire, }, appg.Conf.JwtKey) if err != nil { return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "") } if err = tokener.Save(userId, token); err != nil { return "", srvdef.New(srvdef.StoreLevel, "token存储失败", "") } return token, nil } // GuestLogin 游客登录:按设备 ID 查找已有用户,找不到则自动注册 // 若请求携带有效 token(currentUserId != 0),优先复用该账号 func GuestLogin(ctx context.Context, req *GuestLoginReq, currentUserId int64) (*LoginResp, *srvdef.SrvErr) { var user *usermod.User // 优先用 token 对应的已有用户 if currentUserId > 0 { u, _ := usermod.FindOneUserById(ctx, currentUserId) if u != nil { user = u } } // 按设备 ID 查找 if user == nil { user, _ = findUserByDevice(ctx, req.DevID, req.Mac, req.DeviceId) } isNewUser := false if user == nil { // 未找到 → 自动注册游客 var serr *srvdef.SrvErr user, serr = registerGuest(ctx, req) if serr != nil { return nil, serr } isNewUser = true } else { // 已有用户:更新设备信息和 UA(静默,不影响主流程) updateSet := bson.M{ usermod.FN.User.LastLoginAt: time.Now(), usermod.FN.User.UpdatedAt: time.Now(), } if req.DevID != "" { updateSet[usermod.FN.User.DevID] = req.DevID } if req.Mac != "" && req.Mac != "00:00:00:00:00:00" { updateSet[usermod.FN.User.Mac] = req.Mac } if req.DeviceId != "" { updateSet[usermod.FN.User.DevV2] = req.DeviceId } if req.UA.Device != "" { updateSet[usermod.FN.User.Device] = req.UA.Device } if req.UA.SysType != "" { updateSet[usermod.FN.User.SysType] = req.UA.SysType.String() } if req.UA.AppVer != "" { updateSet[usermod.FN.User.AppVer] = req.UA.AppVer } if req.UA.IP != "" { updateSet[usermod.FN.User.LastIp] = req.UA.IP } _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": updateSet}, ) } if user.Status == pubdef.UserStatusDisable { return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") } token, serr := generateAndSaveToken(user.Id) if serr != nil { return nil, serr } cdn := configser.ImageCdnFromCtx(ctx) return &LoginResp{ Token: token, UserId: user.Id, AccountNo: user.AccountNo, NickName: user.NickName, Status: user.Status, Avatar: netutil.MergeURL(cdn, user.Avatar), InviteCode: user.InviteCode, LoginType: user.LoginType, IsRegister: user.IsRegister, IsNewUser: isNewUser, Recharge: user.Recharge, }, nil } // GetInfo 根据当前登录 token 解析出的用户 ID 查询用户信息 func GetInfo(ctx context.Context, userId int64) (*UserInfoResp, *srvdef.SrvErr) { if userId <= 0 { return nil, srvdef.New(srvdef.InvalidToken, srvdef.InvalidToken.Msg(), "") } user, serr := usermod.FindOneUserById(ctx, userId, options.FindOne().SetProjection(userInfoProjection())) if serr != nil { return nil, serr } if user == nil { return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "") } if user.Status == pubdef.UserStatusDisable { return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") } cdn := configser.ImageCdnFromCtx(ctx) return &UserInfoResp{ UserId: user.Id, AccountNo: user.AccountNo, NickName: user.NickName, Status: user.Status, Avatar: netutil.MergeURL(cdn, user.Avatar), InviteCode: user.InviteCode, LoginType: user.LoginType, IsRegister: user.IsRegister, Recharge: user.Recharge, }, nil } func userInfoProjection() bson.M { return bson.M{ usermod.FN.User.Id: 1, usermod.FN.User.AccountNo: 1, usermod.FN.User.NickName: 1, usermod.FN.User.Status: 1, usermod.FN.User.Avatar: 1, usermod.FN.User.InviteCode: 1, usermod.FN.User.LoginType: 1, usermod.FN.User.IsRegister: 1, usermod.FN.User.Recharge: 1, } } // findUserByDevice 按设备 ID 三级回退查找用户(DevID → Mac → DeviceId) func findUserByDevice(ctx context.Context, devID, mac, deviceId string) (*usermod.User, *srvdef.SrvErr) { invalidMac := "00:00:00:00:00:00" if devID != "" { u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: devID}) if u != nil { return u, nil } } if mac != "" && mac != invalidMac { u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac}) if u != nil { return u, nil } } if deviceId != "" && deviceId != invalidMac { u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId}) if u != nil { return u, nil } } return nil, nil } // registerGuest 自动创建游客用户 func registerGuest(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) { now := time.Now() user := &usermod.User{ Id: mongos.Lid.User.Get(), NickName: getName(), Avatar: randomAvatar(), InviteCode: generateUniqueInviteCode(ctx), Status: pubdef.UserStatusNormal, LoginType: pubdef.LoginTypeDevice, IsRegister: false, DevID: req.DevID, Mac: req.Mac, DevV2: req.DeviceId, Device: req.UA.Device, SysType: req.UA.SysType.String(), AppVer: req.UA.AppVer, OriVer: req.UA.AppVer, RegIp: req.UA.IP, LastIp: req.UA.IP, RegisterTime: now, CreatedAt: now, UpdatedAt: now, } if serr := usermod.InsertOneUser(ctx, user); serr != nil { return nil, serr } return user, nil } // Register 账号注册 // currentUserId != 0 时为游客升级模式:在已有游客账号上绑定账号密码 // currentUserId == 0 时为直接注册新账号 func Register(ctx context.Context, accountNo, password, agentInviteCode string, currentUserId int64, ua UAInfo) (*LoginResp, *srvdef.SrvErr) { // 检查 accountNo 是否已被其他用户占用 exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo}) if exist != nil && exist.Id != currentUserId { return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "") } hashed, err := hashPassword(password) if err != nil { return nil, srvdef.New(srvdef.Failure, "密码处理失败", "") } now := time.Now() // 游客升级:更新已有记录 if currentUserId > 0 { user, _ := usermod.FindOneUserById(ctx, currentUserId) if user == nil { return nil, srvdef.New(srvdef.ErrUserNotExsit, "用户不存在", "") } if user.IsRegister { return nil, srvdef.New(srvdef.ErrDbInputExist, "已完成账号注册,无需重复操作", "") } setDoc := bson.M{ usermod.FN.User.AccountNo: accountNo, usermod.FN.User.Password: hashed, usermod.FN.User.IsRegister: true, usermod.FN.User.LoginType: pubdef.LoginTypeAccount, usermod.FN.User.UpdatedAt: now, } if agentInviteCode != "" && user.AgentInviteCode == "" { setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode } if ua.Device != "" { setDoc[usermod.FN.User.Device] = ua.Device } if ua.SysType != "" { setDoc[usermod.FN.User.SysType] = ua.SysType.String() } if ua.AppVer != "" { setDoc[usermod.FN.User.AppVer] = ua.AppVer } if ua.IP != "" { setDoc[usermod.FN.User.LastIp] = ua.IP } if serr := usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: currentUserId}, bson.M{"$set": setDoc}, ); serr != nil { return nil, serr } // 给邀请人 +1 if agentInviteCode != "" { _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.InviteCode: agentInviteCode}, bson.M{"$inc": bson.M{usermod.FN.User.Invites: 1}, "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()}}, ) } token, serr := generateAndSaveToken(user.Id) if serr != nil { return nil, serr } cdn := configser.ImageCdnFromCtx(ctx) return &LoginResp{ Token: token, UserId: user.Id, AccountNo: accountNo, NickName: user.NickName, Status: user.Status, Avatar: netutil.MergeURL(cdn, user.Avatar), InviteCode: user.InviteCode, LoginType: pubdef.LoginTypeAccount, IsRegister: true, Recharge: user.Recharge, }, nil } // 未登录:直接注册新账号 user := &usermod.User{ Id: mongos.Lid.User.Get(), AccountNo: accountNo, Password: hashed, NickName: getName(), Avatar: randomAvatar(), InviteCode: generateUniqueInviteCode(ctx), AgentInviteCode: agentInviteCode, Status: pubdef.UserStatusNormal, IsRegister: true, LoginType: pubdef.LoginTypeAccount, Device: ua.Device, SysType: ua.SysType.String(), AppVer: ua.AppVer, OriVer: ua.AppVer, RegIp: ua.IP, LastIp: ua.IP, RegisterTime: now, CreatedAt: now, UpdatedAt: now, } if serr := usermod.InsertOneUser(ctx, user); serr != nil { return nil, serr } if agentInviteCode != "" { _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.InviteCode: agentInviteCode}, bson.M{ "$inc": bson.M{usermod.FN.User.Invites: 1}, "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()}, }, ) } token, serr := generateAndSaveToken(user.Id) if serr != nil { return nil, serr } cdn := configser.ImageCdnFromCtx(ctx) return &LoginResp{ Token: token, UserId: user.Id, AccountNo: user.AccountNo, NickName: user.NickName, Status: user.Status, Avatar: netutil.MergeURL(cdn, user.Avatar), InviteCode: user.InviteCode, LoginType: pubdef.LoginTypeAccount, IsRegister: true, }, nil } // Login 用户登录(账号均可) func Login(ctx context.Context, account, password string, ua UAInfo) (*LoginResp, *srvdef.SrvErr) { user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: account}) if user == nil { return nil, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "") } if user.Status == pubdef.UserStatusDisable { return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") } if !checkPassword(password, user.Password) { return nil, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "") } token, serr := generateAndSaveToken(user.Id) if serr != nil { return nil, serr } // 更新最后登录时间、IP 及 UA(fire-and-forget) loginSet := bson.M{ usermod.FN.User.LastLoginAt: time.Now(), usermod.FN.User.UpdatedAt: time.Now(), } if ua.IP != "" { loginSet[usermod.FN.User.LastIp] = ua.IP } if ua.Device != "" { loginSet[usermod.FN.User.Device] = ua.Device } if ua.SysType != "" { loginSet[usermod.FN.User.SysType] = ua.SysType.String() } if ua.AppVer != "" { loginSet[usermod.FN.User.AppVer] = ua.AppVer } _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": loginSet}, ) cdn := configser.ImageCdnFromCtx(ctx) return &LoginResp{ Token: token, UserId: user.Id, AccountNo: user.AccountNo, NickName: user.NickName, Status: user.Status, Avatar: netutil.MergeURL(cdn, user.Avatar), InviteCode: user.InviteCode, LoginType: user.LoginType, IsRegister: user.IsRegister, Recharge: user.Recharge, }, nil } // UpdateProfileReq 修改用户资料的参数(所有字段均为可选) type UpdateProfileReq struct { AccountNo string // 用户账号(用于定位用户,必传) Avatar *string // 新头像 URL(可选) NikeName *string // 昵称 OldPassword *string // 旧密码明文(修改密码时必传) NewPassword *string // 新密码明文(修改密码时必传) } // UpdateProfile 修改用户资料:支持修改头像、密码,或两者同时修改 func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr { user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: req.AccountNo}) if user == nil { return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "") } setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()} // 修改头像 if req.Avatar != nil && *req.Avatar != "" { setDoc[usermod.FN.User.Avatar] = *req.Avatar } if req.NikeName != nil && *req.NikeName != "" { setDoc[usermod.FN.User.NickName] = *req.NikeName } // 修改密码:两者必须同时提供 if req.OldPassword != nil || req.NewPassword != nil { if req.OldPassword == nil || req.NewPassword == nil { return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "") } if !checkPassword(*req.OldPassword, user.Password) { return srvdef.New(srvdef.AccountErrPasswordError, "旧密码错误", "") } hashed, err := hashPassword(*req.NewPassword) if err != nil { return srvdef.New(srvdef.Failure, "密码处理失败", "") } setDoc[usermod.FN.User.Password] = hashed } // 没有任何需要更新的字段 if len(setDoc) == 1 { return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "") } return usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc}, ) } func getName() string { const limit = 300 totalCnt, err := redis.SCard(redis.NameSource) if err != nil || totalCnt < limit { if len(appg.Name.FamilyNames) == 0 || len(appg.Name.AfterNames) == 0 { // fmt.Println(totalCnt, err, appg.Name.FamilyNames) return "游客" + strconv.FormatInt(time.Now().Unix(), 10) } for i := 0; i < limit/100; i++ { go func() { var names []string for j := 0; j < limit/3; j++ { length := 2 + rand.Intn(2) if length == 2 { name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] + appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))] names = append(names, name) } if length == 3 { name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] + appg.Name.MiddleNames[utils.RandInt(0, len(appg.Name.MiddleNames))] + appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))] names = append(names, name) } } _ = redis.SAdd(redis.NameSource, names) }() time.Sleep(100 * time.Millisecond) } _ = redis.Expire(redis.NameSource, redis.NameSourceExpire) } name, err := redis.SPop(redis.NameSource) if name == "" || err != nil { return "游客" + strconv.FormatInt(time.Now().Unix(), 10) } return name }