2026-07-06 11:10:19 +08:00

165 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
}