2026-04-25 13:21:39 +08:00

468 lines
17 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 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
}