165 lines
6.7 KiB
Go
165 lines
6.7 KiB
Go
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))
|
||
}
|