package auth import ( "context" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" invitedomain "hyapp/services/user-service/internal/domain/invite" userdomain "hyapp/services/user-service/internal/domain/user" ) // QuickCreateAccount 创建一个已完成资料且已设置密码的账号。 // 普通联调账号会立即返回登录态;全站机器人只返回 uid/display_id,不创建 session,也不能通过后续接口登录。 func (s *Service) QuickCreateAccount(ctx context.Context, password string, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, error) { meta.AppCode = appcode.Normalize(meta.AppCode) registration.AppCode = meta.AppCode registration = normalizeThirdPartyRegistration(registration, meta) meta.Platform = registration.Platform meta.Language = registration.Language meta.Timezone = registration.Timezone password = strings.TrimSpace(password) if password == "" { return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "password is required") } if registration.DeviceID == "" || registration.Platform == "" { // refresh session 必须绑定设备,平台也会进入注册来源和风控审计。 return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "device_id and platform are required") } if err := validateThirdPartyRegistrationRequiredForCreate(registration); err != nil { // 快捷账号直接跳过 onboarding,因此必须一次性写齐客户端准入所需的最小资料。 return authdomain.Token{}, err } if err := validateThirdPartyRegistration(registration); err != nil { return authdomain.Token{}, err } if s.authRepository == nil { return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured") } maxAccountsPerDevice, err := s.ensureDeviceCanCreatePasswordAccount(ctx, registration, meta) if err != nil { return authdomain.Token{}, err } registration, err = s.resolveRegistrationCountry(ctx, registration) if err != nil { return authdomain.Token{}, err } passwordHash, err := hashPassword(password) if err != nil { return authdomain.Token{}, err } isGameRobot := userdomain.IsGameRobotRegisterSource(registration.Source) var lastErr error for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ { user, displayIdentity := s.newUserWithIdentity(attempt) user = applyThirdPartyRegistration(user, registration) user.ProfileCompleted = true user.ProfileCompletedAtMs = user.CreatedAtMs user.OnboardingStatus = userdomain.OnboardingStatusCompleted displayUserID, err := s.authRepository.AllocateDisplayUserID(ctx, registration.AppCode) if err != nil { return authdomain.Token{}, err } user.DefaultDisplayUserID = displayUserID user.CurrentDisplayUserID = displayUserID displayIdentity.AppCode = registration.AppCode displayIdentity.DisplayUserID = displayUserID displayIdentity.DefaultDisplayUserID = displayUserID if !userdomain.ValidDisplayUserID(displayIdentity.DisplayUserID) { // 分配器或历史种子异常时不写库,直接尝试下一个候选。 continue } var session authdomain.Session var refreshToken string if !isGameRobot { var err error session, refreshToken, err = s.newSession(registration.AppCode, user.UserID, registration.DeviceID) if err != nil { return authdomain.Token{}, err } } account := authdomain.PasswordAccount{ AppCode: registration.AppCode, UserID: user.UserID, DisplayUserID: displayUserID, PasswordHash: passwordHash, HashAlg: hashAlgBcrypt, CreatedAtMs: user.CreatedAtMs, UpdatedAtMs: user.UpdatedAtMs, } inviteBind := invitedomain.BindCommand{ AppCode: registration.AppCode, InvitedUserID: user.UserID, InvitedRegionID: registration.RegionID, InviteCode: registration.InviteCode, Source: "quick_account_creation", RequestID: meta.RequestID, BoundAtMs: user.CreatedAtMs, } err = s.authRepository.CreatePasswordUser(ctx, user, displayIdentity, account, session, inviteBind, maxAccountsPerDevice) if err == nil { if isGameRobot { // 全站机器人账号是后台运营资产,不是 App 登录主体;创建成功后不给 access/refresh token, // 也不写登录成功审计和 IP 风控任务,避免后台批量造号污染登录链路与新增用户统计。 return authdomain.Token{ AppCode: user.AppCode, UserID: user.UserID, DisplayUserID: user.CurrentDisplayUserID, DefaultDisplayUserID: user.DefaultDisplayUserID, DisplayUserIDKind: string(user.CurrentDisplayUserIDKind), ProfileCompleted: user.ProfileCompleted, OnboardingStatus: string(user.OnboardingStatus), TokenType: tokenType, }, nil } s.audit(ctx, meta, user.UserID, loginPassword, "", resultSuccess, "") token, tokenErr := s.issueToken(user, session.SessionID, refreshToken, session.ExpiresAtMs) if tokenErr != nil { return authdomain.Token{}, tokenErr } s.importIMAccountAfterCommit(ctx, user, meta) s.enqueueLoginIPRiskJob(ctx, meta, token, "account", registration.Platform, registration.Language, registration.Timezone) return token, nil } if xerr.IsCode(err, xerr.DisplayUserIDExists) { // 默认短号冲突可重试,避免少量并发建号把快捷入口打成失败。 lastErr = err continue } s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, err } if lastErr != nil { s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.DisplayUserIDAllocateFailed)) } return authdomain.Token{}, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed") } func (s *Service) ensureDeviceCanCreatePasswordAccount(ctx context.Context, registration authdomain.ThirdPartyRegistration, meta Meta) (int32, error) { config, err := s.GetRegisterRiskConfig(ctx) if err != nil { s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.CodeOf(err))) return 0, err } displayUserIDs, err := s.authRepository.ListDisplayUserIDsByRegisterDeviceID(ctx, registration.AppCode, registration.DeviceID) if err != nil { // 快捷建号和三方注册共享设备注册上限;读取失败不能放行,否则会突破账号数量约束。 s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.CodeOf(err))) return 0, err } count := len(displayUserIDs) if count < int(config.MaxAccountsPerDevice) { return config.MaxAccountsPerDevice, nil } s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.DeviceAlreadyRegistered)) return 0, xerr.New(xerr.DeviceAlreadyRegistered, xerr.DeviceAlreadyRegisteredMessage(count, displayUserIDs)) }