package authser import ( "context" "strings" "time" "videoweb/app/appg" "videoweb/pub/common/enum/consts" "videoweb/pub/common/log" "videoweb/pub/common/srvdef" "videoweb/pub/common/useragent" "videoweb/pub/common/utils" "videoweb/pub/models/usermod" "videoweb/pub/mongos" "videoweb/pub/pubdef" "go.mongodb.org/mongo-driver/bson" "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 := strings.ToUpper(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 strings.ToUpper(utils.GetRandomLetters(12)) } // LoginResp 登录/注册成功后的响应数据 type LoginResp struct { Token string `json:"token"` UserId int64 `json:"userId"` AccountNo string `json:"accountNo"` Email string `json:"email"` NickName string `json:"nickName"` Introduction string `json:"introduction"` // 简介 VipType pubdef.VipType `json:"vipType"` CurrentVipExpire time.Time `json:"currentVipExpire"` // VIP 过期时间,客户端需结合 vipType 判断是否真正有效 ShowCardId int64 `json:"showCardId"` // 最新会员卡id ShowCardName string `json:"showCardName"` // 最新会员卡名称 Status pubdef.UserStatus `json:"status"` Avatar string `json:"avatar"` // 头像 InviteCode string `json:"inviteCode"` // 邀请码 Type pubdef.UserType `json:"type"` // 身份类型:0游客 1普通用户 LoginType pubdef.LoginType `json:"loginType"` // 登陆方式 IsRegister bool `json:"isRegister"` // 是否已注册 IsNewUser bool `json:"isNewUser"` // 本次是否新建游客 PrivacyMode bool `json:"privacyMode"` // 隐私模式 Score int64 `json:"score"` // 积分 Recharge int64 `json:"recharge"` // 金币余额 } // 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 } // generateCrawlerToken 签发爬虫观看 JWT,不写入 Redis,避免占用真实用户槽位。 func generateCrawlerToken() (string, *srvdef.SrvErr) { token, err := utils.GenerateUserToken(utils.UserClaims{ UserID: pubdef.CrawlerUserId, Expire: consts.UserJwtTokenExpire, }, appg.Conf.JwtKey) if err != nil { return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "") } return token, nil } // CrawlerLoginResp 爬虫虚拟游客资料,不对应任何数据库用户。 func CrawlerLoginResp(token string) *LoginResp { return &LoginResp{ Token: token, UserId: pubdef.CrawlerUserId, NickName: "游客", Status: pubdef.UserStatusNormal, Type: pubdef.UserTypeGuest, LoginType: pubdef.LoginTypeDevice, IsRegister: false, IsNewUser: false, } } // Register 用户注册 // guestUserId:请求头 Authorization 解析出的用户ID;若为游客则升级该用户,否则新建 // agentInviteCode:邀请人的邀请码,可为空 func Register(ctx context.Context, guestUserId int64, accountNo, email, password, agentInviteCode string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) { accountNo = strings.TrimSpace(accountNo) email = strings.TrimSpace(email) password = strings.TrimSpace(password) // 检查账号是否已存在 exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo}) if exist != nil { return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "") } // 检查邮箱是否已存在 exist, _ = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Email: email}) if exist != nil { return nil, srvdef.New(srvdef.ErrDbInputExist, "邮箱已被注册", "") } hashed, err := hashPassword(password) if err != nil { return nil, srvdef.New(srvdef.Failure, "密码处理失败", "") } now := time.Now() // 请求头带游客 token:升级当前游客为普通用户 if guestUserId > 0 { guest, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: guestUserId}) if serr == nil && isGuestUser(guest) { setDoc := bson.M{ usermod.FN.User.AccountNo: accountNo, usermod.FN.User.Email: email, usermod.FN.User.Password: hashed, usermod.FN.User.Type: pubdef.UserTypeNormal, usermod.FN.User.IsRegister: true, usermod.FN.User.LoginType: pubdef.LoginTypeAccount, usermod.FN.User.RegisterTime: now, usermod.FN.User.LastLoginAt: now, usermod.FN.User.UpdatedAt: now, usermod.FN.User.LastIp: ip, } if agentInviteCode != "" && guest.AgentInviteCode == "" { setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode setDoc[usermod.FN.User.IsDirect] = false guest.IsDirect = false } if v := strings.TrimSpace(ua.DevID); v != "" { setDoc[usermod.FN.User.DevID] = v } 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, guest, ua) applyChannelIfMissing(setDoc, guest, channel) if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: guest.Id}, bson.M{"$set": setDoc}); serr != nil { return nil, serr } if agentInviteCode != "" && guest.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: now}, }, ) } guest.AccountNo = accountNo guest.Email = email guest.Type = pubdef.UserTypeNormal guest.IsRegister = true guest.LoginType = pubdef.LoginTypeAccount guest.RegisterTime = now token, serr := generateAndSaveToken(guest.Id) if serr != nil { return nil, serr } return toLoginResp(ctx, guest, token, true), nil } } user := &usermod.User{ Id: mongos.Lid.User.Get(), AccountNo: accountNo, Email: email, Password: hashed, NickName: appg.GenerateNickname(), Avatar: randomAvatar(), InviteCode: generateUniqueInviteCode(ctx), AgentInviteCode: agentInviteCode, Type: pubdef.UserTypeNormal, Status: pubdef.UserStatusNormal, IsRegister: true, LoginType: pubdef.LoginTypeAccount, 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, DistrictCode: normalizeChannel(channel), IsDirect: resolveUserDistrict(channel, agentInviteCode, ip, ua.SysType).IsDirect, LastLoginAt: now, RegisterTime: now, CreatedAt: now, UpdatedAt: now, } if serr := usermod.InsertOneUser(ctx, user); serr != nil { return nil, serr } // 若填写了邀请码,给对应用户的 invites +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 } return toLoginResp(ctx, user, token, false), nil } // Login 用户登录(账号或邮箱均可) func Login(ctx context.Context, account, password string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) { // 账号或邮箱均可登录 user, _ := usermod.FindOneUser(ctx, bson.M{ "$or": []bson.M{ {usermod.FN.User.AccountNo: account}, {usermod.FN.User.Email: 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 } now := time.Now() setDoc := bson.M{ usermod.FN.User.LastLoginAt: now, usermod.FN.User.UpdatedAt: now, } if ip != "" { setDoc[usermod.FN.User.LastIp] = ip } if v := strings.TrimSpace(ua.DevID); v != "" { setDoc[usermod.FN.User.DevID] = v } 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 } // 已有账号的历史用户统一修正为普通用户 if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal { setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal setDoc[usermod.FN.User.IsRegister] = true user.Type = pubdef.UserTypeNormal user.IsRegister = true } mergeUAExtFieldsToSet(setDoc, user, ua) applyChannelIfMissing(setDoc, user, channel) _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc}, ) return toLoginResp(ctx, user, token, true), nil } // GetInfo 获取当前登录用户的最新信息(不刷新 token) func GetInfo(ctx context.Context, userId int64, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) { if pubdef.IsCrawlerUser(userId) { return CrawlerLoginResp(""), nil } user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId}) if user == nil { return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "") } now := time.Now() setDoc := bson.M{} if v := strings.TrimSpace(ua.DevID); v != "" && v != user.DevID { setDoc[usermod.FN.User.DevID] = v } if v := strings.TrimSpace(ua.DevType); v != "" && v != user.Device { setDoc[usermod.FN.User.Device] = v } if v := strings.TrimSpace(ua.SysType); v != "" && v != user.SysType { setDoc[usermod.FN.User.SysType] = v } if v := strings.TrimSpace(ua.Ver); v != "" && v != user.AppVer { setDoc[usermod.FN.User.AppVer] = v } if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal { setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal setDoc[usermod.FN.User.IsRegister] = true user.Type = pubdef.UserTypeNormal user.IsRegister = true } mergeUAExtFieldsToSet(setDoc, user, ua) if len(setDoc) > 0 { setDoc[usermod.FN.User.UpdatedAt] = now _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc}) } return toLoginResp(ctx, user, "", true), nil } func mergeUAExtFieldsToSet(setDoc bson.M, user *usermod.User, ua useragent.UA) { if user == nil { return } if v := strings.TrimSpace(ua.SessionID); v != "" && v != user.SessionID { setDoc[usermod.FN.User.SessionID] = v } if v := strings.TrimSpace(ua.DeviceBrand); v != "" && v != user.DeviceBrand { setDoc[usermod.FN.User.DeviceBrand] = v } if v := strings.TrimSpace(ua.DeviceModel); v != "" && v != user.DeviceModel { setDoc[usermod.FN.User.DeviceModel] = v } if v := strings.TrimSpace(ua.SystemName); v != "" && v != user.SystemName { setDoc[usermod.FN.User.SystemName] = v } if v := strings.TrimSpace(ua.SystemVersion); v != "" && v != user.SystemVersion { setDoc[usermod.FN.User.SystemVersion] = v } } // UpdateProfileReq 修改用户资料的参数(所有字段均为可选) type UpdateProfileReq struct { UserId int64 // 用户ID(从 token 中获取) NickName *string // 新昵称(可选) Avatar *string // 新头像 URL(可选) Introduction *string // 简介(可选) PrivacyMode *bool // 隐私模式:true-开启,false-关闭(可选) OldPassword *string // 旧密码明文(修改密码时必传) NewPassword *string // 新密码明文(修改密码时必传) } // UpdateProfile 修改用户资料:支持修改头像、简介、密码,或组合修改 func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr { user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId}) if user == nil { return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "") } setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()} if req.NickName != nil && *req.NickName != "" { setDoc[usermod.FN.User.NickName] = *req.NickName } if req.Avatar != nil && *req.Avatar != "" { setDoc[usermod.FN.User.Avatar] = *req.Avatar } if req.Introduction != nil { setDoc[usermod.FN.User.Introduction] = strings.TrimSpace(*req.Introduction) } if req.PrivacyMode != nil { setDoc[usermod.FN.User.PrivacyMode] = *req.PrivacyMode } // 修改密码:两者必须同时提供 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}, ) } // ResetPassword 通过账号+邮箱双重校验后重置密码 func ResetPassword(ctx context.Context, accountNo, email, newPassword string) *srvdef.SrvErr { user, _ := usermod.FindOneUser(ctx, bson.M{ usermod.FN.User.AccountNo: accountNo, usermod.FN.User.Email: email, }) if user == nil { return srvdef.New(srvdef.ErrUserNotExsit, "账号或邮箱不匹配", "") } hashed, err := hashPassword(newPassword) if err != nil { return srvdef.New(srvdef.Failure, "密码处理失败", "") } return usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": bson.M{ usermod.FN.User.Password: hashed, usermod.FN.User.UpdatedAt: time.Now(), }}, ) }