fix: 修复批量更新凭证明细与缓存TTL抖动

- BatchUpdateCredentials 返回 success/failed/results 及 success_ids/failed_ids

- billing jitteredTTL 改为只减不增,确保TTL不超上界

- crypto/rand 失败时随机ID降级避免 panic

- OpenAI SelectAccount 失败日志去重并补充字段

- 修复两处类型断言以通过 errcheck
This commit is contained in:
yangjianbo
2026-02-07 21:18:03 +08:00
parent bc3ca5f068
commit 4a20a2a8ba
6 changed files with 61 additions and 18 deletions

View File

@@ -810,20 +810,38 @@ func (h *AccountHandler) BatchUpdateCredentials(c *gin.Context) {
updates = append(updates, accountUpdate{ID: accountID, Credentials: account.Credentials})
}
// 阶段二:依次更新,任何失败立即返回(避免部分成功部分失败)
// 阶段二:依次更新,返回每个账号的成功/失败明细,便于调用方重试
success := 0
failed := 0
successIDs := make([]int64, 0, len(updates))
failedIDs := make([]int64, 0, len(updates))
results := make([]gin.H, 0, len(updates))
for _, u := range updates {
updateInput := &service.UpdateAccountInput{
Credentials: u.Credentials,
}
updateInput := &service.UpdateAccountInput{Credentials: u.Credentials}
if _, err := h.adminService.UpdateAccount(ctx, u.ID, updateInput); err != nil {
response.Error(c, 500, fmt.Sprintf("Failed to update account %d: %v", u.ID, err))
return
failed++
failedIDs = append(failedIDs, u.ID)
results = append(results, gin.H{
"account_id": u.ID,
"success": false,
"error": err.Error(),
})
continue
}
success++
successIDs = append(successIDs, u.ID)
results = append(results, gin.H{
"account_id": u.ID,
"success": true,
})
}
response.Success(c, gin.H{
"success": len(updates),
"failed": 0,
"success": success,
"failed": failed,
"success_ids": successIDs,
"failed_ids": failedIDs,
"results": results,
})
}

View File

@@ -219,9 +219,8 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
log.Printf("[OpenAI Handler] Selecting account: groupID=%v model=%s", apiKey.GroupID, reqModel)
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionHash, reqModel, failedAccountIDs)
if err != nil {
log.Printf("[OpenAI Handler] SelectAccount failed: %v", err)
log.Printf("[OpenAI Handler] SelectAccount failed: groupID=%v model=%s tried=%d err=%v", apiKey.GroupID, reqModel, len(failedAccountIDs), err)
if len(failedAccountIDs) == 0 {
log.Printf("[OpenAI Gateway] SelectAccount failed: %v", err)
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "Service temporarily unavailable", streamStarted)
return
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log"
"strings"
"time"
)
// TransformGeminiToClaude 将 Gemini 响应转换为 Claude 格式(非流式)
@@ -345,13 +346,25 @@ func buildGroundingText(grounding *GeminiGroundingMetadata) string {
// generateRandomID 生成密码学安全的随机 ID
func generateRandomID() string {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, 12)
id := make([]byte, 12)
randBytes := make([]byte, 12)
if _, err := rand.Read(randBytes); err != nil {
panic("crypto/rand unavailable: " + err.Error())
// 避免在请求路径里 panic极端情况下熵源不可用时降级为伪随机。
// 这里主要用于生成响应/工具调用的临时 ID安全要求不高但需尽量避免碰撞。
seed := uint64(time.Now().UnixNano())
if err != nil {
seed ^= uint64(len(err.Error())) << 32
}
for i := range id {
seed ^= seed << 13
seed ^= seed >> 7
seed ^= seed << 17
id[i] = chars[int(seed)%len(chars)]
}
return string(id)
}
for i, b := range randBytes {
result[i] = chars[int(b)%len(chars)]
id[i] = chars[int(b)%len(chars)]
}
return string(result)
return string(id)
}

View File

@@ -22,8 +22,12 @@ const (
// jitteredTTL 返回带随机抖动的 TTL防止缓存雪崩
func jitteredTTL() time.Duration {
jitter := time.Duration(rand.Int63n(int64(2*billingCacheJitter))) - billingCacheJitter
return billingCacheTTL + jitter
// 只做“减法抖动”,确保实际 TTL 不会超过 billingCacheTTL避免上界预期被打破
if billingCacheJitter <= 0 {
return billingCacheTTL
}
jitter := time.Duration(rand.Int63n(int64(billingCacheJitter)))
return billingCacheTTL - jitter
}
// billingBalanceKey generates the Redis key for user balance cache.

View File

@@ -13,7 +13,12 @@ var sseScannerBuf64KPool = sync.Pool{
}
func getSSEScannerBuf64K() *sseScannerBuf64K {
return sseScannerBuf64KPool.Get().(*sseScannerBuf64K)
v := sseScannerBuf64KPool.Get()
buf, ok := v.(*sseScannerBuf64K)
if !ok || buf == nil {
return new(sseScannerBuf64K)
}
return buf
}
func putSSEScannerBuf64K(buf *sseScannerBuf64K) {

View File

@@ -483,7 +483,11 @@ func (s *SubscriptionService) GetActiveSubscription(ctx context.Context, userID,
return nil, err
}
// singleflight 返回的也是缓存指针,需要浅拷贝
cp := *value.(*UserSubscription)
sub, ok := value.(*UserSubscription)
if !ok || sub == nil {
return nil, ErrSubscriptionNotFound
}
cp := *sub
return &cp, nil
}