refactor(service): 统一时间戳解析,支持多种格式

新增 Account.GetCredentialAsTime 方法,统一处理凭证中的时间戳字段,
兼容 RFC3339 字符串、Unix 时间戳字符串和数字类型。

- 重构 Claude/Gemini/Antigravity TokenRefresher.NeedsRefresh
- 移除重复的 parseExpiresAt/parseAntigravityExpiresAt 函数
- 简化 GetOpenAITokenExpiresAt 实现
- 新增 RFC3339 格式单元测试用例
This commit is contained in:
shaw
2025-12-31 16:25:45 +08:00
parent aac7dd6b08
commit 81213f2324
9 changed files with 55 additions and 81 deletions

View File

@@ -110,6 +110,28 @@ func (a *Account) GetCredential(key string) string {
}
}
// GetCredentialAsTime 解析凭证中的时间戳字段,支持多种格式
// 兼容以下格式:
// - RFC3339 字符串: "2025-01-01T00:00:00Z"
// - Unix 时间戳字符串: "1735689600"
// - Unix 时间戳数字: 1735689600 (float64/int64/json.Number)
func (a *Account) GetCredentialAsTime(key string) *time.Time {
s := a.GetCredential(key)
if s == "" {
return nil
}
// 尝试 RFC3339 格式
if t, err := time.Parse(time.RFC3339, s); err == nil {
return &t
}
// 尝试 Unix 时间戳(纯数字字符串)
if ts, err := strconv.ParseInt(s, 10, 64); err == nil {
t := time.Unix(ts, 0)
return &t
}
return nil
}
func (a *Account) GetModelMapping() map[string]string {
if a.Credentials == nil {
return nil
@@ -324,19 +346,7 @@ func (a *Account) GetOpenAITokenExpiresAt() *time.Time {
if !a.IsOpenAIOAuth() {
return nil
}
expiresAtStr := a.GetCredential("expires_at")
if expiresAtStr == "" {
return nil
}
t, err := time.Parse(time.RFC3339, expiresAtStr)
if err != nil {
if v, ok := a.Credentials["expires_at"].(float64); ok {
tt := time.Unix(int64(v), 0)
return &tt
}
return nil
}
return &t
return a.GetCredentialAsTime("expires_at")
}
func (a *Account) IsOpenAITokenExpired() bool {

View File

@@ -12,7 +12,6 @@ import (
"log"
"net/http"
"regexp"
"strconv"
"strings"
"time"
@@ -187,9 +186,8 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
// Check if token needs refresh
needRefresh := false
if expiresAtStr := account.GetCredential("expires_at"); expiresAtStr != "" {
expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64)
if err == nil && time.Now().Unix()+300 > expiresAt {
if expiresAt := account.GetCredentialAsTime("expires_at"); expiresAt != nil {
if time.Now().Add(5 * time.Minute).After(*expiresAt) {
needRefresh = true
}
}

View File

@@ -191,7 +191,7 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc
// isTokenExpired 检查 token 是否过期
func (r *AntigravityQuotaRefresher) isTokenExpired(account *Account) bool {
expiresAt := parseAntigravityExpiresAt(account)
expiresAt := account.GetCredentialAsTime("expires_at")
if expiresAt == nil {
return false
}

View File

@@ -55,7 +55,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
}
// 2. 如果即将过期则刷新
expiresAt := parseAntigravityExpiresAt(account)
expiresAt := account.GetCredentialAsTime("expires_at")
needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= antigravityTokenRefreshSkew
if needsRefresh && p.tokenCache != nil {
locked, err := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
@@ -72,7 +72,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
if err == nil && fresh != nil {
account = fresh
}
expiresAt = parseAntigravityExpiresAt(account)
expiresAt = account.GetCredentialAsTime("expires_at")
if expiresAt == nil || time.Until(*expiresAt) <= antigravityTokenRefreshSkew {
if p.antigravityOAuthService == nil {
return "", errors.New("antigravity oauth service not configured")
@@ -91,7 +91,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
if updateErr := p.accountRepo.Update(ctx, account); updateErr != nil {
log.Printf("[AntigravityTokenProvider] Failed to update account credentials: %v", updateErr)
}
expiresAt = parseAntigravityExpiresAt(account)
expiresAt = account.GetCredentialAsTime("expires_at")
}
}
}
@@ -128,18 +128,3 @@ func antigravityTokenCacheKey(account *Account) string {
}
return "ag:account:" + strconv.FormatInt(account.ID, 10)
}
func parseAntigravityExpiresAt(account *Account) *time.Time {
raw := strings.TrimSpace(account.GetCredential("expires_at"))
if raw == "" {
return nil
}
if unixSec, err := strconv.ParseInt(raw, 10, 64); err == nil && unixSec > 0 {
t := time.Unix(unixSec, 0)
return &t
}
if t, err := time.Parse(time.RFC3339, raw); err == nil {
return &t
}
return nil
}

View File

@@ -2,7 +2,6 @@ package service
import (
"context"
"strconv"
"time"
)
@@ -34,16 +33,11 @@ func (r *AntigravityTokenRefresher) NeedsRefresh(account *Account, _ time.Durati
if !r.CanRefresh(account) {
return false
}
expiresAtStr := account.GetCredential("expires_at")
if expiresAtStr == "" {
expiresAt := account.GetCredentialAsTime("expires_at")
if expiresAt == nil {
return false
}
expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64)
if err != nil {
return false
}
expiryTime := time.Unix(expiresAt, 0)
return time.Until(expiryTime) < antigravityRefreshWindow
return time.Until(*expiresAt) < antigravityRefreshWindow
}
// Refresh 执行 token 刷新

View File

@@ -50,7 +50,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
// 2) Refresh if needed (pre-expiry skew).
expiresAt := parseExpiresAt(account)
expiresAt := account.GetCredentialAsTime("expires_at")
needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew
if needsRefresh && p.tokenCache != nil {
locked, err := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
@@ -66,7 +66,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
if err == nil && fresh != nil {
account = fresh
}
expiresAt = parseExpiresAt(account)
expiresAt = account.GetCredentialAsTime("expires_at")
if expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew {
if p.geminiOAuthService == nil {
return "", errors.New("gemini oauth service not configured")
@@ -83,7 +83,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
account.Credentials = newCredentials
_ = p.accountRepo.Update(ctx, account)
expiresAt = parseExpiresAt(account)
expiresAt = account.GetCredentialAsTime("expires_at")
}
}
}
@@ -154,18 +154,3 @@ func geminiTokenCacheKey(account *Account) string {
}
return "account:" + strconv.FormatInt(account.ID, 10)
}
func parseExpiresAt(account *Account) *time.Time {
raw := strings.TrimSpace(account.GetCredential("expires_at"))
if raw == "" {
return nil
}
if unixSec, err := strconv.ParseInt(raw, 10, 64); err == nil && unixSec > 0 {
t := time.Unix(unixSec, 0)
return &t
}
if t, err := time.Parse(time.RFC3339, raw); err == nil {
return &t
}
return nil
}

View File

@@ -2,7 +2,6 @@ package service
import (
"context"
"strconv"
"time"
)
@@ -22,16 +21,11 @@ func (r *GeminiTokenRefresher) NeedsRefresh(account *Account, refreshWindow time
if !r.CanRefresh(account) {
return false
}
expiresAtStr := account.GetCredential("expires_at")
if expiresAtStr == "" {
expiresAt := account.GetCredentialAsTime("expires_at")
if expiresAt == nil {
return false
}
expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64)
if err != nil {
return false
}
expiryTime := time.Unix(expiresAt, 0)
return time.Until(expiryTime) < refreshWindow
return time.Until(*expiresAt) < refreshWindow
}
func (r *GeminiTokenRefresher) Refresh(ctx context.Context, account *Account) (map[string]any, error) {

View File

@@ -43,17 +43,11 @@ func (r *ClaudeTokenRefresher) CanRefresh(account *Account) bool {
// NeedsRefresh 检查token是否需要刷新
// 基于 expires_at 字段判断是否在刷新窗口内
func (r *ClaudeTokenRefresher) NeedsRefresh(account *Account, refreshWindow time.Duration) bool {
s := account.GetCredential("expires_at")
if s == "" {
expiresAt := account.GetCredentialAsTime("expires_at")
if expiresAt == nil {
return false
}
expiresAt, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return false
}
return time.Until(time.Unix(expiresAt, 0)) < refreshWindow
return time.Until(*expiresAt) < refreshWindow
}
// Refresh 执行token刷新

View File

@@ -33,6 +33,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) {
},
wantRefresh: true,
},
{
name: "expires_at as RFC3339 - expired",
credentials: map[string]any{
"expires_at": "1970-01-01T00:00:00Z", // RFC3339 格式,已过期
},
wantRefresh: true,
},
{
name: "expires_at as string - far future",
credentials: map[string]any{
@@ -47,6 +54,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) {
},
wantRefresh: false,
},
{
name: "expires_at as RFC3339 - far future",
credentials: map[string]any{
"expires_at": "2099-12-31T23:59:59Z", // RFC3339 格式,远未来
},
wantRefresh: false,
},
{
name: "expires_at missing",
credentials: map[string]any{},