package memory import ( "context" "sync" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" userdomain "hyapp/services/user-service/internal/domain/user" ) // Repository 是 user-service 的测试用内存存储。 // 它不作为生产路径,只用于 service 和 transport 层单元测试。 type Repository struct { mu sync.RWMutex users map[int64]userdomain.User activeDisplayIndex map[string]int64 liveDisplayIndex map[string]int64 lastChangeAtMs map[int64]int64 passwordAccounts map[int64]authdomain.PasswordAccount sessions map[string]authdomain.Session sessionByRefresh map[string]string thirdParty map[string]authdomain.ThirdPartyIdentity leases map[string]userdomain.PrettyDisplayUserIDLease activeLeaseByUser map[int64]string activeLeaseByPretty map[string]string audits []authdomain.LoginAudit } // NewRepository 创建空内存仓库。 func NewRepository() *Repository { return &Repository{ users: make(map[int64]userdomain.User), activeDisplayIndex: make(map[string]int64), liveDisplayIndex: make(map[string]int64), lastChangeAtMs: make(map[int64]int64), passwordAccounts: make(map[int64]authdomain.PasswordAccount), sessions: make(map[string]authdomain.Session), sessionByRefresh: make(map[string]string), thirdParty: make(map[string]authdomain.ThirdPartyIdentity), leases: make(map[string]userdomain.PrettyDisplayUserIDLease), activeLeaseByUser: make(map[int64]string), activeLeaseByPretty: make(map[string]string), } } // PutUser 写入用户主记录,测试可以用它准备数据。 func (r *Repository) PutUser(user userdomain.User) { r.mu.Lock() defer r.mu.Unlock() user = user.NormalizeDisplayUserIDState() r.users[user.UserID] = user if user.DefaultDisplayUserID != "" { r.liveDisplayIndex[user.DefaultDisplayUserID] = user.UserID } if user.CurrentDisplayUserID != "" { r.activeDisplayIndex[user.CurrentDisplayUserID] = user.UserID r.liveDisplayIndex[user.CurrentDisplayUserID] = user.UserID } } // GetUser 查询单个用户。 func (r *Repository) GetUser(_ context.Context, userID int64) (userdomain.User, error) { r.mu.RLock() defer r.mu.RUnlock() user, ok := r.users[userID] if !ok { return userdomain.User{}, xerr.New(xerr.NotFound, "user not found") } return user.NormalizeDisplayUserIDState(), nil } // BatchGetUsers 查询多个用户,缺失用户不会出现在返回 map 中。 func (r *Repository) BatchGetUsers(_ context.Context, userIDs []int64) (map[int64]userdomain.User, error) { r.mu.RLock() defer r.mu.RUnlock() result := make(map[int64]userdomain.User, len(userIDs)) for _, userID := range userIDs { if user, ok := r.users[userID]; ok { result[userID] = user.NormalizeDisplayUserIDState() } } return result, nil } // CreateUserWithIdentity 在同一临界区写入用户和 active display_user_id。 func (r *Repository) CreateUserWithIdentity(_ context.Context, user userdomain.User, identity userdomain.Identity) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.users[user.UserID]; exists { return xerr.New(xerr.Conflict, "user_id already exists") } if _, exists := r.liveDisplayIndex[identity.DisplayUserID]; exists { return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } user = user.NormalizeDisplayUserIDState() user.DefaultDisplayUserID = identity.DisplayUserID user.CurrentDisplayUserID = identity.DisplayUserID user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindDefault r.users[user.UserID] = user r.activeDisplayIndex[identity.DisplayUserID] = user.UserID r.liveDisplayIndex[identity.DisplayUserID] = user.UserID return nil } // GetUserIdentity 按 user_id 返回当前 active display_user_id。 func (r *Repository) GetUserIdentity(_ context.Context, userID int64) (userdomain.Identity, error) { r.mu.RLock() defer r.mu.RUnlock() user, ok := r.users[userID] if !ok { return userdomain.Identity{}, xerr.New(xerr.NotFound, "user not found") } user = user.NormalizeDisplayUserIDState() if user.CurrentDisplayUserID == "" { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDNotFound, "display_user_id not found") } return user.EffectiveIdentity(), nil } // ResolveDisplayUserID 只解析 active 短号归属。 func (r *Repository) ResolveDisplayUserID(_ context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error) { r.mu.Lock() defer r.mu.Unlock() r.expireAllPrettyLocked(nowMs) userID, ok := r.activeDisplayIndex[displayUserID] if !ok { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDNotFound, "display_user_id not found") } user := r.users[userID].NormalizeDisplayUserIDState() return user.EffectiveIdentity(), nil } // ChangeDisplayUserID 原子维护旧短号释放、新短号激活和用户快照。 func (r *Repository) ChangeDisplayUserID(_ context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) { r.mu.Lock() defer r.mu.Unlock() user, ok := r.users[command.UserID] if !ok { return userdomain.Identity{}, xerr.New(xerr.NotFound, "user not found") } user = user.NormalizeDisplayUserIDState() if user.DisplayUserIDExpired(command.ChangedAtMs) { if _, err := r.expirePrettyLocked(command.UserID, "", command.ChangedAtMs, command.RequestID); err != nil { return userdomain.Identity{}, err } user = r.users[command.UserID].NormalizeDisplayUserIDState() } if user.CurrentDisplayUserIDKind == userdomain.DisplayUserIDKindPretty { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDPrettyActive, "pretty display_user_id is active") } if user.DefaultDisplayUserID == command.NewDisplayUserID { return user.EffectiveIdentity(), nil } if ownerID, exists := r.liveDisplayIndex[command.NewDisplayUserID]; exists && ownerID != command.UserID { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } if lastChangedAt := r.lastChangeAtMs[command.UserID]; lastChangedAt > 0 && command.CooldownMs > 0 && command.ChangedAtMs-lastChangedAt < command.CooldownMs { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDCooldown, "display_user_id change is cooling down") } delete(r.activeDisplayIndex, user.CurrentDisplayUserID) delete(r.liveDisplayIndex, user.DefaultDisplayUserID) r.activeDisplayIndex[command.NewDisplayUserID] = command.UserID r.liveDisplayIndex[command.NewDisplayUserID] = command.UserID user.DefaultDisplayUserID = command.NewDisplayUserID user.CurrentDisplayUserID = command.NewDisplayUserID user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindDefault user.CurrentDisplayUserIDExpiresAtMs = 0 user.UpdatedAtMs = command.ChangedAtMs r.users[command.UserID] = user r.lastChangeAtMs[command.UserID] = command.ChangedAtMs return user.EffectiveIdentity(), nil } // ApplyPrettyDisplayUserID 原子创建靓号租约,并把默认短号从 active 变为 held。 func (r *Repository) ApplyPrettyDisplayUserID(_ context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) { r.mu.Lock() defer r.mu.Unlock() user, ok := r.users[command.UserID] if !ok { return userdomain.Identity{}, "", xerr.New(xerr.NotFound, "user not found") } user = user.NormalizeDisplayUserIDState() if user.DisplayUserIDExpired(command.NowMs) { if _, err := r.expirePrettyLocked(command.UserID, "", command.NowMs, command.RequestID); err != nil { return userdomain.Identity{}, "", err } user = r.users[command.UserID].NormalizeDisplayUserIDState() } if user.CurrentDisplayUserIDKind == userdomain.DisplayUserIDKindPretty { return userdomain.Identity{}, "", xerr.New(xerr.DisplayUserIDPrettyActive, "pretty display_user_id is active") } if _, exists := r.liveDisplayIndex[command.PrettyDisplayID]; exists { return userdomain.Identity{}, "", xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display_user_id is not available") } expiresAtMs := command.NowMs + command.LeaseDurationMs lease := userdomain.PrettyDisplayUserIDLease{ LeaseID: command.LeaseID, DisplayUserID: command.PrettyDisplayID, UserID: command.UserID, Status: userdomain.PrettyLeaseStatusActive, StartsAtMs: command.NowMs, ExpiresAtMs: expiresAtMs, PaymentReceiptID: command.PaymentReceiptID, Source: command.Source, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, } delete(r.activeDisplayIndex, user.CurrentDisplayUserID) r.activeDisplayIndex[command.PrettyDisplayID] = command.UserID r.liveDisplayIndex[command.PrettyDisplayID] = command.UserID r.leases[command.LeaseID] = lease r.activeLeaseByUser[command.UserID] = command.LeaseID r.activeLeaseByPretty[command.PrettyDisplayID] = command.LeaseID user.CurrentDisplayUserID = command.PrettyDisplayID user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindPretty user.CurrentDisplayUserIDExpiresAtMs = expiresAtMs user.UpdatedAtMs = command.NowMs r.users[command.UserID] = user return user.EffectiveIdentity(), command.LeaseID, nil } // ExpirePrettyDisplayUserID 恢复默认短号;重复过期返回当前默认身份,保证补偿任务可重试。 func (r *Repository) ExpirePrettyDisplayUserID(_ context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error) { r.mu.Lock() defer r.mu.Unlock() return r.expirePrettyLocked(command.UserID, command.LeaseID, command.NowMs, command.RequestID) } // SetPassword 为已有用户首次写入密码身份。 func (r *Repository) SetPassword(_ context.Context, account authdomain.PasswordAccount) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.users[account.UserID]; !exists { return xerr.New(xerr.NotFound, "user not found") } if _, exists := r.passwordAccounts[account.UserID]; exists { return xerr.New(xerr.PasswordAlreadySet, "password already set") } r.passwordAccounts[account.UserID] = account return nil } // FindPasswordByDisplayUserID 先解析当前 active display_user_id,再读取用户密码身份。 func (r *Repository) FindPasswordByDisplayUserID(_ context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error) { r.mu.Lock() defer r.mu.Unlock() r.expireAllPrettyLocked(nowMs) userID, exists := r.activeDisplayIndex[displayUserID] if !exists { return authdomain.PasswordAccount{}, xerr.New(xerr.NotFound, "password account not found") } account, exists := r.passwordAccounts[userID] if !exists { return authdomain.PasswordAccount{}, xerr.New(xerr.NotFound, "password account not found") } account.DisplayUserID = displayUserID return account, nil } // CreateSession 创建新的 refresh session。 func (r *Repository) CreateSession(_ context.Context, session authdomain.Session) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.sessionByRefresh[session.RefreshTokenHash]; exists { return xerr.New(xerr.Conflict, "refresh token hash already exists") } r.sessions[session.SessionID] = session r.sessionByRefresh[session.RefreshTokenHash] = session.SessionID return nil } // FindActiveSessionByRefreshHash 查找未过期且未吊销的 session。 func (r *Repository) FindActiveSessionByRefreshHash(_ context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error) { r.mu.RLock() defer r.mu.RUnlock() sessionID, exists := r.sessionByRefresh[refreshTokenHash] if !exists { return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked") } session := r.sessions[sessionID] if session.RevokedAtMs > 0 { return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked") } if session.ExpiresAtMs <= nowMs { return authdomain.Session{}, xerr.New(xerr.SessionExpired, "session expired") } return session, nil } // ReplaceSession 原子吊销旧 session 并创建新 session。 func (r *Repository) ReplaceSession(_ context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error { r.mu.Lock() defer r.mu.Unlock() oldSession, exists := r.sessions[oldSessionID] if !exists || oldSession.RevokedAtMs > 0 { return xerr.New(xerr.SessionRevoked, "session revoked") } oldSession.RevokedAtMs = revokedAtMs oldSession.UpdatedAtMs = revokedAtMs r.sessions[oldSessionID] = oldSession r.sessions[newSession.SessionID] = newSession r.sessionByRefresh[newSession.RefreshTokenHash] = newSession.SessionID return nil } // RevokeSession 按 session_id 或 refresh token hash 吊销 session。 func (r *Repository) RevokeSession(_ context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error) { r.mu.Lock() defer r.mu.Unlock() if sessionID == "" { sessionID = r.sessionByRefresh[refreshTokenHash] } session, exists := r.sessions[sessionID] if !exists { return false, xerr.New(xerr.SessionRevoked, "session revoked") } if session.RevokedAtMs > 0 { return true, nil } session.RevokedAtMs = revokedAtMs session.UpdatedAtMs = revokedAtMs r.sessions[sessionID] = session return true, nil } // FindThirdPartyIdentity 查找 provider + subject 绑定。 func (r *Repository) FindThirdPartyIdentity(_ context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error) { r.mu.RLock() defer r.mu.RUnlock() identity, exists := r.thirdParty[thirdPartyKey(provider, providerSubject)] if !exists { return authdomain.ThirdPartyIdentity{}, xerr.New(xerr.NotFound, "third-party identity not found") } return identity, nil } // CreateThirdPartyUser 原子创建用户、默认短号、三方绑定和首个 session。 func (r *Repository) CreateThirdPartyUser(_ context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session) error { r.mu.Lock() defer r.mu.Unlock() key := thirdPartyKey(thirdParty.Provider, thirdParty.ProviderSubject) if _, exists := r.thirdParty[key]; exists { return xerr.New(xerr.Conflict, "third-party identity already exists") } if _, exists := r.liveDisplayIndex[identity.DisplayUserID]; exists { return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } if _, exists := r.users[user.UserID]; exists { return xerr.New(xerr.Conflict, "user_id already exists") } user = user.NormalizeDisplayUserIDState() user.DefaultDisplayUserID = identity.DisplayUserID user.CurrentDisplayUserID = identity.DisplayUserID user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindDefault r.users[user.UserID] = user r.activeDisplayIndex[identity.DisplayUserID] = user.UserID r.liveDisplayIndex[identity.DisplayUserID] = user.UserID r.thirdParty[key] = thirdParty r.sessions[session.SessionID] = session r.sessionByRefresh[session.RefreshTokenHash] = session.SessionID return nil } // RecordLoginAudit 记录登录审计,内存实现仅用于测试断言。 func (r *Repository) RecordLoginAudit(_ context.Context, audit authdomain.LoginAudit) error { r.mu.Lock() defer r.mu.Unlock() r.audits = append(r.audits, audit) return nil } func thirdPartyKey(provider string, providerSubject string) string { return provider + ":" + providerSubject } func (r *Repository) expireAllPrettyLocked(nowMs int64) { for userID, user := range r.users { user = user.NormalizeDisplayUserIDState() if user.DisplayUserIDExpired(nowMs) { _, _ = r.expirePrettyLocked(userID, "", nowMs, "lazy_expire") } } } func (r *Repository) expirePrettyLocked(userID int64, leaseID string, nowMs int64, _ string) (userdomain.Identity, error) { user, ok := r.users[userID] if !ok { return userdomain.Identity{}, xerr.New(xerr.NotFound, "user not found") } user = user.NormalizeDisplayUserIDState() if user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty { return user.EffectiveIdentity(), nil } if user.CurrentDisplayUserIDExpiresAtMs > nowMs { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not expired") } activeLeaseID := r.activeLeaseByUser[userID] if leaseID != "" && activeLeaseID != "" && activeLeaseID != leaseID { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not active") } if activeLeaseID != "" { lease := r.leases[activeLeaseID] lease.Status = userdomain.PrettyLeaseStatusExpired lease.UpdatedAtMs = nowMs r.leases[activeLeaseID] = lease delete(r.activeLeaseByPretty, lease.DisplayUserID) delete(r.activeLeaseByUser, userID) } delete(r.activeDisplayIndex, user.CurrentDisplayUserID) delete(r.liveDisplayIndex, user.CurrentDisplayUserID) r.activeDisplayIndex[user.DefaultDisplayUserID] = userID r.liveDisplayIndex[user.DefaultDisplayUserID] = userID user.CurrentDisplayUserID = user.DefaultDisplayUserID user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindDefault user.CurrentDisplayUserIDExpiresAtMs = 0 user.UpdatedAtMs = nowMs r.users[userID] = user return user.EffectiveIdentity(), nil }