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