租户库共库分表实现

This commit is contained in:
richjohnrambo
2026-09-23 09:36:17 +08:00
parent 137d6fb6da
commit 7c1fab5065
4 changed files with 131 additions and 27 deletions
+58 -6
View File
@@ -4294,7 +4294,7 @@ type verificationCodeCache struct {
Action string `json:"action,omitempty"`
PhoneHash string `json:"phone_hash"`
PhoneMask string `json:"phone_mask,omitempty"`
TargetUser types.Uid `json:"target_user,omitempty"`
TargetUser string `json:"target_user,omitempty"`
CodeHash string `json:"code_hash"`
VerificationTokenHash *string `json:"verification_token_hash,omitempty"`
State types.VerificationState `json:"state"`
@@ -4320,7 +4320,7 @@ func verificationCodeCacheFromType(vc *types.VerificationCode) verificationCodeC
Action: vc.Action,
PhoneHash: vc.PhoneHash,
PhoneMask: vc.PhoneMask,
TargetUser: vc.TargetUser,
TargetUser: vc.TargetUser.UserId(),
CodeHash: vc.CodeHash,
VerificationTokenHash: vc.VerificationTokenHash,
State: vc.State,
@@ -4344,7 +4344,7 @@ func (vc verificationCodeCache) toType() *types.VerificationCode {
Action: vc.Action,
PhoneHash: vc.PhoneHash,
PhoneMask: vc.PhoneMask,
TargetUser: vc.TargetUser,
TargetUser: types.ParseUserId(vc.TargetUser),
CodeHash: vc.CodeHash,
VerificationTokenHash: vc.VerificationTokenHash,
State: vc.State,
@@ -4405,6 +4405,58 @@ type UserSecurityPersistenceInterface interface {
type userSecurityMapper struct{}
type actionTokenCache struct {
ID int64 `json:"id,omitempty"`
TenantID types.TenantID `json:"tenant_id"`
UserID string `json:"user_id"`
TokenID string `json:"token_id"`
Action string `json:"action"`
Target string `json:"target,omitempty"`
TokenHash string `json:"token_hash"`
State int16 `json:"state"`
ExpiresAt time.Time `json:"expires_at"`
ConsumedAt *time.Time `json:"consumed_at,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
func actionTokenCacheFromType(token *types.ActionToken) actionTokenCache {
if token == nil {
return actionTokenCache{}
}
return actionTokenCache{
ID: token.ID,
TenantID: token.TenantID,
UserID: token.UserID.UserId(),
TokenID: token.TokenID,
Action: token.Action,
Target: token.Target,
TokenHash: token.TokenHash,
State: token.State,
ExpiresAt: token.ExpiresAt,
ConsumedAt: token.ConsumedAt,
CreatedAt: token.CreatedAt,
UpdatedAt: token.UpdatedAt,
}
}
func (token actionTokenCache) toType() *types.ActionToken {
return &types.ActionToken{
ID: token.ID,
TenantID: token.TenantID,
UserID: types.ParseUserId(token.UserID),
TokenID: token.TokenID,
Action: token.Action,
Target: token.Target,
TokenHash: token.TokenHash,
State: token.State,
ExpiresAt: token.ExpiresAt,
ConsumedAt: token.ConsumedAt,
CreatedAt: token.CreatedAt,
UpdatedAt: token.UpdatedAt,
}
}
// Security is a singleton for user security metadata.
var Security UserSecurityPersistenceInterface
@@ -4461,7 +4513,7 @@ func (userSecurityMapper) CreateActionToken(tenantID types.TenantID, token *type
now := types.TimeNow()
token.CreatedAt = now
token.UpdatedAt = now
return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), token)
return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), actionTokenCacheFromType(token))
}
func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error) {
@@ -4476,7 +4528,7 @@ func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.
return nil, types.ErrMalformed
}
cacheID := actionTokenCacheID(uid, tokenID, action, target)
var cached types.ActionToken
var cached actionTokenCache
ok, err := GetCachedObject(tenantID, cacheNamespaceActionToken, cacheID, &cached)
if err != nil {
return nil, err
@@ -4490,7 +4542,7 @@ func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.
if err = DeleteCachedObject(tenantID, cacheNamespaceActionToken, cacheID); err != nil {
return nil, err
}
return &cached, nil
return cached.toType(), nil
}
func (userSecurityMapper) GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error) {
+50 -1
View File
@@ -55,6 +55,7 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
Action: "signup",
PhoneHash: "phone-hash",
PhoneMask: "+861****000",
TargetUser: types.ParseUserId("usrR25C_ZgHzyg"),
CodeHash: "code-hash",
VerificationTokenHash: &tokenHash,
State: types.VerificationStateVerified,
@@ -81,6 +82,7 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
}
if !strings.Contains(string(cachePayload), `"code_hash":"code-hash"`) ||
!strings.Contains(string(cachePayload), `"phone_hash":"phone-hash"`) ||
!strings.Contains(string(cachePayload), `"target_user":"usrR25C_ZgHzyg"`) ||
!strings.Contains(string(cachePayload), `"max_attempts":5`) {
t.Fatalf("cache verification JSON missing internal fields: %s", cachePayload)
}
@@ -91,8 +93,55 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
}
got := cached.toType()
if got.CodeHash != vc.CodeHash || got.PhoneHash != vc.PhoneHash ||
got.State != vc.State || got.Attempts != vc.Attempts || got.MaxAttempts != vc.MaxAttempts ||
got.TargetUser != vc.TargetUser || got.State != vc.State || got.Attempts != vc.Attempts || got.MaxAttempts != vc.MaxAttempts ||
got.VerificationTokenHash == nil || *got.VerificationTokenHash != tokenHash {
t.Fatalf("cache round trip lost fields: got %#v", got)
}
}
func TestActionTokenCachePreservesInternalFields(t *testing.T) {
now := time.Now().UTC().Round(time.Millisecond)
consumedAt := now.Add(time.Minute)
token := &types.ActionToken{
ID: 9,
TenantID: 7,
UserID: types.ParseUserId("usrR25C_ZgHzyg"),
TokenID: "token-id",
Action: "close_youth_mode",
Target: "youth_mode",
TokenHash: "token-hash",
State: 1,
ExpiresAt: now.Add(5 * time.Minute),
ConsumedAt: &consumedAt,
CreatedAt: now,
UpdatedAt: consumedAt,
}
publicPayload, err := json.Marshal(token)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(publicPayload), "token-hash") || strings.Contains(string(publicPayload), "token-id") {
t.Fatalf("public action token JSON leaked internal fields: %s", publicPayload)
}
cachePayload, err := json.Marshal(actionTokenCacheFromType(token))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(cachePayload), `"token_hash":"token-hash"`) ||
!strings.Contains(string(cachePayload), `"token_id":"token-id"`) ||
!strings.Contains(string(cachePayload), `"state":1`) {
t.Fatalf("cache action token JSON missing internal fields: %s", cachePayload)
}
var cached actionTokenCache
if err := json.Unmarshal(cachePayload, &cached); err != nil {
t.Fatal(err)
}
got := cached.toType()
if got.TokenHash != token.TokenHash || got.TokenID != token.TokenID ||
got.UserID != token.UserID || got.State != token.State || got.Action != token.Action {
t.Fatalf("cache round trip lost fields: got %#v", got)
}
}