2026-06-26 10:15:48 +08:00

239 lines
8.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"
"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 写登录审计;调用方不会让审计失败影响主链路。