package auth import ( "context" "database/sql" "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" invitestorage "hyapp/services/user-service/internal/storage/mysql/invite" "hyapp/services/user-service/internal/storage/mysql/shared" userstorage "hyapp/services/user-service/internal/storage/mysql/user" ) func (r *Repository) FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error) { var identity authdomain.ThirdPartyIdentity err := r.db.QueryRowContext(ctx, ` SELECT app_code, user_id, provider, provider_subject, COALESCE(provider_union_id, ''), created_at_ms, updated_at_ms FROM third_party_identities WHERE app_code = ? AND provider = ? AND provider_subject = ? `, appcode.FromContext(ctx), provider, providerSubject).Scan(&identity.AppCode, &identity.UserID, &identity.Provider, &identity.ProviderSubject, &identity.ProviderUnionID, &identity.CreatedAtMs, &identity.UpdatedAtMs) if err == sql.ErrNoRows { // 未绑定三方身份时由 service 转入注册路径。 return authdomain.ThirdPartyIdentity{}, xerr.New(xerr.NotFound, "third-party identity not found") } if err != nil { return authdomain.ThirdPartyIdentity{}, err } return identity, nil } func (r *Repository) FindUserIDByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error) { var userID int64 err := r.db.QueryRowContext(ctx, ` SELECT user_id FROM users WHERE app_code = ? AND register_device_id = ? LIMIT 1 `, appcode.Normalize(appCode), deviceID).Scan(&userID) if err == sql.ErrNoRows { // 未查到占用者时,service 才允许新三方身份走注册事务。 return 0, xerr.New(xerr.NotFound, "registered device not found") } if err != nil { return 0, err } return userID, nil } func (r *Repository) CountUsersByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error) { var count int64 if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM users WHERE app_code = ? AND register_device_id = ? `, appcode.Normalize(appCode), deviceID).Scan(&count); err != nil { return 0, err } return count, nil } func (r *Repository) ListDisplayUserIDsByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) ([]string, error) { rows, err := r.db.QueryContext(ctx, ` SELECT current_display_user_id FROM users WHERE app_code = ? AND register_device_id = ? ORDER BY user_id ASC `, appcode.Normalize(appCode), deviceID) if err != nil { return nil, err } defer rows.Close() displayUserIDs := make([]string, 0) for rows.Next() { var displayUserID string if err := rows.Scan(&displayUserID); err != nil { return nil, err } displayUserIDs = append(displayUserIDs, displayUserID) } if err := rows.Err(); err != nil { return nil, err } return displayUserIDs, nil } // CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。 func (r *Repository) CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, inviteBind invitedomain.BindCommand, maxAccountsPerDevice int32) error { // 三方首次登录注册必须同时写用户、默认短号、三方绑定和首个 session。 tx, err := r.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if err := ensureRegisterDeviceQuotaTx(ctx, tx, user.AppCode, user.RegisterDeviceID, maxAccountsPerDevice); err != nil { // 事务内再次校验设备上限,避免两个并发注册都在事务外读到旧计数后同时插入。 return err } if err := userstorage.InsertUserIdentity(ctx, tx, user, identity); err != nil { // 默认短号冲突由 service 重试。 return mapAuthDuplicateError(err) } if _, err := invitestorage.EnsurePrimaryCodeForUser(ctx, tx, user.AppCode, user.UserID, user.CreatedAtMs); err != nil { return err } if _, err := invitestorage.BindRelation(ctx, tx, inviteBind); err != nil { return err } _, err = tx.ExecContext(ctx, ` INSERT INTO third_party_identities (app_code, user_id, provider, provider_subject, provider_union_id, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?) `, appcode.Normalize(thirdParty.AppCode), thirdParty.UserID, thirdParty.Provider, thirdParty.ProviderSubject, shared.NullableString(thirdParty.ProviderUnionID), thirdParty.CreatedAtMs, thirdParty.UpdatedAtMs) if err != nil { return mapAuthDuplicateError(err) } if err := insertSession(ctx, tx, session); err != nil { // session 插入失败会回滚用户和三方绑定。 return err } if err := userstorage.InsertUserRegisteredOutbox(ctx, tx, user); err != nil { // 完整注册入口会传入 profile_completed=true;旧三方登录仍是 pending 用户,这里会按用户状态自动跳过。 return err } return tx.Commit() } // CompleteThirdPartyRegistration 原子补齐旧三方 pending 用户资料并写入可续期 session。 // 它只服务 provider subject 已绑定但尚未 completed 的兼容路径,避免 Flutter 完成资料时再创建第二个用户。 func (r *Repository) CompleteThirdPartyRegistration(ctx context.Context, command userdomain.CompleteOnboardingCommand, session authdomain.Session) (userdomain.User, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.User{}, err } defer tx.Rollback() user, err := userstorage.QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID) if err == sql.ErrNoRows { return userdomain.User{}, xerr.New(xerr.NotFound, "user not found") } if err != nil { return userdomain.User{}, err } if !user.CanLogin() { // pending 用户可能已被后台禁用;注册完成入口不能绕过用户状态限制创建新 session。 return userdomain.User{}, xerr.New(xerr.UserDisabled, "user is disabled") } if user.ProfileCompleted { // 幂等兼容:已完成用户只补 session,不重写用户资料,避免重复提交覆盖用户后续修改。 if err := insertSession(ctx, tx, session); err != nil { return userdomain.User{}, err } if err := tx.Commit(); err != nil { return userdomain.User{}, err } return userstorage.New(r.db).GetUser(ctx, command.UserID) } inviteSnapshot := command.InviteCode if command.InviteCode != "" { binding, err := invitestorage.BindRelation(ctx, tx, invitedomain.BindCommand{ AppCode: command.AppCode, InvitedUserID: command.UserID, InvitedRegionID: command.RegionID, InviteCode: command.InviteCode, Source: "third_party_register", RequestID: command.RequestID, BoundAtMs: command.CompletedAtMs, }) if err != nil { return userdomain.User{}, err } if binding.InviteCode != "" { inviteSnapshot = binding.InviteCode user.InviteBinding = binding } } _, err = tx.ExecContext(ctx, ` UPDATE users SET username = ?, avatar = ?, gender = ?, country = ?, region_id = ?, invite_code = CASE WHEN ? = '' THEN invite_code ELSE ? END, profile_completed = ?, profile_completed_at_ms = ?, onboarding_status = ?, updated_at_ms = ? WHERE user_id = ? AND app_code = ? `, command.Username, command.Avatar, command.Gender, command.Country, shared.NullableRegionID(command.RegionID), inviteSnapshot, inviteSnapshot, true, command.CompletedAtMs, string(userdomain.OnboardingStatusCompleted), command.CompletedAtMs, command.UserID, appcode.Normalize(command.AppCode)) if err != nil { return userdomain.User{}, err } user.Username = command.Username user.Avatar = command.Avatar user.Gender = command.Gender user.Country = command.Country user.RegionID = command.RegionID if inviteSnapshot != "" { user.InviteCode = inviteSnapshot } user.ProfileCompleted = true user.ProfileCompletedAtMs = command.CompletedAtMs user.OnboardingStatus = userdomain.OnboardingStatusCompleted user.UpdatedAtMs = command.CompletedAtMs if err := insertSession(ctx, tx, session); err != nil { // session 和资料完成必须同事务提交,否则 Flutter 会拿不到可落 AppAuthSession 的 refresh token。 return userdomain.User{}, err } if err := userstorage.InsertUserRegisteredOutbox(ctx, tx, user); err != nil { // UserRegistered 是统计和注册完成事实,必须和首次资料完成一起提交。 return userdomain.User{}, err } if err := tx.Commit(); err != nil { return userdomain.User{}, err } updated, err := userstorage.New(r.db).GetUser(ctx, command.UserID) if err != nil { return userdomain.User{}, err } updated.InviteBinding = user.InviteBinding return updated, nil } // RecordLoginAudit 写登录审计;调用方不会让审计失败影响主链路。